Mindon.IDEA

Air off, Mind on ~ / Javascript+Golang, Sci, Health… /

Effective C++读书笔记

BlogMS original blog key: 1000141891, blog id: airoff History stat: 浏览/评论:370/0 , 日期:2005年1月7日 09:56

filename.h

/*

  • Copyright © 2004, company & department

  • All rights reserved.

*

  • 文件名称: filename.h

  • 文件标识:

  • 摘要: 简要描述本文件的内容

*

  • 当前版本: 1.1

  • 作者: 输入作者(或修改者)名字

  • 完成日期: 2001年7月20日

*

  • 取代版本:1.0

  • 原作者: 输入原作者(或修改者)名字

  • 完成日期: 2001年5月10日

*/

ifndef MY_H / 防止重复引用 /

define MY_H

/ 预处理块 /

include <stdio.h> / 引用标准库的头文件,编译器将从标准库目录开始搜索 /

include "my.h" / 引用非标准库的头文件,编译器将从用户目录开始搜索 /

/ 函数和类结构声明等 /

/ 不提倡使用全局变量,尽量不要在头文件中出现象extern int value 这类声明 /

endif

/* 头文件的作用

(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只

要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库

功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。

(2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件

中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错

的负担。

*/

/* 目录结构安排 inc,src

私有的头文件可以和定义文件存放于同一个目录

尽可能在定义变量的同时初始化该变量(就近原则)

代码行最大长度宜控制在70 至80 个字符以内

应当将修饰符* 和& 紧靠变量名

注释是对代码的“提示”,而不是文档

注释的位置应与被描述的代码相邻,可以放在代码的上方或右方



Windows 应用程序的标识符通常采用“大小写”混排的方式

Unix 应用程序的标识符通常采用“小写加下划线”的方式

变量的名字应当使用“名词”或者“形容词+名词”

全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)

类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身



WINDOWS

类名和函数名用大写字母开头的单词组合而成

变量和参数用小写字母开头的单词组合而成

常量全用大写的字母,用下划线分割单词

静态变量加前缀s_(表示static)

如果不得已需要全局变量,则使全局变量加前缀g_(表示global)

类的数据成员加前缀m_(表示member),这样可以避免数据成员与成员函数的参数同名。



不可将布尔变量直接与TRUE、FALSE 或者1、0 进行比较

不可将浮点变量用“==”或“!=”与任何数字比较

应用 if ((x&gt;=-EPSINON) && (x&lt;=EPSINON))

应当将指针变量用“==”或“!=”与NULL 比较



建议for 语句的循环控制变量的取值采用“半开半闭区间”写法



在C++ 程序中只使用const 常量而不使用宏常量

需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义

文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。



const 数据成员只在某个对象生存期内是常量

枚举常量在整个类中都是恒定的常量(不会占用对象的存储空间,在编译时被全部求值。

缺点是:它的隐含数据类型是整数,最大值有限)



一般地,应将目的参数放在前面,源参数放在后面。

如果参数是指针,且仅作输入用,则应在类型前加const,以防止该  指针在函数体内被意外修改。

如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,

这样可以省去临时对象的构造和析构过程,从而提高效率。

避免函数有太多的参数,参数个数尽量控制在5 个以内。



不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return 语句返回。

有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。

return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。



函数的功能要单一,不要设计多用途的函数。

数体的规模要小,尽量控制在50 行代码之内。

尽量避免函数带有“记忆”功能。



断言assert 是仅在Debug 版本起作用的宏,它用于检查“不应该”发生的情况。

使用断言捕捉不应该发生的非法情况。

在函数的入口处,使用断言检查参数的有效性(合法性)

使用断言对假定进行检查。



引用的主要功能是传递函数的参数和返回值。

(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。

(2)不能有NULL 引用,引用必须与合法的存储单元关联(指针则可以是NULL)。

(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。



内存分配方式有三种:

(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的

整个运行期间都存在。例如全局变量,static 变量。

(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函

数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集

中,效率很高,但是分配的内存容量有限。

(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意

多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存

期由我们决定,使用非常灵活,但问题也最多。



常见的内存错误及其对策如下:

- 内存分配未成功,却使用了它

- 内存分配虽然成功,但是尚未初始化就引用它

- 内存分配成功并且已经初始化,但操作越过了内存的边界。

- 忘记了释放内存,造成内存泄露。

- 释放了内存却继续使用它。



&gt; 用malloc 或new 申请内存之后,应该立即检查指针值是否为NULL。

&gt; 不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

&gt; 避免数组或指针的下标越界。

&gt; 动态内存的申请与释放必须配对,防止内存泄漏。

&gt; 用free 或delete 释放了内存之后,立即将指针设置为NULL,防止产生“野指针”





数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着

(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。



指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来

操作动态内存。指针远比数组灵活,但也更危险。

用运算符sizeof 可以计算出数组的容量(字节数)。



如果函数的参数是一个指针,不要指望用该指针去申请动态内存。

如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”。

可以用函数返回值来传递动态内存



(1)指针消亡了,并不表示它所指的内存会被自动释放。

(2)内存被释放了,并不表示指针会消亡或者成了NULL 指针。

“野指针”的成因主要有:

    指针变量没有被初始化;

    指针p 被free 或者delete 之后,没有置为NULL;

    指针操作超越了变量的作用范围。



malloc 与free 是C++/C 语言的标准库函数,new/delete 是C++的运算符。

它们都可用于申请动态内存和释放内存。



(1)判断指针是否为NULL,如果是则马上用return 语句终止本函数。

(2)判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。



如果p 是NULL 指针,那么free 对p 无论操作多少次都不会出问题。

如果p 不是NULL 指针,那么free 对p 连续操作两次就会导致程序运行错误。



在用delete 释放对象数组时,留意不要丢了符号‘[]’



C++增加了重载(overloaded)、内联(inline)、const 和virtual四种新机制。

其中重载和内联机制既可用于全局函数也可用于类的成员函数,

const 与 virtual 机制仅用于类的成员函数。

extern“C” {}

全局函数被调用时应加‘::’标志。



成员函数被重载的特征:

(1)相同的范围(在同一个类中);

(2)函数名字相同;

(3)参数不同;

(4)virtual 关键字可有可无。



覆盖是指派生类函数覆盖基类函数,特征是:

(1)不同的范围(分别位于派生类与基类);

(2)函数名字相同;

(3)参数相同;

(4)基类函数必须有virtual 关键字。



“隐藏”是指派生类的函数屏蔽了与其同名的基类函数

(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual

关键字,基类的函数将被隐藏(注意别与重载混淆)。

(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual

关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)



参数缺省值只能出现在函数的声明中,而不能出现在定义体中。



如果运算符被重载为全局函数,那么只有一个参数的运算符叫做一元运算符,有两

个参数的运算符叫做二元运算符。

如果运算符被重载为类的成员函数,那么一元运算符没有参数,二元运算符只有一

个右侧参数,因为对象自己成了左侧参数。



关键字inline 必须与函数定义体放在一起才能使函数成为内联

inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。

以下情况不宜使用内联:

(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。



class String

{

    public:

        String(const char *str = NULL); // 普通构造函数

        String(const String &other); // 拷贝构造函数

        ~String(void); // 析构函数

        String & operate =(const String &other); // 赋值函数

    private:

        char *m_data; // 用于保存字符串

}



// String 的普通构造函数

String::String(const char *str)

{

    if(str==NULL) {

        m_data = new char[1];

        *m_data = ‘\0’;

    } else {

        int length = strlen(str);

        strcpy(m_data, str);

    }

}



// String 的析构函数

String::~String(void)

{

    delete [] m_data;

    // 由于m_data 是内部数据类型,也可以写成delete m_data;

}



// 拷贝构造函数

String::String(const String &other)

{

    // 允许操作other 的私有成员m_data

    int length = strlen(other.m_data);

    m_data = new char[length+1];

    strcpy(m_data, other.m_data);

}

// 赋值函数

String & String::operate =(const String &other)

{

    // (1) 检查自赋值

    if(this == &other)

        return *this;

    // (2) 释放原有的内存资源

    delete [] m_data;

    // (3)分配新的内存资源,并复制内容

    int length = strlen(other.m_data);

    m_data = new char[length+1];

    strcpy(m_data, other.m_data);

    // (4)返回本对象的引用

    return *this;

}





构造函数初始化表的使用规则:

    如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。

    类的const 常量只能在初始化表里被初始化

    的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。

    非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。



成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。



如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”

的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。



拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。





如果输入参数采用“指针传递”,那么加const 修饰可以防止意外地改动该指针,起到保护作用。

如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该

输入参数本来就无需保护,所以不要加const 修饰。

对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const 引用传递”,目的是提高效率。

对于内部数据类型的输入参数,不要将“值传递”的方式改为“const 引用传递”。

否则既达不到提高效率的目的,又降低了函数的可理解性。



如果给以“指针传递”方式的函数返回值加const 修饰,那么函数返回值(即指针)

的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。

如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储

单元中,加const 修饰没有任何价值。

函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数

中,目的是为了实现链式表达。

const 成员函数的声明中 const 关键字只能放在函数声明的尾部



不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率。

已知strcpy 函数的原型是

char strcpy(char strDest, const char *strSrc);

其中strDest 是目的字符串,strSrc 是源字符串。

(1)不调用C++/C 的字符串库函数,请编写函数strcpy

char strcpy(char strDest, const char *strSrc)

{

assert((strDest!=NULL) && (strSrc !=NULL)); // 2分

char *address = strDest; // 2分

while( (*strDest++ = * strSrc++) != ‘\0’ ) // 2分

    NULL ;

return address ; // 2分

}

*/

