《C++要笑着学》|C++要笑着学(面向对象(下))

火速猛戳订阅《C++要笑着学》趣味教学博客
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

前言:
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
我是柠檬叶子C。上一章我们一步步地实现了日期类,这一章我们继续往后讲解知识点,比如说友元啊,初始化列表啊、静态成员和内部类,把这些拿出来讲一讲。还是保持最近养成的写作习惯,在讲解知识点之前,我都会用一个例子或问题进行引入,做到"循序渐进" 地讲解。
如果觉得文章不错,可以 "一键三连" 支持一下博主!你们的关注就是我更新的最大动力!
Ⅰ.友元(friend) 0x00 引入 - 日期类的流提取
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
观察下面这个日期类,我们是调用 Print 成员函数来打印的:

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

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片


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

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

这样当然是不行的,主要的原因还是这个是一个操作符。
是C++里面的 流插入 ,这里的意思就是要像流里面插入一个 d1。
我们说过,内置类型是支持运算符的,而自定义类型是不支持的,
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
它是不知道该怎么输出的,输入也是一样的道理,也是不知道该怎么去输入。
cin >> d1; ?

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
那怎样才能向我们内置类型一样去用 流插入 和 流提取 呢?
依然可以使用重载这个运算符的方法来解决!

我们先来看一下文档:cplusplus.com - The C++ Resources Network
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
cout 其实是一个全局类型的对象,这个对象的类型是 ostream 。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

说个题外话,内置类型之所以能直接支持你用,是因为 ostream 已经帮你写好了。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

所谓的 "自动识别类型" ,不过只是函数重载而已……
你是 int 它就匹配 int ,你是 char 它就匹配 char 。

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
我们现在知道了, 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; }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
我们是想输出年月日的,我们这里就可以自己控制格式来输出了。

test.cpp
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
这时我们发现cout << d1 还是识别不了,调不动。
这里不识别的原因是因为它是按参数走的,第一个参数是左操作数,第二个参数是右操作数。
双操作数的运算符重载时,规定第一个参数是左操作数,第二个参数是右操作数。
我们这里是成员函数…… 那第一个参数是……《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

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

所以,我们在调用这个流插入重载时就需要:
d1.operator<<(cout);


《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
因为这种原因,我们要直接写就会成这样:
#include "Date.h"int main(void) { Date d1(2022, 3, 20); // cout << d1; ? 不识别,调不动 d1 << cout; // d1.operator<<(cout); return 0; }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

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

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
什么鬼?流倒灌??!
这不符合我们对 "流" 的理解,我们正常理解流插入,是对象流到 cout 里面去。
你现在是流插入到对象里面去。不像是 "流插入" 了,听上去更像是 "流倒灌"……
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
实现成这样,确实可以调用,但是这样用起来也太奇怪了,也不符合我们的使用习惯。
cout << d1; ? 这才是我们习惯的用法!如何实现?

因为被隐含的 this 指针参数给占据了,所以就一定会是左操作数,
这时如果写成成员函数,双操作数的左操作数一定是对象。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
……
基于这样的原因,我们如果还是想让 cout 到左边去,就不能把他重载成成员函数了。
可以直接把它重载成全局的,在类外面,不是成员函数了就没有这些隐含的东西了!
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
这样的话就可以让第一个参数变为左操作数,即 ——
out 在第一个位置,Date& d 在第二个位置:
void operator<<(ostream& out, const Date& d) { out << d._year << "/" << d._month << "/" << d._day << endl; }

这个时候调用是肯定能调的动了,调的是全局函数。
但我们现在面临的问题是,不能访问私有的问题。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

? 不能访问私有的问题改如何解决?把 private 改为 public ?
这种方式肯定是不好的,当然我们可以写个 getYear getMonth getDay 去获取它们。
这样也可以,但是输入的时候怎么办?我们再实现 cin 流体去的时候是要 "写" 的。
这时候就麻烦了,你还得写一个 set,属实是麻烦,有没有更好地办法可以解决这种问题呢?
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
有!C++ 引入了一个东西叫做 —— 友元。


0x01 友元的概念
friend

