c进阶-程序环境和预处理

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

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

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行/运行环境,它用于实际执行代码

可执行的机器指令,即二进制的指令

图解:

注:

  • 翻译环境通常就是编译器提供的

  • 执行环境通常是操作系统提供的

二、详解编译+链接

1.翻译环境

  • 组成一个程序的每一个源文件通过编译过程分别转换成目标文件。

  • 每个目标文件有链接器捆绑在一起,形成一个单一而完整的可执行程序。

  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

  • 翻译环境(从.c到.exe) = 编译(编译器,即cl.exe) + 链接(链接器,即link.exe)

图解:

注:

  • VS2019底层所用编译器是cl.exe,链接器是link.exe

  • Codeblocks底层所用编译器是GCC

  • Clion底层所用编译器是clang

2.编译本身也分为几个阶段

我们用linux gcc 来演示编译和链接:

先看代码:

test.c

#include<stdio.h>

int main()
{
    int a = 10;
    int b = 20;
    int c = Add(a, b);
    printf("%d\n", c);

    return 0;
}

add.c

#include<stdio.h>

int Add(int x, int y)
{
    return x + y;
}

编译链接的画图解释:

预编译/预处理:

头文件替换 删除注释 条件编译 不会检查错误

gcc test.c -E -o test.i
//作用:让test.c文件在完成预编译后停下来
-E:让程序在完成预编译后停下来
test.c :指定预编译的文件
-o: output的英文简写,作用是指定输出
test.i: 开辟文件,将-o输出的结果存放到这里

具体做的事儿如下:

(1)将所有的#define删除,并且展开所有的宏定义。说白了就是字符替换

(2)处理所有的条件编译指令,#ifdef #ifndef #endif等,就是带#的那些

(3)条件语句中符合判断条件的会保留,不符合的会删除

(3)处理#include,将#include指向的文件插入到该行处

(4)删除所有注释

(5)添加行号和文件标示,这样的在调试和编译出错的时候才知道是是哪个文件的哪一行

(6)保留#pragma编译器指令,因为编译器需要使用它们。

编译:

检查错误 生成汇编文件 实质是把高级语言编译成机器可识别的汇编语言

gcc test.i -S (->test.s)
-S:让程序在完成编译后停下来,生成一个默认后缀名为.s 的文本文件
(->test.s):是完成编译后生成的

具体做的事儿:

把C语言代码翻译成了汇编代码,要完成这个,需要进行以下行为:

  • 语法分析

  • 词法分析

  • 语义分析

  • 符号汇总

汇编代码:

汇编:

将汇编文件生成二进制文件

gcc test.s -c (->test.o)
//作用:让test.s文件在完成汇编后停下来,并生成一个默认后缀名为.o 的文本文件

注:

windows环境下的目标文件格式是xxx.obj

linux 环境下的目标文件格式是xxx.o

二进制代码:

具体做的事儿:

(1)把汇编指令转换成二进制指令

(2)形成符号表(这部分看视频,笔记不好做)

链接:链接器把目标文件与所需要的附加的目标文件(如静态链接库、动态链接库)链接起来成为可执行的文件

具体做的事儿:

(1)合并段表

(2)符号表的合并和重定位

(3)将汇编生成的OBJ文件、系统库的OBJ文件、库文件链接起来,最终生成可执行程序。

VIM(Linux环境下的编辑器)学习资料
简明VIM练级攻略:
https://coolshell.cn/articles/5426.html
给程序员的VIM速查卡
https://coolshell.cn/articles/5479.html

3.运行环境

程序执行的过程:

1.程序必须载入到内存中。在有操作系统的环境中,一般这个由操作系统完成在独立的环境中(单片机),程序的载入必须由手工安排(焊接到板子里去),也可能是通过可执行代码置入只读内存来完成。

2.程序的执行便开始,接着调用main函数(main函数是程序的入口)

3.开始执行程序代码,这个时候程序将使用一个运行时堆栈(即栈帧),存储函数的局部变量和返回地址,程序同时也可以使用静态内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。

4.终止程序,正常终止main函数,也有可能是意外终止。

注:想完全搞懂这方面的知识,推荐《程序员的自我修养》

三、预处理\编译详解

1.预定义符号:

__FILE__      // 进行编译的源文件
__LINE__      // 文件当前的行号
__DATE__      // 文件被编译的日期
__TIME__      // 文件被编译的时间
__STDC__      // 如果编译器遵循ANSI C,其值为1,否则未定义;VS2019不遵循,gcc遵循

举例:

case 一:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
    int i = 0;
    for (i = 1; i <= 10; i++)
    {
        printf("name:%s file:%s line:%d date:%s time:%s i=%d\n", __func__, __FILE__, __LINE__, __DATE__, __TIME__, i);//
    }
}

运行结果:

注:

(1)这个代码是在2023年2月3号晚上21:08分运行的

(2)以后可以用__FILE__来确定文件的地址

(3)以后可以用__func__来确定printf在哪个函数里面运行的

(4)__是在非汉字输入法中利用Shift+_两次完成的

case 二:利用这个记录的特点将程序写进日志

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
    FILE* pf = fopen("text.txt", "w");
    if (pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    int i = 0;
    for (i = 1; i <= 10; i++)
    {
        fprintf(pf,"name:%s file:%s line:%d date:%s time:%s i=%d\n", __func__, __FILE__, __LINE__, __DATE__, __TIME__, i);//
    }

    fclose(pf);
    pf = NULL;
}

运行结果:

2.#define

#define 定义标识符

语法:
#define name stuff

举例:

case 一:常量

#define NUM 100

case 二:常量表达式

#define NUM 100+200

case 三:字符串

#define STR "abcdef"
int main()
{
    char* str=STR;
}

case 四:文本替换

#define MAX 1000
#define STR "hello bit"

int main()
{
    int a = MAX;
    printf("%d\n",MAX);
    printf("%s\n",STR);
    return 0;
}

case 五:为关键字创建一个简短的名字

#define reg register  //其作用相当于typedef

case 六:在写case语句的时候自动把break写上

#define CASE break;case

case 七:用更形象的符号来替换一种实现

#define do_forever for(;;)

case 八:如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)

#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )

注意:#define定义的标识符将会在预处理阶段进行替换:

//#define定义的内容不见了
int main()
{
    int a = 1000;
    printf("%d\n",1000);
    printf("%s\n","hello bit");
    return 0;
}

在VS中观察这一现象:

1.右键点击:

2.右键点击属性

3.将预处理到文件修改为 是

4.执行程序:此时会报错,但不用理会。此时后台会生成.i后缀的文件

5.找到该程序的Debug文件夹,并打开

6.用VS打开test.i文件

7.将文件拉到末尾查看

注:观察完后想要运行代码,需要把是改成否

警告:

在define定义标识符时,末尾不要添加否则可能出现错误

运行结果:报错

#define定义宏

#define 机制包括一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏

下面是宏的申明方式:

#define name(parament-list) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中

注意:

参数列表的左括号必须与name紧邻。

如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

举例:

case 一:

#define MAX(x,y) (x>y?x:y)
int main()
{
    int a = 10;
    int b = 20;
    int c = MAX(a, b);
    printf("%d\n",c);
}

替换后:

int main()
{
    int a = 10;
    int b = 20;
    int c = (a>b?a:b);
    printf("%d\n",c);
}

警告:

这个宏存在着一个问题:宏是完成替换的!

下面的代码将会与我们想要的结果相差甚远

case 二:由宏替换产生的表达式并没有按照预想的次序进行求值。

#define SQUARE(X) X*X

int main()
{
    int a=9;
    int r = SQUARE(a+1);
    printf("%d\n",r);
    
    return 0;
}

运行结果:19 居然不是100?

上面的代码在预处理阶段将会替换为以下代码:

int main()
{
    int a=9;
    int r = a + 1*a + 1;
    printf("%d\n",r);
    
    return 0;
}

case 三:在宏定义上加上两个括号,这个问题就能解决

#define SQUARE(X) (X)*(X)

int main()
{
    int a=9;
    int r = SQUARE(a+1);
    printf("%d\n",r);
    
    return 0;
}

运行结果:100

case 四:上面案例但仍然会存在问题,如下代码:

#define DOUBLE(X) (X) + (X)

int main()
{
    int r = 10 * DOUBLE(3);
    printf("%d\n", r);

    return 0;
}

上面的代码在进行宏替换时会变成:

int main()
{
    int r = 10 * 3 + 3;
    return 0;
}

但我们实际想要的结果是 : 10*(3+3)

要想解决这个问题,同样的,也需要再加上一个括号:

#define DOUBLE(X) ((X) + (X))

注:所以用于对数值表达式进行求值的宏定义都应该用这种方法加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。在表达式整体加上括号,对每个字母加括号最保险,即不要吝啬括号!!!👌

例如:

#define MAX(x,y) ((x)>(y)?(x):(y))

#define替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先

被替换。

2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。

3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上

述处理过程

图解:

注意:

1.宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,是不能出现递归的

2.当预处理器搜索#define定义的符号时,字符串常量的内容并不被搜索。

例如:

#define M 100
int main()
{
    int b = M;
    printf("M do not be found\n",M);
    return 0;
}

图解:

#和##

1.#的作用:# 把一个宏参数变成对应的字符串

2.字符串自动连接

示例:

char* p = "hello " "world\n";
printf("hello" " world\n");
printf("%s", p);

运行结果:

  1. 字符串作为宏参数

示例1:把一个字符串拆除多个字符串,作用一样

#include <stdio.h>
int main()
{
    int a = 10;
    printf("The value of ""a"" is ""%d""\n", a);
}

运行结果:

示例2:多个数据输出打印,不用#

#include <stdio.h>
int main()
{
    int a = 10;
    printf("The value of a is %d\n", a);

    int b = 20;
    printf("The value of a is %d\n", a);
}

运行结果:

示例3:多个数据输出打印

#define PRINT(N) printf("The value of "#N" is %d\n",N)
#include <stdio.h>
int main()
{
    int a = 10;
    PRINT(a);//#N变成了"a"

    int b = 20;
    PRINT(b);//#N变成了"b"
}

运行结果:

示例4:多种类型数据输出打印

#define PRINT(N,format) printf("The value of "#N" is "format"\n",N)
#include <stdio.h>
int main()
{
    int a = 10;
    PRINT(a,"%d");

    double pai = 3.14;
    PRINT(pai,"%lf");
}

运行结果:

##的作用:##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符

示例一:

#include <stdio.h>
#define CAT(name,num) name##num
int main()
{
    int class108 = 108;
    printf("%d\n", CAT(class, 108));//CAT(class, 108)的返回结果是class108
}

运行结果:

示例二:


#define ADD_TO_SUM(num, value) \
 sum##num += value;
//效果为sum5+=10;
int main()
{
    int sum5 = 0;
    ADD_TO_SUM(5, 10);
    printf("%d\n", sum5);
}

运行结果:10

注:这样的连接必须产生一个合法的标识符,否则其结果就是未定义的

带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果
副作用就是表达式求值的时候出现的永久性效果

示例一:

int main()
{
    int a=1;
    int b=a+1;
    int b=++a;
}

注意:

上面两个b都实现了在a的值基础上加1,但是产生了不同的效果:

  • 第一个b是a值未变,b加1 =》a+1;//不带副作用 a不改变

  • 第二个b是a值改变(a+1),b加一 =》a++;//带有副作用 a会改变

示例二:

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
//这里宏是替换后才计算
x = 5; y = 8; z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
 
//预处理后:z = ( (x++) > (y++) ? (x++) : (y++));
//(x++) > (y++) 比较时x=5>y=8 比较后x=6 y=9
//返回时y=9 返回后y=10
//输出结果:x=6 y=10 z=9

注意:对于带副作用的参数,宏体内可能会求值多次,结果难以预料

宏和函数对比

宏通常被应用于执行简单的运算

示例:比较大小

#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不用函数来完成这个任务?

原因有二:

1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多(这个可以写一个宏和函数,分别转到反汇编中去看,一看行数大小就可以一目了然了)

示例:

#define MAX(x,y) ((x)>(y)?(x):(y))
int Max(int x,int y)
{
    return x > y ? x : y;
}
int main()
{
    int a = 0;
    int b = 20;
    int c = 0;
    c = MAX(a, b);
    c = Max(a, b);
}

如何查看程序的汇编代码:https://blog.csdn.net/weixin_67916525/article/details/128888802

c = MAX(a, b)转反汇编:

c = Max(a, b)转反汇编:

所以宏比函数在程序的规模和速度方面更胜一筹

2. 更为重要的是函数的参数必须声明为特定的类型

所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以

用于>来比较的类型。

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

宏:
#define MAX(x,y) ((x)>(y)?(x):(y))

宏是类型无关的

宏的缺点:当然和函数相比宏也有劣势的地方:

1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序

的长度。

2. 宏是没法调试的

3. 宏由于类型无关,也就不够严谨。

4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到

#define MALLOC(num, type) (type *)malloc(num * sizeof(type))
 
MALLOC(10, int);//类型作为参数(开辟想要类型的空间)
//预处理器替换之后:(int *)malloc(10 * sizeof(int));

注:非常便利,简直是奇效~😄

宏和函数优劣表

那有没有可以把二者的优点结合起来的工具?

答:还真有,那就是内敛(联)函数(c99标准下)

