C++摘要

Published: by Creative Commons Licence

  • Tags:

指针和引用

int a = b;

上面这句代码,a和b除了值一样,是独立的两个变量,修改a不会对b产生任何影响。

int &a = b;

上面这句代码,a是b的一个别名,换句话说a和b共享同一块内存区域,修改a的同时b也会被修改。引用必须在定义的时候提供别名对象。

int *a = &b;

上面这句代码,a是指向b内存区域的一个指针,我们可以通过指针间接操作b占用的内存区域。

指针运算

template<typename T> T *next(T* ptr){
    return ptr + 1;
}
template<typename T> T *prev(T* ptr){
    return ptr - 1;
}

上面演示了指针的运算,虽然指针是以整数的方式存储的,但是指针的运算和整数的运算不同。对指针加1,等价于将指针赋值为&ptr[1],即指针的值实际上增加了sizeof(T)。减法相同。

函数默认值

void foo(int a, int b = 1, int *c = 0)
{
    ...
}


foo(1, 2);

我们可以为函数参数提供默认值,有默认值的参数可以传递也可以不传递。

一个参数可以有默认值的前提条件是所有右侧的参数都具有默认值。一个参数在函数调用时可以传递的前提条件是所有左侧参数都被传递了。

函数的默认值可以在函数声明的时候提供,也可以在函数定义的时候提供,但是不能在两处都提供。一般推荐在函数声明的时候提供,因为函数声明一般出现在头文件中, 具有更好的可见性。

变量作用域

int a;

上面在文件作用域定义了一个int对象。它在可见区域是整个文件。对象会在程序启动时创建,并且被初始化为0,程序结束时销毁。

void foo()
{
    int a;
}

上面我们在栈上创建了一个int对象,这个对象仅在函数中可见。这个对象在函数调用时创建,而在函数结束时被销毁。对象不保证被初始化,其可以是任意值。

void foo()
{
    static int a;
}

上面我们创建了一个函数静态变量,变量在函数中可见。对象会在程序启动时创建,并且被初始化为0,程序结束时销毁。

int *foo(){
    return new int[10];
}

上面我们在堆上面分配了一个大小为10的数组,并且返回。堆上分配的对象只有在显式调用delete命令时才会被销毁。

函数内联

inline int max(int a, int b){
    return a <= b ? b : a;
}

上面我们演示了函数的内联。由于函数调用具有较大的开销,为了避免开销,我们可以用inline关键字将函数标志位内联函数,编译器会在函数调用处将函数调用替换为函数的内容,从而避免调用开销。但是是否执行内联取决于编译器的选择。一般只有简单短小的函数会被内联。

内联函数的定义一般要放在头文件中。因为编译器在编译期执行内联,因此内联函数在头文件中必须可见。

函数重载

void foo(int a){}
double foo(double a){}

上面我们定义了两个同名函数,但是参数列表不同。这种就叫做函数重载。当你调用函数时,编译器会根据你的参数列表选择合适的函数。注意返回值类型不能作为重载的依据。

函数模板

template<typename E>
void foo(vector<E> &vec){}

上面用template关键字定义了一个函数模板。对于不同的模板参数,都会产生一份函数的副本。我们可以利用函数模板实现快速创建多份函数体类似的重载函数。

foo<int>(vector<int>());
foo<double>(vector<double>());

上面代码的两次调用会创建两份重载函数。

template<typename E>
void foo(vector<E> &vec){}
template<typename E>
void fool(set<E> &set){}

上面展示了如何重载函数模板。

函数指针

const vector<int> (*funcPtr)(int) = 0;

上面我们定义了一个名字为funcPtr的指针变量,这个变量可以指向一个入参为一个int参数,返回值为const vector<int>的函数。

vector<int> ans = funcPtr(1);

上面我们调用了funcPtr并取得了返回值,这相当于调用了funcPtr指向的函数。

const vector<int> foo(int x){}

const vector<int> (*funcPtr)(int) = foo;

上面我们将funcPtr变量指向了foo函数。

函数指针也可以指向类成员函数。

class IntHolder{
public:
    typedef void (*FuncPtr)(int);
    void inc(int n)
    {
        _val += n;
    }
private:
    int _val;
};

IntHolder holder;
IntHolder::FuncPtr func = &IntHolder::inc;
(holder.*func)(10);
((&holder)->*func)(10);

