模板

可以用模板来定义函数(称为类属函数),或者定义类(称为类属类)。目的是对不同类型的对象采用相同的一套操作,写起来非常简单。

目的类似的做法还有宏、重载函数和函数指针的方法,但是都有较大的局限性。

模板的原理是,编译器判断需要某种版本的函数或类时,再根据模板填入合适的类型生成一个具体的函数或者类。

类属函数

使用template<class T1, class T2...>这样的语句放在需要使用未确定类型的前面,后面就可以把这个这个未确定类型当做真正的类型使用和书写。在使用的时候可以隐式实例化。例如:

#include <iostream>

template<class T>
void f(T a){
    std::cout<<a;
}

int main() {
    f(1); // 隐式实例化为int类型
    f(2.3);// 隐式实例化为double类型
    f<char>('a');// 显式实例化为char类型
}

除了可以带未确定类型参数,还可以带普通参数,比如template<class T, int size>,在下面就可以把size当做一个编译时已经确定的常量来使用。需要注意普通参数必须写在类型参数之后。在调用时必须进行显式地实例化模板size的值,不能让编译器自己判断类型了。比如func<int,10>(1),第一个参数是把int作为T的实际类型,第二个参数10是size的确定值,必须指出。

另外,还可以在模板的参数中使用默认值,类似于template<class T1=int, int size=100>,这样,如果在显式实例化的时候如果使用类似Stack<> s;的声明则默认使用int和100的版本。如果指定了普通参数的默认值,也不需要显示实例化。函数模板的默认值可以灵活的指定,例如可以template<class T1=int, class T2>这样写,但是类模板的参数要和函数的默认值那样,只能从右往左指定默认值,中间不能跳,template<class T1=int, class T2>的写法就不行了。这涉及到历史原因。

模板的定义也可以进行嵌套,例如template<template<typename T>>

模板可以和重载函数进行配合使用,会优先匹配重载函数,如果没有再匹配模板的版本。

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

int  x, y, z;
double l, m, n;
z = max(x,y);
l = max(m,n);

double max(int a, double b)
{  return a>b? a : b; }

//这里优先匹配上面的重载函数。由于类型不同,不匹配模板的版本
double c=max(x,m);

类属类

例如:

// 普通参数要在类型参数后面
template<class T, int size>
class Stack {
    T buffer[size];
public:
    void push(T x);
    T pop();
};

// 注意,此处也需要声明模板,并且必须在类名空间那里也有<T>
template<class T, int size>
void Stack<T,size>::push(T x) {...具体实现}

template<class T, int size>
T Stack<T,size>::pop() {...具体实现}

int main() {
    //类属类只能被显式实例化
    Stack<int,100> st1;
    Stack<int,200> st3;
    Stack<double,100> st2;
}

使用模板定义类,在使用的时候需要进行进行显式实例化。实际上编译器会根据实际需要的类型生成多个类的版本,而类中的静态成员属于实例化之后的类,不同模板参数的类不共享静态成员。

在使用默认的模板参数时,必须从右向左进行标识默认值,在上面提过了。

在C++中,模板的完整定义通常都放在头文件中,否则模板类的方法可能不能被实例化。由于一个模板被实例化的时间是在使用点,所以,如果在模块A中要使用模块B中定义的某模板的实例,而在模块B中未使用这个实例,则模块A无法使用这个实例。

异常处理

C++使用throw抛出异常,使用try监控异常并使用catch捕获异常。

  • try { 语句块 } 监控语句块中抛出的异常,要和catch配合使用。
  • throw <表达式> 。在catch内部的throw可以无参数,表示将捕获到的异常再次抛出
  • catch(<类型> [变量名] ) { 语句序列 }。捕获并在语句序列中定义处理措施。变量名可选。这里catch的类型也可以是任何类型,比如int和double,但是通常会使用自定义的异常类型。

如果异常抛出时刻,本身没有在try监控中,或者虽然监控了但是没有对应的catch捕获,则会自动抛出给调用者,在Java中还需要在方法声明中写出可能抛出的异常,但是C++中不需要。

另外在程序流程上需要注意,一旦有任何异常被抛出,程序会立刻终止当前的状态,后面的语句均不会被继续执行,而是直接跳转到能够捕获异常的catch中执行异常处理的语句。即使异常处理语句执行完毕,依然不会返回到终止前的语句继续执行,而是执行异常处理后面的内容。这里也就要求对于资源的占用在异常处理的时候要小心,否则异常终止很容易导致内存泄漏之类的问题。加入代码中没有任何位置可以捕获到抛出的异常,则会交给系统的abort进行处理,导致程序终止。

例如:

#include <iostream>
using namespace std;

void f(){
    throw 1;
    cout<<"throw 1"<<endl;
    throw 1.0;
    cout<<"throw 1.0"<<endl;
}

int main(){
    try {
        f();
    } catch (double) {
        cout<<"catch 1.0"<<endl;
    } catch (int) {
        cout<<"catch 1"<<endl;
    }
}

//输出:catch 1

从上面的例子看出,抛出异常后面的语句不会被执行。catch的时候不会进行隐式类型转换,只能匹配精确一致的类型或者该类型的派生类。捕获的时候依照catch的前后顺序依次尝试类型匹配。因为catch能够匹配派生类,所以多个catch的编排顺序要注意,如果不小心把catch基类放在catch派生类前面,派生类异常会被基类的catch率先匹配处理,派生类的catch就失去意义了。

