C++进阶篇6---C++11新语法

目录

目录

一、统一的列表初始化

 二、声明

1.auto

2.decltype

3.nullptr

三、范围for

四、STL中的变化

五、右值引用和移动语义(重点)


一、统一的列表初始化

在c++11之前,我们能用{}初始化数组和结构体

struct Point {
	int x;
	int y;
};
int main()
{
	int a[] = { 1,2,3,4 };
	Point p = { 1,1 };
	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;
}


//创建对象时也可以使用列表初始化方式调用构造函数初始化
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date x(2023, 11, 30);

	Date y = { 2023,11,30 };
	Date z{ 2023,11,30 };
	return 0;
}

这些功能的实现和initializer_list这个容器有关

使用场景:

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器增加了std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator= 的参数,这样就可以用大括号赋值
int main()
{
	vector<int>v = { 0,1,2,3,4 };
	list<int>l = { 2,3,5,6,30 };
	map<string, string>mp = { {"string","字符串"} ,{"sort","排序"} };
	for (auto x : v)
	{
		cout << x << " ";
	}
	cout << endl;

	for (auto x : l)
	{
		cout << x << " ";
	}
	cout << endl;

	for (auto x : mp)
	{
		cout << x.first << ":" << x.second << endl;
	}

    v = { 1,2,34,5,6 };
	for (auto x : v)
	{
		cout << x << " ";
	}
	cout << endl;
	return 0;
}

这个{}初始化和赋值不难实现,我就拿之前写过的模拟实现的vector来举个例子,这里放关键的函数,如果对vector的模拟实现感兴趣可以去看C++入门篇8,里面有完整模拟实现代码


 二、声明

1.auto

C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型,简单来说,就是能自动推导数据类型,相信大家都很熟悉,这里就不多说了

2.decltype

关键字decltype将变量的类型声明为表达式指定的类型
int main()
{
	int x = 1;
	double y = 2.2;
	decltype(x * y) ret = x * y;//这个用法和auto没啥区别
	vector<decltype(x * y)>v;//这里只能用decltype
	//vector<auto>v,错误写法
	return 0;
}

也就是说,当我们需要类型作为参数时,只能用decltype

3.nullptr

这个也不多说,因为C++官方将NULL定义为了0,所以加了一个nullptr表示空指针


三、范围for

底层就是迭代器遍历容器。

int main()
{
	vector<int>v{ 1,2,3,4,5,6 };
	for (auto& e : v)//范围for
		cout << e << " ";
	return 0;
}

四、STL中的变化

多了静态数组、单链表和哈希表,还有一些接口,如cbegin、cend、emplace等

大致说说这些容器的情况:静态数组array比较鸡肋,因为vector完全够用,forward_list单链表也作用不大,也就是比较省空间,哈希表还是很有用的


五、右值引用和移动语义(重点)

我们之前学的引用又被叫做左值引用,其实不管是什么引用,都是给对象取别名

什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取名
int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pval = *p;
	return 0;
}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);
	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	// 这里编译会报错:error C2106: "=": 左操作数必须为左值
	//10 = 1;
	//x + y = 1;
	//fmin(x, y) = 1;
	return 0;
}

注意:右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说被右值引用过的右值具备了左值的性质,即可以被取地址+被赋值,如果不想被赋值可以用const修饰右值引用,这个了解一下,实际中右值引用的使用场景并不在于此,这个特性也不重要。

int main()
{
	double&& x = 1.1;
	const double&& y = 1.2;
	x = 1.3;
	y = 1.5;//错误
	return 0;
}

那么左值引用能引用右值吗?右值引用能引用左值吗?

int main()
{
	//左值引用可以引用右值,但要加const修饰,本质是权限的放大问题
	//int& t1 = 1;//不行
	const int& t2 = 1;

	//右值引用无法引用左值
	int x = 0;
	//int&& rx = x;
	//const int&& rx = x;
	int&& rx = move(x);//但可以引用被move以后的左值
	return 0;
}

总结:

1.左值引用只能引用左值,但加上const的左值引用既能引用左值,也能引用右值

2.右值引用只能引用右值,但是右值引用可以引用被move过的左值

了解了上面的内容之后,我们来谈谈右值引用的作用和使用场景

C++中引入引用的概念本意是为了节省空间,左值引用已经满足了大部分的场景,如传参,做返回值(该对象在出了函数作用也还存在),但是如果该对象出了函数作用域后就销毁呢?如果不需要深度拷贝还好,一旦需要深度拷贝,就会浪费开辟空间需要的时间,如下面的场景---基于我在C++初级篇7string中附上的代码

我们可以很明显的感觉到在上面的过程中,空间的创建其实是不必要的,我们可以直接将str的资源直接交给s,没有必要另外创建两个对象

