学而时习之:C++中的封装&多态

48 阅读7分钟

C++ 中的封装

封装是面向对象编程(OOP)的核心概念之一。其思想是将数据成员和方法绑定成一个整体。

类可以隐藏实现细节,仅对外公开其他类所需的功能。通过将类的数据和方法设为私有,可以在不影响使用该类的代码的前提下,日后随意更改其内部表示或实现。
封装有助于提高代码的可维护性、可读性和易用性;同时,通过对变量赋值进行校验和控制,它还能保持数据的完整性。

1. C++ 中封装的实现方式

  1. 将变量声明为私有(private):把类的数据成员设为私有,使其无法被外部直接访问,从而确保数据被隐藏。
  2. 使用 gettersetter:提供公共的 getter 和 setter 函数,以安全地访问和修改私有变量;这些方法内部可加入校验逻辑,确保只写入合法数据。
  3. 合理运用访问修饰符:数据成员用 private 隐藏信息;向外部提供受控访问的成员函数则用 public
#include <iostream>
#include <string>
using namespace std;

// 表示一个程序员的类
class Programmer
{
  private:
    string name; // 私有变量

  public:
    // 获取私有数据的 getter 方法
    string getName()
    {
        return name;
    }

    // 修改私有数据的 setter 方法
    void setName(string newName)
    {
        name = newName;
    }
};

int main()
{
    Programmer p;
    p.setName("Geek");                        // 设置名字
    cout << "Name=> " << p.getName() << endl; // 获取名字
    return 0;
}
Name=> Geek

解释: 在上面的示例中,我们使用了封装机制,并通过 getter(getName)setter(setName) 方法来访问和修改私有数据。这种封装机制保护了 Programmer 对象的内部状态,并提供了更好的控制和灵活性,来决定如何访问和修改 name 属性。

2.封装的最佳实践

将类的数据设为私有,以隐藏实现细节并降低耦合。使用 gettersetter 函数代替公共字段,以控制访问权限。确保只有合法值才能赋给私有变量。对于不应被外部修改的数据(如 ID),不要提供 setter 方法。

3.封装的优势

  • 数据隐藏:封装限制了对私有类变量的直接访问,防止敏感数据被未授权访问。
  • 提高可维护性:可以在不影响使用类的代码的前提下,更改类的内部实现。
  • 增强安全性:封装允许在 setter 方法中进行验证和控制,防止无效或有害的值。
  • 代码可重用性:封装后的类可以在不同程序中重用,而无需暴露内部细节。
  • 更好的模块化:封装将数据和方法集中在一个类中,促进代码的组织与模块化。

4.封装的劣势

  • 增加代码复杂度:为每个变量编写 getter 和 setter 函数会使代码变长、略为复杂。
  • 性能开销:通过函数访问私有数据而非直接访问,会带来轻微的性能损失。
  • 某些场景下灵活性降低:过度限制访问可能会妨碍其他类高效地扩展或使用该类。

C++ 中的多态性

“多态”一词的含义是“多种形态”。在 C++ 中,多态这一概念可以作用于函数和运算符:同一个函数名在不同情境下可以表现出不同的行为;同样,一个运算符在不同上下文中也可以执行不同的操作。 多态.png

1. 编译期多态(Compile-Time Polymorphism)

又称“早绑定”或“静态多态”。在这种多态中,编译器在编译阶段就根据上下文决定函数或运算符的具体行为。它通过函数重载运算符重载来实现。

A. 函数重载(Function Overloading)

函数重载是面向对象语言的一种特性:两个或多个函数可以同名,但只要它们的参数个数或类型不同,就能表现出不同的行为。这样的函数称为 重载函数 ,因此该机制叫做函数重载。

示例代码

#include <bits/stdc++.h>
using namespace std;

class Geeks {
public:
    // 版本 1:相加两个整数
    void add(int a, int b) {
        cout << "Integer Sum = " << a + b << endl;
    }

