《深入理解C++11》笔记(第二章. 保证稳定性和兼容性)

今天二刷《深入理解C++11》,就顺带把我在印象笔记的摘录传到CSND上,禁止转载!!!

全部笔记链接:

注:C++98/03称为旧标准,C++11称为新标准

保证稳定性和兼容性

统一初始化

  • 结构体用如下初始方式
  structA a={"abc", "def"};
  • 而类在旧标准不能使用如下初始化方式,但新标准可以支持
    classA a={"123", "456"};
  • 初始化还可以用来初始化动态分配的数组
    #include <iostream>
    using namespace std;
    int main()
    {
        int *ar=new int[5]{1,2,3,4,5};
        cout<<"ar:";
        for(int i=0;i<5;i++)
            cout<<ar[i]<<" ";
        cout<<endl;
  • 始化还可以在构造函数初始化器中初始化类成员数组
#include <iostream>
using namespace sd;
class A
{
public:
    A():arry{1,2,3,4}{
    }
    void showArry()const
    {
        for(int i=0; i<4; ++i)
            cout<<arry[i]<<endl;
    }
private:
    int arry[4];
};
int main()
{
    classA m;
    m.showArry();
    return 0;
}

预定义宏

#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 Stardard version: " << __STDC_VERSION__ << endl;
        cout << "ISO/IEC " << __STDC_ISO_10646__ << endl;                // ISO/IEC 200009
}
// 编译选项:g++ -std=c++11 2-1-1.cpp

func

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

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

_Pragma操作符

在C/C++标准中,#pragma是一条预处理的指令(preprocessor directive)。

#pragma once 

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

_Pragma("once");  

#pragma则不能在宏中展开,因此从灵活性上来讲,C++11的_Pragma具有更大的灵活性。

变长参数的宏定义以及 VA_ARGS

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

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

宽窄字符串的连接

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

long long整型

在C++11中,标准要求long long整型可以在不同平台上有不同的长度,但至少有64位。我们在写常数字面量时,可以使用LL后缀(或是ll)标识一个long long类型的字面量,而ULL(或ull、Ull、uLL)表示一个unsigned long long类型的字面量。比如:

    long long int lli = -9000000000000000000LL;
    unsigned long long int ulli = -9000000000000000000ULL;

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

扩展的整型

而无论是扩展的整型还是标准的整型,其转化的规则会由它们的“等级”(rank)决定。而通常情况,我们认为有如下原则:

  • 长度越大的整型等级越高,比如long long int的等级会高于int。
  • 长度相同的情况下,标准整型的等级高于扩展类型,比如long long int和_int64如果都是64位长度,则long long int类型的等级更高。
  • 相同大小的有符号类型和无符号类型的等级相同,long long int和unsigned long long int的等级就相同。

而在进行隐式的整型转换的时候,一般是按照低等级整型转换为高等级整型,有符号的转换为无符号。这种规则其实跟C++98的整型转换规则是一致的。

宏__cplusplus

#ifdef __cplusplus
extern "C" {
#endif
 // 一些代码       
#ifdef __cplusplus
}
#endif

程序员可能认为__cplusplus这个宏只有“被定义了”和“未定义”两种状态。事实上却并非如此,__cplusplus这个宏通常被定义为一个整型值。而且随着标准变化,__cplusplus宏一般会是一个比以往标准中更大的值。比如在C++03标准中,__cplusplus的值被预定为199711L,而在C++11标准中,宏__cplusplus被预定义为201103L。
这点变化可以为代码所用。比如程序员在想确定代码是使用支持C++11编译器进行编译时,那么可以按下面的方法进行检测:

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

这里,使用了预处理指令#error,这使得不支持C++11的代码编译立即报错并终止编译。

静态断言

 assert宏在中的实现方式类似于下列形式, 在C++中,程序员也可以定义宏NDEBUG来禁用assert宏。

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

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

静态断言与static_assert

 断言assert宏只有在程序运行时才能起作用。而#error只在编译器预处理时才能起作用。有的时候,我们希望在编译时能做一些断言。

static_assert(sizeof(int) == 8, "This 64-bit machine should follow this!");
int main()
{ 
    return 0; 
}

 将static_assert写在函数体外通常是较好的选择,这让代码阅读者可以较容易发现static_assert为断言而非用户定义的函数。而反过来讲,必须注意的是,static_assert的断言表达式的结果必须是在编译时期可以计算的表达式,即必须是常量表达式。如果读者使用了变量,则会导致错误;
 如果程序员需要的只是运行时的检查,那么还是应该使用assert宏。

noexcept修饰符与noexcept操作符

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

  • 从语法上讲,noexcept修饰符有两种形式
    一种就是简单地在函数声明后加上noexcept关键字。比如:
    void excpt_func() noexcept;
    另外一种则可以接受一个常量表达式作为参数,如下所示:
    void excpt_func() noexcept (常量表达式);
     常量表达式的结果会被转换成一个bool类型的值,该值为true,表示函数不会抛出异常,反之,则有可能抛出异常。这里,不带常量表达式的noexcept相当于声明了noexcept(true),即不会抛出异常。

  • 在C++11中使用noexcept可以有效地阻止异常的传播与扩散。

  • noexcept作为一个操作符时,通常可以用于模板。

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