那么如何实现呢?

string(string&& tmp)//移动构造
    :_str(nullptr)
    , _size(0)
    , _capacity(0)
{
    swap(tmp);
}

string& operator=(string&& tmp)//移动赋值
{
    swap(tmp);
    return *this;
}

这里的右值又称为将亡值,即生命周期快要结束,那么我们就可以将这个变量的资源交给需要它的对象,如下图

注意:string&&和string&虽然都是引用,但是类型是不同的,所以虽然const string&也能引用右值,但是C++的函数调用要求使用参数最匹配的函数,所以左值和右值的调用会分别调用最匹配的

当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义, 这里的强转指的是函数返回值
template<class _Ty>//下面的函数参数和万能引用有关,后面再说,这里只要记住函数返回值被强转成了右值
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
    // forward _Arg as movable
    return ((typename remove_reference<_Ty>::type&&)_Arg);
}
//要谨慎使用move,不然可能出现下面的情况 
int main()
{
    zxws::string s1("hello world");
    // 这里s1是左值,调用的是拷贝构造
    zxws::string s2(s1);
    // 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
    // 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
    // 资源被转移给了s3,s1被置空了。
    zxws::string s3(std::move(s1));
    return 0;
}

 

 STL容器插入接口函数也增加了右值引用版本,提高了插入效率


函数模板中的万能引用和完美转发

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 Perfect(T&& x)//万能引用,既可以传左值,也可以传右值
{
    Fun(forward<T>(x));//forward<T>(x)在传参的过程中保持了x的原生类型属性,称为完美转发
    //可能有人觉得多此一举,但是上面我曾说过右值引用过的右值具有左值的属性,
    //所以如果写Fun(x)在传参时,传右值结果会是传的左值,
    //而如果写Fun(move(x)),则左值也会变成右值
    //所以这里用完美转发forward<T>(),解决所有问题
}

int main()
{
    int x = 1;
    Perfect(x);
    const int y = 0;
    Perfect(y);
    Perfect(move(x));
    Perfect(move(y));
    return 0;
}

注意:只有当T&&中的T是被推导出来的时候,T&&才是万能引用


六、新的类功能

C++11新增了两个默认成员函数---移动构造和移动赋值

  • 如果没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

 还多了几个关键字:

  • 强制生成默认函数的关键字default
  • 禁止生成默认函数的关键字delete
  • 继承和多态中的final与override
class A {
public:
    A(){}
    A(const A& a) = delete;
    ~A() = default;

    //final 和 override 在多态中讲过
};

七、可变参数模板

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

//sizeof...(args) 可以查参数个数

 举个例子

void _ShowList()
{
    cout << endl;
}

template <class T, class ...Args>
void _ShowList(T val, Args...args)
{
    cout << val << " ";
    _ShowList(args...);
}

template <class ...Args>
void ShowList(Args... args)
{
    _ShowList(args...);
}

int main()
{
    ShowList(1, 2.2, 'x', "hhhh");//打印
    return 0;
}

上面的这段代码可以打印不同类型的参数,大家可以看一下,带入递归,理解一下

解析:上面的代码可以看成是模板参数的递归,正常的递归函数都是需要有递归出口的,而上面模板函数的递归出口在于参数列表为空,下面画个图帮大家理解一下

这个还有另一种打印方式

template <class T>
int Print(T val)
{
    cout << val << " ";
    return 0;
}
template <class ...Args>
void ShowList(Args... args)
{
    int a[] = { Print(args)... };//{(Print(args), 0)...}将会展开成((Print(arg1),0),(Print(arg2),0), (Print(arg3),0), etc... )
    //利用创建数组需要知道开辟空间大小,强行让编译器执行打印函数
    cout << endl;
}

int main()
{
    ShowList(1, 2.2, 'x', "hhhh");
    return 0;
}

上面的打印代码确实很难理解,也很奇怪,无法理解的话,就暂且认为它是一种语法规定就行

实际上,可变参数列表的用处不在上面所说的打印,而是在于emplace系列接口的实现,给一个emplace_back的函数声明

template <class... Args>
void emplace_back (Args&&... args);

它既支持可变参数,也支持万能引用,那么相对正常的插入,它的优势体现在哪里?

就单纯拿push_back和emplace_back来比较,我写一个list中emplace_back的模拟实现给大家看看

如果看不太明白,可以用emplace_back("hello",1)和push_back(make_pair("hello",2))去代入 

其实push_back()也就比emplace多了一次移动拷贝,效率上差不了多少(在需要深度拷贝的时候),当不需要深度拷贝且类比较大时,emplace的效率就会比较高

(C++11语法较多,其他重要的语法会在后续章节进行讲解,敬请期待)

未完待续…………

  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值