2.1 基礎語法
2.1.1 關鍵字
以下單詞或字符在C語言中有特殊含義,稱作關鍵字:
- include
- define
- ifdef
- ifndef
- endif
- extern
- typedef
- static
- const
- struct
- union
- void
- signed
- unsigned
- char
- short
- int
- long
- float
- double
- if
- else
- for
- do
- while
- break
- continue
- goto
以上關鍵字的作用將在後續章節講解。
2.1.2 特殊符號
C語言中常會用到以下符號:
- 賦值運算:=、+=、-=、*=、/=、%=、&=、|=
- 算數運算:+、-、*、/、%、++、–
- 比較運算符:==、>=、<=、!=、>、<
- 邏輯運算:&&、||、!
- 位運算:&、|、~、^、>>、<<
- 指針運算:*、&
- 其他:;、#、{、}、[、]、0x、0b、//、/*、*/
以上符號中的運算符將在學習數據類型之後進行說明。
2.1.3 註釋
註釋是一些說明性的文字,他並不影響程序的邏輯、執行和運算。僅僅是幫助編程開發人員更好閱讀和理解代碼。
C語言有行註釋和塊註釋,下面是幾個行註釋:
// 這是一個行註釋。
// This is a line comment.
下面是一個塊註釋:
/*
* @author: lion chen [email protected]
* @brief: 這是一個Doxygen風格的塊註釋。
*/
C語言的編碼和註釋有很多種風格,你會發現每種風格有各自的優缺點。你可以嘗試多種風格,但成熟的軟件系統會採用統一的風格,這樣的要求經常由《編碼規範》來約定。本教程主要使用 Doxygen 風格的註釋。
2.1.4 字面常量
常量可以理解爲固定不變的量,是與變量相對的概念。常量一經申明變不允許再發生改變。以下均是常量:
- 16、0xFA90、0b1010
- 25.1
- “Have Fun!”、‘x’、“15996699996”、“2*3.1415926535898*r”
常量的不變性是指如下賦值語句都是錯誤的:
- “var”=“Have Fun!”
- “var”=1024
- 16=0x16
而下列寫法是可以編譯通過的:
- “var”==“Have Fun!”
- “var”!=1024
- 16<0x16
因爲以上並非賦值語句,而是比較語句。通過英文雙引號表達的是字符串變量,他們可以是一串字符。使用英文單引號表達的是字符常量,他只可以包含一個字符。而類似於16、25.1這種的是數字量。數字量分爲整數和浮點數,他們有多種方式去表達方式。
2.1.5 整數的表達方式
除了常用的十進制方式以外,在C語言甚至其他語言中,還經常用到二進制和十六進制數。
- 0b 開頭的表達二進制數,僅使用0、1表達
- 0x 開頭的表達十六進制數,除09外,使用AF來表達10~15
二進制、十進制、十六進制間通過 8421BCD 碼進行轉換。
- 二進制:0b 1010
- 十進制:1*23+0*22+1*21+0*20
- 也就是:1*8+0*4+1*2+0*1
且每 4 位二進制數可表示一位十六進制數:
- 二進制:0b 1010 0101
- 十六進制:0xA5
2.1.6 變量
在C語言中,可以使用字符來表示數字量或字符串等。就好像數學裏用x表示一些數那樣。這樣的量,可以被反覆修改,被稱作變量。變量只可以使用英文字符或下劃線“_”開頭,可包含0~9的數字,不可包含其他字符。以下是一些變量:
- int aint
- unsigned long _along
- double flt0, flt1, flt2
- void* pointer
變量需要被定義才能夠使用,在定義變量的時候,在變量前面的用於描述變量的C關鍵字表示了變量的類型和作用範圍。
例如 static 表示變量是靜態的,而 short 表示了有符號的32位整數。
所謂有符號、無符號,即變量所表示的數是否包含負數。
- unsigned char: 0~255
- signed char:-128~127
- char = signed char
char是8位數,可表示2^8=256個數,無符號的char從0開始,取256個整數,最大是255。當表示有符號數時,則用128個數表示-1-128,另一半表示0127。
其他類型的位數爲:
- short 16位
- int 32位
- long 與總線位寬有關,不低於32位
- long long 64位
- float 32位浮點數,非常不精確
- double 64位浮點數,精確
另一種變量是如下定義的:
int* pointer;
這種變量被稱作指針,或指針變量,後續會進行詳細說明,並且我們會不斷的提到它。
之前我們說變量的值可以被反覆修改,也就是可以這樣做:
int aint;
aint = -56; // 初始化賦值。
aint += 3;
aint ++
這種做法叫做變量賦值。有時候我們需要在定義變量的時候就爲變量賦值,可以這樣做:
double aflt = 3.14;
變量的第一次賦值被稱作初始化。注意,如果一個變量沒有經過初始化就被使用了,那是很危險的,尤其是沒有初始化的指針變量。
通常,在定義一個變量的同時,我們就聲明瞭他,但是這個變量通常只在當前源文件中可見,而一個軟件項目會包含很多源文件。如果我們想在另一個源文件中使用這個變量,就需要聲明他。例如,在 a.c 這個源文件中定義和初始化變量:
/**
* @file: a.c
*/
short ashort=16;
然後我們在 b.c 文件中聲明並使用他:
/**
* @file: b.c
*/
extern short ashort;
ashort++;
經常會遇到需要將一種類型變量賦值給另一種變量的情況,在賦值的過程中,將發生類型轉換。如果將位數少的變量賦值給位數多的變量,這個轉換過程將會很自然。
char a = 25;
long b = a; // b=25.
但如果反過來,將位數多的變量賦值給位數少的,將會截取低位的部分數據進行賦值。
short a = 0xAA55;
char b = a; // b=0x55.
事實上,在進行類型轉換時,如果按照上面的寫法,將會產生編譯警告。正確的做法是顯式的明確指出這裏要進行類型轉換。這叫做類型強制轉換。
short a = 0xAA55;
char b = (char)a; // 強制轉換成char型.
在進行上述類型轉換時,並沒有改變a的類型,只是賦值了一份a的值,然後進行擴展或裁剪,再將結果賦值給b。而a本身的值和類型並未發生任何變化。
2.1.7 符號常量
有時候我們希望用一個符號來代替某個字面常量,這看起來很像一個不允許改變值的變量,我們用 const 關鍵字來修飾它,使之成爲符號常量。例如:
const long cnvar0=666;
const char cnvar1='C';
一個符號常量只能在定義時被初始化,並且不能夠被再次賦值。例如下面做法是錯的:
const int cnint = 256;
cnint++;
2.1.8 運算符
c語言運算符主要包括賦值運算、算數運算、邏輯運算、位運算、比較運算等。運算符主要涉及到優先級,結合性以及前加加和後加加的問題。例如:
int val=5, mask=0b0100;
printf("val=%d.\n", val++); // val=5.
printf("val=%d.\n", ++val); // val=7.
if(val<=10 && val>=5)
{
printf("True\n");
}
val &= mask; // val=4.
TODO: 有一個非常常用的特殊運算符,即sizeof運算符。
2.1.9 宏
通過使用宏,可以指導c編譯器做一些特別的工作。例如使用define關鍵字來做一些替代的工作:
#define BASE_ADDR (0x25)
#define REG1_OFFSET (0x01)
#define REG2_OFFSET (0x02)
printf("Register 1 addr=0x%x.\n", BASE_ADDR+REG1_OFFSET); // addr=0x25+0x01
printf("Register 2 addr=0x%x.\n", BASE_ADDR+REG2_OFFSET); // addr=0x25+0x02
這稱作宏替換。宏替換可以提高程序的可移植性,例如上述例子中,只要修改 #define BASE_ADDR (0x25) 一處便可以修改所有寄存器的地址。
注意,定義宏的時候一般不加分號,這是因爲在進行宏展開時,會將宏名替換爲後邊的全部,如果有多餘符號存在,則會產生如下問題:
#define BASE_ADDR 0x25;
#define REG1_OFFSET 0x01
#define REG1_ADDR (BASE_ADDR+REG1_OFFSET) // 替換後 REG1_ADDR 爲 0x25;+0x01,而這不是一條有效的c語言語句.
括號也在宏定義中擔當着重要角色,我們看下面的例子:
#define BASE_ADDR 0x25
#define REG1_OFFSET 0x01
#define REG1_ADDR_0 (BASE_ADDR+REG1_OFFSET)
#define REG1_ADDR_1 BASE_ADDR+REG1_OFFSET
int a = REG1_ADDR_0*2; // a=(0x25+0x01)*2=76
int b = REG1_ADDR_1*2; // b=0x25+0x01*2=39
另外,我們很少使用小寫英文字母作爲宏的名稱。
很多參考中將宏理解爲編譯期間的文本替換,編譯器會將宏展開,這與程序執行期間發生的事情有本質差別。
2.1.10 typedef
typedef也是一個能夠有效提高程序可維護性的關鍵字。它允許你聲明一個自定義的類型。例如:
typedef unsigned short U16
U16 a = 0x55AA; // 等於 unsigned short a.
需要注意的是,typedef與宏替換很像,很容易將二者混淆。關鍵的區別在於,宏替換僅僅是簡單的字面替換,而typedef僅用於聲明某個類型。比如:
#define MY_STRUCT struct A{...}
MY_STRUCT x;
MY_STRUCT y;
這樣的代碼會產生奇異,由於宏的字面替換,最終聲明瞭兩個一樣的結構體類型,並分別用它們去定義x和y。編譯器很難區分x和y的類型,它們看起來相同,卻又不同。這樣的問題我們用typedef來解決:
typedef struct A{} MY_STRUCT;
MY_STRUCT x;
MY_STRUCT y;
由於typedef不會產生字面替換,僅僅是聲明瞭新的類型,因此,此處與定義兩個普通變量沒有差別,並且不存在上述的問題。
關於結構體這一複合類型,將在後續章節詳細說明。
2.1.11 指針
前文提及的變量,無論是哪種類型的,在程序運行起來之後,都會佔用一定的內存空間。所佔用具體空間的大小,與其類型有關。而每個變量所在的位置,便是內存地址。
我們可以通過變量名來獲取變量,此外,也可以通過變量地址來獲取變量。這是通過指針操作來實現的:
// 假設系統是從低位開始尋址.
unsigned int a = 0x11223344;
char* pa = (char*)&a; // 通過 & 符號取變量 a 的地址. 指針 pa 的值便是變量 a 的首地址.
printf("%d.\n", *pa); // 0x44.
pa++;
printf("%d.\n", *pa); // 0x33.
pa++;
printf("%d.\n", *pa); // 0x22.
pa++;
printf("%d.\n", *pa); // 0x11.
pa++;
在變量類型後面加*號,便是定義指針類型變量。根據類型的不同,指針的類型也不同,有long*,short*等。
可以通過&符號來取地址,由於指針代表了變量地址,因此,&取出的地址可以賦值給指針變量。
我們說指針變量,意味着指針本身也是一個變量,是變量就有存儲空間和存儲地址。存儲空間便是類型的長度,指針代表了內存地址,因此其長度總是與內存總線寬度一致。如32位機則指針變量長度爲4,64位機爲8。
指針也有存儲地址,意味着可以通過另一個指針來索引指針變量,另一個指針便成爲了二級指針。
2.1.12 語句
只有詞法沒有句法就無法構成完整的語言。因此要學習c語言的語句。
與自然語言的一個區別在於,c語言通常以英文分號作爲一句話的結束。
int a; // 這句話說完了.
a = 16; // 這句話也說完了.
a++ // 這句話沒說完...
c語言有賦值語句,判斷語句,條件語句,循環語句,分支語句等。這些名稱與語句的功能相對應,因此很好區別。
我們會把一些語句用大括號包裹起來,形成語句塊。
{
int a, b, c;
a = 60;
b = 3;
c = a*b;
}
語句塊非常有用,經常出現在條件語句,循環語句或者分支語句的後面。之後講到函數時,你會注意到,函數體本身就是一個語句塊。
2.1.13 c/c++文件
c/c++ 語言程序被寫入到文件中,這些文件有特殊的擴展名,C 文件擴展名是“.c”和“.h”;C++ 文件擴展名爲“.cpp”和“.h”。
其中 .h 文件稱作頭文件,其餘都是源碼文件。源文件是我們編寫程序實體的主要文件,變量和函數的定義都在源文件中。
項目很容易產生多個源文件,所以也常會出現在源文件 a.c 中需要訪問 b.c 中的變量或者函數的情況。
這時候通過以下方法來實現。
#include <b.h>
/**
* 或者 #include "b.h"
* 主要差別是搜索頭文件的路徑不同。
* 可以指定頭文件的絕對或相對路徑如下:
* #include "/path/b.h"
*/
其中,b.h 包含了 b.c 中可以被外界訪問到的變量或函數等資源的聲明。
這裏體現出了定義和聲明的區別。定義一個變量或者函數,會爲它分配實際的存儲空間,還要實現函數的具體功能。而聲明只是在告訴大家:“嘿,這裏有個變量 a,那裏有個函數 void fun1(void)!”。
所以,聲明不會分配存儲空間,也不需要實現函數的具體功能。
頭文件中包含了很多的聲明,define 或者 typedef,它告訴外界存在這些資源。如果你想在源碼中使用這些資源,你只要 include 這個頭文件即可。
一旦包含了某個頭文件,便可以使用這個頭文件中聲明的內容。需要注意的是,頭文件不但可以被源文件包含,被頭文件包含,還可以被多次包含,這就有可能使得這個頭文件被重複包含。重複包含是不允許的,可以使用 ifdef 等宏避免。
/**
* @file: a.h
*/
#ifndef A_H
#define A_H
extern int a;
void func1(int x);
#endif // A_H