c++中关于数组的构造、析构,以及a-1(a是数组名)的意义
2014-09-08 20:51
465 查看
昨天群里有人问到关于数组的构造、析构的顺序问题,这里就我的理解范围解释一下,当然我对编译器原理并非是否熟悉,这些也是一个精通C++编译器的大神教我的,这里分享出来。
OK,先定义一个类,方便起见,类中增加了一个成员变量,并在构造时进行自增,构造函数和析构都进行了打印操作。类的定义如下:
上面定义了一个数组(为求特殊性,个数用了17个。),这样系统会自动对数组中每个元素进行构造和析构,所以构造和析构的顺序就显而易见了。如下:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202007/31/b696987e0ab4eaddc7ce194f64073f5d)
不管在看到上面结果之前你是如何猜测的,也不论你对错与否,上面就是数组常规变量的构造与析构的过程。很显然,a[i].index = i,所以构造的顺序是从数组0开始,逐位构造;而析构的时候,则是从a[16]开始,也就是最后一个开始逐次析构。
其实上面的结论也很容易猜测到,熟悉变量构造和析构顺序的同学知道,任何一个函数体的构造,都是从第一个变量开始的,而先构造的变量在函数体结束后会更靠后的被析构。
当然变量是保存在栈区的,这些变量的空间一般是由系统分配。而数组的线性连续特点,使得数组里的内容必须满足有序性,所以构造时需要从小到大逐次构造。而析构时,则需要满足栈区的操作方式,当然这也与普通变量的析构顺序相同。总而言之,普通的变量,析构时总体会满足“FILO”,也就是先构造的会更靠后被析构。
当然,上面的是对变量的构造、析构顺序的一个测试和简单分析,对指针型的变量呢?
在测试之前,首先要明确几点就是,指针型的变量也是需要空间的,起分配规则与普通变量无异,但指针变量所指向的内容则是在执行时才确定的,也就是执行到new或malloc才会进行构造或分配,同时只有执行到delete或者free才会进行析构或回收。所以基本可以确定普通的变量的构造与析构顺序是完全独立随意的。
当然new的内容存在堆区,所以也不存在FILO的操作制约等等。
然后问题就是,对数组的构造是怎样呢?
带着疑惑,对上面的程序稍作修改可以容易得出结论:
所以你问系统或者问编译器,它会如何构造和析构,它会告诉你,它会和上面的流程保持一致。
此时可能你觉得不会有什么问题,但这里其实有一个很大的问题,就是指针与数组不同的是,对数组而言,编译器在编译时就可以知道数组长度,从而从最后一个元素进行析构,但是指针如何做到?如果没有一个变量保存数组的长度是否还能完成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的值达到预期效果,测试代码和运行截图如下:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202007/31/c72dea2a189066b7afffc69fa15a990a)
首先打印的是变量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);
具体可以参照如下代码:
上面的程序我做了几个测试,首先是打印出a-1的值,当然是和构造时的17是一致的。同时为了验证析构时的顺序,我尝试通过强制行为对a-1的值进行修改。我这里讲原来的17修改为11,然后再来看此时析构的结果:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202007/31/3469a842a42044c0f0cc6724d49012a2)
如上图,首先正常打印出了数组长度17,然后仔细看析构的内容,是否发现析构异常了,只析构了从a[10]~a[0],因为我手动将a-1的内容修改为了11,而在delete[]的时候,系统也无法验证该值的正确性,所以“信任”了该值,认为该"数组"的长度就是11,析构了11个元素。就产生了上面的输出。
相信看了这些代码应该对数组构造和析构的顺序有了比较鲜明的理解。
这里简单总结一下
1、对于变量型的数组,构造与析构严格遵守栈区的操作流程,先构造的后析构;
2、指针型的变量new出的数组,也会从0开始逐次构造,从最后一个元素开始逐次析构;
3、new出的数组,会将数组长度保存到数组-1的位置中;
4、只有new出的数组才会有数组-1的长度这个值的存在,如果是变量定义的数组,该位置的值无意义。
当然如果有其他人有更多补充,欢迎指正!
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的长度这个值的存在,如果是变量定义的数组,该位置的值无意义。
当然如果有其他人有更多补充,欢迎指正!
相关文章推荐
- Delphi和C++的语法区别 (关于构造和析构)
- C++入门学习:继承中的构造和析构以及同名成员情况
- 《More effective C++》 中条款三 不要用多态方式处理数组以及数组的析构
- 关于C++中 map 的意义以及用法
- C++构造和析构以及虚函数应用
- 关于C中字符数组,字符指针以及C++中string类型的两两转换及排序
- 关于C++继承体系中类的构造与析构的顺序【转贴】
- C++入门学习:虚析构、构造中不能实现多态、基类指针指向派生类数组的弊端
- 关于在C/C++语言中,函数如何返回数组,数组如何作为参数传递以及返回数组的函数该如何调用问题的总结
- C++构造和析构以及虚函数应用
- C++ 对象构造与析构以及内存布局
- C++对象的构造、赋值和析构
- 关于JSON对象,以及联合数组,eval函数的使用参考
- Effective C++ 3nd 读书摘要(一、让自己习惯C++ ; 二、构造,析构,赋值运算)Item1 - 12
- C/C++问答(3):关于构造和析构函数使用多态
- 一劳永逸:关于C/C++中指针、数组与函数复合定义形式的直观解释
- C++中异常处理中的构造和析构
- 关于C++字符 以及编码 宽字符
- C++学习手记(三)——构造与析构
- Effective C++ (5) 几个关于数组的问题