关于C++中的delete[] 和 new[] 操作符的讨论


起因

最近一直在看 《C++程序设计》 ,不得不说华章推出的“黑皮书”系列质量是真的高。在这本书的“动态内存管理“这一章里,有以下代码:

double *array = new delete[3];
delete[] array;

这段代码用 new[] 操作符创建了一个 double 数组,然后用 delete[] 操作符将数组删除。

实践

这就让我很不解啊, C++ 又不知道我创建的数组的长度,如何做到删除呢?我为此专门试了一下。为方便观察,增加了赋值和输出。

第一次实验

代码如下:

#include <iostream>

using namespace std;

int main(void)
{
    double *array = new double[3]{1,2,3};

    cout<<array<<endl;
    cout<<array[0]<<endl;
    cout<<array[1]<<endl;
    cout<<array[2]<<endl;

    delete[] array;

    cout<<array<<endl;
    cout<<array[0]<<endl;
    cout<<array[1]<<endl;
    cout<<array[2]<<endl;

    return 0;
}

输出如下:

0xe31760
1
2
3
0xe31760
7.35363e-317
7.35022e-317
3

整个事情就魔幻起来了,看样子,C++ 成功删除了数组的第1,2 个元素,但第三个元素没有删除。对了,g++ 编译器配置如下:

PS C:\Users\peler>g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=D:/Strawberry/c/bin/../libexec/gcc/x86_64-w64-mingw32/8.3.0/lto-wrapper.exe
Target: x86_64-w64-mingw32
Configured with: ../../../src/gcc-8.3.0/configure --host=x86_64-w64-mingw32 --build=x86_64-w64-mingw32 --target=x86_64-w64-mingw32 --prefix=/mingw64 --enable-shared --enable-static --disable-multilib --enable-languages=c,c++,fortran,lto --enable-libstdcxx-time=yes --enable-threads=posix --enable-libgomp --enable-libatomic --enable-lto --enable-graphite --enable-checking=release --enable-fully-dynamic-string --enable-version-specific-runtime-libs --enable-libstdcxx-filesystem-ts=yes --disable-libstdcxx-pch --disable-libstdcxx-debug --disable-bootstrap --disable-rpath --disable-win32-registry --disable-nls --disable-werror --disable-symvers --with-gnu-as --with-gnu-ld --with-arch=nocona --with-tune=core2 --with-libiconv --with-system-zlib --with-gmp=/opt/build/prerequisites/x86_64-w64-mingw32-static --with-mpfr=/opt/build/prerequisites/x86_64-w64-mingw32-static --with-mpc=/opt/build/prerequisites/x86_64-w64-mingw32-static --with-isl=/opt/build/prerequisites/x86_64-w64-mingw32-static --with-pkgversion='x86_64-posix-seh, Built by strawberryperl.com project' --with-bugurl=https://sourceforge.net/projects/mingw-w64 CFLAGS='-O2 -pipe -fno-ident -I/opt/build/x86_64-830-posix-seh-rt_v6/mingw64/opt/include -I/opt/build/prerequisites/x86_64-zlib-static/include -I/opt/build/prerequisites/x86_64-w64-mingw32-static/include' CXXFLAGS='-O2 -pipe -fno-ident -I/opt/build/x86_64-830-posix-seh-rt_v6/mingw64/opt/include -I/opt/build/prerequisites/x86_64-zlib-static/include -I/opt/build/prerequisites/x86_64-w64-mingw32-static/include' CPPFLAGS=' -I/opt/build/x86_64-830-posix-seh-rt_v6/mingw64/opt/include -I/opt/build/prerequisites/x86_64-zlib-static/include -I/opt/build/prerequisites/x86_64-w64-mingw32-static/include' LDFLAGS='-pipe -fno-ident -L/opt/build/x86_64-830-posix-seh-rt_v6/mingw64/opt/lib -L/opt/build/prerequisites/x86_64-zlib-static/lib -L/opt/build/prerequisites/x86_64-w64-mingw32-static/lib ' LD_FOR_TARGET=/opt/build/x86_64-830-posix-seh-rt_v6/mingw64/bin/ld.exe
Thread model: posix
gcc version 8.3.0 (x86_64-posix-seh, Built by strawberryperl.com project)

