各位编程爱好者,大家好!
今天,我们将深入探讨 C++ 中一个既基础又极易混淆的概念:const 指针。它在 C++ 的类型系统中扮演着至关重要的角色,是编写安全、高效和可维护代码的基石之一。然而,由于其语法上的细微差别,许多开发者,包括经验丰富的老兵,也常常会在 const int* 和 int* const 之间感到困惑。
作为一名编程专家,我的目标是彻底揭开 const 指针的神秘面纱。我们将从 const 的基本哲学开始,逐步深入到指针的基础知识,然后详细剖析这两种常见的 const 指针类型,并探讨它们的变体、实际应用以及一些高级话题和最佳实践。请准备好您的注意力,我们即将踏上这段旅程。
一、const 的哲学:不变性与安全性
在深入指针之前,我们首先要理解 const 关键字的本质。const,即 "constant"(常量)的缩写,其核心思想是不变性(Immutability)。当我们将一个变量声明为 const 时,我们是在向编译器和未来的代码维护者承诺:这个变量的值在其生命周期内将保持不变。
为什么我们需要不变性?
- 代码安全性与健壮性: 防止意外修改。想象一个函数接收一个重要配置参数,如果这个参数可以被函数内部随意修改,那么就可能导致程序行为异常。
const能够强制执行“只读”权限,避免这类错误。 - 代码意图表达:
const是一种强大的自我文档工具。当您看到一个const声明时,您立即知道这个数据不应该被修改。这使得代码更易于理解和维护。 - 编译器优化: 编译器知道
const变量的值不会改变,这为它提供了更多的优化机会,例如将变量存储在只读内存区域,或者在某些情况下直接进行常量折叠(constant folding)。 - 多线程安全: 在并发编程中,不变的数据是天生线程安全的,无需额外的同步机制。
const能够帮助我们识别和利用这些线程安全的数据。 - 接口设计: 在函数参数和返回值中使用
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 关键字可以出现在指针声明的两个主要位置,这直接导致了两种截然不同的含义:
const作用于指针所指向的值(即数据)。const作用于指针本身(即地址)。
为了帮助记忆,一个常用的经验法则是“从右往左读”,或者更直观地理解为“*`` 号的左边和右边**”。
- 如果
const出现在*的左边,它修饰的是指针所指向的数据,表示数据是常量。 - 如果
const出现在*的右边,它修饰的是指针本身,表示指针是常量。
让我们逐一详细剖析这两种情况。
四、const int*:指向常量的指针 (Pointer to const)
const int*,有时也写成 int const* (含义完全相同),表示一个指向 const int 的指针。这意味着:
- 您不能通过这个指针来修改它所指向的
int类型数据的值。数据被视为常量。 - 您可以修改指针本身,使其指向不同的
int类型数据(无论是常量还是非常量)。
解析语法:
我们可以这样理解 const int* p;:
*p表示p是一个指针。int表示p指向一个int类型的数据。const出现在int的左边(也就是*的左边),它修饰的是int,表示这个int是const的。
类比: 想象您有一张地图,上面标记着一个房子的位置。这是一张“只读”地图,您可以根据地图找到房子,但不能通过修改地图来改变房子本身(比如把房子从三层改成两层)。然而,您可以把这张地图扔掉,换一张新的地图来指示另一所房子的位置。
代码示例:
#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;:
p是一个const的变量。*表示p是一个指针。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;:
p是一个const的变量(最右边的const修饰p)。*表示p是一个指针。int表示p指向一个int类型的数据。- 最左边的
const修饰int,表示这个int是const的。
结合起来,p 是一个 const 的指针,它指向一个 const 的 int。
类比: 这是一个刻有地址的门牌号,固定地钉在一扇门上,而且这扇门后面的屋子里所有东西都是用胶水固定住的,你不能移动它们。你不能换门牌号,也不能改变屋内的摆设。
代码示例:
#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 修饰它左边的内容,除非它是最左边的,此时它修饰它右边的内容。
让我们用这个规则来分析前面提到的各种情况:
-
int value;value是一个int。
-
const int value;(等价于int const value;)const在最左边,所以它修饰右边的int。- 结果:
value是一个const int。
-
*`int ptr;`**
*ptr是一个int。所以ptr是一个指向int的指针。
-
*`const int ptr;
** (等价于int const* ptr;`)const修饰int。- 结果:
*ptr是一个const int。所以ptr是一个指向const int的指针。 ptr本身没有被const修饰,所以ptr是可变的。
-
*`int const ptr;`**
const修饰ptr。- 结果:
ptr本身是const的。 *ptr是一个int。- 所以
ptr是一个const指针,指向一个int。
-
*`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_data 的 const 限制,仅对函数内部的 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 运算符之一,它专门用于添加或移除类型的 const 或 volatile 属性。通常,它被用来移除 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. typedef 与 const 的陷阱
typedef 可以为现有类型创建别名。然而,当 typedef 与 const 结合时,可能会出现一些意想不到的行为,因为 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 时,最好避免将 const 与 typedef 别名本身结合,而是直接将 const 应用到最终的类型上,例如 const char*。
3. constexpr 与 const
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,让它成为您代码健壮性的守护者。