C++ 继承

一、什么是C++继承?

简单来说,继承就是"子类继承父类的属性和方法",就像现实生活中,子女会继承父母的某些特征(比如外貌、性格),同时也会有自己独有的特点。在C++中,我们把被继承的类称为基类(父类) ,继承父类的类称为派生类(子类)

核心目的:减少代码冗余,提升代码可维护性,建立类之间的层次关系。

二、继承的基本语法

C++继承的语法非常简洁,核心格式如下:

cpp 复制代码
// 基类(父类)
class 基类名
{
    // 成员变量、成员函数(public、protected、private)
};

// 派生类(子类)继承基类
class 派生类名 : 继承方式 基类名 
{
    // 派生类自己的成员变量、成员函数
};

这里有一个关键知识点:继承方式。C++提供了3种继承方式,不同的继承方式会影响基类成员在派生类中的访问权限,这也是继承的重点和难点,我们后面详细说。

继承定义格式:

还有两种定义格式:

cpp 复制代码
//父类
class Preson
{
public:
	void fun();
};

//子类
class Student:Preson//没指定继承方式,默认private继承
{
public:

};

//子类
struct Teacher:Preson//没指定继承方式,默认public继承
{

};

下⾯我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/ 电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。当然他们 也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣ 的独有成员函数是学习,⽼师的独有成员函数是授课。

cpp 复制代码
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 = "张三";   // 姓名	
	int _age = 18;// 年龄
	string _address;//地址
	string _tel;//电话
	string _title;//职称
};

下⾯我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复⽤这些成员,就 不需要重复定义了,省去了很多⿇烦。

cpp 复制代码
//Person类
class Person
{
public:
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name = "张三";//姓名
	string _address;//地址
	string _tel;//电话
private:
	int _age = 18;//年龄
};

//学生类
class Student :public Person
{
public:
	//学习
	void study()
	{
		identity();
		cout << _tel << endl;
	}
protected:
	int _stuid;//学号
};

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

int main()
{
	Student s;
	Teacher t;
	s.study();
	t.teaching();
	return 0;
}

三、3种继承方式(重点!)

继承方式决定了基类的public、protected、private成员在派生类中的访问权限,简单来说,就是"基类的成员在子类中能被访问到什么程度"。3种继承方式分别是:public(公有继承)、protected(保护继承)、private(私有继承)。

首先我们回顾一下类的访问权限(基础回顾):

  • public:类内、类外都能访问(最开放);

  • protected:类内、子类能访问,类外不能访问;

  • private:只有类内能访问,子类、类外都不能访问(最封闭)。

下面我们分别讲解3种继承方式,用表格和示例帮大家理清逻辑(重点记公有继承,实际开发中最常用)。

cpp 复制代码
//Person类
class Person
{
public:
	void identity()
	{
		cout << "void identity()"<< _name << endl;
	}
	void eat()
	{
		cout << "吃饭"<<endl;
	}
	void sleep()
	{
		cout << "睡觉" << endl;
	}
	void address()
	{
		cout << _address << endl;
	}
	string _name = "张三";//姓名
	string _address;//地址
	string _tel;//电话
private:
	int _age = 18;//年龄
};

//学生类
class Student :public Person
{
public:
	//学习
	void study()
	{
		cout << "今天学C++" << endl;
	}
protected:
	int _stuid;//学号
};

int main()
{
	//创建派生类对象
	Student s;
	//访问继承自基类成员
	s._name = "小明";
	s._address = "广州";
	s.eat();//继承的方法
	s.sleep();//继承的方法
	s.address();//继承的方法
	//访问派生类自己的方法
	s.study();

	return 0;
}

结果:

从示例中可以看到,Student类没有定义_name、_address成员和eat()、sleep()、address()方法,但因为它继承了Person类,所以可以直接使用这些成员和方法,同时还能添加自己的特有方法study(),这就是继承的魅力。

1. 公有继承(public)

最常用的继承方式,也是最推荐的方式。

规则:基类的public成员 → 派生类的public成员;基类的protected成员 → 派生类的protected成员;基类的private成员 → 派生类无法访问(无论哪种继承,基类private成员子类都无法直接访问)。

通俗理解:基类的"公开内容"和"保护内容",子类继承后,保持原有的访问级别(公开的还是公开,保护的还是保护),只有基类的"私有内容",子类拿不到。

基类Person中,函数identity、eat、sleep、address是public,Student公有继承Person后,这些成员在Student中还是public。所以在main函数中(类外)可以直接访问s._name、s._eat()。

2. 保护继承(protected)

较少使用,通常用于特殊场景(比如希望子类能访问基类成员,但类外不能访问)。

通俗理解:基类的"公开内容",子类继承后变成"保护内容",类外不能访问了;基类的"保护内容",子类继承后还是"保护内容"。

cpp 复制代码
//学生类
class Student :protected Person
{
public:
	//学习
	void study()
	{
		cout << "今天学C++" << endl;
	}
protected:
	int _stuid;//学号
};

int main()
{
	//创建派生类对象
	Student s;
	//s._name = "小明";//err! _name在Student中是protected,类外不能访问
	//s.eat();//err! eat()在Student中是protected,类外不能访问
	s.study();//正确!study是Student的public成员
	return 0;
}

3. 私有继承(private)

极少使用,会将基类的所有可继承成员(public、protected)都变成派生类的private成员,子类的子类无法再继承这些成员。

通俗理解:基类的"公开内容"和"保护内容",子类继承后都变成自己的"私有内容",不仅类外不能访问,连子类的子类也不能访问。

cpp 复制代码
#include<iostream>
using namespace std;
class Person
{
public:
	void identity()
	{
		cout << "void identity()" << _name << endl;
		cout << "_age=" << _age << endl;//在父类对象中调用了私有,子类能间接调用
	}
protected:
	string _name = "张三";//姓名
	string _address;//地址
	string _tel;//电话
private:
	int _age = 18;//年龄
};

class Student :public Person
{
public:
	//学习
	void study()
	{
		identity();
		//cout << _age << endl;//拥有父类的私有,但是不能直接访问
		cout << _tel << endl;
	}
protected:
	int _stuid;//学号
};

int main()
{
	Student s1;
	s1.study();
	return 0;
}

图解:

总结:3种继承方式对比表

基类成员访问权限 公有继承(public) 保护继承(protected) 私有继承(private)
public 派生类public 派生类protected 派生类private
protected 派生类protected 派生类protected 派生类private
private 无法访问 无法访问 无法访问

核心记住2点:

① 基类private成员,无论哪种继承,子类都无法直接访问;

② 实际开发中,90%以上的场景用公有继承(public),其余两种极少用。

四、继承中的构造函数和析构函数

1. 构造函数的执行顺序

规则:先执行基类的构造函数,再执行派生类的构造函数

原因:派生类继承了基类的成员,只有先初始化基类的成员,才能初始化派生类自己的成员(就像先有父母,才有子女,子女的出生依赖于父母)。

2. 析构函数的执行顺序

规则:先执行派生类的析构函数,再执行基类的析构函数(后定义的先析构)。

原因:派生类的成员依赖于基类的成员,要先释放派生类自己的成员,再释放基类的成员(就像先注销子女的信息,再注销父母的信息,避免依赖错误)。

示例:

cpp 复制代码
//Person类
class Person
{
public:
	//构造
	Person()
	{
		cout << "Person构造" << endl;
	}
	//析构
	~Person()
	{
		cout << "Person析构" << endl;
	}
	
private:
	int _age = 18;//年龄
};
//学生类
class Student :public Person
{
public:
	//构造
	Student()
	{
		cout << "Student构造" << endl;
	}
	//析构
	~Student()
	{
		cout << "Student析构" << endl;
	}
protected:
	int _stuid;//学号
};

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

结果:

五、继承的核心特性(易错点)

1. 子类不能继承基类的4种东西

很多新手会误以为子类能继承基类的所有内容,其实不是,以下4种内容子类无法继承:

  • 基类的构造函数和析构函数(只能调用,不能继承);

  • 基类的private成员(无法直接访问,除非通过基类的public/protected成员函数间接访问);

  • 基类的友元函数(友元关系不能继承,基类的友元不能访问子类的成员);

  • 基类的赋值运算符重载函数(可以继承,但通常需要子类自己重写)。

2. 继承中的作⽤域

隐藏规则重点

1.成员变量隐藏:只要变量名相同,无论类型、参数是否一致,均构成隐藏;

2.成员函数隐藏:只要函数名相同,无论参数列表、返回值是否一致,均构成隐藏(区别于多态的重写);

3.访问基类被隐藏的成员:需使用基类作用域解析符 :: 显式访问。

  1. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。

示例:

cpp 复制代码
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是⾮常容易混淆
class Person
{
protected:
	int _num = 111;// ⾝份证号
	string _name = "王彪"; // 姓名
};

class Student : public Person
{
 public:
	void Print()
	{
		cout << " 姓名:"<<_name<< endl;
		cout << " 身份证号: "<<Person::_num<< endl;//显式访问基类被隐藏的_num
		cout << " 学号: "<<_num<<endl;
	}
 protected:
	int _num = 999; // 学号
};
int main()
{
	Student s1;
	s1.Print();
	return 0;
};

结果:

来点列题加深印象:

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

A. 重载 B.隐藏 C.没关系

2.下⾯程序的编译运⾏结果是什么(A)

A. 编译报错 B.运⾏报错 C.正常运⾏

cpp 复制代码
class A
{
  public:
  void func()
  {
     cout << "func()" << endl;
  } 
};

class B : public A
{
public:
	void func(int i)
	{
		cout << "func(int i)" << i << endl;
	}
};
int main()
{
	B b;
	b.func(10);
	b.func();//正确修改:b.A::func();
	return 0;
};

题解:b.fun()没传参,就没指定类域,所以报错。

六、继承的特殊场景:友元、静态成员与类型转换

1. 继承与友元

核心规则:友元关系不能继承。基类的友元函数只能访问基类的成员,无法访问派生类的private/protected成员;若想让基类友元访问派生类成员,需将该友元函数也声明为派生类的友元。

示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

class Student;//前置声明

class Person
{
public:
	//友元关系不能被继承
	friend void Display(const Person& p, const Student& s);
protected:
	string _name="张三";//姓名
};

class Student :public Person
{
	friend void Display(const Person& p, const Student& s);//声明为派生类友元,才能访问_stuNum
protected:
	int _stuNum=123456;//学号
};

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;
}

2. 继承与静态成员

核心规则:基类定义的static静态成员,在整个继承体系中只有一份实例,无论派生出多少个子类,都共用这一个静态成员。

示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

class Person
{
public:
	string _name="张三";
	static int _count;//静态成员类里面定义
};

int Person::_count = 0;//类外初始化

class Student :public Person
{
protected:
	int _stuNum=123456;
};

int main()
{
	Person p;
	Student s;
	//这里可以看到非静态成员_name的地址是不一样的
	cout << &p._name << endl;
	cout << &s._name << endl;

	//这里可以看到静态成员_count的地址是一样的
	cout << &p._count << endl;
	cout << &s._count << endl;
	
	//公有的情况下,父子类指定类域都可以访问静态成员
	cout << Person::_count << endl;
	cout << Student::_count << endl;

	return 0;
}

3. 基类与派生类的类型转换

仅在public继承下,支持以下类型转换(形象称为"切片"或"切割"):

  • 派生类对象可以赋值给基类对象、基类指针、基类引用(仅切割出基类部分);

  • 基类对象不能赋值给派生类对象(派生类有额外成员,无法初始化);

  • 基类指针/引用可以通过强制类型转换赋值给派生类指针/引用,但只有当基类指针/引用指向派生类对象时,转换才安全(多态中可使用dynamic_cast安全转换)。

示例:

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

class Student :public Person
{
public:
	int _NO;//学号
};

int main()
{
	Student sobj;
	//1.派生类对象->基类指针/引用/对象(合法)
	Person* pp = &sobj;
	Person& rp = sobj;
	Person pobj = sobj;//调用基类拷贝构造完成切片

	//2.基类对象->派生类对象(非法,编译报错)
	//sobj=pobj;
	return 0;
}

七、多继承与菱形继承(难点)

前面讲的都是"单继承"(一个派生类只有一个直接基类),而C++还支持"多继承"(一个派生类有两个及以上直接基类)。多继承虽然灵活,但会带来菱形继承问题,这也是C++语法的一个难点。

1. 菱形继承的问题

菱形继承是多继承的特殊情况:两个派生类继承同一个基类,再由一个派生类继承这两个派生类。此时会出现两个核心问题:

  • 数据冗余:派生类对象中会有两份基类成员(比如下面的Assistant对象中,会有两份Person成员);

  • 二义性:访问基类成员时,无法确定访问的是哪一个派生类继承的基类成员。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 基类:Person
class Person {
public:
    string _name; // 姓名
};

// 派生类1:Student(继承Person)
class Student : public Person {
protected:
    int _num; // 学号
};

// 派生类2:Teacher(继承Person)
class Teacher : public Person {
protected:
    int _id; // 职工编号
};

// 派生类3:Assistant(继承Student和Teacher)→ 菱形继承
class Assistant : public Student, public Teacher {
protected:
    string _majorCourse; // 主修课程
};

int main() {
    Assistant a;
    // a._name = "peter"; // 错误:二义性,无法确定访问哪个Person的_name
    a.Student::_name = "xxx"; // 显式指定,解决二义性,但无法解决数据冗余
    a.Teacher::_name = "yyy";
    return 0;
}

图解:

2. 解决方法:虚继承

C++引入虚继承(virtual关键字),可以解决菱形继承的数据冗余和二义性问题。核心原理:虚继承会让所有派生类共享一份基类成员,不再重复存储。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class Person {
public:
    string _name;
};

// 虚继承:在继承方式前加virtual
class Student : virtual public Person {
protected:
    int _num;
};

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

// 菱形继承,此时Assistant对象中只有一份Person成员
class Assistant : public Student, public Teacher {
protected:
    string _majorCourse;
};

int main() {
    Assistant a;
    a._name = "peter"; // 无歧义,正常访问
    return 0;
}

注意:虚继承会增加底层实现的复杂度和性能损耗,因此实际开发中,尽量避免设计菱形继承。很多编程语言(如Java)直接不支持多继承,就是为了规避这个问题。

八、继承与组合(设计原则)

除了继承,C++还有另一种代码复用方式------组合。两者的核心区别的是:

  • 继承(is-a关系):派生类是一个基类对象,基类的内部细节对派生类可见,耦合度高(基类修改会影响所有子类),属于"白箱复用";

  • 组合(has-a关系):一个类中包含另一个类的对象,被组合的对象细节不可见,耦合度低(只需依赖接口),属于"黑箱复用"。

设计原则:优先使用组合,而非继承

组合的耦合度更低,代码维护性更好;但如果类之间确实是"is-a"关系(如Student is a Person),或需要实现多态,就必须使用继承。如果两者都适用,优先选择组合。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 轮胎类
class Tire {
protected:
    string _brand = "米其林";
    size_t _size = 17;
};

// 组合:Car has a Tire(车有轮胎)
class Car {
protected:
    string _colour = "白色";
    Tire _t1, _t2, _t3, _t4; // 组合轮胎对象
};

// 继承:BMW is a Car(宝马是车)
class BMW : public Car {
public:
    void Drive() {
        cout << "宝马:好开-操控佳" << endl;
    }
};

int main() {
    BMW b;
    b.Drive();
    return 0;
}

核心要点回顾

  1. 继承的价值:类层次复用,减少冗余,建立清晰的类结构;

  2. 访问权限与继承方式:记住核心公式,重点掌握公有继承;

  3. 默认成员函数:构造先基类后派生类,析构先派生类后基类;

  4. 隐藏规则:同名成员会屏蔽,需用作用域解析符访问基类成员;

  5. 特殊场景:友元不继承、静态成员共享、public继承支持切片;

  6. 菱形继承:用虚继承解决,但尽量避免设计;

  7. 设计原则:优先组合,按需继承。

相关推荐
Spliceㅤ2 小时前
项目:基于qwen的点餐系统
开发语言·人工智能·python·机器学习·自然语言处理
ZHOUPUYU2 小时前
PHP与WebSocket实时通信的原理到生产级应用
开发语言·html·php
宝耶2 小时前
Java面试2:final、finally、finalize 的区别?
java·开发语言·面试
码云数智-大飞2 小时前
生死时速:高并发秒杀系统的架构设计与防超卖实战
开发语言
DREW_Smile2 小时前
数据在内存中的存储
c语言·开发语言
吴声子夜歌3 小时前
JavaScript——对象
开发语言·javascript·ecmascript
华科大胡子3 小时前
开源项目Git贡献
c++
不会写DN3 小时前
Js常用的字符串处理
开发语言·前端·javascript
dreamxian3 小时前
苍穹外卖day10
java·开发语言·spring boot