C++继承和派生

一、概念

C++中,继承就是在一个已存在的类的基础上建立一个新的类。已存在的类称为基类,又称父类;新建立类称为派生类,又称为子类。
基类是对派生类的抽象,派生类是对基类的具体化。

1.派生类的定义与构成

1)派生类的定义

  class 派生类名:类派生列表{

    成员列表
  };

类派生列表指定了一个或多个基类,形式如下:

访问权限标号 基类名1,访问权限标号 基类名2,...

2)派生类的构成

派生类由两部分组成:第一部分是从基类继承得到的,另一部分是自己定义的新成员,这些成员仍然分为三种访问属性。

注意,友元关系是不能继承的:一方面,基类的友元对派生类成员没有特殊的访问权限;另一方面,如果基类被授予了友元关系,则只有基类有特殊访问权限,该基类的派生类不能访问授予友元关系的类。

实际编程中,设计一个派生类包括三个方面工作:

a)从基类接收成员。

除了构造函数与析构函数之外,派生类会把基类的全部成员继承过来。

b)调整基类成员的访问。

程序员可以对接收的成员指定访问策略。

c)在定义派生类时增加新的成员。
  
另外还应该自己定义派生类的构造函数和析构函数,因为它们不能从基类继承过来。

二、规则

1.继承重要说明

1)子类拥有父类的所有成员变量和成员函数
2)子类就是一种特殊的父类
3)子类对象可以当作父类对象使用
4)子类可以拥有父类没有的方法和属性

2.public、protected、private,三个关键字分别修饰变量的权限:

public:  修饰的成员变量 方法 在类的内部 类的外部都能使用

protected: 修饰的成员变量方法,在类的内部使用 ,在继承的子类内部中可用 ;其他 类的外部不能被使用。

private:   修饰的成员变量方法 只能在类的内部使用 不能在类的外部

3.不同的继承方式会改变继承成员的访问属性

1)C++中的继承方式会影响子类的对外访问属性

public继承方式: 父类成员在子类中保持原有访问级别

private继承方式: 父类成员在子类中变为private成员。
             (注意,在子类中变成了私有的,子类本身内部可以访问,只是不能访问父类私有的而已)

protected继承方式:父类中public成员会变成protected,其余两个保持不变。

2)private成员在子类中依然存在,但是却无法访问到。

重点:不论种方式继承基类,派生类都不能直接使用基类的私有成员。

4.派生类访问控制的结论

1)protected 关键字 修饰的成员变量 和成员函数 ,是为了在家族中使用 ,是为了继承。

2)项目开发中 一般情况下 是 public。保护继承与私有继承在实际编程中极少使用,它们只在技术理论上有意义。

三、类型兼容性原则

类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。类型兼容规则中所指的替代包括以下情况:

子类对象可以当作父类对象使用

子类对象可以直接赋值给父类对象

子类对象可以直接初始化父类对象

父类指针可以直接指向子类对象

父类引用可以直接引用子类对象

在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继承的成员。

类型兼容规则是多态性的重要基础之一。


#include <iostream>
using namespace std;

class Parent
{
public:
	void printP()
	{
		cout<<"我是爹..."<<endl;
	}

	Parent()
	{
		cout<<"parent构造函数"<<endl;
	}

	Parent(const Parent &obj) //未具体实现的copy构造函数
	{
		cout<<"copy构造函数"<<endl;
	}

private:
	int a;
};

class child : public Parent
{
public:
	void printC()
	{
		cout<<"我是儿子"<<endl;
	}
protected:
private:
	int c;
};

//C++编译器 是不会报错的 .....
void howToPrint(Parent *base)
{
	base->printP(); //父类的 成员函数 

}

void howToPrint2(Parent &base)
{
	base.printP(); //父类的 成员函数 
}
void main()
{
	Parent p1;
	p1.printP();

	child c1;
	c1.printC();
	c1.printP();

	//赋值兼容性原则 
	//1-1 基类指针 (引用) 指向 子类对象
	Parent *p = NULL;
	p = &c1;
	p->printP();  

	//1-2 指针做函数参数
	howToPrint(&p1);
	howToPrint(&c1); 

	//1-3引用做函数参数
	howToPrint2(p1);
	howToPrint2(c1); 

	//第二层含义
	//可以让子类对象   初始化   父类对象
	//子类就是一种特殊的父类
	Parent p3 = c1;  //用一个类去初始化另一个类的时候回调用copy构造函数

	cout<<"hello..."<<endl;
	system("pause");
	return ;
}

四、继承中派生类的构造和析构函数

问题:如何初始化父类成员?父类与子类的构造函数有什么关系?

首先:
1、子类对象在创建时会首先调用父类的构造函数
2、父类构造函数执行结束后,执行子类的构造函数
如果:
3、当父类的构造函数有参数时,需要在子类的初始化列表中显示调用
最后:
4、析构函数调用的先后顺序与构造函数相反

#include <iostream>
using namespace std;

//结论
//先 调用父类构造函数调用 再调用 子类构造函数
//析构的顺序  和构造相反

class Parent
{
public:
	Parent(int a, int b)
	{
		this->a = a;
		this->b = b;
		cout<<"父类构造函数..."<<endl;
	}
	~Parent()
	{
		cout<<"析构函数..."<<endl;
	}

