{}初始化列表

C++98:{} 只可对 数组结构体元素 进行列表初始值

struct Point
{
	 int _x;
	 int _y;
};
int main()
{
	 int array1[] = { 1, 2, 3, 4, 5 };
	 int array2[5] = { 0 };
	 Point p = { 1, 2 };
	 return 0;
}

C++11:{} 对所有的内置类型和自定义类型,使用初始化列表时,等号(=)可有可无;

struct Point
{
	 int _x;
	 int _y;
};
int main()
{
	 int x1 = 1;
	 int x2{ 2 };
	 int array1[]{ 1, 2, 3, 4, 5 };
	 int array2[5]{ 0 };
	 Point p{ 1, 2 };
	 // C++11中列表初始化也可以适用于new表达式中
	 int* pa = new int[4]{ 0 };
	 return 0;
}

创建对象时使用列表初始化方式调用构造函数初始化

int main()
{
	 Date d1(2022, 1, 1); // old style
	 // C++11支持的列表初始化,这里会调用构造函数初始化
	 Date d2{ 2022, 1, 2 };
	 Date d3 = { 2022, 1, 3 };
	 return 0;
}

std::initializer_list

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,也可作为operator=的参数,这样就可以用大括号赋值

int main()
{
 	vector<int> v = { 1,2,3,4 };
 	list<int> lt = { 1,2 };
 	// 这里{"sort", "排序"}会先初始化构造一个pair对象
 	map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
// 使用大括号对容器赋值
	v = {10, 20, 30};
 	return 0;
}

auto (必须显示初始化)

自动类型推断。必须显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

decltype (将变量的类型声明为表达式指定的类型)

template<class T1, class T2>
void F(T1 t1, T2 t2)
{
	decltype(t1 * t2) ret;
	cout << typeid(ret).name() << endl;
}
int main()
{
	const int x = 1;
	double y = 2.2;
	decltype(x * y) ret; // ret的类型是double
	decltype(&x) p;      // p的类型是int*
	cout << typeid(ret).name() << endl;
	cout << typeid(p).name() << endl;
	F(1, 'a');//输出int
	return 0;
}

左值引用(可&,可修改)和右值引用(不可&,不可修改)

  1. 都是给对象取别名。
  2. 左值引用可以取地址,右值引用不可以取地址
左值引用
  1. 左值引用只能引用左值,不能引用右值。

  2. 但是const左值引用 既可引用左值,也可引用右值

  3. 左值引用做参数和做返回值都可以提高效率。不拷贝

  4. 当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)

  5. 局部变量值返回在这里插入图片描述
    在这里插入图片描述

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
在这里插入图片描述
函数返回局部对象 = 产生临时右值
const T& 能绑右值,是万能引用参数
没移动构造 → 拷贝构造兜底,执行深拷贝
有移动构造 → 移动构造优先,直接偷资源,不深拷贝

移动构造

移动构造本质是将参数右值的资源窃取过来,同时将自己的资源给这个“右值将亡值”带走,不用做深拷贝了

移动构造是 C++11 引入的一种特殊构造函数,它把一个 “临时对象” 或 “即将被销毁的对象” 的资源 “偷” 过来,而不是复制。

核心目的:
避免不必要的深拷贝
提升性能
让对象可以高效转移资源所有权

C++11 之后:
编译器识别到临时对象是 “右值”,自动调用移动构造,把资源直接转移,零拷贝

什么时候会调用移动构造?
  1. 函数返回局部对象
  2. 用临时对象初始化新对象
  3. std::move () 之后的对象
  4. 容器插入右值
移动构造的典型实现
class String {
private:
    char* data;
    int len;
public:
    // 移动构造
    String(String&& other) noexcept 
        : data(other.data), len(other.len) 
    {
        other.data = nullptr;//源对象必须置空
							//否则源对象析构时会把资源释放,导致目标对象悬空
        other.len = 0;
    }

    ~String() {
        delete[] data;
    }
};
移动构造的底层原理
  1. 右值引用 &&
    用来绑定临时对象,标识 “这个对象马上就要死了,可以偷它的资源”。
  2. 转移资源所有权
    移动构造不分配新内存,只是把指针、句柄、资源从源对象转移到目标对象。
  3. 源对象必须置空
    否则源对象析构时会把资源释放,导致目标对象悬空。
移动构造的问答

移动构造解决了什么问题?
解决临时对象深拷贝性能差的问题,实现资源转移。
移动构造的参数是什么?
右值引用 T&&。
为什么移动构造要把源对象置空?
防止源对象析构时释放资源,导致目标对象悬空。
什么时候不会自动生成移动构造?
手动写了拷贝构造、析构、拷贝赋值中的任意一个。
移动构造为什么要加 noexcept?
让 STL 容器(如 vector)扩容时优先使用移动,而不是拷贝,提升性能。

什么时候移动会退化回拷贝?
  1. 源对象是 “左值” 且未用 std::move 转换
    移动构造只能绑定右值(临时对象 / 将亡值),如果源对象是左值(有名字、可寻址),编译器会直接调用拷贝构造,除非用 std::move 显式转为右值
    std::move(a) 后,(a 已被 “掏空”,资源被转移

  2. 类未定义移动构造,仅定义了拷贝构造

如果类没有手动 / 自动生成移动构造(比如手动写了拷贝构造 / 析构),编译器会 “退化” 到拷贝构造,即使源对象是右值。


void swap(string& s) {
    std::swap(this->_str, s._str);
    std::swap(this->_size, s._size);
    std::swap(this->_capacity, s._capacity);
}

// 移动构造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	cout << "string(string&& s) -- 移动语义" << endl;
	//string tmp(s.str) // 不需要构造一个tmp,so不需要深拷贝了
	swap(s);
}

int main()
{
	 bit::string ret2 = bit::to_string(-1234);
	 //这里没有调用深拷贝的拷贝构造,而是调用了移动构造,
	 //移动构造中没有新开空间拷贝数据(没有这个构造函数string tmp(s.str),这个构造函数里会开辟新空间,移动构造不需要这个tmp,所以不开空间拷贝数据)
	 //所以效率提高了
	 return 0;
}

移动赋值

bit::string to_string(int val)
{
	bit::string str;
	//.....

	return str;
}

// 移动赋值
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动语义" << endl;
	swap(s);
	return *this;
}
int main()
{
	 bit::string ret1;
	 ret1 = bit::to_string(1234);
	 return 0;
}
// 运行结果:
// string(string&& s) -- 移动构造
// string& operator=(string&& s) -- 移动赋值

解释:
调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象ret1接收,编译器就没办法优化了。bit::to_string函数中会先用返回值str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值

右值引用

  1. 右值引用只能引用右值,不能引用左值。
  2. 但是右值引用可以引用move以后的左值。//move唯一的功能就是将一个左值强制转化为右值引用

STL容器插入接口函数也增加了右值引用版本

push_back()的实现里有newnode = new node(val)来开辟空间
在这里插入图片描述

在这里插入图片描述

小结:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

完美转发

#define _CRT_SECURE_NO_WARNINGS

#include<iostream>
using namespace std;

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性--》完美转发
template<typename T>
void PerfectForward(T&& t)//左右值都能接收
{
	Fun(t);//退化为左值
}
int main()
{
	PerfectForward(10);  // 右值
	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

在这里插入图片描述

std::forward 完美转发在传参的过程中保留对象原生类型属性

在这里插入图片描述

更多推荐