目录
一. sizeof 和 strlen 的对比
1.1 sizeof
sizeof计算变量所占内存空间的大小,单位是字节,如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。
sizeof 只关注占用内存空间的大小,不在乎内存中存放什么数据;并且sizeof中表达式不计算
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int a = 10;
printf("sizeof(a): %d\n", sizeof(a));
printf("sizeof(a+3.14): %d\n", sizeof(a+3.14));
printf("sizeof(int): %d\n", sizeof(int));
return 0;
}
通过运行该代码可以得到
其中a是int类型 占4个字节
a+3.14 由于a算术提升到了double类型 因此占8个字节
1.2 strlen
strlen是C语言库函数,功能是求字符串长度
- 计算字符串的实际长度(不包含’\0’)
- 必须接收以’\0’结尾的有效字符串指针
strlen("hello"); // 返回5
- 遇到第一个’\0’停止计数
strlen("hel\0lo"); // 返回3
练习strlen函数 请自己思考一下 下面函数会打印那些值
#include <stdio.h>
#include<string.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;
}
运行结果如下
其中由于arr1并没有 ' \0 ' 结尾 因此打印随机值 35
1.3 对比表格
特性 | sizeof 运算符 |
strlen 函数 |
---|---|---|
类型 | 编译时运算符(非函数) | 运行时函数(来自 <string.h> ) |
作用对象 | 变量、类型或表达式 | 仅适用于以 \0 结尾的字符串 |
计算时机 | 编译时确定 | 运行时计算 |
返回值 | 对象/类型占用的内存字节数(含 \0 ) |
字符串长度(不含 \0 ) |
参数示例 | sizeof(int) , sizeof(arr) |
strlen("hello") |
对指针的行为 | 返回指针本身的大小(通常4/8字节) | 计算指针指向的字符串长度 |
对数组的行为 | 返回整个数组的字节大小 | 将数组退化为指针后计算长度 |
对字符串字面量 | 包含 \0 的总大小(如 "abc" 返回4) |
不包含 \0 的长度(如 "abc" 返回3) |
时间复杂度 | O(1)(编译时完成) | O(n)(需遍历字符串到 \0 ) |
二. 数组和指针笔试题解析
首先我们先再次明确数组名的意义:
- sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组所占内存的大小。
- &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
- 除此之外所有的数组名都表示首元素的地址。
2.1 一维数组
首先请思考一下 下面这段代码将会打印什么
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a[] = { 1,2,3,4 };
printf("%zu\n", sizeof(a));
printf("%zu\n", sizeof(a + 0));
printf("%zu\n", sizeof(*a));
printf("%zu\n", sizeof(a + 1));
printf("%zu\n", sizeof(a[1]));
printf("%zu\n", sizeof(&a));
printf("%zu\n", sizeof(*&a));
printf("%zu\n", sizeof(&a + 1));
printf("%zu\n", sizeof(&a[0]));
printf("%zu\n", sizeof(&a[0] + 1));
return 0;
}
思考完了请验证你的答案是否正确
典型输出(64位系统):
16 // 整个数组大小
8 // 指针大小
4 // int大小
8 // 指针大小
4 // int大小
8 // 指针大小
16 // 整个数组大小
8 // 指针大小
8 // 指针大小
8 // 指针大小
注释讲解如下
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a[] = { 1,2,3,4 }; // 定义一个包含4个int元素的数组
printf("%zu\n", sizeof(a)); // 1. 整个数组的大小:4个int × 4字节/int = 16字节
printf("%zu\n", sizeof(a + 0)); // 2. 数组名a在表达式中退化为指针,a+0是指向第一个元素的指针,指针大小通常为4或8字节
printf("%zu\n", sizeof(*a)); // 3. 解引用数组名得到第一个元素,int类型大小为4字节
printf("%zu\n", sizeof(a + 1)); // 4. 数组名a退化为指针,a+1是指向第二个元素的指针,指针大小
printf("%zu\n", sizeof(a[1])); // 5. 第二个元素的大小,int类型为4字节
printf("%zu\n", sizeof(&a)); // 6. &a是"指向整个数组的指针",但仍然是指针,指针大小
printf("%zu\n", sizeof(*&a)); // 7. 解引用数组指针得到整个数组,大小为16字节
printf("%zu\n", sizeof(&a + 1)); // 8. 指向数组后面位置的指针,指针大小
printf("%zu\n", sizeof(&a[0])); // 9. 指向第一个元素的指针,指针大小
printf("%zu\n", sizeof(&a[0] + 1)); // 10. 指向第二个元素的指针,指针大小
return 0;
}
关键点说明:
- 数组名在大多数情况下会退化为指针,但在
sizeof(a)
和&a
操作中不会退化。- 指针的大小取决于系统架构(32位系统通常4字节,64位系统通常8字节)。
&a
是"指向整个数组的指针",类型是int(*)[4]
,但它的值仍然是数组的起始地址。*&a
等价于a
,所以sizeof(*&a)
就是整个数组的大小。- 数组元素的地址运算(如
a+1
、&a[0]+1
)得到的是指针,不是数组。
2.2 字符数组
2.2.1 代码练习一
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%zu\n", sizeof(arr));
printf("%zu\n", sizeof(arr + 0));
printf("%zu\n", sizeof(*arr));
printf("%zu\n", sizeof(arr[1]));
printf("%zu\n", sizeof(&arr));
printf("%zu\n", sizeof(&arr + 1));
printf("%zu\n", sizeof(&arr[0] + 1));
}
典型输出(64位系统):
6 // 整个数组大小
8 // 指针大小
1 // char大小
1 // char大小
8 // 指针大小
8 // 指针大小
8 // 指针大小
注释解析如下
#include<stdio.h>
int main()
{
char arr[] = { 'a','b','c','d','e','f' }; // 定义一个包含6个char元素的字符数组
printf("%zu\n", sizeof(arr)); // 1. 整个数组的大小:6个char × 1字节/char = 6字节
printf("%zu\n", sizeof(arr + 0)); // 2. 数组名arr在表达式中退化为指针,arr+0是指向第一个元素的指针,指针大小通常为4或8字节
printf("%zu\n", sizeof(*arr)); // 3. 解引用数组名得到第一个元素,char类型大小为1字节
printf("%zu\n", sizeof(arr[1])); // 4. 第二个元素的大小,char类型为1字节
printf("%zu\n", sizeof(&arr)); // 5. &arr是"指向整个数组的指针",但仍然是指针,指针大小
printf("%zu\n", sizeof(&arr + 1)); // 6. 指向数组后面位置的指针,指针大小
printf("%zu\n", sizeof(&arr[0] + 1)); // 7. 指向第二个元素的指针,指针大小
return 0;
}
关键点说明:
- 数组名在大多数情况下会退化为指针,但在
sizeof(arr)
和&arr
操作中不会退化。- 指针的大小取决于系统架构(32位系统通常4字节,64位系统通常8字节)。
&arr
是"指向整个数组的指针",类型是char(*)[6]
,但它的值仍然是数组的起始地址。- 字符数组
arr
的大小是6字节,因为每个char
类型占用1字节。- 数组元素的地址运算(如
arr+0
、&arr[0]+1
)得到的是指针,不是数组。
2.2.2 代码练习二
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%zu\n", strlen(arr));
printf("%zu\n", strlen(arr + 0));
printf("%zu\n", strlen(&arr));
printf("%zu\n", strlen(&arr + 1));
printf("%zu\n", strlen(&arr[0] + 1));
}
典型输出(64位系统):
36//随机值
36//随机值
36//随机值
30//随机值-6
35//随机值-1
注释解析如下:
int main()
{
// 定义并初始化一个字符数组,注意没有包含字符串终止符'\0'
char arr[] = { 'a','b','c','d','e','f' };
// 1. 计算从数组首元素开始的"字符串"长度
printf("%zu\n", strlen(arr));
// arr作为数组名,在表达式中退化为指向首元素的指针
// strlen会从'a'开始向后查找'\0',但数组中没有'\0'
// 会继续访问数组后面的内存,直到偶然遇到'\0'
// 结果是不可预测的随机值,且可能导致未定义行为
// 2. 计算从数组首元素开始的"字符串"长度
printf("%zu\n", strlen(arr + 0));
// arr + 0 仍然指向数组首元素
// 情况与第一个printf完全相同
// 结果也是随机值,且可能与第一个printf相同
// 3. 计算从数组地址开始的"字符串"长度
printf("%zu\n", strlen(&arr));
// &arr 是"指向整个数组的指针"(类型为 char(*)[6])
// 但它的值与arr相同,都是数组的起始地址
// 情况与前两个printf相同,结果也是随机值
// 4. 计算从数组末尾后一个位置开始的"字符串"长度
printf("%zu\n", strlen(&arr + 1));
// &arr + 1 跳过整个数组(6个字节),指向数组之后的内存位置
// strlen从这个新位置开始查找'\0'
// 结果也是随机值,但通常比前几个结果小6(因为跳过了6个字符)
// 注意这仍然是未定义行为
// 5. 计算从数组第二个元素开始的"字符串"长度
printf("%zu\n", strlen(&arr[0] + 1));
// &arr[0] + 1 指向数组的第二个元素'b'
// 从这个位置开始查找'\0'
// 结果也是随机值,但通常比第一个结果小1
// 同样属于未定义行为
return 0;
}
关键点总结:
字符串终止符的重要性:
strlen
依赖\0
来确定字符串结束位置- 这个数组没有以
\0
结尾,导致strlen
会越界访问指针运算:
arr
和arr+0
都指向首元素&arr
是整个数组的指针(类型不同但值相同)&arr + 1
跳过整个数组&arr[0] + 1
指向第二个元素
2.2.3 代码练习三
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
char arr[] = "abcdef";
printf("%zu\n", sizeof(arr));
printf("%zu\n", sizeof(arr + 0));
printf("%zu\n", sizeof(*arr));
printf("%zu\n", sizeof(arr[1]));
printf("%zu\n", sizeof(&arr));
printf("%zu\n", sizeof(&arr + 1));
printf("%zu\n", sizeof(&arr[0] + 1));
}
典型输出(64位系统):
7 // 整个数组大小(包括'\0')
8 // 指针大小
1 // char大小
1 // char大小
8 // 指针大小
8 // 指针大小
8 // 指针大小
注释解析如下:
#include<stdio.h>
int main()
{
char arr[] = "abcdef"; // 定义一个字符串数组,包含6个字符和1个'\0',共7个元素
printf("%zu\n", sizeof(arr)); // 1. 整个数组的大小:7个char × 1字节/char = 7字节
// 注意:字符串字面量"abcdef"会自动添加'\0',所以数组大小是7不是6
printf("%zu\n", sizeof(arr + 0)); // 2. arr在表达式中退化为指针,arr+0是指向第一个元素的指针
// 指针大小通常为4(32位)或8(64位)字节
printf("%zu\n", sizeof(*arr)); // 3. 解引用数组名得到第一个元素'a',char类型大小为1字节
printf("%zu\n", sizeof(arr[1])); // 4. 第二个元素'b'的大小,char类型为1字节
printf("%zu\n", sizeof(&arr)); // 5. &arr是"指向整个数组的指针",指针大小
printf("%zu\n", sizeof(&arr + 1)); // 6. 指向数组后面位置的指针,指针大小
printf("%zu\n", sizeof(&arr[0] + 1)); // 7. 指向第二个元素的指针,指针大小
return 0;
}
关键点说明:
字符串数组的特殊性:
char arr[] = "abcdef"
会创建一个包含7个元素的数组:‘a’,‘b’,‘c’,‘d’,‘e’,‘f’,‘\0’- 所以
sizeof(arr)
是7而不是6数组名退化为指针:
- 在大多数表达式中,数组名会退化为指向其首元素的指针
- 但在
sizeof(arr)
和&arr
操作中不会退化指针大小:
- 32位系统:通常4字节
- 64位系统:通常8字节
- 示例输出假设是64位系统
指针类型差异:
arr+0
和&arr[0]+1
是char*
类型&arr
和&arr+1
是char(*)[7]
类型(指向整个数组的指针)- 但所有指针的
sizeof
结果相同
2.2.4 代码练习四
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int main()
{
char arr[] = "abcdef";
printf("%zu\n", strlen(arr));
printf("%zu\n", strlen(arr + 0));
printf("%zu\n", strlen(&arr));
printf("%zu\n", strlen(&arr + 1));
printf("%zu\n", strlen(&arr[0] + 1));
}
典型输出(64位系统):
6
6
6
26
5
注释解析如下:
int main()
{
// 定义并初始化一个字符串数组,包含6个字符和1个'\0'终止符
char arr[] = "abcdef"; // 等价于 {'a','b','c','d','e','f','\0'}
// 1. 计算字符串长度
printf("%zu\n", strlen(arr)); // 输出6
// arr作为数组名,在表达式中退化为指向首元素的指针
// strlen从'a'开始向后查找'\0',在'f'后找到'\0'
// 计算结果是6个字符
// 2. 计算从首元素开始的字符串长度
printf("%zu\n", strlen(arr + 0)); // 输出6
// arr + 0 仍然指向数组首元素
// 情况与第一个printf完全相同
// 结果也是6
// 3. 计算从数组地址开始的字符串长度
printf("%zu\n", strlen(&arr)); // 输出6
// &arr 是"指向整个数组的指针"(类型为 char(*)[7])
// 但它的值与arr相同,都是数组的起始地址
// 情况与前两个printf相同,结果也是6
// 4. 计算从数组末尾后一个位置开始的字符串长度
printf("%zu\n", strlen(&arr + 1)); // 未定义行为
// &arr + 1 跳过整个数组(7个字节),指向数组之后的内存位置
// strlen从这个新位置开始查找'\0'
// 这是未定义行为,可能返回随机值或导致程序崩溃
// 5. 计算从数组第二个元素开始的字符串长度
printf("%zu\n", strlen(&arr[0] + 1)); // 输出5
// &arr[0] + 1 指向数组的第二个元素'b'
// 从这个位置开始查找'\0',会找到'c','d','e','f','\0'
// 结果是5个字符
return 0;
}
关键点总结:
字符串终止符:
- 字符串"abcdef"实际上包含7个字节:6个字符+1个’\0’
strlen
计算的是’\0’前的字符数量指针运算:
arr
和arr+0
都指向首元素&arr
是整个数组的指针(类型不同但值相同)&arr + 1
跳过整个数组(7个字节)&arr[0] + 1
指向第二个元素安全与未定义行为:
- 前4个
printf
都是安全的,因为都在数组范围内查找’\0’strlen(&arr + 1)
是未定义行为,因为它访问了数组外的内存结果分析:
- 前三个输出都是6(完整字符串长度)
- 第五个输出是5(从第二个字符开始计算)
- 第四个输出不可预测
2.2.5 代码练习五
#include<stdio.h>
int main()
{
char* p = "abcdef";
printf("%zu\n", sizeof(p));
printf("%zu\n", sizeof(p + 1));
printf("%zu\n", sizeof(*p));
printf("%zu\n", sizeof(p[0]));
printf("%zu\n", sizeof(&p));
printf("%zu\n", sizeof(&p + 1));
printf("%zu\n", sizeof(&p[0] + 1));
}
典型输出(64位系统):
8 // 指针大小
8 // 指针大小
1 // char大小
1 // char大小
8 // 指针大小
8 // 指针大小
8 // 指针大小
注释解析如下:
#include<stdio.h>
int main()
{
char* p = "abcdef"; // 定义一个字符指针p,指向字符串常量"abcdef"
printf("%zu\n", sizeof(p)); // 1. 指针p本身的大小:在32位系统为4字节,64位系统为8字节
printf("%zu\n", sizeof(p + 1)); // 2. 指针运算结果的大小:p+1是指向第二个字符的指针,大小与p相同
printf("%zu\n", sizeof(*p)); // 3. 解引用指针得到字符'a'的大小:char类型为1字节
printf("%zu\n", sizeof(p[0])); // 4. 等同于*(p+0),即第一个字符'a'的大小:1字节
printf("%zu\n", sizeof(&p)); // 5. 取指针p的地址,得到指向指针的指针:大小仍为指针大小
printf("%zu\n", sizeof(&p + 1)); // 6. 指针p的地址加1,指向下一个指针位置:大小仍为指针大小
printf("%zu\n", sizeof(&p[0] + 1)); // 7. 等同于&(*(p+0))+1,即指向第二个字符的指针:大小仍为指针大小
return 0;
}
关键点说明:
指针大小:所有指针的大小相同,取决于系统架构(32位系统4字节,64位系统8字节)
字符串常量:
"abcdef"
是字符串常量,存储在只读内存区域,以’\0’结尾指针运算:
p + 1
:指向字符串的第二个字符’b’&p + 1
:指向内存中下一个指针位置&p[0] + 1
:等同于p + 1
,指向第二个字符解引用操作:
*p
和p[0]
都得到第一个字符’a’,大小为1字节
2.2.6 代码练习六
#include<stdio.h>
#include<string.h>
int main()
{
char* p = "abcdef";
printf("%zu\n", strlen(p));
printf("%zu\n", strlen(p + 1));
printf("%zu\n", strlen(&p));
printf("%zu\n", strlen(&p + 1));
printf("%zu\n", strlen(&p[0] + 1));
return 0;
}
运行结果如下:
6 // strlen(p)
5 // strlen(p+1)
随机值或崩溃 // strlen(&p)
随机值或崩溃 // strlen(&p+1)
5 // strlen(&p[0]+1)
注释解析如下
#include<stdio.h>
#include<string.h>
int main()
{
// 定义一个字符指针p,指向字符串常量"abcdef"
// 字符串常量会自动以'\0'结尾
char* p = "abcdef";
// 1. 计算字符串p的长度
printf("%zu\n", strlen(p));
// p指向字符串"abcdef"的首地址
// strlen从'a'开始计算,直到遇到'\0'为止
// 结果是6(a b c d e f 共6个字符)
// 2. 计算从p+1位置开始的字符串长度
printf("%zu\n", strlen(p + 1));
// p+1指向字符串的第二个字符'b'
// strlen从'b'开始计算,直到'\0'
// 结果是5(b c d e f 共5个字符)
// 3. 计算从指针p的地址开始的"字符串"长度
printf("%zu\n", strlen(&p));
// &p是指针变量p本身的地址,不是字符串地址
// strlen会将指针地址当作字符数组起始地址
// 这是未定义行为,结果不可预测
// 可能返回随机值或导致程序崩溃
// 4. 计算从&p+1位置开始的"字符串"长度
printf("%zu\n", strlen(&p + 1));
// &p+1是指向p变量之后的内存位置
// 同样不是有效的字符串地址
// 未定义行为,结果不可预测
// 5. 计算从第二个字符地址开始的字符串长度
printf("%zu\n", strlen(&p[0] + 1));
// &p[0]是字符串首字符'a'的地址
// &p[0]+1是第二个字符'b'的地址
// 等同于p+1,结果是5
return 0;
}
关键点总结:
字符串常量:
"abcdef"
是字符串常量,自动以\0
结尾strlen
可以正确计算其长度指针操作:
p
指向字符串首地址p+1
指向第二个字符&p
是指针变量的地址,不是字符串地址安全与危险操作:
- 安全操作:
p
和p+1
等指向字符串内部的指针- 危险操作:
&p
和&p+1
不是字符串地址,会导致未定义行为
2.3 二维数组
#include<stdio.h>
int main()
{
int a[3][4] = { 0 };
printf("%zu\n", sizeof(a));
printf("%zu\n", sizeof(a[0][0]));
printf("%zu\n", sizeof(a[0]));
printf("%zu\n", sizeof(a[0] + 1));
printf("%zu\n", sizeof(*(a[0] + 1)));
printf("%zu\n", sizeof(a + 1));
printf("%zu\n", sizeof(*(a + 1)));
printf("%zu\n", sizeof(&a[0] + 1));
printf("%zu\n", sizeof(*(&a[0] + 1)));
printf("%zu\n", sizeof(*a));
printf("%zu\n", sizeof(a[3]));
}
典型输出(64位系统):
48 // 整个数组
4 // 单个元素
16 // 一行
8 // 指针
4 // 元素
8 // 指针
16 // 一行
8 // 指针
16 // 一行
16 // 一行
16 // 一行(编译时计算)
注释解析如下:
#include<stdio.h>
int main()
{
int a[3][4] = { 0 }; // 定义一个3行4列的二维整型数组
printf("%zu\n", sizeof(a)); // 1. 整个数组的大小:3×4×4字节 = 48字节
printf("%zu\n", sizeof(a[0][0]));// 2. 单个元素的大小:int类型 = 4字节
printf("%zu\n", sizeof(a[0])); // 3. 第一行的大小:4个int = 16字节
// a[0]是第一行的数组名,不会退化为指针
printf("%zu\n", sizeof(a[0] + 1));// 4. 第一行第二个元素的地址:指针大小 = 8字节
// a[0]退化为指针,+1指向下一个元素
printf("%zu\n", sizeof(*(a[0] + 1)));// 5. 第一行第二个元素的大小:int = 4字节
printf("%zu\n", sizeof(a + 1)); // 6. 第二行的地址:指针大小 = 8字节
// a退化为指向第一行的指针,+1指向第二行
printf("%zu\n", sizeof(*(a + 1)));// 7. 第二行的大小:4个int = 16字节
// *(a+1)等价于a[1]
printf("%zu\n", sizeof(&a[0] + 1));// 8. 第二行的地址:指针大小 = 8字节
// &a[0]是第一行的地址,+1指向第二行
printf("%zu\n", sizeof(*(&a[0] + 1)));// 9. 第二行的大小:4个int = 16字节
// *(&a[0]+1)等价于a[1]
printf("%zu\n", sizeof(*a)); // 10. 第一行的大小:4个int = 16字节
// *a等价于a[0]
printf("%zu\n", sizeof(a[3])); // 11. 第四行的大小:4个int = 16字节
// 虽然a只有3行,但sizeof是编译时操作
// 不会实际访问a[3]
return 0;
}
关键点说明:
二维数组的内存布局:
int a[3][4]
在内存中是按行连续存储的12个int。数组名退化规则:
a
在大多数情况下退化为指向第一行的指针(类型为int(*)[4]
)a[0]
在大多数情况下退化为指向第一个元素的指针(类型为int*
)sizeof的特殊性:
- 在
sizeof(a)
和sizeof(a[0])
中,数组名不会退化为指针sizeof
是编译时操作,不会实际计算表达式指针运算:
a + 1
和&a[0] + 1
都指向第二行a[0] + 1
指向第一行的第二个元素
三. 指针运算笔试题解析
注: 以下试题请先思考
3.1 试题练习一
#include <stdio.h>
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1); // 关键点1
printf("%d,%d", *(a + 1), *(ptr - 1)); // 关键点2
return 0;
}
关键点解析:
&a + 1
:
a
是一个int数组,&a
的类型是int (*)[5]
(指向包含5个int的数组的指针)- 当对数组指针进行
+1
运算时,会跳过整个数组的大小(5*sizeof(int))- 所以
&a + 1
指向数组末尾之后的位置指针转换:
(int*)(&a + 1)
将这个数组指针强制转换为普通的int指针- 现在
ptr
是一个指向int的指针,指向数组末尾之后的位置输出表达式:
*(a + 1)
:
a
在表达式中退化为指向首元素的指针(int*)a + 1
指向第二个元素(值为2)- 所以
*(a + 1)
输出2
*(ptr - 1)
:
ptr
指向数组末尾之后的位置ptr - 1
回退一个int的大小,指向最后一个元素5- 所以
*(ptr - 1)
输出5
3.2 试题练习二
#include<stdio.h>
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000; // 初始化结构体指针p指向地址0x100000
int main()
{
printf("%p\n", p + 0x1); // 关键点1
printf("%p\n", (unsigned long)p + 0x1); // 关键点2
printf("%p\n", (unsigned int*)p + 0x1); // 关键点3
return 0;
}
关键点解析:
结构体大小计算:
struct Test
的大小取决于内存对齐(假设在32位系统上):
int Num
:4字节char* pcName
:4字节(指针)short sDate
:2字节char cha[2]
:2字节short sBa[4]
:8字节(4个short)- 总大小:4 + 4 + 2 + 2 + 8 = 20字节(假设编译器没有额外填充)
p + 0x1
:
p
是struct Test*
类型,指针算术以结构体大小为步长p + 1
会跳过整个结构体的大小(20字节)- 所以
p + 0x1
=0x100000
+0x14
(20的十六进制)=0x100014
(unsigned long)p + 0x1
:
- 将指针
p
强制转换为unsigned long
,变成普通整数- 整数加法直接加1:
0x100000
+0x1
=0x100001
(unsigned int*)p + 0x1
:
- 将
p
强制转换为unsigned int*
(指向4字节int的指针)- 指针算术以
unsigned int
大小(4字节)为步长(unsigned int*)p + 1
=0x100000
+0x4
=0x100004
3.3 试题练习三
#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) }; // 关键点1:注意这里用的是逗号表达式
int* p;
p = a[0]; // 关键点2:获取第一行的首地址
printf("%d", p[0]); // 关键点3:输出p[0]
return 0;
}
关键点解析:
数组初始化:
- 表面上看是初始化一个3行2列的二维数组
- 但实际上使用的是逗号表达式而不是常规的花括号初始化
- 在C语言中,
(x, y)
是逗号表达式,其值为最后一个表达式的值- 所以实际初始化值为:
(0, 1)
→ 1(2, 3)
→ 3(4, 5)
→ 5- 由于只提供了3个值,但数组需要6个值(3×2),剩余元素会被初始化为0
- 实际数组内容为:
a[0][0] = 1 a[0][1] = 3 a[1][0] = 5 a[1][1] = 0 (自动补0) a[2][0] = 0 (自动补0) a[2][1] = 0 (自动补0)
指针赋值:
a[0]
表示二维数组的第一行(即{1, 0}
)p = a[0]
将p指向第一行的第一个元素- 输出
p[0]
:
p[0]
等价于*(p+0)
,即第一个元素的值- 根据上面的初始化,
a[0][0]
的值为1
3.4 试题练习四
#include <stdio.h>
int main()
{
int a[5][5]; // 定义一个5x5的二维数组
int(*p)[4]; // 定义一个指向包含4个int的数组的指针
p = a; // 关键点1:将a强制转换为指向4元素数组的指针
printf("%p,%d\n",
&p[4][2] - &a[4][2], // 关键点2:指针相减
&p[4][2] - &a[4][2]); // 关键点3:结果转换为整数
return 0;
}
关键点解析:
指针类型转换:
a
是int[5][5]
类型,&a[0]
是int(*)[5]
(指向5个int的指针)p
是int(*)[4]
(指向4个int的指针)p = (int(*)[4])a
将5列的数组指针强制转换为4列的指针指针运算:
p[4]
相当于*(p + 4)
,会跳过4 * sizeof(int[4])
= 16个inta[4][2]
是第4行第2列元素(从0开始计数)地址计算:
&p[4][2]
=a + 4*4 + 2
=a + 18
(int单位)&a[4][2]
=a + 4*5 + 2
=a + 22
(int单位)&p[4][2] - &a[4][2]
=(a + 18) - (a + 22)
=-4
输出格式:
%p
会以指针形式输出-4(即0xFFFFFFFC
,补码表示)%d
直接输出-4
总结:
- 指针类型转换导致指针算术的步长不同(4 vs 5)
- 指针相减的结果是元素间隔数(-4表示p[4][2]在a[4][2]前面4个int位置)
- 这个例子展示了指针类型对算术运算的影响
3.5 试题练习五
#include <stdio.h>
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // 初始化2行5列的二维数组
int* ptr1 = (int*)(&aa + 1); // 关键点1
int* ptr2 = (int*)(*(aa + 1)); // 关键点2
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1)); // 关键点3
return 0;
}
关键点解析:
数组初始化:
aa
是一个2行5列的二维数组,内存布局如下:aa[0][0] = 1 aa[0][1] = 2 aa[0][2] = 3 aa[0][3] = 4 aa[0][4] = 5 aa[1][0] = 6 aa[1][1] = 7 aa[1][2] = 8 aa[1][3] = 9 aa[1][4] = 10
ptr1
的赋值:
&aa
是"指向整个二维数组的指针",类型是int (*)[2][5]
&aa + 1
会跳过整个数组(2×5=10个int),指向数组末尾之后的位置(int*)
强制转换为int指针,所以ptr1
指向数组末尾之后的位置
ptr2
的赋值:
aa + 1
:aa
会退化为指向第一行的指针(类型int (*)[5]
)aa + 1
指向第二行(即{6,7,8,9,10}
)*(aa + 1)
解引用得到第二行(类型int [5]
,会退化为指向第一个元素的指针)(int*)
强制转换是多余的,但结果不变输出表达式:
*(ptr1 - 1)
:
ptr1
指向数组末尾之后ptr1 - 1
回退一个int,指向最后一个元素aa[1][4]
(值为10)*(ptr2 - 1)
:
ptr2
指向第二行第一个元素aa[1][0]
(值为6)ptr2 - 1
回退一个int,指向aa[0][4]
(值为5)
总结:
&aa + 1
跳过整个二维数组aa + 1
跳过一行(5个int)- 指针运算时要注意指针的类型和步长
- 输出结果是最后一个元素
10
和第一行最后一个元素5
3.6 试题练习六
#include <stdio.h>
int main()
{
char* a[] = { "work","at","alibaba" }; // 关键点1:指针数组
char** pa = a; // 关键点2:二级指针指向数组首元素
pa++; // 关键点3:指针移动
printf("%s\n", *pa); // 关键点4:解引用输出
return 0;
}
关键点解析:
指针数组
char* a[]
:
a
是一个数组,包含3个char*
类型的指针元素- 每个指针指向一个字符串常量:
a[0]
指向"work"a[1]
指向"at"a[2]
指向"alibaba"二级指针
char** pa = a
:
a
在表达式中退化为指向首元素的指针(即char**
类型)pa
指向a[0]
,也就是指向"work"的指针指针运算
pa++
:
pa
是char**
类型,pa++
会使指针移动一个char*
的大小(通常4或8字节)- 移动后
pa
指向a[1]
,即指向"at"的指针解引用输出
*pa
:
*pa
获取pa
当前指向的值,即a[1]
(指向"at"的指针)printf
使用%s
格式打印该指针指向的字符串
总结:
- 指针数组
a
存储的是指向字符串常量的指针- 二级指针
pa
初始指向数组的第一个元素pa++
使指针移动到数组的第二个元素*pa
解引用得到指向"at"的指针,%s
打印出字符串"at"这个例子展示了指针数组和二级指针的配合使用,以及指针运算如何遍历指针数组。
3.7 试题练习七
#include <stdio.h>
int main()
{
// 初始化字符串指针数组
char* c[] = { "ENTER","NEW","POINT","FIRST" };
// 初始化指向c数组元素的指针数组
char** cp[] = { c + 3, c + 2, c + 1, c };
// 三级指针指向cp数组
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[0] -> "ENTER" c[1] -> "NEW" c[2] -> "POINT" c[3] -> "FIRST"
指针数组cp:
cp[0] = c + 3 -> &c[3] -> "FIRST" cp[1] = c + 2 -> &c[2] -> "POINT" cp[2] = c + 1 -> &c[1] -> "NEW" cp[3] = c -> &c[0] -> "ENTER"
三级指针:
cpp -> cp
逐步解析:
第一次输出:
**++cpp
++cpp
:cpp先自增,现在指向cp[1]
*cpp
:解引用得到cp[1] -> c + 2
**cpp
:再次解引用得到c[2] -> “POINT”输出:
POINT
第二次输出:
*-- * ++cpp + 3
++cpp
:cpp自增,现在指向cp[2]
*cpp
:解引用得到cp[2] -> c + 1
-- *cpp
:c + 1减1变为c + 0
*-- *cpp
:解引用得到c[0] -> “ENTER”
+3
:指针运算,指向"ENTER"的第3个字符’E’之后输出:
ER
(从第3个字符开始)第三次输出:
*cpp[-2] + 3
cpp[-2]
:相当于*(cpp - 2),即cp[0] -> c + 3
*cpp[-2]
:解引用得到c[3] -> “FIRST”
+3
:指针运算,指向"FIRST"的第3个字符’R’之后输出:
ST
(从第3个字符开始)第四次输出:
cpp[-1][-1] + 1
cpp[-1]
:相当于*(cpp - 1),即cp[1] -> c + 2
cpp[-1][-1]
:相当于*(*(cpp - 1) - 1) -> *(c + 2 - 1) -> c[1] -> “NEW”
+1
:指针运算,指向"NEW"的第1个字符’N’之后输出:
EW
(从第1个字符开始)
关键点总结:
多级指针的解引用需要从外向内逐步分析
指针运算会改变指针的指向位置
数组下标访问和指针运算可以互相转换(如
cpp[-1]
等价于*(cpp - 1)
)字符串指针加上偏移量会从指定位置开始输出
这个例子展示了C语言中复杂指针操作的强大能力,但也显示了这类代码容易造成混淆的特点。在实际开发中,建议使用更清晰的表达方式。
本篇内容到此结束 如果对你有所帮助 希望能一键三连 谢谢
往期回顾:
《初探指针世界:揭开内存管理与编程优化的第一篇章》-----指针一
《C 语言指针进阶:const 修饰、断言机制与传址调用深度解析》----指针二
《C 语言指针高级指南:字符、数组、函数指针的进阶攻略》----指针三
《从回调函数到 qsort:C 语言指针高级应用全攻略》----指针四