一. 纯虚函数
1.
class Vehicle
{
public:
virtual accelerete(double) = 0;
};
我们不希望出现Vehicle的对象,只是将Vehicle作为基类来使用,并且所有的派生类都重写accelerete方法,
甚至不希望有人调用Vehicle::accelerete()。
将accelerete定义为纯虚函数可以实现我们的要求。
在虚函数声明的参数列表后面加上“=0”:
class C
{
virtual void f() = 0;
};
我们不需要在此为纯虚函数C::f()提供任何定义。那些声明(或继承)了纯虚函数的类就是抽象类。任何试图创建
一个抽象基类对象的操作都将导致编译期错误的产生。
如果一个类派生自C 并重写了C::f(),它就将成为具体(非抽象)类:
class D:public C
{
void f();
};
我们通常都将抽象基类用于接口的声明,这时我们不需要为该接口声明一个完整的实现。该接口描述了所有派生自该类
的对象都应该支持的抽象操作;派生类必须实现这些抽象操作。
class Vehicle
{
public:
virtual double accelerete(double) = 0;
virtual double speed() = 0;
};
由于Vehicle是一个抽象类,任何创建Vehicle的对象的尝试都会导致编译期错误的产生。
Vehicle v; //编译期错误:Vehicle 是抽象类。
class Car: public Vehicle
{
public:
virtual void accelerete(double);
virtual void speed();
};
class Bicycle:public Vehicle
{
public:
virtual double accelerete(double);
virtual double speed();
};
由于在Car和Bicycle中,基类中的所有纯虚函数都被重写了,所以我们就可以得到这两个类创建出来的对象。
即使我们得不到Vehicle的对象,我们仍然可以使用指向Vehicle的指针和引用:
void full_stop(Vehicle &v)
{
v.accelerete(-v.speed());
}
一个继承(而没有重写)了纯虚函数的类同样也是一个抽象类:
class Land_Vehicle: public Vehicle
{
};
Land_Vehicle v; //编译期错误:Land_vehicle是个抽象类。
对于那些类似于Vehicle这种“用来描述一系列的派生类”的类,我们总是推荐使用纯虚函数和抽象类的做法。
析构函数不能说明为纯虚的:
class Vehicle
{
public:
virtual ~Vehicle() = 0; //不好的做法。
};
在Vehicle的任意派生类中,在调用它们的析构函数是都会调用到Vehicle::~Vehicle()。
由于我们必须为该析构函数提供一个定义(否则就会得到一个链接错误),因此将它声明为纯虚函数毫无意义。
二. 关于继承的细节和陷阱
1. 没有被继承的东西
当我们使用继承时,要时刻牢记,下面的这些东西没有从基类中被继承:
(1). 构造函数(包括复制构造函数)。如果我们没有声明复制构造函数,那么编译器将会自动为我们创建一个。
在这个被创建的复制构造函数中,它会调用类中所有的非静态的数据成员以及基类的复制构造函数。
(2). 析构函数。如果我们没有声明析构函数,并且所有的非静态数据成员和基类都有析构函数,编译器就会为
我们自动生成一个这样的析构函数:它会调用那些非静态数据成员以及基类中的析构函数。如果类中的某
个基类的析构函数是虚函数,那么这个合成的析构函数也将是虚函数。
(3). 赋值操作符。如果没有声明赋值操作符,编译器也会自动合成一个:它会去调用所有非静态数据成员和基
类中的赋值操作符。
(4). 被隐藏的成员函数。如果在基类中存在的成员函数在派生类中没有被重写,并且在派生类中还声明了一个
和该函数有着相同名字但参数列表不同的成员函数,那么在基类中的那个成员函数就将被隐藏。
例如:
class Car
{
public:
void steer(int degrees);
};
class AutoPilot
{
public:
AutoPilot();
};
class Smart_car:public Car
{
public:
void steer(AutoPilot); //Car::steer(int) 被覆盖了。
};
Smart_car c;
c.steer(45); //编译期错误:无法将AutoPilot转化为int
如果不希望隐藏基类中的函数,就应该在派生类中对他们进行重新声明:
class Smart_car : public Car
{
public:
void steer(int i)
{
Car::steer(i);
}
void steer(AutoPilot&);
};
2. 在派生类中指定virtual关键字
当重写一个虚函数(不管它是不是纯虚函数)时,不需要明确地使用virtual 这个关键字;编译器会自动将这个和
基类中有着同样名字及参数列表的函数视为虚函数:
class Base
{
public:
virtual void f();
};
class Derived: public Base
{
public:
void f(); //等同于“virtual void f()”.
};
不过最好还是指定virtual这个关键字,这会使阅读我们的代码的人更容易了解代码。并且使用virtual与否不会对
程序的意思带来任何影响。
3. 在构造函数和析构函数中调用虚函数
当在构造函数和析构函数中调用虚函数时,它的运行结果可能会有所不同。当构造函数正在创建派生类中的基类部
分时,被构造的对象就被视为基类(而不是派生类)的一个对象。这意味着调用的虚函数将会是正在被构造的基类中
的那个成员函数,而不是派生类中的成员函数。
例如:
class Base
{
public:
Base();
virtual void debug_print()
{
cout<< "Base::Base();/n" <<endl;
}
};
class Derived : public Base
{
public:
Derived();
virtual void debug_print()
{
cout<< "Derived::Derived();/n"<<endl;
}
};
Base::Base()
{
debug_print();
}
main()
{
Base b;
Derived d;
}
上面的程序会如下输出:
Base::Base();
Base::Base();
即使正在构造的是Derived中的Basebuf,在Base的构造函数中调用的debug_print也会是Base::debug_print().
出现此情况的原因是:对象的基类部分的构造要早于其数据成员。当Derived中的Base部分被构造时,Derived
中的其他数据成员还都没有被构造。此时调用Derived中的虚函数将变得毫无意义:因为它可能会去试图访问Derived
中那些还没有被初始化的数据成员。(换个角度来看,当Base的构造函数被调用时,正在被构造的对象实际上不是Derived,
因此 不可能调用到 Derived的成员函数。)
该逻辑也存在于析构函数中调用虚函数这种情况:
Base::~Base()
{
debug_print();
}
上面的析构函数调用中,调用的总是Base::debug_print(),即使当被摧毁的是Derived中的Base部分是也是如此。
当我们调用Base 的析构函数时,Derived中的数据成员已经早就被析构了,因此再去调用Derived中的debug_print()
也将变得毫无意义。
记住:只有在构造和析构的过程中调用虚函数才会导致这种特殊情况的出现。在其他情况下,虚函数的行为都是正常的:
Base::Base()
{
debug_print(); //调用的是Base::debug_print().
Base *bp = new Derived;
bp->debug_print(); //调用时的是 Derived::debug_print().
}
总结:
1. 继承描述的是is-a 的关系:派生类所实现的对象应该是基类所实现的对象集中的一个子集。
2. 当继承是接口的一部分时,请使用公有继承。只有当继承被用来隐藏实现细节时,才需要使用私有继承和
保护继承。
3. 对于私有继承的大部分使用,我们都可以用组合来替代;只有当派生类需要重写(私有)基类中的虚函数时,
才不能那样做。
4. 在派生类中被重写的虚函数应该和基类中的抽象模型相一致。
5. 构造函数,析构函数以及赋值操作符不能被继承。
多重继承总结:
1. 只有在派生类包括多个基类,并且这多个基类间没有继承关系时,我们才应该使用多重继承。
2. 使用虚基类来避免在同一对象中出现多分基类子对象。
3. 基类对象的构造顺序和它们在类声明中(而不是在构造函数的定义中)的顺序一样,析构相反。
4. 为每个基类明确地指定存取类型。