1. 程式人生 > 實用技巧 >c語言快速學習

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         66
View 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: fffffffb
View 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() 被宣告為以下原型:

  1. void perror( const char * );

下面是另一個函式原型的宣告,引數列表中的關鍵字 void 表示該函式沒有引數:

  1. 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)表示式顯式地將表示式的返回值丟棄,例如,如下程式碼丟棄了函式返回值:

  1. (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