15. 面向对象

youncyb 发布于 20 天前 82 次阅读 C++


本章节探讨了面向对象编程(OOP)的基本概念和应用,包括类和对象的定义、继承、多态、封装等核心原则。面向对象编程通过将数据和功能封装在一起,使代码更易于维护、扩展和复用。

1. 基类和派生类

1.1 基类和派生类定义

基类(Base Class):一个类可以作为其他类的基础,提供成员变量和成员函数的定义。通过继承的方式,其他类可以复用基类的功能。

派生类(Derived Class):继承自基类的类,能够访问基类的 publicprotected 成员(但不能访问 private 成员)。派生类可以扩展或重写基类的功能。

类成员类型

  • 公有(public):任何地方都可以访问公有成员,包括类的实例、其他类以及非成员函数。
  • 私有(private):私有成员只能在类的内部访问,不能从类的实例访问。可以通过友元函数和友元类访问。
  • 受保护(protected):受保护成员可以在类的内部以及派生类中访问,但不能在类的实例中访问。可通过派生类对象的友元访问。

基类有两种函数:一种是不希望派生类修改的的函数(如果严格不希望被修改,则可以在方法后添加 final 关键字),一种是希望派生类修改的函数,后者被称之为 “虚函数(Virtual Function)”。虚函数是 C++对象继承时动态绑定的关键。

定义一个基类,如果基类中存在虚函数,则虚函数必须定义,而不仅仅的声明。

// 定义基类
class Base {
public:
    // 基类构造函数
    Base(const std::string& name) : name(name) {
        std::cout << "Base constructor called.\n";
    }

    // 基类的成员函数
    void display() const {
        std::cout << "Base class name: " << name << std::endl;
    }
    
    // 基类的虚函数,需要定义函数体。
    virtual void change_name(const std::string &n)
    {
        name = n;
    }

protected:
    std::string name; // 受保护的成员变量,派生类可以访问
};

定义 Base 的派生类,C++ 11 可以使用 override 指明该函数是覆盖了基类的虚函数

// 定义派生类,继承自Base
class Derived : public Base {
public:
    // 派生类构造函数,调用基类构造函数初始化基类部分
    Derived(const std::string& name, int value) 
        : Base(name), derivedValue(value) {
        std::cout << "Derived constructor called.\n";
    }

    // 派生类的成员函数
    void show() const {
        std::cout << "Derived class name: " << name 
                  << ", value: " << derivedValue << std::endl;
    }
    
    // 派生类的重载虚函数
    void change_name(const std::string &n) override
    {
        name = "Derived" + n;
    }

private:
    int derivedValue; // 派生类特有的成员变量
};

可以注意到,派生类的格式:class Derived : public Base,表明是公有继承。对于继承关系,也分为 3 种:

  • private 继承:class 关键字的默认类型,使用 private 继承后,基类的公有成员和受保护(protected)的成员,在派生类中都变为私有的。如果派生类的派生类想访问 Base 的成员则不可能。
  • protected 继承:将基类的公有成员在派生类中变为受保护成员,无法影响私有成员。
  • public 继承:公有则是公有,受保护则是受保护,私有则是私有。

1.2 动态绑定

定义:动态绑定是指在程序运行时根据实际对象的类型来决定调用哪个函数版本的机制。C++ 中,通过 基类的指针或引用 调用虚函数时,会根据指向的对象的 动态类型(实际类型)来选择对应的函数版本。这种运行时的多态性使得程序更灵活。

实现方式:通过将基类的函数声明为 virtual,使得函数支持动态绑定。

例如下面代码中 Base 指针的对象实际上调用的是派生类的函数:

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

class Derived : public Base {
public:
    void show() const override { // 重写基类的虚函数
        std::cout << "Derived show()" << std::endl;
    }
};

int main() {
    Base* b = new Derived(); // 基类指针指向派生类对象
    b->show(); // 调用 Derived 的 show(),实现动态绑定
    delete b;
    return 0;
}

通过上面代码,我们可以得到:

  • b 的静态类型是 Base*,其在该变量声明时,就已经确定,静态类型不会随着程序的变化而变化。

  • b 的动态类型是 Drived 对象,所以其调用 show 函数,会使用派生类的 show 函数。

