计算机 · 2021年12月19日 0

Modern C++

按照微软为visual studio准备的c++文档重新学习和梳理一下自己的c++知识,只考虑通用 的部分,不考虑只在windows平台上提供的c++特性。
原文档地址

Modern C++

Modern C++与C-style C++的比较:

  • Stack-based scope instead of heap or static global scope
  • 自动的类型推导和引用而不是显示的指定类型名字
  • 使用智能指针而不是原始指针
  • 对于字符串使用std::stringstd::wstring类型,而不是使用原始的字符串数组char[]
  • 使用C++标准库提供的容器而不是原始数组和自定义容器
  • 使用C++标准库提供的算法而不是自己写的
  • 使用异常处理各种出错状况
  • 使用C++标准库提供的Lock-free的线程间通信机制而不是其他的(可能有锁的)线程间通信机制
  • Inline lambda函数
  • 基于范围的for循环

Modern C++的类型系统

  • 变量(variable)
  • 对象(object)
  • POD类型(POD type, plain old data)
    没太弄清具体定义,反正记住C里面只有POD类型就好了

值类型(Value Types)

C++里的类默认是values types。
关于值类型(Value Types)和引用类型(Reference Types)的辨析:

As previously stated, C++ classes are by default value types. They can be specified as reference types, which enable polymorphic behavior to support object-oriented programming. Value types are sometimes viewed from the perspective of memory and layout control, whereas reference types are about base classes and virtual functions for polymorphic purposes. By default, value types are copyable, which means there is always a copy constructor and a copy assignment operator. For reference types, you make the class non-copyable (disable the copy constructor and copy assignment operator) and use a virtual destructor, which supports their intended polymorphism. Value types are also about the contents, which, when they are copied, always give you two independent values that can be modified separately. Reference types are about identity – what kind of object is it? For this reason, “reference types” are also referred to as “polymorphic types”.

如何声明一个Reference Type?

没有语法上的标准方法,只要从功能上让这个类的复制构造函数和赋值函数不能用就行了。比如将这两个方法设置为private:

// cl /EHsc /nologo /W4  

class MyRefType {  
private:  
    MyRefType & operator=(const MyRefType &);  
    MyRefType(const MyRefType &);  
public:  
    MyRefType () {}  
};  

int main()  
{  
    MyRefType Data1, Data2;  
    // ...  
    Data1 = Data2;  
}  

拷贝的优化,move语义

通过用右值引用作为参数来定义move construction和move assignment,减少内存拷贝。

#include <memory>  
#include <stdexcept>  
using namespace std;  
// ...  
class my_class {  
    unique_ptr<BigHugeData> data;  
public:  
    my_class( my_class&& other )   // move construction  
        : data( move( other.data ) ) { }  
    my_class& operator=( my_class&& other )   // move assignment  
    { data = move( other.data ); return *this; }  
    // ...  
    void method() {   // check (if appropriate)  
        if( !data )   
            throw std::runtime_error("RUNTIME ERROR: Insufficient resources!");  
    }  
};  

move-only 类型:只能move,不能拷贝。
关于这个move方法,现在还不是很清楚到底干了什么和有什么优点,先记在这里后面再补充。

显示的类型转换Explicit conversions(casts)

C-style的两种强制转换,效果等价。

(int) x; // old-style cast, old-style syntax  
int(x); // old-style cast, functional syntax

更好的强制转换是使用下面之一的运算符(一个明显的好处是更容易找到代码中使用了强制转换的地方):

  • static_cast
    只在编译时检查这类转换。可以用于compatible的类型之间的强制转换。
  • dynamic_cast
    在运行时多花费点算力来检查这个pointer-to-base到pointer-to-derived的dynamic_cast是否有效。
Base* b = new Base();  

// Run-time check to determine whether b is actually a Derived*  
Derived* d3 = dynamic_cast<Derived*>(b);  

// If b was originally a Derived*, then d3 is a valid pointer.  
if(d3)  
{  
   // Safe to call Derived method.  
   cout << d3->DoSomethingMore() << endl;  
}  
else  
{  
   // Run-time check failed.  
   cout << "d3 is null" << endl;  
}  

