C++11
- 一、C++11简介
- 二、一致的列表初始化
-
- 1.{}初始化
- 2. std::initializer_list
- 三、申明
-
- 1. auto
- 2. decltype
- 3. nullptr
- 四、右值援用和移动语义
-
- 1. 左值援用和右值援用
- 2. 左值援用与右值援用比拟
- 3. 右值援用经常使用场景和意义
- 4. 右值援用援用左值及其一些更深化的经常使用场景剖析
- 5. 完美转发
- 五、新的类配置
-
- 1. 自动成员函数
- 2. 类成员变量初始化
- 3. 强迫生成自动函数的关键字 default
- 4. 制止生成自动函数的关键字 delete
- 5. 承袭和多态中的 final 与 override 关键字
一、C++11简介
在 2003 年 C++ 规范委员会曾经提交了一份技术修订表(简称TC1),使得 C++03 这个名字曾经取代了 C++98 称为 C++11 之前的最新 C++ 规范称号。不过由于 C++03(TC1) 关键是对 C++98 规范中的破绽启动修复,言语的外围部分则没有改变,因此人们习气性的把两个规范兼并称为 C++98/03 规范。
从 C++0x 到 C++11,C++ 规范10年磨一剑,第二个真正意义上的规范珊珊来迟。相比于 C++98/03,C++11 则带来了数量可观的变动,其中蕴含了约 140 个新个性,以及对 C++03 规范中约 600 个毛病的修正,这使得 C++11 更像是从 C++98/03 中孕育出的一种新言语。相比拟而言,C++11 能更好地用于系统开发和库开发、语法愈加泛华和繁难化、愈加稳固和安保,不只配置更弱小,而且能优化程序员的开发效率,公司实践名目开发中也用得比拟多,所以咱们要作为一个重点去学习。C++11 参与的语法个性十分篇幅十分多,咱们这里没方法一 一解说,所以本章关键解说实践中比拟适用的语法。
C++11的起源:1998年是 C++ 规范委员会成立的第一年,原本方案以后每 5 年视实践须要更新一次性规范,C++ 国内规范委员会在钻研 C++03 的下一个版本的时刻,一开局方案是 2007 年颁布,所以最后这个规范叫 C++07。然而到06年的时刻,官网觉得2007年必需完不成 C++07,而且官网觉得 2008 年或许也完不成。最后罗唆叫C++ 0x。x 的意思是不知道究竟能在07还是08还是09年成功。结果 2010 年的时刻也没成功,最后在2011年初于成功了 C++ 规范。所以最终定名为C++11。
二、一致的列表初始化
1.{}初始化
在C++98中,规范准许经常使用花括号{}对数组或许结构体元素启动一致的列表初始值设定。比如:
struct Point{int _x;int _y;};int main(){int array1[] = { 1,2,3,4,5 };int array2[5] = { 0 };Point p = { 0, 1 };return 0;}
C++11 扩展了用大括号括起的列表(初始化列表)的经常使用范畴,使其可用于一切的内置类型和用户自定义的类型,经常使用初始化列表时,可参与等号(=),也可不参与。
struct Point{int _x;int _y;};int main(){int array1[]{ 1,2,3,4,5 };int array2[5]{ 0 };Point p{ 0, 1 };// C++11中列表初始化也可以适用于new表白式中int* pa = new int[4]{ 1,2,3,4 };return 0;}
创立对象时也可以经常使用列表初始化方式调用结构函数初始化。
class Date{public:Date(int year, int month, int day):_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}private:int _year;int _month;int _day;};int main(){Date d1(2022, 1, 1); // old style// C++11支持的列表初始化,这里会调用结构函数初始化// 结构+拷贝结构->优化间接结构Date d2{ 2022, 1, 2 };Date d3 = { 2022, 1, 3 };return 0;}
2. std::initializer_list
std::initializer_list 的引见文档:std::initializer_list
咱们先来看看 std::initializer_list 是什么类型的:
int main(){auto i = { 10,20,30 };cout << typeid(i).name() << endl;return 0;}
首先咱们来看一个疑问,以下代码中,v1、l1、d1 的初始化方式是一样的吗?
int main(){vector<int> v1 = { 1,2,3,4,5 };list<int> l1 = { 10, 20, 30 };Date d1 = { 2024, 1, 9 };return 0;}
其中,v1 和 l1 的初始化方式是一样的,v1 和 l1 的 {} 内的数据会被识别成 initializer_list 类型,这是 C++11 新参与的类型,每个容器都参与了经常使用 initializer_list 的结构函数,数据被识别成 initializer_list 类型后再调用相应的结构函数启动初始化,参考文档:
然而 d1 是多参数结构类型转换,是结构+拷贝结构经过优化之后间接结构!
{ 2024, 1, 9 };
会被识别成一个 Date 对象,结构成功之后再去拷贝结构 d1,然而这个环节会被编译器启动优化;这种状况当且仅当 {} 内的参数个数和 Date 中的结构函数的参数个数一样的时刻!当他们的参数个数不婚配的时刻,{} 内也会被识别成
initializer_list
类型,这时刻由于参数个数不婚配会报错!
所以咱们假设在以前模拟成功的vector中经常使用 initializer_list 去初始化对象的时刻,是会报错的,由于咱们以前没有写相应的结构函数, initializer_list 的结构函数也很繁难,咱们可以繁难写一个,如下:
vector(initializer_list<T> lt){reserve(lt.size());for (auto& e : lt){push_back(e);}}
须要留意的是,当经常使用大括号对容器赋值
v = {10, 20, 30};
这个时刻调的是赋值重载,而不是
initializer_list
的结构。
雷同, map 也支持 initializer_list 去初始化,文档中也有相应的结构函数:
例如代码:
int main(){map<string, string> dict = { {"sort", "排序"}, {"insert", "拔出"} };return 0;}
首先,
{"sort", "排序"}
和
{"insert", "拔出"}
会被识别成一个
pair
类型,而这两个经常使用的 {} 括起来就被识别成
initializer_list
类型,从而去初始化对象。
三、申明
c++11 提供了多种简化申明的方式,尤其是在经常使用模板时。
1. auto
在 C++98 中 auto 是一个存储类型的说明符,标明变量是部分智能存储类型,然而部分域中定义部分的变量自动就是智能存储类型,所以 auto 就没什么价值了。C++11 中废除 auto 原来的用法,将其用于成功智能类型推断。这样要求必需启动显示初始化,让编译器将定义对象的类型设置为初
始化值的类型。
auto 咱们以前用得也不少了,常罕用来推断比拟长的类型和范畴 for. 这里就不再多启动引见。
2. decltype
关键字 decltype 将变量的类型申明为表白式指定的类型。 decltype 可以推导对象的类型,这个类型是可以用来模板实参,或许再定义对象。
例如经常使用场景:
template<class T1, class T2>void F(T1 t1, T2 t2){decltype(t1 * t2) ret;cout << typeid(ret).name() << endl;}int main(){const int x = 1;double y = 2.2;decltype(x * y) ret; // ret的类型是doubledecltype(&x) p; // p的类型是int*// 类型以字符串方式失掉到cout << typeid(ret).name() << endl;cout << typeid(p).name() << endl;vector<decltype(ret)> v; // 经常使用 ret 的类型去实例化 vectorF(1, 'a');return 0;}
3. nullptr
由于C++中NULL被定义成字面量0,这样就或许回带来一些疑问,由于0既能指针常量,又能示意整形常量。所以出于明晰和安保的角度思考,C++11中新增了 nullptr,用于示意空指针。
#ifndef NULL#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif#endif
四、右值援用和移动语义
1. 左值援用和右值援用
传统的 C++ 语法中就有援用的语法,而 C++11 中新增了的右值援用语法个性,所以从如今开局咱们之前学习的援用就叫做左值援用。无论左值援用还是右值援用,都是给对象取别名。
首先咱们须要知道, 什么是左值?什么是左值援用?
左值是一个示意数据的表白式(如变量名或解援用的指针),咱们可以失掉它的地址 + 可以对它赋值, 左值可以出现赋值符号的左边,右值不能出如今赋值符号左边 。定义时 const 润色符后的左值,不能给他赋值,然而可以取它的地址。 左值援用就是给左值的援用,给左值取别名 。
例如以下左值和左值援用:
int main(){// 以下的p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;// 以下几个是对上方左值的左值援用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;return 0;}
那么 什么是右值?什么是右值援用?
右值也是一个示意数据的表白式,如:字面常量、表白式前往值,函数前往值(这个不能是左值援用前往)等等, 右值可以出如今赋值符号的左边,然而不能出现出如今赋值符号的左边,右值不能取地址 。右值援用就是对右值的援用,给右值取别名。
例如以下右值和右值援用:
int main(){double x = 1.1, y = 2.2;// 以下几个都是经常出现的右值10;x + y;fmin(x, y);// 以下几个都是对右值的右值援用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);// 这里编译会报错:error C2106: “=”: 左操作数必需为左值10 = 1;x + y = 1;fmin(x, y) = 1;return 0;}
须要留意的是右值是不能取地址的,然而给右值取别名后,会造成右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量 10 的地址,然而 rr1 援用后,可以对 rr1 取地址,也可以修正 rr1 ;假设不想 rr1 被修正,可以用 const int&& rr1 去援用,是不是觉得很神奇,这个了解一下实践中右值援用的经常使用场景并不在于此,这个个性也不关键。
例如以下代码:
int main(){double x = 1.1, y = 2.2;int&& rr1 = 10;const double&& rr2 = x + y;rr1 = 20; // 可以修正rr2 = 5.5; // 报错return 0;}
2. 左值援用与右值援用比拟
左值援用总结:
-
左值援用只能援用左值,不能援用右值。
-
然而 const 左值援用既可援用左值,也可援用右值。
int main(){// 左值援用只能援用左值,不能援用右值。int a = 10;int& ra1 = a; // ra1 为 a 的别名//int& ra2 = 10; // 编译失败,由于10是右值,左值不能援用右值// const左值援用既可援用左值,也可援用右值。const int& ra3 = 10;const int& ra4 = a;return 0;}
右值援用总结:
- 右值援用只能右值,不能援用左值。
- 然而右值援用可以援用 move 以后的左值。
其中, move 的作用就是将一个左值强迫转换为右值,使它具备右值的性质。
int main(){// 右值援用只能右值,不能援用左值。int&& r1 = 10;// error C2440: “初始化”: 不可从“int”转换为“int &&”// message : 不可将左值绑定到右值援用int a = 10;int&& r2 = a; // error// 右值援用可以援用 move 以后的左值int&& r3 = std::move(a);return 0;}
3. 右值援用经常使用场景和意义
前面咱们可以看到左值援用既可以援用左值和又可以援用右值,那为什么 C++11 还要提出右值援用呢?是不是弄巧成拙呢?上方咱们来看看左值援用的短板,右值援用是如何补齐这个短板的!
咱们先看看以前成功的 string 类:
namespace Young{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){//cout << "string(char* str)" --- 结构<< endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝结构string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}~string(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}//string operator+=(char ch)string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}string to_string(int val){string str;return str;}private:char* _str;size_t _size;size_t _capacity; // 不蕴含最后做标识的\0};}
左值援用的经常使用场景:
-
援用传参和援用前往都能提高效率
void func1(Young::string s){}void func2(const Young::string& s){}int main(){Young::string s1("hello world");// func2 的调用咱们可以看到左值援用做参数缩小了拷贝,提高效率的经常使用场景和价值func1(s1);func2(s1);// string operator+=(char ch) 传值前往存在深拷贝// string& operator+=(char ch) 传左值援用没有拷贝提高了效率s1 += '!';return 0;}
左值援用的短板:
然而当函数前往对象是一个部分变量,出了函数作用域就不存在了,就不能经常使用左值援用前往,只能传值前往。例如:
Young::string to_string(int x)
函数中可以看到,这里只能经常使用传值前往,传值前往会造成至少1次拷贝结构(假设是一些旧一点的编译器或许是两次拷贝结构)。
例如:
Young::string to_string(int x){Young::string ret;while (x){int val = x % 10;x /= 10;ret += ('0' + val);}reverse(ret.begin(), ret.end());return ret;}int main(){Young::string ret = Young::to_string(10);return 0;}
其中上述的拷贝环节如下图所示:
上述环节本应该是两次拷贝结构,然而普通会被编译器优化成一次性拷贝结构。
这就是左值援用的短板,以后往值是一个部分对象的时刻还是只能启动传值前往,这样假设是自定义类型的话,会形成深拷贝的代价。这时刻右值援用的价值就表现进去了,可以经常使用右值援用和移动语义处置上述疑问。
右值援用和移动语义:
首先咱们在
Young::string
中参与
移动结构
,
移动结构实质是将参数右值的资源窃取上来
,占位已有,那么就不用做深拷贝了,所以它叫做移动结构,就是窃取他人的资源来结构自己,为什么可以间接窃取他人的资源呢?首先咱们先将右值分为以下两种:
- 纯右值:内置类型右值
- 将亡值:自定义类型的右值
在上述例子中, to_string 中的前往值 ret 就是一个自定义类型的右值,行将亡值,这时刻咱们假设加上移动语义的结构和赋值,那么在 to_string 前往的时刻, ret 被识别成一个将亡值,就会去调移动语义的结构,由于 ret 是一个将亡值,所以咱们可以间接窃取它的资源来结构自己;反正你曾经是一个将亡值了,倒不如把你的资源给我,这样就省去了深拷贝的代价,就是这个意思。
上方咱们在
Young::string
中参与移动语义的结构和赋值:
// 移动结构string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动语义" << endl;swap(s);}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动语义" << endl;swap(s);return *this;}
咱们继续调用 to_string 观察能否还会启动深拷贝:
然而假设是以下场景又会有一些变动:
int main(){Young::string ret;ret = Young::to_string(10);return 0;}
该场景和上述场景的区别在于,该场景经常使用一个已存在的对象接纳 to_string 的前往值。咱们观察会有什么区别:
这里运转后,咱们看到调用了一次性移动结构和一次性移动赋值。由于假设是用一个曾经存在的对象接纳,编译器就没方法优化了。
Young::to_string
函数中会先用前往的
ret
生成结构生成一个暂时对象,然而咱们可以看到,编译器把
ret
识别成了右值,行将亡值,调用了移动结构。而后在把这个暂时对象做为
Young::to_string
函数调用的前往值赋值给接纳的
ret
,这里调用的移动赋值。联合下图了解:
STL的容器在C++11以后,都参与了移动结构和移动赋值,如下图:
4. 右值援用援用左值及其一些更深化的经常使用场景剖析
依照语法,右值援用只能援用右值,但右值援用必定不能援用左值吗?由于:有些场景下,或许真的须要用右值去援用左值成功移动语义。当须要用右值援用援用一个左值时,可以经过 move 函数将左值转化为右值。 C++11 中, std::move() 函数位于 头文件中,该函数名字具备蛊惑性,它并不搬移任何物品,惟一的配置就是将一个左值强迫转化为右值援用,而后成功移动语义。
上方咱们看一个疑问,如下代码:
int main(){Young::string s1("hello, world!\n");Young::string s2(s1);Young::string s3 = move(s1);return 0;}
咱们将上述代码中的 s1 启动 move 操作,而后 move 前往一个 s1 的右值,再去结构 s3 ,此时会出现的疑问是什么呢?咱们调试观察:
如上图,当咱们结构完 s3 之后,由于咱们将 s1 转换为了右值,所以这里调用的是移动结构,将 s3 和 s1 的资源调换,此时 s1 就变成了空串!所以咱们不能随意将一个左值启动 move 操作,否则或许会发生意想不到的结果!
STL容器拔出接口函数也参与了右值援用版本:
如下代码:
int main(){list<Young::string> lt;Young::string s1("1111");// 这里调用的是拷贝结构lt.push_back(s1);// 上方调用都是移动结构lt.push_back("2222");lt.push_back(move(s1));return 0;}
上方咱们将以前模拟成功的 list 拿上来,咱们自己成功一个右值援用的 push_back 和 insert :
// 拔出节点 --- 左值版本iterator insert(iterator pos, const T& x){Node* newnode = new Node(x);Node* cur = pos._node;Node* prev = cur->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return newnode;}// 拔出节点 --- 右值版本iterator insert(iterator pos, T&& x){Node* newnode = new Node(x);Node* cur = pos._node;Node* prev = cur->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return newnode;}// 尾插 --- 左值版本void push_back(const T& x){insert(end(), x);}// 尾插 --- 右值版本void push_back(T&& x){insert(end(), x);}
上方咱们测试一下咱们模拟成功的 list 的右值版本的拔出:
如上图,第一次性深拷贝是初始化的结果,不用管,然而咱们经常使用的 push_back 不应该都是移动结构吗?为什么会有一次性深拷贝?上方咱们画图剖析一下:
实质上, 右值被右值援用援用以后的属性是左值 ,即上图中, to_string 前往的值是右值,所以会婚配右值援用的 push_back 版本,然而在 push_back 中, x 的属性却是左值,所以在调用 insert 时,会调用左值版本 insert 也就会造成深拷贝。
那么为什么 右值被右值援用援用以后的属性是左值呢? 由于必需只能是左值,由于右值是不能间接修正,然而右值被右值援用以后,须要被修正,例如咱们上方成功的移动构培育足以说明,例如下图:
那么咱们上方那个疑问应该如何处置呢?咱们可以将右值援用后的左值经常使用 move 变为右值,继续经常使用右值去处置,这里须要改变的就比拟多,咱们须要一层一层地去改,例如下图:
最后咱们看结果,确实成功了移动拷贝:
5. 完美转发
模板中的&& 万能援用:
模板中的不代表右值援用,而是万能援用,其既能接纳左值又能接纳右值。模板的万能援用只是提供了能够接纳同时接纳左值援用和右值援用的才干,然而援用类型的惟一作用就是限度了接纳的类型,后续经常使用中都退步成了左值,咱们宿愿能够在传递环节中坚持它的左值或许右值的属性, 就须要用咱们上方学习的完美转发。例如上方代码是函数模板的万能援用:
template<typename T>void PerfectForward(T&& t){Fun(t);}
咱们可以尝实验证一下能否会依照咱们的需求调用相应的函数:
void Fun(int& x) { cout << "左值援用" << endl; }void Fun(const int& x) { cout << "const 左值援用" << endl; }void Fun(int&& x) { cout << "右值援用" << endl; }void Fun(const int&& x) { cout << "const 右值援用" << endl; }template<typename T>void PerfectForward(T&& t){Fun(t);}int main(){PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;}
结果如下:
为什么所有都是左值援用呢?咱们上方解释过,由于右值被右值援用后,还是左值的属性,所以 t 就是左值。那么咱们想坚持实参的属性应该怎样做呢?这时刻就要用到完美转发了。
std::forward 完美转发在传参的环节中保管对象原生类型属性:
// forward<T>(t)在传参的环节中坚持了 t 的原生类型属性。template<typename T>void PerfectForward(T&& t){Fun(forward<T>(t));}
上述代码
Fun(forward<T>(t));
就是完美转发的经常使用,它能坚持原对象的属性。留意,完美转发要和模板的万能援用搭配经常使用,由于假设不是万能援用,那么它就只能是普通的右值援用,此时左值不能传参。
所以完美转发的经常使用场景有哪些呢?其实咱们曾经接触过了,上方的 push_back 的疑问就可以经常使用完美转发处置,咱们将 move 改成完美转发的方式,并且介绍经常使用完美转发的方式,如下图:
总结:右值援用的移动语义进去以后,对深拷贝的类的影响比拟大,自定义类的深拷贝传值前往影响也较大,由于移动结构和移动赋值进去以后缩小了它们的深拷贝;一些容器的拔出接口也新增了右值版本,也缩小了深拷贝。然而右值援用关于浅拷贝的类是没无心义的,由于它们没有资源可以转移。
五、新的类配置
1. 自动成员函数
原来 C++ 类中,有 6 个自动成员函数:
- 结构函数
- 析构函数
- 拷贝结构函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
最后关键的是前4个,后两个用途不大。自动成员函数就是咱们不写编译器会生成一个自动的。
C++11 新增了两个: 移动结构函数 和 移动赋值运算符重载。
针对 移动结构函数 和 移动赋值运算符重载 有一些须要留意的点如下:
- 假设你没有自己成功移动结构函数,且没有成功析构函数 、拷贝结构、拷贝赋值重载中的恣意一个,也就是都没有成功 。那么编译器会智能生成一个自动移动结构。自动生成的移动结构函数,关于内置类型成员会口头逐成员按字节拷贝,自定义类型成员,则须要看这个成员能否成功移动结构,假设成功了就调用移动结构,没有成功就调用拷贝结构。
- 假设你没有自己成功移动赋值重载函数,且没有成功析构函数 、拷贝结构、拷贝赋值重载中的恣意一个,也就是都没有成功 ,那么编译器会智能生成一个自动移动赋值。自动生成的移动结构函数,关于内置类型成员会口头逐成员按字节拷贝,自定义类型成员,则须要看这个成员能否成功移动赋值,假设成功了就调用移动赋值,没有成功就调用拷贝赋值。(自动移动赋值跟上方移动结构齐全相似)
- 假设你提供了移动结构或许移动赋值,编译器不会智能提供拷贝结构和拷贝赋值。
2. 类成员变量初始化
C++11准许在类定义时给成员变量初始缺省值,自动生成结构函数会经常使用这些缺省值初始化,这个咱们在类和对象曾经引见过了,这里就不再细讲了。
3. 强迫生成自动函数的关键字 default
C++11可以让你更好的管理要经常使用的自动函数。假定你要经常使用某个自动的函数,然而由于一些要素这个函数没有自动生成。比如:咱们提供了拷贝结构,就不会生成移动结构了,那么咱们可以经常使用 default 关键字显示指定移动结构生成。
例如以下代码:
class Person{public:Person(const char* name = "", int age = 0):_name(name), _age(age){}/*Person(const Person& p):_name(p._name), _age(p._age){}*//*Person& operator=(const Person& p){if(this != &p){_name = p._name;_age = p._age;}return *this;}*/// 强迫编译器生成Person(Person&& p) = default;Person(const Person& p) = default;~Person(){}private:Young::string _name;int _age;};
4. 制止生成自动函数的关键字 delete
假设能想要限度某些自动函数的生成,在 C++98 中,是该函数设置成 private ,并且只申明补丁已,这样只需其他人想要调用就会报错。在 C++11 中更繁难,只需在该函数申明加上 =delete 即可,该语法批示编译器不生成对应函数的自动版本,称 =delete 润色的函数为删除函数。
Person(Person&& p) = delete; // 不让生成成功Person(const Person& p) = default; // 强迫编译器生成
5. 承袭和多态中的 final 与 override 关键字
这个咱们在承袭和多态的时刻曾经引见过,这里也不再做多引见。
还没有评论,来说两句吧...