向对象八股文(长期更新_整理收集_排版未优化_day04_20个)

1、面向对象八股文(长期更新_整理收集_排版已优化_day01_20个)
2、面向对象八股文(长期更新_整理收集_排版未优化_day02_20个)
3、面向对象八股文(长期更新_整理收集_排版未优化_day03_20个)
4、面向对象八股文(长期更新_整理收集_排版未优化_day04_20个)
61、什么时候用static?
62、为什么要引入static?
63、为什么要使用智能指针?
64、堆(heap)和栈(stack)的区别?
65、程序的内存模型分为那几个区域?
66、知道如果用free去清理new出来的内存会产生什么问题吗?
67、C++多态实现的机制?(重载和重写的区别?)
68、虚函数表
69、为什么list不能使标准库算法sort()?
70、解决哈希冲突的方法有哪些?
71、容器适配器?
72、定位内存泄露
73、如何避免死锁
74、STL各容器的实现原理
75、线程之间的通信方式
76、红黑树比AVL的优势,为何用红黑树
77、在函数参数传递的时候,什么时候使用指针,什么时候使用引用?
78、 堆和栈有什么区别
79、堆快一点还是栈快一点?(字节提前批一面)

61、什么时候用static?

在 C++ 中,static 关键字有多种用法,涵盖不同的场景和目的。以下是一些常见的使用场景及其具体含义:

1. 静态局部变量

在函数内部定义的静态变量,其作用域仅限于函数内部,但它的生命周期贯穿整个程序运行期。每次调用函数时,static 局部变量不会被重新初始化,而是保留上一次的值。

2. 静态全局变量

在文件作用域(即函数外部)定义的静态变量,其作用域仅限于定义它的文件。这种使用方式可以限制全局变量的可见性,避免命名冲突。

3. 类的静态成员变量

类的静态成员变量是类级别的变量,而不是对象级别的变量。它们在所有对象之间共享,并且只有一个实例。必须在类外部定义并初始化。

4. 类的静态成员函数

静态成员函数是属于类而不是对象的成员函数,因此不能访问非静态成员变量和成员函数。它们可以直接通过类名调用。

5. 静态类对象

静态类对象的生命周期贯穿整个程序的运行期,且只会被构造一次,程序结束时才会被析构。它们可以用来实现单例模式(Singleton pattern)。

6.总结

  • 静态局部变量:在函数内使用,保持值在函数调用之间。
  • 静态全局变量:在文件作用域使用,限制变量在文件内可见。
  • 类的静态成员变量:在类中使用,所有对象共享,必须在类外部定义。
  • 类的静态成员函数:在类中使用,不依赖于对象,可以直接通过类名调用。
  • 静态类对象:用于实现单例模式,确保类只实例化一次。

这些不同的用法使得 static 关键字在 C++ 编程中非常有用,帮助开发者管理变量的作用域和生命周期。

62、为什么要引入static?

在 C++ 中,引入 static 关键字有多个重要的原因,主要涉及到变量和函数的生命周期、作用域、内存管理和程序结构。以下是引入 static 的几个关键原因:

1. 控制变量的作用域和可见性

(1) 静态局部变量

  • 目的:在函数内部定义一个静态变量,使其值在函数调用之间保持不变。

  • 示例

  • 好处:避免每次调用函数时重新初始化变量,同时保持函数局部变量的特性,不影响其他函数或全局作用域。

(2) 静态全局变量

  • 目的:在文件作用域内定义一个静态变量,使其只能在该文件中访问,防止命名冲突。

  • 示例

    // file1.cpp
    static int globalVar = 0;  // 静态全局变量,只在 file1.cpp 内可见
    
    void increment() {
        globalVar++;
    }
    
    // file2.cpp
    extern void increment();
    
    int main() {
        increment();
        // 无法直接访问 globalVar,因为它在 file1.cpp 内部是静态的
        return 0;
    }
    
  • 好处:限制变量的可见性,避免不同文件之间的命名冲突,增强代码模块化。

2. 共享数据和函数

(1) 类的静态成员变量

  • 目的:在类中定义一个静态变量,使其在所有对象间共享。

  • 示例

    class MyClass {
    public:
        static int staticVar;
    
        static void staticFunction() {
            std::cout << "Static variable: " << staticVar << std::endl;
        }
    };
    
    int MyClass::staticVar = 10;  // 定义并初始化静态成员变量
    
    int main() {
        MyClass::staticFunction();  // 输出: Static variable: 10
        MyClass::staticVar = 20;
        MyClass::staticFunction();  // 输出: Static variable: 20
        return 0;
    }
    
  • 好处:在所有对象之间共享数据,不需要实例化对象即可访问静态成员,减少内存开销。

(2) 类的静态成员函数

  • 目的:在类中定义一个静态函数,使其不依赖于对象,可以通过类名直接调用。

  • 示例

    class MyClass {
    public:
        static void staticFunction() {
            std::cout << "This is a static member function." << std::endl;
        }
    };
    
    int main() {
        MyClass::staticFunction();  // 直接通过类名调用
        return 0;
    }
    
  • 好处:提供了可以不依赖于对象的类级别函数,方便进行工具函数或与静态成员变量交互。

3. 实现设计模式

(1) 单例模式

  • 目的:通过静态类对象确保一个类只有一个实例。

  • 示例

    class Singleton {
    private:
        Singleton() { std::cout << "Singleton created." << std::endl; }
        ~Singleton() { std::cout << "Singleton destroyed." << std::endl; }
    
    public:
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    
        static Singleton& getInstance() {
            static Singleton instance;  // 静态类对象
            return instance;
        }
    };
    
    int main() {
        Singleton& singleton1 = Singleton::getInstance();
        Singleton& singleton2 = Singleton::getInstance();
        // 只会输出一次 "Singleton created."
        return 0;
    }
    
  • 好处:控制实例化次数,确保全局访问点,常用于资源管理类。

