当前位置: 首页 > news >正文

c++11(类的新功能与可变参数模板)

8 新的类功能

默认成员函数

原来C++类中,有6个默认成员函数:

1. 构造函数

2. 析构函数

3. 拷贝构造函数

4. 拷贝赋值重载

5. 取地址重载

6. const 取地址重载

重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。 C++11 新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

如果你没有自己实现移动构造函数且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类 型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造。

如果你没有自己实现移动赋值重载函数,且没默认生成的移动构造函数,对于内 置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值。(默认移动赋值跟上面移动构造 完全类似) 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

默认生成的移动构造和移动赋值会做什么?

默认生成的移动构造函数:对于内置类型的成员会完成值拷贝(浅拷贝),对于自定义类型的成员,如果该成员实现了移动构造就调用它的移动构造,否则就调用它的拷贝构造。
默认生成的移动赋值重载函数:对于内置类型的成员会完成值拷贝(浅拷贝),对于自定义类型的成员,如果该成员实现了移动赋值就调用它的移动赋值,否则就调用它的拷贝赋值。

验证默认生成的移动构造和移动赋值所做的工作.模拟实现一个简化版的string类,类当中只编写了几个我们需要用到的成员函数。

namespace cl { class string { public: //构造函数 string(const char* str = "") { _size = strlen(str); //初始时,字符串大小设置为字符串长度 _capacity = _size; //初始时,字符串容量设置为字符串长度 _str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0') strcpy(_str, str); //将C字符串拷贝到已开好的空间 } //交换两个对象的数据 void swap(string& s) { //调用库里的swap ::swap(_str, s._str); //交换两个对象的C字符串 ::swap(_size, s._size); //交换两个对象的大小 ::swap(_capacity, s._capacity); //交换两个对象的容量 } //拷贝构造函数(现代写法) string(const string& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(const string& s) -- 深拷贝" << endl; string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象 swap(tmp); //交换这两个对象 } //移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 移动构造" << endl; swap(s); } //拷贝赋值函数(现代写法) string& operator=(const string& s) { cout << "string& operator=(const string& s) -- 深拷贝" << endl; string tmp(s); //用s拷贝构造出对象tmp swap(tmp); //交换这两个对象 return *this; //返回左值(支持连续赋值) } //移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动赋值" << endl; swap(s); return *this; } //析构函数 ~string() { //delete[] _str; //释放_str指向的空间 _str = nullptr; //及时置空,防止非法访问 _size = 0; //大小置0 _capacity = 0; //容量置0 } private: char* _str; size_t _size; size_t _capacity; }; }

再编写一个简单的Person类,Person类中的成员name的类型就是我们模拟实现的string类。

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: cl::string _name; //姓名 int _age; //年龄 };

Person类当中没有实现移动构造和移动赋值,但拷贝构造、拷贝赋值和析构函数Person类都实现了,因此Person类中不会生成默认的移动构造和移动赋值,可以通过下面的代码来验证:

int main() { Person s1("张三", 21); Person s2 = std::move(s1); //想要调用Person默认生成的移动构造 return 0; }

用一个右值去构造s2对象,但由于Person类没有生成默认的移动构造函数,因此这里会调用Person的拷贝构造函数(拷贝构造既能接收左值也能接收右值),这时在Person的拷贝构造函数中就会调用string的拷贝构造函数对name成员进行深拷贝。

如果要让Person类生成默认的移动构造函数,就必须将Person类中的拷贝构造、拷贝赋值和析构函数全部注释掉,这时用右值去构造s2对象时就会调用Person默认生成的移动构造函数。

Person默认生成的移动构造,对于内置类型成员age会进行值拷贝,而对于自定义类型成员name,因为我们的string类实现了移动构造函数,因此它会调用string的移动构造函数进行资源的转移。
而如果我们将string类当中的移动构造函数注释掉,那么Person默认生成的移动构造函数,就会调用string类中的拷贝构造函数对name成员进行深拷贝。
要验证Person类中默认生成的移动赋值函数可以用下面的代码,验证方式和上面验证移动构造的方式是一样的。

int main() { Person s1("张三", 21); Person s2; s2 = std::move(s1); //想要调用Person默认生成的移动赋值 return 0; }

说明一下:

我们在模拟实现的string类的拷贝构造、拷贝赋值、移动构造和移动赋值函数中都打印了一条提示语句,因此可以通过控制台输出判断是否调用了对应的函数。
由于VS2013没有完全支持C++11,因此上述代码无法在VS2013当中验证,需要使用更新一点的编译器进行验证,比如VS2019。

类成员变量初始化

默认生成的构造函数,对于自定义类型的成员会调用其构造函数进行初始化,但并不会对内置类型的成员进行处理。于是C++11支持非静态成员变量在声明时进行初始化赋值,默认生成的构造函数会使用这些缺省值对成员进行初始化。(这里不是初始化,而是给声明的成员变量一个缺省值。)比如:

class Person { public: //... private: //非静态成员变量,可以在成员声明时给缺省值 cl::string _name = "张三"; //姓名 int _age = 20; //年龄 static int _n; //静态成员变量不能给缺省值 };

强制生成默认函数的关键字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: bit::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: bit::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); return 0; }

继承和多态中的final与override关键字

这个在这前的继承多态就讲过了,这里随便提一下

被final修饰的类叫做最终类,最终类无法被继承。比如:

class NonInherit final //被final修饰,该类不能再被继承 { //... };

final修饰虚函数,表示该虚函数不能再被重写,如果子类继承后重写了该虚函数则编译报错。比如:

//父类 class Person { public: virtual void Print() final //被final修饰,该虚函数不能再被重写 { cout << "hello Person" << endl; } }; //子类 class Student : public Person { public: virtual void Print() //重写,编译报错 { cout << "hello Student" << endl; } };

override修饰子类的虚函数,检查子类是否重写了父类的某个虚函数,如果没有没有重写则编译报错。比如:

//父类 class Person { public: virtual void Print() { cout << "hello Person" << endl; } }; //子类 class Student : public Person { public: virtual void Print() override //检查子类是否重写了父类的某个虚函数 { cout << "hello Student" << endl; } };

可变参数模板的概念


可变参数模板是C++11新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接受可变参数的函数模板和类模板。

在C++11之前,类模板和函数模板中只能包含固定数量的模板参数,可变模板参数无疑是一个巨大的改进,但由于可变参数模板比较抽象,因此使用起来需要一定的技巧。
在C++11之前其实也有可变参数的概念,比如printf函数就能够接收任意多个参数,但这是函数参数的可变参数,并不是模板的可变参数。

函数的可变参数模板定义方式如下:

template<class …Args> 返回类型 函数名(Args… args) { //函数体 }
template<class ...Args> void ShowList(Args... args) {}


模板参数Args前面有省略号,代表它是一个可变模板参数,我们把带省略号的参数称为参数包,参数包里面可以包含0到N ( N ≥ 0 ) N(N\geq 0)N(N≥0)个模板参数而args则是一个函数形参参数包。
模板参数包Args和函数形参参数包args的名字可以任意指定,并不是说必须叫做Args和args。
现在调用ShowList函数时就可以传入任意多个参数了,并且这些参数可以是不同类型的。比如:

int main() { ShowList(); ShowList(1); ShowList(1, 'A'); ShowList(1, 'A', string("hello")); return 0; }

在函数模板中通过sizeof计算参数包中参数的个数。比如:

template<class ...Args> void ShowList(Args... args) { cout << sizeof...(args) << endl; //获取参数包中参数的个数 }

但是我们无法直接获取参数包中的每个参数,只能通过展开参数包的方式来获取,这是使用可变参数模板的一个主要特点,也是最大的难点。

特别注意,语法并不支持使用args[i]的方式来获取参数包中的参数。比如:

template<class ...Args> void ShowList(Args... args) { //错误示例: for (int i = 0; i < sizeof...(args); i++) { cout << args[i] << " "; //打印参数包中的每个参数 } cout << endl; }

要获取参数包中的各个参数,只能通过展开参数包的方式来获取,一般我们会通过递归或逗号表达式来展开参数包。

递归展开参数包

递归展开参数包的方式如下:

  • 给函数模板增加一个模板参数,这样就可以从接收到的参数包中分离出一个参数出来。
  • 在函数模板中递归调用该函数模板,调用时传入剩下的参数包。
  • 如此递归下去,每次分离出参数包中的一个参数,直到参数包中的所有参数都被取出来。

比如我们要打印调用函数时传入的各个参数,那么函数模板可以这样编写:

//展开函数 template<class T, class ...Args> void ShowList(T value, Args... args) { cout << value << " "; //打印分离出的第一个参数 ShowList(args...); //递归调用,将参数包继续向下传 }

我们可以在刚才的基础上,再编写一个无参的递归终止函数,该函数的函数名与展开函数的函数名相同。如下:

//递归终止函数 void ShowList() { cout << endl; } //展开函数 template<class T, class ...Args> void ShowList(T value, Args... args) { cout << value << " "; //打印分离出的第一个参数 ShowList(args...); //递归调用,将参数包继续向下传 }

逗号表达式展开参数包

这种展开参数包的方式,不需要通过递归终止函数,是直接在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数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在 数组构造的过程展开参数包

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

STL容器中的emplace相关接口函数

cplusplus.com/reference/vector/vector/emplace_back/

cplusplus.com/reference/list/list/emplace_back/

template <class... Args> void emplace_back (Args&&... args);

C++11标准给STL中的容器增加emplace版本的插入接口,比如list容器的push_front、push_back和insert函数,都增加了对应的emplace_front、emplace_back和emplace函数。如下:

这些emplace版本的插入接口支持模板的可变参数,比如list容器的emplace_back函数的声明如下:

emplace系列接口的使用方式

emplace系列接口的使用方式与容器原有的插入接口的使用方式类似,但又有一些不同之处。

以list容器的emplace_back和push_back为例:

调用push_back函数插入元素时,可以传入左值对象或者右值对象,也可以使用列表进行初始化。
调用emplace_back函数插入元素时,也可以传入左值对象或者右值对象,但不可以使用列表进行初始化。
除此之外,emplace系列接口最大的特点就是,插入元素时可以传入用于构造元素的参数包。
比如:

int main() { list<pair<int, string>> mylist; pair<int, string> kv(10, "111"); mylist.push_back(kv); //传左值 mylist.push_back(pair<int, string>(20, "222")); //传右值 mylist.push_back({ 30, "333" }); //列表初始化 mylist.emplace_back(kv); //传左值 mylist.emplace_back(pair<int, string>(40, "444")); //传右值 mylist.emplace_back(50, "555"); //传参数包 return 0; }

emplace系列接口的工作流程

emplace系列接口的工作流程如下:

先通过空间配置器为新结点获取一块内存空间,注意这里只会开辟空间,不会自动调用构造函数对这块空间进行初始化。
然后调用allocator_traits::construct函数对这块空间进行初始化,调用该函数时会传入这块空间的地址和用户传入的参数(需要经过完美转发)。
在allocator_traits::construct函数中会使用定位new表达式,显示调用构造函数对这块空间进行初始化,调用构造函数时会传入用户传入的参数(需要经过完美转发)。
将初始化好的新结点插入到对应的数据结构当中,比如list容器就是将新结点插入到底层的双链表中

emplace系列接口的意义

由于emplace系列接口的可变模板参数的类型都是万能引用,因此既可以接收左值对象,也可以接收右值对象,还可以接收参数包。

如果调用emplace系列接口时传入的是左值对象,那么首先需要先在此之前调用构造函数实例化出一个左值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,会匹配到拷贝构造函数。
如果调用emplace系列接口时传入的是右值对象,那么就需要在此之前调用构造函数实例化出一个右值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,就会匹配到移动构造函数。
如果调用emplace系列接口时传入的是参数包,那就可以直接调用函数进行插入,并且最终在使用定位new表达式调用构造函数对空间进行初始化时,匹配到的是构造函数。

总结一下:

传入左值对象,需要调用构造函数+拷贝构造函数。
传入右值对象,需要调用构造函数+移动构造函数。
传入参数包,只需要调用构造函数。
当然,这里的前提是容器中存储的元素所对应的类,是一个需要深拷贝的类,并且该类实现了移动构造函数。否则调用emplace系列接口时,传入左值对象和传入右值对象的效果都是一样的,都需要调用一次构造函数和一次拷贝构造函数。

实际emplace系列接口的一部分功能和原有各个容器插入接口是重叠的,因为容器原有的push_back、push_front和insert函数也提供了右值引用版本的接口,如果调用这些接口时如果传入的是右值对象,那么最终也是会调用对应的移动构造函数进行资源的移动的。

emplace接口的意义:

emplace系列接口最大的特点就是支持传入参数包,用这些参数包直接构造出对象,这样就能减少一次拷贝,这就是为什么有人说emplace系列接口更高效的原因。
但emplace系列接口并不是在所有场景下都比原有的插入接口高效,如果传入的是左值对象或右值对象,那么emplace系列接口的效率其实和原有的插入接口的效率是一样的。
emplace系列接口真正高效的情况是传入参数包的时候,直接通过参数包构造出对象,避免了中途的一次拷贝。

如果要验证我验证
们上述对emplace系列接口的说法,需要借助一个深拷贝的类,下面模拟实现了一个简化版的string类,类当中只编写了我们需要用到的成员函数。

namespace cl { class string { public: //构造函数 string(const char* str = "") { cout << "string(const char* str) -- 构造函数" << endl; _size = strlen(str); //初始时,字符串大小设置为字符串长度 _capacity = _size; //初始时,字符串容量设置为字符串长度 _str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0') strcpy(_str, str); //将C字符串拷贝到已开好的空间 } //交换两个对象的数据 void swap(string& s) { //调用库里的swap ::swap(_str, s._str); //交换两个对象的C字符串 ::swap(_size, s._size); //交换两个对象的大小 ::swap(_capacity, s._capacity); //交换两个对象的容量 } //拷贝构造函数(现代写法) string(const string& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(const string& s) -- 拷贝构造" << endl; string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象 swap(tmp); //交换这两个对象 } //移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 移动构造" << endl; swap(s); } //拷贝赋值函数(现代写法) string& operator=(const string& s) { cout << "string& operator=(const string& s) -- 深拷贝" << endl; string tmp(s); //用s拷贝构造出对象tmp swap(tmp); //交换这两个对象 return *this; //返回左值(支持连续赋值) } //移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动赋值" << endl; swap(s); return *this; } //析构函数 ~string() { //delete[] _str; //释放_str指向的空间 _str = nullptr; //及时置空,防止非法访问 _size = 0; //大小置0 _capacity = 0; //容量置0 } private: char* _str; size_t _size; size_t _capacity; }; }

由于我们在string的构造函数、拷贝构造函数和移动构造函数当中均打印了一条提示语句,因此我们可以通过控制台输出来判断这些函数是否被调用。

下面我们用一个容器来存储模拟实现的string,并以不同的传参形式调用emplace系列函数。比如:

int main() { list<pair<int, cl::string>> mylist; pair<int, cl::string> kv(1, "one"); mylist.emplace_back(kv); //传左值 cout << endl; mylist.emplace_back(pair<int, cl::string>(2, "two")); //传右值 cout << endl; mylist.emplace_back(3, "three"); //传参数包 return 0; }

运行结果如下:

说明一下:

模拟实现string的拷贝构造函数时复用了构造函数,因此在调用string拷贝构造的后面会紧跟着调用一次构造函数。
为了更好的体现出参数包的概念,因此这里list容器中存储的元素类型是pair,我们是通过观察string对象的处理过程来判断pair的处理过程的。
这里也可以以不同的传参方式调用push_back函数,顺便验证一下容器原有的插入函数的执行逻辑。比如:

int main() { list<pair<int, cl::string>> mylist; pair<int, cl::string> kv(1, "one"); mylist.push_back(kv); //传左值 cout << endl; mylist.push_back(pair<int, cl::string>(2, "two")); //传右值 cout << endl; mylist.push_back({ 3, "three" }); //列表初始化 return 0; }

http://www.jsqmd.com/news/900987/

相关文章:

  • 2026电子行业ERP精选推荐榜:覆盖电子元器件/PCBA加工/SMT贴片/FPC柔性版/线束连接器/开关电源等PLM+MES一体化智能管理系统厂家 - 品牌企业推荐师(官方)
  • ChatGPT危机公关不是“发声明”,而是“重写信任契约”:独家披露头部金融/医疗/教育行业已验证的6维可信度重建框架
  • 2026年半导体展详细介绍,简单直白筛选合适行业展会 - 品牌2025
  • 5分钟从零到一:Deep-Live-Cam实时AI换脸系统的终极实践指南
  • 如何15分钟完成OpenCore EFI配置:终极黑苹果自动化工具指南
  • 对比直接使用官方 API 与通过 Taotoken 调用的便捷性差异
  • 帐篷厂家推荐就选山东春和!户外遮阳帐篷实力厂家,支持四角广告展销帐篷定制,出口品质,厂家直供 - 栗子测评
  • 2026年 广州不锈钢水泵厂家实力排行:不锈钢离心泵、多级泵、自吸泵、增压泵、排污泵、卧式离心泵、304水泵品牌推荐 - 品牌企业推荐师(官方)
  • 从通用AI到定制化LLM系统:架构解析与工程实践指南
  • 3天内让简历进入面试池!ChatGPT+ATS兼容性校验模板(含12家头部企业JD解析规则库)限时开放
  • 2026年靠谱的均质机饮料设备回收/杀菌机饮料设备回收/灌装机饮料设备回收/梁山贴标机饮料设备回收厂家选择推荐 - 品牌宣传支持者
  • 数字记忆守护者:用WeChatMsg将微信对话转化为永恒数字资产
  • 2026年靠谱的理瓶机二手饮料设备/二手饮料设备储罐优质公司推荐 - 品牌宣传支持者
  • AiVIS:视觉智能解析引擎,让AI精准读懂网页内容
  • 鸣潮自动化终极指南:解放双手的智能游戏助手完整教程
  • 还在找气雾罐空气清新剂定制厂家?山东铭赫支持来样定制,从配方到罐装一站式搞定,专属香型打造你的独家记忆 - 栗子测评
  • TCI Toolkit:为持久化LLM智能体构建可观测性与稳定性监控仪表盘
  • 如何用WeChatMsg打造个人数字记忆库:完全免费的本地数据守护方案
  • 基于Claude Agent SDK构建具备自我修复能力的AI内容代理系统
  • Citra模拟器终极指南:3步在Windows、macOS和Linux上畅玩任天堂3DS游戏 [特殊字符]
  • ESP32开发环境终极配置指南:告别安装烦恼的完整解决方案
  • 2026年热门的双玻玻璃隔断/玻璃隔断/单玻玻璃隔断/办公室玻璃隔断厂家推荐与选型指南 - 行业平台推荐
  • 【ChatGPT婚礼策划辅助实战指南】:20年婚庆技术顾问亲授5大高转化AI协同工作流
  • 华为存储DeviceManager密码策略怎么设?教你关闭密码过期,避免定期重置的麻烦
  • 深度解析ResNet-50 v1.5架构:为什么它比原始版本更准确?
  • Video2X终极指南:3大核心技术实现视频超分辨率与帧插值快速处理
  • 杯子厂家只推这一家!山东杯精灵:双层玻璃杯源头工厂、临沂定制玻璃杯厂家哪家好,答案在这里,批发更优惠 - 栗子测评
  • 2026年宝钢镀锌HC700/980DHD+Z吉帕钢推荐榜:超强镀锌板/汽车用高强钢/轻量化热成型钢厂家实力解析 - 品牌企业推荐师(官方)
  • 2026年知名的铝合金玻璃隔断/青岛全钢玻璃隔断/青岛单层玻璃隔断/百叶玻璃隔断可靠供应商推荐 - 品牌宣传支持者
  • 2026年球阀厂家推荐排行榜:不锈钢球阀/碳钢球阀/美标球阀/法兰球阀/丝扣球阀/NPT球阀/保温球阀/夹套球阀/三通球阀定制优选 - 品牌企业推荐师(官方)