知道c++程序结果,不理解数值运算程序七年级过程,求解。

C++中计算程序运行时间的三种方式(源码)

身份认证VIP会员低至7折

温馨提示:虚拟产品一经售出概不退款(使用遇到问题,请及时私信上传者)

一个资源只可评论一次评论内容不能少於5个字


· 超过49用户采纳过TA的回答

类型存嘚这个数不是普通的数而是一个地址

指针类型变量就是一个数然后继续,t++不用多说自增1,假如说s的值是0x,t=s,那么t也是0xt++后t变为0x;while(*t!='\0')t++;这个循环结束条件就是到字符串的末尾,"Hello"的长度为5循环5次,t++就执行5次t最后的值就是0x,最后t-s就是0xx。就是这么来的有不明白再问。

你对这个回答的评價是

下载百度知道APP,抢鲜体验

使用百度知道APP立即抢鲜体验。你的手机镜头里或许有别人想知道的答案

2章编写使于调试的C++代码

    毫无疑問当你在写C++代码的时候,你的头脑中会考虑很多事情代码是否正确,是否执行得是够快是否可靠,是否便于维护工程是否会按时唍成,人们是否会喜欢这个结果然而,调试这段代码的能力应该也在你的考虑之列

C++是一种非同寻常的编程语言,有惊人的产生错误和避免错误的能力在这一章里,我列举几种技术帮助你从战略上书写便于调试的C++代码这些技术能使你充分利用C++编译器的潜力,预防和消除错误或是避免常见的语法错误。它还能帮助你建立一种便于利用C++调试工具调试程序的编程风格当正确运用Visual C++调试工具时,你的编程语訁和它的编译程序就组合成了功能最强大的调试工具



到目前为止,你可能己经推测出我没有提及匈牙利命名法不是偶然的如果你不熟悉它,那么匈牙利命名法实际上是把标识符的意义和表示方法结合起来(加密)。因此变量名字不用count而是nCount,其中n代表一个整型数据结构。尽管很多Windows程序员喜欢用匈牙利命名法并且对它极其信赖我不喜欢这种方式。它比我能想象到的其他任何东西都更加不利于Windows程序的可读性

    匈牙利命名法在早期的Windows编程中很流行,并且它的确起了很大作用想想第一个Windows程序是在ANSI C之前写出来的,那时的函数还没有原型并且这些程序主要是基于16位的存储介质(默认情况下函数指针为长指针而数据指计为短指针),通常只有一个数据段用户则运行多个程序实例。在这種情况下如果不用一个显式的LPSTR类型转换就调用下面的语句,就会发生错误:

    因为编译器不知道需要的是长指针因此它会用一个默认的短指针去代替,这样就出现了问题在这种情况下,程序员必须像一个人工的编译器一样手工地检查每一行代码,保证所有的函数调用傳递的参数都具有正确的类型和正确的指针长度一旦没有做到这一点就会发生严重的错误。在这种环境下我也会使用匈牙利命名法的。

    时代发生了变化现代的C++编译器带有原型和强大的类型检查功能,能够检查出大多数类型不匹配错误(也有例外比如一般类型中的void, WPARAMLPARAM以忣可变参数函数像wsprintf)。車实是代码编译器告诉了你关于代码本身的很多信息比如说,当编译下面这条语句的时候:

我们知道firstNamename是字符串根据上下文list是一个指向list对象的指针。函数的上下文通常能够提供足够的信息让我们根据代码决定变量类型就像在散文中我们根据句子的仩下文来理解文章一样。让编译器来处理类型的检查是一个好主意在检查类型错误方面,现代的编译器肯定比程序员更合适

匈牙利命洺法的最大问题在于结果代码难于阅读(有些人也说最大的问题是把表示法与变量名字放在一起严承违反了数据抽象的原则)。尽管用匈牙利表示法前缀并没有强迫你使用不好的变量名字但是结果却往往是这样的。用匈牙利命名法的程序员似乎把注意力集中到了前缀上面而没囿关心这种做法导致的可读性的降低结果就是这种命名法变得极其难懂。因此尽管可以把指向工具箱的指针命名为IpszToolTipText匈才利命名法迷们會有种难以抗拒的冲动要把它叫为lpszttt

匈牙利命名法还有一个严重的问题就是它使程序难以维护因为它把表示和意义结合起来了。如果表礻发生变化,变量名字也要跟着变化不幸的是,一旦某一个前缀确定下来来(比如说在一个已经公布的API函数里)它就很难再改变了。例如WPARAMLPARAM变量现在都是32位的,但是它们过时的前缀却说明它们的大小不相同;同样地lp前缀中的l表示长指针,但是长指针在32位的Wmdows里面是没有意义的尽管这些前缀是错误和误导性的,但是它们都已经根深蒂固不易改变了。用匈牙利命名法给函数命名麻烦更大因为当函数返回值改變的时候,你的选择要么是留着错误的前缀要么是冒风险去修改一大堆的代码。

最后一个问题是匈牙利命名法并没有传递多少信息要想完全理解一个变量,你需要知道的不只是它的类型变量的作用域是什么?是全局的、局部的还是成员数据?变量是一个标量(scalar)还是个數组(array)?如果是数组数组的维数是什么?变量是否恒定(const)是否是静态变量(static),是否是可变的(volatile)?所有这些信息都和数据类型一样重要,但是却不一定總是被编进匈牙利前缀里面因此,任何只是基于匈牙利前缀得到的结论都可能不正确注意,除了作用域信息其他的信息都可以通过Visual C++嘚源代码编辑器中的数椐提示得到,此时你并不需要运行调试工具这些数据提示制作得非常棒。

    由不完整的信息带来的问题喑示了解决變量命名的更好办法一个好的命名传统就是指示出变量的作用域以便你在需要的时候检查它的定义。你的命名方法要明确地指出一个变量是全局的、局部的还是成员数据。依赖变量的定义比依赖匈牙利前缀更加有用和可靠

    在有限的场合下,匈牙利命名法是有用的。事实仩,有些匈牙利前缀在Windows编程中是非常标准的以至于如果不用它们反而会引起混淆,例如

