系统性学习C语言-第十六讲-深入理解指针(6)
1. sizeof
和 strlen
的对比
1.1 sizeof
在学习操作符的时候,我们学习了 sizeof
,sizeof
计算变量所占内存空间大小的,单位是字节,
如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。
sizeof
只关注占用内存空间的大小,不在乎内存中存放什么数据。
比如:
#include <stdio.h>
int main()
{
int a = 10;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof a);
printf("%d\n", sizeof(int));
return 0;
}
1.2 strlen
strlen
是 C语言 库函数,功能是求字符串长度。函数原型如下:
size_t strlen ( const char * str );
统计的是从 strlen
函数的参数 str
中这个地址开始向后,\0
之前字符串中字符的个数。
strlen
函数会⼀直向后找 \0
字符,直到找到为止,所以可能存在越界查找。
#include <stdio.h>
int main()
{
char arr1[3] = {'a', 'b', 'c'};
char arr2[] = "abc";
printf("%d\n", strlen(arr1));
printf("%d\n", strlen(arr2));
printf("%d\n", sizeof(arr1));
printf("%d\n", sizeof(arr2));
return 0;
}
1.3 sizeof
和 strlen
的对比
2. 数组和指针笔试题解析
2.1 一维数组
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));
在对代码进行分析之前我们先回忆几个重要知识点。
-
数组名通常代表数组首元素的地址,但是有例外。
-
sizeof(数组名)
,此时数组名代表整个数组。 -
&数组名
,此时取出的地址为整个数组的地址。
在对上面的知识点进行回忆后,我们便可以对代码进行分析。
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a)); //为数组名的特殊用法,代表整个数组,结果为 16
printf("%d\n",sizeof(a+0)); //数组名并未单独放在 sizeof 内部,所以代表数组首元素,结果为 4/8
printf("%d\n",sizeof(*a)); //数组名为首元素地址,对其解引用后代表首元素,首元素类型为整形,结果为 4
printf("%d\n",sizeof(a+1));//数组首元素地址 + 1,仍为地址,结果为 4/8
printf("%d\n",sizeof(a[1]));//数组中第二个元素的字节数大小,结果为 4
printf("%d\n",sizeof(&a));//取出整个数组的地址,但仍为地址,结果为 4/8
printf("%d\n",sizeof(*&a));//先取出整个数组的地址,然后再解引用,仍然相当于直接求整个数组的字节数大小,结果为 16
printf("%d\n",sizeof(&a+1));//取出整个数组的地址然后 + 1,仍为地址,结果为 4/8
printf("%d\n",sizeof(&a[0]));//求数组第一个元素的地址的字节数大小,仍为地址,结果为 4/8
printf("%d\n",sizeof(&a[0]+1));//相当于求第二个元素的地址字节数大小,仍为地址,结果为 4/8
这里作者的运行环境为 32位,故地址的字节数为 4。
2.2 字符数组
练习 1:
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));
与上面一样的步骤,接下来我们对代码进行解析。
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));//计算整个数组的字节数大小,为 6
printf("%d\n", sizeof(arr+0));//计算数组首元素地址的大小,为 4/8
printf("%d\n", sizeof(*arr));//计算首元素的大小,元素为字符类型,为 1
printf("%d\n", sizeof(arr[1]));//计算首元素的大小,元素为字符类型,为 1
printf("%d\n", sizeof(&arr));//取出整个数组的地址,结果仍为地址,字节数为 4/8
printf("%d\n", sizeof(&arr+1));//取出整个数组的地址,然后 + 1,相当于跳过了整个数组后的地址,仍为地址,字节数为 4/8
printf("%d\n", sizeof(&arr[0]+1));//相当于取出的是整个数组第二个元素的地址,仍为地址,字节数为 4/8
练习 2:
char arr[] = {'a','b','c','d','e','f'};
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));
解析:
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));//从首元素地址开始计算,由于 strlen 的特性,数组中并没有存储 \0,最终结果为随机值
printf("%d\n", strlen(arr+0));//从首元素开始计算,与上面的解析同理,最终结果仍为随机值
printf("%d\n", strlen(*arr));//*arr 解析出的结果为 a,a 的 ASCII值为 97,strlen 会将这个值作为地址进行访问,最终结果为非法访问
printf("%d\n", strlen(arr[1]));//arr[1] 解析出的结果为 b,与上面的解析同理,最终结果仍为非法访问
printf("%d\n", strlen(&arr));//从数组的地址开始计算,结果为随机值,与上面的解析同理
printf("%d\n", strlen(&arr+1));//从跳过整个数组的第一个地址开始计算,结果为随机值,与上面解析同理
printf("%d\n", strlen(&arr[0]+1));//从数组的第二个元素地址开始计算,结果为随机值,与上面解析同理
因有非法访问,所以无法插入实机演示结果
练习 3:
char arr[] = "abcdef";
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));
解析:
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//求整个数组的字节数,结果为 7
printf("%d\n", sizeof(arr+0));//求数组首元素地址的字节数,结果为 4/8
printf("%d\n", sizeof(*arr));//求首元素的字节数,元素类型为字符,结果为 1
printf("%d\n", sizeof(arr[1]));//求数组第二个元素的字节数,元素类型为字符,结果为 1
printf("%d\n", sizeof(&arr));//求整个数组地址的字节数,结果为 4/8
printf("%d\n", sizeof(&arr+1));//求跳过整个数组后第一个地址的字节数,结果为 4/8
printf("%d\n", sizeof(&arr[0]+1));//求数组第二个地址的字节数,结果为 4/8
练习 4:
char arr[] = "abcdef";
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));
解析:
char arr[] = "abcdef";
printf("%d\n", strlen(arr));//从首元素地址开始计算,结果为 6
printf("%d\n", strlen(arr+0));//从首元素地址开始计算,结果为 6
printf("%d\n", strlen(*arr));//*arr 地结果为 a ,对应地 ASCII值 为 97,strlen 会将 97 作为地址进行访问,结果为非法访问
printf("%d\n", strlen(arr[1]));//结果为非法访问,arr[1] 结果为 b,其余解析与上面一样
printf("%d\n", strlen(&arr));//从整个数组地地址开始计算,结果为 6
printf("%d\n", strlen(&arr+1));//从跳过整个数组地第一个地址开始计算,结果为随机值
printf("%d\n", strlen(&arr[0]+1));//从数组地第二个元素地址开始计算,结果为 5
练习 5:
char *p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p+1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p+1));
printf("%d\n", sizeof(&p[0]+1));
解析:
char *p = "abcdef";
printf("%d\n", sizeof(p));//计算指针 p 的大小,结果为 4/8
printf("%d\n", sizeof(p+1));//计算第二字符 b 的地址大小,结果为 4/8
printf("%d\n", sizeof(*p));//计算第一元素 a 的字节大小,结果为 1
printf("%d\n", sizeof(p[0]));//计算第一元素 a 的字节大小,结果为 1
printf("%d\n", sizeof(&p));//计算指针 p 的地址大小,结果为 4/8
printf("%d\n", sizeof(&p+1));//计算跳过指针 p 地址后的第一个地址大小,结果为 4/8
printf("%d\n", sizeof(&p[0]+1));//计算 b 的地址大小,结果为 4/8
练习 6:
char *p = "abcdef";
printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));
解析:
char *p = "abcdef";
printf("%d\n", strlen(p));//从 p 开始计算,结果为 6
printf("%d\n", strlen(p+1));//从数组的第二个元素开始计算,结果为 5
printf("%d\n", strlen(*p));//*p 解析为 a ,ASCII值 为 97,strlen 会将 97 当作地址进行访问,结果为非法访问
printf("%d\n", strlen(p[0]));//p[0] 解析为 a,结果为非法访问
printf("%d\n", strlen(&p));//从 p 的地址开始计算,结果为随机值
printf("%d\n", strlen(&p+1));//从跳过 p 的地址后的第一个地址进行计算,结果为随机值
printf("%d\n", strlen(&p[0]+1));//从数组的第二个元素的地址开始计算,结尾为 5
2.3 二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));
解析:
int a[3][4] = {0};
printf("%d\n",sizeof(a));//计算整个 a 数组的字节数大小,结果为 48
printf("%d\n",sizeof(a[0][0]));//计算第一行第一个元素的大小,元素为整形类型,结果为 4
printf("%d\n",sizeof(a[0]));//a[0] 解析为 a数组 第一行的数组名,计算整个第一行的字节数大小,结果为 16
printf("%d\n",sizeof(a[0]+1));//a[0]+1 解析为 &a[0][1] ,计算第一行第二个元素地址的大小,结果为 4/8
printf("%d\n",sizeof(*(a[0]+1)));//*(a[0]+1) 解析为 a[0][1],计算第一行第二个元素的大小,结果为 4
printf("%d\n",sizeof(a+1));//计算 a数组 第二行地址的大小,结果为 4/8
printf("%d\n",sizeof(*(a+1)));//计算 a数组 第二行的大小,结果为 16
printf("%d\n",sizeof(&a[0]+1));//取出跳过第一行后的第一个地址,也就是第二行的地址,结果为 4/8
printf("%d\n",sizeof(*(&a[0]+1)));//取出第二行的地址然后解引用,求第二行的大小,结果为 16
printf("%d\n",sizeof(*a));// a 为首元素 a[0] 的地址,对其进行解引用表示第一行,求第一行的大小,结果为16
printf("%d\n",sizeof(a[3]));//a 数组并没有第四行,最终结果为报错
到此我们再对数组名的意义进行总结:
-
sizeof(数组名)
,这里的数组名表示整个数组,计算的是整个数组的大小。 -
&数组名
,这里的数组名表示整个数组,取出的是整个数组的地址。 -
除此之外所有的数组名都表示首元素的地址
3. 指针运算笔试题解析
3.1 题目1:
#include <stdio.h>
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf( "%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
解析:
int *ptr = (int *)(&a + 1);
对于指针 ptr
所指向的地址 &a + 1
代表这跳过整个 a
数组地址后的第一个地址。
所以 *(ptr - 1)
就是对指针 ptr
的前一个地址进行解引用,也就是数组中最后一个元素的地址进行解引用。
对于 *(a + 1)
,a + 1
表示数组中第二个元素的地址,所以 *(a + 1)
表示对第二个元素的地址进行解引用。
最终的结果为 2,5
3.2 题目2
//在X86环境下
//假设结构体的⼤⼩是20个字节
//程序输出的结果是啥?
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000;
int main()
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
解析:
要解出正确答案,我们就要清楚 指针 +1 ,会产生怎样的操作,指针 + 1 会根据指针不同的步长,从而跳过不同的字节数,
所以对于 p + 0x1
跳过的就是结构体的字节数,结构体 Test
的字节数大小为 20 ,所以 p + 0x1
跳过的字节数大小为 20,
20 转换为 16进制 为 0x14,所以结果为 0x100014,同样的对于 (unsigned int*)p + 0x1
我们跳过则为 unsigned int 类型的字节数,
为 4 字节,所以最终结果为 0x100004,但是在(unsigned long)p + 0x1
p 被转换成了无符号长整形类型,不再为指针,
所以这时的 +1 就不能再遵循指针的规则,而是遵循整形的算术规则,正常 + 1,结果为 0x100001,
对于 X86 环境下的地址显示,32位系统在显示地址时最多能显示 8 位,所以我们要在结果前补上两个 0,
最终结果为:
3.3 题目3
#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0];
printf( "%d", p[0]);
return 0;
}
解析:
对于这道题,我们一定要辨析好二维数组的初始化方式,题目中二维数组的初始化分组部分使用的是 ()
,而并非 {}
,
而小括号内部使用了逗号表达式,逗号表达式返回的是表达式中最后一个值,
所以实际二维数组的初始化其实应该为这样 int a[3][2] = { 1, 3, 5 };
所以指针 p
,p = a[0];
取到的是数组第一行地址,第一行包含的元素为 1,3
,所以 p[0]
取到的第一个元素为 1,
最终结果为 1 。
3.4 题目4
//假设环境是x86环境,程序输出的结果是啥?
#include <stdio.h>
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
解析:
首先我们对于 p[4][2]
进行分析, p[4][2]
相当于 *(p + 4)[2]
,这里就和指针步长扯上关系了,指针 p
的类型为 int [4]
,
所以每次 + 1 时,跳过 4 个整形类型的字节,因为 p = a
,所以指针 p
与 数组 a
的首元素地址时一样的,
所以 p[4][2]
实际上跳过了 a
数组的 18 个元素,而 a[4][2]
跳过了数组的 22 个元素,指针 - 指针计算出的是指针之间的元素个数,
所以结果为 -4,但对于第一个 -4 我们要化成十六进制地址的形式,我们先写出 -4 的源码,然后求出补码。
源码:10000000 00000000 00000000 00000100
反码:11111111 11111111 11111111 11111011
补码:11111111 11111111 11111111 11111100
补码的十六进制:FFFFFFFC
所以最终的结果为:
3.5 题目5
#include <stdio.h>
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int *ptr1 = (int *)(&aa + 1);
int *ptr2 = (int *)(*(aa + 1));
printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
解析:
我们先对指针ptr1
进行解析,int *ptr1 = (int *)(&aa + 1);
其中 &aa + 1
是跳过整个 aa
数组后的第一个整形指针地址
*ptr2 = (int *)(*(aa + 1));
其中 *(aa + 1)
中 aa
表示数组首元素地址,为 &aa[0]
,+ 1 后指向 &aa[1]
,
所以指针 ptr2
实际代表数组 aa
的第二行。因为 ptr1,ptr2
指针的类型都为整形指针类型,所以 +1,-1 都只会跳过一个整形的地址
所以 *(ptr1 - 1)
是对 aa
数组的最后一个元素地址进行解引用,*(ptr2 - 1)
是对数组第一行最后一个元素地址进行解引用
所以最终的结果为:
3.6 题目6
#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
解析:
我们先来分析字符指针数组 a
,a
的成员有三个,依次分别为 work
,at
,alibaba
。
因为 char**pa = a;
,所以指针 pa
指向数组 a
的首元素地址,pa++;
相当于 a[0] + 1
,数组 a
的第一个元素为 work
,+1后
指向第二个元素 at
,所以最终的打印结果为 at
。
3.7 题目7
#include <stdio.h>
int main()
{
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);
return 0;
}
解析:
这里我们先将所有结构以图标的形式呈现出来,以便我们更好地进行观察。
printf("%s\n", **++cpp);
现在我们再对代码进行分析,**++cpp
,其中 cpp
指针指向 cp
的首元素,在 ++
后指向了 cp
的第二个元素 c + 2
,
所以结果为 POINT
。
printf("%s\n", *--*++cpp+3);
在 *--*++cpp+3
中 ++cpp
此时指向 cp
的第三个元素 c + 1
,解引用符号的结合顺序更高,先结合
再与 --
符号进行结合,此时就变成了 *--(c + 1) + 3
,结合后变成 *c + 3
,此时就是对 c
数组的首元素,
ENTER
的第四个元素开始输出,最终结果为 ER
。
printf("%s\n", *cpp[-2]+3);
此时 cpp
指针在与两个自增符号进行结合后,已经指向了 cp
数组的第三个元素,所以 cpp[-2]
指向了 cp
数组的第一个元素,
就表示为 *cp+3
,也就是从 FIRST
的第四个字符开始输出,最终结果为 ST
。
printf("%s\n", cpp[-1][-1]+1);
此时的 cpp[-1]
代表 cp
的第二个元素,简化为 cp[1][-1]+1
再次简化为 *((c + 2) - 1) + 1
也就是 *(c + 1) + 1
,
从 NEW
的第二个字符开始输出,结果为 EW
。