彻底搞懂 `const` 指针:`const int*` 和 `int* const` 到底有什么区别?

各位编程爱好者,大家好!

今天,我们将深入探讨 C++ 中一个既基础又极易混淆的概念:const 指针。它在 C++ 的类型系统中扮演着至关重要的角色,是编写安全、高效和可维护代码的基石之一。然而,由于其语法上的细微差别,许多开发者,包括经验丰富的老兵,也常常会在 const int*int* const 之间感到困惑。

作为一名编程专家,我的目标是彻底揭开 const 指针的神秘面纱。我们将从 const 的基本哲学开始,逐步深入到指针的基础知识,然后详细剖析这两种常见的 const 指针类型,并探讨它们的变体、实际应用以及一些高级话题和最佳实践。请准备好您的注意力,我们即将踏上这段旅程。


一、const 的哲学:不变性与安全性

在深入指针之前,我们首先要理解 const 关键字的本质。const,即 "constant"(常量)的缩写,其核心思想是不变性(Immutability)。当我们将一个变量声明为 const 时,我们是在向编译器和未来的代码维护者承诺:这个变量的值在其生命周期内将保持不变。

为什么我们需要不变性?

  1. 代码安全性与健壮性: 防止意外修改。想象一个函数接收一个重要配置参数,如果这个参数可以被函数内部随意修改,那么就可能导致程序行为异常。const 能够强制执行“只读”权限,避免这类错误。
  2. 代码意图表达: const 是一种强大的自我文档工具。当您看到一个 const 声明时,您立即知道这个数据不应该被修改。这使得代码更易于理解和维护。
  3. 编译器优化: 编译器知道 const 变量的值不会改变,这为它提供了更多的优化机会,例如将变量存储在只读内存区域,或者在某些情况下直接进行常量折叠(constant folding)。
  4. 多线程安全: 在并发编程中,不变的数据是天生线程安全的,无需额外的同步机制。const 能够帮助我们识别和利用这些线程安全的数据。
  5. 接口设计: 在函数参数和返回值中使用 const 是设计良好接口的关键。例如,一个读取数据的函数不应该修改它所读取的数据,通过 const 指针或引用可以强制实现这一点。

基本 const 变量:

#include <iostream>

int main() {
    // 声明一个整型常量
    const int max_attempts = 3;
    // max_attempts = 4; // 编译错误:assignment of read-only variable 'max_attempts'

    std::cout << "Max attempts: " << max_attempts << std::endl;

    // 声明一个常量引用
    int value = 100;
    const int& const_ref = value;
    // const_ref = 200; // 编译错误:assignment of read-only reference 'const_ref'

    // 可以通过原变量修改值
    value = 150;
    std::cout << "Value via const_ref: " << const_ref << std::endl; // 输出 150

    return 0;
}

在这个例子中,max_attempts 的值被固定为 3,任何试图修改它的操作都会导致编译错误。常量引用 const_ref 允许您通过它读取 value,但不能修改 value。这展示了 const 在非指针上下文中的基本行为。现在,让我们将这个概念扩展到指针。


二、指针基础:理解内存地址的艺术

在讨论 const 指针之前,我们必须确保对 C++ 指针的基础有扎实的理解。指针本质上是一个变量,它的值是另一个变量的内存地址。通过指针,我们可以间接地访问和操作它所指向的变量。

指针的声明、初始化与解引用:

#include <iostream>

int main() {
    int score = 95; // 声明一个整型变量

    // 声明一个指向 int 的指针
    // int* ptr;
    // 初始化指针,使其指向 score 的内存地址
    int* ptr = &score; 

    std::cout << "变量 score 的值: " << score << std::endl;
    std::cout << "变量 score 的内存地址: " << &score << std::endl;
    std::cout << "指针 ptr 的值 (score 的地址): " << ptr << std::endl;
    std::cout << "通过指针 ptr 解引用访问 score 的值: " << *ptr << std::endl;

    // 通过指针修改 score 的值
    *ptr = 100;
    std::cout << "修改后 score 的值: " << score << std::endl; // 输出 100

    // 让指针指向另一个变量
    int another_score = 88;
    ptr = &another_score; // 指针 ptr 现在指向 another_score
    std::cout << "指针 ptr 现在指向的变量的值: " << *ptr << std::endl; // 输出 88

    return 0;
}

从上述代码中我们可以看到:

  • int* ptr; 声明了一个名为 ptr 的指针,它被设计来存储 int 类型变量的地址。
  • ptr = &score;score 变量的内存地址赋给 ptr
  • *ptr解引用操作符,它访问 ptr 所指向的内存地址处的值。
  • 我们可以通过 *ptr = 100; 来修改 ptr 所指向的变量的值。
  • 我们可以通过 ptr = &another_score; 来改变指针 ptr 本身的值,使其指向另一个变量。

理解这两点——修改指针所指向的值*ptr = ...)和修改指针本身的值ptr = ...)——是区分不同 const 指针类型的关键。


三、const 关键字在指针中的位置:核心区别的源头

在 C++ 中,const 关键字可以出现在指针声明的两个主要位置,这直接导致了两种截然不同的含义:

  1. const 作用于指针所指向的值(即数据)。
  2. const 作用于指针本身(即地址)。

为了帮助记忆,一个常用的经验法则是“从右往左读”,或者更直观地理解为“*`` 号的左边和右边**”。

  • 如果 const 出现在 *左边,它修饰的是指针所指向的数据,表示数据是常量。
  • 如果 const 出现在 *右边,它修饰的是指针本身,表示指针是常量。

让我们逐一详细剖析这两种情况。


四、const int*:指向常量的指针 (Pointer to const)

const int*,有时也写成 int const* (含义完全相同),表示一个指向 const int 的指针。这意味着:

  • 不能通过这个指针来修改它所指向的 int 类型数据的值。数据被视为常量。
  • 可以修改指针本身,使其指向不同的 int 类型数据(无论是常量还是非常量)。

解析语法:
我们可以这样理解 const int* p;

  1. *p 表示 p 是一个指针。
  2. int 表示 p 指向一个 int 类型的数据。
  3. const 出现在 int 的左边(也就是 * 的左边),它修饰的是 int,表示这个 intconst 的。

类比: 想象您有一张地图,上面标记着一个房子的位置。这是一张“只读”地图,您可以根据地图找到房子,但不能通过修改地图来改变房子本身(比如把房子从三层改成两层)。然而,您可以把这张地图扔掉,换一张新的地图来指示另一所房子的位置。

代码示例:

#include <iostream>

int main() {
    int value1 = 10;
    int value2 = 20;

    // 声明一个指向常量的指针
    const int* ptr_to_const = &value1;

    std::cout << "初始时,ptr_to_const 指向 value1" << std::endl;
    std::cout << "ptr_to_const 指向的值: " << *ptr_to_const << std::endl; // 输出 10

    // 尝试通过 ptr_to_const 修改它指向的数据 (编译错误)
    // *ptr_to_const = 15; 
    // 编译错误信息可能类似:assignment of read-only location '*ptr_to_const'

    // 但是,我们可以通过原始变量修改数据
    value1 = 15;
    std::cout << "通过原始变量修改后,ptr_to_const 指向的值: " << *ptr_to_const << std::endl; // 输出 15

    // 我们可以修改指针本身,让它指向另一个变量
    ptr_to_const = &value2; 
    std::cout << "修改指针指向后,ptr_to_const 指向的值: " << *ptr_to_const << std::endl; // 输出 20

    // 也可以指向一个真正的常量
    const int const_value = 30;
    ptr_to_const = &const_value;
    std::cout << "指向一个常量后,ptr_to_const 指向的值: " << *ptr_to_const << std::endl; // 输出 30

    return 0;
}

关键点总结:

  • const int* p;
  • *p(解引用 p 得到的值)是 const 的,不可修改。
  • p(指针本身)是可变的,可以指向其他地址。

类型转换与安全性:

