深入浅出C++继承:从基础到实践

Posted 泡沫o0

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入浅出C++继承:从基础到实践相关的知识,希望对你有一定的参考价值。

深入浅出C++继承:从基础到实践

一、引言

1. 面向对象编程简介

面向对象编程(Object-Oriented Programming,OOP)是一种编程范式,它以对象作为基本单位,通过对象之间的交互来完成各种任务。在面向对象编程中,我们将数据和操作数据的方法封装到一个实体(对象)中,从而简化了代码的组织和复用。面向对象编程的核心概念包括类(Class)、对象(Object)、封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。

2. 继承在面向对象编程中的重要性

继承是面向对象编程的一个关键特性,它允许创建一个新的类(派生类),继承一个现有类(基类)的属性和方法。继承的主要优势在于代码重用和模块化,它可以减少重复代码,降低系统复杂性,并提高代码的可维护性和扩展性。


通过继承,派生类可以继承基类的数据成员和成员函数,同时可以添加新的数据成员和成员函数,以实现特定的功能。继承还有助于实现抽象层次的设计,从而使得软件的架构更加清晰。此外,继承也为实现多态提供了基础,多态允许我们使用基类指针或引用来操作派生类对象,提高了代码的灵活性。


继承呈现了面向对象程序设计的层析结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用。
被继承的类称为父类或基类,继承的类称为子类或派生类。“子类”和“父类”通常放在一起称呼,“基类”和“派生类”通常放在一起称呼。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class).

本文将深入探讨C++中继承的相关概念、用法和技巧,帮助读者更好地理解和应用面向对象编程的核心技术。
全文将从以下几个方面展开:

  • C++继承基础:了解类的定义与实例化、继承的基本概念(包括基类与派生类、单继承与多继承)、访问修饰符以及构造函数与析构函数在继承中的行为。
  • 派生类的特点:探讨成员函数重载、成员函数覆盖以及内存布局等方面的内容。
  • 虚函数与多态:介绍虚函数的概念与作用,以及多态的实现原理(包括动态绑定和虚函数表)和纯虚函数与抽象类。
  • C++11中的继承增强:探讨C++11标准中关于继承的新特性,如委托构造函数、显示覆盖和继承指示符、继承构造函数以及继承与类型推导。
  • 多继承与虚拟继承:深入了解多继承的概念与应用场景、虚拟继承的原理,以及菱形继承问题及其解决方法。
  • 实战:设计模式中的继承与组合:通过策略模式、模板方法模式和组合模式等实例,展示继承在实际开发中的应用。

二、C++继承基础

1. 类的定义与实例化

在C++中,类是一种自定义的数据类型,它封装了数据成员(属性)和成员函数(方法)。类的定义使用关键字class,后面跟类名和类体,类体包含在一对大括号中。类的实例化是通过创建类的对象来实现的。
例如:

class Animal 
public:
    void eat() 
        // ...
    
;

Animal cat;

2. 继承的基本概念

a. 基类与派生类

在C++中,继承关系涉及到两种类型的类:基类和派生类。基类是已经定义好的类,派生类则是通过继承基类而创建的新类。派生类继承了基类的属性和方法,并可以根据需求添加新的属性和方法。继承使用冒号:表示,后面跟访问修饰符和基类名。例如:

class Mammal : public Animal 
    // ...
;

b. 单继承与多继承

C++支持单继承和多继承。单继承是指派生类只继承一个基类,而多继承是指派生类继承多个基类。多继承的基类之间使用逗号分隔。例如:

class Dog : public Mammal 
    // ...
;
class FlyingMammal : public Mammal, public Bird 
    // ...
;

3. 访问修饰符

访问修饰符用于控制类的成员的访问权限。C++有三种访问修饰符:public、protected和private。

a. public

public访问修饰符表示类的成员(数据成员和成员函数)可以在类的内部、派生类和类外部访问。在继承时,使用public继承意味着基类的public成员在派生类中也是public的。

b. protected

protected访问修饰符表示类的成员只能在类的内部和派生类中访问,但不能在类外部访问。在继承时,使用protected继承意味着基类的public和protected成员在派生类中都是protected的。

c. private

private访问修饰符表示类的成员只能在类的内部访问,不能在派生类和类外部访问。在继承时,使用private继承意味着基类的public和protected成员在派生类中都是private的。

三种继承方式的差别

public继承方式

  • 基类中所有 public 成员在派生类中为 public 属性;
  • 基类中所有 protected 成员在派生类中为 protected 属性;
  • 基类中所有 private 成员在派生类中不能使用。

protected继承方式

  • 基类中的所有 public 成员在派生类中为 protected 属性;
  • 基类中的所有 protected 成员在派生类中为 protected 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

private继承方式

  • 基类中的所有 public 成员在派生类中均为 private 属性;
  • 基类中的所有 protected 成员在派生类中均为 private 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

说明

基类private成员在派生类中无论以什么方式继承都是不可见(但会被继承,占用内存)的。
基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接访问,但需要在派生类中能访问,就定义为protected。
使用关键字class时默认的继承方式是private,使用struct的默认继承方式是public,不过最好显示地写出继承方式。
在实际运用中一般都使用的是public继承,几乎很少去使用protected/private继承.


4. 构造函数与析构函数在继承中的行为

  • 构造函数和析构函数在继承中具有特殊的行为。
    当创建一个派生类对象时,基类的构造函数会先于派生类的构造函数被调用,以确保基类成员被正确地初始化,构造函数的执行顺序和继承顺序相同。
  • 同样,析构函数的调用顺序是相反的:派生类的析构函数先于基类的析构函数被调用。这样可以确保资源在释放时遵循逆序的原则。
  • 在虚继承中,虚基类是由最终的派生类初始化的,换句话说,最终派生类的构造函数必须要调用虚基类的构造函数。

在C++中,当创建派生类对象时,基类的构造函数会被自动调用,先于派生类的构造函数执行。基类的析构函数也会被自动调用,但执行顺序与构造函数相反,即派生类的析构函数先执行,然后才是基类的析构函数。如果需要在派生类构造函数中显式调用基类的构造函数,可以在初始化列表中指定基类构造函数。例如:

class Mammal : public Animal 
public:
   Mammal() : Animal() 
       // ...
   
;

class Dog : public Mammal 
public:
   Dog() : Mammal() 
       // ...
   
;

需要注意的是,当派生类析构函数执行完毕后,基类的析构函数会自动被调用,因此不需要在派生类析构函数中显式调用基类析构函数.
若派生类没有显式地调用基类的构造函数,编译器会自动调用基类的默认构造函数。如果基类没有默认构造函数,派生类必须在其构造函数的初始化列表中显式地调用基类的构造函数。例如:

class Animal 
public:
   Animal() 
       // ...
   
;

class Mammal : public Animal 
public:
   Mammal() : Animal() 
       // ...
   
;

5. 继承的语法

/**          普通继承       **/
//单继承
class 派生类名:[继承方式] 基类名
    派生类新增加的成员
;
//多继承
class D: public A, private B, protected C
    //类D新增加的成员
;
/**          虚拟继承       **/
class 派生类名: virtual[继承方式] 基类名
    派生类新增加的成员
;

三、派生类的特点

1. 成员函数重载(Function Overloading)

成员函数重载是指在同一个类中定义多个同名的成员函数,但它们的参数类型或参数个数不同。这允许我们使用相同的函数名来处理不同的参数类型或参数个数,从而简化代码。派生类可以重载基类的成员函数,但要注意参数类型或参数个数必须有所不同。例如:

class Animal 
public:
   void makeSound() 
       // ...
   
;

class Dog : public Animal 
public:
   void makeSound(int times) 
       for (int i = 0; i < times; ++i) 
           // ...
       
   
;



2. 成员函数覆盖(Function Overriding)

成员函数覆盖是指派生类中定义与基类中同名且具有相同参数列表的成员函数。这允许派生类修改或扩展基类成员函数的行为。当派生类对象调用覆盖的成员函数时,将执行派生类中的实现,而不是基类中的实现。例如:

class Animal 
public:
   void makeSound() 
       // ...
   
;

class Dog : public Animal 
public:
   void makeSound() 
       // Dog-specific sound
   
;


3. 名字遮蔽(Name Hiding)

是指在派生类中定义了一个和基类中同名的成员函数(无论是重载还是覆盖),这个函数将遮蔽(隐藏)基类中同名的函数。
所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。
继承中遮蔽不考虑重载的情况,即遮蔽只检测函数名不检测参数.
在派生类中,如果要访问基类中被遮蔽的函数,可以使用作用域解析运算符“::”来显式指定调用基类中的函数。名字遮蔽只发生在派生类中。
以下是一个 C++ 的名字遮蔽示例:

#include <iostream>

using namespace std;

class Base 
public:
    void func() 
        cout << "This is the Base class function." << endl;
    
;

class Derived : public Base 
public:
    void func(int x)    // 派生类中定义了一个同名的成员函数,遮蔽了基类中的 func 函数
        cout << "This is the Derived class function. Parameter: " << x << endl;
    
;

int main() 
    Derived d;
    // 通过派生类的对象调用 func 函数
    d.func(10);  // 输出:This is the Derived class function. Parameter: 10

    // 通过派生类的对象调用基类中的 func 函数
    d.Base::func();  // 输出:This is the Base class function.

    return 0;


在上面的示例中,派生类 Derived 中定义了一个同名的成员函数 func,该函数的参数列表不同于基类 Base 中的 func 函数。因此,派生类中的函数遮蔽了基类中的同名函数。在 main 函数中,我们通过派生类的对象 d 调用 func 函数,将会调用派生类中的实现。如果我们要调用基类中被遮蔽的函数,可以使用作用域解析运算符 :: 显式指定调用基类中的函数,如 d.Base::func()。


4. 内存布局

在C++中,派生类对象的内存布局是由基类成员和派生类成员共同组成的。基类成员位于派生类成员之前,这保证了基类构造函数可以正确初始化基类成员。了解内存布局对于理解构造函数和析构函数的调用顺序以及如何在派生类中访问基类成员非常重要。例如:

class Animal 
public:
   int age;
;

class Dog : public Animal 
public:
   int weight;
;

Dog dog;

在这个例子中,Dog对象的内存布局是先有Animal类的成员(age),然后是Dog类的成员(weight)。因此,Animal类的构造函数会先于Dog类的构造函数被调用,确保基类成员得到正确的初始化。同样,析构函数的调用顺序是相反的,派生类的析构函数先于基类的析构函数被调用,以确保资源按照逆序释放。


5.基类和派生类之间的作用域关系

  • 派生类可以访问基类的公有成员和保护成员,但不能访问基类的私有成员。
  • 派生类可以继承基类的公有成员和保护成员,但不会继承基类的私有成员。
  • 派生类可以定义与基类同名的成员函数或成员变量,这会导致基类的同名成员被遮蔽。此时,如果需要访问基类的同名成员,可以使用作用域解析运算符 :: 来指定要访问的成员。
  • 派生类可以重载基类的成员函数,即在派生类中定义一个与基类同名但参数列表不同的函数。此时,派生类中的函数会隐藏基类中的同名函数,但如果需要访问基类中的同名函数,仍然可以使用作用域解析运算符 :: 来指定要访问的成员。
  • 派生类可以覆盖基类的虚函数,即在派生类中定义一个与基类同名、参数列表和类型都相同的虚函数。此时,当通过基类指针或引用调用该函数时,实际调用的是派生类中的虚函数,而不是基类中的虚函数。


总的来说,派生类可以在继承基类的同时,增加自己的成员和方法,并且可以重载基类的成员函数和覆盖基类的虚函数。但需要注意的是,派生类不能直接访问基类的私有成员,但可以通过公有/保护成员函数来访问。


四、虚函数与多态

1. 虚函数的概念与作用

虚函数是基类中用关键字virtual声明的成员函数,它允许派生类覆盖该函数的实现。虚函数的主要作用是实现多态,即允许基类的指针或引用指向派生类对象,并在运行时确定调用哪个类的成员函数实现。这增强了代码的灵活性和可扩展性。

class Animal 
public:
   virtual void makeSound() 
       // ...
   
;