//Output: d3 is null;
  • const_cast
    将变量的const属性给去掉或者反过来给变量加上const属性。
  • reinterpret_cast
    用于两个完全不相关的类型之间的转换。这个就比较接近C-style的显示强制转换了。

统一初始化和委托构造函数(Uniform Initialization and Dlegating Constructors)

用花括号初始化(Brace Initialization)

#include <string>  
using namespace std;  

class class_a {  
public:  
    class_a() {}  
    class_a(string str) : m_string{ str } {}  
    class_a(string str, double dbl) : m_string{ str }, m_double{ dbl } {}  
double m_double;  
string m_string;  
};  

int main()  
{  
    class_a c1{};  
    class_a c1_1;  

    class_a c2{ "ww" };  
    class_a c2_1("xx");  

    // order of parameters is the same as the constructor  
    class_a c3{ "yy", 4.4 };  
    class_a c3_1("zz", 5.5);  
}

当类有非默认构造函数时,用花括号去初始化变量,编译器会按照参数的数目和顺序去匹配构造函数, 如果类没有非默认构造函数,那么就按类的成员变量的顺序和花括号里的内容去初始化变量。
如果默认构造函数被比较为delete,那么通过空花括号来调用默认构造函数的方式不可用。
可以在任何需要初始化变量的地方使用花括号初始化这种方式来初始化变量。

initializer_list Constructors

可以专门用一个initializer_list来保存上面所说的花括号的对象列表。例如:

#include <initializer_list>
initializer_list<int> int_list{5, 6, 7};  

initializer_list变量可以拷贝,只是拷贝后的变量的成员是被拷贝的变量的成员的引用。

initializer_list<int> ilist1{ 5, 6, 7 };  
initializer_list<int> ilist2( ilist1 );  
if (ilist1.begin() == ilist2.begin())  
    cout << "yes" << endl; // expect "yes"  

委托构造函数(Delegating Constructors)

constructor (. . .) : constructor (. . .)
在使用委托构造函数时,不能同时使用列表初始化成员变量,即constructor(...) : constructor( ...), member1(value1), member2(value2)...或者constructor(...) : member1(value1), member2(value2), ... constructor(...)是不允许的。
使用委托构造函数时,会先调用成员变量自己的初始化代码,然后调用委托构造函数来初始化。

class class_a {  
public:  
    class_a() {}  
    class_a(string str) : m_string{ str } {}  
    class_a(string str, double dbl) : class_a(str) { m_double = dbl; }  
    double m_double{ 1.0 };  
    string m_string{ m_double < 10.0 ? "alpha" : "beta" };  
};  

int main() {  
    class_a a{ "hello", 2.0 };  //expect a.m_double == 2.0, a.m_string == "hello"  
    int y = 4;  
}  

开发者需要避免委托构造函数的调用链不会形成一个环。

对象的声明周期和资源管理

As long as there are no cycles and every link in the DAG is represented by an object that has a destructor (instead of a raw pointer, handle, or other mechanism), then resource leaks are impossible because the language prevents them.

You can use raw pointers for non-ownership and observation. A non-owning pointer may dangle, but it can’t leak.

所以关键在于不要用raw pointer来own resource,就不用担心资源泄露。

容易出问题的static变量

Use static lifetime sparingly (global static, function local static) because problems can arise. What happens when the constructor of a global object throws an exception? Typically, the app faults in a way that can be difficult to debug. Construction order is problematic for static lifetime objects, and is not concurrency-safe. Not only is object construction a problem, destruction order can be complex, especially where polymorphism is involved. Even if your object or variable isn’t polymorphic and doesn’t have complex construction/destruction ordering, there’s still the issue of thread-safe concurrency. A multithreaded app can’t safely modify the data in static objects without having thread-local storage, resource locks, and other special precautions.

RAII(Resource acquisition is initialization)

确保对象拥有其对应的资源。

智能指针(Smart Pointers)

使用智能指针来帮助避免内存泄露、资源泄露和实现异常安全。
智能指针的意义:

In practical terms, the main principle of RAII is to give ownership of any heap-allocated resource—for example, dynamically-allocated memory or system object handles—to a stack-allocated object whose destructor contains the code to delete or free the resource and also any associated cleanup code.

只在必要的地方使用raw pointer:

In modern C++, raw pointers are only used in small code blocks of limited scope, loops, or helper functions where performance is critical and there is no chance of confusion about ownership.

微软给的提示:

Always create smart pointers on a separate line of code, never in a parameter list, so that a subtle resource leak won’t occur due to certain parameter list allocation rules.

C++ Standard Library Smart Pointers

  • unique_ptr
    只能move,不能复制
  • shared_ptr
    包含同一个指针的shared_ptr总是相等的(equal),包含不同指针的shared_ptr是不等的。
// Initialize two separate raw pointers.
// Note that they contain the same values.
auto song1 = new Song(L"Village People", L"YMCA");
auto song2 = new Song(L"Village People", L"YMCA");

// Create two unrelated shared_ptrs.
shared_ptr<Song> p1(song1);    
shared_ptr<Song> p2(song2);

// Unrelated shared_ptrs are never equal.
wcout << "p1 < p2 = " << std::boolalpha << (p1 < p2) << endl;
wcout << "p1 == p2 = " << std::boolalpha <<(p1 == p2) << endl;

// Related shared_ptr instances are always equal.
shared_ptr<Song> p3(p2);
wcout << "p3 == p2 = " << std::boolalpha << (p3 == p2) << endl; 
  • weak_ptr
    weak_ptr实现的是observer,不会拥有资源。
  • pimpl idiom
    pointer to implementation,就是在要暴露出来给别人用的类里面声明一个用于实现该类的impl类和相应的impl类指针;然后这个impl类的实现写在cpp文件里面,这样便实现了对类的封装。
// my_class.h  
class my_class {  
   //  ... all public and protected stuff goes here ...  
private:  
   class impl; unique_ptr<impl> pimpl; // opaque type here  
};  
// my_class.cpp  
class my_class::impl {  // defined privately here  
  // ... all private data and functions: all of these  
  //     can now change without recompiling callers ...  
};  
my_class::my_class(): pimpl( new impl )  
{  
  // ... set impl values ...   
}  

字符串和I/O的格式化

使用c++原生的<iomanip>来格式化字符串和输出是很折磨人的,所以微软建议使用Boot库里的Boost.Format来简化这种工作。

错误和异常处理

  • 使用asserts来检查不应该发生的错误,而使用异常来检查可能会发生的错误。
  • 当检测错误的代码和处理错误的代码可以较好地分开的时候就使用异常。分不开的地方和性能很重要的地方就考虑使用错误码吧。
  • 对于会抛出异常或者传递异常的函数,应该明确指出该函数是strong guarantee、basic guarantee、nothrow三种exception guarantee中的哪一种。
  • 抛出异常时by value,捕获异常时by reference。
  • 不要使用exception specifications(这是什么?)
  • 能使用标准库提供的exception就用或者使用继承自标准库的异常。
  • 不要让异常从析构函数或者memory-deallocation函数里抛出来。

三种Exception Guarantee:

  1. No-fail Guarantee
    No exception would be thrown out of the function.
  2. Strong GuaranteeThe strong guarantee states that if a function goes out of scope because of an exception, it will not leak memory and program state will not be modified. A function that provides a strong guarantee is essentially a transaction that has commit or rollback semantics: either it completely succeeds or it has no effect.
  3. Basic GuaranteeThe basic guarantee is the weakest of the three. However, it might be the best choice when a strong guarantee is too expensive in memory consumption or in performance. The basic guarantee states that if an exception occurs, no memory is leaked and the object is still in a usable state even though the data might have been modified. +

词法约定

注释

c++的注释采用first syntax,不允许nested comment。即下面的注释是不允许的:

/* outer comments /* inner comments */ */

数值,布尔类型和指针的字面值

对于浮点类型的数,默认是当做double,但是可以通过添加F/f后缀表示将其当做float,添加L/l 后缀表示将其当做long double。

字符串和字符的字面值

Universal character names

\UNNNNNNNN表示一个8个16进制数位的Unicode code point,而用\uNNNN表示前4个16 进制数位为0的Unicode code point。
c++里的几种字符:

  • narrow character
    用于表示ascii字符,对应char
  • UTF-8字符
    u8前缀,对应char
  • 宽字符
    L前缀,用于表示UCS-2或者UTF-16,对应wchar_t
  • UTF-16
    u前缀,对应char16_t
  • UTF-32
    U前缀,对应char32_t

