预处理, 展开头文件/宏替换/去掉注释/条件编译 (test.i main .i)
编译, 检查语法,生成汇编 ( test.s main .s)
汇编, 汇编代码转换机器码 (test.o main.o)
链接 链接到一起生成可执行程序 a.out
预处理名称 | 意义 |
#define | 宏定义 |
#undef | 撤销已定义的宏 |
#include | 使编译器将另一个源文件 嵌入本文件中 |
#if | #if #else #endif 的作用是控制编译的代码 如: #if 1 代码A #else 代码B #endif 则程序编译的时候就只编译代码A 如果 #if 0 代码A #else 代码B #endif 则程序编译的时候就只编译代码B 其中代码A和代码B可以是函数也可以是几句代码。 所以 #if #else #endif 只是一个起控制预编译的代码;如果不加#,当然就是一个简单的判断语句。 |
#elif | |
#else | |
#endif | |
#ifdef | #ifdef macro如果宏被使用#define 定义,命令后边的代码被编译。 #ifndef macro如果宏没有被使用#define 定义,命令后边的代码被编译。 A few side notes: #elif 命令是一种缩略形式,它和”elseif”具有同样的作用, 你也可以在#if之后使用”defined”或者”!defined”来扩展功能。 下边是一个例子: #ifdef DEBUG cout << "This is the test version, i=" << i << endl; #else cout << "This is the production version!" << endl; #endif你应该注意到,这个例子和在代码中插入/删除大量的”cout”语句相比,使调试变得更容易 |
#ifndef | |
#error | 用于生成一个编译错误消息 用法: #error message message不需要用双引号包围 |
#error编译指示字用于自定义程序员特有的编译错误消息,类似的,#warning用于生成编译警告。
#error是一种预编译指示字,可用于提示编译条件是否满足
#line编译指示字的本质是重定义 __LINE__ 和 __FILE__(由此可以看出来是由预处理进行处理的)
#pragma #pragma 用于指示编译器完成一些特定的动作。#pragma 所定义的很多指示字是编译器特有的,在不同的编译器间是不可移植的。 预处理期将忽略它不认识的 #pragma 指令,不同的编译器可能以不同的方式解释同一条 #pragma 指令。一般用法:#pragma parameter。 注意:不同的 parameter 参数语法和意义各不相同! #pragma message:a> message 参数在大多数的编译器中都有相似的实现;b> message 参数在编译时输出消息到编译输出窗口中;c> message 用于条件编译中可提示代码的版本信息。它与 #error 和 #warning 不同,#pragma message 仅仅代表一条编译消息,不代表程序错误。还有下列几种预处理宏(是双下划线)
__LINE__ 表示正在编译的文件的行号
__FILE__表示正在编译的文件的名字__DATE__表示编译时刻的日期字符串,例如: "25 Dec 2007"
__TIME__ 表示编译时刻的时间字符串,例如: "12:30:55"
__STDC__ 判断该文件是不是定义成标准 C 程序
- 宏优点1代码复用性2提高性能
- 宏缺点1 不可调试(预编译阶段进行了替换),2无类型安全检查3可读性差,容易出错。
- 编译
编译阶段是检查语法,生成汇编,这个属于程序员的必备知识,我们学习一门语言第一步就是知晓语法,其中比较生涩的有左值右值,指针的使用,内存的管理,数据结构的使用,这将会是一场持久战 ,贯穿在整个学习生涯。
在这里我截取优先级问题,这个可能会通过编译但是不一定达到程序员想要的结果。
汇编
- 汇编代码转换机器码 这个阶段,非底层的程序员不需要考虑
- 编译器不会搞错的。也与c/c++开发者无关,但是我们可以利用反汇编来调试代码,学习汇编语言依然是必备的。
链接
下百度百科的介绍
静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。
链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一块生成可执行程序。
静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。
动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。
仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。
当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。
将源文件中用到的库函数与汇编生成的目标文件.o合并生成可执行文件。该可执行文件会变大很多,一般是调用自己电脑上的。
静态库和应用程序编译在一起,在任何情况下都能运行,而动态库是动态链接,文件生效时才会调用。
很多代码编译通过,链接失败就极有可能在静态库和动态库这出现了纰漏,要视情况解决。缺少相关所需文件,就会链接报错。这个时候就要检查下本地的链接库是不是缺损。
预处理器的粘合剂:## 运算符
和 # 运算符一样,## 运算符可以用于类函数宏的替换部分。另外,## 运算符还可用于类对象宏(object-like macro)的替换部分。这个运算符把两个语言符号组合成单个语言符号。例如,可以定义如下宏:
#define XNAME(n) x ## n
宏调用 XNAME(4) 会展开成 x4 。
说明:类对象宏就是用来代表值的宏。如,#define PI 3.141593 中的PI。
#include <stdio.h>
#define XNAME(n) x ## n
#define PRINT_XN(n) printf("x" #n " = %d/r/n", x ## n);
int main(void)
{
int XNAME(1) = 14; // 变为 int x1 = 14;
int XNAME(2) = 20; // 变为 int x2 = 20;
PRINT_XN(1) // 变为 printf("x1 = %d/r/n", x1);
PRINT_XN(2) // 变为 printf("x2 = %d/r/n", x2);
return 0;
}
// 输出:
x1 = 14
x2 = 20
可变参数宏 …和_ _VA_ARGS_ _
_ _VA_ARGS_ _ 是一个可变参数的宏,很少人知道这个宏,这个可变参数的宏是新的C99规范中新增的,目前似乎只有gcc支持(VC6.0的编译器不支持)。
实现思想就是宏定义中参数列表的最后一个参数为省略号(也就是三个点)。这样预定义宏_ VA_ARGS _就可以被用在替换部分中,替换省略号所代表的字符串
#define PR(...) printf(__VA_ARGS__)
int main()
{
int wt=1,sp=2;
PR("hello\n");
PR("weight = %d, shipping = %d",wt,sp);
return 0;
}
输出结果:
hello
weight = 1, shipping = 2
省略号只能代替最后面的宏参数。
#define W(x,...,y)错误!
#include< >和#include“ ”的区别
- #include< > 引用的是编译器的类库路径里面的头文件。
假如你编译器定义的自带头文件引用在 C:\Keil\c51\INC\ 下面,则 #include<stdio.h> 引用的就是 C:\Keil\c51\INC\stdio.h 这个头文件,不管你的项目在什么目录里, C:\Keil\c51\INC\stdio.h 这个路径就定下来了,一般是引用自带的一些头文件,如: stdio.h、conio.h、string.h、stdlib.h 等等。
- #include" " 引用的是你程序目录的相对路径中的头文件。
假如你的项目目录是在 D:\Projects\tmp\ ,则 #include"my.h" 引用的就是 D:\Projects\tmp\my.h 这个头文件,一般是用来引用自己写的一些头文件。如果使用 #include" " ,它是会先在你项目的当前目录查找是否有对应头文件,如果没有,它还是会在对应的引用目录里面查找对应的头文件。例如,使用 #include “stdio.h” 如果在你项目目录里面,没有 stdio.h 这个头文件,它还是会定位到 C:\Keil\c51\INC\stdio.h 这个头文件的。
_Generic(泛型)
- _Generic(泛型)的参数:
_Generic ( assignment-expression , generic-assoc-list )
_Generic((var), type1 : …, type2 : … default : …)
第一个参数为一个表达式,
接下来的参为 一个类型 一个冒号 一个值
default表示其他类型,也就是所定义的type中没有的类型,就会跳到default!
_Generic (x,int:1,char:'I',char * str:"MY NAMES ",default:0)
# 宏定义
#define MY_TYPE(x) _Generic((x),\
int:,\
char:\,
default:'other')
int main(){
int a=8;
printf("%s",MY_TYPE(a));
}
---
结果:int
对用宏定义必须 将逻辑 定义为一行代码,或定义为多行,但每行使用“\”连接
内联函数
在C语言中,如果一些函数被频繁调用,不断地有函数入栈,即函数栈,会造成栈空间或栈内存的大量消耗。
为了解决这个问题,特别的引入了inline修饰符,表示为内联函数。
栈空间就是指放置程式的局部数据也就是函数内数据的内存空间,在系统下,栈空间是有限的,
假如频繁大量的使用就会造成因栈空间不足所造成的程式出错的问题,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭。
#include <stdio.h>
//函数定义为inline即:内联函数
inline char* dbtest(int a)
{
return (i % 2 > 0) ? "奇" : "偶";
}
int main()
{
int i = 0;
for (i=1; i < 100; i++)
{
printf("i:%d 奇偶性:%s /n", i, dbtest(i));
}
}
上面的例子就是标准的内联函数的用法,使用inline修饰带来的好处我们表面看不出来,其实在内部的工作就是在每个for循环的内部任何调用dbtest(i)的地方都换成了(i%2>0)?“奇”:"偶"这样就避免了频繁调用函数对栈内存重复开辟所带来的消耗。
- 关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。
如下风格的函数Foo 不能成为内联函数:
inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y){}
而如下风格的函数Foo 则成为内联函数:
void Foo(int x, int y);
inline void Foo(int x, int y) // inline 与函数定义体放在一起{}
- inline的使用是有所限制的
- inline只适合涵数体内代码简单的函数数使用,不能包含复杂的结构控制语句例如while、switch,并且内联函数本身不能是直接递归函数(自己内部还调用自己的函数)。
- 省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。
- 另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
- 以下情况不宜使用内联:
- (1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
- (2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了inline 不应该出现在函数的声明中)。