class Dog : public Animal 
public:
   void makeSound() 
// Dog-specific sound

;

class Cat : public Animal 
public:
void makeSound() 
// Cat-specific sound

;

2. 多态的实现

a. 动态绑定

动态绑定是指在运行时根据对象的实际类型确定调用哪个成员函数实现。
这使得我们可以使用基类的指针或引用来操作派生类对象,并根据对象的实际类型执行相应的成员函数。例如:

Animal* animal = new Dog();
animal->makeSound(); // Calls Dog's implementation

animal = new Cat();
animal->makeSound(); // Calls Cat's implementation

b. 虚函数表

虚函数表(Virtual Function Table,vtable)是一种用于实现动态绑定的技术。每个包含虚函数的类都有一个与之关联的虚函数表,表中存储了类的虚函数指针。当我们通过基类的指针或引用调用虚函数时,编译器会根据对象的实际类型查找虚函数表,并调用相应的虚函数实现。


3. 纯虚函数与抽象类

纯虚函数是用关键字virtual声明的,并在其声明后添加= 0的成员函数。纯虚函数没有实现,用于指定派生类必须实现的接口。包含纯虚函数的类被称为抽象类,抽象类不能实例化,只能作为基类。派生类必须实现抽象类中的所有纯虚函数,否则派生类也将成为抽象类。例如:

class Animal 
public:
   virtual void makeSound() = 0;
;

class Dog : public Animal 
public:
   void makeSound() 
       // Dog-specific sound
   
;

class Cat : public Animal 
public:
   void makeSound() 
       // Cat-specific sound
   
;

五、C++11中的继承增强

1. 委托构造函数

C++11引入了委托构造函数,允许一个构造函数调用同类中的其他构造函数,从而避免代码重复。委托构造函数使用构造函数的初始化列表进行调用。例如:

class Animal 
public:
   Animal() : Animal(0) 
       // ...
   

   Animal(int age) : age(age) 
       // ...
   

private:
   int age;
;

2. 显示覆盖和继承指示符

C++11引入了两个新的指示符:override和final。override用于显示指示派生类中的函数覆盖了基类中的虚函数,有助于提高代码的可读性和编译时错误检查。

class Animal 
public:
   virtual void makeSound() 
       // ...
   
;

class Dog : public Animal 
public:
   void makeSound() override 
       // Dog-specific sound
   
;

final指示符可以用于类或虚函数,表明类不能被继承,或虚函数不能被派生类覆盖。

class Animal 
public:
   virtual void makeSound() final 
       // ...
   
;

class Dog : public Animal 
   // Cannot override makeSound() because it is marked as final
;


3. 继承构造函数

C++11支持继承构造函数,允许派生类继承基类的构造函数,从而简化派生类的构造函数定义。使用关键字using来声明继承构造函数。

class Animal 
public:
   Animal() 
       // ...
   

   Animal(int age) : age(age) 
       // ...
   

private:
   int age;
;

class Dog : public Animal 
public:
   using Animal::Animal; // Inherit constructors from Animal
;

4. 继承与类型推导

C++11引入了类型推导(auto和decltype),使得编写泛型代码更加简洁。继承机制与类型推导可以结合使用,从而简化派生类的实现。例如,派生类可以使用auto关键字根据基类成员函数的返回类型自动推导返回类型。例如,假设我们有一个基类Container,它有一个成员函数size()返回容器中元素的个数。派生类可以使用auto关键字根据基类成员函数的返回类型自动推导返回类型。

class Container 
public:
   std::size_t size() const 
       // ...
   
;

class DerivedContainer : public Container 
public:
   auto getSize() const 
       return size(); // The return type is automatically deduced as std::size_t
   
;

此外,C++11引入的decltype关键字可以用于根据表达式的类型推导出类型。结合继承,我们可以用decltype来定义派生类中与基类成员函数相关的类型别名或变量类型。

class Container 
public:
   using SizeType = std::size_t;

   SizeType size() const 
       // ...
   
;

class DerivedContainer : public Container 
public:
   decltype(size()) getSize() const 
       return size(); // The return type is automatically deduced as Container::SizeType (std::size_t)
   
;

