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

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

六、重定向機制

  • 輸出重定向

    1. 把程序的輸出流,重定向到一個新的文件中,填充文件內容【不覆蓋】:
    ./main.out >> output.txt
    //或者
    ./main.out  1>> output.txt
    
    1. 把上一步執(zhí)行的結果,重定向到一個文件中,覆蓋文件內容:
    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. 將要輸入的參數(shù)值,寫入一個文件input.txt中,然后代替鍵盤鍵入:
    ./main.out < input.txt
    

七、結構體相關

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

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

九、動態(tài)鏈表與靜態(tài)鏈表

  1. 兩者都是動態(tài)數(shù)據(jù)結構
///鏈表結點結構體
struct node{
  int data;
  node * next;
};
  1. 兩者的區(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位】
  • 用戶內存分配結構:
    1. 代碼段【內存中的最低段位】:代碼編譯后的二進制數(shù)據(jù)加載到內存中的最低位處,即:代碼段
    2. 數(shù)據(jù)段【內存中的第二低段位】:聲明一些全局變量(全局或函數(shù)中的)靜態(tài)變量或者常量,則放在了數(shù)據(jù)段處
    3. 堆【內存中的第三低段位】
    4. 自由可分配內存【內存中的第四低段位】
    5. ?!緝却嬷械牡诙叨挝?,最高段位是系統(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分配的棧地址位置高,);
操作系統(tǒng)對內存的管理和分配
  • 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
    

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

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容