关联型容器 Associative Containers

关联型容器通常需要键满足 <,否则可以自定义一个:

bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) {
return lhs.isbn() < rhs.isbn();
}
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);

关联性容器定义了三种类型:

  • key_type
  • mapped_type
  • value_type
    • setkey_type
    • mappair<const key_type, mapped_type>

set 的迭代器都是 const_iterator

.lower_bound() >=,.upper_bound() >,.equal_range()

除此之外,还有键可以重复的 multisetmultimap

unordered_setunordered_map 使用 hash,不保证顺序

动态内存 Dynamic Memory

智能指针:

  • shared_ptr<T> spunique_ptr<T> up
  • p.get() 返回 p 中的指针
  • make_shared<T>(args) 返回指向动态分配类型 T 的对象的 shared_ptr,使用 args 初始化对象

.reset 更新引用计数并可能会删除指向的对象

也可以显式指定析构函数:shared_ptr<T, D> s2(d)unique_ptr<T, D> u2(d),一个例子:

connection c = connect(&d);
unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);

转移所有权的方法:

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

weak_ptr 是不会控制指向对象的生命周期的智能指针,其通常指向由 shared_ptr 管理的对象

因为有可能指向已经被释放的值,故不能直接访问,而是通过 .lock():如果已经 w.expired() == true,则返回 nullptr;否则返回指向的对象的 shared_ptr

也支持动态分配数组:unique_ptr<T[]> u

但注意当使用 shared_ptr 时,要指定析构函数:

shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });

allocator 可以将分配从构造中分离出来

  • 定义 allocator<T> a
  • a.allocate(n) 分配原始的,为构造的内存以存放 n 个 T 类型的对象
  • a.construct(p, args)p 必须指向 T 类型原始内存的指针,args 是用来构造的参数
  • a.destroy(p):运行析构函数
  • a.deallocate(p, n):归还 allocate 的内存

一个例子:

allocator<string> alloc;
auto const p = alloc.allocate(n);
auto q = p;
alloc.construct(q++);
alloc.construct(q++, 10, 'c');
alloc.construct(q++, "hi");
while (q != p)
alloc.destroy(--q);
alloc.deallocate(p, n);

类似于 copyfill 的算法,allocator 也有相对应的算法:uninitialized_copy(b, e, b2)uninitialized_fill(b ,e t)

复制控制 Copy Control

复制,赋值和销毁

copy constructorFoo(const Foo&);

复制初始化:

string s(dots); // 直接初始化
string s2 = dots; // 复制初始化

有的类使用 explicit 指定必须使用直接初始化:

vector<int> v1(10); // ok
vector<int> v1 = 10; // error

copy assignmentFoo& operator=(const Foo&);

析构函数 destructor~Foo();

以上三者必须同时定义

使用 = default 来显式要求编译器生成默认的版本

极少数类(如 iostream)会禁止复制,使用 = delete

复制控制和资源管理

尽管很少见,但赋值运算符必须支持自己给自己赋值,故其一定要先复制 rhs 操作数,再删除 lhs 操作数

尽管 swap 不是必须的,但是定义 swap 可以有更好的优化:

inline void swap(HasPtr &lhs, HasPtr &rhs) {
using std::swap; // 一定要这么使用,而不是指定 std::swap
swap(lhs.ps, rhs.ps);
swap(lhs.i, rhs. i);
}

Swap

可以在赋值中使用 swap,这种做法比较安全:

HasPtr& HasPtr::operator=(HasPtr rhs) {
swap(*this, rhs);
return *this;
}

移动对象

IOunique_ptr 不可以被复制,但可以被 move

左值引用右值引用

可以使用 std::move 将左值转换为右值

相对应的,就有了 move constructor:

StrVec::StrVec(StrVec &&s) noexcept : elements(s.elements), first_free(s.first_free), cap(s.cap) {
s.elements = s.first_free = s.cap = nullptr; // 让 s 能够安全地调用析构函数
}

因为 move constructor 没有分配资源,所以并不会抛出异常,故要加上 noexcept

push_back() 保证了如果发生异常,则原有数据保持不变。那么如果 move constructor 发生了异常,则旧数据和新数据都会损坏,故只能选择 copy constructor。所以需要显式指明不会抛出异常

对应的也就有 move assignment:

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;
}

只有当一个类没有定义自己的复制控制的成员时,编译器才会合成 move 成员

一种同时定义了 copy 和 move 赋值的方法:

HasPtr& operator=(HasPtr rhs) {
swap(*this, rhs);
return *this;
}

一般的迭代器在解引用时返回的是左值引用,但是 move iterator 返回的却是一个右值引用

将迭代器转为移动迭代器的做法:make_move_iterator()

区分复制和移动参数的重载函数:

void push_back(const X&); // 复制
void push_back(X&&); // 移动

类似于 const *this,我们也可以指定 *this 是左值还是右值:

Foo& operator=(const Foo&) &; // this 是一个左值

重载运算和转换 Overloaded Operations and Conversions

可以显式调用运算符:

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

成员函数与非成员函数

输入流和输出流重载

算术运算符通常使用符号赋值重载:

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

同时定义

支持初始化列表的赋值:

StrVec& StrVec::operator=(initializer_list<string> il) {
auto data = alloc_n_copy(il.begin(), il.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}

定义下标运算符时,通常要定义两个版本:const 和非 const 的:

class StrVec {
public:
std::string& operator[](std::size_t n) { return elements[n]; }
const std::string& operator[](std::size_t n) const { return elements[n]; }
}

自增/自减分为前缀/后缀版本,用一个为 0 的参数来区分:

class StrBlobPtr {
public:
// 前缀版本
StrBlobPtr& operator++();
StrBlobPtr& operator--();
// 后缀版本
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);
}

甚至可以重载函数调用运算符,像函数一样使用,叫函数对象

struct absInt {
int operator()(int val) const {
return val < 0 ? -val : val;
}
}

absInt absObj;
int ui = absObj(-42);

事实上,lambda 就是函数对象

对于一个捕获了局部变量的 lambda,其相当于构造函数带该参数,并保存在其成员变量中

functional 头文件中定义了很多类型,如:plus<T>greater<T>

有多种可调用对象,如函数,函数指针,lambda,重载了 () 的对象等,该头文件中还定义了函数原型以统一这些表达:

map<string, function<int(int, int)>> binops = {
{"+", add}, // 函数指针
{"-", std::minus<int>()}, // 库函数对象
{"/", div()}, // 用户定义的函数对象
{"*", [](int i, int j) { return i * j; }} // 匿名 lambda
}

类型转换函数的基本形式:operator 类型名() const

前面可以加上 explicit,强制要求显式转换

面向对象编程 Object-Oriented Programming

概览

基类 base class

class Quoto {
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
};

派生类 derived class

class Bulk_quote : public Quote {
public:
double net_price(std::size_t) const override;
};

动态绑定(运行时绑定):根据运行时是对基类还是派生类的引用,调用不同类中的函数

基类一般会定义一个 virtual 析构函数,使得其动态绑定

virtual 关键字表示希望派生类覆盖该成员

定义基类和派生类

派生类会自动转换为基类,如:

Bulk_quote bulk;
Quote *p = &bulk; // 指向 bulk 的 Quote 部分
Quote &p = bulk; // 引用 bulk 的 Quote 部分

基类先初始化,然后才是派生类的成员:

Bulk_quote(const std::string& book, double p, double disc) :
Quote(book, p), discount(dics) { };

对于 static 成员,则整个层次结构中只有一个该成员实例

使用 final 来防止一个类被继承:class NoDerived final { ... };

静态类型(编译时)和动态类型(运行时)

基类向派生类没有隐式转换

注意这只在引用指针之间讨论,在基类和派生类对象之间没有隐式转换

虚函数

只有在通过指针或引用调用虚函数时,才会在运行时处理,否则在编译时解决

一旦某个函数被定义为 virtual,则会在所有派生类中都定义为 virtual

可以使用 override 显式指明覆盖了基类中的某个虚函数

注意只有 virtual 函数可以 overridefinal 函数不会被 override

可以绕开虚函数机制

抽象基类

纯虚函数 pure virtual 不会被定义,方法是在声明时在后面加 = 0

有纯虚函数的类是抽象基类 abstract base class,不能创建抽象基类的对象,其相当于提供了一个要求派生类覆盖的接口

派生类的构造函数只初始化它的直接基类

访问控制和继承

publicprivate 继承不影响该类的访问控制,但会影响这个类的用户(包括其派生类)能否访问到其基类的成员

friend 不会被继承

类内部的 using 声明可以使其 private 继承来的被访问到

继承下的类作用域

名字的查找发生在编译时

构造函数和复制控制

析构函数必须是 virtual

构造时先构造基类,析构时反过来

容器与继承

如果我们想要容纳以继承关联的对象,一般放智能指针,这样使用的就是动态类型

模板和泛型编程 Templates and Generic Programming

面向对象编程和泛型变成都是处理编写程序时不知道的类型的,但是 OOP 在运行时处理,泛型则在编译时处理

定义一个模板

函数模板:

template <typename T>
int compare(const T &v1, const T &v2) {
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
cout << compare(1, 0) << endl; // 可以推断出模板参数类型

我们也可以定义非类型参数 nontype parameters,即表示的是一个而不是类型,在模板函数内部一定是一个常量表达式

template <unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M]) {
return strcmp(p1, p2);
}
compare("hi", "mom"); // 使用

为了生成实例化,编译器需要定义函数模板或类模板成员函数,所以这两者一般都放在头文件中

在类的外部定义类模板成员函数:

template <typename T>
返回类型 Blob<T>::成员函数名(参数列表)

只有当某个成员被使用时,它才会被实例化

可以让友元的成为模板的所有实例的朋友,也可以一对一成为友元

可以使模板类型参数成为友元:

template <typename Type> class Bar {
friend Type;
};

可以定义类模板的类型别名:

template <typename T> using twin = pair<T, T>;
twin<strin> authors;

类模板和函数参数支持设置默认值,如:

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

可以显式实例化来避免开销:

extern template 声明; // 实例化声明
template 声明; // 实例化定义

unique_ptr 在编译时绑定了 deleter,而 shared_ptr 在运行时绑定了 deleter

模板参数推断

模板类型参数的仅有的自动转换是 const 转换和数组或函数指针转换

而常规的转换应用于非模板参数的类型上

函数模板可以显式指定参数类型:

template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);

sum<long long>(i, lng);

这是一个返回未知类型的函数:

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

其返回的是一个引用,但是如果是想要按值返回,则可以显式类型改变 type transformationremove_reference<>

引用合并:

  • X& &X& &&X&& & 都合并为 X&
  • X&& && 合并为 X&&

故即使参数的形式是右值 &&,也可能合并为左值 &

因此,函数参数中的对模板类型的右值引用可以绑定到左值上,即若一个左值传递给这个参数,则会被实例化为左值引用

使用右值的函数模板通常被重载为:

template <typename T> void f(T&&); // 绑定到非 const 右值
template <typename T> void f(const T&); // 左值和 const 右值

实际上,std::move 的原理如下:

template <typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}

std::forward 可以在保持原始参数类型的情况下传递参数,其返回的类型是 T&&,例如实现一个交换调用参数顺序的模板函数:

template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 && t2) {
f(std::forward<T2>(t2), std::forward<T1>(t1));
}

重载和模板

当有几个重载函数都能够匹配调用时,会选择最详细的那个

如果还有非模板函数也能匹配,则更优先匹配非模板函数

