C++面经

apple

多态介绍

  1. 定义:首先,可以解释一下什么是多态。多态(Polymorphism)是面向对象编程的一个重要特性,它允许我们使用父类的指针或引用来操作子类对象。这样,同一个函数或者操作符可以对不同类型的对象产生不同的行为。

  2. 两种形式:C++中的多态主要有两种形式:静态多态(或编译时多态)和动态多态(或运行时多态)。静态多态通过模板和函数重载实现;动态多态则需要使用虚函数和继承。

  3. 虚函数可能带来的运行时开销、虚函数应该提供默认实现或声明为纯虚函数等。

静态多态

静态多态,也称为编译时多态,是指在编译时就能确定函数调用的具体版本。C++中主要通过两种方式来实现静态多态:函数重载和模板。

函数重载:你可以定义多个名称相同但参数列表不同的函数,这些函数就构成了一组重载函数。当你调用一个重载函数时,编译器会根据传入的参数类型和数量在编译时选择合适的函数版本。例如:

void print(int i) {
    std::cout << "Printing int: " << i << std::endl;
}

void print(double d) {
    std::cout << "Printing double: " << d << std::endl;
}

在这个例子中,print 函数就有两个版本:一个接受 int 参数,另一个接受 double 参数。具体调用哪个版本取决于你传入的参数类型。

模板:模板是一种让函数或类能够处理多种数据类型的机制。对于模板函数或模板类,你只需编写一份代码,然后编译器会在需要的时候为你生成针对特定类型的代码。例如:

template <typename T>
void print(const T& t) {
    std::cout << "Printing: " << t << std::endl;
}

这里的 print 函数是一个模板函数,它可以接受任何数据类型的参数。具体处理哪种数据类型的版本由你传入的参数类型在编译时决定。

静态多态的优点是效率高,因为函数调用版本的选择发生在编译时,所以没有运行时开销。缺点是所有可能的版本必须在编译时都已知,这限制了代码的灵活性。

除了多态和继承,面向对象的其他核心概念有:

封装:隐藏内部实现,仅暴露必要接口。
抽象:简化复杂系统,展示关键信息。
组合:使用已有对象构建新对象。
关联/聚合:表达类之间的关系。
接口:定义行为规范。

size_of 是在编译期还是在运行期确定

sizeof 是在编译期确定的。编译器知道每种数据类型的大小,因此能够在编译时计算出 sizeof 表达式的值。这也意味着 sizeof 可以用于数组的长度等编译期常量。对于动态分配的内存(如使用 malloc 或 new 创建的),sizeof 只能返回指针本身的大小,而不是它所指向的内存块的大小。

写一个生产者消费者模型

#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>

std::queue<int> produced_nums;
std::mutex mtx;
std::condition_variable cv;

// 生产者函数
void producer(int id)
{
    for (int i = 0; ; i++) {
        {
            std::unique_lock<std::mutex> lock(mtx);
            std::cout << "producing " << i << '\n';
            produced_nums.push(i);
        }
        cv.notify_all();
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); # 这句代码使当前线程暂停执行指定的时间,这里是100毫秒。用于模拟生产者生产数据所需的时间,防止过快地生产数据。
    }
}

// 消费者函数
void consumer(int id)
{
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        while (produced_nums.empty()) {
            cv.wait(lock);
        }

        std::cout << "consuming " << produced_nums.front() << '\n';
        produced_nums.pop();
    }
}

int main()
{
    std::thread p1(producer, 0);
    std::thread c1(consumer, 0);

    p1.join();
    c1.join();

    return 0;
}

单继承和菱形继承时候的虚函数表内存分布情况

在单继承和菱形继承中,虚函数表的内存分布情况略有不同。

  1. 单继承

在单继承中,在子类没有覆盖父类的方法的情况下派生类会继承基类的虚函数表指针(vptr),该指针指向基类的虚函数表。基类的虚函数表中包含了基类的虚函数和派生类覆盖的虚函数。派生类中新增的虚函数会加入到基类虚函数表的末尾。

具体来说,假设单继承的类为 Derived,其中包含一个基类 Base,Base 中有一个虚函数 Func。那么 Derived 类的对象内存布局如下:

   +------------------------+
   |  Base::vptr             |
   +------------------------+
   |  Derived's members      |
   |  ...                    |
   +------------------------+

其中,Base::vptr 指向虚函数表的起始地址,虚函数表中包含了 Base 和 Derived 类的虚函数。如果 Derived 类覆盖了 Func 虚函数,那么覆盖后的 Func 实现会替代原有的 Base::Func 实现。

  1. 菱形继承

如果不使用虚继承,那么在菱形继承中,每个派生类都会包含两个基类的虚函数表指针,因此该类的对象将包含两个虚指针。具体来说,假设一个类 Diamond 继承了两个基类 Base1 和 Base2,不使用虚继承,那么 Diamond 类的对象内存布局如下:

   +------------------------+
   |  Diamond::Base1::vptr   |
   +------------------------+
   |  Base1's members        |
   |  ...                    |
   +------------------------+
   |  Diamond::Base2::vptr   |
   +------------------------+
   |  Base2's members        |
   |  ...                    |
   +------------------------+
   |  Diamond's members      |
   |  ...                    |
   +------------------------+

在这个例子中,Diamond 类包含了两个虚指针 Diamond::Base1::vptr 和 Diamond::Base2::vptr,它们分别指向了 Base1 和 Base2 的虚函数表。由于派生类同时继承了两个基类的虚函数表指针,因此会浪费一部分内存空间。此外,如果这两个基类中有相同的虚函数,那么在调用这个虚函数时会出现二义性错误。

因此,为了避免内存浪费和虚函数调用二义性的问题,应该使用虚继承来进行菱形继承。 3. 虚继承
如果使用虚继承,那么在菱形继承中,派生类会继承基类的虚函数表指针,并且合并基类的虚函数表,从而避免内存浪费和虚函数调用二义性的问题。因此,该类的对象只会包含一个虚指针(vptr),指向合并后的虚函数表。

具体来说,假设一个类 Diamond 继承了两个基类 Base1 和 Base2,使用虚继承,那么 Diamond 类的对象内存布局如下:

   +------------------------+
   |  Diamond::vptr          |
   +------------------------+
   |  Base1's members        |
   |  ...                    |
   +------------------------+
   |  Base2's members        |
   |  ...                    |
   +------------------------+
   |  Diamond's members      |
   |  ...                    |
   +------------------------+

在这个例子中,Diamond 类继承了 Base1 和 Base2 两个虚基类,因此它的对象只会包含一个虚指针 Diamond::vptr,指向合并后的虚函数表。由于虚指针是从虚基类继承而来的,因此派生类不需要自己再创建一个指向自己的虚指针。

使用虚继承可以有效避免内存浪费和虚函数调用二义性的问题,因此在菱形继承中应该尽可能使用虚继承。

菱形继承会造成什么问题

菱形继承可能会造成以下两个问题:

  1. 内存浪费

在菱形继承中,由于派生类同时继承了两个虚基类,这两个虚基类中包含了相同的成员变量,因此会造成内存浪费。具体来说,如果派生类中直接访问这个成员变量,那么实际上访问的是两个虚基类中的其中一个,另一个成员变量则被浪费掉了。这种情况下,可以使用虚继承来解决内存浪费的问题。

  1. 虚函数调用二义性

在菱形继承中,派生类会继承两个虚基类的虚函数表指针(vptr),这两个虚函数表指针指向同一个虚函数表。如果这两个虚基类中有一个共同的虚函数,在派生类中调用这个虚函数时,就会出现二义性错误。这种情况下,需要明确指定调用的虚基类中的虚函数,避免出现二义性错误。

需要注意的是,菱形继承的问题并不是绝对的,具体是否会出现问题取决于具体的代码实现和设计。在使用菱形继承时,需要仔细考虑内存布局和虚函数的调用问题,确保程序的正确性和可维护性。

讲下 cpp 的多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

cpp 多态是怎么实现的

C++中的虚函数是实现多态的重要机制。在 C++中,当一个类中声明了虚函数时,编译器会为该类生成一个虚函数表(vtable),并将该类的每个对象中都存储一个指向虚函数表的指针(vptr)。虚函数表是一个包含了所有虚函数地址的表格,每个虚函数在表格中的位置是固定的。

当程序通过一个基类指针或引用调用虚函数时,实际执行的是该对象的实际类型对应的虚函数。具体实现过程如下:

  1. 在程序运行时,当对象被创建时,会在对象中存储一个指向虚函数表的指针(vptr)。

  2. 当程序通过一个基类指针或引用调用虚函数时,编译器会将该调用转化为一个间接调用,即通过对象中存储的虚函数表指针找到对应的虚函数地址,然后调用该地址对应的函数。

  3. 如果对象是基类类型,则调用基类中的虚函数;如果对象是派生类类型,则调用派生类中的虚函数。这个过程中,实际调用的函数是由对象的实际类型来决定的,而不是由引用或指针的类型来决定的,因此实现了多态。

  4. 如果派生类没有重写基类的虚函数,则调用基类中的虚函数;如果派生类重写了基类的虚函数,则调用派生类中的虚函数。

虚函数的实现过程中,由于需要查找虚函数表,因此会带来一定的性能开销。为了减少这种开销,编译器通常会对虚函数进行优化,如使用内联函数、缓存虚函数表等技术来提高性能。

malloc/free 和 new/delete?

  • malloc 和 free 是 C 库函数,也可以在 c++中使用。而 new 和 delete 仅适用于 c++;
  • malloc 和 new 都是在堆中动态分配内存,但是 new 会调用类的构造函数,而 malloc 不会;
  • free() 释放内存但不调用类的析构函数,delete 释放内存并调用类的析构函数。

Placement new:

  • Placement new 是 C++ 中的一个变体 new 运算符。 普通的 new 操作符做两件事:(1)分配内存(2)在分配的内存中构造一个对象。
  • 在 placement new 中,我们可以传递一个预先分配的内存并在传递的内存中构造一个对象。
  • 正常 new 在堆中分配内存并在那里构造对象,而使用 placement new,对象构造可以在已知地址完成。
  • 对于普通的 new,它不知道它指向的地址或内存位置,而它指向的地址或内存位置在使用 placement new 时是已知的。