一个全局函数想用对象去访问 private 或者 public ,就可以用友元来解决。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
友元分为 友元函数 和 友元类 。
比如刚才我们想访问 Date 类,我就可以把它定义为 友元函数 ,友元的声明要放到类里面。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
友元会破坏封装,能不用就不用!友元就像是黄牛,破坏了管理规则。


0x02 友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
它不属于任何类,但需要在类的内部进行声明,声明时要加 friend 关键字。

我们现在就可以去解决刚才的问题了:
Date.h
class Date { public: friend void operator<<(ostream& out, const Date& d); // 友元的声明 out << _year << "/" << _month << "/" << _day << endl; } //...private: int _year; int _month; int _day; };

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

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

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

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

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

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

现在实现的不支持。这和连续赋值很像,只是连续赋值是从右往左,这里是从左往右。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

连续插入 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; }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片


《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
解决了流插入,我们再来顺便实现一下流提取。
这样我们上一章实现的日期类,基本上就完整了。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
查完文档我们可以知道,该对象的类型是 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; };

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
流提取因为要把输入的东西写到对象里去,会改变,所以这里当然不能加 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; }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片


注意事项:
① 友元函数可以访问类的 private 和 protected 成员,但并不代表能访问类的成员函数。
② 友元函数不能用 const 修饰。
③ 友元函数可以在类定义的任何地方申明,可以不受类访问限定符的控制。
④ 一个函数可以是多个类的友元函数。
⑤ 友元函数的调用和普通函数的调用原理相同。

0x03 友元类
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
友元类的所有成员函数都可以是另一个类的友元函数,
都可以访问另一个类中的非公有成员。

friend class 类名;

① 友元关系是单向的,不具有交换性。
② 友元关系不具有传递性(朋友的朋友不一定是朋友)。
如果 B 是 A 的友元,C 是 B 的友元,则不能说明 C 是 A 的友元。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片


定义一个友元类:
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; };

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
这里 Date 是 Time 的友元,我们在日期类里就可以访问时间类的私有成员了。

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

但是时间类里不能访问日期类,因为这是 "单向好友" ,
如果想在时间类里访问日期类,我们可以在日期类里声明:
class Date { friend class Time; // ... }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
这样,它们之间就是 "双向好友" 了 —— 互相成为对方的友元。

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片



Ⅱ.初始化列表 0x00 引入 - 难缠的初始化问题
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
我们知道,常量必须在定义时初始化。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

const int j; ? const int j = 0; ?在定义的时候就要初始化,不初始化就会出问题, 因为常量只有一次初始化的机会,就是在定义的时候。


《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
我们现在再来思考这个问题:
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; // 这一块是声明,我们不应该在这初始化。 };

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
初始化要在空间上给值,你这里有空间吗?你没空间啊!
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

? 这也不行,那也不行,那我们该如何初始化这个烦人的 _N 呢?
基于这种原因,C++ 就搞出了一个叫做初始化列表的东西。


0x01 概念及使用方法
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
我们之前学习创建对象时,编译器通过调用构造函数,给对象赋初值。
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; };

上面的构造函数调用后,对象中已经有了一个初始值,
但是不能将其作为类对象成员进行初始化,构造函数体中的语句只能将其作为 "赋初值" ,
而不能称作是 "初始化" 。 因为初始化只能初始化一次, 而构造函数体内可以多次赋值。
所以我们现在来学习一种 "初始化" 的方式
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
初始化列表 —— 成员变量定义的地方。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

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

代码演示:
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; }

浅调一下吧 ~
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片


《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
现在,我们再来看刚才的问题,如何初始化 _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++ 这里并没有规定必须要怎么怎么样。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片



0x02 使用时注意事项
① 每个成员变量再初始化列表中只能出现一次,即 初始化只能初始化一次。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片


② 必须在定义时就初始化的成员变量,要在初始化列表初始化。
类中包含以下成员,必须放在初始化列表位置进行初始化: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); }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
内置类型的成员,在函数体和初始化列表初始化都可以,
自定义类型的成员,建议在初始化列表初始化,这样更高效。

④ 成员变量在类中的声明顺序就是在初始化列表中的初始化顺序,与其在初始化列表中出现的顺序无关。
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(); }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

