日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網(wǎng)站首頁 編程語言 正文

一起來學習C語言的程序環(huán)境與預處理_C 語言

作者:Green_756 ? 更新時間: 2022-05-27 編程語言

1.程序的翻譯環(huán)境和執(zhí)行環(huán)境

要支持c語言的實現(xiàn),會有不同的編譯器出現(xiàn),而這些編譯器都要遵循ANSI C,都存在兩種環(huán)境

第1種是翻譯環(huán)境,在這個環(huán)境中源代碼被轉換為可執(zhí)行的機器指令。 第2種是執(zhí)行環(huán)境,它用于實際執(zhí)行代碼。

.obj為后綴的就是目標文件

而一個項目中可能會有很多.c后綴的源文件,分別處理后每經(jīng)過編譯器單獨處理,然后會生成對應的目標文件(.obj),然后總體經(jīng)過連接器處理,最終變成可執(zhí)行程序。

目標文件最后還要加上鏈接庫整體一起通過鏈接器鏈接,變成可執(zhí)行程序.

鏈接庫:在編寫代碼的時候,會有一些不屬于我們自己寫的函數(shù)(如printf),這些函數(shù)是自帶的庫里面包含的,這些庫就叫鏈接庫

(補函數(shù)的聲明與定義里面的靜態(tài)庫)

從源文件生成可執(zhí)行程序的這一個過程就叫做翻譯環(huán)境

2.gcc C語言編譯器來演示編譯過程

2.1編譯

預編譯→編譯→匯編

預編譯(預處理):

文本操作:

1.頭文件的包含,#include——預編譯指令,將包含的頭文件給展開

2.刪除注釋(注釋被空格替換)

3.#define定義符號的替換

2.2編譯:

生成.s的文件

把c語言代碼轉換成匯編代碼

1.語法分析

2.詞法分析

3.語義分析

4.符號匯總——匯總的是全局符號

《程序員的自我修養(yǎng)》——通俗地講解代碼編譯過程的細節(jié)

匯編:

生成了test.o

把匯編代碼轉換成二進制指令

形成符號表:

框內(nèi)是十六進制是地址

鏈接:

最終將.o文件鏈接成.exe可執(zhí)行程序

1.合并段表

2.符號表的合并和重定位(像Add一開始地址為默認0和另一個.c文件內(nèi)的Add地址的為0x200,會重新定位)

符號表的意義:多個目標文件進行鏈接的時候會通過符號表查看來自外部的符號是否真實存在

2.3運行環(huán)境

1.程序必須載入內(nèi)存中。在有操作系統(tǒng)的環(huán)境中:一般這個由操作系統(tǒng)完成。在獨立的環(huán)境中,程序的載入必須由手工安排(電焊好伐),也可能是通過可執(zhí)行代碼置入只讀內(nèi)存來完成。

2.程序的執(zhí)行便開始。接著便調用main函數(shù)。

3.開始執(zhí)行程序代碼。這個時候程序將使用一個運行時堆棧(stack)(也就是之前博客中寫到的函數(shù)棧幀的創(chuàng)建與銷毀),存儲函數(shù)的局部變量和返回地址。程序同時也可以使用靜態(tài)(static)內(nèi)存,存儲于靜態(tài)內(nèi)存中的變量在程序的整個執(zhí)行過程一直保留他們的值。

4.終止程序。正常終止main函數(shù);也有可能是意外終止。

3詳解預處理

3.1預定義符號

  • __DATE__
  • __FILE__
  • __LINE__
  • __TIME__
  • __STDC__? ?//如果編譯器遵循ANSI C,其值為1,否則未定義

作用:記錄日志:可記錄在哪個文件在哪個日期在什么時候在哪個文件在哪一行

#define _CRT_sECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
    printf("%s\n", __FILE__);
    printf("%s\n", __TIME__);
    printf("%d\n", __LINE__);
    printf("%s\n", __DATE__);
    return 0;
}

