2-4 C++11新特性
C++11引入了很多新特性,有些使代码更简洁、方便,有些使代码更安全、可靠,最关键的是有些特性使C++支持更强大的功能设计。本章对一些常用和必要学习的特性进行介绍。
1. 可变参数模板
在2-3章函数模板和类模版介绍中,模板参数的数量必须是确定的,要想在同一个模板类或函数中实现不同数量的参数,则需要编写多个模板类和函数的副本。可变模版参数(variadic templates)是C++11新特性中的重磅炸弹,它对模板参数进行更高度的泛化,不仅支持参数类型不确定,而且支持参数数量不确定。
1.1 可变参数模板函数
无论是在声明函数模板还是类模板,首先要定义占位符。可变参数模板在typename或class后面带上省略号“...”。
#include <iostream>
using namespace std;
template <typename/class... T>
void func(T... args)
{
cout << sizeof...(args) << endl; // 打印可变参数的个数
}
int main()
{
func(); // 0
func(1); // 1
func(1, "abs"); // 2
return 0;
}
上面可变参数模板的定义中,T是占位符需实例化时指明类型,省略号“...”表明该模版可接受一个参数包,参数包中可以包含0到任意个模板参数。参数包需要以展开的方式遍历每个参数,因此如何展开和遍历参数包是最大的难点。
1.2 以递归方式展开参数包(可变参数模板函数)
在可变参数模板函数中,可是以递归的方式去展开可变参数包,同时提供一个递归终止函数的副本,用于结束递归。 举个例子:
// 实现一个打印方法,对所有入参进行递归打印
#include <iostream>
using namespace std;
//递归终止函数
template <class T> // 必须声明在可变参数模板函数之前
void printParams(T param)
{
cout << "last parameter " << param << endl;
}
template <class T, class ...Args> // 这里需要声明两个占位符,第一个是T,第二个是可变参数占位符Args
void printParams(T param, Args... rest)
{
// printParams调用时传的第一个形参对应与T param
//第二个到最后一个形参对应于Args... rest
cout << "parameter " << param << endl;
// 将可变参数包传递进行递归
// 在进入下次函数调用时,可变参数包的第一个参数对应成为T param
// 即每次递归可变参数包的参数数量都会少一个,直到可变参数包中只剩下一个参数时
// 会调用我们已经定义好的递归终止函数
printParams(rest...);
}
int main(void)
{
printParams(1, 'c', 3.14 , "hello world");
return 0;
}
/* 调用结果:
parameter 1
parameter c
parameter 3.14
last parameter hello world
*/
1.3 可变参数模板类
类似的,可变参数模板类在类定义前声明模板占位符。 例如:
template<typename ... Types>
class Myclass
{
};
此时,可以使用0个或任意个参数去实例化Myclass。
Myclass<> obj1;
Myclass<int> obj2;
Myclass<int, string> obj3;
如果想避免使用0个参数去实例化对象,可以在模板占位符声明中在可变参数包之前定义一个普通占位符,例如:
template<typename T, typename ... Types>
class Myclass
{
};
Myclass<> obj1; // 不合法的实例化
Myclass<int> obj2;
Myclass<int, string> obj3;
此时需要注意,可变参数模板包必须是最后一个占位符参数。
template<typename ... Ts, typename T>
class Myclass // 这样定义模板是不合法的,因为无法推断出占位符T的变量类型
{};
1.4 递归继承方式展开参数包——tuple容器
tuple又叫元祖,是一个固定大小的不同类型变量值的集合。tuple是C++11引入的STL标准库容器,std::tuple理论上可以有无数个任意类型的成员变量。本节剖析tuple中可变参数模板类的实现,关于tuple容器的用法这里不进行详细介绍。 tuple的实现代码简化如下:
#include <iostream>
// 前向声明
template<typename... Values> class tuple;
// 终止类前向声明
template<> class tuple<> {};
// 参数模板定义了Head和Tail占位符,其中Tail是个参数包
template<typename Head, typename... Tail>
// 继承于除Head之外的Tail参数包基tuple类
class tuple<Head, Tail...> : private tuple<Tail...>
{
typedef tuple<Tail...> inherited; // 基类类型定义
public:
tuple() {}
tuple(Head v, Tail... vtail) : m_head(v), inherited(vtail...) {}
Head& head()
{
return m_head;
}
inherited& tail()
{
// tail()函数返回基类对象,基类对象和派生类对象的内存起始地址是一样的。
// 返回*this再强制转化为基类inherited类型。
return dynamic_cast<inherited&>(*this);
}
protected:
Head m_head; // 参数包的第一个参数
}
通过tuple类的实现可以知道,tuple类继承于无head参数的基tuple类。同样的,基tuple类递归继承于无head参数的基tuple类,直到最终继承于空参数的tuple类。在tuple类中定义了Head类型的成员m_head用于访问可变参数包的第一个参数,tail()函数返回了基类的对象。因此,可以通过如下的方式打开tuple中的可变参数包。
// 创建一个tuple实例,放入了int, float, std::string类型的变量,值为0,3.14和"hello world"
tuple<int, float, std::string> t(0, 3.14, "hello world");
// 打印前3个变量值
std::cout << t.head() << " " << t.tail().head() << " " << t.tail().tail().head() << std::endl;
2.右值引用与移动语义
2.1 左值 & 右值
左值和右值的概念乍一看感觉很陌生,但其实它们存在于我们写过的每一份C++代码中,C++程序中所有的值不是左值就是右值。 左值通常是指具有变量名或对象名、在表达式结束后依然存在的变量。相对的右值通常是指没有变量名、在表达式结束后就不再存在的临时值。区分左值与右值的一个关键原则是对表达式取地址,能成功取到内存地址的为左值,否则为右值。例如:
// 以下可以对表达式取地址的是左值
int i;
char c;
string str;
// 以下不跟变量关联的字面量值:0、'!'、"hello"为右值
i = 0;
c = '!';
str = "hello";
i = 1+2; // 1+2表达式的结果是一个临时值
// 非引用的函数返回临时变量值为右值
string func()
{
return string("hello world");
}
2.2 左值引用 & 右值引用
左值引用就是常见的变量引用,也是变量的别名。由于C++11中引入了右值引用,因此C++11之前的变量引用被称为左值引用(lvalue reference)。
右值通常是在表达式结束后不再存在的值,因此想要在表达式结束后还使用右值,C++11新特性中引入了右值引用。右值引用使用的符号是&&,例如:
int&& x = 1; // 1是右值 x是右值引用
int y = 0; // y是左值
int &&r = y; // 编译器错误,不能将左值赋值给右值引用
// 函数定义
string func()
{
return string("hello world");
}
string && rRef = func(); // func()的返回值是右值,rRef是右值的引用
- 因此,右值引用可以使右值的生命周期得以延续。如rRef引用了func()的返回值,在func函数的return表达式语句结束后,返回值的生命周期终结。但通过右值引用,返回值作为右值又重获新生,其生命期将与右值引用类型变量rRef的生命周期一样。实际上,rRef是一个左值,可以对其取地址。
注意:左值引用和右值引用只能引用相符的类型,如果绑定类型不正确会编译失败。但有一个特例,const修饰的左值引用既可以引用左值,又可以引用右值。但缺点是,const修饰引用为只读类型,无法修改。 例如:
void func(const string& ref)
{
return ;
}
string s1 = "hello";
func(s1); // 传入左值作为实参
func("hello"); // 传入右值作为实参
- 右值引用的另一个优势是减少内存的拷贝。例如:
void func_val(string value) // 形参为string的左值
{
}
void func_ref(string&& ref) // 形参为string的右值引用
{
}
func_val("hello"); // 调用func_val时会产生一次拷贝构造
func_ref("hello"); // 没有拷贝构造发生
注意:验证上例需要在编译时加上-fno-elide-constructors,关闭编译器的返回值优化,否则编译器默认开启了返回值优化,编译器会使用一个对象代替临时变量的构造和函数形参的传递,因此一次拷贝构造都不会发生。
总结:
- 左值引用T&, 只能引用左值
- 右值引用T&&,只能引用右值
- 常量左值引用const T&, 既可以引用左值又可以引用右值
- 已命名的右值引用,编译器会认为是个左值
- 编译器有返回值优化,但不应该过于依赖
2.3 移动构造函数
通过学习拷贝构造函数,我们认识到拷贝构造函数是一种特殊的构造函数,它的入参是这个类的对象或对象引用,拷贝构造函数的工作就是将实参的对象赋值为自己成员,尤其是指针成员动态申请了内存时,拷贝构造函数要实现深拷贝。再回顾下拷贝构造函数介绍时的案例:
#include <iostream>
#include <vector>
using namespace std;
class student
{
public:
// 构造函数
student()
{
score_list = new int[10]; // 指针动态申请10个int变量所需的内存
}
~student()
{
delete score_list;
}
// 拷贝构造函数
student(const student& stu)
{
static_copy++;
score_list = new int[10]; // 指针成员动态申请10个int变量所需的内存
// 深拷贝
for (int i = 0; i < 10; i++)
{
score_list[i] = stu.score_list[i]; // 循环复制score_list中的值
}
}
public:
static int static_copy; // 静态变量,用于统计student类调用拷贝构造函数的次数
int * score_list;
};
int student::static_copy = 0;
在上面这个类中,我们定义了int*指针成员并动态申请了内存,在拷贝构造函数中实现了指针成员的深拷贝。当程序需要使用vector去push_back多个student对象时,vector的push_back函数会将
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> C++工程师面试真题解析! </p> <p> 邀请头部大厂创作者<a href="https://www.nowcoder.com/profile/73627192" target="_blank">@Evila</a> 及牛客教研共同打磨 </p> <p> 助力程序员的求职! </p>