1.对于变量、指针和指针变量的关系,大家可以看看下面的图:
指针的概念:
指针与指针变量:
大家要注意:
a.普通变量存储数值,指针变量存储地址,即指针
b.指针变量的数据类型决定了其寻址范围的大小
c.通过变量来访问变量的存储空间叫做直接访问;先获取其地址,再根据地址找到存储单元叫做间接访问。
d.只要是指针变量,无论是什么类型,在32位的CPU下,都占4个字节。
大家看看下面这个例子:
很简单,对于指针变量,刚刚已经说得很明确,都是4个字节(32位CPU下);而对于其他的普通变量,该是什么类型,就是什么类型,所以,打印:
大家要知道,int *p中的*仅仅是告诉编译器p是一个指针变量用来存储地址的,如:int a = 10;那么有a = *( &a );指针变量的数据类型本质上就是其指向对象的类型。
我们操作指针的时候要注意:
定义什么样的指针变量(保存什么样的变量地址)由一次操作的几个内存单元决定
要怎样获取合法的地址
怎样去读、写
2.在嵌入式开发中,对于ARM裸板操作,经常用到强制类型转换,例如:
# define GPFCON *( (volatile unsigned *)0x56000050 )
这个例子是将一个地址与GPFCON绑定,实现操作GPFCON就是操作0x56000050这个地址,我们一起来看看,这是怎样实现的!
首先,0x56000050是一个具体的十六进制数值,(volatile unsigned *)0x56000050,这条语句是将这个具体的数值转化成了一个无符号的地址,不过这个地址不能被编译器优化,最后,在前面还有一个星号*,表示解指针,说明取的是内容,那么这句话就可以这样理解了:在内存中有一块内存,地址(逻辑地址)是0x56000050,里面的内容是空的,然后,我们用GPFCON来给它定义一下,这样,操作GPFCON就是操作逻辑地址0x56000050了。
还是上面那个例子,我们来稍稍改动一下:
我们一起来分析一下这个打印结果:
大家先要了解CPU的存储模式,即字节序,也就是大家常说的大端小端模式,记住八个字:
小端模式:高高低低;大端模式:高低低高
小端模式下是高位存储在高地址,低位存储在低地址;大段模式下高位存储在低地址,低位存储在高地址。
而我们一般的英特尔CPU则是小端模式,至于为什么,以后会有解释。就是符合上面的高高低低原则。所以对于0x1234,34是低位,12是高位,所以存储的顺序是34 12,这有两个字节,而char则是一个字节的,所以截取了前面的高字节34,所以打印0x34。而刚刚说了的指针变量在32位的cpu下,都是4个字节,所以,都可以打印出来,对于上面的加一操作,只要记住指针变量的加一是加的该指针变量内存单元的长度即可。
3.在用指针操作内存单元的时候,我们会经常发现一个很纠结的错误,那就是段错误:segment fault,是由于虚拟内存管理单元的异常所致,而该异常则通常是由于解引用一个未初始化或非法值的指针引起的。在gcc下,我们可以用GDB进行调试,调试的步骤如下:
执行命令:
$ ulimit –c //查看有没有限制,如果结果为0,则有限制
$ ulimit –c unlimited //去掉限制
$ ulimit –c //再次查看,结果应该为unlimited
$ gcc –g xxx.c // xxx.c表示c源文件,声称调试文件
$ ls // ls查看有没有生成调试用的core文件
$ ./a.out //执行可执行文件,会报段错误
$ gdb a.out core //用GDB调试,在调试命令中输入r,然后回车两次即可看到在哪儿发生了段错误。
在我们写代码的时候,经常会有人将“NULL”和“NUL”混淆,一个“L”的NULL用于结束一个ASCII字符码,两个“L”的NULL用于表示什么也指向,即空指针。
4.大家要区分好*p, *p++,(*p)++, *++p, ++*p这几个的不同:
*p++先取指针p指向的值,再将指针p自增1;
(*p)++先去指针p指向的值,再将该值自增1;
*++p先将指针p自增1(此时指向数组第二个元素),*操作再取出该值
++*p先取指针p指向的值,再将该值自增1
大家在用程序进行测试的时候,记得使用不同的指针变量,还是上次说的,指针进行自增或者自减运算,其地址都会改变。很简单,这里就不举例子了!
5.总结一下,我们在操作指针的时候要注意的问题:
a.指针变量存的是地址
b.指针变量的数据类型决定其寻址范围
c.指针变量的算术运算,加减常数指的是元素个数
d.注意指针的指向,一般初始化为NULL
e.定义什么样的指针变量由一次操作几个内存单元决定
f.指针变量相互赋值要类型匹配
g.注意指针的当前值。即++或者- -之后,地址会发生相应的改变
h.连续的地址相减的值为元素的个数
6.大家看看这个程序:
没有什么实际作用,仅仅是为了测试所用。打印:
由结果可以得出下面的结论:
a + i <=> &a[i];
a[i] <=> *(a + i) <=> *(p + i) <=> p[ i];
注意,这只是针对于一维数组。至于二维数组,待会儿会专门讲解。
7.大家对于二维数组,是怎么理解的?看看下面的图:
二维数组的理解:
二维数组元素的引用:
对于二维数组a[3][4],其内部的存储结构图有:
我们要记住二维数组和指针的关系:
*行指针=列指针;&列指针=行指针
例如,对于上面的a[3][4]数组,如果我们想要访问a[2][2],则我们可以访问以下的地址,它们都是等效的:
&a[2][2],a[2]+2,a[0]+2*4+2,(int *)(a+2)+2,*(&a[0]+2)+2
我们也可以直接使用下面的值,也是等效的:
a[2][2],*(a+2)+2,*a+2*4+2
在二维数组a[3][4]中,若有int *p1 = a[0], *p2 = a[1], *p3 = a[2];我们可以用一个数组表示:int *p[3] = {a[0], a[1], a[2]};这种结构就是传说中的指针数组,大家这样理解:指针数组,重点是数组,只是里面的元素是指针罢了,可以这样看:int* p[3] = {a[0, a[1], a[2]]};每个元素都是指针变量,而对应的p则是一个二级指针常量。
当然,与之对应的还有一个数组指针:int (*p)[3];数组指针,实质上还是一个指针,只不过指向了一个数组,p是一个数组指针,指向整个数组。
其实类似的还有很多,什么数组指针数组等等。它们的重点都是在最后,其实都不是很难,只是数组或者指针的一种组合而已。
8.下面的一个代码中有很多陷阱,等着我们跳,你会掉坑里吗?
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
/*
程序首先申请一个char类型的指针str,并把str指向NULL(即str里存的是NULL的地址,*str为NULL中的值为0),调用函数的过程中做了如下动作:
1、申请一个char类型的指针p,
2、把str的内容copy(传)到了p里(这是参数传递过程中系统所做的),
3、为p指针申请了100个空间,
4、返回Test函数.最后程序把字符串hello world拷贝到str指向的内存空间里.到这里错误出现了!str 的空间始终为 NULL 而并没有实际 的空间.深刻理解函数调用的第 2 步,将不难发现问题所在!(注意:传递 的参数和消除的参数)
*/
/*
void GetMem(char *p)
{
printf("p = str = %p\n",p);
p = (char *)malloc(100);
strcpy(p,"hello world\n");
printf(p);
printf("p = %p\n",p);
}
void Test(void)
{
char *str;
GetMem(str);
// str = (char *)malloc(100);
strcpy(str,"hello!\n");
printf("str = %p\n",str);
}
*/
/*
char *GetMem(void)
{
char p[] = "hello world\n";
// printf(p);
// printf("%p",p);
return p;
// free(p); //数组里面的内容已经被释放,
//只是返回一个地址,并没有内容
}
void Test()
{
char *str = NULL;
str = GetMem();
printf(str);
// printf("str = %p\n",str);
}
*/
/*
void GetMem(char **p, int n)
{
*p = (char *)malloc(n);
}
void Test(void) //内存泄漏,需要用free来释放内存空间
{
char *str = NULL;
GetMem(&str,100);
strcpy(str,"hello\n");
printf(str);
// free(str);
}
*/
void Test(void)
{
char *str = (char *)malloc(7);
strcpy(str,"hello\n");
free(str);
printf(str);
if(str != NULL)
{
strcpy(str,"world\n");
printf(str);
}
}
int main()
{
Test();
return 0;
}
/*
1、C中内存分为四个区
栈:用来存放函数的形参和函数内的局部变量。由编译器分配空间,在函数执行完后由编译器自动释放。
堆:用来存放由动态分配函数(如malloc)分配的空间。是由程序员自己手动分配的,并且必须由程序员使用free释放。如果忘记用free释放,会导致所分配的空间一直占着不放,导致内存泄露。堆,顺序随意。栈,后进先出(Last-In/First-Out)。
全局区(静态区):用来存放全局变量和静态变量。存在于程序的整个运行期间,是由编译器分配和释放的。
文字常量区:例如char *c =“123456”;则”123456”为文字常量,存放于文字常量区。也由编译器控制分配和释放。
程序代码区:用来存放程序的二进制代码。
2、内存分配方式
内存分配方式有三种:
1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整
个运行期间都存在。例如全局变量,static变量。
2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数
执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
3、常见的内存错误及其对策
发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。常见的内存错误及其对策如下:
1)内存分配未成功,却使用了它。
编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理。
2)内存分配虽然成功,但是尚未初始化就引用它。
犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。
3)内存分配成功并且已经初始化,但操作越过了内存的边界。
例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。
4)忘记了释放内存,造成内存泄露。
含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。
【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
【规则3】注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
【规则4】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
【规则5】动态内存的申请与释放必须配对,防止内存泄漏。
【规则6】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
4、动态分配释放内存举例
用malloc动态分配内存后一定要判断一下分配是否成功,判断指针的值是否为NULL。
内存分配成功后要对内存单元进行初始化。
内存分配成功且初始化后使用时别越界了。
内存使用完后要用free(p)释放,注意,释放后,p的值是不会变的,仍然是一个地址值,仍然指向那块内存区,只是这块内存区的值变成垃圾了。为了防止后面继续使用这块内存,应在free(p)后,立即p=NULL,这样后面如果要使用,判断p是否为NULL时就会判断出来。
*/