UP | HOME

CPlusPlus

Table of Contents

CPlusPlus note.

<!– more –>

CPlusPlus Language

C++基础

C++标准库

12 动态内存

动态内存与智能指针
shared_ptr

通过引用计数实现指针。

  // 空智能指针,可以指向string类型的对象
  shared_ptr<string> p1;

  // q为shared_ptr,q的引用计算器会增加
  shared_ptr<T> p(q);
  // q为内置指针,q必须指向new分配的内存,且能够转换为T*类型
  shared_ptr<T> p(q);
  // u为unique_ptr,p从u接管了对象的所有权;将u置空
  shared_ptr<T> p(u);
  // p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete
  shared_ptr<T> p(q, d);
  // p是shared_ptr p2的copy,唯一的区别是p将用可调用对象d来代替delete
  shared_ptr<T> p(p2,d);

  shared_ptr<int> p3 = make_shared<int>(42);
  shared_ptr<string> p4 = make_shared<string>(10, '9');
  shared_ptr<int> p5 = make_shared<int>();

  // p 和 q都为shared_ptr。下面赋值操作,首先会递减p的引用计数,如果计数为0则释放p所管理的内存,然后,递增q的引用计数,最后,p指向q所指对象。
  p = q;

  // 若p.use_count()为1,则返回true;否则返回false
  p.unique();

  // 返回共享对象的智能指针的数量
  p.use_count();

  // 如果p是唯一指向其对象的shared_ptr, reset会释放该对象,并将p置空;否则,p引用计数减1,并将p置空
  p.reset();
  // 如果p是唯一指向其对象的shared_ptr, reset会释放该对象,并令p指向内置指针q,初始化引用计数为1;否则,p原始的引用计数减1,并令p指向内置指针q,初始化现在的引用计数为1
  p.reset(q);
智能指针和异常

智能指针通过析构函数来管理动态分配的内存,因此,发生异常时,退出调用栈会调用局部变量的析构函数,这样可以保证智能指针管理的内存可以被释放。

智能指针的陷阱:

  • 不要使用相同的内置指针初始化(或 reset)多个智能指针。这导致多个引用计数对应一个对象。
  • 不 delete get() 返回的指针。
  • 不使用 get()初始化或 reset 另一个智能指针。
  • 如果使用了 get() 返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
  • 如果使用智能指针管理的资源不是 new 分配的内存,记住传递给它一个删除器。
unique_ptr

unique_ptr 不支持 copy 或赋值。不能 copy unique_ptr 有一个例外:我们可以 copy 或赋值一个将要被销毁的 unique_ptr。

unique_ptr<int> clone(int p)
{
    return unique_ptr<int>(new int(p));
}
unique_ptr<int> clone(int p)
{
    unique_ptr<int> ret(new int(p));
    // ...
    return ret;
}
// 空unique_ptr,可以指向类型为T的对象。u1会使用delete释放它的指针
unique_ptr<T> u1;
// u2会使用类型为D的可调用对象来释放它的指针
unique_ptr<T, D> u2;
// 空unique_ptr,可以指向类型为T的对象。用类型为D的对象d代替delete
unique_ptr<T, D> u(d);
// 释放u指向的对象,将u置为空
u = nullptr;
// u放弃对指针的控制权,返回其管理的指针,并将u置为空 
// Tips: release不会释放u所指向对象的内存空间
u.release();
// 释放u指向的对象
u.reset();
u.reset(nullptr);

// q为内置指针。下面操作会释放u指向的对象,并令u管理指针q指向的内存
u.reset(q);
weak_ptr

weak_ptr 是一种不控制所指对象生存期的智能指针,它指向由一个 shared_ptr 管理的对象。将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。

// 空weak_ptr可以指向类型为T的对象
weak_ptr<T> w; 
// sp为shared_ptr, 将w绑定到sp
weak_ptr<T> w(sp);
// 将w置空
w.reset();
// 与w共享对象的shared_ptr的数量
w.use_count();
// expired 译为 过期。 若w.use_count()为0,返回true,否则返回false
w.expired();
// 如果w.expired()为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr
w.lock();
动态数组
new 与数组
//
// === 创建动态数组 初始化动态数组 ===
//
// get_size确定分配多少个int; pia指向第一个int
// 分配一个数组会得到一个元素类型的指针
int *pia = new int[get_size()];

// 10个未初始化的int
int *pia = new int[10]; 
// 10个值初始化为0的int 
int *pia = new int[10]();  
// 10个空string
string *psa = new string[10];
// 10个空string
string *psa = new string[10]();

// 10个int,前4个用给定初始化器初始化,剩余的进行值初始化为0
int *pia = new int[10]{0,1,2,3}; 
// 10个string,前4个用给定初始化器初始化,剩余的进行值初始化为空string
string *psa = new string[10](){"a", "an", "the"};

// 动态分配一个空数组是合法的
// 下面代码是正确的,但是不能对cp进行解引用
char *cp = new char[0];

//
// === 释放动态数组
//
// pa必须指向一个动态分配的数组或空
delete [] pa;

//
// === 智能指针管理动态数组
//
unique_ptr<int[]> up(new int[10]);
// 返回up管理的指针,将up置空
// Tips: 不会释放up管理指针所指对象的内存
up.release();

unique_ptr<int[]> up1(new int[10]);
// 自动用delete []销毁其指针
up1.reset();

unique_ptr<int[]> up2(new int[10]);
// 返回u拥有的数组中位置i处的对象
up2[0];

// c++11 shared_ptr不支持数组
shared_ptr<int> sp(new int[10], [](int* p){ delete[] p; });
for(size_t i=0; i!=10; ++i)
{
    // c++11 shared_ptr不支持下标运算符,使用get获取一个内置指针
    *(sp.get()+i) = i;
}
// c++17 shared_ptr支持数组
shared_ptr<int[]> sp(new int[10]);
for(size_t i=0; i<10; i++)
{
    // c++17 shared_ptr支持下标运算符
    sp[i] = i;
}
allocator 类

allocator 类将内存分配和构造对象分开。

// 可分配string的allocator对象
allocator<string> alloc;

// 分配n个未初始化的string
// allocator分配的内存是未构造的
auto const p = alloc.allocate(n);

// p必须为类型为string的指针,指向一块原始内存;args被传递给string的构造函数,用来在p指向的内存中构造一个string对象
// 为了使用allocate返回的内存,必须用construct构造对象。使用未构造的内存,其行为是未定义的
alloc.construct(p, args);

// p必须为类型为string的指针,该方法对p所指的对象执行析构函数
alloc.destroy(p);

// 释放从p指针开始的内存,这块内存保存了n个类型为string的对象
// p必须是一个之前由allocate返回的指针,n必须为创建时要求的大小
// 调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy
alloc.deallocate(p, n);

// 从迭代器b和e指出的输入范围中copy原始到迭代器b2指定的未构造的原始内存中。b2指向的内存必须足够大,能容纳输入序列中元素的copy
uninitialized_copy(b, e, b2);
// 从迭代器b指向的元素开始,copy n个元素到b2开始的内存中。b2指向的内存必须足够大,能容纳输入序列中元素的copy
uninitialized_copy_n(b, n, b2);
// 从迭代器b和e指出的原始内存范围中创建对象,对象的值均为t的copy
uninitialized_fill(b, e, t);
// 从迭代器b指出的原始内存开始创建n个对象,对象的值均为t的copy。b指向的内存必须足够大,能容纳n元素
uninitialized_fill_n(b, e, t);

类设计者的工具

13 Copy 控制

一个类通过下面五种特殊的成员函数来控制该类型的对象拷贝、移动、赋值和销毁时做什么:

  • 拷贝构造函数
  • 拷贝赋值运算符
  • 移动构造函数
  • 移动赋值运算符
  • 析构函数
拷贝 赋值与销毁 Copy Assign And Destory

如果构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
如果类没有定义拷贝构造函数,编译器会为我们合成一个 copy 构造函数。(Tips:有其他构造函数时,不会合成默认构造函数。即使有其他构造函数,编译器也会合成拷贝构造函数)。

拷贝构造函数是为了方便地从一个已有的类对象,构造一个新的该类的对象(新对象是已有对象的克隆)。

Copy 初始化
string dots(10, '.');                   // 直接初始化
string s(dots);                         // 直接初始化
string s2 = dots;                       // 拷贝初始化
string null_book = "9-999-99999-9";     // 拷贝初始化
string nines = string(100, '9');        // 拷贝初始化

除了在使用=定义变量时,在下列情况下也发生 Copy 初始化:

  • 将一个对象作为实参传递给一个非引用类型的形参
  • 从一个返回类型为非引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
  • 初始化标准库容器,或调用其 insert 或 push 成员时,容器会对其元素进行拷贝初始化
  • 调用 emplace 时,会对创建的元素进行直接初始化

编译器可以绕过拷贝构造函数,直接创建对象,如下:

string null_book = "9-999-99999-9";     // 拷贝初始化

// 编译器可以将上面代码改写为
string null_book("9-999-99999-9");      // 编译器略过了拷贝构造函数
Copy 赋值运算符

如果类没有定义自己的拷贝赋值运算符,编译器会为其合成一个。

拷贝赋值运算符是为了方便地使用一个类对象,填充另一个类对象的内容。

class Foo
{
public:
    // 为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用
    // 标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用
    Foo& operator=(const Foo&); // 赋值运算符
}

合成的拷贝赋值运算符,会将右侧运算对象的每个非 static 成员赋予左侧运算对象的对应成员,这一工作是通过成员类型的拷贝赋值运算符来完成的。对于数组类型的成员,逐个赋值数组元素。合成拷贝赋值运算符返回一个指向其左侧运算对象的引用。

析构函数

构造函数包含一个初始化部分(初始化对象的非 static 数据成员)和一个函数体。成员初始化是在函数体指向前,按照它们在类中出现的顺序进行初始化的。
析构函数包含一个函数体和一个析构部分。析构函数中,先执行函数体,然后按照成员初始化顺序的逆序销毁成员。

析构函数没有返回值,也不接受参数。
Tips: 析构函数的函数体自身并不直接销毁成员,成员是在析构函数的函数体之后隐含的析构部分中被销毁的。

Common

什么时候需要自定义拷贝构造函数、拷贝赋值操作符、析构函数?

  • 需要析构函数的类也需要拷贝和赋值操作
  • 需要拷贝操作的类也需要赋值操作,反之亦然

将拷贝控制成员定义为 =default 可以显示地要求编译器生成合成的版本。当我们在类内用=default 修饰成员的声明时,合成的函数将隐式地声明为内联的(就像任何其他类内声明的成员函数一样)。只对成员的类外定义使用=default, 可以使合成的成员不是内联函数。

class Foo
{
public:
    Foo() = default;
    Foo(const Foo&) = default;
    Foo& operator=(const Foo&);
    ~Foo() = default;
}

// 合成的拷贝赋值操作符不是内联函数
Foo& Foo::operator=(const Foo&) = default;

使用=delete 可以指出我们希望将对应的函数定义为删除的:

class Foo
{
public:
    Foo() = default;
    Foo(const Foo&) = delete;
    Foo& operator=(const Foo&) = delete;
    ~Foo() = default;
}

Tips:
可以将任何函数(包括非拷贝控制函数)标记为 delete。将析构函数标记为删除后,编译器将不允许在栈上创建此类对象,同时无法销毁该类的对象。

下面情况下,合成的拷贝控制成员会被编译器标记为删除:

  • 类的某个成员的析构函数是删除的或不可访问的。则类的合成析构函数被定义为删除,类合成的拷贝构造函数也被定义为删除。
  • 类的某个成员的拷贝构造函数是删除的或不可访问的。则类的合成拷贝构造函数被定义为删除。
  • 类的某个成员的拷贝赋值运算符是删除的或不可访问的,或类有一个 const 的或引用成员,则类的合成拷贝赋值运算符被定义为删除。
  • 类的某个成员的析构函数是删除的或不可访问的,或是类有一个引用成员,它没有类内初始化器,或是类有一个 const 成员,它没有类内初始化器且类型未显式定义默认构造函数,则该类的合成默认构造函数被定义为删除
拷贝控制和资源管理
定义行为像值的类

编写赋值运算符时,需要注意以下两点:

  • 如果将一个对象赋予它自身,赋值运算符必须能正确工作
  • 大多数赋值运算组合了析构函数和拷贝构造函数的工作
class HasPtr
{
public:
    HasPtr(const std::string& s=std::string()):ps(new std::string(s)), i(0){}
    HasPtr(const HasPtr& p):ps(new std::string(*p.ps)), i(p.i){}
    HasPtr& operator=(const HasPtr& p);
    ~HasPtr(){ delete ps; }
private:
    std::string *ps;
    int i;
}
HasPtr& HasPtr::operator=(const HasPtr& rhs)
{
    // 拷贝底层string
    auto newp = new string(*rhs.ps);
    // 释放旧内存
    delete ps;
    // 从右侧运算对象拷贝数据到本对象
    ps = newp;
    // 返回本对象
    return *this;
}
定义行为像指针的类

下面定义一个使用引用计数的类:

class HasPtr
{
public:
    HasPtr(const std::string& s=std::string()):ps(new std::string(s)), i(0), use(new std::size_t(1)){}
    HasPtr(const HasPtr& p):ps(p.ps), i(p.i), use(p.use){ ++*use; }
    HasPtr& operator=(const HasPtr& p);
    ~HasPtr();
    // ......
private:
    std::string *ps;
    int i;
    std::size_t *use; // 用来记录有多少对象共享*ps成员
}
HasPtr::~HasPtr()
{
    if(--*use==0)
    {
        delete ps;
        delete use;
    }
}
HasPtr& HasPtr::operator=(const HasPtr& rhs)
{
    ++*rhs.use;      // 递增右侧运算对象的引用计数
    if(--*use==0)    // 递减本对象的引用计数,如果没其他用户引用,则释放对象分配的成员
    {
        delete ps;
        delete use;
    }
    ps = rhs.ps;     // 将数据从rhs拷贝到本对象
    use = rhs.use;
    return *this;
}
交换操作

对于那些与重排元素顺序的算法一起使用的类,算法在需要交换两个元素时会调用 swap 函数。如果一个类定义了自己的 swap,那么算法将使用类自定义版本。否则,算法将使用标准库定义的 swap。

Tips: swap 函数应该调用 swap,而不是 std::swap

class HasPtr
{
    friend void swap(HasPtr&,HasPtr&);
};
inline void swap(HasPtr& lhs, HasPtr& rhs)
{
    using std::swap;
    swap(lhs.ps, rhs.ps);
    swap(lhs.i, rhs.i);
}

class Foo
{
    friend void swap(HasPtr&,HasPtr&);
private:
    HasPtr hasPtr;
};
inline void swap(Foo& lhs, Foo& rhs)
{
    using std::swap;
    // 如果存在类型特定的swap版本,swap会调用与之匹配的版本。如果不存在类型特定的版本,则会使用std中的版本(假定作用域中有using声明)
    swap(lhs.hasPtr, rhs.hasPtr);
}

定义了 swap 函数的类,通常使用 swap 来实现它们的赋值运算符。其使用了一种被称作拷贝并交换 (Copy and Swap) 的技术,这种技术将左侧运算对象和右侧运算对象的一个副本进行交换,这种技术可以正确处理自赋值。

// TIPS: 参数不是引用类型
HasPtr& operator=(HasPtr rhs)
{
    swap(*this, rhs);
    return *this;
    // 退出作用域后,rhs临时变量销毁,释放了 lhs 原始内存
}

// 如果 HasPtr为值类型类, 通过拷贝构造函数创建临时变量rhs,
//      swap将lhs和rhs的内容进行交换,退出作用域后rhs 释放lhs原来的内容;
// 如果 HasPtr为指针类型类,通过拷贝构造函数创建临时变量rhs,rhs.use 引用计数增加1,
//      swap将lhs和rhs的内容进行交换,退出作用域后rhs使得原来lhs的引用计数减少1;
动态内存管理类
StrVec 设计

StrVec 是标准库 vector 类的一个简化版本。此处的简化是不使用模板,该类只用于 string。因此将其命名为 StrVec。
StrVec 使用了和 vector 一样的内存管理策略。我们将使用一个 allocator 来获得原始内存。在需要添加新元素时用 allocator 的 construct 成员在原始内存中创建对象。需要删除一个元素时,使用 destroy 成员来销毁元素。

每个 StrVec 有三个指针成员指向其元素所使用的内存:

  • elements 指向分配的内存中的首元素
  • first_free 指向最后一个实际元素之后的位置
  • cap 指向分配的内存末尾之后的位置

StrVec 有一个类型为 allocator<string>,名为 alloc 的静态成员。alloc 成员会分配 StrVec 使用的内存。

StrVec 还包含下面 4 个工具函数:

  • alloc_n_copy 会分配内存,并 copy 一个给定范围中的元素。
  • free 会销毁构造的元素并释放内存
  • chk_n_alloc 保证 StrVec 至少有容纳一个新元素的空间。如果没有空间添加新元素,chk_n_alloc 会调用 reallocate 来分配更多内存
  • reallocate 在内存用完时为 StrVec 分配新内存。

Tips:
虽然,每个 string 对象对应的字符数量不同,但是,每个 string 对象所占内存的大小是相同的。

class StrVec
{
public:
    StrVec(): // the allocator member is default initialized
        elements(nullptr), first_free(nullptr), cap(nullptr) { }
    StrVec(const StrVec&); // copy constructor
    StrVec &operator=(const StrVec&); // copy assignment
    ~StrVec(); // destructor
    void push_back(const std::string&); // copy the element
    size_t size() const { return first_free - elements; }
    size_t capacity() const { return cap - elements; }
    std::string *begin() const { return elements; }
    std::string *end() const { return first_free; }
    // ...
private:
    std::allocator<std::string> alloc; // allocates the elements
    // used by the functions that add elements to the StrVec
    void chk_n_alloc() { if (size() == capacity()) reallocate(); }
    // utilities used by the copy constructor, assignment operator, and destructor
    std::pair<std::string*, std::string*> alloc_n_copy (const std::string*, const std::string*);
    void free(); // destroy the elements and free the space
    void reallocate(); // get more space and copy the existing elements

    std::string *elements; // pointer to the first element in the array
    std::string *first_free; // pointer to the first free element in the array
    std::string *cap; // pointer to one past the end of the array
};
StrVec 实现
void StrVec::push_back(const string& s)
{
    chk_n_alloc(); // ensure that there is room for another element
    // construct a copy of s in the element to which first_free points
    alloc.construct(first_free++, s);
}

pair<string*, string*>StrVec::alloc_n_copy(const string *b, const string *e)
{
    // allocate space to hold as many elements as are in the range
    auto data = alloc.allocate(e - b);
    // initialize and return a pair constructed from data and
    // the value returned by uninitialized_copy
    // uninitialized_copy 是标准库函数,被定义在memory头文件中
    return {data, uninitialized_copy(b, e, data)};
}

void StrVec::free()
{
    // may not pass deallocate a 0 pointer; if elements is 0, there's no work to do
    if (elements) {
        // destroy the old elements in reverse order
        for (auto p = first_free; p != elements; /* empty */)
            alloc.destroy(--p);
        alloc.deallocate(elements, cap - elements);
    }
}

StrVec::StrVec(const StrVec &s)
{
    // call alloc_n_copy to allocate exactly as many elements as in s
    auto newdata = alloc_n_copy(s.begin(), s.end());
    elements = newdata.first;
    first_free = cap = newdata.second;
}

StrVec::~StrVec() { free(); }

