[學習][記錄] c語言:從放棄到入門

9-3變量生命週期和修飾符

生命週期

函數的聲明週期

  起於調用,結束於調用結束

局部變量的生命週期

   起於調用,結束於調用結束

main函數的生命週期

   main開始  == 進程的開始   main函數的結束  == 進程的結束

全局變量的生命週期

	起於main調用,結束於main調用結束

修飾符

auto

      含義:只能用於修飾局部變量,表示該變量存儲於棧stack
      特點: 隨用隨開,用完消失
      C中默認的局部變量,就是auto類型,所以通常將其省略   c++ auto自動類型

register

    含義:只能修飾局部變量,原則上,將內存中的變量升級到CPU寄存器中存儲,這樣訪問速度會更快,但由於CPU寄存器數量有限,通常會在程序優化階段,被優化爲普通auto類型,可以通過彙編代碼來查看,優化過程(與平臺和編譯有關)
    特點:可以避免cpu與內存的頻繁交互 
    一般程序從內存讀取數據 放到寄存器 進行運算 運算結果寫入到內存

關於各個存儲部件中讀寫速度

		硬盤:7200轉
	    內存:頻率1333MHZ 1600MHZ  帶寬 = 頻率*64/8
    	緩存:
    	CPU:

extern

	含義:只能用於修飾全局變量,全局變量本身是全局可用的,但是由於文件是單個完成編譯,
	並且編譯是自上而下的,
	所以說,對於不是在本範圍內定義的全局變量,要想使用必須用extern 進行申明,
	如果不加上可能會造成重定義。
	
    特點:
	1.可以省略,聲明在前,定義在後,不論局部還是全局。	
	   main.c  :   int a;  other.c : int a =200;編譯不會報錯
	 2.外延性 某文件定義的全局變量 可在其他文件使用

c語言編譯是跨文件的

      編譯過程:單文件編譯 單文件每個編譯成xxx.o
      					鏈接所有.o文件和lib.so文件
      					生成可執行文件xxx.out

9-4 static

修飾局部變量

   特點:1.初始化值爲0,且只初始化一次
   			 2.生命週期等同於程序

修飾全局變量

	特點:1.全局的外延性消失,變成文件內部的全局變量  也適用於函數
			   2.存儲在data段

小結問題:

  1. extern int a;
    int a= 200;
    是不是同一個a?

  2. 局部變量和全局變量儲存的位置有什麼不同?

  3. static修飾的變量值所保存的位置在哪裏 才導致是累計變化的?
    保存在data段中

9-5 字符串常量——9-7字符串的輸入輸出

定義

	是雙引號括起的任意字符序列

字符串大小

	看到的大小,比我們實際字面量要多一個,
	最後的字符’\0’,我們稱爲字符串結束字符,是系統對雙引號引起的字符串自動加設 的,而非手動干預。

字符串存儲

	 字符串的存儲,不像其它普通類型的常量自變量的存儲一樣,普通類型自變量通常存儲 在代碼段(text),
	 而字符串,則存儲在數據段,且是隻讀數據段。
	  也就是通常意義上的常量區,但是常量區這種說法,是不太精確的,也不提倡。 

拓展

   棧區(stack):臨時變量,局部變量,生命週期結束即銷燬
   堆區(heap):一些new出來的變量 存儲的地方 特定銷燬時間統一銷燬,析構函數
   數據段(data):用來存放程序中已初始化的全局變量的一塊內存區域,屬於靜態內存分配。
   代碼段(text):用來存放程序執行代碼的一塊內存區域,只讀,且不可修改。可能包含一些只讀的常數變量,例如字符串常量等。 
   bbs段:bss段(bss segment)通常是指用來存放程序中未初始化的全局變量的一塊內存區域

在這裏插入圖片描述

字符數組和字符串區別

	1.字符串數組(棧區)是將text區的常量字符串 拷貝到棧區 字符串值的改變實際上是棧的值改變
	   字符串指針 是直接指向 text區常量字符串地址 故不能改變text區的值
	2.相同點:字符數組長度大於等於字符串長度

字符串的輸入輸出

    puts函數:printf的自帶換行
    scanf函數:默認遇到空格結束輸入,scanf("%[\n]s")遇到回車結束輸入,但不檢查輸入長度,不安全
    gets函數:空格也可以讀入,但不檢查輸入長度,不安全。
    fgets函數:參數列表(接收變量,長度,輸入流)

tips

    1.printf(“”) 空串有換行功能
    2.gets();不檢查預留存儲區是否能夠容納實際輸入的數據,
    換句話說,如果輸入的字符數目大於數組的長度,gets 無法檢測到這個問題,就會發生內存越界,所以編程時建議使用 fgets()。
    char *fgets(char *s, int size, FILE *stream);
    fgets() 雖然比 gets() 安全,但安全是要付出代價的,
    代價就是它的使用比 gets() 要麻煩一點,有三個參數。
    它的功能是從 stream 流中讀取 size 個字符存儲到字符指針變量 s 所指向的內存空間。它的返回值是一個指針,指向字符串中第一個字符的地址。
	3. c中允許 c++不允許 
	 char a[2] = "china";
     printf("%s",a);
     輸出結果:ch
     4.char * p = "china";
	   printf("p = %p  p+1 = %p  p[0]=%c 0[p] =%c\n", p, p + 1, p[0], 0[p]);
	   printf("  = %p      = %p      =%c      =%c\n", "china", "china"+1, "china"[0], 0["china"]);
	   輸出結果:地址不相等 與視頻不符合

