变量与常量:C++ 中 const 关键字的正确使用姿势

变量与常量: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 关键字的本质是"权限控制",通过明确"不可修改"的语义,实现三大核心价值:

  1. 安全性:从编译器层面阻止误修改操作,减少逻辑 bug。

  2. 可读性:明确告知开发者变量/函数的语义,提升代码可维护性。

  3. 性能优化:const 引用/指针避免值拷贝,编译器可基于 const 语义做额外优化(如常量折叠)。

使用原则:

  • 定义常量时,优先用 const(C++11+ 用 constexpr)替代 #define

  • 函数参数为复杂类型时,优先用 const 引用传递,兼顾性能与安全性。

  • 类的成员函数若不修改成员变量,务必声明为 const 成员函数。

  • 避免随意用 const_cast 移除 const 限定,坚守类型安全。

掌握 const 的正确用法,是从"入门级"开发者迈向"专业级"开发者的重要一步。在实际开发中,应养成"能加 const 就加 const"的习惯,让代码更健壮、更易维护。

相关推荐
小镇学者2 小时前
【python】python有必要像go或者nodejs那样做多版本切换吗?
开发语言·python·golang
hetao17338372 小时前
2026-01-14~15 hetao1733837 的刷题笔记
c++·笔记·算法
qiuiuiu4132 小时前
正点原子RK3568学习日志21-实验1-字符设备点亮led
linux·学习
世人万千丶2 小时前
鸿蒙跨端框架 Flutter 学习 Day 3:综合实践——多维数据流与实时交互实验室
学习·flutter·华为·交互·harmonyos·鸿蒙
世人万千丶2 小时前
鸿蒙跨端框架 Flutter 学习 Day 3:工程实践——数据模型化:从黑盒 Map 走向强类型 Class
学习·flutter·ui·华为·harmonyos·鸿蒙·鸿蒙系统
hoiii1872 小时前
C# 俄罗斯方块游戏
开发语言·游戏·c#
Darkershadow2 小时前
蓝牙学习之Provision(7)bind (1)
学习·蓝牙·ble·mesh
huaqianzkh2 小时前
WinForm + DevExpress 控件的「完整继承关系」
开发语言
PNP Robotics2 小时前
PNP机器人分享具身操作策略和数据采集
大数据·人工智能·学习·机器人