StrVec &StrVec::operator=(const StrVec &rhs)
{
    // call alloc_n_copy to allocate exactly as many elements as in rhs
    auto data = alloc_n_copy(rhs.begin(), rhs.end());
    free();
    elements = data.first;
    first_free = cap = data.second;
    return *this;
}

void StrVec::reallocate()
{
    // we'll allocate space for twice as many elements as the current size
    auto newcapacity = size() ? 2 * size() : 1;
    // allocate new memory
    auto newdata = alloc.allocate(newcapacity);
    // move the data from the old memory to the new
    auto dest = newdata; // points to the next free position in the new array
    auto elem = elements; // points to the next element in the old array
    for (size_t i = 0; i != size(); ++i)
    {
        alloc.construct(dest++, std::move(*elem++));
    }
    free(); // free the old space once we've moved the elements
    // update our data structure to point to the new elements
    elements = newdata;
    first_free = dest;
    cap = elements + newcapacity;
}
对象移动

使用对象移动的情形:

  1. vector 实现中,当内存空间不够,在重新分配内存后,从旧内存将元素移动到新内存(此时,使用 Copy 是不必要的)。
  2. IO 类或 unique_ptr 都包含不能被共享的资源(如指针或 IO 缓冲)。因此,这些类型的对象不能拷贝但可以移动。
右值引用

不能将左值引用绑定到要求转换的表达式、字面常量或是返回右值的表达式。
不能将右值引用直接绑定到一个左值上,可以将右值引用绑定到要求转换的表达式、字面常量或是返回右值的表达式。

Tips:
左值和右值是表达式的属性。一些表达式生成会要求左值,而另外一些则生成会要求右值。一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式。
返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都是生成右值。
变量可以看作只有一个运算对象而没有运算符的表达式,变量表达式都是左值。

int i = 42;
int &r = i;             // 正确:r引用i
int && rr = i;          // 错误:不能将一个右值引用绑定到一个左值上
int &r2 = i*42;         // 错误:i*42是一个右值
const int &r3 = i*42;   // 正确:可以将一个const的引用绑定到一个右值上
int &&rr2 = i*42;       // 正确:将rr2绑定到乘法结果上

int &&rr1 = 42;         // 正确:字面常量是右值
int &&rr2 = rr1;        // 错误:表达式rr1是左值!  解释:rr1是右值引用类型的变量,而变量是左值,不能将右值引用绑定到左值上
标准库 move 函数

标准库函数 std::move 可以将一个左值转换为其对应的右值。

// 为了避免潜在的名字冲突,此处使用std::move而不是move
int && rr3 = std::move(rr1);

我们可以销毁一个移后源对象,可以给它赋新值,但是不能使用一个移后源对象的值。

移动构造函数和移动赋值运算符

移动构造函数和移动赋值运算符从给定对象“窃取”资源而不是拷贝资源。除了完成资源移动,必须确保移后源对象处于销毁它是无害的状态,移动源对象必须处于有效状态,但是用户不能对其值进行任何假设。

StrVec::StrVec(StrVec&& s) noexcept
: elements(s.elements), first_free(s.first_free), cap(s.cap)
{
    // 令s进入这样的状态——对其运行析构函数是安全的
    s.elements = s.first_free = s.cap = nullptr;
}

