【C++】C++11【上】列表初始化|声明|新容器|右值引用|完美转发|新的类功能

1、 C++11简介

C++0x C++11 C++ 标准 10 年磨一剑,第二个真正意义上的标准珊珊来迟。相比于c ++98/03 C++11 则带来了数量可观的变化,其中包含了约 140 个新特性,以及对 C++03 标准中 600 个缺陷的修正,这使得 C++11 更像是从 C++98/03 中孕育出的一种新语言。相比较而言,C++11 能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更 强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以要作为一个 重点去学习 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

2、 统一的列表初始化

2.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 扩大了用大括号括起的列表 ( 初始化列表 ) 的使用范围,使其可用于所有的内置类型和用户自
定义的类型, 使用初始化列表时,可添加等号 (=) ,也可不添加
class Point
{
public:
	Point(int x = 0, int y = 0) : _x(x), _y(y)
	{}

private:
	int _x;
	int _y;
};

int main()
{
	int x = 1;
	int y{ 2 };//c++11,没必要学,没意义

	//C++11才支持的花括号列表初始化(容器都可以用{}初始化,数组也适用)

    int array1[]{ 1, 2, 3, 4, 5 };
    int array2[5]{ 0 };
	
    vector<int> v1{ 1,2,3,4,5 };
	vector<int> v2 = { 1,2,3,4,5 };

	list<int> l1{ 1,2,3,4,5 };
	list<int> l2 = { 1,2,3,4,5 };

	map<string, int> m{ {"苹果", 1},{"西瓜",3},{"香蕉",2}};//map的每个对象都是pair
	map<string, int> m1 = { {"苹果", 1},{"西瓜",3},{"香蕉",2} };

	//对于类的构造函数的初始化
	Point p1(1, 2); //正常情况下支持的
	Point p2{ 1, 2 };
	Point p3 = { 1, 2 };

	//c++11中的列表初始化也可用于new表达式
	int* pa = new int[4] {0};
    
    return 0;
}

2.2 std::initializer_list

①、类型(其为一个类模板) 

template<class T> class initializer_list;

②、使用场景

std::initializer_list可以认为是一个容器,其 一般作为构造函数的参数,C++11 STL中的不少容器就增加std::initializer_list 作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。
//initializer_list的使用(两种写法)
auto i1 = { 10, 20, 30 }; //the type of i1 is an initializer_list
initializer_list<int> i2 = { 1, 2, 3 };

//容器是如何支持这种花括号的列表初始化的呢?
//使模拟实现的vector也支持{}初始化和赋值
/*vector(initializer_list<T>l)
	:_capacity(l.size()), _size(0)
{	//为了讲解,这里假设vector没用指针实现,而是用_capacity,_size实现
	_array = new T[_capacity];
	for (auto e : l)
		_array[_size++] = e;
}*/
//其他容器也类似

注:容器支持花括号列表初始化,本质是增加了一个initializer_list的构造函数,initializer_list可以接收{}列表  


 3、声明

 c++11提供了多种简化声明的方式,尤其是在使用模板时。

 3.1 auto和范围for

C++98 auto 是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto 就没什么价值了。 C++11 中废弃 auto 原来的用法,将其用于 实现自动类型推断。这样要求必须进行显式初始化,让编译器将定义对象的类型设置为初
始化值的类型。
//auto和范围for(熟悉)->简化了代码的写法

//auto不能做形参和返回值
//auto func(auto e)
//{}
int main()
{
	std::map<std::string, std::string> dict = { {"leverage","影响力"}, {"acre", "英亩"} };
	std::map<std::string, std::string>::iterator it1 = dict.begin();
	auto it2 = dict.begin();//用auto明显更方便书写了

	//注:这里当容器存的对象比较大时或这个对象要做深拷贝,如string
	//最好给引用和const,可以减少拷贝,提高效率
	for (const auto& e : dict) 
	{//容器支持范围for原理:范围for会被编译器替换成迭代器,则支持迭代器就支持范围for
		cout << e.first << e.second << endl;//acre英亩\nleverage影响力
    }

	//注:auto生成的迭代器是可以当参数进行传参的
	//因为auto生成对象跟只用类型对象是一样的,即it1与it2是一样的,没有区别   
	//唯一区别:it2类型是编译器自动推导出来的
	//auto的优势就是可以把类型比较复杂的地方,简化代码的写法

	//除了STL的容器可以用范围for用法,数组也可以(原生指针也可以认为是天然迭代器,如vector
	//string等迭代器就是原生指针
	int a[] = { 1,2,3,4,5,6 };
	for (auto e : a)
	{
		cout << e << " ";
	}
	cout << endl;
}

3.1decltype

