C++程序设计原理与实践(第二版)思考题答案

思考题 解答

  • 前言
  • 第一章:计算机、人与程序设计
  • 第二章:Hello, World!
  • 第三章:对象、类型和值
  • 第四章:计算
  • 第五章:错误
  • 第六章:编写一个程序
  • 第七章:完成一个程序
  • 第八章:函数相关的技术细节
  • 第九章:类相关的技术细节
  • 第十章:输入输出流
  • 第十一章:定制输入输出
  • 第十二章:向量和自由空间
  • 第十三章:向量和数组
  • 第十四章:向量、模板和异常
  • 第十五章:容器和迭代器
  • 第十六章:算法和映射
  • 第十七章:一个显示模型
  • 第十八章:图形类
  • 第十九章:设计图形类
  • 第二十章:绘制函数图和数据图
  • 第二十一章:图形用户界面
  • 第二十二章:理念和历史
  • 第二十三章:文本处理
  • 第二十四章:数值计算
  • 第二十五章:嵌入式系统程序设计
  • 第二十六章:测试

前言

我买的这本是C++之父Bjarne Stroustrup所著的Programming: Principles and Practice Using C++ 2th中文版。看到原版标题就应该明白,这本书是教你程序设计的,目的是使你掌握程序设计原理并付诸实践,而C++是作为教学的工具语言。作者在本书的前言和引言有提到,如果你只是想掌握C++语法的话,那么这本书或许并不合适;如果你希望编写供他人使用的高质量程序(并希望将来达到专业水准),那么这本书将是伴你启航的最佳图书,助你迈出专业之路的第一步。
我从今天开始更新这本书的思考题与习题的个人解答,并不代表标准答案,可能会有错误和bug,希望各位书友不吝赐教,指出问题,感激感恩, 欢迎大家与我交流,QQ:244930142。(2021.04.10)

第一章:计算机、人与程序设计

1、什么是软件?
答:软件是运行在计算机上的程序的集合。

2、软件为什么重要?
答:我们的文明建立在计算机与软件之上。我们日常生活中一天所做的事情,有相当一部分依赖于计算机与软件。为了将食物提供与你,需要通过计算机计划生产、运输和存储方案;我们所阅读的书籍有电子书和纸质书,这些都需要计算机帮助来出版和传播;今天的娱乐业也大量采用计算机;医生为你检查身体所用的仪器也是计算机和软件控制的;等等。现代生活离不开计算机,而每台计算机都需要运行软件,否则它们只是一堆由硅、金属和塑料组成的硬件罢了。因此软件很重要。

3、软件哪里重要?
答:与第二题相似,但本题主要体现软件的作用域。软件在城市交通、文化娱乐传媒、医疗健康、工业制造、科学研究等领域都很重要。

4、如果有些软件失败,会出现什么问题?列举一些例子。
答:如果操作系统失败,出Bug,那么会蓝屏(点名Windows)。如果车辆控制系统出问题,车会失控,轻则如车祸,重则出人命

5、软件在哪些地方扮演重要角色?列举一些例子。
答:与第三题相似。以医疗为例,医疗中使用的各种扫描仪就是通过软件使用各种复杂算法来将脉冲信号转化为图像的。

6、哪些工作与软件开发相关?列举一些例子。
答:系统程序员、客户端程序员、服务器程序员、Web前端后端程序员。。。程序员。

7、计算机科学与编程之间的区别是什么?
答:好问题!现在好多人都以为计算机科学 == 写代码。。。但其实编程只是计算机科学中的子学科,是一种支撑技术。美国政府“蓝皮书”对计算机科学的定义是:“对计算系统和计算的系统研究。这个学科造就的知识体系包含理解计算系统和方法的理论,设计方法、算法和工具,测试概念的方法,分析和验证的方法,以及知识的表示和实现。”而计算机编程使用特定的编程语言来解决特定的计算问题。

8、在船舶的设计、建造和使用中,软件使用在哪些地方?
答:CAD软件在船舶设计中辅助设计人员制图、模拟与仿真。CAM在船舶建造过程中,控制各种小型机器人和机械臂对船体进行组装切割与焊接等。船舶航行过程中,各种软件用于定位、控制航行方向和监控航行状态等。

9、什么是服务器机群?
答:提供如Web服务等服务的多台计算机的集合。

10、你在线提出哪种类型的查询?列举一些例子。
答:百度搜索中输入特定的关键词,返回与该关键词有关联的网页。在某论坛中查询指定用户或帖子。

11、软件在科学方面有哪些应用?
答:各科学领域采集得到的大规模数据需要软件进行处理和分析,生成有用结果,供科研人员判断。例如在航天领域,月球探测器拍摄的月球照片通过信道传递回地面,地面需要对接受的信号进行处理,这里面涉及如何以最小比特数无差错传输图片,采用何种算法对照片进行渲染以恢复颜色和减小失真,这些都需要软件的帮助。

12、软件在医疗方面有哪些应用?
答: 电子病历、医疗扫描成像设备、微创手术等。

13、软件在娱乐方面有哪些应用?
答:动漫的人物建模就需要软件的帮助,吐槽一句,有些动漫的建模不忍直视,不知道是人的问题还是软件的问题。。。

14、 我们期待中的好软件的一般特点有哪些?
答:界面美观,功能完备且不臃肿、使用时响应迅速无BUG、容易上手操作不复杂、价格不贵或者免费最好、有定期的维护与更新、保护和尊重用户隐私、软件安全漏洞少。欢迎补充

15、一个软件开发者看起来是什么样的?
答:好问题,好多人都以为程序员是穿格子衫的油腻秃子(笑)。真正的软件开发者,首先自身要有实力,掌握软件开发的思想和方法。其次善于沟通,软件开发是一项团队合作的实例,如何与团队成员进行有效沟通并达成一致想法和前进方向是很重要的,另外与客户沟通,明确需求并向客户表达软件产品的优势同样非常重要。

16、软件开发的阶段有哪些?
答:大致可分四个阶段:

  • 分析:问题是什么?用户想要什么?用户需要什么?用户可以负担什么?我们需要哪种可靠性?
  • 设计:我们如何解决问题?系统的整体架构将是怎样的?系统包括哪些部分?这些部分之间如何通信?系统与用户之间如何通信?
  • 编程:用代码表达问题的解决方案(设计),以满足所有约束(时间、空间、金钱、可靠性等)的方式编写代码。保证这些代码是正确和可维护的。
  • 测试:通过系统化的测试方法确保系统在所要求的所有情况下都正确工作。
    注意,编程+测试通常称为实现,软件开发这几个阶段并不独立,也没有严格的顺序。

17、软件开发为什么困难?列举一些原因
答:分析阶段可能没法完整理解问题,或者无法非常明确用户的需求;设计阶段也有可能会出现一些逻辑错误直到实现时才发现,这时候就需要重新设计以修正解决方案;编程时可能由于过度追求细节而导致延期交付;测试时由于测试集设计不合理,没能发现隐藏bug。要避免这些问题很难,成本与效益需要折中,导致软件开发并不是想象中那么简单。

18、软件的那些用途为你的生活带来便利?
答:比如说移动支付软件,可以让我出远门也不用带很多现金,而且平时公交地铁等一个乘车码就搞定了。

19、软件的哪些用途为你的生活带来更多困难?
答:去外面餐厅吃饭,很多都要求扫码点餐,但是会泄露个人隐私,而且并不比用纸质菜单点餐更快捷(不过可能省纸了,保护环境吧?)

第二章:Hello, World!

1、“Hello, World!”程序的目的是什么?
答:输出“Hello, World!”字符串到标准输出上。

2、说出一个函数的4个部分。
答:函数返回值类型、函数名、参数列表、函数体

3、说出必须出现在每个C++程序中的一个函数。
答:main 函数

4、在"Hello, World!"程序中,return 0; 这行的目的是什么?
答:结束函数,并返回值0

5、编译器的目的是什么?
答:将源代码从人类可读的格式转换为机器可以理解的东西。例如将.cpp与.h源文件编译为.obj或.o后缀的目标代码文件。

6、#include 语句的目的是什么?
答:指示计算机从这个语句中指定的文件中提供(“包含”)功能。一般是包含程序要用的变量、类或函数的声明。

7、文件名后缀为.h在C++中表示什么?
答:表示头文件,一般包含各种声明(书上有提到是包含各种定义,我感觉不太对,一般定义不会放头文件里)。

8、链接器为你的程序做什么?
答:链接各目标文件,重定位函数和全局与静态变量,生成可执行文件。

9、源文件和目标文件之间的区别是什么?
答:源文件是文本文件,是人类可读格式的程序文件。而目标文件是将源文件编译后生成的二进制文件,是机器可读的,用于后续生成可执行文件。

10、IDE是什么?它能为你做什么?
答:Integrated Development Environment,集成开发环境,它是一款将编辑器、编译器、链接器,调试器等合为一体的软件,方便程序员编程和测试。

11、即使你理解教材中所有的东西,为什么还是需要实践?
答:实践是检验真理的唯一标准。

第三章:对象、类型和值

1、术语prompt的含义是什么?
答:提示符,提示信息

2、那种操作符用于读入值到一个变量?
答:输入操作符 >>

3、如果你希望用户在你的程序中为一个命名为number的变量输入一个整形值,如何用两行代码来提示用户输入并将值输入你的程序中?
答:

	cout << "Enter an integer.\n";
	cin >> number;

4、\n的名称是什么,它的目的是什么?
答:换行符的转义字符,在需要输出换行符的场景中代替键盘输入的换行符。

5、怎样终止输入一个字符串?
答:在键盘中键入一个换行(按下回车键)。当遇到while(cin >> val)时候,可以键入ctrl+z或ctrl+d来提供一个终止输入信号。

6、怎样终止输入一个整数?
答:键入换行(按下回车键)。若遇到循环,那么直接键入一个非数字字符即可跳出循环。

7、如何将

	cout << "Hello, ";
	cout << first_name;
	cout << "!\n";

编写为一行代码?
答:

	cout << "Hello, " << first_name << "!\n";

8、什么是对象?
答:对象是用来保存一个指定类型值的一些内存单元。换句话说,用来存储数据的一些“位置”被称为对象。

9、什么是字面值常量?
答:字面值常量(literal)用于表示不同类型的值。(见书本附录A2)

10、有哪些不同类型的字面值常量?
答:整数、浮点、布尔、字符、字符串、指针字面值常量。

11、什么是变量?
答:变量是一个命名的对象。

12、char、int和double的典型大小是多少?
答:分别为8bit 1字节、32bit 4字节、64bit 8字节。

13、我们用哪种方式测试内存中的实体(例如int和string)大小?
答:sizeof运算符,sizeof(int),sizeof(string)

14、操作符=与==之间的区别是什么?
答:=是赋值操作符,而==是比较操作“等于”的意思。

15、什么是一个定义?
答:定义是一个声明,但同时也为对象分配了内存空间。

16、什么是初始化,它和赋值的区别是什么?
答:初始化是给一个变量它自己的的初值,赋值是给一个变量一个新的值。两者在逻辑上不同。从原则上来说,初始化之前变量为空,即初始化是在定义一个变量时给它一个初值,而赋值则在放入一个新值前,必须先将旧的值清空。

17、什么是字符串连接,如何使它在C++中工作?
答:字符串连接是对string变量或string常量的c++标准库方法,操作符为+。要使它在C++中工作,首先需要包含相应的C++标准库,然后使用+连接两个字符串。

18、在以下名字中,哪些在C++中是合法的?如果一个名字是不合法的,为什么?
答:

  • 合法:This_little_pig、This_1_is_fine (不建议,有时候1和小写 不太好区分)、_this_is_ok
    (不建议以下划线开头作为名字,避免和C++实现与系统函数冲突)、MiniMineMine、number
  • 不合法:2_For_1_special(名字不能以数字开头)、latest ting (名字中间不能有空格)、the_$12_method(名字只能包含数字、字母和下划线,$字符不能用于名字)、correct?(同理,?不能用于名字)

19、请举出5个你不会使用的合法名字的例子,因为他们容易引起混淆。
答:(1)以下划线开始的名字_underline,(2)带有小写l的名字fl,(3)带有数字1的名字f1,(4)带有大写字母O的名字fO,(5)带有数字0的名字f0,(6)带有小写字母o的名字fo,(7)全部都是大写的名字NAME(避免与宏搞混)

20、选择名字的好规则有哪些?
答:使用有特定含义的名字,也就是选择有助于人们理解你的程序的名字,比如partial_sum, element_cout, stable_partition,但不要选择太长的名字,不使用标准库中的内容作为名字,不用下划线开头作为名字,不使用全部大写字母的名字,建议使用首字母大写来定义自己的类型。

21、什么是类型安全,为什么它是重要的?
答:每个对象在定义时被分配一个类型,对于一个程序或程序的一个部分,如果使用的对象符合它们规定的类型,那么它们是类型安全的。因为类型安全可以保证我们不会因为一些细微的又难以发现的错误丢失信息(比如不安全转化中的窄化操作可能会丢失信息)。

22、为什么从double转换成int是一件坏事?
答:因为int在内存中的size往往比double小,因此从double转换成int是一种窄化操作,会丢失有效比特位(C++是以截断小数部分的形式转换的)。

23、请定义一个判断从一种类型到另一种类型的转换是否安全的规则。
答:个人愚见,从窄类型转换为宽类型是安全的;从宽类型转换为窄类型时,如果宽类型变量原值的有效比特位能够在窄类型变量中放下,且不会产生符号歧义(比如说转换为窄类型时刚好最高位是1,那就是补码中的负数表示了,而在原宽类型变量中原值是正数),那么也是安全的。
题外话:永远记住初始化你的变量,因为一个变量在没有初始化之前使用它,是被认为类型不安全的。
C++11引入了一种初始化方式,可彻底避免窄化转换,我们应该用{}列表记号来初始化,列表记号也被称为通用统一初始化。
(2021.04.10)

第四章:计算

1、什么是计算?
答:对于计算机而言,计算意味着基于输入生成输出的过程。

2、什么是计算的输入和输出?举例说明。
答:输入是信息进入计算机或程序,而输出是指信息从计算机和程序中离开。比如我写博客打字时,键盘敲击键入的字符,就是输入,而屏幕上显示出我键入的字符,就是输出。

3、当表示计算的时候,程序员需要谨记哪三项要求?
答:正确、简单、高效。

4、什么是表达式?
答:表达式是程序的最基本组成单元。表达式就是从一些操作数计算一个值。计算过程中可能还有操作符,最简单的表达式是字面常量。

5、在本章内容中,表达式和语句有什么区别?
答:表达式表示的是某个结果值,而语句则是用来实现程序功能的。

6、什么是左值?列出要求用左值的运算符。为什么这些运算符需要使用左值?
答:左值表示某种类型的对象,它拥有一块存储区域。 = += -= *= /= %= |= &= ++(前置) --(前置)。
这些运算符的结果需要被赋值到一个存储区域中。

7、什么是常量表达式?
答:操作数为常量(符号常量、字面常量)的表达式。

8、什么是字面常量?
答:用于表示不同类型的值,例如10, ‘a’, 3.14, “Norah”

9、什么是符号常量,我们应该如何使用它?
答:符号常量表示那些在初始化后值就不再改变的数值量。在定义一个变量时用constexpr或者const关键字作为定义的补充描述。其中constexpr符号常量必须给定一个在编译时已知的值,而cosnt符号常量则可以修饰非常量表达式。比如constexpr double pi = 3.14; const double c = 2 * pi * r;

10、什么是魔术常量?举例说明。
答:不能被直接识别的字面常量通常被戏称为魔术常量。我们要避免使用它。
比如299792458是一个物理常量,表示光在真空中传播速度,单位是米,不过一般人看到这数字都不会意识到它是光速。

11、哪些运算符既可以用于整型也可以用于浮点型?
答:P52页中的表格里,除了取模运算符只能用于整型以外,其他都是整型浮点型通用。

12、哪些运算符只能用于整型而不能用于浮点型?
答:取模运算符%, 自增++ 自减–

13、哪些运算符可以用于字符串?
答:+ = == != > < >= <=

14、什么情况下程序员更喜欢用 switch 语句而不是 if 语句?
答:在要判断的元素值是整型、字符型或枚举类型的前提下,如果与多个常量进行比较,那么用switch会更直观方便。

15、switch语句常见的问题有哪些?
答:常见问题:忘记为case语句添加break;switch语句括号中的值选择了其他类型(比如字符串类型);case语句中的值使用了变量(要求必须是常量表达式);在两个case语句中使用相同的数值。

16、for循环语句的循环控制中每一部分的功能是什么?他们的执行顺序是怎样的?
答:三个部分:第一部分初始化循环控制变量等变量;第二部分循环判断,用以判断是否进入循环体;第三部分更新循环控制变量等变量。在首次执行for语句时,1.执行第一部分,2.执行第二部分,3.进入循环体,4.执行第四部分,这一轮循环结束,下一轮循环开始于2.第二部分的执行,如果满足,则依次3与4步骤,否则退出循环。

17、什么情况下应该使用for循环?什么情况下应该使用while循环?
答:这并没有刻板的规定,只不过根据个人喜好以及代码清晰易理解可维护角度来看,for循环可能更加普适一些,不过while循环适合判断条件和循环控制变量更新简单的那种循环。

18、如何输出一个字符型数据的数值?
答:使用类型转换,int a = int(‘a’)或者int b = int{‘b’},然后输出int变量a和b,就知道字符型数据的数值了。

19、函数原型char foo(int x)的含义是什么?
答:表示这是一个名字为foo的函数,其参数列表显示,需要有一个int型参数,函数返回char类型返回值。

20、什么情况下你将在程序中定义一个单独的函数?列出原因。
答:这个函数执行的功能较为复杂,单独列出该函数方便维护。或者这个函数执行的功能在程序中很多地方要用到,那么单独定义函数后,在用到该功能时只需调用该函数即可。

21、哪些操作可以用于整型数据而不能用于字符串?
答:+(加) - * / % ++ – +=n(加n)

22、哪些操作可以用于字符串而不能用于整型数据?
答:+(连接) +=(添加到结尾)

23、vector中第三个元素的索引号是多少?
答:2

24、如何用for循环来打印输出vector的所有数据元素?

	vector v {element1, element2, element3, ...};
	for (type x : v)
		cout << x << '\t';

25、语句vectoralphabet(26);的含义是什么?
答:定义一个名为alphabet的元素类型为char的vector变量,其中有26个元素,每个都初始化为空字符。

26、描述vector中push_back()的含义。
答:push_back()是 vector 的成员函数,用来向 vector 对象填充数据,它将一个新元素添加到 vector 对象中,该元素成为 vector 对象的最后一个元素。

27、vector 的成员函数size()的功能是什么?
答:返回 vector 对象的元素个数。

28、为什么 vector 被如此广泛地使用?
答:因为它功能完备,从本章来看,它可以不预先指定 vector 大小,然后自由添加数据,并能通过一些成员函数得到目前的值,而且遍历所有序列元素比较方便,可以用范围for循环。

29、如何将一个 vector 中的数据排序?
答:用标准库函数sort(v);
(2021.04.26)

第五章:错误

1、举出四种主要错误类型并给出它们的简洁定义。
答:

  • 编译时错误:由编译器发现的错误,可进一步细分为,语法错误和类型错误。
  • 链接时错误:当链接器试图将对象文件链接为可执行文件时发现的错误。
  • 运行时错误:程序运行时发现的错误,可进一步细分为,由计算机(硬件或操作系统)检测出的错误,由库(比如标准库)检测出的错误,由用户代码检测出的错误。
  • 逻辑错误:由程序员发现的会导致不正确结果的错误,一般最难发现。

2、在学生练习程序中,什么类型的错误我们可以忽略?
答:我认为都不能忽略。

3、每一个完整的程序应该能提供什么保证?
答:
(1)对于所有合法输入应输出正确结果;
(2)对于所有非法输入应输出错误信息;
(3)不需要关心硬件故障;
(4)不需要关心系统软件故障;
(5)发现一个错误后,允许程序终止。

4、举出三种可以减少程序错误,开发出符合要求的软件的方法。
答:下列三种方法任何一种都不能保证完全消除错误,我们必须同时使用这三种方法。

  • 精心组织软件结构以减少错误。
  • 通过调试和测试,消除大部分程序错误。
  • 确定余下的错误是不重要的。

5、为什么我们会讨厌调试?
答:因为调试费时费力,效率低下,很搞心态。