1.3 类型转换

通过 1.2 的代码 Base* b = new Derived(); 可以看出,派生类可以向基类进行隐式转化,原因很好理解,派生类中包含了基类的信息,所以可以进行转化。但基类却是无法转化为派生类对象,因为其信息中不包含派生类的信息。如果没有通过指针或引用的方式,直接从派生类转化为基类,则会丢失派生类自身的信息,只保留派生类中基类的信息

Derived d;
Base b = d; // 发生对象切割,b 只包含 Derived 中的 Base 部分

虽然不允许隐式的从基类转换到派生类,但是可以**通过基类的指针或引用指向派生类对象,然后再转换为派生类对象。**此时,有两种方式:

static_cast

  • 用法static_cast<Derived*>(basePtr) 可以将基类指针转换为派生类指针。

  • 风险static_cast 不会进行运行时检查,即使实际对象不是派生类的实例,转换也会成功,但后续使用该指针访问派生类特有的成员时可能会导致未定义行为。

    Base* basePtr = new Derived(); // 基类指针指向派生类对象
    Derived* derivedPtr = static_cast<Derived*>(basePtr); // 静态转换为派生类指针
    derivedPtr->show(); // 安全,basePtr 实际上指向 Derived 对象
    

dynamic_cast

  • 用法dynamic_cast<Derived*>(basePtr) 可以在运行时检查基类指针指向的实际对象类型。如果转换成功,返回派生类的指针;如果失败,返回 nullptr(用于指针转换)或抛出异常(用于引用转换)。

  • 要求:基类必须含有至少一个虚函数(通常是虚析构函数),才能使用 dynamic_cast,因为它需要运行时类型信息(RTTI)。

    Base* basePtr = new Derived(); // 基类指针指向派生类对象
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 动态转换为派生类指针
    
    if (derivedPtr) {
        derivedPtr->show(); // 转换成功,可以安全调用
    } else {
        std::cout << "Conversion failed!" << std::endl; // 转换失败
    }
    

    使用 dynamic_cast 可以确保转换的安全性,因为只有当 basePtr 实际指向 Derived 对象时,转换才会成功。

2. 虚函数与抽象基类

C++的核心思想是多态(polymorphism),表示C++能通过指针或引用调用基类对象中的一个虚函数,直到运行时才会知道调用的是哪个版本,判定的依据是指针或引用在运行时的真实类型。

2.1 虚函数默认实参

虚函数也存在默认实参,当通过基类的指针或引用调用函数,使用的是基类中的定义的默认实参。如下代码所示,输出的是基类中的默认实参。

#include <iostream>

class Base {
public:
    virtual void display(int x = 10) const {
        std::cout << "Base::display, x = " << x << std::endl;
    }
};

class Derived : public Base {
public:
    void display(int x = 20) const override {
        std::cout << "Derived::display, x = " << x << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();
    ptr->display();

    delete ptr;
    return 0;
}

2.2 回避虚函数机制

某些情况下,希望调用基类的虚函数,可以通过域运算符

ptr->Base::display();

2.3 抽象基类

纯虚函数:被赋值为0的虚函数。

抽象基类定义:至少包含一个纯虚函数,无法直接实例化,通常作为派生类的基类,以定义接口。

class AbstractBase {
public:
    virtual void someFunction() = 0; // 纯虚函数
    virtual ~AbstractBase() = default; // 虚析构函数
};

3. 访问控制与类作用域

3.1 派生类向基类的转换与可访问级别

派生类向基类的转换遵循以下规则:

  • 公有继承可以使得派生类的实例对象转换为基类对象
  • 无论何种继承方式,派生类的成员函数和友元都可以将自身对象转换为基类对象。
#include <iostream>

class B {
public:
    void show() const {
        std::cout << "Base class B" << std::endl;
    }
};

class D_public : public B {
    // public继承
public:
    void accessBase() {
        B* basePtr = this; // 合法,public继承允许隐式转换
        basePtr->show();
    }
};

class D_protected : protected B {
    // protected继承
public:
    void accessBase() {
        B* basePtr = this; // 合法,protected继承在成员函数中允许转换
        basePtr->show();
    }
};

class D_private : private B {
    // private继承
public:
    void accessBase() {
        B* basePtr = this; // 合法,private继承在成员函数中允许转换
        basePtr->show();
    }
};

int main() {
    D_public d1;
    d1.accessBase(); // 合法

    D_protected d2;
    d2.accessBase(); // 合法
    // B* basePtr2 = &d2; // 不合法,protected继承在类外部不可访问

    D_private d3;
    d3.accessBase(); // 合法
    // B* basePtr3 = &d3; // 不合法,private继承在类外部不可访问

    return 0;
}

3.2 更改继承成员的访问级别

通过不同的继承类型:私有、公有、受保护,可以改变基类的成员在派生类中的可访问性,例如:使用私有继承,则派生类的派生类或者派生类的实例对象,都无法访问基类的公有成员。

当我们希望基类的某些成员可以被派生类继续派生,或者可以被派生类的实例对象访问,而另一些不希望被继续派生。可以通过using关键字改变继承的成员的访问控制级别,其遵循以下规则:

protected 成员提升为 public,或者将 public 成员降低为 protected,但无法更改 private 成员的访问权限,因为 private 成员在派生类中是不可见的。

#include <iostream>

class Base {
public:
    void publicMethod() {
        std::cout << "Base public method" << std::endl;
    }

protected:
    void protectedMethod() {
        std::cout << "Base protected method" << std::endl;
    }

private:
    void privateMethod() {
        std::cout << "Base private method" << std::endl;
    }
};

class Derived : public Base {
public:
    // 使用 using 将 protectedMethod 提升为 public
    using Base::protectedMethod;

    // 使用 using 将 publicMethod 降低为 protected
protected:
    using Base::publicMethod;
};

int main() {
    Derived obj;

    // 可以访问 protectedMethod,因为它被提升为 public
    obj.protectedMethod();

    // 不能直接访问 publicMethod,因为它被降低为 protected
    // obj.publicMethod(); // 错误:publicMethod 在 Derived 中是 protected

    return 0;
}

3.3 类作用域

派生类的作用域优先于基类的作用域,其符合向上查找规则,派生类同名函数与成员会隐藏基类相同的名字。

派生类新添加的函数无法被基类指针或引用访问,基类指针或引用只可以看到派生类中基类相关的信息。其作用域可见范围实际上与静态类型绑定。

#include <iostream>

class Base {
public:
    virtual void baseFunction() const {
        std::cout << "Base function" << std::endl;
    }
};

class Derived : public Base {
public:
    void baseFunction() const override { // 重写基类方法
        std::cout << "Derived function" << std::endl;
    }

    void derivedFunction() const { // 派生类新增的函数
        std::cout << "Derived specific function" << std::endl;
    }
};

int main() {
    Base* ptr = new Derived(); // 基类指针指向派生类对象

    ptr->baseFunction(); // 调用的是派生类中重写的baseFunction
    // ptr->derivedFunction(); // 错误:基类指针无法访问派生类新增的函数

    delete ptr;
    return 0;
}

如果没有覆盖基类中所有版本的同名函数,那么基类中的其他重载版本函数会被派生类隐藏。通过使用using 关键字,不仅可以使用派生类的func,还可以使用基类的func,不必担心基类func因名字相同而被派生类隐藏。

using Base::func; // 声明基类的func,使得其在子类中可见
void func(int xxx) const override { return xxx; } // 只覆盖基类中func(int)

d.func(123); // 重载版本1 接受整形,Drived
d.func(1.23); // 重载版本2 接受浮点型,Base
d.func("123"); // 重载版本3 接受字符串字面值类型,Base

基类的析构函数必须是虚函数,其原因是:

指针可以根据动态类型,选择对应版本的析构函数。如果不是虚函数,则只会调用基类的析构函数,派生类自身申请的内存和资源对象则得不到释放。

对派生类的析构,会优先析构派生类的资源,其次才会析构基类的资源。

#include <iostream>

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

class Derived : public Base {
public:
    Derived() { 
        data = new int[10]; // 派生类分配资源
        std::cout << "Derived constructor" << std::endl; 
    }
    
