C語言程序設計【提升篇】

1. 前言


 

  企業須要能幹活的人,須要能上戰場的兵。 node

1.1 技術層次

對於解決問題的解決方案有清晰的架構圖,那麼對於技術學習也要分清層次:linux

 

1.2 接口的封裝設計

//初始化網絡鏈接句柄socket,也叫環境初始化
int socketclient_init(void** handle);
 
//發送報文接口
int socketclient_send(void* handle, unsigned char* buf, int buflen);

//接收報文接口
int socketclient_recv(void* handle, unsigned char* buf, int* buflen);

//socket環境釋放
int socketclient_destroy(void** handle);

1.3 過程的封裝設計

 

//打印函數
void PrintArray(int arr[],int len)
{
//打印 for (int i = 0; i < len; i++)
{ printf(
"%d ", arr[i]); } printf("\n"); } void MySort(int arr[],int len) { //排序 for (int i = 0; i < len; i++)   { for (int j = len - 1; j > i; j--)   {   if (arr[j] < arr[j - 1]){   int temp = arr[j];   arr[j] = arr[j - 1];   arr[j - 1] = temp;  }   }  } }   void test()
{   
int arr[] = { 10, 50, 20, 90, 30 };   int len = sizeof(arr) / sizeof(int);   PrintArray(arr, len);   MySort(arr, len);   PrintArray(arr, len); }

1.4 戰前準備

1.4.1 聽課要求

  1. 專心聽講、積極思考;c++

  2. 遇到不懂的暫時先記下,課後再問;程序員

  3. 建議準備一個筆記本(記錄重點、走神的時間);面試

  4. 當堂動手運行,不動手,永遠學不會;編程

  5. 杜絕邊聽邊敲(若是老師講的知識點很熟,你能夠邊聽邊敲)、杜絕犯困聽課。windows

  6. 若是時間容許,請課前作好預習;數組

  7. 從筆記、代碼等資料中複習上課講過的知識點。儘可能少回看視頻,別對視頻產生依賴,能夠用2倍速度回看視頻;緩存

  8. 按時完成老師佈置的練習,記錄練習中遇到的BUG和解決方案,根據本身的理解總結學到的知識點;網絡

  9. 初學者 應該抓住重點,不要鑽牛角尖遇到問題了,優先本身嘗試解決,其次谷歌百度,最後再問老師;

  10. 若是時間容許,能夠多去網上找對應階段的學習資料面試題,注意做息,積極鍛鍊。

1.4.2 資料管理

 


 

2. C語言概述

  歡迎你們來到c語言的世界,c語言是一種強大的專業化的編程語言。

1.1 C語言的起源

  貝爾實驗室的Dennis Ritchie在1972年開發了C,當時他正與ken Thompson一塊兒設計UNIX操做系統,然而,C並非徹底由Ritchie構想出來的。它來自Thompson的B語言。

1.2 使用C語言的理由

  在過去的幾十年中,c語言已成爲最流行和最重要的編程語言之一。它之因此獲得發展,是由於人們嘗試使用它後都喜歡它。過去不少年中,許多人從c語言轉而使用更強大的c++語言,但c有其自身的優點,仍然是一種重要的語言,並且它仍是學習c++的必經之路。

  • 高效性。c語言是一種高效的語言。c表現出一般只有彙編語言才具備的精細的控制能力(彙編語言是特定cpu設計所採用的一組內部制定的助記符。不一樣的cpu類型使用不一樣的彙編語言)。若是願意,您能夠細調程序以得到最大的速度或最大的內存使用率。

  • 可移植性。c語言是一種可移植的語言。意味着,在一個系統上編寫的c程序通過不多改動或不通過修改就能夠在其餘的系統上運行。

  • 強大的功能和靈活性。c強大而又靈活。好比強大靈活的UNIX操做系統即是用c編寫的。其餘的語言(Perl、Python、BASIC、Pascal)的許多編譯器和解釋器也都是用c編寫的。結果是當你在一臺Unix機器上使用Python時,最終由一個c程序負責生成最後的可執行程序。

1.3 C語言標準

1.3.1 K&R C

  起初,C語言沒有官方標準。1978年由美國電話電報公司(AT&T)貝爾實驗室正式發表了C語言。布萊恩·柯林漢(Brian Kernighan) 和 丹尼斯·裏奇(Dennis Ritchie) 出版了一本書,名叫《The C Programming Language》。這本書被 C語言開發者們稱爲K&R,不少年來被看成 C語言的非正式的標準說明。人們稱這個版本的 C語言爲K&R C

  K&R C主要介紹瞭如下特點:結構體struct)類型;長整數(long int)類型;無符號整數(unsigned int)類型;把運算符=+和=-改成+=和-=。由於=+和=-會使得編譯器不知道使用者要處理i = -10仍是i =- 10,使得處理上產生混淆。

  即便在後來ANSI C標準被提出的許多年後,K&R C仍然是許多編譯器的最準要求,許多老舊的編譯器仍然運行K&R C的標準。

1.3.2 ANSI C/C89標準

  1970到80年代,C語言被普遍應用,從大型主機到小型微機,也衍生了C語言的不少不一樣版本。1983年,美國國家標準協會(ANSI)成立了一個委員會X3J11,來制定 C語言標準。 

  1989年,美國國家標準協會(ANSI)經過了C語言標準,被稱爲ANSI X3.159-1989 "Programming Language C"。由於這個標準是1989年經過的,因此通常簡稱C89標準。有些人也簡稱ANSI C,由於這個標準是美國國家標準協會(ANSI)發佈的。

  1990年,國際標準化組織ISO)和國際電工委員會IEC)把C89標準定爲C語言的國際標準,命名爲ISO/IEC 9899:1990 - Programming languages -- C[5]  。由於此標準是在1990年發佈的,因此有些人把簡稱做C90標準。不過大多數人依然稱之爲C89標準,由於此標準與ANSI C89標準徹底等同。

  1994年,國際標準化組織(ISO)和國際電工委員會(IEC)發佈了C89標準修訂版,名叫ISO/IEC 9899:1990/Cor 1:1994[6]  ,有些人簡稱爲C94標準

  1995年,國際標準化組織(ISO)和國際電工委員會(IEC)再次發佈了C89標準修訂版,名叫ISO/IEC 9899:1990/Amd 1:1995 - C Integrity[7]  ,有些人簡稱爲C95標準

1.3.3 C99標準

  1999年1月,國際標準化組織(ISO)和國際電工委員會(IEC)發佈了C語言的新標準,名叫ISO/IEC 9899:1999 - Programming languages -- C ,簡稱C99標準。這是C語言的第二個官方標準。

例如:

  • 增長了新關鍵字 restrict,inline,_Complex,_Imaginary,_Bool

  • 支持 long long,long double _Complex,float _Complex 這樣的類型

  • 支持了不定長的數組。數組的長度就能夠用變量了。聲明類型的時候呢,就用 int a[*] 這樣的寫法。不過考慮到效率和實現,這玩意並非一個新類型。


 3. 內存分區

3.1 數據類型

3.1.1 數據類型概念

什麼是數據類型?爲何須要數據類型?
數據類型是爲了更好進行內存的管理,讓編譯器能肯定分配多少內存。

  咱們現實生活中,狗是狗,鳥是鳥等等,每一種事物都有本身的類型,那麼程序中使用數據類型也是來源於生活。 

  當咱們給狗分配內存的時候,也就至關於給狗建造狗窩,給鳥分配內存的時候,也就是給鳥建造一個鳥窩,咱們能夠給他們各自建造一個別墅,可是會形成內存的浪費,不能很好的利用內存空間。

  咱們在想,若是給鳥分配內存,只須要鳥窩大小的空間就夠了,若是給狗分配內存,那麼也只須要狗窩大小的內存,而不是給鳥和狗都分配一座別墅,形成內存的浪費。

  當咱們定義一個變量,a = 10,編譯器如何分配內存?計算機只是一個機器,它怎麼知道用多少內存能夠放得下10?

  因此說,數據類型很是重要,它能夠告訴編譯器分配多少內存能夠放得下咱們的數據。

狗窩裏面是狗,鳥窩裏面是鳥,若是沒有數據類型,你怎麼知道冰箱裏放得是一頭大象!

數據類型基本概念:

類型是對數據的抽象;
類型相同的數據具備相同的表示形式、存儲格式以及相關操做;
程序中全部的數據都一定屬於某種數據類型;
數據類型能夠理解爲建立變量的模具: 固定大小內存的別名;

 

 

 3.1.2 數據類型別名

示例代碼:

 

typedef unsigned int u32;
typedef struct _PERSON{
char name[64];
int age;
}Person;
void test(){
u32 val; //至關於 unsigned int val;
Person person; //至關於 struct PERSON person;
}

3.1.3 void數據類型 

  void字面意思是」無類型」,void* 無類型指針,無類型指針能夠指向任何類型的數據。

  void定義變量是沒有任何意義的,當你定義void a,編譯器會報錯。

  void真正用在如下兩個方面:

    • 對函數返回的限定;
    • 對函數參數的限定;

示例代碼:

//1. void修飾函數參數和函數返回
void test01(void){
printf("hello world");
}

//2. 不能定義void類型變量
void test02(){
void val; //報錯
}

//3. void* 能夠指向任何類型的數據,被稱爲萬能指針
void test03(){
int a = 10;
void* p = NULL;
p = &a;
printf("a:%d\n",*(int*)p);
char c = 'a';
p = &c;
printf("c:%c\n",*(char*)p);
}

//4. void* 經常使用於數據類型的封裝
void test04(){
//void * memcpy(void * _Dst, const void * _Src, size_t _Size);
}

3.1.4 sizeof操做符

  sizeof是c語言中的一個操做符,相似於++、--等等。sizeof可以告訴咱們編譯器爲某一特定數據或者某一個類型的數據在內存中分配空間時分配的大小,大小以字節爲單位。

基本語法:

sizeof(變量);
sizeof 變量;
sizeof(類型);

sizeof 注意點: 

  • sizeof返回的佔用空間大小是爲這個變量開闢的大小,而不僅是它用到的空間。和現今住房的建築面積和實用面積的概念差很少。因此對結構體用的時候,大多狀況下就得考慮字節對齊的問題了;

  • sizeof返回的數據結果類型是unsigned int;

  • 要注意數組名和指針變量的區別。一般狀況下,咱們總以爲數組名和指針變量差很少,可是在用sizeof的時候差異很大,對數組名用sizeof返回的是整個數組的大小,而對指針變量進行操做的時候返回的則是指針變量自己所佔得空間,在32位機的條件下通常都是4。並且當數組名做爲函數參數時,在函數內部,形參也就是個指針,因此再也不返回數組的大小;

示例代碼:

//1. sizeof基本用法
void test01(){
int a = 10;
printf("len:%d\n", sizeof(a));
printf("len:%d\n", sizeof(int));
printf("len:%d\n", sizeof a);
}
//2. sizeof 結果類型 void test02(){ unsigned int a = 10; if (a - 11 < 0){ printf("結果小於0\n"); } else{ printf("結果大於0\n");   } int b = 5; if (sizeof(b) - 10 < 0){ printf("結果小於0\n"); } else{ printf("結果大於0\n");   } } //3. sizeof 碰到數組 void TestArray(int arr[]){ printf("TestArray arr size:%d\n",sizeof(arr)); } void test03(){ int arr[] = { 10, 20, 30, 40, 50 }; printf("array size: %d\n",sizeof(arr)); //數組名在某些狀況下等價於指針 int* pArr = arr; printf("arr[2]:%d\n",pArr[2]); printf("array size: %d\n", sizeof(pArr)); //數組作函數函數參數,將退化爲指針,在函數內部再也不返回數組大小 TestArray(arr); }

3.1.5 數據類型總結

  • 數據類型本質是固定內存大小的別名,是個模具,C語言規定:經過數據類型定義變量;

  • 數據類型大小計算(sizeof);

  • 能夠給已存在的數據類型起別名typedef;

  • 數據類型的封裝(void 萬能類型);

3.2 變量

3.1.1 變量的概念

  既能讀又能寫的內存對象,稱爲變量;若一旦初始化後不能修改的對象則稱爲常量。

變量定義形式: 類型  標識符, 標識符, … , 標識符

3.1.2 變量名的本質 

  • 變量名的本質:一段連續內存空間的別名;

  • 程序經過變量來申請和命名內存空間 int a = 0;

  • 經過變量名訪問內存空間;

  • 不是向變量名讀寫數據,而是向變量所表明的內存空間中讀寫數據;

修改變量的兩種方式:

void test(){
int a = 10;
//1. 直接修改
a = 20;
printf("直接修改,a:%d\n",a);

//2. 間接修改
int* p = &a;
*p = 30;
printf("間接修改,a:%d\n", a);
}

3.3 程序的內存分區模型

3.3.1 內存分區

3.3.1.1 運行以前

咱們要想執行咱們編寫的c程序,那麼第一步須要對這個程序進行編譯。

1)預處理:宏定義展開、頭文件展開、條件編譯,這裏並不會檢查語法
2)編譯:檢查語法,將預處理後文件編譯生成彙編文件
3)彙編:將彙編文件生成目標文件(二進制文件)
4)連接:將目標文件連接爲可執行程序

  當咱們編譯完成生成可執行文件以後,咱們經過在linux下size命令能夠查看一個可執行二進制文件基本狀況: 

  經過上圖能夠得知,在沒有運行程序前,也就是說程序沒有加載到內存前,可執行程序內部已經分好3段信息,分別爲代碼區(text)、數據區(data)和未初始化數據區(bss)3 個部分(有些人直接把data和bss合起來叫作靜態區或全局區)。

代碼區
存放 CPU 執行的機器指令。一般代碼區是可共享的(即另外的執行程序能夠調用它),使其可共享的目的是對於頻繁被執行的程序,只須要在內存中有一份代碼便可。代碼區一般是隻讀的,使其只讀的緣由是防止程序意外地修改了它的指令。另外,代碼區還規劃了局部變量的相關信息。
全局初始化數據區/靜態數據區(data段)
該區包含了在程序中明確被初始化的全局變量、已經初始化的靜態變量(包括全局靜態變量和局部靜態變量)和常量數據(如字符串常量)。

未初始化數據區(又叫 bss 區)
存入的是全局未初始化變量和未初始化靜態變量。未初始化數據區的數據在程序開始執行以前被內核初始化爲 0 或者空(NULL)。
整體來說說,程序源代碼被編譯以後主要分紅兩種段:程序指令和程序數據。代碼段屬於程序指令,而數據域段和.bss段屬於程序數據。

那爲何把程序的指令和程序數據分開呢?

程序被load到內存中以後,能夠將數據和代碼分別映射到兩個內存區域。因爲數據區域對進程來講是可讀可寫的,而指令區域對程序來說說是隻讀的,因此分區以後呢,能夠將程序指令區域和數據區域分別設置成可讀可寫或只讀。這樣能夠防止程序的指令有意或者無心被修改;
當系統中運行着多個一樣的程序的時候,這些程序執行的指令都是同樣的,因此只須要內存中保存一份程序的指令就能夠了,只是每個程序運行中數據不同而已,這樣能夠節省大量的內存。好比說以前的Windows Internet Explorer 7.0運行起來以後, 它須要佔用112 844KB的內存,它的私有部分數據有大概15 944KB,也就是說有96 900KB空間是共享的,若是程序中運行了幾百個這樣的進程,能夠想象共享的方法能夠節省大量的內存。
3.3.1.2 運行以後 

  程序在加載到內存前,代碼區和全局區(data和bss)的大小就是固定的,程序運行期間不能改變。而後,運行可執行程序,操做系統把物理硬盤程序load(加載)到內存,除了根據可執行程序的信息分出代碼區(text)、數據區(data)和未初始化數據區(bss)以外,還額外增長了棧區、堆區。

代碼區(text segment)
加載的是可執行文件代碼段,全部的可執行代碼都加載到代碼區,這塊內存是不能夠在運行期間修改的。

未初始化數據區(BSS)
加載的是可執行文件BSS段,位置能夠分開亦能夠緊靠數據段,存儲於數據段的數據(全局未初始化,靜態未初始化數據)的生存週期爲整個程序運行過程。

全局初始化數據區/靜態數據區(data segment)
加載的是可執行文件數據段,存儲於數據段(全局初始化,靜態初始化數據,文字常量(只讀))的數據的生存週期爲整個程序運行過程。

棧區(stack)
棧是一種先進後出的內存結構,由編譯器自動分配釋放,存放函數的參數值、返回值、局部變量等。在程序運行過程當中實時加載和釋放,所以,局部變量的生存週期爲申請到釋放該段棧空間。
 
堆區(heap)
堆是一個大容器,它的容量要遠遠大於棧,但沒有棧那樣先進後出的順序。用於動態內存分配。堆在內存中位於BSS區和棧區之間。通常由程序員分配和釋放,若程序員不釋放,程序結束時由操做系統回收。

類型

做用域

生命週期

存儲位置

auto變量

一對{}內

當前函數

棧區

static局部變量

一對{}內

整個程序運行期

初始化在data段,未初始化在BSS段

extern變量

整個程序

整個程序運行期

初始化在data段,未初始化在BSS段

static全局變量

當前文件

整個程序運行期

初始化在data段,未初始化在BSS段

extern函數

整個程序

整個程序運行期

代碼區

static函數

當前文件

整個程序運行期

代碼區

register變量

一對{}內

當前函數

運行時存儲在CPU寄存器

字符串常量

當前文件

整個程序運行期

data段

注意:創建正確程序運行內存佈局圖是學好C的關鍵!!

3.3.2 分區模型 

3.3.2.1 棧區

  由系統進行內存的管理。主要存放函數的參數以及局部變量。在函數完成執行,系統自行釋放棧區內存,不須要用戶管理。

 

#char* func(){
char p[] = "hello world!"; //在棧區存儲 亂碼
printf("%s\n", p);
return p;
}
void test(){
char* p = NULL;
p = func();  
printf("%s\n",p); 
}

 

3.3.2.2 堆區

  由編程人員手動申請,手動釋放,若不手動釋放,程序結束後由系統回收,生命週期是整個程序運行期間。使用malloc或者new進行堆的申請。

char* func()
{
char* str = malloc(100);
strcpy(str, "hello world!");
printf("%s\n",str);
return str;
}
void test01()
{
char* p = NULL;
p = func();
printf("%s\n",p);
}
void allocateSpace(char* p)
{
p = malloc(100);
strcpy(p, "hello world!");
printf("%s\n", p);
}
void test02()
{
char* p = NULL;
allocateSpace(p);
printf("%s\n", p);
}

 

堆分配內存API:

#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);

功能:
在內存動態存儲區中分配nmemb塊長度爲size字節的連續區域。calloc自動將分配的內存 置0。

參數:
nmemb:所需內存單元數量
size:每一個內存單元的大小(單位:字節)

返回值:
成功:分配空間的起始地址
失敗:NULL

 

#include <stdlib.h>
void *realloc(void *ptr, size_t size);

功能:
從新分配用malloc或者calloc函數在堆中分配內存空間的大小。
realloc不會自動清理增長的內存,須要手動清理,若是指定的地址後面有連續的空間,那麼就會在已有地址基礎上增長內存,若是指定的地址後面沒有空間,那麼realloc會從新分配新的連續內存,把舊內存的值拷貝到新內存,同時釋放舊內存。

參數:
ptr:爲以前用malloc或者calloc分配的內存地址,若是此參數等於NULL,那麼和realloc與malloc功能一致
size:爲從新分配內存的大小, 單位:字節
返回值:
成功:新分配的堆內存地址
失敗:NULL

示例代碼: 

void test01(){ 
int* p1 = calloc(10,sizeof(int));
if (p1 == NULL){
return;
}
for (int i = 0; i < 10; i ++){
p1[i] = i + 1;
}
for (int i = 0; i < 10; i++){
printf("%d ",p1[i]);
}
printf("\n");
free(p1);
}
void test02(){
int* p1 = calloc(10, sizeof(int));
if (p1 == NULL){
return;
}
for (int i = 0; i < 10; i++){
p1[i] = i + 1;
}
int* p2 = realloc(p1, 15 * sizeof(int));
if (p2 == NULL){
return;
}
printf("%d\n", p1);
printf("%d\n", p2);
//打印
for (int i = 0; i < 15; i++){
printf("%d ", p2[i]);
}
printf("\n");
//從新賦值
for (int i = 0; i < 15; i++){
p2[i] = i + 1;
}
//再次打印
for (int i = 0; i < 15; i++){
printf("%d ", p2[i]);
}
printf("\n");
free(p2);
}

3.3.2.3 全局/靜態區

 

  全局靜態區內的變量在編譯階段已經分配好內存空間並初始化。這塊內存在程序運行期間一直存在,它主要存儲全局變量靜態變量常量

 

注意

(1)這裏不區分初始化和未初始化的數據區,是由於靜態存儲區內的變量若不顯示初始化,則編譯器會自動以默認的方式進行初始化,即靜態存儲區內不存在未初始化的變量。

(2)全局靜態存儲區內的常量分爲常變量和字符串常量,一經初始化,不可修改。靜態存儲內的常變量是全局變量,與局部常變量不一樣,區別在於局部常變量存放於棧,實際可間接經過指針或者引用進行修改,而全局常變量存放於靜態常量區則不能夠間接修改。

(3)字符串常量存儲在全局/靜態存儲區的常量區。 

示例代碼

int v1 = 10;//全局/靜態區
const int v2 = 20; //常量,一旦初始化,不可修改
static int v3 = 20; //全局/靜態區
char *p1; //全局/靜態區,編譯器默認初始化爲NULL
//那麼全局static int 和 全局int變量有什麼區別?
void test(){
static int v4 = 20; //全局/靜態區
}
加深理解

char
* func(){ static char arr[] = "hello world!"; //在靜態區存儲 可讀可寫 arr[2] = 'c'; char* p = "hello world!"; //全局/靜態區-字符串常量區 //p[2] = 'c'; //只讀,不可修改 printf("%d\n",arr); printf("%d\n",p); printf("%s\n", arr); return arr; } void test(){ char* p = func(); printf("%s\n",p); }

字符串常量是否可修改?字符串常量優化:

ANSI C中規定:修改字符串常量,結果是未定義的。

ANSI C並無規定編譯器的實現者對字符串的處理,例如:

1.有些編譯器可修改字符串常量,有些編譯器則不可修改字符串常量。

2.有些編譯器把多個相同的字符串常量當作一個(這種優化可能出如今字符串常量中,節省空間),有些則不進行此優化。若是進行優化,則可能致使修改一個字符串常量致使另外的字符串常量也發生變化,結果不可知。

因此儘可能不要去修改字符串常量

C99標準:

char *p = "abc"; defines p with type ‘‘pointer to char’’ and initializes it to point to an object with type ‘‘array of char’’ with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array, the behavior is undefined.

 

字符串常量地址是否相同

tc2.0,同文件字符串常量地址不一樣

Vs2013,字符串常量地址同文件和不一樣文件都相同

Dev c++、QT同文件相同不一樣文件不一樣

3.3.2.4 總結

  在理解C/C++內存分區時,常會碰到以下術語:數據區,堆,棧,靜態區,常量區,全局區,字符串常量區,文字常量區,代碼區等等,初學者被搞得雲裏霧裏。在這裏,嘗試捋清楚以上分區的關係。

  數據區包括:堆,棧,全局/靜態存儲區。
  全局/靜態存儲區包括:常量區,全局區、靜態區。
  常量區包括:字符串常量區、常變量區。
  代碼區:存放程序編譯後的二進制代碼,不可尋址區。

  能夠說,C/C++內存分區其實只有兩個,即代碼區和數據區。

3.3.3 函數調用模型

3.3.3.1 函數調用流程

  棧(stack)是現代計算機程序裏最爲重要的概念之一,幾乎每個程序都使用了棧,沒有棧就沒有函數,沒有局部變量,也就沒有咱們現在能見到的全部計算機的語言。在解釋爲何棧如此重要以前,咱們先了解一下傳統的棧的定義:

  在經典的計算機科學中,棧被定義爲一個特殊的容器,用戶能夠將數據壓入棧中(入棧,push),也能夠將壓入棧中的數據彈出(出棧,pop),可是棧容器必須遵循一條規則:先入棧的數據最後出棧(First In First Out,FIFO).

  在經典的操做系統中,棧老是向下增加的。壓棧的操做使得棧頂的地址減少,彈出操做使得棧頂地址增大。

  棧在程序運行中具備極其重要的地位。最重要的,棧保存一個函數調用所須要維護的信息,這一般被稱爲堆棧幀(Stack Frame)或者活動記錄(Activate Record).一個函數調用過程所須要的信息通常包括如下幾個方面:

函數的返回地址;
函數的參數;
臨時變量;
保存的上下文:包括在函數調用先後須要保持不變的寄存器。

咱們從下面的代碼,分析如下函數的調用過程:

 

int func(int a,int b){
int t_a = a;
int t_b = b;
return t_a + t_b;
}
int main(){
int ret = func(10, 20);
int a = 20;
return EXIT_SUCCESS;
}

 

 

 

 

3.3.3.2 調用慣例 

     如今,咱們大體瞭解了函數調用的過程,這期間有一個現象,那就是函數的調用者和被調用者對函數調用有着一致的理解,例如,它們雙方都一致的認爲函數的參數是按照某個固定的方式壓入棧中。若是不這樣的話,函數將沒法正確運行。

     若是函數調用方在傳遞參數的時候先壓入a參數,再壓入b參數,而被調用函數則認爲先壓入的是b,後壓入的是a,那麼被調用函數在使用a,b值時候,就會顛倒。

所以,函數的調用方和被調用方對於函數是如何調用的必須有一個明確的約定,只有雙方都遵循一樣的約定,函數纔可以被正確的調用,這樣的約定被稱爲」調用慣例(Calling Convention)」.一個調用慣例通常包含如下幾個方面:

函數參數的傳遞順序和方式

函數的傳遞有不少種方式,最多見的是經過棧傳遞。函數的調用方將參數壓入棧中,函數本身再從棧中將參數取出。對於有多個參數的函數,調用慣例要規定函數調用方將參數壓棧的順序:從左向右,仍是從右向左。有些調用慣例還容許使用寄存器傳遞參數,以提升性能。

棧的維護方式

在函數將參數壓入棧中以後,函數體會被調用,此後須要將被壓入棧中的參數所有彈出,以使得棧在函數調用先後保持一致。這個彈出的工做能夠由函數的調用方來完成,也能夠由函數自己來完成。

爲了在連接的時候對調用慣例進行區分,調用慣例要對函數自己的名字進行修飾。不一樣的調用慣例有不一樣的名字修飾策略。

事實上,在c語言裏,存在着多個調用慣例,而默認的是cdecl.任何一個沒有顯示指定調用慣例的函數都是默認是cdecl慣例。好比咱們上面對於func函數的聲明,它的完整寫法應該是:

 int _cdecl func(int a,int b);

注意: _cdecl不是標準的關鍵字,在不一樣的編譯器裏可能有不一樣的寫法,例如gcc裏就不存在_cdecl這樣的關鍵字,而是使用__attribute__((cdecl)).

 

調用慣例

出棧方

參數傳遞

名字修飾

cdecl

函數調用方

從右至左參數入棧

下劃線+函數名

stdcall

函數自己

從右至左參數入棧

下劃線+函數名+@+參數字節數

fastcall

函數自己

前兩個參數由寄存器傳遞,其他參數經過堆棧傳遞。

@+函數名+@+參數的字節數

pascal

函數自己

從左至右參數入棧

較爲複雜,參見相關文檔

3.3.3.2 函數變量傳遞分析

 

 

 

 

3.3.4 棧的生長方向和內存存放方向

 

//1. 棧的生長方向
void test01(){
int a = 10;
int b = 20;
int c = 30;
int d = 40;
printf("a = %d\n", &a);
printf("b = %d\n", &b);
printf("c = %d\n", &c);
printf("d = %d\n", &d);
//a的地址大於b的地址,故而生長方向向下
}

//2. 內存生長方向(小端模式)
void test02(){
//高位字節 -> 地位字節
int num = 0xaabbccdd;
unsigned char* p = &num;

//從首地址開始的第一個字節
printf("%x\n",*p);
printf("%x\n", *(p + 1));
printf("%x\n", *(p + 2));
printf("%x\n", *(p + 3));
}

4. 指針強化 

4.1 指針是一種數據類型

4.1.1 指針變量

  指針是一種數據類型,佔用內存空間,用來保存內存地址。

void test01(){
int* p1 = 0x1234;
int*** p2 = 0x1111;
printf("p1 size:%d\n",sizeof(p1));
printf("p2 size:%d\n",sizeof(p2));

//指針是變量,指針自己也佔內存空間,指針也能夠被賦值
int a = 10;
p1 = &a;
printf("p1 address:%p\n", &p1);
printf("p1 address:%p\n", p1);
printf("a address:%p\n", &a);
}

4.1.2 空指針野指針

①空指針

 

  標準定義了NULL指針,它做爲一個特殊的指針變量,表示不指向任何東西。要使一個指針爲NULL,能夠給它賦值一個零值。爲了測試一個指針百年來那個是否爲NULL,你能夠將它與零值進行比較。

  對指針解引用操做能夠得到它所指向的值。但從定義上看,NULL指針並未執行任何東西,由於對一個NULL指針因引用是一個非法的操做,在解引用以前,必須確保它不是一個NULL指針。

 若是對一個NULL指針間接訪問會發生什麼呢?結果因編譯器而異。

  不容許向NULL和非法地址拷貝內存

void test(){
char *p = NULL;
//給p指向的內存區域拷貝內容
strcpy(p, "1111"); //err
char *q = 0x1122;
//給q指向的內存區域拷貝內容
strcpy(q, "2222"); //err
}

② 野指針 

在使用指針時,要避免野指針的出現:

  野指針指向一個已刪除的對象或未申請訪問受限內存區域的指針。與空指針不一樣,野指針沒法經過簡單地判斷是否爲 NULL避免,而只能經過養成良好的編程習慣來盡力減小。對野指針進行操做很容易形成程序錯誤。

什麼狀況下回致使野指針?

指針變量未初始化

任何指針變量剛被建立時不會自動成爲NULL指針,它的缺省值是隨機的,它會亂指一氣。因此,指針變量在建立的同時應當被初始化,要麼將指針設置爲NULL,要麼讓它指向合法的內存。

指針釋放後未置空

有時指針在free或delete後未賦值 NULL,便會令人覺得是合法的。別看free和delete的名字(尤爲是delete),它們只是把指針所指的內存給釋放掉,但並無把指針自己幹掉。此時指針指向的就是「垃圾」內存。釋放後的指針應當即將指針置爲NULL,防止產生「野指針」。

指針操做超越變量做用域

不要返回指向棧內存的指針或引用,由於棧內存在函數結束時會被釋放。

void test(){
int* p = 0x001; //未初始化
printf("%p\n",p);
*p = 100;
}

操做野指針是很是危險的操做,應該規避野指針的出現: 

初始化時置 NULL

指針變量必定要初始化爲NULL,由於任何指針變量剛被建立時不會自動成爲NULL指針,它的缺省值是隨機的。

釋放時置 NULL

當指針p指向的內存空間釋放時,沒有設置指針p的值爲NULL。delete和free只是把內存空間釋放了,可是並無將指針p的值賦爲NULL。一般判斷一個指針是否合法,都是使用if語句測試該指針是否爲NULL。

4.1.3 間接訪問操做符

  經過一個指針訪問它所指向的地址的過程叫作間接訪問,或者叫解引用指針,這個用於執行間接訪問的操做符是*。

  注意:對一個int*類型指針解引用會產生一個整型值,相似地,對一個float*指針解引用會產生了一個float類型的值。

int arr[5];
int *p = * (&arr);
int arr1[5][3] arr1 = int(*)[3]
&arr1

 在指針聲明時,* 號表示所聲明的變量爲指針

在指針使用時,* 號表示操做指針所指向的內存空間

1)* 至關經過地址(指針變量的值)找到指針指向的內存,再操做內存

2)* 放在等號的左邊賦值(給內存賦值,寫內存)

3)* 放在等號的右邊取值(從內存中取值,讀內存)

//解引用
void test01(){
//定義指針
int* p = NULL;
//指針指向誰,就把誰的地址賦給指針
int a = 10;
p = &a;
*p = 20;//*在左邊當左值,必須確保內存可寫
//*號放右面,從內存中讀值
int b = *p;
//必須確保內存可寫
char* str = "hello world!";
*str = 'm';
printf("a:%d\n", a);
printf("*p:%d\n", *p);
printf("b:%d\n", b);
}

4.1.4 指針的步長

  指針是一種數據類型,是指它指向的內存空間的數據類型。指針所指向的內存空間決定了指針的步長。指針的步長指的是,當指針+1時候,移動多少字節單位。

思考以下問題:

int a = 0xaabbccdd;
unsigned int *p1 = &a;
unsigned char *p2 = &a;
//爲何*p1打印出來正確結果?
printf("%x\n", *p1);
//爲何*p2沒有打印出來正確結果?
printf("%x\n", *p2);
//爲何p1指針+1加了4字節?
printf("p1  =%d\n", p1);
printf("p1+1=%d\n", p1 + 1);
//爲何p2指針+1加了1字節?
printf("p2  =%d\n", p2);
printf("p2+1=%d\n", p2 + 1);

4.2 指針的意義_間接賦值

 

4.2.1 間接賦值的三大條件

 

經過指針間接賦值成立的三大條件:

  • 1)2個變量(一個普通變量一個指針變量、或者一個實參一個形參)

  • 2)創建關係

  • 3)經過 * 操做指針指向的內存

void test(){
int a = 100;    //兩個變量
int *p = NULL;
//創建關係
//指針指向誰,就把誰的地址賦值給指針
p = &a;
//經過*操做內存
*p = 22;
}
 4.2.2 如何定義合適的指針變量

 

void test(){
int b;  
int *q = &b; //0級指針
int **t = &q;
int ***m = &t;
}

 

4.2.3 間接賦值:從0級指針到1級指針
int func1(){ return 10; }
void func2(int a){
a = 100;
}
//指針的意義_間接賦值
void test02(){
int a = 0;
a = func1();
printf("a = %d\n", a);

//爲何沒有修改?
func2(a);
printf("a = %d\n", a);
}
//指針的間接賦值
void func3(int* a){
*a = 100;
}
void test03(){
int a = 0;
a = func1();
printf("a = %d\n", a);
//修改
func3(&a);
printf("a = %d\n", a);
}
4.2.4 間接賦值:從1級指針到2級指針 
void AllocateSpace(char** p){
*p = (char*)malloc(100);
strcpy(*p, "hello world!");
}
void FreeSpace(char** p){
if (p == NULL){
return;
}
if (*p != NULL){
free(*p);
*p = NULL;
}
}
void test(){
char* p = NULL;
AllocateSpace(&p);
printf("%s\n",p);
FreeSpace(&p);
if (p == NULL){
printf("p內存釋放!\n");
}
}

4.2.4 間接賦值的推論 

  • 1級指針形參,去間接修改了0級指針(實參)的值。

  • 2級指針形參,去間接修改了1級指針(實參)的值。

  • 3級指針形參,去間接修改了2級指針(實參)的值。

  • n級指針形參,去間接修改了n-1級指針(實參)的值。

4.3 指針作函數參數

  指針作函數參數,具有輸入和輸出特性:

  • 輸入:主調函數分配內存

  • 輸出:被調用函數分配內存

4.3.1 輸入特性

void fun(char *p /* in */)
{
//給p指向的內存區域拷貝內容
strcpy(p, "abcddsgsd");
}
void test(void)
{
//輸入,主調函數分配內存
char buf[100] = { 0 };
fun(buf);
printf("buf  = %s\n", buf);
}
4.3.2 輸出特性 
void fun(char **p /* out */, int *len)
{
char *tmp = (char *)malloc(100);
if (tmp == NULL)
{
return;
}
strcpy(tmp, "adlsgjldsk");
//間接賦值
*p = tmp;
*len = strlen(tmp);
}

void test(void)
{
//輸出,被調用函數分配內存,地址傳遞
char *p = NULL;
int len = 0;
fun(&p, &len);
if (p != NULL)
{
printf("p = %s, len = %d\n", p, len);
}

4.4 字符串指針強化

4.4.1 字符串指針作函數參數

4.4.1.1 字符串基本操做
//字符串基本操做
//字符串是以0或者'\0'結尾的字符數組,(數字0和字符'\0'等價)
void test01(){

//字符數組只能初始化5個字符,當輸出的時候,從開始位置直到找到0結束
char str1[] = { 'h', 'e', 'l', 'l', 'o' };
printf("%s\n",str1);

//字符數組部分初始化,剩餘填0
char str2[100] = { 'h', 'e', 'l', 'l', 'o' };
printf("%s\n", str2);

//若是以字符串初始化,那麼編譯器默認會在字符串尾部添加'\0'
char str3[] = "hello";
printf("%s\n",str3);
printf("sizeof str:%d\n",sizeof(str3));
printf("strlen str:%d\n",strlen(str3));

//sizeof計算數組大小,數組包含'\0'字符
//strlen計算字符串的長度,到'\0'結束
//那麼若是我這麼寫,結果是多少呢?
char str4[100] = "hello";
printf("sizeof str:%d\n", sizeof(str4));
printf("strlen str:%d\n", strlen(str4));

//請問下面輸入結果是多少?sizeof結果是多少?strlen結果是多少?
char str5[] = "hello\0world"; 
printf("%s\n",str5);
printf("sizeof str5:%d\n",sizeof(str5));
printf("strlen str5:%d\n",strlen(str5));

//再請問下面輸入結果是多少?sizeof結果是多少?strlen結果是多少?
char str6[] = "hello\012world";
printf("%s\n", str6);
printf("sizeof str6:%d\n", sizeof(str6));
printf("strlen str6:%d\n", strlen(str6));
}

八進制和十六進制轉義字符:

     在C中有兩種特殊的字符,八進制轉義字符和十六進制轉義字符,八進制字符的通常形式是'\ddd',d是0-7的數字。十六進制字符的通常形式是'\xhh',h是0-9或A-F內的一個。八進制字符和十六進制字符表示的是字符的ASCII碼對應的數值。

好比 :

n '\063'表示的是字符'3',由於'3'的ASCII碼是30(十六進制),48(十進制),63(八進制)。

n '\x41'表示的是字符'A',由於'A'的ASCII碼是41(十六進制),65(十進制),101(八進制)。

4.4.1.2 字符串拷貝功能實現
//拷貝方法1
void copy_string01(char* dest, char* source ){
for (int i = 0; source[i] != '\0';i++){
dest[i] = source[i];
}
}
//拷貝方法2
void copy_string02(char* dest, char* source){
while (*source != '\0' /* *source != 0 */){
*dest = *source;
source++;
dest++;
}
}
//拷貝方法3
void copy_string03(char* dest, char* source){
//判斷*dest是否爲0,0則退出循環
while (*dest++ = *source++){}
}
//拷貝方法4
//1)應該判斷下傳入的參數是否爲NULL
//2)最好不要直接使用形參
int copy_string04(char* dest, char* source){
if (dest == NULL){
return -1;
}
if (source == NULL){
return -2;
}
char* src = source;
char* tar = dest;
while (*tar++ = *src++){}
return 0;
}
4.4.1.3 字符串反轉模型

void reverse_string(char* str){
if (str == NULL){
return;
}
int begin = 0;
int end = strlen(str) - 1;
while (begin < end){
//交換兩個字符元素
char temp = str[begin];
str[begin] = str[end];
str[end] = temp;
begin++;
end--;
}
}
void test(){
char str[] = "abcdefghijklmn";
printf("str:%s\n", str);
reverse_string(str);
printf("str:%s\n", str);
}

4.4.2 字符串的格式化 

4.4.2.1 sprintf
#include <stdio.h>
int sprintf(char *str, const char *format, ...);
功能:
     根據參數format字符串來轉換並格式化數據,而後將結果輸出到str指定的空間中,直到    出現字符串結束符 '\0'  爲止。
參數:
str:字符串首地址
format:字符串格式,用法和printf()同樣
返回值:
成功:實際格式化的字符個數
失敗: - 1
void test(){
//1. 格式化字符串
char buf[1024] = { 0 };
sprintf(buf, "你好,%s,歡迎加入咱們!", "John");
printf("buf:%s\n",buf);
memset(buf, 0, 1024);
sprintf(buf, "我今年%d歲了!", 20);
printf("buf:%s\n", buf);

//2. 拼接字符串
memset(buf, 0, 1024);
char str1[] = "hello";
char str2[] = "world";
int len = sprintf(buf,"%s %s",str1,str2);
printf("buf:%s len:%d\n", buf,len);

//3. 數字轉字符串
memset(buf, 0, 1024);
int num = 100;
sprintf(buf, "%d", num);
printf("buf:%s\n", buf);

//設置寬度 右對齊
memset(buf, 0, 1024);
sprintf(buf, "%8d", num);
printf("buf:%s\n", buf);

//設置寬度 左對齊
memset(buf, 0, 1024);
sprintf(buf, "%-8d", num);
printf("buf:%s\n", buf);

//轉成16進制字符串 小寫
memset(buf, 0, 1024);
sprintf(buf, "0x%x", num);
printf("buf:%s\n", buf);

//轉成8進制字符串
memset(buf, 0, 1024);
sprintf(buf, "0%o", num);
printf("buf:%s\n", buf);
}

4.4.2.2 sscanf 

#include <stdio.h>
int sscanf(const char *str, const char *format, ...);
功能:
    從str指定的字符串讀取數據,並根據參數format字符串來轉換並格式化數據。
參數:
str:指定的字符串首地址
format:字符串格式,用法和scanf()同樣
返回值:
成功:實際讀取的字符個數
失敗: - 1

格式

做用

%*s或%*d

跳過數據

%[width]s

讀指定寬度的數據

%[a-z]

匹配a到z中任意字符(儘量多的匹配)

%[aBc]

匹配a、B、c中一員,貪婪性

%[^a]

匹配非a的任意字符,貪婪性

%[^a-z]

表示讀取除a-z之外的全部字符

 

//1. 跳過數據
void test01(){
char buf[1024] = { 0 };
//跳過前面的數字
//匹配第一個字符是不是數字,若是是,則跳過
//若是不是則中止匹配
sscanf("123456aaaa", "%*d%s", buf); 
printf("buf:%s\n",buf);
}

//2. 讀取指定寬度數據
void test02(){
char buf[1024] = { 0 };
//跳過前面的數字
sscanf("123456aaaa", "%7s", buf);
printf("buf:%s\n", buf);
}

//3. 匹配a-z中任意字符
void test03(){
char buf[1024] = { 0 };
//跳過前面的數字
//先匹配第一個字符,判斷字符是不是a-z中的字符,若是是匹配
//若是不是中止匹配
sscanf("abcdefg123456", "%[a-z]", buf);
printf("buf:%s\n", buf);
}

//4. 匹配aBc中的任何一個
void test04(){
char buf[1024] = { 0 };
//跳過前面的數字
//先匹配第一個字符是不是aBc中的一個,若是是,則匹配,若是不是則中止匹配
sscanf("abcdefg123456", "%[aBc]", buf);
printf("buf:%s\n", buf);
}

//5. 匹配非a的任意字符
void test05(){
char buf[1024] = { 0 };
//跳過前面的數字
//先匹配第一個字符是不是aBc中的一個,若是是,則匹配,若是不是則中止匹配
sscanf("bcdefag123456", "%[^a]", buf);
printf("buf:%s\n", buf);
}

//6. 匹配非a-z中的任意字符
void test06(){
char buf[1024] = { 0 };
//跳過前面的數字
//先匹配第一個字符是不是aBc中的一個,若是是,則匹配,若是不是則中止匹配
sscanf("123456ABCDbcdefag", "%[^a-z]", buf);
printf("buf:%s\n", buf);
}

4.5 一級指針易錯點

4.5.1 越界

void test(){

char buf[3] = "abc";

printf("buf:%s\n",buf);

}

4.5.2 指針疊加會不斷改變指針指向

void test(){

char *p = (char *)malloc(50);

char buf[] = "abcdef";

int n = strlen(buf);

int i = 0;

 

for (i = 0; i < n; i++)

{

*p = buf[i];

p++; //修改原指針指向

}

free(p);

}

4.5.3 返回局部變量地址

char *get_str()

{

char str[] = "abcdedsgads"; //棧區,

printf("[get_str]str = %s\n", str);

return str;

}

4.5.4 同一塊內存釋放屢次

void test(){

char *p = NULL;

p = (char *)malloc(50);

strcpy(p, "abcdef");

if (p != NULL)

{

//free()函數的功能只是告訴系統 p 指向的內存能夠回收了

// 就是說,p 指向的內存使用權交還給系統

//可是,p的值仍是原來的值(野指針)p仍是指向原來的內存

free(p);

}

if (p != NULL)

{

free(p);

}

}

4.6 const使用

//const修飾變量
void test01(){
//1. const基本概念
const int i = 0;
//i = 100; //錯誤,只讀變量初始化以後不能修改
//2. 定義const變量最好初始化
const int j;
//j = 100; //錯誤,不能再次賦值
//3. c語言的const是一個只讀變量,並非一個常量,可經過指針間接修改
const int k = 10;
//k = 100; //錯誤,不可直接修改,咱們可經過指針間接修改
printf("k:%d\n", k);
int* p = &k;
*p = 100;
printf("k:%d\n", k);
}
//const 修飾指針
void test02(){
int a = 10;
int b = 20;
//const放在*號左側 修飾p_a指針指向的內存空間不能修改,但可修改指針的指向
const int* p_a = &a;
//*p_a = 100; //不可修改指針指向的內存空間
p_a = &b; //可修改指針的指向
//const放在*號的右側, 修飾指針的指向不能修改,可是可修改指針指向的內存空間
int* const p_b = &a;
//p_b = &b; //不可修改指針的指向
*p_b = 100; //可修改指針指向的內存空間
//指針的指向和指針指向的內存空間都不能修改
const int* const p_c = &a;
}
//const指針用法
struct Person{
char name[64];
int id;
int age;
int score;
};
//每次都對對象進行拷貝,效率低,應該用指針
void printPersonByValue(struct Person person){
printf("Name:%s\n", person.name);
printf("Name:%d\n", person.id);
printf("Name:%d\n", person.age);
printf("Name:%d\n", person.score);
}
//可是用指針會有反作用,可能會不當心修改原數據
void printPersonByPointer(const struct Person *person){
printf("Name:%s\n", person->name);
printf("Name:%d\n", person->id);
printf("Name:%d\n", person->age);
printf("Name:%d\n", person->score);
}
void test03(){
struct Person p = { "Obama", 1101, 23, 87 };
//printPersonByValue(p);
printPersonByPointer(&p);
}

5. 指針的指針(二級指針)

5.1 二級指針基本概念

  這裏讓咱們花點時間來看一個例子,揭開這個即將開始的序幕。考慮下面這些聲明:

int a = 12;

int *b = &a;

它們以下圖進行內存分配:

 

 

 

   假定咱們又有了第3個變量,名叫c,並用下面這條語句對它進行初始化:

c = &b;

它在內存中的大概模樣大體以下:

 

  問題是:c的類型是什麼?顯然它是一個指針,但它所指向的是什麼?變量b是一個「指向整型的指針」,因此任何指向b的類型必須是指向「指向整型的指針」的指針,更通俗地說,是一個指針的指針。

  它合法嗎?是的!指針變量和其餘變量同樣,佔據內存中某個特定的位置,因此用&操做符取得它的地址是合法的。

  那麼這個變量的聲明是怎樣的聲明的呢?

int **c = &b;

  那麼這個**c如何理解呢*操做符具備從右想作的結合性,因此這個表達式至關於*(*c),咱們從裏向外逐層求職*c訪問c所指向的位置,咱們知道這是變量b.第二個間接訪問操做符訪問這個位置所指向的地址,也就是變量a.指針的指針並不難懂,只須要留心全部的箭頭,若是表達式中出現了間接訪問操做符,你就要隨箭頭訪問它所指向的位置。

5.2 二級指針作形參輸出特性

二級指針作參數的輸出特性是指由被調函數分配內存。

//被調函數,由參數n肯定分配多少個元素內存
void allocate_space(int **arr,int n){
//堆上分配n個int類型元素內存
int *temp = (int *)malloc(sizeof(int)* n);
if (NULL == temp){
return;
}
//給內存初始化值
int *pTemp = temp;
for (int i = 0; i < n;i ++){
//temp[i] = i + 100;
*pTemp = i + 100;
pTemp++;
}
//指針間接賦值
*arr = temp;
}
//打印數組
void print_array(int *arr,int n){
for (int i = 0; i < n;i ++){
printf("%d ",arr[i]);
}
printf("\n");
}
//二級指針輸出特性(由被調函數分配內存)
void test(){
int *arr = NULL;
int n = 10;
//給arr指針間接賦值
allocate_space(&arr,n);
//輸出arr指向數組的內存
print_array(arr, n);
//釋放arr所指向內存空間的值
if (arr != NULL){
free(arr);
arr = NULL;
}
}

5.3 二級指針作形參輸入特性

 

  二級指針作形參輸入特性是指由主調函數分配內存。

 

//打印數組
void print_array(int **arr,int n){
for (int i = 0; i < n;i ++){
printf("%d ",*(arr[i]));
}
printf("\n");
}
//二級指針輸入特性(由主調函數分配內存)
void test(){
int a1 = 10;
int a2 = 20;
int a3 = 30;
int a4 = 40;
int a5 = 50;
int n = 5;
int** arr = (int **)malloc(sizeof(int *) * n);
arr[0] = &a1;
arr[1] = &a2;
arr[2] = &a3;
arr[3] = &a4;
arr[4] = &a5;
print_array(arr,n);
free(arr);
arr = NULL;
}

5.4 強化訓練_畫出內存模型圖 

void mian()
{
//棧區指針數組
char *p1[] = { "aaaaa", "bbbbb", "ccccc" };
//堆區指針數組
char **p3 = (char **)malloc(3 * sizeof(char *)); //char *array[3];
int i = 0;
for (i = 0; i < 3; i++)
{
p3[i] = (char *)malloc(10 * sizeof(char)); //char buf[10]
sprintf(p3[i], "%d%d%d", i, i, i);
}
}

5.4 多級指針 

將堆區數組指針案例改成三級指針案例:

//分配內存
void allocate_memory(char*** p, int n){
if (n < 0){
return;
}
char** temp = (char**)malloc(sizeof(char*)* n);
if (temp == NULL){
return;
}
//分別給每個指針malloc分配內存
for (int i = 0; i < n; i++){
temp[i] = malloc(sizeof(char)* 30);
sprintf(temp[i], "%2d_hello world!", i + 1);
}
*p = temp;
}
//打印數組
void array_print(char** arr, int len){
for (int i = 0; i < len; i++){
printf("%s\n", arr[i]);
}
printf("----------------------\n");
}
//釋放內存
void free_memory(char*** buf, int len){
if (buf == NULL){
return;
}
char** temp = *buf;
for (int i = 0; i < len; i++){
free(temp[i]);
temp[i] = NULL;
}
free(temp);
}
void test(){
int n = 10;
char** p = NULL;
allocate_memory(&p, n);
//打印數組
array_print(p, n);
//釋放內存
free_memory(&p, n);
}

 


 

6. 位運算 

  能夠使用C對變量中的個別位進行操做。您可能對人們想這樣作的緣由感到奇怪。這種能力有時確實是必須的,或者至少是有用的。C提供位的邏輯運算符和移位運算符。在如下例子中,咱們將使用二進制計數法寫出值,以便您能夠了解對位發生的操做。在一個實際程序中,您能夠使用通常的形式的整數變量或常量。例如不適用00011001的形式,而寫爲25或者031或者0x19.在咱們的例子中,咱們將使用8位數字,從左到右,每位的編號是7到0。

6.1位邏輯運算符

  4個位運算符用於整型數據,包括char.將這些位運算符成爲位運算的緣由是它們對每位進行操做,而不影響左右兩側的位。請不要將這些運算符與常規的邏輯運算符(&& ||和!)相混淆,常規的位的邏輯運算符對整個值進行操做。

6.1.1 按位取反~

一元運算符~將每一個1變爲0,將每一個0變爲1,以下面的例子:

~(10011010)

01100101

  假設a是一個unsigned char,已賦值爲2.在二進制中,2是00000010.因而-a的值爲11111101或者253。請注意該運算符不會改變a的值,a仍爲2。

 

unsigned char a = 2;   //00000010
unsigned char b = ~a;  //11111101
printf("ret = %d\n", a); //ret = 2
printf("ret = %d\n", b); //ret = 253

 

6.1.2 位與(AND: &

  二進制運算符&經過對兩個操做數逐位進行比較產生一個新值。對於每一個位,只有兩個操做數的對應位都是1時結果才爲1。

   (10010011) 

 & (00111101) 

 = (00010001)

C也有一個組合的位與-賦值運算符&=。下面兩個將產生相同的結果:

val &= 0377

val = val & 0377

6.1.3 位或(OR: |

二進制運算符|經過對兩個操做數逐位進行比較產生一個新值。對於每一個位,若是其中任意操做數中對應的位爲1,那麼結果位就爲1.

(10010011)

  | (00111101)

  = (10111111)

C也有組合位或-賦值運算符 |=

val |= 0377

val = val | 0377

6.1.4 位異或:

  二進制運算符^對兩個操做數逐位進行比較。對於每一個位,若是操做數中的對應位有一個是1(但不是都是1),那麼結果是1.若是都是0或者都是1,則結果位0.

(10010011)

  ^ (00111101)

  = (10101110)

C也有一個組合的位異或-賦值運算符: ^=

val ^= 0377

val = val ^ 0377

 6.1.5 用法

6.1.5.1 打開位

已知:10011010:

  1. 將位2打開

  flag |  10011010

(10011010)

|(00000100)

=(10011110)

 

  1. 將全部位打開。

  flag | ~flag

(10011010)

|(01100101)

=(11111111)

6.1.5.2 關閉位

  flag & ~flag

(10011010)

&(01100101)

=(00000000)

6.1.5.3 轉置位

 轉置(toggling)一個位表示若是該位打開,則關閉該位;若是該位關閉,則打開。您能夠使用位異或運算符來轉置。其思想是若是b是一個位(1或0),那麼若是b爲1則b^1爲0,若是b爲0,則1^b爲1。不管b的值是0仍是1,0^b爲b.

flag ^ 0xff

(10010011)

^(11111111)

=(01101100)

 6.1.5.4 交換兩個數不須要臨時變量

//a ^ b = temp;
//a ^ temp = b;
//b ^ temp = a
 (10010011)
^(00100110)
=(10110101)

 (10110101)
^(00100110)
  10010011

  int a = 10;
  int b = 30;

6.2 移位運算符 

  如今讓咱們瞭解一下C的移位運算符。移位運算符將位向左或向右移動。一樣,咱們仍將明確地使用二進制形式來講明該機制的工做原理。

6.2.1 左移 <<

  左移運算符<<將其左側操做數的值的每位向左移動,移動的位數由其右側操做數指定。空出來的位用0填充,而且丟棄移出左側操做數末端的位。在下面例子中,每位向左移動兩個位置。

(10001010) << 2

(00101000)

該操做將產生一個新位置,可是不改變其操做數。

<< 1 = 2;

<< 1 = 4;

<< 1 = 8;

<< 2 = 32

左移一位至關於原值*2.

6.2.2 右移 >>

右移運算符>>將其左側的操做數的值每位向右移動,移動的位數由其右側的操做數指定。丟棄移出左側操做數有段的位。對於unsigned類型,使用0填充左端空出的位。對於有符號類型,結果依賴於機器。空出的位可能用0填充,或者使用符號(最左端)位的副本填充。

//有符號值

(10001010) >> 2

(00100010)     //在某些系統上的結果值

(10001010) >> 2

(11100010)     //在另外一些系統上的解僱

//無符號值

(10001010) >> 2

(00100010)    //全部系統上的結果值

6.2.3 用法:移位運算符

  移位運算符可以提供快捷、高效(依賴於硬件)對2的冪的乘法和除法。

number << n

number乘以2的n次冪

number >> n

若是number非負,則用number除以2的n次冪

 


 

6. 多維數組

6.1 一維數組

  • 元素類型角度:數組是相同類型的變量的有序集合

  • 內存角度:連續的一大片內存空間

 

 

 

   在討論多維數組以前,咱們還須要學習不少關於一維數組的知識。首先讓咱們學習一個概念。

6.1.1 數組名

考慮下面這些聲明:

int a;

int b[10];

  咱們把a稱做標量,由於它是個單一的值,這個變量是的類型是一個整數。咱們把b稱做數組,由於它是一些值的集合。下標和數名一塊兒使用,用於標識該集合中某個特定的值。例如,b[0]表示數組b的第1個值,b[4]表示第5個值。每一個值都是一個特定的標量。

  那麼問題是b的類型是什麼?它所表示的又是什麼?一個合乎邏輯的答案是它表示整個數組,但事實並不是如此。在C中,在幾乎全部數組名的表達式中,數組名的值是一個指針常量,也就是數組第一個元素的地址。它的類型取決於數組元素的類型:若是他們是int類型,那麼數組名的類型就是「指向int的常量指針」;若是它們是其餘類型,那麼數組名的類型也就是「指向其餘類型的常量指針」。

請問:指針和數組是等價的嗎?

  答案是否認的。數組名在表達式中使用的時候,編譯器纔會產生一個指針常量。那麼數組在什麼狀況下不能做爲指針常量呢?在如下兩種場景下:

n 當數組名做爲sizeof操做符的操做數的時候,此時sizeof返回的是整個數組的長度,而不是指針數組指針的長度。

n 當數組名做爲&操做符的操做數的時候,此時返回的是一個指向數組的指針,而不是指向某個數組元素的指針常量。

int arr[10];
//arr = NULL; //arr做爲指針常量,不可修改
int *p = arr; //此時arr做爲指針常量來使用
printf("sizeof(arr):%d\n", sizeof(arr)); //此時sizeof結果爲整個數組的長度
printf("&arr type is %s\n", typeid(&arr).name()); //int(*)[10]而不是int*

6.1.2 下標引用 

int arr[] = { 1, 2, 3, 4, 5, 6 };

  首先,咱們說數組在表達式中是一個指向整型的指針,因此此表達式表示arr指針向後移動了3個元素的長度。而後經過間接訪問操做符從這個新地址開始獲取這個位置的值。這個和下標的引用的執行過程徹底相同。因此以下表達式是等同的:*(arr + 3) ,這個表達式是什麼意思呢?

*(arr + 3)
arr[3]

問題1:數組下標能否爲負值?

問題2:請閱讀以下代碼,說出結果:

int arr[] = { 5, 3, 6, 8, 2, 9 };
int *p = arr + 2;
printf("*p = %d\n", *p);
printf("*p = %d\n", p[-1]);

那麼是用下標仍是指針來操做數組呢?對於大部分人而言,下標的可讀性會強一些。 

6.1.3 數組和指針

指針和數組並非相等的。爲了說明這個概念,請考慮下面兩個聲明:

int a[10];

int *b;

  聲明一個數組時,編譯器根據聲明所指定的元素數量爲數組分配內存空間,而後再建立數組名,指向這段空間的起始位置。聲明一個指針變量的時候,編譯器只爲指針自己分配內存空間,並不爲任何整型值分配內存空間,指針並未初始化指向任何現有的內存空間。 

  所以,表達式*a是徹底合法的,可是表達式*b倒是非法的。*b將訪問內存中一個不肯定的位置,將會致使程序終止。另外一方面b++能夠經過編譯,a++卻不行,由於a是一個常量值。

6.1.4 做爲函數參數的數組名

  當一個數組名做爲一個參數傳遞給一個函數的時候發生什麼狀況呢?咱們如今知道數組名其實就是一個指向數組第1個元素的指針,因此很明白此時傳遞給函數的是一份指針的拷貝。因此函數的形參其實是一個指針。可是爲了使程序員新手容易上手一些,編譯器也接受數組形式的函數形參。所以下面兩種函數原型是相等的:

int print_array(int *arr);

int print_array(int arr[]);

  如今咱們清楚了,爲何一維數組中無須寫明它的元素數目了,由於形參只是一個指針,並不須要爲數組參數分配內存。另外一方面,這種方式使得函數沒法知道數組的長度。若是函數須要知道數組的長度,它必須顯式傳遞一個長度參數給函數。咱們能夠使用任何一種聲明,但哪個更準確一些呢?答案是指針。由於實參其實是個指針,而不是數組。一樣sizeof arr值是指針的長度,而不是數組的長度。

6.2 多維數組

若是某個數組的維數不止1個,它就被稱爲多維數組。接下來的案例講解以二維數組舉例。

void test01(){
//二維數組初始化
int arr1[3][3] = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
int arr2[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int arr3[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
//打印二維數組
for (int i = 0; i < 3; i++){
for (int j = 0; j < 3; j ++){
printf("%d ",arr1[i][j]);
}
printf("\n");
}
}

6.2.1 數組名 

一維數組名的值是一個指針常量,它的類型是「指向元素類型的指針」,它指向數組的第1個元素。多維數組也是同理,多維數組的數組名也是指向第一個元素,只不過第一個元素是一個數組。例如:

int arr[3][10]

6.2.2 指向數組的指針(數組指針)

  能夠理解爲這是一個一維數組,包含了3個元素,只是每一個元素剛好是包含了10個元素的數組。arr就表示指向它的第1個元素的指針,因此arr是一個指向了包含了10個整型元素的數組的指針。

  數組指針,它是指針,指向數組的指針。

  數組的類型由元素類型數組大小共同決定:int array[5]  的類型爲  int[5];C語言可經過typedef定義一個數組類型:

  定義數組指針有一下三種方式:

//方式一
void test01(){
//先定義數組類型,再用數組類型定義數組指針
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
//有typedef是定義類型,沒有則是定義變量,下面代碼定義了一個數組類型ArrayType
typedef int(ArrayType)[10];
//int ArrayType[10]; //定義一個數組,數組名爲ArrayType
ArrayType myarr; //等價於 int myarr[10];
ArrayType* pArr = &arr; //定義了一個數組指針pArr,而且指針指向數組arr
for (int i = 0; i < 10;i++){
printf("%d ",(*pArr)[i]);
}
printf("\n");
}
 
//方式二
void test02(){
int arr[10];
//定義數組指針類型
typedef int(*ArrayType)[10];
ArrayType pArr = &arr; //定義了一個數組指針pArr,而且指針指向數組arr
for (int i = 0; i < 10; i++){
(*pArr)[i] = i + 1;
}
for (int i = 0; i < 10; i++){
printf("%d ", (*pArr)[i]);
}
printf("\n");
}

//方式三
void test03(){
int arr[10];
int(*pArr)[10] = &arr;
for (int i = 0; i < 10; i++){
(*pArr)[i] = i + 1;
}
for (int i = 0; i < 10; i++){
printf("%d ", (*pArr)[i]);
}
printf("\n");
}

6.2.3 指針數組(元素爲指針)

 

6.3.1 棧區指針數組

 

//數組作函數函數,退化爲指針
void array_sort(char** arr,int len){
for (int i = 0; i < len; i++){
for (int j = len - 1; j > i; j --){
//比較兩個字符串
if (strcmp(arr[j-1],arr[j]) > 0){
char* temp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = temp;
}
}
}
}
//打印數組
void array_print(char** arr,int len){
for (int i = 0; i < len;i++){
printf("%s\n",arr[i]);
}
printf("----------------------\n");
}
void test(){
//主調函數分配內存
//指針數組
char* p[] = { "bbb", "aaa", "ccc", "eee", "ddd"};
//char** p = { "aaa", "bbb", "ccc", "ddd", "eee" }; //錯誤
int len = sizeof(p) / sizeof(char*);
//打印數組
array_print(p, len);
//對字符串進行排序
array_sort(p, len);
//打印數組
array_print(p, len);
} 

 6.3.2 堆區指針數組

//分配內存
char** allocate_memory(int n){
if (n < 0 ){
return NULL;
}
char** temp = (char**)malloc(sizeof(char*) * n);
if (temp == NULL){
return NULL;
}
//分別給每個指針malloc分配內存
for (int i = 0; i < n; i ++){
temp[i] = malloc(sizeof(char)* 30);
sprintf(temp[i], "%2d_hello world!", i + 1);
}
return temp;
}
//打印數組
void array_print(char** arr,int len){
for (int i = 0; i < len;i++){
printf("%s\n",arr[i]);
}
printf("----------------------\n");
}
//釋放內存
void free_memory(char** buf,int len){
if (buf == NULL){
return;
}
for (int i = 0; i < len; i ++){
free(buf[i]);
buf[i] = NULL;
}
free(buf);
}
void test(){
int n = 10;
char** p = allocate_memory(n);
//打印數組
array_print(p, n);
//釋放內存
free_memory(p, n);
}

6.2.4二維數組三種參數形式

6.2.4.1 二維數組的線性存儲特性

void PrintArray(int* arr, int len){
for (int i = 0; i < len; i++){
printf("%d ", arr[i]);
}
printf("\n");
}
//二維數組的線性存儲
void test(){
int arr[][3] = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
int arr2[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int len = sizeof(arr2) / sizeof(int);
//如何證實二維數組是線性的?
//經過將數組首地址指針轉成Int*類型,那麼步長就變成了4,就能夠遍歷整個數組
int* p = (int*)arr;
for (int i = 0; i < len; i++){
printf("%d ", p[i]);
}
printf("\n");
PrintArray((int*)arr, len);
PrintArray((int*)arr2, len);
}

6.2.4.2 二維數組的3種形式參數 

//二維數組的第一種形式
void PrintArray01(int arr[3][3]){
for (int i = 0; i < 3; i++){
for (int j = 0; j < 3; j++){
printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
}
}
}
//二維數組的第二種形式
void PrintArray02(int arr[][3]){
for (int i = 0; i < 3; i++){
for (int j = 0; j < 3; j++){
printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
}
}
}
//二維數組的第二種形式
void PrintArray03(int(*arr)[3]){
for (int i = 0; i < 3; i++){
for (int j = 0; j < 3; j++){
printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
}
}
}
void test(){
int arr[][3] = { 
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
PrintArray01(arr);
PrintArray02(arr);
PrintArray03(arr);
}

6.3總結

6.3.1 編程提示

  • 源代碼的可讀性幾乎老是比程序的運行時效率更爲重要

  • 只要有可能,函數的指針形參都應該聲明爲const

  • 在多維數組的初始值列表中使用完整的多層花括號提供可讀性

6.3.2 內容總結

  在絕大多數表達式中,數組名的值是指向數組第1個元素的指針。這個規則只有兩個例外,sizeof和對數組名&。

  指針和數組並不相等。當咱們聲明一個數組的時候,同時也分配了內存。可是聲明指針的時候,只分配容納指針自己的空間。

  當數組名做爲函數參數時,實際傳遞給函數的是一個指向數組第1個元素的指針。

  咱們不單能夠建立指向普通變量的指針,也可建立指向數組的指針。


 

7. 結構體

7.1 結構體基礎知識

7.1.1 結構體類型的定義

struct Person{
char name[64];
int age;
};
typedef struct _PERSON{
char name[64];
int age;
}Person;

  注意:定義結構體類型時不要直接給成員賦值,結構體只是一個類型,編譯器尚未爲其分配空間,只有根據其類型定義變量時,才分配空間,有空間後才能賦值。 

7.1.2 結構體變量的定義

struct Person{
char name[64];
int age;
}p1; //定義類型同時定義變量
struct{
char name[64];
int age;
}p2; //定義類型同時定義變量
struct Person p3; //經過類型直接定義 

7.1.3 結構體變量的初始化

struct Person{
char name[64];
int age;
}p1 = {"john",10}; //定義類型同時初始化變量
struct{
char name[64];
int age;
}p2 = {"Obama",30}; //定義類型同時初始化變量
struct Person p3 = {"Edward",33}; //經過類型直接定義

7.1.4 結構體成員的使用

struct Person{
char name[64];
int age;
};
void test(){
//在棧上分配空間
struct Person p1;
strcpy(p1.name, "John");
p1.age = 30;
//若是是普通變量,經過點運算符操做結構體成員
printf("Name:%s Age:%d\n", p1.name, p1.age);
//在堆上分配空間
struct Person* p2 = (struct Person*)malloc(sizeof(struct Person));
strcpy(p2->name, "Obama");
p2->age = 33;
//若是是指針變量,經過->操做結構體成員
printf("Name:%s Age:%d\n", p2->name, p2->age);
}

7.1.5 結構體賦值 

7.1.5.1 賦值基本概念

  相同的兩個結構體變量能夠相互賦值,把一個結構體變量的值拷貝給另外一個結構體,這兩個變量仍是兩個獨立的變量。

struct Person{
char name[64];
int age;
};
void test(){
//在棧上分配空間
struct Person p1 = { "John" , 30};
struct Person p2 = { "Obama", 33 };
printf("Name:%s Age:%d\n", p1.name, p1.age);
printf("Name:%s Age:%d\n", p2.name, p2.age);
//將p2的值賦值給p1
p1 = p2;
printf("Name:%s Age:%d\n", p1.name, p1.age);
printf("Name:%s Age:%d\n", p2.name, p2.age);
}

  

 


 7.1.5.1 深拷貝和淺拷貝

 

//一個老師有N個學生
typedef struct _TEACHER{
char* name;
}Teacher;
void test(){
Teacher t1;
t1.name = malloc(64);
strcpy(t1.name , "John");
Teacher t2;
t2 = t1;
//對手動開闢的內存,須要手動拷貝
t2.name = malloc(64);
strcpy(t2.name, t1.name);
if (t1.name != NULL){
free(t1.name);
t1.name = NULL;
}
if (t2.name != NULL)
  { free(t2.name); t1.name = NULL;   } }

 

7.1.6 結構體數組

struct Person{
char name[64];
int age;
};
void test(){
//在棧上分配空間
struct Person p1[3] = {
{ "John", 30 },
{ "Obama", 33 },
{ "Edward", 25}
};
struct Person p2[3] = { "John", 30, "Obama", 33, "Edward", 25 };
for (int i = 0; i < 3;i ++){
printf("Name:%s Age:%d\n",p1[i].name,p1[i].age);
}
printf("-----------------\n");
for (int i = 0; i < 3; i++){
printf("Name:%s Age:%d\n", p2[i].name, p2[i].age);
}
printf("-----------------\n");

//在堆上分配結構體數組
struct Person* p3 = (struct Person*)malloc(sizeof(struct Person) * 3);
for (int i = 0; i < 3;i++){
sprintf(p3[i].name, "Name_%d", i + 1);
p3[i].age = 20 + i;
}
for (int i = 0; i < 3; i++){
printf("Name:%s Age:%d\n", p3[i].name, p3[i].age);
}
}

7.2 結構體嵌套指針 

7.2.1 結構體嵌套一級指針
struct Person{
char* name;
int age;
};
void allocate_memory(struct Person** person){
if (person == NULL){
return;
}
struct Person* temp = (struct Person*)malloc(sizeof(struct Person));
if (temp == NULL){
return;
}
//給name指針分配內存
temp->name = (char*)malloc(sizeof(char)* 64);
strcpy(temp->name, "John");
temp->age = 100;
*person = temp;
}
void print_person(struct Person* person){
printf("Name:%s Age:%d\n",person->name,person->age);
}
void free_memory(struct Person** person){
if (person == NULL){
return;
}
struct Person* temp = *person;
if (temp->name != NULL){
free(temp->name);
temp->name = NULL;
}
free(temp);
}
void test(){
struct Person* p = NULL;
allocate_memory(&p);
print_person(p);
free_memory(&p);
}

7.2.2 結構體嵌套二級指針

//一個老師有N個學生
typedef struct _TEACHER{
char name[64];
char** students;
}Teacher;
void create_teacher(Teacher** teacher,int n,int m){
if (teacher == NULL){
return;
}
//建立老師數組
Teacher* teachers = (Teacher*)malloc(sizeof(Teacher)* n);
if (teachers == NULL){
return;
}
//給每個老師分配學生
int num = 0;
for (int i = 0; i < n; i ++){
sprintf(teachers[i].name, "老師_%d", i + 1);
teachers[i].students = (char**)malloc(sizeof(char*) * m);
for (int j = 0; j < m;j++){
teachers[i].students[j] = malloc(64);
sprintf(teachers[i].students[j], "學生_%d", num + 1);
num++;
}
}
*teacher = teachers;
}
void print_teacher(Teacher* teacher,int n,int m){
for (int i = 0; i < n; i ++){
printf("%s:\n", teacher[i].name);
for (int j = 0; j < m;j++){
printf("  %s",teacher[i].students[j]);
}
printf("\n");
}
}
void free_memory(Teacher** teacher,int n,int m){
if (teacher == NULL){
return;
}
Teacher* temp = *teacher;
for (int i = 0; i < n; i ++){
for (int j = 0; j < m;j ++){
free(temp[i].students[j]);
temp[i].students[j] = NULL;
}
free(temp[i].students);
temp[i].students = NULL;
}
free(temp);
}
void test(){
Teacher* p = NULL;
create_teacher(&p,2,3);
print_teacher(p, 2, 3);
free_memory(&p,2,3);
}

7.3 結構體成員偏移量

//一旦結構體定義下來,則結構體中的成員內存佈局就定下了
typedef struct Teacher
{
char a;  
int b;      
int c;        
} Teacher;
void test(){
Teacher  t1;
Teacher*p = NULL;
p = &t1;
int offsize1 = (int)&(p->b) - (int)p;  //age 相對於結構體 Teacher的偏移量
int offsize2 = (int)&(((Teacher *)0)->b);//絕對0地址 age的偏移量
int offsize3 = offsetof(Teacher, b);
printf("offsize1:%d \n", offsize1);
printf("offsize2:%d \n", offsize2);
printf("offsize3:%d \n", offsize3);
}

7.4 結構體字節對齊 

  在用sizeof運算符求算某結構體所佔空間時,並非簡單地將結構體中全部元素各自佔的空間相加,這裏涉及到內存字節對齊的問題。

  從理論上講,對於任何變量的訪問均可以從任何地址開始訪問,可是事實上不是如此,實際上訪問特定類型的變量只能在特定的地址訪問,這就須要各個變量在空間上按必定的規則排列, 而不是簡單地順序排列,這就是內存對齊。

7.4.1 內存對齊

7.4.1.1 內存對齊緣由

  咱們知道內存的最小單元是一個字節,當cpu從內存中讀取數據的時候,是一個一個字節讀取,因此內存對咱們應該是入下圖這樣:

 

 

   可是實際上cpu將內存當成多個塊,每次從內存中讀取一個塊,這個塊的大小多是二、四、八、16等,

  那麼下面,咱們來分析下非內存對齊和內存對齊的優缺點在哪?

  內存對齊是操做系統爲了提升訪問內存的策略。操做系統在訪問內存的時候,每次讀取必定長度(這個長度是操做系統默認的對齊數,或者默認對齊數的整數倍)。若是沒有對齊,爲了訪問一個變量可能產生二次訪問。

至此你們應該可以簡單明白,爲何要簡單內存對齊?

提升存取數據的速度。好比有的平臺每次都是從偶地址處讀取數據,對於一個int型的變量,若從偶地址單元處存放,則只需一個讀取週期便可讀取該變量;可是若從奇地址單元處存放,則須要2個讀取週期讀取該變量。

某些平臺只能在特定的地址處訪問特定類型的數據,不然拋出硬件異常給操做系統。

7.4.1.2 如何內存對齊

  • 對於標準數據類型,它的地址只要是它的長度的整數倍。

  • 對於非標準數據類型,好比結構體,要遵循一下對齊原則:

1. 數組成員對齊規則。第一個數組成員應該放在offset爲0的地方,之後每一個數組成員應該放在offset爲min(當前成員的大小,#pargama pack(n))整數倍的地方開始(好比int在32位機器爲4字節,#pargama pack(2),那麼從2的倍數地方開始存儲)。
2. 結構體總的大小,也就是sizeof的結果,必須是min(結構體內部最大成員,#pargama pack(n))的整數倍,不足要補齊。
3. 結構體作爲成員的對齊規則。若是一個結構體B裏嵌套另外一個結構體A,仍是以最大成員類型的大小對齊,可是結構體A的起點爲A內部最大成員的整數倍的地方。(struct B裏存有struct A,A裏有char,int,double等成員,那A應該從8的整數倍開始存儲。),結構體A中的成員的對齊規則仍知足原則一、原則2。

手動設置對齊模數

#pragma pack(show)
顯示當前packing alignment的字節數,以warning message的形式被顯示。

#pragma pack(push)
將當前指定的packing alignment數組進行壓棧操做,這裏的棧是the internal compiler stack,同事設置當前的packing alignment爲n;若是n沒有指定,則將當前的packing alignment數組壓棧。

#pragma pack(pop)
從internal compiler stack中刪除最頂端的reaord; 若是沒有指定n,則當前棧頂record即爲新的packing alignement數值;若是指定了n,則n成爲新的packing alignment值

#pragma pack(n)
指定packing的數值,以字節爲單位,缺省數值是8,合法的數值分別是1,2,4,8,16

7.4.2 內存對齊案例 

#pragma pack(4)
typedef struct _STUDENT{
int a;
char b;
double c;
float d;
}Student;
typedef struct _STUDENT2{
char a;
Student b; 
double c;
}Student2;
void test01(){
//Student
//a從偏移量0位置開始存儲
//b從4位置開始存儲
//c從8位置開始存儲
//d從12位置開存儲
//因此Student內部對齊以後的大小爲20 ,總體對齊,總體爲最大類型的整數倍 也就是8的整數倍 爲24
printf("sizeof Student:%d\n",sizeof(Student));
//Student2
//a從偏移量爲0位置開始 8
//b從偏移量爲Student內部最大成員整數倍開始,也就是8開始 24
//c從8的整數倍地方開始,也就是32開始
//因此結構體Sutdnet2內部對齊以後的大小爲:40 , 因爲結構體中最大成員爲8,必須爲8的整數倍 因此大小爲40
printf("sizeof Student2:%d\n", sizeof(Student2));
}

8. 文件操做 

  文件在今天的計算機系統中做用是很重要的。文件用來存放程序、文檔、數據、表格、圖片和其餘不少種類的信息。做爲一名程序員,您必須編程來建立、寫入和讀取文件。編寫程序從文件讀取信息或者將結果寫入文件是一種常常性的需求。C提供了強大的和文件進行通訊的方法。使用這種方法咱們能夠在程序中打開文件,而後使用專門的I/O函數讀取文件或者寫入文件。

8.1 文件相關概念

8.1.1 文件的概念

  一個文件一般就是磁盤上一段命名的存儲區。可是對於操做系統來講,文件就會更復雜一些。例如,一個大文件能夠存儲在一些分散的區段中,或者還會包含一些操做系統能夠肯定其文件類型的附加數據,可是這些是操做系統,而不是咱們程序員所要關心的事情。咱們應該考慮如何在C程序中處理文件。

8.1.2 流的概念

  流是一個動態的概念,能夠將一個字節形象地比喻成一滴水,字節在設備、文件和程序之間的傳輸就是流,相似於水在管道中的傳輸,能夠看出,流是對輸入輸出源的一種抽象,也是對傳輸信息的一種抽象。

  C語言中,I/O操做能夠簡單地看做是從程序移進或移出字節,這種搬運的過程便稱爲流(stream)。程序只須要關心是否正確地輸出了字節數據,以及是否正確地輸入了要讀取字節數據,特定I/O設備的細節對程序員是隱藏的。

8.1.2.1 文本流

  文本流,也就是咱們常說的以文本模式讀取文件。文本流的有些特性在不一樣的系統中可能不一樣。其中之一就是文本行的最大長度。標準規定至少容許254個字符。另外一個可能不一樣的特性是文本行的結束方式。例如在Windows系統中,文本文件約定以一個回車符和一個換行符結尾。可是在Linux下只使用一個換行符結尾。

  標準C把文本定義爲零個或者多個字符,後面跟一個表示結束的換行符(\n).對於那些文本行的外在表現形式與這個定義不一樣的系統上,庫函數負責外部形式和內部形式之間的翻譯。例如,Windows系統中,在輸出時,文本的換行符被寫成一對回車/換行符。在輸入時,文本中的回車符被丟棄。這種沒必要考慮文本的外部形勢而操縱文本的能力簡化了可移植程序的建立。

8.1.2.1 二進制流

  二進制流中的字節將徹底根據程序編寫它們的形式寫入到文件中,並且徹底根據它們從文件或設備讀取的形式讀入到程序中。它們並未作任何改變。這種類型的流適用於非文本數據,可是若是你不但願I/O函數修改文本文件的行末字符,也能夠把它們用於文本文件。

  c語言在處理這兩種文件的時候並不區分,都當作是字符流,按字節進行處理。

咱們程序中,常常看到的文本方式打開文件和二進制方式打開文件僅僅體如今換行符的處理上。

  好比說,在widows下,文件的換行符是\r\n,而在Linux下換行符則是\n.

當對文件使用文本方式打開的時候,讀寫的windows文件中的換行符\r\n會被替換成\n讀到內存中,當在windows下寫入文件的時候,\n被替換成\r\n再寫入文件。若是使用二進制方式打開文件,則不進行\r\n和\n之間的轉換。 那麼因爲Linux下的換行符就是\n,因此文本文件方式和二進制方式無區別。

8.2 文件的操做

8.2.1 文件流總覽

  標準庫函數是的咱們在C程序中執行與文件相關的I/O任務很是方便。下面是關於文件I/O的通常概況。

程序爲同時處於活動狀態的每一個文件聲明一個指針變量,其類型爲FILE*。這個指針指向這個FILE結構,當它處於活動狀態時由流使用。
流經過fopen函數打開。爲了打開一個流,咱們必須指定須要訪問的文件或設備以及他們的訪問方式(讀、寫、或者讀寫)。Fopen和操做系統驗證文件或者設備是否存在並初始化FILE。
根據須要對文件進行讀寫操做。
最後調用fclose函數關閉流。關閉一個流能夠防止與它相關的文件被再次訪問,保證任何存儲於緩衝區中的數據被正確寫入到文件中,而且釋放FILE結構。

  標準I/O更爲簡單,由於它們並不須要打開或者關閉。 

  I/O函數以三種基本的形式處理數據:單個字符文本行二進制數據。對於每種形式都有一組特定的函數對它們進行處理。

輸入/輸出函數家族

家族名

目的

可用於全部流

只用於stdin和stdout

getchar

字符輸入

fgetc、getc

getchar

putchar

字符輸出

fputc、putc

putchar

gets

文本行輸入

fgets

gets

puts

文本行輸出

fputs

puts

scanf

格式化輸入

fscanf

scanf

printf

格式化輸出

fprintf

printf

8.2.2 文件指針

  咱們知道,文件是由操做系統管理的單元。當咱們想操做一個文件的時候,讓操做系統幫咱們打開文件,操做系統把咱們指定要打開文件的信息保存起來,而且返回給咱們一個指針指向文件的信息。文件指針也能夠理解爲代指打開的文件。這個指針的類型爲FILE類型。該類型定義在stdio.h頭文件中。經過文件指針,咱們就能夠對文件進行各類操做。

  對於每個ANSI C程序,運行時系統必須提供至少三個流-標準輸入(stdin)、標準輸出(stdout)、標準錯誤(stderr),它們都是一個指向FILE結構的指針。標準輸入是缺省狀況下的輸入來源,標準輸出時缺省狀況下的輸出設置。具體缺省值因編譯器而異,一般標準輸入爲鍵盤設備、標準輸出爲終端或者屏幕。

  ANSI C並未規定FILE的成員,不一樣編譯器可能有不一樣的定義。VS下FILE信息以下:

struct _iobuf { 
        char  *_ptr;         //文件輸入的下一個位置
        int   _cnt;          //剩餘多少字符未被讀取
        char  *_base;        //指基礎位置(應該是文件的其始位置)
        int   _flag;         //文件標誌
        int   _file;         //文件的有效性驗證
        int   _charbuf;      //檢查緩衝區情況,若是無緩衝區則不讀取
        int   _bufsiz;       //文件的大小
        char  *_tmpfname;    //臨時文件名
}; 
typedef struct _iobuf FILE;

8.2.3 文件緩衝區 

  • 文件緩衝區

  ANSI C標準採用「緩衝文件系統」處理數據文件 所謂緩衝文件系統是指系統自動地在內存區爲程序中每個正在使用的文件開闢一個文件緩衝區從內存向磁盤輸出數據必須先送到內存中的緩衝區,裝滿緩衝區後才一塊兒送到磁盤去 若是從磁盤向計算機讀入數據,則一次從磁盤文件將一批數據輸入到內存緩衝區(充滿緩衝 ),而後再從緩衝區逐個地將數據送到程序數據區(給程序變量) 。

那麼文件緩衝區有什麼做用呢?

如咱們從磁盤裏取信息,咱們先把讀出的數據放在緩衝區,計算機再直接從緩衝區中取數據,等緩衝區的數據取完後再去磁盤中讀取,這樣就能夠減小磁盤的讀寫次數,再加上計算機對緩衝區的操做大大快於對磁盤的操做,故應用緩衝區可大大提升計算機的運行速度。

 

8.2.4 文件打開關閉

8.2.4.1 文件打開(fopen)

  文件的打開操做表示將給用戶指定的文件在內存分配一個FILE結構區,並將該結構的指針返回給用戶程序,之後用戶程序就可用此FILE指針來實現對指定文件的存取操做了。當使用打開函數時,必須給出文件名、文件操做方式(讀、寫或讀寫)。

FILE * fopen(const char * filename, const char * mode);
功能:打開文件
參數:
filename:須要打開的文件名,根據須要加上路徑
mode:打開文件的權限設置
返回值:
成功:文件指針
失敗:NULL

方式

含義

「r」

打開,只讀,文件必須已經存在。

「w」

只寫,若是文件不存在則建立,若是文件已存在則把文件長度截斷(Truncate)爲0字節。再從新寫,也就是替換掉原來的文件內容文件指針指到頭。

「a」

只能在文件末尾追加數據,若是文件不存在則建立

「rb」

打開一個二進制文件,只讀

「wb」

打開一個二進制文件,只寫

「ab"

打開一個二進制文件,追加

「r+」

容許讀和寫,文件必須已存在

「w+」

容許讀和寫,若是文件不存在則建立,若是文件已存在則把文件長度截斷爲0字節再從新寫 。

「a+」

容許讀和追加數據,若是文件不存在則建立

「rb+」

以讀/寫方式打開一個二進制文件

「wb+」

以讀/寫方式創建一個新的二進制文件

「ab+」

以讀/寫方式打開一個二進制文件進行追加

 示例代碼:

void test(){
FILE *fp = NULL;
// "\\"這樣的路徑形式,只能在windows使用
// "/"這樣的路徑形式,windows和linux平臺下均可用,建議使用這種
// 路徑能夠是相對路徑,也但是絕對路徑
fp = fopen("../test", "w");
//fp = fopen("..\\test", "w");
if (fp == NULL) //返回空,說明打開失敗
{
//perror()是標準出錯打印函數,能打印調用庫函數出錯緣由
perror("open");
return -1;
}
}

應該檢查fopen的返回值!如何函數失敗,它會返回一個NULL值。若是程序不檢查錯誤,這個NULL指針就會傳給後續的I/O函數。它們將對這個指針執行間接訪問,並將失敗.

 8.2.4.2 文件關閉(fclose)

  文件操做完成後,若是程序沒有結束,必需要用fclose()函數進行關閉,這是由於對打開的文件進行寫入時,若文件緩衝區的空間未被寫入的內容填滿,這些內容不會寫到打開的文件中。只有對打開的文件進行關閉操做時,停留在文件緩衝區的內容才能寫到該文件中去,從而使文件完整。再者一旦關閉了文件,該文件對應的FILE結構將被釋放,從而使關閉的文件獲得保護,由於這時對該文件的存取操做將不會進行。文件的關閉也意味着釋放了該文件的緩衝區。

int fclose(FILE * stream);
功能:關閉先前fopen()打開的文件。此動做讓緩衝區的數據寫入文件中,並釋放系統所提供的文件資源。
參數:
stream:文件指針
返回值:
成功:0
失敗:-1

  它表示該函數將關閉FILE指針對應的文件,並返回一個整數值。若成功地關閉了文件,則返回一個0值,不然返回一個非0值. 

8.2.4 文件讀寫函數回顧

按照字符讀寫文件:fgetc(), fputc()

按照行讀寫文件:fputs(), fgets()

按照塊讀寫文件:fread(), fwirte()

按照格式化讀寫文件:fprintf(), fscanf()

按照隨機位置讀寫文件:fseek(), ftell(), rewind()

 

 


 8.2.4.1 字符讀寫函數回顧

int fputc(int ch, FILE * stream);
功能:將ch轉換爲unsigned char後寫入stream指定的文件中
參數:
ch:須要寫入文件的字符
stream:文件指針
返回值:
成功:成功寫入文件的字符
失敗:返回-1
int fgetc(FILE * stream);
功能:從stream指定的文件中讀取一個字符
參數:
stream:文件指針
返回值:
成功:返回讀取到的字符
失敗:-1
int feof(FILE * stream);
功能:檢測是否讀取到了文件結尾
參數:
stream:文件指針
返回值:
非0值:已經到文件結尾
0:沒有到文件結尾
void test(){
//寫文件
FILE* fp_write= NULL;
//寫方式打開文件
fp_write = fopen("./mydata.txt", "w+");
if (fp_write == NULL){
return;
}
char buf[] = "this is a test for pfutc!";
for (int i = 0; i < strlen(buf);i++){
fputc(buf[i], fp_write);
}
fclose(fp_write);
//讀文件
FILE* fp_read = NULL;
fp_read = fopen("./mydata.txt", "r");
if (fp_read == NULL){
return;
}
#if 0
//判斷文件結尾 注意:多輸出一個空格
while (!feof(fp_read)){
printf("%c",fgetc(fp_read));
}
#else
char ch;
while ((ch = fgetc(fp_read)) != EOF){
printf("%c", ch);
}
#endif
}

  將把流指針fp指向的文件中的一個字符讀出,並賦給ch,當執行fgetc()函數時,若當時文件指針指到文件尾,即遇到文件結束標誌EOF(其對應值爲-1),該函數返回一個 -1 給ch,在程序中經常使用檢查該函數返回值是否爲 -1 來判斷是否已讀到文件尾,從而決定是否繼續。

8.2.4.2 行讀寫函數回顧
int fputs(const char * str, FILE * stream);
功能:將str所指定的字符串寫入到stream指定的文件中, 字符串結束符 '\0'  不寫入文件。
參數:
str:字符串
stream:文件指針
返回值:
成功:0
失敗:-1
char * fgets(char * str, int size, FILE * stream);
功能:從stream指定的文件內讀入字符,保存到str所指定的內存空間,直到出現換行字符、讀到文件結尾或是已讀了size - 1個字符爲止,最後會自動加上字符 '\0' 做爲字符串結束。
參數:
str:字符串
size:指定最大讀取字符串的長度(size - 1)
stream:文件指針
返回值:
成功:成功讀取的字符串
讀到文件尾或出錯: NULL

 

void test(){
//寫文件
FILE* fp_write= NULL;
//寫方式打開文件
fp_write = fopen("./mydata.txt", "w+");
if (fp_write == NULL){
perror("fopen:");
return;
}
char* buf[] = {
"01 this is a test for pfutc!\n",
"02 this is a test for pfutc!\n",
"03 this is a test for pfutc!\n",
"04 this is a test for pfutc!\n",
};
for (int i = 0; i < 4; i ++){
fputs(buf[i], fp_write);
}
fclose(fp_write);
//讀文件
FILE* fp_read = NULL;
fp_read = fopen("./mydata.txt", "r");
if (fp_read == NULL){
perror("fopen:");
return;
}
//判斷文件結尾
while (!feof(fp_read)){
char temp[1024] = { 0 };
fgets(temp, 1024, fp_read);
printf("%s",temp);
}
fclose(fp_read);
}

8.2.4.3 塊讀寫函數回顧

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以數據塊的方式給文件寫入內容
參數:
ptr:準備寫入文件數據的地址
size: size_t 爲 unsigned int類型,此參數指定寫入文件內容的塊數據大小
nmemb:寫入文件的塊數,寫入文件數據總大小爲:size * nmemb
stream:已經打開的文件指針
返回值:
成功:實際成功寫入文件數據的塊數,此值和nmemb相等
失敗:0

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以數據塊的方式從文件中讀取內容
參數:
ptr:存放讀取出來數據的內存空間
size: size_t 爲 unsigned int類型,此參數指定讀取文件內容的塊數據大小
nmemb:讀取文件的塊數,讀取文件數據總大小爲:size * nmemb
stream:已經打開的文件指針
返回值:
成功:實際成功讀取到內容的塊數,若是此值比nmemb小,但大於0,說明讀到文件的結尾。
失敗:0
typedef struct _TEACHER{
char name[64];
int age;
}Teacher;
void test(){
//寫文件
FILE* fp_write= NULL;
//寫方式打開文件
fp_write = fopen("./mydata.txt", "wb");
if (fp_write == NULL){
perror("fopen:");
return;
}
Teacher teachers[4] = {
{ "Obama", 33 },
{ "John", 28 },
{ "Edward", 45},
{ "Smith", 35}
}
for (int i = 0; i < 4; i ++){
fwrite(&teachers[i],sizeof(Teacher),1, fp_write);
}
//關閉文件
fclose(fp_write);

//讀文件
FILE* fp_read = NULL;
fp_read = fopen("./mydata.txt", "rb");
if (fp_read == NULL){
perror("fopen:");
return;
}
Teacher temps[4];
fread(&temps, sizeof(Teacher), 4, fp_read);
for (int i = 0; i < 4;i++){
printf("Name:%s Age:%d\n",temps[i].name,temps[i].age);
}
fclose(fp_read);
}

8.2.4.4 格式化讀寫函數回顧

int fprintf(FILE * stream, const char * format, ...);
功能:根據參數format字符串來轉換並格式化數據,而後將結果輸出到stream指定的文件中,指定出現字符串結束符 '\0'  爲止。
參數:
stream:已經打開的文件
format:字符串格式,用法和printf()同樣
返回值:
成功:實際寫入文件的字符個數
失敗:-1
int fscanf(FILE * stream, const char * format, ...);
功能:從stream指定的文件讀取字符串,並根據參數format字符串來轉換並格式化數據。
參數:
stream:已經打開的文件
format:字符串格式,用法和scanf()同樣
返回值:
成功:實際從文件中讀取的字符個數
失敗: - 1

注意fscanf遇到空格和換行時結束。 

 

void test(){
//寫文件
FILE* fp_write= NULL;
//寫方式打開文件
fp_write = fopen("./mydata.txt", "w");
if (fp_write == NULL){
perror("fopen:");
return;
}
fprintf(fp_write,"hello world:%d!",10);
//關閉文件
fclose(fp_write);
//讀文件
FILE* fp_read = NULL;
fp_read = fopen("./mydata.txt", "rb");
if (fp_read == NULL){
perror("fopen:");
return;
}
char temps[1024] = { 0 };
while (!feof(fp_read)){
fscanf(fp_read, "%s", temps);
printf("%s", temps);
}
fclose(fp_read);
}

8.2.5.5 隨機讀寫函數回顧

 

int fseek(FILE *stream, long offset, int whence);
功能:移動文件流(文件光標)的讀寫位置。
參數:
stream:已經打開的文件指針
offset:根據whence來移動的位移數(偏移量),能夠是正數,也能夠負數,若是正數,則相對於whence往右移動,若是是負數,則相對於whence往左移動。若是向前移動的字節數超過了文件開頭則出錯返回,若是向後移動的字節數超過了 文件末尾,再次寫入時將增大文件尺寸。
whence:其取值以下:
SEEK_SET:從文件開頭移動offset個字節
SEEK_CUR:從當前位置移動offset個字節
SEEK_END:從文件末尾移動offset個字節
返回值:
成功:0
失敗:-1
long ftell(FILE *stream);
功能:獲取文件流(文件光標)的讀寫位置。
參數:
stream:已經打開的文件指針
返回值:
成功:當前文件流(文件光標)的讀寫位置
失敗:-1
void rewind(FILE *stream);
功能:把文件流(文件光標)的讀寫位置移動到文件開頭。
參數:
stream:已經打開的文件指針
返回值:
無返回值
typedef struct _TEACHER{
char name[64];
int age;
}Teacher;
void test()
//寫文件
FILE* fp_write = NULL;
//寫方式打開文件
fp_write = fopen("./mydata.txt", "wb");
if (fp_write == NULL){
perror("fopen:");
return;
}
Teacher teachers[4] = {
{ "Obama", 33 },
{ "John", 28 },
{ "Edward", 45 },
{ "Smith", 35 }
};
for (int i = 0; i < 4; i++){
fwrite(&teachers[i], sizeof(Teacher), 1, fp_write);
}
//關閉文件
fclose(fp_write);
//讀文件
FILE* fp_read = NULL;
fp_read = fopen("./mydata.txt", "rb");
if (fp_read == NULL){
perror("fopen:");
return;
}
Teacher temp;
//讀取第三個數組
fseek(fp_read , sizeof(Teacher) * 2 , SEEK_SET);
fread(&temp, sizeof(Teacher), 1, fp_read);
printf("Name:%s Age:%d\n",temp.name,temp.age);
memset(&temp,0,sizeof(Teacher));
fseek(fp_read, -(int)sizeof(Teacher), SEEK_END);
fread(&temp, sizeof(Teacher), 1, fp_read);
printf("Name:%s Age:%d\n", temp.name, temp.age);
rewind(fp_read);
fread(&temp, sizeof(Teacher), 1, fp_read);
printf("Name:%s Age:%d\n", temp.name, temp.age);
fclose(fp_read);
}

8.4 文件讀寫案例

8.4.1 讀寫配置文件

struct info{
char key[64];
char val[128];
};
struct config{
FILE *fp; //保存文件指針
struct info *list; //保存配置信息
int lines; //配置信息條數
};
//加載配置文件
int load_file(char *path, struct config **myconfig){
if (NULL == path){
return -1;
}
//以讀寫的方式打開文件
FILE *fp = fopen(path, "r+");
if (NULL ==fp){
printf("文件打開失敗!\n");
return -2;
}
//配置文件信息分配內存
struct config *conf = (struct config *)malloc(sizeof(struct config));
conf->fp = fp;
conf->list = NULL;
//指針的間接賦值
*myconfig = conf;
return 0;
}
//統計文件行數
int count_file(struct config *config){
if (NULL == config){
return -1;
}
char buf[1024] = { 0 };
int lines = 0;
while (fgets(buf, 1024, config->fp)){
//若是是註釋則不統計
if (buf[0] == '#'){ continue; }
lines++;
}
//將文件指針重置到開始位置
fseek(config->fp,0, SEEK_SET);
return lines;
}
//解析配置文件
int parse_file(struct config *config){
if (NULL == config){
return -1;
}
//得到配置文件行數
config->lines = count_file(config);
//給每一行配置信息分配內存
config->list = (struct info *)malloc(sizeof(struct info) * config->lines);
int index = 0;
char buf[1024] = { 0 };
while (fgets(buf, 1024, config->fp)){
//去除每一行最後的\n字符
buf[strlen(buf) - 1] = '\0';
//若是是註釋則不顯示
if (buf[0] == '#'){
continue;
}
memset(config->list[index].key, 0, 64);
memset(config->list[index].val, 0, 128);
char *delimit = strchr(buf, ':');
strncpy(config->list[index].key, buf, delimit - buf);
strncpy(config->list[index].val, delimit + 1, strlen(delimit + 1));
memset(buf, 0 , 1024);
index++;
}
return 0;
}
const char *get_file(struct config *config, char *key){
if (NULL == config){
return NULL;
}
if (NULL == key){
return NULL;
}
for (int i = 0; i < config->lines;i ++){
if (strcmp(config->list[i].key,key) == 0){
return config->list[i].val;
}
}
return NULL;
}
void destroy_file(struct config *config){
if (NULL == config){
return;
}
//關閉文件指針
fclose(config->fp);
config->fp = NULL;
//釋放配置信息
free(config->list);
config->list = NULL;
free(config);
}
void test(){
char *path = "./my.ini";
struct config *conf = NULL;
load_file(path, &conf);
parse_file(conf);
printf("%s\n", get_file(conf, "username"));
printf("%s\n", get_file(conf, "password"));
printf("%s\n", get_file(conf, "server_ip"));
printf("%s\n", get_file(conf, "server_port"));
printf("%s\n", get_file(conf, "aaaa"));
destroy_file(conf);
}

9. 鏈表

9.1 鏈表基本概念

9.1.1 什麼是鏈表

  • 鏈表是一種經常使用的數據結構,它經過指針將一些列數據結點,鏈接成一個數據鏈。相對於數組,鏈表具備更好的動態性(非順序存儲)。

  • 數據域用來存儲數據,指針域用於創建與下一個結點的聯繫。

  • 創建鏈表時無需預先知道數據總量的,能夠隨機的分配空間,能夠高效的在鏈表中的任意位置實時插入或刪除數據。

  • 鏈表的開銷,主要是訪問順序性和組織鏈的空間損失。

數組和鏈表的區別:

數組:一次性分配一塊連續的存儲區域。

優勢:隨機訪問元素效率高

缺點:1) 須要分配一塊連續的存儲區域(很大區域,有可能分配失敗)

      2) 刪除和插入某個元素效率低

鏈表:無需一次性分配一塊連續的存儲區域,只需分配n塊節點存儲區域,經過指針創建關係。

優勢:1) 不須要一塊連續的存儲區域

  2) 刪除和插入某個元素效率高

缺點:隨機訪問元素效率低

9.1.2 有關結構體的自身引用

問題1:請問結構體能夠嵌套本類型的結構體變量嗎?

問題2:請問結構體能夠嵌套本類型的結構體指針變量嗎?

typedef struct _STUDENT{
char name[64];
int age;
}Student;
typedef struct _TEACHER{
char name[64];
Student stu; //結構體能夠嵌套其餘類型的結構體
//Teacher stu;
//struct _TEACHER teacher; //此時Teacher類型的成員尚未肯定,編譯器沒法分配內存
struct _TEACHER* teacher; //不論什麼類型的指針,都只佔4個字節,編譯器可肯定內存分配
}Teacher;
  • 結構體能夠嵌套另一個結構體的任何類型變量;

  • 結構體嵌套本結構體普通變量(不能夠)。本結構體的類型大小沒法肯定,類型本質:固定大小內存塊別名;

  • 結構體嵌套本結構體指針變量(能夠), 指針變量的空間能肯定,32位, 4字節, 64位, 8字節;

9.1.3 鏈表節點

  你們思考一下,咱們說鏈表是由一系列的節點組成,那麼如何表示一個包含了數據域和指針域的節點呢?

  鏈表的節點類型其實是結構體變量,此結構體包含數據域和指針域:

  • 數據域用來存儲數據;

  • 指針域用於創建與下一個結點的聯繫,當此節點爲尾節點時,指針域的值爲NULL;

typedef struct Node
{
//數據域
int id;
char name[50];
//指針域
struct Node *next;       
}Node;

9.1.4 鏈表的分類

  鏈表分爲:靜態鏈表和動態鏈表

  靜態鏈表和動態鏈表是線性錶鏈式存儲結構的兩種不一樣的表示方式:

  • 全部結點都是在程序中定義的,不是臨時開闢的,也不能用完後釋放,這種鏈表稱爲「靜態鏈表」。

  • 所謂動態鏈表,是指在程序執行過程當中從無到有地創建起一個鏈表,即一個一個地開闢結點和輸入各結點數據,並創建起先後相鏈的關係。

9.1.4.1 靜態鏈表

typedef struct Stu
{
int id;	//數據域
char name[100];
struct Stu *next; //指針域
}Stu;
void test()
{
//初始化三個結構體變量
Stu s1 = { 1, "yuri", NULL };
Stu s2 = { 2, "lily", NULL };
Stu s3 = { 3, "lilei", NULL };
s1.next = &s2; //s1的next指針指向s2
s2.next = &s3;
s3.next = NULL; //尾結點
Stu *p = &s1;
while (p != NULL)
{
printf("id = %d, name = %s\n", p->id, p->name);
//結點日後移動一位
p = p->next; 
}
}

9.1.4.2 動態鏈表 

typedef struct Stu{
int id;	//數據域
char name[100];
struct Stu *next; //指針域
}Stu;
void test(){
//動態分配3個節點
Stu *s1 = (Stu *)malloc(sizeof(Stu));
s1->id = 1;
strcpy(s1->name, "yuri");
Stu *s2 = (Stu *)malloc(sizeof(Stu));
s2->id = 2;
strcpy(s2->name, "lily");
Stu *s3 = (Stu *)malloc(sizeof(Stu));
s3->id = 3;
strcpy(s3->name, "lilei");
//創建節點的關係
s1->next = s2; //s1的next指針指向s2
s2->next = s3;
s3->next = NULL; //尾結點
//遍歷節點
Stu *p = s1;
while (p != NULL)
{
printf("id = %d, name = %s\n", p->id, p->name);
//結點日後移動一位
p = p->next; 
}
//釋放節點空間
p = s1;
Stu *tmp = NULL;
while (p != NULL)
{
tmp = p;
p = p->next;
free(tmp);
tmp = NULL;
}
}

9.1.4.3 帶頭和不帶頭鏈表 

  • 帶頭鏈表:固定一個節點做爲頭結點(數據域不保存有效數據),起一個標誌位的做用,之後無論鏈表節點若是改變,此頭結點固定不變。

  • 不帶頭鏈表:頭結點不固定,根據實際須要變換頭結點(如在原來頭結點前插入新節點,而後,新節點從新做爲鏈表的頭結點)。

9.1.4.4 單向鏈表、雙向鏈表、循環鏈表

  單向鏈表:

  雙向鏈表:

  循環鏈表:

 

9.2 鏈表基本操做

9.2.1 建立鏈表

使用結構體定義節點類型:

typedef struct _LINKNODE
{
int id; //數據域
struct _LINKNODE* next; //指針域
}link_node;

  編寫函數:link_node* init_linklist() 

  創建帶有頭結點的單向鏈表,循環建立結點,結點數據域中的數值從鍵盤輸入,以 -1 做爲輸入結束標誌,鏈表的頭結點地址由函數值返回.

typedef struct _LINKNODE{
int data;
struct _LINKNODE* next;
}link_node;
link_node* init_linklist(){
//建立頭結點指針
link_node* head = NULL;
//給頭結點分配內存
head = (link_node*)malloc(sizeof(link_node));
if (head == NULL){
return NULL;
}
head->data = -1;
head->next = NULL;
//保存當前節點
link_node* p_current = head;
int data = -1;
//循環向鏈表中插入節點
while (1){
printf("please input data:\n");
scanf("%d",&data);
//若是輸入-1,則退出循環
if (data == -1){
break;
}
//給新節點分配內存
link_node* newnode = (link_node*)malloc(sizeof(link_node));
if (newnode == NULL){
break;
}
//給節點賦值
newnode->data = data;
newnode->next = NULL;
//新節點入鏈表,也就是將節點插入到最後一個節點的下一個位置
p_current->next = newnode;
//更新輔助指針p_current
p_current = newnode;
}
return head;
}

9.2.2 遍歷鏈表 

  編寫函數:void foreach_linklist(link_node* head)

  順序輸出單向鏈表各項結點數據域中的內容:

//遍歷鏈表
void foreach_linklist(link_node* head){
if (head == NULL){
return;
}
//賦值指針變量
link_node* p_current = head->next;
while (p_current != NULL){
printf("%d ",p_current->data);
p_current = p_current->next;
}
printf("\n");
}

9.2.3 插入節點

  編寫函數: void insert_linklist(link_node* head,int val,int data).

  在指定值後面插入數據data,若是值val不存在,則在尾部插入。

//在值val前插入節點
void insert_linklist(link_node* head, int val, int data){
if (head == NULL){
return;
}
//兩個輔助指針
link_node* p_prev = head;
link_node* p_current = p_prev->next;
while (p_current != NULL){
if (p_current->data == val){
break;
}
p_prev = p_current;
p_current = p_prev->next;
}
//若是p_current爲NULL,說明不存在值爲val的節點
if (p_current == NULL){
printf("不存在值爲%d的節點!\n",val);
return;
}
//建立新的節點
link_node* newnode = (link_node*)malloc(sizeof(link_node));
newnode->data = data;
newnode->next = NULL;
//新節點入鏈表
newnode->next = p_current;
p_prev->next = newnode;
}

9.2.4 刪除節點

  編寫函數: void remove_linklist(link_node* head,int val)

  刪除第一個值爲val的結點.

//刪除值爲val的節點
void remove_linklist(link_node* head,int val){
if (head == NULL){
return;
}
//輔助指針
link_node* p_prev = head;
link_node* p_current = p_prev->next;
//查找值爲val的節點
while (p_current != NULL){
if (p_current->data == val){
break;
}
p_prev = p_current;
p_current = p_prev->next;
}
//若是p_current爲NULL,表示沒有找到
if (p_current == NULL){
return;
}
//刪除當前節點: 從新創建待刪除節點(p_current)的前驅後繼節點關係
p_prev->next = p_current->next;
//釋放待刪除節點的內存
free(p_current);
}

9.2.5 銷燬鏈表 

  編寫函數: void destroy_linklist(link_node* head)

  銷燬鏈表,釋放全部節點的空間.

//銷燬鏈表
void destroy_linklist(link_node* head){
if (head == NULL){
return;
}
//賦值指針
link_node* p_current = head;
while (p_current != NULL){
//緩存當前節點下一個節點
link_node* p_next = p_current->next;
free(p_current);
p_current = p_next;
}
}

10. 函數指針和遞歸函數 

10.1 函數指針

10.1.1 函數類型

  經過什麼來區分兩個不一樣的函數?

  一個函數在編譯時被分配一個入口地址,這個地址就稱爲函數的指針,函數名錶明函數的入口地址。

  函數三要素: 名稱、參數、返回值。C語言中的函數有本身特定的類型。

  c語言中經過typedef爲函數類型重命名:

typedef int f(int, int);    // f 爲函數類型
typedef void p(int);    // p 爲函數類型

  這一點和數組同樣,所以咱們能夠用一個指針變量來存放這個入口地址,而後經過該指針變量調用函數。 

  注意:經過函數類型定義的變量是不可以直接執行,由於沒有函數體。只能經過類型定義一個函數指針指向某一個具體函數,才能調用。

typedef int(p)(int, int);
void my_func(int a,int b){
printf("%d %d\n",a,b);
}
void test(){
p p1;
//p1(10,20); //錯誤,不能直接調用,只描述了函數類型,可是並無定義函數體,沒有函數體沒法調用
p* p2 = my_func;
p2(10,20); //正確,指向有函數體的函數入口地址
}

10.1.2 函數指針(指向函數的指針)

  • 函數指針定義方式(先定義函數類型,根據類型定義指針變量);

  • 先定義函數指針類型,根據類型定義指針變量;

  • 直接定義函數指針變量;

int my_func(int a,int b){
printf("ret:%d\n", a + b);
return 0;
}
//1. 先定義函數類型,經過類型定義指針
void test01(){
typedef int(FUNC_TYPE)(int, int);
FUNC_TYPE* f = my_func;
//如何調用?
(*f)(10, 20);
f(10, 20);
}
//2. 定義函數指針類型
void test02(){
typedef int(*FUNC_POINTER)(int, int);
FUNC_POINTER f = my_func;
//如何調用?
(*f)(10, 20);
f(10, 20);
}
//3. 直接定義函數指針變量
void test03(){
int(*f)(int, int) = my_func;
//如何調用?
(*f)(10, 20);
f(10, 20);
}

10.1.3 函數指針數組

  函數指針數組,每一個元素都是函數指針。

void func01(int a){
printf("func01:%d\n",a);
}
void func02(int a){
printf("func02:%d\n", a);
}
void func03(int a){
printf("func03:%d\n", a);
}
void test(){
#if 0
//定義函數指針
void(*func_array[])(int) = { func01, func02, func03 };
#else
void(*func_array[3])(int);
func_array[0] = func01;
func_array[1] = func02;
func_array[2] = func03;
#endif
for (int i = 0; i < 3; i ++){
func_array[i](10 + i);
(*func_array[i])(10 + i);
}
}

10.1.4 函數指針作函數參數(回調函數)

  函數參數除了是普通變量,還能夠是函數指針變量。

//形參爲普通變量
void fun( int x ){}

//形參爲函數指針變量
void fun( int(*p)(int a) ){}
//加法計算器
int plus(int a,int b){
return a + b;
}
//減法計算器
int minus(int a,int b){
return a - b;
}
//計算器
#if 0
int caculator(int a,int b,int(*func)(int,int)){
return func(a, b);
}
#else
typedef int(*FUNC_POINTER)(int, int);
int caculator(int a, int b, FUNC_POINTER func){
return func(a, b);
}
#endif

  注意:函數指針和指針函數的區別:函數指針變量常見的用途之一是把指針做爲參數傳遞到其餘函數,指向函數的指針也能夠做爲參數,以實現函數地址的傳遞。

  • 函數指針是指向函數的指針;

  • 指針函數是返回類型爲指針的函數;


11. 預處理

11.1 預處理的基本概念

  C語言對源程序處理的四個步驟:預處理、編譯、彙編、連接。

  預處理是在程序源代碼被編譯以前,由預處理器(Preprocessor)對程序源代碼進行的處理。這個過程並不對程序的源代碼語法進行解析,但它會把源代碼分割或處理成爲特定的符號爲下一步的編譯作準備工做。

11.2 文件包含指令(#include)

11.2.1 文件包含處理

  「文件包含處理」是指一個源文件能夠將另一個文件的所有內容包含進來。C語言提供了#include命令用來實現「文件包含」的操做。

11.2.2 #incude<>和#include""區別

  •  "" 表示系統先file1.c所在的當前目錄找file1.h,若是找不到,再按系統指定的目錄檢索。

  • < > 表示系統直接按系統指定的目錄檢索。

注意:

  • 1. #include <>經常使用於包含庫函數的頭文件;

  • 2. #include ""經常使用於包含自定義的頭文件;

  • 3. 理論上#include能夠包含任意格式的文件(.c .h等) ,但通常用於頭文件的包含;

11.3 宏定義

11.3.1 無參數的宏定義(宏常量)

  若是在程序中大量使用到了100這個值,那麼爲了方便管理,咱們能夠將其定義爲:

  const int num = 100; 可是若是咱們使用num定義一個數組,在不支持c99標準的編譯器上是不支持的,由於num不是一個編譯器常量,若是想獲得了一個編譯器常量,那麼能夠使用:

  #define num 100

  在編譯預處理時,將程序中在該語句之後出現的全部的num都用100代替。這種方法使用戶能以一個簡單的名字代替一個長的字符串,在預編譯時將宏名替換成字符串的過程稱爲「宏展開」。宏定義,只在宏定義的文件中起做用。

#define PI 3.1415
void test(){
double r = 10.0;
double s = PI * r * r;
printf("s = %lf\n", s);
}

  說明: 

  • 1) 宏名通常用大寫,以便於與變量區別;

  • 2) 宏定義能夠是常數、表達式等;

  • 3) 宏定義不做語法檢查,只有在編譯被宏展開後的源程序纔會報錯;

  • 4) 宏定義不是C語言,不在行末加分號;

  • 5) 宏名有效範圍爲從定義到本源文件結束;

  • 6) 能夠用#undef命令終止宏定義的做用域;

  • 7) 在宏定義中,能夠引用已定義的宏名;

11.3.2 帶參數的宏定義(宏函數)

  在項目中,常常把一些短小而又頻繁使用的函數寫成宏函數,這是因爲宏函數沒有普通函數參數壓棧、跳轉、返回等的開銷,能夠調高程序的效率。

  宏經過使用參數,能夠建立外形和做用都與函數相似地類函數宏(function-like macro). 宏的參數也用圓括號括起來。

#define SUM(x,y) ((x)+(y))
void test(){
//僅僅只是作文本替換 下例替換爲 int ret = ((10)+(20));
//不進行計算
int ret = SUM(10, 20);
printf("ret:%d\n",ret);
}

注意:

  • 1) 宏的名字中不能有空格,可是在替換的字符串中能夠有空格。ANSI C容許在參數列表中使用空格;

  • 2) 用括號括住每個參數,並括住宏的總體定義。

  • 3) 用大寫字母表示宏的函數名。

  • 4) 若是打算宏代替函數來加快程序運行速度。假如在程序中只使用一次宏對程序的運行時間沒有太大提升。

 

11.4 條件編譯

11.4.1 基本概念

  通常狀況下,源程序中全部的行都參加編譯。但有時但願對部分源程序行只在知足必定條件時才編譯,即對這部分源程序行指定編譯條件。

11.4.2 條件編譯

  n 防止頭文件被重複包含引用;

