Table of Contents
- 第10章 泛型算法
- 第12章 动态内存
- 第13章 拷贝控制
- 第14章 操作重载与类型转换
- 第15章 面向对象程序设计
- 第16章 模板与泛型编程
- 第17章 标准库特殊设施
- 第18章 用于大型程序的工具
- 第19章 特殊工具与技术
第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数据。
直接管理内存
可以直接用new
和delete
管理内存,但非常容易出错
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
不支持拷贝和赋值,但可以将指针的所有权从一个非const
的unique_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 拷贝控制和资源管理
管理类外资源必须定义拷贝控制成员
行为像值的类
保证一个对象赋予其自身时,赋值运算符能正常工作
- 将右侧运算对象拷贝到局部临时对象
- 销毁左侧对象的现有成员
- 将临时对象拷贝到左侧运算对象中
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
,表示不抛出异常。内存分配与释放的具体操作使用malloc
与free
。
#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限定符
当对象的值可能在程序控制范围之外被改变时(如系统时钟),将其声明为volatile
。volatile
的用法和const
基本一致,一个区别是:不是使用合成的拷贝函数初始化volatile
对象,或从volatile
对象赋值。
volatile int a;
#链接指示:extern “C”
c++使用链接指示指出任何非c++函数使用的语言。链接指示不能出现在类定义或函数定义内部,必须在函数的每个声明中都出现
extern "C" void f();
extern "C"{
void g();
void (*pf)(int); // C指针与C++不同
}