(一)全新的C++语言

一、概述

C++ 的最初目标就是成为 “更好的 C”,因此新的标准首先要对基本的底层编程进行强化,能够反映当前计算机软硬件系统的最新发展和变化(例如多线程)。另一方面,C++对多线程范式的支持增加了语言的复杂度,通常一个范式就相当于其他的一门编程语言,学习难度大。为了使能够让C++吸引更多的用户,避免“曲高和寡”的局面,C++标准组委会有指定了一些具体的设计目标:

  • 保持稳定性和兼容性
  • 尽量使用库而不是扩展语言来增加新特性
  • 对初级用户和高级用户都能提高良好的支持
  • 增强类型的安全性
  • 增强直接操作硬件时的效率和功能

新的C++11/14 标准基本实现了这些目标,而且依然较好地保持了与之前版本的兼容性。

二、左值与右值

1. 定义

C++11/14 标准描述了左值与右值的含义,简略如下:

  • 所有表达式的结果不是左值就是右值
  • 左值(lvalue)是一个函数或者对象实例
  • 失效值(xvalue,expiring value)是生命周期即将结束的对象
  • 广义左值(glvalue,generalized lvalue)包括左值和失效值
  • 右值(rvalue)包括失效值、临时对象、以及不关联对象的值(如字面值)
  • 纯右值(pvalue)是非失效值的那些右值

这里给一个简单的解释:左值是一个可以用来存储数据的变量,有实际的内存地址(即变量名),表达式结束后依然存在,(历史上)它因在赋值操作符左边而得名;而右值(更准确来说是“非左值”)是一个“匿名”的“临时”变量,它在表达式结束时生命周期终止,不能存放数据,可以被修改,以科研不被修改(被const 修饰)。基于此,我们可以总结出鉴别左值和右值的简单方法:

  • 左值可以用取地址操作符 “&” 获取地址
  • 右值则无法使用 “&” (报错)
int x = 0; //对象实例,有名,x是左值
int* p = &++x; //可以取地址,++x是左值
++x = 10;  //前置++返回的是左值,可以赋值
p = &x++;  //后置++返回的是一个临时对象,不能取地址或者赋值,是右值,报错

在这里插入图片描述
在这里插入图片描述
因为右值是“临时”的,生命周期即将结束,之后无人会关系它的值,所以我们可以把它的所有内容转移到其他对象中,消除昂贵的拷贝代价。

2. 右值引用

有了右值的概念,右值引用也应运而生,C++11/14 标准使用 “T&&” 的形式表示右值引用,而原来的 “T&” 则表示左值引用,两者可以简称为右引用和左引用,分别表示右值对象和左值对象(但在C++98 不能引用右值)。对一个对象使用右值引用,意味着显式地标记这个对象是右值,可以被转移来优化。同时也为它添加了一个“临时的名字”,生命周期得到了延长,不会在表达式结束时消失,而是与右值引用绑定到了一起。它们都可以被 const 修饰,“const&" 是一个”万能引用“,可以引用任何对象(但增加了常量性),虽然”const T&&" 是正确的,但因为右值引用对象是“临时的”,即将消失,对它增加常量性会使得它无法修改,也无法转移,没有实际意义。

int& r1 = ++x; //左值引用
int&& r2 = x++; //右值引用,引用了自增对象后的临时对象xvalue
const int& r3 = x++; //常量左值引用,也可以引用右值
const int&& r4 = x++; //常量右值引用,无意义
cout << r2 << endl;  //右引用延长生命期,右值对象在表达式结束后仍然存在

C++11/14对此做出了新的规定——引用折叠:对引用类型 TR(可能是左引用或右引用)再进行左引用操作是 T&,右引用操作则不变化(仍然是TR),因此函数参数如果使用 T&& 的形式将总保持不变。

3. 转移语义

因为右值引用对象可以被转移进而优化代码,所以C++11/14标准头文件< utility > 里专门定义了便捷函数 std::move()来实现“转移”对象,它是一个模板函数,声明如下:

template<class T>
typename remove_reference<T>::type&& move(T&& t) noexcept;

move() 函数其实并没有任何“转移”操作,只是把一个对象明确地转换为“匿名”的右值引用,也即是说对该对象确认是右值对象,可以被安全地转移,相当于:

static_cast<T&&>(t);  //静态强制转换, 转型为右值引用

