欧易

欧易(OKX)

国内用户最喜爱的合约交易所

火币

火币(HTX )

全球知名的比特币交易所

币安

币安(Binance)

全球用户最多的交易所

C++编程调试秘笈:c++的缺陷来自哪里?

时间:2022-10-09 17:40:43 | 浏览:3210

C++语言是非常独特的。虽然实际上所有的编程语言都从其他语言中吸收了一些思路、语法元素和关键字C++却是吸收了另一种完整的语言,即C语言。事实上, C++语言的创建者Bjarne Stroustrup原先把他的新语言命名为"带类的C"。这意

C++语言是非常独特的。虽然实际上所有的编程语言都从其他语言中吸收了一些思路、语法元素和关键字C++却是吸收了另一种完整的语言,即C语言。事实上, C++语言的创建者Bjarne Stroustrup原先把他的新语言命名为"带类的C"。这意味着如果我们已经使用了一些C代码,并且由于某种原因(例如科研或贸易)切换到一种面向对象的语言,就不需要在移植代码方面采取任何措施,只要安装新的C+编译器,就可以对旧的C代码进行编译了,并且效果和原先的一模一样。我们甚至会觉得已经完成了从C到C++的转换。最后这种想法虽然距离真相还很远,用真正的C++所编写的代码与C代码看上去存在很明显的区别,但它还是提供了一个逐渐过渡的选项。也就是说,我们可以从现在编译运行的C代码出发,逐渐引入用C++所编写的新代码段,慢慢与它们混合在一起,最终实现到纯C++的切换。因此, C++的层次式设计具有它独特的市场推动力。

但是,其中还是存在一些复杂的地方:随着C的完整语法被新语言完整地吸收,它的设计哲学和存在的问题也同样被吸收。C语言是在1969年~1973年期间由Dennis Rithie在贝尔实验室创建的,其出发点是为了编写Unix操作系统。这项工作的一个伴随成果是诞生了一种高效的高级编程语言(与需要编写每条计算机指令的汇编语言相比)。也就是说,它所产生的编译后的代码应该具有尽可能快的速度。这种新的C语言的其中-项公开原则是,用户不应该为他没有使用到的特性而受到拖累。因此,为了追求高效的编译代码,对于程序员没有提出明确的要求, C就绝对不会加以考虑。C语言是为了速度而不是为了舒适而创建的,这就产生了一些问题。

首先,程序员可以创建一个某种长度的数组,并用一个超出该数组边界的索引值访问一个元素。更容易被滥用的是C的指针运算,程序员可以把指针运算所产生的任何值作为内存地址并对它进行访问,不管这块内存是否应该被访问。(实际上,这两个问题其实是同一个,只不过使用了不同的语法。)

程序员还可以在运行时使用calloc()和malloc()函数动态分配内存,并使用free()函数负责动态内存的销毁。但是,如果忘了销毁或者不小心销毁了多次,其结果可能是灾难性的。

我们将在本书的第二部分深入讨论这些问题中的每一个。需要重视的是, C++在继承整个C时,除了传承它的高效原则,还继承了它的所有问题。因此, C++代码中的部分缺陷就来源于C。但是,故事并没有结束。除了来自于C的问题, C++自身也存在一些问题。例如,大多数人认为友函数和多重继承并不是良好的编程思路。C++具有自己分配内存的方法,它并不是调用像calloc()或malloc()这样的函数,而是使用操作符new, new操作符并不仅仅分配内存,它还创建对象,即调用它们的构造函数。与C的精神相同,使用delete操作符删除动态分配的内存是程序员的责任。现在的情况看起来与C相同:我们分配了一些内存,然后删除它。但是,复杂之处在于C++具有两种不同的new操作符:

在第一种情况下, new操作符创建了一个MyClass类型的对象。在第二种情况下,它创建了一个相同类型的对象数组。与之对应的是, C++具有两种不同的delete操作符:

当然,一旦使用了"带方括号的new"创建对象,就需要使用"带方括号的delete"删除它们。这样就可能导致一种新的错误:混用new和delete ,其中一个带了方括号而另一个没有带方括号。如果出现了这种错误,就会对内存堆产生巨大的破坏。因此,我们可以总结如下: C++的缺陷大部分来源于C,但C++t引入了一些自讨苦吃的新方法。我们将在本书的第二部分讨论这些话题。

