• 1_1cpp_used


    init

    background

    C++这门语言是一个追求底层的语言, 老实说我为什么选择C++就是因为它够底层, 让我能知道底层大致在干什么。 但是在学习的过程很明显存在不具体的问题, 而且C++语言的语法非常多,
    是要做减法的。 基于这个背景, 我积累了一下以自己为中心的C++最佳实践和理解。

    Content

    认知

    适应领域

    关注于高性能或者底层领域, 而不是业务领域, 业务领域有点像用汽车穿过小巷, 浪费时间还效率低。

    • 因为C++在运行的时候是直接编译成二进制机器代码的, 虽然有些东西是要运行中决定, 例如函数的创建, 但是本身运行速度就很快, 不像java和python等语言需要额外一些工具帮你翻译成机器代码。 虽然python那些不用管理内存的语言开发速度很快, 但是内存垃圾回收容易导致突然变慢, 这在游戏等高性能领域是不被允许的, 你卡了别人不卡直接把你杀了, 电商搜索业务员你卡了或者慢了别人就换app搜了, 而下单业务等可以慢一点,因为已经决定好买了, 愿意花这点时间。 如果不那么在乎性能开销,那么 C++ 并不是最好的语言选择(Java、Go、Python 等正是填补了这些领域);或者软件规模不大、无需很多抽象手段来管理软件的复杂度,那么 C 语言就足够。但如果性能是软件的关键指标,同时又有越来越大的复杂度,那么 C++ 几乎是独一无二的选择。我们看到 C++ 这些年来的发展,都是紧扣 40 年前 Bjarne 为 C++ 设定的“生态位”与“核心精神”而开展的。
    优劣点

    优点与通用行业领域知识绑定多,没35岁危机, 入门难, 壁垒高。 缺点就是生态太少了, 如果做业务效率太低, 各个C++领域生殖隔离。

    • 行业壁垒 : C++很难, 容易形成技术壁垒。 因为没有方便的库, 新手和很多浮躁的人不会进来, 而现在年轻人进来少, 因此值得学习。 C++ 语言本身一直在发展, 而且他有右值引用等等极其优化性能的设计, 不像python java等只有官方规定的规则, 不能极致优化。 C++的性能是go的5倍, 在并发、网络、模板等方面具有先天优势。 但是如果是界面开发或者客户端等不是高性能的地方, C++并不占优势。学到后期要去改源码, 裁剪源码, 形成自己的知识体系。 而且不同领域的C++开发认识的C++完全不一样, 写嵌入式的C++程序员对C++理解难以提升。 写视觉和音视频的话很多也都是c with class。 很多时候你会发现没有统一的库, 反而是好事, 将开发能力给了开发人员。 越是底层人员有占比能力的领域, 越不容易被资本收割, 例如嵌入式和互联网。
    • 语言本身第三方库比较乱,感觉啥都做不了。 *C/C++ 整套的语法不具备“功能完备性”,单纯地使用这门语言本身提供的功能无法创建任何有意义的程序,必须借助操作系统的 API 接口函数来达到相应的功能。 **当然,随着 C++ 语言标准和版本的不断更新升级,这种现状正在改变;而像 Java、Python 这类语言,其自带的 SDK 提供了各种操作系统的功能。举个例子,C/C++ 语言本身不具备网络通信功能,必须使用操作系统提供的网络通信函数(#include如 Socket 系列函数);而对于 Java 来说,其 JDK 自带的 java.net 和 java.io 等包则提供了完整的网络通信功能。我在读书的时候常常听人说,QQ、360 安全卫士这类软件是用 C/C++ 开发的,但是当我学完整本 C/C++ 教材以后,仍然写不出来一个像样的窗口程序。许多过来人应该都有类似的困惑吧?其原因是一般 C/C++ 的教材不会教你如何使用操作系统 API 函数的内容。**C/C++ 语言需要直接使用操作系统的接口功能,这就造成了 C/C++ 语言繁、难的地方。**如操作内存不当容易引起程序宕机,不同操作系统的 API 接口使用习惯和风格也不一样。接口函数种类繁多,开发者如果想开发跨平台的程序,必须要学习多个平台的接口函数和对应的系统原理。Java 这类语言,很多功能即使操作系统提供了,如果 Java 虚拟机不提供,开发人员也无法使用。
    • C++本身因为历史包袱的原因, 在新的版本中一直在优化之前的东西。
    • 之前看了一个视频, 讲了很多C语言底层的东西, 从这里可以看出来,这种语言根本不适合上层开发,要程序员考虑的东西太多了,一点也不友好, 用这个语言做业务开发太慢了。精力全花在轮子上, 业务一点也没做。
    • java关注易用性, C++关注性能, 易用性和性能难以同时追求最优。
    C++ 语言认知

    C++的特点

    • 控制硬件, 控制生命周期, zero抽象。 支持三种编程方式, 并且一直会改进。 不用自己额外开线程做垃圾回收, 大部分简单变量可以搞成栈上的数据,相比较堆数据汇编语句更少, 可以在编译期花时间进行优化提升运行效率。

    C++重要的是不要迷失方向

    • C++本身支持面向对象、 面向过程, 函数编程, 模板编程, 模板元编程, 因此包含的太多了, 需要我们自己把我好度, 只用自己应该用的, 不要炫技。

    内功

    程序运行原理, 从编写到运行的过程
    1. 编译 :代码写完之后, 我们开始进行程序的编译;(感悟, 所有语言转换成可执行文件之后, 都是指令和数据了, 所以可以跨语言调用。)
      预处理(预处理如 #include、#define 等预编译指令的展开与解析, 生成 .i 或 .ii 文件)编译(编译器进行词法分析、语法分析、语义分析、中间代码生成、目标代码生成、优化,生成 .s 文件)

    2. 汇编(汇编器把汇编码翻译成机器码,生成 .o 文件)。

    3. 链接( 这个过程第一阶段符号表解析 是把各个.o 文件的elf中同样段进行合并, 所有的符号表中und的变量都要在这过程中查找在哪, 解析成功之后给所有的符号分配虚拟地址。 , 最后生成 .out 文件 (注意这些过程根据objdump 查看elf文件) elf文件会包括了所有的信息。 可执行文件.out和.o大部分都相同, 但是可执行文件还有一个program 告诉系统哪些内容加载到内存中。 一般只加载代码段和数据段。

    4. 加载: 操作系统在装载应用程序时,主要分为三步:
      4.1. 为进程分配虚拟内存(逻辑内存),一个进程是4G, 分为txt , data ,bss(存储为0或者未初始化的全局变量) , heap, shared stack(从上往下增加的, 要删除一个必须把下面的都删除了, 因此没有内存碎片) kernel 。 动态库和mmap会加载到headp到stack之间。

    5. 2 建立虚拟内存与可执行文件的映射(在内核态的页表, 以4K大小为单位,将虚拟内存地址与可执行文件的偏移建立映射关系。形成一个虚拟地址到物理地址的页表,注意这里不是寻址的页表)

    4.3. 将CPU的指令寄存器设置为应用程序的入口地址(入口地址在elf文件中),启动运行。

    1. 开始运行 :在完成3步后,程序开始执行。CPU在脉冲的操作下, 将程序指令地址读到PC指令寄存器,然后如果需要数据, cpu把数据通过地址总线读到存储寄存器中, 然后运算单元对数据进行处理, 处理完成继续程序指令寄存器。

    6: 运行过程中堆栈调用过程:
    栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的。堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表)。栈里面不仅有压栈出栈的函数调用, 还能分配局部变量。 这里面能讲解很深的~, 一定要注意。堆栈调用非常关键,深刻理解很有用。 这里面我简单说一下石磊老师课程里面的代码, 局部变量不产生地址, 直接根据栈的偏移量计算的。 直接是一个move指令。 调用函数中$ { $ 会存储之前栈的地址,开辟sum函数的栈空间, 保存之前栈的下一步汇编地址。 然后 执行函数的指令, 最后将存储return结果的形参变量内容交给一个寄存器。 右括号负责将这个调用栈回退(pop 得到之前的栈底的地址, 赋值给当前的栈底esp, 并将call function 的下一行指令直接给pc寄存器执行,(一般在这就回退到真正的main函数栈顶), 然后将存返回值的寄存器内容给main栈中的ret , 并开始从main栈顶继续执行。 在这过程中可能会反复发生调度算法切换,但是没事,不影响正常的程序调用。

    7 : 运行过程中虚拟内存如何工作
    当我们代码中访问到具体的数据时候,而不是只是申请虚拟内存的时候, 就会发生页表的实际分配。 接下来就让我们看看这里是怎么切换的。

    内存管理单元(MMU)管理着虚拟地址空间和物理内存的转换,操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,存储着程序地址空间到物理内存空间的映射表。页表寻址中可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存时(缓存不命中),会产生一次缺页异常加中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。如果执行出错那么就直接退出了。在mmu上查找虚拟地址的物理地址时候,如果内存满了等会做缺页置换算法。例如LRU, 总之可以不用做实际的转换, 直接将物理地址返回给cpu。

    如果需要实际访问的话第一次没有缓存而且标志位为空的话会产生缺页异常,如果映射整体是三级表 , 就是经历(1)逻辑地址转线性地址, 这样我们拿到了32位的地址, 然后拆分10 ,10 ,12 大小分别找页表,页, 物理地址。还要注意缺页异常是前提, 随后触发中断, 去真正的将外存放到内存中,说明已经做了一次实际映射,这种就不是malloc , 而是实际的访问了。

    8 : cpu的调度算法
    每个进程的PCB都是存在所有进程共享的内核空间中,操作系统管理进程,也就是在内核空间中管理的,在内核空间中通过链表管理所有进程的PCB,如果有一个进程要被创建,实际上多分配了这么一个4G的虚拟内存,并在共享的内核空间中的双向链表中加入了自己的PCB。PCB(Process Control Block)进程控制块,描述进程的基本信息和运行状态。
    还要注意, 在多道程序而且多用户的情况下,组织多个作业或任务时,就要解决处理器的进程调度。 如果CPU调度算法生效了, 需要进行程序寄存器上下文的保存, 之后再去调度其他的进程。常见的方法有先来先服务法等。

    9: 优化性能

    最后在整个阶段我们需要考虑几个优化方面的问题 :(1)内存碎片的避免, (2)如何进行优化核态和用户态, 库函数在这个过程中做了什么?

    关于内存碎片的避免就是采用每天定时启动, 此外还有malloc底层使用了内存池。采用不同的链表绑定不同需求, 如果请求的大小在链表中满足直接返回, 不满足再去重新开辟。

    第二个内核态是发生在一些文件读写或者事件响应中的, 内核态拥有最高权限,可以访问所有系统指令;用户态则只能访问一部分指令。一些对硬件操作或者重要的指令只有内核态才能访问到。例如:当读取文件等(read,write),需要进入内核态。 进入的方式就是软中断和硬件中断。 中断是当前程序需要暂停当前的任务去做的事情, 为了区分不同的中断,每个设备有自己的中断号。系统有0-255一共256个中断。系统有一张中断向量表,用于存放256个中断的中断服务程序入口地址。每个入口地址对应一段代码,即中断服务程序。 一般需要保存现场(当前的执行位置和当前状态到两个寄存器上), 模式切换, 找中断表中的函数, 执行函数, 返回恢复状态。

    那么内核态和用户态的交互太多是非常影响性能的, 一般我们使用read, mmap.sendfile去调用内核, 但是不同的方法效率不一样,1. 调用read函数读取文件, 需要进入内核态再返回用户态。这里面拷贝过程比较多。2上面那个步骤可以使用mmap去避免一次拷贝。 kafka使用了这个机制做消息持久化, 它开辟了一个磁盘的mmap , 数据直接从用户态映射到磁盘。
    3. 0用户态拷贝 是通过sendfile实现的, 就是说内核直接打开这个底层磁盘, 将数据直接通过内核态发送给客户端。

    第三个库函数问题: 其实一些库函数在应用层添加了缓存区, 使用库函数调用可以大大减少系统调用的次数, 这个和系统的内核态还不一样。

    new,malloc, 堆栈的认知

    小飞 : 这个一般就是new和malloc分配的, 一般都是有底层内存池来管理的,避免内存碎片产生, 不是每次请求都要执行一次系统中断去调用的。 brk、sbrk、mmap都属于系统调用,若每次申请内存,都调用这三个,那么每次都会产生系统调用,影响性能;其次,这样申请的内存容易产生碎片,因为他不像栈,释放之前前面的必须释放,没有内存碎片。
    所以malloc采用的是内存池(ptmalloc)来减少使用堆内存引起的内存碎片,也就是自己一次申请一块足够大的空间,然后自己来管理,用于大量频繁地new/delete操作。每次配置一大块内存,并维护对应的16个空闲链表,大小从8字节到128字节。如果有小额内存被释放,则回收到空闲链表中。
    (1)如果有相同大小的内存需求,则直接从空闲链表中查找对应大小的子链表。
    (2)如果在自由链表中查找不到或者空间不够,则向内存池进行申请。
    老张: 不错, 内存碎片是发生在代码长时间运行的时候, 由于堆里面很多节点被小内存的数据结构占领了, 如果当你要分配比较大的数据结构,这时候很容易找不到足够大的连续空间, 从而一直服务失败, 这是非常难受的, 所有语言都有这个问题, 一般C++是用一些开源的内存池, 例如google的tmalloc去分配, 他的分配原理是非常复杂的, 这种组件基本不是个人实现的, 你自己造别人根本不敢用。

    老张: 说道这里, 你晓得java的gc是什么意思么?为什么java不容易内存泄漏

    小飞: 这个我知道, 这些语言堆上数据基本上都是自动释放的, 例如他会定期扫描你有没有不用的堆内存,然后给你释放掉, 但是C++如果用普通指针就不会, 因为它不知道其他的地方用到这个堆数据没, 因此需要手动释放。 但是gc运行的时间是不能固定的, 如果刚好你的程序在处理关键的业务, 但是gc要进行大量的不用堆数据释放, 就会占用cpu过多, 导致你的业务突然变卡, 这在游戏和搜索行业是绝对不容发生的, 因为这种业务场景, 用户不会容忍卡0.1s的 。

    RAII的思想

    资源通过{}进行释放或者回收

    零开销原则

    引用 + 指针 + 右值引用这三条, 让资源返回的非常合适。

    动态库 | 静态库| 源代码编译的理解

    一般动态库编译运行速度慢, 静态库编译的话如果你依赖的各种库链接的静态库因为版本不一致, 函数定义不同, 那么链接起来有版本问题, 还要找源码进行修改。 其实还不如源码全部下载到本地编译进来, 速度还是非常可以的。

    基本语法: 函数、变量、类

    C++基本语法就是数据类型, 循环控制, 函数这些。 比如下面的这个代码。

    // main.cpp
    #include 
    
    #include "calc.h"
    using namespace std;
    
    int main(int args, char** argc) {
       
      double a = 0.2;
      int aa = 0;
      double b = 2.2;
    
      double c = sum(a, b);
      double d = multi(a, b);
      double e = multi(a);
      double f = multi(aa);
    
      cout << "c : " << c << endl;
      cout << "d : " << d << endl;
      cout << "e : " << e << endl;
      cout << "f : " << f << endl;
    
      for (int i = 0; i < 10; ++i) {
       
        cout << "i:" << i << endl;
      }
    }
    \end{
       lstlisting}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    // calc.h
    
    
    #pragma once
    
    #include 
    using namespace std;
    
    double sum(double a, double b);
    
    double multi(double a, double b = 2.2);
    
    double multi(int a, double b = 2.2);
    
    double multi(vector<int>& a, vector<int>& b);
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    // calc.cpp
    
    
    
    
    double sum(double a, double b) {
        return a + b; }
    
    double multi(double a, double b) {
        return a + b; }
    
    double multi(int aa, double b) {
        re
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  • 相关阅读:
    RK3588开发笔记-USB3.0接口调试
    Graph WaveNet:用于时空图建模的图神经网络结构
    关于QGraphicsView通过eventFilter无法过滤鼠标事件
    蓝牙AOA融合定位技术汇总
    IGraph使用实例——线性代数计算(blas)
    使用python来访问Hadoop HDFS存储实现文件的操作
    关于接口|常见电商API接口种类|接口数据类型|接口请求方法
    十进制转换成2进制
    工业互联网数据监测预警解决方案
    Seata之TCC模式
  • 原文地址:https://blog.csdn.net/liupeng19970119/article/details/127833466