关于 const 的一切
引言
const 是 C++ 语言中的一个关键字,用于声明常量。它可以用于变量、指针、引用、成员函数等,表示这些实体的值在初始化后不能被修改。正确使用 const 可以提高代码的可读性、安全性和优化性能。
本文将使用简单示例并结合 C++/WinRT、Windows App SDK、WinUI 3 实战,包括:
- 顶层/底层 const、auto/模板推导影响
- 成员函数 const 与 this 指针类型、基于 const 的重载、迭代器语义
- mutable 与“逻辑常量性”、缓存场景
- 返回值/生命周期、NRVO、const 抑制移动的影响
- 内存布局与段(.rdata/栈/静态存储期),const 不改变对象布局
- C++/WinRT/WinUI 3 实战:事件处理参数签名(IInspectable const&/Args const&)、VM 属性 getter const noexcept、hstring/param::hstring 选择、只读集合 IVectorView、span/array_view 的只读视图
- API 设计清单与进阶示例(只读缓冲、可移动/只读接口分离)
什么时候使用 const
- 函数参数:
- 如果函数参数是一个指针或引用,并且函数不需要修改该参数的值,可以将其声明为
const。例如:
- 如果函数参数是一个指针或引用,并且函数不需要修改该参数的值,可以将其声明为
cpp
void print(const string& str);- 成员函数:
- 如果成员函数不修改类的成员变量,可以将其声明为
const。例如:
- 如果成员函数不修改类的成员变量,可以将其声明为
cpp
class MyClass {
public:
void display() const;
};- 局部变量:
- 如果局部变量在初始化后不需要修改,可以将其声明为
const。例如:
- 如果局部变量在初始化后不需要修改,可以将其声明为
cpp
const int max_value = 100;- 返回值:
- 如果函数返回一个指针或引用,并且不希望调用者修改返回的对象,可以将其声明为
const。例如:
- 如果函数返回一个指针或引用,并且不希望调用者修改返回的对象,可以将其声明为
cpp
const string& getName() const;示例
以下是一些使用 const 的示例:
cpp
#include <iostream>
#include <vector>
using namespace std;
void print(const vector<int>& vec) {
for (const auto& elem : vec) {
cout << elem << " ";
}
cout << endl;
}
class MyClass {
public:
MyClass(int value) : value(value) {}
void display() const {
cout << "Value: " << value << endl;
}
private:
int value;
};
int main() {
const int max_value = 100;
vector<int> numbers = {1, 2, 3, 4, 5};
print(numbers);
MyClass obj(42);
obj.display();
return 0;
}在这些示例中,const 关键字用于函数参数、成员函数、局部变量和循环变量,确保这些变量在使用过程中不会被修改。
深入理解 const:函数运行原理、类型系统与内存布局
下面从 C++ 类型系统、调用约定、对象模型与 C++/WinRT(Windows App SDK、WinUI 3)实践的角度深入理解 const。
1) 顶层/底层 const 与类型推导
- 顶层(top-level)const:限定对象本身不能被重新赋值。
int* const p:p是常量指针(指针本身不可变),但指向的int可变。
- 底层(low-level)const:限定对象指向/引用的目标不可变。
int const* p或const int* p:指向常量int的指针(指向目标不可修改)。
- 引用总是底层 const 的别名:
int const& r = x;表示通过r不能改x。 - 模板与 auto 推导:
auto x = expr;会去掉顶层const;auto&/auto const&保留底层const。- 模板参数
T对T const&仅推导T,底层const体现在引用层。
要点:顶层 const 影响“能否给变量重绑定/赋值”,底层 const 影响“能否通过该别名修改目标”。
2) 成员函数的 const 与 this 指针
void f() const的隐式this类型为MyClass const* const:- 成员函数内不能修改非
mutable成员; - 不能把
this重新绑定到别处(顶层 const)。
- 成员函数内不能修改非
- 可以对同名函数基于
const重载:
cpp
struct Buffer {
uint8_t* data(); // 可变访问
uint8_t const* data() const; // 只读访问
};- 引用限定符与
const配合:T&&成员函数(右值限定)常用于启用移动;const会禁止移动(见下文)。
3) 迭代器与容器的 const 语义
begin()与begin() const返回不同类型:非常量对象给可写迭代器,常量对象给只读迭代器。- 为自定义容器提供成对的
const/非const访问器有助于“只读视图”。
4) mutable 与“逻辑常量性”
mutable成员允许在const成员函数中被修改,用于缓存、统计计数等逻辑不破坏抽象不变量的场景。- 很多带引用计数的类型(如字符串、小对象优化容器)内部的计数实现通常被视为逻辑常量性的一部分; 在 C++/WinRT 的
winrt::hstring中,返回值拷贝成本低(共享底层HSTRING)。
5) 返回值、生命周期与移动
- 不要返回“指向局部对象的
const&”——悬垂引用。 - 给临时对象绑定
const&可延长临时生命周期到该引用作用域结束:
cpp
const std::string& r = std::string("tmp"); // OK,临时延长到 r 的作用域结束“返回
const值”的反模式:const T f();对调用者几乎无益,反而妨碍移动(不能对const值调用移动赋值/构造)。- 更佳:直接返回
T(值),由编译器进行返回值优化(NRVO)或移动。
const会抑制移动:
cpp
std::string s;
std::string const sc;
auto x = std::move(s); // 移动
auto y = std::move(sc); // 仍然是拷贝,因为对象是 const- 设计参数时:只读大对象用
T const&,可转移所有权用T(按值)或T&&,避免把需要移动的值声明为const。
6) 内存布局与段(MSVC/Windows 视角)
const不改变对象的内存布局(字段顺序、vptr、对齐均不变)。- 作用域与存储期:
- 局部
const通常在栈上; - 具有静态/全局存储期的
const(尤其带内部链接)常放在只读节(如.rdata); - 字符串字面量放在只读节,共享存储,修改是未定义行为。
- 局部
mutable成员与常量对象同样位于对象内存中;对象整体不一定在只读节,是否放入只读节由存储期与链接属性决定,而非类型的const与否。
7) 与 C++/WinRT、Windows App SDK、WinUI 3 的实践
- 事件处理签名采用只读引用,避免拷贝:
cpp
void MainWindow::OnClick(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
{
// sender/e 语义上只读;通常无需也不应修改
}- 属性(Property)/绑定(Binding):
- ViewModel 的只读 getter 建议
const noexcept,表达只读与不抛异常:
- ViewModel 的只读 getter 建议
cpp
struct PersonViewModel : winrt::implements<PersonViewModel, winrt::Windows::UI::Xaml::Data::INotifyPropertyChanged>
{
winrt::hstring Name() const noexcept { return m_name; } // 按值返回,hstring 开销低
void Name(winrt::hstring const& v) { if (m_name != v) { m_name = v; RaiseChanged(L"Name"); } }
private:
winrt::hstring m_name;
};hstring按值返回通常高效;对大型缓冲区参数,优先使用视图:- 文本:
winrt::param::hstring或winrt::hstring const&; - 数组/缓冲:
winrt::array_view<T const>或std::span<T const>。
- 文本:
只读集合视图:
- WinRT 接口按值返回智能句柄式对象(拷贝成本低),但可通过只读接口暴露:
cpp
winrt::Windows::Foundation::Collections::IVectorView<int> Items() const; // 只读视图IDL/ABI 边界:
- WinRT IDL 不存在 C++ 的
const成员函数概念;只读通过readonly属性表达; - C++/WinRT 投影会将逻辑只读的成员生成为
const成员函数,便于在 C++ 侧进行const校验与重载。
- WinRT IDL 不存在 C++ 的
线程与 UI 亲和性:
const不等于线程安全;WinUI 3 UI 类型需要在 UI 线程访问;- 即使成员函数是
const,若触及 UI 状态仍需切回 UI 线程(如co_await winrt::resume_foreground(DispatcherQueue()))。
Lambda 捕获与事件:
- 在事件回调中优先捕获只读引用或值的只读视图,避免不必要拷贝与数据竞争;
auto const handler = [...] { ... };明确只读意图。
8) API 设计清单(结合 WinUI 3/C++/WinRT)
- 输入参数:
- 文本:
winrt::param::hstring(最灵活),或winrt::hstring const&。 - 缓冲:
std::span<T const>或winrt::array_view<T const>。 - 大对象:
T const&;小标量按值。
- 文本:
- 返回值:
- 值语义类型(如
hstring、智能句柄、WinRT 接口)按值返回; - 大型内部容器可返回只读视图接口或
std::span<T const>; - 避免
const T(值)返回类型。
- 值语义类型(如
- 成员函数:
- 只读操作标记
const noexcept; - 提供成对的
const/非const访问器以区分读写; - 需要缓存时用
mutable并保证线程安全。
- 只读操作标记
9) 进阶示例
- 指针/引用 const 组合:
cpp
int x = 0;
int* const p1 = &x; // 指针本身不可变
int const* p2 = &x; // 指向只读 int
int const* const p3 = &x;// 指针与目标都只读
void foo(int const& r);
foo(x); // 通过只读引用传参- 只读缓冲参数(WinRT/C++20):
cpp
void Process(std::span<uint8_t const> bytes);
// 或 C++/WinRT 传统写法:
void Process(winrt::array_view<uint8_t const> bytes);- 事件处理与绑定:
cpp
void MainWindow::OnSelectionChanged(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Controls::SelectionChangedEventArgs const& e)
{
// 读取 e.AddedItems() 等,不修改 e 本身
}- 可移动与 const:
cpp
std::vector<int> Build(); // 返回值可移动/优化
void Use(std::vector<int> v); // 需要所有权时按值接受
void Read(std::vector<int> const& v); // 只读访问结论:
const是类型系统的组成部分,既影响重载/推导,也影响接口设计与可读性;- 它不改变对象布局或调用约定,但可能影响优化(启用只读路径、消除写屏障);
- 在 C++/WinRT、WinUI 3 中,合理运用
const(只读 getter、参数视图、事件只读引用)可提升 API 清晰度与效率,同时要避免用const阻断必要的移动与可变操作。