对于raw string literal,则在上面的前缀后追加上R即可。

#include <string>  
using namespace std::string_literals; // enables s-suffix for std::string literals  

int main()  
{  
    // Character literals  
    auto c0 =   'A'; // char  
    auto c1 = u8'A'; // char  
    auto c2 =  L'A'; // wchar_t  
    auto c3 =  u'A'; // char16_t  
    auto c4 =  U'A'; // char32_t  

    // String literals  
    auto s0 =   "hello"; // const char*  
    auto s1 = u8"hello"; // const char*, encoded as UTF-8  
    auto s2 =  L"hello"; // const wchar_t*  
    auto s3 =  u"hello"; // const char16_t*, encoded as UTF-16  
    auto s4 =  U"hello"; // const char32_t*, encoded as UTF-32  

    // Raw string literals containing unescaped \ and "  
    auto R0 =   R"("Hello \ world")"; // const char*  
    auto R1 = u8R"("Hello \ world")"; // const char*, encoded as UTF-8  
    auto R2 =  LR"("Hello \ world")"; // const wchar_t*  
    auto R3 =  uR"("Hello \ world")"; // const char16_t*, encoded as UTF-16  
    auto R4 =  UR"("Hello \ world")"; // const char32_t*, encoded as UTF-32  

    // Combining string literals with standard s-suffix  
    auto S0 =   "hello"s; // std::string  
    auto S1 = u8"hello"s; // std::string  
    auto S2 =  L"hello"s; // std::wstring  
    auto S3 =  u"hello"s; // std::u16string  
    auto S4 =  U"hello"s; // std::u32string  

    // Combining raw string literals with standard s-suffix  
    auto S5 =   R"("Hello \ world")"s; // std::string from a raw const char*  
    auto S6 = u8R"("Hello \ world")"s; // std::string from a raw const char*, encoded as UTF-8  
    auto S7 =  LR"("Hello \ world")"s; // std::wstring from a raw const wchar_t*  
    auto S8 =  uR"("Hello \ world")"s; // std::u16string from a raw const char16_t*, encoded as UTF-16  
    auto S9 =  UR"("Hello \ world")"s; // std::u32string from a raw const char32_t*, encoded as UTF-32  
}  

用户定义的字面值

通过定义operator ""可以实现直接在代码里使用复数那种效果。

ReturnType operator "" _a(unsigned long long int);   // Literal operator for user-defined INTEGRAL literal  
ReturnType operator "" _b(long double);              // Literal operator for user-defined FLOATING literal  
ReturnType operator "" _c(char);                     // Literal operator for user-defined CHARACTER literal  
ReturnType operator "" _d(wchar_t);                  // Literal operator for user-defined CHARACTER literal  
ReturnType operator "" _e(char16_t);                 // Literal operator for user-defined CHARACTER literal  
ReturnType operator "" _f(char32_t);                 // Literal operator for user-defined CHARACTER literal  
ReturnType operator "" _g(const     char*, size_t);  // Literal operator for user-defined STRING literal  
ReturnType operator "" _h(const  wchar_t*, size_t);  // Literal operator for user-defined STRING literal  
ReturnType operator "" _i(const char16_t*, size_t);  // Literal operator for user-defined STRING literal  
ReturnType operator "" _g(const char32_t*, size_t);  // Literal operator for user-defined STRING literal  
ReturnType operator "" _r(const char*);              // Raw literal operator  
template<char...> ReturnType operator "" _t();       // Literal operator template  

基本概念

