C 語言入門 / C 變數、宣告和常量
C 語言支援靈活的變數型別和結構,以及常用的算術和數學函式,以及一些 C 語言特有的有趣運算子。本章將詳細介紹它們,最後簡要討論預處理器命令。
C 語言包含以下基本資料型別
| 型別 | 用途 | 大小(位) | 範圍 |
|---|---|---|---|
| char | 字元 | 8 | -128 到 127 |
| unsigned char | 字元 | 8 | 0 到 255 |
| short | 整數 | 16 | -32,768 到 32,767 |
| unsigned short | 整數 | 16 | 0 到 65,535 |
| int | 整數 | 32 | –2,147,483,648 到 2,147,483,647 |
| unsigned int | 整數 | 32 | 0 到 4,294,967,295 |
| long | 整數 | 32 | -2,147,483,648 到 2,147,483,647 |
| unsigned long | 整數 | 32 | 0 到 4,294,967,295 |
| float | 實數 | 32 | 1.2 × 10−38 到 3.4 × 1038 |
| double | 實數 | 64 | 2.2 × 10−308 到 1.8 × 10308 |
| long double | 實數 | 128 | 3.4 × 10−4932 到 1.2 × 104932 |
這些是代表值。定義在不同的實現之間往往有所不同。例如,在某些系統中,“int” 為 16 位,“long double” 可能為 64 位。唯一保證的是優先順序
short <= int <= long float <= double <= long double
C 語言的一個特點是,雖然存在“unsigned char” 資料型別,但出於某種原因,許多處理單個字元的函式要求變數被宣告為“int” 或“unsigned int”。
宣告的形式為
int myval, tmp1, tmp2;
unsigned int marker1 = 1, marker2 = 10;
float magnitude, phase;
變數名至少可以是 31 個字元長,雖然現代編譯器始終支援更長的名稱。變數名可以由字母、數字和 "_"(下劃線)字元組成;第一個字元必須是字母。雖然可以在變數名中使用大寫字母,但傳統的 C 語言用法將大寫字母保留用於常量名稱。以 "_" 開頭的字元也是合法的,但通常保留用於標記內部庫名稱。
C 語言允許在同一語句中宣告多個變數,用逗號分隔宣告。可以在宣告時初始化變數。宣告的常數值可以用多種格式宣告
128 decimal int 256u decimal unsigned int 512l decimal long int 0xAF hex int 0173 octal int 0.243 float 0.1732f float 15.75E2 float 'a' character "giday" string
C 語言中定義了許多特殊字元
'\a' alarm (beep) character '\p' backspace '\f' formfeed '\n' newline '\r' carriage return '\t' horizontal tab '\v' vertical tab '\\' backslash '\?' question mark '\'' single quote '\"' double quote '\0NN' character code in octal '\xNN' character code in hex '\0' null character
可以使用“define” C 預處理器宣告指定“符號常量”
#define PI 3.141592654
還有一個“const” 宣告,它定義一個只讀變數,例如 ROM 中的記憶體位置
const int a;
- 可以宣告和初始化陣列
int myarray[10];
unsigned int list[5] = { 10, 15, 12, 19, 23 };
float rdata[128], grid[5][5];
所有 C 語言陣列的起始索引都是 0,因此“list” 的索引為 0 到 4。“rdata” 中的元素將按如下方式訪問
for( i = 0; i <= 127; i = i + 1 )
{
printf ( "%f\n", rdata[i] );
}
C 語言不會對陣列訪問進行嚴格的邊界檢查。很容易超出陣列的邊界,唯一的症狀是程式行為非常奇怪。
- 字元陣列尤其重要,它們用於儲存
字串
char s[128];
strcpy( s, "This is a test!");
字串“This is a test!” 透過“strcpy()” 函式初始化“s”,該函式將在後面的章節中討論。儲存的字串將包含一個結束“空”字元(ASCII 碼為 0 的字元,用“\0” 表示)。C 語言函式使用空字元來確定字串的結尾,記住空字元的存在很重要。
好奇的讀者可能想知道為什麼要使用“strcpy()” 函式來初始化字串。這樣做似乎更容易
char s[128] = "This is a test!";
事實上,這是一個荒謬的操作,要解釋原因,必須介紹“指標”的概念。
- C 語言程式可以定義包含變數或
陣列地址的指標。例如,可以定義一個名為
int *ptr;
-- 的指標,它給出變數的地址,而不是變數本身。然後可以使用以下語句將值放入該位置
*ptr = 345;
相反地,可以使用“&” 獲取變數的地址
int tmp;
somefunc( &tmp );
總結一下
- 指標的宣告形式為:“*myptr”。
- 如果“myvar” 是一個變數,那麼“&myvar” 就是指向該變數的指標。
- 如果“myptr” 是一個指標,那麼“*myptr” 將給出該指標的變數資料。
指標很有用,因為它們允許函式透過引數變數返回值。否則,函式只會獲取變數包含的資料,而無法訪問變數本身。
C 語言的一個奇特性質是,陣列的名稱實際上指定了指向陣列中第一個元素的指標。例如,給出字串宣告
char s[256];
-- 那麼函式呼叫
somefunc( s )
-- 實際上會將字元陣列的地址傳遞給函式,並且函式將能夠修改它。但是
s[12]
-- 給出索引為 12 的陣列值。請記住,這是第 13 個元素,因為索引始終從 0 開始。
C 語言中字串還有更多奇特性質。另一個有趣的地方是,字串字面量實際上會評估為指向它所定義字串的指標。這意味著在以下操作中
char *p;
p = "Life, the Universe, & Everything!";
-- “p” 最終成為指向 C 編譯器儲存字串字面量的記憶體的指標,而“p[0]” 將評估為“L”。類似地,以下操作
char ch;
ch = "Life, the Universe, & Everything!"[0];
-- 將把字元“L” 放入變數“ch” 中。
這很好,但為什麼要關心呢?之所以關心是因為這解釋了為什麼操作
char s[128] = "This is a test!";
-- 是荒謬的。這個語句告訴 C 編譯器保留 128 位元組的記憶體,並將名為“s” 的指標指向它們。然後它保留另一個記憶體塊來儲存“This is a test!”,並將“s” 指向該塊。這意味著最初分配的 128 位元組記憶體塊現在處於空閒狀態且無法使用,並且程式實際上正在訪問儲存“This is a test!” 的記憶體。
這在一段時間內似乎可以正常工作,直到程式嘗試將更多位元組儲存到該塊中,而這些位元組超過了為“This is a test!” 保留的 16 個位元組。由於 C 語言對邊界檢查很差,這可能會造成各種問題。
這就是通常需要“strcpy()” 的原因。對於不會被修改或不會被用來儲存比初始化時更多資料的字串,不需要這樣做,在這種情況下,以下語句可以正常工作
char *p;
p = "Life, the Universe, & Everything! ";
當將字串作為引數傳遞給函式時,這些問題會變得特別棘手。以下示例展示瞭如何避免這些陷阱
/* strparm.c */
#include <stdio.h>
#include <string.h>
char *strtest( char *a, char *b );
int main ()
{
char a[256],
b[256],
c[256];
strcpy( a, "STRING A: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" );
strcpy( b, "STRING B: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" );
strcpy( c, "STRING C: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" );
printf( "Initial values of strings:\n" );
printf( "\n" );
printf( " a = %s\n", a );
printf( " b = %s\n", b );
printf( " c = %s\n", c );
printf( "\n" );
strcpy( c, strtest( a, b ));
printf( "Final values of strings:\n" );
printf( "\n" );
printf( " a = %s\n", a );
printf( " b = %s\n", b );
printf( " c = %s\n", c );
printf( "\n" );
return 0;
}
char *strtest( char *x, char *y )
{
printf( "Values passed to function:\n" );
printf( "\n" );
printf( " x = %s\n", x );
printf( " y = %s\n", y );
printf( "\n" );
strcpy( y, "NEWSTRING B: abcdefghijklmnopqrstuvwxyz0123456789" );
return "NEWSTRING C: abcdefghijklmnopqrstuvwxyz0123456789";
}
- 可以在 C 語言中定義“結構”,它們是不同資料元素的集合
/* struct.c */
#include <stdio.h>
#include <string.h>
struct person /* Define structure type. */
{
char name[50];
int age;
float wage;
};
void display( struct person );
int main()
{
struct person m; /* Declare an instance of it. */
strcpy( m.name, "Coyote, Wile E." ); /* Initialize it. */
m.age = 41;
m.wage = 25.50f;
display( m );
return 0;
}
void display( struct person p )
{
printf( "Name: %s\n", p.name );
printf( "Age: %d\n", p.age );
printf( "Wage: %4.2f\n", p.wage );
}
這個程式有一些有趣的特性
- 必須透過“struct” 宣告定義結構,然後才能宣告任何結構本身。在本例中,我們定義了一個名為“person” 的結構型別。
- 然後,透過定義結構型別 (“struct person”) 來宣告結構的例項 (“m”)。
- 使用“點” 表示法訪問結構的元素 (“m.name”、“m.age” 和“m.wage”)。
只要結構型別相同,就可以使用單個賦值語句將一個結構複製到另一個結構
struct person m, n;
...
m = n;
也可以宣告結構陣列
struct person group[10];
...
strcpy( group[5].name, "McQuack, Launchpad" );
-- 甚至可以在結構宣告中嵌入結構
struct trip_rec
{
struct person traveler;
char dest[50];
int date[3];
int duration;
float cost;
}
-- 在這種情況下,巢狀結構將按如下方式訪問
struct trip_rec t1;
...
strcpy( t1.traveler.name, "Martian, Marvin" );
結構的名稱定義了一個變數,而不是一個地址。如果將結構的名稱傳遞給函式,則該函式只會在其結構的本地副本上工作。要返回值,必須指定一個地址
setstruct( &mystruct );
有一個捷徑可以用來獲取結構的元素,使用指向結構的指標而不是結構本身。如果“sptr” 是指向“person” 型別結構的指標,則可以按如下方式訪問其欄位
strcpy( sptr->name, "Leghorn, Foghorn" );
sptr->age = 50;
sptr->wage = 12.98;
- C 語言包含一個與結構類似的概念,稱為“聯合”。聯合的宣告方式與結構非常相似。例如
union usample
{
char ch;
int x;
}
不同之處在於,聯合可以儲存這兩個值中的任何一個,但不能同時儲存。可以在上面定義的聯合例項中儲存一個“char” 值或一個“int” 值,但不能同時儲存兩者。只為聯合分配足夠的空間來儲存其中宣告的最大專案的價值,並且使用相同的記憶體來儲存所有宣告專案的價值。聯合不經常使用,這裡不再進一步討論。
- 以下示例程式展示了結構的實際應用。它測試了一組用於對三維向量執行操作的函式
vadd(): Add two vectors. vsub(): Subtract two vectors. vdot(): Vector dot product. vcross(): Vector cross product. vnorm(): Norm (magnitude) of vector. vangle(): Angle between two vectors. vprint(): Print out vector.
程式如下
/* vector.c */
#include <stdio.h>
#include <math.h>
#define PI 3.141592654
struct v
{
double i, j, k;
};
void vadd( struct v, struct v, struct v* );
void vprint( struct v );
void vsub( struct v, struct v, struct v* );
double vnorm( struct v );
double vdot( struct v, struct v );
double vangle( struct v, struct v );
void vcross( struct v, struct v, struct v* );
int main()
{
struct v v1 = { 1, 2, 3 }, v2 = { 30, 50, 100 }, v3;
double a;
printf( "Sample Vector 1: " );
vprint( v1 );
printf( "Sample Vector 2: " );
vprint( v2 );
vadd( v1, v2, &v3 );
printf( "Vector Add: " );
vprint( v3 );
vsub( v1, v2, &v3 );
printf( "Vector Subtract: " );
vprint( v3 );
vcross( v1, v2, &v3 );
printf( "Cross Product: " );
vprint( v3 );
printf( "\n" );
printf( "Vector 1 Norm: %f\n", vnorm( v1 ) );
printf( "Vector 2 Norm: %f\n", vnorm( v2 ) );
printf( "Dot Product: %f\n", vdot( v1, v2 ) );
a = 180 * vangle( v1, v2) / PI ;
printf( "Angle: %3f degrees.\n", a );
return 0;
}
void vadd( struct v a, struct v b, struct v *c ) /* Add vectors. */
{
c->i = a.i + b.i;
c->j = a.j + b.j;
c->k = a.k + b.k;
}
double vangle( struct v a, struct v b ) /* Get angle between vectors. */
{
double c;
c = vdot( a, b ) / ( vnorm( a ) * vnorm( b ) );
return acos( c );
}
void vcross( struct v a, struct v b, struct v *c ) /* Cross product. */
{
c->i = a.j * b.k - a.k * b.j;
c->j = a.k * b.i - a.i * b.k;
c->k = a.i * b.j - a.j * b.i;
}
double vdot( struct v a, struct v b ) /* Dot product of vectors. */
{
return a.i * b.i + a.j * b.j + a.k * b.k;
}
double vnorm ( struct v a ) /* Norm of vectors. */
{
return sqrt( a.i * a.i + a.j * a.j + a.k * a.k );
}
void vprint ( struct v a ) /* Print vector. */
{
printf( " I = %6.2f J = %6.2f K = %6.2f\n", a.i, a.j, a.k );
}
void vsub ( struct v a, struct v b, struct v *c ) /* Subtract vectors. */
{
c->i = a.i - b.i;
c->j = a.j - b.j;
c->k = a.k - b.k;
}
- 現在應該清楚區域性變數和全域性變數的概念了。它
還可以將區域性變數宣告為“static”,這意味著它會從函式的一次呼叫保留到下一次呼叫。例如
#include <stdio.h>
void testfunc( void );
int main()
{
int ctr;
for( ctr = 1; ctr <= 8; ++ctr )
{
testfunc();
}
return 0;
}
void testfunc( void )
{
static int v;
printf( "%d\n", 2*v );
++v;
}
這會列印
0 2 4 6 8 10 12 14
-- 因為整數的初始值為 0。不要依賴預設值!
- 還有另外兩個變數宣告應該被識別出來,雖然
很少有理由使用它們:“register”,它宣告一個變數應該分配給 CPU 暫存器,“volatile”,它告訴編譯器變數的內容可能會自發改變。
這些宣告比表面上看到的要複雜。 “register” 宣告是可選的:如果可以,變數將被載入到 CPU 暫存器中,如果不行,它將按正常方式載入到記憶體中。由於一個好的最佳化編譯器會盡力利用 CPU 暫存器,因此這通常不是一個很有用的操作。
乍一看,“volatile” 宣告似乎很荒謬,就像“halt and catch fire” 這樣的“笑話” 計算機命令一樣。實際上,它用於描述可以獨立於程式操作改變的硬體暫存器,例如即時時鐘的暫存器。
- C 語言在資料型別之間的轉換方面相當靈活。在許多情況下,
型別轉換會透明地發生。例如,如果將一個“char” 轉換為“short” 資料型別,或者將一個“int” 轉換為“long” 資料型別,則轉換後的資料型別可以輕鬆地容納原始資料型別中的任何值。
從更大的資料型別轉換為更小的資料型別可能會導致奇特的錯誤。對於有符號和無符號資料型別之間的轉換也是如此。因此,應該小心地處理型別轉換,通常最好是顯式地進行轉換,使用“強制轉換” 操作。例如,可以如下將“int” 值強制轉換為“float” 值
int a;
float b;
...
b = (float)a;
- 可以在 C 語言中定義自定義“列舉” 型別。例如
enum day
{
saturday, sunday, monday, tuesday, wednesday, thursday, friday
};
-- 定義列舉型別“day”,它包含一週中各天的值。實際上,這些值僅僅是一組連續整數值相關聯的文字常量。預設情況下,集合從 0 開始,並依次遞增,因此這裡的“saturday” 的值為 0,“sunday” 的值為 1,依此類推。如果需要,可以指定任何一組數字分配
enum temps
{
zero = 0, freeze = 32, boil = 220
};
當然,使用一組 “#define” 指令也可以實現同樣的功能,但這是一種更簡潔的解決方案。一旦型別定義完成,就可以像下面這樣宣告該型別的變數:
enum day today = wednesday;
變數 “today” 將作為一個 “int” 型別變數,並允許對 “int” 型別變數進行有效的操作。再次提醒,C 語言在邊界檢查方面做得不多,不建議依賴 C 編譯器發出警告。
- 最後,可以使用 “typedef” 宣告來定義自定義資料型別。
typedef ch[128] str;
然後,可以像下面這樣宣告該型別的變數:
str name;