C++经验谈(摘抄)

一、用C++ 而不是 C
1.用const代替#define来创建常量
2.用enum代替#define来创建常量集合
3.用内联(inline)函数代替#define宏
4.用new和delete代替malloc和free
5.用输入输出流(iostreams)代替stdio
6.选择C++语言而不是C预处理,但是要理解调试代码的时候需要使用预处理器
7.检测浮点指针的值。浮点指针没有精确的二进制表示法,所以不要期望它们会有精确的值。因此,也不应该用比较两个浮点指针的值的方法来判断二者是否相等。eg:
#include<float.h>
if (fabs(floatValue - 42.0) < FLT_EPSILON ) ...;
这里FLT_EPSILON 是浮点值的最大表示误差,而DBL_EPSILON是双浮点值的最大表示误差。
8.如果在VC++中使用dynamic_cast,记住要在project中设置Enable Run_time Type Information选项
9.如果循环变量的增加操作在每次循环的时候都必须执行的话,那么就用for语句代替while语句。eg:
POSITION pos;
int line;
for(line = 0,pos=linelist.GetHeadPosition();
    pose!=0;
    line++,lintlist.GetNext(pos)) {
.....
}
10.如果构造函数真的需要虚函数,那么就使用单独的一个初始化函数好了。
11.保证析构函数中的异常在析构函数内部得到处理。最后,要保证基类的析构函数是虚函数。这样,就算对象是一个指向基类的指针,也会调用派生类的析构函数。否则,如果基类的析构函数不是虚函数,就会引起资源泄漏(resource leak)。

二、使用断言
12.用MFC时选择ASSERT宏
13.不要使用VERIFY宏,使用断言宏
14.在使用CObject派生类的对象之前都要调用ASSERT_VALID宏
15.总是在调用ASSERT_KINDOF宏之前调用ASSERT_VALID宏
16.移植代码的时候也移植断言
17.要想有效的利用断言需要一定的策略,不要随意的把断言分布到你的代码里面,而是遵从某些已经建立起来的模式
18."Assert the word",是说:不要试图选择哪一个断言要发现错误,而是根据你的断言策略断言一切
19.最有力的断言是用来验证变量之间的固定关系。这些关系称为不变关系(invariants)。其中,类不变关系和循环不变关系最为普遍
20.设计程序的同时就设计不变关系,在写代码之前理解它们,并且为它们建立文档
21.公有成员函数比私有和保护成员函数需要更全面的断言
22.充分实现AssertValid函数,不要使用MFC ClassWizard提供的默认实现
23.建议对AssertValid函数采用下面的断言模式:
void CMyObject::AssertValid()
{
 //  check the immediate base class first
 CMyObjectBaseClass::AssertValid();
 
 // check the data members not in the base class
 ASSERT_VALID(m_pObject1);
 ASSERT_VALID(m_pObject2);

 // now check the class invariants not checked by the base class
 // be sure to document the invariants
 ASSERT(m_Value != illegalValue);
 ASSERT(m_Object1.GetSize() == m_Size);
 ...
}
24.不正确的使用断言会导致错误。断言是用来揭示错误的,而不是用来纠正运行时刻错误的。
25.如果你的程序是防御性的,别忘了使用断言。如果你使用断言,也别忘了防御性编程。这两种技术最好结合在一起使用。
26.考虑使用_ASSERTE(FALSE)来简化防御性的编程和断言的结合。要想得到更有描述性的断言信息,考虑使用_ASSERTE("Problem description." == 0)。
27.断言不是错误处理的替代品。
28.这种方法让你不用打扰窗口画图就可以在出现窗口后,随时察看导致断言失效的代码。
void CMyview::OnDraw (CDC* pDC) {
 int previousReportMode = _CrtSetReportMode(_CRT_ASSERT,
     _CRTDBG_MODE_DEBUG):
 CMyDoc* pDoc = GetDocument();
 ASSERT_VALID(pDoc);
 ...  // draw the window
 if (previousReportMode != -1)
  _CrtSetReportMode(_CRT_ASSERT,previousReportMode);
}
29.使用GetObjectType函数来断言一个具有有效句柄的GDI对象(察看返回值是否为零),或者某个特定的GDI对象(察看返回值是否是某个特殊值)。eg:
_ASSERT(GetObjectType(hBrush) == OBJ_BRUSH);
但是,要意识到GetObjectType函数可能返回一些让人吃惊的结果。如,下面的断言失效:
HBRUSH hBrush = CreateSolidBrush(RGB(0,0,0));
DeleteObject(hBrush);
_ASSERTE(GetObjectType(hBrush) == OBJ_BRUSH);
因为黑色的刷子是一个备用设备的对象(也就是不能删除),因此 DeleteObject 函数调用就没有作用。
30.MSDN文档声称IsBadCodePtr、IsBadReadPtr、IsBadStringPtr和IsBadWritePtr这几个API函数在接收到坏指针的时候就会在调试版本里导致断言失效。这个说法是错误的,必须把这些函数包装在断言语句中。