    // 版本 2:相加两个浮点数
    void add(double a, double b) {
        cout << "Float Sum = " << a + b << endl;
    }
};

int main() {
    Geeks gfg;

    gfg.add(10, 2);      // 调用整数版本
    gfg.add(5.3, 6.2);   // 调用浮点版本

    return 0;
}
Integer Sum = 12
Float Sum = 11.5

解释:
上面定义了两个同名函数 add(),但参数类型不同:一个接收 int,另一个接收 double。编译器在编译期根据实参类型选择正确的版本,使得同一个函数名可以适用于不同的数据类型。

B. 运算符重载(Operator Overloading)

C++ 允许为特定数据类型给运算符赋予新的含义,这种能力称为运算符重载
例如,加法运算符 + 用于整数时表示相加,用于字符串时则可实现拼接;<<>> 原本只是位左移/右移运算符,但在输入输出流中却被用作流插入/提取运算符,这正是运算符重载的体现。

示例代码

#include <iostream>
using namespace std;

class Complex {
public:
    int real, imag;

    Complex(int r, int i) : real(r), imag(i) {}

    // 重载 '+' 运算符,实现两个复数相加
    Complex operator+(const Complex& obj) {
        return Complex(real + obj.real, imag + obj.imag);
    }
};

int main() {
    Complex c1(10, 5), c2(2, 4);

    // 使用 '+' 运算符将 c1 与 c2 相加
    Complex c3 = c1 + c2;
    cout << c3.real << " + i" << c3.imag;
    return 0;
}
12 + i9

解释: 默认情况下,诸如 +- 等运算符并不支持用户自定义类型,因为它们不知道该对这些类型做什么。通过运算符重载,我们可以为这些运算符定义针对自定义类型的行为,就像上面为 Complex 类重载 + 一样。

注意:在 C++ 中,绝大多数运算符都可以被重载,但少数几个运算符
::(作用域解析)、.(成员访问)、.*(成员指针访问)、?:(条件)、sizeof(大小)
不允许重载,因为它们直接关系到语言的核心语义,为了保持代码清晰和编译器完整性,语言禁止对它们进行重载。

2. 运行时多态(Runtime Polymorphism)

又称“迟绑定”或“动态多态”。与编译期多态相反,函数调用的绑定不是在编译阶段决定,而是在程序运行时才确定。实现手段是虚函数(virtual function)+ 函数覆盖(overriding)

A. 函数覆盖(Function Overriding)

当派生类重新定义了基类的某个成员函数时,就说该基类函数被“覆盖”{也叫:函数重写}。要想产生运行时多态,基类必须把该函数声明为虚函数

示例代码

#include <bits/stdc++.h>
using namespace std;

class Base {
public:
    // 虚函数
    virtual void display() {
        cout << "Base class function";
    }
};

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

int main() {
    Base* basePtr;           // 基类指针
    Derived derivedObj;      // 派生类对象

    basePtr = &derivedObj;   // 让基类指针指向派生类对象

    basePtr->display();      // 运行时决定调用哪个版本
    return 0;
}
Derived class function

解释:基类 Basedisplay() 声明为虚函数,派生类 Derived 对其进行了覆盖。虽然通过基类指针 basePtr 调用 display(),但由于运行时才根据指针实际指向的对象类型(Derived)解析函数地址,最终执行的是派生类的版本,打印出 Derived class function。这就是运行时多态的典型表现。

编译期多态 vs 运行时多态

两者最核心的区别如下:

编译期多态(Compile-Time Polymorphism)运行时多态(Run-Time Polymorphism)
又称静态绑定(static binding)又称动态绑定(dynamic binding)
通过函数重载运算符重载实现通过虚函数 + 函数覆盖实现
由编译器在编译阶段决定调用哪个函数程序运行阶段依据 vtable 动态决议
早绑定,速度更快灵活,但性能略低