[C++] 继承 | 隐藏 | 赋值兼容(切片)| 把基类当函数一样的调用构造

发布于:2025-04-16 ⋅ 阅读:(34) ⋅ 点赞:(0)

目录

知识点

继承的定义

基本特性

作用域

赋值兼容

派生类的创建销毁

构造函数

拷贝构造


实验代码+知识点回忆

// 实验一:继承方式验证
#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();
}

代码解析:

  1. 实验一实现要点
  • 创建Base类包含三种访问属性的成员变量
  • 通过三种继承方式派生子类:
    • 公有继承保持基类访问属性
    • 私有继承将所有基类成员转为私有
    • 保护继承将public转为protected
  • 通过成员函数验证访问权限
  1. 实验二实现要点
  • 使用作用域解析运算符::处理同名成员
  • 通过构造函数初始化列表分别初始化基类成员
  • 工资(wages)作为派生类自有成员直接初始化
  • 在show()方法中调用基类display()实现信息展示
  1. 运行效果
姓名:张三
年龄:35
性别:男
地址:北京
电话:13800138000
职称:教授
职务:系主任
工资:15000.5

建议调试时重点关注:

  1. 不同继承方式下的成员访问权限
  2. 同名成员的作用域解析过程
  3. 多重继承的构造函数调用顺序

知识点

  • 继承就是一种解决代码复用问题的方式。
  • 它允许用户创建一个新的类,继承自一个已经存在的类,从而继承和复用父类的属性和方法。
  • 通过继承,可以在不改变父类的前提下,为子类添加额外的属性和方法,实现功能的扩展。

总而言之,在C++中,继承是代码复用的重要手段。


以 下就是一个简单的继承结构,child继承了parent

  • 其中parent这种被别人继承的类叫做:基类 / 父类
  • child这种继承别人的类叫做:派生类 / 子类

当派生类继承了基类,派生类就可以使用基类内的成员和函数。

class parent
{
            
public:
	int _age;
};

class child : public parent
{
            
public:
	int _id;
};

继承的定义

Person是父类,也称作基类。Student是子类,也称作派生类。

访问方式

  1. 基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == 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

  1. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过显示的写出继承方式。
  2. 在实际运用中一般使用都是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;
};

以上示例中,childparent1继承到了_age属性,从parent2继承到了_name属性。


基本特性

继承关系中,有以下注意点:

  1. 继承后,派生类有可能只增改了基类的成员函数,而成员变量是一样的,所以基类和派生类的大小可能是一样的
  2. 友元关系不能继承,基类的友元不能访问子类的私有和保护成员
  3. 对于基类的静态成员,派生类和基类共用,派生类不会额外创建静态成员
  4. 如果不希望一个类被继承,可以将这个类的构造函数或者析构函数用private修饰
  5. 继承后,派生类的初始化列表指向顺序为继承顺序

作用域

基类与派生类有两个分别独立的作用域

当派生类继承了基类的成员后,如果派生类自己创建了与基类同名的成员,那么派生类成员将屏蔽对同名基类成员的直接访问,这种情况叫做隐藏

示例:

class A
{
            
	void func()
	{
            }

public:
	int num;
};

class B : public A
{
            
	void func()
	{
            }

public:
	int num;
};

在以上继承关系中,B继承了A的num变量与func函数,而B类自己还创建了同名的funcnum

  • 那么此时A的funcnum就称为被隐藏,在B内部直接访问numfunc,就是访问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;
};

规则:

  1. 派生类的对象可以赋值给基类的对象
student s;
person p = s;

如下图:

可以将一个派生类的成员赋值给基类成员,此时只取出派生类中属于基类的部分来构造基类,不属于基类的部分被丢弃,这称为切片

  1. 派生类的指针可以转换为基类的指针
  2. 派生类的引用可以转换为基类的引用
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)就是在显式调用基类的拷贝构造,不过在调用基类的拷贝构造时,传入的却是派生类的引用。这是为什么?

  • 刚在赋值兼容处说过:派生类的引用可以转化为基类的引用
  • 所以此处在传参时会发生一次隐式的切片,基类的拷贝构造只访问派生类的基类部分,来拷贝出一个基类。
  • 注意:拷贝构造也属于构造函数,所以拷贝构造在初始化列表中如果没有显式调用拷贝构造,就会隐式调用默认构造函数。

网站公告

今日签到

点亮在社区的每一天
去签到