上面我们演示了怎么指向函数指针,以及如何调用指针指向的函数。其中.*->*是专门设计用来调用成员方法的运算符。

头文件

由于在使用函数之前,必须声明它。而不同的源文件中可以需要包含同样的声明内容。我们可以把这些公共的声明部分,提取到头文件中,这样就可以减少编码量。

由于声明可以出现多次,但是定义只能有一次,因此,头文件中不会包含函数定义,函数定义一般放在源文件中。

定义只能有一次这个规则存在一些特例:

  • inline函数在编译期展开,因此必须把定义放在头文件中。
  • const修饰的变量的作用域为所在源文件,因此可以放在头文件中。
  • 模板函数、模板类在编译期创建副本,因此必须把定义放在头文件中。
inline void swap(int &a, int &b){a ^= b; b ^= a; a ^= b;}
template<typename T> T& min(T& a, T& b){return a < b ? a : b;}
const double PI = 3.1415926;

上面就是一个合法的头文件内容。但是需要注意的是头文件中不允许出现一般变量的定义,因为变量只能定义一次。下面的案例是错误的:

int id;

如果有多个源文件包含了这个头文件,那么id变量就出现在了多个源文件中,这会导致编译失败。

extern int id;

上面用extern关键字正确的声明了变量。

#include<iostream>
#include "MyHeader.h"

上面我们演示了如何包含源文件,尖括号用于包含标准的头文件,而引号用于包含自定义的头文件。

STL泛型算法

在algorithm头文件中声明了下面算法。

分类 算法
search find, count, adjacent_find, find_if, count_if, binary_search, find_first_of
sort merge, partial_sort, partition, random_shuffle, reverse, rotate, sort
copy, deletion, substitution copy, remove, remove_if, replace, replace_if, swap, unique
relational equal, includes, mismatch
generation, mutation fill, for_each, generate, transform
numeric accmulate, adjacent_difference, partial_sum, inner_product
set set_union, set_difference

上面的很多算法允许接受一个函数对象作为入参,来实现策略模式。我们可以在functional头文件中找到许多预先实现好的函数对象。

分类 类名
arithmetic plus, minus, negate, multiplies, divides, modules
relation less, less_equal, greater, greater_equal, equal_to, not_equal_to
logical logical_and, logical_or, logical_not

STL容器

下面列出所有容器都支持的方法。

方法 解释
==, != 判断容器是否相同(不同)
= 拷贝容器
empty 判断容器是否为空
size 返回容器中元素数目
clear 删除所有元素
begin 返回容器头部迭代器,指向容器第一个元素
end 返回容器尾部迭代器,指向容器尾部后一个元素
insert 插入一个或多个元素到容器中
erase 从容器中删除一个或多个元素

STL中的vector,deque,list是序列型的,它们中元素按照插入位置有前后的概念。因此它们提供了下面额外的接口。

方法 解释
front 获取容器最头部的元素
back 获取容器尾部的元素
push_front 在容器头部插入一个元素
push_back 在容器尾部插入一个元素
pop_front 删除头部元素
pop_back 删除尾部元素

STL中的map代表的是一种映射关系,其保存的元素是pair类型,由keyvalue组成。

要查询与某个key关联的值,可以通过:

map<string, string> mobiles;
cout << mobiles["dalt"];

上面的例子中假如使用的key-"dalt"没有被提前加入到map中,那么map会创建一个值类型的默认值并关联key加入到map中。因此用这种方式判断一个关键字是否存在于map中会导致副作用。map还提供了另外两个方法findcount,前者返回位置在关键字处的迭代器(如果没有则返回end),后者返回这个关键字在map中的出现次数,这两个操作没有副作用。

迭代器

我们可以用指针加偏移的方式遍历数组或vector,但是有一些数据结构的内存不是连续的,比如链表,树。因此迭代器是在这些数据结构遍历操作上的一次封装,提供了一致的接口。你可以通过*iter获取迭代器当前指向的元素,或者用iter->访问迭代器当前指向的元素指针的成员。

头文件iterator中定义了一个迭代器的适配器,用于为一些不支持迭代器操作的类装配一致的迭代器接口。

比如istream_iterator可以将输入流封装为迭代器。

istream_iterator<string> is(cin);
istream_iterator<string> eof;

上面的代码将标准输入流适配成了迭代器,而第二行代码没有提供入参,其对应的就是文件结尾。

