C++11 新增语法剖析

目录

    • 传统艺能
    • 十年磨(不出)一剑
    • 初始化列表统一化
    • initializer_list
      • 应用场景
    • 声明简化
    • 移动构造/移动赋值函数
      • 意义
    • 初始化赋值支持
    • delete
    • final & override
      • final修饰类
      • final修饰虚函数
      • override修饰虚函数

传统艺能

小编是双非本科大一菜鸟不赘述,欢迎米娜桑来指点江山哦(QQ:1319365055)

非科班转码社区诚邀您入驻
小伙伴们,打码路上一路向北,彼岸之前皆是疾苦
一个人的单打独斗不如一群人的砥砺前行
这是我和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
直达: 社区链接点我
___C++11 新增语法剖析_第1张图片

十年磨(不出)一剑

众所周知 C++ 是一个历来小心谨慎,不敢尝试冒进的 “刻板” 语言,C++ 标准委员会按常理都会一段时间对 C++ 进行更新修正。广为人知的是 C++ 上一个版本 C++98 ,本来说十年磨一剑,在 18 年会搞出 C++18 ,但是很明显标准委员会摸鱼的摸鱼,犹豫的犹豫,计划一拖再拖最后问世于 2011 年,索性得名——C++11。

相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。

相比较而言,C++11能更好地用于系统开发和库开发、语言更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多。但是由于 C++ 的保守不敢向 python 一样激进,到现在都没有一个网络库,这也是 C++ 一直以来的一大遗憾。

接下来就来看看 C++11 有哪些亮眼的语法吧

初始化列表统一化

C++98 允许使用大括号 {} 对数组或者结构体元素进行统一的列表初始值设定,举个栗子:

struct Num
{
	int _x;
	int _y;
};
int main()
{
	//数组元素初始化
	int a[] = { 1, 2, 3};
	int b[3] = { 1 };

	//结构体元素初始化
	Num n = { 1, 2 };
	return 0;
}

众所周知,{} 的内容就是初始化列表,C++11 拓宽了 {} 业务范围,所有的内置类型和自义定类型都可以使用初始化列表初始化,在 {} 之后可以使用 = ,也可以不使用,但是在进行 new 表达式初始化时一定不能加 = (C++11新增语法)

struct Num
{
	int _x;
	int _y;
};
int main()
{
	//{}对内置类型初始化
	int x1 = { 1 }; //等号
	int x2{ 2 };    //不加等号

	//{}对数组元素进行初始化
	int a[]{1, 2, 3}; //等号
	int b[3]{1};      //不加等号

	//{}对结构体元素进行初始化
	Num p{ 1, 2 }; //不加等号

	//C++11中列表初始化也可用于new表达式中(C++98无法初始化)
	int* p1 = new int[3]{0};       
	int* p2 = new int[3]{1,2,3}; 
	return 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, 10, 07);

	//C++11支持列表初始化这里调用构造函数初始化
	Date d2 = { 2022, 10, 08 }; //等号
	Date d3{ 2022, 10, 09 };    //不加等号
	return 0;
}

initializer_list

initializer_list 是一个容器,是 C++11 新增的,但是他不像其他容器一样提供了花里胡哨的成员函数

提供了 begin 和 end 函数,用于迭代器遍历;以及获取容器中的元素个数的 size 函数。

C++11 新增语法剖析_第2张图片
下面两个是非成员的重载,只针对了 begin 和 end。

initializer_list 本质就是一个大括号括起来的列表,如果用auto关键字定义一个变量来接收一个大括号括起来的列表,然后以 t y p e i d ( 变量名 ) . n a m e ( ) \color{red} {typeid(变量名).name()} typeid(变量名).name() 的方式查看该变量的类型,此时会发现该变量的类型就是 initializer_list

int main()
{
	auto num = { 1, 2, 3 };
	cout << typeid(num).name() << endl; //class std::initializer_list
	return 0;
}

应用场景

