long long 整型

相比于C++ 98,C++ 11整型最大的改变就是多了 long long。

long long整型有两种:long long 和 unsigned long long。在C++ 11中,标准要求 long long 整型可以在不同平台上有不同的长度,但至少要有64位。我们可以在赋值的时候使用LL后缀来标识 long long类型的字面量,或者ULL 表示一个unsigned long long 类型的字面量。

long long int a = 9000000000000000LL;
unsigned long long int b = 9000000000000000ULL;
/** * 在C++ 11中有很多与long long 等价: * long long 、signed long long 、 long long int、 signed long long int */

与其他整型一样,要了解平台上long long 大小的方法就是查看 <climits>,与long long 有关的宏有三个:LLONG_MINLLONG_MAXULLONG_MIN。分别代表了平台上最小的long long值,最大的long long值以及最大的unsigned long long值。

//以下代码测试各平台的long long大小
#include <climits>
#include <cstdio>
using namespace std;

int main()
{
   
	long long ll_min = LLONG_MIN;
	long long ll_max = LLONG_MAX;
	unsigned long long ull_max = ULLONG_MAX;
}

扩展整型

C++ 11一共只定义以下五种标准的有符号的整型。

  • signed char
  • short int
  • int
  • long int
  • long long int

标准同时规定,每一种有符号的整型都有一种对应的无符号整数版本,且有符号整型与其对应的无符号整型具有相同的存储空间大小

在这五种整型之外,C++ 11标准也对这样的扩展做出了一些规定:其允许编译器扩展自有的所谓扩展整型。这些扩展整型的长度可以比最长的标准整型还长,也可以介于两个标准整数的位数之间

简单的说,C++ 11规定,扩展的整型必须和标准类型,有符号类型和无符号类型占用同样大小的内存空间。对于C/C++ 来说,当运算、传参等类型不匹配的时候,整型间会发生隐式的转换,这又叫做整型的提升

int a = 1;
long long int b = 2;
long long int c = a + b;
//a会被提升成long long

整型提升一般有如下规则:

  • 长度越大的整型等级越高
  • 长度相同时,标准整型的等级高于扩展类型
  • 相同大小的有符号和无符号整型等级相同

在进行隐式整型转换的时候,一般是按照低等级整型转换成为高等级整型,有符号的转换为无符号的

宏 __cplusplus

在C和C++混合编写的代码中,头文件中会看到如下声明:

#ifdef __cplusplus
extern "C" {
   
#endif
//code
#ifdef __cplusplus
}
#endif

以上做法通常会使程序员认为__cplusplus这个宏只有被定义了和未定义两种状态。事实上,__cplusplus这个宏被定义为一个整型值。而随着标准变化,__cplusplus宏一般会是一个比以往标准中更大的值。可以用以下代码检测编译器是否支持C++ 11:

#if __cplusplus <201103L
	#error "should use C++ 11"
#endif

静态断言

断言:运行时与预处理时

通常情况下,断言就是将一个返回值总是需要为的判别式放在语句中,用于排除在设计的逻辑上不应该产生的情况。

在C++ 中,标准在<cassert><assert.h>中为开发者提供了assert宏,用于在运行时进行断言。例子如下:

#include <cassert>
using namespace std;

char* ArrayAlloc(int n)
{
   
	assert(n > 0);
	return new char[n];
}

int main()
{
   
	char* a = ArrayAlloc(0);
}
//如果未满足打印一下信息
//xxxx:xxxx.cpp: 6: char* ArrayAlloc(int):Assertion 'n>0' failed.
//Aborted

下面我们来看一下assert宏的实现:

#ifdef NDEBUG
#define assert(expr) {statcic_cast<void> (0)}
#else
...
#endif

可以看到,一旦定义了NDBUG宏,assert宏将被展开为一条无意义的语句,并且极大可能被编译器优化掉。

静态断言和static_assert

前面的例子可以看出来,貌似assert宏只在运行时才起作用。而#error只在编译器预处理时才起作用。而如何在编译时做一些断言呢?

//在这里我们想得到编译时断言,却是运行时断言
#include <cassert>
using namespace std;

//枚举编译器对各种特性的支持
enum FeatureSupports
{
   
	C99 = 0x0001;
	ExtInt = 0x0002;
	SAssert = 0x0004;
	NoExcept = 0x0008;
	SMAX = 0x0010;
}

//一个编译器类型,包括名称、特性支持等
struct Complier{
   
	const char* name;
	int spp;
}

int main()
{
   
	//检查枚举类型是否完备
	assert((SMAX - 1) == (C99|ExtInt|SAssert|NoExcept));

	Complier a = {
   "abc",(C99|SAssert)};
	//...
	if(...)
	{
   
		...
	}
}