三、使用跟踪语句
31.OutputDebugString API函数,MFC AfxOutputDebugString 宏和MFC AfxDumpStack 函数在所有版本中均被编译,但是其他所有的跟踪语句仅在定义了_DEBUG符号时,才能被编译。
32.跟踪语句不能包含程序代码或对程序代码有间接的影响作用。跟踪语句的目的是向程序员提供信息,而不是用户。
33.与断言的区别:
(1)跟踪语句是无条件的。断言是有条件的布尔语句,而跟踪语句却总可以执行。
(2)跟踪语句不直接显示bug。断言用于显示出bug,而跟踪语句用于显示程序执行和变量值。
(3)跟踪语句可以被随便的忽略。默认情况下,断言打断程序的执行,并弹出一个消息框等待用户或程序员的响应;跟踪语句则将信息输出到调试窗口或文件中,于是很容易被程序员忽略。这种特点使得跟踪语句对于整体上程序的检验和程序的警告是非常理想的。一个很好的类比是:断言对编译器错误,跟踪语句对编译器警告。
34.void OutputDebugString(LPCTSTR traceText),这个windows API函数是Windows的一部分,因此它一直是有效的,这使得这个函数适用于在程序启动和结束过程中的跟踪。与此相反,Visual C++的C运行时刻函数库和MFC跟踪语句并不适用跟踪程序启动和结束,因为它们仅在加载了它的DLL之后才有效。如果你只想在调试版本中使用OutputDebugString,可以使用下面的宏来实现:
#ifdef _DEBUG
#define OutputTranceString(text) OutputDebugString(text)
#else
#define OutputTranceString(text) ((void) (0))
35.ANSI C++运行时刻函数库中并没有跟踪语句,但是它有用于跟踪的标准字符模式的输出流,具体有:C语音的stderr流和C++语言的cerr 和 clog流。stderr不需要缓冲区,cerr使用单位缓冲,clog使用完全缓冲。
36.使用MFC Tracer工具来控制MFC本身输出的跟踪语句
37.跟踪语句的有效使用需要一个策略,太多的跟踪消息会降低它们的有效性。如下是两个基本的跟踪语句策略:
 (1) 调试器补充策略:使用跟踪语句补充由交互式调试器提供的信息
 (2) 调试器代替策略:使用跟踪语句代替交互式调试器
38.使用综合诊断跟踪语句能让你查看程序中最引入注意的事件;使用特殊诊断跟踪语句来解决一个特殊的问题;一旦确定的问题已经解决,将用于特殊诊断的跟踪语句删除。
39.在交互式调试器不能解决问题的情况下,如:调试服务器、跨机器调试(如DCOM)、夸程序设计语言调试遇到编程语句、跨进程调试遇到进程、调试线程、远程调试、遇到那些因为海森堡不确定原理而很难调试的程序动作。使用跟踪语句解决问题。
40.跟踪调试技巧
 (1)使用DebugView实用程序
 (2)考虑提供一个重定向输出设置
 (3)处理长字符串
 (4)处理大量的跟踪输出
 (5)产生调试报告
 (6)输出独立行,而且不要忘了新行(newline)字符
 (7)别忘了检查跟踪语句
 注意:每当你的程序中有错误而你想得到更多信息的时候,你应该去查看一下跟踪消息