C++11/14 标准为class 新增加了参数类型为 “T&& "的转移构造函数和转移赋值函数,只要类实现了这两个特殊函数就能够利用右值对象”零成本“构造,这就是转移语义。即现代C++语言优化性能的重要手段,只要对象被move()标记为右引用,就可以转移资源,不会进行”深拷贝“。以下实现转移构造函数和转移赋值函数:

class moveable
{
private:
	int x;
public:
	moveable() {}; //缺省构造函数

	moveable(moveable&& other)  //转移构造函数
	{
		std::swap(x, other.x);
	}

	moveable& operator=(moveable&& other) //转移赋值函数
	{
		std::swap(x, other.x);
		return *this;
	}
public:
	static moveable create() //工厂函数创建对象
	{
		moveable obj;   //栈上创建对象
		return obj;  //返回临时对象,即右值,会引发转移语义
	}
};

moveable类里有一个工厂函数,它直接返回函数内的局部变量obj,在函数返回时是一个临时对象,也就是右值(无需再使用std::move()),而moveable类被定义了转移构造函数,所以可以直接使用临时对象的值来创建对象,避免了拷贝的代价,如:

moveable m1;   //缺省构造函数创建对象 
moveable m2(std::move(m1)); //条用转移构造函数,m1被转移 
moveable m3 = moveable::create(); //调用转移赋值函数

C++标准库里的string、vector、deque等组件都实现了转移构造函数和转移赋值函数,可以利用转移语义优化,所以现在在函数里返回一个大容器对象是非常高效的。这些标准容器(std::array除外),还特别增加了emplace() 系列函数,可以使用语义直接插入元素,进一步提高了运行性能,如:

vector<complex<double>> v;  //标准序列容器
v.emplace(3, 4); //直接使用右值插入元素,无需构造再拷贝

map<string, int> m;  //标准映射容器
m.emplace("metroid", "prime");  //直接使用右值插入元素,无需构造再拷贝

4. 完美转发

标准头文件< utility >里还有一个函数std::forward(),用于泛型编程时实现”完美转发“,可以把函数的参数原封不动的转发给其他函数,如下声明:

template <class T> T&& forward(T& t) noexcept;
template <class T> T&& forward(T&& t) noexcept;

forward()在使用时必须指定模板参数,它引用C++11/14 标准的引用折叠规则。


void check(int&)  //左值引用
{
	cout << "lvalue" << endl;
}

void check(int&&) //右值引用
{ 
	cout << "rvalue" << endl;
}

template<typename T>
void print(T&& v)
{
	check(std::forward<T>(v));  //完美转发,依据函数参数类型调用不同的函数
}
int x = 10;
print(x);  //传递左值引用,输出 ' lvalue '
print(std::move(x));  //传递右值引用,输出 'rvalue '

在这里插入图片描述

三、自动推导类型

C++11/14 标准增加了两个关键字: auto 和 decltype 。它们可以推到出表达式的类型信息,它们的推到能力可以极大的简化代码。

1. auto

C++是一种强静态语言,任何变量、表达式都要有明确的类型,例如:

long x = 0L; //声明x为long类型
const char* s = "hello"; //声明s为字符指针类型 

对于简单的变量,可以很容易写出它的类型,但由于类、命名空间、模板等技术的扩展应用,变量的类型逐渐变得越来越复杂,有的类型名字甚至很难写出正确的类型:

map<string, string>::iterator iter = m.begin(); //迭代器
???  f = bind1st(std::less<int>(), 2); //很难推导出正确的函数对象类型

事实上,编译器是知道这些表达式的类型的,但在C++11/14 之前,这些信息都隐藏在编译器内部。C++11/14 标准重新定义了auto关键字的语义,能够在编译器自动推导出表达式的类型。

auto x = 0L; //x为long类型
auto s = "hello"; //s为字符指针类型 
auto iter = m.begin(); //迭代器
auto  f = bind1st(std::less<int>(), 2); //推导出正确的函数对象类型

auto的用法相当简单,但也有几个需要注意的地方:

  • auto 只能用于赋值语句里的类型推导,不能直接声明变量
  • auto 总数能推导出值类型(非引用)
  • auto 运行使用”const / volatile / & / *" 等修饰符修饰,从而得到新类型
  • auto&& 总是推导出引用类型

下列代码示范了auto的更多用法:

int x = 0;
const long y = 100;
volatile string s("one punch");
auto a1 = ++x; //值类型int
auto& a2 = x;  //引用类型
auto a3 = y * y; //值类型long
auto& a4 = y;   //引用类型
auto a5 = std::move(y);  //值类型long,右引用被忽略
auto&& a6 = std::move(y); //引用类型const long&&
const auto a7 = x + x; //常引用类型const int
auto* a8 = &y; //const long*, auto本身推导为值类型
auto&& a9 = s; //引用类型volatile string&
auto a10;  //不是赋值初始化,无法推导,编译错误

auto还可以用于函数的返回值声明处,自动推导函数的返回值类型

auto func(int x) { return x * x; } //int

现代C++,编程中应当尽量使用auto,它不会右任何的效率损失,而且带来了更好的返回值类型和可读性。

2. decltype

auto关键字能够在赋值语句里推导类型,但这只是C++语言里一种很少见的应用场景,要想在任意场合都能得到表达式的类型需要使用另一个关键字:decltype。它在技术和用法上与sizeof非常相似,因为都需要编译器在编译期计算类型,但sizeof返回的是整数,而decltype返回的是类型。

decltype(expreesion)   //获取表达式类型,编译器计算

decltype 可以像 auto 一样用在赋值语句,但可以根据表达式的结果类别和表达式的性质推断出引用或非引用,能够更精确地控制类型:

decltype(x) d1 = x;  //int
decltype(&x) d2 = &x; //int*
decltype(x)& d3 = x;  //int&
decltype(x + y) d4 = x + y; //long
decltype(y)& d5 = y;//const long&

除了赋值语句,decltype 还可以用在变量声明、类型定义、函数参数列表、模板参数列表等任意的地方,因为它实际上就是一个编译器的类型名(只是通过表达式计算得到)

decltype(std::less<int>()) func; //声明一个函数对象,注意不是赋值语句

decltype(0.0f) func(decltype(0L) x) {return x * x;} //用于函数返回值和参数声明

typedef decltype(func)* fun_ptr; //简单地定义函数指针裂隙

vector<int> v;
decltype(v)::iterator iter; //计算v的类型,再取其迭代器类型

template<typename T> 
class demo {};//模板类

demo<decltype(v)> obj; //模板参数使用decltype

auto 和 decltype 用法一样,但同样有语法细节需要注意:

  • decltype(e) 的形式获得表达式计算结果的值类型
  • decltype((e)) 的形式获得表达式计算结果的引用类型,类似 auto&& 的效果
int x = 0; //int
const volatile int y = 0; //直接对它所在内存读取数据,而不是使用保存在寄存器的的备份
decltype(x) d1 = x;   //int
decltype((x)) d2 = x;  //int&
decltype(y) d3 = y; //const volatile int
decltype((y)) d4 = y; //const volatile int&

下面示例更好的解释decltype(x) 和 decltype((x)) 的区别:

decltype(p->x) d5 = 42;    //int
decltype((p->x)) d6 = p->x;  //int&
decltype(p->x)& d7 = p->x; //报错

这里我们声明了一个volatile的指针p,decltype(p->x) 只能得到的值类型int,失去了volatile修饰,而decltype((p->x)) 可以得到p->x 的真正引用类型。

3. decltype(auto)

auto和decltype这两个关键字都可以推导类型,但用法有差异。auto的使用更加方便,但用途有限,只能用在赋值语句里; decltype用法广,可以任意推导表达式的类型,但使用时必须在括号内写全表达式,用法不便。C++14 标准增加了一种新的语法,运行将两者结合起来,即 “decltype(auto)” ,使用decltyoe 的语义推导,但用的却是 auto 语法,如:

//仅 C++ 14
decltype(auto) x = 6; //int
decltype(auto) y = 7L;  //long
decltype(auto) z = x + y; //long 

四、面向过程编程

C++语言继承了C语言的传统,支持最基本的面向过程编程,这个编程范式里C++11/14 的变化并不多,但增加的新特性却可以改进程序,甚至改变我们的编程思维。

1. 空指针

一直以来,在C/C ++ 语言里空指针都使用宏 NULL 表示,它的定义通常是 “0”,即:

#define  NULL 0  //空指针宏NULL定义

但NULL存在严重的缺陷,它实际上是一个整数,而不是真正的指针,所以有时候会造成语义混淆(例如重载函数的参数)。C++11/14 增加了新的关键字“nullptr" ,彻底解决了这个问题,增加了安全性。
"nullptr"明确地表示空指针的概念,可以完全替代NULL,它可以隐式转化为任意类型的指针,也可以与指针进行比较运算,但绝不能转化为非指针都其他类型:

