【C++】C++继承入门(下):友元、静态成员与菱形继承的底层逻辑

📌 相关专栏

很高兴你点开这篇文章✨

这里会持续更新更多有用的内容,关注我,一起慢慢变好呀

👍 点赞 ⭐ 收藏 💬 评论


文章目录

  • 前言
  • [1. 友元与继承:友元关系不具有传递性](#1. 友元与继承:友元关系不具有传递性)
    • [1.1 理解](#1.1 理解)
    • [1.2 代码示例](#1.2 代码示例)
  • [2. 静态成员在继承体系中的共享性](#2. 静态成员在继承体系中的共享性)
    • [2.1 核心结论](#2.1 核心结论)
    • [2.2 静态成员总结](#2.2 静态成员总结)
  • [3. 单继承与多继承](#3. 单继承与多继承)
    • [3.1 单继承](#3.1 单继承)
    • [3.2 多继承](#3.2 多继承)
  • [4. 菱形继承的问题:数据冗余与二义性](#4. 菱形继承的问题:数据冗余与二义性)
    • [4.1 什么是菱形继承?](#4.1 什么是菱形继承?)
    • [4.2 代码示例](#4.2 代码示例)
    • [4.3 数据冗余](#4.3 数据冗余)
    • [4.4 二义性](#4.4 二义性)
  • [5. 虚继承:彻底解决菱形继承问题](#5. 虚继承:彻底解决菱形继承问题)
    • [5.1 虚继承的语法](#5.1 虚继承的语法)
    • [5.2 虚继承后的内存布局](#5.2 虚继承后的内存布局)
    • [5.3 虚继承的核心规则](#5.3 虚继承的核心规则)
    • [5.4 构造顺序验证](#5.4 构造顺序验证)
    • 虚继承下的构造顺序:
  • [6. 完整测试用例](#6. 完整测试用例)
    • [6.1 友元测试](#6.1 友元测试)
    • [6.2 静态成员测试](#6.2 静态成员测试)
    • [6.3 菱形继承问题测试](#6.3 菱形继承问题测试)
    • [6.4 虚继承测试](#6.4 虚继承测试)
  • [7. 总结](#7. 总结)
  • 本文全部代码
    • [🐾 test.cpp](#🐾 test.cpp)

前言

在掌握了继承的基础语法后,我们需要深入理解继承体系中的一些进阶特性:

  • 友元关系是否会被继承?
  • 静态成员在派生类中如何共享?
  • 多继承会带来什么问题?
  • 菱形继承的数据冗余和二义性如何解决?

🐶 🐾 ✨ 🐾 🐶


1. 友元与继承:友元关系不具有传递性

1.1 理解

基类的友元函数/类无法直接访问派生类的私有/保护成员。

  • 可以类比为:你朋友的朋友不等同于你的朋友
  • 如果需要友元访问派生类成员,就必须在派生类中重新说明友元

1.2 代码示例

前置声明Student(为了让编译器走到友元函数时能向上查找到 Student):提前告诉编译器Student是一个类

cpp 复制代码
// 前置声明:告诉编译器 Student 是一个类
class Student;

// 基类 Person
class Person
{
public:
    // 声明 Display 是友元函数
    friend void Display(const Person& p, const Student& s);
protected:
    string _name = "千余";
};

// 派生类 Student
class Student : public Person
{
    //  必须再次声明友元,否则 Display 无法访问 Student 的保护成员
    friend void Display(const Person& p, const Student& s);
protected:
    int _stuid = 888;			//学号
};

// 友元函数实现
void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;    //  Person 声明了友元,可访问
    cout << s._stuid << endl;   //  Student 也声明了友元,才能访问
}

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

结论 :友元关系不具有继承性。如果需要友元访问派生类成员,必须在派生类中重新声明友元。

🐶 🐾 ✨ 🐾 🐶


2. 静态成员在继承体系中的共享性

2.1 核心结论

基类的静态成员在整个继承体系中仅存在一份,派生类和基类共享该成员,不会因为继承而产生多个副本。

  • 也就是说对于基类的静态成员而言,对其进行初始化则说所有派生类的该成员都会被初始化成相同值,并且一个类的静态成员进行修改也会影响其他所有相关的派生类和基类
cpp 复制代码
class Person
{
public:
    string _name;           // 非静态:每个对象一份
    static int _count;      // 静态:整个类共用一份
};

// 静态成员必须在类外初始化
int Person::_count = 1;

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

void Test2()
{
    Person p;
    Student s;

    // 非静态成员:每个对象独立
    cout << &p._name << endl;   // 地址1
    cout << &s._name << endl;   // 地址2(不同)

    // 静态成员:全家族共用一份
    cout << &p._count << endl;   // 地址A
    cout << &s._count << endl;   // 地址A(相同)
    cout << &Person::_count << endl;   // 地址A
    cout << &Student::_count << endl;  // 地址A

    // 修改一个,全部改变
    Person::_count++;
    cout << p._count << endl;     // 2
    cout << s._count << endl;     // 2
    cout << Student::_count << endl; // 2
}
  • 公有的情况下,基类派生类指定类域都可以访问静态成员,静态成员不属于任何一个对象,是存在静态区的,可以把静态成员看成被类域限制的全局变量,所以要访问就需要使用域作用限定符::来突破类域访问

2.2 静态成员总结

  • 存储位置:在静态区,不属于任何对象
  • 初始化: 必须在类外进行
  • 访问方式 :可通过对象、类名(类名::)访问
  • 继承特性: 所有派生类共享同一份静态成员
  • 静态函数 :只能访问静态成员,不能访问非静态成员

🐶 🐾 ✨ 🐾 🐶


3. 单继承与多继承

3.1 单继承

单继承 :一个派生类只有⼀个直接基类的关系

🐾单继承模型


3.2 多继承

多继承 :一个派生类有两个或以上直接基类的继承关系

cpp 复制代码
class Student { /* ... */ };
class Teacher { /* ... */ };

class Assistant : public Student, public Teacher
{
    // 继承了 Student 和 Teacher 的所有成员
};

🐾多继承模型

🐶 🐾 ✨ 🐾 🐶


4. 菱形继承的问题:数据冗余与二义性

4.1 什么是菱形继承?

菱形继承是指一个派生类同时继承两个基类,而这两个基类又共同继承自一个顶层基类的结构 :

🐾型:


4.2 代码示例

cpp 复制代码
lass Person 
{
public:
	string _name; //会被Assistant继承两次
};

// 中间基类1
class Student : public Person 
{
protected:
	int _num; //学号
};

// 中间基类2
class Teacher : public Person 
{
protected:
	int _id; //职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; //主修课程 
};

4.3 数据冗余

Assistant 对象中包含两份 Person 的 _name

4.4 二义性

cpp 复制代码
查看二义性:

1.在"Assistant a;"处打断点,按F5开始调试,调试->窗口->监视;

2.点开a前面的小箭头:
					+a    Assistant
						+Student      // 第一个基类部分
							+Person   // 里面有一个 _name
							_num
						+Teacher      // 第二个基类部分
							+Person   // 里面又有一个 _name
							_id
						_majorCourse

3.再分别展开Student和Teacher:
					展开  Student  → 里面有个  Person  → 有一个  _name
					展开  Teacher  → 里面也有个  Person  → 有另一个  _name
cpp 复制代码
void Test4()
{
    Assistant a;
    
    //  错误:_name 访问不明确
    // a._name = "张三";
    
    //  二义性:两个都叫_name,编译器不知道要的是哪个
    //			必须显式指定访问哪个基类的 _name
    a.Student::_name = "李四";
    a.Teacher::_name = "王五";
}

结论 :菱形继承会导致数据冗余(两份 Person)和访问二义性(不知道用哪个 _name)。

🐶 🐾 ✨ 🐾 🐶


5. 虚继承:彻底解决菱形继承问题

如果没有 virtual 虚继承, Assistant 继承 Student 和 Teacher ,而两者又都继承 Person ,会导致 :

  1. Person 子对象在 Assistant 里存在两份,造成数据冗余;
  2. 访问 a._name 时会产生二义性,编译器不知道该用哪一 份 Person 的 _name

🐾 因此,虚继承的作用是 :让顶层基类只存在一份,不再重复


5.1 虚继承的语法

在中间基类的继承方式前添加 virtual 关键字 :

cpp 复制代码
//顶层基类
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
	}
public:
	string _name; // 姓名
};

// 中间基类1:虚继承Person(添加virtual)
//virtual,谁(Person)被继承多次就在继承谁(Person)的那些子类(Student)加
class Student : virtual public Person
{
public:
	Student(const char* name, int num)
		:Person(name)		// 在虚继承下,中间基类会暂时不初始化顶层基类,所以这行会被跳过
		, _num(num)			// 只会初始化自己的成员变量
	{
	}
protected:
	int _num; //学号
};

// 中间基2:虚继承Person(添加virtual)
//virtual,谁(Person)被继承多次就在继承谁(Person)的那些子类(Teacher)加
class Teacher : virtual public Person
{
public:
	Teacher(const char* name, int id)
		:Person(name)		// 在虚继承下,中间基类会暂时不初始化顶层基类,所以这行会被跳过
		, _id(id)			// 只会初始化自己的成员变量
	{
	}
protected:
	int _id; // 职工编号
};

// 最终派生类:菱形继承(Person成员会被合并成仅一份)
class Assistant : public Student, public Teacher
{
public:

	// 关键:虚继承下,顶层基类的构造由最终派生类显式调用
	Assistant(const char* name1, const char* name2, const char* name3)
		:Person(name1)			//只有这个会执行,下面两个初始化不会进去
		, Student(name2, 1)
		, Teacher(name3, 2)
		, _majorCourse("计算机")
	{
	}

protected:
	string _majorCourse; // 主修课程
};


5.2 虚继承后的内存布局

没有虚继承时 :构造的顺序是 Person->Student->Person->Teacher->Assistant,会造成两次Person


5.3 虚继承的核心规则

  • 中间类 Student 、 Teacher 里的 Person(name) 会被编译器直接忽略、不执行
  • 只有最终派生类 Assistant 才能初始化 Person

5.4 构造顺序验证

cpp 复制代码
void Test5()
{
    // 虽然有三次 Person(name) 调用
    // 但实际只执行 Assistant 中的 Person(name1)
    // 其他两次会被编译器忽略
    Assistant a("张三", "李四", "王五");
    
    // a 的 _name = "张三"
}

🐾 验证

虚继承下的构造顺序:

  1. 先构造顶层基类 Person(由最终派生类调用)
  2. 再按声明顺序构造中间基类 Student、Teacher
  3. 最后构造最终派生类 Assistant

🐶 🐾 ✨ 🐾 🐶


6. 完整测试用例

6.1 友元测试

cpp 复制代码
void Test1()
{
    Person p;
    Student s;
    Display(p, s);  // 输出:千余\n888
}

6.2 静态成员测试

cpp 复制代码
void Test2()
{
	Person p;
	Student s;

	//非静态成员:每个对象独立
	//输出两个"不同"的地址,p和s是两个不同对象,各自有各自的_name
	cout << &p._name << endl;
	cout << &s._name << endl;
	cout << endl;

	//静态成员:全家族共用一份
	//输出"同一个"地址,静态成员在静态区,不属于任何对象,Person+所有派生类Student共用这一个_count
	cout << &p._count << endl;
	cout << &s._count << endl;
	cout << endl;

	//修改一个,全部变
	cout << p._count << endl;		// 1
	cout << s._count << endl;		// 1
	cout << endl;
	Person::_count++;
	
	// 公有的情况下,基类派生类指定类域都可以访问静态成员
	// 静态成员不属于任何一个对象,是存在静态区的
	// 可以把静态成员看成被类域限制的全局变量,所以要访问就需要使用域作用限定符::来突破类域访问
	cout << Person::_count << endl;		// 2
	cout << Student::_count << endl;	// 2
	cout << endl;
}

6.3 菱形继承问题测试

cpp 复制代码
void Test4()
{
    Assistant a;
    a.Student::_name = "李四";
    a.Teacher::_name = "王五";
    
    cout << a.Student::_name << endl;  // 李四
    cout << a.Teacher::_name << endl;  // 王五(两个不同的 _name)
}

6.4 虚继承测试

cpp 复制代码
void Test5()
{
    Assistant a("张三", "李四", "王五");
    // a 中只有一份 _name,值为 "张三"
}

🐶 🐾 ✨ 🐾 🐶


7. 总结

C++ 继承的进阶特性:

  • 友元与继承: 友元关系不具有继承性,需在派生类中重新声明
  • 静态成员 :整个继承体系共享一份静态成员
  • 单继承: 一个派生类只有一个直接基类
  • 多继承: 一个派生类有多个直接基类
  • 菱形继承问题: 数据冗余 + 二义性
  • 虚继承 :通过 virtual 解决菱形继承问题

菱形继承解决方案对比

方案 数据冗余是否存在 二义性的问题 使用便利性
普通多继承 存在 存在 需显式指定类域
指定类域访问 存在 解决 麻烦,仍有冗余
虚继承 解决 解决 便捷,自动共享

谁被继承多次,谁就在那些子类加 virtual

🐾 Person 被 Student 和 Teacher 继承

  • class Student : virtual public Person
  • class Teacher : virtual public Person

🐶 🐾 ✨ 🐾 🐶


本文全部代码

🐾 test.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
using namespace std;
//每使用一段代码都要把其他的代码注释掉



//1.友元------友元不能被继承
/*		基类的友元函数/类无法直接访问派生类的私有成员,可以类比成你的朋友的朋友不等同于是你的朋友,
因此友元关系不具有继承性,如果需要友元访问派生类成员,就必须在派生类中重新说明友元*/

//前置声明Student(为了让编译器走到友元函数时能向上查找到 Student):提前告诉编译器Student是一个类
class Student;

//基类---Person定义
class Person
{
public:

	//声明Display是友元函数,使其能访问Person的保护成员_name
	friend void Display(const Person& p, const Student& s);	

protected:
	string _name = "千余"; //姓名
};

//派生类Student的定义---继承Person的成员,但基类的友元不会自动生成派生类的友元
class Student : public Person
{
	//如果这里不再声明一次友元,Display就无法访问Student的保护成员
	friend void Display(const Person& p, const Student& s);

protected:
	int _stuid = 888; //学号
};

//友元函数的实现
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;	//正常访问,因为Person声明了Display为友元
	cout << s._stuid << endl;	//只有Student也声明友元,才能正常使用

	//验证了:基类的友元不能直接访问派生类的私有/保护成员,必须要在派生类中单独声明
}

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

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

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/*  2.静态成员在继承体系中具有共享性:
	基类的静态成员(静态变量/静态函数)在整个继承体系中仅存在一份,派生类和基类共享该成员,不会因为继承而产生多个。
这与非静态成员不同 ------ 非静态成员每个对象独一份。
	也就是说对于基类的静态成员而言,对其进行初始化则说所有派生类的该成员都会被初始化成相同值,
并且一个类的静态成员进行修改也会影响其他所有相关的派生类和基类。*/

class Student;
//静态成员
class Person
{
public:
	string _name;		//非静态:每个对象一份

	static int _count;	//静态:整个类共用一份
};

//静态成员必须在类外初始化
int Person::_count = 1;

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

void Test2()
{
	Person p;
	Student s;

	//非静态成员:每个对象独立
	//输出两个不同的地址,p和s是两个不同对象,各自有各自的_name
	cout << &p._name << endl;
	cout << &s._name << endl;
	cout << endl;

	//静态成员:全家族共用一份
	//输出同一个地址,静态成员在静态区,不属于任何对象,Person+所有派生类Student共用这一个_count
	cout << &p._count << endl;
	cout << &s._count << endl;
	cout << endl;

	//修改一个,全部变
	cout << p._count << endl;		// 1
	cout << s._count << endl;		// 1
	cout << endl;
	Person::_count++;
	
	// 公有的情况下,基类派生类指定类域都可以访问静态成员
	// 静态成员不属于任何一个对象,是存在静态区的
	// 可以把静态成员看成被类域限制的全局变量,所以要访问就需要使用域作用限定符::来突破类域访问
	cout << Person::_count << endl;		// 2
	cout << Student::_count << endl;	// 2
	cout << endl;
}
int main()
{
	Test2();
	return 0;
}

/*结论:
		静态成员变量必须在类外初始化,否则会触发链接错误;

		静态成员函数只能访问静态成员变量,无法访问非静态成员;

		公有的情况下,基类派生类指定类域也可以访问静态成员;

		继承体系中所有类(基类,派生类)共享同一份静态成员,修改一处会影响全局。

*/


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
单继承与多继承
		单继承:一个派生类只有⼀个直接基类时称这个继承关系为单继承
		多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承


	模型
		单继承:					class Person
										|
							class Student:publilc Person
										|
						  class PostGraduate:public Student


		多继承:		class Student				class Teacher
								\						/
						class AssistanT:public Student,public Teacher
*/



//多继承
class Student
{
protected:
	string _name; //姓名
};

class Teacher
{
protected:
	int _id = 123; //职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; //主修课程 
};

void Test3()
{
	Assistant a;

}

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

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/*
菱形继承:虚继承解决"数据冗余"与"二义性"
菱形继承是指"一个派生类同时继承两个基类,而这两个基类又共同继承自一个顶层基类"的结构(并非一定是个菱形结构的图)

模型:
										class Person
										/		 \
				class Student:public Person		class Teacher:public Person
										\		  /
						class Assistant:public Student,public Teacher							
										
									
*/


// 顶层基类
 
class Person 
{
public:
	string _name; //会被Assistant继承两次
};

// 中间基类1
class Student : public Person 
{
protected:
	int _num; //学号
};

// 中间基类2
class Teacher : public Person 
{
protected:
	int _id; //职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; //主修课程 
};

/*
查看二义性:

1.在"Assistant a;"处打断点,按F5开始调试,调试->窗口->监视;

2.点开a前面的小箭头:
					+a    Assistant
						+Student      // 第一个基类部分
							+Person   // 里面有一个 _name
							_num
						+Teacher      // 第二个基类部分
							+Person   // 里面又有一个 _name
							_id
						_majorCourse

3.再分别展开Student和Teacher:
					展开  Student  → 里面有个  Person  → 有一个  _name
					展开  Teacher  → 里面也有个  Person  → 有另一个  _name
*/

void Test4()
{
	Assistant a;
	//a._name = "张三"; //error C2385: 对"_name"的访问不明确(到底是Student::_name还是Teacher::_name呢?)

	//二义性:两个都叫_name,编译器不知道要的是哪个
	a.Student::_name = "李四";
	a.Teacher::_name = "王五";

	// 只能显式指定,但数据冗余仍存在,没有解决
	cout << a.Student::_name << endl;  // 输出李四
	cout << a.Teacher::_name << endl;  // 输出王五
}
int main()
{
	Test4();
	return 0;
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////

//虚继承----彻底解决菱形继承问题---让顶层基类只存在一份,不再重复

/*
如果没有  virtual  虚继承, Assistant  继承  Student  和  Teacher ,而两者又都继承  Person ,会导致:
 
1.  Person  子对象在  Assistant  里存在两份,造成数据冗余;
​
2. 访问  a._name  时会产生二义性,编译器不知道该用哪一份  Person  的  _name

*/


//没有虚继承时:构造的顺序是 Person->Student->Person->Teacher->Assistant,会造成两次Person


/*
虚继承规则: - 中间类  Student 、 Teacher  里的  Person(name)  会被编译器直接忽略、不执行
​			 - 只有最终派生类  Assistant  才能初始化  Person
*/

//顶层基类
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
	}
public:
	string _name; // 姓名
};

// 中间基类1:虚继承Person(添加virtual)
//virtual,谁(Person)被继承多次就在继承谁(Person)的那些子类(Student)加
class Student : virtual public Person
{
public:
	Student(const char* name, int num)
		:Person(name)		// 在虚继承下,中间基类会暂时不初始化顶层基类,所以这行会被跳过
		, _num(num)			// 只会初始化自己的成员变量
	{
	}
protected:
	int _num; //学号
};

// 中间基2:虚继承Person(添加virtual)
//virtual,谁(Person)被继承多次就在继承谁(Person)的那些子类(Teacher)加
class Teacher : virtual public Person
{
public:
	Teacher(const char* name, int id)
		:Person(name)		// 在虚继承下,中间基类会暂时不初始化顶层基类,所以这行会被跳过
		, _id(id)			// 只会初始化自己的成员变量
	{
	}
protected:
	int _id; // 职工编号
};

// 最终派生类:菱形继承(Person成员会被合并成仅一份)
class Assistant : public Student, public Teacher
{
public:

	// 关键:虚继承下,顶层基类的构造由最终派生类显式调用
	Assistant(const char* name1, const char* name2, const char* name3)
		:Person(name1)			//只有这个会执行,下面两个初始化不会进去
		, Student(name2, 1)
		, Teacher(name3, 2)
		, _majorCourse("计算机")
	{
	}

protected:
	string _majorCourse; // 主修课程
};

/*
验证:
1.断点打在"Assistant a("张三", "李四", "王五");"

2.按F5->窗口->监视->展开a:
				+a  Assistant
				   +Person        // 只有一份!
					  _name:张三
				   +Student
					  _num:1
				   +Teacher
					  _id:2
				   _majorCourse:计算机


*/
void Test5()
{
	Assistant a("张三", "李四", "王五");
	
	//上面有三次Person(name),但其实就只有在Assistant里一次,其它两次会跳过。
	//所以是张三
}

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

🐶 🐾 ✨ 🐾 🐶


  1. 欢迎留言交流
  2. 期待你的评论与建议
  3. 留下你的想法吧

谢谢你看到这里呀

如果喜欢这篇内容,点个关注,下次更新不迷路✨

👍 点赞 ⭐ 收藏 💬 评论

相关推荐
小短腿的代码世界1 小时前
行情快照与增量更新引擎:Qt在高频交易数据分发中的核心架构——你的行情推送为什么延迟了500ms?
开发语言·qt·架构
初中就开始混世的大魔王1 小时前
6 Fast DDS-传输层
开发语言·c++·中间件·信息与通信
啊森要自信2 小时前
【GUI自动化测试】控件、鼠标键盘操作与多场景自动化
c语言·开发语言·python·adb·ipython
YJlio2 小时前
《Sysinternals实战指南》16.5 Ctrl2Cap 工具详解:把 Caps Lock 变成 Ctrl 的键盘改造与回退方法
linux·运维·服务器·网络·python·学习·计算机外设
花北城2 小时前
【C#】ABP框架服务端开发
开发语言·c#·abp
电商API_180079052472 小时前
Python 实现闲鱼商品列表批量采集,接口异常重试机制搭建
大数据·开发语言·数据库·爬虫·python
DogDaoDao2 小时前
深入理解 Qt:从原理到实战的全景指南
开发语言·qt·程序员
放下华子我只抽RuiKe52 小时前
FastAPI 全栈后端(四):认证与授权
开发语言·前端·javascript·python·深度学习·react.js·fastapi
我是唐青枫2 小时前
Java Spring Data JPA 实战指南:Repository 查询、分页与实体映射
java·开发语言