变量与常量:C++ 中 const 关键字的正确使用姿势
在 C++ 编程中,const 关键字是一个高频且核心的语法元素,它用于声明"不可修改"的实体,既能保障代码安全性,又能提升程序可读性与性能。但 const 的用法灵活多变,在不同场景(变量、函数、类、指针等)下有着细微差异,稍不注意就会出现逻辑错误或语法漏洞。本文将系统梳理 const 的核心应用场景,拆解其底层逻辑,分享正确使用的实操技巧。
一、基础认知:const 修饰变量与常量
const 最基础的用法是修饰变量,使其成为"只读变量"(严格来说,C++ 中 const 变量并非绝对意义上的常量,需结合场景区分)。
1. 基本语法与特性
cpp
#include <iostream>
using namespace std;
int main() {
const int a = 10; // 声明const变量a,初始化后不可修改
// a = 20; // 编译错误:assignment of read-only variable 'a'
const int b; // 错误:const变量必须在声明时初始化(除全局/静态存储区变量外)
return 0;
}
核心特性:
-
const 变量一旦初始化,其值无法通过赋值语句修改,编译器会对修改操作直接报错,从语法层面杜绝误修改风险。
-
const 变量必须在声明时初始化(局部 const 变量强制要求,全局/静态 const 变量若未初始化,编译器会默认赋 0,但不推荐此写法)。
-
const 变量的作用域与普通变量一致,局部 const 变量存储在栈区,全局 const 变量存储在只读数据区(修改会触发运行时错误)。
2. const 与 #define 的区别(常量定义场景)
很多开发者会用 #define 定义常量,但在 C++ 中,const 修饰的变量更推荐用于常量定义,二者核心差异如下:
| 特性 | const 变量 | #define 宏定义 |
|---|---|---|
| 类型检查 | 有,编译器会进行严格的类型校验 | 无,仅做文本替换,易引发类型隐患 |
| 作用域 | 遵循变量作用域规则,可通过 extern 扩展 | 全局有效,从定义处到文件结束,无法限定局部作用域 |
| 调试性 | 可被调试器识别(如 gdb 可查看值) | 预处理阶段被替换,调试器无法追踪 |
| 副作用 | 无,仅为变量声明 | 可能因文本替换引发歧义(如 #define PI 3.1415 用于表达式时) |
结论:C++ 中优先使用 const 定义常量,仅在需要全局文本替换(如跨文件统一配置)时考虑 #define。 |
二、核心场景:const 修饰指针与引用
const 与指针、引用结合是面试高频考点,核心在于区分"const 修饰的是指针本身"还是"指针指向的内容",引用同理(引用本身不可修改,const 仅修饰引用指向的对象)。
1. const 修饰指针(三种场景)
cpp
#include <iostream>
using namespace std;
int main() {
int x = 10, y = 20;
// 1. 指向常量的指针(const 修饰指向的内容)
const int* p1 = &x;
// *p1 = 30; // 错误:无法修改指向的常量值
p1 = &y; // 正确:指针本身可修改,指向新的地址
// 2. 指针常量(const 修饰指针本身)
int* const p2 = &x;
*p2 = 30; // 正确:可修改指向的内容
// p2 = &y; // 错误:指针本身不可修改,无法指向新地址
// 3. 指向常量的指针常量(const 同时修饰内容和指针)
const int* const p3 = &x;
// *p3 = 30; // 错误:无法修改指向的内容
// p3 = &y; // 错误:指针本身不可修改
return 0;
}
记忆技巧:const 靠近谁,就修饰谁 。靠近 int 则修饰指向的内容,靠近 * 则修饰指针本身;若两侧都有 const,则两者均被修饰。
2. const 修饰引用
引用本身是变量的别名,不可被重新赋值,因此 const 修饰引用仅表示"引用指向的对象不可修改",语法为 const 类型& 引用名 = 变量名。
cpp
#include <iostream>
using namespace std;
int main() {
int a = 10;
const int& ref = a; // const引用,指向a且不可修改a的值
// ref = 20; // 错误:无法通过const引用修改对象值
a = 20; // 正确:可直接修改原变量,const引用会同步更新值
// const引用可绑定常量(普通引用不可)
const int& ref2 = 100; // 合法,编译器会生成临时变量存储100
// int& ref3 = 100; // 错误:普通引用无法绑定常量
return 0;
}
核心用途:在函数参数中使用 const 引用,可避免值拷贝(提升性能),同时防止函数内部修改实参的值,是函数传参的常用优化手段。
三、进阶应用:const 修饰函数
const 可修饰函数的参数、返回值,以及类的成员函数,各自承担不同的语义。
1. const 修饰函数参数
主要用于避免函数内部误修改参数值,尤其适用于复杂类型(如类对象、数组),减少拷贝开销。
cpp
#include <string>
using namespace std;
// 传递字符串常量引用,避免拷贝且防止修改
void printString(const string& str) {
// str += "test"; // 错误:无法修改const参数
cout << str << endl;
}
2. const 修饰函数返回值
适用于返回值为指针或引用的场景,防止外部通过返回值修改函数内部的数据。
cpp
#include <iostream>
using namespace std;
int x = 10, y = 20;
// 返回指向常量的指针,外部无法通过指针修改值
const int* getPointer() {
return &x;
}
int main() {
const int* p = getPointer();
// *p = 30; // 错误:返回值为const指针,无法修改
return 0;
}
3. const 修饰类的成员函数
这是类编程中的核心用法,声明格式为 返回值 函数名(参数列表) const,表示该成员函数不会修改类的任何成员变量(mutable 修饰的变量除外)。
cpp
#include <iostream>
using namespace std;
class Person {
private:
string name;
int age;
public:
Person(string n, int a) : name(n), age(a) {}
// const成员函数,仅读取成员变量,不修改
void showInfo() const {
cout << "姓名:" << name << ",年龄:" << age << endl;
// age = 30; // 错误:const成员函数无法修改普通成员变量
}
// 普通成员函数,可修改成员变量
void setAge(int a) {
age = a;
}
};
int main() {
const Person p1("张三", 25); // const对象,仅能调用const成员函数
p1.showInfo(); // 正确
// p1.setAge(30); // 错误:const对象无法调用非const成员函数
Person p2("李四", 30); // 普通对象,可调用所有成员函数
p2.showInfo();
p2.setAge(35);
return 0;
}
关键规则:
-
const 成员函数不能修改类的非 mutable 成员变量,也不能调用类的非 const 成员函数(避免间接修改成员变量)。
-
const 对象只能调用 const 成员函数,普通对象可调用所有成员函数。
-
若需要在 const 成员函数中修改某个变量,可将该变量声明为
mutable(如mutable int count;),mutable 变量不受 const 约束。
四、常见误区与避坑技巧
1. 误区一:const 变量一定是编译期常量
只有"用常量表达式初始化"的 const 变量才是编译期常量,可用于数组大小、switch case 等场景;若用变量初始化,则为运行期常量,无法用于编译期场景。
cpp
#include <iostream>
using namespace std;
int main() {
const int a = 10;
int arr[a]; // 正确:a是编译期常量
int b = 20;
const int c = b;
// int arr2[c]; // 错误:c是运行期常量,无法作为数组大小(C++11后可通过 constexpr 解决)
return 0;
}
替代方案:C++11 引入 constexpr 关键字,专门用于声明编译期常量,优先级高于 const。
2. 误区二:const 指针与非 const 指针可随意转换
C++ 不允许将 const 指针赋值给非 const 指针(防止通过非 const 指针修改 const 内容),但允许将非 const 指针赋值给 const 指针(缩小权限,安全)。
cpp
#include <iostream>
using namespace std;
int main() {
int x = 10;
const int* p1 = &x; // 正确:非const指针赋值给const指针
int* p2 = p1; // 错误:const指针无法赋值给非const指针
// 强制转换(不推荐,破坏类型安全)
int* p3 = const_cast<int*>(p1);
*p3 = 20; // 运行时未报错,但修改了原变量,违反const语义
return 0;
}
提示:尽量避免使用 const_cast 强制移除 const 限定,仅在兼容旧代码等特殊场景下使用。
3. 误区三:类的 const 成员变量可在构造函数中修改
类的 const 成员变量必须在初始化列表中初始化,无法在构造函数体内赋值(构造函数体执行时,成员变量已完成初始化,const 变量无法二次赋值)。
cpp
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
const int id; // const成员变量
string name;
public:
// 正确:在初始化列表中初始化const成员变量
Student(int i, string n) : id(i), name(n) {}
// 错误:试图在构造函数体内赋值const成员变量
// Student(int i, string n) {
// id = i;
// name = n;
// }
};
int main() {
Student s(1001, "王五");
return 0;
}
五、总结:const 的核心价值与使用原则
const 关键字的本质是"权限控制",通过明确"不可修改"的语义,实现三大核心价值:
-
安全性:从编译器层面阻止误修改操作,减少逻辑 bug。
-
可读性:明确告知开发者变量/函数的语义,提升代码可维护性。
-
性能优化:const 引用/指针避免值拷贝,编译器可基于 const 语义做额外优化(如常量折叠)。
使用原则:
-
定义常量时,优先用
const(C++11+ 用constexpr)替代#define。 -
函数参数为复杂类型时,优先用 const 引用传递,兼顾性能与安全性。
-
类的成员函数若不修改成员变量,务必声明为 const 成员函数。
-
避免随意用
const_cast移除 const 限定,坚守类型安全。
掌握 const 的正确用法,是从"入门级"开发者迈向"专业级"开发者的重要一步。在实际开发中,应养成"能加 const 就加 const"的习惯,让代码更健壮、更易维护。