int* p1 = nullptr;  //初始化为空指针
vector<int>* p2 = nullptr;//

assert(!p1 && !p2); //
assert(10 >= nullptr); //报错

nullptr 与 NULL 有重要区别,它的强类型的,但类型不是int或者void*,而是一个专用的类型nullptr_t ,其利用decltype 的能力:

typedef decltype(nullptr) nullptr_t; //nullptr_t的定义

还需要注意,nullptr 并不是指针,而是一个类型为nullptr_t 的编译期常量实例,只是其行为很像指针,所以我们也可以使用nullptr_t 任意定义与nullptr 等价的空指针常量,如:

nullptr_t nil; //初始化一个新的空指针常量
double* p3 = nil; //使用nil初始化空指针
assert(nil == p3); //完全等价

2. 初始化

C++中初始化是一个基本操作,但C++98 标准并没有非常明确的定义,而且初始化的语法也不一致。C++11/14 标准对这个问题给出了完整的解决方案,统一使用花括号 ”{}“ 初始化变量,称为 ”列表初始化“,例如:

int x{};   //x缺省初始化,值为0
double y{ 2.13 };  //
string s{ "hellow" }; //
complex<double> c(1, 1); //
int a[] = { 1, 2, 3 }; //
vector<int> v = { 4, 5, 6 }; //

在函数里也可以使用 ”{ . . . }" 作为值返回,类型自动推导:

set<int> get_set()
{
	return { 2, 3, 9 };  //直接使用花括号返回一个集合容器
}

实际上,花括号形式的语法会生成一个类型为 std::initialized_list 的对象,它定义在头文件< initializer_list > 里,具有类似标准容器的接口,只要实现对它的构造函数就可以支持列表的初始化。

3. 新式for循环

遍历并操作 array、vector等容器里的元素是C ++里常见的操作,通常我们会使用循环语句,利用容器的首尾位置来完成,例如:

int a[] = {1, 2, 3, 5};
for(inti = 0 ;i < 4;i  ++)   //按int索引遍历
{
	cout << a[i] << " " ;
}

vector<int> v = {12, 25};
for(auto iter = v.begin(); iter != v.end(); iter ++)  //使用迭代器遍历
{
	cout << *iter << " " ;
}

C++11/14 引入了一种更简单便捷的方式,无需显式使用迭代器首尾位置,也无需解引用迭代器,就可以直接访问容器序列里的元素,如:

for (auto x : v)	cout << x << " "; //直接访问,无须遍历
for (const auto& x : v) cout << x << " "; //推导为常引用类型

这种新式for循环的正式名称是“基于范围遍历的for”(range-based for),使用两个“:” 分割了两个表达式,第一个是遍历容器时的元素类型,通常我们使用auto来自动推导,第二个是目标容器。
在声明元素类型时使用auto推导出类型,有拷贝代价,也不能修改元素,所以可以为auto添加修饰,如:“const auto& / auto&& "来避免拷贝,或者使用 auto& 来修改元素的值。

for(auto & x : v) cout << ++x << " ";  //推导为引用类型,可修改值

新式for循环支持C++ 内建数组和所有标准容器,对于其他类型,只要它具有begin(),end()成员函数,或者能够使用函数std::begin() 和 std::end() 确定迭代范围就可以应用于 for。注意:新式for循环只是一种”语法糖“,本质上还是使用迭代器来实现。

auto&& _range = v;
for (auto _begin = std::begin(_range);  //获得容器的引用
		_end != std::end(_range);  //确定范围迭代起点
		_begin != _end; _begin++);  //确定范围迭代终点
{
	auto x = *_begin; //解引用
	...
}

迭代范围已经在for循环开始前就确定好了,所以在for循环里我们不能变动容器,也不能增减容器里的元素,否则会导致遍历的迭代器失败,发生为定义的错误。

4. 新式函数声明

C++ 11/14 增加了一种新的函数语法,允许返回类型后置,它使用了auto 和 decltype 的类型推导能力,基本形式为:

auto func(...) -> type {...} //语法

有两处变化,首先,函数返回值必须使用auto来占位;其次,函数名后需要用 ”-<type " 的形式来声明真正的返回值类型,这里的“type" 可以说任意类型,也包括 decltype,如:

auto func(int x) -> declype(x)
{
	return x * x;
}

这种语法看起来十分怪异,但实际上在泛型编程时,函数返回值的可能类型需要由实际的参数来决定,所以有必要将返回值类型的声明”延后“,如下:

