A Cross-Platform Memory Leak Detector
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 new, sizeof(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 file
, line
, 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_new: new_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 true
; new_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 is initialization; [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的更多相关文章
- vld for memory leak detector (release version)
有没有这样的情况,无法静态的通过启动和退出来查找内存泄露,比如网络游戏,你总不能直接关游戏那玩家怎么办? 现在vld支持release,我们可以动态的找. 1.在release版本使用vld了.< ...
- Linux C/C++ Memory Leak Detection Tool
目录 . 内存使用情况分析 . 内存泄漏(memory leak) . Valgrind使用 1. 内存使用情况分析 0x1: 系统总内存的分析 可以从proc目录下的meminfo文件了解到当前系统 ...
- Cocos开发中性能优化工具介绍之Visual Studio内存泄漏检测工具——Visual Leak Detector
那么在Windows下有什么好的内存泄漏检测工具呢?微软提供Visual Studio开发工具本身没有什么太好的内存泄漏检测功能,我们可以使用第三方工具Visual Leak Detector(以下简 ...
- Cocos性能优化工具的开发介绍Visual Studio内存泄漏检测工具——Visual Leak Detector
然后,Windows下有什么好的内存泄漏检測工具呢?微软提供Visual Studio开发工具本身没有什么太好的内存泄漏检測功能.我们能够使用第三方工具Visual Leak Detector(下面简 ...
- VisualStudio 怎么使用Visual Leak Detector
VisualStudio 怎么使用Visual Leak Detector 那么在Windows下有什么好的内存泄漏检测工具呢?微软提供Visual Studio开发工具本身没有什么太好的内存泄漏检测 ...
- 使用Visual Leak Detector for Visual C++ 捕捉内存泄露
什么是内存泄漏? 内存泄漏(memory leak),指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况.内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段 ...
- 使用Visual Leak Detector检测内存泄漏[转]
1.初识Visual Leak Detector 灵活自由是C/C++语言的一大特色,而这也为C/C++程序员出了一个难题.当程序越来越复杂时,内存的管理也会变得越加复杂,稍有不慎就会出现内存问题 ...
- vld(Visual Leak Detector) 内存泄露检测工具
初识Visual Leak Detector 灵活自由是C/C++语言的一大特色,而这也为C/C++程序员出了一个难题.当程序越来越复 杂时,内存的管理也会变得越加复杂,稍有不慎就会出现内存问题.内存 ...
- 【翻译自mos文章】Windows平台下的 Oraagent Memory Leak
来源于: Oraagent Memory Leak (文档 ID 1956840.1) APPLIES TO: Oracle Database - Enterprise Edition - Versi ...
随机推荐
- bzoj 1195: [HNOI2006]最短母串 爆搜
1195: [HNOI2006]最短母串 Time Limit: 10 Sec Memory Limit: 32 MBSubmit: 894 Solved: 288[Submit][Status] ...
- 计算几何模板 (bzoj 1336,poj 2451 ,poj3968)
poj 3968 (bzoj 2642) 二分+半平面交,每次不用排序,这是几个算几版综合. #include<iostream> #include<cstdio> #incl ...
- 基于opencv的小波变换
基于opencv的小波变换 提供函数DWT()和IDWT(),前者完成任意层次的小波变换,后者完成任意层次的小波逆变换.输入图像要求必须是单通道浮点图像,对图像大小也有要求(1层变换:w,h必须是2的 ...
- Android-x86虚拟机安装配置全攻略
转自Android-x86虚拟机安装配置全攻略 注:这里安装从简,具体请参考虚拟机Vmware安装运行安卓4.0详细教程 Android-x86虚拟机安装配置网上有很多,但是全部说明白的确不多,希望这 ...
- 【Xamarin开发 Android 系列 5】 Xamarin 的破解
原文:[Xamarin开发 Android 系列 5] Xamarin 的破解 有关这个话题,十分敏感,公司开发还是支持下商业版权吧,毕竟一帮猴子辛辛苦苦没日没夜的干活,不说开宝马奔驰,吃饭还是必须的 ...
- 【Xamarin开发 Android 系列 1】环境部署搭建
原文:[Xamarin开发 Android 系列 1]环境部署搭建 开篇自然先扯一段,近几年移动互联网如果熊猫零食一样,蔓延迅速.楼主身为一个微软忠实的粉丝,无奈,老爹不给力.Silverlight开 ...
- 行为树实现AI逻辑
http://blog.csdn.net/kenkao/article/details/6099966 http://www.aisharing.com/archives/99 http://www. ...
- 【转】Android bluetooth介绍(三): 蓝牙扫描(scan)设备分析
原文网址:http://blog.csdn.net/xubin341719/article/details/38584469 关键词:蓝牙blueZ A2DP.SINK.sink_connect.s ...
- HDU-1701 Binary Tree Traversals
http://acm.hdu.edu.cn/showproblem.php?pid=1710 已知先序和中序遍历,求后序遍历二叉树. 思路:先递归建树的过程,后后序遍历. Binary Tree Tr ...
- 文本编辑器 CKEditor 用法
最新文本编辑器,FCK升级版:CKEditor.NET CKEditor.NET.dll 版本:3.6.4.0 官方网址:http://ckeditor.com/ 效果图: 配置web.c ...