《林锐-高质量C/C++编程指南》笔记

1.if语句规则

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

根据布尔类型的语义,零值为“假”(记为FALSE),任何非零值都是“真”(记为TRUE)。TRUE 的值究竟是什么并没有统一的标准。例如Visual C++ 将TRUE 定义为1,而Visual Basic 则将TRUE 定义为-1。

假设布尔变量名字为flag,它与零值比较的标准if 语句如下:

if (flag) // 表示 flag 为真

if (!flag) // 表示 flag 为假

其它的用法都属于不良风格,例如:

if (flag == TRUE)

if (flag == 1 )

if (flag == FALSE)

if (flag == 0)

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

千万要留意,无论是 float 还是 double 类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。

假设浮点变量的名字为 x,应当将

if (x == 0.0) // 隐含错误的比较

转化为

const float EPSINON = 0.00001;

if ((x>=-EPSINON) && (x<=EPSINON))

其中 EPSINON 是允许的误差(即精度)。

 

2. 循环语句的效率

C++/C 循环语句中,for 语句使用频率最高,while 语句其次,do 语句很少用。提高循环体效率的基本办法是降低循环体的复杂性

l 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU 跨切循环层的次数

l  如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。

这是因为逻辑判断在循环体里面时老要进行逻辑判断,打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。

 

3. switch语句不能忘了default处理

即使程序真的不需要default 处理,也应该保留语句 default : break; 这样做并非多此一举,而是为了防止别人误以为你忘了 default 处理。

 

4. const与 #define 的比较

C++ 语言可以用 const 来定义常量,也可以用 #define 来定义常量。但是前者比后者有更多的优点:

(1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。

(2) 有些集成化的调试工具可以对 const 常量进行调试, 但是不能对宏常量进行调试。

在 C++ 程序中只使用 const 常量而不使用宏常量,即 const 常量完全取代宏常量

 

5.类中的常量

有时我们希望某些常量只在类中有效。由于#define 定义的宏常量是全局的,不能达到目的,于是想当然地觉得应该用const 修饰数据成员来实现。const 数据成员的确是存在的,但其含义却不是我们所期望的。const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其 const 数据成员的值可以不同不能在类声明中初始化 const 数据成员

以下用法是错误的,因为类的对象未被创建时,编译器不知道 SIZE 的值是什么。

class A
{…
const int SIZE = 100; // 错误,企图在类声明中初始化 const 数据成员
int array[SIZE]; // 错误,未知的 SIZE
};

const 数据成员的初始化只能在类构造函数的初始化表中进行,例如

class A
{…
A(int size); // 构造函数
const int SIZE ;
};
A::A(int size) : SIZE(size) // 构造函数的初始化表
{
…
}
A a(100); // 对象 a 的 SIZE 值为 100
A b(200); // 对象 b 的 SIZE 值为 200

但是怎样才能建立在整个类中都恒定的常量呢?别指望 const 数据成员了,应该用类中的枚举常量来实现。例如

class A
{…
enum { SIZE1 = 100, SIZE2 = 200}; // 枚举常量
int array1[SIZE1];
int array2[SIZE2];
};

枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如 PI=3.14159)。

 

6.函数设计

6.1 输入参数以值传递的方式传递对象使用“const &”

如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。

6.2 在函数体的“出口处”,对return 语句的正确性和效率进行检查。

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

(2)要搞清楚返回的究竟是“值”、“指针”还是“引用”。

(3)如果函数返回值是一个对象,要考虑 return 语句的效率。例如

return String(s1 + s2);

这是临时对象的语法,表示“创建一个临时对象并返回它”。不要以为它与“先创建一个局部对象temp 并返回它的结果”是等价的,如

String temp(s1 + s2);

return temp;

实质不然,上述代码将发生三件事。首先, temp 对象被创建,同时完成初始化;然后拷贝构造函数把temp 拷贝到保存返回值的外部存储单元中;最后,temp 在函数结束时被销毁(调用析构函数)。然而“创建一个临时对象并返回它”的过程是不同的,编译器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了效率

类似地,我们不要将

return int(x + y); // 创建一个临时变量并返回它

写成

int temp = x + y;

return temp;

由于内部数据类型如 int,float,double 的变量不存在构造函数与析构函数, 虽然该 “临时变量的语法”不会提高多少效率,但是程序更加简洁易读。

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

