C++基础

基础语法

  1. 在C和C++语言中的全局变量和静态变量都是会自动初始化为0,堆和栈中的局部变量不会初始化而拥有不可预测的值。 C++保证了所有对象与对象成员都会初始化,但其中基本数据类型的初始化还得依赖于构造函数。
  2. 在C语言中int a;表示声明了整型a但未初始化,而C++中的对象总是会被初始化的,无论是否写了圆括号或者是否写了参数列表;定义基本数据类型变量(单个值、数组)的同时可以指定初始值,如果未指定C++会去执行默认初始化(default-initialization)。变量初始化
  3. size_t,ssize_t,int和long的区别
  4. gcc test.c -o test编译指令分为四个阶段进行,即预处理(也称预编译,Preprocessing, 把头文件内容真正引入cpp文件中)、编译(Compilation生成汇编代码.s文件)、汇编 (Assembly编译为目标文件.o)和连接(Linking包括静态连接库中需要的库文件)。汇编语言入门教程
  5. 函数库分为静态库和动态库两种。静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在。无论静态库,还是动态库,都是由.o文件创建的。
    • .o 一个.c或.cpp文件对应一个.o文件, 也就是编译器编译cpp源代码文件生成的目标文件
    • .a是静态库,是好多个.o合在一起,多个.a可以链接生成一个可执行文件;静态库文件名的命名规范是以lib为前缀,紧接着跟静态库名,扩展名为.a
    • .so是动态库,文件名命名规范和静态库文件名命名规范类似,也是在动态库名增加前缀lib,但其文件扩展名为.so
    • 默认情况下, GCC在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链接库,如果需要的话可以在编译时加上-static选项,强制使用静态链接库。

智能指针boost::scoped_ptr/std::auto_ptr

boost::scoped_ptr和std::auto_ptr(已被C++11废弃)非常类似,是一个简单的智能指针,它能够保证在离开作用域后对象被自动释放,避免由于忘记手动调用delete而造成内存泄漏。实现原理都是利用了一个栈上的对象去管理一个堆上的对象,从而使得堆上的对象随着栈上的对象销毁时自动删除。

两者区别

  1. 不能转换所有权:boost::scoped_ptr所管理的对象生命周期仅仅局限于一个区间(该指针所在的”{}”之间),无法传到区间之外,无法拷贝。这就意味着boost::scoped_ptr对象是不能作为函数的返回值的(std::auto_ptr可以)。
  2. 不能共享所有权:这点和std::auto_ptr类似。这个特点一方面使得该指针简单易用。另一方面也造成了功能的薄弱——不能用于stl的容器中。
  3. 不能用于管理数组对象:由于boost::scoped_ptr是通过delete来删除所管理对象的,而数组对象必须通过deletep[]来删除,因此boost::scoped_ptr是不能管理数组对象的,如果要管理数组对象需要使用boost::scoped_array类。
  4. 如何在他们之间取舍取决于是否需要转移所管理的对象的所有权(如是否需要作为函数的返回值)。如果没有这个需要的话,大可以使用boost::scoped_ptr,让编译器来进行更严格的检查,来发现一些不正确的赋值操作。

C++ 显式类型转换

分别为转换方式,目标类型,被转换的值。
1
2
3
4
5
6
7

1. ```static_cast<type>(expression)```: 不提供运行时的检查的类型转换方式
* 主要用于父类和子类之间指针和引用的转换;进行上行转换,把子类对象的指针/引用转换为父类指针/引用,这种转换是安全的;进行下行转换,把父类对象的指针/引用转换成子类指针/引用,这种转换是不安全的
* 用于基本数据类型之间的转换,例如把int转char,int转enum等,需要编写程序时来确认安全性
* 把void指针转换成目标类型的指针(这是极其不安全的)

2. ```dynamic_cast<type>(expression)```: 在运行时检查类型转换是否合法,具有一定的安全性,额外消耗一些性能;仅适用于指针或引用转换,若指针转换失败,则返回空指针,若引用转换失败,则抛出异常。经常用于将指针转换为void*: ```A *pA = new A; void *pV = dynamic_cast<void *>(pA);
  1. 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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    4. ```reinterpret_cast```非常激进的指针类型转换,在编译期完成,可以转换任何类型的指针,所以极不安全。非极端情况不要使用。

    ### 守护进程
    #### Supervisor
    1. Supervisor是一个C/S系统, 允许用户监控和管理一系列运行于类Unix系统上的进程。通常,我们使用Supervisor将一个普通的命令行进程变为后台daemon,并监控这一些进程,以便这些进程在意外退出后能够重新启动。
    2. Supervisor有以下优点:
    * 简单, ini风格的配置文件,容易阅读,对每个进程都提供了许多选项以便失败重启和日志轮替。
    * 集中,一站式的进程启动,停止,监控平台。进程可以单独管理或分组管理,更提供了Web管理界面。
    * 高效,通过fork/exec启动子进程且子进程不会进入守护模式,当子进程意外终止时,操作系统会立即发送信号给Supervisor,而不像其他方案那样,需要通过PID轮询。
    * 可扩展,拥有非常简单的事件通知协议,任何编程语言程序都能监控它,也提供了XML-RPC管理接口,也为Python开发者预留了扩展空间。
    兼容性,除了Windows,几乎可以运行于任何操作系统,已经测试过的包括Linux, Mac OS X, Solaris, 和 FreeBSD。
    * 久经考验,虽然开发非常活跃,但Supervisor并不是新软件,它已经存在多年,广泛运行于许多服务器上。

    #### systemd
    1. Systemd是一个Linux操作系统下的系统和服务管理器。它被设计成向后兼容SysV启动脚本,并提供了大量的特性,如开机时平行启动系统服务,按需启动守护进程,支持系统状态快照,或者基于依赖的服务控制逻辑,已成为大多数发行版的标准配置。可以通过 systemctl --version 命令来查看使用的版本
    3. systemd 和 supervisord 各有长短,不存在哪一方绝对的碾压。systemd 跟 Linux 紧密结合,所需的依赖少,其提供的保障自然比 supervisord 更可靠。然而在强大的能力背后,也有配置复杂、不易上手等问题。

    ### 常用测试工具
    1. Apache自带ApacheBench程序对Apache或其它类型的服务器进行网站访问压力测试。

    > ApacheBench命令原理:ab命令会创建很多的并发访问线程,模拟多个访问者同时对某一URL地址进行访问。它的测试目标是基于URL的,因此,既可以用来测试Apache的负载压力,也可以测试nginx、lighthttp、tomcat、IIS等其它Web服务器的压力。

    > ab命令对发出负载的计算机要求很低,既不会占用很高CPU,也不会占用很多内存,但却会给目标服务器造成巨大的负载,其原理类似CC攻击。自己测试使用也须注意,否则一次上太多的负载,可能造成目标服务器因资源耗完,严重时甚至导致死机。

    2. [服务器性能评估相关指标QPS](https://ruby-china.org/topics/26221)

    ### C++常用错误调试工具

    #### core dump
    core dump是大多数UNIX系统实现的一种特性,当进程运行的过程中异常终止或崩溃时,操作系统会将进程当前的内存映像和一部分相关的调试信息写入core文件,例如寄存器信息(包括程序指针、栈指针等)、内存管理信息、其他处理器和操作系统状态和信息。我们可以使用```ulimit -c unlimited```命令来开启core dump。

    Linux中信号是一种异步事件处理的机制,每种信号对应有其默认的操作,默认操作主要包括忽略该信号(Ingore)、暂停进程(Stop)、终止进程(Terminate)、终止并发生core dump(core)等;```kill -l```可以看到linux里边的信号种类,默认动作中可能产生core文件的信号包括以下种类:

    信号名字 | 说明 | 默认动作
    --- | --- | ---
    SIGABRT(6) | 异常终止(调用abort函数产生此信号)| 终止+core
    SIGBUS(7) | 硬件故障,比如出现某些内存故障 | 终止+core
    SIGEMT | 硬件故障 | 终止+core
    SIGFPE(8) | 算术异常,比如除以0,浮点溢出等 | 终止+core
    SIGILL(4) | 非法硬件指令 | 终止+core
    SIGIOT(29) | 硬件故障 | 终止+core
    SIGQUIT(3) | 终端退出符,比如Ctrl+C | 终止+core
    SIGSEGV(11) | 无效内存引用 | 终止+core
    SIGSYS(31) | 无效系统调用 | 终止+core
    SIGXCPU(24) | 超过CPU限制(setrlimit)| 终止+core/忽略
    SIGXFSZ(25) | 超过文件长度限制(setrlimit)| 终止+core/忽略

    这就是为什么我们使用```ctrl+z```来挂起一个进程或者```Ctrl+C```结束一个进程均不会产生 core dump,因为前者会向进程发出SIGTSTP信号,该信号的默认操作为暂停进程(Stop Process);后者会向进程发出SIGINT信号,该信号默认操作为终止进程(Terminate Process)。同样上面提到的 kill -9 命令会发出 SIGKILL 命令,该命令默认为终止进程。而如果我们使用 Ctrl+\ 来终止一个进程,会向进程发出 SIGQUIT 信号,默认是会产生 core dump 的。还有其它情景会产生core dump, 如:程序调用 abort() 函数、访存错误、非法指令等等。

    #### dmesg
    **段错误**是指访问的内存超出了系统给这个程序所设定的内存空间, 例如访问了不存在的内存地址,访问了系统保护的内存地址,访问了只读的内存地址等等情况。```dmesg```命令显示linux内核的环形缓冲区信息,我们可以从中获得诸如系统架构、cpu、挂载的硬件,RAM等多个运行级别的大量的系统信息。通过dmesg命令可以查看发生段错误的程序名称、引起段错误发生的内存地址、指令指针地址、堆栈指针地址、错误代码、错误原因等。

    1. dmesg信息解读: 如下是bucket bin文件段错误信息,ip信息会用到后续objdump反编译分析

    ```bucket[14306]: segfault at b8c6000 ip 0000000000c3dece sp 00007ffd2c96ae80 error 6 in bucket[400000+eea000]

error number是6, 转成二进制就是110, 即bit2=1, bit1=1, bit0=0, 按照上面的解释,我们可以得出这条信息是由于用户态程序写操作访问越界,访问的是无效地址造成的。

error number是由三个字位组成的,从高到底分别为bit2 bit1和bit0,所以它的取值范围是0~7.

  • bit2: 值为1表示是用户态程序内存访问越界,值为0表示是内核态程序内存访问越界
  • bit1: 值为1表示是写操作导致内存访问越界,值为0表示是读操作导致内存访问越界
  • bit0: 值为1表示没有足够的权限访问非法地址的内容,值为0表示访问的非法地址根本没有对应的页面,也就是无效地址
  1. addr2line -e 进程名 c3dece; 使用addr2line命令获取出错具体行号

objdump

objdump命令是Linux下的反汇编目标文件或者可执行文件的命令,使用objdump生成二进制的相关信息,并重定向到文件中

-d(-S尽可能完全反编译) bin/bucket > segfaultDump```
1
2

```grep -n -A 10 -B 10 "c3dece" ./segfaultDump

pstack和strace

strace跟踪程序使用的底层系统调用,可输出系统调用被执行的时间点以及各个调用耗时;pstack工具对指定PID的进程输出函数调用栈。一般两者配合调用,先用strace查看耗时的系统调用,然后用pstack看函数调用栈信息,找到出问题的函数。

  1. 在Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通过系统调用访问硬件设备。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    	
    # -f -F选项设置同时跟踪fork和vfork出来的进程,输出到~/straceout.txt,myserver是要启动和调试的程序
    strace -f -F -o ~/straceout.txt myserver

    # 跟踪28979进程的所有系统调用(-e trace=all),并统计系统调用的花费时间,以及开始时间(并以可视化的时分秒格式显示)
    strace -o output.txt -T -tt -e trace=all -p 28979
    2. ```pstack pid```: print a stack trace of a running process
    3. [实例讲解如何使用strace+pstack利器分析程序性能](https://blog.csdn.net/jiang1013nan/article/details/17558165)

    #### 其它
    1. ldd:使用ldd命令查看二进制程序的共享链接库依赖,包括库的名称、起始地址,这样可以确定段错误到底是发生在了自己的程序中还是依赖的共享库中。例如```ldd bin/bucket

  2. nm:使用nm命令列出二进制文件中的符号表,包括符号地址、符号类型、符号名等,这样可以帮助定位在哪里发生了段错误。

    bin/bucket```
    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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42


    #### 参考链接
    1. [Linux工具](http://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/objdump.html)



    ### condition_variable/mutex/thread_pool

    1. **条件变量(Condtion Variable)是在多线程程序中用来实现“等待->唤醒”逻辑常用的方法,条件变量能使一个或多个线程进入阻塞状态,直到接到另一个线程的通知(notify),或者发生超时或虚假唤醒时,才退出阻塞**。例如,应用程序A中包含两个线程t1和t2。t1需要在bool变量test_cond为true时才能继续执行,而test_cond的值是由t2来改变的,这种情况下可供选择的方案有两种:

    * t1定时的去轮询变量test_cond,如果test_cond为false,则继续休眠;如果test_cond为true,则开始执行。
    * 利用条件变量,t1在test_cond为false时调用cond_wait进行等待,t2在改变test_cond的值后,调用cond_signal,唤醒在等待中的t1,告诉t1 test_cond的值变了,这样t1便可继续往下执行。
    * Wait_for: 在条件变量收到信号或者指定的超时发生前,线程一直处于阻塞状态;
    * Wait_until:在条件变量收到信号或者指定的时刻到达之前,线程一直处于阻塞状态。

    2. **Thread相关的函数**:

    * t.join():使调用线程(本例是指主线程)一直处于阻塞状态,直到正在执行的线程t执行结束。如果线程函数返回某个值,该值也将被忽略。不过,该函数可以接收任意数量的参数,尽管可以向线程函数传递任意数量的参数,但是所有的参数应当按值传递。如果需要将参数按引用传递,那要向下例所示那样,必须将参数用std::ref 或者std::cref进行封装。

    ```cpp
    void func(int i, double d, const std::string& s)
    {
    std::cout << i << ", " << d << ", " << s << std::endl;
    }

    void func1(int& a)
    {
    a++;
    }

    int main()
    {
    std::thread t(func, 1, 12.50, "sample");
    t.join();

    int a = 42;
    std::thread t1(func, std::ref(a));
    t1.join();

    return 0;
    }

    • Detach: 允许执行该方法的线程脱离其线程对象而继续独立执行。脱离后的线程不再是可结合线程(你不能等待它们执行结束)。

    • get_id: 返回当前线程的id

    • yield:在处于等待状态时,可以让调度器先运行其他可用的线程。

    • sleep_for:阻塞当前线程,时间不少于其参数指定的时间。

    • sleep_util:在参数指定的时间到达之前,使当前线程一直处于阻塞状态。

  3. std::recursive_mutex:允许同一个线程多次获取同一个互斥量,可获取的互斥量的最大次数并没有具体说明。但是一旦超过最大次数,再对lock进行调用就会抛出std::system_error错误异常。

  4. Timed_mutex和Recursive_timed_mutex:try_lock_for() 和 try_lock_until(),分别用于在某个时间段里或者某个时刻到达之间获取该互斥量

  5. Lock_guard: 在构造对象时,它试图去获取互斥量的所有权(通过调用lock()),在析构对象时,自动释放互斥量(通过调用unlock()).这是一个不可复制的类。

  6. Unique_lock: 这个一通用的互斥量封装类,不同于lock_guard,它还支持延迟加锁,时间加锁和递归加锁以及锁所有权的转移和条件变量的使用。这也是一个不可复制的类,但它是可移动类。

  7. C++11 中的线程、锁和条件变量

Linux中的线程同步机制-Futex/Mutexes/Condition Variables/POSIX Semaphores

Futex是一种用户态和内核态混合的同步机制。首先,同步的进程间通过mmap共享一段内存,futex变量就位于这段共享 的内存中且操作是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的futex变量,如果没有竞争发生,则只修改futex,而不 用再执行系统调用了。当通过访问futex变量告诉进程有竞争发生,则还是得执行系统调用去完成相应的处理(wait 或者 wake up)。简单的说,futex就是通过在用户态的检查,(motivation)如果了解到没有竞争就不用陷入内核了,大大提高了low-contention时候的效率。
Linux多线程同步机制参考链接

C++的RAII —- 资源获取即初始化

C++的一种利用C++构造的对象最终会被销毁的原则来管理资源避免泄露的用法。具体做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。

如何使用RAII ?
把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。例如对socket、互斥锁、文件句柄和内存的申请和释放。

其它

  1. C++ 11 auto关键字编译时类型推断。
  2. std::map::emplace_hint 暗示指定位置前插入元素;make_shared构造类型对象和make_pair构造pair
  3. 常用第三方库:boost,gflag,glog,folly,chromium,bazel代码构建
  4. 性能分析和内存泄漏:gperftools
  5. C++单例:std::atomic + DCLP, std::once_flag
  6. 线程池:rocksdb/util/thread_pool_imp.h
  7. C++智能指针:boost::smart_ptr, std::shared_ptr, std::unique_ptr
  8. boost::shared_ptr 主要的功能是,管理动态创建的对象的销毁。它的基本原理就是记录对象被引用的次数,当引用次数为 0 的时候,也就是最后一个指向某对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。共享指针对象重载了 operator* 和 operator-> , 所以你可以像通常的指针一样使用它. shared_ptr
  9. C++左值与右值引用: C++( 包括 C) 中所有的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,那些可以在多条语句中使用的对象。所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。右值是指临时的对象,它们只在当前的语句中有效。左值的声明符号为”&”, 为了和左值区分,右值的声明符号为”&&”。右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。
  10. C++11之后支持lamda语法:std::function可调用实体,std::bind返回可调用实体,用于后续作为回调函数