C語言程式的構成
與C++、Java相比,C語言其實很簡單,但卻非常重要。因為它是C++、Java的基礎。不把C語言基礎打紮實,很難成為程式員高手。
一、C語言的結構
先通過一個簡單的例子,把C語言的基礎打牢。
/* clang01_1.c */
#include <stdio.h>
int main( void )
{
printf("這是勸學網的C語言教程。\n");
return 0;
}
C語言的結構要掌握以下幾點:
- C語言的注釋是/* ··· */,而不是//···,//是C++的單行注釋,有的C語言版本也認可。
- C語言區分大小寫,每句以分号結尾。
- C語言程式是從main函數開始的。函數的傳回值如果預設則為int,而不是void。
- 函數必須用return來傳回。即使void類型也不建議省略。
- 使用函數時須包含相應的頭檔案。自定義的頭檔案用雙引号,C語言自身的頭檔案用<···>
二、main()函數的寫法與含義
main()的參數和傳回值全部省略,這和上例含義相同。省略寫法是一種很不好的習慣。
main() int main( void )
{ {
··· 等同于 ···
} }
main()的參數是一種不限個數的寫法,argc代表參數的個數,真正的參數是放在argv[]數組裡面的。注意:當數組當參數用時,數組被降格為指針。初學者先照着樣子寫,以後小雅會詳細說明指針和數組的差別。
int main( int argc, char *argv[]) int main( int argc, char **argv)
{ {
··· 也可寫成 ···
} }
三、頭檔案的意義
每個C程式通常分為兩個檔案。一個檔案用于儲存程式的聲明(declaration),稱為頭檔案。另一個檔案用于儲存程式的實作(implementation),稱為定義(definition)檔案。 C程式的頭檔案以“.h”為字尾,C 程式的定義檔案以“.c”為字尾。
頭檔案的内容也可以直接寫C程式中,但這是很不好的習慣。許多初學者用了頭檔案,卻不明其理。在此略作說明。
- 通過頭檔案來調用庫功能。在很多場合,源代碼不便(或不準)向使用者公布,隻要向使用者提供頭檔案和二進制的庫即可。使用者隻需要按照頭檔案中的接口聲明來調用庫功 能,而不必關心接口怎麼實作的。編譯器會從庫中提取相應的代碼。
- 頭檔案能加強類型安全檢查。如果某個接口被實作或被使用時,其方式與頭檔案中 的聲明不一緻,編譯器就會指出錯誤,這一簡單的規則能大大減輕程式員調試、改錯的 負擔。
關于頭檔案的内容,初學者還必須注意。
- 頭檔案中可以和C程式一樣引用其它頭檔案,可以寫預處理塊,但不能寫語句指令。
- 可以申明函數,但不可以定義函數。
- 可以申明常量,但不可以定義變量。
- 可以“定義”一個宏函數。注意:宏函數很象函數,但卻不是函數。其實還是一個申明。
- 結構的定義、自定義資料類型一般也放在頭檔案中。
- #include <filename.h>,編譯系統會到C語言固定目錄去引用。#include "filename.h",系統一般首先在目前目錄查找,然後再去環境指定目錄查找。
四、好的風格是成功的關鍵
版本申明、函數功能說明、注釋等是C語言程式的一部分。不養成很好的習慣則不能成為C語言高手(專業人員)。
由于以上部分都是容易忽略的知識點,勸學網上有就直接轉了。轉自勸學網http://www.quanxue.cn/JC_CLanguage/CLang/CLang01.html
二、比較、邏輯、位運算符
隻有類型相同(或C語言能自動轉換)的表達式才能比較,如果類型不同就必須用函數轉換。例如:判斷一字元串的長度是否等于10,就要用strlen()将字元串的長度求出來變成了整型,才能和10比較。
比較運算符隻有6個,即:等于(==)、不等于(!=)、大于(>)、小于(<)、大于等于(>=)、小于等于(<=)。比較運算符也叫關系運算符。
邏輯運算符隻有3個,即:與AND(&&)、或OR(||)、非NOT(!)。
位運算符隻有6個,即:與AND(&)、或OR(|)、非NOT(~)、異或XOR(^)、左移ShiftLeft(<<)、右移ShiftRight(>>)。
三、數組
- 數組名也是一變量名,定義時須指定類型和長度。
- 長度可以方括号中直接指定,也可以通過指派來間接指定。
- 數組可以在定義時直接指派,也可以定義時不指派,之後再指派。
- 當使用超出範圍的值時,編譯不出錯,但運作會出錯。(上例運作時出錯後,選“忽略”後得到的結果)
數組的位址
弄清數組位址對使用數組有很大好處,另外,有的函數的參數是指針(如scanf函數),如果要用數組的某一進制素作參數,就必須知道其位址。
#include <stdio.h>
int main( void )
{
int i;
int iArr[7];
char cArr[] = {\'Q\',\'U\',\'A\',\'N\',\'X\',\'U\',\'E\'};
//輸出iArr數組和cArr數組的位址
printf("iArr=%p, cArr=%p\n\n", iArr, cArr);
//輸出iArr[i]數組和cArr[i]數組的位址
for (i=0; i<7; i++) {
printf("iArr[%d]=%p, cArr[%d]=%p\n", i, &iArr[i], i, &cArr[i]);
}
return 0;
}
- 數組iArr是int類型,是以它的位址是按4位元組遞增。
- 數組cArr是char類型,是以它的位址是按1位元組遞增。
- 數組元素的位址是通過數組元素前面加“&”來取得。(如:&iArr[3])
- 數組名單獨使用時,代表該數組的首位址。(iArr等同于&iArr[0])(注意:以後使用指針會經常用到這一點)
四、字元數組和字元串的重定義
字元數組就是字元串嗎?有人說是,因為書上這麼寫,教師也這麼教的。小雅不敢說書上或教師們錯了,但至少可以說許多初學者都混淆了這兩個概念。是以,在這此将這2個概念再明确一下。
- 字元數組,完整地說叫字元類型的數組。字元數組不一定是字元串。
- 字元串是最後一個字元為NULL字元的字元數組。字元串一定是字元數組。
- 字元數組的長度是固定的,其中的任何一個字元都可以為NULL字元。
- 字元串隻能以NULL結尾,其後的字元便不屬于該字元串。
- strlen()等字元串函數對字元串完全适用,對不是字元串的字元數組不适用。
#include <stdio.h>
#include <string.h>
int main( void )
{
//這是字元數組賦初值的方法
char cArr[] = {\'Q\',\'U\',\'A\',\'N\',\'X\',\'U\',\'E\'};
//這是字元串賦初值的方法
char sArr[] ="quanxue";
//用sizeof()求長度
printf("cArr的長度=%d\n", sizeof (cArr)); //長度為7
printf("sArr的長度=%d\n", sizeof (sArr)); //長度為8,最後一位是NULL
//用printf的%s列印内容
printf("cArr的内容=%s\n", cArr); //不能正确顯示
printf("sArr的内容=%s\n", sArr); //可以正确顯示
//用strlen()求長度
printf("cArr的長度=%d\n", strlen(cArr)); //不正确的結果
printf("sArr的長度=%d\n", strlen(sArr)); //NULL不在計算範圍
return 0;
}
從上面例子看來,還要注意以下幾點:
- char sArr[] = "quanxue";這種方式,編譯時會自動在末尾增加一個NULL字元。
- NULL字元也就是\'\0\',在ASCII表中排在第一個,用16進制表示為0x00。
- sizeof()運算符求的是字元數組的長度,而不是字元串長度。
- strlen()函數求的是字元串長度,而不是字元數組。它不适用于字元串以外的類型。
- char sArr[] = "quanxue";也可以寫成char sArr[8] = "quanxue";(注意:是8而不是7)
字元數組和字元串數組的轉化
字元數組中插入一個NULL字元,NULL字元前面(包括NULL字元)就成了字元串,一般NULL字元插在有效字元的最後。
#include <stdio.h>
#include <string.h>
int main( void )
{
//因為最後有NULL,是以這就變成了字元串
char cArr[] = {\'Q\', \'U\', \'A\', \'N\', \'X\', \'U\', \'E\', \'\0\'};
//因為少定義了一位,最後無NULL,是以這就變成了字元數組
char sArr[7] ="quanxue";
//最後一個元素未指派
char tArr[16] ="www.quanxue.cn";
//用sizeof()求長度
printf("cArr: %2d ,%2d\n", strlen(cArr), sizeof (cArr));
printf("sArr: %2d ,%2d\n", strlen(sArr), sizeof (sArr));
printf("tArr: %2d ,%2d\n", strlen(tArr), sizeof (tArr));
//将tArr的最後一個元素指派,字元串就成了字元數組
tArr[15] = \'!\';
//作為字元數組,将顯示16個字元
for (i=0; i<16; i++) printf("%c", tArr[i]); //字元數組的顯示方法
//作為字元串,将顯示14個字元。
printf("\n%s\n", tArr); //字元串的顯示方法
return 0;
}
數組的輸入輸出『gets(),puts()』
getchar()和putchar()函數是單個字元的輸入輸出,gets()和puts()是字元串的輸入輸出,也是标準函數,在stdio.h中被定義。
五、指針
指針符号『*』和位址符号『&』
『&』符号是取變量的位址,『*』符号是取位址的内容(即:值)。兩個操作正好相反。例如:“&i”就是取變量i的位址,“*(&i)”就是取“&i”這個位址的值,其實就是變量i。即然如此,為什麼還要定義指針呢?原來,用『&』所取到的位址,自身隻能用而不能修改。是以,直接把『&』取到的位址放到指針變量中去,既然指針變量也是變量,這個變量就可以任意存放其它位址。
#include <stdio.h>
int main( void )
{
int i = 100, j=200;
int *p;
p = &i; //變量i的位址賦給p
printf("&i=%p *(&i)=%d\n", &i, *(&i));
printf(" p=%p *p =%d\n\n", p, *p);
p = &j; //變量j的位址賦給p
printf("&j=%p *(&j)=%d\n", &j, *(&j));
printf(" p=%p *p =%d\n", p, *p);
return 0;
}
指針變量的指派和指針的指派
上例中p是指針變量,*p是p的指針,p存放的是某個變量的位址,*p存放的是某個變量的值。當*p的内容改變時,p所指的變量的内容也發生改變,因為是同一個位址的存貯單元的值發生改變。同理,當p所指的變量的值發生改變時,*p的内容也随之改變。
#include <stdio.h>
int main( void )
{
int i = 100, j=200;
int *p;
p = &i; //變量i的位址賦給p
*p = 500; //将500賦給p指針
//變量i的内容也随之改變為500
printf("&i=%p *(&i)=%d\n", &i, *(&i));
printf(" p=%p *p =%d\n\n", p, *p);
p = &j; //變量j的位址賦給p
j++; //将p指針的内容+1
//指針*p的内容也随之改變201
printf("&j=%p *(&j)=%d\n", &j, *(&j));
printf(" p=%p *p =%d\n", p, *p);
return 0;
}
被初始化的是指針變量還是指針
上面2例,指針變量都是用的p,初學者不要認為隻能用p,既然是變量,隻要不違反命名規則都可以。當指針變量被定義時立即指派,這時被指派的是指針變量還是指針呢?下面這段程式請大家千萬注意!
#include <stdio.h>
int main( void )
{
char str[] ="http://www.quanxue.cn/";
char *ptr ="http://www.51minge.com/";
char *point;
point = str; //将str數組的位址賦給指針變量point
point[11] = \'Q\', point[15] = \'X\';
printf("str=%s\n",str);
ptr[13] = \'M\', ptr[16] = \'G\'; //這句是錯誤的,删除後結果如下
printf("ptr=%s\n",ptr);
return 0;
}
-
str[] = "http://www.quanxue.cn/";中str是數組變量,當位址賦給point之後,point[11]就是str[11],是以其内容可以改變。char
-
*ptr = "http://www.51minge.com/";中指派的性質和上面的str不同。這并不是将"http://www.51minge.com/"賦給*ptr指針,而是先定義一個常量"http://www.51minge.com/",這個常量是定義在“棧”裡面,然後将這個常量的位址賦給ptr,而不是*ptr。常量是不能被修改的,是以ptr[13]也就出錯了。這是初學者經常犯的錯誤。char
int i = 129;
int num[] = {50, 25, 75, 100};
int *pt1 = 125; //錯誤1: 125作為位址賦給pt1, 這段記憶體是OS用的
int *pt2 = i ; //錯誤2: 129作為位址賦給pt2, 這段記憶體是OS用的
int *pt3 = &i; //正确: 變量i的位址賦給pt3, 因為i是基本類型,是以要加&符号
int *pt4 = num; //正确: 數組num的位址賦給pt4,因為num是數組,變量名就代表位址
char *pt5 ;
*pt5 ="ABCDE"; //錯誤3: "ABCDE"是字元串,也是數組, 此處更是常量
pt5 ="ABCDE"; //正确: 指派時賦的是位址,是以隻能賦給指針變量pt5
int *pt6 ;
pt6 =i; //錯誤4: 129作為位址賦給pt6, 這段記憶體是OS用的
*pt6 =i; //錯誤5: 129賦給指針pt6,但pt6尚未配置設定位址,沒有空間存放i的值
不指派的指針和NULL
未指派的指針變量是不能被使用的,其位址指向未不能使用的空間。建議定義時如果暫不使用,先賦NULL。為一個指針申請空間時,一定義要判斷其是否為空,因為配置設定記憶體失敗時傳回NULL。不僅如此,甚至在使用指針時都應該判斷一下是否為空。下面講到的記憶體配置設定是重要内容,在下下章詳細介紹。
#include <stdio.h>
#include <stdlib.h>
int main( void )
{
char *pchar;
//下面這句出錯,忽略之後繼續
printf("pchar=%p\n", pchar); //未配置設定的指針不可用
pchar = NULL;
printf("pchar=%p\n\n", pchar); //空指針可用于表達式中
//pchar為空時,動态配置設定記憶體
if (!pchar) {
pchar = ( char *)malloc(20); //動态配置設定記憶體
if (!pchar) { //初學者不要忘記, 這是必要的判斷
printf("記憶體配置設定失敗。");
exit(-1); //退出
}
gets(pchar);
printf(" pchar=%p\n*pchar=%s\n", pchar, pchar);
free(pchar); //記憶體釋放後,指針變量的位址不變
if (pchar) { //pchar并不為空
printf("\n pchar=%p\n*pchar=%s\n", pchar, pchar); //pchar成了“野指針”
}
}
return 0;
}
六、指針、數組和字元串
一、數組和指針的關系
下面仍然是初學者容易搞錯的地方。指針變量加n或減n,并不是位址加n或減n,而是目前所指的位址向後或向前跳n次所指的位址。
#include <stdio.h>
int main( void )
{
int num[] = {50, 25, 75, 100};
int *pt;
pt = num + 1; //故意将第2個元素位址賦給指針變量
//顯示指針變量所指的位址以及指針的值
printf("pt =%d, *pt =%d\n\n", pt, *pt);
//當指針變量減1或加1時,所指的位址并不減1或加1,而是加減4,因為int是4個位元組
printf("pt-1=%d, *(pt-1)=%d\n", pt-1, *(pt-1));
printf("pt =%d, *pt =%d\n", pt, *pt);
printf("pt+1=%d, *(pt+1)=%d\n", pt+1, *(pt+1));
//顯示pt當作數組用時的值。(注意:[-1]仍然是正确的)
printf("\npt[0]=%d, pt[-1]=%d\n", pt[0], pt[-1]);
return 0;
}
二、指針數組
char型的指針數組相當于二維字元數組,并不等于說指針數組可以直接轉化為二為字元數組,相反字元數組可以直接轉化為指針數組。因為二維字元數組的位址是連續的,而指針數組所指的元素不一定連續(如下的m1、m2、m3的位址可以不連續,長度也可以不一樣)。
#include <stdio.h>
int main( void )
{
char *m1 ="www.quanxue.cn";
char *m2 ="www.51minge.com";
char *m3 ="這兒是小雅的C語言教程";
char *message[3]; //| #include <stdio.h>
int i; //|
//| int main( void )
message[0] = m1; //| {
message[1] = m2; //| char *message[] = {"www.quanxue.cn",
message[2] = m3; //|"www.51minge.com",
//|"這兒是小雅的C語言教程"};
for (i=0; i<3; i++) { //| for (i=0; i<3; i++) {
printf("%s\n", message[i]); //| printf("%s\n", message[i]);
} //| }
//|
return 0; //| return 0;
} //| }
三、指向指針的指針
在第一章講main()函數的參數時,已經見過指針的指針,這和指針數組有相同的作用,但還是有細小的差別。指針數組可以在定義時直接初始化,而指向指針的指針不行。正如二維數組一樣,不指定第二維長度不能直接初始化一樣。即不能char str[][]={"...", "...", ...}
#include <stdio.h>
int main( void )
{
char *message[] = {"www.quanxue.cn","www.51minge.com","這兒是小雅的C語言教程"};
char **p; //指向指針的指針
int i;
p = message; //指向指針的指針賦初值
for (i=0; i<3; i++) {
printf("%s\n", p[i]);
}
return 0;
}
四、指針的長度
讓許多初學者遺憾的是,C語言沒有提供數組長度的函數,但可以用sizeof()運算符先求數組的總長度,再求出數組類型的長度,二者相除便得到數組的長度。C語言更大的一個遺憾便是,sizeof()對指針變量求值時,結果總是4,這是因為指針變量的内容是位址,位址總是4個位元組來表示。
是以有經驗的程式設計人員,在用指針作參數時,一般總是同時多定義一個參數,來存放其長度。也就是指針和其長度同時傳遞過去。另外,數組長度如果事先知道,一般定義為常量。
#include <stdio.h>
int main( void )
{
char *msg[] = {"www.quanxue.cn","www.51minge.com","這兒是小雅的C語言教程"};
double dNum[] = {12.5, 24.55, 100.83};
double *p = dNum;
printf("dNum的size:%2d, 數組個數是:%d\n", sizeof (dNum), ( int ) sizeof (dNum)/ sizeof ( double ));
printf(" p的size:%2d, 數組個數是:%d\n", sizeof (p ), ( int ) sizeof (p )/ sizeof ( double ));
printf(" msg的size:%2d, 數組個數是:%d\n", sizeof (msg ), ( int ) sizeof (msg )/ sizeof ( char *));
return 0;
}
七、為指針動态配置設定記憶體
C語言程式員要嚴防記憶體洩漏,這個“記憶體洩漏”就是由動态記憶體配置設定引起的。指針是C語言和其它語言的最大差別,也是很多人不能跨入C語言的一道門檻。既然指針是這麼一個“危險”的壞東西,幹嗎不取消它呢?
其實指針本身并沒有好壞,它隻是一種操作位址的方法,學會了便可以發揮其它語言難以匹敵的功能,沒學會的話,隻能做其它語言的程式員,也同樣發揮你的光和熱。小雅本人也在C語言門外徘徊多年,至今仍屬于初學者。
一、變量和數組可以通過指針來轉換
“
int
*x”中的x究竟是不是數組?光看這一句小雅無法告訴你,因為它既可表示單個變量内容,也可表示數組。下面是小雅專門為你準備的例子,了解之後,對動态配置設定時長度計算有好處。
#include <stdio.h>
int main( void )
{
int *num = NULL;
int *x, y[] = {12, 22,32}, z = 100;
//下面示範,指針既可充當變量、也可充當數組
x=&z; //整型變量的位址賦給x
printf("*x=%d, x[0]=%d\n", *x, x[0]);
x = y; //數組的位址賦給x
printf("*x=%d, x[ 0]=%d, x[ 1]=%d, x[2]=%d\n", *x, x[0], x[1], x[2]);
x = y + 1; //數組的第二位位址賦給x
printf("*x=%d, x[-1]=%d, x[ 0]=%d, x[1]=%d\n", *x, x[-1], x[0], x[1]);
x = y + 2; //數組的第三位位址賦給x
printf("*x=%d, x[-2]=%d, x[-1]=%d, x[0]=%d\n", *x, x[-2], x[-1], x[0]);
return 0;
}
二、動态配置設定記憶體
前面講到的指針,基本上将已經定義好的變量的位址賦給指針變量,現在要學的是向作業系統申請一塊新的記憶體。申請到的記憶體,必須在某個地方手動釋放,是以下面2個函數必須配對使用。malloc()和free(),都是标準函數,在stdlib.h中定義。
根據不同的電腦使用狀況,申請記憶體有可能失敗,失敗時傳回NULL,是以,動态申請記憶體時,一定要判斷結果是否為空。malloc()的傳回值類型是“void *”,是以,不要忘記類型轉換。(許多人都省略了。)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main( void )
{
char *p ;
p = ( char *)malloc(40 * sizeof ( char )) ;
if (p == NULL) { //這個判斷是必須的
printf("記憶體配置設定出錯!");
exit(1);
}
strcpy(p,"這是勸學網C語言教程。\n"); //不要忘記給新記憶體指派
printf("%s", p);
free(p); //過河一定要拆橋
p = NULL ; //釋放後的指針置空,這是非常好的習慣,防止野指針。
return 0;
}
三、隐蔽的記憶體洩漏
記憶體洩漏主要有以下幾種情況:
- 記憶體配置設定未成功,卻使用了它。
- 記憶體配置設定雖然成功,但是尚未初始化就引用它。
- 記憶體配置設定成功并且已經初始化,但操作越過了記憶體的邊界。
- 忘記了釋放記憶體,造成記憶體洩露。
- 釋放了記憶體卻繼續使用它。
下面的程式造成記憶體洩漏,想想錯在何處?如何修改?
#include <stdio.h>
#include <stdlib.h>
int main( void )
{
int *p, i;
p = ( int *)malloc(6 * sizeof( int )) ;
if (p == NULL) { //判斷是否為空
printf("記憶體配置設定出錯!");
exit(1);
}
for (i=0; i<6; i++) {
p++;
*p = i;
printf("%2d", *p);
}
printf("\n");
free(p); //這句運作時出錯
return 0;
}
四、對動态記憶體的錯誤觀念
有人對某一隻在函數内使用的指針動态配置設定了記憶體,用完後不釋放。其理由是:函數運作結束後,函數内的所有變量全部消亡。這是錯誤的。動态配置設定的記憶體是在“堆”裡定義,并不随函數結束而消亡。
有人對某動态配置設定了記憶體的指針,用完後直接設定為NULL。其理由是:已經為NULL了,這就釋放了。這也是錯誤的。指針可以任意指派,而記憶體并沒有釋放;相反,記憶體釋放後,指針也并不為NULL。
八、return和exit、assert的差別
return語句是結束目前函數。而exit是結束main()函數,即整個程式,一般都是在遇到非常錯誤時才調用exit()。assert()是一個宏定義,在assert.h中申明,用來在DEBUG方式診斷程式,當參數中的條件不成立時,中斷main()函數。建議多多使用assert()。
九、變量和函數
在函數之外定義的變量是全局變量,在函數内定義的變量是這個函數的局部變量。局部就是隻能在目前函數内使用,而全局變量可以在任何一個函數中使用。
注意:一般而言,全局變量總是在所有函數之前定義,但如果某全局變量定義在兩個函數之間,則定義處後面的函數可以使用,而其前面函數不能使用。
有人說靜态變量相當于全局變量,這句話其實不對。全局變量變成靜态,就失去了靜态的意義,是以,靜态一般是加在局部變量上的。那麼,究竟什麼是靜态的局部變量呢?靜态變量随函數的定義而定義,如果已經存在就延用,但并不随函數的結束而消亡。在某一函數中定義的靜态局部變量,不能在其它函數使用。
當很多人編寫同一程式時,一般程式會被分割成幾個檔案。當幾個人都定義了某一全局變量時,編譯時不出錯,Link時将出錯。解決這個問題的辦法:将其中一個定義原封不動,其餘的定義前加上extend(即外部的定義)。
剛才所說是許多書上說的,小雅做了n次試驗,證明上述編譯時也不錯,Link時也不錯,也就是說extend完全是多餘的。大概上面所說是幾十年前的版本吧。事實上與extend同列在一起的還有auto、regist等變量修飾符。auto是差別B語言的,早就沒用了,regist是将變量放到寄存器來運算,小雅認為基本沒有這種需要。
拆成多個檔案,多次定義全局變量時要注意:
- 變量的資料類型要一緻。
- 有長度的數組和沒定義長度的數組可以視為同一資料類型。
- 數組和指針不能視為同一資料類型。