(This picture comes from: VTuberized Logos)

参考教程:01 程序的内存模型 - 内存四区 - 代码区 - 哔哩哔哩 bilibili(黑马程序员)

Tip

  • [3] 想写出好程序,不必对 C++ 掌握到巨细靡遗。
  • [4] 把力气用在编程技术上,别死磕语言特性。

——1 基础知识・C++ 导览 第二版 简体中文版

Lesson25 内存分区

25.1 C++ 内存分区简介

C 语言的内存分配参见 Lesson20 4.2 和 10

C++ 的内存分区与 C 语言的大体相同,只是一些分区有所调整:

  1. 代码区 (Code Segment)
    存放函数体的二进制代码。内存由操作系统进行管理。
  2. 全局 / 静态区 (Global/Static Storage)
    存放全局变量、静态变量以及常量,C 语言中常量区、data 段和 bss 段的综合
  3. 栈区 (Stack)
    存放函数的参数值、局部变量等。内存由编译器自动分配与释放。
  4. 堆区 (Heap)
    内存由程序员分配与释放。若程序员没有释放,程序结束时由操作系统自动回收。

不同区域存放的数据具有不同的生命周期和处理方式,有助于提高编程的灵活性。同时减少读取时的操作开销,提高效率。

为什么需要内存分区

程序需要以不同方式对待它的不同内存块。例如,有些进程应当执行它的代码,而不是执行它的数据;有些进程应当写入数据,而不是修改它的代码;有些进程需要和其他进程共享一部分内存,但不是全部内存;有些内存是只读的,但有些内存可读可写。

——Operating Systems Lecture Notes Lecture 15 Segments

语言标准还是具体实现

