Table of Contents


第10章 泛型算法

10.1 概述

#include <algorithm>
#include <numeric>

10.2 初识泛型算法

int sum = accumulate(vec.begin(), vec.end(), 0);
string sum = accumulate(vec.begin(), vec.end(), string(""));

fill(vec.begin(), vec.end(), 0);
fill_n(vec.begin(), size, 0);	// 不能对空容器写
auto ret = copy(vec1.begin(), vec1.end(), vec2.begin());	// 返回vec2递增后的迭代器
replace(vec.begin(), vec.end(), 0, 10);

sort(vec.begin(), vec.end());
auto ret = unique(vec.begin(), vec.end());	// 返回不重复元素后的第一个位置迭代器

10.3 定制操作

lambda 表达式

[capture list](parammeter list) -> return type {function body}

可以忽略参数列表和返回类型

如果函数体只有一个return语句,返回类型由返回表达式类型推断,否则返回void

只对lambda所在函数中的非static变量使用捕获列表

[](const string& a, const string& b){return a.size() < b.size();}
[sz](const string& a){return a.size() < sz;}
[](const string& a){cout << a;}

lambda包含值捕获与引用捕获。值捕获对值进行拷贝,在创建lambda对象时进行拷贝。默认情况下值捕获不会改变其值,可以加上mutable改变。lambda捕获的都是局部变量,因此引用捕获时需保证局部变量始终存在。

auto f = [v1]() mutable {return ++v1;}

lambda函数体包含除return之外其他语句,需显示的指明返回类型

[](int i) -> int {if (i < 0) return -i; else return i;}

绑定函数

find_if只接受单变量函数,而check_size有两个变量,可以通过bind函数解决

#include <functional>
using namespace std::palceholders;
bool check_size(const string& s, int sz){return s.size() >= sz;}
find_if(vec.begin(), vec.end(), bind(check_size, _1, sz));

10.4 再探迭代器

插入迭代器

  • back_inserter
  • front_inserter
  • inserter
copy(vec1.begin(), vec1.end(), back_inserter(vec2));

反向迭代器

  • reverse_iterator
string line = "abc def ghi";
auto r = find(line.rbegin(), line.rend(), ' ');
for(auto it = r.base(); it != line.end(); it++)
    cout << *it;
// output: ghi

第12章 动态内存

12.1 动态内存与智能指针

智能指针负责自动的释放对象

shared_ptr

智能指针属于模板,默认初始化为空指针

#include <memory>
shared_ptr<string> p;

最安全的分配和使用动态内存的方法是调用make_shared函数

shared_ptr<string> p = make_shared<string>(2, '9');
auto p = make_shared<string>(2, '9');

每个shared_ptr有一个相关联的引用计数,计数器会根据情况递增或递减。当计数器为0时,指针就会自动释放管理的对象和相关联的对象

auto r = make_shared<int>(42);
r = q;	// 递增q引用计数,递减r引用计数

shared_ptr可以实现程序在多个对象间共享数据

class StrBlob{
public:
    StrBlob();
private:
    shared_ptr<vector<string>> data;
};

当对StrBlob进行拷贝、赋值时,它的shared_ptr成员被拷贝、赋值,实现多个对象共享data数据。

直接管理内存

可以直接用newdelete管理内存,但非常容易出错

int *p = new int(10);
const int *pc = new const int(10);
delete p;
delete pc;

shared_ptr与new结合

接受指针参数的智能指针构造函数是explicit的,我们不能进行内置指针到智能指针间的隐式转换,因此必须使用直接初始化方式

shared_ptr<int> p1 = new int(10);	// wrong
shared_ptr<int> p2(new int(10));	// right

shared_ptr<int> clone(int p){
	return shared_ptr<int>(new int(p));
}

避免智能指针与普通指针混合使用

if (!p.unique())
	p.reset(new string(*p))	// 不唯一,分配新的拷贝
*p += newVal;	// 唯一,可以直接改变对象

unique_ptr

unique_ptr不支持拷贝和赋值,但可以将指针的所有权从一个非constunique_ptr转移到另一个

unique_ptr<string> p2(p1.release());
p3.reset(p1.release());

不能拷贝和赋值unique_ptr有一个例外:可以拷贝和赋值一个将要被销毁的unique_ptr,例如从函数返回的unique_ptr

unique_ptr<int> clone(int p){
	return unique_ptr<int>(new int(p));
}

weak_ptr

weak_ptr指向一个shared_ptr所管理的对象,并且不改变shared_ptr的引用计数

创建weak_ptr时,要用shared_ptr初始化

auto p = make_shared<int>(10);
weak_ptr<int> wp(p);

weak_ptr的对象可能不存在,因此需要调用lock检查对象是否依然存在

if(shared_ptr<int> np = wp.lock()){
	// 当对象存在,lock返回true时,进入函数体
}

12.2 动态数组

#allocator类

new操作执行内存分配与对象构造,有时我们并不需要构造,只需内存分配与对象初始化,此时可以用allocator模板。

#include <memory>
allocator<string> alloc;
auto const p = alloc.allocate(n);	// 分配未初始化的内存
auto q = p;
alloc.construct(q++, "hi");
alloc.construct(q++, 10, 'c');		// 在内存上构造
while(q != p)
	alloc.destroy(--q);		// 释放构造的内存
alloc.deallocate(p,n);			// 释放原始内存

auto p = alloc.allocate(v.size(), 2);
auto q = uninitialized(v.begin(), v.end(), p);	// 将v中元素拷贝至p
uninitialized_fill_n(q, v.size(), 10);		// 将q中元素赋值10

第13章 拷贝控制

13.1 拷贝、赋值与销毁

拷贝构造函数第一个参数是引用类型,几乎总是一个const引用,通常不是explicit

class Foo{
public:
	Foo();
	Foo(const Foo&);
}

拷贝初始化产生情况:

  • =定义变量
  • 函数传递返回非引用类型的对象
  • 花括号列表初始化

重载赋值运算符

Foo& operator=(const Foo& a1){
	...
	return *this;
}

析构函数

析构函数不接受参数,因此不能被重载

class Foo{
public:
	~Foo();
}

阻止拷贝

class Foo{
public:
	Foo(const Foo&) = delete; // 阻止拷贝
}

如果一个类有数据成员不能被构造等,则对应的成员函数也将被定义为删除的

13.2 拷贝控制和资源管理

管理类外资源必须定义拷贝控制成员

行为像值的类

保证一个对象赋予其自身时,赋值运算符能正常工作

  1. 将右侧运算对象拷贝到局部临时对象
  2. 销毁左侧对象的现有成员
  3. 将临时对象拷贝到左侧运算对象中
class A{
public:
    A& operator=(const A& rhs){
        auto newp = new string(*rhs.ps);
        delete ps;
        ps = newp;
        i = rhs.i;
        return *this;
    }
private:
    string *ps;
    int i;
};

行为像指针的类

class A{
public:
    A& operator=(const A& rhs){
        ps = rhs.ps;
        i = rhs.i;
        return *this;
    }
private:
    shared_ptr<string> ps;
    int i;
};

13.3 对象移动

某些情况下,对象拷贝后就立即被销毁了,移动而非拷贝就能大幅提升性能

IO或unique_ptr无法被拷贝但可以被移动

新标准中,可以用容器保存不可拷贝的类型,只要它们能被移动即可

右值引用

右值引用必须绑定到右值,只能绑定到一个将要销毁的对象

int i = 10;
int &r1 = i;
int &&r2 = i*10;
const int &r3 = i*10;
int &&r4 = r2;	// 错误,r2是左值

move函数:可以获得绑定到左值上的右值引用,但原来的左值将不能再使用

#include <utility>
int &&r5 = std::move(r1);

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

移动构造函数的第一个参数是一个右值引用

移动构造函数不分配任何新内存,它接管给定s中的内存,并将s中的指针置为nullptr,保证移动后对象继续存在,而源对象被销毁。

A(A &&s) noexcept	// 必须承诺函数不抛出异常,否则编译器还是会调用拷贝构造函数
: element(s.element) {
	s.element = nullptr;
}
  • 只有当一个类没有定义任何版本的拷贝控制成员,且每个非static成员均可以移动时,编译器才会为它合成移动构造函数。如果没有移动构造函数,则成员是通过拷贝来完成移动的。
  • 如果定义了移动构造函数,则必须定义拷贝构造函数,否则这些成员默认被定义为删除的。

建议:所有5个拷贝控制操作(拷贝构造、拷贝赋值、移动构造、移动赋值、析构)应该看做一个整体:如果一个类定义了任何一个拷贝控制操作,就应该定义所有5个操作。

