您的位置:首页 > 编程语言 > C语言/C++

c++中关于数组的构造、析构,以及a-1(a是数组名)的意义

2014-09-08 20:51 465 查看
昨天群里有人问到关于数组的构造、析构的顺序问题,这里就我的理解范围解释一下,当然我对编译器原理并非是否熟悉,这些也是一个精通C++编译器的大神教我的,这里分享出来。

OK,先定义一个类,方便起见,类中增加了一个成员变量,并在构造时进行自增,构造函数和析构都进行了打印操作。类的定义如下:

#include <iostream>
#include <cstdio>
using namespace std;
int total = 0;
class A
{
public :
A(int idx)
{
this->index = idx;
cout<<"I am the construct function of idx " << this->index << "."<<endl;
};
A()
{
this->index = total++;
cout<<"I am the construct function of idx " << this->index << "."<<endl;
};
~A()
{
cout<<"I am the destrcut function of idx " << this->index << "." << endl;
}
int index;
};

int main()
{
//normal var
//it will be contructed and destructed automaticly
A a[17];
for(int i=0 ; i<17 ; i++)
cout << "a[" <<i<<"].index = " << a[i].index<<endl;
return 0;
}


上面定义了一个数组(为求特殊性,个数用了17个。),这样系统会自动对数组中每个元素进行构造和析构,所以构造和析构的顺序就显而易见了。如下:



不管在看到上面结果之前你是如何猜测的,也不论你对错与否,上面就是数组常规变量的构造与析构的过程。很显然,a[i].index = i,所以构造的顺序是从数组0开始,逐位构造;而析构的时候,则是从a[16]开始,也就是最后一个开始逐次析构。

其实上面的结论也很容易猜测到,熟悉变量构造和析构顺序的同学知道,任何一个函数体的构造,都是从第一个变量开始的,而先构造的变量在函数体结束后会更靠后的被析构。

当然变量是保存在栈区的,这些变量的空间一般是由系统分配。而数组的线性连续特点,使得数组里的内容必须满足有序性,所以构造时需要从小到大逐次构造。而析构时,则需要满足栈区的操作方式,当然这也与普通变量的析构顺序相同。总而言之,普通的变量,析构时总体会满足“FILO”,也就是先构造的会更靠后被析构。

当然,上面的是对变量的构造、析构顺序的一个测试和简单分析,对指针型的变量呢?

在测试之前,首先要明确几点就是,指针型的变量也是需要空间的,起分配规则与普通变量无异,但指针变量所指向的内容则是在执行时才确定的,也就是执行到new或malloc才会进行构造或分配,同时只有执行到delete或者free才会进行析构或回收。所以基本可以确定普通的变量的构造与析构顺序是完全独立随意的。

当然new的内容存在堆区,所以也不存在FILO的操作制约等等。

然后问题就是,对数组的构造是怎样呢?

带着疑惑,对上面的程序稍作修改可以容易得出结论:

#include <iostream>
#include <cstdio>
using namespace std;
int total = 0;
class A
{
public :
A(int idx)
{
this->index = idx;
cout<<"I am the construct function of idx " << this->index << "."<<endl;
};
A()
{
this->index = total++;
cout<<"I am the construct function of idx " << this->index << "."<<endl;
};
~A()
{
cout<<"I am the destrcut function of idx " << this->index << "." << endl;
}
int index;
};

int main()
{
//normal var
//it will be contructed and destructed automaticly
//A a[17];
A *a = new A[17];
for(int i=0 ; i<17 ; i++)
cout << "a[" <<i<<"].index = " << a[i].index<<endl;
delete [] a;
return 0;
}
而运行结果与上图完全一致。

所以你问系统或者问编译器,它会如何构造和析构,它会告诉你,它会和上面的流程保持一致。

此时可能你觉得不会有什么问题,但这里其实有一个很大的问题,就是指针与数组不同的是,对数组而言,编译器在编译时就可以知道数组长度,从而从最后一个元素进行析构,但是指针如何做到?如果没有一个变量保存数组的长度是否还能完成delete操作?如果有,这个变量保存在哪?

那么实际上答案是很确定的,有这样一个变量,这就是传说中的a-1。首先在构造时保存该数值,在析构时,读出该值,并指向数组最后位置,并逐次进行析构操作。

当然这里多提1句,就是对于单个变量A a = new A();delete [] a;这样的操作,实际上是合法的,同样的析构时会读出a-1,当然在一般情况下该值为0,所以在正常情况下,delete[]与delete完成的都是对a的析构。但若该位置的值被修改,析构时并不会进行验证,有可能会导致析构时读取了非法指针等情况造成崩溃。

update 9/9:

上面的解释实际上是有问题的,之前没有细看。其实在一般情况下一个变量的-1位置是不为0的,也表现了该变量是内存空间的一个随机位置。所以一般情况下如果执行delete [] a;程序是会crash的。如果遇到为0的情况,由于获取到的数组长度为0,实际上也不会做任何析构操作。

