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

编译系统基础

工具链

  • 一个典型的工具链是:链接器-编译器(汇编器一般不接触)-预处理器-文本编辑器-构建系统

    • 调试工具及profile工具通常作为独立的部分出现.
  • 构建系统,或者称为工程管理器,对于开发意义重大,是工具链中重要的一环,其意义在于组织文件,管理源代码,明确目标输出,组织编译/链接的顺序和关系.

    • 常见的构建系统有,VS的nmake,QT的qmake,跨平台的CMAKE(使用makelist.txt),unix平台的make(使用makefile).其中CMAKE是目前事实上的标准构建系统,bazel也随着google项目的扩张越来越流行
    • VS和QT这样的IDE对自己的构建系统更加友好,往往可以按GUI的形式设置构建参数,CMAKE等则都需要开发者手动写构建文件.
  • 使用#ifndef 或者#pragma once可以有效的避免重复include,一个正常的头文件都应该包含include guard.

  • 一个典型的编译模型是: 每个源文件都是一个Module,编译完成后将输出对应的obj文件,链接器负责将这些obj合并成一个最终的obj.

    • 注意,各个Module的编译是彼此独立的,例如#ifndef就是各个Module单独判定的,它只能保证Module内只包含一份代码,不同的Module完全可以包含相同的定义.
  • 重定义是一个同时涉及编译和链接的问题,它相当复杂,在编译阶段的重定义会直接报错,在链接阶段,多个Module中的重复定义则可以按照一定的规则进行处理.

  • 在不够熟悉链接器的规则以前,为了避免#include引起的重定义,在头文件内的编程习惯应当如下:

    • 只进行函数声明
    • 只进行全局变量的extern声明
    • 只进行类声明
      • 成员函数可以进行inline形式的定义
    • 类模板的偏特化.(函数模板的特化一定是全特化,只应该定义在cpp文件中)
    • 模板的默认实现.
    • 各种被内联的函数(inline,constexpr等).
    • 枚举.
    • 常量相关的#defineconst定义可以出现在头文件中(const默认是文件私有的,所以不会引起重定义)
      • 在一些特殊的场景,可能必须要用extern const int a;这样的语法
  • extern "C"仅仅指明了链接规范,使用时一定要加{},避免一些隐晦的错误.

    • extern "C"{int a};是定义,而extern "C" int a;是声明,这一点区别很奇怪
    • extern "C"定义的函数还可能影响函数指针的类型,例如extern "C" void (*p1)();void (*p2)();,这两个指针可能指向的是两种不同的函数
      • std::functionlambda等C++专有特性所返回的指针一定是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"
  • 大部分编译器都是针对特定平台的,编译器会通过预定义宏给出很多平台信息.
    • 指示目标编译平台,是否支持多线程编译,自然字节长度,等等
    • 指示文件名,函数名等信息.

Read more

Coroutine

从一般概念上说, 协程是特殊的函数调用: 被调用的函数可以在可控的位置被中断,然后在下一次调用时,继续从上次中断的位置继续执行。 本文主要通过Python的协程来介绍协程, 这是我唯一熟悉的一种协程实现. Classic Coroutine 下面的python代码很好的说明了协程的核心功能 def co_routine(): recv0 = yield 996 # hangs here after first coro.send assert recv0 == "Second" yield 711 # hangs here after second coro.send return def main(): coro = co_routine() # Create a new coroutine object value = coro.send(None)

By Edimetia3D

GDB with Python

这篇文章的主要应用场景是调试Python的C/C++ Extension 1. 同时使用pdb / gdb 进行调试. 通俗点说, 既可以break在 .py 文件中,也可以break在 .cc 文件中 2. 在gdb中不但可以获得常规的调试信息, 还可以获得python VM 的调试信息, 例如获得python的调用栈, 访问Python局部变量等. 这将会在调试exception时(如Segmentfalut)非常有用, 这种场景下, 定位 Python VM 正运行到哪一行代码往往可以提供一些直观的重要信息. 第一步: 编译源码以获得一些辅助数据. 我们并不真的需要使用从源码编译的Python, 但是一些调试相关的辅助文件需要从源码中获得, 包括 python-gdb.py及debug symbol等. 在 https://www.python.org/ftp/python/ 或 https://github.com/python/cpython

By Edimetia3D

Bazel Notes

这是一篇2019年左右的记录, 内容可能过时, 也不太全面 杂谈 Bazel是Google为Monorepo服务而开发的构建工具. 首先是巨大,当问题的规模变大,事情总是会变得更复杂. 而Google面对的"巨大Monorepo",应该是世间罕有的. 然后是Monorepo,这极大的影响了代码的组织风格.例如,你要写一个操作系统内核ProjectOS,还要写一个游戏ProjectGame.在传统的开发习惯中,这两个项目会组织到两个不同的Repo里,PorjectOS和ProjectGame之间无法直接相互引用,例如,你在ProjectOS里写了一个高级的数据结构,想要在Game里也使用,要么直接复制粘贴,要么是创建一个新的CommonRepo,把可公用的代码都放在Common里,然后两个项目各自引入Common作为依赖. 使用MonoRepo则不存在这个问题,Game可以直接依赖OS内的组件,按照Bazel的语法描述,就是在Game中可以直接使用@ProjectOS//path/to/package:AdvancedStruct.当然,你仍然可以选择重构一

By Edimetia3D

Unix related things

这是一篇2017年左右的记录, 仅用作分享 杂 * 在shell内能干的事,我们都可以比较简单地通过系统调用实现. * `称为反引号,^称为脱字符,常用来表示CTRL * windows的系统调用是不开放的,windows下只能直接使用windows.h里的windows API. * /dev目录下的设备是供用于程序直接使用的,主要由block,char,pipe,socket类型 * 并不是所有设备都能映射为这种形式 * /sys/device/目录称为sysfs,他下面存放了所有设备的信息.(不能直接从/dev获得任何设备信息) * udevadm info --query=all --name="/dev/sda1"可以用于查询/dev下某个设备对应的sysfs路径 权限系统 * 权限系统由两部分组成 * 文件属性:用于标注文件owner,所属组,以及权限的设定(默认只有owner和root可以修改权限设置) *

By Edimetia3D