八个默认成员函数
在C++11之前,一个类中有如下六个默认成员函数:
其中前四个成员函数最重要,后面两个成员函数一般不会用到,这里“默认
”的意思就是你不写编译器会自动生成。在C++11标准中又增加了两个默认成员函数,分别是移动构造函数和移动赋值重载函数。
默认移动构造和移动赋值的生成条件
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类中默认生成的移动赋值函数可以用下面的代码,验证方式和上面验证移动构造的方式是一样的。
int main()
{
Person s1("张三", 21);
Person s2;
s2 = std::move(s1); //想要调用Person默认生成的移动赋值
return 0;
}
说明一下:
默认生成的构造函数,对于自定义类型的成员会调用其构造函数进行初始化,但并不会对内置类型的成员进行处理。于是C++11支持非静态成员变量在声明时进行初始化赋值,默认生成的构造函数会使用这些缺省值对成员进行初始化。比如:
class Person
{
public:
//...
private:
//非静态成员变量,可以在成员声明时给缺省值
cl::string _name = "张三"; //姓名
int _age = 20; //年龄
static int _n; //静态成员变量不能给缺省值
};
注意: 这里不是初始化,而是给声明的成员变量一个缺省值。
C++11可以让我们更好的控制要使用的默认成员函数,假设在某些情况下我们需要使用某个默认成员函数,但是因为某些原因导致无法生成这个默认成员函数,这时可以使用default关键字强制生成某个默认成员函数。
例如,下面的Person类中实现了拷贝构造函数:
class Person
{
public:
//拷贝构造函数
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
private:
cl::string _name; //姓名
int _age; //年龄
};
这时如下代码就无法编译成功了,因为Person类中编写了拷贝构造函数,导致无法生成默认的构造函数,因为默认构造函数生成的条件是没有编写任意类型的构造函数,包括拷贝构造函数。
int main()
{
Person s; //没有合适的默认构造函数可用
return 0;
}
这时我们就可以使用default关键字强制生成默认的构造函数,如下:
class Person
{
public:
Person() = default; //强制生成默认构造函数
//拷贝构造函数
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
private:
cl::string _name; //姓名
int _age; //年龄
};
说明一下: 默认成员函数都可以用default关键字强制生成,包括移动构造和移动赋值。
当我们想要限制某些默认函数生成时,可以通过如下两种方式:
例如,要让一个类不能被拷贝,可以用=delete修饰将该类的拷贝构造和拷贝赋值。
class CopyBan
{
public:
CopyBan()
{}
private:
CopyBan(const CopyBan&) = delete;
CopyBan& operator=(const CopyBan&) = delete;
};
说明一下: 被=delete修饰的函数可以设置为公有,也可以设置为私有,效果都一样。
final修饰类
被final修饰的类叫做最终类,最终类无法被继承。比如:
class NonInherit final //被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修饰虚函数
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新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接受可变参数的函数模板和类模板。
本篇博客只讲解函数模板的可变参数。
函数的可变参数模板定义方式如下:
template<class …Args>
返回类型 函数名(Args… args)
{
//函数体
}
例如:
template<class ...Args>
void ShowList(Args... args)
{}
说明一下:
0
到N
( N ≥ 0 )个模板参数,而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...); //递归调用,将参数包继续向下传
}
这样一来,当递归调用ShowList函数模板时,如果传入的参数包中参数的个数为0,那么就会匹配到这个无参的递归终止函数,这样就结束了递归。
鉴于此,我们可以将展开函数和递归调用函数的函数名改为ShowListArg,然后重新编写一个ShowList函数模板,该函数模板的函数体中要做的就是调用ShowListArg函数展开参数包。比如:
//递归终止函数
void ShowListArg()
{
cout << endl;
}
//展开函数
template<class T, class ...Args>
void ShowListArg(T value, Args... args)
{
cout << value << " "; //打印传入的若干参数中的第一个参数
ShowListArg(args...); //将剩下参数继续向下传
}
//供外部调用的函数
template<class ...Args>
void ShowList(Args... args)
{
ShowListArg(args...);
}
这时无论外部调用时传入多少个参数,最终匹配到的都是同一个函数了。
编写带参的递归终止函数
除了编写无参的递归终止函数,也可以编写带参数的递归终止函数来终止递归,比如这里编写带一个参数的递归终止函数:
//递归终止函数
template<class T>
void ShowListArg(const T& t)
{
cout << t << endl;
}
//展开函数
template<class T, class ...Args>
void ShowListArg(T value, Args... args)
{
cout << value << " "; //打印传入的若干参数中的第一个参数
ShowList(args...); //将剩下参数继续向下传
}
//供外部调用的函数
template<class ...Args>
void ShowList(Args... args)
{
ShowListArg(args...);
}
这样一来,在递归调用过程中,如果传入的参数包中参数的个数为1,那么就会匹配到这个递归终止函数,这样也就结束了递归。但是需要注意,这里的递归调用函数需要写成函数模板,因为我们并不知道最后一个参数是什么类型的。
但该方法有一个弊端就是,我们在调用ShowList函数时必须至少传入一个参数,否则就会报错。因为此时无论是调用递归终止函数还是展开函数,都需要至少传入一个参数。
判断参数包中参数的个数(不可行!)
既然我们可以通过sizeof计算出参数包中参数的个数,那我们能不能在ShowList函数中设置一个判断,当参数包中参数个数为0时就终止递归呢?比如:
//错误示例
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value << " "; //打印传入的若干参数中的第一个参数
if (sizeof...(args) == 0)
{
return;
}
ShowList(args...); //将剩下参数继续向下传
}
这种方式是不可行的,原因如下:
通过列表获取参数包中的参数
数组可以通过列表进行初始化,比如:
int a[] = {1,2,3,4}
除此之外,如果参数包中各个参数的类型都是整型,那么也可以把这个参数包放到列表当中初始化这个整型数组,此时参数包中参数就放到数组中了。比如:
//展开函数
template<class ...Args>
void ShowList(Args... args)
{
int arr[] = { args... }; //列表初始化
//打印参数包中的各个参数
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
}
这时调用ShowList函数时就可以传入多个整型参数了。比如:
int main()
{
ShowList(1);
ShowList(1, 2);
ShowList(1, 2, 3);
return 0;
}
但C++并不像Python这样的语言,C++规定一个容器中存储的数据类型必须是相同的,因此如果这样写的话,那么调用ShowList函数时传入的参数只能是整型的,并且还不能传入0个参数,因为数组的大小不能为0,因此我们还需要在此基础上借助逗号表达式来展开参数包。
通过逗号表达式展开参数包
虽然我们不能用不同类型的参数去初始化一个整型数组,但我们可以借助逗号表达式。
这样一来,在执行逗号表达式时就会先调用处理函数处理对应的参数,然后再将逗号表达式中的最后一个整型值作为返回值来初始化整型数组。比如:
//处理参数包中的每个参数
template<class T>
void PrintArg(const T& t)
{
cout << t << " ";
}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗号表达式
cout << endl;
}
说明一下:
{(PrintArg(args), 0)...}
将会展开成{(PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0), etc...}
。这时调用ShowList函数时就可以传入多个不同类型的参数了,但调用时仍然不能传入0个参数,因为数组的大小不能为0,如果想要支持传入0个参数,也可以写一个无参的ShowList函数。比如:
//支持无参调用
void ShowList()
{
cout << endl;
}
//处理函数
template<class T>
void PrintArg(const T& t)
{
cout << t << " ";
}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗号表达式
cout << endl;
}
实际上我们也可以不用逗号表达式,因为这里的问题就是初始化整型数组时必须用整数,那我们可以将处理函数的返回值设置为整型,然后用这个返回值去初始化整型数组也是可以的。比如:
//支持无参调用
void ShowList()
{
cout << endl;
}
//处理函数
template<class T>
int PrintArg(const T& t)
{
cout << t << " ";
return 0;
}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{
int arr[] = { PrintArg(args)... }; //列表初始化
cout << endl;
}
emplace版本的插入接口
C++11标准给STL中的容器增加emplace版本的插入接口,比如list容器的push_front、push_back和insert函数,都增加了对应的emplace_front、emplace_back和emplace函数。如下:
这些emplace版本的插入接口支持模板的可变参数,比如list容器的emplace_back函数的声明如下:
注意: emplace系列接口的可变模板参数类型都带有“&&”,这个表示的是万能引用,而不是右值引用。
emplace系列接口的使用方式
emplace系列接口的使用方式与容器原有的插入接口的使用方式类似,但又有一些不同之处。
以list容器的emplace_back和push_back为例:
比如:
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系列接口的工作流程如下:
emplace系列接口的意义
由于emplace系列接口的可变模板参数的类型都是万能引用,因此既可以接收左值对象,也可以接收右值对象,还可以接收参数包。
总结一下:
当然,这里的前提是容器中存储的元素所对应的类,是一个需要深拷贝的类,并且该类实现了移动构造函数。否则调用emplace系列接口时,传入左值对象和传入右值对象的效果都是一样的,都需要调用一次构造函数和一次拷贝构造函数。
实际emplace系列接口的一部分功能和原有各个容器插入接口是重叠的,因为容器原有的push_back、push_front和insert函数也提供了右值引用版本的接口,如果调用这些接口时如果传入的是右值对象,那么最终也是会调用对应的移动构造函数进行资源的移动的。
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");
cout << endl;
cout << endl;
mylist.emplace_back(kv); //传左值
cout << endl;
mylist.emplace_back(pair<int, cl::string>(2, "two")); //传右值
cout << endl;
mylist.emplace_back(3, "three"); //传参数包
return 0;
}
说明一下:
这里也可以以不同的传参方式调用push_back函数,顺便验证一下容器原有的插入函数的执行逻辑。比如:
int main()
{
list<pair<int, cl::string>> mylist;
pair<int, cl::string> kv(1, "one");
cout << endl;
cout << endl;
mylist.push_back(kv); //传左值
cout << endl;
mylist.push_back(pair<int, cl::string>(2, "two")); //传右值
cout << endl;
mylist.push_back({ 3, "three" }); //列表初始化
return 0;
}
码文不易,本章到此结束,还请多多支持哦!!!