Try   HackMD

C 語言強制轉型 (casting)

#include <stdio.h>
int main() {
    unsigned int a = 10;
    double b = (double)a;
    printf("a = %u, b = %lf in decimal form\n", a, b);
    printf("a = %x, b = %x in hex form\n", a, b);
}

以上是一個簡單的 C 語言 casting,把無號數轉換成浮點數

a = 10, b = 10.000000 in decimal form
a = a, b = 15 in hex form 

可以看到儘管表示同一個數字,但是實際上在不同資料型態間二進位表示是完全不同的,這也是為什麼在 C 語言中我們需要透過 Casting 強制轉型才能讓一個數值在不同資料型態間轉換

不過你有想過,強制轉型底層是怎麼實做的嗎?

char to intshort to int

int main() {
    char c = 0x30;
    int b = (int) c;
    short s = 0x30;
    int a = (int) s;
}
    push    rbp
    mov     rbp, rsp
    mov     BYTE PTR [rbp-1], 48
    movsx   eax, BYTE PTR [rbp-1]
    mov     DWORD PTR [rbp-8], eax
    mov     WORD PTR [rbp-10], 48
    movsx   eax, WORD PTR [rbp-10]
    mov     DWORD PTR [rbp-16], eax
    mov     eax, 0
    pop     rbp
    ret
  1. 把原本只佔 1 BYTE 的char 形態用int Double WORD (4 BYTE) 的方法取出。
  2. 把原本只佔 1 WORD 的short 形態用int Double WORD (4 BYTE) 的方法取出。

int to float

int 轉換成 float 就比較困難,float 因為遵守 IEEE 754 的規定雖然可以表示小數點但是數值的表示變得複雜

int main() {
    int a = -10;
    float b = (float) a;
    return 0;
}

下面是利用 godbolt 所產生的組合語言

    push    rbp
    mov     rbp, rsp
    mov     DWORD PTR [rbp-4], -10
    cvtsi2ss        xmm0, DWORD PTR [rbp-4]
    movss   DWORD PTR [rbp-8], xmm0
    mov     eax, 0
    pop     rbp
    ret

cvtsi2ss 即是把int轉換成float的 x86 指令,這個指令的全名是

CVTSI2SS — Convert Doubleword Integer to Scalar Single-Precision Floating-Point Value

也就是用一個指令來 convert。至於實作就看各家晶片製造商的做法。

xmm1 這個暫存器也很有趣,根據維基百科的敘述,xmm1 是在 SSE ( Streaming SIMD Extensions ) 指令集的時候加入的暫存器,有 128 bits 可以進行單指令多資料的操作。

除了資料型態轉換可能會改變原始資料儲存方式,C 語言中的「型別修飾字」(qualifiers)會不會也會影響呢?

int to unsigned int

int main() { int a = -10; unsigned int b = (unsigned int) a; printf("a=%x, b=%x\n", a, b); }

事實上,這邊印出來的 hex 型態都是相同的 0xfffffff6

其實 unsigned 這樣的修飾字並不會影響資料數值,不過在讀取的時候對同一筆資料的解讀方式會不一樣

C99 規格書裡面應該可以找到 reference,我是找 type qualifier 不過符合的量還是太多 陳奕熹

int and const int

int main() {
    int a = 10;
    const int b = 10;
    printf("a = %x, &a = %x\n", a, &a);
    printf("b = %x, &a = %x\n", b, &b);
}

十分意外的,這是編譯後的結果

a = a, &a = 7fffffffdd90
b = a, &a = 7fffffffdd94
/* 用 gdb 看 stack pointer 在 7fffffffddb0 */

a, b在二進制時表現相同這點,從前面的 unsigned 部份就可以理解到二進制不會被改動

不過令人意外的是,被標記為 const 的資料並不會被存到下面 C語言記憶體配置圖中的 text 區域

int main() {
    int a = 10;
    const int b = (const int) a; 
}
main:
  push rbp
  mov rbp, rsp
  mov DWORD PTR [rbp-4], 10    /* 從這邊開始是 line 4 */
  mov eax, DWORD PTR [rbp-4]
  mov DWORD PTR [rbp-8], eax   /* line 4 結束 */
  mov eax, 0
  pop rbp
  ret

有趣的是,從編譯出來的組合語言可以知道,事實上

int b = a;
const int b = (unsigned int) a;

兩者在完全沒做最佳化(-O0)時,編譯的結果就已經一樣了