注意并不是两个函数的名字相同就能构成重载。全局函数和类的成员函数同名不算重载,因为函数的作用域不同。例如:
2、当心隐式类型转换导致重载函数产生二义性
void output( int x)
{
cout << " output int " << x << endl ;
}
void output( float x)
{
cout << " output float " << x << endl ;
}
void main(void)
{
int x = 1;
float y = 1.0;
output(x); // output int 1
output(y); // output float 1
output(1); // output int 1
// output(0.5); // error! ambiguous call, 因为自动类型转
output(int(0.5)); // output int 0
output(float(0.5)); // output float 0.5
3、 成员函数的重载、覆盖(override)与隐藏很容易混淆,C++程序员必须要搞清楚概念,否则错误将防不胜防。
重载与覆盖
成员函数被重载的特征:
(1)相同的范围(在同一个类中) ;
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类) ;
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有 virtual 关键字。
本来仅仅区别重载与覆盖并不算困难,但是 C++的隐藏规则使问题复杂性陡然增加。
这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual
关键字,基类的函数将被隐藏(注意别与重载混淆) 。
(2) 如果派生类的函数与基类的函数同名, 并且参数也相同, 但是基类函数没有 virtual
关键字。此时,基类的函数被隐藏(注意别与覆盖混淆) 。
4、参数缺省值只能出现在函数的声明中,而不能出现在定义体中。 如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。
5、 如果运算符被重载为全局函数,那么只有一个参数的运算符叫做一元运算符,有两个参数的运算符叫做二元运算符。
如果运算符被重载为类的成员函数,那么一元运算符没有参数,二元运算符只有一个右侧参数,因为对象自己成了左侧参数。
运算符 规则
所有的一元运算符 建议重载为成员函数
= () [] -> 只能重载为成员函数
+= -= /= *= &= |= ~= %= >>= <<= 建议重载为成员函数
所有其它运算符 建议重载为全局函数
6、C++ 语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作类的数据成员。所以在 C++ 程序中,应该用内联函数取代所有宏代码, “断言 assert”恐怕是唯一的例外。assert 是仅在 Debug 版本起作用的宏,它用于检查“不应该”发生的情况
关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用
故inline 是一种“用于实现的关键字” ,而不是一种“用于声明的关键字” 。
定义在类声明之中的成员函数将自动地成为内联函数
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的
执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收
获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大
消耗更多的内存空间。以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。
第 9 章 类的构造函数、析构函数与赋值函数
C++编译器将自动为 A 产生四个缺省的函数,如
A(void); // 缺省的无参数构造函数
A(const A &a); // 缺省的拷贝构造函数
~A(void); // 缺省的析构函数
A & operate =(const A &a); // 缺省的赋值函数
这不禁让人疑惑,既然能自动生成函数,为什么还要程序员编写?
原因如下:
(1)如果使用“缺省的无参数构造函数”和“缺省的析构函数” ,等于放弃了自主“初
始化”和“清除”的机会,C++发明人 Stroustrup 的好心好意白费了。
(2) “缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”
的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
根据经验,不少难以察觉的程序错误是由于变量没有被正确初始化或清除造成的,
除了名字外,构造函数与析构函数的另一个特别之处是没有返回值类型
1、如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。类的 const 常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式。类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。
非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率
2、一个有趣的现象是,成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的 构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。
3、现将 a 赋给 b,缺省赋值函数的“位拷贝”意味着执行 b.m_data = a.m_data。这将造成三个错误: 一是 b.m_data 原有的内存没被释放,造成内存泄露;二是b.m_data 和 a.m_data 指向同一块内存,a 或 b 任何一方变动都会影响另一方;三是在对象被析构时,m_data 被释放了两次。
4、基类与派生类的析构函数应该为虚(即加 virtual 关键字) 。
5、在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值
第 10 章 类的继承与组合
1、所以更加严格的继承规则应当是:若在逻辑上 B 是 A 的“一种” ,并且 A 的所有功
能和属性对 B 而言都有意义,则允许 B 继承 A 的功能和属性。
第 11 章 其它编程经验
1、 使用 const 提高函数的健壮性
对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const 引用传递” ,目的是提高效率。例如将 void Func(A a) 改为 void Func(const A &a)。
对于内部数据类型的输入参数, 不要将 “值传递” 的方式改为 “const 引用传递” 否则既达不到提高效率的目的, 又降低了函数的可理解性。 例如 void Func(int x) 不应该改为 void Func(const int &x)。
如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加 const 修饰的同类型指针。
函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数 中,目的是为了实现链式表达。
任何不会修改数据成员的函数都应该声明为 const 类型 。如果在编写 const 成员函数时,不慎修改了数据成员,或者调用了其它非 const 成员函数,编译器将指出错误,这无疑会提高程序的健壮性。
2 、提高程序的效率
不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率。
以提高程序的全局效率为主,提高局部效率为辅。
在优化程序的效率时,应当先找出限制效率的“瓶颈” ,不要在无关紧要之处优化。
先优化数据结构和算法,再优化执行代码。
有时候时间效率和空间效率可能对立,此时应当分析那个更重要,作出适当的折衷。例如多花费一些内存来提高性能。
不要追求紧凑的代码,因为紧凑的代码并不能产生高效的机器码 。
3 一些有益的建议
变量(指针、数组)被创建之后应当及时把它们初始化,以防止把未被初始化的变量当成右值使用。
如果原有的代码质量比较好,尽量复用它。但是不要修补很差劲的代码,应当重新编写。
尽量不要使用与具体硬件或软件环境关系密切的变量。
把编译器的选择项设置为最严格状态。
如果可能的话,使用 PC-Lint、LogiScope 等工具进行代码审查。
第 8 章 C++函数的高级特性
问题是在 C++/C 程序中,我们可以忽略函数的返回值 。在这种情况下,编译器和程序员都不知道哪个 Function 函数被调用。
所以只能靠参数而不能靠返回值类型的不同来区分重载函数。编译器根据参数为每个重载函数产生不同的内部标识符。例如编译器为 Eat 函数产生象eat_beef、_eat_fish、_eat_chicken 之类的内部标识符(不同的编译器可能产生不同风格的内部标识符) 。
1、