C++ 基础核心机制 (基于 C++ Primer 第2章)
摘要:涵盖变量声明与定义的区别、引用与指针的复合类型,以及 const 限定符的深度解析(顶层/底层 const)。
2.2 变量 (Variables)
1. 变量定义 vs 声明 (Definition vs Declaration)
C++ 支持分离编译机制,因此区分了声明和定义:
- 声明 (Declaration):告诉编译器名字和类型。
- 使用
extern关键字且不初始化。 - 示例:
extern int i;(仅仅是声明)
- 使用
- 定义 (Definition):负责创建与名字关联的实体(分配内存)。
- 示例:
int j;(声明并定义) - 示例:
extern int k = 0;(任何包含了显式初始化的声明即成为定义)
- 示例:
注意:变量只能被定义一次,但可以被声明多次。
2. 列表初始化 (List Initialization) (C++11)
使用花括号 {} 进行初始化。
int a = 0;int a = {0};int a{0};- 特点:如果使用列表初始化且存在丢失信息的风险(如
long double转int),编译器会报错。
2.3 复合类型 (Compound Types)
1. 引用 (References)
- 定义:为对象起的别名。
int &ref = val; - 核心特性:
- 必须初始化。
- 一旦绑定,无法重新绑定到其他对象。
- 引用不是对象,不占内存(逻辑上),没有引用的引用。
2. 指针 (Pointers)
- 定义:指向另外一种类型的复合类型。
int *p = &val; - 核心特性:
- 本身是对象,允许赋值和拷贝。
- 无需定义时初始化(但建议初始化)。
- 空指针:推荐使用
nullptr(C++11),避免使用NULL。 - void*:一种特殊的指针类型,可存放任意对象的地址,但不能直接操作其指向的对象(因为不知道类型)。
🔥 2.4 const 限定符
const 的核心作用是定义由编译器强制执行的只读约束。
1. 基础用法
必须初始化:
const对象一旦创建就不能修改,所以必须在定义时初始化。cppconst int i = 42; // ✅ 正确 // const int k; // ❌ 错误:未初始化作用域:默认情况下,
const对象仅在文件内有效。如果要在多个文件共享,需在声明和定义时都加上extern。
2. const 的引用 (Reference to const)
习惯称为“常量引用”(虽然不严谨,因为引用本身不是对象,无法设为常量)。
允许绑定字面值或表达式(普通引用不行):
cppconst int &r1 = 42; // ✅ 正确 const int &r2 = r1 * 2; // ✅ 正确 // int &r3 = 42; // ❌ 错误:非常量引用不能绑定字面值只读视角:通过
const引用通过别名访问对象时,不能修改它。但原对象本身如果不是const,依然可以通过其他途径修改。
3. 指针与 const (难点)
A. 指向常量的指针 (Pointer to const)
- 含义:不能通过该指针修改它所指向的对象。
- 写法:
const在*左边。 - 助记:“底层的 const”,指向的东西是常量。cpp
const int *p; // *p = 10; // ❌ 错误:不能修改指向的值 // p = &j; // ✅ 正确:可以修改 p 自己的指向
B. const 指针 (Const Pointer)
- 含义:指针本身是常量,一旦指向某地址,就不能指向别处。
- 写法:
const在*右边。 - 助记:“顶层的 const”,指针自己是常量。cpp
int *const p = &i; // p = &j; // ❌ 错误:不能修改 p 的指向 // *p = 10; // ✅ 正确:如果 i 不是 const,可以修改值
4. 顶层 const vs 底层 const (Top-level vs Low-level const)
这是《C++ Primer》中非常重要的概念区分,决定了对象拷贝时的规则。
| 术语 | 含义 | 适用对象 | 拷贝规则 |
|---|---|---|---|
| 顶层 const (Top-level) | 对象本身是常量 | 任意对象 (如 int, int*, 类的对象) | 无限制。拷贝时,顶层 const 被忽略(我只读不改,复制一份给你随便改,没问题)。 |
| 底层 const (Low-level) | 指向的对象是常量 | 指针、引用等复合类型 | 有限制。拷贝时,拷入和拷出的对象必须具有相同的底层 const 资格(或者非常量转常量)。 |
代码示例:
int i = 0;
int *const p1 = &i; // 顶层 const (p1 自身不能动)
const int ci = 42; // 顶层 const (ci 自身不能动)
const int *p2 = &ci; // 底层 const (p2 指向的内容不能动)
const int *const p3 = p2; // 靠右的是顶层,靠左的是底层
const int &r = ci; // 用于声明引用的 const 都是底层 const
// --- 拷贝测试 ---
i = ci; // ✅ 正确:拷贝 ci 的值给 i,顶层 const 被忽略
p2 = p3; // ✅ 正确:p2 和 p3 都有底层 const
// int *p4 = p2; // ❌ 错误:p2 包含底层 const,而 p4 没有。
// 如果允许赋值,那么通过 p4 就能修改 p2 指向的常量了,这破坏了承诺。5. constexpr 和常量表达式 (C++11)
常量表达式:值不会改变且在编译过程就能得到计算结果的表达式。
constexpr 变量:C++11 允许将变量声明为
constexpr,由编译器来验证它是否是一个常量表达式。cppconstexpr int mf = 20; constexpr int limit = mf + 1;constexpr 指针:
constexpr把它所定义的对象置为了顶层 const。
cppconst int *p = nullptr; // 指向常量的指针 (底层 const) constexpr int *q = nullptr; // 一个常量指针 (顶层 const) !!! 注意区别
难点解析:声明解读与 const 拷贝规则
1. 复杂声明解读:从右向左 (Right-to-Left)
书中原话核心:理解复杂声明最简单的方法是 “从变量名开始,从右向左读” (Start at the variable name and read from right to left)。
之所以容易混淆,是因为修饰符(* 和 &)是依附于变量名的,而不是依附于基本类型的。
🔍 解读法则
- 找到变量名:这是起点。
- 向右看(基本没有修饰符会出现在变量名右边,除了数组
[])。 - 向左看:由近及远,依次解读修饰符。
&读作 "是一个引用"。*读作 "指向..."。const读作 "是常量"。
- 最后看基本类型:读作 "的是 int/double..."。
💡 经典案例分析
案例 A:int *&r = p;
- 第1步 (找 r):
r是变量名。 - 第2步 (向左看 &):
&r->r是一个引用。 - 第3步 (再向左看 *):
*&r->r引用的是一个指针。 - 第4步 (看类型 int):
int *&r->r引用的是一个指向 int 的指针。 - 总结:
r是对指针p的引用。
案例 B:int *const p; (顶层 const)
- 第1步 (找 p):
p是变量名。 - 第2步 (向左看 const):
const p->p是一个常量。 - 第3步 (再向左看 *):
*const p->p是一个常量指针(一旦指向谁,就不能改指向)。 - 第4步 (看类型 int):
int *const p-> 这个常量指针指向 int。
案例 C:const int *p; (底层 const)
- 第1步 (找 p):
p是变量名。 - 第2步 (向左看 *):
*p->p是一个指针。 - 第3步 (再向左看 const int):指向的是 const int。
- 总结:
p指向一个 int 常量(不能通过 p 修改那个 int)。
2. 拷贝操作中的 const 规则 (Copy Rules)
在执行 a = b(拷贝/赋值)操作时,编译器会检查 const 属性。这里的核心规则取决于 const 是 顶层 (Top-level) 还是 底层 (Low-level)。
⚔️ 核心规则表
| const 类型 | 所在位置 | 规则口诀 | 解释 |
|---|---|---|---|
| 顶层 const (对象自己是常量) | int i = 0;int *const p = &i; | 直接无视 | 拷贝时,顶层 const 会被忽略。 (我只是自己不能变,但我把值复印一份给你,你爱怎么改怎么改,跟我没关系。) |
| 底层 const (指向的是常量) | const int *p;const int &r; | 严格限制 | 拷贝时,底层 const 必须保留。 (我有“只读”权限,拷给你之后,你也必须承诺“只读”。不能因为换了个人拿,就可以随便修改了。) |
💻 详细代码示例
假设有以下变量定义:
int i = 0;
const int ci = 42; // 顶层 const (ci 自己不能变)
int *const p1 = &i; // 顶层 const (p1 自己不能变)
const int *p2 = &ci; // 底层 const (p2 指向的东西不能变)✅ 场景 1:顶层 const 被忽略 (Ignored)
// 1. 简单的值拷贝
int a = ci;
// 正确:ci 是顶层 const。拷贝时,把 42 这个数给 a。
// a 只是一个普通 int,以后 a 想变成 100 都可以。ci 的“常量性”不影响 a。
// 2. 指针本身的拷贝
int *p3 = p1;
// 正确:p1 是顶层 const (常量指针)。
// 把 p1 存的地址拷贝给 p3。p3 是普通指针,以后 p3 可以指别人,跟 p1 没关系。❌ 场景 2:底层 const 必须匹配 (Enforced)
// 3. 试图丢掉底层 const 资格
int *p4 = p2;
// ❌ 错误!
// 分析:
// p2 有底层 const(它承诺不修改指向的对象)。
// p4 没有底层 const(它是一个普通指针,可以通过 *p4 修改对象)。
// 如果这行代码通过,我们就可以通过 p4 去修改 ci 的值了!这破坏了 p2 的承诺。
// 4. 只有读写权限匹配才行
const int *p5 = p2;
// ✅ 正确:p5 也有底层 const,大家都不修改,安全。⚠️ 场景 3:权限缩小(允许)vs 权限放大(禁止)
权限缩小 (Safe):非 const 转 const。
cppint x = 10; const int *p6 = &x; // ✅ 正确:x 是普通 int (读写)。p6 说“我只读”。 // 既然本来能读写,你只读当然没问题。权限放大 (Unsafe):const 转 非 const。
cppconst int y = 10; int *p7 = &y; // ❌ 错误:y 是只读的。p7 想拥有读写权限。禁止!
