目录
一、前言
继承
是 面向对象三大特性之一(封装、继承、多态),所有的面向对象(OO
)语言都具备这三个基本特征,封装相关概念已经在《类和对象》系列中介绍过了,今天主要学习的是继承
,即如何在父类的基础之上,构建出各种功能更加丰富的子
二、什么是继承 ?
什么是继承?是继承 -- 遗产 还是继承 -- 花呗?答案都不是,先来看看官方解释:
继承(inheritance)机制是 ----面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有基类(父类)特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类(子类)
💢继承的概念💢
继承相关概念:
- 被继承对象:父类 / 基类 (
base
) - 继承方:子类 / 派生类 (
derived
)
继承的本质 就是 ------------ 复用代码
举个例子 : 假设我现在要设计一个校园管理系统,那么肯定会设计很多角色类,比如学生、老师、保安、保洁等等之类的。
设计好以后,我们发现,有些数据和方法是每个角色都有的,而有些则是每个角色独有的。
为了复用代码、提高开发效率,可以从各种角色中选出共同点,组成 基类,比如每个 人 都有姓名、年龄、联系方式等基本信息,而 教职工 与 学生 的区别就在于 管理与被管理,因此可以在 基类 的基础上加一些特殊信息如教职工号 表示 教职工,加上 学号 表示学生,其他细分角色设计也是如此
这样就可以通过 继承
的方式,复用 基类 的代码,划分出各种 子类
像上面共同拥有的数据和方法我们可以重新设计一个类Person ,然后让 Student 和 Teacher 去继承它,如下:
// 大众类 --- 基础属性
class Person
{
public:
Person(string name = string(), string tell = string(), int age = int())
:_name(name)
,_tell(tell)
,_age(age)
{}
void Print()
{
cout << "我的名字是 :" << _name << endl;
cout << "我的电话是 :" << _tell << endl;
cout << "我的年龄是 :" << _age << endl;
}
protected:
string _name; // 姓名
string _tell; // 电话
int _age; // 年龄
};
// 学生类 --- 派生/子属性
class Student : public Person
{
public:
Student(int stuId = 1578)
:Person("XAS","123456789",26)
,_stuId(stuId)
{
cout << "我是一个学生" << endl;
cout << "以下是我的个人信息 :" << endl;
cout << endl;
cout << "我的学号为 :" << _stuId << endl;
}
protected:
int _stuId; // 学号
};
// 老师类 --- 派生/子属性
class Teacher : public Person
{
public:
Teacher(int workId = 2024916)
:Person("xas","987654321",26)
,_workId(workId)
{
cout << "我是一个老师" << endl;
cout << "以下是我的个人信息 :" << endl;
cout << endl;
cout << "我的工号为 :" << _workId << endl;
}
protected:
int _workId; // 工号
};
int main()
{
Student s;
s.Print();
cout << "---------------------" << endl;
cout << "---------------------" << endl;
Teacher t;
t.Print();
return 0;
}
继承后,父类的 Person 的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student 和 Teacher 复用了 Person
💢继承的定义💢
了解完 继承相关概念 后,就可以开始学习 使用继承 了
🥝定义格式
格式如下: Person 是 父类,也称作基类; Student 是 子类,也称作派生类。
格式为 :子类 : 继承方式 父类
,比如 class a : public b
就表示 a
继承了 b
,并且还是 公有继承
注:Java
中的继承符号为 extern
,而 C++
中为 :
🍇继承权限
继承有权限的概念,分别为:公有继承(public
)、保护继承(protected
)、私有继承(private
)
没错,与 类 中的访问 限定修饰符 一样,不过这些符号在这里表示 继承权限
简单回顾下各种限定符的用途
- 公有 public:公开的,任何人都可以访问
- 保护 protected:保护的,只有当前类和子类可以访问
- 私有 private:私有的,只允许当前类进行访问
权限大小:公有 > 保护 > 私有
保护 protected 比较特殊,只有在 继承 中才能体现它的价值,否则与 私有 作用一样
此时我们发现 ---- 访问权限:三种 继承权限:三种
根据排列组合,可以列出以下多种搭配方案
父类成员 / 继承权限 | public | protected | private |
父类的 public 成员 |
外部可见,子类中可见 | 外部不可见,子类中可见 | 外部不可见,子类中可见 |
父类的 protected 成员 |
外部不可见,子类中可见 | 外部不可见,子类中可见 | 外部不可见,子类中可见 |
父类的 private 成员 |
都不可见 | 都不可见 | 都不可见 |
注:所谓的--外部--其实就是 子类对象
总结:
- 无论是哪种继承方式,父类中的
private
成员始终不可被 [子类 / 外部] 访问;当外部试图访问父类成员时,依据min(父类成员权限, 子类继承权限)
,只有最终权限为public
时,外部才能访问 - 在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承,也不提倡使用 protetced/private继承,因为 protetced/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
如何证明呢? 通过一下的代码,我们来验证上面的结论!!
// 父类
class A
{
public:
int _a;
protected:
int _b;
private:
int _c;
};
// 子类
class B : public A
{
public:
B()
{
cout << _a << endl;
cout << _b << endl;
cout << _c << endl;
}
};
int main()
{
// 外部(子类对象)
B b;
b._a;
}
- public 继承
- protected 继承
- private 继承
之所以说
C++
的继承机制设计复杂了,是因为protected
和private
继承时的效果一样
其实 C++
中搞这么多种情况(9种)完全没必要,实际使用中,最常见到的组合为 public : public
和 protected : public
如何优雅的使用好 ---- 继承权限 ?
对于只想自己类中查看的成员,设为 private
,对于想共享给子类使用的成员,设为 protected
,其他成员都可以设为 public
比如在张三家中,张三家的房子面积允许公开,家庭存款只限家庭成员共享,而个人隐私数据则可以设为私有
class Home
{
public:
int area = 500; //500 平米的大房子
};
class Father : public Home
{
protected:
int money = 50000; //存款五万
private:
int privateMoney = 100; //私房钱,怎能公开?
};
class Zhangsan : public Father
{
public:
Zhangsan()
{
cout << "我是张三" << endl;
cout << "我知道我家房子有 " << area << " 平方米" << endl;
cout << "我也知道我家存款有 " << money << endl;
cout << "但我不知道我爸爸的私房钱有多少" << endl;
}
};
class Xiaoming
{
public:
Xiaoming()
{
cout << "我是小明" << endl;
cout << "我只知道张三家房子有 " << Home().area << " 平方米" << endl;
cout << "其他情况我一概不知" << endl;
}
};
int main()
{
Zhangsan z;
cout << "================" << endl;
Xiaoming x;
return 0;
}
三、基类与派生类对象的赋值转换
在继承中,允许将 子类 对象直接赋值给 父类,但不允许 父类 对象赋值给 子类
- 这其实很好理解,儿子以后可以当父亲,父亲还可以当儿子吗?
并且这种 赋值 是非常自然的,编译器直接处理,不需要调用 赋值重载 等函数
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。
(1) 子类对象可以赋值给父类对象
//基类
class Person
{
public:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
//派生类
class Student : public Person
{
public:
int _id;
};
int main()
{
Person p;
Student s;
s._name = "张三";
s._sex = "男";
s._age = 20;
s._id = 8888;
p = s; // 子类对象赋值给父类对象
return 0;
}
通过调式可以看到,为什么没有把 id 赋值过去呢?
这里有个形象的说法叫切片或者切割,相当于把派生类中父类那部分切来赋值过去,如图所示:
(2) 子类对象可以赋值给父类指针
//基类
class Person
{
public:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
//派生类
class Student : public Person
{
public:
int _id;
};
int main()
{
Student s;
s._name = "张三";
s._sex = "男";
s._age = 20;
s._id = 8888;
Person* p = &s;
return 0;
}
可以看到,当父类对象是一个指针的时候,照样可以赋值过去:
子类对象赋值给父类指针切片图:
(3) 子类对象可以赋值给父类引用
//基类
class Person
{
public:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
//派生类
class Student : public Person
{
public:
int _id;
};
int main()
{
Student s;
s._name = "张三";
s._sex = "男";
s._age = 20;
s._id = 8888;
Person& rp = s;
return 0;
}
可以看到,当父类对象是一个引用的时候,也可以赋值过去:
子类对象赋值给父类引用切换图片:
(4) 父类对象不能赋值给子类对象
//基类
class Person
{
public:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
//派生类
class Student : public Person
{
public:
int _id;
};
int main()
{
Student s;
Person p;
s = p;
return 0;
}
编译会报错:
四、继承的作用域
在继承体系中 基类 和 派生类 都有独立的作用域,如果子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫 隐藏,也叫重定义。
代码示例: Student 的 _num 和 Person的 _num 构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆。
// 基类
class Person
{
protected:
string _name = "Edison"; // 姓名
int _num = 555; // 身份证号
};
// 派生类
class Student : public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "学号:" << _num << endl;
}
protected:
int _num = 888; // 学号
};
int main()
{
Student s1;
s1.Print();
return 0;
}
运行可以看到,访问的是子类中的_num (类似于局部优先的原则)
那么如果我想访问父类中的_num 呢 ? 可以使用基类 :: 基类成员显示的去访问 :
// 基类
class Person
{
protected:
string _name = "Edison"; // 姓名
int _num = 555; // 身份证号
};
// 派生类
class Student : public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << " 身份证号:" << Person::_num << endl;
}
protected:
int _num = 888; // 学号
};
int main()
{
Student s1;
s1.Print();
return 0;
}
可以看到,此时就是访问的父类中的 _num
还有一点需要注意的是 : 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
// 基类
class A
{
public:
void fun()
{
cout << "A::func()" << endl;
}
};
// 派生类
class B : public A
{
public:
void fun(int i)
{
cout << "B::func()" << endl;
cout << "func(int i)->" << i << endl;
}
};
int main()
{
B b;
b.fun(10);
return 0;
}
可以看到,默认是去调用子类的 fun() 函数,因为成员函数满足函数名相同就构成隐藏。
如果想调用父类的 fun() 还是需要指定作用域
// 基类
class A
{
public:
void fun()
{
cout << "A::func()" << endl;
}
};
// 派生类
class B : public A
{
public:
void fun(int i)
{
cout << "B::func()" << endl;
cout << "func(int i)->" << i << endl;
}
};
int main()
{
B b;
b.A::fun();
return 0;
}
运行可以看到,此时就是调用父类中的 fun()
注意 :B 中的 fun 和 A 中的 fun 不是构成函数重载,而是隐藏 !函数重载的要求是在同一作用域里面!!
另外,在实际中在继承体系里面最好不要定义同名的成员。
五、派生类中的默认成员函数
派生类(子类)也是 类,同样会生成 六个默认成员函数(用户未定义的情况下)
不同于单一的 类,子类 是在 父类 的基础之上创建的,因此它在进行相关操作时,需要为 父类 进行考虑
这里我们只以下面的两个类为基础,讨论四类默认成员函数:构造函数、拷贝构造、赋值运算符重载、析构函数
class Person
{
public:
Person(const std::string name = std::string(), const int age = 18, const int sex = 1)
: _name(name)
, _age(age)
, _sex(sex)
{
std::cout << "Person()" << std::endl;
}
Person(const Person& p)
: _name(p._name)
, _age(p._age)
, _sex(p._sex)
{
std::cout << "Person(const Person& p)" << std::endl;
}
Person& operator= (const Person& p)
{
std::cout << "operator= (const Person& p)" << std::endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
std::cout << "~Person()" << std::endl;
}
protected:
std::string _name;
int _age = 18;
int _sex = 1;
};
class Student : public Person
{
public:
protected:
long long _st_id;
};
💢默认成员函数的调用 💢
🔥构造函数与析构函数🔥
int main()
{
Student st;
return 0;
}
output:
Person() // s1 的构造
~Person() // s1 的析构
说明了,派生类的默认构造函数和默认析构函数都会 -----自动调用基类的构造和析构
🔥拷贝构造🔥
int main()
{
Student st1;
Student st2(st1);
return 0;
}
output:
Person() // s1 的构造函数
Person(const Person& p) // s2 拷贝构造
~Person() // s2 的析构函数
~Person() // s1 的析构函数
说明了,派生类的默认拷贝构造会自动调用基类的拷贝构造
🔥赋值运算符重载 🔥
int main()
{
Student st1, st2;
st1 = st2;
return 0;
}
output:
Person() // s1 的拷贝构造
Person() // s2 的拷贝构造
operator= (const Person& p) // 赋值重载
~Person() // s2 的析构函数
~Person() // s1 的析构函数
说明了,派生类的默认赋值运算符重载会自动调用基类的赋值运算符重载
实际上,我们可以将派生类的成员分成三部分:基类成员、内置类型成员、自定义类型成员
继承相较于我们以前学的类和对象,可以说就是多了基类那一部分
当调用派生类的默认成员函数时,对于基类成员都会调用对应基类的默认成员函数来处理
💢显示成员函数的调用 💢
🔥构造函数 🔥
当实现派生类的构造函数时,就算不显示调用基类的构造,系统也会自动调用基类的构造:
class Student : public Person
{
public:
Student(long long st_id = 111)
: _st_id(st_id)
{
std::cout << "Student()" << std::endl;
}
protected:
long long _st_id;
};
int main()
{
Student st;
return 0;
}
output:
Person()
Student()
~Person()
如果需要显示的调用基类的构造函数,应该这样写:
Student(long long st_id = 111)
: _st_id(st_id)
, Person("xas", 18)
{
std::cout << "Student()" << std::endl;
}
特别注意:
Person("xas", 18)
如果放在初始化列表,那就使显式调用基类的构造函数;如果放在函数体内,那就使创建一个基类的匿名对象
🔥拷贝构造 🔥
当实现派生类的拷贝构造时,如果没有显式调用基类的拷贝构造,那么系统就会自动调用基类的构造
class Student : public Person
{
public:
Student(long long st_id = 111)
: _st_id(st_id)
, Person("xas", 18)
{
std::cout << "Student()" << std::endl;
}
Student(const Student& s)
: _st_id(s._st_id)
{
std::cout << "Student(const Student& s)" << std::endl;
}
protected:
long long _st_id;
};
int main()
{
Student st1;
Student st2(st1);
return 0;
}
output:
Person()
Student()
Person() //系统自动调用了基类的构造
Student(const Student& s)
~Person()
~Person()
也可以像显示调用构造函数一样显式调用基类的拷贝构造:
Student(const Student& s)
: Person(s)
, _st_id(s._st_id)
{
std::cout << "Student(const Student& s)" << std::endl;
}
🔥赋值运算符重载 🔥
在实现派生类的赋值运算符重载时,如果没有显式调用基类的赋值运算符重载,系统也不会自动调用基类的赋值运算符重载
class Student : public Person
{
public:
Student(long long st_id = 111)
: _st_id(st_id)
, Person("xas", 18)
{
std::cout << "Student()" << std::endl;
}
Student& operator= (const Student& s)
{
std::cout << "operator= (const Student& s)" << std::endl;
if (this != &s)
{
_st_id = s._st_id;
}
return *this;
}
protected:
long long _st_id;
};
int main()
{
Student st1, st2;
st1 = st2;
return 0;
}
output:
Person()
Student()
Person()
Student()
operator= (const Student& s)
~Person()
~Person()
因此,在实现派生类的赋值运算符重载时,必须显示调用基类的赋值运算符重载:
Student& operator= (const Student& s)
{
std::cout << "operator= (const Student& s)" << std::endl;
if (this != &s)
{
Person::operator=(s); //由于基类和派生类的赋值运算符重载构成隐藏,因此要用 :: 指定类域
_st_id = s._st_id;
}
return *this;
}
🔥析构函数🔥
在实现派生类的析构函数时,不要显式调用基类的析构函数,系统会在派生类的析构完成后自动调用基类的析构
class Student : public Person
{
public:
Student(long long st_id = 111)
: _st_id(st_id)
, Person("xas", 18)
{
std::cout << "Student()" << std::endl;
}
~Student()
{
std::cout << "~Student()" << std::endl;
}
protected:
long long _st_id;
};
int main()
{
Student st1;
return 0;
}
output:
Person()
Student()
~Student()
~Person()
- 和前面的默认成员函数不同,在实现派生类的析构时,基类的析构不能显式调用
- 这是因为,如果显示调用了基类的析构,就会导致基类成员的资源先被清理,如果此时派生类成员还访问了基类成员指向的资源就,就会导致野指针问题
- 因此,必须保证析构顺序为先子后父,保证数据访问的安全
六、继承与友元
友元关系不能继承,也就是说 基类友元不能访问子类私有和保护成员,只能访问自己的私有和保护成员。
下面代码中,Display 函数是基类 Person 的友元,但是 Display 函数不是派生类 Student 的友元,也就是说 Display 函数无法访
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 函数也能够访问派生类Student 的私有和保护成员,只需要在派生类Student 当中进行友元声明。
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s); // 声明Display是Person的友元
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
friend void Display(const Person& p, const Student& s); // 声明Display是Student的友元
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;
}
七、继承与静态成员
如果基类中定义了static 静态成员,则整个继承体系里面只有一个这样的成员。 无论派生出多少个子类,都只有一个 static 成员实例。
下面代码中,在基类 Person 当中定义了静态成员变量 _ count, 派生类 Student 和 Graduate 继承了Person, 但是,在整个继承体
// 基类
class Person
{
public:
Person() { ++_count; }
protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
// 静态成员在类外面定义
int Person::_count = 0;
// 派生类
class Student : public Person
{
protected:
int _stuNum; // 学号
};
// 派生类
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
int main()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
Person s;
cout << " 人数 :" << Person::_count << endl;
cout << " 人数 :" << Student::_count << endl;
cout << " 人数 :" << s4._count << endl;
return 0;
}
我们定义了5个对象,那么每定义一个对象都会去调用一次++ _count ,打印以后可以看到,这几个对象里面的 _count 都是一样的:
同时,我们还可以打印一下地址,可以看到也是同-个:
总结: 关于父类中的静态成员,子类继续下来以后都是同一个,类似于“传家宝"。
八、菱形继承
💧 单继承💧
一个子类只有一个直接父类时称这个继承关系为单继承
💧 多继承💧
一个子类有两个或以上直接父类时称这个继承关系为多继承
💧 菱形继承💧
C++
支持多继承,即支持一个子类继承多个父类,使其基础信息更为丰富,但凡事都有双面性,多继承 在带来巨大便捷性的同时,也带来了个巨大的坑:菱形继承问题
🍍概念
首先
C++
允许出现多继承的情况,如下图所示
这样看很正常是吧,但如果出现以下这种 重复继承 的情况,就比较麻烦了
此时 普通人X 会纠结于使用哪一个 不用吃饭 的属性!这对于编译器来说,是一件无法处理的事
🍉现象
将上述概念转化为代码,观察实际现象
注:多继承时,只需要在 父类 之后,添加
,
号,继续增加想要继承的父类
class Person
{
public:
string _name; //姓名
};
//本科生
class Undergraduate : public Person
{};
//研究生
class Postgraduate : public Person
{};
//毕业生
class Graduate : public Undergraduate, public Postgraduate
{};
int main()
{
Graduate g1;
g1._name = "zhangsan";
return 0;
}
无法编译!
原因分析:
Undergraduate
中继承了 Person
的 _name
,Postgraduate
也继承了 Person
的 _name
Graduate
多继承 Undergraduate
、Postgraduate
后,同时拥有了两个 _name
,使用时,无法区分!
通过监视窗口查看信息:
解决方法:
想要解决二义性很简单,通过 ::
限制访问域即可
Graduate g1;
g1.Undergraduate::_name = "zhangsan";
cout << g1.Undergraduate::_name << endl;
但这没有从本质上解决问题!而且还没有解决数据冗余问题
真正的解决方法:虚继承
注:虚继承是专门用来解决 菱形继承 问题的,与多态中的虚函数没有直接关系
虚继承:在菱形继承的腰部继承父类时,加上
virtual
关键字修饰被继承的父类
class Person
{
public:
string _name; //姓名
};
//本科生
class Undergraduate : virtual public Person
{};
//研究生
class Postgraduate : virtual public Person
{};
//毕业生
class Graduate : public Undergraduate, public Postgraduate
{};
int main()
{
Graduate g1;
g1._name = "zhangsan";
cout << g1._name << endl;
return 0;
}
此时可以解决 菱形继承 的 数据冗余 和 二义性 问题
虚继承是如何解决菱形继承问题的?
- 利用 虚基表 将冗余的数据存储起来,此时冗余的数据合并为一份
- 原来存储 冗余数据 的位置,现在用来存储 虚基表指针
此时无论这个 冗余 的数据存储在何处,都能通过 基地址 + 偏移量 的方式进行访问
九、继承和组合
public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象。
而 组合 是一种 has-a 的关系。假设 B 组合了 A,每个 B对象中都有一个 A对象。
举个例子: 轿车和奔驰就构成 is-a 的关系,所以可以使用继承。
// 车类
class Car
{
protected:
string _colour = "黑色"; // 颜色
string _num = "川A66688"; // 车牌号
};
// 奔驰
class Benz : public Car
{
public:
void Drive()
{
cout << "好开-操控" << endl;
}
};
再举个例子:汽车和轮胎之间就是 has-a 的关系,它们之间则适合使用组合。
// 轮胎
class Tire {
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
// 汽车
class Car {
protected:
string _colour = "黑色"; // 颜色
string _num = "川A66688"; // 车牌号
Tire _t; // 轮胎
};
实际项目中,更推荐使用 组合 的方式,这样可以做到 解耦,避免因父类的改动而直接影响到子类
- 公有继承:
is-a
—> 高耦合,可以直接使用父类成员- 组合:
has-a
—> 低耦合,可以间接使用父类成员
十、继承的总结和反思
1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
2. 多继承可以认为是C++的缺陷之一,很多后来的面向对象语言都没有多继承,如Java。
十一、共勉
以下就是我对 【C++】继承 的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对 【C++】多态 的理解,请持续关注我哦!!!