// buffer on stack
unsigned char buf[sizeof(int)*2] ;

// placement new in buf
int *pInt = new (buf) int(3);

重载、隐藏、重写(覆盖)三者的区别

重载(overload),函数名相同,但是参数的类型、顺序、个数不同(只要可以分辨),重载不关心函数返回类型

隐藏:是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。下图会报错,原因是子类隐藏了父类的函数

什么是内存对齐?为什么进行内存对齐?

  • 现代计算机中内存都是按照 byte 划分的,从理论上讲对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它会要求这些数据的首地址的值是某个数 k(通常为 4 或 8)的倍数,这就是所谓的内存对齐;
#include <iostream>
struct
{
    char y;
    int x;
} s;
int main()
{
    std::cout << sizeof(int) << std::endl;
    std::cout << sizeof(char) << std::endl;
    std::cout << sizeof(s) << std::endl;
    return 0;
}
// 输出
// 4
// 1
// 8
  • 内存对齐能够提高 cpu 读取数据的速度,减少访问数据的出错性。

堆和栈区别

堆和栈是两种重要的用于内存管理的数据结构,在编程中有着不同的用途。以下是它们的主要区别:

  1. 管理方式:

堆(Heap):在运行时动态分配和释放的内存空间。程序员负责申请和释放堆上的空间,如果忘记释放已使用的空间,可能会导致内存泄漏。

栈(Stack):由操作系统自动分配和释放,通常用于存储函数调用时的局部变量、返回地址等信息。当函数返回时,其在栈上的空间被自动回收。

  1. 生命周期:

堆:堆上的内存生命周期由程序员控制,从手动申请开始,到手动释放结束。

栈:栈上的内存生命周期与函数执行周期相同,函数每次调用时,相关的局部变量都会创建新的存储空间,函数结束时这些空间将被自动回收。

  1. 分配大小:

堆:堆的大小受可用系统内存的限制,可以动态分配大块内存。

栈:栈的大小在进程启动时由操作系统设定,且通常比堆小得多。如果尝试在栈上分配过多内存,可能会导致栈溢出。(ulimit 设置)

  1. 访问速度:

堆:访问堆上的内存一般比访问栈上的内存慢,因为必须通过指针来访问堆中的内存。

栈:访问栈内存更快,因为数据存储的位置是在编译时就确定了的,而且栈上的数据通常都存储在 CPU 的缓存中。

总的来说,堆和栈各有优势,选择使用哪一个取决于具体需求,例如数据的生命周期、所需内存的大小等因素。

C++常量在哪个区

函数内部的 const 变量和对象的 const 成员变量通常会被存放在栈上, 字符串字面量和 const 修饰的全局/静态变量会在常量区

如果子类重写了父类的方法,在子类中如何访问

直接用父类显式调用

class Parent {
public:
    void foo() {
        cout << "This is the parent's foo()" << endl;
    }
};

class Child : public Parent {
public:
    void foo() {
        Parent::foo();
        cout << "This is the child's foo()" << endl;
    }
};