C++ 对 const 指针的类型转换有严格的规定,以维护 const 的不变性承诺。

  • int* 可以隐式转换为 const int* 这是安全的,因为您只是在增加限制(从可写变为只读)。
    int non_const_var = 100;
    int* ptr_non_const = &non_const_var;
    const int* ptr_to_const_from_non_const = ptr_non_const; // 允许,安全
    // *ptr_to_const_from_non_const = 200; // 编译错误
  • const int* 无法隐式转换为 int* 这是不安全的,因为您试图移除一个限制(从只读变为可写)。如果允许,您就可以通过 int* 修改原本是 const 的数据,从而违反 const 的承诺。
    const int const_var = 50;
    const int* ptr_to_const_var = &const_var;
    // int* ptr_non_const_from_const = ptr_to_const_var; // 编译错误
    // 错误信息可能类似:invalid conversion from 'const int*' to 'int*'

    如果您确实需要移除 const 限制(通常在与遗留代码交互或有特殊保证时),必须使用 const_cast。但请注意,const_cast 是一种危险的操作,如果原始变量本身就是 const 的,通过 const_cast 去修改它会导致未定义行为(Undefined Behavior)。我们将在后面详细讨论 const_cast

int const*const int*

再次强调,int const* p;const int* p; 是完全等价的。const 关键字可以放在类型名的左边或右边,只要在 * 的左边即可。

int value = 42;
const int* p1 = &value; // 推荐写法
int const* p2 = &value; // 同样有效,但不如 p1 常用

// p1 和 p2 的行为完全一致
// *p1 = 43; // 编译错误
// *p2 = 43; // 编译错误
// p1 = &another_value; // 允许
// p2 = &another_value; // 允许

在现代 C++ 中,const int* 更为普遍和推荐,因为它将 const 作为类型修饰符放在最前面,增强了可读性。


五、int* const:常量指针 (Constant Pointer)

int* const 表示一个指向 int 的常量指针。这意味着:

  • 可以通过这个指针来修改它所指向的 int 类型数据的值。数据是可变的。
  • 不能修改指针本身,使其指向不同的 int 类型数据。指针一旦初始化,就不能再指向其他地址。

解析语法:
我们可以这样理解 int* const p;

  1. p 是一个 const 的变量。
  2. * 表示 p 是一个指针。
  3. int 表示 p 指向一个 int 类型的数据。
    结合起来,p 是一个 const 的指针,它指向一个 int

类比: 想象一个刻有地址的门牌号,它固定地钉在某扇门上。你可以打开这扇门,改变屋子里的摆设(修改数据),但你不能把这个门牌号从这扇门上取下来,然后钉到另一扇门上(不能改变指针指向)。

代码示例:

#include <iostream>

int main() {
    int value1 = 10;
    int value2 = 20;

    // 声明一个常量指针
    // 注意:常量指针必须在声明时初始化
    int* const const_ptr = &value1;

    std::cout << "初始时,const_ptr 指向 value1" << std::endl;
    std::cout << "const_ptr 指向的值: " << *const_ptr << std::endl; // 输出 10

    // 我们可以通过 const_ptr 修改它指向的数据
    *const_ptr = 15;
    std::cout << "通过 const_ptr 修改后,value1 的值: " << value1 << std::endl; // 输出 15
    std::cout << "const_ptr 指向的值: " << *const_ptr << std::endl; // 输出 15

    // 尝试修改指针本身,让它指向另一个变量 (编译错误)
    // const_ptr = &value2; 
    // 编译错误信息可能类似:assignment of read-only variable 'const_ptr'

    return 0;
}

关键点总结:

  • int* const p;
  • *p(解引用 p 得到的值)是可变的,可以修改。
  • p(指针本身)是 const 的,不可修改(一旦初始化)。

重要提示: 由于常量指针本身的值不能改变,它必须在声明时进行初始化

int* const my_ptr; // 编译错误:'my_ptr' declared as a const variable must be initialized

六、const int* const:指向常量的常量指针 (Constant Pointer to const)

const 关键字同时出现在 * 的左边和右边时,它意味着既不能通过指针修改数据,也不能修改指针本身。这是最严格的 const 指针类型。

解析语法:
我们可以这样理解 const int* const p;

  1. p 是一个 const 的变量(最右边的 const 修饰 p)。
  2. * 表示 p 是一个指针。
  3. int 表示 p 指向一个 int 类型的数据。
  4. 最左边的 const 修饰 int,表示这个 intconst 的。

结合起来,p 是一个 const 的指针,它指向一个 constint

类比: 这是一个刻有地址的门牌号,固定地钉在一扇门上,而且这扇门后面的屋子里所有东西都是用胶水固定住的,你不能移动它们。你不能换门牌号,也不能改变屋内的摆设。

