一、C語言程序的構成
與C++、Java相比,C語言其實很簡單,但卻非常重要。因為它是C++、Java的基礎。不把C語言基礎打扎實,很難成為程序員高手。
1、C語言的結構
先通過一個簡單的例子,把C語言的基礎打牢。
C語言的結構要掌握以下幾點:
(1)C語言的注釋是/* ··· */,而不是//···,//是C++的單行注釋,有的C語言版本也認可。
(2)C語言區分大小寫,每句以分號結尾。
(3)C語言程序是從main函數開始的。函數的返回值如果缺省則為int,而不是void。
(4)函數必須用return來返回。即使void類型也不建議省略。
(5)使用函數時須包含相應的頭文件。自定義的頭文件用雙引號,C語言自身的頭文件用
2、main()函數的寫法與含義
main()的參數和返回值全部省略,這和上例含義相同。省略寫法是一種很不好的習慣。
main()的參數是一種不限個數的寫法,argc代表參數的個數,真正的參數是放在argv[]數組里面的。注意:當數組當參數用時,數組被降格為指針。初學者先照著樣子寫,以后小雅會詳細說明指針和數組的區別。
3、頭文件的意義
每個C程序通常分為兩個文件。一個文件用于保存程序的聲明(declaration),稱為頭文件。另一個文件用于保存程序的實現(implementation),稱為定義(definition)文件。
C程序的頭文件以“.h”為后綴,C 程序的定義文件以“.c”為后綴。
頭文件的內容也可以直接寫C程序中,但這是很不好的習慣。許多初學者用了頭文件,卻不明其理。在此略作說明。
(1)通過頭文件來調用庫功能。在很多場合,源代碼不便(或不準)向用戶公布,只要向用戶提供頭文件和二進制的庫即可。用戶只需要按照頭文件中的接口聲明來調用庫功 能,而不必關心接口怎么實現的。編譯器會從庫中提取相應的代碼。
(2)頭文件能加強類型安全檢查。如果某個接口被實現或被使用時,其方式與頭文件中 的聲明不一致,編譯器就會指出錯誤,這一簡單的規則能大大減輕程序員調試、改錯的 負擔。
關于頭文件的內容,初學者還必須注意。
(1)頭文件中可以和C程序一樣引用其它頭文件,可以寫預處理塊,但不能寫語句命令。
(2)可以申明函數,但不可以定義函數。
(3)可以申明常量,但不可以定義變量。
(4)可以“定義”一個宏函數。注意:宏函數很象函數,但卻不是函數。其實還是一個申明。
(5)結構的定義、自定義數據類型一般也放在頭文件中。
(6)#include ,編譯系統會到C語言固定目錄去引用。#include "filename.h",系統一般首先在當前目錄查找,然后再去環境指定目錄查找。
4、好的風格是成功的關鍵
版本申明、函數功能說明、注釋等是C語言程序的一部分。不養成很好的習慣則不能成為C語言高手(專業人員)。
二、比較、邏輯、位運算符
只有類型相同(或C語言能自動轉換)的表達式才能比較,如果類型不同就必須用函數轉換。例如:判斷一字符串的長度是否等于10,就要用strlen()將字符串的長度求出來變成了整型,才能和10比較。
比較運算符只有6個,即:等于(==)、不等于(!=)、大于(>)、小于(=)、小于等于(<=)。比較運算符也叫關系運算符。
邏輯運算符只有3個,即:與AND(&&)、或OR(||)、非NOT(!)。
位運算符只有6個,即:與AND(&)、或OR(|)、非NOT(~)、異或XOR(^)、左移ShiftLeft(<>)。
三、數組
(1)數組名也是一變量名,定義時須指定類型和長度。
(2)長度可以方括號中直接指定,也可以通過賦值來間接指定。
(3)數組可以在定義時直接賦值,也可以定義時不賦值,之后再賦值。
(4)當使用超出范圍的值時,編譯不出錯,但運行會出錯。(上例運行時出錯后,選“忽略”后得到的結果)
數組的地址
弄清數組地址對使用數組有很大好處,另外,有的函數的參數是指針(如scanf函數),如果要用數組的某一元素作參數,就必須知道其地址。
1.數組iArr是int類型,所以它的地址是按4字節遞增。
2.數組cArr是char類型,所以它的地址是按1字節遞增。
3.數組元素的地址是通過數組元素前面加“&”來取得。(如:&iArr[3])
4.數組名單獨使用時,代表該數組的首地址。(iArr等同于&iArr[0])(注意:以后使用指針會經常用到這一點)
四、字符數組和字符串的重定義
字符數組就是字符串嗎?有人說是,因為書上這么寫,教師也這么教的。小雅不敢說書上或教師們錯了,但至少可以說許多初學者都混淆了這兩個概念。因此,在這此將這2個概念再明確一下。
1.字符數組,完整地說叫字符類型的數組。字符數組不一定是字符串。
2.字符串是最后一個字符為NULL字符的字符數組。字符串一定是字符數組。
3.字符數組的長度是固定的,其中的任何一個字符都可以為NULL字符。
4.字符串只能以NULL結尾,其后的字符便不屬于該字符串。
5.strlen()等字符串函數對字符串完全適用,對不是字符串的字符數組不適用。
從上面例子看來,還要注意以下幾點:
(1)char sArr[] = "quanxue";這種方式,編譯時會自動在末尾增加一個NULL字符。
(2)NULL字符也就是'\0',在ASCII表中排在第一個,用16進制表示為0x00。
(3)sizeof()運算符求的是字符數組的長度,而不是字符串長度。
(4)strlen()函數求的是字符串長度,而不是字符數組。它不適用于字符串以外的類型。
(5)char sArr[] = "quanxue";也可以寫成char sArr[8] = "quanxue";(注意:是8而不是7)
字符數組和字符串數組的轉化
字符數組中插入一個NULL字符,NULL字符前面(包括NULL字符)就成了字符串,一般NULL字符插在有效字符的最后。
數組的輸入輸出『gets(),puts()』
getchar()和putchar()函數是單個字符的輸入輸出,gets()和puts()是字符串的輸入輸出,也是標準函數,在stdio.h中被定義。
五、指針
指針符號『*』和地址符號『&』
『&』符號是取變量的地址,『*』符號是取地址的內容(即:值)。兩個操作正好相反。例如:“&i”就是取變量i的地址,“*(&i)”就是取“&i”這個地址的值,其實就是變量i。即然如此,為什么還要定義指針呢?原來,用『&』所取到的地址,自身只能用而不能修改。因此,直接把『&』取到的地址放到指針變量中去,既然指針變量也是變量,這個變量就可以任意存放其它地址。
指針變量的賦值和指針的賦值
上例中p是指針變量,*p是p的指針,p存放的是某個變量的地址,*p存放的是某個變量的值。當*p的內容改變時,p所指的變量的內容也發生改變,因為是同一個地址的存貯單元的值發生改變。同理,當p所指的變量的值發生改變時,*p的內容也隨之改變。
被初始化的是指針變量還是指針
上面2例,指針變量都是用的p,初學者不要認為只能用p,既然是變量,只要不違反命名規則都可以。當指針變量被定義時立即賦值,這時被賦值的是指針變量還是指針呢?下面這段程序請大家千萬注意!
(1)charstr[] ="http://www.quanxue.cn/";中str是數組變量,當地址賦給point之后,point[11]就是str[11],所以其內容可以改變。
(2)char*ptr ="http://www.51minge.com/";中賦值的性質和上面的str不同。這并不是將"http://www.51minge.com/"賦給*ptr指針,而是先定義一個常量"http://www.51minge.com/",這個常量是定義在“棧”里面,然后將這個常量的地址賦給ptr,而不是*ptr。常量是不能被修改的,因此ptr[13]也就出錯了。這是初學者經常犯的錯誤。
不賦值的指針和NULL
未賦值的指針變量是不能被使用的,其地址指向未不能使用的空間。建議定義時如果暫不使用,先賦NULL。為一個指針申請空間時,一定義要判斷其是否為空,因為分配內存失敗時返回NULL。不僅如此,甚至在使用指針時都應該判斷一下是否為空。
六、指針、數組和字符串
一、數組和指針的關系
下面仍然是初學者容易搞錯的地方。指針變量加n或減n,并不是地址加n或減n,而是當前所指的地址向后或向前跳n次所指的地址。
二、指針數組
char型的指針數組相當于二維字符數組,并不等于說指針數組可以直接轉化為二為字符數組,相反字符數組可以直接轉化為指針數組。因為二維字符數組的地址是連續的,而指針數組所指的元素不一定連續(如下的m1、m2、m3的地址可以不連續,長度也可以不一樣)。
三、指向指針的指針
在第一章講main()函數的參數時,已經見過指針的指針,這和指針數組有相同的作用,但還是有細小的區別。指針數組可以在定義時直接初始化,而指向指針的指針不行。正如二維數組一樣,不指定第二維長度不能直接初始化一樣。即不能char str[][]={"...", "...", ...}
四、指針的長度
讓許多初學者遺憾的是,C語言沒有提供數組長度的函數,但可以用sizeof()運算符先求數組的總長度,再求出數組類型的長度,二者相除便得到數組的長度。C語言更大的一個遺憾便是,sizeof()對指針變量求值時,結果總是4,這是因為指針變量的內容是地址,地址總是4個字節來表示。
因此有經驗的編程人員,在用指針作參數時,一般總是同時多定義一個參數,來存放其長度。也就是指針和其長度同時傳遞過去。另外,數組長度如果事先知道,一般定義為常量。
七、為指針動態分配內存
C語言程序員要嚴防內存泄漏,這個“內存泄漏”就是由動態內存分配引起的。指針是C語言和其它語言的最大區別,也是很多人不能跨入C語言的一道門檻。既然指針是這么一個“危險”的壞東西,干嗎不取消它呢?
其實指針本身并沒有好壞,它只是一種操作地址的方法,學會了便可以發揮其它語言難以匹敵的功能,沒學會的話,只能做其它語言的程序員,也同樣發揮你的光和熱。小雅本人也在C語言門外徘徊多年,至今仍屬于初學者。
一、變量和數組可以通過指針來轉換
“int*x”中的x究竟是不是數組?光看這一句小雅無法告訴你,因為它既可表示單個變量內容,也可表示數組。下面是小雅專門為你準備的例子,理解之后,對動態分配時長度計算有好處。
二、動態分配內存
前面講到的指針,基本上將已經定義好的變量的地址賦給指針變量,現在要學的是向操作系統申請一塊新的內存。申請到的內存,必須在某個地方手動釋放,因此下面2個函數必須配對使用。malloc()和free(),都是標準函數,在stdlib.h中定義。
根據不同的電腦使用狀況,申請內存有可能失敗,失敗時返回NULL,因此,動態申請內存時,一定要判斷結果是否為空。malloc()的返回值類型是“void *”,因此,不要忘記類型轉換。(許多人都省略了。)
三、隱蔽的內存泄漏
內存泄漏主要有以下幾種情況:
(1)內存分配未成功,卻使用了它。
(2)內存分配雖然成功,但是尚未初始化就引用它。
(3)內存分配成功并且已經初始化,但操作越過了內存的邊界。
(4)忘記了釋放內存,造成內存泄露。
(5)釋放了內存卻繼續使用它。
下面的程序造成內存泄漏,想想錯在何處?如何修改?
四、對動態內存的錯誤觀念
有人對某一只在函數內使用的指針動態分配了內存,用完后不釋放。其理由是:函數運行結束后,函數內的所有變量全部消亡。這是錯誤的。動態分配的內存是在“堆”里定義,并不隨函數結束而消亡。
有人對某動態分配了內存的指針,用完后直接設置為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是將變量放到寄存器來運算,小雅認為基本沒有這種需要。
拆成多個文件,多次定義全局變量時要注意:
(1)變量的數據類型要一致。
(2)有長度的數組和沒定義長度的數組可以視為同一數據類型。
(3)數組和指針不能視為同一數據類型。
文章就分享到這里了,希望對大家有幫助!
自學C/C++編程難度很大,不妨和一些志同道合的小伙伴一起學習成長!
C/C++編程學習QQ群:1121833361,有問題需要交流或者想要C語言學習視頻可以加群一起成長哦!