数组、指针的区别

  • 概念

    • 数组是用于储存多个相同类型数据的集合。 数组名是首元素的地址。
    • 指针相当于一个变量,但是它和不同变量不一样,它存放的是其它变量在内存中的地址。  指针名指向了内存的首地址。
  • 区别

    • 赋值:同类型指针变量可以相互赋值;数组不行,只能一个一个元素的赋值或拷贝
    • 存储方式
      • 数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下标进行访问的,数组的存储空间,不是在静态区就是在栈上。
      • 指针的存储空间不能确定。它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。
    • 求 sizeof
      • 数组所占存储空间的内存大小:sizeof(数组名)/sizeof(数据类型)
      • 在 32 位平台下,无论指针的类型是什么,sizeof(指针名)都是 4,在 64 位平台下,无论指针的类型是什么,sizeof(指针名)都是 8。
    • 初始化
    // 数组
    int a[5] = {0};
    char b[] = "Hello";                         // 按字符串初始化,大小为6
    char c[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 按字符初始化
    int *arr = new int[10];                     // 动态创建一维数组
    
    // 指针
    int *p = new int(0); // 指向对象的指针
    delete p;
    
    int *p1 = new int[10]; // 指向数组的指针
    delete[] p1;           // 指向类的指针:
    
    string *p2 = new string;
    delete p2;
    
    int **pp = &p; // 指向指针的指针(二级指针)
    **pp = 10;
    
    • 指针操作:
      • 数组名的指针操作。数组指针也称指向一维数组的指针,亦称行指针。访问数组中第 i 行 j 列的一个元素,有几种操作方式:(p[i]+j)、((p+i)+j)、((p+i))[j]、p[i][j]。其中,优先级:()>[]>*。这几种操作方式都是合法的。
      int a[3][4];
      int(*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组
      p = a;      //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
      p++;        //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]
      return 0;
      
      • 指针变量的数组操作
      char *str = "hello,douya!";
      str[2] = 'a';
      *(str+2) = 'b';
      //这两种操作方式都是合法的。
      

extern 'C' 的作用过程

extern "C" 是一种链接规范(Linkage Specification),在 C++代码中经常会看到,它用于解决 C++和 C 之间的函数名称混淆问题。

在 C++中,函数支持重载,也就是说可以有多个同名但参数不同的函数。为了区分这些函数,C++在编译时会对函数名进行修饰(Name Mangling),生成一个独特的名字。而 C 语言并不支持函数重载,因此也就没有名字修饰的过程。

C++中 struct 和 class 的区别

struct 默认继承和访问权限都是 public,而 class 默认继承和访问权限都是 private

class 可以定义类模板 <template typename>

2、说说构造函数有几种,分别什么作用

  • 默认构造函数

如果没写构造函数,就会生成一个无参数的构造函数

  • 拷贝构造函数

如果没写拷贝构造函数,系统自定生成一个,但是生成的是浅拷贝。参数是类的指针,

两种情况:

Line line1(10);
Line line2 = line1; // 这里也调用了拷贝构造函数
  • 移动构造函数

    指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。

C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。

Move(Move&& source)
        : data{ source.data }
    {

        cout << "Move Constructor for "
             << *source.data << endl;
        source.data = nullptr;
    }

map 的迭代器和 vector 的迭代器有什么区别

map 是双向的,vector 可以随机访问

常用的迭代器按功能强弱分为输入、输出、正向、双向、随机访问五种,这里只介绍常用的三种。

  • 正向迭代器。假设 p 是一个正向迭代器,则 p 支持以下操作:++p,p++,*p。此外,两个正向迭代器可以互相赋值,还可以用==!=运算符进行比较。
  • 双向迭代器。双向迭代器具有正向迭代器的全部功能。除此之外,若 p 是一个双向迭代器,则--pp--都是有定义的。--p使得 p 朝和++p相反的方向移动。
  • 随机访问迭代器。随机访问迭代器具有双向迭代器的全部功能。若 p 是一个随机访问迭代器,i 是一个整型变量或常量,则 p 还支持以下操作:
    • p+=i:使得 p 往后移动 i 个元素。
    • p-=i:使得 p 往前移动 i 个元素。
    • p+i:返回 p 后面第 i 个元素的迭代器。
    • p-i:返回 p 前面第 i 个元素的迭代器。
    • p[i]:返回 p 后面第 i 个元素的引用。

vector 中 clear 函数内存释放的机制

vector.clear()并不会真正释放内存,clear 实际所做的是为 vector 中所保存的所有对象调用析构函数(如果有的话),然后初始化 size 这些东西,让觉得把所有的对象清除了。

介绍下纯虚函数

在 C++中,纯虚函数是一种特殊的虚函数。它在基类中声明, 并使用 = 0 来标记。这意味着该函数没有默认的实现。纯虚函数需要由任何直接或间接继承基类的派生类进行重写和实现。

class AbstractClass {
public:
    virtual void pureVirtualFunction() = 0; // 纯虚函数
};

unordered_map 的底层结构

unordered_map 内部实现了一个哈希表(也叫散列表),通过把关键码值映射到 Hash 表中一个位置来访问记录,查找时间复杂度可达 O(1),其中在海量数据处理中有着广泛应用。因此,元素的排列顺序是无序的。

类型转换

在C++11中,主要有四种类型转换运算符:

  1. static_cast:这是最常用的类型转换方式,用于在相关类型之间进行转换,如非多态类型的转换、整型和枚举型转换、void*和指针类型之间进行转换等。
  2. dynamic_cast:这是一种安全的多态类型转换运算符,仅适用于含有虚函数的类层次结构。它在运行时对类型信息进行检查,如果转换不可能完成,则会返回nullptr。
  3. const_cast:常用于移除常量性(constness)或者易变性(volatileness)。例如,可以将const int转换为int,或者将volatile int转换为int(volatile1.禁止优化2.保证内存访问顺序)。
  4. reinterpret_cast:完成完全无关类型的指针之间或指针和数之间的互转。就是把内存里的值重新解释*

B+树的优点

  1. 更适合磁盘或其他块存储操作: 在 B+ 树中,所有记录节点都保持在叶子节点,并且这些叶子节点形成了一个链接列表。这意味着对整个范围的搜索可以在磁盘上进行顺序访问,而不是随机访问,从而显著提高了效率。

  2. 查询效率更稳定: 在 B 树中,由于非叶节点也包含关键字信息,因此可能需要深入到某个分支才能找到我们要的数据,也就是说查询效率是不平衡的。但在 B+树中,所有查询都要查找到叶子节点,查询路径长度相同,查询时间具有稳定性。

  3. 插入和删除简单: 由于所有的数据值都在叶子节点,因此更改树(如插入和删除)将只影响叶节点,这种变化相对于 B 树来说更加容易处理。

  4. 利于大范围的查询: B+ 树的叶子节点之间通过指针连接,必要时可以做到全表扫描。这在需要大范围的元素查找时,比 B 树具有更高的效率。

  5. 每个节点存储更多的关键字: 在 B+树中,内部节点不保存数据信息,只保存关键字信息,因此每个节点可以保存更多的关键字,树的高度更低,有助于减少 I/O 次数。

请你说下 fork 函数中父进程和子进程的内容差异

  • 复制了父进程的内容: 当调用 fork() 时,子进程继承了父进程的代码、数据段、堆和栈等内存布局,打开的文件描述符,还有环境变量。

  • 新创建或者更改的内容: 子进程拥有自己唯一的进程 ID,其父进程 ID 设置为原进程的 ID。子进程不会继承父进程的子进程,所有待处理的信号都被清除。

  • 共享的内容: 虽然子进程拷贝了父进程的文件描述符,但这些文件描述符指向的是相同的文件表项,因此父子进程实际上是共享文件的。

迭代器什么时候会失效?

  • 当容器调用erase()方法后,当前位置到容器末尾元素的所有迭代器全部失效。
  • 当容器调用insert()方法后,当前位置到容器末尾元素的所有迭代器全部失效。
  • 如果容器扩容,在其他地方重新又开辟了一块内存。原来容器底层的内存上所保存的迭代器全都失效了,如 vector pushback

STL 有几大组件?

  • 容器:一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。
  • 算法:STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 中,少部分位于头文件 中。
  • 迭代器:在 c++STL 中,对容器中数据的读和写,是通过迭代器完成的,扮演着容器和算法之间的胶合剂。
  • 函数对象:如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)。
  • 适配器:可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。
  • 内存分配器:为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。

c++从源代码文件到可执行文件的经历过程

  • 预编译:
    • 将所有的#define 删除,并且展开所有的宏定义
    • 处理所有的条件预编译指令,如#if、#ifdef
    • 处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。
    • 过滤所有的注释
    • 添加行号和文件名标识。
  • 编译
    • 词法分析:将源代码的字符序列分割成一系列的记号。
    • 语法分析:对记号进行语法分析,产生语法树。
    • 语义分析:判断表达式是否有意义。
    • 代码优化:
    • 目标代码生成:生成汇编代码。
    • 目标代码优化:
  • 汇编:这个过程主要是将汇编代码转变成机器可以执行的指令。
  • 链接:将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接。
    • 静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows 下以.lib 为后缀,Linux 下以.a 为后缀。
    • 动态链接,是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当你删除动态库时,可执行程序就不能运行。生成的动态链接库,Windows 下以.dll 为后缀,Linux 下以.so 为后缀。

简述一下 C++11 中 Lambda 新特性

  1. 利用 lambda 表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象;
  2. 每当你定义一个 lambda 表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个 lambda 表达式就会返回一个匿名的闭包实例,其实一个右值。

所以,我们上面的 lambda 表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为 lambda 捕捉块。

  1. lambda 表达式的语法定义如下:
[capture] (parameters) mutable ->return-type {statement};
即 [捕获列表](参数)mutable -&gt; 返回值 {函数体}
  1. lambda 必须使用尾置返回来指定返回类型,可以忽略参数列表和返回值,但必须永远包含捕获列表和函数体;

多态实现的原理

  • 静态多态:编译器根据实参类型来推断调用哪个函数,如果有就调用,没有就报错;
  • 动态多态:非虚函数总是在编译时根据调用该函数的对象,引用或指针的类型而确定。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定或指针所指向的对象所属类型定义的版本。需要动态绑定条件:
    • 1、虚函数。基类中必须有虚函数,派生类中必须重写虚函数。
    • 2、通过基类类型的指针或引用来调用虚函数。

为什么构造函数不能是虚函数

1、 在对象创建时,虚函数的调用需要通过虚表实现,但是此时对象还没有创建完成,虚表也未被构造,因此无法调用虚函数。

2、 此外,将构造函数声明为虚函数也没有意义,因为构造函数的目的是创建对象并初始化其状态,而虚函数的目的是在运行时通过对象的实际类型来调用其相应的实现。因此,构造函数不能是虚函数。

虚函数和构造函数可以抛出异常吗?

构造函数可以抛出异常,虚函数也可以抛出异常。在构造函数中,如果出现错误,例如初始化失败,可以抛出异常。这将使对象的创建失败,并允许调用方处理异常。同样,在析构函数中,如果出现错误,例如释放资源失败,也可以抛出异常。

需要注意的是,在构造函数中抛出异常可能会导致对象没有完全构造完成就被销毁,因此可能需要手动管理资源以避免内存泄漏。在析构函数中抛出异常也可能会导致未能正确地清理对象,并可能会导致资源泄漏。因此,应该谨慎地在构造函数和析构函数中使用异常。

C++11 新语法

智能指针 ,lambda 表达式,auto 自动推到类型,右值引用和移动语义,列表初始化, Range-based for loops, 智能指针,Lambda 表达式

列表初始化的优点

  1. 更一致的语法
  2. 防止窄化转换(Narrowing conversions)如 int pi = 3.14 如果列表初始化 pi{3.14} 会报错
  3. 清楚地表示想要调用的构造函数
std::vector<int> v(5, 10);   // 调用第一个构造函数,创建一个有5个元素,每个元素值为10的vector
std::vector<int> w {5, 10};  // 调用第二个构造函数,创建一个包含两个元素5和10的vector

C++ map 的底层

map 的底层是自平衡二叉树(红黑树)
1、每个节点要么是红色,要么是黑色
2、根节点是黑色,叶子结点是黑色(nil)节点
3、如果一个节点是红色,那么他的子节点是黑色
4、每个节点,从该节点到后代叶子结点中均包含相同数量的黑色节点

C++ static 关键字

在 C++中,static 关键字可以用来修饰全局变量,也可以用来修饰类的成员变量和成员函数。当 static 关键字用来修饰全局变量时,它的含义和普通的全局变量有所不同。具体来说,static 关键字可以让这个变量只在该编译单元内并且程序运行一直存在,在内存中被放在静态变量区。

与普通的全局变量不同,使用 static 修饰的全局变量只能在定义它的编译单元中访问,其他的编译单元无法访问这个变量。这种方式可以避免全局变量在不同的编译单元中重名的问题,同时也可以提高程序的安全性和可维护性。

C++中的构造函数和析构函数可以声明为 inline 吗

inline函数是一种特殊的函数,可以在调用处直接将其代码插入到程序中,从而避免函数调用的开销。在 C++中,可以使用inline关键字来声明一个函数为inline函数。通常情况下,适合使用inline函数的函数包括:函数体比较小;频繁调用的函数等。
构造函数和析构函数也可以声明为inline函数,但是通常情况下不建议这样做。因为构造函数和析构函数的执行通常比较复杂,包括调用父类构造函数、初始化成员变量、释放资源等操作,如果将它们声明为inline函数,会导致程序变得难以维护。此外,由于构造函数和析构函数的调用是自动的,因此与普通函数的调用开销相比,这种优化效果相对较小,因此一般不建议将构造函数和析构函数声明为inline函数。

C++ 中 sizeof 数组

在 C++ 中,sizeof 数组返回的是整个数组所占用的内存空间的字节数。具体来说,如果数组是一个静态数组,那么 sizeof 数组返回的是该数组中所有元素所占用的总内存空间的字节数;如果数组是一个指针,则 sizeof 数组返回的是指针本身所占用的内存空间的字节数,而不是指针所指向的内存空间的字节数。

下面是一些示例代码,用于说明 sizeof 数组的使用方法:

#include <iostream>

using namespace std;

int main() {
    int arr[5] = {1, 2, 3, 4, 5};

    // sizeof 数组返回的是数组中所有元素所占用的总内存空间的字节数
    cout << "sizeof(arr) = " << sizeof(arr) << endl; // 输出 20,即 5 个 int 类型变量所占用的总内存空间的字节数

    // sizeof 指针返回的是指针本身所占用的内存空间的字节数
    int* ptr = arr;
    cout << "sizeof(ptr) = " << sizeof(ptr) << endl; // 输出 8,即 64 位系统中指针所占用的内存空间的字节数

    return 0;
}

需要注意的是,sizeof 数组的结果与数组的元素类型和元素个数有关,而与数组的内容无关。例如,如果数组中包含了指针类型的元素,那么 sizeof 数组返回的结果将包含指针本身所占用的内存空间的字节数,而不仅仅是指针所指向的内存空间的字节数。

c++中 strlen 和 sizeof 字符串

在 C++ 中,strlen 函数返回的是一个字符串的长度,即该字符串中字符的个数,但不包括字符串末尾的空字符('\0')。而 sizeof 操作符返回的是一个字符串所占用的内存空间的字节数,包括字符串末尾的空字符('\0')。

下面是一些示例代码,用于说明 strlen 和 sizeof 字符串的使用方法:

#include <iostream>
#include <cstring>

using namespace std;

int main() {
    char str1[] = "hello";
    char str2[] = {'w', 'o', 'r', 'l', 'd', '\0'};
    char* str3 = "C++";

    // strlen 返回字符串的长度,不包括字符串末尾的空字符('\0')
    cout << "strlen(str1) = " << strlen(str1) << endl; // 输出 5,即 "hello" 中字符的个数
    cout << "strlen(str2) = " << strlen(str2) << endl; // 输出 5,即 "world" 中字符的个数
    cout << "strlen(str3) = " << strlen(str3) << endl; // 输出 3,即 "C++" 中字符的个数

    // sizeof 返回字符串所占用的内存空间的字节数,包括字符串末尾的空字符('\0')
    cout << "sizeof(str1) = " << sizeof(str1) << endl; // 输出 6,即 "hello" 所占用的内存空间的字节数,包括 '\0'
    cout << "sizeof(str2) = " << sizeof(str2) << endl; // 输出 6,即 "world" 所占用的内存空间的字节数,包括 '\0'
    cout << "sizeof(str3) = " << sizeof(str3) << endl; // 输出 8,即指针所占用的内存空间的字节数

    return 0;
}

需要注意的是,如果字符串中包含了 Unicode 字符或多字节字符,strlen 函数返回的长度可能会不正确,因为它只会计算单字节字符的数量。此时可以使用 Unicode 编码或多字节编码相关的库函数来计算字符串的长度。另外,如果字符串中没有包含空字符('\0'),那么 sizeof 字符串返回的结果将不包括空字符所占用的字节数。

设计模式

设计模式是一种被经过验证的、被证明在特定情况下可行的解决问题的方法。常见的设计模式有以下几种:

  1. 工厂模式(Factory Pattern):用于创建对象的模式,通过工厂类封装对象的创建过程,将对象的创建和使用分离开来。

  2. 单例模式(Singleton Pattern):用于保证一个类只有一个实例,通过私有化构造函数、提供静态方法等方式实现。

  3. 代理模式(Proxy Pattern):用于控制对对象的访问,通过代理对象对真实对象的访问进行控制和增强。

  4. 观察者模式(Observer Pattern):用于对象间的一对多依赖关系,当一个对象状态发生改变时,它的所有依赖者都会收到通知并自动更新。

  5. 装饰器模式(Decorator Pattern):用于动态地给一个对象添加一些额外的功能,通过嵌套多个装饰器实现功能的叠加。

  6. 策略模式(Strategy Pattern):用于定义一系列算法,将每个算法都封装起来,并使它们之间可以互换。

  7. 模板方法模式(Template Method Pattern):用于定义一个算法框架,将具体的实现留给子类来实现。

  8. 适配器模式(Adapter Pattern):用于将一个类的接口转换成客户端所期望的另一个接口,使得原本不兼容的接口可以一起工作。

  9. 迭代器模式(Iterator Pattern):用于遍历集合或容器中的元素,将遍历算法与集合本身分离开来,从而提高代码的可复用性和可维护性。

  10. 建造者模式(Builder Pattern):用于将一个复杂的对象的构建过程和它的表示分离开来,使同样的构建过程可以创建不同的表示。

malloc 底层实现

malloc 函数的内部实现通常是通过维护一个空闲链表来管理可用的内存块。当调用 malloc 函数时,它会遍历空闲链表,查找一个大小足够的内存块,并返回该内存块的起始地址。如果没有足够大的内存块,malloc 函数会向操作系统申请一段新的内存空间,并将其添加到空闲链表中。

当调用 free 函数释放内存时,malloc 函数会将该内存块添加到空闲链表中,并进行内存合并操作,将相邻的空闲内存块合并成一个更大的内存块,以便于后续的内存分配。

placement new

placement new 是一种 C++中的特殊形式的 new 操作符,它可以在已经分配好的内存块上构造一个对象,而不需要再分配新的内存空间。

在 C++中,通常使用 new 操作符来动态分配内存并构造对象。new 操作符会先调用 operator new 函数分配内存,然后调用对象的构造函数来初始化对象。而 placement new 可以直接在已经分配好的内存块上调用对象的构造函数来初始化对象,而不需要再分配新的内存空间。

placement new 的语法如下:

void* operator new(size_t size, void* ptr);

其中,第一个参数表示要分配的内存块大小,第二个参数表示指向已经分配好的内存块的指针。使用 placement new 时,需要在已经分配好的内存块上调用构造函数来初始化对象,示例如下:

void* mem = malloc(sizeof(MyClass));  // 分配内存
MyClass* obj = new(mem) MyClass();    // 在内存块上构造对象

在上面的示例中,我们首先使用 malloc 函数分配了一个大小为 MyClass 的内存块,然后使用 placement new 在这个内存块上构造了一个 MyClass 对象。需要注意的是,使用 placement new 时,需要将分配的内存地址作为构造函数的参数传递给 new,这样构造函数就会在这个内存块上进行初始化,而不是分配一个新的内存块。

需要注意的是,使用 placement new 构造对象时,需要手动调用对象的析构函数来释放对象占用的资源,示例如下:

obj->~MyClass();   // 手动调用析构函数释放资源
free(mem);         // 释放内存

总之,placement new 是一种特殊形式的 new 操作符,可以在已经分配好的内存块上构造对象,而不需要再分配新的内存空间。

动态链接中的全局作用域表

在程序链接过程中,全局偏移表(Global Offset Table,简称 GOT)是动态链接的重要组成部分。GOT 主要用于实现位置无关代码(Position Independent Code,简称 PIC),这在动态链接库(Dynamic Shared Object,简称 DSO)或者 Position Independent Executable(简称 PIE)中非常重要。

全局偏移表的主要作用如下:

存储全局数据的地址: 对于动态链接库来说,由于其在内存中的位置在加载时才能确定,因此不能在编译时直接使用绝对地址访问全局数据。为解决这个问题,编译器会生成一张全局偏移表来存储所有全局数据的地址,然后通过相对于这张表的偏移来访问具体的数据。

支持动态函数调用: 全局偏移表还可以支持动态函数调用。程序在运行时,如果需要调用一个动态链接库中的函数,实际上会先查找全局偏移表,根据表中存储的函数地址来进行函数调用。具体的函数地址在程序初次调用时由动态链接器填入。

总的来说,全局偏移表是实现动态链接和位置无关代码的重要技术手段,它让我们可以在运行时动态地确定和修改全局数据和函数的地址。

正则表达式

regex_match(string, regex("pattern"))
返回 bool 值,代表是否匹配成功;

虚继承是如何实现的

  1. 虚基类指针: 每个实例对象中都会有一个指向虚基类的指针(通常位于对象内存布局的开始处),这就是虚基类指针。

  2. 虚基类表: 虚基类表是一个保存了虚基类相关信息的表格,包括虚基类与派生类的偏移量等信息。

  3. 访问虚基类成员: 当我们访问虚基类的成员时,首先会通过虚基类指针找到虚基类表,然后通过虚基类表找到虚基类成员相对于派生类的偏移,最后根据这个偏移找到虚基类成员。

说下红黑树

红黑树是一种自平衡二叉查找树,它是一种二叉树,其中每个节点都带有一个额外的信息:节点的颜色,可以是红色或黑色。

红黑树的性质如下:

  • 每个节点要么是红色,要么是黑色。
  • 根节点是黑色的。
  • 每个叶子节点(NIL 节点,空节点)都是黑色的。
  • 如果一个节点是红色的,则它的子节点必须是黑色的。
  • 从任意一个节点到其每个叶子节点的所有路径都包含相同数目的黑色节点

为什么用红黑树不用 AVL 树

Map 使用红黑树而不是 AVL 树的原因主要是在于红黑树相比于 AVL 树有更好的性能表现,尤其是在插入和删除操作较为频繁的情况下。
红黑树和 AVL 树都是自平衡二叉查找树,它们都可以保证在最坏情况下的时间复杂度为 O(log n),但是它们的平衡策略不同。AVL 树保证左右子树的高度差不超过 1,而红黑树则保证黑色节点的高度差不超过 2。
相比于 AVL 树,红黑树的平衡性要稍微差一些,但是它的旋转和变色操作更少,因此在插入和删除操作较为频繁的情况下,红黑树的性能更好。此外,红黑树的实现也更加简单,易于理解和实现。
因此,Map 使用红黑树而不是 AVL 树,主要是考虑到红黑树在实际应用中具有更好的性能表现和更简单的实现方式。

宏定义和 inline

  • inline 在编译期间展开,而宏在预编译时进行文本替换
  • inline 标识的是一个函数,采用的是参数传递,而宏定义的是文本替换的一种方式
  • 内联函数会进行类型安全检查、语法判断,而宏在文本替换后可能会导致编译错误
  • 宏是无类型的,将其用于任何类型,运算都是有意义的,内联函数则需要通过创建模板使得函数独立于类型

#ifdef __cplusplus

ifdef __cplusplus 是 C++中的预处理指令,用于检查是否在 C++ 环境中编译代码。

这个预处理指令的作用主要是解决 C 语言和 C++ 语言之间的兼容性问题。因为 C++ 是 C 的超集,C++ 编译器能够理解 C 代码,但是 C 编译器并不能理解所有 C++ 代码。所以如果你正在编写一个既需要在 C 环境中编译,也需要在 C++ 环境中编译的头文件,就需要用到 #ifdef __cplusplus。

其使用方式一般如下:

#ifdef __cplusplus
extern "C" {
#endif
// C code here
#ifdef __cplusplus
}
#endif

上述代码中,extern "C" 告诉 C++ 编译器按照 C 语言的规则来编译和链接该部分的代码。这样做的好处是可以防止 C++ 编译器将函数名进行名字修饰(mangle),使得在链接时能够找到正确的函数实现。

当编译器遇到 #ifdef __cplusplus 时,如果当前环境是 C++,那么它会处理 #ifdef 和 #endif 之间的内容。否则,它会忽略这部分内容。

explicit 作用

explicit 关键字用于防止 C++ 编译器执行隐式转换构造函数。使用 explicit 关键字定义的构造函数只能显式地进行类型转换,不能在隐式转换的情况下被调用。

class MyClass {
public:
  operator int() const { return m_x; }
private:
  int m_x;
};

int main() {
  int x = MyClass(42);  // 编译错误:无法将 MyClass 隐式转换为 int
  int y = static_cast<int>(MyClass(42));  // 显式类型转换
  return 0;
}

delete 构造函数有什么用

将构造函数和析构函数设置为 delete 可以在编译时防止对象的创建和销毁,这可以用于以下几种情况:

  1. 禁止对象的拷贝和移动
    如果一个类需要禁止对象的拷贝和移动,可以将其复制构造函数、移动构造函数、复制赋值运算符和移动赋值运算符都设置为 delete。这样,当其他代码试图拷贝或移动对象时,编译器将会产生错误。

  2. 实现单例模式
    如果一个类只需要一个对象实例,可以将其构造函数设置为 private 或 protected,并将其复制构造函数、移动构造函数、复制赋值运算符和移动赋值运算符都设置为 delete。然后,在类中定义一个静态成员变量和一个静态方法来获取唯一的对象实例。这样,其他代码就无法创建多个对象实例。

C++写一个单例模式

class Singleton {
public:
  // 获取单例对象的全局访问点
  static Singleton& getInstance() {
      static Singleton instance;
      return instance;
  }

  // 禁止拷贝构造函数和赋值运算符
  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

  // 单例类的其他方法和属性
  void doSomething() {
      // ...
  }

private:
  // 私有构造函数,禁止外部实例化
  Singleton() {
      // ...
  }

  // 私有析构函数,禁止外部删除实例
  ~Singleton() {
      // ...
  }
};

枚举和宏定义

枚举和宏定义都是 C/C++中定义常量的方式,但它们之间有以下的异同:

异同点:

  1. 都可以用于定义常量,可以提高程序的可读性和可维护性。

  2. 都可以用于定义常量字符串等复杂类型。

  3. 都可以在程序中多次使用。

不同点:

  1. 宏定义是在预处理阶段进行替换,是一种文本替换技术,而枚举是在编译阶段进行处理,是一种类型安全的常量定义方式。

  2. 宏定义没有类型检查,容易出现类型错误,而枚举可以避免这种问题

  3. 宏定义可以定义任何类型的常量,而枚举只能定义整型常量。

  4. 宏定义可以被重新定义,而枚举不可以。

  5. 枚举可以用于 switch 语句中,使得代码更加简洁易读。

综上所述,枚举和宏定义都可以用于定义常量,但是枚举具有类型安全和可读性好的特点,而宏定义则更加灵活,可以定义任何类型的常量。在实际开发中,应该根据具体情况选择使用哪种方式,遵循代码规范和设计原则,提高程序的可维护性和可读性。

inline 的作用

,它提示编译器将函数体内的代码直接插入到调用该函数的地方,而不是通过函数调用的方式执行。这样做可以减少函数调用的开销,提高程序的执行效率。
inline 关键字只是向编译器发出提示,是否将函数作为内联函数实现,最终是否内联由编译器决定。

inline 函数必须在头文件中定义,否则编译器无法在调用处将函数代码插入到程序中。

inline 函数不能包含复杂的控制语句,如循环或递归,否则会导致内联函数的代码量过大,反而会降低程序执行效率。

对于虚函数和递归函数,即使使用 inline 关键字,编译器也不会将其内联。

inline 函数中不能使用 static 关键字,因为 static 表示函数仅可在当前文件中使用,而 inline 函数需要在多个文件中共享。

unique_ptr 底层实现

本质上就是 RAII,注意要把拷贝构造函数和移动构造函数删除

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

    ~unique_ptr() { delete ptr_; }

    unique_ptr(const unique_ptr&) = delete;            // 禁止复制构造
    unique_ptr& operator=(const unique_ptr&) = delete; // 禁止赋值操作

    unique_ptr(unique_ptr&& u) noexcept {            // 移动构造函数
        ptr_ = u.release();
    }

    unique_ptr& operator=(unique_ptr&& u) noexcept { // 移动赋值运算符
        reset(u.release());
        return *this;
    }

    T* get() const noexcept { return ptr_; }

    T* release() noexcept {               // 释放所有权
        T* result = ptr_;
        ptr_ = nullptr;
        return result;
    }

    void reset(T* p = nullptr) noexcept {   // 重置内部指针
        delete ptr_;
        ptr_ = p;
    }

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

private:
    T* ptr_;
};

noexcept 的作用

noexcept 是 C++11 中引入的一个关键字,用于表示函数是否会抛出异常。具体来说,如果一个函数被声明为 noexcept,则表明该函数不会抛出任何异常,包括 C++ 异常和操作系统异常。

noexcept 关键字主要有以下作用:

优化代码性能:noexcept 声明可以让编译器针对不抛出异常的函数进行优化,从而提高代码的性能。

提高代码可靠性:使用 noexcept 声明可以让代码更加可靠,因为调用 noexcept 函数不会抛出异常,可以避免程序异常终止等问题。

改善代码风格:使用 noexcept 声明可以使代码更加简洁,更容易理解,因为它明确了函数的异常保证,可以避免需要额外的代码来处理异常的情况。

需要注意的是,如果一个 noexcept 函数在运行时抛出异常,将会导致 std::terminate 函数被调用,从而导致程序终止。

C++内存模型

栈区(Stack):由编译器自动分配和释放,存储局部变量、函数参数和返回地址等信息。栈区的内存分配和释放速度非常快,但是大小有限,通常只能存储较小的数据。

堆区(Heap):由程序员手动分配和释放,存储动态分配的内存,大小不受限制。堆区的内存分配和释放速度较慢,且容易产生内存泄漏和内存碎片等问题。