四、使用异常和返回值
1.使用返回值处理错误需要程序员严格的编程作风,程序员不论是否有这种习惯,这都是非常不希望的。
2.正确的异常处理是C++中的一个常识。异常通过发出错误信号,可以让程序代码和错误处理代码分开,而且不会让程序忽略错误。
3.必须了解使用了哪种错误处理方法,返回值还是抛出异常。如果不知道,那么你的程序肯定有问题。
4.异常是基于每个线程而提出并处理的;异常不能被线程忽略,必须被处理;未处理的异常会使进程结束,而不仅仅是线程结束;异常处理在释放栈时会释放所有的栈对象,因此避免了资源的漏洞;异常处理需要大量的额外操作,使得它并不适于经常运行的代码。详细的说,catch块有一些开销(overhead),但是try块有很少的开销;因此只有在抛出异常的时候才会有很多的异常操作开销;你可以抛出任何类型的异常对象,但不包括整数。
5.返回值可以指示正常和不正常的函数运行,但不能阻止线程的继续运行;返回值很容易被忽略;返回值在典型的情况下是一个整数,通常映射符合于一个预定义的值;返回值能高效的传递和接收;
6.所有的非错误的状态信息都应该使用返回值
7.返回值用于大多数情况下可以随意忽略而不会出现问题的错误
8.在循环中的错误处理必须快速,因为异常的额外开销,所以为了得到更好的性能,使用返回值是一个更好的选择。在这种情况下,如果你真要使用异常,可以创建一个函数来将返回值转化未异常。
9.使用于中间语言模块中的错误。
10.使用Windows API的错误处理机制。用SetLastError设置错误代码,通过GetLastError检测这个错误代码。
11.从C++异常处理的观点看,故障应该被认为是错误。
12.你必须使用/Eha调试器选项来扑获使用C++异常操作机制的操作系统异常。
14.Windows结构异常处理:使用_try,_except,_finally,_leave关键字和RaiseException API函数;由Windows支持,不适于其他操作系统;不处理C++对象的解析;作为硬件异常或操作系统异常的结果抛出,也可作为RaiseException函数结果被抛出。
15.C++异常处理:使用try,throw和catch等关键字;仅被C++语言支持;处理C++对象的解析;可以抛出任何类型的C++对象。异常对象可以从标准的异常基本类派生,也可以从任何类派生,或者它们也可以是内置的类型;作为throw语句的结果被抛出。
16.Visual C++ 使用结构异常处理机制实现C++异常。
17.结构异常处理不能处理对象的解析,因此你应该在C++程序中一直使用C++异常。然而,因为C++异常不能处理硬件和操作系统异常,你的程序需要将结构异常转化为C++异常。
18.为了正确处理硬件和操作系统异常,你可以创建自己的异常类并使用_set_se_translator函数安装一个结构异常向C++异常的转化器。
19.不要扑获那些不能恢复所产生问题的转化后的结构异常。
20.在很少抛出异常的情况下使用异常的代价并不是很大,而且这样做确实可以提高性能。
21.异常策略中最重要的一部分实际上就是有一个策略。不要在事后弥补。
22.异常扑获规则:扑获处理器按顺序提供;如果扑获处理器扑获了同一类型或指向同一类型抛出对象的指针,则应扑获异常。如果扑获处理器扑获了一个公共基类或者指向一个公共基类抛出对象的指针,则应扑获异常;一个省略扑获处理器扑获任何类型的异常,因此它总是放在最后。
23.定义一个异常基类来处理程序代码抛出的异常。
 class CProgramException : public exception {
 public:
 CProgramException (const _exString &_what_arg) :
  exception(_what_arg) {}
};
使用CProgramException类使得异常处理更加简单,因为可以通过处理这种基类扑获所有的程序中的异常。如果需要的话,也可以使用额外的成员数据全面描述特定的问题。
24.使用auto_ptr或者一个类似的指针类通过限制局部变量的动态分配来自动释放资源:
void LeakFreeFunction (int arg) {
 auto_ptr<CMyObject> pObject(new CMyObject(arg));
 ... // do something that throws an exception
 // can still call member functions as normal
 pObject->MemberFunction();
 // no need to delete pObject
}
注意:auto_ptr仅在使用delete释放资源时使用。
25.使用异常处理更简单,更可靠,更有效,可以创建更健壮的代码。然而,你应该只在意外的情况下使用异常处理。如果你认为一个指针应该时空值,这种条件下就直接在代码中检查这个值,而不要使用异常。
26.非MFC的C++异常应该通过引用来扑获。使用引用扑获异常不需要删除异常对象(因为使用引用扑获的异常会在栈中传送),而且它保留了多态性(因此你扑获的异常对象正是你抛出的异常对象)。使用指针扑获异常需要你删除对象,而使用值扑获对象会导致对象的“分片”(slicing),也就是说,将派生的异常对象转化为扑获的数据类型。
27.MFC异常应该通过指针来扑获。因为它们通常从堆中分配,当你处理完异常之后,你需要调用Delete成员函数:
...
catch (CFileException *e) {
 // handle file exception
 ...
 e->Delete(); // required to prevent a memory leak
}
因此,你不可以使用省略扑获处理器扑获MFC异常,因为者会导致一个内存泄漏。你必须使用Delete成员函数删除MFC异常,而不要用delete操作符,因为一些MFC异常作为静态对象创建。
28.一旦扑获了异常,你可以通过执行下列典型动作的一些组合来处理它:
 (1) 什么也不要做。
 (2) 修改这个问题并重试代码。
 (3) 修改这个问题但不要重试代码。
 (4) 如果用户需要的话,向用户显示错误信息。
 (5) 如果出现的问题不是程序错误的话,输出一个诊断的跟踪消息。
 (6) 如果出现的问题是程序错误,输出一个断言。
 (7) 在日志文件中记录这个问题。
 (8) 如果异常是不可恢复的,停止进程的运行。
 (9) 整理已分配的资源。
 (10) 重新抛出这个异常,使得高级函数也能处理这个异常,特别是在当前函数不能完全解决的情况下。你可以重新抛出同一个异常对象,或抛出一个新的异常对象。
