系列文章目录
文章目录
- 系列文章目录
- 一、新的类功能
- 默认成员函数
- 类成员变量初始化
- 强制生成默认函数的关键字default
- 禁止生成默认函数的关键字delete
- 继承和多态中的final与override关键字
- 二、可变参数模板
- 递归函数方式展开参数包
- 逗号表达式展开参数包
- STL容器中的empalce_back与push_back的区别
一、新的类功能
默认成员函数
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的
C++11 新增了两个:移动构造函数和移动赋值运算符重载
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
我们用之前自实现的string类来观察上述规律
namespace Tlzns
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){_str = new char[_capacity + 1];strcpy(_str, str);}void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 移动构造string(string&& s){swap(s);cout << "string(string&& s) -- 移动构造" << endl;}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}// 移动赋值string& operator=(string&& s){swap(s);cout << "string& operator=(string&& s) -- 移动赋值" << endl;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){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;};
}
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(){}
private:Tlzns::string _name;int _age;
};
int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}
在Person类没有实现移动构造函数,且实现析构函数 、拷贝构造、拷贝赋值重载的情况下
注:自实现的string类中已经支持移动构造和移动赋值重载
我们将Person类中实现的析构函数 、拷贝构造、拷贝赋值重载都注释掉后
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(){}*/
private:Tlzns::string _name;int _age;
};
可以观察到编译器默认生成了移动构造和移动赋值重载
类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化
class A
{
public:~A() {}
private:int _a = 1;// 缺省值Tlzns::string _s = "aaa";// 缺省值
};
这里的缺省值会在构造/拷贝构造的初始化列表使用:
如果在构造/拷贝构造有这两个的初始化就不会走缺省值,谁不在初始化列表初始化就走谁的缺省值,相当于是一层额外的保险,如果初始化列表没有,就会走这里获得缺省值
强制生成默认函数的关键字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(Person&& p) = default;Person& operator= (Person&& p) = default;private:Tlzns::string _name;int _age;
};
将移动拷贝和移动赋值重载函数加上default,这样即使实现了析构函数 、拷贝构造、拷贝赋值重载,编译器也会默认生成
禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成
在C++98中,是该函数设置成private,并且只声明不定义,这样只要其他人想要调用就会报错
在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数
继承和多态中的final与override关键字
继承和多态中的内容
在C++中,final关键字可以用于类和成员函数。当用于类时,它表示该类不能被继承(Inheritance)。当用于成员函数时,它表示该成员函数不能在子类中被覆盖(Overriding)
如果派生类在虚函数声明时使用了override描述符,那么该函数必须重载其基类中的同名函数,否则代码将无法通过编译
二、可变参数模板
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数
递归函数方式展开参数包
// 递归终止函数
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;
}
逗号表达式展开参数包
template <class T>
void PrintArg(T t)
{cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(args), 0)... };cout << endl;
}
int main()
{ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数,这种就地展开参数包的方式实现的关键是逗号表达式,我们知道逗号表达式会按顺序执行逗号前面的表达式
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组,{(printarg(args), 0)…}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)],由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
STL容器中的empalce_back与push_back的区别
template <class... Args>
void emplace_back (Args&&... args);
我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用
而有这样的设计的empalce_back与push_back的区别在哪呢
- 构造函数的使用:
emplace_back 可以直接将构造函数所需的参数传递过去,然后构建一个新的对象并将其填充到容器的尾部,它不会立即拷贝或移动新对象的副本,而是直接在容器尾部创建该对象,只调用了一次构造函数。push_back 首先会利用传入的参数调用构造函数构造一个临时的对象,然后将这个临时对象拷贝或移动到容器中,这个过程涉及到拷贝构造函数的使用,可能会导致额外的性能开销 - 效率问题:
使用 emplace_back 可以更好地避免内存的拷贝和移动,从而提升容器插入元素的性能,如果容器的大小接近于某个特定的数值(如1, 2, 4, 8等),每次扩容都需要从头开始复制所有元素,这可能导致效率降低。在这种情况下,emplace_back 由于是在容器末尾直接创建新对象,因此不会有这样的问题 - 支持的功能:
emplace_back 可以接受多个构造参数,并且支持原地构造,push_back 支持右值引用,但不支持传入多个构造参数,且总是会进行拷贝构造
综上所述,emplace_back 在效率和某些功能上优于 push_back,尤其是在处理大型容器时,因为它避免了不必要的拷贝和移动操作