Memory leakage has been a permanent annoyance for C/C++ programmers. Under MSVC, one useful feature of MFC is report memory leaks at the exit of an application (to the debugger output window, which can be displayed by the integration environment or a debugger). Under GCC, current available tools like mpatrol are relatively difficult to use, or have a big impact on memory/performance. This article details the implementation of an easy-to-use, cross-platform C++ memory leak detector (which I call debug_new), and discusses the related technical issues.

Basic usage

Let’s look at the following simple program test.cpp:

int main()
{
int* p1 = new int;
char* p2 = new char[];
return ;
}

Our basic objectives are, of course, report two memory leaks. It is very simple: just compile and link debug_new.cpp. For example:

cl -GX test.cpp debug_new.cpp (MSVC)
g++ test.cpp debug_new.cpp -o test (GCC)

The running output is like follows:

Leaked object at 00341008 (size 4, <Unknown>)
Leaked object at 00341CA0 (size 10, <Unknown>)

If we need clearer reports, it is also trivial: just put this at the front of test.cpp:

#include "debug_new.h"

The output after adding this line is:

Leaked object at 00340FB8 (size 10, test5.cpp:5)
Leaked object at 00340F80 (size 4, test5.cpp:4)

Very simple, isn’t it?

Background knowledge

In a new/delete operation, C++ compilers generates calls to operator new and operator delete (allocation and deallocation functions) for the user. The prototypes of operator new and operator delete are as follows:

void* operator new(size_t) throw(std::bad_alloc);
void* operator new[](size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
void operator delete[](void*) throw();

For new int, the compiler will generate a call to “operator new(sizeof(int))”, and for new char[10], “operator new(sizeof(char) * 10)”. Similarly, for delete ptr and delete[] ptr, the compiler will generate calls to “operator delete(ptr)” and “operator delete[](ptr)”. When the user does not define these operators, the compiler will provide their definitions automatically; when the user do define them, they will override the ones the compiler provides. And we thus get the ability to trace and control dynamic memory allocation.

In the meanwhile, we can adjust the behaviour of new operators with new-placements, which are to supply additional arguments to the allocation functions. E.g., when we have a prototype

void* operator new(size_t size, const char* file, int line);

we may use new ("hello", 123) int to generate a call to “operator new(sizeof(int), "hello", 123)”. This can be very flexible. One placement allocation function that the C++ standard ([C++1998]) requires is

void* operator new(size_t size, const std::nothrow_t&) throw();

in which nothrow_t is usually an empty structure (defined as “struct nothrow_t {};”), whose sole purpose is to provide a type that the compiler can identify for overload resolution. Users can call it via new (std::nothrow) type (nothrow is a constant of type nothrow_t). The difference from the standard new is that when memory allocation fails, new will throw an exception, but new(std::nothrow) will return a null pointer.

One thing to notice is that there is not a corresponding syntax like delete(std::nothrow) ptr. However, a related issue will be mentioned later in this article.

For more information about the above-mentioned C++ language features, please refer to [Stroustrup1997], esp. sections 6.2.6, 10.4.11, 15.6, 19.4.5, and B.3.4. These features are key to understanding the implementation described below.

Principle and basic implementation

Similar to some other memory leakage detectors, debug_new overrides operator new, and provides macros to do substitues in user’s programs. The relevant part in debug_new.h is as follows:

void* operator new(size_t size, const char* file, int line);
void* operator new[](size_t size, const char* file, int line);
#define DEBUG_NEW new(__FILE__, __LINE__)
#define new DEBUG_NEW

Let’s look at the test.cpp after including debug_new.h: new char[10] will become “new("test.cpp", 4) char[10]” after preprocessing, and the compiler will generate a call to “operator new[](sizeof(char) * 10, "test.cpp", 4)” accordingly. If I define “operator new(size_t, const char*, int)” and “operator delete(void*)” (as well as “operator new[]...” and “operator delete[]...”; for clarity, my discussions about operator new and operator delete also cover operator new[] and operator delete[] without mentioning specifically, unless noted otherwise) indebug_new.cpp, I can trace all dynamic memory allocation/deallocation calls, and check for unmatched news and deletes. The implementation may be as simple as using just a map: add a pointer to map in new, and delete the pointer and related information in delete; report wrong deleting if the pointer to delete does not exist in the map; report memory leaks if there are still pointers to delete in the map at program exit.

However, it will not work if debug_new.h is not included. And the case that some translation units include debug_new.h and some do not are unacceptable, for although two operator news are used — “operator new(size_t, const char*, int)” and “operator new(size_t)” — there is only one operator delete! The operator delete we define will consider it an invalid pointer, when given a pointer returned by “operator delete(void*)” (no information about it exists in the map). We are facing a dilemma: either to misreport in this case, or not to report when deleting a pointer twice: none is satisfactory behaviour.

So defining the global “operator new(size_t)” is inevitable. In debug_new.h, I have

void* operator new(size_t size)
{
return operator new(size, "<Unknown>", );
}

Implement the memory leak detector as I have described, you will find it works under some environments (say, GCC 2.95.3 w/ SGI STL), but crashes under others (MSVC 6 is among them). The reason is not complicated: memory pools are used in SGI STL, and only large chunks of memory will be allocated by operator new; in STL implementations which do not utilize such mechanisms, adding data to map will cause a call to operator new, which will add data to map, and this dead loop will immediately cause a stack overflow that aborts the application. Therefore I have to stop using the convenient STL container and resort to my own data structure:

struct new_ptr_list_t
{
new_ptr_list_t* next;
const char* file;
int line;
size_t size;
};

Every time one allocates memory via newsizeof(new_ptr_list_t) more bytes will be allocated when calling malloc. The memory blocks will be chained together as a linked list (via the next field), the file name, line number, and object size will be stored in the fileline, and size fields, and return (pointer-returned-by-malloc + sizeof(new_ptr_list_t)). When one deletes a pointer, it will be matched with those in the linked list. If it does match — pointer-to-delete == (char*)pointer-in-linked-list + sizeof(new_ptr_list_t) — the linked list will be adjusted and the memory deallocated. If no match is found, a message of deleting an invalid pointer will be printed and the application will be aborted.

In order to automatically report memory leaks at program exit, I construct a static object (C++ ensures that its constructor will be called at program initialization, and the destructor be called at program exit), whose destructor will call a function to check for memory leaks. Users are also allowed to call this function manually.

Thus is the basic implementation.

Improvements on usability

The above method worked quite well, until I began to create a large number of objects. Since each delete needed to search in the linked list, and the average number of searches was a half of the length of the linked list, the application soon crawled. The speed was too slow even for the purpose of debugging. So I made a modification: the head of the linked list is changed from a single pointer to an array of pointers, and which element a pointer belongs to depends on its hash value. — Users are allowed to change the definitions of _DEBUG_NEW_HASH and _DEBUG_NEW_HASHTABLESIZE (at compile-time) to adjust the behaviour of debug_new. Their current values are what I feel satisfactory after some tests.

I found in real use that under some special circumstances the pointers to file names can become invalid (check the comment in debug_new.cpp if you are interested). Therefore, currently the default behaviour ofdebug_new is copying the first 20 characters of the file name, instead of storing the pointer to the file name. Also notice that the length of the original new_ptr_list_t is 16 bytes, and the current length is 32 bytes: both can ensure correct memory alignments.

In order to ensure debug_new can work with new(std::nothrow), I overloaded “void* operator new(size_t size, const std::nothrow_t&) throw()” too; otherwise the pointer returned by anew(std::nothrow) will be considered an invalid pointer to delete. Since debug_new does not throw exceptions (the program will report an alert and abort when memory is insufficient), this overload just callsoperator new(size_t). Very simple.

It has been mentioned previously that a C++ file should include debug_new.h to get an accurate memory leak report. I usually do this:

#ifdef _DEBUG
#include "debug_new.h"
#endif

The include position should be later than the system headers, but earlier than user’s own header files if possible. Typically debug_new.h will conflict with STL header files if included earlier. Under some circumstances one may not want debug_new to redefine new; it could be done by defining _DEBUG_NEW_REDEFINE_NEW to 0 before including debug_new.h. Then the user should also use DEBUG_NEW instead ofnew. Maybe one should write this in the source:

#ifdef _DEBUG
#define _DEBUG_NEW_REDEFINE_NEW 0
#include "debug_new.h"
#else
#define DEBUG_NEW new
#endif

and use DEBUG_NEW where memory tracing is needed (consider global substitution).

Users might choose to define _DEBUG_NEW_EMULATE_MALLOC, and debug_new.h will emulate malloc and free with debug_new and delete, causing malloc and free in a translation unit including debug_new.hto be traced. Three global variables are used to adjust the behaviour of debug_newnew_output_fp, default to stderr, is the stream pointer to output information about memory leaks (traditional C streams are preferred to C++ iostreams since the former is simpler, smaller, and has a longer and more predictable lifetime); new_verbose_flag, default to false, will cause every new/delete to output trace messages when set to truenew_autocheck_flag, default to true (which will cause the program to call check_leaks automatically on exit), will make users have to call check_leaks manually when set to false.

One thing to notice is that it might be impossible to ensure that the destruction of static objects occur before the automatic check_leaks call, since the call itself is issued from the destructor of a static object indebug_new.cpp. I have used several techniques to better the case. For MSVC, it is quite straightforword: “#pragma init_seg(lib)” is used to adjust the order of object construction/destruction. For other compilers without such a compiler directive, I use a counter class as proposed by Bjarne ([Stroustrup1997], section 21.5.2) and can ensure check_leaks will be automatically called after the destruction of all objects defined in translation units that include debug_new.h. For static objects defined in C++ libraries instead of the user code, there is a last resort: new_verbose_flag will be set to true after the automaticcheck_leaks call, so that all later delete operations along with number of bytes still allocated will be printed. Even if there is a misreport on memory leakage, we can manually confirm that no memory leakage happens if the later deletes finally report that “0 bytes still allocated”.

Debug_new will report on deleteing an invalid pointer (or a pointer twice), as well as on mismatches of new/delete[] or new[]/delete. A diagnostic message will be printed and the program will abort.

Exception safety and thread safety are worth their separate sections. Please read on.

Exception in the constructor

Let’s look at the following simple program:

#include <stdexcept>
#include <stdio.h> void* operator new(size_t size, int line)
{
printf("Allocate %u bytes on line %d\n", size, line);
return operator new(size);
} class Obj {
public:
Obj(int n);
private:
int _n;
}; Obj::Obj(int n) : _n(n)
{
if (n == ) {
throw std::runtime_error("0 not allowed");
}
} int main()
{
try {
Obj* p = new(__LINE__) Obj();
delete p;
} catch (const std::runtime_error& e) {
printf("Exception: %s\n", e.what());
}
}

Any problems seen? In fact, if we compile it with MSVC, the warning message already tells us what has happened:

test.cpp(27) : warning C4291: 'void *__cdecl operator new(unsigned int,int)' : no matching operator delete found; memory will not be freed if initialization throws an exception

Try compiling and linking debug_new.cpp also. The result is as follows:

Allocate 4 bytes on line 27
Exception: 0 not allowed
Leaked object at 00341008 (size 4, <Unknown>)

There is a memory leak!

Of course, this might not be a frequently encountered case. However, who can ensure that the constructors one uses never throw an exception? And the solution is not complicated; it just asks for a compiler that conforms well to the C++ standard and allows the definition of a placement deallocation function ([C++1998], section 5.3.4; drafts of the standard might be found on the Web, such as here). Of compilers I have tested, GCC (2.95.3 or higher) and MSVC (6.0 or higher) support this feature quite well, while Borland C++ Compiler 5.5.1 and Digital Mars C++ compiler (all versions up to 8.38) do not. In the example above, if the compiler supports, we should declare and implement an “operator delete(void*, int)” to recycle the memory allocated by new(__LINE__); if the compiler does not, macros need to be used to make the compiler ignore the relevant declarations and implementations. To make debug_new compile under such a non-conformant compiler, users need to define the macro HAS_PLACEMENT_DELETE (Update: The macro name is HAVE_PLACEMENT_DELETE from Nvwa version 0.8) to 0, and take care of the exception-in-constructor problem themselves. I wish you did not have to do this, since in that case your compiler is really out of date!

Thread safety

My original version of debug_new was not thread-safe. There were no synchronization primitives in the standard C++ language, and I was unwilling to rely on a bulky third-party library. At last I decided to write my own thread-transparency layer, and the current debug_new relies on it. This layer is thin and simple, and its interface is as follows:

class fast_mutex
{
public:
void lock();
void unlock();
};

It supports POSIX threads and Win32 threads currently, as well as a no-threads mode. Unlike Loki ([Alexandrescu2001]) and some other libraries, threading mode is not to be specified in the code, but detected from the environment. It will automatically switch on multi-threading when the -MT/-MD option of MSVC, the -mthreads option of MinGW GCC, or the -pthread option of GCC under POSIX environments, is used. One advantage of the current implementation is that the construction and destruction of a static object using a static fast_mutex not yet constructed or already destroyed are allowed to work (withlock/unlock operations ignored), and there are re-entry checks for lock/unlock operations when the preprocessing symbol _DEBUG is defined.

Directly calling lock/unlock is error-prone, and I generally use an RAII (resource acquisition iinitialization; [Stroustrup1997], section 14.4.1) helper class. The code is short and I list it here in full:

class fast_mutex_autolock
{
fast_mutex& _M_mtx;
public:
explicit fast_mutex_autolock(fast_mutex& __mtx) : _M_mtx(__mtx)
{
_M_mtx.lock();
}
~fast_mutex_autolock()
{
_M_mtx.unlock();
}
private:
fast_mutex_autolock(const fast_mutex_autolock&);
fast_mutex_autolock& operator=(const fast_mutex_autolock&);
};

I am quite satisfied with this implementation and its application in the current debug_new.

Special improvement with gcc/binutils

Using macros has intrinsic problems: it cannot work directly with placement new, for it is not possible to expand an expression like “new(special) MyObj” to record file/line information without prior knowledge of the “special” stuff. What is more, the definition of per-class operator new will not work since the preprocessed code will be like “void* operator new("some_file.cpp", 123)(size_t ...)” — the compiler will not love this.

The alternative is to store the instruction address of the caller of operator new, and look up for the source line if a leak is found. Obviously, there are two things to do:

  • Get the caller address of operator new;
  • Convert the caller address to a source position.

There is no portable way to achieve these, but the necessary support has already been there for ready use if the GNU toolchain is used. Let’s just look at some GNU documentation:

`__builtin_return_address (LEVEL)'
This function returns the return address of the current function,
or of one of its callers. The LEVEL argument is number of frames
to scan up the call stack. A value of `0' yields the return
address of the current function, a value of `1' yields the return
address of the caller of the current function, and so forth. The LEVEL argument must be a constant integer. On some machines it may be impossible to determine the return
address of any function other than the current one; in such cases,
or when the top of the stack has been reached, this function will
return `0'.
(gcc info page)
addr2line
********* addr2line [ -b BFDNAME | --target=BFDNAME ]
[ -C | --demangle[=STYLE ]
[ -e FILENAME | --exe=FILENAME ]
[ -f | --functions ] [ -s | --basename ]
[ -H | --help ] [ -V | --version ]
[ addr addr ... ] `addr2line' translates program addresses into file names and line
numbers. Given an address and an executable, it uses the debugging
information in the executable to figure out which file name and line
number are associated with a given address. The executable to use is specified with the `-e' option. The
default is the file `a.out'.
(binutils info page)

So the implementation is quite straightforward and like this:

void* operator new(size_t size) throw(std::bad_alloc)
{
return operator new(size, __builtin_return_address(), );
}

When a leak is found, debug_new will try to convert the stored caller address to the source position by popening an addr2line process, and display it if something useful is returned (it should be the case if debugging symbols are present); otherwise the stored address is displayed. One thing to notice is that one must tell debug_new the path/name of the process to make addr2line work. I have outlined the ways in the doxygen documentation.

If you have your own routines to get and display the caller address, it is also easy to make debug_new work with it. You may check the source code for details. Look for _DEBUG_NEW_CALLER_ADDRESS andprint_position_from_addr.

Important update in 2007

With an idea coming from Greg Herlihy’s post in comp.lang.c++.moderated, a better solution is implemented. Instead of defining new to “new(__FILE__, __LINE__)”, it is now defined to “__debug_new_recorder(__FILE__, __LINE__) ->* new”. The most significant result is that placement new can be used with debug_new now! Full support for new(std::nothrow) is provided, with its null-returning error semantics (by default). Other forms (like “new(buffer) Obj”) will probably result in a run-time warning, but not compile-time or run-time errors — in order to achieve that, magic number signatures are added to detect memory corruption in the free store. Memory corruption will be checked on freeing the pointers and checking the leaks, and a new function check_mem_corruption is added for your on-demand use in debugging. You may also want to define _DEBUG_NEW_TAILCHECK to something like 4 for past-end memory corruption check, which is off by default to ensure performance is not affected.

The code was heavily refactored during the modifications. I was quite satisfied with the new code, and I released Nvwa 0.8 as a result.

Summary

So I have presented my small memory leakage detector. I’ll make a summary here, and you can also consult the online doxygen documentation for the respective descriptions of the functions, variables, and macros.

This implementation is relatively simple. It is lacking in features when compared with commercial applications, like Rational Purify, or even some open-source libraries. However, it is

  • Cross-platform and portable: Apart from the code handling threading (which is separated from the main code) and providing special GCC support (which is automatically on when GCC is detected), only standard language features are used. It should compile under modern C++ compilers. It is known to work with GCC (2.95.3 and later), MSVC 6/7.1, and Borland C++ Compiler 5.5.1.
  • Easy to use: Because “void* operator new(size_t)” is overloaded too, memory leaks could be detected without including my header file. — I myself use it this way habitually in nearly every C++ program. — Generally, I check for the leak position only after I see memory leaks reported by debug_new.
  • Flexible: Its behaviour can be tailored by macros at compile time.
  • Efficient: It has a very low overhead, and it can be used in debugging applications that require high performance.
  • Open-source: It is released in the zlib/libpng licence and you have the freedom to use, change, or redistribute as you like.

With the recent improvements, some of the old restrictions are gone. The macro new or DEBUG_NEW in debug_new.h can mostly work if the newed object has operator news as class member functions, or ifnew(std::nothrow) is used in the code, though the macro new must be turned off when defining any operator news. Even in the worst case, linking only debug_new.cpp should always work, as long as the allocation operation finally goes to the global operator new(size_t) or operator new(size_t, std::nothrow_t).

Source is available, for your programming pleasure, in the CVS (most up to date) or download of Stones of Nvwa.

May the Source be with you!

Bibliography

[Alexandrescu2001]  Andrei Alexandrescu. Modern C++ Design: Generic Programming and Design Patterns Applied. Addison-Wesley, 2001.
[C++1998]  ISO/IEC. Programming Languages — C++. Reference Number ISO/IEC 14882:1998(E), 1998.
[Stroustrup1997]  Bjarne Stroustrup. The C++ Programming Language (Third Edition). Addison-Wesley, 1997.

HTML for code syntax highlighting is generated by Vim

Note: Although I would love to, I did not succeed in making this page conform to the HTML 4.01 specification. I guess W3C is to blame. Why should <font color=...> be forbidden inside a <pre> block (Vim currently generate HTML code this way), while the clumsier <span style="color: ..."> is allowed? However, this is fixable after all (I have already written a converter indeed). There is worse to come: <nobr> is not a valid tag. When a major browser could break a line after a “-” and no mechanisms are provided by the standard to achieve the no-breaking effect, using something like <nobr> is inevitable (and I can name other cases where <nobr> is needed). I cannot fix the browser, so I have to choose to break the standard. Detailed online information about this problem can be found here.

2004-3, Chinese version first published here at IBM developerWorks China 
2004-11-28, rewritten in English (at last) by Wu Yongwei 
2007-12-31, last updated by Wu Yongwei

A Cross-Platform Memory Leak Detector的更多相关文章

  1. vld for memory leak detector (release version)

    有没有这样的情况,无法静态的通过启动和退出来查找内存泄露,比如网络游戏,你总不能直接关游戏那玩家怎么办? 现在vld支持release,我们可以动态的找. 1.在release版本使用vld了.< ...

  2. Linux C/C++ Memory Leak Detection Tool

    目录 . 内存使用情况分析 . 内存泄漏(memory leak) . Valgrind使用 1. 内存使用情况分析 0x1: 系统总内存的分析 可以从proc目录下的meminfo文件了解到当前系统 ...

  3. Cocos开发中性能优化工具介绍之Visual Studio内存泄漏检测工具——Visual Leak Detector

    那么在Windows下有什么好的内存泄漏检测工具呢?微软提供Visual Studio开发工具本身没有什么太好的内存泄漏检测功能,我们可以使用第三方工具Visual Leak Detector(以下简 ...

  4. Cocos性能优化工具的开发介绍Visual Studio内存泄漏检测工具——Visual Leak Detector

    然后,Windows下有什么好的内存泄漏检測工具呢?微软提供Visual Studio开发工具本身没有什么太好的内存泄漏检測功能.我们能够使用第三方工具Visual Leak Detector(下面简 ...

  5. VisualStudio 怎么使用Visual Leak Detector

    VisualStudio 怎么使用Visual Leak Detector 那么在Windows下有什么好的内存泄漏检测工具呢?微软提供Visual Studio开发工具本身没有什么太好的内存泄漏检测 ...

  6. 使用Visual Leak Detector for Visual C++ 捕捉内存泄露

    什么是内存泄漏? 内存泄漏(memory leak),指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况.内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段 ...

  7. 使用Visual Leak Detector检测内存泄漏[转]

      1.初识Visual Leak Detector 灵活自由是C/C++语言的一大特色,而这也为C/C++程序员出了一个难题.当程序越来越复杂时,内存的管理也会变得越加复杂,稍有不慎就会出现内存问题 ...

  8. vld(Visual Leak Detector) 内存泄露检测工具

    初识Visual Leak Detector 灵活自由是C/C++语言的一大特色,而这也为C/C++程序员出了一个难题.当程序越来越复 杂时,内存的管理也会变得越加复杂,稍有不慎就会出现内存问题.内存 ...

  9. 【翻译自mos文章】Windows平台下的 Oraagent Memory Leak

    来源于: Oraagent Memory Leak (文档 ID 1956840.1) APPLIES TO: Oracle Database - Enterprise Edition - Versi ...

随机推荐

  1. uva 1449 - Dominating Patterns

    简单的AC自动机: #include<cstdio> #include<cstring> #include<queue> #define maxn 150005 u ...

  2. PowerDesigner从SqlServer 数据库中导入实体模型

    此篇是之前写的,从我的CSDN博客挖过来的- 一.开启数据库服务并配置ODBC数据源 1.开启数据库服务 (1)通过SQL Server Configuration Manager配置工具启动SQL ...

  3. 使用 .NET 平台,如何玩转 Universal Windows 应用?

    2015年7月30日 本文作者是 Managed Languages 团队项目经理 Lucian Wischik. 不久前,Visual Studio 2015上新增 Windows 10 应用的开发 ...

  4. [转贴] C++内存管理检测工具 Valgrind

    用C/C++开发其中最令人头疼的一个问题就是内存管理,有时候为了查找一个内存泄漏或者一个内存访问越界,需要要花上好几天时间,如果有一款工具能够帮助我们做这件事情就好了,valgrind正好就是这样的一 ...

  5. Qt创建窗体的过程

    版权声明 本文为原创作品,请尊重作者的劳动成果.转载必须保持文章完整性,并以超链接形式注明原始作者“ tingsking18”和 主站点地址,方便其他朋友提问和指正. QT源码解析(一) QT创建窗口 ...

  6. 带圆角的EditText

    转载请注明出处:http://blog.csdn.net/krislight/article 1.定义一个Drawable <?xml version="1.0" encod ...

  7. Navigation Drawer介绍

    在2013 google IO当天,Android团的更新了Support库,新版本(V13)的Support库中新加入了几个比较重要的功能. 添加 DrawerLayout 控件,支持创建  Nav ...

  8. JAVA 抛出与声明异常

    在编程过程中,我们往往会遇到这种情况,在当前环境中无法解决,比如用户传入的参数错误,IO设备问题等.此时,就要从当前环境中抛出异常提交给上级来处理. 在JAVA语言中,使用throw关键字来抛出异常. ...

  9. Learing WCF Chapter1 WCF Services

    WCF ServicesWCF services are the new distributed boundary in an enterprise application—with an empha ...

  10. bzoj1878

    像我这种蒟蒻这道题从在线算法思考基本毫无思路 但是发现题目中只涉及到询问而不涉及到修改,这类题目一般都是离线算法大概 考虑到这题为什么不能直接区间求值,因为区间中同色点会被重复计算(废话) 下面我们就 ...