[Cpp基础] [01] Cpp编译系统基础

Last Updated on 2020年4月14日

编译系统基础

工具链

  • 一个典型的工具链是:链接器-编译器(汇编器一般不接触)-预处理器-文本编辑器-构建系统
    • 调试工具及profile工具通常作为独立的部分出现.
  • 构建系统,或者称为工程管理器,对于开发意义重大,是工具链中重要的一环,其意义在于组织文件,管理源代码,明确目标输出,组织编译/链接的顺序和关系.
    • 常见的构建系统有,VS的nmake,QT的qmake,跨平台的CMAKE(使用makelist.txt),unix平台的make(使用makefile)
    • VS和QT这样的IDE,由于需要进行一些额外的处理,还自定了更高级的工程文件格式.vxproj.pro
  • 头文件应当尽可能的避免重复#include,一方面重复include往往会引起一些编译错误,另一方面,即便不出错,也可能引起不必要的代码膨胀,使用#ifndef guard 或者#pragma once都可以避免这样的行为.
  • 重定义是一个非常复杂的问题,C++的单一定义规则ODR其实要比我们想象的灵活一些,正是ODR使我们可以在头文件中定义enum,类,struct,以及模板定义.
  • 正因为ODR比较复杂,因此,为了避免#include引起的重定义,应当记住,在头文件内的编程习惯如下:
    • 只进行函数声明
    • 只进行全局变量的extern声明
    • 只进行类声明
      • 成员函数可以进行inline形式的定义
    • 通过ODR规则来进行区别的内容定义在头文件内
      • Template定义.
      • 各种被内联的函数(inline,constexpr等).
      • 枚举.
    • 常量相关的#defineconst定义可以出现在头文件中(const默认是文件私有的,所以不会引起重定义)
      • 在一些特殊的场景,可能必须要用extern const int a;这样的语法

注意,编译过程中,每个文件xx.cpp彼此独立的判定#ifndef.就是说#ifndef只能防止链式的重复包含,而不能避免重定义.

  • extern “C”仅仅指明了链接规范,使用时一定要加{},避免一些隐晦的错误.
    • extern "C"{int a};是定义,而extern "C" int a;是声明,这一点区别很奇怪
    • extern "C"定义的函数还可能影响函数指针的类型,例如extern "C" void (*p1)();void (*p2)();,这两个指针指向的是两种不同的函数(C函数和C++函数可能有不同的ABI,所以被视作两种函数,有的编译器可以自动处理这个问题).
    • std::functionlambda所返回的指针一定是C++型链接的,不可能是extern "C"风格的指针.

使用动态库和静态库

  • 从初学者的角度看: 在大部分的使用场景中,对开发者而言,静态库和动态库是没有区别的.
    • 静态库和动态库涉及的内容过多,不适合在这里展开.
文件 功能
.h 包含库的接口声明
lib_xx.a/.lib 静态库,包含了编译后的目标代码(.obj).
.dll + .lib Windows的动态库体系,.lib称为导入库,用于提供动态库的符号信息仅用于链接阶段..dll则仅用于运行阶段.如果没有导入库,也可以手动载入dll,手动链接和linux下用法类似.
.so/.dylib Unix的动态库,编译&运行使用同一个文件.

预处理器

预处理器是一个独立于编译器的程序,它是一个纯文本的处理器,负责在编译前对所有的待编译文件做文本批处理.源代码中可以使用来指导预处理器进行操作.

  • 最常用的#include "xx.h"宏,指示预处理器查找”xx.h”文件,并将该文件的内容插入到当前位置
  • #define宏可以用于定义宏函数和常量
    • 宏函数实质就是将”实参文本”替换到后面的定义语句中.
    • 宏函数不支持重载,不支持递归.
    • ##可以将实参文本拼接,例如#define FUN(x,y) x##y展开后就是xy
    • #可以为实参文本加””,例如#define CODE_RAW(str) #str展开后就是"str"
  • 大部分编译器都是针对特定平台的,编译器会通过预定义宏给出很多平台信息.
    • 指示目标编译平台,是否支持多线程编译,自然字节长度,等等
    • 指示文件名,函数名等信息.