【C++要笑着学】友元 | 初始化列表 | 关键字explicit | 静态成员static | 内部类(一)

简介: 我是柠檬叶子C。上一章我们一步步地实现了日期类,这一章我们继续往后讲解知识点,比如说友元啊,初始化列表啊、静态成员和内部类,把这些拿出来讲一讲。还是保持最近养成的写作习惯,在讲解知识点之前,我都会用一个例子或问题进行引入,做到"循序渐进" 地讲解。

前言


我是柠檬叶子C。上一章我们一步步地实现了日期类,这一章我们继续往后讲解知识点,比如说友元啊,初始化列表啊、静态成员和内部类,把这些拿出来讲一讲。还是保持最近养成的写作习惯,在讲解知识点之前,我都会用一个例子或问题进行引入,做到"循序渐进" 地讲解。


如果觉得文章不错,可以 "一键三连" 支持一下博主!你们的关注就是我更新的最大动力!


Ⅰ.  友元(friend)


0x00 引入 - 日期类的流提取

观察下面这个日期类,我们是调用 Print 成员函数来打印的:


#include <iostream>
using namespace std;
class Date {
public:
  Date(int year, int month, int day) {
  _year = year;
  _month = month;
  _day = day;
  }
  void Print() const {
  printf("%d-%d-%d\n", _year, _month, _day);
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Date d1(2022, 3, 20);
  d1.Print();
  return 0;
}

c8b9caf3723ca66303187ca0c806c006_8adb823415be4a7485e7843f4e78673c.png

❓ 我们此时思考一个问题,我们能不能这样输出一下 d1 呢?


cout << d1;

7ede280808755a895966fe0f274193ee_6a466431388340cd91ac53461a961dcf.png


这样当然是不行的,主要的原因还是这个是一个操作符。


是C++里面的 流插入 ,这里的意思就是要像流里面插入一个 d1。


我们说过,内置类型是支持运算符的,而自定义类型是不支持的,


它是不知道该怎么输出的,输入也是一样的道理,也是不知道该怎么去输入。

cin >> d1;  ❌

那怎样才能向我们内置类型一样去用 流插入 和 流提取 呢?


依然可以使用重载这个运算符的方法来解决!


🔍 我们先来看一下文档:cplusplus.com - The C++ Resources Network


cout 其实是一个全局类型的对象,这个对象的类型是 ostream 。

95ffaa7a63a51b6073599b8b88e12315_2ed3642cbccd49d6bd6f074399be76a9.png



说个题外话,内置类型之所以能直接支持你用,是因为 ostream 已经帮你写好了。

1c75bb7e4b326e99a45a7c3c295fa964_b88a4d173316414bb22d3de21a2e8646.png

所谓的 "自动识别类型" ,不过只是函数重载而已……


你是 int 它就匹配 int ,你是 char 它就匹配 char 。


我们现在知道了, cout 是一个 ostream 类型的对象了,我们来重载一下。


💬 Date.h


class Date {
public:
  Date(int year, int month, int day) {
  _year = year;
  _month = month;
  _day = day;
  }
  void Print() const {
  printf("%d-%d-%d\n", _year, _month, _day);
  }
  void operator<<(ostream& out);
private:
  int _year;
  int _month;
  int _day;
};


💬 Date.cpp


void Date::operator<<(ostream& out) {
  out << _year << "/" << _month << "/" << _day << endl;
}

我们是想输出年月日的,我们这里就可以自己控制格式来输出了。


💬 test.cpp

83116f254bfb79814cdb5acd63a77f9f_6788d1801faa46ad9a8fe0f2a960da23.png


这时我们发现  cout << d1 还是识别不了,调不动。


这里不识别的原因是因为它是按参数走的,第一个参数是左操作数,第二个参数是右操作数。


双操作数的运算符重载时,规定第一个参数是左操作数,第二个参数是右操作数。


我们这里是成员函数…… 那第一个参数是……


void operator<<(ostream& out);  成员函数,默认第一个参数是隐含的this

所以,我们在调用这个流插入重载时就需要:


d1.operator<<(cout);

因为这种原因,我们要直接写就会成这样:


#include "Date.h"
int main(void)
{
  Date d1(2022, 3, 20);
  // cout << d1;  ❌ 不识别,调不动
  d1 << cout;  // d1.operator<<(cout);
  return 0;
}

0fc345cc3a4008e901acccacaad3e42a_d3ae5a53e2f540cabc58880e2e3b23e4.png

可以打印出来了。可以是可以,但是这样看起来就变扭了:


d1 << cout;

什么鬼?流倒灌??!


这不符合我们对 "流" 的理解,我们正常理解流插入,是对象流到 cout 里面去。


你现在是流插入到对象里面去。不像是 "流插入" 了,听上去更像是 "流倒灌"  ……


实现成这样,确实可以调用,但是这样用起来也太奇怪了,也不符合我们的使用习惯。


cout << d1;    ✅ 这才是我们习惯的用法!如何实现?

因为被隐含的 this 指针参数给占据了,所以就一定会是左操作数,


这时如果写成成员函数,双操作数的左操作数一定是对象。


……


基于这样的原因,我们如果还是想让 cout 到左边去,就不能把他重载成成员函数了。


可以直接把它重载成全局的,在类外面,不是成员函数了就没有这些隐含的东西了!


这样的话就可以让第一个参数变为左操作数,即 ——


💬 out 在第一个位置,Date& d 在第二个位置:


void operator<<(ostream& out, const Date& d) {
  out << d._year << "/" << d._month << "/" << d._day << endl;
}

这个时候调用是肯定能调的动了,调的是全局函数。


但我们现在面临的问题是,不能访问私有的问题。

3191ba9620b39b97dff59df4094bcbc2_0437e2789c3a4794b2baead24061459e.png

❓ 不能访问私有的问题改如何解决?把 private 改为 public ?


这种方式肯定是不好的,当然我们可以写个 getYear getMonth getDay 去获取它们。


这样也可以,但是输入的时候怎么办?我们再实现 cin 流体去的时候是要 "写" 的。


这时候就麻烦了,你还得写一个 set,属实是麻烦,有没有更好地办法可以解决这种问题呢?


有!C++ 引入了一个东西叫做 —— 友元。


0x01 友元的概念

friend

一个全局函数想用对象去访问 private 或者 public ,就可以用友元来解决。


友元分为 友元函数 和 友元类 。


比如刚才我们想访问 Date 类,我就可以把它定义为 友元函数 ,友元的声明要放到类里面。


友元会破坏封装,能不用就不用!友元就像是黄牛,破坏了管理规则。


0x02 友元函数

📚 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数。


它不属于任何类,但需要在类的内部进行声明,声明时要加 friend 关键字。


我们现在就可以去解决刚才的问题了:


💬 Date.h


class Date {
public:
  friend void operator<<(ostream& out, const Date& d);  // 友元的声明
  //...
private:
  int _year;
  int _month;
  int _day;
};
💬 Date.cpp
void operator<<(ostream& out, const Date& d) {
  out << d._year << "/" << d._month << "/" << d._day << endl;
}

9fae44905da22d6dd6eaa1f8e51babde_bbdc94c9ef324885a42d22ca68a6508a.png

💬 test.cpp


int main(void)
{
  Date d1(2022, 3, 20);
  cout << d1;
  return 0;
}

fe2701d2c9572bcb9cba0fbba5dea791_a1cde79151c84121937efc0f02ae4f0c.png

我们终于可以 cout << d1 打印日期了。


❓ 如果我们想连续地输出呢?我想在这又输出 d1 又输出 d2。


cout << d1 << d2;

现在实现的不支持。这和连续赋值很像,只是连续赋值是从右往左,这里是从左往右。

323541064a18e2460a848ae0cdac7358_c1b2981b5e0c43a093e806a0824a28ba.png


连续插入 d1 和 d2 实际上就是两次函数的调用,这里先执行的是 cout << d1,


因为调用函数后返回值是 void,void 会做这里的左操作数,


所以当然不支持连续输出了,我们可以改一下,


我们把返回值改为 ostream 就行,把 out 返回回去。


💬 Date.h


class Date {
public:
    // ...
  friend ostream& operator<<(ostream& out, const Date& d);    // 友元的声明
private:
  int _year;
  int _month;
  int _day;
};

💬 Date.cpp


#include "Date.h"
ostream& operator<<(ostream& out, const Date& d) {
  out << d._year << "/" << d._month << "/" << d._day << endl;
  return out;
}

💬 test.cpp


int main(void)
{
  Date d1(2022, 3, 20);
  Date d2(2021, 5, 1);
  cout << d1 << d2;
  return 0;
}

fe8475ea6d1f100849dea8aa7c646ccb_7dad9bab61d34e569557a26e82ac477e.png


解决了流插入,我们再来顺便实现一下流提取。


这样我们上一章实现的日期类,基本上就完整了。

433e9c6fb57e9d8aabfec457308f4317_cfc459b240034e33a51cef785622a566.png



查完文档我们可以知道,该对象的类型是 istream 。


💬 Date.h


class Date {
public:
    // ...
  friend ostream& operator<<(ostream& out, const Date& d);
  friend istream& operator>>(istream& in, Date& d);
private:
  int _year;
  int _month;
  int _day;
};

流提取因为要把输入的东西写到对象里去,会改变,所以这里当然不能加 const 。


💬 Date.cpp

#include "Date.h"
istream& operator>>(istream& in, Date& d) {
  in >> d._year >> d._month >> d._day;
  return in;
}

💬 test.cpp


int main(void)
{
  Date d1(2022, 3, 20);
  Date d2(2021, 5, 1);
  printf("请输入两个日期:\n");
  cin >> d1 >> d2;
  printf("你输入的日期是:\n");
  cout << d1 << d2;
  return 0;
}

46edcf95bcbba956b908ef3bec8dd79b_1dd8fa746d09463282afef5b1581448d.png

📌 注意事项:


① 友元函数可以访问类的 private 和 protected 成员,但并不代表能访问类的成员函数。


② 友元函数不能用 const 修饰。


③ 友元函数可以在类定义的任何地方申明,可以不受类访问限定符的控制。


④ 一个函数可以是多个类的友元函数。


⑤ 友元函数的调用和普通函数的调用原理相同。


0x03 友元类

友元类的所有成员函数都可以是另一个类的友元函数,


都可以访问另一个类中的非公有成员。


friend class 类名;

① 友元关系是单向的,不具有交换性。


② 友元关系不具有传递性(朋友的朋友不一定是朋友)。


