C++ 11新特性 (三)_c++ 什么是让初始化行为具有确定的效果,2024年最新太完整了

25 阅读6分钟
// 添加移动构造函数
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 ls; ls.push_back("hello"); ls.push_back("world"); ...... list ls1 = ls; // 需要拷贝, 效率低 list ls2 = move(ls);


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


### forward 转发


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



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

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


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



#include using namespace std;

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

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

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

int main() { testForward(520); int num = 1314; testForward(num); testForward(forward(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 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 #include 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 #include 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 #include 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 #include using namespace std;

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

int main(void) { initializer_list 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 #include #include using namespace std;

class Test { public: Test(std::initializer_list list) { for (auto it = list.begin(); it != list.end(); ++it) { cout << *it << " "; m_names.push_back(*it); } cout << endl; } private: vector 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关键字


一般的using关键子我们都是用来声明当前文件的命名空间,比如标准库的命名空间std-> using namespace std;  
 但在c++11中,它的用处还有几个  
 1:取代typedef

//---------------------------------------test2 可以取代typedef了,而且更加灵活 using myIntVec = std::vector;
void testUsing2()
{
myIntVec mvec = { 1, 2, 3, 4, 5 };
mvec.push_back(123);
for (int num : mvec)

img img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取