effective.cpp

/*/

1, 尽量用const和inline而不用#define

2, 尽量用<iostream>而不用<stdio.h>

如果使用了#include &lt;iostream&gt;, 得到的是置于名字空间std(见条款28)下的iostream 库的元素;

如果使用#include &lt;iostream.h&gt;,得到的是置于全局空间的同样的元素。在全局空间获取元素会导致名字冲突,

而设计名字空间的初衷正是用来避免这种名字冲突的发生。

3, 尽量用new 和delete 而不用malloc 和free

必须对strdup 返回的指针进行free 操作

4, 尽量使用C++风格的注释

5, 对应的new 和delete 要采用相同的形式

如果你调用new时用了[],调用delete时也要用[]。如果调用new 时没有用[],那调用delete 时也不要用[]。

为了避免混乱,最好杜绝对数组类型用typedefs

6, 析构函数里对指针成员调用delete

删除空指针是安全的

除非类成员最初用了new,否则是不用在析构函数里用delete 的

7, 预先准备好内存不够的情况

当内存分配请求不能满足时,调用你预先指定的一个出错处理函数。

set_new_handler 的输入参数是operator new 分配内存失败时要调用的出 错处理函数的指针,

返回值是set_new_handler 没调用之前就已经在起作用的旧的出错处理函数的指针。

&gt;&gt;一个设计得好的new-handler 函数必须实现下面功能中的一种:

  产生更多的可用内存; 安装另一个不同的new-handler 函数; 卸除new-handler;

  抛出std::bad_alloc 或从std::bad_alloc 继承的其他类型的异常;

  没有返回。典型做法是调用abort 或exit。

8, 写operator new 和operator delete 时要遵循常规

C++标准要求,即使在请求分配0 字节内存时,operator new 也要返回一个合法指针。

9, 避免隐藏标准形式的new

10, 如果写了operator new 就要同时写operator delete

11, 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符

12, 尽量使用初始化而不要在构造函数里赋值

13, 初始化列表中成员列出的顺序和它们在类中声明的顺序相同

14, 确定基类有虚析构函数

15, 让operator=返回*this 的引用

16, 在operator=中对所有数据成员赋值

17, 在operator=中检查给自己赋值的情况

&gt;&gt; 对象将如何被创建和摧毁?

&gt;&gt; 对象初始化和对象赋值有什么不同?

&gt;&gt; 通过值来传递新类型的对象意味着什么?

&gt;&gt; 新类型的合法值有什么限制?

&gt;&gt; 新类型符合继承关系吗?

&gt;&gt; 允许哪种类型转换?

&gt;&gt; 什么运算符和函数对新类型有意义?

&gt;&gt; 哪些运算符和函数要被明确地禁止?

&gt;&gt; 谁有权访问新类型的成员?

&gt;&gt; 新类型的通用性如何?

18, 争取使类的接口完整并且最小

19, 分清成员函数,非成员函数和友元函数

20, 避免public 接口出现数据成员

21, 尽可能使用const

22, 尽量用“传引用”而不用“传值”

23, 必须返回一个对象时不要试图返回一个引用

24, 在函数重载和设定参数缺省值间慎重选择

25, 避免对指针和数字类型重载

26, 当心潜在的二义性

27, 如果不想使用隐式生成的函数就要显式地禁止它

28, 划分全局名字空间

29, 避免返回内部数据的句柄

30, 避免这样的成员函数:其返回值是指向成员的非const 指针或引用,但成员的访问级比这个函数要低

31, 千万不要返回局部对象的引用,也不要返回函数内部用new 初始化的指针的引用

32, 尽可能地推迟变量的定义

33, 明智地使用内联

34, 将文件间的编译依赖性降至最低

35, 使公有继承体现"是一个" 的含义

36, 区分接口继承和实现继承

37, 决不要重新定义继承而来的非虚函数

38, 决不要重新定义继承而来的缺省参数值

39, 避免"向下转换" 继承层次

40, 通过分层来体现"有一个" 或"用…来实现"

41, 区分继承和模板

当对象的类型不影响类中函数的行为时,就要使用模板来生成这样一组类。

当对象的类型影响类中函数的行为时,就要使用继承来得到这样一组类。

42, 明智地使用私有继承

私有继承意味着&quot;用...来实现&quot;。

43, 明智地使用多继承

44, 说你想说的;理解你所说的

45, 弄清C++在幕后为你所写、所调用的函数

一个拷贝构造函数,一个赋值运算符,一个析构函数,一对取址运算符{,缺省构造函数}。

46, 宁可编译和链接时出错,也不要运行时出错

47, 确保非局部静态对象在使用前被初始化

48, 重视编译器警告

49, 熟悉标准库

50, 提高对C++的认识

//*/

Comments