template<typename T, typename U>  //目标参数列表
auto calc(Tt, U u)  -> decltype(t + u)  //后置函数声明
{ reutrn t + u; }    //返回两个变量之和

后置式函数声明语法虽然不常用,但在关键时刻能够解决特定的问题

五、面向对象编程

面向对象编程是一种很重要的编程范式,可以很好的控制类的封装、继承和多态

1. default

C ++11/14 重用了关键字default,可以显示地声明类的缺省构造 / 析构等特殊成员函数,能很好的表示代码意图。default 的用法于声明纯虚构函数的语法类似,在构造 / 析构 等成员函数后面是用 ”=default" 就可以了,如:

class default_demo
{
public:
	//显式知道构造函数和析构函数使用编译器的缺省实现
	default_demo() = default; 
	~default_demo() = default;

	//显式知道拷贝构造函数和拷贝赋值函数使用编译器的缺省实现
	default_demo(const default_demo&) = default;
	default_demo& operator = (const default_demo&) = default;

	//显式知道转移构造函数和转移赋值函数使用编译器的缺省实现
	default_demo(default_demo&&) = default; 
	default_demo& operator= (default_demo&&) = default;
};

使用default 声明缺省构造函数后并不影响其他构造函数的重载于实现,我们仍然可以编写其他形式的构造函数:

class default_demo
{
public:
	... //之前的default 构造、析构函数
	int defautl_demo(int x);
};

2.delete

与defaule 类型,在C++11/14 里关键字delete 也增加了一种用法,可以显式地禁用某些函数——通常是类的拷贝构造函数和构造函数,以阻止对象的拷贝,如:

class delete_demo
{
public:
	delete_demo() = default; //使用default 缺省实现
	~delete_demo() = default;

	//显式禁用拷贝构造函数和拷贝赋值函数
	delete_demo(const delete_demo&) = delete;
	delete_demo& operator = (const delete_demo) = delete;
};

delete_demo s1;  //声明一个对象
delete_demo s2 = s1; //无法拷贝赋值,发生编译错误

在这里插入图片描述
显式delete不仅可以用于类成员函数,也可以作用于普通函数,禁用某些形式的重载。

3. override

C++ 的类继承体系里有虚函数的概念,它可以允许时动态绑定,是实现多态的关键。虚函数的声明需要使用virtual 关键字,如果一个成员函数是虚函数,那么在后续派生类里的同名函数都会说虚函数,无须再使用virtual 修饰。
但当继承关系较复杂或者派生类里的成员函数很多时,阅读者很难分辨出哪些函数继承自基类,哪些函数是派生类特有的,增加了代码的维护成本,且派生类可能无意使用了同名但签名不同的函数 “覆盖” 了基类的虚函数。
如下示例:

struct base 
{
	virtual  ~base() = default; //虚析构函数

	virtual void f() = 0; //纯虚函数

	virtual void g() const {}; //虚函数,const修饰

	void h() {}
};

struct derived : public base //派生类
{
	virtual ~derived() = default; //虚析构函数,用default修饰

	void f() {} //虚函数重载
	void g() {} //不是虚函数重载,签名不同,无const修饰
	void h() {} //不是虚函数重载,直接覆盖
};

base类很清晰,它定义了两个虚函数接口 f() 和 g(),还有一个非虚函数 h(),但是单独看derived类却信息有限,f()、g()、h() 三个函数中只要 f() 是正确的虚函数重载,g() 因为少了 “const” 修饰,函数与基类不同,是一个新的成员函数,而h() 函数则与虚函数无任何关系,直接是derived类自己专有的函数,“覆盖”了base类的原函数实现。

unique_ptr<base> p(new derived); //一个派生类对象,使用基类指针

// unique_ptr: 禁止拷贝和赋值,只能 unique_ptr<int> p1(new int(20)); 

p->f(); //正确,调用了派生类的f()
p->g(); //错误,调用了基类的g()
p->h(); //调用了基类的h(),未实现原意图

C++11/14里增加了一个特殊的标识符 “override" ,它可以显式地标记虚函数的重载,明确代码编写的意图,派生类里的成员函数如果使用了override 修饰,则必须是虚函数,而且签名也必须与基类的声明一致。即,可修改代码为如下所示:

void f() override{} //虚函数重载
void g() const override{} //虚函数重载

4. final

