【C++】继承详解

💗个人主页💗

⭐个人专栏------C++学习

💫点击关注🤩一起学习C语言💯💫

目录

[1. 继承的概念及定义](#1. 继承的概念及定义)

[1.1 继承的概念](#1.1 继承的概念)

[1.2 继承定义](#1.2 继承定义)

1.2.1定义格式

1.2.2继承关系和访问限定符

1.2.3继承基类成员访问方式的变化

2.基类和派生类对象赋值转换

3.继承中的作用域

4.派生类的默认成员函数

[4.1 默认构造](#4.1 默认构造)

[4.2 拷贝构造](#4.2 拷贝构造)

[4.3 析构函数](#4.3 析构函数)

[4.4 赋值运算符](#4.4 赋值运算符)

[5. 复杂的菱形继承及菱形虚拟继承](#5. 复杂的菱形继承及菱形虚拟继承)

[5.1 单继承](#5.1 单继承)

[5.2 多继承](#5.2 多继承)

[5.3 菱形继承](#5.3 菱形继承)

[5.4 菱形继承](#5.4 菱形继承)


1. 继承的概念及定义

1.1 继承的概念

在C++中,继承是一种面向对象编程的重要概念,它允许一个类(称为派生类或子类)继承另一个类(称为基类或父类)的属性和行为。通过继承,子类可以获得父类的数据成员和成员函数,并且还可以添加自己的数据成员和成员函数。

cpp 复制代码
class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18;  // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
//Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可
//以看到变量的复用。调用Print可以看到成员函数的复用。
class Student : public Person
{
protected:
	int _stuid; // 学号
};

class Teacher : public Person
{
protected:
	int _jobid; // 工号
};
int main()
{
	Student s;
	Teacher t;
	s.Print();
	t.Print();
	return 0;
}

C++中的继承主要有三种类型:公有继承、私有继承和保护继承。

  1. 公有继承(public inheritance):使用public关键字表示。在公有继承中,基类的公有成员、保护成员可以在派生类中访问,派生类对象可以用来替代基类对象。

  2. 私有继承(private inheritance):使用private关键字表示。在私有继承中,基类的公有成员、保护成员都变为派生类的私有成员,只能在派生类内部访问。

  3. 保护继承(protected inheritance):使用protected关键字表示。在保护继承中,基类的公有成员、保护成员都变为派生类的保护成员,基类的私有成员对派生类是不可访问的。

派生类可以通过继承来扩展或修改基类的行为,它可以重新定义基类的成员函数(称为函数重写或函数覆盖),也可以添加新的成员函数和数据成员。通过继承,可以实现代码的重用和模块化,提高程序的可维护性和可扩展性。

1.2 继承定义

1.2.1定义格式

下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。

1.2.2继承关系和访问限定符

1.2.3继承基类成员访问方式的变化

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected 成员 派生类的private 成员
基类的protected 成员 派生类的protected 成员 派生类的protected 成员 派生类的private 成员
基类的private成 员 在派生类中不可见 在派生类中不可见 在派生类中不可 见
cpp 复制代码
#include <iostream>
using namespace std;

class Base 
{
public:
    int publicMember;
protected:
    int protectedMember;
private:
    int privateMember;
};

class DerivedPublic : public Base 
{
public:
    void accessMembers() 
    {
        publicMember = 1;      // 可以访问基类的公有成员
        protectedMember = 2;   // 可以访问基类的保护成员
        // privateMember = 3;  // 无法访问基类的私有成员
    }
};

class DerivedProtected : protected Base 
{
public:
    void accessMembers() 
    {
        publicMember = 1;      // 可以访问基类的公有成员
        protectedMember = 2;   // 可以访问基类的保护成员
        // privateMember = 3;  // 无法访问基类的私有成员
    }
};

class DerivedPrivate : private Base 
{
public:
    void accessMembers() 
    {
        publicMember = 1;      // 可以访问基类的公有成员
        protectedMember = 2;   // 可以访问基类的保护成员
        // privateMember = 3;  // 无法访问基类的私有成员
    }
};

int main() 
{
    DerivedPublic derivedPublic;
    derivedPublic.publicMember = 1;    // 可以访问基类的公有成员
    // derivedPublic.protectedMember = 2;  // 无法访问基类的保护成员

    DerivedProtected derivedProtected;
    // derivedProtected.publicMember = 1;   // 无法访问基类的公有成员
    // derivedProtected.protectedMember = 2;  // 无法访问基类的保护成员

    DerivedPrivate derivedPrivate;
    // derivedPrivate.publicMember = 1;   // 无法访问基类的公有成员
    // derivedPrivate.protectedMember = 2;  // 无法访问基类的保护成员

    return 0;
}

总结:

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强。

2.基类和派生类对象赋值转换

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类 的指针是指向派生类对象时才是安全的。
cpp 复制代码
#include <iostream>
using namespace std;

class Person
{
protected:
    string _name; // 姓名
    string _sex;  // 性别
    int _age; // 年龄
};
class Student : public Person
{
public:
    int _No; // 学号
};
int main()
{
    Student sobj;
    // 1.子类对象可以赋值给父类对象/指针/引用
    Person pobj = sobj;
    Person* pp = &sobj;
    Person& rp = sobj;

    //2.基类对象不能赋值给派生类对象
    sobj = pobj;

    // 3.基类的指针可以通过强制类型转换赋值给派生类的指针
    pp = &sobj;
    Student * ps1 = (Student*)pp; // 这种情况转换时可以的。
    ps1->_No = 10;

    pp = &pobj;
    Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
    ps2->_No = 10;
    return 0;
}

3.继承中的作用域

在继承中,作用域的规则如下:

  1. 如果派生类中没有与基类成员同名的成员,则可以直接访问基类成员。
  2. 如果派生类中存在与基类成员同名的成员,且未使用作用域解析运算符::,则在派生类范围内,该同名成员会隐藏基类的同名成员,但仍然可以通过作用域解析运算符::来访问基类成员。
  3. 如果派生类中存在与基类成员同名的成员,并且使用了作用域解析运算符::,则可以明确指定访问基类成员。
cpp 复制代码
#include <iostream>
using namespace std;

class Base 
{
public:
    int member;
    void function() 
    {
        cout << "Base function()" << endl;
    }
};

class Derived : public Base 
{
public:
    int member;    // 与基类成员同名的成员
    void function() 
    {
        cout << "Derived function()" << endl;
    }
    void accessMembers() 
    {
        member = 10;             // 访问派生类的成员
        Base::member = 20;       // 访问基类成员
        function();              // 调用派生类的函数
        Base::function();        // 调用基类的函数
    }
};

int main() 
{
    Derived d;
    d.accessMembers();

    return 0;
}
  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。

4.派生类的默认成员函数

派生类的默认成员函数指的是在派生类中自动生成的成员函数。

4.1 默认构造

如果派生类没有显式地定义构造函数,并且基类有默认构造函数,那么派生类会自动生成默认构造函数。默认构造函数会自动调用基类的默认构造函数来初始化继承的成员变量。如果基类没有默认构造函数,或者有但是不可访问(private),则需要在派生类中显式定义构造函数。

编译器会默认先调用父类的构造函数,再调用子类的构造函数。

cpp 复制代码
#include <iostream>
using namespace std;

class Base 
{
public:
    Base() 
    {
        cout << "Base default constructor" << endl;
    }
};

class Derived : public Base 
{
public:
    Derived() 
    {
        cout << "Derived default constructor" << endl;
    }
};

int main() 
{
    Derived d; // 创建派生类对象
    return 0;
}

在这个示例中,派生类Derived继承了基类Base的默认构造函数。当创建Derived类的对象时,首先会调用Base类的默认构造函数,然后再调用Derived类的默认构造函数。因此,输出结果中先打印了Base default constructor,然后打印了Derived default constructor

需要注意的是,如果派生类Derived显式定义了自己的构造函数,而没有显式调用基类的构造函数,那么派生类的默认构造函数将不再隐式继承基类的默认构造函数,需要在派生类的构造函数中显式调用基类的构造函数进行初始化。

4.2 拷贝构造

如果派生类没有显式地定义拷贝构造函数,并且基类有拷贝构造函数,那么派生类会自动生成拷贝构造函数。拷贝构造函数会自动调用基类的拷贝构造函数来拷贝继承的成员变量。

cpp 复制代码
#include <iostream>
using namespace std;

class Base 
{
public:
    Base(int val) 
        : value(val) 
    {
        cout << "Base constructor" << endl;
    }

    Base(const Base& other) 
        : value(other.value) 
    {
        cout << "Base copy constructor" << endl;
    }

    int getValue() const 
    {
        return value;
    }

private:
    int value;
};

class Derived : public Base 
{
public:
    Derived(int val1, int val2) 
        : Base(val1), 
        data(val2) 
    {
        cout << "Derived constructor" << endl;
    }

    Derived(const Derived& other) 
        : Base(other), 
        data(other.data) 
    {
        cout << "Derived copy constructor" << endl;
    }

    int getData() const 
    {
        return data;
    }

private:
    int data;
};

int main() 
{
    Derived d1(10, 20); // 创建派生类对象d1
    Derived d2(d1); // 使用拷贝构造函数创建派生类对象d2

    cout << d1.getValue() << " " << d1.getData() << endl;
    cout << d2.getValue() << " " << d2.getData() << endl;

    return 0;
}

在这个示例中,派生类Derived继承了基类Base的拷贝构造函数。当使用拷贝构造函数创建Derived类的对象时,首先会调用基类Base的拷贝构造函数,然后再调用Derived类的拷贝构造函数。因此,输出结果中先打印了Base copy constructor,然后打印了Derived copy constructor

在派生类Derived的拷贝构造函数中,使用了成员初始化列表(member initializer list)来显式调用基类Base的拷贝构造函数。这样可以确保派生类对象的基类部分也能正确地被复制。同时,派生类还能复制自己的成员数据。

4.3 析构函数

析构函数和构造函数相反,编译器默认先调用子类的析构函数,再调用父类的析构函数。

如果派生类没有显式地定义析构函数,并且基类有析构函数,那么派生类会自动生成析构函数。析构函数会自动调用基类的析构函数来销毁继承的成员变量。

cpp 复制代码
#include <iostream>
using namespace std;

class Base 
{
public:
    Base() 
    {
        cout << "Base constructor" << endl;
    }

    ~Base() 
    {
        cout << "Base destructor" << endl;
    }
};

class Derived : public Base 
{
public:
    Derived()
    {
        cout << "Derived constructor" << endl;
    }

    ~Derived() 
    {
        cout << "Derived destructor" << endl;
    }
};

int main() 
{
    Derived d; // 创建派生类对象d

    return 0;
}

派生类Derived继承了基类Base的析构函数。当派生类对象d被创建时,首先会调用基类Base的构造函数,然后调用派生类Derived的构造函数。当程序结束时,会先调用派生类Derived的析构函数,然后调用基类Base的析构函数。

4.4 赋值运算符

如果派生类没有显式地定义赋值运算符,并且基类有赋值运算符,那么派生类会自动生成赋值运算符。赋值运算符会自动调用基类的赋值运算符来赋值继承的成员变量。

cpp 复制代码
#include <iostream>
using namespace std;
#include <cstring>

class Base 
{
public:
    Base(const char* str = "") 
    {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }

    Base(const Base& other) 
    {
        length = other.length;
        data = new char[length + 1];
        strcpy(data, other.data);
    }

    Base& operator=(const Base& other) 
    {
        if (this != &other) 
        {
            delete[] data;
            length = other.length;
            data = new char[length + 1];
            strcpy(data, other.data);
        }
        return *this;
    }

    ~Base() 
    {
        delete[] data;
    }

protected:
    char* data;
    int length;
};

class Derived : public Base 
{
public:
    Derived(const char* str = "") 
        : Base(str) 
    {}

    Derived& operator=(const Derived& other) 
    {
        if (this != &other) 
        {
            Base::operator=(other);
            // 进行派生类特有的赋值操作
        }
        return *this;
    }
};

int main() 
{
    Derived d1("Hello");
    Derived d2("World");

    d1 = d2;

    return 0;
}

派生类Derived中重载了赋值运算符。首先,派生类中的赋值运算符首先调用基类Base的赋值运算符,以确保基类部分的数据正确赋值。然后,派生类可以进行自己特有的赋值操作。

main函数中,创建了两个Derived对象d1d2,并将d2赋值给d1。在赋值过程中,基类部分的数据会被正确赋值,然后派生类部分可以进行特有的赋值操作。

注意,在赋值运算符中,需要首先判断是否进行自赋值的检查。如果没有进行检查,可能会导致误删除data指针指向的内存,并导致未定义行为。

5. 复杂的菱形继承及菱形虚拟继承

5.1 单继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

5.2 多继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

5.3 菱形继承

菱形继承:菱形继承是多继承的一种特殊情况。

菱形继承是指一个派生类同时继承自两个基类,而这两个基类之间又继承自同一个基类。这样就形成了继承关系中的菱形结构。

cpp 复制代码
class Person
{
public:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _num; //学号
};
class Teacher : public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
int main()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	a._name = "peter";
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
	return 0;
}

5.4 菱形继承

虚拟继承用于解决多重继承中的菱形继承问题以及避免重复继承基类的成员的问题。通过使用虚拟继承,可以确保在继承体系中只有一个实例对象。

虚拟继承是通过在派生类的继承列表中使用 virtual 关键字来实现的。

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和 Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地 方去使用。

cpp 复制代码
class Person
{
public:
	string _name; // 姓名
};
class Student : virtual public Person
{
protected:
	int _num; //学号
};
class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
int main()
{
	Assistant a;
	a._name = "peter";
	return 0;
}
相关推荐
蜀黍@猿5 分钟前
C/C++基础错题归纳
c++
古希腊掌管学习的神12 分钟前
[LeetCode-Python版]相向双指针——611. 有效三角形的个数
开发语言·python·leetcode
赵钰老师13 分钟前
【R语言遥感技术】“R+遥感”的水环境综合评价方法
开发语言·数据分析·r语言
雨中rain19 分钟前
Linux -- 从抢票逻辑理解线程互斥
linux·运维·c++
就爱学编程21 分钟前
重生之我在异世界学编程之C语言小项目:通讯录
c语言·开发语言·数据结构·算法
Oneforlove_twoforjob44 分钟前
【Java基础面试题025】什么是Java的Integer缓存池?
java·开发语言·缓存
emoji1111111 小时前
前端对页面数据进行缓存
开发语言·前端·javascript
每天都要学信号1 小时前
Python(第一天)
开发语言·python
TENET信条1 小时前
day53 第十一章:图论part04
开发语言·c#·图论
生信圆桌1 小时前
【生信圆桌x教程系列】如何安装 seurat V5版本R包,最详细安装手册
开发语言·r语言