X

曜彤.手记

随记,关于互联网技术、产品与创业

吉 ICP 备10004938号

“深入理解 C++11:C++11 新特性解析与应用” 读书笔记(一)


作为一本之前已经读过两遍的书,终于决定在第三次“复习”的时候做下读书笔记了。鉴于之前已经完整读过《Primer C++ 5th》、《Effective C++ 3th》两本书,因此本文仅作为查缺补漏之用,对于前两本书中没有提到一些诸如“最小垃圾回收”、以及“原子类型与原子操作”等内容进行回顾与记录。整个 C++ 系列还有一本想完整仔细阅读的《Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14》可能会稍微往后放了,由于 C++14 仅作为 C++11 的微小改进和补充,对于一些常用的特性其实已经在实际项目中开始使用了。而对于诸如 std::future 以及 std::promise 等特性,由于其涉及异步和并发相关场景,因此可以参考《C++ Concurrency in Action 2th》一书,暂时没有实际需求便先不打算进行了解。

  1. (Page:20)C++98/03 标准:C++03 标准产生于2003年 WG21 提交的 TC1 技术勘误表,对语言核心内容没有改动。因此常被合在一起称为 C++98/03 标准。
  2. (Page:21)WG21:C++ 语言标准委员会;WG14:C 语言标准委员会。其中 WG21 更倾向于使用库而不是扩展语言来实现新的 C++ 特性
  3. (Page:31)final 与 override 为标识符,并非关键字,因此可以被当做变量名进行重定义。
  4. (Page:33)const 与 constexpr 区别:const 不一定保证编译时的常量性,只保证运行时变量无法变更;而 constexpr 则可以保证编译时的常量性。
  5. (Page:39)一些用于检查机器对 C 标准以及 C 库支持情况的预定义宏,使用前先检查是否被定义(#ifdef):
int main(int argc, char** argv) {
  std::cout << __STDC_HOSTED__ << std::endl;  // 是否包含完整的标准 C 库;
  std::cout << __STDC__ << std::endl;  // 实现是否与 C 标准一致;
  std::cout << __STDC_VERSION__ << std::endl; // 支持的 C 标准版本;
  std::cout << __STDC_ISO_10646__ << std::endl; // 表示 C++ 编译环境符合某个版本的 IOS/IEC 10646 标准(通用字符集)版本;
  return 0;
}
  1. (Page:40)在 C++11 中,宏 __func__ 可返回所在函数/类的名字,可用于轻量级的代码调试。实现上编译器会自动隐式地在函数定义之后定义 func 标识符。
  2. (Page:41)在 C++11 中,宏 _Pragma 的用法类似 #pragma,用于向编译器传达语言标准以外的信息。但作为操作符,其可以用于宏定义并进行宏展开。
// 示例来源于 OpenMP 应用(一套支持跨平台共享内存方式的多线程并发的编程API);
#define Pragma(x) _Pragma(#x)
#define OMP(directive) Pragma(omp directive)

int main(int argc, char** argv) {
  omp_set_dynamic(0);
  omp_set_num_threads(2);
  OMP(parallel) {  // 其内部的语句将被多个线程并行执行;
    printf("Hello!\n");
  }
}
  1. (Page:42)变长参数宏与 std::fprintf:
#include <cstdio>
#define LOG(...) { \
  fprintf(stderr, "%s: Line %d:\t", __FILE__, __LINE__); \
  fprintf(stderr, __VA_ARGS__); \
  fprintf(stderr, "\n"); \
}
int main() {
  int x = 3;
  LOG("x = %d", x);  // "x.cpp: Line 12: x = 3"
}
  1. (Page:43)C++ 标准规定 long long 至少有64位长度。
  2. (Page:44)强类型的语言遇到函数引数类型和实际调用类型不符合的情况经常会直接出错或者编译失败;而弱类型的语言常常会实行隐式转换,或者产生难以意料的结果。所以从这方面来看,C/C++ 是一种弱类型语言
  3. (Page:45)数据类型的 rank 相同时,一般按照低等级整型转换为高等级整型,有符号的转换为无符号
  4. (Page:46)使用 __cplusplus 宏判断编译使用的 C++ 版本:
#if __cplusplus < 201103L
  #error "should use C++11 implementaton."
#endif
  1. (Page:51)如上一条所示,#error 宏为预处理时的”断言“;而 static_assert() 为编译时静态断言,可达到率百分之百。其中使用的表达式必须为常量表达式;assert() 为动态运行时断言,只能断言到被运行到的代码块。
  2. (Page:59)在 C++11 中,可以使用 sizeof 对类成员表达式(非类实例成员)进行操作。
  3. (Page:61)形如 friend int; 在内置类型上的友元声明一般会被编译器忽略,因此这对于模板友元是一个方便的地方。
  4. (Page:65)final 和 override 关键字:
struct A {
  virtual void foo() {
    std::cout << "A" << std::endl;
  }
};
struct B : public A {
  void foo() final override {
    std::cout << "B" << std::endl;
  }
};
int main(int argc, char** argv) {
  A a, *fa = &a;
  B b, *fb = &b;
  fb->foo();  // 动态调用,若派生类没实现,则调用基类的同名同参虚函数,使用 final 可以防止虚函数被派生类复写;
  return 0;
}
  1. (Page:67)override 关键字可以帮助开发者确认被标记的函数正确地重载了其父类中的虚函数,而非想要新添加成员函数。这在多继承或派生类继承链较深的情况下十分有帮助。
  2. (Page:68)模板参数对于非引用类型,在推导时会丢失 top-level 常量性。若为引用类型,当模板参数为 “&&” 右值引用时可以保持引用的左值性和常量性。其中对于 T,仅能推导出原类型或者对应的左值引用类型。
template<typename T>
void ftVal(T t) {
  std::cout << typeid(T).name() << std::endl;
}
template<typename T>
void ftRef(T&& t) {
  std::cout << typeid(T).name() << std::endl;
}
int main(int argc, char** argv) {
  const int vInt = 10;
  const int&& vIntRR = 100;
  const char* const vChar = "Hello, world!";
  auto& vIntRef = vInt;
  ftVal(vInt);  // "i";
  ftVal(vChar);  // "PKc";
  ftRef(vIntRR); // T = const int&;
  ftRef(100); // T = int;
  ftRef('c'); // T = char;
  ftRef(vInt); // T = const int&;
  ftRef(vChar); // T = const char* const&;
  ftRef(vIntRef); // T = const int&;
  return 0;
}
  1. (Page:72)外部(extern)模板声明可以放置在头文件中以便于使用。
  2. (Page:73)一般来说,外部模板声明可用于优化编译及链接时间,建议仅在项目比较大的情况下再使用。对于大部分正常的模板实例化,编译器已经会进行一定程度的冗余实例优化。
  3. (Page:74)接受匿名和局部类型的模板:
template<typename T>
void foo(T t) {}
struct {} so;
int main(int argc, char** argv) {
  struct {} si;
  foo(si);
  foo(so);
  return 0;
}
  1. (Page:78)根据 C++ 名字查找规则,派生类中的同名成员函数会覆盖基类中的函数,而不能跨作用域进行重载。但经过 using 改变作用域可见性后,在派生类中便可以与基类中同名成员函数组成重载关系。
struct A {
  void foo() { std::cout << 'A' << std::endl; }
};
struct B : public A {
  using A::foo;
  void foo(int x) { std::cout << 'B' << x << std::endl; }
};
int main(int argc, char** argv) {
  B b;
  b.foo();
  b.foo(100);
  return 0;
}
  1. (Page:80)在派生类中发生冲突的多继承类构造函数,需要被单独定义。
  2. (Page:81)继承的构造函数不受 using 可见性的影响。
struct A {
  int v;
  A(int v) : v(v) {}
};
class B : public A {
  using A::A;
};
int main(int argc, char** argv) {
  B b(100);
  return 0;
}
  1. (Page:83)委派构造函数(调用其他构造函数进行初始化)不能有初始值列表,即构造函数不能同时“委派”和使用初值列表。对于剩余的初始化操作,只能在构造函数的函数体中进行。
struct A {
  int x;
  A() {}
  // A(int x) : A(), x(x) {}  // wrong!
};
  1. (Page:84)由于目标构造函数的执行总是优先于委派构造函数,因此避免目标构造函数和委托构造函数中初始化相同的成员通常是必要的。
  2. (Page:85)基于委派构造,使用构造模板函数产生目标的泛型构造函数:
struct A {
  std::list<int> lst;
  template<typename T>
  A(T start, T end) : lst(start, end) {}
};
int main(int argc, char** argv) {
  std::vector<int> v = {1, 2, 3};
  A a(v.begin(), v.end());
  for (auto e : a.lst) { std::cout << e << std::endl;    }
  return 0;
}
  1. (Page:94)将亡值:std::move 的返回值、类型为 T&& 将要被移动的对象(被右值引用类型变量标记的右值);纯右值:返回的临时变量值、字面量值、lambda 表达式、运算表达式、类型转换函数的返回值。
  2. (Page:95)左值引用是具名变量值的别名,右值引用是不具名(匿名)变量的别名。
  3. (Page:101)移动构造的常用方式:
