Skip to content

八股

  • 建立别名的两种方式
    • define 预处理器 #define byte char 预处理器会在编译程序时使用 char 替换所有 byte
    • 使用关键字 typedeftypedef char byte
  • 由于 define 只是简单的字符串替换而并不是真正的定义别名,可能存在问题

    #define FLOAT_POINTER float*
    FLOAT_POINTER pa, pb;
    float* pa, pb;
    //并没有得到两个指针对象
    

  • 右值引用

    • 使用&&定义 int&& rvalueRef = 5;
    • 右值引用允许你安全地绑定到一个临时对象,从而可以修改它们(专门用来绑定到右值上)
    • 移动语义允许将一个对象的资源从一个对象转移到另一个对象,这样可以避免不必要的复制,提高性能。
    • 将左值转化为右值并进行绑定 int&& rvalueRef2 = std::move(variable);
  • 使用移动构造函数
    std::vector<int> source;
    std::vector<int> destination = std::move(source);  // 使用移动语义,避免数据拷贝
    
  • 左值:表达式结束后依然存在的持久对象可以取地址,可以将一个右值赋给左值。
  • 右值:表达式结束就不再存在的临时对象。不可取地址,不可以通过内置(不包含重载) & 来获取地址

  • include<>和""的区别

    • # include \ 通常在编译器或者 IDE 中预先指定的搜索目录中进行搜索,通常会搜索 /usr/include 目录,此方法通常用于包括标准库头文件;
    • # include "filename" 在当前源文件所在目录中进行查找,如果没有;再到当前已经添加的系统目录(编译时以 -I 指定的目录)中查找,最后会在 /usr/include 目录下查找。
  • NULL 和 nullptr 区别

    • NULL 在 C 语言中被引入,随后也被 C++采纳。在 C++中,NULL 通常被定义为 0 或者 ((void*)0),意味着它实际上是一个整数类型的零值。因为 NULL 被定义为 0,所以它可以被隐式转换为任何指针类型,但也可以被隐式转换为整数类型,这可能会导致类型混淆的问题,特别是在函数重载的上下文中。
    • nullptr是在C++11中引入的,作为一种新的空指针常量类型。nullptr的类型是std::nullptr_t,它可以被隐式转换为任何指针类型,但不可以被转换为整数类型
  • 悬空指针:指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间

  • 野指针:不确定其指向的指针,未初始化的指针为“野指针”

  • 函数指针

    • 通过函数名就可以直接获得函数的地址
    • 函数指针的声明 double (*pf)(int); (返回值为 double、有一个参数 int)
    • 使用函数指针 double y=(*pf)(5);
  • 可以直接使用 memcmp 比较结构体吗

    • 需要重载操作符 == 判断两个结构体是否相等,不能用函数 memcmp 来判断两个结构体是否相等,因为 memcmp 函数是逐个字节进行比较的,而结构体存在内存空间中保存时存在字节对齐,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无法比较。
  • 多文件编译

    • C++名空间:C++引入命名空间是为了避免合作开发项目时产生的命名冲突;命名空间至少包含它们的声明部分,所以多文件编程时, 命名空间在.h 头文件中
    • const 全局变量是文件作用域的,可以 extern 在头文件声明,其他文件再导入头文件
    • C++项目中的所有代码是分别进行编译的,只需要在编译成目标文件后再进行链接
      • 当文件b.cpp 需要调用 a() 函数时,只需要先声明一下该函数即可,这是因为,编译器在编译b.cpp 的时候,会生成一个符号表,类似 a() 这样看不到定义的符号就存到这个表中。在链接阶段,编译器就会在别的目标文件中寻找这个符号的定义,一旦找到,程序就可以顺利生成,否则,链接错误。
    • 头文件里面有很多声明,include 头文件就类似从头文件引入(复制)声明。.h 头文件作用是被其他 .cpp包含进去,其本身并不参与编译,但它的内容会在多个 .cpp 中得到编译
    • 强符号:由用户明确定义的函数和已初始化的全局变量(不允许有多个重名)
    • 弱符号:未初始化的全局变量、具有默认值的全局变量
  • 使用 #pragam once 以及宏定义是为了防止一个头文件在一个编译单元内被重复引用
    • 但是全局变量也不应该被在头文件进行定义,这会造成重定义,应该在一个. c 中定义,在头文件中声明 extern
  • 分离式编译优缺点
    • 优点:增加编译效率(只需要重新编译修改了的文件);并行编译
    • 缺点:链接较慢,依赖关系复杂,构建配置复杂,难以进行全局优化
  • 对于两个自定义数据类型/类,如何互相持有指针并使用方法?

    • 虽然在前面声明了 B,A 可以直接持有 B 的指针/引用,但是不能使用 B 的具体成员或方法(使用不完全类型)
    • 应该在 B 下面类外实现 A 的方法
      struct B; // 前向声明B
      struct A {
          B* b;
          void func(); // 声明成员函数
      };
      struct B {
          A* a;
          void func() {
              // 这里可以使用a,因为A已经被定义
          }
      };
      void A::func() {
          // 这里可以使用b,因为B的定义已经完成
      }
      
  • 堆上内存分配机制:申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小

  • 如何限制类对象只能建立在堆上或者栈上?

  • 只能建立在堆上:
    • private 构造函数,提供一个方法来创建对象(成员函数 new 出来,可以使用 private 构造)
    • private 析构函数,由于栈上空间由编译器负责,因为无法释放,编译器会拒绝在栈上创建对象。还需要提供成员方法用于释放对象 void destory(){delete this;}
  • 只能建立在栈上

    • 将operator new()设为私有即可
  • 线程有自己的栈空间,但是堆空间是由进程中所有线程共享

    • 在多线程环境中,每个线程都必须有自己的栈空间,这是因为栈用于存储函数调用的局部变量、返回地址等。这些信息是线程执行流程中特有的,需要独立管理,以保证线程之间的执行流程不互相干扰。
    • 如果每个线程的栈空间过大,给每个线程分配独立的栈会消耗大量的内存资源,特别是在创建大量线程的应用程序中。因此,操作系统和开发者通常会限制线程栈的大小,以优化内存使用。
  • 静态链接

    • 生成可执行文件时将全部外部调用函数全部拷贝到最终可执行文件中
  • 动态链接

    • 代码在生成可执行文件时,该程序所调用的部分程序被放到动态链接库或共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息
  • contexpr

    • 用于声明变量、函数或对象构造函数在编译时是常量表达式(可以在编译时确定)
    • 使用 constexpr 可以提高程序的性能,因为它减少了运行时的计算需求
    • 只是一个建议,程序会进行判断
  • sizeof

    • 是一个运算符不是一个函数
    • 计算是在编译时进行的,结果是一个编译常数
  • lambda 原理

    • lambda 表达式可以捕获外部对象作用域中的变量
    • lambda 表达式实际上是一个匿名类的实例,这个类重载了 operator()。当捕获外部变量时,这些变量被作为匿名类的成员存储。这意味着每个 lambda 表达式的类型都是唯一的。通过捕获变量,lambda 表达式与其捕获的外部变量一起形成了一个闭包。
    • 闭包的生命周期不受外部变量原始作用域的限制。即使外部变量的作用域结束,只要闭包还存在,通过值捕获的变量的副本仍然可以访问。
  • 闭包:闭包是一个函数以及该函数声明时的词法环境的组合。这个环境包含了闭包创建时在作用域中的所有局部变量。简而言之,闭包允许你从一个函数内部访问外部函数作用域的变量。(闭包在内部保持了外部函数作用域中的变量)
  • 闭包的用途:

    • 数据封装和私有化:闭包可以创建包含私有数据的函数,这些数据不能从外部直接访问,只能通过闭包提供的函数访问。
    • 维持状态:闭包允许回调函数访问其他的局部变量,维持执行上下文的状态。
  • 类的大小

    • 遵循结构体的成员变量对齐原则。
    • 与普通成员变量有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响。因为静态数据成员被类的对象共享,并不属于哪个具体的对象。
    • 虚函数对类的大小有影响,是因为虚函数表指针的影响。
    • 虚继承对类的大小有影响,是因为虚基表指针带来的影响。
    • 空类的大小是一个特殊情况,空类的大小为 1,空类同样可以被实例化,而每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址,所以 sizeof (A) 的大小为 1
  • 函数重载

    • 函数重载的关键是函数的参数列表——也称为函数特征标(unction signature),如果两个函数的参数数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同(相同类型的引用和本身为相同特征标)
    • 特征标不同才能进行重载
  • 模板像宏定义,它不能节省空间,如果使用了 int 和 double 版本的 swap,则最终的代码段会包含两个 swap 函数:最终仍将由两个独立的函数定义,就像以手工方式定义了这些函数一样。最终的代码不包含任何模板,而只包含了为程序生成的实际函数。使用模板的好处是,它使生成多个函数定义更简单、更可靠。

  • 头文件的内容

    • 函数原型
    • # define 、const 定义的符号常量
    • 结构、类声明
    • 内联函数
  • 变量只能一次定义,但是可以多次声明 extern

    • 如果在 a.h 或者 a.cpp 中定义了一个全局变量 (extern) int a=10,那么在其他文件中需要使用 extern int a声明这个全局变量已经在其他地方定义过了。
    • 如果一个全局变量定义在头文件 a.h 中,就不能在多个源文件中引用 a.h,否则会造成多重定义错误。更一般的做法是,在 a.h声明,而在 a.cpp 中定义,然后其他源文件引用a.h
  • mutable:即使类变量为 const,其某个成员也可以被修改

  • 名空间

  • 创建名空间
    namespace Jill{
        double bucket{double n}{...};
        double fetch;
        int pal;
        struct Hill{...};
    }
    
  • using :将名空间引入当前作用域

    • using 声明 using Jill::fetch
    • using 编译 `using namespace Jack;
    • 如果某个名称已经在函数中声明了,则不能用 using声明导入相同的名称。
    • 如果使用 using 编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全局变量一样,不过仍可以使用作用域解析运算符。
  • static

    • 局部静态变量:程序执行期间只初始化一次,值在函数调用(多次调用)之间保持不变
    • 全局静态变量:全局变量默认是整个程序可见,但用 static 修饰之后编程文件作用域
    • 静态成员:类外初始化
    • 静态成员函数:静态成员函数只能访问静态成员(没有 this 指针)
  • const

    • 声明常量变量
    • 成员函数中的应用
      • 不可修改的函数参数
      • 常量成员函数 void myFunction() const {} 表示不会修改成员变量状态
      • 常量返回值
    • 常量对象:只能使用常量成员函数
    • const 指针(指针指向的内容不可变,但是指针本身的内容可以改变。): const int *p = &x;
    • const 修饰指针,则指针为不可变量,指针指向的内容可以变 int* const p = &a;
    • const 引用是指向 const 对象的引用,可以读取变量,但不能通过引用修改指向的对象 const int &ref = i;
  • volatile

    • 当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 volatile,告知编译器不应对这样的对象进行优化。
    • 即每次都从原始内存读取,不适用缓存优化等
  • extern C 的用途

    • 由于 C 语言并不支持函数重载,在 C 语言中函数不能重名,因此编译 C 语言代码的函数时不会带上函数的参数类型,一般只包括函数名。(编译后的函数名不同)
    • 如果在 C++ 中调用一个使用 C 语言编写的模块中的某个函数 test,C++ 是根据 C++ 的函数名称修饰方式来查找并链接这个函数,去在生成的符号表查找这个函数的代码,此时就会发生链接错误。而此时我们用 extern C 声明,那么在链接时,C++ 编译器则按照 C 语言的函数命名规则 test 去符号表中查找对应的函数。
      extern "C"{
          int strcmp(const char*, const char*);
      }
      
  • malloc 和 new

    • malloc 根据需要分配的字节数size 来分配内存,并返回一个指向分配的内存的指针。如果分配失败,返回 NULLmalloc 返回一个 void* 类型的指针,使用时通常需要将其显式转换为目标类型的指针。malloc仅分配内存,不调用任何构造函数来初始化对象,也不会调用析构函数来清理对象。
    • new 分配足够的内存来存储特定类型的对象,并返回该类型的指针。new 还会调用对象的构造函数来初始化对象。new 不仅分配内存,还负责调用构造函数初始化对象。相对应地,delete 操作符会调用析构函数并释放内存。
  • 说说 new[]和 delete[]为什么要配套使用

    • C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。
  • inline 的优点

    • 减少函数调用时的开销
  • 内存 5 大区

    • 栈区:局部变量、参数、返回地址
    • 堆区:动态内存分配(生命周期较长)
    • 全局/静态存储区:存储全局变量、静态变量、常量(程序开始时分配,程序结束时释放)全局变量和静态局部变量放在一块,未初始化的放在另一块。
    • 文字常量区:常量在统一运行被创建,常量区的内存是只读的,程序结束后由系统释放。
    • 程序代码区:存放程序的二进制代码,内存由系统管理
  • 类的内存布局
    • 一个类或结构体的实例(对象)在内存中通常按其声明的顺序存储其数据成员。
  • 内存对齐

    • 数据成员应该从其类型的大小是它自己大小的最大2的幂的边界开始。例如,假设一个 int 是4字节,它应该从地址是4的倍数的地方开始。
    • 结构体或类的总大小应该是其最宽基本成员的倍数,以便在数组中每个元素都能保持适当的对齐。
    • 内存对齐是为了优化内存访问性能,虽然这可能导致内存空间的一定程度浪费。
  • 类型转换

    • 静态转换(static_cast):静态转换可以在编译时期完成,一般用于基本数据类型的转化以及指针、引用的向上转化
    • 动态转换(dynamic_cast):动态转换可以在运行时期完成,可以安全的加你选哪个向下转化,会在运行时期检查对象实际类型,确保转化的有效性
    • 常量转换(const_cast):常量转换用于去掉表达式的const属性,使其变成非常量表达式。
    • 重解释转换(reinterpret_cast):重解释转换用于进行各种类型之间的强制转换,包括指针、引用、整数之间的转换。
  • 智能指针

    • share_ptr:允许多个 shared_ptr 实例指向同一个对象。shared_ptr 使用引用计数来跟踪有多少个指针共享同一个资源,当最后一个这样的指针被销毁时,资源也会被自动释放。
    • unique_ptr:独占所有权的智能指针,确保同一时间内只有一个unique_ptr 可以指向给定的对象。当 unique_ptr 被销毁时(例如,离开作用域或被显式删除),它指向的对象也会被自动删除。
      • 独占所有权语义,不可复制,但可以移动。
      • std::unique_ptr<int> ptr2 = std::move(ptr);
      • shared_ptr 轻量,没有引用计数的开销。
  • uniqueptr 的实现原理
    • 禁止拷贝unique_ptr 通过删除拷贝构造函数和拷贝赋值操作符来防止对象被拷贝。这是因为允许拷贝会导致两个对象指向同一资源,违反了 unique_ptr 的唯一所有权原则。
    • 支持移动unique_ptr实现了移动构造函数和移动赋值操作符,允许将资源的所有权从一个unique_ptr转移到另一个。这通过交换源unique_ptr和目标unique_ptr的内部指针来实现,而源unique_ptr随后被设置为nullptr,表明它不再拥有资源。
  • share_ptr 的实现原理
    • 内部维护两个指针成员,一个指向管理的数据地址,一个指向控制块(都在堆上)
    • image.png
  • 控制块内存储:

    • 引用计数(多少个指针共享对象)
      • 当我们销毁一个 shared_ptr 时,引用计数减1。当引用计数减为0时,我们删除指向实际数据的指针和指向引用计数的指针。
      • 当我们拷贝一个 shared_ptr 时,引用计数加1。
      • 当我们赋值一个 shared_ptr 时,我们首先递减左侧运算对象的引用计数。如果引用计数变为0,我们就释放左侧运算对象分配的内存以及引用计数的内存。然后拷贝右侧运算对象的数据指针和引用计数指针,最后递增引用计数。
    • 弱引用计数:提供了一种不拥有对象所有权的方式来观察资源。(不影响内存释放)
    • 删除器(自定义如何释放对象)、分配器(创建时为对象分配内存)
  • weak_ptr

    • 一种弱引用,指向 shared_ptr 所管理的对象,而不影响所指对象的生命周期
    • weak_ptr 对它所指向的 shared_ptr 所管理的对象没有所有权,不能对它解引用,因此若要读取引用对象,必须要转换shared_ptr
    • 通过 lock 方法实现的,lock 方法尝试从 weak_ptr 创建一个 shared_ptr 实例
      • 如果 weak_ptr 所观察的对象仍然存在(即,至少有一个 shared_ptr 实例仍然拥有该对象),lock 会成功创建一个 shared_ptr 实例,这个新创建的 shared_ptr 会与其他 shared_ptr 共享所有权,从而确保对象在使用期间不会被销毁。
      • 如果所观察的对象已经被销毁(即,没有任何shared_ptr实例拥有该对象),lock会返回一个空的shared_ptr实例。这使得调用者可以检查返回的shared_ptr是否为空,从而安全地判断对象是否还存在。
      • 在使用对象之前需要检查对象是否已经被释放
  • 循环引用

    • 两个或多个对象之间通过 shared_ptr 相互引用,形成了一个环,导致它们的引用计数都不为0,从而导致内存泄漏。
    • 在程序中找到循环引用后用 weak_ptr 替换替换一个 share_ptr 来来打破循环
  • 完美转发 forward

    • 允许将参数“完美”地转发给另一个函数,保持原始参数的值类别(左值、右值)不变。
  • std:: move 的实现原理

    • 万能模板引入参数
    • 移除引用
    • 强制类型转换得到右值引用
  • 流和缓冲区

面向对象

  • 类的声明

    • 通常在头文件中进行
    • 声明时可以直接加入成员方法,会被自动称为内联函数
    • 在类的外部定义成员函数时需要同时提供类名和函数名:Stock::set_tot()
  • return *this 是对 this 指针的解引用。它意味着将当前对象的引用返回。

  • C++多态的形式:

    • 静态多态:函数多态(重载);模版
    • 动态多态:虚函数重写;基类指针(引用)指向子类
  • 操作符重载:

    class Time
    {
    private:
        int hours;
        int minutes;
    public:
        Time();
        Time (int h,int m=0);
        void AddMin(int m)
        void AddHr(int h):
        void Reset(int h=0,int m=0)
        Time operator+(const Time &t) const;
        void Show() const;
    }
    
    Time Time::operator+(const Time &t) const
    {
        Time sum;
        sum.minutes = minutes+t.minutes;
        sum.hours=hours+t.hours+sum.minutes/60;
        sum.minutes %= 60:
        return sum;
    }
    

  • 只能重载已经存在的操作符
  • 不改变优先级
  • 有的操作符不能重载
  • 一些操作符必须使用成员函数重载、一些必须使用外部函数
    • 涉及到对类成员进行访问、修改的重载通常为成员函数,如[]()->
      char & String::operator[](int i) { return str[i]; }
      
  • <<的重载必须使用友元函数

    • 通常来说使用使用友元函数时有两个参数,使用成员函数时只有一个参数(this 隐含给出)
      ostream& operator<<(ostream& os,const Time t)
      {
          os<<t.hours<<"hours,"<<t.minutes<<"minutes";
          return os;
      }
      
  • 友元:

    • 友元函数
    • 友元类
    • 友元成员函数
    • 需要在类声明中添加友元声明 friend Time operator*(double m,const Time t);
  • 类的自动转化

    • 类可以根据构造函数进行自动转换(其他类型->本类型)
      Stonewt myCat;//create a Stonewt object
      mycat=19.6;//use Stonewt(double)to convert 19.6 to Stonewt
      
  • 添加 explicit 关键字在构造函数前面禁止利用构造函数进行自动转换
  • 重载转换函数(本类型->其他类型)

    class Stonewt
    {
    private:
        ...
    public:
        operator int() const;
        operator double() const;
    }
    
    //defination below
    Stonewt::operator int() const
    {
        return int(pounds+0.5);
    }
    Stonewt::operator double() const
    {
        return pounds;
    }
    

  • 特殊成员函数

    • 默认构造函数
    • 默认析构函数
    • 复制构造函数(将一个对象复制到新创建的对象中)Class_name(const Class_name &)
      • 接受一个指向类对象的常量引用作为参数。
      • 默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的(静态类成员不变)。
      • 使用场景:通过同类创建对象时使用;作为函数参数传递;作为函数返回值
    • 赋值运算符
      • 将己有的对象给另一个对象时,将使用重载的赋值运算符:StringBad & StringBad::operator=(const StringBad &)
      • 使用场景:对已经创建的对象进行赋值操作
    • 地址运算符
    • 移动构造函数
    • 移动赋值运算符
  • 类型萃取:使用模板技术来萃取类型(包含自定义类型和内置类型)的某些特性,用以判断该类型是否含有某些特性,从而在泛型算法中来对该类型进行特殊的处理用来提高效率或者得到其他优化。

  • 构造派生类时必须先创建基类对象,因此程序将使用默认的基类构造函数

  • 函数重载:同一作用域内存在多个同名函数,但这些函数的参数列表不同

  • 函数重写:多态是通过虚函数来实现的。如果一个基类定义了一个虚函数,那么派生类可以重写这个函数。然后,我们可以通过基类的指针或引用来调用这个函数,而实际执行的是派生类版本的函数。这就是所谓的动态绑定。(override 在派生类中用于显式地指明函数重写了基类中的虚函数, 仅用于编译检查)

    class Base {
    public:
        virtual void show() {
            std::cout << "Base show" << std::endl;
        }
    };
    
    class Derived : public Base {
    public:
        void show() override { // 明确标记重写
            std::cout << "Derived show" << std::endl;
        }
    };
    

  • 函数隐藏:如果派生类中的函数和基类中的函数具有相同的名称但参数列表不同,那么派生类中的函数将会隐藏基类中所有同名的函数,无论参数列表是否相同。(所以,应尽量避免这种情况。)

  • 虚函数的工作原理

    • 虚函数表(vtable):当一个类中声明了虚函数,编译器会为这个类生成一个虚函数表。这个表是一个函数指针数组,数组中的每个元素都是指向类的虚函数的指针。每个类的虚函数表是唯一的,所有该类的对象都共享同一个虚函数表。
    • 虚指针(vptr):当一个类对象被创建时,如果该类中有虚函数,那么编译器会在对象内部添加一个虚指针。这个虚指针指向该类的虚函数表。如果有派生类对象,那么派生类对象的虚指针会指向派生类的虚函数表。
    • 动态绑定:当我们通过基类指针或引用调用虚函数时,编译器会查找指针或引用所指向的对象的虚指针,然后通过虚指针找到对应的虚函数表,最后在虚函数表中查找并调用相应的虚函数。这个过程是在运行时进行的,因此称为动态绑定。
  • 虚函数表在编译时期创建,为每个有虚函数的类生成一个虚函数表
  • 虚函数指针在对象构造时进行初始化,在构造函数的代码中插入设置虚函数表指针的操作

  • 纯虚函数 virtual double Area() const=0;

    • 不能创建包含纯虚函数的类的对象
  • 虚继承的实现原理

    • 使用一个虚基类表,虚基类指针
    • 虚表中记录了虚基类与本类的偏移地址
    • 通过虚基类指针引用公共对象,对象中只保存一份父类的对象,确保了无论通过哪个派生路径访问基类,都是同一个实例。
  • 如果派生类中定义了 new:则需要定义析构函数、复制构造函数以及赋值运算符

  • 构造函数一般不定义为虚函数: 虚函数表指针是在创建对象之后才有的,因此不能定义成虚函数。

  • 析构函数一般定义成虚函数:析构函数定义成虚函数是为了防止内存泄漏,因为当基类的指针或者引用指向或绑定到派生类的对象时,如果未将基类的析构函数定义成虚函数,会调用基类的析构函数,那么只能将基类的成员所占的空间释放掉,派生类中特有的就会无法释放内存空间导致内存泄漏。

  • 继承方式:public、private、protected

    • private 将所有权限改变为 private 指的是派生类对外,而不是针对派生类(对于派生类还是原先的 public、private...)
  • 多重继承

    • 带来的问题:从两个不同的基类继承同名方法:从不同的直接基类中继承同一个类的多个实例
    • 对于同名方法可以使用作用域受限解决 A::
    • 对于菱形继承使用虚基类解决
  • 虚基类
    class Singer:virtual public Worker{...};
    class Waiter:virtual public Worker{...};
    class SingingWaiter:public Singer, public Waiter{...};
    
  • 继承的 Singer 和 Waiter 对象共享一个 Worker 对象。
  • 在普通的继承关系中:派生类的构造函数只能通过中间类的构造函数来间接调用基类的构造函数。虚继承关系中派生类可以直接调用,不需要通过中间类

  • 使用成员初始化列表效率更高

    • 由于 C++ 规定对象的成员变量的初始化动作发生在进入自身的构造函数本体之前,那么在执行构造函数之前首先调用默认的构造函数为成员变量设初值,在进入函数体之后,再显式调用该成员变量对应的构造函数。(即重复构造)
    • 用户自定义类型如果使用类初始化列表,直接调用该成员变量对应的构造函数即完成初始化;在进入构造函数函数体之前进行
    • image.png|325
  • 为什么拷贝函数必须为引用:

    • 避免拷贝构造函数无限制的递归而导致栈溢出。
    • (函数传参也需要拷贝函数)
  • 禁止类被继承

    • 使用 final class Base final
  • 使用虚函数和私有构造函数实现*
    #include <iostream>
    using namespace std;
    
    template <typename T>
    class Base{
        friend T;
    private:
        Base(){
            cout << "base" << endl;
        }
        ~Base(){}
    };
    
    class B:virtual public Base<B>{   //一定注意 必须是虚继承
    public:
        B(){
            cout << "B" << endl;
        }
    };
    
    class C:public B{
    public:
        C(){}     // error: 'Base<T>::Base() [with T = B]' is private within this context
    };
    
    
    int main(){
        B b;  
        return 0;
    }
    
  • B 是 Base 的友元,但是 C 不是,因而 C 无法调用 Base 的构造函数进行初始化

STL

  • resize 和 reverse 的区别
    • resize(n) 操作调整容器的大小以使其能够容纳 n 个元素。如果当前大小小于 n,则在容器末尾添加足够数量的元素以达到指定的大小,新添加的元素将被初始化(对于基本类型,通常是零初始化;对于类类型,则调用默认构造函数)。如果当前大小大于 n,则容器末尾多余的元素将被删除。改变容器中实际存储的元素数量时
    • reserve(n)操作用于请求改变容器的容量至少足以容纳n个元素。这是一种优化操作,其目的是减少因后续插入操作而导致的多次内存分配和复制

容器

vector

  • 底层是一个动态数组,包含三个迭代器(空间头尾以及使用空间尾部)
  • 底层为动态数组,空间不足时申请 1.5/2 倍空间并进行拷贝并释放原空间(旧迭代器会全部失效,删除元素时也会引起)
  • size 返回元素数目,capacity 返回容量

list

  • list 底层是双向链表

deque

  • 底层通过多个固定大小的数组(块)来实现,从而可以实现在两端快速添加或删除元素。当在deque的任一端插入元素时,如果该端的当前块已满,容器会自动分配一个新的块并将其链接到现有的块序列中。
  • queue、stack 底层都是借助 deque 实现的

priority_queue

  • 堆实现

map 、set、multiset、multimap

  • 红黑树实现

unordered_map、unordered_set

  • 哈希表

stack

  • stack 是一个容器适配器,并不是一个容器,只是提供了之中特定的数据处理方式但是本身没有提供数据存储方式,可以使用 deque、vector、list
  • 容器适配器是一种特殊的容器,通过提供一个特定的接口来限制对另一个容器的访问方式。
  • 常见的容器适配器有 stack、queue、priority_queue

迭代器

  • 迭代器的分类:
    • 输入迭代器:只读迭代器,在每个被遍历的位置上只能读取一次
    • 输出迭代器:只写迭代器,在每个被遍历的位置上只能被写一次
    • 前向迭代器:具输入和输出迭代器的能力,但是它可以对同一个位置重复进行读和写。但它不支持operator–,所以只能向前移动。
    • 双向迭代器:很像前向迭代器,只是它向后移动和向前移动同样容易。
    • 随机访问迭代器:有双向迭代器的所有功能。而且,它还提供了“迭代器算术”,即在一步内可以向前或向后跳跃任意位置,包含指针的所有操作,可进行随机访问,随意移动指定的步数。

算法

  • sort 的实现:
    • 采用快速排序算法作为主要排序算法,辅以其他算法(如插入排序和堆排序)来处理特定情况,以提高排序效率。这种混合排序策略的目的是在不同的数据集和不同大小的数据集上都能提供最佳的性能。
    • 在数组较小或几乎已排序的情况下,插入排序可以提供更好的性能。
    • 数据集的大小超过快速排序能高效处理的阈值时(递归深度较深),会使用堆排序
    • pivot 选择优化:
      • 三位(或者更多)取中:头中尾取三个数的中位值
      • 当数据量较大时效率较高

libc 标准库的实现

stdlib