思考一下为什么 initializer_list 没有提供相应的增删查改的接口函数,其实内涵就是告诉你咱们 initializer_list 的目的不是去存储数据的,而是为了让其他容器支持列表初始化:

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()
{
	//用大括号括列表对容器初始化
	vector<int> v = { 1, 2, 3, 4, 5 };
	list<int> l = { 10, 20, 30, 40, 50 };
	vector<Date> vd = { Date(2022, 8, 29), Date{ 2022, 8, 30 }, { 2022, 8, 31 } };
	map<string, string> m{ make_pair("sort", "排序"), { "insert", "插入" } };

	//用大括号列表对容器赋值
	v = { 5, 4, 3, 2, 1 };
	return 0;
}

C++98并不支持直接用列表对容器进行初始化,这种初始化方式是在C++11引入initializer_list后才支持的,而这些容器之所以支持使用列表进行初始化:
根本原因是因为 C + + 11 给这些容器增加了一个构造函数 \color{red} {根本原因是因为 C++11 给这些容器增加了一个构造函数} 根本原因是因为C++11给这些容器增加了一个构造函数
,这个构造函数是以 i n i t i a l i z e r l i s t 作为参数的 \color{red} {,这个构造函数是以 initializer_list 作为参数的} ,这个构造函数是以initializerlist作为参数的

C++11 新增语法剖析_第3张图片

当用列表对容器进行初始化时,这个列表被识别成 initializer_list 类型,于是就会调用这个新增的构造函数对该容器进行初始化。这个新增的构造函数要做的就是遍历 initializer_list 中的元素,然后将这些元素依次插入到要初始化的容器当中即可。

namespace cl
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		vector(initializer_list<T> il)
		{
			_start = new T[il.size()];
			_finish = _start;
			_endofstorage = _start + il.size();
			//迭代器遍历
			//typename initializer_list::iterator it = il.begin();
			//while (it != il.end())
			//{
			//	push_back(*it);
			//	it++;
			//}
			//范围for遍历
			for (auto e : il)
			{
				push_back(e);
			}
		}
		vector<T>& operator=(initializer_list<T> il)
		{
			vector<T> tmp(il);
			std::swap(_start, tmp._start);
			std::swap(_finish, tmp._finish);
			std::swap(_endofstorage, tmp._endofstorage);
			return *this;
		}
	private:
		iterator _start;
		iterator _finish;
		iterator _endofstorage;
	};
}

使用迭代器方式遍历时,需要在迭代器类型前面加上typename关键字,指明这是一个类型名字。因为这个迭代器类型定义在一个类模板中,在该类模板未被实例化之前编译器是无法识别这个类型的。最好也增加一个以 initializer_list 作为参数的赋值运算符重载函数以支持直接用列表对容器对象进行赋值,但实际也可以不增加,下面代码依然可以正常执行:

vector<int> v = { 1, 2, 3 };
v = { 3, 2, 1 };

对于第一行代码,就是调用以initializer_list作为参数的构造函数完成对象的初始化。
而对于第二行代码,会先调用initializer_list作为参数的构造函数构造出一个vector对象,然后再调用vector原有的赋值运算符重载函数完成两个vector对象之间的赋值。

声明简化

C++11 提供了许多简化声明的方法,比较突出的就是 a u t o \color{red} {auto} auto

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

int main()
{
	int i = 10;
	auto p = &i;
	auto pf = strcpy;

	cout << typeid(p).name() << endl;  //int *
	cout << typeid(pf).name() << endl; //char * (__cdecl*)(char *,char const *)

	map<string, string> dict = { { "sort", "排序" }, { "insert", "插入" } };
	//map::iterator it = dict.begin();
	auto it = dict.begin();  //简化代码

	return 0;
}

自动类型推断在某些场景下是非常必要的,因为编译器要求定义变量必须先给出变量实际类型,而如果自己设定的类型可能会出问题。比如:

int main()
{
	short a = 32670;
	short b = 32670;
	//c如果给成short,会造成数据丢失,如果能够让编译器根据a+b的结果推导c的实际类型,就不会存在问题
	short c = a + b;
	return 0;
}