移动迭代器

移动迭代器解引用生成右值引用。通过make_move_iterator将普通迭代器转化为移动迭代器。

vector<string> v(make_move_iterator(s.begin()), make_move_iterator(s.end()));

右值引用与成员函数

通常,我们在一个对象上调用成员函数,而不管该对象是左值还是右值。

auto n = (s1+s2).find('a');	// 在右值上调用成员函数

可以在参数列表后加上引用限定符&&&,指出this指向一个左值或右值。

Foo& operator=(const Foo&) &;	// 只能用于左值

第14章 操作重载与类型转换

data1 + data2;
operator+(data1, data2);

data1 += data2;
data1.operator+=(data2);

14.1 输入输出运算符

输出«

输出运算符第一个形参是非常量ostream引用。非常量是因为向流写内容会改变其状态,引用是因为无法直接复制ostream对象。第一个形参是常量引用。常量是因为不会改变打印的对象,引用是因为避免复制实参。

ostream& operator<<(ostream& os, const A& item){
	os << item.f();
	return os;
}

通常,输出运算符尽量减少格式化操作。

输入输出运算符必须是普通的非成员函数,而不能是类的成员函数。

输入»

istream& operator>>(istream& is, A& item){
	is >> item.elements;
	if(is)			// 检查输入是否成功
		...
	else
		item = A();	// 输入失败,对象赋予成默认状态
	return is;
}

14.2 算数和关系运算符

A operator+(const A& lhs, const A& rhs){
	A sum = lhs;
	sum += rhs;
	return sum;
}

定义关系运算符><时,通常需要定义顺序关系,同时与定义的==含义一致。

14.3 赋值运算符

除了拷贝赋值、移动赋值,还有花括号列表赋值

vector<string> v = {'a', 'b', 'c'};

A& operator=(initializer_list<string> li){
	auto data = alloc_n_copy(li.begin(), li.end());
	free();					// 释放当前内存空间
	elements = data.first;	// 更新数据成员,指向新空间
	return *this;
}

14.4 下标运算符

下标运算符必须是成员函数,返回所访问元素的引用,最好同时定义常量与非常量版本。

class A{
public:
	string& operator[](int n) {return elements[n];}
	const string& operator[](int n) const {return elements[n];}
private:
	string *elements;
}

14.5 递增递减运算符

建议设置为成员函数

前置版本

A& operator++(){
	check();	// 检查在容器中的位置
	++curr;
	return *this;
}

后置版本

A& operator++(int){ // int形参起标志作用,不会用到
	A ret = *this;
	++*this;
	return ret;
}

调用

p.operator++(0);	// 后置版本
p.operator++();		// 前置版本

14.6 成员访问运算符

string& operator*() const{
	auto p = check();
	return (*p)[curr];
}

string* operator->() const{
	return & this->operator*();	// 返回*运算符的地址
}

14.7 函数调用运算符

int operator()(int val) const{
	return val;
}

如果类定义了调用运算符,则该类的对象成为函数对象

标准库函数对象

算术 关系 逻辑
plus equal_to logical_and
minus not_equal_to logical_or
multiplies greater logical_not
divides greater_equal  
modules less  
negate less_equal  
sort(vec.begin(), vec.end(), greater<int>();)

function类型

#include <functional>
int add(int i, int j) {return i+j;}
auto mod = [](int i, int j) {return i%j};
stuct divide{
	int operator()(int i, int j) {return i/j;}
}

function<int(int, int)> f1 = add;		// 函数指针
function<int(int, int)> f2 = divide();	// 函数对象
function<int(int, int)> f3 = [](int i, int j) {return i%j};	// lambda
cout << f1(2,4);

map<string, function<int, int>> binops = {
	{"+", add},
	{"/", divide()},
	{"%", [](int i, int j) {return i%j}},
}
binops["+"](2,4);
binops["/"](2,4);
binops["%"](2,4);

不能直接将重载函数的名字存入function对象中,除非存入函数指针,或lambda函数。

14.8 重载、类型转换与运算符

类型转换运算符

类型转换运算符负责将类类型转换成其他类型。

operator type() const;

type为除void之外的任何可以作为返回类型的类型。类型转换运算符没有返回类型,也没有形参,且必须是类的成员函数。

