美文网首页
《深入理解C++11特性》

《深入理解C++11特性》

作者: Ewitter | 来源:发表于2019-01-06 19:12 被阅读0次

相当鸡肋的书,看了primer就不要看这个书了吧。

1. C++11与C99兼容的宏

__STDC_HOSTED_ : 若编译器的目标系统环境中包含完整的标准C库,
                 那么这个宏定义为1,否则宏值为0

__STDC__ : C编译器通常用这个宏值表示编译器的实现是否和C标准一致;
           C++11中这个宏是否定义以及定义为何值将由编译器来决定。

__STDC_VERSION__ :C编译器通常用此宏表示所支持的C标准的版本,
                  eg 1999mml;C++11标准中此宏是否定义以及定义为何值由编译器决定。

__STDC_ISO_10646__ :此宏通常定义为一个yyymmL 格式的整数常量,
                    eg 199712L,用来表示C++ 编译环境符合某版本的ISO/IEC 10646标准。
#include <iostream>
using namespace std;

int main()
{
    cout<<"Standard Clib: "<<__STDC_HOSTED__<<endl; //Standard Clib: -1
    cout<<"Standard C: "<<__STDC__<<endl;   //Standard C: 1
    //cout<<"C Standard version: "<<__STDC_VERSION__<<endl;
    //ISO/IEC 200009,不同编译器版本值不同
    cout<<"ISO/IEC "<<__STDC_ISO_10646__<<endl; 
    return 0;
}

2. 预定义标识符__func__

很多编译器都支持C99标准的__func__预定义标识符,其基本功能是返回所在函数的名字。

#include <iostream>
#include <string>
using namespace std;

const char *hello(){    return __func__;}
const char *world(){    return __func__;}

int main(){ 
    cout<<hello()<<", "<<world()<<endl; //hello, world
    return 0;
}

事实上,按照标准定义,编译器会隐式地在函数的定义之后定义func标识符,eg上述hello函数,其实际定义等同于如下代码:

const char *hello(){
    static const char *__func__="hello";
    return __func__;
}

__func__对于轻量级的调试代码有很大作用,但在C++11中,甚至允许使用在类或结构体中:

#include <iostream>
using namespace std;

struct TestStruct{
    TestStruct():name(__func__){}
    const char *name;
};
int main(){
    Test ts;
    cout<<ts.name<<endl;    //TestStruct
}

但是将__func__标识符作为函数参数的默认值 是不允许的,eg:void FuncFail(string func_name=__func__){} //无法通过编译
因为 在参数声明时,__func__还未被定义

3. _Pragma操作符

在C/C++标准中 #pragma是一条预处理命令,用于向编译器传达语言标准以外的一些信息,eg:
#pragma once
(若编译器支持该指令则)此指令会指示编译器该头文件应只被编译一次,
    与如下代码定义的头文件效果一致:

#ifndef THIS_HEADER
#define THIS_HEADER
//一些头文件的定义
#endif

在C++11中,定义了与 #pragma 功能相同的操作符_Pragma

使用格式:_Pragma (字符串字面量)

其使用方法和 sizeof 一样,将字符串字面量作为参数写在括号里即可,
    即 _Pragma ("once");和 #pragma once效果一样。

但是注意 _Pragma 是 操作符,故可用在一些宏中,eg:
#define  CONCAT(x)  PRAGMA(concat on #x)
#define  PRAGMA(x)  _Pragma(#x)
CONCAT(..\concat.dir)

这里 CONCAT(..\concat.dir) 最终产生 _Pragma(concat on "..\concat.dir") 的效果
    (此处为了显示语法效果,应该无编译器支持这样的 _Pragma语法)

4. 变长参数的宏定义及__VA_ARGS__

在C99标准中可使用变长参数的宏定义。变长参数宏定义指宏定义中参数列表的最后一个参数为省略号,而预定义宏 __VA_ARGS__ 可在宏定义的实现部分替换省略号所代表的的字符串,eg:

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