代码示例:

#include <iostream>

int main() {
    int value1 = 10;
    int value2 = 20;

    // 声明一个指向常量的常量指针
    // 必须在声明时初始化
    const int* const const_ptr_to_const = &value1;

    std::cout << "初始时,const_ptr_to_const 指向 value1" << std::endl;
    std::cout << "const_ptr_to_const 指向的值: " << *const_ptr_to_const << std::endl; // 输出 10

    // 尝试通过 const_ptr_to_const 修改它指向的数据 (编译错误)
    // *const_ptr_to_const = 15; 
    // 编译错误信息可能类似:assignment of read-only location '*const_ptr_to_const'

    // 尝试修改指针本身,让它指向另一个变量 (编译错误)
    // const_ptr_to_const = &value2; 
    // 编译错误信息可能类似:assignment of read-only variable 'const_ptr_to_const'

    // 可以通过原始变量修改数据(如果原始变量不是const)
    value1 = 100;
    std::cout << "通过原始变量修改后,const_ptr_to_const 指向的值: " << *const_ptr_to_const << std::endl; // 输出 100

    return 0;
}

关键点总结:

  • const int* const p;
  • *p(解引用 p 得到的值)是 const 的,不可修改。
  • p(指针本身)是 const 的,不可修改(一旦初始化)。

七、const 放置规则的深层理解

我们已经通过 * 的左右侧来区分 const 的作用,现在让我们更系统地理解 const 关键字在 C++ 声明中的通用规则。这个规则不仅适用于指针,也适用于其他复杂的类型声明。

核心规则:const 修饰它左边的内容,除非它是最左边的,此时它修饰它右边的内容。

让我们用这个规则来分析前面提到的各种情况:

  1. int value;

    • value 是一个 int
  2. const int value; (等价于 int const value;)

    • const 在最左边,所以它修饰右边的 int
    • 结果:value 是一个 const int
  3. *`int ptr;`**

    • *ptr 是一个 int。所以 ptr 是一个指向 int 的指针。
  4. *`const int ptr;** (等价于int const* ptr;`)

    • const 修饰 int
    • 结果:*ptr 是一个 const int。所以 ptr 是一个指向 const int 的指针。
    • ptr 本身没有被 const 修饰,所以 ptr 是可变的。
  5. *`int const ptr;`**

    • const 修饰 ptr
    • 结果:ptr 本身是 const 的。
    • *ptr 是一个 int
    • 所以 ptr 是一个 const 指针,指向一个 int
  6. *`const int const ptr;`**

    • 最右边的 const 修饰 ptr
    • 最左边的 const 修饰 int
    • 结果:ptr 是一个 const 指针,*ptr 是一个 const int

这个规则在处理更复杂的声明时尤其有用,例如指向指针的指针:

int x = 10;
int* p = &x;

// 指向 int 的指针的指针 (p_ptr 是一个指向 int* 的指针)
int** p_ptr = &p; 
std::cout << "**p_ptr: " << **p_ptr << std::endl; // 输出 10

// ----------------------------------------------------

// 指向 (指向 const int 的指针) 的指针
// const int** p_ptr_to_const_int_ptr;
// 允许改变 p_ptr_to_const_int_ptr
// 允许改变 *p_ptr_to_const_int_ptr (即 int* 指针本身)
// 不允许改变 **p_ptr_to_const_int_ptr (即 int 值)
const int** p_ptr_to_const_int_ptr; 
// 例如:
const int* p_const_int = &x;
p_ptr_to_const_int_ptr = &p_const_int;
// **p_ptr_to_const_int_ptr = 20; // 编译错误

// ----------------------------------------------------

// 指向 (const int* (常量指针)) 的指针
// int** const p_ptr_to_const_ptr; // 错误,不能这样写
// 应该这样理解:int* const* p_ptr_to_const_ptr;
// 允许改变 p_ptr_to_const_ptr
// 允许改变 **p_ptr_to_const_ptr (即 int 值)
// 不允许改变 *p_ptr_to_const_ptr (即指向 int 的指针本身)
int* const* ptr_to_const_ptr_to_int; 
int y = 30;
int* const const_ptr_to_y = &y;
ptr_to_const_ptr_to_int = &const_ptr_to_y;
// *ptr_to_const_ptr_to_int = &x; // 编译错误,不能改变指向的 const_ptr_to_y
**ptr_to_const_ptr_to_int = 40; // 允许,改变 y 的值

