C++继承,虚,转换规则探究

以下讨论的东西都是在VS2005下跑出来的,如果想知道别的编译器规则,请照跑一遍。以下是类定义,函数内容为打印出当前函数名称,所以就不再贴了。
class Base
{
public:
Base();
Base(const Base & o);
virtual ~Base();
virtual Base & operator = (const Base & o);

void function1();
virtual void function2();
void function3();
virtual void function4();
//virtual void function5();
virtual void function6();
};
class Derive : public Base
{
public:
Derive();
Derive(const Derive & o);
virtual ~Derive();
virtual Derive & operator = (const Derive & o);

void function1();
virtual void function2();
virtual void function3();
void function4();
//compiler error
//int function5();
protected:
virtual void function6();
public:
};
首先我们讨论继承下的构造/析构顺序。
pa = dynamic_cast(new Derive ());
delete pa;
Base::Base
Derive::Derive
Derive::~Derive
Base::~Base
关于这段代码多说两句,如果我们把class Derive : public Base中的public删除,就会出现C2243错误,看来默认是私有继承。
先是基类构造,然后是继承类构造。先是继承类析够,然后是基类析够。然后我们将virtual ~Base();的virtual删除,结果就变成了。
Base::Base
Derive::Derive
Base::~Base
注意继承类的析构没了。所以如果你打算让人继承你的类,记得将类的析构改成virtual,否则他怎么写析构都不会被调用的。
然后是虚函数继承。
pa->function1 ();
pa->function2 ();
pa->function3 ();
pa->function4 ();
结果是这样。
Base::function1
Derive::function2
Base::function3
Derive::function4
Derive::function6
看来,虚特性出来不出来完全看基类。注意到上面的function5么?假设你继承了一个类,打算写一个函数,和基类里面的某个虚函数具有一样的名称和参数,但是返回不一样。嘟嘟~~抱歉,编译器错误。而且注意function6,即使在继承类中声明说这是保护函数,也可以通过公开的基类函数的虚特性进行调用。
下面我们要说一下拷贝构造函数,这不可避免的要说到定义。
Derive::Derive(const Derive & o)
{
printf (“Derive::Derive copy constructer\n”);
}
猜猜这个会出什么结果?
Base::Base
Derive::Derive copy constructer
要是经常看我blog的人就不会意外,继承类的拷贝构造函数调用的是基类的普通构造函数。如果你打算让基类也拷贝构造,那这么做。
Derive::Derive(const Derive & o):Base (o)
{
printf (“Derive::Derive copy constructer\n”);
}
然后是拷贝构造函数的使用时机。运行代码如下,我们逐步分析。
Base ta = *pa;
Base::Base copy constructer
Base::~Base
当对象声明时,如果加一个=,则以=后的对象来构造当前对象,这是拷贝构造的第一个用法。
Derive tb = *static_cast(pa);
Base::Base copy constructer
Derive::Derive copy constructer
Derive::~Derive
Base::~Base
当然,如果我们声明继承类的时候,一样拷贝构造。
//compiler error
//Derive tc = ta;
当我们试图用基类构造继承类的时候,理所当然的,出错了。
void test1 (Base &)
{
printf (“test1\n”);
}
test1(*pa);
输出:test1
如果我们以一个对象调用的时候,如果是引用,当然是不拷贝的。
void test2 (Base)
{
printf (“test2\n”);
}
test2(*pa);
Base::Base copy constructer
test2
Base::~Base
如果是直接调用,首先是拷贝构造,然后调用,最后析构。
Base& test3 ()
{
printf (“test3\n”);
return Base ();
}
pb = &test3();
test3
Base::Base
Base::~Base
当返回对象引用的时候,只有很正常的构造和析构。
Base test4 ()
{
printf (“test4\n”);
return Base ();
}
pb = &test4();
test4
Base::Base
Base::~Base
返回对象本身的话,哎,怎么会这样?
熟悉语言的应该看出来了,return Base ();的时候,先跑了一次构造,建立在栈里面,返回的时候要copy到堆中。拷贝构造呢?
这就是传说中的返回构造优化拉,直接构造在堆上面,省掉一次copy,下面我们看看原始的状态。
Base& test5 ()
{
Base b;
printf (“test5\n”);
return b;
}
pb = &test5();
Base test6 ()
{
Base b;
printf (“test6\n”);
return b;
}
pb = &test6();
Base::Base
test5
Base::~Base
Base::Base
test6
Base::Base copy constructer
Base::~Base
Base::~Base
大家看到了?5的时候先构造,再传回,和返回对象引用的时候行为一致。6的时候可没有返回构造优化,于是先构造,然后拷贝。删除的时候先删除原始对象,再删除拷贝对象,大家可以自行证实这点。
我们再修改上面的调用为下面的。
Base td = test5();
Base::Base
test5
Base::~Base
Base::Base copy constructer
Base::~Base
首先是5的构造,析构,然后才是td的拷贝构造,析构。这个顺序,熟悉语言的人应该感觉到奇怪了吧。按照推论,应当是先拷贝再析构的。如果你这么觉得,还是先看完下面的东西吧。
Base te = test6();
Base::Base
test6
Base::Base copy constructer
Base::~Base
Base::~Base
这才是预计的顺序。注意,这里并没有调用两次拷贝构造。虽然贝壳并不了解机制,不过估计又是一种返回构造优化。
5中例子觉得迷惑的人,不妨在拷贝构造里面打个断点,看看你copy的对象是什么,无效对象!!!!
返回引用的情况下,一旦返回对象的生命周期结束了,返回的数据就无法保证有效。因此返回局部对象是非常危险的,唯一的里外就是3例子中在返回的时候构造一个新的对象而引发的返回构造优化。
下面是拷贝构造和operator =的区别和调用时间。
Base ya = *pa;
Base yb;
yb = *pa;
Base::Base copy constructer
Base::Base
Base::operator =
Base::~Base
Base::~Base
上面一个是拷贝构造,下面一个是普通构造加operator =。
最后是全部的定义和源码,类的定义参考最上面的。
void test1 (Base &)
{
printf (“test1\n”);
}
void test2 (Base)
{
printf (“test2\n”);
}
Base& test3 ()
{
printf (“test3\n”);
return Base ();
}
Base test4 ()
{
printf (“test4\n”);
return Base ();
}
Base& test5 ()
{
Base b;
printf (“test5\n”);
return b;
}
Base test6 ()
{
Base b;
printf (“test6\n”);
return b;
}
int _tmain(int argc, _TCHAR* argv[])
{
Base *pa, *pb;

pa = dynamic_cast(new Derive ());

// test inherit function rule
//pa->function1 ();
//pa->function2 ();
//pa->function3 ();
//pa->function4 ();
//pa->function6 ();

//test copy constructer
//pb = dynamic_cast(new Derive (*static_cast(pa)));
//delete pb;
//Base ta = *pa;
//Derive tb = *static_cast(pa);
//compiler error
//Derive tc = ta;
//test1(*pa);
//test2(*pa);
//pb = &test3();
//pb = &test4();
//pb = &test5();
//pb = &test6();
//Base td = test5();
//Base te = test6();

//diffrence between copy cotr and operator =
//Base ya = *pa;
//Base yb;
//yb = *pa;

delete pa;
return 0;
}