class SmallInt{
public:
	SmallInt(int i = 0): val(i)			// int -> SmallInt
	operator int() const {return val;}	// SmallInt -> int
private:
	int val;
}

显示的类型转换运算符

class SmallInt{
public:
	explicit operator int() const {return val;}
}

SmallInt si = 3;
si + 3;						// wrong
static_cast<int>(si) + 3;	// right

第15章 面向对象程序设计

15.1 定义基类和派生类

虚函数:基类希望派生类进行覆盖的函数,加上关键词virtual修饰。当调用虚函数时,调用将被动态绑定。基类的虚函数在派生类中隐式的也是虚函数。

派生类通过类派生列表指明它从哪些类继承而来。

class B: public A{
    ...
}

B b;
A *p = &b;  // 派生类到基类的隐式转换

派生类可以访问基类的公有和受保护成员

通过关键字final防止继承。final修饰类,表示类不能被继承;final修饰虚函数,表示虚函数不能被覆盖。

class Base final{
	...
}

15.2 虚函数

对每个虚函数都需要有定义,因为连编译器也无法确定到底使用哪个虚函数。

派生类的虚函数返回类型与基类一致,除非返回对自身的引用。

override可以用来说明派生类中的虚函数。如果使用override标记了某函数,但该函数并没有覆盖已存在的虚函数,编译器将报错。

A: virtual void f1(int) const;
B: void f1(int) const override;

通过作用域运算符可以实现强迫虚函数执行某个特定版本

int i = p->f();
int i = p->A::f();

15.3 抽象基类

含有纯虚函数的类是抽象基类,不能直接创建抽象基类的对象。

class A{
public:
	f() = 0;
}

15.4 访问控制与继承

protected成员:

  • 类用户无法访问
  • 派生类成员和友元可以访问
  • 派生类成员只能通过派生类对象访问基类的受保护成员★★
class Base{
protected:
    int mem;
}
class Derive:public Base{
    friend void f1(Derive& d) {d.mem;}      // right
    friend void f2(Base& b) {b.mem;}        // wrong
}

注意:

  • 基类中的访问说明符决定基类成员的访问权限。
  • 派生访问说明符是为了1、限制派生类用户对基类成员的访问权限,2、控制继承自派生类的新类的访问权限。
  • 友元关系不具有传递性,基类的友元与派生类的友元无关。

派生类向基类的转换:

  • 只有D公有的继承B,用户代码才能使用派生类向基类的转换
  • D的成员函数和友元始终能使用派生类向基类的转换
  • 只有D继承B的方式是共有的或受保护的,则D的派生类成员和友元才能使用派生类向基类的转换

通过using声明,可以将该类的基类中的任何可访问成员标记出来

class Base{
public:
    int size;
}
class Derive: private Base{
public:
    using Base::size;   // 由于private继承,size默认是Derived私有成员,using将其声明为公有
}

15.5 继承中的类作用域

名字冲突时,派生类的成员将隐藏基类的同名成员,通过作用域运算符能使用隐藏的成员。

派生类成员不会重载基类同名成员,即使形参列表不一致。★★

派生类成员会覆盖基类中所有重载函数,通过使用using使得基类成员在派生类中可见,可以有选择的覆盖一些成员。

15.6 构造函数与拷贝控制

虚析构函数

delete一个类的指针的话,指针的实际指向与指针类型会不一致,因此需要定义虚析构函数。

class Base{
public:
    virtual ~Base() = default;
}

继承的构造函数

一个类只继承其直接基类的构造函数。

class Derive: public Base{
public:
    using Base::Base();	// 继承基类的构造函数
}

作用于构造函数时,using将令编译器产生代码,并且不会改变构造函数的访问级别。

15.8 容器与继承

vector<shared_ptr<Base>> vec;
vec.push_back(make_shared<Base>(...));
vec.push_back(make_shared<Derive>(...));

第16章 模板与泛型编程

16.1 定义模板

#函数模板

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

可以在模板中定义非类型参数,必须是常量表达式

template <unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M]){
	...;
}

为了生成实例化版本,编译器需要模板的定义,因此,模板的头文件包含函数的声明与定义,而非模板的头文件只包含声明

#类模板

定义在类模板之外的成员要加上template关键字。

使用类模板时要提供模板实参,但在类模板作用域内就不必。