// ----------------------------------------------------

// 指向 int 的常量指针 的 常量指针
// int* const* const p_const_ptr_to_const_ptr;
// 不允许改变 p_const_ptr_to_const_ptr
// 不允许改变 *p_const_ptr_to_const_ptr
// 允许改变 **p_const_ptr_to_const_ptr
int* const* const const_ptr_to_const_ptr_to_int = &const_ptr_to_y;
// const_ptr_to_const_ptr_to_int = &p_const_int; // 编译错误
// *const_ptr_to_const_ptr_to_int = &x; // 编译错误
**const_ptr_to_const_ptr_to_int = 50; // 允许,改变 y 的值

这种多级指针和 const 的组合非常复杂,但核心规则始终有效。实践中,过多的 const 嵌套可能会降低可读性,因此在设计时应权衡。


八、const 指针在函数参数中的应用

在函数参数中使用 const 指针是 C++ 中实现 const 正确性的最常见和最重要的应用之一。它允许函数接收数据,同时向调用者保证这些数据不会被函数内部修改。

*1. 传递指向常量的指针 (`const Type`)**

这是最常见和推荐的用法,尤其当您想避免复制大型对象,并且函数仅需要读取数据时。

#include <iostream>
#include <vector>

// 函数 func1 接收一个指向常量的 int 指针
// 它保证不会修改 ptr_data 所指向的 int 值
void print_value(const int* ptr_data) {
    if (ptr_data) {
        std::cout << "Value: " << *ptr_data << std::endl;
        // *ptr_data = 100; // 编译错误:不能修改指向的数据
    } else {
        std::cout << "Null pointer provided." << std::endl;
    }
}

// 接收一个指向常量的 vector<int> 指针
void print_vector_elements(const std::vector<int>* vec_ptr) {
    if (vec_ptr) {
        std::cout << "Vector elements: ";
        for (int val : *vec_ptr) { // 解引用 vector<int>* 得到 vector<int>
            std::cout << val << " ";
        }
        std::cout << std::endl;
        // (*vec_ptr)[0] = 99; // 编译错误:不能修改 vector 的元素
    }
}

int main() {
    int my_int = 42;
    print_value(&my_int); // 传入非 const 变量的地址

    const int const_int = 100;
    print_value(&const_int); // 传入 const 变量的地址 (同样允许)

    std::vector<int> my_vec = {1, 2, 3, 4, 5};
    print_vector_elements(&my_vec);

    return 0;
}

优点:

  • 安全性: 编译器强制执行只读访问,防止函数内部意外修改外部数据。
  • 灵活性: 可以接受指向 const 对象的指针,也可以接受指向非 const 对象的指针。
  • 效率: 避免了大数据对象的拷贝开销。
  • 清晰的接口: 函数签名明确表达了其不修改参数数据的意图。

*2. 传递常量指针 (`Type const`)**

这种用法相对不常见,因为 C++ 默认就是值传递指针。当指针作为参数传递时,它本身是按值拷贝的,函数内部对指针的修改(使其指向另一个地址)不会影响到调用者传入的原始指针。因此,将参数声明为 Type* const 并没有太大意义,因为它只限制了函数内部的局部指针变量,而这个局部变量的生命周期仅限于函数内部。

#include <iostream>

// 参数是一个常量指针。这意味着在函数内部,ptr_data 无法被修改以指向另一个地址。
// 但 ptr_data 所指向的数据可以被修改。
void modify_value_through_const_ptr_param(int* const ptr_data) {
    if (ptr_data) {
        std::cout << "Original value: " << *ptr_data << std::endl;
        *ptr_data = 99; // 允许:修改指向的数据
        std::cout << "Modified value: " << *ptr_data << std::endl;
        // ptr_data = nullptr; // 编译错误:不能修改常量指针本身
    }
}

int main() {
    int my_val = 10;
    std::cout << "Before call: my_val = " << my_val << std::endl; // 输出 10
    modify_value_through_const_ptr_param(&my_val);
    std::cout << "After call: my_val = " << my_val << std::endl; // 输出 99

    return 0;
}

