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%内容,订阅专栏后可继续查看/也可单篇购买

C++岗面试真题解析 文章被收录于专栏

<p> C++工程师面试真题解析! </p> <p> 邀请头部大厂创作者<a href="https://www.nowcoder.com/profile/73627192" target="_blank">@Evila</a> 及牛客教研共同打磨 </p> <p> 助力程序员的求职! </p>

全部评论
#include <iostream> using namespace std; int main() {     int x = 0, y = 1;     // func为lambad类型,以值方式捕获x,以引用方式捕获y     auto func = [x,&y](int a, int b) -> int { return a + b + x + y; };     cout<<func(x,y)<<endl; // 输出8 } //输出2
1 回复 分享
发布于 2022-03-07 11:17

相关推荐

头像
11-06 10:58
已编辑
门头沟学院 嵌入式工程师
双非25想找富婆不想打工:哦,这该死的伦敦腔,我敢打赌,你简直是个天才,如果我有offer的话,我一定用offer狠狠的打在你的脸上
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
点赞
1
分享
牛客网
牛客企业服务