C语言基础15——程序环境和预处理。程序环境讲解、宏讲解、条件编译、文件包含、offsetof宏实现

目录

程序的翻译环境和执行环境

翻译环境

编译的三个阶段

编译的每个阶段

运行环境

预处理详解

预定义符号

#define

#define定义标识符

#define定义宏

#define替换规则

#和##

带副作用的宏

宏和函数的对比

#undef —用于移除一个宏定义

命令行定义(了解)

条件编译

常见的条件编译指令

文件包含

头文件被包含的方式

嵌套文件包含

解决头文件重复包含问题

其他预处理指令

练习

二进制数的奇偶数位交换宏实现

offsetof宏的实现


程序的翻译环境和执行环境

在ANSI C的任何一种实现中,都存在两个不同的环境:

第一种:翻译环境,在这个环境中源代码被转换为可执行的机器指令。

第二种:执行环境,它用于实际代码执行。

翻译环境

在这里插入图片描述

/*
 * 组成一个程序的每个源文件通过编译过程,分别转换成目标代码。(object code)
 * 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
 * 连接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将需要的函数也链接到程序中。
*/

编译的三个阶段

  • sum.c

    int g_val = 2022;
    void print(const char* str)
    {
        printf("%s\n",str);
    }
    
  • test.c

    #include <stdio.h>
    int main()
    {
        extern void print(char* str);
        exten int g_val;
        printf("%d\n",g_val);
        printf("hello world\n");
        return 0;
    }
    

在这里插入图片描述

编译的每个阶段

/*
 * 1.预处理 选项 gcc -E test.c -o test.i
 *   预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。
 * 2.编译 选项 gcc -S test.c
 *   编译完成之后就停下来,结果保存在test.s中。
 * 3.汇编 gcc -c test.c
 *   汇编完成之后就停下来,结果保存在test.o中。
 */

VIM学习资料
简明VIM练级攻略:https://coolshell.cn/articles/5426.html
给程序员的VIM速查卡:https://coolshell.cn/articles/5479.html

在这里插入图片描述

运行环境

推荐阅读:《程序员的自我修养》

程序执行过程

/*
 * 程序执行过程:
 * 1.程序必须载入内存中。在有的操作系统的环境中:一般这个由操作系统完成。
 *   在独立的环境中,程序的载入必须手动安排,也可能是通过可执行代码置入只读内存来完成。
 * 2.程序开始执行,接着调用main函数。
 * 3.开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。
 *   程序同时也可以使用静态(static)内存,存储静态内存中的变量在程序的整个执行过程一直保留他们的值。
 *   堆栈就表示栈区。堆区简称堆。
 * 4.终止程序。正常终止main()函数;也有可能是意外终止。
 */

预处理详解

预定义符号

//这些预定义符号都是语言内置的
//__FILE__		//进行编译的源文件
//__LINE__		//当前代码所在行号
//__DATE__		//文件被编译的日期
//__TIME__		//文件被编译的时间
//__FUNCTION__	//当前代码所在函数
//__STDC__		//如果编译器遵顼ANSI C标准,则其值为1,否则未定义
    
#include <stdio.h>

int main()
{
    printf("%s\n",__FILE__);//当前文件名
    printf("%d\n",__LINE__);//当前代码所在行号
    printf("%s\n",__DATE__);//当前日期
    printf("%s\n",__TIME__);//当前时间
    printf("%s\n",__FUNCTION__);//当前函数名。
    printf("%d\n",__STDC__);//如果编译器遵循ANSI C,则其值为1,否则未定义。

    //打印日志信息
//    FILE* pf = fopen("log.txt","a+");
//    if(pf == NULL)
//    {
//        perror("fopen");
//        return 1;
//    }
//    int i;
//    for(i=0 ; i<10 ; i++)
//    {
//        fprintf(pf,"%s %d %s %s %d\n",__FILE__,__LINE__,__DATE__,__TIME__,i);
//    }
//
//    //关闭文件
//    fclose(pf);
//    pf = NULL;
    return 0;
}