StrVec& StrVec::operator=(StrVec && rhs) noexcept
{
    // 直接检测自赋值
    if(this!=&rhs)
    {
        free();
        elements = rhs.elements;
        first_free = rhs.first_free;
        cap = rhs.cap;
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
    return *this;
}
  • 什么时候调用移动构造函数和移动赋值运算符

    当提供给构造或赋值函数的参数是右值时,才会调用移动版本的构造函数和赋值运算符

  • 为什么移动构造函数不需要处理自移动问题?

    因为移动构造函数 lhs 是新构造的对象,rhs 是已存在对象,lhs 肯定不可能等于 rhs,故不需要判断 lhs 和 rhs 的相等的问题。

合成的移动操作

只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非 static 数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。

下面情况下,合成的移动操作被定义为删除:

  • 有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符的情况类似。
  • 有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的。
  • 类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。
  • 有类成员是 const 的或是引用类型的,则类的移动赋值运算符被定义为删除的。

Tips:定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些合成的成员默认地被定义为删除的。

移动右值,拷贝左值。但如果没有移动构造函数,右值也被拷贝。

class HasPtr{
public:
    HasPtr(HasPtr&& p) noexcept : ps(p.ps) {p.ps = 0;}
    // 赋值运算符既是移动赋值运算符,也是拷贝赋值运算符
    HasPtr& operator=(HasPtr rhs)
    {
        swap(*this, rhs);
        return *this;
    }
}

// 上面的赋值运算符有一个非引用参数,因此参数要进行拷贝初始化。依赖于实参的类型,拷贝初始化要么使用拷贝构造函数,要么使用移动构造函数(左值被拷贝,右值被移动)
hp = hp2;              // hp2是一个左值; hp2通过拷贝构造函数来拷贝
hp = std::move(hp2);   // std::move将一个右值引用绑定到hp2上,因此实参是一个右值引用,所以移动构造函数是精确匹配的。
移动迭代器

标准库函数 make_move_iterator 函数可以将一个普通迭代器转换为一个移动迭代器。移动迭代器的解引用运算符生成一个右值引用。
uninitialized_copy 对输入序列中的每个元素调用 construct 来将元素“拷贝或移动”到目的位置。下面代码中,我们传递给他的是移动迭代器,因此解引用运算符生成的是一个右值引用,这意味着 construct 将使用移动构造函数来构造元素。

  void StrVec::reallocate()
  {
      auto newcapacity = size() ? 2 * size() : 1;
      auto first = alloc.allocate(newcapacity);
      auto last = uninitialized_copy(
                                     make_move_iterator(begin()),
                                     make_move_iterator(end()),
                                     first
                                     );

      free();
      elements = first;
      first_free = last;
      cap = elements + newcapacity;
  }
右值引用和成员函数

区分移动和拷贝的成员函数通常,拷贝版本的成员函数接受一个 const T&参数(拷贝版本不需要修改实参的值,因此前面有 const 限定),移动版本的成员函数接受一个 T&&参数。

class StrVec{
public:
    void push_back(const std::string&);  // 拷贝元素
    void push_back(std::string&&);       // 移动元素
};

引用限定符&或&&,分别用于指出 this 可以指向一个左值或右值。类似于 const 限定符,引用限定符只能用于非 static 成员函数,且必须同时出现在函数的声明和定义中。
&限定的函数,我们只能将它用于左值;&&限定的函数,只能用于右值。

class Foo{
public:
    Foo &operator=(const Foo&) &;    // 只能向可修改的左值赋值
};

一个函数可以同时用 const 和引用限定。在此情况下,引用限定符必须跟随在 const 限定符之后:

class Foo{
public:
    Foo someMembFunc() & const;        // 错误
    Foo anotherMembFunc() const &;     // 正确
};

成员函数可以根据是否有 const 来区分重载版本,引用限定符也可以区分重载版本,还可以综合引用限定符和 const 来区分成员函数的重载版本。
Tips:

  • 如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。
Common

五个拷贝控制成员应该看作一个整体:一般来说,如果类定义了任何一个拷贝操作,它就应该定义所有五个操作。如前所述,某些类拥有一个资源,而拷贝成员必须拷贝此资源,必须定义拷贝构造函数、拷贝赋值操作符和析构函数这些类才能正确工作。一般来说,拷贝一个资源会导致一些额外开销。在这种拷贝并非必要的情况下,定义了移动构造函数和移动赋值运算符的类就可以避免此问题。

在移动构造函数和移动赋值运算符这些类实现代码之外的地方,只有当你确信需要进行移动操作且移动操作是安全的,才可以使用 std::move。

16 模板与泛型编程

利用 OOP 的多态和模板编程都可以实现一套代码处理多种类型。他们的不同之处在于:
OOP 多态处理运行起来传入的函数参数的类型可以不同,但是这些不同类型属于同一个继承树,函数形参的类型为基类。
模板编程则利用编译器在编译的时候为不同类型生成不同的实例。

定义模板
函数模板

函数模板通过函数调用的实参推导出函数的形参类型,进而推导出模板实参,从而可以将模板实参绑定到模板参数。

template<typename T>
int compare(const T& v1, const T& v2)
{
    if(v1 > v2) return 1;
    else if(v1 < v2) return -1;
    else return 0;
}

// 函数实参类型为int --> 函数形参类型为const int& --> 模板实参为int --> 模板参数为 int
// 编译器使用推断出的模板参数实例化一个特定版本的函数 int compare(const int& v1, const int& v2);
cout << compare(1, 0) << endl;
  • 模板参数

    模板参数分为模板类型参数和非类型模板参数。

    // T 为模板类型参数
    template<typename T>
    void foo()
    {
        // .......
    }
    
    // N和M为非类型模板参数
    // 在模板定义内,非类型模板参数是常数值,在需要常量表达式的地方可以使用非类型模板参数(例如,用于指定数组大小)
    // TIPS: 非类型模板参数的模板实参必须是常量表达式
    template<unsigned N, unsigned M>
    int compare(const char (&p1)[N], const char (&p2)[M])
    {
        return strcmp(p1, p2);
    }
    
    // 编译器实例化的特定版本为 int compare(const char (&p1)[3], const char (&p1)[4]);
    cout << compare("hi", "mom") << endl;
    
  • inline constexpr 修饰符

    可以使用 inline constexpr 修饰模板函数,修饰符放在模板参数列表之后返回值类型之前

    template<typename T> inline T min(const T&, const T&);
    
    // 下面模板函数返回数组大小
    template<unsigned N, typename T>
    constexpr unsigned array_len(const T (&arr)[N])
    {
        cout << N << endl;
        return N;
    }
    array_len("hi");
    
类模板

类模板是生成类的蓝图,和函数模板不同的是,编译器无法为类模板推断模板参数类型。因此实例化类模板时,必须指定模板实参。

  • 定义类模版

    当使用类模板类型时,必须提供模板实参,但这一规则有一个例外,在类模板自己的作用域中,可以直接使用模板名而不提供实参。在类模板外定义成员时,并不在类的作用域中,直到遇到类名才表示进入类的作用域。

    template <typename T> class Blob
    {
    public:
        typedef T value_type;
        typedef typename std::vector<T>::size_type size_type;
        // constructors 
        Blob();
        Blob(std::initializer_list<T> il);
        // number of elements in the Blob
        size_type size() const { return data->size(); }
        bool empty() const { return data->empty(); }
        // add and remove elements
        void push_back(const T &t) {data->push_back(t);}
        // move version; see § 13.6.3 (p. 548)
        void push_back(T &&t) { data->push_back(std::move(t)); } void pop_back();
        // element access
        T& back();
        T& operator[](size_type i); // defined in
    private:
        std::shared_ptr<std::vector<T>> data;
        // throws msg if data[i] isn't valid
        void check(size_type i, const std::string &msg) const;
    };
    
    template <typename T>
    void Blob<T>::check(size_type i, const std::string &msg) const
    {
        if (i >= data->size())
            throw std::out_of_range(msg);
    }
    
    template <typename T>
    class BlobPtr
    {
    public:
        BlobPtr(): curr(0) { }
        BlobPtr(Blob<T> &a, size_t sz = 0): wptr(a.data), curr(sz) { }
        T& operator*() const
        {
            auto p = check(curr, "dereference past end");
            return (*p)[curr]; // (*p) is the vector to which this object points
        }
        // increment and decrement 注意 下面两个函数的返回值类型省略了模板参数
        BlobPtr& operator++(); // prefix operators
        BlobPtr& operator--();
    private:
        // check returns a shared_ptr to the vector if the check succeeds std::shared_ptr<std::vector<T>>
        check(std::size_t, const std::string&) const;
        // store a weak_ptr, which means the underlying vector might be destroyed std::weak_ptr<std::vector<T>> wptr;
        std::size_t curr; // current position within the array
    };
    
    // 注意 下面函数的返回值类型不在类作用域内,不能省略模板参数
    BlobPtr<T> BlobPtr<T>::operator++(int)
    {
        // no check needed here; the call to prefix increment will do the check
        BlobPtr ret = *this; // save the current value  Tips: 在类作用域内可省略模板参数
        ++*this; // advance one element; prefix ++ checks the increment
        return ret; // return the saved state
    }
    
  • 类模板和友元

    下面代码将 BlobPtr 和 模板版本的 Blob 相等运算符定义为 Blob 的友元:

    // 模板1对1友元,需要对模板友元进行前置声明
    template <typename> class BlobPtr;
    template <typename> class Blob;
    bool operator==(const Blob<T>&, const Blob<T>&);
    
    template <typename T>
    class Blob
    {
        // each instantiation of Blob grants access to the version of
        // BlobPtr and the equality operator instantiated with the same type
        friend class BlobPtr<T>;
        friend bool operator==<T> (const Blob<T>&, const Blob<T>&);
        // ......
    };
    
    Blob<char> a;    // BlotPtr<char> 和 bool operator==(const Blob<char>&, const Blob<char>&) 都是 Blob<char> 的友元。
    
    // Pal 模板类的前置声明
    template <typename T> class Pal;
    class C
    {
        // Pal模板类的Pal<C>实例是类C的友元,因此需要前置声明
        friend class Pal<C>; 
        // Pal2模板类的所有实例都是类C 的友元,因此不需要前置声明
        template <typename T> friend class Pal2;
    };
    
    // Pal 模板类的前置声明
    template <typename T> class Pal;
    template <typename T> class C2
    {
        // Pal模板类的Pal<C>实例是类C2的友元,因此需要前置声明
        friend class Pal<T>;
        // Pal2 目标类的所有实例都是类C2 的友元,因此不需要前置声明
        template <typename X> friend class Pal2;
        // Pal3 为非模板类,因此不需要前置声明
        friend class Pal3;
    };
    
  • 模板类型别名
    template<typename T> using twin = pair<T, T>;
    twin<string> authors; // authors is a pair<string, string>
    twin<int> win_loss;   // win_loss is a pair<int, int>
    twin<double> area;    // area is a pair<double, double>
    
    template <typename T> using partNo = pair<T, unsigned>;
    partNo<string> books; // books is a pair<string, unsigned>
    partNo<Vehicle> cars; // cars is a pair<Vehicle, unsigned>
    partNo<Student> kids; // kids is a pair<Student, unsigned>
    
  • 类模板 static 成员
    template <typename T> class Foo
    {
    public:
        static std::size_t count() { return ctr; }
    
        // other interface members private:
        static std::size_t ctr;
    };
    
    // 定义并初始化静态成员
    template <typename T> size_t Foo<T>::ctr = 0;
    
模板参数

模板参数的作用域遵循普通作用域规则。一个模板参数名的可用范围是在其声明之后,到模板声明或定义结束之前。与任何其他名字一样,模板参数名会隐藏外层作用域中声明的相同名字。

模板声明必须包含模板参数,但是模板参数的名字可以和定义中不同。

  • 模板参数的类型成员

    假设 T 为模板参数,当编译器遇到 T::mem 这样的代码时,无法识别 T::mem 为类型成员,还是一个 static 数据成员。但是,为了处理模板编译器必须确定 T::mem 到底是哪一者。这样才能确认 T::mem * p; 这样的代码是定义一个 T::mem 类型的指针对象,还是计算 T::mem 和 p 的乘积。为此,编译器默认 T::mem 为数据成员,如果需要指定 T::mem 为类型成员,需要在 T::mem 前加 typename。

    template <typename T>
    typename T::value_type top(const T& c)
    {
        if (!c.empty())
            return typename T::value_type();
        else
            return c.back();
    }
    
  • 默认模板实参

    函数模板默认模板实参

    template <typename T, typename F = less<T>>
    int compare(const T &v1, const T &v2, F f = F())
    {
        if (f(v2, v1)) return 1;
        if (f(v1, v2)) return -1;
        return 0;
    }
    
    bool i = compare(0, 42); // uses less; i is -1 // result depends on the isbns in item1 and item2
    Sales_data item1(cin), item2(cin);
    bool j = compare(item1, item2, compareIsbn);
    

    类模板默认模板实参

    template <class T = int>
    class Numbers
    {
    public:
        Numbers(T v = 0): val(v) { }
    private:
        T val;
    };
    
    Numbers<long double> lots_of_precision;
    Numbers<> average_precision;
    
成员模板
控制实例化

当模板被使用的时候才会实例化,这一特性意味着,当两个或多个独立编译的源文件你用了相同的模板,并提供了相同的目标参数时,每个文件中都有该模板的一个实例。
在大系统中,在多个文件中实例化相同模板实例的额外开销可能非常严重。可以通过显示实例化来避免这种开销。一个显示实例化有如下形式:

extern template class Blob<string>; // 实例化声明
template Blob<string>;              // 实例化定义

当编译器遇到实例化声明不会在该文件中生成实例化代码。将一个实例化声明为 extern 就表示承诺在程序的其他位置有该声明对应的实例化定义。在使用模板之前没有对模板进行声明,编译器就会自动对其进行实例化。

Tips: 实例化定义会实例化所有成员。

模板实参推断
类型转换

和非模板函数一样,调用模板函数时,传递给函数模板的实参被用来实例化函数的形参。如果一个函数形参的类型使用了模板类型参数,则该形参的初始化使用特殊规则。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。

与往常一样,顶层 const 无论是在形参中还是在实参中,都会被忽略。其他转换中,能在调用中应用于函数模板的包括以下两项:

  1. const 转换:
  2. 数组或函数指针的转换:
void f1(const int arg)
{
    std::cout << arg;
}
void f2(int arg)
{
    std::cout << arg;
}
void f3(const int& arg)
{
    std::cout << arg;
}
void f4(const int* arg)
{
    std::cout << *arg;
}
int i=10;
const int j=10;
const int& k = 10;
f1(i);    // 形参中的顶层const 被忽略。值传递,可以使用int i初始化 const int 类型的形参 arg
f2(j);    // 实参中的顶层const被忽略。值传递,可以使用const int j 初始化 int 类型的形参 arg
f2(k);    // 实参中的顶层const被忽略。值传递,可以使用const int& k 初始化 int 类型的形参 arg
f3(i);    // 形参中的顶层const被忽略。引用传递,可以使用int j 初始化 const int& 类型的形参 arg
重载与模板
可变参数模板

可以接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包,表示零个或多个函数参数。

模板参数列表中,class… 或 typename… 指出接下来的参数表示零个或多个类型的列表;
一个类型后面跟一个省略号表示零个或多个给定类型的非类型参数的列表;
在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。

template<typename T, typename... Args> 
void foo(const T& t, const Args&... reset);

int i=0; double d=3.14; string s="hi";
// 编译器从实参推断模板参数类型,以及参数包中参数的数目。
foo(i, s, 42, d);     // 包中有3个参数  编译器实例化版本void foo(const int&, const string&, const int&, const double&)
foo(i, s, "hi");      // 包中有2个参数  编译器实例化版本void foo(const int&, const string&, const char[3]&)
foo(i, s);            // 包中有1个参数  编译器实例化版本void foo(const int&, const string&)
foo(i);               // 空包            编译器实例化版本void foo(const int&)

sizeof… 运算符用于获得包中元素数量

template<typename ... Args> 
void g(Args... args)
{
    std::count << sizeof...(Args) << std::endl;  // 类型参数的数目
    std::count << sizeof...(args) << std::endl;  // 函数参数的数目
}
支持可变参数的 print 函数
template<typename T>
ostream& print(ostream& os, const T& t)
{
    return os << t;
}
template<typename T, typename... Args>  // 模板参数包
ostream& print(ostream& os, const T& t, const Args&... rest)  // 函数参数包
{
    os << t << ", ";
    // 可变参数版本的print函数接受三个参数:一个ostream&,一个const T&和一个参数包
    // 此处调用只传递了两个实参,因此rest中的第一个实参被绑定到t,剩余实参形成下一个print调用的参数包
    return print(os, rest...);
}

print(cout, 1, "hi", 42); // 包中有两个参数
// 上面的函数会按下面方式递归执行:
//                                  t       rest...       output
// print(cout, i, "hi", 42)         1       "hi", 42      1
// print(cout, "hi", 42)            "hi"    42            hi
// print(cout, 42)                                        42          // 调用非可变参数版本的print
包展开 Pack Expansion

展开一个包就是将它分解为构成的元素,展开一个包时,我们要提供用于每个展开元素的模式。对每个元素应用模式,获得展开后的列表。

template<typename T, typename... Args>
// 下面函数形参数部分(const Args&... rest) ,展开了模板参数包Args,为print生成函数参数列表
// 模式是const Args&,编译器将模式应用到模板参数包Args中的每个元素,得到一个逗号分割的零个或多个类型的列表,每个类型都形如const type&
// 例如:print(cout, 1, "hi", 42); 被实例化为 ostream& print(ostream&, const int&, const char[3]&, const int&)
ostream& print(ostream& os, const T& t, const Args&... rest) 
{
    os << t << ", ";
    // 下面函数实参部分(rest...),展开了函数参数包rest,为print调用生成实参列表
    // 模式是函数参数包的名字(rest),此模式展出一个由包中元素组成的,逗号分割的列表。
    return print(os, rest...);  // 展开
}

template<typename... Args>
ostream& errorMsg(ostream& os, const Args&... rest)
{
    // 模式是debug_rep(rest),此模式表示对函数参数包rest中的每个元素调用debug_rep。展开结果将是一个逗号分割的debug_rep调用列表
    // 例如: errorMsg(cerr, fncName, code.num(), otherData, "other", item); 被编译器变为
    // errorMsg(cerr, debug_rep(fncName), debug_rep(code.num()), debug_rep(otherData), debug_rep("other"), debug_rep(item));
    return print(os, debug_rep(rest)...);
}

Tips: 展开包的模式会独立地应用于包中的每个元素

转发参数包
template<class... Args>
inline void StrVec::emplace_back(Args&&... args) //将Args扩展为一个右值引用的列表
{
    chk_n_alloc();
    // 下面调用的展开为 std::forward<Args>(args)...,其既展开了模板参数包Args,也展开了函数参数包args.
    // 此模式生成如下形式的元素 std::forward<Ti>(ti) Ti表示模板参数包中第i个元素的类型,ti表示函数参数包中第i个元素
    alloc.construct(first_free++, std::forward<Args>(args)...);
}

高级主题

19 特殊工具与技术

控制内存分配
重载 new 和 delete
// new 表达式的工作机制
// 第一步 new表达式调用一个名为operator new或operator new[] 的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或者对象的数组)
// 第二步 编译器运行相应的构造函数以构造这些对象,并为其传入初始值
// 第三步 对象被分配了空间并构造完成,返回一个指向该对象的指针
string *sp = new string("a value");
string *arr = new string[10];

// delete表达式的工作机制
// 第一步 对sp所指的对象或者arr所指的数组中的元素执行对应的析构函数
// 第二步 编译器调用名为operator delete 或 operator delete[] 的标准库函数释放内存空间。
delete sp;
delete[] arr;

用户可以自定义 operator new 函数和 operator delete 函数。编译器对 operator new 函数和 operator delete 函数的选择按照如下规则:

  1. 先在类和基类作用域中查找
  2. 再在全局作用域中查找
  3. 前面都没找到,则使用标准库中的版本

Tips: 可以使用域运算符忽略类中的函数。::new ::delete 会直接在全局作用域中查找匹配的函数

重载 operator new 函数与 operator delete 函数的规则:

  • operator new 函数与 operator delete 函数是隐式静态函数,即使不显示地声明 static,其也为静态函数。因此,它们不能操作类的任何数据成员。
  • operator new 函数和 operator new[] 函数返回类型必须是 void*,第一个形参的类型必须是 size_t 且该形参不能含有默认实参,其为申请的内存空间的字节数。我们可以为这两个函数提供额外的形参。此时,用到这些自定义函数的 new 表达式需要使用 placement new 将实参传给新增的形参。
  • void* operator new(size_t, void*); 不允许重新定义这个版本,该形式只供标准库使用。
  • operator delete 和 operator delete[] 的返回类型必须为 void, 第一个形参的类型必须是 void*。
  • 我们将 operator delete 和 operator delete[]定义为类成员函数时,该函数可以包含另外一个类型为 size_t 的形参。此时,该形参的初始值是第一个形参所指对象的字节数。size_t 形参可用于删除继承体系中的对象。如果基类有一个虚析构函数,则传递给 operator delete 的字节数将因待删除指针所指对象的动态类型不同而有所区别。
// 标准库中默认的operator new 和 operator delete函数
void* operator new(size_t);
void* operator new[](size_t);
void  operator delete(void*) noexcept;
void  operator delete[](void*) noexcept;

void* operator new(size_t, nothrow_t&) noexcept;
void* operator new[](size_t, nothrow_t&) noexcept;
void  operator delete(void*, nothrow_t&) noexcept;
void  operator delete[](void*, nothrow_t&) noexcept;
// 下面为operator new 和 operator delete的简单实现方式
void *operator new(size_t size)
{
    if(void* mem = malloc(size))
    {
        return mem;
    }
    else
    {
        throw bad_alloc();
    }
}

void operator delete(void *mem) noexcept
{
    free(mem);
}
  • delete[] 如何知道需要调用多少次析构函数的?

    c++使用 new SomeType[elemCount] 为数组分配内存空间时,会多分配 4 个字节,并用开始的 4 个字节存放数组元素数量 elemCount。 调用 delete[] ptr 时,就会调用 elemCount 次析构函数。

  • delete[] 如何知道需要释放多少内存空间的?

    delete[] 将内存空间的指针向前偏移 4 字节,传递给 free 函数,free 函数控制释放多少内存空间。

  • free 函数如何知道需要释放多少内存空间的?

    malloc 申请内存时,会申请额外的空间来记录申请内存的大小。调用 free 时,通过读取记录的信息来释放内存。

placement new

对于 operator new 分配的内存空间来说,我们无法使用 construct 函数构造对象,应该使用 placement new 形式构造对象。
placement new 有如下几种形式:
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size]{ braced initializer list }

