1、 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。
2、 如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。
3、 即使程序真的不需要default 处理,也应该保留语句 default : break。
4、 C 语言用#define
来定义常量(称为宏常量)。 C++
语言除了 #define
外还可以用 const 来定义常量(称为 const
常量)。
5、 const 与 #define 的比较
Ø const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
Ø 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。
6、 需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
7、 const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其 const 数据成员的值可以不同。
8、 不能在类声明中初始化const 数据成员。以下用法是错误的,因为类的对象未被创建时,编译器不知道 SIZE 的值是什么。
class A
{…
const intSIZE = 100; // 错误,企图在类声明中初始化 const 数据成员
intarray[SIZE]; // 错误,未知的 SIZE
};
9、 const 数据成员的初始化只能在类构造函数的初始化表中进行。
10、 枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如 PI=3.14159)。
11、 一般地,应将目的参数放在前面,源参数放在后面。
12、 有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。
13、 很多程序错误是由非法参数引起的,我们应该充分理解并正确使用“断言”( assert)来防止此类错误。
14、 return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
15、 断言 assert 是仅在 Debug 版本起作用的宏,它用于检查“不应该”发生的情况。
16、 引用的一些规则如下:
Ø 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
Ø 不能有 NULL 引用,引用必须与合法的存储单元关联(指针则可以是 NULL)。
Ø 一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
17、 内存分配方式有三种:
Ø 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量, static 变量。
Ø 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
Ø 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
18、 常见的内存错误及其对策如下:
Ø 内存分配未成功,却使用了它。
Ø 内存分配虽然成功,但是尚未初始化就引用它。
Ø 内存分配成功并且已经初始化,但操作越过了内存的边界。
Ø 忘记了释放内存,造成内存泄露。
Ø 释放了内存却继续使用它。(使用 free 或 delete 释放了内存后,没有将指针设置为 NULL。导致产生“野指针”。)
19、 数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
20、 不能对数组名进行直接复制与比较。
21、 用运算符 sizeof 可以计算出数组的容量(字节数)。sizeof(p)得到的是一个指针变量的字节数,相当于 sizeof(char*),而不是 p 所指的内存容量。 C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。
22、 注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
23、 如果函数的参数是一个指针,不要指望用该指针去申请动态内存。(可以用指针的指针)
24、 发现指针 p 被 free 以后其地址仍然不变(非 NULL),只是该地址对应的内存是垃圾, p 成了“野指针”。如果此时不把 p 设置为 NULL,会让人误以为 p 是个合法的指针。
25、 free()之后由于指针所指向的内存已经被释放,所以其它代码有机会改写其中的内容,相当于该指针从此指向了自己无法控制的地方,也称为野指针。
26、 我们发现指针有一些“似是而非”的特征:
Ø 指针消亡了,并不表示它所指的内存会被自动释放。
Ø 内存被释放了,并不表示指针会消亡或者成了 NULL 指针。
27、 “野指针”的成因主要有两种:
Ø 指针变量没有被初始化。任何指针变量刚被创建时不会自动成为 NULL 指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为 NULL,要么让它指向合法的内存。
Ø 指针 p 被 free 或者 delete 之后,没有置为 NULL,让人误以为 p 是个合法的指针。
Ø 指针操作超越了变量的作用范围。
28、 malloc 与 free 是 C++/C 语言的标准库函数,new/delete 是 C++的运算符。它们都可用于申请动态内存和释放内存。
29、 对于非内部数据类型的对象而言,光用 maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于 malloc/free。因此 C++语言需要一个能完成动态内存分配和初始化工作的运算符 new,以及一个能完成清理与释放内存工作的运算符 delete。注意 new/delete 不是库函数。
30、 由于内部数据类型的“对象”没有构造与析构的过程,对它们而言 malloc/free 和new/delete 是等价的。
31、 如果用 free 释放“ new 创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用 delete 释放“ malloc 申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差。所以 new/delete 必须配对使用, malloc/free 也一样。
32、 malloc/free 的使用我们应当把注意力集中在两个要素上:“类型转换”和“ sizeof”。
Ø malloc 返回值的类型是 void *,所以在调用 malloc 时要显式地进行类型转换,将 void * 转换成所需要的指针类型。
Ø malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。
33、 如果 p 是 NULL 指针,那么 free 对 p 无论操作多少次都不会出问题。如果 p 不是 NULL 指针,那么 free 对 p连续操作两次就会导致程序运行错误。
34、 如果用 new
创建对象数组,那么只能使用对象的无参数构造函数。例如
Obj *objects = newObj[100]; // 创建100
个动态对象不能写成
Obj *objects = newObj[100](1);// 创建100
在用delete 释放对象数组时,留意不要丢了符号‘ []’。例如
delete []objects; // 正确的用法
delete objects; // 错误的用法
后者相当于delete objects[0],漏掉了另外 99
个对象。
35、 c++中为什么static成员函数不能声明为const?(注意是成员函数)
答:这是C++的规则,const修饰符用于表示函数不能修改成员变量的值,该函数必须是含有this指针的类成员函数,函数调用方式为thiscall,而类中的static函数本质上是全局函数,调用规约是__cdecl或__stdcall,不能用const来修饰它。
36、 重载和内联机制既可用于全局函数也可用于类的成员函数, const 与virtual 机制仅用于类的成员函数。
37、 只能靠参数而不能靠返回值类型的不同来区分重载函数。
38、 注意并不是两个函数的名字相同就能构成重载。全局函数和类的成员函数同名不算重载,因为函数的作用域不同。
39、 当心隐式类型转换导致重载函数产生二义性。
40、 成员函数被重载的特征:
Ø 相同的范围(在同一个类中);
Ø 函数名字相同;
Ø 参数不同;
Ø virtual 关键字可有可无。
41、 覆盖是指派生类函数覆盖基类函数,特征是:
Ø 不同的范围(分别位于派生类与基类);
Ø 函数名字相同;
Ø 参数相同;
Ø 基类函数必须有virtual 关键字。
42、 “隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
Ø 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。【参数不同一定会被隐藏】
Ø 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。【没有virtual一定会被隐藏】
43、 参数缺省值只能出现在函数的声明中,而不能出现在定义体中。
44、 如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。
45、 在 C++运算符集合中,有一些运算符是不允许被重载的。这种限制是出于安全方面的考虑,可防止错误和混乱。
Ø 不能改变 C++内部数据类型(如int,float 等)的运算符。
Ø 不能重载‘ .’,因为‘ .’在类中对任何成员都有意义,已经成为标准用法。
Ø 不能重载目前 C++运算符集合中没有的符号,如#,@,$等。原因有两点,一是难以理解,二是难以确定优先级。
Ø 对已经存在的运算符进行重载时,不能改变优先级规则,否则将引起混乱。
46、 在 C 程序中,可以用宏代码提高执行效率。宏代码本身不是函数,但使用起来象函数。预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的 CALL调用、返回参数、执行 return 等过程,从而提高了速度。使用宏代码最大的缺点是容易出错,预处理器在复制宏代码时常常产生意想不到的边际效应。使用宏代码还有另一种缺点:无法操作类的私有数据成员。
47、 C++ 语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以*操作类的数据成员。所以在 C++ 程序中,应该用内联函数取代所有宏代码。
48、 关键字 inline
必须与函数定义体放在一起才能使函数成为内联,仅将 inline
放在函数声明前面不起任何作用。
49、 定义在类声明之中的成员函数将自动地成为内联函数。
50、 内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
51、 以下情况不宜使用内联:
Ø 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
Ø 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
52、 每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。对于任意一个类 A,如果不想编写上述函数,C++编译器将自动为 A 产生四个缺省的函数,如:
Ø A(void); //缺省的无参数构造函数
Ø A(const A &a); //缺省的拷贝构造函数
Ø ~A(void); //缺省的析构函数
Ø A & operate =(const A &a); //缺省的赋值函数
53、 构造函数与析构函数的另一个特别之处是没有返回值类型,这与返回值类型为 void 的函数不同。
54、 构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表)。初始化表位于函数参数表之后,却在函数体 {} 之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。
55、 类的 const 常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化。
56、 类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方的效率不完全相同。
57、 非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。
58、 构造和析构的次序:构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。
59、 成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。
60、 拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。
61、 深度复制(针对《c++ primer plus》一书string例题的解释):复制构造函数应当复制字符串并将副本的地址赋给str成员,而不仅仅是复制字符串的地址,这样每个对象都有自己的字符串,而不是引用另一个对象的字符串,调用析构函数时都将释放不同的字符串,而不会试图去释放已经被释放的字符串。
62、 何时调用复制构造函数:新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。每当程序生成了对象副本时,编译器都将使用复制构造函数。具体的说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。记住,按值传递意味着创建原始变量的一个副本。编译器生成临时对象时,也将使用复制构造函数。何时生成临时对象随编译器而异,但无论那种编译器,当按值传递和返回对象时,都将调用复制构造函数。
63、 String类的赋值函数要做的工作:
Ø 自我检查赋值的情况
Ø 释放成员指针以前指向的内存
Ø 复制数据而不仅仅是数据的地址
Ø 指向一个返回对象的引用(不要将 return *this 错写成 return this)
64、 基类与派生类的析构函数应该为虚(即加 virtual关键字)。
#include <iostream.h>
classBase
{
public:
virtual ~Base() { cout<< "~Base" << endl ; }
};
classDerived : public Base
{
public:
virtual ~Derived() { cout<< "~Derived" << endl; }
};
voidmain(void)
{
Base * pB = new Derived; // upcast
delete pB;
}
输出结果为:
~Derived
~Base
如果析构函数不为虚,那么输出结果为
~Base
65、 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种行为非常重要,因为这样基类指针或引用可以指向派生类对象。
66、 用 const
修饰函数的参数:
如果参数作输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”,都不能加 const
修饰,否则该参数将失去输出功能。const
只能修饰输入参数。(参数做输出用不能加const,const只能修饰输入参数)
67、 如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加 const
修饰。
68、 const 成员函数的声明看起来怪怪的: const 关键字只能放在函数声明的尾部,大概是因为其它地方都已经被占用了。