《深入理解C++11》笔记

版权声明:本文为博主原创文章,可以转载但必须注明出处。 https://blog.csdn.net/nirendao/article/details/87991113

整理一下以前的笔记,聊作这个月的Blog吧。

第1章 新标准的诞生 & 第2章 保证稳定性和兼容性

C++11有约140个新特性,修正了C++03的约600个错误。

C++11的新关键字:alignas、alignof、decltype、auto(重新定义)、static_assert、using(重新定义)、noexcept、nullptr、constexpr、thread_local

override、final

2个具有特殊含义的新标识符。如果这两个标识符出现在成员函数之后,它们的作用是标注一个成员函数是否可以被重载。不过也可以在C++11代码中定义出override、final这样名称的变量。此时,它们只是标识了普通的变量名称。

宽窄字符连接

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

long long

long long整型有两种:long long和unsigned long long。在C++11中,标准要求long long整型可以在不同平台上有不同的长度,但至少有64位。

一些宏:

#error “<error info>”

#NDEBUG  禁用assert宏

static_assert: 编译时期做的断言。

noexcept关键字:

默认为noexcept(true), 也可以写为noexcept(<expr>), expr返回一个bool值。声明为 noexcept 的函数,一旦抛出异常,程序调用 std::terminate() 而终止。delete函数默认就是noexcept的。

类声明时初始化变量

在类或结构体声明的时候,可给变量定义。优点是:不需要在初始化列表里写好多个变量了,相当于有了默认值。

但是,对于非常量的静态成员变量,程序员还是要到头文件以外去定义它。这可以保证在编译时,类静态成员的定义最后只存在于一个目标文件中。

final

类的成员函数被声明为final之后,将不能再被重写。

override:

用来修饰成员函数,表示该函数一定是虚函数,即其父类或祖类并有这个虚函数而现在需要重写。

函数模板可以有默认参数

C++98中引入了函数模板和类模板。但是C++98只支持类模板有默认模板参数,而函数模板却不能有默认模板参数。C++11中取消了这条限制。

template <typename T=int> 
class TTT {};

template <typename T=int>
void func() {}

外部模板:

extern template void func<int>(int){}

类似于声明外部变量,声明一个外部模板使得编译器和链接器不必去做多余的模板生成和去重的工作。


第3章 通用为本,专用为末

3.1 继承构造函数

B继承A. A有多个构造函数,且每个构造函数的参数都不同。那么B自己的构造函数是不是也要写多份呢?

使用 using A::A; 在B的构造函数中即可。

C++11中的继承构造函数被设计为和派生类中各种默认函数(默认构造、析构、拷贝构造、赋值运算符)一样,是默认就有的。

struct A{
    A(int i) {}
    A(double d, int i) {}
    A(float f, int i, const char *c) {}
    // ...
};

struct B : A{
    using A::A;  // 继承构造函数
    // ...
    virtual void ExtraInterface() {}
};

一旦使用了继承构造函数,编译器就不会再生成默认构造函数了。

struct A{ A(int){} };
struct B:A { using A::A; }
B b;  // B没有默认构造函数了

3.2 委派构造函数

class Info {
public: 
    Info() { InitRest(); }
    Info(int i): Info() {type = i;}    // 委派构造函数
    Info(char e): Info() {name = e; }  // 委派构造函数

private:
    int type {1};
    char name {'a'};
    void InitRest() {/*其他初始化*/}

};

Info(int i) 和 Info(char e) 是委派构造函数。

C++中,构造函数不能同时是“委派”和使用初始化成员列表。所以,如果委派构造函数要给变量赋值,初始化代码必须放在函数体中。

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

能够取地址、有名字的就是左值;不能取地址、没有名字的就是右值。

C++11中,右值由2个概念构成,一个是纯右值(prvalue, Pure R value), 另一个是将亡值(xvalue,eXpiring value)。