问题

不看不知道,一看吓一跳。问题出现了,我电脑中的 g++ 居然是 StrawberryPerl 中的,用了这么久都没发现,肯定是系统变量的问题,真是 “C生万物” 啊。虽然我觉得这不会影响运行结果,但我还是修改了一下,现在版本正常:

PS C:\Users\peler> g++ -v
Using built-in specs.
COLLECT_GCC=D:\MinGW\bin\g++.exe
COLLECT_LTO_WRAPPER=d:/mingw/bin/../libexec/gcc/i686-w64-mingw32/11.2.0/lto-wrapper.exe
Target: i686-w64-mingw32
Configured with: ../source/gcc-11.2.0/configure --build=x86_64-pc-linux-gnu --target=i686-w64-mingw32 --host=i686-w64-mingw32 --disable-shared --enable-static --disable-nls --disable-multilib --prefix=/home/hendrik/mingw/target/mingw-w64-i686 --with-sysroot=/home/hendrik/mingw/target/mingw-w64-i686 --with-mpc=/home/hendrik/mingw/target/pkgs/mpc/mpc-1.2.1-x86_64 --with-mpfr=/home/hendrik/mingw/target/pkgs/mpfr/mpfr-4.1.0-x86_64 --with-gmp=/home/hendrik/mingw/target/pkgs/gmp/gmp-6.2.1-x86_64 --with-isl=/home/hendrik/mingw/target/pkgs/isl/isl-0.18-x86_64 --enable-languages=c,c++ --enable-fully-dynamic-string --enable-lto
Thread model: win32
Supported LTO compression algorithms: zlib
gcc version 11.2.0 (GCC)

第二次实验

代码还是一样的:

#include <iostream>

using namespace std;

int main(void)
{
    double *array = new double[3]{1,2,3};

    cout<<array<<endl;
    cout<<array[0]<<endl;
    cout<<array[1]<<endl;
    cout<<array[2]<<endl;

    delete[] array;

    cout<<array<<endl;
    cout<<array[0]<<endl;
    cout<<array[1]<<endl;
    cout<<array[2]<<endl;

    return 0;
}

输出:

0x11215a0
1
2
3
0x11215a0
1.64077e-303
2
0

这就更悬了, g++ 把数组第二项保留,其余的删除了。

第三次实验

我开始怀疑是类型的问题,于是把代码修改了,还增加了列表长度:

#include <iostream>

using namespace std;

int main(void)
{
    int *array = new int[10]{10,20,30,40,50,60,70,80,90,100};

    cout<<array<<endl;
    cout<<array[0]<<endl;
    cout<<array[1]<<endl;
    cout<<array[2]<<endl;
    cout<<array[3]<<endl;
    cout<<array[4]<<endl;
    cout<<array[5]<<endl;
    cout<<array[6]<<endl;
    cout<<array[7]<<endl;
    cout<<array[8]<<endl;
    cout<<array[9]<<endl;

    delete[] array;

    cout<<array<<endl;
    cout<<array[0]<<endl;
    cout<<array[1]<<endl;
    cout<<array[2]<<endl;
    cout<<array[3]<<endl;
    cout<<array[4]<<endl;
    cout<<array[5]<<endl;
    cout<<array[6]<<endl;
    cout<<array[7]<<endl;
    cout<<array[8]<<endl;
    cout<<array[9]<<endl;

    return 0;
}

结果:

0x1b015a0
10
20
30
40
50
60
70
80
90
100
0x1b015a0
28341288
28311744
30
40
50
60
70
80
90
100

和上面大同小异吧,还是没有完全删除。

第四次实验

我又想到换一个编译器尝试,信息如下:

Microsoft Visual Studio Community 2019
版本 16.11.11
VisualStudio.16.Release/16.11.11+32228.343
Microsoft .NET Framework
版本 4.8.04161

已安装的版本: Community

Visual C++ 2019   00435-00000-00000-AA668
Microsoft Visual C++ 2019

Microsoft Visual C++ 向导   1.0
Microsoft Visual C++ 向导

Microsoft Visual Studio VC 软件包   1.0
Microsoft Visual Studio VC 软件包

运行同上的代码,由于有成熟的 DEBUG 系统,在输出删除数组的值时报错了:
查看图片

有趣的是,错误信息是:

引发了异常: 读取访问权限冲突。
**array** 是 0x8123。

g++ 不同,原本指向被删除的数组的指针被改变了,并且不是变为 nullptr ,试了好几次,都是 0x8123 。这可能是什么特定的值吧。这让我联想起机械硬盘删除数据的方法——把删除的地方标记为空,但数据却没有消失,只是在等待下一次存数据时覆盖掉。 Visual C++ 的策略是不是也如此,删除数组时只将指针指向一个特定值,把原来的空间在系统的内存管理里标记为空?

第五次实验

我又专门写了以下代码尝试:

#include <iostream>

using namespace std;

int main(void)
{
    int* array = new int[10]{ 10,20,30,40,50,60,70,80,90,100 };
    int* array_copy = array;

    cout << array << endl;
    cout << array[0] << endl;
    cout << array[1] << endl;
    cout << array[2] << endl;
    cout << array[3] << endl;
    cout << array[4] << endl;
    cout << array[5] << endl;
    cout << array[6] << endl;
    cout << array[7] << endl;
    cout << array[8] << endl;
    cout << array[9] << endl;

    delete[] array;

    cout << array << endl;
    cout << array_copy << endl;
    cout << array_copy[0] << endl;
    cout << array_copy[1] << endl;
    cout << array_copy[2] << endl;
    cout << array_copy[3] << endl;
    cout << array_copy[4] << endl;
    cout << array_copy[5] << endl;
    cout << array_copy[6] << endl;
    cout << array_copy[7] << endl;
    cout << array_copy[8] << endl;
    cout << array_copy[9] << endl;

    return 0;
}

输出如下:

0129F918
10
20
30
40
50
60
70
80
90
100
00008123
0129F918
-572662307
-572662307
-572662307
-572662307
-572662307
-572662307
-572662307
-572662307
-572662307
-572662307

E:\ProgramProject\VisualStudioProjects\Visual C++\test\test\Debug\test.exe (进程 1752)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

删除成功了!原来位置的值变了!不过也是好几次都是 -572662307 。这个有时间在讨论,暂且先放在这儿。

探究

那回到原本的问题,C++是怎么知道我的数组有多少个呢?我想到几点可能:

猜想

  • C++保存了列表长度等信息
  • C++像读取链表一样,判断下一个元素是不是赋过值的。如果是,删除;否则,结束。

想归想,但由于能力不够,还是得用万能的搜索引擎,参考的网址放在附录了。大概意思就是我猜想的第一条,C++会在数组前面保存数组的信息。如图:

查看图片

####验证

代码如下:

#include <iostream>

using namespace std;

int main(void)
{
    int* p = new int[10];
    int n1 = *((int*)p - 1);
    int n2 = *((int*)p - 2);
    int n3 = *((int*)p - 3);
    int n4 = *((int*)p - 4);

    cout << n1 << endl;
    cout << n2 << endl;
    cout << n3 << endl;
    cout << n4 << endl;

    return 0;
}

输出:

-33686019
161
40
1

E:\ProgramProject\VisualStudioProjects\Visual C++\test\test\Debug\test.exe (进程 13416)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

第三行的 40 就是数组的长度,用 40/sizeof(int) 可以算出来,为 10 ,和实际一样。严谨起见,其它条件不变,将 “定义数组” 的一行改为:

int* p = new int[20];

再次实验,输出如下:

-33686019
163
80
1

E:\ProgramProject\VisualStudioProjects\Visual C++\test\test\Debug\test.exe (进程 12092)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

第三行变为 80 ,以 40/sizeof(int) 计算,结果为20。

###思考

真相大白了,写一些自己的想法。

对C++的看法

在我看来,C++C 提升到了一个新的高度 可以说,将 “C是套了一层壳的汇编” 提升到了 “高级程序设计语言” (从另一个层面说, C++ 定义了 “高级程序设计语言” )。其中的 重载,模板,自定义操作符 等功能让我震惊。特别是 自定义操作符 ,就算在 “从C++发展出来” 语言中我也没听说过。这些功能也说明了为何 “C生C++,C++生万物” ,实在是太强大了!这也体现了我之前想创造自己的编程语言的想法,是多么可笑。不过,从将近一年多断断续续的摸爬滚打中积累出来的经验是宝贵的,毕竟把 《编译原理》 看的差不多了,还是有很多收获的。

C++为什么没有数组越界检测

回到这件事,我一开始觉得第一条猜想最不可能,但事实就是如此。那既然这样, C++ 为何不加入数组的越界检测呢?这样就不会出现 for 循环中的诡异错误。难道是为了和 C 接轨?不过,再思考一下,我们似乎也不需要这样的功能了,在先进一点的 IDE 中,如 Visual Studio 都有成熟的 DEBUG 功能,甚至在编写阶段编辑器就会有提示。操作系统中的内存管理也更成熟,进程之间的内存分离让我们不小心写出这种 BUG 时不至于让其他软件,甚至整个系统崩掉。数组越界检测的功能好像已经用不同的方式实现了。为此再在编译器中加这个功能,似乎就不那么必要了,还会拖慢速度。

我们该使用 new[] 和 delete[] 吗

在网上浏览关于这方面的信息时,看到过好几次这样的讨论。在我看来,这种操作让 C++ 不再硬核了。以前认为 C++ 很硬核,因为比起 Java 等很多语言的“垃圾回收”,“自动装箱”等功能,**C++**很多时候连内存管理都得自己搞,用刘墉的话说就是“没有暗箱操作”。但了解 new[]delete[] 实现后,发现 C++ 编译器还是帮我们做了很多事情的。就像在 Python 中创建列表,眼看就简单一行,实际上内存中还存着其它内容。
查看图片

这样是更智能了,但也让很多东西不清晰可见了。而且,只有用 new[] 创建的数组在 g++ 的编译下才会保存长度等信息, delete[] 才能正确删除。但是,如果一个程序很长很复杂,很多代码还不是我写的,那我怎么知道一个数组能不能用 delete[] 释放呢?搞不好还会出现 BUG

附录

参考资料

写第一篇博客的感受

深夜,吃完泡面 (不是足时发酵的老坛酸菜) ,把满书桌的作业搬走,拿出电脑,打开一个小时前刚下载的 Typora ,用白天看着 《了不起Markdown》 学会的知识,把自己平常编程的一件小事记录下来。神清气爽。身为一名初中生,从小学接触编程开始,没有人讨论编程相关的问题就是我最难受的一件事——有些同学喜欢数学,还可以跟老师讨论,而且课间还在研究题目看着就很好学。而我抱着本编程书就显得尴尬多了,而且要不是我露过几手,肯定还以为我在装B(而且上初中了学校还不让带课外书,好兄弟都被收了无数本小说了。保险起见,我现在都带英文的,这样被发现了也有点借口)。而自学编程摸爬滚打了这么久,CSDN博客园简书什么的肯定是久仰大名,于是便有了自己写博客的想法。我知道自己的思考很不成熟,对各种事的认知上还存在偏差,文章估计也没多少人看,但至少以后回顾我这段时光,不是“这个人很懒,什么也没有留下”了。(格局大了)


文章作者: peler
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 peler !
评论
  目录