2013年2月19日
#
C語言中對于下面的兩種情況,是否相同呢? char a[] = "abcdefg";---------------1 char *p = "abcdefg";-----------------2 在談到這些區(qū)別時,應該先談一下計算機中對變量是如何存儲的。從編譯原理中我們知道,對于所有的變量他都會影射到一個符號表中。為了簡化,這里給出一種最簡單的便于理解的符號表: 表1 一個簡單的符號表示例 以上表格中a代表一個變量,0xffaa則為變量a的內容的存儲地址;p代表另一個變量,0xffcc為變量p的內容的存儲地址。對于數組型的變量和指針型的變量,其地址代表的含義不同。 對于數組a: 這個0xffaa地址就是其存放數組內容的首地址了。對于a[i]的引用步驟如下: 步驟一、取出i的值,將他與0xffaa相加; 步驟二、取出為(0xffaa+i)中的內容。 對于指針p: 這個0xffcc地址就是中存放的不是字符串的內容,而是一個地址,這個地址才是字符串的首地址,對p[i]或者用指針表示*(p+i)的應用步驟如下: 步驟一、取出0xffcc地址中的內容,例如為0xffdf; 步驟二、取出地址0xffdf中的內容。 數組和指針的對比如下圖:
下面是在VC6.0下作的一個試驗,通過這個試驗大家可以看到,雖然同過[]和通過*引用都一樣,但在內部處理的方法是不一樣的。 #include "stdafx.h" #include "stdio.h" int main(int argc, char* argv[]) { int a[3]={1,2,3}; int *p =a; printf("a:%d,&a:%d,a[0]:%d,*a:%d,p:%d,&p:%d,*p:%d,p[0]:%d",a,&a, a[0],*a,p,&p,*p,p[0]); return 0; } 輸出結果: a:1310580,&a:1310580,a[0]:1,*a:1,p:1310580,&p:1310576,*p:1,p[0]:1。 由上面的分析可知,如果在一個文件中定義了一個數組int maychar[100],那么下面的聲明就是完全錯誤的。 extern int *maychar; 這樣的話,在引用時他就會按照指針的方法來引用數組。正確的聲明應該是exter int maychar[];這里數組的大小并不重要。下面將指針與數組的區(qū)別用表格的形式列出如下: 指針 | 數組 | 保存數據的地址 | 保存數據 | 間接訪問數據 | 直接訪問 | 通常用于動態(tài)數據結構 | 通常用于存儲固定數目數據類型相同的元素 | 相關操作malloc(),free()等 | 隱式分配和刪除 | 同常指向匿名數據 | 自身即為數據名 |
表2 指針與數組的區(qū)別 還要提醒一點的就是: char a[] = "abcdefg";---------------數組內容能修改(字符數組) char *p = "abcdefg";-----------------內容不能修改(字符串常量) 在ANSI C中,初始化指針是所創(chuàng)建的字符串時常量,被定義為只讀,如果試圖通過指針修改這個字符串的值,程序就會出現為定義的行為。
2012年1月20日
#
可以說2012是轉折的一年。
我也開始思考自己。 公司在轉型,我也在轉變。 2012年的目標: 一.控制自己。 做自己對自己持續(xù)受益的事情。 二.不斷地學習。學習前人的經驗,避免前人的錯誤。 看書(學習技能,學習歷史),觀察周圍的人,思考。
三.認識自己。努力去做好自己。建設自己的原則。(每天思考) 四.更加激情地去熱愛生活,去融入到生活,工作中。真誠對待每一個人。
2011年6月17日
#
排序算法是一種基本并且常用的算法。由于實際工作中處理的數量巨大,所以排序算法對算法本身的速度要求很高。
而一般我們所謂的算法的性能主要是指算法的復雜度,一般用O方法來表示。在后面我將給出詳細的說明。
對于排序的算法我想先做一點簡單的介紹,也是給這篇文章理一個提綱。
我將按照算法的復雜度,從簡單到難來分析算法。
第一部分是簡單排序算法,后面你將看到他們的共同點是算法復雜度為O(N*N)(因為沒有使用word,所以無法打出上標和下標)。
第二部分是高級排序算法,復雜度為O(Log2(N))。這里我們只介紹一種算法。另外還有幾種算法因為涉及樹與堆的概念,所以這里不于討論。
第三部分類似動腦筋。這里的兩種算法并不是最好的(甚至有最慢的),但是算法本身比較奇特,值得參考(編程的角度)。同時也可以讓我們從另外的角度來認識這個問題。
第四部分是我送給大家的一個餐后的甜點——一個基于模板的通用快速排序。由于是模板函數可以對任何數據類型排序(抱歉,里面使用了一些論壇專家的呢稱)。
現在,讓我們開始吧: 一、簡單排序算法 由于程序比較簡單,所以沒有加什么注釋。所有的程序都給出了完整的運行代碼,并在我的VC環(huán)境下運行通過。因為沒有涉及MFC和WINDOWS的內容,所以在BORLAND C++的平臺上應該也不會有什么問題的。在代碼的后面給出了運行過程示意,希望對理解有幫助。 1.冒泡法: 這是最原始,也是眾所周知的最慢的算法了。他的名字的由來因為它的工作看來象是冒泡: #include <iostream.h> void BubbleSort(int* pData,int Count) { int iTemp; for(int i=1;i<Count;i++) { for(int j=Count-1;j>=i;j--) { if(pData[j]<pData[j-1]) { iTemp = pData[j-1]; pData[j-1] = pData[j]; pData[j] = iTemp; } } } } void main() { int data[] = {10,9,8,7,6,5,4}; BubbleSort(data,7); for (int i=0;i<7;i++) cout<<data[i]<<" "; cout<<"\n"; } 倒序(最糟情況) 第一輪:10,9,8,7->10,9,7,8->10,7,9,8->7,10,9,8(交換3次) 第二輪:7,10,9,8->7,10,8,9->7,8,10,9(交換2次) 第一輪:7,8,10,9->7,8,9,10(交換1次) 循環(huán)次數:6次 交換次數:6次 其他: 第一輪:8,10,7,9->8,10,7,9->8,7,10,9->7,8,10,9(交換2次) 第二輪:7,8,10,9->7,8,10,9->7,8,10,9(交換0次) 第一輪:7,8,10,9->7,8,9,10(交換1次) 循環(huán)次數:6次 交換次數:3次 上面我們給出了程序段,現在我們分析它:這里,影響我們算法性能的主要部分是循環(huán)和交換, 顯然,次數越多,性能就越差。從上面的程序我們可以看出循環(huán)的次數是固定的,為1+2+...+n-1。 寫成公式就是1/2*(n-1)*n。 現在注意,我們給出O方法的定義: 若存在一常量K和起點n0,使當n>=n0時,有f(n)<=K*g(n),則f(n) = O(g(n))。(呵呵,不要說沒學好數學呀,對于編程數學是非常重要的!!!) 現在我們來看1/2*(n-1)*n,當K=1/2,n0=1,g(n)=n*n時,1/2*(n-1)*n<=1/2*n*n=K*g(n)。所以(n)=O(g(n))=O(n*n)。所以我們程序循環(huán)的復雜度為O(n*n)。 再看交換。從程序后面所跟的表可以看到,兩種情況的循環(huán)相同,交換不同。其實交換本身同數據源的有序程度有極大的關系,當數據處于倒序的情況時, 交換次數同循環(huán)一樣(每次循環(huán)判斷都會交換),復雜度為O(n*n)。當數據為正序,將不會有交換。復雜度為O(0)。亂序時處于中間狀態(tài)。正是由于這樣 的原因,我們通常都是通過循環(huán)次數來對比算法。 2.交換法: 交換法的程序最清晰簡單,每次用當前的元素一一的同其后的元素比較并交換。 #include <iostream.h> void ExchangeSort(int* pData,int Count) { int iTemp; for(int i=0;i<Count-1;i++) { for(int j=i+1;j<Count;j++) { if(pData[j]<pData[i]) { iTemp = pData[i]; pData[i] = pData[j]; pData[j] = iTemp; } } } } void main() { int data[] = {10,9,8,7,6,5,4}; ExchangeSort(data,7); for (int i=0;i<7;i++) cout<<data[i]<<" "; cout<<"\n"; } 倒序(最糟情況) 第一輪:10,9,8,7->9,10,8,7->8,10,9,7->7,10,9,8(交換3次) 第二輪:7,10,9,8->7,9,10,8->7,8,10,9(交換2次) 第一輪:7,8,10,9->7,8,9,10(交換1次) 循環(huán)次數:6次 交換次數:6次 其他: 第一輪:8,10,7,9->8,10,7,9->7,10,8,9->7,10,8,9(交換1次) 第二輪:7,10,8,9->7,8,10,9->7,8,10,9(交換1次) 第一輪:7,8,10,9->7,8,9,10(交換1次) 循環(huán)次數:6次 交換次數:3次 從運行的表格來看,交換幾乎和冒泡一樣糟。事實確實如此。循環(huán)次數和冒泡一樣也是1/2*(n-1)*n,所以算法的復雜度仍然是O(n*n)。由于我們無法給出所有的情況,所以只能直接告訴大家他們在交換上面也是一樣的糟糕(在某些情況下稍好,在某些情況下稍差)。 3.選擇法: 現在我們終于可以看到一點希望:選擇法,這種方法提高了一點性能(某些情況下) 這種方法類似我們人為的排序習慣:從數據中選擇最小的同第一個值交換,在從省下的部分中選擇最小的與第二個交換,這樣往復下去。 #include <iostream.h> void SelectSort(int* pData,int Count) { int iTemp; int iPos; for(int i=0;i<Count-1;i++) { iTemp = pData[i]; iPos = i; for(int j=i+1;j<Count;j++) { if(pData[j]<iTemp) { iTemp = pData[j]; iPos = j; } } pData[iPos] = pData[i]; pData[i] = iTemp; } } void main() { int data[] = {10,9,8,7,6,5,4}; SelectSort(data,7); for (int i=0;i<7;i++) cout<<data[i]<<" "; cout<<"\n"; } 倒序(最糟情況) 第一輪:10,9,8,7->(iTemp=9)10,9,8,7->(iTemp=8)10,9,8,7->(iTemp=7)7,9,8,10(交換1次) 第二輪:7,9,8,10->7,9,8,10(iTemp=8)->(iTemp=8)7,8,9,10(交換1次) 第一輪:7,8,9,10->(iTemp=9)7,8,9,10(交換0次) 循環(huán)次數:6次 交換次數:2次 其他: 第一輪:8,10,7,9->(iTemp=8)8,10,7,9->(iTemp=7)8,10,7,9->(iTemp=7)7,10,8,9(交換1次) 第二輪:7,10,8,9->(iTemp=8)7,10,8,9->(iTemp=8)7,8,10,9(交換1次) 第一輪:7,8,10,9->(iTemp=9)7,8,9,10(交換1次) 循環(huán)次數:6次 交換次數:3次 遺憾的是算法需要的循環(huán)次數依然是1/2*(n-1)*n。所以算法復雜度為O(n*n)。 我們來看他的交換。由于每次外層循環(huán)只產生一次交換(只有一個最小值)。所以f(n)<=n 所以我們有f(n)=O(n)。 所以,在數據較亂的時候,可以減少一定的交換次數。 4.插入法: 插入法較為復雜,它的基本工作原理是抽出牌,在前面的牌中尋找相應的位置插入,然后繼續(xù)下一張 #include <iostream.h> void InsertSort(int* pData,int Count) { int iTemp; int iPos; for(int i=1;i<Count;i++) { iTemp = pData[i]; iPos = i-1; while((iPos>=0) && (iTemp<pData[iPos])) { pData[iPos+1] = pData[iPos]; iPos--; } pData[iPos+1] = iTemp; } } void main() { int data[] = {10,9,8,7,6,5,4}; InsertSort(data,7); for (int i=0;i<7;i++) cout<<data[i]<<" "; cout<<"\n"; } 倒序(最糟情況) 第一輪:10,9,8,7->9,10,8,7(交換1次)(循環(huán)1次) 第二輪:9,10,8,7->8,9,10,7(交換1次)(循環(huán)2次) 第一輪:8,9,10,7->7,8,9,10(交換1次)(循環(huán)3次) 循環(huán)次數:6次 交換次數:3次 其他: 第一輪:8,10,7,9->8,10,7,9(交換0次)(循環(huán)1次) 第二輪:8,10,7,9->7,8,10,9(交換1次)(循環(huán)2次) 第一輪:7,8,10,9->7,8,9,10(交換1次)(循環(huán)1次) 循環(huán)次數:4次 交換次數:2次 上面結尾的行為分析事實上造成了一種假象,讓我們認為這種算法是簡單算法中最好的,其實不是,因為其循環(huán)次數雖然并不固定,我們仍可以使用O方 法。從上面的結果可以看出,循環(huán)的次數f(n)<= 1/2*n*(n-1)<=1/2*n*n。所以其復雜度仍為O(n*n)(這里說明一下,其實如果不是為了展示這些簡單排序的不同,交換次數仍然 可以這樣推導)。現在看交換,從外觀上看,交換次數是O(n)(推導類似選擇法),但我們每次要進行與內層循環(huán)相同次數的‘=’操作。正常的一次交換我們 需要三次‘=’,而這里顯然多了一些,所以我們浪費了時間。 最終,我個人認為,在簡單排序算法中,選擇法是最好的。 二、高級排序算法: 高級排序算法中我們將只介紹這一種,同時也是目前我所知道(我看過的資料中)的最快的。 它的工作看起來仍然象一個二叉樹。首先我們選擇一個中間值middle程序中我們使用數組中間值,然后把比它小的放在左邊,大的放在右邊(具體的實現是從兩邊找,找到一對后交換)。然后對兩邊分別使用這個過程(最容易的方法——遞歸)。 1.快速排序: #include <iostream.h> void run(int* pData,int left,int right) { int i,j; int middle,iTemp; i = left; j = right; middle = pData[(left+right)/2]; //求中間值 do{ while((pData[i]<middle) && (i<right))//從左掃描大于中值的數 i++; while((pData[j]>middle) && (j>left))//從右掃描大于中值的數 j--; if(i<=j)//找到了一對值 { //交換 iTemp = pData[i]; pData[i] = pData[j]; pData[j] = iTemp; i++; j--; } }while(i<=j);//如果兩邊掃描的下標交錯,就停止(完成一次) //當左邊部分有值(left<j),遞歸左半邊 if(left<j) run(pData,left,j); //當右邊部分有值(right>i),遞歸右半邊 if(right>i) run(pData,i,right); } void QuickSort(int* pData,int Count) { run(pData,0,Count-1); } void main() { int data[] = {10,9,8,7,6,5,4}; QuickSort(data,7); for (int i=0;i<7;i++) cout<<data[i]<<" "; cout<<"\n"; } 這里我沒有給出行為的分析,因為這個很簡單,我們直接來分析算法:首先我們考慮最理想的情況 1.數組的大小是2的冪,這樣分下去始終可以被2整除。假設為2的k次方,即k=log2(n)。 2.每次我們選擇的值剛好是中間值,這樣,數組才可以被等分。 第一層遞歸,循環(huán)n次,第二層循環(huán)2*(n/2)...... 所以共有n+2(n/2)+4(n/4)+...+n*(n/n) = n+n+n+...+n=k*n=log2(n)*n 所以算法復雜度為O(log2(n)*n) 其他的情況只會比這種情況差,最差的情況是每次選擇到的middle都是最小值或最大值,那么他將變成交換法(由于使用了遞歸,情況更糟)。但是你認為這種情況發(fā)生的幾率有多大??呵呵,你完全不必擔心這個問題。實踐證明,大多數的情況,快速排序總是最好的。 如果你擔心這個問題,你可以使用堆排序,這是一種穩(wěn)定的O(log2(n)*n)算法,但是通常情況下速度要慢 于快速排序(因為要重組堆)。 三、其他排序 1.雙向冒泡: 通常的冒泡是單向的,而這里是雙向的,也就是說還要進行反向的工作。 代碼看起來復雜,仔細理一下就明白了,是一個來回震蕩的方式。 寫這段代碼的作者認為這樣可以在冒泡的基礎上減少一些交換(我不這么認為,也許我錯了)。 反正我認為這是一段有趣的代碼,值得一看。 #include <iostream.h> void Bubble2Sort(int* pData,int Count) { int iTemp; int left = 1; int right =Count -1; int t; do { //正向的部分 for(int i=right;i>=left;i--) { if(pData[i]<pData[i-1]) { iTemp = pData[i]; pData[i] = pData[i-1]; pData[i-1] = iTemp; t = i; } } left = t+1; //反向的部分 for(i=left;i<right+1;i++) { if(pData[i]<pData[i-1]) { iTemp = pData[i]; pData[i] = pData[i-1]; pData[i-1] = iTemp; t = i; } } right = t-1; }while(left<=right); } void main() { int data[] = {10,9,8,7,6,5,4}; Bubble2Sort(data,7); for (int i=0;i<7;i++) cout<<data[i]<<" "; cout<<"\n"; } 2.SHELL排序 這個排序非常復雜,看了程序就知道了。 首先需要一個遞減的步長,這里我們使用的是9、5、3、1(最后的步長必須是1)。 工作原理是首先對相隔9-1個元素的所有內容排序,然后再使用同樣的方法對相隔5-1個元素的排序以次類推。 #include <iostream.h> void ShellSort(int* pData,int Count) { int step[4]; step[0] = 9; step[1] = 5; step[2] = 3; step[3] = 1; int iTemp; int k,s,w; for(int i=0;i<4;i++) { k = step[i]; s = -k; for(int j=k;j<Count;j++) { iTemp = pData[j]; w = j-k;//求上step個元素的下標 if(s ==0) { s = -k; s++; pData[s] = iTemp; } while((iTemp<pData[w]) && (w>=0) && (w<=Count)) { pData[w+k] = pData[w]; w = w-k; } pData[w+k] = iTemp; } } } void main() { int data[] = {10,9,8,7,6,5,4,3,2,1,-10,-1}; ShellSort(data,12); for (int i=0;i<12;i++) cout<<data[i]<<" "; cout<<"\n"; } 呵呵,程序看起來有些頭疼。不過也不是很難,把s==0的塊去掉就輕松多了,這里是避免使用0步長造成程序異常而寫的代碼。這個代碼我認為很值得一看。 這個算法的得名是因為其發(fā)明者的名字D.L.SHELL。依照參考資料上的說法:“由于復雜的數學原因避免使用2的冪次步長,它能降低算法效率。”另外算法的復雜度為n的1.2次冪。同樣因為非常復雜并“超出本書討論范圍”的原因(我也不知道過程),我們只有結果了。 最后,希望大家愉快的編程。有什么意見,給我提吧!
2011年5月17日
#
用VS2005調試一個程序,出現“沒有找到MFC80D.DLL……”的提示使程序不能運行,刪掉Debug文件夾重新編譯問題依舊,上網查了一下,有說是vs路徑的原因,有說是vs沒裝好的原因。
在“啟動調試F5”的工具圖標右側有一欄“解決方案配置”,無意中將其中的“Debug”改為“Release”,F5通過,運行正常,項目目錄下生成“Release”文件夾,Debug方式生成的“Debug"文件夾是無用的。原因如下:
DEBUG和RELEASE 版本差異及調試相關問題: I. 內存分配問題
1. 變量未初始化。下面的程序在debug中運行的很好。
thing * search(thing * something) BOOL found; for(int i = 0; i < whatever.GetSize(); i++) { if(whatever[i]->field == something->field) { /* found it */ found = TRUE; break; } /* found it */ } if(found) return whatever[i]; else return NULL; 而在release中卻不行,因為debug中會自動給變量初始化found=FALSE,而在release版中則不會。所以盡可能的給變量、類或結構初始化。
2. 數據溢出的問題 如:char buffer[10]; int counter; lstrcpy(buffer, "abcdefghik");
在debug版中buffer的NULL覆蓋了counter的高位,但是除非counter>16M,什么問題也沒有。但是在release版中,counter可能被放在寄存器中,這樣NULL就覆蓋了buffer下面的空間,可能就是函數的返回地址,這將導致ACCESS ERROR。 3. DEBUG版和RELEASE版的內存分配方式是不同的。如果你在DEBUG版中申請 ele 為 6*sizeof(DWORD)=24bytes,實際上分配給你的是32bytes(debug版以32bytes為單位分配),而在release版,分配給你的就是24bytes(release版以8bytes為單位),所以在debug版中如果你寫ele[6],可能不會有什么問題,而在release版中,就有ACCESS VIOLATE。
II. ASSERT和VERIFY
1. ASSERT在Release版本中是不會被編譯的。
ASSERT宏是這樣定義的
#ifdef _DEBUG #define ASSERT(x) if( (x) == 0) report_assert_failure() #else #define ASSERT(x) #endif 實際上復雜一些,但無關緊要。假如你在這些語句中加了程序中必須要有的代碼 比如
ASSERT(pNewObj = new CMyClass);
pNewObj->MyFunction();
這種時候Release版本中的pNewObj不會分配到空間
所以執(zhí)行到下一個語句的時候程序會報該程序執(zhí)行了非法操作的錯誤。這時可以用VERIFY :
#ifdef _DEBUG #define VERIFY(x) if( (x) == 0) report_assert_failure() #else
#define VERIFY(x) (x) #endif 這樣的話,代碼在release版中就可以執(zhí)行了。
III. 參數問題:
自定義消息的處理函數,必須定義如下:
afx_msg LRESULT OnMyMessage(WPARAM, LPARAM);
返回值必須是HRESULT型,否則Debug會過,而Release出錯
IV. 內存分配
保證數據創(chuàng)建和清除的統(tǒng)一性:如果一個DLL提供一個能夠創(chuàng)建數據的函數,那么這個DLL同時應該提供一個函數銷毀這些數據。數據的創(chuàng)建和清除應該在同一個層次上。
V. DLL的災難
人們將不同版本DLL混合造成的不一致性形象的稱為 “動態(tài)連接庫的地獄“(DLL Hell) ,甚至微軟自己也這么說(http://msdn.microsoft.com/library/techart/dlldanger1.htm)。
如果你的程序使用你自己的DLL時請注意:
1. 不能將debug和release版的DLL混合在一起使用。debug都是debug版,release版都是release版。
解決辦法是將debug和release的程序分別放在主程序的debug和release目錄下
2. 千萬不要以為靜態(tài)連接庫會解決問題,那只會使情況更糟糕。
VI. RELEASE板中的調試:
1. 將ASSERT() 改為 VERIFY() 。找出定義在"#ifdef _DEBUG"中的代碼,如果在RELEASE版本中需要這些代碼請將他們移到定義外。查找TRACE(...)中代碼,因為這些代碼在RELEASE中也不被編譯。請認真檢查那些在RELEASE中需要的代碼是否并沒有被便宜。
2. 變量的初始化所帶來的不同,在不同的系統(tǒng),或是在DEBUG/RELEASE版本間都存在這樣的差異,所以請對變量進行初始化。
3. 是否在編譯時已經有了警告?請將警告級別設置為3或4,然后保證在編譯時沒有警告出現.
VII. 將Project Settings" 中 "C++/C " 項目下優(yōu)化選項改為Disbale(Debug)。編譯器的優(yōu)化可能導致許多意想不到的錯誤,請參考http://www.pgh.net/~newcomer/debug_release.htm
1. 此外對RELEASE版本的軟件也可以進行調試,請做如下改動:
在"Project Settings" 中 "C++/C " 項目下設置 "category" 為 "General" 并且將"Debug Info"設置為 "Program Database"。
在 "Link"項目下選中"Generate Debug Info"檢查框。
"Rebuild All"
如此做法會產生的一些限制:
無法獲得在MFC DLL中的變量的值。
必須對該軟件所使用的所有DLL工程都進行改動。
另:
MS BUG:MS的一份技術文檔中表明,在VC5中對于DLL的"Maximize Speed"優(yōu)化選項并未被完全支持,因此這將會引起內存錯誤并導致程序崩潰。
2. www.sysinternals.com有一個程序DebugView,用來捕捉OutputDebugString的輸出,運行起來后(估計是自設為system debugger)就可以觀看所有程序的OutputDebugString的輸出。此后,你可以脫離VC來運行你的程序并觀看調試信息。
3. 有一個叫Gimpel Lint的靜態(tài)代碼檢查工具,據說比較好用。http://www.gimpel.com 不過要化$的。
參考文獻:
1) http://www.cygnus-software.com/papers/release_debugging.html
2) http://www.pgh.net/~newcomer/debug_release.htm
2010年12月2日
#
--------------- 什么是 Hash Hash 的重要特性 Hash 函數的實現 主要的 Hash 算法 Hash 算法的安全問題 Hash 算法的應用 結 論 --------------- Hash,一般翻譯做“散列”,也有直接音譯為"哈希"的,就是把任意長度的輸入(又叫做預映射, pre-image),通過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小于輸入的空間,不同的輸入可能會散列成相同的輸出,而不可能從散列值來唯一的確定輸入值。
數學表述為:h = H(M) ,其中H( )--單向散列函數,M--任意長度明文,h--固定長度散列值。 在信息安全領域中應用的Hash算法,還需要滿足其他關鍵特性: 第一當然是單向性(one-way),從預映射,能夠簡單迅速的得到散列值,而在計算上不可能構造一個預映射,使其散列結果等于某個特定的散列值,即構造相應的M=H-1(h)不可行。這樣,散列值就能在統(tǒng)計上唯一的表征輸入值,因此,密碼學上的 Hash 又被稱為"消息摘要(message digest)",就是要求能方便的將"消息"進行"摘要",但在"摘要"中無法得到比"摘要"本身更多的關于"消息"的信息。 第二是抗沖突性(collision-resistant),即在統(tǒng)計上無法產生2個散列值相同的預映射。給定M,計算上無法找到M',滿足H(M)=H(M') ,此謂弱抗沖突性;計算上也難以尋找一對任意的M和M',使?jié)M足H(M)=H(M') ,此謂強抗沖突性。要求"強抗沖突性"主要是為了防范所謂"生日攻擊(birthday attack)",在一個10人的團體中,你能找到和你生日相同的人的概率是2.4%,而在同一團體中,有2人生日相同的概率是11.7%。類似的,當預映射的空間很大的情況下,算法必須有足夠的強度來保證不能輕易找到"相同生日"的人。 第三是映射分布均勻性和差分分布均勻性,散列結果中,為 0 的 bit 和為 1 的 bit ,其總數應該大致相等;輸入中一個 bit 的變化,散列結果中將有一半以上的 bit 改變,這又叫做"雪崩效應(avalanche effect)";要實現使散列結果中出現 1bit 的變化,則輸入中至少有一半以上的 bit 必須發(fā)生變化。其實質是必須使輸入中每一個 bit 的信息,盡量均勻的反映到輸出的每一個 bit 上去;輸出中的每一個 bit,都是輸入中盡可能多 bit 的信息一起作用的結果。 Damgard 和 Merkle 定義了所謂“壓縮函數(compression function)”,就是將一個固定長度輸入,變換成較短的固定長度的輸出,這對密碼學實踐上 Hash 函數的設計產生了很大的影響。Hash函數就是被設計為基于通過特定壓縮函數的不斷重復“壓縮”輸入的分組和前一次壓縮處理的結果的過程,直到整個消息都被壓縮完畢,最后的輸出作為整個消息的散列值。盡管還缺乏嚴格的證明,但絕大多數業(yè)界的研究者都同意,如果壓縮函數是安全的,那么以上述形式散列任意長度的消息也將是安全的。這就是所謂 Damgard/Merkle 結構: 在下圖中,任意長度的消息被分拆成符合壓縮函數輸入要求的分組,最后一個分組可能需要在末尾添上特定的填充字節(jié),這些分組將被順序處理,除了第一個消息分組將與散列初始化值一起作為壓縮函數的輸入外,當前分組將和前一個分組的壓縮函數輸出一起被作為這一次壓縮的輸入,而其輸出又將被作為下一個分組壓縮函數輸入的一部分,直到最后一個壓縮函數的輸出,將被作為整個消息散列的結果。 MD5 和 SHA1 可以說是目前應用最廣泛的Hash算法,而它們都是以 MD4 為基礎設計的。 1) MD4 MD4(RFC 1320)是 MIT 的 Ronald L. Rivest 在 1990 年設計的,MD 是 Message Digest 的縮寫。它適用在32位字長的處理器上用高速軟件實現--它是基于 32 位操作數的位操作來實現的。它的安全性不像RSA那樣基于數學假設,盡管 Den Boer、Bosselaers 和 Dobbertin 很快就用分析和差分成功的攻擊了它3輪變換中的 2 輪,證明了它并不像期望的那樣安全,但它的整個算法并沒有真正被破解過,Rivest 也很快進行了改進。 下面是一些MD4散列結果的例子: MD4 ("") = 31d6cfe0d16ae931b73c59d7e0c089c0 MD4 ("a") = bde52cb31de33e46245e05fbdbd6fb24 MD4 ("abc") = a448017aaf21d8525fc10ae87aa6729d MD4 ("message digest") = d9130a8164549fe818874806e1c7014b MD4 ("abcdefghijklmnopqrstuvwxyz") = d79e1c308aa5bbcdeea8ed63df412da9 MD4 ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") = 043f8582f241db351ce627e153e7f0e4 MD4 ("12345678901234567890123456789012345678901234567890123456789012345678901234567890") = e33b4ddc9c38f2199c3e7b164fcc0536 2) MD5 MD5(RFC 1321)是 Rivest 于1991年對MD4的改進版本。它對輸入仍以512位分組,其輸出是4個32位字的級聯(lián),與 MD4 相同。它較MD4所做的改進是:
1) 加入了第四輪 2) 每一步都有唯一的加法常數; 3) 第二輪中的G函數從((X ∧ Y) ∨ (X ∧ Z) ∨ (Y ∧ Z)) 變?yōu)?((X ∧ Z) ∨ (Y ∧ ~Z))以減小其對稱性; 4) 每一步都加入了前一步的結果,以加快"雪崩效應"; 5) 改變了第2輪和第3輪中訪問輸入子分組的順序,減小了形式的相似程度; 6) 近似優(yōu)化了每輪的循環(huán)左移位移量,以期加快"雪崩效應",各輪的循環(huán)左移都不同。 盡管MD5比MD4來得復雜,并且速度較之要慢一點,但更安全,在抗分析和抗差分方面表現更好。 消息首先被拆成若干個512位的分組,其中最后512位一個分組是“消息尾+填充字節(jié)(100…0)+64 位消息長度”,以確保對于不同長度的消息,該分組不相同。64位消息長度的限制導致了MD5安全的輸入長度必須小于264bit,因為大于64位的長度信息將被忽略。而4個32位寄存器字初始化為A=0x01234567,B=0x89abcdef,C=0xfedcba98,D=0x76543210,它們將始終參與運算并形成最終的散列結果。 接著各個512位消息分組以16個32位字的形式進入算法的主循環(huán),512位消息分組的個數據決定了循環(huán)的次數。主循環(huán)有4輪,每輪分別用到了非線性函數 F(X, Y, Z) = (X ∧ Y) ∨ (~X ∧ Z) G(X, Y, Z) = (X ∧ Z) ∨ (Y ∧ ~Z) H(X, Y, Z) =X ⊕ Y ⊕ Z I(X, Y, Z) = X ⊕ (Y ∨ ~Z) 這4輪變換是對進入主循環(huán)的512位消息分組的16個32位字分別進行如下操作:將A、B、C、D的副本a、b、c、d中的3個經F、G、H、I運算后的結果與第4個相加,再加上32位字和一個32位字的加法常數,并將所得之值循環(huán)左移若干位,最后將所得結果加上a、b、c、d之一,并回送至ABCD,由此完成一次循環(huán)。 所用的加法常數由這樣一張表T[i]來定義,其中i為1…64,T[i]是i的正弦絕對值之4294967296次方的整數部分,這樣做是為了通過正弦函數和冪函數來進一步消除變換中的線性性。 當所有512位分組都運算完畢后,ABCD的級聯(lián)將被輸出為MD5散列的結果。下面是一些MD5散列結果的例子: MD5 ("") = d41d8cd98f00b204e9800998ecf8427e MD5 ("a") = 0cc175b9c0f1b6a831c399e269772661 MD5 ("abc") = 900150983cd24fb0d6963f7d28e17f72 MD5 ("message digest") = f96b697d7cb7938d525a2f31aaf161d0 MD5 ("abcdefghijklmnopqrstuvwxyz") = c3fcd3d76192e4007dfb496cca67e13b MD5 ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") = d174ab98d277d9f5a5611c2c9f419d9f MD5 ("12345678901234567890123456789012345678901234567890123456789012345678901234567890") = 57edf4a22be3c955ac49da2e2107b67a 參考相應RFC文檔可以得到MD4、MD5算法的詳細描述和算法的C源代碼。 3) SHA1 及其他 SHA1是由NIST NSA設計為同DSA一起使用的,訪問http://www.itl.nist.gov/fipspubs可以得到它的詳細規(guī)范--[/url]"FIPS PUB 180-1 SECURE HASH STANDARD"。它對長度小于264的輸入,產生長度為160bit的散列值,因此抗窮舉(brute-force)性更好。SHA-1 設計時基于和MD4相同原理,并且模仿了該算法。因為它將產生160bit的散列值,因此它有5個參與運算的32位寄存器字,消息分組和填充方式與MD5相同,主循環(huán)也同樣是4輪,但每輪進行20次操作,非線性運算、移位和加法運算也與MD5類似,但非線性函數、加法常數和循環(huán)左移操作的設計有一些區(qū)別,可以參考上面提到的規(guī)范來了解這些細節(jié)。下面是一些SHA1散列結果的例子: SHA1 ("abc") = a9993e36 4706816a ba3e2571 7850c26c 9cd0d89d SHA1 ("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") = 84983e44 1c3bd26e baae4aa1 f95129e5 e54670f1 其他一些知名的Hash算法還有MD2、N-Hash、RIPE-MD、HAVAL等等。上面提到的這些都屬于"純"Hash算法。還有另2類Hash算法,一類就是基于對稱分組算法的單向散列算法,典型的例子是基于DES的所謂Davies-Meyer算法,另外還有經IDEA改進的Davies-Meyer算法,它們兩者目前都被認為是安全的算法。另一類是基于模運算/離散對數的,也就是基于公開密鑰算法的,但因為其運算開銷太大,而缺乏很好的應用前景。 沒有通過分析和差分攻擊考驗的算法,大多都已經夭折在實驗室里了,因此,如果目前流行的Hash算法能完全符合密碼學意義上的單向性和抗沖突性,就保證了只有窮舉,才是破壞Hash運算安全特性的唯一方法。為了對抗弱抗沖突性,我們可能要窮舉個數和散列值空間長度一樣大的輸入,即嘗試2^128或2^160個不同的輸入,目前一臺高檔個人電腦可能需要10^25年才能完成這一艱巨的工作,即使是最高端的并行系統(tǒng),這也不是在幾千年里的干得完的事。而因為"生日攻擊"有效的降低了需要窮舉的空間,將其降低為大約1.2*2^64或1.2*2^80,所以,強抗沖突性是決定Hash算法安全性的關鍵。 在NIST新的 Advanced Encryption Standard (AES)中,使用了長度為128、192、256bit 的密鑰,因此相應的設計了 SHA256、SHA384、SHA512,它們將提供更好的安全性。 Hash算法在信息安全方面的應用主要體現在以下的3個方面: 1) 文件校驗 我們比較熟悉的校驗算法有奇偶校驗和CRC校驗,這2種校驗并沒有抗數據篡改的能力,它們一定程度上能檢測并糾正數據傳輸中的信道誤碼,但卻不能防止對數據的惡意破壞。 MD5 Hash算法的"數字指紋"特性,使它成為目前應用最廣泛的一種文件完整性校驗和(Checksum)算法,不少Unix系統(tǒng)有提供計算md5 checksum的命令。它常被用在下面的2種情況下: 第一是文件傳送后的校驗,將得到的目標文件計算 md5 checksum,與源文件的md5 checksum 比對,由兩者 md5 checksum 的一致性,可以從統(tǒng)計上保證2個文件的每一個碼元也是完全相同的。這可以檢驗文件傳輸過程中是否出現錯誤,更重要的是可以保證文件在傳輸過程中未被惡意篡改。一個很典型的應用是ftp服務,用戶可以用來保證多次斷點續(xù)傳,特別是從鏡像站點下載的文件的正確性。 更出色的解決方法是所謂的代碼簽名,文件的提供者在提供文件的同時,提供對文件Hash值用自己的代碼簽名密鑰進行數字簽名的值,及自己的代碼簽名證書。文件的接受者不僅能驗證文件的完整性,還可以依據自己對證書簽發(fā)者和證書擁有者的信任程度,決定是否接受該文件。瀏覽器在下載運行插件和java小程序時,使用的就是這樣的模式。 第二是用作保存二進制文件系統(tǒng)的數字指紋,以便檢測文件系統(tǒng)是否未經允許的被修改。不少系統(tǒng)管理/系統(tǒng)安全軟件都提供這一文件系統(tǒng)完整性評估的功能,在系統(tǒng)初始安裝完畢后,建立對文件系統(tǒng)的基礎校驗和數據庫,因為散列校驗和的長度很小,它們可以方便的被存放在容量很小的存儲介質上。此后,可以定期或根據需要,再次計算文件系統(tǒng)的校驗和,一旦發(fā)現與原來保存的值有不匹配,說明該文件已經被非法修改,或者是被病毒感染,或者被木馬程序替代。TripWire就提供了一個此類應用的典型例子。 更完美的方法是使用"MAC"。"MAC" 是一個與Hash密切相關的名詞,即信息鑒權碼(Message Authority Code)。它是與密鑰相關的Hash值,必須擁有該密鑰才能檢驗該Hash值。文件系統(tǒng)的數字指紋也許會被保存在不可信任的介質上,只對擁有該密鑰者提供可鑒別性。并且在文件的數字指紋有可能需要被修改的情況下,只有密鑰的擁有者可以計算出新的散列值,而企圖破壞文件完整性者卻不能得逞。 2) 數字簽名 Hash 算法也是現代密碼體系中的一個重要組成部分。由于非對稱算法的運算速度較慢,所以在數字簽名協(xié)議中,單向散列函數扮演了一個重要的角色。 在這種簽名協(xié)議中,雙方必須事先協(xié)商好雙方都支持的Hash函數和簽名算法。 簽名方先對該數據文件進行計算其散列值,然后再對很短的散列值結果--如Md5是16個字節(jié),SHA1是20字節(jié),用非對稱算法進行數字簽名操作。對方在驗證簽名時,也是先對該數據文件進行計算其散列值,然后再用非對稱算法驗證數字簽名。 對 Hash 值,又稱"數字摘要"進行數字簽名,在統(tǒng)計上可以認為與對文件本身進行數字簽名是等效的。而且這樣的協(xié)議還有其他的優(yōu)點: 首先,數據文件本身可以同它的散列值分開保存,簽名驗證也可以脫離數據文件本身的存在而進行。 再者,有些情況下簽名密鑰可能與解密密鑰是同一個,也就是說,如果對一個數據文件簽名,與對其進行非對稱的解密操作是相同的操作,這是相當危險的,惡意的破壞者可能將一個試圖騙你將其解密的文件,充當一個要求你簽名的文件發(fā)送給你。因此,在對任何數據文件進行數字簽名時,只有對其Hash值進行簽名才是安全的。 3) 鑒權協(xié)議 如下的鑒權協(xié)議又被稱作"挑戰(zhàn)--認證模式:在傳輸信道是可被偵聽,但不可被篡改的情況下,這是一種簡單而安全的方法。 需要鑒權的一方,向將被鑒權的一方發(fā)送隨機串(“挑戰(zhàn)”),被鑒權方將該隨機串和自己的鑒權口令字一起進行 Hash 運算后,返還鑒權方,鑒權方將收到的Hash值與在己端用該隨機串和對方的鑒權口令字進行 Hash 運算的結果相比較(“認證”),如相同,則可在統(tǒng)計上認為對方擁有該口令字,即通過鑒權。 POP3協(xié)議中就有這一應用的典型例子: S: +OK POP3 server ready <1896.697170952@dbc.mtview.ca.us> C: APOP mrose c4c9334bac560ecc979e58001b3e22fb S: +OK maildrop has 1 message (369 octets) 在上面的一段POP3協(xié)議會話中,雙方都共享的對稱密鑰(鑒權口令字)是tanstaaf,服務器發(fā)出的挑戰(zhàn)是<1896.697170952@dbc.mtview.ca.us>,客戶端對挑戰(zhàn)的應答是MD5("<1896.697170952@dbc.mtview.ca.us>tanstaaf") = c4c9334bac560ecc979e58001b3e22fb,這個正確的應答使其通過了認證。 散列算法長期以來一直在計算機科學中大量應用,隨著現代密碼學的發(fā)展,單向散列函數已經成為信息安全領域中一個重要的結構模塊,我們有理由深入研究其設計理論和應用方法。
原文 http://www.cnblogs.com/finallyliuyu/archive/2010/10/11/1848130.html
一、C++中不能使用random()函數
==================================================================================
本文由青松原創(chuàng)并依GPL-V2及其后續(xù)版本發(fā)放,轉載請注明出處且應包含本行聲明。
C++中常用rand()函數生成隨機數,但嚴格意義上來講生成的只是偽隨機數(pseudo-random integral
number)。生成隨機數時需要我們指定一個種子,如果在程序內循環(huán),那么下一次生成隨機數時調用上一次的結果作為種子。但如果分兩次執(zhí)行程序,那么由
于種子相同,生成的“隨機數”也是相同的。
在工程應用時,我們一般將系統(tǒng)當前時間(Unix時間)作為種子,這樣生成的隨機數更接近于實際意義上的隨機數。給一下例程如下:
#include <iostream> #include <ctime> #include <cstdlib> using namespace std;
int main() { double random(double,double); srand(unsigned(time(0))); for(int icnt = 0; icnt != 10; ++icnt) cout << "No." << icnt+1 << ": " << int(random(0,10))<< endl; return 0; }
double random(double start, double end) { return start+(end-start)*rand()/(RAND_MAX + 1.0); } /* 運行結果 * No.1: 3 * No.2: 9 * No.3: 0 * No.4: 9 * No.5: 5 * No.6: 6 * No.7: 9 * No.8: 2 * No.9: 9 * No.10: 6 */ 利用這種方法能不能得到完全意義上的隨機數呢?似乎9有點多哦?卻沒有1,4,7?!我們來做一個概率實驗,生成1000萬個隨機數,看0-9這10個數出現的頻率是不是大致相同的。程序如下: #include <iostream> #include <ctime> #include <cstdlib> #include <iomanip> using namespace std;
int main() { double random(double,double); int a[10] = {0}; const int Gen_max = 10000000; srand(unsigned(time(0))); for(int icnt = 0; icnt != Gen_max; ++icnt) switch(int(random(0,10))) { case 0: a[0]++; break; case 1: a[1]++; break; case 2: a[2]++; break; case 3: a[3]++; break; case 4: a[4]++; break; case 5: a[5]++; break; case 6: a[6]++; break; case 7: a[7]++; break; case 8: a[8]++; break; case 9: a[9]++; break; default: cerr << "Error!" << endl; exit(-1); } for(int icnt = 0; icnt != 10; ++icnt)
cout << icnt << ": " << setw(6) <<
setiosflags(ios::fixed) << setprecision(2) <<
double(a[icnt])/Gen_max*100 << "%" << endl; return 0; }
double random(double start, double end) { return start+(end-start)*rand()/(RAND_MAX + 1.0); } /* 運行結果 * 0: 10.01% * 1: 9.99% * 2: 9.99% * 3: 9.99% * 4: 9.98% * 5: 10.01% * 6: 10.02% * 7: 10.01% * 8: 10.01% * 9: 9.99% */ 可知用這種方法得到的隨機數是滿足統(tǒng)計規(guī)律的。
另:在Linux下利用GCC編譯程序,即使我執(zhí)行了1000000次運算,是否將random函數定義了inline函數似乎對程序沒有任何影響,有理由相信,GCC已經為我們做了優(yōu)化。但是冥冥之中我又記得要做inline優(yōu)化得加O3才行...
不行,于是我們把循環(huán)次數改為10億次,用time命令查看執(zhí)行時間: chinsung@gentoo ~/workspace/test/Debug $ time ./test 0: 10.00% 1: 10.00% 2: 10.00% 3: 10.00% 4: 10.00% 5: 10.00% 6: 10.00% 7: 10.00% 8: 10.00% 9: 10.00%
real 2m7.768s user 2m4.405s sys 0m0.038s chinsung@gentoo ~/workspace/test/Debug $ time ./test 0: 10.00% 1: 10.00% 2: 10.00% 3: 10.00% 4: 10.00% 5: 10.00% 6: 10.00% 7: 10.00% 8: 10.00% 9: 10.00%
real 2m7.269s user 2m4.077s sys 0m0.025s
前一次為進行inline優(yōu)化的情形,后一次為沒有作inline優(yōu)化的情形,兩次結果相差不大,甚至各項指標后者還要好一些,不知是何緣由...
=================================================================================
random函數不是ANSI C標準,不能在gcc,vc等編譯器下編譯通過。
可改用C++下的rand函數來實現。 1、C++標準函數庫提供一隨機數生成器rand,返回0-RAND_MAX之間均勻分布的偽隨機整數。
RAND_MAX必須至少為32767。rand()函數不接受參數,默認以1為種子(即起始值)。
隨機數生成器總是以相同的種子開始,所以形成的偽隨機數列也相同,失去了隨機意義。(但這樣便于程序調試) 2、C++中另一函數srand(),可以指定不同的數(無符號整數變元)為種子。但是如果種子相同,偽隨機數列也相同。一個辦法是讓用戶輸入種子,但是仍然不理想。 3、 比較理想的是用變化的數,比如時間來作為隨機數生成器的種子。 time的值每時每刻都不同。所以種子不同,所以,產生的隨機數也不同。 // C++隨機函數(VC program) #include <stdio.h> #include <iostream> #include <time.h> using namespace std; #define MAX 100 int main(int argc, char* argv[]) { srand( (unsigned)time( NULL ) );//srand()函數產生一個以當前時間開始的隨機種子.應該放在for等循環(huán)語句前面 不然要很長時間等待 for (int i=0;i<10;i++) cout<<rand()%MAX<<endl;//MAX為最大值,其隨機域為0~MAX-1 return 0; } 二、rand()的用法 rand()不需要參數,它會返回一個從0到最大隨機數的任意整數,最大隨機數的大小通常是固定的一個大整數。 這樣,如果你要產生0~10的10個整數,可以表達為: int N = rand() % 11; 這樣,N的值就是一個0~10的隨機數,如果要產生1~10,則是這樣: int N = 1 + rand() % 10; 總結來說,可以表示為: a + rand() % n
其中的a是起始值,n是整數的范圍。 a + rand() % (b-a+1)
就表示 a~b之間的一個隨機數若要0~1的小數,則可以先取得0~10的整數,然后均除以10即可得到隨機到十分位的10個隨機小數,若要得到隨機到百
分位的隨機小數,則需要先得到0~100的10個整數,然后均除以100,其它情況依此類推。 通常rand()產生的隨機數在每次運行的時候都是與上一次相同的,這是有意這樣設計的,是為了便于程序的調試。若要產生每次不同的隨機數,可以使用srand( seed )函數進行隨機化,隨著seed的不同,就能夠產生不同的隨機數。 如大家所說,還可以包含time.h頭文件,然后使用srand(time(0))來使用當前時間使隨機數發(fā)生器隨機化,這樣就可以保證每兩次運行時可以得到不同的隨機數序列(只要兩次運行的間隔超過1秒)。
2010年11月26日
#
2010年11月8日
#
摘要: 使用ADO的具體方法
網上關于ADO的使用方法很多,這邊我個人就整理出一個使用ADO的方法的具體步驟:1、用#import引入ADO庫文件在stdafx.h文件中添加#import "c:\program files\common files\system\ado\msado15.dll"no_namespaces rename("EOF" adoEOF")
2、 數... 閱讀全文
2010年10月11日
#
這兩天遇到一個問題,就是運行可執(zhí)行文件時,出現"can not initialize data binding"錯誤,原因:
使用DATAGRID控件,除了注冊MSDATGRD.OCX外,還需要注冊一下MSSTDFMT.DLL才可以。MSSTDFMT.DLL是微軟
標準數據格式對象相關動態(tài)鏈接庫文件,引用名稱為“Microsoft Data Formatting Object
Library”,如果在開發(fā)程序中有數據綁定,就是通過它對數據格式化后再綁定到控件的。如果用到數據綁定控件,那么就要記得把
MSSTDFMT.DLL加到安裝程序里面。
注:有的電腦注冊MSDATGRD.OCX、MSSTDFMT.DLL,所以未出現此類情況。
解決方法:
方法一:
程序打包時,將MSDATGRD.OCX、MSSTDFMT.DLL都加載上去。
方法二:
開始-〉運行:
regsvr32 MSDATGRD.OCX
regsvr32 MSSTDFMT.DLL
2010年9月17日
#
一、
什么是對齊,以及為什么要對齊: 1.
現代計算機中內存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定變量的時候經常在特定的
內存地址訪問,這就需要各類型數據按照一定的規(guī)則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。 2.
對齊的作用和原因:各個硬件平臺對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數據只能從某些特定地址開始存取。其他平臺可能沒有這種情況,
但是最常見的是如果不按照適合其平臺的要求對數據存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設為
32位)如果存放在偶地址開始的地方,那么一個讀周期就可以讀出,而如果存放在奇地址開始的地方,就可能會需要2個讀周期,并對兩次讀出的結果的高低
字節(jié)進行拼湊才能得到該int數據。顯然在讀取效率上下降很多。這也是空間和時間的博弈。 二、對齊的實現
通常,我們寫程序的時候,不需要考慮對齊問題。編譯器會替我們選擇適合目標平臺的對齊策略。當然,我們也可以通知給編譯器傳遞預編譯指令而改變對指定數據
的對齊方法。
但是,正因為我們一般不需要關心這個問題,所以因為編輯器對數據存放做了對齊,而我們不了解的話,常常會對一些問題感到迷惑。最常見的就是struct數
據結構的sizeof結果,出乎意料。為此,我們需要對對齊算法所了解。 對齊的算法:
由于各個平臺和編譯器的不同,現以本人使用的gcc version
3.2.2編譯器(32位x86平臺)為例子,來討論編譯器對struct數據結構中的各成員如何進行對齊的。 設結構體如下定義:
struct A { int a; char b; short c; };
結構體A中包含了4字節(jié)長度的int一個,1字節(jié)長度的char一個和2字節(jié)長度的short型數據一個。所以A用到的空間應該是7字節(jié)。但是因為編譯器
要對數據成員在空間上進行對齊。 所以使用sizeof(strcut A)值為8。 現在把該結構體調整成員變量的順序。
struct B { char b; int a; short c; };
這時候同樣是總共7個字節(jié)的變量,但是sizeof(struct B)的值卻是12。 下面我們使用預編譯指令#pragma pack
(value)來告訴編譯器,使用我們指定的對齊值來取代缺省的。 #pragma pack (2) /*指定按2字節(jié)對齊*/
struct C { char b; int a; short c; };
#pragma pack () /*取消指定對齊,恢復缺省對齊*/ sizeof(struct C)值是8。
修改對齊值為1: #pragma pack (1) /*指定按1字節(jié)對齊*/ struct D { char
b; int a; short c; }; #pragma pack ()
/*取消指定對齊,恢復缺省對齊*/ sizeof(struct D)值為7。
對于char型數據,其自身對齊值為1,對于short型為2,對于int,float,double類型,其自身對齊值為4,單位字節(jié)。
這里面有四個概念值: 1)數據類型自身的對齊值:就是上面交代的基本數據類型的自身對齊值。 2)指定對齊值:#pragma
pack (value)時的指定對齊值value。 3)結構體或者類的自身對齊值:其成員中自身對齊值最大的那個值。
4)數據成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中較小的那個值。
有了這些值,我們就可以很方便的來討論具體數據結構的成員和其自身的對齊方式。有效對齊值N是最終用來決定數據存放地址方式的值,最重要。有效對齊N,就
是表示“對齊在N上”,也就是說該數據的"存放起始地址%N=0".而數據結構中的數據變量都是按定義的先后順序來排放的。第一個數據變量的起始地址就是
數據結構的起始地址。結構體的成員變量要對齊排放,結構體本身也要根據自身的有效對齊值圓整(就是結構體成員變量占用總長度需要是對結構體有效對齊值的整
數倍,結合下面例子理解)。這樣就不難理解上面的幾個例子的值了。 例子分析: 分析例子B; struct B {
char b; int a; short c; };
假設B從地址空間0x0000開始排放。該例子中沒有定義指定對齊值,在筆者環(huán)境下,該值默認為4。第一個成員變量b的自身對齊值是1,比指定或者默認指
定對齊值4小,所以其有效對齊值為1,所以其存放地址0x0000符合0x0000%1=0.第二個成員變量a,其自身對齊值為4,所以有效對齊值也為
4,所以只能存放在起始地址為0x0004到0x0007這四個連續(xù)的字節(jié)空間中,復核0x0004%4=0,且緊靠第一個變量。第三個變量c,自身對齊
值為2,所以有效對齊值也是2,可以存放在0x0008到0x0009這兩個字節(jié)空間中,符合0x0008%2=0。所以從0x0000到0x0009存
放的都是B內容。再看數據結構B的自身對齊值為其變量中最大對齊值(這里是b)所以就是4,所以結構體的有效對齊值也是4。根據結構體圓整的要求,
0x0009到0x0000=10字節(jié),(10+2)%4=0。所以0x0000A到0x000B也為結構體B所占用。故B從0x0000到0x000B
共有12個字節(jié),sizeof(struct B)=12; 同理,分析上面例子C: #pragma pack (2)
/*指定按2字節(jié)對齊*/ struct C { char b; int a; short
c; }; #pragma pack () /*取消指定對齊,恢復缺省對齊*/
第一個變量b的自身對齊值為1,指定對齊值為2,所以,其有效對齊值為1,假設C從0x0000開始,那么b存放在0x0000,符合0x0000%1=
0;第二個變量,自身對齊值為4,指定對齊值為2,所以有效對齊值為2,所以順序存放在0x0002、0x0003、0x0004、0x0005四個連續(xù)
字節(jié)中,符合0x0002%2=0。第三個變量c的自身對齊值為2,所以有效對齊值為2,順序存放
在0x0006、0x0007中,符合0x0006%2=0。所以從0x0000到0x00007共八字節(jié)存放的是C的變量。又C的自身對齊值為4,所以
C的有效對齊值為2。又8%2=0,C只占用0x0000到0x0007的八個字節(jié)。所以sizeof(struct C)=8.
有
了以上的解釋,相信你對C語言的字節(jié)對齊概念應該有了清楚的認識了吧。在網絡程序中,掌握這個概念可是很重要的喔,在不同平臺之間(比如在Windows
和Linux之間)傳遞2進制流(比如結構體),那么在這兩個平臺間必須要定義相同的對齊方式,不然莫名其妙的出了一些錯,可是很難排查的哦^_^。
|