C++中const修饰符的作用及使用误区有哪些?

C++
VIP/

一、const 的核心作用

 

const 是 C++ 类型系统的核心特性之一,本质是编译器强制的只读约束,既能提高代码可读性(明确告诉其他开发者这个变量 / 函数不会修改内容),又能让编译器提前捕获意外修改的逻辑错误,还能帮助编译器做性能优化。
下面分场景讲解 const 的核心用法:

1.1 定义只读变量(常量)

 

const 最基础的用法是修饰变量,定义后不可修改。相比 C 语言常用的#define宏定义常量,const 有三个核心优势:

 

  • ✅ 类型安全:const 是编译期检查,有明确的类型,会做类型匹配校验;而#define是预处理文本替换,没有类型检查,容易出现隐式类型转换的 bug
  • ✅ 作用域控制:const 遵循 C++ 的块作用域规则,可以定义在类、函数、命名空间内,不会污染全局作用域;而#define默认是全局的,除非手动#undef
  • ✅ 可调试:const 变量是真实存在的变量,调试时可以直接查看值;而#define在预处理阶段就被替换了,调试时看不到对应的符号

代码示例:

 

cpp
// 宏定义常量(不推荐)
#define PI_MACRO 3.14159
// const定义常量(推荐)
const double PI_CONST = 3.14159;
// C++11及以后推荐用constexpr明确表示编译期常量
constexpr double PI_CONSTEXPR = 3.14159;
void test_const_var() {
    // PI_CONST = 3.14; // 编译错误:const变量不可修改
    int a = PI_MACRO + 1; // 预处理后变成 3.14159 + 1,无类型检查
    int b = PI_CONST + 1; // 编译警告:double转int丢失精度,有类型检查
}

 

[!NOTE]
注意:只有用常量表达式初始化的 const 变量才是编译期常量,如果用运行期值初始化(比如const int n = rand();),只是运行期只读,不能用于数组大小、模板参数等需要编译期常量的场景。这种情况推荐用 C++11 引入的constexpr明确标识编译期常量,避免歧义。

1.2 修饰指针:区分顶层 const 与底层 const

 

const 修饰指针是最容易混淆的场景,核心是区分顶层 const底层 const

 

  • 👉 顶层 const:修饰指针本身,表示指针的指向不可修改(不能指向其他地址)
  • 👉 底层 const:修饰指针指向的内容,表示不能通过该指针修改指向的内容
    判断规则非常简单:const 在*左边就是底层 const,在*右边就是顶层 const,和 const 在类型前 / 后无关

代码示例:

 

cpp
int a = 10;
int b = 20;
// 1. 底层const:指向的内容不可修改,指针本身可以改
const int *p1 = &a; // 等价于 int const *p1 = &a;
// *p1 = 100; // 编译错误:底层const,不能修改指向的内容
p1 = &b; // 合法:指针本身可以改指向
// 2. 顶层const:指针本身不可修改,指向的内容可以改
int *const p2 = &a;
*p2 = 100; // 合法:可以修改指向的内容
// p2 = &b; // 编译错误:顶层const,指针本身不可改指向
// 3. 同时有顶层和底层const:指针和指向的内容都不可改
const int *const p3 = &a; // 等价于 int const *const p3 = &a;
// *p3 = 100; // 编译错误
// p3 = &b; // 编译错误

 

[!TIP]
引用本身天生就是顶层 const(一旦绑定就不能改指向),所以不存在int &const r的写法。const 修饰引用都是底层 const,表示引用的对象不可修改,const int &rint const &r完全等价。

1.3 修饰引用:常引用

 

const 修饰的引用称为「常引用」,是开发中使用频率极高的特性,核心优势有两个:

 

  1. 函数传参时避免拷贝,同时保证安全:传大对象(比如 string、vector、自定义类)时,用常引用传参既可以避免值拷贝的开销,又能保证函数内部不会修改实参。
  2. 可以绑定临时对象 / 右值:C++ 规定非 const 左值引用不能绑定到临时对象(右值),而常引用可以延长临时对象的生命周期到引用的作用域。

代码示例:

 

cpp
// 错误:非const左值引用不能绑定临时对象
// string &r1 = string("hello");
// 正确:常引用可以绑定临时对象,生命周期延长到r2的作用域
const string &r2 = string("hello");
// 函数传参用常引用,高效又安全
void print_str(const string &s) {
    // s += "world"; // 编译错误:常引用不可修改
    cout << s << endl;
}
print_str("hello"); // 合法:"hello"转为临时string,绑定到const引用

1.4 修饰函数参数

 