可变模板

可变模板 variadic template 是接受可变数量的参数的模板函数或类:

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

可以使用 sizeof...() 获取包中的元素数量

通常使用递归实现展开:

template <typename T>
ostream &print(ostream &os, const T &t, const T &t) {
return os << t;
}
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args& ... rest) {
os << t << ", ";
return print(os, rest...);
}

通常和 std::forward<Argss>(args)... 一起使用转发给别的函数

模板特化

我们可以接管编译器做的工作,即模板特化 template specialization

template <>
int compare(const char* const &p1, const char* const &p2) {
return strcmp(p1, p2);
}

模板特化也适用于类模板,但要注意定义在同一个命名空间中

类模板可以部分特化,但函数模板不行

专业化的库设施 Specialized Library Facilities

tuple 类型

tuplepair 类似,但是支持容纳多个元素

定义:

tuple<size_t, size_t, size_t> item{1, 2, 3};

或者使用 make_tuple

访问其中的元素:

tuple_element<1, decltype(item)>::type cnt = get<1>(item);

tuple 支持关系比较

bitset 类型

定义 32 位:bitset<32> bitvec(1U);,其中低位为 1

也可以从字符串中获取:bitset<32> bitvec4("1100");,注意最低位为 0

其支持丰富的操作位的函数

以字符串格式 IO

正则表达式

随机数

使用到了两个类——随机数引擎 random-number engines随机数分布类 random-number distribution classes

生成均匀 [0, 9] 的均匀的随机数:

uniform_int_distribution<unsigned> u(0, 9);
default_random_engine e(time(0));
for (size_t i = 0; i < 10; ++i)
cout << u(e) << " ";

还支持生成小数 uniform_real_distribution<>、正态分布 normal_distribution<>

再探 IO 库

操作符 manipulator 用于控制输出的格式

如十进制、八进制,精度

还支持更低级的 IO:

  • is.get(ch) 读下一个字节到 ch 中
  • os.put(ch) 把下一个字符 ch 放到 os 中
  • is.get() 返回下一个字节为 int,因为可能读到的是 EOF
  • is.unget() 放回一个字节
  • is.peek() 返回下一个字节为 int,但不会移除
  • is.putback(ch) 把 ch 放回 is

还有支持多字节的版本:

  • is.getline(sink, size, delim) 读至多 size 个字节至 EOF 或 delim 到 sink 指向的位置,注意 delim 不会被存储。delim 也会被保存的版本:get
  • is.ignore(size, delim)
  • os.write(source, size)

随机读写(p 代表g 代表,但实际上操作的是同一个标志,本没有什么区别):

  • tellg() 获取当前位置
  • seekg(pos) 移动当前位置
  • seekg(off, from)

用于大型程序的工具 Tools for Large Programs

异常处理

当一个异常发生时,函数会返回并向上传递异常,直到被捕捉或退出程序,这个过程叫堆栈展开 stack unwinding,在该过程中,局部对象的析构函数会被调用,所以要注意向异常传递的指针所指向的对象在异常处理时是否存在

如果一个 catch 无法完全解决问题,需要再抛出异常 rethrow,方法很简单:throw;

捕捉所有异常的方法 catch(...)

函数 try 块 function try block 能够将 catch 与语句和构造函数的初始化阶段结合起来:

Blob::Blob(std::initializer_list<int> il) try :
data(std::make_shared<std::vector<int>>(il)) { }
catch (const std::bad_alloc &e) { handle_out_of_memory(e); }

noexcept(true) 表示这个函数不会抛出异常,noexcept(false) 表示这个函数可能抛出异常,noexcept(函数) 可以判断某个函数是否会抛出异常,通常用于定义一个是否抛出异常取决于函数 g() 的函数:

void f() noexcept(noexcept(g()));

异常也有继承的层次结构,可以自定义异常类型

命名空间

