C++的一些概念复杂而且存在一些容易使用错误的地方,这里进行部分总结.
基础
const关键字
const 是一个关键字,用于表示常量。
const 可以用于修饰变量、函数、指针等:
1.修饰常量
当 const 修饰变量时,该变量将被视为只读变量,即不能被修改。
对于确定不会被修改的变量,应该加上 const,这样可以保证变量的值不会被无意中修改,也可以使编译器在代码优化时更加智能。
但理论上也可以赋给const int*再进行类型转换修改,1
2
3
4
5
6
7
8
9
10
11
using namespace std;
int main() {
const int num = 10;
const int* cp = #
int* p = const_cast<int*>(cp);
*p = 20;
cout<<num;
return 0;
}
但编译器可能会进行优化,替换为实际值。因此应该遵循 C/C++ 语言中 const 的语义,尽量不修改只读变量的值。
2.const修饰函数参数
当 const 修饰函数参数时,表示函数内部不会修改该参数的值。这样做可以使代码更加安全,避免在函数内部无意中修改传入的参数值。
3.修饰函数返回值
当 const 修饰函数返回值时,表示函数的返回值为只读,不能被修改。这样做可以使函数返回的值更加安全,避免被误修改。
4.修饰指针或引用
const 关键字可以用来修饰指针,用于声明指针本身为只读变量或者指向只读变量的指针。
4.1指向只读变量的指针
4.2只读指针
4.3只读指针指向只读变量
const 关键字同时修饰了指针本身和指针所指向的变量,使得指针本身和所指向的变量都成为只读变量。因此,指针本身不能被修改,也不能通过指针修改所指向的变量。
4.4常量引用
常量引用是指引用一个只读变量的引用,因此不能通过常量引用修改变量的值。
5.修饰成员函数
当 const 修饰成员函数时,表示该函数不会修改对象的状态(就是不会修改成员变量)。const 的对象就可以调用这些成员方法了,因为 const 对象不允许调用非 const 的成员方法。
const 的成员函数不能调用非 const 的成员函数,原因在于 const 的成员函数保证了不修改对象状态,但是如果调用了非 const 成员函数,那么这个保证可能会被破坏。
static关键字
可以用于变量和函数
1.static修饰全局变量
static 修饰全局变量可以将变量的作用域限定在当前文件中,使得其他文件无法访问该变量。
同时,static 修饰的全局变量在程序启动时被初始化(可以简单理解为在执行 main 函数之前,会执行一个全局的初始化函数,在那里会执行全局变量的初始化),生命周期和程序一样长。
2.static修饰局部变量
static 修饰局部变量可以使得变量在函数调用结束后不会被销毁,而是一直存在于内存中,下次调用该函数时可以继续使用。
由于 static 修饰的局部变量的作用域仅限于函数内部,所以其他函数无法访问该变量
3.static修饰函数
static 修饰函数可以将函数的作用域限定在当前文件中,使得其他文件无法访问该函数。
由于 static 修饰的函数只能在当前文件中被调用,因此可以避免命名冲突和代码重复定义
4.static修饰类成员变量和函数
static 修饰类成员变量和函数可以使得它们在所有类对象中共享,且不需要创建对象就可以直接访问。
volatile关键字
volatile是 C 语言中的一个关键字,用于修饰变量,表示该变量的值可能在任何时候被外部因素更改,例如硬件设备、操作系统或其他线程。
当一个变量被声明为volatile时,编译器会禁止对该变量进行优化,以确保每次访问变量时都会从内存中读取其值,而不是从寄存器或缓存中读取,避免因为编译器优化而导致出现不符合预期的结果。
即便是volatile关键字可以确保编译器不对变量进行优化,但上面任然存在并发问题,counter++操作仍然可能导致数据不一致。
为了解决这个问题,需要使用互斥锁、原子操作或其他同步机制。
字节对齐
字节对齐是内存分配的一种策略。当分配内存时,编译器会自动调整数据结构的内存布局,使得数据成员的起始地址与其自然对齐边界(一般为自己大小的倍数)相匹配
理论上,任何类型的变量都可以从任意地址开始存放。然而实际上,访问特定类型的变量通常需要从特定对齐的内存地址开始。因为如果不对数据存储进行适当的对齐,可能会导致存取效率降低
各种数据类型需要按照一定的规则在内存中排列(起始地址),而不是顺序地一个接一个排放,这种排列就是字节对齐。例如,有些平台每次读取都是从偶数地址开始。如果一个 int 类型(假设为 32 位系统)存储在偶数地址开始的位置,那么一个读周期就可以读取这 32 位。
但如果存储在奇数地址开始的位置,则需要两个读周期,并将两次读取的结果的高低字节拼凑才能得到这 32 位数据,显然这会显著降低读取效率。
为了避免性能损耗,编译器会遵循一个基本原则:变量的内存起始地址,必须是该变量自身大小的整数倍
总结: 字节对齐有助于提高内存访问速度,因为许多处理器都优化了对齐数据的访问。但是,这可能会导致内存中的一些空间浪费。
字节对齐规则
1. 自然对齐边界
对于基本数据类型,其自然对齐边界通常为其大小。
例如,char 类型的自然对齐边界为 1 字节,short 为 2 字节,int 和 float 为 4 字节,double 和 64 位指针为 8 字节。具体数值可能因编译器和平台而异。
2. 结构体对齐
结构体内部的每个成员都根据其自然对齐边界进行对齐,也就是可能在成员之间插入填充字节。
结构体本身的总大小也会根据其最大对齐边界的成员进行对齐(比如结构体成员包含的最长类型为int类型,那么整个结构体要按照4的倍数对齐),以便在数组中正确对齐。
3.联合体对齐
联合体的对齐边界取决于其最大对齐边界的成员。联合体的大小等于其最大大小的成员,因为联合体的所有成员共享相同的内存空间。
4.编译器指令
可以使用编译器指令(如 #pragma pack)更改默认的对齐规则。这个命令是全局生效的。这可以用于减小数据结构的大小,但可能会降低访问性能。
5.对齐属性
在 C++11 及更高版本中,可以使用 alignas 关键字为数据结构或变量指定对齐要求。这个命令是对某个类型或者对象生效的。例如,alignas(16) int x; 将确保 x 的地址是 16 的倍数
6.动态内存分配
大多数内存分配函数(如 malloc 和 new)会自动分配足够对齐的内存,以满足任何数据类型的对齐要求。
字节序
字节序是指在多字节数据类型(如整数、浮点数等)中,字节在内存中的存储顺序。主要有两种字节序:大端字节序(Big-endian)和小端字节序(Little-endian)
大端字节序: 高位字节存储在低地址处,低位字节存储在高地址处。例如,一个4字节的整数0x12345678,在大端字节序的系统中,
小端字节序: 低位字节存储在低地址处,高位字节存储在高地址处。
常见大小端字节序
1. 网络传输
在网络传输过程中,通常使用大端字节序(Big-endian),也称为网络字节序,这是 TCP/IP 协议的规定,多字节数据在网络上传输时使用大端字节序。
因此,如果本地系统使用的是小端字节序,那么就需要在传输之前将其转换为大端字节序。一般通过使用htonl()、htons()、ntohl()和ntohs()等函数来完成
2. Linux
Linux 操作系统在不同的硬件平台上可能使用不同的字节序。例如,x86 和 x86_64(Intel 和 AMD 处理器)是小端字节序(Little-endian),而 PowerPC 和 SPARC 等其他架构可能使用大端字节序(Big-endian)。
所以具体的字节序取决于运行 Linux 的硬件平台。
3. Windows
Windows 操作系统主要运行在 x86 和 x86_64(Intel 和 AMD处理器)架构上,这些处理器使用小端字节序(Little-endian)。
4. Mac
一般使用 Intel 处理器或 Apple 自家的 M1 芯片(基于ARM架构),这些处理器都采用小端字节序(Little-endian)
class类和struct结构体差别
在 C++ 中,class(类)和 struct(结构体)的底层实现和核心能力几乎完全相同。
C++ 的 struct 继承自 C 语言,但进行了全面魔改——在 C++ 里,struct 同样可以拥有构造函数、析构函数、成员函数、虚函数,并且支持继承和多态。
差别:
- 默认的访问权限(最核心区别)
struct的成员默认是公有的(public)。class的成员默认是私有的(private)。
- 默认的继承权限
当一个类继承自另一个类/结构体且没有显式指定继承方式时:
struct默认是公有继承(public)。class默认是私有继承(private)
- 模板参数中的微小差别(
template)
在声明泛型模板时,你可以使用 class 或 typename,但不能使用 struct
适合用 struct 的场景:
当你需要定义一个“纯粹的数据集合”,它只包含一堆变量,没有复杂的业务逻辑和封装保护(类似 C 语言的结构体或 DTO / POJO)时,用 struct。
特征:成员全是
public,通常没有或只有简单的构造函数。
在 C++11 及之后的标准中,POD 类型需要同时满足两个独立条件:
1.平凡(Trivial):类型具有默认的构造/拷贝/移动/析构函数(可自动生成且非虚)
2.标准布局(Standard Layout):内存布局与 C 兼容,成员排列顺序符合特定规则
同时满足平凡性和标准布局的类型称为 POD 类型,这类数据可以安全使用 memcpy 等底层内存操作,因为它们的内存布局与 C 完全兼容且没有特殊处理需求。
适合用 class 的场景:
当你的对象具有复杂的内部状态、需要隐藏实现细节、对外提供特定接口(面向对象的核心思想)时,用 class。
特征:包含
private内部变量,对外暴露public的方法(如 Getter/Setter、业务函数),有严格的封装性。
宏定义和内联函数的区别
宏定义(#define)和内联函数(inline)都是为了减少函数调用开销和提高代码运行效率而引入的机制,但是它们的实现方式和作用机制略有不同。
#define本质是文字替换,在编译之前,预处理器(Preprocessor)会扫描你的代码。看到 MAX(a, b),它就机械地把这一行字擦掉,换成后面的三目运算符表达式。编译器在编译时,根本不知道 MAX 这个名字的存在,它看到的已经是替换后的臃肿表达式了。
写宏函数一定要注意:参数和函数体应当用括号包围,避免因运算优先级导致的错误
inline 声明的函数是一个真正的函数。
- 编译器在编译它时,会进行语法分析、类型检查、模板特化。
- 当遇到调用处时,编译器会根据当前上下文的优化策略,自主决定要不要把它“内联展开”。
- 特别注意:
inline对编译器来说只是一个建议(Suggestion),而不是强制命令。如果你的内联函数内部写了复杂的循环、递归,或者代码量巨大,编译器会理智地拒绝内联,依然把它当做普通函数来调用。
函数内联展开本质减少了函数调用压栈,跳转,出栈等成本.针对本身非常简短的函数内联减少调用成本.
内联展开遵循空间换时间的原则。每一次展开,都会让最终生成的二进制可执行文件(
.exe或.so)体积变大。
- 如果不内联:函数在内存中只有一份拷贝。哪怕被调用 1000 次,也只是跳过去执行那同一段代码,程序体积很小。
- 如果强制内联:被调用 1000 次,这段代码就会在不同的地方被完整复制 1000 遍。
在现代 C++(C++11 及更新标准)中,为了安全和可维护性,我们应当尽量避免使用 #define 来定义常量和函数逻辑。
- 需要定义常量时:用
const或constexpr代替#define。 - 需要定义简短的高频函数时:用
inline(或者类的内联成员函数)代替#define。 - 什么场景下还必须用
#define?:只有在需要用到条件编译(如#ifndef _DEBUG)或者宏拼接符号(如#和##进行动态变量名/日志打印生成)时,才是#define不可替代的舞台。
define和typedef区别
预处理器只进行简单的文本替换,不涉及类型检查。与宏定义不同,typedef 是在编译阶段处理的,有更严格的类型检查。
宏定义没有作用域限制,只要在宏定义之后的地方,就可以使用宏。
通常用于定义常量、简单的表达式或简单的代码片段。
typedef 遵循 C++ 的作用域规则,可以受到命名空间、类等结构的作用域限制。typedef 通常用于定义复杂类型的别名,使代码更易读和易于维护
宏定义不支持模板,因此不能用于定义模板类型别名。
typedef 可以与模板结合使用,但在 C++11 之后,推荐使用 using 关键字定义模板类型别名。1
2
3
4
5
6
7
8
9
10
11// 使用 typedef 定义模板类型别名
template <typename T>
struct MyContainer {
typedef std::vector<T> Type;
};
// 使用 using 定义模板类型别名(C++11 及以后)
template <typename T>
struct MyContainer {
using Type = std::vector<T>;
};
explicit关键字
explicit 通常用于构造函数的声明中,用于防止隐式转换
当将一个参数传递给构造函数时,如果构造函数声明中使用了 explicit 关键字,则只能使用显式转换进行转换,而不能进行隐式转换。这种机制可以防止编译器自动执行预期外的类型转换,提高代码的安全性。
隐式类型转换:当你只有一个类型T1,但是当前表达式需要类型为T2的值,如果这时候T1自动转换为了T2,那么这就是隐式类型转换。
可以使用 explicit 关键字可以防止不必要的隐式转换,提高代码的可读性和安全性。
尤其是构造函数参数只有一种类型的,强烈建议加上 explicit
extern作用
一般而言,C++全局变量的作用范围仅限于当前的文件,但同时C++也支持分离式编译,允许将程序分割为若干个文件被独立编译,于是就需要在文件间共享变量数据,这里extern就发挥了作用。
extern 用于指示变量或函数的定义在另一个源文件中,并在当前源文件中声明。
说明该符号具有外部链接(external linkage)属性。
也就是告诉编译器: 这个符号在别处定义了,你先编译,到时候链接器会去别的地方找这个符号定义的地址。
凡是没有带extern的声明同时也都是定义。
而对函数而言,带有{}是定义,否则是声明。如果想声明一个变量而非定义它,就在变量名前添加关键字extern,且不要显式的初始化变量。
链接属性
在 C++ 中,链接属性是指程序在编译、链接和执行阶段如何处理符号(变量、函数、类等)的可见性和重复定义。
外部链接: 外部链接的符号可以在不同的源文件之间共享,并且在整个程序执行期间可见。全局变量和函数都具有外部链接。
内部链接: 内部链接的符号只能在当前源文件内部使用,不能被其他源文件访问。用 static 修饰的全局变量和函数具有内部链接
无链接的符号只能在当前代码块(函数或代码块)内部使用,不能被其他函数或代码块访问。用 const 或 constexpr 修饰的常量具有无链接属性( 通常情况下编译器是不会为const对象分配内存,也就无法链接)。
外部 C 链接的符号与外部链接类似,可以在不同的源文件之间共享,并且在整个程序执行期间可见。它们具有 C 语言的名称和调用约定,可以与 C 语言编写的代码进行交互。在 C++ 中,可以用 extern "C" 关键字来指定外部 C 链接,从而使用一些 C 的静态库。
这些链接属性可以通过关键字 extern、static、const 和 extern "C" 来显式地指定。在实际的开发中,正确地理解和处理链接属性对于编写可重用、高效、可维护的代码非常重要
编译链接过程中,extern 的作用如下:
- 在编译期,
extern用于告诉编译器某个变量或函数的定义在其他源文件中,编译器会为它生成一个符号表项,并在当前源文件中建立一个对该符号的引用。
这个引用是一个未定义的符号,编译器在后续的链接过程中会在其他源文件中查找这个符号的定义。
- 在链接期,链接器将多个目标文件合并成一个可执行文件,并且在当前源文件中声明的符号,会在其它源文件中找到对应的定义,并将它们链接起来
extern C
如果不用 extern C,由于 C++ 和 C 语言在编译和链接时使用的命名规则不同,这会导致 C++ 代码无法调用 C 语言编写的函数或变量(链接时找不到符号)
函数命名规则
由于需要支持重载,一个函数的链接名(Linkage Name)是由函数的名称、参数类型和返回值类型等信息组成的,用于在编译和链接时唯一标识该函数。函数的链接名的生成规则在不同的编译器和操作系统上可能有所不同,一般是由编译器自动处理,不需要手动指定,这个规则常常叫做Name Mangling
常见的规则:
- Microsoft Visual C++ 编译器(Windows):函数的名称会被编译器修改为一个以 “?” 开头的名称,并加上参数类型和返回值类型等信息,以避免链接冲突。例如,函数
int add(int a, int b)的链接名可能是?add@@YAHHH@Z。 - GCC 编译器(Linux):也会加上参数类型和返回值类型等信息。例如,函数
int add(int a, int b)的链接名可能是_Z3addii。 - Clang 编译器(MacOS):函数的链接名的生成规则与 GCC 编译器类似,但稍有不同。例如,函数
int add(int a, int b)的链接名可能是_Z3addii。
而 C 语言的链接函数名规则又和 上面三个 C++ 不一样,通过在 C++ 代码中使用 extern "C" 关键字,可以将 C++ 编译器的命名规则转换为 C 语言的命名规则,从而使得 C++ 代码可以调用 C 语言的函数或变量。
Name Mangling(名称修饰 / 改名机制)是编译器在编译源代码时,将变量、函数或方法的原始名称,转化为包含更多上下文信息的“独特新名称”的技术。
使用 extern "C" 声明的函数或变量会采用 C 语言的链接规则,即符号的名称和调用约定与 C 语言相同。
需要注意 extern "C" 关键字只对函数的名称和调用约定起作用,对于函数的参数类型和返回值类型没有影响。所以,在使用 extern "C" 声明函数时,需要保证函数的参数类型和返回值类型与 C 语言的定义相同,否则可能会导致编译错误或运行时错误。
mutable作用
mutable是C++中的一个关键字,用于修饰类的成员变量,表示该成员变量即使在一个const成员函数中也可以被修改。因为在C++中,如果一个成员函数被声明为const,那么它不能修改类的任何成员变量,除非这个成员变量被声明为mutable。
如果需要在const函数里面修改一些跟类状态无关的数据成员,那么这个函数中对应的成员就应该被mutable来修饰。允许在const成员函数中修改特定的成员变量,以支持内部实现所需的功能,同时仍然保持外部不变性。
四种强制类型转换
在 C++ 中,更推荐使用四个转换操作符来实现显式类型转换:
- static_cast
- dynamic_cast
- const_cast
- reinterpret_cast
static_cast
- 静态(编译期检查),将一个基本类型转为另一个基本类型
- 指针类型之间转换.将一个指针类型转换为另一个指针类型,尤其是在类层次结构中从基类指针转换为派生类指针。这种转换不执行运行时类型检查,可能不安全,要自己保证指针确实可以互相转换。
- 引用类型之间的转换
C++ 的 RTTI 信息是存放在虚函数表(vtable)里的。如果基类没有虚函数,它就不会生成虚函数表,
dynamic_cast也就失去了赖以生存的底层数据,编译器会直接报错。
面向对象
多态
C++中的多态是指同一个函数或者操作在不同的对象上有不同的表现形式。
C++实现多态的方法主要包括虚函数、纯虚函数和模板函数
其中虚函数、纯虚函数实现的多态叫动态多态,模板函数、重载等实现的叫静态多态。
区分静态多态和动态多态的一个方法就是看决定所调用的具体方法是在编译期还是运行时,运行时就叫动态多态。
虚函数、纯虚函数实现多态
在 C++ 中,可以使用虚函数来实现多态性。
虚函数是指在基类中声明的函数,它在派生类中可以被重写。
当我们使用基类指针或引用指向派生类对象时,通过虚函数的机制,可以调用到派生类中重写的函数,从而实现多态。
C++ 的多态必须满足两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数是虚函数,且必须完成对基类虚函数的重写
虚函数表
虚函数是通过一张虚函数表(Virtual Table)来实现的,简称为V-Table。在这个表中,存放的是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。
这个类的实例内存中都有一个虚函数表的指针,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
在底层,当一个类声明一个虚函数时,编译器会为该类创建一个虚函数表(Virtual Table)。
这个表存储着该类的虚函数指针,这些指针指向实际实现该虚函数的代码地址。
每个对象都包含一个指向该类的虚函数表的指针,这个指针在对象创建时被初始化,通常是作为对象的第一个成员变量。
当调用一个虚函数时,编译器会通过对象的虚函数指针查找到该对象所属的类的虚函数表,并根据函数的索引值(通常是函数在表中的位置,编译时就能确定)来找到对应的虚函数地址。
然后将控制转移到该地址,实际执行该函数的代码。对于派生类,其虚函数表通常是在基类的虚函数表的基础上扩展而来的。在派生类中,如果重写了基类的虚函数,那么该函数在派生类的虚函数表中的地址会被更新为指向派生类中实际实现该函数的代码地址。
C++的动态多态必须满足两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数是虚函数,且必须完成对基类虚函数的重写
其中第一条很重要,当我们使用派生类的指针去访问/调用虚函数时,实际上并未发生动态多态,因为编译时就能确定对象类型为派生类型,然后直接生成调用派生类虚函数的代码即可,这种叫做静态绑定。通过基类的指针或引用调用虚函数才能构成多态,因为这种情况下运行时才能确定对象的实际类型,这种称为动态绑定
纯虚函数
纯虚函数是一种在基类中声明但没有实现的虚函数。
它的作用是定义了一种接口,这个接口需要由派生类来实现。
包含纯虚函数的类称为抽象类(Abstract Class)。抽象类仅仅提供了一些接口,但是没有实现具体的功能。作用就是制定各种接口,通过派生类来实现不同的功能,从而实现代码的复用和可扩展性。
另外,抽象类无法实例化,也就是无法创建对象。原因很简单,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。
虚函数使用了一种称为虚函数表(vtable)的机制。然而,在调用构造函数时,对象还没有完全创建和初始化,所以虚函数表可能尚未设置。
这意味着在构造函数中使用虚函数表会导致未定义的行为。只有执行完了对象的构造,虚函数表才会被正确的初始化。
内存管理
RAII
资源获取即初始化(Resource Acquisition Is Initialization,简称 RAII)是一种 C++ 编程技术,它将在使用前获取(分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥量、磁盘空间、数据库连接等有限资源)的资源的生命周期与某个对象的生命周期绑定在一起。
确保在控制对象的生命周期结束时,按照资源获取的相反顺序释放所有资源。
同样,如果资源获取失败(构造函数退出并带有异常),则按照初始化的相反顺序释放所有已完全构造的成员和基类子对象所获取的资源。
这利用了核心语言特性(对象生命周期、作用域退出、初始化顺序和堆栈展开),以消除资源泄漏并确保异常安全。
在实际的 C/C++ 开发中,我们经常会遇到诸如 coredump、segmentfault 之类的内存问题,使用指针也会出现各种问题,比如:
- 野指针:未初始化或已经被释放的指针被称为野指针
- 空指针:指向空地址的指针被称为空指针
- 内存泄漏:如果在使用完动态分配的内存后忘记释放,就会造成内存泄漏,长时间运行的程序可能会消耗大量内存。
- 悬空指针:指向已经释放的内存的指针被称为悬空指针
- 内存泄漏和悬空指针的混合:在一些情况下,由于内存泄漏和悬空指针共同存在,程序可能会出现异常行为。
智能指针是一种可以自动管理内存的指针,它可以在不需要手动释放内存的情况下,确保对象被正确地销毁。
这种指针可以显著降低程序中的内存泄漏和悬空指针的风险。
在C++中,智能指针常用的主要是两个类实现:
- std::unique_ptr
- std::shared_ptr
智能指针是一种可以自动管理内存的指针,它可以在不需要手动释放内存的情况下,确保对象被正确地销毁。这种指针可以显著降低程序中的内存泄漏和悬空指针的风险。
std::unique_ptr是一个独占所有权的智能指针,它保证指向的内存只能由一个unique_ptr拥有,不能共享所有权。
当unique_ptr超出作用域时,它所指向的内存会自动释放。1
2
3
4
5
6
7
8
9
int main() {
std::unique_ptr<int> ptr(new int(10));
std::cout << *ptr << std::endl; // 输出10
// unique_ptr在超出作用域时自动释放所拥有的内存
return 0;
}
std::shared_ptr是一个共享所有权的智能指针,它允许多个shared_ptr指向同一个对象,当最后一个shared_ptr超出作用域时,所指向的内存才会被自动释放。
举个栗子:1
2
3
4
5
6
7
8
9
10
int main() {
std::shared_ptr<int> ptr1(new int(10));
std::shared_ptr<int> ptr2 = ptr1; // 通过拷贝构造函数创建一个新的shared_ptr,此时引用计数为2
std::cout << *ptr1 << " " << *ptr2 << std::endl; // 输出10 10
// ptr2超出作用域时,所指向的内存不会被释放,因为此时ptr1仍然持有对该内存的引用
return 0;
}
总的来说,智能指针可以提高程序的安全性和可靠性,避免内存泄漏和悬空指针等问题。
但需要注意的是,智能指针不是万能的,也并不是一定要使用的,有些场景下手动管理内存可能更为合适
