大牛查漏補缺 -- C語言注意點

前言: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;
    }
    
  • 關于宏替換
    1. 特點:直接從代碼上替換字符串,不考慮c的語法。
    2. 優勢:可不考慮參數類型,如:#define ADD(a,b) (a+b),我們可以使用 ADD(1,2),也可以使用ADD(1.5,2.3)
  • 關于typedef
    1. 與宏的區別:
    2. 宏替換在預處理過程中就執行,而typedef在預編譯過程后的.i文件中,不會進行任何替換操作。
    3. 宏以下的所有代碼,都可以使用到宏,而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命令。
  • 步驟
    1. 首先,在這堆c文件所在目錄下,創建一個文件,名為Makefile
    $ vi Makefile
    
    1. 然后,輸入該文件的內容如下:
    # 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
    
    1. 保存文件,最后,在這個目錄下,執行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,表示程序執行有錯誤。
    }
    

六、重定向機制

  • 輸出重定向

    1. 把程序的輸出流,重定向到一個新的文件中,填充文件內容【不覆蓋】:
    ./main.out >> output.txt
    //或者
    ./main.out  1>> output.txt
    
    1. 把上一步執行的結果,重定向到一個文件中,覆蓋文件內容:
    ls /etc > etc.txt
    
    1. 標準輸出流和標準錯誤流分別輸出到不同文件,正常輸出為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
    
  • 輸入重定向

    1. 將要輸入的參數值,寫入一個文件input.txt中,然后代替鍵盤鍵入:
    ./main.out < input.txt
    

七、結構體相關

  1. 結構體定義與數組賦值
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}};
}
  1. 結構體指針
  • 指向單個結構體對象
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
  1. 結構體的大小【重要】
    <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。
}

八、聯合體

  1. 聯合體的定義
union data{ ///聯合體 data,它所占的內存空間為最大元素所占的空間,所以,為4byte。
  int a;
  char b;
  int c;
}

九、動態鏈表與靜態鏈表

  1. 兩者都是動態數據結構
///鏈表結點結構體
struct node{
  int data;
  node * next;
};
  1. 兩者的區別
  • 靜態鏈表寫法
    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位】
  • 用戶內存分配結構:
    1. 代碼段【內存中的最低段位】:代碼編譯后的二進制數據加載到內存中的最低位處,即:代碼段
    2. 數據段【內存中的第二低段位】:聲明一些全局變量(全局或函數中的)靜態變量或者常量,則放在了數據段處
    3. 堆【內存中的第三低段位】
    4. 自由可分配內存【內存中的第四低段位】
    5. 棧【內存中的第二高段位,最高段位是系統內存】:記錄函數當前運行時的狀態,記錄“代碼運行到第幾行,內部的變量所在內存地址等信息等(如: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
    

    以上的示例代碼中,有三個注意點,用注釋標記出來:

    1. 注意點一:p+=3; *p=4;等價于:①p[3]=4*(p+3) = 4,相當于讓p所指的地址之后的第三個地址賦上4。
    2. 注意點二和注意點四:這里就可以結合以上說的 ‘gcc對變量和內存地址指向的優化’ 來解釋,gcc將同一類型的變量放在連續分配的內存空間中排列,于是,當p指向a的地址,a地址和b地址之間,還存在變量i所在的地址(由于gcc的優化整理),所以,這里才有了p[1]=1;
    3. 注意點三:這里可以得出,指針運算中,*(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優化,ss3的內存地址實際上是相鄰的,s原始占有6個字節,而s3原始占有10個字節,那么,如果輸入大串字符,則會覆蓋s甚至s3的原來的內容甚至其他內存空間的內容,這樣一來,就會造成很嚴重的后果!!
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容