template <typename T> A<T> A<T>::f(){	// 作用域外 A<T>
	A ret = *this;			// 作用域内 A
}

模板类与友元:

  • 当一个类包含友元时,类与友元各自是否为模板是相互无关的。
  • 引用模板的一个特定实例,必须首先声明模板自身
  • 引用模板的所有实例,无需首先声明模板自身
template<typename T> class A;
template<typename T> class C{
	// 引用特定实例,须先声明
	friend class A<T>;
	// 引用所有实例,无须先声明
	template<typename X> friend class B;
}

可以为类模板定义一个类型别名

template <typename T> using twin = pair<T, T>
twin<string> sth;		//twin<string> <==> pair<string, string>

类模板的静态成员

template <typename T> class A{
public:
	static count() {return cnt;}
	...
}

A<int> a;
ct = a.count();
auto ct = A<int>::count();	// 加上<int>

#模板参数

模板参数会隐藏外层作用域中的相同名字。但是,在模板内不能重用模板参数名。

当遇到T::value_type时,编译器不知道这是个类型参数的名字,还是一个数据成员。C++默认这是一个数据成员,可以通过关键字typename告诉编译器这是一个类型。

template <typename T> typename T::value_type count() {}

C++11允许我们为函数和类模板提供默认实参

template <typename T, typename F = less<T>>
int compare(const T& v1, const T& v2, F f = F()){
	if(f(v1, v2)) return -1;
	...
}

template<class T = int> class A{
	...
}

#成员模板

类包含的本身是模板的成员函数称为成员模板。成员模板不能是虚函数。

template <typename T> class A{
	// 声明
	template <typename IT>  A();
}

// 定义
template <typename T> template <typename IT>
A<T>::A() { }

#控制实例化

模板在使用时才被实例化,因此相同的实例可能出现在多个对象文件中,通过显示实例化可以避免这种情况。

external template class A<int>;			// 声明
template int compare(const int&, const int&);	// 定义

对于一个给定的实例化版本,可能有多个external声明,但只有一个定义。

16.2 模板实参推断

从函数实参来确定模板是惨的过程称为模板实参推断

#类型转换与模板类型参数

支持的类型转换:const转换、数组和函数指针的转换

#函数模板显示实参

当函数返回类型与参数列表中的类型均不相同时,无法推断返回类型。可以指定显示模板实参

template<typename T1, typename T2, typename T3> T3 sum(T2, T1) ;
auto val = sum<long long, int, long>(i, j);

#尾置返回类型

用显示模板实参会给用户带来负担,因此可以用尾置返回类型

template<typename IT>
auto f(IT beg, IT end) -> decltype(*beg){
	return *beg;
}

标准库类型转换模板#include <type_traits>,可以用来进一步获取元素类型。

remove_reference<decltype(*sth)>::type // => sth的类型
remove_pointer...
remove_extent...

#函数指针与实参推断

template<typename T> int compare(const T&, const T&);
int (*p)(const int&, const int&) = compare;

16.3 重载与模板

一个调用有多个重载函数匹配时:

  • 多个重载模板时,选择最特例化版本。f(T*)而非f(const T&)
  • 一个非模板函数与一个模板函数,选择非模板版本。

16.4 可变参数模板

可变参数模板是指一个接受可变数目参数的模板函数或模板类。可变数目的参数为参数包,用...表示包。

template <typename T, typename... Args>
void foo(const T& t, const Args& ... rest);
// Args模板参数包, rest函数参数包

通过sizeof可以返回包中元素的个数。

template <typename... Args> void g(Args ... args){
	cout << sizeof...(Args);	// 类型参数数目
	cout << sizeof...(args);	// 函数参数数目
}

编写可变参数函数模板

通过递归调用的形式

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){
	return print(os, rest...);
}

包扩展

扩展包就是将它分解成构成的元素,对每个元素应用模式,获得扩展后的列表。在模式右边家...触发扩展。

template<typename T, typename... Args> 
ostream &print(ostream& os, const T& t, const Args&... rest){	// 扩展Args
	return print(os, rest...);				// 扩展rest
}
template<typename T, typename... Args> 
ostream &print(ostream& os, const T& t, const Args&... rest){
	return print(os, A(rest)...);				// 对每一项调用A(),然后打印
}

16.5 模板特例化

函数模板特例化