因为我们先声明的是 _a2,所以在初始化列表里我们先初始化的是 _a2,
因为这里是 _a2(_a1), _a1 此时还是没有得到传过去的 1,
此时还是随机值,所以 _a2 就被初始化成随机值了。
按照声明顺序然后是 _a1, _a1 接收到了1,自然会初始化成 1。
最后按顺序打印 ——1 和 随机值。

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


0x03 初始化列表的总结
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

① 初始化列表 - 成员定义变量的地方。初始化只能初始化一次。
② const、引用、没有构造函数的自定义类型 成员变量必须在初始化列表初始化,因为它们都必须在定义的时候初始化,
③ 对于像其他类型成员变量,如 int _year、int_month 这些,在哪初始化都可以。
初始化列表就是给成员变量找了一个依次处理的地方。
内置类型的成员,在函数体和在初始化列表初始化都是可以的
自定义类型的成员,建议在初始化列表初始化


0x04C++11的成员初始化新玩法
C++11 支持非静态成员变量在声明时进行初始化赋值,
但是要注意 这里是给声明变量设立缺省值,而不是初始化 。
因为这里是声明,你怎么能给他初始化呢?
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
(再次有请范大将军说两句)

使用方法演示:
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; }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

这里和之前讲的缺省参数类似,如果你给它初始化了:
// ...class A { public: A(int a1) : _a1(a1)// 我明确的给了值 {}private: int _a1 = 0; B _bb; }; int main(void) { A aa(100); return 0; }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片


当然,不仅仅能给内置类型设置缺省值,还可以给自定义类型设置:
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 是一个单参数类型的构造函数,
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
它可以隐式类型转换,这个我们下面会讲。
再比如说,你还可以给一个匿名对象:
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); };

【《C++要笑着学》|C++要笑着学(面向对象(下))】拿缺省值去构造、再拷贝构造,编译器会优化。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
我们再来看个更逆天的,甚至还可以调函数:
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++要笑着学》|C++要笑着学(面向对象(下))
文章图片
再讲之前我们先做一点点铺垫。在C语言中,对于隐式类型转换:
int main(void) { double d = 1.1; int i = d; return 0; }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

? 为什么会支持隐式类型转换呢?
因为他们是意义相同的类型,比如 char、int、double 这些类型都是可以互相转,因为它们都是表示数据大小的。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

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;


《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
铺垫完了,我们现在在观察下列代码:
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++要笑着学》|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; };



Ⅳ.静态成员(static) 0x00 引入 - 计算类中创建了多少个类对象
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
如果我们要计算一个类中创建了多少个类对象,我们可以用 count 计算一下。
int count = 0; // 全局变量class A { public: A(int a = 0) : _a(a) { count++; } A(const A& aa) : _a(aa._a) { count++; }private: int _a; }; void f(A a) { ; }int main(void) { A a1; A a2 = 1; f(a1); cout << count << endl; return 0; }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片


