继承
类之间的关系
继承:在已有类的基础上创建新类的过程
一个 B 类继承A类,或称从类 A 派生类 B
类 A 称为基类(父类),类 B 称为派生类(子类)
基类和派生类
访问控制
不论种方式继承基类,派生类都不能直接使用基类的私有成员
派生类的生成过程
派生类的生成过程经历了三个步骤:
(1)吸收基类成员(全部吸收(构造、析构除外),但不一定可见)
●在C++的继承机制中,派生类吸收基类中除构造函数和析构函数之外的全部成员。
(2) 改造基类成员
● 通过在派生类中定义同名成员(包括成员函数和数据成员)来屏蔽(隐藏)在派生类中不起作用的部分基类成员。
(3) 添加新成员
●仅仅继承基类的成员是不够的,需要在派生类中添加新成员,以保证派生类自身特殊属性和行为的实现。
派生类对象结构
公有继承
#include<iostream>
using namespace std ;
class A
{ public :
void get_XY() { cout << "Enter two numbers of x, y : " ; cin >> x >> y ; }
void put_XY() { cout << "x = "<< x << ", y = " << y << '\n' ; }
protected: int x, y ;
};
class B : public A
{ public :
int get_S() { return s ; };
void make_S() { s = x * y ; }; // 使用基类数据成员x,y
protected: int s;
};
class C : public B
{ public :
void get_H() { cout << "Enter a number of h : " ; cin >> h ; }
int get_V() { return v ; }
void make_V() { make_S(); v = get_S() * h ; } // 使用基类成员函数
protected: int h, v;
};
int main()
{ A objA ;
B objB ;
C objC ;
cout << "It is object_A :\n" ;
objA.get_XY() ;
objA.put_XY() ;
cout << "It is object_B :\n" ;
objB.get_XY() ;
objB.make_S() ;
cout << "S = " << objB.get_S() << endl ;
cout << "It is object_C :\n" ;
objC.get_XY() ;
objC.get_H();
objC.make_V() ;
cout << "V = " << objC.get_V() << endl ;
}
题目
定义一个基类person(不定义构造函数)
姓名、性别、年龄(访问权限设置为私有)
定义公有的成员函数set_p()
定义公有的成员函数display_p(),显示person的信息
再由基类派生出学生类(不定义构造函数,采用公有继承的方式)
增加学号、班级、专业和入学成绩
定义公有成员函数set_t()
定义成员函定义公有的成员函数display_s(),显示所有的信息
#include<iostream>
#include <string>
using namespace std;
class Person
{
string name;
int age;
string sex;
public:
void set_p() {
cout<<"name\tage\tsex"<<endl;
cin>>name>>age>>sex;
}
void show_p() {
cout<<name<<" "<<age<<" "<<sex<<endl;
}
};
class student :public Person
{
string no;
string zhuanye;
string t_class;
float score;
public:
void set_t(){
set_p(); //调用继承于基类的成员函数访问继承于基类的私有数据成员
cout<<"zhuanye\tt_class\tscore"<<endl;
cin>>zhuanye>>t_class>>score;
}
void show_t() {
show_p();
cout<<zhuanye<<" "<<t_class<<" "<<score<<endl;
}
};
#include<iostream>
#include <string>
using namespace std;
class Person
{
string name;
int age;
string sex;
public:
void set() {
cout<<"name\tage\tsex"<<endl;
cin>>name>>age>>sex;
}
void show() {
cout<<name<<" "<<age<<" "<<sex<<endl;
}
};
class student :public Person
{
string no;
string zhuanye;
string t_class;
float score;
public:
void set(){ //隐藏了基类中的同名成员
Person::set(); //调用继承于基类的成员函数访问继承于基类的数据成员
cout<<"zhuanye\tt_class\tscore"<<endl;
cin>>zhuanye>>t_class>>score;
}
void show() {
Person::show();
cout<<zhuanye<<" "<<t_class<<" "<<score<<endl;
}
};
重名成员
派生类定义了与基类同名的成员,在派生类中访问同名成员时屏蔽(hide)了基类的同名成员
在派生类中使用基类的同名成员,显式地使用类名限定符:
类名 :: 成员
#include<iostream> //例8-4
using namespace std ;
class A
{ public:
int a1, a2 ;
A( int i1=0, int i2=0 ) { a1 = i1; a2 = i2; }
void print()
{ cout << "a1=" << a1 << '\t' << "a2=" << a2 << endl ; }
};
class B : public A
{ public:
int b1, b2 ;
B( int j1=1, int j2=1 ) { b1 = j1; b2 = j2; }
void print() //定义同名函数
{ cout << "b1=" << b1 << '\t' << "b2=" << b2 << endl ; }
void printAB()
{ A::print() ; //派生类对象调用基类版本同名成员函数
print() ; //派生类对象调用自身的成员函数
}
};
int main()
{ B b ;
b.A::print();
b.printAB();
}
派生类中访问静态成员
基类的初始化
派生类构造函数和析构函数的定义规则
派生类的构造函数的定义
●派生类的数据成员既包括基类的数据成员,也包括派生类新增数据成员。
问题1:如何对基类中的成员进行初始化?
最佳解决方案:调用基类构造函数对基类成员进行初始化。
在C++中,派生类构造函数的一般格式为:
派生类::派生类名(参数总表):基类名(参数表)
{
// 派生类新增成员的初始化语句
}
注意:这是基类有构造函数且含有参数时使用
派生类析构函数
(1)当派生类中不含对象成员时
●在创建派生类对象时,构造函数的执行顺序是:基类的构造函数→派生类的构造函数;
●在撤消派生类对象时,析构函数的执行顺序是:派生类的析构函数→基类的析构函数。
(2)当派生类中含有对象成员时
●在定义派生类对象时,构造函数的执行顺序:基类的构造函数→对象成员的构造函数→派生类的构造函数;
●在撤消派生类对象时,析构函数的执行顺序:派生类的析构函数→对象成员的析构函数→基类的析构函数。
多继承
一个类有多个直接基类的继承关系称为多继承
多继承声明语法
class 派生类名 : 访问控制 基类名1 , 访问控制 基类名2 , … , 访问控制 基类名n
{
数据成员和成员函数声明
};
类 C 可以根据访问控制同时继承类 A 和类 B 的成员,并添加自己的成员
多个基类的派生类构造函数可以用初始式调用基类构造函数初始化数据成员。
执行顺序与单继承构造函数情况类似。多个直接基类构造函数执行顺序取决于定义派生类时指定的各个继承基类的顺序。
一个派生类对象拥有多个直接或间接基类的成员。不同名成员访问不会出现二义性。如果不同的基类有同名成员,派生类对象访问时应该加以识别。
多继承的简单应用
class Base1
{ public:
Base1(int x) { value = x ; }
int getData() const { return value ; }
protected:
int value;
};
class Base2
{ public:
Base2(char c) { letter=c; }
char getData() const { return letter;}
protected:
char letter;
};
class Derived : public Base1, public Base2
{
friend ostream &operator<< ( ostream &, const Derived & ) ;
public :
Derived ( int, char, double ) ;
double getReal() const ;
private :
double real ;
};
int main()
{
Base1 b1 ( 10 ) ;
Base2 b2 ( 'k' ) ;
Derived d ( 5, 'A', 2.5 ) ;
:
return 0;
}
多继承的构造函数
派生类名(参数总表):基类名1(参数表1),基类名2(参数表2),…,基类名n(参数表n)
{
// 派生类新增成员的初始化语句
}
多继承方式下构造函数的执行顺序:
●先执行所有基类的构造函数
●再执行对象成员的构造函数
●最后执行派生类的构造函数
处于同一层次的各基类构造函数的执行顺序取决于定义派生类时所指定的基类顺序
与派生类构造函数中所定义的成员初始化列表顺序没有关系。
内嵌对象成员的构造函数执行顺序与对象在派生类中声明的顺序一致
多继承的析构函数
●析构函数名同样与类名相同,无返回值、无参数,而且其定义方式与基类中的析构函数的定义方式完全相同。
●功能是在派生类中对新增的有关成员进行必要的清理工作。
●析构函数的执行顺序与多继承方式下构造函数的执行顺序完全相反,首先对派生类新增的数据成员进行清理,再对派生类对象成员进行清理,最后才对基类继承来的成员进行清理。
虚基类
如果一个派生类从多个基类派生,而这些基类又有一个共同
的基类,则在对该基类中声明的名字进行访问时,可能产生二义性。
class B { public : int b ;} ;
class B1 : public B { private : int b1 ; } ;
class B2 : public B { private : int b2 ; } ;
class C : public B1 , public B2
{ public : int f ( ) ; private : int d ; } ;
有:
C c ;
c . B ; // error
c . B :: b ; // error,从哪里继承的?
c . B1 :: b ; // ok,从B1继承的
c . B2 :: b ; // ok ,从B2继承的
建立 C 类的对象时,B 的构造函数将被调用两次:一次由B1调用,另一次由 B2 调用,以初始化 C 类的对象中所包含的两个 B 类的子对象
虚基类
如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性。
如果在多条继承路径上有一个公共的基类,那么在继承路径的某处汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象。
要使这个公共基类在派生类中只产生一个子对象,必须对这个基类声明为虚继承,使这个基类成为虚基类。
虚继承声明使用关键字 virtual
赋值兼容规则
●赋值兼容规则指在程序中需要使用基类对象的任何地方,都可以用公有派生类的对象来替代。
赋值兼容规则中所指的替代包括以下的情况:
a 派生类的对象可以赋给基类对象
b 派生类的对象可以初始化基类的引用
c 派生类的对象的地址可以赋给基类类型的指针
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员
赋值兼容应注意的问题
(1)声明为指向基类的指针可以指向它的公有派生类的对象,但不允许指向它的私有派生类的对象。
(2)允许将一个声明为指向基类的指针指向其公有派生类对象,但是不能将一个声明为指向派生类对象的指针指向其基类的一个对象。
(3) 声明为指向基类对象的指针,当其指向公有派生类对象时,只能用它来直接访问派生类中从基类继承来的成员,而不能直接访问公有派生类的定义的成员。
关于继承 注意
1.在继承中,private类型不能被继承
2.派生类中可以定义一个与基类同名成员,但是原来的还存在
3.派生类申请自己的空间,与基类的成员没有任何关系
4.定义派生类对象时,只是复制基类的空间,但是没有赋值
5.子类与父类中函数如果有重名,如果子类中隐藏了父类的方法,那么父类中同名的方法(重载方法)都将被隐藏。
6.公有继承中,派生类对基类的所有成员进行复制,仅把数据的“型”复制,数据的“值”之间没有关系,在此基础上在添加新的成员。
7.基类初始化中,构造函数执行顺序:基类->对象成员-> 派生类
虚函数与多态性
多态性(Polymorphism)是指一个名字,多种语义;或界面相同,多种实现。
重载函数是多态性的一种简单形式。
虚函数允许函数调用与函数体的联系在运行时才进行,称为动态联编。
多态的实现
多态性的实现和联编这一概念有关。所谓联编(Binding,绑定)就是把函数名与函数体的程序代码连接(联系)在一起的过程。
联编分成两大类:静态联编和动态联编。
静态联编优点:调用速度快,效率高,但缺乏灵活性;动态联编优点:运行效率低,但增强了程序灵活性。
C++为了兼容C语言仍然是编译型的,采用静态联编。为了实现多态性,利用虚函数机制,可部分地采用动态联编
多态从实现的角度来讲可以划分为两类:编译时的多态和运行时的多态。
编译时的多态是通过静态联编来实现的。静态联编就是在编译阶段完成的联编。编译时多态性主要是通过函数重载和运算符重载实现的。
运行时的多态是用动态联编实现的。动态联编是运行阶段完成的联编。运行时多态性主要是通过虚函数来实现的。
静态联编
联编是指一个程序模块、代码之间互相关联的过程。
静态联编,是程序的匹配、连接在编译阶段实现,也称为早期匹配。
重载函数使用静态联编。
动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编。
switch 语句和 if 语句是动态联编的例子。
普通成员函数重载可表达为两种形式:
- 在一个类说明中重载
例如: void Show ( int , char ) ;
void Show ( char * , float ) ; - 基类的成员函数在派生类重载。有 3 种编译区分方法:
(1)根据参数的特征加以区分
例如: void Show ( int , char ); 与
void Show ( char * , float ); 不是同一函数,编译能够区分
( 2)使用“ :: ”加以区分
例如: A :: Show ( );
有别于 B :: Show ( );
(3)根据类对象加以区分(根据this指针类型区分)
例如: Aobj . Show ( ) 调用 A :: Show ( )
Bobj . Show ( ) 调用 B :: Show ( )
基类指针和派生类指针与基类对象和派生类对象4种可能匹配:
直接用基类指针引用基类对象;
直接用派生类指针引用派生类对象;
用基类指针引用一个派生类对象;
用派生类指针引用一个基类对象。
基类指针引用派生类对象
利用 p,可以通过 B_obj 访问所有从 A 类继承的元素 ,
但不能用 p访问 B 类自定义的元素 (除非用了显式类型转换)
派生类指针引用基类对象
== 派生类指针只有经过强制类型转换之后,才能引用基类对象 ==
#include<iostream>
using namespace std ;
class Date{
public:
Date( int y, int m, int d ) { SetDate( y, m, d ); }
void SetDate( int y, int m, int d ) { year = y ; month = m ; day = d ; }
void Print() { cout << year << '/' << month << '/' << day << "; " ; }
protected :
int year , month , day ;
} ;
class DateTime : public Date
{ public :
DateTime( int y, int m, int d, int h, int mi, int s ) : Date( y, m, d ) { SetTime( h, mi, s ); }
void SetTime( int h, int mi, int s ) { hours = h; minutes = mi; seconds = s; }
void Print()//函数覆盖
{ ( ( Date * ) this ) -> Print(); //转换成基类类型指针
cout << hours << ':' << minutes << ':' << seconds << '\n' ;
}//先调用基类显示年月日,再调用本类时分秒
private: int hours , minutes , seconds ;
};
int main() {
DateTime dt( 2009, 1, 1, 12, 30, 0 ) ;
dt.Print() ;
}
虚函数
根据赋值兼容规则,可以将派生类的地址赋值给基类的指针。基类成员可以访问,派生类新增的成员不可访问。 重写时调用的是派生类继承的成员
在编译阶段,基类指针对函数的操作只能绑定到基类的成员函数
根据赋值兼容,用基类类型的指针指向派生类,就可以通过这个指针来使用类(基类或派生类)的成员函数。
如果这个函数是普通的成员函数,通过基类类型的指针访问到的只能是基类的同名成员。
而如果将它设置为虚函数,则可以使用基类类型的指针访问到指针正在指向的派生类的同名函数。从而实现运行过程的多态。
实现动态联编方式的前提
●先要声明虚函数
●类之间满足赋值兼容规则
●通过指针与引用来调用虚函数。
虚函数和动态联编
冠以关键字 virtual 的成员函数称为虚函数
实现运行时多态的关键首先是要说明虚函数,另外,必须用
基类指针调用派生类的不同实现版本
基类指针虽然获取派生类对象地址,却只能访问派生类从基类继承的成员
通过基类指针只能访问从基类继承的成员
虚函数和基类指针
注意:
一个虚函数,在派生类层界面相同的重载函数都保持虚特性
虚函数必须是类的成员函数
不能将友元说明为虚函数,但虚函数可以是另一个类的友元
析构函数可以是虚函数,但构造函数不能是虚函数
虚函数的重载特性
1.在派生类中重载基类的虚函数要求函数名、返回类型、参数个数、
2.参数类型和顺序完全相同
3. 如果仅仅返回类型不同,C++认为是错误重载
如果函数原型不同,仅函数名相同,丢失虚特性
虚析构函数
构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数
析构函数可以是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象
纯虚函数和抽象类
纯虚函数是一种特殊的虚函数,
在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。
纯虚函数是一个在基类中说明的虚函数,在基类中没有定义, 要求任何派生类都定义自己的版本
纯虚函数为各派生类提供一个公共界面
纯虚函数说明形式:
virtual 类型 函数名(参数表)= 0 ;
一个具有纯虚函数的基类称为抽象类
class figure //抽象类
{ protected : double x,y;
public: void set_dim(double i, double j=0) { x = i ; y = j ; }
virtual void show_area() = 0 ;
};
class triangle : public figure
{ public :
void show_area()
{ cout<<"Triangle with high "<<x<<" and base "<<y <<" has an area of "<<x*0.5*y<<"\n"; }
};
class square : public figure
{ public:
void show_area()
{ cout<<"Square with dimension "<<x<<"*"<<y <<" has an area of "<<x*y<<"\n"; }
};
class circle : public figure
{ public:
void show_area()
{ cout<<"Circle with radius "<<x;
cout<<" has an area of "<<3.14*x*x<<"\n";
}
};
纯虚函数在派生类中必须重写
虚函数与多态的应用
虚函数和多态性使成员函数根据调用对象的类型产生不同的动作
多态性特别适合于实现分层结构的软件系统,便于对问题抽象时定义共性,实现时定义区别
实例:计算雇员工资
class Employee
{
public:
Employee(const int,const string );
virtual ~Employee();
const string getName() const;
const int getNumber() const;
virtual double earnings() const=0;//雇员工资纯虚函数
virtual void print() const;
protected:
int number; // 编号
string name; // 姓名
};
class Manager : public Employee
{
public:
Manager(const int , const string, double =0.0);
~Manager() { }
void setMonthlySalary(double); //新增函数
virtual double earnings() const;
virtual void print() const;
private:
double monthlySalary ;
};
class HourlyWorker : public Employee
{
public:
HourlyWorker(const long, const string, double=0.0, int =0 );
~HourlyWorker(){}
void setWage(double);
void setHours(int);
virtual double earnings() const;
virtual void print() const;
private:
double wage;
double hours;
};
class PieceWorker : public Employee
{
public:
PieceWorker(const long , const string, double =0.0, int =0 );
~PieceWorker() { }
void setWage ( double ) ; //完成件数
void setQuantity ( int ) ; //价值
virtual double earnings() const;
virtual void print() const;
private:
double wagePerPiece;
int quantity;
};
注意:
1.重写 :函数返回值类型 函数名 参数列表 完全一致称为重写
2.虚函数使用过程中,需要用父类指针指向子类
3.在子类中的同名虚函数是可加关键字virtual也可不加,一般加上
4.函数的覆盖是同名且所有参数都一样,重载是名一样,返回值等其他都可以不一样
5.抽象类不能直接生成对象,可以定义指针/引用对象
6.派生抽象类中的纯虚函数一定重写
课程感受
到目前为止,12周的理论课算是结束了。12周老师的作业不仅让我学到了知识,还磨练了意志,可以说之前从来没有因为学习熬到那么晚(自制力比较差)。每一次因为作业都会比平时更焦虑,更烦躁,在家学习气氛也不如在学校,但是目前也没有什么开学的消息,所有还是静下心来好好复习吧。12周的学习让我对程序设计有了更新的认识,收获了很多知识,也明白了很多道理,很感谢老师,虽然严厉但认真负责,严师出高徒嘛,这样才会让我们学到更多。以后就是课程设计的作业了,希望自己认真研究,寻找编程的乐趣,慢慢的不去害怕,培养起兴趣。