比方说给设备句柄命名的传统方式是hDC,用其他的名芓就会引起混淆,因此不要把它叫做deviceConlextHandle当你需要在一个相同的作用域下命名一个对象、一个指向对象的指针、一个对象的句柄的时候(这种凊况在WimJows编程中间很常见),匈牙利表示法都是很有用的

尽管有它的缺点,匈牙利命名法仍然非常流行当然了,如果你的编程小组使用匈牙利表示法那么你也应该遵循己经建立的编程标准,因为一致性和清晰性一样重要我知道如果你己经打定了主意,我就很难改变你的看法了(等一下一一让我再给你最后一击:在匈牙利命名法中整数怎么表示是n还是i?以?为前缀代表什么是float还是BOOL flag?如果你不知道,你就要恏好思考下了)如果你还没决定,那就别再使用匈牙利命名法了它不是书写Windows程序的正确方法。你可以采用很多方法来提高你的代码的可靠性和可读性但是匈牙利命名法不是这些方法之一。

    尽管匈牙利命名法过去很有用但是时代改变了。用匈牙利命名法书写的程序难以閱读难以维护,也容易被人误解

C++语言中,可以在一行里面书写好几个语句它对简洁表示法的继承鼓励了这种做法。当你在选择一行Φ要安排哪些语句的时候要时刻牢记调试的时候是面向行的,除非你在汇编代码一级进行调试调试手段比如断点(breakpoint)、命令(如StepInto,StepOverSet Next Cursor,断言(assertions)trace语句,还有MAP文件都是基于源代码的行的用过于复杂的行会使得难于设置断点、跟踪程序或是决定出错的代码和错误的断言,从而不能充分利用这些调试工具从调试的角度出发,每一个语句行都应该作为一个单独的原子单位

    一些程序员十分仔细地排列他们的代码,然洏有一些程序员让代码处于“天然”的状态毫不奇怪,我觉得统一排列的代码更可取不仅仅使得代码更好看,而且排列使模式更好识別如果代码没有遒照模式,就说明出现了错误比如说,当我们试图标记出下面这段代仍的错误时:

    这种没有棑列的代码难以快速扫描现在试着在这段排列好的代码里面标记错误:

后者与前者有着明显的区别——错误跳到你面前了(在这个例子里,“=”错误地代替了“!=”“&”错误地代替了“&&”。注意要求统一排列并没有意味着你要把所有的语句都排列得一丝不乱到了可笑的程度用两三个不同的排列方式也不错。例

    考虑用统一的排列方式能够使类和函数的定义、变景的定义和ifwhileforswitch等语句更加明显用空格而不是用他键能够使语句的排列独立于tab设置。在Visual C++中你可以用Tools里面的Option命令来设置TabsInsert space选项(如果你曾经维护用不同的tab设置书写的代码,你就能明白我的意思)

Language》一书来学习寫C程序的。过了—段时间之后如果你试图把书平放在桌子上或者甚至把它从屋子里的这头扔到那头,它都可能打开到第49页这里面是C的優先级和结合率表。为了保证书写出正确的程序我经常査阅这一面,因为这些规则告诉你什么时候必须用括号

    很多年我都不再看这些表了,并不是说我已经记住了这些表或者是这些规则能够在线获得相反,是因为我采用了Steve Maguire在《Writing solid Code》一书中的建议:不要査书!为什么这是┅条好建议记住你的代码的读者是你自己、其他的程序员以及编译器。尽管编译器已经牢记了优先级和结合率大多数程序员却不一定莋到了这一点。所以如果你不能确定是否需要用括号你就需要用括号。继续下去并且使用它们即使它们并不是必需的。读者并不需要査阅优先级表和结合律表来理解你的代码也不需要查阅这些表来决定你是否用错了。这种策略帮助你节约了时间避免了错误,并且使伱的代码易于理解最好的是,使用多余的括号并不影响编译后的代码因此不会带来性能的损失。当然了除非没有括号的时候产生了錯误。

用好的注释能使你的代码不易出错有其是那些由于维扩不善所带来的错误。又一次记住你的源代码的读者是你自己、其他的程序员(包括维护程序员)以及编译器;虽然编译器不懂注释。但是程序员明白由于将来可能其他的程序员要维护你的代码,因此要保证提供給他们必需的并且是易于理解的注释记往要给类、结构和函数以及任何你在代码中间的假定做出解释。

给那写令人困惑的、带点小窍门嘚或是不常见的代码做出注释尤为重要有人或许会说,这些代码应该重新书写而不是做出注释但是有时候这样的代码是必要的,如果伱需要解释一些代码中无法说明清楚的东西那就用注释吧。尤其是当你在开发的时候做出了一个不好的修改那么你必须做出注释。比洳如果你修改了代码,以为它会改善性能但是实际上导致了一个错误的发生,那么你一定要在注释里指出这个改变以及问题的本质否则,其他的人(甚至你白已)就可能在将来做同样的“改进”想得到更多的信息,请看Steve

    这里有几种采用C++语含本身和它的编译器来防止错误囷避免隐含的失误的方法

前三种技术用C++语言而不是C预处理。使用预处理的问题在于编译器对于预处理器所作的事情一无所知因此无法鼡数据类型检查错误和不一致的地方,预处理的名字不在符号表里因此也不能用调试工具来检查预处理常量。相似地预处理宏被编译進去,因此你不能用调试工具跟踪宏预处理宏由于优先级的问题和传递参数带来的副作用而臭名远扬。相反编译器能充分了解constenumlinline语呴,从而能在编译的时候对出现的问题向你发出警告由于内联函数在调试的时候是和普通函数一样被编译的,你可以用调试工具跟踪里媔的执行佴是你却不可能跟踪预处理宏。

    选择语言而不是预处现有一个很重要的异常:在很多调试代码里面预处理起到了很重要的作鼡。原因是调试代码经常需要从非调试代码里面得到不同的行为而最有效的办法就是让预处理为调试创建不同的代码。因此本书中包含了大量的基于预处理的调试代码。

    选择C++语言而不是C预处理但是要理解调试代码的时候需要使用预处理器。

在创建对象、类型的安全性囷灵活性方面使用new/deletemalloc/free要好。malloc只是分配内存new不仅分配内存(而且正好是需要的大小),还调用了对象构造函数。相似地free只是释放内存,而delete先調用对象析构函数然后才释放内存实际上,你不能用malloc创建C++对象因为你不能直接调用构造函数。为了安全起见你可以通过_set_new_handler的使用,在new絀现错误的时候抛出异常而此时malloc只是在默认的时候返回0——虽然你也可以通过_set_new_mode的使用使malloc在失效的时候和new有一样的行为(见第5章示例)。用new还能带来类型的安全性你不会把结果指针赋值给不相容的指针类型。相反malloc返回的是void*。因此总是需要一步强制类型转换另外,new运算符可鉯被类重载因此提供了更大的灵活性。由于Visual C++中的mallocnew使用相同的核心内存管理程序(因此有相同的出错诊断支持)mallocfree就没有任何优势了。

朂后使用C++输入输出流(也就是<<>>运算符)而不是使用C标准输入输出库(也就是printf/sprintfscanf/sscanf),这样做有利于安全性和扩展性从调试的角度来看,标准输叺输出函数的最大问题在于编译器不能对控制流参数进行任何类型检测实际上,编译器对这些函数了解甚少甚至你没能把指针传递给scanf,它都无法对你做出警告相反,输入输出流中的任何问题都能在编译时刻检测出来进一步,你可以把输入输出流的运算符运用到任何C++類中而标准输入输由库函数只能用在内嵌的数据类型上面。关于这个方面的更多介绍可以参看Scott

    把不同文件之间共享的各种定义写在头攵件里面,使用头文件保证所有的定义对于各个编译单元来说都是相同的注意,C++把外部定义的符号隔离开来以保证类型安全的链接,這一点避免了很多类型的错误匹配但是,当extern C也包含进来的时候使用强制类型转换、类型指针或者函数定义都有可能出现类型的错误匹配。另外数组和指针接受相同的处理,因此下面的这段代码可以链接:

[0]会产生一个存取违例为什么会这样?把所有的共享定义放在头攵件里不要在你的.cpp文件里面看到extern关键字。

    另外最好把参数名字放在头文件的函数原型里面。尽管编译器可能不在乎但是参数名字能使代码更易于阅读和使用。

在使用变量之前一定要记住:把它们初始化在初始化之前就使用变量肯定会产生错误(但是一个未经初始化就作為函数的输出参数的变量应该被认为是初始化了的)。这个观点很简单也很明显但是在初始化变最后面的细节却是十分复杂的。内嵌(built-in)(或是內在(intrinsic))数据类型的变量(比如charintfloat)如果是全局变量,都是在装载程序的时候自动初始化为0的但是如果它们是自动(automatic)(栈中的局部变量)或是用new汾配的(在堆(heap)里面)就没有被初始化(实际上,如果你选择了编译里的/GZ选项自动变量就会在调试的时候被初始化,这一点我们在这一章的稍后蔀分再讨论)对象不管存储在什么地方,都是通过构造函数来初始化的由于大多数构造函数都能把它们的对象初始化到一个良好定义的狀态(毕竟这就是构造的数的作用),你通常不需要对对象进行初始化比如说下面的由Visual C++ ClassWizard产生的微软基本类库(MFC)的初始化代码并小需要,因为CString类嘚对象己经在构造函数里被初始化为空串了

不幸的是,这也有例外不是所有的对象都由构造函数初始化的。比如说MFC中的CPoint,CRectCSize就不是由咜们的默认构造函数初始化的,因此你总是要明确地初始化这些别象虽然这是MFC中的一个缺陷,但我猜想这是刻意安排的因为这样这些對象就可以和API中对应的RECTPOINTSIZE相同了。在构造函数里面你总要初始化数据成员。毕竟数据成员都需要在某种程度上被初始化那就把这个任务交给构造函数来完成好了。

你必须明确为在栈中和堆里分配的数组和数据结构进行初始化尽管可以用库函数memset来初始化,API函数里的ZeroMemory对於Windows程序更为方便但是不要用这些函数来初始化对象,因为这样的初始化会破坏基类和数据成员构造函数的工作比如说,不要做下面的倳情:

而是应该分别初始化每个需要初始化的数据成员

builds)中比在调试版本中要做得好,因为变量的使用是由优化器来检查的这个事实说明偠经常运行发布版本,尤其是你要跟踪一个复杂的错误的时候

    经常创建发布版本来帮助检测未初始化的变量

    如果位掩码不是0或者2的幂佽方那么位掩码就容易出错。如果每个位都是2的幂次方在整数的表示法里面,一位就代表一个状态比如说,下面的位掩码就可以正確使用:

    不幸的是Windows程序中使用的位掩码不都是2的非0次方,这能使一个整数中包含更多的信息5考虑下面这种用法:

    两条位掩码的语句都是鈈正确的这种类型的错误很难被追踪到,因为代码看上去很正确有可能在大多数情况下,代码都会正常工作比如说,如果你写的代碼处理静态控件文本那么你不会意识到你的代码混淆了居中文本样式和图标样式,直到别人用图标样式测试你的程序一一这也不是一个顯而易见的测试这些位掩码的语句应该正确书写如下:

不幸的是,这个问题意味着你不能抽象使用位掩码检查是否正确书写位掩码语呴的唯一方式就是査看位掩码的声明,实际上这种类型的位掩码错误是无法得知的,除非你知道特定的位掩码值在书写位掩码代码的時候为了避免出现问题,一定要检查这些声明,并且使用必要的掩码宏(比如说HRESULT_CODE())或者子域掩码(比如说SS_TYPEMASK())

尽管C++现在有一个内嵌的布尔类型(即bool,徝为truefalse大小为1个字节),Windows程序通常还是使用BOOL定义如下:

   C++中,一个布尔表达式如果值为0则为假如果有其他的值则为真。这就意味着下媔这段看起来正确的语句实际上有错误:

API函数的返同值为布尔值的时候返回的不仅仅是0或者1(IsWindow函数就是一个很好的例子)。实际上Windows API文档总昰用0和非0来代表返回值。如果booleanValue1000次运行中有一次等于2了你就很难在调试的时候发现错误。

    显然布尔表达式不应该检査是否为真而应该檢查是否为假。刚才的例子正确书写后表示如下:

    布尔表达式应该检查是否为假而不是检查是否为真

   C++中间使用整型变量通常比较直接,泹是有几个典型的错误应该注意:

    ?减一错误一个常见的错误发生在一个整型变量做减一运算的时候,尤其是在循环中整型变量作为循环控制变量时。一个简单的检测方法就是做一个测试尤其是要覆盖第一个和最后一个索引值,如果这个测试通过了那么其他的全部實例也就正确了。

    ?除零除法运算中用零做除数会导致除零异常。在做除法的时候如果你不能保证除数不为0,那就要处理可能出现的異常

?溢出。整型变量大小是有限的因此如果它们的值过大就会最终导致溢出,尽管32位的Windows程序没有16位的程序那么容易发生溢出但是當有符号数有正的或者负的过大值,或是无符号数可能出现负数时都需要检査是否发生了溢出。要么避免要么处理溢出同时,用Limits.h中定義的最小值和最大值来作为整型数据类型的大小限制

    ?用无符号数来检査有符号数。显然无符号数和有符4数有不同的最大值和最小值。比如说一个无符号数永远都不会等于-1,因此下面的语句不会像预期的那样起作用:

C++的编译器善于在比较变量的时候发现无符号数与囿符号数之间的不匹配,但是它却不能发现一个变量和一个超出界限的常景进行比较时的问题所以不要指望编译器会对这类错误提出警告(对于不匹配的比较要十分小心,当你比较一个无符号数和一个有符号数的时候编译器会把有符号数转换为无符号数,这可能不是你所唏望的事情当unsignedInt等于0xFFFFFFFF时,前一个语句会成真)编译器给你警告的时候,不要急于用强制类型转换来消除警告先想想自己要做什么,在这種情况下一个进行强制类型转换的替代方法就是不再区分无符号数与有符号数,只用有符好数尽管这种办法孴起来有点严历,但是当無符号数经常与有符号数混淆的时候使用无符号数的好处就降低了。

    字符型变量的问题与整型变量的问题相同另外还有两个考虑因素。

    ?字符类型的数据结构是有符号的在Visual C++里面,字符型变量是有符号的这个意料不到的细节经常被忽略,比如说不要指望一个字符型嘚变量等于255。符号扩展也会给你带来预期不到的结果

    ?溢出。由于字符型变量的取值范围在-128~127之间如果你不仔细考虑的话,很容易发生溢出下面这段代码是一个无限循环的例子:

有意思的是,编译器并不报错?

    浮点型的指针变量也有相同的犯错误的可能性

    ?被零除。囷整数一样被0.0除也会产生问题。但是不同的是被0除并不会导致默认的异常发生,而是赋了一个奇怪的值——“1.#INFO,如果你原意的话,伱出可以用下面的代码来产生浮点指针异常

在做除法的时候,如果你不能保证除数不为0.0那就要处理可能出现的异常。

    ?上溢或下溢佷难想象double类型的变量会发生溢出,但是float类型的就相对容易发生上溢或下溢如果你不能保证不发生上溢或下溢,那就要处理可能出现的异瑺同样,可以用Foat.h中定义的最小值和最大值来作为浮点数据类型的大小限制

    ?检测浮点指针的值。浮点指针没有精确的二进制表示法所以不要期望它们会有精确的值。因此也不应该用比较两个浮点指针的值的方法来判断二者是否相等。比如要位测一个变量是否为42.0,應该用下面的代码;

这里FLT_EPSILON是浮点值的最大表示误差而DBL_EPSILON是双浮点值的最大表示误差。

C++代码中指针以问题众多而臭名昭著,但是你可以采取一些步骤来消除其中的一部分问题第一步是你怎么创建指针和用指针来结束一个对象。初始化一个指针的时候要么让其指向一个囿效的内存地址,要么设为0(空指针)这样才避免了指针指向了无效地址。看起来似乎很明显但是一个常见的错误就是当指向一个对象的指针被释放了以后。重新使用这个指针时就忘了再次初始化看下面的这段代码:

在这个例子里面,当pObject1指向的对象被释放了后pObject1就指向了┅个无效的内存地址。这个不稳定的指针可能会引起问题除非是不再使用它,或者这个指针是一个数据成员并且这段代码在一个析构函數中pObjecr2pObject3在它们所指向的对象被释放之后,都被正确地重新初始化然而,C++语言保证delete 0是无害的因此没必要破坏你的代码来避免删除一个涳指针,就像pObject3一样

    消除指针问题的第二步是关于怎样处理指针为空的可能性。考虑下面一段代码:

第一种方法比较冒险因为代码假设指针永远都不会为空。但是如果指针为空了就会引起很大的问题。第二种方法具有健壮性因为它处理了可能出现的存取异常情况。这種代码书写起来也比较简单你不用枳心检查所有出错的情况,问题是怎样处理这类异常尤其是你不知道究竟是哪里出错了。假设在try这個模块里面有大量的语句都有可能发生异常,那么就很复杂了第三种方法,虽然也很健壮需要较多的代码,但是能准确定位出错的玳码你会选择哪种方法?在第5章“使用异常和返回值”中会详细回答这个问题

    当停止使用一个指针的时候,最终的问题是:你能保证指针指向了一个无效的地址叫除非你能检查指针的值(在前面的代码里面),否则你的答案就是否定的要避免出错,就要处理这种情况

    偠避免出错,回收指针所指的对象的时候要重新初始化这个指针并且要在指针被释放之前为空时就对其进行处理。

    句柄和指针很相似洇此它们也有相同的问题。记住释放句柄指向的对象也要重新初始化这个句柄并且要处理句柄为空的情况。

    很多Windows中的数据结构使用的技術可以用在你自己的数据结构中。特别是Windows总是把结构的大小作为结构中的第一个数椐成员下面是一个典型的例子:

这样做有两个好处。第一个这个值起到了版本标识的作用。因此你将来可以在结构中加入新的数据成员,并且很方便区分不同的结构版本第二个好处,这个值还可以作为结构中的一个信号这样就很容易判断出结构是否出现了问题,比如说你希望这个值是20,但是返回了0x4F98D638,不管其他返回徝是否正确你都可以知道结构中肯定出现错误了。你可以在调试代码的时候和把结构存入永久存储之前检査这个值

    如果你使用这种技術的话,要保证这个值的合理性使之能够在出现问题的时候,明显地表示出来不幸的是,使用这种结构的Windows API函数要求这个值必须正确否则就不能正常工作,并且没有任何声明忘记初始化这些值是很容易的,这种毫无声息的失败使得我们需要大量时间来调试程序

    顺便說一下,你能通过定义下面的模板来创建一个结构从而避免初始化时的错误

   C++中,你可以有效地将大的对象传递给函数也可以传递指針或者引用。每种方法都有自己的优点看看下面这段函数定义:

    带指针的函数有两个好处:策一个你可以给函数传递一个空指针,就像默认参数值表示的—样在PointerFunction这个例子里,另一个好处就是通过使用取地址运算符可以轻易地说明参数可以被以下函数所改变:

    但是如果你偠使用指针,就没有这个好处了

    上面的例子揭示了依赖取地址运算符来决定一个变量是否能被修改的做法的缺陷。C++中使用的方法是查找函数原型里面是否有const属性

    相反,引用是对象的别名因此,它必须和有效的对象相关联不存在空的引用和没有初始化的引用。相应地当你在函数中收到一个引用参数时,你可以肯定这是个有效的对象在你使用之前也没必要检査对象的有效性。从测试的角度来看这個好处使得用引用作为参数比用指针作为参数更为健壮,虽然可能会丧失一些灵活性因为你不能有空的引用。

强制类型转换是C++的一个表達式它将一种数据类型强制转换成另一种数据类型。实际强制类型转换对于编译后的代码的效果依赖于涉及到的数据类型进行强制类型转换的时候,将会调用相应的构造函数或是转换函数来创建一个新类型中的临时对象将一个引用转换到一个对象有相同的效果。但是把指针从一个类型转换到另一个类型的时候,并没有改变指针而是消除了一个编译错误(使用不正确时,还会引起新的编译错误)这里囿一些编译器不能做的强制类型转换,比如说在没有所需要的构造函数和转换函数的时候将一个函数指针转换到数据指针或者反过来。這样的转换总是会引起编译错误

    下面的代码说明了当你进行强制转换的时候世界会发生什么:

尽管在理论上强制类型转换并不必要,但茬实际中却是十分必要的这一点在Windows程序中尤其重要,因为Windows程序总是会使用大量多态的数据类型不幸的是,使用强制类型转换是一种危險的做法因为很容易产生错误。问题就在于强制类型转换破坏了编译器进行类型检查的功能而这正是编译器查找错误的最有效的机制。如果编译器能够做强制类型转换它就会不提出警告就自动完成。为了保证安全性每一个强制类型转换都需要你手工进行类型检査,從而增加了你的负担因此,不要匆忙就使用强制类型转换除非没有别的更好的办法。

    可能最容易发生错误的强制类型转换就是向下强淛类型转换(downcast)也就是把一个指向基类的指针强制转换到指向派生类的指针。这里有一个标准的例子:

只要SomeFunction返回指向CDerived1对象的指针这段代码僦总是会成功运行的。但是在这段代码中并不能保证前提总是真,当前提为假实际上pDerived就会指向失效的内存,因为它的对象己经无效了这样给m_Data赋值就会引起存取异常。如果在向下转换发生后不久程序就崩溃了那么很有能是无效的向下转换出了问题。

    普逋的强制类型转換和向下强制类型转换尤其会给维护带来麻烦在这个例子中,即使你现在能够证明SomeFunction总是会返回指向CDerived1类对象的指针但是产生这个结果的環境却有可能在将来发生变化。一个小小的改动就会破坏这段代码甚至编译器都不会给出警告。

    尽管强制类型转换容易出问题但是使鼡新的C++中的强制类型转换仍然会减少出错的危险。下面是C++风格的强制类型转换的一个小结:

?static_castC风格的强制类型转换除了语法的区别外鈈同的地方在于static_cast不能在指针类型与非指针类型之间进行转换,也不能消除类型中的constvolatile属性最重要的是,它能在编译时刻就验证被转换的變量与目标类型之间是否相容(用尖括号表示)如果类型转换不合理,就会导致编译时刻错误由于它使用起来更加安全并且不进行运行时(runtime)嘚检查,因此更偏向于使用这种强制类型转换我们并不推荐用向下强制类型转换,因为它需要进行运行时刻检査来保证安全性

?dynamic_cast在运荇时刻对强制类型转换进行检查。当指针无效时返同0,若是无效的引用强制类型转换(记住不存在空引用)就发生bad_cast异常。建议使用这种强淛类型转换来进行向下强制类型转换

?const_cast消除类型中的constvolatile属性。比较典型的是用来消除类型中的const属性这种参数并末在程序中被修改,但昰也没有被声明为const类型

    ?reinterpret_cast能转换不相容的数据类型。特别是在指针类型和非指针类型之间进行转换比如在把一个指针传递给多态LPARAM参数時进行的转换。

注意dynamic_cast需要C++运行时刻类型信息(RTTI)这就需要一个vtable,也就意味着你不能在没有虚函数的时候使用它进行向下强制类型转换对象(会导致编译时刻错误)。这看上去似乎是个不幸的限制但是如果你的类中没有任何虚函数,你也许并不需要大量的向下强制类型转换因为这個类根本就不是一个基类。在上一个例户中应用dynamic_cast就得到了下面这段代码:

    C++风格的强制类型转换比C风格的强制类型转换更加安余、明确,吔更加容易在源代码中定位

使用dynamic_cast使得向下“强制类型转换更加安全,但是带来了一些性能的损失由于更加清晰地指出了你想做什么,C++風格的强制类型转换使得结果代码易于理解和维护更加明确的代码也避兔了偶尔发生的错误强制类型转换,从而增加了安全性比如说,你不会一不小心删除一个类型的const属性并且,C++风格的强制类型转换看上去和C风格的强制类型转换也很不同这种差异是故意造成的。C++风格的强制类型转换语法使其更容易在源代码中被发现你只需要査找所有“_cast”就行了。C风格的强制类型转换看上去都很简单但是不像C++语訁,它们不能用Find in Files命令查找尽管可搜索性一开始看起来并不重要,但是记住Find in Files命令是一个重要的调试工具它能帮助你査找所有具有相似错誤的代码段。

    C++风格的强制类型转换能消除限多错误但是经常最好的解决办法是压根儿就不用强制类型转换。这里有一些避免使用强制类型转换的技术

    ?避免使用多态数据类型。多态数据类型需要使用强制类型转换在你使用Windows API的时候,你会经常用到多态数据类型但是如果你用C++类库的时候会有更多的选择。

    比如说基于模板能提供类型的安全性,但是基于指针就需要进行强制类型转换

   PS:使用多态并没有錯;发生向下转换往往是设计上的缺陷造成的,或者是历史遗留问题)

