c++11相关知识

C++11列表初始化 右值引用

文章目录

    • 列表初始化
      • 初始化列表的类型是怎样的
    • auto关键字和decltype关键字
      • auto
      • decltype
    • 右值引用
      • 1、左值和左值引用
      • 2、右值和右值引用
      • 左右值引用的特点
    • 右值引用的作用
      • 右值引用的使用场景
    • 万能引用和完美转发
    • 可变参数模板
      • 递归展开参数包
      • 逗号表达式展开参数包
    • lambda表达式
      • lambda表达式格式(也称lambda函数)
      • 捕捉列表
    • 包装器

列表初始化

c++11中的列表{}拓展了c++98中的用法,c++11的初始化列表不仅仅可以用来初始化数组还可以用来初始化STL容器及对象。

例如:

//初始化列表初始化vector
vector<int> v = { 1,2,43,45 };//不省略等号的初始化
vector<int> v1{ 23,3,51,0 };//省略等号的初始化
//列表初始化list容器
list<int> l1{5,3,9,2,6};//省略等号的初始化
//列表初始化map容器
map<string, int> m{ {"car",1},{"animal",2},{"house",5} };
Person p("张三", "33", "123");//普通初始化
Person p1{ "李四", "53", "124" };//列表初始化
Person* pp = new Person{ "王五","66","125" };//列表初始化

注:用初始化列表初始化容器或对象的时候列表前的等号可以省略!

c++11相关知识_第1张图片

初始化列表的类型是怎样的

auto l = { 2,4,5,3,2 };//l是初始化列表类型 用auto自动推导类型后打印
cout << typeid(l).name() << endl;//用typeid().name()打印其类型

答案:class std::initializer_list<>为初始化列表的类型

c++11相关知识_第2张图片

auto关键字和decltype关键字

auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局
部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将
其用于实现自动类型推导。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初
始化值的类型。

int i=10;
auto a=20;//编译器将会自动推导a的类型为int
auto p=&i;//根据显示初始化自动推导p的类型为int*
set<int> s{2,3,4,5};
auto it=s.begin();//自动推导it为set的迭代器类型

decltype

关键字decltype将变量的类型声明为表达式指定的类型。 其与auto关键字比较类似。

//用法:decltype(表达式) + 变量名
const int a=10;
int b=30;
decltype(a) c;
decltype(a+b) d;
decltype(&a) p;

decltype将括号中的表达式的类型推导出来供后边的变量使用,使其成为后面跟着的变量的类型!

右值引用

c++中的左值引用我们都知道,是为了减少传递参数和函数返回值时的拷贝构造提高效率。

那么右值引用是什么呢?接着往下看吧

1、左值和左值引用

左值是一个表示数据的表达式,如变量名,对象名。是可以获取地址并且可以对其赋值的(除const左值外),左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边,但是右值不可以出现在赋值符号的左边。定义const修饰的左值不可以给它赋值,但是可以取其地址。左值引用就是对左值的引用!

//a p b都是左值 因为他们都可以取到地址 对于const类型的左值可以取地址但是不可赋值
int a=10;
int* p=&a;
const int b=20;

int& ra=a;
int*& rp=p;
const int& rb=b;
//下面报错 不可以对const左值赋值!
rb=30;

c++11相关知识_第3张图片

2、右值和右值引用

右值也是一个表示数据进而表达式,但是这里的数据不是变量,而是具有常性的常量,如字面常量,表达式返回值,函数返回值(函数以值返回的时候,不会直接返回函数中的值,而是会拿函数中要返回的值拷贝一个临时变量,返回该临时变量,而临时变量具有常性(右值))等。右值可以出现在赋值符号的右边,不能出现在左边,右值不可取地址,右值引用就是对右值的引用!

注:右值分为两类,一类是字面常量等纯右值,一类是类似函数返回值构造出的临时对象的将亡值

int a=0,b=2;
//常见的右值类型
1//字面常量
a+b//表达式返回值
func(a,b)//函数返回值    

int func(int a, int b)
{
	return a + b;
}
void test1()
{
	int a = 1, b = 2;
	//对表达式返回值的引用
	int&& c = a+b;
	//对函数返回值的引用
	int&& k = func(a, b);
	//对字面常量的引用
	int&& x = 8;
	cout << x << endl;
	//通过右值引用修改右值
	x = 9;
	cout << x << endl;
	//右值被修改
    

}

c++11相关知识_第4张图片

上面的例子中x是字面常量8的引用,那么相当于x就是常量8 ,那么怎么可以对x赋值呢?

虽然右值不可以取地址,但是给右值取别名(右值引用)后,会导致右值被存储到特定的位置,并且可以取到该位置的地址,也就是说,对于字面常量不可以取到其地址,但是其被引用后就可以取到地址,取到地址就可以被修改,如果不想被修改就可以给将右值引用设为const类型的右值引用。

左右值引用的特点

左值引用特点:

  • 左值引用只能引用左值,不能引用右值
  • const左值引用既可以引用左值,也可以引用右值
void test()
{
    int a=1;
    int& r1=a;//正确 a为左值
    int& r2=1;//错误 1为字面常量 为右值
    
    const int& r3=a;//正确 const左值引用可以引用左值
    const int& r4=1;//正确 const左值引用可以引用右值
}

右值引用特点

  • 右值引用只能引用右值,不能引用左值
  • 右值引用可以引用被move后的左值
void func()
{
    int a=3;
    int&& r1=1;//正确 右值引用引用右值
    int&& r2=a;//错误 右值引用不可引用左值
    
    int&& r3=std::move(a);//正确 右值引用可以引用被move后的左值
}

注意:move只能将左值变右值,不能将右值变左值

c++11相关知识_第5张图片

右值引用的作用

上面介左值引用的时候讲到了const左值引用既可以引用左值也可以引用右值。那么为啥还需要引入右值引用这个概念呢? 往下看

左值引用的优缺点:

  • 优点:可以通过调用函数的时候通过参数用引用传参,减少深拷贝,函数返回值也可以用引用返回,提高效率。(不可以返回出了函数作用域就销毁的变量或对象的引用)
  • 缺点:当函数的返回值是一个局部变量,出了作用域就不存在了,就不能使用左值引用返回,只能传值返回,因为其出来作用域就会被销毁,资源还给操作系统。销毁后继续访问就是非法访问。

下面拿string类结合myto_string函数来举例说明:

namespace zh
{
	class string
	{
		typedef char* iterator;
	public:
        //构造函数
		string(const char* str = "")
			:_size(strlen(str))
			,_capacity(_size)
		{
			//这里需要注意的是要开辟的空间是需要包含\0在内的 不然释放的时候就会越界了!!!
			int a = 0, b = 0;
			_str = (char*)malloc(sizeof(char) * strlen(str)+1);
			while(_str[a++]=str[b++])
			{ }
			
		}

		void swap(string& str)
		{
			std::swap(_str, str._str);
			std::swap(_size, str._size);
			std::swap(_capacity, str._capacity);
		}
        //拷贝构造 深拷贝
		string(const string& str)
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
			cout << "string(const string& str) 深拷贝" << endl;
			string tmp(str._str);
			swap(tmp);

		}
        //赋值重载 深拷贝
		string& operator=(const string& str)
		{
			cout << "string& operator=(const string& str) 深拷贝赋值" << endl;
			string tmp(str);
			swap(tmp);
			return *this;
		}
        
        //传参用右值引用  新构造方法:移动构造
		string(string&& str)
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
			cout << "移动构造" << endl;
			swap(str);
			
		}
		//传参用右值引用 新赋值重载:移动重载
		string& operator=(string&& str)
		{
			cout << "移动赋值重载" << endl;
			swap(str);
			return *this;
		}
        
        }
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		~string()
		{
			delete _str;
		}
		const char* c_str()
		{
			return _str;
		}
        private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}
        
zh::string myto_string(int x)//数字转字符 这里是传值返回
{
    //这里的tmp是局部变量,出作用域就销毁,所以不可以传引用返回。
	zh::string tmp;
	while (x)
	{
		tmp += (x%10 + '0');
		x /= 10;
	}
	std::reverse(tmp.begin(), tmp.end());
	return tmp;
}

调用

int main()
{
    zh::string s=myto_string(10);
    zh::string str1(s);
    zh::string str2(move(s));//将s变为左值,和str2交换资源,那么s就会变成空,str2变成10
    zh::string str2(myto_string(10));
}

当把string中的移动构造屏蔽后运行执行代码 **zh::string=myto_string(10);**的时候myto_string的返回值会拷贝构造出一个临时对象进行返回,该临时对象又会去拷贝构造出s,这两次拷贝构造会被优化为一次也就是直接用tmp去拷贝构造出s。这里的拷贝构造的原理是先用形参去拷贝出一个和形参具有一样数剧的对象,再让需要构造的对象和该临时对象交换数据完成一次拷贝构造,所以这种拷贝构造需要额外开辟空间!!!

c++11相关知识_第6张图片

当string中有实现右值引用的拷贝构造函数的时候,调用**zh::string=myto_string(10);**的时候myto_string的返回值本身就是一个局部变量,出了作用域就会销毁,但是它的资源正是我们所想返回的,所以可以将其通过move变成右值,然后调用移动构造构造出临时对象,临时对象也是右值,再让临时对象去调用移动构造构造出s即可,那么这里是两次移动构造,编译器就会优化为一次移动构造,也就是该过程最终只有一次移动构造!!!

移动构造的实现原理是移动右值的资源,将其资源和需要该资源的对象交换,充分利用了有效的资源,且没有开辟额外的空间,效率相比传统的拷贝构造是非常有优势的!!!

c++11相关知识_第7张图片

右值引用的使用场景

如果一个函数的返回值是局部对象,那么就可以实现该对象的右值引用的拷贝构造函数,也就是所谓的移动构造了,同样还可以实现该对象的赋值移动构造,移动构造可以充分利用局部对象的资源,让其在销毁前发出自己的最后一道光芒,将自己的资源转移给需要它的对象,整个过程只存在交换,不存在开辟空间,效率非常高!!!

还有就是一个对象可以用右值来构造就可以实现该对象的移动构造,移动拷贝构造,移动赋值构造等;例如person类就可以采用右值引用做参数的构造函数。

万能引用和完美转发

当模板参数带上&&后就会变成万能引用,可以接收左值,const左值,右值,const右值。

例如:

void show(int& x) { cout << "左值引用" << endl; }
void show(int&& x) { cout << "右值引用" << endl; }
void show(const int& x) { cout << "const左值引用" << endl; }
void show(const int&& x) { cout << "const右值引用" << endl; }

template<class T>
void testUniversalReference(T&& t)
{
    
    show(t);
	
}
int main()
{
	int a = 1;
	const int ra1 = 2;//const左值引用
    
	testUniversalReference(a);//左值
	testUniversalReference(ra1);//const 左值
    //将左值move成右值
	testUniversalReference(std::move(a));//右值 
	testUniversalReference(std::move(ra1));// const 右值
}
  • 这里的模板参数T 加上&& 后就变成了 T&& ,这就是万能引用,但是t是一个引用,它是一个左值。当函数传参时传过来的是左值,那么T就是左值,传过来的是右值T就是右值,但是这只是T的类型与传递过来的参数类型有关,t的类型是一个万能引用类型,它是左值,当调用show(t)的时候,这里的t就是一个左值,那么与之匹配的也就全是形参为左值引用的show函数了。

show(t)的运行结果:
c++11相关知识_第8张图片

可以观察到,不论实参传的是左值还是右值,通过万能引用t接收后,再调用show函数,都是匹配的形参为左值引用和const左值引用的show函数!!! 这是因为t始终是左值,那么调用show函数自然匹配的也是形参为左值的函数了

解决方法:完美转发 就是将t用std::forward(t)转化一下,将其变成与T相同的类型,传过来的是左值,那么t被完美转发就变成左值,同理传过来的是右值t就会被完美转发为右值,这样去调用show函数的时候就可以根据传递的实参的类型去匹配与之对应的函数了。

需要注意的是这里的forward ,尖括号中的类型必须是模板参数的类型,也就是实参的类型,需要和实参的类型保持一致才可以完成完美转发!!!

引入完美转发的代码:

void show(int& x) { cout << "左值引用" << endl; }
void show(int&& x) { cout << "右值引用" << endl; }
void show(const int& x) { cout << "const左值引用" << endl; }
void show(const int&& x) { cout << "const右值引用" << endl; }

template<class T>
void testUniversalReference(T&& t)
{
	//这里的T是接收参数的类型 如果是左值引用传过来那么T就是左值引用 如果是右值引用传过来就是右值引用 
	//但是这里的t是引用的引用就是左值 而不是右值  这是关键 t是左值  那么show的时候都是走的左值引用 没有右值引用
	//
	//如果想要还原t的类型和T的类型一致 就可以用std::forward(t)来解决   这叫做完美转发
	show(std::forward<T>(t));	
	
}
int main()
{
	int a = 1;
	const int ra1 = 2;//const左值引用
    
	testUniversalReference(a);//左值
	testUniversalReference(ra1);//const 左值
    //将左值move成右值
	testUniversalReference(std::move(a));//右值 
	testUniversalReference(std::move(ra1));// const 右值
}

运行结果:

c++11相关知识_第9张图片

实参被t接收后,经过完美转发将t转化为了与实参一致的类型,然后调用show函数就调到了与之正确匹配的函数,实参是左值t调的是形参为左值的show函数,实参是右值,t调用的是形参为右值的show函数!!! 这就是完美转发的作用

