目录
一、sizeof 和 strlen() 对比
1. sizeof 操作符
sizeof 是一个 C 语言操作符,用于计算变量、数据类型或表达式所占用的字节数。当计算类型名的大小时,必须使用括号括起;计算变量的大小时可以省略。如下代码:
当使用 sizeof 计算类型名大小不加括号时,编译器会报错。且由于操作符 sizeof 的返回值为 size_t 类型,所以输出时需要使用 %zd 格式。
2. sizeof 操作符不会计算表达式的值
如下代码:
int a = 10;
short b = 2;
printf("%zd\n", sizeof(b = a + b));
printf("b = %hd\n", b);
按理来说,第三条语句首先计算 b = a + b,然后 b 的值为 12,由于 b 的类型为 short,所以整个 sizeof 表达式的结果为 2。所以上述代码的运行结果应该是打印 2 和 12。我们来看看程序运行结果如何:
可以看到 b 的值没变,这是因为 sizeof 不会计算表达式的值,b = a + b,那最终结果是 b 的值,b 是 short 类型,那么该表达式的值就直接为 2,并不会去计算表达式。我们要知道,表达式的计算是在程序运行时进行的,而 sizeof 操作符的计算是在编译过程中进行的。而程序的运行的步骤是:预处理->编译->汇编->链接,然后再生成的可执行程序。
3. strlen() 函数
strlen() 是 C 语言标准库中的一个计算字符串长度的函数,包含在头文件 string.h 中。该函数的原型如下:
// strlen() 函数原型
size_t strlen(const char *s);
该函数从传入的字符指针地址处开始往后计算字符数,直到遇到空字符,然后返回计算的字符数,不包括空字符。由于不需要改变字符串的值,所以在参数中加上了 const 进行修饰。下面是对该函数的简单使用:
4. 确保传入 strlen() 函数的地址后面有空字符
如果传入 strlen() 函数的地址后面没有空字符,那么 strlen() 函数会往后一直计算字符个数,知道遇到空字符位置,这就造成了越界访问。如下代码:
可以看到,字符数组的长度本应为 3,可计算出的却是 42。由于字符数组 tmp 的末尾没有空字符,所以 strlen() 函数往后一直计算直到遇到空字符,而系统其他内存空间的值是未知的,所以最终显示得是一个随机值。
5. sizeof 和 strlen() 对比表格
sizeof | strlen() |
---|---|
1. sizeof 是一个操作符 | 1. strlen() 是一个标准库函数 |
2. sizeof 计算操作数所占内存大小(字节) | 2. strlen() 函数计算字符串的长度,统计 \0 之前的字符个数 |
3. sizeof 不会计算表达式的值 | 3. strlen() 函数不会关心越界问题,只要没遇到 \0 就会一直往后计算 |
二、数组和指针笔试题解析
1. 一维数组
int a[] = {1,2,3,4};
1. printf("%d\n",sizeof(a));
2. printf("%d\n",sizeof(a+0));
3. printf("%d\n",sizeof(*a));
4. printf("%d\n",sizeof(a+1));
5. printf("%d\n",sizeof(a[1]));
6. printf("%d\n",sizeof(&a));
7. printf("%d\n",sizeof(*&a));
8. printf("%d\n",sizeof(&a+1));
9. printf("%d\n",sizeof(&a[0]));
10.printf("%d\n",sizeof(&a[0]+1));
解析:
- sizeof + 数组名,计算整个数组的大小,16
- a + 0 表示数组首元素的地址,int* 类型,4/8
- *a 是数组首元素,int 类型,4
- a + 1 表示数组第二个元素的地址,4/8
- a[1] 表示数组第二个元素,int 类型,4
- &a 表示整个数组的地址,int (*)[4] 类型,4/8
- *&a 等价于 a,等同于 sizeof + 数组名,16
- &a + 1 是数组 a 后第一个字节的地址,int (*)[4] 类型,4/8
- &a[0] 是数组首元素地址 4/8
- &a[0] + 1 是数组第二个元素的地址,4/8
下面是 64 位环境下,程序运行结果:
2. 字符数组
1. 代码 A
char arr[] = {'a','b','c','d','e','f'};
1. printf("%d\n", sizeof(arr));
2. printf("%d\n", sizeof(arr+0));
3. printf("%d\n", sizeof(*arr));
4. printf("%d\n", sizeof(arr[1]));
5. printf("%d\n", sizeof(&arr));
6. printf("%d\n", sizeof(&arr+1));
7. printf("%d\n", sizeof(&arr[0]+1));
解析:
- sizeof + 数组名,计算整个数组的大小 6
- arr + 0 是数组首元素的地址,4/8
- *arr 是数组首元素,1
- arr[1] 是数组第二个元素,1
- &arr 是整个数组的地址,char (*)[6] 类型,4/8
- &arr + 1 是数组 arr 后第一个字节的地址,char (*)[6] 类型,4/8
- &arr[0]+1 是数组第二个元素的地址 4/8
在 64 位环境下,程序运行结果:
2. 代码 B
char arr[] = {'a','b','c','d','e','f'};
1. printf("%d\n", strlen(arr));
2. printf("%d\n", strlen(arr+0));
3. printf("%d\n", strlen(*arr));
4. printf("%d\n", strlen(arr[1]));
5. printf("%d\n", strlen(&arr));
6. printf("%d\n", strlen(&arr+1));
7. printf("%d\n", strlen(&arr[0]+1));
解析:
8. 从字符数组 arr 首地址开始计算字符串长度,由于该字符数组末尾没有空字符,造成越界访问,结果为随机值
9. 随机值
10. *a 是数组首元素字符 ‘a’,其 ASCII 值为 65,而 strlen 函数需要 const char* 类型,那么 65 会被强制类型转换为 const char*,然后从该地址进行计算,这样就会造成非法访问。
11. 非法访问
12. &arr 是整个数组的地址,char (*)[6] 类型,会被强制类型转换为 const char*,相当于从数组首元素开始计算,越界访问,随机值
13. &arr + 1 是数组 arr 后面第一个字节的地址,类型为 char (*)[6],被强制类型转换为 char*,然后从该地址开始计算,非法访问
14. &arr[0]+1 是数组第二个元素的地址,越界访问,随机值
3. 代码C
char arr[] = "abcdef";
1. printf("%d\n", sizeof(arr));
2. printf("%d\n", sizeof(arr+0));
3. printf("%d\n", sizeof(*arr));
4. printf("%d\n", sizeof(arr[1]));
5. printf("%d\n", sizeof(&arr));
6. printf("%d\n", sizeof(&arr+1));
7. printf("%d\n", sizeof(&arr[0]+1));
解析:
- sizeof + 数组名,计算整个数组的大小,后面还有一个空字符,7
- arr+0 是数组首元素的地址,4/8
- *arr 是数组首元素,1
- arr[1] 是数组第二个元素,1
- &arr 取出的是整个数组的地址,char (*)[7] 类型,4/8
- &arr+1 指向数组 arr 后面第一个字节,char (*)[7] 类型,4/8
- &arr[0]+1 是数组第二个元素的地址,4/8
在 64 位环境下,程序运行结果:
4. 代码 D
char arr[] = "abcdef";
1. printf("%d\n", strlen(arr));
2. printf("%d\n", strlen(arr+0));
3. printf("%d\n", strlen(*arr));
4. printf("%d\n", strlen(arr[1]));
5. printf("%d\n", strlen(&arr));
6. printf("%d\n", strlen(&arr+1));
7. printf("%d\n", strlen(&arr[0]+1));
解析:
- arr 字符串的首地址,计算字符串的长度 6
- arr+0 是字符串的首地址,6
- *arr 是字符串的首字符,非法访问
- arr[1] 是字符串的第二个字符,非法访问
- &arr 是整个字符串的地址,char (*)[7] 类型,传入 strlen() 函数时,被强制类型转换为 char*,从字符串首字符开始计算,6
- &arr+1 是字符串后面第一个字符的地址,类型 char (*)[7],传入 strlen() 函数时,被强制类型转换为 char*,然后开始计算,非法访问
- &arr[0]+1 时字符串第二个字符的地址,5
5. 代码 E
char *p = "abcdef";
1. printf("%d\n", sizeof(p));
2. printf("%d\n", sizeof(p+1));
3. printf("%d\n", sizeof(*p));
4. printf("%d\n", sizeof(p[0]));
5. printf("%d\n", sizeof(&p));
6. printf("%d\n", sizeof(&p+1));
7. printf("%d\n", sizeof(&p[0]+1));
解析:
- p 是 char* 类型的指针,指向字符串第一个字符,4/8
- p+1 指向字符串第二个字符,4/8
- *p 是字符串第一个字符,1
- p[0] 是字符串第一个字符,1
- &p 是指针 p 的地址,char** 类型,二级指针,4/8
- &p+1 是指针 p 的地址的下一个字节的地址,char** 类型,4/8
- &p[0]+1 是字符串第二个字符的地址,4/8
在 64 位环境下,程序的运行结果:
6. 代码 F
char *p = "abcdef";
1. printf("%d\n", strlen(p));
2. printf("%d\n", strlen(p+1));
3. printf("%d\n", strlen(*p));
4. printf("%d\n", strlen(p[0]));
5. printf("%d\n", strlen(&p));
6. printf("%d\n", strlen(&p+1));
7. printf("%d\n", strlen(&p[0]+1));
解析:
- p 是字符串第一个字符的地址,计算整个字符串的长度 6
- p+1 是字符串第二个字符的地址,5
- *p 是字符串第一个字符,非法访问
- p[0] 是字符串第一个字符,非法访问
- &p 是指针 p 的地址,char** 类型,传入 strlen() 函数时,被强制类型转换为 char*,然后从该地址开始计算,越界访问,随机值
- &p+1 是指针 p 的地址后面第一个字节的地址,char** 类型,传入 strlen() 函数时,被强制类型转换为 char*,然后从该地址开始计算,非法访问
- &p[0]+1 是字符串第二个字符的地址,5
3. 二维数组
int a[3][4] = {0};
1. printf("%d\n",sizeof(a));
2. printf("%d\n",sizeof(a[0][0]));
3. printf("%d\n",sizeof(a[0]));
4. printf("%d\n",sizeof(a[0]+1));
5. printf("%d\n",sizeof(*(a[0]+1)));
6. printf("%d\n",sizeof(a+1));
7. printf("%d\n",sizeof(*(a+1)));
8. printf("%d\n",sizeof(&a[0]+1));
9. printf("%d\n",sizeof(*(&a[0]+1)));
10. printf("%d\n",sizeof(*a));
11. printf("%d\n",sizeof(a[3]));
解析:
- sizeof + 数组名,计算整个数组的大小,48
- a[0][0] 是二维数组的首元素,4
- a[0] 是二维数组 a 的第一行,也是一个数组,sizeof + 数组名 16
- a[0]+1 是二维数组 a 的第一行的第二个元素的地址,int (*)[4] 类型,4/8
- *(ar[0]+1) 是二维数组 a 的第一行的第二个元素,4
- a+1 是二维数组 a 的第二行的地址,int (*)[4] 类型,4/8
- *(a+1) 是二维数组 a 的第二行,也就是 sizeof + 第二行数组名,16
- &a[0]+1 是二维数组 a 的第二行的地址,int(*)[4] 类型,4/8
- *(&a[0]+1) 是二维数组的第二行,16
- *a 是二维数组 a 的第一行,也就是 sizeof + 第一行数组名,16
- a[3]是二维数组 a 的第四行,实际上并没有第四行,但是 sizeof 可以根据前三行推测出第四行的类型,这里并没有进行访问,所以不存在越界的问题,16
三、指针运算笔试题解析
1. 题目 A
#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;
}
//程序的结果是什么?
解析:
指针 ptr 指向数组 a 后面的第一个字节,*(a+1) 是数组 a 的第二个元素,而 *(ptr - 1) 是数组的第 5 个元素。如下图:
所以应该输出 2,5
64 位环境下,运行结果如下:
2. 题目 B
//在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;
}
解析:
在 p + 0x1 中由于 p 是 struct Test* 类型,所以其加 1 应该增加 sizeof(struct Test) 也就是 20 个字节,而 %p 又是按照十六进制输出,所以第一个 printf() 函数打印 100014。
在 (unsigned long)p + 0x1 中,p 被强制转换为 unsigned long 类型,现在就是算数运算,其加 1 就是加 1,所以第二个 printf() 函数打印 1000001。
在 (unsigned int*)p + 0x1 中,p 被强制类型转换为 unsigned int* 类型,所以其加 1 应该增加 sizeof(unsigned int) 也就是 4 个字节,所以第三个 printf() 函数打印 1000004。
在 64 位环境下,运行结果如下:
3. 题目 C
#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[0] 是二维数组 a 的第一行,在 p = a[0] 中,a[0] 代表第一行首元素的地址,所以 p[0] 是第一行的首元素。然后回过头来观察二维数组 a 的初始化语句,其中使用了逗号表达式,逗号表达式从左往右依次计算各个表达式,然后最终的结果是右侧表达式的值。所以实际上应该是: int a[3][2] = {1,3,5},所以程序应该输出 1。
在 64 为环境下,程序运行结果如下:
4. 题目 D
//假设环境是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;
}
解析:
a 是二维数组首行地址,在表达式 p = a 中,a 的值被强制类型转换为 int (*)[4] 然后赋值给 p。所以现在 p 和 a 的值都是数组 a 的第一个元素的第一个字节的地址,但是它们的类型分别为 int (*)[5] 和 int (*)[4],所以 p[4][2] 是数组 a 的第 19 个元素,而 a[4][2] 是数组 a 的第 23 个元素。而当两个指针指向同一块空间时,它们相减的结果是它们直接差的元素个数,所以 &p[4][2] - &a[4][2] 的值为 -4。它的二进制补码为:
11111111111111111111111111111100
当使用 %p 十六进制地址输出时,显示 fffffffc,使用 %d 输出时,-4
在 32 位环境下,输出结果如下:
5. 题目 E
#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;
}
解析:
在 int *ptr1 = (int *)(&aa + 1); 中,&aa + 1 是二维数组 aa 后的第一个字节的地址,然后被强制类型转换为 int* 赋值给 ptr1。
在 int *ptr2 = (int *)(*(aa + 1)); 中,aa +1 是二维数组第二行的地址,然后解引用获得第二行,也就是第二行首元素的地址,然后再被强制类型转换为 int* 被赋值给 ptr2。
现在两个指针指向如下:
所以 ptr1 - 1 指向 10,也就是 a[1][4],而 ptr2 - 1 指向 5 也就是 a[0][4]。
在 64 位环境下,程序运行结果如下:
6. 题目 F
#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
解析:
指针数组 a 的每个元素分别指向了一个字符串,每个元素的类型都是 char*,而在表达式 char**pa = a 中,a 是数组首元素的地址,char ** 类型,现在 pa 也指向数组 a 的首元素。然后 pa++,pa 指向数组 a 的第二个元素,然后解引用拿到第二个元素,也就是指向字符串常量 “at” 的指针,然后按照 %s 的格式输出,打印 at。具体关系如下图:
在 64 为环境下,程序运行结果如下:
7. 题目 G
#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() 语句,**++cpp,解引用操作符和前置底层操作符优先级相同,两个操作符都是右结合,所以先计算 ++cpp,然后 cpp 指向数组 cp 的第二个元素,
然后解引用 *++cpp,拿到数组 CP 的第二个元素,然后再次解引用 **++cpp,拿到数组 C 的第三个元素,然后打印字符串 POINT。
然后看第二条 printf() 语句,*–*++cpp+3,首先 ++cpp,cpp 指向数组 cp 的第三个元素,
接着解引用 *++cpp,拿到数组 cp 的第三个元素,然后进行前置自减操作,–*++cpp,让数组 cp 的第三个元素指向数组 c 的第一个元素,
然后解引用 *–*++cpp 拿到数组 cp 的第一个元素,然后加 3,*–*++cpp+3,拿到指向字符串 “ENTER” 第四个字符的地址,然后打印 ER
接下来看第三条 printf() 语句,*cpp[-2]+3,首先 cpp[-2],也就是 *(cpp-2) 拿到数组 cp 的第一个元素,然后解引用拿到数组 c 的第四个元素,然后加 3,拿到指向字符串 “FIRST” 的第四个字符的地址,然后打印 ST
接下来看第四条 printf() 语句,cpp[-1][-1]+1,首先 cpp[-1],拿到数组 cp 的第二个元素,然后 cpp[-1][-1] 拿到数组 c 的第二个元素,然后加 1 得到字符串 “NEW” 第二个字符的地址,然后打印 EW
在 64 为环境下,代码运行结果如下: