《深入理解C++11》笔记(第三章. 通用为本,专用为末)

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

全部笔记链接:

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

通用为本,专用为末

1. 继承构造函数

  • 如果派生类要使用基类的成员函数的话,可以通过using声明(using-declaration)来完成。
    #include <iostream>
    using namespace std;
    struct Base {
        void f(double i){ cout << "Base:" << i << endl; }
    };
    struct Derived : Base {
        using Base::f;
        void f(int i) { cout << "Derived:" << i << endl; }
    };
    int main() {
      Base b;
      b.f(4.5);   // Base:4.5
      Derived d;
      d.f(4.5);   // Base:4.5
    }
    // 编译选项:g++ 3-1-3.cpp
  • 在C++11中,上面这种想法被扩展到了构造函数上。子类可以通过使用using声明来声明继承基类的构造函数。
    struct A { A(int) {} };
    struct B { B(int) {} };
    struct C: A, B {
        using A::A;
        using B::B;
    };

A和B的构造函数会导致C中重复定义相同类型的继承构造函数。这种情况下,可以通过显式定义继承类的冲突的构造函数,阻止隐式生成相应的继承构造函数来解决冲突。比如:

    struct C: A, B {
        using A::A;
        using B::B;
        C(int){}
    };
  • 不过继承构造函数只会初始化基类中成员变量,对于派生类中的成员变量,则无能为力。不过配合类成员的初始化表达式,为派生类成员变量设定一个默认值还是没有问题的。
  • 有的时候,基类构造函数的参数会有默认值。对于继承构造函数来讲,参数的默认值是不会被继承的。事实上,默认值会导致基类产生多个构造函数的版本,这些函数版本都会被派生类继承。参数默认值会导致多个构造函数版本的产生,因此程序员在使用有参数默认值的构造函数的基类的时候,必须小心。

2. 委派构造函数

与继承构造函数类似的,委派构造函数也是C++11中对C++的构造函数的一项改进,其目的也是为了减少程序员书写构造函数的时间。通过委派其他构造函数,多构造函数的类编写将更加容易。

class Info {
public:
    Info() : Info(1, 'a') { }
    Info(int i) : Info(i, 'a') { }
    Info(char e): Info(1, e) { }
private:
    Info(int i, char e): type(i), name(e) { /* 其他初始化 */ }
    int   type;
    char name;
    // ...
};
// 编译选项:g++ -c -std=c++11 3-2-4.cpp
  • 由于在C++11中,目标构造函数的执行总是先于委派构造函数而造成的。因此避免目标构造函数和委托构造函数体中初始化同样的成员通常是必要的。
  • 委派构造的一个很实际的应用就是使用构造模板函数产生目标构造函数。eg:
#include <list>
#include <vector>
#include <deque>
using namespace std;
class TDConstructed {
    template<class T> TDConstructed(T first, T last) :
        l(first, last) {}
    list<int> l;
public:
    TDConstructed(vector<short> & v):
        TDConstructed(v.begin(), v.end()) {}
    TDConstructed(deque<int> & d):
          TDConstructed(d.begin(), d.end()) {}
  };
  // 编译选项:g++ -c -std=c++11 3-2-6.cpp

可以说,委托构造使得构造函数的泛型编程也成为了一种可能。

  • 在异常处理方面,如果在委派构造函数中使用try的话,那么从目标构造函数中产生的异常,都可以在委派构造函数中被捕捉到。eg:
#include <iostream>
using namespace std;
class DCExcept {
public:
    DCExcept(double d)
        try : DCExcept(1, d) {
            cout << "Run the body." << endl;
            // 其他初始化
        }
        catch(...) {
            cout << "caught exception." << endl;
        }
private:
    DCExcept(int i, double d){
        cout << "going to throw!" << endl;
        throw 0;
    }
    int type;
    double data;
};
int main() {
    DCExcept a(1.2);
}
// 编译选项:g++ -std=c++11 3-2-7.cpp

3. 右值引用:移动语义和完美转发

