目录
实验代码+知识点回忆
// 实验一:继承方式验证
#include <iostream>
using namespace std;
class Base {
public:
int pub_var = 10;
protected:
int pro_var = 20;
private:
int pri_var = 30;
};
// 公有继承
class PublicDerived : public Base {
public:
void accessCheck() {
cout << "Public继承访问:" << pub_var; // ✔️ 可访问
cout << pro_var; // ✔️ 可访问
// cout << pri_var; // ❌ 不可访问
}
};
// 私有继承
class PrivateDerived : private Base {
public:
void accessCheck() {
cout << "Private继承访问:" << pub_var; // ✔️ 转为私有
cout << pro_var; // ✔️ 转为私有
// cout << pri_var; // ❌
}
};
// 保护继承
class ProtectedDerived : protected Base {
public:
void accessCheck() {
cout << "Protected继承访问:" << pub_var; // ✔️ 转为保护
cout << pro_var; // ✔️ 保持保护
// cout << pri_var; // ❌
}
};
// 实验二:多重继承
class Teacher {
protected:
string name;
int age;
string gender;
string address;
string phone;
public:
Teacher(string n, int a, string g, string addr, string ph)
: name(n), age(a), gender(g), address(addr), phone(ph) {}
void display();
string title;
};
class Cadre {
protected:
string name;
int age;
string gender;
string address;
string phone;
public:
Cadre(string n, int a, string g, string addr, string ph)
: name(n), age(a), gender(g), address(addr), phone(ph) {}
string post;
};
class Teacher_Cadre : public Teacher, public Cadre {
public:
double wages;
Teacher_Cadre(string n, int a, string g, string addr, string ph,
string t, string p, double w)
: Teacher(n,a,g,addr,ph), Cadre(n,a,g,addr,ph), wages(w) {
title = t;
post = p;
}
void show();
};
// 类外定义成员函数
void Teacher::display() {
cout << "姓名:" << name << "\n年龄:" << age
<< "\n性别:" << gender << "\n地址:" << address
<< "\n电话:" << phone << "\n职称:" << title << endl;
}
void Teacher_Cadre::show() {
Teacher::display();
cout << "职务:" << post << "\n工资:" << wages << endl;
}
int main() {
// 实验一验证
PublicDerived d1;
d1.pub_var = 1; // ✔️
// d1.pro_var = 2; // ❌
// 实验二验证
Teacher_Cadre tc("张三", 35, "男", "北京", "13800138000",
"教授", "系主任", 15000.5);
tc.show();
}
代码解析:
- 实验一实现要点:
- 创建Base类包含三种访问属性的成员变量
- 通过三种继承方式派生子类:
-
- 公有继承保持基类访问属性
- 私有继承将所有基类成员转为私有
- 保护继承将public转为protected
- 通过成员函数验证访问权限
- 实验二实现要点:
- 使用作用域解析运算符
::
处理同名成员 - 通过构造函数初始化列表分别初始化基类成员
- 工资(wages)作为派生类自有成员直接初始化
- 在show()方法中调用基类display()实现信息展示
- 运行效果:
姓名:张三
年龄:35
性别:男
地址:北京
电话:13800138000
职称:教授
职务:系主任
工资:15000.5
建议调试时重点关注:
- 不同继承方式下的成员访问权限
- 同名成员的作用域解析过程
- 多重继承的构造函数调用顺序
知识点
- 继承就是一种解决代码复用问题的方式。
- 它允许用户创建一个新的类,继承自一个已经存在的类,从而继承和复用父类的属性和方法。
- 通过继承,可以在不改变父类的前提下,为子类添加额外的属性和方法,实现功能的扩展。
总而言之,在C++中,继承是代码复用的重要手段。
以 下就是一个简单的继承结构,child
继承了parent
。
- 其中
parent
这种被别人继承的类叫做:基类 / 父类 child
这种继承别人的类叫做:派生类 / 子类
当派生类继承了基类,派生类就可以使用基类内的成员和函数。
class parent
{
public:
int _age;
};
class child : public parent
{
public:
int _id;
};
继承的定义
Person是父类,也称作基类。Student是子类,也称作派生类。
访问方式
- 基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected >private。
2.不可见,语法上限制访问(类里面和外面都不能用),private 是类外面不能使用,类里面可以
基类的 private 在派生类中不可见,即(基类) 父类的私有成员,子类无论如何都用不了
#include <iostream>
#include <string>
class Person {
protected:
std::string _name = "zhangsan";
int _age = 18;
public:
void Print() const {
std::cout << "_name: " << _name << std::endl;
std::cout << "_age: " << _age << std::endl;
}
};
class Student : public Person {
public:
void Func() const {
std::cout << "name: " << _name << std::endl;
std::cout << "age: " << _age << std::endl;
}
protected:
int _stuid;
};
int main() {
Student s;
// 测试Student的Func()方法
s.Func();
// 测试继承自Person的Print()方法
s.Print();
return 0;
}
所以父类中不想被子类使用的部分,就可以设置为 private
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
当然,与访问限定符一样,继承方式也是有默认值的:
- 用
class
定义的类,默认继承方式是private
- 用
struct
定义的类,默认的继承方式是public
多继承:
一个派生类可以同时继承多个基类:
class parent1
{
public:
int _age;
};
class parent2
{
public:
int _name;
};
class child : public parent1, public parent2
{
public:
int _id;
};
以上示例中,child
从parent1
继承到了_age
属性,从parent2
继承到了_name
属性。
基本特性
继承关系中,有以下注意点:
- 继承后,派生类有可能只增改了基类的成员函数,而成员变量是一样的,所以基类和派生类的大小可能是一样的
- 友元关系不能继承,基类的友元不能访问子类的私有和保护成员
- 对于基类的静态成员,派生类和基类共用,派生类不会额外创建静态成员
- 如果不希望一个类被继承,可以将这个类的构造函数或者析构函数用
private
修饰 - 继承后,派生类的初始化列表指向顺序为继承顺序
作用域
基类与派生类有两个分别独立的作用域
当派生类继承了基类的成员后,如果派生类自己创建了与基类同名的成员,那么派生类成员将屏蔽对同名基类成员的直接访问,这种情况叫做隐藏
示例:
class A
{
void func()
{
}
public:
int num;
};
class B : public A
{
void func()
{
}
public:
int num;
};
在以上继承关系中,B继承了A的num
变量与func
函数,而B类自己还创建了同名的func
与num
。
- 那么此时A的
func
与num
就称为被隐藏,在B内部直接访问num
与func
,就是访问B自己的num
。 - 如果想要访问A的成员,需要限定作用域。
即:当基类和派生类的函数或者成员变量名相同的时候,我们会保留派生类的函数名,不用继承的基类当中所有的,这种就叫做隐藏,隐藏的就是基类的变量
B b;
b.func(); // 默认访问B的func函数
b.A::func(); // 访问A的func函数
此外,函数重载要求两个函数在同一个作用域,而基类与派生类是两个不同作用域,所以就算参数不同也不能构成重载。所以只要基类与派生类内的函数名相同就构成隐藏,不考虑参数。
赋值兼容
赋值兼容是一个基类与派生类之间的转换规则,其可以让派生类转换为父类。
以如下的继承关系做讲解:
class person
{
public:
string _name;
string _sex;
int _age;
};
class student : public person
{
public:
int _No;
};
规则:
- 派生类的对象可以赋值给基类的对象
student s;
person p = s;
如下图:
可以将一个派生类的成员赋值给基类成员,此时只取出派生类中属于基类的部分来构造基类,不属于基类的部分被丢弃,这称为切片
。
- 派生类的指针可以转换为基类的指针
- 派生类的引用可以转换为基类的引用
student s;
person* pp = &s;
person& rp = s;
如图:
这种使用基类的指针或引用指向派生类的行为是合法的
- 因为基类的成员派生类都有,所以通过基类指针访问派生类,不会出现越界等行为。
派生类的创建销毁
派生类是如何创建销毁的?因为派生类内部还包含了一个基类,那么基类这一部分要如何初始化?
- 其实想要理解这一部分,就记住一句话
派生类的默认成员函数,把基类当作一个类成员变量处理。
- 接下来讲解构造函数,拷贝构造,赋值重载,析构函数这几个与创建销毁相关的函数,来理解派生类是如何创建销毁的。
构造函数
- 派生类构造函数将基类当作一个成员变量,不会直接初始化基类的成员,而是通过调用基类的构造函数。
- 在一般的类中,类内部如果有其他类的成员变量,构造函数会在初始化列表调用其构造函数。
- 如果不直接调用,那么会隐式调用其相应的默认构造函数。
如下:
class person
{
public:
string _name;
};
class child : public person
{
public:
child(string name, int num)
:person(name)
,_num(num)
{
}
private:
int _num;
};
:person(name)
就是在初始化列表显式地调用父类构造函数。派生类会先调用基类的构造函数,再调用自己的构造函数
拷贝构造
派生类拷贝构造将基类当作一个成员变量,不会直接拷贝基类的成员,而是通过调用基类的拷贝构造。
- 在一般的类中,类内部如果有其他类的成员变量,拷贝构造会在初始化列表调用其拷贝构造。
- 如果不直接调用,那么会隐式调用其相应的默认构造函数。
如下:
class person
{
public:
string _name;
};
class child : public person
{
public:
child(const person& c)
:parent(c) //隐式切片
,_num(c.num)
{
}
private:
int _num;
};
:parent(c)
就是在显式调用基类的拷贝构造,不过在调用基类的拷贝构造时,传入的却是派生类的引用。这是为什么?
- 刚在赋值兼容处说过:派生类的引用可以转化为基类的引用
- 所以此处在传参时会发生一次隐式的切片,基类的拷贝构造只访问派生类的基类部分,来拷贝出一个基类。
- 注意:拷贝构造也属于构造函数,所以拷贝构造在初始化列表中如果没有显式调用拷贝构造,就会隐式调用默认构造函数。