同理也提供了类似ostream_iterator类,用于将输出流适配为迭代器。

ostream_iterator<string> os(cout, " ");

class Stack
{
public:
    void push(int x);
    int pop();
    int size(){return _stack.size();}
    bool empty();

private:
    vector<int> _stack;
};

上面是一个类的声明,private块中的成员仅能在类内部被访问,public块中的成员可以在任意处被访问。

一个成员函数的定义可以放在类中,也可以放在类外。放在类中将被自动视作是内联函数(比如size是内联函数)。如果放在类外,且希望内联,我们需要在定义之前加上inline关键字。

inline bool Stack::empty()
{
    return _stack.empty();
}
int Stack::pop()
{
    int ans = _stack.back();
    _stack.pop_back();
    return ans;
}
void Stack::push(int x)
{
    _stack.push_back(x);
}

上面演示了如何在类外定义成员函数,函数empty前面加了inline关键字,因此被视作内联函数,其余函数则是非内联的普通函数。

类的声明一般要放在头文件中,遵循头文件的规则,内联函数必须在头文件中定义,而非内联函数则需要定义在源文件中。

new和delete

new用于分配内存并创建对象,而delete用于销毁对象并回收内存。

int *intptr = new int();
delete intptr;

new也可以用于创建数组,但是对应的我们需要使用delete[]销毁数组。

int *arr = new int[10];
delete[] arr;

构造器

构造器负责初始化类成员。

class Triangle
{
public:
    Triangle();
    Triangle(int len);
    Triangle(int len, int beg_pos);
};

上面定义了三个构造器,构造器是没有返回值且函数名与类名相同的成员方法。构造器也支持重载。如果你没有提供任何构造器,编译器会帮你创建一个不接受任何参数的空构造器。

Triangle t1; //调用默认构造器
Triangle t2(1);
Triangle t3(1, 2);
Triangle t4 = 1; //等价于Triangle t4(1);
Triangle t5(); 

上面的五行是初始化代码,其中第一行第一行调用了默认构造器,而第四行则是调用了只有一个参数的构造器。第五行是错误的,因为第五行你实际上并不是调用默认构造器,而是声明了一个返回类型为Triangle的函数,正确的调用默认构造器的方法就是第一行。

class Triangle
{
public:
    Triangle(int len, int bp): _name("Triangle")
    {
       // ...
    }
private:
    string _name;
    int _next, _length, _beg_pos;
};

构造器可以通过成员初始化列表将参数提供给自己成员的构造器,成员的构造器会在类的构造器调用之前被调用。

我们可以通过下面的方法显式的调用构造器。这样即使常量成员也是可以修改的,实际上下面的方法是复用了this代表的内存,并重新初始化。

class Integer{
private:
    const int i_;
public:
Integer(int i): i_(i){}
void reinit(){
    new (this) Integer(1);
}
}

如果只是希望在构造器中调用其他构造器,还有一种叫做转发的方式。

class Integer{
private:
    const int i_;
public:
Integer(int i): i_(i){}
Integer():Integer(0){}
}

析构器

析构器负责释放成员资源。

class Matrix
{
public:
    Matrix(int row, int col): _row(row), _col(col)
    {
        _pmat = new double[row * col];
    }
    ~Matrix()
    {
        delete[] _pmat;
    }
private:
    int _row, _col;
    double *_pmat;
};

析构器没有返回函数,名称必须与类名相同,不允许有参数。一般情况下在析构器中我们只需要回收分配在堆上的内存。

浅拷贝和深拷贝

如果你没有定义复制拷贝函数,一般会使用默认的复制拷贝函数,这个函数简单复制拷贝所有的成员变量(浅拷贝)。

如果你在构造器中分配内存给某个成员指针,而在析构器中释放,那么使用复制拷贝函数将会导致两个对象共享了同一块内存,而当一个对象被回收时,另外一个对象对内存的访问就是非法的了。这时候你需要提供自定义的复制拷贝函数实现深拷贝。

class Matrix
{
public:
    Matrix(int row, int col): _row(row), _col(col)
    {
        _pmat = new double[row * col];
    }
    Matrix(const Matrix &mat)
    {
        (*this) = mat;
    }
    Matrix &operator=(Matrix &mat)
    {
         //深拷贝
        _row = mat._row;
        _col = mat._col;
        _pmat = new double[_row * _col];
        for(int i = 0; i < row * col; i++)
        {
            _pmat[i] = mat._pmat[i];
        }
        return *this;
    }
    ~Matrix()
    {
        delete[] _pmat;
    }
private:
    int _row, _col;
    double *_pmat;
};

