本文是第一部分,翻譯的內容為int類型的轉換,內存分配。
c語言是系統程序,嵌入式系統以及很多其他應用程序的一種選擇。然而似乎對計算機不是特別感興趣,才不會去接觸c語言,熟悉c語言的各個方面,以及特別多的細節是一個巨大的挑戰。本文試著提供較多的資料來闡述其中的一部分。包括:int類型的轉換,內存分配,數組的指針轉換,顯式的內存函數,Interpositioning(不太理解) ,向量變化。
int 溢出和類型轉換
很多c語言程序員都傾向於假設對int類型基本的操作是安全的,使用的時候不會過多的去審查。實際上這些操作很容易出問題。思考下後面的代碼:
int main(int argc, char** argv) {
long i = -1;
if (i < sizeof(i)) {
printf(OK
);
}
else {
printf(error
);
}
return 0;
}
(本人注:結果是error,出乎很多人的意料吧,下面是作者的解釋)
導致這樣的原因是變量i被轉換成了unsigned int類型。所以它的值不再是-1,而是size_t的最大值,這是由於sizeof操作符的類型導致的。
具體的原因在C99/C11標准的常用算術轉換章節中找到:
“如果操作符中有unsinged int類型,並且操作符的優先級大於或者等於其他操作符的時候,需要將signed int轉換為unsinged int.
size_t在c語言標准中被定義為至少16位的unsinged int 類型。通常size_t的位數是和系統相關的,int類型的大小和size_t至少是相等的,於是上述的規則強行的把變量轉換為unsinged int.
(關於sizof的介紹可以查看http://blog.csdn.net/sword_8367/article/details/4868283)
在我們使用int類型大小的是,就會存在一些問題。c語言標准並沒有明確的定義short, int ,long ,long long 以及他們unsinged版本的大小。只是把最小的大小強制規定了。以x86_64框架為例子,long類型在linux上為64個字節,相反在64位的windows上仍然是32個字節。為了使代碼更好的移植,通常的方法是用長度固定的類型,例如unit16_t 或者int32_t, 他們在C99標准的stdint.h頭文件中定義。下面三種int類型在那裡被定義:
1明確大小的:uint8_t uint16_t int32_t 等等
2定義類型的最小長度的:uint_least9_t, uint_least16_t, int_least32_t等
3最高效,定義最小長度的:uint_fast8_t, uint_fast16_t, int_fast32_t等
但是不幸的是,使用stdint.h並不能避免所有的問題。”integral promotion rule(int類型轉換的規則)裡面這樣說:
If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. All other types are unchanged by the integer promotions.
如果一個int可以表現原始類型所有的值,那麼這個值被轉換為int,否則轉換為unsinged int .這叫做int類型轉換,所有其他的類型在int類型轉換中不會被改變。
下面的代碼在32為上結果為65536, 在16為機器上為0;
uint32_t sum()
{
uint16_t a = 65535;
uint16_t b = 1;
return a+b;
}
int類型轉換保持變量的符號,不過一個簡單的char類型轉換是被轉換為有符號數還是無符號數呢?
通常char類型轉換要依靠硬件結構和操作系統,通常在特定平台的程序二進制接口中被確定的。如果你發現char被提升為siged char ,下面的代碼會打印-128,127(例如x86框架),否則為128,129.gcc加上編譯選項-funsigned-char強制的將x86平台上提升為無符號數。
char c = 128;
char d = 129;
printf(%d,%d ,c,d);
內存分配和內存管理
malloc, calloc,realloc,free
malloc分配一個以bytes為單位的,指定大小的,未初始化的內存空間。如果大小為0,返回結果取決於操作系統,在c語言或者POSIX中沒有明確說明這個行為.
如果空間大小必須是0,這個結果由編譯器決定:返回一個空指針或者是唯一的指針。
malloc(0)通常會返回一個唯一的合法的指針。任何一種返回方式,必須保證在調用free函數的時候,不能報錯。其中空指針,free函數不會做任何操作。
所以如果以一個表達式的結果作為malloc的參數的時候,需要測試int越界。
size_t computed_size;
if (elem_size && num > SIZE_MAX / elem_size) {
errno = ENOMEM;
err(1, overflow);
}
computed_size = elem_size*num;
void * calloc(size_t nelem, size_t elsize);
一般情況下,分配一系列相同大小的空間的時候,應該使用calloc,這樣不用表達式去計算大小()。另外它會初始化內存空間為0.釋放分配的空間,使用free.
void* realloc(void* ptr, unsigned newsize);
realloc將會改變之前分配內存的大小。函數返回的指針指向新的內存位置,裡面的內容可能會和原來的內容有相同的部分。如果新分配的大小比原來的大,增加的空間就可能沒有被初始化。如果參數中舊指針為空,大小不等於0,那麼作用等同於malloc。如果參數中大小為0,舊指針非空,那麼產生的結果取決於操作系統。
大部分操作系統去釋放舊指針的內存,返回malloc(0)或者返回NULL.例如,windows會釋放內存,並且返回NULL,OpenBSD也會釋放內存,並且會返回指向大小為0的指針。
如果realloc失敗了會返回NULL,並且會留下曾經分配的內存。所以不僅要檢測參數是否溢出,當realloc分配失敗的時候,還要正確處理舊的內存空間。
#include
#include
#include
#include
#define VECTOR_OK 0
#define VECTOR_NULL_ERROR 1
#define VECTOR_SIZE_ERROR 2
#define VECTOR_ALLOC_ERROR 3
struct vector {
int *data;
size_t size;
};
int create_vector(struct vector *vc, size_t num) {
if (vc == NULL) {
return VECTOR_NULL_ERROR;
}
vc->data = 0;
vc->size = 0;
/* check for integer and SIZE_MAX overflow */
if (num == 0 || SIZE_MAX / num < sizeof(int)) {
errno = ENOMEM;
return VECTOR_SIZE_ERROR;
}
vc->data = calloc(num, sizeof(int));
/* calloc faild */
if (vc->data == NULL) {
return VECTOR_ALLOC_ERROR;
}
vc->size = num * sizeof(int);
return VECTOR_OK;
}
int grow_vector(struct vector *vc) {
void *newptr = 0;
size_t newsize;
if (vc == NULL) {
return VECTOR_NULL_ERROR;
}
/* check for integer and SIZE_MAX overflow */
if (vc->size == 0 || SIZE_MAX / 2 < vc->size) {
errno = ENOMEM;
return VECTOR_SIZE_ERROR;
}
newsize = vc->size * 2;
newptr = realloc(vc->data, newsize);
/* realloc faild; vector stays intact size was not changed */
if (newptr == NULL) {
return VECTOR_ALLOC_ERROR;
}
/* upon success; update new address and size */
vc->data = newptr;
vc->size = newsize;
return VECTOR_OK;
}
避免致命錯誤
在動態內存申請上,避免錯誤的通用方法,小心翼翼的寫代碼,盡可能的做好異常保護。但是有很多常見的問題上,有一些方法可以避免他們。
1 )重復調用free導致崩潰
這個問題由free函數的參數為下列情況引起:空指針,或者指針沒有用malloc等函數分配的(野指針),或者已經被free/recalloc釋放了(野指針)。為了避免這個問題,可以采取下列方法:
1 如果不能立即給指針賦有效的值,那麼在聲明的時候,初始化指針為NULL,
2 gcc 和clang 都會對未初始化的變量進行警告。
3 不要用同一個指針去指向靜態內存和動態內存。
4 在使用free之後,將指針設置為NULL, 這樣如果你不小心又調用free,也不會出錯。
5 為了避免兩次釋放,在測試和調試的時候,使用assert 或許類似的函數。
char *ptr = NULL;
/* ... */
void nullfree(void **pptr) {
void *ptr = *pptr;
assert(ptr != NULL)
free(ptr);
*pptr = NULL;
}
2 )通過空指針或者未初始化的指針訪問內存。
使用上述規則,你的代碼只需要處理空指針或有效的指針。只需要在函數或者代碼段開始的時候,檢測動態內存的指針是否為空。
3 )訪問越界的內存
訪問越界的內存並不一定都會導致程序崩潰。程序可能繼續操作使用錯誤的數據,產生危險的後果,或者程序可能利用這些操作,進入其他的分支,或者進入執行代碼。逐步的人工檢測數組邊界和動態內存邊界,是主要避免這些危險的主要方法。內存邊界的信息可以人工跟蹤。數組的大小可以用sizeof函數,但是有時候array也會被轉換為指針,(例如在函數中,sizeof 會返回指針的大小,而不是數組。)c11標准中的接口Annex k 是邊界檢測的接口,定義了一系列新庫函數,提供了一些簡單安全的方法去代替標准庫(例如string 和I/O操作) 還有一些開源的方法例如 slibc,但是他的接口沒有廣泛采用。基於BSD系統(包括Mac OS X)提供了strlcpy,strlcat函數,可以更好的進行字符串操作。對於其他系統可以使用libbsd libraray.很多操作系統提供了接口,控制獲取內存區域,保護內存讀寫例如posix mporst,這些機制主要用於整個內存頁的控制。
避免內存洩露
內存洩露是由於有些動態內存不在使用了,但是程序沒有釋放而導致的。所以真正理解分配的內存空間作用域,最重要的是 free函數什麼時候調用。但是隨著程序復雜性的增強,這個就會變得越來越困難,所以在開始的設計中需要加入內存管理的功能。下面是一些方法去解決這些問題:
1)啟動的時候申請
將所有需要的堆內存分配防止程序啟動的時候可以讓內存管理變得簡單。在程序結束的時候由操作系統釋放(這裡的意思是程序結束調用free麼?還是程序關閉後系統自己free)。在很多情況下,這個方法是令人滿意的,特別是程序批處理輸入,然後完成。
2)可變長度的數組
如果你需要一個可變大小的臨時存儲空間,生命周期只在一個函數中,那麼可以考慮使用VLA(可變長度數組)。但是使用它是受限制的,每個函數使用它的空間不能超過百個字節。因為可變長度數組在C99中定義的(C11優化)有自動存儲區域,它和其他的自動變量一樣有一定的范圍。盡管標准沒有明確指出,通常會將VLA放在棧空間中。 VLA的最大可以分配的內存空間大小為 SIZE_MAX字節。先要知道目標平台的棧空間大小,我們要謹慎使用,確保不出現棧溢出,或者讀取內存段下面的錯誤數據。
3)人工引用計數
這個技術的背後思想是記錄每次分配和失去引用的數目。在每次分配引用的時候計數增加,每次失去引用的時候分配減少。當引用的數目為0的時候,表示內存空間不再使用了,然後進行釋放。但是C語言不支持自動析構(實際上,GCC和Clang都支持cleanup擴展)但並不意味著要重寫分配操作符,通過人工的調用retain/release來完成計數。 函數。換一個思路,程序中有多個地方會占用或者解除和一塊內存空間的關系。即便是使用這個方法,要遵守很多准則來確保不會忘記調用release(導致內存洩露)或過多的調用(提前釋放)。但是如果一個內存空間的生命期,是由由外部事件確定,並且程序的結構決定,它會用各種方法來處理內存空間,那麼使用這種麻煩的方法也是很值得的。下面代碼塊是一個簡單的引用計數去進行內存管理。
#include
#include
#define MAX_REF_OBJ 100
#define RC_ERROR -1
struct mem_obj_t{
void *ptr;
uint16_t count;
};
static struct mem_obj_t references[MAX_REF_OBJ];
static uint16_t reference_count = 0;
/* create memory object and return handle */
uint16_t create(size_t size){
if (reference_count >= MAX_REF_OBJ)
return RC_ERROR;
if (size){
void *ptr = calloc(1, size);
if (ptr != NULL){
references[reference_count].ptr = ptr;
references[reference_count].count = 0;
return reference_count++;
}
}
return RC_ERROR;
}
/* get memory object and increment reference counter */
void* retain(uint16_t handle){
if(handle < reference_count && handle >= 0){
references[handle].count++;
return references[handle].ptr;
} else {
return NULL;
}
}
/* decrement reference counter */
void release(uint16_t handle){
printf(release
);
if(handle < reference_count && handle >= 0){
struct mem_obj_t *object = &references[handle];
if (object->count <= 1){
printf(released
);
free(object->ptr);
reference_count--;
} else {
printf(decremented
);
object->count--;
}
}
}
如果你不考慮各個編譯器的兼容性,你可以使用cleanup attribute在c語言中模仿自動析構。
(參考http://blog.csdn.net/haozhao_blog/article/details/14093155
http://gcc.gnu.org/onlinedocs/gcc/Variable-Attributes.html)
void cleanup_release(void** pmem) {
int i;
for(i = 0; i < reference_count; i++) {
if(references[i].ptr == *pmem)
release(i);
}
}
void usage() {
int16_t ref = create(64);
void *mem = retain(ref);
__attribute__((cleanup(cleanup_release), mem));
/* ... */
}
cleanup_release的另一個缺點是根據對象的地址去釋放,而不是根據引用的個數。因此cleanup_release 在引用數組的查找上耗費巨大。一種補救的方法是是修改retain的接口,返回指向結構體mem_obj_t的指針。另外一種方法是用下面的宏,它創建變量去保存引用的數目,並且和cleanup attribute相關聯。
/
* helper macros */
#define __COMB(X,Y) X##Y
#define COMB(X,Y) __COMB(X,Y)
#define __CLEANUP_RELEASE __attribute__((cleanup(cleanup_release)))
#define retain_auto(REF) retain(REF); int16_t __CLEANUP_RELEASE COMB(__ref,__LINE__) = REF
void cleanup_release(int16_t* phd) {
release(*phd);
}
void usage() {
int16_t ref = create(64);
void *mem = retain_auto(ref);
/* ... */
}
4 內存池
如果一個程序運行的時候會經過很多步驟,在每一個步驟開始的時候可能有內存池。任何時候程序需要分配內存的時候,其中的一個內存池就會被使用。根據分配內存的生命周期去選擇內存池,並且內存池屬於程序的某個階段。在每一個階段結束,內存池被立刻釋放。這個方法在長期運行的程序十分受歡迎,例如守護進程,它可以在整體上降低內存的碎片化。下面是內存池管理的一個簡單例子。
#include
#include
struct pool_t{
void *ptr;
size_t size;
size_t used;
};
/* create memory pool*/
struct pool_t* create_pool(size_t size) {
struct pool_t* pool = calloc(1, sizeof(struct pool_t));
if(pool == NULL)
return NULL;
if (size) {
void *mem = calloc(1, size);
if (mem != NULL) {
pool->ptr = mem;
pool->size = size;
pool->used = 0;
return pool;
}
}
return NULL;
}
/* allocate memory from memory pool */
void* pool_alloc(struct pool_t* pool, size_t size) {
if(pool == NULL)
return NULL;
size_t avail_size = pool->size - pool->used;
if (size && size <= avail_size){
void *mem = pool->ptr + pool->used;
pool->used += size;
return mem;
}
return NULL;
}
/* release memory for whole pool */
void delete_pool(struct pool_t* pool) {
if (pool != NULL) {
free(pool->ptr);
free(pool);
}
}
實現一個內存池,是一個比較困難的事情。或許一些存在的庫可以滿足你的需求。
GNU libc obstack
Samba talloc
Ravenbrook Memory Pool System
5) 數據結構
很多內存管理的問題可以歸結為使用正確的數據結構去存儲數據。選擇哪種數據結構主要是由訪問數據,保存數據的算法需求來決定的,類似於使用鏈式表,哈希表,樹等能帶來額外的增益,例如遍歷數據結構和快速釋放數據。盡管在標准庫中沒有支持數據結構,但是下面有一些有用的庫。
For traditional Unix implementation of linked lists and trees see BSD's queue.h and tree.h macros both are part of libbsd.
GNU libavl
Glib Data Types
For additional list see http://adtinfo.org/index.html
6 )標記和清理垃圾收集器
另一種方法是使用自動垃圾回收機制,從而減少人工的釋放內存。指針引用是內存不使用的時候就想釋放,而垃圾機制相反,是由特定事件觸發的,例如內存分配失敗,或者分配達到某個水平線。標記和掃除算法是實現垃圾機制的一種方法。一開始,它會遍歷堆空間中所有的以前分配的內存引用,標記哪些還可以達到的引用,清理哪些沒有被標記的引用。
或許,在c語言中最出名的垃圾收集機制為Boehm-Demers-Weiser conservative garbage collector。垃圾機制的缺點是性能開銷和導致程序有不確定的停頓。另一個問題是有malloc引起的,它不能被垃圾回收機制管理,需要人工管理。
另外無法預知的停頓在實時系統中是不能接受的,但是很多環境上還是優點大於缺點。在性能的一方面,他們甚至宣稱為高性能的。Mono project GNU Objective C runtime和Irssi IRC client都使用了Boehm GC。