Skip to main content
  1. Docs/

C/CPP 笔记

·30 mins· ·
Owl Dawn
Author
Owl Dawn
Table of Contents

C
#

memory
#

  • memcpy 和 memmove

    void *memcpy(void *restrict s1, const void *restrict s2, size_t n);
    void *memmove(void *s1, const void *s2, size_t n);
    

    都是将s2指向位置的n字节数据拷贝到s1指向的位置,区别就在于关键字 restrict,memcpy 假定两块内存区域没有数据重叠

    memcpy 顺序的循环,把字节一个一个拷贝

    memmove 会对拷贝的数据作检查,确保内存没有覆盖,如果发现会覆盖数据,简单的实现是调转开始拷贝的位置,从尾部开始拷贝。在 C99 实现中,是将内容拷贝到临时空间,再拷贝到目标地址中

    memcpy 的速度比 memmove 快一点,如果使用者可以确定内存不会重叠,则可以选用 memcpy,否则 memmove 更安全一些。另外一个提示是第三个参数是拷贝的长度,如果你是拷贝 10 个 double 类型的数值,要写成 sizeof(double) * 10,而不仅仅是 10。

遍历
#

平常遍历二维数组习惯一行一行遍历还是一列一列?这两个从性能方面有什么区别

  • 行遍历是个好习惯,符合空间局部性原理。因为在内存中,数据是按行排列的,也就是说,行内相邻的两个数,在内存中也是相邻的。如果按列遍历,就有可能由于内存不足导致缺页中断反复横跳(内存颠簸)。从而导致效率低下。
  • 当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。 缓存从内存中抓取一般都是整个数据块,所以它的物理内存是连续的,几乎都是同行不同列的,而如果内循环以列的方式进行遍历的话,将会使整个缓存块无法被利用,而不得不从内存中读取数据,而从内存读取速度是远远小于从缓存中读取数据的。
  • 分页调度:物理内存是以页的方式进行划分的,当一个二维数组很大是如 int[128][1024],假设一页的内存为 4096 个字节,而每一行正好占据内存的一页,如果以列的形式进行遍历,就会发生 128 * 1024 次的页面调度,而如果以行遍历则只有 128 次页面调度,而页面调度是有时间消耗的,因而调度次数越多,遍历的时间就越长。

CPP
#

关键字
#

define 和 typedef
#

typedef 定义类型的别名,编译阶段有效,有类型检查的功能;有作用域限制

typedef int* tp;
const tp ptr;    // 此时 ptr 为指针常量,使用 define 则为指向常量的指针
tp ptr1, ptr2;   // 二者都为指针,使用 define 则不是

define 是宏定义,发生在预处理阶段,只进行文本替换,不进行任何检查;没有作用域限制

inline
#

最早作为内联优化提示,用于引导将所修饰的且满足内联条件的函数折叠进调用处。

现代编译器已十分智能,不太 care 这个指示

现在使用 inline 标记一个函数时,允许编译单元中有相同签名的实体,最后链接时只保留一个。就可以将函数的实现写在头文件里,而不用担心冲突了。而如果没有 inline 标记的函数,你把函数体写在头文件里,该头文件被多个源文件包含并使用时,链接时就会冲突。

static
#

全局(静态)存储区、分为 DATA 段和 BSS 段。DATA 段(全局初始化区)存放初始化的全局变量和静态变量;BSS 段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。其中 BBS 段在程序执行之前会被系统自动清 0,所以未初始化的全局变量和静态变量在程序执行之前已经为 0。

在 C++ 中 static 的内部实现机制:静态数据成员要在程序一开始运行时就必须存在。因为函数在程序运行中被调用,所以静态数据成员不能在任何函数内分配空间和初始化。

静态数据成员要实际的分配空间,不能在类的声明中定义。类声明只声明一个类的尺寸和规格,不进行实际的内存分配

  • static 被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间,未经初始化的静态全局变量会被程序自动初始化为 0,静态数据成员按定义出现的先后顺序依次初始化,注意静态成员嵌套时,要保证所嵌套的成员已经初始化了。消除时的顺序是初始化的反顺序。
  • 在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
  • static 修饰全局变量或函数的时候,这个全局变量或函数只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
  • 不想被释放的时候,可以使用 static 修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。
  • 未经初始化的静态全局变量会被程序自动初始化为 0
  • **静态局部变量:**在全局数据区分配内存;始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束。
  • 类的静态成员变量必须先初始化再使用。

register
#

用于指示变量的寄存器优化,现在编译器不太care这个指示,c++11开始标记为废弃,c++17正式废用

explicit
#

一般用于修饰一个参数(或除第一个参数,其他参数有默认值)的类构造函数,表明该构造函数是显式的,防止类构造函数的隐式自动转换,对应的关键字是 implicit(隐式)

修饰多参数的构造函数时,不能使用 initialized_list 的隐式转换(带等号的 copy-list-initialization 不允许调用 explicit 构造函数)

未声明则默认为隐式

隐式构造函数只有一个参数的时候,编译时会有一个缺省的转换操作,将该类数据转换为该类对象

extern
#

extern 可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。此外 extern 也可用来进行链接指定。

  • 与 “C” 连用表示按照C的规则翻译对应函数名而不是 c++
  • 声明函数或全局变量范围,其==声明==的函数和变量可以在本模块或其他模块中使用

当extern有定义时,相当于没有extern

extern 和 static 不能同时修饰一个变量,static 作用域仅限于本文件,其他编译单元看不到

const
#

volatile
#

volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据,即使这个值已经保存在了寄存器中。与并发无关,对并发使用 std::atomic

mutable
#

被 mutable 修饰的变量,将永远处于可变的状态,即使在一个 const 函数中

如需要在 const 函数里修改一些与类状态无关的数据成员,则这个数据成员可以被 mutable 修饰

cast
#

const_cast

用于修改类型的 constvolatile 属性

  • 它是唯一能做到这一点的 C++ 风格的强制转型,而 C 不提供消除 const 的机制。一般用于指针的 const 属性

    const int* const_p = &constant;
    int* modifier = const_cast<int*>(const_p);
    *modifier = 7;
    

static_cast

static_cast < type-id > ( expression )

expression 转换为 type-id 类型

  • 没有运行时类型检查来保证转换的安全性
  • 可将 non-const 对象转型为 const 对象
  • static_cast 不能转换掉 expressionconstvolitale、或者 __unaligned 属性。

dynamic_cast

只用于对象的指针和引用,主要用于执行“安全的向下转型”,也就是说,要确定一个对象是否是一个继承体系中的一个特定类型。如果下行转换不安全的话其会返回空指针。

==将基类的指针或引用安全地转换成派生类的指针或引用==,并用派生类的指针或引用调用非虚函数。

  • 是唯一不能用旧风格语法执行的强制转型,也是唯一可能有重大运行时代价的强制转型。
  • 当用于多态类型时(包含虚函数),它允许任意的隐式类型转换以及相反过程。
  • 使用 dynamic_cast 是需要开销的,根据 RTTI 检查操作是否有效

reinterpret_cast

能够在非相关的类型之间转换,操作结果只是简单的从一个指针到别的指针的值的二进制拷贝。

  • 在类型之间指向的内容不做任何类型的检查和转换

  • 必须是一个指针、引用、算术类型、函数指针或者成员指针

  •   int n = 9;
      // reinterpret_cast 仅仅是复制 n 的比特位到 d,因此d 包含无用值。
      double d = reinterpret_cast<double&> (n);
    

应用场景:hash 函数将指针转换为 整型数据

delete
#

delete 和 delete[] 的区别

==对数组使用 delete 不会出现内存泄漏==

使用 delete 仅调用了第一个数组元素的析构函数,然后将整块数组内存释放

使用 delete [] 会依次调用各个元素的析构函数,再将整个数组内存释放

delete[] 是如何知道析构的大小的

对于简单类型,new[] 计算好大小后调用 operator new;

对于复杂数据结构,new[] 先调用 operator new[] 分配内存,然后再前四个字节写入数组大小 n,然后调用 n 次构造函数没针对复杂类型,new[] 会额外存储数组大小

free
#

malloc 申请得到内存后,再

在大多数实现中,free 不会将内存返回给操作系统(极少数情况下)。操作系统只能处理特定大小和对齐方式的内存块,即只能处理虚拟内存管理器可以处理的块(通常为 512 字节的倍数)。free 会将内存块放入到自己的空闲块列表(循环内存块链表)中(会尝试将地址空间中的相邻块融合在一起)。

每个内存块需要额外的数据,且随着尺寸变小,更多的碎片会发生。

当需要新的内存块时,空闲列表是 malloc 寻找的第一个位置。在操作系统调用新内存之前会扫描空闲列表,当发现一个块比内存大时会分成两部分,一个返回调用者,一个放回空闲列表。

内部会有各种优化,比如对块大小排序,分割、分配算法等

malloc、free / brk、mmap
#

底层由 brkmmapmunmap 这些系统调用实现

  • 对小于128k 的内存,brk 是将数据段(.data)的最高地址指针 _edata 往高地址推,第一次访问分配的虚拟地址空间时,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。分配的内存需要等到高地址空间释放后才能释放,将 _edata 指针回推。当最高地址空间的空闲内存超过 128kb 时执行内存紧缩操作,将 _edata 指针回推

  • 对大于128k 的内存, mmap 是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

    mmap 是一种内存映射方法,将一个文件或其他对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址一一对应的关系。内核空间对这片区域的改变也直接反映到用户空间,实现不同进程的文件共享。

    linux 内核使用 vm_area_struct 结构表示一个独立的虚拟内存区域,一个进程使用多个 vm_area_struct 来分别表示不同类型的虚拟内存区域

    当 vm_area_struct 数目较少时,按照升序以单链表的形式组织结构,在数目较多时使用 AVL 树来实现。

    ==mmap 函数是创建一个新的 vm_area_struct 结构,并将其与物理地址相连。==

mmap 内存映射原理:
#
  • 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
    1. 用户空间调用 mmap
    2. 在当前进程虚拟地址空间中寻找一段空间满足要求的连续的虚拟地址
    3. 为此虚拟区分配一个 vm_area_struct 结构,接着对这个结构的各个域进行初始化
    4. 将新建的虚拟结构(vm_area_struct)插入到进程的虚拟地址区的链表或树中
  • 内核空间系统调用 mmap 实现文件物理地址和进程虚拟地址的一一映射关系
    1. 通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,加入到 struct file 中
    2. 将用户空间与设备内存相连,对虚拟地址的访问转换为对设备的访问
    3. 通过 inode 模块找到对应的文件,也就是磁盘的物理地址
    4. 建立页表,实现文件地址和虚拟地址区域的映射关系。这里只建立了映射关系,主存中没有对应物理地址的数据
  • 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到主存的拷贝
    1. 进程的读写,通过查询页表发现这一段地址不在物理页面上,引发缺页异常
    2. 缺页异常判断,申请调页
    3. 判断 swap cache 中有没有需要访问的内存页,如果没有调用 nopage 把所缺的页从磁盘装入主存
    4. 之后可以进行读写,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。可以调用 msync() 来强制同步,

mmap函数第一种用法是映射磁盘文件到内存中;而malloc使用的mmap函数的第二种用法,即匿名映射,匿名映射不映射磁盘文件,而是向映射区申请一块内存

calloc / realloc
#

calloc 申请的空间值时初始化为 0 的,realloc 给动态分配的空间分配额外的空间,用于扩充容量

内存分区
#

内存中的栈区是处于高地址以地址的增长方向为上,栈地址是向下增长的。从上往下,栈区是分配局部变量空间。堆区是从下往上,堆区的地址是向上增长的用于分配程序员申请的内存空间。

栈是向下增长的,是一块连续的区域。也就是说栈从栈顶的地址和栈的最大容量是系统预先规定好的

堆是向上增长的,向高地址递增的,因为它是一个用链表来存储的空闲地址空间。自然是个不连续的内存区域。堆的大小受限于计算机系统中有效的虚拟内存。

堆和栈的区别

1、管理方式不同;

2、空间大小不同;

3、能否产生碎片不同;

4、生长方向不同;

5、分配方式不同;

6、分配效率不同;

  • 一般由程序员分配释放,并指明大小, 若程序员不释放,动态内存的生命周期由程序员自己决定,程序结束时可能由OS回收。提供了动态分配的功能

    在操作系统中有一个记录空闲内存地址的表,这是一种链式结构。它记录了有哪些还未使用的内存空间。当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

    对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。由于找到的堆结点的大小不一定正好等于申请的大小,系统会将自动的多余的那部分重新放入空闲链表中。

  • 由编译器自动分配释放 ,存放函数参数值,局部变量值等。其操作方式类似于数据结构中栈。栈的效率比较高

  • 自由存储区

    C++中通过 new 和 delete 动态分配和释放对象的抽象概念,通过 new 来申请的内存区域可称为自由存储区。默认使用堆来实现自由存储

  • 全局/静态存储区

    全局变量和静态变量,内存在程序编译的时候就已经分配好了,这块内存在程序的整个运行期间都存在

  • 程序代码区

    存放函数体的二进制代码。

  • 常量存储区

    常量字符串存放这里。程序结束后由系统释放

placement new
#

new表达式允许我们向new传递额外的参数

int *p2 = new (nothrow)int;//如果分配失败,new返回一个空指针,nothrow使不会抛出异常

多态
#

==为不同的数据类型的实体提供统一的接口==

c++中,多态表示“以一个 public base class 指针或引用寻址出一个 derived class object ”

支持方式

  • 一组隐式转换操作,将派生类指针转化为指向基类类型的指针

    shape* ps = new circle();
    
  • 经由虚函数机制

  • 经由 dynamic_casttypeid 运算符(将基类的指针或引用安全的转换为派生类的指针或引用(需要至少包含虚函数))

    if(circle* pc = dynamic_cast<circle*>(ps))...
    

静态多态
#

继承时,函数没有使用 virtual 关键字,用基类指针指向派生类对象,此时在编译时就已经确定了以基类的函数来当做输出的函数,所以用基类指针指向子类对象还是会调用基类的函数,这就是静态链接(静态多态)

也成为编译期多态,编译器在编译期间,根据函数实参的类型,推断出调用哪个函数

  • 函数重载、运算符重载
  • 函数模板使用,使用泛型来定义函数,通过将类型作为参数传递给模板,使编译器生成该类型函数

动态多态
#

即运行时多态,在程序执行期间判断所引用对象的实际类型,根据其实际类型调用相应的方法

静态绑定
#

编译时根据调用函数(方法或过程)提供的信息,把它对应的具体函数确定下来,即在编译时就把调用函数名和具体函数绑定在一起

动态绑定
#

在编译程序时还不能确定函数所对应的具体函数,只有在程序运行过程中才能确定函数调用多对应的具体函数,即在程序运行时才把调用函数名与具体函数绑定在一起。

#

RTTI指针

指向存储运行时类型信息(type_info)的地址,用于运行时类型识别,用于 typeid 和 dynamic_cast。

构造函数
#

成员初始化列表:

编译器会再任何显示用户代码段之前,以适当的顺序在构造函数之内安插初始化操作

对于一些复杂类型,少了一次调用构造函数的过程,在函数体中赋值会多一次拷贝构造调用。

在构造函数中调用另一个构造函数(注意不要无限套娃)

  • c++11 支持:函数初始化列表中调用另一个构造函数

  • 用 this 指针显式调用构造函数(一定要加上作用域名)

  • 在原始内存覆盖

    test(){
        new(this)tst(b);
    }
    tst(int val) : member(val);
    

可虚

构造函数不可以是虚函数,且不要再构造函数中调用虚函数(父类在子类之前构造,此时子类数据成员还未初始化,调用子类虚函数不安全)

  • 构造函数就是为了在编译阶段确定对象的类型以及为对象分配空间,如果类中有虚函数,那就会在构造函数中初始化虚函数表,虚函数的执行却需要依赖虚函数表。如果构造函数是虚函数,那它就需要依赖虚函数表才可执行,而只有在构造函数中才会初始化虚函数表,鸡生蛋蛋生鸡的问题,很矛盾,所以构造函数不能是虚函数。
  • **虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。**构造函数本身就是要初始化实例,那使用虚函数就没有实际意义
  • 构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数

析构函数一定是虚函数(如果基类的析构函数不是虚函数,会导致派生类无法被析构)

  • 用基类类型指针绑定派生类实例时,如果基类析构函数不是虚函数,则只会析构基类

不要再析构函数中调用虚函数(先调用子类的析构函数在调用基类,调用基类析构函数时子类对象成员已被销毁)

**构造函数和析构函数中可以调用虚函数,**但是没有动态绑定的效果,父类构造函数中调用的仍然是父类版本的函数,子类中调用的仍然是子类版本的函数。析构函数也是一样,子类先进行析构,这时,如果有virtual函数的话,子类的内容已经被析构了,C++会视其父类,执行父类的virtual函数。

抛出异常

构造函数中可以抛出异常,但会导致析构函数不能被调用,已经申请到资源的内部成员变量会被系统依次调用析构函数,可能会造成内存泄露(可以使用try catch避免new运算符出现的内存泄漏)

析构函数不可以抛出异常,会导致未定义行为,除非在析构函数内消化所有异常或者结束程序

怎样阻止一个类被实例化

  • 将构造函数声明为 private 或 protected

怎样禁止自动生成拷贝构造函数

  • 手动重写,设置为 private
  • 设为 delete
  • 定义一个基类,将拷贝构造和拷贝赋值设为 private

delete this
#

在成员函数中调用 delete this

不可以在类的析构函数中调用 delete this,会导致栈溢出,delete this 会调用本对象的析构函数,而析构函数又调用 delete this,形成无限递归

在成员函数中调用delete this得确保this是由new配置而来(非自定义的new版本,最原始的),且确保该成员函数是最后一个调用this的(delete后不能查看它﹑将它与其它指针或是 NULL 相比较﹑印出其值﹑对它转型等)。析构函数中调用delete this,出导致死循环,造成堆栈溢出。

模板类中可以使用虚函数吗、成员模板函数可以是虚函数吗

可以使用虚函数,但是注意模板不同实例化是不同的类

模板成员函数不可以是虚函数

当前的编译器都期望在处理类的定义的时候就能确定这个类的虚函数表的大小,如果允许有类的虚成员模板函数,那么就必须要求编译器提前知道程序中所有对该类的该虚成员模板函数的调用,而这是不可行的。 对于一个模板函数,不同的模板参数会产生出不同的函数。这样的话,如果要知道类内包含多少个虚函数,就只能去代码中寻找。这就不单单是多文件的问题,还有RTTI的问题了。

const 修饰的对象能调用成员函数吗

只能调用 const 成员函数,不能调用非 const 成员函数,非 const 对象可以调用 const 成员函数

非 const 成员函数中的隐式参数为普通 this 指针,const 成员函数的隐式参数为 const this 指针

继承
#

三种继承方式
#

公有继承、保护继承、私有继承

在struct继承中,如果没有显式给出继承类型,则默认的为public继承;在class继承中,如果没有显式给出继承类型,则默认的为private继承;

不管是哪种继承方式,在派生类内部都可以访问基类的公有成员 和保护成员 , 基类的私有成员在子类中不可见

一般不推荐多继承,但是可以继承两个个父类,其中一个是单纯接口

相关问题
#

有一个类A,里面有个类B类型的b,还有一个B类型的*b,什么情况下要用到前者,什么情况下用后者

  • 一个具体的类和一个类的指针,主要差别就是占据的内存大小和读写速度。类占据的内存大,但是读写速度快。类指针内存小,但是读写需要解引用。所以可知,以搜索为主的场景中,应当使用类。以插入删除为主的场景中,应当使用类指针

    or

    从类B对类A的依附性上考虑:如果类B是一个依附于类A存在的对象,没有单独存在的意义,那就没必要用有指针模式,白消耗一个指针(例如眼睛依附于士兵存在,士兵死了眼睛也没有单独存在的意义);反之如果并没有依附关系,那就得用指针(例如武器和士兵,士兵死了武器还可以被别的士兵捡走,所以得用指针)。

泛型
#

c++ 泛型支持静态多态,当类型信息可得的时候,利用编译期多态能够获得最大的效率和灵活性

每个实际类型都已被指明的泛型都会有独立地编码产生,即 list<int>list<string> 生成的是不同的代码

增加了代码空间,以空间换时间

STL
#

相关问题
#