可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比与C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数是一个巨大的改进 。

template<class ... Ts>
//Ts是一个模板参数包 ts是一个函数形参参数包
// 声明一个参数包Ts... ts,这个参数包中可以包含0到任意个模板参数。
template <class ...Ts>
void ShowList(Ts... ts)
{}

ts前面有三个点 代表它是一个可变模板参数,这种带三点的参数叫做参数包,它里面可以包含多个模板参数,但是我们不能直接获取参数包中的每个参数,只能通过展开参数包的方式来获取它里面的每个参数,下面来介绍几种获取参数包中参数的方法。

递归展开参数包

//方法一
//空参的重载函数是为了当参数包中的参数空了的时候走这个函数递归结束 出口
void tiePackage()
{

}

template<class T,class ...S>
void tiePackage(T t, S... s)
{
	cout << sizeof...(s) << endl;
	cout <<typeid(t).name()<<" " <<t << endl;
    //递归 每次都会将s... 拆分成两份 第一份为一个参数 第二个为拆分出的子参数包
    //使得调用tiePackage(T t,S ...s)
    //如果参数包为空就走无参的重载函数结束!!!
	tiePackage(s...);
}

c++11相关知识_第10张图片

注意:这里的理解思路就是当参数包不为空的时候就会一直拆分成两份,调用自己递归,其中第一份是是参数包的第一个参数,第二份是出第一个参数外的子参数包,这两份作为两个参数,使得函数可以继续调用自己。如果参数包空就走空参的重载函数结束递归。递归调用传的实参一定得是参数包!!!这样才可以递归拆分参数包获取参数包中的每个参数。

逗号表达式展开参数包

我们知道,逗号表达式是从前往后依次计算表达式,并且表达式的值为最后一个表达式的值。

因此可以通过下面这样的方法来展开参数包

template<class T>
void func(const T& t)
{
	cout << typeid(t).name() <<" :"<<t << endl;
}

template<class ...S>
void tiePackage(S ...s)
{
	int arr[]{ (func(s),0)... };
}

c++11相关知识_第11张图片

通过调用func函数去解析参数包中的第一个参数,然后逗号表达式执行完返回值为0,这个返回值会被放到数组中,因为这里的(func(s),0)后边有三个点,说明其是一个参数包,这里的列表会逐个解析参数包中的每个参数,并用逗号表达式的返回值去初始化数组。

改造逗号表达式展开参数包

  • 可以简化逗号表达式,让函数有返回值即可,就可以根据函数返回值来初始化数组的同时展开了参数包!
template<class T>
int func(const T& t)
{
	cout << typeid(t).name() <<" :"<<t << endl;
    return 0;
}

template<class ...S>
void tiePackage(S ...s)
{
	int arr[]{ func(s)... };
}

c++11相关知识_第12张图片

lambda表达式

lambda表达式格式(也称lambda函数)

[捕捉列表](参数列表)mutable->返回值类型{函数体}
例如实现一个两数相加的lambda表达式
[](int a,int b)->int{return a+b;};
//上式可以简化为 返回值可由编译器自动推导为int
[](int a,int b){return a+b;};

解析:

  • c++11有auto关键字,可以自动推导类型,如果lambda表达式的返回值可以被明确地推导出来,那么表达式中的返回值和箭头就都可以省略掉

  • 如果lambda表达式没有参数,那么参数列表也可以省略掉。

  • lambda是一个const 函数,其参数是带const 属性的,所以是右值,不可以赋值,修改,如果要对其修改,就需要在参数列表后面加上关键字mutable

捕捉列表

  • 捕捉列表是可以为空的
  • 捕捉列表可以对当前作用域的变量进行捕捉,可以是值捕捉,也可以是引用捕捉。
  • 捕捉列表可由多个捕捉项组成,每个捕捉项由逗号分割
  • 值捕捉项和引用捕捉项可以有相同对象,但是不允许两个值捕捉项有重复捕捉的对象,两个引用捕捉项也是如此,例如a b 为当前作用域变量 可以有[=,&a,&b] 但是不可以有[=,a,b] 后者有重复值捕捉,编译器会报错
  • [=]代表以值捕捉的形式捕捉当前作用域的所有变量,也可以指定值捕捉某个对象[对象名](不要加=)
  • [&]代表以引用捕捉的形式捕捉当前作用域的所有变量,也可以指定引用捕捉某个对象[&对象名]
  • [this]表示的是值捕捉的形式捕捉当前对象的this指针!!!
