C++11的更新介绍(初始化、声明、右值引用)

f62e50faa284456a8e3dd2569bed4a49.jpeg

🪐🪐🪐欢迎来到程序员餐厅💫💫💫

          主厨:邪王真眼

主厨的主页:Chef‘s blog  

所属专栏:c++大冒险

总有光环在陨落,总有新星在闪烁

C++11小故事:

       1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准C++国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫 C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也 完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。最终定名为C++11。( 包含了约140个新特性,以及对C++03标准中约600个缺陷的修正

 统一的列表初始化

{}初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
struct Point
{
 int _x;
 int _y;
};
int main()
{
 int array1[] = { 1, 2, 3, 4, 5 };
 Point p = { 1, 2 };
 return 0;
}

         C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和自定义的类型,他的本意或许是想给所有变量一个统一的初始化方案使用初始化列表时,可添加“=”,也可不添加)

  • 对内置类型:


int x2{ 2 };
int x2={2};
 int array1[]{ 1, 2, 3, 4, 5 };
 double y={1.0};
double y{1.0};
 Point p{ 1, 2 };
 // C++11中列表初始化也可以适用于new表达式中
 int* pa = new int[4]{ 0 };
  • 对于结构体:

struct Point
{
 int _x;
 int _y;
};
int main()
{
struct Point p{1,0};
struct Point{1,0};
}
  •  创建对象时使用列表初始化调用构造函数初始化

class Date
{
public:
 Date(int year, int month, int day)
 :_year(year)
 ,_month(month)
 ,_day(day)
 {
 cout << "Date(int year, int month, int day)" << endl;
 }
private:
 int _year;
 int _month;
 int _day;
};
int main()
{
 Date d1(2022, 1, 1); // old style
 // C++11支持的列表初始化,这里会调用构造函数初始化
 Date d2{ 2022, 1, 2 };
 Date d3 = { 2022, 1, 3 };
 return 0;
}

 注意事项:

  1. d1是调用了构造函数
  2. d2是c++11的新初始化并且直接调用构造函数
  3. d3:{2022,1,3}先发生隐式类型转换,通过调用构造函数生成一个类,在以拷贝构造的方式生成d3,证明方法:就是用explicit修饰构造函数(会封锁他的隐类类型转换)就会导致报错(如下图所示)158b8c5e8f8c4e0a9920e7aacc66fd41.png

 std::initializer_list

typeid是操作符,不是函数。运行时获知变量类型名称,可以使用 typeid(变量).name()

  • 1.std::initializer_list是什么类型:

int main()
{
	// the type of il is an initializer_list 
	auto il = { 10, 20, 30 };
	cout << typeid(il).name() << endl;
	return 0;
}
此时的il就是initializer
ead0b569e8434154a2a1b521f3dbdfe3.png
  • 2.std::initializer_list使用场景

之前把给vector初始化多个值时有两种方法,用 造一个数组然后用迭代器或者 一直push_back
vector<int> v;
for (int i = 0; i < 5; i++)
{
    v.push_back(arr[i]);
}
C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。

现在就可以直接用initize_list数据初始化了

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

其实这个新类型就是一个类模板, 

ed6532abdb9142d3af0501f35dfb752b.png

他的接口也是十分简单

91d80e9eac34459a9295b5d48e561cf7.png

  1. constructor:构造函数
  2. size:表示长度,
  3. begin,end:迭代器

initializer_list本质上是一个通过迭代器访问数组的容器。当其它容器(vector)通过initializer_list构造自己,其实就该容器遍历initize_list的迭代器,从而把里面的元素一个一个插入

让模拟实现的vector也支持{}初始化和赋值

template<class T>
class vector {
public:
     typedef T* iterator;
     vector(initializer_list<T> l)
     {
         _start = new T[l.size()];
         _finish = _start + l.size();
         _endofstorage = _start + l.size();
         iterator vit = _start;
         typename initializer_list<T>::iterator lit = l.begin();
         while (lit != l.end())
         {
             *vit++ = *lit++;
         }
     }
     vector<T>& operator=(initializer_list<T> l) {
         vector<T> tmp(l);
         std::swap(_start, tmp._start);
         std::swap(_finish, tmp._finish);
         std::swap(_endofstorage, tmp._endofstorage);
         return *this;
     }
private:
     iterator _start;
     iterator _finish;
     iterator _endofstorage;
 };

声明

auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局
部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将
其用于实现自动类型推断。
  • 此时的p就是int*
	int i = 10;
	auto p = &i;
	cout<<typeid(i).name()<<endl;
	cout << typeid(p).name();

738ea5e397e9453e9a7fcbf84ef54d80.png

  • auto帮我们省去了写冗长的变量类型名的麻烦
auto pf = strcpy;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();

注意事项:

  1. auto 定义变量必须在定义时初始化
  2. 函数形参以及返回值不能用auto

    decltype

decltype可以检测一个变量或表达式的类型,并且拿这个类型去声明新的类型

	int a = 1;
	double b = 1.0;
	cout<<typeid(decltype( a * b)).name();

6e1ca2647f4348d7a24866be125a249d.png

  • decltype的一些使用使用场景
	int a = 1;
	double b = 1.0;
	decltype(a * b) c = 1;
	cout<<typeid(c).name();

836dd14bfb8c4693a08293171c4ca1e1.png


nullptr

在C/C++编程习惯中,如果一个指针没有合法的指向,我们基本都是按照如下 方式初始化:

void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何

种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如

void f(int)
{
 cout<<"f(int)"<<endl;
}
void f(int*)
{
 cout<<"f(int*)"<<endl;
}
int main()
{
 f(0);
 f(NULL);
 f((int*)NULL);
 return 0;
}

f4d199be65b64b8489e3748f9126057d.png

程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的

初衷相悖。

在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器

默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void

*)0。

注意:

  • 1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptrC++11作为新关键字引入
  • 2. C++11中,sizeof(nullptr) sizeof((void*)0)所占的字节数相同。
  • 3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。 

    范围for循环

        这个我们在前面的课程中已经进行了非常详细的讲解,这里就不进行讲解了,请参考C++入门
+STL容器部分的博客。

 右值引用和移动语义

 

         传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。 无论左值引用还是右值引用,都是给对象取别名

 左值和右值

  • 左值

左值是一个表示数据的表达式(如变量名或解引用的指针), 我们可以获取它的地址+可以对它赋
值,左值可以出现赋值符号=的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左
值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用。
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;

省流:左值显著的特征是可以取地址,但是不一定可以被修改。

  • 右值

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引
用返回)等等, 右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址,给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}

 省流:右值显著的特征是不可以取地址,不可以被修改。

简单辨析了什么是左值,什么是右值,现在我们知道左值与右值的最大区别在于可不可以取地址


右值引用

对左值的引用就是左值引用

我们最开始学的引用就是左值引用,不记得的同学可以去看看我写的这篇博客


c++入门知识

  • 右值引用语法

ok,现在假定看到这的老铁都已经知晓了左值引用
右值引用的语法是:类型名&&
int x = 1, y = 1;
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
  •  左值引用可以引用右值吗

  1. 左值引用能引用左值,一般不能引用右值。
  2.  但是const左值引用既可引用左值,也可引用右值(常量具有常性,不能修改,如果我们直接把一个常量交给引用,就可能通过引用来修改这个常量,这违背了常性。因此不能直接引用一个右值常量,但是当我们使用const引用,就无法被修改,那么就可以引用了)
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;   // ra为a的别名
    //int& ra2 = 10;   // 编译失败,因为10是右值
    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
  •  右值引用可以引用左值吗

 原本右值引用是无法引用左值的,但是c++11提供了一个函数move可以把一个左值强转为右值

int a = 10;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);

注意:move不会改变参数本身的左值属性,这一点可以参考强制类型转化: 

e125d424a99c4116854ffe6db364d76e.png 

r3右值引用一个被move的左值a,修改r3也会修改左值a,因为r3即是a的别名 

int a = 10;
int&& r3 = move(a);
r3 = 1;
cout << a;

25601c83185d4fbc82f44ab24841bada.png

8ab31c2eeb6c41f5a6e1f17c318b0afb.png

  • 关于对常量的右值引用:

先说结论 

当右值引用了常量,引用会把常量区中的数据拷贝一份到栈区,然后该引用指向栈区中拷贝后的数据

int&& r3 = 10;
r3 = 1;

理由一:

如果r3拿到的就是常量10本身,那就可以对其进行修改,那以后我们在用10给别的变量赋值时,他就不再是10了,这显然是十分荒谬的,因此得证

理由二:

看地址

如果r3拿到的就是常量10本身,那他的地址因该是常量区,与10地址相同,但显然r3与r4地址相近,是在栈区。