还有一个与 auto 相似的关键字,名为 decltype ,他可以将变量的类型声明为表达式指定的类型, 那为什么不用 typeid(变量名).name() 来获取类型呢?通过 t y p e i d ( 变量名 ) . n a m e ( ) \color{red} {typeid(变量名).name()} typeid(变量名).name() 的方式确实可以获取一个变量的类型,但无法用获取到的这个类型去定义变量。

decltype 除了能推演表达式类型,还能推演函数返回值类型,指定函数返回值类型。比如:

//decltype 推演表达式类型

template<class T1, class T2>
void F(T1 t1, T2 t2)
{
	decltype(t1*t2) ret;
	cout << typeid(ret).name() << endl;
}
int main()
{
	const int x = 1;
	double y = 2.2;

	decltype(x*y) ret;
	decltype(&x) p;
	cout << typeid(ret).name() << endl; //double
	cout << typeid(p).name() << endl;   //int const *

	F(1, 'a'); //int
	F(1, 2.2); //double

	return 0;
}

//decltype 推演函数返回值类型

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;
}

decltype 指定函数返回值类型

template<class T1, class T2>
auto Add(T1 t1, T2 t2)->decltype(t1+t2)
{
	decltype(t1+t2) ret;
	ret = t1 + t2;
	cout << typeid(ret).name() << endl;
	return ret;
}
int main()
{
	cout << Add(1, 2) << endl;;   //int
	cout << Add(1, 2.2) << endl;; //double
	return 0;
}

移动构造/移动赋值函数

C++98 里,一共有 6 个默认成员函数:构造函数,析构函数,拷贝构造函数,拷贝赋值函数,取地址重载函数,const取地址重载函数(前四个成员函数最重要,后面两个成员函数一般不会用到)

到了 C++11 ,新增了 2 个默认成员函数: 移动构造函数和移动赋值重载函数 \color{red} {移动构造函数和移动赋值重载函数} 移动构造函数和移动赋值重载函数

移动构造函数的生成条件:没有自己实现移动构造函数,并且没有自己实现析构函数、拷贝构造函数和拷贝赋值函数。移动赋值重载函数同理。但是麻烦的事情就是他和 C++98 的不同,并不是单纯的没有实现移动构造和移动赋值编译器就会默认生成,所以导致如果我们实现了移动构造/移动赋值,就算没有实现拷贝构造和拷贝赋值,编译器也不会生成默认的拷贝构造和拷贝赋值

意义

他们对于内置类型会完成值拷贝(浅拷贝),对于自定义类型成员,如果成员实现了移动构造就调用它的移动构造,否则调用它的拷贝构造。我们可以用一个简化的 string 类来验证一下:

namespace exam
{
	class string
	{
	public:
		//构造函数
		string(const char* str = "")
		{
			_size = strlen(str); //初始时,字符串大小设置为字符串长度
			_capacity = _size; //初始时,字符串容量设置为字符串长度
			_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
			strcpy(_str, str); //将C字符串拷贝到已开好的空间
		}
		//交换两个对象的数据
		void swap(string& s)
		{
			//标准库swap
			std::swap(_str, s._str); 
			std::swap(_size, s._size); 
			std::swap(_capacity, s._capacity); 
		}
		//拷贝构造函数(现代写法)
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp(s._str); //构造函数构造出一个C字符串对象
			swap(tmp); //交换这两个对象
		}
		//移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}
		//拷贝赋值函数(现代写法)
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 深拷贝" << endl;

			string tmp(s); //s拷贝构造出对象tmp
			swap(tmp); //交换这两个对象
			return *this; 
		}
		//移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}
		//析构函数
		~string()
		{
			delete[] _str;  
			_str = nullptr; //置空,防止非法访问
			_size = 0;      
			_capacity = 0;  
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

还需要一个 person 类,成员 name 就是我们实现的 string

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& operator=(const Person& p)
	{
		if (this != &p)
		{
			_name = p._name;
			_age = p._age;
		}
		return *this;
	}
	//析构函数
	~Person()
	{}
private:
	exam::string _name;
	int _age;        
};

