【.com快译】那是2013年11月初,我和朋友准备参加计算协会主办的一年一度的国际大学生程序设计大赛机械(ACM)(ICPC)区域赛,选题为各种算法和数据结构。据我了解,跳表在编程比赛中并不常用,但它是一种用户维护有序元素的数据结构。我们认为将跳转表添加到我们的库中可能是个好主意。(注:我们选择的编程语言C++已经通过其标准库提供了平衡二叉搜索树,但是不支持扩充,而这在编程竞赛中经常需要。)于是,我们晚上开??始行动,添加跳表到你自己的图书馆。我的朋友找到了旧的实现,将用低级C编写的程序转换为更易于使用的高级C++。一旦完成,我们就开始测试它,首先我们做了一些小的手动测试,没有发现任何问题。然后进行更全面的测试,开始生成大量的随机测试用例(testcases),将跳表结果与C++集进行对比。在查看了一些旧的Github提交后,我发现这个程序基本上是这样的:代码中某处有一个错误导致了内存损坏。我们花了很长时间分析代码,寻找可能的解释,但一无所获。然后想到我们可能在转换过程中犯了一个错误,我们返回并检查了原始实现,修改了测试生成器,并使用低级接口再次运行程序。这次运行顺利,没有报错!我们更加确定在转换过程中出现了一些错误,然后我们回到转换后的代码中更详细地分析代码,但没有任何进展。***我们决定拉出大佬,用GDB调试器(https://en.wikipedia.org/wiki/GNU_Debugger)运行程序。我朋友对GDB比较有经验,让他带路。我的记忆有点模糊,但大致是这样。我们观察到的第一件事是错误发生在节点的析构函数中,然后在那里释放了一些内存。不幸的是,还没有发现为什么会这样。在调试器中迭代,我们得到了很大的突破。跳表的析构函数似乎被调用了不止一次。这可以解释我们在节点的析构函数中看到的奇怪行为和内存错误。修改代码输出一些调试信息后,我们确认是这样的,但是也很奇怪。我们没有显式地调用析构函数,所以它唯一(隐式地)被调用的地方是在程序的末尾。于是从头查代码,终于明白为什么会莫名其妙的多次调用析构函数了。我还记得我和我的朋友同时发现了这一点,我们对视了一秒钟,笑了起来,意识到我们有多可笑。根本原因在于size()函数。不是跳表的size()函数,而是我在代码开头定义的size()函数,如下:警告信息,在测试代码中使用了几次。例如,当我们确保跳转列表中的元素个数(t1)等于集合中的元素个数(t2)时:那么,这个函数有什么问题呢?问题在于参数x是按值传递的(这是C++中的默认模式),而不是按引用传递。这意味着无论何时我们调用一个函数,向它传递一些对象(这里是跳转列表),都会发生以下情况:1.对象被复制2.复制被传递给函数。3.函数使用对象的这个副本执行;4.当函数完成时,副本被销毁(通过调用析构函数)。因此,每当测试代码调用size(t1)时,它都会使用复制构造函数,制作跳过列表的副本,并调用副本的析构函数。当复制构造函数或析构函数未实现时,C++提供了一个合理的默认实现。实际上,如果我们只是使用默认实现,就不会出现此错误。然而,当一个对象分配内存时(就像跳板的情况),用户通常希望自定义实现一个复制构造函数来复制分配内存的实际值(而不是像默认实现那样只复制指针)。但是我们只是实现了一个析构函数而不是复制构造函数。所以当跳过列表的副本被复制时(对于size()函数),默认的复制构造函数只是复制指针。然后,在调用副本的析构函数之后,它会释放指针的内存。而且由于这是跳板的原始副本使用的内存,因此现在无法使用。但是测试代码一直走下去,遇到很多内存错误,最终崩溃。但这并不是size()函数引起的唯一不良反应。考虑以下代码:它创建一个包含100万个整数的向量,然后遍历该向量,将每个值设置为42。这通常会在普通计算机上运行几毫秒(在我的计算机上实际上需要4毫秒)。但是,由于ize()的参数是按值传递的,因此将100万个元素的向量复制到循环的每次迭代中。由于有100万次迭代,这需要很长时间。事实上,这在我的电脑上花费了13分38秒。不管怎样,让我们??回到我和我的朋友意识到是什么导致内存错误的那一刻。我不仅知道当前编写的size()函数可能在做什么,而且我还明白,因为这个函数实际上来自默认的C++模板(我将编辑器配置为在创建新的C++文件时自动导入该模板),基本上我写了很久的所有C++代码都会出现这个错误。至少几个月!当然,这很容易解决。从这个代码提交(https://github.com/SuprDewd/CompetitiveProgramming/commit/a72e4ec132d595beb7614c11e41bebf76e12f937)可以看出,解决这个问题后我们的测试程序运行没有任何问题。后记很高兴我发现了这个问题,这是一个很好的学习过程。从那时起,我就对如何声明函数非常小心。希望这是一件好事。不过,我已经反思过这件事了。为什么会出现这个错误?如前所述,我开始使用size()函数的原因是为了消除编译器警告。在C++标准库中,大多数容器的size()函数返回一个size_t类型的整数,是一个无符号整数。因此,如果您编译这样一段代码:for(inti=0;i