关键字 decltype 将变量的类型声明为表达式指定的类型
//类型推导,属于RTTI (run time type identification)【了解】
//程序运行时对象的类型识别
int main()
{
	int a = 10;
	int b = 20;
	double c = 10;

	auto d = a + b;
	auto e = a + c;

	//拿到类型名称的字符串
	cout << typeid(d).name() << endl; //int
	cout << typeid(e).name() << endl; //double
	string s;
	cout << typeid(s).name() << endl; //class std::basic_string<char,....>

	//若想定义一个跟d一样类型的对象
	//typeid(d).name() f;   //报错,故用decltype
	
	//通过对象去推类型
	decltype(e) g;
	decltype(e) h;

	cout << typeid(g).name() << endl; //double
	cout << typeid(h).name() << endl; //double

	return 0;
}

3.3 nullptr


 4、新容器

容器中的一些新方法
如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。 比如提供了 cbegin cend 方法返回 const 迭代器等等,但是实际意义不大,因为 begin end也是可以返回 const 迭代器的,这些都是属于锦上添花的操作。
实际上 C++11 更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本。
但是这些接口到底意义在哪?网上都说他们能提高效率,他们是如何提高效率的?
请看下面的右值引用和移动语义的讲解。另外 emplace还涉及模板的可变参数,也需要再继续深入学习后面的知识。

5、 右值引用

5.1左值引用和右值引用

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

什么是左值?什么是左值引用?
左值是一个表示数据的表达式 ( 如变量名或解引用的指针 ) 我们可以获取它的地址 + 可以对它赋
值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边 。定义时 const 修饰符后的左
值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
// 以下的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;

什么是右值?什么是右值引用?

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

// 以下几个都是常见的右值
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;
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址,也就是说例如:不能取字面量 10 的地址,但是 rr1 引用后,可以对 rr1 取地
址,也可以修改 rr1 。如果不想 rr1 被修改,可以用 const int&& rr1 去引用,是不是感觉很神奇,
这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x+y;
rr1 = 20;
rr2 = 5.5;  // 报错

5.2 左值引用与右值引用比较 

 左值引用总结:

1. 左值引用只能引用左值,不能引用右值。
2. 但是 const 左值引用既可引用左值,也可引用右值。(这也说明了我们建议加const的原因之一)
//左值引用不能直接引用右值,const左值引用既可以引用左值,也可引用右值
//int& e = 10;
//int& f = x + y;
const int& e = 10;
const int& f = x + y;
右值引用总结:
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以 move 以后的左值。
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能 真的需要用右值去引用左值实现移动语义。 当需要用右值引用引用一个左值时,可以通过 move 函数将左值转化为右值 C++11 中, std::move() 函数位于 头文件中,该函数名字具有迷惑性,它 并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
//右值引用只能引用右值,不能引用左值,但可以引用move后的左值
//error C2440: “初始化”: 无法从“int”转换为“int &&”
//message : 无法将左值绑定到右值引用
//int&& mm = a;
int&& mm = move(a);

5.3 左值和右值引用使用场景及意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值(加const才行),那为什么 C++11还要提出右值引用呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的。
左值引用的使用场景:
做参数和做返回值都可以提高效率。
void func1(string s)
{}
void func2(const string& s)
{}
int main()
{
 string s1("hello world");
 // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
 func1(s1);
 func2(s1);
 // string operator+=(char ch) 传值返回存在深拷贝
 // string& operator+=(char ch) 传左值引用没有拷贝提高了效率
 s1 += '!';
 return 0;
}
左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如: string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少 1 次拷贝构造 ( 如果是一些旧一点的编译器可能是两次拷贝构造 )
这里用string中的to_string来说明这个问题:
namespace mz
{
	string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;
			str += ('0' + x);
		}
		if (flag == false)
		{
			str += '-';
		}
		std::reverse(str.begin(), str.end());
		return str;
	}
}
int main()
{
	// 在mz::string to_string(int value)函数中可以看到,这里只能使用传值返回,
	// 传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
	string ret1 = mz::to_string(1234);
	string ret2 = mz::to_string(-1234);
	cout << ret1 << endl; //1234
	cout << ret2 << endl; //-1234

	return 0;
}


右值引用的使用场景:

场景1(简单应用)

template<class T>
void f(T& a)
{
	cout << "void f(const T& a)" << endl;
}

template<class T>
void f(T&& a)
{
	cout << "void f(const T&& a)" << endl;
}

int main()
{
	int x = 10;
	//f的左值右值引用的参数不同,故构成函数重载
	f(x);  //void f(const T& a)  ->匹配左值引用
	f(10); //void f(const T&& a) ->匹配右值引用

	return 0;
}

场景2

C++11又将右值区分为:纯右值和将亡值
纯右值:基本类型的常量或者临时对象
将亡值:自定义类型的临时对象

以我们模拟实现的String为例,看看右值引用的应用场景 

class String
{
public:
	String(const char* str = " ")
	{
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}

	//s2(s1)
	String(const String& s)
	{
		cout <<"String(const String& s)-拷贝构造-效率低" << endl;
		_str = new char[strlen(s._str) + 1];
		strcpy(_str, s._str);
	}