全局区(Global):存储全局变量和静态变量,程序启动时分配,程序结束时释放。全局区的内存分配和释放速度较快,但是容易产生命名冲突和数据共享等问题。

常量区(Constant):存储常量字符串和全局常量等,不允许修改。常量区的内存分配和释放由编译器自动处理,通常位于代码段或只读数据段。

代码区(Code):存储程序的可执行代码,由操作系统加载到内存中执行。代码区的内存分配和释放由操作系统管理,通常位于只读代码段。

堆 heap :
由new分配的内存块,其释放编译器不去管,由我们程序自己控制(一个new对应一个delete)。如果程序员没有释放掉,在程序结束时OS会自动回收。涉及的问题:“缓冲区溢出”、“内存泄露”

栈 stack :
是那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。
存放在栈中的数据只在当前函数及下一层函数中有效,一旦函数返回了,这些数据也就自动释放了。

全局/静态存储区 (.bss段和.data段) :
全局和静态变量被分配到同一块内存中。在C语言中,未初始化的放在.bss段中,初始化的放在.data段中;在C++里则不区分了。

常量存储区 (.rodata段) :
存放常量,不允许修改(通过非正当手段也可以修改)

代码区 (.text段) :
存放代码(如函数),不允许修改(类似常量存储区),但可以执行(不同于常量存储区)

模板可以使用选虚函数吗,为什么

可以

右值引用解决了什么问题

左值(Lvalue):它们是可以从内存中寻址的对象,或者拥有名字的对象,比如变量。
右值(Rvalue):它们是临时对象,或者不拥有名字的对象,比如返回值、字面量等。

  • 避免拷贝操作
  • 修改临时对象
int&& rvalue_ref = 1;
这个特性非常重要,因为它允许我们修改临时对象。此外,使用右值引用,我们可以将资源(如内存或文件句柄)从一个对象安全地"移动"到另一个对象,而无需进行昂贵的深拷贝操作。这对于大型对象或者只能移动的资源来说,性能提升极大。

一个最常见的例子就是 std::move,它可以将一个左值转换为右值引用,以便进行移动操作而非拷贝操作:

std::string str1 = "Hello, World.";
std::string str2 = std::move(str1);  // 将 str1 的内容"移动"到 str2
上述代码中,str1的内容被移动到str2,而不是复制过去。这样,str2现在拥有原先str1的内存,str1则处于一个有效但未定义的状态。这样做的好处就是避免了不必要的内存分配和释放操作,提高了程序的效率。

完美转发

"完美转发"是 C++11 中引入的一种新特性,它能够在函数模板中精确地保持参数的类型。也就是说,它可以将参数以原始形式(包括全部 cv 限定符和引用类型)传递给另一个函数。

在早期的 C++版本中,所有的参数都是按值传递的。这意味着在函数调用过程中,参数可能会被拷贝,且不能保持其原有的类型特性,例如是否为 const、是否为引用等。这就是所谓的"不完美转发"。

然而,在 C++11 中,引入了右值引用和 std::forward 函数,使我们可以实现"完美转发"。

举个例子:

template <typename T>
void wrapper(T&& arg) {
    // 完美转发arg到foo函数
    foo(std::forward<T>(arg));
}

在本例中,wrapper 函数接收一个万能引用(universal reference),指的是 T&&类型的参数,由 Scott Meyers 首次提出这个术语),并将其完美转发给 foo 函数。不论我们传递给 wrapper 函数的参数是左值还是右值,它们都会以原样的方式传递给 foo 函数。

这样做的优点是:

避免不必要的对象拷贝,提高程序效率。
保留参数的类型信息,例如是否为 const 或引用等,使得行为更加符合预期。
这就是所谓的"完美转发"。

智能指针的缺点

虽然智能指针在管理动态内存方面有很多优点,但是它们仍然存在一些弊端,包括:

  1. 对象所有权的问题

智能指针的设计初衷是为了解决内存管理的问题,但是智能指针本身也具有所有权的概念。在使用智能指针时,需要明确对象的所有权关系,否则会出现对象被多个智能指针所管理的情况,导致内存泄漏或多次释放同一块内存的问题。

  1. 循环引用的问题

智能指针可能导致循环引用的问题。如果两个或多个对象之间相互引用,并且它们都使用智能指针来管理内存,就会导致循环引用的问题。这种情况下,智能指针可能无法正确释放内存,从而导致内存泄漏的问题。

  1. 不支持数组的管理

智能指针不支持数组的管理。虽然可以使用 std::unique_ptr<T[]> 来管理动态分配的数组,但是 std::unique_ptr<T[]> 并不是标准智能指针的一种,它需要使用自定义删除器来释放数组的内存。

  1. 性能问题

智能指针的实现需要使用额外的内存和计算时间,可能会导致一定的性能开销。此外,在多线程环境下,智能指针的引用计数可能需要使用原子操作,也会对性能产生一定的影响。

综上所述,虽然智能指针在管理动态内存方面有很多优点,但是它们也存在一些弊端。在使用智能指针时,需要注意对象的所有权关系、循环引用的问题、不支持数组的管理和性能问题等方面的考虑。

说一下 inline

inline 是 C++ 中的一个关键字,用于修饰函数,表示该函数可以被编译器内联展开

函数内联是一种编译器优化技术,它的作用是将函数调用的过程转化为函数体的直接执行,从而减少函数调用的开销,提高程序的执行效率。具体来说,编译器会将内联函数的代码复制到每个调用该函数的地方,从而避免了函数调用的开销,减少了代码的跳转次数,提高了程序的运行速度。

使用 inline 关键字修饰函数时,编译器并不一定会将函数内联展开,具体是否内联展开取决于编译器的优化策略和代码的具体情况。通常情况下,编译器会对一些简单的函数进行内联展开,如只包含一行代码的函数或者只有几行代码的函数。

需要注意的是,使用 inline 关键字修饰函数时,应该遵循以下几点原则:

内联函数的代码应该比较简单,不要包含过多的复杂逻辑和控制语句,否则会导致代码膨胀,反而降低程序的执行效率。