继承函数的拷贝构造

    从基类继承一个子类,基类有一个拷贝构造函数,子类重载了一个。那么在子类拷贝构造的时候会自动调用基类的拷贝构造函数吗?
    答案是不会,自动调用的是基类的构造函数。
    子类中如果需要调用基类的拷贝构造函数,需要这样用。
    D (const D & o): B (o){…}

RTTI的几个应用

    贝壳最近研究了下RTTI,发现几个有意思的事情。
    dynamic_cast的应用。
    dynamic_cast可以将一个指针的类型试图转换为指定的类型,是否能转换要看当前指针的动态类型是否是指定类型的子类,而不管指针的声明类型。当失败时返回NULL,因此可以用来识别一个指针的动态类型是否是某个类的子类。
    typeid的应用。
    typeid可以获得某个类的类型信息,最主要的就是name。指明了当前是哪个类,这在串行化中是必要的信息。当判断一个指针的动态类型是否就是某个类的时候可以这样typeid(*p)==typeid(class)。

继承类静态对象虚拟化

    其实这个标题不准确,准确的说,应该是继承类拥有自己的基类静态对象。
    我们知道,类中的静态对象本质上是全局变量,不过名字在类命名空间里面。如果类B有静态对象S,D继承了B(先按照public继承讨论,其他其实也一样)。那么在D里面访问S的时候,其实是访问的B命名空间里面的静态对象S。验证代码如下:
class           B {
  protected:
    static int      S;
};
int B::S = 0;
class D:public B {
  public:
    void print () {
        printf ("%d\n", S);
    };
};
class E:public B {
  public:
    static void rewrite () {
        S = 1;
    };
};
int _tmain (int argc, _TCHAR * argv[]){
    D d;
    d.print ();
    E::rewrite ();
    d.print ();
    return 0;
}
    上例中可以看到,两个继承类,其实都是将父类的命名空间导入而已。假定我们要使得每类专有一个静态成员,例如我们要计算每个类的生成对象个数,怎么办呢?
    如果不用继承,我们可以在每个类里面加一个静态成员。然后在构造函数中加1,析构中减1。但是如果我们想把这个功能放到基类中,事情就麻烦了。因为所有类从同一个基类派生,我们算出来的其实是所有从基类继承的类的总对象生成个数。
    当然,我们可以用实现的方法来做。把所需要的功能抽离出来,放到一个单独的计数类中。然后构造的时候调用加1,析构的时候减1。听起来很蠢,那是因为例子容易的关系。比较复杂的时候,这样抽象相对简单的。计数类可以用于多个类,实现了代码重用。但是仍旧没有解决关键问题,怎么让继承类特化基类的静态对象?(虚拟化和特化的意思差不多,就是针对具体对象使用具体方法)
    答案最后被我在More Effective C++中找到了,正确的方法不是寻找一个特化的方法,而是继承不同的基类,使用同一套代码。既然是不同基类,怎么具备同一套代码呢?想到了吧,模版。
template
class           B {
  protected:
    static int      S;
};
class D:public B {
  public:
    void print () {
        printf ("%d\n", S);
    };
};
int B::S = 0;

class E:public B {
  public:
    static void rewrite () {
        S = 1;
    };
};
int B::S = 0;

int _tmain (int argc, _TCHAR * argv[])
{
    D d;
    d.print ();
    E::rewrite ();
    d.print ();
    return 0;
}
    看到了?每个继承类拥有了自己的静态成员。如果我们把上面的例子中,S改成ID。就可以做到RTTI了(只是还需要很多技巧)。

基础类

    设计程序的时候,往往觉得怎么这么困难,基础类为什么不多提供些能力。现在风水轮流转,我开始设计基础类了。
    站着说话不腰疼,设计基础类才发现基础类这东西真不是人做的。调用上讲究非常多。要返回引用呢?还是值?是需要const呢,还是不能const。返回的时候是一次拷贝构造呢?还是两次。算子需要不需要设计成friend,重复代码能不能消除。着重效率还是安全性,线程安全不安全。这些问题真是活活逼死人啊。
    现在正在设计实现一下几个类,有兴趣的可以一起来研究。
LargeInteger 超大整数实现 计算RSA的时候很有用,考虑在内部实现一些有用的算法
Matrix模板类,容器类 矩阵实现 设计的时候就考虑到内部容纳的不一定是数据,也可能是字符串或者超大数
vector2D 两维矢量 专门针对平面计算优化
Line 两维线 专门针对平面计算优化