Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

基础语法与编译模型 / Language & Compilation

1. C++ 语言基础完整学习手册 / Complete C++ Language Basics Guide

2. C++ 语言基础与程序运行 / C++ Language Basics and Program Execution

3. C++ 程序是怎样做出来的:从源码到可执行文件 / From Source to Executable

4. 值、类型与对象:C++ 程序中的第一层现实 / Values, Types, and Objects

5. 表达式、运算符与控制流 / Expressions, Operators, and Control Flow

6. 函数、作用域与重载 / Functions, Scope, and Overloading

7. 数组、指针与字符串:从地址到借用范围 / Arrays, Pointers, and Strings

8. 引用、const 与类型推导:从复制到借用 / References, const, and Type Deduction

9. 让数据说清楚自己:枚举、结构体、联合体与类型别名 / Enums, Structs, Unions, and Aliases

10. 一个头文件怎样影响整个程序:预处理、头文件与命名空间 / Preprocessor, Headers, and Namespaces

11. 基础 I/O、错误与调试:让程序会观察自己的失败 / Basic I/O, Errors, and Debugging

本页目录

引用、const 与类型推导:从复制到借用 / References, const, and Type Deduction ​

本文属于 C++ 语言基础模块的“接口与类型系统”部分。

上一篇讲了地址、指针和借用。本篇继续追问:一个函数拿到对象时,究竟是复制一份、借用原对象,还是允许修改原对象?

本文会从可观察的程序行为开始,再进入引用绑定、对象生命周期、const、auto、decltype 和右值引用。

1. 同一个参数位置,三种不同承诺 ​

cpp
#include <string>

void receive_by_value(std::string text);
void receive_read_only(const std::string& text);
void receive_mutable(std::string& text);
1
2
3
4
5

它们都接收一个字符串,但关系不同:

text
receive_by_value
  +-- 函数得到独立的 string 对象
  +-- 修改它不影响调用者原对象

receive_read_only
  +-- 函数借用调用者对象
  +-- 不能通过参数修改对象

receive_mutable
  +-- 函数借用调用者对象
  +-- 可以修改调用者对象
1
2
3
4
5
6
7
8
9
10
11

这不是为了少写几个字符。

参数形式表达了:

text
是否复制
是否允许修改
借用是否可能被保存
资源是否可能被转移
1
2
3
4

2. 第一个实验:观察修改发生在哪里 ​

cpp
#include <iostream>
#include <string>

void by_value(std::string text) {
    text += " inside";
    std::cout << "function: " << text << '\n';
}

void by_reference(std::string& text) {
    text += " inside";
    std::cout << "function: " << text << '\n';
}

void by_const_reference(const std::string& text) {
    std::cout << "function: " << text << '\n';
}