内联函数的代码应该放在头文件中,以便编译器能够在每个调用该函数的地方进行内联展开。

在某些情况下,使用 inline 关键字并不能提高程序的执行效率,甚至会降低程序的性能。因此,在使用 inline 关键字时,应该根据具体情况进行判断和选择。

综上所述,inline 是 C++ 中的一个关键字,用于修饰函数,表示该函数可以被编译器内联展开,从而提高程序的执行效率。在使用 inline 关键字时,应该遵循一定的原则和注意事项,以保证程序的正确性和可靠性。

inline 和宏定义的区别

  • 内联函数在编译时展开,可以做一些类型检测处理。宏在预编译时展开;内联函数直接嵌入到目标代码中,宏是简单的做文本替换。
  • C++中引入了类及类的访问控制,**在涉及到类的保护成员和私有成员就不能用宏定义来操作 **

哈希冲突解决办法

哈希处理冲突的方式主要有以下几种:

链地址法(Chaining):将哈希表中冲突的元素存储在同一个链表中,每个链表节点存储一个元素。当插入新元素时,如果发生冲突,则将新元素添加到链表的末尾。当查找元素时,首先根据哈希值找到对应的链表,然后在链表中顺序查找目标元素。链地址法是一种简单有效的哈希冲突解决方法,适用于元素数量较多的情况。

开放地址法(Open Addressing):将所有元素都存储在哈希表中,当发生冲突时,根据特定的规则查找哈希表中下一个未被占用的位置,直到找到合适的位置为止。开放地址法可以避免链表操作的开销,但需要保证哈希表中至少留有一部分空间,否则容易出现死循环。常用的开放地址法包括线性探测、二次探测和双重哈希等。

再哈希法(Rehashing):在哈希表中发生冲突时,使用第二个哈希函数重新计算哈希值,直到找到一个空槽为止。再哈希法可以避免链表操作和二次探测的缺点,但需要使用多个哈希函数,增加了计算的复杂度。

建立公共溢出区(Overflow Area):将哈希表中冲突的元素存储在一个公共的溢出区中,当插入新元素时,如果发生冲突,则将新元素添加到溢出区中。当查找元素时,首先根据哈希值找到对应的槽位,如果槽位为空,则表明目标元素不存在;如果槽位有元素,则需要在哈希表中查找目标元素,如果没有找到,则需要在溢出区中查找。

以上是主要的哈希处理冲突的方式。不同的方式适用于不同的场景,开放地址法适合元素数量较少的情况,链地址法适合元素数量较多的情况,再哈希法和建立公共溢出区则是一些特殊场景下的解决方案

虚函数可以 inline 吗

虚函数可以被声明为 inline 函数,但是它们的行为与非虚函数的 inline 函数不同。

当一个函数被声明为 inline 时,编译器会尝试将函数的代码插入到每个调用该函数的地方,从而避免函数调用的开销,提高程序的执行效率。然而,虚函数的动态绑定特性使得编译器无法在编译时确定调用哪个函数,因此它们不能像非虚函数一样被完全展开。

在某些情况下,编译器可能会将虚函数的调用转换为静态绑定,从而实现类似于 inline 函数的效果。这种情况下,虚函数会被完全展开,并且调用该函数的代码会被替换为函数的实际代码。但是,这种转换需要满足一定的条件,例如函数必须是非虚函数,或者编译器必须能够确定调用的对象类型等。

因此,虚函数可以被声明为 inline 函数,但是它们的行为与非虚函数的 inline 函数不同,而且是否能够实现真正的 inline 取决于具体的实现和调用方式。

手撕代码:写一个 string 类

以下是一个简单的 string 类的实现,包括构造函数、析构函数、拷贝构造函数、赋值运算符、下标运算符、比较运算符、长度函数和字符串连接函数。

好的,下面是一个简单的 String 类的实现,包括默认构造函数、拷贝构造函数、移动构造函数、析构函数和 operator= 运算符的实现。

#include <cstring>
#include <iostream>

class String {
public:
    // 默认构造函数
    String() : data_(nullptr), size_(0), capacity_(0) {}

    // 拷贝构造函数
    String(const String& other) : data_(nullptr), size_(0), capacity_(0) {
        reserve(other.size_);
        std::memcpy(data_, other.data_, other.size_ + 1);
        size_ = other.size_;
    }

    // 移动构造函数
    String(String&& other) noexcept : data_(other.data_), size_(other.size_), capacity_(other.capacity_) {
        other.data_ = nullptr;
        other.size_ = 0;
        other.capacity_ = 0;
    }

    // 析构函数
    ~String() {
        if (data_) {
            delete[] data_;
        }
    }

    // operator= 运算符
    String& operator=(const String& other) {
        if (this != &other) {
            reserve(other.size_);
            std::memcpy(data_, other.data_, other.size_ + 1);
            size_ = other.size_;
        }
        return *this;
    }

    // 获取字符串长度
    size_t size() const { return size_; }

    // 判断是否为空
    bool empty() const { return size_ == 0; }

    // 添加字符
    void push_back(char c) {
        if (size_ + 1 > capacity_) {
            reserve(size_ + 1);
        }
        data_[size_] = c;
        size_++;
        data_[size_] = '\0';
    }

    // 清空字符串
    void clear() {
        delete[] data_;
        data_ = nullptr;
        size_ = 0;
        capacity_ = 0;
    }

private:
    char* data_;
    size_t size_;
    size_t capacity_;

    // 分配内存
    void reserve(size_t new_capacity) {
        if (new_capacity > capacity_) {
            char* new_data = new char[new_capacity + 1];
            if (data_) {
                std::memcpy(new_data, data_, size_ + 1);
                delete[] data_;
            }
            data_ = new_data;
            capacity_ = new_capacity;
        }
    }
};

int main() {
    String s1; // 调用默认构造函数
    std::cout << "s1: " << s1.size() << "\n"; // s1: 0

    String s2 = "Hello, world!"; // 调用拷贝构造函数
    std::cout << "s2: " << s2.size() << "\n"; // s2: 13

    String s3 = std::move(s2); // 调用移动构造函数
    std::cout << "s3: " << s3.size() << "\n"; // s3: 13
    std::cout << "s2: " << s2.size() << "\n"; // s2: 0

    s1 = s3; // 调用 operator= 运算符
    std::cout << "s1: " << s1.size() << "\n"; // s1: 13

    s1.push_back('!');
    std::cout << "s1: " << s1.size() << "\n"; // s1: 14

    s1.clear();
    std::cout << "s1: " << s1.size() << "\n"; // s1: 0

    return 0;
}

在移动构造函数中,我们将 other 对象的指针、长度和容量直接转移给新对象,然后将 other 对象的指针、长度和容量置为 0 或 nullptr,以防止其析构时重复释放指针所指的内存。

capacity 是指 String 对象当前已分配的内存空间大小,它与 size 是两个不同的概念。

size 表示 String 对象中实际存储的字符数,它不包括最后的空字符('\0'),而 capacity 则表示 String 对象当前已经分配的内存空间大小(以字节为单位),它是为了在添加字符时避免频繁分配内存空间,从而提高程序的效率。

例如,在添加字符之前,如果 String 对象已经分配了足够的内存空间,那么就可以直接在已有的内存空间中添加新字符,而不必重新分配内存空间。这个过程就是通过 reserve() 函数实现的。当需要添加的字符数超过当前的 capacity 时,reserve() 函数会重新分配更大的内存空间,并将原来的数据复制到新的内存空间中。

在 String 类中,我们跟踪 capacity 的主要目的是为了实现 push_back() 函数,该函数用于向 String 对象的末尾添加一个字符。当添加的字符数超过当前的 capacity 时,它会自动重新分配更大的内存空间,从而避免了频繁的内存分配和释放操作。

i++++i的区别,哪个快?

++i 快点 i++ 和 ++i 对于基本类型(如 int,float 等)来说,在性能上没有明显的区别。现代编译器会对这种情况进行优化,使得两者的运行速度几乎相同。

但是当我们使用复杂类型(如迭代器,自定义类等)时,可能就会有不同了
在这样的情况下:

对于前缀递增 ++it,它将 it 加一,并返回增加后的值。只涉及一次操作。
对于后缀递增 it++,它首先需要创建一个临时的原始 it 的副本,然后增加 it,最后返回原始副本的值。涉及两次操作。
因此,对于复杂类型,通常 ++i 会比 i++ 更快,因为它避免了创建临时对象。

但是,请注意,大多数现代编译器对于这类代码进行了高度优化,因此在实际使用中,您可能看不到任何显著的性能差异。除非你正在编写对性能极度敏感的代码,否则在选择使用 i++ 还是 ++i 时,可读性和代码清晰度应该是更重要的考虑因素。

// 前缀形式:
int& int::operator++() //这里返回的是一个引用形式,就是说函数返回值也可以作为一个左值使用
{//函数本身无参,意味着是在自身空间内增加1的
  *this += 1;  // 增加
  return *this;  // 取回值
}

//后缀形式:
const int int::operator++(int) //函数返回值是一个非左值型的,与前缀形式的差别所在。
{//函数带参,说明有另外的空间开辟
  int oldValue = *this;  // 取回值
  ++(*this);  // 增加
  return oldValue;  // 返回被取回的值
}

一个是返回引用,另一个返回值

vector 的 clear

在 C++ 中,可以使用 vector 的 clear() 成员函数清除其内部存储的元素。调用 clear() 后,vector 的大小将变为 0,但是内存空间并不会被释放,仍然保留在 vector 中以备下次使用。其实也就是 size 会变成 0,但是 capacity 不变

不允许使用 auto 的场景

  • 不能作为函数参数
  • 类的非静态成员变量的初始化
  • 定义数组
  • 推导定义数组

constexpr 的作用

编译时求值
好处:

  • 安全性增加 。通过编译时检查避免运行时错误产生。
  • 效率提高 。可以在编译时完成复杂的构造,而无需在运行时再次构造。

尾递归

主要是空间复杂度
尾递归是指递归函数的递归调用语句是函数中的最后一条语句。

尾递归能够优化是因为:

尾递归不需要保存调用帧(call stack frame),只需要覆盖当前帧就可以了。

所以尾递归可以被编译器优化成迭代,空间复杂度可以降到 O(1)。

非尾递归需要保存每一层的调用帧,空间复杂度是 O(n),n 是递归深度。

举个例子:

尾递归:

def factorial(n, acc=1):
    if n == 0:
        return acc
    return factorial(n-1, n*acc)

非尾递归:

def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n-1)  #  这里还需要有x上一个n,不是单纯只有递归函数

第一个例子是尾递归,可以被优化。第二个不是,需要保存每一层 factorial 的调用帧,空间复杂度是 O(n)。

所以尾递归对于递归深度很大的函数来说很有用,可以避免栈溢出。许多函数式语言的编译器会自动优化尾递归。

写一个 shared_ptr

template <class T>
class SmartPoint {
private:
  T* ptr; // 指向动态分配对象的原始指针
  std::size_t* refCount; // 维护指针引用计数

  void dispose() { // 释放函数,处理内存释放逻辑
      --(*refCount);
      if (*refCount == 0) {
          delete ptr;
          delete refCount;
      }
  }

public:
  // 构造函数
  explicit SmartPoint(T* p = nullptr) : ptr(p), refCount(new std::size_t(1)) {}

  // 拷贝构造函数
  SmartPoint(const SmartPoint& sp) : ptr(sp.ptr), refCount(sp.refCount) {
      ++(*refCount);
  }

  // 赋值操作符重载
  SmartPoint& operator=(const SmartPoint& sp) {
      // 自我赋值检查
      if (this != &sp) {
          dispose(); // 使用封装的释放函数
          ptr = sp.ptr;
          refCount = sp.refCount;
          ++(*refCount);
      }
      return *this;
  }

  // 解引用操作符重载
  T& operator*() const { return *ptr; }

  // 间接访问成员操作符重载
  T* operator->() const { return ptr; }

  // 析构函数
  ~SmartPoint() {
      dispose(); // 使用封装的释放函数
  }
};

什么是野指针

野指针是一种编程术语,主要出现在 C/C++等允许直接操作内存的编程语言中。

野指针具体指的是:

已经释放(delete 或 free)但没有置为 nullptr 的指针。
未初始化的指针。
如果对这样的指针进行解引用或者访问,就可能导致程序运行错误,例如段错误(segmentation fault),甚至修改系统的关键数据,导致系统崩溃。因此,处理指针时要特别小心,尤其是在使用完毕后记得将指针置为 nullptr,以避免成为野指针。

下面是一个简单的例子:

int* ptr = new int(10); // 分配内存并初始化
delete ptr; // 释放内存
// 这时候 ptr 就是一个野指针,因为它已经被删除,但仍然持有之前分配的内存的地址

为了防止以上情况发生,应该在释放内存后立即将指针置为空:

int* ptr = new int(10); // 分配内存并初始化
delete ptr; // 释放内存
ptr = nullptr; // 将指针置为null,防止变成野指针

join 和 detach 的区别

线程的 join()和 detach()是两种不同的方法,用于管理线程的生命周期和资源回收。

join(): 当一个线程被创建并开始执行后,调用该线程的 join()方法会使当前线程等待,直到被调用的线程执行完毕。换句话说,join()方法会阻塞当前线程,直到被调用的线程完成其任务。这意味着在调用线程的 join()方法之后,程序会暂停执行,直到被调用的线程结束。

detach(): 当一个线程被创建并开始执行后,可以调用该线程的 detach()方法来分离它。分离线程后,它将变成一个“后台线程”,不再与主线程有关联。当一个线程被分离后,它将独立地运行,不再受主线程控制。一旦一个线程被分离,就无法再使用 join()方法等待它的完成,也无法获取它的返回值。

简而言之,join()方法用于等待一个线程的完成,并获取它的结果,而 detach()方法用于将一个线程从主线程中分离出来,使其独立运行。需要注意的是,如果不显式地调用 join()或 detach()方法,当主线程结束时,所有仍然运行的线程都会被自动分离。

make_shared 有什么好处

在 C++中,shared_ptr 是一个智能指针,用于实现共享所有权的概念。它可以有多个 shared_ptr 指针指向相同的对象。这是一种非常好的方式来处理由动态分配创建的对象,因为当最后一个 shared_ptr 停止指向对象时,对象就会被删除。

创建 shared_ptr 有两种常见的方法:

方法一:使用 new 操作符

std::shared_ptr<int> p1(new int(5));

这种方式直接使用 new 操作符申请内存并初始化 shared_ptr。这种方式虽然简单明了,但是如果在 new 操作和 shared_ptr 构造之间抛出异常,可能会导致资源泄露。

方法二:使用 std::make_shared 函数

std::shared_ptr<int> p2 = std::make_shared<int>(5);

make_shared 函数模板将用其参数来构造给定类型的对象,并返回此对象的 shared_ptr。这种方式更优,因为它提供了异常安全。还有一个额外的性能优势,即这种方式只进行一次动态内存分配,而 new 操作符则需要两次。此外,make_shared 更高效,原因是它同时分配内存以适应控制块和用户数据,这样可以减少内存碎片和内存管理开销。

对比:

  • 异常安全:std::make_shared 具有更好的异常安全性。
  • 性能:std::make_shared 只需要一次内存分配,而直接使用 new 需要两次,所以 std::make_shared 性能更优。
  • 内存管理:std::make_shared 同时分配内存以适应控制块和用户数据,可以减少内存碎片和内存管理开销。
    在谈论 std::make_shared 提供的安全性时,我们通常指的是异常安全。这主要涉及到两个因素:

内存泄漏:当我们使用 new 操作符创建一个对象,然后将其传递给 std::shared_ptr 时,如果在这两个步骤之间发生异常,那么新分配的对象可能无法被正确删除,从而导致内存泄漏。例如:

cpp
try {
    std::shared_ptr<int> p(new int(5));
} catch (...) {
    // 如果在 shared_ptr 构造函数执行之前发生异常,那么新分配的 int 对象就会泄漏
}

在上面的代码中,如果在 new int(5) 和 std::shared_ptr 构造函数之间有一个异常被抛出,那么新分配的内存无法被正确回收。但是,如果我们使用 std::make_shared,由于对象的创建和 shared_ptr 的构造是一次原子操作,所以不会出现这种问题。

强异常安全保证:即使在异常发生时,std::make_shared 也能保持程序状态的完整性。它遵循了"构造-复制-销毁"(construct-copy-destroy)惯例,其中所有可能失败的操作(如内存分配)都在修改任何数据之前完成。这意味着在异常发生时,不会有半构造的 shared_ptr 留下。

因此,std::make_shared 更“安全”,因为它提供了更好的异常安全性,尤其是在内存分配或其他可能失败的操作中。

什么是虚假唤醒,如何解决

在 C++中,虚假唤醒是指当一个线程在等待某个条件变量时,即使没有收到显式的通知,也可能被唤醒。这可能会导致程序行为异常或者错误。

解决虚假唤醒的方法通常是使用循环来检查预期的条件,而非仅靠单个 if 语句。这样,即使发生虚假唤醒,线程会再次检查该条件,发现条件并未满足,则继续等待。

例如:

std::unique_lock<std::mutex> lock(mtx);
while(!condition) {
    cv.wait(lock);
}

也可以使用 lambda 表达式来解决,不用 while 循环,只有在 g_deque 不为空的情况下才会返回 true

cv.wait(lock, [] {return !q.empty(); });

手撕 string 类

#include <iostream>
#include <cstring>

class MyString {
private:
    char* m_data; // 用于存储字符串的字符数组

public:
    // 默认构造函数
    MyString() : m_data(nullptr) {}

    // 带参构造函数
    MyString(const char* str) {
        if (str != nullptr) {
            size_t length = std::strlen(str);
            m_data = new char[length + 1];
            std::strcpy(m_data, str);
        } else {
            m_data = nullptr;
        }
    }

    // 拷贝构造函数
    MyString(const MyString& other) {
        if (other.m_data != nullptr) {
            size_t length = std::strlen(other.m_data);
            m_data = new char[length + 1];
            std::strcpy(m_data, other.m_data);
        } else {
            m_data = nullptr;
        }
    }

    // 移动构造函数
    MyString(MyString&& other) noexcept {
        m_data = other.m_data;
        other.m_data = nullptr;
    }

    // 析构函数
    ~MyString() {
        delete[] m_data;
    }

    // 重载赋值运算符
    MyString& operator=(const MyString& other) {
        if (this != &other) {
            delete[] m_data;

            if (other.m_data != nullptr) {
                size_t length = std::strlen(other.m_data);
                m_data = new char[length + 1];
                std::strcpy(m_data, other.m_data);
            } else {
                m_data = nullptr;
            }
        }
        return *this;
    }

    // 输出字符串
    void print() {
        if (m_data != nullptr) {
            std::cout << m_data << std::endl;
        } else {
            std::cout << "(Empty)" << std::endl;
        }
    }
};

int main() {
    MyString str1; // 默认构造函数
    str1.print(); // 输出为空

    MyString str2("Hello"); // 带参构造函数
    str2.print(); // 输出 "Hello"

    MyString str3 = str2; // 调用拷贝构造函数
    str3.print();

    MyString str4 = std::move(str2); // 调用移动构造函数
    str4.print();
    str2.print(); // 输出为空,因为数据被移动

    return 0;
}

内联函数

内联函数是一种用于优化程序性能的 C++特性。它允许编译器将函数的代码插入调用它的地方,而不是通过常规的函数调用机制进行调用。这可以减少函数调用的开销,从而提高程序的执行效率

  • 函数体简短:内联函数适用于函数体较短的函数,通常不宜超过 10 行左右。
  • 频繁调用的函数:如果一个函数被频繁调用,可以考虑将其定义为内联函数,以减少函数调用的开销。

限制:

内联函数的代码较长会导致代码膨胀,因为每次调用都会复制一份函数体到调用位置,这可能会增加可执行文件的大小

内联函数不能包含复杂的控制结构,比如循环和递归,因为这些会使得函数体过长,影响性能。

什么情况下编译器可能不会内联一个内联函数?
回答:编译器可能不会内联一个内联函数的情况包括:

  • 函数体过长:如果函数体较长,编译器可能认为内联会导致代码膨胀,从而选择不进行内联。
  • 递归函数:内联函数不能包含递归调用,所以递归函数不会被内联。
  • 虚函数:虚函数通常在运行时动态绑定,所以不适合内联。

map

  1. std::map 底层实现:
    • 红黑树:std::map 的标准实现通常是使用红黑树。红黑树是一种自平衡二叉查找树,它具有以下特性:
      • 每个节点都带有颜色属性,可以是红色或黑色。
      • 根节点是黑色的。
      • 每个叶子节点(NIL 节点,即空节点)是黑色的。
      • 如果一个节点是红色的,则它的两个子节点都是黑色的。
      • 从根节点到每个叶子节点的路径上,黑色节点的数量是相同的。
  2. 空间复杂度:
    • std::map 使用红黑树作为底层数据结构时,空间复杂度为 O(N),其中 N 是 std::map 中存储的键值对数量。每个键值对需要一个节点来存储,同时可能还有一些额外的指针和元数据。
    • 需要注意的是,红黑树作为自平衡二叉查找树,相对于其他平衡二叉查找树(如 AVL 树),其节点所带的额外颜色属性会增加空间开销,但它可以提供较好的平衡性能,保持树的高度较低,从而保证常数时间的查找、插入和删除操作

c++和 java 的区别

内存管理: - c++需要程序员手动管理内存,new 分配,delete 释放,这样更加灵活。 - java 内置 gc,不需要手动管理

平台依赖性: - c++生成机器码,他是平台相关 - java 是字节码可以在任何 java 虚拟机上运行

面对对象: - c++ 支持多重继承 - java 不支持

讲一讲c++的new

new的步骤

  1. 根据传入的参数计算需要分配的内存大小
  2. 分配内存
  3. 调用构造函数
  4. 返回指针

失败会返回nullptr

抛出异常情况,会抛出std::bad_alloc

const关键字

  • 阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;

  • 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;

  • 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;

  • 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量,类的常对象只能访问类的常成员函数;

  • 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。

  • const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以访问const对象内的所有数据成员;

  • 非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员;

  • const类型变量可以通过类型转换符const_cast将const类型转换为非const类型;

  • const类型变量必须定义的时候进行初始化,因此也导致如果类的成员变量有const类型的变量,那么该变量必须在类的初始化列表中进行初始化;

  • 对于函数值传递的情况,因为参数传递是通过复制实参创建一个临时变量传递进函数的,函数内只能改变临时变量,但无法改变实参。则这个时候无论加不加const对实参不会产生任何影响。但是在引用或指针传递函数调用中,因为传进去的是一个引用或指针,这样函数内部可以改变引用或指针所指向的变量,这时const 才是实实在在地保护了实参所指向的变量。因为在编译阶段编译器对调用函数的选择是根据实参进行的,所以,只有引用传递和指针传递可以用是否加const来重载。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。

vector reserve和resize

  1. resize
  • resize(n)会改变vector的大小,使其包含n个元素。如果n大于当前的大小,那么新的元素会被添加到vector的末尾,如果n小于当前的大小,那么末尾的元素会被删除。resize会改变vector的size()。
  1. reserve
  • reserve(n)不会改变vector的大小,它只是预先分配足够的内存,以便在未来可以容纳n个元素。reserve不会改变vector的size(),但可能会改变capacity()。reserve的主要目的是为了优化性能,避免在添加元素时频繁进行内存分配。

push_back和emplace_back的区别

emplace_back通常在性能上优于push_back,因为它可以避免不必要的复制或移动操作。

  • push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素)

  • emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。

c和c++的区别

  • C++中new和delete是对内存分配的运算符,取代了C中的malloc和free。
  • 标准C++中的字符串类取代了标准C函数库头文件中的字符数组处理函数(C中没有字符串类型)。
  • C++中用来做控制态输入输出的iostream类库替代了标准C中的stdio函数库。
  • C++中的try/catch/throw异常处理机制取代了标准C中的setjmp()和longjmp()函数。
  • 在C++中,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言中是不允许的。也就是C++可以重载,C语言不允许。
  • C++语言中,允许变量定义语句在程序中的任何地方,只要在是使用它之前就可以;而C语言中,必须要在函数开头部分。而且C++不允许重复定义变量,C语言也是做不到这一点的
  • 在 C++中,除了值和指针之外,新增了引用。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的。
  • C++相对与C增加了一些关键字,如:bool、using、dynamic_cast、namespace等等

emplace_back的原理

实现原理

emplace_back 的实现原理主要体现在"就地构造"(in-place construction)上。当你使用push_back时,需要先创建对象,然后再将该对象拷贝或移动到容器中;而当你使用emplace_back时,则是直接在容器内存空间中构造对象,省去了拷贝或移动的步骤。

这个特性在处理大对象或者复杂对象时尤其有用,因为这样可以避免不必要的对象拷贝和临时对象的创建,从而提高程序的运行效率。

例如,以下代码展示了push_backemplace_back之间的区别:

cpp复制代码std::vector<std::string> vec;

// 使用push_back添加元素
vec.push_back(std::string("hello"));

// 使用emplace_back添加元素
vec.emplace_back("hello");在以上例子中,`vec.push_back(std::string("hello"))` 创建了一个临时 `std::string` 对象,并将其传给 `push_back`,然后 `push_back` 会将这个临时对象拷贝到向量中。而使用 `vec.emplace_back("hello")`,则直接在向量的内存空间中构造了一个 `std::string` 对象,没有发生任何拷贝或者临时对象的产生。

注意事项

虽然 emplace_back 在许多情况下比 push_back 更高效,但如果你已经有了一个完整的对象,并希望将它添加到容器中,那么 push_back 可能会更合适,因为这时候 emplace_back 就无法利用它的“就地构造”优势了。

此外,emplace_back 本身并不能保证一定比 push_back 快,其性能提升取决于对象的复制/移动成本以及编译器的优化程度。

float是如何存储的

我们来看一个具体的例子,即如何将浮点数 "58.5" 存储为一个 IEEE 754 标准的单精度(32位)浮点数。

首先,把这个十进制数转换为二进制形式。整数部分 58 转换为二进制是 111010,小数部分 0.5 转换为二进制是 .1。因此,58.5 的二进制表示为 111010.1

接下来,我们需要将它标准化为科学记数法,也就是使得只有一个非零的数字在小数点前面。所以,111010.1 变成了 1.110101 * 2^5

  • 符号位(Sign):58.5 是正数,所以符号位是 0。
  • 指数位(Exponent):由于我们将数变成了 1.xxxx * 2^5 的形式,所以 5 就是我们的指数。但是在存储时,我们需要加上偏置值(bias),对于单精度 float,偏置值是 127。因此,实际存储的指数值为 5 + 127 = 132,它的二进制形式是 10000100
  • 尾数位(Fraction/Mantissa):在标准化后的数中,1.110101 的尾数部分是 110101。由于我们的尾数部分有23位,所以需要用 0 来将其填满,变为 11010100000000000000000

所以,最后的结果是符号位、指数位和尾数位连在一起:01000010011010100000000000000000

以上就是如何将浮点数 "58.5" 存储为 IEEE 754标准的32位浮点数的过程。

mmap的原理

malloc 是一个库函数,用于在设计程序时分配内存。它是 C 和 C++ 等编程语言的一部分。当你调用 malloc 来请求内存时,操作系统会通过两种方式之一(或两者的组合)来提供内存:brkmmap

1. brk

brksbrk 是更改数据段大小的系统调用。当程序启动时,操作系统为其分配一个连续的内存块,这个内存块被划分为几个区域,其中一个重要的区域就是数据段。数据段末尾有一个叫做 "程序断点" 的特殊位置,使用 brksbrk 可以移动这个断点。

当我们对小块内存进行频繁的分配和释放时,brk 显得非常有效。例如,如果我们需要分配一些小块的内存,然后在稍后释放它们,那么使用 brk 能够避免造成大量的内存碎片。

2. mmap

相比于 brkmmap 则是一种更加灵活的内存分配方式。它可以从操作系统中请求任意大小的内存,并且这块内存不需要位于数据段附近。当我们请求大块的内存,或者希望内存能够自动释放时,使用 mmap 会更加方便。

brk 不同,mmap 分配的内存区域在进程地址空间中可能不是连续的,但在物理内存中必定是连续的。此外,mmap 还具有许多其他功能,例如文件映射。

3.选择方式

对于小块内存的管理,malloc 通常会选择线性并且连续的 brk;而对于大块内存的管理,由于需要避免内存碎片,malloc 则会选择使用 mmap 系统调用。

mmap 在 Web 服务器文件传输中的使用,主要基于以下几个原因:

mmap为什么在webserver中使用

1. 高效的文件访问

当你使用 mmap 映射一个文件到内存时,操作系统不会立即加载这个文件的全部内容。相

此外,由于这种方法避免了缓冲区的使用和多余的内存复制,所以可以提高 I/O 效率。

2. 操作简单

文件映射到内存后,就可以像操作常规内存一样操作这个文件。你可以使用指针直接读取或修改文件,无需再调用 read/write 等系统调用,简化了代码。

3. 文件共享

mmap 提供了一种很好的方式来实现进程间的文件共享。对于 Web 服务器这样的并发程序,往往会有多个进程或线程需要访问同一个文件。使用 mmap 可以使所有进程或线程都看到同样的文件内容,提高了数据一致性。

4. 零拷贝 (Zero-copy)

mmap 还能配合 Linux 中的 sendfile 系统调用实现零拷贝文件传输,大大提高了网络 I/O 的效率。sendfile 可以直接从 mmap 创建的内存页发送数据到网络,避免了数据在用户空间和内核空间之间的拷贝,降低了 CPU 的负载。

总的来说,在 Web 服务器中使用 mmap 能够提高文件操作效率,简化编程,利于共享,并且能配合其他技术(如 sendfile)进一步优化性能。

为什么把static成员变量设置成

posted @   chx9  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示