?使用更加广泛的基类C++程序通过使用基类中的虚函数从而实现了哆态行为。你经常可以把类的行为从基类移至派生类来消除强制类把转换对于特定数据类型的特殊处理(通常是通过if语句和强制类型转换囲同实现的)可以利用在桩类中增加虚函数来消除(尽管这种做法有时候对于一般类并不可行,比如说MFCCObject类或是其它你不能修改的类)注意对於特殊情况下的类型处理会损害类的可扩展性,因为创建新的派生类需要加入更多的特殊实例

?提供特殊的存取函数。你可以通过提供特殊的函数来消除强制类型转换这种函数就是做必要的强制类型转换的。在MFC中比方说,CView的派生类总是提供一个特殊的GetDocument函数它能返回囸确的CDocument派生类的对象。

?让编译器隐式处理类型转换C++能隐式处现很多类型转化。从而消除了做强制类型转换的必要你可以提供合适的類型转换函数来帮助编译器做这种隐式类型转换(不幸的是,这种隐式类型转换有有自己的问题这点你必须意识到。细节部分参看Scott Meyer的《More Effective C++》苐五条)比如说,下面的类型转换就不需要强制类型转换其中类的定义同上:

    另一种消除强制类型转换的方法是使用C++相关变量(covariant)。相关变量返回类型就是基类虚函数返回基类类型的时候派生类重载了虚函数,并且返回派生类的类型不幸的是,Visual C++编译器现在并不支持这种标准特征因此这种方法不予考虑。

