继承
继承是使代码可以复用的重要手段,也是面向对象程序设计的核心思想之一。简单的说,继承是指一个对象直接使用另一对象的属性和方法。
C++ 中的继承关系就好比现实生活中的父子关系,继承一笔财产比白手起家要容易得多,原始类称为基类,继承类称为派生类,基类是对派生类的抽象,派生类是对基类的具体化。它们是类似于父亲和儿子的关系,所以也分别叫父类和子类。而子类又可以当成父类,被另外的类继承。
派生类的声明方式
声明派生类的一般格式为:
class 派生类名:继承方式 基类名{
派生类新增加的成员
};
继承方式包括:public,protected,private,继承方式是可选的,如果不写继承方式,默认的是private
样例1
给出文件<people.h><run.h>
#ifndef PEO_H_
#define PEO_H_
#include<string>
#include<iostream>
using namespace std;
class People{
public:
string Name;
void PrintName();
};
void People::PrintName(){
cout << "姓名:" << Name << endl;
}
#endif
#include "usr.h"
int main(){
int id;
string name;
cin >> id >> name ;
Student st;
Set(id,name,&st);
st.PrintSID();
st.PrintName();
}
要求实现文件<usr.h> 实现如下:
#include "people.h"
#include <string>
#include <iostream>
using namespace std;
class Student:public People{
private:
int SID;
public:
void PrintSID();
friend void Set(int sid,string name,Student *ptr);
};
void Student::PrintSID(){
cout << "学号:" << SID << endl;
}
void Set(int sid,string name,Student *ptr){
ptr->SID=sid;
ptr->Name=name;
}
保护继承
保护继承相对于公有继承,访问性有所降低,父类的公有成员在子类中变成了保护成员,也就无法在外部通过一个对象访问父类成员了,但是对于这个子类的子类仍然是可见的(因为可见性只是降到了 protected )。
如果要保护继承一个类,只需继承时在类名前面加上 protected 关键字即可。
样例1
给出文件<people.h><run.h>
#ifndef PEO_H_
#define PEO_H_
#include<string>
#include<iostream>
using namespace std;
class People{
public:
string Name;
void PrintName();
};
void People::PrintName(){
cout << "姓名:" << Name << endl;
}
#endif
#include "usr.h"
int main(){
int id;
string name;
cin >> id >> name ;
Student st;
Set(id,name,&st);
st.PrintSID();
((People*)&st)->PrintName();
}
要求实现文件<usr.h> 实现如下:
注意到main()中Set()为正常函数(不是成员函数),并且需要访问protected类型的People,所以这里声明为类外的友元函数
#include "people.h"
#include <string>
#include <iostream>
using namespace std;
class Student:protected People{
public:
int SID;
void PrintSID();
friend void Set(int sid,string name,Student* ptr);
};
void Student::PrintSID(){
cout << "学号:" << SID << endl;
}
void Set(int sid,string name,Student* ptr){
ptr->SID=sid;
ptr->Name=name;
}
重载、重定义(隐藏、覆盖)、重写
重载
产生原因:主要是因为在C++中,编译器在编译.cpp文件中当前作用域的同名函数时,函数生成的符号由返回值类型(不起决定作用)+形参类型和顺序(起决定作用)的组成。
作用:用同一个函数名命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。
重定义
重定义也叫隐藏,指的是在继承关系中,子类实现了一个和父类名字一样的函数,(只关注函数名,和参数与返回值无关)这样的话子类的函数就把父类的同名函数隐藏了。
下面给出一个易错的例子:
class A{
public:
int _x;
void f(){ cout << "A" << endl; }
};
class B:public A{
public:
void f(int a){ cout << "B" << endl; }
};
void test(){
B b;
b.f();
}
上述例子会报错,因为B中的f()覆盖了父类A的f(),所以函数定义是f(int a);
重写
重写指的在继承关系中,子类中定义了一个与父类极其相似的虚函数。
具体怎么相似:函数名必须相同,参数列表必须相同,返回值可以不相同,但是必须是父子关系的指针或引用。通过重写,可以实现动态多态,何为动态多态,就是当父类的指针或引用指向被重写的虚函数时,父类的指针或引用指向谁就调用谁的虚函数,而不是说根据类型。
在这里,如果去掉父类的虚函数的virtual,则构不成多态,如果去掉子类虚函数的virtual可以构成多态,可以理解为编译器优化。
样例
给出文件<people.h><run.h>
#ifndef PEO_H_
#define PEO_H_
#include<string>
#include<iostream>
using namespace std;
class People{
public:
string Name;
void PrintName();
};
void People::PrintName(){
cout << "姓名:" << Name << endl;
}
#endif
#include "usr.h"
int main(){
int i,j;
string name;
cin >> i >> j >> name;
Graduate st;
Set(name,i,j,&st);
((Student*)&st)->PrintSID();
((People*)&st)->PrintName();
st.PrintResearchID();
}
要求实现文件<usr.h> 实现如下:
注意到Student与Graduate中均有Set(),当方法名相同时,子类的函数会覆盖父类的函数(尽管他们的传参列表不相同)(但是可以通过作用域限制来访问Student::Set())(覆盖),
class Student:private People{
public:
int SID;
void PrintSID(){ cout<<"学号:"<<SID<<endl; }
//添加一个 Set 函数来设置父类的 Name 成员
void Set(string name){ Name=name; }
};
class Graduate:private Student{
public:
int ResearchID;
void PrintResearchID(){ cout<<"研究方向: "<<ResearchID<<endl; }
//添加一个 Set 函数来设置父类的 SID 成员
void Set(string name,int sid){ Student::Set(name); SID=sid; }
friend void Set(string name,int sid,int rid,Graduate *ptr);
};
void Set(string name,int sid,int rid,Graduate *ptr){
ptr->Set(string name,int sid);//调用的是Graduate中的成员函数
ptr->ResearchID=rid;
}
多继承访问基类成员
多继承访问基类成员大体与单继承一致,但当继承的多个父类中有同名的成员时,要访问其中一个成员就不能简单的只写成员名了,必须使用作用域运算符(::
)来指定是哪一个类的成员。
样例
#include <string>
#include <iostream>
using namespace std;
class Wolf{
public:
string Name;
int Shape;
void PrintState(){cout<<"姓名:"<<Name<<",爪子锋利度为:"<<Shape<<endl;}
};
class Human{
public:
string Name;
int Intell;
void PrintState(){cout<<"姓名:"<<Name<<",智力为:"<<Intell;}
};
class Werewolf:public Wolf,public Human{
public:
void SetName(string name){
Wolf::Name=name;
Human::Name=name;
}
void SetState(int shape,int intell){
Shape=shape;
Intell=intell;
}
void PrintAllState(){
Wolf::PrintState();
Human::PrintState();
}
};
子类调用父类构造函数:
子类可以继承父类所有的成员变量和成员方法,但不继承父类的构造方法。因此,在创建子类对象时,为了初始化从父类继承来的数据成员,系统需要调用其父类的构造方法。
1.如果子类没有定义构造函数,则调用父类的默认构造方法。
2.在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数,则会调用父类的默认构造函数。
3.在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数 且父类只定义了自己的有参构造函数,则会出错(如果父类只有有参数的构造方法,则子类必须显示调用此带参构造方法)。
4.如果子类调用父类带参数的构造方法,需要用初始化父类成员对象的方式。
无语的输出格式
!!!注意子类是如何初始化的
#include<cmath>
using namespace std;
class Point{
protected:
int x1,y1;
public:
Point(int a,int b):x1(a),y1(b){};
};
class Line:public Point{
protected:
int x2,y2;
public:
Line(int a,int b,int c,int d):Point(a,b),x2(c),y2(d){};
double dis(int x1,int y1,int x2,int y2){
return sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
}
};
class Triangle:public Line{
private:
double s,area,x,y,z;
protected:
int x3,y3;
public:
Triangle(int a,int b,int c,int d,int e,int f):Line(a,b,c,d),x3(e),y3(f){};
void Area(){
double x=dis(x1,y1,x2,y2);
double y=dis(x1,y1,x3,y3);
double z=dis(x3,y3,x2,y2);
double s=(x+y+z)*1.0/2;
area=sqrt(s*(s-x)*(s-y)*(s-z));
}
void print(){
printf("( %d,%d ) ( %d,%d ) ( %d,%d )\n",x1,y1,x2,y2,x3,y3);
if(area-(int)area<1e-6)printf("area=%.lf\n",area);
else printf("area=%.1lf\n",area);
}
};
虚函数
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
offset
首先学习一下offset函数C++内置的offsetof函数,能自动返回结构对象中,某变量距离对象首地址的偏移值
#include<iostream>
using namespace std;
struct a{int x,y;};
class b{
public:
char ch;
int x;
int *p;
int y,z;
};
int main(){
cout<<offsetof(a,x)<<endl;
cout<<offsetof(a,y)<<endl;
cout<<offsetof(b,x)<<endl;
cout<<offsetof(b,y)<<endl;
cout<<offsetof(b,z)<<endl;
}
类的虚表
每个包含了虚函数的类都包含一个虚表,虚函数表保存的是虚函数的指针,因此虚表的大小是虚函数个数*4个字节。
我们来看以下的代码。类 A 包含虚函数vfunc1,vfunc2,由于类 A 包含虚函数,故类 A 拥有一个虚表。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
类 A 的虚表如下图所示:
对于虚表,需要注意的是:
虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。
虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。
虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。
我们知道,当一个类A继承另一个类B时,类 A 会继承类 B 的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表,下面根据继承方式的不同,描述继承类的虚表
一般继承(无虚函数覆盖)
假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,对于实例Derive d,Derive类虚函数表如下:
从表中可以看到下面几点:
虚函数按照其声明顺序放于表中。
父类的虚函数在子类的虚函数前面。
一般继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,派生类只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
从表中可以看到下面几点:
覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
没有被覆盖的函数依旧。
多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
每个父类都有自己的虚表。
子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。
虚表指针
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,在编译阶段,编译器在类中添加了一个指针*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表,*__vptr一般在对象内存分布的最前面。
下面用代码验证一下虚表指针的存在
#include<iostream>
using namespace std;
class a{ public: void f(){} };
class b{ public: virtual void f(){} };
int main(){
a tema; b temb;
cout<<sizeof(tema)<<endl;
cout<<sizeof(temb)<<endl;
return 0;
}
虚表指针的初始化确实发生在构造函数的调用过程中, 但是在执行构造函数体之前,即进入到构造函数的"{“和”}"之前。 为了更好的理解这一问题, 我们可以把构造函数的调用过程细分为两个阶段,即:
1.进入到构造函数体之前。在这个阶段如果存在虚函数的话,虚表指针被初始化。如果存在构造函数的初始化列表的话,初始化列表也会被执行。
2.进入到构造函数体内。这一阶段是我们通常意义上说的构造函数。
动态绑定
C++ 是如何利用虚表和虚表指针来实现动态绑定的?我们先看下面的代码:
class A{
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B :public A{
public:
virtual void vfunc1();
void func1();
private:
int m_data3;
};
class C:public B{
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};
其对象模型如下图所示:
由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类 A 的虚表(A vtbl),类 B 的虚表(B vtbl),类 C 的虚表(C vtbl)。类 A,类 B,类 C 的对象都拥有一个虚表指针*__vptr,用来指向自己所属类的虚表。类 A 包括两个虚函数,故 A vtbl 包含两个指针,分别指向A::vfunc1()和A::vfunc2()。
类 B 继承于类 A,故类 B 可以调用类 A 的函数,但由于类 B 重写了B::vfunc1()函数,故 B vtbl 的两个指针分别指向B::vfunc1()和A::vfunc2()。
类 C 继承于类 B,故类 C 可以调用类 B 的函数,但由于类 C 重写了C::vfunc2()函数,故 C vtbl 的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()。
虽然上图看起来有点复杂,但是只要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。
非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。
假设我们定义一个类 B 的对象。由于temb是类 B 的一个对象,故temb包含一个虚表指针,指向类 B 的虚表。
int main(){
B temb;
}
现在,我们声明一个类 A 的指针p来指向对象temb。虽然p是基类的指针只能指向基类的部分,但是虚表指针亦属于基类部分,所以p可以访问到对象temb的虚表指针。temb的虚表指针指向类 B 的虚表,所以p可以访问到 B vtbl。
int main(){
B temb;
A* p=&temb;
}
当我们使用p来调用vfunc1()函数时,会发生什么现象?
int main(){
B temb;
A* p=&temb;
p->vfunc1();
}
程序在执行p->vfunc1()时,会发现p是个指针,且调用的函数是虚函数,接下来便会进行以下的步骤。
首先,根据虚表指针p->_vptr来访问对象temb对应的虚表。虽然指针p是基类A类型,但是_vptr也是基类的一部分,所以可以通过p->_vptr可以访问到对象对应的虚表。
然后,在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了,所以可以根据所调用的函数定位到虚表中的对应条目。对于 p->vfunc1()的调用,B vtbl 的第一项即是vfunc1对应的条目。
最后,根据虚表中找到的函数指针,调用函数。从图 3 可以看到,B vtbl 的第一项指向B::vfunc1(),所以 p->vfunc1()实质会调用B::vfunc1() 函数。
如果p指向类 A 的对象,情况又是怎么样?
int main(){
A tema;
A* p=&tema;
p->vfunc1();
}
当tema在创建时,它的虚表指针__vptr已设置为指向 A vtbl,这样p->__vptr就指向 A vtbl。vfunc1在 A vtbl 对应在条目指向了A::vfunc1()函数,所以 p->vfunc1()实质会调用A::vfunc1()函数。
我们把经过虚表调用虚函数的过程称为动态绑定,其表现出来的现象称为运行时多态。动态绑定区别于传统的函数调用,传统的函数调用我们称之为静态绑定,即函数的调用在编译阶段就可以确定下来了。
那么,什么时候会执行函数的动态绑定?这需要符合以下三个条件。
1.通过指针来调用函数
2.指针 upcast 向上转型(继承类向基类的转换称为 upcast)3.调用的是虚函数。
如果一个函数调用符合以上三个条件,编译器就会把该函数调用编译成动态绑定,其函数的调用过程走的是上述通过虚表的机制。
派生类访问基类私有
派生类不能直接访问基类的私有成员,若要访问必须使用基类的接口,即通过其成员函数。实现方法有如下两种:
1.在基类的声明中增加保护成员,将基类中提供给派生类访问的私有成员定义为保护成员。
2.将需要访问基类私有成员的派生l类成员函数声明为友元。
例题
例一
!!!注意COL的实现 运用作用域限定,静态访问
#include<cmath>
using namespace std;
class TR1{
private:
double x,y,z;
public:
TR1(double x1,double y1,double z1):x(x1),y(y1),z(z1){};
virtual double area(){
double s=(x+y+z)/2;
return sqrt(s*(s-x)*(s-y)*(s-z));
}
double peri(){ return x+y+z; }
};
class COL:public TR1{
private:
double height;
public:
COL(double x1,double y1,double z1,double h):TR1(x1,y1,z1),height(h){};
double volume(){ return TR1::area()*height; }
double area(){ return 2*TR1::area()+peri()*height; }
};
例二
#include<cmath>
#include<iostream>
using namespace std;
class RECT{
protected:
double x,y;
public:
RECT():x(0),y(0){};
RECT(double x1,double y1):x(x1),y(y1){};
virtual double area(){ return x*y; }
double peri(){ return (x+y)*2; }
int isSquare(){ return (x==y); }
};
class CUB:public RECT{
protected:
double height;
public:
CUB(double h,double x1,double y1):RECT(x1,y1),height(h){};
double volume(){ return RECT::area()*height; }
double area(){ return 2*RECT::area()+peri()*height; }
};
例三
#include<cmath>
#include<iostream>
using namespace std;
class Base{
protected:
string name;
int num;
public:
Base(){};
void print(){cout<<"姓名:"<<name<<' '<<num<<endl;}
};
class Student:public Base{
public:
Student(){
cout<<"姓名:";
cin>>name;
cout<<"考试成绩:";
cin>>num;
};
int Isgoodstudent(){ return(num>90); }
};
class Teacher:public Base{
public:
Teacher(){
cout<<"姓名:";
cin>>name;
cout<<"每年发表论文数:";
cin>>num;
};
int Isgoodteacher(){ return(num>3); }
};
例四
#include<iostream>
#include<string>
using namespace std;
class base{
public:
int price,kucun;
string place;
void print(){
cout<<"库存量:"<<kucun<<" 产地:"<<place<<" 单价:"<<price<<" ";
}
base(int ku,int pr,string pl):kucun(ku),price(pr),place(pl){}
void in_something(int n){ kucun+=n; }
void out_something(int n){ kucun-=n; }
};
class Shirt:public base{
public:
string buliao;
Shirt(int ku,int pr,string pl,string bu):base(ku,pr,pl),buliao(bu){}
void print1(){
cout<<"衬衫数据:"<<endl;
print();
cout<<"布料:"<<buliao<<" 总价格:"<<price*kucun<<endl;
}
};
class Wardrobe:public base{
public:
string mucai,yanse;
Wardrobe(int ku,int pr,string pl,string ya,string mu):base(ku,pr,pl),mucai(mu),yanse(ya){}
void print2(){
cout<<"立柜数据:"<<endl;
print();
cout<<"木料: "<<mucai<<" 颜色:"<<yanse<<" 总价格:"<<price*kucun<<endl;
}
};
class Cap:public Shirt{
public:
string style;
Cap(int ku,int pr,string pl,string bu,string st):Shirt(ku,pr,pl,bu),style(st){}
void print3(){
cout<<"帽子数据:"<<endl;
print();
cout<<"布料:"<<buliao<<" 样式:"<<style<<" 总价格:"<<price*kucun<<endl;
}
};
例五
#include<iostream>
#include<string>
using namespace std;
class Vehicle{
protected:
int wheels,weight;
public:
Vehicle(int wh,int we):wheels(wh),weight(we){}
void show(){ cout<<"车轮个数:"<<wheels<<endl; }
};
class Car:public Vehicle{
public:
int passager_load;
Car(int wh,int we,int pa):Vehicle(wh,we),passager_load(pa){}
void display(){
cout<<"车轮个数:"<<wheels<<" 载人数:"<<passager_load<<endl;
}
};
class Truck:public Car{
public:
int payload;
Truck(int wh,int we,int pa,int pay):Car(wh,we,pa),payload(pay){}
void display1(){ display(); }
};