引言
面向对象的支持是C++较C的一大区别。面向对象的几个特点是封装、继承、多态。
1. 封装
封装的目的是为了让接口和实现分离,这种是逻辑上的分离,而不是在实现时一定要分开定义interface和class。通过封装,让类的使用者直接根据需要调用相应接口即可,无需关注接口的实现细节,可以理解为是一种数据抽象。
最典型的理解,我们可以结合标准模板库来思考,其中有序列式容器,关联式容器。
序列式容器
我们可以不关心序列式容器内部的实现机制,不管用数组还是用列表,但是对外提供了一致的接口,比如:push_back, pop_front等等类似的接口。
2. 继承
继承一方面是业务层面的理解,业务对象之间确实存在这样的父子关系;从代码层面为了代码复用。子类一方面可以使用跟自己特性无关的父类接口,另外一方面可以在自身重新定义跟自己特性相关的接口。
继承模式在GUI相关的技术中应用的特别多,看看MFC中的类继承体系就可知。
但是继承并不是唯一可以重用代码的途径,在我看来,组合的方式更易使用。过多的继承体系让人难以驾驭!
3. 多态
多态是统一代码的好方式,典型的用法是用父类引用或者父类指针对应各子类对象,并调用相同的函数名;通过动态绑定,运行的是各自对象的各自实现。相对于通过if else的各种判断,用多态的办法,一句话就OK。
注意点
在设计实现类时,有一些问题需要重点考虑。构造函数、析构函数、拷贝构造函数、赋值操作符重载。
构造函数
构造函数负责在创建对象时,对对象成员的初始化。 这里有个注意的地方,就是初始化列表以及在构造函数体内的赋值操作。在一般情况下,这两种操作并无区别,但是如果类成员中有const类型成员或者引用,这种只能初始化,不能赋值。
class A
{
public:
A(int a_, int b_)
{
a = a_;
b = b_;
}
private:
const int a;
int& b;
};
error C2789: “A::a”: 必须初始化常量限定类型的对象
error C2530: “A::b”: 必须初始化引用
error C2166: 左值指定 const 对象
可以看到,这种情况下编译器会报错。
拷贝构造函数
拷贝构造函数是用同类型的对象去初始化自身。一般定义格式是:
A(const A& rhs);
类的实现着自己定义相同类型不同对象之间的拷贝行为。一般涉及到指针情况时,自己去实现该函数,比较安全;
赋值操作符
赋值操作符是用同类型的对象给自身赋值,一般定义为:
A& operator=(const A& rhs)
对于赋值操作符,要考虑不能自身赋值自身的问题。
析构函数
析构函数是当对象生命周期到达时,被自动调用的函数,非用户主动调用。如果对象中申请了资源,就在析构函数中进行释放。一般在存在继承关系的情况下,将析构函数定义为虚函数。
如果某些类的对象作者认为在内存中应该只有一份,不应该拷贝来拷贝去,那么可以将类实现为uncopyable类型。怎么实现呢?
private:
A(const A& rhs);
A& operator(constA& rhs);
将类的拷贝构造函数和赋值操作符设置为private即可。当发生赋值操作值,由于调用了private类型的方法,是非法操作。编译器无法通过。
新版本的C++可以通过如下方式做到禁用拷贝,将涉及到拷贝的函数声明为delete
class C
{
public:
C(int a_, int b_) : a(a_), b(b_)
{}
C(const C& rhs) = delete;
C& operator=(const C& rhs) = delete;
int a;
int b;
};
当编写了拷贝代码时,编译器会报如下错误:
error C2280: “C &C::operator =(const C &)”: 尝试引用已删除的函数
附
在某些领域,使用面向对象方法可以很好的提炼出各种业务模型和接口,比如说做GUI,使用面向对象的方法就非常自然;在其他的系统中,我们有时候会使用基于对象的方法去做开发,降低程序的复杂度。