C++继承机制:面向对象编程的基石

目录

[1 继承的概念和定义](#1 继承的概念和定义)

[1.1 什么是继承](#1.1 什么是继承)

[1.2 继承定义](#1.2 继承定义)

[1.2.1 定义格式](#1.2.1 定义格式)

[1.2.2 继承基类成员访问方式的变化](#1.2.2 继承基类成员访问方式的变化)

[1.3 继承类模板](#1.3 继承类模板)

[2 基类和派生类间的转换](#2 基类和派生类间的转换)

[3 继承中的作用域](#3 继承中的作用域)

[3.1 隐藏规则](#3.1 隐藏规则)

[3.2 两道继承作用域的选择题](#3.2 两道继承作用域的选择题)

[4 派生类的默认成员函数](#4 派生类的默认成员函数)

[5 实现一个不能被继承的类](#5 实现一个不能被继承的类)

[6 继承与友元](#6 继承与友元)

[7 继承与静态成员](#7 继承与静态成员)

[8 多继承与菱形继承问题](#8 多继承与菱形继承问题)

[8.1 继承模型](#8.1 继承模型)

[8.2 虚继承](#8.2 虚继承)

[9 继承与组合](#9 继承与组合)


1 继承的概念和定义

1.1 什么是继承

继承机制是在面向对象编程时使代码可以复用的重要手段 ,可以对原有的类进行扩展,可以通过增加新的成员函数或者成员变量,产生新的类,这个类叫做派生类,也可以叫做子类;原有的类叫做基类,也可以叫做父类。继承是对类设计层次的复用

下面通过一个例子引出继承的用法,我们分别设计两个类:学生(Student)、老师(Teacher),Student和Teacher类中都有姓名/地址/电话/年龄等成员变量,都有identity身份认证函数。当然这两个类中有一些不同的成员变量和函数,比如老师的独有成员变量是职称,学生的独有成员变量是学号;老师的独有成员函数是授课,学生的独有成员函数是学习:

cpp 复制代码
#include <iostream>
using namespace std;
class Student
{
public:
	//身份认证
	void identity()
	{
		//...
	}
	//学习
	void study()
	{
		//...
	}
protected:
	string _name = "peter"; // 姓名
	string _address;//地址
	string _tel;//电话
	int _age = 18;//年龄

	int _stuid;//学号
};
class Teacher
{
public:
	//身份认证
	void identity()
	{
		//...
	}
	//授课
	void teaching()
	{
		//...
	}
protected:
	string _name = "jack"; // 姓名
	string _address;//地址
	string _tel;//电话
	int _age = 28;//年龄
	int _stuid;//学号

	string _title;//职称
};

这两个类除了单独的成员变量和成员函数不同外,其余属性和方法都相同,这样就显得冗余,这时候我们可以将公共的成员放到Person类中,Student和Teacher类都继承Person,这样就可以复用这些成员,就不去重复定义了:

cpp 复制代码
class Person
{
public:
	//身份认证
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name = "jack"; // 姓名
		string _address;//地址
		string _tel;//电话
		int _age;//年龄
};
class Student :public Person
{
public:
	//学习
	void study()
	{
		//...
	}
protected:
	int _stuid;//学号
};
class Teacher :public Person
{
public:
	//授课
	void teaching()
	{
		//...
	}
protected:
	string title;//职称
};

int main()
{
	Student s;
	Teacher t;
	//都能调用到identity函数
	s.identity();
	t.identity();
	return 0;
}

1.2 继承定义

1.2.1 定义格式

上述中我们看到的Person是基类,也称作父类。Student是派生类,也称作子类,用一张图举例表明继承的定义:

继承方式有三种:public继承、protected继承、private继承,而基类中不同访问限定符(public,protected,private)的成员,在不同继承中有对应的规定,下面用一个表格说明:

1.2.2 继承基类成员访问方式的变化

|----------------|-----------------|-----------------|---------------|
| 类成员/继承方式 | public继承 | protected继承 | private继承 |
| 基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |

这个表格可以理解性的记忆。

1 基类中的private成员无论以什么方式都是不可见的,也就是说,私有成员虽然继承到了派生类中,但是在派生类内部和外部都不可以去访问基类的私有成员。

2 如果基类成员不想在类外被访问,在派生类中可以访问,就定义为protected成员。protected成员是因继承才被创造出来。

3 基类的其他成员在派生类的访问方式等于min{成员在基类的访问限定符,继承方式},其中public>protected>private。比如基类的protected成员使用public继承,在派生类就是protected成员;基类的public成员使用protected继承,在派生类就是protected成员,取二者的较小方。

4 使用关键字class时默认的继承方式是private,使用struct时默认继承方式是public,不过最好显示写出继承方式。

5 在实际使用中一般都是public继承,不提倡使用protected/private继承,原因是protected/private继承下来的成员都只能在派生类里面使用,扩展维护性不强。

1.3 继承类模板

一个类也可以继承库里面的类模板,如vector,list等,举例说明:

cpp 复制代码
namespace as
{
	template <class T>
	class stack :public std::vector<T>
	{
	public:
		void push(const T& x)
		{
			push_back(x);
		}
		void pop()
		{
			pop_back();
		}
		const T& top()
		{
			return back();
		}
		bool empty()
		{
			return empty();
		}
	};
}
int main()
{
	as::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	cout << endl;
	return 0;
}

可以通过继承vector模板来模拟实现stack的一些功能,但是这么写运行时会编译报错,原因在于调用函数时没有指定类域,在main函数中,stack<int>实例化时,vector<int>也实例化,因为模板是按需实例化,如果成员函数没有实例化,就会报错:

cpp 复制代码
namespace as
{
	template <class T>
	class stack :public std::vector<T>
	{
	public:
		void push(const T& x)
		{
			//需要指定类域
			vector<int>::push_back(x);
		}
		void pop()
		{
			vector<int>::pop_back();
		}
		const T& top()
		{
			return vector<int>::back();
		}
		bool empty()
		{
			return vector<int>::empty();
		}
	};
}
int main()
{
	as::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	while (!st.empty())
	{
		cout << st.top() << " ";//4 3 2 1
		st.pop();
	}
	cout << endl;
	return 0;
}

2 基类和派生类间的转换

public继承的派生类对象可以赋值给基类的指针或者基类的引用。这么做就是将派生类中基类的那部分切出来,基类的指针或引用指向的是派生类中切出来的基类那部分。

cpp 复制代码
class Person
{
protected:
	string _name;
	string _gender;
	int _age;
};

class Student :public Person
{
public:
	int _No;
};

int main()
{
	Student s;

	//派生类对象可以赋值给基类的指针或引用
	Person* pp = &s;
	Person& rp = s;

	//基类对象不能赋值给派生类对象
	Person p;
	s = p;//错误
	return 0;
}

基类对象不能赋值给派生类对象,派生类对象可以赋值给基类对象。

3 继承中的作用域

3.1 隐藏规则

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

派生类和基类中有同名成员,派生类成员将屏蔽基类同名成员的访问,这种情况叫隐藏,如果在派生类中想要访问,需要使用基类::基类成员的方式进行访问。

cpp 复制代码
class Person
{
protected:
	string _name = "peter";
	int _num = 111;
};
class Student :public Person
{
public:
	void Print()
	{
		cout << _name << endl;//打印peter
		cout << Person::_num << endl;//打印111
		cout << _num << endl;//打印999
	}
protected:
	int _num = 999;
};
int main()
{
	Student s;
	s.Print();
	return 0;
}

3.2 两道继承作用域的选择题

1 A和B类的两个func构成什么关系()

A 重载 B 隐藏 C 没关系

2 下面程序的编译运行结果是什么?

A 编译报错 B 运行报错 C 正常运行

class A

{

public:

void fun()

{

cout << "func()" << endl;

}

};

class B : public A

{

public:

void fun(int i)

{

cout << "func(int i)" << i << endl;

}

};

int main()

{

B b;

b.fun(10);

return 0;

};

答案:B C

4 派生类的默认成员函数

(1)派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表显示调用。

cpp 复制代码
class Person
{
public:
	Person(const char* name="xxx")
		:_name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name;
};
class Student :public Person
{
public:
	Student(const char* name,int num,const char* address)
		:Person(name)//编写方式 基类名(成员)
		,_num(num)
		,_address(address)
	{
		
	}
protected:
	int _num = 1;
	string _address = "北京市";
};
int main()
{
	Student s("zhangsan",1,"北京市");
	return 0;
}

(2)派生类的拷贝构造函数必须调用基类的拷贝构造函数完成对基类的拷贝初始化,一般来说调用基类的拷贝构造函数就够用了,如果派生类有深拷贝的资源,才需要在派生类内部实现拷贝构造

cpp 复制代码
class Person
{
public:
	Person(const char* name="xxx")
		:_name(name)
	{
		cout << "Person()" << endl;
	}

	//拷贝构造
	Person(const Person& p)
		:_name(p._name)
	{
		cout << "Person(const Person & p)" << endl;
	}
protected:
	string _name;
};
class Student :public Person
{
public:
	Student(const char* name,int num,const char* address)
		:Person(name)
		,_num(num)
		,_address(address)
	{}

	//可以不用写,有深拷贝资源再写
	Student(const Student& s)
		:Person(s)//派生类对象可以传给基类引用或指针
		,_num(s._num)
		,_address(s._address)
	{ }
protected:
	int _num = 1;
	string _address = "北京市";
};
int main()
{
	Student s("zhangsan",1,"北京市");
	Student s2(s);
	return 0;
}

(3)派生类的赋值重载必须要调用基类的operator=完成基类的复制,注意如果派生类写operator=,会隐藏基类的operator=,需要指定基类作用域显示调用基类的operator=

cpp 复制代码
class Person
{
public:
	Person(const char* name="xxx")
		:_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(const Person& p)" << endl;
		if (this != &p)
		{
			_name = p._name;
		}
		return *this;
	}
protected:
	string _name;
};
class Student :public Person
{
public:
	Student(const char* name,int num,const char* address)
		:Person(name)
		,_num(num)
		,_address(address)
	{}

	//可以不用写,有深拷贝资源再写
	Student(const Student& s)
		:Person(s)//派生类对象可以传给基类引用或指针
		,_num(s._num)
		,_address(s._address)
	{ }

	//可以不用写,有深拷贝资源再写
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			//基类和派生类operator=构成隐藏关系
			Person::operator=(s);//派生类对象可以传给基类的引用
			_num = s._num;
			_address = s._address;
		}
	}
protected:
	int _num = 1;
	string _address = "北京市";
};
int main()
{
	Student s("zhangsan",1,"北京市");
	//Student s2(s);
	Student s2 = s;
	return 0;
}

(4)派生类的析构函数会在被调用完成后自动调用基类析构函数清理基类成员。因为这样能保证派生类对象先清理,再清理基类成员的顺序,派生类和基类析构成隐藏关系,具体到后面的多态再详细说明。

(5)派生类对象初始化先调用基类构造再调用派生类构造

(6)派生类对象析构先调用派生类析构再调用基类析构

cpp 复制代码
class Person
{
public:
	Person(const char* name="xxx")
		:_name(name)
	{
		cout << "Person()" << endl;
	}

	//析构
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name;
};
class Student :public Person
{
public:
	Student(const char* name,int num,const char* address)
		:Person(name)
		,_num(num)
		,_address(address)
	{}


	~Student()
	{
		//有需要显示释放的资源,才需要自己实现
		//析构顺序,先子后父
		//子类的析构和父类的析构构成隐藏关系
	}
protected:
	int _num = 1;
	string _address = "北京市";
};
int main()
{
	Student s("zhangsan",1,"北京市");
	//Student s2(s);
	//Student s2 = s;
	return 0;
}

5 实现一个不能被继承的类

方法1:基类的构造函数私有,派生类的构成无法调用基类的构造函数,派生类无法实例化对象

方法2:C++11中新增了一个final关键字,final修改基类,派生类就不能继承了,这个相较于方法1常用。

cpp 复制代码
class Base final//这个类不能被继承
{
public:
	void func()
	{
		cout << "Base::func" << endl;
	}
protected:
	int a = 0;
};

6 继承与友元

友元关系不可以被继承,基类有友元声明,不可以访问派生类的私有和保护乘员。解决办法是在派生类中也进行友元声明:

cpp 复制代码
class Student;
class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name="zhangsan";
};

class Student :public Person
{
	friend void Display(const Person& p, const Student& s);//让Display也成为Student,因为友元不能被继承
protected:
	int _num=1;
};

void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._num << endl;
}
int main()
{
	Person p;
	Student s;
	Display(p, s);
	return 0;
}

7 继承与静态成员

如果基类定义了static静态成员,无论派生出多少个类,都只有这一个static成员实例。

cpp 复制代码
class Person
{
public:
	string _name;
	static int _count;
};

int Person::_count = 0;
class Student :public Person
{
	int _num;
};
int main()
{
	Person p;
	Student s;
	cout << &p._name << endl;
	cout << &s._name << endl;
	cout << endl;
	cout << &p._count << endl;
	cout << &s._count << endl;
	return 0;
}

运行结果:

可以看到非静态成员的地址是不一样的,说明派生类和基类对象各自有一份。静态成员的地址是一样的,说明派生类和基类共用同一份静态成员。

8 多继承与菱形继承问题

8.1 继承模型

单继承:一个派生类只有一个直接基类时称这个继承关系是单继承

多继承:一个派生类有两个或以上直接基类时,这个继承关系是多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员放到最后面。

cpp 复制代码
class A
{
public:
	string _name;
};
class B
{
protected:
	int _num;
};
class C :public A,public B//多继承
{
protected:
	int _id;
};

菱形继承:菱形继承是多继承的一种特殊情况,举例:

cpp 复制代码
class Person
{
public:
	string _name;
};
class Student :public Person
{
protected:
	int _num;
};
class Teacher :public Person
{
protected:
	int _id;
};
class Assistant :public Teacher, public Student//菱形继承
{
protected:
	string _majorCourse;
};

int main()
{
	Assistant a;
	a._name = "peter";//这里会报错:"对_name的访问不明确",因为Student和Teacher都有成员变量_name,产生二义性问题


	//需要显示指定访问哪个基类成员访问二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "peter";
	a.Teacher::_name = "peter";
	return 0;
}

我们写代码时不要设计出菱形继承的模型,在Assistant的对象中Person成员会有两份,造成数据冗余,支持多继承就一定会有菱形继承,所以使用多继承时要谨慎,避免写出菱形继承。

8.2 虚继承

在菱形继承中可以使用虚继承解决菱形继承带来的数据冗余和二义性问题

cpp 复制代码
class Person
{
public:
	string _name;
};

//使用虚继承Person类
class Student :virtual public Person
{
protected:
	int _num;
};
//使用虚继承Person类
class Teacher :virtual public Person
{
protected:
	int _id;
};
class Assistant :public Teacher, public Student//菱形继承
{
protected:
	string _majorCourse;
};

int main()
{
	Assistant a;
	//使用虚继承,解决数据冗余和二义性
	a._name = "peter";
	return 0;
}

即使使用菱形虚拟继承,无论是使用还是底层都会复杂很多,不建议设计出菱形继承。

9 继承与组合

  • public继承是一种is-a的关系。就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  • 基类的改变,对派生类有很大影响,派生类和基类依赖性强,耦合高,这种通过生成派生类的复用叫做白箱复用。
  • 对象组合是类继承之外的另一种复用选择,组合类没有很强的依赖关系,耦合度低。这种复用被称为黑箱复用。
  • 我们常说写出来的代码要"高内聚,低耦合",所以优先使用组合,而不是继承。继承也有它的用武之地,不过当类的关系既适合用继承(is-a)也适合用组合(has-a),就用组合。
cpp 复制代码
template<class T>
 class stack : public vector<T>//继承
 {};

 template<class T>//组合
 class stack
 {
 public:
 vector<T> _v;
 };
 int main()
 {
 return 0;
 }

以上就是有关继承的内容,如果这篇文章对你有用,可以点点赞哦,你的支持就是我写下去的动力,后续会不断的分享知识。

相关推荐
G_dou_3 小时前
Rust安装
开发语言·后端·rust
9ilk3 小时前
【仿RabbitMQ的发布订阅式消息队列】--- 模块设计与划分
c++·笔记·分布式·后端·中间件·rabbitmq
恒者走天下4 小时前
面试的时候项目怎么聊,才能发挥最大的价值
c++
小白黑科技测评4 小时前
2025 年编程工具实测:零基础学习平台适配性全面解析!
java·开发语言·python
ejinxian4 小时前
Python 3.14 发布
java·开发语言·python
喜欢读源码的小白4 小时前
【Spring Boot + Spring Security】从入门到源码精通:藏经阁权限设计与过滤器链深度解析
java·开发语言·spring boot·spring security
NEU-UUN4 小时前
C语言 . 第二章第二节 . 分支结构
c语言·开发语言
千里镜宵烛4 小时前
Lua-function的常见表现形式
开发语言·junit·lua
阿巴~阿巴~4 小时前
线程局部存储(Thread-Local Storage, TLS)
linux·服务器·开发语言·c++·线程·虚拟地址空间·线程局部存储