复制拷贝函数是构造器的一个重载,其接受的参数为const ClassType &类型的不可修改引用。

const和mutable

const int a;
const vector<int> b;

const修饰符用于修饰变量的时候表示这个变量是不可以修改的。一个不可修改的变量,我们只能调用它的被const修饰的成员方法。

class Triangle
{
public:
    int length() const {return _length;}
    int beg_pos() const;
};

int Triangle::beg_pos() const
{
    return _beg_pos;
}

上面演示了如何使用const修饰符修饰成员函数。一个成员函数被修饰为const函数,那么它就不能直接或间接修改自己的成员变量。如果方法在类外定义,那么这个方法的声明和定义上都需要加上const修饰符。

class Triangle
{
public:
    int& length() const {return _length;}
};

上面我们在const方法中将自己的字段暴露到外部去,这会导致编译错误。但是由于const修饰符也是一种重载的标志,因此我们可以提供两个函数。

class Triangle
{
public:
    int& length() {return _length;}
    const int& length() const {return length;}
};

这样我们对一个用const修饰的Tirangle对象调用某个方法,则会调用const修饰的版本,而对一个不用const修饰的Triangle对象调用某个方法,则会调用非const版本。

但是假如我们必须面对一个场景,const修饰的方法必须修改成员变量的话,我们可以利用mutable关键字修饰这个成员变量,表示这个成员变量的修改不会影响整个对象的不可变性。

class Triangle
{
public:
    void length(int length) const {_length = length;}
private:
    mutable int _length;
};

this

Triangle &Triangle::copy(Triangle &other)
{
    //...
    return *this;
}

Triangle &c = a.copy(b);

上面这个方法将other的字段拷贝给自己后将自己返回。this用于表示指向自己的指针。

其实现方式是编译器将每个函数得参数列表之前插入参数this,而在调用时则提供调用方作为参数。

Triangle &copy(Trianglee *this, Triangle &other)
{
    //...
    return *this;
}
Triangle &c = copy(a, b);

静态成员

class Buffer
{
private:
    static int _buf_size;
};

int Buffer::_buf_size = 1024;

静态成员变量是唯一一份的对象。

静态成员声明的时候用static修饰,定义的时候不需要。静态成员的生命周期类似于定义在文件作用域的全局变量,因此不能将定义放在头文件中,当然如果你用const修饰它,那么就需要直接放在头文件中了。

class Buffer
{
private:
    static int _buf_size = 1024;
    int _buf[_buf_size];
};

类方法中访问静态成员跟访问普通成员无异。

static不仅能修饰成员变量,也可以修饰方法。如果一个方法不会使用到任意非静态成员变量,那么这个方法就可以修饰为静态方法。

class Triangle
{
public:
    static bool equals(const Triangle *a, const Triangle *b);
};

bool Triangle::equals(const Triangle *a, const Triangle *b)
{
    return a == b;
}

静态方法的声明处需要用static修饰,定义的时候不需要。

要在类外部访问某个类公开的静态成员,需要在成员名称前加上类名::

bool is_equal = Triangle::equals(0, 0);

运算符重载

class IntHolder
{
public:
    bool operator==(IntHolder &other)
    {
        return _val == other._val;
    }

    int _val;
}

我们可以重载运算符。只需要用operator修饰运算符即可。

运算符重载的规则如下:

  • 不可以引入新的运算符,除了.,.*,::,?:4个运算符外,其它的都可以被重载。
  • 运算符的操作数不可改变。
  • 运算符的优先级不可改变。
  • 运算符函数的参数列表中,至少一个参数为class类型,即我们不能重新定义指针、整数等之间的运算。

运算符函数可以是成员函数,也可以是普通函数。

bool IntHolder::operator==(IntHolder &other)
{
    return _val == other._val;
}
bool operator==(IntHolder &a, IntHolder &b)
{
    return a._val == b._val;
}

++和–运算符由于有前置和后置两种情况,而这两种情况的参数都是相同的,因此没法进行重载定义两套。于是C++语言提供了一种变通的方法,后置版本的运算符函数会带上一个int类型的参数(这个参数在调用时由编译器提供0)。