int main() {
    std::string name = "Ada";

    by_value(name);
    std::cout << "after value: " << name << '\n';

    by_reference(name);
    std::cout << "after reference: " << name << '\n';

    by_const_reference(name);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

预期现象:

text
function: Ada inside
after value: Ada
function: Ada inside
after reference: Ada inside
function: Ada inside
1
2
3
4
5

第一次改的是副本,第二次改的是调用者对象,第三次只观察。

如果文件名是 parameter-kinds.cpp,先用简单命令:

bash
g++ parameter-kinds.cpp -o parameter-kinds
1

命令含义:

text
g++
  +-- 启动 C++ 编译器驱动

parameter-kinds.cpp
  +-- 输入源码

-o parameter-kinds
  +-- 指定输出程序名
1
2
3
4
5
6
7
8

运行:

bash
./parameter-kinds
1

PowerShell 中常见写法:

powershell
.\parameter-kinds.exe
1

3. 本文基础词小注释 ​

3.1 别名 ​

别名是给同一个对象增加另一个访问名称:

cpp
int value = 10;
int& alias = value;
1
2
text
value ----+
          +----> 同一个 int 对象
alias ----+
1
2
3

3.2 借用 ​

借用表示可以访问对象,但不负责拥有或销毁对象。

引用通常表达非空借用,std::string_view 表达字符范围的只读借用。

3.3 顶层 const ​

顶层 const 修饰变量对象本身:

cpp
int* const pointer = nullptr;
1

这里不能给 pointer 重新赋地址。

3.4 底层 const ​

底层 const 修饰指针指向的对象:

cpp
const int* pointer = nullptr;
1

这里可以改变指针指向哪里,但不能通过它修改目标 int。

3.5 临时对象 ​

没有长期名字、在表达式计算过程中产生的对象叫临时对象:

cpp
std::string{"hello"}
1

3.6 类型推导 ​

类型推导是编译器根据初始化表达式确定类型:

cpp
auto count = 3;
1

count 仍然是静态类型 int,不是运行时动态类型。

4. 引用:给已有对象另一个名字 ​

cpp
int value = 10;
int& reference = value;
reference = 20;
1
2
3

reference 不是新建的 int,而是 value 的别名。

text
名字 value -----+
                +----> 同一个对象:20
名字 reference -+
1
2
3

实验:引用赋值不是重新绑定 ​

cpp
#include <iostream>

int main() {
    int first = 1;
    int second = 2;
    int& reference = first;

    reference = second;

    std::cout << "first = " << first << '\n';
    std::cout << "second = " << second << '\n';
}
1
2
3
4
5
6
7
8
9
10
11
12

输出:

text
first = 2
second = 2
1
2

reference = second 的意思是把 second 的值写入当前绑定对象 first,不是让引用改绑到 second。

引用一旦绑定,不能像普通指针那样重新指向另一个对象。

5. 引用和指针为什么不能简单画等号 ​

常见说法是“引用就是不能为空的指针”。它可以帮助形成实现直觉,但不是完整语言定义。

方面引用 T&指针 T*
初始化必须绑定对象应该初始化
无目标状态正常语义中不能表达可用 nullptr
修改指向不能重新绑定可以
访问语法像对象本身需要 * 或 ->
指针运算不支持数组场景可支持
所有权通常不表达通常也不表达

ABI 可能把引用参数实现为地址传递,但源码层面的语义仍不同:

text
引用:别名关系
指针:保存指针值的对象
1
2

6. 绑定规则:哪些对象能被哪种引用接收 ​

先观察:

cpp
int mutable_value = 1;
const int fixed_value = 2;

int& a = mutable_value;
const int& b = mutable_value;
const int& c = fixed_value;
1
2
3
4
5
6

一个初步表格:

text
                int&        const int&       int&&
可修改左值      可以        可以             通常不行
const 左值      不行        可以             不行
临时结果        不行        可以             可以
1
2
3
4

非常量左值引用不能直接绑定普通临时量:

cpp
// int& bad = 42;
1

因为 int& 表达了可修改借用,而 42 没有适合这种借用的稳定身份。

只读引用可以观察临时量:

cpp
const int& view = 42;
1

7. 生命周期:引用活着不等于对象活着 ​

引用变量的生命周期和被引用对象的生命周期是两件事。

错误例子:

cpp
const std::string& bad() {
    std::string local = "temporary";
    return local;
}
1
2
3
4
text
进入 bad()
  |
  v
创建 local
  |
  v
返回 local 的引用
  |
  v
函数结束,local 销毁
  |
  v
调用者得到悬空引用
1
2
3
4
5
6
7
8
9
10
11
12
13

实验:不要相信“这次打印正常” ​

cpp
#include <iostream>
#include <string>

const std::string& bad() {
    std::string local = "temporary";
    return local;
}

int main() {
    std::cout << bad() << '\n';
}
1
2
3
4
5
6
7
8
9
10
11

编译器通常会发出返回局部对象引用的警告。

即使偶尔打印出正确文字,也不代表程序正确,因为对象已经结束生命周期。

7.1 直接绑定临时量 ​

cpp
const std::string& name = std::string{"Ada"};
1

这种直接绑定语境通常会把临时对象的生命周期延长到 name 的作用域结束。

但不要把延长理解成“引用可以无限转接”:

cpp
const std::string& identity(const std::string& value) {
    return value;
}

const std::string& bad = identity(std::string{"Ada"});
1
2
3
4
5

参数、返回引用和调用者的保存并不会自动建立无限生命周期。

8. const 保护的是访问路径 ​

cpp
int value = 1;
const int& read_only = value;
1
2

不能通过 read_only 修改:

cpp
// read_only = 2;
1

但原对象不是 const,所以仍可通过其他路径修改:

cpp
value = 3;
1
text
const 限制通过这条路径能做什么
不保证整个程序没有其他可写路径
1
2

这就是:

cpp
void inspect(const std::string& text);
1

通常表达“不应通过该参数修改”,而不是“程序中任何地方都不会修改”。

9. const 指针的三种写法 ​

cpp
int value = 1;

const int* p1 = &value;
int* const p2 = &value;
const int* const p3 = &value;
1
2
3
4
5

从变量名向外读:

text
const int* p1
  +-- p1 是指针,指向 const int

int* const p2
  +-- p2 是 const 指针,指向 int

const int* const p3
  +-- p3 是 const 指针,指向 const int
1
2
3
4
5
6
7
8

行为:

cpp
p1 = nullptr; // 合法
// *p1 = 2;   // 不合法

// p2 = nullptr; // 不合法
*p2 = 2;         // 合法
1
2
3
4
5

顶层和底层 const ​

text
int* const
      ^
      +-- 指针对象本身 const:顶层 const

const int*
     ^
     +-- 目标 int const:底层 const
1
2
3
4
5
6
7

按值复制通常丢掉顶层 const:

cpp
const int source = 42;
auto copy = source;
1
2

copy 是独立的 int,不是只读别名。

10. auto:静态类型的自动书写 ​

cpp
auto count = 3;
1

编译期会把它确定为:

cpp
int count = 3;
1

运行时不会再改变类型。

10.1 普通 auto 通常按值推导 ​

cpp
const int value = 42;
const int& reference = value;

auto a = value;
auto b = reference;
1
2
3
4
5

a 和 b 都是独立的 int:

text
value
  +-- reference:别名
  +-- a:值副本
  +-- b:值副本
1
2
3
4

普通 auto 通常去掉引用和顶层 const。

10.2 auto& 继续借用 ​

cpp
const int value = 42;
const auto& view = value;
1
2

view 的类型是 const int&。

这里的 & 决定了“不要复制,继续绑定”。

10.3 Range-for 的区别 ​

cpp
std::vector<int> values{1, 2, 3};

for (auto value : values) {
    value += 10;
}
1
2
3
4
5

修改的是副本。

cpp
for (auto& value : values) {
    value += 10;
}
1
2
3

修改容器元素。

cpp
for (const auto& value : values) {
    std::cout << value << '\n';
}
1
2
3

只读借用,避免复制。

11. 类型推导实验:让编译器暴露结果 ​

可以用 decltype 写一个故意不完整的模板:

cpp
template <typename T>
struct show_type;

const int value = 42;
show_type<decltype(value)> check;
1
2
3
4
5

编译器错误信息往往会把 T 展开成推导类型。

这个实验不是为了依赖错误,而是为了熟悉:

text
源码写法
  |
  v
编译器推导
  |
  v
错误信息显示类型
1
2
3
4
5
6
7

也可以在正常代码中使用:

cpp
static_assert(std::is_same_v<decltype(value), const int>);
1

这里 static_assert 是编译期断言。如果条件为假,程序在编译阶段失败。

12. decltype:询问名字或表达式的类型 ​

cpp
int value = 0;
int& reference = value;

decltype(value) a = 1;
decltype(reference) b = value;
1
2
3
4
5

结果:

text
decltype(value)      int
decltype(reference)  int&
1
2

对于未加括号的名字,decltype(name) 有特殊规则,接近询问名字的声明类型。

cpp
decltype((value)) c = value;
1

这里 (value) 是左值表达式,所以得到 int&。

核心对照:

text
decltype(x)      看未加括号的名字规则
decltype((x))    看表达式值类别规则
1
2

实验:括号改变推导结果 ​

cpp
int number = 10;

decltype(number) first = 1;
decltype((number)) second = number;

second = 20;
1
2
3
4
5
6

second 是引用,所以 number 变成 20。

13. 值类别:左值、右值到底在描述什么 ​

左值和右值不是变量类型,而是表达式类别。

先记住最有用的直觉:

text
左值 lvalue
  +-- 有稳定身份,通常可以取地址

右值 rvalue
  +-- 更像计算结果或可被消费的值
1
2
3
4
5
cpp
int value = 10;

value       // 左值
value + 1   // 纯右值
42          // 纯右值
1
2
3
4
5

值类别影响:

text
能否绑定某种引用
是否能修改原对象
是否适合移动资源
1
2
3

13.1 更完整的分类 ​

text
表达式
  |
  +-- glvalue:有对象身份
  |     |
  |     +-- lvalue:普通具名对象
  |     +-- xvalue:有身份但资源可复用
  |
  +-- prvalue:纯计算值或初始化结果

rvalue = prvalue + xvalue
1
2
3
4
5
6
7
8
9
10

基础阶段不必立刻背完分类名。先能回答:

text
表达式有没有稳定对象身份?
能不能绑定 T&?
是否适合把资源交给移动操作?
1
2
3

14. 右值引用和 std::move ​

cpp
std::string text = "hello";
std::string moved = std::move(text);
1
2

std::move(text) 本身不移动字符。

它只是把表达式转换成可供移动构造选择的形式。

text
std::move(text)
  +-- 改变表达式值类别

string 移动构造
  +-- 可能接管 text 的字符资源
1
2
3
4
5

移动后 text 仍是有效对象,但具体内容通常是有效而未指定。

不要写成“移动后一定为空”。

14.1 具名右值引用是左值表达式 ​

cpp
std::string&& reference = std::string{"hello"};
1

虽然变量类型是右值引用,但表达式 reference 有名字和身份,所以它作为表达式是左值。

再次移动时:

cpp
std::move(reference);
1

15. constexpr 和 const 的区别 ​

cpp
const int runtime_value = read_value();
constexpr int compile_time_value = 42;
1
2

前者表示初始化后不能通过该名字修改,但值来自运行时。

后者要求能在编译期求值:

cpp
constexpr int square(int value) {
    return value * value;
}

constexpr int size = square(4);
1
2
3
4
5

同一个 constexpr 函数也能运行时调用:

cpp
int input = read_value();
int result = square(input);
1
2

提示:

text
consteval
  +-- 调用必须产生编译期结果

constinit
  +-- 要求静态存储期变量进行静态初始化
1
2
3
4
5

不要把这些词和普通 const 混为一谈。

16. 借用视图和保存视图的危险 ​

cpp
class View {
public:
    explicit View(std::string_view text) : text_(text) {}

private:
    std::string_view text_;
};
1
2
3
4
5
6
7

这个类保存借用关系。

危险调用:

cpp
View view{std::string{"temporary"}};
1
text
temporary string
  +-- owner,负责字符生命周期

string_view
  +-- borrower,只保存地址和长度

完整表达式结束
  |
  v
owner 销毁
  |
  v
borrower 悬空
1
2
3
4
5
6
7
8
9
10
11
12
13

如果类必须保存内容,更直接的设计是取得所有权:

cpp
class Owner {
public:
    explicit Owner(std::string text)
        : text_(std::move(text)) {}

private:
    std::string text_;
};
1
2
3
4
5
6
7
8

17. 参数设计:从对象关系出发 ​

text
需要独立副本?
  -> 按值传递

必须存在,只读借用?
  -> const T&

必须存在,可修改?
  -> T&

允许没有对象?
  -> T* 或更明确的可选观察关系

需要转移资源?
  -> 按值、unique_ptr 或资源专用类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14

17.1 按值传递不一定慢 ​

cpp
void consume(std::string text);
1

调用者可以选择:

cpp
consume(existing);           // 复制或省略复制
consume(std::move(existing)); // 允许转移资源
1
2

如果函数本来就需要一份独立对象,按值参数可能比“先 const 引用再复制”更清晰。

17.2 小对象不必强行引用 ​

cpp
void f(int value);
1

对很小的值类型,按值传递通常简单直接。const int& 可能带来不必要的间接访问和生命周期复杂度。

选择时同时考虑:

text
对象大小
复制成本
所有权
是否保存
调用者是否需要继续使用原对象
1
2
3
4
5

18. 调用约定的运行时直觉 ​

函数参数需要按照平台 ABI 传递。对于:

cpp
void update(int& value);
1

实现上经常可以把引用理解为传递对象地址:

text
调用者
  |
  | 传递 value 的地址
  v
update()
  |
  +-- 通过引用语义访问同一个 int
1
2
3
4
5
6
7

但源码层面仍然不同:

text
引用不能正常表达空值
引用不能重新绑定
引用不能做指针算术
1
2
3

值参数和引用参数的本质区别是对象关系,而不是某条固定汇编指令。

19. 常见误解和实验排查 ​

误解一:引用赋值会重新绑定 ​

cpp
int a = 1;
int b = 2;
int& ref = a;
ref = b;
1
2
3
4

结果是 a 变成 2。

误解二:const 对象绝不会改变 ​

只读引用限制自己的访问路径:

cpp
int value = 1;
const int& view = value;
value = 2;
1
2
3

误解三:普通 auto 保留引用 ​

cpp
int value = 1;
int& ref = value;
auto copy = ref;
copy = 2;
1
2
3
4

value 仍是 1,因为 copy 是值副本。

误解四:decltype(x) 和 decltype((x)) 相同 ​

括号把名字放进表达式规则,结果可能变成引用。

误解五:std::move 立即清空对象 ​

它只改变值类别,真正的移动由后续构造或赋值完成。

误解六:右值引用变量本身是右值 ​

具名变量表达式通常是左值。

20. 用编译器检查类型和借用错误 ​

类型错误通常能被编译器发现:

cpp
int value = 1;
const int& read_only = value;

// read_only = 2; // 编译错误
1
2
3
4

但生命周期错误可能需要警告、Sanitizer 或人工追踪:

cpp
std::string_view bad() {
    return std::string{"temporary"};
}
1
2
3

检查借用接口时要同时问:

text
类型是否允许这种绑定?
owner 什么时候销毁?
borrower 是否会被保存?
底层存储是否可能重新分配?
1
2
3
4

21. 最小实验路线 ​

依次完成:

  1. 比较值传递和引用传递;
  2. 让 const T& 绑定临时字符串;
  3. 返回局部对象引用,观察警告;
  4. 比较 auto、auto&、const auto&;
  5. 比较 decltype(x) 与 decltype((x));
  6. 观察 std::move 前后对象状态;
  7. 保存 string_view 并测试临时字符串;
  8. 用 -S 观察值参数和引用参数。

查看汇编:

bash
g++ -std=c++17 -S parameter-kinds.cpp -o parameter-kinds.s
1
text
-S
  +-- 生成汇编文本,不生成最终可执行文件

-o parameter-kinds.s
  +-- 指定汇编输出文件名
1
2
3
4
5

不必先背指令,先观察:

text
值参数是否操作副本?
引用参数是否需要找到原对象?
1
2

22. 自测与答案 ​

引用能不能重新绑定? ​

不能。引用赋值是在修改当前绑定对象。

const int& 能不能绑定普通 int? ​

可以。它创建只读访问路径,不会让原对象自动变成真正的 const 对象。

auto x = const_value 通常是什么类型? ​

独立的非 const 值类型,例如 int。

decltype((value)) 为什么常是引用? ​

因为带括号的具名变量是左值表达式,decltype 根据值类别得到引用类型。

std::move 做了什么? ​

把表达式转换为适合移动操作选择的值类别,本身不执行资源转移。

保存 string_view 最重要的检查是什么? ​

确认字符 owner 的生命周期和存储稳定性覆盖 view 的整个使用期。

23. 本篇总结与下一篇连接 ​

text
调用者对象
  |
  +-- 按值 ------------> 函数得到独立副本
  |
  +-- T& --------------> 函数借用并可修改
  |
  +-- const T& --------> 函数借用但只读
  |
  +-- T&& -------------> 函数可接收并可能消费资源
1
2
3
4
5
6
7
8
9

类型推导决定这些关系是否保留:

text
auto
  +-- 通常按值推导,可能丢掉引用和顶层 const

auto&
  +-- 保留借用关系

const auto&
  +-- 保留只读借用关系

decltype
  +-- 根据名字或表达式值类别精确询问类型
1
2
3
4
5
6
7
8
9
10
11

下一篇 枚举、结构体、联合体与别名 会继续回答:

text
如果一个对象由多个字段组成,
这些字段在内存中如何排列?
1
2

它会把本文的引用、类型、对象表示和内存布局继续连接起来。

最后更新于:

Pager
上一篇7. 数组、指针与字符串:从地址到借用范围 / Arrays, Pointers, and Strings
下一篇9. 让数据说清楚自己:枚举、结构体、联合体与类型别名 / Enums, Structs, Unions, and Aliases

持续记录,持续成长

Copyright © Tidenflow