在本章中,我们深入探讨了 C++ 的核心——类!这是本教程中最重要的一个章节,因为它为后续内容奠定了基础。
章节回顾
在过程式编程中,重点在于创建“过程”(在 C++ 中称为函数),以实现程序逻辑。我们将数据对象传递给这些函数,这些函数对数据执行操作,然后可能会返回一个结果供调用者使用。
而在面向对象编程(通常缩写为 OOP)中,重点在于创建包含属性和一组明确定义行为的程序定义数据类型。
类不变式是一个条件,它必须在整个对象生命周期内为真,以使对象保持有效状态。如果对象的类不变式被破坏,则该对象处于无效状态,进一步使用该对象可能会导致意外或未定义行为。
类是一种程序定义的复合类型,它将数据和操作这些数据的函数捆绑在一起。
属于类类型的函数称为成员函数。成员函数所作用的对象通常称为隐式对象。非成员函数则用来区分成员函数。如果类类型没有数据成员,建议使用命名空间。
const
成员函数是一种保证不会修改对象或调用任何非 const
成员函数(因为它们可能会修改对象)的成员函数。如果一个成员函数不会(且永远不会)修改对象的状态,则应将其声明为 const
,以便它可以在非 const
和 const
对象上调用。
类类型的每个成员都有一个称为访问级别的属性,它决定了谁可以访问该成员。访问级别系统有时非正式地称为访问控制。访问级别是按类定义的,而不是按对象定义的。
公有成员是类类型的成员,它们没有任何访问限制。公有成员可以被任何人访问(只要它们在作用域内)。这包括同一类的其他成员。公有成员也可以被类外的代码访问,我们称这类代码为公共代码。公共代码的示例包括非成员函数以及其他类类型的成员。
默认情况下,结构体的所有成员都是公有的。
私有成员是只能被同一类的其他成员访问的类类型成员。
默认情况下,类的成员是私有的。具有私有成员的类不再是聚合类型,因此不能再使用聚合初始化。考虑使用“m_”前缀命名私有成员,以帮助区分它们与局部变量、函数参数和成员函数的名称。
我们可以通过使用访问说明符显式设置成员的访问级别。结构体通常应避免使用访问说明符,以便所有成员默认为公有。
访问函数是一个简单的公有成员函数,其作用是检索或更改私有成员变量的值。访问函数有两种形式:获取器和设置器。获取器(有时也称为访问器)是返回私有成员变量值的公有成员函数。设置器(有时也称为修改器)是设置私有成员变量值的公有成员函数。
类类型的接口定义了类类型的用户将如何与该类类型的对象进行交互。由于只有公有成员可以从类类型外部访问,因此类类型的公有成员构成了它的接口。因此,由公有成员组成的接口有时也被称为公有接口。
类类型的实现包括使类按预期行为的实际代码。这包括存储数据的成员变量以及包含程序逻辑并操作成员变量的成员函数主体。
在编程中,数据隐藏(也称为信息隐藏或数据抽象)是一种通过隐藏程序定义数据类型的实现来强制分离接口和实现的技术。
术语封装有时也用来指代数据隐藏。然而,这个术语也用于指代将数据和函数捆绑在一起(不考虑访问控制),因此它的使用可能会产生歧义。
在定义类时,建议先声明公有成员,再声明私有成员。这突出了公有接口,同时弱化了实现细节。
构造函数是一种特殊的成员函数,用于初始化类类型对象。为了创建非聚合类类型对象,必须找到匹配的构造函数。
成员初始化列表允许你在构造函数内初始化成员变量。成员初始化列表中的成员变量应按它们在类中定义的顺序列出。建议使用成员初始化列表来初始化成员,而不是在构造函数主体内赋值。
不带参数(或所有参数都有默认值)的构造函数称为默认构造函数。如果用户没有提供初始化值,则使用默认构造函数。如果非聚合类类型对象没有用户声明的构造函数,编译器将生成一个默认构造函数(以便类可以进行值初始化或默认初始化)。这个构造函数称为隐式默认构造函数。
构造函数可以将初始化委托给同一类类型的另一个构造函数。这个过程有时称为构造函数链,这样的构造函数称为委托构造函数。构造函数可以委托或初始化,但不能同时进行。
临时对象(有时也称为匿名对象或无名对象)是一个没有名称且仅在单个表达式期间存在的对象。
拷贝构造函数是一种用于用同类型的现有对象初始化对象的构造函数。如果你没有为类提供拷贝构造函数,C++ 将为你创建一个执行逐成员初始化的公有隐式拷贝构造函数。
“只要如此”规则表明,只要修改不会影响程序的“可观察行为”,编译器可以随意修改程序以生成更优化的代码。对“只要如此”规则的一个例外是拷贝省略。拷贝省略是一种编译器优化技术,允许编译器移除对象的不必要拷贝。当编译器优化掉对拷贝构造函数的调用时,我们说构造函数已被省略。
我们编写的用于将值转换为或从程序定义类型转换的函数称为用户定义转换。可以用于执行隐式转换的构造函数称为转换构造函数。默认情况下,所有构造函数都是转换构造函数。
我们可以使用 explicit
关键字告诉编译器某个构造函数不应被用作转换构造函数。这样的构造函数不能用于拷贝初始化或拷贝列表初始化,也不能用于隐式转换。
默认情况下,将接受单个参数的构造函数声明为 explicit
。如果类型之间的隐式转换在语义上等价且性能良好(例如从 std::string
到 std::string_view
的转换),你可以考虑将构造函数声明为非 explicit
。不要将拷贝或移动构造函数声明为 explicit
,因为这些构造函数不执行转换。
成员函数(包括构造函数)可以是 constexpr
。自 C++14 起,constexpr
成员函数不再是隐式的 const
。
测验时间
作者注
曾经是本课一部分的 21 点测验已移至第 17.x 课——第 17 章总结与测验。
问题 1
a) 编写一个名为 Point2d
的类。Point2d
应包含两个类型为 double
的成员变量:m_x
和 m_y
,默认值均为 0.0。
提供一个构造函数和一个 print()
函数。
以下程序应能运行:
#include <iostream>
int main()
{
Point2d first{};
Point2d second{ 3.0, 4.0 };
// Point2d third{ 4.0 }; // 如果取消注释,应报错
first.print();
second.print();
return 0;
}
应输出:
Point2d(0, 0)
Point2d(3, 4)
显示答案
b) 现在添加一个名为 distanceTo()
的成员函数,它接受另一个 Point2d
作为参数,并计算它们之间的距离。给定两个点 (x1, y1) 和 (x2, y2),可以使用公式 std::sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2))
计算它们之间的距离。std::sqrt
函数位于头文件 <cmath>
中。
以下程序应能运行:
#include <cmath>
#include <iostream>
int main()
{
Point2d first{};
Point2d second{ 3.0, 4.0 };
first.print();
second.print();
std::cout << "两点之间的距离: " << first.distanceTo(second) << '\n';
return 0;
}
应输出:
Point2d(0, 0)
Point2d(3, 4)
两点之间的距离: 5
显示答案
问题 2
在第 13.10 课——传递和返回结构体中,我们使用了一个 Fraction
结构体编写了一个简短的程序。参考答案如下:
#include <iostream>
struct Fraction
{
int numerator{ 0 };
int denominator{ 1 };
};
Fraction getFraction()
{
Fraction temp{};
std::cout << "请输入分子的值: ";
std::cin >> temp.numerator;
std::cout << "请输入分母的值: ";
std::cin >> temp.denominator;
std::cout << '\n';
return temp;
}
Fraction multiply(const Fraction& f1, const Fraction& f2)
{
return { f1.numerator