placement new 允许我们在一个特定的、预先分配的内存地址上构造对象。
placement new 和 allocator 的 construct 成员非常相似,但在它们之间也有一个重要的区别。传给 construct 的指针必须指向同一个 allocator 对象分配的空间,但是传给 placement new 的指针无须指向 operator new 分配的内存。placement new 表达式的指针甚至不需要指向动态内存。

显示调用析构函数与使用 allocate 的 destroy 很类似。调用析构函数可以清除给定的对象但是不会释放对应的内存空间。

运行时类型识别
dynamic_cast 运算符

dynamic_cast 运算符有以下几种形式:
dynamic_cast<type*>(pObj) / 转换失败返回 0
dynamic_cast<type&>(refObj) /
转换失败抛出 bad_cast 异常
dynamic_cast<type&&>(rrefObj) // 转换失败抛出 bad_cast 异常

typeid

typeid(e) //e 可以是任意表达式或类型的名字。

  • 如果表达式是一个引用,则 typeid 返回该引用所引对象的类型。
  • typeid 作用域数组或函数时,并不执行向指针的标准类型转换。因此,如果对数组 a 执行 typeid(a),则所得的结果是数组类型而非指针类型
  • typeid 作用于指针时,返回的结果是该指针的静态编译时类型

typeid 操作的结果是一个常量对象的引用,该对象的类型是标准库类型 type_info 或者 type_info 的公有派生类型。