内敛函数:https://blog.csdn.net/qq_33757398/article/details/81390151

📖如果你不想看那么多废话,我直接总结:

  • 普通函数用inline这个关键字后,就变成内敛函数

  • 内敛函数和宏一样,只有计算,没有函数调用和返回执行速度快

  • 内敛函数和函数一样,既没有优先级这样的缺点,也没有副作用

命名约定

一般来讲函数的宏的使用语法很相似(语言本身没法区分二者)

约定的命名习惯:

宏名全部大写//eg: c = MAX(a, b);
函数名不要全部大写//eg: c = Max(a, b);

注意:并不是所有的宏都是大写,有些是全小写,比如:offsetof

#undef

作用:

这条指令用于移除一个宏定义

语法:

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

示例:

#include<stdio.h>
#define M 10
int main()
{
    printf("%d\n", M);
#undef M
    printf("%d\n", M);
    return 0;
}

运行结果:

命令行定义

许多C的编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程

示例:vs演示不了,这里用linux演示

当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写)
#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; 
}
编译指令: gcc - D SZ = 10 programe . c
注:-D后面有无空格效果都一样

图示:

条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃可以使用条件编译

示例:

调试性的代码,删除可惜,保留又碍事,可以选择性的编译

原程序:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
    int arr[10] = { 0 };
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        arr[i] = i;
        printf("%d ",arr[i]);
    }
}

调试程序:运行

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
    int arr[10] = { 0 };
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        arr[i] = i;
#if 1//为真, printf("%d ",arr[i])运行
        printf("%d ",arr[i]);
#endif 
    }
}

调试程序:不运行

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
    int arr[10] = { 0 };
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        arr[i] = i;
#if 0//为假, printf("%d ",arr[i])不运行,不参与编译
        printf("%d ",arr[i]);
#endif 
    }
}

预处理情况:

常见的条件编译类型:

  • 单分支条件编译(参照上面的示例)

#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
又如:
#if 1+2
//..
#endif
  • 多个分支的条件编译

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

示例:

#define NUM 1
#include <stdio.h>
int main()
{
#if NUM==1
    printf("haha\n");
#elif NUM==2
    printf("hehe\n");
#else
    printf("xixi\n");
#endif
}

运行结果:

  • 判断是否被定义

#if defined(symbol)//是否定义
#ifdef symbol
 
#if !defined(symbol)//是否未定义
#ifndef symbol

示例:是否定义

#define NUM 1
int main()
{
#if defined(NUM)//等价于#ifdef NUM
    printf("如果NUM定义了,则参与编译\n");
#endif
}

运行结果:

示例:是否未定义

#define NUM 1
int main()
{
#if !defined(MAX)//等价于#ifndef MAX
    printf("如果MAX未定义,则参与编译\n");
#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 指令可以使另外一个文件被编译
  • 替换方式:

预处理器先删除这条指令,并用包含文件的内容替换

注:这样一个源文件被包含 10 次,那就实际被编译 10 次

头文件包含方式

本地文件包含:

  • 语法:

#include "filename"

图示:

解决方案资源管理器:

源文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>//引用库里面的头文件
#include "Add.h"//引用自己写的头文件
int main()
{
    int a = 1;
    int b = 2;
    int ret = Add(a, b);
    printf("%d\n",ret);
}

头文件:

int Add(int x,int y)
{
    return x + y;
}

注:包含库里面的头文件就用<>,包含自己写的头文件就用“”

  • 查找策略:

先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误

图示一:在源文件所在目录下查找

图示二:在标准位置查找头文件

  • Linux环境的标准头文件的路径:

/usr/include

  • VS环境的标准头文件的路径:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

注:按照自己的安装路径去找

库文件包含
  • 语法:

#include <filename.h>
  • 查找策略:

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误

注:对于库文件也可以使用 “” 的形式包含, 但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了

嵌套文件包含
在项目文件中会出现文件被重复包含的情形

图示:

comm.h和comm.c是公共模块。

test1.h和test1.c使用了公共模块。

test2.h和test2.c使用了公共模块。

test.h和test.c使用了test1模块和test2模块

注:这样最终程序中就会出现两份 comm.h 的内容,造成了文件内容的重复

解决方案

  • 示例1:条件编译

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif   //__TEST_H__

图示:

条件编译前

条件编译后

  • 示例2:

#pragma once
//避免头文件的重复引入

其他预处理指令

#error
#pragma
#line
...
不做介绍,自己去了解。
#pragma pack()在结构体部分介绍。

注:参考《C语言深度解剖》学习

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值