C++指针笔试题1
分析 char*(*p)[3][6];
的类型
p
是一个指针,它指向的类型是char*[3][6]
(一个 3 行 6 列、元素为char*
的二维数组)。
计算 sizeof(p)
p
是指针,在 32 位系统中,指针的大小固定为 4 字节,所以 sizeof(p) = 4
。
计算 sizeof(*p)
*p
表示 p
指向的对象,即 char*[3][6]
类型的二维数组。
- 这个二维数组有
3
行、6
列,每个元素是char*
(大小为 4 字节)。 - 数组总大小 = 行数 × 列数 × 每个元素的大小,即
3 × 6 × 4 = 72
,所以sizeof(*p) = 72
。
计算 sizeof(**p)
**p
表示二维数组中第一行的数组(类型为 char*[6]
,即 1 行 6 列、元素为 char*
的一维数组)。
- 该一维数组有
6
个元素,每个元素是char*
(大小为 4 字节)。 - 数组总大小 = 列数 × 每个元素的大小,即
6 × 4 = 24
,所以sizeof(**p) = 24
。
为什么会有 *p
和 **p
?
指针的解引用(*
)本质是 “获取指针指向的对象”,而对象的类型决定了 “是否可以继续解引用”:
p
的类型:char*(*)[3][6]
(指向二维数组的指针)p
存储的是 “整个二维数组的起始地址”,解引用*p
得到的是p
指向的二维数组本身(类型为char*[3][6]
)。注意:这个二维数组虽然是 “实体”,但在多数语境下会隐式退化—— 作为 “由多个行数组组成的集合”,它会退化为指向 “第一行数组” 的指针(类似一维数组名退化为指向首元素的指针)。其实,退化是常态,** 只要*p
离开 “不会退化的特殊语境”(如sizeof(*p)
、&*p
),就会自动退化 **。*p
的类型:char*[3][6]
(二维数组)→ 退化后为char*(*)[6]
(指向行数组的指针)当*p
退化为 “指向第一行数组的指针” 时,对其再次解引用(**p
),得到的就是 这个指针指向的对象—— 即二维数组的第一行数组(类型为char*[6]
)。
最终输出
代码会依次输出:
plaintext
4
72
24
备注:
1、
#include <stdio.h>int main() {int arr[5] = {1,2,3,4,5};int (*p)[5] = &arr; // 指向数组的指针(数组指针)int* q = arr; // 指向int的指针(普通指针)// 1. 指针本身的sizeof:结果相同printf("sizeof(p) = %zu\n", sizeof(p)); // 32位系统输出4,64位输出8printf("sizeof(q) = %zu\n", sizeof(q)); // 结果和上面完全一致// 2. 指针指向对象的sizeof:结果不同(类型差异的体现)printf("sizeof(*p) = %zu\n", sizeof(*p)); // *p是整个数组,大小=5*4=20printf("sizeof(*q) = %zu\n", sizeof(*q)); // *q是单个int,大小=4return 0;
}
2、
1. 数组名 arr
不是指针,而是 “数组类型的标识符”
数组名代表的是整个数组的实体,而非指针变量。例如:
int arr[5]; // arr 是 "int[5]" 类型的数组,不是指针
- 指针是独立的变量,存储内存地址,可被赋值修改(如
int* p; p = &x;
); - 数组名是 “常量标识符”,不能被赋值(如
arr = &x;
会报错),它与数组在内存中的存储绑定,本身不占用额外内存(指针变量则占用内存)。
2. 数组名可以用 sizeof
,因为 sizeof
识别数组类型
sizeof(arr)
计算的是整个数组的总字节数,这是因为 sizeof
是编译期运算符,能直接识别数组的完整类型(如 int[5]
):
int arr[5];
printf("%zu", sizeof(arr)); // 输出 20(5个int,每个4字节)
- 若
arr
是指针,sizeof(arr)
会输出指针大小(如 8 字节),但实际输出的是数组总大小,证明arr
不是指针。 - 只有数组名作为
sizeof
的操作数时,才不会退化(保持数组类型),这是sizeof
对数组的特殊处理。
3. 数组名 “退化” 为指针的原因:简化数组访问
在多数语境下(除 sizeof(arr)
、&arr
等少数情况),数组名会隐式转换为指向首元素的指针(类型为 元素类型*
),这是 C 语言设计的便利性:
int arr[5];
int* p = arr; // 等价于 p = &arr[0],arr 退化为 int* 类型
退化的目的是方便通过指针访问数组元素(如 arr[i]
等价于 *(arr + i)
),避免每次都手动写 &arr[0]
。
常见的退化场景:
- 数组名作为函数参数(如
void func(int arr[])
中,arr
实际是int*
指针); - 数组名参与指针运算(如
arr + 1
指向arr[1]
); - 数组名赋值给同类型指针变量(如上面的
p = arr
)。
4. 数组名arr只占编译时内存不占运行时内存。
- 若问 “编译阶段
arr
是否占内存”:是,它作为编译器符号表的一条记录,占用编译器进程的内存(但这是编译器的工作内存,与你的程序无关)。 - 若问 “程序运行时
arr
是否占内存”:否,此时只有数组的实体数据在内存中,arr
这个名字已被编译器转换为具体地址,不再以独立形式存在。
5.若 q
是 int*
类型,&q
的类型就固定为 int**
,不能用 int*
类型的 r
接收,否则编译器会报错(类型不匹配)。
6.int *p[2][5]
和 int (*p)[2][5]
—— 完全不一样
这两个声明的本质、p
的类型、用途都截然不同,核心区别是 p
是 “数组” 还是 “指针”。
1. int *p[2][5]
—— 二维数组(元素为 int*
)
- 结合逻辑:
[]
的优先级高于*
,所以p
先与[2][5]
结合,声明为 “2 行 5 列的二维数组”;数组的每个元素是int*
(指向int
的指针)。 - 类型:
p
是int*[2][5]
类型(二维数组,元素为一级指针)。 - 内存占用:数组大小 = 行数 × 列数 × 每个元素大小(如 64 位系统中,
int*
占 8 字节,总大小 = 2×5×8 = 80 字节)。 - 示例:可存储 10 个
int*
指针,每个指针指向一个int
变量或数组。
[]
先绑定 p
确定其为数组,*
再限定数组元素的指针属性,int
最后限定指针指向的类型。
2. int (*p)[2][5]
—— 三维数组指针(指向 int[2][5]
类型的二维数组)
- 结合逻辑:括号
()
强制p
先与*
结合,声明为 “指针”;指针指向的类型是int[2][5]
(2 行 5 列的二维数组)。 - 类型:
p
是int (*)[2][5]
类型(指向二维数组的指针,属于 “数组指针”)。 - 内存占用:指针本身的大小(与系统位数相关,64 位系统占 8 字节,32 位占 4 字节),与指向的数组大小无关。
- 示例:需指向一个已存在的
int[2][5]
二维数组(如int arr[2][5]; p = &arr;
)。
7.int *p
、int (*p)
和 int* p
这三种写法中,int *p
和 int* p
完全等价,而 int (*p)
与前两者在 “语法效果上一致(均为一级指针),但括号冗余且不常用”—— 核心区别仅在于 *
与 int
/p
的排版方式,以及是否存在冗余括号,本质都是 “声明一个指向 int
类型的一级指针 p
”。
1. 先明确:C/C++ 中 *
是 “跟随变量” 的,而非 “跟随类型”
首先要纠正一个常见误区:int*
不是一个 “整体类型”(虽然我们常口头说 “int*
类型的指针”),在语法层面,*
是 “绑定到变量名” 的运算符。比如 int* p1, p2;
实际声明的是:p1
是 int*
指针,p2
是普通 int
变量 —— 因为 *
只绑定到 p1
,没绑定到 p2
。这也是为什么 int *p
(*
靠近 p
)和 int* p
(*
靠近 int
)在语法上完全等价:无论 *
排版在哪里,它最终都是与变量 p
结合,声明 p
是指针。
2. 逐一分析三种写法
(1)int *p
和 int* p
—— 完全等价
- 核心逻辑:两者都是声明 “变量
p
是指向int
类型的一级指针”,唯一区别是*
的排版位置(靠近p
还是靠近int
)。 - 编译效果:编译器会将两者解析为完全相同的代码,无任何区别。
- 示例:
int a = 10; int *p1 = &a; // 正确,p1 是 int* 指针 int* p2 = &a; // 正确,p2 也是 int* 指针,与 p1 完全等价 printf("%d %d", *p1, *p2); // 均输出 10
- 开发习惯:两种写法都很常见,
int *p
更强调 “p
是指针”,int* p
更强调 “p
的类型是int*
”,但本质无差异。
(2)int (*p)
—— 语法有效,但括号冗余
- 核心逻辑:括号
()
强制p
先与*
结合,但由于没有其他高优先级运算符(如数组下标[]
、函数()
),括号没有实际作用,最终效果与int *p
一致。 - 编译效果:编译器会忽略冗余的括号,将其解析为 “
p
是int*
指针”,与前两种写法无区别。 - 示例:
int a = 20; int (*p3) = &a; // 正确,p3 是 int* 指针,括号冗余 printf("%d", *p3); // 输出 20
- 注意:实际开发中很少这么写,因为括号不仅没有必要,还会略微降低代码可读性(容易让人误以为有特殊逻辑)。
8.*p
的退化不是 “在取 **p
时才发生”,而是在多数语境下(包括作为 *
运算符的操作数时)会自动发生,**p
只是利用了这种退化的结果。
具体拆解:*p
的退化时机
当 p
是 char*(*)[3][6]
类型(指向二维数组的指针)时:
*p
得到的是char*[3][6]
类型的二维数组(p
指向的实体)。此时,只要*p
出现在 “需要指针的语境” 中(如赋值给指针变量、参与指针运算、作为*
的操作数等),就会立即退化为char*(*)[6]
类型(指向第一行数组的指针)。当执行
**p
时:- 第一步:
*p
先退化(因为它作为*
运算符的操作数,处于 “需要指针的语境”),变成指向第一行的指针(char*(*)[6]
类型); - 第二步:对这个 “指向第一行的指针” 解引用(
*
),得到第一行数组(char*[6]
类型)。
- 第一步:
关键:退化是 “语境触发”,而非 “显式取 *
时才触发”
*p
的退化不是 “等待 **p
时才发生”,而是:** 只要 *p
离开 “不会退化的特殊语境”(如 sizeof(*p)
、&*p
),就会自动退化 **。例如,即使不写 **p
,单独使用 *p
时也会退化:
char* arr[3][6];
char*(*p)[3][6] = &arr; // p 指向二维数组char*(*q)[6] = *p; // 正确:*p 退化后是 char*(*)[6] 类型,可直接赋值给 q
这里 *p
并没有参与 **p
运算,但依然发生了退化(否则无法赋值给 q
)。
总结
退化的触发时机很普遍:*p
的退化不是 “在取 **p
时才发生”,而是在大多数语境下(包括作为 *
运算符的操作数)会自动发生,**p
只是对退化后的指针再次解引用的结果。简单说:退化是 *p
自身在多数语境下的特性,**p
是利用了这个特性进行的二次操作。