class IntHolder
{
public:
    int operator++(int ignore)
    {
        return _val++;
    }
    int operator++()
    {
        return ++_val;
    }
}

C++中重载了<<>>运算符,用于将对象写出到输出流中以及从输入流中读取对象。我们可以复用这个约定,来实现对对象的写入和写出。

class IntHolder
{
public:
    friend ostream& operator<<(ostream& os, const IntHolder &x);
    friend istream& operator>>(istream& os, IntHolder &x);
private:
    int _val;
};

ostream& operator<<(ostream& os, const IntHolder &x)
{
    return os << x._val;
}
istream& operator>>(istream& os, IntHolder &x)
{
    return os >> x._val;
}

这里我们一般通过普通函数来实现<<>>运算符的重载,因为成员方法运算符重载必须把this作为第一个参数,这样我们必须以下面的方式调用方法。

holder << cout;

类型嵌套

利用typedef关键字可以在某个作用域内为某种类型定义别名。比如我们会为vector类中定义一个别名iterator,这样我们就能通过vector<int>::iterator来表示这个迭代器的类型。

class Whatever{
public:
    typedef vector<int> vi;
    vi _var;
};

Whatever w;
Whatever::vi var = w._var; 

友谊

由于类中存在私有成员,如果我们希望私有成员被非类内方法访问,那么我们可以利用friend关键字将类中的私有成员的访问权限授予外部函数。

class IntHolder{
public:
    friend bool operator==(IntHolder &a, IntHolder &b);
private:
    int _val;
};

bool operator==(IntHolder &a, IntHolder &b)
{
    return a._val == b._val;
}

我们只要将用friend关键字修饰的函数声明放到类内,就可以将类中私有成员的访问权限授予给该函数。

我们也可以类似地建立类与类之间的友谊。

class IntHolder{
public:
    friend class IntHolderEqual;
private:
    int _val;
};

class IntHolderEqual{
public:
    bool operator()(IntHolder &a, IntHolder &b)
    {
        return a._val == b._val;
    } 
};

类似地将带friend修饰的类的声明放到需要权限的类中即可。

继承和多态

class LibMat
{
public:
    LibMat(){cout << "LibMat::LibMat()" << endl;}
    virtual ~LibMat();
    virtual void print() const = 0; //pure virtual function
    virtual LibMat *self(){return this;}
};
LibMat::~LibMat(){cout << "LibMat::~LibMat()" << endl;}

class Book : public LibMat
{
public:
    Book(const string &title, const string &author): _title(title), _author(author) {cout << "Book::Book()" << endl;}
    ~Book(){cout << "Book::~Book()" << endl;}
    void print() const
    {
        LibMat::print();
        cout << "Book::print()" << endl;
    }
    Book *self(){return this;}
protected:
    string _title;
    string _author;
};
class AudioBook: public Book
{
public:
    AudioBook(const string &title, const string &author, const string narrator)
        :Book(title, author), _narrator(narrator){}
    ~AudioBook(){cout << "AudioBook::~AudioBook()" << endl;}
    AudioBook *self(){return this;}
protected:
    string _narrator;
};

LibMat *ptr = AudioBook("example", "unknown", "nothing");
ptr->print();

LibMat &ref = AudioBook("example2", "unknown", "nothing");
ref.print();

上面展示了如何进行继承,以及使用虚函数实现多态。子类的构造器在父类构造器调用后调用,子类析构器在父类析构器调用之前调用。我们可以利用父类的指针和引用,指向子类,来实现多态。protected修饰的成员只有在自己和自己的子类中可以被访问。

利用virtual关键字我们可以将一个函数声明为虚拟。虚拟函数与普通函数的区别在于,普通函数的调用在编译期间被决定调用哪个方法,而虚拟函数在执行期间动态决定具体调用哪个方法。普通函数调用是根据调用指针或引用的类型决定的,不存在多态,而虚拟函数可以支持多态。如果一个方法在父类被声明为虚拟函数,那么子类的覆盖版本也默认是虚拟函数(不需要加virtual关键字)。虚拟函数的定义如果发生在类外,那么这时候只需要在声明上加virtual而定义时不需要。

虚拟函数的多态性质在构造器和析构器中不会发生。当在构造器中调用某个虚拟函数,如果调用的是子类的虚拟函数,而这个虚拟函数依赖某个成员的初始化,那么就会有问题,由于这个原因,构造器和析构器中调用的虚拟函数都是静态解析的。