問題?

  1.視頻說   " 字符串,則存儲在數據段"

這篇文章說了
在代碼段中,也有可能包含一些只讀的常數變量,例如字符串常量等

 2.字符串指針和字符數組的區別?
    字符數組可修改內容,有個拷貝過程,data段常量區拷貝到棧區,字符數組實際是棧區
    字符串指針如果指向data段字符串常量 是不可修改地址的,若是指向字符數組反而可以

10-2 從字符串常量到字符數組

10-3 原生字符串處理

strlen(char *) 字符串長度

strcat(char *head,char *tail) 連接兩個字符串

注意:被連接的串必須有足夠空間
but vs 可行…

char firstName[] = "jim";
char familyName[30] = "tim";
strcat(firstName, familyName);
printf("%s", firstName);
輸出:jimtim

優化操作:
原本代碼

   while(*p) p++;
   char *p,*q;
   while(1){
     p * =q*;
     if(p* == '\0'){
		break;	
	 }
	 p++;q++;
   }
   優化後:
   while(*p) p++;
   while(*p++ = *q++);

Tip

  1. int* 和 char* ,float * 長度 是多少?
    指針統一長度爲4byte
	    int a = 1;
		int* ap = &a;
		printf("%d \n", sizeof(ap));
		char * c = "123";
		printf("%d \n", sizeof(c));
		float d = 1.0f;
		float * dp = &d;
		printf("%d \n", sizeof(dp));
輸出:4
       4
       4   
  1. char *ac = “123”;
    printf("%p \n", &ac);
    printf("%p \n", “123”);

    爲什麼輸出地址不同?

3.字符串長度和大小是不同的
長度是指字符長度且不包含\0,大小是指字節大小包含\0

10-7 字符串指針數組入門

指針數組的本質是數組,數組指針的本質是指針。

Tips

1.運行這段代碼 設置字符串池優化

char * pa = "china"; char *pb = "america"; char*pc = "canada"; char*pd = "japan";
		char * cpArr[4] = { pa,pb,pc,pd };
		printf("pa =  %p \n", pa);
		printf("pb =  %p \n", pb);
		printf("pc =  %p \n", pc);
		printf("pd =  %p \n", pd);
	
		for (int i = 0; i < 4; i++)
		{
			printf("%p \n",cpArr[i]);
		}
	
		printf("----------------\n", pd);
	
		char * cpArr2[4] = {"china","america","canada", "japan" };
	
		for (int i = 0; i < 4; i++)
		{
			printf("%p \n", cpArr2[i]);
		}
  1. NULL ,nullptr,0區別

11-1 棧內存和堆內存的基本概念

概念

源程序:源代碼
程序:可執行文件
進程:時間概念可執行性文件被拉起,到結束的這一段過程,稱爲進程
進程空間:可執行文件 被拉起以後 在內存中的分佈情況
內存空間

棧內存

棧存儲的特點

    棧中存放任意類型的變量,但必須是 **auto** 類型修飾的,即自動類型的局部變量,隨用隨開,用完即消。
    內存的分配和銷燬系統自動完成,不需要人工干預。
    分配順序,由高地址到低地址。

棧的大小

	 棧的大小並不大,他的意義並不在於存儲大數據,而在於數據交換。

常見棧溢出

	局部變量申請過多,過大,char[1024*1024*10];
	遞歸層數太多 例如10000層遞歸

堆內存

棧存儲的特點

	堆內存可以存放任意類型的數據,但需要自己申請與釋放。
	分配順序,由低地址到高地址。

堆大小

	堆大小,想像中的無窮大,對於棧來說,大空間申請,唯此,無它耳。但實際使用中,受限於實際內存的大小和內存是否連續性。(基本最大爲用戶空間大小)

Tips