注:以下四题以一个函数声明
int area(int length, int width);为例
6、什么是语法错误?给出五个例子。
答:

	int lenth = 5		//错误,丢失分号;
	int width = 3;
	int s1 = area(length;	//错误,丢失右括号 )
	Int s2 = area(length);	//错误,Int不是数据类型
	int s3 = area('width);	//错误,非终止符`丢失
	char c = 'a;			//错误,缺少右单引号‘

7、什么是类型错误?给出五个例子。
答:

	char c {1000};		//错误,窄化丢失数据
	char c = "c";			//错误,无法从const char *类型转换为char类型
	int x0 = arae(7);	//错误,未声明的函数(函数名拼错了)
	int x1 = area(length);	//错误,参数个数不匹配
	int x2 = area("length", width);	//错误,第一个参数类型不匹配(不能是字符串)

8、什么是链接时错误?给出三个例子。
答:

  • 每个函数在所有编译单元中的声明类型必须严格一致,如果不一致,链接器就会报错。比如我们在另一个编译单元声明double area(int length, int width),然后使用如下代码调用函数int s1 = area(4,3); ,由于double area(int length, int width)未定义,该编译单元在链接时无法找到对应的函数,因此链接器会报错。
  • 程序中的每个函数只能定义一次,否则链接器报错。比如我们最开始的int area(int length, int width)在两个不同的编译单元都定义过,那么将这两个编译单元链接时会出现报错:函数重定义,哪怕它们的定义是一摸一样的也会报错。
  • 同理,程序的其他实体,比如变量和类型,也只能有一个定义,但可以有多个声明,且每个声明必须具有相同类型。否则链接器报错。

9、什么是逻辑错误?给出三个例子。
答:

  • if (a == b);
    	a++; ```
    注意 if 语句后面有一个分号,因此不论如何,都会执行a++;
    
  •  if (a = b)
     	a++;```
     if 语句中是a = b,a被赋值为b的值,如果b≠0,那么a = b+1,结果与预期不一样。
    
  •  for(int i = 0; i >= 0; i++)
     {循环语句块}```
     这就是个死循环了。
    
  • 还有一个经典例子见书中的5.7

10、列出四种本章中讨论的可能导致程序错误的因素。
答:5.2错误来源

  • 缺少规划
  • 不完备的程序
  • 意外的参数
  • 意外的输入
  • 意外状态
  • 逻辑错误

11、你如何能知道一个结果是合理的?回答这类问题,你会用到什么技术?
答:用估计的方法判断结果是否合理。将常识和一些用来解决常见问题的非常简单的数学方法结合起来。

12、对比一下由函数调用者来处理运行时错误和由被调函数来处理运行时错误的异同。
答:相同点,都可以有效处理运行时错误;不同点,字面意思上看,错误处理者不同,被调函数来处理错误的好处在于参数检查只在一个地方实现,我们可以很方便地掌握参数检查全部信息,但是函数调用者处理错误可以知道哪里出错,并且知道如何处理错误。

13、为什么说使用异常比返回一个“错误值”要好?
答:因为返回错误值会带来一些不可接受的麻烦

  • 所有调用者和被调函数都需要进行检查,虽然调用者的检查可能会很简单,但是每次调用都必须编写这段代码,并决定在错误发生时如何进行处理。
  • 调用者可能会忘记做错误检查,这将导致程序在运行时出现不可预测问题,并且很难追溯原因。
  • 许多函数没有可以用作标记错误信息的额外返回值。比如一个用于从输入设备读入整数的函数,其返回值可以是任意整数,那么就没有一个专门的整数来表示错误信息了。
    然而异常把错误检测和错误处理分离,提供了一条可以把各种最好地错误处理方法组合在一起的途径,异常使错误处理变得简单一些。其基本思想就是:如果一个函数发现一个自己不能处理的错误,它不是正常返回,而是抛出 throw 一个异常来表示错误的发生,任一个直接或间接的函数调用者都可以捕捉到这一异常,并确定应该如何处理。函数用 try 语句来处理异常:把所要处理的异常情况罗列在 catch 语句后。

14、你应该如何测试一个输入操作是否成功?
答:测试 cin,我们可以确定输入操作是否成功,见5.6.3

15、描述一下抛出和捕获异常的过程:
答:如果一个函数发现一个自己不能处理的错误,它不是正常返回,而是抛出 throw 一个异常来表示错误的发生,任一个直接或间接的函数调用者都可以捕捉到这一异常,并确定应该如何处理。函数用 try 语句来处理异常:把所要处理的异常情况罗列在 catch 语句后,如果出现异常,则根据异常情况捕获 catch 进对应的异常处理方法程序块中。如果出现一个没被任何调用函数处理的异常,程序终止运行。

16、有一个名为 v 的 vector,为什么 v[v.size()]会导致范围错误?这一调用会导致什么结果?
答:因为 vector 是从0开始标记数据的,也就是说第一个数据的下标是0,那么第 v.size()个数据的下标应该是 v.size()-1,否则会导致范围错误,vector 抛出 out_of_range 异常。

17、描述一下前置条件和后置条件的定义;并举个例子(不能用本章的area()函数),最好是一个带有循环的计算过程。
答:前置条件:函数对于自己参数的要求,如果函数正确执行,这一条必须为真。
后置条件:如果一个函数返回一个值,那么总是要约定这个返回值是怎样的。

18、什么时候你 不会 测试前置条件?
答:

  • 没人会使用错误参数
  • 做前置条件检查会使程序变慢
  • 检查工作太复杂了,这个问题比较严重,我们可以通过编写前置条件注释来缓解。

19、什么时候你 不会 测试后置条件?
答:基本与18的答案类似,加一个情况:返回值可能是任意结果。

20、调试程序时的单步执行是指什么?
答:使用调试器运行程序,但每运行一个指令就暂停,等待调试者发出继续单步运行的指令,再去执行下一条指令。此时可以看到每条指令执行后,所对应的程序状态变化。

21、在调试程序时,注释会有什么帮助?
答:可以提高程序的易读性,注释可以解释代码不能说清楚的部分,告诉调试者程序的名称、目的、版本号、作者、创作时间、总体设计思想、源代码组织架构、复杂代码片段目的、输入数据的前提假设、未完成部分(即程序还不能处理那些情况)。

22、测试与调试有什么不同?
答:写完代码后,可能会有不少错误,我们需要通过调试来查找并排除错误。但错误是永远找不完的,因此,我们需要另一种方法——测试,把一个巨大的、系统选择过地数据集(测试用例集)输入给程序,然后把相关结果与期望值进行比较,以系统地查找错误。然后根据测试结果,再调试可能出错的功能代码,排除错误,并继续进行测试,可以说这是一个似乎停不下来的循环。
(2021.08.04)

第六章:编写一个程序

1、“程序设计就是问题理解”的含义是什么?
答:程序的编写往往都是从一个问题出发,也就是说,借助程序来解决一个实际问题,因此正确理解问题对程序实现是非常关键的。

2、本章详细讲述了计算器程序的设计,简要分析计算器程序应该实现哪些功能?
答:(1)输出提示信息,告诉用户应该如何使用这个计算器程序,即它可以实现哪些运算;(2)实现单词(token)这一类型,表示可以看作一个单元的一个字符序列,例如数字或者运算符,以分词的方式读取包括数字和操作符在内的表达式;(3)设计并实现简单的计算表达式文法;(4)实现单词流,可以从cin中读入单词,并将多读的单词放到缓冲区中等待下一次读取;(5)计算并输出答案,之后等待下一次表达式的输入。

3、如何把一个大问题分解成一系列易于处理的小问题?
答:我们需要先分析问题,判断应该做什么并且给出对当前问题理解的描述,即给出需求集合,然后分别对各项需求给出一定的解决方案,甚至可以对子需求进一步细分成更小的需求;在此过程中,我们寻找可以借助的工具和目前已有的解决方案,来解决某一部分小问题,这些是易于处理的;最后剩下的难以解决的问题就需要我们发挥聪明才智和创新力去完成它了。

4、为什么编写一个程序时,先编写一个小的、功能可控的版本是一个好主意?
答:当我们开始程序设计时,对要求解的问题并不十分了解,只有充分分析并且设计和实现之后才能深入理解要求解的问题,因此实现一个小的、功能可控的版本有助于引出我们的理解、思想和工具中存在的问题;可以让我们看到能否改变问题描述的一些细节使其更加容易处理;避免造成“功能蔓延”,不会大幅增加工作量,毕竟分阶段实现一个程序比一次完成要简单得多。

5、为什么功能蔓延是不好的?
答:因为这会大幅增加我们的编码、调试和测试时间,也就是大幅增加工作量,而分阶段实现一个程序比一次完成要简单得多。

6、软件开发的三个主要阶段是什么?
答:一、分析;二、设计;三、实现。

7、什么是“用例“?
答:如何使用程序的例子。

8、测试的目的是什么?
答:目的是通过一些系统选择过的数据集输入给程序,根据程序输出的结果和期望值进行比较,以查找潜在的错误。

9、根据本章的描述,比较Term、Expression、Number和Primary的不同点。
答:一个Number是一个浮点常量;一个Primary是一个Number或者是 ‘(’ 后接一个Expression再接一个 ‘)’ 这样的形式;Term则是一个Primary,或者Term本身后接 " * / %"这三个运算符之一,再接一个Primary,实际上,Term本身可以看成是一至多个Primary与可选的乘除模运算符组成的,但是运算符必须在Primary之间;Expression则是一个Term或者Expression本身后接 "+ -"再接Term,Expression本身可以看成一至多个Term与可选的加减运算符组成的,且运算符必须在Term之间。

10、在本章中,输入表达式被分解为Term、Expression、Number和Primary等组成部分,试按这种方式分析表达式(17+4)/(5-1)。
答:17、4、5、1都是Number,也都是Primary,进一步都是Term;之后可以把17与5堪称expression,再分别结合’+‘,把4看成Term,结合‘-’ ,把1看成Term,得到17+4和5-1都是Expression,而(17+4)与(5-1)都是Primary,把(17+4)看成Term,结合 ‘/’,把(5-1)看成Primary,得到(17+4)/(5-1)为Term,最后这个Term视作Expression。

11、为什么程序中没有名为number()的函数?
答:floating-point literal 可以通过标准输入cin 获得浮点数值,因此可以直接在primary中处理。

12、什么是单词?
答:单词(token)表示可以看作一个单元的一个字符序列,例如数字或者运算符。

13、什么是文法?文法规则是什么?
答:文法用来定义表达式的语法,文法规则是它在程序中的实现。设计文法需要解决这些问题:(1)如何区分文法规则和单词;(2)如何来排列文法规则(顺序);(3)如何表达可选的模式(多选);(4)如何表达重复的模式(重复);(5)如何辨识出起始的文法规则。

14、什么是类?类的作用是什么?
答:我认为类是一种数据类型和对该数据类型可执行操作的命名集合;类的作用就是定义程序所需要数据和对数据能执行的有效操作。

15、如何为一个类的一个成员提供一个缺省值?
答:

class Token_stream {
public:
    Token_stream();
    Token get();        // get a token
    void putback(Token t);  // put back a token
private:
    bool full; // is buffer full?
    Token buffer;   // a buffer for token by putback()
};

Token_stream::Token_stream()
							:full { false }, buffer { 0 }
{
}
//注意,此时为 full 和 buffer 两个成员设置缺省值

16、在expression()函数中,为什么switch语句的默认处理是退回单词?
答:因为该函数中,处理完一个term后,会继续读入一个单词,如果它不是’+'或‘-’,那么expression函数无法处理它,但它可能在其他地方有用,因此默认操作将该单词退回单词流中。

17、什么是”预读取“?
答:在正式处理读取操作前,从输入流中先读取一些数据,根据这些数据进行判断,如果是需要的数据那么正是读取,否则退回输入流中。

18、putback()函数的功能是什么?为什么说它是有用的?
答:putback()函数将调用者读取的且调用者无法处理的单词再退回单词流中。它有用的点在于使某个单词一旦无法被某个函数处理,可以再放回单词流等待下一次读取时其他函数可以处理。

19、在term()函数中,为什么难以实现取模运算符% ?
答:因为浮点数没有取模运算操作,而我们的number默认为double类型。

20、Token类的两个数据成员的作用是什么?
答:kind表示单词类型,说明它是数字还是运算符;value对数字有意义,存储数字的数值。

21、为什么把类的成员分为private 和 public两种类型?
答:因为我们想要将用户接口和具体实现分开。公有接口应该只包含用户需要的内容,私有实现包括实现公有函数所必须的内容,包括用于处理复杂细节的数据和函数。

22、对于Token_stream类,当缓冲区中有一个单词时,调用get()函数会发生什么情况?
答:get()函数首先判断缓冲区是否有单词,本题中有单词,那么get()函数将缓冲区已满标志位full 置为false,然后返回缓冲区中的单词。

23、在Token_stream类的get()函数中,为什么switch语句增加了对 ’ ; ’ 和 ‘ q ’ 的处理?
答:’ ; ’ 表示程序立即返回表达式结果,‘ q ’ 表示退出程序。

24、应该从什么时候开始测试程序?
答:我认为应该从实现一个原型后就开始测试,之后每增加一个功能都要测试。

25、”用户自定义类型“是什么?我们为什么需要这种机制?
答:用户自己设计和实现的数据类型和对该数据类型所能执行的有效操作,就是“用户自定义类型”。因为我们需要的类型非常多,但是C++语言及其标准库函数提供的类型是有限的,因此在程序设计中,需要用户自定义类型。

26、对于一个C++用户自定义类型,它的接口是指什么?
答:用public: 标识的接口。

27、我们为什么要依赖代码库?
答:因为在编写真正实用的软件时,重新设计基本模块是没有价值的,代码库已经将一些功能实现了,我们使用它可以节省大量工作量。我们没有必要重复造轮子。
(2021.08.11)

第七章:完成一个程序

1、为什么还要对程序的第一版本做这些改进?给出几条原因。
答:第一版程序并不完善,只是能够运行,还有很多有意义的功能没有加入,有意思的想法仍未尝试,新功能的添加必然会要改进甚至重新设计用户接口。程序的错误处理机制尚不完善,第一版遇到错误就直接退出了,但是有些错误是可以恢复的。程序源代码目前有些混乱,注释也并未进行整理,因此需要重构代码,并对注释进行适当的增加或删改。

2、为什么输入表达式1+2; q后,程序没有退出而是给出一个错误信息?
答:

double val{ 0 };

    while (cin)
    {
        cout << '>';
        Token t = ts.get();
        if (t.kind == ';')
            cout << "= " << val << endl;
        else if (t.kind == 'q')
            break;
        else
            ts.putback(t);
        val = expression();
    }

在第一版程序中,main()函数这段代码我们判断输入是分号后不再往后面检查,而是直接继续调用expression() 函数,最后一步一步调用到primary() 函数,然而字符q不是Primary,从而输出错误信息。

3、为什么选择把一个字符常量叫做number?
答:

4、为什么把main() 函数分成两个相互独立的部分,分别实现了什么功能?
答:程序的的基本结构应该清晰明了,易于理解,我们通过分析main()函数的逻辑功能,发现main() 函数主要做了两项逻辑上相互独立的任务:(1) main() 函数搭建程序整体框架——启动程序、结束程序、处理致命错误;(2) main() 函数用一个循环来计算表达式。因此main() 函数可以分成两个相互独立的部分。理想情况下,一个函数只实现一个独立的逻辑功能,而我们的main() 函数实现了两个功能,这使得程序结构变得有些模糊,所以一种改进方法是将表达式计算循环从主函数中分离出来,实现为calculate() 函数。

5、为什么把程序代码分成若干个小函数?试阐明划分原理。
答:划分原理:实现计算逻辑的分离,使代码更清晰(通过使用函数名),利用函数使同样的代码在程序中可以被多次使用,减少程序调试的工作量。

6、代码注释的目的是什么?如何为程序增加注释?
答: 注释一般用于代码本身很难表达思想的情况。换句话说,代码说明它做了什么,但没有表达出它做这些的目的是什么,因此需要注释来补充说明。注释应该是有效的,充分的,清晰的,如果代码足以说明,那就不要添加多余的注释了。具体的增加注释方法,见5.9.1节。

7、narrow_cast的作用是什么?
答:用于类型转换中的窄化操作,如果被窄化的数值不会因为截断等原因与窄化前数值不一样,那么可以正常窄化,否则抛出runtime_error异常。见5.6.4节

8、符号常量的使用方法是什么?
答:符号常量用于为程序中的某些不易通过其字面值理解的常量命名,使其代表某种意义,比如在本章计算器程序中,我们将 ‘8’ 字面值常量命名为 number——const char number = ‘8’; 这样在代表单词的类型为数字时,可以用t.kind = number,易于理解,而不是用t.kind = ‘8’ 这样的魔术常量。

9、为什么关心代码布局?
答:代码越混乱,可读性越差,其中的错误就难以发现,良好的代码布局往往可以使每个函数的代码都能全部显示在屏幕的可视区域上——屏幕之外我们无法看到的代码是最有可能隐藏bug的地方。因此代码布局很重要。

10、如何处理浮点数的模运算(%)?
答:通过标准库函数 fmod()来实现,需要包含头文件。或者通过式子 x % y = x - y * int(x / y) 来处理浮点数的模运算。

11、is_declared() 函数的功能是什么?它是如何工作的?
答:判断参数代表的变量是否已经被声明。它遍历预定义的var_table这一变量向量,将参数字符串与变量向量中的各变量名一一对比,如果找到一样的就立刻返回true,没找到的话最后返回false。

12、let单词对应的输入内容是由多个字符构成的,在修改后的程序中,如何将其作为单个单词读入?
答:程序定义两个符号常量const char let = ‘L’,const string decl = “let”,get() 函数的switch语句中的default 情况内,先判断读入的首个字符是不是字母,如果是,那么读入后面的字符直到非字母或者数字字符,读入的字符被组成字符串,然后与符号常量decl对比,如果相同,那么返回单词Token{let};

cin >> ch;
switch (ch) {
...		//省略前面的代码
	default:
		if (isalpha(ch)) {
			string s;
			s += ch;
			while (cin.get(ch) && (isalpha(ch) || isdigit(ch))) 
				s += ch;
			cin.putback(ch);
			if (s == declkey) return Token{ let };	// 声明关键字
			if (s == quitkey) return Token{ quit };
			return Token{ name, s };
		}
		error("Bad token");
}

13、计算器程序中的变量名可以是什么形式,不能是什么形式,对应的规则是怎么样的?
答:本书给出的规则是只能以字母开头,包含字母和数字的字符串。

14、为什么说以增量方式设计程序是一个比较好的主意?
答:在程序的分析、设计和实现这几个环节,通过增量方式会简化工作量,即单独划分程序各逻辑部分并分别实现和测试。

15、什么时候开始对程序进行测试?
答:我认为完成一个原型后,就要开始进行测试。甚至可以说实现拥有最基础功能的程序结构,并可以运行后,就应该开始测试。

16、什么时候对程序进行再测试?
答:增加一个新功能后,对代码进行清理后。最好是每做一点改动就测试一次。

17、如何决定函数的划分?
答:函数应该反映出该程序的基本结构,一个逻辑独立的功能可以单独划分为一个函数。

18、如何为变量和函数起名字?列出一些可能的理由。
答:变量和函数的名字首先应该遵循C++语法规则,不能用C++保留的关键字作为名字,然后不要与标准库,系统函数名,或者其他一些重要的库的名字重合或者相似(这里所谓的相似,是指名字以下划线开头)。最好选择有特定含义的名字,但也不要太长,比如,如果我们为变量或者函数起名为a,b,c,你可能过一段时间就不知道它代表什么了,如果起名为token,element_count,那么你能知道它意味着什么,另外remain_free_slots_in_symbol_table这个名字太长了影响阅读。使用首字母大写来定义自己的类型,比如计算器程序中的Variable、Token、Token_stream这三个类型,但是名字不要全部大写,因为全部大写的名字可能会和保留作为宏的名字重复。避免使用容易输错、误读或混淆的名字,比如说One和one,数字字符0与小写字母o和大写字母O,数字1和小写字母l,可能会让代码阅读者混淆。

19、为什么添加代码注释?
答:因为要用注释表达代码本身很难表达的思想。

20、注释中应该写些什么内容,什么内容不应该写?
答:见5.9.1节。但是有一句话得记住:最好的注释就是让程序本身来表达,有些意义很明确的代码,不应该有冗长的注释,比如说 x = b + c;这样的代码,不应该有注释。

21、什么时候可以认为已经完成了一个程序?
答:我认为,实现了根据当前需求设计的所有功能,清理完代码和修改注释后,程序编译通过能够正常运行,并且测试也通过了,那此时可以说暂时完成了一个程序。
(2021.08.17)

第八章:函数相关的技术细节

1、声明和定义有何区别?
答:声明declaration语句将名字引入作用域,其作用是:为命名实体(如变量、函数)指定一个类型,(可选)进行初始化(如为变量指定一个初始值或为函数指定函数体);如果一个声明给出了声明实体的完整描述的话,我们称之为定义。我们按惯例用“声明”表示“不是定义的声明”。声明与定义的区别反映出“如何使用一个实体(接口)”与“这个实体如何完成它应该做的事情(实现)”之间的根本区别。

2、我们如何从语法上区分一个函数声明和一个函数定义?
答:从语法上看,函数声明只提供了函数名与类型(参数类型和返回类型),而函数定义还提供了函数体(可执行的语句)。

3、我们如何从语法上区分一个变量声明和一个变量定义?
答:变量声明需要extern关键字,比如extern int a; 而变量定义则是int a; 或 int a = 2; (注意,初始化并不能用来区分变量声明和定义)

4、对于第6章的计算器程序中的函数,为什么不先声明就无法使用?
答:因为C++规范要求名字在使用前定义。我们不能要求编译器通过读程序找出定义,“先声明后使用”的原则简化了阅读。因此我们需要前置声明。

5、int a; 是一个定义,还是只是一个声明?
答:严格的讲,是一个定义,变量a占用内存空间,不过广义的说,它也是声明,毕竟定义也是声明,但声明不一定是定义。再次强调,初始化并不是用来区分变量声明与定义的。

6、为什么说在变量声明时对其初始化是一个好的编程风格?
答:因为我们可能会犯错误,如果在变量声明时候没初始化,之后在没有赋值的情况下使用该变量,会导致产生不可预测的错误,所以我们对变量进行初始化是好的编程风格,这样不容易产生bug。

7、一个函数声明可以包含哪些内容?
答:

返回值类型 函数名(参数列表)
{
	函数体
}
//参数列表包括,参数类型 参数名, [参数类型 参数名 [...]] 

8、恰当使用缩进有什么好处?
答:恰当使用一致的缩进格式来表明嵌套,可以让程序可读性大大提高,从而让错误难以隐藏。

9、头文件的用处是什么?
答:头文件是一些声明的集合,用于管理“别处”定义的功能的声明。为了方便一致性检查,我们在使用声明的源文件和给出定义的源文件中都包含头文件。头文件只能包含那些可以在多个文件中重复多次的声明(如函数声明、类定义和数值常量的定义)。

10、什么是声明的作用域?
答:作用域是一个程序文本区域。每个名字都定义在一个作用域中,其在声明点到作用域结束的区间内有效。作用域的主要作用是保持名字的局部性,使之不影响声明于其他地方的名字。

11、有几种作用域?请各举一例。
答:

  • 全局作用域:在任何其他作用域之外的程序区域,比如说全局变量就处于全局作用域中。
  • 名字空间作用域:一个名字空间作用域嵌套于全局作用域或另一个名字空间作用域中。
//从这里开始就是全局作用域
namespace cyberspace{
	int var = 0;
	void print();
	//一些其他声明与定义
}
namespace punkspace{
	int var = 1;
	void print();
	//一些其他声明与定义
}
using namespace cyberspace;
//从这里开始是cyberspace名字空间作用域,其嵌套于全局作用域
{
	using punkspace::var;
	using punkspace::print();
	//上面两个using声明表示var和print()是在punkspace的名字空间作用域的影响下的
	var = 2;
	print();
}
  • 类作用域:一个类内的程序区域。
class my_class{
//这里面就是类作用域
}
  • 局部作用域:位于{…}大括号之间或函数参数列表中的程序区域。
  • 语句作用域:例如for语句内的程序区域。

12、类作用域和局部作用域有何区别?
答:类作用域是由类定义所划定的程序范围,在类作用域内有效的是类的成员。局部作用域则是打括号之间或参数列表中的程序区域,并非要是类定义中的区域。

13、为什么应该尽量少用全局变量?
答:因为全局变量可能被任何函数修改,但程序员很难知道是哪个函数修改了它,或者说,程序员没有有效的方法获知程序的哪个部分读或者写了一个全局变量;另一个原因是不同编译单元中的全局变量的初始化顺序是不确定的,见P186。

14、传值和传引用有何区别?
答:传值是将参数的值拷贝一份交给函数,一个函数 f() 的传值的参数实际上是 f() 中的局部变量,每次 f() 被调用时都会初始化,这个参数不影响原来的实参。传引用则是传递一个对象的别名,函数 f() 的传引用的参数实际上是调用者的实参对象的别名,也就是说我们可以对传引用方式的参数指向的对象进行读和写,效果相当于对实参对象的读和写。

15、传引用和传常量引用有何区别?
答:传常量引用与传引用的区别在于,传常量引用不能修改传入的对象的值;另外常量引用不需要一个左值,它可以像初始化和传值方式一样进行转换,常量引用传入字面常量时,编译器会为它生成临时对象。见P178。

16、swap() 是什么?
答:交换两个对象的值的函数。

17、定义一个函数,它带有一个 vector类型的传值参数,这样做好吗?
答:不好,因为它是以传值的方式向函数传递vector类型参数,那么就需要拷贝一份交给函数,万一这个vector类型的实参特别大,那么拷贝的代价就很高了,甚至可能会因为内存不足造成程序崩溃。

18、给出一个求值顺序不确定的例子,并说明为什么求值顺序不确定是一个问题。
答:

int i = 0;
int a = ++i + ++i;	//例1
vector<int>v(10);
i = 0;
v[++i] = i;			//例2

例1说明两个++i的顺序不确定的,尽管,对于有先后顺序的情况,谁先谁后不影响结果,但是还有一种可能是它俩同时用 i == 0 这个值进行自增,那么这就是另一种结果了。
例2说明 =(赋值符)在表达式中只是又一种运算符而已,没有特殊地位,不能保证赋值符左边的子表达式在右边的子表达式之前计算。

19、x && y 和 x || y 分别表示什么?
答:x逻辑与y 和 x逻辑或y,表达式的结果是布尔值。

20、下面哪些语法结构符合C++标准:函数中的函数,类中的函数,类中的类,函数中的类?
答:除了函数中的函数不符合C++标准,其他都符合。但是类中的类这种嵌套类只在复杂类中才有用,理想情况是保持类简短、简单;函数中的类这种局部类应当尽量避免。

21、一个活动记录内都包含什么内容?
答:包含函数返回到调用者的返回地址信息,还可能包含一个返回给调用者的值。

22、调用栈是什么?我们为什么需要调用栈?
答:调用栈是一个活动记录栈,每次调用函数时,都会在调用栈中生长出一个记录,返回时其记录不再有用;我们应用调用栈来管理每一次函数调用,使每次函数调用的活动记录(不论是调用相同的函数)都保持独立。

23、名字空间的作用是什么?
答:无须定义一个类型就能将类、函数、数据和类型组织成一个可识别的命名实体,实现这种声明分组功能的C++机制就是名字空间。

24、名字空间和类有何区别?
答:类其实是一个用户自定义类型,来将函数、数据和类型组织到一个类型中;而名字空间则无需定义一个类型即可实现声明分组功能。

25、using声明是什么?
答:如下所示的语法结构就是using声明。

using std::string;
using std::cout;

26、为什么应该避免在头文件中使用using指令?
答:除非是std这种在某个应用领域中大家已经非常熟悉的名字空间,否则最好不要使用using指令。如果在头文件中使用过度的using指令,那么由于在其他文件中往往要包含头文件,会有名字冲突的隐患,即某个源文件定义的某个名字和头文件的using指令的名字空间中的名字发生了冲突。

27、名字空间 std 是什么?
答:C++标准库的功能都定义在 std 名字空间中。
(2021.9.1)

第九章:类相关的技术细节

1、本章中所描述的类的两个组成部分是什么?
答:类的数据成员和类的函数成员。注意是本章中描述的类。

2、一个类中,接口和实现的区别是什么?
答:共同点是它们都是类声明的一部分。区别在于,接口可以被用户直接访问,是类的用户视图;而实现则是类的细节部分,其只能通过接口间接访问,是类的实现者视图。一般公有的接口用标号public: 标识,实现用private: 标识。

3、本章中最初定义的 Date 结构有什么局限和问题?
答:一个结构就是一个成员默认为公有属性的类,结构主要用于成员可以取任意值的数据结构。最初定义的 Date 结构的内部实现细节能够被所有用户知晓,也就意味着成员可以被任意修改,用户可能不通过我们预定义的成员函数来对数据成员进行操作,而是直接操作数据成员,这会令某些定义在成员函数内的检查被绕过,造成隐患。

4、为什么要为Date类型定义构造函数来取代函数 init_day()?
答:因为 init_day() 是一个类外的辅助函数,它并不能保证程序员在定义Date类对象后会调用它,程序员甚至会忘记初始化Date对象后使用该对象,导致出现例如产生垃圾输出等问题。而为Date类型定义构造函数可以在定义类对象时默认或强制初始化,特别是需要参数的构造函数,如果程序员忘记利用它初始化类对象的话,编译器会捕获这个错误,所以我们要为Date类型定义构造函数。

5、什么是不变式?给出一个例子。
答:不变式是判定合法值的规则。我个人理解可以是一组条件式,作用于类上,则用于限定对象,拒绝不合法状态,使对象保持合法状态。比如说我们定义一个红绿灯类,其数据成员的合法值仅限于红、黄、绿三种颜色,不变式就是用于判定颜色是否是合法的三种颜色,当我们定义一个交通指示灯对象时,不变式的存在确保能够该对象的状态合法。

6、什么时候应该将函数定义置于类定义内?什么时候又应该置于类外?为什么?
答:对于简单的、较小的、被频繁调用的函数,我们应该考虑直接在类声明中给出其定义,因为这样函数将成为内联函数,会带来程序性能提升。不过,除去上面的情况,一般我们都应该将函数定义置于类外,有两点好处:1、控制类定义代码规模,不会使我们在类定义中找成员变得困难;2、每当我们对内联函数的函数体作出修改时,所有使用这个类的程序都不得不重新编译,这对大型程序来说使很糟糕的,而将函数定义置于类外(相当于函数体在类外)则无需重新编译所有使用这个类的程序,只需要编译类定义源代码即可。

7、在程序中什么时候应该使用运算符重载?给出一个你可能想重载的运算符列表(对于每一个请给出一个原因)。
答:当需要为用户自定义类型提供习惯的符号表示法的时候,就需要使用运算符重载。== != < <= > >=,这些运算符重载后,可用于以比较的方式的排序中,特别是针对非数值、字符或字符串类型的用户自定义类型排序。

8、为什么应该令一个类的公有接口尽量小?
答:我们希望接口尽量小,是因为小的接口易于学习和记忆,而且实现者也不用为不必要的和很少使用的功能浪费大量时间。小的接口还意味着有错误发生时,我们只需要检查很少的函数就可定位错误。平均来看,公有成员函数越多,查找bug就越复杂。另外,如果类定义改变,只有直接访问类的函数才需要重写,这意味着公有接口需要重写,这是保持公有接口最小化的另一个重要原因。不过尽量小的前提是保证完整性,否则接口就没用了。

9、为一个成员函数加上const限定符有什么作用?
答:在成员函数的声明(定义)中,我们将 const 限定符放置在参数列表右边,就表示这个成员函数可以在一个常量对象上调用,而且一旦将一个成员函数声明为 const,编译器会帮助我们保证这个成员函数不会修改对象。

10、为什么辅助函数最好放在类定义外?
答:我认为,辅助函数放在类外是公有接口最小化的思想的一个体现。如果一个函数可以实现为非成员函数,那么就应该将它的实现放在类外,采用这种方式,函数中的错误就不会直接破坏类对象中的数据。常用的debug技术是“首先排查惯犯“,当类出现问题时,我们首先检查直接访问类的函数(成员函数):几乎可以肯定是这类函数导致的错误,如果我们定义类时采用了保持接口的最小化原则,那么我们在debug时就会比较方便了。另外如果一个类定义发生改变,放在类外的辅助函数可能不需要重写,可以减轻工作量。
(2021.9.8)

第十章:输入输出流

1、对于大多数现代计算机系统,处理输入和输出时,要处理的设备种类有哪些?
答:多媒体类:显示器、显卡、声卡等;文件类:磁盘 CD;事件类:键盘、鼠标;网络类:网卡。

2、istream的基本功能是什么?
答:用于处理输入流,它从某处(如控制台、文件、主存或另外一台计算机)获取字符,将字符序列转换为不同类型的值。

3、ostream的基本功能是什么?
答:处理输出流,它将不同类型的值转换为字符序列,将这些字符发送到某处(如控制台、文件、主存或另外一台计算机)。

4、从本质上看,文件是什么?
答:文件可以简单看作一个从0开始编号的字节序列,且有自己的格式。

5、什么是文件格式?
答:文件格式是一组规则,用来确定文件中的字节的含义。

6、给出四种需要进行I/O的设备类型。
答:我感觉和第一问没什么差别。

7、读取一个文件的四个步骤是什么?
答:

  1. 根据文件名打开文件,即创建一个 ifstream 对象
  2. 检测文件是否成功打开
  3. 从输入流中执行读操作
  4. 关闭文件,刷新 ifstream 缓冲区,释放 ifstream对象

8、写一个文件的四个步骤是什么?
答:

  • 根据文件名打开文件,即创建一个 ofstream 对象
  • 检测文件是否成功打开
  • 向输出流中执行写操作
  • 关闭文件,刷新 ofstream 缓冲区,释放 ofstream对象

9、给出四种流状态的名称和定义。
答:

  • good() 操作成功
  • eof() 到达输入末尾(“文件尾”)
  • fail() 发生某些意外情况(比如想读入数字,却读入字符’x’,一般可以恢复)
  • bad() 发生严重的意外(如磁盘读故障,往往无法恢复)

10、讨论如何解决如下输入问题。
a. 用户输入了要求范围之外的值。
答:用一个 if 语句检查输入值是否在范围内,若在范围外,则输出提示信息,要求用户重新输入正确范围内的值。
b. 未读到值(到达文件末尾)。
答:用eof()方法检查是否因到达文件末尾而未读到值,并结束读取操作。
c. 用户输入了错误类型的数据。
答:用fail() 方法检查是否出错,抛出异常,由异常处理程序决定是重新要求输入还是退出。

11、输入通常在哪些方面比输出更难处理?
答:当需要输入格式化数据时,就会比较难以处理,需要进行各种错误检查。

12、输出通常在哪些方面比输入更难处理?
答:如果程序运行环境中输出设备不可用、队列满或者发生故障的概率比较高,我们就得在每次输出操作之后都检查其状态。

13、我们为什么通常希望将输入输出与计算分离?
答:这是程序设计中,“将程序划分为可分别处理的多个部分” 这一策略的实例,由于输入输出与计算是逻辑分离的,因此我们可以分别分析、设计并实现输入、输出以及计算 程序,提升效率。

14、istream的成员函数clear() 最常用的两个用途是什么?
答:无参数的 clear() 用于将 istream 对象的状态置为 good(),我认为应该是将所有状态位都复位,那么就自然处于 good() 状态了;有参数的 clear() 用于将参数中所指出的 istream 状态位置位(进入相应状态),未指出的状态位会被复位。

15、对于一个用户自定义类型X,<< 和 >> 通常的函数声明形式如何?
答:

ostream& operator<<(ostream&, X&);
istream& operator>>(istream&, X&); 

(2021.09.18)

第十一章:定制输入输出

1、为什么I/O对于程序员来说比较棘手?
答:因为 I/O 是繁杂的,不一致的,程序员不能以统一的方式处理所有的输入,也不应该以单一标准输出,程序的存在是为了服务于人类,而人们都有自己强烈的偏好,I/O就是一个体现人类偏好地例子。作为程序员,我们必须力争在程序复杂性和满足用户个人偏好间达到平衡。

2、符号< 答:通知流应该以十六进制输出任何后续的整型值。

3、十六进制数在计算机科学中的作用是什么?为什么?
答:十六进制多用于输出与硬件相关的信息,原因在于一个十六进制数字精确地表示了4位二进制值。两个十六进制数字可以表示一个8位字节。

4、为你想实现的几个整数输出格式化选项命名。
答:想实现汉字形式的整数(零一二三四五六七八九十百千万亿)。

5、操纵符是什么?
答:改变流的行为的关键字。

6、十进制数的前缀是什么?八进制呢?十六进制呢?
答:十进制数在流输出中一般没有前缀(哪怕showbase也不显示),如果硬要有前缀的话,是0d,
八进制前缀是0,十六进制前缀是0x或0X。

7、浮点数值的默认输出格式是哪种?
答:浮点数值默认输出格式是 defaultfloat,精度是6位数字,四舍五入。

8、什么是域?
答:域可以设定整数、浮点数和字符串输出占用多少个位置,不过不截取,也不持久。

9、解释 setprecision() 和 setw() 的作用?
答:操纵符 setprecision() 设置浮点数精度,操纵符 setw() 精确指定一个整数、浮点数或者字符串输出占用多少个位置,但不截取,也不持久。

10、文件打开模式的目的是什么?
答:文件打开模式的目的:为我们访问文件这个字节序列提供更多的方式。

11、下面哪个操纵符不是持久的:hex、scientific、setprecision()、showbase、setw()?
答:setw() 不是持久的。

12、字符串 I/O 和二进制 I/O 的差异在哪里?
答:字符串 I/O 将字节序列表示为字符串的形式,而二进制 I/O 则将对象在内存中对应的字节序列简单地复制到文件,或者从文件中将字节序列复制到内存中。

13、给出一个例子,说明使用二进制文件比使用文本文件更好。
答:当处理图像、声音等没有适合的字符表示方式的多媒体文件时,使用二进制文件比文本文件更好。

14、给出两个例子,说明 stringstream 的用途。
答:

15、什么是文件位置?
答:以读方式打开的文件,都有一个“读/获取位置”,用seekg()移动位置;而以写方式打开的文件,都有一个“写/放置位置”,用seekp()移动位置。如果是默认字符串I/O,则文件位置可以认为是第x个字符的起始位置(x从0开始计数)。

16、如果你试图定位到文件尾之后,会出现什么结果?
答:结果如何是未定义的,不同操作系统会有不同的行为。我在Windows 10上用 VS2019 试了一下,发现程序并没有报错,不过如果定位到文件尾后的位置,再向该文件写入字符,那么文件会出现乱码。

17、什么情况下,使用面向行的输入比面向类型的输入更好?
答:因为我们要做一些>>做不了的事。直接一行读入一个字符串后,我们可以对该行字符串的每个字符进行操作。比如,当我们遇到一行有数字又有字符和字符串的输入,而且它们的顺序未知时,直接将一行读入再进行处理明显比面向类型的输入更好。

18、isalnum(c) 的功能是什么?
答:判断 c 是不是数字或者十进制数字。
(2021.09.23)

第十二章:向量和自由空间

1、为什么我们需要元素数量可变的数据结构?
答:因为程序实际运行环境中,有很多场景需要添加或者删除元素,这时候元素数量可变的数据结构就会非常有用,另外,针对在编程时难以估计程序实际运行所需元素数量的情况,元素数量可变的数据结构能帮助我们应付这样的情况;预先设定固定数量元素的数据结构存在一些问题,比如为了应付极端情况,元素数量设置过大,而平均情况所需的元素较少,这会导致浪费内存空间。

2、一个典型的程序包含哪四类存储?
答:(1) 代码存储 / 文本存储、(2) 静态存储、(3) 栈存储 / 自动存储、(4) 自由空间 / 堆

3、什么是自由空间?它常用的其他名称是什么?那种运算符支持它?
答:自由空间 (Free Storage),是计算机虚拟内存中的一块区域,用于支持程序运行时的内存分配。它的常用名是堆 (Heap),运算符 new 和 delete 支持它。

4、什么是解引用运算符,为什么我们需要它?
答:解引用运算符 * ,也叫内容运算符,是一种一元运算符,当我们想查看指针指向的对象的值时,需要使用解引用运算符。

5、地址是什么?在C++如何操纵内存地址?
答:地址是指明内存中位置的数字,用于指明内存中某对象所在的存储区域的地址,一般这个数字是对象的首地址。C++中,地址运算符 & 用于获得一个对象的地址,而保存地址的对象被称为指针,Type* 指针 = &对象。解引用运算符* 用于查看指针所指向对象的值 *指针,指针可以有 + 和 - 操作以及使用下标运算符[ ]。

6、指针中包含指向的对象的什么信息?又缺少什么有用的信息?
答:指针中包含指向的对象的地址信息,但指针缺少指向多少个对象的信息。

7、一个指针可以指向什么?
答:指针可以指向某一块特定类型的内存区域,甚至可以是 void 类型;可以指向某类型数组的首个元素。我认为指针是自由的,可以指向任何内存地址。

8、什么是泄露?
答:泄露往往指内存泄漏,主要是程序在运行时,从堆中分配空间给程序使用,但分配的空间在使用完后没有释放,导致其占用的空间在程序接下来的运行过程中无法使用,这就是泄露。

9、什么是资源?
答:在本章中,我觉得资源是指内存空间,尤其是指内存中的堆(自由空间)。

10、我们如何初始化一个指针?
答:

int a{1};
int* pa { &a };		//初始化一个指针
int* pb { nullptr };	//用空指针初始化一个指针

11、什么是空指针?我们什么时候需要使用它?
答:当 0 值被赋给一个指针时,它被称为空指针 nullptr。如果我们没有其他指针用来初始化一个指针,那么就使用空指针 nullptr。

12、我们什么时候需要一个指针(而不是一个引用或具名对象)?
答:

  • 当我们需要指向不同的对象时。
  • 当我们调用函数传入参数时,借用书上P286的例子,incr_p(&x); 这个调用语句中的 & 符号提醒我们x可能会被改变,而 incr_r(x)则不容易感知到x可能会被改变,这种较为明显的警示,让某些人更想要用指针。
  • 对于“没有对象”(用nullptr表示)是有效参数的函数,使用指针参数。所谓的”没有对象”是有效参数的函数,是指不传递对象作为参数,只传递nullptr。

13、什么是析构函数?我们什么时候需要使用它?
答:析构函数就是一个做与构造函数相反事情的函数,确保一个对象销毁前被正确清理。当一个对象离开其作用域、被delete释放时会隐式调用析构函数。

14、我们什么时候需要一个 virtual 析构函数?
答:如果有一个类带有 virtual 函数,则它也需要一个 virtual 析构函数。原因在于如果一个类有 virtual 函数,它很可能作为一个基类使用,它的派生类很可能使用 new 来分配,并通过基类指针来操作,那么派生类很可能也是通过基类指针来进行 delete 操作的,如果基类的析构函数是 virtual 的,则会使用 virtual 调用机制——这个调用会转向派生类的析构函数,从而正确销毁派生类对象。若基类的析构函数不是 virtual,则不会正确销毁派生类对象。

15、成员的析构函数如何被调用?
答:成员的析构函数是在对象被销毁时被对象的析构函数隐式或者间接调用的。

16、什么是转换?我们什么时候需要使用它?
答:C++有static_cast、dynamic_cast、const_cast、reinterpret_cast 这些转换,本章中说的转换主要是指 static_cast,显示类型转换,也就是C中的强制类型转换,用于在两种相关类型之间进行强制转换。比如说,当我们需要将void* 转换为 double*,就可以用

void * pv;
double* pd = static_cast<double*>(pv);

这种方式转换

17、我们如何通过指针访问类成员?
答:成员访问运算符有两个:‘.’ 和 “->”,通过指针访问类成员用"->"。

18、什么是双向链表?
答:对于每一个链接,我们都可以获得其前驱predecessor和后继successor。

19、什么是 this?我们什么时候需要使用它?
答:this 是指向当前对象的指针,即指向调用当前成员函数的对象。每当我们访问一个成员时,都隐式使用了 this 指针,只有当我们要涉及整体对象时才需要显式指出它。
(2021.09.27)

第十三章:向量和数组

1、“买者自慎”的含义是什么?
答:买者自慎” 意指在交易过程中买方应当依赖自己掌握的知识作出判断,卖方对于有关交易的信息没有告知买方的义务。这在C++程序设计中,意味着程序员应该对于C++实现以及C++标准库或者其他第三方库的实现有所了解,也就是说程序员在应用这些实现的时候也需要了解这些实现的内在原理和实现方法。

2、对于类对象而言,拷贝的默认含义是什么?
答:对于类对象,拷贝的默认含义是“拷贝所有的数据成员”,但不拷贝数据成员中的指针所指向的内存区域中的数据。

3、类对象拷贝的默认含义在什么情况下是合适的?什么情况下是不合适的?
答:当类对象的数据成员不包括指针和引用时,类对象拷贝的默认含义是合适的,反之则不合适。

4、什么是拷贝构造函数?
答:拷贝构造函数是一个进行拷贝操作的构造函数,提供一个复制元素的拷贝操作,当用一个类对象初始化另一个类对象时,会被调用。

5、什么是拷贝赋值?
答:通过赋值的方式拷贝,默认的拷贝赋值是逐成员的拷贝,但我们可以定义针对某类对象的拷贝赋值方式。

6、拷贝赋值与拷贝初始化之间有什么区别?
答:拷贝赋值以赋值的方式拷贝类对象,拷贝赋值的左值对象必须是已经定义了的对象;拷贝初始化则以拷贝类对象的方式,定义并初始化一个新的类对象作为被拷贝对象的副本。

7、什么是浅拷贝?什么是深拷贝?
答:

  • 浅拷贝:只拷贝对象的值,包括指针也是只拷贝其地址值。
  • 深拷贝:在浅拷贝基础上,还拷贝原指针指向的数据,并让新指针指向新拷贝的那些数据,原指针和新指针将指向两个不同的对象。

8、一个 vector 对象的副本与该 vector 对象之间有何不同?
答:副本在内存中的位置和 vector 对象不同,也就是说,副本的数据成员的地址,副本的指针成员指向的数据的地址,都与原 vector 对象不同,两者除了值相同以外,就是两个对象。

9、类的七种必要操作有哪些?
答:原题是五种,但我觉得可能是勘误,应该是七种。

  1. 默认构造函数
  2. 接受一个或多个参数的构造函数
  3. 拷贝构造函数(拷贝同一类型的对象,下同)
  4. 拷贝赋值操作
  5. 移动构造函数(移动同一类型的对象,下同)
  6. 移动赋值操作
  7. 析构函数

10、什么是显式构造函数?在什么情况下应该使用显式构造函数?
答:显示构造函数是由关键字 explicit 定义的构造函数,只能用于对象的构造而不能用于隐式转换。由于只接受一个参数(或者除了那一个参数外,其他参数都有默认值)的构造函数定义了一个从其参数类型向所属类的类型转换操作(隐式类型转换),当我们想要禁止将构造函数用于隐式类型转换时,应使用显示构造函数。

11、对于一个类对象而言,哪些操作是被隐式调用的?
答:当一个类对象被构建(初始化对象、用new创建对象、拷贝对象)时,其构造函数会被隐式调用;当一个类对象被赋值给另一个类对象时,若定义了赋值操作,那么拷贝或者移动赋值操作会被隐式调用;当一个类对象被销毁(作用域结束、程序结束、delete用于一个指向类对象的指针)时,析构函数会被隐式调用。

12、什么是数组?
答:数组是内存中连续存储的同构对象序列;也就是说,一个数组中的所有元素都具有相同的类型,并且各元素之间不存在内存间隙。数组中的元素从0开始顺序编号。

13、如何拷贝一个数组?
答:

int x[100];
int y[100];
/*
	...
*/
//现在要将数组y拷贝至数组x
//法1
for (int i = 0; i < 100; ++i)
	x[i] = y[i]
//法2
memcpy(x, y, 100 * sizeof(int));
//法3
copy(y, y + 100, x);

14、如何对数组初始化?
答:char 数组可用字符串字面值常量初始化cpp char ac[] = "Beorn";
所有数组都可以用一个其元素类型值的列表进行初始化。

int a[] = {1,2,3,4,5,6};	//6个int数组
int b[100] = {0,1,2,3,4,5,6,7,8,9);		//剩下的90个元素被初始化为0
double c[100] = { };		//所有元素被初始化为0.0
char chars[] = {'a', 'b', 'c');			//无结尾0,这是字符的数组,不是字符串

15、什么时候应该使用指针参数而不是引用参数?为什么?
答:当我们可能要让这个参数指向的别的对象时,应该使用指针参数而不是引用参数,因为引用参数初始化后就无法使其改变指向别的对象。

16、什么是C风格字符串?
答:C风格字符串是指以0结尾的字符串。

17、什么是回文?
答:回文是一种单词,它顺序拼写和逆序拼写的结果是相同的,anna、petep 和 malayalam都是回文。
(2021.09.30)

第十四章:向量、模板和异常

1、为什么我们需要调整 vector 对象的大小?
答:因为改变大小的操作在实际中是非常常见的,而我们编写程序时,往往事先并不知道程序运行时 vector 对象会获得多少数据,如果固定元素数量,那么分配过大会导致内存资源浪费,过小会使程序出现不可预知的问题(可能会导致缓冲区溢出等问题),因此需要调整 vector 对象的大小。

2、为什么我们需要使用具有不同元素类型的 vector 对象?
答:vector 只是一个向量,而我们在程序设计中会处理各种类型的数据,比如double、string、指针,乃至用户自定义类型,这些数据需要存储在 vector 对象中时,我们就要使用具有不同元素类型的 vector 对象。

3、为什么我们不在所有可能的情况中定义一个具有足够大规模的 vector 对象?
答:因为元素数量和元素类型的可能是无限的,也就是永远也定义不完这样一个足够大规模的 vector 对象。

4、我们需要为一个新的 vector 对象分配多少空闲内存空间?
答:如果定义一个新的 vector 对象时没有定义大小,那么不分配空闲内存空间,否则按照指定的大小分配空闲内存空间,如果是通过拷贝构造形式定义了一个新的 vector 对象,那么无需关心空闲空间,只分配被拷贝对象的元素大小的空间,如果通过移动构造的形式定义的新的 vector 对象,那么其空闲空间与移动过来的 vector 对象的空闲空间大小一致,其实就是那一块相同的空闲内存空间。

5、在何时我们必须将 vector 对象包含的元素拷贝至新的内存空间?
答:当我们分配新空间的大小大于 vector 对象原来的空间大小时,必须将 vector 对象包含的元素拷贝至新的内存空间。

6、在一个 vector 对象构造成功之后,哪些 vector 操作能够改变它的大小?
答:resize()、push_back()、赋值,这三种是第十四章实现的改变 vector 大小的基本操作,标准库 vector 还有erase() 和 insert() 等操作。

7、拷贝结束后,vector 对象的取值如何?
答:如果是自拷贝,那么 vector 对象的取值不变,如果是拷贝的另一个 vector 对象,那么拷贝结束后vector 对象的元素数量大小和空闲空间大小一样,都等于被拷贝对象的元素数量大小,指针指向的内存空间存储的是被拷贝对象的各元素的副本。

8、哪两个操作定义了 vector 的拷贝?
答:拷贝构造 和 拷贝赋值。

9、对于类的对象而言,拷贝的默认含义是什么?
答:对于类对象,拷贝的默认含义是“拷贝所有的数据成员”,但不拷贝数据成员中的指针所指向的内存区域中的数据。

10、什么是模板?
答:模板是一种机制,它令程序员能够使用类型作为类或函数的参数,当随后我们提供具体类型作为参数时,编译器会为之生成特定的类或函数。

11、最有用的两种模板参数类型是什么?
答:类型、整数。

12、什么是泛型编程?
答:编写能够正确处理以参数形式呈现的各种类型的代码,只要这些参数类型满足特定的语法和语义要求。

13、泛型编程与面向对象编程之间有什么区别?
答:

  • 泛型编程由模板支撑,依赖编译时解析(比如被调用函数的选择由编译器在编译时确定)
  • 面向对象编程由类层次和虚函数支撑,依赖运行时解析(比如被调用函数的选择直到运行时才确定)。
    虽然两类编程都被称为多态,但是泛型编程的多态被称为参数化多态,而面向对象编程的多态则是即时多态

14、array 与 vector 有什么区别?
答:array 所包含的元素数在编译时就已确定,且运行时不能更改,vector 则没有这些限制。

15、array 与内置数组有什么区别?
答:数组不知道自身的大小,不能正确拷贝,可以很容易地转换为指针,这些问题使数组容易造成错误,而 array 则类似 vector,不存在这些问题。

16、resize() 和 reserve() 有什么区别?
答:reserve() 改变 vector 指针成员指向的空间的大小,但不会减少分配的空间,也就是说,如果想分配的空间比原空间小,那么reserve() 并不会做出操作;resize() 改变 vector 对象的元素数量,它会首先调用 reserve() 。

17、什么是资源?给出它的定义并举例说明。
答:资源是一种其使用者必须向系统中的“资源管理者”归还(释放)资源,并由“资源管理者“负责资源的回收。内存、锁、句柄、线程句柄、套接字、窗口等都是资源。最简单的例子就是自由存储区的内存空间,我们作为使用者必须通过 new 获得内存空间,然后必须通过 delete 归还所获得的内存空间。

18、什么是资源泄露?
答:获得资源后未归还(释放)资源,就会导致资源泄露。

19、什么是 RAII,它能解决什么问题?
答:Resource Acquisition Is Initialization,RAII,资源获取即初始化。它能解决资源泄露的问题,即通过对象的构造函数获取资源,并通过对应的析构函数释放资源。

20、unique_ptr 的用途是什么?
答:unique_ptr 是一种能够存储指针的对象,它可以被看作一种指针,但不同的是,它拥有所指向的对象,也就是说当销毁 unique_ptr 时,它会 delete 所指向的对象。unique_ptr 对象的 release() 成员函数会提取出它所保存的指针,从而可以返回该指针,并使 unique_ptr 对象保存的指针变为 nullptr,从而销毁 unique_ptr 对象时不会销毁其他任何对象。unique_ptr 的一个重要限制是:不能将一个 unique_ptr 赋予另一个 unique_ptr 从而让它们指向相同的对象。unique_ptr 与普通指针相比,没有额外开销。
unique_ptr 可以作为实现 RAII 的一种方式。
(2021.10.05)

第十五章:容器和迭代器

1、为什么不同人编写的代码看起来会不一样?请举例说明。
答:代码是用来实现某个问题的解,而解决一个问题往往并不是只有一种方法。

2、会有哪些关于数据的简单问题?
答:存储、如何增、删、查、改,等等问题。

3、存储数据有哪些不同的方式?
答:数组、向量、链表、队列、树,等等。。。

4、可以对一组数据做哪些基本操作?
答:收集数据并装入容器,组织数据,提取数据,修改容器,进行简单的数值计算。

5、理想的数据存储方式应该是怎样的?
答:应该能够高效利用内存空间,能够方便地被遍历,能够被高效地查找,增删改时不被破坏原定地存储方式(数据结构不被破坏)。

6、什么是STL序列?
答:从STL角度看,数据集合就是一个序列。序列具有头部和尾部。可以对一个序列从头到尾遍历,对序列中元素进行有选择地读写操作。

7、什么是STL迭代器?它支持哪些操作?
答:STL迭代器是一种可以标识序列中元素的对象。迭代器指向序列中地某个元素(或者序列末端元素之后),可以使用 == 和 != 来对两个迭代器进行比较,可以使用单目运算符 * 来访问迭代器所指向的元素,可以利用操作符 ++ 来令迭代器指向下一个元素。

8、如何把迭代器移到下一个元素?
答:用操作符++

9、如何把迭代器移到上一个元素?
答:只有随机访问迭代器和双向迭代器可以这么做,随机访问迭代器可以对迭代器用操作符 - - 或者 使用操作符 - 向迭代器减去1使之移到上一个元素,双向迭代器可以使用操作符 – 向后移动到上一个元素。

10、当你试图把迭代器移动到序列尾之后时会出现什么情况?
答:序列尾本来就是序列之外的地方,可能是空地址,再往后移动,某些C++STL实现可能会报错。

11、哪些迭代器可以移动到上一个元素?
答:随机访问迭代器和双向迭代器。

12、为什么要把数据与算法分离开?
答:计算过程包括两个主要方面:计算和数据,而数据类型繁杂,数据集存储方式以及想对数据集执行的任务也非常多,如果我们每次都使用基本语言特性从头开始编写每个程序(即不分离数据和算法),费时费力不说,还难以理解和重用。我们希望通过合理组织代码,实现只有当我们想要完成一些全新任务时才需要编写新的代码(意味着实现新的算法无需修改数据组织形式);特别是,我们希望编写一些完成基本任务的代码,使得我们不必每次发现一种新的数据存储方式或者数据解释方式就要重写整个程序(数据组织方式变化不影响算法的通用性)。上面这句话阐明了将数据与算法分离开的原因,我们可以实现某些目的(完成新任务、新的数据组织形式)同时不用修改程序其他部分。

13、什么是STL?
答:STL是标准模板库(Standard Template Library)的简称,它是C++标准库为处理数据序列提供的一个专门的框架。STL提供了容器和通用算法,是兼顾灵活性与性能的函数库设计典范,提供了一个作用于数据结构之上的通用、正确和高效的算法框架。

14、什么是链表?它和向量本质上的区别是什么?
答:链表是一种序列,由“链接”组成一个链式序列,链表中的元素是“链接”的一部分,一个“链接”由这一元素以及一个或多个指针组成。向量的元素在内存中是连续存储的,而链表并不一定是连续存储的,因此链表与向量的本质区别在于,链表能够在不影响其他已有元素的前提下插入和删除元素。

15、链表中的链接是什么?
答:链表中的元素是“链接”的一部分,一个“链接”由这一元素以及一个或多个指针组成。

16、insert() 的功能是什么?erase() 呢?
答:insert() 用于在指定位置前插入一个元素,并返回插入的元素的位置(迭代器)。erase() 用于删除指定位置的元素,并返回被删除元素之后的位置。

17、如何判断一个序列是否为空?
答:begin() == end()

18、list 中的迭代器提供了哪些操作?
答:可以用 ++ 向前移动,用 -- 向后移动,用 * 读写元素,支持iter->m,支持== 和 != 逻辑比较操作。

19、如何使用STL遍历一个容器?
答:用迭代器从前往后(如果可以的话,从后往前)遍历容器。

20、什么时候应该使用 string 而不是 vector?
答:当需要进行字符串操作(例如字符串连接或读取空白符间隔的单词)时才考虑使用 string,其他时候用 vector。

21、什么时候应该使用 list 而不是 vector?
答:当需要在含有大量元素的序列内部较为频繁地使用 insert() 和 erase() 时,用 list。

22、什么是容器?
答:

  • 是一个元素序列 [ begin() : end() )
  • 提供拷贝元素的拷贝操作。拷贝通过赋值操作或者拷贝构造函数来实现
  • 其元素类型命名为 value_type
  • 具有名为 iterator 和 const_iterator 的迭代器类型。迭代器提供具有恰当语义的 *、 ++(前缀和后缀)、==、!= 运算符。list 的迭代器还提供可以在序列中向后移动的 -- 操作(双向迭代器)。vector 的迭代器还提供 --、[ ]、+、- 运算符(随机访问迭代器)
  • 提供 insert()、erase()、front()、back()、push_back()、pop_back()、size() 等操作,vector 和 map 还提供下标操作。
  • 提供比较运算符(==、!=、<、<=、>、>=)进行元素比较,采用字典顺序。

23、容器的 begin() 和 end() 应该实现什么功能?
答:begin() 返回指向容器首个元素的位置的迭代器,end() 返回指向容器最后一个元素之后位置的迭代器,如果容器为空,那么 begin() == end()。

24、STL提供了那些容器?
答:见15.10

25、什么是迭代器类别?STL提供了哪几类迭代器?
答:迭代器类别用来分类提供不同操作集合的迭代器类型。STL提供了输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器。

26、哪些操作是随机访问迭代器提供了而双向迭代器没有提供的?
答:下标运算符[ ] 操作;+ 和 - 运算,向迭代器加上或减去一个整数,对指向同一序列的两个迭代器进行减法操作来得到它们的距离。
(2021.10.24)

第十六章:算法和映射

1、有用的STL算法的例子有哪些?
答:

  • 查找:find(b,e,v) find_if(b,e,p)
  • 计算出现次数:count(b,e,v), count_if(b,e,p)
  • 排序:sort(b,e), sort(b,e,p)
  • 拷贝:copy(b,e,b2), copy_if(b,e,b2,p), unique_copy(b,e,b2)
  • 累积:accumulate(b,e,i), accumulate(b,e,i,op)
  • 内积:inner_product(b,e,b2,i), inner_product(b,e,b2,i,op,op2)
  • 合并:merge(b,e,b2,e2,r) (要求有序)
  • 判断两序列是否相等:equal(b,e,b2)
  • 二分查找子序列:equal_range(b,e,v)
  • 二分查找: binary_search(b,e,v), binary_search(b,e,v,cmp), low_bound(b,e,v), upper_bound(b,e,v)

2、find() 有什么用途?至少给出五个例子。
答:线性搜索序列中出现的第一个指定的元素,其中序列由指向首尾的迭代器指定。

3、count_if() 有什么用途?
答:x = count_if(b,e,p),统计 [b, e) 中满足 p(*iter) 为 true 的元素的个数。

4、sort(b,e) 的排序标准是什么?
答:默认是基于 < (小于) 进行排序的。

5、STL 算法如何将一个容器作为其输入参数?
答:通过容器的迭代器作为输入参数。

6、STL 算法如何将一个容器作为其输出参数?
答:通过容器的迭代器作为输出参数。

7、STL 算法通常如何表示“未找到”或“失败”?
答:通常是返回容器的 end() 迭代器表示“未找到”或“失败”。

8、什么是函数对象?
答:函数对象是一种能够实现函数行为的对象,它提供了一种机制,允许一个“函数”“随身携带”它所要的数据。

9、函数对象与函数之间有哪些区别?
答:函数对象能够存储数据,换句话说该对象具有状态,函数对象在执行函数行为时可以使用它自带的数据(状态),而函数则无法存储数据。另外向一个模板函数以传值的方式传递一个小的函数对象通常能够带来优化的性能,这是函数做不到的。

10、什么是断言?
答:断言是一种返回 true 或 false 的函数。

11、accumulate() 有什么用途?
答:默认的三参数 accumulate() 算法将一个序列中的值累加到累加器上,四参数 accumulate() 算法则允许用户指定要执行的运算,每次迭代都对累加器与本次迭代的元素执行指定的运算。

12、inner_product() 有什么用途?
答:将两个序列对应位置的元素相乘并将结果累加。泛化型:inner_product(b,e,b2,init,op,op2),init 是累加器初始值,op 用于将累加器与新值组合起来,op2 用于组合元素值对。

13、什么是关联容器?至少给出五个例子。
答:数据以(key, value)对形式储存的序列以及相应的操作集合,叫做关联容器。map, set, unordered_map, unordered_set, multimap, multiset

14、list 是一个关联容器吗?为什么不是?
答:list 不是一个关联容器,因为它是线性链表形式的顺序式容器,而关联容器的内部数据结构往往是非线性结构(树、哈希表)。

15、二叉树的基本序性质是什么?
答:如果保存关键字值的节点成员的名字为 key,那么二叉搜索树的基本规则是:

left->key < key && key < right->key

16、对于一棵树而言,对其进行平衡意味着什么?
答:保持平衡的树意味着查找节点时,最多需要查找 log ⁡ 2 ( N ) \log_{2}(N) log2(N)个节点就能找到。而非平衡树的查找的最坏情况为O(N)。

17、map 的每一元素占用了多少空间?
答:(Key, Value)对的空间 + 指向左右子树根节点的指针的空间。

18、vector 的每一元素占用了多少空间?
答:元素本身大小的空间。

19、当可用一个(有序的)map 时,为什么我们还会使用 unordered_map?
答:因为在某些场景下,我们并不需要“有序”这一性质,但我们需要更快的查找速度,特别是对于整数和字符串,使用 unordered_map(也叫哈希表),可以使查找平均代价接近常数且与表中元素数量无关,即O(1)。

20、set 与 map 有何区别?
答:set 可以看作是一个对其值不感兴趣的 map,或干脆看作一个没有值的 map,其元素可能是包含大量信息的对象——我们只需使用一个成员作为关键字。set 优于 map 的一点时我们可以直接使用从迭代器得到的值,其解引用操作直接得到一个元素类型的值。

21、multi_map 与 map 有何区别?
答:multi_map 是关键字可以出现多次的 map。

22、当我们能够“仅仅编写一个简单的循环”时,为什么还应使用 copy() 算法?
答:因为 copy() 算法是已经编写和调试好的代码,而一个循环哪怕再简单,也会比调用copy() 算法的代码多,而且还需要进行测试以消除可能的bug,另外该循环没有通用性,只能用在这里,所以应该使用 copy() 算法

23、什么是二分搜索?
答:二分搜索的前提是一个序列已经排序,假设我们在查找值 x, 首先查看中间元素

  • 如果中间元素的值等于 x,则我们已经找到它了
  • 如果元素的值小于 x,则值等于 x 的元素必然位于右边,因此我们查找右半部分(在右半部分里面进行二分查找)
  • 如果元素的值大于 x,则值等于 x 的元素必然位于左边,因此我们查找左半部分(在左半部分里面进行二分查找)
  • 如果我们到达最后一个元素且仍没有找到 x,那么该序列中没有值为 x 的元素。
    (2021.11.07)

第十七章:一个显示模型

1、我们为什么要使用图形?
答:Dr.Stroustrup(作者)表示,图形很有用,也很有趣,图形提供了许多有趣的代码供参考,图形设计实例非常丰富,图形有利于解释面向对象程序设计的概念机器语言特征,而且一些关键的图形学概念不是那么简单直白的,所以我们要使用图形。

2、什么时候我们尽量不使用图形?
答:我认为当涉及计算机系统底层的时候;或者能用简短的文字描述;或者程序运行的机器没有显示屏幕、没有展现图形的能力或者没有必要展现图形(某些嵌入式设备),尽量不使用图形。

3、为什么图形对程序员来说是有趣的?
答:因为图形可以即时展现一段代码的效果,图形代码易于阅读,图形领域中又非常丰富的具体、贴近实际的例子,可供学习设计策略和设计技术,通过相对较少的图形和GUI代码,就能展示包括类的设计、函数的设计、软件分层(抽象)、库的创建等在内的许多技术,图形化应用提供了一些非常易于理解的面向对象设计实例。

4、什么是窗口?
答:在使用屏幕与计算机进行交互时,通常从屏幕上划分出特定用途的、由程序控制的矩形区域——窗口。基本上,我们将窗口看作一个小屏幕。

5、我们的图形接口类(图形库)在哪个名字空间中?
答:Graph_lib

6、使用我们的图形库实现基本的图形功能,需要哪些头文件?
答:Point.h, Window.h (或Simple_window.h), Graph.h, GUI.h

7、我们使用的最简单的窗口是怎样的?
答:最简单的窗口应当包含窗口在屏幕上的显示位置、宽度、高度和窗口标签。

Simple_window win { Point{ 100, 100 }, 600, 400, "Canvas" };
//创建了一个显示位置(窗口左上角)是100*100像素点的地方,宽度600,
//高度400,窗口标签为“Canvas”的简单窗口
win.wait_for_button();

8、什么是最小化的窗口?
答:不知道题目啥意思,可能是指最简单的没有绘制任何图形的窗口。

9、窗口标签是什么?
答:label,用于标识窗口名。

10、如何设置窗口标签?
答:cpp win.set_label("Canvas #2"); //设置窗口标签为Canvas #2

11、屏幕坐标系是如何工作的?窗口坐标系呢?数学中的坐标系呢?
答:屏幕坐标系的工作方式:屏幕被建模为像素组成的矩形区域,每个像素由 x (水平),y (垂直) 坐标确定。最左端的像素的 x 坐标为0,向右逐步递增,直到最右端的像素为止;最顶端的像素的 y 坐标为0,向下逐步递增,直到最底端的像素为止。窗口坐标系与屏幕坐标系的工作方式一样。数学中的坐标系,如果是笛卡尔坐标系,那么则有x 轴 (水平) 和 y 轴 (垂直),两者交汇处为原点(0, 0),xi 从原点开始水平向右逐步递增,水平向左逐步递减,yi 从原点开始垂直向上逐步递增,垂直向下逐步递减。

12、我们能显示的简单”形状“有哪些?
答:多边形 Polygon、文本 Text、线 Line、矩形 Rectangle、函数 Function。

13、将形状添加到窗口的命令是什么?
答:cpp win.attach(shape_name);

14、你使用哪种基本形状来绘制六边形?
答:Polygon

15、如何在窗口中的某个位置显示文本?
答:

Text t { Point{150,150}, "Hello, graphical world!"};
//创建了一个文本,其显示位置从像素点150*150开始
win.attach(t);

16、如何将你最好的朋友的照片显示在窗口中(使用你自己编写的程序)?
答:

Image friend_photo {Point{100,50}, "friend_photo.jpg"};
//从像素点100*50开始显示我最好的朋友的照片。
win.attach(friend_photo);

17、你创建了一个 Window 对象,但屏幕上没有显示任何内容,可能的原因有哪些?
答:可能对象创建失败,也有可能忘记写win.wait_for_button();这行代码,导致没有将控制权交给GUI系统,因此无法将窗口绘制在屏幕上。

18、你创建了一个形状,但窗口中没有显示任何内容,可能的原因有哪些?
答:可能形状对象创建失败,也有可能是忘记写win.attach(shape_name);这行代码,导致没有将形状添加到窗口中,所以窗口不会显示任何内容。
(2021.11.14)

第十八章:图形类

1、为什么我们不直接使用商业的或者开源的图形库?
答:因为商用或者开源的图形库提供了大量特性,可能会有数百个类,每个类一般有数十个处理函数,这些复杂性会带来较大的学习难度,本书作者不直接使用它们,而是设计接口库以减小学习难度,而且同样能够引入重要的图形和 GUI 概念。

2、为了实现简单的图形显示,你大约需要使用我们的图形接口库中多少个类?
答:这个不好说哦。。。不过作者给出了24个类,用于产生有用的图形输出。

3、为了使用图形接口库,需要哪些头文件?
答:

#include"Point.h"
#include"Graph.h"
#include"Window.h"
#include"Simple_window.h"
#include"GUI.h"

4、哪些类定义了闭合形状?
答:本章介绍的闭合形状有:Closed_polyline, Polygon, Rectangle, Circle, Ellipse

5、我们为什么不简单地使用 Line 表示所有形状?
答:因为不同的形状有不同的几何特性,仅仅靠 Line 可能能够逼近这些形状的样式,但是无法表示其几何特性;有些形状可能需要很多条 Line,这会给编码带来巨大负担;有些形状是曲线,无法用 Line表示。

6、Point 的参数的含义是什么?
答:点的 x 坐标 和 y 坐标,坐标是相对的,是基于点所在的“窗口”的。

7、Line_style 的成员有哪些?

struct Line_style {
    enum Line_style_type {
        solid=FL_SOLID,            // -------
        dash=FL_DASH,              // - - - -
        dot=FL_DOT,                // .......
        dashdot=FL_DASHDOT,        // - . - .
        dashdotdot=FL_DASHDOTDOT,  // -..-..
    };

    Line_style(Line_style_type ss) :s(ss), w(0) { }
    Line_style(Line_style_type lst, int ww) :s(lst), w(ww) { }
    Line_style(int ss) :s(ss), w(0) { }

    int width() const { return w; }
    int style() const { return s; }
private:
    int s;
    int w;
};
  • 枚举类型Line_style_type,用于表示线段样式
  • 三个构造函数
  • 返回线宽和线样式的成员函数
  • 两个私有成员变量:线样式、线宽

8、Color 的成员有哪些?

struct Color {
    enum Color_type {
        red=FL_RED,
        blue=FL_BLUE,
        green=FL_GREEN,
        yellow=FL_YELLOW,
        white=FL_WHITE,
        black=FL_BLACK,
        magenta=FL_MAGENTA,
        cyan=FL_CYAN,
        dark_red=FL_DARK_RED,
        dark_green=FL_DARK_GREEN,
        dark_yellow=FL_DARK_YELLOW,
        dark_blue=FL_DARK_BLUE,
        dark_magenta=FL_DARK_MAGENTA,
        dark_cyan=FL_DARK_CYAN
    };

    enum Transparency { invisible = 0, visible=255 };

    Color(Color_type cc) :c(Fl_Color(cc)), v(visible) { }
    Color(Color_type cc, Transparency vv) :c(Fl_Color(cc)), v(vv) { }
    Color(int cc) :c(Fl_Color(cc)), v(visible) { }
    Color(Transparency vv) :c(Fl_Color()), v(vv) { }    // default color

    int as_int() const { return c; }

    char visibility() const { return v; }
    void set_visibility(Transparency vv) { v=vv; }
private:
    char v;    // invisible and visible for now
    Fl_Color c;
};
  • 枚举类型Color_type,用于隐藏GUI库的颜色实现方式,并将这些颜色实现映射到Color_type枚举值
  • 枚举类型Transparency,用于表示颜色的透明度(是否可见)
  • 四个构造函数
  • 以 int 类型返回形状颜色类型的成员函数
  • 返回透明度的成员函数
  • 设置透明度的成员函数
  • 两个私有成员变量:透明度和颜色类型。

9、什么是 RGB?
答:Red, Green, Blue。工业界的一种颜色标准,通过对红®、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色。

10、两条 Line 和包含两条线的 Lines 有什么区别?
答:两条 Line 是两个独立的 Line 对象,这两个对象是可以被单独处理的;而包含两条线的 Lines 是一个 Lines 对象,这两条线是联系在一起的,必须一起处理。

11、每种 Shape 都有的属性有哪些?
答:位置(点)、颜色、透明度。

12、由5个 Point(顶点)定义的 CLosed_polyline 有多少条边?
答:5条线。

13、如果你定义了 Shape 但没有添加到 Window 中,你会看到什么?
答:什么都看不到。

14、Rectangle 与包含4个 Point(4个顶点)的 Polygon 有什么区别?
答:Rectangle 是矩形,从几何上看它确实包含四个顶点,但是其类的实现仅保存左上角的点和长与宽;包含4个顶点的 Polygon 不一定是 Rectangle,它可以是任何形状,而且它的实现是保存这四个顶点的。

15、Polygon 与 Closed_polyline (书上写的是Closed_polygon,应该是勘误了) 有什么区别?
答:Polygon 与 Closed_polyline 的唯一区别是 Polygon 不允许出现交叉的线。

16、填充(fill)和轮廓(outline)哪个在更上层?
答:轮廓在更上一层。

17、为什么我们没有定义一个 Triangle 类(毕竟我们定义了 Rectangle)?
答:因为 Rectangle 的样式永远是四个内角90°,仅用一个左上角顶点、长和宽就可以描绘,它是简单易实现的;而三角形 Triangle 的三个内角的度数有非常多的可能,一般是通过三个顶点来描绘一个三角形,定义 Triangle 并没有什么特别的意义,本质上就是一个 三个顶点的 Polygon。

18、在 Window 中怎样移动 Shape?
答:通过 move() 成员函数(如果有的话)移动 Shape。

19、怎样为 Shape 设置一行文本的标签?
答:添加一个 Text 对象,该对象可以设置文本,以作为 Shape 的文本标签。

20、能够为 Text 对象中的文本串设置哪些属性?
答:设置颜色、透明度、字体、字号。

21、什么是字体?我们为什么要关心字体?
答:字体是文本的显示形状,关心字体也是因为美观或者说用于醒目标识。

22、Vector_ref 的作用是什么?如何使用?
答:用于保存已经命名和未命名对象,析构时能够对未命名对象进行释放,不会引入内存泄漏问题。
使用方法与标准库 vector 非常类似。

23、Circle 和 Ellipse 的区别是什么?
答:几何角度看,Circle 是两个焦点相同(长短轴一样)的 Ellipse,而 Ellipse 则不一定是 Circle。从类的实现角度看,Circle 包括一个圆心和一个半径,而 Ellipse 包括圆心、长轴和短轴,两者是不同的类。

24、如果指定的文件不包含图像,当用该文件显示一个 Image 时会发生什么现象?
答:会显示一个 error image

25、如何显示图像的一部分?
答:Image 类的 set_mask() 成员函数可以选择显示图像的一部分。
(2021.11.20)

第十九章:设计图形类

1、什么是应用领域?
答:应用领域就是程序设计的目标领域,也就是代码描述的概念所在的领域。

2、什么是理想的命名?
答:逻辑上,等价的操作应该有相同的名字,不同的操作应该有不同的名字。

3、我们可以命名哪些东西?
答:命名数据对象、自定义类型、模板参数名、操作(或者说函数)

4、Shape 类提供了哪些功能?
答:画线、移动、设置颜色、告知颜色、设置线型、告知线型、设置填充颜色、告知填充颜色、访问点、告知点的数量、防止拷贝操作、加入点、修改点。

5、如何区别抽象类和非抽象类?
答:抽象类只能被用作基类,它不能创建对象;非抽象类是具体类,可以创建对象。

6、如何将类设计为抽象类?
答:1. 构造函数的访问控制设置为 protected;2.在类中声明一个或多个需要在派生类中被覆盖的虚函数(即纯虚函数)

7、访问控制能够控制什么?
答:可以控制能否访问类的成员以及控制访问类的成员的方法。可以保护类的描述不会被设计者以不可预见的方式更改,从而使我们能用更少的精力写出更好的类,这就是所谓的“不变式”。

8、私有(private)数据成员有什么好处?
答:封装这些成员,以保护他们不被直接访问和修改,简化维护操作。

9、虚函数是什么?如何区别于一个非虚函数?
答:在基类中定义一个函数,其声明时通过关键字 virtual 被声明为虚函数,而派生类中如果有一个类型和名称完全一样的函数,当用户调用该基类虚函数时,实际上调用的是派生类中的函数。非虚函数就是没有 virtual 声明的函数。

10、什么是基类?
答:基类是派生中作为构造另一个类的基础类的类型。比如 Circle 类派生自 Shape 类,那么 Shape 类就是 Circle 的基类。

11、如何定义一个派生类?
答:定义类的时候,在派生类的类名后面加上“ : base_class_name",其中 base_class_name 是基类名。比如 class Circle : Shape {},就定义了 Circle 为派生类

12、对象的布局意味着什么?
答:对象的布局描述了对象在内存中的布局方法,比如其数据成员的组织顺序以及虚指针(虚表地址)和虚表(虚函数表)在内存中的存在方式和访问方式。

13、使一个类更易于测试,应该做哪些工作?
答:设计良好的访问控制,也就是定义良好的接口,包括命名和接口逻辑都应直观且合理。添加合理的注释。提供接口继承的设计。

14、继承关系图是什么?
答:继承关系图以图的方式描绘”类层次”,显示类之间的关系(主要是基类和派生类之间的继承关系)。

15、保护(protected)对象和私有(private)对象有什么区别?
答:保护对象可以被其所属类及其派生类的成员使用;私有对象只能被其所属类的成员使用。

16、类中的哪些成员可以被它的派生类访问?
答:公有成员、保护成员。

17、如何区别纯虚函数和其他虚函数?
答:纯虚函数就是在声明虚函数时在函数名后添加“=0”

class B {
public:
	virtual void pvf1() = 0;
	virtual void pvf2() const = 0;
};

18、为什么将一个成员函数设计为虚函数?
答:若将基类的成员函数设计为虚函数,那么它就为派生类提供了一个接口函数,派生类可以“直接”使用该接口函数,也可以根据自己的需求而定义自己的接口函数(前提是自定义的函数类型与基类提供的接口函数类型一致)。

19、为什么将一个虚函数设计为纯虚函数?
答:目的之一是为了定义一个虚基类,即不能直接创建对象;目的之二是提供纯粹的接口(带有纯虚函数的类关注的是接口的统一性,纯虚函数一般只有声明,没有代码实现,当然也可以实现,只是没有意义),即它们倾向于不包括任何数据成员(数据成员在派生类中定义),因此没有任何构造函数。

20、覆盖的含义是什么?
答:在派生类中定义一个和基类中虚函数的名称和类型(接口)都相同的函数,以使派生类的函数代替基类中的版本被放入虚表,这种技术称为覆盖。派生类的完成覆盖操作的函数可以通过基类提供的接口进行调用。

21、接口继承和实现继承有什么区别?
答:接口继承是指代码使用基类(接口)提供的接口,而无须知道具体的派生类(实现),需要基类对象参数的函数可以接受派生类对象参数;实现继承是指派生类继承了基类的实现,可以使用基类提供(实现)的功能,从而简化了派生类的实现。

22、什么是面向对象程序设计?
答:建立模型(类)以抽象地反映现实事物的特征和概念,实例化类以产生对象,对象是某个具体的现实事物;类和对象,再结合继承、运行时多态和封装,共同构成面向对象程序设计。(个人愚见)
(2021.11.28)

第二十章:绘制函数图和数据图

1、接受一个参数的函数是怎样的?
答:函数的参数列表只有一个项(参数类型和参数名)。

2、什么情况下你会使用(连续的)线表示数据?什么情况下你会使用点?
答:当数值对的一个数值在某种意义上是另一个数值的函数时,使用(连续的)线表示数据。否则使用点。

3、什么函数(数学公式)定义一条斜线?
答:y = k * x

4、什么是抛物线?
答:抛物线数学上的表示:y = ax^2 + bx + c;物理上表示物体从某一高度以某一初速度v抛出,在重力加速度的作用下坠下地面过程中所经过的轨迹。

5、如何生成 x 轴 和 y 轴?
答:

Axis(Orientation d, Point xy, int length, int num_of_notches = 0, string label = "");

6、什么是默认参数?什么时候使用默认参数?
答:在函数声明中赋予形参的初始值,称为默认参数。如果函数调用者没有给出参数值,那么就使用默认参数。注意:只能将末尾的参数定义为默认参数,如果一个参数有一个默认参数值,那么其后的所有参数都必须有一个默认参数值。

7、如何把函数叠加在一起?
答:不太懂啥意思,我觉得就是把函数相加就行了吧。如果这个函数是指 Function 类对象,那么把它们 attach到窗口即可。

8、如何给一个图形化的函数加上颜色和标签?
答:加上颜色只需要 调用成员函数 set_color() 即可,而添加标签需要再创建一个 Text 对象放在合适位置作为图形化函数的标签。

9、我们说一个级数近似一个函数,这是什么意思?
答:有些函数可能无法精确表示,但可以用级数近似,级数的项越多,表示的函数越精确。比如书中20.5给出了用级数计算指数函数的方法。

10、为什么在编写代码绘制图形之前需要画出它的布局草图?
答:因为如果没有表明窗口中事物位置的布局草图,当程序输出不能反映所要求的结果时,我们将会迷失并且无助。换句话说,在绘制图形前画出布局草图,并定义那些表示位置的符号常量,可以让我们更直观的了解到自己编写代码的目的,减少出错的可能,并为排查错误提供帮助。

11、如何按比例缩放图形使得输入恰好适合显示区域?
答:需要以 数据范围 / 坐标轴范围 作为比例因子。

12、如何按比例缩放输入可以避免试验和误差?
答:比例因子应该是浮点数;另外避免整数除法,在做除法前先把长度数据转换成 double 类型。
另外作者在书中提到了一个程序设计技巧,可以定义函数类,并实例化函数类对象,来帮助我们进行比例缩放,简化代码,避免重复操作,提高程序可读性与正确性。

13、为什么要格式化输入,而不只是让文件包含那些“数字”?
答:格式化输入可以让数据更规范可读,同时易于错误检查。

14、如何设计图形的总体布局?如何在代码中反映出来?
答:首先明确自己要绘制哪些图形,然后确定图形的大小、位置、颜色,如果有函数图或者数据图,那么还需要确定缩放比例和标签位置,然后画出布局草图,按需要进行针对性调整,最后得到总体布局。在代码中体现为一些定义大小、位置,缩放比例的符号常量。
(2021.12.05)

第二十一章:图形用户界面

1、你为什么需要图形用户界面?
答:直观、美观,优秀的图形用户界面是易上手的

2、什么时候你需要非图形用户界面?
答:如果我的计算机的图形显示设备性能孱弱无法支持图形用户界面,我只能使用非图形用户界面。

3、什么是软件的层次结构?
答:自顶向下:用户代码(应用程序)、接口库(也可以设计多层次的接口库)、操作系统(其提供的API)、操作系统内核、设备驱动

4、为什么需要对软件分层?
答:软件分层使程序员在设计、分析和实现程序时,能集中精力于该层次的软件代码中。同时软件分层能够极大增强通用性和可移植性,比如接口库的代码和操作系统的代码可以被不同语言编写的程序使用,用户代码(应用程序)通过接口库,可以运行在多个不同平台的操作系统上。

5、C++程序与操作系统通信时的基本问题是什么?
答:我认为基本问题是数据的传输问题。

6、什么是回调函数?
答:回调函数是一种被提供给“系统”的函数,用于在某种特定的系统事件被触发时,系统可以通过回调函数调用回用户代码。

7、什么是构件?
答:构件是面向软件体系架构的可复用软件模块。构件(component)是可复用的软件组成成份,可被用来构造其他软件。它可以是被封装的对象类、类树、一些功能模块、软件框架(framework)、软件构架(或体系结构Architectural)、文档、分析件、设计模式(Pattern)等。1995年,Ian Graham给出的构件定义如下:构件(Component)是指一个对象(接口规范、或二进制代码),它被用于复用,接口被明确定义。(摘自百度百科)。本章中,我们使用构件来定义通过 GUI 与程序进行交互的形式。

8、构件的另一个名称是什么?
答:控件

9、首字母缩写词 FLTK 是什么意思?
答:Fast Light Tool Kit,快速轻量工具包。

10、FLTK 如何发音?
答:full tick

11、你还听说过其他的 GUI 工具集吗?
答:MFC QT

12、哪些系统使用术语构件?哪些系统更喜欢使用控件一词?
答:不懂。。。

13、构件的例子有哪些?
答:本章中的 Button、In_Box、Out_box、Menu,就是 Widget 的例程。

14、什么时候需要使用输入框?
答:当用户需要通过图形用户界面向程序输入数据,或者程序需要用户通过图形用户界面输入数据时,需要使用输入框。

15、输入框中的值是什么类型的?
答:文本类型

16、什么时候需要使用按钮?
答:程序和用户之间通过图形用户界面交互,程序提供给用户一些靠鼠标点击的操作,这时候需要按钮。

17、什么时候需要使用菜单?
答:菜单可以是按钮向量,是某一类操作的集合,如果程序提供了这一类操作,并且需要对这些操作进行统一设计和管理,那么需要使用菜单。

18、什么是控制流反转?
答:控制流反转的一个含义是程序的执行顺序完全由用户的行为决定,其中的关键点是,程序执行顺序的控制权从程序本身转交给了构件。

19、调试 GUI 程序的基本策略是什么?
答:代码的简化和理解代码的系统方法是关键。

  • 小心使用经过严格验证的程序组件(类、函数、库)
  • 简化所有的新代码,降低程序从最简版本“增长”的速度,仔细逐行检查代码。
  • 检查所有的链接设置
  • 与已经正常运行的程序比较
  • 向朋友解释代码(相当于复习和理解代码,并请“旁观者清"的朋友帮忙找出问题)

20、为什么调试 GUI 程序比调试“普通的流式输入输出程序”更难?
答:因为很难跟踪代码的执行过程,仅仅加入”输出语句“不再有效,使用调试器时可能会遇到许多程序同时运行(多线程),异常并不总是如我们希望的那样正常工作。有些问题不是代码上的问题,只是逻辑上的问题,例如一个窗口恰好放在另一个窗口上,导致另一个窗口被遮盖,此时依靠调试程序寻找错误只是浪费时间。
(2021.12.12)

第二十二章:理念和历史

1、历史有什么用?
答:历史的真正意义在于那些已经在实践中证明自身价值的思想和理念。

2、程序设计语言有什么用处?请给出几个例子。
答:指示机器操作的一种工具;算法的符号表示法;与其他程序员交流的工具;进行实验的工具;控制电脑设备的一种手段;表示各种概念关系的一种方法;表达高层设计的一种方法;绘制图形的工具,等等。

3、请给出几种客观上可以认为是优点的程序设计语言的基本特性。
答:可移植性、类型安全、高性能、长期稳定性、易于学习、有大量软件开发工具可选用、有大量软件组件(比如库)可选用。

4、抽象的含义是什么?更高层的抽象呢?
答:以尽可能一般化的方式来表达解决方案。更高层的抽象更接近于我们对待某种事物的思考方式和对待问题的解决方案,换句话说,更高层的抽象更接近设计理念。

5、我们对代码的四个高层理念是什么?
答:直接表达思想;抽象层次;模块化;一致性和简约主义

6、列出高层程序设计的潜在优点。
答:高层程序设计,其抽象层次更高,更接近设计理念,更容易有良好的结构,从而其更易理解,易于优化和性能调整,此外,修改程序、寻找和修正错误、增加新特性、移植到新的体系结构就更容易。

7、重用是什么?它可以带来什么好处?
答:重用,即使用以前测试过并且在其他地方已经使用过的组件构建系统,也包括组件的设计和使用等工作。好处是可以减轻工作量,从而能够按时交付;使用已经测试过的组件,可以获得更高的正确性;若组件性能较高,则重用它可以带来更高的性能;

8、什么是过程式程序设计?给出一个具体的例子。
答:利用函数(对参数进行操作)构造程序的思想。

double d{0};
cin >> d;
cout << std::sqrt(d) << endl;

9、什么是数据抽象?给出一个具体的例子。
答:首先为应用领域提供一组适合的数据类型,然后使用这些数据类型编写程序。数据抽象非常倚重显式数据隐藏,我们通过接口访问数据类型,而不是直接访问其实现

class Abstract
{
public:
	abstract(int a) : x1{a}, x2{a * 2} {}
	int val1() const { return x1; }
	int val2() const { return x2; }
private:
	int x1, x2;
};

Abstract abst{1};
int a =  abst.x1;	//错误,无法直接访问其实现
int b = abst.val2();	//可以通过接口访问

10、什么是面向对象程序设计?给出一个具体的例子。
答:将类型组织为层次接口,以便用代码直接表达它们之间的关系。之前几章的Shape基类和它的派生类,Widget基类和派生类,都是面向对象程序设计的例子。

11、什么是泛型程序设计?给出一个具体的例子。
答:对于具体算法,通过添加参数来描述算法哪些部分可以变化而不必改变其他部分,从而将算法“提升”到更高的抽象层。下面是书中的例子,结合了泛型、面向对象、和过程式程序设计。

template<class Iter>
void draw_all(Iter b, Iter e)
{
	std::for_each(b, e, std::mem_fun(&Shape::draw));
}

12、什么是多范式程序设计?给出一个具体的例子。
答:混合多种程序设计风格的程序设计方式,就是所谓的多范式程序设计。11题的例子。

13、第一个运行在贮存式计算机上的程序出现在什么时候?
答:1949年由 David Wheeler 编写的平方表程序是第一个运行在贮存式计算机上的程序。
(2021.12.22)

第二十三章:文本处理

1、我们在哪里查找“文本”?
答:各种地方,比如网站、手册、代码源文件、电子邮件……

2、标准库中哪些功能对于文本分析非常有用?
答:std::string, std::regex, std::stringstream…还有好多输入输出功能也很有用。

3、insert() 的插入位置是其位置(或迭代器)之前还是之后?
答:其实是插入在指定的位置(迭代器)上,原来位置及之后的元素向后移动。

4、Unicode 是什么?
答:Unicode(统一码、万国码、单一码)是一种在计算机上使用的字符编码。它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。(摘自百度百科)

5、如何将字符串转换为其他类型?反过来呢?
答:我喜欢用 stringstream,本章也实现了 to_string, from_string, to 这三个函数,可以用于字符串和其它类型之间的转换。

6、假定 s 是一个字符串,cin>>s 和 getline(cin, s) 的区别在哪里?
答:cin>>s 忽略输入流的空白符,之后读入 s 直到输入流中再次出现空白符为止(空白符不读入); getline(cin, s) 则不忽略空白符,将输入流一整行字符串完整读入到 s 中,换句话说,读入输入流中直到换行符或者 EOF 之前的所有字符,换行符会被丢弃。

7、列出标准流。
答:istream, ostream, istringstream, ifstream, ostringstream, ofstream, iostream, stringstream, fstream,具体的类层次图可以看书中P218页。

8、一个 map 对象中的关键字是什么?给出一些关键字类型的例子。
答:map 对象的元素是 (key,value) 对,其关键字 key 是用于访问和排序 (k,v) 对的标识符。

9、如何遍历 map 的元素?
答:map 就是一个标准容器,那么用迭代器从 begin() 到 end() 即可按序遍历整个 map 元素。

10、map 和 multimap 的差别在哪?哪种有用的 map 的操作在 multimap 中不存在,这样设计的原因是什么?
答:map 没有重复的关键字,而 multimap 可以有重复。map 的下标访问在 multimap 中不存在,这是因为 multimap 的 key 和 value 不一定是一一映射关系。

11、向前迭代器需要哪些操作?
答:P29页有介绍,前向迭代器需要有 ++ 操作向前移动,* 操作读写元素,如果 (*p).m 有效,则可以使用简写形式 p->m。

12、空域和域不存在有什么区别?给出两个例子。
答:在文本域搜索中,比如寻找 Address: 域或者 From: 域,这里域是以某个符号名加冒号标识的,成为域标识符,其中空域就是存在域,也就是有域标识符,但是该域的数据为空,而域不存在就是没有与标识符。

13、正则表达式中为什么需要使用转义符?
答:因为正则表达式中的某些特殊符号具有特定的功能,而为了表达这些特殊符号的原义,需要使用转义符。

14、如何将正则表达式存入 regex 变量?
答:现有正则表达式 pattern,若要将其存入 regex 变量,有三种方法,一是使用原始字符串常量初始化regex变量,即
cpp R"(pattern)",注意,这里的pattern不能是变量,它指代的是正则表达式常量字符串;二是使用字符串字面值初始化,这里可能需要转义操作;三是用一个 string 变量赋值给 regex 变量。

//1:
std::regex reg1 {"\\s\\w{3}\\s"};       // literal: must escape escapes
//2:
std::regex reg2 {R"(\s\w{3}\s)"};       // raw literal: regular escapes
//3:
std::regex reg3;                        
std::string pat = "\\s\\w{3}\\s";       // std::string init: same as literal
reg3 = pat; 

15、\w+\s\d{4} 与什么样的字符串匹配?给出三个例子。如果将此模式转换为 regex 变量,需要用什么样的字符串初始化 regex 变量?
答:匹配一个及以上字母、数字或下划线字符,后接一个空格,后接4个数字。例子:
“December 2021”, “25th_December 2021”, “a_2 1234”。转换为 regex 变量的话,有原始字符串常量和字面字符串常量初始化。

std::regex pat1 {R"(\w+\s\d{4})"};
//or
std::regex pat2 {"\\w+\\s\\d{4}"};

16、在程序中,如何确定一个字符串是否是合法的正则表达式?
答:将这个字符串赋值给一个 regex 变量,它会对模式进行检查,如果不合法或者过于复杂,导致无法用于匹配, regex 对象会抛出一个 bad_expression 异常。

17、regex_search() 的功能是什么?
答:搜索,在(任意长的)数据流中搜索与正则式匹配的字符串。

18、regex_match() 的功能是什么?
答:匹配,判断一个字符串(已知长度)是否与模式匹配,即检查模式与给定字符串是否完全匹配

19、如何在正则表达式中表示句点符号 (.) ?
答:用转义的方式,比如 .

20、如何在正则表达式中表示“至少三个”的概念?
答:{3,}

21、字符 7 与 \w 匹配吗?下划线符号 _ 呢?
答:都匹配。

22、如何在正则表达式中表示大写字母?
答:\u 或者 [A-Z]

23、如何自定义字符集?
答:使用 [ ],在两个中括号之间自定义字符集

24、如何从整数域中提取数值?
答:如果确定整数域中都是数字,那么可以借助stringstream或者本章的 from_string(const string& s) 进行转换为整数,如果整数域中可能有其他字符,那么在转换前先使用正则表达式抽取出整数字符。

25、如何用正则表达式表示浮点数?
答:\d*.\d+

26、如何从匹配结果中提取浮点值?
答:

double d {0.0};
string str;
//进行一些操作将匹配结果读入str
//法1:
stringstream ss{str};
ss >> d;
//法2
d = from_string<double>(str);

27、子匹配是什么?如何访问子匹配结果?
答:子匹配是正则表达式中,用小括号括起来的子模式的匹配结果。子模式在正则表达式中从左到右从1到N编号,而匹配后的结果对象中,子匹配和对应的子模式一样,也是从1到N编号,比如本章所述的C++中 smatch 类型变量本质上是一个子匹配的向量,第一个元素(编号0)是完整匹配,第二个元素(编号1)及之后就是各个子匹配,这些子匹配与子模式编号一一对应。
(2021.12.25)

第二十四章:数值计算

1、哪些人使用数值计算?
答:科学家、工程师、统计学家等。

2、精度是什么?
答:我觉得从浮点数角度考虑,就是小数点后的有效位个数吧。

3、什么是溢出?
答:最高有效位丢失。

4、一般来说 double 的宽度是多少? int 呢?
答:double 宽度 8 字节,int 宽度 4 字节。

5、你如何检测溢出?
答:两个正数相加的结果为负数,或者两个负数相加的结果为正数,那就溢出了。

6、在哪里能找到数值限制?如,最大的 int 值是多大?
答:。最大的 int 值是 INT_MAX。

7、数组是什么?行和列呢?
答:数组是一个元素序列,可以通过下标来访问元素(也叫向量)。行是一个y坐标相同的元素序列,列是一个x坐标相同的元素序列。

8、C风格的多维数组是什么?
答:C风格的多维数组是指数组的数组的数组的数组。。。

9、程序设计语言中支持矩阵运算的部分(如矩阵库)必备的特性是什么?
答:P247 24.5节开始的那一段。

10、矩阵的维是什么?
答:如果矩阵可以代表向量空间的话,那矩阵的维就是向量空间的基底的势,即基底中向量的个数。如果是从代码角度看的话,矩阵的维应该是多少个数组的数组的数组吧,比方说二维数组就是数组的数组。

11、一个矩阵可以有多少维(从理论上、数学上看)?
答:无限。

12、子矩阵是什么?
答:与原矩阵的维一样,属于原矩阵的一部分。

13、什么是“广播”运算?请举出一些例子。
答:将任意函数 f 都应用于矩阵。

y = apply(f,x);		//apply就是“广播”操作,将函数 f 应用于矩阵x,并创建一个新的矩阵

14、Fortran 风格的下标和 C 风格的下标有什么区别?
答:Fortran下标(x[,y,z,…])可以接受一个或多个下标,而 C 风格下标总是接受单一下标[x]。

15、如何对矩阵中每个元素都进行一个相同的操作?请举例。
答:循环遍历。
使用相同的内置运算:cpp a *= 7; //a是矩阵
执行相同函数:cpp a.apply(f,7);

16、融合运算是什么?
答:综合多个运算为一个运算的操作。书中的 scale_and_add() 就是融合乘-加运算,对矩阵中每个元素 i 执行 result(i) = arg1(i) * arg2 + arg3(i)。

17、请定义点积运算。
答:对矩阵中每个元素执行 result += arg1(i) * arg2(i),result 的初值为0。

18、什么是线性代数?
答:摘自百度百科:线性代数是数学的一个分支,它的研究对象是向量,向量空间(或称线性空间),线性变换和有限维的线性方程组。

19、什么是高斯消去法?
答:为求解方程组Ax = b,对系数矩阵A和向量b进行变换,使A变为一个上三角矩阵,对角线之下的所有元素均为0。然后通过“回代”,即首先通过最后一个方程求得xn,然后将第n行从系统中消去,继续求解xn-1,依此类推,直到求得x1。

20、(线性代数中的、“现实生活中”的)主元(pivot)是什么?
答:主元是所求的一个解。

21、什么令一个数随机?
答:随机数是一个服从某种分布的序列,无法很容易的从序列前一部分的内容预测出下一个数是什么。随机数标准库,基于发生器和分布两个概念生成随机数,发生器是一个可以产生均匀分布整型值序列的函数对象,分布是一个函数对象,给定一个发生器产生的序列作为输入,分布可以按照相应数学公式产生一个值得序列。

22、什么是均匀分布?
答:均匀分布在相同长度间隔的分布概率是等可能的。
f ( x ) = 1 b − a , a < x < b f(x)=\dfrac{1}{b-a}, a < x < b f(x)=ba1,a<x<b

23、从哪里可以找到标准数学函数?他们支持哪些类型的参数?
答:,支持 float、double、long double 和 complex 参数。

24、复数的虚部是什么?
答:我不知道该怎么回答。虚部可能是这个复数在复数域那个轴上的映射?

25、-1的平方根是什么?
答:纯虚数 i
(2022.04.28)

第二十五章:嵌入式系统程序设计

1、什么是嵌入式系统?给出十个例子,至少有三个是本章未提及的。
答:汽车的中控、T-Box以及控制引擎、底盘、转向、灯、窗等设备的ECU;冰箱的温控系统;洗衣机的控制系统;智能手表;

2、嵌入式系统的特殊之处在哪里?给出常见的五点。
答:可靠性、资源有限、实时响应、不间断正常运行、难以内行维护。

3、给出嵌入式系统中可预测性的定义。
答:如果一个操作在一台给定的计算机上每次的执行时间总是相同的,而同类操作的执行时间也都是相同的,那么我们就称这个操作是可预测的。

4、为什么嵌入式系统的维修很困难?
答:设备可能是物理损坏,或内部程序有问题,损坏情况复杂,难以维修;或者设备处在恶劣环境下,难以接触到并进行维修。

5、为什么出于性能的考虑进行系统优化是个糟糕的主意?
答:因为这种优化往往是”底层优化“,它会使实现的算法和数据结构难以理解、难以修改,而且通常会导致内存优化难以进行,因为大量相似的代码片段出现在很多地方,但又无法共享这些代码,因为它们都有细微的差异。

6、为什么我们更倾向于使用高层抽象而不是低层代码?
答:因为我们想要清晰、干净、易维护的代码,高层抽象能够让程序的正确性容易被验证,而且可以降低系统开发的成本和时间。

7、什么是瞬时错误?为什么我们特别害怕这种错误?
答:瞬时错误是指在”某些时候“会发生,但不会在程序每次运行时都发生的故障。因为这种错误特别难以查找和复现,而且预先的测试中也难以完全排查和预估发生这种错误的可能。

8、如何设计具有故障恢复能力的系统?
答:避免资源泄露,配备副本,系统自检测,模块化系统以迅速排除有错误的代码,对子系统进行监控。

9、为什么我们无法防止所有故障?
答:因为一切皆有可能。

10、什么是领域知识?给出一些应用领域的例子。
答:即关于系统本身、它的工作环境及其使用方式的知识。比如说汽车的ECU系统设计,就需要车辆工程领域的相关知识,比如说汽车运行环境,可能需要的机械原理,法规所规定的安全性限制等等领域知识。

11、为什么对于嵌入式系统程序设计来说领域知识是必要的?
答:因为没有领域知识的话,就无法知道可能导致故障的错误以及错误的原因,从而也就无法设计和实现具有容错能力的系统。

12、什么是子系统?给出一些例子。
答:系统模块化后,被上层系统所监管的下层系统称为子系统。

13、从C++语言的角度,存储可以分为哪三类?
答:

  1. 静态内存:由链接器分配,其生命周期为整个程序的运行期间。
  2. 栈内存(自动内存):在调用函数时分配,当函数返回时释放。
  3. 动态内存(堆) :用 new 操作分配,delete 操作释放。

14、什么情况下你会使用动态内存分配?
答:系统初始化阶段;系统没有对动态内存分配的使用进行限制,并且/或者,少量的临时动态内存分配不会使系统内存资源因为内存碎片问题而泄露,这种情况下我会使用动态内存分配。

15、为什么在嵌入式系统中使用动态内存分配通常是不可行的?
答:两点原因,1. 动态内存分配是不可预测的,它不能保证在固定时间内完成;2. 动态内存分配会造成碎片问题,即,再分配和释放了大量对象后,剩余的内存会”碎片化“——空闲内存被分割成大量小”空洞“,每个空洞都很小,无法容纳程序所需对象,从而使这些空闲内存毫无用处。

16、什么情况下在嵌入式系统中使用 new 是安全的?
答:禁止 delete,并且在初始化阶段使用 new 是安全的。

17、在嵌入式系统中使用 std::vector 的潜在问题是什么?
答:std::vector 是标准库容器,它间接使用了 new 和 detete,会给嵌入式系统带来潜在的动态内存分配的问题。

18、在嵌入式系统中使用异常的潜在问题是什么?
答:嵌入式系统中使用异常的潜在问题在于,对每个 throw,如果不考察更大范围的代码,程序员无法知道需要花费多长时间才能找到与之匹配的 catch,甚至是否存在这样一个 catch 都无法获知,换句话说,它不可预测。

19、什么是递归函数调用?为什么一些嵌入式系统程序员要避开它?替代方法是什么?
答:一个在函数体内调用自己的函数,称为递归函数,其通过分而治之的思想将大的问题分解为小问题,并逐层递归调用,直到到达某个基线条件后返回,之后逐层返回,函数调用层次越深,所需要入栈的数据就越多。嵌入式系统由于内存资源受限,因此过深的递归调用可能会造成栈溢出,因此要尽量避开。替代方法是使用循环。

20、什么是内存碎片?
答:在内存中,空闲空间分散,形成很多小“空洞”,无法满足新的内存需求的情况,就称为内存碎片。

21、什么是垃圾收集器(在程序设计中)?
答:扫描或者跟踪记录内存对象,明确哪些指针指向内存中的确定对象,释放无用(没有指针指向或者没有变量指代的)对象,并清理内存碎片。

22、什么是内存泄漏?它为什么会导致错误?
答:被分配的内存使用完毕后未被释放,从而占据内存资源,导致这块内存无法被继续利用,相当于该内存泄露。系统长时间运行,内存泄漏会使内存资源损失殆尽,从而程序无法继续执行,导致错误发生。

23、什么是资源?请举例。
答:资源就是机器只能提供有限数量的那些东西。处理器运算单元资源、寄存器资源、内存空间资源、存储器存储空间资源,等等。

24、什么是资源泄漏?如何系统地预防?
答:内存泄漏、处理器运算单元被永久或极长时间占用、存储空间被垃圾填满,这些都是资源泄露。为了系统预防资源泄露,对于长期运行程序,除了一些需要一直使用的资源外,其他资源在使用完毕之后都必须释放。

25、为什么不能将对象从一个内存位置简单地移动到另一个位置?
答:因为对象并不知道有哪些指针指向了它,若将对象从一个内存位置简单地移动到另一个位置,而指向它原来位置的指针却无法随之改变,这些指针将没有意义了,引用这些指针可能会产生系统错误。

26、什么是栈?
答:栈是可以分配任意大小的内存空间,最后分配的空间总是最先被释放。

27、什么是存储池?
答:存储池是一组相同大小的对象的集合。

28、为什么栈和存储池不会导致内存碎片?
答:栈只在栈顶一端增长和缩小,其内存空间的分配和释放不会交叉,所以不会导致内存碎片;存储池中所有对象都是相同大小,因此也不会产生内存碎片。

29、为什么 reinterpret_cast 是必要的?它又会导致什么问题?
答:通常,int 类型到指针类型的简单转换( reinterpret_cast )是连接一个应用程序和它的重要硬件资源所必需的。但是这样的转换是完全未经检查的,因此很容易出错。

30、指针作为函数参数有什么危险?请举例.
答:

void func(int* p)
{
	int a = *p;
	*p = 3;
}

上述代码没有检查指针参数是否为 nullptr,可能会导致访问异常。此外,如果调用该函数的调用者不知道其内部实现,那么他在调用时并不会想到 func 函数会修改指针参数指向对象的值,从而产生意外。

31、指针和数组可能引起什么问题?请举例。
答:见25.4.2节

32、在函数接口中,可以用什么机制替代(指向数组的)指针参数?
答:如果开发环境允许使用容器(比如std::vector),那么就在函数接口中使用容器。如果不能,那么就需要自己定义一个与 vector 功能相似但又不使用动态内存分配的容器了,它需要具备以下几个功能:

  • 它只是内存中对象的一个引用(不拥有对象,也不分配、释放对象)。
  • 它“知道”自己的大小(有可能实现范围检查)。
  • 它“知道”元素的确切类型(不会成为类型错误的根源)。
  • 传递代价(拷贝)低,传递方式可以是一个(指针,数量)对。
  • 它不能显式转换为一个指针。
  • 通过接口对象,能容易地描述元素范围的子区域。
  • 它和内置数组一样容易使用。
    于是,本书的 Array_ref 就登场了~

33、”计算机科学第一定律”是什么?
答:每一个问题都会有一个与会的解决办法。

34、位是什么?
答:我的理解是比特

35、字节是什么?
答:x个位的序列

36、通常一个字节有多少位?
答:8

37、位运算有哪些?
答:或 |,与 &,异或 ^,左移位 <<,右移位 >>,补 ~

38、什么是“异或”运算?它有什么用处?
答:两个位相同则结果为0,不同则结果为1。可以用于加密。

39、如何描述位序列?
答:可以用内置类型,比如 bool, char, short, int, long int, long long int,标准库也提供了处理位的方法,std::vector,std::bitset, std::set,文件,或者还可以用枚举和位域来表示二进制位。

40、字中位的习惯编号次序是怎样的?
答:由右边至左(最低有效位至最高有效位)编号。

41、字中字节的习惯编号次序是怎样的?
答:和位的编号次序相似。

42、什么是字?
答:可以把一个字看成是x个字节的序列。

43、通常一个字中有多少位?
答:32

44、0xf7的十进制值是多少?
答:247

45、0xab的位序列是什么?
答:10101011

46、bitset是什么?你什么情况下需要使用它?
答:用于描述和处理二进制位集合,其大小固定,在创建时指定,默认被初始化为全0,但可以指定初始值,初始值可以是无符号整数或者‘0’和‘1’组成的字符串,常用位运算可以用于 bitset,bitset 对 I/O也很有用,可以用于输出字符的位模式,bitset 的位是由右至左编号的。

47、unsigned int和 signed int的区别是什么?
答:无符号整型和有符号整型,前者的最高位也是数值位,而后者的最高位是符号位。

48、什么时候使用 unsigned int 比 signed int更好?
答:当需要表示位集合时,用无符号数。或者表示逻辑属性,其值不能是负数,则用无符号数,其他时候建议还是有符号数吧。

49、如果需要处理的元素数目非常巨大,如何设计一个循环?
答:用元素所在容器提供的 size_type,或者使用迭代器,可能会更好。

50、如果将 -3 赋予一个 unsigned int 变量,它的值会是什么?
答:0xFFFFFFFD

51、我们为什么要直接处理位和字节(而不是处理上层数据类型)?
答:因为有时候应用程序要处理的对象就是位的形式,那么使用位运算就是顺理成章的事了。比如硬件指示器(标识位)、低层通信(需要从字节流中提取不同类型的值)、图形应用(需要用多个层次的图像组成图片)以及加密。

52、什么是位域?
答:一组二进制位和不同大小的数,其中二进制位和数通常是命名的,出现在字的不同位置,称之为“设备寄存器”,位域是一种语言特性,用于处理上述的固定的数据布局。

53、位域的用途是什么?
答:位域用于处理52中描述的固定的数据布局。

54、加密是什么?为什么要加密?
答:加密是指将明文数据转换为密文数据的过程。加密是为了防止其他人看懂明文。

55、能对照片进行加密吗?
答:可以,照片本质上也是字节流。

56、TEA表示什么?
答:Tiny Encryption Algorithm

57、如何以十六进制输出一个数?
答:cout << hex,就可以以十六进制输出一个数了,如果还想要有前面的0x标识,那么就再 cout << showbase。

58、编码规范的目的是什么?列出几条需要编码规范的原因。
答:编码规范的目的是解决表达思想(解决方案表达方式)模糊不清的问题。原因见书P302最上面的几条。

59、为什么没有一个普适的编码规范?
答:编码规范应当针对特定应用领域和特定程序原来设计

60、列出一些好的编码规范应该具备的特点。
答:

  • 应当针对特定应用领域和特定程序原来设计
  • 应该既有指示性,又有限制性,可以推荐一些“基础的”库功能作为指示性原则,通常是最有效的方式
  • 指明程序风格,即,应该指定命名和缩进原则,指定允许使用的语言子集,指定注释原则,指明使用哪些库
  • 以提高程序的:可靠性、可移植性、可维护性、可测试性、重用性、可拓展性、可读性,为目标

61、编码规范如何会起到不好的效果?
答:如果过于强硬指出规范而不解释清楚理论依据,那么会引起程序员的厌恶感;更糟糕的是,如果程序员觉得规范的某些部分毫无益处,甚至妨碍他们写出高质量的程序,就会不停地尝试推翻它。另外,差的编码规范,比如一个限制使用 C 语言子集的 C++ 编码规范是有害的。

62、列出至少十条你认可(发现有用)的编码规范。解释它们为什么有用。
答:开放回答。。。

63、我们为什么要避免使用字母全部大写的标识符?
答:因为这些标识符往往是用于定义宏的
(2022.01.07)

第二十六章:测试

1、制作一个应用程序的列表,对每个程序都给出可能导致最严重后果的bug的简单说明。例如:航班控制程序——坠机:231人死亡;损失价值5亿美元的设备。
答:后台数据库处理程序——遭遇SQL注入攻击,数据被任意窃取或操控。

2、为什么我们不直接证明程序的正确性呢?
答:证明一个普通程序的正确性是现有研究所不能解决的,而且证明本身也会包含错误,整个程序证明领域也是一个非常艰深的研究方向。

3、单元测试与系统测试有什么不同?
答:单元测试可以让我们很快确定错误发生在这个单元(或用来引导测试的代码)中,而系统测试则只能告诉我们系统中存在错误。

4、什么是回归测试,它为什么很重要?
答:回归测试是对系统以及各单元的通过重新运行旧的测试集而进行的完整测试。只要修改了代码,就必须重新测试,即在代码升级后,必须重新进行单元测试,在整个系统提交前,也要重新进行完整的系统测试。这种测试重要在它可以对以前发现的错误的再次检查,避免代码修改将错误重新引入。回归测试用例集可以确保系统的变化不会脱离开发者和用户所认可的范围。

5、测试的目的是什么?
答:测试的目的是系统地查找出错误。

6、为什么 binary_search 不检查它的要求?
答:因为检查它的要求所付出的代价比 binary_search 本身还要高。

7、如果我们不能检查到所有错误,那么我们应该主要查找哪类错误?
答:

  • 与“其他代码”微妙的相关性:检查全局变量、非常量引用参数、指针等的使用。
  • 资源管理:查抄内存管理(new 和 delete)、文件使用、锁等。
  • 查找循环:检查终止条件。
  • 分支语句(if / switch):查抄分支语句中的逻辑错误。

8、在处理数据序列时,错误最可能出现在代码的哪部分?
答:极限情况(大的、小的、空的、奇异分布的输入),边界条件(边界附近的任何情况)。序列末尾往往是错误容易发生的地方。

9、为什么在测试时使用大数值是个好主意?
答:因为使用大数值能够发现程序对输入的检查,以及可以明显发现程序的输出是否如预期的那样(比如发现溢出问题)。我个人觉得原问题应该是为什么在测试时使用大数据集是个好主意?因为可以暴露低效的算法。

10、为什么测试用例经常以数据而不是代码形式出现?
答:因为这样可以放入文件并在后续测试中继续使用。

11、为什么我们要使用大量基于随机数的测试用例?什么时候使用?
答:因为我们通常是程序的编写者,我们在编写和调试代码时已经考虑过可能隐藏有错误数据序列区域。因此在设计测试方案时很可能重复编写程序时所犯的逻辑错误,导致真正的问题被忽略,因此大量基于随机数的测试用例可以帮助我们排查可能的问题。
如果我们需要测试很多操作的累积效应,而一个操作的结果取决于之前的操作是如何处理的话,即系统是有状态的,系统运行就是状态间的迁移,此时基于随机数的测试用例特别有用。

12、为什么测试带有 GUI 的程序很困难?
答:因为 GUI 可能的问题与硬件结合地较深,可能某一分辨率下的显示器能够正常显示,但其他分辨率显示器就出问题了;或者某些显卡可以,而其他显卡则存在问题。

13、为什么需要独立测试一个“单元”?
答:因为任何复杂系统都是由许多单元组成的,这些单元又是由更小的单元组成的,因此需要从最小单元开始独立测试,然后再依次测试更大的单元,直到整个系统测试完毕为止,这样可以有效找出问题所在。

14、可测试性和可移植性的联系是什么?
答:

  • 可测试性:指一个软件工件(软件系统、模组、需求文件或设计文件等)在一给定的测试环境下,可支援测试的程度。(来自维基百科)
  • 可测试性:指软件发现故障并隔离、定位其故障的能力特性,以及在一定的时间和成本前提下,进行测试设计、测试执行的能力。James Bach 这样描述可测试性:软件可测试性就是一个计算机程序能够被测试的容易程度。(来自百度百科)
  • 可移植性:使用高阶语言写成的软件,在不同环境下,是否具备可以被重复使用的性质。一般来说,软件是否具备可移植性的衡量标准,在于进行软件移植时,需要付出多少工时为代价。(来自维基百科)

我认为可测试性和可移植性的联系在于,是否能在不同软件环境下重复测试并得到相同的结果。

15、为什么测试一个类要比测试一个函数困难?
答:因为类包含了接口和实现,包含有数据和对应操作,有类层次,有各种访问控制,这些加深了测试类的难度。

16、测试的可重复性为什么很重要?
答:我认为测试的可重复性能够说明一个系统的稳定性,并反过来表达测试结果的可信度,特别是对于性能测试而言,如果重复得到大致相同的结果,那么系统是稳定的,测试也是可信的。

17、当发现一个“单元”依赖于未检查的假设(前置条件)时,测试者应该怎么办?
答:在调用代码中嵌入对未检查假设的检测,或者要求单元程序的编写者插入能打开要求检测的代码。

18、程序设计者/实现者应该如何做,才能改进测试?
答:P327的26.4节测试方案设计中提到:早测试,经常测试,及早考虑测试问题有助于避免发生在早期的错误(也有助于以后发现错误)。插入能够打开和关闭的检查代码。

19、测试与调试有什么不同?
答:测试是系统地发现错误,而调试则是查找并排除已知错误,调试比测试更专用。

20、什么时候性能是我们要考虑的因素?
答:高并发或者对实时性有要求的程序,性能是开发者和测试人员要考虑的因素。

21、对于如何(容易地)制造低性能问题,给出两个(或更多)例子。
答:

  • 对大数据序列使用冒泡排序或选择排序。
  • 在循环中重复调用会返回相同结果的函数,比如书中P329的
for(int i = 0; i < strlen(s); ++i){
	// 对 s[i] 的一些处理
}

其重复调用 strlen(s),然而该函数返回结果基本相同。

  • 循环中代码需要使用动态分配的内存区域,但是new和delete操作在循环内进行,但实际上可以在循环外分配一块区域,然后供循环内的代码反复使用。
  • 反复构造和析构类对象。
    (2022.05.01)

你可能感兴趣的:(《C++程序设计原理与实践》,第二版,c++,程序设计)