Chapter2 C與C++——2.1 基礎語法

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章