天天看点

C++ 11新特性 (三)

右值引用

C++11 增加了一个新的类型,称为右值引用( R-value reference),标记为 &&

和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:

int num = 10;
//int && a = num;  //右值引用不能初始化为左值
int && a = 10;      

和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:

int && a = 10;
a = 100;
cout << a << endl;      

另外值得一提的是,C++ 语法上是支持定义常量右值引用的,例如

const int&& a = 10;//编译器不会报错      

学到这里,一些读者可能无法记清楚左值引用和右值引用各自可以引用左值还是右值,这里给大家一张表格,方便大家记忆:

C++11 中右值可以分为两种:一个是将亡值( xvalue, expiring value),另一个则是纯右值( prvalue, PureRvalue):

  • 纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
  • 将亡值:与右值引用相关的表达式,比如,T&& 类型函数的返回值、 std::move 的返回值等。

    右值的使用:

    在 C++ 中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。

#include <iostream>
using namespace std;

class Test
{
public:
    Test() : m_num(new int(100))
    {
        cout << "construct: my name is jerry" << endl;
    }

    Test(const Test& a) : m_num(new int(*a.m_num))
    {
        cout << "copy construct: my name is tom" << endl;
    }

    ~Test()
    {
        delete m_num;
    }

    int* m_num;
};

Test getObj()
{
    Test t;
    return t;
}

int main()
{
    Test t = getObj();
    cout << "t.m_num: " << *t.m_num << endl;
    return 0;
};
construct: my name is jerry
copy construct: my name is tom
t.m_num: 100      

过输出的结果可以看到调用 Test t = getObj(); 的时候调用拷贝构造函数对返回的临时对象进行了深拷贝得到了对象 t,在 getObj() 函数中创建的对象虽然进行了内存的申请操作,但是没有使用就释放掉了。如果能够使用临时对象已经申请的资源,既能节省资源,还能节省资源申请和释放的时间,如果要执行这样的操作就需要使用右值引用了,右值引用具有移动语义,移动语义可以将资源(堆、系统对象等)通过浅拷贝从一个对象转移到另一个对象这样就能减少不必要的临时对象的创建、拷贝以及销毁,可以大幅提高 C++ 应用程序的性能。

#include <iostream>
using namespace std;

class Test
{
public:
    Test() : m_num(new int(100))
    {
        cout << "construct: my name is jerry" << endl;
    }

    Test(const Test& a) : m_num(new int(*a.m_num))
    {
        cout << "copy construct: my name is tom" << endl;
    }

    // 添加移动构造函数
    Test(Test&& a) : m_num(a.m_num)
    {
        a.m_num = nullptr;
        cout << "move construct: my name is sunny" << endl;
    }

    ~Test()
    {
        delete m_num;
        cout << "destruct Test class ..." << endl;
    }

    int* m_num;
};

Test getObj()
{
    Test t;
    return t;
}

int main()
{
    Test t = getObj();
    cout << "t.m_num: " << *t.m_num << endl;
    return 0;
};

construct: my name is jerry
move construct: my name is sunny
destruct Test class ...
t.m_num: 100
destruct Test class ...      

通过修改,在上面的代码给 Test 类添加了移动构造函数(参数为右值引用类型),这样在进行 Test t = getObj(); 操作的时候并没有调用拷贝构造函数进行深拷贝,而是调用了移动构造函数,在这个函数中只是进行了浅拷贝,没有对临时对象进行深拷贝,提高了性能。

如果不使用移动构造,在执行 Test t = getObj() 的时候也是进行了浅拷贝,但是当临时对象被析构的时候,类成员指针 int* m_num; 指向的内存也就被析构了,对象 t 也就无法访问这块内存地址了。

在测试程序中 getObj() 的返回值就是一个将亡值,也就是说是一个右值,在进行赋值操作的时候如果 = 右边是一个右值,那么移动构造函数就会被调用。移动构造中使用了右值引用,会将临时对象中的堆内存地址的所有权转移给对象t,这块内存被成功续命,因此在t对象中还可以继续使用这块内存。

