深入理解c++11(笔记):第二章 保证稳定性与兼容性

“Let’s begin!”

2.1 保持与C99兼容

2.1.1 预定义宏

宏名称 功能描述
__STDC_HOSTED__ 如果编译器的目标系统环境中包含完整的标准C库,那么这个宏就定义为1,否则宏的值为0
__STDC__ C编译器中通常用这个宏的值来表示编译器的实现是否和C标准一致。
__STDC_VERSION__ C编译器通常用这个宏来表示所支持的C标准的版本
如果用户重定义(#define)或(#undef)了预定义的宏,那么后果就是“未定义”的。  

2.1.2 __ func __ 预定义标识符

其基本功能就是返回所在函数的名字。

#include <string>
#include <iostream>
using namespace std;
const char *hello() { return __func__; }
const char *world() { return __func__; }
int main()
{
    cout << hello() << "," << world() << endl;
}
// 编译选项:g++ -std=c++11 xx.cpp
// 下面的方式是可行的
struct TestStruct {
    TestStruct() : name(__func__) {}
    const char *name;
}
// 但是这样的形式是无法通过编译的
void FuncFail(string func_name = __func__) {}
// 这是由于在参数声明时,__func__还未被定义

2.1.3 _Pragma 操作符

在C++11中,准定义了与预处理指令#pragma功能相同的操作符_Pragma。

#pragma once等同于_Pragma(“once”)

相比于预处理指令#pragma,由于_Pragma是一个操作符,因此可以用在一些宏中。而#pragma不能在宏中展开,C++11的_Pragma具有更大的灵活性。

#define ON CE("on")
#define CE(x) _Pargma(#x"ce")
ON          // 等同于#pragma once

2.1.4 变长参数的宏定义以及__VA_ARGS__

在C99标准中,程序员可以使用变长参数的宏定义。

#define PR(...) printf(__VA_ARGS__)  

2.1.5 宽窄字符串的连接

窄字符串(char)转换成宽字符串(wchar_t)在之前的C++标准中是未定义的行为。支持C++11标准的编译器会将窄字符串转换成宽字符串,然后再与宽字符串进行连接。

2.2 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 = ULLONG_MAX;
}

2.3 扩展的整型

C++11一共只定义了以下5种标准的有符号整数:

signed char、 short int、 int、 long int、 long long int  

2.4 宏__cplusplus

在C++11中,__cplusplus被预定义为201103L。

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

预处理指令#error,使得非c++11标准的编译器立即报错并终止编译。

2.5 静态断言

2.5.1 断言:运行时与预处理时

在C++中,标准在<cassert>或<assert.h>头文件中为程序员提供了assert宏,用于在运行时进行断言。
在C++中,程序员可以定义宏NDEBUG来禁用assert宏。

// 事实上,assert宏在<cassert>中的实现方式类似于下列形式
#ifdef NDEBUG
#define assert(exptr) (static_cast<void>(0))
#else
// do some things
#endif

事实上,通过预处理指令#if和#error的配合,也可以让程序员在预处理阶段进行断言

2.5.2 静态断言与static_assert

static_assert使用起来非常简单,它接收两个参数,一个是断言表达式,这个表达式通常需要返回一个bool值;一个则是警告信息,它通常是一个字符串。

template <typename t, typename u> int bit_copy(t &a, u &b)
{
    static_assert(sizeof(b) == sizeof(a), "the parameters of bit_copy must have same width.");
};
// 这样的错误信息就会在程序的编译时期打印,非常的方便

注意:static_assert的断言表达式的结果必须是在编译时期可以计算的表达式,即必须是常量表达式。

2.6 noexception修饰符与noexcept操作符

异常通常用于路基上可能发生的错误。
noexcept修饰函数不会抛出异常。与throw()不同的是,在C++中如果noexcept修饰的函数抛出了异常,编译器可以选择直接调用std::terminate()函数来终止程序的运行,这样的效率高于异常机制的throw()。

void excpt_func() noexcept;
void excpt_func() noexcept (常量表达式);
// 缺省为true,表示函数不会抛出异常
// 反之,则表示有异常抛出
// noexcept作为一个操作符时,通常可以用于模板
template <calss T>
void fun() noexcept(noexcept(T())) {}
// 这里fun函数是否是一个noexcept的函数,将由T()表达式是否会抛出异常所决定。
// 这里的第二个noexcept就是一个noexcept操作符。
// 当其参数是一个有可能抛出异常的表达式的时候,其返回值为false,反之为true

虽然noexcept修饰的函数通过std::terminate的调用来结束程序的执行的方式可能会带来很多问题(析构函数正常调用,栈无法自动释放)。但是提高了程序退出的速率。往往是更加有效的。

// 在C++98中,new可能会包含一些抛出的std::bad_alloc异常
void *operator new(std::size_t) throw(std::bad_alloc);
// 在C++11中,使用noexcept(false)来进行替代
void *operator new(std::size_t) noexcept(false);

