C++ 语言入门
在理解 Rust 内存管理的本质之后(参见 Rust 入门 ),再来了解一下 C++ 语言和它的内存管理。这样和 Rust 对比一下,就能更加理解 Rust 内存管理方式和特点。进而更好地使用 C++ 进行编程。
JavaScript, Python, Java 和其它大部分语言都是自动管理内存的。所以写代码时不需要自己去处理内存管理这个事。其中 Java 的自动内存管理依靠的是垃圾回收(Garbage Collection)。所以通常来说,需要自己管理内存的情况不多。内存管理不算是很重要的事情。但了解内存管理的知识也会有助于我们的代码逻辑。
如果从具有垃圾回收(Garbage Collection)的语言出发来类比和理解,可能到后来你就始终弄不明白 C++ 这样的语言到底要怎么写代码。
当年 Ian 刚开始学计算机是从 Pascal 语言开始的。之后马上又学了 C 语言 。因为传言 C 语言比 Pascal 好。而 C 语言的内存管理都是手动的。所以经常会出现一些很奇怪的问题。但是通过学习 Rust 或者 C++ ,你就能从本质上理解用 C 语言怎么会遇到那么稀奇古怪的一些问题:要么你内存泄漏了,要么你就是出现一些内存错误。比如说有一个地方的值莫名其妙地被人改了,你就不知道哪里的代码在改那个内存。因为内存管理弄错了之后,你的指针就有可能指向它不该指的地方。
Rust 语言会强制你去思考这些内存问题,所以你就被迫就要把这些事情按照它的规则做对。然后 Rust 的内存管理思路大致上是对的。就是它强制你有个 ownership 。这样通常情况下只有一个变量可以指向这个对象。然后如果你需要共享的话,你就得自己显式地使用那个 Rc(引用计数)的方式。习惯了 Rust 之后再去写 C 语言或者 C++ ,就不会那么容易有问题了——当然,你还是可能会犯错的。
C++ 的 Smart Pointer 设计,其使用的方式稍微改一点,就能实现和 Rust 同样的目标,而不太增加思维负担。不过 C++ 有一个讨厌的地方是,它的出错信息太难以阅读了。写错代码之后的报错信息令人困惑,不知哪里错了。因为它的 Smart Pointer 这些东西是靠 Template 和宏展开之后实现出来的,它并不是 C++ 语言直接实现了这些东西。所以你一旦出点错,它是展开了一堆内部实现的代码,最后给你说展开之后的代码哪里错了。但展开之后的代码跟你写的代码完全是两码事了。这就是 C++ 的一个问题。
在熟悉 C++ 之后,可以进一步思考一下 C++ 和 Rust 关于内存做法的各自优缺点。
C++ 编程环境设置
首推自然是使用友好功能强大的 CLion 作为 C++ 的 IDE。不过这是收费的软件,30 天免费试用期过后就需要付费订阅。适合需要长期使用 C++ 的玩家们。
作为入门学习体验,也可以退而求其次继续选择免费的 VS Code 。此时要先安装 GNU C++ 或者 Clang C++ 的编译器。对于 MacOS 的用户,推荐用 Clang ,因为它包含在 Xcode 命令行工具里,安装非常方便。下面以 MacOS 的 VS Code 为例配置编程环境:
首先,安装和配置 VS Code ,可以参考这篇上手 Visual Studio Code ;
其次,安装 C++ extension for VS Code,这个也可以在 VS Code 里直接搜索安装;
然后是安装 C++ 编译器 Clang (C++ 需要编译之后运行):
## 安装 Command Line Tools - MacOS 编程软件 Xcode 的命令行工具
$ xcode-select --install
## 完成后可以使用一下命令查看版本说明安装成功了
$ clang --version
Apple clang version 17.0.0 (clang-1700.0.13.5)
Target: arm64-apple-darwin24.5.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin安装完 Clang 之后,在 VS Code 中新建后缀为 .cpp 的文件比如 demo.cpp 就能开始编程了(建议建一个文件夹比较好,因为编译会生成新文件):
#include <iostream>
// #include <...> 是加载外部代码,上面是引入标准输入输出库(以便使用下面的 std::cout 输出内容)
// C++ 的 main 函数一定要返回 int
int main()
{
std::cout << "Hello C++" ;
return 0;
}注意:MacOS 升级到 Sequoia 后,可能需要重装 Command Line Tools 才能正确运行代码,否则会有类似 'iostream' file not found 之类找不到标准库头文件的报错:
$ sudo rm -rf /Library/Developer/CommandLineTools # Uninstall - 先卸载
$ xcode-select --install # Reinstall - 再安装此外,也许你的代码能正常运行,但 VS Code 总在各种 #include <...> 的代码下方标记红色波浪线,提示找不到标准库文件的报错。可参考这里解决:
- 在终端里使用命令
gcc -v -E -x c++ -来获取所有标准库目录:
$ gcc -v -E -x c++ -
...
...
#include <...> search starts here:
/usr/local/include
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1
/Library/Developer/CommandLineTools/usr/lib/clang/17/include
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include
/Library/Developer/CommandLineTools/usr/include
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks (framework directory)
End of search list.- 打开
c_cpp_properties.json文件,把上述目录增加到"includePath"一栏中保存即可(该文件通常在你当前 VS Code 工作目录下的.vscode文件夹中,若没有,参考这里通过 Command Palette 中输入C/C++ Edit Configurations (JSON)创建):
{
"configurations": [
{
"name": "Mac",
"includePath": [
"${workspaceFolder}/**",
"/usr/local/include",
"/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1",
"/Library/Developer/CommandLineTools/usr/lib/clang/17/include",
"/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include",
"/Library/Developer/CommandLineTools/usr/include",
"/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks"
],
"defines": [],
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "macos-clang-arm64",
"compilerPath": "/usr/bin/clang++"
}
],
"version": 4
}其他的一些方便的设置
设置花括号另起一行:
Settings-> 搜索C_Cpp.clang_format_fallbackStyle
在输入框处填上"Visual Studio",也就是使用 VS 的格式
这样在编辑完代码后,就可以使用快捷键option+shift+F来格式化代码,让花括号另起一行
最开始 C 语言的时候大家花括号都是写在下一行的,自从有了 Java ,大家才开始写埃及括号 Egyptian brackets ,然后代码就挨得太近了
要注意的是,该格式化也会把int* p = ...改成int *p = ...,但int*其实是一个「类型」,代表整数指针。写成int *p会和解引用计算*p混淆。正确写法还是要int* p表示变量 p 是整数指针类型;
你如果想抬杠把它理解成先计算*p,然后再把计算结果标记为int类型,那逻辑就绕了。而且类型标记是标记变量和函数的,不是标记表达式的值的;VS Code 默认用的是
g++来编译,最好能加上一个参数-std=c++17,这样就能使用 C++17 标准来编译。这意味着你的代码可以用一些新的写法和功能,比如使用std::make_unique函数等。
参考上手 Visual Studio Code 打开配置文件 settings.jason :Extensions Settings->Code-runner:Executor Map->Edit in settings.jason
在 cpp 那行的g++后面编辑添加std=c++17保存即可。编辑完成后这行的文本如下:"cpp": "cd $dir && g++ -std=c++17 $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt",
开始编程
C++ 和 Java 非常相似,对熟悉 Java 的玩家来说,开始的这些代码和概念可以快速通过:
#include <iostream> // 标准输入输出 - std::cout 等
#include <string> // 字符串拼接、查找、截取等操作 - append 等
#include <memory> // 智能指针和内存管理工具 - std::unique_ptr 和 std::shared_ptr 等
#include <vector> // 动态数组容器(长度可自动扩展)- resize() 等
#include <map> // 键值对映射(有序字典)- std::map 等
#include <stdexcept> // 标准异常类型,用于错误处理 - std::runtime_error 等
#include <cmath> // 数学函数和常量 - sqrt() 等
// 如果没有 using namespace std; 那 cout 和 endl 等就都要加上 std::
// 以下代码只有一个 cout ,所以可以不用担心可能导致的命名冲突
using namespace std;
// C++ 不允许你在函数内部定义函数,编译不通过,所以 fact 定义到外面
// C++ 通过「函数指针」和「Lambda 表达式」来实现类似其他语言的内部函数功能
// 尽管有编译过程,但是函数要定义在调用的前面才能被识别,否则就会报错 undeclared
// fact 定义在 basic_test 之前,basic_test 内部才能调用它
int fact(int n)
{
if (n == 0)
{
return 1;
}
else
{
return n * fact(n - 1);
}
}
// ###########################################################
// 基本语法
// ###########################################################
void basic_test()
{
// 如果没有上面的 using namespace std; 那输出的 cout 和换行 endl 等等就都要加上 std::
// std::cout << "---- basic_test ----" << std::endl;
cout << "---- basic_test ----" << endl;
cout << "Hello World!" << endl;
int x = 2 * 3;
cout << "x = " << x << endl;
int y = 1 + x;
cout << "y = " << y << endl;
const int z = 5; // const 代表常数,不能改动
cout << "z = " << z << endl;
// z = 10; // Error: assignment of read-only variable 'z'
float a = 3.14;
cout << "a(float) = " << a << endl;
double b = 3.14;
cout << "b(double) = " << b << endl;
char c = 'A';
cout << "c = " << c << endl;
bool d = true;
cout << "d = " << d << endl;
string e = "Hello"; // string 类型是小写开头的 Java 是大写 String
cout << "e = " << e << endl;
cout << e + " World!" << endl;
// 这种写法是不行的,标记了 string 类型的 e 才有 .append 这个内部函数可调用
// "Hello" 没标记类型,C++ 无法自动判断类型
// cout << "Hello".append(" World!") << endl;
e.append(" World!");
cout << "e = " << e << endl;
cout << sqrt(25) << endl; // sqrt function is defined in the cmath header file.
cout << fact(5) << endl;
// 注意数组的值用的是花括号 {} 而不是 [],不能写 [1, 2],要写 {1, 2}
string arr[] = {"Hello", "World", "Good", "Morning", "Everyone"};
// C++ 的数组没有 length 函数
// sizeof(arr) 得到整个数组占用多少内存(单位:字节)
// sizeof(string) 则得到 string 对象的大小(单位:字节)
// 两者除一下,计算出数组的长度 length
cout << "arr length = " << sizeof(arr) / sizeof(string) << endl;
for (int i = 0; i < 5; i++) // 打印数组 arr 内容(方法一)
{
cout << "for: " + arr[i] << endl;
}
arr[3] = "Evening";
for (string s : arr) // 打印数组 arr 内容(方法二)
{
cout << "for-each: " + s << endl;
}
// // 上面(方法二)其实建议用 for (const string& s : arr) {...}
// for (const string& s : arr)
// {
// cout << "for-each: " + s << endl;
// }
int arr2[] = {1, 2, 3, 4, 5};
cout << "arr2 length = " << sizeof(arr2) / sizeof(int) << endl;
int total = 0;
for (int i = 0; i < 5; i++)
{
total += arr2[i];
}
cout << "Total = " << total << endl;
// 如果 struct 内部定义了函数的话,这个函数是单独存放的
// 不算 struct 里的 field(struct 里定义的成员变量叫 field,有几个就是几个 field)
// 比如你这里定义了一个 printUser() 函数,然后 unique_ptr<User> u1
// 然后 move 它:unique_ptr<User> u2 move(u1),“所有权”转移到 u2
// 按理说此时变量 u1 被清零了(地址为 0),但 u1.printUser() 仍然能调用
// 除了 struct 外,class 的行为也是如此
// 思考:struct 和 class 有什么区别?哪些情况下时候分别用哪个?
// class 中,默认的成员访问级别是 private,而在 struct 中,默认的成员访问级别是 public;
// class 常用于定义复杂行为和数据封装的对象,如包含多种类方法;
// 而 struct 则用于轻量级的简易数据结构,如坐标等:
struct Point
{
double x; // 默认 public,Point 的外部可访问 p.x
double y; // 默认 public, Point 的外部可访问 p.y
}; // struct 末尾这里得加一个分号,这点和 class 不同
// struct 可以在函数体里定义,当前就在 basic_test 这个函数体内
struct User
{
string name;
int age;
}; // C++ 这里要加一个分号
// User u1 = {"John", 25};
// User u1 = {name : "John", age : 25}; // 加上 key 后更明确(这样写和其他语言像一点)
User u1 = {.name = "John", .age = 25}; // C++ 编译器推荐用这种 .name 的写法
cout << "u1.name = " << u1.name << endl;
cout << "u1.age = " << u1.age << endl;
// 匿名的 struct ,直接作为变量的类型
struct
{
string name;
int age;
} u2 = {"John-2", 25};
cout << "u2.name = " << u2.name << endl;
cout << "u2.age = " << u2.age << endl;
}
// ###########################################################
// 指针和地址
// ###########################################################
void pointer_test()
{
cout << "---- pointer_test ----" << endl;
// 先来理解一下「指针」和「引用」两个概念,并区分 * 和 & 符号的不同作用;
int num = 42;
int* ptr = # // int* 是指针类型(整数指针); &num 中的 & 操作是读取 num 的地址
int& ref = num; // ref 是 num 的引用; int& 是引用类型(整数引用),和上面的 & 不同
cout << "&num = " << &num << endl; // &num = 0x16b532adc // num 指向的内存地址是 0x16b532adc
cout << "ptr = " << ptr << endl; // ptr = 0x16b532adc // ptr 的值是 0x16b532adc(ptr 是指针类型,值就是地址)
cout << "*ptr = " << *ptr << endl; // *ptr = 42 // 这里 * 是解引用,读取 ptr 地址里存放的值;
cout << "ref = " << ref << endl; // ref = 42
cout << "num = " << num << endl; // num = 42
ref = 29;
cout << "num = " << num << endl; // num = 29
cout << "ref = " << ref << endl; // ref = 29
// 直接定义的整数 10 被 C++ 放在栈上(和 Java 不同的是,C++ 的「堆」和「栈」分得很清楚的)
// x 的数据 2 在 stack 上
int x = 2;
cout << "x = " << x << endl; // 直接可以打印出 x 的值 2
cout << "&x = " << &x << endl; // 打印 x 的地址:符号 & 代表取得该变量的地址
// 在 C++ 中,你使用 new 关键字在堆(heap)上分配内存,并得到一个指向该内存的指针。
// 当不再需要这块内存时,必须使用 delete 关键字将其手动释放。
// heap 上的数据不受作用域限制,函数体外仍能继续访问,直到手动释放。
// 此处的定义,数据 int(10) 在 heap 上,指针变量 p 则在 stack 上。
// int(10) 这一数据在当前函数 pointer_test 之外还能访问(比如通过地址),因为在 heap 上。
// 但是变量 p 在 pointer_test 之外则被注销自动释放了,因为 p 在 stack 上。
// int* 的这个 * 号是 int* 这个类型的一部分,下面的 *p 的 * 号则是解引用操作符,两者不一样
// Rust 的 * 只是用来 move 改变 ownership 的,虽然也叫 dereference 但和这的 *p 本质不一样
// C++ 里,看到关键词 new 就要知道它肯定在 heap(堆)上了,因为 new 就是在 heap 上分配内存的操作
int* p = new int(10); // new int(10) 在 heap 上(后续要 delete 手动释放内存),但变量 p 则在 stack 上
// int(10) 这个值为 10 的整数是存放在 heap 上的,有一个对应的 heap 上的地址;
// int(10) 在 heap 上的地址被存放在了变量 p 里;
// p 的类型是 int* ,p 被存放在 stack 上,有一个对应的 stack 上的地址;
// 直接打印 p 会打印出 p 中数据的内存地址,即 10 存放在作为 heap 的内存的地址(0x开头的16进制数)
cout << "p = " << p << endl;
cout << "*p = " << *p << endl;
// int* 是一个「类型」,而 *p 表示「解引用 p」,* 在这两者中的意思是不同的
// 同理,int** 也是一个「类型」,所以虽然 int **q 和 int ** q 也能编译通过,
// 但建议写 int** q ,这样就能反应出 int** 是一个类型标记,这也是它的本质
// 使用 VS Code 格式化代码会使得 int** q 被改写为 int **q ,而这样写会和 **q 混淆
// &p 是取得 p 的地址,和 Rust 中作为借用(borrow)的 & 不同
int** q = &p; // q 本身在 stack 上,指向(存放)同样在 stack 上的 p 的地址 &p
cout << "q = " << q << endl; // 可发现 &p 和 &x 值很接近,因为 p 和 x 在 stack(栈) 上是相邻的
cout << "*q = " << *q << endl;
cout << "**q = " << **q << endl;
// r 存放 q 的地址
int*** r = &q;
cout << "r = " << r << endl;
cout << "*r = " << *r << endl;
cout << "**r = " << **r << endl;
cout << "***r = " << ***r << endl;
// s 存放 r 的地址
int**** s = &r;
cout << "s = " << s << endl;
// 这里 p2 的类型为什么是 int* 而不是 int[]* ?为什么是 int 的指针类型而不是数组的指针类型?
// 因为数组里所有元素是挨在一起相邻储存的,故知道了第一个元素的地址,就知道整个数组的所有元素的地址
// p2 会指向向数组的第一个数据 3 的地址,即 *p2 = 3
// 此外 p2 的内容在 heap 上,后续要手动用 delete 释放内存
int* p2 = new int[5] {3, 5, 7, 9, 11}; // 注意,C++11 之前的标准不支持直接使用列表初始化数组
cout << "p2 = " << p2 << endl;
cout << "*p2 = " << *p2 << endl; // 数组指针指向数组第一个元素的地址,即 3
cout << "p2 + 1 = " << p2 + 1 << endl; // 数组第二元素 5 的地址
cout << "*(p2 + 1) = " << *(p2 + 1) << endl; // 数组第二元素的值,即 5
int** q2 = &p2;
cout << "q2 = " << q2 << endl; // 指针 p2 的地址
cout << "*q2 = " << *q2 << endl; // 指针 p2 存放的内容,即数组的地址
cout << "**q2 = " << **q2 << endl; // 数组的地址对应的内容,即数组第一个元素的内容:3
// 在 C++ 中,你使用 new 关键字在堆(heap)上分配内存,并得到一个指向该内存的指针。
// 当不再需要这块内存时,必须使用 delete 将其手动释放,否则会内存泄漏((Memory Leak)
// 下面分别是释放单个对象和对象数组的方式,分别要用 delete 和 delete[]
// delete 操作符后面跟的虽然是指针变量 p,但它的目标不是释放 p 这个变量本身(p 在 stack 上不需要手动释放)
// 而是释放 p 变量中所存储的那个地址所指向的堆内存。
// 释放完后还要把相关指针设置为 nullptr,防止它们成为悬挂指针(Dangling Pointer)
delete p;
p = nullptr;
delete[] p2;
p2 = nullptr;
// 正因为手动管理 new/delete/delete[] 并记得设置 nullptr 非常繁琐且容易出错
// 比如忘记 delete、重复 delete、用错 delete[] 等
// 现代 C++ (C++11 及以后) 强烈推荐使用智能指针 (std::unique_ptr 和 std::shared_ptr)
// 智能指针可以为你自动处理这一切。参见本文接下来的内容。
}
void address_test()
{
cout << "---- address_test ----" << endl;
int i = 10;
string s = "Hello";
int* pi = new int(10);
// i 和 s 的地址是挨在一块的,都在 栈(stack) 上
// pi 则在堆(heap)上,打印出的地址上可以看到距离 i 和 s 较远
cout << "address of i = " << &i << endl; // stack
cout << "address of s = " << &s << endl; // stack
cout << "pi = " << pi << endl; // heap
delete pi; // 手动释放在 heap 上的内存,即 pi 指针指向的地址的内存
pi = nullptr; // 设置为空指针
}
// ###########################################################
// 传递引用
// ###########################################################
void pass_by_reference(int& x) // x 会成为传入参数的别名
{
cout << "pass_by_reference: x = " << x << endl;
cout << "address of x: " << &x << endl;
x += 1; // 修改 x 就是修改传入的变量
}
void pass_by_reference_const(const int& x)
{
cout << "pass_by_reference_const: x = " << x << endl;
cout << "pass_by_reference_const: address of x: " << &x << endl;
}
void reference_test()
{
cout << "---- reference_test ----" << endl;
int x = 10;
int& y = x; // 同上文的理,int& 是一个类型,& 是 int& 类型的一部分
cout << "x = " << x << endl;
cout << "y = " << y << endl;
x = 20; // 给 x 赋值 20 ,y 也会改变
cout << "x = " << x << endl;
cout << "y = " << y << endl;
y = 30; // 给 y 赋值 30 ,x 也会改变
cout << "x = " << x << endl;
cout << "y = " << y << endl;
// &x and &y are the same - x 和 y 的地址相同
// meaning: x and y is the same variable - // 可看出 x 和 y 就是同一个东西
// y 和 x 的地址一样,说明 stack 上没有分配一个空间位置给 y ,y 只是 x 的一个别名
// 之后会看到 y 这种东西有什么用 - .borrow_mut()
cout << "&x = " << &x << endl;
cout << "&y = " << &y << endl;
// // Can't take non-const reference of temporary
// // 如果这个能通过,那 z 就是 x + 1 的别名,那 z = 100 这样的赋值就能写
// // 但 z = 100 相当于对 x + 1 赋值,语义上就很奇怪的,所以不能这么写
// int& z = x + 1;
// 但是你可以有下面这样一个引用,因为是 const 所以你后续不能对 z 赋值了:
const int& z = x + 1;
cout << "z = " << z << endl;
// 这里定义了名为 z 的 const int& (对常量整数的引用),并将其绑定到表达式 x + 1 的结果上
// z 不是 x + 1 的别名(Alias),而是 x + 1 这个表达式的值的别名
// z 绑定的是一个临时的、值为 11 的新对象
// 由于 z 是 const 的,你不能通过 z 来修改这个值
// 即使你后来修改了 x 的值(比如 x = 50;),z 的值仍然是 11
// 因为 z 绑定的是那个临时对象,与 x 已经没有关系了
// int&:对整数的引用 (Reference to int)
// const int&:对常量整数的引用 (Reference to a constant int)
// const 引用可以绑定到临时对象(右值 rvalue),而普通的非 const 引用则不能
// 上面的 const int& z 虽然感觉上不如直接写成 int z
// 不过 const int& z 没发生拷贝操作,而是直接让 z 指向表达式 x + 1 的临时对象(右值)
// 相当于 heap 上的这个临时对象(右值)的生命周期被延长了,变得和 z 一样
// int z 则是将表达式的值(临时对象,或者说右值)拷贝到 stack 上新创建的内存空间上
// 然后临时对象的生命周期结束被销毁
// 对于 x + 1 这种简单的数来说,拷贝操作不算什么,而对于那些大数据的对象来说,少拷贝更好
// 此外 const int& z 定义的 z 是只读的,不能修改,而 int z 的 z 是变量,可修改
// 综上,C++ 里的 const T& 主要目的是以只读的方式高效地访问对象,避免不必要的拷贝
int z_non_const = x + 1;
cout << "z_non_const = " << z_non_const << endl;
int a = 12;
cout << "&a = " << &a << endl;
cout << "before a = " << a << endl;
pass_by_reference(a);
cout << "after a = " << a << endl;
// Doesn't work because pass_by_reference takes a non-const int&
// 原理和上面的 const int& z = x + 1; 一样
// pass_by_reference(x + 1);
pass_by_reference_const(x + 1); // 参数类型是 const int& ,代表无法更改,所以可允许
// pass_by_reference 和 pass_by_reference_const 这种用法是 C 语言和 C++ 特有的
// 补充知识:
// 左值 (lvalue - locator value)指的是一个具有持久身份(identity)和确定内存地址的对象或表达式。
// 可以把它想象成一个有“家”(内存地址)的东西,可以被反复访问和修改.比如 int x = 9; 中的 x
// 右值 (rvalue - read value)指的是一个没有持久身份、通常是临时的表达式结果。
// 可以把它想象成“一次性用品”,用完即丢,没有固定的“家”。如字面量 (Literals):10 或算术表达式的结果:x + 1
// 由于没有可识别的内存地址:通常不能对右值取地址(&)
}
// ###########################################################
// 面向对象
// ###########################################################
// 虽然在 C++ 中,class 既可以定义在函数外部,也可以定义在函数体内部;
// 但是绝大多数(99% 以上)的情况下,你应该将 class 定义在函数外部,这里是定义在 inheritance_test 外;
// 这是 C++ 编程的标准和推荐做法。将类(如 class Animal)视为一个独立的、可复用的构建模块;
// 在实际项目中,类的声明通常放在头文件(.h 或 .hpp)中,如 Animal.h
// 以便被多个源文件(.cpp)包含和使用:.cpp 文件开头写 #include "Animal.h" 类似 #include <iostream>
// 这天然就要求类(class)的定义位于所有函数之外。
// 在函数内部定义的类被称为局部类 (Local Class)。这是一种不常见的用法,主要用于实现高度的封装和局部化。
// 局部类的定义(作用域)只在它所在的函数体内有效。
// 在现代 C++ (C++11及以后),很多需要局部类的简单场景已经被 Lambda 表达式所取代
// 因为 Lambda 更简洁。但如果这个辅助工具逻辑复杂,需要多个成员函数和变量时,局部类依然有其用武之地。
class Animal
{
// 外部能用的成员和方法都写到 public: 后面
// 不像 Java 那样每个都要写一次 public
public:
virtual void speak() = 0;
// 这里 virtual ... = 0 的写法类似 Java 的 abstract class(抽象类)
// 这个 = 0 的 C++ 语法意味着 speak 是个纯虚函数
// 纯虚函数没有实现,因此其功能要求由子类来定义
// 相当于强制要求继承 Animal 的子类定义 speak 方法(函数)
// C++ 中一个类如果包含至少一个纯虚函数(= 0),那么这个类就是一个抽象类
// 抽象类不能直接实例化(instantiation),它只能作为基类被继承
// C++ 中得写成这种奇怪的样子
// 如果不加 virtual ,那子类就无法 override(重写)这个 speak 方法了
// 如果不写 = 0 ,就无法强制子类定义 speak 函数
// 重写(override)父类函数时,参数类型、顺序、数量,函数返回类型必须完全相同,区别与重载(overload)
// 重载(overload)是同一作用域内(如同一类中),函数名相同但参数列表必须不同(参数类型、数量或顺序)
// 重载(overload)函数的返回值类型可以相同,也可以不同;
// 与抽象类对应的就是普通类(Concrete Class)。普通类指的是没有任何纯虚函数的类,它可以直接被实例化。
// 普通类可以包含虚函数(即实现了虚函数的具体方法),但它不能包含纯虚函数
// virtual 关键字代表支持动态绑定(运行时再判断函数调用哪个,比如子类同名函数)
// 若不写 virtual,则为普通函数(非虚函数),是静态绑定。意味着在编译时就确定了调用哪个函数
// 即基类指针定义 Animal* a = new Dog(); 写了 virtual ,a->eat() 就调用子类 Dog 的 eat,没写就调用基类 Animal 的
virtual void eat()
{
cout << "Animal is eating" << endl;
}
};
// 要在 Dog 里能访问到 Animal 里的成员,就要写上 public
class Dog: public Animal
{
public:
void speak() override
{
cout << "汪!" << endl;
}
// 重写 speak 方法
// 更严谨的做法是在 speak() 后面加上 override
// 类似 Java 的 @Override 标记
// 重写(override)父类函数时,参数类型、顺序、数量,函数返回类型必须完全相同,区别与重载(overload)
// 关键字 override 不写也行(编译器可能有警告提示),但写的话编译器会去检查父类的内容
// 检查如果父类没有 speak ,那就会编译不通过(父类却没有你要重写的函数时,就会报错)
// 显式写 override ,明确地表达程序员的意图永远是最好的
void eat() override
{
cout << "Dog is eating" << endl;
}
void bark()
{
cout << "Dog is barking" << endl;
}
};
class Cat: public Animal
{
private:
int age; // 定义在 private 下方外部就无法直接访问,比如 c.age 就会失败
public:
string name; // 定义在 public 下方外部就能直接访问,比如 c.name
// // 构造函数 - 函数体赋值方式 - C++ 不推荐
// Cat(string n, int a) : name(n), age(a)
// {
// age = a;
// name = n;
// cout << "Cat Constructor called - Assignment\n";
// }
// 构造函数 - C++ 惯例推荐用这种「初始化列表」的写法来给 name 和 age 赋值
Cat(string n, int a) : name(n), age(a)
{
cout << "Cat Constructor called - Initializer List\n";
}
void speak() override
{
cout << "喵!" << endl;
}
};
void inheritance_test()
{
cout << "---- inheritance_test ----" << endl;
// 栈(stack)上面定义了狗 d 和猫 c
// Dog 和 Cat 的实例(instance)在 stack 上(函数的局部变量都在 stack 上)
Dog d; // 在 stack 上
Cat c("Alice", 30); // 在 stack 上
int i = 9; // 在 stack 上
// 定义在 heap 上的 Dog(用 new 定义的都在 heap 上,返回包含 heap 地址)
Dog* d2 = new Dog(); // heap allocation
// 不能这么写,因为 new Dog() 在堆上,一定要个指针来指向它,而不能用普通的变量来存放它
// new Dog() 返回的是一个 heap 上的地址
// Dog d2 = new Dog(); // 编译不通过
// 打印变量们的地址,可以看到 c ,d 和 i 离得很近,都在 stack 上
// 而 d2 则是在 heap 上
// 注意,这里容易有歧义
// 这里说的在 stack 上或者在 heap 上指的是数据内容,即 Dog 和 Cat 的实例(instance)
// d , c , i , d2 这些变量本身都是在 stack 上的
// 然后以 d 为例,d 的内容是一个 Dog 实例,也就是数据本身,所以此时数据本身在 stack
// 但是 d2 的内容不是一个 Dog 实例,它的内容是 heap 上的地址
cout << "dog address: " << &d << endl;
cout << "cat address: " << &c << endl;
cout << "i address: " << &i << endl;
cout << "d2 address: " << d2 << endl; // heap 上的地址
d.speak();
c.speak();
cout << "The cat's name is: " << c.name << endl; // 可以直接访问内部的 name 成员
// cout << "The cat's age is: " << c.age << endl; // 无法直接访问内部的 age 成员
(*d2).speak(); // d2 是一个指针,先解引用得到指向的 Dog 实例,再调用 speak 方法
d2->speak(); // C++ 语法里还可以用箭头来访问指针指向的对象里的成员(方法)
delete d2; // 手动释放 d2 内存,避免内存泄漏
unique_ptr<Dog> d3 = make_unique<Dog>(); // 使用智能指针(自动管理内存)定义 heap 上的 Dog
cout << "unique_ptr d3 address: " << d3 << endl; // heap 上的地址
cout << "run (*d3).eat(): " << endl;
(*d3).eat(); // 静态调用
// 不需要手动调用 delete,unique_ptr 会自动释放内存
// 综上,同一个 Class 可以有 2 种不同的分配方法:栈(stack)上,或者堆(heap)上
// Animal a1 = d; // 编译不通过 - 为什么不能这么写?
// 原因:
// 这里将会把一个实际的 Dog 对象转为一个 Animal 对象,即对象切片(Object Slicing)
// 对象切片会丢失 Dog 类独有的成员(field),比如会丢失 bark 函数
// 然后 Dog 中定义的 speak 函数也会被 Animal 中的 speak 函数替代
// 但 Animal 中有纯虚函数(= 0),是个抽象类,意味着 Animal 中的 speak 没有具体实现,是“空”的
// 编译器不允许有空的实现,于是无法处理这个赋值
// 用一个 Animal 指针 a1 指向 d
Animal* a1 = &d;
cout << "Animal pointer to dog: " << endl;
cout << "animal address: " << a1 << endl;
a1->speak();
// 和 Java 不一样的是,如果上面不是用指针,而是 Animal a = d; 就编译不通过
// 在 C++ 里「用一个父类的变量指向一个子类」叫做多态,你必须要用指针,不能直接用这个变量
// 也就是类型必须是 Animal* 。必须使用基类指针Animal*指向派生类对象。
// 其实 Java 中父类指向子类也是类似。在 Java 里你写 Animal a 的时候,a 其实是一个指针
// Java 里你可以写 Dog d = new Dog(); 然后这个 new Dog() 实际上是在堆(heap)上
// 所以 Java 的这个 d 实际上是一个指针,但 Java 的语法不要求写 Dog* 表示指针
// 同样的表达在 C++ 中要写成:Dog* d = new Dog();
// 根据上面 reference_test 函数体里的写法,这里还可以这样写
Animal& a2 = d;
cout << "Animal pointer to dog: " << endl;
cout << "animal address: " << &a2 << endl;
a2.speak();
// 变量 a1 指向猫
a1 = &c;
cout << "Animal pointer to cat: " << endl;
cout << "animal address: " << a1 << endl;
a1->speak();
(*a1).eat(); // Animal is eating
// 关于上述使用智能指针 unique_ptr<Dog> 定义的 d3
// Animal* a3 = d3; 不能编译,因为 unique_ptr 是一个智能指针,有自定义的指针行为,不能直接赋值给原始指针 Animal*
// 你需要使用 .get() 方法来获取原始指针类型才和 Animal* 匹配
Animal* a3 = d3.get();
cout << "a3 gets Animal pointer from d3. Hence no Object Slicing." << endl;
a3->eat(); // Dog is eating
// // 下面这行代码编译不通过,因为类 Animal 有纯虚函数(= 0),是抽象类,不能实例化
// // 如果类 Animal 没有纯虚函数,则可以正常编译,此时 d3 的所有权会被转移给 a4
// unique_ptr<Animal> a4 = std::move(d3);
}
// ###########################################################
// 模板(Template)
// ###########################################################
// template(模板)类似 Java 的泛型,使得同一段代码可以适应不同的数据类型,从而增加代码的复用性和灵活性
// C++ 中的模板(Template)的语法主要有两种形式:
// 一、函数模板
// template <typename T> 定义了一个模板,T 是占位符类型参数,可以代表任何类型
// T funName(T param1, T param2) 中 funName 是函数名,函数接受两个 T 类型的参数 param1 和 param2
// 返回 T 类型的结果
// 这里也可以使用关键字 class 来代替关键字 typename,它们是等价的:template <class T>
template <typename T, typename U> // 这里定义了 2 个类型参数 T 和 U
T my_add(T a, U b) // my_add 函数接收 T 类型的参数 a 和 U 类型的参数 b
{
return a + b; // my_add 函数返回 T 类型的结果
}
// 二、类模板(下面就以类模板 Auto_ptr 为例)
template <typename T> // T 是类型参数
class Auto_ptr // 这里用 template 功能创建智能指针 Auto_ptr 实现在生命周期结束后自动释放
{
T* m_ptr;
public:
// Pass in a pointer to "own" via the constructor
Auto_ptr(T* ptr=nullptr)
{
this->m_ptr = ptr;
}
// Copy constructor to handle ownership transfer
Auto_ptr(Auto_ptr &a)
{
this->m_ptr = a.m_ptr;
a.m_ptr = nullptr;
cout << "ownership transfer (copy constructor)" << endl;
}
Auto_ptr& operator = (Auto_ptr &a)
{
if (&a == this) {
return *this;
}
delete m_ptr;
m_ptr = a.m_ptr;
a.m_ptr = nullptr;
cout << "ownership transfer (assignment)" << endl;
return *this;
}
// The destructor will make sure it gets deallocated
~Auto_ptr()
{
delete m_ptr;
}
// Overload dereference and operator-> so we can use Auto_ptr like m_ptr.
T& operator * () const { return *m_ptr; }
T* operator -> () const { return m_ptr; }
};
// A sample class to prove the above works
class Resource
{
public:
int x = 42;
Resource() { cout << "Resource acquired\n"; }
~Resource() { cout << "Resource destroyed\n"; }
void sayHi() { cout << "Hi " << x << endl; }
};
void pass_by_value(Auto_ptr<Resource> ptr)
{
cout << "pass_by_value" << endl;
}
void smart_ptr_test()
{
cout << "---------------- smart_ptr_test ----------------" << endl;
Auto_ptr<Resource> r1(new Resource());
r1->sayHi();
// Auto_ptr<Resource> r2(r1);
// pass_by_value(r1);
Auto_ptr<Resource> r2;
r2 = r1;
r2->sayHi();
}
int** pointer_of_pointer()
{
int** p = nullptr;
{
int* x = new int(10);
p = &x;
}
cout << "inside function: p = " << p << endl;
cout << "inside function: *p = " << *p << endl;
cout << "inside function: **p = " << **p << endl;
return p;
}
void pointer_of_pointer_test()
{
cout << "---------------- pointer_of_pointer_test ----------------" << endl;
int** p = pointer_of_pointer();
cout << "outside function: p = " << p << endl;
cout << "outside function: *p = " << *p << endl;
cout << "outside function: **p = " << **p << endl; // wrong result
}
// ###########################################################
// 智能指针之 Unique Pointer
// ###########################################################
// 智能指针的“智能”体现在于它们在生命周期结束时自动释放资源,避免手动管理内存带来的问题,提高代码的安全性和可靠性
// 普通指针需要手动释放内存 delete rawPtr; 而智能指针会在不再需要使用时自动释放分配的内存,从而避免了内存泄漏的风险
// 智能指针还会在底层实现一些机制,如引用计数或所有权管理,以确保内存安全性,避免悬垂指针或重复释放内存等问题
int* pass_pointer(int* p)
{
cout << "----- pass_pointer(int* p) -----" << endl;
cout << "inside function: p = " << p << endl;
cout << "inside function: *p = " << *p << endl;
cout << "----- outside pass_pointer -----" << endl;
return p;
}
struct IntOwner
{
public:
unique_ptr<int> p;
~IntOwner()
{
// 符号 ~ 标记的函数是析构函数(Destructor)
// 它一个特殊的成员函数,它在对象生命周期结束时被调用
// 这里是打印输出字符串 "IntOwner destructor"
cout << "IntOwner destructor" << endl;
}
};
unique_ptr<IntOwner> create_int_owner(unique_ptr<int> p)
{
// 这一行代码创建了一个 unique_ptr 智能指针对象 owner
// <IntOwner> 表示 owner 指针所指向的数据类型是 IntOwner
// new IntOwner() 作为参数传入构造类型为 unique_ptr<IntOwner> 的指针
// 所以 owner(new IntOwner()) 是将其初始化为指向 heap 上 new IntOwner() 实例的地址
// 下文的 shared_ptr<int> p4(p3); 也用到类似语法
unique_ptr<IntOwner> owner(new IntOwner());
owner->p = std::move(p);
return owner;
}
void unique_ptr_test()
{
cout << "---------------- unique_ptr_test ----------------" << endl;
// 相当于 Rust 的:let p1 = String::from("Hello");
// Rust 里的变量默认就是 unique 的,因为只能有一个 owner
// C++ 里要实现 Rust 这种 unique 的单 owner 效果就要用 unique_ptr
// 对比普通指针写法:
int* p = new int(10); // 普通指针,类型是 int*
unique_ptr<int> p1 = make_unique<int>(10); // unique 指针,“类型”是 unique_ptr<int>
// 为啥 unique 指针是这种奇怪的写法?
// 因为它不是 C++ 语言本身(编译器)直接实现的,而是用宏(macro)和模版(template)实现的
// 算是一片自定义的用户代码
cout << "unique_ptr: p1 = " << p1 << endl;
cout << "unique_ptr: *p1 = " << *p1 << endl;
// 编译不通过,因为 p1 是 unique 的,不能和 p2 共享数据
// unique_ptr<int> p2 = p1;
// 如果要 p2 取得 p1 的值,就要用 std::move 转移 “ownership”
// Rust 中默认就是 move ,所以可以直接写 let p2 = p1;
// 在这行之后,上面的 p1 变量就不能用了
unique_ptr<int> p2 = std::move(p1); // 这里明显能看到是 move(比 Rust 清晰)
cout << "unique_ptr: p2 = " << p2 << endl;
cout << "unique_ptr: *p2 = " << *p2 << endl;
cout << "unique_ptr: p1 = " << p1 << endl; // nullptr 在 C++ 中 0x0 表示 null
// 解引用 null pointer 会让程序崩溃中断
// cout << "*p1 = " << *p1 << endl; // Error: dereferencing nullptr
// std::move 实现了类似 Rust 这样转移 ownership 的目的
// 只不过变量 p1 仍然存在,它的值变为了 null ,要注意不要再使用它
// Rust 中 p1 会直接用不了
// C++ 里也可以释放 p1 的内存
delete p1.get();
// 读取 p2 的值,类似 Rust 的 borrow :
// let p3 = &p2;
int* p3 = p2.get();
cout << "normal pointer: p3 = " << p3 << endl;
cout << "normal pointer: *p3 = " << *p3 << endl;
// Rust 里是 let p4 = p3; // p4 也是 borrow 的,owner 还是 p2
int* p4 = pass_pointer(p3);
cout << "normal pointer: p4 = " << p4 << endl;
cout << "normal pointer: *p4 = " << *p4 << endl;
{ // 花括号指定了一个 scope(作用域),int_owner 只在这个范围内有效
unique_ptr<IntOwner> int_owner = create_int_owner(std::move(p2));
cout << "unique_ptr: int_owner->p = " << int_owner->p << endl;
cout << "unique_ptr: *int_owner->p = " << *int_owner->p << endl;
}
// 作为 unique pointer 的 int_owner 出了花括号之外就会被释放,内存被回收
cout << "unique_ptr: p2 = " << p2 << endl; // p2 被 move 了,所以成了 null pointer
}
// ###########################################################
// 智能指针之 Shared Pointer
// ###########################################################
void shared_ptr_test()
{
cout << "---------------- shared_ptr_test ----------------" << endl;
// shared pointer 实际上就是 Rust 里面的 Rc
// shared_ptr 的使用语法和 unique_ptr 类似
shared_ptr<int> p1 = make_shared<int>(10); // 在 heap 里造出 shared pointer
cout << "shared_ptr: p1 = " << p1 << endl; // 指针 p1 指向(存放) heap 里的地址
cout << "shared_ptr: *p1 = " << *p1 << endl;
// 调用 .use_count() 可以查看引用计数
cout << "in scope shared_ptr: p1.use_count() = " << p1.use_count() << endl;
{
// C++ 的赋值依靠一个 copy constructor
// 由于 p1 是 shared pointer ,所以它有一个引用计数在这
// 当这里做这个赋值操作的时候,copy constructor 会增加这个引用计数
// 思考:为何不用其他比如 int& y = x; 的方式来共享,而要用看起来更复杂的 shared_ptr ?
shared_ptr<int> p2 = p1;
cout << "shared_ptr: p2 = " << p2 << endl;
cout << "shared_ptr: *p2 = " << *p2 << endl;
cout << "shared_ptr: p1 = " << p1 << endl;
cout << "shared_ptr: *p1 = " << *p1 << endl;
// 查看引用计数
cout << "in scope shared_ptr: p1.use_count() = " << p1.use_count() << endl;
cout << "in scope shared_ptr: p2.use_count() = " << p2.use_count() << endl;
}
// 出了花括号,p2 会被释放掉,引用计数再次减小为 1
cout << "out scope shared_ptr: p1.use_count() = " << p1.use_count() << endl;
// convert normal pointer to shared_ptr
int* p3 = new int(20);
int* p5w;
{
// p4(p3) 是一种初始化 shared_ptr 的语法
// 通过将 p3 作为参数传递给 p4 的构造函数,p4 将获取对这个动态分配的整数对象的所有权
// 并在 p4 生命周期结束时,会自动释放这个对象
shared_ptr<int> p4(p3); // 把普通指针转换为 Rc 的指针,但这样做不是对的
cout << "shared_ptr: p4 = " << p4 << endl;
cout << "shared_ptr: *p4 = " << *p4 << endl;
cout << "shared_ptr: p4.use_count() = " << p4.use_count() << endl;
// // Can't have two of them converted from raw pointer
// // 上面从 p3 造了一个 shared pointer 指针 p4
// // 这里又从 p3 造了一个 shared pointer 指针 p4w
// // p4 和 p4w 都认为自己的引用计数是 1,这就有问题了:
// // 当 p4 出了作用域之后,由于引用计数是 1 ,所以它会回收一次这个内存
// // 等到 p4w 出了作用域之后,由于引用计数也是 1 ,于是它会再回收一次
// // 然后 p4w 发现没有内存可以回收,于是程序就崩溃了
// shared_ptr<int> p4w(p3);
// cout << "shared_ptr: p4 = " << p4w << endl;
// cout << "shared_ptr: *p4 = " << *p4w << endl;
// cout << "shared_ptr: p4.use_count() = " << p4w.use_count() << endl;
// // 所以在 C++ 里面,永远不应该从一个普通的 pointer 造出一个 shared pointer
// // 就像在 Rust 里面你不能把一个 borrow 的引用变成一个 owned 的
// // 比如下面的 Rust 代码就是这样的表达,无法通过编译:
// // cannot move out of `*x` which is behind a shared reference
// let s = String::from("hello");
// let x = &s;
// let y = *x;
// // C++ 的这些智能指针不是语言自己实现的,所以它没法阻止你这样写,所以程序会崩溃
// // 用 make_shared 来创建 shared pointer 就能避免上述问题,因为会编译不过
// convert shared_ptr to normal pointer
// int* p5 = p4.get(); 这个操作相当于 borrow
// 你可以读取 p4 ,但不能释放 p4 ,你也不能在 p4 出了作用域之外还使用 p5
int* p5 = p4.get();
p5w = p5;
cout << "normal pointer: p5 = " << p5 << endl;
cout << "normal pointer: *p5 = " << *p5 << endl;
}
// // p5w 指针的值(地址)还是 p5 的值,但是内容已经变了
// // p4 除了作用域被释放了,所以内存的内容变了,所以 *p5w 的值是错的
// cout << "p5w = " << p5w << endl;
// cout << "*p5w = " << *p5w << endl;
// // 出了作用域之后,p5w 竟然还能用,说明 C++ 内存的安全保障没有 Rust 那么强
}
void shared_ptr_bug()
{
// 和上文的 p4w 一样,不应该把普通指针转为 shared pointer
// C++ 里,永远不应该从一个普通的 pointer 造出一个 shared pointer
int* x = new int(10);
shared_ptr<int> p1(x); // 能编译通过,但埋下隐患
// shared_ptr<int> p2(x); // can't have this
// This can be avoided by using make_shared
// 使用 make_shared 来创造 shared pointer 可以让编译器检查出此类错误
// shared_ptr<int> p1 = make_shared<int>(x); // 编译不通过
}
void add1(int* x)
{
*x += 1;
}
void shared_ptr_mutation()
{
cout << "---------------- shared_ptr_mutation ----------------" << endl;
// 你可以对 pointer 指向的对象进行修改,这和 Rust 有点不同
// Rust 的 Rc 你是不能修改它的值的,你必须要在 Rc 里套一个 RefCell 才能改它的值
// C++ 没有这类读写锁来防止多个人同时修改同一个内存上的数据
// C++ 只管这些内存到最后被释放掉,而且只能释放一次
// 至于多线程的时候,你要怎么处理这些读写冲突问题,那就是你自己的事情,C++ 不管
shared_ptr<int> p = make_shared<int>(10);
cout << "shared_ptr: p = " << p << endl;
cout << "shared_ptr: *p = " << *p << endl;
// Can use function that takes normal pointer
// and can mutate the value
add1(p.get());
cout << "shared_ptr (after add1): *p = " << *p << endl;
*p += 1;
cout << "shared_ptr (after +=1): *p = " << *p << endl;
}
class Example
{
public:
void sayHello() { cout << "Hello, world!\n"; }
};
void misuseSharedPtr()
{
Example example; // example 是 stack 上的对象,不是指针
// std::shared_ptr<Example> ptr(&example); // 错误用法!
// shared_ptr<Example> ptr(&example, [](Example*){}); // OK
// make_shared<Example>(example) 在 heap 上复制了一份 example 的数据,所以看起来没问题
// 但是一般也不要这么写
shared_ptr<Example> ptr = make_shared<Example>(example); // seems OK, used copy constructor?
ptr->sayHello();
}
class Example2
{
public:
shared_ptr<int> data;
Example2(shared_ptr<int> data) : data(data)
{
cout << "Example2 created with value: " << *data << endl;
}
// 使用默认的拷贝构造函数:
// 这个构造函数的作用是用于创建一个新的 Example2 对象
// 其成员变量 data 的值与另一个 Example2 对象相同
// 使用 = default 来声明拷贝构造函数,表示使用编译器生成的默认实现
// 这种语法通常用于避免手动实现拷贝构造函数,特别是当类中没有需要手动管理的资源时。
Example2(const Example2& other) = default;
~Example2()
{
cout << "Example2 destroyed, value was: " << *data << endl;
}
};
void shared_ptr_test2()
{
cout << "---------------- shared_ptr_test2 ----------------" << endl;
shared_ptr<int> data = make_shared<int>(42);
Example2 a(data);
Example2 b = a; // 拷贝构造
// 修改 b 的 data,a 的 data 也会看到改变
*b.data = 100;
cout << "a's value: " << *a.data << endl; // 输出 100
cout << "b's value: " << *b.data << endl; // 输出 100
cout << "data's value: " << *data << endl; // 输出 100
}
// ###########################################################
// 其他代码行为测试
// ###########################################################
void ref_of_ref()
{
cout << "---------------- ref_of_ref ----------------" << endl;
unique_ptr<string> s = make_unique<string>("Hello");
string* r1 = s.get(); // 把 unique_ptr 的指针拿出来
string** r2 = &r1;
cout << "s = " << s << endl;
cout << "*s = " << *s << endl;
cout << "r1 = " << r1 << endl;
cout << "*r1 = " << *r1 << endl;
cout << "r2 = " << r2 << endl;
cout << "*r2 = " << *r2 << endl;
cout << "**r2 = " << **r2 << endl;
}
void pass_unique_ptr(unique_ptr<int> p)
{
cout << "pass_unique_ptr: p = " << p << endl;
cout << "pass_unique_ptr: *p = " << *p << endl;
}
void pass_unique_ptr_test()
{
cout << "---------------- pass_unique_ptr_test ----------------" << endl;
unique_ptr<int> p = make_unique<int>(10);
cout << "unique_ptr: p = " << p << endl;
cout << "unique_ptr: *p = " << *p << endl;
pass_unique_ptr(std::move(p));
cout << "unique_ptr: p = " << p << endl; // nullptr
// cout << "unique_ptr: *p = " << *p << endl; // Error: dereferencing nullptr
}
unique_ptr<int> output_unique_ptr()
{
unique_ptr<int> p = make_unique<int>(10);
return p;
}
void output_unique_ptr_test()
{
cout << "---------------- output_unique_ptr_test ----------------" << endl;
unique_ptr<int> p = output_unique_ptr();
cout << "output_unique_ptr_test: p = " << p << endl;
cout << "output_unique_ptr_test: *p = " << *p << endl;
}
unique_ptr<int> pass_through_unique_ptr(unique_ptr<int> p)
{
return p;
}
void pass_through_unique_ptr_test()
{
cout << "---------------- pass_through_unique_ptr_test ----------------" << endl;
unique_ptr<int> p = make_unique<int>(10);
cout << "pass_through_unique_ptr_test: p = " << p << endl;
cout << "pass_through_unique_ptr_test: *p = " << *p << endl;
unique_ptr<int> p2 = pass_through_unique_ptr(std::move(p));
cout << "pass_through_unique_ptr_test: p2 = " << p2 << endl;
cout << "pass_through_unique_ptr_test: *p2 = " << *p2 << endl;
cout << "pass_through_unique_ptr_test: p = " << p << endl; // nullptr
// cout << "pass_through_unique_ptr_test: *p = " << *p << endl; // Error: dereferencing nullptr
}
template <typename T>
T pass_through(const T& x)
{
return x;
}
void pass_through_test()
{
cout << "---------------- pass_through_test ----------------" << endl;
int x = 10;
int y = pass_through(x);
cout << "pass_through_test: y = " << y << endl;
string s = "Hello";
string t = pass_through(s);
cout << "pass_through_test: t = " << t << endl;
unique_ptr<int> p = make_unique<int>(10);
// unique_ptr<int> q = pass_through(p); // doesn't work
// cout << "pass_through_test: q = " << q << endl;
}
class IntContainer
{
public:
int* x;
IntContainer(int* x) : x(x) {}
~IntContainer()
{
cout << "IntContainer destructor: x = " << x << endl;
}
};
class RawContainer
{
public:
IntContainer* ic;
RawContainer(IntContainer* ic) : ic(ic) {}
~RawContainer()
{
cout << "RawContainer destructor: ic = " << ic << endl;
}
};
void raw_container_test()
{
cout << "---------------- raw_container_test ----------------" << endl;
unique_ptr<int> int_unique = make_unique<int>(10);
int* int_raw = int_unique.get(); // 读取出 int_unique 的指针放到 int_raw 里 - 相当于 Rust 里的 borrow
// 造出一个 IntContainer 对象,然后把 int_raw 包在里面,然后各种嵌套
// 下面的代码显示这里的内存分配和释放没有问题 - 通过各种 ~ 标记的 destructor 打印确认对象被回收释放
unique_ptr<IntContainer> int_container_unique = make_unique<IntContainer>(int_raw);
IntContainer* int_container_raw = int_container_unique.get();
unique_ptr<RawContainer> raw_container = make_unique<RawContainer>(int_container_raw);
cout << "raw_container_test: int_unique = " << int_unique << endl;
cout << "raw_container_test: int_raw = " << int_raw << endl;
cout << "raw_container_test: int_container_unique = " << int_container_unique << endl;
cout << "raw_container_test: *int_container_unique->x = " << *int_container_unique->x << endl;
cout << "raw_container_test: int_container_raw = " << int_container_raw << endl;
cout << "raw_container_test: raw_container = " << raw_container << endl;
}
class UniqueContainer
{
public:
unique_ptr<IntContainer> ic;
// must pass reference _and_ move because no copy constructor for unique_ptr
UniqueContainer(unique_ptr<IntContainer>& ic) : ic(std::move(ic)) {}
~UniqueContainer()
{
cout << "UniqueContainer destructor: ic = " << ic << endl;
}
};
void unique_container_test()
{
// 这个例子和上面的 raw_container_test 类似,只不过两个对象里面放的是 unique pointer
// 在 C++ 中,函数调用的参数传递会调用 copy structor ,但是 unique pointer 是没有 copy structor 的
// 所以期间会需要 & 符号和 std::move 操作
// 最后通过 destructor 来观察判断内存被正确回收释放
cout << "---------------- unqiue_container_test ----------------" << endl;
unique_ptr<int> int_unique = make_unique<int>(10);
int* int_raw = int_unique.get();
unique_ptr<IntContainer> int_container_unique = make_unique<IntContainer>(int_raw);
// unique_ptr<IntContainer> int_container_unique2 = std::move(int_container_unique);
unique_ptr<UniqueContainer> unique_container = make_unique<UniqueContainer>(int_container_unique);
// unique_ptr<UniqueContainer> unique_container = make_unique<UniqueContainer>(std::move(int_container_unique));
// After move, int_container_unique is nullptr
cout << "int_unique owns = " << (int_unique ? "Yes" : "No") << ", value = " << *int_unique << endl;
cout << "int_raw points to = " << int_raw << endl;
cout << "int_container_unique has been moved = " << (int_container_unique ? "No, not null" : "Yes, nullptr") << endl;
if (unique_container && unique_container->ic)
{
cout << "unique_container->ic->x points to = " << unique_container->ic->x << endl;
cout << "*unique_container->ic->x = " << *unique_container->ic->x << endl;
}
cout << "unique_container owns = " << (unique_container ? "Yes" : "No") << endl;
}
int main() // C++ 中规定 main 函数一定要返回整数,所以是 int main()
{
// basic_test();
// pointer_test();
// address_test();
// reference_test();
// pass_array_test();
// inheritance_test();
// smart_ptr_test();
// pointer_of_pointer_test();
// unique_ptr_test();
// shared_ptr_test();
// shared_ptr_bug();
// misuseSharedPtr();
// shared_ptr_test2();
// shared_ptr_mutation();
// ref_of_ref();
// pass_unique_ptr_test();
// output_unique_ptr_test();
// pass_through_unique_ptr_test();
// pass_through_test();
// raw_container_test();
// unique_container_test();
return 0;
}打赏