下面这个例子展示了throw的一个特性:根据静态绑定类型抛出异常,而不是对象的实际类型。throw的时候会调用拷贝构造函数。

#include <iostream>
using namespace std;

class MyExceptionBase {};

class MyExceptionDerived: public MyExceptionBase {};

// 注意这里声明参数是Base
void f(MyExceptionBase& e) {
    // 一定是抛出Base类型的异常。如果实际类型是派生类,会发生对象切片
    throw e; // 调用基类拷贝构造函数(静态绑定)
}

int main() {
    MyExceptionDerived e;
    try {
        f(e);
    } catch (MyExceptionDerived& e) {
        cout << "MyExceptionDerived" << endl;
    } catch (MyExceptionBase& e) {
        cout << "MyExceptionBase" << endl;
    }
}

// 输出: MyExceptionBase

两个特殊语法:

  • catch(int){ throw; } 无参数throw,将捕获到的异常直接抛出。
  • catch(…) { } 捕获所有异常

还有另外一类系统自动调用的函数,例如构造函数和析构函数。应该在自己的函数内进行处理。构造函数的异常处理有一个特殊之处:初始化列表也可能抛出异常。

在一切函数调用的时候,传递参数时发生的异常都视为调用者抛出的异常,在调用者上下文中进行处理即可。

抛出异常会导致控制流跳转,可能导致多出口引发的处理碎片。在Java中使用finally来善后,但是C++中无finally。这是因为C++中有析构函数,析构函数会做好善后的工作。可以配合智能指针使用,可以很好的处理碎片。

另外,catch可以理解成一个函数,如果括号里的参数是值传递的话,也会发生拷贝构造:

#include <iostream>

class Error {
public:
    virtual void show() {
        std::cout << "error" << std::endl;
    }

    Error() {}

    Error(const Error &error) {
        std::cout << "copy error" << std::endl;
    }
};

class AgeError : public Error {
public:
    void show() override {
        std::cout << "AgeError" << std::endl;
    }
};

int main() {
    try {
        throw AgeError();
    } catch (Error e) {//捕获时拷贝
        e.show();
    } catch (Error& e){//捕获时不拷贝
        e.show();
        throw e;//带参数的throw,抛出时拷贝
    } catch (...){//默认以引用捕获,不拷贝
        throw; //不带参数throw,抛出时不拷贝
    }
}

I/O处理

三类IO操作:控制台IO,文件IO和字符串IO

控制台IO即从控制台读入和在控制台打印,文件IO就是文件读写,而字符串IO则是从字符串中读入和写字符串,把字符串当成输出的空间。

下面是把标准IO重定向到文件的示例。

#include <iostream>
#include <fstream>

using namespace std;

int main(){
    ifstream in("in.txt");
    streambuf *cinbuf = cin.rdbuf(); //save old buf
    cin.rdbuf(in.rdbuf()); //redirect cin to in.txt!

    ofstream out("out.txt");
    streambuf *coutbuf = cout.rdbuf(); //save old buf
    cout.rdbuf(out.rdbuf()); //redirect cout to out.txt!

    string word; 
    cin >> word; //input from the file in.txt
    cout << word << " "; //output to the file out.txt

    cin.rdbuf(cinbuf); //reset to standard input again
    cout.rdbuf(coutbuf); //reset to standard output again

    cin >> word; //input from the standard input
    cout << word; //output to the standard input
}

虚拟化构造器

虚拟化构造器解决的问题是:在仅知道对象的基类(接口)而不知道实际类型时,对对象进行拷贝。实现的方法是在基类中声明虚函数clone(),调用时返回自身的一份拷贝,而派生类可以根据自己的需要重定义这个拷贝的函数。如果基类需要做成接口或抽象类,可以把这个函数在基类中写成纯虚函数。

例如:

#include <iostream>
#include <vector>
using namespace std;

// 共同基类
class NLComponent {
public:
    virtual NLComponent *clone() const = 0;
};

//派生类1
class TextBlock : public NLComponent {
public:
    TextBlock *clone() const override;
};
TextBlock *TextBlock::clone() const {
    return new TextBlock(*this);
}

//派生类2
class Graphic : public NLComponent {
    Graphic *clone() const override;
};
Graphic *Graphic::clone() const {
    return new Graphic(*this);
}

//客户代码
class NewsLetter {
    vector<NLComponent *> components;

public:
    NewsLetter(const NewsLetter &rhs);
};

NewsLetter::NewsLetter(const NewsLetter &rhs) {
    vector<NLComponent *>::const_iterator it;
    for (it = rhs.components.begin(); it != rhs.components.end(); ++it)
        components.push_back((*it)->clone()); // 可见这里不需要知道对象实际类型也能正确拷贝了
}

最后,永远不要尝试在对象数组中使用多态的方法!因为C++数组一定是根据对象的大小来计算偏移量的,这意味着数组中每个元素的占用空间必须一样,否则数组将无法正确根据下标取回数据。因此,不应该把不同派生类和基类放在同一个数组中去利用他们的多态,因为派生类很可能比基类占更多空间。更好的做法是使用指针数组来完成,因为指针类型总是占同样的大小。