C++类的继承

继承

初识继承

继承的作用是提高程序的复用性,定义一个基础类,其余类可以继承基础类的成员属性。相信子类继承父类的说法大家已经熟悉了。举一个形象的例子:

class people
{
public:
	string 姓名;
	int 年龄;
	void 共同行为()
	{
		吃饭睡觉;
	}
}; //基类,基础类,父类

class kid : public people
{
public:
	void 去学校()
	{
	}
}//派生类,子类

class dad : public people
{
public:
	void 去工作()
	{
	}
}//派生类,子类

在子类继承父类所有public属性的同时,也会保留自己的特有属性。除此之外,还有多继承以及继承限定词(还有访问修饰符?)不要着急,且往下看。

继承限定词

public继承:父类成员访问修饰符保持不变;
protected继承:父类public成员访问修饰符降级为protected以下;
private继承:父类所有可被继承的成员访问修饰符降级为private。

所以继承成员可访问性由低到高 public -> protected -> private。

class mom : protected people
{
public:
	void 去买菜()
	{}
}; //除了自身成员,父类继承来的成员全部降为protected

注意父类private成员不能被继承,不要与以上内容混淆~

构造,析构与继承

先用无参数的类演示一下构造函数执行顺序:

#include <iostream>

using namespace std;

class father //父类
{
public:
    father()
    {
        cout << "father" << endl;
    }
};

class son : public father //子类
{
public:
    son()
    {
        cout << "son" << endl;
    }
};

class grandson : public son //孙子类
{
public:
    grandson()
    {
        cout << "grandson" << endl;
    }
};

int main()
{
    grandson c1; //实例化孙子类

    return 0;
}

运行结果如下:
在这里插入图片描述
可以看到,最基础的基类,其构造函数首先调用,然后依次调用子类的构造函数,以此类推。
对于有参数的构造函数,父类构造函数如果有参数,那么子类需要在构造函数中为父类传递参数。同时,孙子类只需要为子类传递参数(因为父类的构造函数参数已经由子类传递)。代码如下:

#include <iostream>

using namespace std;

class father
{
public:
    father(int a)
    {
        a = 1;
        cout << "father" << endl;
    }
};

class son : public father
{
public:
    son(int b) : father(1) //初始化列表传递参数
    {
        b = 1;
        cout << "son" << endl;
    }
};

class grandson : public son
{
public:
    grandson() : son(2) //初始化列表传递参数
    {
        cout << "grandson" << endl;
    }
};

int main()
{
    son c1(1);
    grandson c2;

    return 0;
}

另外,继承的构造函数支持重载,具体取决于参数类型及数量。

析构函数

#include <iostream>

using namespace std;

class father
{
public:
    father(int a)
    {
        a = 1;
        cout << "father" << endl;
    }
    ~father()
    {
        cout << "recycle father" << endl;
    }

};

class son : public father
{
public:
    son(int b) : father(1)
    {
        b = 1;
        cout << "son" << endl;
    }
    son() : father(1)
    {
        cout << "son" << endl;
    }
    ~son()
    {
        cout << "recycle son" << endl;
    }
};

class grandson : public son
{
public:
    grandson()
    {
        cout << "grandson" << endl;
    }
    ~grandson()
    {
        cout << "recycle grandson" << endl;
    }
};

int main()
{
    grandson c2;

    return 0;
}

运行结果如下:
在这里插入图片描述
由此可见,继承时调用顺序为父类构造->子类构造->子类析构->父类构造。

覆盖

父类与子类含有同名成员,覆盖是C++的处理方式。
如果父类与子类均含有数据成员a,在子类作用域下,如果没有通过作用域修饰符声明使用父类的a,则默认采用子类的a。对于函数成员,同样也会覆盖父类的函数执行子类的函数。注意:这里的覆盖不是重载,如果参数列表不一致,编译器不会通过重载的方式匹配父类或子类的成员函数。

多态与虚函数

多态是泛型编程思想的核心,顾名思义,用相同的代码实现不同的功能,常见的用法就是用父类的指针调用子类的函数。其中虚函数是这个概念的语法基础。

class father
{
public:
	virtual void fun() //虚函数
	{}
};

class son : public father
{
public:
	void fun() //函数名字一致,才可以达到虚函数的效果
	{}
};

int main()
{
	father *p = new son;
	p->fun(); //此时如果子类中有实现,则调用子类的实现函数
	return 0;
}

需要注意的是虚函数同覆盖一样 ,不是重载,参数列表不同,父类的同名虚函数是无法调用子类的函数实现的。如果有多个子类,那么虚函数调用以具体分配的指针类型为准。
这里需要强调,虚函数究其本质是重写,由子类具体实现重写父类的virtual函数,是虚函数特有的性质,注意同覆盖和重载的区别。林外,子类重写的函数默认也是虚函数

值得一提的虚函数特点:

  1. 虚函数不能是内联函数;
  2. 构造函数不能是虚函数(构造函数也是内联函数);

虚表

利用父类指针新创建对象的时候,对象都会对应一块内存空间,我们把它称为虚表。除了数据成员以外,函数成员以指针的形式存在于虚表中。创建子类对象时,检测到虚函数在子类中有重写,则存放子类成员函数的调用地址,如果没有重写,则按照普通函数成员进行存放。

class father
{
public:
	int a; //对应成员地址1
	void fun1() //对应成员地址2
	{}
	virtual void fun2() //对应虚表地址->虚函数地址1
	{}
};

根据内存对齐,该类的内存分布可能会有空白但绝不会越界。函数指针存放在对应的虚表地址中。其中1和2的内容是固定的,而3处存放的地址可能是father::fun2()的地址,也可能是son::fun2()的地址(假如子类fun2有重写的话)。
内存分布
由上图可知,虚表存在于对象地址的第一个4字节(32位机),可以根据这个地址找到虚函数表的地址。虚函数表中每个成员都是一个指针(对于32位机就是4字节)。如果子类重写了虚函数,则定向到子类函数的地址,如果没有,则保持基类的成员函数地址。注意在该对象的成员区,非虚函数成员都存放该成员的具体内容,函数存放指针,数据成员按数据类型(int,float等)对齐排布。

虚析构

class father
{
public:
	int a;
	virtual void fun2()
	{}
	virtual ~father
	{}
};

class son : public father
{
public:
	int a;
	void fun2()
	~son //由于基类的析构定义成了虚函数,所以子类的析构默认为虚函数
	{}
};

这样可以避免父类指针创建子类对象,进行内存回收时,不执行子类析构函数导致回收不撤底的情况。具体是将析构函数声明为虚函数时,如果通过父类指针回收子类对象,会由基类一次执行虚构函数直至子类,避免程序因内存错误而崩溃。最好养成一个习惯:一旦要使用继承,基类析构函数一定加上virtual。

纯虚函数

纯虚函数可以没有具体实现,完全由子类继承重写。

class father
{
public:
	int a;
	virtual void fun() = 0; //纯虚函数的形式
	virtual ~father
	{}
};

注意,有纯虚函数的类不能实例化对象。通过father创建对象不会编译通过。只能通过子类继承的方式区创建对象,并且最终至少有一个中间类对纯虚函数进行了重写 有纯虚函数的类叫做抽象类,全部由纯虚函数成员构成的类称为接口类

在类作为基类的时候,需要将析构函数写为虚函数,如果不作为基类使用,则不需要写为虚函数(会占用额外内存开销,但没有问题 )。

虚继承

为了避免继承中产生的二义性,C++的虚继承可以避免多继承中产生的歧义。

class A
{
public:
	int a;
}

class B : public A
{}

class C : public A
{}

class D : public B, public C 
{}
// B和C继承A,D继承B和C(多继承)

上述代码会产生歧义,导致对成员a的重复复制,因此采用虚继承以避免这个问题,代码如下:

class A
{
public:
	int a;
}

class B : virtual public A //此时A类称为虚基类
{}

class C : virtual public A
{}

class D : public B, public C 
{}

虚继承不会对成员a进行多次复制,仅仅是给予子类对基类成员的使用权,可以保证D类从father基类的成员只复制一份。极端情况,如果B和C类都声明了相同的成员x,则D类创建成员的时候依然会产生歧义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值