所谓将亡值,是C++11新增的跟右值引用相关的表达式,这样的表达式通常是“将要被移动的对象”。比如,返回右值引用T&&的函数返回值,std::move的返回值,或者转换为 T&& 的类型转换函数的返回值。

常量左值引用,在C++中是一个万能引用类型,它可以接受非常量左值、常量左值、右值对其进行初始化;

而普通的左值引用,只能接受非常量左值对其进行初始化。

所以,

T& e = ReturnValue();        // 编译错误,不能将一个右值赋给一个普通的左值引用
const T& f = ReturnValue();  // 正确,可以将一个右值赋给一个常量左值引用
T&& g = ReturnValue();       // 正确,可以将一个右值赋给一个右值引用

安全的设计:

通常情况下,程序员会为声明了移动构造函数的类声明一个常量左值为参数的拷贝构造函数,以保证在移动构造不成的时候,可以使用拷贝构造函数。这也是因为常量左值引用可以接受各种值(非常量左值、常量左值、右值)对其进行初始化。

标准库<type_traits>头文件中提供了3个模板类: is_rvalue_reference, is_lvalue_reference, is_reference, 可供我们进行判断,比如:

cout << is_rvalue_reference<string &&>::value; 

配合第4章中的类型推导操作符 decltype, 甚至可以对变量的类型进行推断。

std::move  - 强制转换为右值 

实际上,std::move并不能移动任何东西,它唯一的功能就是将一个左值强制转化为右值引用,继而我们可以通过右值引用使用改值,以用于移动语义。

默认情况下,编译器会隐式地生成一个移动构造函数(隐式表示如果不被使用则不生成)。不过如果程序员声明了自定义的拷贝构造函数、拷贝赋值函数、移动赋值函数、析构函数中的一个或者多个,编译器都不会再为程序员生成默认的版本。

默认的移动构造函数实际上跟默认的拷贝构造函数一样,只能做一些按位拷贝的工作,这对实现移动语义来说是不够的。所以,一般要自己实现移动构造函数。

有了移动语义,就有一个典型的应用是可以实现高性能的置换函数的(swap)。如下:

template <class T>
void swap(T& a, T&b)
{
  T tmp(std::move(a));
  a = std::move(b);
  b = std::move(tmp);
}

对于移动构造函数来说,抛出异常比较危险。因为可能移动语义还没有完成,就抛出了异常,从而导致一些指针称为悬挂指针。因此程序员应该尽量编写不抛出异常的移动构造函数,那么只要为其添加一个 noexcept 关键字就可以了。这样,当抛出异常时,程序就会被std::terminate()终止。

完美转发

所谓完美转发,就是在函数模板中,完全依照模板的参数类型,将参数传递给函数模板中调用的另外一个函数。比如:

template <typename T>
void IamForwarding(T t) { IrunCodeActually(t); }

template<typename T>
void IamForwarding(T&& t) { 
    IrunCodeActually(std::forward(t));  // 利用 std::forward 进行完美转发
}  

对于目标函数IrunCodeActually来说,它总是希望转发函数将参数按照传入Iamforwarding时的类型传递给IrunCodeActually,而不产生额外的开销,就好像转发者不存在一样。

引用折叠(略)

显式转换操作符

classB { 
public: 
    // classB中定义了一个显式转换到classA类型的类型转换符
    explicit operator ClassA() const { return ClassA(); }  
    // ......
}; 

classB b;
classA a(b);  // Good

初始化列表

vector<int> c{1, 3, 5};
map<int, float> d = {{1, 1.0f}, {2, 2.0f}};
double *d = new double{1.2f};

程序员只要 #include <initializer_list> 并且声明一个以initializer_list<T> 模板类为参数的构造函数,同样可以使得自定义的类使用列表初始化。

初始化列表可以防止类型收窄。

int g{2.0f};   // 收窄,无法通过编译
float *h = new float{1e48};  // 收窄,无法通过编译
unsigned char e {-1};  // 收窄,无法通过编译

POD类型

Plain Old Data. C++11将POD划分为2个基本概念的合集: 平凡的(trivial)和标准布局的(standard layout)。

平凡的类或结构体应该符合以下定义:

  1. 拥有平凡的默认构造函数和析构函数

  2. 拥有平凡的拷贝构造函数和移动构造函数

  3. 拥有平凡的拷贝赋值运算符和移动赋值运算符

  4. 不能包含虚函数和虚基类

我们可以使用类模板 template<typename T> struct std::is_trivial 的成员value来判断T类型是否是一个平凡的类型。

标准布局的类或结构体应该符合以下定义:

  1. 所有非静态成员有相同的访问权限,比如全是public的,或全是private的,等。

  2. 在类或结构体继承时,满足以下2种情况之一:

    • 派生类中有非静态成员,且只有一个仅包含静态成员的基类

    • 基类有非静态成员,而派生类没有非静态成员。

  3. 类中第一个非静态成员的类型与其基类不同。

  4. 没有虚函数或虚基类。

  5. 所有非静态数据成员符合标准布局类型,其基类也符合标准布局。

可以使用模板类 template <typename T> struct std::is_standard_layout 的成员value来判断一个类型是否是标准布局的类型。

要判断一个类型是否是POD的,标准库中的 <type_traits> 头文件提供了模板类 

template <typename T> struct std::is_pod的成员value可以进行判断,即: std::is_pod<T>:value  看其值为1或0. 

POD的好处:

  1. 字节赋值: 代码中可以安全地使用memset和memcpy对POD类型进行初始化和拷贝等操作;

  2. 提供对C内存布局兼容: C++程序可以与C函数进行相互操作;

  3. 保证了静态初始化的安全有效

非受限联合体

C++11中,取消了union对于数据成员类型的限制。

用户自定义字面量(略)

即自定义单位之类,如w,kg等

内联名字空间

C++98不允许在不同的名字空间中对模板进行特化。

C++11通过关键字“inline namespace”就可以声明一个内联的名字空间。内联的名字空间允许程序员在父名字空间定义或特化子名字空间的模板。

以下代码是在父名字空间Jim中对子名字空间ToolKit中定义的模板类 SwissArmyKnife进行特化。

// In Jim's code
namespace Jim {
    inline namespace Toolkit {
        template<typename T> class SwissArmyKnife{};
    }
}
// in LiLei's code
namespace Jim {
    template <> class SwissArmyKnife<Knife>{};
}

模板的别名

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

using uint = unsigned int;
cout << is_same<uint, UINT>:value << endl;  // 1

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

一般化的SFINEA规则

SFINEA = Substitution failure is not an error

对重载的模板参数进行展开的时候,如果展开导致了一些类型不匹配,编译器并不会报错。


第4章 易学易用

auto (略)

decltype

int i;
decltype(i) j = 0;

float a;
double b;
decltype(a+b) c;

decltype常常与typedef或using合用:

typedef decltype(vec.begin()) vectype;

追踪返回类型(略)

for_each

