类和对象
1. 面向对象和面向过程的区别是什么?(开放性问题)
1. **抽象级别**: - **面向对象**:以对象(数据和方法的集合)为中心,强调的是数据和行为的封装。 - **面向过程**:以过程(函数或子程序)为中心,强调的是步骤和顺序。2. **数据和方法的关系**: - **面向对象**:数据和处理数据的方法封装在对象中,对象可以包含数据和操作数据的方法。 - **面向过程**:数据和处理数据的方法是分离的,通常数据结构和处理这些数据的函数是分开的。3. **模块化**: - **面向对象**:通过类和对象来实现模块化,类定义了对象的蓝图。 - **面向过程**:通过函数和过程来实现模块化,函数是独立的代码块。4. **代码重用**: - **面向对象**:通过继承和多态性,可以更容易地重用代码。 - **面向过程**:代码重用通常通过函数库来实现,但可能不如面向对象那样灵活。5. **维护和扩展**: - **面向对象**:由于封装和模块化,通常更容易维护和扩展。 - **面向过程**:随着系统的增长,维护和扩展可能会变得更加困难。6. **设计复杂性**: - **面向对象**:设计可能更复杂,因为需要考虑类之间的关系和继承结构。 - **面向过程**:设计可能更直接,因为关注点在于函数的调用和执行。7. **语言支持**: - **面向对象**:许多现代编程语言(如Java、C++、Python、Ruby)天然支持面向对象编程。 - **面向过程**:几乎所有编程语言都支持过程化编程,但一些语言(如C)在支持面向对象特性方面可能不如其他语言。8. **性能**: - **面向对象**:可能会有额外的开销,因为需要处理对象的创建和方法调用。 - **面向过程**:通常在性能上更高效,因为直接调用函数通常比创建对象和调用方法更快。
2.类大小的计算
这里同样是和结构体的内存对齐做法相同,需要注意的是空类的大小是1(主要是为了在地址空间中占位,表示存在这个类)
3.class和struct的区别
struct 一般用于描述一个数据结构集合,而 class 是对一个对象数据的封装;struct 中默认的访问控制权限是 public 的,而 class 中默认的访问控制权限是 private 的,例如:struct A{int iNum; // 默认访问控制权限是 public}class B{int iNum; // 默认访问控制权限是 private}
在继承关系中,struct 默认是公有继承,而 class 是私有继承;class 关键字可以用于定义模板参数,就像 typename,而 struct 不能用于定义模板参数,例如: template<typename T, typename Y> // 可以把typename 换成 classint Func(const T& t, const Y& y) {//TODO}
3.this指针
1)this指针存放在哪里?
this指针是在栈上的,因为他是一个形参,当然有时候在会存在于寄存器上。因为有时候需要频繁的使用this指针,所以放到了寄存器,便于更加快速的使用this指针
2)this指针可以为空嘛?
this指针通常不能为nullptr,因为他是当前对象的地址,然而有一些特殊的情况
1. 静态成员函数:在静态成员函数中,this指针是不可用的,因为静态成员函数不依赖于任何特定的实例。2 .如果是去调用成员函数,由于成员函数并不在对象中,this不会进行解引用,所以即使this是空指针,也不会崩溃。3. 如果this指针是空,还访问了成员变量,那么成员就会崩溃,原因是对空指针进行了解引用。
4.八个默认成员函数
首先先整体清点一下是哪八个成员函数
构造函数,析构函数,拷贝构造函数,赋值运算符重载,取地址操作符重载,const成员函数,移动构造,移动赋值
1)构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象
特性:
1. 函数名与类名相同。2. 无返回值。3. 对象实例化时编译器自动调用对应的构造函数。4. 构造函数可以重载。
问: 关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的
默认构造函数并没有什么用??
答: C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
class Time{public: Time() { cout << "Time()" << endl; _hour = 0; _minute = 0; _second = 0; }private: int _hour; int _minute; int _second;};class Date{private: // 基本类型(内置类型) int _year; int _month; int _day; // 自定义类型 Time _t;};int main(){ Date d; return 0;}
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
class Time{public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}private:int _hour;int _minute;int _second;};class Date{private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;};int main(){Date d;return 0;}
2) 析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
析构函数名是在类名前加上字符 ~。无参数无返回值类型。一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载对象生命周期结束时,C++编译系统系统自动调用析构函数关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
3) 拷贝构造
拷贝构造函数也是特殊的成员函数,其特征如下
拷贝构造函数是构造函数的一个重载形式拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。typedef int DataType;class Stack{public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}private:DataType* _array;size_t _size;size_t _capacity;};int main(){Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;}
这种情况是会崩溃的,因为同一块空间被释放了两次
4)运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字operator后面接需要重载的运算符符号。函数原型:返回值类型 operator操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@重载操作符必须有一个类类型参数用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this.* :: sizeof ?: .
这五类运算符是不能够被重载的,这个在笔试题中会经常性的出现比如我们重载一下Date类的==和<<,代码大概就像这样:
#include <iostream>using namespace std;class Date{friend ostream& operator << (ostream& out, const Date& d);friend bool operator==(const Date& d1, const Date& d2);public:Date(int year, int month, int day) :_year(year),_month(month),_day(day){ }~Date(){cout << "~Date()" << endl;}private:int _year;int _month;int _day;};bool operator==(const Date& d1, const Date& d2){return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;}ostream& operator << (ostream & out, const Date& d){out << d._year << d._month << d._day;return out;}int main(){Date d1(1, 1, 1),d2(2,2,2);cout << (d1 == d2) << endl;;cout << d1 << endl;return 0;}
赋值运算符重载格式:
参数类型:const T&,传递引用可以提高传参效率返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回*this :要复合连续赋值的含义#include <iostream>using namespace std;class Date{friend ostream& operator << (ostream& out, const Date& d);friend bool operator==(const Date& d1, const Date& d2);public:Date(int year, int month, int day) :_year(year),_month(month),_day(day){ }Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}~Date(){cout << "~Date()" << endl;}const Date& operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;return *this;}private:int _year;int _month;int _day;};bool operator==(const Date& d1, const Date& d2){return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;}ostream& operator << (ostream & out, const Date& d){out << d._year << d._month << d._day;return out;}int main(){Date d1(1, 1, 1),d2(2,2,2);cout << (d1 == d2) << endl;d1 = d2 = Date(3,3,3);cout << (d1 == d2) << endl;return 0;}
注意:
赋值运算符只能重载成类的成员函数不能重载成全局函数用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
这里提一嘴关于前置++和后置++的实现(–也是同理的)
#include <iostream>using namespace std;class Date{friend ostream& operator << (ostream& out, const Date& d);friend bool operator==(const Date& d1, const Date& d2);public:Date(int year, int month, int day) :_year(year),_month(month),_day(day){ }Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}Date& operator++()// 前置++{_day += 1; //这里为例简便,我就不考虑什么月份,天数什么的了}Date operator++(int) //后置++{Date temp(*this);_day += 1;return temp;}~Date(){cout << "~Date()" << endl;}const Date& operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;return *this;}private:int _year;int _month;int _day;};bool operator==(const Date& d1, const Date& d2){return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;}ostream& operator << (ostream & out, const Date& d){out << d._year << d._month << d._day;return out;}int main(){Date d1(1, 1, 1),d2(2,2,2);cout << (d1 == d2) << endl;d1 = d2 = Date(3,3,3);cout << (d1 == d2) << endl;return 0;}
5)const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
请思考下面的几个问题:
const对象可以调用非const成员函数吗?非const对象可以调用const成员函数吗?const成员函数内可以调用其它的非const成员函数吗?非const成员函数内可以调用其它的const成员函数吗?const对象不可以调用非const成员函数,非const对象可以调用const成员函数。const成员函数不可以调用其他的非const成员函数,非const成员函数可以调用const成员函数
6)取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!
7/8) 移动构造和移动赋值
(这两个成员函数放到一起进行讨论)
首先来介绍一下什么叫做左值引用,什么叫做右值引用,以及他们的区别是什么
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。什么是左值?什么是左值引用?左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
#include <iostream>using namespace std;int main(){int*p = new int(0);int b = 1;const int c = 2;int*&rp = p;int&pb = b;const int&pc = 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;}
左值引用与右值引用比较
左值引用总结:
#include <iostream>using namespace std;int main(){//int&a = 10;//这种是错误的const int&a = 10;//这种是正确的return 0;}
右值引用总结:
右值引用只能右值,不能引用左值。但是右值引用可以move以后的左值。来看看右值引用使用场景和意义
前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引
用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
namespace bit{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(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;}~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;}private:char *_str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};}
不妨先看看左值引用的应用场景:
void func1(bit::string s){}void func2(const bit::string& s){}int main(){ bit::string s1("hello world"); // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值 func1(s1); func2(s1); // string operator+=(char ch) 传值返回存在深拷贝 // string& operator+=(char ch) 传左值引用没有拷贝提高了效率 s1 += '!'; return 0;}
问:左值引用的短板是什么?
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
也就是说,只要是局部的对象需要返回,左值引用就是失去了作用,因为出作用域之后局部对象就会被销毁,当时候指向的就是一片位置的区域。
那么如果解决这个问题呢?
右值引用和移动语义解决上述问题:在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己(偷梁换柱)。
来看看怎么实现
// 移动构造string(string&& s) :_str(nullptr) ,_size(0) ,_capacity(0){ cout << "string(string&& s) -- 移动语义" << endl; swap(s);}
不仅仅有移动构造,还有移动赋值:在bit::string类中增加移动赋值函数,再去调用bit::to_string(1234),不过这次是将bit::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。
// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动语义" << endl;swap(s);return *this;}int main(){ bit::string ret1; ret1 = bit::to_string(1234); return 0;}// 运行结果:// string(string&& s) -- 移动语义// string& operator=(string&& s) -- 移动语义
这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。
右值引用引用左值及其一些更深入的使用场景分析
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
template<class _Ty>inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT{// forward _Arg as movable return ((typename remove_reference<_Ty>::type&&)_Arg);}
int main(){ bit::string s1("hello world"); // 这里s1是左值,调用的是拷贝构造 bit::string s2(s1); // 这里我们把s1 move处理以后, 会被当成右值,调用移动构造 // 但是这里要注意,一般是不要这样用的,因为我们会发现s1的 // 资源被转移给了s3,s1被置空了。 bit::string s3(std::move(s1)); return 0;}
完美转发(模板中&&的万能引用)
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;}
std::forward 完美转发在传参的过程中保留对象原生类型属性
默认成员函数
原来的C++有6个默认成员函数
1. 构造函数2. 析构函数3. 拷贝构造函数4. 拷贝赋值重载5. 取地址重载6. const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类 型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造。如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内 置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造 完全类似)如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这 个我们在雷和对象默认就讲了,这里就不再细讲了。
强制生成默认函数的关键字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(Person&& p) = default;private:std::string _name;int _age;};int main(){Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;}
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person{public:Person(const char* name = "", int age = 0):_name(name), _age(age){}Person(const Person& p) = delete;private:std::string _name;int _age;};int main(){Person s1;Person s2 = s1; //拷贝构造被删除了Person s3 = std::move(s1); return 0;}
最后提一嘴 可变参数模板
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改 进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。
// Args是一个模板参数包,args是一个函数形参参数包// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。template <class ...Args>void ShowList(Args... args){}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数
包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,
只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特
点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变
参数,所以我们的用一些奇招来一一获取参数包的值。
递归函数方式展开参数包
// 递归终止函数template <class T>void ShowList(const T& t){cout << t << endl;}// 展开函数template <class T, class ...Args>void ShowList(T value, Args... args){cout << value << " ";ShowList(args...);}int main(){ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;}
都到这个份上了,干脆把push和emplace一块看了
template <class... Args>void emplace_back (Args&&... args);
首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和 emplace系列接口的优势到底在哪里呢?
int main(){ std::list< std::pair<int, char> > mylist; // emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象 // 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别 mylist.emplace_back(10, 'a'); mylist.emplace_back(20, 'b'); mylist.emplace_back(make_pair(30, 'c')); mylist.push_back(make_pair(40, 'd')); mylist.push_back({ 50, 'e' }); for (auto e : mylist) cout << e.first << ":" << e.second << endl; return 0;}
int main(){// 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back// 是先构造,再移动构造,其实也还好。 std::list< std::pair<int, bit::string> > mylist; mylist.emplace_back(10, "sort"); mylist.emplace_back(make_pair(20, "sort")); mylist.push_back(make_pair(30, "sort")); mylist.push_back({ 40, "sort"}); return 0;}
得出尽量使用emplace系列