第二章 常量、變量和表達式
1. 繼續Hello World
首先注釋可以跨行,也可以穿插在程序之中;
2.1.帶更多注釋的Hello World
第一個注釋跨了四行,頭尾兩行是注釋的界定符(Delimiter)/*和*/,中間兩行開頭的*號(Asterisk)並沒有特殊含義,只是為了看起來整齊。
使用注釋需要注意兩點:
- 注釋不能嵌套(Nest)使用,就是說一個注釋的文字中不能再出現/*和*/了,例如/* text1 /* text2 */ text3 */是錯誤的,編譯器只把/* text1 /* text2 */看成注釋,后面的text3 */無法解析,因而會報錯。
- 有的C代碼中有類似// comment的注釋,兩個/斜線(Slash)表示從這里直到該行末尾的所有字符都屬於注釋,這種注釋不能跨行,也不能穿插在一行代碼中間。這是從C++借鑒的語法,再C99中被標准化。
像“Hello world. \n”這種由雙引號(Double Quote)引起來的一串字符稱為字符串字面值(String Literal),或者簡稱字符串。
C標准規定的轉義字符
如果再字符串字面值中要表示單引號和問號,既可以使用轉義序列\’和\?,也可以直接用字符‘和?,而要表示\或“則必須使用轉義序列,因為\字符表示轉義而不表示它的字面含義,“表示字符串的界定符而不表示它的字面含義。可見轉義序列有兩個作用:一是把普通字符轉義成特殊字符,例如把字母n轉義成換行符;二是把特殊字符轉義成普通字符,例如\和”是特殊字符,轉移后取它的字面值。Windows上的文本文件用\r\n做行分隔符,許多應用層網絡協議(如HTTP)也用\r\n做行分割符,而Linux和各種UNIX上的文本文件只用\n做行分隔符。
2. 常量
常量(Constant)是程序中最基本的元素,有字符(Character)常量、整數(Integer)常量、浮點數(Floating Point)常量和枚舉常量。
下面看一個例子:
字符常量要用單引號括起來,例如上面的‘}‘,注意單引號只能括一個字符而不能像雙引號那樣括一串字符,字符常量也可以是一餓轉義序列,例如’\n’,這時雖然單引號括了兩個字符,但實際上只表示一個字符。和字符串字面值中使用轉義序列有一點區別,如果再字符常量中要表示雙引號“和問號?,既可以使用轉義序列\”和\?,也可以直接用字符”和?,而要表示‘和\則必須使用轉義序列。
輸出結果為:
printf中的第一個字符串稱為格式化字符串(Format String),它規定了后面幾個常量以何種格式插入到這個字符串中,在格式化字符串中%號(Percent Sign)后面加上字母c、d、f分別表示字符型、整形和浮點型的轉換說明(Conversion Specification),轉換說明只在格式化字符串中占個位置,並不出現在最終的打印結果中,這種用法通常叫做占位符(Placeholder)。這也是一種字面意思與真實意思不同的情況,但是轉換說明和轉義序列又有區別:轉義序列是編譯時處理的,而轉換說明時在運行時調用printf函數處理的。源文件中的字符串字面值是”character: %c\ninteger: %dfloating point: %f\n”,\n占兩個字符,而編譯之后保存在可執行文件中的字符串是character: %c換行integer: %d換行floating point: %f換行,\n已經被替換成一個換行符,而%c不變,然而在運行是這個字符串被傳給printf,printf再把其中的%c,%d,%f解釋成轉換說明。
3. 變量
變量(Variable)是編程語言中最重要的概念之一,變量是計算機存儲器中的一塊命名的空間,可以再里面存儲的一個值(Value),存儲的值是可以隨時變的,比如這次存個字符‘a’下次存個字符‘b’,正因為變量的值可以隨時變所以才叫變量。
應該給變量起有意義的名字,兩個同樣類型的變量可以定義在同一行,如int hour,minute;給變量起名有一定的限制,C語言規定必須以字母或下划線_(Underscore)開頭,后面可以跟若干個字母、數字、下划線,但不能有其他字符。例如這些是合法的變量名:Abc、_abc_、_123。但這些是不合法的變量名:3abc、ab$。其實這個規則不僅適用於變量名,也適應於所有可以由程序員起名的語法元素,例如以后要講的函數名、宏定義、結構體、成員名等,在C語言中這些統稱為標識符(Identifier)。
還有一點要注意,一般來說應避免使用以下划線開頭的標識符,以下划線開頭的標識符只要不和C語言關鍵字沖突都是合法的,但是往往被編譯器用作一些功能擴展,C標准庫也定義了很多以下划線開頭的標識符,所以除非你對編譯器和C標准庫特別清楚,一般應避免使用這種標識符,以免造成命名沖突。
4. 賦值
定義了變量之后,我們要把值存到它們所表示的存儲空間里,可以用賦值(Assignment)語句實現。
注意變量一定要先聲明后使用,編譯器必須先看到變量聲明,才知道firstletter、hour和minute是變量名,各自代表一塊存儲空間。另外,變量聲明中的類型表明這個變量代表多大的一塊存儲空間,這樣編譯器才知道如何讀寫這塊存儲空間。
定義一個變量,就是分配一塊存儲空間並給它命名;給一個變量賦值,就是把一個值保存到這塊存儲空間中。變量的定義和賦值也可以一步完成,這稱為變量的初始化(Initialization),例如要達到上面代碼的效果也可以這樣寫:
在初始化語句中,等號右邊的值叫做Initializer,例如上面的‘a’、11和59。注意,初始化是一種特殊的變量定義語句,而不是一種賦值語句。如果在紙上“跑“一個程序,可以用一個框表示變量的存儲空間,在框的外邊標上變量名,在框里記上它的值,如下圖所示。
你可以用不同形狀的框表示不同類型的變量,這樣可以提醒你給變量賦的值必須符合它的類型。如果所賦的值和變量的類型不符會導致編譯器報警或報錯(這是一種語義錯誤),例如:
注意第三個語句,把“59“賦給minute看起來像是對的,但是類型不對,字符串不能賦給整形變量。既然可以為變量的存儲空間賦值,就應該可以把值取出來用,現在我們取出這些變量的值用printf打印:
也就是說,變量名除了用在等號左邊表示賦值之外,用在別的地方都表示把它的存儲空間中的值取出來替換在那里。不同類型的變量所占的存儲空間大小是不同的,存儲表示方式也不同,最小存儲單位是字節(Byte)。
5. 表達式
常量和變量都可以參與加減乘除運算,例如1+1、hour-1、hour*60+minute、minute/60等。這里的+-*/稱為運算符(Operator),而參與運算的常量和變量稱為操作數(Operand),上面四個由運算符和操作數組成的算是稱為表達式(Expression)。
我們定義:在任意表達式后面加個;號也是一種語句,稱為表達式語句。例如:
這是個合法的語句,但這個語句在程序中起不到任何作用,把hour的值和minute的值取出來加乘,得到的計算結果卻沒有保存,白算了一通。再比如:
這個語句就很有意義,把計算結果保存在另一個變量total_minute里。事實上等號也是一種運算符,賦值語句就是一種表達式語句,等號的優先級比+和*都低,所以先算出等號右邊的結果然后才做賦值操作,整個表達式total_minute = hour*60 + minute加個;號構成一個語句。任何表達式都有值和類型兩個基本屬性。hour*60 + minute的值是由三個int型的操作數計算出來的,所以這個表達式的類型也是int型,它的值是多少呢?C語言規定等號運算符的計算結果就是等號左邊被賦予的那個值,所以這個表達式的值和hour*60 + minute的值相同,也和total_minute的值相同。等號運算符還有一個和+-*/不同的特性,如果一個表達式中出現多個等號,不是從左到右計算而是從右到左計算,例如:
計算的順序是先算hour*60 + minute得到一個結果,然后算右邊的等號,就是把hour*60 + minute的結果賦給變量total_minute,這個結果同時也是整個表達式total_minute = hour*60 + minute的值,再算左邊的等號,即把這個值再賦給變量total。同樣優先級的運算符也是從左到右計算還是從右到左計算稱為運算符的結合性(Associativity)。+-*/是左結合的,等號是右結合的。
比如在一條語句中完成計算、賦值和打印功能:
理解組合(Composition)規則是理解語法規則的關鍵所在,正因為可以根據語法規則任意組合,我們才可以用簡單的常量、變量、表達式、語句搭建出任意復雜的程序。
根據語法規則組合出來的表達式在語義上並不總是正確的。例如:
等號左邊的表達式要求表示一個存儲位置而不是一個值,這是等號運算符和+-*/運算符的又一個顯著不同。有的表達式既可以表示一個存儲位置也可以表示一個值,而有的表達式只能表示值,不能表示存儲位置,minute + 1這個表達式就不能表示存儲位置,放在等號左邊是語義錯誤。表達式所表示的位置稱為左值(lvalue)(允許放在等號左邊),而以前我們所說的表達式的值稱為右值(rvalue)(只能放在等號右邊)。上面的話換一種說法就是:有的表達式既有左值也有右值,而有的表達式只有右值。
如果三個變量int a, b, c;,表達式a = b = c是合法的,先求b = c的值,再把這個值賦給a,而表達式(a = b) = c是不合法的,先求(a = b)的值沒問題,但(a = b)這個表達式不能在做左值了,因此放在 = c的等號左邊是錯的。
關於整數除法運算有一點特殊之處:
執行結果是11 and 0 hours,也就是說59/60得0,這是因為兩個int型操作數相除的表達式仍為int型,只能保存計算結果的整數部分,即使小數部分是0.98也要舍去。
要得到更精確的結果可以這樣:
在第二個printf中,表達式是minute / 60.0,60.0是double型的,/運算符要求左右兩邊的操作數類型一致,而現在並不一致。C語言規定了一套隱式類型轉換規則,在這里編譯器自動把左邊的minute也轉成double型來計算,整個表達式的值也是double型的,再格式化字符串中應該用%f轉換說明與之對應。
6. 字符類型與字符編碼
字符常量或字符型變量也可以當作整數參與運算,例如:
執行的結果是b.
符號在計算機內部也用數字表示,每個字符在計算機內部用一個整數表示,稱為字符編碼(Character Encoding),目前最常用的是ASCII碼。之前我們說“整型”是指int型,而現在我們知道char型本質上就是整數,只不過范圍比int型小,所以以后我們把char型和int型統稱為整數類型(Inteager Type)或簡稱整型。
字符’a’~’z’、’A’~’Z’、‘0‘~’9’的ASCII碼都是連續的,因此表達式’a’+25和‘z’的值相等,’0‘+9的值也相等。注意’0‘~’9‘的ASCII碼是十六進制的30~39,和整數值0~9是不相等的。
字符也可以用ASCII碼轉義序列表示,這種轉義序列由\加上1~3個八進制數字組成,或者由\x或大寫\x加上1~2個十六進制數字組成,可以用在字符常量或字符串字面值中。例如‘\0’表示NUL字符,‘\11’或’\x9’表示Tab字符,“\11”或”\x9”表示由Tab字符組成的字符串。