410144d450c94d988ca98bd6e1bef98a.png


 右值引用使用场景和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引
用呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
  • 左值引用的使用场景:

做参数和做返回值都可以提高效率
void func1(string& s)
{
	;
}
void func2(string s)
{
	;
}
int main()
{
string s1("hello world");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
int a = 0, b = 0;
size_t begin = clock();
while(a++<100000000)
	func2(s1);

size_t end = clock();
cout << end - begin;
while(b++<100000000)
	func1(s1);
begin = clock();
cout << endl << begin - end;
}

 (realse版本)941d0e16be89423fa60c68f2ea009712.png

string& func1(string& s)
{
	return s;
}
string func2(string& s)
{
	return s;
}
int main()
{
string s1("hello world");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
int a = 0, b = 0;
size_t begin = clock();
while(a++<1000000)
	func2(s1);

size_t end = clock();
cout << end - begin;
while(b++<1000000)
	func1(s1);
begin = clock();
cout << endl << begin - end;
}

 

(debug版本)
d6ed96129d604e04a59a2a0c75649ac5.png
  • 左值引用的短板:

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,
只能传值返回。例如:下面的函数只能使用传值返回, 由于返回值会随着函数结束而被销毁,所以要先拷贝构造一个临时变量,接着再由临时变量去个ret2进行拷贝构造。结果就是旧一点的编译器是两次拷贝构造,新的编译器自带优化是1次拷贝构造。(都是深拷贝)
string func()
{
string s("aaa");
	return s;
}
int main()
{
	string s =func();
}

因此,右值引用闪亮登场。 

 我们之前说了被move的左值可以被右值引用,除此之外:

C++会把即将离开作用域的非引用类型的返回值当成右值,这种类型的右值也称将亡值 

产生思想:假设b是将亡值,用它去构造a,那我干嘛还要进行深拷贝,我直接把b的数据转移到a身上不就行了(因为b马上自己也要释放空间,那这些数据也会消失,不如直接给a)

  • 移动构造、移动赋值:

在string中增加移动构造, 移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不

用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

还有移动赋值原理相同

class _string
{
public:
	_string(const char* str = "")//"\0"=="",因为结尾没有'\0'会补
		: _size(strlen(str))
		, _capacity(_size == 0 ? 3 : _size)
	{
		_str = new char[_capacity + 1];//_capacity不包括最后的'\0'
		strcpy(_str, str);
	}
	_string(_string&& s)
		:_str(nullptr)
		, _size(s._size)
		, _capacity(s._capacity)
	{
		cout << "移动构造" << endl;
		std::swap(_str, s._str);
	}
	_string& operator=(_string&&s)
	{
		cout << "移动赋值" << endl;
		std::swap(_str, s._str);
	}
	char* _str;
	size_t _size;
	size_t _capacity;
};
_string get_string()
{
	_string str("");

	return str;
}
int main()
{
	_string s2 = get_string();

	return 0;
}

 相较于之前的两次或一次拷贝构造,因为get_string的返回值是右值,所以直接调用一次或两次移动构造,而移动构造也不是深拷贝,而是直接用swap交换来进行拷贝,效率自然提高


移动构造之所以这么叫,就是因为移走了别人的资源。这部分资源之所以会被移走,就是因为它有右值属性。而它之所以有右值属性,要么就是这个变量是个将亡值,资源不转移就浪费了;要么就是被程序员亲自move了,程序员许可把这个对象的资源转移走,这就保证了资源移动的安全性。

移动语义(Move Semantics)是 C++11 引入的一项重要特性,它允许对象的资源(如堆上分配的内存)在不进行深度复制的情况下进行转移。通过移动语义,可以将对象的资源从一个对象转移到另一个对象,从而避免不必要的内存拷贝,提高程序性能和效率。


万能引用

  • 引用折叠

在下面的函数模板中,T&&并不是表示右值引用,他会识别传参类型,传参为左值就是左值引用,传参是右值就是右值引用

template<class T>
void  PerfectForward(T&& t)
{
	cout << "右值引用" << endl;
}

证明如下 

template<class T>
void  PerfectForward(T&& t)
{
	cout << "右值引用" << endl;
}
template<class T>
void PerfectForward(const T& t)
{
	cout << "左值引用" << endl;
}
int main()
{
	int a = 10;
	PerfectForward(move(a));
	PerfectForward(a);
	return 0;
}

4874cf214248476b81d85364e4da8760.png

发现第二个函数也是调用的第一个函数模板,则说明第一个函数模板所生成的函数比第二个更适合所传参数,我们知道第二个生成了const int&,最适合的是int&,得证。

可以看作是&&折叠成了一个&,所以万能引用又称为引用折叠
 

请看以下的代码

void  func(int&& t)
{
	cout << "右值引用" << endl;
}
void  func(const int&& t)
{
	cout << "const右值引用" << endl;
}
void func(const int&t)
{
	cout << "const左值引用" << endl;
}
void  func(int& t)
{
	cout << "左值引用" << endl;
}
template<class T>
void  PerfectForward(T&& t)
{
	func(t);
}
int main()
{
	const int a = 10;
	PerfectForward(move(a));
	PerfectForward(a);
	int b = 10;
	PerfectForward(move(b));
	PerfectForward(b);
	PerfectForward(10);
	return 0;
}

543669f182f241f1b1df882a75845469.png

所有都是左值,why???
解答:

a右值引用后b,右值引用指向的对象b是右值属性,但是引用本身a是左值属性(对象b有无const属性会被a继承)

有的老铁要说了在把func变成func(move(T))

万万不可啊,假如我传了一个右值那还好,你这样保证了它的属性不改变,可假如人家本来就是左值呢?那你就把它改成了右值了,这不就出问题了

为什么c++11要这么设计呢?

我们右值引用是为了移动语义,说白了就是转移资源,假如引用后依旧是右值,因为右值是不能被修改的,那就实现不了转移资源,但这样也有bug,即是我们可能会多个函数嵌套,最里面才是转移资源,但它在第一层就把值从左值变成了右值,后面面对函数重载就可能调用错,那怎莫让它类型不改变呢

  • 完美转发闪亮登场:

C++提供了一个函数模板forward,称为完美转发,其可以自动识别到参数的左右值类型,从而将其转化为原来的值的类型。

void  func(int&& t)
{
	cout << "右值引用" << endl;
}
void  func(const int&& t)
{
	cout << "const右值引用" << endl;
}
void func(const int&t)
{
	cout << "const左值引用" << endl;
}
void  func(int& t)
{
	cout << "左值引用" << endl;
}
template<class T>
void  PerfectForward(T&& t)
{
	func(forward<T>(t));
}
int main()
{
	const int a = 10;
	PerfectForward(move(a));
	PerfectForward(a);
	int b = 10;
	PerfectForward(move(b));
	PerfectForward(b);
	PerfectForward(10);
	return 0;
}

943875947f5340a9a73f8cb4393e26fb.png 

完美转发实际中的使用场景:

template<class T>
struct ListNode
{
 ListNode* _next = nullptr;
 ListNode* _prev = nullptr;
 T _data;
};
template<class T>
class List
{
 typedef ListNode<T> Node;
public:
 List()
 {
 _head = new Node;
 _head->_next = _head;
 _head->_prev = _head;
 }
 void PushBack(T&& x)
 {
 //Insert(_head, x);
 Insert(_head, std::forward<T>(x));
 }
 void PushFront(T&& x)
 {
 //Insert(_head->_next, x);
 Insert(_head->_next, std::forward<T>(x));
 }
 void Insert(Node* pos, T&& x)
 {
 Node* prev = pos->_prev;
 Node* newnode = new Node;
 newnode->_data = std::forward<T>(x); // 关键位置
 // prev newnode pos
 prev->_next = newnode;
 newnode->_prev = prev;
 newnode->_next = pos;
 pos->_prev = newnode;
 }
 void Insert(Node* pos, const T& x)
 {
 Node* prev = pos->_prev;
 Node* newnode = new Node;
 newnode->_data = x; // 关键位置
 // prev newnode pos
 prev->_next = newnode;
newnode->_prev = prev;
 newnode->_next = pos;
 pos->_prev = newnode;
 }
private:
 Node* _head;
};
int main()
{
 List<bit::string> lt;
 lt.PushBack("1111");
 lt.PushFront("2222");
 return 0;
}

 总结:

我们今天学习了初始化、声明以及右值引用三个点,大概是学完了一半,剩下的就下次再讲啦。

🥰创作不易,你的支持对我最大的鼓励🥰

🪐~ 点赞收藏+关注 ~🪐

e3ff0dedf2ee4b4c89ba24e961db3cf4.gif

 

  • 63
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 35
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值