C++11常用知识(上)【列表初始化 || 简化声明 || 范围for || 左右值 】

本文详细介绍了C++11中的列表初始化、auto类型推断、decltype类、nullptr处理、范围for以及左值和右值的概念,包括它们的用法、特点和在STL容器中的应用。同时涵盖了默认成员函数的生成与控制,以及右值引用的移动语义和使用场景。
摘要由CSDN通过智能技术生成

目录

一. 列表初始化

1)用法

2) initializer_list

小节: 

二,简化声明

1) ,auto

2) ,decltype类

3),nullptr

三,范围for

四,C++11后,STL容器变化

五,左值与右值

1. 左值

2. 右值

3,左值与右值之间的比较

4. 右值引用使用场景

(1,将死右值的移动语义操作 

(2. 编译器传值返回优化

(3,将亡值做参数

(4. 完美转发

 5,默认成员函数

(1. 默认成员函数的强制生成

(2. 默认成员函数的禁止调用生成


嗨!收到一张超美的风景图,希望你每天都能顺心!  

一. 列表初始化

首先,我们需要区分的是,什么是初始化列表与列表初始化。 前者是在类对象创建时,对成员变量进行初始化。后者是C++11优化后添加的新功能。

C++中为了满足泛型编程,基础类型如int, double等内置类型都被重写成了类。这些类被称为包装类,它们提供了一些额外的功能,比如重载运算符、提供类型转换等,以便更好地支持泛型编程。

从下面的用法,我们其实可以窥见一二:

1)用法

其实在C++98中,标准允许使用花括号  {}  对数组或者结构体元素进行 统一的列表初始值设定。比如:
struct Point
{
  int _x;
  int _y;
};
int main()
{
  int array1[] = { 1, 2, 3, 4, 5 };
  int array2[5] = { 0 };
  Point p = { 1, 2 };
  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 };
  // C++11中列表初始化也可以适用于new表达式中
  int* pa = new int[4]{ 0 };

  //自定义类型
   Point p{ 1, 2 };
   map<string, string> z1 = { {"li", "a"}, {"ze", "b"} };
   map<string, string> z2{ {"li", "a"}, {"ze", "b"} };
  return 0;
}
那这是如果实现的呢?

2) initializer_list

其实这个{ }本质上是一个特殊类,叫:std::initializer_list

介绍文档:initializer_list - C++ Reference (cplusplus.com)

那自定义类型,比如:vector如何利用initializer_list来实现列表初始化的呢?? 下面是vector构造函数,C++11的构造方法,在下面我们能看到我们要找的构造 

 

自己制作一个支持initializer_list的构造函数还是比较简单的:

但是我们也要注意:initializer_list中的变量是常量,在静态区,因此这些数据仅仅只能做数据值传递,无法修改。 

小节: 

C++11后,STL容器已经全部支持std::initializer_list列表初始化,作为构造函数的参数,使初始化容器对象就更方便了

二,简化声明

1) ,auto

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

例子 :

    map<string, string> z1 = { {"li", "a"}, {"ze", "b"} };
    
    // auto遍历
	for (auto& e : z1)  
	{
		cout << e.first << endl;
	}
    
    // 推导迭代器
	auto it = z1.begin();

2) ,decltype类

decltype的主要作用是推断表达式的类型,并且保留表达式的const和引用属性(auto 不能保留)。这使得我们可以使用decltype来声明变量、函数返回类型,以及在模板中推断函数返回类型等。

在泛型编程中,decltype可以帮助我们避免重复输入类型信息,提高代码的可读性和可维护性。它还可以用于推断lambda表达式的返回类型,以及在模板元编程中进行类型推断。

总之,decltype类的意义:提供了一种灵活的类型推断机制,使得C++编程更加方便和高效

3),nullptr

在C++11中添加了nullptr是为了解决空指针的歧义问题。在之前的C++版本中,使用NULL来表示空指针可能会引发一些问题,因为NULL可能被定义为0或者(void*)0,这样就会导致一些类型转换的问题

使用nullptr可以明确地表示空指针,避免了歧义,同时也可以提高代码的可读性和安全性。

三,范围for

范围for我们在遍历数据时,经常使用,其底层原理是迭代器。有了范围for后,使用范围for循环可以更加直观简洁地遍历容器或数组中的元素,而不需要手动管理迭代器或索引变量。这使得代码更易读、易维护,并且减少了出错的可能性。范围for循环也可以与自定义类型一起使用,只要该类型支持迭代操作。因此,范围for功能的添加使得C++代码更加现代化和易用。

四,C++11后,STL容器变化

 前面所学的STL中,现在回望这些C++11才出现的容器。

还有一些比如:右值引用等等,这个我们后面细讲。

五,左值与右值

1. 左值

左值是一个表示数据的表达式(如变量名或解引用的指针), 我们 可以获取它的地址 +可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时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& pvalue = *p;
  return 0;
}

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

2. 右值

右值也是一个表示数据的表达式,如: 字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等, 右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
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;
}

3,左值与右值之间的比较

左值 引用总结:
  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右
int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;   // ra为a的别名
    //int& ra2 = 10;   // 编译失败,因为10是右值

    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
    return 0;
}
右值引用总结:
1. 右值引用 只能右值,不能引用左值
2. 但是右值引用可以move以后的左值
int main()
{
 // 右值引用只能右值,不能引用左值。
 int&& r1 = 10;
 
 // error C2440: “初始化”: 无法从“int”转换为“int &&”
 // message : 无法将左值绑定到右值引用
 int a = 10;
 int&& r2 = a;
 
 // 右值引用可以引用move以后的左值
 int&& r3 = std::move(a);
  return 0;
}

