前言:C語言是Java、Objective-C、C++等高級語言的基礎、也是跨平臺開發(fā)的基礎,指針是C語言的重中之重,
&a表示a變量所在地址,*p表示指針p指向的地址的內容……這些常用的、常見的東西我們都比較清晰,這里就再整理一下C相關的注意點和一些技巧,當做知識點的鞏固和完善吧。
C與Linux重溫
一、C 編譯過程
.c文件 -> (預處理) -> .i文件 -> (編譯) -> .s文件 -> (匯編) -> .o文件 -> (鏈接) -> 可執(zhí)行文件
-
$ gcc -o main.i main.c -E或$ gcc -E main.c -o main.i:表示只執(zhí)行到預處理完成階段,生成.i文件。這個過程,是一個處理過程,①展開頭文件:將頭文件中的內容,添加到源代碼中,而不是以頭文件的形式存在;②進行宏替換:單純的字符串替換,預處理階段,宏不會考慮c的語法,如下例子可以說明。
上述代碼經過預處理之后,變成了:#include <stdio.h> #define R 10 #define M int main( M) { int a = R; //.... return 0; }........ <stdio.h>文件所展開的內容,這里忽略 ........ int main() { int a = 10; ///R 被替換成了 10 //......... return 0; } - 關于宏替換
- 特點:直接從代碼上替換字符串,不考慮c的語法。
- 優(yōu)勢:可不考慮參數(shù)類型,如:
#define ADD(a,b) (a+b),我們可以使用ADD(1,2),也可以使用ADD(1.5,2.3)。
- 關于typedef
- 與宏的區(qū)別:
- 宏替換在預處理過程中就執(zhí)行,而typedef在預編譯過程后的.i文件中,不會進行任何替換操作。
- 宏以下的所有代碼,都可以使用到宏,而typedef的作用域有限,如果定義在方法體中,就只能在方法中生效。
二、Linux相關命令
- ls -l : 輸出當前目錄所有文件(名字或者權限等信息系)
- echo $? :查看上一條執(zhí)行的語句的返回碼:0表示執(zhí)行成功
- cat filename: 讀取文件filename的內容,并顯示到終端
- gcc main.c && ./main.c : 這個
&&表示,前一句執(zhí)行成功,后一句才會開始執(zhí)行
三、關于makefile文件
- 復習:關于
gcc命令選項:-
gcc xxx -o filename:表示將xxx文件經過處理,輸出到名為filename的文件。(.m 結尾:Objective-C源碼文件) -
gcc -E hello.c -o hello.i:-E表示只進行到‘預處理’階段,生成.i結尾的預處理后的C源碼文件。(.ii 結尾:預處理后的C++源碼文件) -
gcc -S hello.c -o hello.s:-S表示只進行到‘匯編’階段,生成.s結尾的匯編語言源代碼文件。(-S 結尾:預處理后的匯編語言源碼文件) -
gcc -c hello.c -o hello.o:-c表示只進行到‘編譯’階段,生成.o結尾的編譯后的目標文件。 -
gcc hello.c -o hello:直接生成可執(zhí)行文件。 -
gcc -g hello.c -o hello: 可執(zhí)行文件中加入調試信息
-
- 意義:
將需要編譯的多組.o和.c文件,他們的編譯規(guī)則和編譯順序寫好在一個文件中,這樣就代替了繁雜的gcc命令。 - 步驟
- 首先,在這堆c文件所在目錄下,創(chuàng)建一個文件,名為Makefile
$ vi Makefile- 然后,輸入該文件的內容如下:
# this is make file main.out:lib1.o lib2.o main.c -o hello.out [兩個Tab,表示8個空格]gcc lib1.o lib2.o main.c lib1.o:lib1.c [兩個Tab,表示8個空格]gcc -c lib1.c lib2.o:lib2.c [兩個Tab,表示8個空格]gcc -c lib2.c- 保存文件,最后,在這個目錄下,執(zhí)行
make命令:
make
四、關于C的main函數(shù)
///其中:argv表示執(zhí)行時帶有的參數(shù)個數(shù),argc表示執(zhí)行時所帶參數(shù)列表。
int main(int argv, char* argc[]){
//doSomething;
return 0;///執(zhí)行之后,返回值(0表示成功)
}
例如執(zhí)行:./ main.out -a -l -d,那么,argv=4,argc分別為:./main.out,-a,-l,-d。
五、C的標準輸入輸出流和錯誤流
我們知道,include <stdio.h>的這個頭文件,在我們執(zhí)行應用程序的瞬間,操作系統(tǒng)為程序啟動了一個進程,之后,當包含進這個頭文件后,它會給我們提供一系列指針,指向資源,應用程序被啟動時,他會為我們創(chuàng)建三個文件,分別是:stdin、stdout、stderr,分別對應于:標準輸入、輸出和錯誤流,負責為我們的應用程序提供輸入和輸出數(shù)據(jù)的能力。
- stdin:標準輸出流,默認是我們的屏幕顯示器終端
printf("hello\n");底層是:fprintf(stdin, "hello\n"); - stdout:標準輸入流,默認設備是我們的鍵盤
scanf("%d", &n);底層是:fscanf(stdout, "%d", &n); - stderr:標準錯誤流,報告程序出錯時的輸出操作:
if(……){ fprintf(stderr, "error!"); return 1;//這里很關鍵,返回不是0,表示程序執(zhí)行有錯誤。 }
六、重定向機制
-
輸出重定向
- 把程序的輸出流,重定向到一個新的文件中,填充文件內容【不覆蓋】:
./main.out >> output.txt //或者 ./main.out 1>> output.txt- 把上一步執(zhí)行的結果,重定向到一個文件中,覆蓋文件內容:
ls /etc > etc.txt- 標準輸出流和標準錯誤流分別輸出到不同文件,正常輸出為true.txt ,錯誤輸出為fail.txt:
main.c代碼如下:
#Include<stdio.h> int main(){ int a,b; scanf("%d", &a); scanf("%d", &b); if(0!=b) printf("a/b=%d\n", a/b); else{ fprintf(stderr, "j!=0\n"); return 1; } return 0; }命令行輸入如下:
$ cc main.c -o main.out $ ./main 1>true.txt 2>fail.txt ////如果有輸入的文件,還可以: // $ ./main 1>true.txt 2>fail.txt <input.txt -
輸入重定向
- 將要輸入的參數(shù)值,寫入一個文件input.txt中,然后代替鍵盤鍵入:
./main.out < input.txt
七、結構體相關
- 結構體定義與數(shù)組賦值
struct person{
char name[20];
int age;
};////可以在“;”號前直接定義一個全局變量為me
.......
int main()
{
struct person team[2] = {"xiao_ming", 20, "hua_zai",18};
//或者:struct person team[2] = {{"xiao_ming", 20}, {"hua_zai",18}};
}
- 結構體指針
- 指向單個結構體對象
struct person xiaoming = {"xiaoming", 19};
struct person * p1;
p1 = &xiaoming;
printf("name=%s\n", (*p1).name);///也可以寫成:p1->name 或者 xiaoming.name ,這三種方法是等價的。
- 指向結構體數(shù)組對象
struct person team[2] = {{"xiao_ming", 20}, {"hua_zai",18}};
struct person * p;
p = team; //沒有了‘&’
printf("name=%s\n", p->name);///相當于 team[0].name
printf("name=%s\n", (++p)->name);///相當于 team[1].name
- 結構體的大小【重要】
<u>結構體大小 = 結構體最后一個成員的偏移量 + 最后一個成員的大小 + 末尾的填充字節(jié)數(shù)</u>
例子:
struct data{
int a;///偏移量為0
char b;///偏移量為4【因為偏移量‘4’是b本身大小‘1’的整數(shù)倍,所以,編譯器不會在成員a和b之間填充字節(jié)】
int c;///偏移量為5 --> 8【編譯器在b后面填充了字節(jié),本來是5,最后變成8,因為8才是4的整數(shù)倍】
///這樣一來,整個data結構體的大小是 4+4(1+3)+4=12 ,而判斷得出:12%4(最寬的基本類型int的大小) == 0,所以,最終大小不會再被編譯器填充字節(jié),就是12。
}
八、聯(lián)合體
- 聯(lián)合體的定義
union data{ ///聯(lián)合體 data,它所占的內存空間為最大元素所占的空間,所以,為4byte。
int a;
char b;
int c;
}
九、動態(tài)鏈表與靜態(tài)鏈表
- 兩者都是動態(tài)數(shù)據(jù)結構
///鏈表結點結構體
struct node{
int data;
node * next;
};
- 兩者的區(qū)別
- 靜態(tài)鏈表寫法
main方法中:
///main方法中
struct node a,b,c, * head;
a.data = 1;
b.data = 2;
c.data = 3;
a.next = b;
b.next = c;
c.next = NULL;
head = &a;
///以head為頭結點指針的靜態(tài)鏈表創(chuàng)建成功
///...............
struct node *p = head;
while(p->NULL){
printf("%d\n", p->data);
p = p->next;
}
- 動態(tài)鏈表寫法
include <stdio.h>
include <stdlib.h>//當中有malloc相關api
///鏈表結點結構體
struct node{
int data;
node * next;
};
///創(chuàng)建鏈表的函數(shù)
struct node * create(){
struct node head;
struct node p1,p2;
int n = 0;
p1 = p2 = (struct node)malloc(sizeof(struct node));
scanf("%d", &p1->data);
head = NULL;
while(p1->data!=0){
n++;
if(n==1) head = p1;
else p2->next = p1;
p2 = p1;
p1 = (struct node*)malloc(sizeof(struct node));
scanf("%d", &p1->data);
}
p2 ->next = NULL;
return (head);
}
///main方法中
int main(){
struct node * p;
p =create();
printf("第一個結點的信息:%d\n", p->data);
return 0;
}
```
十、C語言的變量存儲類別
- 根據(jù)變量的生命周期來劃分,分為:靜態(tài)存儲方式和動態(tài)存儲方式
- 靜態(tài)存儲方式:指在程序運行期間分配固定的存儲空間的方式。存放了在整個程序執(zhí)行過程中都存在的變量(如:全局變量)
- 動態(tài)存儲方式:在程序運行期間更具需要進行動態(tài)的分配存儲空間的方式。動態(tài)存儲區(qū)中存放的變量是根據(jù)程序運行的需要而建立和釋放的。(如:函數(shù)形參、自動變量、函數(shù)調用時的現(xiàn)場保護和返回地址等)
- C語言中的存儲類別:自動(auto)、靜態(tài)(static)、寄存器(register)和外部(extern)
自動(auto): 用關鍵字auto定義的變量為自動變量,auto可以省略,auto不寫則隱含定為“自動存儲類別”,屬于動態(tài)存儲方式。如:
auto int a;相當于int a;-
靜態(tài)(static): 用static修飾的為靜態(tài)變量,如果定義在函數(shù)內部的,稱之為靜態(tài)局部變量;如果定義在函數(shù)外部,稱之為靜態(tài)外部變量。如下為靜態(tài)局部變量。
int func(){ static int a = 1; a++; printf("a = %d\n", a); } int main(){ int i = 0; for(;i<10;i++){ func(); }///最終得到:a = 1,a = 2,..., a = 10 }注意:靜態(tài)局部變量屬于靜態(tài)存儲類別,在靜態(tài)存儲區(qū)內分配存儲單元,在程序整個運行期間都不釋放;靜態(tài)局部變量在編譯時賦初值,即只賦初值一次;如果在定義局部變量時不賦初值的話,則對靜態(tài)局部變量來說,編譯時自動賦初值0(對數(shù)值型變量)或空字符(對字符變量)。
-
寄存器(register): 為了提高效率,C語言允許將局部變量得值放在CPU中的寄存器中,這種變量叫“寄存器變量”,用關鍵字register作聲明。如
register int a = 1;注意:只有局部自動變量和形參可以作為寄存器變量;一個計算機系統(tǒng)中的寄存器數(shù)目有限,不能定義任意多個寄存器變量;局部靜態(tài)變量不能定義為寄存器變量。
-
外部(extern): 用extern聲明的的變量是外部變量,外部變量的意義是某函數(shù)可以調用在該函數(shù)之后定義的變量。
int main(){ extern int b; printf("%d\n" , b);///實際上是調用main函數(shù)之后的代碼中的全局變量b,結果為100. } int b = 100;
十一、C語言的內部函數(shù)與外部函數(shù)
-
內部函數(shù):
- 定義:在C語言中不能被其他源文件調用的函數(shù)稱謂**內部函數(shù) **,內部函數(shù)由static關鍵字來定義,因此又被稱謂靜態(tài)函數(shù)。
- 形式:static [數(shù)據(jù)類型] 函數(shù)名([參數(shù)])
-
外部函數(shù):
- 定義:能被其他源文件調用的函數(shù)稱謂**外部函數(shù) **,外部函數(shù)由extern關鍵字來定義。
- 形式:extern [數(shù)據(jù)類型] 函數(shù)名([參數(shù)])
C語言規(guī)定,在沒有指定函數(shù)的作用范圍時,系統(tǒng)會默認認為是外部函數(shù),因此當需要定義外部函數(shù)時extern也可以省略C語言規(guī)定,在沒有指定函數(shù)的作用范圍時,系統(tǒng)會默認認為是外部函數(shù),因此當需要定義外部函數(shù)時extern也可以省略。
Linux 操作系統(tǒng)下對于C語言的內存管理和分配
一、32位和64位操作系統(tǒng)
操作系統(tǒng)理論上會將我們安置的內存條的所有地址空間進行編號(從000……0 到 111……1),直到它所能區(qū)分的位置總數(shù)(比如:我插了兩條4G內存條到64位OS下的電腦時,理論上,就能將整個內存區(qū)分成 2的33次方 個位置)
- 32位操作系統(tǒng)只能使用4G內存(由于CPU的地址總線為32位,對應的尋址空間大小為2的32次方,也就是說:“我最多區(qū)分出 2的32次方 個不同的位置”)
- 64位操作系統(tǒng)可以使用足夠大的內存
二、用戶內存隔離
- 首先, 內存管理和分配是由操作系統(tǒng)來幫我們完成的。
- 64位操作系統(tǒng)中,對于內存分配:
- 0xffffffffffffffff ~ 0x8000000000000000:系統(tǒng)程序使用內存【前16位】
- 0x7fffffffffffffff ~ 0x00:用戶程序使用內存【后48位】
- 用戶內存分配結構:
- 代碼段【內存中的最低段位】:代碼編譯后的二進制數(shù)據(jù)加載到內存中的最低位處,即:代碼段
- 數(shù)據(jù)段【內存中的第二低段位】:聲明一些全局變量、(全局或函數(shù)中的)靜態(tài)變量或者常量,則放在了數(shù)據(jù)段處
- 堆【內存中的第三低段位】:
- 自由可分配內存【內存中的第四低段位】
- ?!緝却嬷械牡诙叨挝?,最高段位是系統(tǒng)內存】:記錄函數(shù)當前運行時的狀態(tài),記錄“代碼運行到第幾行,內部的變量所在內存地址等信息等(如:main函數(shù)開頭連續(xù)聲明兩個int類型變量a和b,那么,a和b所在內存地址數(shù)之間差為4個字節(jié))”。一個函數(shù)可能被多次調用,而每次調用函數(shù),都是一個獨立的棧;先聲明的函數(shù)所處內存地址位置低,后聲明的函數(shù)所處內存地址位置高,而系統(tǒng)對棧的地址分配則相反,先分配的棧所在地址更高(如:main方法調用方法A,那么對main方法分配的棧比對方法A分配的棧地址位置高,);

- gcc對內存分配的優(yōu)化
- 首先,在程序代碼編譯后,gcc編譯器會將零碎的聲明的所有非靜態(tài)變量,按照類型進行歸類,同一類型的變量聲明在一塊,然后去為每一類的變量集合分配內存。這樣一來,同一類型的變量實際在內存的??臻g中的位置是相鄰的。
- 對C語言來說,32位OS中,指針變量占4byte;64位OS中,指針變量占8byte。
三、函數(shù)棧、函數(shù)指針
-
(一)函數(shù)棧
?C中的函數(shù)調用,每一個函數(shù)的調用,系統(tǒng)都會為其分配一個棧,用于存放這個函數(shù)的信息(目前執(zhí)行第幾行、成員變量的信息等),這個就是函數(shù)棧。【注意:函數(shù)內部的靜態(tài)變量位于內存中的數(shù)據(jù)段,而非棧區(qū)】 -
(二)函數(shù)指針
?看個例子:
#include<stdio.h>
int func(int a){
return a*a;
}
int main(){
int b = 3;
int res;
res = (*func)(b);
printf("%d\n",res);
return 0;
}
這里的res = (*func)(b);指的就是:調用func(3),并將返回值賦給res。其中,func表示func函數(shù)所在代碼段中的地址本身,(*func)表示找到這個func名對象對應的代碼段中此函數(shù)打包塊,相當于獲取到整個函數(shù)。
四、指針運算
-
(一)高效率的指針偏移
前面說過,由于gcc對變量指向內存地址的優(yōu)化,同一類對象會被歸在一段連續(xù)分配的內存中。為什么說“高效率”?因為每次指針偏移“1”,就能準確地定位到原地址的下一個對象存儲的首地址,此過程是根據(jù)類型大小而適配的,相對于for循環(huán),指針偏移只需要內部執(zhí)行一條偏移語句即可,所以會“更高效率”。
?下面看看這個示例:#include<stdio.h> int main(){ int a = 3; int b = 2; int arr[3]; int *p = &a; arr[0] = 1; arr[1] = 10; arr[2] = 100; p+=3;///注意點一 *p = 4; p = &a; int i;///注意點二 for(i=0;i<6;i++){ printf("*p = %d , p[%d] = %d\n", *(p+i), i, p[i]);///注意點三 } return 0; }打印結果為:
*p = 3 , p[0] = 3 *p = 1 , p[1] = 1 //注意點四 *p = 2 , p[2] = 2 *p = 4 , p[3] = 4 *p = 10 , p[4] = 10 *p = 100 , p[5] = 100以上的示例代碼中,有三個注意點,用注釋標記出來:
- 注意點一:
p+=3; *p=4;等價于:①p[3]=4②*(p+3) = 4,相當于讓p所指的地址之后的第三個地址賦上4。 - 注意點二和注意點四:這里就可以結合以上說的 ‘gcc對變量和內存地址指向的優(yōu)化’ 來解釋,gcc將同一類型的變量放在連續(xù)分配的內存空間中排列,于是,當p指向a的地址,a地址和b地址之間,還存在變量i所在的地址(由于gcc的優(yōu)化整理),所以,這里才有了
p[1]=1; - 注意點三:這里可以得出,指針運算中,
*(p+n)和p[n]效果是一樣的。
注意:這里專指Linux64位系統(tǒng)下gcc對C語言的相關優(yōu)化支持,在MacOS或其他系統(tǒng)下,可能會有一定差異。
- 注意點一:
(二)字符數(shù)組和指針字符串
看看一下示例:
#include<stdio.h>
int main(){
char s[] = "hello";
char *s2 = "world";
char s3[10];
scanf("%s", s3);
printf("%s, %s ,%s\n", s,s2,s3);
}
注意點:
- 首先,這段代碼,三個變量s、s1、s2都是可以正常輸出的。
- s[] 被當成一個值,而 s2 被當成一個指針,指向這個“world”的首地址。
- 此時,可以看出,指針和字符數(shù)組可以適當混用,在輸入s3時,可知,s3本身指代一個內存地址,所以不用加“&”符號【 s 同理】。
- 但是,如果是要輸入到s2中,就不行了。因為C語言中,字符數(shù)組的大小等于字符數(shù)加上“\0”,這個“\0”是結束符號(比如上述的
s[]的大小就是6個字節(jié)),而*s2本身是指針類型,指向的是內存的代碼段中的“world”(由gcc編譯時決定的,會認為s2指向的是"world"字符串常量),而不是??臻g,而代碼段中的成員是沒有修改權限的,棧和堆中的對象才可以修改。 -
溢出的情況:如果是
scanf("%s" , s);,那么如果輸入超過6個字符,那么,原來s的末尾的\0結束符將被覆蓋,而上述示例中,由于gcc優(yōu)化,s和s3的內存地址實際上是相鄰的,s原始占有6個字節(jié),而s3原始占有10個字節(jié),那么,如果輸入大串字符,則會覆蓋s甚至s3的原來的內容甚至其他內存空間的內容,這樣一來,就會造成很嚴重的后果??!