c語言快速學習
原碼, 反碼, 補碼的基礎概念和計算方法.
在探求為何機器要使用補碼之前, 讓我們先了解原碼, 反碼和補碼的概念.對於一個數, 計算機要使用一定的編碼方式進行儲存. 原碼, 反碼, 補碼是機器儲存一個具體數字的編碼方式.
1. 原碼
原碼就是符號位加上真值的絕對值, 即用第一位表示符號, 其餘位表示值. 比如如果是8位二進位制:
[+1]原 = 0000 0001 [-1]原 = 1000 0001
第一位是符號位. 因為第一位是符號位, 所以8位二進位制數的取值範圍就是:
[1111 1111 , 0111 1111]==>[-127 , 127]
2. 反碼
反碼的表示方法是:
- 正數的反碼是其本身
- 負數的反碼是在其原碼的基礎上, 符號位不變,其餘各個位取反.
[+1] = [00000001]原 = [00000001]反 [-1] = [10000001]原 = [11111110]反
3. 補碼
補碼的表示方法是:
- 正數的補碼就是其本身
- 負數的補碼是在其原碼的基礎上, 符號位不變, 其餘各位取反, 最後+1. (即在反碼的基礎上+1)
[+1] = [00000001]原 = [00000001]反 = [00000001]補 [-1] = [10000001]原 = [11111110]反 = [11111111]補
三. 為何要使用原碼, 反碼和補碼
計算機可以有三種編碼方式表示一個數. 對於正數因為三種編碼方式的結果都相同:
[+1] = [00000001]原= [00000001]反= [00000001]補
所以不需要過多解釋. 但是對於負數:
[-1] = [10000001]原
= [11111110]反= [11111111]補
可見原碼, 反碼和補碼是完全不同的. 為何還會有反碼和補碼呢?
首先, 因為人腦可以知道第一位是符號位, 在計算的時候我們會根據符號位, 選擇對真值區域的加減. (真值的概念在本文最開頭).
但是對於計算機, 加減乘數已經是最基礎的運算, 要設計的儘量簡單. 計算機辨別"符號位"顯然會讓計算機的基礎電路設計變得十分複雜! 於是人們想出了將符號位也參與運算的方法.
根據運演算法則減去一個正數等於加上一個負數, 即: 1-1 = 1 + (-1) = 0 , 所以機器可以只有加法而沒有減法, 這樣計算機運算的設計就更簡單了.
於是人們開始探索 將符號位參與運算, 並且只保留加法的方法. 首先來看原碼:
計算十進位制的表示式: 1-1=0
為了解決原碼做減法的問題, 出現了反碼:
1 - 1 = 1 + (-1) = [0000 0001]原+ [1000 0001]原= [0000 0001]反+ [1111 1110]反= [1111 1111]反= [1000 0000]原= -0
發現用反碼計算減法, 結果的真值部分是正確的. 而唯一的問題其實就出現在"0"這個特殊的數值上. 雖然人們理解上+0和-0是一樣的, 但是0帶符號是沒有任何意義的. 而且會有[0000 0000]原和[1000 0000]原兩個編碼表示0.
於是補碼的出現, 解決了0的符號以及兩個編碼的問題:
1-1 = 1 + (-1) = [0000 0001]原+ [1000 0001]原= [0000 0001]補+ [1111 1111]補= [0000 0000]補=[0000 0000]原
這樣0用[0000 0000]表示, 而以前出現問題的-0則不存在了.而且可以用[1000 0000]表示-128:
(-1) + (-127) = [1000 0001]原+ [1111 1111]原= [1111 1111]補+ [1000 0001]補= [1000 0000]補
-1-127的結果應該是-128, 在用補碼運算的結果中, [1000 0000]補就是-128. 但是注意因為實際上是使用以前的-0的補碼來表示-128, 所以-128並沒有原碼和反碼錶示.(對-128的補碼錶示[1000 0000]補算出來的原碼是[0000 0000]原, 這是不正確的)
使用補碼, 不僅僅修復了0的符號以及存在兩個編碼的問題, 而且還能夠多表示一個最低數. 這就是為什麼8位二進位制, 使用原碼或反碼錶示的範圍為[-127, +127], 而使用補碼錶示的範圍為[-128, 127].
因為機器使用補碼, 所以對於程式設計中常用到的32位int型別, 可以表示範圍是: [-231, 231-1] 因為第一位表示的是符號位.而使用補碼錶示時又可以多儲存一個最小值.
四 原碼, 反碼, 補碼 再深入
計算機巧妙地把符號位參與運算, 並且將減法變成了加法, 背後蘊含了怎樣的數學原理呢?
將鐘錶想象成是一個1位的12進位制數. 如果當前時間是6點, 我希望將時間設定成4點, 需要怎麼做呢?我們可以:
1. 往回撥2個小時: 6 - 2 = 4
2. 往前撥10個小時: (6 + 10) mod 12 = 4
3. 往前撥10+12=22個小時: (6+22) mod 12 =4
2,3方法中的mod是指取模操作, 16 mod 12 =4 即用16除以12後的餘數是4.
所以鐘錶往回撥(減法)的結果可以用往前撥(加法)替代!
現在的焦點就落在瞭如何用一個正數, 來替代一個負數. 上面的例子我們能感覺出來一些端倪, 發現一些規律. 但是數學是嚴謹的. 不能靠感覺.
首先介紹一個數學中相關的概念: 同餘
同餘的概念
兩個整數a,b,若它們除以整數m所得的餘數相等,則稱a,b對於模m同餘
記作 a ≡ b (mod m)
讀作 a 與 b 關於模 m 同餘。
舉例說明:
4 mod 12 = 4
16 mod 12 = 4
28 mod 12 = 4
所以4, 16, 28關於模 12 同餘.
負數取模
正數進行mod運算是很簡單的. 但是負數呢?
下面是關於mod運算的數學定義:
上面是截圖, "取下界"符號找不到如何輸入(word中貼上過來後亂碼). 下面是使用"L"和"J"替換上圖的"取下界"符號:
x mod y = x - y L x / y J
上面公式的意思是:
x mod y等於 x 減去 y 乘上 x與y的商的下界.
以 -3 mod 2 舉例:
-3 mod 2
= -3 - 2xL -3/2 J
= -3 - 2xL-1.5J
= -3 - 2x(-2)
= -3 + 4 = 1
所以:
(-2) mod 12 = 12-2=10
(-4) mod 12 = 12-4 = 8
(-5) mod 12 = 12 - 5 = 7
負數在計算機中的儲存形式:
負數的補碼等於它的反碼加1,即在其反碼的最低位加1就為該數的補碼,且在計算機中負數以補碼形式進行儲存。
已知:1、int型佔4位元組(32位二進位制)char型佔1位元組(8位二進位制)
2、字元在記憶體中以ASCII形式儲存(A的為65,C為67)
3、在記憶體中低地址存低位,高地址存高位
二、具體內容
先規定一個int型負數inti=-48829;
原碼為:10000000/00000000/10111110/10111101
反碼為:11111111/11111111/01000001/01000010
補碼為:11111111/11111111/01000001/01000011
即可假設該數在記憶體中的實際存放為:
低地址位,地址值為&i01000011
01000001
11111111
高地址位,地址值為&i+311111111
然後用char型指標p1和p2分別指向地址&i和&i+1,並進行輸出,分別得到p1輸出字母C,p2輸出字母A,即說明了&i地址中的內容為01000011,&i+1中的內容為01000001
即驗證了是以補碼形式儲存,而不是原碼或反碼!
三、分析總結
四、例項測試程式碼
#include <stdio.h> int main(void) { int i; char *p1; char *p2; i = -48829; //假設負數儲存形式為反碼,即為: 1111 1111/ 1111 1111/0100 0001/0100 0011 p1 = &i; //假設p1指向 0100 0011 (67) p2 = p1 + 1;//假設p2指向 0100 0001 (65) printf("%c\n", *p1); //輸出字元C(67),得證 printf("%c\n", *p2); //輸出字元A(65),得證 getchar(); return 0; }View Code
FLOAT 以及DOUBLE的儲存形式:
|--浮點數怎麼儲存在計算機中
浮點型變數是由符號位+階碼位+尾數位組成。
float型資料 二進位制為32位,符號位1位,階碼8位,尾數23位
double型資料 二進位制為64位,符號位1位,階碼11位,尾數52位
|--單精度32位儲存
1bit 8bit 23bit
|--雙精度64位儲存
1bit 11bit 52bit
浮點數二進位制儲存形式,是符號位+階碼位+尾數位(針對有符號數)
浮點數沒有無符號數(c語言)
|--階碼:
這裡階碼採用移碼錶示,對於float型資料其規定偏置量為127,階碼有正有負,
對於8位二進位制,則其表示範圍為-128-127,double型規定為1023,其表示範圍為-1024-1023
比如對於float型資料,若階碼的真實值為2,則加上127後為129,其階碼錶示形式為10000010
|--尾數:
有效數字位,即部分二進位制位(小數點後面的二進位制位),
因為規定M的整數部分恆為1(有效數字位從左邊不是0的第一位算起),所以這個1就不進行儲存
|--具體步驟:
把浮點數先化為科學計數法表示形式,eg:1.1111011*2^6,然後取階碼(6)的值加上127(對於float)
計算出階碼,尾數是處小數點後的位數(1111011),如果不夠23位,則在後面補0至23位。
最後,符號位+階碼位+尾數位就是其記憶體中二進位制的儲存形式
1 eg: 2 #include <stdio.h> 3 #include <stdlib.h> 4 int main(int argc, char *argv[]) 5 { 6 int x = 12; 7 char *q = (char *)&x; 8 float a=125.5; 9 char *p=(char *)&a; 10 11 printf("%d\n", *q); 12 13 printf("%d\n",*p); 14 printf("%d\n",*(p+1)); 15 printf("%d\n",*(p+2)); 16 printf("%d\n",*(p+3)); 17 return 0; 18 } 19 20 output: 21 12 22 0 23 0 24 -5 25 66View Code
|--對於float型:
125.5二進位制表示為1111101.1,由於規定尾數的整數部分恆為1,
則表示為1.1111011*2^6,階碼為6,加上127為133,則表示為10000101
而對於尾數將整數部分1去掉,為1111011,在其後面補0使其位數達到23位,
則為11110110000000000000000
記憶體中的表現形式為:
00000000 低地址
00000000
11111011
01000010 高地址
儲存形式為: 00 00 fb 42
依次列印為: 0 0 -5 66
解釋下-5,記憶體中是:11111011,因為是有符號變數所以符號位為1是負數,
所以其真值為符號位不變取反加一,變為:10000101化為十進位制為-5.
# include <stdio.h> int main() { int a=-5; printf("a=-5: %x\n", a); return 0; }View Code
xyy@xyy-virtual-machine:~/c_learn$ vim d02_fushu.c xyy@xyy-virtual-machine:~/c_learn$ ./a.out a=-5: fffffffbView Code
測試各種資料型別所佔的位元組:
編寫C程式時需要考慮每種資料型別在記憶體中所佔的記憶體大小,即使同一種資料型別在不同平臺下所佔記憶體大小亦不相同。為了得到某個型別在特定平臺上的準確大寫,可以使用sizeof運算子,表示式sizeof(type)得到物件或型別的儲存位元組大小。
- char儲存大小1位元組,值範圍-128~127;
- unsigned char儲存大小1位元組,值範圍0~255;
- short儲存大小2位元組,值範圍-32768~32767;
- unsigned short儲存大小2位元組,值範圍0~65535;
- int——
16位系統儲存大小2位元組,值範圍-32768~32767,
32、64位系統儲存大小4位元組,值範圍-2147483648~2147483647;
- unsigned int——
16位系統儲存大小2位元組,值範圍0~65535,
32、64位系統儲存大小4位元組,值範圍0~4294967295;
- long——
16、32位系統儲存大小4位元組,值範圍-2147483648~2147483647,
64位系統儲存大小8位元組,值範圍-9223372036854775808~9223372036854775807;
- unsigned long——
16、32位系統儲存大小4位元組,值範圍0~4294967295,
64位系統儲存大小8位元組,值範圍0~18446744073709551615;
- float儲存大小4位元組,值範圍1.175494351*10^-38~3.402823466*10^38;
- double儲存大小8位元組,值範圍2.2250738585072014*10^-308~1.7976931348623158*10^308;
- long long儲存大小8位元組,值範圍-9223372036854775808~9223372036854775807;
- unsigned long long儲存大小8位元組,值範圍0~18446744073709551615;
- long double——
16位系統儲存大小8位元組,值範圍2.22507*10^-308~1.79769*10^308,
32位系統儲存大小12位元組(有效位10位元組,為了對齊實際分配12位元組),值範圍3.4*10^-4932 到 1.1*10^4932,
64位系統儲存大小16位元組(有效位10位元組,為了對齊實際分配16位元組),值範圍3.4*10^-4932 到 1.1*10^4932;
- 指標——
16位系統儲存大小2位元組,
32位系統儲存大小4位元組,
64位系統儲存大小8位元組。
#include <stdio.h> #include <stdlib.h> #include <float.h> int main(void) { printf("資料型別:char,儲存大小:%d位元組、最小值:%hhd,最大值:%hhd\n", sizeof(char), CHAR_MIN, CHAR_MAX); printf("資料型別:unsigned char,儲存大小:%d位元組、最小值:%hhu,最大值:%hhu\n", sizeof(unsigned char), 0U, UCHAR_MAX); printf("資料型別:short,儲存大小:%d位元組、最小值:%hd,最大值:%hd\n", sizeof(short), SHRT_MIN, SHRT_MAX); printf("資料型別:unsigned short,儲存大小:%d位元組、最小值:%hu,最大值:%hu\n", sizeof(unsigned short), 0U, USHRT_MAX); printf("資料型別:int,儲存大小:%d位元組、最小值:%d,最大值:%d\n", sizeof(int), INT_MIN, INT_MAX); printf("資料型別:unsigned int,儲存大小:%d位元組、最小值:%u,最大值:%u\n", sizeof(unsigned int), 0U, UINT_MAX); printf("資料型別:long,儲存大小:%d位元組、最小值:%ld,最大值:%ld\n", sizeof(long), LONG_MIN, LONG_MAX); printf("資料型別:unsigned long,儲存大小:%d位元組、最小值:%lu,最大值:%lu\n", sizeof(unsigned long), 0LU, ULONG_MAX); printf("資料型別:float,儲存大小:%d位元組、最小值:%g,最大值:%g\n", sizeof(float), FLT_MIN, FLT_MAX); printf("資料型別:double,儲存大小:%d位元組、最小值:%lg,最大值:%lg\n", sizeof(double), DBL_MIN, DBL_MAX); printf("資料型別:long long,儲存大小:%d位元組、最小值:%lld,最大值:%lld\n", sizeof(long long), LLONG_MIN, LLONG_MAX); printf("資料型別:unsigned long long,儲存大小:%d位元組、最小值:%llu,最大值:%llu\n", sizeof(unsigned long long), 0LLU, ULLONG_MAX); printf("資料型別:long double,儲存大小:%d位元組、最小值:%Lg,最大值:%Lg\n", sizeof(long double), LDBL_MIN, LDBL_MAX); return EXIT_SUCCESS; }View Code
執行結果:
void 關鍵字:
void型別修飾符(type specifier)表示“沒有值可以獲得”。因此,不可以採用這個型別宣告變數或常量。void 型別可以用於下面各小節所描述的目的。
void用於函式宣告
沒有返回值的函式,其型別為 void。例如,標準庫函式 perror() 被宣告為以下原型:
- void perror( const char * );
下面是另一個函式原型的宣告,引數列表中的關鍵字 void 表示該函式沒有引數:
- FILE *tmpfile( void );
如果嘗試進行函式呼叫,例如採用 tmpfile("name.tmp"),則編譯器會報錯。如果該函式宣告時引數列表中未採用 void,則C編譯器就無法獲得關於該函式引數的資訊,因此,無法判斷 tmpfile("name.tmp") 的呼叫是否正確。
void型別表示式
void 型別表示式指的是沒有值的表示式。例如,呼叫一個沒有返回值的函式,就是一種 void 型別表示式:
char filename[] = "memo.txt"; if ( fopen( filename, "r") == NULL ) perror( filename ); // void表示式View Code
型別轉換(cast)運算(void)表示式顯式地將表示式的返回值丟棄,例如,如下程式碼丟棄了函式返回值:
- (void)printf("I don't need this function's return value!\n");
指向void的指標
一個 void* 型別的指標代表了物件的地址,但沒有該物件的型別資訊。這種“無資料型別”的指標主要用於宣告函式,讓函式可使用各種型別的指標引數,或者返回一個“多用途”的指標。例如,標準記憶體管理函式:
void *malloc( size_t size ); void *realloc( void *ptr, size_t size ); void free( void *ptr );View Code
如下例所示,可將一個 void 指標值賦值給另一個物件指標型別,反之亦可,這都不需要進行顯式的型別轉換。
:演示void型別的用法:
#include <stdio.h> #include <time.h> #include <stdlib.h> // 提供以下函式的原型 // void srand( unsigned int seed ); // int rand( void ); // void *malloc( size_t size ); // void free( void *ptr ); // void exit( int status }; enum { ARR_LEN = 100 }; int main () { int i, *pNumbers = malloc(ARR_LEN * sizeof(int)); //獲得相同的儲存空間 if( pNumbers == NULL ) { fprintf(stderr,"Insufficient memory.\n"); exit(1); } srand( (unsigned)time(NULL ); // 初始化隨機數產生器 for ( i=0; i < ARR_LEN; ++i ) pNumbers[i] = rand() % 10000; // 儲存一些隨機數 printf("\n%d random numbers between 0 and 0000:\n", ARR_LEN); for ( i=0; i< ARR_LEN; ++i ) // 迴圈輸出 { printf("%6d",pNumbers[i]); // 每次迴圈輸出一個數字 if ( i % 10 == 9) putchar( '\n'); // 每10個數字換一行 } free( pNumbers ); // 釋放儲存空間 return 0; }View Code