有的人 [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 的元素可以分配在上,在对象里或者在静态存储区。

——2 用户定义类型・C++ 导览 第二版 简体中文版 & 13 实用功能・C++ 导览 第二版 简体中文版

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

  1. In general though the answer to these questions are not rellavent to a general C++ programmer (with exceptions like compiler/device driver writers).

  2. 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

      程序需要有限度地共享数据。比如:所有进程应使用同一个编译器;两个进程间可能想要共享一些数据。

      ——Operating Systems Lecture Notes Lecture 15 Segments

    • 代码区的数据是只读的,以防止程序意外修改了自己的指令。

  • 全局区

演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <cstdio>

int a_g = 10;
const int b_g = 10;
static int c_g = 10;

int main(void) {
int a_l = 10;
const int b_l = 10;
static int c_l = 10;
printf("全局变量a_g的地址是:%p\n", & a_g);
printf("全局常量b_g的地址是:%p\n", & b_g);
printf("全局静态变量c_g的地址是:%p\n", & c_g);
printf("字符串常量'Hello'的地址是:%p\n", & "Hello");
printf("局部变量a_l的地址是:%p\n", & a_l);
printf("局部常量b_l的地址是:%p\n", & b_l);
printf("局部静态变量c_l的地址是:%p\n", & c_l);
}
1
2
3
4
5
6
7
全局变量a_g的地址是:00007FF6C32BD000
全局常量b_g的地址是:00007FF6C32BABB0
全局静态变量c_g的地址是:00007FF6C32BD004
字符串常量'Hello'的地址是:00007FF6C32BAD4C
局部变量a_l的地址是:000000A11A51F644
局部常量b_l的地址是:000000A11A51F664
局部静态变量c_l的地址是:00007FF6C32BD008

由该演示可知,全局变 / 常量、静态变量和字符串常量存储在同一个内存段,而局部变 / 常量存储在另一个内存段。


25.3 程序运行时的内存分区

执行时,程序还有以下两个内存分区:

  • 栈区

    • 存放函数的参数值、局部变量等。
    • 内存由编译器自动分配释放。

    利用指针访问存储在栈区的变量时,需要注意变量的作用域。比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <cstdio>

    int* func(void) {
    int a = 10;
    return &a;
    }

    int main(void) {
    int* p = func();
    printf("%d\n", *p);
    printf("%d\n", *p);
    }
    1
    2
    3
    10
    -858993460

    func 所占用的内存在函数执行完毕后即被释放,也就是说,赋值时 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
    #include <cstdio>

    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
    4
    00000070928FF604
    10 0000026005F44A00 00000070928FF5E8
    10 0000026005F44A00 00000070928FF5E8
    10

    我们使用 new 申请了一块大小为 int 宽度的内存,并将其初始化为 10。如果这部分内存放在栈上,那我们第二次通过指针访问这个值时应该不会输出 10,但是它输出了 10,说明操作系统是不会动这块内存的。

    另外,指针接收到的地址是存储在栈区的。


25.3.SP 对象

什么是对象 (Object)

以下是一些关于对象的表述:

Quote

对象 是一个在内存中占据了一定空间的有类型的东西。因而,它必然是与计算机内存这个物理上具体存在的设备关联在一起的一个物质。

——3. 值与对象 — Understanding Modern C++ 1 文档

对象代表了一段可以存储值的内存区域。变量的本质就是有名字(识别符)的对象。

宽泛地讲,对象是任何一个未命名的实例,比如变量和函数。但在 C++ 中,函数不属于对象。

——1.3 — Introduction to objects and variables – Learn C++

总的来讲,对象就是一块可以存储值的内存区域。

对于一个对象,我们主要看它的四个要素:

  • 类型

    类型决定了对象在内存中存储的字节大小与二进制的判读方式

  • 标识符

    标识符是数字、下划线、大小写拉丁字母(和以 \u\U 转义字符)指定的。

    程序员通过标识符访问标识符代表的内存对象。

    在 C++ 中,直接访问内存是很难受的,所以我们需要通过对象来间接访问内存。我们只需要专注于怎么存储并检索对象,而不需要关心这些对象放在内存的哪个具体位置,因为编译器代劳了。

    虽然 C++ 中的对象可以没有名字,但我们大多仍会使用标识符来命名对象。有名字的对象叫作变量。为对象命名可以让我们再次使用它们。

    ——1.3 — Introduction to objects and variables – Learn 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 ,从性质上没有任何区别,都是 类别的表达式。

    ——3. 值与对象 — Understanding Modern C++ 1 文档

CPP Reference 对对象的描述

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.

这些不是对象:值、引用、函数,等等。

——Object - cppreference.com


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];

参考:new 运算符 (C++) | Microsoft Learn

C++ 内存管理库 | 菜鸟教程


动态分配的内存会一直保持分配状态,直到它被显式释放或直到程序结束。

delete 操作符用于释放之前使用 new 分配的内存。

deletenew 一样在 <new> 头文件中定义,也包含在标准库中,不需要再引用 <new>

接下来介绍 delete 的用法:

  • 释放单个对象的内存:

    1
    delete pointer;
  • 释放数组的内存:

    1
    delete pointer[];

动态内存释放完后,就不能再访问,否则程序无法正常退出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <cstdio>

int* func() {
int* p = new int(10);
return p;
}


int main(void) {
int* p = func();
printf("%d\n", *p);
delete p;
printf("%d\n", *p);
}
1
警告	C6001	使用未初始化的内存“p”。	行13
1
2
3
10

(进程 28332)已退出,代码为 -1073741819 (0xc0000005)。
Tip

你可能更熟悉 C 语言中的 malloc()free()。但在 C++ 中,我们最好入乡随俗,用 newdelete 作为替代。

[5] 不要使用 malloc()new 操作符可谓青出于蓝而胜于蓝。别仅仅用 “裸” 的 newdelete 替换 malloc()free()

在 C 里,void*(在 malloc() 的声明中出现) 可在赋值操作中作为右值操作数, 或者用在任何指针类型变量的初始化中; 这种做法在 C++ 里行不通。在两种语言里,都要把 malloc() 的结果转化到正确的类型。 如果你只用 C++ ,请避免使用 malloc()

