【C++】面向对象三大特性之一——继承

目录

前言

接着【C++】函数模板进阶、模板分离编译详情请点击,今天来介绍【C++】面向对象三大特性之一------继承

一、继承的概念及定义

1、继承的概念

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

  • 没有继承之前我们设计两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity身份认证的成员函数,设计到两个类里面就是冗余的。但是它们都有其独特的变量和成员函数
  • 我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复用这些成员,不需要重复定义了
cpp 复制代码
//单独设计两个类
class Student
{
public:
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
	// 学习 
	void study()
	{}
protected:
	string _name = "张三"; // 姓名 
	string _address; // 地址 
	string _tel; // 电话 
	int _age = 18; // 年龄 

	int _stuid;  //学号
};


class Teacher
{
public:
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
	// 授课 
	void teaching()
	{}
protected:
	string _name = "张三"; // 姓名 
	string _address; // 地址 
	string _tel; // 电话 
	int _age = 18; // 年龄 

	string title; // 职称 

};

//继承复用
class Person
{
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name = "张三"; // 姓名 
	string _address; // 地址 
	string _tel; // 电话 
	int _age = 18; // 年龄 
};

class Student : public Person
{
public:
	// 学习 
	void study()
	{}
protected:
	int _stuid;  //学号
};


class Teacher : public Person
{
public:
	// 授课 
	void teaching()
	{}
protected:
	string title; // 职称 

};

2、继承定义

定义格式

Person是基类,也称作父类。Student是派生类,也称作子类

  • 继承方式:public、protected、private继承
  • 继承格式:派生类 :继承方式 基类
cpp 复制代码
    // 派生类   继承方式  基类
class Student : public Person

继承基类成员访问方式

基类/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见
  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。基类的私有成员还是被继承到了派生类对象中 ,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它

  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。保护成员限定符是因继承才出现的;

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

  4. ⼀般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强

cpp 复制代码
class Person
{
public:
	void Print()
	{
		cout << _age  << endl;
		cout << _name << endl;
	}
private:
	int _age = 18; // 年龄 
protected:
	string _name = "张三"; // 姓名 
};

// 派生类   继承方式  基类
//class Student : public Person
class Student : protected Person
//class Student : private Person
{
public:
	void func()
	{
		//cout << _age << endl;
		cout << _name<< endl;
		Print();
	}
protected:
	int _stuid;  //学号
};

int main()
{
	Student st;
	st.Print();
	//st.func();
	return 0;
}

基类的私有成员在派生类中不可见,但是派生类还是可以间接拿到私有成员的

cpp 复制代码
class Person
{
public:
	void Print()
	{
		cout << _age << endl;
	}
private:
	int _age = 18; // 年龄 
protected:
	string _name = "张三"; // 姓名 
};

// 派生类   继承方式  基类
class Student : public Person
{
public:
	void func()
	{
		//cout << _age << endl; // 基类的私有成员派生类不可见
		cout << _name << endl;
		Print(); // 继承基类的Print函数,使用Print函数打印_age
	}
protected:
	int _stuid;  //学号
};

int main()
{
	Student st;
	st.func();
	st.Print(); // Print是公有继承,所以派生类可以在类外面访问
	return 0;
}

继承类模板

基类是类模板时,使用基类的成员时需要指定⼀下类域

  • 模板实例化是调用一个才会实例化一个,如果并没有调用是不会实例化出具体的类/函数的
  • 下面代码中,当在派生类类里面的函数push去调用push_back时,实例化出来push,push函数会去调用push_back,它并不明确是谁的push_back,它会先在派生类中找,发现并没有实例化出push_back函数,然后去基类中去找,但是基类中由于没有调用这个函数,因此并没有实例化这个函数,找不到
  • 但是如果派生类类外面直接调用:st.push_back(1),它会直接去实例化出这个函数,完成任务
cpp 复制代码
namespace gy
{
	template<class T>
	class stack : public std::vector<T>
	{
	public:
		void push(const T& x)
		{
			vector<T>::push_back(x);
			//push_back(x); //不能直接使用push_back
		}
		void pop()
		{
			vector<T>::pop_back();
		}
		const T& top()
		{
			return vector<T>::back();
		}
		bool empty()
		{
			return vector<T>::empty();
		}
	};
}

二、基类和派生类间的转换

  1. public继承的派生类对象可以赋值给基类的指针/基类的引用。这里实际上是赋值兼容,发生了切片,也叫做切割。可以把派生类对象理解为两部分,基类的那一部分和派生类自己的那一部分。切片或切割也就是将派生类对象中的基类的那一部分切割出来进行赋值
  2. 基类对象不能赋值给派生类对象
  3. 我们已知的当类型不相同的赋值可以进行强制类型转换或隐式类型转换,但是派生类赋值给基类是一个特例由于发生了赋值兼容转换,不会产生临时变量
cpp 复制代码
class Person
{
public:
private:
	int _age = 18; // 年龄 
protected:
	string _name = "张三"; // 姓名 
};

class Student : public Person
{
public:
protected:
	int _stuid = 123;  //学号
};

int main()
{
	Student st;
	Person* per = &st;
	Person& ref = st;
	return 0;
}

三、继承的作用域

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 当派生类和基类中有同名成员时,派生类中的同名成员将屏蔽基类中对同名成员的直接访问 ,直接对当前派生类的同名成员进行访问,这种情况叫做隐藏 ,也叫做重定义 (在派生类的成员函数中,可以使用 基类::同名成员 的形式直接访问基类的同名成员)
  3. 注意:如果是派生类和基类中的成员函数构成隐藏的话,只要函数名相同就构成隐藏
  4. 在实际的继承体系中,最好不要定义同名成员
cpp 复制代码
class Person
{
public:
	void func()
	{
		cout << "func()" << endl;
	}
protected:
	string _name = "张三"; // 姓名 
};

class Student : public Person
{
public:
	void func(int i)
	{
		cout << "func(int i)" << endl;
	}
protected:
	int _id = 1;  //学号
};

int main()
{
	Student st;
	st.func(10);
	st.func();
	return 0;
}

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

6个默认成员函数,当我们不写的时候,编译器会默认帮我们生成这六个成员函数,在派生类中,这几个成员函数是如何生成的呢?

  1. 派生类的构造函数必须调用基类的默认构造函数完成基类那一部分成员的初始化,如果基类没有默认的构造函数,那么在派生类的构造函数中必须显示调用基类的构造函数
cpp 复制代码
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:
protected:
	int _num = 1; //学号 
};

int main()
{
	Student s1;
}

当基类中没有默认构造函数,就需要在派生类中显示调用基类的构造函数

  • 注意:不能直接:_name(name),因为派生类需要把基类中的成员变量看成一个整体,而不是一个一个的单独的变量,使用:Person(name),就会去调用Person的构造函数
cpp 复制代码
class Person
{
public:
	Person(const char* name)
		: _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 num)
		//:_name(name)
		:Person(name)
		,_num(num)
	{ }
protected:
	int _num = 1; //学号 
};

int main()
{
	Student s1("Lucy", 17);
}
  1. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化(拷贝构造,对于自定义类型调用自己的拷贝构造,对于内置类型进行值拷贝),因此派生类s2拷贝构造s1时:Person类中的_name,调用Person的拷贝构造初始化,_num直接进行值拷贝

如果派生类中的成员变量需要深拷贝,那么基类的拷贝构造就需要我们自己来写

  1. 派生类的operator=必须要调用基类的operator=完成基类的复制。一般情况下派生类不需要写赋值重载函数,但是如果派生类中成员变量需要深拷贝时,需要注意的是派生类operator=隐藏了基类的operator=,所以显式调用基类的operator=,需要指定基类作用域
  2. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。(对于自定义类型调用自己的析构函数,对于内置类型调用默认的析构函数)

注意:

  • 派生类对象初始化先调用基类构造再调派生类构造(先父后子)
  • 派生类对象析构清理先调用派生类析构再调基类的析构(先子后父:为了保证先子后父,所以不需要显示调用)

1、实现一个不能被继承的类

  1. 基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象
  2. C++11新增了⼀个final关键字,final修改基类,表示这个类是最终类,不能被继承,class Person final

五、继承和友元

友元关系不能继承,也就是说基类友元不能访问派生类私有和保护成员

cpp 复制代码
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;
}
int main()
{
	Person p;
	Student s;
	Display(p, s);

	return 0;
}

如果想要能访问派生类中的成员变量,那么就让Display函数也成为派生类的友元即可

六、继承与静态成员

  1. 基类定义了static静态成员,则整个继承体系里面只有⼀个这样的成员。无论派生出多少个派生类,都只有⼀个static成员实例
  2. 但是派生类中父类的成员变量和父类的成员变量,它们虽然都叫那个名字(_id、_name等),但是他们不是同一个变量
  3. 但是静态成员变量,它们是同一个静态成员变量(如果在基类中定义了一个静态成员变量:static int _count

七、多继承及其菱形继承问题

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

多继承:一个子类有两个或以上直接父类时,称这种继承关系为多继承。多继承的使用方法是在子类的位置对多个父类使用逗号,进行间隔,其余方式public形式不变,进行继承

菱形继承:菱形继承是多继承的一种特殊情况,由于继承关系上类似菱形,所以就称这种特殊的多继承为菱形继承。但是会出现数据冗余和二义性

  • 比如Person类这中有一个成员变量_name,那么Student和Teather中也会继承到_name,当菱形继承到A时,A会有两个_name,那么A类访问的时候,就会不知道访问的是哪个_name(_name访问不明确),如果要解决这个问题就得指定访问的是哪个基类的成员
  • 虽然二义性问题用指定基类可以解决,但是数据冗余问题当前知识无法得到解决(虚继承解决数据冗余问题,后面介绍)

1、虚继承

虚继承的使用方式是在菱形继承中进行多继承的前父类的位置(一个子类有两个即以上的父类,这多个父类)使用virtual关键字修饰。实践中一般不用菱形继承

复制代码
class Person
{
public:
protected:
	string _name; // 姓名 
};
class Student : virtual public Person
{
public:
protected:
	int _stuNum; // 学号 
};

class Teather: virtual public Person
{
public:
protected:
	string title; // 职称
};

class A : public Student, public Teather
{
public:
	void Print()
	{
		cout << _name << endl;
	}
protected:
	string _num;
};

int main()
{
	A a;
	a.Print();
	return 0;
}

八、继承和组合

  1. 前面学的适配器就是经典的组合
  2. public继承是一种is-a的关系,也就是说每个派生类的对象都是一个基类对象(白箱复用)
  3. 组合是一种has-a的关系,也就是说有两个对象A和B,B组合了A,也就是说每个B对象中都有一个A对象(黑箱复用)
复制代码
class A
{};
class B:public A       //B和A是继承关系,B继承了A
{};
class C                  //A和C是组合关系,C组合了A,C对象中有A对象
{
private:
	A  _a;
};

在实际应用中优先使用类和类之间的组合而不是类继承

  1. 继承允许你根据基类的实现去定义基类的实现。通过这种复用基类方式生成派生类的方式通常称为白箱复用。在这种继承方式中,基类的内部实现细节对派生类可见。继承的方式一定程度上破坏了基类的封装性,在派生类中可以访问使用基类的公有和保护成员,那么基类的改变,对派生类的影响就很大。基类和派生类之间的依赖关系很高,耦合度高
  2. 组合同样也是一种复用方式。组合要求被组合的对象具有良好定义的接口。这种复用方式被称为黑盒复用。假设B组合了A,那么B仅可以访问A类的公有成员,那么A类的改变,对B的影响不是很大,B对A的依赖性不是很强,所以耦合度低

继承的耦合度高,组合的耦合度低,由于在软件设计层次追求的是高内聚,即类内的关联很强,低耦合,即类外的关联性很弱

  1. 所以当这个类的实现可以使用继承去实现又可以使用组合去实现的时候,优先使用组合
  2. 当这个类比较适合使用继承的去实现时候就使用继承,当比较适合使用组合去实现的时候就使用组合,要实现多态的时候就必须使用继承
相关推荐
零匠学堂20251 小时前
woapi-server为Office Online Server文档在线预览提供文档加载地址
java·运维·服务器·oos·wopi
Tandy12356_1 小时前
手写TCP/IP协议栈——数据包结构定义
c语言·网络·c++·计算机网络
Hui Baby1 小时前
maven自动构建到镜像仓库
java·maven
爱可生开源社区1 小时前
SCALE | 2025 年 11 月《大模型 SQL 能力排行榜》发布
数据库·sql·llm
繁华似锦respect1 小时前
HTTPS 中 TLS 协议详细过程 + 数字证书/签名深度解析
开发语言·c++·网络协议·http·单例模式·设计模式·https
czlczl200209251 小时前
SpringBoot手动配置:WebMvcConfigurer接口实现类的生效原理
java·spring boot·后端
程序员皮皮林1 小时前
SpringBoot + nmap4j 获取端口信息
java·spring boot·后端
小二·1 小时前
Spring框架入门:Spring 中注解支持详解
java·后端·spring
计算机学长felix1 小时前
基于SpringBoot的“某学院教室资产管理系统”的设计与实现(源码+数据库+文档+PPT)
数据库·spring boot·后端