现在最后一个问题,如果C++风格的强制类型转换比C风格的强制类型转换好那为什么Windows代码里面却不能經常看到呢?MFC代码中尤其少见,但是MFC代码中却经常使用向下强制类型转换比如:

习惯的力最是一种解释。事实上C++强制类型转换在Windows编程書中出现不多的原因并不在此(注意这样的例子很少考虑怎么样避免发生错误,调试也只是“留给读者练习”)但是最重要的原因是在这种凊况下使用dynamic_cast会失败。函数CDialog::GetDlgItem总是返回指向CWnd对象的祀指针而不是指向派生类如类CEdit的指针。CDialog::GetDlgItem只是简单调用Windows::GetDlgItem这个API函数返回一个一般的窗口句柄,然后把结果封装在CWnd对象中由于运行时刻类型信息显示出返同的对象是一个CWnd类的,dynamic_cast就会在强制转化为派生类型的时候返回一个空指针紸意,不像一个普通的错误向下强制转换当调用者不是CEdit类的对象(CStatic类的对象)的时候,调用成员函数类似于CEdit:GetLine是无害的因为函数只是一个簡单的::SendMessage wrapper,它发送的消息被安全地忽略掉了尽管static_cast在这种情况下也工作得很好,但是没有提供类型的安全性

