C++ 语言入门

    Programming Language

在理解 Rust 内存管理的本质之后(参见 Rust 入门 ),再来了解一下 C++ 语言和它的内存管理。这样和 Rust 对比一下,就能更加理解 Rust 内存管理方式和特点。进而更好地使用 C++ 进行编程。

如果从具有垃圾回收(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 命令行工具里,安装非常方便。

## 安装 Command Line Tools - MacOS 编程软件 Xcode 的命令行工具
$ xcode-select --install

## 完成后可以使用一下命令查看版本说明安装成功了
$ clang --version
Apple clang version 14.0.0 (clang-1400.0.29.202)
Target: arm64-apple-darwin21.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

安装完 Clang 之后,在 VS Code 中新建后缀为 .cpp 的文件比如 demo.cpp 就能开始编程了:

#include <iostream>

// C++ 的 main 函数一定要返回 int
int main()
{
  std::cout << "Hello C++" ;
  return 0;
}

关于 VS Code 的安装、设置和使用可以参考上手 Visual Studio Code

一些方便的设置:

  • 设置花括号另起一行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>
#include <string>
#include <memory>
#include <vector>
#include <map>
#include <stdexcept>
#include <cmath> // <cmath> 库包含开平方的函数 sqrt

// 如果没有 using namespace std; 那 cout 和 endl 等就都要加上 std::
// 这里我们只有一个 cout ,所以可以不用担心可能导致的命名冲突
using namespace std;

// C++ 不允许你在函数内部定义函数,编译不通过,所以 fact 定义到外面
// 尽管有编译过程,但是函数要定义在调用的前面才能被识别,否则就会报错 undeclared
// fact 定义在 basic_test 之前,basic_test 内部才能调用它
int fact(int n)
{
  if (n == 0)
  {
    return 1;
  }
  else
  {
    return n * fact(n - 1);
  }
}

// int sqrt(int n) {
//   return n * n;
// }

// ###########################################################
//                            基本语法
// ###########################################################

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;

  e.append(" World!");
  cout << "e = " << e << endl;

  cout << sqrt(25) << endl; // sqrt function is defined in the cmath header file.

  cout << fact(5) << endl;

  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++)
  {
    cout << "for: " + arr[i] << endl;
  }

  arr[3] = "Evening";
  for (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 User // struct 可以在函数体里定义
  {
    string name;
    int age;

    // 如果 struct 定义函数的话,这个函数是单独存放的
    // 不算 struct 里的 field
    // 比如你这里定义了一个 printUser() 函数,然后 unique_ptr<User> u1
    // 然后 move 它:unique_ptr<User> u2 move(u1)
    // 按理说此时 u1 被清零了(地址为 0),但 u1.printUser() 仍然能调用
    // 除了 struct 外 class 的行为也是如此
    // 思考:struct 和 class 有什么区别?哪些情况下时候分别用哪个?
    // class 中,默认的成员访问级别是 private,而在 struct 中,默认的成员访问级别是 public;
    // class 常用于定义复杂行为和数据封装的对象,如包含多种类方法;而 struct 则用于轻量级的数据结构,如坐标;

  }; // 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;

  // 直接定义的整数 10 被 C++ 放在栈上 - 和 Java 不同的是,C++ 的「堆」和「栈」分得很清楚的
  // x 的数据 2 在 stack 上
  int x = 2;
  cout << "x = " << x << endl; // 直接可以打印出 x 的值 2 
  cout << "&x = " << &x << endl; // 打印 x 的地址:符号 & 代表取得该变量的地址

  // 这个定义,数据 int(10) 在 heap 上,指针变量 p 则在 stack 上
  // int* 的这个 * 号是 int* 这个类型的一部分,下面的 *p 的 * 号则是解引用操作符,两者不一样
  // Rust 的 * 只是用来 move 改变 ownership 的,虽然也叫 dereference 但和这的 *p 本质不一样
  // C++ 里,看到关键词 new 就要知道它肯定在 heap(堆)上了,因为 new 就是在 heap 上分配内存的操作
  int* p = new int(10);

  // 直接打印 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;

  int*** r = &q;
  cout << "r = " << r << endl;
  cout << "*r = " << *r << endl;
  cout << "**r = " << **r << endl;
  cout << "***r = " << ***r << endl;

  int**** s = &r;
  cout << "s = " << s << endl;

  // int arr[5] = {3, 5, 7, 9, 11};

  // 这里 p2 的类型为什么是 int* 而不是 int[]* ?为什么是 int 的指针类型而不是数组的指针类型?
  // 因为数组里所有元素是挨在一起相邻储存的,故知道了第一个元素的地址,就知道整个数组的所有元素的地址
  // p2 会指向向数组的第一个数据:3 ,即 *p2 = 3
  int* p2 = new int[5] {3, 5, 7, 9, 11};  // 注意,C++11 之前的标准不支持直接使用列表初始化数组
  cout << "p2 = " << p2 << endl;
  cout << "*p2 = " << *p2 << endl;
  cout << "p2 + 1 = " << p2 + 1 << endl;       // 数组第二元素 5 的地址
  cout << "*(p2 + 1) = " << *(p2 + 1) << endl; // 数组第二元素的值,即 5

  int** q2 = &p2;
  cout << "q2 = " << q2 << endl;
}

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
}


// ###########################################################
//                            传递引用
// ###########################################################

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 赋值 20 ,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;


  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++ 特有的
}


// ###########################################################
//                            数组
// ###########################################################

// C++ 的这个类型语法是比较诡异的,数组的类型要写成 int arr[] ,应该是历史遗留问题
// Java 中的数组就比较有规律 int[] arr ,类型是 int[] ,变量名是 arr
// 且这里数组传递的是 reference ,但又不像 pass_by_reference 那样写成 int& arr[]
void pass_array(int arr[])  
{
  for (int i = 0; i < 5; i++) 
  {
    cout << "pass_array: " << arr[i] << endl;
  }
  arr[0] = 100;
}


void pass_array_test()
{
  cout << "---------------- pass_array_test ----------------" << endl;

  // 在栈(stack)上定义一个 array
  int arr[] = {1, 2, 3, 4, 5}; // stack 上

  for (int i = 0; i < sizeof(arr)/sizeof(int); i++) 
  {
    cout << "before pass_array: " << arr[i] << endl;
  }

  pass_array(arr);

  // a[0] 的值在函数 pass_array 中被改变了,说明数组传递进去的时候不是复制一份
  // 而是 arr[] 本身直接进去(很上面的 pass_by_reference 行为相同)
  for (int i = 0; i < sizeof(arr)/sizeof(int); i++) 
  {
    cout << "after pass_array: " << arr[i] << endl;
  }
}


// ###########################################################
//                            面向对象
// ###########################################################

class Animal
{
  // 外部能用的成员和方法都写到 public: 后面
  // 不像 Java 那样每个都要写一次 public
  public:
  // virtual 类似 Java 的 abstract class
  // 强制继承的子类定义 speak 方法
  // C++ 中得写成这中奇怪的样子
  // 如果不加 virtual ,那子类就无法 override 这个 speak 方法了
  // 如果不写 = 0 ,就无法强制子类定义 speak 函数
  virtual void speak() = 0;  
};

// 要在 Dog 里能访问到 Animal 里的成员,就要写上 public
class Dog: public Animal
{
  public:
  // 重载 speak 方法,更严谨的做法是加上 override 
  // 类似 Java 的 @Override 标记
  void speak() override
  {
    cout << "汪!" << endl;
  }
};

class Cat: public Animal
{
  public:
  // override 不写也行,但写的话编译器会检查父类的内容
  // 如果父类没有 speak ,那就会编译不通过(要重载但父类却没有)
  void speak()
  {
    cout << "喵!" << endl;
  }
};

void inheritance_test()
{
  cout << "---------------- inheritance_test ----------------" << endl;

  // 栈(stack)上面定义了狗 d 和猫 c
  // Dog 和 Cat 的实例(instance)在 stack 上
  Dog d; // stack
  Cat c; // stack
  int i = 9;  // stack

  // 定义在 heap 上的 Dog
  Dog* d2 = new Dog();  // heap allocation

  // 不能这么写,因为 new Dog() 在堆上,一定要个指针来指向它,而不能用普通的变量来存放它
  // 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();

  (*d2).speak(); // d2 是一个指针,先解引用得到指向的 Dog 实例,再调用 speak 方法
  d2->speak();  // C++ 语法里还可以用箭头来访问指针指向的对象里的成员(方法)

  // 综上,同一个 Class 可以有 2 种不同的分配方法:栈(stack)上,或者堆(heap)上

  // 用一个 Animal 指针 a1 指向 d
  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* 。其实 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();
}

// ###########################################################
//                        模板(Template)
// ###########################################################

// template 模板类似 Java 的泛型,使得同一段代码可以适应不同的数据类型,从而增加代码的复用性和灵活性

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

打赏