面向对象程序设计笔记 继承与派生

继承

继承是使代码可以复用的重要手段,也是面向对象程序设计的核心思想之一。简单的说,继承是指一个对象直接使用另一对象的属性和方法。

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(); }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值