預處理符號的應用:

//預處理符號的應用——寫日志
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
    int i = 0;
    FILE* pf = fopen("test.txt", "w");
    if (NULL == pf)
    {
        printf("error is%s\n", strerror(errno));
        return 0;
    }
    for (i = 0; i < 5; i++)
    {
        fprintf(pf, "%s\t%s\t%s\t%d\ti=%d\n", __FILE__, __DATE__, __TIME__, __LINE__, i);
    }
    fclose(pf);
    pf = NULL;
    return 0;
}

3.2#define

3.2.1#define定義標識符

兩種用法:

#define MM 100
#define reg register——關鍵字替換
 

#define末尾有時候可以加分號有時又不可以加上分號,

不可以加上分號的情況:

//不可以加上分號的情況
#define _CRT_SECURE_NO_WARNINGS 1
#define MAX(x,y) ((x)>(y)?x:y);
#include <stdio.h>
int main()
{
    int a = 5;
    int b = 3;
    printf("%d\n", MAX(a, b));
    return 0;
}

因為加上分號會使得宏在替換的時候也帶上分號,所以在調用在一些函數(shù)內(nèi)部的時候會出現(xiàn)錯誤。

綜上,當我們定義宏的時候,最好不要加分號在末尾。

3.2.2 #define定義宏

這里也是將全部參數(shù)給替換掉,在預處理的時候就替換掉了,不信的話可以在解決方案處右擊,點擊屬性后選擇預處理,然后就可以在debug里面發(fā)現(xiàn)又應該.i文件,點開后就可以發(fā)現(xiàn)這里已經(jīng)被替換掉了。

#define Max(x,y) ((x)>(y)?(x):(y))
//      Max->宏的名字  
//         x和y->宏的參數(shù)
//                ((x)>(y)?(x):(y))->宏的內(nèi)容

ps:在定義宏的內(nèi)容的時候,最好每個參數(shù)都要加上小括號,然后最后整體加上小括號,否則如果傳入?yún)?shù)不是單獨一個值而是表達式的時候,會產(chǎn)生一些沒有意料到的優(yōu)先級計算改變

Tips:宏后面的參數(shù)的小括號一定要緊挨著宏的名

3.2.3 #define替換規(guī)則

1.先看宏的參數(shù)內(nèi)是不是有define的符號,優(yōu)先替換掉define符號

2.對于宏,參數(shù)名被他們的值替換

注意!!

1.宏的參數(shù)里可以出現(xiàn)其他#define定義的符號,但不可以遞歸

2.當define掃描預處理時,字符串常量的內(nèi)容并不被搜索(也就是說字符串里面的東西是不會被宏預處理的)

3.2.4 #和##

#

相當于把宏的參數(shù)放進字符串中變成所對應的字符串

// #的用法
#define _CRT_SECURE_NO_WARNINGS 1
#define print(x) printf("the value of " #x " is %d\n",x)
#include <stdio.h>
int main()
{
    int a = 5;
    int b = 4;
    print(a);
    print(b);
    return 0;
}

##

可以把兩邊分離片段合成一個符號

#define CAT(C,num) C##num
int main()
{
    int Class104=10000;
    printf("%d\n",CAT(Class,104));
    return 0;
}

3.2.5帶副作用的宏參數(shù)

#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
    int a=3;
    int b=5;
    int m=MAX(a++,b++);//宏的參數(shù)是直接替換進去
    所以替換完之后為:
    int m=((a++)>(b++)?(a++):(b++));//會出現(xiàn)錯誤
    printf("%d\n",m);
    printf("%d %d\n",a,b);
    return 0;
}

3.2.6宏和函數(shù)對比

宏的優(yōu)點:

1.用于調用函數(shù)和從函數(shù)返回的代碼可能比實際執(zhí)行這個小型計算工作所需要的時間更多。 所以宏比函數(shù)在程序的規(guī)模和速度方面更勝一籌