int action1(int &e) {e *= 2;}
int arr[] = {1, 2, 3, 4};
for_each(arr, arr+sizeof(arr)/sizeof(arr[0], action1);   // for_each是模板函数

第5章 提高类型安全

强类型枚举

C中的枚举

enum Gender {Male, Female};  // 0, 1

匿名枚举

enum {Male, Female};  // 0, 1

有名字的enum类型的名字,以及enum的成员的名字,都是全局可见的。

非强类型作用域、允许隐式转换为整型、占用存储空间、符号的不确定性,这些都是枚举类型的缺点。

针对这些缺点,C++11引入了一种新的枚举类型,即“枚举类”,又称“强类型枚举”。其声明只需在enum后面加上关键字 class. 

enum class Type {General, Light, Medium, Heavy};

强类型枚举具有以下优点:

  • 强作用域:强类型枚举成员的名称不会输出到其父作用域空间;

  • 转换限制:不可以与整型隐式地相互转换;

  • 可以指定底层类型:默认的底层类型为int,也可以显式地指定底层类型,具体方法为在枚举名称后面加上“:type”, 其中type可以是除了wchar_t以外的任何整型:

 enum class Type:char {General, Light, Medium, Heavy};

堆内存管理:智能指针与垃圾回收

C++98中,智能指针通过一个模板类 auto_ptr 来实现。程序员不用再显式地调用delete. 这在一定程度上避免了堆内存忘记释放而造成的问题。

不过auto_ptr有一些缺点(拷贝时返回一个左值,不能调用delete [],等等),所以在C++11标准中被废弃了。

C++11标准中改用 unique_ptr, shared_ptr 以及 weak_ptr. 

unique_ptr<int> up1(new int(11));  
unique_ptr<int> up2 = up1;  // 编译错误, 不能复制up1,unique_ptr无拷贝构造函数
cout << *up1 << endl;  // 11

unique_ptr<int> up3 = std::move(up1);  //仅能通过move进行转移,现在up3是唯一的unique_ptr智能指针
cout << *up3 << endl;  // 11
cout << *up1 << endl;  // 运行时错误

up3.reset();      // 显式释放内存
up1.reset();      // 不会导致运行时错误
cout << *up3 << endl;   // 运行时错误,空间已释放


shared_ptr<int> sp1(new int(22));
shared_ptr<int> sp2 = sp1;    // OK 
cout << *sp1 << " " << *sp2 << endl;   // 22 22

sp1.reset();  
cout <<*sp2 << endl;   // 22

从作用上讲,unique_ptr和shared_ptr和以前的auto_ptr保持了一致。

只有在计数归零的时候,shared_ptr才会真正释放所占有的堆内存的空间。

weak_ptr可以指向shared_ptr指针指向的对象内存,却并不拥有该内存。

使用weak_ptr成员函数lock(),则可返回其指向内存的一个shared_ptr对象,且在所指对象内存已经无效的时候,返回指针空值(nullptr)。这在验证shared_ptr的有效性的时候很有作用。

void Check(weak_ptr<int> & wp) 
{
    shared_ptr<int> sp = wp.lock();  // lock()函数返回为shared_ptr
    if (sp != nullptr) {
        cout << still << *sp << endl; 
    else:
        cout << "pointer is invalid. " << endl;
}


int main()
{
    shared_ptr<int> sp1(new int(22));
    shared_ptr<int> sp2 = sp1;
    weak_ptr<int> wp = sp1;  // 指向shared_ptr<int>所指向的对象
    

    cout << *sp1 << endl; // 22
    cout << *sp2 << endl;  // 22
    Check(wp);     // still 22

    sp1.reset();
    cout << *sp2 << endl;  // 22
    Check(wp);     // still 22

    sp2.reset();
    Check(wp);    // pointer is invalid

    return 0;
}

第6章 提高性能及操作硬件的能力

常量表达式

关键字 constexpr 加在函数前。

常量表达式函数的要求很严格,有以下几点:

  • 函数体只有单一的return返回语句

  • 函数必须返回值,不能是void函数

  • 在使用前必须已有定义

  • return返回语句表达式中不能使用非常量表达式函数、全局数据,且必须是一个常量表达式。

常量表达式的构造函数的约束如下:

  • 函数体必须为空

  • 初始化列表只能由常量表达式来赋值

struct Date {
  constexpr Date(int y, int m, int d): year(y), month(m), day(d) {}
  constexpr int GetYear() { return year; }
  constexpr int GetMonth(){ return month; }
  constexpr int GetDay() { return day; }

private:
  int year;
  int month;
  int day;
};

constexpr Date PRCFounded {1949, 10, 1};


# Fib写法-1
constexpr int Fib(int n) {
  return (n==1)?1:((n==2)?1:Fib(n-1)+Fib(n-2));
}

# Fib写法-2
template <long num>
sturct Fib {
  static const long val = Fib<num-1> ::val + Fib<num-2>::val; 
};

template <> struct Fib(2) { static const long val = 1; }
template <> struct Fib(1) { static const long val = 1; }
template <> struct Fib(0) { static const long val = 0; }

变长模板(略)

原子类型与原子操作

在使用的便利性上,pthread不如后来者OpenMP. OpenMP的编译器指令将大部分线程化的工作都交给了编译器完成,而将识别需要线程化的区域的工作交给了程序员。

程序员不再需要为原子类型的数字加锁,在线程里直接就可以用。

atomic_llong total{0};

void func(int)
{
    for (long long i=0; i<100000000LL; ++i) {
        total += i;
    }
}


int main()
{
    thread t1(func, 0);
    thread t2(func, 0);
    t1.join();
    t2.join();
    cout << total << endl; 

    return 0;
}

直观地看,编译器可以保证原子类型在线程间被互斥地访问。这样的设计,从并行编程的角度来看,是由于需要同步的总是数据而不是代码。

#include <cstdatomic> 

其中包括的原子类型有:

atomic_bool,

atomic_char, atomic_uchar,

atomic_int, atomic_uint,

atomic_short, atomic_long, atomic_ulong, atomic_llong, 

atomic_char16_t, atomic_char32_t, atomic_wchar_t, 

程序员可以使用atomic类模板。通过该类模板,程序员可以任意定义出需要的原子类型:

std::atomic<T> t; 

C++11中,原子类型只能从其模板参数类型中进行构造,标准不允许原子类型进行拷贝构造、移动构造、以及使用operator=等,以防止发生意外。

atomic<float> af {1.2f}; //good
atomic<float> af1{af};  // 无法通过编译

但是下面可以:

atomic<float> af{1.2f};
float f=af;
float f1{af};

这是因为atomic类模板总是定义了从atomic<T>到T的类型转换函数的缘故。

有一个比较特殊的布尔型atomic类型:atomic_flag(注意,它和atomic_bool是不同的),相比于其他的atomic类型,atomic_flag是无锁的(lock-free),即线程对其访问不需要加锁。因此对于atomic_flag而言,也就不需要load、store等成员函数进行读写(或者重载操作符)。而典型地,通过atomic_flag的成员 test_and_set 以及 clear,我们可以实现一个自旋锁(spin-lock)。

#include <thread>
#include <atomic>
#include <iostream>
#include <unistd.h>
using namespace std;

std::atomic_flag lock = ATOMIC_FLAG_INIT;

void f(int n) {
    while (lock.test_and_set(std::memory_order_acquire)) // 尝试获得锁
        cout << "Waiting from thread " << n << endl;    // 自旋

    cout << "Thread " << n << " starts working" << endl;
}


void g(int n)
{
    cout << "thread " << n << " is going to start." << endl;
    lock.clear(); 
    cout << "thread " << n << " starts working" << endl;
}


int main()
{
    lock.test_and_set();
    thread t1(f, 1);
    thread t2(g, 2);

    t1.join();
    usleep(100);
    t2.join();

    return 0;
}

内存模型:顺序一致性与memory_order (略)

线程局部存储

通常情况下,线程会拥有自己的栈空间,但是堆空间、静态数据区则是共享的。

从可执行文件的角度来说,静态数据区就是可执行文件的data、bss段的数据,而从C/C++语言层面而言,则对应的是全局/静态变量。

在全局或者静态变量的声明前面加上关键字 __thread, 则可将变量声明为TLS变量

每个线程中将拥有独立的errCode的拷贝。

__thread int errCode; 

C++11标准对TLS标准做了一些统一的规定,与__thread修饰符类似,声明一个TLS变量只要通过thread_local修饰符即可。

int thread_local errCode;

快速退出 quick_exit 与 at_quick_exit

  • terminate函数: 声明在<exception>头文件中,是C++语言异常处理的一部分。基本上有异常的地方,就会调用terminate函数。它在默认的情况下,去调用abort函数。用户可以通过set_terminate函数来改变默认行为。
  • abort函数:在C中的头文件<cstdlib>中定义,比terminate更加底层。默认情况下,它会向符合POSIX标准的系统抛出一个信号(signal):SIGABRT. 
  • exit函数:正常退出函数。会正常调用自动变了的析构函数,并且还会调用atexit注册的函数。
  • quick_exit函数:此函数并不执行析构函数,而只是使程序终止。也是属于正常退出。

此外,使用at_quick_exit注册的函数也可以在quick_exit的时候被调用。


第7章 为改变思考方式而改变

指针空值 - nullptr

nullptr和nullptr_t

  • nullptr_t类型数据可以隐式转换成任意一个指针类型;

  • nullptr_t类型数据不能转换为非指针类型,即使使用reinterpret_cast()的方式也是不可以的。

  • nullptr_t类型数据不适用于算术运算表达式。

  • nullptr_t类型数据可以用于关系运算表达式,但仅能与nullptr_t类型数据或者指针类型数据进行比较,当且仅当关系运算符为==、<=、>=等时返回true. 

默认函数的控制

C++中声明自定义的类,编译器会默认生成一些程序员没有定义的成员函数:

  • 构造函数

  • 拷贝构造函数

  • 拷贝赋值函数(operator=)

  • 移动构造函数

  • 移动拷贝函数

  • 析构函数

此外,C++编译器还会为以下这些自定义类型提供全局默认操作符函数:

  • operator,

  • operator&

  • operator&&

  • operator*

  • operator->

  • operator->*

  • operator new

  • operator delete

一旦声明了自定义版本的构造函数,即使它和编译器自动生成的完全一样,它也使得该类不再是POD类型的。

解决办法是:在需要默认函数定义或声明时加上“=default”,从而显式地指示编译器生成该函数的默认版本

在函数的定义或声明时加上“=delete”,会指示编译器不生成函数的缺省版本

程序员在使用显式删除的时候,应该总是避免explicit函数,反之亦然。

placement new构造的对象,编译器不会为其调用析构函数

lambda函数

lambda函数的语法定义如下:

[capture](parameters) mutable ->return-type {statement};

默认情况下,lambda函数是一个const函数,mutable关键字可以取消其常量性。在使用mutable时,参数列表不可省略。

捕捉列表,是用于捕捉上下文中的变量。

  • [var]  以值传递的方式捕捉变量var

  • [=]    以值传递的方式捕捉父作用域的所有变量,包括this

  • [&var]  以引用的方式捕捉变量var

  • [&]    以引用的方式捕捉父作用域的所有变量,包括this

  • [this]  以值传递的方式捕捉当前的this指针

通过一些组合,捕捉列表可以表示更复杂的意思:

  • [=, &a, &b]

  • [&, a, this]

对于按值方式传递的捕捉列表,其传递的值在lambda函数定义的时候就已经决定了。

而按引用传递的捕捉列表变量,其传递的值则等于lambda函数调用时的值。


第8章 融入实际应用

对齐

alignof(<struct name>)   // C++11标准定义的alignof函数可以查看数据的对齐方式

struct alignas(32) ColorVector {   // 使用C++11的alignas将该结构体对齐到32位
    double r;
    double g;
    double b;

}; 

对齐描述符alignas可以作用于各种数据,如:变量、类的数据成员等,而位域(bit field)和用register声明的变量则不可以。

其他还有:align_storage、align_unio、std::align, 这些从略。

通用属性

Unicode支持

原生字符串字面量

原生字符串字面量,即所见即所得,不再对"\n"等字符进行转义。

C++11中,程序员只需在字符串前加入前缀,即字母R,并在引号中使用小括号,即可声明该字符串为原生字符串了。

cout << R"(Hello,\nWorld!)";  // 将不再换行

(完)

猜你喜欢

转载自blog.csdn.net/nirendao/article/details/87991113