文章目录
一、sizeof和strlen的对比
自学习C语言以来,我们已经见过并使用了sizeof和strlen,在进行下面的指针练习前,我们先来细致地对比一下它们。
1. sizeof
sizeof 是C语言中的一个操作符,是用来计算一个变量或一种数据类型所占空间大小的,单位是字节(字节数的占位符是%zd),用法是sizeof(变量名)
或sizeof 变量名
或sizeof(数据类型)
。sizeof 只关注占用内存空间的大小,不在乎内存中存放数据的具体内容。举个栗子:
int a = 6;
printf("%zd\n",sizeof(a));
printf("%zd\n",sizeof a);
printf("%zd\n",sizeof(int));
结果都是4。
2. strlen
sizeof是一个操作符,而strlen是一个库函数,包含在头文件string.h中。它的功能是求字符串长度
如上,它的定义语法是size_t strlen( const char* str );
,它能统计指针参数str指向的字符开始向后,到 \0 之前的字符个数,返回类型是size_t(无符号整数,占位符是%zd)。strlen函数会一直向后找 \0 字符,直到找到为止,所以可能存在越界查找。比如:
char a1[3] = {'a','b','c'};
char a2[] = "abc";
char a3[3] = "abc";
printf("%zd\n",strlen(a1));
printf("%zd\n",strlen(a2));
printf("%zd\n",strlen(a3));
在字符数组中:
-
如果写成字符串的形式,如a2,会在编译时在这个字符串结尾自动加上一个\0,所以实际上a2有四个元素:a、b、c、\0。但前提是数组大小还能容纳得下一个\0,如果定义成char a3[3] = “abc”,就不会自动加\0了,导致strlen(a3)不断向后查找,造成越界查找,不找到\0不罢休。
-
如果写成单个字符的形式,如a1,就不会自动加\0,就会导致strlen(a1)不断向后查找,造成越界查找,不找到\0不罢休。
这表明在我的内存里,从a1的第一个字符’a’开始到找到\0结束,中间有35个字符;a2的abc后就是\0,打印的就是3;a3不能放得下\0了,从a3的第一个字符’a’开始到找到\0结束,中间有33个字符。
就是这样,很好理解吧。
二、经典笔试题练习:数组与指针
好了,我们用六篇文章初步学习完了指针,接下来的内容,就是指针的综合理解和运用了,思考下面的每一段代码的结果吧:
但我再提醒一个之前讲过的知识点:
数组名代表数组首元素的地址,但两种情况下除外:
- sizeof(数组名):sizeof()中放入数组名,这里的数组名代表整个数组,计算的是整个数组的所占字节大小
- &数组名:取地址操作符&后加上数组名,取出的是整个数组的地址。也就是说:arr是数组首元素地址,而&arr是整个数组的地址,注意区别!
练习1
int a[] = {1,2,3,4};
printf("%zd\n",sizeof(a));
printf("%zd\n",sizeof(a+0));
printf("%zd\n",sizeof(*a));
printf("%zd\n",sizeof(a+1));
printf("%zd\n",sizeof(a[1]));
printf("%zd\n",sizeof(&a));
printf("%zd\n",sizeof(*&a));
printf("%zd\n",sizeof(&a+1));
printf("%zd\n",sizeof(&a[0]));
printf("%zd\n",sizeof(&a[0]+1));
解析:
int a[] = {1,2,3,4};
printf("%zd\n",sizeof(a));
//a是整个数组,结果是16
printf("%zd\n",sizeof(a+0));
//a是首元素地址,a+0就是下标为0的元素的地址(还是首元素),是int*指针类型,在32位环境下结果是4,在64位环境下结果是8
printf("%zd\n",sizeof(*a));
//a是首元素地址,*a是首元素,结果是4
printf("%zd\n",sizeof(a+1));
//a是首元素地址,a+1是下标为1的元素的地址,是int*指针类型,在32位环境下结果是4,在64位环境下结果是8
printf("%zd\n",sizeof(a[1]));
//a[1]是下标为1的元素,结果是4
printf("%zd\n",sizeof(&a));
//a是整个数组,&a是数组地址,是int(*)[4]指针类型,在32位环境下结果是4,在64位环境下结果是8
printf("%zd\n",sizeof(*&a));
//a是整个数组,&a是数组地址,*&a是数组,结果是16
printf("%zd\n",sizeof(&a+1));
//a是整个数组,&a是数组地址,是int(*)[4]指针类型,&a+1指向&a往后16个字节的地址,但也是这个指针类型,在32位环境下结果是4,在64位环境下结果是8
printf("%zd\n",sizeof(&a[0]));
//a[0]是首元素,&a[0]是首元素地址,是int*指针类型,在32位环境下结果是4,在64位环境下结果是8
printf("%zd\n",sizeof(&a[0]+1));
//&a[0]是首元素地址,&a[0]+1指向首元素地址往后4个字节的地址,也就是下一个元素的地址,是int*指针类型,在32位环境下结果是4,在64位环境下结果是8
练习2
char a[] = "abcdef";
printf("%zd\n",strlen(a));
printf("%zd\n",strlen(a+0));
printf("%zd\n",strlen(*a));
printf("%zd\n",strlen(a[1]));
printf("%zd\n",strlen(&a));
printf("%zd\n",strlen(&a+1));
printf("%zd\n",strlen(&a[0]+1));
解析:
char a[] = "abcdef";//这个数组的元素实际上是:a b c d e f \0
printf("%zd\n",strlen(a));
//a是首元素地址,首元素到\0前有6个字符,结果是6
printf("%zd\n",strlen(a+0));
//a是首元素地址,a+0也是首元素地址,首元素到\0前有6个字符,结果是6
printf("%zd\n",strlen(*a));
//a是首元素地址,*a是首元素'a'——ASCII值为97,相当于strlen访问地址97,这是非法访问,程序会报错
printf("%zd\n",strlen(a[1]));
//a[1]是下标为1的元素'b'——ASCII值为98,相当于strlen访问地址98,这是非法访问,程序会报错
printf("%zd\n",strlen(&a));
//&a是整个数组的地址,类型是char(*)[7],当做strlen的参数程序会警告,但它指向的是数组的开头,往后找\0,结果还是6
printf("%zd\n",strlen(&a+1));
//&a+1跳过了整个数组的地址,指向了数组最后一个元素的下一个字节,是未知的内容,从&a+1开始何时再找到\0也是未知的,结果是随机值
printf("%zd\n",strlen(&a[0]+1));
//&a[0]是首元素地址,&a[0]+1是第二个元素的地址,从'b'开始往后找\0,结果是5
练习3
char a[] = {'a','b','c','d','e','f'};
printf("%zd\n",strlen(a));
printf("%zd\n",strlen(a+0));
printf("%zd\n",strlen(*a));
printf("%zd\n",strlen(a[1]));
printf("%zd\n",strlen(&a));
printf("%zd\n",strlen(&a+1));
printf("%zd\n",strlen(&a[0]+1));
解析:
char a[] = {'a','b','c','d','e','f'};
printf("%zd\n",strlen(a));
//a是首元素地址,在内存中从首元素往后何时找到\0是未知的,结果是随机值
printf("%zd\n",strlen(a+0));
//a是首元素地址,a+0还是首元素地址,在内存中从首元素往后何时找到\0是未知的,结果是随机值
printf("%zd\n",strlen(*a));
//a是首元素地址,*a是首元素'a'——ASCII值为97,相当于strlen访问地址97,这是非法访问,程序会报错
printf("%zd\n",strlen(a[1]));
//a[1]是下标为1的元素'b'——ASCII值为98,相当于strlen访问地址98,这是非法访问,程序会报错
printf("%zd\n",strlen(&a));
//&a是整个数组的地址,类型是char(*)[7],当做strlen的参数程序会警告,它指向的是数组的开头,往后何时找到\0是未知的,结果是随机值
printf("%zd\n",strlen(&a+1));
//&a+1跳过了整个数组的地址,指向了数组最后一个元素的下一个字节,是未知的内容,从&a+1开始何时再找到\0也是未知的,结果是随机值
printf("%zd\n",strlen(&a[0]+1));
//&a[0]是首元素地址,&a[0]+1是第二个元素的地址,从第二个元素开始往后何时再找到\0是未知的,结果是随机值
练习4
int a[3][4] = {0};
printf("%zd\n",sizeof(a));
printf("%zd\n",sizeof(a[0][0]));
printf("%zd\n",sizeof(a[0]));
printf("%zd\n",sizeof(a[3]));
printf("%zd\n",sizeof(a[0]+1));
printf("%zd\n",sizeof(*(a[0]+1)));
printf("%zd\n",sizeof(a+1));
printf("%zd\n",sizeof(*(a+1)));
printf("%zd\n",sizeof(&a[0]+1));
printf("%zd\n",sizeof(*(&a[0]+1)));
printf("%zd\n",sizeof(*a));
解析:
int a[3][4] = {0};
printf("%zd\n",sizeof(a));
//a是数组名,结果是整个数组的大小,是48
printf("%zd\n",sizeof(a[0][0]));
//a[0][0]是数组第一行第一列的元素,结果是4
printf("%zd\n",sizeof(a[0]));
//a[0]是第一行的数组名,结果是数组第一行的大小,是16
printf("%zd\n",sizeof(a[3]));
//a[3]是第四行的数组名,虽然这个数组没有第四行,但sizeof不会访问内存的具体数据内容,只是根据类型计算大小,类似于a[0]、a[1]、a[2],结果是16
printf("%zd\n",sizeof(a[0]+1));
//a[0]是第一行的数组名,也就是第一行的首元素地址,a[0]+1是第一行的第二个元素的地址,是int*指针类型,在32位环境下结果是4,在64位环境下结果是8
printf("%zd\n",sizeof(*(a[0]+1)));
//参考上一句,a[0]+1是第一行第二个元素的地址,*(a[0]+1)就是这个元素,即a[0][1],结果是4
printf("%zd\n",sizeof(a+1));
//a是数组第一行的地址,a+1是第二行的地址,是int(*)[4]指针类型,在32位环境下结果是4,在64位环境下结果是8
printf("%zd\n",sizeof(*(a+1)));
//参考上一句,a+1是第二行的地址,*(a+1)就是第二行,结果是16
printf("%zd\n",sizeof(&a[0]+1));
//&a[0]是第一行的地址,&a[0]+1是第二行的地址,是int(*)[4]指针类型,在32位环境下结果是4,在64位环境下结果是8
printf("%zd\n",sizeof(*(&a[0]+1)));
//参考上一句,&a[0]+1是第二行的地址,*(&a[0]+1)是第二行,结果是16
printf("%zd\n",sizeof(*a));
//a是数组第一行的地址,*a就是第一行,结果是16
三、经典笔试题练习:指针运算
练习1
int a[5] = {1,2,3,4,5};
int* p = (int*)(&a+1);
printf("%d %d\n",*(a+1),*(p-1));
解析:
结果是:2 5
练习2
//在X86环境下(32位环境)
long long int* p = 0x100000;
printf("%p\n", p+0x1);
printf("%p\n", (int)p+0x1);
printf("%p\n", (int*)p+0x1);
解析:
0x1是十六进制数,换算成十进制是1。
p是long long int* 类型,+1会跳过8个字节;
(int)p 是int类型,+1就是+1;
(int*)p 是int* 类型,+1会跳过4个字节。
地址用十六进制数表示,但十六进制数打印出来不会有0x,是00,所以结果是:
练习3
int a[3][3] = {(0,1),(2,3),(4,5)};
int*p = a[0];
printf("%d",p[0]);
解析:
这道题容易踩坑,注意看int a[3][3] = {(0,1),(2,3),(4,5)};
里面是( ),不是{ }。( )里面是逗号表达式,结果是最后一个表达式的值!即int a[3][3] = {1,3,5};
知道了这个就好分析了,a[0]是数组第一行的数组名,也就是p指向了数组第一行,p[0]就是第一行里下标为0的元素。结果是1
练习4
int a[5][5];
int(*p)[4];
p = a;
printf("%p\n",&p[4][2]-&a[4][2]);
printf("%d\n",&p[4][2]-&a[4][2]);
解析:
首先要明确的是,“指针-指针”计算结果的绝对值是这两个指针间的元素个数。二维数组中数据在内存中是连续存放的,也就是线性存储的。
&a[4][2]是数组第五行第三列的元素的地址,但&p[4][2]的理解就有些复杂:
这么一看,在内存中&p[4][2]处于低地址处,&a[4][2]处于高地址处,&p[4][2]-&a[4][2]的结果是负数,结果的绝对值是这两个地址中间的元素个数—— 4个。所以&p[4][2]-&a[4][2]的值就是-4。
在第二个printf里,占位符是%d,打印出的当然就是-4了。
而在第一个printf里,占位符是%p,要打印的是地址,也就是要把-4当做地址。地址都是十六进制数呀,所以需要把-4换算成十六进制数。在内存中存的是-4的补码:11111111 11111111 11111111 11111100,四个二进制位换算成一个十六进制位,1111(15)是F,1100(12)是C,最后结果就是FFFFFFFC了。
练习5
int a[2][5] = {1,2,3,4,5,6,7,8,9,10};
int* p1 = (int*)(&a+1);
int* p2 = (int*)(*(a+1));
printf("%d %d",*(p1-1),*(p2-1));
解析:
结果是5 10
练习6
char* a[] = {"mi","ho","yo"};
char** pa = a;
pa++;
printf("%s",*pa);
解析:
数组a有三个地址元素,分别指向"mi\0",“ho\0”,“yo\0”。pa存放的是数组首元素地址,pa++使pa指向数组a的第二个元素的地址,*pa是第二个元素,也就是指向"yo\0"的地址。printf中的%s就会从这个地址往后找\0并打印出其间的字符串,结果是ho
练习7(难)
char* c[] = {"ENTER","NEW","POINT","FIRST"};
char** cp[] = {c+3,c+2,c+1,c};
char*** cpp = cp;
printf("%s\n",**++cpp);
printf("%s\n",*--*++cpp+3);
printf("%s\n",*cpp[-2]+3);
printf("%s\n",cpp[-1][-1]+1);
解析:
这道题相对复杂,我们需要一步步分析:
- 理解代码前三句:
-
理解
printf("%s\n",**++cpp);
++cpp使cpp指向的内容是c+2,再**,就能找到c+2指向的内容,是数组c的第三个元素,%s打印出来,结果是POINT -
理解
printf("%s\n",*--*++cpp+3);
+的优先级低。先执行++cpp,cpp指向了cp数组第三个元素。*++cpp是该元素,即c+1。- -使该地址元素变成c,也就是数组c的首元素地址。再解引用就找到了数组c的首元素,是“ENTER\0”的首字符“E”的地址,最后的+3操作使这个字符指针指向了第二个“E”的地址,%s打印出来结果是ER
-
理解
printf("%s\n",*cpp[-2]+3);
cpp[-2]等价于*(cpp-2),也就是数组cp的首元素c+3。那么*cpp[-2]
就是c+3指向的内容,是数组c的第四个字符指针元素,它存放的是字符串“FIRST\0”的首字符“F”的地址,+3使这个指针指向字符“S”,%s打印出来结果是ST
-
理解
printf("%s\n",cpp[-1][-1]+1);
cpp[-1]等价于*(cpp-1),即c+2。cpp[-1][-1]等价于*(c+2-1)
,即*(c+1)
,是数组c的第二个字符指针元素,这个指针存放的是字符串“NEW\0”的首字符“N”的地址,+1使其指向了字符“E”,%s打印出来结果是EW
综上所述,最后的结果:
本篇完,感谢阅读~
《指针篇》堂堂完结!结束,也是新的起点。自此,指针就是我们学习中不可分割的一部分。