【C++】别再用错public了!C++继承暗坑全图鉴,虚继承才是救世主

🔥小陈又菜个人主页

📖个人专栏《MySQL:菜鸟教程》《小陈的C++之旅》《Java基础》

✨️想多了都是问题,做多了都是答案!



1. 继承的概念和定义

1.1. 继承的概念

继承 (inheritance) 机制是面向对象程序设计 使代码可以复用 的最重要的手段,它允许程序员在
持原有类特性的基础上进行扩展
,增加功能,这样产生新的类,称派生类。继承 呈现了面向对象
程序设计的层次结构
,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用, 继
承是类设计层次的复用。

一个简单的基类:

在一个类派生出另一个类的过程中,原始类我们称作基类,继承类称作派生类。下面我们使用一个例子来进行说明,首先我们要有一个基类Person,然后我们派生出一个Student学生类:

//父类、基类
class Person
{
public:
	void Print()
	{
		cout << "name :" << _name << endl;
		cout << "age :" << _age << endl;
	}
 
//private:
//protected:
//这里是一个C++11的类内初始化特性!
	string _name = "Peter";
	int _age = 18; 
};

我们现在写一个学生类:

首先我们看到派生类的声明方式

//派生类的声明方式
class Student : public Person
{
public:
	void func()
	{
		Person::Print();
	}
protected:
	int _stuId;
};

派生类会继承基类的所有公共部分,同时基类的私有部分也会成为派生类的一部分,总的来说就是Student类定义的对象s,可以使用Person类中的公有方法

我们看到Student是继承自Person,同时添加了Student自己的成员变量和成员函数:


 

从sizeof的输出我们可以看出Student确实是包括了Person中的成员变量的,至于为什么是56(这个可能因为机器不同结果不同),感兴趣的同学可以研究一下。

1.2. 继承的定义

1.2.1. 定义格式

上文我们提到,Person是基类,Student是派生类,继承方式是公有继承:

1.2.2. 继承关系和访问限定符

继承关系除了上面讲到的公有继承(public),还包括保护继承(protected)、私有继承(private),在加上基类成员本身的访问限定,所以组合之后会有9中访问关系。

1.2.3. 访问关系

类成员 / 继承方式

public 继承

protected 继承

private 继承

基类的 public

成员

派生类的 public

成员

派生类的 protected

成员

派生类的 protected

成员

基类的 protected 成员

派生类的 protected成员

派生类的 protected

成员

派生类的 private

成员

基类的 private

 

在派生类中

不可见

在派生类中

不可见

在派生类中

不可见

总结:

  • 基类的private成员,不管怎样继承对于派生类讲都是不可见的。这里的不可见不是说,基类的私有成员没有继承到派生类,而是说处于某种语法上的限制,不允许被访问
  • 基类的私有成员不能被派生类访问,但是如果我不想在类外被使用,但是想要能够让派生类访问,我们可以使用protected。所以说protected就是因为这种情景而设计的
  • 从上面的九个访问关系可以看出,基类的private成员对于派生类不可访问,其他基类成员对于派生类来说,访问关系 = Min(基类中的访问权限,派生类的继承方式),public > protected > private
  • 如果使用的class,默认的继承方式是private;如果是struct,默认的继承方式是public(建建议写出继承方式)
  • 在实际的使用中,一般都是使用公有继承(其他的继承方式可维护性比较低)

下面举一个例子:

int main()
{
	Student s;
	s.Print();
	s._age;
	s._name;
	s._ID;
	return 0;
}

