很久沒有碰C++,下個項目要開始使用C++,所以很多東西需要撿起來重新看看。從今天起記錄一些筆記,方便自己今后查閱。言歸正傳,先從構造函數的初始化列表說起。我把這個知識點細化成3個問題,1.為什么要有初始化列表,它和構造函數中初始化有什么不一樣。2.初始化的順序。3.一些注意的細節。
先說第一個問題。我們有兩個東西,是必須在初始化列表里完成的,一個是const修飾的變量,一個是引用。這點我就不細說了,查閱資料的都可以找到。下面我具體說說類成員。
class Test1{
public:
Test1()
{
cout << "Test1 default constructor" << endl;
}
Test1(int i)
{
cout << "Test1 value constructor" << endl;
}
Test1(const Test1 &obj)
{
cout << "Test1 copy constructor" << endl;
}
Test1& operator = (const Test1 &obj)
{
cout << "Test1 = constructor" << endl;
return *this;
}
~Test1()
{
cout << "Test1 destructor" << endl;
}
};
我定義一個類,然后再定義一個類,其中包含Test1
class Test2
{
public:
Test2()
{
t1 = Test1(1);
}
private:
Test1 t1;
};
我們在構造函數中初始化Test1,我們看看運行結果
Test1 default constructor
Test1 value constructor
Test1 = constructor
Test1 destructor
Test1 destructor我們分析下這個輸出。Test1 default constructor,這說明在進入Test1構造函數之前,已經初始化了t1成員,并且調用的是無參構造函數。Test1 value constructor這個是Test1(1)創建出來的對象。Test1 = constructor,這個表示條用了拷貝構造函數,Test1 destructor這個表示Test1(1)這個臨時對象的析構,Test1 destructor這個表示是t1這個成員對象的析構。從上面的結果來看,構造函數中t1 = Test1(1);其實并不是真正意義是上的初始化,而是一次拷貝賦值。當進入構造函數內部之前,類成員會被默認構造函數初始化。如果說Test1是個很大的對象,這塊會造成性能上的開銷。所以,這點也是使用初始化列表的原因之一。
第二我們再來說下順序問題。簡單的原則是初始化列表里的會先于構造函數中,初始化列表里會按照變量聲明的順序。我們具體看看下面的例子。
class Test3(){
public:
Test3(int x,int y,int z):_z(z),_y(y)
{
_x = x;
}
private:
int _x,_y,_z;
};
按照上面的說法,賦值的順序是_y,_z,_x。
第三個是注意問題,每個成員只能在初始化列表里出現一次。
class Test3{
public:
Test3(int x,int y,int z):_z(z),_y(y),_z(x)
{
_x = x;
}
private:
int _x,_y,_z;
};
比如這種就是問題的。_z被初始化了2次。
posted @
2012-08-14 10:24 梨樹陽光 閱讀(1464) |
評論 (3) |
編輯 收藏
寫服務器的,通常會涉及到內存池的東西,自己在這方面也看了寫了一些東西,有些體會,寫出來跟大家分享下。
內存池基本包含以下幾個東西,第一,初始化。第二,分配內存。第三,回收內存。所謂初始化,就是在服務器啟動的時候,或者第一次需要內存的時候,系統分配很大的一塊內存,方便之后的使用。分配內存,就是從內存池中取出需要的內存給外部使用,當然這里需要考慮的是當內存池中沒有內存可分配時候的處理。回收內存,簡單來說,就是外面對象生命期結束了,將分配出去的內存回收入內存池中。好了簡單概念就說完了,我們先來看一種最簡單的設計方式。
//為了方便描述,這里附上幾個簡單的鏈表操作宏
#define INSERT_TO_LIST( head, item, prev, next ) \
do{ \
if ( head ) \
(head)->prev = (item); \
(item)->next = (head); \
(head) = (item); \
}while(0)
#define REMOVE_FROM_LIST(head, item, prev, next) \
do{ \
if ( (head) == (item) ) \
{ \
(head) = (item)->next; \
if ( head ) \
(head)->prev = NULL; \
} \
else \
{ \
if ( (item)->prev ) \
(item)->prev->next = (item)->next; \
\
if ( (item)->next ) \
(item)->next->prev = (item)->prev; \
} \
}while(0)
struct student
{
char name[32];
byte sex;
struct student *prev,*next;
};
static struct mem_pool
{
//該指針用來記錄空閑節點
struct student *free;
//該變量記錄分配結點個數
size_t alloc_cnt;
}s_mem_pool;
//分配內存“塊”的函數
bool mem_pool_resize(size_t size)
{
//該函數創建size個不連續的對象,把他們通過鏈表的方式加入到s_mem_pool.free中
for ( size_t i = 0;i < size;++i )
{
struct student *p = (struct student *)malloc(sizeof(struct student));
if ( !p )
return false;
p->prev = p->next = NULL;
INSERT_TO_LIST(s_mem_pool.free,p,prev,next);
}
s_mem_pool.alloc_cnt += size;
}
#define MEM_INIT_SIZE 512
#define MEM_INC_SIZE 256
//初始化函數
bool mem_pool_init()
{
if ( !mem_pool_resize(MEM_INIT_SIZE) )
return false;
return true;
}
struct student *get_data()
{
if ( s_mem_pool.free == NULL )
{
if ( !mem_pool_resize(MEM_INC_SIZE) )
return NULL;
}
struct student *ret = s_mem_pool.free;
REMOVE_FROM_LIST(s_mem_pool.free,ret,prev,next)
return ret;
}
void free_data(struct student *p)
{
if ( !p )
return;
memset(p,0,sizeof(struct student));
INSERT_TO_LIST(s_mem_pool.free,p,prev,next)
}
好了最簡單的內存池的大致框架就是這樣。我們先來看下他的過程。首先,在mem_pool_init()函數中,他先分配512個不連續的student對象。每分配出來一個就把它加入到free鏈表中,初始化完成后內存池大概是這樣的

