章节目录

第14章总结

本节阅读量:

在本章中,我们探索了C++的核心——类!这是教程系列中最重要的一章,它为后面的大部分内容奠定了基础。


章节回顾

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

在面向对象编程(通常缩写为OOP)中,重点是创建包含属性和一组定义良好行为的程序定义数据类型。

类不变量是一种必须在对象整个生存期内为真的条件,以便对象保持有效状态。违反类不变量的对象被称为处于无效状态,并且进一步使用该对象可能会导致意外或未定义行为。

类是程序定义的复合类型,它捆绑了数据和处理该数据的函数。

属于类类型的函数称为成员函数。调用成员函数的对象通常称为隐式对象。不是成员函数的函数称为非成员函数,以将它们与成员函数区分开来。如果类类型没有数据成员,则更推荐使用命名空间。

const成员函数是一个成员函数,它保证不会修改对象或调用任何非const成员函数(因为它们可能会修改对象)。不(并且永远不会)修改对象状态的成员函数应成为const,以便可以在非常量对象和常量对象上调用它。

类类型的每个成员都有一个称为访问级别的属性,该属性确定谁可以访问该成员。访问级别系统有时非正式地称为访问控制。访问级别是基于每个类而不是基于每个对象定义的。

public成员是类类型的成员,对如何访问它们没有任何限制。任何人都可以访问公共成员。这包括同一类的其他成员。公共成员也可以被公共代码访问,这指的是存在于给定类类型成员之外的代码,包括非成员函数以及其他类类型的成员。

默认情况下,结构体的所有成员都是公共成员。

私有成员是只能由同一类的其他成员访问的类类型的成员。

默认情况下,类的成员是私有的。具有私有成员的类不再是聚合,因此不能再使用聚合初始化。考虑以“m_”前缀开头命名私有成员,以帮助将它们与局部变量、函数参数和成员函数的名称区分开来。

可以通过使用访问说明符来显式设置成员的访问级别。结构体通常应避免使用访问说明符,以便所有成员都默认为public。

访问函数是一个普通的public成员函数,其任务是检索或更改私有成员变量的值。访问函数有两种风格:getter和setter。Getter(有时也称为访问器)是返回私有成员变量值的公共成员函数。Setter(有时也称为mutator)是设置私有成员变量值的公共成员函数。

类类型的接口定义了类类型的用户将如何与类类型的对象交互。由于只能从类类型外部访问公共成员,因此类类型的公共成员形成其接口。由公共成员组成的接口有时称为公共接口。

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

在编程中,数据隐藏(也称为信息隐藏或数据抽象)是一种通过对用户隐藏程序定义的数据类型的实现来强制实现接口和实现分离的技术。

术语封装有时也用于指代数据隐藏。然而,该术语也用于指将数据和功能捆绑在一起(不考虑访问控制)。

定义类时,最好先声明公共成员,然后声明私有成员。这突出了公共接口,并淡化了实现细节。

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

成员初始值设定项列表允许您从构造函数中初始化成员变量。应按在类中定义成员变量的顺序列出它们。与在构造函数主体中分配值相比,优先使用成员初始值设定项列表来初始化成员。

不带参数(或具有所有默认参数)的构造函数称为默认构造函数。如果用户没有提供初始化值,则使用默认构造函数。如果非聚合类类型对象没有用户声明的构造函数,编译器将生成默认构造函数(以便类可以被值初始化或默认初始化)。此构造函数称为隐式默认构造函数。

允许构造函数将初始化委托给同一类类型的另一个构造函数。这个过程有时被称为构造函数委托,这样的构造函数被称为委托构造函数。构造函数可以委托或初始化,但不能同时委托和初始化。

临时对象(有时称为匿名对象或未命名对象)是没有名称且仅在单个表达式期间存在的对象。

拷贝构造函数是用于使用相同类型的现有对象初始化对象的构造函数。如果不为类提供拷贝构造函数,C++将为您创建一个执行成员级初始化的public隐式拷贝构造函数。

仿佛规则说,编译器可以随意修改程序,以产生更优化的代码,只要这些修改不影响程序的“可观察行为”。“仿佛”规则的一个例外是拷贝省略。拷贝省略是一种编译器优化技术,允许编译器删除不必要的对象复制。当编译器优化掉对拷贝构造函数的调用时,可以说构造函数已被省略。

编写的将值转换为程序定义类型或从程序定义类型转换为值的函数称为用户定义转换。可以用于执行隐式转换的构造函数称为转换构造函数。默认情况下,所有构造函数都是转换构造函数。

可以使用explicit关键字来告诉编译器,构造函数不应用作转换构造函数。这样的构造函数不能用于执行拷贝初始化或拷贝列表初始化,也不能用于执行隐式转换。

默认情况下,使任何接受单个参数的构造函数explicit。如果类型之间的隐式转换在语义上是等效的,并且是性能更好的转换(例如从std::string到std::string_view的转换),则可以考虑使构造函数成为非explicit的。不要使拷贝或移动构造函数explicit,因为它们不执行转换。


习题

作者提示

原先作为本课一部分的21点(blackjack)习题已移至「17.13 第17章总结」。

问题 1

a) 编写一个名为Point2d的类。Point2d应当包含两个double类型的成员变量:m_x和m_y,两者都默认初始化为0.0。

提供一个构造函数和一个print()函数。

下面这个程序应当能够运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#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)
查看答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#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 }; // 取消注释后应当报错

    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头文件中。

下面这个程序应当能够运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#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
查看答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#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.7 结构体作为函数的输入与输出」中,我们编写了一个使用Fraction结构体的短程序。参考解法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#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;
}

请把Fraction从结构体改成类,并把所有函数改写成(非静态的)成员函数。

作者提示

注意:本题并不符合「何时应使用非成员函数或成员函数」的最佳实践。它的目的是考查你是否理解如何把非成员函数改写成成员函数。

查看答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#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; // 这是成员函数,因此可以直接访问成员
        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的构造函数声明为explicit?

查看答案

把构造函数声明为explicit,可以防止通过单个值的隐式转换来创建Fraction。这有助于避免出现f1.multiply(true)这类无意义的情况。

f1.multiply(true)需要把true隐式转换为Fraction。正常情况下,编译器会使用Fraction(int numerator=0, int denominator=1)来完成转换;但如果我们把这个构造函数声明为explicit,它就不能用于隐式转换。由于此时编译器找不到把true转换为Fraction的方法,就会产生编译错误。

问题 4

在上一题中,为什么把getFraction()和print()保留为非成员函数可能更好?

查看答案

使用非成员版本的getFraction(),我们可以在一步之内定义并初始化一个Fraction。而成员版本需要两步:必须先创建对象,再对它调用成员函数。此外,成员版本还会向用户打印应用程序特有的文本提示。

把print()改为非成员函数(并使用访问函数来访问成员),就从类的接口中去掉了这个函数,使类的核心功能更加清晰。这也意味着,如果类的实现将来发生变化,只要接口不变,我们就不必考虑print()是否需要更新(因为我们知道它无法直接访问任何数据成员)。


14.15 转换构造函数和explicit关键字

上一节

15.0 隐藏的“this”指针和成员函数调用

下一节