常宝宝 北京大学计算机科学与技术系 chbb@pku.edu.cn
内容提要 代码重用 类的继承 多态 抽象类 多重继承 虚拟继承
代码重用 面向对象程序设计追求的目标之一是代码重用(code reuse)。 类代码重用的方法之一是组合。即在类中定义对象成员,而对象成员是已存在类的对象。 例如已经定义一个描述“引擎”的类,在定义“汽车”类时,可以为汽车类定义一个“引擎”类的对象成员。 对象和其对象成员之间的关系是一种“has-a”关系,如:一辆汽车有一个引擎。
代码重用 在面向对象的程序设计方法中,代码重用的另一种方式是继承。 如果新设计的类和已存在的类具有相似的功能和相似的接口,可以通过继承的方式重用已存在的类。例如: 定义了“形状”类,则“圆”、 “正方形”和“三角形”可以 继承“形状”类的特性。 在继承关系中,被继承的 类称为基类,通过继承关 系新建的类称为派生类。 派生类和基类之间的关系 是“is-a”关系。例如:“圆” 是一种“形状”。
继承 继承意味着派生类继承了基类的所有特性,基类的所有数据成员和成员函数自动成为派生类的数据成员和成员函数。如: “形状”具有位置、大小、颜色等属性,可以移动、绘制等,由其派生出来的“圆”同样有这些属性和操作。 派生类不完全等同于基类,派生类可以添加自己特有的特性,即可以为派生类增加新的数据成员和成员函数。如: “圆”有半径,“正方形”有边长或顶点位置等。 派生类可以重定义基类中不满足派生类要求的特性,既可以重新定义基类中的成员函数。如: 计算“圆”的面积不能直接使用“形状”面积的计算方法。 在继承关系中,基类的接口是派生类接口的子集,派生类支持集类所有的公有成员函数。
继承 派生类的定义方法如下: class 派生类的类名: 继承类型 基类的类名 { … }; 继承类型有三种: public 公有继承 protected 保护继承 private 私有继承 例子:student.h student.cpp student_test.cpp 从类student派生出类graduate_student
继承 派生类继承了基类的所有数据成员和成员函数,包括静态数据成员和静态成员函数。如: graduate_student gs(“罗小四”, “0987654321”, “杨振宁”); gs.set_score(98.5); 派生类可以增加新的数据成员和成员函数,此时要在派生类中定义这些数据成员和成员函数。如: graduate类中新增了数据成员advisor、title 新增了成员函数get_advisor()、set_thesis_title(…)、get_thesis_title() 派生类可以重新定义基类中不能满足要求的成员函数。如: graduate类中重新定义了基类中的成员函数display()
成员函数的重定义 重新定义一个成员函数时,可以调用基类中的旧版成员函数,此时应使用作用域指示符。如: void graduate_student::display() const { student::display(); cout << "Advisor:" << advisor << endl; cout << "Thesis:" << title << endl; } 若在派生类中重新定义了某个成员函数,通过派生类对象调用该成员函数,调用的是新版的成员函数,通过基类对象调用的是基类中的旧版成员函数。如: student s("王二", "1234567890"); graduate_student gs(“罗小四”, “0987654321”, “杨振宁”); s.display(); gs.display();
名字隐藏 如果在派生类中重新定义了基类中的成员函数,基类中该成员函数及其所有重载版本在派生类中均不再可用。 参见namehiding.cpp class Base { public: int f(); int f(float); ... }; class Derived2 : public Base { int main() { float s = 10.68; Derived2 d2; x = d2.f(); d2.f(s);//错误,由于派生类重 //新定义了f(),导致基类中的 //f(float)不再可用。 } 参见namehiding.cpp
基类中成员在派生类中的可见性 派生类中可以存取或调用基类中的保护成员和公有成员,但不能存取或调用基类的私有成员。(保护成员和私有成员的区别) class base { public: void set(int a, int b, int c); private: int m1; protected: int m2; int m3; }; class derived: public base { public: void f() { cout<<“m1=”<<m1<<endl;//错误 cout<<"m2="<<m2<<endl; cout<<"m3="<<m3<<endl; } }; //m1 是基类的私有成员,派生类的成员//函数无权存取。
继承类型 基类中成员在派生类中的存取权限由继承类型确定,继承类型可以改变基类中成员在派生类中的存取权限。 在定义派生类时,如果没有指出继承类型,则默认为私有继承。 参见例子:access.cpp 通过私有继承和保护继承得到的派生类不支持基类的接口,因而派生类和基类之间不存在“is-a”关系。 在C++程序设计中,私有继承和保护继承很少使用。
派生类对象的内存布局 在为派生类对象分配存储空间时,其中包含一个基类的对象。如:
派生类对象的构造和析构 在派生类构造函数调用前,会自动调用基类构造函数。 在派生类析构函数调用后,会自动调用基类析构函数。 如果需要调用带参数的基类构造函数,应在定义派生类构造函数时,以初始化列表的方式给出,形式如下: 派生类的类名::派生类的类名(构造函数参数表):初始化表 {...} graduate_student::graduate_student(char *pname, char *pid, char*pan):student(pname, pid) { strcpy(advisor, pan); strcpy(title, "" ); } ... graduate_student gs("罗小四", "0987654321", "杨振宁");
派生类对象的构造和析构 对于默认拷贝构造函数而言,在调用派生类的默认构造函数之前会自动调用基类的默认拷贝构造函数。 一旦为派生类定义了拷贝构造函数,则不会自动调用基类的(默认)拷贝构造函数,而是自动调用基类的默认构造函数,此时应在派生类拷贝构造函数后的初始化列表中调用基类的(默认)拷贝构造函数。如: graduate_student::graduate_student(graduate_student& gs):student(gs) { strcpy(advisor, gs.advisor ); strcpy(title, gs.title ); }
赋值运算符重载函数 下列特殊的基类成员函数不会被派生类继承: 构造函数 拷贝构造函数 析构函数 赋值运算符重载函数operator=(...) 如果没有为派生类定义赋值运算符重载函数,编译程序会生成默认赋值运算符重载函数,并且会自动调用基类的赋值运算符重载函数。 如果程序员为派生类重新定义了赋值运算符重载函数,程序员需要显式调用基类的赋值运算符重载函数,此时不会自动调用基类赋值运算符重载函数。调用方式如下: 基类名::operator=(...); 参见例子assignment.cpp
基向类型转换(upcasting) 由于派生类是一种特殊的基类(is-a关系),派生类对象可以作为基类对象使用。 指向派生类对象的指针可以赋值给指向基类对象的指针。派生类对象可以作为基类对象引用的初值。如果是函数调用,派生类对象的实参可以传递给基类对象引用的形参,派生类对象指针实参可以传递给基类对象指针的形参,这称为基向类型转换。 void func(student& s) {...} void func(student* s) {...} void main() { graduate_student gs(“罗小四”, “0987654321”, “杨振宁”); student& rs = gs; //正确 student* ps; ps = &gs; //正确 func(gs); //正确 func(&gs); //正确 }
基向类型转换 当使用了基向类型转换后,用基类引用(或指向基类对象指针)存取派生类对象的数据成员或成员函数时,只能存取从基类中继承来的数据成员或成员函数。如: graduate_student gs(“罗小四”, “0987654321”, “杨振宁”); student& rs = gs; cout<<rs.get_advisor()<<endl;//错误 如果派生类中重新定义了基类中的成员函数,用基类引用(或指向基类对象指针)调用重定义了的成员函数时,调用的仍然是基类中的旧版成员函数。如: graduate_student gs(“罗小四”, “0987654321”, “杨振宁”); student& rs = gs; rs.display();//调用了student::display()
多态 理想的情况是成员函数调用应和引用的具体对象或指针实际指向的对象有关,当基类引用实际引用的是派生类的对象或基类指针实际指向的是派生类对象时,调用派生类中重定义的新版成员函数,而当基类引用实际引用的是基类的对象或基类指针实际指向的是基类对象时,调用基类中的旧版成员函数。这种根据具体对象决定调用不同版本成员函数的特性称为多态性。 为实现多态需要使用虚函数,把一个成员函数定义为虚函数的办法是在返回类型前使用保留字virtual。如: class student { ... virtual void display() const; };
虚函数 虚函数具有传递性,当基类中某个成员函数被声明为虚函数,派生类中重定义该成员函数时,即使没有使用保留字virtual,派生类中的该成员函数也是虚函数。如: class graduate_student:public student { ... void display() const;//虚函数 }; 如果派生类在重定义基类的虚函数时,改变了函数的参数个数、参数类型或返回类型等,则重新定义的函数在派生类中不再是虚函数。 class graduate_student:public student { ... void display( int );//不是虚函数 };
多态 利用基类指针或基类引用调用虚函数时会呈现多态性,具体调用的函数和此时引用或实际指向的对象有关,若实际指向派生类对象,调用派生类中的新版函数,若实际指向基类对象,则调用基类中的旧版函数。如: graduate_student gs(“罗小四”, “0987654321”, “杨振宁”); student& rs = gs; rs.display();//调用了graduate_student::display() 例子: student_virtual.h student_virtual.cpp student_virtual_test.cpp
虚函数 普通函数不能声明为虚函数。 静态成员函数不能声明为虚函数。 内联成员函数不能声明为虚函数。 构造函数不能声明为虚函数。 析构函数可以声明为虚函数。 多态性是面向对象程序设计语言的重要特性,不支持多态的程序设计语言不能称为面向对象的程序设计语言。
纯虚函数 通过基类指针或引用调用的成员函数在必须在基类中定义,否则就不能通过基类指针或引用调用。 有时候,某些成员函数只能在派生类中定义,但又希望支持多态,即希望能通过基类指针或引用调用该成员函数,此时可以在基类中定义纯虚函数。定义纯虚函数的办法为: class 类名 { ... virtual 返回类型 成员函数名(参数表) = 0; }; 所谓纯虚函数指的是暂时不用实现的成员函数。其具体实现可以延迟到派生类中进行。纯虚函数一定是虚函数。 包含纯虚函数的类称为抽象类。
纯虚函数 例子: abstract.cpp 抽象类仅仅定义了类及其派生类的接口。只能用来继承。 不能创建抽象类的对象。因为抽象类中含有没有实现的函数,即纯虚函数。 Instrument a;//错误
纯虚函数 派生类必须重新定义纯虚函数,如果派生类中还有没有重新定义的纯虚函数,则派生类还是抽象类。 虽然不能创建抽象类的对象,但可以创建指向抽象类对象的指针和抽象类对象的引用。如: Wind flute; Percussion drum; Instrument *pi;//正确 pi = &drum; Instrument& ri = flute; //正确
多重继承 可以同时从多个基类派生新类,这称为多重继承。 在多重继承中,多个基类之间用逗号间隔,形式如下: class 派生类: 继承类型 基类1, 继承类型 基类2, ...,继承类型 基类n { … }; 例子: multiple_inheritance.h multiple_inheritance.cpp 在多重继承中,派生类继承了所有基类的数据成员和成员函数.如: sleeping_sofa ss; ss.watch_TV(); //来自基类 sofa ss.fold_out(); ss.sleep(); //来自基类 bed
多重继承中的名字冲突 在多重继承中,如果多个基类中含有同名成员,此时会出现名字冲突。如: sleeping_sofa ss; ss.set_weight(5);//错误,发生了名字冲突 在发生冲突时,应通过指明基类的方式加以区分。如: ss.sofa::set_weight(5);//正确 ss.bed::set_weight(5);//正确
多重继承
多重继承 在多重继承中,一个基类可以通过不同的路径被一个派生类多次继承,从而在派生类对象中产生基类对象的多个拷贝。 为避免这种情况出现,引入虚拟继承的概念。为使派生类中只有一个基类对象拷贝,从该基类继承采用虚拟继承。此时,基类称为虚基类,形式如下: class b1: virtual public base { ... }; class b2: virtual public base { ... };
多重继承 多重继承非常复杂,它的引入导致了许多问题需要解决,多重继承是C++语言最受批评的特性之一。 程序员一般很少使用多重继承。 所以对于多重继承,了解即可。
上机练习内容 《C++程序设计教程》p.380 练习16.1 、16.2、16.3