接下來就是從內存池中取出一個對象get_data()。函數先去判斷是否有空閑的對象,有則直接分配,否則再向系統獲取一"塊"大的內存。調用一次后的內存池大概是這樣的

釋放對象,再把對象加入到Free鏈表中。
以上就是過程的簡單分析,下面我們來看看他的缺點。
第一,內存不是連續的,容易產生碎片
第二,一個類型就得寫一個這樣的內存池,很麻煩
第三,為了構建這個內存池,每個沒對象必須加上一個prev,next指針
好了,我們來優化一下它。我們重新定義下我們的結構體
union student
{
int index;
struct
{
char name[32];
byte sex;
}s;
};
static struct mem_pool
{
//該下標用來記錄空閑節點
int free;
//內存池
union student *mem;
//已分配結點個數
size_t alloc_cnt;
}s_mem_pool;
//分配內存塊的函數
bool mem_pool_resize(size_t size)
{
size_t new_size = s_mem_pool.alloc_cnt+size;
union student *tmp = (union student *)realloc(s_mem_pool.mem,new_size*sizeof(union student));
if ( !tmp )
return false;
memset(tmp+s_mem_pool.alloc_cnt,0,size*sizeof(union student));
size_t i = s_mem_pool.alloc_cnt;
for ( ;i < new_size - 1;++i )
{
tmp[i].index = i + 1;
}
tmp[i].index = -1;
s_mem_pool.free = s_mem_pool.alloc_cnt;
s_mem_pool.mem = tmp;
s_mem_pool.alloc_cnt = new_size;
return true;
}
#define MEM_INIT_SIZE 512
#define MEM_INC_SIZE 256
//初始化函數
bool mem_pool_init()
{
if ( !mem_pool_resize(MEM_INIT_SIZE) )
return false;
return true;
}
union student *get_data()
{
if ( s_mem_pool.free == -1 )
{
if ( !mem_pool_resize(MEM_INC_SIZE) )
return NULL;
}
union student *ret = s_mem_pool.mem+s_mem_pool.free;
s_mem_pool.free = ret->index;
return ret;
}
void free_data(union student *p)
{
if ( !p )
return;
p->index = s_mem_pool.free;
s_mem_pool.free = p - s_mem_pool.mem;
}
我們來看看改進了些什么。第一student改成了聯合體,這主要是為了不占用額外的內存,也就是我們上面所說的第三個缺點,第二,我們使用了realloc函數,這樣我們可以使我們分配出來的內存是連續的。我們初始化的時候多了一個for循環,這是為了記錄空閑對象的下標,當我們取出一個對象時,free可以立刻知道下一個空閑對象的位置,釋放的時候,對象先記錄free此時的值,接著再把free賦值成該對象在數組的下標,這樣就完成了回收工作。
我們繼續分析這段代碼,問題在realloc函數上,如果我們的s_mem_pool.mem已經很大了,在realloc的時候我們都知道,先要把原來的數據做一次拷貝,所以如果數據量很大的情況下做一次拷貝,是會消耗性能的。那這里有沒有好的辦法呢,我們進一步優化
思路大概是這樣
初始化

