1. 程序運行模式
當我們將一個程序交給CPU去執行的時候,CPU只會執行main函數中的代碼,別的地方的代碼是不會執行的,因此如果想要在CPU中執行程序就必須要在main函數中從上到下一句一句執行代碼,并且只有在上一句執行完畢之后才會執行下一句。
mian是程序的入口,當mian函數中的代碼執行完畢之后就會自動結束,所以也是出口。
2. 計算機三大件
CPU : 中央處理器,處理數據的,負責計算,協調其他硬件相互和諧的工作。
內存 :存儲數據 ,臨時,效率高,通過電路存儲,電子式。
硬盤 :存儲數據 ,永久存儲,效率低,效率和轉速有關,機械式。
3. 程序是如何運行的
程序本質就是一對指令,程序存儲在硬盤之中,當雙擊點開之后,CPU會先將程序復制到內存之中,然后CPU再去讀取內存中程序的指令。這是因為在內存中CPU的讀取效率更高。
例如:播放器播放電影,播放時是運行在內存中的,但是電影文件確實保存在硬盤中的,當雙擊電影打開播放器,播放器運行在內存中,然后播放器在去分段的讀取電影文件。
或者英雄聯盟這樣的大型游戲也不是將整個游戲拷貝到內存中,而是只將exe文件拷貝到CPU中,然后需要什么文件就加載哪些文件,不用的就會釋放掉。
4. 程序處理數據
- 要不要存?
程序自己是否要把這些數據存儲起來,對于用戶操作產生的數據,或者一會兒要用的數據可以存儲起來,以便以后顯示方便快捷。 - 存到哪?
存儲到內存中,因為程序自己就在內存當中 - 如何將數據存儲在內存之中?
先在內存中找一個位置,將數據放到這個位置中,當想要使用數據的時候,只要找到這個位置就可以了。
而每一個位置在內存中都有一個獨一無二地址,16進制表示,0x開頭。通過這個地址找到這個位置,然而地址非常難記,所以給位置起個別名,通過別名找到位置。
因此在開辟空間的同時,需要指定空間的別名和類型,而變量就是內存中用來存儲數據的空間,那么變量名就是變量所代表的那塊空間的別名。所以,變量的本質就是內存中存儲數據的那塊兒空間。
聲明一個變量,實際上就是在內存中開辟一塊指定類型和別名的空間
5. 內存中的五大區域
內存當中分為五大區域
為什么要分區個區域?雖然每一個區域都是用來存儲數據的,但是不同的數據存儲在不同的區域,這樣不僅方便系統的管理,也可以使系統更快,更明確的找到要找的地址。
- 棧 - 專門用來存儲局部變量,所有的局部變量都是聲明在棧區域當中。
- 堆 - 允許程序員手動的從堆申請空間來使用。程序員可以自己申請指定字節數的空間。
- BSS段 - 用來存儲未初始化的全局變量和靜態變量,聲明一個全局變量,如果我們沒有初始化,在程序運行最開始的時候,這個變量沒有初始化時是存儲在BSS段,初始化之后,全局變量和靜態變量就會被放到常量區。
- 數據段/常量區 - 用來存儲已經初始化的全局變量、靜態變量,還有常量數據
- 代碼段 - 用來存儲程序的代碼/指令。
注意:我們堆中申請的字節空間,如果我們不主動釋放,那么系統是不會釋放的,除非程序結束
在堆中申請字節空間的步驟 a. 申請 -> b. 使用 -> c. 釋放
如何在堆區申請指定字節數的字節空間呢?C語言提供了三個函數用來申請空間。這三個函數聲明在<stdlib.h>
的系統頭文件中。
malloc()函數
malloc()
參數只有一個:size_t
類型的,也就是unsigned long
。
表示在堆內存中申請參數個連續的字節空間,返回值是void *
表示沒有類型的指針。并且返回的是創建的空間中的第一個字節的地址。
那么我們應該使用什么類型的指針變量來保存malloc()返回的地址?
用什么類型去接受,那要看你想要如何去操作申請的這些字節空間。如果你想要4個字節4個字節去操作那么就要用int *
類型去接收,如果想8個字節8個字節的去錯做那么就用double *
類型去接收。
所以: int *p = malloc(24); 就相當于在堆內存中創建了一個長度為6的整型數組
注意:
1.在堆區申請的字節空間是從低地址向高地址申請,每次申請的字節地址都是從0開始的,并且每次申請的空間不一定是連續的。但是每一次申請的指定個字節,這些字節一定是連續的。
2.在堆區申請的字節里面也是有值的,值是垃圾值,并且值不會自動清零。
3.在向堆區申請字節空間的時候,有可能會申請失敗,如果申請失敗,返回的指針就是NULL值,所以我們在申請空間之后,最好進行判斷 if(p1) 看是否申請成功。
4.申請的空間使用完畢之后,一定要進行釋放 free(p1); 如果沒有free(),那么需要等程序結束之后這些空間才會被釋放。
calloc()
calloc() 作用:向堆區申請指定字節數的空間。
參數1.多少個單位
參數2.每一個單位的字節數
例:callco(3,sizeof(int)); // 表示申請3個4字節數的空間。
同樣有可能申請失敗。與malloc()的優勢,calloc()申請的字節,申請完之后,系統會將申請到的空間自動清零
realloc()
realloc()作用:擴容。
當我們申請的字節空間不夠用的時候,我們可以使用realloc() 擴容。
例:realloc(p1,4); 表示在p1后面申請4個字節空間。
一般會在我們申請的空間后面擴容,但是如果我們申請的空間后面被占用了,或者不夠我們擴容的空間,就會重新去尋找一塊新足夠的空間申請,并將原來的數據拷貝過來,原來的空間將被釋放。
注意:我們只能操作我們申請到的字節空間,如果貿然操作其他字節空間,很有可能修改掉系統的數據,造成嚴重問題。
6. fputs()函數
作用: 將字符串數據輸出到指定的流中。
流: 標準輸出流->控制臺。文件流 --> 磁盤上的文件。
使用格式: fputs(要輸出的字符串,指定的流);
1). 使用fputs函數將字符串數據輸出到標準輸出流,也就是控制臺
標準輸出流: 控制臺. stdout
2). 將字符串存儲到文件中.
a. 要先聲明1個文件指針,指向磁盤上的文件。使用fopen函數可以創建1個指向文件的指針。
fopen函數的兩個參數:
第1個參數: 文件的路徑,代表創建的指針就指向這個文件。
第2個參數: 操作文件的模式,你要對這個文件做什么操作,必須要告訴他。
"w" --> write 代表要向這個文件寫入內容。
"r" --> read 代表從這個文件中讀取數據。
"a" --> apped 追加代表向這個文件中追加數據。
當操作模式是"w"的時候,如果文件不存在, 就會創建這個文件,如果文件存在,就會將原來的文件替換掉。
當操作模式是"a"的時候,如果文件存在則追加。如果不存在就創建這個文件。
b. 使用fputs函數將字符串寫入到指定的文件流中。
fputs(字符串,文件指針);
c. 寫完之后,一定要記得使用fclose()
函數將這個文件關閉。
7. fget()函數
作用: 從指定的流中讀取字符串。
這個流可以是標準輸入流-->控制臺,也可以是文件流。
1). 使用fgets函數從標準輸入流中讀取數據。
使用fgets函數從控制臺接收用戶輸入字符串,scanf函數gets函數也可以實現這個功能。
scanf的缺點
a. 不安全.
b. 輸入的空格會被認為結束.
gets函數的缺點.
a. 不安全.
fgets函數的優點
a. 安全.
b. 空格也會一并接收.
語法:
fgets(要將字符串存儲到哪1個數組中,最多接收多少個長度的字符串,指定流);
第2個參數: 如果參數為n 那么函數最多就接收n-1個長度的字符串,這個參數一般情況下和第1個參數數組的長度一致。
第3個參數:流,stdin: 代表標準輸入流,也就是鍵盤流從控制臺輸入。
為什么fgets函數是安全的?
1. 如果輸入的字符串的長度大于等于了第2個參數n,只會接收前面的n-1個,然后最后1個自動是'\0'。這樣,就不會崩潰。
2. 如果輸入的字符串的長度剛好等于n-1那就是剛好的。
3. 如果輸入的字符串的長度小于了n-1,那么就會將我們最后輸入的換行字符'\n'一并的接收。然后后面才是'\0'結束符。
2). 使用fgets函數從文件流中讀取數據:
就是讀取磁盤上文件的內容.
// 1. 創建1個讀取文件的文件流.
FILE* pFile = fopen("/Users/Itcast/Desktop/abc.txt", "r");
// 2. 準備1個字符數組.準備存儲讀取到的字符串數據.
char content[50];
// 3. 使用fgets函數從指定的文件流中讀取.
fgets(content, 50, pFile);
// 4. 讀取成功:
printf("讀取的內容是: %s\n",content);
// 5. 關閉文件流
fclose(pFile);
8. const修飾基本數據類型的變量
const是1個關鍵字,是來修飾變量的,也就是說在聲明變量的同時,可以使用const關鍵字來修飾。
例: const int num = 10;
一般情況下來說,被const修飾的變量具備一定程度上的不可變性,被const修飾的變量我們叫做只讀變量。
const修飾基本數據類型的變量 int、double、float、char。
1. const int num = 10;
這個時候.num變量的值只能去取值,而不能去修改.
2. int const num = 10;
效果同上.
const修飾數組.
1. const int arr[4] = {10,20,30,40};
數組的元素的值不能修改.
2. int const arr[4] = {10,20,30,40};
效果同上.
const修飾指針;
1. const int* p1 = #
無法通過p1指針去修改指針指向的變量的值,但是如果直接操作變量這是可以的,并且指針變量的值可以改,可以把另外1個變量的地址賦值給這個指針。
2. int const * p1 = #
效果同上
3. int * const p1 = #
p1的值不能修改,但是可以通過p1去修改p1指向的變量的值。
4. int const * const p1 = #
既不能修改p1的值,也不能通過p1去修改p1指向的變量的值。
const的使用場景.
1. const的特點:
被const修飾的變量,是只讀變量,只能取值而不能改值。所以,const變量的值,至始至終都不會發生變化。因此當某些數據是固定的,在整個程序運行期間都不會發生變化,并且你也不允許別人去修改時,可以使用const來修飾這個變量。
3. 當函數的參數是1個指針的時候,函數的內部是有可能會修改實參變量的值的,那么這個時候,可以使用const修飾指針參數,這樣函數內部只會使用我們的值 絕對改不了參數的值。
9. 結構體
不同的數據類型的變量是用來保存不同類型的數據的。而結構體是我們自己定義的數據類型。并指定這個數據類型的變量由哪些小的變量和成的。
語法格式:
例:
struct Student
{
char * name;
int age;
float height;
};
這代表我們新創建了一個數據類型,這個數據類型的名稱叫做 struct Student
這個新類型是由1個char * 類型,一個int 類型,一個float類型的
小的標量聯合而成的。然而只有類型是不夠的,還需要根據類型聲明變量。
聲明結構體類型的變量:
struct Student Tom;
** 結構體變量的初始化**
意義:為結構體變量中的小變量賦值。嚴格意義上是將地址賦值給小變量。結構體變量中的小變量就叫做結構體的成員。
初始化語法 使用點語法。
結構體變量名稱.成員 = 數據;
Tom.name = @"Tom";
什么時候使用結構體
當要保存一個數據,但是發現這個數據需要由其他小變量組成的,這個時候先使用結構體類自定義這個數據類型由哪些小變量合成的,然后在根據這個結構體類型聲明變量,來保存數據。
結構體注意點
一定要先聲明結構體類型,然后在聲明結構體變量。
-
結構體變量也是變量所以可以批量聲明。
struct Car{} BMWCar,BCCar,KDLKCar;
定義結構體名稱要求每一個單詞的首字母大寫。
可以在聲明結構體類型的時候聲明結構體變量。
-
匿名結構體
struct{ // 匿名結構體只能在聲明結構體的同時創建變量,并且不能單獨的聲明變量 }car1;
結構體變量的初始化
- 先聲明變量,在使用點語法一個一個賦值。
- 在聲明結構體變量的同時,就為結構體變量的成員初始化。(最常用)
- 只初始化部分成員,按順序。
- 也可以指定成員初始化。
struct Student jim = {.name = "jim",.age = 18};
結構體變量成員的默認值
聲明一個結構體變量,如果沒有給結構體變量成員賦值,那么成員是有值的,是垃圾值。只要在聲明結構體變量的同時,為一個成員變量初始化,整個結構體就會自動初始化為0,就不在是垃圾值了。
結構體類型的作用域
一般情況下結構體類型都是定義在函數外面,已讓所有函數都可以使用。
結構體變量之間的相互賦值
相同結構體類型的結構體變量是可以賦值的。
結構體變量之間賦值的原理:
將結構體變量中的每一個成員的值,拷貝過來復制一份,然后重新賦值給目標結構體變量中對應的成員。結構體變量之間的賦值是值傳遞。
結構體數組
struct 結構體類型名稱 數組名[數組長度];
可以存儲5個strut 結構體類型名稱 的結構體
struct Student students[5];
數組的類型是struct Student,可以存儲5個struct Student類型的變量。
結構體數組的初始化
students[0] = (struct Student) {"name",16}; // 需要轉化為結構體類型。
或者直接在聲明結構體數組的時候,為結構體賦值。
結構體數組長度的計算
使用sizeof計算出數組占用的總字節數/ 每一個元素占用的字節數
sizeof(students)/ sizeof(struct Student)
結構體指針
struct 結構體類型名稱* 指針名;
struct Student* pStu;
聲明了一個pStu指針變量,這個指針變量的類型是struct Student *。
這個指針就只能指向 struct Student
pStu = &stu1;
使用??梢允褂弥羔橀g接的訪問結構體變量。
(*pStu).name;
(*結構體指針名).成員
pStu -> name;
結構體指針名->成員
結構體嵌套
結構體是可以嵌套的,在一個結構體內部聲明另外一個結構體即可。
結構體與函數
作為參數
結構體是自定義的數據類型,當然可以作為參數,結構體作為參數傳值是值傳遞,如果想要在函數中修改結構體變量的值,可以使用結構體指針。
作為返回值
結構題類型完全可以作為函數的返回值,在返回的時候直接將結構體變量返回即可。如果返回結構體變量的地址,需要將結構體創建在堆區。
10. 枚舉
變量的取值只能是指定的幾個值當中的任意一個,除此之外其他不行,需要自己定義具備限定取值的類型。
作用:支持先創建一種數據類型,這個數據類型的變量的取值被限定。
語法結構:
enum Type
{
Type1,
Type2,
Type3,
};
這個數據類型的名稱叫做 **enum Type **??梢月暶鬟@個類型的變量,這個變量中就只能存儲這其中指定的任意一個。
聲明枚舉類型的變量。
enum 枚舉類型名稱 變量名 = 枚舉類型限定的取值之一。
枚舉作用域
一般定義在函數外,每一個枚舉值都對應一個整形數,默認為0,依次遞增。枚舉類型的變量,無論什么類型 都占據4個字節。而枚舉變量中真正存儲的是,枚舉值對應的整形的數。所以使用%d輸出枚舉的值。
所以也可以直接為枚舉變量賦值整形變量。但是一般不建議這么做 ,可讀性降低。命名規范 首字母大寫,每一個單詞的首字母大寫
枚舉值名稱以枚舉類型名開頭
11.type define 類型定義
作用: 為一個已經存在的數據類型取別名。
例:typedef int clint;
為int數據類型去了一個別名,叫做clint。這個時候clint 完全等價于int 。
因此當數據類型很長的時候既可以為這個數據類型取一個短一點的別名。
typedefine 數據類型 數據類型別名;
typedefine 給結構體和枚舉取別名
1.聲明結構體類型的同時給結構體區別名
typedefine struct Student
{
} Student;
2.最常用是為聲明匿名結構體的同時 取一個短別名。
typedefine struct
{
} Student;
3.枚舉同理
typedefine enum
{
} Dreiction;
12. 預處理指令
C語言從編寫到編譯、鏈接、執行的流程
編譯做的事情
- 先執行原文件中的預處理指令,如果有文件包含指令,就將文件的內容拷貝到寫指令的地方。
- 檢查語法是否符合規范,符合就生成.o目標文件,就是.c 對應的二進制指令。如果不符合語法規范,就報錯不生成.o目標文件。
- 鏈接
為.o的目標文件添加啟動代碼
告訴編譯器要調用的函數在什么地方
調用的時候去正確的地方找實現 - 鏈接成功以后.out文件運行即可。
預處理指令
預處理指令以#開頭,并且都是在編譯之前執行。
預處理指令類型
- 文件包含指令 #include。
- 宏定義:可以將一段代碼定義為一個標識,使用這個標識就代表這段代碼。
- 條件編譯指令:只編譯指定的C代碼為二進制指令。
宏定義
#define 宏名 宏值
#define N 10
會將C代碼中使用宏名的地方替換成宏值 過程叫做宏替換
宏值可以是任意語句,定義宏的時候,并不會去檢查與法,只有當完成了宏替換的時候,才會去檢查替換以后的代碼是否符合語法規范。
如果宏值是一個表達式,那么宏值并不是表達式的值,而是表達式本身。
如果宏值當中包括一個變量名,那么在使用這個宏之前必須保證這個變量已經存在。
無法通過賦值符號位宏賦值。因為宏根本就不是變量。
宏作用域
從定義宏的地方開始,后面的所有地方都可以使用這個宏。就算這個宏定義在這個大括弧里面,在這個后面,哪怕是大括弧的后面都可以使用。
默認情況下,宏從定義的地方一直到文件結束都可以使用,#undef
可以讓宏提前失效
#undef N
解除宏定義,之后宏就不可以使用了 體現實效
字符串優先,也就是字符串中不會識別宏。系統不會認為是一個宏,而認為是字符串的一部分。
宏的層層替換。一個宏值中可以使用另外一個宏名。
#define 和typedef的區別
#define
預處理指令 在預編譯的時候會把宏明替換成宏值,typedef
運行的時候才會執行。
#define
可以將任意的C代碼取一個表示名, typedef
只能為數據類型取名字。
帶參數的宏
在定義宏的時候,宏名是可以帶參數的。在這個宏值當中,可以直接使用這個參數。
#define N(a)
// 如果使用的宏有參數,就必須在使用的時候為宏傳值。
N(10);
宏帶參數替換的原理
先將參數賦值,然后在將宏值里面用到參數的地方替換為值,最后宏替換,將值替換為宏名。
使用帶參數的宏注意點
- 宏不是函數,所以宏的參數不需要添加類型說明。
- 我們在定義宏的時候,編譯器是如何區分宏名和宏值的。
#define 宏名 宏值
宏名中不可以有空格,與參數之間也不可以有空格。 - 為帶參數的宏傳值的時候,是本色傳遞,如果傳遞一個變量,并不是傳遞這個變量的值,而是直接傳遞的就是這個變量的串。
- 宏值一旦換行就認為宏定義結束了,需要使用 \ 來拼接宏
- 宏只適合于少量的代碼。
條件編譯指令
- 預處理指令, 在預編譯階段執行。
- 作用:默認情況下,我們所有的C代碼都會被編譯為二進制代碼,條件編譯指令的作用,可以讓編譯器只編譯部分的代碼。
-
#if #elif #endif
必須要有#endif
#if
和if
語句的區別
- 條件編譯指令是一個預處理指令,在預處理階段執行,而
if
語句是C代碼,在程序運行的時候執行。 - if語句無論如何全都要被編譯為二進制指令,條件編譯指令:只會將符合條件的C代碼編譯為二進制指令。
- 條件編譯指令參數必須為宏。
#ifdef 宏名
如果定義了宏名的宏 就編譯其中的代碼
#ifndef 宏名
如果沒有定義宏名就來到這里。
#endif
13. static 和 extern
C語言當中的兩個關鍵字,是用來修飾變量和函數的。
如果都沒有修飾 默認是 extern 。
static和extern修飾局部變量
static修飾局部變量,那么這個變量就叫做靜態變量,靜態變量不在存儲在棧區,而是存儲在常量區中,當函數執行完畢之后,這個靜態變量不會被回收。
當第一次執行這個函數的時候,就會將這個靜態變量聲明在常量區,第二次去執行這個函數的時候,聲明靜態變量的這句話就不會在執行了,而是直接略過,直接使用靜態變量的值。
所以static修飾的靜態變量,函數結束不會被回收,仍然存在,函數無論執行多少次,這個靜態變量只有一份。
extern不能修飾局部變量。static和extern修飾全局變量的效果
一個全局變量,最完整的步驟也應該分為兩步,1.先寫全局變量的聲明,只定義而不賦值。2.在寫全局變量的定義,定義全局變量并初始化。
全局變量可以只有聲明,如果這樣的話,那么這個全局變量的值背會編譯器自動的去實現,會將這個全局變量自動初始化為0。
全局變量也可以只有定義而沒有聲明,但是這個時候,這個全局變量的定義必須要在使用全局變量的函數的前面。
全局變量的聲明要寫在.h文件中,全局變量的實現要寫在.c文件中。
如果將全局變量定義在模塊之中,這個全局變量就必須要使用static或者extern來修飾
static修飾全局變量,這個全局變量只能在當前 模塊訪問。
extern修飾全局變量,這個全局變量就可以跨模塊訪問。static和extern修飾函數的效果
static修飾函數 函數只可以在當前模塊訪問
extern修飾函數 那么函數可以跨模塊調用
文中如果有不對的地方歡迎指出。我是xx_cc,一只長大很久但還沒有二夠的家伙。
本文已在版權印備案,如需轉載請訪問版權印。92899035