C++11的新鲜事儿~

Posted WoLannnnn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++11的新鲜事儿~相关的知识,希望对你有一定的参考价值。

文章目录

C++11简介

在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于TC1主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑, 第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率

列表初始化

C++98中的初始化问题

在C++98中,标准允许使用花括号对数组元素进行统一的列表初始值设定。比如:

int array1[] = 1,2,3,4,5;
int array2[5] = 0;

对于一些自定义的类型,却无法使用这样的初始化。比如:

vector<int> v1,2,3,4,5;

就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常
不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定
义的类型,使用初始化列表时,可添加等号(=),也可不添加

内置类型的列表初始化

int main()

	// 内置类型变量
	int x1 =  10 ;
	int x2 10 ;
	int x3 = 1 + 2;
	int x4 =  1 + 2 ;
	int x5 1 + 2 ;
    
	// 数组
	int arr1[5] 1,2,3,4,5 ;
	int arr2[] 1,2,3,4,5 ;
    
	// 动态数组,在C++98中不支持
	int* arr3 = new int[5] 1,2,3,4,5 ;
    
	// 标准容器
	vector<int> v 1,2,3,4,5 ;
	map<int, int> m 1,1, 2,2,,3,3,4,4 ;
	return 0;

注意:列表初始化可以在之前使用等号,其效果与不使用=没有什么区别

自定义类型的列表初始化

  1. 标准库支持单个对象的列表初始化

    class Point
    
    public:
    	Point(int x = 0, int y = 0) : _x(x), _y(y)
    	
    
    private:
    	int _x;
    	int _y;
    ;
    
    int main()
    
    	Pointer p 1, 2 ;
    	return 0;
    
    
  2. 多个对象的列表初始化
    多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即
    可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器
    以及获取区间中元素个数的方法size()

    #include <initializer_list>
    template<class T>
    class Vector 
    public:
    	// ...
    	Vector(initializer_list<T> l) : _capacity(l.size()), _size(0)
    	
    		_array = new T[_capacity];
    		for (auto e : l)
    			_array[_size++] = e;
    	
    
    	Vector<T>& operator=(initializer_list<T> l) 
    		delete[] _array;
    		size_t i = 0;
    		for (auto e : l)
    			_array[i++] = e;
    		return *this;
    	
    
    	// ...
    
    private:
    	T* _array;
    	size_t _capacity;
    	size_t _size;
    ;
    

变量类型推导

为什么需要类型推导

在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂,比如

#include <map>
#include <string>
int main()

	short a = 32670;
	short b = 32670;
    
	// c如果给成short,会造成数据丢失,如果能够让编译器根据a+b的结果推导c的实际类型,就不会存在问题
	short c = a + b;
	std::map<std::string, std::string> m "apple", "苹果", "banana","香蕉" ;
    
    // 使用迭代器遍历容器, 迭代器类型太繁琐
    std::map<std::string, std::string>::iterator it = m.begin();
    while (it != m.end())
    
        cout << it->first << " " << it->second << endl;
        ++it;
    
    
    return 0;

C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁

decltype类型推导

为什么需要decltype

auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力

template<class T1, class T2>
T1 Add(const T1& left, const T2& right)

	return left + right;

如果能用加完之后结果的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identification 运行时类型识别)。
C++98中确实已经支持RTTI:

  • typeid只能查看类型不能用其结果类定义类型
    typeid的使用:
    typeid可以取出变量的类型,并通过name接口,以字符串形式返回类型名称。但typeid不可以用来定义变量
    下面讲的decltype就可以

  • dynamic_cast只能应用于含有虚函数的继承体系中

运行时类型识别的缺陷是降低程序运行的效率

decltype

decltype是根据表达式的实际类型推演出定义变量时所用的类型,比如:

  1. 推演表达式类型作为变量的定义类型

    int main()
    
    	int a = 10;
    	int b = 20;
    
    	// 用decltype推演a+b的实际类型,作为定义c的类型
    	decltype(a + b) c;
    	cout << typeid(c).name() << endl;
    	return 0;
    
    
  2. 推演函数返回值的类型

    void* GetMemory(size_t size)
    
    	return malloc(size);
    
    
    int main()
    
        // 如果没有带参数,推导函数的类型
        cout << typeid(decltype(GetMemory)).name() << endl;
        // 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
        cout << typeid(decltype(GetMemory(0))).name() <<endl;
        return 0;
    
    

auto和范围for

auto简化了部分代码的写法

比如下面这段代码:

int main()

   //假设我们没有展开std
    std::map<std::string, std::string> m =  "苹果", "apple", "香蕉", "banana" ;
    //遍历m
    //std::map<std::string, std::string>::iterator it = m.begin();
    auto it = m.begin();
    
    while (it != m.end())
    
        std::cout << it->first << ":" << it->second << std::endl;
        ++it;
    
    
    return 0;

很明显直接使用auto比用类型名要简单很多

注意:auto不支持作为形参和返回值类型

上面的遍历可以用返回for实现:

	for (const auto& e : m)//这里是auto和范围for一起使用,使代码简单了很多
    
        std::cout << e.first << ":" << e.second << std::endl;
    

范围for的底层是迭代器实现的,数组也可以使用范围for,指针是天然的迭代器

int main()

	int a[5] =  1, 2, 3 ,4 ,5 ;
	for (const auto& e : a)
	
		cout << e << " ";
	
	cout << endl;

	return 0;


final和override

final用来修饰类,被修饰的类就变成了最终类,不能被继承

final还可以修饰虚函数,这个虚函数就不能被重写了

override可以检查子类是否重写虚函数,没有重写就会报错

class Car
public:
	virtual void Drive()
    
;

class Benz :public Car 
public:
	virtual void Drive() override 
    
        cout << "Benz-舒适" << endl;
    
;

新容器

C++98
string/vector/list/deque/map/bitset + stack/queue/priority_queue

C++11新容器
array(定长数组):实际中用的很少,缺点:定长 + 存储数据的空间在栈上,栈的空间本来就不大

forward_list(单链表):实际中用的很少,缺点:不支持尾插尾删 + insert数据也是在当前位置的后面

unordered_map/unordered_set:推荐使用,因为他们的效率高于map/set

默认成员函数控制

在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造、移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成

显式缺省函数

在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数

class A

public:
	A(int a) : _a(a)
	
    
    //如果我们没有显式指定,是不能使用默认构造函数的,因为在上面我们自己写了构造函数
	// 显式缺省构造函数,由编译器生成
	A() = default;
    
	// 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
	A& operator=(const A& a);
    
private:
	int _a;
;

A& A::operator=(const A& a) = default;

int main()

	A a1(10);
	A a2;
	a2 = a1;
    
	return 0;

如果没有显式指定编译器生成:

注意:如果我们自己定义的构造函数是默认构造函数(无参或全缺省),则只能选择一种默认构造,要不是我们自己定义的,要不是编译器自己生成的

删除默认函数

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

class A

public:
	A(int a) : _a(a)
	
    
	// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
	A(const A&) = delete;
    
	A& operator(const A&) = delete;
    
private:
	int _a;
;

int main()

	A a1(10);
    
	// 编译失败,因为该类没有拷贝构造函数
	//A a2(a1);
    
    // 编译失败,因为该类没有赋值运算符重载
	A a3(20);
	a3 = a2;
	return 0;

注意:避免删除函数和explicit一起使

右值引用

右值引用概念

C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,使用引用,可以提高程序的可读性。

void Swap(int& left, int& right)

	int temp = left;
	left = right;
	right = temp;

int main()

	int a = 10;
	int b = 20;
	Swap(a, b);

为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用

int Add(int a, int b)

	return a + b;

int main()

	const int&& ra = 10;
    
	// 引用函数返回值,返回值是一个临时变量,为右值
	int&& rRet = Add(10, 20);
	return 0;

为了与C++98中的引用进行区分,C++11将该种方式称之为右值引用

左值与右值

左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确

int g_a = 10;
// 函数的返回值结果为引用
int& GetG_A()

	return g_a;

int main()

	int a = 10;
	int b = 20;
    
	// a和b都是左值,b既可以在=的左侧,也可在右侧,
	// 说明:左值既可放在=的左侧,也可放在=的右侧
	a = b;
	b = a;
	const int c = 30;
    
	// 编译失败,c为const常量,只读不允许被修改
	//c = a;
	// 因为可以对c取地址,因此c严格来说不算是左值
	cout << &c << endl;
    
	// 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值
	//b + 1 = 20;
	GetG_A() = 100;
	return 0;

因此关于左值与右值的区分不是很好区分,一般认为:

  1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。
  2. const修饰的常变量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是
    const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),
    C++11认为其是左值。
  3. 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
  4. 如果表达式运行结果或单个变量是一个引用则认为是左值。

总结:

  1. 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质
    判断,比如上述:c常量
  2. 能得到引用的表达式一定能够作为引用,否则就用常引用。

C++11对右值进行了严格的区分:

  • C语言中的纯右值:基本类型的常量或临时对象比如:a+b, 100
  • 将亡值:自定义类型的临时对象。比如:表达式的中间结果、函数按照值的方式进行返回。

引用与右值引用比较

在C++98中的普通引用与const引用在引用实体上的区别 :

int main()

	// 普通类型引用只能引用左值,不能引用右值
	int a = 10;
	int& ra1 = a; // ra为a的别名
    
	//int& ra2 = 10; // 编译失败,因为10是右值
    
	const int& ra3 = 10;
	const int& ra4 = a; // const引用对左值引用
	return 0;

注意: 普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。
C++11中右值引用:只能引用右值,一般情况不能直接引用左值

int main()

    // 10纯右值,本来只是一个符号,没有具体的空间,
    // 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
    int&& r1 = 10;
    r1 = 100;
    
    int a = 10;
    int&& r2 = a; // 编译失败:右值引用不能引用左值
    return 0;

问题:既然C++98中的const类型引用左值和右值都可以引用,那为什么C++11还要复杂的提出右值引用呢?

值的形式返回对象的缺陷

如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:

class String

public:
	String(const char* str = "")
	
		if (nullptr == str)
			str = "";
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	
    
	String(const String& s)
		: _str(new char[strlen(s._str) + 1])
	
		strcpy(_str, s._str);
	
    
	String& operator=(const String& s)
	
		if (this != &s)
		
			char* pTemp = new char[strlen(s._str) + 1];
			strcpy(pTemp, s._str);
			delete[] _str;
			_str = pTemp;
		
		return *this;
	

	String operator+(const String& s)
	
		char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
		strcpy(pTemp, _str);
		strcpy(pTemp + strlen(_str), s以上是关于C++11的新鲜事儿~的主要内容,如果未能解决你的问题,请参考以下文章

《从零开始学Swift》学习笔记(Day 11)——数据类型那些事儿?

C语言项目:扔香蕉的大猩猩(自制游戏),详细思路+源码分享

LQ0120 猴子分香蕉枚举

C ++我不知道如何在字符串(句子)中找到一个单词(例如香蕉,三明治)用户输入句子然后写出那个单词

C语言-字符拷贝(C primer plus 11章)

以下代码迭代并输出香蕉,如何更改它以反转输出?