template<typename T> int compare(const T&, const T&);
template<> int compare(const char* const &p1, const char* const &p2){
	return strcmp(p1, p2);		// 特例化 T => const char*
}
  • 当定义函数模板的特例化时,我们本质上接管了编译器的工作。
  • 重载时,当有多个函数得到匹配时,按以下规则:
    • 非模板函数
    • 特例化模板函数
    • 普通模板函数
  • 模板和特例化版本需要同时声明在头文件中,缺失特化版本会导致错误很难找。

类模板偏特化

  • 只有类可以使用偏特化,函数只能全部特化
  • 在类后面加尖括号表示偏特化方式
    template <class T> class A{};
    template <class T> class A<T&>{}; 	// 偏特化成左值类型
    

第17章 标准库特殊设施

17.1 tuple

tuple类似pair,但tuple可以有任意数量的成员。

tuple<int, vector<string>> t(12, {"abc"});
auto item = make_tuple(...);
auto sth = get<0>(item);

tuple的常见用途是从一个函数返回多个值。

17.2 bitset

bitset大小是一个固定值,从位置0开始为二进制的低位,末位为高位。用整型值初始化bitset时,值会被转换成unsigned long long

#include <bitset>
bitset<32> b(0U);
bitset<32> b("1100");
unsigned long out = b.to_ulong();

17.4 随机数

在头文件random中定义了解决随机数问题的类:随机数引擎类与随机数分布类。

随机数引擎不接受参数,并返回一个unsigned整数。

default_random_engine e;
default_random_engine e(seed);
cout << e();

uniform_int_distribution<unsigned> u(0, 9);
cout << u(e);

对于一个给定的随机数引擎,每次程序运行都会返回相同的数值序列。将其定义为static,则每次调用会接着上次使用的结果。

static default_random_engine e;
static uniform_int_distribution<unsigned> u(0, 9);
cout << u(e);

第18章 用于大型程序的工具

18.1 异常处理

抛出异常

栈展开:当执行throw时,跟在throw后面的语句将不再执行,程序的控制权将转移到catch块。catch会沿着嵌套函数调用链查找,直到找到与异常匹配的catch子句。

栈展开的过程中局部对象会被销毁,析构函数总是执行,但函数中负责销毁对象的代码可能会被跳过。

如果析构函数会引发异常,则应该将该语句放入try子句中。

编译器使用异常抛出表达式来对异常对象进行拷贝初始化。抛出对象不能是指向局部对象的指针。抛出表达式时,该表达式是静态编译时的类型。

异常捕获

catch子句用来捕获异常。catch的参数类型分为引用类型和非引用类型。

catch挑选出来的是第一个与异常匹配的catch子句,因此派生类异常应该在基类异常之前。catch匹配规则是精确匹配,不支持绝大多数类型转换。

重新抛出:空的throw语句重新抛出异常。throw只能在catch语句或其调用函数之内。只有catch参数是引用类型时,才会抛出改变的异常。

catch(...)会捕获所有异常。

try语句与构造函数

构造函数在进入函数体之前首先执行初始化列表,因此构造函数体内的catch无法处理构造函数初始化列表抛出的异常。

函数try语句块

template<typename T>
A<T>::A(initializer_list<T> l) try: data(make_shared(vector<T>(l))){
	...
} catch(bad_alloc &e) {
	...
}

noexcept

通过noexcept说明指定某个函数不会抛出异常。

void f() noexcept;

对于一个函数来说,noexcept说明要么出现在函数的所有声明和定义中,要么一次也不出现。

编译器不会检查noexcept说明,即使函数又抛出了异常,编译器也能顺利通过。在运行时,程序会直接终止。

noexcept可以用在两种情况:确定函数不会抛出异常,根本不知道如何处理异常。

void f() noexcept(true);	// 不会抛出异常
void f() noexcept(false);	// 可能抛出异常

noexcept运算符用来判断表达式是否会抛出异常。

noexcept(e);:当e不含有throw且不抛出异常,表达式为true

函数指针与该函数、虚函数与继承体系中的其他虚函数需要有一致的异常说明。

18.2 命名空间

namespace A{
	....
}

命名空间可以定义在全局作用域,也可以在其他命名空间,单不能在函数或类内部。

命名空间是不连续的。

内联命名空间:内联命名空间内的名字可以被外部空间直接使用。关键字inline出现在命名空间第一次定义的地方,后续的可以省略。