针对以上问题,我们可以使用静态断言,比较经典的就是开源库Boost内置的BOOST_STATIC_ASSERT断言机制。

//用以下代码可实现除0的静态断言
#define assert_static(e)\ do{\ enum{ assert_static__ = 1/(e) };\ }while(0)

在C++ 11标准中,引入static_assert断言来解决这个问题。其使用起来非常简单,它接受两个参数,一个是断言表达式,这个表达式通常需要返回一个bool值;一个则是警告信息,通常是一个字符串。

static_assert(sizeof(b) == sizeof(a),"byte is not equal");
//表达式的结果必须是在编译时期可以计算的表达式!!!

而且静态断言是编译时断言,其可以写在任何地方,包括函数体外。

这里建议写在函数体外,方便开发者明白这是一个断言而不是定义的函数。

noexcept修饰符合noexcept操作符

相比于断言适用于排除逻辑上不可能的,异常通常是用于逻辑上可能发生的错误

void excpt_func() throw(int,double){
   ...}

在函数声明之后,我们定义了一个动态异常throw(int,throw),该声明指出了函数可能抛出的异常的类型。但是,因为在C++ 11中被弃用了,而表示函数不会抛出异常的动态异常声明throw()也被新的noexcept异常声明所取代。

noexcept形如其名,表示其修饰的函数不会抛出异常,不过与throw()动态异常声明不同的是,在C++ 11中如果noexcept修饰的函数抛出了异常编译器可以选择std::terminate()来终止程序的运行。这比基于异常机制的throw()在效率上会高一些。这是因为异常机制会带来一些额外开销,比如函数抛出异常,会导致函数栈被依次地展开,并依次调用析构函数。

从语法上讲,第一种就是简单地在函数声明后加上noexcept关键字:

void excpt_fun() noexcept;

另一种则可以接受一个常量表达式作为参数:

//常量表达式的结果会被转换成一个bool类型的值。该值为true,表示函数不会抛出异常
//不带常量表达式的noexcept相当于noexcept(true)
void excpt_func() noexcept(常量表达式)

在C++ 11中使用noexcept可以有效的阻止异常的传播和扩散。例子如下:

#include <iostream>

using namespace std;

void Throw(){
    throw 1;}
void NoBlockThrow() {
    Throw();}
void BlockThrow() noexcept {
    Throw();}

int main()
{
   
	try{
   
		Throw();
	}
	catch(...){
   
		cout<<"Found Throw"<<endl;
	}
	try{
   
		NoBlockThrow();
	}
	catch(...){
   
		cout<<"NoBlock Throw"<<endl;
	}
	try{
   
		BlockThrow();		//调用terminate中断程序执行
	}
	catch(...){
   
		cout<<"Found Throw 1"<<endl;
	}
}

并且noexcept作为一个操作符,通常可以用于模板:

template <class T>
	void fun() noexcept(noexcept(T())) {
   }

这里,fun函数是否是一个noexcept的函数,将由T()表达式是否抛出异常所决定,第二个noexcept就是一个noexcept操作符。

另外要说一下的就是,在C++ 中,一个类的析构函数不应该抛出异常,那么对于常备析构函数调用的delete函数来说,也应该是noexcept,以提高应用程序的安全性:

void operator delete(void *) noexcept;
void operator delete[](void *) noexcept;

当然,类的析构函数默认也是noexcept(true)。当然,如果程序员显式的为析构函数指定了noexcept,析构函数就不会保持默认值。

快速初始化成员变量

之前我们一直使用“就地”声明的方式来初始化类中静态成员常量。

//C98 中
class Init{
   
public:
	Init():a(0){
   }
	Init(int d):a(d){
   }
private:
	int a;
	const static int b = 0;
	int c = 1;		//成员,无法编译通过
	static int d = 0;	//成员,无法通过编译
	static const double e = 1.3;	//非整型或者枚举,无法通过编译
	static const char* const f = "e";	//非整型或者枚举,无法通过编译
};

所以在C++ 11中,标准还允许使用等号=或者花括号{}进行就地的非静态成员初始化。

struct init{
   
	int a = 1;
	double b{
   1.2};
};

花括号的形式已经成为C++ 11中初始化声明的一种通用形式,而其效果类似于C++ 98中使用圆括号()对自定义变量的表达式列表初始化。不过在C++ 11中,对于非静态成员进行就地初始化,而这却并非等价:

#include <string>
using namespace std;

struct C{
   
	C(int i):c(i){
   }
	int c;
};

struct init{
   
	int a = 1;
	string b("hello");	//无法通过编译
	C c(1);		//无法通过编译
};

非静态成员的sizeof