4. 内存管理

  • 静态局部变量和静态成员变量:在程序的整个生命周期内占用固定内存位置,避免频繁的内存分配和释放,提高性能。

5. 总结

引入 static 关键字主要是为了:

  • 控制变量的生命周期和作用域,使其在函数调用之间保持值,或限制其在文件内可见。
  • 在类中共享数据和函数,使得所有对象可以共享同一个数据,或不依赖于对象调用函数。
  • 实现设计模式,如单例模式,确保类只有一个实例。
  • 优化内存管理,减少内存分配和释放的开销。

通过 static 关键字,C++ 提供了灵活而强大的工具来管理变量和函数的作用域、生命周期和可见性,从而增强程序的安全性、可维护性和性能。

63、为什么要使用智能指针?

使用智能指针(Smart Pointers)的主要目的是提高 C++ 程序的内存管理和资源安全性。智能指针是一种封装了指针的类对象,它们可以自动管理内存分配和释放,从而减少内存泄漏和悬空指针等问题。以下是一些使用智能指针的主要原因:

1、自动内存管理:

智能指针使用 RAII(Resource Acquisition Is Initialization)原则,通过在对象构造和析构时自动分配和释放内存,实现自动内存管理。

这避免了手动管理内存的繁琐工作,减少了程序中出现内存泄漏和悬空指针的可能性。

// 使用智能指针进行自动内存管理
#include <memory>

int main() {
    std::shared_ptr<int> smartPtr = std::make_shared<int>(42);  // 自动分配内存
    // ...

    // 不需要手动释放内存,智能指针在作用域结束时会自动释放
    return 0;
}

2、防止悬空指针:

智能指针可以在对象销毁时自动将指针置空,防止悬空指针的出现。

当智能指针超出其作用域时,它会自动释放指向的对象,并将指针置空,避免了使用悬空指针。

// 避免悬空指针的出现
#include <memory>

void exampleFunction() {
    std::shared_ptr<int> smartPtr = std::make_shared<int>(42);
    // ...
    // 当 smartPtr 超出作用域时,自动释放对象,并将指针置空
}

3、防止内存泄漏:

智能指针的自动内存管理可以有效防止因忘记释放内存而导致的内存泄漏。

使用智能指针时,不需要显式地调用 new 和 delete,减少了出现资源泄漏的风险。

// 防止内存泄漏
#include <memory>

void exampleFunction() {
    std::shared_ptr<int> smartPtr = std::make_shared<int>(42);  // 自动分配内存
    // ...
    // 不需要手动释放内存,智能指针在作用域结束时会自动释放
}

4、共享所有权:

std::shared_ptr 允许多个智能指针共享同一个对象,采用引用计数机制。

当最后一个指向对象的智能指针被销毁时,对象的内存会被释放。

// 共享所有权
#include <memory>

int main() {
    std::shared_ptr<int> smartPtr1 = std::make_shared<int>(42);
    std::shared_ptr<int> smartPtr2 = smartPtr1;  // 共享所有权
    // ...
    // 当 smartPtr1 和 smartPtr2 超出作用域时,对象的内存会被释放
    return 0;
}

总体而言,使用智能指针可以提高代码的安全性和可维护性,减少了手动管理内存的复杂性,帮助程序员避免一些常见的内存管理问题。在现代C++中,智能指针是推荐使用的内存管理方式。

64、堆(heap)和栈(stack)的区别?

堆(heap)栈(stack) 是两种用于存储数据的不同内存区域,它们在分配和管理内存方面有着不同的特点和用途。以下是堆和栈的主要区别:

1、内存分配方式:

栈: 栈是一种有限且自动管理的内存区域。变量在栈上分配,其内存空间在程序的运行时自动管理,包括变量的分配和释放。栈上的内存分配是快速的,但大小有限。
堆: 堆是一种动态分配的内存区域,其大小不受限制。堆上的内存分配需要手动管理,开发人员负责在适当的时候分配和释放内存,以避免内存泄漏。

2、内存生命周期:

栈: 栈上的变量在其所在的函数执行结束时自动释放。局部变量的生命周期由其所在的函数决定。
堆: 堆上的内存可以在程序的任何地方分配,并且在程序员显式释放时才会被释放。堆上的变量的生命周期由程序员控制。

3、速度:

栈: 栈上的内存分配和释放是由编译器自动完成的,速度较快。栈上的内存分配和释放仅涉及移动栈指针。
堆: 堆上的内存分配和释放涉及到动态内存管理,速度相对较慢。

4、大小限制:

栈: 栈的大小通常较小,因为它由操作系统在程序启动时分配,并在运行时不断缩小。栈的大小限制取决于操作系统和编译器的设置。
堆: 堆的大小理论上没有限制,取决于系统的虚拟内存大小。

5、分配效率:

栈: 栈上的内存分配是按照固定顺序进行的,分配和释放速度较快。由于栈上的内存布局较简单,因此效率较高。
堆: 堆上的内存分配和释放可能会涉及到内存碎片的问题,分配和释放速度相对较慢。

6、使用场景:

栈: 适用于局部变量和函数调用的临时数据。由于栈上的内存生命周期短暂,适合存储函数的局部变量、函数参数等。
堆: 适用于需要动态分配内存、生命周期较长或大小不确定的数据。堆上的内存可以通过 new 和 delete(或 malloc 和 free)等手动管理函数进行分配和释放。

总结

总的来说,栈和堆在内存管理上有着不同的特性,选择使用哪种方式取决于数据的生命周期、大小和访问方式。在实际编程中,栈上的内存分配是更加高效和方便的方式,而堆上的内存分配则更加灵活,适用于需要动态管理内存的场景。

65、程序的内存模型分为那几个区域?

程序的内存模型通常分为以下几个区域,每个区域用于存储不同类型的数据,具有不同的生命周期和访问权限:

1、栈(Stack):

栈用于存储函数的局部变量、函数参数、返回地址和临时数据。
栈的分配和释放是由编译器自动管理的,局部变量在函数调用时分配,函数调用结束时自动释放。
栈上的内存分配和释放是快速的,但栈的大小有限。

2、堆(Heap):

堆用于存储动态分配的内存,大小不受限制,由程序员手动管理。
使用 new 和 delete(或 malloc 和 free)等手动管理函数进行堆上内存的分配和释放。
堆上的内存分配和释放可能会涉及到内存碎片的问题。

3、全局区/静态区(Global/Static Area):

全局区用于存储全局变量、静态变量以及常量数据。
全局变量的生命周期是整个程序的运行期间。
静态变量的生命周期也是整个程序的运行期间,但仅在定义它的文件内可见。
常量数据通常被存储在只读内存区域。

4、常量区(Constants Area):

常量区用于存储常量数据,如字符串常量。
常量区的数据在程序运行期间不能被修改。

5、代码区(Code Area):

代码区用于存储程序的机器指令,即可执行的程序代码。
代码区是只读的,不允许修改程序代码。
以上这些区域构成了程序的基本内存模型,不同的编程语言和操作系统可能会有一些细微的差异。在程序运行时,这些区域的数据和指令协同工作,确保程序正常执行。

66、知道如果用free去清理new出来的内存会产生什么问题吗?

使用 free 函数去清理通过 new 分配的内存会导致问题,因为 new 和 free 是不同的内存管理方式,它们不能混用。在C++中,应该使用 delete 去释放通过 new 分配的内存,而在C语言中,使用 free 去释放通过 malloc 或 calloc 分配的内存。

1、产生问题

如果在C++中使用 free 去清理通过 new 分配的内存,可能会导致以下问题:

(1)、未正确调用析构函数:

new 操作符在分配内存的同时会调用对象的构造函数,而 delete 操作符会调用对象的析构函数。如果使用 free 去释放通过 new 分配的内存,将不会调用对象的析构函数,导致对象的资源未正确释放。

(2)、不匹配的内存管理:

new 和 free 是不匹配的内存管理函数。new 操作符分配的内存应该由 delete 操作符释放,而不是由 free 函数释放。混用这两种方式可能导致内存泄漏或者程序崩溃。

2、正确做法

正确的做法是在C++中使用 delete 去释放通过 new 分配的内存,如下所示:

int *ptr = new int;  // 使用new分配内存
// ...
delete ptr;  // 使用delete释放内存
而在C语言中,应该使用 free 去释放通过 malloc 或 calloc 分配的内存:

int *ptr = (int *)malloc(sizeof(int));  // 使用malloc分配内存
// ...
free(ptr);  // 使用free释放内存

总之,内存的分配和释放应该保持一致,遵循语言规范,并使用相应的内存管理函数。在C++中,new 和 delete 是成对使用的;在C语言中,malloc 和 free 是成对使用的。

67、C++多态实现的机制?(重载和重写的区别?)

C++ 中的多态性通过虚函数和继承机制实现,主要包括静态多态性(编译时多态性)和动态多态性(运行时多态性)。

1、静态多态性(编译时多态性):

静态多态性是通过函数重载(Overloading)和运算符重载(Operator Overloading)来实现的。
函数重载允许在同一作用域内定义多个函数,它们具有相同的名称但参数列表不同。
运算符重载允许用户重新定义操作符的行为,以适应不同的数据类型。
静态多态性在编译时根据函数名和参数类型进行决策,称为编译时绑定。

class Example {
public:
    void foo(int x) {
        // ...
    }

    void foo(double x) {
        // ...
    }
};

int main() {
    Example obj;
    obj.foo(42);       // 调用foo(int)
    obj.foo(3.14);     // 调用foo(double)
    return 0;
}

2、动态多态性(运行时多态性):

动态多态性是通过虚函数和继承机制实现的。
虚函数是在基类中声明的函数,可以在派生类中被重写(覆盖)。
在使用基类指针或引用调用虚函数时,根据实际对象的类型调用相应的派生类中的函数。
动态多态性在运行时根据实际对象类型进行决策,称为运行时绑定。

class Shape {
public:
    virtual void draw() {
        // ...
    }
};

class Circle : public Shape {
public:
    void draw() override {
        // 实现Circle特定的绘制
    }
};

int main() {
    Shape* shape = new Circle();  // 基类指针指向派生类对象
    shape->draw();  // 调用Circle中的draw(),而不是Shape中的draw()
    delete shape;
    return 0;
}

3、重载和重写的区别:

1、定义:

重载(Overloading): 在同一作用域内定义多个函数,具有相同的名称但参数列表不同。
重写(Override): 在派生类中重新实现基类中已有的虚函数。

2、关键词:

重载: 使用相同的函数名,但参数列表不同。
重写: 使用 override 关键字,重新实现虚函数。

3、发生的地方:

重载: 在同一类中或者相同作用域内。
重写: 在派生类中。

4、绑定时机:

重载: 静态多态性,编译时绑定。
重写: 动态多态性,运行时绑定。

5、目的:

重载: 提供多个相似功能但参数不同的函数。
重写: 在派生类中重新定义基类的虚函数,实现多态性。
在实际开发中,通过动态多态性实现的虚函数重写更加灵活,使得代码更容易扩展和维护。静态多态性则提供了更多编译时的类型检查和优化。选择使用哪种机制取决于具体的设计需求。

68、虚函数表

虚函数表(Virtual Function Table,简称 vtable)是C++中实现动态多态性(运行时多态性)的关键机制之一。它是一张存储虚函数地址的表,用于在运行时确定实际被调用的函数。

以下是虚函数表的主要特点和工作原理:

1、虚函数的声明:

在基类中使用 virtual 关键字声明的虚函数,表明这个函数可以在派生类中被重写(覆盖)。

class Base {
public:
    virtual void foo() {
        // ...
    }
};

2、虚函数表的生成:

对于每个包含虚函数的类,编译器会在其对象的内存布局中插入一个指向虚函数表的指针(通常称为虚指针或虚函数指针)。
虚函数表是在编译阶段生成的,每个类都有一个对应的虚函数表。

3、虚函数表的内容:

虚函数表中存储的是虚函数的地址,按照其声明顺序排列。
对于每个虚函数,表中的一个槽存储函数的地址。
如果派生类重写了基类中的虚函数,那么相应的槽会被更新为派生类中的函数地址。

4、虚函数调用过程:

当使用基类指针或引用调用虚函数时,实际上是通过虚函数表来确定调用哪个函数。
在运行时,根据对象的虚指针找到对应的虚函数表,然后在表中查找相应槽的地址,最终调用相应的函数。

Base* obj = new Derived();  // 使用基类指针指向派生类对象
obj->foo();  // 调用派生类中的 foo(),而不是基类中的 foo()

5、多重继承和虚函数表:

在多重继承中,每个基类都有自己的虚函数表。当一个类同时继承自多个基类时,它会继承每个基类的虚函数表,并将它们合并成一个新的虚函数表。

考虑下面的代码示例:

在这个示例中,Derived 类同时继承自 Base1Base2 两个基类。因此,Derived 类会包含两个虚函数表,一个用于存储 Base1 的虚函数,另一个用于存储 Base2 的虚函数。当调用 Derived 对象的虚函数时,会根据函数在虚函数表中的位置确定调用哪个基类的虚函数。

需要注意的是,由于 C++ 中不存在默认的虚拟继承,因此虚函数表的合并是由编译器完成的。在合并虚函数表时,编译器需要解决可能存在的二义性问题,确保每个虚函数的调用都是明确的。

虚函数表使得 C++ 中的动态多态性得以实现,允许在运行时确定对象的实际类型,并调用相应的函数。虚函数表的使用是由编译器自动完成的,程序员通常无需手动介入。这个机制为C++中的面向对象编程增加了灵活性和可扩展性。

6、合并的时候如何解决虚函数表冲突的问题

在多重继承中,当一个类继承了多个基类,并且这些基类中存在共同的虚函数时,可能会导致虚函数表的冲突。这种冲突通常被称为虚函数表二义性问题。

编译器会通过一些规则来解决虚函数表的冲突,常见的解决方式包括:

1. 虚函数覆盖: 如果派生类覆盖了基类的虚函数,那么在合并虚函数表时,会优先选择派生类中的虚函数地址。这样就能确保调用的是派生类的版本而不是基类的版本。

2. 虚函数重载: 如果派生类中没有覆盖基类的虚函数,而是添加了同名但参数列表不同的函数,那么编译器会将这些函数添加到合并后的虚函数表中。这样可以实现函数的重载,避免了二义性问题。

3. 手动解决冲突: 如果存在虚函数表的冲突,编译器可能会发出警告或错误信息,提示程序员手动解决冲突。程序员可以通过指定调用哪个基类的函数来解决冲突,或者通过重命名函数来避免冲突。

总的来说,编译器会尽量避免虚函数表的冲突,但在一些特殊情况下可能需要程序员手动介入来解决冲突。因此,在设计多重继承的类时,需要谨慎考虑虚函数的设计,以避免可能出现的二义性和冲突问题。

69、为什么list不能使标准库算法sort()?

std::list 不能直接使用标准库算法 std::sort() 的原因是 std::list 是一种链表(linked list)数据结构,而 std::sort() 算法通常是基于比较的排序算法,要求随机访问迭代器。

链表的特点是每个元素都保存了指向下一个元素的指针,而不是像数组那样在内存中连续存储。因此,链表不支持随机访问,不能直接通过下标来访问元素。而 std::sort() 算法需要在元素之间进行快速的随机访问,以便进行比较和交换,因此不能直接应用于链表。

为了在链表上进行排序,C++ 标准库提供了专门针对链表的排序算法 std::list::sort(),它是 std::list 容器的成员函数,可以对链表中的元素进行排序。这个函数内部会使用链表的特性来实现高效的排序算法,不需要随机访问迭代器。

下面是一个示例,展示了如何使用 std::list::sort() 对链表进行排序:

在这个示例中,我们使用了 std::list::sort() 函数对 std::list 容器中的元素进行排序,而不是使用标准库的 std::sort() 算法。

70、解决哈希冲突的方法有哪些?

1、链地址法(Separate Chaining):

将哈希表的每个槽(桶)维护一个链表(或其他数据结构,如红黑树),将哈希值相同的元素存储在同一槽的链表中。
当发生哈希冲突时,新的元素被添加到相应槽的链表中,而不会覆盖原有的元素。

开放地址法(Open Addressing):

当发生哈希冲突时,通过一定规则寻找下一个可用的槽,将元素插入到找到的槽中。
常见的探测方法包括线性探测、二次探测、双重散列等。

2、再哈希法(Double Hashing):

利用第二个哈希函数,当发生冲突时,通过第二个哈希函数计算一个新的槽位置。
可以提供更灵活的探测方式,减少聚集。
建立公共溢出区(Overflow Area):

将哈希表分为主表和溢出区,当发生冲突时,将冲突的元素存储在溢出区。
需要额外的空间,但可以减少主表的聚集程度。

3、线性探测再散列(Linear Probing with Rehashing):

当发生冲突时,线性探测找到下一个可用槽,如果聚集达到一定程度,进行重新哈希。
在重新哈希时,通常会增大哈希表的大小,以减少聚集。
二次探测再散列(Quadratic Probing with Rehashing):

类似于线性探测再散列,但使用二次探测的方式寻找下一个可用槽。
伪随机数再散列(Random Probing with Rehashing):

使用伪随机数序列来决定探测的步长,以减少线性或二次探测中的聚集。
每种解决哈希冲突的方法都有其优劣和适用场景,选择合适的方法通常取决于具体的应用需求和数据特性。链地址法适用于元素数量较大且分布较均匀的情况,而开放地址法适用于元素数量较小或者要求节省内存的情况

71、容器适配器?

容器适配器是 C++ 标准库中的一种特殊容器,它们提供了不同于标准容器的接口和行为,通常用于解决特定的问题或者简化特定的任务。容器适配器不是真正意义上的容器,而是在已有的容器基础上进行了封装和调整,提供了一些特定的功能。

C++ 标准库提供了三种常见的容器适配器:栈、队列、优先队列

1、std::stack:

栈是一种后进先出(LIFO)的数据结构,只允许在栈顶进行插入和删除操作。C++ 标准库的 std::stack 就是基于已有的容器(默认情况下是 std::deque)实现的栈容器适配器。使用栈适配器可以方便地进行后进先出的操作,常用于实现简单的算法和数据结构
主要操作包括 push(入栈)、pop(出栈)、top(获取栈顶元素)等。

#include <stack>

int main() {
    std::stack<int> myStack;

    myStack.push(1);
    myStack.push(2);
    myStack.push(3);

    while (!myStack.empty()) {
        std::cout << myStack.top() << " ";  // 输出: 3 2 1
        myStack.pop();
    }

    return 0;
}

2、std::queue:

队列是一种先进先出(FIFO)的数据结构,允许在队尾进行插入操作,在队头进行删除操作。C++ 标准库的 std::queue 是基于已有的容器(默认情况下是 std::deque)实现的队列容器适配器。使用队列适配器可以方便地进行先进先出的操作,常用于任务调度和事件处理等场景。

主要操作包括 push(入队)、pop(出队)、front(获取队首元素)、back(获取队尾元素)等。

#include <queue>

int main() {
    std::queue<int> myQueue;

    myQueue.push(1);
    myQueue.push(2);
    myQueue.push(3);

    while (!myQueue.empty()) {
        std::cout << myQueue.front() << " ";  // 输出: 1 2 3
        myQueue.pop();
    }

    return 0;
}

3、std::priority_queue:

优先队列是一种特殊的队列,它允许按照优先级从高到低(或者从低到高)进行插入和删除操作。C++ 标准库的 std::priority_queue 是基于已有的容器(默认情况下是 std::vector)实现的优先队列容器适配器。使用优先队列适配器可以方便地实现按照优先级进行排序的功能,常用于任务调度和贪心算法等场景。

优先队列会按照元素的优先级进行排序,具有较高优先级的元素先被取出。
默认情况下,std::priority_queue 使用的是大顶堆,可以通过提供自定义的比较函数来使用小顶堆。

#include <queue>

int main() {
    std::priority_queue<int> myPriorityQueue;

    myPriorityQueue.push(3);
    myPriorityQueue.push(1);
    myPriorityQueue.push(4);
    myPriorityQueue.push(1);

    while (!myPriorityQueue.empty()) {
        std::cout << myPriorityQueue.top() << " ";  // 输出: 4 3 1 1
        myPriorityQueue.pop();
    }

    return 0;
}

这些容器适配器提供了简化的接口,隐藏了底层容器的具体实现细节,使得使用堆栈、队列和优先队列变得更加方便。在选择使用哪个容器适配器时,可以根据具体的需求和性能要求进行选择。

72、定位内存泄露

定位内存泄漏通常需要使用一些工具和技术来帮助检测和分析程序的内存使用情况。以下是一些用于定位内存泄漏的常见方法:

##1、内存泄漏检测工具:

使用专门的内存泄漏检测工具,例如 Valgrind、AddressSanitizer、Visual Leak Detector(对于Windows平台)等。这些工具能够在程序运行时检测到内存泄漏,并提供详细的报告,指示泄漏的位置、大小等信息。
编译器选项:

在编译时启用一些特定的编译器选项,以帮助检测内存泄漏。例如,使用 GCC 编译器可以添加 -fsanitize=address 或 -fsanitize=leak 选项,而使用 Clang 则可以使用 -fsanitize=address 选项。
2、自定义内存管理:

使用自定义的内存管理工具,例如重载 new 和 delete 运算符,记录分配和释放的内存块,并在程序结束时检查是否有未释放的内存块。
3、内存分析工具:

使用内存分析工具,例如 Massif 工具,它可以生成程序运行期间内存使用的详细报告。通过分析报告,可以找到内存泄漏的位置和原因。
智能指针和资源管理类:

使用智能指针(如 std::shared_ptr、std::unique_ptr)以及资源管理类,以确保资源在不再需要时能够被正确释放。这有助于避免手动内存管理时容易出现的错误。
4、编写单元测试:

编写测试用例,特别是针对自己实现的类和模块,以确保内存的正确分配和释放。通过单元测试可以提前发现潜在的内存泄漏问题。
使用内存分析工具(静态分析):

一些静态分析工具(如 Clang Static Analyzer、Coverity)可以在编译时分析源代码,提供关于潜在内存泄漏的警告和建议。
在定位内存泄漏时,常常需要结合多种方法和工具,以全面了解程序的内存使用情况。注意,一些工具可能会对程序性能产生一定的影响,因此在生产环境中使用时需要谨

73、如何避免死锁

1、产生死锁的四个必要条件:
死锁产生的四个必要条件,也被称为死锁的饥饿条件(Deadlock Conditions)或死锁的必要条件,是由 Edsger W. Dijkstra 在 1968 年首次提出的。这些条件是:

1.1互斥条件(Mutual Exclusion):

至少有一个资源是被独占的,即在一段时间内只能由一个进程使用。如果一个进程占用了资源 A,其他进程就不能同时占用资源 A。
1.2、占有和等待条件(Hold and Wait):
进程可以持有一些资源,并且可以等待获取其他进程持有的资源。即,进程可以同时持有一些资源,同时等待其他资源。这样就可以在资源竞争的时候发生死锁。
1.3非抢占条件(No Preemption):
资源不能被抢占,即如果一个进程持有了某个资源,其他进程只能在它主动释放时才能获取这个资源,而不能被强制剥夺。
1.4、循环等待条件(Circular Wait):
存在一个进程等待序列(P0, P1, …, Pn),其中 P0 等待 P1 持有的资源,P1 等待 P2 持有的资源,…,Pn 等待 P0 持有的资源,形成了一个循环等待的链。
当这四个条件同时满足时,就可能导致死锁的发生。为了避免死锁,至少要破坏其中一个条件。例如,通过使用适当的资源分配策略、引入超时机制、使用死锁检测和恢复算法等方法来降低死锁的发生概率。
2、解决办法

死锁是多线程编程中常见的问题之一,它发生在两个或多个线程无法继续执行,因为每个线程都在等待另一个线程释放某个资源。为了避免死锁,可以采取一些策略和实践:

1.1、按顺序获取锁:

确保线程按照相同的顺序获取锁。这样可以减小死锁的可能性,因为所有线程都以相同的顺序获取锁,从而避免了循环等待。
1.2、使用超时机制:

在获取锁的操作中使用超时机制。如果一个线程在一定时间内无法获取到锁,它可以放弃对锁的请求,释放已经获取的锁,并尝试其他操作或等待一段时间后再次尝试。
1.3、使用适当的锁粒度:

确保锁的粒度适当。过大的锁粒度可能导致竞争过多,而过小的锁粒度可能增加死锁的风险。根据实际情况选择适当的锁粒度。
使用锁的层次结构:

设计良好的锁的层次结构,确保线程获取锁的顺序是有序的。这样可以帮助降低死锁的概率。
避免在持有锁的同时阻塞:

尽量避免在持有锁的情况下阻塞。如果一个线程在持有锁的时候调用了阻塞操作,而阻塞操作需要获取其他锁,就可能导致死锁。
1.4、使用死锁检测工具:

使用死锁检测工具来检测和分析程序中的潜在死锁问题。这类工具可以帮助发现和定位死锁的发生地点。
避免嵌套锁:

尽量避免在持有锁的情况下再次尝试获取其他锁。如果确实需要嵌套锁,确保按照相同的顺序获取。
使用事务:

在数据库等系统中,使用事务来管理对资源的访问,以确保在一组操作中要么全部成功,要么全部失败。
使用无锁数据结构:

考虑使用无锁数据结构,如原子操作、无锁队列等,以减少对锁的依赖。
详细记录和分析死锁事件:

在发生死锁时,记录详细的信息,以便分析死锁的原因,并进行相应的调整。
通过遵循这些实践和设计原则,可以降低死锁的发生概率,提高多线程程序的可靠性。

74、STL各容器的实现原理

STL(标准模板库)提供了多种容器,每种容器都有不同的实现原理。以下是几种常见的STL容器的简要实现原理:

1、数组(std::array):

std::array 是一个固定大小的数组容器,底层通过普通数组实现。它提供了数组的基本功能,并添加了一些安全检查。数组的元素是在栈上分配的,因此访问速度较快。
2、向量(std::vector):

std::vector 是一个动态数组,底层通过动态分配的数组实现。它支持快速的随机访问,同时在末尾进行插入和删除操作的开销也相对较小。当元素数量增加时,std::vector 可能会重新分配内存,但在重新分配时会尽量保留先前分配的元素。
3、链表(std::list):

std::list 是一个双向链表,底层通过节点之间的指针连接实现。它支持在任意位置进行插入和删除操作,但随机访问的性能较差。由于节点之间的指针,std::list 在插入和删除操作上具有较好的性能。
4、队列(std::queue)和栈(std::stack):

std::queue 和 std::stack 是容器适配器,它们通常是在 std::deque 或 std::list 的基础上实现的。队列和栈的底层实现可以选择不同的容器,具体实现取决于编译器和标准库实现。
5、集合(std::set)和映射(std::map):

std::set 和 std::map 通常是通过平衡二叉搜索树(红黑树)实现的。这种数据结构保持元素有序,支持快速查找、插入和删除操作,但相对于 std::unordered_set 和 std::unordered_map 有较高的内存开销。
6、无序集合(std::unordered_set)和无序映射(std::unordered_map):

std::unordered_set 和 std::unordered_map 通常是通过哈希表实现的。这种数据结构提供了快速的查找、插入和删除操作,但不保持元素有序。哈希表的实现可能包括解决哈希冲突的机制,如链地址法或开放寻址法。
以上是一些常见STL容器的简要实现原理。实际的STL实现可能因不同的编译器和操作系统而异。

75、线程之间的通信方式

线程之间的通信是多线程编程中的关键问题,不同线程之间需要协调和共享信息。以下是一些常见的线程间通信方式:

1、共享内存:

多个线程共享同一块内存区域,通过在内存中创建共享数据结构或变量来进行通信。这需要通过锁机制(例如互斥锁、信号量)来保护共享数据,以防止竞态条件。

2、消息队列:

线程通过消息队列发送和接收消息。每个线程都有一个消息队列,通过向队列发送消息,其他线程可以异步地接收消息。这种方式适用于生产者-消费者模型。

3、信号量:

信号量是一种用于线程同步和通信的抽象。它可以用来控制对共享资源的访问,也可以用来进行线程的通信。信号量有两种类型:二进制信号量和计数信号量。

4、条件变量:

条件变量允许一个线程在满足某个条件之前等待,而其他线程则负责通知条件满足并唤醒等待的线程。条件变量通常与互斥锁一起使用,以确保线程安全。

5、互斥锁(Mutex):

互斥锁是一种保护共享资源不被多个线程同时访问的机制。线程在访问共享资源之前,需要先获得互斥锁,访问完成后释放锁。这确保了在同一时间只有一个线程能够修改共享资源。

6、事件:

事件是一种用于线程通信的同步原语。线程可以等待事件的发生,并且其他线程可以触发事件。这种方式常用于一个线程等待另一个线程的完成。

7、管道(Pipe):

管道是一种半双工的通信机制,可以用于两个线程之间的通信。其中一个线程充当写入者,另一个线程充当读取者。管道也常用于进程间通信。

8、屏障(Barrier):

屏障是一种同步机制,允许多个线程等待,直到所有线程都到达屏障位置。一旦所有线程都到达,屏障就会打开,所有线程同时继续执行。
这些通信方式可以根据具体的应用场景和需求选择使用。在实际应用中,通常会结合使用多种方式以满足复杂的线程间通信和同步需求。

76、红黑树比AVL的优势,为何用红黑树

红黑树(Red-Black Tree)和 AVL 树(Adelson-Velsky and Landis Tree)都是自平衡二叉搜索树,用于在动态数据集上进行高效的查找、插入和删除操作。它们各自有一些特点和优势,选择使用红黑树的主要原因包括:

1、平衡调整代价更低:

红黑树的平衡调整操作相对 AVL 树更加简单。红黑树的平衡性是通过颜色标记(红色和黑色)实现的,而 AVL 树则需要通过更复杂的旋转操作来保持平衡。因此,红黑树的插入和删除操作的实现更加高效。
2、更好的插入和删除性能:

由于红黑树的平衡性维护相对宽松,插入和删除操作通常比 AVL 树更快。这对于动态数据集频繁更新的应用场景,如数据库中的索引维护,是一个优势。
3、更好的空间效率:

红黑树需要维护的附加信息较少,只需要一个额外的颜色标记。相比之下,AVL 树需要存储每个节点的平衡因子,这需要更多的额外空间。在存储密集型应用中,红黑树可能更具优势。
4、对于大多数实际用例,性能表现较好:

红黑树在绝大多数情况下能够提供较好的性能,因此在标准库中,如 C++ STL 的 std::map 和 std::set 实现中,通常采用红黑树。红黑树在实践中通常表现稳定,而且在一般的搜索、插入和删除操作中能够取得较好的性能。
虽然红黑树在某些方面相对于 AVL 树有一些优势,但在某些特殊的应用场景,AVL 树可能仍然更适合,尤其是对于更注重搜索性能和对树的平衡性要求较高的场景。选择使用哪种平衡二叉搜索树通常要根据具体应用需求和性能特点做出权衡。
77、 指针和引用的区别
指针和引用是 C++ 中用于处理内存和对象的两种不同的机制。以下是指针和引用的主要区别:

1、定义和声明:

指针是一个变量,它存储一个内存地址,可以指向其他变量或对象。引用是一个别名,它相当于已经存在的变量或对象的别名,没有自己的内存地址。

int x = 10;
int *ptr = &x; // 指针
int &ref = x; // 引用
2、内存地址:

指针有自己的内存地址,存储了指向的变量或对象的地址。引用没有自己的内存地址,它只是被绑定到另一个变量或对象。
3、赋值和指向:

指针可以在运行时被重新赋值,指向不同的变量或对象。引用在创建时必须初始化,并且不能在运行时更改绑定的变量。

int y = 20;
ptr = &y; // 合法,指向另一个变量
ref = y; // 不合法,引用不能重新绑定
4、空值:

指针可以被设置为空指针(nullptr 或 NULL)。引用必须在创建时被初始化,并且不能为 null。
int *ptr = nullptr; // 合法
int &ref = x; // 合法
int &ref2; // 不合法,引用必须初始化
5、语法和操作符:

指针使用 * 操作符用于间接访问指向的对象。引用使用 & 操作符来创建和使用。
int val = *ptr; // 通过指针访问对象的值
int val2 = ref; // 直接使用引用
6、数组和函数参数:

指针可以进行指针算术和用于动态分配内存,而引用不能。指针可以作为数组和函数的参数传递,引用也可以作为函数参数传递。

int arr[5];
int ptrArr = arr; // 合法
void foo(int p) { /
/ }
foo(&x); // 合法,传递指针
总体而言,指针提供了更大的灵活性,但引用提供了更直观和简洁的语法。在 C++ 中,选择使用指针还是引用通常取决于具体的应用场景和需求。

77、在函数参数传递的时候,什么时候使用指针,什么时候使用引用?

在函数参数传递的时候,选择使用指针还是引用通常取决于以下几个因素:

1、可选性:

如果参数可以为 nullptr 或者需要动态分配内存,那么使用指针是一个不错的选择,因为指针可以为 nullptr,并且可以通过 new 关键字进行内存分配。

void funcWithPointer(int *ptr) {
// …
}
2、NULL 检查:

如果你希望在函数内部检查传递的值是否为 nullptr,那么使用指针是有意义的。

void funcWithPointer(int *ptr) {
if (ptr != nullptr) {
// …
}
}
3、数组:

如果需要传递数组,因为数组名会转换为指向数组第一个元素的指针,所以可以使用指针。

void funcWithPointer(int *arr, int size) {
// …
}
4、可变性:

如果函数内部需要修改传递的值,并且希望这些修改在函数外部生效,那么使用引用是更好的选择,因为引用直接操作原始数据。

void funcWithReference(int &ref) {
ref = 42; // 修改原始数据
}
5、可读性:

从代码的可读性出发,引用通常更直观。使用引用可以避免在代码中频繁使用 * 操作符。

void funcWithReference(int &ref) {
// …
}
6、对象所有权:

如果函数需要获取或传递对象的所有权,而不仅仅是访问对象的值,那么可以使用智能指针(如 std::unique_ptr、std::shared_ptr)。

void funcWithSmartPointer(std::unique_ptr ptr) {
// …
}
在实际编程中,根据具体的需求和设计原则,选择使用指针还是引用。总的来说,引用更简洁、直观,而指针提供了更多的灵活性和控制。在现代 C++ 中,使用引用更加推荐,除非需要使用指针的特定功能

78、 堆和栈有什么区别

堆(Heap)和栈(Stack)是计算机内存中两个主要的区域,它们在内存管理和存储方式上有很大的区别。

1、存储位置:

栈: 栈是一块内存区域,用于存储函数调用时的局部变量和函数的调用信息。栈是一种后进先出(LIFO)的数据结构,变量在栈中的分配和释放是自动的,由编译器负责。栈的大小是有限的,通常较小。
堆: 堆是用于动态分配内存的区域,用于存储程序运行时创建的对象和数据。堆是一种无序的数据结构,变量在堆中的分配和释放是手动控制的,需要程序员负责管理。

2、分配和释放:

栈: 栈上的内存分配和释放是由编译器自动管理的。当一个函数被调用时,其局部变量在栈上分配内存,函数执行完毕后,这些变量所占用的栈空间会被自动释放。
堆: 堆上的内存分配和释放是由程序员手动控制的。使用诸如 new 和 delete(或 malloc 和 free)等操作来在堆上分配和释放内存。

3、生命周期:

栈: 变量的生命周期在其所在的作用域内。当函数返回时,局部变量的生命周期结束,相关的栈空间会被释放。
堆: 变量的生命周期可以持续到程序的整个运行期间,直到显式释放内存或程序终止。

3、效率:

栈: 栈上的内存分配和释放是非常高效的,因为只需要移动栈指针,不涉及复杂的内存管理。
堆: 堆上的内存分配和释放可能会更耗时,因为需要进行显式的内存管理,可能涉及内存碎片的问题。

4、大小:

栈: 栈的大小通常比较有限,由操作系统和编译器设定,主要用于存储函数调用信息和局部变量。
堆: 堆的大小理论上是比较大的,受到操作系统和硬件的限制。
在 C++ 中,使用栈上的内存是推荐的方式,因为它具有自动管理和高效的特点。堆上的内存则更适用于需要在运行时动态分配和释放内存的情况,但也需要注意避免内存泄漏和悬空指针等问题。

79、堆快一点还是栈快一点?(字节提前批一面)

在一些情况下,栈上的内存分配和释放可能比堆上的内存操作更快。以下是一些比较栈和堆的性能特点:

1、栈的优势:

内存分配和释放速度快: 栈上的内存分配和释放是通过移动栈指针来实现的,非常高效,通常只涉及一些简单的指针操作。
局部性原理: 栈上的数据具有局部性原理,即相邻的数据项通常在内存中也是相邻的。这有助于提高缓存命中率,从而提高访问速度。

2、堆的优势:

灵活性: 堆上的内存分配和释放由程序员手动控制,提供了更大的灵活性,可以动态地在运行时分配和释放内存。
生存期: 堆上的数据可以在整个程序运行期间存在,而不仅仅是在其所在的作用域内。

3、应用场景:

栈: 适用于存储局部变量、函数调用信息等,特别是对于那些生命周期短、大小确定的数据。
堆: 适用于需要动态分配内存、生命周期较长或大小不确定的数据。

4、注意事项:

栈的大小有限: 栈的大小通常是有限的,由操作系统和编译器设定。如果需要大量的内存或者数据的生命周期比较长,堆可能更适合。

总体而言,栈上的内存操作通常更快,但在一些场景下,使用堆上的内存更为合适。性能的具体差异也取决于具体的应用程序、编译器优化等因素。在选择使用栈还是堆时,应根据具体需求和内存管理的特点来进行权衡。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值