const 修饰函数参数是日常开发最常用的场景,分为两种情况:

 

  • 👉 传值参数加 const:修饰形参本身,函数内部不能修改形参,对外部调用者无影响,属于函数内部实现细节,一般不需要写在函数声明中(只在实现里加即可),小对象传值时没必要加。
  • 👉 传指针 / 引用加 const:属于对外接口的一部分,明确告诉调用者「函数不会修改你传入的实参」,是接口设计的重要规范,大对象传参优先用 const 引用。

代码示例:

 

cpp
// 头文件声明:传值参数不需要写const,顶层const不影响签名
void func(int a, const string &s);
// 实现里可以给传值参数加const,防止内部意外修改
void func(const int a, const string &s) {
    // a = 10; // 编译错误:形参a是const
    // s += "a"; // 编译错误:s是const引用
    cout << a << s << endl;
}

1.5 修饰函数返回值

 

const 修饰函数返回值主要用于返回指针 / 引用的场景,防止返回值被意外修改:

 

  • 如果返回的是内置类型(int、double 等),加 const 没有意义,因为返回的是右值,本来就不能被修改。
  • 如果返回的是指针 / 引用,加 const 可以避免返回值被修改,保证接口安全性。

代码示例:

 

cpp
class Person {
private:
    string name;
public:
    // 返回const引用:外部不能修改成员变量name
    const string& get_name() const { return name; }
};
Person p;
// p.get_name() = "zhangsan"; // 编译错误:返回的是const引用,不可修改

 

[!WARNING]
绝对不要返回局部对象的引用 / 指针,不管加不加 const!局部对象在函数结束后会被销毁,返回的引用 / 指针会变成悬空引用,属于未定义行为。

1.6 修饰类的成员函数:常成员函数

 

const 放在成员函数的参数列表后面,称为「常成员函数」,核心规则是:

 

  • 常成员函数内部不能修改类的非静态成员变量(mutable 修饰的变量除外)
  • 常成员函数只能调用其他常成员函数,不能调用非 const 成员函数
  • const 对象只能调用常成员函数,非 const 对象优先调用非 const 版本的成员函数
    本质上,成员函数后面的 const 是修饰隐含的this指针:普通成员函数的 this 是Type *const this(顶层 const),常成员函数的 this 是const Type *const this(底层 const),所以不能通过 this 修改成员变量。

代码示例:

 

cpp
class Person {
private:
    string name;
    mutable int visit_count = 0; // mutable修饰的变量,允许在const函数中修改
public:
    // 普通成员函数:可以修改成员变量
    void set_name(string n) {
        name = move(n);
    }
    // 常成员函数:不修改成员变量
    const string& get_name() const {
        visit_count++; // 合法:visit_count是mutable
        return name;
    }
};
// const对象只能调用常成员函数
const Person p_const;
p_const.get_name(); // 合法
// p_const.set_name("lisi"); // 编译错误:const对象不能调用非const成员函数
// 非const对象优先调用非const版本(如果有的话)
Person p;
p.set_name("zhangsan"); // 合法
p.get_name(); // 合法:非const对象也可以调用const成员函数

 

[!NOTE]
mutable关键字只能用于类的非静态成员变量,用于标记那些不影响对象逻辑状态的辅助变量(比如计数器、缓存、锁等),不要滥用 mutable 破坏 const 的只读语义。

二、const 常见误区与避坑指南

 

很多开发者对 const 的理解停留在表面,开发中经常踩各种坑,下面整理了 9 个最常见的误区,帮你避开 90% 的 const 相关 bug。

误区 1:搞混顶层 / 底层 const,导致赋值错误

 

底层 const 有严格的赋值兼容性规则:非底层 const 的指针 / 引用可以赋值给底层 const 的,但反过来不行,否则会破坏 const 的只读语义;而顶层 const 在赋值时会被忽略,不影响兼容性。

错误代码:

 

cpp
const int a = 10;
const int *p1 = &a; // p1是底层const
int *p2 = p1; // 编译错误!不能把底层const指针赋值给非底层const指针
// 如果允许上述操作,就可以通过p2修改const变量a的值,违反了const语义

解决方案:

 

如果确实需要修改,必须保证原来的对象本身是可修改的,再用const_cast去掉底层 const(如果原对象是真・常量,修改会导致未定义行为):

 

cpp
int b = 20; // 原对象本身是非const的
const int *p1 = &b;
int *p2 = const_cast<int*>(p1); // 合法,且行为定义:b本身可修改
*p2 = 100; // 合法,b被修改为100

