一、继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称子类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。
下面我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址电话/年龄等成员变量,都有identity身份认证的成员函数,设计到两个类里面就是冗余的。
他们一些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课。
为了解决此类代码冗余的问题,C++引入了继承
二、继承的定义格式
基类(Base Class):这是一个现有的类,它的属性(成员变量)和方法(成员函数)可以被其他类继承。
派生类(Derived Class):这是一个从基类继承而来的类。
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。(因为翻译的原因,所以既叫父类/子类,也叫基类/派生类)
继承定义特点概述:
- 父类private成员在子类中无论以什么方式继承都是不可见的。这里的不可见是指父类的私有成员还是被继承到了子类对象中,但是语法上限制子类对象不管在类里面还是类外面都不能去访问它。
- 父类private成员在子类中是不能被访问,如果父类成员不想在类外直接被访问,但需要在子类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 实际上面的表格我们进行一下总结会发现,父类的私有成员在子类都是不可见。父类的其他成员在子类的访问方式 ==(Min(成员在父类的访问限定符,继承方式),public>protected>private。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在子类的类里面使用,实际中扩展维护性不强。
示例:
#include <iostream>
#include <string>
using namespace std;
// 基类:Person
class Person {
protected:
string name;
string address;
string phoneNumber;
public:
Person(const string& name, const string& address, const string& phoneNumber)
: name(name), address(address), phoneNumber(phoneNumber) {}
// 基类成员函数
void identity() const {
//... ...
}
};
// 派生类:Student
class Student : public Person //此处注意继承的写法
{
private:
string studentNumber;
public:
Student(const string& name, const string& address, const string& phoneNumber, const string& studentID)
: Person(name, address, phoneNumber), studentNumber(studentID) {}
// 派生类独有成员函数
void study() const {
cout << name << " is studying." << endl;
}
};
// 派生类:Teacher
class Teacher : public Person //此处注意继承的写法
{
private:
string title; //职称
public:
Teacher(const string& name, const string& address, const string& phoneNumber, const string& title)
: Person(name, address, phoneNumber), title(title) {}
// 派生类独有成员函数
void teach() const {
cout << name << " is teaching." << endl;
}
};
可以把上面代码改成现在这样:
创建一个基类 Person
来避免冗余,然后让 Student
和 Teacher
继承这个基类。基类 Person
包含共有的成员变量和函数,而 Student
和 Teacher
则包含各自特有的成员变量和函数。
这里我们将学习第三种访问限定符:
public
公有protected
保护 (结合继承规则介绍)private
私有
继承规则如下:
接下来的分析将围绕这张表,逐步学习继承的规则。
先看什么叫继承方式,和3种不同的基类成员呢?
基类成员
表中的类成员指的就是在基类(父类)中的方法和属性,根据访问限定符,来规定上下文中的可见性和可访问性,和以往学习的一样,只是多了protected(保护)的使用。
依然用我们上面的代码来举例说明(为观察效果作出了小改动):
继承方式
- 什么叫继承方式呢?
继承方式(Inheritance Type)指的是在 C++ 中派生类(子类)继承基类(父类)时指定的访问级别。这种访问级别会影响基类成员在派生类中的可见性和访问权限。
继续使用刚刚的代码来说明:
刚刚我们使用的就是公有继承(public)
对应的当然有保护继承和私有继承:
继承规则
知道各种基类成员和继承方式的基本概念之后,我们就可以学习具体的代码怎么写呢?
其实非常简单,大家先看完3个示例,结合下图,就能一一理解清楚了。
示例1:
公有继承使用:
- 基类的
public
成员在派生类中仍然是public
的。 - 基类的
protected
成员在派生类中仍然是protected
的。 - 基类的
private
成员仍然无法在派生类中访问
//基类
class Base {
public:
int publicMember;
protected:
int protectedMember;
private:
int privateMember;
};
//派生类
class Derived : public Base {
public:
void accessBaseMembers() {
publicMember = 10; // 可以访问
protectedMember = 20; // 可以访问
// privateMember = 30; // 无法访问,编译错误
}
};
公有继承是最常用的继承方式,因为它保持了基类成员的访问控制,同时允许派生类公开访问基类的公有接口。
示例2:
保护继承使用:
- 基类的
public
成员在派生类中变成protected
的。 - 基类的
protected
成员在派生类中仍然是protected
的。 - 基类的
private
成员仍然无法在派生类中访问。
//基类
class Base {
public:
int publicMember;
protected:
int protectedMember;
private:
int privateMember;
};
//派生类
class Derived : protected Base {
public:
void accessBaseMembers() {
publicMember = 10; // 可以访问
protectedMember = 20; // 可以访问
// privateMember = 30; // 无法访问,编译错误
}
};
int main() {
Derived d;
// d.publicMember = 10; // 无法访问,编译错误
}
保护继承限制了派生类中对基类成员的访问范围,使得基类的公有成员在派生类中不再是公有的,而是只能在派生类内部和进一步派生的子类中访问。
- 访问限定符
protected
:在类外面不能访问 ——代码种main函数无法访问protected
的成员
通过画图让大家更具体的理解继承里面发生了什么:
【注意】继承做了这些事情,但是实际代码并不能这样写,只是为了形象直观地给大家呈现效果
示例3:
私有继承的使用
- 基类的
public
和protected
成员在派生类中都变成private
的。 - 基类的
private
成员仍然无法在派生类中访问。
class Base {
public:
int publicMember;
protected:
int protectedMember;
private:
int privateMember;
};
class Derived : protected Base {
public:
void accessBaseMembers() {
publicMember = 10; // 可以访问
protectedMember = 20; // 可以访问
// privateMember = 30; // 无法访问,编译错误
}
};
int main() {
Derived d;
// d.publicMember = 10; // 无法访问,编译错误
}
图示:
通过3个例子很直观的观察到继承方式的不同会如何作用于派生类了。
最后注意一在基类中的private成员
:
-
不可访问性:基类的
private
成员(包括数据成员和成员函数)在派生类中是不可访问的。派生类不能直接访问或修改基类的private
成员,即使是通过派生类的成员函数。 -
继承但不可访问:虽然
private
成员被继承到了派生类中,但它们只能通过基类自己的成员函数访问。派生类无法直接访问这些成员,但可以通过基类的public
或protected
成员函数间接访问。 -
控制实现细节:使用
private
访问权限的一个主要目的是隐藏类的实现细节。这样,派生类和外部代码都无法依赖或修改这些私有细节,这增强了类的封装性和安全性。 -
不可重写性:基类的
private
成员函数不能在派生类中被重写。因为它们在派生类中是不可见的,因此派生类无法定义与基类中private
成员函数同名的函数。
示例:
#include <iostream>
#include <string>
using namespace std;
class Base {
private:
int privateData;
void privateFun() {
// 基类的私有成员函数
}
public:
Base() : privateData(0) {}
int getPrivateData() const {
return privateData; // 基类的公共函数可以访问私有成员
}
};
class Derived : public Base {
public:
void Derived_Fun() {
// privateData = 10; // 错误:无法访问基类的私有成员
// privateFun(); // 错误:无法调用基类的私有成员函数
}
};
int main()
{
Derived der;
der.getPrivateData(); //合法
return 0;
}
private总结:private成员无论何种继承方式在子类中都是不可见的。
使用总结:
三、父类与子类间赋值
在C++中,父类与子类之间的赋值涉及一些规则和注意事项,特别是在对象赋值、对象间的指针或引用赋值,以及赋值运算符重载的情况下。子类对象可以赋值给父类对象,但父类对象赋值给子类对象则需要满足特定条件,否则会出现问题。
赋值规则:
- 子类对象 可以赋值给父类对象 (对象切割下面会详细介绍)
- 子类对象 可以赋值给父类指针或引用(父类指针可以指向子类对象)
- 父类对象 不能赋值给子类对象
下面结合代码、图形来解释理解一下这部分内容
3.1 子类对象赋值给父类对象
继续沿用上面示例,main函数中我们实例化出 Student
对象,并试图将其赋值给一个 Person
对象。由于 Person
类并不包含 Student
类独有的 studentNumber
成员变量,赋值时将会发生对象切割。
#include <iostream>
#include <string>
using namespace std;
// 基类:Person
class Person {
protected:
string name;
string address;
string phoneNumber;
public:
Person(const string& name, const string& address, const string& phoneNumber)
: name(name), address(address), phoneNumber(phoneNumber) {}
// 基类成员函数
void identity() const {
//... ...
}
};
// 派生类:Teacher(没用到所以省略了)
// 派生类:Student
class Student : public Person {
private:
string studentNumber;
public:
Student(const string& name, const string& address, const string& phoneNumber, const string& studentNumber)
: Person(name, address, phoneNumber), studentNumber(studentNumber) {}
// 派生类独有成员函数
void study() const {
cout << name << " is studying." << endl;
}
};
int main() {
// 姓名 地址 电话号码 学号
Student student("张三", "上海 xx街", "1356729950", "2024090530");
Person person = student; // 对象切割
person.identity(); // 调用的是 Person 类的 identity() 函数
// person.study(); // 错误:Person 类没有 study() 函数
}
对象切割(Object Slicing)
也称作“切片”,对象切割发生在将一个派生类对象赋值给一个基类对象时。在这个过程中,派生类中的特有成员和行为会被“切割”掉,只保留基类部分。这是因为基类对象无法容纳派生类对象的全部信息。
结合图形在上面main函数中发生的具体过程:
在这个例子中,person
对象将只包含 name
、address
和 phoneNumber
成员,而 studentNumber
和 study()
函数无法访问。也就是说,person
对象只是 student
对象的 Person
部分。先简单理解成这样,但是成员函数C++中做了特殊处理下面会重点讲解。
希望大家结合图形可以很好的理解对象切割这一概念。
3.2 指针和引用赋值
如果我们使用指针或引用,将 Student
对象赋值给 Person
指针或引用,可以避免对象切割,并且允许通过虚函数表调用子类的方法(假设使用了虚函数:多态内容下一章细讲)。不过在当前类设计中,我们没有使用虚函数,但我们仍然可以通过指针或引用调用基类的成员函数。
// 基类:Person
class Person {
protected:
string name;
string address;
string phoneNumber;
public:
Person(const string& name, const string& address, const string& phoneNumber)
: name(name), address(address), phoneNumber(phoneNumber) {}
// 基类成员函数
void identity() const {
cout << name << endl;
cout << address << endl;
cout << phoneNumber << endl;
}
};
// 派生类:Teacher(没用到所以省略了)
// 派生类:Student
class Student : public Person {
private:
string studentNumber;
public:
Student(const string& name, const string& address, const string& phoneNumber, const string& studentNumber)
: Person(name, address, phoneNumber), studentNumber(studentNumber) {}
// 派生类独有成员函数
void study() const {
cout << name << " is studying." << endl;
}
};
int main() {
// 姓名 地址 电话号码 学号
Student student("张三", "上海 xx街", "1356729950", "2024090530");
Person person = student; // 对象切割
Person& personRef = student; // 使用引用,避免对象切割
personRef.identity(); // 调用的是 Person 类的 identity() 函数
Person* personPtr = &student; // 使用指针,避免对象切割
personPtr->identity(); // 调用的是 Person 类的 identity() 函数
//为了观察在identity中打印些信息
}
运行结果:
张三
上海 xx街
1356729950
张三
上海 xx街
1356729950
集合图形理解:
由于 personRef
和 personPtr
都指向 student
对象的 Person
部分,调用 identity()
函数时会表现出多态行为(如果该函数是虚函数),但它们无法直接访问 Student
类中的 study()
函数。因为指针和引用的用法是一样的,所以这里只使用一个作图表示。
3.3 父类对象赋值给子类对象
父类对象不能直接赋值给子类对象!!!
父类对象不能直接赋值给子类对象!!!
父类对象不能直接赋值给子类对象!!!
那父类对象为什么不能直接赋值给子类对象呢?
父类子类我们依旧用上面示例只改main函数做说明:
错误1:
int main() {
// 姓名 地址 电话号码
Person person("狗剩", "北京 xx区", "1350000000");
// 姓名 地址 电话号码 学号
Student student("张三", "上海 xx街", "1356729950", "2024090530");
//student = person; // 错误:不能将父类对象赋值给子类对象
//该操作编译都过不了
}
错误2:
有小伙伴可以在想:可以通过类型转换再赋值啊?可以通过显式转换来尝试强制赋值。但这种方式非常危险,可能导致程序崩溃或不确定的行为,因为子类特有的成员变量在父类中并不存在。
int main() {
// 姓名 地址 电话号码
Person person("狗剩", "北京 xx区", "1350000000");
Student* studentPtr = (Student*)&person; // 不安全的转换
// studentPtr->study(); // 可能导致未定义行为,因为 person 并没有 studentNumber
//此做法虽然可以过编译,但非常不推荐!!!
}
小伙伴们可以结合下面图形应该也是不难理解的:
四、隐藏
接下来我们将详细学习C++中继承体系中对成员函数的处理机制——隐藏。
在往下学习之前我们不能忘记一点:
- 就算是有继承关系,也不妨碍父类依然可以单独像普通类一样使用,一样可以实例化出自己的对象。
4.1 隐藏的概念
隐藏(hiding),也叫重定义,子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这意味着通过子类对象调用该函数时,默认只会调用子类的版本,而父类的同名函数将无法直接被调用。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
【注意】在实际中在继承体系里面最好不要定义同名的成员。
4.2 构成隐藏的条件
- 成员函数的隐藏,只需要函数名相同就构成隐藏。
我们先简单看看看隐藏是什么样子的:
4.3 使用
#include <iostream>
#include <string>
using namespace std;
// 基类:Person
class Person {
protected:
string name;
string address;
string phoneNumber;
public:
Person(const string& name, const string& address, const string& phoneNumber)
: name(name), address(address), phoneNumber(phoneNumber) {}
// 基类成员函数
void identity() const {
cout << "Person:" << endl;
cout << name << endl;
cout << address << endl;
cout << phoneNumber << endl;
}
};
// 派生类:Student
class Student : public Person {
private:
string studentNumber;
public:
Student(const string& name, const string& address, const string& phoneNumber, const string& studentNumber)
: Person(name, address, phoneNumber), studentNumber(studentNumber) {}
// 派生类独有成员函数
void study() const {
cout << name << " is studying." << endl;
}
//identity()成员函数名相同够成隐藏
void identity() const {
cout << "Student:" << endl;
cout << name << endl;
cout << address << endl;
cout << phoneNumber << endl;
cout << studentNumber << endl;
}
};
int main() {
// 姓名 地址 电话号码 学号
Student student("张三", "上海 xx街", "1356729950", "2024090530");
student.identity();
}
运行结果:
Student:
张三
上海 xx街
1356729950
2024090530
在 Student
类中,定义了一个与基类 Person
中同名的 identity()
函数。这个新的 identity()
函数会隐藏基类中的同名函数。具体来说,当通过 Student
对象调用 identity()
函数时,将调用 Student
中的版本,而不是 Person
中的版本。这就是函数隐藏。
当我们对隐藏有了基本的概念,现在来对继承体系的成员函数处理机制 做进一步探讨。
- 继承体系中基类和派生类都有独立的作用域。
在示例代码中:
int main() {
// 姓名 地址 电话号码 学号
Student student("张三", "上海 xx街", "1356729950", "2024090530");
student.identity();
}
Student类的对象调用identity(),派生类的作用域中查找某个名称时,优先查找派生类自己定义的成员
基类和派生类的作用域是独立的,即基类中的名称查找不会受到派生类中同名成员的影响。
关于隐藏我们再看最后一个问题:下面这段代码调用的是父类还是子类的 identity() 呢?
// 基类:Person
#include <iostream>
#include <string>
using namespace std;
class Person {
protected:
string name;
string address;
string phoneNumber;
public:
Person(const string& name, const string& address, const string& phoneNumber)
: name(name), address(address), phoneNumber(phoneNumber) {}
// 基类成员函数
void identity() const {
cout << "Person:" << endl;
cout << name << endl;
cout << address << endl;
cout << phoneNumber << endl;
}
};
// 派生类:Student
class Student : public Person {
private:
string studentNumber;
public:
Student(const string& name, const string& address, const string& phoneNumber, const string& studentNumber)
: Person(name, address, phoneNumber), studentNumber(studentNumber) {}
// 派生类独有成员函数
void study() const {
cout << name << " is studying." << endl;
}
//identity成员函数名相同够成隐藏
void identity() const {
cout << "Student:" << endl;
cout << name << endl;
cout << address << endl;
cout << phoneNumber << endl;
cout << studentNumber << endl;
}
};
int main() {
// 姓名 地址 电话号码 学号
Student student("张三", "上海 xx街", "1356729950", "2024090530");
Person& personRef = student;
personRef.identity();
Person* personPtr = &student;
personPtr->identity();
}
运行结果:
Person:
张三
上海 xx街
1356729950
Person:
张三
上海 xx街
1356729950
是的运行的都是Person中的identity(),详解:
4.3.1储存
编译器为每个类的成员函数生成一份代码,通常存储在程序的代码段(也称为文本段)中。父类和子类的 identity()
函数各自有独立的实现,分别存储在不同的位置
- 父类
Person::identity()
函数:编译器会为Person
类的identity()
函数生成一份代码,并将其存储在代码段中。 - 子类
Student::identity()
函数:同样,编译器也会为Student
类的identity()
函数生成一份代码,并将其存储在代码段中。
在程序的运行过程中,这两个函数的代码是独立存在的,并且不会因为它们的同名而发生冲突。
4.3.2. 函数隐藏的调用机制
在编译时,编译器已经确定了调用哪个函数,这个决定依赖于调用者的静态类型,即声明时的类型——Person。
-
父类指针/引用调用: 如果你通过父类
Person
的指针或引用调用identity()
函数,例如personPtr->identity();
或personRef.identity();
,编译器会在编译时将调用绑定到Person::identity()
的实现。 -
子类对象/指针/引用调用: 如果你通过子类
Student
的对象或引用调用identity()
函数,例如student.identity();
,编译器会将调用绑定到Student::identity()
的实现。