	void printP(int a, int b)
	{
		this->a = a;
		this->b = b;
		cout<<"我是爹..."<<endl;
	}
private:
	int a;
	int b;
};


class child : public Parent
{
public:
	child(int a, int b, int c) : Parent(a, b)   //用初始化列表来调用父类的构造函数
	{
		this->c = c;
		cout<<"子类的构造函数"<<endl;
	}
	~child()
	{
		cout<<"子类的析构"<<endl;
	}
	void printC()
	{
		cout<<"我是儿子"<<endl;
	}
protected:
private:
	int c;
};


void playObj()
{
	child c1(1, 2, 3);
}
void main()
{
	//Parent p(1, 2);
	playObj();


	cout<<"hello..."<<endl;
	system("pause");
	return ;
}

五、继承与组合(对象B包含了对象A)混搭情况下,构造和析构调用原则

原则:先构造父类,再构造成员变量(成员变量是一个对象)、最后构造自己
    先析构自己,在析构成员变量、最后析构父类

假设如下:派生类对象child,继承了父类Parent,同时有组合(包含)了对象Object。


class child : public Parent  //继承了父类
{
public:
	child(char *p) : Parent(p) , obj1(3, 4), obj2(5, 6) //先构造父类,再构造成员变量(成员变量是对象)、最后构造自己
	{                                                                //成员变量的构造是按定义的顺序来构造的。
		this->myp = p;
		cout<<"子类的构造函数"<<myp<<endl;
	}
	~child()
	{
		cout<<"子类的析构"<<myp<<endl;
	}
	void printC()
	{
		cout<<"我是儿子"<<endl;
	}
protected:
	char *myp;
	Object obj1;   //成员变量类型,是对象Object。
	Object obj2;
};

六、继承中的同名成员变量和函数的处理方法

1、当子类成员变量与父类成员变量同名时

2、子类依然从父类继承同名成员

3、在子类中通过作用域分辨符::进行同名成员区分(在派生类中使用基类的同名成员,显式地使用类名限定符)

4、同名成员存储在内存中的不同位置

重点:如果没有使用作用域分辨符,而直接使用得话,默认使用的是子类的成员变量或者函数。

案例:对象B继承了对象A,并且对象B和对象A,都包含了同名变量b。

#include <iostream>
using namespace std;

class A
{
public:
	int a;
	int b;
public:
	void get()
	{
		cout<<"b "<<b<<endl;
	}
	void print()
	{
		cout<<"AAAAA "<<endl;
	}
protected:
private:
};

class B : public A
{
public:
	int b;
	int c;
public:
	void get_child()
	{
		cout<<"b "<<b<<endl;
	}
	void print()
	{
		cout<<"BBBB "<<endl;
	}
protected:
private:
};

//同名成员函数
void main()
{
	B b1; 
	b1.print();            // 没有指定作用域的时候,默认情况使用的是子类的成员函数

	b1.A::print();         //指定作用域A
	b1.B::print();         //指定作用域B

	system("pause");
}

//同名成员变量
void main71()
{
	B b1;
	b1.b = 1;       // 没有指定作用域的时候,默认情况修改的是子类的成员变量
	b1.get_child();

	b1.A::b = 100; //修改父类的b    用作用域分辨符指定作用域
	b1.B::b = 200; //修改子类的b    用作用域分辨符指定作用域

	b1.get();


	cout<<"hello..."<<endl;
	system("pause");
	return ;
}

七、多继承和虚继承

1.多继承
1)多继承的定义以及多继承的语法
a. 一个类有多个基类,这样的继承关系称为多继承;
b. 多个直接基类,构造函数执行顺序,取决于定义派生类时,指定的各个继承基类的顺序。
c. 多继承声明语法:

	class 派生类名: 访问控制符 基类名1,访问控制符 基类名2 
	{ 
		数据成员和成员函数声明; 
	}
	
	class A: public B,public c
	{
	}

图示:
在这里插入图片描述

2.虚继承
虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题。如下图所示:图1,菱形继承

在这里插入图片描述

类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A–>B–>D 这条路径,另一份来自 A–>C–>D 这条路径。

在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B–>D 这条路径,还是来自 A–>C–>D 这条路径。下面是菱形继承的具体实现:

//间接基类A
class A{
protected:
    int m_a;
};
//直接基类B
class B: public A{
protected:
    int m_b;
};
//直接基类C
class C: public A{
protected:
    int m_c;
};
//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //命名冲突
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};
int main(){
    D d;
    return 0;
}

这段代码实现了上图所示的菱形继承,第 25 行代码试图直接访问成员变量 m_a,结果发生了错误,因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。

虽然为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:

void seta(int a){ B::m_a = a; }
void seta(int a){ C::m_a = a; }

但是这种做法显然不是很合理。

因此,C++为了解决多继承时的命名冲突和冗余数据问题,提出了虚继承,使得在派生类中只保留一份间接基类的成员。
如下图2:使用虚继承解决菱形继承中的命名冲突问题

在这里插入图片描述

在继承方式前面加上 virtual 关键字就是虚继承,请看下面的例子:

//间接基类A
class A{
protected:
    int m_a;
};
//直接基类B
class B: virtual public A{  //虚继承
protected:
    int m_b;
};
//直接基类C
class C: virtual public A{  //虚继承
protected:
    int m_c;
};
//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};
int main(){
    D d;
    return 0;
}

这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。

虚继承的实现分析:
虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值