通过结合继承和类型推导,我们可以在派生类中更简洁地使用和扩展基类的功能,提高代码的可读性和可维护性。


六、多继承与虚拟继承

1.多继承的概念与应用场景

多继承是指一个类可以从多个基类派生而来,从而继承多个基类的特征和行为。多继承可以让派生类继承并组合多个基类的功能,提高代码的复用性和灵活性。然而,多继承也可能引入一些问题,如菱形继承问题。因此,在使用多继承时,需要谨慎设计类的层次结构。例如:


class A 
    // ...
;

class B 
    // ...
;

class C : 
        
                

前言

博主通过对C++基础知识的总结,有望写出深入浅出的C++基础教程专栏,并分享给大家阅读,今后的一段时间我将持续更新C++入门系列博文,想学习C++的朋友可以关注我,希望大家有所收获。

6 继承

6.1 继承的基本概念

一个类中包含了若干成员属性和成员函数,不同的类中的成员属性和成员函数各不相同,但有些类与类之间存在特殊的关系,它们所包含的内容有相同之处。

如图1所示为类与类之间的关系。

图1 类与类之间的关系

继承就是利用已存在的类来建立一个新类。已存在的类称为父类或基类,新建立的类称为子类或派生类。子类通过继承的方式可以获得父类中的成员属性和成员函数,同时可以对自己的成员做必要的调整。

总结:

1)一个新的子类从已有的父类获得已有的特性,称为类的继承;从已有的父类产生一个新的子类的过程,称为类的派生。

2)一个基类可以派生出多个派生类,每一个派生类又可以作为基类再派生出新的派生类,因此,基类和派生类是相对而言的。

3)派生类是基类的具体化,基类是派生类的抽象。

语法:class 子类 : 继承方式 父类

例如:class son:public Base

6.2 继承的方式

继承有三种方式:公共继承、保护继承、私有继承。

子类以不同的方式继承父类中的成员属性,则子类中成员属性的访问权限也有差异。

公共继承

父类中的公共权限成员到子类中依然是公共权限;父类中的保护权限成员到子类中依然是保护权限;父类中的私有权限成员,子类访问不到。

保护继承

父类中的公共权限成员到子类中变为保护权限;父类中的保护权限成员到子类中变为保护权限;父类中的私有权限成员,子类访问不到。

私有继承

父类中的公共权限成员到子类中变为私有权限;父类中的保护权限成员到子类中变为私有权限;父类中的私有权限成员,子类访问不到。

图2 继承方式

6.3 继承中的对象模型

父类中所有非静态成员属性都会被子类继承下去;父类中私有成员属性访问不到的原因是被编译器隐藏了,但它确实会被子类继承下去。

class Base
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};

class son :public Base
{
public:
	int m_D;
};

void test()
{
	// 父类中所有非静态成员属性都会被子类继承下去
	// 父类中私有成员属性访问不到的原因是被编译器隐藏了,但它确实会被子类继承下去
	cout << sizeof(son) << endl;  // 输出的son的大小为16个字节
}

利用开发人员命令提示工具查看对象模型:

跳转盘符 F:   跳转文件路径  cd 具体路径下   查看命名  cl /d1 reportSingleClassLayout类名 文件名

如图3所示为演示结果。

图3 对象模型

6.4 继承中的构造函数和析构函数的调用顺序

继承中先调用父类构造函数,再调用子类构造函数,析构函数的调用顺序与构造函数相反。

class Base
{
public:
	Base()
	{
		cout << "Base构造函数的调用" << endl;
	}
	~Base()
	{
		cout << "Base析构函数的调用" << endl;
	}
};

class Son :public Base
{
public:
	Son()
	{
		cout << "Son构造函数的调用" << endl;
	}
	~Son()
	{
		cout << "Son析构函数的调用" << endl;
	}
};

void test()
{
	Son s;
}

6.5 同名成员处理方式

1)子类对象可以直接访问到子类的同名成员;

2)子类对象需要加作用域才可以访问到父类的同名成员;

3)当子类与父类拥有同名的成员函数时,子类会隐藏父类中的同名成员函数,加作用域才可以访问到父类中的同名函数。

