抽象记忆点

小结

  1. 软件系统通常包含数百万行代码,远远超过即使是最有经验的程序员一次能够完全跟踪的范围。
    • 这表明现代软件的规模和复杂性,需要采取有效的管理和设计方法。
  2. 软件系统中的单个错误值可能导致整个系统失败。
    • 这凸显了在编写软件时准确性和健壮性的重要性。
  3. 软件系统中可能的交互数量随着系统组件数量的增加呈指数增长。
    • 这提醒我们,随着系统复杂性的增加,需要考虑和管理其交互。
  4. 抽象提供了一种以更简单的术语呈现复杂对象的方法。
    • 通过抽象,我们可以隐藏细节,使用户能够更轻松地理解和操作对象。
  5. 抽象将用户分为客户端和实现者,每个都有不同的任务。这种分离有时被称为抽象屏障。
    • 这种分离使系统的设计更模块化和可维护,同时提供了清晰的界面和实现。
  6. 抽象描述了许多可能的实现方式,而封装防止客户端窥视这些实现。
    • 封装确保了实现细节的安全性和私密性,使得对象的内部状态可以受到保护。
  7. 客户端与对象交互的方式称为该对象的接口。
    • 接口定义了对象可用的方法和操作,隐藏了实现的细节。
  8. 抽象减少了软件系统中的组件数量,降低了系统的最大复杂性。
    • 通过抽象,我们可以简化系统的设计和实现,减少潜在的错误和复杂度。
  9. C++的结构体缺乏封装性,因为它们的实现即为接口。
    • 这强调了使用类而不是结构体的重要性,以实现更好的封装和抽象。
  10. C++类的概念是接口与实现的实现。
    • 类提供了一种结构化的方法来组织代码和数据,同时隐藏了实现的细节。
  11. 类中列为public的成员形成了该类的接口,并对任何人可见。
    • 这强调了设计良好的接口的重要性,以促进代码的重用和可维护性。
  12. 类中列为private的成员属于类的实现部分,只能被该类的成员函数查看。
    • 封装私有成员确保了对实现细节的访问限制,提高了代码的安全性和可靠性。
  13. 构造函数允许实现者从类被创建的那一刻开始强制执行不变量。
    • 构造函数确保了对象的有效初始化,并在创建对象时执行必要的设置和验证。
  14. 私有成员函数允许实现者在不暴露实现细节给客户端的情况下分解代码。
    • 私有成员函数提供了一种将代码组织成更小、更易管理的部分的方法,同时保持了接口的简洁性和清晰性。
  15. 类的实现通常分为包含类定义的.h文件和包含类实现的.cpp文件。
    • 这种文件分区方法有助于组织代码,并使其更易于维护和理解。
  16. 在实现之前设计类接口,以避免过度专门化接口。
    • 这强调了设计良好的接口对于软件系统的灵活性和可维护性的重要性。