#define

#define用于定义符号、定义宏。

#define定义标识符

  • 语法

    #define name 用于替换name的值
    
  • 使用

    //#define 是定义符号的
    
    #include <stdio.h>
    
    //定义的时候后面不加分号(;),因为是完整替换,如果有了分号,也会被替换过去,而我们使用宏的时候,其语句后面肯定是有分号的,所以这里一定不能加分号。
    #define M 1000
    #define reg register        //为register这个关键字,创建一个简短的名字。
    #define do_forever for(;;)  //用更形象的符号来替换一种实现。
    #define CASE break;case     //在写case的时候自动补上前一个case需要的break;
    //如果定义的stuff过长,可以分成几行写,除了最后一行外,每行的后面都加了一个反斜杠(也叫续行符)
    #define DEBUG_PRINT printf("file:%s  line:%d  date:%s  time:%s\n", \
                                __FILE__,__LINE__,                    \
                                __DATE__,__TIME__)
    
    int main()
    {
        int m = M;
        printf("%d\n",m);
    
        reg int num = 0;
        printf("%d\n",num);
    
        //替换过来就是:for(;;); ,是一个死循环。
        //do_forever;
    
    
        //如果不想要一直写break
        int n = 3;
        switch(n)
        {
    //        case 1:
    //            break;
    //        case 2:
    //            break;
    //        case 3:
    //            break;
            //就可以变为:
            case 1:
            CASE 2:
            CASE 3:
                break;
        }
    
        DEBUG_PRINT;
    
        //#define定义要加后面的分号吗?语法上可以加,但是最好不加。
        //因为是替换,分号也会随着一起替换过去。
        //#define M 1000;
        int a = 10;
        int b = 0;
    //    if(a>10)
    //        b = M;
    //    else
    //        b = -M;
        //如果带有分号,替换后就成为:
    //    if(a>10)
    //        b = 1000;
    //    ;
    //    else
    //        b = -1000;
    //    ;
        //if没加大括号,后面只能跟一条语句,而替换来的M带来了一个分号,就成了两条语句。
        //此时后面紧跟的else语句就变成单独使用了。
    
        return 0;
    }
    

#define定义宏

#define机制规定:允许用参数替换内容,这种实现通常称为宏或定义宏。

/*
 * #define name(参数列表) stuff
 * - 参数列表是一个由逗号隔开的符号表,它们可能出现在stuff中。
 * 注意:
 * - name后必须紧跟参数列表的括号。
 *   如果name后有空格,则(参数列表)就会被解释为stuff的一部分。
 */
#include <stdio.h>

#define SQUARE(X) X*X
#define DOUBLE(X) (X)+(X)

//正确写法:
//#define SQUARE(X) ((X)*(X))
//#define DOUBLE(X) ((X)+(X))
//对于用数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时参数先与相邻操作符之间发生作用。

int main()
{
    //实际是打印:3*3
    printf("%d\n", SQUARE(3));//9

    //这里5+1=6,应该输出36。为什么输出了11呢?
    int a = 5;
    printf("%d\n", SQUARE(a+1));//1
    //思考为什么输出了11?
    //替换后成为:a+1*a+1 = 5+1*5+1 = 11。

    //因为X不止是一个值,还可能是一个表达式
    //所以我们可以修改宏定义为: #define SQUARE(X) (X)*(X)
    //这样就可以达到预期效果了。
    //当然(X)*(X)是一个整体,建议将他们也一起括起来,这样就更加严谨了。
    //#define SQUARE(X) ((X)*(X))

    //预期结果:10*8=80
    printf("%d\n",10*DOUBLE(4));//44
    //替换后成为:10*4+4=44
    //所以要修改宏为:#define DOUBLE(X) ((X)+(X))
    //这样就成为了一个整体了,
    
    //结论:预处理阶段,只是进行替换。编译完进行执行的时候,才计算其表达式。
    return 0;
}

#define替换规则

/*
 * 在程序中进行#define定义符号和宏时,需要涉及几个步骤
 * 1、程序执行到调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果包含,参数中的#define定义的符号先被替换。
 * 2、替换后被插入到程序中原来的位置。定义的宏、参数名,被调用宏时传入的值替换。
 * 3、最后再次对文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,则重复以上过程。
 *
 * 注意:
 * 1、宏的参数和#define定义时,其中可以出现其他#define定义的常量。但是对于宏,不能出现递归。
 * 2、当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
 */
#include <stdio.h>

#define M 100
#define MAX(X,Y) ((X)>(Y)?(X):(Y))

int main()
{
    int max = MAX(101,M);
    //第一步:将M替换为对应值,成为MAX(101,100);
    //第二步,替换宏。MAX(101,100);被替换为:((101)>(100))?(101):(100);
    //第三步:检查替换后的宏中是否还包含其他#define定义的符号。如果有则重复上述过程。

    //预处理器搜索#define定义的符号时,字符串常量中的内容并不被搜索。
    //也就是说"M=%d \n"字符串中的M并不会被搜索,传入的参数M才会被搜索替换。
    printf("M=%d \n",M);
    return 0;
}

#和##

#和##都是只能使用在宏中,不能在程序的函数中使用。但是可以放在定义宏时的函数中

  • #的使用 #变量名,就成为"变量名"

    #include <stdio.h>
    
    //#:把一个宏参数变成相应的字符串。
    //如int a = 10;  #a就成为"a"
    //#只能在宏中使用,将其变量名转换为字符串。不能在程序中的printf()函数中使用
    //这里FORMAT本来就是一个字符串,而我们要使用的是FORMAT字符串的内容,而不是变量名,所以不需要加#。
    #define PRINT(X,FORMAT) printf("the value of "#X" is "FORMAT"\n",X)
    int main()
    {
        //字符串会自动链接在一起
        printf("hello world\n"); //hello world
        printf("hello ""world\n"); //hello world
    
        // 需要打印 the value of a/b/c is 10/20/30
        int a = 10;
        int b = 20;
        int c = 30;
    
        //#变量名,会将变量名当作一个字符串,就成为"变量名"
        PRINT(a,"%d");
        //被替换为:printf("the value of ""a"" is ""%d""\n",a)
        PRINT(b,"%d");
        //被替换为:printf("the value of ""b"" is ""%d""\n",b)
        PRINT(c,"%d");
        //被替换为:printf("the value of ""c"" is ""%d""\n",c)
    
        //此时需要打印一个float类型数:
        float f = 5.5f;
        PRINT(f,"%f");
        //printf("the value of ""f"" is ""%f""\n",f)
    
        return 0;
    }
    
  • ##的使用:合并符号/标识符

    /*
     * - ##用于合并符号。将位于##两边的符号合成一个符号。
     *   它允许从分离的文本片段创建标识符。
     * 注意:这样的合并,必须产生一个合法的标识符,否则结果是未定义的。
     */
    #include <stdio.h>
    
    #define CAT(X,Y) X##Y
    #define CAT2(X,Y,Z) X##Y##Z
    
    int main()
    {
        int class101 = 100;
        printf("%d\n", CAT(class,101));//100
        printf("%d\n", CAT2(class,10,1));//100
        return 0;
    }
    

带副作用的宏

//当宏参数在宏中的定义出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险。
//导致不可预测的后果,副作用就是:表达式求值的时候出现永久性效果。
#define MAX(X,Y) ((X)>(Y)?(X):(Y))