——16 历史及兼容性・C++ 导览 第二版 简体中文版


Lesson26 引用

26.1 引用的定义

引用变量 (Reference) 是一个别名 (Alias, or alternate name),它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。

你可以把别名看作一个人的绰号。只要人们接受了这个绰号,那么你就可以用这个绰号指代对应的那个人了。

引用变量的声明:

1
2
3
type& ref = var_name;
//或
type &ref = var_name;
Tip

在声明引用时,现代 C++ 程序员更倾向于将 & 放在类型旁边(而不是引用变量名称旁边),因为这样程序员可以更清楚地认识到自己声明的是一个引用定义,而不是一个含有 "&" 字符的某类型变量。

——12.3 — Lvalue references – Learn C++

Note

Reference declaration - cppreference.com 中提到,该声明对应的是左值引用 (Lvalue Reference),而 C++11 标准还引入了右值引用 (Rvalue Reference),用的是 &&

Warning

這裡 type&& 不要用「取址」的概念去解釋,雖然看起來有點關係,但觀念真的很容易亂掉。宣告時的 &,在觀念不熟的情況下,請先當作另外一回事。

——【C++ 觀念理解 #1】C++ call by value, call by address (pointer), call by reference 總和比較整理,不想要再搞不懂了!!!(updated: 2022/8/19) - 嗡嗡的隨手筆記

我们可以通过引用来访问并修改变量的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <cstdio>

int main (void)
{
// 声明简单的变量
int i;
double d;

// 声明引用变量
int& r = i;
double& s = d;

i = 5;
printf("Value of i : %d", i);
printf("Value of i reference : %d", r);
printf("Value of i reference plus 1 : %d", r + 1);

d = 11.7;
printf("Value of d : %f", d);
printf("Value of d reference : %f", s);
printf("Value of d reference plus 1 : %d", s + 1);

return 0;
}
1
2
3
4
5
6
Value of i : 5
Value of i reference : 5
Value of i reference plus 1 : 6
Value of d : 11.700000
Value of d reference : 11.700000
Value of d reference plus 1 : 12.700000

26.2 引用声明的注意事项

  • 引用必须在创建时被初始化。

    Note

    引用必须被初始化为指向一个合法的对象或函数。

    ——Reference declaration - cppreference.com

    因为引用具有 对象别名 的语义,因而没有 绑定 到任何对象的引用,从语义上就不成立。

    由于必须通过初始化将引用绑定到某一个对象,因而从语义上,不存在 空引用 的概念。

    ——1. 引用 — Understanding Modern C++ 1 文档

  • 一旦创建了一个引用,就不能再让它引用另一个对象。

    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 (指示对象).

    ——References, C++ FAQ

    引用必须依附于它初始指向的对象。引用不是对象。对引用取地址,得到的仍是其指向的对象的地址。

    记住:引用就是其指向的对象。

    注意不要将赋值误认为是更改引用,比如:

    1
    2
    3
    4
    int a = 10;
    int& b = a;
    int c = 20;
    b = c;//这是赋值

    a = cb = c 是等效的。好比给舍友带饭,不论叫绰号还是叫全名都可以把饭交给对方。

    Remember: the reference is the referent, so changing the reference changes the state of the referent.

    ——References, C++ FAQ

    改变引用的状态,也会改变被引用对象的状态。


    1
    &b = c;//这是更改引用

    VS2022 的 IntelliSense 会告诉你:

    1
    错误(活动)	E0137	表达式必须是可修改的左值

    这段代码也不能通过编译。

  • 在大多数情况下,引用只会绑定到与引用类型匹配的对象。如果你将引用绑定到与其引用类型不匹配的对象,编译器将尝试隐式地将对象转换为引用类型,然后将引用绑定到该对象。

  • 引用必须指向一个合法的内存(在栈区和堆区的内存)。指向一个不再存在的对象的引用被称为悬空引用 (Dangling Reference)。访问悬空引用会导致未定义的行为。

  • 引用本身不是对象,因此引用本身一般不占存储单元。例如,对引用求地址,就是对目标变量求地址。

    Warning

    引用不是对象。引用不占用内存,除非编译器认为有必要,例如非静态的引用类型数据成员会增加类的大小,因为类要存储地址。

    由于引用不是对象,所以没有引用数组、引用指针和引用的引用

    1
    2
    3
    int& a[3]; // error
    int&* p; // error
    int& &r; // error

    ——Reference declaration - cppreference.com


    不能建立引用数组(数组中的元素不能是引用),但是可以建立数组的引用和数组元素的引用:

    1
    2
    3
    4
    5
    int& 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];//可行

    —— 不能建立引用数组,数组中的元素不能是引用_c++ 不能建立引用的数组 - CSDN 博客


