class Ca
{
public:
virtual void fn(void)
{
cout<<"Ca"<<endl;
}
};
class Cb :public Ca
{
public:
~Cb(void)//变为virtual ~Cb(void)
{
fn();
}
void fn(void)
{
cout<<"Cb"<<endl;
}
};
void main()
{
Cb* p = new Cb;//变为Ca* = new Cb;向上映射
delete p;
}
输出:Cb
再照注释改一下,使p向上映射到Ca就没有输出了,在~Cb(void)前加virtual也没输出,怎么回事?
47 个解决方案
#1
明白了,Ca中没有定义虚析构,相当于在派生类中增加新的虚函数,所以调用没结果.
不过我把Ca* p = new Cb;改为Cb b;
Ca* p = &b;
运行时出错是为什么?
另外在上面更改过可正常运行的程序中在析构函数中调用了虚函数,但还是正确地运行了.我想问问假如要在虚析构中加入一条语句比如:virtual ~Cb(){ @ fn(); @ }析构对象在何处发生?第一个@处还是第二个@?应该是第二个吧?如果fn();是虚函数,那么删除vtable又在何处?
不过我把Ca* p = new Cb;改为Cb b;
Ca* p = &b;
运行时出错是为什么?
另外在上面更改过可正常运行的程序中在析构函数中调用了虚函数,但还是正确地运行了.我想问问假如要在虚析构中加入一条语句比如:virtual ~Cb(){ @ fn(); @ }析构对象在何处发生?第一个@处还是第二个@?应该是第二个吧?如果fn();是虚函数,那么删除vtable又在何处?
#2
谁能写一段在析构中调用虚函数由于vtable以被更改导致运行结果和正常时(也就是一般的行为)不一样的?
#3
虚函数只能被用来继承,所以加了virtual后就调用默认析构函数,所以就不输出了
#4
我会不断给帖子加分的
#5
还有我对<C++编程思想>中第14章293页的一句话极不理解:假设虚机制在析构函数中使用,那么调用下面这样的虚函数是可能的:这个函数是在继承层次中比当前的析构函数"更靠外"(更晚派生的).
我总觉得这句话说反了,更晚派生,构造的也就晚,析构的也就早.那么当前的析构函数所在对象相对析构的晚,它所调用的已经析构了呀.哪位大侠给讲讲?
我总觉得这句话说反了,更晚派生,构造的也就晚,析构的也就早.那么当前的析构函数所在对象相对析构的晚,它所调用的已经析构了呀.哪位大侠给讲讲?
#6
怎么没人理??
#7
destructor运行结束之后才能把对象free掉,要不然就乱套了。所以调用虚函数不会有什么问题。
你对构造和析构的理解很成问题。构造和析构一般只能理解为对数据成员的构造和析构。所有的成员函数,无论是不是虚的,都不存在被构造的这么一回事。但是虚函数表的指针(一般都用这个办法实现虚函数)必须被初始化,这是一个程序员不必显示地实现的操作。
你最好看一些如何实现高级语言的文章。总是看那些理论只能让人的头脑越来越晕。
你对构造和析构的理解很成问题。构造和析构一般只能理解为对数据成员的构造和析构。所有的成员函数,无论是不是虚的,都不存在被构造的这么一回事。但是虚函数表的指针(一般都用这个办法实现虚函数)必须被初始化,这是一个程序员不必显示地实现的操作。
你最好看一些如何实现高级语言的文章。总是看那些理论只能让人的头脑越来越晕。
#8
我说的构造是说每个派生类都会修改vtable,构造对象时vptr便指向自己那个类的vtable.
aclass
|construct
|
|aobject|
| vptr--|--->|avtable|
|avfun |
bclass :public aclass
|construct
|
|bobject|
| vptr--|--->|bvtable |
|bvfun |
析构时bvtable便首先无效.在b对象的析构函数调用bvfun应该没问题(你说删除对象在最后).算了,我晕了.其实我觉得在析构调用虚函数不可能会出问题,如果想在a对象中(不管是不是析构)调用bvfun都是不可能的,这有逻辑错误,违反常规.那么<C++编程思想>说的在析构中调用虚函数失败是什么样的代码呢?
aclass
|construct
|
|aobject|
| vptr--|--->|avtable|
|avfun |
bclass :public aclass
|construct
|
|bobject|
| vptr--|--->|bvtable |
|bvfun |
析构时bvtable便首先无效.在b对象的析构函数调用bvfun应该没问题(你说删除对象在最后).算了,我晕了.其实我觉得在析构调用虚函数不可能会出问题,如果想在a对象中(不管是不是析构)调用bvfun都是不可能的,这有逻辑错误,违反常规.那么<C++编程思想>说的在析构中调用虚函数失败是什么样的代码呢?
#9
有这样一段代码:
#include <iostream>
class A
{
public:
~A (void);
virtual void FuncA (void)
{
std::cout<<"aaa";
}
};
class B: public A
{
public:
virtual void FuncA(void)
{
std::cout<<"bbb";
}
};
A::~A (void)
{
FuncA ();
}
int main()
{
B * vb = new B;
delete vb;
}
会输出aaa
创建B对象b的过程:
1. A的构造,vptr指向A的vtable
2. B的构造,vptr指向B的vtable,this指向b
3. B的析构,析构完成后vptr指向A的vtable,this指向b的a部分
如果在这里调用B的vfun便会失败,那么如何这样调用呢?就是在A的析构里!!由于~A()运行时,B部分已被析构,this指向的是A的部分,查vtable便会调出A的vfun
大概清楚了,头也彻底晕了.
发表自己看法者均有分!
#include <iostream>
class A
{
public:
~A (void);
virtual void FuncA (void)
{
std::cout<<"aaa";
}
};
class B: public A
{
public:
virtual void FuncA(void)
{
std::cout<<"bbb";
}
};
A::~A (void)
{
FuncA ();
}
int main()
{
B * vb = new B;
delete vb;
}
会输出aaa
创建B对象b的过程:
1. A的构造,vptr指向A的vtable
2. B的构造,vptr指向B的vtable,this指向b
3. B的析构,析构完成后vptr指向A的vtable,this指向b的a部分
如果在这里调用B的vfun便会失败,那么如何这样调用呢?就是在A的析构里!!由于~A()运行时,B部分已被析构,this指向的是A的部分,查vtable便会调出A的vfun
大概清楚了,头也彻底晕了.
发表自己看法者均有分!
#10
原先我的想法有误,想产生这种情况的话必须是一个对象,且为派生类的对象.若是两个对象一个基类的一个派生类的是无论如何也不会有这样的问题的.最近在看C++对象模型,希望对我有帮助
btw
怎么就我一个在说呀,也不知我上面说的对不对,大家随便发表些看法嘛,都有分的.
btw
怎么就我一个在说呀,也不知我上面说的对不对,大家随便发表些看法嘛,都有分的.
#11
方法是操作对象的,如果对一个对象已经被解构,在其上调用任何方法都是无效的。设想类B派生于A,如果当B的解构函数被调用,从语义上就意味着B不同于A的部分已经消失,这时,调用不同于A的方法将操纵不存在的对象,有可能引起未知的错误,请看下例。
class A{
public:
virtual ~A()
{
fun();
}
virtual void fun()
{
//do nothing
}
};
class B: public A
{
public:
char * p;
B()
{
p= new char[100];//allocate memory
}
virtual ~B()
{
delete[] p;
}
virtual void fun()
{
p[10]=0;//you will got an access violation error if call fun() after
//destructor called.
}
};
class A{
public:
virtual ~A()
{
fun();
}
virtual void fun()
{
//do nothing
}
};
class B: public A
{
public:
char * p;
B()
{
p= new char[100];//allocate memory
}
virtual ~B()
{
delete[] p;
}
virtual void fun()
{
p[10]=0;//you will got an access violation error if call fun() after
//destructor called.
}
};
#12
up
#13
一些不是很成熟的观点共同探讨:
对于理解对象模型,我觉得"正确的代码(对象方法)工作在正确的数据(对象的内存布局/对象数据)上"这句话可以作为理解对象模型很多方面的有用的指导,
在destructor中调用virtual/non-virtual function时对象模型应该保证该function只能工作在"现在"的对象上,那么该function只能是"现在"的对象所拥有的方法(包括继承而来的)。
如: class A: public x{...}; class B: public A{...}; 那么当执行到A::~A()时,其中的调用的任何virtual/non-virtual function(设为foo)只能是类A的方法(包括继承而来的方法),程序能够执行到A::~A()只能说明当前工作的对象是类型为A的对象,没有理由认为这时还可以执行B类的方法(它是为类型为B的对象所设计的,而不是为类型为A的对象所设计的)。
使用对象名称引用方法名的方式调用方法形如:a.foo();(a类型为A) 只能执行A的方法(包括继承而来的),不管foo()是不是virtual。
使用类型指针引用方法名的方式调用方法形如:pa->foo();还是只能执行pa所指对象"第一次产生时"所属类型的方法(包括继承而来的),指针只是程序员对对象类型的泛化(is-a的抽象),指针的调整并不能改变所指对象"第一次产生时"的类型。不要受指针类型的欺骗,应透过指针看对象。
对于理解对象模型,我觉得"正确的代码(对象方法)工作在正确的数据(对象的内存布局/对象数据)上"这句话可以作为理解对象模型很多方面的有用的指导,
在destructor中调用virtual/non-virtual function时对象模型应该保证该function只能工作在"现在"的对象上,那么该function只能是"现在"的对象所拥有的方法(包括继承而来的)。
如: class A: public x{...}; class B: public A{...}; 那么当执行到A::~A()时,其中的调用的任何virtual/non-virtual function(设为foo)只能是类A的方法(包括继承而来的方法),程序能够执行到A::~A()只能说明当前工作的对象是类型为A的对象,没有理由认为这时还可以执行B类的方法(它是为类型为B的对象所设计的,而不是为类型为A的对象所设计的)。
使用对象名称引用方法名的方式调用方法形如:a.foo();(a类型为A) 只能执行A的方法(包括继承而来的),不管foo()是不是virtual。
使用类型指针引用方法名的方式调用方法形如:pa->foo();还是只能执行pa所指对象"第一次产生时"所属类型的方法(包括继承而来的),指针只是程序员对对象类型的泛化(is-a的抽象),指针的调整并不能改变所指对象"第一次产生时"的类型。不要受指针类型的欺骗,应透过指针看对象。
#14
虚析构的机制是----蜕变
假设类的层次是
Ca->Cb->Cc
那么Cc析构以后调用~Cc(),对象蜕变为Cb,Cb析构调用~Cb(),对象蜕变为Ca,Ca析构调用~Ca();保证了对象完整的被销毁。
假设类的层次是
Ca->Cb->Cc
那么Cc析构以后调用~Cc(),对象蜕变为Cb,Cb析构调用~Cb(),对象蜕变为Ca,Ca析构调用~Ca();保证了对象完整的被销毁。
#15
其实关于这个问题:为什么不能在析构函数中调用虚函数会失败。我想书上说的是对的。
理由就是虚函数实现晚捆绑。而析构函数的执行是从远到近。所以如果我们在一个近端的
析构函数想要去CALL远端的虚函数会怎么样?远端的已经被释放掉了。所以。。。
继续讨论吧。其实这一章我也有几个地方不懂。
理由就是虚函数实现晚捆绑。而析构函数的执行是从远到近。所以如果我们在一个近端的
析构函数想要去CALL远端的虚函数会怎么样?远端的已经被释放掉了。所以。。。
继续讨论吧。其实这一章我也有几个地方不懂。
#16
你说的这句“那么调用下面这样的虚函数是可能的:这个函数是在继承层次中比当前的析构函数"更靠外"(更晚派生的).“
我想你理解错了。它的意思是这样:
class a;
class b:public a;
{析构中想调用c中的虚拟函数。}
class c:public b;
这个就是他说的”有可能调用比当前层次更晚的函数“。而这个时候C已经释放掉了。所以。。
我想你理解错了。它的意思是这样:
class a;
class b:public a;
{析构中想调用c中的虚拟函数。}
class c:public b;
这个就是他说的”有可能调用比当前层次更晚的函数“。而这个时候C已经释放掉了。所以。。
#17
对了。这位兄台。你对”继承和Vtable“这个地方理解的怎么样。我总觉得有点不对。。。
大家一起多讨论讨论。把这个搞搞清楚吧。
大家一起多讨论讨论。把这个搞搞清楚吧。
#18
to:xrbeck
你回的第二个帖子我还是不懂,我觉得有点文字游戏.你举的例子b的析构调用比它"更晚派生"的c的虚函数.结果失败.而书上却说"调用下面这样的虚函数是可能的"可能吗???
你有什么问题说出来好了,大家一起讨论.
你回的第二个帖子我还是不懂,我觉得有点文字游戏.你举的例子b的析构调用比它"更晚派生"的c的虚函数.结果失败.而书上却说"调用下面这样的虚函数是可能的"可能吗???
你有什么问题说出来好了,大家一起讨论.
#19
呵呵。我想不是什么文字游戏。你看看他说的”因为有可能会出现这种情况。。“
(而恰恰这种情况是不安全的)。所以”只有成员函数的本地版本被调用。而虚机制被忽略“。。你看看他说的”因为析构函数从外到内被调用。比如我举的哪个例子。
B想去CALL C中的虚拟函数。这个就是他后面说的那句“实际上被调用的函数可能操作在
已被删除的对象上”。。这个时候C已经被删除掉了?不是吗?所以在析构函数中为了防止
这种情况。就忽略了虚机制。而只保证本地版本。比如你在B中调用虚拟函数。只能是
调用自己的。。这是我对这个问题的看法。。呵呵。
我说的疑问如下:(继承和Vtable)
书上说基类指针只能调用自己的。不能调用派生类中重新定义的虚函数。比如它举的这个
例子:cout<<B[1]->shift(3);这个会出错。因为shift是在基类中没有的。。。
我想或许编译器可以发现这个是错误。可是难道这个真的是错误吗?我们知道
(我拿它例子说)
在new derived(7)这句的时候。由派生类的构造函数隐性的生成了vtable.也就是说这个
指向基类的指针其实包括了派生类的vtable.那么为什么我们不能知道去调用shift这个函数呢.
因为其实vtable里面已经含有了这个函数的地址.而且是可以找到的.
那为什么会出错?
(而恰恰这种情况是不安全的)。所以”只有成员函数的本地版本被调用。而虚机制被忽略“。。你看看他说的”因为析构函数从外到内被调用。比如我举的哪个例子。
B想去CALL C中的虚拟函数。这个就是他后面说的那句“实际上被调用的函数可能操作在
已被删除的对象上”。。这个时候C已经被删除掉了?不是吗?所以在析构函数中为了防止
这种情况。就忽略了虚机制。而只保证本地版本。比如你在B中调用虚拟函数。只能是
调用自己的。。这是我对这个问题的看法。。呵呵。
我说的疑问如下:(继承和Vtable)
书上说基类指针只能调用自己的。不能调用派生类中重新定义的虚函数。比如它举的这个
例子:cout<<B[1]->shift(3);这个会出错。因为shift是在基类中没有的。。。
我想或许编译器可以发现这个是错误。可是难道这个真的是错误吗?我们知道
(我拿它例子说)
在new derived(7)这句的时候。由派生类的构造函数隐性的生成了vtable.也就是说这个
指向基类的指针其实包括了派生类的vtable.那么为什么我们不能知道去调用shift这个函数呢.
因为其实vtable里面已经含有了这个函数的地址.而且是可以找到的.
那为什么会出错?
#20
可编译器没那么聪明.你说的对,其实vtable里面已经含有了这个函数的地址.但是找得到吗?不一定,这会产生混乱.vptr调用vtable中的虚函数是通过诸如vptr+n这样的方法,因为所有的vtable有相同的顺序.
to be continue
我先吃饭了,不然我老娘K死我
to be continue
我先吃饭了,不然我老娘K死我
#21
找的到吗?找不到吗?呵呵。我想如果我坚持说“哎。编译器为什么这么笨。下面有个
函数指针也不知道。“那这个有点象钻牛角尖了。。呵呵。
不过我想应该是不知道的。因为正如你所说。在基类的vtable不包括这一项。所以即便
下面就是我们要的函数指针。它也不知道。所以程序中要加显示的转换:(derived *)b[1]
来转换。其实这句话的意思大概就是跟编译器说:喂。你的下面有个函数指针是我想要的。
这样或许我们才能正确的去使用吧。。恩。
你上边的那个问题。我刚写了一下。不知道能不能正确的说明这个问题:
#include "iostream.h"
#include "conio.h"
#include "stdio.h"
class base
{
private:
int i;
public:
base(){cout<<"base default constructor"<<endl;}
virtual ~base()
{show();}
virtual void show(){cout<<"virtual in base destructor"<<endl;}
};
class derived:public base
{
private:
int i;
public:
derived(){cout<<"derived default constructor"<<endl;}
~derived()
{}
void show(){cout<<"virtual in derived destructor"<<endl;}
};
int main(int argc, char* argv[])
{
base *temp=new derived;
delete temp;
getch();
return 0;
}
这里你会发现base的show就是本地版本。而不能因为temp是个派生类。而show是个虚拟函数
它就会去调用派生类的虚拟函数吧。。这个是我的看法。。恩。
对于它说的“对象切片”。你怎么看。他说会让派生类切成基类的大小。可是基类的私有成员
它会如何处理?。。。
函数指针也不知道。“那这个有点象钻牛角尖了。。呵呵。
不过我想应该是不知道的。因为正如你所说。在基类的vtable不包括这一项。所以即便
下面就是我们要的函数指针。它也不知道。所以程序中要加显示的转换:(derived *)b[1]
来转换。其实这句话的意思大概就是跟编译器说:喂。你的下面有个函数指针是我想要的。
这样或许我们才能正确的去使用吧。。恩。
你上边的那个问题。我刚写了一下。不知道能不能正确的说明这个问题:
#include "iostream.h"
#include "conio.h"
#include "stdio.h"
class base
{
private:
int i;
public:
base(){cout<<"base default constructor"<<endl;}
virtual ~base()
{show();}
virtual void show(){cout<<"virtual in base destructor"<<endl;}
};
class derived:public base
{
private:
int i;
public:
derived(){cout<<"derived default constructor"<<endl;}
~derived()
{}
void show(){cout<<"virtual in derived destructor"<<endl;}
};
int main(int argc, char* argv[])
{
base *temp=new derived;
delete temp;
getch();
return 0;
}
这里你会发现base的show就是本地版本。而不能因为temp是个派生类。而show是个虚拟函数
它就会去调用派生类的虚拟函数吧。。这个是我的看法。。恩。
对于它说的“对象切片”。你怎么看。他说会让派生类切成基类的大小。可是基类的私有成员
它会如何处理?。。。
#22
条款14: 确定基类有虚析构函数
有时,一个类想跟踪它有多少个对象存在。一个简单的方法是创建一个静态类成员来统计对象的个数。这个成员被初始化为0,在构造函数里加1,析构函数里减1。(条款m26里说明了如何把这种方法封装起来以便很容易地添加到任何类中,"my article on counting objects"提供了对这个技术的另外一些改进)
设想在一个军事应用程序里,有一个表示敌人目标的类:
class enemytarget {
public:
enemytarget() { ++numtargets; }
enemytarget(const enemytarget&) { ++numtargets; }
~enemytarget() { --numtargets; }
static size_t numberoftargets()
{ return numtargets; }
virtual bool destroy(); // 摧毁enemytarget对象后
// 返回成功
private:
static size_t numtargets; // 对象计数器
};
// 类的静态成员要在类外定义;
// 缺省初始化为0
size_t enemytarget::numtargets;
这个类不会为你赢得一份*防御合同,它离国防部的要求相差太远了,但它足以满足我们这儿说明问题的需要。
敌人的坦克是一种特殊的敌人目标,所以会很自然地想到将它抽象为一个以公有继承方式从enemytarget派生出来的类(参见条款35及m33)。因为不但要关心敌人目标的总数,也要关心敌人坦克的总数,所以和基类一样,在派生类里也采用了上面提到的同样的技巧:
class enemytank: public enemytarget {
public:
enemytank() { ++numtanks; }
enemytank(const enemytank& rhs)
: enemytarget(rhs)
{ ++numtanks; }
~enemytank() { --numtanks; }
static size_t numberoftanks()
{ return numtanks; }
virtual bool destroy();
private:
static size_t numtanks; // 坦克对象计数器
};
(写完以上两个类的代码后,你就更能够理解条款m26对这个问题的通用解决方案了。)
最后,假设程序的其他某处用new动态创建了一个enemytank对象,然后用delete删除掉:
enemytarget *targetptr = new enemytank;
...
delete targetptr;
到此为止所做的一切好象都很正常:两个类在析构函数里都对构造函数所做的操作进行了清除;应用程序也显然没有错误,用new生成的对象在最后也用delete删除了。然而这里却有很大的问题。程序的行为是不可预测的--无法知道将会发生什么。
c++语言标准关于这个问题的阐述非常清楚:当通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。这意味着编译器生成的代码将会做任何它喜欢的事:重新格式化你的硬盘,给你的老板发电子邮件,把你的程序源代码传真给你的对手,无论什么事都可能发生。(实际运行时经常发生的是,派生类的析构函数永远不会被调用。在本例中,这意味着当targetptr 删除时,enemytank的数量值不会改变,那么,敌人坦克的数量就是错的,这对需要高度依赖精确信息的部队来说,会造成什么后果?)
为了避免这个问题,只需要使enemytarget的析构函数为virtual。声明析构函数为虚就会带来你所希望的运行良好的行为:对象内存释放时,enemytank和enemytarget的析构函数都会被调用。
和绝大部分基类一样,现在enemytarget类包含一个虚函数。虚函数的目的是让派生类去定制自己的行为(见条款36),所以几乎所有的基类都包含虚函数。
如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。请看下面的例子,这个例子基于arm("the annotated c++ reference manual")一书的一个专题讨论。
// 一个表示2d点的类
class point {
public:
point(short int xcoord, short int ycoord);
~point();
private:
short int x, y;
};
如果一个short int占16位,一个point对象将刚好适合放进一个32位的寄存器中。另外,一个point对象可以作为一个32位的数据传给用c或fortran等其他语言写的函数中。但如果point的析构函数为虚,情况就会改变。
实现虚函数需要对象附带一些额外信息,以使对象在运行时可以确定该调用哪个虚函数。对大多数编译器来说,这个额外信息的具体形式是一个称为vptr(虚函数表指针)的指针。vptr指向的是一个称为vtbl(虚函数表)的函数指针数组。每个有虚函数的类都附带有一个vtbl。当对一个对象的某个虚函数进行请求调用时,实际被调用的函数是根据指向vtbl的vptr在vtbl里找到相应的函数指针来确定的。
虚函数实现的细节不重要(当然,如果你感兴趣,可以阅读条款m24),重要的是,如果point类包含一个虚函数,它的对象的体积将不知不觉地翻番,从2个16位的short变成了2个16位的short加上一个32位的vptr!point对象再也不能放到一个32位寄存器中去了。而且,c++中的point对象看起来再也不具有和其他语言如c中声明的那样相同的结构了,因为这些语言里没有vptr。所以,用其他语言写的函数来传递point也不再可能了,除非专门去为它们设计vptr,而这本身是实现的细节,会导致代码无法移植。
所以基本的一条是,无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
这是一个很好的准则,大多数情况都适用。但不幸的是,当类里没有虚函数的时候,也会带来非虚析构函数问题。 例如,条款13里有个实现用户自定义数组下标上下限的类模板。假设你(不顾条款m33的建议)决定写一个派生类模板来表示某种可以命名的数组(即每个数组有一个名字)。
template<class t> // 基类模板
class array { // (来自条款13)
public:
array(int lowbound, int highbound);
~array();
private:
vector<t> data;
size_t size;
int lbound, hbound;
};
template<class t>
class namedarray: public array<t> {
public:
namedarray(int lowbound, int highbound, const string& name);
...
private:
string arrayname;
};
如果在应用程序的某个地方你将指向namedarray类型的指针转换成了array类型的指针,然后用delete来删除array指针,那你就会立即掉进"不确定行为"的陷阱中。
namedarray<int> *pna =
new namedarray<int>(10, 20, "impending doom");
array<int> *pa;
...
pa = pna; // namedarray<int>* -> array<int>*
...
delete pa; // 不确定! 实际中,pa->arrayname
// 会造成泄漏,因为*pa的namedarray
// 永远不会被删除
现实中,这种情形出现得比你想象的要频繁。让一个现有的类做些什么事,然后从它派生一个类做和它相同的事,再加上一些特殊的功能,这在现实中不是不常见。namedarray没有重定义array的任何行为--它继承了array的所有功能而没有进行任何修改--它只是增加了一些额外的功能。但非虚析构函数的问题依然存在(还有其他问题,参见m33)
最后,值得指出的是,在某些类里声明纯虚析构函数很方便。纯虚函数将产生抽象类--不能实例化的类(即不能创建此类型的对象)。有些时候,你想使一个类成为抽象类,但刚好又没有任何纯虚函数。怎么办?因为抽象类是准备被用做基类的,基类必须要有一个虚析构函数,纯虚函数会产生抽象类,所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数。
这里是一个例子:
class awov { // awov = "abstract w/o
// virtuals"
public:
virtual ~awov() = 0; // 声明一个纯虚析构函数
};
这个类有一个纯虚函数,所以它是抽象的,而且它有一个虚析构函数,所以不会产生析构函数问题。但这里还有一件事:必须提供纯虚析构函数的定义:
awov::~awov() {} // 纯虚析构函数的定义
这个定义是必需的,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对~awov的调用,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来,最后还是得回去把它添上。
可以在函数里做任何事,但正如上面的例子一样,什么事都不做也不是不常见。如果是这种情况,那很自然地会想到将析构函数声明为内联函数,从而避免对一个空函数的调用所产生的开销。这是一个很好的方法,但有一件事要清楚。
因为析构函数为虚,它的地址必须进入到类的vtbl(见条款m24)。但内联函数不是作为独立的函数存在的(这就是"内联"的意思),所以必须用特殊的方法得到它们的地址。条款33对此做了全面的介绍,其基本点是:如果声明虚析构函数为inline,将会避免调用它们时产生的开销,但编译器还是必然会在什么地方产生一个此函数的拷贝。
有时,一个类想跟踪它有多少个对象存在。一个简单的方法是创建一个静态类成员来统计对象的个数。这个成员被初始化为0,在构造函数里加1,析构函数里减1。(条款m26里说明了如何把这种方法封装起来以便很容易地添加到任何类中,"my article on counting objects"提供了对这个技术的另外一些改进)
设想在一个军事应用程序里,有一个表示敌人目标的类:
class enemytarget {
public:
enemytarget() { ++numtargets; }
enemytarget(const enemytarget&) { ++numtargets; }
~enemytarget() { --numtargets; }
static size_t numberoftargets()
{ return numtargets; }
virtual bool destroy(); // 摧毁enemytarget对象后
// 返回成功
private:
static size_t numtargets; // 对象计数器
};
// 类的静态成员要在类外定义;
// 缺省初始化为0
size_t enemytarget::numtargets;
这个类不会为你赢得一份*防御合同,它离国防部的要求相差太远了,但它足以满足我们这儿说明问题的需要。
敌人的坦克是一种特殊的敌人目标,所以会很自然地想到将它抽象为一个以公有继承方式从enemytarget派生出来的类(参见条款35及m33)。因为不但要关心敌人目标的总数,也要关心敌人坦克的总数,所以和基类一样,在派生类里也采用了上面提到的同样的技巧:
class enemytank: public enemytarget {
public:
enemytank() { ++numtanks; }
enemytank(const enemytank& rhs)
: enemytarget(rhs)
{ ++numtanks; }
~enemytank() { --numtanks; }
static size_t numberoftanks()
{ return numtanks; }
virtual bool destroy();
private:
static size_t numtanks; // 坦克对象计数器
};
(写完以上两个类的代码后,你就更能够理解条款m26对这个问题的通用解决方案了。)
最后,假设程序的其他某处用new动态创建了一个enemytank对象,然后用delete删除掉:
enemytarget *targetptr = new enemytank;
...
delete targetptr;
到此为止所做的一切好象都很正常:两个类在析构函数里都对构造函数所做的操作进行了清除;应用程序也显然没有错误,用new生成的对象在最后也用delete删除了。然而这里却有很大的问题。程序的行为是不可预测的--无法知道将会发生什么。
c++语言标准关于这个问题的阐述非常清楚:当通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。这意味着编译器生成的代码将会做任何它喜欢的事:重新格式化你的硬盘,给你的老板发电子邮件,把你的程序源代码传真给你的对手,无论什么事都可能发生。(实际运行时经常发生的是,派生类的析构函数永远不会被调用。在本例中,这意味着当targetptr 删除时,enemytank的数量值不会改变,那么,敌人坦克的数量就是错的,这对需要高度依赖精确信息的部队来说,会造成什么后果?)
为了避免这个问题,只需要使enemytarget的析构函数为virtual。声明析构函数为虚就会带来你所希望的运行良好的行为:对象内存释放时,enemytank和enemytarget的析构函数都会被调用。
和绝大部分基类一样,现在enemytarget类包含一个虚函数。虚函数的目的是让派生类去定制自己的行为(见条款36),所以几乎所有的基类都包含虚函数。
如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。请看下面的例子,这个例子基于arm("the annotated c++ reference manual")一书的一个专题讨论。
// 一个表示2d点的类
class point {
public:
point(short int xcoord, short int ycoord);
~point();
private:
short int x, y;
};
如果一个short int占16位,一个point对象将刚好适合放进一个32位的寄存器中。另外,一个point对象可以作为一个32位的数据传给用c或fortran等其他语言写的函数中。但如果point的析构函数为虚,情况就会改变。
实现虚函数需要对象附带一些额外信息,以使对象在运行时可以确定该调用哪个虚函数。对大多数编译器来说,这个额外信息的具体形式是一个称为vptr(虚函数表指针)的指针。vptr指向的是一个称为vtbl(虚函数表)的函数指针数组。每个有虚函数的类都附带有一个vtbl。当对一个对象的某个虚函数进行请求调用时,实际被调用的函数是根据指向vtbl的vptr在vtbl里找到相应的函数指针来确定的。
虚函数实现的细节不重要(当然,如果你感兴趣,可以阅读条款m24),重要的是,如果point类包含一个虚函数,它的对象的体积将不知不觉地翻番,从2个16位的short变成了2个16位的short加上一个32位的vptr!point对象再也不能放到一个32位寄存器中去了。而且,c++中的point对象看起来再也不具有和其他语言如c中声明的那样相同的结构了,因为这些语言里没有vptr。所以,用其他语言写的函数来传递point也不再可能了,除非专门去为它们设计vptr,而这本身是实现的细节,会导致代码无法移植。
所以基本的一条是,无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
这是一个很好的准则,大多数情况都适用。但不幸的是,当类里没有虚函数的时候,也会带来非虚析构函数问题。 例如,条款13里有个实现用户自定义数组下标上下限的类模板。假设你(不顾条款m33的建议)决定写一个派生类模板来表示某种可以命名的数组(即每个数组有一个名字)。
template<class t> // 基类模板
class array { // (来自条款13)
public:
array(int lowbound, int highbound);
~array();
private:
vector<t> data;
size_t size;
int lbound, hbound;
};
template<class t>
class namedarray: public array<t> {
public:
namedarray(int lowbound, int highbound, const string& name);
...
private:
string arrayname;
};
如果在应用程序的某个地方你将指向namedarray类型的指针转换成了array类型的指针,然后用delete来删除array指针,那你就会立即掉进"不确定行为"的陷阱中。
namedarray<int> *pna =
new namedarray<int>(10, 20, "impending doom");
array<int> *pa;
...
pa = pna; // namedarray<int>* -> array<int>*
...
delete pa; // 不确定! 实际中,pa->arrayname
// 会造成泄漏,因为*pa的namedarray
// 永远不会被删除
现实中,这种情形出现得比你想象的要频繁。让一个现有的类做些什么事,然后从它派生一个类做和它相同的事,再加上一些特殊的功能,这在现实中不是不常见。namedarray没有重定义array的任何行为--它继承了array的所有功能而没有进行任何修改--它只是增加了一些额外的功能。但非虚析构函数的问题依然存在(还有其他问题,参见m33)
最后,值得指出的是,在某些类里声明纯虚析构函数很方便。纯虚函数将产生抽象类--不能实例化的类(即不能创建此类型的对象)。有些时候,你想使一个类成为抽象类,但刚好又没有任何纯虚函数。怎么办?因为抽象类是准备被用做基类的,基类必须要有一个虚析构函数,纯虚函数会产生抽象类,所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数。
这里是一个例子:
class awov { // awov = "abstract w/o
// virtuals"
public:
virtual ~awov() = 0; // 声明一个纯虚析构函数
};
这个类有一个纯虚函数,所以它是抽象的,而且它有一个虚析构函数,所以不会产生析构函数问题。但这里还有一件事:必须提供纯虚析构函数的定义:
awov::~awov() {} // 纯虚析构函数的定义
这个定义是必需的,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对~awov的调用,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来,最后还是得回去把它添上。
可以在函数里做任何事,但正如上面的例子一样,什么事都不做也不是不常见。如果是这种情况,那很自然地会想到将析构函数声明为内联函数,从而避免对一个空函数的调用所产生的开销。这是一个很好的方法,但有一件事要清楚。
因为析构函数为虚,它的地址必须进入到类的vtbl(见条款m24)。但内联函数不是作为独立的函数存在的(这就是"内联"的意思),所以必须用特殊的方法得到它们的地址。条款33对此做了全面的介绍,其基本点是:如果声明虚析构函数为inline,将会避免调用它们时产生的开销,但编译器还是必然会在什么地方产生一个此函数的拷贝。
#23
不要从没有虚析构函数的类继承,确保基类都有虚席构函数。
#24
我也是这个意思,基类类型中只有value.你用基类类型去操纵只能取的到value,因为它并不知道vptr+1是什么东东.如果想调用shift须用derived类型.调用虚函数的第一关在类型中,类型控制vptr
#25
恩。。我想这个问题好象差不多是这样了。不过不知道天使兄的文章是从什么地方来的。
呵呵。恩。不知道大家对我提问的这个切片的有什么看法呢。。
呵呵。恩。不知道大家对我提问的这个切片的有什么看法呢。。
#26
to:FireAngel
谢谢,等一下我慢慢看
to:xrbeck
我觉得"对象切片"的根本在于拷贝构造函数.
aclass
bclass :public aclass
void test(aclass a)
{
//dosomething
}
void main()
{
bclass b;
test(b);//造成对象切片.这样要构造一个test中的临时对象,由于参数类型是基类的,所以会
//调用aclass的拷贝构造,这个构造函数不可能构造子类的新成员,对象切片也就产
//生了.
}
谢谢,等一下我慢慢看
to:xrbeck
我觉得"对象切片"的根本在于拷贝构造函数.
aclass
bclass :public aclass
void test(aclass a)
{
//dosomething
}
void main()
{
bclass b;
test(b);//造成对象切片.这样要构造一个test中的临时对象,由于参数类型是基类的,所以会
//调用aclass的拷贝构造,这个构造函数不可能构造子类的新成员,对象切片也就产
//生了.
}
#27
恩。这个是切片的时候采取的方法。可是有一点。我们知道派生类并不能继承基类的私有对象。
而在调用copy constructor的时候是用了比如m=b.m这样的赋值。。按照书上说。派生类
包括了基类的一份COPY。那基类的私有成员。。?
而在调用copy constructor的时候是用了比如m=b.m这样的赋值。。按照书上说。派生类
包括了基类的一份COPY。那基类的私有成员。。?
#28
我想应该还是隐含的吧.比如上例中的aclass有个private的int a;bclass有个private的int b;在拷贝构造时,会根据b的属性构造临时对象,b中隐含有a,但b访问不到.
在b的构造时先会调用a的构造,这时int a会被赋值.也就是说b中隐含有int a,且a是被赋值的.我揣测拷贝构造会忽略private
有段代码你看看,写的有点乱
#include <iostream.h>
class a
{
public:
a(int i = 0)
{
h = i;
}
a(a& a1)
{
h = a1.h;
}
void display()
{
cout<<h<<endl;
}
private:
int h;
};
class b :public a
{
public:
b(int i = 0):a(8)
{
x = i;
}
private:
int x;
};
void test(a f)
{
f.display();
}
void main()
{
b b;
test(b);
}
输出8
在b的构造时先会调用a的构造,这时int a会被赋值.也就是说b中隐含有int a,且a是被赋值的.我揣测拷贝构造会忽略private
有段代码你看看,写的有点乱
#include <iostream.h>
class a
{
public:
a(int i = 0)
{
h = i;
}
a(a& a1)
{
h = a1.h;
}
void display()
{
cout<<h<<endl;
}
private:
int h;
};
class b :public a
{
public:
b(int i = 0):a(8)
{
x = i;
}
private:
int x;
};
void test(a f)
{
f.display();
}
void main()
{
b b;
test(b);
}
输出8
#29
诶。。要看懂你写什么是真的很吃力。。呵呵。
不过大概知道吧。恩。我觉得你这个例子似乎不能说明什么问题。
不如把a(a& a1)
{
h = a1.h;
}
改为
h=10;这样还能反映调用了一个拷贝构造函数。
我的理解。的确是有继承基类的私有成员。可是编译器不允许我们直接使用。
我们看看这个:
class base
{
private:
int m;
int n;
public:
base(){cout<<"base constructor"<<endl;}
~base(){cout<<"base destructor"<<endl;}
};
class derived:public base
{
private:
int k;
public:
derived(){cout<<"derived constructor"<<endl;}
~derived(){cout<<"derived destructor"<<endl;}
};
class node
{
private:
int k;
public:
node(){cout<<"node constructor"<<endl;}
~node(){cout<<"node destructor"<<endl;}
};
int main(int argc, char* argv[])
{
node nodetemp;
base temp1;
derived temp2;
base temp3=derived();
cout<<"nodetemp's size is :"<<sizeof(node)<<endl;
cout<<"base's size is :"<<sizeof(temp1)<<endl;
cout<<"derived's size is :"<<sizeof(temp2)<<endl;
cout<<"temp3's size is :"<<sizeof(temp3)<<endl;
getch();
return 0;
}
可以发现派生类其实有基类的成员。(长度为12)。不过是我们不能直接用而已。。
这是我的理解。不知道对不对。。
不过大概知道吧。恩。我觉得你这个例子似乎不能说明什么问题。
不如把a(a& a1)
{
h = a1.h;
}
改为
h=10;这样还能反映调用了一个拷贝构造函数。
我的理解。的确是有继承基类的私有成员。可是编译器不允许我们直接使用。
我们看看这个:
class base
{
private:
int m;
int n;
public:
base(){cout<<"base constructor"<<endl;}
~base(){cout<<"base destructor"<<endl;}
};
class derived:public base
{
private:
int k;
public:
derived(){cout<<"derived constructor"<<endl;}
~derived(){cout<<"derived destructor"<<endl;}
};
class node
{
private:
int k;
public:
node(){cout<<"node constructor"<<endl;}
~node(){cout<<"node destructor"<<endl;}
};
int main(int argc, char* argv[])
{
node nodetemp;
base temp1;
derived temp2;
base temp3=derived();
cout<<"nodetemp's size is :"<<sizeof(node)<<endl;
cout<<"base's size is :"<<sizeof(temp1)<<endl;
cout<<"derived's size is :"<<sizeof(temp2)<<endl;
cout<<"temp3's size is :"<<sizeof(temp3)<<endl;
getch();
return 0;
}
可以发现派生类其实有基类的成员。(长度为12)。不过是我们不能直接用而已。。
这是我的理解。不知道对不对。。
#30
啊?加到300分了?赶快翻书找问题啊!兄弟们快来啊!呵呵。。
#31
把你们的代码反汇编以后看看就什么都明白了。
#32
在DELETE P的时候调用的CB的西勾函数,其中调用FN(VOID),此时首先调用本类的FN()
所以输出CB,这些找一本基础的C++的书,上面有很详细的说明的
所以输出CB,这些找一本基础的C++的书,上面有很详细的说明的
#33
to:xrbeck
我明白你的意思了.你举的例子中derived的对象temp2确实有base中的m,n.只是m,n是隐含的,不能直接访问.必须用自己的方法对基类中的方法进行嵌套,如:
#include <iostream.h>
class base
{
private:
int m;
int n;
public:
base(int M = 2,int N = 3){m = M ;n = N;}
void display(){cout<<m<<" "<<n<<endl;}
~base(){}
};
class derived:public base
{
private:
int k;
public:
derived(int K = 5){k = K;}
void accessmn(){display();}
~derived(){}
};
void main()
{
derived dobj;
dobj.accessmn();
}
我明白你的意思了.你举的例子中derived的对象temp2确实有base中的m,n.只是m,n是隐含的,不能直接访问.必须用自己的方法对基类中的方法进行嵌套,如:
#include <iostream.h>
class base
{
private:
int m;
int n;
public:
base(int M = 2,int N = 3){m = M ;n = N;}
void display(){cout<<m<<" "<<n<<endl;}
~base(){}
};
class derived:public base
{
private:
int k;
public:
derived(int K = 5){k = K;}
void accessmn(){display();}
~derived(){}
};
void main()
{
derived dobj;
dobj.accessmn();
}
#34
诶。是啊是啊。我就是这个意思。。这个是我的理解。。
不过你说的“必须用自己的方法。。”这个可能不对。要访问的话只能根据基类提供的接口吧。恩
哎呀。今天晚上再看看这本书。看看还有什么要讨论的。。呵呵。。
不过你说的“必须用自己的方法。。”这个可能不对。要访问的话只能根据基类提供的接口吧。恩
哎呀。今天晚上再看看这本书。看看还有什么要讨论的。。呵呵。。
#35
大家还有要说的吗?没有的话就给分了(今天晚上)
#36
up
#37
up
#38
回复人: magicblue(小飞侠) (2001-8-20 0:09:44) 得0分
有这样一段代码:
#include <iostream>
class A
{
public:
~A (void);
virtual void FuncA (void)
{
std::cout<<"aaa";
}
};
class B: public A
{
public:
virtual void FuncA(void)
{
std::cout<<"bbb";
}
};
A::~A (void)
{
FuncA ();
}
int main()
{
B * vb = new B;
delete vb;
}
会输出aaa
class b 的析枸函数不存在,调用父类的析构函数,
有这样一段代码:
#include <iostream>
class A
{
public:
~A (void);
virtual void FuncA (void)
{
std::cout<<"aaa";
}
};
class B: public A
{
public:
virtual void FuncA(void)
{
std::cout<<"bbb";
}
};
A::~A (void)
{
FuncA ();
}
int main()
{
B * vb = new B;
delete vb;
}
会输出aaa
class b 的析枸函数不存在,调用父类的析构函数,
#39
faint!
怎么参与的人这么多?
我都不好意思再贴了
怎么参与的人这么多?
我都不好意思再贴了
#40
记住scott Meyers 的一句话吧:
通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。这意味着编译器生成的代码将会做任何它喜欢的事
实际运行时经常发生的是,派生类的析构函数永远不会被调用
通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。这意味着编译器生成的代码将会做任何它喜欢的事
实际运行时经常发生的是,派生类的析构函数永远不会被调用
#41
在C++编程思想上提到
(1)构造顺序:Ca->Cb->Cc; 盖房子当然是从下向上盖的。
析构顺序:Cc->Cb->Ca; 拆房子当然是从上向下拆的。
(2)在构造与析构中虚机制失效,所有在构造与析构中调用的函数都以实函数的方式调用。
(3)关于造型(借用的java术语,CAST),
static_cast<classa *>(classb*):不考虑实际的对象类型会造成重大损失。
dynamic_cast<classa *>(classb *):考虑实际对象类型,是安全的。
你最开始提到的:
明白了,Ca中没有定义虚析构,相当于在派生类中增加新的虚函数,所以调用没结果.
不过我把Ca* p = new Cb;改为Cb b(错误在这里);
Ca* p = &b;
delete p;//错误,会造成对b的两次删除。一次是delete,一次
运行时出错是为什么?
(1)构造顺序:Ca->Cb->Cc; 盖房子当然是从下向上盖的。
析构顺序:Cc->Cb->Ca; 拆房子当然是从上向下拆的。
(2)在构造与析构中虚机制失效,所有在构造与析构中调用的函数都以实函数的方式调用。
(3)关于造型(借用的java术语,CAST),
static_cast<classa *>(classb*):不考虑实际的对象类型会造成重大损失。
dynamic_cast<classa *>(classb *):考虑实际对象类型,是安全的。
你最开始提到的:
明白了,Ca中没有定义虚析构,相当于在派生类中增加新的虚函数,所以调用没结果.
不过我把Ca* p = new Cb;改为Cb b(错误在这里);
Ca* p = &b;
delete p;//错误,会造成对b的两次删除。一次是delete,一次
运行时出错是为什么?
#42
在C++编程思想上提到
(1)构造顺序:Ca->Cb->Cc; 盖房子当然是从下向上盖的。
析构顺序:Cc->Cb->Ca; 拆房子当然是从上向下拆的。
(2)在构造与析构中虚机制失效,所有在构造与析构中调用的函数都以实函数的方式调用。
(3)关于造型(借用的java术语,CAST),
static_cast<classa *>(classb*):不考虑实际的对象类型会造成重大损失。
dynamic_cast<classa *>(classb *):考虑实际对象类型,是安全的。
你最开始提到的:
明白了,Ca中没有定义虚析构,相当于在派生类中增加新的虚函数,所以调用没结果.
不过我把Ca* p = new Cb;改为Cb b(错误在这里);
Ca* p = &b;
delete p;//错误,会造成对b的两次删除错误(DEbug错误)。
//一次是delete p,一次是局部变量的b对象的删除。
运行时出错是为什么?
(4)我觉的public,private,protected等权限修饰符我认为只是编译期间有效C++编译的事,在运行期间是没有这些权限存在的??
(1)构造顺序:Ca->Cb->Cc; 盖房子当然是从下向上盖的。
析构顺序:Cc->Cb->Ca; 拆房子当然是从上向下拆的。
(2)在构造与析构中虚机制失效,所有在构造与析构中调用的函数都以实函数的方式调用。
(3)关于造型(借用的java术语,CAST),
static_cast<classa *>(classb*):不考虑实际的对象类型会造成重大损失。
dynamic_cast<classa *>(classb *):考虑实际对象类型,是安全的。
你最开始提到的:
明白了,Ca中没有定义虚析构,相当于在派生类中增加新的虚函数,所以调用没结果.
不过我把Ca* p = new Cb;改为Cb b(错误在这里);
Ca* p = &b;
delete p;//错误,会造成对b的两次删除错误(DEbug错误)。
//一次是delete p,一次是局部变量的b对象的删除。
运行时出错是为什么?
(4)我觉的public,private,protected等权限修饰符我认为只是编译期间有效C++编译的事,在运行期间是没有这些权限存在的??
#43
我看了
-----------------------------------------------------------
回复人: FireAngel(堕落天使) (2001-8-20 13:36:22) 得30分
条款14: 确定基类有虚析构函数
-----------------------------------------------------------
我用VC6.0 WIN98试了一下
对我前面的内容补充一点:如果基类的析构是非虚的,则会造成不能正确析构对象。
虚析构基类,析构顺序:是指向Cc对象的Ca指针delete时 Cc->Cb->Ca;
若非虚基类 是指向Cc对象的指针delete时 Ca;只析构了Ca (dynamic_cast转换也同样结果)
这里说的基类应该是根基类(第一个基类),这种情况也只对根基类有效。
*************
我在调试中发现:
如果根基类是虚的,则不管它的派生类是否有虚析构,都等同有虚析构函数。不知看法对否?
-----------------------------------------------------------
回复人: FireAngel(堕落天使) (2001-8-20 13:36:22) 得30分
条款14: 确定基类有虚析构函数
-----------------------------------------------------------
我用VC6.0 WIN98试了一下
对我前面的内容补充一点:如果基类的析构是非虚的,则会造成不能正确析构对象。
虚析构基类,析构顺序:是指向Cc对象的Ca指针delete时 Cc->Cb->Ca;
若非虚基类 是指向Cc对象的指针delete时 Ca;只析构了Ca (dynamic_cast转换也同样结果)
这里说的基类应该是根基类(第一个基类),这种情况也只对根基类有效。
*************
我在调试中发现:
如果根基类是虚的,则不管它的派生类是否有虚析构,都等同有虚析构函数。不知看法对否?
#44
不对吧, dynamic和static_cast是一样的吧,
都是安全的,区别在于一个在编译时作类型检查,一个在运行时
很早看的,不知道记得对不对,有一个操作符re**_cast(:(,好久不用c++了)
才是不顾类型的强制转换也。
都是安全的,区别在于一个在编译时作类型检查,一个在运行时
很早看的,不知道记得对不对,有一个操作符re**_cast(:(,好久不用c++了)
才是不顾类型的强制转换也。
#45
我也试过两种,但是MSDN上如是说:
class B { ... };
class D : public B { ... };
void f(B* pb)
{
D* pd1 = dynamic_cast<D*>(pb);
D* pd2 = static_cast<D*>(pb);
}
If pb really points to an object of type D, then pd1 and pd2 will get the same value. They will also get the same value if pb == 0.
If pb points to an object of type B and not to the complete D class, then dynamic_cast will know enough to return zero. However, static_cast relies on the programmer’s assertion that pb points to an object of type D and simply returns a pointer to that supposed D object.
Consequently, static_cast can do the inverse of implicit conversions, in which case the results are undefined. It is left to the programmer to ensure that the results of a static_cast conversion are safe.
------------------------------------------------------------------
你说的强制转换是
reinterpret_cast Operator
C++ Specific —>
reinterpret_cast < type-id > ( expression )
The reinterpret_cast operator allows any pointer to be converted into any other pointer type, and it allows any integral type to be converted into any pointer type and vice versa. Misuse of the reinterpret_cast operator can easily be unsafe. Unless the desired conversion is inherently low-level, you should use one of the other cast operators.
END C++ Specific
class B { ... };
class D : public B { ... };
void f(B* pb)
{
D* pd1 = dynamic_cast<D*>(pb);
D* pd2 = static_cast<D*>(pb);
}
If pb really points to an object of type D, then pd1 and pd2 will get the same value. They will also get the same value if pb == 0.
If pb points to an object of type B and not to the complete D class, then dynamic_cast will know enough to return zero. However, static_cast relies on the programmer’s assertion that pb points to an object of type D and simply returns a pointer to that supposed D object.
Consequently, static_cast can do the inverse of implicit conversions, in which case the results are undefined. It is left to the programmer to ensure that the results of a static_cast conversion are safe.
------------------------------------------------------------------
你说的强制转换是
reinterpret_cast Operator
C++ Specific —>
reinterpret_cast < type-id > ( expression )
The reinterpret_cast operator allows any pointer to be converted into any other pointer type, and it allows any integral type to be converted into any pointer type and vice versa. Misuse of the reinterpret_cast operator can easily be unsafe. Unless the desired conversion is inherently low-level, you should use one of the other cast operators.
END C++ Specific
#46
谢谢你的回复(可惜没分了):)
> 如果根基类是虚的,则不管它的派生类是否有虚析构,都等同有虚析构函数。不知看法对否?
我不太明白你的意思,基类的析构是虚的,派生类的析构当然也是虚的
> 如果根基类是虚的,则不管它的派生类是否有虚析构,都等同有虚析构函数。不知看法对否?
我不太明白你的意思,基类的析构是虚的,派生类的析构当然也是虚的
#47
其实所讨论的一切,就是在讨论编译器是怎么处理的。别忘了,编译器的规则是由那些计算机的专家确定下来的,编译器其实不是智能的,否则相同的程序在不同的时刻或者不同环境下,就有可能出现不同,就是不确定性,这是不允许的。所以这一切都是在那些计算机专家的对OO的理解后,留给我们的就是不同的面向对象的语言。计算机语言就必须是确定的,所以很多讨论的事情可能就是没有道理的,就是那样规定的。就如在linux和 windows的C++对于某些程序编译的结果是截然不同。
很多的事物是在他们出现以后,才去寻找理论去解释。不是解释了什么,事物才出现的。就如有时候做的程序,等用户使用后,他们所使用的某些功能,是事先并没有想到,连你自己也很惊讶的。
很多的事物是在他们出现以后,才去寻找理论去解释。不是解释了什么,事物才出现的。就如有时候做的程序,等用户使用后,他们所使用的某些功能,是事先并没有想到,连你自己也很惊讶的。
#1
明白了,Ca中没有定义虚析构,相当于在派生类中增加新的虚函数,所以调用没结果.
不过我把Ca* p = new Cb;改为Cb b;
Ca* p = &b;
运行时出错是为什么?
另外在上面更改过可正常运行的程序中在析构函数中调用了虚函数,但还是正确地运行了.我想问问假如要在虚析构中加入一条语句比如:virtual ~Cb(){ @ fn(); @ }析构对象在何处发生?第一个@处还是第二个@?应该是第二个吧?如果fn();是虚函数,那么删除vtable又在何处?
不过我把Ca* p = new Cb;改为Cb b;
Ca* p = &b;
运行时出错是为什么?
另外在上面更改过可正常运行的程序中在析构函数中调用了虚函数,但还是正确地运行了.我想问问假如要在虚析构中加入一条语句比如:virtual ~Cb(){ @ fn(); @ }析构对象在何处发生?第一个@处还是第二个@?应该是第二个吧?如果fn();是虚函数,那么删除vtable又在何处?
#2
谁能写一段在析构中调用虚函数由于vtable以被更改导致运行结果和正常时(也就是一般的行为)不一样的?
#3
虚函数只能被用来继承,所以加了virtual后就调用默认析构函数,所以就不输出了
#4
我会不断给帖子加分的
#5
还有我对<C++编程思想>中第14章293页的一句话极不理解:假设虚机制在析构函数中使用,那么调用下面这样的虚函数是可能的:这个函数是在继承层次中比当前的析构函数"更靠外"(更晚派生的).
我总觉得这句话说反了,更晚派生,构造的也就晚,析构的也就早.那么当前的析构函数所在对象相对析构的晚,它所调用的已经析构了呀.哪位大侠给讲讲?
我总觉得这句话说反了,更晚派生,构造的也就晚,析构的也就早.那么当前的析构函数所在对象相对析构的晚,它所调用的已经析构了呀.哪位大侠给讲讲?
#6
怎么没人理??
#7
destructor运行结束之后才能把对象free掉,要不然就乱套了。所以调用虚函数不会有什么问题。
你对构造和析构的理解很成问题。构造和析构一般只能理解为对数据成员的构造和析构。所有的成员函数,无论是不是虚的,都不存在被构造的这么一回事。但是虚函数表的指针(一般都用这个办法实现虚函数)必须被初始化,这是一个程序员不必显示地实现的操作。
你最好看一些如何实现高级语言的文章。总是看那些理论只能让人的头脑越来越晕。
你对构造和析构的理解很成问题。构造和析构一般只能理解为对数据成员的构造和析构。所有的成员函数,无论是不是虚的,都不存在被构造的这么一回事。但是虚函数表的指针(一般都用这个办法实现虚函数)必须被初始化,这是一个程序员不必显示地实现的操作。
你最好看一些如何实现高级语言的文章。总是看那些理论只能让人的头脑越来越晕。
#8
我说的构造是说每个派生类都会修改vtable,构造对象时vptr便指向自己那个类的vtable.
aclass
|construct
|
|aobject|
| vptr--|--->|avtable|
|avfun |
bclass :public aclass
|construct
|
|bobject|
| vptr--|--->|bvtable |
|bvfun |
析构时bvtable便首先无效.在b对象的析构函数调用bvfun应该没问题(你说删除对象在最后).算了,我晕了.其实我觉得在析构调用虚函数不可能会出问题,如果想在a对象中(不管是不是析构)调用bvfun都是不可能的,这有逻辑错误,违反常规.那么<C++编程思想>说的在析构中调用虚函数失败是什么样的代码呢?
aclass
|construct
|
|aobject|
| vptr--|--->|avtable|
|avfun |
bclass :public aclass
|construct
|
|bobject|
| vptr--|--->|bvtable |
|bvfun |
析构时bvtable便首先无效.在b对象的析构函数调用bvfun应该没问题(你说删除对象在最后).算了,我晕了.其实我觉得在析构调用虚函数不可能会出问题,如果想在a对象中(不管是不是析构)调用bvfun都是不可能的,这有逻辑错误,违反常规.那么<C++编程思想>说的在析构中调用虚函数失败是什么样的代码呢?
#9
有这样一段代码:
#include <iostream>
class A
{
public:
~A (void);
virtual void FuncA (void)
{
std::cout<<"aaa";
}
};
class B: public A
{
public:
virtual void FuncA(void)
{
std::cout<<"bbb";
}
};
A::~A (void)
{
FuncA ();
}
int main()
{
B * vb = new B;
delete vb;
}
会输出aaa
创建B对象b的过程:
1. A的构造,vptr指向A的vtable
2. B的构造,vptr指向B的vtable,this指向b
3. B的析构,析构完成后vptr指向A的vtable,this指向b的a部分
如果在这里调用B的vfun便会失败,那么如何这样调用呢?就是在A的析构里!!由于~A()运行时,B部分已被析构,this指向的是A的部分,查vtable便会调出A的vfun
大概清楚了,头也彻底晕了.
发表自己看法者均有分!
#include <iostream>
class A
{
public:
~A (void);
virtual void FuncA (void)
{
std::cout<<"aaa";
}
};
class B: public A
{
public:
virtual void FuncA(void)
{
std::cout<<"bbb";
}
};
A::~A (void)
{
FuncA ();
}
int main()
{
B * vb = new B;
delete vb;
}
会输出aaa
创建B对象b的过程:
1. A的构造,vptr指向A的vtable
2. B的构造,vptr指向B的vtable,this指向b
3. B的析构,析构完成后vptr指向A的vtable,this指向b的a部分
如果在这里调用B的vfun便会失败,那么如何这样调用呢?就是在A的析构里!!由于~A()运行时,B部分已被析构,this指向的是A的部分,查vtable便会调出A的vfun
大概清楚了,头也彻底晕了.
发表自己看法者均有分!
#10
原先我的想法有误,想产生这种情况的话必须是一个对象,且为派生类的对象.若是两个对象一个基类的一个派生类的是无论如何也不会有这样的问题的.最近在看C++对象模型,希望对我有帮助
btw
怎么就我一个在说呀,也不知我上面说的对不对,大家随便发表些看法嘛,都有分的.
btw
怎么就我一个在说呀,也不知我上面说的对不对,大家随便发表些看法嘛,都有分的.
#11
方法是操作对象的,如果对一个对象已经被解构,在其上调用任何方法都是无效的。设想类B派生于A,如果当B的解构函数被调用,从语义上就意味着B不同于A的部分已经消失,这时,调用不同于A的方法将操纵不存在的对象,有可能引起未知的错误,请看下例。
class A{
public:
virtual ~A()
{
fun();
}
virtual void fun()
{
//do nothing
}
};
class B: public A
{
public:
char * p;
B()
{
p= new char[100];//allocate memory
}
virtual ~B()
{
delete[] p;
}
virtual void fun()
{
p[10]=0;//you will got an access violation error if call fun() after
//destructor called.
}
};
class A{
public:
virtual ~A()
{
fun();
}
virtual void fun()
{
//do nothing
}
};
class B: public A
{
public:
char * p;
B()
{
p= new char[100];//allocate memory
}
virtual ~B()
{
delete[] p;
}
virtual void fun()
{
p[10]=0;//you will got an access violation error if call fun() after
//destructor called.
}
};
#12
up
#13
一些不是很成熟的观点共同探讨:
对于理解对象模型,我觉得"正确的代码(对象方法)工作在正确的数据(对象的内存布局/对象数据)上"这句话可以作为理解对象模型很多方面的有用的指导,
在destructor中调用virtual/non-virtual function时对象模型应该保证该function只能工作在"现在"的对象上,那么该function只能是"现在"的对象所拥有的方法(包括继承而来的)。
如: class A: public x{...}; class B: public A{...}; 那么当执行到A::~A()时,其中的调用的任何virtual/non-virtual function(设为foo)只能是类A的方法(包括继承而来的方法),程序能够执行到A::~A()只能说明当前工作的对象是类型为A的对象,没有理由认为这时还可以执行B类的方法(它是为类型为B的对象所设计的,而不是为类型为A的对象所设计的)。
使用对象名称引用方法名的方式调用方法形如:a.foo();(a类型为A) 只能执行A的方法(包括继承而来的),不管foo()是不是virtual。
使用类型指针引用方法名的方式调用方法形如:pa->foo();还是只能执行pa所指对象"第一次产生时"所属类型的方法(包括继承而来的),指针只是程序员对对象类型的泛化(is-a的抽象),指针的调整并不能改变所指对象"第一次产生时"的类型。不要受指针类型的欺骗,应透过指针看对象。
对于理解对象模型,我觉得"正确的代码(对象方法)工作在正确的数据(对象的内存布局/对象数据)上"这句话可以作为理解对象模型很多方面的有用的指导,
在destructor中调用virtual/non-virtual function时对象模型应该保证该function只能工作在"现在"的对象上,那么该function只能是"现在"的对象所拥有的方法(包括继承而来的)。
如: class A: public x{...}; class B: public A{...}; 那么当执行到A::~A()时,其中的调用的任何virtual/non-virtual function(设为foo)只能是类A的方法(包括继承而来的方法),程序能够执行到A::~A()只能说明当前工作的对象是类型为A的对象,没有理由认为这时还可以执行B类的方法(它是为类型为B的对象所设计的,而不是为类型为A的对象所设计的)。
使用对象名称引用方法名的方式调用方法形如:a.foo();(a类型为A) 只能执行A的方法(包括继承而来的),不管foo()是不是virtual。
使用类型指针引用方法名的方式调用方法形如:pa->foo();还是只能执行pa所指对象"第一次产生时"所属类型的方法(包括继承而来的),指针只是程序员对对象类型的泛化(is-a的抽象),指针的调整并不能改变所指对象"第一次产生时"的类型。不要受指针类型的欺骗,应透过指针看对象。
#14
虚析构的机制是----蜕变
假设类的层次是
Ca->Cb->Cc
那么Cc析构以后调用~Cc(),对象蜕变为Cb,Cb析构调用~Cb(),对象蜕变为Ca,Ca析构调用~Ca();保证了对象完整的被销毁。
假设类的层次是
Ca->Cb->Cc
那么Cc析构以后调用~Cc(),对象蜕变为Cb,Cb析构调用~Cb(),对象蜕变为Ca,Ca析构调用~Ca();保证了对象完整的被销毁。
#15
其实关于这个问题:为什么不能在析构函数中调用虚函数会失败。我想书上说的是对的。
理由就是虚函数实现晚捆绑。而析构函数的执行是从远到近。所以如果我们在一个近端的
析构函数想要去CALL远端的虚函数会怎么样?远端的已经被释放掉了。所以。。。
继续讨论吧。其实这一章我也有几个地方不懂。
理由就是虚函数实现晚捆绑。而析构函数的执行是从远到近。所以如果我们在一个近端的
析构函数想要去CALL远端的虚函数会怎么样?远端的已经被释放掉了。所以。。。
继续讨论吧。其实这一章我也有几个地方不懂。
#16
你说的这句“那么调用下面这样的虚函数是可能的:这个函数是在继承层次中比当前的析构函数"更靠外"(更晚派生的).“
我想你理解错了。它的意思是这样:
class a;
class b:public a;
{析构中想调用c中的虚拟函数。}
class c:public b;
这个就是他说的”有可能调用比当前层次更晚的函数“。而这个时候C已经释放掉了。所以。。
我想你理解错了。它的意思是这样:
class a;
class b:public a;
{析构中想调用c中的虚拟函数。}
class c:public b;
这个就是他说的”有可能调用比当前层次更晚的函数“。而这个时候C已经释放掉了。所以。。
#17
对了。这位兄台。你对”继承和Vtable“这个地方理解的怎么样。我总觉得有点不对。。。
大家一起多讨论讨论。把这个搞搞清楚吧。
大家一起多讨论讨论。把这个搞搞清楚吧。
#18
to:xrbeck
你回的第二个帖子我还是不懂,我觉得有点文字游戏.你举的例子b的析构调用比它"更晚派生"的c的虚函数.结果失败.而书上却说"调用下面这样的虚函数是可能的"可能吗???
你有什么问题说出来好了,大家一起讨论.
你回的第二个帖子我还是不懂,我觉得有点文字游戏.你举的例子b的析构调用比它"更晚派生"的c的虚函数.结果失败.而书上却说"调用下面这样的虚函数是可能的"可能吗???
你有什么问题说出来好了,大家一起讨论.
#19
呵呵。我想不是什么文字游戏。你看看他说的”因为有可能会出现这种情况。。“
(而恰恰这种情况是不安全的)。所以”只有成员函数的本地版本被调用。而虚机制被忽略“。。你看看他说的”因为析构函数从外到内被调用。比如我举的哪个例子。
B想去CALL C中的虚拟函数。这个就是他后面说的那句“实际上被调用的函数可能操作在
已被删除的对象上”。。这个时候C已经被删除掉了?不是吗?所以在析构函数中为了防止
这种情况。就忽略了虚机制。而只保证本地版本。比如你在B中调用虚拟函数。只能是
调用自己的。。这是我对这个问题的看法。。呵呵。
我说的疑问如下:(继承和Vtable)
书上说基类指针只能调用自己的。不能调用派生类中重新定义的虚函数。比如它举的这个
例子:cout<<B[1]->shift(3);这个会出错。因为shift是在基类中没有的。。。
我想或许编译器可以发现这个是错误。可是难道这个真的是错误吗?我们知道
(我拿它例子说)
在new derived(7)这句的时候。由派生类的构造函数隐性的生成了vtable.也就是说这个
指向基类的指针其实包括了派生类的vtable.那么为什么我们不能知道去调用shift这个函数呢.
因为其实vtable里面已经含有了这个函数的地址.而且是可以找到的.
那为什么会出错?
(而恰恰这种情况是不安全的)。所以”只有成员函数的本地版本被调用。而虚机制被忽略“。。你看看他说的”因为析构函数从外到内被调用。比如我举的哪个例子。
B想去CALL C中的虚拟函数。这个就是他后面说的那句“实际上被调用的函数可能操作在
已被删除的对象上”。。这个时候C已经被删除掉了?不是吗?所以在析构函数中为了防止
这种情况。就忽略了虚机制。而只保证本地版本。比如你在B中调用虚拟函数。只能是
调用自己的。。这是我对这个问题的看法。。呵呵。
我说的疑问如下:(继承和Vtable)
书上说基类指针只能调用自己的。不能调用派生类中重新定义的虚函数。比如它举的这个
例子:cout<<B[1]->shift(3);这个会出错。因为shift是在基类中没有的。。。
我想或许编译器可以发现这个是错误。可是难道这个真的是错误吗?我们知道
(我拿它例子说)
在new derived(7)这句的时候。由派生类的构造函数隐性的生成了vtable.也就是说这个
指向基类的指针其实包括了派生类的vtable.那么为什么我们不能知道去调用shift这个函数呢.
因为其实vtable里面已经含有了这个函数的地址.而且是可以找到的.
那为什么会出错?
#20
可编译器没那么聪明.你说的对,其实vtable里面已经含有了这个函数的地址.但是找得到吗?不一定,这会产生混乱.vptr调用vtable中的虚函数是通过诸如vptr+n这样的方法,因为所有的vtable有相同的顺序.
to be continue
我先吃饭了,不然我老娘K死我
to be continue
我先吃饭了,不然我老娘K死我
#21
找的到吗?找不到吗?呵呵。我想如果我坚持说“哎。编译器为什么这么笨。下面有个
函数指针也不知道。“那这个有点象钻牛角尖了。。呵呵。
不过我想应该是不知道的。因为正如你所说。在基类的vtable不包括这一项。所以即便
下面就是我们要的函数指针。它也不知道。所以程序中要加显示的转换:(derived *)b[1]
来转换。其实这句话的意思大概就是跟编译器说:喂。你的下面有个函数指针是我想要的。
这样或许我们才能正确的去使用吧。。恩。
你上边的那个问题。我刚写了一下。不知道能不能正确的说明这个问题:
#include "iostream.h"
#include "conio.h"
#include "stdio.h"
class base
{
private:
int i;
public:
base(){cout<<"base default constructor"<<endl;}
virtual ~base()
{show();}
virtual void show(){cout<<"virtual in base destructor"<<endl;}
};
class derived:public base
{
private:
int i;
public:
derived(){cout<<"derived default constructor"<<endl;}
~derived()
{}
void show(){cout<<"virtual in derived destructor"<<endl;}
};
int main(int argc, char* argv[])
{
base *temp=new derived;
delete temp;
getch();
return 0;
}
这里你会发现base的show就是本地版本。而不能因为temp是个派生类。而show是个虚拟函数
它就会去调用派生类的虚拟函数吧。。这个是我的看法。。恩。
对于它说的“对象切片”。你怎么看。他说会让派生类切成基类的大小。可是基类的私有成员
它会如何处理?。。。
函数指针也不知道。“那这个有点象钻牛角尖了。。呵呵。
不过我想应该是不知道的。因为正如你所说。在基类的vtable不包括这一项。所以即便
下面就是我们要的函数指针。它也不知道。所以程序中要加显示的转换:(derived *)b[1]
来转换。其实这句话的意思大概就是跟编译器说:喂。你的下面有个函数指针是我想要的。
这样或许我们才能正确的去使用吧。。恩。
你上边的那个问题。我刚写了一下。不知道能不能正确的说明这个问题:
#include "iostream.h"
#include "conio.h"
#include "stdio.h"
class base
{
private:
int i;
public:
base(){cout<<"base default constructor"<<endl;}
virtual ~base()
{show();}
virtual void show(){cout<<"virtual in base destructor"<<endl;}
};
class derived:public base
{
private:
int i;
public:
derived(){cout<<"derived default constructor"<<endl;}
~derived()
{}
void show(){cout<<"virtual in derived destructor"<<endl;}
};
int main(int argc, char* argv[])
{
base *temp=new derived;
delete temp;
getch();
return 0;
}
这里你会发现base的show就是本地版本。而不能因为temp是个派生类。而show是个虚拟函数
它就会去调用派生类的虚拟函数吧。。这个是我的看法。。恩。
对于它说的“对象切片”。你怎么看。他说会让派生类切成基类的大小。可是基类的私有成员
它会如何处理?。。。
#22
条款14: 确定基类有虚析构函数
有时,一个类想跟踪它有多少个对象存在。一个简单的方法是创建一个静态类成员来统计对象的个数。这个成员被初始化为0,在构造函数里加1,析构函数里减1。(条款m26里说明了如何把这种方法封装起来以便很容易地添加到任何类中,"my article on counting objects"提供了对这个技术的另外一些改进)
设想在一个军事应用程序里,有一个表示敌人目标的类:
class enemytarget {
public:
enemytarget() { ++numtargets; }
enemytarget(const enemytarget&) { ++numtargets; }
~enemytarget() { --numtargets; }
static size_t numberoftargets()
{ return numtargets; }
virtual bool destroy(); // 摧毁enemytarget对象后
// 返回成功
private:
static size_t numtargets; // 对象计数器
};
// 类的静态成员要在类外定义;
// 缺省初始化为0
size_t enemytarget::numtargets;
这个类不会为你赢得一份*防御合同,它离国防部的要求相差太远了,但它足以满足我们这儿说明问题的需要。
敌人的坦克是一种特殊的敌人目标,所以会很自然地想到将它抽象为一个以公有继承方式从enemytarget派生出来的类(参见条款35及m33)。因为不但要关心敌人目标的总数,也要关心敌人坦克的总数,所以和基类一样,在派生类里也采用了上面提到的同样的技巧:
class enemytank: public enemytarget {
public:
enemytank() { ++numtanks; }
enemytank(const enemytank& rhs)
: enemytarget(rhs)
{ ++numtanks; }
~enemytank() { --numtanks; }
static size_t numberoftanks()
{ return numtanks; }
virtual bool destroy();
private:
static size_t numtanks; // 坦克对象计数器
};
(写完以上两个类的代码后,你就更能够理解条款m26对这个问题的通用解决方案了。)
最后,假设程序的其他某处用new动态创建了一个enemytank对象,然后用delete删除掉:
enemytarget *targetptr = new enemytank;
...
delete targetptr;
到此为止所做的一切好象都很正常:两个类在析构函数里都对构造函数所做的操作进行了清除;应用程序也显然没有错误,用new生成的对象在最后也用delete删除了。然而这里却有很大的问题。程序的行为是不可预测的--无法知道将会发生什么。
c++语言标准关于这个问题的阐述非常清楚:当通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。这意味着编译器生成的代码将会做任何它喜欢的事:重新格式化你的硬盘,给你的老板发电子邮件,把你的程序源代码传真给你的对手,无论什么事都可能发生。(实际运行时经常发生的是,派生类的析构函数永远不会被调用。在本例中,这意味着当targetptr 删除时,enemytank的数量值不会改变,那么,敌人坦克的数量就是错的,这对需要高度依赖精确信息的部队来说,会造成什么后果?)
为了避免这个问题,只需要使enemytarget的析构函数为virtual。声明析构函数为虚就会带来你所希望的运行良好的行为:对象内存释放时,enemytank和enemytarget的析构函数都会被调用。
和绝大部分基类一样,现在enemytarget类包含一个虚函数。虚函数的目的是让派生类去定制自己的行为(见条款36),所以几乎所有的基类都包含虚函数。
如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。请看下面的例子,这个例子基于arm("the annotated c++ reference manual")一书的一个专题讨论。
// 一个表示2d点的类
class point {
public:
point(short int xcoord, short int ycoord);
~point();
private:
short int x, y;
};
如果一个short int占16位,一个point对象将刚好适合放进一个32位的寄存器中。另外,一个point对象可以作为一个32位的数据传给用c或fortran等其他语言写的函数中。但如果point的析构函数为虚,情况就会改变。
实现虚函数需要对象附带一些额外信息,以使对象在运行时可以确定该调用哪个虚函数。对大多数编译器来说,这个额外信息的具体形式是一个称为vptr(虚函数表指针)的指针。vptr指向的是一个称为vtbl(虚函数表)的函数指针数组。每个有虚函数的类都附带有一个vtbl。当对一个对象的某个虚函数进行请求调用时,实际被调用的函数是根据指向vtbl的vptr在vtbl里找到相应的函数指针来确定的。
虚函数实现的细节不重要(当然,如果你感兴趣,可以阅读条款m24),重要的是,如果point类包含一个虚函数,它的对象的体积将不知不觉地翻番,从2个16位的short变成了2个16位的short加上一个32位的vptr!point对象再也不能放到一个32位寄存器中去了。而且,c++中的point对象看起来再也不具有和其他语言如c中声明的那样相同的结构了,因为这些语言里没有vptr。所以,用其他语言写的函数来传递point也不再可能了,除非专门去为它们设计vptr,而这本身是实现的细节,会导致代码无法移植。
所以基本的一条是,无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
这是一个很好的准则,大多数情况都适用。但不幸的是,当类里没有虚函数的时候,也会带来非虚析构函数问题。 例如,条款13里有个实现用户自定义数组下标上下限的类模板。假设你(不顾条款m33的建议)决定写一个派生类模板来表示某种可以命名的数组(即每个数组有一个名字)。
template<class t> // 基类模板
class array { // (来自条款13)
public:
array(int lowbound, int highbound);
~array();
private:
vector<t> data;
size_t size;
int lbound, hbound;
};
template<class t>
class namedarray: public array<t> {
public:
namedarray(int lowbound, int highbound, const string& name);
...
private:
string arrayname;
};
如果在应用程序的某个地方你将指向namedarray类型的指针转换成了array类型的指针,然后用delete来删除array指针,那你就会立即掉进"不确定行为"的陷阱中。
namedarray<int> *pna =
new namedarray<int>(10, 20, "impending doom");
array<int> *pa;
...
pa = pna; // namedarray<int>* -> array<int>*
...
delete pa; // 不确定! 实际中,pa->arrayname
// 会造成泄漏,因为*pa的namedarray
// 永远不会被删除
现实中,这种情形出现得比你想象的要频繁。让一个现有的类做些什么事,然后从它派生一个类做和它相同的事,再加上一些特殊的功能,这在现实中不是不常见。namedarray没有重定义array的任何行为--它继承了array的所有功能而没有进行任何修改--它只是增加了一些额外的功能。但非虚析构函数的问题依然存在(还有其他问题,参见m33)
最后,值得指出的是,在某些类里声明纯虚析构函数很方便。纯虚函数将产生抽象类--不能实例化的类(即不能创建此类型的对象)。有些时候,你想使一个类成为抽象类,但刚好又没有任何纯虚函数。怎么办?因为抽象类是准备被用做基类的,基类必须要有一个虚析构函数,纯虚函数会产生抽象类,所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数。
这里是一个例子:
class awov { // awov = "abstract w/o
// virtuals"
public:
virtual ~awov() = 0; // 声明一个纯虚析构函数
};
这个类有一个纯虚函数,所以它是抽象的,而且它有一个虚析构函数,所以不会产生析构函数问题。但这里还有一件事:必须提供纯虚析构函数的定义:
awov::~awov() {} // 纯虚析构函数的定义
这个定义是必需的,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对~awov的调用,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来,最后还是得回去把它添上。
可以在函数里做任何事,但正如上面的例子一样,什么事都不做也不是不常见。如果是这种情况,那很自然地会想到将析构函数声明为内联函数,从而避免对一个空函数的调用所产生的开销。这是一个很好的方法,但有一件事要清楚。
因为析构函数为虚,它的地址必须进入到类的vtbl(见条款m24)。但内联函数不是作为独立的函数存在的(这就是"内联"的意思),所以必须用特殊的方法得到它们的地址。条款33对此做了全面的介绍,其基本点是:如果声明虚析构函数为inline,将会避免调用它们时产生的开销,但编译器还是必然会在什么地方产生一个此函数的拷贝。
有时,一个类想跟踪它有多少个对象存在。一个简单的方法是创建一个静态类成员来统计对象的个数。这个成员被初始化为0,在构造函数里加1,析构函数里减1。(条款m26里说明了如何把这种方法封装起来以便很容易地添加到任何类中,"my article on counting objects"提供了对这个技术的另外一些改进)
设想在一个军事应用程序里,有一个表示敌人目标的类:
class enemytarget {
public:
enemytarget() { ++numtargets; }
enemytarget(const enemytarget&) { ++numtargets; }
~enemytarget() { --numtargets; }
static size_t numberoftargets()
{ return numtargets; }
virtual bool destroy(); // 摧毁enemytarget对象后
// 返回成功
private:
static size_t numtargets; // 对象计数器
};
// 类的静态成员要在类外定义;
// 缺省初始化为0
size_t enemytarget::numtargets;
这个类不会为你赢得一份*防御合同,它离国防部的要求相差太远了,但它足以满足我们这儿说明问题的需要。
敌人的坦克是一种特殊的敌人目标,所以会很自然地想到将它抽象为一个以公有继承方式从enemytarget派生出来的类(参见条款35及m33)。因为不但要关心敌人目标的总数,也要关心敌人坦克的总数,所以和基类一样,在派生类里也采用了上面提到的同样的技巧:
class enemytank: public enemytarget {
public:
enemytank() { ++numtanks; }
enemytank(const enemytank& rhs)
: enemytarget(rhs)
{ ++numtanks; }
~enemytank() { --numtanks; }
static size_t numberoftanks()
{ return numtanks; }
virtual bool destroy();
private:
static size_t numtanks; // 坦克对象计数器
};
(写完以上两个类的代码后,你就更能够理解条款m26对这个问题的通用解决方案了。)
最后,假设程序的其他某处用new动态创建了一个enemytank对象,然后用delete删除掉:
enemytarget *targetptr = new enemytank;
...
delete targetptr;
到此为止所做的一切好象都很正常:两个类在析构函数里都对构造函数所做的操作进行了清除;应用程序也显然没有错误,用new生成的对象在最后也用delete删除了。然而这里却有很大的问题。程序的行为是不可预测的--无法知道将会发生什么。
c++语言标准关于这个问题的阐述非常清楚:当通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。这意味着编译器生成的代码将会做任何它喜欢的事:重新格式化你的硬盘,给你的老板发电子邮件,把你的程序源代码传真给你的对手,无论什么事都可能发生。(实际运行时经常发生的是,派生类的析构函数永远不会被调用。在本例中,这意味着当targetptr 删除时,enemytank的数量值不会改变,那么,敌人坦克的数量就是错的,这对需要高度依赖精确信息的部队来说,会造成什么后果?)
为了避免这个问题,只需要使enemytarget的析构函数为virtual。声明析构函数为虚就会带来你所希望的运行良好的行为:对象内存释放时,enemytank和enemytarget的析构函数都会被调用。
和绝大部分基类一样,现在enemytarget类包含一个虚函数。虚函数的目的是让派生类去定制自己的行为(见条款36),所以几乎所有的基类都包含虚函数。
如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。请看下面的例子,这个例子基于arm("the annotated c++ reference manual")一书的一个专题讨论。
// 一个表示2d点的类
class point {
public:
point(short int xcoord, short int ycoord);
~point();
private:
short int x, y;
};
如果一个short int占16位,一个point对象将刚好适合放进一个32位的寄存器中。另外,一个point对象可以作为一个32位的数据传给用c或fortran等其他语言写的函数中。但如果point的析构函数为虚,情况就会改变。
实现虚函数需要对象附带一些额外信息,以使对象在运行时可以确定该调用哪个虚函数。对大多数编译器来说,这个额外信息的具体形式是一个称为vptr(虚函数表指针)的指针。vptr指向的是一个称为vtbl(虚函数表)的函数指针数组。每个有虚函数的类都附带有一个vtbl。当对一个对象的某个虚函数进行请求调用时,实际被调用的函数是根据指向vtbl的vptr在vtbl里找到相应的函数指针来确定的。
虚函数实现的细节不重要(当然,如果你感兴趣,可以阅读条款m24),重要的是,如果point类包含一个虚函数,它的对象的体积将不知不觉地翻番,从2个16位的short变成了2个16位的short加上一个32位的vptr!point对象再也不能放到一个32位寄存器中去了。而且,c++中的point对象看起来再也不具有和其他语言如c中声明的那样相同的结构了,因为这些语言里没有vptr。所以,用其他语言写的函数来传递point也不再可能了,除非专门去为它们设计vptr,而这本身是实现的细节,会导致代码无法移植。
所以基本的一条是,无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
这是一个很好的准则,大多数情况都适用。但不幸的是,当类里没有虚函数的时候,也会带来非虚析构函数问题。 例如,条款13里有个实现用户自定义数组下标上下限的类模板。假设你(不顾条款m33的建议)决定写一个派生类模板来表示某种可以命名的数组(即每个数组有一个名字)。
template<class t> // 基类模板
class array { // (来自条款13)
public:
array(int lowbound, int highbound);
~array();
private:
vector<t> data;
size_t size;
int lbound, hbound;
};
template<class t>
class namedarray: public array<t> {
public:
namedarray(int lowbound, int highbound, const string& name);
...
private:
string arrayname;
};
如果在应用程序的某个地方你将指向namedarray类型的指针转换成了array类型的指针,然后用delete来删除array指针,那你就会立即掉进"不确定行为"的陷阱中。
namedarray<int> *pna =
new namedarray<int>(10, 20, "impending doom");
array<int> *pa;
...
pa = pna; // namedarray<int>* -> array<int>*
...
delete pa; // 不确定! 实际中,pa->arrayname
// 会造成泄漏,因为*pa的namedarray
// 永远不会被删除
现实中,这种情形出现得比你想象的要频繁。让一个现有的类做些什么事,然后从它派生一个类做和它相同的事,再加上一些特殊的功能,这在现实中不是不常见。namedarray没有重定义array的任何行为--它继承了array的所有功能而没有进行任何修改--它只是增加了一些额外的功能。但非虚析构函数的问题依然存在(还有其他问题,参见m33)
最后,值得指出的是,在某些类里声明纯虚析构函数很方便。纯虚函数将产生抽象类--不能实例化的类(即不能创建此类型的对象)。有些时候,你想使一个类成为抽象类,但刚好又没有任何纯虚函数。怎么办?因为抽象类是准备被用做基类的,基类必须要有一个虚析构函数,纯虚函数会产生抽象类,所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数。
这里是一个例子:
class awov { // awov = "abstract w/o
// virtuals"
public:
virtual ~awov() = 0; // 声明一个纯虚析构函数
};
这个类有一个纯虚函数,所以它是抽象的,而且它有一个虚析构函数,所以不会产生析构函数问题。但这里还有一件事:必须提供纯虚析构函数的定义:
awov::~awov() {} // 纯虚析构函数的定义
这个定义是必需的,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对~awov的调用,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来,最后还是得回去把它添上。
可以在函数里做任何事,但正如上面的例子一样,什么事都不做也不是不常见。如果是这种情况,那很自然地会想到将析构函数声明为内联函数,从而避免对一个空函数的调用所产生的开销。这是一个很好的方法,但有一件事要清楚。
因为析构函数为虚,它的地址必须进入到类的vtbl(见条款m24)。但内联函数不是作为独立的函数存在的(这就是"内联"的意思),所以必须用特殊的方法得到它们的地址。条款33对此做了全面的介绍,其基本点是:如果声明虚析构函数为inline,将会避免调用它们时产生的开销,但编译器还是必然会在什么地方产生一个此函数的拷贝。
#23
不要从没有虚析构函数的类继承,确保基类都有虚席构函数。
#24
我也是这个意思,基类类型中只有value.你用基类类型去操纵只能取的到value,因为它并不知道vptr+1是什么东东.如果想调用shift须用derived类型.调用虚函数的第一关在类型中,类型控制vptr
#25
恩。。我想这个问题好象差不多是这样了。不过不知道天使兄的文章是从什么地方来的。
呵呵。恩。不知道大家对我提问的这个切片的有什么看法呢。。
呵呵。恩。不知道大家对我提问的这个切片的有什么看法呢。。
#26
to:FireAngel
谢谢,等一下我慢慢看
to:xrbeck
我觉得"对象切片"的根本在于拷贝构造函数.
aclass
bclass :public aclass
void test(aclass a)
{
//dosomething
}
void main()
{
bclass b;
test(b);//造成对象切片.这样要构造一个test中的临时对象,由于参数类型是基类的,所以会
//调用aclass的拷贝构造,这个构造函数不可能构造子类的新成员,对象切片也就产
//生了.
}
谢谢,等一下我慢慢看
to:xrbeck
我觉得"对象切片"的根本在于拷贝构造函数.
aclass
bclass :public aclass
void test(aclass a)
{
//dosomething
}
void main()
{
bclass b;
test(b);//造成对象切片.这样要构造一个test中的临时对象,由于参数类型是基类的,所以会
//调用aclass的拷贝构造,这个构造函数不可能构造子类的新成员,对象切片也就产
//生了.
}
#27
恩。这个是切片的时候采取的方法。可是有一点。我们知道派生类并不能继承基类的私有对象。
而在调用copy constructor的时候是用了比如m=b.m这样的赋值。。按照书上说。派生类
包括了基类的一份COPY。那基类的私有成员。。?
而在调用copy constructor的时候是用了比如m=b.m这样的赋值。。按照书上说。派生类
包括了基类的一份COPY。那基类的私有成员。。?
#28
我想应该还是隐含的吧.比如上例中的aclass有个private的int a;bclass有个private的int b;在拷贝构造时,会根据b的属性构造临时对象,b中隐含有a,但b访问不到.
在b的构造时先会调用a的构造,这时int a会被赋值.也就是说b中隐含有int a,且a是被赋值的.我揣测拷贝构造会忽略private
有段代码你看看,写的有点乱
#include <iostream.h>
class a
{
public:
a(int i = 0)
{
h = i;
}
a(a& a1)
{
h = a1.h;
}
void display()
{
cout<<h<<endl;
}
private:
int h;
};
class b :public a
{
public:
b(int i = 0):a(8)
{
x = i;
}
private:
int x;
};
void test(a f)
{
f.display();
}
void main()
{
b b;
test(b);
}
输出8
在b的构造时先会调用a的构造,这时int a会被赋值.也就是说b中隐含有int a,且a是被赋值的.我揣测拷贝构造会忽略private
有段代码你看看,写的有点乱
#include <iostream.h>
class a
{
public:
a(int i = 0)
{
h = i;
}
a(a& a1)
{
h = a1.h;
}
void display()
{
cout<<h<<endl;
}
private:
int h;
};
class b :public a
{
public:
b(int i = 0):a(8)
{
x = i;
}
private:
int x;
};
void test(a f)
{
f.display();
}
void main()
{
b b;
test(b);
}
输出8
#29
诶。。要看懂你写什么是真的很吃力。。呵呵。
不过大概知道吧。恩。我觉得你这个例子似乎不能说明什么问题。
不如把a(a& a1)
{
h = a1.h;
}
改为
h=10;这样还能反映调用了一个拷贝构造函数。
我的理解。的确是有继承基类的私有成员。可是编译器不允许我们直接使用。
我们看看这个:
class base
{
private:
int m;
int n;
public:
base(){cout<<"base constructor"<<endl;}
~base(){cout<<"base destructor"<<endl;}
};
class derived:public base
{
private:
int k;
public:
derived(){cout<<"derived constructor"<<endl;}
~derived(){cout<<"derived destructor"<<endl;}
};
class node
{
private:
int k;
public:
node(){cout<<"node constructor"<<endl;}
~node(){cout<<"node destructor"<<endl;}
};
int main(int argc, char* argv[])
{
node nodetemp;
base temp1;
derived temp2;
base temp3=derived();
cout<<"nodetemp's size is :"<<sizeof(node)<<endl;
cout<<"base's size is :"<<sizeof(temp1)<<endl;
cout<<"derived's size is :"<<sizeof(temp2)<<endl;
cout<<"temp3's size is :"<<sizeof(temp3)<<endl;
getch();
return 0;
}
可以发现派生类其实有基类的成员。(长度为12)。不过是我们不能直接用而已。。
这是我的理解。不知道对不对。。
不过大概知道吧。恩。我觉得你这个例子似乎不能说明什么问题。
不如把a(a& a1)
{
h = a1.h;
}
改为
h=10;这样还能反映调用了一个拷贝构造函数。
我的理解。的确是有继承基类的私有成员。可是编译器不允许我们直接使用。
我们看看这个:
class base
{
private:
int m;
int n;
public:
base(){cout<<"base constructor"<<endl;}
~base(){cout<<"base destructor"<<endl;}
};
class derived:public base
{
private:
int k;
public:
derived(){cout<<"derived constructor"<<endl;}
~derived(){cout<<"derived destructor"<<endl;}
};
class node
{
private:
int k;
public:
node(){cout<<"node constructor"<<endl;}
~node(){cout<<"node destructor"<<endl;}
};
int main(int argc, char* argv[])
{
node nodetemp;
base temp1;
derived temp2;
base temp3=derived();
cout<<"nodetemp's size is :"<<sizeof(node)<<endl;
cout<<"base's size is :"<<sizeof(temp1)<<endl;
cout<<"derived's size is :"<<sizeof(temp2)<<endl;
cout<<"temp3's size is :"<<sizeof(temp3)<<endl;
getch();
return 0;
}
可以发现派生类其实有基类的成员。(长度为12)。不过是我们不能直接用而已。。
这是我的理解。不知道对不对。。
#30
啊?加到300分了?赶快翻书找问题啊!兄弟们快来啊!呵呵。。
#31
把你们的代码反汇编以后看看就什么都明白了。
#32
在DELETE P的时候调用的CB的西勾函数,其中调用FN(VOID),此时首先调用本类的FN()
所以输出CB,这些找一本基础的C++的书,上面有很详细的说明的
所以输出CB,这些找一本基础的C++的书,上面有很详细的说明的
#33
to:xrbeck
我明白你的意思了.你举的例子中derived的对象temp2确实有base中的m,n.只是m,n是隐含的,不能直接访问.必须用自己的方法对基类中的方法进行嵌套,如:
#include <iostream.h>
class base
{
private:
int m;
int n;
public:
base(int M = 2,int N = 3){m = M ;n = N;}
void display(){cout<<m<<" "<<n<<endl;}
~base(){}
};
class derived:public base
{
private:
int k;
public:
derived(int K = 5){k = K;}
void accessmn(){display();}
~derived(){}
};
void main()
{
derived dobj;
dobj.accessmn();
}
我明白你的意思了.你举的例子中derived的对象temp2确实有base中的m,n.只是m,n是隐含的,不能直接访问.必须用自己的方法对基类中的方法进行嵌套,如:
#include <iostream.h>
class base
{
private:
int m;
int n;
public:
base(int M = 2,int N = 3){m = M ;n = N;}
void display(){cout<<m<<" "<<n<<endl;}
~base(){}
};
class derived:public base
{
private:
int k;
public:
derived(int K = 5){k = K;}
void accessmn(){display();}
~derived(){}
};
void main()
{
derived dobj;
dobj.accessmn();
}
#34
诶。是啊是啊。我就是这个意思。。这个是我的理解。。
不过你说的“必须用自己的方法。。”这个可能不对。要访问的话只能根据基类提供的接口吧。恩
哎呀。今天晚上再看看这本书。看看还有什么要讨论的。。呵呵。。
不过你说的“必须用自己的方法。。”这个可能不对。要访问的话只能根据基类提供的接口吧。恩
哎呀。今天晚上再看看这本书。看看还有什么要讨论的。。呵呵。。
#35
大家还有要说的吗?没有的话就给分了(今天晚上)
#36
up
#37
up
#38
回复人: magicblue(小飞侠) (2001-8-20 0:09:44) 得0分
有这样一段代码:
#include <iostream>
class A
{
public:
~A (void);
virtual void FuncA (void)
{
std::cout<<"aaa";
}
};
class B: public A
{
public:
virtual void FuncA(void)
{
std::cout<<"bbb";
}
};
A::~A (void)
{
FuncA ();
}
int main()
{
B * vb = new B;
delete vb;
}
会输出aaa
class b 的析枸函数不存在,调用父类的析构函数,
有这样一段代码:
#include <iostream>
class A
{
public:
~A (void);
virtual void FuncA (void)
{
std::cout<<"aaa";
}
};
class B: public A
{
public:
virtual void FuncA(void)
{
std::cout<<"bbb";
}
};
A::~A (void)
{
FuncA ();
}
int main()
{
B * vb = new B;
delete vb;
}
会输出aaa
class b 的析枸函数不存在,调用父类的析构函数,
#39
faint!
怎么参与的人这么多?
我都不好意思再贴了
怎么参与的人这么多?
我都不好意思再贴了
#40
记住scott Meyers 的一句话吧:
通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。这意味着编译器生成的代码将会做任何它喜欢的事
实际运行时经常发生的是,派生类的析构函数永远不会被调用
通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。这意味着编译器生成的代码将会做任何它喜欢的事
实际运行时经常发生的是,派生类的析构函数永远不会被调用
#41
在C++编程思想上提到
(1)构造顺序:Ca->Cb->Cc; 盖房子当然是从下向上盖的。
析构顺序:Cc->Cb->Ca; 拆房子当然是从上向下拆的。
(2)在构造与析构中虚机制失效,所有在构造与析构中调用的函数都以实函数的方式调用。
(3)关于造型(借用的java术语,CAST),
static_cast<classa *>(classb*):不考虑实际的对象类型会造成重大损失。
dynamic_cast<classa *>(classb *):考虑实际对象类型,是安全的。
你最开始提到的:
明白了,Ca中没有定义虚析构,相当于在派生类中增加新的虚函数,所以调用没结果.
不过我把Ca* p = new Cb;改为Cb b(错误在这里);
Ca* p = &b;
delete p;//错误,会造成对b的两次删除。一次是delete,一次
运行时出错是为什么?
(1)构造顺序:Ca->Cb->Cc; 盖房子当然是从下向上盖的。
析构顺序:Cc->Cb->Ca; 拆房子当然是从上向下拆的。
(2)在构造与析构中虚机制失效,所有在构造与析构中调用的函数都以实函数的方式调用。
(3)关于造型(借用的java术语,CAST),
static_cast<classa *>(classb*):不考虑实际的对象类型会造成重大损失。
dynamic_cast<classa *>(classb *):考虑实际对象类型,是安全的。
你最开始提到的:
明白了,Ca中没有定义虚析构,相当于在派生类中增加新的虚函数,所以调用没结果.
不过我把Ca* p = new Cb;改为Cb b(错误在这里);
Ca* p = &b;
delete p;//错误,会造成对b的两次删除。一次是delete,一次
运行时出错是为什么?
#42
在C++编程思想上提到
(1)构造顺序:Ca->Cb->Cc; 盖房子当然是从下向上盖的。
析构顺序:Cc->Cb->Ca; 拆房子当然是从上向下拆的。
(2)在构造与析构中虚机制失效,所有在构造与析构中调用的函数都以实函数的方式调用。
(3)关于造型(借用的java术语,CAST),
static_cast<classa *>(classb*):不考虑实际的对象类型会造成重大损失。
dynamic_cast<classa *>(classb *):考虑实际对象类型,是安全的。
你最开始提到的:
明白了,Ca中没有定义虚析构,相当于在派生类中增加新的虚函数,所以调用没结果.
不过我把Ca* p = new Cb;改为Cb b(错误在这里);
Ca* p = &b;
delete p;//错误,会造成对b的两次删除错误(DEbug错误)。
//一次是delete p,一次是局部变量的b对象的删除。
运行时出错是为什么?
(4)我觉的public,private,protected等权限修饰符我认为只是编译期间有效C++编译的事,在运行期间是没有这些权限存在的??
(1)构造顺序:Ca->Cb->Cc; 盖房子当然是从下向上盖的。
析构顺序:Cc->Cb->Ca; 拆房子当然是从上向下拆的。
(2)在构造与析构中虚机制失效,所有在构造与析构中调用的函数都以实函数的方式调用。
(3)关于造型(借用的java术语,CAST),
static_cast<classa *>(classb*):不考虑实际的对象类型会造成重大损失。
dynamic_cast<classa *>(classb *):考虑实际对象类型,是安全的。
你最开始提到的:
明白了,Ca中没有定义虚析构,相当于在派生类中增加新的虚函数,所以调用没结果.
不过我把Ca* p = new Cb;改为Cb b(错误在这里);
Ca* p = &b;
delete p;//错误,会造成对b的两次删除错误(DEbug错误)。
//一次是delete p,一次是局部变量的b对象的删除。
运行时出错是为什么?
(4)我觉的public,private,protected等权限修饰符我认为只是编译期间有效C++编译的事,在运行期间是没有这些权限存在的??
#43
我看了
-----------------------------------------------------------
回复人: FireAngel(堕落天使) (2001-8-20 13:36:22) 得30分
条款14: 确定基类有虚析构函数
-----------------------------------------------------------
我用VC6.0 WIN98试了一下
对我前面的内容补充一点:如果基类的析构是非虚的,则会造成不能正确析构对象。
虚析构基类,析构顺序:是指向Cc对象的Ca指针delete时 Cc->Cb->Ca;
若非虚基类 是指向Cc对象的指针delete时 Ca;只析构了Ca (dynamic_cast转换也同样结果)
这里说的基类应该是根基类(第一个基类),这种情况也只对根基类有效。
*************
我在调试中发现:
如果根基类是虚的,则不管它的派生类是否有虚析构,都等同有虚析构函数。不知看法对否?
-----------------------------------------------------------
回复人: FireAngel(堕落天使) (2001-8-20 13:36:22) 得30分
条款14: 确定基类有虚析构函数
-----------------------------------------------------------
我用VC6.0 WIN98试了一下
对我前面的内容补充一点:如果基类的析构是非虚的,则会造成不能正确析构对象。
虚析构基类,析构顺序:是指向Cc对象的Ca指针delete时 Cc->Cb->Ca;
若非虚基类 是指向Cc对象的指针delete时 Ca;只析构了Ca (dynamic_cast转换也同样结果)
这里说的基类应该是根基类(第一个基类),这种情况也只对根基类有效。
*************
我在调试中发现:
如果根基类是虚的,则不管它的派生类是否有虚析构,都等同有虚析构函数。不知看法对否?
#44
不对吧, dynamic和static_cast是一样的吧,
都是安全的,区别在于一个在编译时作类型检查,一个在运行时
很早看的,不知道记得对不对,有一个操作符re**_cast(:(,好久不用c++了)
才是不顾类型的强制转换也。
都是安全的,区别在于一个在编译时作类型检查,一个在运行时
很早看的,不知道记得对不对,有一个操作符re**_cast(:(,好久不用c++了)
才是不顾类型的强制转换也。
#45
我也试过两种,但是MSDN上如是说:
class B { ... };
class D : public B { ... };
void f(B* pb)
{
D* pd1 = dynamic_cast<D*>(pb);
D* pd2 = static_cast<D*>(pb);
}
If pb really points to an object of type D, then pd1 and pd2 will get the same value. They will also get the same value if pb == 0.
If pb points to an object of type B and not to the complete D class, then dynamic_cast will know enough to return zero. However, static_cast relies on the programmer’s assertion that pb points to an object of type D and simply returns a pointer to that supposed D object.
Consequently, static_cast can do the inverse of implicit conversions, in which case the results are undefined. It is left to the programmer to ensure that the results of a static_cast conversion are safe.
------------------------------------------------------------------
你说的强制转换是
reinterpret_cast Operator
C++ Specific —>
reinterpret_cast < type-id > ( expression )
The reinterpret_cast operator allows any pointer to be converted into any other pointer type, and it allows any integral type to be converted into any pointer type and vice versa. Misuse of the reinterpret_cast operator can easily be unsafe. Unless the desired conversion is inherently low-level, you should use one of the other cast operators.
END C++ Specific
class B { ... };
class D : public B { ... };
void f(B* pb)
{
D* pd1 = dynamic_cast<D*>(pb);
D* pd2 = static_cast<D*>(pb);
}
If pb really points to an object of type D, then pd1 and pd2 will get the same value. They will also get the same value if pb == 0.
If pb points to an object of type B and not to the complete D class, then dynamic_cast will know enough to return zero. However, static_cast relies on the programmer’s assertion that pb points to an object of type D and simply returns a pointer to that supposed D object.
Consequently, static_cast can do the inverse of implicit conversions, in which case the results are undefined. It is left to the programmer to ensure that the results of a static_cast conversion are safe.
------------------------------------------------------------------
你说的强制转换是
reinterpret_cast Operator
C++ Specific —>
reinterpret_cast < type-id > ( expression )
The reinterpret_cast operator allows any pointer to be converted into any other pointer type, and it allows any integral type to be converted into any pointer type and vice versa. Misuse of the reinterpret_cast operator can easily be unsafe. Unless the desired conversion is inherently low-level, you should use one of the other cast operators.
END C++ Specific
#46
谢谢你的回复(可惜没分了):)
> 如果根基类是虚的,则不管它的派生类是否有虚析构,都等同有虚析构函数。不知看法对否?
我不太明白你的意思,基类的析构是虚的,派生类的析构当然也是虚的
> 如果根基类是虚的,则不管它的派生类是否有虚析构,都等同有虚析构函数。不知看法对否?
我不太明白你的意思,基类的析构是虚的,派生类的析构当然也是虚的
#47
其实所讨论的一切,就是在讨论编译器是怎么处理的。别忘了,编译器的规则是由那些计算机的专家确定下来的,编译器其实不是智能的,否则相同的程序在不同的时刻或者不同环境下,就有可能出现不同,就是不确定性,这是不允许的。所以这一切都是在那些计算机专家的对OO的理解后,留给我们的就是不同的面向对象的语言。计算机语言就必须是确定的,所以很多讨论的事情可能就是没有道理的,就是那样规定的。就如在linux和 windows的C++对于某些程序编译的结果是截然不同。
很多的事物是在他们出现以后,才去寻找理论去解释。不是解释了什么,事物才出现的。就如有时候做的程序,等用户使用后,他们所使用的某些功能,是事先并没有想到,连你自己也很惊讶的。
很多的事物是在他们出现以后,才去寻找理论去解释。不是解释了什么,事物才出现的。就如有时候做的程序,等用户使用后,他们所使用的某些功能,是事先并没有想到,连你自己也很惊讶的。