C總結與剖析:關鍵字篇 -- <<C語言深度解剖>> 目錄C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>程式的本質:二進位文件變數1.變數:記憶體上的某個位置開闢的空間2.變數的初始化3.為什麼要有變數4.局部變數與全局變數5.變數的大小由類型決定6.任何一個變數,記憶體賦值都是從低地址開始往高地 ...
C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>
目錄- C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>
- 程式的本質:二進位文件
- 變數
- 1.1 關鍵字auto
- 1.2 關鍵字register
- 1.3.1 多文件(extern):
- 進程地址空間
- 1.4類型
- 1.5 關鍵字sizeof
- 1.6關鍵字unsigned和signed
- 1.7 if-else組合
- 1.8switch case組合
- 1.9 do、while、for
- 字元設備
- 1.10 goto關鍵字
- 1.11 void
- 1.12return關鍵字
- 1.13const
- 1.14 volatile
- 1.15 extern(略,已在1.3.1描述)
- 1.16 struct
- 1.17 聯合體union
- 1.18 enum,可以枚舉常量的關鍵字
- 1.19 typedef
- C語言32個基本關鍵字歸類
- C語言中32個基本關鍵字概括總結_
程式的本質:二進位文件
運行程式,即將程式中的數據載入到記憶體中運行
為什麼要載入到記憶體? 1.馮諾依曼體系決定 2.快
變數
1.變數:記憶體上的某個位置開闢的空間
因為變數都是程式運行起來才開闢的
2.變數的初始化
變數的空間被開闢後,就應當具有對應的數據,即必須要初始化.表示該變數與生俱來的屬性就是該初始值
3.為什麼要有變數
電腦是為瞭解決人計算能力不足的問題而誕生的.即,電腦是為了計算的.
而計算,就需要數據
而要計算,任何時刻,不是所有的數據都要立馬被計算,因此有的數據需要暫時被保存起來,等待後續處理. 所以需要變數
4.局部變數與全局變數
- 局部變數:包含在代碼塊中的變數叫做局部變數.局部變數具有臨時性.進入代碼塊,自動形成局部變數,退出代碼塊自動釋放. 局部變數在棧區保存
- 全局變數:在所有函數外定義的變數,叫做全局變數.全局變數具有全局性.全局變數在全局已初始化數據區保存.
- 代碼塊:用花括弧{}括起來的區域,就叫做代碼塊.
5.變數的大小由類型決定
6.任何一個變數,記憶體賦值都是從低地址開始往高地址
所以首地址和取地址永遠都是低地址
1.1 關鍵字auto
預設情況下,編譯器預設所有的局部變數都是auto的,auto一般只能用來修飾局部變數,不能修飾全局變數.ju'bu也叫自動變數.一般情況下都是省略auto關鍵字的.基本永不使用
1.2 關鍵字register
建議性關鍵字,建議編譯器將該變數優化到寄存器上,具體情況由編譯器決定
(不建議大量使用,因為寄存器數量有限)
什麼樣的變數可以採用register?
- 局部的(全局會導致CPU寄存器被長時間占用)
- 高頻被讀取的(提高訪問效率)
- 不會被寫入的(寫入就需要寫回記憶體,後續還需要讀取檢測的話,register就沒有意義了)
寄存器變數是不能被取地址的,因為不在記憶體中,就沒有記憶體地址
register不會影響變數的生命周期,只有static會影響變數的生命周期
1.3.1 多文件(extern):
具有一定規模的項目是需要文件與文件之間進行交互的.如果不能直接跨文件調用,則項目在這方面一定需要很大成本解決.因此C預設是支持跨文件的
-
extern
- 功能:聲明,引入別的源文件的變數或函數
- extern與頭文件的淵源:
最開始時,沒有頭文件,源文件之間的互相引用是通過extern進行的.當項目複雜後,引用需要寫的聲明越來越多,每個源文件引入別的源文件的變數或函數時都需要聲明一次,維護變得麻煩.為瞭解決這種情況,頭文件就出來了,只需聲明一次就可以到處使用.
現在基本上多文件項目都是頭文件放聲明,源文件放定義 - 怎麼聲明變數和函數?
- 變數:
定義:int a = 10;
聲明:extern int a;
(不能給聲明賦值,因為聲明不能開闢空間)
(聲明變數時必須帶extern,因為不帶會區分不了是聲明還是定義) - 函數:
定義:void print(){ //... }
聲明:extern void printf();
(不能帶上函數體)
(聲明時建議帶上extern,不帶也行,因為聲明不帶函數體,且以分號結尾,有明顯區分度)
- 變數:
-
頭文件
- 頭文件一般包含:
- C頭文件
- 所有的變數的聲明
- 所有函數的聲明
- #define, 類型typedef, struct
- 頭文件中函數的聲明可以不帶上extern, 但是變數的聲明必須帶上extern, 因為頭文件最終是要展開到源文件中去的,源文件內可以定義和聲明,那頭文件也可以,如果頭文件中變數聲明不帶extern,則無法區分該變數是聲明還是定義(二義性).
1.03.2 static
static給項目維護給提供了安全保證,像封裝一樣,隱藏實現
功能:
- static修飾全局變數,該變數只在本文件內被訪問, 不能被外部其他文件直接訪問
- static修飾函數,該函數只能在本文件內被訪問,不能被外部其他文件直接訪問
- static局部變數變成靜態變數,放在靜態區,改變了局部變數的生命周期,使其生命周期變長。
- static修飾全局變數和函數,改變的是它們的作用域,生命周期不變.static修飾局部變數,改變的是局部變數的生命周期,作用域不變
- static修飾的局部變數會放在進程地址空間的 已初始化數據區,在進程的整個生命周期內都是有效的.
進程地址空間
-
代碼區
程式執行代碼存放在代碼區,其值不能修改(若修改則會出現錯誤)。
字元串常量和define定義的常量也有可能存放在代碼區。 -
常量區
字元串、數字等常量存放在常量區。
const修飾的全局變數存放在常量區。
程式運行期間,常量區的內容不可以被修改。 -
全局區(靜態區)
全局變數和靜態變數的存儲是放在一塊的,初始化的全局變數和靜態變數在一塊區域,未初始化的全局變數和未初始化的靜態變數在相鄰的另一塊區域。 -
堆區(heap)
堆區由程式員分配記憶體和釋放。
堆區按記憶體地址由低地址到高地址增長,用malloc, calloc, realloc等分配記憶體的函數分配得到的就是在堆上。 -
棧區(stack)
存放內容
臨時創建的局部變數和const定義的局部變數存放在棧區。
函數調用和返回時,其入口參數和返回值存放在棧區。
- 為什麼局部變數具有臨時性? 因為局部變數是存在棧中的.棧具有後進先出(壓棧)的特性,除了作用域後需要將該範圍的所有變數彈出.
1.4類型
- C語言為何有類型? 讓我們能夠對記憶體進行合理化劃分,按需索取,存在類型的目的就是讓我們能合理使用記憶體空間
- 類型為什麼有這麼多種? 實際應用場景很多種,應用場景不同,解決對應的應用場景的計算方式不同,需要空間的大小也是不同的.多種類型目的是讓我們能以最下成本解決多樣化的場景問題.
例如: 登記成績,成績只要0-100分,那使用一1個位元組int8_t/char就足夠. 如果帶浮點,則需要浮點型.
1.5 關鍵字sizeof
sizeof是函數還是關鍵字?
- 證明1:
int a = 10;
printf("%d\n",sizeof(a)); //正確用法
printf("%d\n",sizeof(int)); //正確用法
printf("%d\n",sizeof a ); //正確用法,證明sizeof不是函數
printf("%d\n",sizeof int ); //不存在
- 證明2:函數調用棧幀中sizeof不會壓棧
1.6關鍵字unsigned和signed
數據在電腦中的存儲
- 任何數據在電腦中都必須被轉化成二進位,因為電腦只認識二進位.而電腦還要區分數據是正數還是負數,則二進位又分為
符號位 + 數據位
. - 電腦記憶體儲的整型必須是補碼
- 無符號數和正數的原反補碼相等,直接存入電腦中.負數需要將原碼轉化成補碼再存儲
- 類型決定瞭如何解釋空間內部保存的二進位序列
- 浮點數預設是double類型,如果想要float需要在數後加上f,如
float f = 1.1f
;
原碼 與 補碼的轉化與硬體關係
例: int b = -20; //20 = 16+4 = 2^4^ (10000)~2~+ 2^2^(100)~2~
//有符號數且負數 原碼轉成補碼:
1000 0000 0000 0000 0000 0000 0001 0100 原碼
1111 1111 1111 1111 1111 1111 1111 1011 反碼 = 原碼取反
1111 1111 1111 1111 1111 1111 1111 1100 補碼 = 反碼+1
//補碼轉原碼
方法一: 原理
1111 1111 1111 1111 1111 1111 1111 1100 補碼
1111 1111 1111 1111 1111 1111 1111 1011 反碼 = 補碼-1
1000 0000 0000 0000 0000 0000 0001 0100 原碼 = 反碼取反
方法二: 電腦硬體使用的方式, 可以使用一條硬體電路,完成原碼補碼互轉
1111 1111 1111 1111 1111 1111 1111 1100 補碼
1000 0000 0000 0000 0000 0000 0000 0011 補碼取反
1000 0000 0000 0000 0000 0000 0000 0100 +1
原,反,補的原理:
原反補的概念從時鐘引入, 8點+2 = 10點. 而8點-10也等於10點.即2是-10以12為模的補碼.
-10要轉化成2 ,可以用模-10來得到,但硬體中位數是固定的,模數為1000...,最高位會溢出捨棄.即全0.無法做差.
引入反碼轉換計算:即2 == 模-10 == 模-1+1-10 == 1111... -10 +1 == 反碼+1; 這個111...-10就是反碼,即反碼+1==補碼的由來
在二進位中,全1減任何數都是直接去掉對應的1.所以反碼就是原碼符號位不變,其餘位全部取反
整型存儲的本質
定義unsigned int b = -10;
能否正確運行? 答案是可以的.
定義的過程是開闢空間,而空間只能存儲二進位,並不關心數據的內容
數據要存儲到空間里,必須先轉成二進位補碼.而在寫入空間時,數據已經轉化成補碼
變數存取的過程
- 存: 字面數據必須先轉成補碼,再放入空間中.符號位只看數據本身是否攜帶+-號,和變數是否有符號無關.
- 取: 取數據一定要先看變數本身類型,然後才決定要不要看最高符號位.如果不需要,則直接將二進位轉成十進位.如果需要,則需要轉成原碼,然後才能識別(還需要考慮最高符號位在哪裡,考慮大小端)
類型目前的作用
- 存數據前決定開闢多大的空間
- 讀數據時如何解釋二進位數據
特定數據類型能表示多少個數據,取決於自己所有比特位排列組合的個數
十進位與二進位快速轉換
(前置知識:需要熟記2^0到2^10的十進位結果)
1 -> 2^0
10 -> 2^1
100 -> 2^2
1000 -> 2^3 //1後面跟3個比特位就是2^3
規律: 1後n個0就是2^n,即n等於幾1後面就跟幾個0 --- 十進位轉二進位
反過來就是1後面跟幾個0,就是2的幾次方 --- 二進位轉十進位
因此:2^9 -> 10 0000 0000 // n
例: 67 = 64+2+1 -> 2^6+2^1+2^0 -> 1000000 + 10 + 1
= 0000 0000 .... 0100 0011
同理,二進位轉十進位逆過程即可
大小端位元組序
現象: vs的記憶體視窗中,地址從上到下依次增大,從左到右也依次增大
- 大端:按照位元組為單位,低權值位數據存儲在高地址處,就叫做大端
- 小段:按照位元組為單位,低權值位數據存儲在低地址處,就叫做小端(小小小)
(基本上以小端為主,大端比較少(網路))
大小端存儲方案,本質是數據和空間按照位元組為單位的一種映射關係
(考慮大小端問題是1位元組以上的類型.short,int,double...)
判斷當前機器的位元組序
- 方法1: 對
int a = 1
取首地址,然後(char*)&a
,得到的值是1則為小端,否則為大端 - 方法2: 打開記憶體視窗查看地址與數據的位元組序
"負零"(-128)的理解
(負零的概念並不存在,只是碰巧相像)
-128實際存入到電腦中是以 1 1000 0000 表示的(計組運算器).但空間只有8位,發生截斷,因此得到1000 0000.
而[1111 1111,1000 0001]~[0000 0000,0111 1111]
即[-127,-1]~[0,127] 自然數都已經被使用 .
電腦不能浪費每一個空間(最小的成本儘可能解決大量的計算),自然1000 0000也需要有相應的意義. 因此賦予數值為-128.
因為截斷後也不可能恢復,所以這是一種半計算半規定的做法.
截斷
截斷是空間不足以存放數據時,將高位截斷.
截斷的是高位還是低位? 因為賦值永遠都是從低地址賦起(從低到高依次賦值),因此空間不足時高位直接丟棄.
1 0000 0001 0100
1 1111 1110 1100
0 0000 0000 1010
1 1111 1111 0110
1 0000 0000 1010
建議在無符號類型的數值後帶上u,
預設的數值是有符號的,在數值後加u更加嚴格,unsigned int a = 10u;
1.7 if-else組合
- 表達式: 變數與操作符的組合稱為表達式
- 語句: 以分號結尾的表達式稱為語句
- if(0){ //... }註釋法,在看到if(0)時,有可能這是一個註釋,不推薦這種做法,但是需要認識.
if的執行順序
- 計算功能:先執行完畢if括弧()中的表達式or某種函數,得到表達式的真假結果
- 判定功能:根據表達式結果進行條件判定
- 分支功能:根據判定結果進行分支
(if有判定和分支兩個功能,而switch只有判定而沒有分支功能,因此必須使用break)
操作符的執行順序測試方法
printf("1 ") && printf("2 ");
printf("1 ") || printf("2 ");
C語言的布爾類型
- C89/C90沒有bool類型
- C99 引入了關鍵字為_Bool的類型,在新增的頭文件stdbool.h中.為了保證C/C++的相容性,被重新用巨集寫成了bool.
- 微軟對C語言bool類型也有一套標準,BOOL,FALSE,TRUE. 不推薦使用微軟這套標準,不具備可移植性
浮點數與"零值"比較
-
精度損失:浮點值與實際值不等,可能偏大可能偏小,都屬於精度損失
-
驗證浮點數是否存在精度損失
-
驗證浮點數的差值是否存在精度損失
-
浮點數直接比較驗證
結論: 浮點數在進行比較時,絕對不能使用雙等號
==
來進行比較. 浮點數本身有精度損失,進而導致結果可能有細微的差別. -
-
如何進行浮點數比較
1. x - y == 0的條件是 |x - y| < 精度.
即 x - y > -精度 && x - y < 精度
2.還可以使用fabs函數,C90,<math.h>, double fabs(double x); 返回x的絕對值.
即 fabs(x-y) < 精度
//--------------------------------------------------------------
//方法1,自定義精度
#include<stdio.h>
#include<math.h>
#define EPSILON 0.0000000000000001 //自定義精度
int main()
{
double x = 1.0;
double y = 0.1;
//驗證x - 0.9 是否等於 0.1
if(fabs((x-0.9)- y) < EPSILON ) printf("aaaa\n");
else printf("bbbb\n");
puts("hello world!");
return 0;
}
//方法2:使用C語言提供的精度
#include<stdio.h>
#include<math.h>
#include<float.h>
int main()
{
double x = 1.0;
double y = 0.1;
//驗證x - 0.9 是否等於 0.1
//<float.h> 內置最小精度值 DBL_EPSILON 和 FLT_EPSILON ,1.0+DBL_EPSILON != 1.0 ,EPSILON是改變1.0的最小的值,數學概念,略
if(fabs((x-0.9)- y) < DBL_EPSILON ) printf("aaaa\n");
else printf("bbbb\n");
return 0;
}
- 浮點數與"零值"比較,只需要判定它是否小於EPSILON即可
int main()
{
double x = 0.0;
// double x = 0.00000000000000000000000000001; //很小也可以認為等於0
if(fabs(x) < DBL_EPSILON ) printf("等於0\n");
else printf("不等於0\n");
return 0;
}
補充:如何理解強制類型轉化
強制類型轉化:不改變數據本身,只改變數據的類型
- "123456" -> int:123456
字元串"123456"如何轉化成整型值123456,能強轉嗎? 答案是不能,只能通過演算法進行轉化
因為"123456"的空間至少占了7個,而整型int只占4個位元組.
-
不同類型的0
printf("%d\n",0);
printf("%d\n",'\0');
printf("%d\n",NULL); //(void*)0
1.8switch case組合
- 基本語法結構
//switch只能對整數進行判定
switch(整型變數/常量/整型表達式){
case var1:
break;
case var2:
break;
case var3:
break;
default:
break;
}
推薦使用switch的場景:只能用於整數判定且分支很多的情況下
- switch case 的功能
switch本身沒有判斷和分支能力,switch是拿著結果去找case進行匹配,
case具有判定能力,但沒有分支能力,case是通過break完成分支功能
break具有分支功能,相當於if的分支能力.
default相當else,處理異常情況
(補充) 屏蔽警告的方法
error C4996: 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
方法1:
#pragma warning(disable:4996)
方法2:
#define _CRT_SECURE_NO_WARNINGS //該巨集定義必須寫在文件的首行(頭文件的前面)才有效
(如果巨集沒有巨集值,則只能用在#ifdef等條件編譯語句中,即只用於標識)
-
在case中執行多條語句,建議case後都帶上花括弧.
在case中定義變數,直接寫會警告,需要帶上花括弧,但不建議在case中定義變數,如果非要這麼做,可以封裝成函數來替代.並且
-
多個case執行同樣語句
int main()
{
int n = 0 ;
scanf("%d",&n);
switch (n)
{
case 1: case 2: case 3: case 4: case 5:
puts("周內");
break;
case 6:
puts("周六");
break;
case 7:
puts("周日");
break;
default:
break;
}
return 0;
}
- default可以在switch中的任意位置,一般習慣放在最後的case後
- switch中儘量不要單獨出現return.一般習慣用break,突然return容易搞混
- switch中不要使用bool值,不好維護
- case的值必須是數字常量,不能是
const int a = 1;
這種 - 按執行頻率排列case語句,頻率越高越靠前,能減少匹配次數
1.9 do、while、for
迴圈的基本結構
- 一般的迴圈都必須要有3種功能:
- 迴圈條件初始化
- 迴圈條件判定
- 迴圈條件更新
(死迴圈除外)
int main()
{
int count = 10; //1.迴圈條件初始化
while (count > 10) //2.迴圈條件判定
{
printf("%d\n", count); //3.業務邏輯
count--; //4.迴圈條件更新
}
return 0;
}
- for迴圈
使用樣例:
for(int i = 0; i<10; i++)
{
//業務邏輯
}
for的結構更加緊湊,更清晰
for(1.迴圈條件初始化; 2.迴圈條件判定; 4.迴圈條件更新){
//3.業務邏輯
}
- do-while
//1.迴圈條件初始化
do{
//2.業務邏輯
//3.迴圈條件更新
}while(4.迴圈條件判定);
do while結構需要在while()後加上分號,容易忘記
continue跳轉的位置
- while迴圈continue後會跳轉到迴圈條件判定的位置,之後執行迴圈判定
- for迴圈會跳轉到迴圈條件更新的位置,之後進行迴圈條件更新!!!
迴圈設計的思想推薦
1.儘可能減少迴圈來回多次的跳轉的次數 --- 涉及緩存,局部性原理,CPU命中概率.儘可能讓代碼執行的更加平滑
2.在多重迴圈中,如果有可能,應當將最長的迴圈放在最內層,最短的迴圈放在最外層,以減少CPU跨且迴圈層的次數.
推薦使用for的前閉後開寫法
推薦1:for語句迴圈的次數的計算方式
1.for(int i = 0; i<=9; i++){} //cnt = 9-0+1 = 10次
2.for(int i = 0; i<10; i++){} //cnt = 10-0 = 10次
3.for(int i = 6; i<=9; i++){} //cnt = 9-6+1 = 4次
4.for(int i = 6; i<10; i++){} //cnt = 10-6 = 4次
從計算角度,前閉後開寫法能更加直觀,快速
推薦2:下標映射時,思維清晰,不容易混亂
字元設備
(補充) char有有符號和無符號兩種類型,字元是無符號類型.
(補充) getchar的返回值為什麼是int
如果getchar返回值是char,因為char只能表示0-255個字元,剛好包含所有ascii碼,如果getchar失敗,則沒有多餘的位置返回錯誤信息.因此getchar返回值設計成int,int既能轉化成字元,還有多餘的數值當作錯誤碼來使用.
鍵盤輸入的內容,以及往顯示器中列印的內容,都是字元 --> 鍵盤/顯示器稱為字元設備
驗證:printf scanf的返回值是輸出和輸入的字元數.
1.10 goto關鍵字
goto的用法
- 向下跳轉
int main()
{
puts("hello world 1!");
goto end;
puts("hello world 2!");
end:
puts("hello world 3!");
return 0;
}
-向上跳轉
//用goto實現的迴圈
int main()
{
int i = 0;
start:
printf("[%d] goto running ...\n",i);
i++;
if(i<10) goto start;
return 0;
}
- goto只能在本函數內使用,不能跨函數
- goto很靈活,容易出現問題,因此不受歡迎.但很多項目中goto用得也不少,在有能力的情況下可以使用
1.11 void
-
void不能定義變數,因為void的類型大小是不確定的,編譯器強制不允許定義變數.
-
sizeof(void)的大小,在windows中為0,在linux中為1. 說明void的大小是不明確的,本身就被編譯器解釋成空類型.
-
void作為空類型,理論上是不能開闢空間的,即使開闢了空間,也僅僅作為一個占位符看待(void的作用,告訴編譯器他是空類型).
所以,既然無法開闢空間,那麼也就無法作為正常變數使用,既然無法使用,編譯器乾脆就不讓它定義變數. -
void不僅不能定義變數,而且還不能用於解釋數據,如強轉成void在賦值給int是不允許的:
int a = (void)10
; 以及接受返回值類型為void的函數的返回值也是不允許的; -
(補充)C語言函數可以不帶返回值,預設的返回值類型是int. --- 儘量寫上返回值類型,不寫返回值類型很容易被誤解成返回值類型為void. 而且可讀性還低.
-
void作為函數參數的作用
void test1()
{
puts("test1");
}
void test2(void)
puts("test2");
}
int main()
{
test1(1,2,3,4); //正常運行
test2(1,2,3,4); //linux gcc報錯:too many arguments to function
//windows中是warning C4087: “test2”: 用“void”參數列表聲明
return 0;
}
說明:void作參數時,明確告知編譯器不需要傳參 .而參數列表為空時則不限制 -- 自解釋
-
void*
void *p = NULL;
是可以編譯通過的.因為void*是指針,指針在任何平臺的大小都是明確的.32位占4位元組,64位占8位元組. 大小明確,所以能夠定義變數 -
void*可以被任何類型的指針變數接受,void*也可以接受任意的指針類型(常用)
如在庫,系統介面的設計上,設計成通用介面.
-
void*類型的指針在vs中不能加減,gcc中可以加減
解釋:指針加減的大小是sizeof(類型),以滿足連續的數據存放需要(如數組).
而sizeof(void)在vs中是0,加減沒有意義,所以乾脆強制不能自增自減等操作.而gcc中sizeof(void)為1,即一個占位符的大小,因此還是可以加減. -
GNU C(擴展C)和ANSI C(標準C)
-
void*類型不能解引用
解釋:也是sizeof(void)和void不能定義變數的問題
1.12return關鍵字
-
return語句不可返回指向"棧記憶體"的指針,因為該記憶體在函數結束時會自動銷毀.
-
(補充) 棧幀
- 棧幀:函數在棧區中開闢的空間,就叫棧幀
- 棧幀的大小,編譯器有能力在開闢前可以通過sizeof根據函數內的變數類型和數量預估出來(1.關鍵字本身就是編譯器用的 2.不同的編譯器計算方式不一樣)
- 調用函數,創建棧幀. 函數返回,釋放棧幀(釋放就是允許被覆蓋.空間使用權不再屬於這個函數,別的函數也可以使用)
- 為什麼臨時變數具有臨時性,就是因為函數返回後要釋放棧幀,棧幀被釋放,棧幀內的變數所需的空間自然也被釋放.因此臨時變數具有臨時性
- 棧幀無效並不是清空棧幀的內容,棧幀內容不會改變,但是使用權已歸還.等下個函數調用時,新的棧幀就會覆蓋掉舊的棧幀.
- 越早創建的棧幀地址越高,因為棧是向下(低地址)增長的.
- main函數也是函數,也有自己的棧幀.(調用main函數的函數是_start函數或__tmainCRTSstartup()函數...)
- 遞歸就是不斷向下形成棧幀的過程.因為棧大小是有限制的,遞歸過多有可能會發生棧溢出問題.
-
函數的返回值的返回方式
- 函數的返回值通過保存在寄存器的方式,返回給函數調用方.
- 如果有對應的接受變數,則會形成對應的接受彙編語句.就算沒有變數接受返回值也會將返回值放到寄存器中,只是沒有生成接受語句.
int getDate()
{
int x = 0x11223344;
puts("getDate");
return x;
}
int main()
{
int y = getDate();
return 0;
}
-
返回值具有常屬性...
1.13const
-
const 直接修飾變數時,該變數不可被直接修改(可以間接修改)
-
const 修飾變數放在類型前和類型後是一樣的.
-
既然const可以被間接修改,那const有什麼意義?
- const是給編譯器看的,const能夠讓編譯器在編譯代碼時對用const的變數進行語法檢查,凡是對const修飾的變數有直接修改行為的,編譯器會直接報錯.
- const是給其他程式員看的,其他程式員發現有變數被const修飾時,就知道這個變數不能直接被修改,即這份代碼有了一定的自身描述性(自描述)
-
真正的不可修改例子
(字元串常量不可修改是操作系統級別的保護,真正的不可修改. 原理相關:OS頁表和許可權)
- const是在編譯期間限制不可修改(所有的報錯都是在編譯期間).
- 標準C下const修飾的變數不可以用於定義數組(必須是巨集或字面值).有的擴展C可以.
- 只讀數組
const int a[] = { 1,2,3,4,5 };
, 數組的每個元素都不可被修改 - const修飾返回值,採用"值傳遞"時沒有任何價值,加和不加一樣.如果是"指針傳遞",則接收方也必須加const,約束指針指向的內容不能被修改
帶const的類型
定義整型指針變數p,用於初始化q : int *q = p;
如果p的定義方式是const int *p
,則p的類型是const int*
,因此初始化時q會告警.對p強轉後再賦值,不會告警int*q = (int*)p;
如果p的定義方式是int const *p
,則p的類型是int*
,因此初始化q時不會告警.
帶const的類型可以直接傳給不帶const的類型:許可權可以縮小,但不可以放大
指針(補充)
- 取地址:在C中,任何變數&都是最低地址開始! 取得是一個變數所占空間的最低地址
- 在C中,任何函數傳參都一定會形成臨時變數,包括指針變數.無論傳值還是傳址.
指針變數也是變數,傳址傳參至少會形成一個指針變數用於接受調用方傳過來的地址.
1.14 volatile
-
volatile關鍵字存在說明變數/指令/代碼在程式執行中可被隱含地(彙編層面)改變
-
volatile 是一個類型修飾符(type specifier),就像我們熟悉的 const 一樣,它是被設計用來修飾被不同線程訪問和修改的變數; volatile 的作用是作為指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次都要直接從記憶體中讀值(記憶體可見性)。用於多進程多線程高併發等多執行流的場合
-
const volatile int a = 10;
這句代碼是能正確通過編譯的,const要求你不要進行寫入就可以,volatile意思是CPU讀取時,每次都要從記憶體讀.兩個不衝突. -
雖然volatile就叫做易變關鍵字,但這裡僅僅描述它修飾的變數可能會發生變化,要編譯器註意,並不是要求它對應的變數必須變化!這點必須特別註意.
1.15 extern(略,已在1.3.1描述)
1.16 struct
- 定義結構體,本質是製作類型
- 結構體只能整體初始化,不能整體賦值 (和數組一樣,只能被整體初始化,不能整體賦值)
- 空結構體不同編譯器不同處理,vs不允許定義空結構體類型;gcc允許定義結構體類型,但大小為0,不能用於定義變數,沒有空間的類型一定會報錯.
(定義結構體類型和定義結構體變數不一樣.)
柔性數組(C99)
- 必須放在結構體內
- 一般將大小設成[0],還有些編譯器支持[]沒有常量/字面值
- 一般結構體的第一個元素儘量不要是柔性數組,最好放在最後(不同編譯器支持的方式不同,儘可能統一)
- 柔性數組不占結構體大小
/*柔性數組的定義*/
struct s
{
int a;
//其他成員...
int arr[]; //一般放最後
};
/*柔性數組的使用*/
struct data
{
int num;
int arr[0];
};
int main()
{
struct data *p = malloc(sizeof(struct data) + sizeof(int)*10);
p->num = 10;
for (int i = 0; i < p->num; i++)
{
p->arr[i] = i;
}
return 0;
}
1.17 聯合體union
- 聯合體也是製作類型
- 聯合體的大小是最大成員的大小.
- 聯合體的所有成員的地址永遠都是從聯合體的首地址開始的,它的所有成員相對於基地址的偏移量都為0
- 聯合體每一個成員都可以認為是第一個成員
使用聯合體驗證大小端:
驗證代碼1
union un
{
char a;
int b;
};
int main()
{
union un x;
x.b = 16777216; //0x 0000 0001 ... 0000 0000
printf("%d\n", *(char*)(((char*)&x.a)));
printf("%d\n", *(char*)(((char*)&x.a) + 1));
printf("%d\n", *(char*)(((char*)&x.a) + 2));
printf("%d\n", *(char*)(((char*)&x.a) + 3));
x.b = 1; //0x 0000 0000 ... 0000 0001
printf("%d\n", x.a);
printf("%d\n", *(char*)(((char*)&x.a) + 1));
printf("%d\n", *(char*)(((char*)&x.a) + 2));
printf("%d\n", *(char*)(((char*)&x.a) + 3));
return 0;
}
驗證代碼2
union un
{
int i;
char a[4];
};
int main()
{
union un x;
x.a[0] = 0x78;
x.a[1] = 0x56;
x.a[2] = 0x34;
x.a[3] = 0x12;
printf("0x%x\n",x.i); //vs:0x12345678
return 0;
}
聯合體的記憶體對齊
對齊數的規則:
- 聯合體記憶體對齊數的大小能整除任何一個成員的大小
- 大小大於等於最大一個成員的大小.
- 滿足上述1,2兩點規則的最小整數就是對齊數
union un
{
char a[5];
int b;
};
int main()
{
printf("%d\n",sizeof(union un)); //vs:8
return 0;
}
1.18 enum,可以枚舉常量的關鍵字
-
枚舉也是製作類型
-
枚舉類型的成員是常量/字面值
-
用枚舉類型定義的變數使用枚舉類型內的常量來初始化
-
預設第一個成員的值為0,之後依次遞增
-
每個成員的預設值都是上一個成員的值+1
-
非空枚舉的大小為4,空枚舉在vs下大小為4,gcc不允許定義空枚舉類型
-
枚舉可以認為是一個整型int,可以用整型給枚舉變數初始化和賦值
為什麼要有枚舉?
- 枚舉是個有強相關性的類型.相關性體現在事物與數字的關聯如一周固定有7天,每一天都有自己的名字如星期一,周一,Monday..等.枚舉類型便是根據此而來
- 魔術數字
魔術數字(magic number)是程式設計中所謂的直接寫在程式碼里的具體數值(如“10”“123”等以數字直接寫出的值)。雖然程式作者寫的時候自己能瞭解數值的意義,但對其他程式員而言,甚至製作者本人經過一段時間後,會難以瞭解這個數值的用途,只能苦笑諷刺“這個數值的意義雖然不懂,不過至少程式會動,真是個魔術般的數字”而得名。
- 枚舉本身具有自描述性/自解釋性,能夠對魔術數字有很好的替代.
巨集也有自描述性,為什麼不使用巨集?
- 當一組相關屬性的常量比較少時,使用巨集是完全沒有問題的. 當屬性的數量非常多時,使用巨集則需要些大量的巨集定義,代碼冗長,不利閱讀和控制,並且很難看出屬性與屬性間的關係.而枚舉本身可以作為一組屬性的集合,集合內的屬性很容易看出有相關性,並且易讀易控制.
- 巨集在預編譯時直接將對應的字面值直接替換到代碼中.而枚舉在編譯時,會進行詞法語法分析檢查.
枚舉的特性
- 枚舉與常量間具有相關性(集合)
- 自描述性
- 有語法檢查
1.19 typedef
歷史的誤會-typedef也許應該是typerename
,typedef就是用來給類型重命名的
typedef的功能
- typedef可以一次指定多個別名
typedef int A,B,C;
- typedef 為函數起別名的寫法如下
typedef signed char (*fp)(void);
- typedef 也可以用來為數組類型起別名
typedef int five_ints[5];
five_ints x = {11, 22, 33, 44, 55};
typedef可以簡化類型聲明
C 語言有些類型聲明相當複雜,比如下麵這個
char (*(*x(void))[5])(void);
函數指針 :
typedef 返回值類型 ()(參數);
數組指針 :
typedef 返回值類型 ()[];
右左法則說明:右左法則不是標準C裡面的內容,它是從C標準的聲明規定中歸納出來的語法.C標準的聲明規則是用來解決如何創建聲明.而右左法則是用來解決如何辨識一個聲明
右左法則內容:首先從最裡面的標識符看起,然後往右看,再往左看.每當遇到圓括弧時,就掉轉閱讀方向.一旦解析完 圓括弧內的所有內容,就跳出圓括弧.重覆這個過程直到整個聲明解析完畢.
根據右左法則, x的右邊是(void),說明x是函數.x的左邊是*,說明x的返回值類型是指針.
下一層括弧的右邊是[5],說明是元素個數為5的數組指針,左邊是*,說明元素類型是指針.
下一層括弧的右邊是(void),說明是函數,左邊是char,說明返回值類型是char,即數組元素為char (*)void
所以它是一個函數名為x,參數列表為void, 返回值為 大小為5,類型為char (*)void的數組指針 的函數指針.
typedef 可以簡化複雜的類型聲明,使其更容易理解。首先,最外面一層起一個類型別名。
typedef char (*Func)(void);
Func (*x(void))[5];
這個看起來還是有點複雜,就為裡面一層也定義一個別名。
typedef char (*Func)(void);
typedef Func Arr[5];
Arr* x(void);
上面代碼就比較容易解讀了。
- x是一個函數,返回一個指向 Arr 類型的指針。
- Arr是一個數組,有5個成員,每個成員是Func類型。
- Func是一個函數指針,指向一個無參數、返回字元值的函數。
不要過度使用typedef
使用typedef本質上就是封裝,在某些情況下,封裝隱藏內部細節會給代碼閱讀帶來成本.
typedef與巨集的區別
//區別1:
#define pINT int*
typedef int* PINT
PINT p1,p2;
pINT p3,p4;
p2是什麼類型,p4是什麼類型?
答:p2是整型指針,p4是整型. 可以看出typedef和巨集有一定差別,用typedef更容易理解,更安全
typedef後的類型應理解為定義了一種全新的類型,而不是像巨集一樣直接替換.
typedef重命名後的類型,要註意正確使用修飾關鍵字
- typedef後使用unsigned或signed可能會報錯
typedef int INT;
int main() {
unsigned INT b; //error
}
-
const
-
static
typedef static int int32_t 行不行? 答:不行
因為在定義時存儲類型關鍵字只能存在一個
為什麼typedef屬於存儲類型關鍵字
typedef 的作用並不直接涉及存儲管理或者變數的具體存儲方式,而是用於給現有類型創建一個新的名稱,即類型別名。
儘管如此,將 typedef 歸類為存儲類型關鍵字可能是因為在概念上它確實影響了類型在程式中的“存儲”方式——存儲在類型系統中而不是實際記憶體佈局中。通過 typedef,程式員可以在不改變類型本質的情況下更改類型的名稱,從而在類型層次上“存儲”新的類型定義。這樣做的目的更多是為了提高代碼的可讀性和可維護性,尤其是在處理複雜類型(如指針、數組、函數指針或結構體)時,賦予它們更具描述性的名稱。
實際上,typedef 主要用於類型定義,而不直接影響變數的存儲類別或持續性。它主要在編譯階段起作用,幫助編譯器理解和解析類型,並不影響運行時的行為。
C語言32個基本關鍵字歸類
數據類型關鍵字(12個)
char
:聲明字元型變數或函數short
:聲明短整型變數或函數int
: 聲明整型變數或函數long
:聲明長整型變數或函數signed
:聲明有符號類型變數或函數unsigned
:聲明無符號類型變數或函數float
:聲明浮點型變數或函數double
:聲明雙精度變數或函數,C語言中字面值預設是doublestruct
:聲明結構體變數或函數union
:聲明共用體(聯合)數據類型enum
:聲明枚舉類型void
:聲明函數無返回值或無參數,聲明無類型指針
控制語句關鍵字(12個)
- 迴圈控制(5個)
for
:一種迴圈語句do
:迴圈語句的迴圈體while
:迴圈語句的迴圈條件break
:跳出當前迴圈continue
:結束當前迴圈,開始下一輪迴圈
- 條件語句(3個)
if
: 條件語句else
:條件語句否定分支goto
:無條件跳轉語句
- 開關語句 (3個)
switch
:用於開關語句case
:開關語句分支default
:開關語句中的“其他”分支
- 返回語句(1個)
return
:函數返回語句(可以帶參數,也看不帶參數)
存儲類型關鍵字(5個)
auto
:聲明自動變數,一般不使用extern
:聲明變數是在其他文件中聲明register
:聲明寄存器變數static
:聲明靜態變數typedef
:用以給數據類型取別名(但是該關鍵字被分到存儲關鍵字分類中,雖然看起來沒什麼相關性)
什麼是存儲類型關鍵字?
C語言中,每一個變數和函數都有2個屬性:數據類型和數據的存儲類別。C的存儲類別有4種:自動的(auto)、靜態的(static)、寄存器的(register)、外部的(extern)。變數的存儲類別對應變數的作用域與生命周期。
存儲關鍵字,不可以同時出現,也就是說,在一個變數定義的時候,只能有一個。
其他關鍵字(3個)
const
:聲明只讀變數sizeof
:計算數據類型長度volatile
:說明變數在程式執行中可被隱含地改變
C語言中32個基本關鍵字概括總結_
[引用](https://blog.csdn.net/Liu1013216383/article/details/134808347)
1、數據類型關鍵字(12個)
(1) char :聲明字元型變數或函數註:char 占一個位元組,也就是 8 個二進位位,但它表示的是有符號的類型,所以表示的範圍是 -128~127 ;uchar 表示無符號的類型,所以表示的範圍是 0~255
(2) double :聲明雙精度變數或函數
註:double 占的位元組:16 位編譯器下,double 占 8 個位元組;32 位編譯器下,double 占 8 個位元組;64 位編譯器下,double 占 8 個位元組。
(3) enum :聲明枚舉類型
註:enum 是電腦編程語言中的一種數據類型。枚舉類型:在實際問題中,有些變數的取值被限定在一個有限的範圍內。例如,一個星期內只有七天,一年只有十二個月,一個班每周有六門課程等等。如果把這些量說明為整型,字元型或其它類型顯然是不妥當的。為此,C語言提供了一種稱為“枚舉”的類型。在“枚舉”類型的定義中列舉出所有可能的取值,被說明為該“枚舉”類型的變數取值不能超過定義的範圍。應該說明的是,枚舉類型是一種基本數據類型,而不是一種構造類型,因為它不能再分解為任何基本類型。
(4) float :聲明浮點型變數或函數
註: float 是C語言的基本數據類型中的一種,表示單精度浮點數,占 4 個位元組。
(5) int : 聲明整型變數或函數
註:數據類型占記憶體的位數與操作系統的位數以及編譯器有關,一般情況下在當前主流的編譯器中int 類型無論在 32 位或 64 位系統中都是 4 個位元組。
(6) long :聲明長整型變數或函數
註:C語言中 long 是4個位元組,是一種數據類型,有兩種表現形式:有符號和無符號。
在有符號中,long 的表示數的範圍為:-2147483648~2147483647
在無符號中,long 的表示數的範圍為:0~4294967295(7) short :聲明短整型變數或函數
註:short 是占兩個位元組。short 在C語言中是定義一種整型變數家族的一種,short i ;表示定義一個短整型的變數 i 。
依據程式編譯器的不同short定義的位元組數不同。標准定義short短整型變數不得低於16位,即兩個位元組。編譯器頭文件夾裡面的limits.h定義了short能表示的大小:SHRT_MIN~SHRT_MAX。在32位平臺下如windows(32位)中short一般為16位。
(8) signed :聲明有符號類型變數或函數
註:signed 是預設的,表示這個變數是有符號的,可以存儲整數和負數。
signed 存儲符號是有代價的,代價就是存儲空間中的一個比特位專門用來存儲符號,這一位不能表示數值。一般來說,同類型的 signed 能夠存儲的數的絕對值大小要小於 undigned 。(9) struct :聲明結構體變數或函數
註:在C語言中,可以使用結構體( Struct )來存放一組不同類型的數據。
結構體的定義形式為: struct 結構體名{結構體所包含的變數或數組};
結構體是一種集合,它裡面包含了多個變數或數組,它們的類型可以相同,也可以不同,每個這樣的變
(10) union :聲明共用體(聯合)數據類型
註:C語言中的 union 是聯合體,就是一個多個變數的結構同時使用一塊記憶體區域,區域的取值大小為該結構中長度最大的變數的值。
(11) unsigned :聲明無符號類型變數或函數
註:整型的每一種都有無符號( unsigned )和有符號( signed )兩種類型( float 和 double 總是帶符號的)
在預設情況下聲明的整型變數都是有符號的類型( char 有點特別),如果需聲明無符號類型的話就需要在類型前加上 unsigned 。
無符號版本和有符號版本的區別就是無符號類型能保存 2 倍於有符號類型的數據,比如 16 位系統中一個 int 能存儲的數據的範圍為 -32768~32767 ,而 unsigned 能存儲的數據範圍則是 0~65535。
(12) void :聲明函數無返回值或無參數,聲明無類型指針(基本上就這三個作用)
註:void 被翻譯為"無類型",相應的 void * 為"無類型指針"。常用在程式編寫中對定義函數的參數類型、返回值、函數中指針類型進行聲明。
2、控制語句關鍵字(12個):
A迴圈語句(1) for :一種迴圈語句
註:for 是C語言中的一個關鍵字,主要用來控制迴圈語句的執行
量或數組都稱為結構體的成員( Member )。
int main()
{int a,b;
for(a= 0;a < 100 ;a++)
{
b++;
}
return 0;//表示函數終止
}
a=0 是初始化部分;a<100 是迴圈判斷條件部分(當滿足此條件時才進入執行 for 迴圈中的語句);a++ 是執行完迴圈體語句後的操作 , b++ 是迴圈體語句。(2) do :迴圈語句的迴圈體
註:C語言中 do 是執行某代碼塊的意思,do 關鍵字不能單獨使用,通常用在 do...while
迴圈中。在C語言中,do...while 迴圈是在迴圈的尾部檢查它的條件,do...while 迴圈與 while 迴圈類似,但是 do...while 迴圈不管是真是假都要確保至少執行一次迴圈。
(3) while :迴圈語句的迴圈條件
註:while 語句創建了一個迴圈,重覆執行直到測試表達式為假或0。
while 語句是一種入口條件迴圈,也就是說,在執行多次迴圈之前已決定是否執行迴圈。因此,迴圈有可能不被執行。
迴圈體可以是簡單語句,也可以是複合語句。
int main()
{int a=0,b=0;
while(a==0)
{
b++;
}
return 0;//表示函數終止
}
(4) break :跳出當前迴圈註:C 語言中 break 語句有以下兩種用法:
終止,且程式流將繼續執行緊接著迴圈的下一條語句。當 break 語句出現在一個迴圈內時,迴圈會立即
它可用於終止 switch 語句中的一個 case 。
如果您使用的是嵌套迴圈(即一個迴圈內嵌套另一個迴圈),break 語句會停止執行最內層的迴圈,然後開始執行該塊之後的下一行代碼。(5) continue :結束當前迴圈,開始下一輪迴圈
註:continue 跳過本次迴圈,進入下一次。而 break 是直接跳出迴圈。
比如 for 迴圈,遇到 contimue 生效後,直接重新執行 for 的表達式,也就是本迴圈中 continue 下麵的語句就不執行,跳過迴圈中的一次。
contimue 其作用為結束本次迴圈。即跳出迴圈體中下麵尚未執行的語句,對於 while 迴圈,繼續求解迴圈條件。而對於 for 迴圈程式流程接著求解 for 語句頭中的第三個部分 expression 表達式。
continue 語句只結束本次迴圈,而不終止整個迴圈的執行。而 break 語句則是結束整個迴圈過程,不再判斷執行迴圈的條件是否成立。
B條件語句
(1) if : 條件語句
註:if (表達式) {語句;}
用於單分支選擇結構; 如含有交叉關係,使用併列的if語句;
(2) else :條件語句否定分支(與 if 連用)
註: if (表達式) {語句;} else {語句;}
在C語言中 else 是與 if 一起使用的一個關鍵字,表示如果滿足if條件則不執行 else ,否則執行else 。
(3) goto :無條件跳轉語句
註:goto 語句可以使程式在沒有任何條件的情況下跳轉到指定的位置,所以 goto 語句又被稱為是無條件跳轉語句。
使用 goto 語句只能 goto 到同一函數內,而不能從一個函數里 goto 到另外一個函數里。
不能從一段複雜的執行狀態中的位置 goto 到另外一個位置,比如,從多重嵌套的迴圈判斷中跳出去就是不允許的。
應該避免向兩個方向跳轉。這樣最容易導致"麵條代碼"。
C開關語句
(1) switch :用於開關語句
註:switch 語句也是一種分支語句,常常用於多分支的情況。
(2) case :開關語句分支