#ifndef _SOMEFILE_H
#define _SOMEFILE_H
//須要聲明的變量、函數
//宏定義
//結構體
 
#endif

11.5 一些特殊的預約宏 

  C編譯器,提供了幾個特殊形式的預約義宏,在實際編程中能夠直接使用,很方便。

//    __FILE__    宏所在文件的源文件名
//    __LINE__    宏所在行的行號
//    __DATE__    代碼編譯的日期
//    __TIME__    代碼編譯的時間
void test()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
}

 


 

12. 動態庫的封裝和使用 

12.1 庫的基本概念

  庫是已經寫好的、成熟的、可複用的代碼。每一個程序都須要依賴不少底層庫,不可能每一個人的代碼從零開始編寫代碼,所以庫的存在具備很是重要的意義。

  在咱們的開發的應用中常常有一些公共代碼是須要反覆使用的,就把這些代碼編譯爲庫文件。

  庫能夠簡單當作一組目標文件的集合,將這些目標文件通過壓縮打包以後造成的一個文件。像在Windows這樣的平臺上,最經常使用的c語言庫是由集成按開發環境所附帶的運行庫,這些庫通常由編譯廠商提供。

12.2 windows下靜態庫建立和使用

12.2.1 靜態庫的建立

 

1. 建立一個新項目,在已安裝的模板中選擇「常規」,在右邊的類型下選擇「空項目」,在名稱和解決方案名稱中輸入staticlib。點擊肯定。

 

 