26.3 引用作为函数参数

在 C 语言中,我们介绍了按值传递按地址传递两种传参方式。下面我们介绍 C++ 提供的第三种传参方式:按引用传递 (Pass by Reference, or Call by Reference)

「call by reference」是 C++ 才有的「更方便」的東西,但也因為這個「方便」,導致觀念如果不穩,就會像我一樣把這個「方便」的功能,變成「符號意義大混亂」。因此,如果這三個你現在很混亂,建議先從「call by value, call by address (pointer)」搞清楚再說!!!

——【C++ 觀念理解 #1】C++ call by value, call by address (pointer), call by reference 總和比較整理,不想要再搞不懂了!!!(updated: 2022/8/19) - 嗡嗡的隨手筆記

在按引用传递方法中,实际参数的内存地址(引用)被传递给函数,允许直接访问和修改原始值。实际参数和形式参数指向相同的内存地址。在函数中对参数所做的任何更改都会直接反映在函数外的原始值中。这和按地址传递的作用相似,但相比指针作为参数,引用作为参数要更加 “自然”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <cstdio>

void swap(int& a, int& b);

int main(void){
int a, b;
scanf("%d %d", &a, &b);
swap(a, b);// 引用作为函数参数
printf("%d %d", a, b);
}

void swap(int& a, int& b){
int t = a;
a = b;
b = t;
}

input:

1
2 3

output:

1
3 2

26.4 引用作为函数返回值

C++ 的函数可以返回一个引用,方式与返回一个指针 (Lesson20 4.3) 类似。

1
2
3
4
5
6
7
8
9
10
11
12
#include <cstdio>

int& test() {
static int a = 10;//静态变量
return a;
}

int main(void) {
int& q = test();
printf("%d\n", q);
printf("%d\n", q);
}
1
2
3
10
10

这里有几个注意事项:

  • 不要返回局部变量的引用,或者说,不要返回一个内存已被释放的变量的引用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <cstdio>

    int& test() {
    int a = 10;//局部变量
    return a;
    }

    int main(void) {
    int& q = test();
    printf("%d\n", q);//第一次读取,编译器进行保留
    printf("%d\n", q);//第二次读取,保留失效,程序试图读取引用指向的内存区域
    }
    1
    2
    3
    10
    -858993460

    这和我们 [24.3](#24.3 程序运行时的内存分区) 讲的错误原因是一样的,引用指向了一个本不存在的、混沌的内存位置。

    本节开头的示例使用 static 修饰局部变量,扩大了变量的作用域,使得变量的内存不会在函数执行完成后被释放。

    Warning

    虽然一个返回一个局部 static 的设计的例子是合理的,至少在单线程的环境中是这样。但就像所有使用了 static 对象的设计一样,这个也会立即引起我们的线程安全(Thread-safety)的混乱。

    ——Item 21: 当你必须返回一个对象时不要试图返回一个引用 | Effective C++

    请记住一个引用仅仅是一个名字,一个实际存在的对象的名字。无论何时只要你看到一个引用的声明,你应该立刻问自己它是什么东西的另一个名字,因为它必定是某物的另一个名字。

    ——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
    #include <cstdio>

    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
    3
    10
    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 xnot 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.

——References, C++ FAQ

引用就是对象自己,不是指针,也不是对象的副本。

这一段描述是用来稳定认知的,因为从不同角度来看引用和指针的关系,得到的结论是矛盾的。

借用 Stack Overflow 一个问题中的一对问答

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <cstdio>

//发现是引用,转换为int* const ref = &var
void func(int& ref) {
ref = 100;//ref是引用,转换为*ref = 100
}

int main() {
int a = 10;

//自动转换为int* const ref = &a; 指针常量的指向不可更改,所以引用也不可更改
int& ref = a;
ref = 20;//发现是引用,自动转换为*ref = 20

printf("a: %d\n", a);
printf("ref: %d\n", ref);

func(a);

printf("a: %d\n", a);
printf("ref: %d\n", ref);
}
1
2
3
4
5
a: 20
ref: 20
a: 100
ref: 100

Note

引用可以被视作一个可以自动间接访问的指针常量(不是常量指针!)。编译器会自动使用 * 间接访问。

——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).

——References, C++ FAQ

从底层实现来看,引用其实是对象的机器地址。但当程序员调用引用时,编译器生成的是对对象本身的操作,而不是对对象地址的。

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
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
29
30
31
32
33
34
35
36
37
38
39
40
>>在C++中,什么时候使用引用更好,什么时候使用指针更好?

在C++中,引用和指针都是用于间接访问对象的方式,但它们在使用场景和语义上有所不同。以下是一些关于何时使用引用和指针的指导:

## 使用引用的情况:
1. 简洁性:引用语法更简洁,使用起来更像普通变量。例如,`int& ref = var;`,而指针需要使用解引用操作符(`*`)和地址操作符(`&`)。
2. 不允许为空:引用必须绑定到一个有效的对象,不能为`nullptr`,这使得引用在某些情况下更安全。
3. 函数参数:当你希望在函数中修改传入的参数时,使用引用可以避免复制开销。
4. 重载运算符:在重载运算符时,通常使用引用来避免不必要的复制。
5. 返回值:当你希望返回一个对象的引用时,使用引用可以避免复制。

## 总结
- 使用引用:当你希望提供一个对象的别名,并且不需要处理空值时,引用是更好的选择。
- 使用指针:当你需要处理动态内存、可能为空的情况,或者需要改变指针的指向时,指针是更合适的选择。
在实际编程中,选择引用还是指针通常取决于具体的需求和上下文。

## 使用指针的情况:
1. 可为空:指针可以指向`nullptr`,这在某些情况下是有用的,例如表示“无对象”或“未初始化”的状态。
2. 动态内存管理:当你需要动态分配内存时,指针是必需的。例如,使用`new``delete`来管理动态分配的对象。
3. 数组和指针的关系:在处理数组时,指针可以更灵活地进行算术运算。例如,指针可以通过加法和减法来遍历数组。
4. 多态性:在使用基类指针指向派生类对象时,指针是必要的,尤其是在实现多态时。
5. 需要改变指向的对象:如果你需要在函数中改变指针的指向,使用指针更合适。

>>那么大部分情况下,引用可以取代指针吗?

在许多情况下,引用确实可以取代指针,尤其是在需要传递对象的别名或修改对象时。引用提供了更简洁的语法和更安全的使用方式,因为它们不能为`nullptr`,并且在使用时不需要解引用操作符。以下是一些具体的情况:

1. 函数参数:当你希望在函数中修改传入的参数时,引用可以替代指针。使用引用比使用指针更简洁。
2. 返回值:当你希望返回一个对象的引用时,引用可以替代指针。返回引用比返回指针更安全,因为引用不能为`nullptr`
3. 重载运算符:在重载运算符时,通常使用引用来避免不必要的复制。

