跳转至

C++ 系统知识

12299 个字 8 行代码 预计阅读时间 41 分钟

Abstract

这里归档一些关于 C++ 的一些零散知识,包括面向对象等

代码到可执行程序

  1. 预处理:条件编译,头文件包含,宏替换处理,生成 .i 文件
  2. 编译:将预处理后的文件转化为汇编语言,生成 .s 文件
  3. 汇编:汇编变为目标代码(机器代码,生成 .o 文件
  4. 链接:连接目标代码,生成可执行程序

静态库和动态库

静态库和动态库最本质的区别就是:该库是否被编译进目标(程序)内部

  • 静态库 一般扩展名为.a.lib,在编译的时候会直接整合到目标程序中,所以利用静态函数库编译成的文件比较大,最大的优点就是编译成功的可执行文件可以独立运行,而不再需要向外部要求读取函数库的内容;但是从升级难易度来看明显没有优势,如果函数库更新,需要重新编译

  • 动态库 动态函数库的扩展名一般为.so.dll,这类函数库通常名为libxxx.so或xxx.dll。 与静态函数库被整个捕捉到程序中不同,动态函数库在编译的时候,在程序里只有一个“指向”的位置而已,也就是说当可执行文件需要使用函数库的机制时,程序才会去读取函数库来使用;也就是说可执行文件无法单独运行。这样从产品功能升级角度方便升级,只需要替换对应动态库即可,不必重新编译整个可执行文件。

dll 加载

隐式加载(静态调用)

在程序编译的时候就将 dll 编译到可执行文件中,这种加载方式调用方便,程序发布的时候可以不用带着 dll 缺点是程序会很大

显示加载(动态调用)

在程序运行过程中,需要用到 dll 里的函数时,再动态加载 dll 到内存中 这种加载方式因为是在程序运行后再加载到,所以可以让程序启动更快,而且dll的维护更容易,使得程序如果需要更新,很多时候直接更新dll,而不用重新安装程序 这种加载方式,函数调用稍微复杂一点

C++ 内存管理

内存结构

C++ 内存分区:栈、堆、自由存储区、全局 / 静态存储区、常量区

  • 栈:存放函数的局部变量,由编译器自动分配和释放
  • 堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收
  • 自由存储区:和堆十分相似,存放由 new 分配的内存块,由 delete 释放内存
  • 全局区 / 静态区:存放全局变量和静态变量
  • 常量存储区:存放的是常量,不允许修改

事实上,我在网上看的很多博客,划分自由存储区与堆的分界线就是 new/delete malloc/free。然而,尽管 C++ 标准没有要求,但很多编译器的 new/delete 都是以 malloc/free 为基础来实现的。那么请问:借以 malloc 实现的 new,所申请的内存是在堆上还是在自由存储区上?

从技术上来说,堆(heap)是 C 语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用 malloc() 时就会从中分配,稍后调用 free 可把内存交还。而自由存储是 C++ 中通过 new delete 动态分配和释放对象的抽象概念,通过 new 来申请的内存区域可称为自由存储区。基本上,所有的 C++ 编译器默认使用堆来实现自由存储,也即是缺省的全局运算符 new delete 也许会按照 malloc free 的方式来被实现,这时藉由 new 运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。我们所需要记住的就是:

堆是操作系统维护的一块内存,而自由存储是 C++ 中通过 new delete 动态分配和释放对象的抽象概念。堆与自由存储区并不等价。

内存泄漏

内存泄漏是指应用程序分配某段内存后,失去了对该段内存的控制,因而造成了内存的浪费。 1. 在类的构造函数和析构函数中没有匹配的调用new和delete函数 2. 没有正确地清除嵌套的对象指针 3. 在释放对象数组时在中没有使用 delete[] 4. 指向对象的指针数组不等同于对象数组 1. 缺少拷贝构造函数重载复制运算符:隐式的指针复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。 5. 没有将基类的析构函数定义为虚函数。

避免内存泄漏

RAII 资源获取即初始化,是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。智能指针即 RAII 最具代表的实现

C++ 特性

C++ 的特性(C++11 及以上)

  • 封装、继承、多态
  • 需要在不同的平台上进行编译
  • 编译后的程序可以在操作系统上直接运行
  • 可以面向过程和面向对象两种设计方式
  • 可以直接操作本地的系统库
  • 可以直接使用操作系统提供的接口
  • 编译后仅对变量的类型进行了存储,不可以进行类似反射的操作
  • 支持无符号整型
  • 变量类型的字节长度受操作系统影响
  • 支持指针、引用、值传递
  • 没有默认提供的 GC 系统
  • 由程序员负责管理变量所储存的位置
  • 严格的 RAII
  • 支持重写、重载,包括运算符的重载
  • 多重继承
  • 支持预编译,编译宏定义
  • 支持函数指针,函数对象,lambda 表达式

C++11 新增的特性

  • foreach
  • auto自动类型推断
  • lambda匿名函数
  • 后置返回类型
  • override关键字
  • 禁止重写final
  • 常量表达式constexpr
  • nullptr代表原来的 NULL,而原来的 NULL 更多表示为 0
    • 当存在void a(int x);void a(char *x);时,若使用a(NULL)则会调用前者,这与通常的理解不同,而使用a*nullptr)则会明确的调用后者
  • 元组tuple,可以使用get<>()取出其中一个值,或者使用tie()装包或解包

struct class 的区别

  • struct 默认使用 public,而 class 默认使用 private
  • struct 可以直接使用{}进行初始化,而 class 则需要所有成员变量都是 public 的时候才可以使用

堆和栈的区别

  • 申请方式:栈是系统自动分配,堆是程序员主动申请
  • 申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表,在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上
  • 栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的
  • 申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片
  • 存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制

操作系统角度

  • 堆是操作系统为进程所分配的空间,在 CC++ 语言中用来存放全局变量。由程序员管理,主动申请以及释放的空间,可能会出现内存泄漏,在进程结束后,由操作系统回收
  • 栈是由编译器进行管理由编译器进行申请,释放的空间,通常比堆要小很多,在 CC++ 语言中,2 当调用一个函数时会创建一个栈,当函数结束时则会回收栈的空间。

数据结构角度

  • 堆是一颗完全二叉树,常见的有最大堆和最小堆,以最大堆为例,其满足二叉树中的任意一个节点的孩子节点都比此节点小。通常用来实现优先队列的效果,插入和删除的复杂度均为 \(O(log N)\)
  • 栈是一种线性数据结构,满足先进后出的特点,即最先进入的数据最后离开,常见于 DFS 中。也可以通过单调栈的方式求解一些问题。插入和删除的复杂度均为 \(O(1)\)

C++ 中四种 cast 转换

  • const_cast:对于未定义 const 版本的成员函数,通常需要使用 const_cast 来去除 const 引用对象的 const,完成函数调用。另一种使用方式,结合 static_cast,可以在非 const 版本的成员函数内添加 const,调用完 const 版本的成员函数后,再使用 const_cast 去除 const 限定。
  • static_cast:完成基础数据类型;同一个继承体系中类型的转换;任意类型与空指针类型 void* 之间的转换。
  • reinterpret_cast:无条件转换(什么都可以转int 可以转指针,可能会出问题,尽量少用。
  • dynamic_cast:用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转换(子类和基类。只能转指针或引用。向下转化时,如果是非法的对于指针返回 NULL,对于引用抛异常。它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。

static

静态局部变量:变量属于函数本身,仅受函数的控制。保存在全局数据区,而不是在栈中,每次的值保持到下一次调用,直到下次赋新值。 静态全局变量:定义在函数体外,用于修饰全局变量,表示该变量只在本文件可见,不能被其他文件所用(全局变量可以) 静态函数:静态函数不能被其他文件所用,其他文件中可以定义相同名字的函数,不会发生冲突 静态数据成员:静态数据成员的生存期大雨 class 的实例(静态数据成员是每个 class 有一份,普通数据成员是每个 instance 有一份) 1. 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数; 2. 非静态成员函数可以任意地访问静态成员函数和静态数据成员; 3. 静态成员函数不能访问非静态成员函数和非静态数据成员; 4. 调用静态成员函数,可以用成员访问操作符.->为一个类的对象或指向类对象的指针调用静态成员函数,也可以用类名::函数名调用(因为它本来就是属于类的,用类名调用很正常)

static 变量的特点

  • 该变量在全局数据区分配内存
  • 未经初始化的静态全局变量会被程序自动初始化为 0(自动变量的值是随机的,除非他被显式初始化)
  • 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的

const

修饰普通类型的变量

修饰指针:顶层 const(指针本身是个常量)和底层 const(指针指向对象是一个常量)

修饰函数参数: 1. 函数参数为值传递:值传递(pass-by-value)是传递一份参数的拷贝给函数,因此不论函数体代码如何运行,也只会修改拷贝而无法修改原是对象,这种情况不需要将参数声明为 const。 2. 函数参数为指针:指针传递(pass-by-pointer)只会进行浅拷贝,拷贝一份指针给函数,而不会拷贝一份原始对象。因此,给指针参数加上顶层 const 可以防止指针指向被篡改,加上底层 const 可以防止指向对象被篡改。 3. 函数参数为引用:引用传递(pass-by-reference)又一个很重要的作用,由于引用就是对象的一个别名,因此不需要拷贝对象,减小了开销。这同时也导致可以通过修改引用直接修改原是对象(毕竟引用和原始对象其实是同一个东西),因此,大多数时候,推荐函数参数设置为pass-by-reference-to-const。给引用加上底层 const,既可以减小拷贝开销,又可以防止修改底层所用的对象。

修饰函数返回值:令函数返回一个常量,可以有效防止因用户错误造成的意外,比如“=”。

const 成员函数:const 成员函数不可修改类对象的内容(指针类型的数据成员只能保证不修改该指针指向。原理是 const 成员函数的 this 指针式底层 const 指针,不能同于改变其所指对象的值。

当成员函数 const non-const 版本同时存在时,const object 只能调用 const 版本,non-const object 智能调用 non-const 版本。

extern "C"

extern "C" 的主要作用就是为零能够正确实现C++ 代码调用其他 C 语言代码。 加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般指包括函数名。

C++ 出现以前,很多代码都是 C 语言写的,而且很底层的库也是 C 语言写的,为了更好的支持原来的 C 代码和已经写好的 C 语言库,需要在 C++ 中尽可能支持 C,而 extern "C" 就是其中一个策略。

  1. C++ 代码调用 C 语言代码
  2. C++ 的头文件中使用
  3. 在多个协同开发时,可能有的人比较擅长 C 语言,而有的人擅长 C++

inline

类内定义成员函数默认为 inline

优点

  • inline 定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换(像宏一样展开,没有了调用的开销,效率也很高。
  • 内联函数也是一个真正的函数,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性(宏替换不会检查类型,安全隐患较大)
  • inline 函数可以作为一个类的成员函数,与类的普通成员函数作用相同,可以放为一个类的私有成员和保护成员。内联函数可以用于替换一般的宏定义,最重要的应用在于类的存取函数的定义上面。

缺点

  • 内联函数具有一定的局限性,内联函数的函数体一般来说不能太大,如果内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数(换句话说,你如果使用内联函数,只不过是向编译器提出一个请求,编译器可以拒绝你的请求)这样,内联函数就和普通函数执行效率一样了。
  • inline 说明对编译器来说只是一种建议,编译器可以选择忽略这个建议。比如,你讲一个长达 1000 多行的函数指定为 inline,编译器就会忽略这个 inline,将这个函数还原成普通函数,因此并不是说把一个函数定义为 inline 函数就一定会被编译器识别为内联函数,具体取决于编译器的实现和函数体的大小。

和宏定义的区别

宏是由预处理器对宏进行替代,而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销,你可以像调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。内联函数与带参数的宏定义进行比较,他们的代码效率是一样,但是内联函数要优于宏定义,因为内联函数遵循的类型和作用域规则,它与一般函数更相近,在一些编译器中,一旦关联上内联扩展,将与一般函数一样进行调用,比较方便。

指针和引用

  • 指针有着自己的内存空间,是一个变量类型,指针的大小不会像其他变量一样变化,而引用本质上是“变量的别名”,没有占用内存空间
  • 指针在声明周期内随时可能会 NULL,所以使用时一定要做检查,防止出现空指针、野指针的情况
  • 指针声明时可以暂时不初始化,引用声明时必须初始化
  • 使用sizeof可以求得出在 32 位操作系统下,指针的大小为 4 个字节,而引用则为原对象的大小
  • 指针可以初始化为任意正整数值,而引用必须初始化为一个已经存在的变量
  • 参数传递时,指针需要先进行指针转为引用然后再使用,而引用可以直接操作原对象
  • 指针可以有const属性,而引用没有
  • 指针可以重新赋值,而引用不可以更改
  • 指针可以进行多级指针,而引用只有一级
  • 指针和引用进行 ++(自增)操作的逻辑和结果都不同
  • 当需要返回动态内存分配的对象时,需要使用指针而不是引用,因为引用可能会产生内存泄漏

C++ 四个智能指针

将基本类型指针封装为类对象指针,并在析构函数里编写delete语句删除指针指向的内存空间。
  • 解决什么问题?

    1. 空指针和野指针的问题
    2. 对象重复释放的问题
    3. 内存泄漏的问题
  • auto_ptr(已弃用)

    • 采用所有权模式,任何一个new的对象只能由一个auto_ptr来指向,进行赋值操作会使得原来的指针丢失指向的对象
  • unique_ptr
    • auto_ptr相同,但是进行赋值操作时,会直接报错,而auto_ptr不会
  • shared_ptr
    • 共享指针,允许多个指针指向此对象,同时当所有指向此对象的指针都被析构后,此对象将会被删除
  • weak_ptr
    • 弱共享指针,允许指向其他的shared_ptr对象,此指针不会影响shared_ptr的析构行为,通常用来避免相互指向问题

野指针

指向被释放的或者访问受限内存的指针。

造成野指针的原因: - 指针变量没有被初始化(如果值不定,可以初始化为NULL) - 指针被free或者delete后,没有置为NULL,free和delete只是把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是:“垃圾”内存。释放后的指针应该被置为NULL。 - 指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针。

new malloc

在使用的时候 new,delete 搭配使用,malloc free 搭配使用。

  • 属性:malloc/free 是库函数,需要头文件的支持;new/delete 是关键字,需要编译器的支持
  • 参数:new 申请空间时,无需指定分配空间的大小,编译器会根据类型自行计算;malloc 在申请空间时,需要确定所申请空间的大小
  • 返回值:new 申请空间时,返回的类型是对象的指针类型,无需强制类型转换,符合类型安全的操作符;malloc 申请空间时,返回的是 void* 类型,需要进行强制类型的转换,转换为对象类型的指针
  • 分配失败:new 分配失败时,会抛出 bad_alloc 异常,malloc 分配失败时返回空指针
  • 重载:new/delete 支持重载,malloc/free 不能进行重载
  • 自定义类型实现:new 首先调用 operator new 函数申请空间(底层通过 malloc 实现,然后调用构造函数进行初始化,最后返回自定义类型的指针;delete 首先调用析构函数,然后调用 operator delete 释放空间(底层通过 free 实现malloc/free 无法进行自定义类型的对象的构造和析构
  • 内存区域:new 操作符从自由存储区上为对象动态分配内存,而 malloc 函数从堆上动态分配内存(自由存储区不等于堆)
特征 new/delete malloc/free
分配内存的位置 自由存储区
分配成功 返回完整类型指针 返回 void*
分配失败 默认抛出异常 返回 NULL
分配内存的大小 由编译器根据类型计算得出 必须显式指定字节数
处理数组 有处理数组的 new 版本和 new[] 需要用户计算数组的大小后进行内存分配
以分配内存的扩充 无法直观处理 使用 realloc 简单完成
是否相互调用 可以,看具体的 operator new/delete 实现 不可调用 new
分配内存时内存不足 客户能够制定处理函数或重新制定分配器 无法通过用户代码进行处理
函数重载 允许 不允许
构造函数与析构函数 调用 不调用

构造函数

构造函数的特征

  • 名字和类名相同
  • 没有返回值
  • 生成类的自动执行,不需要调用

为什么构造函数不可以是虚函数

因为虚函数表指针是在构造函数期间创建的,没有虚函数表就没有办法调用虚函数

析构函数

析构函数的作用

  • 如果一个类中有指针,且这个指针指向了一段由此类的实例请求分配的空间,那么需要由析构函数来实现对这块区域的释放,否则会造成内存泄漏

C++ 为什么习惯把析构函数定义为虚函数

  • 党这个类需要作为父类派生的时候,如果程序得到的是此父类的指针,那么此时就无法析构子类,出现内存泄漏

C++ 为什么默认的析构函数不是虚函数

  • 虚函数需要额外的虚函数表和虚函数表指针,对于不会派生的类而言,浪费空间

重载和重写(覆盖)

  • 对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间,子类和父类之间
  • 重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰
  • virtual 关键字:重写的函数基类中必须有 virtual 关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有

C++ 中锁的类型

  • 互斥锁:对于同一个变量只允许一个线程进行读写,若不满足时则会进入阻塞,并且 CPU 不会进入忙等
  • 条件锁:当满足某个条件时,再唤醒此线程,否则一直阻塞状态
  • 自旋锁:不断的检查锁是否满足条件,不释放 CPU,比较耗费 CPU
  • 读写锁:允许有读锁的时候再加读锁,但是有写锁时不再能加任何锁
  • 递归锁:允许同一个线程对同一个锁进行多次加锁

虚函数

虚函数

  • 虚函数由virtual标记
  • 普通的虚函数仍然需要进行实现,所有继承此类的派生类可以重新实现此函数也可以不实现

纯虚函数

  • 纯虚函数在普通的虚函数后,加上=0
  • 当一个类拥有纯虚函数后,则此类变成抽象类, 不可以进行实例化
  • 纯虚函数不需要实现,且所有继承此类的派生类必须实现此函数,否则派生类也是抽象类,不可以实例化

虚函数的实现原理

  • 在类中保存一张虚函数表,表内保存了函数所在的代码段
  • 当其他类继承此类时,复制一份此虚函数表。当其中的虚函数进行实现后,将虚函数表中此函数的指针所指向新的函数的地址
  • 定义类的实例的时候,在类的开头保存了一个指向此虚函数表的指针,当需要调用此函数的时候,通过此指针找到对应的函数地址

静态函数和虚函数的区别

静态函数在编译时就确定了运行的时机,而虚函数则是在运行的过程中动态的得知虚函数地址

strcpy memcpy 的区别

  • 复制的内容不同。strcpy 只能复制字符串,而 memcpy 可以复制任意内容,例如字符数组、整型、结构体、类等。企业中使用 memcpy 很平常,因为需要拷贝大量的结构体参数。memcpy 通常与 memset 函数配合使用。
  • 复制的方法不同。strcpy 不需要指定长度,他遇到被复制字符的串结束符 "\0" 才结束,所以容易溢出。memcpy 则是根据其第三个参数决定复制的长度。因此 strcpy 会复制字符串的结束符 "\0",而 memcpy 则不会复制。

队列和堆栈的模拟

用两个堆栈模拟队列

  • 将两个堆栈命名为 A、B
  • B 堆栈为空,则将 A 堆栈的所有值都推入 B
  • 若需要推入,则推入到 A
  • 若需要推出,则从 B 中推出

用两个队列模拟堆栈

  • 将两个队列命名为 A、B
  • 若需要推入,则推入到 A
  • 若需要弹出,则将 A 中的值除了最后一个,其他都推入到 B 中,且仅留下一个值,然后弹出这个值,并将 AB 队列命名为 AB 队列

右值引用

  • 如何确定一个值是左值还是右值
    • 提供了地址的为左值,左值可以没有值,但是一定有地址
    • 提供了值的为右值,右值可以没有地址,但是一定有值
  • 右值引用的功能
    • 移动语句
    • 完美转发

STL

vector

扩容规则

当空间不足的时候,vector 会扩容至当前空间的 2GCC 下)/1.5(MSVC)

为什么这样扩容

以两倍空间为例,当扩容次数为 30 次左右时,vector 的空间达到 1e9,而通常每次扩容,都会需要在堆上重新分配空间,需要重新移动整个数组到新的空间。由此,可以得出重新分配空间的次数越少越好,同时也要节约内存的占用,因为按照此增长,其内存的重复的分配次数始终在常数范围内,所以采用上述的扩容方式。

MSVC 下的 1.5 倍空间相对于 GCC 下的 2 倍有什么好处和坏处

  • 好处:因为 2 倍空间下,任意一个空间都大于之前所有分配过的空间之和,这就意味着每次进行扩容的时候都需要分配一个新的空间。而在 1.5 倍下,可以重复使用之前的空间,1.5 倍相对于会节约内存
  • 坏处:1.5 倍下的重新分配次数更多,也就意味着需要更多的重新分配空间和重新移动的次数,更加浪费时间

clear 的复杂度

  • 复杂度与已有的元素数量成线性,因为每个元素都需要析构
  • clear 后,并不会改变 vector 的容量上限,只会更新 vector 内的 size 大小

unordered_map map

map 内部实现了一个红黑树(所有元素都是有序的unordered_map 内部实现了一个哈希表(元素的排列是无序的)

map

  • 优点:有序性,这是 map 结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作;内部实现了一个红黑树使得 map 的很多操作在 \(logn\) 的时间复杂度下就可以实现,因此效率非常高
  • 缺点:空间占用率高,因为 map 内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红 / 黑性质,使得每一个节点都占用大量的空间
  • 适用:对于那些有顺序要求的问题,用 map 会更高效一些

unordered_map

  • 优点:因为内部实现了哈希表,因此其查找速度非常快
  • 缺点:哈希表的建立比较耗费时间
  • 适用:对于查找问题unordered_map 会更加高效一些

面向对象程序设计的优点?

封装、继承、多态是面向对象的三大特性 , 这些特性使得面向对象可以设计出低耦合的系统 , 从而提高了系统的灵活性 , 使系统更易维护 , 功能更易复用、拓展 , 但是其性能比面向过程要低。

面向对象编程,即 OOP,是一种编程范式,满足面向对象编程的语言,一般会提供类、封装、继承等语法和概念来辅助我们进行面向对象编程。

类型被设计为将数据和行为捆绑在一起的一种东西,数据和行为被称之为类型的成员。我们可以创建类型的实例,不同的实例包含不同的数据,从而其表现出来的行为也会不同,尽管其代码是一样的。

封装使得类的成员得以有选择性的暴露,一些成员只在类型的内部使用,被称之为私有的(private,一些成员可以被派生类型使用,称之为受保护的(protected,一些成员可以被任何东西使用,称之为公开的(public。而某些语言还提供了内部的(internal)这样的访问修饰符来标识一些只能被同一个程序集或者包使用的成员。

继承可以从一个现有类型派生出新的类型来,派生类继承了基类的所有成员,也可以新增只属于自己的成员。在任何情况下,派生类类型的实例可以被当做基类类型的实例来使用。

虚方法为派生类修改基类的行为提供了一个途径,通过重写(override)虚方法可以修改基类某些方法的行为。当派生类实例被当做基类实例来使用时,这一行为的区别将会被体现出来,这种在运行时不同类型的实例在同样的代码中呈现出完全不同行为的现象被称之为多态。

面向对象编程最初是为了解决 GUI 程序设计问题所提出的,后来面向对象编程被发现也比较适合用于许多特定领域的开发。面向对象编程是目前运用最为广泛的一种编程范式,从而也产生了非常多的解决代码复用的技巧,其中相当一部分技巧在程序中反复出现而被提炼为设计模式。

static 有什么作用?

  1. 隐藏。当我们同时编译多个文件的时候,所有未加 static 的全局变量和函数都具有全局可见性。所以使用 static 在不同的文件中定义同名变量和函数,不需要担心命名冲突。
  2. 保持变量内容的持久。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化,共有两种变量存在静态存储区,全局变量和 static
  3. 默认初始化为 0。全局变量也具有这个属性,因为全局变量也存储在静态存储区,在静态存储区内,所有字节的默认值时 0x00,这一特点可以减少工作量。

const 有什么用?

不要一听到 const 就说是常量,应该说 const 修饰的内容不可改变。定义常量只是一种使用方式,还有 const 数据成员,参数,返回值,成员函数等,被 const 修饰的东西都受到了强制保护,可以预防意外变动,提高程序健壮性。

C C++ 各自是如何定义常量的?有什么不同?

C是使用宏#define定义,C++使用更好的const定义。
  1. const 是有数据类型的,而 define 没有,编译器可以对前者进行静态类型安全检查,对后者仅仅是字符替换,没有检查,会产生错误(边际效应
  2. 有些编译器可以对 const 进行调试,而 define 不行。

既然 C++ 有更好的 const 为啥还要 define

const 无法替代宏作为卫哨来防止文件的重复包含。

#include # include"a.h" 有什么区别?

前者,编译器从标准库路径开始搜索,后者,编译器从用户的工作路径开始搜索。

C++ 什么是多态性?如何体现的?

多态性是面向对象编程继封装和继承之后的第三个基本特征。 他在运行时出现的多态性通过派生类和虚函数实现,基类和派生类中使用同样的函数名,完成不同的操作具体实现。 多态性提高了代码的组织性和可读性,虚函数则根据类型的不同来进行不同的隔离。

什么是动态特性?

在绝大多数情况下,程序的功能是在编译的时候就确定下来,我们称之为静态特性。反之,程序的功能是在运行的时候确定,称之为动态特性。C++中,虚函数,抽象基类,动态绑定和多态构成了出色的动态特性。

什么是封装?C++ 中是如何实现的?

封装来源于信息隐藏的设计理念,是通过特性和行为的组合来创建新数据类型让接口和某个实现相隔离。 C++是通过类来实现的,封装为了使类中成员选择性的暴露,有三个关键字(balabalabala)

什么是 RTTI

RTT 是指运行时类型识别(Run-time type identification)在只有一个指向基类的指针或引用时确定一个对象的准确类型。

什么是深浅拷贝?

深浅拷贝关键在于有没有拷贝分配给成员的资源,例如给指针变量分配内存。浅拷贝只是给成员简单赋值,而深拷贝不仅赋值,还分配资源。 类中默认的拷贝构造函数和赋值构造函数都是浅拷贝,当类的成员变量中出现指针变量时,最好使用深拷贝,避免内存空间多次释放的问题出现。

虚函数表指针为什么放在开头?

位置:虚函数表指针是虚函数表所在位置的地址。虚函数表指针属于对象实例。因而通过 new 出来的对象的虚函数表指针位于堆,声名对象的虚函数表指针位于栈。

虚函数表的存在是编译器依赖的 ( 但所有编译器都是如此 )vptr 存储在对象的开头 . 原因是它提供了一个统一的位置 . 考虑一个类层次结构 :

struct base {
   T data;
   virtual void f();
};
struct derived : base {
   T1 data;
   virtual void g();
};

如果 vptr 存储在对象的末尾,那么对于完整类型 base 的对象,它将位于 sizeof(T) 字节之后 . 现在,当你有一个完整类型的对象 derived 时,base 子对象的布局必须与完整 base 对象的布局兼容,因此 vptr 仍然必须是 sizeof(T) 字节在对象内部,它将位于 derived 对象中间的某个位置 (sizeof(T) 从开始,sizeof(T1) 在结束之前 ). 所以它将不再位于对象的 end .

此外,给定一个 this 指针,虚拟调用需要通过 vtable 进行间接调用,这基本上是取消对 vptr 的引用,添加一个偏移量并跳转到存储在那里的内存位置 . 如果 vptr 存储在对象的末尾,则对于每个虚拟调用,在取消引用 vptr 之前都会对 this 进行额外的添加 .

STL 源码中 hash 的表现

hashtable 是采用开链法来完成的(vector + list)

底层键值序列采用 vector 实现,vector 的大小取的是质数,且相邻质数的大小约为 2 倍关系,当创建 hashtable 时,会自动选取一个接近所创建大小的质数作为当前 hashtable 的大小; 对应键的值序列采用单向 list 实现; 当 hashtable 的键 vector 的大小重新分配的时候,原键的值 list 也会重新分配,因为 vector 重建了相当于键增加了,那么原来的值对应的键可能就不同于原来分配的键,这样就需要重新确定值的键。

重载如何实现

C++ 函数重载底层实现原理是 C++ 利用倾轧技术,来改名函数名,区分参数不同的同名函数。

C++ 中,这三个函数如果在主函数中被调用选择哪一个,由编译器自身决定。

源文件通过编译后,将相同函数名,按照一定的格式,改变成可以区分的,去除了函数在调用时的二义性,从而实现函数的重载。

结合 extern "C".

动态绑定和静态绑定区别

C++ 在面向对象编程中,存在着静态绑定和动态绑定的定义,本节即是主要讲述这两点区分。
我是在一个类的继承体系中分析的,因此下面所说的对象一般就是指一个类的实例。
首先我们需要明确几个名词定义:

  • 静态类型:对象在声明时采用的类型,在编译期既已确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;
  1. 静态绑定发生在编译期,动态绑定发生在运行期;
  2. 对象的动态类型可以更改,但是静态类型无法更改;
  3. 要想实现动态,必须使用动态绑定;
  4. 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;

注意:绝对不要重新定义一个继承而来的 virtual 函数的缺省参数值,因为缺省参数值都是静态绑定(为了执行效率,而 virtual 函数却是动态绑定。

C++ 的几种构造函数

C++ 中的构造函数可以分为 4 类: (1)默认构造函数。以Student类为例,默认构造函数的原型为 Student();//没有参数 (2)初始化构造函数 Student(int num,int age);//有参数 (3)复制(拷贝)构造函数 Student(Student&);//形参是本类对象的引用 (4)转换构造函数 Student(int r) ;//形参是其他类型变量,且只有一个形参

  • 默认构造函数和初始化构造函数在定义类的对象的时候,完成对象的初始化工作。
  • 复制构造函数用于复制本类的对象。
  • 转换构造函数用于将其他类型的变量,隐式转换为本类对象。

什么时候调用拷贝构造函数

当以拷贝的方式初始化对象时会调用拷贝构造函数,这里需要注意两个关键点,分别是以拷贝的方式初始化对象

初始化对象

初始化对象是指,为对象分配内存后第一次向内存中填充数据,这个过程会调用构造函数,对象被创建后必须立即初始化。也就是说只要创建对象就会调用构造函数。

初始化和赋值的区别

初始化和赋值都是将数据写入内存中,从表面看,初始化在很多时候都是以复制的方式来实现的,很容易引起混淆。在定义的同时进行复制叫做初始化,定义完成以后再赋值 ( 不管定义的时候有没有赋值 ) 就叫做赋值。初始化只能由一次,赋值可以由很多次。

初始化对象时会调用构造函数,不同的初始化方式会调用不同的构造函数:

  • 如果用传递进来的实参初始化对象,那么会调用普通的构造函数。
  • 如果用现有对象的数据来初始化对象,就会调用拷贝构造函数,这就是以拷贝的方式初始化对象。

以拷贝的方式来初始化对象的几种情况:

  1. 将其它对象作为实参。
  2. 在创建对象的同时赋值。
  3. 函数的形参为类类型。
  4. 函数返回值为类类型 ( 与编译器有关不绝对 )

C++ 的容器比较,为什么 vector 可以随机访问

stl 容器包含顺序容器和关联容器。顺序容器主要有 vector,list,deque,关联容器主要是 pair、set、map、multiset multimap,所以总共算是 7 种。
所谓随机访问,我的理解是按照数组的方式在内存中顺序存放,只需要根据首地址和相应下标就能寻址到相应的元素。
所以逐个分析如下:
vector的实现原理是数组,所以支持随机访问。
list的实现原理是双向链表,所以不支持。
deque的实现原理是类似数组的双端队列,支持随机访问。
pair是个二元组,一共就两个值,谈不上随机访问。
set、multiset、map、multimap的实现原理是红黑树,不支持随机访问。

所以在上述七种容器中只有 vector deque 两种是支持随机访问的。

vector 底层数组大小分配

vector 的大小有两个变量,一个 size,一个 capacitysize vector 实际容量,capacity vector 最大容量,当 size=capacity 时,vector 需要考虑扩容,vector 会扩容至当前空间的 2GCC 下)/1.5(MSVC)

为什么这样扩容

以两倍空间为例,当扩容次数为 30 次左右时,vector 的空间达到 1e9,而通常每次扩容,都会需要在堆上重新分配空间,需要重新移动整个数组到新的空间。由此,可以得出重新分配空间的次数越少越好,同时也要节约内存的占用,因为按照此增长,其内存的重复的分配次数始终在常数范围内,所以采用上述的扩容方式。

MSVC 下的 1.5 倍空间相对于 GCC 下的 2 倍有什么好处和坏处

  • 好处:因为 2 倍空间下,任意一个空间都大于之前所有分配过的空间之和,这就意味着每次进行扩容的时候都需要分配一个新的空间。而在 1.5 倍下,可以重复使用之前的空间,1.5 倍相对于会节约内存
  • 坏处:1.5 倍下的重新分配次数更多,也就意味着需要更多的重新分配空间和重新移动的次数,更加浪费时间

C++、go、python 的区别

包管理

C++ 这种与平台和编译器相关,很难有语言级别的包管理器, 一般是各个开发环境持有各自的复用管理,比如 linux apt-get, deb, yum,各种 lib header dev 包可以通过其安装的。一般都需要 CMake 或者 Makefile 进行工程管理,所以有相应的学习成本。

Python 有丰富的包管理器,比如,distutils,setuptools, 还有较为流行的 pippip 可以利用 requirments.txt 来安装依赖的库文件。

GO 早期的包管理也是为人诟病,不过 1.11 版本推出 modules 机制,让 go 语言包管理变得更方便简单,还支持 GoProxy,replace,SubCommand 等高级特性。

语法

在语法方面,C++ GO 语言的语法较为相似,但是 GO 语言语法简洁,不需要程序员管理内存,有丰富的 API 可调用,也提供了切片,map 等灵活的数据类型,Python 作为解释型语言在语法方面以优雅著称,更为简单。Python 社区提供了大量的第三方模块,使用方式与标准库类似。它们的功能覆盖 科学计算、人工智能、机器学习、Web 开发、数据库接口多个领域。

性能

在性能方面,Python 作为解释型语言,不需要编译,在性能方面与 C++ GO 语言有很大差距,并行编程这一块因为 GIL 的存在 ,Python 很难充分利用多核 CPU 的优势。GO 的运行效率几乎可以媲美 C/C++,而且天然支持并发编程,可以通过 go 关键字创建 N goroutine(一种用户态线程)来实现并发,同时提供多个 goroutine 的同步机制。

小结

综上所诉,C++ 在性能方面很高,但是开发成本也比较高。Python 开发简单,但是性能较低,使用与一些对性能没要求的服务和工具开发。相对来说,GO 语言的语法简单,开发成本低,而且效率也远高于解释型语言,适合大部分的服务开发。

Lambda 表达式的参数捕获有哪几种情况

  • 值捕获
    • 与传值参数类似,要求值捕获的前提是变量可以拷贝
    • 与传值参数不同的是,被捕获的变量是在 lambda 创建时拷贝,而不是在调用时拷贝
  • 引用捕获
    • 确保被引用的对象在 lambda 表达式执行时还是存在的
  • 隐式捕获
    • 在捕获列表中使用 & = 即表示隐式捕获,指示编译器自动推断捕获列表
    • & 表示采用引用捕获的方式
    • = 表示采用值捕获的方式

Reference


最后更新: 2023年10月8日 15:31:56
创建日期: 2023年9月6日 10:51:03
回到页面顶部