2.1 为什么编译器是捕捉缺陷的最好场合

如果在编译时捕捉缺陷与在运行时捕捉缺陷之间进行选择,只要有可能,都应该在编译时捕捉缺陷。这样做的理由有很多。首先,如果一个缺陷是被编译器所检测到的,我们将看到一条文本信息,准确描述了所发生的错误是什么,它是在哪里发生的,发生在哪个文件以及发生在哪一行。(作者在这里可能稍微有点乐观,因为在有些情况下,尤其是在涉及STL时,编译器所产生的错误信息是相当含糊的,需要花费精力才能推断出编译器实际所描述的含义。但是,编译器总是在不断地完善中,大多数情况下它们对问题的描述是相当清晰的。)

另一个理由是完整的编译(进行了最终链接)覆盖了程序中的所有代码。如果编译器没有返回错误或警告就可以百分之百地确信程序中不存在编译时可以检测到的错误。但对于运行时测试,就不能做出这样的保证。当代码相当庞大时,很难保证所有可能的分枝都被测试到,也无法保证每一行代码都至少执行1次。即使我们能够保证这一点,仍然不够。同一段代码对于一组输入可能正确地完成任务,但对于另一组输入可能无法正确地工作。因此,通过运行时测试,我们永远无法完全保证对所有东西都进行了测试。

最后还存在时间因素:我们在运行代码之前执行编译,因此如果在编译时捕捉到了错误,就可以节省时间。有些运行时错误是在程序的后期出现的,因此可能要等几分钟甚至几小时的运行之后才会发现一个错误。更糟的是,这种错误很可能是无法复制的,它可能以一种看上去随机的方式,在连续运行时出现并消失。相比之下,在编译时捕捉错误就简单得多!

2.2 怎样用编译器捕捉缺陷

现在我们应该已经坚信,只要有可能,就尽量在编译时捕捉错误。但是,怎样才能实现这个目标呢?让我们观察一对例子。

第一个例子是一个Variant类的故事。曾几何时,一家软件公司编写了一个Excel插件。这是一个文件,被Microsoft Excel打开之后向它添加了一些新功能,可以在Excel单元格中被调用。由于Excel单元格可以包含不同类型的数据,包括整数(例如1 )、浮点数(例如3.1415926535)、日期(例如1/12000)甚至是字符串("This is the house that Jack built") ,因此这家公司开发了一个Variant类,它的行为类似于变色龙,可以包含任意上述数据类型。但是,随后有人提出了一个思路,就是一个Variant对象可以包含另一个Variant对象,甚至可以包含一个Variant类型的vector (即std:: vector<Variant> )。这些开始被使用的Variant对象并不仅仅与Excel进行通信,还与内部代码进行通信。因此,当我们观察函数的签名时:

很显然,完全没有办法理解这个函数期望接受什么类型的数据,以及它将返回什么类型的数据。因此,如果它期望接受一个日期数据,而我们向它传递了一个无法组成日期的字符串,这个错误只能在运行时才能被检测到。正如我们刚才讨论的那样,应该尽量在编译时发现错误。因此,这种方法使我们无法使用编译器通过类型安全轻松地捕捉到错误。这个问题的解决方案将在后面讨论,不过简洁的答案就是用不同的C++类表示不同的数据类型。

上面这个例子是真实的,但有些极端。下面是一个更加典型的情况。假设我们正在处理一些金融数据(例如股票的价格) ,并且为每个值加上对应的时间戳,即这个价格被观察时的日期和时间。那么我们应该怎样对时间进行测量呢?最简单的解决方案是对过去某个时间(例如1/1/1970)以来的秒数进行计数。

有人突然意识到实现了这项功能的函数库所提供的是32位的整数,最大值约为20亿左右。如果超过了这个最大值,就会发生溢出而成为负数。在距离时间轴的起点大约68年之后(即2038年)就会发生这种情况。它所导致的问题与著名的"千年虫”问题相似。为了修正这个问题,可能需要检查相当数量的文件,找到所有这些变量,并把它们的类型更改为int64,后者的长度是64位,能够表示的时间长度是32位整数的40亿倍左右,对于再小心谨慎的人来说都是足够的了。

