C/C++ const:那些年我们一起追过的“常量”,原来有这么多秘密

C/C++ const:那些年我们一起追过的“常量”,原来有这么多秘密

目录

修饰变量

修饰常量及数组

与宏常量的比较

const 与指针

指向常量的指针

常量指针

指向常量的常量指针

三种指针总结

const 与函数参数

值传递

指针传递

引用传递

三种传递方式总结

const 与函数返回值

返回 const 值

返回 const 指针

返回 const 引用

类中的 const

const 成员函数

const 对象

mutable 关键字

const 静态成员

const 与 constexpr

变量初始化的时机不同

函数是否可以在编译期调用

修饰指针的含义不同

总结


在编写代码时,我们经常需要保证某些数据不被意外修改,C++ 提供了 const 关键字来实现这一目的。

不要小看 const,它在C++ 中非常重要,变量、指针、函数参数、函数返回值、成员函数等地方都有它的身影。

今天就让我们一步一个脚印的来看看const 的这些用法。

修饰变量

const名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。

修饰常量及数组

const int maxUsers = 100; // maxUsers 从此恒定 100 const int arr[] = { 1,2,3 }; 

const定义时必须初始化(除非用 extern 声明),而一旦初始化,其值在生命周期内不可修改。

与宏常量的比较

与C语言不同,C++中的const变量(尤其是全局const)通常不会分配内存,而是直接嵌入到指令中(类似#define)。

宏常量是用 #define 在预处理阶段定义的:

#define MAX_USERS 100 

它只是单纯的文本替换,在预处理阶段就把代码里的 MAX_USERS 全部替换成 100。

编译器根本不知道有个叫 MAX_USERS 的东西。

从定义点开始,到文件结束(除非 #undef),它会污染所有后续代码,不管你在不在命名空间里。

想象一下你要写一个计算圆面积的函数,用 const 和宏分别定义圆周率:

// const 版本 const double PI = 3.14159; double area(double r) { return PI * r * r; } // 宏版本 #define PI 3.14159 double area(double r) { return PI * r * r; } 

看起来差不多,但坑往往藏在细节里。假如你在另一个文件里不小心写了个变量叫 PI:

int PI = 42; // 合法,PI 只是一个普通变量名 

用 const 版本:完全没问题,PI 作为常量有自己的作用域,不会和这个变量冲突。

用宏版本:编译错误!因为 #define PI 是全局的,编译器会把 int PI = 42; 里的 PI 也替换成 3.14159,变成 int 3.14159 = 42;,显然不合法。

这就是宏的“名称污染”问题。

因此定义常量时,首选 const。

宏的唯一用武之地是那些必须发生在预处理阶段的事情,比如条件编译(#ifdef)、头文件保护、以及一些需要字符串化或拼接的技巧。

所以放下对宏的执念吧,拥抱 const,让代码更安全、更可维护!

const 与指针

又到了C++ 里最让人头疼的组合—— const 与指针。

这俩凑一块儿,就像两个性格迥异的室友:一个总想改变(指针),一个坚决不变(const)。

当它们在一起后,就会出现三种组合:

  • 指向常量的指针(pointer to const)
  • 常量指针(const pointer)
  • 指向常量的常量指针(const pointer to const)

别晕,让咱们一个一个拆解它们。

指向常量的指针

有句话叫:指针可以变,但指向的值不能动。

const int* p; // 或者 int const* p(两种写法等价) 

p 是一个指针,它指向一个 const int。

你可以改变 p 本身,让它指向别处;但你不能通过 p 来修改它当前指向的那个整数。

就像你拿着一张“禁止涂改”的便签,你可以把便签贴到不同的墙上,但无论贴哪,墙上的字都不能改。

int a = 10, b = 20; const int* p = &a; // p 指向 a *p = 30; // 错误!不能通过 p 修改 a p = &b;  // 可以,p 现在指向 b 

可以记住,const 在 * 的左边,但不能改变指向的值。

常量指针

指针本身不能动,但指向的值可以改。

int* const p = &a; // p 是一个常量指针,必须初始化 

p 是一个常量指针,也就是说指针本身是只读的。

一旦它指向某个变量,就不能再指向别处。

但是,你可以通过它修改它指向的那个变量(只要变量本身不是 const)。

int a = 10, b = 20; int* const p = &a; // p 永远指向 a *p = 30; // 可以,a 现在是 30 p = &b; // 错误!不能修改 p 本身 

记住const 在 * 的右边,不能改变指针。

指向常量的常量指针

指针和指向的值都不能动。

const int* const p = &a; 

合体版!p 本身是常量,指向的也是常量。

所以既不能改 p 的指向,也不能通过 p 修改指向的值。

int a = 10, b = 20; const int* const p = &a; // p 永远指向 a,且不能通过 p 改 a *p = 30; // 错误! p = &b; // 错误! 

三种指针总结

类型声明形式指针本身可改?指向的值可改?
指向常量的指针const int* / int const*可改不可改
常量指针int* const不可改可改
指向常量的常量指针const int* const / int const* const不可改不可改

const 与函数参数

const 与函数参数——就像给函数的输入加一道“安检门”,不同的参数传递方式对应不同的安检级别。

值传递

void func(const int x) {     // x 是只读的,不能修改 } 

参数 x 是通过值传递的,函数内操作的是实参的副本。

加上 const 意味着这个副本在函数内是只读的。

在函数内部,你不能修改 x。但这对外面的实参没有任何影响——因为本来就是副本。

其实对于值传递,加不加 const 对外部调用者来说没有区别(反正都是副本)。

它的作用主要是对内:告诉函数的读者(以及编译器)这个参数在函数内不应该被修改,起到自我约束的作用,避免不小心改错。

不过,因为值传递本身就会拷贝,如果对象很大,拷贝开销会很高,所以通常只适用于基本类型或小型对象。

指针传递

指针传递时,const 可以修饰指针本身,也可以修饰指针指向的数据,这就回到了我们之前讨论的“指向常量的指针”和“常量指针”。

在函数参数中,它们分别扮演不同角色:

  • 指向常量的指针:保护数据不被改
void func(const int* p) {     // 不能通过 p 修改 *p,但可以修改 p 本身(比如让 p 指向别处) } 

假如你想通过指针传入一个大型数组或对象,并且承诺不会修改它。

这样调用者可以放心地把数据交给你,即使数据本身是 const 的也能传进来。

这么做避免了拷贝,同时保证了数据只读。

  • 常量指针:保护指针本身不改
void func(int* const p) {     // 不能修改 p 本身(比如让 p 指向别处),但可以通过 p 修改 *p } 

你希望这个指针在函数内始终指向同一个对象(比如用于遍历时固定起点)。

但这种情况很少单独使用,因为通常我们更关心数据是否被修改,而不是指针本身是否变。

  • 指向常量的常量指针:双重保护
void func(const int* const p) {     // 既不能改 p,也不能通过 p 改 *p } 

你想表达“我不仅不修改数据,也不会改变指向”。

不过这种写法有点过度约束,通常用常引用(见下文)更简洁。

引用传递

void func(const MyClass& obj) {     // obj 是常引用,不能通过它修改对象 } 

obj 是传入对象的别名,加上 const 表示这个别名是只读的。

优势:

  • 避免拷贝:对于大型对象(如 std::string、vector),直接传值会拷贝整个对象,开销巨大。传引用则没有拷贝。
  • 保护数据:const 保证了函数内不会修改传入的对象。
  • 接受临时对象:常引用可以绑定到临时对象(右值),比如 func(getString()) 是合法的,而普通引用不行。

这就像你进图书馆看书,图书馆给你一张“只读阅览证”——你可以随便看(引用),但不能在书上写写画画(const),而且这张证不占用你书包空间(无拷贝)。甚至你临时路过图书馆(临时对象),也能拿这张证进去翻翻。

三种传递方式总结

传递方式语法适用场景优势
值传递void f(const int x)基本类型小数据调用者无需担心数据被修改
指针传递void f(const int* p)只读大数组/对象,允许空避免拷贝,保护指向内容
引用传递void f(const T& ref)大对象只读,首选方式避免拷贝和指针语法,保护对象

const 与函数返回值

这是 const 在函数出口设置的“关卡”,告诉调用者:“给你这个返回值,但有些事你不能做!”

不同的返回方式配上 const,效果天差地别,我们逐一拆解。

返回 const 值

const int getAge() { return 10; } 

函数返回的是一个 const 限定的对象(通常是副本)。

不过对基本类型而言,返回const值意义不大,因为右值本来就不能被修改。

但对类类型可防止意外赋值:

const std::string getName() const; // 返回 const 对象 

比如 getName().append("suffix") 会被编译器阻止,因为 append 是非 const 成员函数。

这在某些场景下能避免无意义的修改(毕竟临时对象马上就销毁了)。

返回 const 指针

const int* getData() { return 10; } // 返回 const int*,指向的数据只读 

返回一个指针,指向的数据是 const 的。

调用者不能通过这个指针修改指向的数据,但可以修改指针本身(比如让指针指向别处)。

返回 const 引用

const std::string& getName() { return "xingxing"; }  // 返回 const 引用 

返回一个 const 左值引用,指向某个对象。调用者可以通过这个引用读取对象,但不能修改它。

就像你家墙上开了一扇玻璃窗,你可以透过窗户看到屋里的东西(读取数据),但你不能伸手进去改(不能修改)。窗户本身是固定的(引用不能改指向),而且不占你地方(无拷贝)。

类中的 const

好了,这次我们走进类的内部,看看 const 在类中担任哪些角色。

这里涉及四个角色:const 成员函数、mutable 关键字、const 对象、const 静态成员。

它们各有各的规矩,我们一个个介绍。

const 成员函数

在类的成员函数后面加 const,就是向编译器和调用者承诺:“这个函数不会修改对象的状态(非静态成员变量)。”

class Student { public:     Student(std::string name = nullptr) :_name(name), _age(10) {}     std::string getName() const     {         return name;     }     void setName(const std::string& n)     {         name = n;     }     int getAge()     {         return _age;     } private:     std::string name;     int _age; }; 

我们在getName()成员函数后面加上 const,如果在getName里面调用非const成员函数:

std::string getName() const {     int a = getAge(); // 报错,类型不兼容     return _name; } 

编译器就会报错,同样的,调用非const成员变量也会报错。

因为const向编译器和调用者承诺了不会修改对象状态。

const 对象

假设你有一个 const 对象(比如 const Student s;),它只能调用 const 成员函数。

如果 getName 没加 const,s.getName() 就会编译错误。

当你创建对象时加上 const:

const Student alice("Alice"); alice.getName(); // 可以,因为 getName 是 const alice.setName("Bob"); // 错误!不能调用非 const 成员函数 

这个对象从创建到销毁,如果不是 mutable 成员变量都不可改变。它只能调用 const 成员函数。

此方法常用于表示不可变的数据实体,比如配置文件、常量配置等。

mutable 关键字

我们了解了const 成员函数之后可以知道,在类的成员函数后面加 const后就不能修改其对象。

但生活总有意外——有时候在逻辑上不应该修改对象,可技术上却不得不改一些“内部状态”。比如:

你有一个互斥锁,需要在 const 成员函数里加锁解锁,这当然会修改锁的状态,但逻辑上并不影响对象的数据。

这时候 const 的严格性就成了障碍。那么我们就可以用 mutable 给 const 开个“后门”。

class ThreadSafeCounter { public:     int get() const {         std::lock_guard<std::mutex> lock(m);  // m 被修改(加锁)         return value;     } private:     mutable std::mutex m;     int value = 0; }; 

这里的 m 是 mutable 的,因为加锁操作改变了它的状态,但这并不影响 value 的读取。逻辑上 get() 仍是只读操作。

const 静态成员

最常见的组合是 const static 成员(顺序无所谓,static const 和 const static 等价)。

这表示一个属于类的常量,所有对象都能访问,且不能修改。

class MathConstants { public:     static const double PI;  // 声明     static const int MAX = 100;  // 整型常量可以在类内初始化 }; // 类外定义(如果不在类内初始化) const double MathConstants::PI = 3.14159; 

我们可以看到整型(或枚举类型)的 const 静态成员可以在类内直接初始化,但是非整形的还需在类外定义。

不过在C++17 后 引入了inline static,就不存在以上问题了。

class MathConstants { public:     inline static const double PI = 3.14159; }; 

const 与 constexpr

通过以上内容我们知道const它的核心是“承诺不变”。
你可以用它修饰变量,告诉编译器:“这个值我不会改,你别让我改它。”
但这个值到底是在编译时确定还是运行时确定,const 并不关心。

而constexpr是C++11引入的,它的核心是“编译时可知”。
它强制要求在编译期就能算出值(或至少在编译期可求值)。

变量初始化的时机不同

const int a = 42; // 可以,编译期常量 const int b = rand(); // 也可以,运行时初始化,之后不能改 constexpr int c = 42; // 可以,编译期常量 constexpr int d = rand(); // 错误!rand() 不是常量表达式 
  • const 变量:可以在运行时初始化,之后不能修改。
  • constexpr 变量:必须在编译期初始化,且初始化表达式必须是常量表达式(即编译期可求值)。

constexpr 变量本身也是 const 的,所以 constexpr int x = 42; 等价于 const int x = 42;,但反之不成立。

函数是否可以在编译期调用

const 成员函数:如前问所述,承诺不修改对象状态,与编译期求值无关。

constexpr 函数:如果传入的参数是常量表达式,那么该函数可以在编译期求值;如果传入运行时的值,它也可以像普通函数一样在运行时调用。

constexpr int square(int x) { return x * x; } int arr[square(5)]; // 编译期求值,数组大小合法 int y = 10; int z = square(y); // 运行时调用,也可以 

修饰指针的含义不同

int x = 10; const int* p1 = &x; // 指向常量的指针,可改指向,不可改值 int* const p2 = &x; // 常量指针,不可改指向,可改值 constexpr int* p3 = &x; // ? 

const 指针:我们之前讨论过,规则灵活。

constexpr 指针:C++11 起,constexpr 指针必须初始化为 nullptr、0,或者静态存储期对象的地址(如全局变量、静态变量),因为它们的地址在编译期是已知的。
局部变量的地址在运行时才确定,不能用于初始化 constexpr 指针。

static int slocal = 42; constexpr int* p = &slocal; // 可以 int local = 10; constexpr int* q = &local; // 不可以!local 地址不是编译期常量 

constexpr 指针本身是常量指针(即不能改指向),而且指向的地址必须在编译期确定:

constexpr int* p = &global; *p = 100; // 可以,global 变为 100 

如果希望指向的值也不能改,可以结合 const:

const constexpr int* p = &slocal; *p = 100; // 不可以 

总结

在 C++ 中,const 关键字用于声明一个不可修改的实体,它在编译时提供语义约束并由编译器强制执行。

const 可以修饰基本类型变量、指针、引用(常引用)、函数参数(值传递、指针传递、引用传递)以及函数返回值。

在类中,const 成员函数承诺不修改非 mutable 成员变量,使 const 对象能够调用这些函数。

mutable 成员则允许在 const 成员函数中修改不影响对象逻辑状态的内部数据。

static const 成员定义类级别的常量。

而 constexpr(C++11 起)进一步要求编译期求值,可用于变量、函数和构造函数,实现真正的编译时常量,与 const 互补。

正确使用 const 能提升代码的安全性、可读性,并辅助编译器优化。

Read more

在ESP32-S3部署mimiclaw,基于deepseek并用飞书机器人开展对话-feishu

在ESP32-S3部署mimiclaw,基于deepseek并用飞书机器人开展对话-feishu

最近mimiclaw火爆,其开发团队也在密集更新,我看3天前已经可以用“飞书机器人”对话交互了。 目前网络上能查到的部署资料相对滞后,现在将飞书机器人的部署整理如下: 1. 前提 已经安装好ESP-IDF,并支持vscode编译esp32固件。 2. api-key准备 * 注册deepseek, * 创建APIkey, * 并充值,新注册的用户余额为零,无法使用 3. 飞书机器人 我是在飞书个人版中,创建的机器人。 1. 访问飞书开放平台,单击创建企业自建应用,填写应用名称和描述,选择应用图标,单击创建。 2. 左侧导航栏单击凭证与基础信息 页面,复制App ID(格式如 cli_xxx)和App Secret。 3. 配置事件订阅。 1. 在飞书开放平台左侧导航栏单击事件与回调,在事件配置页签中单击订阅方式,选择使用 长连接 接收事件,单击保存。 2. 在事件配置页面,单击添加事件,

By Ne0inhk

基于大疆MSDK实现的无人机视觉引导自适应降落功能

基于大疆MSDK实现的无人机视觉引导自适应降落功能 概述 最初需求:想要无人机在执行完航线任务后,一键落到一个指定的位置,简化人工控制。 实现一套完整的无人机自主降落功能,通过虚拟摇杆控制使无人机飞向指定位置,再利用视觉识别引导无人机精确降落到具体位置。本文中采用自适应降落策略,根据高度动态调整精度要求和下降速度,以实现安全、精确的降落。 核心点: * 虚拟摇杆导航替代FlyTo功能 * 双轴(X/Y)位置偏移实时调整 * 高度自适应降落策略 * 视觉识别引导定位 * 智能避障管理 系统架构 整体流程 否 是 高于50m 20-50m 5-20m 低于5m 是 否 是 否 否 是 用户触发Return to Vehicle 获取无人机GPS位置 计算与目标点距离 启动虚拟摇杆导航 飞向目标位置 5m/s 距离小于10m? 开始自适应降落 视觉识别系统 计算X/Y偏移量

By Ne0inhk
OpenClaw 钉钉群聊多机器人配置完全指南

OpenClaw 钉钉群聊多机器人配置完全指南

OpenClaw 钉钉群聊多机器人配置完全指南 在团队协作中,配置多个专用机器人可以显著提升工作效率——不同的机器人可以分别负责写作、编码、数据分析等不同任务。本文将详细介绍如何在使用OpenClaw的钉钉群聊中配置多个任务机器人,并进一步讲解如何为每个机器人赋予独特的性格和工作规范。 一、钉钉端配置 首先,我们需要在钉钉开放平台创建多个任务机器人。 1.1 创建机器人 1. 按照上述步骤,根据实际需求创建多个机器人。 机器人创建完成后,务必记下 Client ID 和 Client Secret,这些信息后续配置会用到。 访问 钉钉开发者平台,点击立即创建按钮创建任务机器人。 二、OpenClaw端配置 完成钉钉端的配置后,接下来我们在OpenClaw中进行相应的设置(默认已装过钉钉插件)。 # 安装钉钉渠道插件 openclaw plugins install @dingtalk-real-ai/dingtalk-connector # 重启 gateway openclaw gateway restart 2.1 添加 Agent

By Ne0inhk
3DMAX VR渲染器局部渲染设置教程

3DMAX VR渲染器局部渲染设置教程

VR 渲染器局部渲染设置 VR 渲染器的局部渲染功能灵活适配多种场景(尤其全景图),操作步骤如下: 1. 调出渲染设置面板:在 3DMAX 软件中,直接按下快捷键「F10」,快速打开渲染设置窗口(也可通过顶部菜单栏「渲染」→「渲染设置」手动调出)。 2. 确认渲染器类型:在渲染设置面板中,切换到「指定渲染器」选项卡,确保当前选定的渲染器为「V-Ray 渲染器」(若未选中,点击下拉菜单切换即可)。 1. 打开 VR 帧缓冲器:切换到「V-Ray」选项卡,找到「帧缓冲器」设置项,勾选「启用内置帧缓冲器」(部分版本默认开启),点击右侧「显示 VFB」按钮,调出 VR 帧缓冲窗口。 1.

By Ne0inhk