C++之继承<inheritance>

🌈前言

本篇文章进行C++中类继承(inheritance)的学习!!!


🌸 继承

🌷1、继承的概念与定义

🌸1.1、继承的概念

继承(inheritance):

  • 继承机制是面向对象程序设计中使代码可以复用的重要手段
  • 它允许我们在保持原有类特性的基础上进行扩展,增加功能 ,这样的类,称为“派生类”
  • 继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程,以前我们接触的复用都是函数复用(重载),继承是类设计层次的复用
class Person
{
public:
	void Show() const
	{
		cout << "name: " << _name << endl;
		cout << "age: " << _age << endl;
	}
protected:
	string _name = "peter";
	int _age = 18;
};

class Student : public Person
{
public:
	void ShowPlus() const
	{
		Show();
		cout << "student id: " << _stuId << endl;
	}
protected:
	int _stuId = 1314;
};

class Teacher : Person
{
public:
	void ShowPlus() const
	{
		Show();
		cout << "job id: " << _jobId << endl;
	}
protected:
	int _jobId = 520;
};

int main()
{
	Student s;
	Teacher t;
	
	s.ShowPlus();
	cout << endl;
	t.ShowPlus();
	return 0;
}

注意:

  • 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分
  • 这里体现出了Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。调用ShowPlus可以看到成员函数的复用

在这里插入图片描述


🌹1.2、继承的定义

  1. 定义的格式

下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类

在这里插入图片描述

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

在这里插入图片描述

  1. 继承基类成员访问方式的变化
类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected派生类的protected成员派生类的private成员
基类的private成员在派生类不可见在派生类不可见在派生类不可见

举例1️⃣(public继承):

class Person
{
public:
	void Print()
	{
		cout << _name << endl;
	}
protected:
	string _name;
private:
	int _age;
};

// 公有继承:基类公有成员成为派生类公有成员;保护成员成为派生类保护成员;
// 私有成员成为派生类私有成员,但是语法规定私有成员在派生类不可见
class Student : public Person
{
protected:
	int _stunum; // 学号
};

int main()
{
	Student s;
	return 0;
}

在这里插入图片描述

举例2️⃣(protected继承):

class Person
{
public:
	void Show() const
	{
		cout << "name: " << _name << endl;
		cout << "age: " << _age << endl;
	}
protected:
	string _name = "peter";
	int _age = 18;
private:
	string _character = "固执";
};

// 保护继承,基类公有成员和保护成员都成为派生类的保护成员,派生类可以访问它们,
// 但是基类私有成员还是不可见
class Student : protected Person
{
public:
	void ShowPlus() const
	{
		// 可以直接访问继承过来的基类成员
		Show();
		cout << endl;
		cout << "name: " << _name << endl;
		cout << "age: " << _age << endl;
		cout << "student id: " << _stuId << endl;
	}
protected:
	int _stuId = 1314;
};

int main()
{
	Student s;
	s.ShowPlus();
	return 0;
}

在这里插入图片描述

举例3️⃣(private继承):

class Person
{
public:
	void Show() const
	{
		cout << "name: " << _name << endl;
		cout << "age: " << _age << endl;
	}
protected:
	string _name = "peter";
	int _age = 18;
private:
	string _character = "固执";
};

// 私有继承,基类中的公有成员、保护成员、私有成员都成为派生类私有成员的一部分,
// 但基类的私有成员在派生类中还是不可见
class Student : private Person
{
public:
	void ShowPlus() const
	{
		// 访问自身继承过来的私有成员
		Show();
		cout << endl;
		// 访问自身继承过来的私有成员
		cout << "name: " << _name << endl;
		cout << "age: " << _age << endl;
		cout << "student id: " << _stuId << endl;
	}
protected:
	int _stuId = 1314;
};

int main()
{
	Student s;
	s.ShowPlus();
	return 0;
}

在这里插入图片描述
总结:

  1. 基类private成员在派生类中无论什么方式继承都是不可见的。这里不可见是指:基类的私有成员虽然被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
  2. 基类private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的
  3. 从上面总结中可以看出:基类私有成员在派生类中都是不可见的。基类成员在派生类的访问方式 == Min(父类访问限定符和继承方式中取最小的),public > protected > private
  4. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
  5. 使用关键字class时默认的继承方式是private,使用struct的默认继承方式是public,建议显示写出继承的方式

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

  • 派生类对象可以赋值给 基类的对象 /基类的指针 /基类的引用,这里有个形象的说法叫做切片或切割。意思是:把派生类中从基类继承的那部分切来赋值过去
  • 基类对象不能赋值给派生类对象,因为一个基类对象可能是派生类对象的一部分,也可能不是
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针指向派生类对象时才是安全的。这里的基类如果是多态类型,可以使用RTT的dynamic_cast来进行识别后进行安全转换
class Person
{
protected:
	string _name;  // 名字
	string _sex;   // 性别
	size_t _age;   // 年龄
};