其实,变长参数宏和printf是一对好搭档,eg:

#include <stdio.h>
#define LOG(...) {\
    //可据stderr产生的日志追溯到代码中产生这些记录的位置,对于轻量级调试有很大作用
    fprintf(stderr,"%s: Line %d: \t",__FILE__,__LINE__);\
    fprintf(stderr,__VA_ARGS__);\
    fprintf(stderr,"\n");\
}
int main(){
    int x=3;
    //一些代码
    LOG("x = %d",x);    //文件路径: Line 10:    x=3
}

5. 宽字符串的连接

将窄字符串和宽字符串进行连接时,
    在之前的C++标准中,将窄字符串( char)转换为宽字符串( wchar_t)是未定义的行为;
    而在C++11标准中,支持C++11标准的编译器会将窄字符串转换为宽字符串,
    然后再与宽字符串进行连接。
    事实上,C++11中定义了更多种类的字符串类型(主要为了更好地支持Unicode)(略)。

6. long long整型

long long类型先纳入了C99标准,而后才纳入C++11。 
    其有两种类型:long long 和 unsigned long long。
    
C++11标准要求long long整型可在不同平台上有不同的长度,但至少有64位;
    在写常数时,可以使用 LL(或ll)后缀标识一个long long类型的字面量,
    而ULL(或ull、Ull、uLL)标识一个unsigned long long类型字面量,eg:
    long long int lli=-90000000000000000LL;
    unsigned long long ulli=-900000000000000ULL;

C++11中有很多与long long等价的类型,eg,对于有符号的,下面的类型是等价的:
    long long、signed long long、long long int、signed long long;
    而unsigned long long和unsigned long long int等价。

与其他整型一样,要了解平台上long long大小的方法就是查看<climits>(或<limits.h>中的宏)。
    与 long long 整型相关的一共3个:LLONG_MIN、LLONG_MAX、ULLONG_MIN,
    分别代表平台上 long long的最小值和最大值以及unsigned long long 最大值。


#include <climits>
#include <cstdio>
int main(){
    long long ll_min= LLONG_MIN;
    long long ll_max= LLONG_MAX;
    unsigned long long ull_max= ULLONG_MAX;
    //min of long long: -9223372036854775808
    printf("min of long long: %lld\n",ll_min);  
    //max of long long: 9223372036854775807
    printf("max of long long: %lld\n",ll_max);  
    //max of unsigned long long: 1844674407309551615
    printf("max of unsigned long long: %llu",ull_max);  
}

7. 扩展的类型

在代码中一些整型的名字如UINT、__int16、int64_t等,这些类型有的源自编译器的自行扩展,
    有的来自某些编程环境(如工作在Linux内核的代码中),不一而足。

C++11中一共定义了5种标准的有符号整型:
    signed char、short int、int、long int、long long int。

标准同时规定,每种有符号整型都有一种对应的无符号整数版本,
    且有符号整型与其对应的无符号整数具有相同的存储空间大小。
    如signed int对应的无符号版本整型是unsigned int。
在实际编程中,由于5种基本的整型适用性有限,故有时编译器出于需要会自行扩展一些整型。

在C++11中,标准对这样的扩展做出了一些规定,具体来说:
除了标准整型外,C++11标准允许编译器扩展自由的所谓扩展整型;
    这些扩展整型的长度(占用内存的位数)可以比最长的标准整型
    (long long int,通常是一个64位长度的数据)还长,也可介于两个标准整型的位数之间。
    eg:在128位架构上,编译器看定义一个扩展整型来对应128位的整数,
    而在一些嵌入式平台上,可能需要扩展出48位的整型。
    
但C++11标准并没对扩展出的类型的名称有任何规定或建议,
    只是对扩展类型的使用规则作出了一定的限制:
1、C++11规定,扩展的整型须和标准类型一样,有符号类型和无符号类型占用相同大小内存空间;
2、由于C/C++是弱类型语言,当运算、传参等类型不匹配的时候,整型间会发生隐式的转换,
   这个过程称为整型的提升,eg表达式 (int)a +(long long)b
   通常就会导致(int)a被提升为 long long类型后才与(long long)b进行运算,

  而无论是扩展的整型还是标准的整型,其转化规则由它们的“等级”决定,
    通常情况,认为有如下规则:
    (1)长度越大的整型等级越高,如long long int等级高于 int;
    (2)长度相同时,标准类型的等级高于扩展类型,
       如long long int和_int64都是64位长度时,则long long int类型等级更高;
    (3)相同大小的有符号类型和无符号类型的等级相同,
       如long long int和unsigned long long int等级相同。

3、在进行隐式的整型转换时,一般按照低等级整型转换为高等级整型,
    有符号转换为无符号。这种规则和C++98整型转换规则一致。

在这样的规则支持下,若编译器定义一些自有整型,即使自定义的整型由于名称没被标准收入,
    因而可移植性并不能得到保证,
    但至少编译器开发者和程序员不用担心自定义的扩展整型和标准整型间
    在使用规则上(尤其是整型提升)存在不同的认识。
 eg:
    128位架构上,
    编译器可定义_int128_t为128位的有符号整型(对应无符号类型为_uint128_t),
    于是程序员可使用_int128_t类型保存形如+92233720368547758070的
    超长整数(长于64位的自然数);
    而不用查看编译器文档也知道,一旦整型提升,按照上述规则,如_int128_t a;
    与任何短于它的类型的数据b运算时,都会导致b被隐式转换为_int128_t整型,
    因为扩展的整型须遵守C++11规范。

8. 宏__cplusplus

在C与C++混合编程中,头文件中常有如下声明:
#ifdef  __cplusplus
extern "C" {
#endif
//一些代码
#ifdef  __cplusplus
}
#endif

这种类型头文件可被 #include 到C或C++文件中编译,
    由于extern "C"可抑制C++对函数名、变量名等符号进行名称重整(name mangling),
因此编译出的C目标文件和C++目标文件中的变量、函数名等符号都是相同的(否则不同),
    链接器可可靠地对 两种类型的目标文件进行链接;如此称为了C与C++混用头文件的典型用法。

事实上,__cplusplus这个宏被定义为一个整型值,而且随着标准变化,__cplusplus宏一般会是一个比以往标准中更大的值,如C++03中 __cplusplus被预定为199711L;而C++11中__cplusplus被定义为201103L,这点变化可为代码所用,eg程序员想确定代码是否使用支持C++11编译器进行编译时,可用如下检测:

#if  __cplusplus < 201103L
//使用#error预处理指令,使得不支持C++11的代码编译立即报错并终止编译
     #error  "should use C++11 implementation"
#endif

9. 静态断言

断言(assertion):通常情况下,断言是将一个返回值总是需要为真的判别式放在语句中,
    用于排除在设计的逻辑上不应有的情况。eg一个函数总需要输入在一定范围内的参数,
    那程序员可对该参数使用断言,以迫使程序在该参数发生异常时退出,
    从而避免程序陷入逻辑的混乱。

从某些意义上讲,断言并不是正常程序所须的,但对程序员而言,
    通常断言可助开发者快速定位哪些违反了某些前提条件的程序错误。

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

#include <cassert>
using namespace std;
//一个简单的堆内存数组分配函数
char *ArrayAlloc(int n){
    assert(n>0);    //断言,n必须大于0
    return new char[n];
}
int main(){
    char *a=ArrayAlloc(0);
}

在C++中,可定义 宏NDEBUG来禁用assert宏,这对发布程序来说是必要的。
因为程序用户对程序退出是敏感的,而部分的程序错误未必导致程序全部功能失效;故通过定义 NDEBUG宏发布程序可尽量避免程序退出的状况,而当程序有问题时,通过没有定义宏NDEBUG的版本,程序员可较容易地找到出问题的位置,事实上 assert宏在<cassert>中实现方式类似下列形式:

#ifdef  NDEBUG
#define  assert(expr)  (static_cast<void> (0))
#else
...
#endif

可看到,一旦定义了 NDEBUG宏,assert宏被展开为一条无意义的C语句(通常会被编译器优化掉)

在前面有使用 #error预处理指令,事实上,通过预处理指令#if 和 #error的配合,
    也可让开发者在预处理阶段进行断言,且此种用法很常见。
eg:GNU的 cmathcalls.h头文件中(Linux中位于/usr/include/bits/cmathcalls.h),
    有如下代码:
#ifndef _COMPLEX_H
#error "Never use <bits /cmathcalls.h> directly; include <complex.h> instead"
#endif

若直接包含头文件<bits/cmathcalls.h>并编译,就会引发错误,
#error指令将后面语句输出以提醒用户不要直接使用该头文件,而应包含<complex.h>。
    如此通过预处理时的断言,库发布者可避免一些头文件的引用问题。

静态断言与static_assert
由上述例子可知断言assert宏只在程序运行时起作用,
而#error在编译预处理时起作用

#include <iostream>
#include <cassert>
using namespace std;
//枚举编译器对各种特性的支持,每个枚举值占一位
enum TeatureSupports{
    C99 = 0x001,
    ExtInt=0x002,
    SAssert=0x004,
    NoExcept=0x008,
    SMAX=0x010
};
//一个编译器类型,包括名称、特性支持等
struct Compiler{
    const char *name;
    int spp;    //使用FeatureSupports枚举
};
int main(){
    //检查枚举值是否完备
    assert((SMAX-1)==(C99 | ExtInt | SAssert | NoExcept));
    Compiler a={"abc",(C99 | SAssert)};
    //...
    if(a.spp & C99){
        //some doce
        cout<<"some code"<<endl;
    }
}

上述代码是C中常见的"按位存储属性"的例子,
    该例中,枚举类型FeatureSupports用于列举编译器对各种特性的支持,
    结构体Compiler包含一个int的spp。
    各种特性有"支持"、"不支持"两种状态,为节省存储空间,
    让每个FeatureSupports枚举值占据一个比特位,
    使用时通过"或"运算压缩存储在Compiler的spp成员中(即bitset概念),
    使用时通过检查spp某位来判断编译器是否支持相应特性。

这样的枚举值会很多且在代码维护中不断增加,
    那程序员须想办法对这些枚举校验,如检验是否有重位等;
本例中做法是:使用一个"最大枚举"SMAX,
    并通过比较SMAX-1与其他枚举进行"或"运算来验证是否有枚举值重位,
   eg 当SAssert误定义为0x0001,
    表达式(SMAX-1)==(C99|ExtInt|SAssert|NoExcept)将不成立。
assert宏的校验在运行时才起作用,但校验有时最好在编译时期完成,如下代码:

#include <cassert>
#include <cstring>
using namespace std;

template <typename T,typename U>
int bit_copy(T &a,U &b){
    assert(sizeof(a)==sizeof(b));
    memcpy(&a,&b,sizeof(b));
}
int main(){
    int a=0x2468;
    double b;
    bit_copy(a,b);
}

上述代码中若bit_copy函数不被调用,则无法触发断言,
    即实际上产生断言应在模板实例化时即编译时期。
上述问题的解决方案是进行编译时期的断言即"静态断言",
    eg开源库Boost内置的BOOST_STATIC_ASSERT断言机制(用sizeof操作符),
    我们可用"除0"导致编译器报错实现静态断言。如下代码:

#include <cstring>
using namespace std;
#define assert_static(e) \
    do{\
        enum{assert_static__ =1/e};\
    }while(0)
template <typename T,typename U>
int bit_copy(T &a,U &b){
    assert(sizeof(a)==sizeof(b));
    memcpy(&a,&b,sizeof(b));
}
int main(){
    int a=0x2468;
    double b;
    bit_copy(a,b);
}
上述代码结果和预期一致,即模板实例化时得到编译器错误报告。
但无论哪种静态断言,
    缺陷是:诊断信息不足,不熟悉静态断言的程序员难以准确定位错误根源。

