合理地使用断言
一个断言,它越“笨”,被触发时所能带来的信息就越多,并且也越有价值。这是因为根据信息的理论,事件中的信息随着事件发生的概率的提高而减少。某个assert越不可能触发,当它触发时带来的信息也就越多。
断言的使用规则
- 使用断言捕捉不应该发生的情况,不要混淆非法情况和错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
- 在函数的入口处,使用断言检查参数的有效性。
- 在编写函数时,要进行反复考查,并且自问:”我打算做哪些假定?”一旦确定了假定,就要使用断言对假定进行检查。
- 很多类的书籍中都鼓励程序员进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情发生了,则要使用断言进行报警。
注意:断言不应该用于检查可能失败的函数返回值,比如malloc动态内存分配函数,创建窗口的函数、启动线程的函数。但是你能够使用断言来保证API如文档中所写的那样工作。
标准包含文件中的assert函数的工作流程是在标准错误输出流中显示一条错误信息并且通过调用abort()来中断程序。但是各个编译器其实现的方式也不尽一致。
有时,为了达到标准assert无法达到的目的,必须自己实现一个assert函数,当然这其中的原因很多。例如,你可能觉得用abort()终结程序过于粗鲁。很多情况下,你可能要忽略一个特殊的断言,因为你觉得它是无害的。
某些操作系统和调试器允许用源代码来追踪发生的问题,同样在这种情况下,你不需要abort(),相反你这时应该有选择是否继续跟踪程序代码的自由。
虚函数的实现原理
c++的编译器应该是保证虚函数表的指针存在于对象实例中的最前面的位置(为了保证取得虚函数表的最高性能——如果有多层继承或是多重继承的情况下)。
#include<iostream>
using namespace std;
class BASE{
public :
virtual void f() { cout<<"BASE::f"<<endl;
}
private :
int value;
};
typedef void (*Fun)(void); //前面的typedef不能删,至少在devc++下是这样的
int main(){
int a=3;
int* ptr=&a;
cout<<"测试一个变量转换成指针类型后它的值的情况" <<endl;
//一个变量强制类型转换成指针类型后,输出的值仍是这个变量内存空间里的值,
//强制转换成指针类型只是对这个内存空间中的值的含义进行了改变。
cout<<&a<<"\t"<<(int*)a<<"\t"<<ptr<<endl;
//cout<<*(int*)a<<endl; //这句会报错,因为访问的地址可能是系统保留地址。
BASE b;
Fun pFun;
cout<<"虚函数地址: " <<(int*)(&b)<<endl;
cout<<hex<<*(int*)(&b)<<endl; //输出的值与下面在前面再加上(int*)的效果相同
cout<<"虚函数表中第一个函数的地址"<<(int*)*(int*)(&b)<<endl;
pFun=(Fun)*(int*)*(int*)(&b);
pFun();
// *pFun();
return 0;
}
执行结果:
无虚函数覆盖虚函数表的特点如下:
- 虚函数按照其声明顺序放于表中
- 基类的虚函数在派生类的虚函数前面。
含虚函数覆盖虚函数表特点如下:
- 覆盖的f()函数被放到虚函数表中原来父类虚函数的位置。
- 没有被覆盖的函数,在虚函数中没有任何变化。
多重继承无虚函数覆盖虚函数表特点如下:
- 每个父类都有自己的虚表。
- 子类的成员函数被放到了第一个父类的,(所谓的第一个父类是按照声明顺序来判断的)。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
虚函数的安全缺陷
- 通过父类型的指针无法访问子类自己的虚函数。因为多态也是要基于函数重载的。任何妄图使用父类指针调用子类中的未覆盖的父类的成员函数的行为都会被编译器视为非法。但在运行时,可以通过指针的方式访问虚函数表来达到违反c++语义的行为。(关键是要理解虚函数表以及函数指针的概念)。
- 通过巧妙的机制可以访问non-public 的虚函数。 如果父类的虚函数是priavte或是protected的,但是这些非public 的虚函数同样会存在于虚函数表中,所以我们同样可以访问虚函数表的方式来访问这些non-public 的虚函数。