C語言教程包涵了最核心的指針詳細介紹,如有不足之處歡迎評論,謝謝點贊
一、C語言教程入門
C語言一經出現就以其功能豐富、表達能力強、靈活友善、應用面廣等特點迅速在全世界普及和推廣。C語言不但執行效率高而且可移植性好,可以用來開發應用軟體、驅動、作業系統等。C語言也是其它衆多進階語言的鼻祖語言,是以說學習C語言是進入程式設計世界的必修課!
也可以關注部落客的微信公衆号 “C和C加加” 回複“ZXC”有驚喜哦!
二、C語言的具體結構
簡單來說,一個C程式就是由若幹
頭檔案
和
函數
組成。
-
就是一條預處理指令, 它的作用是通知C語言編譯系統在對C程式進行正式編譯之前需做一些預處理工作。#include <stdio.h>
-
就是實作代碼邏輯的一個小的單元。函數
三、主函數
一個C程式有且隻有一個主函數,即main函數。
- C程式就是執行主函數裡的代碼,也可以說這個主函數就是C語言中的唯一入口。
- 而main前面的int就是主函數的類型.
- printf()是格式輸出函數,這裡就記住它的功能就是在螢幕上輸出指定的資訊
- return是函數的傳回值,根據函數類型的不同,傳回的值也是不同的。
- \n是轉義字元中的換行符。(注意:C程式一定是從主函數開始執行的)
四、規範
- 一個說明或一個語句占一行,例如:包含頭檔案、一個可執行語句結束都需要換行。
- 函數體内的語句要有明顯縮進,通常以按一下Tab鍵為一個縮進。
- 括号要成對寫,如果需要删除的話也要成對删除。
- 當一句可執行語句結束的時候末尾需要有分号。
- 代碼中所有符号均為英文半角符号。
更多詳細進階教程等你領取!可以關注公衆号 “C和C加加” 回複“ZXC”即可免費擷取!
五、程式解釋——注釋
注釋是寫給程式員看的,不是寫給電腦看的。
C語言注釋方法有兩種:
多行注釋: 單行注釋:
/* 注釋内容 */
//注釋一行
六、C的辨別符
C語言規定,辨別符可以是字母(A~Z,a~z)、數字(0~9)、下劃線_組成的字元串,并且第一個字元必須是字母或下劃線。在使用辨別符時還有注意以下幾點:
- 辨別符的長度最好不要超過8位,因為在某些版本的C中規定辨別符前8位有效,當兩個辨別符前8位相同時,則被認為是同一個辨別符。
- 辨別符是嚴格區分大小寫的。例如Imooc和imooc 是兩個不同的辨別符。
- 辨別符最好選擇有意義的英文單詞組成做到"見名知意",不要使用中文。
- 辨別符不能是C語言的關鍵字。想了解更多C語言關鍵字的知識。
七、變量及指派
變量就是可以變化的量,而每個變量都會有一個名字(辨別符)。變量占據記憶體中一定的存儲單元。使用變量之前必須先定義變量,要區分變量名和變量值是兩個不同的概念。
變量定義的一般形式為:資料類型 變量名;
多個類型相同的變量:資料類型 變量名, 變量名, 變量名…;
注意:在定義中不允許連續指派,如int a=b=c=5;是不合法的。
變量的指派分為兩種方式:
- 先聲明再指派
- 聲明的同時指派
八、基本資料類型
C語言中,資料類型可分為:
- 基本資料類型
- 構造資料類型
- 指針類型
- 空類型四大類
最常用的整型, 實型與字元型(char,int,float,double):
整型資料是指不帶小數的數字(int,short int,long int, unsigned int, unsigned short int,unsigned long int):
注:
- int short int long int是根據編譯環境的不同,所取範圍不同。
- 而其中short int和long int至少是表中所寫範圍, 但是int在表中是以16位編譯環境寫的取值範圍。
- 另外 c語言int的取值範圍在于他占用的位元組數 ,不同的編譯器,規定是不一樣。
- ANSI标準定義int是占2個位元組,TC是按ANSI标準的,它的int是占2個位元組的。但是在VC裡,一個int是占4個位元組的。
浮點資料是指帶小數的數字。
生活中有很多資訊适合使用浮點型資料來表示,比如:人的體重(機關:公斤)、商品價格、圓周率等等。
因為精度的不同又分為3種(float,double,long double):
九、格式化輸出語句
格式化輸出語句,也可以說是占位輸出,是将各種類型的資料按照格式化後的類型及指定的位置從計算機上顯示。
其格式為:
printf("輸出格式符",輸出項)
;
當輸出語句中包含普通字元時,可以采用以下格式:
printf("普通字元輸出格式符", 輸出項);
注意:格式符的個數要與變量、常量或者表達式的個數一一對應
十、常量
在程式執行過程中,值不發生改變的量稱為常量。
mtianyan: C語言的常量可以分為直接常量和符号常量。
直接常量也稱為字面量,是可以直接拿來使用,無需說明的量,比如:
- 整型常量:13、0、-13;
- 實型常量:13.33、-24.4;
- 字元常量:‘a’、‘M’
- 字元串常量:”I love imooc!”
在C語言中,可以用一個辨別符來表示一個常量,稱之為符号常量。符号常量在使用之前必須先定義,其一般形式為
#define 辨別符 常量值
#include <stdio.h>
#define POCKETMONEY 10 //定義常量及常量值
int main()
{
// POCKETMONEY = 12; //小明私自增加零花錢對嗎?
printf("小明今天又得到%d元零花錢\n", POCKETMONEY);
return 0;
}
符号常量不可以被改變。
十一、自動類型轉換
資料類型存在自動轉換的情況.
自動轉換發生在不同資料類型運算時,在編譯的時候自動完成。
char
類型資料轉換為
int
類型資料遵循
ASCII
碼中的對應值.
注:
位元組小的可以向位元組大的自動轉換,但位元組大的不能向位元組小的自動轉換
char可以轉換為int,int可以轉換為double,char可以轉換為double。但是不可以反向。
十二、強制類型轉換
強制類型轉換是通過定義類型轉換運算來實作的。其一般形式為:
(資料類型) (表達式)
其作用是把表達式的運算結果強制轉換成類型說明符所表示的類型
在使用強制轉換時應注意以下問題:
- 資料類型和表達式都必須加括号, 如把(int)(x/2+y)寫成(int)x/2+y則成了把x轉換成int型之後再除2再與y相加了。
- 轉換後不會改變原資料的類型及變量值,隻在本次運算中臨時性轉換。
- 強制轉換後的運算結果不遵循四舍五入原則。
十三、運算符号
C語言中運算符:
1.算術運算符
c語言基本運算符:
2.自增與自減運算符
- 自增運算符為
,其功能是使變量的值自增1++
- 自減運算符為
,其功能是使變量值自減1。--
它們經常使用在循環中。自增自減運算符有以下幾種形式:
3.指派運算符
C語言中指派運算符分為簡單指派運算符和複合指派運算符
簡單指派運算符
=
号了,下面講一下複合指派運算符:
複合指派運算符就是在簡單指派符
=
之前加上其它運算符構成.
注意:複合運算符中運算符和等号之間是不存在空格的。
4.關系運算符
C語言中的關系運算符:
關系表達式的值是
真
假
,在C程式用整數
1
0
表示。
注意:
>=, <=, ==, !=
這種符号之間不能存在空格。
5.邏輯運算符
C語言中的邏輯運算符:
6.三目運算符
C語言中的三目運算符:
?:
,其格式為:
表達式1 ? 表達式2 : 表達式3;
執行過程是:
先判斷表達式1的值是否為真,如果是真的話執行表達式2;如果是假的話執行表達式3。
7.運算符大比拼之優先級比較
各種運算符号的順序:
優先級别為1的優先級最高,優先級别為10的優先級别最低。
十四、分支結構
1.簡單if語句
C語言中的分支結構語句中的
if條件
語句。
簡單if語句的基本結構如下:
if(表達式)
{
執行代碼塊;
}
其語義是:如果表達式的值為真,則執行其後的語句,否則不執行該語句。
if()
後面沒有分号,直接寫
{}
2.if-else語句
簡單的
if-else
語句的基本結構:
語義是: 如果表達式的值為真,則執行代碼塊1,否則執行代碼塊2。
3.多重if-else語句
C語言中多重
if-else
語句,其結構如下:
語義是:依次判斷表達式的值,當出現某個值為真時,則執行對應代碼塊,否則執行代碼塊n。
注意:當某一條件為真的時候,則不會向下執行該分支結構的其他語句。
4.嵌套if-else語句
C語言中嵌套
if-else
語句。嵌套
if-else
語句的意思,就是在
if-else
語句中,再寫
if-else
語句。其一般形式為:
十五、循環結構
1.while循環
反複不停的執行某個動作就是江湖人稱的循環 。
C語言中有三種循環結構,先看一下C語言while循環的結構
其中表達式表示循環條件,執行代碼塊為循環體。
while語句的語義是:計算表達式的值,當值為真(非0)時, 執行循環體代碼塊。
- while語句中的表達式一般是關系表達或邏輯表達式,當表達式的值為假時不執行循環體,反之則循環體一直執行。
- 一定要記着在循環體中改變循環變量的值,否則會出現死循環(無休止的執行)。
- 循環體如果包括有一個以上的語句,則必須用{}括起來,組成複合語句。
2.do-while循環
C語言中的
do-while
循環,一般形式如下:
do-while循環語句的語義是:
它先執行循環中的執行代碼塊,然後再判斷while中表達式是否為真,如果為真則繼續循環;如果為假,則終止循環。是以,do-while循環至少要執行一次循環語句。
注意:mtianyan: 使用do-while結構語句時,while括号後必須有分号。
3.for循環
c語言中for循環一般形式:
它的執行過程如下:
- 執行表達式1,對循環變量做初始化;
- 判斷表達式2,若其值為真(非0),則執行for循環體中執行代碼塊,然後向下執行;若其值為假(0),則結束循環;
- 執行表達式3,(i++)等對于循環變量進行操作的語句;
- 執行for循環中執行代碼塊後執行第二步;第一步初始化隻會執行一次。
- 循環結束,程式繼續向下執行。
注意:for循環中的兩個分号一定要寫
在for循環中:
- 表達式1是一個或多個指派語句,它用來控制變量的初始值;
- 表達式2是一個關系表達式,它決定什麼時候退出循環;
- 表達式3是循環變量的步進值,定義控制循環變量每循環一次後按什麼方式變化。
- 這三部分之間用分号 ; 分開。
使用for語句應該注意:
- for循環中的“表達式1、2、3”均可不寫為空,但兩個分号(;;)不能預設。
- 省略“表達式1(循環變量賦初值)”,表示不對循環變量賦初始值。
- 省略“表達式2(循環條件)”,不做其它處理,循環一直執行(死循環)。
- 省略“表達式3(循環變量增減量)”,不做其他處理,循環一直執行(死循環)。
- 表達式1可以是設定循環變量的初值的指派表達式,也可以是其他表達式。
- 表達式1和表達式3可以是一個簡單表達式也可以是多個表達式以逗号分割。
- 表達式2一般是關系表達式或邏輯表達式,但也可是數值表達式或字元表達式,隻要其值非零,就執行循環體。
- 各表達式中的變量一定要在for循環之前定義。
3.三種循環比較
while, do-while和for三種循環在具體的使用場合上是有差別的,如下:
- 在知道循環次數的情況下更适合使用for循環;
- 在不知道循環次數的情況下适合使用while或者do-while循環:
- 如果有可能一次都不循環應考慮使用while循環
- 如果至少循環一次應考慮使用do-while循環。
- 但是從本質上講,while,do-while和for循環之間是可以互相轉換的。
4.多重循環
多重循環就是在循環結構的循環體中又出現循環結構。
在實際開發中一般最多用到三層重循環。
因為循環層數越多,運作時間越長,程式越複雜,是以一般用2-3層多重循環就可以了。另外不同循環之間也是可以嵌套的。
多重循環在執行的過程中,外層循環為父循環,内層循環為子循環,
**父循環一次,子循環需要全部執行完,直到跳出循環。**父循環再進入下一次,子循環繼續執行…
十六、結束語句
1.break語句
那麼循環5次的時候,需要中斷不繼續訓練。在C語言中,可以使用
break
語句進行該操作.
使用
break
語句時注意以下幾點:
- 在沒有循環結構的情況下,break不能用在單獨的if-else語句中。
- 在多層循環中,一個break語句隻跳出目前循環。
2.continue語句
那麼循環5次的時候,需要中斷後繼續訓練。在C語言中,可以使用continue語句進行該操作
continue語句的作用是結束本次循環開始執行下一次循環。
break語句與continue語句的差別是:
break是跳出目前整個循環,continue是結束本次循環開始下一次循環。
十七、局部與全局
C語言中的變量,按作用域範圍可分為兩種,即局部變量和全局變量。局部變量也稱為内部變量。局部變量是在函數内作定義說明的。其作用域僅限于函數内, 離開該函數後再使用這種變量是非法的。在複合語句中也可定義變量,其作用域隻在複合語句範圍内。
全局變量也稱為外部變量,它是在函數外部定義的變量。它不屬于哪一個函數,它屬于一個源程式檔案。其作用域是整個源程式。
十八、變量存儲類别
mtianyan: C語言根據變量的生存周期來劃分,可以分為靜态存儲方式和動态存儲方式。
靜态存儲方式:是指在程式運作期間配置設定固定的存儲空間的方式。靜态存儲區中存放了在整個程式執行過程中都存在的變量,如全局變量。
動态存儲方式:是指在程式運作期間根據需要進行動态的配置設定存儲空間的方式。動态存儲區中存放的變量是根據程式運作的需要而建立和釋放的,通常包括:函數形式參數;自動變量;函數調用時的現場保護和傳回位址等。
C語言中存儲類别又分為四類:
- 自動(auto)、
- 靜态(static)、
- 寄存器的(register)
- 外部的(extern)。
十九、内部函數與外部函數
在C語言中不能被其他源檔案調用的函數稱謂内部函數 ,内部函數由static關鍵字來定義,是以又被稱謂靜态函數,形式為:
static [資料類型] 函數名([參數])
這裡的static是對函數的作用範圍的一個限定,限定該函數隻能在其所處的源檔案中使用,是以在不同檔案中出現相同的函數名稱的内部函數是沒有問題的。
在C語言中能被其他源檔案調用的函數稱謂外部函數 ,外部函數由extern關鍵字來定義,形式為:
extern [資料類型] 函數名([參數])
C語言規定,在沒有指定函數的作用範圍時,系統會預設認為是外部函數,是以當需要定義外部函數時extern也可以省略。
靜态變量隻指派一次
二十、數組初體驗
程式中也需要容器,隻不過該容器有點特殊,它在程式中是一塊連續的,大小固定并且裡面的資料類型一緻的記憶體空間,它還有個好聽的名字叫數組。可以将數組了解為大小固定,所放物品為同類的一個購物袋,在該購
物袋中的物品是按一定順序放置的。
1.我們來看一下如何聲明一個數組:
資料類型 數組名稱[長度];
數組隻聲明也不行啊,看一下數組是如何初始化的。說到初始化,C語言中的數組初始化是有三種形式的,分别是:
- 資料類型 數組名稱[長度n] = {元素1,元素2…元素n};
- 資料類型 數組名稱[] = {元素1,元素2…元素n};
- 資料類型 數組名稱[長度n]; 數組名稱[0] = 元素1; 數組名稱[1] = 元素2; 數組名稱[n-1] = 元素n;
我們将資料放到數組中之後又如何擷取數組中的元素呢?
擷取數組元素時: 數組名稱[元素所對應下标];
如:初始化一個數組 int arr[3] = {1,2,3}; 那麼arr[0]就是元素1。
注意:
數組的下标均以0開始;
數組在初始化的時候,數組内元素的個數不能大于聲明的數組長度;
mtianyan: 如果采用第一種初始化方式,元素個數小于數組的長度時,多餘的數組元素初始化為0;
在聲明數組後沒有進行初始化的時候,靜态(static)和外部(extern)類型的數組元素初始化元素為0,自動(auto)類型的數組的元素初始化值不确定。
2.數組的周遊
數組就可以采用循環的方式将每個元素周遊出來,而不用人為的每次擷取指定某個位置上的元素,例如我們用for循環周遊一個數組:
注意以下幾點:
- 最好避免出現數組越界通路,循環變量最好不要超出數組的長度.
- C語言的數組長度一經聲明,長度就是固定,無法改變,并且C語言并不提供計算數組長度的方法。
由于C語言是沒有檢查數組長度改變或者數組越界的這個機制,可能會在編輯器中編譯并通過,但是結果就不能肯定了,是以還是不要越界或者改變數組的長度
3.數組作為函數參數
數組可以由整個數組當作函數的參數,也可以由數組中的某個元素當作函數的參數:
整個數組當作函數參數,即把數組名稱傳入函數中,例如:
數組中的元素當作函數參數,即把數組中的參數傳入函數中,例如:
數組作為函數參數時注意以下事項:
- 數組名作為函數實參傳遞時,函數定義處作為接收參數的數組類型形參既可以指定長度也可以不指定長度。
- 數組元素作為函數實參傳遞時,數組元素類型必須與形參資料類型一緻。
4.字元串與數組
C語言中,是沒有辦法直接定義字元串資料類型的,但是我們可以使用數組來定義我們所要的字元串。一般有以下兩種格式:
- char 字元串名稱[長度] = “字元串值”;
- char 字元串名稱[長度] = {‘字元1’,‘字元2’,…,‘字元n’,’\0’};
- []中的長度是可以省略不寫的;
- 采用第2種方式的時候最後一個元素必須是’\0’,’\0’表示字元串的結束标志;
- 采用第2種方式的時候在數組中不能寫中文。
- 在輸出字元串的時候要使用:printf(“%s”,字元數組名字);或者puts(字元數組名字);。
5.mtianyan:字元串函數
常用的字元串函數如下(strlen,strcmp,strcpy,strcat,atoi):
使用字元串函數注意以下事項:
- strlen()擷取字元串的長度,在字元串長度中是不包括‘\0’而且漢字和字母的長度是不一樣的
- strcmp()在比較的時候會把字元串先轉換成ASCII碼再進行比較,傳回的結果為0表示s1和s2的ASCII碼相等,傳回結果為1表示s1比s2的ASCII碼大,傳回結果為-1表示s1比s2的ASCII碼小
- strcpy()拷貝之後會覆寫原來字元串且不能對字元串常量進行拷貝
- strcat在使用時s1與s2指的記憶體空間不能重疊,且s1要有足夠的空間來容納要複制的字元串
6.多元數組
多元數組的定義格式是:
資料類型 數組名稱[常量表達式1][常量表達式2]…[常量表達式n];
定義了一個名稱為num,資料類型為int的二維數組。其中第一個[3]表示第一維下标的長度,就像購物時分類存放的購物;第二個[3]表示第二維下标的長度,就像每個購物袋中的元素。
多元數組的初始化與一維數組的初始化類似也是分兩種:
- 資料類型 數組名稱[常量表達式1][常量表達式2]…[常量表達式n] = {{值1,…,值n},{值1,…,值n},…,{值1,…,值n}};
- 資料類型 數組名稱[常量表達式1][常量表達式2]…[常量表達式n]; 數組名稱[下标1][下标2]…[下标n] = 值;
多元數組初始化要注意以下事項:
- 采用第一種始化時數組聲明必須指定列的維數。mtianyan: 因為系統會根據數組中元素的總個數來配置設定空間,當知道元素總個數以及列的維數後,會直接計算出行的維數;
- 采用第二種初始化時數組聲明必須同時指定行和列的維數。
二維數組定義的時候,可以不指定行的數量,但是必須指定列的數量
二十一、C語言最核心的指針
說到指針,就不可能脫離開記憶體,學會指針的人分為兩種,一種是不了解記憶體模型,另外一種則是了解。
不了解的對指針的了解就停留在“指針就是變量的位址”這句話,會比較害怕使用指針,特别是各種進階操作。
而了解記憶體模型的則可以把指針用得爐火純青!
想學好C語言,很關鍵就是搞懂記憶體、指針、還有各種編譯連結,
1、記憶體本質
程式設計的本質其實就是操控資料,資料存放在記憶體中。
是以,如果能更好地了解記憶體的模型,以及 C 如何管理記憶體,就能對程式的工作原理洞若觀火,進而使程式設計能力更上一層樓。
大家真的别認為這是空話,我大一整年都不敢用 C 寫上千行的程式也很抗拒寫 C。
因為一旦上千行,經常出現各種莫名其妙的記憶體錯誤,一不小心就發生了 coredump...... 而且還無從排查,分析不出原因。
相比之下,那時候最喜歡 Java,在 Java 裡随便怎麼寫都不會發生類似的異常,頂多偶爾來個 NullPointerException,也是比較好排查的。
直到後來對記憶體和指針有了更加深刻的認識,才慢慢會用 C 寫上千行的項目,也很少會再有記憶體問題了。(過于自信
「指針存儲的是變量的記憶體位址」這句話應該任何講 C 語言的書都會提到吧。
是以,要想徹底了解指針,首先要了解 C 語言中變量的存儲本質,也就是記憶體。
(1)記憶體編址
計算機的記憶體是一塊用于存儲資料的空間,由一系列連續的存儲單元組成,就像下面這樣,
每一個單元格都表示 1 個 Bit,一個 bit 在 EE 專業的同學看來就是高低電位,而在 CS 同學看來就是 0、1 兩種狀态。
由于 1 個 bit 隻能表示兩個狀态,是以大佬們規定 8個 bit 為一組,命名為 byte。
并且将 byte 作為記憶體尋址的最小單元,也就是給每個 byte 一個編号,這個編号就叫記憶體的位址。
這就相當于,我們給小區裡的每個單元、每個住戶都配置設定一個門牌号,在生活中,我們需要保證門牌号唯一,這樣就能通過門牌号很精準的定位到一家人。
同樣,在計算機中,我們也要保證給每一個 byte 的編号都是唯一的,這樣才能夠保證每個編号都能通路到唯一确定的 byte。
(2)記憶體位址空間
上面我們說給記憶體中每個 byte 唯一的編号,那麼這個編号的範圍就決定了計算機可尋址記憶體的範圍。
所有編号連起來就叫做記憶體的位址空間,這和大家平時常說的電腦是 32 位還是 64 位有關。
早期 Intel 8086、8088 的 CPU 就是隻支援 16 位位址空間,寄存器和位址總線都是 16 位,這意味着最多對
2^16 = 64 Kb
的記憶體編号尋址。
這點記憶體空間顯然不夠用,後來,80286 在 8086 的基礎上将位址總線和位址寄存器擴充到了20 位,也被叫做 A20 位址總線。
當時在寫 mini os 的時候,還需要通過 BIOS 中斷去啟動 A20 位址總線的開關。
但是,現在的計算機一般都是 32 位起步了,32 位意味着可尋址的記憶體範圍是
2^32 byte = 4GB
。
是以,如果你的電腦是 32 位的,那麼你裝超過 4G 的記憶體條也是無法充分利用起來的。
好了,這就是記憶體和記憶體編址。
(3)變量的本質
有了記憶體,接下來我們需要考慮,int、double 這些變量是如何存儲在 0、1 單元格的。
在 C 語言中我們會這樣定義變量:
int a = 999;
char c = 'c';
當你寫下一個變量定義的時候,實際上是向記憶體申請了一塊空間來存放你的變量。
我們都知道 int 類型占 4 個位元組,并且在計算機中數字都是用補碼(不了解補碼的記得去百度)表示的。
999 換算成補碼就是:0000 0011 1110 0111
這裡有 4 個byte,是以需要四個單元格來存儲:
有沒有注意到,我們把高位的位元組放在了低位址的地方,那能不能反過來呢?
當然,這就引出了大端和小端。
像上面這種将高位位元組放在記憶體低位址的方式叫做大端,反之,将低位位元組放在記憶體低位址的方式就叫做小端。
上面隻說明了 int 型的變量如何存儲在記憶體,而 float、char 等類型實際上也是一樣的,都需要先轉換為補碼。
對于多位元組的變量類型,還需要按照大端或者小端的格式,依次将位元組寫入到記憶體單元。
記住上面這兩張圖,這就是程式設計語言中所有變量的在記憶體中的樣子,不管是 int、char、指針、數組、結構體、對象... 都是這樣放在記憶體的。
2、指針是什麼啥?
變量放在哪?上面我說,定義一個變量實際就是向計算機申請了一塊記憶體來存放。
那如果我們要想知道變量到底放在哪了呢?可以通過運算符
&
來取得變量實際的位址,這個值就是變量所占記憶體塊的起始位址。
PS: 實際上這個位址是虛拟位址,并不是真正實體記憶體上的位址
我們可以把這個位址列印出來
printf("%x", &a);
大概會是像這樣的一串數字:0x7ffcad3b8f3c
上面說,我們可以通過
&
符号擷取變量的記憶體位址,那擷取之後如何來表示這是一個位址,而不是一個普通的值呢?
也就是在 C 語言中如何表示位址這個概念呢?
對,就是指針,你可以這樣
int *pa = &a;
pa 中存儲的就是變量 a 的位址,也叫做指向 a 的指針。
在這裡我想談幾個看起來有點無聊的話題:
為什麼我們需要指針?直接用變量名不行嗎?
當然可以,但是變量名是有局限的。
變量名的本質是什麼?
是變量位址的符号化,變量是為了讓我們程式設計時更加友善,對人友好,可計算機可不認識什麼變量 a,它隻知道位址和指令。
是以當你去檢視 C 語言編譯後的彙編代碼,就會發現變量名消失了,取而代之的是一串串抽象的位址。
你可以認為,編譯器會自動維護一個映射,将我們程式中的變量名轉換為變量所對應的位址,然後再對這個位址去進行讀寫。
也就是有這樣一個映射表存在,将變量名自動轉化為位址:
a | 0x7ffcad3b8f3c
c | 0x7ffcad3b8f2c
h | 0x7ffcad3b8f4c
....
說的好!
可是我還是不知道指針存在的必要性,那麼問題來了,看下面代碼:
int func(...) {
...
};
int main() {
int a;
func(...);
};
假設我有一個需求:
要求在 函數裡要能夠修改
func
函數裡的變量
main
,這下咋整,在
a
函數裡可以直接通過變量名去讀寫
main
a
所在記憶體。
但是在
函數裡是看不見
func
的呀。
a
你說可以通過
&
取位址符号,将
a
的位址傳遞進去:
int func(int address) {
....
};
int main() {
int a;
func(&a);
};
這樣在
func
裡就能擷取到
a
的位址,進行讀寫了。
理論上這是完全沒有問題的,但是問題在于:
編譯器該如何區分一個 int 裡你存的到底是 int 類型的值,還是另外一個變量的位址(即指針)。
這如果完全靠我們程式設計人員去人腦記憶了,會引入複雜性,并且無法通過編譯器檢測一些文法錯誤。
而通過
int *
去定義一個指針變量,會非常明确:這就是另外一個 int 型變量的位址。
編譯器也可以通過類型檢查來排除一些編譯錯誤。
這就是指針存在的必要性。
實際上任何語言都有這個需求,隻不過很多語言為了安全性,給指針戴上了一層枷鎖,将指針包裝成了引用。
可能大家學習的時候都是自然而然的接受指針這個東西,但是還是希望這段啰嗦的解釋對你有一定啟發。
同時,在這裡提點小問題:
既然指針的本質都是變量的記憶體首位址,即一個 int 類型的整數。
那為什麼還要有各種類型呢?
比如 int 指針,float 指針,這個類型影響了指針本身存儲的資訊嗎?
這個類型會在什麼時候發揮作用?
解引用
上面的問題,就是為了引出指針解引用的。
pa
中存儲的是
a
變量的記憶體位址,那如何通過位址去擷取
a
的值呢?
這個操作就叫做解引用,在 C 語言中通過運算符
*
就可以拿到一個指針所指位址的内容了。
比如
*pa
就能獲得
a
的值。
我們說指針存儲的是變量記憶體的首位址,那編譯器怎麼知道該從首位址開始取多少個位元組呢?
這就是指針類型發揮作用的時候,編譯器會根據指針的所指元素的類型去判斷應該取多少個位元組。
如果是 int 型的指針,那麼編譯器就會産生提取四個位元組的指令,char 則隻提取一個位元組,以此類推。
下面是指針記憶體示意圖:
pa
指針首先是一個變量,它本身也占據一塊記憶體,這塊記憶體裡存放的就是
a
變量的首位址。
當解引用的時候,就會從這個首位址連續劃出 4 個 byte,然後按照 int 類型的編碼方式解釋。
别看這個地方很簡單,但卻是深刻了解指針的關鍵。
舉兩個例子來詳細說明:
比如:
float f = 1.0;
short c = *(short*)&f;
你能解釋清楚上面過程,對于
f
變量,在記憶體層面發生了什麼變化嗎?或者
c
的值是多少?1 ?
實際上,從記憶體層面來說,
f
什麼都沒變。
如圖:
假設這是
f
在記憶體中的位模式,這個過程實際上就是把
f
的前兩個 byte 取出來然後按照 short 的方式解釋,然後指派給
c
詳細過程如下:
&f取得f 的首位址
(short*)&f
上面第二步什麼都沒做,這個表達式隻是說 :
“噢,我認為
f
這個位址放的是一個 short 類型的變量”
最後當去解引用的時候
*(short*)&f
時,編譯器會取出前面兩個位元組,并且按照 short 的編碼方式去解釋,并将解釋出的值賦給
c
變量。
這個過程
f
的位模式沒有發生任何改變,變的隻是解釋這些位的方式。
當然,這裡最後的值肯定不是 1,至于是什麼,大家可以去真正算一下。
那反過來,這樣呢?
short c = 1;
float f = *(float*)&c;
如圖:
具體過程和上述一樣,但上面肯定不會報錯,這裡卻不一定。
為什麼?
(float*)&c
會讓我們從
c
的首位址開始取四個位元組,然後按照 float 的編碼方式去解釋。
但是
c
是 short 類型隻占兩個位元組,那肯定會通路到相鄰後面兩個位元組,這時候就發生了記憶體通路越界。
當然,如果隻是讀,大機率是沒問題的。
但是,有時候需要向這個區域寫入新的值,比如:
*(float*)&c = 1.0;
那麼就可能發生 coredump,也就是訪存失敗。
另外,就算是不會 coredump,這種也會破壞這塊記憶體原有的值,因為很可能這是是其它變量的記憶體空間,而我們去覆寫了人家的内容,肯定會導緻隐藏的 bug。
如果你了解了上面這些内容,那麼使用指針一定會更加的自如。
3、結構體和指針
結構體内包含多個成員,這些成員之間在記憶體中是如何存放的呢?
struct fraction {
int num; // 整數部分
int denom; // 小數部分
};
struct fraction fp;
fp.num = 10;
fp.denom = 2;
這是一個定點小數結構體,它在記憶體占 8 個位元組(這裡不考慮記憶體對齊),兩個成員域是這樣存儲的:
我們把 10 放在了結構體中基位址偏移為 0 的域,2 放在了偏移為 4 的域。
接下來我們做一個這樣的操作:
((fraction*)(&fp.denom))->num = 5;
((fraction*)(&fp.denom))->denom = 12;
printf("%d\n", fp.denom); // 輸出多少?
上面這個究竟會輸出多少呢?自己先思考下噢~
接下來我分析下這個過程發生了什麼:
首先,
&fp.denom
表示取結構體 fp 中 denom 域的首位址,然後以這個位址為起始位址取 8 個位元組,并且将它們看做一個 fraction 結構體。
在這個新結構體中,最上面四個位元組變成了 denom 域,而 fp 的 denom 域相當于新結構體的 num 域。
是以:
((fraction*)(&fp.denom))->num = 5
實際上改變的是
fp.denom
,而
((fraction*)(&fp.denom))->denom = 12
則是将最上面四個位元組指派為 12。
當然,往那四位元組記憶體寫入值,結果是無法預測的,可能會造成程式崩潰,因為也許那裡恰好存儲着函數調用棧幀的關鍵資訊,也可能那裡沒有寫入權限。
大家初學 C 語言的很多 coredump 錯誤都是類似原因造成的。
是以最後輸出的是 5。
為什麼要講這種看起來莫名其妙的代碼?
就是為了說明結構體的本質其實就是一堆的變量打包放在一起,而通路結構體中的域,就是通過結構體的起始位址,也叫基位址,然後加上域的偏移。
其實,C++、Java 中的對象也是這樣存儲的,無非是他們為了實作某些面向對象的特性,會在資料成員以外,添加一些 Head 資訊,比如C++ 的虛函數表。
實際上,我們是完全可以用 C 語言去模仿的。
這就是為什麼一直說 C 語言是基礎,你真正懂了 C 指針和記憶體,對于其它語言你也會很快的了解其對象模型以及記憶體布局。
4、多級指針
說起多級指針這個東西,我以前上學的時候最多了解到 2 級,再多真的會把我繞暈,經常也會寫錯代碼。
你要是給我寫個這個:
int ******p
能把我搞崩潰,我估計很多同學現在就是這種情況????
其實,多級指針也沒那麼複雜,就是指針的指針的指針的指針......非常簡單。
今天就帶大家認識一下多級指針的本質。
首先,我要說一句話,沒有多級指針這種東西,指針就是指針,多級指針隻是為了我們友善表達而取的邏輯概念。
首先看下生活中的快遞櫃:
這種大家都用過吧,每個格子都有一個編号,我們隻需要拿到編号,然後就能找到對應的格子,取出裡面的東西。
這裡的格子就是記憶體單元,編号就是位址,格子裡放的東西就對應存儲在記憶體中的内容。
假設我把一本書,放在了 03 号格子,然後把 03 這個編号告訴你,你就可以根據 03 去取到裡面的書。
那如果我把書放在 05 号格子,然後在 03 号格子隻放一個小紙條,上面寫着:「書放在 05 号」。
你會怎麼做?
當然是打開 03 号格子,然後取出了紙條,根據上面内容去打開 05 号格子得到書。
這裡的 03 号格子就叫指針,因為它裡面放的是指向其它格子的小紙條(位址)而不是具體的書。
明白了嗎?
那我如果把書放在 07 号格子,然後在 05 号格子 放一個紙條:「書放在 07号」,同時在03号格子放一個紙條「書放在 05号」
這裡的 03 号格子就叫二級指針,05 号格子就叫指針,而 07 号就是我們平常用的變量。
依次,可類推出 N 級指針。
是以你明白了嗎?同樣的一塊記憶體,如果存放的是别的變量的位址,那麼就叫指針,存放的是實際内容,就叫變量。
int a;
int *pa = &a;
int **ppa = &pa;
int ***pppa = &ppa;
上面這段代碼,
pa
就叫一級指針,也就是平時常說的指針,
ppa
就是二級指針。
記憶體示意圖如下:
不管幾級指針有兩個最核心的東西:
- 指針本身也是一個變量,需要記憶體去存儲,指針也有自己的位址
- 指針記憶體存儲的是它所指向變量的位址
這就是我為什麼多級指針是邏輯上的概念,實際上一塊記憶體要麼放實際内容,要麼放其它變量位址,就這麼簡單。
怎麼去解讀
int **a
這種表達呢?
int ** a
可以把它分為兩部分看,即
int*
和
*a
,後面
*a
中的
*
表示
a
是一個指針變量,前面的
int*
表示指針變量
a
隻能存放
int*
型變量的位址。
對于二級指針甚至多級指針,我們都可以把它拆成兩部分。
首先不管是多少級的指針變量,它首先是一個指針變量,指針變量就是一個
*
,其餘的
*
表示的是這個指針變量隻能存放什麼類型變量的位址。
int****a
表示指針變量
a
隻能存放
int***
5、指針與數組
(1)一維數組
數組是 C 自帶的基本資料結構,徹底了解數組及其用法是開發高效應用程式的基礎。
數組和指針表示法緊密關聯,在合适的上下文中可以互換。
如下:
int array[10] = {10, 9, 8, 7};
printf("%d\n", *array); // 輸出 10
printf("%d\n", array[0]); // 輸出 10
printf("%d\n", array[1]); // 輸出 9
printf("%d\n", *(array+1)); // 輸出 9
int *pa = array;
printf("%d\n", *pa); // 輸出 10
printf("%d\n", pa[0]); // 輸出 10
printf("%d\n", pa[1]); // 輸出 9
printf("%d\n", *(pa+1)); // 輸出 9
在記憶體中,數組是一塊連續的記憶體空間:
第 0 個元素的位址稱為數組的首位址,數組名實際就是指向數組首位址,當我們通過
array[1]
或者
*(array + 1)
去通路數組元素的時候。
實際上可以看做
address[offset]
,
address
為起始位址,
offset
為偏移量,但是注意這裡的偏移量
offset
不是直接和
address
相加,而是要乘以數組類型所占位元組數,也就是:
address + sizeof(int) * offset
學過彙編的同學,一定對這種方式不陌生,這是彙編中尋址方式的一種:基址變址尋址。
看完上面的代碼,很多同學可能會認為指針和數組完全一緻,可以互換,這是完全錯誤的。
盡管數組名字有時候可以當做指針來用,但數組的名字不是指針。
最典型的地方就是在 sizeof:
printf("%u", sizeof(array));
printf("%u", sizeof(pa));
第一個将會輸出 40,因為
array
包含有 10 個int類型的元素,而第二個在 32 位機器上将會輸出 4,也就是指針的長度。
為什麼會這樣呢?
站在編譯器的角度講,變量名、數組名都是一種符号,它們都是有類型的,它們最終都要和資料綁定起來。
變量名用來指代一份資料,數組名用來指代一組資料(資料集合),它們都是有類型的,以便推斷出所指代的資料的長度。
對,數組也有類型,我們可以将 int、float、char 等了解為基本類型,将數組了解為由基本類型派生得到的稍微複雜一些的類型,
數組的類型由元素的類型和數組的長度共同構成。而
sizeof
就是根據變量的類型來計算長度的,并且計算的過程是在編譯期,而不會在程式運作時。
編譯器在編譯過程中會建立一張專門的表格用來儲存變量名及其對應的資料類型、位址、作用域等資訊。
sizeof
是一個操作符,不是函數,使用
sizeof
時可以從這張表格中查詢到符号的長度。
是以,這裡對數組名使用
sizeof
可以查詢到數組實際的長度。
pa
僅僅是一個指向 int 類型的指針,編譯器根本不知道它指向的是一個整數,還是一堆整數。
雖然在這裡它指向的是一個數組,但數組也隻是一塊連續的記憶體,沒有開始和結束标志,也沒有額外的資訊來記錄數組到底多長。
是以對
pa
使用
sizeof
隻能求得的是指針變量本身的長度。
也就是說,編譯器并沒有把
pa
和數組關聯起來,
pa
僅僅是一個指針變量,不管它指向哪裡,
sizeof
求得的永遠是它本身所占用的位元組數。
(2)二維數組
大家不要認為二維數組在記憶體中就是按行、列這樣二維存儲的,實際上,不管二維、三維數組... 都是編譯器的文法糖。
存儲上和一維數組沒有本質差別,舉個例子:
int array[3][3] = {{1, 2,3}, {4, 5,6},{7, 8, 9}};
array[1][1] = 5;
或許你以為在記憶體中
array
數組會像一個二維矩陣:
1 2 3
4 5 6
7 8 9
可實際上它是這樣的:
1 2 3 4 5 6 7 8 9
和一維數組沒有什麼差別,都是一維線性排列。
當我們像
array[1][1]
這樣去通路的時候,編譯器會怎麼去計算我們真正所通路元素的位址呢?
為了更加通用化,假設數組定義是這樣的:
int array[n][m]
通路:
array[a][b]
那麼被通路元素位址的計算方式就是:
array + (m * a + b)
這個就是二維數組在記憶體中的本質,其實和一維數組是一樣的,隻是文法糖包裝成一個二維的樣子。
6、 void 指針
想必大家一定看到過 void 的這些用法:
void func();
int func1(void);
在這些情況下,void 表達的意思就是沒有傳回值或者參數為空。
但是對于 void 型指針卻表示通用指針,可以用來存放任何資料類型的引用。
下面的例子就 是一個 void 指針:
void *ptr;
void 指針最大的用處就是在 C 語言中實作泛型程式設計,因為任何指針都可以被賦給 void 指針,void 指針也可以被轉換回原來的指針類型, 并且這個過程指針實際所指向的位址并不會發生變化。
比如:
int num;
int *pi = #
printf("address of pi: %p\n", pi);
void* pv = pi;
pi = (int*) pv;
printf("address of pi: %p\n", pi);
這兩次輸出的值都會是一樣:
平常可能很少會這樣去轉換,但是當你用 C 寫大型軟體或者寫一些通用庫的時候,一定離不開 void 指針,這是 C 泛型的基石,比如 std 庫裡的 sort 函數申明是這樣的:
void qsort(void *base,int nelem,int width,int (*fcmp)(const void *,const void *));
所有關于具體元素類型的地方全部用 void 代替。
void 還可以用來實作 C 語言中的多态,這是一個挺好玩的東西。
不過也有需要注意的,不能對 void 指針解引用
int num;
void *pv = (void*)#
*pv = 4; // 錯誤
因為解引用的本質就是編譯器根據指針所指的類型,然後從指針所指向的記憶體連續取 N 個位元組,然後将這 N 個位元組按照指針的類型去解釋。
比如 int *型指針,那麼這裡 N 就是 4,然後按照 int 的編碼方式去解釋數字。
但是 void,編譯器是不知道它到底指向的是 int、double、或者是一個結構體,是以編譯器沒法對 void 型指針解引用。
關于指針想寫的内容還有很多,這其實也隻算是開了個頭,限于篇幅,以後有機會補齊以下内容:
- 二維數組和二維指針
- 數組指針和指針數組
- 指針運算
- 函數指針
- 動态記憶體配置設定: malloc 和 free
- 堆、棧
- 函數參數傳遞方式
- 記憶體洩露
- 數組退化成指針
- const 修飾指針
- ...
到此基本上涵蓋了 C 語言的基本知識。