在编写函数时, 要进行反复的考查, 并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。


7. 数组和指针内存管理

7.1 数组内存区

数组要么在静态存储区被创建 (如全局数组), 要么在栈上被创建。 数组名对应着 (而不是指向)一块内存其地址与容量在生命期内保持不变,只有数组的内容可以改变

7.2 运算符sizeof()与数组、指针

用运算符sizeof(数组名)可以计算出数组的容量(字节数)。而sizeof(指针名)= 4。

注意当数组作为函数的参数进行传递时,被调函数中该数组明自动退化为同类型的指针

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


毛病出在函数GetMemory 中。编译器总是要为函数的每个参数制作临时副本,指针参数 p 的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p 的内容,就导致参数 p 的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p 申请了新的内存,只是把_p 所指的内存地址改变了,但是 p 丝毫未变。所以函数 GetMemory并不能输出任何东西。事实上,每执行一次 GetMemory 就会泄露一块内存,因为没有用free 释放内存。

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


也可以用函数返回值来传递动态内存。这种方法更加简单:


7.4 malloc/free与new/delete

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

对于非内部数据类型的对象而言,光用 maloc/free 无法满足动态对象的要求。

由于对象在 创 建 的 同 时 要 自 动 执 行 构 造 函 数 , 对 象 在 消 亡 之 前 要 自 动 执 行 析 构 函 数 。 由 于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

可以理解为:new =malloc() + 对象数据类型构造函数

                        delete= 对象数据类型析构函数+ free()

7.5. new/delete 的使用要点

运算符 new 使用起来要比函数 malloc简单得多,例如:

int *p1 = (int *)malloc(sizeof(int) * length);

int *p2 = new int[length];

这是因为 new 内置了 sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言, new 在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new 的语句也可以有多种形式。


8. C++函数重载

如果同名函数的参数不同(包括类型、顺序不同),那么容易区别出它们是不同的函数。

如果同名函数仅仅是返回值类型不同,有时可以区分,有时却不能,因为在 C++/C 程序中,我们可以忽略函数的返回值,在这种情况下,编译器和程序员都不知道哪个 Function 函数被调用。

所以只能靠参数而不能靠返回值类型的不同来区分重载函数

 

并不是两个函数的名字相同就能构成重载。全局函数和类的成员函数同名不算重载,因为函数的作用域不同。例如:

void Print(…); // 全局函数

class A

{…

void Print(…);// 成员函数

}

不论两个 Print 函数的参数是否 不同,如果 类的某个成 员函数要调 用全局 函 数

Print,为了与成员函数 Print 区别,全局函数被调用时应加‘::’标志。如

::Print(…); // 表示 Print 是全局函数而非成员函数


9 关于C++中的重载、重写(覆盖)、隐藏

成员函数被重载(overload)的特征:

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

(2)函数名字相同;

(3)参数不同;

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

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

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

(2)函数名字相同;

(3)参数相同;

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

 

所谓隐藏,在《林锐-高质量C、C++编程指南》中有例子如下:

#include <iostream.h>
class Base
{
public:
virtual void f(float x){ cout <<"Base::f(float) " << x << endl; }
void g(float x){ cout <<"Base::g(float) " << x << endl; }
void h(float x){ cout <<"Base::h(float) " << x << endl; }
};
 
classDerived : public Base
{
public:
virtualvoid f(float x){ cout << "Derived::f(float) " << x<< endl; }
voidg(int x){ cout << "Derived::g(int) " << x << endl;}
voidh(float x){ cout << "Derived::h(float) " << x <<endl; }
};

使用情况如下:

voidmain(void)
{
Derivedd;
Base*pb = &d;
Derived*pd = &d;
 
 
pb->f(3.14f);// Derived::f(float) 3.14
pd->f(3.14f);// Derived::f(float) 3.14
 
pb->g(3.14f);// Base::g(float) 3.14
pd->g(3.14f);// Derived::g(int) 3 (surprise!)
 
pb->h(3.14f);// Base::h(float) 3.14 (surprise!)
pd->h(3.14f);// Derived::h(float) 3.14
}

首先说明:

l  基类类型指针可以指向派生类型对象

l  基类指针可以指向一个派生类对象,但派生类指针不能指向基类对象。

