14-x 第 14 章总结和测验

在本章中,我们深入探讨了C++的核心——类!这是本教程系列中最关键的一章,它为后续内容奠定了基础。

章节回顾

过程式编程 procedural programming中,重点在于创建实现程序逻辑的“过程”(在C++中称为函数)。我们将数据对象传递给这些函数,函数对数据执行操作,然后可能返回结果供调用方使用。

面向对象编程Object-oriented programming(通常简称为OOP)则侧重于创建包含属性与一组明确定义行为的程序定义数据类型。

类不变式class invariant是指对象在整个生命周期内必须保持为真的条件,以确保对象处于有效状态。违反类不变式的对象被视为无效状态invalid state,继续使用该对象可能导致意外或未定义的行为。

class是程序定义的复合类型,它将数据与操作该数据的函数捆绑在一起。

属于类型的函数称为成员函数member functions。被调用的对象常被称为隐式对象implicit object。非成员函数则称为非成员函数non-member functions,以区别于成员函数。若类类型不含数据成员,建议改用命名空间替代。

const成员函数const member function是保证不修改对象且不调用任何非const成员函数(因其可能修改对象)的成员函数。凡是不修改(且永远不会修改)对象状态的成员函数都应声明为const,这样它既可作用于非const对象,也可作用于const对象。

类型的每个成员都具有称为访问级别access level的属性,该属性决定了谁可以访问该成员。访问级别系统有时被非正式地称为访问控制access controls。访问级别是在类级别定义的,而不是在对象级别定义的。

公共成员Public members是类型的成员,对它们的访问没有任何限制。任何人the public都可以访问公共成员(只要它们在作用域内)。这包括同一类的其他成员。公共成员也可被外部代码访问——即存在于特定类类型成员之外的代码。公共成员的示例包括非成员函数以及其他类类型的成员。

默认情况下,结构体所有成员均为公共成员。

私有成员Private members是类类型中仅能被同一类其他成员访问的成员。

默认情况下,类成员均为私有。具有私有成员的类不再属于聚合类型,因此无法使用聚合初始化。建议为私有成员命名时添加“m_”前缀,以便与局部变量、函数参数及成员函数名称区分。

可通过访问限定符access specifier显式设置成员访问级别。结构体通常应避免使用访问限定符,使所有成员默认公开。

访问函数access function是用于获取或修改私有成员变量值的简单公有成员函数。访问函数分为两种类型:获取器和设置器。获取器Getters(有时也称为访问器accessors)是返回私有成员变量值的公有成员函数;设置器Setters(有时也称为修改器mutators)是设置私有成员变量值的公有成员函数。

类型的接口interface定义了用户如何与该类型的对象交互。由于仅公共成员可被类型的外部访问,因此类型的公共成员共同构成其接口。基于此,由公共成员组成的接口有时被称为公共接口public interface

类型的实现implementation包含使类按预期行为的代码,包括存储数据的成员变量,以及包含程序逻辑并操作成员变量的成员函数主体。

在编程中,数据隐藏data hiding(亦称信息隐藏information hiding数据抽象data abstraction)是通过向用户隐藏程序定义数据类型的实现来强制分离接口与实现的技术。

封装encapsulation一词有时也指代数据隐藏。但该术语同时用于描述数据与函数的捆绑(不考虑访问控制),故其用法可能存在歧义。

定义类时,建议先声明公共成员,最后声明私有成员。此举能突出公共接口,淡化实现细节。

构造函数constructor是一种用于初始化类类型对象的特殊成员函数。创建非聚合类类型对象时,必须找到匹配的构造函数。

成员初始化列表Member initializer list允许您在构造函数内部初始化成员变量。成员初始化列表中的成员变量应按其在类中定义的顺序列出。建议优先使用成员初始化列表初始化成员,而非在构造函数主体中赋值。

不带参数(或所有参数均为默认值)的构造函数称为默认构造函数default constructor。当用户未提供初始化值时,系统将调用默认构造函数。若非聚合类型的对象未声明构造函数,编译器将自动生成默认构造函数(使该类支持值初始化或默认初始化),此类构造函数称为隐式默认构造函数implicit default constructor

构造函数可将初始化委托给同类型的另一个构造函数,此过程称为构造函数链constructor chaining 式调用,相关构造函数称为委托构造函数delegating constructors。构造函数只能选择委托或初始化,不可同时执行两者。

临时对象 temporary object(有时称为匿名对象anonymous object无名对象unnamed object)是无名称且仅在单个表达式作用域内存在的对象。

复制构造函数copy constructor用于通过同类型现有对象初始化新对象。若未为类提供显式复制构造函数,C++将自动生成采用成员逐项初始化的公共隐式复制构造函数implicit copy constructor

如同规则as-if rule规定:只要不影响程序的“可观察行为”,编译器可随意修改程序以生成更优化的代码。“如同规则”的例外之一是复制省略Copy elision。复制省略是编译器优化技术,可消除对象的冗余复制。当编译器优化掉对复制构造函数的调用时,我们称该构造函数已被省略elided

用于将值转换为程序定义类型或反向转换的函数称为用户定义转换user-defined conversion。可用于执行隐式转换的构造函数称为转换构造函数converting constructor。默认情况下,所有构造函数均为转换构造函数。

可通过显式explicit关键字告知编译器某构造函数不应作为转换构造函数使用。此类构造函数既不能用于复制初始化或复制列表初始化,也不能用于隐式转换。