为了防止全局命名空间造成的命名空间污染 namespace pollution,引入了命名空间 namespace 用于防止命名冲突

命名空间的定义中包含着各命名的声明:

namespace cpp {
class Sales_data;
}

使用:

cpp::Sales_data s;

命名空间的定义可以被分到多个文件中

在全局作用域定义的名字被定义在全局命名空间 global namespace 中,显式的使用方法为:::成员名

命名空间可以嵌套

内联命名空间 inline namespace 中的名字可以不需要命名空间名限定即可使用

匿名命名空间 unnamed namespace 中的名字只会局限于当前文件,无法显式指定,用于替代 static 来是名称局限于当前文件中

命名空间别名 namespace alias 用于定义某命名空间的缩写:

namespace primer = cplusplus_primer;
  • using 声明 using declaration 每次只引入一个命名空间中的成员
  • using 指令 using directive 一次引入了所有的成员
  • 推荐使用前者,因为如果发生了名称冲突,则会在编译时报错,而后者是在运行时报错,且更容易发生冲突

注意到这种写法是允许的:

operator>>(std::cin, s);

其中的 operator 前并没有加上 std:: 也能正常运行,因为首先会在对象 scin 定义的作用域中查找相关的 operator

因此,std::movestd::forward 通常要连着前面的限定一起写,因为如果使用 using std::move 的话,一开始并不是在 std 命名空间中查找 move 函数的,而 move 是个非常常见的名称,容易被误定义过。为了避免这种事,一定要连上 std::

多重和虚拟继承

一个多继承的例子:

class Panda : public Bear, public Endangered { ... }

其构造函数的写法和平常的没什么区别:

Panda::Panda(std::string name, bool onExhibit)
: Bear(name, onExhibit, "Panda")
Endangered(Endangered::critical) { }

最终基类一定第一个初始化,然后按声明的顺序

如果有不同的基类的相同名字的成员,则必须指定是哪个基类的成员

注意到 BearEndangered 继承了相同的基类 ZooAnimal,为了防止同时继承了这两个类的派生类存在多个 ZooAnimal 部分,需要将这两个类声明为虚继承 virtual inheritance,那个共同的基类叫虚基类 virtual base class

声明方法如下:

class Bear : virtual public ZooAnimal { ... }

虚继承中构造函数保证了虚基类最先初始化,且只被初始化一次

专门的工具和技术 Specialized Tools and Techniques

控制内存分配

new 表达式构造了对象并调用 new 函数分配内存

delete 同理

运行时类型识别

运行时类型识别 run-time type identification(RTTI) 通过两个运算符提供:

  • typeid 返回给定表达式的类型
  • dynamic_cast 操作符将指向基类的指针或引用转换为指向派生类的指针或引用

对于指针,dynamic_cast 失败会返回 nullptr,故一般用法:

if (Derived *dp = dynamic_cast<Derived*>(bp)) {

} else {

}

对于引用,则抛出 bad_cast 异常

注意指针的 typeid 返回的是编译时的类型

枚举

enum color {red, yellow, green}; // 无作用域的枚举
enum class peppers {red, yellow, green}; // 有作用域的枚举
color eyes = green; // 无作用域的枚举可以直接使用
peppers p2 = peppers::red; // 有作用域的枚举必须显式指定

默认情况下从 0 开始,每次加 1,可以在定义时指定每个枚举量的值

枚举量的值是一个 constexpr 的量

我们可以指定枚举量的类型:

enum intValues : unsigned long long;

类成员指针

针对数据成员的指针:

const string Screen::*pdata = &Screen::contents; // 定义
auto s = myScreen.*pdata; // 使用

当然,也可以是成员函数的指针:

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

可以通过 function<> 将其转换为一个可调用对象,或者直接使用 mem_fn

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

嵌套类

在类中声明另一个类:

class TextQuery {
public:
class QueryResult;
}

然后在外部定义:

class TextQuery::QueryResult {
QueryResult(std::string);
}