sizeof作为一个特殊的运算符,在C++ 11中也得到了了扩展:

#include <iostream>
using namespace std;

struct People{
   
public:
	int hand;
	static People* all;
};

int main()
{
   
	People p;
	cout<<sizeof(p.hand)<<endl;	
	cout<<sizeof(People::all)<<endl;	
	cout<<sizeof(People::hand)<<endl;	//C98中错误,C++ 11中通过
}

扩展的friend语法

friend关键字用于声明类的友元,友元可以无视类中的成员属性:

class Poly;
typedef Poly P;

class Lilei{
   
	friend class Poly;	//C++ 98,11均通过
};

class Jim{
   
	friend Poly;	//C++ 98失败,11通过
};

我们可以看到,在C++ 11中,将不需要class关键字。这意味着程序员可以为类模板声明友元了。

class P;

template <typename T> 
class People
{
   
	friend T;	
}

People<P> PP;		//P为PP的友元
People<int> Pi;		//被忽略

final/override控制

我们先回顾一下关于重载的概念:

一个类A中声明的虚函数fun在其派生类B中再次被定义,且B中fun和A重的原型一样,那么我们就称函数fun重载了A的fun函数

通常情况下,一旦在基类A中的成员函数fun被声明为virtual的,那么对于其派生类B而言,fun总是能够被重载的。有些时候如果我们不想fun函数在B中被重载,我们可以使用final关键字

struct Object
{
   
	virtual void fun() = 0;
};

struct Base: public Object
{
   
	void fun() final;
};

struct Derived: public Base
{
   
	void fun();		//无法通过编译
};

在C++ 中重载还有一个特点,就是对于基类声明为virtual的函数,之后的重载版本都不需要在声明该重载函数为virtual。即使在派生类中声明了virtual该关键字也会被屏蔽。

而且C++ 11中为了帮助程序员写继承结构复杂的类型,引入了虚函数描述符override,如果派生类在虚函数声明时使用override描述符,那么该函数必须重载其基类中的同名函数,否则代码无法通过编译

class Base
{
   
public:
	virtual void Turing() = 0;
	virtual void Dijkstra() = 0;
	virtual void VNeuann(int g) = 0;
	virtual void Dknuth() const;
	void Print();		
};

struct DerivedMid: public Base
{
   
	void Turing() override;
	void Dikjstra() override;	//拼写错误
	void VNeuann(double g) override;	//参数不一致
	void Dknuth() override;		//常量性不一致
	void Print() override;		//非虚函数重载
};

模板函数的默认模板参数

在C++ 11中,模板和函数一样,可以有默认的参数。

void Def(int m = 3){
   }	//C++ 98成功,11成功

template <typename T = int>
class DefClass{
   };		//C++ 98成功,11成功

template <typename T = int>
void DefTempParm(){
   };	//C++ 98失败,11成功

这里要注意一点,与类模板不同,在为多个默认模板参数声明指定默认值的时候,程序员必须遵守“从右至左”原则。

外部模板

外部模板存在意义

外部这个词其实之前就已存在于C++ 中了,比如:

extern int i;

之所以要这个东西,可以假设下面一种情况:如果我们在 a 与 b 中同时定义了**全局变量 i **的话,i 会在 a 与 b 的数据区同时存在。那么链接器在连接 a.o 与 b.o 的时候,就会报告错误,因为无法决定相同的符号是否需要合并。

对于函数模板来说,现在我们遇到的几乎是一模一样的问题,不过发生问题的不是数据,而是代码!

如果我们写了一下代码:

//test.h
template <typename T>
void fun(T){
   }

//test1.cpp
#include "test.h"
void test1() {
   fun(3);}

//test2.cpp
#include "test.h"
void test2() {
   fun(4);}

这样的话,我们在 test1.o 和 test2.o 文件中会有两份一模一样 fun<int>(int)代码

代码重复和数据重复
数据重复,编译器往往无法分辨是否要共享的数据
代码重复,为了节省空间,保留其一就可,但是这样链接器的工作太过冗余!!

显式的实例化和外部模板声明

外部模板的使用实际依赖于C++ 98的已有特性,即显式实例化

template <typename T>
void fun(T){
   }

我们只需要声明 template void fun<int>(int)就可以实例化函数。而在C++ 11中又加入了外部模板的声明:

extern template void fun<int>(int);

这样我们刚刚的代码就可以改写成如下形式了:

//test1.cpp
#include "test.h"
template void fun<int>(int);
void test1() {
   fun(3);}

//test2.cpp
#include "test.h"
extern template void fun<int>(int);
void test2() {
   fun(4);}

参考文献

[1] IBM XL编译器中国开发团队.深入理解C++11.机械工业出版社.2013.06.