可以看到,modify_value_through_const_ptr_param 函数内部对 ptr_dataconst 限制,仅对函数内部的 ptr_data 拷贝有效。在函数外部,&my_val 仍然是可变的。因此,通常我们不会在函数参数中使用 Type* const

3. const 成员函数与 this 指针

这是一个与 const 指针密切相关的概念。在一个类的成员函数声明后面加上 const 关键字,表示这个成员函数不会修改对象的状态。

class MyClass {
public:
    int value;

    MyClass(int v) : value(v) {}

    // 普通成员函数,可以修改对象状态
    void increment() {
        value++;
    }

    // const 成员函数,承诺不修改对象状态
    // 在 const 成员函数中,this 指针的类型是 const MyClass* const
    void print_value() const { 
        std::cout << "Current value: " << value << std::endl;
        // value++; // 编译错误:在 const 成员函数中不能修改非 mutable 成员
    }
};

int main() {
    MyClass obj(10);
    obj.increment();
    obj.print_value(); // 输出 11

    const MyClass const_obj(20);
    // const_obj.increment(); // 编译错误:对 const 对象调用非 const 成员函数
    const_obj.print_value(); // 允许:对 const 对象调用 const 成员函数

    return 0;
}

当一个成员函数被声明为 const 时,它的隐式 this 指针的类型实际上是 const MyClass* const。这意味着:

  • 您不能通过 this 指针修改类成员(除非它们被声明为 mutable)。
  • 您不能改变 this 指针本身(它始终指向当前对象)。

这是 const 指针在面向对象编程中一个非常重要的应用,确保了对象的常量正确性。


九、高级话题与最佳实践

1. const_cast:移除 const 限制

const_cast 是 C++ 中四种 cast 运算符之一,它专门用于添加或移除类型的 constvolatile 属性。通常,它被用来移除 const 限制。

语法: const_cast<Type*>(expression)

用途:

  • 与遗留 C 风格 API 交互: 有些 C 库函数可能声明参数为 char* 但实际上并不会修改数据。为了调用这些函数,您可能需要将 const char* 转换为 char*
  • 调用非 const 成员函数但知道它不会修改对象: 比如,一个 const 对象需要调用一个非 const 的成员函数,但你知道这个函数实际上是“逻辑 const”(即它不会改变对象的状态)。

危险性与未定义行为:
如果原始对象本身就是 const 的,通过 const_cast 移除 const 后再尝试修改它,会导致未定义行为

#include <iostream>

void modify_data(int* p) {
    if (p) {
        *p = 200;
    }
}

int main() {
    // 情况 1: 原始变量是非 const 的
    int non_const_val = 10;
    const int* ptr_to_non_const = &non_const_val;

    // 此时使用 const_cast 是安全的,因为 non_const_val 本身是可修改的
    modify_data(const_cast<int*>(ptr_to_non_const));
    std::cout << "Non-const value after const_cast: " << non_const_val << std::endl; // 输出 200

    // 情况 2: 原始变量是 const 的
    const int const_val = 30;
    const int* ptr_to_const = &const_val;

    // 试图通过 const_cast 修改一个真正是 const 的变量,导致未定义行为
    // 尽管编译通过,但运行时可能崩溃或产生不可预测的结果
    // 在某些系统上,const 变量可能存储在只读内存区域,修改会导致段错误
    modify_data(const_cast<int*>(ptr_to_const)); // 这是一个危险的操作!
    std::cout << "Const value after const_cast (UB risk): " << const_val << std::endl; // 输出可能仍然是 30,也可能崩溃,也可能输出 200

    return 0;
}

忠告: 尽量避免使用 const_cast。如果必须使用,请确保您完全理解其风险,并且只有在原始对象本身不是 const 的情况下才尝试修改。

2. typedefconst 的陷阱

typedef 可以为现有类型创建别名。然而,当 typedefconst 结合时,可能会出现一些意想不到的行为,因为 typedef 是简单的文本替换。

typedef char* PCHAR; // PCHAR 是 char* 的别名