STL里迭代器什么情况下失效

  • 序列式容器,删除当前的 iterator 会使后面所有元素的 iterator 都失效。这是因为 vetor, deque 使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置。所以不能使用 erase(iter++) 的方式,但 erase 方法可以返回下一个有效的 iterator。
  • 对于关联容器(如 map, set,multimap,multiset ) ,删除当前的 iterator,仅仅会使当前的 iterator 失效,只要在 erase 时,递增当前iterator即可。(dataMap.erase(itr++)先把 iter 传值到 erase 里面,然后 iter 自增,然后执行erase,所以iter在失效前已经自增了。)
  • 链表式容器(如 list),删除当前的 iterator,仅仅会使当前的 iterator 失效,这是因为 list 之类的容器,使用了链表来实现,插入、删除一个结点不会对其他结点造成影响。只要在 erase 时,递增当前 iterator 即可,并且 erase 方法可以返回下一个有效的 iterator。

容器为什么不能存储引用类型

  • c++ 中不能声明或定义指向引用的指针,容器中的 pointer iterator 等类型声明会出错

仿函数

  • 每个仿函数有自己的型别
  • 仿函数可以拥有自己的数据成员和成员变量,意味着仿函数拥有状态

内存管理
#

C++内存工具

释放 类属 可否重载
malloc() free C函数
new delete C++表达式
::operator new() ::operator delete() C++函数 可以
allocater<T>::allocate() allocator<T>::deallocate() C++标准库 可自由设计以发配任何容器

几个基本操作分解
#

new expresssion
#

Complex* pc = new Complex(1,2);

会被编译器转换为

Complex *pc;
try{
    void* mem = operator new(sizeof(Copmplex));  //allocate
    pc = static_cast<Complex*>(mem);             //cast
    pc -> Complex(1,2);                          //construct注意!!用户不能这样调用Ctor,但是Dtor可以,如果想直接调用Ctor,可以运用placement new,
}
catch( std::bad_alloc ){
    //若allocation失败就不执行constructor
}
  1. 分配内存,调用operator new函数
  2. 转型,转换指针类型
  3. 构造,调用Ctor

new expression (array)
#

Complex* pca = new Complex[3];
...
delete[] pca;

会调用3次Ctor,delete[]也会调用3次Dtor

使用普通delete,对class without pointer member可能没有影响,如本例

对class with pointer member通常有影响,会发生内存泄漏,如下图,只会唤起一次dtor

1584707640514

对int等类型,加不加[]无所谓,因为没有调用析构函数

placement new
#

指new(p),或指::operator new(size,void*)

此操作允许我们将对象构建与已经分配的内存上

#include <new>
char* buf = new char[sizeof(Complex)*3];
Complex* pc = new(buf)Complex(1,2);
...
delete[] buf;

new()操作被编译器转换为

Complex *pc;
try{
    void* mem = operator new(sizeof(Complex),buf);  //allocate
    //void* operator new(size_t,void* loc){return loc}  此函数只返回地址
    pc = static_cast <Complex*>(mem);               //cast
    pc -> Complex::Complex(1,2);                    //construct
}
catch( std::bad_alloc ){
    //若allocation失败就不执行constructor
}

重载
#

1584708987892

如果重载,建议不要重载全局版本,影响太大,建议使用1版本

或者使用new(p)进行模拟

c++容器则将ctor与dtor包装在两个函数中construct() destroy()

1584709433157

**重载全局版本的示例(影响贼大)**如上图,右侧为vc6.0的原版

重载非全局版

  • 重载函数应为static,创建对象时,没有对象供你调用该函数
  • 第一个参数必须是size_t其余参数以new所指定的placement arguments为初值,如Fpp* pf = new(300,'c')Foo,其中300和’c’即placement arguments
  • 只有当new所调用的ctor抛出exception,才会调用这些对应重载版的operator delete(),用来处理异常

1584710015609

1584774050830

设计简易alloctor
#

  1. 添加指针
    #

    1584773141932

    每次调用malloc()时会在分配内存头尾添加cookie,一次调用24块内存,然后用指针切开,减少了malloc的调用次数

  2. 使用嵌入式指针
    #

    使用union将储存的数据当做指针,当分配一大块内存需要将其切分时,只需要操作指针

    1584773403567

CPP 11
#

智能指针
#

线程安全:读安全,写不安全

shared_ptr
#

允许多个指针指向同一个对象

每个 shared_ptr 都有一个关联的引用计数,一个 shared_ptr 的计数器变为 0,它就会自动释放自己所管理的对象。

==不能将一个内置指针隐式转换为一个智能指针,必须直接初始化形式来初始化一个智能指针==

==不要使用 get 初始化另一个智能指针或为智能指针赋值==

shared_ptr<int> p1 = new int(1024);//错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024));//正确:使用了直接初始化形式
shared_ptr<int> p(new int(42));//引用计数为1
int *q = p.get();//正确:但使用q时要注意,不要让它管理的指针被释放
{
	//新程序块
	//未定义:两个独立的share_ptr指向相同的内存
	shared_ptr(q);
	
}//程序块结束,q被销毁,它指向的内存被释放
int foo = *p;//未定义,p指向的内存已经被释放了

reset 会更新引用计数,如果需要的话,会释放 p 的对象。reset 成员经常和 unique 一起使用,来控制多个 shared_ptr 共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:

if(!p.unique())
    p.reset(new string(*p));//我们不是唯一用户,分配新的拷贝
*p += newVal;//现在我们知道自己是唯一的用户,可以改变对象的值

即使程序块过早结束,智能指针也能确保在内存不再需要时将其释放

  • 不使用相同的内置指针值初始化(或 reset )多个智能指针
  • 不 delete get() 返回的指针
  • 不使用 get() 初始化或 reset 另一个智能指针
  • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
  • 如果你使用智能指针管理的资源不是 new 分配的内存,记住传递给它一个删除器

unique_ptr
#

独占所指向的对象,不支持普通的拷贝或赋值操作,可以 move

(可以拷贝或赋值一个将要被销毁的 unique_ptr。最常见的例子是从函数返回一个函数内局部变量 unique_ptr.)

可以通过调用 release 或 reset 将指针所有权从一个(非 const)unique_ptr 转移给另一个 unique

//将所有权从p1(指向string Stegosaurus)转移给p2
unique_ptr<string> p2(p1.release());//release将p1置为空
unique_ptr<string>p3(new string("Trex"));
//将所有权从p3转移到p2
p2.reset(p3.release());//reset释放了p2原来指向的内存

调用 release 会切断 unique_ptr 和它原来管理的的对象间的联系。release 返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。

weak_ptr
#

不具有普通指针的行为,没有重载 operator 和 ->*

引用计数有一个问题就是互相引用形成环(环形引用),这样两个指针指向的内存都无法释放。需要使用 weak_ptr 打破环形引用。

不控制所指向对象生存期,指向一个由 shared_ptr 管理的对象,不会改变 shared_ptr 的计数

由于对象可能不存在,不能使用 weak_ptr 直接访问对象,必须调用 lock,此函数检查 weak_ptr 指向的对象是否存在。如果存在,lock 返回一个指向共享对象的 shared_ptr,如果不存在,lock 将返回一个空指针

auto_ptr
#

较早版本,向后兼容,具有部分 unique_ptr 的属性

有拷贝语义,拷贝后源对象无效,可能引发问题

scoped_ptr
#

不能使用拷贝来构造新的对象也不能执行赋值操作,比 weak_ptr 更安全。

实现
#

shared_ptr

template <class T>
class ptr_base{
public:
    ptr_base(T* p) : ptr(p), count(1) {}
    ~ptr_base(){
        delete ptr;
    }
    T* ptr;
    size_t count;
};

template <class T>
class smartPtr{
public:
    explicit smartPtr(T* p = nullptr){
        if(p != nullptr){
            ptr = new ptr_base<T>(p);
        }
        else
            ptr = nullptr;
    }

    smartPtr(const smartPtr& rhs) : ptr(rhs.ptr){
        increase();
    }

    smartPtr& operator=(const smartPtr& rhs){
        if(this != &rhs){   // !!!!注意
            decrease();     // !!!! 注意
            ptr = rhs.ptr;
            increase();
        }
        return *this;
    }

    ~smartPtr(){
        decrease();
    }

    T* operator->() const {
        return ptr->ptr;
    }

    T& operator*() const {
        return *(ptr->ptr);
    }


private:
    ptr_base<T>* ptr;

    void increase(){
        ++(ptr->count);
    }

    void decrease(){
        if(--(ptr->count) == 0){
            delete ptr;
        }
    }
};

unique_ptr

template <class T>
class unique_ptr{
public:
    explicit unique_ptr(T* p = nullptr) : ptr(p) { }

    ~unique_ptr(){
        del();
    }

    unique_ptr(const unique_ptr& rhs) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;

    unique_ptr(unique_ptr&& rhs) noexcept : ptr(rhs.ptr){
        rhs.ptr = nullptr;
    }
    unique_ptr& operator=(unique_ptr&& rhs) noexcept{
        reset(rhs.release());
        return *this;
    }

    T* operator->() const noexcept { return ptr; }

    T& operator*() const noexcept { return *ptr; }

    T* release() noexcept{
        T* tmp = ptr;
        ptr = nullptr;
        return tmp;
    }

    void reset(T* p = nullptr) noexcept{
        del();
        ptr = p;
    }

private:
    T* ptr;
    void del(){
        if (ptr) {
            delete ptr;
            ptr = nullptr;
        }
    }
};

lambda 表达式
#

为何使用
#

  • 表达式定义在函数内部,定义离使用地点很近,修改方便
  • 函数指针可能会阻止内联,而函数符和 lambda 通常不会
  • lambda 可访问作用域内的任何动态变量,可按值访问、按引用访问等

constexpr
#

让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证 len_foo 在编译期就应该是一个常量表达式

从C++14 开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句,

委托构造
#

可以在同一个类中,一个构造函数可以调用另一个构造函数,从而达到简化代码的目的

面向对象包装器
#

std::function 函数的容器,可以更加方便的将函数、函数指针作为对象进行处理

std::bind 可绑定函数的参数

bind 对于不事先绑定的参数,通过 std::placeholders 传递的参数是通过引用传递的

bind 对于预先绑定的函数参数是通过值传递的

auto f1 = std::bind(fun,1,2,3); //表示绑定函数 fun 的第一,二,三个参数值为: 1 2 3
f1(); //print:1  2  3

auto f2 = std::bind(fun, placeholders::_1,placeholders::_2,3);
//表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别有调用 f2 的第一,二个参数指定
f2(1,2);//print:1  2  3

auto f3 = std::bind(fun, placeholders::_2,placeholders::_1,3);
//表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别有调用 f3 的第二,一个参数指定
//注意: f2  和  f3 的区别。
f3(1,2);//print:2  1  3

时间
#

新增 时间段、时钟、时间点

模板类ratio,用于表示比率

#include <ration>
template<std::intmax_t Num, std::intmax_t Denom = 1> //前者是分子,后者是分母
class ratio;

时间段 duration

表示一段时间间隔,记录时间长度,可表示几秒钟、几分钟等

template<class Rep, class Period = std::ratio<1>> class duration;
// 第一个模板参数Rep是一个数值类型,表示时钟个数
typedef duration <Rep, ratio<60,1>> minutes;
typedef duration <Rep, ratio<1,1>> seconds;
std::this_thread::sleep_for(std::chrono::seconds(3)); //休眠三秒
std::this_thread::sleep_for(std::chrono:: milliseconds (100)); //休眠100毫秒

时间点 time point

用来获取1970.1.1以来的秒数和当前的时间, 可以做一些时间的比较和算术运算

Clocks

表示当前的系统时钟,内部有time_point, duration, Rep, Period等信息,它主要用来获取当前时间,以及实现time_t和time_point的相互转换。

  • system_clock:从系统获取的时钟;
  • steady_clock:不能被修改的时钟;
  • high_resolution_clock:高精度时钟,实际上是system_clock或者steady_clock的别名。

可以通过now()来获取当前时间点

正则表达式
#

C++11 提供的正则表达式库操作 std::string 对象,模式 std::regex (本质是 std::basic_regex ) 进行初始化,通过 std::regex_match 进行匹配,从而产生 std::smatch(本质是 std::match_results 对象)。

  • std::regex 类,该类型需要一个代表正则表达式的字符串和一个文法选项作为输入

  • std::match_results 类,该类用来记录匹配的结果,这是一个模板类,该类的模板参数是一个迭代器类型,对于 std::string 来说我们定义了 smatch 作为 match_results<string::const_iterator> 作为别名。

  • std::sub_match 类,该类其实封装了两个迭代器,第一个代表开始部分,第二个代表结束部分,提供原字符串的某一个子串作为结果。

    smatch[0] 匹配整个字符串,然后依次匹配每个括号

匹配函数:

bool std::regex_match(...)      // 全文匹配,输入的字符串要和正则表达式全部匹配,才认为匹配成功
bool std::regex_search(...)     // 在输入的字符串中不断搜索符合正则表达式描述的子字符串,然后将第一个匹配到的子字符串返回
string std::regex_replace(...)  // 可以将匹配的子字符串替换为提供的字符串。

RAII
#

Resource Acquistion is Initialization 资源获取即初始化,即在构造函数中申请分配资源,在析构函数中释放资源,将资源的生命周期和对象的生命周期绑定。实现资源和状态的安全管理

智能指针是 RAII 的一个场景,使用智能指针,可实现自动的内存管理

并发
#

原子操作
#

使实例化一个原子类型,将一个原子类型读写操作从一组指令,最小化到单个 cpu 指令

可以通过 std::atomic<T>::is_lock_free 来检查该原子类型是否需支持原子操作

内存顺序
#

C++11 为原子操作定义了六种不同的内存顺序 std::memory_order 的选项,表达了四种多线程间的同步模型

  • **宽松模式:**在此模型下,单个线程内的原子操作都是顺序执行的,不允许指令重排,但不同线程间 原子操作的顺序是任意的

    通过 std::memory_relaxed 指定

  • **释放 / 消费模型:**开始限制进程间的操作顺序,如果某个线程需要修改某个值,但另一个线程会对该值的某次操作产生依赖,即后者依赖前者

  • 释放 / 获取模型

  • 顺序一致模型

noexcept
#

使用 noexcept 修饰过的函数如果抛出异常,编译器会使用 std::terminate() 来立即终止程序运行。

noexcept 还能够做操作符,用于操作一个表达式,当表达式无异常时,返回true,否则返回false。

编译
#

优化
#

  • o0:不做任何优化,这是默认的编译选项
  • o1:优化会消耗少量的编译时间,它主要对代码的分支,常量以及表达式等进行优化。
  • o2:会尝试更多的寄存器级的优化以及指令级的优化,它会在编译期间占用更多的内存和编译时间。
  • o3: 在O2的基础上进行更多的优化
  • os:相当于-O2.5。是使用了所有-O2的优化选项,但又不缩减代码尺寸的方法。