误区 2:成员函数该加 const 不加,导致 const 对象无法调用

 

很多开发者写成员函数时,只要不修改成员变量就应该加 const,但经常忘记加,导致 const 对象调用时报错,降低了接口的通用性。

错误代码:

 

cpp
class Person {
private:
    string name;
public:
    // 错误:get_name不会修改成员变量,但没加const
    string& get_name() {
        return name;
    }
};
const Person p;
// p.get_name(); // 编译错误:const对象不能调用非const成员函数

解决方案:

 

所有不修改成员变量的成员函数一律加 const,同时可以提供 const / 非 const 的重载版本:

 

cpp
class Person {
private:
    string name;
public:
    // 非const版本:给非const对象调用,返回可修改的引用
    string& get_name() {
        return name;
    }
    // const版本:给const对象调用,返回const引用
    const string& get_name() const {
        return name;
    }
};

误区 3:非 const 引用绑定临时对象,导致编译错误

 

C++ 规定非 const 左值引用不能绑定到临时对象(右值),很多开发者传参时忘记加 const,导致传字符串常量、临时对象时报错。

错误代码:

 

cpp
void print_str(string &s) {
    cout << s << endl;
}
// print_str("hello"); // 编译错误:"hello"转为临时string,不能绑定到非const引用
// print_str(string("world")); // 编译错误:同样是临时对象

解决方案:

 

如果函数不需要修改传入的参数,一律用 const 引用传参:

 

cpp
void print_str(const string &s) { // 加const
    cout << s << endl;
}
print_str("hello"); // 合法

误区 4:const 和 #define 混用,导致重定义、作用域问题

 

#define 是预处理宏,没有作用域,会全局替换,和 const 混用容易出现重定义、意外替换的问题。

错误代码:

 

cpp
#define MAX 1024
const int MAX = 2048; // 编译错误:宏MAX被替换为1024,变成 const int 1024 = 2048; 语法错误

解决方案:

 

尽量用 const/constexpr 代替 #define 定义常量,有类型安全、作用域可控的优势:

 

cpp
// #define MAX 1024 // 删掉宏
constexpr int MAX = 1024; // 用constexpr代替

误区 5:以为 const 变量一定是编译期常量

 

只有用常量表达式初始化的 const 才是编译期常量,用运行期值初始化的 const 只是运行期只读,不能用于数组大小、模板参数等编译期常量场景。

错误代码:

 

cpp
int n = rand();
const int SIZE = n; // 运行期初始化,不是编译期常量
// int arr[SIZE]; // 编译错误:C++标准不支持变长数组(VLA是GCC扩展,非标准)

解决方案:

 

如果需要编译期常量,用 C++11 引入的constexpr明确标识,编译器会强制检查是否是编译期常量:

 

cpp
constexpr int SIZE = 1024; // 编译期常量
int arr[SIZE]; // 合法

误区 6:顶层 const 用于函数重载,导致重复定义

 

函数签名不考虑顶层 const,只有底层 const 会影响函数签名,所以顶层 const 不能用于重载函数。

错误代码:

 

cpp
// 编译错误:重复定义,顶层const不影响签名
void func(int a) {}
void func(const int a) {} // 和上面的func是同一个函数
// 编译错误:重复定义,指针的顶层const不影响签名
void func(int *p) {}
void func(int *const p) {} // 和上面的func是同一个函数

解决方案:

 

只有底层 const 可以用于重载:

 

cpp
// 合法重载:指针的底层const不同
void func(int *p) {}
void func(const int *p) {} // 不同的函数
// 合法重载:引用的底层const不同
void func(int &a) {}
void func(const int &a) {} // 不同的函数
// 合法重载:成员函数的const(底层const修饰this)
class Person {
public:
    void func() {}
    void func() const {} // 不同的函数
};

误区 7:滥用 mutable 破坏 const 语义

 

mutable 是为了允许 const 函数修改不影响对象逻辑状态的辅助变量,如果滥用 mutable 修改核心成员变量,会完全破坏 const 的只读约定,导致逻辑 bug。

错误代码:

 

cpp
class Order {
private:
    mutable int amount; // 订单金额是核心状态,不该用mutable
public:
    // const函数修改核心状态,逻辑错误
    void set_amount(int a) const {
        amount = a; // 因为mutable编译通过,但语义错误
    }
};

解决方案:

 

mutable 只能用于计数器、缓存、锁等不影响对象逻辑等价性的辅助变量,核心业务成员变量绝对不要用 mutable。

误区 8:返回 const 引用指向局部对象,导致悬空引用

 

