飒飒

目录

3. 编译及预处理

(1). 编译过程

  1. 预编译

    1. 处理所有的注释,以空格代替

    2. 将所有的#define删除,并且展开所有的宏定义

    3. 处理条件编译指令#if,#ifdef,#elif,#else,#endif

    4. 处理#include,展开被包含的文件

    5. 保留编译器所需要使用的#pragma指令

      示例:gcc –E file.c –o file.i

  2. 编译

    1. 对与处理文件进行词法分析,语法分析和语义分析

      • 词法分析:分析关键字,标识符,立即数等是否合法

      • 语法分析:分析表达式是否遵循语法规则

      • 语义分析:在语法分析的基础上进一步分析表达式是否合法

    2. 分析结束后进行代码优化生成相应的汇编代码文件

      示例:gcc –S file.i –o file.s

  3. 汇编

    1. 汇编器将汇编代码转变为机器的可以执行指令

    2. 每条汇编语句几乎都对应一条机器指令

      示例:gcc –c file.s –o file.o

(2). 链接过程

  链接器的主要作用是把各个模块之间相互作用的部分处理好,使得各个模块之间能够正确的衔接

  1. 静态链接

    1. 由连接器在链接时蒋库的内容直接加入到可执行程序中

    2. Linux下静态库的创建和使用

      • 编译静态库源码: gcc –c lib.c –o lib.o

      • 生成静态库文件: ar -q lib.a lib.o

      • 使用静态库编译: gcc main.c lib.a –o main.out

    //20-1.c
    #include <stdio.h>
    
    extern char* name();
    extern int add(int a, int b);
    
    int main()
    {
        printf("Name: %s\n", name());
        printf("Result: %d\n", add(2, 3));
    
        return 0;
    }
    
    //slib.c
    char* name()
    {
        return "Static Lib";
    }
    
    
    int add(int a, int b)
    {
        return a + b;
    }
    
  2. 动态链接

    1. 可执行程序在运行时才动态加载库进行链接

    2. 库的内容不会进入可执行程序当中

    3. Linux下动态库的创建和使用

      • 编译动态库源码: gcc –shared dlib.c –o dlib.so

      • 使用动态库编译: gcc main.c –ldl –o main.out

      • 关键系统调用

        dlopen:  打开动态库文件

        dlsym:   查找动态库中的函数并返回调用地址、

        dlclose:  关闭动态库文件

    //20-2.c
    #include <stdio.h>
    #include <dlfcn.h>
    
    int main()
    {
        void* pdlib = dlopen("./dlib.so", RTLD_LAZY);
    
        char* (*pname)();
        int (*padd)(int, int);
    
        if( pdlib != NULL )
        {
            pname = dlsym(pdlib, "name");
            padd = dlsym(pdlib, "add");
     
            if( (pname != NULL) && (padd != NULL) )
            {
                printf("Name: %s\n", pname());
                printf("Result: %d\n", padd(2, 3));
            }
    
            dlclose(pdlib);
        }
        else
        {
            printf("Cannot open lib ...\n");
        }
    
        return 0;
    }
    
    //dlib.c
    char* name()
    {
        return "Dynamic Lib";
    }
    
    int add(int a, int b)
    {
        return a + b;
    }
    

(3). 宏的定义和使用分析

  1. 定义宏常量

    1. define定义宏常量可以出现在代码的任何地方。

    2. define从本行开始,之后的代码都可以直接使用这个宏常量。

    3. define定义的宏常量本质为字面量

  2. 定义宏表达式

    1. define表达式有函数调用的假象,却不是函数(只是替换)。

    2. define表达式可以比函数更强大(函数数组参数会退化成一个指针)。

    3. define表达式比函数更容易出错(不要再宏参数中写表达式)。

  3. 宏代码块的定义(宏强于函数的优势)

    1. 宏表达式在预编译期被处理,编译器不知道宏表达式的存在。

    2. 宏表达式用“实参”完全代替形参,不进行任何运算。

    3. 宏表达式没有任何的“调用”开销。

    4. 宏表达式中不可能出现递归定义。

    5. 宏参数可以是任何C语言实体

      • 宏编写的_MIN_参数类型可以是int, float等等

      • 宏的参数可以是类型名

    #include <stdio.h>
    #include <malloc.h>
    
    #define MALLOC(type, x)   (type*)malloc(sizeof(type)*x)
    #define FREE(p)           (free(p), p=NULL)
    
    #define LOG_INT(i)        printf("%s = %d\n", #i, i)
    #define LOG_CHAR(c)       printf("%s = %c\n", #c, c)
    #define LOG_FLOAT(f)      printf("%s = %f\n", #f, f)
    #define LOG_POINTER(p)    printf("%s = %p\n", #p, p)
    #define LOG_STRING(s)     printf("%s = %s\n", #s, s)
    
    int main()
    {
        int* pi = MALLOC(int, 5);
        char* str = "test demo";
        
        LOG_STRING(str);
        
        LOG_POINTER(pi);
        
        FREE(pi);
        
        LOG_POINTER(pi);
        
        return 0;
    }
    
  4. 宏定义的常量或表达式没有作用域的限制,若想让宏只在定义的函数中起作用,在函数最后加上#undef 宏名

  5. 定义日志宏

    1. 内置宏

    2. 日志宏(time函数得到当前系统时间ms,localtime转换为可识别的本地时间,asctime把时间转换为ASC码)

  6. define f (x) ((x)-1)代表什么意思?

    代表把f定义为 (x) ((x)-1),宏定义对空格敏感,但宏调用对空格不敏感。

(4). 条件编译使用分析

  1. 基本概念

    1. 条件编译的行为类似于C语言中的if…else

    2. 条件编译是预编译指示命令,用于控制是否编译某段代码

    3. 执行条件:#if 条件成立,#Ifdef 宏定义存在

  2. 条件编译的本质

    1. 预编译器根据条件编译指令有选择的删除代码,编译器不知道代码分支的存在

    2. If…else…语句在运行期进行分支判断,而条件编译指令在预编译期进行分支判断

    3. 可以通过命令行定义宏

        gcc –Dmacro=value file.c

       或 gcc –Dmacro file.c

  3. include的本质

    1. include的本质是将已经存在的文件内容嵌入到当前文件中

    2. include的间接包含同样会产生嵌入文件内容的操作,若间接包含同一个头文件会产生编译错误

  4. 条件编译的意义

    1. 条件编译使得我们可以按不同的条件编译不同的代码段,因而可以产生不同的目标代码

    2. if…#else…#endif被预编译器处理;而if…else语句被编译器处理,必然被编译进目标代码

    3. 实际工程中条件编译主要用于一下两种情况:

      • 不同的产品线共用一份代码

      • 区分编译产品的调试版和发布版

(5). #error和#line使用分析

  1. 自定义错误消息

    1. error用于生成一个编译错误消息,并停止编译。

    2. 用法:#error message (注:message不需要双引号包围)

    3. error编译指示字用于自定义程序员特有的编译错误消息。类似的,#warning用于生成编译警告,但不会停止编译。(\”字符串\”)

    4. error是一种预编译器指示字,可用于提示编译条件是否满足

    (__cplusplus c ++内置宏,判断编译器是不是c ++)

     ```
     #ifndef __cplusplus
         	#error This file should be processed with c++ compiler
     #endif
     ```
    
    1. ++编译过程中的任意错误信息,都无法生成最终的可执行程序++
  2. 重定义行号和文件名

    1. line用于强制指定新的行号和编译文件名,并对源程序的代码重新编号。

    2. 用法:#line number filename (注:filename 可省略)

    3. line编译指示字的本质是重定义__LINE__和__FILE__

      (原本用于防止不同人员编写同一程序时出错,方便查找。)

(6). #pragma预处理分析

  1. pragma简介

    1. pragma是编译器指示字,用于指示编译器完成一些特定的动作

    2. pragma所定义的很多指示字是编译器特有的

    3. pragma在不同的编译器间是不可移植的

      • 预处理器将忽略它不认识的#pragma指令

      • 两个不同的编译器可能以不同的方式解释同一条#pragma指令

      一般用法:

        #pragma parameter (++注:不同的parameter参数语法和意义各不相同++)

  2. pragma message

    1. message参数在大多数的编译器中都有相似的实现

    2. message参数在编译时输出消息到编译输出窗口中

    3. message可用于条件编译中可提示代码的版本信息

    4. 与 #error和 #warning不同,#pragma message仅仅代表一条编译信息,不代表程序错误

  3. pragma once

    1. pragma once 用于保证头文件只被编译一次

    2. pragma once效率上比#ifdef..#define…#endif高,但与编译器相关的,不一定被支持,可以如下使用,保证可靠与实用

  4. 内存对齐

    1. 什么是内存对齐?

      不同类型的数据在内存中按照一定的规则排列;而不是顺序的一个接一个的排放,这就是对齐。

    2. 为什么需要内存对齐?

      • CPU对内存的读取不是连续的,而是分成块读取的,块的大小只能是1、2、4、8、16字节

      • 当读取操作的数据未对齐,则需要两次总线周期来访问内存,因此性能会大打折扣

      • 某些硬件平台只能从规定的相对地址处读取某些特定类型的数据,否则抛出硬件异常

  5. pragma pack(能够改变编译器的默认对齐方式)

     struct占用的内存大小

    1. 第一个成员起始于0偏移处

    2. 每个成员按其类型大小和pack参数中++较小++的一个进行++对齐++

      • ++偏移地址必须能被对齐参数整除++

      • ++结构体成员的大小取其内部长度最大的数据成员作为其大小++

    3. 结构体总长度必须为所有对齐参数的整数倍

结构体大小的计算(Intel和微软的面试题)

注:gcc编译器默认4字节对齐,不支持8字节对齐

#include <stdio.h>

#pragma pack(8)

struct S1
{                 // 对齐参数    偏移地址    大小
    short a;      // 2           0           2
    long b;       // 4           4           4
};

struct S2
{                 // 对齐参数    偏移地址    大小
    char c;       // 1           0           1
    struct S1 d;  // 4           4           8
    double e;     // 8           16
};

#pragma pack()

int main()
{
    printf("%d\n", sizeof(struct S1));    // 8
    printf("%d\n", sizeof(struct S2));    // 24

    return 0;
}

(7). #和##运算符使用解析

  1. 运算符

    1. 运算符用于在预处理期将宏参数转换为字符串,因此只在宏定义中有效

    2. 运算符在宏中的妙用

      #include <stdio.h>
      
      #define CALL(f, p) (printf("Call function %s\n", #f), f(p))
         
      int square(int n)
      {
          return n * n;
      }
      
      int func(int x)
      {
          return x;
      }
      
      int main()
      {
          int result = 0;
          
          result = CALL(square, 4);
          
          printf("result = %d\n", result);
          
          result = CALL(func, 10);
          
          printf("result = %d\n", result);
      
          return 0;
      }
      
      
  2. 运算符

    1. 运算符用于在预处理期粘连两个标识符,因此只在宏定义中有效

    2. 利用##定义结构类型(高通公司用法)

      #include <stdio.h>
      
      #define STRUCT(type) typedef struct _tag_##type type;\
                           struct _tag_##type
      
      STRUCT(Student)
      {
          char* name;
          int id;
      };
      
      int main()
      {
          
          Student s1;
          Student s2;
          
          s1.name = "s1";
          s1.id = 0;
          
          s2.name = "s2";
          s2.id = 1;
          
          printf("s1.name = %s\n", s1.name);
          printf("s1.id = %d\n", s1.id);
          printf("s2.name = %s\n", s2.name);
          printf("s2.id = %d\n", s2.id);
      
          return 0;
      }
      

撒打算

撒打算

打撒所

posted @ 2019-06-06 15:47  呆着、  阅读(170)  评论(0)    收藏  举报