但是现在又出现了另一个问题。有些程序员使用了int64 num_of_seconds形式,另一些人则使用了int64num_of_millisec形式,还有一些人使用了int64 num_of_microsec形式。编译器绝对没有办法判断出一个接受毫秒时间的函数实际所传递的是表示微秒的时间,反之亦然。当然,我们可以对需要分析的股票价格所处的时间间隔预设一些条件,例如从1990年直到未来的某个时刻(例如3000年) ,然后在运行时增加一项安全检查,确保传递给函数的值必须位于这个时间间隔之内。但是,这将导致许多函数都需要配备这种安全检查,可能需要花费大量的人力。如果有人在将来决定回过头来分析20世纪期间的股票价格又会怎么样呢?

2.3处理类型的正确方式

现在,如果我们创建一个Time类,在内部实现中隐藏了从什么时间开始,以及用什么时间单位(秒、毫秒等)进行测量等细节,上面这些杂七杂八的问题就可以轻松得以避免。这种方法的一个优点是如果我们错误地传递了其他日期数据,而不是传递了时间(现在用Time类型表示) ,编译器马上就能捕捉到这种错误。这种方法的另一个优点是,如果Time类当前是用毫秒实现的,并且以后为了提高精度用微秒表示,我们只需要编辑一个类,修改内部实现的细节,而不会影响其余的代码。

因此,我们怎样才能在编译时而不是在运行时捕捉类型错误呢?我们首先可以用一个单独的类表示每种类型的数据。我们用int表示整数,用double表示浮点数,用std::string表示文本,用Date表示日期,用Time表示时间,对于其他类型的数据也都用一个单独的类表示。但是,只采用这种做法仍然是不够的。假设我们有两个类Apple和Orange ,并有一个期望接受一个Orange类型的参数的函数:

但是,我们可能不小心向它提供了APPle类型的对象:

在有些情况下,这样的代码可以通过编译,因为C++编译器试图向我们提供帮助。只要可能,它会把Apple平静地转换为Orange,这可能通过以下两种方式发生。

(1)如果Orange类具有一个只接受一个Apple类型的参数的构造函数。

(2)如果Apple类具有一个可以把它转换为Orange的操作符。

当Orange类具有下面这样的定义时,就会发生第一种情况:

它甚至可以像下面这样:

即使在最后这个例子中,构造函数看上去像是具有两个输入,但它也可以只用一个参数就可以被调用,因此它也可以隐式地把Apple转换为Orange,这个问题的解决方案是用关键字explici声明这类构造函数。这种做法可以防止编译器执行自动(隐式)转换,这样我们就可以迫使程序员在期望接受Orange的地方必须使用Orange :

第二个例子需要对应地修改为:

另一种让编译器知道怎么把Apple转换为Orange的方法是提供一个转换操作符:

这个操作符在此处的出现是非同寻常的,说明程序员用一种明确的方式向编译器提供了一种把Apple转换为Orange的方法,它并不是什么错误。因此,对所有接受一个参数的构造函数用关键字explicit进行声明,这是值得推荐的做法。一般而言,隐式转换的所有可能性都是不好的思路。因此,如果想按照上面这个例子一样在Apple类中提供一种把Apple转换为Orange的方法,下面是一种更好的方法:

在这个例子中,为了把Apple转换为Orange,需要采用下面的方式:

另外还有一种方法可以混合不同的数据类型,即使用枚举(enum)。考虑下面这个例子:假设我们定义了下面这两个枚举,分别表示一周中的某天以及月份。

这些常量实际上都是整数(例如,C内置的int类型)。如果我们有一个期望接受一周中的某天作为参数的函数:

下面这个调用将会在不产生任何警告的情况下通过编译:

在运行时,我们能顾采取的措施不多,因为JAN和MON都是与1相等的整数。捕捉这类缺陷的方法是不使用创建整数的“单纯功能”枚举,而是使用创建新类型的枚举:

在这种情况下,期望接受一周中的某天为参数的函数将被声明为:

像下面这样试图用一个Month值调用这个函数:

将会产生编译错误:

这正是我们在这个例子中期待产生的效果。

但是,这种方法具有一个消极因素。在这个例子中,用枚举创建整型常量时,我们可以编写如下的代码:

但是当我们使用枚举创建新类型时,如下面的写法:

