【54.1 本節閱讀前的名詞約定。】
變量可以粗略的分成兩類,一類是全局變量,一類是局部變量。如果更深一步精細劃分,全局變量還可以分成“普通全局變量”和“靜態全局變量”,局部變量也可以分成“普通局部變量”和“靜態局部變量”,也就是說,若精細劃分,可以分成四類。其中“靜態全局變量”和“靜態局部變量”多了一個前綴“靜態”,這個前綴“靜態”是因為在普通的變量前面多加了一個修飾關鍵詞“static”,這部分的內容后續章節會講到。本節重點為了讓大家理解內存模型的“棧”,暫時不考慮“靜態變量”的情況,人為約定,本節所涉及的“全局變量”僅僅默認為“普通全局變量”,“局部變量”僅僅默認為“普通局部變量”。
【54.2 如何判定全局變量和局部變量?】
全局變量就是在函數外面定義的變量,局部變量就是在函數內部定義的變量,這是最直觀的判定方法。下面的例子能很清晰地說明全局變量和局部變量的判定方法:
unsigned char a; //在函數外面定義的,所以是全局變量。
void main() //主函數
{
unsigned char b; //在函數內部定義的,所以是局部變量。
b=a;
while(1)
{
}
}
【54.3 全局變量和局部變量的內存模型。】
單片機內存包括ROM和RAM兩部分,ROM存儲的是單片機程序中的指令和一些不可更改的常量數據,而RAM存放的是可以被更改的變量數據,也就是說,全局變量和局部變量都是存放在RAM,但是,雖然都是存放在RAM,全局變量和局部變量之間的內存模型還是有明顯的區別的,因此,分了兩個不同的RAM區,全局變量占用的RAM區稱為“全局數據區”,局部變量占用的RAM區稱為“棧”,因為我后面會用賓館來比喻“棧”,為了方便記憶,大家可以把“棧”想象成 “客棧”來記憶。它們的內存模型到底有什么本質的區別呢?“全局數據區”就像你自己家的房間,是唯一的,一個房間的地址只能你一個人住(假設你還沒結婚的時候),而且是永久的,所以說每個全局變量都有唯一對應的RAM地址,不可能重復的。而“棧”就像賓館客棧,一年下來每天晚上住的人不一樣,每個人在里面居住的時間是有期限的,不是長久的,一個房間的地址一年下來每天可能住進不同的人,不是唯一的。“全局數據區”的全局變量擁有永久產權,“棧”區的局部變量只能臨時居住在賓館客棧,地址不是唯一的,有期限的。全局變量像私人區,局部變量像公共區。“棧”的這片公共區,是給程序里所有函數內部的局部變量共用的,函數被調用的時候,該函數內部的每個局部變量就會被分配對應到“棧”的某個RAM地址,函數調用結束后,該局部變量就失效,因此它對應的“棧”的RAM空間就被收回以便給下一個被調用的函數的局部變量占用。請看下面這個例子,我借用“賓館客棧”來比喻局部變量所在的“棧”。
void HanShu(void); //子函數的聲明
void HanShu(void) //子函數的定義
{
unsigned char a; //局部變量
a=1;
}
void main() //主函數
{
HanShu() ; //子函數的調用
}
分析:上述例子,單片機從主函數main往下執行,首先遇到HanShu子函數的調用,所以就跳到HanShu函數的定義那里開始執行,此時的局部變量a開始被分配在RAM的“棧區”的某個地址,相當于你入住賓館被分配到某個房間。單片機執行完子函數HanShu后,局部變量a在RAM的“棧區”所分配的地址被收回,局部變量a消失,被收回的RAM地址可能會被系統重新分配給其它被調用的函數的局部變量,此時相當于你離開賓館,從此你跟那個賓館的房間沒有啥關系,你原來在賓館入住的那個房間會被賓館老板重新分配給其他的客人入住。全局變量的作用域是永久性不受范圍限制的,而局部變量的作用域就是它所在函數的內部范圍。全局變量的“全局數據區”是永久的私人房子(這里的“永久”僅僅是舉一個例子,別拿“70年產權”來抬杠),局部變量的“棧”是臨時居住的“客棧”。重要的事情說兩遍,再次總結如下:
(1)每定義一個新的全局變量,就意味著多開銷一個新的RAM內存。而每定義一個局部變量,只要在函數內部所定義的局部變量總數不超過單片機的“棧”區,此時的局部變量不開銷新的RAM內存,因為局部變量是臨時借用“棧”區的,使用后就還給“棧”,“棧”是公共區,可以重復利用,可以服務若干個不同的函數內部的局部變量。
(2)單片機每次進入執行函數時,局部變量都會被初始化改變,而全局變量則不會被初始化,全局變量是一直保存之前最后一次更改的值。
【54.4 三個常見疑問。】
第一個疑問:
問:“全局數據區”和“棧區“是誰在幕后分配的,怎么分配的?
答:是C編譯器自動分配的,至于怎么分配,誰分配多一點,誰分配少一點,C編譯器會有一個默認的比例分配,我們一般都不用管。
第二個疑問:
問:“棧”區是臨時借用的,子函數被調用的時候,它內部的局部變量才會“臨時”被分配到“棧”區的某個地址,那么問題來了,誰在幕后主持“棧區”這些分配的工作,難道也是C編譯器?C編譯器不是在編譯程序的時候一次性就做完了編譯工作然后就退出歷史舞臺了嗎?難道我們程序已經在單片機內部運轉的時候,編譯器此時還在幕后指手畫腳的起作用?
答:單片機已經上電開始運行程序的時候,編譯器是不可能起作用的。所以,真相只有一個,“棧區”分配給函數內部局部變量的工作,確實是C編譯器做的,唯一需要注意的地方是,它不是“現炒現賣”,而是在單片機上電前,C編譯器就把所有函數內部的局部變量的分配工作就規劃好了,都指定了如果某個函數一旦被調用,該函數內部的哪個局部變量應該分到“棧區”的哪個地址,C編譯器都是事先把這些“后事”都交代完畢了才“結束自己的生命”,后面,等單片機上電開始工作的時候,雖然C編譯器此時“不在”了,但是單片機都是嚴格按照C編譯器交代的“遺囑”開始工作和分配“棧區”的。因此,“棧區”的“臨時分配”非真正嚴格意義上的“臨時分配”。
第三個疑問:
問:函數內部所定義的局部變量總數不超過單片機的“棧”區的RAM數量,那,萬一超過了“棧”區的RAM數量,后果嚴重嗎?
答:后果特別嚴重。這種情況,專業術語叫“爆棧”。程序會出現異常,而且是莫名其妙的異常。為了避免這種情況,一般在編寫程序的時候,函數內部都不能定義大數組的局部變量,局部變量的數量不能定義太多太大,尤其要避免剛才所說的定義開辟大數組局部變量這種情況。大數組的定義應該定義成全局變量,或者定義成“靜態的局部變量”(“靜態”這部分相關的內容后面章節會講到)。有一些C編譯器,遇到“爆棧”的情況,會好心跟你提醒讓你編譯不過去,但是也有一些C編譯器可能就不會給你提醒,所以大家以后做項目寫函數的時候,要對“爆棧”心存敬畏。
【54.5 全局變量和局部變量的優先級。】
剛才說到,全局變量的作用域是永久性并且不受范圍限制的,而局部變量的作用域就是它所在函數的內部范圍,那么問題來,假如局部變量和全局變量的名字重名了,此時函數內部執行的變量到底是局部變量還是全局變量?這個問題就涉及到優先級。注意,當面對同名的局部變量和全局變量時,函數內部執行的變量是局部變量,也就是局部變量在函數內部要比全局變量的優先級高。為了深刻理解“全局變量和局部變量的優先級”,強烈建議大家必須仔細看完下面列舉的三個練習例子。
【54.6 例程練習和分析。】
請看下面第一個例子:
/\*---C語言學習區域的開始。-----------------------------------------------\*/
unsigned char a=5; //此處第1個a是全局變量。
void main() //主函數
{
unsigned char a=2; //此處第2個a是局部變量。跟上面全局變量的第1個a重名了!
View(a); //把a發送到電腦端的串口助手軟件上觀察。
while(1)
{
}
}
/\*---C語言學習區域的結束。-----------------------------------------------\*/
分析:
上述例子,有2個變量重名了!其中一個是全局變量,另外一個是局部變量。此時輸出顯示的結果是5還是2?正確的答案是2。因為在函數內部,函數內部的局部變量比全局變量的優先級更加高。此時View(a)是第2個局部變量的a,而不是第1個全局變量的a。雖然這里的兩個a重名了,但是它們的內存模型不一樣,第1個全局變量的a是分配在“全局數據區”是具有唯一的地址的,而第2個局部變量的a是被分配在臨時的“棧”區的,寄生在main函數內部。
再看下面第二個例子:
/\*---C語言學習區域的開始。-----------------------------------------------\*/
void HanShu(void); //函數聲明
unsigned char a=5; //此處第1個a是全局變量。
void HanShu(void) //函數定義
{
unsigned char a=3; //此處第2個a是局部變量。
}
void main() //主函數
{
unsigned char a=2; //此處第3個a也是局部變量。
HanShu(); //子函數被調用
View(a); //把a發送到電腦端的串口助手軟件上觀察。
while(1)
{
}
}
/\*---C語言學習區域的結束。-----------------------------------------------\*/
分析:
上述例子,有3個變量重名了!其中一個是全局變量,另外兩個是局部變量。此時輸出顯示的結果是5還是3還是2?正確的答案是2。因為,HanShu這個子函數是被調用結束之后,才執行View(a)的,就意味HanShu函數內部的局部變量(第2個局部變量a)是在執行View(a)語句的時候就消亡不存在了,所以此時View(a)的a是第3個局部變量的a(在main函數內部定義的局部變量的a)。
再看下面第三個例子:
/\*---C語言學習區域的開始。-----------------------------------------------\*/
void HanShu(void); //函數聲明
unsigned char a=5; //此處第1個a是全局變量。
void HanShu(void) //函數定義
{
unsigned char a=3; //此處第2個a是局部變量。
}
void main() //主函數
{
HanShu(); //子函數被調用
View(a); //把a發送到電腦端的串口助手軟件上觀察。
while(1)
{
}
}
/\*---C語言學習區域的結束。-----------------------------------------------\*/
分析:
上述例子,有2個變量重名了!其中一個是全局變量,另外一個是局部變量。此時輸出顯示的結果是5還是3?正確的答案是5。因為,HanShu這個子函數是被調用結束之后,才執行View(a)的,就意味HanShu函數內部的局部變量(第2個局部變量)是在執行View(a)語句的時候就消亡不存在了,同時,因為此時main函數內部也沒有定義a的局部變量,所以此時View(a)的a是必然只能是第1個全局變量的a(在main函數外面定義的全局變量的a)。
【54.7 如何在單片機上練習本章節C語言程序?】
直接復制前面章節中第十一節的模板程序,練習代碼時只需要更改“C語言學習區域”的代碼就可以了,其它部分的代碼不要動。編譯后,把程序下載進帶串口的51學習板,通過電腦端的串口助手軟件就可以觀察到不同的變量數值,詳細方法請看第十一節內容。
- 首頁
- 第一節:我的價值觀
- 第二節:初學者的疑惑
- 第三節:單片機最重要的一個特性
- 第四節:平臺軟件和編譯器軟件的簡介
- 第五節:用Keil2軟件關閉,新建,打開一個工程的操作流程
- 第六節:把.c源代碼編譯成.hex機器碼的操作流程
- 第七節:本節預留
- 第八節:把.hex機器碼程序燒錄到單片機的操作流程
- 第九節:本節預留
- 第十節:程序從哪里開始,要到哪里去?
- 第十一節:一個在單片機上練習C語言的模板程序
- 第十二節:變量的定義和賦值
- 【TODO】第十三節:賦值語句的覆蓋性
- 【TODO】第十四節:二進制與字節單位,以及常用三種變量的取值范圍
- 【TODO】第十五節:二進制與十六進制
- 【TODO】第十六節:十進制與十六進制
- 【TODO】第十七節:加法運算的5種常用組合
- 【TODO】第十八節:連加、自加、自加簡寫、自加1
- 【TODO】第十九節:加法運算的溢出
- 【TODO】第二十節:隱藏中間變量為何物?
- 【TODO】第二十一節:減法運算的5種常用組合。
- 【TODO】第二十二節:連減、自減、自減簡寫、自減1
- 【TODO】第二十三節:減法溢出與假想借位
- 【TODO】第二十四節:借用unsigned long類型的中間變量可以減少溢出現象
- 【TODO】第二十五節:乘法運算中的5種常用組合
- 【TODO】第二十六節:連乘、自乘、自乘簡寫,溢出
- 【TODO】第二十七節:整除求商
- 【TODO】第二十八節:整除求余
- 【TODO】第二十九節:“先余后商”和“先商后余”提取數據某位,哪家強?
- 【TODO】第三十節:邏輯運算符的“與”運算
- 【TODO】第三十一節:邏輯運算符的“或”運算
- 【TODO】第三十二節:邏輯運算符的“異或”運算
- 【TODO】第三十三節:邏輯運算符的“按位取反”和“非”運算
- 【TODO】第三十四節:移位運算的左移
- 【TODO】第三十五節:移位運算的右移
- 【TODO】第三十六節:括號的強制功能---改變運算優先級
- 【TODO】第三十七節:單字節變量賦值給多字節變量的疑惑
- 【TODO】第三十八節:第二種解決“運算過程中意外溢出”的便捷方法
- 【TODO】第三十九節:if判斷語句以及常量變量的真假判斷
- 【TODO】第四十節:關系符的等于“==”和不等于“!=”
- 【TODO】第四十一節:關系符的大于“>”和大于等于“>=”
- 【TODO】第四十二節:關系符的小于“<”和小于等于“<=”
- 【TODO】第四十三節:關系符中的關系符:與“&&”,或“||”
- 【TODO】第四十四節:小括號改變判斷優先級
- 【TODO】第四十五節: 組合判斷if...else if...else
- 【TODO】第四十六節: 一維數組
- 【TODO】第四十七節: 二維數組
- 【TODO】第四十八節: while循環語句
- 【TODO】第四十九節: 循環語句do while和for
- 【TODO】第五十節: 循環體內的continue和break語句
- 【TODO】第五十一節: for和while的循環嵌套
- 【TODO】第五十二節: 支撐程序框架的switch語句
- 【TODO】第五十三節: 使用函數的三要素和執行順序
- 【TODO】第五十四節: 從全局變量和局部變量中感悟“棧”為何物
- 【TODO】第五十五節: 函數的作用和四種常見書寫類型
- 【TODO】第五十六節: return在函數中的作用以及四個容易被忽略的功能
- 【TODO】第五十七節: static的重要作用
- 【TODO】第五十八節: const(./book/或code)在定義數據時的作用
- 【TODO】第五十九節: 全局“一鍵替換”功能的#define
- 【TODO】第六十節: 指針在變量(./book/或常量)中的基礎知識
- 【TODO】第六十一節: 指針的中轉站作用,地址自加法,地址偏移法
- 【TODO】第六十二節: 指針,大小端,化整為零,化零為整
- 【TODO】第六十三節: 指針“化整為零”和“化零為整”的“靈活”應用
- 【TODO】第六十四節: 指針讓函數具備了多個相當于return的輸出口
- 【TODO】第六十五節: 指針作為數組在函數中的入口作用
- 【TODO】第六十六節: 指針作為數組在函數中的出口作用
- 【TODO】第六十七節: 指針作為數組在函數中既“入口”又“出口”的作用
- 【TODO】第六十八節: 為函數接口指針“定向”的const關鍵詞
- 【TODO】第六十九節: 宏函數sizeof(./book/)
- 【TODO】第七十節: “萬能數組”的結構體
- 【TODO】第七十一節: 結構體的內存和賦值
- 【TODO】第七十二節: 結構體的指針
- 【TODO】第七十三節: 結構體數據的傳輸存儲和還原
- 【TODO】第七十四節: 結構體指針在函數接口處的頻繁應用
- 【TODO】第七十五節: 指針的名義(例:一維指針操作二維數組)
- 【TODO】第七十六節: 二維數組的指針
- 【TODO】第七十七節: 指針唯一的“單向輸出”通道return
- 【TODO】第七十八節: typedef和#define和enum
- 【TODO】第七十九節: 各種變量常量的命名規范
- 【TODO】第八十節: 單片機IO口驅動LED
- 【TODO】第八十一節: 時間和速度的起源(指令周期和晶振頻率)
- 【TODO】第八十二節: Delay“阻塞”延時控制LED閃爍
- 【TODO】第八十三節: 累計主循環的“非阻塞”延時控制LED閃爍
- 【TODO】第八十四節: 中斷與中斷函數
- 【TODO】第八十五節: 定時中斷的寄存器配置
- 【TODO】第八十六節: 定時中斷的“非阻塞”延時控制LED閃爍
- 【TODO】第八十七節: 一個定時中斷產生N個軟件定時器
- 【TODO】第八十八節: 兩大核心框架理論(四區一線,switch外加定時中斷)
- 【TODO】第八十九節: 跑馬燈的三種境界
- 【TODO】第九十節: 多任務并行處理兩路跑馬燈
- 【TODO】第九十一節: 蜂鳴器的“非阻塞”驅動
- 【TODO】第九十二節: 獨立按鍵的四大要素(自鎖,消抖,非阻塞,清零式濾波)
- 【TODO】第九十三節: 獨立按鍵鼠標式的單擊與雙擊
- 【TODO】第九十四節: 兩個獨立按鍵構成的組合按鍵
- 【TODO】第九十五節: 兩個獨立按鍵的“電腦鍵盤式”組合按鍵
- 【TODO】第九十六節: 獨立按鍵“一鍵兩用”的短按與長按
- 【TODO】第九十七節: 獨立按鍵按住不松手的連續均勻觸發
- 【TODO】第九十八節: 獨立按鍵按住不松手的“先加速后勻速”的觸發
- 【TODO】第九十九節: “行列掃描式”矩陣按鍵的單個觸發(原始版)
- 【TODO】第一百節: “行列掃描式”矩陣按鍵的單個觸發(優化版)
- 【TODO】第一百零一節: 矩陣按鍵鼠標式的單擊與雙擊
- 【TODO】第一百零二節: 兩個“任意行輸入”矩陣按鍵的“有序”組合觸發
- 【TODO】第一百零三節: 兩個“任意行輸入”矩陣按鍵的“無序”組合觸發
- 【TODO】第一百零四節: 矩陣按鍵“一鍵兩用”的短按與長按
- 【TODO】第一百零五節: 矩陣按鍵按住不松手的連續均勻觸發
- 【TODO】第一百零六節: 矩陣按鍵按住不松手的“先加速后勻速”觸發
- 【TODO】第一百零七節: 開關感應器的識別與軟件濾波
- 【TODO】第一百零八節: 按鍵控制跑馬燈的啟動和暫停和停止
- 【TODO】第一百零九節: 按鍵控制跑馬燈的方向
- 【TODO】第一百一十節: 按鍵控制跑馬燈的速度
- 第一百一十一節: 工業自動化設備的開關信號的運動控制
- 【TODO】第一百一十二節: 數碼管顯示的基礎知識
- 【TODO】第一百一十三節: 動態掃描的數碼管顯示數字
- 【TODO】第一百一十四節: 動態掃描的數碼管顯示小數點
- 【TODO】第一百一十五節: 按鍵控制數碼管的秒表
- 【TODO】第一百一十六節: 按鍵控制數碼管的倒計時
- 【TODO】第一百一十七節: 按鍵切換數碼管窗口來設置參數
- 【TODO】第一百一十八節: 按鍵讓某位數碼管閃爍跳動來設置參數
- 【TODO】第一百一十九節: 一個完整的人機界面的程序框架的脈絡
- 【TODO】第一百二十節: 按鍵切換窗口切換局部來設置參數
- 【TODO】第一百二十一節: 可調參數的數碼管倒計時
- 【TODO】第一百二十二節: 利用定時中斷做的“時分秒”數顯時鐘
- 【TODO】第一百二十三節: 一種能省去一個lock自鎖變量的按鍵驅動程序
- 【TODO】第一百二十四節: 數顯儀表盤顯示“速度、方向、計數器”的跑馬燈
- 【TODO】第一百二十五節: “雙線”的肢體接觸通信
- 【TODO】第一百二十六節: “單線”的肢體接觸通信
- 【TODO】第一百二十七節: 單片機串口接收數據的機制
- 【TODO】第一百二十八節: 接收“固定協議”的串口程序框架
- 【TODO】第一百二十九節: 接收帶“動態密匙”與“累加和”校驗數據的串口程序框架
- 【TODO】第一百三十節: 接收帶“動態密匙”與“異或”校驗數據的串口程序框架
- 【TODO】第一百三十一節: 靈活切換各種不同大小“接收內存”的串口程序框架
- 【TODO】第一百三十二節:“轉發、透傳、多種協議并存”的雙緩存串口程序框架
- 【TODO】第一百三十三節:常用的三種串口發送函數
- 【TODO】第一百三十四節:“應用層半雙工”雙機串口通訊的程序框架