class Student : public Person
{
public:
	string _stuid; // 学号
};

void Test()
{
	Student s;
	// 1、派生类对象可以赋值给基类对象/基类指针/基类引用 -- 底层会进行隐式类型转换,将派生类的基类部分切给基类
	Person p = s;
	Person* ps = &s;
	Person& pss = s;

	// 2、基类不能赋值给派生类
	// S = p;

	// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
	Person* pp = &p;
	Student* pps1 = &s;
	pps1 = (Student*)pp; // 这种情况虽然可以,但是存在越界访问问题
	// pps1->_stuid = "abcdef"; // 引发异常,pps1只拿到了基类的部分,不能访问派生类的成员

	Student* pps2 = (Student*)ps; // 这种情况是可以的,ps指向一个派生类的对象
	pps2->_stuid = "123456";
}

int main()
{
	Test();
	return 0;
}

在这里插入图片描述


🌺3、继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域
  2. 基类和派生类中有同名成员,派生类将屏蔽基类对同名成员的直接访问,这种情况称为“隐藏”,也叫“重定义”(在基类成员函数中,可以使用 基类名::基类成员 来显示访问)
  3. 需要注意的是:如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意实际在继承体系里面最好不要定义同名的成员

举个栗子🌰1️⃣:

class Person
{
protected:
	string _name = "peter";
	int _ID = 1314;
};

// Student的_ID和Person的_ID构成隐藏关系,可以看出这样代码虽然能跑,但是容易混淆
class Student : Person
{
public:
	void Show()
	{
		cout << "名字: " << _name << endl;
		// 基类成员_ID被隐藏,需要通过指定类域来显示调用
		cout << "身份证号: " << Person::_ID << endl;
		cout << "学号: " << _ID << endl;
	}
protected:
	int _ID = 520;
};

void Test()
{
	Student s;
	s.Show();
}

int main()
{
	Test();
	return 0;
}

在这里插入图片描述

举个栗子🌰2️⃣:

// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏
class A
{
public:
	void fun()
	{
		cout << "fun()" << endl;
	}
};

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

void Test()
{
	B b;
	b.A::fun(); // A类的成员函数被隐藏,显示调用
	b.fun(1); // 通过传参数调用
}

int main()
{
	Test();
	return 0;
}

在这里插入图片描述


🌺4、派生类的默认成员函数

6个默认成员函数,"默认"的意思是指我们不写,编译器会帮我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

  1. 派生类的构造函数是通过调用基类的构造函数来初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表中显示调用初始化
  2. 派生类的拷贝构造函数必须通过调用基类的拷贝构造函数来完成基类的拷贝初始化
  3. 派生类的operator=必须通过显示调用基类的operator=来完成基类的复制
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序(栈:先进后出)
  5. 派生类对象初始化先调用基类构造再调用派生类构造
  6. 派生类对象析构清理先调用派生类析构再调用基类的析构函数进行清理

在这里插入图片描述在这里插入图片描述
栗子🌰:

class Person
{
public:
	Person(const char* name = "Peter")
		: _name(name)
	{
		cout << "Person()" << 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 ID)
		: Person(name)
		, _ID(ID)
	{
		cout << "Student(const char* name, int ID)" << endl;
	}

	// 调用父类拷贝构造函数初始化子类中父类的数据 -- person(this, s)
	Student(const Student& s)
		: Person(s)
		, _ID(s._ID)
	{
		cout << "Student(const Student& s)" << endl;
		_name = s._name;
	}

	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		// 通过调用基类赋值运算符重载来初始化派生类中基类的成员
		if (this != &s)
		{
			// Person::operator=(this, s) -- *this = s
			Person::operator=(s);
			_ID = s._ID;
		}
		return *this;
	}

	~Student() 
	{
		// 显示调用父类析构时,会多释放一次,因为子类释放顺序默认是"先子后父"
		// 显示调用会打乱原来的顺序(先父后子),编译器会等显示调用完后再按默认的顺序进行释放
		//Person::~Person(); // 最好不要显示调用
		cout << "~Student()" << endl;
	 }
protected:
	int _ID; // 学号
};

int main()
{
	{
		Student s1("abc", 123);
		Student se(s1);
		s1 = se;
	}
	return 0;
}

在这里插入图片描述


🍀5、继承与友元

  • 友元函数不属于类,它是一个函数,但是可以间接访问类的私有和保护成员
  • 友元关系不能继承,也就是说基类友元不能访问派生类私有和保护成员
  • 友元不具备传递性,假如:A是B的朋友,B是C的朋友,但A和C不是朋友

举个例子🌰1️⃣:友元函数

class Student; // 前置声明,否则下面使用时不可见
class Person
{
public:
	friend void Display(const Person& p, const Student& s); // 友元函数
public:
	string _name = "peter"; // 姓名
};

