C 进阶 — 程序环境和预处理

news/2024/12/26 12:08:22/

C 进阶 — 程序环境和预处理

主要内容

程序的编译和执行环境

C 程序编译和链接

预定义符号

预处理指令 #define

预处理指令 #include

预处理指令 #undef

预处理操作符 # 和 ##

宏和函数对比

命令行定义

条件编译

一 程序的编译和执行环境

ANSI C 存在两个不同环境

1、编译环境,将源代码转换为可执行的机器指令

2、执行环境,实际执行代码

二 C 程序编译和链接

2.1 编译

程序的每个源文件通过编译过程转换为目标文件(object code),并由链接器(linker)捆绑在一起,形成一个完整的可执行程序

链接器会引入程序用到的 C 库函数,及个人函数库,将所需函数链接到程序中

image-20241225190320147

查看编译过程
#include <stdio.h>
int main()
{int i = 0;for(i=0; i<10; i++){printf("%d ", i);}return 0;
}

1、预处理 gcc -E print_arr.c -o print_arr.i 预处理完成后停止,结果保存在 print_arr.i 文件中

2、编译 gcc -S print_arr.c 编译完成后停止,结果保存在 print_arr.s

3、汇编 gcc -c print_arr.c 汇编完成后停止,结果保存在 print_arr.o

示例代码

//sum.c
int g_val = 2016;
void print(const char *str)
{printf("%s\n", str);
}//test.c
#include <stdio.h>
int main()
{extern void print(char *str);extern int g_val;printf("%d\n", g_val);print("hello bit.\n");return 0;
}

image-20241225195007984

image-20241225195207023

2.2 运行环境

程序执行过程

1、程序必须载入内存中,在有操作系统的环境中,一般由操作系统完成。在独立环境中,程序载入通过可执行代码置入只读内存完成

2、程序开始执行,调用 main 函数

3、执行程序代码,程序使用一个运行栈(stack),存储函数局部变量和返回地址。也可使用静态(static)内存,存储于静态内存中的变量生命周期是整个程序的执行过程

4、终止程序,main 函数正常终止,或意外终止

三 预处理

3.1 预定义符号

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

预定义符号是语言内置的,如下例

printf("FILE %s LINE %d\n", __FILE__, __LINE__);

3.2 #define

3.2.1 #define 定义标识符
//语法:
#define name stuff
#define MAX 1000
#define reg register  //为 register 关键字, 创建一个简短的名字
#define do_forever for(;;)   //用更形象的符号来替换一种实现
#define CASE break;case      //写 case 语句时自动把 break 补上// 如果定义的 stuff 过长, 可分成几行写, 除最后一行外, 每行后面加一个反斜杠 ( 续行符 )
#define DEBUG_PRINT printf("FILE:%s\tLINE:%d\t \date:%s\ttime:%s\n" ,\__FILE__,__LINE__ ,       \__DATE__,__TIME__ )

在 define 定义标识符时,能不能在最后加上 ;

如下例

#define MAX 1000;
#define MAX 1000//建议不要加上 ; 容易导致问题, 比如下面场景
if(condition)max = MAX;
elsemax = 0;
3.2.2 #define定义宏

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

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

注意 参数列表左括号必须与 name 紧邻,两者间有任何空白存在,参数列表就会被解释为 stuff 的一部分

宏示例

#define SQUARE(x) x * xint a = 5;
printf("%d\n" ,SQUARE( a + 1) ); //打印 11

预处理器文本替换时,参数 x 被替换成 a + 1,所以上述语句实际上变成了 printf ("%d\n",a + 1 * a + 1 );

#define SQUARE(x) (x) * (x)int a = 5;
printf("%d\n" ,10 * DOUBLE(a)); //打印 55

替换后 printf ("%d\n",10 * (5) + (5));

因此 #define DOUBLE( x) ( ( x ) + ( x ) )

总结:用于对数值表达式进行求值的宏定义,都应该用上述方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符间不可预料的相互作用

3.2.3 #define 替换规则

#define 定义符号和宏时涉及几个步骤

1、在调用宏时,首先对参数检查,是否包含任何由 #define 定义的符号,如果是它们首先被替换

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

3、再次对结果文件进行扫描,看它是否包含任何由 #define 定义的符号,如果是则重复上述过程

注意:

1、宏参数和 #define 定义中可以出现其它 #define 定义的符号,但宏不能递归

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

3.2.4 # 和

如何把参数插入到字符串中 ?

char* p = "Hello ""World\n";
printf("Hello"" World\n"); //输出 Hello World
printf("%s", p);		   //输出 Hello World

字符串有自动连接的特点

#define PRINT(FORMAT, VALUE)\printf("the value is "FORMAT"\n", VALUE);
...
PRINT("%d", 10);
//这里只有当字符串作为宏参数时, 才可以把字符串放在字符串中

使用 # 把一个宏参数变成对应的字符串

int i = 10;
#define PRINT(FORMAT, VALUE)\printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
...
PRINT("%d", i+3); //the value of i+3 is 13

代码中的 #VALUE 会预处理器处理为 "VALUE"

## 的作用

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

#define ADD_TO_SUM(num, value) \sum##num += value;
...
int sum5 = 0;
ADD_TO_SUM(5, 10);		//作用是:给 sum5 增加 10

注:连接结果必须是合法标识符,否则结果是未定义的

3.2.5 带副作用的宏参数

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

x+1;		//不带副作用
x++;		//带有副作用#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=6 y=10 z=9
3.2.6 宏和函数对比

宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个

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

为什么不使用用函数

1、调用函数有开销,宏比函数效率更高

2、函数参数必须声明特定类型,宏是类型无关的

宏的缺点

1、除非宏比较短,否则可能大幅度增加程序长度

2、宏没法调试

3、宏由于类型无关,不够严谨

4、宏可能会带来运算符优先级问题

宏有时可以做函数做不到的事,比如:宏参数可以使用类型,但函数不行

#define MALLOC(num, type)\(type *)malloc(num * sizeof(type))
...
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 * sizeof(int));

宏和函数的对比

image-20241225205145944

一般的命名约定

宏名全部大写

函数名不要全部大写

3.3 #undef

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

3.4 命令行定义

许多 C 编译器允许在命令行中定义符号,用于启动编译过程

例如:当同一个源文件要编译出一个程序的不同版本时(假定某程序中声明了一个某长度数组,如果机器内存有限,需要一个很小数组,但是另外一个机器内存大些,需要一个数组能够大些)

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

编译指令

gcc -D ARRAY_SIZE=10 programe.c

3.5 条件编译

条件编译,在编译程序时将一条语句(一组语句)编译或者放弃编译