未命名的命名空间的变量有静态的生存周期,在第一次使用前创建,直到程序结束才销毁。未命名的命名空间不能跨越多个文件。

使用using可能导致名字冲突,此时必须用作用域运算符::明确指出使用的版本。

18.3 多重继承与虚继承

class Derive: public Base1, public Base2{}

基类的构造顺序与派生列表中基类出现的顺序一致,与派生类构造函数初始值列表中基类的顺序无关。

如果一个类从多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义自己的版本。

struct D: public B1, public B2{
	using B1::B1;
	using B2::B2;
	D(): B1(s), B2(s) {}	// 基类构造函数出现冲突,需要自定义
}

虚继承

   B
  / \
 D1 D2
  \ /
   C

class D1: public virtual b {}

当出现上述继承时,C中会两次出现B的成员,可以将D1、D2定义为B的虚继承。虚继承只影响C,不会对D1、D2有影响。

如果D1、D2对B的成员有修改,则D中的成员会产生二义性。解决办法是在派生类中为成员自定义新的实例。

虚继承的构造函数,先初始化虚基类,在依次初始化直接基类。

第19章 特殊工具与技术

19.1 控制内存分配

#重载new和delete

new过程:

  • 调用operator new函数,分配足够大的内存空间
  • 编译器运行相应的构造函数,传入初始值
  • 对象分配空间并构造完成,返回指向该对象的指针

delete过程:

  • 对指针所指对象运行析构函数
  • 调用operator delete函数,释放内存空间

operator new函数只能定义在全局空间类作用域内。编译器首先在类及其基类的作用域内查找,否则在全局作用域查找。::new表示只在全局作用域查找。

operator new接口

// 标准库的定义
void* operator new(size_t);
void* operator new(size_t, nothrow_t&) noexcept;
void* operator new(size_t, void*); 		// 该版本仅供系统使用,不能被重载

// 使用
int *p = new int;
int *p = new (nothrow) int;	// new的定位式,使用额外形参

operator new返回类型是void*,第一个形参是size_t,表示存储指定类型所需的空间,不能含有默认实参。可以为它提供额外的形参,使用时必须使用new的定位式。operator delete返回类型是void,第一个形参是void*。可以为它提供另外一个size_t的形参,初始值是第一个形参所指对象的字节数。重载delete时必须指定noexcept,表示不抛出异常。内存分配与释放的具体操作使用mallocfree

#include <cstdlib>
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);}

#定位new表达式

allocator类似,operator new负责内存的分配,但不会构造对象。区别是allocator中用construct构造对象,现在要用定位new构造对象。同时,定位new的指针并不需要指向operator new分配的内存。

new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }

string *p = new string("..");
p->~string();			// 销毁对象却不释放内存

19.2 运行时类型识别(RTTI)

当RTTI运算符作用于某种类型的指针或引用,且该类型含有虚函数时,运算符将使用所绑定对象的动态类型。最好使用虚函数管理动态类型,使用RTTI运算符必须保证类型被正确转换。

#dynamic_cast

dynamic_cast<type*>(e);
dynamic_cast<type&>(e);
dynamic_cast<type&&>(e);

type必须是类类型,通常情况下应该含有虚函数。e的类型必须是type类或其公有基类、派生类,否则转换失败。如果失败,指针类型返回0,引用类型抛出bad_cast异常。

if(Derive *dp = dynamic_cast<Derive*>(bp))
	// 使用dp指向的Derive对象
else
	// 使用bp指向的Base对象

#include <typeinfo>
void f(const Base& b){
	try{
		const Derive& d = dynamic_cast<Derive&>(b);
		// 使用b引用的Derive对象
	} catch (bad_cast){
		// 处理失败情况
	}
}

#typeid运算符

typeid(e)返回对象的类型,e可以是任意表达式或类型的名字。typeid(e)作用于数组或函数时,不会执行向指针的标准转换。当e不是类或一个不包含虚函数的类,typeid(e)表示对象的静态类型,否则为动态类型。

Derive *dp = new Derive;
Base *bp = dp;
typeid(*dp) == typeid(*bp) == typeid(Derive)
typeid(bp) == type(Base*)

#使用RTTI

例子:为具有继承关系的类实现相等运算符