   如果 B 是 A 的友元,C 是 B 的友元,则不能说明 C 是 A 的友元。


💬 定义一个友元类:


class Date;   // 前置声明
class Time {
  friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
  Time(int hour = 0, int minute = 0, int second = 0)
  : _hour(hour)
  , _minute(minute)
  , _second(second)
  {}
private:
  int _hour;
  int _minute;
  int _second;
};
class Date {
public:
  Date(int year = 1900, int month = 1, int day = 1)
  : _year(year)
  , _month(month)
  , _day(day)
  {}
  void SetTimeOfDate(int hour, int minute, int second)
  {
  // 直接访问Time类私有的成员变量
  _t._hour = hour;
  _t._minute = minute;
  _t._second = second;
  }
private:
  int _year;
  int _month;
  int _day;
  Time _t;
};


这里 Date 是 Time 的友元,我们在日期类里就可以访问时间类的私有成员了。


但是时间类里不能访问日期类,因为这是 "单向好友" ,


如果想在时间类里访问日期类,我们可以在日期类里声明:


class Date {
    friend class Time;
    // ...
}

这样,它们之间就是 "双向好友" 了 —— 互相成为对方的友元。



Ⅱ.  初始化列表


0x00 引入 - 难缠的初始化问题

我们知道,常量必须在定义时初始化。

3f2d0a380adf3e2cfc3e2e2d493cba15_d451e64eec2d4720a8243c6eab943720.png

const int j;       ❌
const int j = 0;   ✅

在定义的时候就要初始化,不初始化就会出问题,

因为常量只有一次初始化的机会,就是在定义的时候。

我们现在再来思考这个问题:


class Date {
public:
  Date(int year, int month, int day) {
  this->_year = year;
  this->_month = month;
  this->_day = day;
  }
private:
    /* 声明部分 */
  int _year;
  int _month;   
  int _day;
  const int _N;
};

❓ 如果没有初始化列表,那常量 _N 该在哪里初始化呢?


private:
    /* 声明部分 */
  int _year;
  int _month;   
  int _day;
  const int _N = 10;   // 这一块是声明,我们不应该在这初始化。
};

