Skip to content

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 doubleint),编译器会报错。

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 对象一旦创建就不能修改,所以必须在定义时初始化。

    cpp
    const int i = 42;      // ✅ 正确
    // const int k;        // ❌ 错误:未初始化
  • 作用域:默认情况下,const 对象仅在文件内有效。如果要在多个文件共享,需在声明和定义时都加上 extern

2. const 的引用 (Reference to const)

习惯称为“常量引用”(虽然不严谨,因为引用本身不是对象,无法设为常量)。

  • 允许绑定字面值或表达式(普通引用不行):

    cpp
    const 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 资格(或者非常量转常量)。

代码示例:

cpp
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,由编译器来验证它是否是一个常量表达式。

    cpp
    constexpr int mf = 20;
    constexpr int limit = mf + 1;
  • constexpr 指针

    • constexpr 把它所定义的对象置为了顶层 const。
    cpp
    const 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)。

之所以容易混淆,是因为修饰符(*&)是依附于变量名的,而不是依附于基本类型的。

🔍 解读法则

  1. 找到变量名:这是起点。
  2. 向右看(基本没有修饰符会出现在变量名右边,除了数组 [])。
  3. 向左看:由近及远,依次解读修饰符。
    • & 读作 "是一个引用"。
    • * 读作 "指向..."。
    • const 读作 "是常量"。
  4. 最后看基本类型:读作 "的是 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 必须保留


(我有“只读”权限,拷给你之后,你也必须承诺“只读”。不能因为换了个人拿,就可以随便修改了。)

💻 详细代码示例

假设有以下变量定义:

cpp
int i = 0;
const int ci = 42;     // 顶层 const (ci 自己不能变)
int *const p1 = &i;    // 顶层 const (p1 自己不能变)
const int *p2 = &ci;   // 底层 const (p2 指向的东西不能变)

✅ 场景 1:顶层 const 被忽略 (Ignored)

cpp
// 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)

cpp
// 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。

    cpp
    int x = 10;
    const int *p6 = &x; 
    // ✅ 正确:x 是普通 int (读写)。p6 说“我只读”。
    // 既然本来能读写,你只读当然没问题。
  • 权限放大 (Unsafe):const 转 非 const。

    cpp
    const int y = 10;
    int *p7 = &y; 
    // ❌ 错误:y 是只读的。p7 想拥有读写权限。禁止!

Released under the MIT License.