任务描述
有人说指针是C语言的灵魂,对于我们刚学过指针的声明和引用来说,似乎还体会不到指针为什么是C语言的灵魂,本任务通过接下来会对指针的进阶进行讲解,将从指针与数组的关系、指针与函数的关系进行讲解,对其能够灵活运用,在追寻C语言灵魂的道路上不觉得孤单。
学习目标
了解指针与整数的加减法。
掌握指针数组的使用。
了解指针与函数的关系。
掌握指针作为函数的参数。
掌握指向函数的指针的使用。
掌握malloc函数与free函数的使用。
相关知识
指针变量保存的是地址,而地址本质上是一个整数,地址加地址没有意义,但是指针与整数进行加减却有一定的意义。
指针加上或减去一个整数n,将相对于当前位置前移或后移n个存储单元,不是字节,对于 int 来讲一个单元是 4 个字节。
一个存储单元的长度(即占的字节数)取决于指针的类型,因此,p+(-)n所表示的实际内存地址(以字节编号)是:p+(-)n*sizeof(指针的数据类型)(单位:字节)
实例9- 5 指针与整数的加法运算。
#include
int main()
{
int a=23;
int* p=&a; //p 指向 a
printf("p指向a的地址%p \n",p); //输出 p 指向的变量
p=p+1; //p 加 1
printf("p+1运算之后的结果%p ",p);//输出p 的值
getchar();
return 0;
}
运行结果如图9-11所示
图9- 11 实例9-5的运行结果
指针的加减法是指针和普通整数运算,p=p+n 表示 p 向下指 n 个单元,p=p-1 表示 p 向上指 n 个单元,上述案例指针变化如下图9-12所示。
图9- 12 实例9-5指针指向说明
在许多 C 程序中,指针常被用于引用数组,或者作为数组的元素。指向数组的指针常被简称为数组指针,而具有指针类型元素的数组则被称为指针数组,这两个概念大家不要混淆。
定义指针数组格式如下:
数据类型 *指针数组名[大小];
例如: int *ptr[3];
说明: ptr 声明为一个指针数组,由 3 个整数指针组成,因此,ptr 中的每个元素,都是一个指向 int 值的指针。
接下来我们通过案例来带大家更深一步来了解指针数组,案例如下。
实例9- 6 指针数组定义。
#include
int main()
{
int arr[] = {10, 100, 200};
int i, *ptr[3];
for ( i = 0; i < 3; i++) {
/* 赋值为整数的地址 */
ptr[i] = &arr[i];
}
for ( i = 0; i < 3; i++) {
printf("数组的元素arr[%d] = %d\n", i, *ptr[i] );
}
getchar();
return 0;
}
运行结果如下图 9-13所示。
图9- 13 实例9-6运行结果
上述案例中我们通过遍历的方式把数组的元素的地址赋值给指针数组的与元素,然后再遍历出指针数组运算所对应的数值。
数组指针就是指向数组元素的指针变量,那么数组元素的地址是什么呢?前面我们已经讲过数组本质上是一片连续的内存空间,数组元素有独立的空间,数组就好像一列火车,数组元素是火车每个车厢。因此,每个数组元素也都有自己的内存空间地址,简称数组元素地址。
那么我们就可以使用指针变量来保存数组元素地址,下面来认识一下如何定义一个指向一维数组的指针,其语法格式如下
一维数组元素的数据类型 *指针变量名;
例如:
int a[10]={1,3,5,7,9,11,13,15,17,19};
int* p; //定义指向 int 变量的指针变量 p
pa=&a[0] //把a 数组第0 个元素地址赋给指针变量p,也可以认为指针变量 p 指 向数组a 第 0 个元素,
如图9-14所示:
图9- 14 指针指向数组的元素
在 C 语言中,数组名就是数组首元素地址。
例如:int a[10],a 与&a[0]完全等价,所以可以认为 a+i 等价于&a[i],a+i 指向 a[i]的地址,那么*(a+i)就是 a+i 所指向的数组元素 a[i]的值,因此,*(a+i)与 a[i]等价。
实例9- 7通过指针指向数组元素来遍历数组。
#include
int main()
{
int a[5]={12,231,13,14,35};
int i;
int len=sizeof(a)/sizeof(int);
printf("通过指针指向数组元素值来遍历数组\n");
for (i=0;i { printf("%d ",*(a+i)); } getchar(); return 0; } 运行结果如图 9-15所示。 图9- 15 实例9-7运行结果 上述案例中我们通过*(a+i)与 a[i]等价实现求遍历数组元素。 前面讲解了如何使用指针指向一维数组并访问数组中的元素,接下来针对指针指向二维数组元素的相关知识进行详细地讲解。 1.获取二维数组的地址 设有整型二维数组如下:int a[3][4]={{0,1,2,3},{4,5,6,7},{8,9,10,11}} 实例9- 8 通过&取值运算获取二维数组的地址值。 #include int main() { int a[3][4]={{0,1,2,3},{4,5,6,7},{8,9,10,11}}; int i=0; int j=0; for(i=0;i<3;i++){ for(j=0;j<4;j++){ printf("%p",&(a[i][j])); printf("\t"); } printf("\n"); } getchar(); return 0; } 获取的地址值如下图 9-16所示: 图9- 16 实例9-8运行结果 上述案例其实二位数组在内存中的分布是一维线性的,整个数组占用一块连续的内存,也就是先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行;每行中的 4 个元素也是依次存放。数组 a 为 int 类型,每个元素占用 4 个字节,整个数组共占用 4×(3×4) = 48 个字节。 2.指向二维数组的指针变量 接下来把二维数组a分解为一维数组a[0],a[1],a[2]之后,设p为指向二维数组的指针变量。可定义为: int (*p)[4]; 它表示p是一个指针变量,它指向包含4个元素的一维数组。若指向第一个一维数组a[0],其值等于a,a[0]或&a[0][0]如下图9-17所示。 图9- 17 a,a[0]或&a[0][0]的关系 而p+i则指向一维数组a[i],而*(p+i)是取a[i]的数据,也就是获取i行的数据。从前面的分析可得出*(p+i)+j是二维数组i行j 列的元素的地址,而*(*(p+i)+j)则是i行j列元素的值。 实例9- 9 利用指针遍历二维数组。 int main () { int a[3][4]={{0,1,2,3},{4,5,6,7},{8,9,10,11}}; int i=0; int j=0; int (*p)[4]=a; for(i=0;i<3;i++){ for(j=0;j<4;j++){ printf("%d",*(*(p+i)+j)); printf("\t"); } printf("\n"); } 运行结果如下图 9-18所示。 图9- 18 实例9-9运行结果 上述案例通过*(*(p+i)+j)实现遍历二维数组元素的遍历。 在学习函数的参数那一任务的时候,我们讲到了函数参数有两种方式,一个是值传递另外一种就是地址值传递,我们前面已经讲过了指针就是地址,那么指针作为函数参数的本质就是地址值传递。 那么指针作为参数有什么样的意义呢,一个典型的例子就是交换两个变量的值。 有些初学者可能会使用下面的方法来交换两个变量的值: 实例9- 10 定义普通形参函数实现交换两个两个变量。 #include void swap(int a, int b){ int temp; //临时变量 temp = a; a = b; b = temp; } int main(){ int a = 23,b = 45; printf("函数调用前:a = %d, b = %d\n", a, b); swap(a, b); printf("函数调用后:a = %d, b = %d\n", a, b); return 0; } 运行结果如图9-19所示: 图9- 19 实例9-10运行结果 上述案例,从结果可以看出,a、b 的值并没有发生改变,交换失败。这是因为 swap() 函数内部的 a、b 和 main() 函数内部的 a、b 是不同的变量,占用不同的内存,它们除了名字一样,没有其他任何关系,swap() 交换的是它内部 a、b 的值,不会影响它外部(main() 内部) a、b 的值。 改用指针变量作参数后就很容易解决上面的问题: 实例9- 11 定义指针形参函数实现交换两个两个变量。 #include void swap(int *p1, int *p2){ int temp = *p1; //表示将 n1 这个指针指向的变量的值赋给 temp *p1= *p2; // 表示将 n2 这个指针指向的变量的值赋给 n1这个指针指向的变量 *p2= temp; //表示将 temp 值赋给 n2这个指针指向的变量 } int main(){ int a =23,b =45; printf("函数调用前:a = %d, b = %d\n", a, b); swap(&a, &b); printf("函数调用后:a = %d, b = %d\n", a, b); return 0; } 运行结果如图9-20所示: 图9- 20 实例9-11运行结果 上述案例,调用 swap() 函数时,将变量 a、b 的地址分别赋值给 p1、p2,这样 *p1、*p2 代表的就是变量 a、b 本身,交换 *p1、*p2 的值也就是交换 a、b 的值。 需要注意的是临时变量 temp,它的作用特别重要,因为执行*p1 = *p2;语句后 a 的值会被 b 的值覆盖,如果不先将 a 的值保存起来以后就找不到了。 上述代码,执行在内存的情况如下图9-21所示。 图9- 21 实例9-11指针参数的内存状况 用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。 1.函数的指针 前面一已经学过指针指针变量也学了指针指向数组,实际上指针还可以指向函数,当指针指向一个函数的时候,这个指针就叫做函数指针。 一个函数会占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。 把函数的这个首地址赋予一个指针变量,使指针变量指向函数 所在的内存区域,然后通过指针变量就可以找到并调用该函数。 2.函数指针的定义 定义函数指针与声明函数类似,不同的是需要将函数名替换为“(* 函数指针名)”,并将函数中的参数名去掉。 函数指针定义格式如下: 函数返回值类型 (* 函数指针名) (函数参数列表); 注意( )的优先级高于*,所以第一个括号不能省略 3.函数指针的使用 为了更好的理解指针指向函数的使用,我们一起来写个案例: 实例9- 12 定义函数指针。 # include int max(int, int);//函数声明 int main() { int(*p)(int, int);//定义一个函数指针 int a, b, c; //把函数max赋给指针变量p, 使p指向Max函数 p = max; printf("输入a和b的值:"); scanf("%d%d", &a, &b); c = (*p)(a, b); //通过函数指针调用Max函数 printf("a = %d\nb = %d\nmax = %d\n", a, b, c); return 0; } int max(int x, int y) //定义Max函数 { return x > y?x:y; } 输出结果是:如图9-22所示 图9- 22 实例9-12运行结果 上述案例中我们定义了函数指针指向了max函数,是实现了求出最大值的结果。 在学习数组的时候,我们知道引用字符串的方式却有两种,字符数组方式例如:char name1[]="hello";,另外一种char*类型的变量进行引用方式,例如 char* name2="hello";,用我们现在的知识来理解的话就是指针指向字符串。 但是这两种方式是有区别的,请看示例代码。 实例9- 13 打印引用字符串的地址值。 #include int main() { char ch1[]="happy1"; char * ch2="happy2"; printf("ch1的内存地址值%p\n",ch1); printf("ch2的内存地址值%p",ch2); getchar(); return 0; } 运行结果如下图9-23所示 图9- 23 实例9-13运行结果 上述案例中明显ch1和ch2的地址举例很远,那是因为他们在内存不同的区域。那它们分别在内存什么区域里呢,带着这个问题我们先来了解以下C程序在内区的五大区域。五大区域分别为:栈区Stack,堆区Heap,BSS区,数据区(常量区)Data和代码区Text。接下来我们简单的介绍一下这五大区域都存什么数据。存放的数据如下图9-23所示: 图9- 24 内存的五大区域 说明:堆区Heap:是由程序员自行向系统申请的一块存储空间,需要程序员自行释放,后面我们会来学怎么动态分配内存。 回到我们的代码,当两个字符都是局部变量的时候,ch1字符数组是存在栈区的,字符串里面的字符存在字符数组的每一个元素中,如图9-24所示。 图9- 25 ch1字符数组内存分配 ch2也是声明在栈区的,字符串数据是以字符数组形式存在常量区的,ch2指向存储在常量区的"happy2",如图9-25所示。 图9- 26 ch2字符数组内存分配 那么如果字符ch1和ch2全都是成员变量呢? 实例9- 14 打印引用字符串的地址值。 #include char ch1[]="happy1"; char * ch2="happy2"; int main() { printf("ch1的内存地址值%p\n",ch1); printf("ch2的内存地址值%p",ch2); getchar(); return 0; } 运行结果如下图9-27所示。 图9- 27 实例9-14运行结果 上述案例中明显ch1和ch2的地址距离明显近了,因为他们都是全局变量,所以都存在常量区,内存分配情况如下9-28所示。 图9- 28 实例9-14内存分配情况 说明:ch1字符数组是存储在常量区的,字符串的 每1个字符是存储在这个数组中的每一个元素中。ch2指针也是存储在常量区的。字符串也是以字符数组的形式存储在常量区, ch2指针中存储的是“happy2"这个字符串在常量区的地址。 综上所述,字符串引用的这两种方式在内存中存储的结构是不同的。 1.以字符数组存储:不论是局部变量或是全局变量都是一个字符数组。然后字符串的每一个字符存储在数组的元素之中。 2. 以字符指针存储:不论是局部变量或是全局变量都是都现有一个字符指针变量,字符串数据是以字符数组的形式存储在常量区的。 上一节我们介绍了内存中的五大区域,其中有一个区域是堆区,有些时候我们需要在内存主动申请内存来保存我们的数据,实现动态分配内存的效果,头文件 #include 1.malloc() 函数原型:void * malloc(size_t size) (1)作用:在内存的动态存储区(堆区)中分配一个长度为size的连续空间。 (2)若申请成功,返回指向这片内存空间的指针 ,若失败 ,则会返回NULL, 所以我们在用malloc()函数开辟动态内存之后,一定要判断函数返回值是否为NULL. (3)返回值的类型为void*型,malloc()函数并不知道连续开辟的size个字节是存储什么类型数据的 ,所以需要我们自行决定 ,方法是在malloc()前加强制转 ,转化成我们所需类型 ,例如: int *pNumber = (int*)malloc(100); // 分配100字节内存 int *pNumber = (int*)malloc(25*sizeof(int)); //存储25个int内存 (4)如果size为0,此行为是未定义的,会发生未知错误,取决于编译器 2.free( )函数 函数原型:void free(void* ptr) 在堆中申请的内存空间不会像在栈中存储的局部变量一样 ,函数调用完会自动释放内存 ,如果我们不手动释放,直到程序运行结束才会释放,这样就可能会造成内存泄漏,所以当我们申请的动态内存不再使用时 ,一定要及时释放。 (1)p是最近一次调用calloc或malloc函数时的函数返回值 。 (2)free函数无返回值,free()不能重复释放一块内存。 例如: free(pNumber ); // 释放p 所指向的已分配的动态空间 。 为了更深入的理解malloc函数我们来讲一个动态内存分配案例: 实例9- 15 动态创建数组,输入5个学生的成 绩,另外一个函数检测成绩低于60 分的,输出不合格的成绩。 #include #include int main() { void check(int *); int * p,i; p = (int *)malloc(5*sizeof(int)); for ( i = 0; i < 5; i++) { scanf("%d",p + i); } check(p); free(p); getchar(); return 0; } void check(int *p) { int i; printf("\n不及格的成绩 有: "); for (i =0; i < 5; i++) { if(p[i] < 60) { printf(" %d ", p[i]); } } } 运行结果 如图9-29所示 图9- 29 实例9-15运行结果9-2-4 指向二位数组的指针
9-2-5 指针作为函数参数
9-2-6 指向函数的指针
9-2-7 指针与字符串
9-2-8 动态分配内存