behavior”就是说你的程序就要崩溃了。

如果你是用MFC注意从CObject派生的类并不使用运行时刻类型信息,而是用它们自己的运行时刻类信息(RTCI)——虽然在MFC工程中设定了RTTI后两者你都可鉯使用。采用RTCIdynamic_castMFC中的版本是一个DYNAMIC_DOWNCAST宏它对于无效的强制类型转换返回了一个空指针,还有一个STATlC_DOWNCAST宏它在无效强制类型转换的时候返回空指针,并且对调试版本中的错误强制类型转换显示一个断言在发布版本里再执行这个强制类型转换。这种奇怪的权衡(tradeoff)使你要么不能拥囿强健的代码,要么不能让出错的强制类型转换自己把自已显示由来你可以用自定义的MFC强制类型转换宏来消除这种权衡,做法如下:

从某种意义上说使用const与使用强制类型转换正好相反。当强制类型转换消除了编译器通过强大的类型检査来査找错误的能力的时候使用const能增强这种能力。当你在写下const的时候就是告诉了编译器:这个变量是不应该发生变化的,因此要保证它不被改变const属性是最好的类型拓展。比如char*const char*,相关但不是同一个类型在程序中仔细地使用const是一种好办法,能帮助编泽器在编译时刻帮你发现错误这远比在运行时刻发現它们要好。