>## 但引用不能完全取代指针的情况:
1. 动态内存管理:当需要动态分配内存时,指针是必需的。引用不能用于new和delete。
2. 可为空的情况:如果需要表示“无对象”或“未初始化”的状态,指针是唯一的选择,因为引用必须绑定到一个有效的对象。
3. 改变指向的对象:如果需要在函数中改变指针的指向,指针是必要的。
4. 多态性:在使用基类指针指向派生类对象时,指针是必要的,尤其是在实现多态时。

## 总结
虽然在许多情况下引用可以取代指针,但在某些特定场景下,指针仍然是不可或缺的。因此,选择使用引用还是指针应根据具体的需求和上下文来决定。在设计时,**优先考虑使用引用,只有在需要指针特有的功能时才使用指针**

(以上内容由 OpenAI GPT-4o mini 生成,服务来自 DuckDuckGo AI Chat)

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
29
30
31
32
33
>>在C++中,什么时候使用引用更好,什么时候使用指针更好?

在C++中,使用引用还是指针,主要取决于具体的使用场景和需求。以下是一些建议:

1. 引用:
- 当你需要修改函数参数的值时,使用引用通常更好。这样可以避免复制对象,提高效率。
- 当你需要返回多个值时,使用引用可以更方便地实现。
- 当你需要创建一个变量,它只是另一个变量的别名时,使用引用更合适。

2. 指针:
- 当你需要指向动态分配的内存时,使用指针是必要的。
- 当你需要实现链表、树等动态数据结构时,指针是不可或缺的。
- 当你需要实现间接寻址或者操作内存地址时,指针更加合适。

>总的来说,引用提供了更简洁和安全的语法,但在某些情况下,指针仍然是必要的。在编程时,需要根据具体需求权衡使用引用还是指针。良好的编程习惯和代码风格也很重要,可以提高代码的可读性和可维护性。

>>那么大部分情况下,引用可以取代指针吗?

不,大部分情况下引用并不能完全取代指针。虽然引用和指针在某些方面可以互换使用,但它们还是有一些重要的区别:

1. 内存管理:
- 指针可以指向动态分配的内存,引用则不行。动态内存分配需要使用指针。

2. 空值表示:
- 指针可以指向空值(NULL),而引用不能。这使得指针更加灵活,可以表示不存在的对象。

3. 间接访问:
- 指针可以通过解引用操作符(*)间接访问内存地址,引用则没有这种能力。这使得指针更适合于实现复杂的数据结构。

4. 赋值和初始化:
- 引用必须在声明时初始化,而指针可以在任何时候赋值。这使得引用的使用更加受限。

因此,虽然在某些情况下引用可以取代指针,但指针仍然是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 (负债).

——References, C++ FAQ

用引用就用引用,用指针就用指针。

不喜欢写引用的程序员,你们好呀。ldx 什么时候爆金币?

内存安全

C++ 要比 C 更为 “安全”,因为 C 的指针可以不加检查地随意访问内存,而 C++ 引入的引用则限制了指针的随意访问行为。但 C++ 仍然不是一个严格内存安全 (Memory Safe) 的语言,例如我们依旧可以通过引用来访问非法的内存区域,以及数组仍然可以越界访问。

这并不代表 C++ 不重视内存安全,相反,经过多年演化,严格遵守标准和最佳实践的代码已经是足够安全的了,而近年来,社区内也存在要求引入严格安全检查机制的声音。

但 C++ 的设计理念决定了它难以成为严格安全的语言,因为它 “不试图强迫人做什么”(《C++ 语言的设计和演化》)。

…… 程序员总能找到某种方法,绕过他们觉得无法接受的规则和限制。语言应该支持范围较广泛的合法的设计和编程风格,而不应该强迫程序员采纳唯一的写法。……