如果其参数,即T()是一个有可能抛出异常的表达式,则noexpect(T())返回值为false(func为noexcept(false),有可能会抛出异常),否则返回值为true(func为noexcept(true),不会抛出异常)。

  • noexcept更大的作用是保证应用程序的安全。比如一个类析构函数不应该抛出异常,那么对于常被析构函数调用的delete函数来说,C++11默认将delete函数设置成noexcept,就可以提高应用程序的安全性。
    void operator delete(void*) noexcept;
    void operator delete noexcept;
    而同样出于安全考虑,C++11标准中让类的析构函数默认也是noexcept(true)的,但是,如果程序员显式地为析构函数指定了noexcept(false) 或者类的基类或者类的成员变量有 noexcept(false) 的析构函数,析构函数就不会再保持默认值。。
#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;
    }
}

eg1:
C++98中,存在着使用throw()来声明不抛出异常的函数。
template class A {
public:
static constexpr T min() throw() { return T(); }
static constexpr T max() throw() { return T(); }
static constexpr T lowest() throw() { return T(); }

而在C++11中,则使用noexcept来替换throw()。
template class A {
public:
static constexpr T min() noexcept { return T(); }
static constexpr T max() noexcept { return T(); }
static constexpr T lowest() noexcept { return T(); }

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

快速初始化成员变量

非静态成员的sizeof

  • C++98标准中,对非静态成员变量使用sizeof是不能够通过编译的
#include <iostream>
using namespace std;
struct People {
public:
    int hand;
    static People * all;
};
int main() {
    People p;
    cout << sizeof(p.hand) << endl;           // C++98 中通过, C++11 中通过
    cout << sizeof(People::all) << endl;      // C++98 中通过, C++11 中通过
    cout << sizeof(People::hand) << endl;     // C++98 中错误, C++11 中通过
}
// 编译选项:g++ *.cpp

在C++11中,对非静态成员变量使用sizeof操作是合法的。而在C++98中,只有静态成员,或者对象的实例才能对其成员进行sizeof操作。TODO

  • 如果读者只有一个支持C++98标准的编译器,在没有定义类实例的时候,要获得类成员的大小,我们通常会采用以下的代码:

      sizeof(((People*)0)->hand);
    

    这里我们强制转换0为一个People类的指针,继而通过指针的解引用获得其成员变量,并用sizeof求得该成员变量的大小。

扩展的friend语法

  • friend关键字用于声明类的友元,友元可以无视类中成员的属性。无论成员是public、protected或是private的,友元类或友元函数都可以访问,这就完全破坏了面向对象编程中封装性的概念。
  • 在C++11中,声明一个类为另外一个类的友元时,不再需要使用class关键字。虽然在C++11中这是一个小的改进,却会带来一点应用的变化—程序员可以为类模板声明友元了。
  • eg:
template <typename T> class DefenderT {
public:
    friend T;
    void Defence(int x, int y){}
    void Tackle(int x, int y){}
private:
    int pos_x = 15;
    int pos_y = 0;
    int speed = 2;
    int stamina = 120;
};

template <typename T> class AttackerT {
public:
    friend T;
    void Move(int x, int y){}
    void SpeedUp(float ratio){}
private:
    int pos_x = 0;
    int pos_y = -30;
    int speed = 3;
    int stamina = 100;
};
using Defender = DefenderT<int>;     // 普通的类定义,使用int做参数
using Attacker = AttackerT<int>;

#ifdef UNIT_TEST
class Validator {
public:
    void Validate(int x, int y, DefenderTest & d){}
    void Validate(int x, int y, AttackerTest & a){}
};
using DefenderTest = DefenderT<Validator>;  // 测试专用的定义,Validator类成为友元
using AttackerTest = AttackerT<Validator>;
int main() {
    DefenderTest d;
    AttackerTest a;
    a.Move(15, 30);
    d.Defence(15, 30);
    a.SpeedUp(1.5f);
    d.Defence(15, 30);
    Validator v;
    v.Validate(7, 0, d);
    v.Validate(1, -10, a);
    return 0;
}
#endif
// 编译选项:g++ 2-9-4.cpp -std=c++11-DUNIT_TEST

using Defender = DefenderT;
使用内置类型int作为模板参数的时候,DefenderT会被实例化为一个普通的没有友元定义的类型。这样一来,我们就可以在模板实例化时才确定一个模板类是否有友元,以及谁是这个模板类的友元。这是一个非常有趣的小特性,在编写一些测试用例的时候,使用该特性是很有好处的。

final/override控制

  • final关键字的作用
    1. 禁止继承
    2. 禁止重写
  • 基类中的虚函数是否可以使用final关键字呢?
    答案是肯定的,不过这样将使用该虚函数无法被覆盖,也就失去了虚函数的意义。如果不想成员函数被覆盖,程序员可以直接将该成员函数定义为非虚的。而final通常只在继承关系的“中途”终止派生类的覆盖中有意义。从接口派生的角度而言,final可以在派生过程中任意地阻止一个接口的可覆盖性,这就给面向对象的程序员带来了更大的控制力。
  • 在C++中重载还有一个特点,就是对于基类声明为virtual的函数,之后的覆盖版本都不需要再声明该覆盖函数为virtual。为解决由此带来的不清晰问题,引入override:

    如果派生类在虚函数声明时使用了override描述符,那么该函数必须重载其基类中的同名函数,否则代码将无法通过编译。

    struct Base {
            virtual void Turing() = 0;
            virtual void Dijkstra() = 0;
            virtual void VNeumann(int g) = 0;
            virtual void DKnuth() const;
            void Print();
        };
    struct DerivedMid: public Base {
        // void VNeumann(double g);
        // 接口被隔离了,曾想多一个版本的VNeumann函数
    };
    struct DerivedTop : public DerivedMid {
        void Turing() override;
        void Dikjstra() override;              // 无法通过编译,拼写错误,并覆盖
        void VNeumann(double g) override;      // 无法通过编译,参数不一致,并非覆盖
        void DKnuth() override;                // 无法通过编译,常量性不一致,并非覆盖
        void Print() override;                 // 无法通过编译,非虚函数覆盖
    };
    // 编译选项:g++ -c -std=c++11 2-10-3.cpp

如果没有override修饰符,DerivedTop的作者可能在编译后都没有意识到自己犯了这么多错误。因为编译器对以上3种错误不会有任何的警示。这里override修饰符则可以保证编译器辅助地做一些检查。

模版函数的默认模版参数

  • template
    void DefTempParm() {}; // c++98编译失败,c++11编译通过
  • 与类模板有些不同的是,在为多个默认模板参数声明指定默认值的时候,程序员必须遵照“从右往左”的规则进行指定。而这个条件对函数模板来说并不是必须的
  • 函数模板的参数推导规则也并不复杂。简单地讲:如果能够从函数实参中推导出类型的话,那么默认模板参数就不会被使用,反之,默认模板参数则可能会被使用。
template <class T, class U = double>
void f(T t = 0, U u = 0) {};
void g()
{
    f(1, 'c'); // f<int, char>(1, 'c')
    f(1);      // f<int, double>(1, 0), 使用了默认模板参数double
    f();       // 错误: T无法被推导出来
    f<int>();  // f<int, double>(0, 0), 使用了默认模板参数double
    f<int, char>(); // f<int, char>(0, 0)
}

定义了一个函数模板f,f同时使用了默认模板参数和默认函数参数。可以看到,由于函数的模板参数可以由函数的实参推导而出:在f(1)这个函数调用中,实例化出了模板函数的调用应该为f<int, double>(1, 0),其中,第二个类型参数U使用了默认的模板类型参数double,而函数实参则为默认值0。类似地,f()实例化出的模板函数第二参数类型为double,值为0。而表达式f()由于第一类型参数T的无法推导,从而导致了编译的失败。
而通过这个例子也可以看到,默认模板参数通常是需要跟默认函数参数一起使用的。还有一点应该注意:模板函数的默认形参值不是模板参数推导的依据。函数模板参数的选择,终究是由函数的实参推导而来的。

外部模板

extern template void fun(int); // 外部模板的声明

  • 类似外部变量声明,不过相比于外部变量声明,不使用外部模板声明并不会导致任何问题。算作一种优化手段,因为可能会低估模版实例化展开的开销,大量的模板使用会在代码中产生大量的冗余。这种冗余,有的时候已经使得编译器和链接器力不从心。只有在项目比较大的情况下。我们才建议用户进行这样的优化。总的来说,就是在既不忽视模板实例化产生的编译及链接开销的同时,也不要过分担心模板展开的开销。

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

局部的类型和匿名的类型在C++98中都不能做模板类的实参,但是C++11可以。

template <typename T> class X {};
template <typename T> void TempFun(T t){};
struct A{} a;
struct {int i;}b;             // b是匿名类型变量
typedef struct {int i;}B;    // B是匿名类型
void Fun()
{
    struct C {} c;            // C是局部类型
    X<A> x1;     // C++98通过,C++11通过
    X<B> x2;     // C++98错误,C++11通过
    X<C> x3;     // C++98错误,C++11通过
    TempFun(a); // C++98通过,C++11通过
    TempFun(b); // C++98错误,C++11通过
    TempFun(c); // C++98错误,C++11通过
}
// 编译选项:g++ -std=c++11 2-13-1.cpp

除了匿名的结构体之外,匿名的联合体以及枚举类型,在C++98标准下也都是无法做模板的实参的。

注意:

template <typename T> struct MyTemplate { };
int main() {
    MyTemplate<struct { int a; }> t; // 无法编译通过, 匿名类型的声明不能在模板实参位置
    return 0;
}
// 编译选项:g++ -std=c++11 2-13-2.cpp
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值