C++ - 对象模型之 类对象在执行时是如何生成的

C++对象模型目录

C++ - 对象模型之 编译器何时才会自行添加构造函数

C++ - 对象模型之 内存布局

C++ - 对象模型之 成员函数调用

C++ - 对象模型之 构造和析构函数都干了什么

C++ - 对象模型之 类对象在执行时是如何生成的

C++ - 对象模型之 模板、异常、RTTI的实现


C++ - 对象模型之 类对象在执行时是如何生成的


内部数据类型的处理如下:

全局变量和数组是在main函数执行前,分配在了数据段,程序结束后内存由系统收回;

局部静态变量和数组是在程序第一次运行至此时分配在数据段,程序结束后内存由系统收回;

由malloc生成的数据分配在堆,如果需要释放,必须手动调用free,没有进行释放的,程序结束后内存由系统收回;

对于内部数据类型,数组和普通变量没有区别。

但是对于类对象,除了分配和释放相应内存外,还需要调用构造函数和析构函数,那么类对象就不能完全和内部数据类型处理一样。

下面将对几种情况进行分别说明,注意,这些lei都至少有构造函数或者析构函数或者两者都有,否则,分配类对象和分配内部数据类型变量一样。


局部对象

1. 局部对象像内部数据类型变量一样,先在栈上分配内存;

2. 至少在使用该对象之前,调用对象的构造函数进行构造;

3. 在函数所有离开的地方(即return之前)调用对象的析构函数;

所以,如果在函数的开头定义类对象的话,可能会有多个地方调用析构函数,这样会增加代码量。所以,和C程序一般将变量定义在开头不同,建议C++程序将类对象定义在使用的附近,这样可能会减少调用析构的次数。


全局对象

1. 全局对象的内存会在main之前分配到数据段;

2. 每个对象都对应一个_sti()函数,该函数调用构造函数,然后所有全局对象的_sti()函数都会在main函数最开始位置被执行一遍,这个叫做静态初始化;

3. 相应的,每个_std()函数,该函数调用析构函数,所有的_std()函数会在main()最后的位置执行。


局部静态对象

该对象最大的难度是只能被构造一次,即构造函数只能执行一次。我们很自然得想到可以为每个局部静态对象设定一个全局的标示符,如果标示符符合什么条件,表示没有还没有构造,如果标示符符合什么条件,就表示已经构造了。该标识位可以是bool、也可以是指向静态对象的指针(未构造时,指针为NULL,构造后,是地址)。

不过这样的作法,玩意标示符被外界修改成了false或者NULL怎么办。


对象数组

需要对数组的每个元素都要执行一次构造,编译器会生成一个vec_new函数或者它的变体对这些元素进行构造,函数可能如下:

void* vec_new(

void* array,

size_t elem_size,

int elem_count,

void (*constructor)(void *),

void (*destructor)(void *)

)。

当然,当类无构造函数和析构函数时,vec_new不会被调用。

如果数组是全局的,那么会在main函数最前面执行,如果local的,则会在分配内存时调用,如果local static,会在第一次调用时被调用。

但是,由于对象如果要声明为数组,必须保证对象无constructor或者有default construtor或者有参数都是默认值的constructor,所以,假设constructor是带有默认值参数的,那么这些参数是如何在调用constructor进行赋值的呢?显然vec_new无法做到。所以,根据这种情况,编译器又生成了一个函数,叫做stub constructor,假定类名为class1结构如下:

class1::class1(){

class1(默认参数列表);

}

这和C++语法是相违背的,你可以试试写一个无参数的构造函数和带有参数都是默认参数的构造函数,是无法编译通过的。我们要记住,这是一个特例。


new对象

new作为C++的运算符,在编译的时候会被处理为类似以下逻辑的代码,如:

Class1* p = new Class1();

被处理为:

p = _new(sizeof(Class1));

if(p != NULL)

    p = Class1::Class1(p);

先通过_new在堆上分配内存,然后如果分配成功的话,再调用构造函数。

还记得,为什么一个空类的大小也是1吗?因为要保证两个空类的对象地址不一样,所以,当sizeof(Class1)为0时,_new函数会分配1字节的内存给p。

同理,delete p 会被处理为如下:

if(p != NULL)

    _delete(p);


_new和_delete是如何实现的呢?

虽然没有硬性规定,但是一般情况下,总是以标准的malloc和free函数实现。


new数组对象

对于数组对象,最主要的难点在于delete数组时,是如何知道数组一共有多少对象。我们还是来看看new []都干了什么。

new []首先会分配内存,然后和对象数组一样调用vec_new函数为每个元素进行构造,不同的是,new []调用的vec_new函数第一个参数是0,告诉系统要在堆上进行分配。和对象数组其实就这个区别。难点就在于delete []和delete区别比较大。

如何让在delete数组对象时,知道数组的维数呢?我目前知道两种方式:

方式1. 增加一个word内存记录,这个在effective C++上有说明,在数组最前面增加这个记录;

方式2. 额外维护一个“联合数组”用来保存进程中所有的数组的地址和数组的维数,有些编译器可能还会增加其他信息,如destructor的地址。

如果为了节省内存,不维护destructor的地址,那么在delete时就要明白destructor的地址。这个destructor地址是通过delete p的p对象的类型来确定的,但是有种情况就会出现错误

当base* p = new derived[10],时,delete [] p时,调用的destructor将是base的destructor,所以,为了能够保证调用正确的derived 的 destructor,p指针指向的类型最好不要更改。


我的疑问

1. 知道元素的个数很难吗

在c编程时,我们分配和释放内存,是malloc和free,malloc会指定要分配的内存长度,但是free时,只要给定正确的内存起始地址就可以释放掉正确长度的内存。这显然利用了操作系统对内存的管理机制。

那么,new []实际底层也是调用malloc进行分配,delete时调用free,应该很容易知道new []时分配的内存长度,长度除以一个对象的size,就可以得到对象的数目。

所以,这个问题还需要进一步的研究,肯定有些机制没弄明白,元素个数不会这么容易知道的。


placement new对象

在已有的一块内存上重新分配一个对象。所以我们自然要求这块内存能够放得下这个对象。placement new共有两个步骤

1. 改变内存的类型 Class1 * p = new(existting_mem) Class1;

2. 执行构造函数;

常见问题

1. 如果连续两次执行 p = new(existting_mem) Class1,第二次执行时,应该执行一次class1的析构函数,对于这样的问题,只能交由程序员自己避免这样的问题,C++并没有提供一个万全之策;

2. Base b; new(&b)Derived; 假设Base和Derived类都声明了virtual f(),那么b.f()执行的是Base的还是Derived,很遗憾,编译器一般都会去调用Base::f();

对于placement new用法比较少,也比较简单,所以使用的时候一定注意即可。


临时性对象

如:

Class1 A, B;

if(A+B){

    //do something;

}

A+B就会产生一个临时对象,if再判断临时对象。


至于这个临时对象如何被释放,C++标准并没有给出明白的规定。

临时对象,程序员一般看不到,它编译器自动生成的,这可能会产生效率上的问题,至于如何避免效率上的问题,靠时间慢慢体会吧。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值