就无法通过编译。因此,如果我们需要迭代枚举的值,可以像原来一样使用整数。

当然,任何规则都有例外,有时候程序员有理由编写像Variant这样的类,允许进行隐式类型转换以满足特定的需要。但是,绝大多数时候应该完全避免隐式类型转换,这就允许我们充分利用编译器检查不同变量类型的功能,早期(即在编译时)捕捉潜在的错误。

现在,假设我们已经尽自己所能使用了类型安全。遗憾的是,除了bool和char类型之外,每种类型可能包含的值的数量都是天文数字,通常只有一小部分值是合理的。例如,如果我们使用double类型表示股票的价格,可以很合理地确定股票的价格将在0到10 000之间波动(唯一的例外是Berkshire Hathaway公司的股票,它的主人Warren Buffet显然并不相信把股票价格保持在合理范围内是个好主意,因此他从不对股票进行除权,在本书写作之时这个股票的价格是每股10万美元)。但即使是Berkshire Hathaway这样的股票,它的价格仍然只使用了double类型的很小一部分,因为double的范围高达

,并且还包含了完全不适合表示股票价格的负数。由于大多数类型只有一小部分值是合理的,因此总是存在一些只能在运行时才能诊断的错误。

事实上,C语言的大多数问题,例如指定了越界索引,或通过指针运算不恰当地访问内容,只能在运行时才能得到诊断。由于这个原因,本书的剩余部分主要专注于讨论捕捉运行时错误。

本章所讨论的在编译时诊断错误的规则如下。

1、禁止隐式类型转换:用关键字explicit声明一个接受1个参数的构造函数,并避免使用转换操作

符。

2、用不同的类表示不同的数据类型。

3、不要使用枚举创建整型常量,而是用它们创建新类型。

本文节选自《C++编程调试秘笈》

本书介绍了C++程序员经常犯的一些编程错误,并且给出了可以用来避免这些错误的规则。本书基于C++开发者社群的实践,介绍了如何安全地使用C++库。


专栏

C Primer Plus官方视频解读

作者:异步社区

¥99

5人已购

查看

相关资讯

编程和乐高机器人啥关系、编程启蒙到底怎么做|逃妈说编程(下)

于是快马加鞭地赶出了下篇。 其实本来写完上篇后我脑袋里对下篇的框架已经有了大概的构思。但后来从文章的留言,读者微信群里的讨论,以及大家私下给平台小助手、给我发的微信留言中发现,“咦,原来大家最关心的问题跟我之前的设想还是有点儿出入哦~”。于

少儿编程热,需要冷思考编程培训需要来次“编程”

少儿编程热,需要冷思考(大家谈)在不少城市,少儿编程课外培训日渐火爆。很多人认为,作为数字世界的通用语言,学习编程能有效锻炼孩子的思维能力。但也有人提出问题:受理解力所限,幼儿园阶段就送孩子学编程是否符合教育规律?出于“莫输在起跑线上”的心

孩子应不应该学编程,编程的优缺点要知道,孩子才能真正学以致用

孩子成长中的每一件事都不是小事,成长是一次没有返航的旅程。家长为了孩子能够长成参天大树,会带孩子学各种各样的知识,少儿编程便成为了很多家长的心头好。对于编程课,你了解多少但是很多家长并不知道孩子适不适合学习编程,也不知道编程是什么?就知道学

少儿编程培训值得吗?业内老师:跟真正编程两码事

少儿编程越来越火,从一线城市到二三线城市,编程培训机构如雨后春笋般涌现。少儿编程培训为什么这么火?家长们的“狂热”究竟值不值得?家长:人工智能是趋势,希望孩子早接触“机器人课程确实挺贵的,一下子就交了1万5左右的套餐费,每次课大概160元左

少儿编程十大培训机构有哪些?少儿编程课程怎么选?课程全面测评

这两年,少儿编程越来越火了,不少地区已将它纳入中小学教育。不少国家的政策将少儿编程以及人工智能编入了教育学习项目,甚至浙江编程是高考选考科目,各位宝妈们都处处欲动,很重视孩子的编程教育。想必很多家长在给孩子选课的路程多少经历了很久,那么小编

C/C++编程笔记:C语言编程知识要点总结!大一C语言知识点(全)