3.2 移动语义
  • std::move基本等同于一个类型转换:
    static_cast<T&&>(lvalue);
3.3 完美转发
  • 引用折叠
    如果间接的创建一个引用的引用,则这些引用就会“折叠”。在所有情况下(除了一个例外),引用折叠成一个普通的左值引用类型。一种特殊情况下,引用会折叠成右值引用,即右值引用的右值引用, T&& &&。
    • X& &、X& &&、X&& &都折叠成X&
    • X&& &&折叠为X&&

4 显示转换操作符

  • 类型转换函数
operator 类型名( ) 
{ 
    实现转换的语句 
    return ...
}

注意事项:

  1. 函数体中有return语句,返回值的类型是由类型名来确定的;
  2. 没有返回类型,没有参数列表;
  3. 只能作为成员函数;
  4. 从函数形式可以看到,它与运算符重载函数相似,都是用关键字operator开头,只是被重载的是类型名。
  • 显示类型转换操作符
    在C++11中,标准将explicit的使用范围扩展到了自定义的类型转换操作符上,以支持所谓的“显式类型转换”。explicit关键字作用于类型转换操作符上,意味着只有在直接构造目标类型(也即拷贝构造初始化是不行的)或显式类型转换的时候可以使用该类型
class ConvertTo {};
class Convertable {
public:
    explicit operator ConvertTo () const { return ConvertTo(); }
};
void Func(ConvertTo ct) {}
void test() {
    Convertable c;
    ConvertTo ct(c);          // 直接初始化,通过
    ConvertTo ct2 = c;       // 拷贝构造初始化,编译失败
    ConvertTo ct3 = static_cast<ConvertTo>(c);  // 强制转化,通过
    Func(c);                   // 拷贝构造初始化,编译失败
}
// 编译选项: g++ -std=c++11 3-4-3.cpp

所谓显式类型转换并没完全禁止从源类型到目标类型的转换,不过由于此时拷贝构造非显式类型转换不被允许,那么我们通常就不能通过赋值表达式或者函数参数的方式来产生这样一个目标类型。

5 列表初始化

  • 初始化列表

  • 防止类型收窄
    在C++11中,使用初始化列表进行初始化的数据编译器是会检查其是否发生类型收窄的;
    且列表初始化是C++11中唯一一种可以防止类型收窄的初始化方式。

6 POD类型

  • template <typename T>
    struct std::is_trivial;
    类模板is_trivial的成员value可以用于判断T的类型是否是一个平凡的类型

7 非受限联合体

8 用户自定义字面量

实例
#include <cstdlib>
#include <iostream>
using namespace std;
typedef unsigned char uint8;
struct RGBA{
    uint8 r;
    uint8 g;
    uint8 b;
    uint8 a;
    RGBA(uint8 R, uint8 G, uint8 B, uint8 A = 0):
        r(R), g(G), b(B), a(A){}
};
RGBA operator "" _C(const char* col, size_t n) {     // 一个长度为n的字符串col
    const char* p = col;
    const char* end = col + n;
    const char* r, *g, *b, *a;
    r = g = b = a = nullptr;
    for(; p != end; ++p) {
        if (*p == 'r') r = p;
        else if (*p == 'g') g = p;
        else if (*p == 'b') b = p;
        else if (*p == 'a') a = p;
      }
      if ((r == nullptr) || (g == nullptr) || (b == nullptr))
          throw;
      else if (a == nullptr)
          return RGBA(atoi(r+1), atoi(g+1), atoi(b+1));
      else
          return RGBA(atoi(r+1), atoi(g+1), atoi(b+1), atoi(b+1));
  }
  std::ostream & operator << (std::ostream & out, RGBA & col) {
      return out << "r: " << (int)col.r
          << ", g: " << (int)col.g
          << ", b: " << (int)col.b
          << ", a: " << (int)col.a << endl;
  }
  void blend(RGBA && col1, RGBA && col2) {
      // Some color blending action
      cout << "blend " << endl << col1 << col2 << endl;
  }
  int main() {
      blend("r255 g240 b155"_C, "r15 g255 b10 a7"_C);
  }
  // 编译选项:g++ -std=c++11 3-8-2.cpp
