编译器

把高级语言编译成可执行语言工具,分为前端后后端,前端值得是高级语言的解析,后端是指翻译解析之后的结果为机器语言

  • 多文件 ** 连接 多文件编译可以有两种方式,一是直接编译为一个可以执行文件,二是按模块或者按文件编译为库,然后连接到执行文件

    • 连接方式有两种, 一是静态连接,把所有的库文件打包到最后的生成文件中,优点是不需要额外的依赖外部环境,独立性强,缺点是文件体积大 二是动态链接,为了解决静态链接的缺点,执行文件在执行到库相关的代码的时候才加载库,有一点需要注意的是,程序运行的时候,在使用到动态库的时候才映射动态库到内存空间中。原理是编译待援在编译的时候,会更具声明生成函数的调用逻辑,但是只是一个地址跳转语句,所以,只要不调用,就不会有问题,当调用到了。才会加载库然后映射库的地址,这个完整的过程称为重定向。 动态连接 C语言编程透视
  • 声明 声明是为了在编译的时候编译器能进行完整的上下文编译。他需要更具声明来确定编译信息,否则编译器无法确定编译中的语句信息,声明可以辅助完成这个情况, 所以理论上编译的时候是可以不需要实现的,可以在其他编译单元中实现声明的函数,其声明的文件可以不引用头文件,即两个编译单元完全可以无任何联系,除了声明之外,在连接的时候,连接器会根据编译出来的信息去确定函数调用情况,这里有一个问题,按上述的描述,是一个声明对应一个实现,如果有一个声明对应多个实现呢 == : 会有覆盖问题,如果多个动态链接库都有同一个声明的实现,则连接的时候连接第一个,后面的则忽略,这也提醒我们,在大型项目中,避免同名全局函数或者变量,使用namespace或者static限制作用域,

g++ -o tt ../main.cpp -ldl ./libhellolib.so ./libhellolib1.so 
LD_LIBRARY_PATH=$PWD ./tt
  • 头文件,避免公用代码的重复,预处理时展开头文件,需要使用#pragma once避免重复引用,头文件只是简单的文件替换,理论上的可以替换任何文本。

cmake * 子模块,使用add_subdirectory引入 * 第三方库 * 只是头文件,直接指定头文件目录编译即可 * 使用子模块 * 使用为连接库 * 使用git模块

  • STL

    • 重点为容器和算法
  • lambda表达式,实质上是仿函数,是一个结构体实现()运算的重载,捕获的时候按照声明的新式捕获参数,建议使用的时候明确使用的参数,使用哪个就捕获哪一个,否则他实际上会占据一定的大小的,配合std::function使用

  • CTAD — complie-time argument deduction,编译器参数推断,C++17引入的,可以在编译器按照上下文推断类型,具体表现在lambda参数可以使用auto,容器可以不适用<>

  • ranges https://zhuanlan.zhihu.com/p/350068132

  • module

  • raii 获取资源即初始化,释放资源即销毁,具体的实现是使用构造函数和析构函数,当前的实现为智能指针,其他用户自己管理的资源最好也使用raii,遮这样在函数有多个出口的时候,就不会有意外的情况,本质上还是自己管理资源,不想其他的语言有GC

    • 异常安全,C++中异常机制在回溯栈的时候会析构对象,所以如果没有实现RAII,则自己管理的内存则无法释放,C++的异常可以发生在任何地方,如果发生在析构函数中,则需要自己处理,不要在析构函数中抛出异常的,在构造函数中的时候,也需要捕获异常然后释放已经申请的资源,构造函数异常的时候,是不会调用析构函数的,
  • 构造函数

    • 构造函数有时候会隐士的生成对象,即使没有显示声明,使用explicit避免这种情况,单参数的时候会,多参数使用{},调用的时候也会
      • 直接使用多参数的时候,()和{}是有区别的,()除了正常的使用外,其他情况不具备特殊含义
          int a = (10, 11);
          int a = {10, 11};
              tt t(1, 2);
      

    tt q{1, 3}; tt w = {1, 4}; tests({1,5}); ``` 上面的语句1正确,a的值为11,2错误,{}这种用法的意义是参数列表,是会构造对象的。ps,调用构造函数的时候,有具体的对象的时候两种括号无差别,但是无对象的时候有区别,如上,细品

    • 默认构造函数
    • 拷贝构造( A(A const & a) ) A a = aa;
    • 移动构造( A(A && a) )
    • 赋值构造( A&operator=(A const& a) ) A a; a = aa;
    • 移动赋值( A&operator=(A && a) )
    • =delte和=default
    • 类内部变量可以赋初值
    • 三五法则
      • 拷贝构造或者赋值构造需要区分深拷贝和浅拷贝,这也是构造函数肯可能引入的问题,例如浅拷贝导致内存的重复释放,
      • 各种构造函数更多的是需要考虑当前对象的来源,如果是直接从零开始的,则是普通的构造函数,如果是从别的对象来的,则需要考虑深浅拷贝的问题,以及构造之后别的对象是否还需要的问题,简而言之,就是资源细节上的考虑,只要内涉及到资源的操作,则需要多话费一些心思区考虑,
  • 函数返回多值

    • 使用结构体打包,结构体可以使用{}任意构造,且好处是可以获取变量的名字
    • tuple
    • pair
    • 各种结构方式
    • 结构化绑定,类似rust
  • option 成功则优质返回,否则返回nullopt,搭配has_value等使用,类似rust

  • variant

  • 智能指针

    • RAII的具体体现
    • unique_ptr 禁止拷贝,只允许移动,避免产生多个对象
    • shared_ptr 使用计数器记录对象,允许存在多个对象,相互引用问题,

限于篇幅,此处放出一些扩展知识供学有余力的同学研究: P-IMPL 模式 虚函数与纯虚函数 拷贝如何作为虚函数 std::unique_ptr::release() std::enable_shared_from_this dynamic_cast std::dynamic_pointer_cast 运算符重载 右值引用 && std::shared_ptr和 std::any 只提供了关键字,详细信息请善用搜索引擎:bing.com。(不要用 baidu.com,那个是搜广告用的) 如果感兴趣,我可以增添一节专门讲动态多态。

  • 模板

    • 更加广义上的重载机制,模板会在编译的时候,在依据上下文信息编译代码,调用的时候可以使用<>声明模板参数,在17之后有CTAD,可以省略这一步,直接和调用普通函数一致,但是需要明确的是他是需要从上下文进行类型的推到,如果编译时上下文无法推到类型信息,则还是需要使用尖括号,
    • 模板可以是CLASS或者typename,也可以是整形,例如tuple的get函数使用整形模板参数指定参数index,但是参数只能是整形,不知道是从哪里开的口子,
      • 此时模板参数必须是编译期间可以确定的常量,如果是表达式,可以使用constexpri修饰,
      • 使用整形的一个特殊原因是由于模板参数是编译器编译期间可以确定的,则此时可以根据模板参数进行代码优化,例如函数的参数列表使用bool确定函数是否是debug模式,此时由于参数是变量,所以无法优化,必须有一个if跳转逻辑。但是假如是模板参数,则会在编译的时候确定生成的代码是什么,可以直接在代码生成的时候优化掉
      • 编译期常量,是编译器在编译期间的可以通过直接运算或者其他方式运算得到一个确切的值,需要使用constexpr修饰,否则即使是一个值,他也不能用在有要求的地方,例如模板参数
      • 模板的惰性编译,
        • 模板会工具编译时候遇到的模板参数生成不同的函数,函数名字的参数类型不一样,具体可以使用objdump查看生成的函数名,所以一个模板可能有多份代码的生成,需要考虑实际的使用情况,如果一个模板没有使用到,此时他的模板参数按什么生成,所以模板是在遇到调用的时候才生成的,是惰性的,此过程隐含两个信息,一是编译的时候需要调用才生成代码,二是编译的时候能找到模板的说明代码,因为编译是分为不同的编译单元的,如果在编译模板的编译单元的时候没有发现模板调用,导致没有生成代码,其他的编译单元发现了调用,但是模板的编译单元已编译完成,所以模板需要注意的是使用的时候最好确保所有的使用的代码都能看见模板实现,简单的就是模板最好放在头文件实现,不要分开。否则需要在模板的编译单元里面添加模板参数的声明。
  • auto只是推到类型,不包含引用信息和const,可以手动指定

  • if constexpr

  • int& int&& const int &

    • 常饮用范围最小,其他两种引用都可以转换为长应用

9 内存模型和名称空间

  • 编译的基本单位是翻译单元,简单的理解就是单个的C++源文件,不同的翻译单元最终使用连接器链接

存储持续性,作用域,链接性

变量存储的生命周期为

  • 自动存储持续性
    局部变量的生命周期
  • 静态存储持续性
    静态变量的生命周期
  • 线程存储持续性
    生命周期存在在线程内,使用thread_local声明
  • 动态存储 使用new动态分配

限定变量的作用域,全局变量项目全局可见,有同名变量的时候会报错,此时是外部连接性,使用static限定,此时是内部链接性,只在一个翻译单元中可见。此外,还存在作用域覆盖的现象,作用域最小的级别最高, 在其他文件中想使用全局变量,需要使用extern声明,函数表现也是一样的。 具有全局作用域的变量在启动的时候初始化,在main之前运行 const会导致变量的连接性变为内部连接性,只能在一个翻译单元可见,如果外部想使用,则需要使用extern,extern和const不冲突,但是和static冲突,因为他们本意是相反的,

C和C++对函数的编译名称处理不同,所以在链接的需要定义协议让连接器可以正确的查找函数名。extern可以指定语言协议,在声明的时候,使用extern "C" int aa()可以让编译器按照c的命名规则去链接函数aa

符号表

.dynsym 是动态链接库中对外可见的符号 .symtab 是当前文件中的符号,shared object文件中包含了.dynsym.symtab,strip 可以去掉 .symtab 节,只保留.dynsym 节,节省空间。

使用 visibility 属性控制符号的可见性,一般不对外的符号建议隐藏,

-fvisibility=hidden -fvisibility-inlines-hidden 连接的时候加上这两个参数,可以隐藏符号