4. 右值引用使用场景

聊使用场景前我们需要先更深入的了解右值引用: 右值是不可获取地址的,同时我们在用左值引用时,因为其不可修改性,还必须添加 const 才能接收。那我们思考下面代码

补充: 

因此,我们可以这么理解右值引用是用来修改右值的工具,那什么场景下使用该工具?  请观察下面是简易实现的string类代码:

(1,将死右值的移动语义操作 

namespace bit
{
  class string
  {
   public:
   typedef char* iterator;
 iterator begin() {return _str;}
 iterator end(){return _str + _size;}

 string(const char* str = "")
 :_size(strlen(str))
 , _capacity(_size)
 {
 //cout << "string(char* str)" << endl;
 _str = new char[_capacity + 1];
 strcpy(_str, str);
 }

 // s1.swap(s2)
 void swap(string& s){::swap(_str, s._str);
 ::swap(_size, s._size);
 ::swap(_capacity, s._capacity);}

 // 拷贝构造
 string(const string& s)
 :_str(nullptr)
 {
 cout << "string(const string& s) -- 深拷贝" << endl;
 string tmp(s._str);
 swap(tmp);
 }
 // 赋值重载
 string& operator=(const string& s)
 {
cout << "string& operator=(string s) -- 深拷贝" << endl;
 string tmp(s);
 swap(tmp);
 return *this;
 }

 // 移动构造=====================================
 string(string&& s)
 :_str(nullptr)
 ,_size(0)
 ,_capacity(0)
 {cout << "string(string&& s) -- 移动语义" << endl;
 swap(s);}

 // 移动赋值=====================================
 string& operator=(string&& s)
 {cout << "string& operator=(string&& s) -- 移动语义" << endl;
 swap(s);
 return *this;}

 ~string(){
  delete[] _str;
  _str = nullptr;}

 char& operator[](size_t pos)
 {assert(pos < _size);
   return _str[pos];}

 void reserve(size_t n){if (n > _capacity)
 {char* tmp = new char[n + 1];
  strcpy(tmp, _str);
  delete[] _str;
  _str = tmp;
  _capacity = n;}}

 void push_back(char ch){if (_size >= _capacity)
 {size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
 reserve(newcapacity);}
  _str[_size] = ch;
  ++_size;
  _str[_size] = '\0';}

 //string operator+=(char ch)
 string& operator+=(char ch){push_back(ch);
 return *this;}

 const char* c_str() const{return _str;}

 private:
 char* _str;
 size_t _size;
 size_t _capacity; // 不包含最后做标识的\0
 };
}

我们用右值引用,从引用中区分出将死右值引用,同时重载赋值函数移动语义,间接达到了减少一次拷贝的功能,解析如下: 

因此,自定义类型的左值经过move库函数操作后, 被转化为了将亡右值后,未来可能出现将亡右值数据被替换,因此move慎用!! 

(2. 编译器传值返回优化

面对局部对象的传值返回的性能消耗,编译器也做了优化,如下:

 当然这种优化跟编译器的版本,新旧有关,我们了解即可。

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。 当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中, std::move()函数位于 头文件中,该函数名字具有迷惑性,它 并不搬移任何东西,唯一的功能就是将一个 左值强制转化为右值引用 ,然后实现移动语义

(3,将亡值做参数

对于参数是 将亡右值的插入接口,C++11就可以用 移动拷贝提高效率。

对于我们使用接口来说,STL 容器插入接口函数也增加了右值引用版本:
http://www.cplusplus.com/reference/list/list/push_back/
http://www.cplusplus.com/reference/vector/vector/push_back/

(4. 完美转发

模板中的&& 万能引用  

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<typename T>
void PerfectForward(T&& t)  // 模板中的&&,叫做万能引用(引用折叠)
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。                           
{
 Fun(t);
}

int main()
{
   PerfectForward(10);           // 右值
   int a;
   PerfectForward(a);            // 左值
   PerfectForward(std::move(a)); // 右值
   const int b = 8;
   PerfectForward(b);      // const 左值
   PerfectForward(std::move(b)); // const 右值
  return 0;
}

但万能引用,却不一定“万能”,模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,不代表就能表示左右值引用类型,引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。

头文件:utility

用万能引用接受过数据,尽量每次用完美转发的形式重新获取准确数据类型再作为参数。

 5,默认成员函数

原来 C++ 类中,有 6 个默认成员函数:
1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载
最后重要的是前 4 个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
1. 没有自己实现 移动构造 函数,且析构函数 、拷贝构造、拷贝赋值重载 都未实现 。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝;自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
2. 没有自己实现 移动赋值重载 函数,且析构函数 、拷贝构造、拷贝赋值重载 都未实现 ,那么编译器会自动生成一个默认移动赋值默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝;自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。( 默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造 者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

(1. 默认成员函数的强制生成

关键字:  default

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
 class Person
 {
 public:
	 Person(const char* name = "", int age = 0)
		 :_name(name)
		 , _age(age)
	 {}

	 Person(const Person& p) // 拷贝构造,移动构造+移动赋值无法生成
		 :_name(p._name)
		 , _age(p._age)
	 {
		 cout << "拷贝构造" << endl;
	 }

	Person(Person&& p) = default; // 强制生成移动构造
	Person& operator=(Person&& p) = default; // 移动赋值基于移动构造
 private:
	 string _name;
	 int _age;
 };
 int main()
 {
	 Person s1("1111", 2);
	 Person s2 = s1;
	 Person s3 = std::move(s1);
	 return 0;
 }

(2. 默认成员函数的禁止调用生成

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称= delete修饰的函数为删除函数

结语

   本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力

  • 33
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值