14-2 类介绍

在上一章中,我们介绍了结构体(13.7节——结构体、成员及成员选择简介),并探讨了结构体如何将多个成员变量打包成单个对象,使其能够作为整体进行初始化和传递。换言之,结构体为存储和移动相关数据值提供了便捷的封装方式。

请看以下结构体示例:

#include <iostream>

struct Date
{
    int day{};
    int month{};
    int year{};
};

void printDate(const Date& date)
{
    std::cout << date.day << '/' << date.month << '/' << date.year; // assume DMY format
}

int main()
{
    Date date{ 4, 10, 21 }; // initialize using aggregate initialization
    printDate(date);        // can pass entire struct to function

    return 0;
}

在上面的示例中,我们创建了一个Date对象,然后将其传递给一个打印日期的函数。该程序输出:

image

提醒:
在本教程中,我们使用的所有结构体均为聚合体。关于聚合体的详细讨论请参见第13.8课——结构体聚合初始化

尽管结构体功能强大,但其存在若干缺陷,在构建大型复杂程序时(尤其是由多名开发者协作开发的项目)可能带来挑战。


类不变量问题

结构体最大的缺陷或许在于无法有效记录和强制执行类不变量。在第9.6节——assert与static_assert中,我们曾将不变量invariant定义为“某个组件执行期间必须成立的条件”。

在类类型(包括结构体、类和联合体)的语境中,类不变量class invariant是指对象在整个生命周期内必须保持为真的条件,以确保对象始终处于有效状态。违反类不变量的对象被视为处于无效状态invalid state,继续使用该对象可能导致意外unexpected 未定义undefined的行为。

关键见解:
使用违反类不变量的对象可能会导致意外或未定义的行为。

首先,考虑以下结构:

struct Pair
{
    int first {};
    int second {};
};

first 和second 成员可以独立设置为任意值,因此Pair结构体没有不变量。

现在考虑以下几乎相同的结构体:

struct Fraction
{
    int numerator { 0 };
    int denominator { 1 };
};

我们从数学中知道,分母为零的分数在数学上是未定义的(因为分数的值等于其分子除以分母——而除以零在数学上是未定义的)。因此,我们必须确保Fraction对象的分母成员变量绝不能被设置为0。若发生此情况,该Fraction对象将处于无效状态,后续使用该对象可能导致未定义行为。

例如:

#include <iostream>

struct Fraction
{
    int numerator { 0 };
    int denominator { 1 }; // class invariant: should never be 0
};

void printFractionValue(const Fraction& f)
{
     std::cout << f.numerator / f.denominator << '\n';
}

int main()
{
    Fraction f { 5, 0 };   // create a Fraction with a zero denominator
    printFractionValue(f); // cause divide by zero error

    return 0;
}

在上例中,我们通过注释记录了Fraction类的不变量。同时提供了默认成员初始化器,确保用户未提供初始化值时分母默认为1。这保证了当用户决定为Fraction对象赋值初始化时,该对象仍保持有效。这已是一个不错的开端。

但没有任何机制能阻止我们明确违反此类不变量: 创建分数对象f时,我们通过聚合初始化将分母显式设为0。虽然这不会立即引发问题,但对象已处于无效状态,后续使用可能导致意外或未定义行为。

后续调用printFractionValue(f)时,程序因除零错误终止运行,正是这种情况的体现。

顺带一提……
一个小改进是在printFractionValue方法开头添加assert(f.denominator != 0);。这既增强了代码的文档价值,也更清晰地标识了被违反的预设条件。但从行为层面看,这并不会改变实际结果。我们真正需要在问题源头(成员初始化或赋值错误时)捕获这些问题,而非在下游(错误值被使用时)。

鉴于分数示例的相对简单性,避免创建无效的分数对象应该不难实现。然而,在使用大量结构体、具有众多成员的结构体,或成员间存在复杂关系的结构体的复杂代码库中,理解哪些值的组合可能违反某些类不变量就未必那么显而易见了。


更复杂的类不变量

Fraction类的类不变量很简单——分母成员不能为0。这个概念容易理解,避免这种情况也不算太难。

当结构体的成员必须具有相关值时,类不变量就变得更具挑战性了。