#include <stdio.h>
#define __DEBUG__
int main()
{int i = 0;int arr[10] = {0};for(i=0; i<10; i++){arr[i] = i;#ifdef __DEBUG__printf("%d\n", arr[i]);	 //观察数组是否赋值成功 #endif 					 //__DEBUG__}return 0;
}

常见的条件编译指令

//1.
#if 常量表达式//...
#endif
//常量表达式由预处理器求值
如:
#define __DEBUG__ 1
#if __DEBUG__//..
#endif//2.多个分支的条件编译
#if 常量表达式//...
#elif 常量表达式//...
#else//...
#endif//3.判断是否被定义
#if defined(symbol)
#ifdef symbol#if !defined(symbol)
#ifndef symbol//4.嵌套指令
#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif

3.6 文件包含

#include 指令可以使另外一个文件被编译,就像它实际出现于 #include 指令的地方一样

替换的方式:

  1. 预处理器先删除这条指令,并用包含文件的内容替换
  2. 这样一个源文件被包含 10 次,那就实际被编译 10 次
3.6.1 头文件被包含的方式

本地文件包含

#include "filename"

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

//Linux 环境标准头文件路径
/usr/include//VS2013 环境标准头文件路径
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

库文件包含

#include <filename.h>

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

3.6.2 嵌套文件包含

如下图 comm.hcomm.c 是公共模块。test1.htest1.c 使用了公共模块,test2.htest2.c 使用了公共模块,test.htest.c 使用了 test1 模块和 test2 模块。这样最终程序中就会出现两份 comm.h 内容,造成了文件内容重复

image-20241225225211027

使用条件编译解决上述问题

每个头文件的开头写

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

或者

#pragma once

避免头文件的重复引入

四 其他预处理指令

#error
#pragma
#line

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


http://www.ppmy.cn/news/1558262.html

相关文章

Docker怎么关闭容器开机自启,批量好几个容器一起操作?

环境&#xff1a; WSL2 docker v25 问题描述&#xff1a; Docker怎么关闭容器开机自启&#xff0c;批量好几个容器一起操作&#xff1f; 解决方案&#xff1a; 在 Docker 中&#xff0c;您可以使用多种方法来关闭容器并配置它们是否在系统启动时自动启动。以下是具体步骤和…

第四节、电机定角度转动【51单片机-L298N-步进电机教程】

摘要&#xff1a;本节介绍电机转动角度计算步骤&#xff0c;从而控制步进电机转角 一、 计算过程 1.1 L28N每控制步进电机转动一步&#xff0c;根据程序拍数设置情况&#xff0c;计算步进电机步距角度step_x s t e p x s t e p X … … ① step_{x} \frac{step}{X} ……① s…

Zettlr(科研笔记) v3.4.1 中文版

Zettlr是款适合写作者和研究人员使用的Markdown编辑器&#xff0c;免费开源&#xff0c;功能简洁&#xff0c;具备Markdown所有基本功能&#xff0c;内置各种运算符&#xff0c;还可以调用计数器&#xff0c;可以完美替代Word和收费的文字处理器。 软件特点 从应用程序中直接管…

ROS1入门教程6:复杂行为处理

一、新建项目 # 创建工作空间 mkdir -p demo6/src && cd demo6# 创建功能包 catkin_create_pkg demo roscpp rosmsg actionlib_msgs message_generation tf二、创建行为 # 创建行为文件夹 mkdir action && cd action# 创建行为文件 vim Move.action# 定义行为…

论文阅读--Variational quantum algorithms

文献类型&#xff1a;期刊论文 作者&#xff1a;M. Cerezo&#xff08;Los Alamos National Laboratory&#xff09; 年份&#xff1a;2021 期刊&#xff1a;Nature 影响因子&#xff1a;44.8 摘要&#xff1a;由于计算成本极高&#xff0c;模拟复杂量子系统或解决大规模线性代…

deepin 安装 zookeeper

deepin 安装 zookeeper 1、升级软件 sudo apt updatesudo apt -y dist-upgrade2、安装常用软件 sudo apt -y install gcc make openssl libssl-dev libpcre3 libpcre3-dev libgd-dev \rsync openssh-server vim man zip unzip net-tools tcpdump lrzsz tar wget3、开启ssh …

工业自动化通信方式解析:串口通信、网口通信与PLC通信

在工业自动化领域&#xff0c;通信是实现设备之间数据交互和控制的关键。常见的通信方式包括串口通信、网口通信和PLC通信。不同的通信方式有其独特的特点、优势和适用场景&#xff0c;本文将对这三种通信方式进行深入解析&#xff0c;帮助您在实际项目中选择合适的通信方式。 …

【UE5 C++课程系列笔记】11——FString、FName、FText的基本使用

目录 概念 常用操作示例 一、FString 1.1 创建字符串 1.2 字符串拼接 1.3 字符串长度 1.4 字符串查找 1.5 字符串替换 1.6 比较字符串 二、FName 2.1 创建FName 2.2 比较FName 2.3 在容器中使用 FName 三、FText 3.1 创建FText 3.2 格式化FText 3.3 显示文本…