int main() {
    char str[] = "Hello";
    char str2[] = "World";

    // 1. const PCHAR p;
    // 展开后是 char* const p;
    // 这是一个常量指针,指向 char
    const PCHAR p = str; 
    // *p = 'h'; // 允许:修改指向的数据
    // p = str2; // 编译错误:assignment of read-only variable 'p'

    // 2. const char* p2;
    // 这是一个指向常量的指针
    const char* p2 = str;
    // *p2 = 'h'; // 编译错误:assignment of read-only location '*p2'
    p2 = str2; // 允许:修改指针本身

    // 3. PCHAR const p3;
    // 展开后是 char* const p3;
    // 与 const PCHAR p 相同
    PCHAR const p3 = str;
    // *p3 = 'h'; // 允许
    // p3 = str2; // 编译错误

    return 0;
}

结论: typedef 别名会带走类型中的一部分,然后 const 会作用在别名上。const PCHAR 意味着 PCHAR 类型的变量是 const 的。因为 PCHAR 本身就是 char*,所以 const PCHAR 等价于 char* const。这与 const char* 是不同的。

为了避免这种混淆,在涉及指针的 typedef 时,最好避免将 consttypedef 别名本身结合,而是直接将 const 应用到最终的类型上,例如 const char*

3. constexprconst

constexpr 是 C++11 引入的关键字,用于在编译时评估表达式。当 constexpr 用于变量声明时,它隐含了 const 语义。

constexpr int compile_time_constant = 100; // compile_time_constant 也是 const 的
// compile_time_constant = 200; // 编译错误

// constexpr 指针
int val = 5;
constexpr int* ptr_to_val = &val; // 编译错误:ptr_to_val 必须指向一个 constexpr 对象
// 实际上,constexpr 指针必须指向一个编译时已知的地址,这通常意味着指向全局静态存储区或字面量。
// 或者,如果 ptr_to_val 被声明为 const int* const,它仍然可以指向一个非 constexpr 变量。
// 但如果 ptr_to_val 本身是 constexpr,那么它必须在编译时确定其指向的地址,并且该地址的内容也应该是 constexpr 的。

// 更常见的是指向 constexpr 变量的 constexpr 指针
constexpr int static_val = 10;
constexpr const int* ptr_to_static_val = &static_val; // 允许,ptr_to_static_val 是一个 const 指针
                                                     // 并且它指向一个 const int,同时 ptr_to_static_val 本身也是一个编译时常量

constexpr 更强调编译时求值,而 const 强调不变性。对于变量,constexpr 变量总是 const 的。

4. 现代 C++ 与 const 的应用

现代 C++ 鼓励使用 const 引用 (const Type&) 作为函数参数,因为它结合了效率(避免拷贝)和安全性(只读访问),且语法更简洁。当需要处理可能为空或需要重新指向的场景时,const 指针仍然是首选。

例如,C++17 引入的 std::string_view 和 C++20 的 std::span 都是只读视图,它们内部包含了指向数据的指针或迭代器,但这些视图自身是 const 正确的,可以安全地传递和使用,而无需担心底层数据被意外修改。


十、const 指针类型总结表

为了更清晰地对比,我们用表格来总结不同 const 指针类型的行为。

声明 含义 能否修改指针本身 (地址) 能否通过指针修改数据 必须初始化 示例 (假设 p 指针)
int* p; 指向 int 的普通指针 p = &other_var; *p = 20;
const int* p; 指向 const int 的指针 (数据是常量) p = &other_var; *p = 20; (错误)
int const* p; 等同于 const int* p; p = &other_var; *p = 20; (错误)
int* const p; 指向 int 的常量指针 (指针本身是常量) p = &other_var; (错误) *p = 20;
const int* const p; 指向 const int 的常量指针 (数据和指针都是常量) p = &other_var; (错误) *p = 20; (错误)

十一、 结束语

通过今天的深入讲解,我们彻底剖析了 const 关键字在指针声明中的各种用法。理解 const int* (指向常量的指针) 和 int* const (常量指针) 之间的细微而本质的区别,是掌握 C++ const 正确性的关键一步。这种理解不仅有助于避免常见的编程错误,更能提升代码的安全性、可读性和可维护性。在未来的 C++ 编程实践中,请务必积极拥抱 const,让它成为您代码健壮性的守护者。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注