2. 基类和派生类赋值类型转换

  • 派生类对象可以赋值给基类对象/基类的指针/基类的引用(这里的规则是:切片规则,意思就是派生类会将原来基类中的那些成员切割下来
  • 基类的对象不能赋值给派生类
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,前提是基类的指针必须是指向派生类才是安全的

切片

class Person
{
 
//protected:
public:
	string _name; // 姓名
	string _sex;  // 性别
	int	_age;	 // 年龄
};
 
class Student : public Person
{
public:
	int _No; // 学号
};

1. 子类对象赋值给父类 -- 对象、指针、引用,天然语法支持,没有类型转换

Person p;
Student s;
p = s;				//对象
Person& rp = s;		//引用
Person* ptrp = &s;	//指针

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

3. 基类的指针可以通过强制类型转换赋值给派生类

Person p;
Student s;

p = s;				//对象
Person& rp = s;		//引用
Person* ptrp = &s;	//指针

ptrp = &s;
Student* ptrs = (Student*)ptrp;
cout << ptrs->_No << endl;

ptrp = &p;
Student* ptrs2 = (Student*)ptrp;
cout << ptrs2->_No << endl; 

大家可以看一下这段代码的最后两部分,我们都试图通过使用强制类型转换的方式将基类指针赋值给派生类,但是仔细阅读会发现还是有所区别,前置ptrp指向派生类,后者ptrp指向基类:

我们发现因为ptrp是指向派生类的,所以在进行派生类的强制类型转换的时候指针的结构并没有冲突。但是,后者ptrp是指向了基类,而此时继续进行派生类的强制类型转换,指针指向的布局并不符合Student类,所以这个时候会发上访问到无效的内存数据

3. 继承的作用域

  • 基类和派生类都有自己独立的作用域
  • 当子类和父类出现同名成员,子类会屏蔽对父类中同名成员的访问,这种情况叫隐藏、重定义(在子类成员函数中可以通过 基类名::基类成员 进行访问)
  • 如果是同名成员函数的隐藏,只需要函数相同即可,不需要参数列表一致
  • 在实际中,应该尽量避免同名成员的出现

1. 成员变量导致隐藏关系

class Person
{
protected:
	string _name = "张三"; // 姓名
	int _num = 111; 	    // 身份证号
};
 
class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << " 学号:" << _num << endl;//会使用自己的成员变量 999
		cout << " 身份证号:" << Person::_num << endl; //要指定作用域
	}
protected:
	int _num = 999; // 学号
};
int main()
{
	Student s;
	s.Print();
	return 0;
}

2. 成员方法导致隐藏关系

class A
{
public:
	void fun()
	{
		cout << "A::func()" << endl;
	}
};
class B : public A
{
public:
	void fun()
	{
		cout << "B::func()" << endl;
	}
};

int main()
{
	B b;
	b.fun();//b的fun()隐藏了A的fun()
	b.A::fun();//指定作用域即可访问到A的func()
	return 0;
};

4. 派生类的默认成员函数

我们在之前刚学习C++类和对象时,就提到了类会有6个默认成员函数,那么在派生类的影响下,会对这6个默认成员函数有什么影响呢?

  • 派生类不会继承基类的构造函数,所以派生类必须先调用基类的构造函数来初始化基类的那一部分成员(如果基类没有默认构造函数,那就必须显式调用),然后再调用派生类构造函数
  • 派生类先调用自己的析构函数,然后自动调用基类的析构函数清理基类成员(析构顺序:派生析构 → 派生类成员析构 → 基类析构

  • 派生类拷贝构造函数会自动调用基类的拷贝构造函数

  • 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制

  • 因为后续一些场景析构函数需要构成重写, 重写的条件之一是函数名相同 。 那么编译器会对析构函数名进行特殊处理,处理成destrutor() ,所以父类析构函数不加 virtual 的情况下,子类析构函数和父类析构函数构成隐藏关系

class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
		cout << "Person(const char* name)" << endl;
	}
	//拷贝构造
	Person(const Person& p)
		:_name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}
	
	//赋值拷贝
	Person& operator=(const Person& p)
	{
		cout << "Person& operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;
 
		return *this;
	}
 
	~Person()
	{
		cout << "~Person()" << endl;
	}
 
protected:
	string _name;//姓名
};
 
class Student : public Person
{
public:
	Student(const char* name = "", int num = 0)
		:_num(num)
		,Person(name)
	{
		cout << "Student(const char* name = "", int num = 0)" << endl;
	}
 