? 如果我不想让这个 count 可以被人在外面随便改呢?
int main(void) { A a1; A a2 = 1; f(a1); cout << cnt << endl; count++; // 我可以在类外修改它 return 0; }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
有没有办法可以把 count 和类贴合起来呢?让这个 count 专门用来计算我 A 这个类的。
我们先试着把它定义成 —— 成员变量:
class A { public: A(int a = 0) : _a(a) { _count++; } A(const A& aa) : _a(aa._a) { _count++; }private: int _a; int _count = 0; // 定义成成员变量 };

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
但是这样还是不行!这样的话每个对象里面都有一个 count,
我们是希望的是每个对象创建的时候去++的是同一个变量,而不是每个对象里面都有一个。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
那该怎么办呢?
类里面可以定义静态成员,在成员变量前面加一个 static,就是静态成员。
我们继续往下看 ~ 《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片



0x01 静态成员的概念
声明为 static 的类成员称为类的静态成员,用 static 修饰的成员变量,称为静态成员变量。
用 static 修饰的成员函数,称为静态成员函数,静态的成员变量一定要在类外进行初始化。
class A { public: A(int a = 0) : _a(a) { _sCount++; } A(const A& aa) : _a(aa._a) { _sCount++; }private: int _a; // 静态成员变量属于整个类,所有对象,生命周期在整个程序运行期间。 static int _sCount; // 这里以 _s 为前缀,是为了一眼就看出它是静态成员变量。 };


0x02 静态成员的特性
① 静态成员为所有类对象所共享,不属于某个具体的实例。
② 静态成员变量必须在类外定义,定义时不添加 static 关键字。
③ 类静态成员即可用类名 :: 静态成员变量或者对象 . 来访问。
④ 静态成员函数没有隐藏的 this 指针,不能访问任何非静态成员。
⑤ 静态成员和类的普通成员一样,也有 public、protected、private 三种访问级别,也可以具有返回值。

0x03 静态成员函数的访问
如果它是公有的,我们就可以在类外对它进行访问:
class A { public: A(int a = 0) : _a(a) { _sCount++; } A(const A& aa) : _a(aa._a) { _sCount++; }// private: int _a; static int _sCount; }; void f(A a) { ; }int main(void) { A a1; A a2 = 1; f(a1); cout << A::_sCount<< endl; // 使用类域对它进行访问 /* 这里不是说是在 a1 里面找,这里只是帮助他突破类域罢了 */ cout << a1._sCount << endl; cout << a2._sCount << endl; return 0; }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
但是如果它是私有的,我们可以提供一个公有的成员函数。
我们写一个公有的 GetCount 成员函数,让它返回 _sCount 的值,
这样我们就可以在类外调用该函数,就可以访问到它了。

还有没有更好的方式?让我不用对象就可以访问到它呢?
静态成员函数:
static int GetCount() { return _sCount; }

它的好处是没有 this 指针,它只能访问静态的成员变量。
当然,我们还可以用友元,但是未免有些没必要了。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片



Ⅴ.内部类 0x00 内部类的概念
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

如果在 《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
类中定义 《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
类,我们称 《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
的内部类。
class A { class B { ; }; };


0x01 内部类特性
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
此时这个内部类是一个独立的类,它不属于外部类,
更不能通过外部类的对象去调用内部类,外部类对内部类没有任何特权。

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
但是,内部类就是外部类的友元类,
内部类可以通过外部类的对象参数来访问外部类中的所有成员,像极了殖民行为。

B 是 A 的内部类:
class A { private: static int _s_a1; int _a2; public: class B {// B天生就是A的友元 public: void foo(const A& a) { cout << _s_a1 << endl; // ? cout << a._a2 << endl; // ? } private: int _b1; }; };


0x02 详细探索内部类
? 我们用 sizeof 计算 A 类的大小,得到的结果会是什么?
#include using namespace std; class A { private: static int _s_a1; int _a2; public: class B { private: int _b1; }; }; int A::_s_a1 = 1; int main(void) { cout << "A的大小为: " << sizeof(A) << endl; return 0; }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

① 内部类 B 和在全局定义是基本一样的,它只是受外部类 A 类域的限制,定义在 A 的类域中。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

class A { private: static int _s_a1; int _a2; public: class B { private: int _b1; }; }; int A::_s_a1 = 1; int main(void) { A aa; A::B bb; // 用A的类域指定即可(前提是公有的) return 0; }

《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

② 内部类 B 天生就是外部类 A 的友元,也就是 B 中可以访问 A 的私有,A 不能访问 B 的私有(或保护)。
所以,A 类型的对象里没有 B,跟 B 没什么关系,计算 sizeof 当然也不会带上B。
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

舔的好!那我就勉为其难地,让你做我的朋友吧:
《C++要笑着学》|C++要笑着学(面向对象(下))
文章图片

class A { private: static int _s_a1; int _a2; public: class B {// B天生就是A的友元 friend class A; // 声明A是B的友元 private: int _b1; }; };

如此一来,A 和 B 就互通了。

参考资料:
Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .
. C++reference[EB/OL]. []. http://www.cplusplus.com/reference/.
百度百科[EB/OL]. []. https://baike.baidu.com/.
比特科技. C++[EB/OL]. 2021[2021.8.31]. .
笔者:王亦优
更新: 2022.3.28
? 勘误:暂无
声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!
本章完。

    推荐阅读