C语言-程序环境与预处理
程序怎么诞生与运行
基本预处理符号与指令
如何书写宏
例子一:offsetof 偏移量(结构体类型,成员变量)
例子二:二进制奇偶位互换(int)
语句型
返值型
getc()与getchar()有些编译器上是宏定义的,实际上就是接受了stdin(可以理解为键盘输入的文件指针)
宏与函数的区别与优劣
一些不常见的指令
Ⅰ.程序怎么诞生与运行
预编译(预处理)(显然,这个过程快于一切,所以后面代码中提到宏就会被替换,而不能作为变量名)
在这个过程中,编译器会将源文件的文本进行“更改”
将注释忽略,头文件内容的搬运,宏对源文件某位置内容的替换,等一系列预处理指令
这个过程是完全替代的过程,是本次博客的重点,等一下会重点讲
遇到“嵌套的情况”,编译器只要检测有文本中仍然
编译
语法错误,编译错误
在这个过程中,编译器会将你写的代码编译会汇编代码
语法分析
词法分析
语义分析
符号汇总
汇编
一般汇编不会出错
在这个过程,编译器会将汇编代码,汇编为一系列东西,如
形成符号表
汇编指令,二进制指令,形成(.o文件)(二进制文件)
链接
在这个过程,链接器会将(.o文件)结合“链接库”合并成段表,完成符号表的合并和符号表的重定位
一般是隔离编译,一起链接的(多个文件编译完,一切链接)
这就好说明为什么没有定义函数,但是调用了它,显示的是链接错误
运行
运行错误,与目标实际情况不符,说明思路有问
可以在Linux环境下去查看这个过程,通过指令
Ⅱ.基本预处理符号与指令&基本预处理符号与指令
2.1 预定义符号 (宏是替换的,可以作为数组元素个数,因为有些编译器不允许变长数组1)
这些都是系统内置的,直接可以用!
FILE //进行编译的源文件
LINE //文件当前的行号
DATE //文件被编译的日期
TIME //文件被编译的时间
STDC //如果编译器遵循ANSI C,其值为1,否则未定义
printf("file:%s line:%d\n", __FILE__, __LINE__);
2.2 #define
#define 可以定义符号,也可以定义宏,但是我理解为都是宏,因为都是替换的过程,只不过符号的话,相当于没“形参”(如果宏那边形参没用到,那就跟替换没区别),省略括号
2.2.1 书写宏
#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 max 5 + 6 int main() { printf("%d ",max * 6); return 0; }
一定要注意这里,替换过来是 5 + 6 * 6,所以结果并不是 66,而是 41,应该是 #define max (5 + 6)
#define pow(x) x * x int main() { printf("%d ", max(5 + 6)); return 0; }
同样的,预处理不会对非预处理的地方进行运算,所以这里是把 5 + 6 这个整体传了过去,也就是-> 5 + 6 * 5 + 6 ,结果不是 121,而是 41,应该是#define pow(x)((x) * (x))
2.2.2 #define 的工作就是
有参考数的时候,将参数整体传到“形参”里,然后将定义的宏里对应位置替换了,然后再替换到原来的位置,最后这句指令在可能多次预处理后,删除
没参数的时候,后面的内容完美无缺地替换过去
2.2.3 例子
offsetof模拟
C/C++库函数查询网址:网址
offsetof (type,member)
返回值 type member
无 结构体类型名 成员变量名
可能你发现了,这里宏的好处体现了,没有类型,但是这里得是结构体,虽然你要怎么定义宏无所谓,但是替换过去能运行才行
#define OFFSETOF(TYPE,MEMBER) (int)&(((TYPE*)0)->MEMBER) // -0 省略了
你可能有一个顾虑,就是这里用了0->C语言的NULL,受保护的0地址空指针(java中null为不指向任何),但是这里只是用了这个地址的性质找到成员变量的地址罢了,并没有对值进行修改,也没有把地址引用的东西怎么怎么滴,所以没关系
我的代码仓库,码云
奇数偶数位互换
就是把一个int类型的变量的值,奇数的二进制位与偶数的二进制位互换
类函数型:
#define CHANGE(N)\ int M = 0; \ int i = 0; \ for (i = 0; i < 32; i++)\ {\ if (i % 2 == 0)\ M |= (((N) >> 1) & (1 << i)); \ else \ M |= (((N) << 1) & (1 << i)); \ }\ n=M;
返回值型
#define C(N) ((0b01010101010101010101010101010101&(N))<<1)\ +((0b10101010101010101010101010101010&(N))>>1) //不会影响原来变量的值
Ⅲ.函数与宏的对比
属性 宏 函数
代码长度 每次出现宏都会替换可能会大大增加代码长度 调用函数就可以了,长度就那么多
执行速度 快,“就是走下去一路没有阻碍,不用开辟空间然后各种操作” 需要给函数开辟空间,调用完需要销毁,这样就要花时间
操作符优先级 没进行运算,是替换,所以容易出错 运算结果直接传参
带有副作用的表达式参数 可能会一错再错,因为可能如果替换很多次,这个副作用就更大了!(副作用是指,这个表达式会影响变量的值,例如,i++ , i = 1) 只会副作用一次,就是把运算结果传过去就好
参数类型 无顾虑 C中没有java的重载,C特别要顾虑类型,不然就会导致结果不对,甚至报错
调试 替换了之后,按 F11 不能查看情况 按 F11 可查看函数情况,容易找到错误的源头
递归 无递归 有递归,一些必须递归写的(如,汉诺塔)只有函数能写
Ⅳ.其他的预处理指令
#undef 将 #define 定义的内容取消掉,具体操作就是将这一指令以后的的宏
“ # 与 ## ”
"#"就是将宏里面的符 # 连接的参数(只有宏形参可以)转化为字符串
"##"就是连接两个左右符号,然后当作新符号
这里可以当作是预处理之前的预处理,在宏把形参替换后,他们就开始改变宏定义的文本了
这里有一个小技巧,就是两个字符串之间会直接连接,例如,“abc”“cde”-> “abccde” 两个相邻双引号融合
int i = 10;
#define PRINT(FORMAT, VALUE)\
printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
...
PRINT("%d", i+3);//产生了什么效果?
通过这个宏,我们实现了打印变量名的要求!
#define ADD_TO_SUM(num, value) \ sum##num += value; ... ADD_TO_SUM(5, 10);//作用是:给sum5增加10. //当然如果你这里的5如果是变量,它并不会把值代入进去, //例如 x ,它会说 numx += 10
♥条件编译
就是通过一些处理,让此次运行与调试不经过一些代码段
1.
#if 常量表达式 //... #endif //常量表达式由预处理器求值。 如: #define __DEBUG__ 1 #if __DEBUG__ //.. #endif
2.多个分支的条件编译
#if 常量表达式(例如 1 > 2) //... #elif 常量表达式 //... #else //... #endif
3.判断是否被定义
#if defined(symbol) #ifdef symbol #if !defined(symbol) #ifndef symbol 4.嵌套指令 #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
如 #if 后为真,则保留,假则删除
必须有#endif 否则不知道删除与保留多少
#if defined 与#if !defined + 宏,(这个是在代码运行前,所以不应该写任何变量)意思是如果被定义就保留
等价于#ifdef 与#ifndef
#elif,支持分支,有elif defined 没有elifdef,因为defined + 宏 相当于一个预处理的真假判断式!
♥头文件包含,#include,
当然include很常见,但是放在这里讲更方便,因为include,可以包含宏定义
原理
用 < > 会直接到库里面找头文件 #include<stdio.h>
用 “ ” 会先从你的相对文件路径(也可以绝对路径的文件名)下找有没有这个头文件,否则到库里找,如果找不到就报错 #include "filename"
然后将文件内的内容复制粘贴过来,当然宏也要替换掉
头文件包含嵌套重复
就是头文件可能在以后包含的时候被多次包含,并且头文件里可能也包含了其他头文件,等等,如果每次包含都会增加代码长度,那就得不偿失了
所以结合刚才的知识,有了这个解决方法
#ifndef __TEST_H__//第一次可以包含,第二次将跳过 #define __TEST_H__ //头文件的内容 //头文件的内容 //头文件的内容 #endif //__TEST_H__
一些IDE2,在建立头文件是时候会自动添加:>
#pragma once 一样能解决问题 ♥其他的 #error #pragma #line ...
不做介绍,自己去了解吧。
不过 #pragma , 刚才用到了,还有个用处
#pragma pack(4) ->结构体的默认对齐数
文章到此结束!谢谢观看 —>内容参考【比特科技】
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭🦆!
这是我的代码仓库!(在马拉圈的22.12里)代码仓库
邮箱:2040484356@qq.com
并不是会变长的数组,是以变量作为数组长度的数组 ↩︎
集成开发环境 ↩︎