自学内容网 自学内容网

解锁C++继承的奥秘:从基础到精妙实践(上)


在这里插入图片描述

前言

继承是C++面向对象编程的核心特性之一,它允许程序员创建层次化的类结构,从而实现代码的重用和扩展。在这篇文章中,我们将深入探讨C++继承的基础概念,包括基类与派生类的关系、多重继承的处理、虚函数与多态的应用,以及如何在复杂系统中有效利用继承来构建可维护且扩展性强的代码架构。通过系统的学习,你将对C++继承有更深入的理解,并能够在实际开发中灵活应用这些知识。


🍭一、继承的定义和方式

在C++中,继承(Inheritance) 是面向对象编程(OOP)中的一个核心概念,它允许一个类(子类或派生类)从另一个类(基类或父类)继承属性和行为(成员变量和成员函数)。通过继承,子类不仅可以复用基类的已有功能,还可以扩展或修改其行为。这种机制大大提高了代码的复用性和扩展性。

1.1 继承的定义

继承使得一个类可以获取另一个类的特性。具体来说,子类会继承基类的成员变量和成员函数,并且可以添加新的成员或修改已有的成员。子类通过继承关系,可以拥有基类的公共和受保护(protected)成员。

语法格式

class 派生类名 : 继承方式 基类名 {
    // 子类的成员和函数
};
  • 派生类名:指子类的名称。
  • 继承方式:指定了继承的访问控制方式,主要有三种:publicprotectedprivate
  • 基类名:指父类的名称。

1.2 继承方式

继承方式基类成员访问权限基类的public成员基类的protected成员基类的private成员
Public继承子类的访问权限publicprotected不可见
Protected继承子类的访问权限protectedprotected不可见
Private继承子类的访问权限privateprivate不可见

说明:

  1. Public继承
    • 基类的public成员在子类中保持为public,可以从外部直接访问。
    • 基类的protected成员在子类中保持为protected,只能在子类内部和其派生类中访问。
    • 基类的private成员对子类不可见,但可以通过基类的publicprotected函数间接访问。
  2. Protected继承
    • 基类的public成员在子类中变为protected,只能在子类及其派生类中访问,外部不可访问。
    • 基类的protected成员保持为protected
    • 基类的private成员对子类不可见。
  3. Private继承
    • 基类的publicprotected成员在子类中都变为private,只能在子类内部访问,外部和其派生类都不可访问。
    • 基类的private成员对子类不可见。

1.3 继承示例

#include <iostream>
using namespace std;

// 基类:Person
class Person {
public:
    string name;
    int age;

    void displayInfo() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

// 派生类:Student,公有继承Person类
class Student : public Person {
public:
    int studentID;

    void displayStudentInfo() {
        cout << "Student ID: " << studentID << endl;
    }
};

int main() {
    // 创建派生类对象
    Student student;
    student.name = "John";  // 可以访问基类的public成员
    student.age = 20;       // 可以访问基类的public成员
    student.studentID = 12345;

    // 调用基类的成员函数
    student.displayInfo();  // 输出: Name: John, Age: 20
    // 调用派生类的成员函数
    student.displayStudentInfo();  // 输出: Student ID: 12345

    return 0;
}

【注意事项】:

  1. 访问权限:继承方式决定了子类对基类成员的访问权限(publicprotectedprivate)。
  2. 构造函数和析构函数:基类的构造函数不会被继承,但可以通过子类的构造函数显式调用。析构函数在子类对象销毁时会自动调用。
  3. 虚函数与多态:继承与虚函数(virtual)配合使用,可以实现运行时多态,这在子类重写基类函数时尤其有用。

🍭二、基类和派生类的赋值转换

在C++中,基类和派生类之间的赋值和转换遵循一些规则和限制,主要涉及到指针引用。当涉及到对象赋值时,我们需要注意对象的静态类型(编译时的类型)和动态类型(运行时的类型),这与继承、多态以及向上和向下转换密切相关。

2.1 向上转换(Upcasting)

向上转换是指把派生类对象的指针或引用赋值给基类的指针或引用。由于派生类继承了基类的所有公开和受保护成员,基类可以“容纳”派生类对象的一部分。向上转换是安全的,且自动进行。

示例:

#include <iostream>
using namespace std;

// 基类:Base
class Base {
public:
    virtual void display() {
        cout << "Base class" << endl;
    }
};

// 派生类:Derived
class Derived : public Base {
public:
    void display() override {
        cout << "Derived class" << endl;
    }
};

int main() {
    Derived derivedObj;

    // 向上转换:派生类对象的指针赋值给基类的指针
    Base* basePtr = &derivedObj;
    basePtr->display();  // 输出: Derived class (如果基类方法是虚函数)

    // 向上转换:派生类对象的引用赋值给基类的引用
    Base& baseRef = derivedObj;
    baseRef.display();  // 输出: Derived class

    return 0;
}

注意

