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. 设计原则:优先组合,按需继承。

相关推荐
AI人工智能+电脑小能手5 小时前
【大白话说Java面试题 第87题】【Mysql篇】第17题:分布式事务的实现原理?
java·数据库·分布式·mysql·面试
isyangli_blog7 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008117 小时前
FastAPI APIRouter
开发语言·python
Benszen7 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆7 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木7 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
Cosolar7 小时前
从零写一个 Attention Is All You Need
人工智能·面试·架构
MC皮蛋侠客8 小时前
C++17 多线程系列(五):C++17 并行算法——从串行到并行的零成本迁移
c++·多线程
杨充8 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~8 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言