C++ 16:C++11新特化

列表初始化

C++98中⼀般数组和结构体可以⽤{}进⾏初始化,但C++11 引入了统一的列表初始化语法,使用 {} 来初始化任何类型的对象,包括内置类型、自定义类型和容器,省略了=,在某些场景方便了许多

cpp 复制代码
struct Date{
    int yeae;
    int month;
    int day;
};
int main()
{
    //C++ 98
    Date d1={2025,11,11};
    int arr[]={1,2,3};
    
    //C++11
    Date d2{2025,11,11};
    int a[]{1,2,3};
    vector<int>v{1,2,3};
}

initializer_list

上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个
值去构造初始化,那么我们得实现很多个构造函数才能⽀持

这个类的本质是底层开⼀个数组,将数据拷⻉
过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。

cpp 复制代码
// 这⾥是pair对象的{}初始化和map的initializer_list构造结合到⼀起⽤了
map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"}};
// initializer_list版本的赋值⽀持
v1 = { 10,20,30,40,50 };

右值引用和移动语义

左值和右值

左值:有名称,有地址的对象(eg:变量)

右值:字⾯值常量,临时变量,匿名对象

左值与右值的核心区别是能否取地址,左值可以,右值不行

左值引用和右值引用

Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别
名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。
注意

左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值) (move是一个函数模板,进行强制转换)

变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变
量表达式的属性是左值

cpp 复制代码
// 右值引用给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");

// 左值引用不能直接引用右值,但是const左值引用可以引用右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");

// 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s;

// b、r1、rr1都是变量表达式,都是左值
cout << &b << endl;
cout << &r1 << endl;
cout << &rr1 << endl;

引用延长生命周期

右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改

cpp 复制代码
int main()
{
     std::string s1 = "Test";
    // std::string&& r1 = s1; // 错误:不能绑定到左值

     const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期
     // r2 += "Test"; // 错误:不能通过到 const 的引⽤修改

     std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期
    r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改
    std::cout << r3 << '\n';
    return 0;
}

左值与右值参数匹配

C++可以重载函数,但我们都知道要最匹配的才最合适

重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。

cpp 复制代码
void f(int& x)
{
    std::cout << "左值引⽤重载 f(" << x << ")\n";
}
 
void f(const int& x)
{
    std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
 
void f(int&& x)
{
    std::cout << "右值引⽤重载 f(" << x << ")\n";
}
 
int main()
{
    int i = 1;
    const int ci = 2;
    f(i); // 调⽤ f(int&)
    f(ci); // 调⽤ f(const int&)
    f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)
    f(std::move(i)); // 调⽤ f(int&&)
    int&& x = 1;
    f(x); // 调⽤ f(int& x)
    f(std::move(x)); // 调⽤ f(int&& x)
    return 0;
}

移动构造与移动赋值

前面我们说过,有拷贝构造与拷贝赋值是一种函数,这里

移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。

cpp 复制代码
class String {
public:
    String(String&& other) { // 移动构造
        data_ = other.data_;
        other.data_ = nullptr;
    }
    String& operator=(String&& other) { // 移动赋值
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            other.data_ = nullptr;
        }
        return *this;
    }
};

移动赋值与移动构造在对于string,vector的这些需要深拷贝的,有极大意义,因为都变成了浅拷贝(进行掠夺资源)

为了匹配参数,我们传的都是右值,且右值一般都是临时变量,生命周期只有一行,那么即使对它进行掠夺资源,对后面也无伤大雅

俗语:相当于一个人去世后,他的遗产被其他人给掠夺了,也对他本人无影响

类型分类

C++11以后,进⼀步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。

纯右值是指那些字面值常量或求值结果相当于字面值或是⼀个不具名的临时对象。 C++11中的纯右值概念划分等价于 C++98中的右值。

将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如

move(x) 、 static_cast<X&&>(x)

泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。

引用折叠

C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或 typedef
中的类型操作可以构成引⽤的引⽤。
通过模板或 typedef 中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规
则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。

cpp 复制代码
//// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x)
{}
int main()
{
	//引用折叠,只要有一个左值引用(无论前后),都会被折叠,成为左值引用
	typedef int& lref;
	typedef int&& rref;
	int n = 0;

	lref& a = n;//a int&
	lref&& b = n;//b int&
	rref& c = n;//c int&
	rref&& d = 1;// d int&&  rref 与&&都是右值引用都是符合,才不会被重叠,最后才会为右值引用

	 ///没有折叠->实例化为void f1(int& x)
	f1<int>(n);
	//f1<int>(0); // 报错

	// 折叠->实例化为void f1(int& x)
	f1<int&>(n);
	//f1<int&>(0); // 报错

	// 折叠->实例化为void f1(int& x)
	f1<int&&>(n);
	//f1<int&&>(0); // 报错

	// 折叠->实例化为void f1(const int& x)
	f1<const int&>(n);
	f1<const int&>(0);

	// 折叠->实例化为void f1(const int& x)
	f1<const int&&>(n);
	f1<const int&&>(0);

	// 没有折叠->实例化为void f2(int&& x)
	//f2<int>(n); // 报错
	f2<int>(0);

	// 折叠->实例化为void f2(int& x)
	f2<int&>(n);
	//f2<int&>(0); // 报错

	// 折叠->实例化为void f2(int&& x)
	//f2<int&&>(n); // 报错
	f2<int&&>(0);
	return 0;
}

万能引用

一个函数模板即可解决左值与右值的传参

cpp 复制代码
//万能引用
template <class T>
void Function(T&& a)
{
	int  b= 0;
	T& x = b;
	x++;//不影响后面结果,后置++,返回++之前的值
	cout << &b << " " << &x << endl;
	//传的参数为左值,T为int&,那么x为a的别名,地址一样
	//反之,如果传的参数为右值,T为int,那么x为int,x与a的地址不一样
}
int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(10);

	int a;
    //a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
	Function(a); // 左值

	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(std::move(a));
	return 0;
}

完美转发

上面的万能引用是好用,但我们前面说过变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值(意味着,一个万能引用函数里面再套一个函数,即使我们传的是右值,但仍会调到下一个函数的左值类型的函数)

完美转发即帮助我们解决这个问题

完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,下⾯⽰例中传递给 Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤返回。

cpp 复制代码
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 < class T>
void Function(T&& t)
{
	Fun(forward<T>(t));//如果T是int,说明右值引用,那么就会强转一下再传
	//完美转发还是引⽤折叠的⽅式实现
}

int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(10); // 右值

	int a;
	// a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
	Function(a); // 左值

	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(std::move(a)); // 右值

	const int b = 8;
	// a是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int&t)
	Function(b);

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
	Function(std::move(b)); // const 右值

	return 0;
}

可变模板参数

基本语法

可变数⽬的参数被称 为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函数参数。

cpp 复制代码
template<typename... Args>
void print(Args... args) 
{
	cout <<sizeof...() <<endl;
}
//可变模板参数为我们减少了代码的重复量,下面的print,如果没有模板参数,要写多个
int main()
{
	print();//包里0个参数
	print(1);//包里1个参数
	print(1,23.2);//包里2个参数
	print(1,"dfsfd");//包里2个参数
	print(1,string("sdjkl"),2.23);//包里3个参数
	return 0;
}

包扩展

对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个
包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元
素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。

cpp 复制代码
//包扩展(解析出参数包内容)
void ShowList()
{
	//编译器递归终止条件,参赛包的个数为0
	cout << endl;//
}

//编译时进行递归,函数重载
template<class T, class ...Args>
void ShowList(T&&x,Args&&...args)
{
	//不可,下面这个是运行时参数包为0,终止应该是编译时参数包个数为0
	//if (sizeof... (args) == 0)
	//	return;//终止
	cout << x << " ";
	//args是N个参数的参数包
	//调用ShowList,参数包第一个传给x,剩下N-1个参数传给第二个参数包
	ShowList(args...);
}

template<class ...Args>
void print(Args&&... x)
{
	//cout << sizeof...(x) << endl;
	ShowList(x...);
	//cout << ...x << endl;
}
template<class T>
const T& getarg(const T& x)
{
	cout << x << " ";
	return x;//有值返回即可,随便什么
}
//可变模板参数为我们减少了代码的重复量,下面的print,如果没有模板参数,要写多个
int main()
{
	print();//包里0个参数
	print(1);//包里1个参数
	print(1,23.2);//包里2个参数
	print(1,"dfsfd");//包里2个参数
	print(1,string("sdjkl"),2.23);//包里3个参数
	return 0;
}

虽然包扩展编译解析时是递归开展的,但我们不能在函数内部使用递归获得包的数据,因为我们的是运行时递归

empalce系列接口

cpp 复制代码
template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position,Args&&... args);

empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container<T>,
empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。

cpp 复制代码
int main()
{
    list<pair<string,int>> lt;
    lt.push_back({"苹果",1});
    lt.emplace_back("苹果,1);
}

上面push_back需要先创建一个临时变量再进行插入,但emplace_back因为支持可变模板参数,可以直接进行插入

总结:
emplace系列兼容push系列和 insert系列
部分场景,emplace系列可以直接构造
但另外两个:构造+移动构造/拷贝 构造

新的类功能

默认的移动构造和移动赋值

C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
如果你没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀
个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执
⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤
移动构造,没有实现就调⽤拷⻉构造。

同样,如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个,编译器会⾃动⽣成⼀个默认移动赋值。

defult与delete

假设你要使⽤某个默认的函数,但是因为⼀些原因 这个函数没有默认⽣成。
eg:我们提供了拷⻉构造,就不会⽣成移动构造了可以使⽤default关键字显⽰指定移动构造⽣成。

想要限制某些默认函数的⽣成,只需在该函数声明加上=delete即可,该语法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。

cpp 复制代码
class Person
{
 public:
    Person(const char* name = "", int age = 0)
    :_name(name)
        , _age(age)
    {}
    Person(const Person& p)
        :_name(p._name)
        ,_age(p._age)
        {}
    Person(Person&& p) = default;
    //Person(const Person& p) = delete;
private:
    bit::string _name;
    int _age;
};
    int main()
{
    Person s1;
    Person s2 = s1;
    Person s3 = std::move(s1);
    return 0;
}

Lambda

表达式语法

lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部
它没有类型,我们⽤auto或者模板参数定义的对象去接收 lambda 对象。
表达式语法格式

capture-list\] (parameters)-\> return type { function boby } > \[capture-list\] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据\[\]来 > 判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使 ⽤,捕捉列表可以传值和传引⽤捕捉,具体细节7.2中我们再细讲。捕捉列表为空也不能省略。 > (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略 > -\>return type :返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此 > 部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。 > {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以 > 使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略。 ```cpp int main() { // 1、捕捉为空也不能省略 // 2、参数为空可以省略 // 3、返回值可以省略,可以通过返回对象⾃动推导 // 4、函数不体能省略 auto add1 = [](int x, int y)->int {return x + y; }; cout << add1(1, 2) << endl; auto func1 = [] { cout << "hello word"; return 0; }; func1(); return 0; } ``` ### 捕捉列表 lambda 表达式中默认只能⽤ lambda 函数体和参数中的变量,如果想⽤外层作⽤域中的变量就 需要进⾏捕捉 三种捕捉方式 > 1. > 在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。\[x, y, \&z\] 表⽰x和y值捕捉,z引⽤捕捉 > 2. > 在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表⽰隐式值捕捉,在捕捉列表写⼀个\&表⽰隐式引⽤捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些变量。 > 3. > 在捕捉列表中混合使⽤隐式捕捉和显⽰捕捉。\[=, \&x\]表⽰其他变量隐式值捕捉,x引⽤捕捉;\[\&, x, y\]表⽰其他变量引⽤捕捉,x和y值捕捉。**当使⽤混合捕捉时,第⼀个元素必须是\&或=,并且\&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉。** lambda在函数局部域,可以捕捉自己位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使 ⽤。 **这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。** lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改mutable加在参数列表的后⾯可以取消其常量性,但形参也无法影响形参,且加了后,参数列表不可为空 ```cpp int y = 0; auto func2 = []() { y++; }; int main() { // 只能用当前lambda局部域和捕捉的对象和全局对象 int a = 0, b = 1, c = 2, d = 3; auto func1 = [a, &b](int x)mutable { // 值捕捉的变量不能修改,引用捕捉的变量可以修改 a++; b++; int ret = a + b + x + y; return ret; }; cout << func1(1) << endl; //func2(); // 隐式值捕捉 // 用了哪些变量就捕捉哪些变量 auto func2 = [=] { int ret = a + b + c; return ret; }; cout << func2() << endl; // 隐式引用捕捉 // 用了哪些变量就捕捉哪些变量 auto func3 = [&] { a++; c++; //d++; }; func3(); cout << a << " " << b << " " << c << " " << d << endl; // 混合捕捉,但不能又引用又传值 &,= // 混合捕捉1 auto func4 = [&, a, b] { //a++; //b++; c++; d++; return a + b + c + d; }; func4(); cout << a << " " << b << " " << c << " " << d << endl; //混合捕捉2 auto func5 = [=, &a, &b] { return (++a) + (++b); }; func5(); return 0; } ``` ### lambda的原理 lambda 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for 这样的东西。范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个 lambda 以后,编译器会⽣成⼀个对应的仿函数的类。 ## 包装器 ### function ```cpp template class function; // undefined template class function; ``` 被定义在\头⽂件中 **std::function 的优势就是统⼀类型,** 函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型 ```cpp #include int f(int a, int b) { return a + b; } struct Functor { int operator()(int a, int b) { return a + b; } }; class Plus { public: Plus(int n=10) :_n(n) { } static int pushi(const int& a, const int& b) { return a + b; } double pusha( double a, double b) { return a + b * _n; } private: int _n; }; ``` ### bind bind 是⼀个函数模板,它也是⼀个可调⽤对象的包装器,可以把他看做⼀个函数适配器,对接收 的fn可调⽤对象进⾏处理后返回⼀个可调⽤对象。 bind 可以⽤来**调整参数个数和参数顺序**。 bind 也在\这个头⽂件中。 arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表⽰ newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表⽰⽣成的可调⽤对象 中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3....这些占 位符放到placeholders的⼀个命名空间中。 ```cpp using placeholders::_1; using placeholders:: _2; using placeholders:: _3; int func(int a, int b) { return a + b-2*b; } int main() { auto s1 = bind(func, _1, _2);// 5是_1,再传给a , 10是_2,再传给10 cout << s1(5, 10) << endl; //-15 auto s2 = bind(func, _2, _1);//5是_1,再传给b 10是_2,再传给a cout << s2(5, 10) << endl;//5 auto s3 = bind(func, 10, _1);//10传给a, 5是_1,再传给b cout << s3(5) << endl;//10 return 0; } ``` ## 智能指针 下一章详讲 ## 总结 C++11 > 列表初始化简便了初始化数据的代码 > > 右值引用(`T&&`)的引入实现了移动拷贝,提高效率,也实现了完美转发,解决模板参数的转发问题 > > 可变模板参数简便了我们处理多个数据的代码 > > lambda和包装器使代码形式多样化

相关推荐
MSTcheng.2 小时前
【C++】set / multiset 保姆级教程:从底层原理到实战应用!
开发语言·c++·set
_dindong2 小时前
算法杂谈:回溯路线
数据结构·算法·动态规划·bfs·宽度优先
巴拉巴拉~~2 小时前
KMP 算法通用步进器组件:KmpStepperWidget 横向 / 纵向 + 匹配进度 + 全样式自定义
java·服务器·开发语言
weixin_307779132 小时前
赋能插件,驱动图表:Jenkins ECharts API插件详解
运维·开发语言·自动化·jenkins·echarts
IMPYLH2 小时前
Lua 的 Math(数学) 模块
开发语言·笔记·lua
才鲸嵌入式2 小时前
香山CPU(国产开源)的 SoC SDK底层程序编写,以及其它开源SoC芯片介绍
c语言·单片机·嵌入式·arm·cpu·verilog·fpga
xiaoye-duck2 小时前
吃透C++类和对象(中):拷贝构造函数的深度解析
c++
kaikaile19952 小时前
雷达仿真中时域与频域脉冲压缩的对比 MATLAB实现
开发语言·matlab
胡闹542 小时前
【EasyExcel】字段赋值错乱问题
java·开发语言