種類 | 儲存位置 | 建立、刪除方式 |
區域變數 | stack或暫存器 | 進入函式時配置空間,函式return時釋放。 |
全域變數 | bss或data段 | 程式開始時就配置空間,程式執行期間會一直存在。 |
動態配置 | heap | 用malloc()等函式配置,用free()釋放。 |
種類 | 建立 | 刪除 |
區域變數 | void function1(){ SPImagePlane plane; } 宣告同時呼叫建構子。 |
離開作用域時自動呼叫解構子並刪除。 |
全域變數 或class裡的static member |
struct Class1{ static SPImagePlane plane; }; SPImagePlane Class1::plane; 儲存在全域空間,main()之前就會呼叫建構子。 |
main() return後呼叫解構子。 |
函式裡的static變數 | void function1(){ static SPImagePlane plane; } 儲存在全域空間,第一次進入函式時呼叫建構子。 從產生的組合語言得知,compiler會加上判斷式檢查是否已呼叫過建構子,第二次以後進入函式就不呼叫。 |
main() return後呼叫解構子。 |
placement new | void* p=malloc(sizeof(SPImagePlane)); SPImagePlane* plane=new(p) SPImagePlane; 手動配置空間再直接呼叫建構子。 可以如上用malloc,或用其他自訂方法。 |
plane->~SPImagePlane(); free(p); 直接呼叫解構子再釋放空間。 |
new | SPImagePlane* plane=new SPImagePlane; 自動用系統預設方法配置空間(一般用malloc),並呼叫建構子。 |
delete plane; 自動呼叫解構子,再用free()釋放空間。 |
void function1(){ { SPImagePlane paperDoll[4]; //配置四個SPImagePlane …… //使用SPImagePlane } //在此呼叫解構子並回收空間 …… //繼續處理 } |
儘量使用區域變數,非必要不要使用全域變數和動態分配。
例如函式裡需要臨時物件,函式return前就刪除,可能有人會這樣寫使用動態配置有額外的效能消耗
void function1(){
SPTextPlane* plane=new SPTextPlane;
……
delete plane;
}
比較好的方法是配置在stack上
void function1(){
SPTextPlane plane;
……
}
區域變數離開作用域後就消失,所以如果有個函式是產生一個陣列或struct再傳回給上層的函式,可能無可避免要用new。
(此例取自引擎的繪圖部分,計算貼圖坐標填進陣列)
short* generateTexCoord(PlaneHasTexCoord* plane){
short* texCoord=new short[8];
…… //填入texCoord
return texCoord;
}
void outerFunction(){
……
short* texCoord=generateTexCoord(plane);
…… //使用texCoord
delete[] texCoord;
}
可以改寫成上層配置變數,然後把它的指標傳入函式。
void fillTexCoord(PlaneHasTexCoord* plane, short* texCoord){
texCoord[0]=……;
texCoord[1]=……;
…… //填入texCoord
}
void outerFunction(){
……
short texCoord[8];
fillTexCoord(plane, texCoord);
…… //使用texCoord
}
Windows API裡常見這種用法,Windows API要產生一個struct並填入資料通常是在外面宣告struct再把它的指標傳入,很少主動產生一個struct。
如果是固定長度的陣列,即WCHAR wcharBuffer[10],長度是寫在程式裡的數值或用const定義,可以如左邊宣告。
如果執行時期才能確定長度呢?即WCHAR wcharBuffer[len]的len是變數而不是常數。(這種稱為variable length array)
轉換文字編碼常碰到這種情況
//charArray為char*型態
int outLen=MultiByteToWideChar(CP_UTF8, 0, charArray, -1, NULL, 0); //傳回需要的buffer大小
WCHAR wcharBuffer[outLen]; //配置記憶體
MultiByteToWideChar(CP_UTF8, 0, charArray, -1, wcharBuffer, outLen); //實際做轉換
GCC和clang能接受陣列長度是變數,可以照上面寫。
但是VC不支援這種寫法,這樣寫編譯會跳error。
這時可用一個函式代替用法跟malloc()差不多,不過它是配置在stack上,函式return時會自動回收空間,不用手動呼叫free()釋放。
#include<malloc.h>
void* _alloca(size_t size);
上例要改成這樣一個WCHAR佔兩byte,所以要乘以sizeof(WCHAR)才是byte數。
int outLen=MultiByteToWideChar(CP_UTF8, 0, charArray, -1, NULL, 0);
WCHAR* wcharBuffer=(WCHAR*)_alloca(outLen*sizeof(WCHAR));
MultiByteToWideChar(CP_UTF8, 0, charArray, -1, wcharBuffer, outLen);
為此我寫了一個macro,可以根據compiler選擇能用的方式。
#ifdef _MSC_VER
#define VARARRAY(type,name,num) type* name=(type*)_alloca((num)*sizeof(type))
#elif defined __GNUC__ || defined __clang__
#define VARARRAY(type,name,num) type name[num]
#endif
//用法
VARARRAY(WCHAR, wcharBuffer, outLen);
如果物件內部用到好幾個陣列,這些陣列產生、刪除是同進退,把總共需要的byte數算出來再一次malloc()解決。
例如艾莉兒身上用來繪製tilemap的class,需要計算各格子的螢幕坐標和貼圖坐標,繪圖時傳給D3D和OpenGL的函式。
class SPTilePlane{
short* screenCoord; //螢幕坐標
short* texCoord; //貼圖坐標
……
};
一個矩形是4個坐標(D3D11和OpenGL 3.3不再有四邊形primitive,我用index array模擬出四邊形),毎個坐標是兩個short,所以一個格子是8個short。
建構式、初始化和解構式有這樣的內容
SPTilePlane::SPTilePlane():texCoord(0){} //標示此class還沒初始化
void SPTilePlane::init(int planeID, int imageID,
const TilePlaneMetric* metric, int fill, int visible){
//TilePlaneMetric是關於尺寸的資訊
……
this->cols=metric->cols; //欄數
this->rows=metric->rows; //列數
const int SHORTNUM=this->cols*this->rows*8; //需要的short數量
this->texCoord=(short*)malloc(SHORTNUM*sizeof(short)*2);
//一個short是2 byte,所以乘以sizeof(short)
//有screenCoord和texCoord兩個陣列要用,所以乘以2
this->screenCoord=this->texCoord+SHORTNUM;
//這樣screenCoord會緊接在texCoord之後
……
}
SPTilePlane::~SPTilePlane(){
if(this->texCoord)
free(this->texCoord);
}
class都做成建構子沒有任何參數,呼叫init()函式才真正初始化,這樣做目的是要能在struct和class裡靜態宣告,方便配置在stack上。
struct OptionMenu{
……
//顯示模式
BorderCursor graphicModeCursor;
//vsync
BorderCursor vsyncCursor;
//音量
SPImagePlane bgmBar1;
SPImagePlane bgmBar2;
SPImagePlane seBar1;
SPImagePlane seBar2;
……
OptionMenu(); //建構子裡呼叫各member的init()初始化
};
//像這樣使用
void optionMenu(){
OptionMenu optionMenu;
//之後操作物件member做option畫面的處理
……
}
如果陣列元素是需要呼叫建構子的物件,要如何呼叫建構子呢?上面說的placement new在此就派上用場了。雖然class叫StraightBullet,Cyber Sprite裡其實不是用這個方法管理子彈,只是隨便舉個比較簡單的例子。
//建立
StraightBullet* bullets=(StraightBullet*)malloc(sizeof(StraightBullet)*num);
const float ANGLE_STEP=M_PI*0.5/(num-1);
float nowAngle=0;
for(int i=0;i<num;i++){
new(bullets+i) StraightBullet(startX, startY, nowAngle);
nowAngle+=ANGLE_STEP;
}
//刪除
for(int i=0;i<num;i++){
bullets[i].~StraightBullet();
}
free(bullets);
如果把pool allocator做成一個global的模組,做出這三個類似malloc()的函式呢?內部處理是先用malloc()配置一大塊pool再從裡面切,呼叫一次poolMalloc()傳回一小塊,等一個pool用完再malloc()一塊新的。
void* REGPARM poolMalloc(uint32_t size);
void* REGPARM poolRealloc(const void* ptr, uint32_t size);
void REGPARM poolFree(const void* ptr);
要求空間小的時候提升速度的效果比較明顯,大塊就不太有需要使用,而且導致pool過大也不好,我是設成252 byte以下才能用這個分配器。
REGPARM是什麼請參照這篇:改造calling convention,我在艾莉兒身上用了很多提升速度的技巧。
根據實測,這個技巧威力很大,配置幾千個小塊空間時,自訂分配器速度是malloc()、free()的3倍,而且是一勞永逸的工,一次做好以後其他軟體也可以沿用。
不過,目前暫不想公開詳細做法,看我以後會不會改變主意。
#include<malloc.h> size_t _msize(void* pointer); |
#include<malloc.h> size_t malloc_usable_size(void* pointer); |
malloc() | malloc_usable_size() |
7 | 24 |
125 | 136 |
5000 | 5016 |
1048320 (=1024×1024-256) |
1048560 |
1048576 (=1024×1024) |
1052656 |
活動與參展 (0)
└活動與參展資訊 (1)
└活動與製作後記 (11)
└販售會遊戲團調查 (14)
遊戲團隊「電子妖精實驗室」 (0)
└重要消息 (4)
└Cyber Sprite遊戲秘密 (2)
└製作進度 (26)
創作 (0)
└繪圖 (24)
└程式 (48)
└故事、劇本 (3)