深入理解指针(八)

22 阅读13分钟

一、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;
}

        输出结果如下:

char arr1[3] = { 'a', 'b', 'c' };

        这里定义的是一个字符数组,长度是3,没有 '\0' 结尾。strlen 的作用是:从当前位置开始统计字符个数,直到遇到 '\0' 为止。但是 arr1 里面没有 '\0',所以 strlen(arr1) 会继续往后读内存,直到碰到某个偶然的 '\0' 才停。因此strlen(arr1) 是未定义行为,结果不确定(可能是3,也可能更大,甚至崩溃)。

char arr2[] = "abc"

        这里是字符串初始化。字符串 "abc" 实际存储为:a  b  c  \0,所以数组长度是 4。strlen 统计到 '\0' 为止,不包含 '\0'。所以strlen(arr2) = 3

        sizeof 计算的是整个数组所占字节数。arr1 有 3 个 char,每个 char 占 1 字节,所以sizeof(arr1) = 3

        arr2数组里一共有 4 个字符(包括 '\0'),所以sizeof(arr2) = 4

1.3 sizeof 和strlen的对比

sizeofstrlen
1. sizeof是操作符1.strlen是库函数,使用需要包含头文件string.h
2. sizeof计算操作数所占内存的大小,单位是字节2.strlen是求字符串长度的,统计的是\0之前的字符个数
3. 不关注内存中存放什么数据3. 关注内存中是否有 \0 ,如果没有 \0 ,就会持续往后找,可能会越界

二、数组和指针小练习

2.1 ⼀维数组

        假设在64 位系统请计算下面的值:

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(a):a 是整个数组,包含 4 个 int,4 × 4 = 16

sizeof(a + 0):a 在表达式中会退化为指向首元素的指针,a + 0 仍是 int*,所以是8

sizeof(*a):*a 等价于 a[0],是一个 int,答案为4

sizeof(a + 1):a + 1 还是 int*,答案为8

sizeof(a[1]):a[1] 是 int,答案为4

sizeof(&a):&a 是指向整个数组的指针,类型是 int (*)[4],但本质仍是指针,答案为8

sizeof(*&a):*&a 等价于 a,本身是数组,答案为16

sizeof(&a + 1):&a + 1 是指向下一个数组的指针(步长是整个数组大小),类型仍是指针,答案为8

sizeof(&a[0]):&a[0] 是 int*,答案为 8

sizeof(&a[0] + 1):&a[0] + 1 仍是 int*,答案为8如果你在 32 位系统上,所有指针相关的结果都会变成 4。

答案如图:

2.2 字符数组

代码一:

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));

解析:

sizeof(arr):arr 是整个数组,包含 6 个 char,6 × 1 = 6

sizeof(arr + 0):arr 在表达式中会退化为指向首元素的指针,arr + 0 仍是 char*,所以是 8

sizeof(*arr):*arr 等价于 arr[0],是一个 char,答案为 1

sizeof(arr[1]):arr[1] 是 char,答案为 1

sizeof(&arr):&arr 是指向整个数组的指针,类型是 char (*)[6],但本质仍是指针,答案为 8

sizeof(&arr + 1):&arr + 1 是指向下一个数组的指针,但仍是指针类型,答案为 8

sizeof(&arr[0] + 1):&arr[0] + 1 是 char*,答案为 8

答案如图:

代码二:

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));

解析:

strlen(arr):arr 指向首元素,但数组中没有 '\0',strlen 会继续向后读内存直到遇到 '\0',结果不确定

strlen(arr + 0):arr + 0 等价于 arr,同样没有 '\0',结果不确定

strlen(*arr):*arr 是字符 'a',类型是 char,不是地址,传给 strlen 会当作地址使用,程序行为未定义,会直接崩溃

strlen(arr[1]):arr[1] 是字符 'b',不是地址,传给 strlen 同样是结果不确定

strlen(&arr):&arr 是整个数组的地址,类型是 char (*)[6],传给 strlen 会从该地址开始找 '\0',数组中没有 '\0',结果不确定

strlen(&arr + 1):&arr + 1 指向数组末尾之后的位置,再向后找 '\0',结果不确定

strlen(&arr[0] + 1):&arr[0] + 1 指向 arr[1],但后面依然没有 '\0',结果不确定

代码三:

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));

解析:

sizeof(arr):arr 是整个数组,包含 6 个字符加 1 个 '\0',共 7 个字节,答案为 7

sizeof(arr + 0):arr 在表达式中会退化为指向首元素的指针,arr + 0 仍是 char*,答案为 8

