前言:C語言是Java、Objective-C、C++等高級語言的基礎、也是跨平臺開發的基礎,指針是C語言的重中之重,
&a
表示a變量所在地址,*p
表示指針p指向的地址的內容……這些常用的、常見的東西我們都比較清晰,這里就再整理一下C相關的注意點和一些技巧,當做知識點的鞏固和完善吧。
C與Linux重溫
一、C 編譯過程
.c文件 -> (預處理) -> .i文件 -> (編譯) -> .s文件 -> (匯編) -> .o文件 -> (鏈接) -> 可執行文件
-
$ gcc -o main.i main.c -E
或$ gcc -E main.c -o main.i
:表示只執行到預處理完成階段,生成.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的語法。
- 優勢:可不考慮參數類型,如:
#define ADD(a,b) (a+b)
,我們可以使用ADD(1,2)
,也可以使用ADD(1.5,2.3)
。
- 關于typedef
- 與宏的區別:
- 宏替換在預處理過程中就執行,而typedef在預編譯過程后的.i文件中,不會進行任何替換操作。
- 宏以下的所有代碼,都可以使用到宏,而typedef的作用域有限,如果定義在方法體中,就只能在方法中生效。
二、Linux相關命令
- ls -l : 輸出當前目錄所有文件(名字或者權限等信息系)
- echo $? :查看上一條執行的語句的返回碼:0表示執行成功
- cat filename: 讀取文件filename的內容,并顯示到終端
- gcc main.c && ./main.c : 這個
&&
表示,前一句執行成功,后一句才會開始執行
三、關于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
:直接生成可執行文件。 -
gcc -g hello.c -o hello
: 可執行文件中加入調試信息
-
- 意義:
將需要編譯的多組.o和.c文件,他們的編譯規則和編譯順序寫好在一個文件中,這樣就代替了繁雜的gcc
命令。 - 步驟
- 首先,在這堆c文件所在目錄下,創建一個文件,名為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
- 保存文件,最后,在這個目錄下,執行
make
命令:
make
四、關于C的main函數
///其中:argv表示執行時帶有的參數個數,argc表示執行時所帶參數列表。
int main(int argv, char* argc[]){
//doSomething;
return 0;///執行之后,返回值(0表示成功)
}
例如執行:./ main.out -a -l -d
,那么,argv=4,argc分別為:./main.out
,-a
,-l
,-d
。
五、C的標準輸入輸出流和錯誤流
我們知道,include <stdio.h>
的這個頭文件,在我們執行應用程序的瞬間,操作系統為程序啟動了一個進程,之后,當包含進這個頭文件后,它會給我們提供一系列指針,指向資源,應用程序被啟動時,他會為我們創建三個文件,分別是:stdin、stdout、stderr,分別對應于:標準輸入、輸出和錯誤流,負責為我們的應用程序提供輸入和輸出數據的能力。
- stdin:標準輸出流,默認是我們的屏幕顯示器終端
printf("hello\n");
底層是:fprintf(stdin, "hello\n");
- stdout:標準輸入流,默認設備是我們的鍵盤
scanf("%d", &n);
底層是:fscanf(stdout, "%d", &n);
- stderr:標準錯誤流,報告程序出錯時的輸出操作:
if(……){ fprintf(stderr, "error!"); return 1;//這里很關鍵,返回不是0,表示程序執行有錯誤。 }
六、重定向機制
-
輸出重定向
- 把程序的輸出流,重定向到一個新的文件中,填充文件內容【不覆蓋】:
./main.out >> output.txt //或者 ./main.out 1>> output.txt
- 把上一步執行的結果,重定向到一個文件中,覆蓋文件內容:
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
-
輸入重定向
- 將要輸入的參數值,寫入一個文件input.txt中,然后代替鍵盤鍵入:
./main.out < input.txt
七、結構體相關
- 結構體定義與數組賦值
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 ,這三種方法是等價的。
- 指向結構體數組對象
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>結構體大小 = 結構體最后一個成員的偏移量 + 最后一個成員的大小 + 末尾的填充字節數</u>
例子:
struct data{
int a;///偏移量為0
char b;///偏移量為4【因為偏移量‘4’是b本身大小‘1’的整數倍,所以,編譯器不會在成員a和b之間填充字節】
int c;///偏移量為5 --> 8【編譯器在b后面填充了字節,本來是5,最后變成8,因為8才是4的整數倍】
///這樣一來,整個data結構體的大小是 4+4(1+3)+4=12 ,而判斷得出:12%4(最寬的基本類型int的大小) == 0,所以,最終大小不會再被編譯器填充字節,就是12。
}
八、聯合體
- 聯合體的定義
union data{ ///聯合體 data,它所占的內存空間為最大元素所占的空間,所以,為4byte。
int a;
char b;
int c;
}
九、動態鏈表與靜態鏈表
- 兩者都是動態數據結構
///鏈表結點結構體
struct node{
int data;
node * next;
};
- 兩者的區別
- 靜態鏈表寫法
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為頭結點指針的靜態鏈表創建成功
///...............
struct node *p = head;
while(p->NULL){
printf("%d\n", p->data);
p = p->next;
}
- 動態鏈表寫法
include <stdio.h>
include <stdlib.h>//當中有malloc相關api
///鏈表結點結構體
struct node{
int data;
node * next;
};
///創建鏈表的函數
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語言的變量存儲類別
- 根據變量的生命周期來劃分,分為:靜態存儲方式和動態存儲方式
- 靜態存儲方式:指在程序運行期間分配固定的存儲空間的方式。存放了在整個程序執行過程中都存在的變量(如:全局變量)
- 動態存儲方式:在程序運行期間更具需要進行動態的分配存儲空間的方式。動態存儲區中存放的變量是根據程序運行的需要而建立和釋放的。(如:函數形參、自動變量、函數調用時的現場保護和返回地址等)
- C語言中的存儲類別:自動(auto)、靜態(static)、寄存器(register)和外部(extern)
自動(auto): 用關鍵字auto定義的變量為自動變量,auto可以省略,auto不寫則隱含定為“自動存儲類別”,屬于動態存儲方式。如:
auto int a;
相當于int a;
-
靜態(static): 用static修飾的為靜態變量,如果定義在函數內部的,稱之為靜態局部變量;如果定義在函數外部,稱之為靜態外部變量。如下為靜態局部變量。
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 }
注意:靜態局部變量屬于靜態存儲類別,在靜態存儲區內分配存儲單元,在程序整個運行期間都不釋放;靜態局部變量在編譯時賦初值,即只賦初值一次;如果在定義局部變量時不賦初值的話,則對靜態局部變量來說,編譯時自動賦初值0(對數值型變量)或空字符(對字符變量)。
-
寄存器(register): 為了提高效率,C語言允許將局部變量得值放在CPU中的寄存器中,這種變量叫“寄存器變量”,用關鍵字register作聲明。如
register int a = 1;
注意:只有局部自動變量和形參可以作為寄存器變量;一個計算機系統中的寄存器數目有限,不能定義任意多個寄存器變量;局部靜態變量不能定義為寄存器變量。
-
外部(extern): 用extern聲明的的變量是外部變量,外部變量的意義是某函數可以調用在該函數之后定義的變量。
int main(){ extern int b; printf("%d\n" , b);///實際上是調用main函數之后的代碼中的全局變量b,結果為100. } int b = 100;
十一、C語言的內部函數與外部函數
-
內部函數:
- 定義:在C語言中不能被其他源文件調用的函數稱謂**內部函數 **,內部函數由static關鍵字來定義,因此又被稱謂靜態函數。
- 形式:static [數據類型] 函數名([參數])
-
外部函數:
- 定義:能被其他源文件調用的函數稱謂**外部函數 **,外部函數由extern關鍵字來定義。
- 形式:extern [數據類型] 函數名([參數])
C語言規定,在沒有指定函數的作用范圍時,系統會默認認為是外部函數,因此當需要定義外部函數時extern也可以省略C語言規定,在沒有指定函數的作用范圍時,系統會默認認為是外部函數,因此當需要定義外部函數時extern也可以省略。
Linux 操作系統下對于C語言的內存管理和分配
一、32位和64位操作系統
操作系統理論上會將我們安置的內存條的所有地址空間進行編號(從000……0 到 111……1),直到它所能區分的位置總數(比如:我插了兩條4G內存條到64位OS下的電腦時,理論上,就能將整個內存區分成 2的33次方 個位置)
- 32位操作系統只能使用4G內存(由于CPU的地址總線為32位,對應的尋址空間大小為2的32次方,也就是說:“我最多區分出 2的32次方 個不同的位置”)
- 64位操作系統可以使用足夠大的內存
二、用戶內存隔離
- 首先, 內存管理和分配是由操作系統來幫我們完成的。
- 64位操作系統中,對于內存分配:
- 0xffffffffffffffff ~ 0x8000000000000000:系統程序使用內存【前16位】
- 0x7fffffffffffffff ~ 0x00:用戶程序使用內存【后48位】
- 用戶內存分配結構:
- 代碼段【內存中的最低段位】:代碼編譯后的二進制數據加載到內存中的最低位處,即:代碼段
- 數據段【內存中的第二低段位】:聲明一些全局變量、(全局或函數中的)靜態變量或者常量,則放在了數據段處
- 堆【內存中的第三低段位】:
- 自由可分配內存【內存中的第四低段位】
- 棧【內存中的第二高段位,最高段位是系統內存】:記錄函數當前運行時的狀態,記錄“代碼運行到第幾行,內部的變量所在內存地址等信息等(如:main函數開頭連續聲明兩個int類型變量a和b,那么,a和b所在內存地址數之間差為4個字節)”。一個函數可能被多次調用,而每次調用函數,都是一個獨立的棧;先聲明的函數所處內存地址位置低,后聲明的函數所處內存地址位置高,而系統對棧的地址分配則相反,先分配的棧所在地址更高(如:main方法調用方法A,那么對main方法分配的棧比對方法A分配的棧地址位置高,);
- gcc對內存分配的優化
- 首先,在程序代碼編譯后,gcc編譯器會將零碎的聲明的所有非靜態變量,按照類型進行歸類,同一類型的變量聲明在一塊,然后去為每一類的變量集合分配內存。這樣一來,同一類型的變量實際在內存的棧空間中的位置是相鄰的。
- 對C語言來說,32位OS中,指針變量占4byte;64位OS中,指針變量占8byte。
三、函數棧、函數指針
-
(一)函數棧
?C中的函數調用,每一個函數的調用,系統都會為其分配一個棧,用于存放這個函數的信息(目前執行第幾行、成員變量的信息等),這個就是函數棧。【注意:函數內部的靜態變量位于內存中的數據段,而非棧區】 -
(二)函數指針
?看個例子:
#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函數所在代碼段中的地址本身,(*func)
表示找到這個func名對象對應的代碼段中此函數打包塊,相當于獲取到整個函數。
四、指針運算
-
(一)高效率的指針偏移
前面說過,由于gcc對變量指向內存地址的優化,同一類對象會被歸在一段連續分配的內存中。為什么說“高效率”?因為每次指針偏移“1”,就能準確地定位到原地址的下一個對象存儲的首地址,此過程是根據類型大小而適配的,相對于for
循環,指針偏移只需要內部執行一條偏移語句即可,所以會“更高效率”。
?下面看看這個示例:#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對變量和內存地址指向的優化’ 來解釋,gcc將同一類型的變量放在連續分配的內存空間中排列,于是,當p指向a的地址,a地址和b地址之間,還存在變量i所在的地址(由于gcc的優化整理),所以,這里才有了
p[1]=1;
- 注意點三:這里可以得出,指針運算中,
*(p+n)
和p[n]
效果是一樣的。
注意:這里專指Linux64位系統下gcc對C語言的相關優化支持,在MacOS或其他系統下,可能會有一定差異。
- 注意點一:
(二)字符數組和指針字符串
看看一下示例:
#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”的首地址。
- 此時,可以看出,指針和字符數組可以適當混用,在輸入s3時,可知,s3本身指代一個內存地址,所以不用加“&”符號【 s 同理】。
- 但是,如果是要輸入到s2中,就不行了。因為C語言中,字符數組的大小等于字符數加上“\0”,這個“\0”是結束符號(比如上述的
s[]
的大小就是6個字節),而*s2
本身是指針類型,指向的是內存的代碼段中的“world”(由gcc編譯時決定的,會認為s2
指向的是"world"字符串常量),而不是棧空間,而代碼段中的成員是沒有修改權限的,棧和堆中的對象才可以修改。 -
溢出的情況:如果是
scanf("%s" , s);
,那么如果輸入超過6個字符,那么,原來s
的末尾的\0
結束符將被覆蓋,而上述示例中,由于gcc優化,s
和s3
的內存地址實際上是相鄰的,s
原始占有6個字節,而s3
原始占有10個字節,那么,如果輸入大串字符,則會覆蓋s
甚至s3
的原來的內容甚至其他內存空間的內容,這樣一來,就會造成很嚴重的后果!!