C++11标准中引入了 static_assert 断言解决这个问题,static_assert仅接受两个参数,
    一个返回bool值的断言表达式,一个是警告信息(通常是字符串)。
用static_assert替换一下上一段代码中bit_copy的声明:
template <typename T,typename U>
int bit_copy(T &a,U &b){
    static_assert(sizeof(a)==sizeof(b),\
      "the parameters of bit_copy must have same width");
    memcpy(&a,&b,sizeof(b));
}
再次编译时出现如下结果:
error: static assertion failed: 
    the parameters of bit_copy must have same width

C++中函数不能像static_assert一样独立于任何调用之外运行,故将其写在函数体外比较好,
但是static_assert 的断言表达式结果必须在编译时可得到结果,
即必须是常量表达式,否则会导致错误。

10. noexcept修饰符和noexcept操作符

1). 作为修饰符,修饰函数以说明函数是否会抛出异常

两种形式:
type   funcName() nocept;
type   funcName() nocept(expression);
noexcept默认值是true,表示函数不会抛出异常,expression是一个常量表达式,
    该常量表达式的结果会转化为一个bool值,true表不会抛出异常,反之表会抛出异常。

#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; //Found throw.
    }
    try{
        NoBlockThrow();
    }
    catch(...){
        cout<<"Throw is not blocked."<<endl;    //Throw is not blocked.
    }
    try{
        BlockThrow();//terminate called after throwing an instance of 'int'
    }
    catch(...){
        cout<<"Found throw 1."<<endl;
    }
}

执行上述代码,可知当声明了函数不抛出异常而事实上函数抛出了异常时,
该函数会调用std::terminal终止程序的运行。
使用效果与C++98中throw()效果一样(C++98中用throw()声明不抛出异常的函数)。

2)作为操作符

template <class T> void fun() noexcept(noexcept(T()));
上面代码里的第二个noexcept就是一个操作符;
    当T()是一个可能抛出异常的表达式时,noexcept操作符返回false。

通过std::terminal终止程序
    有时可能带来问题如析构函数不能正常调用以释放对象、栈空间的正常释放; 
为了安全考虑,C++11中让类的析构函数默认是noexcept(true)的,
    但程序员显示指定noexcept或类的基类或成员有noexcept(false)的析构函数,
    则析构函数不会保持默认值,eg下面代码:

#include <iostream>
using namespace std;
struct A{   ~A(){       throw 1;    }};
struct B{   ~B() noexcept(false){       throw 2;    }};
struct C{   B b;};
int funA(){ A a;}
int funB(){ B b;}
int funC(){ C c;}
int main(){
    try{
        funB();
    }catch(...){        cout<<"caught funB "<<endl; //caught funB
    }
    try{
        funC();
    }catch(...){        cout<<"caught funC "<<endl; //caught funC
    }
    try{
        funA(); //terminate called after throwing an instance of 'int'
    }catch(...){        cout<<"caught funA "<<endl;
    }
}

上述代码中无论析构函数为noexcept(false)的类B或包含了B的实例对象的类C,
    其析构函数都可抛出异常。

11. 快速初始化成员变量

"就地"声明:使用"="加初始值方式来初始化 类静态成员常量。
    C++98 要求该静态常量成员只能是枚举型或整型才能就地初始化。
    (非静态成员的初始化需在构造函数中进行)eg代码:
#include <iostream>
using namespace std;
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++98中编译结果如注释所述。
C++11中对非静态成员变量初始化除了初始化列表形式外,
    还允许使用"="或者"{}"进行就地的非静态成员变量初始化。

相关文章

网友评论

      本文标题:《深入理解C++11特性》

      本文链接:https://www.haomeiwen.com/subject/jdhrcqtx.html