练习

  1. 在我们讨论抽象时,我们谈到接口和模块化如何指数地降低了系统的最大复杂性。你能想到现实世界中引入间接性如何使得复杂系统更易于管理的例子吗?
    • 在现实世界中,一个例子是网络路由器。路由器通过在网络设备之间引入抽象层,如IP地址和路由表,来管理网络流量的路由。这种抽象层使得网络管理员可以轻松地管理和配置网络,而不必直接处理底层硬件和协议细节。
  2. getFrequency和setFrequency等函数的动机是什么,相对于只有一个公有频率数据成员来说?
    • 使用getFrequency和setFrequency函数而不是公有频率数据成员的动机在于封装。通过提供访问和修改频率的公共接口,类可以隐藏其内部实现细节,并且可以在必要时对这些操作进行验证和限制,确保数据的一致性和有效性。
  3. 构造函数是在什么时候调用的?为什么构造函数有用?
    • 构造函数在创建类的新实例时被调用。它用于初始化新对象的状态,确保对象在被使用之前处于一个已知的有效状态。构造函数可以执行必要的设置和验证,以确保对象的一致性和完整性。
  4. 公有成员函数和私有成员函数有什么区别?
    • 公有成员函数是类的接口的一部分,可以被任何人访问和调用。私有成员函数是类的实现的一部分,只能被类的其他成员函数调用,对外部代码不可见。
  5. 类的.h文件中放置什么?.cpp文件中呢?
    • 类的.h文件包含类的声明和接口,包括成员变量和公有成员函数的声明。.cpp文件包含类的实现细节,包括构造函数、析构函数和其他私有成员函数的定义。
  6. 我们在很大程度上谈论了流库和STL,却没有提及这些库在幕后是如何实现的。解释一下为什么抽象使得可以在不完全了解其工作原理的情况下使用这些库。
    • 抽象使得可以在不了解其实现细节的情况下使用这些库,因为它们提供了清晰简洁的接口,隐藏了复杂的内部实现。通过使用这些接口,开发人员可以方便地使用库中提供的功能,而不必担心其底层的实现细节。
  7. 假设C++的设计略有不同,即标记为私有的数据成员只能读取但不能写入。也就是说,如果一个名为volume的数据成员被标记为私有,那么客户端可以通过写入myObject.volume来读取值,但不能直接写入volume变量。这将阻止类的客户端错误地修改实现,因为任何可能改变对象数据成员的操作都必须通过公共接口进行。然而,这种设置存在严重的设计缺陷,会使得类的实现变得困难。这个缺陷是什么?(提示:回想一下之前的音量/衰减例子)
    • 这种设置的主要设计缺陷在于它破坏了封装性。封装的一个关键原则是将数据和操作放在一起,并确保它们是一致的。如果数据成员只能读取而不能写入,那么类的内部状态可能会在未受控制的情况下被外部修改,这会导致不一致和不可预测的行为,从而破坏了类的封装性和可维护性。
  8. 下面是一个表示购物清单的类的接口:
1
2
3
4
5
6
7
8
class GroceryList {
public:
GroceryList();
void addItem(string quantity, string item);
void removeItem(string item);
string itemQuantity(string item);
bool itemExists(string item);
};

实现GroceryList类的代码如下:

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
#include <iostream>
#include <map>
#include <string>

using namespace std;

class GroceryList {
private:
map<string, string> items;

public:
GroceryList() {}

void addItem(string quantity, string item) {
items[item] = quantity;
}

void removeItem(string item) {
items.erase(item);
}

string itemQuantity(string item) {
if (items.find(item) != items.end()) {
return items[item];
} else {
return ""; // Return empty string if item not found
}
}

bool itemExists(string item) {
return items.find(item) != items.end();
}
};
  1. 为什么使用GroceryList类而不只是使用原始的map<string, string>?
    • GroceryList类提供了更高层次的抽象,使得添加、删除和查询商品更加简单和直观。它封装了map的细节,提供了更清晰的接口,使得代码更易于维护和理解。
  2. GroceryList类需要构造函数吗?为什么或为什么不?
    • 是的,GroceryList类需要构造函数。构造函数用于初始化类的内部状态,例如清单中包含的商品列表。
  3. 给出你在STL中遇到的带参数的构造函数的一个例子。
    • 一个例子是vector的构造函数,它接受一个初始大小作为参数:vector<int> vec(10);
  4. 参数化构造函数有什么用?
    • 参数化构造函数允许我们在创建对象时提供初始值,从而初始化对象的状态。这使得对象在创建时可以立即设置为特定的状态,而不需要调用额外的初始化函数。
  5. 下面是一个代表Keno游戏的类KenoGame的接口:
1
2
3
4
5
6
7
class KenoGame {
public:
KenoGame();
void addNumber(int value);
size_t numChosen();
size_t numWinners(vector<int>& values);
};

实现KenoGame类的代码如下:

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
#include <iostream>
#include <vector>

using namespace std;

class KenoGame {
private:
vector<int> chosenNumbers;

public:
KenoGame() {}

void addNumber(int value) {
chosenNumbers.push_back(value);
}

size_t numChosen() {
return chosenNumbers.size();
}

size_t numWinners(vector<int>& values) {
size_t count = 0;
for (int num : chosenNumbers) {
if (find(values.begin(), values.end(), num) != values.end()) {
count++;
}
}
return count;
}
};
  1. 参考STL容器章节中Snake的实现。设计并实现一个代表蛇的类。在公共接口中支持哪些操作?如何实现它?
    • 蛇类的公共接口可能包括移动、增长、检查碰撞等操作。它可以通过一个链表或数组来存储蛇的身体部分,每次移动时更新身体部分的位置。

欢迎关注我的其它发布渠道