如果你在使用const方面经验丰富你就会知道有一个重要的细节。由于你不能把const类型的变量传递给非const类型的参数那么如果你的程序没有正确使用const(也就是说没有在所有不能发生改变的变量前面声明为const类型)时,你引入了一个const变量后果会波及整个程序。你就会不得不妀变很多函数的定义并且在你的类里增如很多const操作符。使用const就是一个不动则已一动就动全身的问题。这个问题可以通过从开始就正确使用const并且始终严格遵守这个规则来彻底消除。在程序写成后再加入const实在是个很头疼的问题

    任何人都知道什么是for语句,什么是while语句以忣它们之间的区别。尽管如此还是能够经常发现在应该使用for语句的时候使用了while语句,问题就在于虽然

在计算上面是相同的但是如果在語句后面在加一句continue后,这种相等关系就会被破坏因为for语句能够保证加一操作可以被执行,但是while语句就不能了因此,在while循环后面加上continue语呴是一件很危险的事情因为人们总是忘记执行变量增加的操作。当continue语句很少能够被执行到的时候这种错误就不容易被检查出来了。依峩的经验while语句经常出错的地方一般是在初始化、检测,或者变量增加操作很复杂或是增加操作根本就不是什么加一运算符,而是类似於GetNextObject这样的函数在这些场合下,都可以用for语句来代替while语句

    这个原则在当for语句中使用多个需要增加的变量的时候也同样适用。例如下面的玳码:

    当给一个对象分配了内存后调用构造函数就能使对象到达一个良定义的状态(well-defined state)。相似地调用析构函数正好和构造函数做的相反。┅般是在分配给对象的内存被解除分配前释放任何占用的资源。两种运算如果使用不当的时候邡会引起错误;

    做到以下几点后你就可以茬构造函数中避免发生:错误:

一个类通常会有好几个版本的构造函数比如说默认的(就是没有任何参数)构造函数、拷贝构造函数以及其它囿各种各样参数的构造函数。由于这些构造函数的代码都很相似你可以通过调用一个保护的或者私有的辅助函数来完成绝大部分的工作,这样就能够避免复制以及潜在的维护方面的问题比如说,MFC CString类有七个构造函数这些构造函数都调用了保护的CString::Init的数。

一个更困难的问题昰处理可能出错的构造函数通常,构造函数需要分配内存创建资源或者打开文件,但是不能保证这些运算都能成功并且,构造函数沒有返回值因此也没有直接的方法来显示出现了错误。一个常见的解決方法(在很多MFC类中使用)就是把对象创建分为两步:笫一步让构造函数以一种不会出错的方式初始化对象;第二步是让某些初始化函数(比如Init或者Open)完成工作,这一步可能出错比如说,MFC中的CFile默认构造函数以昰简单地把文件句柄设为0并且把状态布尔变量设为falseCFiIe::Open函数真正打开文件但是可能由错,

另一种方法是使用异常并且分二个阶段进行初始化过程,这些都是在构造函数中完成的第一阶段,以不会出错的方式初始化对象到达一个已知的状态第二阶段,用可能在try段内出錯的代码初始化对象最后一阶段,就是在catch代码里面处理异常如果出现异常,就会在构造函数里消除分配的资源并且再次抛出并常。紸意只有在成功构造对象后才能调用析构函数。因此不要指望在构造函数抛出异常的时候用构函数来清除对象。下面的代码是一个典型的采用异常处理的构造函数代码:

auto_ptr智能指针模板类或是那些依赖于C++语言来清除对象的手段而不是程序员的自我约束。这种方法在第9章“内存调试”中讨论所有这些构造函数都很有效,你可以选择最适合你的一种但是,如果你的构造函数在程序里可能会出问题那么朂好写得让它易于理解。

    要知道构造函数中的虚函数并不像一般的虚函数也就是说,如果基类的构造函数调用了一个虚函数调用的实際是虚函数的基类版本,而不是重载后的派生类版本否则,如果基类调用了派生类版本的虚函数就会引起存取异常,因为此时还没有構造基类的数据成员如果你的构造函数真的需要虚函数,那么就使用单独的个初始化函数好了

    相似地,你可以通过做到下面的几点来避免在析构函数里出错

异常处理中一个关键的细节就是在栈展开的过程中抛出的异常会终止整个程序。由于在处理异常的时候经常要调鼡析构函数因此析构函数尤其容易犯这个错误。如果在处理异常的时候调用某个析构函数并且这个折构函数中出现的异常没有在这个析构函数中得到处理,程序就会被终止因此,一定要保证析构函数的异常在析构函数中得到处理就像下面这段代码中写的那样:

    最后,要保证基类的析构函数是虚函数这样,就算对象是个指向基类的指针也会调用派生类的析构函数。否则如果基类的析构函数不是虛函数,就会引起资源泄漏在析构函数中,虚函数与构造函数里的虚函数不一样

    如果一个基类的构造函数分配了资源,这个类就需要┅个虚的析构函数另一个不太明显的事实就是这个类还需要一个拷贝构造函数和赋值运算符。Marshall Cline把这一条列为“大三法则”(Big Three)即“如果一個类需耍一个析构函数,或者一个拷以构造函数或者一个复制运算符,那么它就三个都需要”这条规则的道理显而易见,如果类里面沒有提供拷贝构造函数和赋值运算符C++编译器会自动加上。一旦构造函数分配了资源编译器对这些函数的实现就肯定会出错。虚析构函數就是一个明显的标志如果你不希望类中出现这些函数,你可以在类中定义它们为private并且不予实现,从而避免编译器自动加入这样,任何对这些函数的使用都会在链接的时候报错

    把错误从程序中消除的最好办法就是让编译器帮你把它们找出来。任何方法都没有这个重偠这一个部分将为你提供几种技术来提高编译器查错的能力。

    尽量釆用编译时刻检查而不是运行时刻检查

    这一章的很多编程问题都能影响编译器査找错误的能力。比如说编程风格就是一个因素。考虑下面的语句:

    Visual C++高高兴兴地编译这条语句不给你任何瞥告。这条语句的問题就在于相等运算符==容易与赋值运算符=混淆如果不知道程序上下文,最好假设程序员实际意思是

    要避免这个问题很多程序员都采用丅面的风格:

    当然,如果左边的操作数不是常量这种风格就没有作用了。你可能会考虑使用这种风格但是我推荐另一祌更加优秀的解決方法。这就是使用最高的编译警告级别(/W4)而不是默认的警有级别(/W3)如果你用/W4,那么语句

    当两个操作数都是变量的时候你也会收到警告:

    Visiml C++6.0中介绍了/GZ编译选项它是用来帮助发现那些在发布版本里才发现的错误,这些错误在调试版本里都无法发现这个编译选项的作用如下:

    ?茬通过函数指针调用函数时,检查栈指针确认是否有调用规则不匹配

不幸的是,使用/W4层的警告有一个缺点和那些有用的鳘告一起,你還会收到大量的假警告很多标准Windows头文件因为使用非标准语言特征导致/W4警告。某些类型的程序比如说使用标准模板库(STL)的程序,在采用/W4警告时尤其容易导致假警告不停地筛选这些消息是一件浪费时间的事情,并且这些假警告使真正有用的警告不易被发现由于你使用最高層的警告来发现代码中的错误,而不是处理那些不重要的细节或是标准Windows头文件(这个你永远都不要随便改变)中问题因此你需要消除某些警告。如果你使用MFC那么Afx.h头文件能帮你解决大部分问题。

parameter”(警告C4100:未被引用的形参)Witidows程序中,不用到所有的函数参数是一件很普通的事情比如:

    解决这个问题的一个做法是从函数声明中删除参数名字,这样就能减少警告例如:

    第一个版本只是简单把参数删除了,这使代碼难于理解;第二个版本稍好但是看起来比较笨拙。MFC提供了一种我认为比较好的办法(这祌方法你也可以很容易地使用到非MFC程序中)——宏如下:

    当某个变量在发布版本中没有被使用时,就会用到UNUSED宏:如果这个变量压根就没有使用过时就用UNUSED_ALWAYS宏。这些宏“接触”(touch)到这些变量使编译器以为它们被使用过了。正如下面的例子:

    你可以使用#pragma warning编译器指示来禁止整个程序、特定的头文件、特定的代码文件或是特定的某一行代码的特定警告这就看你把#pragma指示放在什么地方了。这个指示非常灵活你可能需要参看Visual C++文档来了解使用这个指示的多种途径。比洳说你可以通过在头文件stdafx.h中间加入#pragma来减少整个工程(project)中的警告。

你也可以用下面的技术为某一行代码消除警告:

    最后一个例子如果你采鼡了一个没有明确使用/W4的库文件,用下面的方式你可以临时用/W3对其进行编译:

    由于这祌#pragma指示不是非常一目了然,最好你能给出注释解釋一下这个#pragma是干什么的以及为什么这样使用。就像在上面这个例户中一样

waming)法则,即只有在编译时不出现警告代码才算是可以掊受的(不存在“没有错误的编译”法则,因为有错误的代码根本就不可能被编译通过)当然,这条法则只有在假警告被消除了以后才能应用要求消除所有警告有两点好处;它强迫你检査所有的编译警告,当代码发生改动后引入的警告则很明显这条法则很有意义,但是你必须意识箌两个问题:

    1.消除编译错误和消除代码中的问题并不是完全相同的

第一个问题是说,通过抑制住警告而不是发现代码中的潜在问题来消除编译警告是一件简单的事情指针指向了不相容的数据类型?只要再进行一下强制类型转换就可以了出现了sign/unsign的不匹配?同样进行一佽强制类型转换就行了。强制类型转换是编译警告的排泄管道你可以通过它解决一切问题。尽管强制类型转换在很多场合下都是正确的但是你在做强制类型转换之前一定要仔细分析出现问题的特定代码。自动进行强制类型转换能够抑制编译警告,但是有可能掩盖住了重要嘚问题所在

    在消除编译警告之前要对它们进行仔细检查

variable”(警告C4101'id':未被引用的局部变量)只是因为你没有把代码写完。如果立即为了抑淛警告而修改代码就破坏了警告的价值

“没有警告的编译”法则的一个根本问题是,编译警告能帮助你发现错误因此不要轻易地清除警告。处理编译警告的核心是要发现问题而不是抑制警告本身。当然如果你做得到的话,应该立即解决代码中的问题或是抑制没有用嘚警告但是如果警告能够发现一个合法的问题,并且需要你在将来解决的话那就让那个警告留着好了。因此我并不推荐使用/WX编译选項(它把所有的警告都当成错误来对待),因为它会强迫你不考虑具体情况就抑制所有警告

    编译警告是一个好东西,在代码中保留一些编译警告比保留错误要好

    最后的一点就是“没有警告的编译”法则对于大的程序开发小组来说很有帮助。但是前提是允许出现异常最终目標是消除错误,而不是消除警告

    通过检查某些需要改进的程序,Caigill得出了一些C++风格的指导方针这里的“风格”不是书写代码的风格,而昰对语言的最根本的特征的使用风格比如说,抽象、继承、虚函数以及操作符重载

1999。(PS:本书确实不错搞管理的人也可以看看。可惜本書没有中文版)

Errors”都是与我们这一章密切相关的

something”这样的小窍门已经没用了。但是我认为多余三分之二的技巧仍然是有用的尤其是在避免出错这个方面。那些对于为了提高效率的短视行为的警告尤其有竞思所有的例子都是用FortranPL/1写的。

这本书涵盖了与编程相关的所有方面并且全面、细致地分析了编程风格,这是其他任何书都无法比拟的主题包括函数设计、数据设计、变量命名、基本数据类型的使用、玳码组织、条件语句和循环语句的使用、程序结构、程序复杂性、布局和风格以及文档。注意例子都是用C或者Pascal书写的并没有包括C++

    学习怎么正确使用C++编程的最终资源主要讨论一些基本问题。精华读物

我要回帖

更多关于 数值运算程序七年级 的文章

 

随机推荐