當把一個char類型轉換成int型的時候計算機里究竟而發生了什麼?
看過回答之後發現非科班,沒學過彙編,這是困惑的根源
一直對基本類型的cast疑惑不解,比如
#include &
void main(){
char x;
x="a";
printf("%c
",x);x=(int)x;
printf("%d",x);
}
這個時候x變數的內存發生了什麼樣的變化?
補充一個問題,CPU知不知道這個變數是char類型的還是int類型的?如果知道的化,這個標記是存在哪裡的?
首先,要知道一件事情:你在代碼里寫的事情,計算機未必會幹。
比如:
char var = "a";
var = (int) var;
沒準人家編譯器看你沒有產生實際效用,就直接給砍了。
更嚴格來講,你應當寫:
char var1 = "a";
int var2 = (int) var1;
這就有點實際意義了。
一直對基本類型的cast疑惑不解
C的基本類型,大都能對應到CPU能處理的標準數字類型。char基本都是一個8位有符號整數,最高位是符號位。int是不低於16位的有符號整數(所有的常見平台上都是32位),最高位是符號位。
那麼進行這種轉換,基本上就是編譯器把這件事情翻譯成一個或者幾個CPU指令,大概是:
- 從地址var1讀內容到寄存器;
- 執行8位到32位的有符號整數轉換;
- 將結果從寄存器寫到地址var2,寫32位。
對於ASCII,a的數值是97,用八位的二進位數表示,就是01100001。把它轉換到int,實際上就是在求一個32位有符號的值,數值意義和01100001要相等。編譯器會把你這個表達式翻譯成對應的CPU操作。結果是00000000 00000000 00000000 01100001。
看起來好像除了填上一堆零,啥都沒幹?
實際上填零是重要操作,因為內存空間可以是髒的:在你給丫賦值之前,丫可能還帶著上次計算完之後的垃圾。
而且,如果是負數,就會很不一樣。比如:
char var1 = -1;
int var2 = (int) var1;
由於現代計算機幾乎全都用補碼來表示負數,於是負一的8位二進位表示,是11111111;而32位表示,則是11111111 11111111 11111111 11111111。不難看到,對於有符號數的長度轉換,並不總是填零的。
所以CPU在這裡面,還是需要做一些微小的工作。
======================
補充運行時類型的問題:
C這種語言,要的就是運行時不帶類型記號的裸奔快感。比如對於類似這樣的源代碼:
int a = some_procedure();
int b = some_other_procedure();
int c = a + b;
最後的那個相加,在不優化的情況下,通常會產生類似這樣的幾個指令:
- 從地址a取32位數據到寄存器1;
- 從地址b取32位數據到寄存器2;
- 執行32位整形加法;
- 將32位數據寫到地址c;
可以看到,這裡面沒有類型。對數據的操作方式就是類型。
具體進行了什麼操作看彙編代碼就知道了。為了生成的彙編比較簡潔,把代碼簡化了一下:
char foo(){
char c = "a";
c = (int)c;
return c;
}
使用gcc -S test.c得到的彙編代碼如下:
pushq %rbp
movq %rsp, %rbp
movb $97, -1(%rbp) # 將97放到棧中
movzbl -1(%rbp), %eax # 內存中1位元組拷貝到eax寄存器中,默認eax寄存器存儲返回值
popq %rbp
ret
因為是將內存中1位元組拷貝四位元組長的寄存器eax中,所以使用了movzbl,該指令會將eax前三個位元組的內容填充0,默認使用eax存儲返回值。因為我們返回的是char類型,後續調用該函數時,只會使用eax中的al寄存器。
使用gcc -S -O2 test.c開啟優化得到的彙編代碼如下:
movl $97, %eax
ret
可以看到省掉了很多步驟,直接將立即數97放入到eax中,和之前的效果是一樣的。
類型信息是用來指導編譯器翻譯彙編代碼的,編譯器會根據變數的類型選擇相應的機器指令,比如的short + short會使用addw指令,int + int會使用add指令。如果兩個操作數的類型不相同,編譯器會插入一條拓展指令,也就是所謂的向上轉型。
編譯器完成翻譯後類型信息就已經丟失了,CPU只需要執行指令即可。
區別在於讀寫內存的長度。
byte,word,double word
... 所以還有個東西叫對齊
char to int會進行符號擴展,如果高8位是1,高8至32位全是1。
要是unsigned char ,高位全是0,低8位一樣。
具體到彙編,有2條對應指令實現高位擴展。CPU沒有變數類型的概念。如果硬要說有,那就是有整型和浮點型的區別而已。
題主應該是沒有學過彙編,如果想具體地了解清楚可以學習彙編之後再來看該程序編譯出的彙編代碼。因為彙編代碼基本上就是CPU在執行該程序時真正依照的指令。回到原問題上,其實對於這個程序,正常的編譯器基本都會發現x的值在整個程序中都沒有改變,從而直接去掉變數x,將整個程序優化為功能類似於如下的C代碼
printf("%c","a");printf("%d","a");不過我們依照題主的意思,不對程序進行編譯時優化,那麼事實上的程序流程多半是下面這樣(要想看到事實上的流程還是要看程序彙編代碼):
將值65(即"a"的ASCII碼)存至32位寄存器A從寄存器A取出值並以字元形式列印(即將65對應的字元"a"列印出來)從寄存器A取出值並以整數形式列印(即列印65)
可以看到以上代碼中並沒有出現類型轉換。事實上,char與int的區別(也包括short與int的區別等),都只是C語言中對於數據類型劃分的一種標準,凡是C語言中長度小於等於32位元組的數據類型在32位CPU中的表現形式都是一樣的(即32位寄存器)。但如果需要將數據存入內存,以上的數據類型所佔的長度就可能會不相同,這裡也要考慮內存對齊等話題,不作深入探討。
總之,C語言中,char與int在32位CPU上沒有區別,而在內存中也僅有長度區別。本質上都是整數。字元型的本質也只是一個存儲著字元ASCII碼的整型變數。在這些類型中做數據類型轉換時,底層硬體看到的幾乎只有數據長度的變化而已。
附註:有符號型變數與無符號型變數,整型變數與浮點型變數在相互轉換時,除了數據長度,數據內容也可能發生變化。詳情還請自行了解這些數據類型的編碼格式。一個位元組的長度,最常用於保存單位元組字元,比如英文字母和標點符號,所以我認為是當初在語言設計上,給這種一個位元組長度的類型,起了個好認的名字,叫作了 char。
或許你是被名字搞懵了,實質上,它就是一段八位二進位序列,跟是不是字元、是不是整型,沒有什麼關係。
而更長長度的類型,因為在西方語言中,一般並不會用來保存字元,而且又廣泛用於存儲整型數字,所以你看到的類型名,就定義成了 short、int、long、long long 等。
在 C 語言里,所謂數據類型,其實就是為了讓編碼者更好的理解這個數據是幹啥用的,而不是它到底是啥。對於計算機而言,它們都是一段一段的二進位序列,這些序列到底應該獨立存在、還是成組存在、還是把某一位二進位位用來表達整數的正負符號等,你喜歡怎麼來,聲明一下類型,編譯器就能明白。
當經過編譯器編譯後的代碼交給 CPU 執行的時候,CPU 才沒有什麼字元、數字的概念,到底要幹啥,取決於你在數據上用的操作指令,也即對應你在語言里寫的操作符或函數,比如加減乘除、printf 等。
所以,給 char 加上 unsigned 前綴存儲整數 186,沒有問題。直接拿兩個 char 相加、相乘,不管是變數還是字面量,沒有問題。甚至把一個 GBK 字元存到 unsigned short 里按中文輸出,也沒有問題。
部分內容憑過往記憶回答,恐有疏漏,還望指正。
這樣的轉型,其實只是改變了位的解釋吧,尺寸不一樣還會加幾個零或剪一下。別噴我,我說錯了講一下就是了。
孩咂,你咋把強轉寫成這樣了。會帶壞其他小朋友的。
先回答問題吧,有影響。
先看兩段代碼,
沒有強轉:
int main() {
char ac = 1;
asm("nop");//把我當分割線吧
ac = ac;
asm("nop");//把我當分割線吧
}
有強轉:
int main() {
char ac = 1;
asm("nop");//把我當分割線吧
ac = (int)ac;
asm("nop");//把我當分割線吧
}
這個是問題對伐,我們看生成的目標代碼:
gcc -g -c cast.c
objdump -d -D cast.o
有一堆代碼,主要看main下面的(還好我們打了分割線),
沒有強轉:
8: 90 nop //還認識我吧
9: 8a 45 ff movb -1(%rbp), %al
c: 88 45 ff movb %al, -1(%rbp)
f: 90 nop //還認識我吧
有強轉:
8: 90 nop //還認識我吧
9: 0f be 45 ff movsbl -1(%rbp), %eax
d: 88 c1 movb %al, %cl
f: 88 4d ff movb %cl, -1(%rbp)
12: 90 nop //還認識我吧
這一條多出的指令:
9: 0f be 45 ff movsbl -1(%rbp), %eax
就是為強轉生成的語句,整個有強轉的代碼翻譯成C就是,先把char(-1(%rbp))轉成int(臨時變數且匿名)(eax),然後在切下來低幾位(al)放回char(先給cl,再給-1(%rbp))。所以並沒有啥卵用。
編譯器也被你教壞了。。。
實驗環境,MacBook Pro (15-inch, 2017) macOS Sierra version 10.12.5
不確定給編譯器加O會不會有其他結果,有興趣的話可以試試。
CPU不知道的,編譯器才知道,char型和int型唯一不一樣的地方就是它的長度。前者一個位元組,後者兩個。這帶來兩方面的影響,一個是計算時用多長的寄存器和對應計算指令,另一個是訪問內存時訪問多長的數據。
對於計算來講,用一個位元組來計算較大數的加法可能會得到意料之外的結果,因為溢出了。所以編程老師會推薦盡量使用int。
從訪問內存的角度,你的例子不涉及。但如果改一改,改成把char*變成int*的話,訪存的寬度就變了,本來訪問一個位元組,現在變成訪問四個位元組,其餘三個位元組來自於這個位元組的周圍。這種現象可以誤用也可以善用。簡單的善用方法就是我要列印一個八位元組的緩衝區時可以寫成列印兩個4位元組int,或一次8位元組long long,而不是列印八次或寫一個for循環。
想搞明白這個問題,了解下彙編就好了。在 cpu看來沒有什麼變數類型一說,這個東西是編譯器層面的
@Xi Yang 回答的很好,只是有點抽象,題主十分拿衣服,在評論里問了半天似乎也沒問出個所以然了,其實拿彙編代碼直接舉例估計會直白很多
這個每個架構不一樣
x86下,無論什麼數據長度,指令都是一樣的,區別在於用什麼寄存器(AL:8位、AX:16位、EAX:32位、RAX:64位)
比如
char a = 5;
int b = (int) a;
char c = (char) b;
如果沒有任何優化,生成代碼應該如下:
mov al, 5 ;把5載入8位寄存器AL
mov [byte ptr a], al
xor eax, eax ;對eax進行清零,因為x86在載入8位寄存器時不會對高位清零
mov al, [byte ptr a]
mov [dword ptr b], eax
mov [byte ptr c], al
這得益於x86的寄存器模式,RAX是完整的64位寄存器,EAX是其低32位,AX是其低16位,AH和AL分別是低16位中的高8位和低8位,他們都是同一個寄存器的不同部分
也有一些架構不是這樣做的,指令不同,或者乾脆沒有對8位數操作的指令也是有可能的,就需要一些更多的操作,比如 0x000000ff之類的,這些編譯器都幫你做了,但實際上每種架構需要區別對待
回到x86,得益於其寄存器結構,可以做到一些特別是RISC架構做不到的優化,比如
char a = 12;
char b = 34;
short c = a | (b &<&< 8);
這個,可以優化成
mov al, [byte ptr a]
mov ah, [byte ptr b]
mov [word ptr c], ax
而不需要實際的移位和或運算
總之,你的問題,「究竟發生了什麼」,這個需要不同的架構不同的對待,可能發生完全不同的事情,既可能是選用了不同的寄存器,也有可能是進行了與操作,或者天知道有那些奇葩的架構需要按常識不能理解的操作
當然如果像題主這麼拿衣服的代碼,大多數情況下編譯器是什麼都不會做的,直接給你優化掉了
推薦閱讀:
※OpenMP在實際開發中應用多嗎?
※對於 C/C++ 函數指針的困惑?
※有沒有使用「==」判斷浮點數相等與否出現錯誤的例子?
※怎樣開發一款有限元軟體,從哪些方面學習?
※C++ #include " " 與 <>有什麼區別?