一、C语言程序的构成与C++、Java相比,C语言其实很简单,但却非常重要。因为它是C++、Java的基础。不把C语言基础打扎实,很难成为程序员高手。1、C语言的结构先通过一个简单的例子,把C语言的基础打牢。C语言的结构要掌握以下几点:(1

C/C++,被誉为“最经典的编程语言”,不仅是因为编程入门需要学

1、从C到C++计算机诞生初期,用机器语言或汇编语言编写程序;第一种高级语言FORTRAN诞生于1954年;BASIC语言(1964)是由FORTRAN语言的简化而成的是为初学者设计的小型高级语言;C语言是1972年由美国贝尔实验室的 D.

编程入门:编程语言(Java、C++)先学那个好?

这个问题在入门阶段一直是争议十分大的问题,反正学长每次去网上找新手应该学什么编程语言的时候,就会出现五花八门的推荐,n多语言,n多推荐,搞的新手一会听说这种语言适合新手,于是学习这种语言。一会又有人说xx语言是垃圾、弱爆了,学了也没用,之类

编程入门:如何正确认识编程?除了高薪,我再告诉你一些秘密

假期在家待得怎么样?是不是天天过着衣来伸手饭来张口的日子,当然也时常伴随着妈妈的“嫌弃”和“唠叨”?哈哈~文章来源:博学谷接下来,丫姐跟你说一件丫姐在过年期间发生的有意思的事儿:小侄子:姑姑,你会不会编程?我答:会啊...小侄子:那你教教我

编程语言盘点:2021年程序员五大编程语言!C++不在其中?

选择第一门语言是非常重要的,因为这是搭建基础的开始,自此以后我们会逐渐走进并了解编程世界。但老实说,选择哪一种编程语言并不十分重要,重要的是我们需要掌握它,并用它来提高自己解决问题和开发的能力。此后,要再从一种语言切换到另一种语言就会变得轻

编程:史上最全的计算机编程语言列表来了!你不进来了解一下吗?

计算机编程语言可用于将指令传达给计算机。它们基于某些句法和语义规则,定义了编程语言中每种结构的含义。现在我得到了一个凡是可以找得到的每种编程语言的列表。我将它们分为以下几类:解释型编程语言函数式编程语言编译型编程语言过程式编程语言脚本编程语

初学编程:C语言/C++编程新手入门学习方法及书籍推荐

C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制)。C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样

C/C++编程笔记:运算符—所有编程语言的基础!一文带你搞懂

运算符是任何编程语言的基础。因此,如果不使用运算符,则C / C ++编程语言的功能是不完整的。我们可以将运算符定义为符号,以帮助我们对操作数执行特定的数学和逻辑计算。换句话说,我们可以说运算符对运算对象进行运算。例如,考虑以下语句:c =

C/C++编程笔记:《C语言》——数组知识详解,学编程建议收藏!

不要看这个图简单,底层就是这样的。数组是一个整体,它的内存是连续的;也就是说,数组元素之间是相互挨着的,彼此之间没有一点点缝隙。这一点很重要,连续的内存为指针操作(通过指针来访问数组元素)和内存处理(整块内存的复制、写入等)提供了便利,这使

编程语言丨C++是不是最难学的编程语言?教你如何自学C++语言

C++是否是最难学的编程语言?这个问题我不确定。很多人都感觉或者说听说C++是最难学的,但是有句老话说得好:难者不会,会者不难!这不,说C++是最难的编程语言,还是有许多的程序员学会了,而且鉴于这门语言的强大,学会的人薪资待遇还相当不错。在

友情链接

网址导航 SEO域名抢注宝宝起名网妈妈知道币圈丹霞山旅游攻略霸王茶姬会员日积家大师收藏家芽庄旅游网调酒师培训网保时捷跑车网原油期货网红水河第一湾资讯网人力资源资讯网足浴养生网仓鼠品种网海天味业A股虎牙直播资讯网黄鹤楼旅游攻略贵阳新闻头条网
python编程教学网-python数据库开发教程、python基础知识入门、python数据库编程入门、python语法基础、python下载安装教程、python下载手机版、python翻译器下载手机版、python翻译器代码、python语言翻译、python基础代码、python编程自学网。
python编程教学网 dadeji.cn ©2022-2028版权所有