void testlambda()
{
	//lambda表达式的格式为
	//[捕捉列表](参数列表)mutable->返回值类型{ 函数体 }
	//当函数没有参数时参数列表和括号可以一起省略  lambda函数默认就是const函数加上mutable可解除其const属性 
	//当返回值类型明确时 返回值也可以省略


	int a = 10, b = 23;
	//这里的add就是一个lambda表达式 
	auto add = [](int a, int b) {return a + b; };
	cout << add(a, b) << endl;
	//捕捉列表为空 调用靠自己传参
	auto add1 = [](int a, int b)mutable->int {return a + b; };
	cout << add(1, 45) << endl;
	//捕捉变量 a b 返回a+b  这里的捕捉列表中的a b只是上面 a b 的拷贝,而非 a b 称为复制捕捉 
	auto add2 = [a, b] {return a + b; };
	cout << add2() << endl;
	//捕捉列表中的等于号代表着以复制捕捉的形式捕捉当前作用域的所有变量 
	auto add3 = [=] {  return a + b; };//这里的a b就是上面的 a b的值拷贝,可理解为函数的形参
	cout << add3() << endl;
	//引用捕捉 捕捉到的是该作用域中变量的引用 可以通过引用改变变量 
	auto add4 = [&a](int b) {a -= 10; return a + b; };
	cout << add4(b) << endl;
	//引用捕捉 捕捉当前作用域中所有变量的引用
	auto add5 = [&] { a += 10; return a;};
	cout << add5() << endl;
	//引用捕捉的方式可以实现在lambda表达式中改变其捕捉的对象!!! 
}

c++11相关知识_第13张图片

注意:lambda表达式之间不可以相互复制,即使格式一模一样也不可以。

因为在反汇编下,每个lambda表达式都是由lambda_(uuid)::operator()构成的(实质性lambda的底层原理就是一个仿函数),它们的uuid不相同,代表是不同的类型,不可以相互赋值!!!但是奇怪的是可以用一个lambda表达式去拷贝构造出另一个lambda表达式,它们功能上相同,但是类型不同。

c++11相关知识_第14张图片

c++11相关知识_第15张图片

c++11相关知识_第16张图片

包装器

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器

std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参

注意:

  • 包装器接收静态成员函数的时候需要指定类中的见静态成员函数(可以加& 也可以不加&)

相关代码:

//普通函数
int f(int a,int b)
{
	cout <<"int f(int a,int b) "<< endl;
	return a + b;
}
//仿函数
struct F
{
	double operator()(double a,double b)
	{
		cout << "double operator()(double a,double b) " << endl;
		return a + b;
	}
};

class caculate
{
public:
	caculate(int x=0)
		:_x(x)
	{}
    //成员函数
	static double clt(double a,double b)
	{
		return a + b;
	}
	int clt1(int a, int b)
	{
		return a - b - _x;
	}

private:
	int _x;
};

void mytestpackagefunc()
{
    //lambda表达式
	auto lamf = [](char a, char b) {cout << "[](char a,char b){} " << endl; return a-'0'+  b-'0' + '0'; };
    //包装器接收普通函数
	std::function<int(int,int)> func= f;
	cout << func(4, 4) << endl;
     //用包装器接收lambda表达式
	std::function<char(char, char)> func1 = lamf;
	cout << func1('1', '3') << endl;
    //包装器接收仿函数
	std::function<double(double, double)> func2 = F();
	cout << func2(2.22, 3.33) << endl;
	//包装器接收静态成员函数
	std::function<double(double, double)>  func3 = caculate::clt;
	cout << func3(2.3, 4.2) << endl;
	//包装器接收非静态成员函数
	std::function<int(caculate,int, int)> func4 = &caculate::clt1;
	cout << func4(caculate(),3, 4) << endl;
	//bind() 用来调整函数的参数个数和顺序
	std::function<int(int, int)> func5 = bind(&caculate::clt1,caculate(), placeholders::_1, placeholders::_2);
	cout << func5(2, 9) << endl;
	//这里的_1  _2 代表着接收的是第几个参数 
	std::function<int(int, int)> func6 = bind(&caculate::clt1, caculate(), placeholders::_2, placeholders::_1);
	cout << func6(2, 9) << endl;
	

}

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可
调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而
言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M
可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺
序调整等操作。

bind()的第一个参数是一个可调用对象,往后都是参数列表i,这里的例子中的第二个参数就是一个匿名对象,替代非静态成员函数中的this指针,再后面的参数就是函数的实际参数,通过bind还可以用placeholders::_x 来代表这个位置接收的是调用的时候实参中的第几个参数,以此来调整参数顺序。

c++11相关知识_第17张图片

你可能感兴趣的:(c++,c++,开发语言,c语言,c++11)