
(This picture comes from: VTuberized Logos)
参考教程:01 程序的内存模型 - 内存四区 - 代码区 - 哔哩哔哩 bilibili(黑马程序员)
- [3] 想写出好程序,不必对 C++ 掌握到巨细靡遗。
- [4] 把力气用在编程技术上,别死磕语言特性。
Lesson25 内存分区
25.1 C++ 内存分区简介
C 语言的内存分配参见 Lesson20 4.2 和 10
C++ 的内存分区与 C 语言的大体相同,只是一些分区有所调整:
- 代码区 (Code Segment)
存放函数体的二进制代码。内存由操作系统进行管理。 - 全局 / 静态区 (Global/Static Storage)
存放全局变量、静态变量以及常量,C 语言中常量区、data 段和 bss 段的综合 - 栈区 (Stack)
存放函数的参数值、局部变量等。内存由编译器自动分配与释放。 - 堆区 (Heap)
内存由程序员分配与释放。若程序员没有释放,程序结束时由操作系统自动回收。
不同区域存放的数据具有不同的生命周期和处理方式,有助于提高编程的灵活性。同时减少读取时的操作开销,提高效率。
程序需要以不同方式对待它的不同内存块。例如,有些进程应当执行它的代码,而不是执行它的数据;有些进程应当写入数据,而不是修改它的代码;有些进程需要和其他进程共享一部分内存,但不是全部内存;有些内存是只读的,但有些内存可读可写。
语言标准还是具体实现
有的人 [1] 依照 C++ 标准认为 C/C++ 中的内存分区没有堆和栈,数据是在内存的任意位置中存储的,变量只有自动 / 动态存储期 [2] 之分。我们在内存分区中常用的堆和栈概念是由特定的操作系统(具体地说,是 Linux 系统)实现的,而其他平台可能使用的是其他模式。
如果不和编译器或者硬件驱动打交道,我们不必关心这些问题。大部分操作系统都使用相似的内存分区。特别对于二进制逆向,我们在 IDA 中看到的可不是所谓 “自动存储期” 之类,而是 .text 等实际的内存分区。
这个问题还有一个更现实的回答:在教学与工作中,人们已经接受了内存分区的说法。那些人所坚持的理论可能无法被接受,因为其对一般开发带来了不必要的负担。
Stroustrup 的 Programing Principles and Practice Using C++ 中有提到可以将 “自动内存” 称为 “栈内存”,而 “动态内存” 可称为 “堆内存” 或 “动态内存”。另外,在该书中提出的内存分区是(这就偏向语言规则而不是具体实现了):
![]()
(据 Programing Principles and Practice Using C++, 3rd Edition 重绘)
new
运算符在一块叫自由存储(free store,也叫动态内存)或堆的区域里分配内存。 分配在自由存储上的对象,与其被创建的作用域无关, 而是会一直 “存活” 下去,直到用delete
运算符把它销毁。定义在
<array>
中的array
是个给定类型的定长序列, 其中的元素数量要在编译期指定。 因此,array
的元素可以分配在栈上,在对象里或者在静态存储区。
C doesn’t specify any particular memory layout, so you need to refer to a specific C implementation.
——Is this the layout of memory in C? - Stack Overflow
In general though the answer to these questions are not rellavent to a general C++ programmer (with exceptions like compiler/device driver writers).
There’s very little that’s actually definitive about C++ memory layouts. However, most modern OS’s use a somewhat similar system, and the segments are separated based on permissions.
——How is the memory layout of a C/C++ program? - Stack Overflow
如果你是指字面上的 “堆栈”,事实是全文检索 C 标准文档根本没有 stack 和 heap 这两个词。
如果你是想讨论是否有这个层次的抽象,那大多数语言都已经抽象掉了这两个概念,包括 C。
如果你在讨论是否存在这一层次的底层实现,基本没有什么主流操作系统跑得掉。
—— 在语言中比如 c 为什么要设计堆栈呢?解决了什么问题? - 知乎
任何语言除了汇编是不会有内存分区的概念的,内存本身就是物理层面的东西,所以 C++ 中才会有生命周期的抽象概念。堆栈的内存分区实际是系统层面的,体现了系统在程序运行时如何调用运行等等过程,和实际的程序运行分不开,作为写 C/C++ 的人来说了解这些肯定是有利的,便于准确定位一些 bug 的位置。
——【辟谣】C++ 根本没有堆和栈!的评论区
25.2 程序运行前的内存分区
代码编译后生成了 .exe 可执行文件。在执行前,该程序的内存分为两个区域:
-
代码区
-
存放 CPU 执行的机器指令
-
代码区的内存是共享的。一个体现是对于频繁被执行的程序,在内存中只需要存一份代码。
Note程序需要有限度地共享数据。比如:所有进程应使用同一个编译器;两个进程间可能想要共享一些数据。
-
代码区的数据是只读的,以防止程序意外修改了自己的指令。
-
-
全局区
-
存放全局变量和静态变量
Note全局变量在所有函数(包括
main
函数)之外声明,通常在代码的开始处。它们在整个程序的执行期间都存在,并且在程序的任何地方都可以被访问和修改。——【新手解答 4】深入探索 C 语言:全局变量声明、全局函数声明 + 宏定义 - 阿里云开发者社区)
静态变量使用
static
关键字修饰,在程序刚开始运行时就完成初始化,也是唯一的一次初始化。——c++ 静态变量(static) - USTC 丶 ZCC - 博客园
C 语言中的静态变量在函数多次调用时仍能保留它的值。它保留了上一次函数执行时的值,这个值在下一次函数执行时不会被初始化掉。
-
还包含常量区,存放字符串常量和其他常量(如全局常量)
-
该区域的内存在程序结束后由操作系统释放
-
演示:
1 |
|
1 | 全局变量a_g的地址是:00007FF6C32BD000 |
由该演示可知,全局变 / 常量、静态变量和字符串常量存储在同一个内存段,而局部变 / 常量存储在另一个内存段。
25.3 程序运行时的内存分区
执行时,程序还有以下两个内存分区:
-
栈区
- 存放函数的参数值、局部变量等。
- 内存由编译器自动分配释放。
利用指针访问存储在栈区的变量时,需要注意变量的作用域。比如:
1
2
3
4
5
6
7
8
9
10
11
12
int* func(void) {
int a = 10;
return &a;
}
int main(void) {
int* p = func();
printf("%d\n", *p);
printf("%d\n", *p);
}1
2
310
-858993460func
所占用的内存在函数执行完毕后即被释放,也就是说,赋值时func
返回的地址指向的内存位置上不是10
,而是一个随机数了,因此第二个printf
输出了一个非常奇怪的数。在 VS2022 上,编译器会提示
warning
:1
警告 C4172 返回局部变量的地址或临时 : a
“临时” 是什么东西临时变量是在程序执行过程中临时存储数据的变量。它们在程序中被创建并用于存储临时的数据,一旦不再需要,它们就会被销毁。临时变量通常用于在程序中进行一些计算、操作或存储中间结果。(C++ 中产生临时变量的常见场景总结与建议 - CSDN 博客)
临时变量大多数情况下是算数表达式的结果。(C++ 临时变量)
那第一个
printf
确实输出了正确的数值,为什么?这其实是编译器的功劳。编译器认为你可能误用了局部变量,所以会为你保留一次a
的初始值。另外,函数的形参也存储在栈区。
-
堆区
-
内存由程序员分配释放。若程序员不释放,程序结束时由操作系统回收。
-
C++ 使用
new
操作符在堆区开辟动态内存。开辟的内存需要用指针接收并访问。
演示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int* func() {
int* p = new int(10);
return p;
}
int main(void) {
int* p = func();
int a = 20;
printf("%p\n", &a);
printf("%d %p %p\n", *p, p, &p);
printf("%d %p %p\n", *p, p, &p);
printf("%d\n", *p);
}1
2
3
400000070928FF604
10 0000026005F44A00 00000070928FF5E8
10 0000026005F44A00 00000070928FF5E8
10我们使用
new
申请了一块大小为int
宽度的内存,并将其初始化为 10。如果这部分内存放在栈上,那我们第二次通过指针访问这个值时应该不会输出 10,但是它输出了 10,说明操作系统是不会动这块内存的。另外,指针接收到的地址是存储在栈区的。
-
25.3.SP 对象
什么是对象 (Object)?
以下是一些关于对象的表述:
对象 是一个在内存中占据了一定空间的有类型的东西。因而,它必然是与计算机内存这个物理上具体存在的设备关联在一起的一个物质。
——3. 值与对象 — Understanding Modern C++ 1 文档
对象代表了一段可以存储值的内存区域。变量的本质就是有名字(识别符)的对象。
宽泛地讲,对象是任何一个未命名的实例,比如变量和函数。但在 C++ 中,函数不属于对象。
总的来讲,对象就是一块可以存储值的内存区域。
对于一个对象,我们主要看它的四个要素:
-
类型
类型决定了对象在内存中存储的字节大小与二进制的判读方式
-
标识符
标识符是数字、下划线、大小写拉丁字母(和以
\u
及\U
转义字符)指定的。程序员通过标识符访问标识符代表的内存对象。
在 C++ 中,直接访问内存是很难受的,所以我们需要通过对象来间接访问内存。我们只需要专注于怎么存储并检索对象,而不需要关心这些对象放在内存的哪个具体位置,因为编译器代劳了。
虽然 C++ 中的对象可以没有名字,但我们大多仍会使用标识符来命名对象。有名字的对象叫作变量。为对象命名可以让我们再次使用它们。
-
地址
对象按照语言标准和 ABI (Application Binary Interface) 存储在内存中,我们可以通过
&
运算符来获取对象的内存存储地址。 -
值
值用来初始化对象。
值的本质A single piece of data is called a value. Some instances of useful concept that can be represented as data. Common examples of values include letters (e.g.
a
), numbers (e.g.5
), and text (e.g.Hello
).——1.3 — Introduction to objects and variables – Learn C++
简单说, 值 是一个纯粹的数学抽象概念,比如数字
10
,或者字符'a'
, 或者布尔值false
,等等。它们完全不需要依赖于计算机或者内存而存在,就只是一个纯粹的值:不需要存储到内存,当然也就不可修改。那么
1+2
呢?这是一个表达式,但这个表达式的求值结果也是一个 值 。因而,这是一个值类别的表达式 。 而数字10
同样是一个表达式,其求值的结果毫无疑问也是一个 值 —— 它自身。 因而,在这个角度,1+2
和数字10
,从性质上没有任何区别,都是 值 类别的表达式。
An object, in C++, has
- size (can be determined with
sizeof
); 内存大小 - alignment requirement (can be determined with
alignof
); 对齐要求 - storage duration (automatic, static, dynamic, thread-local); 存储期
- lifetime (bounded by storage duration or temporary); 生存期
- type; 类型
- value (which may be indeterminate, e.g. for default-initialized non-class types); 值
- optionally, a name. 名字(可选)
The following entities are not objects: value, reference, function, enumerator, type, non-static class member, template, class or function template specialization, namespace, parameter pack, and this.
这些不是对象:值、引用、函数,等等。
25.4 new
操作符和 delete
操作符
new
操作符用于在堆上动态分配内存。
new
操作符在 <new>
头文件中定义,不过,通常情况下,标准库已经包含了这个头文件,所以你不需要显式地包含它。但是,为了确保代码的可读性和规范性,建议显式地包含 <new>
头文件。
new
操作符的语法为:
1 | new type[size](initializer) |
这里的一些参数是可选的。下面介绍分情况下 new
操作符的用法:
-
只分配一个对象的内存:
1
type* pointer = new type;
-
分配一个对象的内存,并将其初始化为
initializer
:1
type* pointer = new type(initializer);
-
分配包含
size
个对象的数组的内存:1
type* pointer = new type[size];
动态分配的内存会一直保持分配状态,直到它被显式释放或直到程序结束。
delete
操作符用于释放之前使用 new
分配的内存。
delete
和 new
一样在 <new>
头文件中定义,也包含在标准库中,不需要再引用 <new>
。
接下来介绍 delete
的用法:
-
释放单个对象的内存:
1
delete pointer;
-
释放数组的内存:
1
delete pointer[];
动态内存释放完后,就不能再访问,否则程序无法正常退出:
1 |
|
1 | 警告 C6001 使用未初始化的内存“p”。 行13 |
1 | 10 |
你可能更熟悉 C 语言中的 malloc()
和 free()
。但在 C++ 中,我们最好入乡随俗,用 new
和 delete
作为替代。
[5] 不要使用 malloc()
。new
操作符可谓青出于蓝而胜于蓝。别仅仅用 “裸” 的 new
和 delete
替换 malloc()
和 free()
。
在 C 里,void*
(在 malloc()
的声明中出现) 可在赋值操作中作为右值操作数, 或者用在任何指针类型变量的初始化中; 这种做法在 C++ 里行不通。在两种语言里,都要把 malloc()
的结果转化到正确的类型。 如果你只用 C++ ,请避免使用 malloc()
。
Lesson26 引用
26.1 引用的定义
引用变量 (Reference) 是一个别名 (Alias, or alternate name),它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。
你可以把别名看作一个人的绰号。只要人们接受了这个绰号,那么你就可以用这个绰号指代对应的那个人了。
引用变量的声明:
1 | type& ref = var_name; |
在声明引用时,现代 C++ 程序员更倾向于将 &
放在类型旁边(而不是引用变量名称旁边),因为这样程序员可以更清楚地认识到自己声明的是一个引用定义,而不是一个含有 "&" 字符的某类型变量。
在 Reference declaration - cppreference.com 中提到,该声明对应的是左值引用 (Lvalue Reference),而 C++11 标准还引入了右值引用 (Rvalue Reference),用的是 &&
。
這裡 type&
的 &
不要用「取址」的概念去解釋,雖然看起來有點關係,但觀念真的很容易亂掉。宣告時的 &
,在觀念不熟的情況下,請先當作另外一回事。
我们可以通过引用来访问并修改变量的内容:
1 |
|
1 | Value of i : 5 |
26.2 引用声明的注意事项
-
引用必须在创建时被初始化。
Note引用必须被初始化为指向一个合法的对象或函数。
——Reference declaration - cppreference.com
因为引用具有 对象别名 的语义,因而没有 绑定 到任何对象的引用,从语义上就不成立。
由于必须通过初始化将引用绑定到某一个对象,因而从语义上,不存在 空引用 的概念。
-
一旦创建了一个引用,就不能再让它引用另一个对象。
You can’t separate (分离) the reference from the referent.
Unlike a pointer, once a reference is bound (绑定) to an object, it can not be “reseated”(重定向) to another object. The reference isn’t a separate object. It has no identity. Taking the address of a reference gives you the address of the referent. Remember: the reference is its referent (指示对象).
引用必须依附于它初始指向的对象。引用不是对象。对引用取地址,得到的仍是其指向的对象的地址。
记住:引用就是其指向的对象。
注意不要将赋值误认为是更改引用,比如:
1
2
3
4int a = 10;
int& b = a;
int c = 20;
b = c;//这是赋值a = c
和b = c
是等效的。好比给舍友带饭,不论叫绰号还是叫全名都可以把饭交给对方。Remember: the reference is the referent, so changing the reference changes the state of the referent.
改变引用的状态,也会改变被引用对象的状态。
1
&b = c;//这是更改引用
VS2022 的 IntelliSense 会告诉你:
1
错误(活动) E0137 表达式必须是可修改的左值
这段代码也不能通过编译。
-
在大多数情况下,引用只会绑定到与引用类型匹配的对象。如果你将引用绑定到与其引用类型不匹配的对象,编译器将尝试隐式地将对象转换为引用类型,然后将引用绑定到该对象。
-
引用必须指向一个合法的内存(在栈区和堆区的内存)。指向一个不再存在的对象的引用被称为悬空引用 (Dangling Reference)。访问悬空引用会导致未定义的行为。
-
引用本身不是对象,因此引用本身一般不占存储单元。例如,对引用求地址,就是对目标变量求地址。
Warning引用不是对象。引用不占用内存,除非编译器认为有必要,例如非静态的引用类型数据成员会增加类的大小,因为类要存储地址。
由于引用不是对象,所以没有引用数组、引用指针和引用的引用:
1
2
3int& a[3]; // error
int&* p; // error
int& &r; // error——Reference declaration - cppreference.com
不能建立引用数组(数组中的元素不能是引用),但是可以建立数组的引用和数组元素的引用:
1
2
3
4
5int& arr[3] = {2,3,4};//声明"引用数组"是错误的,arr并没有自己的空间来存放后面的值
//--------------------------------
int arr[3] = {2,3,4};//arr是数组变量名,int[3]是类型
int (&ref)[3] = arr;//正确,&ref是引用名,int[3]是类型
int& p = arr[3];//可行
26.3 引用作为函数参数
在 C 语言中,我们介绍了按值传递和按地址传递两种传参方式。下面我们介绍 C++ 提供的第三种传参方式:按引用传递 (Pass by Reference, or Call by Reference)。
「call by reference」是 C++ 才有的「更方便」的東西,但也因為這個「方便」,導致觀念如果不穩,就會像我一樣把這個「方便」的功能,變成「符號意義大混亂」。因此,如果這三個你現在很混亂,建議先從「call by value, call by address (pointer)」搞清楚再說!!!
在按引用传递方法中,实际参数的内存地址(引用)被传递给函数,允许直接访问和修改原始值。实际参数和形式参数指向相同的内存地址。在函数中对参数所做的任何更改都会直接反映在函数外的原始值中。这和按地址传递的作用相似,但相比指针作为参数,引用作为参数要更加 “自然”。
1 |
|
input:
1 | 2 3 |
output:
1 | 3 2 |
26.4 引用作为函数返回值
C++ 的函数可以返回一个引用,方式与返回一个指针 (Lesson20 4.3) 类似。
1 |
|
1 | 10 |
这里有几个注意事项:
-
不要返回局部变量的引用,或者说,不要返回一个内存已被释放的变量的引用。
1
2
3
4
5
6
7
8
9
10
11
12
int& test() {
int a = 10;//局部变量
return a;
}
int main(void) {
int& q = test();
printf("%d\n", q);//第一次读取,编译器进行保留
printf("%d\n", q);//第二次读取,保留失效,程序试图读取引用指向的内存区域
}1
2
310
-858993460这和我们 [24.3](#24.3 程序运行时的内存分区) 讲的错误原因是一样的,引用指向了一个本不存在的、混沌的内存位置。
本节开头的示例使用
static
修饰局部变量,扩大了变量的作用域,使得变量的内存不会在函数执行完成后被释放。Warning虽然一个返回一个局部
static
的设计的例子是合理的,至少在单线程的环境中是这样。但就像所有使用了static
对象的设计一样,这个也会立即引起我们的线程安全(Thread-safety)的混乱。请记住一个引用仅仅是一个名字,一个实际存在的对象的名字。无论何时只要你看到一个引用的声明,你应该立刻问自己它是什么东西的另一个名字,因为它必定是某物的另一个名字。
——Item 21: 当你必须返回一个对象时不要试图返回一个引用 | Effective C++
绝不要返回指向栈中的局部对象的指针或引用,或返回指向堆中对象的引用 [3],或返回指向静态局部变量对象的指针或引用
—— 笔记 - 关于《Effective C++》 中的 55 个做法 | GuKaifeng’s Blog
使用引用返回的对象,其生存期必须大于函数的作用域,否则会造成垂悬引用。永远不要使用引用返回非静态的本地变量或者临时变量。
——12.12 — Return by reference and return by address – Learn C++
-
若函数返回引用,则函数的调用可以作为左值,进行赋值操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
int& test() {
static int a = 10;//静态变量
return a;
}
int main(void) {
int& q = test();
printf("%d\n", q);
test() = 20;
printf("%d\n", q);
}1
2
310
20函数
test
实际上是返回了a
本身,我们是对变量原名进行操作。Tip若函数返回了一个非静态的引用,调用者可以通过引用修改返回值。
——12.12 — Return by reference and return by address – Learn C++
26.5 引用的本质
Here i
is aliase for main’s x
. In other words, i
is x
— not a pointer to x
, nor a copy of x
, but x
itself. Anything you do to i
gets done to x
, and vice versa (反之亦然). This includes taking the address of it. The values of &i
and &x
are identical (相同的).That’s how you should think of references as a programmer.
Important note: Even though a reference is often implemented using an address in the underlying assembly language, please do not think of a reference as a funny looking pointer to an object.
引用就是对象自己,不是指针,也不是对象的副本。
这一段描述是用来稳定认知的,因为从不同角度来看引用和指针的关系,得到的结论是矛盾的。
catphive: References are not a kind of pointer. They are a new name for an existing object.
Christoph: true if you go by language semantics, not true if you actually look at the implementation; C++ is a far more ‘magical’ language that C, and if you remove the magic from references, you end up with a pointer
引用在 C++ 内部的实现就是一个指针常量。下面结合实例讲解:
1 |
|
1 | a: 20 |
引用可以被视作一个可以自动间接访问的指针常量(不是常量指针!)。编译器会自动使用 *
间接访问。
——Pointers vs References in C++ - GeeksforGeeks
Underneath it all, a reference i
to object x
is typically the machine address of the object x
. But when the programmer says i++
, the compiler generates code that increments x
. In particular, the address bits (地址信息) that the compiler uses to find x
are not changed.
A C programmer will think of this as if you used the C style pass-by-pointer, with the syntactic (语法的) variant (变体) of (1) moving the &
from the caller into the callee, and (2) eliminating (消灭) the *
s.
In other words, a C programmer will think of i
as a macro (宏) for (*p)
, where p
is a pointer to x
(e.g., the compiler automatically dereferences the underlying pointer; i++
is changed to (*p)++
; i = 7
is automatically changed to *p = 7
).
从底层实现来看,引用其实是对象的机器地址。但当程序员调用引用时,编译器生成的是对对象本身的操作,而不是对对象地址的。
C 程序员多半会认为 C++ 的按引用传递就是 C 的按地址 (指针) 传递,只是前者将 &
从调用者移动到被调用者,同时消灭了 *
而已。换句话说,引用就是间接访问的宏。
既然引用和指针在底层上是相同的,那我们是不是可以用引用来替代指针呢?答案是:很多情况下,可行!
以下的论述展示了引用与指针的差别。
引用 | 指针 | |
---|---|---|
重指定 | 引用不能重新指定变量 | 指针可以重新指定变量 |
内存地址 | 与原始变量共享同一地址 | 有自己的不同的地址 |
工作方式 | 直接指向另一个变量 | 存储变量地址 |
Null 值 | 没有 null 值 | 可以赋值为 null 值 |
函数参数 | 按值传递方式 | 按引用传递方式 |
(来源:Pointers vs References in C++ - GeeksforGeeks)
引用 | 指针 |
---|---|
声明时必须初始化 | 声明时可以不初始化 |
无法重新指定为另一个对象 | 可以重新指定为不同对象 |
不可以是 null | 可以是 null |
自动解引用 | 必须显性手动解引用 |
(来源:Reference vs Pointer in C++: 6 Key Differences to Know)
或者看看这个 Stack Overflow 问题:c++ - What are the differences between a pointer variable and a reference variable? - Stack Overflow 以及它的相关问题。
ChatGPT 的回答
1 | >>在C++中,什么时候使用引用更好,什么时候使用指针更好? |
(以上内容由 OpenAI GPT-4o mini 生成,服务来自 DuckDuckGo AI Chat)
1 | >>在C++中,什么时候使用引用更好,什么时候使用指针更好? |
(来自 Haiku Claude 3,服务由 DuckDuckGo AI Chat 提供)
总的来讲,C++ 的引用比指针少了灵活性,多了安全性。写起来,引用也比指针更简洁。
ISO CPP 也倾向于使用引用:
Use references when you can, and pointers when you have to.
Note: Old line C programmers sometimes don’t like references since they provide reference semantics (语义) that isn’t explicit in the caller’s code. After some C++ experience, however, one quickly realizes this is a form of information hiding, which is an asset (资产) rather than a liability (负债).
能用引用就用引用,得用指针就用指针。
不喜欢写引用的程序员,你们好呀。ldx 什么时候爆金币?
C++ 要比 C 更为 “安全”,因为 C 的指针可以不加检查地随意访问内存,而 C++ 引入的引用则限制了指针的随意访问行为。但 C++ 仍然不是一个严格内存安全 (Memory Safe) 的语言,例如我们依旧可以通过引用来访问非法的内存区域,以及数组仍然可以越界访问。
这并不代表 C++ 不重视内存安全,相反,经过多年演化,严格遵守标准和最佳实践的代码已经是足够安全的了,而近年来,社区内也存在要求引入严格安全检查机制的声音。
但 C++ 的设计理念决定了它难以成为严格安全的语言,因为它 “不试图强迫人做什么”(《C++ 语言的设计和演化》)。
…… 程序员总能找到某种方法,绕过他们觉得无法接受的规则和限制。语言应该支持范围较广泛的合法的设计和编程风格,而不应该强迫程序员采纳唯一的写法。……
……“可能的错误” 在 C++ 里并不是一个错误。例如,写一个能允许歧义使用的生命本身并不是错误,错误的是那些存在歧义性的使用,而不是这个错误的可能性。……
——《C++ 语言的设计和演化》
从更基础的计算机科学角度来看,“引用” 这一概念还有更深层次的意义。这一部分仅作扩展。
In computer programming, a reference is a value that enables a program to indirectly access a particular datum(数据), such as a variable’s value or a record, in the computer’s memory or in some other storage device. The reference is said to refer to the datum, and accessing the datum is called dereferencing the reference. A reference is distinct from the datum itself.
A reference is an abstract data type and may be implemented in many ways. Typically, a reference refers to data stored in memory on a given system, and its internal value is the memory address of the data, i.e. a reference is implemented as a pointer. For this reason a reference is often said to “point to” the data. Other implementations include an offset (difference) between the datum’s address and some fixed “base” address, an index, or identifier used in a lookup operation into an array or table, an operating system handle, a physical address on a storage device, or a network address such as a URL.
26.6 常量引用
常量引用主要用于修饰形参,防止误操作。
1 |
|
如果我们用
const
修饰一个引用变量,那么这个引用将能够绑定到任何类型的参数。——12.6 — Pass by const lvalue reference – Learn C++
大部分情况下,我们都不希望函数修改参数,因此优先按
const
引用传参而不是按 non-const 引用传参。
例如接下来的程序:
1 |
|
函数 showValue
的用途本来只是打印数据,但是有人粗心地给本地变量 val
指定了初始值。
如果我们不使用 const
修饰形参呢?
1 | val = 1000 |
好嘛,原来的数据也被污染成函数中指定的初始值了。这可不行,我们不能让函数修改我们的数据。
为函数形参加上 const
试试:
1 | void showValue(const int& val){ |
1 | val = 100 |
很好!函数正确执行了我们的预期操作。
ISO CPP 给出了三种参数传递方式的选择建议:
如果你需要改变传进来的对象,那么按引用传递和按指针传递都是可以的。如果允许传入 “非对象”(如一个空指针),那么按指针传递更容易理解。
如果你不希望改变传进来的对象,并且这个对象很大,按常量引用传递。
其他情况,建议按值传递。
【辟谣】C++ 根本没有堆和栈!- 哔哩哔哩 - bilibili,注意煽动性标题和评论区中作者与观众间的割裂互动。一个比较好的观看建议是:“只能说初学者建议不管,研究底层的请记住这是操作系统的东西,而程序并不和操作系统绑定。” ↩︎
存储期 - 谷雨同学的 C++ 教程,如你需要了解存储期概念,请参考这篇文章。 ↩︎
虽然堆区不存在局部变量的被动销毁问题,但如果被返回的函数的引用只是作为一个临时变量出现,而没有将其赋值给一个实际的变量,那么就可能造成这个引用所指向的空间(由
new
分配)无法释放的情况(由于没有具体的变量名,故无法用delete
手动释放该内存),从而造成内存泄漏。因此应当避免这种情况的发生(02 - 这一次得弄懂 C++ 中的引用 - 知乎) ↩︎