	//s3(右值-将亡值)
	String(String&& s)
		:_str(nullptr)
	{
		//传进来的是个将亡值,反正你都要亡了,我的目的是跟你有一样大的空间
		//不如把你的空间给我
		cout << "String(String&& s)-移动拷贝构造-效率高" << endl;
		swap(_str, s._str);
	}

	//s3 = s4
	String& operator=(const String& s)
	{
		cout << "String& operator=(const String& s)-拷贝赋值-效率低" << endl;

		if (this != &s)
		{
			char* newstr = new char[strlen(s._str) + 1];
			strcpy(newstr, s._str);

			delete[] _str;
			_str = nullptr;
		}

		return *this;
	}

	//s3 = 右值-将亡值
	String& operator=(String&& s)
	{
		cout << "String& operator=(const String&& s)-移动赋值-效率高" << endl;
		swap(_str, s._str); //直接掠夺s的资源即可,反正你s都要亡了

		return *this;
	}
	
	//第二个层次的问题
	//s1 + s2
	String operator+(const String& s2)
	{
		String ret(*this);
		//ret.append(s2._str); //因append我们没有模拟实现,故这里注释掉

		return ret;	//返回的是右值
	}

	//s1 += s2
	String& operator+=(const String& s2)
	{
		//this->append(s2);

		return *this; //返回的是左值
	}

	~String()
	{
		delete[]_str;
	}
private:
	char* _str;
};

String f(const char* str)
{
	String tmp(str);
	return tmp; // 返回tmp拷贝的临时对象
}


int main()
{
	String s1("左值");
	String s2(s1);				 //参数是左值
	//String s3(String("右值"));  //编译器会优化掉
	String s3(f("右值-将亡值"));	  //参数是右值-将亡值(传给你用,用完我就析构了),但编译器也会优化掉
	String s4(move(s1));			

	String s5("zuo值");
	s5 = s1;

	String s6("s6");
	String s7("s7");

	String s8 = s6 += s7; //拷贝构造
	String s9 = s6 + s7;  //移动构造

	return 0;

①、拷贝构造和移动拷贝构造的场景 (可以减少拷贝)

移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不 用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

 注:正常的左值引用时拷贝构造,而右值引用是移动拷贝构造

 ②、拷贝赋值和移动赋值的场景与①类似(也可以减少拷贝)

③、左值和右值做返回值的场景

 operator+与operator+=的返回值

注:operator+返回的是一个右值,因为既有拷贝构造又有移动构造,编译器会选择最匹配的参数调用,用这个右值构造s9,就会匹配调用移动构造。这里其实是个移动语义。

 总结:

以前我们写的函数总是避免用传值做返回值,因为会有深拷贝,但这里有右值引用后,会有移动构造和移动赋值减少拷贝,即不会深拷贝了,那你想传值返回就传值返回(很便利)

 ④、所有深拷贝类(vector/list/map/set..),都可以加两个右值引用做参数的移动拷贝和移动赋值 

  

总结:

⑤、右值引用做函数参数,减少拷贝 

 

 ⑥、左值引用和右值引用减少拷贝的对比


 6、完美转发

模板中的&& 万能引用

解决:利用完美转发,std::forward 完美转发在传参的过程中保留对象原生类型属性

完美转发实际中的使用场景:
list的模拟实现中
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));//复用Insert中可能丢失右值属性,故完美转发
 }
 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;
}

7、新的类功能

7.1默认成员函数

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

7.2强制生成默认函数的关键字default:

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原 因这个函数没有默认生成。
①、有拷贝构造导致的编译器无法生成默认构造函数
class A
{
public:

    A(const int& a)
		:_a(a)
	{}

	//因为你写了拷贝构造,编译器就不会生成默认构造了
	//法一、自己写一个默认构造
	//法二、default:指定编译器显式的生成(C++11做法)
	A() = default; 

private:
	int _a = 10;
};

int main()
{
	A aa1;     //若不用default,则失败
	A aa2(aa); //成功

	return 0;
}

②、有拷贝构造导致的编译器无法生成移动构造

我们可以 使用 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)
	{}

	Person(Person&& p) = default;
private:
	string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}

7.2禁止生成默认函数的关键字delete:

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

	//若要求A的对象不能拷贝和赋值(防拷贝)

	//C++98的做法:只给声明,不给实现,这样别人就无法拷贝对象
	//缺陷:导致链接不上,且别人可以再类外定义
//	A(const int& a); 
//	A& operator=(const A& aa);

	//为解决上面缺陷,private限定,类外也无法定义
//private:
//	A(const int& a);
//	A& operator=(const A& aa);

	//C++11的做法:用delete定义为删除函数
	A(const int& a) = delete;
	A& operator=(const A& aa) = delete;

	A(const int& a)
		:_a(a)
	{}

private:
	int _a = 10;
};

int main()
{
	A aa1;
	A aa2(aa1);
	aa1 = aa2;

	return 0;
}

C++【下】链接:【C++】C++11【下】lambda表达式|thread线程库-CSDN博客

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值