需要预先学习:


1. 封装

封装(encapsulation)有两层含义:

  1. 把数据(成员变量)和操作数据的行为(成员函数)打包到类中
  2. 通过访问权限限定符控制谁能访问这些成员,把实现细节藏起来

在 类与对象 里,成员统一用了 public,任何代码都能直接读写成员变量。这带来一个问题:如果有人给 m_age 赋了 -1,程序照单全收,对象就处于“年龄是负数”的非法状态。封装要做的,就是收起成员的直接访问权,只通过类提供的成员函数来操作数据,由类自己守住数据的合法性。


1.1 访问权限限定符

C++ 提供三种访问权限限定符,用它们把类体分成几个区域:

限定符 类内部 类外部
public 可访问 可访问
private 可访问 不可访问
protected 可访问 不可访问

private 与 protected 在"类外部不可访问"上是一致的,区别在于继承时派生类能否访问(详见 继承)。本节统一用 private 表示私有成员。

#include <iostream>
#include <string>

class Person
{
public:                          // 公开接口
    int get_age() const
    {
        return m_age;            // 成员函数可以访问私有成员
    }

private:                         // 私有数据
    std::string m_name;
    int m_age = 0;
};

int main()
{
    Person zhangsan;
    std::cout << zhangsan.m_age << std::endl;   // 错误!类外不能访问私有成员

    return 0;
}

编译时报错:

error: 'int Person::m_age' is private within this context

规则:

  • 成员函数无论写在类内的哪个区域,都可以访问类的所有成员
  • 类外部(如 main 中)只能访问 public 成员
  • class 的成员默认是 private,struct 的成员默认是 public(见下文"struct 与 class")

1.2 通过公开接口操作私有数据

私有成员变量不可直接访问,于是类对外提供一对公开的成员函数来读写它们:

  • getter:读取成员变量的值,通常形如 get_age(),声明为 const
  • setter:修改成员变量的值,通常形如 set_age(),可以在其中校验新值的合法性
#include <iostream>
#include <string>

class Person
{
public:
    void set_age(int age)        // setter:校验后才写入
    {
        if (age >= 0)
            m_age = age;
        else
            std::cout << "年龄不合法:" << age << std::endl;
    }

    int get_age() const          // getter:只读
    {
        return m_age;
    }

private:
    std::string m_name;
    int m_age = 0;
};

int main()
{
    Person zhangsan;
    zhangsan.set_age(20);
    std::cout << zhangsan.get_age() << std::endl;
    zhangsan.set_age(-1);        // 非法值被拦截
    std::cout << zhangsan.get_age() << std::endl;

    return 0;
}

编译并运行代码后,看到这样的输出:

20
年龄不合法:-1
20

可以看到,-1 被 setter 拦了下来,m_age 始终保持合法。如果不封装,m_age 被直接写成 -1,没人能拦住。


1.3 封装带来的好处

1. 保证数据处于合法状态

数据只能通过 setter 写入,非法值在入口处被拦截,对象不会出现"年龄为负"这类状态。

2. 实现细节可以自由替换

使用者只依赖公开接口,不关心内部如何实现。只要接口不变,类的内部可以随意改造而不影响任何使用者:

class Thermometer
{
public:
    double get_celsius() const { return m_t; }

private:
    double m_t = 0.0;            // 内部存的是摄氏温度
};

如果某天想把内部存储换成开尔文温度,只需改动类内部的几行代码,get_celsius() 的调用方一行都不用动:

class Thermometer
{
public:
    double get_celsius() const { return m_t - 273.15; }   // 接口不变,内部换算

private:
    double m_t = 273.15;         // 内部改存开尔文温度
};

3. 降低耦合

使用方的代码只和公开接口打交道,不会碰内部细节,类的改动不会波及使用方,程序的各个部分更容易独立修改和维护。


1.4 struct 与 class

在 C++ 里,struct 和 class 几乎是同一个东西——struct 同样可以有成员函数、构造函数和继承。唯一的区别是默认访问权限:

类型 默认访问权限
struct public
class private
struct Point
{
    int m_x;        // 默认 public,外部可直接访问
    int m_y;
};

class Point2
{
    int m_x;        // 默认 private,外部不可访问
    int m_y;
};

约定俗成的用法:struct 用于简单的数据打包(成员大多是公开的),class 用于需要封装的完整类型。