[TOC]
# 基本概念
c语言对源程序处理的四个步骤:预处理,编译,汇编,链接
预处理是在程序源代码被编译之前,由预处理器(Preprocessor)对程序源代码进行的处理.这个过程并不对程序的源代码语法进行解析,但它会把源代码分割或处理成为特定的符号为下一步的编译做准备工作
# 文件包含指令
文件包含处理是指一个源文件可以将另外一个文件的全部内容包含进来.c语言提供了`#include`命令用来实现"文件包含"的操作

## `#include<>`和`#include ""`区别
* ""表示系统先在file1.c所在的当前目录找file1.h,如果找不到,再按系统指定的目录检索
* `<>`表示系统直接按系统指定的目录检索
注意:
1. `#include<>`常用于包含库函数的头文件
2. `#include ""`常用于包含自定义的头文件
3. 理论上`#include`可以包含任意格式的文件(`.c .h`等),但一般用于头文件的包含
# 宏参数的字符串化
`#` 的用法
**#用来将宏参数转换为字符串**,也就是在宏参数的开头和末尾添加引号。例如有如下宏定义:
`#define STR(s) #s`
那么:
~~~
printf("%s", STR(c.biancheng.net));
printf("%s", STR("c.biancheng.net"));
~~~
分别被展开为:
~~~
printf("%s", "c.biancheng.net");
printf("%s", "\"c.biancheng.net\"");
~~~
可以发现,即使给宏参数“传递”的数据中包含引号,使用#仍然会在两头添加新的引号,而原来的引号会被转义。
将上面的例子补充完整:
~~~
#include <stdio.h>
#define STR(s) #s
int main() {
printf("%s\n", STR(c.biancheng.net));
printf("%s\n", STR("c.biancheng.net"));
return 0;
}
运行结果:
c.biancheng.net
"c.biancheng.net"
~~~
注意: 对空格处理
当传入参数名间存在空格时,编译器将会自动连接各个子字符串,用每个子字符串中只以一个空格连接,忽略其中多余一个的空格
~~~
printf("%s\n", STR(abc 123));
~~~
输出
~~~
abc 123
~~~
# 宏参数的连接
`##`的用法
`##`称为连接符,用来将宏参数或其他的串连接起来。例如有如下的宏定义:
~~~
#define CON1(a, b) a##e##b
#define CON2(a, b) a##b##00
~~~
那么:
~~~
printf("%f\n", CON1(8.5, 2));
printf("%d\n", CON2(12, 34));
~~~
将被展开为:
~~~
printf("%f\n", 8.5e2);
printf("%d\n", 123400);
~~~
将上面的例子补充完整:
~~~
#include <stdio.h>#define CON1(a, b) a##e##b#define CON2(a, b) a##b##00int main() { printf("%f\n", CON1(8.5, 2)); printf("%d\n", CON2(12, 34)); return 0;}
~~~
运行结果:
~~~
850.000000
123400
~~~
注意: 当用`##`连接形参时,`##`前后的空格可有可无
另外,如果`##`后的参数本身也是一个宏的话,`##`会阻止这个宏的展开
# 宏定义
## 无参数的宏定义(宏常量)
如果在程序中大量用到了100这个值,那么为了方便管理,我们可以将其定义为: `const int num = 100;`
但是如果我们使用num定义一个数组,在不支持c99标准的编译器上是不支持的,因为num不是一个编译器常量,如果想得到一个编译器常量,那么可以使用:
~~~
#define num 100
~~~
在编译预处理时,将程序中在该语句以后出现的所有的num都用100代替.这种写法使用户能以一个简单的名字代替一个长的字符串,在预处理时将宏名替换成字符串的过程称为**宏展开**.宏定义,只在宏定义的文件中起作用
说明:
1. 宏名一般用于大写,以便于与变量区别
2. 宏定义可以是常数,表达式等
3. 宏定义不作语法检查,只有在编译被宏展开后的源程序才会报错
4. 宏定义不是c语言,不在行末加分号
5. 宏名有效范围为从定义到本源文件结束
6. 可以用`#undef`命令终止宏定义的作用域
## 带参数的宏定义(宏函数)
在项目中,经常把一些短小又频繁使用的函数写成宏函数,这是由于宏函数没有普通函数参数压栈,跳转,返回等开销,可以调高程序的效率
宏通过使用参数,可以创建外形和作用都与函数类似的类函数宏(function-like macro)
宏的参数也用园括号括起来
~~~
#define SUM(x, y) ((x) + (y))
void test() {
//仅仅只是做文本替换,下边替换为int ret = ((10) + (20));
//不进行计算
int ret = SUM(10, 20);
printf("ret:%d\n", ret);
}
~~~
注意:
1. 宏的名字中不能有空格,但是在替换的字符串中可以有空格.ANSIC允许在参数列表中使用空格
2. 用括号括住每一个参数,并括住宏的整体定义
3. 用大写字母表示宏的函数名
4. 如果打算宏代替函数来加快程序运行速度.假如在程序中只使用一次宏对程序的运行时间没有太大提高
# 一些特殊的预定义宏
c编译器,提供了几个特定形式的预定义宏,在实际编程中可以直接使用,很方便
~~~
//__FILE__ 宏所在文件的源文件名
//__LINE__ 宏所在行的行号
//__DATE__ 代码编译的日期
//__TIME__ 代码编译的时间
int main() {
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
getchar();
return 0;
}
~~~
输出
~~~
/Users/jdxia/Desktop/study/studyc/main.c
7
Oct 19 2019
22:35:14
~~~
# 条件编译
一般情况下,源程序中所有的行都参加编译.但是有时希望对部分源程序行只在满足一定条件下才编译,即对这部分源程序行指定编译条件
~~~
//#define FLAG
#define FLAG
#ifdef FLAG
void func(long a) {
}
#else
void func(int a) {
}
#endif
~~~

## `#if`的用法
~~~
#include <stdio.h>
int main(){
#if _WIN32
printf("This is Windows!\n");
#else
printf("Unknown platform!\n");
#endif
#if __linux__
printf("This is Linux!\n");
#endif
return 0;
}
~~~
## `#ifdef` 的用法
#ifdef 用法的一般格式为:
~~~
#ifdef 宏名
程序段1
#else
程序段2
#endif
~~~
它的意思是,如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。
也可以省略 `#else`:
~~~
#ifdef 宏名
程序段
#endif
~~~
VS/VC 有两种编译模式,Debug 和 Release。在学习过程中,我们通常使用 Debug 模式,这样便于程序的调试;而最终发布的程序,要使用 Release 模式,这样编译器会进行很多优化,提高程序运行效率,删除冗余信息。
为了能够清楚地看到当前程序的编译模式,我们不妨在程序中增加提示,请看下面的代码:
~~~
#include <stdio.h>
#include <stdlib.h>
int main(){
#ifdef _DEBUG
printf("正在使用 Debug 模式编译程序...\n");
#else
printf("正在使用 Release 模式编译程序...\n");
#endif
system("pause");
return 0;
}
~~~
当以 Debug 模式编译程序时,宏 \_DEBUG 会被定义,预处器会保留第 5 行代码,删除第 7 行代码。反之会删除第 5 行,保留第 7 行
## `#ifndef`的用法
`#ifndef` 用法的一般格式为:
~~~
#ifndef 宏名
程序段1
#else
程序段2
#endif
~~~
与 `#ifdef` 相比,仅仅是将 `#ifdef` 改为了 `#ifndef`。它的意思是,如果当前的宏未被定义,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 `#ifdef` 的功能正好相反。
## 三者之间的区别
最后需要注意的是,#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。
例如,下面的形式只能用于 #if:
~~~
#include <stdio.h>
#define NUM 10
int main(){
#if NUM == 10 || NUM == 20
printf("NUM: %d\n", NUM);
#else
printf("NUM Error\n");
#endif
return 0;
}
~~~
运行结果:
NUM: 10
再如,两个宏都存在时编译代码A,否则编译代码B:
~~~
#include <stdio.h>
#define NUM1 10
#define NUM2 20
int main(){
#if (defined NUM1 && defined NUM2)
//代码A
printf("NUM1: %d, NUM2: %d\n", NUM1, NUM2);
#else
//代码B
printf("Error\n");
#endif
return 0;
}
~~~
运行结果:
NUM1: 10, NUM2: 20
`#ifdef` 可以认为是 `#if defined` 的缩写
# `#error`命令
#error 指令用于在编译期间产生错误信息,并阻止程序的编译,其形式如下:
~~~
#error error_message
~~~
例如,我们的程序针对 Linux 编写,不保证兼容 Windows,那么可以这样做:
~~~
#ifdef WIN32
#error This programme cannot compile at Windows Platform
#endif
~~~
WIN32 是 Windows 下的预定义宏。当用户在 Windows 下编译该程序时,由于定义了 WIN32 这个宏,所以会执行 `#error `命令,提示用户发生了编译错误,错误信息是:
~~~
This programme cannot compile at Windows Platform
~~~
这和发生语法错误的效果是一样的,程序编译失败。请看下面的截图:

需要注意的是:报错信息不需要加引号`" "`,如果加上,引号会被一起输出。例如将上面的 #error 命令改为:
~~~
#error "This programme cannot compile at Windows Platform"
~~~
那么错误信息如下:

再如,当我们希望以 C++ 的方式来编译程序时,可以这样做:
~~~
#ifndef __cplusplus
#error 当前程序必须以C++方式编译
#endif
~~~
- c语言
- 基础知识
- 变量和常量
- 宏定义和预处理
- 随机数
- register变量
- errno全局变量
- 静态变量
- 类型
- 数组
- 类型转换
- vs中c4996错误
- 数据类型和长度
- 二进制数,八进制数和十六进制数
- 位域
- typedef定义类型
- 函数和编译
- 函数调用惯例
- 函数进栈和出栈
- 函数
- 编译
- sizeof
- main函数接收参数
- 宏函数
- 目标文件和可执行文件有什么
- 强符号和弱符号
- 什么是链接
- 符号
- 强引用和弱引用
- 字符串处理函数
- sscanf
- 查找子字符串
- 字符串指针
- qt
- MFC
- 指针
- 简介
- 指针详解
- 案例
- 指针数组
- 偏移量
- 间接赋值
- 易错点
- 二级指针
- 结构体指针
- 字节对齐
- 函数指针
- 指针例子
- main接收用户输入
- 内存布局
- 内存分区
- 空间开辟和释放
- 堆空间操作字符串
- 内存处理函数
- 内存分页
- 内存模型
- 栈
- 栈溢出攻击
- 内存泄露
- 大小端存储法
- 寄存器
- 结构体
- 共用体
- 枚举
- 文件操作
- 文件到底是什么
- 文件打开和关闭
- 文件的顺序读写
- 文件的随机读写
- 文件复制
- FILE和缓冲区
- 文件大小
- 插入,删除,更改文件内容
- typeid
- 内部链接和外部链接
- 动态库
- 调试器
- 调试的概念
- vs调试
- 多文件编程
- extern关键字
- 头文件规范
- 标准库以及标准头文件
- 头文件只包含一次
- static
- 多线程
- 简介
- 创建线程threads.h
- 创建线程pthread
- gdb
- 简介
- mac使用gdb
- setjump和longjump
- 零拷贝
- gc
- 调试器原理
- c++
- c++简介
- c++对c的扩展
- ::作用域运算符
- 名字控制
- cpp对c的增强
- const
- 变量定义数组
- 尽量以const替换#define
- 引用
- 内联函数
- 函数默认参数
- 函数占位参数
- 函数重载
- extern "C"
- 类和对象
- 类封装
- 构造和析构
- 深浅拷贝
- explicit关键字
- 动态对象创建
- 静态成员
- 对象模型
- this
- 友元
- 单例
- 继承
- 多态
- 运算符重载
- 赋值重载
- 指针运算符(*,->)重载
- 前置和后置++
- 左移<<运算符重载
- 函数调用符重载
- 总结
- bool重载
- 模板
- 简介
- 普通函数和模板函数调用
- 模板的局限性
- 类模板
- 复数的模板类
- 类模板作为参数
- 类模板继承
- 类模板类内和类外实现
- 类模板和友元函数
- 类模板实现数组
- 类型转换
- 异常
- 异常基本语法
- 异常的接口声明
- 异常的栈解旋
- 异常的多态
- 标准异常库
- 自定义异常
- io
- 流的概念和类库结构
- 标准io流
- 标准输入流
- 标准输出流
- 文件读写
- STL
- 简介
- string容器
- vector容器
- deque容器
- stack容器
- queue容器
- list容器
- set/multiset容器
- map/multimap容器
- pair对组
- 深浅拷贝问题
- 使用时机
- 常用算法
- 函数对象
- 谓词
- 内建函数对象
- 函数对象适配器
- 空间适配器
- 常用遍历算法
- 查找算法
- 排序算法
- 拷贝和替换算法
- 算术生成算法
- 集合算法
- gcc
- GDB
- makefile
- visualstudio
- VisualAssistX
- 各种插件
- utf8编码
- 制作安装项目
- 编译模式
- 内存对齐
- 快捷键
- 自动补全
- 查看c++类内存布局
- FFmpeg
- ffmpeg架构
- 命令的基本格式
- 分解与复用
- 处理原始数据
- 录屏和音
- 滤镜
- 水印
- 音视频的拼接与裁剪
- 视频图片转换
- 直播
- ffplay
- 常见问题
- 多媒体文件处理
- ffmpeg代码结构
- 日志系统
- 处理流数据
- linux
- 系统调用
- 常用IO函数
- 文件操作函数
- 文件描述符复制
- 目录相关操作
- 时间相关函数
- 进程
- valgrind
- 进程通信
- 信号
- 信号产生函数
- 信号集
- 信号捕捉
- SIGCHLD信号
- 不可重入函数和可重入函数
- 进程组
- 会话
- 守护进程
- 线程
- 线程属性
- 互斥锁
- 读写锁
- 条件变量
- 信号量
- 网络
- 分层模型
- 协议格式
- TCP协议
- socket
- socket概念
- 网络字节序
- ip地址转换函数
- sockaddr数据结构
- 网络套接字函数
- socket模型创建流程图
- socket函数
- bind函数
- listen函数
- accept函数
- connect函数
- C/S模型-TCP
- 出错处理封装函数
- 多进程并发服务器
- 多线程并发服务器
- 多路I/O复用服务器
- select
- poll
- epoll
- epoll事件
- epoll例子
- epoll反应堆思想
- udp
- socket IPC(本地套接字domain)
- 其他常用函数
- libevent
- libevent简介