模板
可以用模板来定义函数(称为类属函数),或者定义类(称为类属类)。目的是对不同类型的对象采用相同的一套操作,写起来非常简单。
目的类似的做法还有宏、重载函数和函数指针的方法,但是都有较大的局限性。
模板的原理是,编译器判断需要某种版本的函数或类时,再根据模板填入合适的类型生成一个具体的函数或者类。
类属函数
使用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++数组一定是根据对象的大小来计算偏移量的,这意味着数组中每个元素的占用空间必须一样,否则数组将无法正确根据下标取回数据。因此,不应该把不同派生类和基类放在同一个数组中去利用他们的多态,因为派生类很可能比基类占更多空间。更好的做法是使用指针数组来完成,因为指针类型总是占同样的大小。