C++ 对象模型复习
一:对象模型
C++面向对象的实现,相对于C耗费成本是由virutal引起的。包括
- virtual function机制,用来支持执行期绑定。
- virutal base class 虚基类机制,以实现共享虚基类的subobject
此外还有多重继承下,发生在其第二或后继派生类之间的转换。
C++对象模型,所有非静态数据成员存储在对象本身中,所有的静态数据成员,成员函数(包括静态与非静态)都置于对象之外。虚函数的支持是在每个class内部维护一个vptr指向vtbl,vtbl存放所有虚函数指针。vptr的设定和重置都由每一个class的构造函数/析构函数/拷贝分配函数自动完成。每一个typeinfo也放在vtbl之中(用来支持RTTI),vtbl之中还有virtual base subobject的offset值。
主要有三种方法支持多态:(1)基类使用引用或指针接收派生类。(2)可调用虚函数触发多态。(3)经由dynamic_cast和typeid运算符。
当一个基类对象被直接初始化为一个派生类对象时,派生类对象就会被切割,仅剩基类类型大小的内存。
总结:
总而言之,多态是一种威力强大的设计机制,允许通过将派生类的指针赋给基类指针,让基类可以根据赋值给它的子类的特性以不同的方式运作。需要付出的代价就是额外的间接性,包括虚表和RTTI两方面。
二:构造函数语意学
四种情况下编译器自动合成trival构造函数:
1.包含带有默认构造函数的对象成员的类 2. 继承自带有默认构造函数的基类的类 3.带有虚函数的类 4.带有一个虚基类的类
拷贝构造函数同理,否则执行默认位逐次拷贝。
NRV优化不必说。
成员初始化列表必须使用的情况:1.有const成员 2.有引用类型成员 3.有没有默认构造函数的成员对象 4.基类对象没有默认构造函数
三:Data语意学
影响C++类的大小的三个因素:
- 支持特殊功能所带来的额外负担(对各种virtual的支持)
- 编译器对特殊情况的优化处理(空class)
- 内存对齐
一般对象的布局顺序为:vptr、基类成员(按声明顺序)、自身数据成员、虚基类数据成员(按声明顺序)
四:Function语意学
C++支持三种类型成员函数,分别是static、nonstatic、virtual。
非静态成员函数:和非成员函数由相同的效率,因为编译器内部会将其转换为非静态成员函数,加上this指针作为额外参数。此外还要额外对函数的名称进行处理。
虚拟成员函数p->function转换为(*p->vptr[1])(p),引入虚表下表。
静态成员函数实际上相当于全局函数。不能够存取类中非静态成员,不能调用非静态成员函数。不能够声明为const、voliatle或virtual。它不需要经由对象调用,当然,通过对象调用也被允许。
消极多态与积极多态:
Point ptr = new Point3d //Point3d继承自Point
在这种情况下,多态可以在编译期完成(虚基类情况除外),因此被称作消极多态(没有进行虚函数的调用)。相对于消极多态,则有积极多态--指向的对象类型需要在执行期才能决定。积极多态的例子如虚函数和RTTI:
//例1,虚函数的调用
ptr->z();
//例2,RTTI的应用
if(Point3d *p = dynamic_cast<Point3d*>(ptr))
return p->z();
虚基类子对象为什么是运行时确定的,而不是编译器? 因为虚基类的构造函数在继承体系中被编译器压制到了最派生类才构造,所以集成体系中间的类不能再编译器确定虚基类子对象的位置。因为它下面可能还有多层继承。所以要引入一个间接性,为虚继承体系中每个class引入一个虚基类子对象offset,在运行时填充该offset。
在多重继承中支持虚函数,其复杂度围绕在第二个或后继基类身上,以及“必须调整this指针这一点”。多重继承中调整this指针(注意没说虚函数):比如对于base *pbase = new derived(这里没有多态),新的derived对象以指向base subobject。如果没有这样的调整,指针的任何非多态运用都将失败,向base->data_base,存取操作会失败!或者delete base,这个操作执行时又必须把this指针指到头部,因为要析构的大小是derived对象大小。这些操作必须在执行期完成,也就是this指针跳跃的这些offset必须指到,其实会存于vtbl之中。如果base是派生类的最基类,那么this无需调整,因为他们起始位置是一样的。
有三种情况,第二或后继的基类会影响对虚函数的支持:
- 通过一个指向第二基类的指针,调用派生类虚函数(指针必须后移)。
- 通过一个派生类的指针,调用第二个基类中一个继承而来的virtual function,此情形派生类指针必须再次调整,以指向第二个基类子对象。
- 第三种情况发生于一个语言扩充性质之下,允许一个虚函数返回值类型发生变化,可能是基类类型,或者派生类类型。比如base* pb=pb->clone(),该函数返回值为derived*类型,所以此处必须调整offset。
五
六
七
1.为什么catch字句的异常声明通常被声明为引用?
-
可以避免由异常对象到catch字句中的对象的拷贝,特别是对象比较大时。
-
能确保catch字句对异常对象的修改能再次抛出
-
能确保正确地调用与异常类型相关联的虚拟函数,避免对象切割
-
异常对象的声明周期?
-
产生:throw className()时产生
-
销毁:该异常最后一个catch字句退出时销毁
-
注意:因为一场可能在catch子句中重新被抛出,所以在到达最后一个处理该异常的catch子句之前,异常对象是不能销毁的。
RTTI:
- RTTI只支持多态类,也就是说没有定义虚函数的类是不能进行RTTI的。
- 对指针的dynamic_cast失败时会返回NULL,对引用的话,失败会抛出bad_cast_exception.
- typeid可以放回const type_info&,用以获取类型信息。
关于1是因为RTTI的实现时通过vptr来获取存储在虚函数表中的type_info*,事实上为非多态类提供RTTI,也没有多大意义。2的原因在于指针可以被赋值为0,以表示no object,但是引用不行。关于3,UI然第一点指出RTTI只支持多肽类,但typeid和typeinfo同样可适用于内建类型及所有非多态类。与多态类的差别在于,非多态类的type_info对象是静态取得,**所以不能叫“执行期类型识”。
dynamic_cast主要用于类层次之间的上行装换和下行转换,还可以用于类之间的交叉转换。上行转换时,static_cast是安全的。在进行下行转换时,dynamic_cast有类型检查的功能,比static_cast更安全:
#include <iostream>
#include <assert.h>
class base {
public:
virtual void fun() { std::cout<<"base"<<std::endl; } //去掉virtual dynamic_cast会编译失败
};
class derived : public base {
public:
char* str[100];
};
void test(base* b)
{
derived* d1 = static_cast<derived*>(b);
//d1->str[1] = "hello"; //使用static_cast不会类型检查,如果b指针真得是derived*类型此处就没问题,如果b实际上就是base类型,此处段错误
derived* d2 = dynamic_cast<derived*>(b);
assert(d2 == NULL); //使用dynamic_cast,如果b只是base*类型,上一句强转称为derived*类型会失败,d2会返回NULL。
}
int main()
{
base *b;
test(b);
return 0;
}
另外,dynamic_cast还支持交叉转化(cross cast),也就是继承体系中统一层级的对象之间的转换。而使用static_cast这种无关连的类的转换会编译不通过。
#include <iostream>
class A
{
public:
int m_iNum;
virtual void f(){}
};
class B:public A
{
};
class D:public A
{
};
void foo()
{
B* pb = new B;
pb->m_iNum = 100;
//D* pd1 = static_cast<D*>(pb); //compile error
//D* pd2 = dynamic_cast<D*>(pb);//pd2isNULL
delete pb;
}
int main()
{
foo();
}
posted on 2019-02-06 00:10 TestCNBlogss 阅读(92) 评论(0) 编辑 收藏 举报