#include <string>

struct Employee
{
    std::string name { };
    char firstInitial { }; // should always hold first character of `name` (or `0`)
};

在上述(设计欠佳)的结构体中,成员变量 firstInitial 存储的字符值必须始终与 name 的首字符一致。

当初始化Employee对象时,用户需确保类不变量得到维护。若name被赋予新值,用户还需负责同步更新firstInitial。这种关联性对使用Employee对象的开发者而言可能不够直观,即便意识到也可能被遗忘。

即使编写辅助函数来创建和更新Employee对象(确保firstInitial始终取自name的首字符),我们仍需依赖用户主动调用这些函数。

简言之,依赖对象使用者维护类不变量极易导致问题代码。

核心洞见
依赖对象使用者维护类不变量极易引发问题。

理想情况下,我们希望让类类型具备防弹特性:要么完全避免对象进入无效状态,要么能在状态异常时立即发出信号(而非放任未定义行为在未来某个随机时刻发生)。

结构体(作为聚合体)缺乏以优雅方式解决此问题的机制。


类简介

在开发C++时,Bjarne Stroustrup希望引入能够让开发者创建更直观的程序定义类型的功能。他还致力于为大型复杂程序中常见的陷阱和维护难题(如前文提及的类不变量问题)寻找优雅的解决方案。

借鉴其他编程语言(特别是首个面向对象语言Simula)的经验,Bjarne确信能够开发出通用且功能强大的程序定义类型,几乎适用于任何场景。为致敬Simula,他将这种类型命名为类。

与结构体类似,类是程序定义的复合类型,可包含多种不同类型的成员变量。

关键洞见:
从技术角度看,结构体与类几乎完全相同——因此任何使用结构体实现的示例均可改用类实现,反之亦然。但实践中,我们对结构体和类的用法存在差异。
第14.5节--公有与私有成员及访问限定符 将全面解析结构体与类的技术差异与实践差异。

关联内容:
第14.8节--数据隐藏(封装)的优势 将阐述类如何解决不变量问题。


定义类

由于类是程序定义的数据类型,必须先定义才能使用。类的定义与结构体类似,只是使用 class 关键字代替 struct。例如,以下是一个简单的员工类的定义:

class Employee
{
    int m_id {};
    int m_age {};
    double m_wage {};
};

相关内容:
我们将在后续的第14.5节课程——《公共与私有成员及访问限定符》中探讨为何类成员变量常以“m_”为前缀。
为展示类与结构体的相似性,以下程序与本节开头展示的程序等效,但Date现在是类而非结构体:

#include <iostream>

class Date       // we changed struct to class
{
public:          // and added this line, which is called an access specifier
    int m_day{}; // and added "m_" prefixes to each of the member names
    int m_month{};
    int m_year{};
};

void printDate(const Date& date)
{
    std::cout << date.m_day << '/' << date.m_month << '/' << date.m_year;
}

int main()
{
    Date date{ 4, 10, 21 };
    printDate(date);

    return 0;
}

这将输出:

image

相关内容:
我们将在接下来的第14.5节课程中讲解访问限定符的概念——公共与私有成员及访问限定符。


C++标准库的大部分内容都是类

你可能早已在不知不觉中使用了类对象。无论是std::string还是std::string_view,它们都是以类的形式定义的。事实上,标准库中绝大多数非别名类型都是以类形式定义的!

类是C++的灵魂所在——它们如此基础,以至于C++最初被命名为“带类的C”!一旦熟悉了类,你在C++中的大部分时间都将用于编写、测试和使用它们。


测验时间

问题 #1

给定一组数值(年龄ages、门牌号address numbers等),我们可能需要知道该组中的最小值和最大值。由于最小值和最大值存在关联,我们可以将它们组织成如下结构体:

struct minMax
{
    int min; // holds the minimum value seen so far
    int max; // holds the maximum value seen so far
};

然而,按当前写法,该结构体存在未明确定义的类不变量。该不变量是什么?

显示解答

不变量是 min <= max。若 min 超过 max,使用此结构体的任何代码都可能出现故障。
posted @ 2025-12-26 18:37  游翔  阅读(34)  评论(0)    收藏  举报