- 起因
- 实践
- 第一次实验
- 问题
- 第二次实验
- 第三次实验
- 第四次实验
- 第五次实验
- 探究
- 猜想
- 对C++的看法
- C++为什么没有数组越界检测
- 我们该使用 new[] 和 delete[] 吗
- 附录
- 参考资料
- 写第一篇博客的感受
- 共同进步
最近一直在看 《C++程序设计》 ,不得不说华章推出的**“黑皮书”系列质量是真的高。在这本书的“动态内存管理“**这一章里,有以下代码:
double *array = new delete[3]; delete[] array;
这段代码用 new[] 操作符创建了一个 double 数组,然后用 delete[] 操作符将数组删除。
实践这就让我很不解啊, C++ 又不知道我创建的数组的长度,如何做到删除呢?我为此专门试了一下。为方便观察,增加了赋值和输出。
第一次实验代码如下:
#includeusing namespace std; int main(void) { double *array = new double[3]{1,2,3}; cout< 输出如下:
0xe31760 1 2 3 0xe31760 7.35363e-317 7.35022e-317 3整个事情就魔幻起来了,看样子,C++ 成功删除了数组的第1,2 个元素,但第三个元素没有删除。对了,g++ 编译器配置如下:
PS C:Userspeler>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:Userspeler> g++ -v Using built-in specs. COLLECT_GCC=D:MinGWbing++.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)第二次实验代码还是一样的:
#includeusing namespace std; int main(void) { double *array = new double[3]{1,2,3}; cout< 输出:
0x11215a0 1 2 3 0x11215a0 1.64077e-303 2 0这就更悬了, g++ 把数组第二项保留,其余的删除了。
第三次实验我开始怀疑是类型的问题,于是把代码修改了,还增加了列表长度:
#includeusing namespace std; int main(void) { int *array = new int[10]{10,20,30,40,50,60,70,80,90,100}; cout< 结果:
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++ 的策略是不是也如此,删除数组时只将指针指向一个特定值,把原来的空间在系统的内存管理里标记为空?
第五次实验我又专门写了以下代码尝试:
#includeusing 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:ProgramProjectVisualStudioProjectsVisual C++testtestDebugtest.exe (进程 1752)已退出,代码为 0。 要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。 按任意键关闭此窗口. . .删除成功了!原来位置的值变了!不过也是好几次都是 -572662307 。这个有时间在讨论,暂且先放在这儿。
探究那回到原本的问题,C++是怎么知道我的数组有多少个呢?我想到几点可能:
猜想
- C++保存了列表长度等信息
- C++像读取链表一样,判断下一个元素是不是赋过值的。如果是,删除;否则,结束。
想归想,但由于能力不够,还是得用万能的搜索引擎,参考的网址放在附录了。大概意思就是我猜想的第一条,C++会在数组前面保存数组的信息。如图:
####验证
代码如下:
#includeusing 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:ProgramProjectVisualStudioProjectsVisual C++testtestDebugtest.exe (进程 13416)已退出,代码为 0。 要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。 按任意键关闭此窗口. . .第三行的 40 就是数组的长度,用 40/sizeof(int) 可以算出来,为 10 ,和实际一样。严谨起见,其它条件不变,将 “定义数组” 的一行改为:
int* p = new int[20];再次实验,输出如下:
-33686019 163 80 1 E:ProgramProjectVisualStudioProjectsVisual C++testtestDebugtest.exe (进程 12092)已退出,代码为 0。 要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。 按任意键关闭此窗口. . .第三行变为 80 ,以 40/sizeof(int) 计算,结果仍为10。
###思考
真相大白了,写一些自己的想法。
对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 。写第一篇博客的感受
- https://www.zhihu.com/question/25556263/answer/32589012
- https://www.zhihu.com/question/25438329/answer/36980855
- https://www.zhihu.com/question/266130564/answer/314313658
- https://www.cnblogs.com/simplepaul/p/6861210.html
- https://blog.csdn.net/pizi0475/article/details/7575188
- https://www.zhihu.com/question/291750903/answer/477250840
- https://www.zhihu.com/question/481334153/answer/2090890026
- https://en.cppreference.com/w/cpp/language/delete
- https://en.cppreference.com/w/cpp/memory/new/operator_delete
深夜,吃完泡面 (不是足时发酵的老坛酸菜) ,把满书桌的作业搬走,拿出电脑,打开一个小时前刚下载的 Typora ,用白天看着 《了不起Markdown》 学会的知识,把自己平常编程的一件小事记录下来。神清气爽。身为一名初中生,从小学接触编程开始,没有人讨论编程相关的问题就是我最难受的一件事——有些同学喜欢数学,还可以跟老师讨论,而且课间还在研究题目看着就很好学。而我抱着本编程书就显得尴尬多了,而且要不是我露过几手,肯定还以为我在装B(而且上初中了学校还不让带课外书,好兄弟都被收了无数本小说了。保险起见,我现在都带英文的,这样被发现了也有点借口)。而自学编程摸爬滚打了这么久,CSDN,博客园,简书什么的肯定是久仰大名,于是便有了自己写博客的想法。我知道自己的思考很不成熟,对各种事的认知上还存在偏差,文章估计也没多少人看,但至少以后回顾我这段时光,不是 “这个人很懒,什么也没有留下” 了。(格局大了)
共同进步欢迎各位大佬指正错误,评论区讨论。互关也行。


![关于C++中的delete[] 和 new[] 操作符的讨论 关于C++中的delete[] 和 new[] 操作符的讨论](http://www.mshxw.com/aiimages/31/836682.png)