2.在解決方案資源管理器的頭文件中添加,mylib.h文件,在源文件添加mylib.c文件(即實現文件)。

 

 

3.在mylib.h文件中添加以下代碼:

 

#ifndef TEST_H
#define TEST_H
int myadd(int a,int b);
#endif

4.mylib.c文件中添加以下代碼:

 

#include"test.h"
int myadd(int a, int b)
{
return a + b;
}

 

5. 配置項目屬性。由於這是一個靜態連接庫,因此應在項目屬性的「配置屬性」下選擇「常規」,在其下的配置類型中選擇「靜態庫(.lib)。

6.編譯生成新的解決方案,在Debug文件夾下會獲得mylib.lib (對象文件庫),將該.lib文件和相應頭文件給用戶,用戶就能夠使用該庫裏的函數了。

12.2.2 靜態庫的使用

方法一:配置項目屬性

A、添加工程的頭文件目錄:工程---屬性---配置屬性---c/c++---常規---附加包含目錄:加上頭文件存放目錄。

B、添加文件引用的lib靜態庫路徑:工程---屬性---配置屬性---連接器---常規---附加庫目錄:加上lib文件存放目錄。

C  而後添加工程引用的lib文件名:工程---屬性---配置屬性---連接器---輸入---附加依賴項:加上lib文件名。