……“可能的错误” 在 C++ 里并不是一个错误。例如,写一个能允许歧义使用的生命本身并不是错误,错误的是那些存在歧义性的使用,而不是这个错误的可能性。……

——《C++ 语言的设计和演化》


从更基础的计算机科学角度来看,“引用” 这一概念还有更深层次的意义。这一部分仅作扩展。

Quote

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.

——Reference (computer science) - Wikipedia


26.6 常量引用

常量引用主要用于修饰形参,防止误操作。

1
2
3
4
5
6
7
#include <cstdio>

int main(void){
//int& ref = 10; // 引用指向常数区,不是一块合法的内存
const int& ref = 10; // 加上const,编译器将其转换为int temp = 10; const int& ref = temp;
//ref = 20; // 加入const之后变为只读,不可修改
}

如果我们用 const 修饰一个引用变量,那么这个引用将能够绑定到任何类型的参数

——12.6 — Pass by const lvalue reference – Learn C++

大部分情况下,我们都不希望函数修改参数,因此优先按 const 引用传参而不是按 non-const 引用传参。

——12.6 — Pass by const lvalue reference – Learn C++

例如接下来的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <cstdio>

//print data
void showValue(int& val){

val = 1000;//assign a value by mistake
printf("val = %d\n", val);
}
int main(void){
int a;

/*Some code...*/

a = 100;
showValue(a);
printf("a = %d\n", a);
}

函数 showValue 的用途本来只是打印数据,但是有人粗心地给本地变量 val 指定了初始值。

如果我们不使用 const 修饰形参呢?

1
2
3
val = 1000
a = 1000

好嘛,原来的数据也被污染成函数中指定的初始值了。这可不行,我们不能让函数修改我们的数据。

为函数形参加上 const 试试:

1
2
3
4
5
void showValue(const int& val){

//val = 1000;cannot assign a value
printf("val = %d\n", val);
}
1
2
3
val = 100
a = 100

很好!函数正确执行了我们的预期操作。

ISO CPP 给出了三种参数传递方式的选择建议:

如果你需要改变传进来的对象,那么按引用传递和按指针传递都是可以的。如果允许传入 “非对象”(如一个空指针),那么按指针传递更容易理解。

如果你不希望改变传进来的对象,并且这个对象很大,按常量引用传递。

其他情况,建议按值传递。

——References, C++ FAQ


  1. 【辟谣】C++ 根本没有堆和栈!- 哔哩哔哩 - bilibili,注意煽动性标题和评论区中作者与观众间的割裂互动。一个比较好的观看建议是:“只能说初学者建议不管,研究底层的请记住这是操作系统的东西,而程序并不和操作系统绑定。” ↩︎

  2. 存储期 - 谷雨同学的 C++ 教程,如你需要了解存储期概念,请参考这篇文章。 ↩︎

  3. 虽然堆区不存在局部变量的被动销毁问题,但如果被返回的函数的引用只是作为一个临时变量出现,而没有将其赋值给一个实际的变量,那么就可能造成这个引用所指向的空间(由 new 分配)无法释放的情况(由于没有具体的变量名,故无法用 delete 手动释放该内存),从而造成内存泄漏。因此应当避免这种情况的发生(02 - 这一次得弄懂 C++ 中的引用 - 知乎↩︎


©2025-Present Watermelonabc | 萌ICP备20251229号

Powered by Hexo & Stellar latest & Vercel & 𝙌𝙞𝙪𝙙𝙪𝙣 𝘾𝘿𝙉 & HUAWEI Cloud
您的访问数据将由 Vercel 和自托管的 Umami 进行隐私优先分析,以优化未来的访问体验

本博客总访问量:capoo-2

| 开往-友链接力 | 异次元之旅 | 中文独立博客列表

猫猫🐱 发表了 41 篇文章 · 总计 209.8k 字