    ~Derived() override { 
        delete[] data; // 释放派生类分配的资源
        std::cout << "Derived destructor" << std::endl; 
    }
private:
    int* data;
};

int main() {
    Base* ptr = new Derived(); // 基类指针指向派生类对象
    delete ptr; // 通过基类指针删除对象,调用析构函数
    return 0;
}

如果在在构造函数和析构函数中调用虚函数:

当在构造函数或析构函数中调用虚函数时,编译器会确保调用的是与当前对象的静态类型相对应的虚函数版本。这种行为是由于对象在构造或析构过程中,其多态性机制尚未完全生效。

在构造和析构函数中,虚函数不会表现出多态性,原因如上。

同时C++也规定虚析构函数会阻止编译器自动合成移动操作。

4. 构造函数与拷贝控制

4.1 构造函数

派生类只能继承直接基类的构造函数,无法继承默认构造函数、拷贝构造函数、移动构造函数等。通过上文可知,using关键字可以改变某个名字在当前作用域的可见性,但如果对构造函数使用using,则编译器会在派生类中生成一个与基类相同的构造函数,而不会改变构造函数的访问级别。

#include <iostream>

class Base {
public:
    Base() {
        std::cout << "Base default constructor" << std::endl;
    }
    
    Base(int x) {
        std::cout << "Base parameterized constructor with x = " << x << std::endl;
    }
};

class Derived : public Base {
public:
    using Base::Base; // 继承 Base 的构造函数

    // 派生类可以有自己的构造函数
    Derived(double y) {
        std::cout << "Derived constructor with y = " << y << std::endl;
    }
};

int main() {
    Derived d1;         // 调用 Base 的默认构造函数
    Derived d2(10);     // 调用 Base 的参数化构造函数
    Derived d3(5.5);    // 调用 Derived 的构造函数
    return 0;
}

4.2 拷贝控制

当派生类定义了拷贝构造函数移动构造函数拷贝赋值运算符移动赋值运算符时,这些操作负责处理整个对象,包括基类部分的成员。编译器不会自动生成基类部分的拷贝或移动操作,因此派生类的拷贝或移动操作必须显式地调用基类的相应操作,确保基类的成员得到正确拷贝或移动。

#include <iostream>
#include <string>

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

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

    Base(Base&& other) noexcept : baseData(std::move(other.baseData)) {
        std::cout << "Base move constructor" << std::endl;
    }

    Base& operator=(const Base& other) {
        if (this != &other) {
            baseData = other.baseData;
            std::cout << "Base copy assignment" << std::endl;
        }
        return *this;
    }

    Base& operator=(Base&& other) noexcept {
        if (this != &other) {
            baseData = std::move(other.baseData);
            std::cout << "Base move assignment" << std::endl;
        }
        return *this;
    }

private:
    std::string baseData;
};

class Derived : public Base {
public:
    Derived(const std::string& b, const std::string& d) : Base(b), derivedData(d) {
        std::cout << "Derived constructor" << std::endl;
    }

    // 拷贝构造函数
    Derived(const Derived& other) : Base(other), derivedData(other.derivedData) {
        std::cout << "Derived copy constructor" << std::endl;
    }

    // 移动构造函数
    Derived(Derived&& other) noexcept : Base(std::move(other)), derivedData(std::move(other.derivedData)) {
        std::cout << "Derived move constructor" << std::endl;
    }

    // 拷贝赋值运算符
    Derived& operator=(const Derived& other) {
        if (this != &other) {
            Base::operator=(other); // 调用基类的拷贝赋值运算符
            derivedData = other.derivedData;
            std::cout << "Derived copy assignment" << std::endl;
        }
        return *this;
    }

    // 移动赋值运算符
    Derived& operator=(Derived&& other) noexcept {
        if (this != &other) {
            Base::operator=(std::move(other)); // 调用基类的移动赋值运算符
            derivedData = std::move(other.derivedData);
            std::cout << "Derived move assignment" << std::endl;
        }
        return *this;
    }

private:
    std::string derivedData;
};

int main() {
    Derived d1("base data", "derived data");
    Derived d2 = d1;                // 调用拷贝构造函数
    Derived d3 = std::move(d1);     // 调用移动构造函数

    d2 = d3;                        // 调用拷贝赋值运算符
    d3 = std::move(d2);             // 调用移动赋值运算符

    return 0;
}