int main()
{
    //运算过后,b=2,a=1
//    int a = 1;
//    int b = a+1;
    //运算过后b=2,a=2。此时就说,++a是有副作用的,把a本身也改变了。
//    int a = 1;
//    int b =++a;

    int a = 5;
    int b = 8;
    int m = MAX(a++,b++);
    //被替换后成为:((a++)>(b++)?(a++):(b++));
    //执行时,先执行:(a++)>(b++),8++>5++,执行过后,b=9,a=6
    //然后返回b++ ——> int m =b++;  先赋值,所以m=9,然后b++后是10,所以b此时是10。
    //a与b被永久改变了。
    printf("%d\n",m);//9
    printf("%d\n",a);//6
    printf("%d\n",b);//10

    //此时MAX就是带有副作用的宏

}

宏和函数的对比

/*
 *
 * 宏通常被应用于执行简单的运算。比如在两个数种查找较大的一个
 *   #define MAX(X,Y) ((X)>(Y)?(X):(Y))
 * 为什么不使用函数来完成呢?int Max(int x,int y){return x>y?x:y;}
 * 原因:
 * - 用于调用函数和从函数返回的代码,可能比执行这个小型计算所需要的时间更多。
 *   所以如果是小型功能等,在程序的规模、速度方面,宏比函数更胜一筹。
 * - 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。
 *   反之,这个宏可以适用于整型、长整型、浮点型等,只要可以用>来比较的类型都可以使用。
 *   宏是无关类型的。
 *
 * 但是也不是说,宏是万能的。宏与函数相比,也有劣势的地方:
 * - 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅增加程序的长度。
 *   如果这个宏使用的次数很多,那么相当于重复使用大量代码。此时将其定义为函数,会比宏更加高效。
 * - 宏没办法调试,因为宏在预处理时,就被替换掉了。而在程序运行时才可以进行调试。在进行调试时,宏已经被替换,所以宏无法调试。
 * - 宏的参数无关类型,因此不够严谨。
 * - 宏可能会带来运算符优先级的问题,导致程序容易出错。如果传入宏的不时一个变量,而是一个表达式。
 *   此时,多种操作符交集在一起,就可能出现难以预料的结果。
 */

#define MAX(X,Y) ((X)>(Y)?(X):(Y))

int Max(int x,int y)
{
    return x>y?x:y;
}

int main()
{

    int a = 5;
    int b = 8;

    //在预处理阶段被替换为:int m = (a)>(b)?(a):(b)?
    //运行程序时,直接执行这一行代码即可。
    int m = MAX(a,b);

    //而调用函数时,查看其汇编代码,发现调用函数就需要好几行才能进入到函数内,
    //进入到函数内才开始执行比较,比较完再返回值。
    m = Max(a,b);

    //并且Max()只能求两个int类型的数的最大值。
    //而宏MAX()中可以传入的类型,只要可以用>、<比较,不管是什么类型,就都可以传入
}

宏有时候可以做到函数做不到的事情,例如:宏的参数中可以出现类型,而函数不行。

//宏的参数中可以出现类型
#define MALLOC(num,type) (type*)malloc(num*sizeof(type));

int main()
{
    //如调用动态开辟空间函数malloc —— (int*)malloc(10*sizeof(int));  不能使用malloc(10,int)

    //使用宏定义之后,就成为:
    //int* p = MALLOC(10,int);
    //被替换后就成为:int* p = (int*)malloc(10*sizeof(int));
    return 0;
}

宏与函数的对比

属性#define定义的宏函数
代码长度每次使用时,宏代码都会被插入到程序中。如果使用过多,除了非常小的宏之外,程序的长度会大大变长。函数代码只出现在一个地方。每次需要使用时,直接调用即可。函数本身就时为了解耦合、解决代码大量重复的。
执行速度更快存在函数的调用和返回时的额外开销,相对较慢
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则临近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写时多加括号函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测
带有副作用参数参数可能被宏替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果函数参数只在传参时求职一次,结果更容易控制
参数类型宏的参数与类型无关,只要对参数的操作是合法的,他就可以适用于任何数据类型。函数的参数与类型有关,如果参数的类型不同,就需要不同的参数,即使他们执行任务的逻辑可能是相同的,类型不同,也不能使用。
调试宏是不方便调试的函数是可以逐句调试的
递归宏不能递归函数可以递归

命名约定

一般来讲,函数与宏的使用非常相似。C语言本身不能帮我们区分,一般我们的使用习惯是:

  • 把宏名称全部大写
  • 函数名称不要全部大写(可以采用驼峰式命名)

#undef —用于移除一个宏定义

//#undef 宏名称
//如果现存一个宏,名字需要被重新定义,那么它的旧名字首先要被移除。

#define M 100

int main()
{
    printf("%d\n",M);//100
#undef M
    //printf("%d\n",M);//被移除后的宏就不能使用了。

    return 0;
}

命令行定义(了解)

/*
 * 许多C的编译器都提供了一种能力:允许在命令行中定义符号,用于启动编译过程。
 * - 例如我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。
 *   假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组。
 *   但是另外一个机器内存大写,我们又需要一个很大的数组。
 * - 而使用命令行参数,我们想要大的,就传入一个大的数;我们想要小的,就传一个小的数。
 */

//Linux下指令:gcc test.c -D SIZE=100
//Linux下指令:gcc test.c -D SIZE=10
//windows环境下的集合环境,百度相应的命令行参数设置,然后按照其进行输入即可

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

条件编译

//在编译一个程序的时候,我们如果要将一条语句(一组语句)编译或放弃,就需要使用条件编译指令。
//例如:调试行的代码,弃之可惜,食之无味。我们就可以进行选择性的编译。

#include <stdio.h>
#define __DEBUG__
int main()
{
    //如果定义了__DEBUG__,则会执行其中的代码。
#ifdef __DEBUG__
    printf("debuging");
#endif
    //如果没有定义,则不会执行
#ifdef __DIJIA__
    printf("召唤迪迦");
#endif
    return 0;
}

常见的条件编译指令

  • 单分支

    #if 常量表达式
        代码
    #endif
    

    使用

    //不为0就编译执行
    #define DBUG 1
    #if DBUG
        //执行这里的代码
    #endif
    
    //为0不执行
    #define DBUG 0
    #if DBUG
        //如果为0,则这里的代码不编译执行
    #endif
    
    
    //也可以这样使用:
        //需要编译执行的:
        #if 1
            int main()
            {
                //...
                reutrn 0;
            }
        #endif
    
        //不需要编译执行的
        #if 0
            int main()
            {
                //...
                reutrn 0;
            }
        #endif
    
  • 多分支

    #if 常量表达式
        //...
    #elif 常量表达式
        //...
    #else
        //...
    #endif
    

    使用

    #include <stdio.h>
    
    #define MAX 10
    #define MIN 1
    
    int main()
    {
    #if 1==2
        printf("haha");
    #elif 1==1
        printf("hehe\n");//编译执行
    #else
        printf("heihei");
    #endif
    
    #if MAX < MIN
        printf("大的小");
    #elif MAX == MIN
        printf("hehe");
    #else
        printf("MAX>MIN\n");//编译执行
    #endif
        return 0;
    }
    
  • 判断是否定义

    //只判断其是否定义。不判断其定义的值
    //判断symbol是否定义。如果定义,则编译执行其中的代码。反之,则不编译。
        //(定义执行)形式一:
        #if defined(symbol)
            //...
        #endif
        
        //(定义执行)形式二
        #ifdef symbol
            //...
        #endif
    
    //判断symbol是否定义。如果未定义,则编译执行其中的代码。如果定义了,则不编译。
        //(未定义执行)形式一
        #if !defined(symbol)
            //...
        #endif
    
        //(未定义执行)形式二
        #ifndef symbol
            //...
        #endif
    

    使用

    #define symbol 0;
    #define test 3;
    int main()
    {
        //定义就编译执行,不管其值是几。
    #if defined(symbol)
        printf("hehe\n");   //hehe
    #endif
        
        //test2未定义不编译。
    #ifdef test2
        printf("hhhhh\n");
    #endif
    
        //test定义了不编译。
    #if !defined(test)
        printf("haha\n");
    #endif
        
        //未定义执行,只要不定义就编译执行。
    #ifndef test2
        printf("test2未定义"); //test2未定义
    #endif
    }
    
  • 嵌套使用

    #if defined(OS_UNIX)
      #ifdef OPTION1
        unix_version_option1();
      #endif
      #ifdef OPTION2
        unix_version_option2();
      #endif
    #elif defined(OS_MSDOS)
      #ifdef OPTION2
        msdos_version_option2();
      #endif
    #endif
    