noexcept更大的作用是保证应用程序的安全。一个类析构函数不应抛出异常。因此应在析构函数以及析构函数经常调用的delete函数设置成noexcept,借此提高程序的安全性
C++11中析构函数在缺省的情况下被默认设置为noexcept(true),从而阻止了异常的扩散。

2.7 快速初始化成员变量

在C++98中,支持了在类声明中使用等号“=”来初始化类中的静态成员常量。

struct init { int a = 1; double b{1.2}};
// 这在c++11中是合法的声明

简而言之:在C++11中类成员的初始化形式非常多样。
此外值得注意的是,对于非常亮的静态成员变量,C++11则与C++98保持了一致。程序员还是需要到头文件以外去定义它,这会保证编译时,类静态成员的定义最后只存在于一个目标文件中。

2.8 非静态成员的sizeof

C++98中,费静态成员变量使用sizeof是不能够通过编译的。

#include <iostream>
using namespace std;
struct Prople {
public:
    int hand;
    static Peiple *all;
};
int main() {
    People p;
    // C++98, C++11 均通过
    cout << sizeof(p.hand) << endl;
    cout << sizeof(People::all) << endl;
    // C++98不通过,C++11通过
    cout << sizeof(People::hand) << endl;

    // C++98的技巧
    sizeof(((People*)0)->hand);
}

2.9 扩展的friend语法

friend关键字用于声明类的友员,友员可以无视类中成员的属性。无论成员是public、protected或是private的,友元类或友元函数都可以访问。
但是同时也破坏了面向对象编程中封装性的概念。因此friend关键字充满了争议性。

// C++11中,可以为类模板声明友员了。这是C++98中无法做到的
class P;
template <typename T> class People {
    friend T;
};
People<P> PP;   //类型P在这里是People类型的友员
People<int> Pi; //对于int类型模板参数,友员声明被忽略(因为是基础类型的缘故)

2.10 final/override控制

C++11采用了final关键字来组织函数继续重写。final关键字的作用是使派生类不可覆盖它所修饰的虚函数。

struct Object {
    virtual void fun() = 0;
};
struct Base : public Object {
    void fun() final;
};
struct Derived : public Base {
    void fun(); //无法通过编译
};

基类中的虚函数也是可以使用final关键字的,不过这样该虚函数无法被子类中重写,这样就失去了虚函数的意义。(虚函数生来就是要被子进程重写的)
在C++中有一个特点,对于积累声明为virtual的函数,之后重写版本都不需要声明该函数为virutal。

在C++11中为了帮助程序员写继承结构复杂的类型,引入了虚函数描述符override。声明后函数必须与重载积累中的函数同名,否则无法通过编译。

2.11 模板函数的默认模板参数

C++11中增加了默认参数模板功能

template <class T, class U = double>
void f(T t = 0, U u = 0);
void g() {
    f(c, 'c');      // f<int, char>(1, 'c')
    f(1);           // f<int, double>(1, 0)
    f();            // 错误,无法被解析
    f<int>();       // f<int, double>(0, 0)
    f<int, char>(); // f<int, char>(0, 0)
}

2.12 外部模板

“外部模板”是C++11中一个关于模板性能上的改进。

extern int i;
// 这样做的好处是,生成的多个目标文件中只有i变量的一份定义

对于源代码中出现的每一处模板实例化,编译器都需要去做实例化的工作;而在谅解时,连接器还需要移除重复的实例化代码。
在广泛使用模板的项目中,由于编译器会产生大量冗余代码,会极大增加编译器的编译时间和链接时间。

image

// C++11中,加入了外部模板的声明。
extern template void fun<int>(int);

实际上,C++11中“模板的显示实例化定义、外部模板声明和使用”好比“全局变量的定义、外部声明和使用”方式的再次使用。外部模板定义应该算作一种对编译器的编译时间及空间的优化手段。再有在项目较大的情况下,才有这样优化的意义。

image

2.13 局部和匿名类型作模板实参

在C++98中,标准对模板实参的类型还有一些限制。(局部的类型和匿名的类型在C++98中都不能做模板类的实参)

template<typename T> class X {};
template<typename T> void TempFun(T t) {};

struct A{} a;
struct {int i;} b;          // 匿名类型变量
typedef struct {int i;} B;  // 匿名类型

void Fun()
{
    struct C {} c;          // 局部类型
    
    X<A> x1;
    X<B> x2;                // C++98错误,C++11通过
    X<C> x3;                // C++98错误,C++11通过
    
    TempFun(a);
    TempFun(b);             // C++98错误,C++11通过
    TempFun(c);             // C++98错误,C++11通过
    
    // 但是,下面这种写法在C++11中也是不被允许的
    X<struct {int a;}> d;
}