规则
  • 如果字面量是整形数,那么字面量操作符函数只能接受unsigned long long以及const char* 类型的参数。当字面量超出unsigned long long长度限制时,编译器会自动将该字面量转化为以\0为结束符的字符串,并调用以const char* 为参数的版本来处理。

    例如:out(222222222222222222222222222222_Ex);由于没有实现const char*类型的字面量操作符,所以译失败。

  • 如果字面量为浮点数,那么字面量操作符只能接受long double以及const char* 类型的参数。超出长度的规则和上面一样。
  • 如果字面量为字符串,则只能接受const char*, size_t类型的参数(已知长度字符串)。
  • 如果字面量为字符,则只能接受一个char的参数。
注意
  • 定义字面量操作符时,operator"“和后缀需要有空格(实测不需要也行,operator与”"之间有无空格都可以)。
  • 后缀建议以下划线开始。不宜使用非下划线后缀的用户自定义字符串常量,否则会被编译器告警。如201203L这样的字面量,后缀"L"无疑会引起一些混乱的状况。

9 内联名字空间

实例
#include <iostream>
using namespace std;
namespace Jim {
#if __cplusplus == 201103L
    inline
#endif
    namespace cpp11{
            struct Knife{ Knife() { cout << "Knife in c++11." << endl; } };
            // ...
    }
    #if __cplusplus < 201103L
        inline
    #endif
        namespace oldcpp{
            struct Knife{ Knife() { cout << "Knife in old c++." << endl; } };
            // ...
        }
    }
    using namespace Jim;
    int main() {
        Knife a;              // Knife in c++11. (默认版本)
        cpp11::Knife b;      // Knife in c++11. (强制使用cpp11版本)
        oldcpp::Knife c;     // Knife in old c++. (强制使用oldcpp11版本)
    }
    // 编译选项:g++ -std=c++11 3-9-4.cpp

这对程序库的发布很有好处,因为需要长期维护的程序库,可能版本间的接口和实现等都随着程序库的发展而发生了变化。那么根据需要将合适的名字空间导入到父名字空间中,无疑会方便库的使用。

注意
  1. C++98标准不允许在不同的名字空间中对模板进行特化。
  2. C++11引入的“内联的名字空间”允许程序员在父名称空间定义或特化子名字空间的模版。
  3. 匿名名字空间无法允许在父名字空间的模板特化。这也是C++11中为什么要引入新的内联名字空间的一个根本原因。
  4. 名字空间的内联会破坏该名字空间本身具有的封装性,所以程序员不应该在需要隔离名字的时候使用inline namespace关键字。
ADL

“参数关联名称查找”,即ADL(Argument-Dependent name Lookup)。

namespace ns_adl{
struct A{};
void ADLFunc(A a){} // ADLFunc定义在namespace ns_adl中
}
int main() {
ns_adl::A a;
ADLFunc(a); // ADLFunc无需声明名字空间
}

ADL带来了一些使用上的便利性,不过也在一定程度上破坏了namespace的封装性。很多人认为使用ADL会带来极大的负面影响

10 模板的别名

在C++11中,using关键字的能力已经包括了typedef的部分。

在使用模板编程的时候,using的语法甚至比typedef更加灵活。比如下面这个例子:

template<typename T> using MapString = std::map<T, char*>;
MapString<int> numberedString;

在这里,我们“模板式”地使用了using关键字,将std::map<T, char*>定义为了一个MapString类型,之后我们还可以使用类型参数对MapString进行类型的实例化,而使用typedef将无法达到这样的效果。

11 一般的SFINEA规则

在C++模板中,有一条著名的规则,即SFINEA - Substitution failure is not an error,中文直译即是“匹配失败不是错误”。更为确切地说,这条规则表示的是对重载的模板的参数进行展开的时候,如果展开导致了一些类型不匹配,编译器并不会报错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值