基类类型指针可以指向派生类型对象但是无法使用不存在于基类只存在于派生类的元素。(所以我们需要虚函数和纯虚函数)

原因是这样的:

在内存中,一个基类类型的指针是覆盖N个单位长度的内存空间。当其指向派生类的时候,由于派生类元素在内存中堆放是:前N个是基类的元素,N之后的是派生类的元素。于是基类的指针就可以访问到基类也有的元素了,但是此时无法访问到派生类(就是N之后)的元素。

 

实际上基类中pb->f(float)被子类相同函数覆盖,所以pb->f(3.14f)调用的为子类f()函数

pd->f(float)自然调用自身f()函数。

 

pb->g(3.14f)只能访问对象中基类包含的内存区,故调用基类g()函数

pd->g(3.14f),pd首先在派生类包含区中进行函数匹配,匹配到了同名函数(尽管参数不一致,但是可以隐式转换)Derived::g(int),则基类中更吻合的函数Base::g(float)被忽略,可看做被派生类对象隐藏。

实际上如果派生类中没有同名函数g()或者不能进行参数类型隐式转换,pd->g(3.14)将调用基类中的对应函数。

 

pb->h(3.14f);只能访问对象中基类包含的内存区,故调用基类h()函数

pd->h(3.14f);查找派生类中函数时即发现同名函数Derived::h(),则调用该函数,可看做基类的h()函数被派生类对象隐藏。

 

总结:隐藏是相对于派生类对象而言

隐藏规则:

(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

(2)如果派生类的函数与基类的函数同名,并且参数也相同, 但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

          基类的同名函数被隐藏后,如果实参类型不能隐式转换成派生类对应函数形参类型,则报错,尽管基类对应被隐藏函数数据类型更吻合,但是没办法,它被隐藏了。


10.内联

在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。

这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。假如内联函数是成员函数,对象的地址(this)会被放在合适的地方,这也是预处理器办不到的。

 

C++ 语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作类的数据成员。所以在 C++ 程序中,应该用内联函数取代所有宏代码。

“断言 assert”恐怕是唯一的例外,assert 是仅在Debug 版本起作用的宏,它用于检查“不应该”发生的情况。为了不在程序的Debug 版本和Release 版本引起差别,assert 不应该产生任何副作用。如果 assert 是函数,由于函数调用会引起内存、代码的变动,那么将导致 Debug版本与 Release 版本存在差异。所以assert 不是函数,而是宏

 

关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。所以说,inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”

 

定义在类声明之中的成员函数将自动地成为内联函数

11.类的构造函数、析构函数、赋值函数

每个类只有一个析构函数一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。对于任意一个类A,如果不想编写上述函数, C++编译器将自动为A 产生四个缺省的函数:缺省的无参构造函数、缺省的拷贝构造函数、缺省的析构函数、缺省的赋值函数。“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。

11.1 构造函数初始化表

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

l 类的const常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。

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

11.2 拷贝构造函数与赋值函数

>如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。

以类String 的两个对象a,b 为例,假设a.m_data 的内容为“hello”, b.m_data 的内容为“world”。现将 a 赋给 b,缺省赋值函数的“位拷贝”意味着执行 b.m_data = a.m_data。这将造成三个错误:一是 b.m_data 原有的内存没被释放,造成内存泄露;二是b.m_data 和 a.m_data 指向同一块内存,a 或 b 任何一方变动都会影响另一方;三是在对象被析构时,m_data 被释放了两次。

>拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。二者非常容易混淆,常导致错写、错用。

 

拷贝构造函数与赋值函数实例:

// 拷贝构造函数
String::String(constString &other)
{
// 允许操作other 的私有成员m_data
intlength = 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)分配新的内存资源,并复制内容
intlength = strlen(other.m_data);
m_data= new char[length+1];
strcpy(m_data,other.m_data);
// (4)返回本对象的引用
return*this;
}

类 String 拷贝构造函数与普通构造函数的区别是:在函数入口处无需与 NULL 进行比较,这是因为“引用”不可能是 NULL,而“指针”可以为 NULL。

11.3 如何在派生类中实现类的基本函数

基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:

>派生类的构造函数应在其初始化表里调用基类的构造函数。

>基类与派生类的析构函数应该为虚(即加 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
deletepB;
}

输出结果为:

~Derived

~Base

如果析构函数不为虚,那么输出结果为

~Base

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值