29.和/EHa相对的是同步异常(/EHs),而不是/GX。/GX实际上是/EHsc的简化形式。/GX表示编译器应该假设extern "C" 的函数不抛出C++异常,而/EHs则抛出。
30.为用户和调用环境记录异常。通常,异常对象类型用于向调用环境通知出现的问题,而问题的描述字符串用于向用户通知。
31.Visual C++的默认情况下,new和malloc对于错误不会抛出异常,但你可以通过使用_set_new_handler安装一个处理器,让new针对错误抛出异常。你也可以让malloc通过调用_set_new_mode使用同一处理器。
#include <new.h>

class bad_alloc : public exception {
public:
 bad_alloc(const __exString& what_arg) : exception (what_arg) {}
};

int NewHandler (size_t size) {
 throw bad_alloc("Operator new couldn't allocate memory");
 return 0;
}

int APIENTRY WinMain (HINSTANCE hInstance,HINSTANCE hPrevInstance,
        LPSTR lpCmdLine,int nCmdShow) {
 _set_new_handler(NewHandler);
 _set_new_mode(1);   // use NewHandler for malloc as well
 ...
}

32.如果已存在的代码中没有设定new返回空值,那么你应该始终让new出错时抛出一个异常。
33.浮点数和整数不一样,在默认情况下它被零除不会出现异常,但是会出现一个非常奇怪的值"1.#INFO" (它表示这个值并不是一个数字)。要让检测浮点数问题更简单一些,你应该用如下的代码让浮点数错误抛出异常。
#include <float.h>
int cw=controlfp(0,0);
cw &= ~(EM_OVERFLOW | EM_UNDERFLOW | EM_INEXACT | EM_ZERODIVIDE | EM_DENORMAL | EM_INVALID);
_controlfp(cw, MCW_EM);
浮点数异常处理器必须调用_clearfp作为它的第一条指令来清空浮点数异常。

你可能感兴趣的:(C++)