#include <iostream>
#include <typeinfo>
using namespace std;
void f()
{
    const auto& t_info = typeid(t);
    cout << t_info.name() << endl;
}
使用 RTTI

使用 RTTI 来实现具有继承关系的类实现相等运算符

class Base
{
    friend bool opertator=(const Base& lhs, const Base& rhs)
    { return typeid(lhs)==typeid(rhs) && lhs.equal(rhs); }
public:
    // Base的接口成员
protected:
    //
    virtual bool equal(const Base&) const
    {
        // 执行比较两个Base对象的操作并返回结果
    }
}

class Derived: public Base
{
public:
    // Derived的其他接口成员
protected:
    bool equal(const Base&) const
    {
        auto r = dynamic_cast<const Derived&>(rhs);
        // 执行比较两个Derived对象的操作并返回结果
    }
}
type_info 类

type_info 类对象支持如下操作:

t1 == t2         // t1和t2表示同一种类型,返回true
t1 != t2         // 
t.name()         // 返回一个C分格字符串,表示类型名称
t1.before(t2)    // 返回一个bool值,表示t1是否位于t2之前。before所采用的顺序关系是依赖于编译器的
枚举类型
// 下面为不限定作用域的枚举类型
enum color
{
    red,
    yellow,
    green
};

// 指定enum元素的类型,默认类型为int
enum IntValues : unsigned long long
{
    charType = 255,
    shortType = 65535,
    intType = 65535,
    longType = 9294967295UL,
    longLongType = 18446744073709551615ULL,
};