C++的类体系非常灵活,但这种灵活有时候会带来麻烦,没有阻止类继承或者阻止重载虚函数的手段,对于标准库里面的vector、list等容器,设计者也不希望它们派生出子类,但在C++11/14 之前没有语言层面的强制保证。
与override一样,它也增加了一个特殊的标识符 ”final”,不仅可以控制类的继承,也可以控制虚函数:

  • 在类名后面使用final,显式地禁止类被继承,即不能有派生类
  • 在虚函数后使用final,显式地禁用该函数在子类里再被重载

override 和 final 可以婚姻,更好的标记类的继承体系虚函数,如下:

struct interface
{
	virtual void f() = 0; //纯虚函数
	virtual void g() = 0;
};

struct abstrace : public interface
{
	void f() override final {} //f()不能再被继承和重载
	void g() override;  //不能再被继承,还可以重载
};

struct last_final final : public abstrace
{
	void f() {}; //不能重载
	void g() {}; //g() 仍然可以重载
};

struct error : public last_final {}; //last_final 不能被继承

在这里插入图片描述
final也不是关键字,仅在类声明里有特殊含义,但不建议再将它作为其他的标识符。

5. 委托构造

有时候我们会声明多个不同形式的构造函数,用于不同情况下创建对象,这些代码大多都有初始化成员变量,非常类似,仅有少量不同,但代码却不能复用,导致代码冗余。而其常用的一种解决方法就是实现一个特殊的初始化函数(通常是init),然后在每个构造函数里调用它,如:

class demo
{
private:
	int x, y;
	void init(int x, int y) { x = x, y = y };

public:
	demo() {  //缺省构造函数
		init(0, 0);
	}

	demo(int a) { //但参数构造函数
		init(a, 0);
	}

	demo(int a, int b) { //双参数构造函数
		init(a, b);
	}
};

C++ 11/14 标准引入了委托构造函数(delegating constructor)的概念,解决方法相同,但不需要再次构造一个特殊的初始化函数,而是可以直接调用本类的其他构造函数,把对象的构造工作“委托”给其他构造函数来完成,能够更好的简化代码,如下:

class demo
{
public:
	demo() : demo(0, 0) {} //缺省构造函数,委托给双参数构造函数

	demo(int a) :demo(a, 0) {} //单参数构造函数委托给双参数的构造函数

	demo(int a, int b) { x = a, y = b; } //双参数构造函数,被其他构造函数调用
private:
	int x;
	int y;
};

委托构造函数可以配合类成员在初始化是使用,在类声明时先初始化成员变量,然后再使用委托构造函数金星额外的构造操作,进一步简化代码。

六、泛型编程

自从关键字template出现后,泛型编程逐渐成为C++的主流编程范式之一,更扩展出了允许在编译器的模板元编程。泛型编程使用泛型容器、泛型算法成为可能,深刻地改变了C++语言,同时也影响了C++之外的语言。

1. 类型别名

C++ 11/14 扩展了using关键字的能力,可以完成与typedef 相同的工作,使用 “using alias= type ” ,的形式为类型起别名,如:

using int64 = long; //long 类型的别名为int64
using ll = long long;  //long long 类型的别名为 ll

它与typedef 的顺序恰好相反,易于理解。但是不止于此,它超越了typedef ,可以结合template 关键字为模板类声明 “部分特化” 的别名,如:

template<typename T>  //为标准容易map取别名
using int_map = std::map<int, T>; //
int_map<string> m;  //使用别名,省略了一个模板参数

template<typename T ,typename U> //自定义类,两个模板参数
class demo final {} ;  


template<typename T> //保留一个模板参数
using demo_long = demo<T, long>  //别名,第二个参数固定

demo<char, double>; //原模板类,给出两个参数
demo_long<char>; //模板别名,给出一个参数

对于拥有复杂模板参数列表的类来说,using的别名用法可以给出一些常用的形式,简化模板类的使用,增加代码可读性。

2. 编译期常量

3. 静态断言

C语言提供断言assert,它是一个宏,可以允许时验证某些条件是否成立,有利于保证程序的正确运行,但泛型编程主要工作在编译期,assert 不起作用。而C++11/14 增加了关键字static_assert ,它是编译期的断言,可以在编译期加入诊断信息,提前检查可能发生的错误。其用法与assert 基本一致:

static_assert(condition, message); //

如果bool值表达式在编译期的计算结果为false,那么编译器会报错,并给出message的提示信息。static_assert 通常需要配合type_traits 库来使用:

static_assert(sizeof(int) == 4, "it must be 32bit !!!");

4. 可变参数模板

七、函数式编程

函数式编程(functional programming)是与面向过程编程、泛型编程并列一种编程范式,它基于 “那麽达” 演算理论,把计算过程视为数学函数的组合运算。引入了lambda 表达式,更好支持函数式编程,简化代码。

1. lambda 表达式

C++ 11/14 标准里,lambda 表达式实际上是对函数对象的一种强化和扩展,可以直接就定义“匿名”的函数对象(所谓的“语法糖”)。基本形式:

[] (params) {...}  //

这里的 [] 称为lambda 表达式引出操作符,它之后的代码就是lambda 表达式,形式如同一个标准的函数,圆括号里是函数的参数,而花括号内则是函数体,可以实现任何功能。lambda 表达式的类型称为“闭包”,无法直接写出,所以通常需要使用auto 的类型推导功能来存储,如下:

auto f1 = [](int x) 
{	
	return x * x; 
};   //末尾需要分号

int a[] = {9, 2, 3, 1, 6};
sort(a, a + 5, [](int x, int y){ return x < y; }); //排序

lambda 表达式的返回值会自动推导,但也可以使用新的返回值后置语法:

auto f = [](int x) -> long { . . .}; //指定返回值类型为 long

2. 捕获外部变量

lambda 表达式的功能不止于此,它还能捕获外部变量。其完整声明语法是:

[captures] (params) mutable -> type {. . .} //

操作符 [] 里的“captures” 称为“捕获对象”,可以捕获表达式外部作用域的变量,在函数式内部直接使用。

操作符功能
[]无捕获,函数式内不能访问任何外部变量
[=]以值(拷贝)的方式捕获所有外部变量,函数体内可以访问但不能修改
[&]以引用的方式捕获所有变量,函数体内可以访问并修改
[var]以值(拷贝)的方式捕获某个外部变量,函数体可以访问但不能修改
[&var]以引用的方式捕获某个外部变量,函数体内可以访问并修改
[this]捕获this指针,可以访问类的成员变量和成员函数
[=, &var]引用捕获变量var,其他外部变量使用值捕获
[&, var]值捕获变量var,其他外部变量使用引用捕获

下面的代码示范了这些列表的用法:

int x = 0, y = 0;
auto f1 = [=]() { return x; }; //以值方式捕获使用变量,不能修改
auto f2 = [&]() {return x++; }; //以引用方式捕获变量,可修改
auto f3 = [x]() { return x; }; //值捕获x
auto f4 = [x, &y]() {y += x; }; //值捕获x,引用捕获y,可修改y
auto f5 = [&, y]() {x += y; }; //值捕获y,其他外部变量为引用捕获
auto f6 = []() {return x; }; //无捕获,不能使用外部变量,报错

需要注意值捕获发生在lambda 表达式的声明之前,如果使用值方式捕获,即使之和变量发生变化,lambda 表达式也不会感知,仍然使用最新的值;如想要使用外部变量的最新值就必须使用引用的不会方式,担心变量的生命周期,防止引用失败。
lambda 表达式还可以使用关键字 mutable 修饰,它为值捕获添加了一个例外情况,允许变量在函数体也能修改,但这只是内部的拷贝,不会影响外部的变量,如:

auto f = [=]() {return x ++;};  //可以在内部修改,不影响外部变量

lambda 表达式也可以转换为一个签名相同的函数指针,但需要注意转换时,它必须是无捕获列表的,如:

typedef void (*func)(); //函数指针类型
func p1 = []() {cout << endl; }; //直接赋值

auto g = [&]() { x++; }; //有捕获的lambda 的表达式
func p2 = g; //无法转换,报错

八、并发编程

并发编程是提高程序允许效率的一个必备手段,C++11/14 充分考虑了这个现实的需求,以库的形式提供了较好的支持,如< thread> 、< mutex >、< atomic >等,还新增了关键词 thread_local ,它实现了线程的本地村村,是一个与extern 、static 类型的变量类型存储指示标记。
线程本地存储是多线程编程里的概念,是指变量在进程中拥有不止一个实例,每个线程都会由于一个完全独立的、“线程本地化” 的拷贝,多个线程对变量的读写互不干扰,完全避免了竞、同步的麻烦,如:

extern int x; //外部变量,实体存储在外部,非本编译单元
static int y = 0; //静态变量,实体存在本编译单元内
thread_local int z = 0;  //线程局部存储,每个线程都拥有独立的实体