2.更為重要的是函數(shù)的參數(shù)必須聲明為特定的類型。 所以函數(shù)只能在類型合適的表達式上使用。反之這個宏怎可以適用于整形、長整型、浮點型等可以 用于>來比較的類型。

宏的缺點:

1.每次使用宏的時候,一份宏定義的代碼將插入到程序中。除非宏比較短,否則可能大幅度增加程序的長度。

2.宏是沒法調試的。

3.宏由于類型無關,也就不夠嚴謹。

4.宏可能會帶來運算符優(yōu)先級的問題,導致程容易出現(xiàn)錯

可從以下方面比較宏與函數(shù)的區(qū)別:

  • 代碼長度——宏如果不是特別小的話,每一次使用的時候都要替換成宏的定義,可能會導致最終代碼特別長,大幅增長程序長度,而函數(shù)每次都只調用那一段代碼
  • 執(zhí)行速度——宏只需要執(zhí)行一行的代碼,而函數(shù)擁有調用函數(shù),執(zhí)行代碼,返回參數(shù)這三步操作,所以相對來說會慢一些
  • 操作符優(yōu)先級——由于宏是不經(jīng)過計算直接將參數(shù)傳進去的,所以在傳參后可能會有優(yōu)先級的不同導致結果與我們想要的最終結果有出入,除非加上括號。相對的函數(shù)參數(shù)只在函數(shù)調用的時候求值一次,會比較容易猜測結果。
  • 帶有副作用的參數(shù)——參數(shù)可能被替換到宏體中的多個位置,所以帶有副作用的參數(shù)求值可能會產(chǎn)生不可預料的結果。函數(shù)參數(shù)只在傳參的時候求值一次,結果更容易控制。
  • 參數(shù)類型——宏的參數(shù)類型相對自由,只要對參數(shù)的操作合法,就可以任何只要符合規(guī)定的參數(shù)類型。函數(shù)的參數(shù)類型是固定死的。
  • 調試——宏不方便調試。函數(shù)能夠調試。
  • 遞歸——宏不能夠遞歸,而函數(shù)可以遞歸

3.2.7 命名的約定

一般來講宏與函數(shù)的使用語法很類似,所以以后使用這種方法區(qū)分宏與函數(shù):

  • 宏名全部大寫
  • 函數(shù)名不要全部大寫(可開頭或部分大寫)

3.3 undef

去除一個宏定義

#undef 宏名

3.4命令行定義

許多C 的編譯器提供了一種能力,允許在命令行中定義符號。用于啟動編譯過程。 例如:當我們根據(jù)同一個源文件要編譯出不同的一個程序的不同版本的時候,這個特性有點用處。(假定某個程序中聲明了一個某個長度的數(shù)組,如果機器內(nèi)存有限,我們需要一個很小的數(shù)組,但是另外一個機器內(nèi)存大些,我們需要一個數(shù)組能夠大些。)

#include <stdio.h>
int main()
{
    int array [SZ];
    int i = 0;
    for(i = 0; i< SZ; i ++)
   {
        array[i] = i;
   }
    for(i = 0; i< SZ; i ++)
   {
        printf("%d " ,array[i]);
   }
    printf("\n" );
    return 0;
}
 

在這里我們可以知道SZ這個符號始終沒有被定義,到這里為止我們的程序還是無法運行的,會報錯,但是:

編譯指令:

//linux 環(huán)境演示
gcc -D SZ=10 programe.c

在編譯完這一行以后程序就能夠執(zhí)行了,這是因為我們在命令行中已經(jīng)將SZ這個符號定義好了。

3.5 條件編譯

有一段代碼,編譯了麻煩,刪去了可惜,這時可以選擇是否編譯,這時候就要用到條件編譯。

