做学生时, 写个”Hello World”, 若是Linux, 用个g++ main.cpp就可以了. 但对多些的源码工程, 一行命令就难以搞定了. 这个时候, 大家熟悉的make命令就派上用场. 这个书上很少教说, 我在网上边搜边学后折腾了个简单的makefile, 应付简单的工程…
1 | ifeq ($(dd),debug) export dd := debug BIN_DIR=./debug CPPLAGS=-g -pg -fPIC -Wall -D_FILE_OFFSET_BITS=64 BUILDS=$(DIRS) lib test else #release export dd := release BIN_DIR=./release CPPLAGS=-O3 -fPIC -D_FILE_OFFSET_BITS=64 BUILDS=$(DIRS) lib endif TEST= DIRS=$(BIN_DIR) BASE_DIR=. LIB_PATH=$(BASE_DIR)/lib SHARE_PATH=$(LIB_PATH) STATIC_PATH=$(LIB_PATH) SHARE_LIBS=boost_thread STATIC_LIBS=libboost_system.a libevent.a INC_PATH=./ SRC_DIRS=./ ./bits ./algo ./ext SRCS=$(foreach SRC_DIRS,$(SRC_DIRS),$(wildcard $(SRC_DIRS)/*.cxx)) OBJS=$(patsubst %.cxx,%.o,$(SRCS)) INC_SHARE=$(addprefix -L, SHARE_PATH/) $(addprefix -l, $(SHARE_LIBS)) INC_STATIC=$(addprefix $(STATIC_PATH)/, $(STATIC_LIBS)) .SECONDARY :%.o %(OBJS) %.o:%.cxx @echo "Compile $@ ($<)." @g++ $(CPPLAGS) -I $(INC_PATH) -c $< -o $@ all: $(BUILDS) $(DIRS): mkdir -p $@ lib: $(OBJS) @echo 'build static lib' @ar -rc libbang.a $(OBJS) mv $(OBJS) libbang.a $(BIN_DIR) test: @echo 'build unit test' g++ -DUNIT_TEST -DTEST_$(TEST) $(CPPLAGS) -o $(TEST).exe $(SRCS) -I $(INC_PATH) $(INC_SHARE) $(INC_STATIC) -lcurl install: @echo 'install lib' cp $(BIN_DIR)/libbang.a $(LIB_PATH) clean: @echo 'clean up the generated files' rm ./debug/ -rf; rm ./release/ -rf; .PHONY: all lib test install clean $(DIRS) |
用这个Makefile文件, 即可编译链接了. make dd=debug进行debug方式的编译. 而make dd=release则进行release编译, 以适合生产环境的需要和性能优化. 那这种区别编译是怎么做到的呢? 我们如何在实际使用时干预已经写好的代码行为?答案就是本篇的主题, 宏.
如上的Makefile文件中, 有定义CPPLAGS, 这个就是传输给编译器的宏, 在编译的预处理期可以宏展开, 以替换源码中间相关的宏定义. 像这里就定义了FILE_OFFSET_BITS(在Makefile里会用-Dxxx来定义xxx宏). 这个宏直接作用到了系统的features.h文件, 进而作用到很多头文件, 以满足我们使用64位的大文件的需要. 以stdio.h为例:
1 | // features.h的相关定义 |
我们在CPPFLAGS里定义了FILE_OFFSET_BITS, 那么编译时会展开typedef __offset_t off_t, 这样的话就能使用大文件, 不会被著名的4G文件困扰.
当初的百度云盘的文件仅能支持4G以内的文件, 和windows32位系统类似, 我的第一反应是不是没定义这个宏啊:)–玩笑了
其实不仅仅是这一点, 我们只要随便翻看开源的C代码项目, 都能看到其Makefile里有各种各样的宏定义以方便条件编译, 跨平台, 定制所使用的库, 控制软件版本, 遵守哪种标准/协议等等.
不仅仅是Linux, 在windows下也是如此. 虽然VS对编译项做了层层包裹, 形成令人眼花缭乱的编译控制面板, 但本质上也是一样的, 也是通过宏来控制程序特性.如下图中的cocos2d的一处windows工程配置, 通过定义一些系统的宏(WIN32, _DEBUG, _WINDOWS, …) 来规制程序的基本形态, 又同时可定义一些工程用的宏来定制程序特性,如COCOS2D_DEBUG=1, 来作为DEBUG开关, 控制一些DEBUG模式下的输出, COCOS2DXWIN32_EXPORTS来进行动态库的导出编制.
总的来说, 宏在编译时起着开关的作用, 串联系统需要和程序特性, 让开发的代码尽可能地复用, 而又不至于在编译使用时过于繁杂. 一般说来, 宏开关主要控制以下几个方面:
- 跨平台兼容. 很多程序/库需要跨平台, 用诸如_WIN32, _GNUC等来控制某些代码的条件编译, 以适配不同的系统API
- 依赖库的定制, 像有些库对STL是不大感冒的, 但出于礼貌会考虑兼容一下, 这个时候就给出一个宏, 让你自己来选择是否用STL的一些容器, 不选就默认是自己的容器了.
- 基本单位/变量的控制, 上述的64位文件即为如此. 为了兼容多种格式的文件, 宏只得出马. 同样的还有时间time_t等
- 函数行为控制, 著名的cplusplus来兼容C函数和C++函数的编译(这两种的编译后的函数是不一样), 还有stdcalll, cdecl, pascal等
- 工程定制. 有些项目可能会基于同一套代码编译出库,可执行程序,测试用例程序等三种形式出来. 曾经见过一个项目, 用来同一套代码, 用宏和选择源码编译的方式, 编译出10几种程序来, 个个不同…
不仅仅在条件编译时, 宏可以很好地作为开关来使用. 在代码实现时, 宏的身影也无处不在. 我们可以用宏做一些方便的事儿,而并不会带来太大的麻烦.
- 如下的一个示例程序, 分别用宏和静态变量定义了一组变量, 两者在代码量上是相当的, 逻辑也足够清晰.
1 | #include <stdio.h> |
- 以上小程序的执行结果:
1 | xxx:~$ g++ -Wall t.cpp t.cpp: 在函数‘int main()’中: t.cpp:27:14: 警告: 在有符号和无符号整数表达式间比较 [-Wsign-compare] t.cpp: 在全局域: t.cpp:14:20: 警告: ‘PRODUCT’定义后未使用 [-Wunused-variable] t.cpp:15:20: 警告: ‘VERSION’定义后未使用 [-Wunused-variable] xxx:~$ ./a.out PI 3.141593 3.141593 3 badibu.com-1.0.2 xxx:~$ g++ -DUSE_MACRO t.cpp xxx:~$ ./a.out PI 3.141593 3.141593 3 badibu.com-1.0.2 |
可以看到用宏的话, 有一些小小的优点. 一则避免了不必要的警告, 而同样的效果在不用宏时, 得选择手动地审阅并强制类型转换. 二则对于此例中的PRODUCT_INFO, 用宏的话可以很讨巧地用宏拼接即可, 而不用宏的话, 则得实现个拼接函数才能达到相似效果.
当使用宏定义时, 若并不增加歧义性, 而简洁了代码, 那懒惰的我建议你, 不妨用用.
而使用宏函数或者宏片段时, 有时候更是能做到事半功倍. 前段时间在一个项目中用到了curl库, 这个库对函数进行了很有意思的封装, 有一整套的curl_easy_setopt(xxx)函数, 且其函数系有统一的错误处理机制, 会返回CURLCODE值, 并用curl_easy_strerror获取错误信息. 针对这一特点, 我定制了以下的宏片段, 以节省代码编写, 也显得语义统一.
1 | #define CALL(s, x) do { \ |
从该片段可以看到, 这里的宏片段很难用普通函数替代. 他的逻辑和调用处的函数逻辑是一体的, 只是因为某些原因重复或者相似代码太多, 故而使用宏片段来替换. 其实这种取巧在很多项目里都会有, 包括咱们的C++标准库:
1 | /// c++/4.6.3/valarray |
该部分宏在template类valarray中实现, 以简化相似函数的实现. 可以看到, 这部分宏函数是template的模板实现, 只是其模板对象是函数名和类型名, 而这个若换成template自身去实现,估计模板的作者都得头大一回. 而且其一旦出错, 其调试信息基本上也是不可调试的…
除了这些取巧的地方, 其实宏函数基本上是工程项目的常客. 尤其是在日志函数/日志类的实现部分. C/C++语言本身并无代码文件的基本信息原语(function, fileline, filename), 而这些信息是调试的必备, 有了出错处的函数名,对应代码行, 程序员基本上就能锁定第一现场. 但因为没有原语, 这里只能请出宏来帮忙.
1 | /// leveldb/db/c_test.c #define CheckNoError(err) \ if ((err) != NULL) { \ fprintf(stderr, "%s:%d: %s: %s\n", __FILE__, __LINE__, phase, (err)); \ abort(); \ } #define CheckCondition(cond) \ if (!(cond)) { \ fprintf(stderr, "%s:%d: %s: %s\n", __FILE__, __LINE__, phase, #cond); \ abort(); \ } int main(int argc, char** argv) { // ... some code StartPhase("leveldb_free"); db = leveldb_open(options, dbname, &err); CheckCondition(err != NULL); leveldb_free(err); err = NULL; StartPhase("open"); leveldb_options_set_create_if_missing(options, 1); db = leveldb_open(options, dbname, &err); CheckNoError(err); CheckGet(db, roptions, "foo", NULL); // ... some code } |
以上片段是leveldb里的一个测试代码片段, 其中用到宏函数来进行assert判定, 这里也是没办法用普通函数替换的, 原因是其中的FILE, LINE正是我们需要得到的输出信息, 而若用普通函数的话, 这里的FILE, LINE就不能显示正确了.
其实还有很多地方, 宏函数都可以给我们露一手, 演出惊艳一幕. 在有大量相似代码时, 不妨考虑使用宏函数, 清爽一下代码. 在有普通函数搞不定或者太繁琐时, 也不妨考虑下宏函数, 说不定就能手到擒来.
宏, C/C++基本语法和template构成C/C++语系的三大基石. 我们阅读各种经典项目时都能发现宏的身影. 可惜的是, 自教学之始, 到各种大佬大师的口诛笔伐, 宏在C++ coder的世界里始终是三等公民, 甚至被无条件排斥. 不可否认, 宏确实是劣质代码的罪首之一, 宏定义重复, 宏覆盖, 宏展开异常等等带来各种难以调试的bug. 但若我们能正确使用, 宏不乏其可爱之处, 并不是那么坏的存在.