class Base{
	friend bool operator==(const Base&, const Base&);
protected:
	virtual bool equal(const Base&) const;
}

class Derive: public Base{
protected:
	virtual bool equal(const Base&) const{
		auto r = dynamic_cast<const Derive&>(rhs);
	}
}

bool operator==(const Base& lhs, const Base& rhs){
	return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}

19.3 枚举类型

枚举属于字面值常量类型,初始值应该是常量表达式。默认情况下,枚举值从0开始,依次加1。不限定作用域类型可以隐式的转换成int型。

enum class pepper{red, yellow, green};	// 限定作用域
enum color{red, yellow, green};		// 不限定作用域类型
enum {a=1, b=3, c=10};

color hair = red;
color hair = color::red;
pepper p = pepper::green;

19.4 类成员指针

成员指针是指可以指向类的非静态成员的指针。初始化时,令其指向类的某个成员,但不指定哪个对象;直到使用成员指针,才提供成员所属对象。

#数据成员指针

class Screen{
public:
	typedef std::string::size_type pos;
	char get_cursor() const { return contents[cursor]; }
	char get() const;
	char get(pos ht, pos wd) const;
	static const std::string Screen::*data() { return &Screen::contents; }
private:
	std::string contents;
	pos cursor;
	pos height, width;
}

const string Screen::*pdata = &Screen::contents;	// pdata并非指向实际数据
const string Screen::*pdata = Screen::data();

Screen myScreen;
auto s = myScreen.*pdata;	// 指向实际数据

#成员函数指针

指向成员函数的指针需要指定目标函数的返回类型和形参列表。如果存在重载,需要显示的声明函数类型。

auto pmf = &Screen::get_cursor;
char (Screen::*pmf2)(Screen::pos, Screen::pos) const = &Screen::get;

char c = (myScreen->*pmf)();
char c2 = (myScreen->*pmf2)(0, 0);

using Action = char (Screen::*)(Screen::pos, Screen::pos) const;
Action get = &Screen::get;

成员函数指针表

class Screen{
public:
	Screen& up();
	Screen& down();
	using Action = Screen& (Screen::*)();
	enum Directions { UP, DOWN };
	Screen& move(Direction cm){
		return (this->*Menu[cm])();
	}
private:
	static Action Menu[];
}
Screen::Action  Screen::Menu[] = {&Screen::up, Screen::down};

myScreen.move(Screen::UP);

#将成员指针用作可调用对象

成员指针不是可调用对象,不支持函数调用运算符,因为它们需要先绑定到特定的对象上。

使用function生成可调用对象,必须指定函数类型

function<bool (const string&)> fcn = &string::empty;
find_if(v.begin(), v.end(), fcn);

使用mem_fn生成可调用对象,让编译器推断成员类型

find_if(v.begin(), v.end(), mem_fn( &string::empty));

auto f = mem_fn( &string::empty);
f(&v[0]);		// 传入引用,使用->*调用函数 
f(*v.begin());		// 传入对象,使用.*调用函数 

使用bind生成可调用对象

find_if(v.begin(), v.end(), bind( &string::empty, _1));

auto f = bind( &string::empty, _1);
f(&v[0]);		// 传入引用,使用->*调用函数 
f(*v.begin());		// 传入对象,使用.*调用函数 

19.8 固有的不可移植的特性

不可移植的特性指因机器而异的特性,这种类型的程序从一台机器转移到另一台机器时,需要重新编写代码。

#位域

位域通常是无符号整型或枚举类型。在类内连续定义的位域可能压缩在同一整数的相邻位,因此提供了存储压缩。位域不存在指针。通常使用内置的位运算符处理超过1位的位域。

typedef unsigned int Bit;
class File{
	Bit mode: 2;
	Bit modified: 1;
}

#volatile限定符

当对象的值可能在程序控制范围之外被改变时(如系统时钟),将其声明为volatilevolatile的用法和const基本一致,一个区别是:不是使用合成的拷贝函数初始化volatile对象,或从volatile对象赋值。

volatile int a;

#链接指示:extern “C”

c++使用链接指示指出任何非c++函数使用的语言。链接指示不能出现在类定义或函数定义内部,必须在函数的每个声明中都出现

extern "C" void f();
extern "C"{
	void g();
	void (*pf)(int);	// C指针与C++不同
}

ShengYg

Step after step the ladder is ascended.


Tags • note