  • 在向上转换中,派生类对象被“视为”基类对象。这意味着通过基类指针或引用访问派生类对象时,无法直接访问派生类中特有的成员函数或属性。
  • 如果基类中的方法使用了虚函数virtual),则在运行时会调用派生类中的重写方法(即多态)。

2.2 向下转换(Downcasting)

向下转换是指将基类的指针或引用转换为派生类的指针或引用。因为派生类通常比基类包含更多的信息,向下转换是有风险的,必须小心使用。C++提供了dynamic_cast来进行安全的向下转换。

示例:

#include <iostream>
using namespace std;

// 基类:Base
class Base {
public:
    virtual void display() {
        cout << "Base class" << endl;
    }
};

// 派生类:Derived
class Derived : public Base {
public:
    void display() override {
        cout << "Derived class" << endl;
    }

    void specialMethod() {
        cout << "Derived class specific method" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();  // 向上转换
    basePtr->display();  // 输出: Derived class(因为基类的display是虚函数)

    // 向下转换:将Base*转换为Derived*
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    if (derivedPtr) {
        derivedPtr->specialMethod();  // 输出: Derived class specific method
    } else {
        cout << "Conversion failed!" << endl;
    }

    delete basePtr;
    return 0;
}

注意

  • 向下转换使用dynamic_cast是安全的,如果转换失败,它会返回nullptr,因此需要检查转换是否成功。
  • 如果基类的指针实际上不是指向派生类对象,强制向下转换将会失败,导致指针变成nullptr。向下转换通常用于启用多态行为,确保基类指针能安全地转换为实际的派生类。

2.3 对象赋值的限制

在C++中,不能直接将派生类对象赋值给基类对象(非指针或引用的对象)。如果这样做,基类只能接收到派生类的基类部分,派生类的其他成员会被“丢弃”。这可能导致意外的行为,因此建议避免使用直接赋值。

示例:

#include <iostream>
using namespace std;

// 基类:Base
class Base {
public:
    int baseVar;
    virtual void display() {
        cout << "Base class" << endl;
    }
};

// 派生类:Derived
class Derived : public Base {
public:
    int derivedVar;
    void display() override {
        cout << "Derived class" << endl;
    }
};

int main() {
    Derived derivedObj;
    Base baseObj = derivedObj;  // 非指针/引用赋值,切割派生类对象

    baseObj.display();  // 输出: Base class

    return 0;
}

注意

  • 在对象赋值中,Base baseObj = derivedObj;会将derivedObj的基类部分赋值给baseObj,这是**切割(slicing)**问题,派生类的特性会丢失。baseObj仅仅是一个Base类的对象。
  • 要避免切割问题,可以使用指针或引用来进行赋值。

🍭三、继承中的作用域

在C++中,继承中的作用域(Scope in Inheritance)涉及基类与派生类之间成员的可见性和访问权限。作用域决定了子类可以访问父类哪些成员,以及如何在子类中访问、覆盖或隐藏基类成员。作用域与访问控制(publicprotectedprivate)密切相关,并且需要考虑成员函数、变量、构造函数等在继承关系中的可见性。

3.1 基类成员的可见性

基类的成员在派生类中的可见性主要受继承方式和成员的访问修饰符控制,下面是对每种修饰符的详细说明:

  • public成员:在public继承中,基类的public成员在派生类中仍然保持为public,可以在派生类的外部和内部访问。如果是protected继承,则这些成员在派生类中变为protected;在private继承中,这些成员在派生类中变为private
  • protected成员protected成员在publicprotected继承中,依旧保持为protected。这意味着这些成员只能在派生类内部或其派生类中访问。在private继承中,基类的protected成员变为private,只能在派生类内部访问。
  • private成员private成员始终不能在派生类中直接访问,只有通过基类的publicprotected方法间接访问。

3.2 名称隐藏(Name Hiding)

C++ 中的派生类可以定义与基类成员同名的成员,这种情况下,派生类的成员会隐藏基类的同名成员。这种行为叫做名称隐藏,它不仅适用于成员变量,还适用于成员函数。

示例代码:

#include <iostream>
using namespace std;

class Base {
public:
    int x = 10;   // 基类成员变量