很多开发者以为返回 const 引用就安全,但如果返回的是局部对象的引用,不管加不加 const,函数结束后局部对象都会销毁,引用会悬空,导致未定义行为。

错误代码:

 

cpp
const string& get_name() {
    string name = "zhangsan";
    return name; // 错误:返回局部对象的引用,函数结束后name被销毁
}
// string s = get_name(); // 未定义行为:引用悬空,可能崩溃、可能乱码

解决方案:

 

如果返回的是局部对象,直接返回值即可(C++11 及以后会自动移动,效率很高),不要返回引用:

 

cpp
string get_name() {
    string name = "zhangsan";
    return name; // 合法,返回值会被移动/拷贝
}

误区 9:const 位置写反,导致语义错误

 

很多新手搞不清 const 的位置,把const int *写成int *const,导致实际语义和预期不符。

错误代码:

 

cpp
// 预期:不能修改指向的内容,指针可以改指向
int *const p = &a; // 实际:指针不能改指向,内容可以改
*p = 100; // 编译通过,但不符合预期

解决方案:

 

记住核心规则:const 在左边是底层 const(改内容),在右边是顶层 const(改指针),如果怕搞混,可以遵循「const 右绑定」的规则:const 默认修饰它右边的符号,比如:

 

  • const int *p:const 修饰 int,所以指向的 int 是 const
  • int *const p:const 修饰 *,所以指针本身是 const

三、const 最佳实践

 

工业界的 C++ 编码规范中,const 的使用有明确的约定,遵循这些实践可以大幅提升代码质量:

1. 「能加 const 就加」原则

 

凡是不会被修改的变量、函数参数、成员函数,一律加 const:

 

  • 提高代码可读性:明确告诉其他开发者这个实体是只读的
  • 编译器提前捕获错误:意外修改只读实体时直接编译报错
  • 帮助编译器优化:const 实体可以被编译器做常量折叠、只读段优化等

2. 大对象传参优先用 const&

 

对于自定义类、STL 容器等占用内存较大的对象,传参一律用const T&,既避免值拷贝的开销,又保证不会修改实参;对于 int、double、指针等小对象(<=8 字节),直接传值即可,比传引用效率更高。

3. 类成员函数默认加 const(除非需要修改成员)

 

所有不修改类成员变量的成员函数,必须加 const,保证 const 对象可以调用,提升接口的通用性。

4. 用 const/constexpr 代替 #define 定义常量

 

#define 没有类型检查、没有作用域,容易出问题,定义常量优先用:

 

  • C++11 及以后:constexpr 用于编译期常量,const用于运行期只读变量
  • C++03:const 用于编译期 / 运行期常量

5. 尽量避免使用 const_cast

 

const_cast 是 C++ 的类型转换中最容易出问题的一个,除非你 100% 确定原对象是可修改的,否则不要用 const_cast 去掉底层 const,很容易导致未定义行为。

6. const 位置统一,提升可读性

 

const 的位置(const int还是int const)只要团队统一即可,推荐遵循「const 尽可能靠近被修饰的对象」的规则,比如底层 const 写int const *,顶层 const 写int *const,这样更直观。

7. 不要返回局部对象的引用 / 指针(无论是否 const)

 

返回引用 / 指针时,必须保证指向的对象生命周期长于引用 / 指针的生命周期,局部对象、临时对象的引用 / 指针绝对不能返回。

四、总结

 

const 是 C++ 类型安全体系的核心特性之一,它不是负担,而是帮助我们写出更安全、更易维护代码的工具。很多开发者觉得 const 麻烦,是因为没有系统理解它的规则,只要掌握了本文讲的作用、误区和最佳实践,你会发现 const 能帮你提前规避大量低级错误,大幅提升调试效率。

购买须知/免责声明
1.本文部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责。
2.若您需要商业运营或用于其他商业活动,请您购买正版授权并合法使用。
3.如果本站有侵犯、不妥之处的资源,请在网站右边客服联系我们。将会第一时间解决!
4.本站所有内容均由互联网收集整理、网友上传,仅供大家参考、学习,不存在任何商业目的与商业用途。
5.本站提供的所有资源仅供参考学习使用,版权归原著所有,禁止下载本站资源参与商业和非法行为,请在24小时之内自行删除!
6.不保证任何源码框架的完整性。
7.侵权联系邮箱:188773464@qq.com
8.若您最终确认购买,则视为您100%认同并接受以上所述全部内容。

海外源码网 C++ C++中const修饰符的作用及使用误区有哪些? https://moyy.us/21977.html

相关文章

猜你喜欢