1.memset(pm,1,10*sizeof(int);

   爲每個字節賦值爲1;一個int 實際4個字節 4個0x01
   printf("%#x");
   %#表示的輸出提示方式,如果是8進制,在前面加0,如果是十進制,不加任何字符,如果是十六進制,會加上0x 

2.calloc和malloc

   calloc在動態分配完內存後,自動初始化該內存空間爲零,而malloc不做初始化,分配到的空間中的數據是隨機數據

3.realloc

void *realloc(void *ptr, size_t size);
功能:擴容(縮小)原有內存的大小。通常用於擴容,縮小會會導致內存縮去的部分數據丟失。
參數:
void *ptr:ptr 表示待擴容(縮小)的指針, ptr 爲之前用 malloc 或 者 calloc 分配的內存地址。或 ptr==NULL,則該函數等同於 malloc。
size_t:size 表示擴容(縮小)後內存的大小。
返回值:
成功返回非空指針指向申請的空間 ,失敗返回 NULL。返回的指針,可能與 ptr 的值相同,也有可能不同。若相同,則說明在原空間後面申請,否則,則可能後續空間不足,重新申請的新的連續空間,原數據拷貝到新空間,原有空間自動釋放。
原理:先判斷當前的指針是否有足夠的連續空間,如果有,擴大mem_address指向的地址,並且將mem_address返回,如果空間不夠,先按照newsize指定的大小分配空間,將原有數據從頭到尾拷貝到新分配的內存區域,而後釋放原來mem_address所指內存區域(注意:原來指針是自動釋放,不需要使用free),同時返回新分配的內存區域的首地址。即重新分配存儲器塊的地址。

malloc 使用流程 申請 判空 使用 釋放 置空

問題1:指針接收 創建的內存空間(malloc),釋放時怎麼知道釋放(free)多少內存空間的

	heap的每個塊兒頭有保存本塊的大小以及本塊分配的大小。
	free並不知道那個指針所指空間的大小,它要先查找當前heap的頭部,然後整個塊兒釋放掉。

問題2:如何用malloc/new 定義一維,二維,三維數組?用free/delete銷燬呢?malloc/free和new/delete區別?

問題3:malloc/calloc/realloc 區別與不同?

問題4:常見的內存泄露和檢測內存泄露的常見方式?內存泄漏和野指針?

12-1 結構體 共用體 枚舉

typedef深入分析

定義

	用自定義名字爲已有數據類型命名。其實叫 typerename 更合適。
	形如:typedef 現在類型名 新類型名;

typedef 和#define 的區別

typedef 是以;號結尾的 C 語言語句。而#define 則是預處理階段的文本替換。有時
他們是可以互換的,但有時不可以。

	typedef a b;
	#define a b

Tips

1.typedef char *pChar; pChar a; a等同於?
試着預測輸出並調試以下代碼:

 {
  char *p,q;
  printf("sizeof(p) = %d  sizeof(q) = %d \n",sizeof(p),sizeof(q));// 4,1
  typedef char *pChar;
  pChar a,b;
  printf("sizeof(a) = %d  sizeof(b) = %d \n",sizeof(a),sizeof(b));// 4, 4
  #define DpChar char*;
  DpChar m,n;
  printf("sizeof(m) = %d  sizeof(n) = %d \n",sizeof(m),sizeof(n));// 4 ,1
  }

2.總結
 新類型名一般用大寫表示,以便於區別。
 用 typedef 只能聲明新的類型名,不能創造新的類型,只是爲已經存在的類型起
一個別名,也不能用來定義變量,即只能用其聲明的類型來定義變量;
 有時也可用宏定義來代替 typedef 的功能,但是宏定義是由預處理完成的,而
typedef 則是在編譯時完成的,更爲靈活方便。
 typedef 可以讓類型更見名知意,更便於移值。

結構體的初始化

初始化及成員訪問

點成員運算符(.) 優先級等同-> 但比*高

Tips

	問題一
	  初始化和賦值在c++中實際含義是?
	問題二
	  形參和實參之間傳遞是什麼關係?
	問題三
	  typedef和#define 本質區別在哪裏?
	問題四
	   結構體作形參爲什麼要傳指針?

結構體數組及應用

結構體嵌套和結構體大小

結構體嵌套

	結構體中,嵌套結構體,稱爲結構體嵌套。結構體中,既可以嵌套結構體類型變量,
也可以嵌套結構體類型,後一種方式不推薦。

結構體類型大小

結構體成員內存分佈

首成員在低地址,尾成員在高地址。

內存對齊

對齊規則

目的是解決:一個成員變量需要多個機器週期去讀的現象,稱爲內存不對齊。爲什麼要對齊
呢?本質是犧牲空間,換取時間的方法。

不同的編譯器和處理器,其結構體內部的成員有不同的對齊方式
x86(linux 默認#pragma pack(4), window 默認#pragma pack(8))。linux 最大支持 4 字節對齊。

方法:

	①取 pack(n)的值(n= 1 2 4 8--),取結構體中類型最大值 m。兩者取小即爲外對齊大 小 Y= (m<n?m:n)。 
	②將每一個結構 體的成員大小與 Y 比較取小者爲 X,作爲內對齊大小. 
	③所謂按 X 對齊,即爲地址(設起始地址爲 0)能被 X 整除的地方開始存放數據。
	④外部對齊原則是依據 Y 的值(Y 的最小整數倍),進行補空操作。

外對齊和內對齊:

	 外對齊Y:保證讀取結構體的起始地址到結束地址,表示結構體之間的對齊
	 內對齊X:保證從結構體內變量起始的地址到結束的地址 正好是該變量的長度,結構體內成員變量之間的對齊		

結構體中指針使用注意事項

1.向結構體內未初始化的指針拷貝

結構體中,包含指針,注意指針的賦值,切不可向未知區域拷貝。

struct student
{
 char*name;
 int score;
}stu;
int main()
{
 strcpy(stu.name,"Jimy");
 stu.score=99;
 return 0;
}

name 指針並沒有指向一個合法的地址,這時候其內部存的只是一些亂碼。所以在
調用 strcpy 函數時,會將字符串 “Jimy” 往亂碼所指的內存上拷貝,內存 name 指針根
本就無權訪問,導致出錯。 同樣stu.name = “Jimy”;可以的,name指向常量區,但是將來name不可改

int main()
{
	 struct student *pstu;
	 pstu = (struct student*)malloc(sizeof(struct student));
	 strcpy(pstu->name,"Jimy");
	 pstu->score=99;
	 free(pstu);
	 return 0;
}

爲指針變量 pstu 分配了內存,但是同樣沒有給 name 指針分配內存。錯誤與上面
第一種情況一樣,解決的辦法也一樣。這裏用了一個 malloc 給人一種錯覺,以爲也給
name 指針分配了內存。

2.未釋放結構體內指針所指向的空間

從內向外依次釋放空間。

Tip

1.結構體中嵌套構造類型成員的對齊(數組、結構體成員)

2.深拷貝和淺拷貝

	深拷貝:拷貝內存的內容,結構體之間互不影響。
	淺拷貝:直接地址賦值,指針共享一片內存。一個結構體發生變化,另一個結構體也會發生變化。

13-2 單鏈表

14-2 文本文件和二進制文件

文件流

C 語言把文件看作是一個字符的序列,即文件是由一個一個字符組成的字符流,因 此 c 語言將文件也稱之爲文件流。即,當讀寫一個文件時,可以不必關心文件的格式或
結構。

文件類型

文件,物理上是二進制,所以文本文件與二進制文件的區別並不是物理上的,而是邏輯上的。
文本文件是基於字符編碼的文件,常見的編碼有 ASCII 編碼,二進制文件是基於值編碼
的文件。

文本文件

以 ASCII 碼格式存放,一個字節存放一個字符。 文本文件的每一個
字節存放一個 ASCII 碼,代表一個字符。這便於對字符的逐個處理,但佔用存儲空間
較多,而且要花費時間轉換。

二進制文件

以值(補碼)編碼格式存放。二進制文件是把數據以二進制數的格
式存放在文件中的,其佔用存儲空間較少。數據按其內存中的存儲形式原樣存放。

用例:

	int main() {
	short a = 10000;
	
	FILE * fp = fopen("ascii.txt", "w");
	fprintf(fp, "%d", a);//文本寫
	fclose(fp);


	FILE *fp2 = fopen("bin.txt", "w");
	char buf[] = "abcd";
	fwrite(&a, 2, 1, fp2);//字節寫
	//fwrite(buf, 4, 1, fp2);//字節寫
	fclose(fp2);

	return 0;
}

文件緩存

爲什麼要有緩衝區(buffer) 原因爲多種,有兩個重點:
1 從內存中讀取數據比從文件中讀取數據要快得多。
2 對文件的讀寫需要用到 open、read、write 等系統底層函數,而用戶進程每調用
一次系統函數都要從用戶態切換到內核態,等執行完畢後再返回用戶態,這種切
換要花費一定時間成本
(對於高併發程序而言,這種狀態的切換會影響到程序性
能)。

文件的打開和關閉

FILE 結構體

FILE 結構體是對緩衝區和文件讀寫狀態的記錄者,所有對文件的操作,都是通過
FILE 結構體完成的。
typedef struct {
 short level; /* 緩衝區滿/空程度 */
 unsigned flags; /* 文件狀態標誌 */
 char fd; /* 文件描述符 */
 unsigned char hold; /* 若無緩衝區不讀取字符 */
 short bsize; /* 緩衝區大小 */
 unsigned char *buffer; /* 數據傳送緩衝區位置 */
 unsigned char *curp; /* 當前讀寫位置 */
 unsigned istemp; /* 臨時文件指示 */
 short token; /* 用作無效檢測 */
} FILE ; /* 結構體類型名 FILE */

在開始執行程序的時候,將自動打開 3 個文件和相關的流:標準輸入流(stdin)、標
準輸出流(stdout)和標準錯誤(stderr),它們都是 FIEL*型的指針。流提供了文件和程序的
通信通道。

fopen

fopen作用
在這裏插入圖片描述
如果讀寫的是二進制文件,則還要加 b,比如 rb, r+b 等。 unix/linux 不區分文本和
二進制文件。

fclose

作用:強制輸出緩存內容 然後關閉FILE*

文件的讀和寫

一次讀一個字符

fputc

fgetc

feof

特點:feof 這個函數,是去讀標誌位判斷文件是否結束的。即在讀到文件結尾的時候再
去讀一次,標誌位纔會置位,此時再來作判斷文件處理結束狀態,文件到結尾。如果用
於打印,則會出現多打一次的的現象

一次讀一行字符

windows 換行符 ‘\n’ = 0x0d 0a;
linux 換行符 ‘\n’ = 0x 0a;

tips

1.fprintf(fp,fmt,buff) 文本寫出
fwrite(buff,size,count,fp) 字節寫出
2.亂碼原由
二進制文件讀取由acsii碼的方式讀取
3.文件緩存win和linux區別
win會立即輸出,linux會等待緩存滿了再輸出,加上\n會立刻輸出緩存
4.rewind(fp) 將文件指針重置到文件頭

位運算符

預處理

預處理操作,不是 c 語言語句,故語句末尾沒有分號,在預處理階段完成,本質是替
換操作。
發生時段:
在這裏插入圖片描述

宏定義變量

不帶參宏:
#define 定義的宏,只能在一行內表達(換行符表示結束而非空格),如果想多行表
達,則需要加續行符。

#define PI 3.14\
15926

宏常量,常被 const/ enum 變量取代,用於定義文件路徑則被常用。

#define FILEPATH "E:\\English\\listen_to_this\\listen_to_this_3"
#define ERR_EXIT(m)\
do\ {\
 printf("Err:%s",m);\
 exit(-1);\
}while(0) //此處的分號,可有可無

宏常量的缺陷 解決這一些問題,要不吝惜使用括號。

#define N 2+3 // #define N (2+3)
int main(void)
{
	 int num = N*2;
	 return 0;
 }

宏類型:
宏可以給類型起別名,因其缺點,常被 typedef 取代

 #define CHARP char *
int main(void)
{
 CHARP p,q;
 printf("p = %d q = %d\n",sizeof(p),sizeof(q));
 return 0;
}

帶參宏(宏函數)

#define str(x) x
#define str(x) “aa”#x"bb" //#字符串化
#define str(x) x*2 \
x+x // ‘’ 續行符號

#undef MAX

條件編譯

#ifdef #ifndef
#elif
#endif

預定義宏

頭文件包含的意義

全寫入,被包含的文件中。包含是支持嵌套的。
在這裏插入圖片描述

方式<>

#include<stdio.h>,從系統指定路徑中搜索包含頭文件,linux 中的系統路徑爲
(/usr/include)

方式" "

#include"myString.h",從工程當前路徑中搜索包含頭文件,如果當前工程路徑下
沒有的話,則到系統路徑下搜索包含。

其他

#運算符 利用宏創建字符串

將替換符 字符串化,解決字符串中,不可被替換的參數問題。字符串如下的書寫
也是合理的。

//#define str(x) #x
//#define str(x) "aaaaaaaaxaaaaaaaaa"
#define str(x) "aaaaaaaa"#x"aaaaaaaaa"
int main()
{
 printf("%s\n",str(100));
 return 0;
}

##運算符 預處理的粘和劑

#解決了雙引號中無法替換問題,##解決了非雙引號中粘連無法替換的問題。

//#define sum(a,b) (aa+bb)
#define sum(a,b) (a##a+b##b)
int main()
{
 printf("%d\n",sum(2,3));
 return 0;
}

預定義宏

DATE 進行預處理的日期(“MMmm dd yyyy"形式的字符串文字)
FILE 代表當前源代碼文件名的字符串文字
LINE 代表當前源代碼中的行號的整數常量
TIME 源文件編譯時間,格式"hh:mm:ss”
func 當前所在函數名
在打印調試信息時打印這兩
個宏 FILE LINE 可以給開發者非常有用的提示

變參函數

參數的個數也是可變的,也就是說,在形參表中可以不明確指定傳遞參數的個數和類型,一個常見的庫函數 printf()就是如此。這種函數稱之爲變參函數。

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
//num 個 int 型數相乘
int mul(int num, int data1, ... )
{
	 int total = data1;
	 int arg, i;
	 va_list ap;
	 va_start(ap, data1);
	 for(i = 1; i < num; i++)
	 {
	 arg = va_arg(ap, int);
	 total *= arg;
	 }
	 va_end(ap);
	 return total;
}
	//i 個 int 型數相乘
long mul2(int i, ...)
{
	 int *p, j;
	 p = &i + 1; //p 指向參數列表下一個位置
	 long s = *p;
	 for (j = 1; j < i; j++)
	 s *= p[j];
	 return s;
}
int main()
{
 printf("%d\n", mul(3, 2, 3, 5));
 printf("%d\n", mul2(3, 2, 3, 5));
 return 0;
}

變參宏

VA_ARGS 是一個可變參數的宏,這個可變參數的宏是新的 C99 規範中新增
的,目前似乎只有 gcc 支持(VC6.0 的編譯器不支持)。宏前面加上##的作用在於,當
可變參數的個數爲 0 時,這裏的##起到把前面多餘的","去掉的作用,否則會編譯出錯, 你
可以試試。

#define debug(...) printf(__VA_ARGS__)
#define dgbmsg(fmt,...) printf(fmt,__VA_ARGS__)
#define LOGSTRINGS(fm, ...) printf(fm,__VA_ARGS__) 
#define debug(format, ...) fprintf (stderr, format, __VA_ARGS__)
#define debug(format, args...) fprintf (stderr, format, args)

在標準 C 裏,你不能省略可變參數,但是你卻可以給它傳遞一個空的參數。例如,
下面的宏調用在 ISO C 裏是非法的,因爲字符串後面沒有逗號:
debug (“A message”)
GNU CPP 在這種情況下可以讓你完全的忽略可變參數。在上面的例子中,編譯器仍
然會有問題(complain),因爲宏展開後,裏面的字符串後面會有個多餘的逗號。
CPP 使用一個特殊的’##’操作。書寫格式爲:
#define debug(format, …) fprintf (stderr, format, ## VA_ARGS)
這裏,如果可變參數被忽略或爲空,’##’操作將使預處理器(preprocessor)去除掉
它前面的那個逗號。如果你在宏調用時,確實提供了一些可變參數,GNU CPP 也會工作
正常,它會把這些可變參數放到逗號的後面。象其它的 pasted macro 參數一樣,這些參
數不是宏的擴展。

進階

數據類型

1.補碼
在這裏插入圖片描述
有符號數正負數二進制排列規律:
負數爲對應的正數 取反+1
0的補碼是0
2. int 和 long ,long long 區別
編譯器不同 int和long的含義可能是不同的

而 long long類型是 8個字節,64位
unsigned long long:0~2^64-1
long long:-2^63~ 2^63-1
3.類型轉化
顯示轉化
1>小數據賦給大變量 不會造成數據的丟失,系統爲了保證數據的完整性,還提供了符號擴充行爲。
2>大數據賦給小變量 會發生截斷行爲,有可能會造成數據丟失
隱式轉化
1>整體提升 32位機中 所有低於32位的整型數據 在運算過程中先要轉化爲 32 位的
整型數據,然後才參與運算
2>混合提升
(1)有long double 類型參與運算,先全部轉化爲long double
沒有(1)則(2)有double 類型參與運算,先全部轉化爲double類型
沒有(2)則(3)有unsign int 類型參與運算,先全部轉化爲unsign int類型
沒有(3)則(4)有 int 類型參與運算,先全部轉化爲int類型
3>強制轉化

進程空間:

在這裏插入圖片描述
在這裏插入圖片描述

壓棧和出棧

每次壓棧前會保存當前函數環境包括變量值,然後新創建一塊棧區來進行操作。
每次出棧會銷燬當前棧頂,最後return到上一個函數所在地址。

一維數組

本質

數組是用於存儲相同數據類型數據,且在內存空間連續的一種數據結構類型。數組 三要素。
在這裏插入圖片描述
類型:type [N]
定義:type name[N]
大小:sizeof(type [N]) 或 sizeof(name)

初始化

int array[10] = {1,2,3}; //部分初始化
int array2[10] = {[3] = 10};
int array1[10] = {0}; //清零
int array3[10] = {1,2,3,4,5,6,7,8,9,0,1,2};//越界不檢

訪問

數組名是數組的唯一標識符,數組的每一個元素都是沒有名字的。數據名有兩重含 義:
(1)數組名作整體訪問

int main(void) {
  int arr[10] = {1,2,3,4,5,6,7,8,9,0};
  //int[10] arr; 
  printf("sizeof(arr) = %d\n",sizeof(arr)); 
  printf("&arr = %p\n",&arr); 
  printf("&arr+1 = %p\n",&arr+1); 
  int(*pa)[10] = &arr; 
  printf("pa = %p\n",pa); 
  printf("pa+1 = %p\n",pa+1); 
  return 0; 
}

輸出結果:

sizeof(arr) = 40
&arr = 007AF9DC
&arr+1 = 007AFA04
pa = 007AF9DC
pa+1 = 007AFA04

pa +1 爲數組最後一個元素的後的第一個地址

(2)數組名作起始地址訪問成員

name[i] == *(name+i) == i[name]
int main(void) {
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    printf("arr[0] = %d\n",arr[0]);
    printf("*(arr+0) = %d\n",*(arr+0));
    printf("arr = %p\n",arr);
    printf("arr +1 = %p\n",arr+1);
    printf("&arr[0] = %p\n",&arr[0]);
    printf("&arr[0] +1 = %p\n",&arr[0]+1);
    return 0;
}

輸出結果:

arr[0] = 1
*(arr+0) = 1
arr = 010FF7E4
arr +1 = 010FF7E8
&arr[0] = 010FF7E4
&arr[0] +1 = 010FF7E8

作參數傳遞

參數傳遞,旨在傳遞三要素(起始地址,步長, 範圍)

void selectSort(int *p,int n)// int 表示步長,p表示起始地址,n表示範圍

返回堆中一維數組

返回值返回(一級指針)

char * allocMem(int n) {
 char *p = (char*)malloc(n);
 return p;
}

參數返回(二級指針)

int allocMem(char **p,int n)
{ 
  *p = (char*)malloc(n); 
  return *p== NULL?-1:1;
}

二維數組

本質

二維數組的本質是一維數組,只不過,一維數組的成員又是一個一維數組而己。三 要素:
在這裏插入圖片描述

type name[M][N] == type[N] name[M] 
int arr[3][4] == int[4] arr[3] // 步長爲int[4] 起始地址爲arr 範圍3

初始化

行可以省,列不可以省,部分初始化和清零依然適用
int array[2][3] = {[1][2]=3}; //c99
int array[][3];=> int[3] arry[];//行可以省 列不可以省 行 初始化 自動賦值

訪問

數組名,是數組的唯一標識符。
(1)數組名作爲整體訪問

int main(void) {
    int arr[3];
    //int[3] arr;
    printf("sizeof(arr) = %d sizeof(int[3]) = %d\n",sizeof(arr),sizeof(int[3]));
    printf("&arr = %p\n",&arr);
    printf("&arr+1 = %p\n",&arr+1);
    int array[3][4];
    //int[4] array[3] type array[3]; type[3] array;
    printf("sizeof(array) = %d sizeof(int[3][4]) = %d\n",sizeof(array),sizeof(int[3][4]));
    printf("&array = %p\n",&array);
    printf("&array +1 = %p\n",&array+1);
    return 0;
}

輸出結果:

sizeof(arr) = 12 sizeof(int[3]) = 12
&arr = 005CFDF4
&arr+1 = 005CFE00
sizeof(array) = 48 sizeof(int[3][4]) = 48
&array = 005CFDC0
&array +1 = 005CFDF0

&array+1 同一維數組 數組結尾地址

(2)數組名作起始地址訪問成員
(3)結論
a 表示第 0 行首地址,a+i 表示第 i 行首地址

 *(a+i), a[i]&a[i][0]表示第 i 行第 0 個元素地址 
 *(a+i)+j, a[i]+j, &a[i][j]表示第 i 行第 j 個元素地址 
 *(*(a+i)+j) *(a[i]+j), a[i][j]表示第 i 行第 j 個元素

線性存儲

二維數組在邏輯上是二維的,但是在存儲上卻是一維的。正是這個特點,也可以用 一維數組的方式去訪問二維數組的。

int main() { 
   int array[2][3] = {9,8,7,6,5,4}; 
   for(int i=0;i<2; i++) {
    for(int j=0;j<3;j++) {
     printf("%d ",array[i][j]); 
    }
    putchar(10); 
    }
    int *p = (int *)array; 
    for(int i=0; i<6;i++) { 
    printf("%d ",p[i]); 
    }
    return 0; 
}

參數傳遞

二維數組本質是常量數組指針,所以跟其對應的形參也應該是數組指針類型的變 量

void displayArray(int(*p)[4],int n) //步長int[4] 首地址p,範圍n

數組指針

一次可以移動一個字節指針,稱爲 char 類型的指針,
一次可以移動二個字節的指 針,稱爲 short
類型的指針。
一次可以移動一個數組大小的指針,是什麼類型的指針的,又該如何稱謂呢?
比如 二維數組名: arr[3][4]; arr+1 一次加 1 的大小,就是 int[4]類型的大小,即一個數組 的大小。

定義

int [N] *pName; =>int (*pName)[N];

語法解析:“()”的優先級比“[]”高, “*”號和 pName 構成一個指針的定義, 指針的類型爲 int [N]。

別名

typedef int (*TYPE)[N];
TYPE a;//等價於 int (*a)[N];

數組指針與數組名

解析 一維數組名 二維數組名
示例 int arr[4] int arr[3][4]
本質 一級指針 數組指針
引用 &arr 數組指針 &arr 數組指針

應用

(1)二維數組傳參

void displayArray(int(*p)[4],int n)

(2)一維空間的二維訪問

int main() {
    int arr[12] = {1,2,3,4,5,6,7,8,9,10,11,12};
    int (*p)[2] = (int(*)[2])arr;
    for(int i=0; i< sizeof(arr)/sizeof(int[2]);i++) {
        for(int j=0; j<2; j++) {
            printf("%d\t",p[i][j]);
        }
        putchar(10);
    }
}

輸出結果:
1 2
3 4
5 6
7 8
9 10
11 12

多維指針

本質分析

type name[x][y][z] == type [y][z] name[x]; //步長爲 type[][],首地址name,範圍 x

指針

編址

變量地址

對變量取地址,取得是最低位字節的地址。32 位機下,大小均爲 4。

int main(void) {
    char a;
    short b;
    int c;
    printf("&a = %p\n",&a);
    printf("&b = %p\n",&b);
    printf("&c = %p\n",&c);
    printf("sizeof(&a) = %d\n",sizeof(&a));
    printf("sizeof(&b) = %d\n",sizeof(&b));
    printf("sizeof(&c) = %d\n",sizeof(&c));
    return 0;
}

輸出結果:

&a = 00CFF897
&b = 00CFF890
&c = 00CFF88C
sizeof(&a) = 4
sizeof(&b) = 4
sizeof(&c) = 4

本質

指針的本質,就一個有類型的地址。

int main(void) {
    int a = 0x12345678;
    printf("%p\n",&a);
    printf("%d\n",*(&a));
    //printf("%d\n",a);
    //printf("%x\n",*((int*)0x0060FEAC));
    return 0;
}

指針變量

內存的地址,即指針(常量),存放該地址的變量就是指針變量。此變量,必須滿足 3 個條件,大小爲 4,有類型,區別於其它變量。

type *var;

type 決定了類型(步長),* 表示該變量是指針,var 用於存儲地址。

變量大小

指針變量 大小始終爲4字節
數組變量 大小爲 類型(即步長)* 範圍 字節

int main(void) {
    char *pa;
    int *pi;
    printf("sizeof(pa) = %d sizeof(pb) = %d\n", sizeof(pa),sizeof(pi));
    printf("sizeof(char*) = %d,sizeof(int*) = %d\n",sizeof(char*),sizeof(int*));

    char *pm = (char*)malloc(100);
    printf("sizeof(pm) = %d\n",sizeof(pm));

    int (*parr)[10];
    printf("sizeof(parr) = %d\n",sizeof(parr));

    int arr[100];
    printf("sizeof(arr) = %d\n",sizeof(arr));
    printf("sizeof(&arr) = %d\n",sizeof(&arr));
    return 0;
}

輸出結果:

sizeof(pa) = 4 sizeof(pb) = 4
sizeof(char*) = 4,sizeof(int*) = 4
sizeof(pm) = 4
sizeof(parr) = 4
sizeof(arr) = 400
sizeof(&arr) = 4

變量類型

類型,決定了,從 var 存放的地址開始的尋址能力。

int main(void) {

    int a = 0x12345678;
    char *pa = &a;
    printf("%x\n",*pa);
    short *ps = &a;
    printf("%x\n",*ps);
    int *pi = &a;
    printf("%x\n",*pi);

    return 0;
}

輸出結果:

78
5678
12345678

注意:值的存儲 由低到高

引用與解引用

int main(void) {
    int a = 0x12345678;
    printf("%x\n",*&a);
    int arr[3];
    printf("arr = %p\n",arr);
    printf("&arr = %p\n",&arr);
    printf("arr+1 = %p\n",arr+1);
    printf("&arr+1 = %p\n",&arr+1);
    printf("*&arr = %p\n",*&arr);
    printf("*&arr + 1 = %p\n",*&arr+1);
    return 0;
}

輸出結果:

12345678
arr = 006FFAC4
&arr = 006FFAC4
arr+1 = 006FFAC8
&arr+1 = 006FFAD0
*&arr = 006FFAC4
*&arr + 1 = 006FFAC8

在地址 方面,
數組arr => &arr ,也有arr => *&arr
但arr+1 !=> &arr+1 (步長不一樣)

運算

指針的運算是,地址值+類型的運算。

int main(void) {

    int a[10];
    printf("a = %p\n",a);
    printf("a+1 = %p\n",a+1);
    printf("&a[9] - &a[4] = %d\n",&a[9] - &a[4]);
    printf("(int)&a[9]-(int)&a[4] = %d\n",(int)&a[9] -(int)&a[4]);

    return 0;
}

輸出結果:

a = 0116FAE8
a+1 = 0116FAEC
&a[9] - &a[4] = 5
(int)&a[9]-(int)&a[4] = 20

解釋幾點:
a和a+1是 運算是以步長(此時爲int)爲單位 +1即移動一個Int長度
&a[9],&a[5]都是轉爲指針int* 也是以int爲步長單位的,所以9-5+1=5步
(int)&a[9],(int)&a[4] 被強轉爲字節爲單位的了 所以結果爲 5*4 =20字節

補充幾個例子:
例1

int main() {
    int a[5] = {1,2,3,4,5};
    int *ptr1 = (int *)(&a + 1);//此時ptr1指向a末尾後面一個int
    int *ptr2 = (int *)((int)a + 1);//此時ptr2指向a起始地址後面一個字節
    printf("%x, %x",ptr1[-1], *ptr2);//ptr前移一個int 即a[5],ptr2取一個int長度包含了a[0]後三個字節和a[1]第一個字節
    return 0;
}

輸出結果:
5, 2000000

解釋
int *ptr1 = (int *)(&a + 1);//此時ptr1指向a末尾後面一個int
int *ptr2 = (int *)((int)a + 1);//此時ptr2指向a起始地址後面一個字節
printf("%x, %x",ptr1[-1], *ptr2);//ptr前移一個int 即a[5],ptr2取一個int長度包含了a[0]後三個字節和a[1]第一個字節

例2

int main(int argc, char **argv) {
    int a;
    int *p = &a;
    printf("%x, %x\n",p,p+1);
    printf("%x, %x",(int)p,(int)p+1);
    //printf("%x, %x",(void*)p,(void*)p+1);
    return 0;
}

輸出結果:
84fd44, 84fd48
84fd44, 84fd45

同理,(int *)和(int)步長區別

二級指針

定義:

二級指針,是一種指向指針的指針。我們可以通過它實現間接訪問數據,和改變一級
指針的指向問題。

初始化:

二級指針 實際上存儲着一級指針的地址

作用:間接數據訪問

(1)改變一級指針指向的內容
(2)改變一級指針指向
同理 N級指針 是爲了間接訪問N-1級指針數據

FILE與 sqlite3 函數設計的比較:
FILE* file = fopen(filePathStr,modeStr); // 返回句柄 不利於錯誤判斷
if()file == NULL) return -1;
sqlite3 *db = NULL;
int rc = sqlite_open(&db,sql,NULL,NULL); //錯誤碼 和句柄分開返回
if(rc == SUCCESS)

FILE 和 sqlite3 均是描述資源的句柄(即一個結構體)。獲取文件句柄的方式是通過
返回值,而獲取數據庫句柄的方式是參數。
FILE *的設計方式,並不是很好。原因是,返回值中要容錯出錯碼。而最合理的
方式,是將出錯碼和返回值分開。

步長

步長爲一級指針長度 即4 bytes

二級指針和指針數組

const修飾符修飾指針

作用增強程序的鍵壯性。但凡被修飾過的變量,其值不可修改

const 修飾指針

const 修飾變量

const 修飾變量,此時稱爲常變量。常變量,有常量的屬性,比用宏定義的常量有 了類型的屬性
特點:聲明時就必須定義,且定義之後不可修改。且發生在編譯時期。

可以通過指針修改const修飾的局部變量,但不可以修改const全局變量

const int b = 100;
int main(void)
{
	const int a = 100;
	printf("temp a = %d\n", a);
	int *pa = &a;
	*pa = 300;
	printf("after temp a = %d\n",a);

	int *pb = &b;
	printf("global b = %d\n", b);
	pb = 250;
	printf("after global  b = %d\n", b);

	system("pause");
	return 0;
}

輸出結果:
temp a = 100
after temp a = 300
global b = 100
after global  b = 100
請按任意鍵繼續. . .

const 修飾符

(1) const 修飾指針,表示指針的指向是恆定的,不可更改。

int * const p = &a;

const修飾p,指針p是不可變的,也就是p指向的內存單元不可變。即p的指向不可變,p指向的內存單元的內容可以變。
  
(2) const 修飾指針指向的值 不能改變
const int *p = a;
a的值不能改變

(3) const int* const p = &a;
*p和p都被const修飾了,所以p指向的內存單元,和p指向內存單元中存放的內容都是不可變的。

函數指針與回調函數

函數的本質

函數的本質是一段可執行性代碼段。函數名,則是指向這段代碼段的首地址。

回調函數

回調(函數作參數)
回調函數,本質也是一種函數調用,先將函數以指針的方式傳入,然後,調用。這種寫法的好處是,對外提供函數類型,而不是函數定義。這樣我們只需要依據函數類型 和函數功能提供函數就可以了。給程序的書寫帶來了很大的自由。

練習
解釋((void() ()) 0)();
地址爲0的函數強轉爲(void(*)())函數的調用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章