c++11特性——新的类功能、STL的变化
引入
上一篇文章中,我们专门针对于c++11中的可变参数模板进行了讲解。包括基本语法,使用,注意事项、包扩展和解析。最重要的就是讲解了emplace系列的接口。
这篇文章中,我们将引入一些新的内容:
比如c++11引入了右值引用和移动语义,类的构造和赋值有了新的方式:移动构造和移动赋值。这些其实也算是类的默认成员函数。但是它的行为是怎样的呢?这需要去讲解。
还有就是针对于c++11的STL库,引入了一些新的容器,增加了一些新的语法。
还有比较用的多的就是lambda对象和包装器的内容。
当然这些内容现在来说怎么看都是一脸懵的。但是不要着急,我们后面一个一个地讲。
新的类功能
我们先来看本篇文章地第一个部分——类的新功能。
新的默认成员函数
c++11引入了右值引用和移动语义,本意是为了大大地减少拷贝。当传递的数据是右值的时候,会调用移动构造。传址返回时进行特殊处理,调用移动构造。
在c++98的语法中,我们了解到对于一个类而言,是有6个默认成员函数的:
分别是默认构造、析构、拷贝构造、拷贝赋值重载、普通对象和const对象取地址运算符重载。
只不过说,对于取地址重载这个行为绝大部分情况下是不需要我们去做的。我们不写编译器会自己生成的。但是前面四个是需要看情况而定的。这里对以往的知识做一些复习。
但是进入c++11后,引入了新的两个构造函数——移动构造和移动赋值。那么它们的行为是如何呢?什么时候会自动生成?什么时候不需要写?这些让我们一起来探讨一下。
直接来看规则:
- 如果没有自行实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上⾯移动构造完全类似)。
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
移动构造和移动赋值重载的逻辑还是很像的,我们实现过就知道,本质就是掠夺资源。所以行为基本上是一样的。只不过针对的场景不一样。
在没有实现移动构造/移动赋值重载的情况下,还不能实现析构、拷贝构造、拷贝赋值重载的任意一个的情况下,编译器才会自动生成一个移动构造/移动赋值重载。这个规则相比于其他的默认成员函数可就复杂的多。
其实很好理解。什么时候需要自行实现析构函数——有指向资源的时候。那么要自己写析构函数的时候,必然是深拷贝的数据类型,那必然要写拷贝构造和拷贝赋值。所以它们三个看着像是成对出来的。实现了拷贝构造,其实就可以让程序正常运行了。只不过说,对于一些指向资源很多的类型,效率会比较低。效率低不等于不行。
所以编译器就会认为,当前已经有这些函数了,所以不用移动版本的接口也是可以的。只不过效率没那么高。如果想提高效率就自己写一份移动版本的接口就可以了。
反过来发现,自行实现了移动构造/移动赋值,那么拷贝版本的编译器就不会自行实现了。这是需要我们及其注意的。
举例验证
这个部分将找几个测试用例测试一下上面说的对不对。
首先为了能够验证成功,需要使用一下上节课改进后的string。这里再次展示一下:
//自行模拟实现的string
#include<iostream>
#include<assert.h>
#include<string.h>
using namespace std;
namespace my {
class string{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin(){
return _str;
}
iterator end(){
return _str + _size;
}
const_iterator begin() const{
return _str;
}
const_iterator end() const{
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);
}
void swap(string& s){
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s){
push_back(ch);
}
}
// 移动构造
string(string&& s){
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
string& operator=(const string& s){
cout << "string& operator=(const string& s) -- 拷贝赋值" <<
endl;
if (this != &s){
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s){
push_back(ch);
}
}
return *this;
}
// 移动赋值
string& operator=(string&& s){
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string(){
//cout << "~string() -- 析构" << endl;
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];
if (_str)
{
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;
}
size_t size() const{
return _size;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
第一种情况:不实现析构、拷贝赋值和拷贝构造、以便使用默认的移动赋值和移动构造;删除自定义类型的移动构造和移动赋值,自定义类型会调用拷贝构造。
class Person {
public:
//默认构造
Person(const char* name = "zhangsan", int age = 0)
:_name(name),
_age(age)
{
cout << "Person(const char* name, int age) 默认构造" << endl;
}
void Print() {
cout << _name.c_str() << " " << _age << endl;
}
private:
my::string _name;
int _age;
};
int main() {
//没有实现析构 拷贝构造 拷贝赋值
//调用默认的移动构造和移动赋值
Person p1;
cout << "person p1: ";
p1.Print();
cout << "-----------------------------------------" << endl;
//默认的移动构造 对于内置类型 直接值拷贝
//对于自定义类型 如果实现了移动构造 就调用它的移动构造
//反之调用它的拷贝构造
//如果没有实现拷贝构造就调用这个自定义类型的默认的拷贝构造(浅拷贝)
Person p2 = move(p1);
cout << "person p2: ";
p2.Print();
cout << "-----------------------------------------" << endl;
//默认的移动赋值
//移动构造类似
//如果自定义类型没有移动赋值 就调用拷贝赋值
//拷贝赋值都没有只能调用它的默认生成的拷贝赋值(浅拷贝)
Person p3;
p3 = move(p2);
cout << "person p3: ";
p3.Print();
cout << "-----------------------------------------" << endl;
return 0;
}
我们自行实现一个叫Person的类。我们按照要求去使用默认的移动构造和移动赋值,我们来看看上面这段代码输出的结果:

重点看第二个和第三个部分。第二个部分使用了默认的移动构造。my::string是自定义类型,调用了它的移动构造。int是内置类型,直接进行值拷贝。第三部分是使用了默认的移动赋值。一样的,my::string调用了它的移动构造,int直接进行值拷贝了。
现在把my::string中实现的移动赋值和移动构造删掉看看:

我们再次运行程序,看看输出结果是什么:

很明显,调用的是拷贝系列了。
第二种情况:实现拷贝构造、析构、拷贝赋值的任意一个,看看还能不能用默认的移动构造和移动赋值:
class Person {
public:
//默认构造
Person(const char* name = "zhangsan", int age = 0)
:_name(name),
_age(age)
{
cout << "Person(const char* name, int age) 默认构造" << endl;
}
Person(const Person& per)
:_name(per._name),
_age(per._age)
{}
void Print() {
cout << _name.c_str() << " " << _age << endl;
}
private:
my::string _name;
int _age;
};
int main() {
Person p1;
cout << "person p1: ";
p1.Print();
cout << "-----------------------------------------" << endl;
Person p2 = move(p1);
cout << "person p2: ";
p2.Print();
cout << "-----------------------------------------" << endl;
Person p3;
p3 = move(p2);
cout << "person p3: ";
p3.Print();
cout << "-----------------------------------------" << endl;
return 0;
}
这段代码的运行结果:

最后调用的全是拷贝构造,为什么?
我们先看第二个部分,把p1move成右值后,正常应该是调用移动构造,构造给p2的。但是调用的是拷贝构造。为什么?因为我们写的拷贝构造参数是const Person& per,const左值引用可以接收右值。所以走的是这个。所以Person p2 = move(p1)这整个表达式走的其实是Person的拷贝构造。
很多人可能会觉得有没有可能是走移动构造,但只不过是my::string没有移动系列的构造和赋值呢?不是的,这里我已经恢复了那两个接口了。要想验证很简单,把Person的拷贝构造参数中的const删掉试试看:

立马报错。可见确实是写了拷贝构造就没有自行生成移动构造了。
当然也可以通过调试看,方法很多,能明白原理就可以。
至于其它的情况就不进行演示了,本质都是换汤不换药。
第三种情况:自行生成移动构造/移动赋值,就不会生成拷贝构造和拷贝赋值:
class Person {
public:
//默认构造
Person(const char* name = "zhangsan", int age = 0)
:_name(name),
_age(age)
{
cout << "Person(const char* name, int age) 默认构造" << endl;
}
//移动构造
Person(Person&& per) {
std::swap(_age, per._age);
_name.swap(per._name);
}
//移动赋值
Person& operator=(Person&& per) {
_name = per._name;
_age = per._age;
return *this;
}
void Print() {
cout << _name.c_str() << " " << _age << endl;
}
private:
my::string _name;
int _age;
};
int main() {
Person p1;
cout << "person p1: ";
p1.Print();
cout << "-----------------------------------------" << endl;
// C2280 Person::Person(const Person&) 尝试引用已删除的函数
//Person p2 = p1;
//移动构造
Person p2 = move(p1);
cout << "-----------------------------------------" << endl;
Person p3;
// C2280 Person::operator=(const Person&) 尝试引用已删除的函数
//p3 = p2;
//移动赋值
p3 = move(p2);
cout << "-----------------------------------------" << endl;
return 0;
}
测试代码就放在这里了。其实道理就是上面讲的规则。对号入座验证就可以了。我在这里就不过多赘述了。感兴趣的读者可以自行验证。
成员变量声名时给缺省值
成员变量声明时给缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表用这个缺省值初始化。这个在学习类和对象的时候就已经讲过。这里再复习一下。
缺省值其实就是默认值。在c++11后,可以使用赋值运算符=把一个默认值赋值给成员变量。但是这个变量是给函数的初始化列表用的。
之前讲过,所有的成员变量都会先走初始化列表。如果有传值,那么就用传入的值。如果没有显示在初始化列表的,对于内置类型,先看是否有缺省值,如果有用缺省,反之是随机值。对于自定义类型,会调用默认构造。如果没有默认构造就报错。
这些是在类和对象的时候学习的,如果忘记了就去复习一下。
defult和delete
在刚开始学习类和对象的时候,对于默认构造这个地方是最难理解的。
默认构造——不传参就能调用的构造函数。但是如果类种写了任意一种构造函数,那么编译器就不会再生成默认构造。有时候经常会报错。所以c++11引入了一个新的关键字default,用于强制生成对应的构造函数(编译器默认生成的那个版本)。
其实早就已经用过了,比如现在有个红黑树RBTree。不想写默认构造的时候我们可以这样写:
RBTree() = defalut;,使用default进行强制生成。当然强制生成移动构造和拷贝构造也是可以的:
Person(const Person& per) = default;
Person(Person&& per) = default;
建议是要强制生成就两个一起强制生成。因为前面讲过,只要写了拷贝构造就不会有默认的移动构造。写了移动构造就不会有默认的拷贝构造。可以把他们当作成对出现的。
再来看关键字delete,这个可以能想限制某些默认函数的生成。比如,在有些情况下,是不希望这个数据类型可以进行拷贝的。这个类型是有的,而且我们总是接触。就是输入流istream和输出流ostream这两个类。它们是不希望能够被拷贝的。
可以直接这样用:ostream(const ostream& os) = delete;,这样子从类外面看相当于这个函数贝删除了。但是类里面我们又不能再写一个一模一样的在类里面。这就限制了输出流ostream贝拷贝的情况。
注意这里的default和delete这两种用法只能在c++11之后的标准使用。如果编译器支持的标准是c++98的话是用不了的。那么c++98以前怎么限制某个默认函数不被生成呢?
c++98的做法是把这个函数限定在类的private域内,而且里面什么也不写,或者声名一些补丁在里面。一旦外界进行拷贝构造的调用就会报错。
final和override
这个我们在将类的继承和多态的时候就见识过。
final,中文意思有最后的,最终的意思。它主要放在两个部分使用:
- 在类的声名后面加入一个final,意为这个类是最终类,是无法继承的。
也就是说,如果有一个类不想被继承,那么就直接对这个类加以final关键字进行限定。 - 在多态的讲解中,涉及到一个虚函数重写的概念。重写就是对函数体部分(里面的内容),在子类中继承后进行重写,但是参数部分保留不变。但是如果想要控制某个虚函数不被重写的话,那就可以在虚函数后面加上final,这样子类重写这个虚函数就会报错了。
对于override,是用来检测是否进行虚函数的重写的:
C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug调试会得不偿失,因此C++11提供了override,可以帮助检测是否重写。但是注意,编译器必须支持c++11以后的版本。
直接在虚函数后面加上override,写错了编译的时候就会报错的。
STL的变化(简单讲解)
这个部分我们就简单讲解带过一下:
-
比如前段时间学的unordered_map和unordered_set这两个容器,底层都是基于哈希表实现的。但是哈希表在c++98是有实现的。但是对于这两个容器是直到c++11的时候才引入。

这里的forward_list其实就是我们最为熟悉的不带头结点的单链表。array底层就是个静态数组,在讲解非类型模板参数的时候说到过。 -
STL中容器的新接口也不少,最重要的就是右值引用和移动语义相关的push/insert/emplace系列接口和移动构造和移动赋值,还有initializer_list版本的构造等,这些前面都讲过了,还有一些无关痛痒的如cbegin/cend等需要时查查文档即可。
-
新加入范围for进行容器的遍历。不过范围for的底层就是通过迭代器实现的。
更多推荐
所有评论(0)