sizeof(*arr):*arr 等价于 arr[0],是一个 char,答案为 1

sizeof(arr[1]):arr[1] 是 char,答案为 1

sizeof(&arr):&arr 是指向整个数组的指针,类型是 char (*)[7],但本质仍是指针,答案为 8

sizeof(&arr + 1):&arr + 1 是指向下一个数组的指针,但仍是指针类型,答案为 8

sizeof(&arr[0] + 1):&arr[0] + 1 是 char*,答案为 8

答案如图:

代码四:

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));

解析:

strlen(arr):arr 指向字符串首字符,直到遇到 '\0' 结束,"abcdef" 长度为 6,答案为 6

strlen(arr + 0):arr + 0 等价于 arr,仍指向字符串首字符,答案为 6

strlen(*arr):*arr 是字符 'a',不是地址,传给 strlen 会崩溃

strlen(arr[1]):arr[1] 是字符 'b',不是地址,传给 strlen 会崩溃

strlen(&arr):&arr 是整个数组的地址,类型是 char (*)[7],但起始地址与 arr 相同,从这里开始能正确读到 '\0',答案为 6

strlen(&arr + 1):&arr + 1 指向数组末尾之后的位置,从那里找 '\0',结果不确定

strlen(&arr[0] + 1):&arr[0] + 1 指向 'b',字符串为 "bcdef",长度为 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));

解析:

sizeof(p):p 是指针变量,类型是 char*,在 64 位系统上占 8 字节,答案为 8

sizeof(p + 1):p + 1 仍是 char*,答案为 8

sizeof(*p):*p 等价于 p[0],是一个 char,答案为 1

sizeof(p[0]):p[0] 是 char,答案为 1

sizeof(&p):&p 是指向指针变量 p 的地址,类型是 char**,但本质仍是指针,答案为 8

sizeof(&p + 1):&p + 1 仍是指针类型,答案为 8

sizeof(&p[0] + 1):&p[0] 是 char*,加 1 后仍是 char*,答案为 8

代码六:

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));

解析:

strlen(p):p 指向字符串首字符 "abcdef",长度为 6,答案为 6

strlen(p + 1):p + 1 指向字符 'b',字符串为 "bcdef",长度为 5,答案为 5

strlen(*p):*p 是字符 'a',不是地址,传给 strlen 会崩溃

strlen(p[0]):p[0] 是字符 'a',不是地址,传给 strlen 会崩溃

strlen(&p):&p 是指向指针变量 p 的地址,不是字符串首地址,传给 strlen 会崩溃或输出随机值

strlen(&p + 1):&p + 1 仍不是字符串地址,传给 strlen 会崩溃或输出随机值

strlen(&p[0] + 1):&p[0] + 1 指向字符 'b',字符串为 "bcdef",长度为 5,答案为 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]));

解析:

sizeof(a):a 是整个二维数组,3×4 个 int,每个 4 字节,答案为 48

sizeof(a[0][0]):a[0][0] 是 int,答案为 4

sizeof(a[0]):a[0] 是第一行数组,含 4 个 int,答案为 16

sizeof(a[0] + 1):a[0] 在表达式中退化为指向首元素的指针,a[0] + 1 是 int*,答案为 8

sizeof( (a[0] + 1)): (a[0] + 1) 是 a[0][1],类型是 int,答案为 4

sizeof(a + 1):a 在表达式中退化为 int (*)[4],a + 1 是指向第二行的指针,答案为 8

sizeof( (a + 1)): (a + 1) 是第二行数组 a[1],类型 int[4],答案为 16

sizeof(&a[0] + 1):&a[0] 是指向第一行的指针,&a[0] + 1 是指向下一行的指针,答案为 8

sizeof( (&a[0] + 1)): (&a[0] + 1) 是第二行数组 a[1],类型 int[4],答案为 16

sizeof(*a):*a 等价于 a[0],第一行数组 int[4],答案为 16

sizeof(a[3]):a[3] 类型是 int[4](sizeof 在编译期计算类型,不访问内存),答案为 16

三、指针小练习

代码一:

#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;
}

        a 是一个长度为 5 的整型数组,a 在表达式中会退化为指向首元素的指针,因此 *(a + 1) 等价于 a[1],值为 2。

        &a 是整个数组的地址,类型是 int (*)[5]&a + 1 指向数组 a 之后的位置,也就是数组末尾的下一个地址,把它强制转换为 int* 后赋值给 ptr,此时 ptr 指向数组末尾之后的位置。再计算 *(ptr - 1),即访问数组最后一个元素 a[4],值为 5。