声明和定义

  • Overview of Declarators
    declarator指定变量名,以及该变量究竟是对象、指针还是引用、数组。
  • Specifiers
    • storage-class-specifier
    • type-specifier
    • function-specifier
    • friend
    • typedef
    • __declspec(extended-decl-modifier-seq)
  • Initializers
    1. Zero initialization
      • 数值类型的变量被初始化为0(或者0.0,甚至0.00000000,等等)
      • 字符变量被初始化为’\0′
      • 指针被初始化为nullptr
      • 数组,POD,结构体和联合类型将成员初始为0
      发生Zero initialization的时机:
      • 程序启动时,所有具有静态生命周期(static duration)的有名字的变量。这些变量在稍后可能会再被初始化一遍
      • 使用{}初始化标量和POD类型时,标量和POD里的成员都用zero initialization
      • 数组只按值初始化了一部分元素时,剩下的元素都用zero initialization
    2. Default initialization
      就是说对于类、结构体和联合类型使用默认构造函数初始化。当没有指定初始化表达式或者使用new关键字时就是用的默认构造函数初始化
      • 常量变量必须要指定一个初始化表达式,而不能使用默认构造函数或者zero initialization
    3. Value initialization
    4. Copy initialization
    5. Direct initialization
    6. List initialization
    7. Aggregate initialization
    8. Reference initialization
  • 使用typedef和using简化复杂类型的声明
  • Storage classes
    • static
    • extern
    • thread_local

基础类型

内置运算符及其优先级和结合性

表达式

语句

命名空间

枚举类型

联合类型

函数

运算符重载

类和结构体

lambda表达式

数组

引用

指针

异常处理

try,throw和catch语句

throw表达式的operand可以是任何类型的对象。catch表达式根据其要捕获的exception 指定其operand为相应的类型。如果catch表达式想捕获任意类型的异常,那么就将其operand 写为...。如果想在catch块里将捕获的异常重新抛出,那么用不带operand的throw表达式 就可以了。重新抛出的异常是被捕获的同一个异常,而不是其拷贝。

catch语句的匹配

如果catch语句的参数是引用,那么catch语句里的异常对象就正好是抛出的那个异常对象, 否则是其拷贝。

一个抛出的异常可以被下列catch语句捕获:

  • 没有指定异常类型的catch语句,即catch(...)
  • 参数类型和异常类型相同的catch语句,由于catch的参数是原异常的拷贝,所以const,volatile 等修饰符是被忽略的
  • 参数类型正好是异常类型的引用
  • 带const或者volatle修饰符的异常类型的引用(a reference to a const or volatile form of the same type as the exception object)
  • 参数类型是异常类型的基类的catch语句,同理const和volatile修饰符被忽略
  • 参数类型是异常类型的积累的引用…
  • 带const或者volatile修饰符的异常类型的积累的引用
  • 参数类型是一个指针,而且抛出的异常正好也是一个指针类型而且该指针可以通过指针standard pointer conversion rules转换为catch的参数类型指针

stack unwinding

异常不被捕获或者在stack unwinding的过程中出了异常,运行时函数terminate会被调用。
在进入catch块之前,try语句里的自动变量会被销毁掉(stack unwinding)。

exception specification

指定函数不抛出任何异常:

  • noexcept
  • noexcept(true)
  • throw()

未被捕获的异常

默认情况下,如果有未被捕获的异常,系统会调用terminate(terminate会调用abort)。不 过也可以通过set_terminate设置terminate调用指定的函数。

// exceptions_Unhandled_Exceptions.cpp  
// compile with: /EHsc  
#include <iostream>  
using namespace std;  
void term_func() {  
   cout << "term_func was called by terminate." << endl;  
   exit( -1 );  
}  
int main() {  
   try  
   {  
      set_terminate( term_func );  
      throw "Out of memory!"; // No catch handler for this exception  
   }  
   catch( int )  
   {  
      cout << "Integer exception raised." << endl;  
   }  
   return 0;  
}  

断言和用户提供的信息

模板

Event Handling

一、基础

2.运算优先级

加、减、模运算是一个优先级别的。
完整的运算符优先级列表,可以看到,优先级最低,组合运算符、三元运算符、逻辑运算符、位运算等都挺低的。

3.函数参数默认值

可以为函数的参数指定默认值,不过只能从后往前指定。
不能写出下面这种代码:

int add(int a, int b=2);
int add(int a);

在编译的时候如果有调用add(2),那么编译器不知道你到底要调用哪个函数。

4.在初始化列表中初始化const成员变量

const成员变量只能在构造函数的初始化列表中进行初始化

5.使用友元函数或者friend class来访问private变量或成员函数

6.不能被重载的运算符

7.前置/后置自增/减运算符的重载声明

前置的需要一个形式参数,后置的不需要。