编译过程
#

  • 预处理

    预编译成 main.i 文件,去掉注释、进行宏替换、增加行号信息等

    用于将所有的 #include 头文件以及宏定义替换成真正的内容

    gcc -E -I./dic E 预处理 I 指定头文件目录

  • 编译

    将 main.i 文件经过语法分析、代码优化和汇总符号等步骤后,编译形成 main.S 的汇编文件,里面存放的都是汇编代码

    将经过预处理之后的程序转换成特定汇编代码

    -S

  • 汇编

    从 main.S 变成二进制可重定位目标文件 main.o

    将汇编代码转换成机器码

    -c

  • 链接

    将多个目标文件以及所需的库文件(.so 等)连接成最终的可执行文件

    1. 合并段,合并所有目标文件的段,并调整段偏移和段长度,相同属性的段合并,组织在一个界面上
    2. 合并符号表
    3. 符号解析,所有引用符号的地方都要找到符号定义的地方
    4. 分配内存地址
    5. 符号重定位,在编译过程中不分配地址,所有数据出现的地方都是零地址,符号重定位后,把分配地址回填到数据和函数调用出现的地方,对于数据是绝对地址,对函数调用时偏移量

二进制可重定位文件由 ELF header + 各种段组成。ELF header 占 64 个字节,里面存放着文件类型、支持的平台、程序入口点地址等信息

静态链接
#

  • 静态链接是指以目标文件为单位,将用到的库函数拷贝到可执行文件中.
  • 会让二进制文件更大,需要更多内存和磁盘存储.
  • linux下的.a文件和windows下的.lib文件

优点

静态库代码是在编译时链接的,因此最终的可执行文件在运行时不依赖于静态库,即没有额外的运行时加载成本。

缺点

  • 从静态库中拷贝代码,并将其用于编译个构建最终的可执行文件,对静态库的任何更改(升级),都需要重新编译主程序
  • 每个进程都有自己的代码和数据副本,内存占用量大

动态链接
#

动态库在程序编译时不会被链接到代码中,而是在程序运行时才会被载入。不同的应用程序如果调用相同的库,那么在内存中只有一份该共享库的实例

  • 运行时被载入,升级方便
  • 可实现进程之间的资源共享,成为共享库

**缺点:**将链接推迟到了程序运行时,每次执行程序都需要进行链接,性能会有一定损失

头文件重复
#

  • 使用 #ifndef

    依赖于宏名字不能冲突,可保证同一文件或内容完全相同的文件不会被同时包含,缺点是宏名可能会“撞车”

  • 使用 pragma once

    保证同一文件不会被编译多次,但是如果某头文件有多个拷贝,不能保证不被重复包含

Effective c++
#

Effective C++ Note
#

条款02:尽量以const,enum,inline替换#define
#

尽量以编译器替换预处理器

#define不被视为语言的一部分

  • 一定要使用宏时,记住为宏中的所有实参加上小括号
  • 对单纯常量,最好以const对象或enum替换#define
  • 对形似函数的宏,最好改用inline函数替换

条款32:确定pubilc继承塑模出is-a关系
#

public inheritance意味着is-a关系

has-a 条款38 is-implemented-in-terms-of 条款39

  • 当派生类中不应该有基类的某成员时

    1. 将该成员设为虚函数

    2. 在派生类中重新定义该函数,使它产生一个运行期错误,如error("Attempt to XXXX")

      如果调用了派生类中的该函数,则在运行期才能检测出来

    3. 基类中不声明该函数

条款33:避免遮掩继承而来的名称
#

内层作用域的名称会遮掩外围作用域的名称

class Base{
    void mf();
    void mf(int);
}
class Derived:public Base{
    using Base::mf; //让Base类内名为mf的所有东西在Derived作用域没都可见
}

条款34:区分接口继承和实现继承
#

函数接口继承(function interfaces) 函数实现继承(function implementation)

  • 声明一个pure virtual函数的目的是 让派生类之继承函数接口

    1. pure virtual函数必须被任何派生类重新声明
    2. 在抽象类中通常都没有定义
      • 可以为pure virtual函数提供定义,但是在调用时必须明确指出其类名称,如ps->Base::vir()
  • 声明impure virtual函数的目的是 让派生类继承该函数的接口和缺省实现(即默认实现,缺省即缺省派生类的重新定义)

    派生类中忘记重新定义该函数时将使用基类中的缺省实现,可能会产生危险

    1. 切断virtual函数接口 和 其缺省实现之间的连接

      另外声明定义一个函数A(protected)代替缺省实现(可用inline),当需要使用缺省实现时直接在派生类函数定义中加入A

    2. 将函数声明为pure virtual函数,并为其添加定义,派生类需要缺省实现时,在派生类定义中调用基类该虚函数

      但不能让缺省实现和外部函数享有不同的保护级别

    3. 声明non-virtual函数的目的是 另派生类继承函数的接口和一份强制性实现

条款35:考虑virtual函数以外的其他选择
#

  • Template Method模式

    借由Non-Virtual Interface(NVI)手法实现

    通过public non-virtual成员函数间接调用private virtual函数

  • 借由函数指针实现Strategy模式

    将函数指针作为原函数的参数,即用函数指针替换virtual函数

    • 优点:每个对象可以拥有自己的不同函数
    • 缺点:外部函数不能直接访问非公有成员,可能需要降低类的封装性
  • 借由tr1::function完成Strategy模式

    p173

  • 古典Strategy模式

    另外声明定义一个类,需要的话可以继续派生

Related

MySQL 笔记
·25 mins
操作系统笔记
·45 mins
Matlab笔记
·8 mins
Redis 笔记
·9 mins
算法笔记
·5 mins
计算机网络笔记
·37 mins