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对象,然后将其传递给一个打印日期的函数。该程序输出:

提醒:
在本教程中,我们使用的所有结构体均为聚合体。关于聚合体的详细讨论请参见第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;
}
这将输出:

相关内容:
我们将在接下来的第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,使用此结构体的任何代码都可能出现故障。

浙公网安备 33010602011771号