代码二:

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;
}

        p 是指向 struct Test 的指针,初始化为地址 0x100000

   p + 0x1 是按 struct Test* 类型进行指针运算,指针会移动一个结构体的大小,因此输出地址是 0x100000 加上 sizeof(struct Test)

   (unsigned long)p + 0x1 将指针强制转换为整数再加 1,进行的是普通整数加法,结果地址是 0x100001

        (unsigned int*)p + 0x1 将指针转换为 unsigned int* 后再加 1,由于 unsigned int 占 4 字节,所以地址移动了 4 个字节,输出地址为 0x100004

代码三:

#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;
}

        数组 a[3][2] 使用了 (0,1)、(2,3)、(4,5) 来初始化,其中逗号运算符会先计算前面的表达式再计算后面的表达式,整个表达式的值是最后一个,因此 (0,1) 的值是 1,(2,3) 的值是 3,(4,5) 的值是 5。

        二维数组按行优先展开,实际初始化等价于 int a[3][2] = {1, 3, 5},未显式初始化的元素自动补 0,所以数组内存布局为 a[0][0]=1, a[0][1]=3, a[1][0]=5, a[1][1]=0, a[2][0]=0, a[2][1]=0

        p = a[0] 将第一行数组退化为指针,指向 a[0][0],再通过 p[0] 访问的就是 a[0][0],值为 1。因此程序输出为 1

代码四:

#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;
}

       数组 a[5][5] 是一个 5×5 的整型数组,而 p 被定义为指向含 4 个 int 的数组的指针 int (*p)[4],然后 p = a 将其指向 a[0]。由于 p 的行宽与 a 的实际行宽不同,后续的指针运算会产生偏移量差异。

        表达式 &p[4][2] 的计算相当于 *(p + 4) + 2,每步跳过 4 个 int,再加 2,总偏移 18 个 int;而 &a[4][2] 指向数组第五行第三个元素,总偏移 22 个 int。两者相减得到 &p[4][2] - &a[4][2] = -4,表示偏移差 4 个 int。

        因此 printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]) 的输出为 -4,-4

代码五:

#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;
}

        数组 aa[2][5] 初始化为 110,按行优先存储在内存中。ptr1 = (int*)(&aa + 1) 将整个二维数组末尾的地址强制转换为 int*,指向数组末尾后的第一个元素,*(ptr1 - 1) 就访问到数组最后一个元素 aa[1][4],值为 10。

        ptr2 = (int*)(*(aa + 1)) 首先 aa + 1 指向第二行,*(aa + 1) 取到第二行数组并退化为指针,指向 aa[1][0]*(ptr2 - 1) 则访问第二行前一个元素,即第一行最后一个元素 aa[0][4],值为 5。

        因此 printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1)) 输出 10,5

代码六:

#include <stdio.h>
int main()
{
char* a[] = { "work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}

        数组 a 是一个字符指针数组,存储了 "work", "at", "alibaba" 三个字符串的首地址。pachar** 类型,指向数组 a 的首元素,也就是指向 "work" 的指针。

        执行 pa++ 后,pa 指向数组的第二个元素,也就是指向 "at" 的指针。最后,*pa 就是 "at" 字符串的首地址,printf("%s\n", *pa) 输出该字符串。

因此程序的输出为:at

代码七:

#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;
}

        数组 c 存储了四个字符串 "ENTER", "NEW", "POINT", "FIRST"cp 是一个指向 char* 的数组,按顺序指向 c 中的不同元素:cp[0] = c+3 → "FIRST"cp[1] = c+2 → "POINT"cp[2] = c+1 → "NEW"cp[3] = c → "ENTER"cppchar***,初始指向 cp[0]

        执行 **++cpp 时,cpp 指向 cp[1]*cpp 等于 c+2,再解引用一次得到 c[2],即 "POINT",所以输出第一行是 "POINT"

        第二行 *--*++cpp + 3++cpp 指向 cp[2],解引用得到 c+1,再 -- 后指向 c[0],再加 3 个字符偏移得到 "ENTER" 的第四个字符开始,即 "ER"

        第三行 *cpp[-2] + 3cpp[-2] 指向 cp[0]c+3,解引用得到 "FIRST",加 3 个字符偏移得到 "ST"。最后一行 cpp[-1][-1] + 1cpp[-1] 指向 cp[1]c+2cpp[-1][-1] 解引用为 c+1"NEW",加 1 个字符偏移得到 "EW"

因此四行输出依次为:POINT、ER、ST、EW