    void show() { // 基类成员函数
        cout << "Base class show() called" << endl;
    }
};

class Derived : public Base {
public:
    int x = 20;   // 派生类成员变量,隐藏了基类的x

    void show() { // 派生类成员函数,隐藏了基类的show()
        cout << "Derived class show() called" << endl;
    }

    void display() {
        cout << "Derived class x: " << x << endl;  // 输出派生类的x
        cout << "Base class x: " << Base::x << endl;  // 显式调用基类的x
    }
};

int main() {
    Derived d;
    d.show();  // 输出: Derived class show() called
    d.display();  // 输出:
                  // Derived class x: 20
                  // Base class x: 10
    return 0;
}

在上面的例子中,派生类定义了一个名为x的成员变量,它隐藏了基类的同名成员x。在display()函数中,我们通过Base::x来显式访问基类的成员变量。同样,派生类的show()方法隐藏了基类的show()方法。

重要注意点:

  • 要访问被隐藏的基类成员,可以使用作用域解析运算符::),如Base::xBase::show()
  • 如果基类的show()是虚函数(virtual),那么即使派生类定义了同名的show(),也会根据实际对象类型进行动态调用,而不会发生隐藏。

3.3 虚函数与多态中的作用域

如果基类中的函数是虚函数(virtual),那么当在派生类中覆盖该虚函数时,基类的函数不会被隐藏,而是会被动态绑定到派生类对象。在这种情况下,调用派生类对象时,即使是通过基类的指针或引用,也会调用派生类中覆盖的函数。

示例代码:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void show() { // 基类虚函数
        cout << "Base class show() called" << endl;
    }
};

class Derived : public Base {
public:
    void show() override { // 覆盖基类的虚函数
        cout << "Derived class show() called" << endl;
    }
};

int main() {
    Base* basePtr;
    Derived derivedObj;

    basePtr = &derivedObj;
    basePtr->show();  // 输出: Derived class show() called

    return 0;
}

注意

  • 即使使用基类指针basePtr指向派生类对象,在调用show()时,仍然会动态绑定到派生类的show()方法。这是虚函数和多态的行为。
  • 如果基类函数不是虚函数,调用会被静态绑定到基类函数上,而不会调用派生类函数。

3.4 构造函数与析构函数的作用域

  • 构造函数:派生类无法直接调用基类的构造函数,但可以在派生类的构造函数中通过初始化列表显式调用基类构造函数。
  • 析构函数:基类的析构函数如果是虚函数,派生类对象被销毁时会先调用派生类的析构函数,再调用基类的析构函数。这在使用指向基类的指针删除派生类对象时尤为重要。

示例:

#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base constructor called" << endl;
    }
    virtual ~Base() {  // 基类析构函数应该是虚函数
        cout << "Base destructor called" << endl;
    }
};

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

int main() {
    Base* basePtr = new Derived();  // 基类指针指向派生类对象
    delete basePtr;  // 输出:
                     // Derived destructor called
                     // Base destructor called
    return 0;
}

注意

  • 如果基类析构函数不是虚函数,使用基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,这可能会导致内存泄漏或资源未正确释放。

3.5 使用using声明改变作用域

C++允许使用using声明将基类的某些成员引入到派生类中,以便修改其访问权限。例如,基类中的protected成员可以通过using声明在派生类中变为public

示例:

class Base {
protected:
    int protectedVar;
public:
    Base() : protectedVar(5) {}
};

class Derived : public Base {
public:
    using Base::protectedVar;  // 将基类的protected成员变为public
};

int main() {
    Derived d;
    d.protectedVar = 10;  // 可以直接访问protectedVar
    return 0;
}

🍭四、派生类的默认成员函数

在C++中,派生类的默认成员函数是指当你定义一个派生类时,编译器会自动为你生成的一些特殊成员函数。即使你没有显式定义这些函数,编译器也会根据特定规则生成这些默认函数。这些默认成员函数包括:默认构造函数、拷贝构造函数、移动构造函数、赋值运算符、移动赋值运算符、析构函数

这些默认成员函数可以被显式定义或覆盖(尤其是在需要特殊操作时)。下面我们分别讨论每个默认成员函数在派生类中的行为。

4.1 默认构造函数

如果你没有为派生类定义构造函数,编译器会自动生成一个默认的构造函数。这个默认构造函数会首先调用基类的默认构造函数,然后执行派生类中成员变量的默认初始化。

  • 如果基类有默认构造函数,派生类的默认构造函数会隐式调用基类的默认构造函数。
  • 如果基类没有默认构造函数,你必须在派生类的构造函数中显式调用基类的其他构造函数。

示例:

