引言
面向对象编程基于三个基本概念:数据抽象、继承和动态绑定。
在C++中,我们用类进行数据抽象。用类派生从一个类继承另一个类:派生类包含基类的所有成员。动态绑定,编译器在运行时决定是用基类定义的函数版本还是派生类定义的版本。
继承和动态绑定从两个方面简化了我们的程序:
- 通过继承,程序员可以很容易地编写那些相似但是不相同的新类,比如从一个父类派生出两个新类,那么这两个新类将十分相似。
- 通过动态绑定,程序员可以很容易地编写忽略这些相似类之间差异的程序。
许多应用程序的特性可以用一些相关但略有不同的概念来描述。例如,书店可以为不同的书提供不同的定价策略。有些书按照原价出售,而另一些书可以根据不同的折扣策略出售。面向对象编程与这种应用非常匹配。通过继承可以定义一些类型,来模拟不同种类的书,通过动态绑定来编写程序,使用这些类型而又忽略它们之间的差异。
概述
面向对象编程的关键思想是多态性(polymorphism)。
- 继承:共享公共的东西,仅仅特化本质上不同的东西。派生类能够继承基类的成员,派生类可以无需改变地使用那些与派生类特性无关的操作,派生类可以重定义那些与派生类相关的成员函数。
- 继承层次:我们称因继承而相关联的类的集合为一个继承层次。
- 动态绑定:程序可以无须关心对象的具体类型地使用继承层次中任意类型的对象。
* 在C++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。
定义基类和派生类
定义基类
class Item_base {
public:
Item_base(const std::string book = "", double sales_price = 0.0)
: isbn(book), price(sales_prices)
{}
std::string book() cosnt { return isbn; }
virtual double net_price(std::size_t n) cosnt {
return n * price;
}
virtual ~Item_base() {}
private:
std::string isbn;
protected:
double price;
};
定义派生类
class Bulk_item : public Item_base {
public:
double net_price(std::size_t) const;
private:
std::size_t min_qty;
double discount;
};
double Bulk_item::net_price(size_t cnt) const
{
if(cnt >= min_qty)
return cnt * (1 - discount) * price;
else
return cnt * price;
}
- virtual:保留字virtual的目的是启用动态绑定。成员函数默认为非虚函数,对非虚函数的调用在编译时确定。为了指明函数为虚函数,在其返回类型前面加上保留字virtual。除了构造函数之外,任意非static成员函数都可以是虚函数。保留字virtual只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。
- 访问控制和继承:public可以被类的用户访问,private只能由基类和友元访问。protected介于前两者之间,不能被类的用户访问,可以被基类和友元访问,并且可以被派生类访问。
- 派生类只能通过派生类对象访问其基类的protected成员,派生类对基类类型对象的protected成员没有特殊的访问权限。
void Bulk_item::memfcn(const Bulk_item &d, const Item_base &b)
{
double ret = price;
ret = d.price;// ok: uses price from a Bulk_item object
ret = b.price;// error: no access to price from an Item_base
}
- 派生类继承基类的成员并且可以定义自己的附加成员。每个派生类对象包含两个部分:从基类继承的成员和自己定义的成员。一般而言,派生类只(重)定义那些与基类不同或扩展基类行为的方面。
派生类和虚函数
- 派生类一般会重定义所继承的虚函数。如果派生类没有重定义某个虚函数,则使用基类中定义的版本。
- 派生类必须对想要重定义的每个继承成员进行声明。
- 派生类中虚函数的声明必须与基类中的定义完全匹配。但有一个例外,返回对基类类型引用(或指针)的虚函数,派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。
- 一旦函数在基类中声明为虚函数,它就一直为虚函数。
派生类与基类
- 派生类对象包含基类对象作为子对象
- 派生类中的函数可以使用基类的成员
- 用作基类的类必须是已定义的
- 可以用派生类做基类
- 正确的派生类声明:
class Bulk_item;
class Item_base;
vitural 与其他成员函数
要触发动态绑定,必须满足两个条件:第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;第二,必须通过基类类型的引用或指针进行函数调用。
//calculate and print price for given number of copies, applying any discount
void print_total(ostream &os, const Item_base &item, size_t n)
{
os << "ISBN:" << item.book() << "\tnumber sold: "
<< "\ttotal price: " << item.net_price(n) << endl;
}
Item_base item; // object of base type
print_total(cout, item, 10); // passes reference to an Item_base object
Item_base *p = &item; // p point to a Item_base object
Bulk_item bulk; // object of derived type
print_total(cout, bulk, 10); // pass reference to an Bulk_item object
p = &bulk; // p point to a Bulk_item object
对于非虚函数,在编译时确定非virtual调用。
覆盖虚函数机制
Item_base *baseP = &derived;
double d = baseP->Item_base::net_price(42);
*只有在成员函数中的代码才应该使用作用于操作符覆盖虚函数机制。
*派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。
共有、私有和受保护的继承
每个类控制它所定义的成员的访问。派生类可以进一步限制但不能放松对所继承的成员的访问。
- 如果是公有继承,基类成员保持自己的访问级别。
- 如果是受保护继承,基类的public和protected成员在派生类中为protected成员。
- 如果是私有继承,基类的所有成员在派生类中为private成员。
*公有继承属于接口继承,受保护继承和私有继承属于实现继承。
*派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。
默认继承保护级别:class 为 private, struct 为 public。
尽管私有继承在使用 class 保留字时是默认情况,但这在实践中相对罕见。因为私有继承是如此罕见,通常显式指定private是比依赖于默认更好的办法。显式指定可清楚指出想要私有继承而不是一时疏忽。
友元关系和继承
友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
继承与静态成员
如果基类定义了static成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个static成员只有一个实例。
转换和继承
每个派生类对象包含一个基类部分,这意味着可以像使用基类对象一样在派生类对象上执行操作。因为派生类对象也是基类对象,所以存在从派生类引用到基类类型引用的自动转换。
基类类型对象既可以作为独立对象存在,也可以作为派生类对象的一部分而存在,因此,一个基类对象可能是也可能不是一个派生类对象的部分,结果,没有从基类引用(或基类指针)到派生类引用(或派生类指针)的自动转换。
*没有从派生类类型对象到基类类型对象的直接转换。
派生类到基类转换的可访问性
要确定到基类的转换是否可访问,可以考虑基类的public成员是否可访问,如果可以,转换时可访问的,否则,转换时不可访问的。
如果是public继承,则用户代码和后代类都可以使用派生类到基类的转换。如果类使用private或protected继承派生的,则用户代码不能将派生类类型对象转换为基类对象。如果是private继承,则从private继承类派生的类不能转换为基类。如果是protected继承,则后续派生类的成员可以转换为基类类型。
构造函数和复制控制
每个派生类对象由派生类中定义的(非static)成员加上一个或多个基类子对象构成,这一事实影响着派生类类型对象的构造、复制、赋值和撤销。当构造、复制、赋值和撤销派生类对象时,也会构造、复制、赋值和撤销这些基类子对象。
构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果不定义自己的默认构造函数和复制控制成员,就将使用合成版本。
基类构造函数和复制控制
本身不是派生类的基类,其构造函数和复制控制基本上不受继承影响。
*某些类需要只希望派生类使用的特殊构造函数,这样的构造函数应定义为protected。
派生类构造函数
合成的派生类默认构造函数,将调用基类的默认构造函数初始化基类。当定义自己的默认构造函数时,要调用基类的构造函数来进行适当的初始化。需要注意的是,只能初始化直接基类。这样做,是为了尊重基类的接口。
复制控制和继承
定义派生类复制构造函数:如果派生类显式定义自己的复制构造函数或赋值操作符,则该定义将完全覆盖默认定义。被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制和赋值。如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分。
派生类赋值操作符:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式复制。
派生类析构函数:派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数值负责清除自己的成员,对象的撤销顺序和构造顺序相反。
虚析构函数
要保证运行适当的析构函数,基类中的析构函数必须为虚函数。
构造函数和析构函数中的虚函数
*构造函数和赋值操作符不是虚函数。
如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。
继承情况下的类作用域
名字查找在编译时发生,即通过编译时的静态类型来确定调用的函数或数据成员。
名字冲突与继承,派生类屏蔽基类的同名成员。可以使用作用域操作符访问被屏蔽的成员。
纯虚函数
可以通过纯虚函数定义一个抽象类,作为具体类的基类。含有纯虚函数的类不能被实例化。
class MyClass {
public:
void pureVirtualFunction() = 0;
};
容器和继承
保存基类的容器不能保存派生类。强制将派生类转换为基类存入容器,将导致派生类的派生部分丢失。从而将容器中的成员强制转换为派生类时,类的派生部分是未定义的。