move 转移

在 C++11 添加了右值引用,并且不能使用左值初始化右值引用,如果想要使用左值初始化一个右值引用需要借助 std::move () 函数,使用std::move方法可以将左值转换为右值。使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。

从实现上讲,std::move 基本等同于一个类型转换:static_cast<T&&>(lvalue);,函数原型如下:

template<class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT
{   // forward _Arg as movable
    return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
}      

使用方法如下:

class Test
{
public:
    Test(){}
    ......
}
int main()
{
    Test t;
    Test && v1 = t;          // error
    Test && v2 = move(t);    // ok
    return 0;
}      

在第 4 行中,使用左值初始化右值引用,因此语法是错误的。

在第 5 行中,使用 move() 函数将左值转换为了右值,这样就可以初始化右值引用了。

list<string> ls;
ls.push_back("hello");
ls.push_back("world");
......
list<string> ls1 = ls;        // 需要拷贝, 效率低
list<string> ls2 = move(ls);      

如果不使用 std::move,拷贝的代价很大,性能较低。使用 move 几乎没有任何代价,只是转换了资源的所有权。如果一个对象内部有较大的堆内存或者动态数组时,使用 move () 就可以非常方便的进行数据所有权的转移。另外,我们也可以给类编写相应的移动构造函数(T::T(T&& another))和和具有移动语义的赋值函数(T&& T::operator=(T&& rhs)),在构造对象和赋值的时候尽可能的进行资源的重复利用,因为它们都是接收一个右值引用参数。

forward 转发

右值引用类型是独立于值的,一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了。如果需要按照参数原来的类型转发到另一个函数,可以使用 C++11 提供的 std::forward () 函数,该函数实现的功能称之为完美转发。

// 函数原型
template <class T> T&& forward (typename remove_reference<T>::type& t) noexcept;
template <class T> T&& forward (typename remove_reference<T>::type&& t) noexcept;

// 精简之后的样子
std::forward<T>(t);      

下面通过一个例子演示一下关于 forward 的使用:

#include <iostream>
using namespace std;

template<typename T>
void printValue(T& t)
{
    cout << "l-value: " << t << endl;
}

template<typename T>
void printValue(T&& t)
{
    cout << "r-value: " << t << endl;
}

template<typename T>
void testForward(T && v)
{
    printValue(v);
    printValue(move(v));
    printValue(forward<T>(v));
    cout << endl;
}

int main()
{
    testForward(520);
    int num = 1314;
    testForward(num);
    testForward(forward<int>(num));
    testForward(forward<int&>(num));
    testForward(forward<int&&>(num));

    return 0;
}

l-value: 520
r-value: 520
r-value: 520

l-value: 1314
r-value: 1314
l-value: 1314

l-value: 1314
r-value: 1314
r-value: 1314

l-value: 1314
r-value: 1314
l-value: 1314

l-value: 1314
r-value: 1314
r-value: 1314      

列表初始化

关于 C++ 中的变量,数组,对象等都有不同的初始化方法,在这些繁琐的初始化方法中没有任何一种方式适用于所有的情况。为了统一初始化方式,并且让初始化行为具有确定的效果,在 C++11 中提出了列表初始化的概念。

统一的初始化:

在 C++11 中,列表初始化变得更加灵活了,来看一下下面这段初始化类对象的代码

#include <iostream>
using namespace std;

class Test
{
public:
    Test(int) {}
private:
    Test(const Test &);
};

int main(void)
{
    Test t1(520);
    Test t2 = 520; 
    Test t3 = { 520 };
    Test t4{ 520 };
    int a1 = { 1314 };
    int a2{ 1314 };
    int arr1[] = { 1, 2, 3 };
    int arr2[]{ 1, 2, 3 };
    return 0;
}      

聚合体

