變量
變量(variable)可以理解成一塊內存區域的名字。通過變量名,可以引用這塊內存區域,獲取里面存儲的值。由於值可能發生變化,所以稱為變量,否則就是常量了。
變量名
變量名在 C 語言里面屬於標識符(identifier),命名有嚴格的規范。
- 只能由字母(包括大寫和小寫)、數字和下划線(
_
)組成。 - 不能以數字開頭。
- 長度不能超過63個字符。
下面是一些無效變量名的例子。
$zj
j**p
2cat
Hot-tab
tax rate
don't
上面示例中,每一行的變量名都是無效的。
變量名區分大小寫,star
、Star
、STAR
都是不同的變量。
並非所有的詞都能用作變量名,有些詞在 C 語言里面有特殊含義(比如int
),另一些詞是命令(比如continue
),它們都稱為關鍵字,不能用作變量名。另外,C 語言還保留了一些詞,供未來使用,這些保留字也不能用作變量名。下面就是 C 語言主要的關鍵字和保留字。
auto, break, case, char, const, continue, default, do, double, else, enum, extern, float, for, goto, if, inline, int, long, register, restrict, return, short, signed, sizeof, static, struct, switch, typedef, union, unsigned, void, volatile, while
另外,兩個下划線開頭的變量名,以及一個下划線 + 大寫英文字母開頭的變量名,都是系統保留的,自己不應該起這樣的變量名。
變量的聲明
C 語言的變量,必須先聲明后使用。如果一個變量沒有聲明,就直接使用,會報錯。
每個變量都有自己的類型(type)。聲明變量時,必須把變量的類型告訴編譯器。
int height;
上面代碼聲明了變量height
,並且指定類型為int
(整數)。
如果幾個變量具有相同類型,可以在同一行聲明。
int height, width;
// 等同於
int height;
int width;
注意,聲明變量的語句必須以分號結尾。
一旦聲明,變量的類型就不能在運行時修改。
變量的賦值
C 語言會在變量聲明時,就為它分配內存空間,但是不會清除內存里面原來的值。這導致聲明變量以后,變量會是一個隨機的值。所以,變量一定要賦值以后才能使用。
賦值操作通過賦值運算符(=
)完成。
int num;
num = 42;
上面示例中,第一行聲明了一個整數變量num
,第二行給這個變量賦值。
變量的值應該與類型一致,不應該賦予不是同一個類型的值,比如num
的類型是整數,就不應該賦值為小數。雖然 C 語言會自動轉換類型,但是應該避免賦值運算符兩側的類型不一致。
變量的聲明和賦值,也可以寫在一行。
int num = 42;
多個相同類型變量的賦值,可以寫在同一行。
int x = 1, y = 2;
注意,賦值表達式有返回值,等於等號右邊的值。
int x, y;
x = 1;
y = (x = 2 * x);
上面代碼中,變量y
的值就是賦值表達式(x = 2 * x
)的返回值2
。
由於賦值表達式有返回值,所以 C 語言可以寫出多重賦值表達式。
int x, y, z, m, n;
x = y = z = m = n = 3;
上面的代碼是合法代碼,一次為多個變量賦值。賦值運算符是從右到左執行,所以先為n
賦值,然后依次為m
、z
、y
和x
賦值。
C 語言有左值(left value)和右值(right value)的概念。左值是可以放在賦值運算符左邊的值,一般是變量;右值是可以放在賦值運算符右邊的值,一般是一個具體的值。這是為了強調有些值不能放在賦值運算符的左邊,比如x = 1
是合法的表達式,但是1 = x
就會報錯。
變量的作用域
作用域(scope)指的是變量生效的范圍。C 語言的變量作用域主要有兩種:文件作用域(file scope)和塊作用域(block scope)。
文件作用域(file scope)指的是,在源碼文件頂層聲明的變量,從聲明的位置到文件結束都有效。
int x = 1;
int main(void) {
printf("%i\n", x);
}
上面示例中,變量x
是在文件頂層聲明的,從聲明位置開始的整個當前文件都是它的作用域,可以在這個范圍的任何地方讀取這個變量,比如函數main()
內部就可以讀取這個變量。
塊作用域(block scope)指的是由大括號({}
)組成的代碼塊,它形成一個單獨的作用域。凡是在塊作用域里面聲明的變量,只在當前代碼塊有效,代碼塊外部不可見。
int a = 12;
if (a == 12) {
int b = 99;
printf("%d %d\n", a, b); // 12 99
}
printf("%d\n", a); // 12
printf("%d\n", b); // 出錯
上面例子中,變量b
是在if
代碼塊里面聲明的,所以對於大括號外面的代碼,這個變量是不存在的。
代碼塊可以嵌套,即代碼塊內部還有代碼塊,這時就形成了多層的塊作用域。它的規則是:內層代碼塊可以使用外層聲明的變量,但外層不可以使用內層聲明的變量。如果內層的變量與外層同名,那么會在當前作用域覆蓋外層變量。
{
int i = 10;
{
int i = 20;
printf("%d\n", i); // 20
}
printf("%d\n", i); // 10
}
上面示例中,內層和外層都有一個變量i
,每個作用域都會優先使用當前作用域聲明的i
。
最常見的塊作用域就是函數,函數內部聲明的變量,對於函數外部是不可見的。for
循環也是一個塊作用域,循環變量只對循環體內部可見,外部是不可見的。
for (int i = 0; i < 10; i++)
printf("%d\n", i);
printf("%d\n", i); // 出錯
上面示例中,for
循環省略了大括號,但依然是一個塊作用域,在外部讀取循環變量i
,編譯器就會報錯。
數據類型
C 語言的每一種數據,都是有類型(type)的,編譯器必須知道數據的類型,才能操作數據。所謂“類型”,就是相似的數據所擁有的共同特征,那么一旦知道某個值的數據類型,就能知道該值的特征和操作方式。
基本數據類型有三種:字符(char)、整數(int)和浮點數(float)。復雜的類型都是基於它們構建的。
字符類型
字符類型指的是單個字符,類型聲明使用char
關鍵字。
char c = 'B';
上面示例聲明了變量c
是字符類型,並將其賦值為字母B
。
C 語言規定,字符常量必須放在單引號里面。
在計算機內部,字符類型使用一個字節(8位)存儲。C 語言將其當作整數處理,所以字符類型就是寬度為一個字節的整數。每個字符對應一個整數(由 ASCII 碼確定),比如B
對應整數66
。
字符類型在不同計算機的默認范圍是不一樣的。一些系統默認為-128
到127
,另一些系統默認為0
到255
。這兩種范圍正好都能覆蓋0
到127
的 ASCII 字符范圍。
只要在字符類型的范圍之內,整數與字符是可以互換的,都可以賦值給字符類型的變量。
char c = 66;
// 等同於
char c = 'B';
上面示例中,變量c
是字符類型,賦給它的值是整數66。這跟賦值為字符B
的效果是一樣的。
兩個字符類型的變量可以進行數學運算。
char a = 'B'; // 等同於 char a = 66;
char b = 'C'; // 等同於 char b = 67;
printf("%d\n", a + b); // 輸出 133
上面示例中,字符類型變量a
和b
相加,視同兩個整數相加。占位符%d
表示輸出十進制整數,因此輸出結果為133。
單引號本身也是一個字符,如果要表示這個字符常量,必須使用反斜杠轉義。
char t = '\'';
上面示例中,變量t
為單引號字符,由於字符常量必須放在單引號里面,所以內部的單引號要使用反斜杠轉義。
這種轉義的寫法,主要用來表示 ASCII 碼定義的一些無法打印的控制字符,它們也屬於字符類型的值。
\a
:警報,這會使得終端發出警報聲或出現閃爍,或者兩者同時發生。\b
:退格鍵,光標回退一個字符,但不刪除字符。\f
:換頁符,光標移到下一頁。在現代系統上,這已經反映不出來了,行為改成類似於\v
。\n
:換行符。\r
:回車符,光標移到同一行的開頭。\t
:制表符,光標移到下一個水平制表位,通常是下一個8的倍數。\v
:垂直分隔符,光標移到下一個垂直制表位,通常是下一行的同一列。\0
:null 字符,代表沒有內容。注意,這個值不等於數字0。
轉義寫法還能使用八進制和十六進制表示一個字符。
\nn
:字符的八進制寫法,nn
為八進制值。\xnn
:字符的十六進制寫法,nn
為十六進制值。
char x = 'B';
char x = 66;
char x = '\102'; // 八進制
char x = '\x42'; // 十六進制
上面示例的四種寫法都是等價的。
整數類型
簡介
整數類型用來表示較大的整數,類型聲明使用int
關鍵字。
int a;
上面示例聲明了一個整數變量a
。
不同計算機的int
類型的大小是不一樣的。比較常見的是使用4個字節(32位)存儲一個int
類型的值,但是2個字節(16位)或8個字節(64位)也有可能使用。它們可以表示的整數范圍如下。
- 16位:-32,768 到 32,767。
- 32位:-2,147,483,648 到 2,147,483,647。
- 64位:-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。
signed,unsigned
C 語言使用signed
關鍵字,表示一個類型帶有正負號,包含負值;使用unsigned
關鍵字,表示該類型不帶有正負號,只能表示零和正整數。
對於int
類型,默認是帶有正負號的,也就是說int
等同於signed int
。由於這是默認情況,關鍵字signed
一般都省略不寫,但是寫了也不算錯。
signed int a;
// 等同於
int a;
int
類型也可以不帶正負號,只表示非負整數。這時就必須使用關鍵字unsigned
聲明變量。
unsigned int a;
整數變量聲明為unsigned
的好處是,同樣長度的內存能夠表示的最大整數值,增大了一倍。比如,16位的signed int
最大值為32,767,而unsigned int
的最大值增大到了65,535。
unsigned int
里面的int
可以省略,所以上面的變量聲明也可以寫成下面這樣。
unsigned a;
字符類型char
也可以設置signed
和unsigned
。
signed char c; // 范圍為 -128 到 127
unsigned char c; // 范圍為 0 到 255
注意,C 語言規定char
類型默認是否帶有正負號,由當前系統決定。這就是說,char
不等同於signed char
,它有可能是signed char
,也有可能是unsigned char
。這一點與int
不同,int
就是等同於signed int
。
整數的子類型
如果int
類型使用4個或8個字節表示一個整數,對於小整數,這樣做很浪費空間。另一方面,某些場合需要更大的整數,8個字節還不夠。為了解決這些問題,C 語言在int
類型之外,又提供了三個整數的子類型。這樣有利於更精細地限定整數變量的范圍,也有利於更好地表達代碼的意圖。
short int
(簡寫為short
):占用空間不多於int
,一般占用2個字節(整數范圍為-32768~32767)。long int
(簡寫為long
):占用空間不少於int
,至少為4個字節。long long int
(簡寫為long long
):占用空間多於long
,至少為8個字節。
short int a;
long int b;
long long int c;
上面代碼分別聲明了三種整數子類型的變量。
默認情況下,short
、long
、long long
都是帶符號的(signed),即signed
關鍵字省略了。它們也可以聲明為不帶符號(unsigned),使得能夠表示的最大值擴大一倍。
unsigned short int a;
unsigned long int b;
unsigned long long int c;
C 語言允許省略int
,所以變量聲明語句也可以寫成下面這樣。
short a;
unsigned short a;
long b;
unsigned long b;
long long c;
unsigned long long c;
不同的計算機,數據類型的字節長度是不一樣的。確實需要32位整數時,應使用long
類型而不是int
類型,可以確保不少於4個字節;確實需要64位的整數時,應該使用long long
類型,可以確保不少於8個字節。另一方面,為了節省空間,只需要16位整數時,應使用short
類型;需要8位整數時,應該使用char
類型。
整數類型的極限值
有時候需要查看,當前系統不同整數類型的最大值和最小值,C 語言的頭文件limits.h
提供了相應的常量,比如SCHAR_MIN
代表 signed char 類型的最小值-128
,SCHAR_MAX
代表 signed char 類型的最大值127
。
為了代碼的可移植性,需要知道某種整數類型的極限值時,應該盡量使用這些常量。
SCHAR_MIN
,SCHAR_MAX
:signed char 的最小值和最大值。SHRT_MIN
,SHRT_MAX
:short 的最小值和最大值。INT_MIN
,INT_MAX
:int 的最小值和最大值。LONG_MIN
,LONG_MAX
:long 的最小值和最大值。LLONG_MIN
,LLONG_MAX
:long long 的最小值和最大值。UCHAR_MAX
:unsigned char 的最大值。USHRT_MAX
:unsigned short 的最大值。UINT_MAX
:unsigned int 的最大值。ULONG_MAX
:unsigned long 的最大值。ULLONG_MAX
:unsigned long long 的最大值。
整數的進制
C 語言的整數默認都是十進制數,如果要表示八進制數和十六進制數,必須使用專門的表示法。
八進制使用0
作為前綴,比如017
、0377
。
int a = 012; // 八進制,相當於十進制的10
十六進制使用0x
或0X
作為前綴,比如0xf
、0X10
。
int a = 0x1A2B; // 十六進制,相當於十進制的6699
有些編譯器使用0b
前綴,表示二進制數,但不是標准。
int x = 0b101010;
注意,不同的進制只是整數的書寫方法,不會對整數的實際存儲方式產生影響。所有整數都是二進制形式存儲,跟書寫方式無關。不同進制可以混合使用,比如10 + 015 + 0x20
是一個合法的表達式。
printf()
的進制相關占位符如下。
%d
:十進制整數。%o
:八進制整數。%x
:十六進制整數。%#o
:顯示前綴0
的八進制整數。%#x
:顯示前綴0x
的十六進制整數。%#X
:顯示前綴0X
的十六進制整數。
int x = 100;
printf("dec = %d\n", x); // 100
printf("octal = %o\n", x); // 144
printf("hex = %x\n", x); // 64
printf("octal = %#o\n", x); // 0144
printf("hex = %#x\n", x); // 0x64
printf("hex = %#X\n", x); // 0X64
浮點數類型
任何有小數點的數值,都會被編譯器解釋為浮點數。所謂“浮點數”就是使用 m * be 的形式,存儲一個數值,m
是小數部分,b
是基數(通常是2
),e
是指數部分。這種形式是精度和數值范圍的一種結合,可以表示非常大或者非常小的數。
浮點數的類型聲明使用float
關鍵字,可以用來聲明浮點數變量。
float c = 10.5;
上面示例中,變量c
的就是浮點數類型。
float
類型占用4個字節(32位),其中8位存放指數的值和符號,剩下24位存放小數的值和符號。float
類型至少能夠提供(十進制的)6位有效數字,指數部分的范圍為(十進制的)-37
到37
,即數值范圍為10-37到1037。
有時候,32位浮點數提供的精度或者數值范圍還不夠,C 語言又提供了另外兩種更大的浮點數類型。
double
:占用8個字節(64位),至少提供13位有效數字。long double
:通常占用16個字節。
注意,由於存在精度限制,浮點數只是一個近似值,它的計算是不精確的,比如 C 語言里面0.1 + 0.2
並不等於0.3
,而是有一個很小的誤差。
if (0.1 + 0.2 == 0.3) // false
C 語言允許使用科學計數法表示浮點數,使用字母e
來分隔小數部分和指數部分。
double x = 123.456e+3; // 123.456 x 10^3
// 等同於
double x = 123.456e3;
上面示例中,e
后面如果是加號+
,加號可以省略。注意,科學計數法里面e
的前后,不能存在空格。
另外,科學計數法的小數部分如果是0.x
或x.0
的形式,那么0
可以省略。
0.3E6
// 等同於
.3E6
3.0E6
// 等同於
3.E6
布爾類型
C 語言原來並沒有為布爾值單獨設置一個類型,而是使用整數0
表示偽,所有非零值表示真。
int x = 1;
if (x) {
printf("x is true!\n");
}
上面示例中,變量x
等於1
,C 語言就認為這個值代表真,從而會執行判斷體內部的代碼。
C99 標准添加了類型_Bool
,表示布爾值。但是,這個類型其實只是整數類型的別名,還是使用0
表示偽,1
表示真,下面是一個示例。
_Bool isNormal;
isNormal = 1;
if (isNormal)
printf("Everything is OK.\n");
頭文件stdbool.h
定義了另一個類型別名bool
,並且定義了true
代表1
、false
代表0
。只要加載這個頭文件,就可以使用這幾個關鍵字。
#include <stdbool.h>
bool flag = false;
上面示例中,加載頭文件stdbool.h
以后,就可以使用bool
定義布爾值類型,以及false
和true
表示真偽。
字面量的類型
字面量(literal)指的是代碼里面直接出現的值。
int x = 123;
上面代碼中,x
是變量,123
就是字面量。
編譯時,字面量也會寫入內存,因此編譯器必須為字面量指定數據類型,就像必須為變量指定數據類型一樣。
一般情況下,十進制整數字面量(比如123
)會被編譯器指定為int
類型。如果一個數值比較大,超出了int
能夠表示的范圍,編譯器會將其指定為long int
。如果數值超過了long int
,會被指定為unsigned long
。如果還不夠大,就指定為long long
或unsigned long long
。
小數(比如3.14
)會被指定為double
類型。
字面量后綴
有時候,程序員希望為字面量指定一個不同的類型。比如,編譯器將一個整數字面量指定為int
類型,但是程序員希望將其指定為long
類型,這時可以為該字面量加上后綴l
或L
,編譯器就知道要把這個字面量的類型指定為long
。
int x = 123L;
上面代碼中,字面量123
有后綴L
,編譯器就會將其指定為long
類型。這里123L
寫成123l
,效果也是一樣的,但是建議優先使用L
,因為小寫的l
容易跟數字1
混淆。
八進制和十六進制的值,也可以使用后綴l
和L
指定為 Long 類型,比如020L
和0x20L
。
int y = 0377L;
int z = 0x7fffL;
如果希望指定為無符號整數unsigned int
,可以使用后綴u
或U
。
int x = 123U;
L
和U
可以結合使用,表示unsigned long
類型。L
和U
的大小寫和組合順序無所謂。
int x = 123LU;
對於浮點數,編譯器默認指定為 double 類型,如果希望指定為其他類型,需要在小數后面添加后綴f
(float)或l
(long double)。
科學計數法也可以使用后綴。
1.2345e+10F
1.2345e+10L
總結一下,常用的字面量后綴有下面這些。
f
和F
:float
類型。l
和L
:對於整數是long int
類型,對於小數是long double
類型。ll
和LL
:Long Long 類型,比如3LL
。u
和U
:表示unsigned int
,比如15U
、0377U
。
u
還可以與其他整數后綴結合,放在前面或后面都可以,比如10UL
、10ULL
和10LLU
都是合法的。
下面是一些示例。
int x = 1234;
long int x = 1234L;
long long int x = 1234LL
unsigned int x = 1234U;
unsigned long int x = 1234UL;
unsigned long long int x = 1234ULL;
float x = 3.14f;
double x = 3.14;
long double x = 3.14L;
溢出
每一種數據類型都有數值范圍,如果存放的數值超出了這個范圍(小於最小值或大於最大值),需要更多的二進制位存儲,就會發生溢出。大於最大值,叫做向上溢出(overflow);小於最小值,叫做向下溢出(underflow)。
一般來說,編譯器不會對溢出報錯,會正常執行代碼,但是會忽略多出來的二進制位,只保留剩下的位,這樣往往會得到意想不到的結果。所以,應該避免溢出。
unsigned char x = 255;
x = x + 1;
printf("%d\n", x); // 0
上面示例中,變量x
加1
,得到的結果不是256
,而是0
。因為x
是unsign char
類型,最大值是255
(二進制11111111
),加1
后就發生了溢出,256
(二進制100000000
)的最高位1
被丟棄,剩下的值就是0
。
再看下面的例子。
unsigned int ui = UINT_MAX; // 4,294,967,295
ui++;
printf("ui = %u\n", ui); // 0
ui--;
printf("ui = %u\n", ui); // 4,294,967,295
上面示例中,常量UINT_MAX
是 unsigned int 類型的最大值。如果加1
,對於該類型就會溢出,從而得到0
;而0
是該類型的最小值,再減1
,又會得到UINT_MAX
。
溢出很容易被忽視,編譯器又不會報錯,所以必須非常小心。
for (unsigned int i = n; i >= 0; --i) // 錯誤
上面代碼表面看似乎沒有問題,但是循環變量i
的類型是 unsigned int,這個類型的最小值是0
,不可能得到小於0的結果。當i
等於0,再減去1
的時候,並不會返回-1
,而是返回 unsigned int 的類型最大值,這個值總是大於等於0
,導致無限循環。
為了避免溢出,最好方法就是將運算結果與類型的極限值進行比較。
unsigned int ui;
unsigned int sum;
// 錯誤
if (sum + ui > UINT_MAX) too_big();
else sum = sum + ui;
// 正確
if (ui > UINT_MAX - sum) too_big();
else sum = sum + ui;
上面示例中,變量sum
和ui
都是 unsigned int 類型,它們相加的和還是 unsigned int 類型,這就有可能發生溢出。但是,不能通過相加的和是否超出了最大值UINT_MAX
,來判斷是否發生了溢出,因為sum + ui
總是返回溢出后的結果,不可能大於UINT_MAX
。正確的比較方法是,判斷UINT_MAX - sum
與ui
之間的大小關系。
下面是另一種錯誤的寫法。
unsigned int i = 5;
unsigned int j = 7;
if (i - j < 0) // 錯誤
printf("negative\n");
else
printf("positive\n");
上面示例的運算結果,會輸出positive
。原因是變量i
和j
都是 unsigned int 類型,i - j
的結果也是這個類型,最小值為0
,不可能得到小於0
的結果。正確的寫法是寫成下面這樣。
if (j > i) // ....
sizeof 運算符
sizeof
是 C 語言提供的一個運算符,返回某種數據類型或某個值占用的字節數量。它的參數可以是數據類型的關鍵字,也可以是變量名或某個具體的值。
// 參數為數據類型
int x = sizeof(int);
// 參數為變量
int i;
sizeof(i);
// 參數為數值
sizeof(3.14);
上面的第一個示例,返回得到int
類型占用的字節數量(通常是4
或8
)。第二個示例返回整數變量占用字節數量,結果與前一個示例完全一樣。第三個示例返回浮點數3.14
占用的字節數量,由於浮點數的字面量一律存儲為 double 類型,所以會返回8
,因為 double 類型占用的8個字節。
sizeof
運算符的返回值,C 語言只規定是無符號整數,並沒有規定具體的類型,而是留給系統自己去決定,sizeof
到底返回什么類型。不同的系統中,返回值的類型有可能是unsigned int
,也有可能是unsigned long
,甚至是unsigned long long
,對應的printf()
占位符分別是%u
、%lu
和%llu
。這樣不利於程序的可移植性。
C 語言提供了一個解決方法,創造了一個類型別名size_t
,用來統一表示sizeof
的返回值類型。該別名定義在stddef.h
頭文件(引入stdio.h
時會自動引入)里面,對應當前系統的sizeof
的返回值類型,可能是unsigned int
,也可能是unsigned long
。
C 語言還提供了一個常量SIZE_MAX
,表示size_t
可以表示的最大整數。所以,size_t
能夠表示的整數范圍為[0, SIZE_MAX]
。
printf()
有專門的占位符%zd
或%zu
,用來處理size_t
類型的值。
printf("%zd\n", sizeof(int));
上面代碼中,不管sizeof
返回值的類型是什么,%zd
占位符(或%zu
)都可以正確輸出。
如果當前系統不支持%zd
或%zu
,可使用%u
(unsigned int)或%lu
(unsigned long int)代替。
類型的自動轉換
某些情況下,C 語言會自動轉換某個值的類型。
賦值運算
賦值運算符會自動將右邊的值,轉成左邊變量的類型。
(1)浮點數賦值給整數變量
浮點數賦予整數變量時,C 語言直接丟棄小數部分,而不是四舍五入。
int x = 3.14;
上面示例中,變量x
是整數類型,賦給它的值是一個浮點數。編譯器會自動把3.14
先轉為int
類型,丟棄小數部分,再賦值給x
,因此x
的值是3
。
這種自動轉換會導致部分數據的丟失(3.14
丟失了小數部分),所以最好不要跨類型賦值,盡量保證變量與所要賦予的值是同一個類型。
注意,舍棄小數部分時,不是四舍五入,而是整個舍棄。
int x = 12.99;
上面示例中,x
等於12
,而不是四舍五入的13
。
(2)整數賦值給浮點數變量
整數賦值給浮點數變量時,會自動轉為浮點數。
float y = 12 * 2;
上面示例中,變量y
的值不是24
,而是24.0
,因為等號右邊的整數自動轉為了浮點數。
(3)窄類型賦值給寬類型
字節寬度較小的整數類型,賦值給字節寬度較大的整數變量時,會發生類型提升,即窄類型自動轉為寬類型。
比如,char
或short
類型賦值給int
類型,會自動提升為int
。
char x = 10;
int i = x + y;
上面示例中,變量x
的類型是char
,由於賦值給int
類型,所以會自動提升為int
。
(4)寬類型賦值給窄類型
字節寬度較大的類型,賦值給字節寬度較小的變量時,會發生類型降級,自動轉為后者的類型。這時可能會發生截值(truncation),系統會自動截去多余的二進制位,導致難以預料的結果。
int i = 321;
char ch = i; // ch 的值是 65 (321 - 256)
上面例子中,變量ch
是char
類型,寬度是8個二進制位。變量i
是int
類型,將i
賦值給ch
,后者只能容納i
(二進制形式為101000001
,共9位)的后八位,前面多出來的二進制位被丟棄,保留后八位就變成了01000001
(十進制的65,相當於字符A
)。
浮點數賦值給整數類型的值,也會發生截值,浮點數的小數部分會被截去。
double pi = 3.14159;
int i = pi; // i 的值為 3
上面示例中,i
等於3
,pi
的小數部分被截去了。
混合類型的運算
不同類型的值進行混合計算時,必須先轉成同一個類型,才能進行計算。轉換規則如下:
(1)整數與浮點數混合運算時,整數轉為浮點數類型,與另一個運算數類型相同。
3 + 1.2 // 4.2
上面示例是int
類型與float
類型的混合計算,int
類型的3
會先轉成float
的3.0
,再進行計算,得到4.2
。
(2)不同的浮點數類型混合運算時,寬度較小的類型轉為寬度較大的類型,比如float
轉為double
,double
轉為long double
。
(3)不同的整數類型混合運算時,寬度較小的類型會提升為寬度較大的類型。比如short
轉為int
,int
轉為long
等,有時還會將帶符號的類型signed
轉為無符號unsigned
。
下面例子的執行結果,可能會出人意料。
int a = -5;
if (a < sizeof(int))
do_something();
上面示例中,變量a
是帶符號整數,sizeof(int)
是size_t
類型,這是一個無符號整數。按照規則,signed int 自動轉為 unsigned int,所以a
會自動轉成無符號整數4294967291
(轉換規則是-5
加上無符號整數的最大值,再加1),導致比較失敗,do_something()
不會執行。
所以,最好避免無符號整數與有符號整數的混合運算。因為這時 C 語言會自動將signed int
轉為unsigned int
,可能不會得到預期的結果。
整數類型的運算
兩個相同類型的整數運算時,或者單個整數的運算,一般來說,運算結果也屬於同一類型。但是有一個例外,寬度小於int
的類型,運算結果會自動提升為int
。
unsigned char a = 66;
if ((-a) < 0) printf("negative\n");
else printf("positive\n");
上面示例中,變量a
是 unsigned char 類型,這個類型不可能小於0,但是-a
不是 unsigned char 類型,會自動轉為 int 類型,導致上面的代碼輸出 negative。
再看下面的例子。
unsigned char a = 1;
unsigned char b = 255;
unsigned char c = 255;
if ((a - 5) < 0) do_something();
if ((b + c) > 300) do_something();
上面示例中,表達式a - 5
和b + c
都會自動轉為 int 類型,所以函數do_something()
會執行兩次。
函數
函數的參數和返回值,會自動轉成函數定義里指定的類型。
int dostuff(int, unsigned char);
char m = 42;
unsigned short n = 43;
long long int c = dostuff(m, n);
上面示例中,參數變量m
和n
不管原來的類型是什么,都會轉成函數dostuff()
定義的參數類型。
下面是返回值自動轉換類型的例子。
char func(void) {
int a = 42;
return a;
}
上面示例中,函數內部的變量a
是int
類型,但是返回的值是char
類型,因為函數定義中返回的是這個類型。
類型的顯式轉換
原則上,應該避免類型的自動轉換,防止出現意料之外的結果。C 語言提供了類型的顯式轉換,允許手動轉換類型。
只要在一個值或變量的前面,使用圓括號指定類型(type)
,就可以將這個值或變量轉為指定的類型,這叫做“類型指定”(casting)。
(unsigned char) ch
上面示例將變量ch
轉成無符號的字符類型。
long int y = (long int) 10 + 12;
上面示例中,(long int)
將10
顯式轉為long int
類型。這里的顯示轉換其實是不必要的,因為賦值運算符會自動將右邊的值,轉為左邊變量的類型。
可移植類型
C 語言的整數類型(short、int、long)在不同計算機上,占用的字節寬度可能是不一樣的,無法提前知道它們到底占用多少個字節。
程序員有時控制准確的字節寬度,這樣的話,代碼可以有更好的可移植性,頭文件stdint.h
創造了一些新的類型別名。
(1)精確寬度類型(exact-width integer type),保證某個整數類型的寬度是確定的。
int8_t
:8位有符號整數。int16_t
:16位有符號整數。int32_t
:32位有符號整數。int64_t
:64位有符號整數。uint8_t
:8位無符號整數。uint16_t
:16位無符號整數。uint32_t
:32位無符號整數。uint64_t
:64位無符號整數。
上面這些都是類型別名,編譯器會指定它們指向的底層類型。比如,某個系統中,如果int
類型為32位,int32_t
就會指向int
;如果long
類型為32位,int32_t
則會指向long
。
下面是一個使用示例。
#include <stdio.h>
#include <stdint.h>
int main(void) {
int32_t x32 = 45933945;
printf("x32 = %d\n", x32);
return 0;
}
上面示例中,變量x32
聲明為int32_t
類型,可以保證是32位的寬度。
(2)最小寬度類型(minimum width type),保證某個整數類型的最小長度。
- int_least8_t
- int_least16_t
- int_least32_t
- int_least64_t
- uint_least8_t
- uint_least16_t
- uint_least32_t
- uint_least64_t
上面這些類型,可以保證占據的字節不少於指定寬度。比如,int_least8_t
表示可以容納8位有符號整數的最小寬度的類型。
(3)最快的最小寬度類型(fast minimum width type),可以使整數計算達到最快的類型。
- int_fast8_t
- int_fast16_t
- int_fast32_t
- int_fast64_t
- uint_fast8_t
- uint_fast16_t
- uint_fast32_t
- uint_fast64_t
上面這些類型是保證字節寬度的同時,追求最快的運算速度,比如int_fast8_t
表示對於8位有符號整數,運算速度最快的類型。這是因為某些機器對於特定寬度的數據,運算速度最快,舉例來說,32位計算機對於32位數據的運算速度,會快於16位數據。
(4)可以保存指針的整數類型。
intptr_t
:可以存儲指針(內存地址)的有符號整數類型。uintptr_t
:可以存儲指針的無符號整數類型。
(5)最大寬度整數類型,用於存放最大的整數。
intmax_t
:可以存儲任何有效的有符號整數的類型。uintmax_t
:可以存放任何有效的無符號整數的類型。
上面的這兩個類型的寬度比long long
和unsigned long
更大。