4)同名静态成员处理方式与同名非静态成员处理方式一样,只是在访问方式上有所不同,不仅可以利用对象访问成员,还可以利用类名来访问成员。

7 多态

7.1 多态的基本概念

多态分为两类:

静态多态:函数重载和运算符重载。

动态多态:派生类和虚函数实现运行时多态。

区别:
静态多态的函数地址早绑定——编译阶段确定函数地址;动态多态的函数地址晚绑定——运行阶段确定函数地址。

总结:

多态满足条件:1)有继承关系;2)子类重写父类中的虚函数。

多态使用条件:父类指针或引用指向子类对象。

例7.1
class Animal
{
public:
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
};

class Cat :public Animal
{
public:
	virtual void speak()
	{
		cout << "小猫在说话" << endl;
	}
};

class Dog :public Animal
{
public:
	virtual void speak()
	{
		cout << "小狗在说话" << endl;
	}
};

void Dospeak(Animal & animal)   // 父类引用指向子类对象
{
	animal.speak();
}

void test()
{
	Cat cat;
	Dospeak(cat);
	Dog dog;
	Dospeak(dog);
}

加上virtual关键字后,speak就变成了虚函数,此时编译器就无法在编译阶段确定函数地址,只能在运行阶段确定函数地址,实现函数的调用。

7.2 多态的原理剖析

以例7.1中的程序对多态的原理进行分析,Animal类的内部结构如图4所示。

图4 Animal类的内部结构

当Cat类没有重写父类中的虚函数时,会继承Animal类中的成员函数。其内部结构如图5所示。

图5 Cat类的内部结构

当子类重写父类中的虚函数, 子类中的虚函数表内部会把继承的虚函数地址替换成子类的虚函数地址,其内部结构如图6所示,当父类的指针或引用指向子类对象时,就可以发生多态。

图6 Cat类的内部结构

7.3 纯虚函数和抽象类

纯虚函数语法:virtual 返回值类型 函数名(参数列表)=0;

当类中有了纯虚函数,则称该类为抽象类。

抽象类无法实例化对象;子类必须重写父类中的纯虚函数,否则也属于抽象类。

class Base
{
public:
	virtual void func() = 0;
};
class Son :public Base
{
public:
	virtual void func()
	{
		cout << "func的调用!" << endl;
	}
};

void test()
{
	// 抽象类无法实例化对象
	Base b;   // 错误
	new Base; // 错误

	Base *b = new Son;
	b->func();
}

7.4 虚析构和纯虚析构

问题:父类指针在析构时,不会调用子类的析构函数,如果子类有堆区属性,将导致内存泄露。利用虚析构和纯虚析构均可以解决这个问题。

虚析构和纯虚析构的共性:1)可以解决父类指针释放子类对象;2)都需要有具体的函数实现。

虚析构和纯虚析构的区别:如果是纯虚析构,该类属于抽象类,无法实例化对象。

    虚析构语法:
                virtual ~类名(){}
    纯虚析构语法:    
         声明       virtual ~类名()=0;
         实现       类名::~类名(){}

总结:

1)虚析构和纯虚析构就是解决父类指针释放子类对象的问题;

2)如果子类中没有堆区数据,可以不写虚析构或纯虚析构;

3)拥有纯虚析构函数的类属于抽象类,纯虚析构需要声明也需要实现。

结束语

大家的点赞和关注是博主最大的动力,博主所有博文中的代码文件都可分享给您(除了少量付费资源),如果您想要获取博文中的完整代码文件,可通过C币或积分下载,没有C币或积分的朋友可在关注、点赞和评论博文后,私信发送您的邮箱,我会在第一时间发送给您。博主后面会有更多的分享,敬请关注哦!

以上是关于深入浅出C++继承:从基础到实践的主要内容,如果未能解决你的问题,请参考以下文章

简单的自我介绍

C到C++的升级

C++基础——C++面向对象之类对象与继承基础总结(类和对象概念构造函数与析构函数this指针继承)

Linux下跨语言调用C++实践

《C++语言基础》实践參考——数组作数据成员

最新110G超强C语言和C++编程0基础从入门到精通自学教程免费送!!!