文件包含

#include叫做“文件包含”,include语句包含并运行指定文件

  • 预处理器发现 #include 指令后,就会寻找指令后面<>中的文件名,并把这个文件的内容包含到当前文件中。

  • 被包含文件中的文本将替换源代码文件中的#include 指令, 就像你把被包含文件中的全部内容键入到源文件中的这个位置一样。

头文件被包含的方式

  • 本地文件包含

    #include "文件名" 
    
    //表示编译系统首先在当前的源文件目录中查找,如果没有找到,则编译器在其存放头文件存放的目录路径去搜索系统头文件。
    //如果还找不到则提示编译错误
    
  • 库文件包含

    //表示编译系统根据系统头文件存放的目录路径去搜索系统头文件,而不是在源文件目录去查找。
    //如果找不到则编译报错
    #include <文件名> 
    
  • 两种包含方式的区别

    //系统定义的头文件通常使用尖括号;用户自定义的头文件通常使用双引号。
    //一般来说,如果为调用库函数而用#include命令来包含相关的头文件,则用尖括号,以节约查找时间。
    //如果要包含的是用户自己编写的文件(这种文件一般都在用户当前的目录中),一般用双引号。
    //若文件不在当前目录中,在双引号内应该给出文件路径(如#include"D:\wang\file2.h“)
    
    /*
     * 文件包含命令的使用方法:
     * - 当一个文件被包含时,其中所包含的代码继承了 include 所在行的变量范围。
     *   从该处开始,调用文件在该行处可用的任何变量在被调用的文件中也都可用。不过所有在包含文件中定义的函数和类都具有全局作用域。
     * - 如果 include 出现于调用文件中的一个函数里,则被调用的文件中所包含的所有代码将表现得如同它们是在该函数内部定义的一样。所以它将遵循该函数的变量范围。
     * - 文件包含命令可以出现在文件的任何位置,但通常放置位置在文件的开头处。一条#include命令只能指定一个被包含的文件。
     * - 文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。
     * - 当一个C程序分散在若干个文件中时,可以将多个文件公用的符号常量定义和宏定义等单独写成一个文件,然后在其他需要这些定义和说明的源文件中用文件包含命令包含该头文件。
     *   这样可以避免在每个文件的开头都去重复书写那些共用量,也可以避免因输入或修改失误造成的不一致性。
     */
    
  • Linux环境的标准头文件的路径

    /usr/include
    
  • CLion的标准头文件路径

    //按照自己的安装路径寻找
    D:\Program Files (x86)\CLion 2021.3.2\bin\mingw\x86_64-w64-mingw32\include
    

嵌套文件包含

在这里插入图片描述

/*
 * - add.h和add.c是公共模块。test1.h和test.2h中都使用了add.h中的内容。
 *   而test.h和test.c中使用了test1.h和test.2h
 * - 此时,最终程序中就包含了两份add.h的内容。这样就造成了文件内容的重复
 */

解决头文件重复包含问题

  • 条件编译

    每个头文件开头写

    /*
     * 如果__TEST_H__未定义执行。
     * - 第一次包含该头文件,__TEST_H__肯定是未定义的。
     * - 如果是第二次包含该头文件,则不会执行。因为这里是__TEST_H__未定义执行。
     *   第一次进来的时候定义过了,所以第二次就不进行编译执行了。
     */
    #ifndef __TEST_H__
    #define __TEST_H__ //定义__TEST_H__
        //头文件内容
    #endif
    
    //注意: #ifndef的方式依赖于宏名字不能冲突,这不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件不会被不小心同时包含。当然,缺点就是如果不同头文件的宏名不小心“撞车”,可能就会导致头文件明明存在,编译器却硬说找不到声明的状况
    //优势:由语言支持所以移植性好
    
  • 将该语句写入文件

    /*
     * #pragma once
     * - #pragma once 一般由编译器提供保证:同一个文件不会被包含多次。
     *   注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。
     * - 你无法对一个头文件中的一段代码作pragma once声明,而只能针对文件。
     *   其好处是,你不必再担心宏名冲突了,当然也就不会出现宏名冲突引发的奇怪问题。大型项目的编译速度也因此提高了一些。
     * - 对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。
     *   当然,相比宏名冲突引发的“找不到声明”的问题,这种重复包含很容易被发现并修正。
     * - 另外,这种方式不支持跨平台!
     */
    
    //注意:#pragma once则由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。带来的好处是,你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名碰撞引发的“找不到声明”的问题,重复包含更容易被发现并修正。
    //优势:可以避免名字冲
    

推荐:《高质量C/C++编程指南》中附录的考试试卷(非常重要)

笔试题

  • 头文件中的ifndef/define/endif是干什么用的?

    —— 防止头文件被重复多次包含

  • #include <filename.h> 与#include "filename.h"有什么区别?

    //系统定义的头文件通常使用尖括号;用户自定义的头文件通常使用双引号。
    //一般来说,如果为调用库函数而用#include命令来包含相关的头文件,则用尖括号,以节约查找时间。
    
    //""一般用来包含自定义从的头文件
    //查找:先去当前的源文件目录中查找,如果没有找到,则编译器在其存放头文件存放的目录路径去搜索系统头文件。
    
    //<>一般用来包含库函数的头文件。
    //表示编译系统根据系统头文件存放的目录路径去搜索系统头文件,而不是在源文件目录去查找。
    

其他预处理指令

#error
#pragma
//#pragma pack(4) 设置默认对齐数为4
//#pragma pack() 取消设置的默认对齐数,还原为默认
#line
....

参考《C语言深度解剖》进行学习

练习

  • ___的作用是将源程序文件进行处理,生成一个中间文件,编译系统对此中间文件进行编译并产生目标代码。

    编译预处理、汇编、生成安装文件、编译。

    解析:

    编译的三个阶段都可能生成中间文件。但是后面又说了,对此中间文件进行编译,也就是说在编译之前的那个步骤,那就只能是:编译预处理 了。

  • 由多个源文件组成的C程序,经过编译、预处理、链接等阶段,生成最终的可执行程序。哪个阶段可以发现被调用的函数未定义?

    预处理 、 编译 、 链接 、 执行

    解析:

    在链接阶段的时候,才会把多个目标文件与链接库进行链接。此时才能发现未定义的函数。

  • 哪个不是预处理指令?

    #define 、 #if 、 #undef 、 #end

    #end不是预处理指令,#endif才是

  • 关于头文件

    #include “文件名.h” 编译器寻找头文件时,是从指定的库目录寻找。错误,是从当前文件的目录下寻找

    多个源文件同时用到的全局变量,它的声明和定义都放在头文件中,是好的编程习惯。错误,因为会被多个源文件同时用到,则可能头文件会出现多次包含。如果做了防止头文件重复包含的解决方法还好;如果没有,那么如果一个源文件中多次包含了这个头文件,那么就会出现全局变量的重复定义。

    //那么头文件中一般写什么呢?
    //1、一般包含其他头文件
    //2、类型的定义(类型重命名),自定义数据类型等。注意是类型的定义,而不是变量的定义。
    //3、函数的声明。声明可以多次,但是定义不可以
    