以上代码声明了三个变量: x 使用extern 修饰,是一个外部变量; y 使用static 修饰,是一个静态变量,只能在本实现文件内访问,在多线程下是不安全的; 而 z 使用了 thread_local 关键字,是线程安全的,每个线程都有一份属于直接的独立的实例。下面代码验证了其结果:

auto f = [&]() {  //lambda 表达式,线程实际执行的函数
	y++;
	z++;
	cout << y << " " << z << endl; //输出值
	};

thread t1(f);  //启动两个线程
thread t2(f);

t1.join();  //回收线程
t2.join();

cout << y << " " << z << endl;  //在主线程里输出变量值

运行结果如下:
在这里插入图片描述
可见,静态变量y在进程里是唯一的,两个线程都改变了y的值,而z因为是thread_local 的,两个子线程和主线程分别持有互相独立的三个实例,所有子线程里均独立的加 1,而主线程因为没有对z做任何操作,所有值为0.
thread_local 变量的声明周期比较特殊,它在线程启动时构造,在线程结束时析构,也就是说仅在线程的声明周期里是有效的,比static 变量的生命周期短,但比普通局部变量的生命周期长。thread_local 仅适用于线程需要独立存储的情况,当线程间需要共享资源访问时,仍然需要使用互斥量等保护机制。

九、面向安全编程

1. 无异常保证

C++ 的异常机制比较复杂,允许程序抛出任意类型的对象作为异常,为了规范异常的使用,C++ 提出了 “ 异常规范” 的概念,可以使用 throw (. . .)的形式来说明函数可能会抛出异常,但用得较少。而C++11/14 里 “异常规范” 被废弃,保留了一个很小的功能:声明函数不会抛出任何异常,并且引入一个关键字 noexcept 来明确表明这个含义,如:

void func() noexcept;  //函数决不会抛出异常

使用它可以减少异常处理的成本,提高运行效率。

2. 内联命名空间

C++ 使用命名空间来解决命名冲突的问题,关键字 namespace 可以声明一个专有的作用域,其内的所有变量、函数或者类都不会与外部发生冲突,但是使用时也必须加上命名空间的限定,或者使用using打开 命名空间。它通常需要用一个名字来标识,但C++ 也允许不使用命名空间来声明一个“匿名” 的命名空间,这是相当与使用 static 静态初始化了命名空间里的成员,如:

namespace{
	int x=  0;  //具有静态属性
}
assert(x == 0);  //可直接访问

也可以增肌一个 inline 关键字修饰,使其也不需要命名空间限定,可直接访问

inline namespace  temp
	int xx = 0;
}
assert(xx == 0);

内联命名空间的这个特性对于代码的版本化很有用,可以在多个子命名空间里实现不同的功能,而且发布的时候对外只暴露一个实现,隔离版本的差异,有利于维护,如:

namespace release {  //对外发布的命名空间
	namespace v001 { //旧版本
		void func() {}
	} 
	inline namespace v002 { //使用inline内联
		void func() {}
	}
}

release::func();  //看不到子命名空间,直接使用父命名空间

3. 强枚举类型 enum

enum color{  //弱枚举类型
a = 1, b = 2, c = 3
};

assert(b == a + 1); //可以运算
int a = 1;   // 报错,编译错误
强枚举类型可以使用 ”struct / class “ 的形式声明, 但它不能被隐式转化为整数
enum class color{ . . . }
auto x = color::a; //正确,必须使用类型名
auto y = a; //错误,没有类型名限定
auto z = color::a + 1; //错误,不能隐式转化为整数运算

十、一些特性

标准预定义宏 ”_cplusplus " ,是一个整型常数,可辨别编译器版本
cout << "C++ : " << _cplusplus << endl;

超长整数 uLL、LL
auto a = 52313LL ;
auto b = 2147483647uLL ;

原始字符串 		->   两边不超过16个字符
auto s = R"hello \\\\ world"; //不需要转义
auto b = R"***(biographicldd infsfspl)***"; //定界符 ***
auto d = R"====(Dark souls)====";  //定界符 ===


auto f = 2.14f' //后缀f 指示float
auto s = L"wide char";  //前缀L,指示wcahr_t
auto x = 0x199L;  //前缀0x:十六进制;  后缀L:long类型

.........这里就不一一列举了
至此结束

总结

  • 21
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

leisure-pp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值