struct A {
  A() = default;
  A(int v) : p(new int(v)) {}
  ~A() { delete p; }
  A(const A&) = delete;
  A& operator=(const A&) = delete;
  A(A&& rhs) noexcept : p(rhs.p) { rhs.p = nullptr; }
  A& operator=(A&& rhs) noexcept { p = rhs.p; rhs.p = nullptr; return *this; }
  int getPV() const { return *p; }
 private:
  int* p;
};
A getTempA(int v) { return A(v); }
int main(int argc, char** argv) {
  auto a = getTempA(100);  // 自 C++17 起,某些 RVO/NRVO 类似的临时值消除过程会由编译器强制执行,而不受 -fno-elide-constructors 参数的影响;
  std::cout << a.getPV() << std::endl;
  return 0;
}
  1. (Page:103)标准库 STL 中的一些容器类型只会使用被标记为不会抛出异常的移动构造和移动赋值函数。std::move_if_noexcept()
  2. (Page:105)使用 std::forward 保持模板传递时的参数类型:
void bar(int& v) { std::cout << "int& v" << std::endl; }
void bar(const int& v) { std::cout << "const int& v" << std::endl; }
void bar(int&& v) { std::cout << "int&& v" << std::endl; }
void bar(const int&& v) { std::cout << "const int&& v" << std::endl; }
template<typename T>
void foo(T&& t) { bar(std::forward<T>(t)); }
int main(int argc, char** argv) {
  int x = 1;
  const int y = 2;
  int&& z = 3;
  const int&& k = 4;
  foo(x);  // "int& v";
  foo(y);  // "const int& v";
  foo(z);  // "int& v";
  foo(k);  // "const int& v";
  foo(10);  // "int&& v";
  foo(static_cast<const int&&>(10));  // "const int&& v";
  return 0;
}
  1. (Page:108)利用完美转发做包装函数:
template<typename T, typename U>
void PerfectForward(T&& t, U& f) { f(std::forward<T>(t)); }
  1. (Page:109)在构造函数有默认参数值的情况下,构造函数仍有可能被隐式调用
struct A {
  A(int x, int y = 10) {}
};
int main(int argc, char** argv) {
  A a = 1;
  return 0;
}
  1. (Page:110)将 explicit 应用于自定义类型转换操作符以阻止自定义类型的隐式自动转换:
struct A {
  explicit operator bool() { return true; }
};
int main(int argc, char** argv) {
  A x, y;
  // std::cout << x + y << std::endl; // 2;
  return 0;
}
  1. (Page:116)对于 const 变量来说,如果新类型可以完整存放其值,则通过列表初始化方式赋值时并不会出现 narrowing 问题。
  2. (Page:127)若非受限联合体有非 POD 成员,且该成员有非平凡的构造函数,则该联合体的默认构造函数/析构函数将被标记为删除,需要进行自定义。
union T {
  std::string s;
  int v;
};
int main(int argc, char** argv) {
  // T t;  // wrong!
  return 0;
}
  1. (Page:128)pseudo-destructor:伪析构函数需要保证显式调用非类类型的析构函数的语法是有效,因此可以编写代码而不必知道给定类型是否存在析构函数(内置类型 or 自定义类型)。
struct A {
  A() = default;
  ~A() { std::cout << "Real Destructor A." << std::endl; }
};
using TypeA = A;
using TypeInt = int;
int main(int argc, char** argv) {
  TypeA v;
  TypeInt i = 10;
  v.~TypeA();
  i.~TypeInt();
  return 0;
}
  1. (Page:131)自定义字面量值(operator “” [_Literal]):
struct A {
  int v;
  A(int v) : v(v) {}
};
A operator "" _toA(const unsigned long long v) {
  return A(v);
}
int main(int argc, char** argv) {
  auto t = 10_toA;
  std::cout << t.v << std::endl;
  return 0;
}
  1. (Page:132)自定义字面量值(operator “”)的参数要求:
  1. (Page:140)SFINEA 规则:匹配失败不是错误。即对重载的模板参数进行展开的时候,如果展开导致了一些类型不匹配,编译器并不会报错。基于此规则,编译器会对某些模板使用更为精确的版本来实例化,另外一些则使用通用版本进行实例化。
  2. (Page:144)一般函数内没有声明为 static 的变量总是具有自动存储期的局部变量。
  3. (Page:150)auto 关键字不能保持变量而非引用的顶层 CV(const/volatile)特性。