二进制数的奇偶数位交换宏实现

/*
 * 实现一个宏,把二进制的奇数位与偶数位进行交换。
 * 思路:
 * - 偶数位右移一位,奇数位左移一位。然后这两个位移后的数进行相加。
 *
 * 实现:
 * - 先取出这个数的奇数位与偶数位。
 * - 如果要取出一个数的偶数位:
 *   把这个数与10101010 10101010 10101010 10101010进行按位与(&)即可,得出的结果的所有偶数位,就是这个数的偶数位
 *   10101010 10101010 10101010 10101010转换为十六进制就是:0xAAAAAAAA
 * - 如果要去除一个数的奇数位:
 *   把这个数与01010101 01010101 01010101 01010101进行按位与(&)即可,得出的结果的所有奇数位,就是这个数的奇数位
 *   01010101 01010101 01010101 01010101转换为十六进制就是:0x55555555
 */

#define SWAP(N) ((N & 0xaaaaaaaa) >>1 )+((N & 0x55555555) <<1)

int main()
{
    int num = 10;
//    int ret = ( (num & 0xaaaaaaaa) >> 1 )+( ( num & 0x55555555 ) << 1 );
    int ret = SWAP(num);
    printf("%d\n",ret);
    return 0;
}

offsetof宏的实现

//写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
/*
 * offsetof()宏
 * 宏原型:offsetof(type,member)
 * 作用:返回数据结构或联合类型中成员的偏移量值,以字节为单位。
 *
 * 参数:
 * type:一个结构类型。或联合类型
 * member:结构类型成员/联合体成员。
 *
 * 返回值:该宏返回类型为size_t的值,表示type中成员的偏移量。
 */
#include <stddef.h>

/*
 * - 假设0位置处就是struct结构体类型的变量,我们将其转换为结构体类型指针: (struct_name*)0
 *   就可以模拟访问结构体了,此时的((struct_name*)0)就是一个指针变量
 * - 如果要求哪个成员的偏移量,就指向哪个成员,本来是取出这个成员的地址,然后减去0处的地址
 *   但是因为我们这里直接假设0位置处是我们结构体开始的地方了。所以不用再去减,因为-0,还是本身。
 *   因为0位置成了结构体的起始位置,被强制转换为了结构体指针,就可以模拟指向其成员,它会根据成员本身所在位置进行偏移。
 * - 就像整型数组指针一次偏移4个字节、字符数组指针一次偏移一个字节。
 *   结构体指也是,会根据其成员所在空间,进行偏移。
 * - 指向想要求偏移量的成员后,取出其地址,此时的地址是相较结构体起始位置在0位置的地址。
 *   也就是说当前成员在结构体中排到哪里,就取出哪里的地址。
 *   因为求的偏移量是一个数,所以我们将其强制转换为int类型。
 *
 *
 * 注意:
 * - 这个过程中没有开辟空间,只是假设这个结构体在0位置处,然后模拟指针指向其成员处。
 *   然后取出地址,强制转换为一个int类型数。这个数就是成员据0位置的偏移量。
 */

//NULL是一个被预定义的符号,表示0地址。
//&是取地址。
//#include <stdio.h>
//#define OFFSETOF(struct_name,mem_name) (int)&( ( (struct_name*)NULL ) -> mem_name )

#define OFFSETOF(struct_name,mem_name) (int)&( ( (struct_name*)0 ) -> mem_name )
struct A
{
    int a;
    short b;
    int c;
    char d;
};

int main()
{
    printf("%d\n",OFFSETOF(struct A,a));//0
    printf("%d\n",OFFSETOF(struct A,b));//4
    printf("%d\n",OFFSETOF(struct A,c));//8
    printf("%d\n",OFFSETOF(struct A,d));//12
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值