在 C++11 中,列表初始化的使用范围被大大增强了,但是一些模糊的概念也随之而来,在前面的例子可以得知,列表初始化可以用于自定义类型的初始化,但是对于一个自定义类型,列表初始化可能有两种执行结果:

#include <iostream>
#include <string>
using namespace std;

struct T1
{
    int x;
    int y;
}a = { 123, 321 };

struct T2
{
    int x;
    int y;
    T2(int, int) : x(10), y(20) {}
}b = { 123, 321 };

int main(void)
{
    cout << "a.x: " << a.x << ", a.y: " << a.y << endl;
    cout << "b.x: " << b.x << ", b.y: " << b.y << endl;
    return 0;
}

a.x: 123, a.y: 321
b.x: 10, b.y: 20      

非聚合体

对于聚合类型的类可以直接使用列表初始化进行对象的初始化,如果不满足聚合条件还想使用列表初始化其实也是可以的,需要在类的内部自定义一个构造函数, 在构造函数中使用初始化列表对类成员变量进行初始化:

#include <iostream>
#include <string>
using namespace std;

struct T1
{
    int x;
    double y;
    // 在构造函数中使用初始化列表初始化类成员
    T1(int a, double b, int c) : x(a), y(b), z(c){}
    virtual void print()
    {
        cout << "x: " << x << ", y: " << y << ", z: " << z << endl;
    }
private:
    int z;
};

int main(void)
{
    T1 t{ 520, 13.14, 1314 };   // ok, 基于构造函数使用初始化列表初始化类成员
    t.print();
    return 0;
}      

另外,需要额外注意的是聚合类型的定义并非递归的,也就是说当一个类的非静态成员是非聚合类型时,这个类也可能是聚合类型,比如下面的这个例子:

#include <iostream>
#include <string>
using namespace std;

struct T1
{
    int x;
    double y;
private:
    int z;
};

struct T2
{
    T1 t1;
    long x1;
    double y1;
};

int main(void)
{
    T2 t2{ {}, 520, 13.14 };
    return 0;
}      

可以看到,T1 并非一个聚合类型,因为它有一个 Private 的非静态成员。但是尽管 T2 有一个非聚合类型的非静态成员 t1,T2 依然是一个聚合类型,可以直接使用列表初始化的方式进行初始化。

最后强调一下 t2 对象的初始化过程,对于非聚合类型的成员 t1 做初始化的时候,可以直接写一对空的大括号 {},这相当于调用是 T1 的无参构造函数。

std::initializer_list

在 C++ 的 STL 容器中,可以进行任意长度的数据的初始化,使用初始化列表也只能进行固定参数的初始化,如果想要做到和 STL 一样有任意长度初始化的能力,可以使用 std::initializer_list 这个轻量级的类模板来实现。

先来介绍一下这个类模板的一些特点:

  • 它是一个轻量级的容器类型,内部定义了迭代器 iterator 等容器必须的概念,遍历时得到的迭代器是只读的。
  • 对于 std::initializer_list 而言,它可以接收任意长度的初始化列表,但是要求元素必须是同种类型 T
  • 在 std::initializer_list 内部有三个成员接口:size(), begin(), end()。
  • std::initializer_list 对象只能被整体初始化或者赋值。

std::initializer_list,使用初始化列表 { } 作为实参进行数据传递即可。

#include <iostream>
#include <string>
using namespace std;

void traversal(std::initializer_list<int> a)
{
    for (auto it = a.begin(); it != a.end(); ++it)
    {
        cout << *it << " ";
    }
    cout << endl;
}

int main(void)
{
    initializer_list<int> list;
    cout << "current list size: " << list.size() << endl;
    traversal(list);

    list = { 1,2,3,4,5,6,7,8,9,0 };
    cout << "current list size: " << list.size() << endl;
    traversal(list);
    cout << endl;
    
    list = { 1,3,5,7,9 };
    cout << "current list size: " << list.size() << endl;
    traversal(list);
    cout << endl;

    
    // 直接通过初始化列表传递数据 //
    
    traversal({ 2, 4, 6, 8, 0 });
    cout << endl;

    traversal({ 11,12,13,14,15,16 });
    cout << endl;


    return 0;
}


current list size: 0

current list size: 10
1 2 3 4 5 6 7 8 9 0

current list size: 5
1 3 5 7 9

2 4 6 8 0

11 12 13 14 15 16      
  • std::initializer_list拥有一个无参构造函数,因此,它可以直接定义实例,此时将得到一个空的
  • std::initializer_list,因为在遍历这种类型的容器的时候得到的是一个只读的迭代器,因此我们不能修改里边的数据,只能通过值覆盖的方式进行容器内部数据的修改。虽然如此,在效率方面也无需担心,
  • std::initializer_list的效率是非常高的,它的内部并不负责保存初始化列表中元素的拷贝,仅仅存储了初始化列表中元素的引用。

作为构造函数参数 std::initializer_list

自定义的类如果在构造对象的时候想要接收任意个数的实参,可以给构造函数指定为 std::initializer_list 类型,在自定义类的内部还是使用容器来存储接收的多个实参。

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Test
{
public:
    Test(std::initializer_list<string> list)
    {
        for (auto it = list.begin(); it != list.end(); ++it)
        {
            cout << *it << " ";
            m_names.push_back(*it);
        }
        cout << endl;
    }
private:
    vector<string> m_names;
};

int main(void)
{
    Test t({ "jack", "lucy", "tom" });
    Test t1({ "hello", "world", "nihao", "shijie" });
    return 0;
}
jack lucy tom
hello world nihao shijie      

using关键字

//---------------------------------------test2 可以取代typedef了,而且更加灵活  
    using myIntVec = std::vector<int>;  
    void testUsing2()  
    {  
        myIntVec mvec = { 1, 2, 3, 4, 5 };  
        mvec.push_back(123);  
        for (int num : mvec)  
            printf("--- num:%d\n", num);  
      
        std::cout << is_same < std::vector<int>, myIntVec>::value << std::endl; // 1  
    }  
      
    template <typename T>  
    using MapStr = std::map<T, std::string>;  
    void testUsing3()  
    {  
        MapStr<int> intStrMap;  
        intStrMap.insert(make_pair(123, "aaa"));  
        intStrMap.insert(make_pair(456, "bbb"));  
      
        MapStr<std::string> strstrMap;  
        strstrMap.insert(make_pair("ccc", "ddd"));  
        strstrMap.insert(make_pair("eee", "fff"));  
    }      
//---------------------------------------test1  
using namespace std;  
  
class Base  
{  
public:  
    void menfcn()  
    {  
        cout << "Base function" << endl;  
    }  
  
    void menfcn(int n)  
    {  
        cout << "Base function with int" << endl;  
    }  
//private:   
//  void menfcn(std::string _name) {}//会让基类using时报不可访问的错  
};  
  
class Derived : private Base  
{  
public:  
    using Base::menfcn;//using声明只能指定一个名字,不能带形参表,且基类的该函数不能有私有版本,否则编译报错  
                        //using父类方法,主要是用来实现可以在子类实例中调用到父类的重载版本  
    int menfcn(int num)  
    {  
        cout << "Derived function with int : "<< num << endl;  
        return num;  
    }  
};  
  
/* 
“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下: 
1、如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆) 
2、如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆) 
使用了using关键字,就可以避免1的情况,是的父类同名函数在子类中得以重载,不被隐藏 
*/  
  
void testUsing1()  
{  
    Base b;  
    Derived d;  
    b.menfcn();  
    d.menfcn();//如果去掉Derived类中的using声明,会出现错误:error C2660: 'Derived::menfcn' : function does not take 0 arguments      
    d.menfcn(123);  
    /* 
    Base function 
    Base function 
    Derived function with int : 123 
    */  
}