應用場景:當我們使用在不同系統(tǒng)時,比如在用到windows系統(tǒng)時我們需要用到這一段代碼,而在Linus系統(tǒng)上又要用到另一段代碼而不能用windows那段代碼的時候,不可以刪除,因為要實現(xiàn)一個程序的跨平臺使用,這時候就需要用到條件編譯來選擇什么時候使用哪段代碼。

Tips:我們要明確條件編譯指令也是預處理指令

//條件編譯
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
#if 1 //這里的常量非0,于是執(zhí)行,如果為0,則不執(zhí)行
        printf("%d\n", i);
#endif
        return 0;
    }
}

不能放變量,因為是預處理階段執(zhí)行的,而變量在預處理中還沒有出現(xiàn),所以我們只能放常量進去,否則放變量進去的話只能判定為0。

常見的條件編譯指令:

1.#if 常量/常量表達式? ? ?#endif

2.#if 常量表達式 #elif 常量表達式 #else 常量表達式? ? #endif

3.判斷是否被定義:只要你定義了宏就為真

  • #if defined(宏名) ...#endif #ifdef 宏名 ... #endif
  • 如果要做到的是沒有定義,則在前面加上!

#if !defined(宏名) ...#endif? ? ? ? ? ? ?#ifdef !宏名 ... #endif

4.嵌套指令

//全部類型的條件編譯
#define _CRT_SECURE_NO_WARNINGS 1
#define VALUE 200
#define TEST 20
#include <stdio.h>
int main()
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
#if 1
        printf("%d\n", i);
#endif
    }
#if VALUE<100
    printf("value<100\n");
#elif VALUE>=100&&VALUE<=150
    printf("value>=100且value<=150\n");
#else
    printf("value>150\n");
#endif
#ifdef VALUE
    printf("VALUE已定義\n");
#else
    printf("VALUE未定義\n");
#endif
#if defined(VALUES)
    printf("VALUES已定義\n");
#else
    printf("VALUES未定義\n");
#endif
    //嵌套指令
#if VALUE<=150
#if defined(VALUE)
    printf("VALUE小于或等于150,VALUE已定義\n");
#else
    printf("VALUE小于或等于150,VALUE未定義\n");
#endif
#elif VALUE>150&&VALUE<=200
#ifdef TEST
    printf("VALUE大于150且小于等于200,TEST已定義\n");
#else
    printf("VALUE大于150且小于等于200,TEST未定義\n");
#endif
#endif
    return 0;
}

3.6文件包含

我們知道在預編譯時會包含頭文件,而頭文件例如<stdio.h>從預編譯.i文件上看我們可以知道有2000多行代碼,若真的重復包含了五六次,則代碼量直接上升到了一萬多行,這時候就會使得代碼過于冗長,同時也占用很多內(nèi)存,這個時候我們就需要文件包含來確認是否重復包含了同一個頭文件。

方法一:

//在頭文件中
#ifndef __TEST_H__
#define __TEST_H__
int Add(int x,int y);
#endif

方法二:

//在頭文件中
#pragma once
int Add(int x,int y);
 

ps:這個#pragma once是比較高級的寫法,在遠古編譯器里面是無法使用的(如vc)

3.6.1頭文件被包含的方式

在小綠本人之前的三子棋以及掃雷的博客中都有自己創(chuàng)造頭文件而我們在引用自己創(chuàng)造的頭文件時是以#include "fun.h" 這樣的形式引用的,但是在引用庫函數(shù)時確實以#include <stdio.h>的方式引用,那么用不同的符號引用頭文件有什么不一樣呢?

""的查找策略

""的查找策略是:先在源文件所在的目錄下查找,如果沒有找到,編譯器就像查找?guī)旌瘮?shù)頭文件一樣在標準位置查找頭文件。再如果找不到就提示編譯錯誤。

<>的查找策略

查找頭文件直接去標準位置下查找,如果找不到就直接提示編譯錯誤。

總結

原文鏈接:https://blog.csdn.net/Green_756/article/details/123674688

欄目分類
最近更新