默认情况下,所有接受单个参数的构造函数均为显式构造函数。若类型间的隐式转换在语义上等价且性能良好(例如从 std::string 到 std::string_view 的转换),可考虑将构造函数设为非显式。请勿将复制构造函数或移动构造函数设为显式,因其不执行转换操作。

成员函数(包括构造函数)可为 constexpr。自 C++14 起,constexpr 成员函数不再隐式具有 const 属性。


测验时间

作者注:
原属于本课的黑杰克测验已移至第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 }; // should error if uncommented

    first.print();
    second.print();

    return 0;
}

这段代码应输出:

Point2d(0, 0)
Point2d(3, 4)

image

显示解决方案

#include <iostream>

class Point2d
{
private:
	double m_x{ 0.0 };
	double m_y{ 0.0 };

public:
	Point2d() = default;

	Point2d(double x, double y)
		: m_x{ x }, m_y{ y }
	{
	}

	void print() const
	{
		std::cout << "Point2d(" << m_x << ", " << m_y << ")\n";
	}
};

int main()
{
    Point2d first{};
    Point2d second{ 3.0, 4.0 };

    // Point2d third{ 4.0 }; // should error if uncommented

    first.print();
    second.print();

    return 0;
}

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 << "Distance between two points: " << first.distanceTo(second) << '\n';

    return 0;
}

这段代码应输出:

Point2d(0, 0)
Point2d(3, 4)
Distance between two points: 5

image

显示解决方案

#include <cmath>
#include <iostream>

class Point2d
{
private:
	double m_x{ 0.0 };
	double m_y{ 0.0 };

public:
	Point2d() = default;

	Point2d(double x, double y)
		: m_x{ x }, m_y{ y }
	{
	}

	void print() const
	{
		std::cout << "Point2d(" << m_x << ", " << m_y << ")\n";
	}

	double distanceTo(const Point2d& other) const
	{
		return std::sqrt(
            (m_x - other.m_x)*(m_x - other.m_x) +
            (m_y - other.m_y)*(m_y - other.m_y)
            );
	}
};

int main()
{
    Point2d first{};
    Point2d second{ 3.0, 4.0 };

    first.print();
    second.print();

    std::cout << "Distance between two points: " << first.distanceTo(second) << '\n';

    return 0;
}

问题 #2

在第 13.10 节——传递和返回结构体中,我们编写了一个使用 Fraction 结构体的简短程序。参考解决方案如下所示:

#include <iostream>

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

Fraction getFraction()
{
    Fraction temp{};
    std::cout << "Enter a value for numerator: ";
    std::cin >> temp.numerator;
    std::cout << "Enter a value for denominator: ";
    std::cin >> temp.denominator;
    std::cout << '\n';

    return temp;
}

Fraction multiply(const Fraction& f1, const Fraction& f2)
{
    return { f1.numerator * f2.numerator, f1.denominator * f2.denominator };
}

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

int main()
{
    Fraction f1{ getFraction() };
    Fraction f2{ getFraction() };

    std::cout << "Your fractions multiplied together: ";

    printFraction(multiply(f1, f2));

    return 0;
}

将结构体中的分数转换为类。将所有函数转换为(非静态)成员函数。

作者注:
注意:本测验并未遵循关于何时应使用非成员函数或成员函数的最佳实践。其目的是测试你是否理解如何将非成员函数转换为成员函数。

显示解答

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    explicit Fraction(int numerator=0, int denominator=1)
        : m_numerator { numerator }, m_denominator { denominator}
    {
    }

    void getFraction()
    {
        std::cout << "Enter a value for numerator: ";
        std::cin >> m_numerator; // this is a member function, so we can access members directly
        std::cout << "Enter a value for denominator: ";
        std::cin >> m_denominator;
        std::cout << '\n';
    }

    Fraction multiply(const Fraction& f) const
    {
        return Fraction{ m_numerator * f.m_numerator, m_denominator * f.m_denominator };
    }

    void printFraction() const
    {
        std::cout << m_numerator << '/' << m_denominator << '\n';
    }
};

int main()
{
    Fraction f1{};
    f1.getFraction();

    Fraction f2{};
    f2.getFraction();

    std::cout << "Your fractions multiplied together: ";

    f1.multiply(f2).printFraction();

    return 0;
}

问题 #3

在先前测验的解决方案中,为何将Fraction构造函数显式化?

显示解答

显式声明构造函数可防止通过单值隐式转换创建Fraction对象,从而避免f1.multiply(true)这类无意义操作。

f1.multiply(true) 要求将 true 隐式转换为 Fraction。通常编译器会使用 Fraction(int numerator=0, int denominator=1) 构造函数实现转换,但若将其显式化,该构造函数便不再适用于隐式转换。此时编译器无法找到将 true 转换为 Fraction 的途径,从而触发编译错误。

问题 #4

在先前测验问题中,为何将getFraction()和print()保留为非成员函数可能更优?

显示解答

使用非成员版本的 getFraction() 时,我们可以在一步内完成分数对象的定义和初始化。而成员版本需要两步操作:先创建对象,再调用其成员函数。该版本还会为用户打印应用程序专用的文本提示。

通过将print()移至非成员函数(并使用访问函数获取成员),我们将其从类接口中移除,使类的核心功能更简洁。这还意味着:只要接口不变(因其无法直接访问数据成员),即使类实现发生变更,我们也无需考虑print()是否需要更新。
posted @ 2025-12-31 09:35  游翔  阅读(16)  评论(0)    收藏  举报