方法二:使用編譯語句

#pragma comment(lib,"./mylib.lib")

方法三:添加工程中

就像你添加.h和.c文件同樣,把lib文件添加到工程文件列表中去.

切換到"解決方案視圖",--->選中要添加lib的工程-->點擊右鍵-->"添加"-->"現有項"-->選擇lib文件-->肯定.

12.2.3 靜態庫優缺點

靜態庫對函數庫的連接是放在編譯時期完成的,靜態庫在程序的連接階段被複制到了程序中,和程序運行的時候沒有關係;

程序在運行時與函數庫再無瓜葛,移植方便。

浪費空間和資源,全部相關的目標文件與牽涉到的函數庫被連接合成一個可執行文件。

內存和磁盤空間

  靜態連接這種方法很簡單,原理上也很容易理解,在操做系統和硬件不發達的早期,絕大部門系統採用這種方案。隨着計算機軟件的發展,這種方法的缺點很快暴露出來,那就是靜態連接的方式對於計算機內存和磁盤空間浪費很是嚴重。特別是多進程操做系統下,靜態連接極大的浪費了內存空間。在如今的linux系統中,一個普通程序會用到c語言靜態庫至少在1MB以上,那麼若是磁盤中有2000個這樣的程序,就要浪費將近2GB的磁盤空間。

程序開發和發佈

  空間浪費是靜態連接的一個問題,另外一個問題是靜態連接對程序的更新、部署和發佈也會帶來不少麻煩。好比程序中所使用的mylib.lib是由一個第三方廠商提供的,當該廠商更新容量mylib.lib的時候,那麼咱們的程序就要拿到最新版的mylib.lib,而後將其從新編譯連接後,將新的程序整個發佈給用戶。這樣的作缺點很明顯,即一旦程序中有任何模塊更新,整個程序就要從新編譯連接、發佈給用戶,用戶要從新安裝整個程序。

12.3 windows下動態庫建立和使用

  要解決空間浪費和更新困難這兩個問題,最簡單的辦法就是把程序的模塊相互分割開來,造成獨立的文件,而不是將他們靜態的連接在一塊兒。簡單地講,就是不對哪些組成程序的目標程序進行連接,等程序運行的時候才進行連接。也就是說,把整個連接過程推遲到了運行時再進行,這就是動態連接的基本思想。

12.3.1 動態庫的建立

1. 建立一個新項目,在已安裝的模板中選擇「常規」,在右邊的類型下選擇「空項目」,在名稱和解決方案名稱中輸入mydll。點擊肯定。

2.在解決方案資源管理器的頭文件中添加,mydll.h文件,在源文件添加mydll.c文件(即實現文件)。

3.在test.h文件中添加以下代碼:

#ifndef TEST_H
#define TEST_H
__declspec(dllexport) int myminus(int a, int b);
#endif

5.test.c文件中添加以下代碼:

 

#include"test.h"
__declspec(dllexport) int myminus(int a, int b){
return a - b;
}

 

5. 配置項目屬性。由於這是一個態連接庫,因此應在項目屬性的「配置屬性」下選擇「常規」,在其下的配置類型中選擇「態庫(.dll)。

6.編譯生成新的解決方案,在Debug文件夾下會獲得mydll.dll (對象文件庫),將該.dll文件.lib文件和相應頭文件給用戶,用戶就能夠使用該庫裏的函數了。

疑問一:__declspec(dllexport)是什麼意思?

動態連接庫中定義有兩種函數:導出函數(export  function)和內部函數(internal  function)。 導出函數能夠被其它模塊調用,內部函數在定義它們的DLL程序內部使用。

疑問二:動態庫的lib文件和靜態庫的lib文件的區別?

在使用動態庫的時候,每每提供兩個文件:一個引入庫(.lib)文件(也稱「導入庫文件」)和一個DLL(.dll)文件。雖然引入庫的後綴名也是「lib」,可是,動態庫的引入庫文件和靜態庫文件有着本質的區別,對一個DLL文件來講,其引入庫文件(.lib)包含該DLL導出的函數和變量的符號名,而.dll文件包含該DLL實際的函數和數據。在使用動態庫的狀況下,在編譯連接可執行文件時,只須要連接該DLL的引入庫文件,該DLL中的函數代碼和數據並不複製到可執行文件,直到可執行程序運行時,纔去加載所需的DLL,將該DLL映射到進程的地址空間中,而後訪問DLL中導出的函數。

12.3.2 動態庫的使用

方法一:隱式調用

建立主程序TestDll,將mydll.h、mydll.dll和mydll.lib複製到源代碼目錄下。
(P.S:頭文件Func.h並非必需的,只是C++中使用外部函數時,須要先進行聲明)
在程序中指定連接引用連接庫 : #pragma comment(lib,"./mydll.lib") 

方法二:顯式調用

HANDLE hDll; //聲明一個dll實例文件句柄
hDll = LoadLibrary("mydll.dll"); //導入動態連接庫
MYFUNC minus_test; //建立函數指針
//獲取導入函數的函數指針
minus_test = (MYFUNC)GetProcAddress(hDll, "myminus");

 

12. 遞歸函數

12.1 遞歸函數基本概念

  C經過運行時堆棧來支持遞歸函數的實現。遞歸函數就是直接或間接調用自身的函數。

12.2 普通函數調用

void funB(int b){
printf("b = %d\n", b);
}
void funA(int a){
funB(a - 1);
printf("a = %d\n", a);
}
int main(void){
funA(2);
    printf("main\n");
return 0;
}

函數的調用流程以下:

12.3 遞歸函數調用

 

void fun(int a){
if (a == 1){
printf("a = %d\n", a);
return; //中斷函數很重要
}
fun(a - 1);
printf("a = %d\n", a);
}
int main(void){
fun(2);
printf("main\n");
return 0;
}

函數的調用流程以下:

 

 做業:

遞歸實現給出一個數8793,依次打印千位數字八、百位數字七、十位數字九、個位數字3。
void recursion(int val){
if (val == 0){
return;
}
int ret = val / 10;
recursion(ret);
printf("%d ",val % 10);
}

12.4 遞歸實現字符串反轉

 

int reverse1(char *str){
if (str == NULL)
{
return -1;
}
if (*str == '\0') // 函數遞歸調用結束條件
{
return 0;
}
reverse1(str + 1);
printf("%c", *str);
return 0;
}
char buf[1024] = { 0 };  //全局變量
int reverse2(char *str){
if (str == NULL) 
{
return -1;
}
if ( *str == '\0' ) // 函數遞歸調用結束條
{
return 0;
}
reverse2(str + 1);
strncat(buf, str, 1);
return 0;
}
int reverse3(char *str, char *dst){
if (str == NULL || dst == NULL) 
{
return -1;
}
if (*str == '\0') // 函數遞歸調用結束條件
{
return 0;
}
reverse3(str + 1);
strncat(dst, str, 1);
return 0;
}

13. 面向接口編程

13.1案例背景

  通常的企業信息系統都有成熟的框架。軟件框架通常不發生變化,能自由的集成第三方廠商的產品。

13.2 案例需求

  要求在企業信息系統框架中集成第三方廠商的socket通訊產品和第三方廠商加密產品。軟件設計要求:模塊要求鬆、接口要求緊。

13.3 案例要求

  • 1)能支持多個廠商的socket通訊產品入圍

  • 2)能支持多個第三方廠商加密產品的入圍

  • 3)企業信息系統框架不輕易發生框架

13.4 編程提示

  • 1)抽象通訊接口結構體設計(CSocketProtocol

  • 2)框架接口設計(framework)

  • 3)

    • a) 通訊廠商1入圍(CSckImp1
    • b) 通訊廠商2入圍(CSckImp2)
  • 4)

    • a) 抽象加密接口結構體設計(CEncDesProtocol

    •  b) 升級框架函數(增長加解密功能)

    • c) 加密廠商1入圍(CHwImp)、加密廠商2入圍(CCiscoImp)

  • 5)框架接口分文件
相關文章
相關標籤/搜索