GDI+編程中的一條錯誤信息及其原因分析
GDI+編程中的一條錯誤信息及其原因分析 公司不讓用盜版,遂准備逐一將各軟件要麽換成開源的,要麽就自己寫,看了看,就數Acdsee最簡單了(有些高級功能根本用不著),行,從這個入手吧。
需求分析:基本的圖片查看功能,圖片格式轉換功能,基本的圖形變換功能。
技術可行性分析:MS提供的GDI+已經提供了比較專業的圖形顯示、格式轉換功能,而且簡單易用。
....
OK,就緒,開始幹吧。
但是在程序編寫的過程中,有條錯誤信息讓我很不解。程序中有如下語句:
bmPhoto = new Bitmap( THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, PixelFormat24bppRGB );
每次DEBUG編譯的時候總是報告如下的錯誤:
error C2660: 'new' : function does not take 3 parameters
開始以爲是Bitmap的構造函數的問題,但是查了一下,Bitmap明明有個構造函數:
Bitmap(IN INT width,
IN INT height,
IN PixelFormat format = PixelFormat32bppARGB);
那會是什麽問題呢?上網討論了一下,最終將問題鎖定在MFC程序中的這樣一個宏定義上:
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
這幾行從來都不會引起我們注意的代碼有什麽問題呢?爲什麽會使得我們的代碼報告如上所述的編譯錯誤呢?
讓我們來看看DEBUG_NEW的定義(在afx.h中):
#if defined(_DEBUG) && !defined(_AFX_NO_DEBUG_CRT)
// Memory tracking allocation
void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
#define DEBUG_NEW new(THIS_FILE, __LINE__)
#if _MSC_VER >= 1200
void AFX_CDECL operator delete(void* p, LPCSTR lpszFileName, int nLine);
#endif
看到這裏你可能會想,new被define成了DEBUG_NEW,而後者又被define成了new(...),這不是成了個循環?非也。由于afx.h早于任何其它頭文件被包含(stdafx.h包含afxwin.h,afxwin.h又包含了afx.h,而MFC要求我們在任何有效代碼之前包含stdafx.h,當然,這不是必須的),所以DEBUG_NEW的定義早于後面的#define new DEBUG_NEW,也就是說這個define只對後面的代碼有效,對前面已經include了的afx.h中的代碼是無效的。
上面只是題外話,現在回到正題。
MFC重載operator new,是爲了方便定位內存泄漏,重載後的operator new會記錄下所分配的每塊內存對應的__FILE__和__LINE__信息。一般來講,標准的operator new的聲明如下:
void *__cdecl operator new(size_t);
即它只有一個參數,只接收一個size信息。我們的如下代碼
int* pi = new int; // the same as int* pi = new int(); or int* pi = new int[1];
等價于
int* tpi = (int*)operator new(sizeof(int)); // attention: this line cannot pass compilation if you have define DEBUG_NEW
int* pi = tpi;
同理,定義DEBUG_NEW前,文章開頭報錯的這條語句:
Bitmap* bmPhoto = new Bitmap( THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, PixelFormat24bppRGB );
等價于
Bitmap* tbmPhoto = (Bitmap*)operator new(sizeof(Bitmap));
tbmPhoto->Bitmap( THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, PixelFormat24bppRGB ); // initialize variable
Bitmap* bmPhoto = tbmPhoto;
但是現在,由于DEBUG_NEW使用的是被重載的operator new:
void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
上述代碼等價于:
Bitmap* tbmPhoto = (Bitmap*)operator new(sizeof(Bitmap), __FILE__, __LINE__);
tbmPhoto->BitmapBitmap( THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, PixelFormat24bppRGB ); // initialize variable
Bitmap* bmPhoto = tbmPhoto;
回過頭來看gdiplus.h中的operator new的聲明(在GdiplusBase.h中):
class GdiplusBase
{
public:
void (operator delete)(void* in_pVoid)
{
DllExports::GdipFree(in_pVoid);
}
void* (operator new)(size_t in_size)
{
return DllExports::GdipAlloc(in_size);
}
void (operator delete[])(void* in_pVoid)
{
DllExports::GdipFree(in_pVoid);
}
void* (operator new[])(size_t in_size)
{
return DllExports::GdipAlloc(in_size);
}
};
它重載了operator new,並且沒有提供一個可以容納3個參數的operator new,同時基于這樣一個事實:
不同命名域(指全局命名空間與有名命名空間之間,父類與子類,全局與類內部)內進行重載時,下一級的命名空間會覆蓋掉上一級的定義,除非顯示調用上一級的定義。
因此,全局的重新定義的operator new並不能用于Bitmap類。也正因爲這一原因,編譯器會報告:
Bitmap* tbmPhoto = (Bitmap*)Bitmap::operator new(sizeof(Bitmap), __FILE__, __LINE__);
error C2660: 'new' : function does not take 3 parameters
知道了這一點,要修正這一問題,只需給class GdiplusBase多重載幾個operator new即可。修正後的class GdiplusBase如下:
#ifdef _DEBUG
namespace Gdiplus
{
namespace DllExports
{
#include <GdiplusMem.h>
};
#ifndef _GDIPLUSBASE_H
#define _GDIPLUSBASE_H
class GdiplusBase
{
public:
void (operator delete)(void* in_pVoid)
{
DllExports::GdipFree(in_pVoid);
}
void* (operator new)(size_t in_size)
{
return DllExports::GdipAlloc(in_size);
}
void (operator delete[])(void* in_pVoid)
{
DllExports::GdipFree(in_pVoid);
}
void* (operator new[])(size_t in_size)
{
return DllExports::GdipAlloc(in_size);
}
void * (operator new)(size_t nSize, LPCSTR lpszFileName, int nLine)
{
return DllExports::GdipAlloc(nSize);
}
void operator delete(void* p, LPCSTR lpszFileName, int nLine)
{
DllExports::GdipFree(p);
}
};
#endif // #ifndef _GDIPLUSBASE_H
}
#endif // #ifdef _DEBUG
OK,問題已解決,其實這只是個重載operator new的問題,但這個問題由于DEBUG_NEW這個不起眼的宏,倒還真變得有點複雜。
最後總結一下,在進行operator new重載時應注意:
1.new operator是不可以重載的,可以重載的是operator new。new operator 首先調用 operator new,然後調用構造函數(如果有的話)。new operator的這個行爲是不可以重載的,可以重載的僅僅是operator new,也就是內存分配。
2.重載operator new是一件必須十分小心的事情,在編寫MFC程序或者你所編寫的系統重載了全局的operator new時,尤其需要注意,同時應注意所有的#include頭文件最好添加在所有define之前,以免造成受到後續對new的重定義的影響。你可以嘗試在你的MFC程序的#define new DEBUG_NEW一句之後,添加#include <vector>,你會收到一大堆莫名奇妙的錯誤提示(DEBUG編譯時才有),這正是由于#define new DEBUG_NEW和後面的static char THIS_FILE[] = __FILE__;造成的影響。
3.operator new/delete在性質上類似于靜態函數,你可以直接通過類名來訪問它們。
4.理解了operator new的基本概念,要理解頭文件NEW中的placement new/delete的實現也就不是什麽難事了,頭文件NEW中的placement new/delete的實現如下:
#ifndef __PLACEMENT_NEW_INLINE
#define __PLACEMENT_NEW_INLINE
inline void *__cdecl operator new(size_t, void *_P)
{return (_P); }
#if _MSC_VER >= 1200
inline void __cdecl operator delete(void *, void *)
{return; }
#endif
#endif
附:
(轉貼)C++的各種new簡介
1.new T
第一種new最簡單,調用類的(如果重載了的話)或者全局的operator new分配空間,然後用類型後面列的參數來調用構造函數,用法是
new TypeName(initial_args_list).
如果沒有參數,括號一般可以省略.例如
int *p=new int;
int *p=new int(10);
int *p=new foo('hello');
通過調用delete來銷毀:
delete p;
2. new T[]
這種new用來創建一個動態的對象數組,他會調用對象的operator new[]來分配內存(如果沒有則調用operator new,搜索順序同上),然後調用對象的31m默認構造函數初始化每個對象用法:
new TypeName[num_of_objects];
例如
int *p= new int[10];
銷毀時使用operator delete31m[]
3.new()T 和new() T[]
這是個帶參數的new,這種形式的new會調用operator new(size_t,OtherType)來分配內存,這裏的OtherType要和new括號裏的參數的類型兼容,這種語法通常用來在某個特定的地址構件對象,稱爲placement new,前提是operator new(size_t,void*)已經定義,通常編譯器已經提供了一個實現,包含<new>頭文件即可,這個實現只是簡單的把參數的指定的地址返回,因而new()運算符就會在括號裏的地址上創建對象.
需要說明的是,第二個參數不是一定要是void*,可以識別的合法類型,這時候由C++的重載機制來決定調用那個operator new.
當然,我們可以提供自己的operator new(size_,Type),來決定new的行爲,比如
char data[1000][sizeof(foo)];
inline void* operator new(size_t ,int n)
{
return data[n];
}
就可以使用這樣有趣的語法來創建對象:
foo *p=new(6) foo(); //把對象創建在data的第六個單元上的確很有意思
標准庫還提供了一個nothrow的實現:
void* operator new(std::size_t, const std::nothrow_t&) throw();
void* operator new[](std::size_t, const std::nothrow_t&) throw();
就可以實現調用new失敗時不抛出異常
new(nothrow) int(10);
// nothrow 是std::nothrow_t的一個實例
placement new 創建的對象不能直接delete來銷毀,而是要調用對象的析夠函數來銷毀對象,至于對象所占的內存如何處理,要看這塊內存的具體來源.
4. operator new(size_t)
這個的運算符分配參數指定大小的內存並返回首地址,可以爲自定義的類重載這個運算符,方法就是在類裏面聲明加上
void *operator new(size_t size)
{
//在這裏分配內存並返回其地址
}
無論是否聲明,類裏面重載的各種operator new和operator delete都是具有static屬性的.
一般不需要直接調用operator new,除非直接分配原始內存(這一點類似于C的malloc),在沖突的情況下要調用全局的operator加上::作用域運算符:
::operator new(1000); // 分配1000個31m字節
返回的內存需要回收的話,調用對應的operator delete
5.operator new[](size_t)
這個也是分配內存,,只不過是專門針對數組,也就是new T[]這種形式,當然,需要時可以顯式調用
6.operator new(size_t size, OtherType other_value)
和operator new[](size_t size, OtherType other_value)
參見上面的new()
需要強調的是,new用來創建對象並分配內存,它的行爲是不可改變的,可以改變的是各種operator new,我們就可以通過重載operator new來實現我們的內存分配方案.
參考資料:
1.PRB: Microsoft Foundation Classes DEBUG_NEW Does Not Work with GDI+. http://support.microsoft.com/default.aspx?scid=kb;en-us;317799
2.VC++6.0中內存泄漏檢測. http://blog.vckbase.com/bruceteen/archive/2004/10/28/1130.aspx
3.More Effective C++. Item 8: Understand the different meanings of new and delete.