int main()
{
	Person s1("嘉然", 19);
	Person s2 = std::move(s1); //想调用默认生成的移动构造,move赋予了右值属性
	return 0;
}

虽然 Person 类当中没有实现移动构造和移动赋值,但拷贝构造、拷贝赋值和析构函数都实现了,因此不会生成默认的移动构造和移动赋值

代码中用右值去构造 s2 对象,但由于 Person 类没有生成默认移动构造函数,因此这里会调用 Person 拷贝构造函数(拷贝构造既能接收左值也能接收右值),这时在Person的拷贝构造函数中就会调用string的拷贝构造函数对name成员进行深拷贝。

如果要让 Person 类生成默认的移动构造函数,就必须将 Person 类中的拷贝构造、拷贝赋值和析构函数全部注释掉,这时用右值去构造 s2 对象时就会调用 Person 默认生成的移动构造函数。

Person 默认生成的移动构造,对于内置类型成员 age 会进行值拷贝,而对于自定义类型成员 name 会深拷贝,因为我们的 string 类实现了移动构造函数,因此它会调用 string 的移动构造函数进行资源转移。

如果我们将 string 类当中的移动构造函数注释掉,那么 Person 默认生成的移动构造函数,就会调用 string 类中的拷贝构造函数对 name 成员进行深拷贝

初始化赋值支持

默认生成的构造函数,对于自定义类型成员会调用其构造函数进行初始化,但不会对内置类型成员进行处理。于是 C++11 支持非静态成员变量在声明时进行初始化赋值,默认生成的构造函数会使用这些缺省值对成员进行初始化, 注意这里不是直接初始化,只是给一个缺省值! \color{red} {注意这里不是直接初始化,只是给一个缺省值!} 注意这里不是直接初始化,只是给一个缺省值!

class Person
{
public:
	//...
private:
	//非静态成员变量,可以在成员声明时给缺省值
	cl::string _name = "张三"; 
	int _age = 20;             
	static int _n; //静态成员变量不能给缺省值
};

delete

delete 在 C++98 中全是用于对资源的释放删除,在 C++11 中 delete 有了新的功能他可以禁止生成默认函数,比如如果想要实现一个不能被继承的类,我就有两种方法:

  1. 可以将该函数设置成私有,并且只用声明不用定义,这样当外部调用该函数时就会报错
  2. 可以在该函数声明后面加上=delete,表示让编译器不生成该函数的默认版本,我们将=delete修饰的函数称为删除函数

或者要让一个类不能被拷贝,可以用 =delete 修饰他的拷贝构造和拷贝赋值。

class CopyBan
{
public:
	CopyBan()
	{}
private:
	CopyBan(const CopyBan&) = delete;
	CopyBan& operator=(const CopyBan&) = delete;
};

final & override

final修饰类

被final修饰的类叫做最终类,最终类无法被继承

class cl final //被final修饰,该类不能再被继承
{
	//...
};

final修饰虚函数

final修饰虚函数,表示该虚函数不能再被重写,如果子类继承后重写了该虚函数则编译报错

class Person
{
public:
	virtual void Print() final //final修饰虚函数不能再被重写
	{
		cout << "Person" << endl;
	}
};
//子类
class Student : public Person
{
public:
	virtual void Print() //重写编译报错
	{
		cout << "Student" << endl;
	}
};

override修饰虚函数

override修饰子类的虚函数,检查子类是否重写了父类的某个虚函数,如果没有没有重写则编译报错

class Person
{
public:
	virtual void Print()
	{
		cout << "Person" << endl;
	}
};
//子类
class Student : public Person
{
public:
	virtual void Print() override //检查子类是否重写父类的某个虚函数
	{
		cout << "Student" << endl;
	}
};

aqa 芭蕾 eqe 亏内,代表着开心代表着快乐,ok 了家人们

你可能感兴趣的:(C++,c++,开发语言)