一个类如果要支持多态,我们需要将它的析构函数以及所有子类的析构函数定义为虚拟函数,否则使用多态的情况下,删除指针会导致只有父类的析构器被调用。

在LibMat函数中,我们通过在虚拟函数声明尾部加上= 0将其声明为纯虚函数,纯虚函数没有方法体。一个类中如果有纯虚函数,那么这个类就是抽象类,抽象类是不可实例化的。

如果子类有与父类同名的成员变量或者相同参数列表的同名函数,那么子类的成员就会覆盖父类的成员。如果子类希望调用父类的成员,需要在成员之前加上类名限定符,比如Book中调用LibMat::print(),就可以调用LibMatprint方法。

如果子类要覆盖父类方法,必须方法的参数列表、方法名、const修饰符、返回值完全一致才行。但是有一个例外,如果父类返回某个基类的引用或指针,则子类可以返回该基类的某个子类的引用或指针。

RTTI

头文件typeinfo中提供了typeid方法,用于子执行期进行反射操作。

complex<double> x;
type_info ti = typeid(x);

type_info对象还支持==!=,我们可以利用这个性质来判断对象是否相同类型。

static_cast方法可以用于强制转换指针类型。

if(typeid(*ps) == typeid(Fibonacci))
{
    Fibonacci *pf = static_cast<Fibonacci*>(ps);
    pf->gen_elems(64);
}

由于static_cast是强制转换,因此需要提前用typeid来确定转换可以进行。dynamic_cast运算符提供有条件的转换,它在执行期进行校验,如果转换无法进行,那么就返回0,否则返回转换后的结果。

if(Fibonacci *pf = dynamic_cast<Fibonacci*>(ps))
    pf->gen_elems(64);

类模板

template<typename T>
class BinaryTree;

template<typename E>
class BTNode
{
    friend class BinaryTree<E>;
};

BTNode<string> node;

上面演示了如何声明模板类之间的友元关系,以及如何定义一个模板类变量。

template<typename E>
class BTNode
{
public:
    BinaryTree();
    bool empty() {return _root == 0;}
};

template<typename E>
inline BinaryTree<E>::BinaryTree() : _root(0){}

如果你把函数定义放在类内,那么模板类的写法与普通类的写法无异,如果你放在类外,那么就需要进行变更。

实现模板类时,初始化类成员的操作应该在构造器的参数列表完成,而不是手动在构造器赋值,因为你在定义类的时候无法确定调用者传入的类型参数时基础类型还是自建的类。

template<typename E>
class BTNode
{
public:
    BTNode(const E& val): _val(val){}
    //BTNode(const E& val){_val = val;}
private:
    E _val;
}

下面我们在类中定义友元函数。

template<class E>
class BinaryTree
{
    template<class T>
    friend ostream& operator<<(ostream &, const BinaryTree<T>&);
};

template<T>
inline ostream& operator<<(ostream &os, const BinaryTree<T> &bt)
{
    //...
}

模板参数不一定需要是类型,也可以是整数等常量,甚至可以有默认值。

template<int R, int C, typename E=double>
class Matrix
{
private:
    E _mat[R][C];
}

模板类中可以有模板函数:

template<typename T>
class Holder
{
public:
    template<typename C> 
    C cast();
private:
    T _val;
};

template<typename T>
template<typename C>
C Holder<T>::cast(){return dynamic_cast<C>(_val);}

模板的特化和偏特化

template<class T1, class T2>
class A
{
public:
    T1 first;
    T2 second;
};

template<class T>
T max(const T a, const T b)
{
    return a > b ? a : b;
}

上面是模板类和模板函数,我们可以为其某些模板副本提供特殊实现。

template<>
class A<int, double>
{
public:
    int first;
    double second;
};

template<>
int max<int>(const int a, const int b){
    return abs(a) > abs(b) ? a : b;
}

全特化是在所有模板参数确定的情况下的特殊化,而偏特化,则可以根据一部分参数进行特化。

template<T2>
class A<int, T2>
{
public:
    int first;
    T2 second;
}

上面是偏特化的例子,函数是不可以偏特化的。

异常处理

void check(int index)
{
    if(index >= range)
    {
        throw iterator_overflow(index, range);
    }
}

