C程序设计编码规范(中文).doc_第1页
C程序设计编码规范(中文).doc_第2页
C程序设计编码规范(中文).doc_第3页
C程序设计编码规范(中文).doc_第4页
C程序设计编码规范(中文).doc_第5页
已阅读5页,还剩28页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1 编程风格指南编程风格指南 Dave Parker, 7/25/00 概述概述 本文列出了开发工作中C和C+程序设计编码的常用规范,指出C+的可用与应避免的特性,以及之所以这 么做的基本原则。本文并非C/C+语法说明,而是着重于我们认为重要之处。阅读者起码应具备C+基本知 识。 Contents目录目录 1.总体目标总体目标 . 3 2.类类 . 3 1.1类与结构4 1.2公共,私有,和保护成员 4 1.3DATA MEMBERS数据成员. 4 1.4虚函数 . 5 1.5CONSTRUCTORS构造函数 5 1.6DESTRUCTORS析构函数 6 1.7NEW AND DELETE动态内存分配:. 7 1.8运算符 . 7 1.9继承 . 7 1.9.1接口继承与实现. 8 1.1.2继承和包容. 9 1.1.3多重继承. 10 3.OTHER C+ FEATURES其他其他C+特性特性. 10 3.1CONSTANTS AND ENUMERATIONS常量与枚举.10 1.2REFERENCES参考. 11 1.3CONST PARAMETERS AND FUNCTIONS常数参数与函数11 1.4DEFAULT ARGUMENTS缺省参数 12 1.5FUNCTION OVERLOADING函数重载. 12 1.6OPERATOR OVERLOADING 运算符重载.13 4.COMMON C/C+ ISSUES C/C+共用主题共用主题.13 1.1GLOBAL VARIABLES全局变量. 14 1.2MACROS AND INLINE FUNCTIONS 宏和内联函数.14 1.3OPTIMIZATION优化. 14 1.4WARNINGS 警告. 15 1.5PRIVATE DATA AND FUNCTIONS 私有的数据和函数.15 1.6TYPEDEFS. 15 1.7BASIC DATA TYPES 基本数据类型 15 1.8POINTERS指针 17 1.9SWITCH STATEMENTS 开关语句.17 1.10ASSERTS 断言. 17 2 1.11ERRORS AND EXCEPTIONS 错误和异常 18 5.FORMATTING CONVENTIONS 格式惯例格式惯例19 5.1NAMING CONVENTIONS 命名规则 19 1.2FUNCTION PROTOTYPES 函数原型. 20 1.3VARIABLE DECLARATIONS 变量声明. 20 1.4CLASS DECLARATIONS 类声明. 21 1.5COMMENTS 注释 21 1.5.1File Headers and Section Separators 文件头和区块分隔符21 1.1.2Function Headers 函数头 22 1.1.3In-Code Comments 代码中的注释23 1.1.4Attention Markers 注意标记 23 1.6MISC. FORMATTING CONVENTIONS 杂项24 1.7SOURCE FILE ORGANIZATION源文件组织25 1.7.1Public Interface Files公共接口文件25 1.1.2Private Interface Files 私有接口文件.26 1.1.3Implementation Files 实现文件. 26 1.1.4Base Filenames 基本文件命名 26 6.INTERFACES TO DLLS DLL的接口的接口. 27 6.1C FUNCTIONS AND GLOBAL VARIABLES C函数与全局变量.27 6.2COMMON C/C+ PUBLIC HEADER FILES 通用的C/C+公共头文件.27 1.3LIGHTWEIGHT COM OBJECTS AND ISIMPLEUNKNOWN 轻量级COM对象和ISIMPLEUNKNOWN.28 7.附录附录A:基本匈牙利命名参考:基本匈牙利命名参考. 31 7.1匈牙利命名方法 31 7.2标准前缀32 7.3标准限定词 32 3 1. 总体目标总体目标 C+是一种复杂的程序语言,提供了多种程序途径,试图对每种特性加以利用,会导致混乱、低效、或维护 问题。每一个开发者必须对他要用到的特性非常熟悉,甚至成为专家,而避免使用其它特性以使小组内的 风格一致。我们对C+特性的使用相当保守,很多时候我们仅仅在使用我们熟悉的C。 决定使用哪些特性,有一些基本目标可遵循,如下所列。如果使用过程中出现疑惑,请考虑一下这些基本 的目标。有时目标之间会发生冲突,我们可以折衷一下,也可以依据自己的经验来选择(我们的或其他C+ 开发小组的经验)。 1.简洁Simplicity. 如果不能确定,那么请选择更简洁的方式,因为程序中的BUG很多时候不是代码有 误,而是太复杂所致。 2.清楚Clarity. 代码应清晰,要使别人能理解你的程序。 3.高效Efficiency. 运行速度和程序大小都很重要,使用C+并不意味着代码将冗长而缓慢。有许多方法可 以使程序与常用的C方法比,一样快甚至更快。通常情况下,大多数人会在权衡速度和大小时优先考虑 速度。请记住:20%的代码占用了80%的运行时间。在很多时候,我们更应关心程序占用内存的大小。 4.适当Appropriateness.使用适当的语法结构做你想做的抽象或操作,不要滥用语言。不要使用看来相当 古怪的结构,这会迷惑住认为你水平很高的朋友们。 5.从C到C+自然过渡Natural transition from C to C+. 我们都从C程序起步,其他看我们的代码的人也 还是C程序员,在可能的情况下,避免使用会使C程序员误解的C+结构。 6.及早捕获错误Catch Errors Early.寄望于编译器捕捉每一个错误过于理想化,用测试代码(如 ASSERTS)捕获错误不失为一种好方法。在程序中给出有利于编译器捕捉错误的定义。 7.快速BUILDS Fast builds. 8.一致性Consistency. 程序员或编程小组在编程风格上不能太独特,因为其他人需要阅读并理解这些代 码。每个程序员都应在编程中为保持一致风格而作出努力,以便其他人更容易阅读或调试这些代码。 2. 类类 C+的类是将代码和数据封装到一个单元的最好途径,提供了一个面向对象的实现的好范例,以及其他特 性,如:灵活的存取控件 ,方便安全的多态性,通过继承实现代码复用。 类是C内建类型的扩展,可允许您定义自己的类型及在其上的操作。在极端情况下,程序中的每一段数据都 可作为类的一个实例。然而,在实际运用中我们却不能这么做。除非有充分的理由,如面向对象或多态性 要求,我们才会使用类。许多有经验的程序员都知道,太多的使用类会使系统复杂并且低效。尽管这不是 设计精良的类导致的失误,但复杂的类层次结构不可避免地造成不必要的复杂性,过度的抽象原理极易造 成程序低效。 通常情况下,我们应避免在堆栈中直接分配类或通过值传递类,因为这样会使构造函数和析构函数调用很 多次。大多数类应通过NEW来动态分配内存,用DELETE释放内存空间,通过指针传递类。另外,我们不 能将有构造函数的类的实例定义为全局变量,这会导致C RUNTIME的许多代码被联接进来,并且由于要在 启动时构造类的实例,会造成致命的性能上的失误。相对复杂的对象而不是简单的数据类型,可放在堆 里。要判断何时使用类,没有一个更好的简捷的方案时,才使用类。 摘要: 用类实现面向对象原理的封装性 用类实现多态性 避免在堆栈中直接分配类实例,用NEW和DELETE动态分配内存,通过指针传递。这意味着不能将类 当作简单的数据类型。 不能将有构造函数的类的实例定义为全局变量 不是所有东西都可以当作类,在需要的时候使用。 4 2.1 类与结构类与结构 在C+里,结构也有与类相同的成员函数和操作。事实上,类与结构的区别在于:结构的所有成员缺省是公 共存取,而类是私有存取。然而,这不是我们判断使用类或结构的依据。直觉上,我们在也仅仅只在有成 员函数存在的时候才使用类。 摘要: 仅在有成员函数存在的情况下使用类代替结构 2.2 公共,私有,和保护成员公共,私有,和保护成员 如上所述,结构的所有成员缺省是公共存取,而类是私有存取。然而,对结构我们一般使用缺省情况。在 类中,我们将所有成员(包括数据和操作)明确的定义为公共、保护或私有,并在代码中放在不同的部 分。例如: class Foo public: Foo(); Foo(); void Hey(int I); void Ack(); protected: int m_iValue; private: int m_iStuff; void LocalHelperSub(); ; 摘要: Declare all class members explicitly as public, protected, or private, in groups in that order.定义所有 类成员时,应明确标注公共、保护、私有。 2.3 Data Members数据成员数据成员 数据成员应使用m_name的命名规则,在name用作为通常的匈牙利本地变量命名时。这会使成员函数易读 (在成员与本地数据之间不会冲突),对参数与成员也可以使用相同的匈牙利命名法。见下例。 数据成员一般不定义为公共的,因为这会毁坏类的抽象性的作用。要有效输出数据成员,定义内联的GET 与SET成员函数。这会被优化成与公用数据成员一样的代码。例如: 5 class Counter public: int CItems() const return m_cItems; void SetCItems(int cItems) m_cItems = cItems; private: int m_cItems; ; 摘要:摘要 数据成员使用m_name的命名规则 不要定义公共数据成员。如果考虑性能的话使用内联的存取函数。 2.4 虚函数虚函数 虚函数允许派生类,重载基类的一个函数,通过提供它们自己的实现,使最末端的类方法通过一个对象指 针被调用,即使这个指针被定义为指向基类的指针。这是多态性通常的实现方法。例如,所有COM接口的 方法都是虚拟的,因为你总要通过标准接口实现多态性。 不象简单的成员函数,虚函数会带来一些性能代价,因为它需要通过VTABLE来调用。如果一个类有至少 一个虚函数,它的大小会比所有数据成员大小之和多出4字节,用以存放VTABLE的指针。而其他虚函数只 会使VTABLE中增加一个入口,VTABLE是静态的,每个类有一个而不是每个对象。主要考虑类有没有虚 函数。除了内存上的开销,通过间接指针调用也会带来开销。在32位代码中很快很紧凑,但仍然会影响速 度与大小。最糟的是虚函数不可以内联,即使最微不足道的工作也会造成函数调用。 因为有开销,除非需要,否则你不要在类中使用虚函数。然而,要确信有价值时才使用。尤其是如果你用 的基类需要一个析构函数,那么它应被定义成虚拟的,以允许派生类析构添加的成员。如果析构函数不是 虚拟的,那么在使用多态性的地方(这时对象指针被定义成指向基类的指针),总是调用基类的析构函 数,即使一个派生类对象添加了成员变量并在它自己的析构函数中释放。只有当基类的析构函数是虚函数 时,派生类的析构函数才会被调用。这一概念也适用于你往你的类中增加其它类型的方法的时候。实际 上,用到多态性的时候基类的方法大都是这样。这一点在下面关于继承这一节有更为详细的论述。 尽管虚函数比正常的成员函数有性能差距,但它们是实现多态性的最有效的方法。(还不包括面向对象包 装的好处) 摘要: 使用虚函数实现多态性 虚函数会带来开销,所以要小心使用 基类的析构函数应为虚函数,如果考虑多态性的话。 2.5 Constructors构造函数构造函数 啊,构造函数,所有C+新手的恶梦。尽量少用构造函数的原因有一个-C程序员不熟悉它们并且可能被迷 惑。另一个原因是众所周知的在调用函数(除非是内联的)时的性能开销。 然而,使用构造函数可以消除未初始化数据的危险,并使程序易读(假如你熟悉它的话)。与构造函数相 对应,使用析构函数以防止内存泄漏或其他资源管理问题。 幸运的是,这主要是在当类被定义在堆栈上或通过值传递时,而这两种情况都是我们要避免的。我们大多 数的类应该是通过指针传递的动态内存对象。在这种情况下,构造函数对创建这些动态对象的函数而言, 只是一个帮助函数。使用构造函数的好处是保证每一个对象都用正确的值初始化(如果你希望初始化数据 成员),但是要防止由于多余的初始化带来的潜在的性能问题。简单地为每一个成员变量赋值是对的。非 常简单的构造函数可以是内联的。 最重要的是,构造函数永远不能失败,因为没有一个错误处理机制使调用者处理构造函数失败的情况。所 有可能的失败(比如说内存分配)应该放到独立的初始化成员函数中(比如说叫FINIT)。在这种情况下, 6 通常把对象的创建包在一个函数里(可以是全局函数或类的成员函数),该函数调用NEW和FINIT,并返 回FINIT的结果。 class Foo public: Foo(int cLines) m_hwnd = NULL; m_cLines = cLines virtual Foo(); BOOL FInit(); void DoSomething(); private: HWND m_hwnd; int m_cLines; ; BOOL FCreateFoo(int cLines, Foo *ppfoo) if (*ppfoo = new Foo(cLines) = NULL) return FALSE; if (*ppFoo-FInit() return TRUE; delete *ppFoo; *ppFoo = NULL; return FALSE; BOOL Foo:FInit() m_hwnd = CreateWindow(.); return (m_hwnd != NULL); 摘要: 不要在构造函数中做太多工作。 当使用构造函数时,请确认所有数据成员均初始化过 非常简单的构造函数可以是内联的。 构造函数不能失败,把内存分配和其它可能的失败放到FINIT方法中。 考虑做一个创建函数,包装NEW和FINIT操作。 2.6 Destructors析构函数析构函数 如果类有资源要释放,析构函数是一种很便捷的方法。对我们来说,当通过DELETE释放一个对象时,在这 里释放该对象占用的所有资源。调用堆栈上分配的类的析构函数很棘手,但我们可以不使用堆栈类而避 免。 析构函数要很小心的毁掉对象,不管它是如何创建地,如何使用地。如果你实现了一个在析构函数前释放 部分资源的方法,一定要复原这些成员(比如把指针置为NULL),这样析构函数就不会试图再次释放它们 了。析构函数不需要复原任何成员,因为对象在析构后就不能再使用了。 与构造函数一样,析构函数也不能失败。上边曾经提到过,基类的析构函数应定义为虚函数以实现多态 性。 上例的析构函数应定义为: 7 Foo:Foo() if (m_hwnd != NULL) DestroyWindow(m_hwnd); 摘要: 用析构函数集中释放资源。 如果资源在对象析构前被释放,要复原相应的成员(比如把指针置为NULL),这样析构函数就不会试 图再次释放它们了。 析构函数没有返回值,不能失败。 析构函数应定义为虚函数,如果多态性必须使用的话。 2.7 New and Delete动态内存分配:动态内存分配: C+的NEW和DELETE可使程序实现类的动态内存分配(代替低层的ANSI C中的标准库函数malloc和 free),这样构造函数和析构函数会被恰当地调用。我们实现了全局的NEW和DELETE,NEW分配内存并 调用构造函数,DELETE调用析构函数。 注意:应该有一种机制检测分配内存失败。对NEW来说,调用的代码负责检查。在分配失败时,我们的内 存管理器返回0,NEW也就返回0(这时构造函数不被调用)。因此调用NEW的代码要检查返回是否为0, 如上例所示。 你不应该定义其它的NEW和DELETE(比如说类一级的)运算符,而要用全局的以避免混合内存模式。 摘要: 使用new and delete动态分配或释放类的内存。 我们在OFFICE底层的内存管理器中实现了全局的NEW和DELETE。 检查NEW的返回值 避免定义其它的NEW和DELETE运算符(使用OFFICE定义的) 2.8 运算符运算符 理想情况下,你不需要为类定义一个运算符。如果你要做的话,多半是运算符“=”,但是除非你真的需要 这一能力,不要定义运算符。可能性小一些的有运算符“=”和“!=”,同样只在你真正需要时再定义。 不要定义标准运算符比如“+” 运算符做与标准语义不符的操作。甚至也不要定义与标准语义类似的操 作,比如,用“+” 运算符做合并或串联操作。只用运算符做高效的操作。 摘要: 理想情况下,类不需要定义运算符。 定义有意义的运算符,比如=,=,和!= 。只在你真正需要的时候定义。 不要定义标准运算符做与标准语义不符的操作。 只用运算符做高效的操作。 有疑问时,做一个成员函数,这样操作是明确的。 2.9 继承继承 继承是一种很有用的技术,但经常被人们滥用,因为它有可能导致复杂的类继承,难懂也难改。下面描述 了继承的几种不同用法,把它们和其它技术比较并提供了何时使用的概述。 继承在多数时候很好用,并不意味着在哪儿都用。过深或过宽的继承树难以理解,难以浏览,难以维护。 8 摘要: 不要仅仅因为能用继承就用。要保守地使用继承。 2.9.1 接口继承与实现接口继承与实现 许多人认为继承是共享代码的一种方法。然而,使用继承最好的方法是只通过继承接口实现多态性。经典 的例子是用抽象类(所有的方法都是纯虚函数)作为接口类,一个或多个实现类继承它,并以不同的方式 实现。OLE COM就是一例。在C+中,COM接口是抽象类,别的实现类继承接口并实现接口的所有方法。 在这种情况下,继承是使对象表现得与它所支持的接口完全一致(在VTABLE中有一样的方法、一样的顺 序、一样的参数类型和一样的返回值)的一种简单的方式。这一点是由每一个实现类都继承自同一个接口 类,而接口类只需在头文件中定义一次。注意:当类实现继承来的纯虚方法时,它必须再次声明,因为从 语言的角度看,这仍然是“重载”基类的方法。例如: / The interface base class provides interface only class FooInterface public: virtual void DoThis() = 0;/ pure virtual virtual void DoThat(int i) = 0; / pure virtual ; / The implementation class implements the FooInterface interface class FooImplementation: public FooInterface public: virtual void DoThis(); virtual void DoThat(); void FooImplementation:DoThis() . . 上面是整个基类只是接口的一个例子。然而,接口的继承也可发生在单个成员函数级别上,这时基类里也 包含一些方法的实现。这种情况下,有纯虚拟的成员函数。下面的DrawObj:Draw 方法就是一个例子。 上例没有用继承来共享代码。而继承也可用于这一目的,在基类中提供的实现,在派生类中也可使用。有 两种有趣的情况:如果在基类中定义了一个方法的实现,而它既可直接用,又可重载,那么基类中的方法 可看作是接口的缺省实现。这时方法应被定义为虚拟的,使用任何一个重载了该方法的类都能得到正确的 结果。另一种情况是,如果基类中定义了一个不可重载的方法(因为它对基类的私有数据作了标准的操 作),那么基类就定义了一个接口并需要实现。在这种情况下,方法不能是虚拟的。反过来说,当继承一 个类时,不要重载任何非虚函数,因为这会带来维护问题。 通常,上面两种继承的实现方式可以在单个类中一起使用。关键在于决定基类提供每个方法的目的,是单 纯的接口、接口加缺省实现还是需要实现的接口。例如: 9 / A base class for drawing objects class DrawObj public: virtual void Draw() = 0; / interface only virtual BOOL FHitTest(POINT pt); / default implementation void GetBounds(RECT *pr); / required implementation private: Rect m_rBounds;/ bounding rectangle ; BOOL DrawObj:FHitTest() return PtInRect(pt, m_rBounds); void DrawObj:GetBounds(RECT *pr) *pr = m_rBounds; 在这个例子里,DRAW方法是纯需的,因为它仅仅指定了一个多态应用的接口。派生类必须定义DRAW方 法。FHitTest方法定义了接口并提供了缺省实现,派生类可以不定义该方法,也可以重载该方法做特殊的 点击测试。GetBounds方法是需要实现的一个例子。基类要求所有对象都以同样的方式定义“边界”,她 不允许任何人改变。在这种情况下,该成员不需要是虚拟的(不是清晰和效率的原因),因为总是会用基 类的实现。 摘要: 接口继承可用于确保接口的一致性。 接口类可从只有纯虚方法的接口类继承接口。 当用继承从基类共享代码的时候 使用纯虚函数只提供接口。 使用虚函数提供接口和缺省实现。 使用非虚函数提供接口和需要的实现。 2.9.2 继承和包容继承和包容 滥用继承的通常情况是将继承作为在类似对象间共享代码的方式,有多种方式可共享代码,比较简单的技 术是包容和委托(一个类包含另一个类,并使用其中的功能),在传统的结构化编程中,我们对此很熟悉 并使用得很好。 用继承而不是包容的主要原因是实现多态性(通过虚函数)。如果要实现多态性,继承则很适合;如果会 使某个派生类有超过一个的基类,则包容更适合。例如,如果你想完成一个可滚动的窗口,并且已经有了 一个滚动条类,你会注意到一个窗口会有两个滚动条:横向和纵向,即使你在起初并未设计到。所以,该 窗口应该包含一个滚动条,而不是从滚动条继承。 当你决定从别的类继承时,通常将原来的类分为基类与派生类,并且只从基类继承。要把真正需要共享的 那部分代码分出来。例如:你有一个矩形绘图对象,你希望得到一个椭圆对象,并且希望实现多态性,一 个椭圆并不需要两个矩形,所以你决定让椭圆继承矩形。但也许你真正会将矩形类分为绘图对象类和一个 单独派生的矩形类,然后让椭圆继承绘图对象类,而不是矩形。这样可以做到单独修改矩形对象,即使现 在还不需要。上一段的例子中,绘图对象基类也许是由纯虚函数、虚函数、非虚方法的组合。 注意,包容迫使你使用被包含对象的公有接口,然而继承还允许你使用被保护的成员。包容的封装性比继 承更好。实际上,继承打破了封装性,因为它创建了对基类实现的依赖。尤其对重载的函数,对基类的修 改不会对所有的派生类生效。 10 摘要: 注意继承与包容的区别,在不能确定时,使用包容 2.9.3 多重继承多重继承 我们应避免多重继承,多重继承会带来一系列问题,包括:名字的冲突,某些操作的效率问题,维护问 题。 当你创建一个大而复杂的继承层次时(避免上面提到的),你会发现你在用多重继承从两个不同的地方共 享代码。这是多重继承最危险的地方,它导致了最微妙的依赖关系。多重继承也有其它形式。最安全的是 只从接口作多重继承,这样没有来自基类的任何代码。但即使这样也有名字冲突等问题,所以我们根本就 不用它。 每当你想用多重继承的时候,你也许过度使用了继承,某些情况下,你应当转而使用包容。一旦你继承了 一个类,你要用包容来得到别的特性。注意,你可以包容某个类任意次,而不带来任何问题。 摘要: 不要使用多重继承 Given only single inheritance, inheritance is a 搒ilver bullet” which you have only one of, so use it sparingly and judiciously. 3. Other C+ Features其他其他C+特性特性 下面谈谈C+中不直接和类有关的新特性。 3.1 Constants and Enumerations常量与枚举常量与枚举 C+向C中添加了真常量的概念。在C里,你可以使用#define或声明一个“const”全局变量。然 而,#define不是类型安全的,const变量占用实际内存,并且没有优化。例如: / C alternatives: #define dxMin 0/ not type safe const DX dxMin = 0;/ just a real global variable 在C+里,const语法声明了一个真正的特定类型的常量,编译器会做类型检查,然后用实际的值代替,并 做优化。甚至调试器也知道这个符号。例如: / C+ solution: const DX dxMin = 0;/ type safe, optimized, and debug symbol 所以,真正的C+常量比传统的C+ #define要好。注意,它们不能用在C/C+共享的文件中,因为C编译 器会给它们分配内存。 C+也使C中枚举类型变得安全了。在C+中,枚举定义了一种类型,并声明了该类型的常量。你可以向平 常一样使用这种类型,比如用作函数的参数,编译器会强迫你传入一个该类型的枚举值(你也可以在需要 的时候做类型转换)。枚举类型也可用在类中,以限制其范围。例如: 11 class Foo public: enum GMODE gmNo = 0, gmYes = 1, gmMaybe = -1 ; void InsertGNode(GMODE gm); ; 摘要: 在C+中使用const 或 enum 代替 #define 定义常量 3.2 References参考参考 C+增加了表达对象引用的能力,这主要应用在把类作为参数传递,避免了调用拷贝构造函数的开销。这是 很有价值的,但更直接的做法是通过指针传递类,正象我们在C中所用的。对为一个用惯了C的人来说,看 到传引用就象看到传值,你会猜测构造函数是否被调用。此外,何时用引用,就好象你有一个对象的本地 拷贝,能够方便地使用,但是事实上你仅仅有对象指针(编译器做了转换),所有的操作都是间接的。我 们应该用指针来明确表明这种间接使用。用“*”或“-”代替“.”并不费事,却是程序更加清晰。引用对 指针的真正优势是那他们被保证被初始化(他们不会是NULL的或指向垃圾)。对我们来说,这优势没有多 少价值。 另外,注意当你通过指针传递对象的时候,用“CONST”标记正式参数为只读(参看下面的“ 常量”部 分)。这又与引用有关,因为一些C+程序员的惯例是:对只读对象传引用,对其他对象传指针(当然要达 到这中安全性,你仍然需要把引用声明为常量,因为C+允许你通过引用改变参数的值)。这一惯例非常合 理,但是对C程序员而言,因为看起来很奇怪而感到困惑。所以我们将对每一个对象传指针,并应用 “CONSY”获得安全性。 引用还有其他奇异的用法,比如从运算符函数中返回一个引用值,有时在你定义该运算符时是非常必要 的。但是既然我们不打算使用许多运算符函数(因为我们不使用基于堆栈的类),在大多数情况下我们将 能避免引用。 摘要: 避免引用,对大对象传指针。 3.3 Const Parameters and Functions常数参数与函数常数参数与函数 正如上面提到的,你应该用CONST把正式参数标记为只读。这允许编译程序帮你查错,并在某种意义充当 你函数的文档,在某些情况下还能使编译程序产生更好的代码。例如: /* Copy the contents of fooSrc into fooDst. */ void CopyFoo(const FOO *fooSrc, FOO *fooDst); 你也能声明非指针的正式参数作为常量(包括指针参数的实际指针部分,和该指针指向的对象,在这种情 况下“const”一词将出现两次),但这样不太好,会使函数原型难以阅读,所以它是可选的。这将确保你 不会把这个参数作为局部变量使用并改变它的值。当然,有时函数这样做的目的是避免声明和建立局部变 量,在这种情况下,你就不能应用常量 (并不是说这不是好的程序设计风格,但是我们不会不准它)。另 一方面,如果你不打算在函数里改变参数的值,把它声明为常量可以使编译程序产生更好的代码。注意, 这时对调用程序而言,起不到文档的作用。例如: /* Copy cb bytes of the contents of fooSrc into fooDst. In addition to not changing what fooSrc points at, my implementation promises not to change the values of any of the local parameters within the function (like you care.). */ void CopyFooCb(const FOO *const fooSrc, FOO *const fooDst, const int cb); 除了声明参数常量之外,你也能声明成员函数为常量,表明该函数不修改对象。更进一步,这允许编译程 序做可能的优化工作,并充当文档。例如: 12 class Counter public: int CItems() const return m_cItems; void SetCItems(int cItems) m_cItems = cItems; private: int m_cItems; ; 摘要: 用CONST标记只读指针参数(指针指向的对象,而不是指针本身) 用CONST标记不改变对象值的成员函数 仅当你关心可能的性能改善时,才用CONST标记参数 3.4 Default Arguments缺省参数缺省参数 缺省参数是一个非常酷的特性。它是这样一种方式:为函数增加一个仅仅某些调用才要传的参数,以致其 他情况变得简单。很不幸,没有效率上的提高,实际上编译程序向你隐藏了要做的工作。如果你有个函数 需要一个必备参数和四个可选的参数,那么每次调用这个函数都将传递所有五个参数,在每种情况下你所 用的代码和时间是相等的。此外,你甚至不能通过缺省参数达到这个目的:尝试一些新代码而不会影响原 有的调用。因为你仍然不得不查找所有原有的调用并重新Build那些文件(如果你在加了一个缺省参数后做 增量Build,会打乱原有的调用)。最后,缺省参数和重载(我们也应避免)很容易混淆。 然而有些时候,某一个参数对调用是完全无关(例如,另一个参数的值已告诉你所有信息)。注意,这和 缺省参数不一样,因为实际上这参数根本没有缺省值。在这些情况下,最好定义一个值为0的无类型的常量 “NA”,表示“没有使用”,把它作为实参传递。这比传递NULL指针或布尔值FALSE更好,因为它清楚 地表明这个值根本无关紧要。例如: #define NA 0/ universal “not applicable“ parameter value /* If fShow then show the object, else hide it. If showing then redraw it only if fRedraw. void Show(BOOL fShow, BOOL fRedraw); void Ack() Show(TRUE, TRUE); . Show(FALSE, NA); 摘要: 不要使用缺省参数 在调用时,为不使用的参数传递“NA”(定义为0) 3.5 Function Overloading函数重载函数重载 函数重载仅仅是懒惰的命名方案。它很象多态性的一种形式,但不要把它和真正的多态性混为一谈,因为 在编译时已做了所有决定。它仅仅是为了简化函数名并在不同场合重复使用。这样一种懒惰的命名方案得 不偿失(试着决定哪个函数被调用,改变偶然的失错等等)并且在某些情况下也干扰匈牙利命名法的正常 使用。最后,函数重载和强制类型转换合在一起,会使人十分困惑。 13 摘要: 不要重载函数 3.6 Operator Overloading 运算符重载运算符重载 运算符主要用在类里。在前一部分已讨论过。运算符也能在全局范围被重载。例如,你可以定义当左边是 Foo而右边是Bar时,运算符+做什么。和在类里使用不一样,现在允许你完全控制左边的操作数。无论如 何,同样的问题依然存在并且更为严重(由于范围更大)。功能被隐藏(可能带来效率问题),并且会导 致混乱,所以我们将避免使用。 摘要: 不要使用运算符重载,尤其是在全局范围 4. Common C/C+ IssuesC/C+共用主共用主题题 下面讨论标致C的特性。 #ifdefs 首先,每个人都应当努力尽量少用“#ifdef”。含有许多 #ifdef 程序真的很难读。我们总可以这样做:引入 正确的抽象,或隔离 #ifdef 以减小头文件,或者在特定的头文件中加入正确的定义,以致于在主程序中根 本没必要用 #ifdef 。支持 #ifdef 的主要论点是可使代码大小最小。然而,每个人都该知道,优化程序在简化 含有常数的句子时做得最好。例如: / Wrong: #ifdef MAC if (x = 3 | Foo() | FSomeMacMode() #else if (x = 3 | Foo() #endif / Right: / In a header file for each non-Mac platform, there is #define FSomeMacMode() FALSE / Then the using code can just be if (x = 3 | Foo() | FSomeMacMode() 在本例中,编译程序完全能在编译时去掉FALSE值。此外,如果整个“if”的值为FALSE,那么编译程序在 编译时会去掉整个“if”子句。 如果必须用 #ifdef ,我们宁愿用 #if 来代替,因为它比较短,还允许逻辑操作,比如在: int Foo() int x = 3 #if MAC #endif return x; 注意,当FALSE时,象DEBUG这样的标志仍然没有定义,但是在这里编译程序做了正确的工作(把它当作 被定义为0)。保持标志没定义意味着 #ifdef 也行,假如这被意外使用的话。 这例子还显示,当有嵌套的 #if 时要适当地缩进,这样容易阅读。当然,这样也行。C编译程序已接受这好 几年了。 14 除标准标识符由我们的Build进程定义以外,我们也将使用标识符UNUSED和LATER ,它们从未定义,以分 别标识现在不用了但因为一些原因保留下来的代码,和现在没有但最终会激活的代码。 摘要: 尽量少用“#ifdef”,通过好的抽象,更好地划分文件,或者在头文件中定义合适的常量和宏 #if 比 #ifdef 好 缩进 #ifdef ,以增加代码嵌套时的可读性 用#if UNUSED 标识现在不用了但因为一些原因保留下来的代码 用#if LATER 标识现在没有但最终会激活的代码 4.1 Global Variables全局变量全局变量 全局变量是通常坏的程序设计风格。具体说,当它们用于保存状态时,常会带来麻烦,因为一不注意它们 就不同步了。每个人都从他们过去的体验中知道了这点,因而没必要做血淋淋的详述。除了这些问题之 外,全局变量使同一进程内DLL的多重使用、重进入、以及多线程非常难以完成。在Office项目里我们要当 心这些。 由于 DLL/进程/线程问题(相同 DLL的所有实例在同一进程,所有线程共享同一个全局变量),多数情况 下,要把全局变量限制在同一实例的数据结构里。在Office项目里,这个结构是 MSOINST ,它由每次调用 MsoFInitOffice 分配并返回。 当你使用全局变量 (比如确实要在所有进程间共享的数据),要在匈牙利类型前加前缀以使该全局变量易 读。 摘要: 尽量少用全局变量 在全局变量的匈牙利类型前加前缀 4.2 Macros and Inline Functions 宏和内联函数宏和内联函数 在C里类似于函数或过程的宏在C+中通常能更好地用内联函数表示,因为这样使它类型安全,并避免计算 宏的多重参数带来的问题。例如: /Wrong #define XOutset(x) (x) + 1) #define XMin(x1, x2) (x1) Release() = 1); / release and verify ref count 摘要: 用Assert做只在调试时做的检查,用AssertDo 做总是执行但只在调试时检查的工作。 4.11Errors and Exceptions 错误和异常错误和异常 有两种基本方式处理代码中意外的错误(比如内存分配失败)。一是返回错误代码并做检查(必需依次返 回给调用者),另一种是抓住和掷出异常。在某些情况下异常很有用并且更有效率,但是它们也更难理解 并会导致无法预测的结果。整个问题的核心在于你看到异常处理的代码就能说出它在做什么。我们会在一 些地方使用异常(现在还不清楚它将是C+异常还是我们创建的别的东西),但一般情况下将使用普通的返 回码,以使代码更直接。无论如何,不可能把异常掷出DLL边界,所以所有DLL导出的API或方法,在失败 时必须返回错误代码。 当在连续的操作中有许多步骤可能失败时,最好用GOTO把所有的出错情况转到公用的错误处理程序(这是 一种在数据一致初始化的基础上恢复的方式)。这大概是C/C+中用GOTO的最好地方。例如: BOOL fRet = FALSE; FOO *pfoo = NULL; BAR *pbar = NULL; ACK *pack = NULL; if (!FCreateFoo( /. if (!FCreateBar( /. if (!FCreateAck( / Use data in normal way. fRet = TRUE;/ Success return flag Error: /* This is common error and success cleanup code. */ if (pack != NULL) DeleteAck(pack); if (pbar != NULL) DeleteBar(pbar); if (pfoo != NULL) DeleteFoo(pfoo); return fRet; 有一个用在函数上的惯例,如果它返回指针并可能失败(比如因为对象被创建并返回),就让该函数返回 布尔值,并在参数里返回指针。这样明确表明该函数可能失败,并防止因为调用者不检查NULL返回值而带 来的错误。与此对应,不能失败的取指针函数可以直接返回指针。例如: 19 BOOL FCreateFoo(FOO *pfoo);/ can fail FOO *PfooGet(BAR *pbar);/ get a FOO from a BAR, cannot fail 摘要: 大多数情况下,错误代码返回值比异常处理好,因为简单。 对DLL导出的API或方法,总是用错误码。 在复杂情况下使用返回值时,用GOTO实现通用清除代码。 返回指针而可能失败的函数,应当返回布尔值并在参数中返回指针,而不是返回NULL表示失败。 5. Formatting Conventions 格式惯例格式惯例 下面描述各种格式惯例,我们将用它来产生看起来一样的代码以在小组里得到可读性。 5.1 Naming Conventions 命名规则命名规则 我们将用微软的匈牙利命名规则为标识符命名(参看见下面的附录A)。我们也将用依字母顺序排列的文本 文件记下我们为项目发明的匈牙利标记。每当你增加匈牙利标记的时候,把它加到这文件中。当声明类 时,可以标上建议该类型变量所用的匈牙利名字,但这并非必需的。 当用匈牙利命名时,记得做适当的抽象。适当的时候发明抽象标记并一致地应用它们。对全局变量,在类 型标记前用匈牙利前缀,使它看起来清晰。 除了匈牙利规则之外,我们将加标准前缀“Mso”(Microsoft Office)到大多数从我们的动态链接库导出的 标识符之前。这个简单方法告诉应用程序这个函数来自哪里,并防止和他们已经定义的符号冲突。接口成 员函数名和参数名是唯一的例外,因为不需要限制它们的范围。这个“Mso”前缀居于普通匈牙利标记之 前,其大小写取决于标识符的类型(例如,对函数用“Mso”,对类型定义用“MSO”,对常量用 “mso”)。 COM接口名用前缀“Mso”。 保存界面指针的变量这样命名:以“pi”(pointer to interface)开头,然后是 接口名的短缩写(最好用每个单词的首字母)。变量中不包含“mso”(看下面的例子)。 下例展示了各种标识符的前缀: void *MsoPvAlloc(int cb);/ exported API has Mso MSOTBCR tbcr;/ exported type has MSO, but variable doesn抰 #define msotbbsPushed 1/ enum/bit constant has mso and another tag #define msoCUsersMax 99/ plain constant has mso then hungarian IMsoToolbarSet *pits;/ interface name has IMso, variable doesn抰 pits-FUpdate();/ interface member has plain Hungarian pit-PitsGetToolb

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论