	//拷贝构造
	Student(const Student& s)
		:Person(s)
		,_num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}
 
	//赋值构造
	// s1 = s3
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_num = s._num;
		}
		cout << "Student& operator=(const Student& s)" << endl;
 
		return *this;
	}
 
	//父类和子类析构函数构成隐藏关系
	//原因:多态的需要,析构函数名同意会被处理成destructor()
	//为了保证析构的顺序,先子后父
	//子类析构函数完成后会自动调用父类析构函数,所以不需要我们显示调用
	~Student()
	{
		//父类的析构函数我们不需要显示调用
		//Person::~Person();
		cout << "~Student()" << endl;
	}
protected:
	int _num; //学号
 
};

5. 继承与友元

  • 派生类不会继承基类的友元
  • 基类的友元对于派生类没有特殊权限
class Base {
    friend void friendOfBase(Base& b);
private:
    int basePrivate;
};

class Derived : public Base {
private:
    int derivedPrivate;
};

void friendOfBase(Base& b) {
    b.basePrivate = 10; // 合法(Base的友元)
    // 无法访问 Derived::derivedPrivate(即使参数是Derived对象)
}
  • 派生类的友元只能访问自身的成员
class Derived : public Base {
    friend void friendOfDerived(Derived& d);
private:
    int derivedPrivate;
};

void friendOfDerived(Derived& d) {
    d.derivedPrivate = 20; // 合法(Derived的友元)
    // d.basePrivate = 30; // 错误!无法访问基类的私有成员
}
  • 如果要在派生类中访问基类的私有成员,需要在派生类中提供相同的友元函数
class Base {
    friend void friendOfDerived(Derived& d); // 显式声明
private:
    int basePrivate;
};

class Derived : public Base {
    friend void friendOfDerived(Derived& d);
private:
    int derivedPrivate;
};

void friendOfDerived(Derived& d) {
    d.derivedPrivate = 20; // 合法(Derived的友元)
    d.basePrivate = 30;  // 合法(现在是Base的友元)
}

6. 继承与静态成员

当基类中定义了一个static静态成员,无论派生了多少个子类,这个静态成员都只有一个:

class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
 
int Person::_count = 0;
 
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
 
class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};
//
int main()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	Person s;
 
	cout << " 人数 :" << Person::_count << endl;
	cout << " 人数 :" << Student::_count << endl;
	cout << " 人数 :" << s4._count << endl;
 
	//同一个地址
	cout << " 人数 :" << &Person::_count << endl;
	cout << " 人数 :" << &Student::_count << endl;
	cout << " 人数 :" << &s4._count << endl;
 
	return 0;
}

我们可以看出不管是从值还是地址的角度,基类中的静态成员都只有一个!

7. 菱形继承,菱形虚拟继承

  • 单继承,一个子类只有一个直接父类

  • 多继承,一个子类有多个直接父类,这种继承关系叫做多继承

  • 菱形继承是多继承的一种特殊情况

7.1. 菱形继承问题

从上面的图中可以看出,因为Assistant同时继承了Student和Teacher,所以就会导致有两份Person成员:

#include <iostream>
using namespace std;

class Person
{
public:
	string _name; // 姓名
	int _a[10000]; //会有多份_a
};
class Student : Person
{
protected:
	int _num; //学号
};
class Teacher : Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	Assistant asst;
	asst._name = "张三"; //只有一份_name
	cout << asst._name << endl;
	return 0;
}

因为两份Person成员的存在,所以访问_name的时候就会出现二义性,不知道是哪一个。

7.2. 虚拟继承

虚拟继承可以解决二义性指向不明以及数据冗余的问题,就是在Student和Teacher继承Person时候进行虚拟继承:

//继承的时候使用vittual关键字
class Student : virtual  public Person
{
protected:
	int _num; //学号
};
class Teacher : virtual  public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

(本篇完)

评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值