void get(int index)
{
    try
    {
        check(index);
    }
    catch(int errno)
    {
        //
    }
    catch(string)
    {
        //
    }
    catch(iterator_overflow &iof)
    {
        //
        throw; //重新抛出异常
    }
    catch(...) //捕获所有类型的异常
    {
        //
    }
}

throw用于抛出异常,异常可以是任何对象。catch用于捕获特定类型的异常对象,catch的时候类型是必须指定的,但是捕获的异常名称可以忽略。在catch块中可以利用throw命令将异常重新抛出。用...可以捕获所有类型的异常。

如果某个异常抛出后没有被捕获,那么会调用标准库的terminate()——其默认行为是在中断整个程序的执行。

异常有时候会导致资源没有正确释放。

void f()
{
    int *p = new int;
    m.acquire();

    process(p);
    
    m.release();
    delete p;
}

如果process函数抛出异常,那么资源就不会被正确释放。我们可以用auto_ptr来管理,因为异常抛出会导致退栈,对应的就会调用栈上分配的对象的析构函数。

#include<memory>

class MutexLock
{
public:
    MutexLock(Mutex m) : _lock(m){_lock.acquire();}
    ~MutexLock(){lock.release();}
private:
    Mutex &_lock;
};
void f()
{
    auto_ptr<int> p(new int);
    MutexLock ml(m);
    process(p);
}

要沿用标准库的异常,需要继承exception基类,它的what方法用于返回异常描述信息。我们只需要覆盖这个方法即可。

类型转换

类型转换运算符是一种特殊的成员函数,它负责将一个类的对象转换为其它类型。类型转换运算符的形式为:

operator type() const;

其中type表示一种特定类型。

类型转换函数必须是类的成员函数,且不能有返回类型,参数列表必须为空(const不是强制的)。

enable_if

考虑我们实现一个数值的包装类。

template<class T>
struct Num {
    T val;
    Num(T _val): val(_val) {}
    Num<T> modular(const Num<T>& rhs) {
        return val % rhs.val;
    }
};

很显然只有整数类型才能实现模运算。而不是整数类型会导致编译错误。

    Num<f32> a = 1.0;
    Num<f32> b = 1.0;
    a.modular(b);

编译报告的错误是: error: invalid operands of types 'float' and 'const float' to binary 'operator%'。错误告诉我们是Num这个类有问题,而不是调用者的问题,这很显然会误导使用者。C++允许我们使用enable_if来为泛型参数增加边界条件。

template<class T>
struct Num {
    T val;
    Num(T _val): val(_val) {}
    //只有当T时整数的时候,这个函数才会出现
    enable_if_t<is_integral_v<T>, Num<T>> modular(const Num<T>& rhs) {
        return val % rhs.val;
    }
};

再次编译会发现报告的错误是:'struct Num<float>' has no member named 'modular',这个错误明显明显的多。

有时候如果我们希望根据泛型参数选择使用的方法,会出现一些问题,比如下面的代码:

template <bool P> struct A {
  enable_if_t<P, bool> work() {
    return true;
  }
  enable_if_t<!P, bool> work() {
    return false;
  }
};

int main() {
  A<true> a;
  A<false> b;
  b.work();
  cout << a.work() << ' ' << b.work();
}

上面的代码会报下面异常:error: no type named 'type' in 'struct std::enable_if<false, void>'

原因可以从下面链接得知:Selecting a member function using different enable_if conditions

简单说就是只有发生模板参数替换的时候才会触发SFINAE,但是这里参数B确认后再调用work方法后并不会发生模板参数替换。

因此解决方法就是在work方法上加上新的模板参数。

下面是改后的代码:

template <bool P> struct A {
  template <bool PP = P> std::enable_if_t<PP == P && PP, bool> work() {
    return true;
  }
  template <bool PP = P> std::enable_if_t<PP == P && !PP, bool> work() {
    return false;
  }
};

匿名函数

C++允许我们使用匿名函数。

auto sort_func = [&](auto i, auto j) mutable -> bool {
    return a[i] < a[j];
};
sort(foo.begin(), foo.end(), sort_func);

其中起始的是捕获子句:

  • []:不捕获任何变量
  • [&]:按引用捕获变量
  • [=]:拷贝外部变量

mutable是可选的,如果指定则表示可以修改按值捕获的引用。

decltype

decltype是C++提供的一个关键字,它的用法为decltype(expression),它会在编译期被替换为expression的类型。比如decltype(1 + 1)会被替换为int

参考资料