33 / 80 · C++11 · 约 9 分钟
读懂指针声明 Pointer declarators
读复杂声明要从名字出发,按括号与声明符的结合关系分层分析。数组指针、指针数组、函数指针并不等价;const 修饰哪一层也决定能否改指针或目标,用类型别名可以让接口更清晰。
从名字开始逐层读
先找到名字,再识别直接包围它的声明结构。int* slots[3] 中名字首先组成数组声明,因此 slots 是三个指针的数组;int (*row)[3] 的括号先把名字与星号结合,因此 row 是指向三个整数所成数组的指针。两者不仅语法不同,步进单位也不同。
函数同理:int* make() 声明返回整数指针的函数,int (*apply)(int) 声明指向函数的指针,目标函数接受整数并返回整数。不要依赖含糊的顺时针口诀;括号先确定层次,数组和函数后缀再决定这一层的类型。
const 需要逐层定位
const int* p 与 int const* p 都表示通过 p 不得修改整数,但 p 可重新指向别处。int* const p 则固定指针本身,仍允许修改目标。const int* const p 同时限制这两件事,不代表没有其他别名能修改一个原本非 const 的对象。
多级指针必须继续逐层分析。int** 不能直接转成 const int**,否则可以经由第二级指针把一个真正只读对象的地址塞进可写整数指针。增加一层 const 不是处处安全;让编译器拒绝这种转换是在保护写入权限。
用别名表达意图而非藏住复杂性
using Unary = int (*)(int) 把函数指针抽成可读名字,回调接口因此能直接写 Unary。别名不会创造新类型,也不改变 const 的语义:若 using P = int*,则 const P 是常量指针,而不是指向常量整数的指针。
示例同时操作指针数组、数组指针和函数指针,并用 static_assert 确认别名含义。声明列表中的星号属于各自声明符,int* a, b 只有 a 是指针。实际维护代码中一个声明写一个名字,通常比要求读者反复拆解更可靠。
容易答错的地方
int (*p)[3]不是int**;前者指向完整数组,不能用强制转换把二维数组伪装成后者。- 类型别名的 const 修饰整个别名类型,不能把别名当文本宏展开后再猜修饰对象。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <type_traits>
int increment(int value) { return value + 1; }
int main() {
int values[]{3, 5, 7};
int* slots[3]{&values[0], &values[1], &values[2]};
int (*row)[3] = &values;
using Unary = int (*)(int);
Unary apply = &increment;
assert(*slots[1] == 5);
assert((*row)[2] == 7);
assert(apply((*row)[0]) == 4);
using P = int*;
static_assert(std::is_same<const P, int* const>::value, "const qualifies pointer");
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread memory-pointer-declarations.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
解释 const int* table[2],并写出“指向含两个只读整数的数组的指针”。
查看参考答案
table 是长度为二的数组,每个元素是可改指向的指针,但不能通过这些指针修改整数。目标声明是 const int (*p)[2]。也可以写 using Row = const int[2]; Row* p = nullptr;;括号把 p 与星号结合,避免再次得到指针数组。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。