一、再谈指针大小
在【指针初阶】的一开始,我就有讲到过对于指针的大小在32为平台下均为4个字节,在64位平台下均为8个字节上面在学习了各种指针的进阶操作后,我们再来看看
代码:
- 首先给出接下去我要进行对比的代码
int Add(int x, int y) { return x + y; } int Sub(int x, int y) { return x - y; } int* Open(int n) { int* a = (int*)malloc(sizeof(int) * n); if (NULL == a) { perror("fail malloc"); exit(-1); } return a; } int main(void) { int a = 10; int* p = &a; int** pp = &p; double f = 3.14; double* ff = &f; double** fff = &ff; char ch = 'c'; const char* pc = &ch; char* const pc2 = &ch; int a1 = 1; int b1 = 2; int c1 = 3; int d1 = 4; int e1 = 5; int* parr[5] = { &a1, &b1, &c1, &d1, &e1 }; int b[5] = { 1,2,3,4,5 }; int(*pb)[5] = &b; int n = 10; int* arr = Open(n); int (*pf)(int, int) = Add; int (*pfArr[2])(int, int) = { Add, Sub }; int (*(*ppfArr)[2])(int, int) = &pfArr; printf("%d\n", sizeof(p)); printf("%d\n", sizeof(pp)); printf("%d\n", sizeof(ff)); printf("%d\n", sizeof(fff)); printf("%d\n", sizeof(pc)); printf("%d\n", sizeof(pc2)); printf("%d\n", sizeof(parr)); printf("%d\n", sizeof(pb)); printf("%d\n", sizeof(arr)); printf("%d\n", sizeof(pf)); printf("%d\n", sizeof(pfArr)); printf("%d\n", sizeof(ppfArr)); return 0; }
运行结果:
- x86环境下运行的结果如下
- x64环境下运行的结果如下
【总结一下】:
- 所以,一个指针的大小完全不是取决于它的类型,而是取决于平台,无论你是一级指针、二级指针、指针数组、数组指针等等,只要它在32位平台下,那么均为4个字节。因为在32位平台下,有32个地址总线,那么32位就可以表示【2^32^】的寻址范围,==即任何一个值都需要用32个1或0来表示==
- “指针需要多大空间,取决于地址的存储需要多大空间”,每一个数据的表示都是32位,1B又等于8b,因此每一块地址都需要【4B】的空间去容纳,又因为在内存中地址值得其实就是指针,这也就是为何在32位平台下👉==指针均为4个字节==👈
二、难题攻坚战🗡
接下去是我在日常学生的作业题里跳出来的一些难题,放在这里与读者一同讨论一番
第一题【指针运算】
下面关于指针运算说法正确的是:( C )
A.整形指针+1,向后偏移一个字节 B.指针-指针得到是指针和指针之间的字节个数 C.整形指针解引用操作访问4个字节 D.指针不能比较大小
解析:
注意:此题说法不明确,整型指针的类型不一定就是int*
,可能还有长整型、短整型
A. 错误,因为整型指针的类型为int*
,所以 + 1会向后偏移4个字节 B. 错误,两个指针相减,指针必须指向一段连续空间,减完之后的结构代表两个指针之间相差元素的个数C. 正确,整型指针指向的是一个整型的空间,解引用操作访问4个字节D. 错误,指针中存储的是地址,地址可以看成一个数据,因此是可以比较大小的
第二题【指针偏移】
下面代码的结果是:( B )
int main() { int arr[] = { 1,2,3,4,5 }; short* p = (short*)arr; int i = 0; for (i = 0; i < 4; i++) { *(p + i) = 0; } for (i = 0; i < 5; i++) { printf("%d ", arr[i]); } return 0; }
A.1 2 3 4 5 B.0 0 3 4 5 C.0 0 0 0 5 D.1 0 0 0 0
解析:
但就从代码来看,你可以在脑海中模拟一下试试最后的结果会是多少🤔
- 马上我们就来分析洗一下,首先看到一个arr数组,数组里面有5个元素,每个元素的类型都是
int
,然后取到arr的数组名,【数组名为首元素地址】,那么它的类型就是int*
,但是呢此时我将它的地址转换为short*
,即短整型指针,给到对应的指针变量p,接下去通过for循环内部指针的偏移来访问到数组中的内容,对数组的值去进行一个修改,那此时会有几个值发生变化呢?
for (i = 0; i < 4; i++) { *(p + i) = 0; }
- 在【指针初阶】我就有讲过对于指针的类型来说决定了一次可以访问多少个字节,那看到前面是
short
,对于短整型来说一次就可以访问两个字节的数据,又因为arr是一个整型数组,里面的每个元素在内存中所占的字节数都是4,那么这个for循环执行了4次后,就访问了8个字节的数据,即前两个数组元素被改成了【0】
真的是这样的吗?我们可以通过【内存】来看看
- 通过上面这四张图所对应的for循环四次执行过程,相信你一定明白了为什么访问四次只能改变两个数组元素,就在于
short*
类型的指针一次能访问的也就只有2个字节,访问4次是8个字节,那也刚好是2个数组元素的大小
运行结果:
第三题【指针访问字节数】
在小端机器中,下面代码输出的结果是:( C )
int main() { int a = 0x11223344; char *pc = (char*)&a; *pc = 0; printf("%x\n", a); return 0; }
A.00223344 B.0 C.11223300 D.112233
解析:
- 本题其实和第二题比较类似,变量a是一个十六进制的整数,
&a
取出它的地址后类型即为int*
,然后将其强转为char*
后令指针pc指向这块地址,但是指针pc却无法访问到变量a中所有的数据,因为char*
类型的指针解引用一次只能访问1个字节
*pc = 0;
一样,我们还是可以通过观察【内存】来看看*pc
究竟修改了多少内容
- 以下我是使用一行显示一个字节,这样可以方便观察修改的情况,因为VS是小端存放的,因此可以观察到原本的
11223344
放到内存中变成了44332211
- 可以看到,通过
*pc
我们访问到了变量a的第一个字节,并且将其修改为【0】
- 不过这个是在内存中的样子,此时若是要显示打印在屏幕上的话还要将其再做一个转换,所以最后显示的结果便是
11223300
第四题【指针数组】
下面哪个是指针数组:( A )
A.int* arr[10]; B.int* arr[]; C.int **arr; D.int (*arr)[10];
解析:
- 本题你可能会觉得很简单,完全没有比较讲,但是我在看同学们做下来的情况后,却发现这题也错得蛮多的,所以专门放在这里讲解一下
A. 这个没问题,是最标准的指针数组,arr和[]
先结合,表明它是一个数组,数组有10个元素,每个元素都是一个int*
类型的指针
B. 你可能会觉得它也是一个指针数组,但是放到VS中去编译一下是编不过的,报出了不允许使用不完整的类型的错误,如果你看不明白这一点的话,说明C语言数组不过关,可以再回去看看,若是在定义数组的时候,没有指定数组大小的话,就一定要为其进行初始化,也就是要给出数组具体的内容,否则编译器都不知道要分配多少空间给他
C. 这是一个二级指针,并不是指针数组
D. 对于int (*arr)[10]
来说,arr与*
相结合了,所以它是一个指针,什么指针呢?朝外一看有一个[]
,表明这个指针指向一个数组的地址,数组里面有10个元素,每个元素都是的类型都是int
。那很明显这就是一个【数组指针】
第五题【数组指针 + 函数指针】
声明一个指向含有10个元素的数组的指针,其中每个元素是一个函数指针,该函数的返回值是int,参数是int*,正确的是( C )
A.(int *p[10])(int*) B.int [10]*p(int *) C.int (*(*p)[10])(int *) D.int ((int *)[10])*p
解析:
A. 错误,()
加的地方不对,编译报错,应该是这样int(*p[10])(int*);
此时的p为一个数组,数组里面存放都是指针,而且均为函数指针,该函数指针指向的函数返回值是int
,参数是int*
。但是不符合题意,题面意思是p要为一个指针
B/D. []
只能在标识符右边,双双排除
C. p首先和*
结合,表明它是一个指针,指针朝外一看,它指向一个数组,数组有10个元素,去掉数组名后,可以看到每个元素的类型,为int(*)(int*)
,都是一个函数指针,并其他们都指向一个返回值是int
,参数是int*
的函数。即这是一个【指向函数指针数组的指针】,符合题目意思
三、指针和数组笔试题解析✒
本模块,我将通过
sizeof()
与strlen()
在指针与数组上的映射,来带你更加深入地理解它们在内存的分布
sizeof() 是操作符,不是函数,它是用来计算对象或者类型创建的对象所占内存空间的大小
1、简易一维数组
首先第一个先简单一点,来个一维数组练练手 (doge),==请你仅通过草稿纸验算的方式,计算出每个结果==
代码:
int main(void) { //一维数组 int a[] = { 1,2,3,4 }; printf("%d\n", sizeof(a)); printf("%d\n", sizeof(a + 0)); printf("%d\n", sizeof(*a)); printf("%d\n", sizeof(a + 1)); printf("%d\n", sizeof(a[1])); printf("%d\n", sizeof(&a)); printf("%d\n", sizeof(*&a)); printf("%d\n", sizeof(&a + 1)); printf("%d\n", sizeof(&a[0])); printf("%d\n", sizeof(&a[0] + 1)); return 0; }
解析:
算出来了嘛🤗,我们来一一分析一下
- 首先第一点你要知道的就是数组名即为首元素地址,不过有两个例外
sizeof(数组名)
—— 数组名表示整个数组,计算的是整个数组的大小,单位是字节&数组名
—— 数组名表示数组名表示整个数组,取出的是整个数组的地址,取出的是整个数组的地址
- 除了以上两点外直接出现数组名即为==首元素地址==
- 首先第一个,a作为数组名单独放在
sizeof
内部,此时计算的是数组的总大小,单位是【字节】,数组中有4个元素,每个元素的类型都是int,即4个字节,那结果就是 16
printf("%d\n", sizeof(a));
- 接下去第二个,此时a是并不是单独放在
sizeof
内部,而且也没有&
,所以数组名a指的就是首元素地址,==对于一个地址来说我们在指针初阶部分讲了在内存中就是指针==,那对于指针来说即为 4 / 8,在32位平台下运行就是4个字节,在在32位平台下运行就是8个字节
printf("%d\n", sizeof(a + 0));
- 然后第三个,通过观察可以发现,并没有出现
sizeof(数组名)
和&数组名
这两种形态,所以a就是首元素地址,类型是int*
那么*a
就是对其进行解引用,获取到的便是【首元素】,类型是int
,那一个整型的大小是多少呢?没错,就是 4个字节
printf("%d\n", sizeof(*a));
- 第四个,
a
指的是首元素地址,a + 1
向后偏移了一个整型,即为第二个元素的地址,那就和第二个一样计算的是一个地址的大小,即指针的大小,为 4 / 8
printf("%d\n", sizeof(a + 1));
- 第五个很简单,就是计算数组中第二个元素的大小,那很简单,就是 4个字节
printf("%d\n", sizeof(a[1]));
- 第六个
&a
即为&数组名
,取出是整个数组的地址,这个其实我在上面初讲指针的时候有提到过,整个数组的地址其实和数组的首元素的地址是一样的,那么整个数组的地址它也是一个地址,那只要是地址即为 4 / 8个字节
printf("%d\n", sizeof(&a));
- 通俗一些来说,其实地址就像是门牌号一样,那数组中每个元素的地址和整个数组的地址并没有高低贵贱之分,而是,不是说数组的地址就来得高大上一些,它们一视同仁
- 小插曲,我们再来看第七个,第一眼就看到
&a
,那么还是一样取出的是整个数组的地址,那对整个数组的地址进行解引用得到的便是整个数组,因为数组的地址是存到到数组指针中的,它的类型即为int (*)[5]
- 对一个整型指针解引用获取到的是一个整型
- 对一个字符型指针解引用获取到的是一个字符
- 对一个数组指针解引用获取到的是一个数组
- 那么此时计算的便是一个数组的大小,即为 16,其实你也可以这么去看,
&
是取到这个数组的地址,*
又对进行解引用,通过这个地址找到找到这里面所存放的内容,这么一来一去就产生了抵消,最后也就变成了sizeof(a)
,那便是我们上面说到过的,这种sizeof(数组名)
的形式,==计算的也是整个数组的大小==
printf("%d\n", sizeof(*&a));
- 我先说第九个:很明显,就是去计算数组首元素地址的大小,为 4 / 8
printf("%d\n", sizeof(&a[0]));
- 好,下面两个一起说,好做一个对比,
&a[0]
上面讲过了,是取出数组首元素的地址,它的类型是int*
,那对于一个整型指针来说,以此可以访问的字节数是4个字节,即数组中的一个元素,那么此时它就指向了2这个元素的地址处,它就等价于&a[1]
;对于&a
来说,取出的是整个数组的地址,其类型为int (*)[4]
,那么它一次性可以访问的字节数即为整个数组的所有元素之和,此时它就指向了4后面的这块地址
printf("%d\n", sizeof(&a[0] + 1)); printf("%d\n", sizeof(&a + 1));
- 可以看到,无论是指向哪里,它们都是一个地址,一个地址的大小就为 4 / 8字节
运行结果:
- 首先在32(x86)为平台下运行试试【指针大小为4个字节】
- 然后在64(x64)为平台下运行试试【指针大小为8个字节】
好,看完整型数组后,我们来看看字符数组
2、不带 '\0' 的字符数组
- 首先你要明确的一点就是这个数组里面有几个元素,在数组章节我就有着重讲到过,若是将一个字符数组定义成如下形式的话,末尾是不会带
\0
的,数组会根据初始化的内容来确定它里面的元素个数,所以下面这个数组的数组元素是6个而不是7个
代码:
int main(void) { //字符数组 char arr[] = { 'a','b','c','d','e','f' }; printf("%d\n", sizeof(arr)); printf("%d\n", sizeof(arr + 0)); printf("%d\n", sizeof(*arr)); printf("%d\n", sizeof(arr[1])); printf("%d\n", sizeof(&arr)); printf("%d\n", sizeof(&arr + 1)); printf("%d\n", sizeof(&arr[0] + 1)); return 0; }
解析:
- 首先第一个:很明显就是我们上面所提到的特殊模式。因此
sizeof(数组名)
计算的就是整个数组的大小,数组有6个元素,每个元素都是char类型的,在内存中占1个字节,那结果就是 6
printf("%d\n", sizeof(arr));
- 第二个:arr并不是单独放在
sizeof
中的,那它就是数组名,数组名即为首元素地址,此时计算的就是第一个元素的地址,但只要地址的话即为 4 / 8字节
printf("%d\n", sizeof(arr + 0));
- 第三个:arr既没有单独放在
sizeof
中,也没有&
,那么它就是首元素地址,对首元素地址进行*
解引用,此时获取到的就是首元素,数组的首元素是[a]
,类型是【char】,那大小即为 1
printf("%d\n", sizeof(*arr));
- 第四个很简单,就是计算数组arr中第一个元素的大小,那也是 1个字节
printf("%d\n", sizeof(arr[1]));
- 那下面这个呢? 很明显看到
&数组名
,那么取出的就是整个数组的地址,上面说过了,它还是一个地址,那么就是 4 / 8字节
printf("%d\n", sizeof(&arr));
- 一样的,
&arr
取到整个数组的地址,因为其类型是一个数组指针,那么 + 1就跳过一个数组的大小,此时它就指向了字符[f]
后面的这个地址,那既然是地址的话也还是 4 / 8字节
printf("%d\n", sizeof(&arr + 1));
- 最后,
&arr[0]
取到的是数组首元素的地址,它的类型是int*
,+ 1可以访问4个字节的大小,即为&arr[1]
,此时它算的还是一个地址的大小,那请说出答案!: 4 / 8字节
printf("%d\n", sizeof(&arr[0] + 1));
运行结果:
- 首先在32(x86)为平台下运行试试【指针大小为4个字节】
- 然后在64(x64)为平台下运行试试【指针大小为8个字节】
看完
sizeof()
之后,我们再来看看strlen()
strlen() 是函数,它是用来求字符串长度的,计算的是字符串之前 '\0' 出现的字符个数,如果没有看到 '\0' 会继续往后找
代码:
printf("%d\n", strlen(arr)); printf("%d\n", strlen(arr + 0)); printf("%d\n", strlen(*arr)); printf("%d\n", strlen(arr[1])); printf("%d\n", strlen(&arr)); printf("%d\n", strlen(&arr + 1)); printf("%d\n", strlen(&arr[0] + 1));
解析:
- 首先来看一下它的内存分布,可以看到它是内存中一块连续的空间,但是因为这个字符数组并没有
\0
,所以我们无法确定它的结束标志
- 那么我们首先来看第一个,
arr
放在strlen()内部,注意这里并不是sizeof()内部,而且也没有&数组名
,所以arr表示的是数组的首元素地址,就是从字符a这个位置开始往后计算这个字符数组的长度,上面说过了,strlen()会向后查找直到\0
为止,但是呢又因为这个字符数组内部本身并不存在\0
,那它就会继续往后查找,==可是对于arr数组后面的这块位置是随机的,是否具有\0
是不确定的==,因此最终的结果是 随机值
printf("%d\n", strlen(arr));
- 好,接下去第二个其实和第一个是一样的,因为arr是首元素地址,+ 0之后的结果还是一样的,为 随机值
printf("%d\n", strlen(arr + 0));
- arr依旧是首元素地址,那对首元素地址进行解引用获取到的就是【首元素】,首元素就是字符
a
,类型是char,但是strlen()要为其传入的是类型为char*
的地址,所以strlen就会将a的ASCLL码值97当做地址进行传入
printf("%d\n", strlen(*arr));
- 对于ASCLL码我们它是美国国家标注协会ISO所定义的标准,那在我们C语言中就是已经存在了的,==它是属于内存中的一块固定地址,这块地址我们是无法去使用的,内存也不会将其分配给我们==,所以此时我们使用strlen()去访问这块地址的时候其实属于非法访问,调试一下看看💻
- 可以看到我标出的位置
0x00000061
这个位置发生了冲突,这是在内存中以十六进制的形式来表示地址,将其转换为十进制表示即为97,那正好对应了我们上面所分析的为strlen()传入了字符a
的ASCLL码值97,所以可以看出这块地址确实是无法访问的
- 那如果你清楚了上面这个,其实对于下面的这个也是一样的,
arr[1]
这个数组元素也不是一个地址,而是一个字符,此时会将b
的ASCLL码值98传入strlen(),那此时我们去访问这块地址的时候也是属于非法访问
printf("%d\n", strlen(arr[1]));
可以看到,最后结果也是 err,通过进制转换可以发现正好与b的ASCLL码值98相对应
- 可以看到,出现了
&数组名
的情况,那此时我们就获取到了整个数组的地址,那整个数组的地址和数组首元素的地址是一样的,都位于字符a
这个位置,那么从这个位置向后找\0
,就和第一题一样是不确定的,字符数组本身不具备\0
,其他地址处也可能没有\0
,因此最终的结果为 随机值
printf("%d\n", strlen(&arr));
- 在上一题中,
&arr
取出了整个数组的地址,它的类型为int (*)[6]
,是一个数组指针,那一个数组指针 + 1就跳过了整个数组,来到了字符f
后面的这块地址处,接着向后查找,去找\0
,但结果我们知道,还是一个 随机值,不过这个随机值会比上面的这个随机值少6,因为要减去已经跳过的6个数组元素
printf("%d\n", strlen(&arr + 1));
- 好接下去最后一个,首先取到的是数组的首元素地址,它的类型是
char*
,那么 + 1就会跳过一个数组的元素,来到&arr[1]
这个为止,即字符b
所在的地址处,此时继续向后查找还是一个 随机值,这个随机值会比上面的这个随机值少1,因为要减去已经跳过的1个数组元素a
printf("%d\n", strlen(&arr[0] + 1));
运行结果:
- 这里没有指针,我就直接在32为平台下运行了,将两个结果为err的注释掉后,最终的结果和我们上面分析的是一样的
看完了上面这些,你是否对指针和数组的理解又有了进一步的理解呢😉坐稳了,下一班车即将到达:car: