1.列表初始化

C++98中一般只有数组与结构体才能使用{}列表初始化。

而C++11以后想统一了初始化格式,试图实现一切对象皆可以{}列表初始化。

内置类型支持,自定义类型也支持,自定义类型本质是进行类型转换,中间会生成临时对象,在经过编译器优化后变成了直接构造。

列表初始化过程中可以省略=。

2.initializer初始化

列表初始化不便于容器的初始化,当容器要初始化大量数据时意味着容器内部要有更多与之参数匹配的构造函数。

这时我们使用C++11库提供的类initializer,这个类的本质是在底层中开辟一个数组,并将数据拷贝过来,类中有两个指针分别指向数组的开始和结束,类支持迭代器遍历。

C++11的STL容器都提供了一个接受库的initializer参数的构造函数,能够支持{x1,x2...xn}这种形式的任意多个值的列表初始化。

3.右值引用

C++98中就有着引用的语法,C++11中又新增了右值引用的语法,在C++11之前的引用被称为左值引用,无论是左值引用还是右值引用本质上都是在给对象取别名。

int x=1;
T& a=x;//左值引用
T&& b=1;//右值引用

在之前的文章中我们有所提到了左值与右值的区别,在这里就不做过多的赘述。

只要记住左值能取地址,右值不能取地址。

4.左值引用

int x=1;
T& a=x;//左值引用
T&& b=1;//右值引用

如上述代码,左值引用就是给左值取别名,右值引用就是给右值取别名。

左值引用不能直接引用右值,需要加上const,否则会构成权限放大问题。

右值引用不能直接引用左值,需要move(左值),否则会构成权限缩小问题。

int x=1;
T& a=x;//左值引用绑定左值
T&& b=1;//右值引用绑定右值
const T& c=1;//通过const修饰能够绑定右值

变量表达式都是左值属性,这也就意味着当右值被右值引用后,这个右值引用变量表达式是左值属性,你可以通过对右值引用变量来修改它所绑定的对象。

move是库里的一个函数模版,本质上进行强制类型转换,将左值标记为了右值,实质上是只将这个表达式改为了右值,对象本身不发生任何改变。

int x=1;
T&& a=(T&&)x;//move实质,强制类型转换

从汇编的角度来看,不管是左值引用还是右值引用都是在底层通过封装对应指针而实现的。

int x=1;
T& a=x;//左值引用绑定左值
T&& b=1;//右值引用绑定右值
const T& c=1;//通过const修饰能够绑定右值
T&& d=move(x);//通过move标记x为右值属性

需要注意,要使用move函数要包头文件,include<utility>。

还有一些右值引用实例。

//右值引用
T&& x=string("111");//匿名对象
T&& y=a+b;
T&& x1=max(a,b);//函数返回值

5.引用延长生命周期

右值引用与const左值引用可以为临时对象延长生命周期,与引用变量的生命周期保持一致,但这些对象无法被修改更无法被随意更换存储空间。

6.左值与右值的参数匹配

在C++98中,使用const左值引用作为参数的函数,实参传递左值与右值都能与之匹配。

C++11中分别重载了左值引用,const左值引用,右值引用作为函数参数,那么传的参数就会自适应匹配。

右值引用变量在用于表达式时属性是左值。

左值引用的使用场景主要是用于左值引用传参和左值引用返回时减少拷贝,修改对应的实参和返回对象。

但也有些场景无法使用传左值引用返回,例如返回一个局部对象时,即便是C++11推出的右值引用来返回这个右值,也无法解决。

因为当函数结束,这个局部对象就析构销毁了,左值引用与右值引用即使将变量的生命周期延长与引用变量一致,但在根本上是无法改变变量的存储位置。

传值返回,存在拷贝开销;指针返回,却带来内存管理风险。

当然还可以使用输出型参数。

创建个变量,将变量传入函数,任其在函数体内被修改,可以避免传值返回引发的两次拷贝。

int add(int x,int y,int& sum)
{
    sum=x+y;
    return sum;
}

int main()
{
    int sum=0,a=10,b=100;
    add(a,b,sum);
    cout<<sum<<endl;
    return 0;
}

7.移动构造与移动赋值

在之前我们其实也是见到过移动构造的原理的,那就是在实现拷贝构造的现代写法上。

//交换资源函数
void swap(string& x)
{
	::swap(_str, x._str);
	::swap(_size, x._size);
	::swap(_capacity, x._capacity);
}
//拷贝构造
string(const string& a)
{
	string temp(a._str);
	swap(temp);
}

移动构造函数,一种类似于拷贝构造函数,它要求第一个参数是该类类型的右值引用,额外的参数必须得有缺省值。

移动赋值,与拷贝赋值函数构成重载,类似于拷贝赋值函数,它要求第一个参数必须得是该类类型的右值引用。

他们的本质是进行夺舍右值对象的资源,而不是像拷贝构造和拷贝赋值一样去拷贝资源,毕竟在交换完右值对象的资源后,右值对象一出作用域就会被自动析构销毁,从而避免了拷贝的开销。

//交换资源函数
void swap(string& x)
{
	::swap(_str, x._str);
	::swap(_size, x._size);
	::swap(_capacity, x._capacity);
}
//移动构造函数
string(string&& a)
{
	cout << "string(string&& a)" << endl;
	swap(a);
}
//移动赋值
string& operator=(string&& a)
{
	if (this != &a)
	{
		cout << "string& operator=(string&& a)" << endl;
		swap(a);
	}
	return *this;
}

7.1用右值引用解决传值返回问题

利用移动构造解决传值返回问题

namespace my
{
	class string
	{
	public:
		//默认构造
		string(const char* str = "")
			:_size(strlen(str)), _capacity(_size)
		{
			cout << "string(const char* str = "")" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		//交换资源函数
		void swap(string& x)
		{
			::swap(_str, x._str);
			::swap(_size, x._size);
			::swap(_capacity, x._capacity);
		}
		//拷贝构造
		string(const string& a)
		{
			string temp(a._str);
			swap(temp);
		}
		//移动构造函数
		string(string&& a)
		{
			cout << "string(string&& a)" << endl;
			swap(a);
		}
		//移动赋值
		string& operator=(string&& a)
		{
			if (this != &a)
			{
				cout << "string& operator=(string&& a)" << endl;
				swap(a);
			}
			return *this;
		}
		//重载+=运算符
		string& operator+=(const string& a)
		{
			size_t size = _size + a._size;
			if (size > _capacity)
			{
				_capacity = size;
				char* n_str = new char[_capacity * 2];
				if (_str)
				{
					strcpy(n_str, _str);
					delete[] _str;
				}
				_str = n_str;
			}
			strcpy(_str + _size, a._str);
			_size = size;
			return *this;
		}
		////重载赋值操作符,现代版本
		//string& operator=(const string& a)
		//{
		//	string temp(a);
		//	swap(temp);
		//	return *this;
		//}
		//重载赋值操作符,手动版本
		string& operator=(const string& a)
		{
			if (this != &a)
			{
				char* n_str = new char[a._capacity + 1];
				strcpy(n_str, a._str);
				delete[] _str;
				_str = n_str;
				_size = a._size;
				_capacity = a._capacity;
			}
			return *this;
		}
		// 返回c语言指针,可以不用重载流运算符
		const char* c_str() const
		{
			return _str;
		}
		//析构函数
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = _size;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
	//传值返回函数,模拟+=
	string add(string& a, string& b)
	{
		string str(a);
		str += b;
		cout << (void*)str.c_str() << endl;
		return str;
	}
}

int main()
{
	my::string a1 = "111", a2 = "222";
	my::string a3 = add(a1, a2);
	cout << (void*)a3.c_str() << endl;
	return 0;
}

最终程序的输出结果为

str与a3地址相同,证明了没有进行拷贝构造重新分配地址,而是移动语义生效交换两者资源。

7.2编译器下对有无移动构造与移动赋值的优化

VS2022debug版本下,在上述代码下

当没有移动构造时。

编译器会在str的构造与str的拷贝构造临时对象,临时对象拷贝构造a3基础上,直接优化成str构造a3。

当既有拷贝构造又有移动构造时。

编译器会直接优化,str本质就是对a3对象的引用。

8.类型区别

C++11以后,对类型进行了更细的划分。

右值被分为了纯右值和将亡值。

纯右值等价于C++98中的右值。

将亡值指的是生命周期快结束时,可以被移动资源的对象,如匿名对象,被move的左值,即将被销毁的局部变量等。

泛左值包含了将亡值和左值。

9.引用折叠

普通模板虽能通过实例化适配不同类型的模板参数,但在引入左值引用与右值引用后,若想让模板自动区分并实例化出对应引用类型,单纯依赖模板特化会导致代码冗余、扩展性不足。

这个时候就要提到引用折叠这一概念了,并且与上文所提到的右值引用表达式是一个左值属性也有极大的关系。

引用折叠是指只有右值引用的右值引用才是右值引用,其他的都是左值引用。

typedef int&& ri;
typedef int& li;//这里使用typedef来观察引用折叠
int x=1;
int&& a=1;//右值引用
int& a1=x;//左值引用
int& b=a;//左值引用

ri& c=x;//左值引用
ri&& c1=1;//右值引用
li& c2=x;//左值引用
li&& c3=x;//左值引用

可以看做是只看最终的引用类型,再去判断绑定值类型是否一致。

通过这一点,再与模版相结合,于是就诞生了万能引用函数模版,根据折叠引用,传左则左,传右则右,函数模版参数为右值引用。

template<class T>
void add(T&& x)
{
    ...;
}

int main()
{
    int n=0;
    add<int&>(n);//左值引用
    add<int&&>(0);//右值引用
    add<const int&&>(0);//const修饰的右值引用
    add<const int&>(n);//const修饰的左值引用/右值绑定
    return 0;
}

10.完美转发

当右值被右值引用绑定后,右值引用表达式的属性变成左值,再传给下一个函数参数时的左值属性会导致匹配到左值引用的重载版本。

若要继续保持右值属性,则要使用完美转发。

完美转发forward本质上是一个模版参数,主要是通过引用折叠所实现的。

在传给下个函数的参数前面加上forward<T>,所需要被包含的头文件是utility。

void fun(int& x)
{
    cout<<"void fun(int& x)"<<endl;
}

void fun(int&& x)
{
    cout<<"void fun(int&& x)"<<endl;
}

template<class T>
void add(T&& x)
{
    fun(forward<T>(x));//完美转发
}

int main()
{
    add(1);
    return 0;
}

11.可变参数模版

11.1语法与原理

C++11支持可变参数模版,意味着支持可变数量参数的函数模版和类模版。

可变数量的参数被称为参数包,存在两种参数包,函数参数包,模版参数包。

template<class ...Args>
void add(Args ...args)
{
    cout<<sizeof...(args)<<endl;
}

我们用省略号来指出一个模版参数或者函数参数的参数包,用class/typename ...Args来指出接下来的参数表示0或者多个类型的模版参数,其中Args是一个类型名。

函数参数包也可以使用左/右值引用,遵循引用折叠。

可变参数模版与模版类类似,使用sizeof...计算参数包的参数个数。

可变参数模版是类型泛化基础上叠加数量变化。

11.2包扩展

参数包的核心操作是查询大小和扩展。扩展通过在模式后附加省略号 (...) 实现,它会将参数包展开,为每个元素应用模式,从而生成一个新的列表。

C++还支持更复杂的包扩展,直接将参数包依次展开作为实参传给函数处理。

如下,通过递归调用实现的万能引用可变参数模版。

void showlist()
{
	//利用递归的终止条件,当参数包内的1参数个数为0时直接匹配当前函数
	cout << endl;
}

//解析参数包内容
template<class T,class ...Args>
void showlist(T&& x, Args&& ...args)
{
	cout << x << " ";
	showlist(args...);
}

template<class ...Args>
void print(Args ...args)
{
	showlist(args...);
	cout << endl;
	cout << sizeof...(args) << endl;//计算有多少个可变参数数量
}

int main()
{
	/*print(1, 1);
	print(1, 1, 1, 1, 11, 1, 1);*/
	int x1{ 1 };//一切对象可以使用列表初始化,可以省略=
	cout << x1 << endl;
	return 0;
}

完美转发与基于模板部分特化和递归的编译期类型检查(如参数包类型包含性判断)等模板元编程技巧,在此暂不介绍。

11.3emplace系列接口

C++11之后的STL容器都新增了empalce系列接口,empalce系列的接口均为可变模版参数,兼容了push和insert系列,还支持了一些新的用法。

当有一个容器为container<T>时,empalce支持直接插入构造T对象的参数,可以直接在容器空间上调用T的构造函数来构造对象。

因为emplace能够直接在目标容器原地址上进行直接够造对象,所以它的总体上效率很高,可以代替日常insert和push的使用。

在此基础上如果我们需要实现万能引用模版,就需要使用完美转发来确保变量的右值属性不会被更改。

12.默认移动构造和移动赋值

C++98中,类有着六个默认成员函数,构造函数/拷贝构造/析构函数/拷贝赋值重载/取地址重载/const取地址重载,核心的是前四个成员函数。

默认成员函数就是你不写,编译器会默认生成一个,在C++11后提供的两个默认成员函数就是移动构造和移动赋值。

当你没有实现移动构造,析构函数,拷贝构造,拷贝赋值重载中的任一个时,编译器就会自动生成一个默认移动构造。

默认生成的移动构造会对内置类型成员逐字节拷贝,也就是浅拷贝,自定义类型成员,则是有移动构造就调用移动构造,无则调用拷贝构造。

当你没有实现移动赋值重载,且没有实现析构,拷贝,拷贝赋值重载中的任一个时,编译器就会自动生成一个默认移动赋值。

默认移动赋值与默认移动构造完全类似。

13.关键字

当我们显式的提供了拷贝构造函数时,编译器就不会生成移动构造函数了,若要让其强制生成移动构造函数时就需要使用关键字default。

在C++98中限制对应的默认函数版本生成,只需要将访问限定符改为private即可;在C++11中只需要声明关键字delete,意思是删除函数对应的默认版本。

class string
{
public:
    string(const char* str="")
    :_size=(strlen(str)),_capacity(_size)
    {...};
    string(const string& a)
    {
        ...;
    }
    //在有拷贝构造函数下用关键字强制生成移动构造函数
    string(string&& a)=default;
    //当没有显式实现拷贝构造函数时,用关键字强制删除默认拷贝构造函数版本
    //string(const string& a)=delete;
    ~string()
    {
        ...;
    }
  
private:
    char* str=nullptr;
    size_t _size=0;
    size_t _capacity=0;
}

14.STL中的一些变化

在 STL 容器中,unordered_map和unordered_set最为常用。

C++11为容器带来的新接口中,与右值引用和移动语义相关的(如 emplace 系列、移动构造 / 赋值)以及支持 initializer_list 的构造函数是重中之重。

其他如 cbegin/cend 等,用到时查阅文档即可,至于容器的范围 for 循环遍历,之前也已介绍过。

15.lambda表达式

lambda表达式本质上是一个匿名函数对象,跟普通的函数不同,它可以定义在函数内部。

lamdba表达式的类型一般是用auto或者是模版参数定义的对象去接收lambda对象。

lamdba表达式的语法规则,[捕捉列表] (参数列表) ->返回值类型 {函数体}。

编译器根据[]判断是否为lamdba函数,捕捉列表能够提取上下文的变量供lamdba函数使用,捕捉列表可以使用传值或者引用,但捕捉列表不可以被省略。

参数列表和普通函数参数列表相似可以连同()省略。

返回值类型,用于追踪返回类型形式声明函数的返回类型,没有返回值时可以省略。一般返回值类型确定时,也可以省略这部分,由编译器对其进行类型推导。

函数体,与普通的函数体完全类似,可以使用捕捉的变量,函数体内容可以省略。

可以说[] {};是最简lambda表达式。

//lambda表达式
auto fun=[](int x,int y)->int{return x+y;};
cout<<fun(1,1)<<endl;

15.1捕捉列表

lambda表达式只能捕捉定义所在的作用域当中的变量。

其中捕捉又分为显式捕捉与隐式捕捉。

显式捕捉又分为传值与传引用,传值过来的可以当做被const修饰过的变量,不能被修改。

//显式捕捉
int x = 1;
//传值,x不能被修改
auto fun = [x](int a)->int {a = 1; return a + x; };
//传引用,x可以被修改
auto fun1 = [&x](int a)->int {x = 2; a = 1; return a + x; };
cout << fun1(1) << endl;
cout << fun(10) << endl;

[=]隐式值捕捉,所有变量;[&],隐式的引用捕捉。

其中隐式捕捉,不是全部捕捉,而是你用哪个就捕捉哪个

混合捕捉,大部分想引用捕捉,特例的用值捕捉[&,a,b],并且混合捕捉时第一个元素必须是&或=,后续元素必须是默认方式的反向例外。

在lambda表达式的参数列表后面加上mutable能取消值捕捉的const属性,在函数体内对其任意修改不会影响到外部的变量,参数列表不能省略,该关键字是给()参数列表里面的函数调用运算符使用的。

//隐式捕捉
int x11 = 0, x2 = 1;
//隐式值捕捉
auto fun = [=] {return x11 + x2; };
//隐式引用捕捉
auto fun1 = [&] { x11 = 1; return x11 + x2; };
//混合捕捉
auto fun2 = [&, x2] { x11 = 2; return x11 + x2; };
cout << fun() << endl;
cout << fun1() << endl;
cout << fun2() << endl;

lambda表达式如果在函数局部域中,它可以捕捉表达式位置之前定义的变量,不能捕捉静态局部变量与全局变量,当然静态局部变量与全局变量不需要捕捉即可使用,意味着当lambda表达式定义在全局中,捕捉列表必须为空。

15.2lambda应用

在学习lambda之前,对于可调用对象只有函数指针和仿函数对象。

函数指针类型定义麻烦,而仿函数对象需要定义一个类,相对比而言lambda表达式的使用就变得十分简单了。

并且lambda表达式在1很多地方都用的到,如线程中定义线程的执行函数逻辑,智能指针中的定制删除器等。

15.3lambda原理

lambda与范围for很像,编译后从汇编指令层角度来看,都是通过类似的封装来实现的,范围for是迭代器,而lambda则是仿函数对象,也就是说当我们写了一个lambda表达式后,编译器会生成一个对应的仿函数的类。

仿函数的类名是编译按一定规则生成的,保证不同的lambda生成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体,lambda的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,当然隐式捕捉的话,编译器是用谁传谁。

16.包装器

16.1function

function是一个类模版也是一个包装器,它的实例对象可以包装存储其他可调用的对象,如仿函数对象,函数指针,lambda表达式等,存储的可调用对象则称为function的目标。

若function中不含目标,则称空,调用空的function的目标会导致抛bad_function_call异常出来,这时需要try-catch来捕获异常。

function被包含在<functional>头文件中。

function的优势就是在于统一不同类型的调用对象,对其进行包装,这样在很多地方方便了声明可调用对象。

在包装非静态成员函数时要在前面指明类域并且加上&才能取到地址。

普通成员对象还包含了一个隐含的this指针参数,所以绑定时传对象或者对象的指针过去即可。

int add(int x,int y)
{
    return x+y;
}

class fun
{
public:
    int operator()(int x, int y)
    {
        return x+y;
    }
    
    int device(int x,int y)
    {
        return x-y;
    }
    
    static int funi(int x,int y)
    {
        return x*y;
    }
}

int main()
{
    fun obj;
    function<int(int, int)> f = [](int x, int y)->int {return x + y; };//包装lambda
    function<int(int,int)> func=fun();//包装可调用对象
    function<int(int,int)> func1=add;//包装函数
    function<int(fun&,int,int)> func2=&fun::device;//包装普通成员函数
    function<int(int,int)> func3=fun::funi;//包装静态成员函数
    cout<<f(1,1)<<endl;
    cout<<func(1,1)<<endl;
    cout<<func1(1,1)<<endl;
    cout<<func2(obj,1,1)<<endl;
    cout<<func3(1,1)<<endl;
    return 0;
}

至于为什么func能直接调用,是因为func内部存储的目标operator()是对象本身包含了所需逻辑,不需要外部资源了,func2却只是类成员中的地址。

至于func3则是包装静态成员函数,它没有隐含的this指针。

总结就是,func带对象,func2带地址,func2调用需要额外传对象。

16.2bind

bind是一个函数模版,也是一个可以调用对象的包装器,也可以看做是一个函数适配器,本质上返回的是一个仿函数对象。

它可以用来调整参数个数,参数顺序,也被包含在了头文件<functional>里。

bind语法:

auto newcallable=bind(callable,arg_list);

其中newcallable是一个可调用对象,arg_list函数参数包。

当调用newcallable,会调用callable,并传给它参数包中的参数,参数包中可能包含如_n的名字n为整数,这些参数为占位符表示newcallable的参数,占据了传递给callable对应参数的位置。

数值n表示生成可调用对象中参数的位置,如_1为第一个参数以此类推,这些占位符会放到placeholders的一个命名空间中。

int func(int x,int y)
{
    return (x+1)*y;
}

int main()
{
    auto a0=bind(func,_1,_2);
    cout<<a0(1,2)<<endl;
    //改变参数顺序
    auto a1=bind(func,_2,_1);
    cout<<a1(1,2)<<endl;
    //改变参数
    auto a2=bind(func,_1,100);
    auto a3=bind(func,100,_1);
    cout<<a2(1,1)<<endl;
    cout<<a3(1,2)<<endl;
    return 0;
}

与function结合,当对成员函数,lambda之类的进行绑死时。

class fun
{
public:
    int operator()(int x, int y)
    {
        return x+y;
    }
    
    int device(int x,int y)
    {
        return x-y;
    }
    
    static int funi(int x,int y)
    {
        return x*y;
    }
}

int main()
{
    //用bind绑死固定参数,每次就不需要传对象了
    function<int(int,int)> a1=bind(&fun::device,fun(),_1,_2);
    cout<<a1(1,2)<<endl;
    //对lambda进行绑定,用于重复计算之类的场景
    auto a2=[] (int x,int y)->int{return x+y;};
    function<int(int,int)> a3=bind(a2,_1,_2);
    return 0;
}

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