G1.魔数
所谓魔数和魔字符串就是指在代码中出现但没有解释的数字常量或字符串。如果在某个程序中你使用了魔数,那么在几个月(或几年)后你将很可能不知道它的含义是什么。(百度百科)
看下面这个例子:
class students{
T courses[10];
……
};
上面的数字10就是一个魔数,那它是代表一个最大限定值,还是实际大小,亦或其他?对于维护人员,会造成迷惑?
解决方案很简单:
class students{
enum {maxCourses = 10};
T courses[maxCourses];
……
};
这样就很清楚了,这里的10是一个最大限定值,同时枚举运行时不占用任何空间和资源。
G2.全局变量
全局变量可以“方便”地进行消息传递,但是它增加了程序耦合度,难以维护,可使用单例模式替代。
G3.混淆数组和初始值
int *p = new int(12);
delete []p;
这里的p是指向一个初始值为12的整数,并不是数组;
正确:
int *p = new int[12];
delete []p;
G4.运算符->
运算符->是二元运算符,左边的操作符是指针,右边操作符为类成员,但是重载->是一元操作符。
看下面这个例子:
class Ptr{
public:
Ptr(T *);
T* operator->();
……
};
Ptr p(new T);
p->f(); //p.operator->()->f();
这里的重载->并没有吃掉->
再来看一个例子:
class Ptr{
public:
Ptr(T *);
T* operator->();
……
};
class GoodPtr{
public:
GoodPtr(T *);
Ptr* operator->();
……
};
GoodPtr p(new T);
p->f(); //p.operator->()->operator->()->f();
G5.切割(Slicing)
把派生类对象复制到基类对象中,将会出现切割。
class Person{……};
class Student: public person{……};
Person person;
Student student;
person = student;
person将会是student的person部分的副本,复制后person的行为会是Person的行为。而不是Student的行为。
通过引用或指针可以避免这个问题。
G6.隐式转换(Implicit Conversion)
看下面这个例子:
class String{
String(const char *);
……
};
String name(“kk”);
name = “JJ”; //Implicit Conversion
隐式转换往往使代码难以理解,且可能产生隐藏的错误。比较安全的做法是将构造函数声明为explicit。explicit关键字告诉编译器可以显示做转换,但不能做隐式转换。
G7.构造函数的初始化与赋值
构造函数要求合乎初始化要求的值都要初始化,然后进行赋值。一般情况下,在初始化列表中进行初始化而不是赋值,否则将增加代码大小和运行时间。
G8.基类构造函数调用虚函数?
当基类被构造时,不会调用派生类重写的该虚函数,因为此时对象还不是一个派生类的对象,所以如果 Base::Base()调用了虚函数 virt(),则 Base::virt() 将被调用,即使 Derived::virt()存在。
同样,当基类被析构时,对象已经不再是一个派生类对象了,所以如果 Base::~Base()调用了virt(),则 Base::virt()得到控制权,而不是重写的 Derived::virt() 。
试想,如果 Base::Base()调用了虚函数 virt(),这个规则使得 Base::virt()被调用。如果不按照这个规则,Derived::virt()将在派生对象的派生部分被构造之前被调用,此时属于派生对象的派生部分的某个成员对象还没有被构造,而 Derived::virt()却能够访问它。这将是灾难。
G9.基类的非虚函数调用虚函数?
这是可以的。有时(并非总是!)这是一个好主意。例如,假设所有Shape(图形)对象有一个公共的打印算法。但这个算法依赖于它们的面积并且它们都有不同的方法来计算面积。在这种情况下,Shape的area()方法必须是virtual的(可能是纯虚(pure-virtual)的),但Shape::print()可以在Shape中被定义为非虚(non-virtual)的,前提是所有派生类不会需要不同的打印算法。
void Shape::print() const
{
float a = this->area(); // area() 为纯虚
// …
}
G10.虚函数表的初始化
虚函数表的初始化发生在构造函数的调用过程中,在进入构造函数体前,如果存在虚函数,虚函数表被初始化。如果存在构造函数的初始化列表的话,初始化列表也会被执行(G7)。
G11. C++的动态绑定和静态类型
当你有一个对象的指针,而对象实际是该指针类型的派生类(例如:一个 Vehicle*指针实际指向一个Car 对象)。由此有两种类型:指针的(静态)类型(在此是Verhicle),和指向的对象的(动态)类型(在此是Car)。
静态类型意味着成员函数调用的合法性被尽可能早地检查:编译器在编译时。编译器用指针的静态类型决定成员函数调用是否合法。如果指针类型能够处理成员函数,那么指针所指对象当然能很好的处理它。例如,如果 Vehicle 有某个成员函数,则由于Car是一种Vehicle,那么Car 当然也有该成员函数。
动态绑定意味着成员函数调用的代码地址在最终时刻才被决定:基于运行时的对象动态类型。因为绑定到实际被调用的代码这个过程是动态完成的(在运行时),所以被称为“动态绑定”。动态绑定是虚函数导致的结果之一。
那么为什么虚函数能指向虚表的索引了,却不能指向真正的调用者?因为G10
G12.虚析构函数
当你可能通过基类指针删除派生类对象时。需要虚析构函数。
虚函数绑定到对象的类的代码,而不是指针/引用的类。如果基类有虚析构函数,delete basePtr时(即基类指针),继承类对象类型的析构函数被调用,而不是该指针的类型的析构函数。
类应该有虚析构函数,除非这个类没有虚函数。原理:如果有虚函数,说明你想通过基类指针来使用派生对象,并且你所可能做的事情之中,可能包含了调用析构函数(通常通过delete隐含完成)。
G13.虚函数的开销
编译器为每个含有至少一个虚函数的类创建一个v-table。例如,如果Cirle类有虚函数ddraw()、move() 和 resize(),那么将有且只有一个和Cricle类相关的v-table,即使有一大堆Circle对象。并且每个 Circle对象的 v-poiner将指向 Circle的这个 v-table。该 v-table自己有指向类的各个虚函数的指针。例如,Circle 的v-table 会有三个指针:一个指向Circle::draw(),一个指向 Circle::move(),还有一个指向Circle::resize()。
在分发一个虚函数时,运行时系统跟随对象的 v-pointer找到类的 v-table,然后跟随v-table中适当的项找到方法的代码。
以上技术的空间开销是存在的:每个对象一个额外的指针(仅仅对于需要动态绑定的对象),加上每个方法一个额外的指针(仅仅对于虚方法)。时间开销也是有的:和普通函数调用比较,虚函数调用需要两个额外的步骤(得到v-pointer的值,得到方法的地址)。由于编译器在编译时就通过指针类型解决了非虚函数的调用,所以这些开销不会发生在非虚函数上。