初始化要在空间上给值,你这里有空间吗?你没空间啊!

eb94012c3e72e2b32a27de0189d47fe0_e034679c6883448b9e37c77d412e485c.png



❓ 这也不行,那也不行,那我们该如何初始化这个烦人的 _N 呢?


基于这种原因,C++ 就搞出了一个叫做初始化列表的东西。


0x01 概念及使用方法

我们之前学习创建对象时,编译器通过调用构造函数,给对象赋初值。


class Date {
public:
  Date(int year, int month, int day) {
  this->_year = year;
  this->_month = month;
  this->_day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};

上面的构造函数调用后,对象中已经有了一个初始值,


但是不能将其作为类对象成员进行初始化,构造函数体中的语句只能将其作为 "赋初值"

而不能称作是 "初始化" 。 因为初始化只能初始化一次, 而构造函数体内可以多次赋值。


所以我们现在来学习一种 "初始化" 的方式


初始化列表 —— 成员变量定义的地方。

eead79ee339ca3b8f5e8f1987e1e5568_bc93c84be9fd470da6294b29ba38bc4d.png



📚 初始化列表:以一个冒号开始,逗号间隔的数据成员列表。每个成员变量后面跟一个放在括号中的初始值或表达式。


💬 代码演示:


class Date {
public:
  Date(int year, int month, int day)
  : _year(year)
  , _month(month)
  , _day(day) 
  {
  ;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Date d1(2022, 3, 20);
  return 0;
}


🐞 浅调一下吧 ~

da4911f77f16c4d044bf29ee83fcb491_1715db91249e4cb396680c452ab86ec1.png



现在,我们再来看刚才的问题,如何初始化 _N


① 全部用初始化列表初始化


class Date {
public:
  Date(int year, int month, int day)
  : _year(year)
  , _month(month)
  , _day(day)
  , _N(10)
  {
  ;
  }
private:
  int _year;
  int _month;
  int _day;
  const int _N;
};


初始化列表是这些成员变量定义的地方,这里就相当于在定义的时候就初始化了。


② 只处理 _N


class Date {
public:
  Date(int year, int month, int day)
  : _N(10)   // 只处理_N ,这样也是可以的。
  {
  _year = year;
  _month = month;
  _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
  const int _N;
};

这种情况就是部分成员变量(年月日)在定义的时候不初始化,在函数体内初始化。


所以,并不是必须要用初始化列表初始化,也不是必须用函数体内初始化,


而是你可以灵活地控制,C++ 这里并没有规定必须要怎么怎么样。


0x02 使用时注意事项

① 每个成员变量再初始化列表中只能出现一次,即 初始化只能初始化一次。

6c74e981e079ab873f47cfd0a51eecbc_e5bbe2e12a9e474a8b8406a2d8ba4f7c.png

② 必须在定义时就初始化的成员变量,要在初始化列表初始化。


类中包含以下成员,必须放在初始化列表位置进行初始化:

1.  const成员变量                       const int _N;

2.  引用成员变量                         int& ref;

3.  没有默认构造函数的自定义类型成员变量     A _aa;  

class A {
public:
  A(int a) {
  _a = a;
  }
private:
  int _a;
};
class Date {
public:
  Date(int year, int month, int day, int i)
  : _N(10)
  , _ref(i)
  , _aa(0)
  {
  _year = year;
  _month = month;
  _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
  const int _N;
  int& _ref;
  A _aa;
};


③ 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。


class Time {
public:
  Time(int hour = 0)
  :_hour(hour)
  {
  cout << "Time()" << endl;
  }
private:
  int _hour;
};
class Date {
public:
  Date(int day)
  {}
private:
  int _day;
  Time _t;
};
int main()
{
  Date d(1);
}


内置类型的成员,在函数体和初始化列表初始化都可以,


自定义类型的成员,建议在初始化列表初始化,这样更高效。


④ 成员变量在类中的声明顺序就是在初始化列表中的初始化顺序,与其在初始化列表中出现的顺序无关。


class A {
public:
  A(int a)
  :_a1(a)
  , _a2(_a1)  // 先执行它
  {}
  void Print() {
  cout << _a1 << " " << _a2 << endl;
  }
private:
  int _a2;   // _a2 先声明
  int _a1;
};
int main() {
  A aa(1);
  aa.Print();
}


19581b0397a344330e24b49d65791574_3845161c4045405ca771dfb69877709b.png


因为我们先声明的是 _a2,所以在初始化列表里我们先初始化的是 _a2,


因为这里是 _a2(_a1), _a1 此时还是没有得到传过去的 1,


此时还是随机值,所以 _a2 就被初始化成随机值了。


按照声明顺序然后是 _a1, _a1 接收到了1,自然会初始化成 1。


最后按顺序打印 ——  1 和 随机值。


🔺 建议:一个类,尽量声明的顺序和初始化列表出现的顺序保持一致,就不容易出问题。


0x03 初始化列表的总结


① 初始化列表 - 成员定义变量的地方。初始化只能初始化一次。


② const、引用、没有构造函数的自定义类型 成员变量必须在初始化列表初始化,因为它们都必须在定义的时候初始化,


③ 对于像其他类型成员变量,如 int _year、int_month 这些,在哪初始化都可以。


初始化列表就是给成员变量找了一个依次处理的地方。


内置类型的成员,在函数体和在初始化列表初始化都是可以的


自定义类型的成员,建议在初始化列表初始化


0x04  C++11的成员初始化新玩法

C++11 支持非静态成员变量在声明时进行初始化赋值,


但是要注意 这里是给声明变量设立缺省值,而不是初始化 。


因为这里是声明,你怎么能给他初始化呢?


(再次有请范大将军说两句)


💬 使用方法演示:


class B {
public:
  B(int b = 0)
  : _b(b)
  {}
private:
  int _b;
};
class A {
private:
  int _a1 = 0;   // 这里是给成员变量缺省值!!!
  B _bb;
};
int main(void)
{
  A aa;
  return 0;
}

de2c3ff5e16b148365f5ec65768704c5_c647c284e61b4101ad835a499f1f655b.png

💬 这里和之前讲的缺省参数类似,如果你给它初始化了:


// ...
class A {
public:
  A(int a1)
  : _a1(a1)   // 我明确的给了值
  {}
private:
  int _a1 = 0;
  B _bb;
};
int main(void)
{
  A aa(100);
  return 0;
}

17cc05820d0f2aecb01794f60cb5b5c5_c1e5b48bf811424594f601d378d7c6c0.png


💬 当然,不仅仅能给内置类型设置缺省值,还可以给自定义类型设置:


class B {
public:
  B(int b = 0)
  : _b(b)
  {}
private:
  int _b;
};
class A {
public:
  A(int a1)
  : _a1(a1)
  {}
private:
  int _a1 = 0;
  B _bb1 = 10;  // 给一个10
};


这里我可以给 10 的原因是因为 B 是一个单参数类型的构造函数,


它可以隐式类型转换,这个我们下面会讲。


再比如说,你还可以给一个匿名对象:


class B {
public:
  B(int b = 0)
  : _b(b)
  {}
private:
  int _b;
};
class A {
public:
  A(int a1)
  : _a1(a1)
  {}
private:
  int _a1 = 0;
  B _bb1 = B(20);
};

拿缺省值去构造、再拷贝构造,编译器会优化。


我们再来看个更逆天的,甚至还可以调函数:


class A {
public:
  A(int a1)
  : _a1(a1)
  {}
private:
  int _a1 = 0;
  B _bb1 = B(20);
  int* P = (int*)malloc(4 * 10);   // 甚至可以调函数
    // int arr[10] = {1,2,3,4,5};  经测试,VS13不支持,VS19+支持

……  但是,这些方式给的都是缺省值!


🔺 总结:如果你在初始化列表阶段没有对成员变量初始化,他就会使用缺省值初始化。


Ⅲ.  关键字 explicit


0x00 引入 - 隐式类型转换

再讲之前我们先做一点点铺垫。在C语言中,对于隐式类型转换:


int main(void)
{
  double d = 1.1;
  int i = d;
  return 0;
}

cadeb5f056c7bc7ccfe76863918ded15_ffc2715a7f124142a545118ac3cf335e.png

❓ 为什么会支持隐式类型转换呢?


因为他们是意义相同的类型,比如 char、int、double 这些类型都是可以互相转,因为它们都是表示数据大小的。

ebe0372f975cccce51fe80c58931a32d_dd9a4cbda686488a9bf26dbbb3c22598.png


int main(void)
{
  // 隐式类型转换 - 相近类型
  double d = 1.1;
  int i = d;
  // 强制类型转换 - 无关类型
  int* p = &i;
  int j = (int)p;
  return 0;
}

这里 d 也不是直接转给 i,p 也不是直接转给 j 的,我们之前讲过,中间会生成一个临时变量。


我们在讲引用的时候详细讲过这一点,所以我们这里可以加一个 const


double d = 1.1;
  const int i = d;

铺垫完了,我们现在在观察下列代码:


class Date {
public:
  Date(int year)
  : _year(year)
  {
  ;
  }
private:
  int _year;
};
int main(void)
{
  Date d1(2022);
  Date d2 = 2022;    // 隐式类型转换
  return 0;
}


❓ 这里是隐式类型的转换,为什么支持一个整型转换成日期类相关的类型呢?


整型和日期类本来是没有关系的,但是你支持一个单参数的构造函数后,


整型就可以去构造一个日期类的对象,这个日期类的对象自然可以赋值给他了。


本来用 2022 构造成一个临时对象 Date(2022) ,在用这个对象拷贝构造 d2,


但是 C++ 编译器在连续的一个过程中,编译器为了提高效率,多个构造会被优化,合二为一。


所以这里被优化成,直接就是一个构造了。


并不是所有的编译器都会这么做,C++标准并没有规定,但是新一点的编译器一般都会这么做。


虽然他们两都是直接构造,但是过程是不一样的。

Date d1(2022);
Date d2 = 2022;  // 隐式类型转换

本来是一次构造 + 依次拷贝构造,这里直接优化了。

如果你不想让这种 "转换" 发生,C++提供了一种关键字 —— 艾克斯·普塞特  explicit


0x01 explicit 关键字介绍

构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还具有类型转换的作用。


📚 用 explicit 关键字修饰构造函数,可以禁止单参构造函数的隐式类型转换。


class Date {
public:
  explicit Date(int year)
  : _year(year)
  {
  ;
  }
private:
  int _year;
};


相关文章
|
1月前
|
存储 安全 编译器
【C++专栏】C++入门 | auto关键字、范围for、指针空值nullptr
【C++专栏】C++入门 | auto关键字、范围for、指针空值nullptr
33 0
|
8天前
|
编译器 C++
【C++】类与对象(static、explicit、友元、隐式类型转换、内部类、匿名对象)
【C++】类与对象(static、explicit、友元、隐式类型转换、内部类、匿名对象)
8 2
|
12天前
|
程序员 C++
为什么c++要引入class关键字
总之,C++引入 `class`关键字是为了支持面向对象编程,通过封装、继承、多态和抽象等特性,提供了更强大、灵活和可维护的编程工具,使得程序开发更加高效和可扩展。这使C++成为一种强大的编程语言,广泛用于各种应用领域。
23 1
|
15天前
|
存储 Java C++
【C++类和对象】探索static成员、友元以及内部类
【C++类和对象】探索static成员、友元以及内部类
|
15天前
|
C语言 C++
【C++入门】关键字、命名空间以及输入输出
【C++入门】关键字、命名空间以及输入输出
|
1月前
|
C++
|
1月前
|
存储 编译器 Linux
【C++】C++入门第二课(函数重载 | 引用 | 内联函数 | auto关键字 | 指针空值nullptr)
【C++】C++入门第二课(函数重载 | 引用 | 内联函数 | auto关键字 | 指针空值nullptr)
|
7天前
|
设计模式 安全 算法
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
16 0