// 下面为限定作用域的枚举类型
enum class open_modes
{
    input,
    output,
    append
};
enum class EnumChar : unsigned char
{ kEnum1, kEnum2 }

// 枚举类型的前置声明,声明和定义必须一致
enum color::int;                     // 不限定作用域的,必须指定成员类型
enum IntValues : unsigned long long; //

enum class open_modes;               // 正确,此处声明成员类型默认为int,和定义一致
enum class EnumChar;                 // 错误,此处声明成员类型默认为int,定义的地方为unsinged char
enum class EnumChar : unsigned char; // 正确

Effective Modern C++

Deducing Types

Understand template type deduction

// 下面为模板函数的声明
// ParamType 有以下三种情况
// case 1 : ParamType 为指针或引用(非 universal 引用)
// case 2 : ParamType 为universal 引用
// case 3 : ParamType 既不是指针也不是引用
template<typename T>
void f(ParamType param);  

// 下面为模板函数的实例化
f(expr);
ParamType 为指针或引用
/////////////////////
// subcase 1
template<typename T>
void f(T& param);     // ParamType 为引用

int x = 27;           // x 为 int
const int cx = x;     // cx 为 const int
const int& rx = x;    // rx 为 x的const别名

f(x);    // ParamType为int&       -- T 为int
f(cx);   // ParamType为const int& -- T 为const int
f(rx);   // ParamType为const int& -- T 为const int

/////////////////////
// subcase 2
template<typename T>
void f(const T& param);     // ParamType 为引用

int x = 27;                 // x 为 int
const int cx = x;           // cx 为 const int
const int& rx = x;          // rx 为 x的const别名

f(x);           // ParamType为 constint& -- T 为int
f(cx);          // ParamType为const int& -- T 为int
f(rx);          // ParamType为const int& -- T 为int

/////////////////////
// subcase 3
template<typename T>
void f(T* param);           // ParamType 为引用

int x = 27;                 // x 为 int
const int* cx = &x;         // cx 为 const int*

f(&x);                      // ParamType为       int* -- T 为 int
f(cx);                      // ParamType为 const int* -- T 为 const int
ParamType 为 universal 引用
template<typename T>
void f(T&& param);       // ParamType 为universal引用

int x = 27;              // x 为 int 左值
const int cx = x;        // cx 为 const int
const int& rx = x;       // rx 为 const int&

f(x);                    // x为左值ParamType为int&        -- T 为int&
f(cx);                   // cx为左值ParamType为const int& -- T 为const int&
f(rx);                   // rx为左值ParamType为const int& -- T 为const int&
f(27);                   // 27为右值ParamType为int&&      -- T 为int
ParamType 既不是指针也不是引用
template<typename T>
void f(T param);       // ParamType 为既不是指针也不是引用

int x = 27;              // x 为 int 左值
const int cx = x;        // cx 为 const int
const int& rx = x;       // rx 为 const int&

f(x);                    // ParamType为int -- T 为int
f(cx);                   // ParamType为int -- T 为int
f(rx);                   // ParamType为int -- T 为int
f(27);                   // ParamType为int -- T 为int

auto

From C++98 to C++10 and C++14

Smart Points

Rvalue Reference, Move Semantics and Perfect Forwarding

Lambda Expression

The Concurrency API

Misc

Usage

List

按值删除元素
_lessonList.erase(std::remove(_lessonList.begin(), _lessonList.end(), lesson));

如何显示类的内存布局?

选中指定 cpp 文件,右键属性,C/C++ | 命令 | 其他选项中,输入下面命令
/d1 reportAllClassLayout
/d1 reportSingleClassLayoutXXX (XXX 为类名)

参考资料