class Student : public Person
{
protected:
	string _stuid = "1314520"; // 学号
};

// 友元函数不属于类,不能被继承,基类友元不能访问派生类私有和保护成员
inline void Display(const Person& p, const Student& s)
{
	cout << "名字: " << p._name << endl;
	// 不能访问派生类中私有和保护成员
	// cout << "学号: " << s._stuid << endl;
}

void Test()
{
	Person p;
	Student s;
	Display(p, s);
}

int main()
{
	Test();
	return 0;
}

举个例子🌰2️⃣:友元类

class Person
{
	// Student是Person的朋友,Student可以访问Person的私有和保护成员
	friend class Student;
protected:
	string _name = "peter";
private:
	string _jobid = "123";
};

class Student
{
public:
	void Show(const Person& p) const
	{
		cout << p._name << endl;
		cout << p._jobid << endl;
	}
private:
	string _stuid = "456";
};


class Teachaer : public Student
{
public:
	void ShowPlus(const Person& p) const
	{
		// Student是Person的朋友,但Teacher不是Person的朋友,不能访问
		// 友元不具备传递性
		// cout << p._name << endl;
		// cout << p._jobid << endl;
	}
};

void Test()
{
	Person p;
	Student s;
	s.Show(p);
}

🍁6、继承与静态成员

  • 基类定义了static静态成员,则在整个继承体系中只有一个这样的成员。无论派生出多少个派生类,都只有一个static成员实例
  • 静态成员受访问限定符的控制,如果它在基类中是私有成员,则派生类无权访问它
class Person
{
public:
	// 静态成员作用域在类中,生命周期为全局,声明为公有成员,在类外可以通过指定类域进行访问
	static int _count; // 统计人数
	Person() { ++_count; }
protected:
	string _name;
};
int Person::_count = 0;

class Student : public Person
{
protected:
	string _stuid;
};

class Graduate : public Student
{
protected:
	string _seminarCourse; //研究科目
};

void Test()
{
	Graduate gdt1;
	Graduate gdt2;
	Graduate gdt3;
	// 通过指定类域进行访问
	cout << "研究科目人数: " << Person::_count << endl;
}

🍂7、复杂的菱形继承及菱形虚拟继承

继承分为:

  • 单继承:一个派生类只有一个直接基类时,称这个继承关系为单继承
    在这里插入图片描述

  • 多继承:一个派生类有两个或以上直接派生类时,称这个继承关系为多继承
    在这里插入图片描述

  • 菱形继承:多重继承中的一种特殊情况
    在这里插入图片描述


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

在这里插入图片描述

// 菱形继承
class Person
{
public:
	string _name;
};

class Student : public Person
{
protected:
	int _num;
};

class Teacher : public Person
{
protected:
	int _id;
};

// Assistant继承Student 和 Teacher
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse;
};

void Test()
{
	Assistant ast;
	// 报错,ast中有两份_name,二义性
	ast._name = "peter";
	
	// 需要进行显示指定基类域进行初始化
	ast.Student::_name = "peter";
	ast.Teacher::_name = "Aim";
}

解决方案:

  1. 虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系所示,在Student 和 Teacher继承Person时使用虚拟(virtual)继承,即可解决问题
  2. 需要注意的是,虚拟继承不要在其他地方使用`
class A
{
public:
	int _a;
};

class B : public A
{
public:
	int _b;
};

class C : 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;
}

在这里插入图片描述


  • 虚拟继承解决数据数据冗余和二义性原理
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._a = 0;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
}

下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?

  • 这里是通过了B和C的两个指针,指向的一张表这两个指针叫虚基表指针,这两个表叫虚基表,虚基表中存的偏移量。通过偏移量可以找到下面的A
    在这里插入图片描述

🍃 8、继承的总结和反思

  1. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java
  2. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱继承。否则在复杂度及性能上都有问题

  • 继承和组合
  1. public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象
  2. 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象
  3. 优先使用对象组合,而不是类继承
  4. 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高
  5. 象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装
  6. 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合
// Car和BMW Car和Benz构成is-a的关系
class Car {
protected:
	string _colour = "白色";		// 颜色
	string _num = "陕ABIT00";	// 车牌号
};
class BMW : public Car {
public:
	void Drive() { cout << "好开-操控" << endl; }
};

class Benz : public Car {
public:
	void Drive() { cout << "好坐-舒适" << endl; }
};

// Tire和Car构成has-a的关系
class Tire {
protected:
	string _brand = "Michelin"; // 品牌
	size_t _size = 17;			// 尺寸
};

class Car {
protected:
	string _colour = "白色";		// 颜色
	string _num = "陕ABIT00";	// 车牌号
	Tire _t;					// 轮胎
};

好了,全部知识已经写完了,感谢大家支持!!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值