c++中关于数组的构造、析构,以及a-1(a是数组名)的意义昨天群⾥有⼈问到关于数组的构造、析构的顺序问题,这⾥就我的理解范围解释⼀下,当然我对编译器原理并⾮是否熟悉,这些也是⼀个精通C++编译器的⼤神教我的,这⾥分享出来。
OK,先定义⼀个类,⽅便起见,类中增加了⼀个成员变量,并在构造时进⾏⾃增,构造函数和析构都进⾏了打印操作。类的定义如下:
#include <iostream>
#include <cstdio>协商英语
using namespace std;
int total = 0;
class A
{
gwt
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;
affinity}
int index;
};
int main()
{
//normal var
//it will be contructed and destructed automaticly
A a[17];
for(int i=0 ; i<17 ; i++)
revi的名词
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的操作制约等等。
然后问题就是,对数组的构造是怎样呢?
带着疑惑,对上⾯的程序稍作修改可以容易得出结论:
class A
{
public :
A(int idx)
{
this->index = idx;
cout<<"I am the construct function of idx " << this->index << "."<<endl;
};
A()
dnv{
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;
acidreturn 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的值达到预期效果,测试代码和运⾏截图如下:
class A
{
i am down
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);
具体可以参照如下代码: