C++类对象大小规则总结
- 成员函数不占类对象的内存空间
- 一个类对象至少占1字节内存空间
- 成员变量是包含在每个对象中,占字节的
- 成员函数虽然也写在类定义中,但成员函数不占类对象的字节空间,也就是说,成员函数是跟着类走的,跟类对象没有关系,不管用这个类产生了多少个该类的对象
- 静态成员变量不计算在对象的sizeof内
- 普通成员函数和静态成员函数不计算在sizeof内
虚基类
如果某个派生类的部分或全部直接基类是从另一个共同基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同名称,因此派生类中也就会产生同名现象,对这种类型的同名成员也要使用作用域限定符来唯一标识,而且必须用直接基类来限定。
这种情况下,派生类对象在内存中就同时拥有成员a的两份同名拷贝。对于数据成员来说,虽然两个a可以存放不同的值,也可以使用作用域限定符来区分并访问,但很多情况下,我们只需要一个这样的数据副本。同一成员的多份副本增加了内存开销。C++中提供了虚基类技术来解决这一问题。
上例中其实Base0的成员函数fun0()的代码只有一个副本,之所以调用fun0()函数,仍需要需要直接基类名加以限定,是因为调用非静态函数,必须用对象来调用。
class Base0 {
public:
int var0;
void fun0(){
printf("Base0.show()\n");
}
protected:
};
class Base1 : public Base0 {
};
class Base2 : public Base0 {
};
class Derived : public Base1, public Base2 {
};
int main(){
Derived d;
d.Base1::fun0();
d.Base2::fun0();
d.Base1::var0 = 1;
d.Base2::var0 = 2;
printf("var0=%d\n", d.Base1::var0);
printf("var0=%d\n", d.Base2::var0);
}
//输出
Base0.show()
Base0.show()
var0=1
var0=2
上例中,Derived 类的对象中存在两个Base类的子对象,即Base1的对象和Base2的对象。因此调用show()函数时,需要使用Base1或Base2加以限定,这样才能明确针对哪个Base对象的调用。
当某类的部分或全部基类是从另一个共同基类派生而来时,这些直接基类从上一级共同基类中继承来的成员就拥有相同的名称。在派生类对象中,这些同名数据成员就拥有多个副本,可以使用作用域限定符来唯一标识并访问他们,也可以将共同基类设置为虚基类。
这时从不同路径继承过来的同名数据成员在内存中就只有一个副本,同一个函数名也只有一个映射,这样就解决了同名成员的唯一标识问题。
class Base0 {
public:
int var0;
void fun0(){
printf("Base0.fun0()\n");
}
};
class Base1 : virtual public Base0 {
public:
int var1;
};
class Base2 : virtual public Base0 {
public:
int var2;
};
class Derived : public Base1, public Base2 {
public:
int var;
void fun(){
printf("Derived.fun()\n");
}
};
int main(){
Derived d;
d.var0 = 2;
d.fun();
return 0;
}
//输出
Derived.fun()
比较一下使用作用域限定符和虚基类技术,前者在派生类中拥有同名成员的多个副本,分别通过限定基类名称来唯一标识,可以存放不同的数据,进行不同的操作;后者只维护一份成员副本。相比之下,前者可以容纳更多的数据,而后者使用更为简洁,内存空间更为节省。具体程序设计中,要根据实际问题的需要来选用合适的方法。
如果虚基类声明有带参的构造函数,并且没有声明无参构造函数,这时,在整个继承关系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化列表中列出对虚基类的初始化。
如果虚基类声明有带参的构造函数,并且没有声明无参构造函数,这时,在整个继承关系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化列表中列出对虚基类的初始化。
class Base0 {
public:
Base0(int var):var0(var){}
int var0;
void fun0(){
printf("Base0.fun0()\n");
}
};
class Base1 : virtual public Base0 {
public:
Base1(int var):Base0(var){}
int var1;
};
class Base2 : virtual public Base0 {
public:
Base2(int var):Base0(var){}
int var2;
};
class Derived : public Base1, public Base2 {
public:
Derived(int var):Base0(var),Base1(var),Base2(var){}
int var;
void fun(){
printf("Derived.fun()\n");
}
};
int main(){
Derived d(1);
d.var0 = 2;
d.fun();
return 0;
}
建立对象d时,Derived是虚基类Base0的最远派生类,建立一个对象时,如果这个对象含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
而且,只有最远派生类的构造函数会调用虚基类的构造函数,该派生类的基它基类(Base1和Base2)对虚基类构造函数的调用都自动被忽略。
构造一个类的对象的一般顺序
如果构造方法中有类类型参数,则先执行拷贝构造函数
如果该类有直接或间接的虚基类,则先执行虚基类的构造函数
如果该类有其他基类,则按照它们在继承声明列表中出现的次序,分别执行它们的构造函数,但构造过程中,不再执行它们的虚基类构造函数(有参构造方法的调用,需要在初始化列表中声明)
按照在类定义中出现的次序,对派生类中新增的成员对象进行初始化。对于类类型的成员对象,如果出现在构造函数的初始化列表中,则以其中指定的参数执行构造函数或复制构造函数,如未出现,则执行默认构造函数;对于基本数据类型的成员,如果出现在构造函数的初始化列表中,则使用其中指定的值为其赋初始,否则什么也不做。
执行构造函数的函数体
类型转换
dynamic_cast
用于向下转型,即将基类指针或引用转型为派生类指针或引用。如果指针实际上指向派生类对象,则转型成功,返回子类对象地址,否则失败,返回NULL。由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表,因此要有虚函数,
当我们将dynamic_cast用于某种类型的指针或引用时,只有该类型至少含有虚函数时(最简单是基类析构函数为虚函数),才能进行这种转换。否则,编译器会报错。
static_cast
用于向下转型,基实就是强制转换。不管基类指针是否指向子类对象,一律把指针转型后返回。转换不保证安全()。
- 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。
- 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
- 把空指针转换成目标类型的空指针。
- 把任何类型的表达式转换成void类型。
const_cast 去除变量的const限定
在C++里,把常量指针(即指向常量的指针)赋值给非常量指针时,会提示错误,这时候就需要用到const_cast
const int i = 100;
int * ip = &i; //报错
int * ip = const_cast<int*>(&i); //正确
reinterpret_cast
可以用在"没有关系"的类型之间,而用static_cast来处理的转换就需要两者具有"一定的关系"了。
就是不理会数据本来的语义,而重新赋予它新的语义。