[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簡介