int main(int argc, char** argv) {
  int x = 0;
  const int& y = x;
  auto& z = y;  // "const int &z";
  return 0;
}
  1. (Page:151)auto 实际上是一个将要推导出类型的占位符。
  2. (Page:152)使用 auto 推导数组类型时需要显式指定类型为指针,否则会退化为指针地址对应的整型。
  3. (Page:160)decltype(e) 的类型推导规则:
  1. (Page:161)注意 decltype 在推导自增运算符表达式时的区别:
int main(int argc, char** argv) {
  int i = 10;
  decltype(i++) x;  // int;
  decltype(++i) x = i;  // "int&";
  return 0;
}
  1. (Page:164)与 auto 不同的是,放置于 decltype 后面的*号不会被编译器忽略,因此在推导指针类型时不需要另外放置该符号。
  2. (Page:165)追踪返回类型:
template<typename T1, typename T2>
auto sum(T1& t1, T2& t2) -> decltype(t1 + t2) {
  return t1 + t2;
}
  1. (Page:167)定义类型:“一个函数返回一个函数指针,这个函数指针的返回值是一个函数指针”:
int(*(*foo)())() {};
auto foo() -> auto (*)() -> int(*)() {};
  1. (Page:179)非强类型枚举类的缺点:非强类型作用域(污染全局环境)、允许隐式转换为整型、占用存储空间及符号性不确定;
  2. (Page:184)只能使用右值来初始化一个 std::unique_ptr,而 std::unique_ptr 可以通过 std::move 来交换所有权:
int main(int argc, char** argv) {
  auto up = std::make_unique<int>(10);
  auto nup = std::move(up);
  std::cout << (up == nullptr) << std::endl;  // 1;
  std::cout << *nup << std::endl;  // 10;
  return 0;
}
  1. (Page:186)由于 std::shared_ptr 控制块堆内存的释放与引用托管对象的 std::weak_ptr 的数量有关。因此在不需要使用 std::weak_ptr 时,可以通过 std::weak_ptr::reset 及时切断引用。
  2. (Page:187)两种常用的垃圾回收方式:
  1. (Page:189)C++ 垃圾回收:贝姆(Boehm)垃圾收集器。
  2. (Page:190)检查是否支持最小垃圾回收及安全派生指针:
int main(int argc, char** argv) {
  std::cout << "Pointer safety: ";
  switch (std::get_pointer_safety()) {
    case std::pointer_safety::strict: std::cout << "strict\n"; break;
    case std::pointer_safety::preferred: std::cout << "preferred\n"; break;
    case std::pointer_safety::relaxed: std::cout << "relaxed\n"; break;
  }
  return 0;
}
  1. (Page:193)数组大小参数、switch-case 的 case 语句以及枚举类成员都需要使用编译期常量进行初始化。
  2. (Page:195)constexpr 函数:
  1. (Page:197)const 常量和 constexpr 常量的区别:大多数情况下两者没有区别。但如果在全局命名空间中,编译器一定会为 const 产生数据,而 constexpr 除非有代码显式使用了它的地址,否则一般不会为其生成数据,而仅当做编译期的值(编译时替换),类似枚举值。
  2. (Page:198)constexpr 构造函数:
class A {
  int i;
 public:
  constexpr A(int i) : i(i) {} 
  constexpr int getV() const { return i; }
};
int main(int argc, char** argv) {
  constexpr int v = 10;
  constexpr A a{v};
  int arr[a.getV()] = {1, 2, 3, 4};
  return 0;
}
  1. (Page:199)当声明为常量表达式的模板函数后,而某个其实例化结果不满足常量表达式的需求的话,则 constexpr 关键字会被自动忽略。
  2. (Page:202)constexpr 元编程 / template 元编程,两者均是图灵完备的。
// template - TMP;
template<size_t n>
constexpr int fib() { return fib<n - 1>() + fib<n - 2>() + 1; }
template<> constexpr int fib<0>() { return 1; }
template<> constexpr int fib<1>() { return 1; }

// constexpr - CMP;
constexpr int fib(int i) {
  return i == 1 ? 1 : ((i == 2) ? 1 : fib(i - 1) + fib(i - 2));
}
int main(int argc, char** argv) {
  int arrA[fib(5)] = {0};
  int arrB[fib<3>()] = {0};
  return 0;
}


这是文章底线,下面是评论
  暂无评论,欢迎勾搭 :)