原问题如下:
#include <iostream>
using namespace std;
class base{
public:
virtual void fun1()
{
cout << "fun1 called !" << endl;
}
void fun2()
{
cout << "fun2 called !" << endl;
}
};
int main()
{
base s;
cout << sizeof(s) << endl;
return 0;
}
代码的结果为4。
这个我觉得是因为那个虚函数表里的一个指针占了4个字节
但是如果我去掉virtual 代码的结果为1
类中的普通成员函数占对象的空间吗?数据成员所占空间的计算是否也是和结构体类似呢?
这个就不明白了,请各位指点。。
我的回答如下:
这个涉及到类和结构体,在C++内部的排列方式。
我也不是很了解,只能就自己了解的一点知识做点回答,欢迎大家指正。
我们知道,C和C++虽然都支持结构体,但是,实际上表现是不一样的。C++的结构体,可以认为是类的一种变体,二者的差异性,类中成员,如果不声明,默认是Private的,结构体中成员,如果不声明,则默认是Public的。
但是,在C++里面,二者内部都可以内置成员函数,而C的结构体,内部只允许存在成员变量,如果需要内置成员函数,需要程序员显式声明函数指针变量,换句话说,就是C在结构体中管理成员函数,是程序员自己来管理,C++则是编译器代为管理。
这意味着什么呢?
在C++中,成员函数和成员变量,都是类和结构体的成员,但二者有所差异。
编译器在编译每个类时,不管这个类以后会实例化几个对象,首先,它会提取这些类的共性,放到一起,做成一个表。
比如类里面的非虚函数,这类函数,所有的对象共享一段函数代码,自然没有必要每个对象内部都设置一个函数指针,这太浪费内存了。
因此,一个类,所有的非虚函数,会被编译器排成一个符号表,放置在特定的编译期基础变量区。这实际表现看,是放在exe文件里面的,在调用一个程序时,是直接从文件中读出,并经过地址修订,准备使用,这部分连基栈都算不上,算是常量区了,所有的常量也是放在这个区。
嗯,函数内部的静态变量,类中的静态变量,静态函数,都是这个区。
那,除掉这些,类里面还有什么呢?
还有虚函数,我们知道,虚函数表示可能继承,事实上,多次(不是多重)继承后,一个类的虚函数内部会有一个栈,每个虚函数都有一个栈,每次调用该函数,会从栈顶开始call,当然,如果程序员愿意,也可以在继承的虚函数内部,通过调用父类的同名虚函数,逐级向下call,直至call完所有的虚函数为止。
这就说明,虚函数和普通成员函数不同,每个对象都有可能变化,因此,编译器就不敢把这个函数的指针,放在常量区,必须跟着对象走,注意,不是类,类是没有实体的,因此,不存在sizeof,只有对象存在大小。
还有就是普通成员变量,这些内容,每个对象也是不一样的,因此,每个对象必须自己建立一个表来管理,否则大家就混了。
因此,我们知道了,每个类,实例化对象之后,其实对象的实体在内存中的存储,就只包含虚函数和普通成员变量,这是C++编译器为了节约内存做得优化。
我们回到你的代码看,你的代码中,fun2是普通函数,被编译器放到常量区去了,因此,不占用对象空间,虚函数fun1,则需要占用,我们知道,32位操作系统,一个指针是4Bytes,函数指针也是指针,因此,你的结果是4Bytes。
取消了virtual 之后,fun1也变成了普通函数,因此和fun2等同处理,就不再占用对象空间,因此,对象空间为0了。
不过,我隐隐约约听谁说过,C++语言不允许对象空间为0,这样的话,对象指针就没有落点了,因此,一个对象的空间,至少占用1Byte,这就是你的结果为1的原因。
不知道这样能不能帮你解惑,呵呵,一家之言哈,欢迎拍砖。
====================================
肖老师,你的这篇文章解我心中长久以来的疑虑,对菜鸟我极为有用!
老师你在文章中说:......类里面还有什么呢?
还有虚函数,我们知道,虚函数表示可能继承,事实上,多次(不是多重)继承后,一个类的虚函数内部会有一个栈,每个虚函数都有一个栈,每次调用该函数,会从栈顶开始call,当然,如果程序员愿意,也可以在继承的虚函数内部,通过调用父类的同名虚函数,逐级向下call,直至call完所有的虚函数为止。
--------------------------------
肖老师,上面这段话菜鸟我看来看去还是不懂,能否请你稍微解释的详细一些?也许对虚函数调用的内部实现机制听了你说的就明白了.不好意思,给你添麻烦了.
====================================
这是说,类的虚函数,实际上内部存储上,表现为一个函数指针栈,栈底,是基类这个函数的指针,往上,实际上是继承类,该虚函数的继承函数的指针,一个类,被继承几次,比如说3次,最后一次继承,这个栈就有3层。有点绕。
举个例子吧
class A
{
virtual void Func(void)
};
class B : public A
{
virtual void Func(void)
};
class C : public B
{
virtual void Func(void)
};
这个A类,里面的Func指针就是它自己
B就是一个栈了,栈底是A::Func,栈顶是B::Func
而C就是三层的栈了,在B的基础上,栈顶又压入了C::Func
基本上就是这个管理关系。
我的话的意思是,在任何一层继承函数,都可以去手动去call父类的对应函数,完成对整个栈链上所有函数的调用。
因为我们知道,一个类的虚函数,一旦被继承,原来的父类函数指针就被压倒栈下面去了,从栈顶看,只有最后一层的函数指针。
比如C这个类看,我们看它的Func,只要它继承并实现了,那么,调用Func一定只能调用C::Func,B和A的由于看不到,因此是不会被调用的。
当然,如果C没有实现这个虚函数,则Func的栈上,没有C::Func,因此,直接Call会Call到B::Func,以此类推,如果B没有实现这个虚函数,表示未继承,则Call会Call到A::Func,这就是虚函数继承中,后实现的覆盖前实现的原理。
当然,如果A内没有实现Func的实体,做了一个纯虚函数,而B和C这些继承类也不实现,那么,编译器在构造符号表的时候,就会找不到任何一个Func的实体,该虚函数栈为空,无法连接,因此会报连接失败的错误,编译不能通过。
这种栈式管理,有好有坏,好处是后面的继承类,可以选择实现虚函数,也可以选择不实现,偷个懒。程序不会出错,下次调用该函数,会自动沿着它的继承关系,寻找父类以及更往前的爷爷类的函数实体,至少能找到一个执行其功能,简化开发。
但是,也有一个坏处,就是一个虚函数,一旦被继承类实现了,则父类的必然被覆盖,如果父类有什么内置的功能,就没有办法执行了,这很麻烦,由于面向对象的继承关系,我们总是希望,继承类的对应函数,只要完成它相对于父类增加的那部分功能就够了,父类的功能,还能继续执行,免得写重复的代码。
这个例子在MFC开发中很多,很多时候,我们的一个窗口类,是从CDialog这个类继承的,而CDialog,又是CWnd这个类继承的。针对一个虚函数方法,比如说CWnd::Create这个方法。
virtual BOOL Create( LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect...
我们知道,创建一个窗口有一大堆事情要做,这些事情,MFC已经在CWnd的Create这个函数里面实现好了,但好死不死,它把这个函数方法设置为虚函数了,就是说,后续继承类可以自己来实现这个方法。
我们这么来假设,如果我们那个工程的窗口,继承自CDialog,然后,我们自己实现了这个Create方法,那完蛋了,由于C++这个覆盖特性,执行的时候,就只执行我们这个Create了,下面的CDailog::Create和CWnd::Create都执行不了,除非我们把那两个函数内部所有的代码抄一遍,否则,这个Create根本没有办法完成我们希望完成的功能。他失去了创建窗口的功能。
因此,为了解决这个问题,C++允许继承类的虚函数,显式调用父类的虚函数,以实现父类的基础功能,最后,才是我们自己新增加的代码。
这个意思主要是说,虚函数的继承,看似省事,但他不是想当然会先实现父类功能,后调用新增代码,需要我们手动call。
再看看这个例子,我们以VC建立一个MFC的对话框工程,就叫test。
// CtestDlg 对话框
class CtestDlg : public CDialog
{
// 构造
public:
CtestDlg(CWnd* pParent = NULL); // 标准构造函数
// 对话框数据
enum { IDD = IDD_TEST_DIALOG };
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
// 实现
protected:
HICON m_hIcon;
// 生成的消息映射函数
virtual BOOL OnInitDialog(); //看好这一句啊,虚函数
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
DECLARE_MESSAGE_MAP()
};
注意其中OnInitDialog,好,我们来看看VC自动为我们生成的这个函数是怎么写的:
BOOL CtestDlg::OnInitDialog()
{
CDialog::OnInitDialog(); //看这句,在干吗?
// 将“关于...”菜单项添加到系统菜单中。
// IDM_ABOUTBOX 必须在系统命令范围内。
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
CString strAboutMenu;
strAboutMenu.LoadString(IDS_ABOUTBOX);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// 设置此对话框的图标。当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标
// TODO: 在此添加额外的初始化代码
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}
注意到没,由于继承类的虚函数一旦实现,父类的虚函数就被自动屏蔽,VC也必须手动实现对父类虚函数的层级调用,才能完成基本功能。
很多时候,我们的同学,手动继承一个类之后,玩虚函数老是忘了这个手动调用父类,结果发现,虚函数功能越继承越少,甚至继承到功能没有了,就是搞忘了这点。
但是,上述代码是VC的向导自动添加的,VC并没有对此作显式说明,结果,大家在只用IDE开发的过程中,老是关注不到这个细节,自己做的时候就出错。这类问题还很多。
这在开发中,表现出来的就是,向导的代码永远正确,自己的手工代码一写就错,慢慢弄下来,搞得很多人都不敢手工写代码了。
我原来写文章建议,初学者不要用IDE,其实就是指,这些细节被IDE自动完成,程序员关注不到,就学不到真东西,搞得哪天一手写,就出错。
呵呵,扯远了,虚函数的继承关系,我知道的大概就这么点,已经全部兜给你了,再不够,只有请你自己查书了。
呵呵,好累。
====================================
#include <iostream>
using namespace std;
class base{
public:
virtual void fun1()
{
cout << "fun1 called !" << endl;
}
void fun2()
{
cout << "fun2 called !" << endl;
}
double d;
static int a;
};
int main()
{
base s;
cout << sizeof(s) << endl;
return 0;
}
疑问 1 double 是8个字节 这个程序运行是16个字节 除了虚函数的4个 那多余处理的4个呢?
疑问 2 static int a;我将将static int 换成double是24个没错,可我将static 去掉,那么个int应该是4个字节 共
4+8+4=16 可程序运行是24个 why?
====================================
晕了,这个问题很难,我够呛能回答出来。
先做做试验吧。
#include <iostream>
using namespace std;
class base
{
public:
virtual void fun1()
{
cout << "fun1 called !" << endl;
}
void fun2()
{
cout << "fun2 called !" << endl;
}
};
void TestBase(void)
{
base b;
printf("object length=%d\n",sizeof(b));
dbg_bin((char*)&b,sizeof(b));
}
我先打印一下基本类的内容,这个dbg_bin是我自己的工具,二进制输出一段内存区的内容。打印结果如下:
object length=4
0013FE8C - E4 58 44 00 .XD.
对象长度4Bytes,内部只有一个指针,应该就是Fun1的指针,0x004458E4
ok,我们再来打印你的例子,这里先说明一点,在你的例子中,由于a从头到尾都没有被使用过,因此实际上编译器根本就没有为其分配单元,不信,你到main中试图访问一下a,会失败,正解是应该在类声明完毕后,马上做一次显式的静态变量赋值动作,以强迫编译器为a分配内存单元。
#include <iostream>
using namespace std;
class base
{
public:
virtual void fun1()
{
cout << "fun1 called !" << endl;
}
void fun2()
{
cout << "fun2 called !" << endl;
}
double m_dDouble; //d
static int m_nStaticInt; //a
};
int base::m_nStaticInt=15; //看好这里,一次赋值动作,强迫编译器分配
void TestBase(void)
{
base b;
b.m_dDouble=0.0;
printf("object length=%d\n",sizeof(b));
printf("b.m_dDouble=%f\n",b.m_dDouble);
printf("base::m_nStaticInt=%d\n",b.m_nStaticInt);
dbg_bin((char*)&b,sizeof(b));
}
我们来看看输出结果。
object length=16
b.m_dDouble=0.000000 //这应该是8Bytes的0
base::m_nStaticInt=15 //赋值成功,但是,注意下面,没有它的踪迹
0013FE80 - 90 38 44 00 CC CC CC CC 00 00 00 00 00 00 00 00 .8D.............
我们看看哈,第一个4Bytes,应该是虚函数fun1的指针,0x00443890
第二个,4个cc,这是vc编译器在debug模式下默认的占位数字,没有意义,只是表示从进程空间创建以来,还没有为其填充过数字,这几个单元,我猜测是没有使用,属于占位符。
后面8个00,显然是double的空间,a不见了,因为静态变量,不随对象走,是在基栈中,所有这个类的对象都看得见,大家共用一个变量内存区。
ok,我们再把a的static修饰去掉看看。
#include <iostream>
using namespace std;
class base
{
public:
virtual void fun1()
{
cout << "fun1 called !" << endl;
}
void fun2()
{
cout << "fun2 called !" << endl;
}
double m_dDouble; //d
int m_nStaticInt; //a
};
void TestBase(void)
{
base b;
b.m_dDouble=0.0;
b.m_nStaticInt=15; //赋初值,好辨认
printf("object length=%d\n",sizeof(b));
printf("b.m_dDouble=%f\n",b.m_dDouble);
printf("b.m_nStaticInt=%d\n",b.m_nStaticInt);
dbg_bin((char*)&b,sizeof(b));
}
好,打印出来看。
object length=24
b.m_dDouble=0.000000
b.m_nStaticInt=15
0013FE78 - 90 38 44 00 CC CC CC CC 00 00 00 00 00 00 00 00 .8D.............
0013FE88 - 0F 00 00 00 CC CC CC CC ........
前面都差不多,不过,a放进对象里面了,值是15(0xF),后面又多了四个占位Bytes。
ok,再做个实验,我们把a换成double
#include <iostream>
using namespace std;
class base
{
public:
virtual void fun1()
{
cout << "fun1 called !" << endl;
}
void fun2()
{
cout << "fun2 called !" << endl;
}
double m_dDouble; //d
double m_dDouble2; //a
};
void TestBase(void)
{
base b;
b.m_dDouble=0.0;
b.m_dDouble2=1.0;
printf("object length=%d\n",sizeof(b));
printf("b.m_dDouble=%f, size=%d\n",b.m_dDouble,sizeof(b.m_dDouble));
printf("b.m_dDouble2=%f, size=%d\n",b.m_dDouble2,sizeof(b.m_dDouble2));
dbg_bin((char*)&b,sizeof(b));
}
来看看结果:
object length=24
b.m_dDouble=0.000000, size=8
b.m_dDouble2=1.000000, size=8
0013FE78 - 90 38 44 00 CC CC CC CC 00 00 00 00 00 00 00 00 .8D.............
0013FE88 - 00 00 00 00 00 00 F0 3F .......?
具体原因我也解释不清楚啦,能看到的就这些,貌似这个C++对象是以8Bytes取整的,即不够8Bytes,会填充占位符,这个我没找到相关理论依据,只能说这是编译器的特性了。
在很多时候,32位编译器为了优化,可能会以4Bytes的整数倍,做向上取整,这是常见手法,不过,这8Bytes取整,我也第一回遇到,我也搞不懂。看有没有哪位高人帮忙解释一下?
====================================