【C++自学笔记】详细解读——C++面向对象之继承(内含详细理解菱形继承和菱形虚拟继承)

一、继承的概念及定义

1、继承的概念

继承机制是面向对向程序涉及使代码可以复用的最重要的手段,它允许程序猿在保持原有类特性的基础上进行了扩展,增加功能,产生新的类,称为派生类(子类)。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程,继承是类设计层次的复用。

2、继承的定义

先看一个例子:

class Person {
public:
	void Print() {
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	//C++11特性:支持定义时进行初始化
	string _name = "Peter";
	int _age = 18;
};
class Student :public Person {
protected:
	int _stuid;
	int _major;
};

C++中继承的定义格式为:

其中继承方式和访问限定符遵循如下规定:

3、三种继承方式:

public继承:

  1. 派生类(子类)继承基类(父类)的数据;
  2. 共有的继承方式,可以访问父类的共有成员(public)与保护成员(protected),但无法访问私有成员(private);
  3. 基类中的public在派生类中也是public;
  4. 基类中的protected在派生类中也是protected;
  5. 基类中的私有成员,仍然归父类所有,虽然子类继承了,但是无法进行访问(可以调用父类的共有方法来访问)

protected继承:

  1. 保护的继承方式;
  2. 基类中的public到派生类中称为protected;
  3. 基类中的保护成员访问权限不变,依旧是protected;
  4. 基类中的私有成员也是一样,被继承了,但是用不了;

private继承:

  1. 私有的继承方式;
  2. 在基类中所有的成员变量权限均变为私有;

注意点:

  • 基类(父类)private 成员在派生类(子类)中无论以什么方式继承都是不可见的;(实际上被继承了,但是语法限制无法访问)
  • 保护成员限定符是因为继承才出现的;
  • 基类的其他成员在子类中的访问方式总是取最小的(public>protected>private);
  • 使用关键字 class 默认的继承方式是 private,但是使用 struct 时默认的访问方式为 public;
  • 在实际运用的过程中一般都是 public 继承,很少使用 protected/private 继承;

二、基类和派生类对象赋值转换

1、派生类对象何以赋值给 基类的对象/基类的指针/基类的引用 ;(切片/切割:把派生类中父类的那部分切来赋值)

class Preson {
protected:
	string _name;
	string _sex;
	int _age;
};
class Student :public Preson {
public:
	int _No;
};

void Test() {
	Student s;
	//子类对象赋值给父类对象/指针/引用
	Preson p = s;
	Preson* pp = &s;
	Preson& rp = s;

}

2、基类对象不能赋值给派生类对象;

3、基类的指针可以通过强制类型转换赋值给派生类的指针,但是必须是基类的指针是指向派生类对象时才是安全的;

void Test() {
	Student s;
	//子类对象赋值给父类对象/指针/引用
	Preson p = s;
	Preson* pp = &s;
	Preson& rp = s;

	//基类对象不能赋值给派生类对象
	//s = p;

	//基类的指针可以通过强制类型转换赋值给派生类的指针
	pp = &s;
	Student* ps1 = (Student*)pp;
	ps1->_No = 10;

	pp = &p;
	Student* ps2 = (Student*)pp;//可能会存在越界访问的问题
	ps2->_No = 10;
}

三、继承中的作用域

在继承体系中基类和派生类都有独立的作用域;

如果父类和子类中由同名成员,子类成员将会屏蔽父类对同名成员的直接访问,这种现象叫做隐藏,也叫重定义;(在子类成员函数中,可以使用 基类::基类成员 显示访问);

如果时成员函数的隐藏,只需要函数名相同就构成隐藏;

在实际运用过程中,继承体系里面最好不要定义同名的成员;

class Person {
protected:
	string _name = "xiaoxiao";//Name
	int _num = 111; //PersonID
};
class Student :public Person {
public:
	void Print() {
		cout << "Name = " << _name << endl;
		cout << "PersonID = " << _num << endl;
		cout << "StudentID = " << _num << endl;
	}
protected:
	int _num = 999; //StudentID
};
void Test() {
	Student s1;
	s1.Print();
}

那么如果存在同名函数呢?

class A {
public:
	void fun() {
		cout << "func" << endl;
	}
};
class B :public A {
public:
	void fun(int i) {
		fun();
		cout << "fun(int i)-->" << i << endl;
	}
};
void Test1() {
	B b;
	b.fun(10);
}

这个代码会报错:

需要在B类fun中调用的fun前面叫上作用域限定符;

==>>

B中的fun和A中的fun并不是构成重载!!因为两者不在同一个作用域!

B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏;

四、派生类的默认成员函数

类中的6个默认成员函数,“默认”就意味着,就算我们不写,编译器也会自动生成,为了详细的理解,我们先上一段代码:
 


class Preson {
public:
	Preson(const char* name = "peter")
		:_name(name)
	{
		cout << "Preson()" << endl;
	}
	Preson(const Preson& p) 
		:_name(p._name)
	{
		cout << "Preson(const Preson& p)" << endl;
	}
	Preson& operator=(const Preson& p) {
		cout << "Preson& operator=(const Preson& p)" << endl;
		if (this != &p) {
			_name = p._name;
		}
		return *this;
	}
	~Preson() {
		cout << "~Preson()" << endl;
	}
protected:
	string _name;
};
class Student :public Preson {
public:
	Student(const char* name, int num)
		:Preson(name)
		, _num(num)
	{
		cout << "Studetn()" << endl;
	}
	Student(const Student& s)
		:Preson(s)
		, _num(s._num)
	{
		cout << "Student(const Studetn& s)" << endl;
	}
	Student& operator=(const Student& s) {
		cout << "Student& operator=(const Student& s)" << endl;
		if (this != &s) {
			Preson::operator = (s);
			_num = s._num;
		}
		return *this;
	}
	~Student() {
		cout << "~Student()" << endl;
	}
protected: 
	int _num;
};
void Test() {
	Student s1("jack",18);
	Student s2(s1);
	Student s3("rose", 17);
	s1 = s3;
}

运行结果:

通过上面的代码,可以得出结论:派生类的默认成员函数只是负责了自己的成员,继承所来的基类的成员还是需要调用基类中的成员函数进行初始化;

总结一下:

  1. 派生类的构造函数:必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用;
  2. 派生类的拷贝构造函数:必须调用基类的拷贝构造函数完成基类的拷贝初始化;
  3. 派生类的赋值运算符重载函数:必须调用基类的opertor=完成基类的复制;
  4. 派生类的析构函数:会在被调用完成后自动调用基类的析构函数清理基类成员,因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

派生类对向初始化先调用基类构造在调用派生类构造;派生类对象析构清理先调用派生类西沟再调用基类的析构;

一道常见的面试题:实现一个无法被继承的类

==>> (C++98)将该类的构造函数设为 private (私有化);

==>> (C++11)再类名后面加上关键字 final;

五、继承与友元

友元关系不能继承!!!!(基类友元不能访问子类私有和保护成员)

#if 1
//继承与友元
class Student;
class Person {
public:
	friend void DisPlay(const Person& p, const Student& s);
protected:
	string _name;
};
class Student :public Person {
protected:
	int _stuNum;
};
void DisPlay(const Person& p,const Student& s) {
	cout << p._name << endl;
	cout << s._stuNum << endl;
}
#endif

以上代码会报错:

六、继承与静态成员

基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员!(无论派生出多好个子类,都只有一个 static 成员实例)

#if 1
//继承与静态成员
class Person {
public:
	Person() { ++_count; }
protected:
	string _name;
public:
	static int _count;
};
int Person::_count = 0;
class Student :public Person {
protected:
	int _num;
};
class Graduate :public Student {
protected:
	string _seminarCourse;
};
void Test(){
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << "人数" << Person::_count << endl;
	Student::_count = 0;

	cout << "人数" << Person::_count << endl;
}
#endif

七、复杂的菱形继承及菱形虚拟继承

1、单继承:一个子类只有一个直接的父类时成这个继承关系为单继承;

2、多继承:一个子类有两个或两个以上直接父类时称这个继承关系为多继承;

3、菱形继承:菱形继承时多继承的一种特殊情况;

菱形继承的问题:从下面的对象成员构造模型构造,可以看出菱形继承有 数据冗余 和 二义性 的问题,在Assistant的对象成员中 Preson 成员会有两份;

看一段代码:

#if 1
//菱形继承
class Person {
public:
	string _name;
};
class Student :public Person {
protected:
	int _num;
};
class Teacher :public Person {
protected:
	int _id;
};
class Assistant :public Student, public Teacher {
protected:
	string _major;
};
void Test() {
	Assistant a;
	a._name = "peter";
}
#endif

结果==

如果要解决菱形继承的二义性问题,就必须显示指定访问那个父类的成员!但是数据冗余的问题 无法解决;

void Test() {
	Assistant a;
	//a._name = "peter";
	a.Student::_name = "zhangsan";
	a.Teacher::_name = "lisi";
}

虚拟继承可以解决领袖继承的 二义性 和 数据冗余 问题。如上面的继承关系,在Student 和Teacher 的继承 Person 时使用虚拟继承,即可解决问题,需要注意的问题是,虚拟继承不要在其他地方去使用。

虚拟继承解决 数据冗余 和 二义性 的原理:

虚拟继承最主要的功能就是,使得在继承过程中,不管派生类中有多少个共用基类,都只公用一个基类。因此在派生类中,前4个字节首先存了一个指针,这个指针指向一个类似数组的区域,里面装着要访问基类对象时,该指针要偏移的偏移量。

#if 1
//探究虚拟继承的原理
class A {
public:
	int _a;
};
class B : virtual public A {
public:
	int _b;
};
class C : virtual public A {
public:
	int _c;
};
class D :public B, public C {
public:
	int _d;
};
void Test() {
	D d;
	d.B::_a = 1;
	d.C::_a = 2;

	d._b = 3;
	d._c = 4;
	d._d = 5;

	//cout << d.B::_a << endl;
	//cout << d.C::_a << endl;
	//cout << d._b << endl;
	//cout << d._c << endl;
	//cout << d._d << endl;

}
#endif

打开内存窗口(左:菱形继承;右:菱形虚拟继承):

1、当 d 对象被实例化完毕后:

     

 

2、执行 d.B::_a = 1后:

     

3、执行 d.C::_a = 1后:

    

4、执行完后面的语句(左边可见 数据冗余:A多次重复出现 ):

     

5、细论菱形虚拟继承的内存:

被圈出的地方,实际上就是开始说过的4个字节存了一个指针,这个就是指向虚基表,通过虚基表中指定的偏移量,从而找到公共的那块基类成员数据。在这张图中就是地址 0x012FF704 这块内存; 

 

结论:派生类 D 通过 B 和 C 两个指针,指向的一张表。这两个指针叫做虚基表指针,这两个表叫做虚基表。虚基表中存的偏移量。通过偏移量就可以找到 A。  

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_ClivenZ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值