#include <iostream>
using namespace std;

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

class Derived : public Base {
public:
    // 自动生成的默认构造函数
    // Derived() : Base() { }
};

int main() {
    Derived d;  // 输出: Base default constructor
    return 0;
}

注意

  • 如果你在基类中定义了非默认构造函数(带参数),你需要在派生类中显式调用基类的构造函数。

4.2 拷贝构造函数

派生类的默认拷贝构造函数是编译器生成的,当派生类对象被拷贝时,它会首先调用基类的拷贝构造函数,然后依次拷贝派生类中的成员。拷贝构造函数实现的是浅拷贝,即成员逐个复制。

示例:

#include <iostream>
using namespace std;

class Base {
public:
    Base(const Base& b) {
        cout << "Base copy constructor" << endl;
    }
};

class Derived : public Base {
public:
    Derived(const Derived& d) : Base(d) {
        cout << "Derived copy constructor" << endl;
    }
};

int main() {
    Derived d1;
    Derived d2 = d1;  // 输出:
                      // Base copy constructor
                      // Derived copy constructor
    return 0;
}

注意

  • 如果基类的拷贝构造函数被删除或不可用,则派生类的拷贝构造函数也无法使用。
  • 如果派生类定义了自定义拷贝构造函数,必须显式调用基类的拷贝构造函数来确保基类部分被正确复制。

4.3 移动构造函数

如果你没有定义移动构造函数,编译器会为派生类自动生成一个默认的移动构造函数,它会调用基类的移动构造函数并移动派生类的成员。这个函数实现资源转移而非复制,适用于实现高效的资源管理(如动态分配的内存、文件句柄等)。

示例:

#include <iostream>
using namespace std;

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

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

int main() {
    Derived d1;
    Derived d2 = move(d1);  // 输出:
                            // Base move constructor
                            // Derived move constructor
    return 0;
}

注意

  • 如果基类没有定义移动构造函数,基类部分将使用拷贝构造函数,这可能会导致效率问题。

4.4 赋值运算符(拷贝赋值运算符)

编译器会自动生成一个拷贝赋值运算符,当派生类对象被赋值给另一个对象时,拷贝赋值运算符会被调用。这个运算符会依次调用基类的拷贝赋值运算符和派生类中各成员的赋值运算符。

示例:

#include <iostream>
using namespace std;

class Base {
public:
    Base& operator=(const Base& b) {
        cout << "Base copy assignment operator" << endl;
        return *this;
    }
};

class Derived : public Base {
public:
    Derived& operator=(const Derived& d) {
        Base::operator=(d);  // 显式调用基类的赋值运算符
        cout << "Derived copy assignment operator" << endl;
        return *this;
    }
};

int main() {
    Derived d1, d2;
    d1 = d2;  // 输出:
              // Base copy assignment operator
              // Derived copy assignment operator
    return 0;
}

注意

  • 与拷贝构造函数类似,如果基类的赋值运算符不可用,派生类的赋值运算符也会不可用。

4.5 移动赋值运算符

默认情况下,编译器会生成一个移动赋值运算符,用于对象之间的移动赋值。这会调用基类的移动赋值运算符并移动派生类中的成员。

示例:

#include <iostream>
using namespace std;

class Base {
public:
    Base& operator=(Base&& b) {
        cout << "Base move assignment operator" << endl;
        return *this;
    }
};

class Derived : public Base {
public:
    Derived& operator=(Derived&& d) {
        Base::operator=(move(d));  // 显式调用基类的移动赋值运算符
        cout << "Derived move assignment operator" << endl;
        return *this;
    }
};

int main() {
    Derived d1, d2;
    d1 = move(d2);  // 输出:
                    // Base move assignment operator
                    // Derived move assignment operator
    return 0;
}

4.6 析构函数

派生类的默认析构函数是编译器生成的,用于销毁对象。当派生类对象被销毁时,析构函数会首先销毁派生类的成员,然后调用基类的析构函数。如果基类的析构函数是虚函数,派生类的析构函数会自动变成虚函数。

示例:

#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base() {  // 基类析构函数为虚函数
        cout << "Base destructor" << endl;
    }
};

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

int main() {
    Base* basePtr = new Derived();
    delete basePtr;  // 输出:
                     // Derived destructor
                     // Base destructor
    return 0;
}

注意

  • 如果基类析构函数不是虚函数,可能只会调用基类的析构函数,而不会调用派生类的析构函数,导致资源泄漏。

结语

在这里插入图片描述

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!
在这里插入图片描述


原文地址:https://blog.csdn.net/suye050331/article/details/142743210

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!