只有当a-1的值为1时才会完成对a的析构,可以通过手动修改a-1的值达到预期效果,测试代码和运行截图如下:

#include <iostream>
#include <cstdio>
using namespace std;
int total = 0;
class A
{
public :
A(int idx)
{
this->index = idx;
cout<<"I am the construct function of idx " << this->index << "."<<endl;
};
A()
{
this->index = total++;
cout<<"I am the construct function of idx " << this->index << "."<<endl;
};
~A()
{
cout<<"I am the destrcut function of idx " << this->index << "." << endl;
}
int index;
};

int main()
{
A *a = new A[231];
A *b = new A();
/*
for(int i=0 ; i<17 ; i++)
cout << "a[" <<i<<"].index = " << a[i].index<<endl;
*/
unsigned char *point = (unsigned char *)a;
//printf("%d %d\n",point , point -1);
int num = *(point -1)<<12 | (*(point - 2) << 8) | (*(point -3)<< 4) | *(point - 4);
//*(point - 4) = 11;
cout << num << endl;
delete [] a;
point = (unsigned char *)b;
num = *(point -1)<<12 | (*(point - 2) << 8) | (*(point -3)<< 4) | *(point - 4);
cout << num << endl;
*(point - 4) = 1;
*(point - 3) = 0;
*(point - 2) = 0;
*(point - 1) = 0;
delete [] b;
return 0;
}




首先打印的是变量a-1的值(逆序后的),然后强制赋值为1,delete[]a,完成析构。
如果不进行强制赋值,则可能会crash,当然如果是刚好分配到一块比较空的区域a-1为0,则不会做任何操作,可以自行测试,我这边是测试过的,结论与我分析的一样。

-----------------------------------------------------------------------

喜欢测试的人一定会很有兴趣的打印出a-1的值,然后说“不对啊,*(a-1)并不是a的长度17啊”,实际上,这种编译器级别(且叫他这个名字因为我也不知道这个过程在哪里完成的,因为C++是没有所谓的源码的,他的实现都靠汇编或者机器语言)的操作,对变量的伸展方向与它的寻址方向是一致的,因为该操作是“向后”寻址,所以这个a-1的数值也与一般的变量的高低位构成是相反的。

一般一个4字节变量会由从低到高4个字节完成,假设是abcd01,abcd02,abcd03,abcd04,这样的一个变量最终表示的数值就是

*(abcd01)<<12 | *(abcd02)<<8 | *(abcd03)<<4 | *(abcd04)

对于上面的a-1的逆向伸展方向而言,他在寄存器里会反向,所以表示的实际的数值为(假设a的地址为point,a-1对于的4个字节为point-1~point-4)

*(point -1)<<12 | (*(point - 2) << 8) | (*(point -3)<< 4) | *(point - 4);

具体可以参照如下代码:

#include <iostream>
#include <cstdio>
using namespace std;
int total = 0;
class A
{
public :
A(int idx)
{
this->index = idx;
cout<<"I am the construct function of idx " << this->index << "."<<endl;
};
A()
{
this->index = total++;
cout<<"I am the construct function of idx " << this->index << "."<<endl;
};
~A()
{
cout<<"I am the destrcut function of idx " << this->index << "." << endl;
}
int index;
};

int main()
{
A *a = new A[17];
for(int i=0 ; i<17 ; i++)
cout << "a[" <<i<<"].index = " << a[i].index<<endl;
unsigned char *point = (unsigned char *)a;
//printf("%d %d\n",point , point -1);
int num = *(point -1)<<12 | (*(point - 2) << 8) | (*(point -3)<< 4) | *(point - 4);
*(point - 4) = 11;
cout << num << endl;
delete [] a;
return 0;
}


上面的程序我做了几个测试,首先是打印出a-1的值,当然是和构造时的17是一致的。同时为了验证析构时的顺序,我尝试通过强制行为对a-1的值进行修改。我这里讲原来的17修改为11,然后再来看此时析构的结果:



如上图,首先正常打印出了数组长度17,然后仔细看析构的内容,是否发现析构异常了,只析构了从a[10]~a[0],因为我手动将a-1的内容修改为了11,而在delete[]的时候,系统也无法验证该值的正确性,所以“信任”了该值,认为该"数组"的长度就是11,析构了11个元素。就产生了上面的输出。

相信看了这些代码应该对数组构造和析构的顺序有了比较鲜明的理解。

这里简单总结一下

1、对于变量型的数组,构造与析构严格遵守栈区的操作流程,先构造的后析构;

2、指针型的变量new出的数组,也会从0开始逐次构造,从最后一个元素开始逐次析构;

3、new出的数组,会将数组长度保存到数组-1的位置中;

4、只有new出的数组才会有数组-1的长度这个值的存在,如果是变量定义的数组,该位置的值无意义。

当然如果有其他人有更多补充,欢迎指正!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: