C++ 11——保持稳定性与扩展
目录
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_MIN
、LLONG_MAX
和ULLONG_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.