再次分配的時候,我們只需要重新分配新的內存單元,而不需要拷貝之前的內存單元。

因此基于此思路,我們修改我們的代碼
#include <stdio.h>
#include <stdlib.h>
struct student
{
int index;
char name[32];
byte sex;
};
static struct mem_pool
{
//該下標用來記錄空閑節點
int free;
//內存池
struct student **mem;
//已分配塊個數
size_t block_cnt;
}s_mem_pool;
#define BLOCK_SIZE 256 //每塊的大小
//分配內存塊的函數
bool mem_pool_resize(size_t block_size)
{
size_t new_cnt = s_mem_pool.block_cnt + block_size;
struct student **tmp = (struct student **)realloc(s_mem_pool.mem,new_size*sizeof(struct student *));
if ( !tmp )
return false;
memset(tmp+s_mem_pool.block_cnt,0,size*sizeof(struct student*));
for ( size_t i = s_mem_pool.block_cnt;i < new_cnt;++i )
{
tmp[i] = (struct student *)calloc(BLOCK_SIZE,sizeof(struct student));
if ( !tmp[i] )
return false;
size_t j = 0;
for(;j < BLOCK_SIZE - 1;++j )
{
tmp[i][j].index = i*BLOCK_SIZE+j+1;
}
if ( i != new_cnt-1 )
tmp[i][j].index = (i+1)*BLOCK_SIZE;
else
tmp[i][j].index = -1;
}
s_mem_pool.free = s_mem_pool.alloc_cnt*BLOCK_SIZE;
s_mem_pool.mem = tmp;
s_mem_pool.block_cnt = new_cnt;
return true;
}
#define MEM_INC_SIZE 10
//初始化函數
bool mem_pool_init()
{
if ( !mem_pool_resize(MEM_INIT_SIZE) )
return false;
return true;
}
struct student *get_data()
{
if ( s_mem_pool.free == -1 )
{
if ( !mem_pool_resize(MEM_INC_SIZE) )
return NULL;
}
struct student *ret = s_mem_pool.mem[s_mem_pool.free/BLOCK_SIZE]+s_mem_pool.free%BLOCK_SIZE;
int pos = s_mem_pool.free;
s_mem_pool.free = ret->index;
ret->index = pos;
return ret;
}
void free_data(struct student *p)
{
if ( !p )
return;
int pos = p->index;
p->index = s_mem_pool.free;
s_mem_pool.free = pos;
}
這里不一樣的地方主要在mem_pool_resize函數中,mem變成了2級指針,每次realloc的時候只需要分配指針數組的大小,無須拷貝對象,這樣可以提高效率,但是為了在釋放的時候把對象放回該放的位置,我們這里在結構體里加入了index變量,記錄它的下標。在內存池里,它表示下個空閑對象的下標,在內存池外,它表示在內存池中的下標。總的來說滿足了一個需求,卻又帶來了新的問題,有沒有更好的方法呢,答案是肯定,不過今天先寫到這里,明天繼續。
posted @
2012-07-19 11:41 梨樹陽光 閱讀(3518) |
評論 (2) |
編輯 收藏
昨天在看一篇文章的時候,突然想起了這個基礎性的問題,自己一直對它的區別不是很清楚,于是今天上午研究下了,分享下自己的理解。(對它很清楚的同學們可以略過此篇文章)
從存儲方式來說,文件在磁盤上的存儲方式都是二進制形式,所以,文本文件其實也應該算二進制文件。那么他們的區別呢,各自的優缺點呢?不急,我慢慢道來。
先從他們的區別來說,雖然都是二進制文件,但是二進制代表的意思不一樣。打個比方,一個人,我們可以叫他的大名,可以叫他的小名,但其實都是代表這個人。二進制讀寫是將內存里面的數據直接讀寫入文本中,而文本呢,則是將數據先轉換成了字符串,再寫入到文本中。下面我用個例子來說明。
我們定義了一個結構體,表示一個學生信息,我們打算把學生的信息分別用二進制和文本的方式寫入到文件中。
struct Student
{
int num;
char name[20];
float score;
};
我們定義兩個方法,分別表示內存寫入和文本寫入
//使用二進制寫入
void write_to_binary_file()
{
struct Student stdu;
stdu.num = 111;
sprintf_s(stdu.name,20,"%s","shine");
stdu.score = 80.0f;
fstream binary_file("test1.dat",ios::out|ios::binary|ios::app); //此處省略文件是否打開失敗的判斷
binary_file.write((char *)&stdu,sizeof(struct Student));//二進制寫入的方式
binary_file.close();
}
//文本格式寫入
void write_to_text_file()
{
struct Student stdu;
stdu.num = 111;
sprintf_s(stdu.name,20,"%s","shine");
stdu.score = 80.0f;
FILE *fp = fopen("test2.dat","a+"); //此處省略文件是否打開失敗的判斷
fprintf(fp,"%d%s%f",stdu.num,stdu.name,stdu.score); //將數據轉換成字符串(字符串的格式可以自己定義)
fclose(fp);
}
//MAIN函數調用前面兩個方法
int _tmain(int argc, _TCHAR* argv[])
{
write_to_binary_file();
write_to_text_file();
return 0;
}
我們來看下,文件里面的格式
2進制文件
文本文件
2進制文件里面將111編碼成6F,1個字節,這剛好是111的16進制表示,而文本文件中則寫成31,31,31用了3個字節,表示111。73 68 69 6E 65 表示shine,之后2進制文件里是幾個連續的FE,而文本文件中是38 30......文本文件將浮點數80.000000用了38(表示8) 30(表示0) 2E(表示.) 30(表示0) 30(表示0) 30(表示0) 30(表示0) 30(表示0) 30(表示0),二進制文件用了4個字節表示浮點數00 00 A0 42
通過這里我們可以初見端倪了,二進制將數據在內存中的樣子原封不動的搬到文件中,文本格式則是將每一個數據轉換成字符寫入到文件中,他們在大小上,布局上都有著區別。由此可以看出,2進制文件可以從讀出來直接用,但是文本文件還多一個“翻譯”的過程,因此2進制文件的可移植性好。
posted @
2012-07-12 09:59 梨樹陽光 閱讀(13023) |
評論 (5) |
編輯 收藏
先看一個簡單的使用例子
求任意個自然數的平方和:
int SqSum(int n,)
{
va_list arg_ptr;
int sum = 0,_n = n;
arg_ptr = va_start(arg_ptr,n);
while(_n != 0)
{
sum += (_n*_n);
_n = va_arg(arg_ptr,int);
}
va_end(arg_ptr);
return sum;
}
首先解釋下函數參數入棧情況
在VC等絕大多數C編譯器中,默認情況下,參數進棧的順序是由右向左的,因此,參數進棧以后的內存模型如下圖所示:
最后一個固定參數的地址位于第一個可變參數之下,并且是連續存儲的。
| 最后一個可變參數(高內存地址處) | 第N個可變參數 | 第一個可變參數 | 最后一個固定參數 | 第一個固定參數(低內存地址處)
明白上面那個順序,就知道其實可變參數就是玩弄參數的地址,已達到“不定”的目的
下面我摘自VC中的源碼來解釋
va_list,va_start,va_arg,va_end宏
1.其實va_list就是我們平時經常用的char*
typedef char * va_list;
2.va_start該宏的目的就是將指針指向最后一個固定參數的后面,即第一個不定參數的起始地址
#define va_start(ap,v)( ap = (va_list)&v + _INTSIZEOF(v) )
v即表示最后一個固定參數,&v表示v的地址,
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
該宏其實是一個內存對齊的操作。即表示大于sizeof(n)且為sizeof(int)倍數的最小整數。這句話有點繞,其實舉幾個例子就簡單了。比如1--4,則返回4,5--8則返回8
3.va_arg 該宏的目的是將ap指針繼續后移,讀取后面的參數,t表示參數類型。該宏首先將ap指針移動到下一個參數的起始地址ap += _INTSIZEOF(t),然后將本參數的值返回
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
4.va_end將指針賦空
#define va_end(ap) ap = (va_list)0
有了這個分析我們可以把上例中的代碼重新翻譯下
int SqSum(int n,)
{
char *arg_ptr;
int sum = 0,_n = n;
arg_ptr = (char *)&n + 4;//本機上sizeof(int) = 4
while(_n != 0)
{
sum += (_n*_n);
arg_ptr += 4;
_n = *(int *)(arg_ptr-4);
}
arg_ptr = (void*)0;
}
這樣我們也可以寫出我們自己的printf了
posted @
2012-07-12 09:51 梨樹陽光 閱讀(1054) |
評論 (1) |
編輯 收藏
1.制作自己的動態庫和靜態庫
linux下動態庫以.so結尾,靜態庫以.a結尾,它們都以lib開頭,比如一個庫名為net,那么它的全名應該是libnet.so或者libnet.a。
我們有兩個文件,hello.c和test.c,下面是兩個文件的內容
//hello.c
#include <stdio.h>
void my_lib_func()
{
printf("Library routine called\r\n");
}
//test.c
#include <stdio.h>
int main()
{
my_lib_func();
return 1;
}
test.c調用了hello.c的方法,我們把hello.c封裝成庫文件。無論是靜態庫還是動態庫,都是由.o文件組成,我們先把gcc -c hello.c生成.o文件
制作靜態庫
ar crv libmyhello.a hello.o,ar是生成靜態庫的命令,libmyhello.a是我的靜態庫名。下一步就是在我的程序中使用靜態庫
可以看到已經有了Library routine called的結果,說明調用成功了。
下面我們刪除libmyhello.a,看看程序是否還是運行正常

我們發現程序依然運行正常,說明靜態庫已經連接進入我們的程序中
制作動態庫

我們看見動態庫libmyhello.so已經生成,下面繼續使用

找不到庫文件,這個時候我們把so文件拷貝到/usr/lib下面

運行成功
2.動態庫和靜態庫同時存在的調用規則
我們可以發現,不論是動態庫還是靜態庫,程序編譯連接的時候都是加的參數-l,那么當他們同時存在的時候,程序會選擇動態庫還是靜態庫呢。我們做個嘗試。

我們同時存在libmyhello.a和libmyhello.so,我們發現運行的時候,出現找不到動態庫的錯誤,由此,我們可以得出結論,同時存在動態庫和靜態庫的時候,gcc會優先選擇動態庫
posted @
2012-07-11 15:15 梨樹陽光 閱讀(1493) |
評論 (0) |
編輯 收藏