Skip to content

C++ 资深工程师面试题详解

您好!作为一名资深的C++工程师,我非常乐意为您详细解答这些问题。这些问题覆盖了C++语言的核心概念、标准库、系统编程以及内存管理等多个重要领域,体现了对C++开发者能力的全面考察。以下是我的详细解答。


1、虚函数的实现机制?虚函数表和虚表指针的内存空间布局,他们的分别存放在哪里?

虚函数(Virtual Function)是C++实现多态性(Polymorphism)的关键机制,它允许通过基类指针或引用调用派生类中重写的同名函数。这种机制的核心是虚函数表(Virtual Table, vtable)虚表指针(Virtual Table Pointer, vptr)

实现机制:

当一个类声明了虚函数(或者继承了带有虚函数的基类),编译器会为该类创建一个唯一的虚函数表。这个vtable本质上是一个函数指针数组,存储了类中所有虚函数的地址。如果派生类重写了基类的虚函数,那么在派生类的vtable中,相应的位置会被替换为派生类重写后的函数地址。如果派生类没有重写,则该位置将继续存储基类虚函数的地址。

当通过基类指针或引用调用一个虚函数时,实际的调用过程如下:

  1. 通过对象实例的vptr找到对应的vtable。
  2. 在vtable中查找被调用虚函数的地址(这通常是一个固定的偏移量)。
  3. 解引用该地址,执行相应的函数代码。

这个过程被称为动态绑定(Dynamic Binding)运行时绑定(Late Binding),因为具体调用哪个函数是在程序运行时才确定的,而不是在编译期。

内存空间布局与存放位置:

为了清晰地说明内存布局,我们来看一个典型的例子:

cpp
class Base {
public:
    virtual void func1() { /* ... */ }
    virtual void func2() { /* ... */ }
private:
    long long a;
};

class Derived : public Base {
public:
    void func1() override { /* ... */ } // 重写
    virtual void func3() { /* ... */ }   // 新增虚函数
private:
    long long b;
};
内存区域存放内容详细说明
.rodata 段虚函数表 (vtable)vtable是类级别的,一个类只有一张。它通常存放在程序的可执行文件的只读数据段(.rodata.rdata),因为一旦在编译期确定,运行时就不应被修改。对于上面的例子,BaseDerived各自拥有一张vtable。Derived的vtable会包含func1的重写版本地址、继承的func2地址以及新增的func3地址。
对象内存虚表指针 (vptr)vptr是对象级别的,每个包含虚函数的类的实例都会有一个vptr。它作为对象的一个隐藏成员,通常位于对象内存布局的起始位置。vptr在对象构造时由构造函数初始化,指向其所属类的vtable。因此,vptr存放在对象所在的内存区域,如果对象在栈上,vptr就在栈上;如果对象在堆上,vptr就在堆上。
.text 段函数代码所有成员函数(包括虚函数和非虚函数)的二进制代码本身存放在代码段(.text),这是一个只读的内存区域。vtable中存储的仅仅是这些函数的入口地址。

总结: 虚函数机制通过vptr和vtable实现了运行时的函数派发,是C++多态的核心。vtable是类相关的,存储在只读数据段;vptr是对象相关的,存储在对象本身的内存空间中。

2、class和struct有什么区别?

在C++中,classstruct的底层实现几乎完全相同,它们都可以包含数据成员、成员函数、构造函数、析构函数、继承、虚函数等。它们之间最主要的区别在于默认的成员访问权限默认的继承方式

特性classstruct详细说明 -
默认成员访问权限privatepublic如果不显式指定publicprivateprotectedclass的成员默认为private,而struct的成员默认为public。这是两者最核心的语法区别。 -
默认继承方式privatepublic当一个类继承自另一个类时,如果不指定继承方式,class默认使用private继承,而struct默认使用public继承。例如,class Derived : Base等同于class Derived : private Base,而struct Derived : Base等同于struct Derived : public Base。 -

使用惯例:

尽管在技术上它们几乎可以互换,但在C++社区中,开发者们形成了一套广泛遵循的使用惯例:

  • struct:通常用于表示纯数据聚合体(Plain Old Data, POD),即只包含数据成员,几乎没有或只有非常简单的成员函数(如构造函数)。它强调的是数据的组织结构。例如,一个Point结构体可能只包含xy两个坐标成员。
  • class:通常用于表示具有复杂行为、需要封装和数据隐藏的对象。它强调的是“类”的概念,即数据和操作数据的行为的统一体,并且通常会定义明确的接口(public成员)和内部实现(private成员)。

遵循这些惯例可以使代码更具可读性和可维护性,让其他开发者能更快地理解你的设计意图。

3、构造函数里面可以使用this指针吗?

可以。 this指针在成员函数(包括构造函数和析构函数)中是一个隐含的参数,它指向调用该成员函数的对象实例。

在构造函数执行时,对象的内存空间已经被分配,并且this指针已经指向了这块内存。因此,在构造函数内部,你可以安全地使用this指针来访问该对象的数据成员或调用其其他成员函数。

使用场景:

  1. 区分同名参数和成员变量:这是最常见的用法。当构造函数的参数名与成员变量名相同时,必须使用this->来明确指定访问的是成员变量。

    cpp
    class MyClass {
    public:
        MyClass(int x) {
            this->x = x; // 使用this指针区分参数x和成员x
        }
    private:
        int x;
    };
  2. 在成员初始化列表中:虽然不直接写this,但成员初始化列表本身就是通过this指针来初始化成员的。

  3. 将对象自身传递给其他函数:在构造函数中,有时需要将当前正在构造的对象注册到某个管理器或传递给其他函数。

    cpp
    class Registry;
    
    class Gadget {
    public:
        Gadget(Registry& registry) {
            registry.registerGadget(this); // 将自身注册到外部系统中
        }
    };

需要注意的风险:

虽然可以使用this指针,但在构造函数中需要警惕一个重要的风险:不要通过this指针调用虚函数

在基类的构造函数执行期间,对象的类型仍然是基类类型,vptr指向的是基类的vtable。如果此时调用一个虚函数,执行的将是基类版本的函数,而不是派生类中可能存在的重写版本。这违背了虚函数的设计初衷,并可能导致未定义的行为或逻辑错误。C++标准规定,在构造或析构期间调用虚函数,其动态类型就是当前正在执行构造或析构的这个类的类型。

4、智能指针有哪些分类?分别有什么作用?

智能指针是C++11标准库引入的重要特性,位于<memory>头文件中。它们是模板类,通过RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制来自动管理动态分配的内存,有效防止内存泄漏。

主要的智能指针有以下三种:

智能指针 (Smart Pointer)核心作用与所有权模型 -
std::unique_ptr独占所有权 (Exclusive Ownership)unique_ptr确保在任何时候只有一个智能指针实例可以拥有和管理一个给定的动态分配对象。当unique_ptr被销毁时(例如离开作用域),它所管理的对象也会被自动删除。由于其独占性,unique_ptr是不可复制的,但可以通过std::move转移所有权。这是现代C++中管理动态资源的首选方式,因为它开销极低,几乎与裸指针相同。 -
std::shared_ptr共享所有权 (Shared Ownership)shared_ptr允许多个智能指针实例共同拥有和管理同一个动态分配对象。它内部维护一个引用计数(Reference Count),记录有多少个shared_ptr正指向该对象。每当一个新的shared_ptr指向该对象(通过拷贝构造或拷贝赋值),引用计数加一;每当一个shared_ptr被销毁或指向其他对象,引用计数减一。当引用计数变为零时,表示没有任何shared_ptr再指向该对象,此时对象会被自动删除。shared_ptr适用于需要将对象所有权在多个模块或线程间共享的场景。 -
std::weak_ptr非拥有型观察者 (Non-owning Observer)weak_ptr是一种不控制对象生命周期的智能指针,它指向由shared_ptr管理的对象,但不会增加引用计数。它的主要作用是观察一个对象,同时避免循环引用(Circular References)。循环引用是指两个或多个对象通过shared_ptr相互持有,导致它们的引用计数永远无法归零,从而造成内存泄漏。weak_ptr可以通过lock()方法尝试获取一个指向被观察对象的shared_ptr。如果对象仍然存在,lock()会返回一个有效的shared_ptr;如果对象已被销毁,则返回一个空的shared_ptr。这提供了一种安全地访问可能已被销-

5、enable_shared_from_this()和shared_from_this()的作用和原理是什么?以及在实际项目是否遇到使用这两个函数的实际场景?

std::enable_shared_from_thisstd::shared_from_this 是与 std::shared_ptr 配合使用的重要工具,用于解决在类成员函数内部需要获取自身 shared_ptr 的问题。

作用:

假设一个类的对象 obj 正被一个 shared_ptr 管理。如果在 obj 的某个成员函数中,需要将 this 指针传递给一个需要 shared_ptr 的函数(例如,将自己注册到某个异步回调中),我们不能直接 std::shared_ptr<MyClass>(this)。这样做会创建一个全新的、独立的 shared_ptr,它与管理 obj 的外部 shared_ptr 拥有不同的引用计数控制块。当外部的 shared_ptr 销毁对象后,这个新创建的 shared_ptr 就会变成悬空指针;或者当这个新指针销毁时,它会尝试再次删除已经被删除的对象,导致二次释放,引发程序崩溃。

enable_shared_from_thisshared_from_this 就是为了安全地解决这个问题。通过让类公有继承自 std::enable_shared_from_this<T>,该类的成员函数就可以调用 shared_from_this() 方法,安全地获取一个与外部 shared_ptr 共享所有权的、指向当前对象的 shared_ptr

原理:

std::enable_shared_from_this<T> 内部包含一个 std::weak_ptr<T> 成员。当我们通过 std::make_shared<T>()std::shared_ptr<T>(new T()) 创建一个继承了 enable_shared_from_this 的类的对象时,shared_ptr 的构造函数会检测到这个继承关系,并自动将这个内部的 weak_ptr 初始化,使其指向当前对象。这个过程是“侵入式”的,shared_ptr 会“告知”对象它正在被 shared_ptr 管理。

当我们在成员函数中调用 shared_from_this() 时,它实际上是调用内部 weak_ptrlock() 方法。lock() 会返回一个指向该对象的 shared_ptr,并且这个新的 shared_ptr 与所有其他管理该对象的 shared_ptr 共享同一个引用计数,从而保证了生命周期管理的正确性。

重要限制: 必须确保在调用 shared_from_this() 时,至少已经存在一个 shared_ptr 拥有该对象。如果在对象被 shared_ptr 管理之前就调用它(例如,在构造函数中直接调用),将会抛出 std::bad_weak_ptr 异常。

实际项目场景:

是的,我在实际项目中多次遇到并使用了这个机制,尤其是在异步编程回调的场景中。

一个典型的例子是基于 Boost.Asioasio 的网络编程:

cpp
#include <iostream>
#include <memory>
#include <asio.hpp>

class Connection : public std::enable_shared_from_this<Connection> {
public:
    // ... 构造函数等 ...

    void start() {
        // 异步读取数据,回调函数是类的成员函数
        // 这里需要传递一个能安全持有自身的 shared_ptr
        auto self = shared_from_this();
        asio::async_read(socket_, buffer_,
            [this, self](const asio::error_code& ec, std::size_t bytes_transferred) {
                if (!ec) {
                    // 处理数据,然后继续下一次读取
                    // self 保证了在回调执行时,Connection 对象依然存活
                    start();
                }
            });
    }

private:
    asio::ip::tcp::socket socket_;
    asio::streambuf buffer_;
};

在这个例子中,Connection 对象代表一个网络连接。start() 函数发起一个异步读操作。这个操作可能在 start() 函数返回后的任意时刻完成。为了确保当回调函数被调用时,Connection 对象仍然是存活的,我们需要将一个 shared_ptr 绑定到回调的闭包中。通过 shared_from_this() 获取的 self 就起到了这个作用,它延长了 Connection 对象的生命周期,直到异步操作完成并且回调函数执行完毕。

6、lambda有什么作用,C++11-20各有什么什么改进等?捕获的类型有哪几种,有啥作用如果在lambda函数中捕获this指针,会不会有什么风险?

Lambda 表达式(也称 Lambda 函数)是 C++11 引入的一个强大特性,它允许在代码中就地定义一个匿名的函数对象。这极大地简化了代码,尤其是在与 STL 算法或异步编程库配合使用时。

主要作用:

  1. 定义局部函数:可以在函数内部定义一个临时的、只在局部作用域使用的函数,避免了为了一个小功能而单独定义一个具名函数或函数对象的麻烦。
  2. 简化回调和算法调用:作为 STL 算法(如 std::for_each, std::find_if)的谓词(Predicate)或作为异步任务的回调函数,使代码更紧凑、更具可读性。
  3. 闭包(Closure):Lambda 可以捕获其所在作用域的变量,将函数和其相关的环境(捕获的变量)打包在一起,形成一个闭包。

捕获类型:

Lambda 的捕获列表([]中的内容)决定了它如何从外部作用域获取变量。

捕获方式语法示例作用 -
值捕获 (By Value)[=][var]捕获的变量在 Lambda 创建时被拷贝一份存储在闭包中。Lambda 内部对该变量的修改不会影响外部的原始变量。[=] 表示默认以值捕获所有在 Lambda 中使用的外部变量。 -
引用捕获 (By Reference)[&][&var]捕获的变量在 Lambda 内部作为引用使用。Lambda 内部对该变量的修改会直接影响外部的原始变量。[&] 表示默认以引用捕获所有在 Lambda 中使用的外部变量。使用引用捕获需要非常小心,必须确保 Lambda 的生命周期不会超过被捕获变量的生命周期,否则会导致悬空引用。 -
this 指针捕获[this]专门用于在类成员函数中捕获当前对象的 this 指针。这使得 Lambda 内部可以像在成员函数中一样直接访问类的成员变量和成员函数,而无需显式使用 this->。捕获 this 实际上是按值捕获了 this 指针本身。 -

C++11 到 C++20 的改进:

  • C++11: 引入 Lambda 表达式的基础语法,包括捕获列表、参数列表和函数体。
  • C++14: 引入泛型 Lambda (Generic Lambdas),允许在参数列表中使用 auto 关键字,使得 Lambda 可以接受任意类型的参数,类似于模板函数。同时引入了捕获初始化 (Init Capture),允许在捕- |

捕获 this 指针的风险:

如果在 Lambda 中捕获了 this 指针,需要警惕悬空指针的风险。这通常发生在异步操作中。

当一个成员函数创建了一个捕获 this 的 Lambda,并将这个 Lambda 作为回调(例如,提交给一个线程池或用于网络异步I/O),该成员函数可能很快就执行完毕,甚至拥有 this 指针的对象本身也可能在回调被执行之前就被销毁了。当回调最终被执行时,它所捕获的 this 指针就成了一个指向无效内存的悬空指针。此时访问任何成员变量或调用成员函数都会导致未定义行为,通常是程序崩溃。

如何规避风险?

这正是 std::enable_shared_from_thisshared_from_this() 发挥作用的经典场景。正确的做法是:

  1. 让类继承 std::enable_shared_from_this
  2. 确保对象由 std::shared_ptr 管理。
  3. 在创建 Lambda 时,不直接捕获 [this],而是捕获一个通过 shared_from_this() 获取的 shared_ptr 副本。
cpp
// 错误的方式
[this]() { this->doSomething(); }

// 正确的方式(C++14 及以后)
[self = shared_from_this()]() { self->doSomething(); }

// 正确的方式(C++11)
auto self = shared_from_this();
[self]() { self->doSomething(); }

通过捕获 shared_ptr,可以保证只要 Lambda 本身还存活(例如,还在异步任务队列中),它所引用的对象就一定不会被销毁,从而完美地解决了悬空指针问题。


7、平时用C++哪个标准比较多?

在目前的专业开发实践中,我主要使用 C++17C++20 这两个标准,并积极跟进 C++23 的新特性。选择哪个标准通常取决于项目的具体要求、团队的技术栈以及编译器的支持情况。

  • C++17 是当前许多成熟项目的主流选择。它在 C++11 和 C++14 的基础上提供了大量实用的增强功能,如结构化绑定(Structured Bindings)、if constexprstd::optionalstd::variantstd::any、文件系统库(std::filesystem)以及并行STL算法等。这些特性在提升代码表达能力、安全性和性能方面都非常有价值,并且得到了主流编译器(GCC, Clang, MSVC)的广泛和稳定支持。

  • C++20 是一个里程碑式的版本,引入了许多革命性的特性,我会在新项目或允许使用新标准的环境中优先采用。其中最重要的四大特性是:

    • Concepts (概念):从语法层面约束模板参数,提供更清晰的编译错误信息,极大地改善了泛型编程的体验。
    • Ranges (范围库):提供了一种全新的、更函数式的方式来处理序列数据,代码更简洁、更易读,并且能有效避免迭代器错误。
    • Coroutines (协程):为异步编程提供了语言级的支持,使得编写高效、易读的异步代码成为可能,是对传统回调和 future/promise 模式的重大改进。
    • Modules (模块):旨在取代传统的头文件包含机制,可以显著加快编译速度,并解决宏污染和循环依赖等问题。

    除此之外,C++20 还包括三路比较运算符(<=>)、constevalconstinit、日历和时区库等众多改进。

总的来说,对于需要高度稳定性和广泛兼容性的现有大型项目,我会坚持使用 C++17。对于追求更高开发效率和性能的新项目,我会积极拥抱 C++20。了解并掌握最新的C++标准是作为一名资深工程师保持技术竞争力的关键。

8、常用的STL容器有什么?平时用过哪些容器,详细介绍一下?

STL(Standard Template Library, 标准模板库)的容器是C++开发中不可或缺的工具。它们可以大致分为三类:序列容器关联容器容器适配器

我平时几乎用遍了所有主流的STL容器,以下是我对最常用的一些容器的详细介绍和使用心得:

序列容器 (Sequence Containers)

序列容器中的元素是线性排列的,可以按顺序访问。

  • std::vector (动态数组)

    • 介绍vector 是我使用最频繁的容器,没有之一。它将元素存储在一段连续的内存空间中,类似于动态数组。这使得它可以通过索引进行极快地随机访问(时间复杂度 O(1))。在尾部添加或删除元素(push_back, pop_back)通常是摊还常数时间 O(1),除非发生内存重新分配。但在中间或头部插入/删除元素则非常慢(O(n)),因为它需要移动之后的所有元素。
    • 使用场景:当需要频繁地随机访问元素,并且主要在容器尾部进行操作时,vector 是最佳选择。它是大多数情况下的默认首选容器。
  • std::deque (双端队列)

    • 介绍deque(double-ended queue)与 vector 类似,但它提供了在头部和尾部都能高效插入和删除元素的能力(均为摊还常数时间 O(1))。它的内部实现通常是一系列不连续的内存块(通过一个中控映射表管理),因此其随机访问速度略慢于 vector,但仍然是 O(1)。迭代器比 vector 的迭代器复杂。
    • 使用场景:当需要一个类似 vector 的容器,但又需要在两端进行频繁的插入和删除操作时,deque 是理想选择。例如,实现一个工作窃取队列(Work-Stealing Queue)。
  • std::list (双向链表)

    • 介绍list 是一个双向链表,每个元素都存储着指向前一个和后一个元素的指针。它的优点是在任何位置插入和删除元素都非常快(O(1)),前提是你已经有了指向该位置的迭代器。缺点是它不支持快速的随机访问(访问第n个元素需要 O(n) 的时间),并且每个元素都需要额外的指针开销,内存占用比 vector 更大。
    • 使用场景:当需要对容器进行大量的、任意位置的插入和删除操作,而对随机访问性能要求不高时,list 是合适的。一个重要的特性是,list 的插入和删除操作不会使指向其他元素的迭代器、指针或引用失效。

关联容器 (Associative Containers)

关联容器根据键(key)来快速查找元素,通常基于红黑树或哈希表实现。

  • std::map (有序键值对)

    • 介绍map 存储的是键-值(key-value)对,并根据键自动进行排序。它的底层实现通常是红黑树,这是一种自平衡的二叉搜索树。因此,map 中的元素总是有序的,并且插入、删除、查找操作的平均和最坏时间复杂度都是对数级别 O(log n)。
    • 使用场景:当需要存储键值对,并且需要根据键进行排序或范围查找(例如,查找所有键在某个范围内的元素)时,map 是不二之选。
  • std::unordered_map (哈希表)

    • 介绍unordered_map 同样存储键值对,但它不保证元素的顺序。它的底层实现是哈希表。通过哈希函数,它可以实现极快的平均查找、插入和删除速度(平均时间复杂度 O(1))。但在最坏情况下(所有元素哈希冲突),性能会退化到 O(n)。
    • 使用场景:当需要极高的键值对查找性能,且不关心元素顺序时,unordered_map 是首选。在绝大多数需要字典或映射的场景中,它都比 map 更高效。
  • std::setstd::unordered_set

    • 介绍setunordered_set 分别是 mapunordered_map 的“只有键”的版本。它们用于存储唯一的元素集合,并分别提供有序和无序的存储方式。其底层实现和性能特征与对应的 map 版本相同。
    • 使用场景:当需要快速判断一个元素是否存在于集合中,或者需要存储不重复的元素时使用。例如,用 set 去除重复项并排序。
容器类型底层数据结构时间复杂度 (平均)主要优点主要缺点
std::vector连续数组随机访问: O(1), 尾部增删: O(1)内存连续,缓存友好,随机访问最快中间增删慢 (O(n))
std::deque分块数组随机访问: O(1), 两端增删: O(1)头尾操作高效内存不连续,随机访问比 vector 稍慢
std::list双向链表任意位置增删: O(1)任意位置增删极快,迭代器稳定性好不支持随机访问 (O(n)),额外指针开销大
std::map红黑树增删查: O(log n)元素有序,支持范围查找性能不如哈希表,有树结构开销
std::unordered_map哈希表增删查: O(1)查找性能极高元素无序,可能发生哈希冲突导致最坏性能 O(n)

9、vector的扩容机制是怎么样的?

std::vector 的扩容机制是其实现动态数组功能的核心。当向 vector 中添加元素(例如,使用 push_back)而其内部存储空间已满时(即 size() == capacity()),vector 会自动进行扩容。

扩容过程大致如下:

  1. 分配新内存vector 会向内存管理器申请一块新的、更大的连续内存空间。这个新的大小通常是当前容量(capacity())的某个倍数。这个倍数被称为增长因子(Growth Factor)。在不同的标准库实现中,这个因子可能不同,常见的是 1.52。例如,GCC 的 libstdc++ 通常使用 2,而 MSVC 的 STL 则倾向于 1.5。

  2. 移动或复制元素:将 vector 中原有的所有元素从旧的内存区域移动复制到新的内存区域。C++11 之后,如果元素的类型支持移动构造函数(move constructor)并且保证不抛出异常(noexcept),则会优先使用 std::move 来转移元素,这比复制高效得多,因为它只涉及指针等内部状态的转移,而不涉及深层资源拷贝。否则,就会调用拷贝构造函数来复制每个元素。

  3. 释放旧内存:元素全部转移完毕后,vector 会释放掉原来那块较小的、旧的内存空间。

  4. 更新内部指针:最后,vector 的内部指针会更新,指向新的内存区域,并且其 capacity() 也会更新为新分配的内存大小。

这个“倍增”策略的设计非常关键。它确保了连续 push_back 操作的**摊还时间复杂度(Amortized Time Complexity)**为 O(1)。虽然单次扩容操作的成本是 O(n)(其中 n 是 vector 中元素的数量),但由于扩容不是每次 push_back 都发生,而是随着元素数量的增加,其发生的频率越来越低,因此将高成本的扩容操作的开销分摊到多次 push_back 操作上,平均下来每次操作的成本仍然是常数时间。

可以通过 reserve() 成员函数来主动预留内存空间,如果能预知将要存储的元素数量,调用 reserve() 可以避免多次自动扩容带来的性能开销,从而提高程序效率。

10、里面提到扩容会涉及元素的移动和复制,那么具体是怎么判断移动还是复制的,如果是复制的话是深拷贝还是浅拷贝?

这是一个非常深入的好问题,涉及到 C++11 的移动语义和异常安全保证。

判断移动还是复制的机制:

vector 扩容时,标准库会使用 std::move_if_noexcept 来决定是移动元素还是复制元素。这个决策基于以下两个条件:

  1. 类型是否支持移动:首先,元素的类型 T 必须有移动构造函数 T(T&&)
  2. 移动构造函数是否为 noexcept:其次,这个移动构造函数必须被声明为 noexcept,即保证在移动过程中不会抛出异常。

std::move_if_noexcept(x) 的行为是:

  • 如果 T 的移动构造函数是 noexcept 的,move_if_noexcept(x) 会返回一个右值引用 std::move(x),从而触发移动操作。
  • 如果 T 没有移动构造函数,或者移动构造函数没有被声明为 noexceptmove_if_noexcept(x) 会返回一个左值引用 x,从而触发拷贝操作。

为什么要求 noexcept

这是为了提供强异常安全保证(Strong Exception Guarantee)vector 在进行扩容时,承诺如果在这个过程中发生异常,vector 的状态会回滚到操作之前的状态,即原 vector 保持有效,不会有数据丢失。

  • 如果使用拷贝构造函数,在将元素从旧内存复制到新内存的过程中,假如复制第 i 个元素时抛出异常,此时新内存中的 i-1 个元素是有效的副本,而旧内存中的所有原始元素都还完好无损。vector 可以简单地释放掉新分配的内存,然后保持原样,强异常安全得到保证。

  • 但如果使用一个可能抛出异常的移动构造函数,情况就不同了。当移动第 i 个元素时,它的资源可能已经从旧内存的 source 对象中“窃取”了一部分,然后在这个过程中抛出了异常。此时,source 对象的状态已经被破坏(资源被部分移走),而新内存中的目标对象也未能成功构造。vector 无法恢复到操作之前的状态,导致数据丢失和状态不一致。这就是为什么标准库要求,只有在移动操作承诺不会抛出异常时,才使用它进行扩容。

复制是深拷贝还是浅拷贝?

vector 不得不进行复制时,它调用的是元素的拷贝构造函数。这个拷贝是**深拷贝(Deep Copy)还是浅拷贝(Shallow Copy)**完全取决于该类型的拷贝构造函数是如何实现的。

  • 对于内置类型(如 int, double)或只包含内置类型成员的简单 struct/class,其默认的拷贝构造函数执行的是按位复制,这可以看作是浅拷贝。
  • 对于管理动态资源的类(例如,一个包含裸指针的类,该指针指向堆上分配的内存),一个正确实现的拷贝构造函数必须执行深拷贝。它需要为新对象分配自己的内存,并将原对象所指向的数据内容复制过来,而不是仅仅复制指针值。如果只进行浅拷贝,会导致两个对象共享同一份资源,当其中一个对象析构并释放资源后,另一个对象就会持有一个悬空指针。

STL 容器(如 vector)本身被设计为能够正确地处理包含动态资源的对象。只要你放入 vector 的对象遵循了**“Rule of Three/Five/Zero”**(即正确实现了拷贝构造、拷贝赋值、析构函数,以及C++11后的移动构造和移动赋值),vector 的扩容机制就能安全、正确地工作。

11、如果循环里面用迭代器去遍历map的话,什么情况下可能导致迭代器失效?

对于 std::map(以及 std::set, std::multimap, std::multiset 等基于红黑树的有序关联容器),其迭代器的稳定性非常好。在遍历 map 的过程中,只有一种情况会导致迭代器失效:

当且仅当指向被删除元素的迭代器会失效。

具体来说:

  • 插入操作 (insert):向 map 中插入新元素不会使任何现有迭代器失效。因为 map 的底层是节点式结构(红黑树),插入新元素只是在树中新增一个节点,并调整一些指针,不会影响到其他节点的位置。

  • 删除操作 (erase)

    • map::erase(iterator):当使用一个迭代器来删除元素时,只有这个被删除元素的迭代器会失效。指向 map 中其他所有元素的迭代器、指针和引用都保持有效。
    • map::erase(key):通过键来删除元素,同样,只有被删除元素的迭代器会失效。

因此,在循环中遍历 map 并删除元素时,需要特别小心处理迭代器。一个常见的错误写法是:

cpp
// 错误示例:迭代器失效
for (auto it = my_map.begin(); it != my_map.end(); ++it) {
    if (should_delete(it->second)) {
        my_map.erase(it); // 错误!it 在这里失效了,后续的 ++it 是未定义行为
    }
}

my_map.erase(it) 之后,it 就变成了一个悬空迭代器,再对它进行 ++it 操作会导致程序崩溃或未定义行为。

正确的处理方式有两种:

1. 使用 erase 的返回值(C++11 及以后):

map::erase(iterator) 会返回一个指向被删除元素下一个元素的迭代器。这是最现代、最简洁、最高效的方式。

cpp
// 正确方式 1 (推荐)
for (auto it = my_map.begin(); it != my_map.end(); /* no increment here */) {
    if (should_delete(it->second)) {
        it = my_map.erase(it); // erase返回下一个有效迭代器
    } else {
        ++it;
    }
}

2. 在删除前递增迭代器副本:

在 C++11 之前,erase 的返回值是 void。在这种情况下,需要在删除迭代器之前,先将它递增并保存副本。

cpp
// 正确方式 2 (C++98/03 风格)
for (auto it = my_map.begin(); it != my_map.end(); ) { // 注意循环条件的写法
    if (should_delete(it->second)) {
        auto to_erase = it++; // 先递增 it,保存要删除的迭代器
        my_map.erase(to_erase);
    } else {
        ++it;
    }
}

相比之下,std::vectorstd::deque 的迭代器失效规则要复杂得多。在这些容器中进行插入或删除操作,可能会导致指向该位置之后所有元素的迭代器全部失效,甚至在 vector 发生扩容时,所有迭代器都会失效。这也是 maplist 在需要频繁增删元素的场景下的优势所在。


12、进程和线程的区别?进程的通信方式有哪些?介绍一下

进程(Process)和线程(Thread)是操作系统中两个核心且紧密相关的概念,但它们在资源所有权和执行流方面有着本质的区别。

进程与线程的区别:

| 特性 | 进程 (Process) | 线程 (Thread) -| | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 资源所有权 | 进程是资源分配的基本单位。操作系统会为每个进程分配独立的地址空间(包括代码、数据、堆栈)、文件描述符、内存等系统资源。一个进程的崩溃通常不会影响到其他进程。 - | | 执行单位 | 线程是CPU调度的基本单位。一个进程可以包含一个或多个线程,这些线程共享所属进程的全部资源(地址空间、文件等)。线程是实际的执行流,有自己的程序计数器、栈和寄存器集合。 - | | 创建与销毁 | 创建进程的开销很大,因为它需要分配独立的内存空间和各种系统资源(即 fork()CreateProcess)。销毁进程时也需要回收所有这些资源。 - | | 通信与同步 | 进程间通信(IPC)相对复杂且开销较大,因为它们地址空间独立,需要借助操作系统提供的机制。线程间通信非常方便,因为它们共享数据,可以直接读写同一进程的全局变量和堆内存。但也因此需要使用锁、信号量等同步机制来避免数据竞争。 - | | 健壮性 | 进程间相互独立,一个进程的崩溃不会影响其他进程,系统健壮性高。 - |

简单来说,可以把进程比作一个工厂,它有自己的厂房、设备和资源。而线程则是工厂里的工人,多个工人共享工厂的资源,协同完成生产任务。一个工厂至少要有一个工人。

进程的通信方式 (IPC - Inter-Process Communication):

由于进程拥有独立的内存空间,它们之间的通信必须通过操作系统内核提供的机制来完成。常见的IPC方式有:

  1. 管道 (Pipes)

    • 匿名管道 (Anonymous Pipes):半双工的,只能在具有亲缘关系(父子、兄弟)的进程间使用。数据像水流一样单向流动,一端写入,一端读出。生命周期随进程结束而结束。
    • 命名管道 (Named Pipes / FIFO):也是半双工,但它在文件系统中有一个可见的路径名。因此,它允许无亲缘关系的任意两个进程之间进行通信。生命周期独立于进程。
  2. 消息队列 (Message Queues)

    • 消息队列是存放在内核中的一个消息链表。它克服了管道只能承载无格式字节流以及缓冲区大小受限的缺点。每个消息都有自己的类型,接收方可以按类型接收消息。它独立于发送和接收进程,即使进程退出了,消息队列中的内容也不会丢失。
  3. 共享内存 (Shared Memory)

    • 这是最快的IPC方式。它允许多个进程将同一块物理内存区域映射到它们各自的虚拟地址空间中。这样,一个进程写入共享内存的数据,可以被其他进程立即看到,无需在内核和用户空间之间进行数据拷贝。但正因为如此,必须使用信号量互斥锁等同步机制来保证数据的一致性。
  4. 信号量 (Semaphores)

    • 信号量本质上是一个计数器,常用于控制多个进程对共享资源的访问。它不直接传输数据,而是作为一种同步原语。当一个进程想要访问资源时,需要先获取信号量(计数器减一),如果计数器为0,则进程阻塞。当进程释放资源时,会释放信号量(计数器加一),唤醒等待的进程。
  5. 套接字 (Sockets)

    • 套接字是一种更为通用的IPC机制,它不仅可以用于同一台主机上的进程间通信,还可以用于不同主机之间(即网络通信)。它提供了面向连接(如TCP)和无连接(如UDP)两种模式。
  6. 信号 (Signals)

    • 信号是Unix/Linux系统中用于通知接收进程某个异步事件已经发生的一种机制。例如,SIGINT(由 Ctrl+C 产生)通知进程中断。它能传递的信息量很小,通常只用于简单的通知。
IPC 方式优点缺点适用场景
管道简单易用单向,只能在亲缘进程间(匿名),缓冲区有限简单的数据流传输
消息队列可按类型收发,独立于进程容量有限,需要注意消息大小异步通信,解耦生产者和消费者
共享内存速度最快,无内核拷贝需要手动同步,实现复杂高性能、大数据量交换的场景
信号量可实现进程/线程同步与互斥本身不传输数据控制对共享资源的访问
套接字通用性最强,可跨网络开销相对较大本地或网络间的任意进程通信

13、线程间的同步和互斥怎么解决?

在多线程环境中,由于多个线程共享同一进程的资源(尤其是内存),为了防止数据竞争(Data Race)和保证程序逻辑的正确性,必须采取同步(Synchronization)和互斥(Mutual Exclusion)措施。

互斥(Mutual Exclusion)

互斥是指,在任意时刻,只允许一个线程访问某个共享资源(也称为临界区,Critical Section)。其目的是保护共享数据不被多个线程同时修改而导致状态不一致。

同步(Synchronization)

同步是指,协调多个线程的执行顺序,确保它们按照预定的次序来操作。一个线程的执行可能依赖于另一个线程的某个结果或状态。同步不仅包含互斥,还包括了线程间的协作。

解决这些问题的主要机制如下:

  1. 互斥锁 (Mutex)

    • 这是实现互斥最基本的工具。一个互斥锁只有两种状态:锁定(locked)和未锁定(unlocked)。当一个线程想要进入临界区时,它必须先尝试获取(lock)这个锁。如果锁是未锁定的,该线程成功获取锁,进入临界区,并将锁置为锁定状态。其他任何线程此时尝试获取该锁都会被阻塞,直到持有锁的线程完成操作并释放(unlock)该锁。
    • C++11 提供了 std::mutexstd::recursive_mutex(允许同一线程多次获取锁)、std::timed_mutex(支持超时等待)等。
    • 为了避免忘记解锁而导致死锁,最佳实践是使用 RAII 风格的锁管理器,如 std::lock_guardstd::unique_locklock_guard 在构造时加锁,在析构时(离开作用域)自动解锁,非常安全简单。unique_lock 功能更强大,提供了更灵活的锁定策略(如延迟加锁、尝试加锁、移动锁所有权),并可以与条件变量配合使用。
  2. 条件变量 (Condition Variable)

    • 条件变量用于实现线程间的同步,即“等待-通知”机制。它允许一个线程在某个条件未满足时阻塞原子地释放互斥锁,然后等待其他线程满足该条件后将其唤醒。
    • 它必须与一个互斥锁配合使用。一个线程检查条件(在锁的保护下),如果不满足,就调用 wait()wait() 会原子地释放锁并让线程休眠。另一个线程(生产者)在改变了条件后,调用 notify_one()notify_all() 来唤醒一个或所有等待的线程。
    • C++11 提供了 std::condition_variable。它是实现生产者-消费者模型的关键。
  3. 信号量 (Semaphore)

    • 信号量可以看作是一个更广义的互斥锁。一个互斥锁可以看作是容量为 1 的信号量。信号量允许多个线程(最多N个)同时访问一个资源池。当线程请求资源时,如果信号量计数大于0,则计数减一,线程继续执行。如果为0,则线程阻塞。当线程释放资源时,计数加一,并可能唤醒一个等待的线程。
    • C++20 标准库中引入了 std::counting_semaphore
  4. 原子操作 (Atomic Operations)

    • 对于非常简单的共享数据(如计数器、标志位),使用重量级的互斥锁开销可能过大。原子操作提供了一种无锁(Lock-Free)的编程方式。它能保证对单个变量的读、写、修改操作是不可分割的,不会被其他线程中断。
    • C++11 在 <atomic> 头文件中提供了 std::atomic<T> 模板,可以对整型、布尔型、指针等类型进行原子操作(如 fetch_add, exchange, compare_exchange_strong)。原子操作通常比锁更高效,但使用起来也更复杂,需要对内存模型有深入的理解。
  5. 读写锁 (Shared Mutex / Readers-Writer Lock)

    • 这是一种更精细的锁,它区分了读操作写操作。允许多个线程同时进行读操作,但只允许一个线程进行写操作,并且读和写是互斥的。这适用于“读多写少”的场景,可以大大提高并发性能。
    • C++17 引入了 std::shared_mutex,可以通过 std::shared_lock 获取读锁,通过 std::unique_lockstd::lock_guard 获取写锁。

总结:

机制主要用途C++ STL 支持关键点
互斥锁互斥访问临界区std::mutex, std::lock_guard, std::unique_lock保证同一时间只有一个线程访问共享资源。
条件变量线程间同步(等待/通知)std::condition_variable与互斥锁配合,实现当条件满足时才继续执行的逻辑。
信号量控制并发访问数量std::counting_semaphore (C++20)允许多个(N个)线程并发访问资源。
原子操作无锁数据修改std::atomic<T>对简单数据类型的操作提供无锁、线程安全的保证,性能高。
读写锁读多写少场景优化std::shared_mutex, std::shared_lock (C++17)允许多个读者或一个写者,提高并发读的性能。

14、传输层有哪些协议?

传输层是OSI模型和TCP/IP模型中的关键一层,它负责在端到端(end-to-end)的通信实体之间提供数据传输服务。其最重要、最广为人知的两个协议是 TCPUDP

1. TCP (Transmission Control Protocol, 传输控制协议)

TCP 是一种面向连接的、可靠的、基于字节流的传输层协议。它的设计目标是在不可靠的IP网络上提供可靠的数据传输。

  • 面向连接 (Connection-Oriented):在数据传输之前,通信双方必须先建立一个连接。这个过程被称为三次握手(Three-way Handshake)。数据传输结束后,需要通过**四次挥手(Four-way Wave)**来断开连接。
  • 可靠性 (Reliable):TCP 通过多种机制来保证数据的可靠交付:
    • 序列号和确认应答 (Sequence and Acknowledgment Numbers):TCP 将发送的数据分割成段(Segment),并为每个字节分配一个序列号。接收方收到数据后会发送一个ACK确认号,告诉发送方它期望收到的下一个字节的序列号。如果发送方在一定时间内没有收到某个段的确认,它会重传该段。
    • 流量控制 (Flow Control):通过**滑动窗口(Sliding Window)**机制,接收方可以告诉发送方自己还有多少缓冲区空间可用,从而防止发送方发送过快导致接收方无法处理。
    • 拥塞控制 (Congestion Control):当网络发生拥塞时,TCP 会主动减慢发送速率,以缓解网络压力。它通过慢启动(Slow Start)、拥塞避免(Congestion Avoidance)、快重传(Fast Retransmit)和快恢复(Fast Recovery)等算法来实现。
  • 字节流 (Byte Stream):TCP 将应用程序交付的数据视为一个无边界的字节流。它不保留应用程序写入数据的边界。例如,应用层连续写入两次数据(100字节和200字节),接收方可能一次性收到300字节,也可能分多次收到。

适用场景:对数据完整性和顺序性要求极高的应用,如文件传输(FTP)、网页浏览(HTTP/HTTPS)、电子邮件(SMTP/POP3)、远程登录(SSH)等。

2. UDP (User Datagram Protocol, 用户数据报协议)

UDP 是一种无连接的、不可靠的、面向数据报的传输层协议。它提供了非常基本的数据传输服务,开销极小。

  • 无连接 (Connectionless):发送数据前不需要建立连接,直接将数据报(Datagram)打包并发送出去。每个数据报都是独立的,有自己的目标地址和端口。
  • 不可靠 (Unreliable):UDP 不保证数据报的交付、不保证它们的顺序、也不保证数据的完整性。数据报可能会丢失、重复或乱序到达。它没有重传机制、没有流量控制、也没有拥塞控制。
  • 面向数据报 (Datagram-Oriented):UDP 保留了应用程序发送消息的边界。如果应用层发送了一个100字节的数据报,接收方收到的也一定是一个完整的100字节的数据报。它不会合并或拆分数据报。
  • 开销小,速度快:由于没有复杂的连接管理和可靠性机制,UDP 的头部开销非常小(仅8字节,而TCP头部至少20字节),处理速度非常快。

适用场景:对实时性要求高、能容忍少量数据丢失的应用,如在线游戏、视频会议、实时音视频流(RTP)、DNS查询、DHCP等。这些应用通常在应用层自己实现部分可靠性机制。

特性TCP (传输控制协议)UDP (用户数据报协议)
连接性面向连接 (三次握手/四次挥手)无连接
可靠性可靠 (确认、重传、排序)不可靠 (尽力而为)
控制有流量控制和拥塞控制
速度较慢较快
头部开销大 (>= 20字节)小 (8字节)
数据模型字节流数据报
适用场景文件传输、网页、邮件等要求高可靠性的场景实时游戏、视频会议、DNS等要求低延迟的场景

除了 TCP 和 UDP,还有一些其他的传输层协议,虽然不那么常用,但也值得了解:

  • SCTP (Stream Control Transmission Protocol, 流控制传输协议):它结合了 TCP 和 UDP 的一些优点,提供了多流(Multi-streaming)、多宿主(Multi-homing)等特性,并且既可以提供可靠的有序传输,也可以提供不可靠的无序传输。常用于电信信令网络。
  • QUIC (Quick UDP Internet Connections):由 Google 开发,现在已成为 HTTP/3 的基础。它构建在 UDP 之上,但在应用层实现了类似 TCP 的可靠性、拥塞控制和流量控制,同时解决了 TCP 的队头阻塞问题,并集成了 TLS 加密,实现了更快的连接建立和更好的移动网络性能。

15、在Linux系统下,有一个client和server,他们要建立连接通信需要调用哪些函数?

在 Linux 系统下使用 C/C++ 进行基于 TCP 的网络编程,客户端(Client)和服务器(Server)建立连接和通信所涉及的系统调用函数(Socket API)是固定且经典的流程。

服务器端 (Server) 的流程:

服务器的角色是被动等待客户端的连接请求,并为每个接受的连接提供服务。

  1. socket() - 创建套接字

    • int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    • 这是第一步,向内核申请一个网络通信的“端点”。
    • AF_INET 表示使用 IPv4 协议。
    • SOCK_STREAM 表示创建一个面向连接的、可靠的 TCP 套接字。
    • 0 表示使用默认协议。函数返回一个套接字文件描述符(socket file descriptor),后续所有操作都通过它进行。
  2. bind() - 绑定地址和端口

    • bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    • 将创建的套接字与一个具体的 IP 地址和端口号关联起来。服务器必须绑定到一个众所周知的端口上,这样客户端才能找到它。
    • 通常服务器会绑定到 INADDR_ANY,表示接受发送到本机任意网络接口的连接请求。
  3. listen() - 监听连接请求

    • listen(sockfd, backlog);
    • 将套接字从一个主动的连接发起者转变为一个被动的连接监听者。从此,该套接字可以接受客户端的连接请求。
    • backlog 参数指定了“已完成连接队列”和“未完成连接队列”的总大小,即内核可以为这个套接字排队的最大连接请求数。
  4. accept() - 接受连接

    • int connfd = accept(sockfd, (struct sockaddr *)&cli_addr, &clilen);
    • 这是一个阻塞函数。它会从已完成连接队列中取出一个连接请求,并为这个新的连接创建一个新的套接字connfd)。
    • 后续服务器与该客户端的所有通信都通过这个新的 connfd 进行,而原来的 sockfd 则继续保持监听状态,等待其他客户端的连接。
    • 这个函数也会填充客户端的地址信息到 cli_addr 中。
  5. read() / recv()write() / send() - 数据交换

    • read(connfd, buffer, size);write(connfd, buffer, size);
    • 连接建立后,服务器就可以使用标准的 I/O 函数或专门的 socket I/O 函数,通过 connfd 与客户端进行双向的数据收发。
  6. close() - 关闭连接

    • close(connfd);
    • 当与一个客户端的通信结束后,服务器需要关闭这个为它创建的连接套接字 connfd
    • 当服务器程序本身要关闭时,也需要 close(sockfd) 来关闭监听套接字。

客户端 (Client) 的流程:

客户端的角色是主动向服务器发起连接请求。

  1. socket() - 创建套接字

    • int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    • 与服务器一样,客户端首先也需要创建一个 TCP 套接字。
  2. connect() - 发起连接

    • connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    • 这是客户端的核心步骤。它向 serv_addr 中指定的服务器 IP 地址和端口号发起一个连接请求(即发送 SYN 包,TCP 三次握手的开始)。
    • 这个函数也是阻塞的,直到连接成功建立(收到服务器的 SYN+ACK)或连接失败(例如,超时、服务器拒绝连接等),它才会返回。
    • 客户端通常不需要调用 bind(),操作系统会自动为其分配一个临时的端口号。
  3. write() / send()read() / recv() - 数据交换

    • write(sockfd, buffer, size);read(sockfd, buffer, size);
    • 连接成功后,客户端就可以通过 sockfd 与服务器进行数据收发。
  4. close() - 关闭连接

    • close(sockfd);
    • 通信结束后,客户端调用 close() 来关闭套接字,这会触发 TCP 四次挥手的开始。

总结流程:

步骤服务器 (Server)客户端 (Client)
1socket() (创建监听套接字)socket() (创建连接套接字)
2bind() (绑定地址和端口)-
3listen() (开始监听)-
4accept() (阻塞等待,接受连接,返回新套接字)connect() (阻塞发起连接)
5read()/write() (通过新套接字与客户端通信)read()/write() (通过连接套接字与服务器通信)
6close() (关闭连接套接字,最终关闭监听套接字)close() (关闭连接套接字)

16、new和malloc的区别是什么?

newmalloc 都是用于在堆(Heap)上动态分配内存的,但它们分属于不同的语言层级,并且在行为上有本质的区别。

特性new / delete -
本质是 C++ 的运算符 (Operator)。编译器会将其转换为对 operator new 函数的调用。 -
类型安全类型安全new T 会返回一个 T* 类型的指针,无需类型转换。 -
对象生命周期new 不仅仅是分配内存,它还会调用对象的构造函数 (Constructor) 来初始化对象。delete 则会先调用对象的析构函数 (Destructor),然后再释放内存。这是一个完整的对象生命周期管理。 -
内存大小自动计算需要分配的内存大小。new T 会根据类型 T 的大小来分配。new T[10] 会分配 10 * sizeof(T) 的空间(可能还有额外空间记录数组大小)。 -
异常处理如果内存分配失败,new 默认会抛出 std::bad_alloc 异常。可以通过 new(std::nothrow) 的形式来阻止抛出异常,此时如果失败会返回 nullptr,行为类似于 malloc。 -
重载operator newoperator delete 是可以被重载的。这允许我们自定义类的内存分配行为,例如实现内存池(Memory Pool)来优化特定对象的分配和回收。 -
特性malloc / free -
本质是 C 标准库中的函数 (Function)。 -
类型安全非类型安全malloc 返回的是 void* 类型的通用指针,必须通过强制类型转换static_cast 或 C 风格的 (T*))转换成目标类型的指针。这可能引入错误。 -
对象生命周期malloc 只负责分配指定大小的原始内存,它不了解 C++ 对象,因此不会调用构造函数free 只负责释放内存,不会调用析构函数。如果用 malloc 分配 C++ 对象,必须手动使用定位 new (Placement New) 来调用构造函数,并在释放前手动调用析构函数,非常繁琐且容易出错。 -
内存大小必须手动计算所需内存的大小,例如 malloc(10 * sizeof(T))。容易出错,比如忘记乘以 sizeof(T)。 -
异常处理如果内存分配失败,malloc 会返回 NULL (或 C++ 中的 nullptr)。需要程序员手动检查返回值来判断是否成功。 -
重载malloc 是一个库函数,不能被重载。 -

总结:

在 C++ 程序中,应该始终优先使用 newdelete。它们是 C++ 语言的一部分,能够正确处理对象的构造和析构,保证类型安全,并且支持异常处理和重载,这对于编写健壮、可维护的面向对象代码至关重要。

mallocfree 是 C 语言的遗产,只应该在需要与 C 库交互,或者进行非常底层的、不涉及 C++ 对象的内存操作时才使用。混用 newfree,或者 mallocdelete,是绝对禁止的,会导致未定义行为(通常是内存泄漏或程序崩溃)。

17、如果我只有一个2G的物理内存,new一个4G的对象,能实现吗?为什么

可以实现,但这并不意味着你真的“分配”了 4G 的物理内存。

这个问题的核心在于理解现代操作系统中的**虚拟内存(Virtual Memory)**机制。

为什么可以实现?

  1. 虚拟地址空间 (Virtual Address Space)

    • 当你调用 new 时,操作系统并不会立即为你分配 4G 的物理内存(Physical RAM)。它首先做的是在你的进程的虚拟地址空间中,为你预留(reserve)一块 4G 大小的连续地址范围。
    • 对于一个 64 位操作系统,其虚拟地址空间非常巨大(理论上是 2^64 字节),远超任何实际的物理内存大小。因此,从虚拟地址的角度看,预留 4G 空间是完全没有问题的。new 操作符此时会返回这个虚拟地址范围的起始地址,所以从程序的角度看,它成功获得了一个指针。
  2. 惰性分配 (Lazy Allocation) / 按需分页 (Demand Paging)

    • 仅仅预留了虚拟地址并不会消耗物理内存。物理内存只有在你实际访问(读取或写入)这块虚拟内存的某个页面(Page,通常是 4KB)时,才会由操作系统通过**缺页异常(Page Fault)**机制来分配。
    • 当你第一次访问 new 返回的 4G 空间中的某个地址时,CPU 发现这个虚拟地址没有映射到任何物理内存页,于是触发一个缺页异常。操作系统的异常处理程序会介入,分配一个物理内存页,并建立虚拟地址到物理地址的映射关系,然后程序才能继续执行。这个过程对应用程序是透明的。

会发生什么?

  • 如果只是分配而不使用new char[4 * 1024 * 1024 * 1024] 这行代码本身很可能会成功执行,因为它只涉及虚拟地址空间的分配。你的程序会得到一个指针,但物理内存的消耗几乎没有增加。

  • 如果尝试使用全部内存:如果你接下来尝试向这 4G 的空间中写入数据(例如,用一个循环去填充整个数组),情况就不同了。

    • 一开始,操作系统会不断地为你分配可用的 2G 物理内存页。当 2G 的物理内存全部被占用后,操作系统会开始使用**页面交换(Paging / Swapping)**机制。
    • 它会将物理内存中一些不常用的页面(可能属于你的进程,也可能属于其他进程)写入到硬盘上的一个特殊区域,称为**交换空间(Swap Space)**或页面文件(Page File)。然后把这块被换出的物理内存页分配给你当前需要访问的虚拟地址。
    • 这个过程会持续进行。你的 4G 对象的数据会在 2G 的物理内存和硬盘的交换空间之间来回倒换。这会导致程序的性能急剧下降,因为硬盘的读写速度比内存慢几个数量级。这个现象被称为颠簸(Thrashing)

结论:

  • 能实现吗?new 操作符成功返回一个指针的角度看,。这得益于虚拟内存机制。
  • 能有效使用吗? 不能。一旦你尝试使用的内存量(工作集)超出了物理内存的容量,系统就会发生颠簸,性能会变得极差,程序几乎无法正常工作,甚至可能因为交换空间耗尽或性能过低而被操作系统终止。

所以,虽然语法上允许你 new 一个比物理内存更大的对象,但在实践中,任何需要频繁访问远超物理内存大小的数据结构的设计都是不切实际的,需要重新考虑算法和数据结构(例如,采用内存映射文件或分块处理等技术)。

18、两个线程交替打印字符串,一个打印"1234",一个打印”abcd",要求打印输出为"1a2b3c4d"这种类型?

这是一个经典的多线程同步问题,考察的是如何精确地控制两个线程的执行顺序,让它们“一个接一个”地执行。使用 C++11 的 std::mutexstd::condition_variable 可以非常优雅地解决这个问题。

核心思想:

  1. 共享状态:需要一个共享的布尔标志位(例如 print_num_turn),用来指示当前应该由哪个线程执行打印操作。
  2. 互斥访问:对这个共享标志位以及 std::cout 的访问必须是互斥的,因此需要一个 std::mutex
  3. 等待与通知:当一个线程发现还不是轮到自己执行时,它应该进入等待状态,并允许另一个线程执行。当一个线程完成了自己的打印任务后,它需要通知另一个等待的线程“现在轮到你了”。这正是 std::condition_variable 的用武之地。

下面是完整的 C++ 实现代码:

cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <string>

// 共享资源
std::mutex mtx;
std::condition_variable cv;

// 共享状态:true 表示轮到打印数字的线程,false 表示轮到打印字母的线程
bool print_num_turn = true;

const std::string numbers = "1234";
const std::string letters = "abcd";

// 打印数字的线程函数
void print_numbers() {
    for (char c : numbers) {
        // 使用 unique_lock,因为它能与 condition_variable 配合使用
        std::unique_lock<std::mutex> lock(mtx);

        // 等待轮到自己。wait会检查lambda表达式,如果为false,则解锁并阻塞;
        // 被唤醒后,会再次加锁并重新检查lambda。
        // 这种写法可以防止“虚假唤醒” (spurious wakeup)。
        cv.wait(lock, [] { return print_num_turn; });

        // 轮到自己,执行打印
        std::cout << c;

        // 切换状态,轮到另一个线程
        print_num_turn = false;

        // 通知另一个正在等待的线程
        cv.notify_one();
    }
}

// 打印字母的线程函数
void print_letters() {
    for (char c : letters) {
        std::unique_lock<std::mutex> lock(mtx);

        // 等待轮到自己 (print_num_turn == false)
        cv.wait(lock, [] { return !print_num_turn; });

        // 轮到自己,执行打印
        std::cout << c;

        // 切换状态,轮到另一个线程
        print_num_turn = true;

        // 通知另一个正在等待的线程
        cv.notify_one();
    }
}

int main() {
    // 创建两个线程
    std::thread t1(print_numbers);
    std::thread t2(print_letters);

    // 等待两个线程执行完毕
    t1.join();
    t2.join();

    std::cout << std::endl;

    return 0;
}

代码逻辑详解:

  1. print_numbers 线程

    • 进入循环,准备打印第一个数字 '1'。
    • 获取 unique_lock,锁住互斥量 mtx
    • 调用 cv.wait(lock, []{ return print_num_turn; });
      • main 函数开始时 print_num_turntrue,所以 lambda 返回 truewait 函数直接返回,线程继续执行。
    • 打印 '1'。
    • print_num_turn 设置为 false,表示接下来该轮到字母线程了。
    • 调用 cv.notify_one(),唤醒可能正在等待的 print_letters 线程。
    • 循环结束,lock 析构,自动解锁 mtx
    • 进入下一次循环,准备打印 '2'。但此时 print_num_turnfalse,所以 cv.wait 会使其阻塞,直到 print_letters 线程将其唤醒。
  2. print_letters 线程

    • 启动后,进入循环,准备打印 'a'。
    • 获取 unique_lock(可能会因为 t1 正持有锁而阻塞一会儿)。
    • 调用 cv.wait(lock, []{ return !print_num_turn; });
      • 此时 print_num_turntrue,lambda 返回 false。于是 wait 函数会原子地释放 lock 并使 t2 线程进入阻塞状态。
    • t1 打印完 '1' 并调用 cv.notify_one() 后,t2 被唤醒。
    • wait 函数在返回前会重新获取 lock,并再次检查 lambda 条件。此时 print_num_turn 已经是 false 了,lambda 返回 truewait 成功返回。
    • 打印 'a'。
    • print_num_turn 设置为 true
    • 调用 cv.notify_one(),唤醒正在等待的 t1 线程。
    • lock 析构,解锁。

这个过程交替进行,print_num_turn 标志位就像一个令牌,两个线程轮流持有它,从而实现了 1a2b3c4d 的精确交替打印。

16、new和malloc的区别是什么?

newmalloc都是用于动态内存分配的机制,但它们在C++中有着本质的区别和不同的使用场景。

new运算符

  • 类型安全new是C++的运算符,它在分配内存时会调用对象的构造函数来初始化对象,并在释放内存时调用析构函数。它返回的是具体类型的指针,无需强制类型转换。
  • 内存分配失败处理:当内存分配失败时,new默认会抛出std::bad_alloc异常。可以通过new(std::nothrow)来避免抛出异常,此时如果分配失败会返回nullptr
  • 可重载newdelete运算符可以被用户重载,以实现自定义的内存管理策略。
  • 分配单个对象或数组new可以分配单个对象(new T)或对象数组(new T[N])。分配数组时,new会额外存储数组大小信息,以便delete[]能正确调用所有元素的析构函数。
  • 效率:通常情况下,new在分配小块内存时可能会比malloc稍慢,因为它涉及到构造函数的调用和可能的异常处理机制。

malloc函数

  • 非类型安全malloc是C标准库函数,它只负责分配指定大小的原始内存块,不关心内存中存储的数据类型,也不会调用构造函数或析构函数。它返回void*指针,需要显式地进行类型转换。
  • 内存分配失败处理:当内存分配失败时,malloc会返回NULL指针,需要手动检查返回值。
  • 不可重载malloc是库函数,不能被用户重载。
  • 只分配原始内存malloc只能分配原始的字节块,不区分单个对象或数组,因此没有对应的free[]函数,统一使用free释放。
  • 效率:在某些情况下,malloc可能比new更快,因为它不涉及对象的构造和析构。

总结对比

特性newmalloc
语言C++运算符C标准库函数
类型安全是,返回具体类型指针,无需转换否,返回void*,需强制类型转换
构造/析构调用构造函数和析构函数不调用构造函数和析构函数
失败处理默认抛出std::bad_alloc异常,或返回nullptrnothrow返回NULL指针
可重载
分配对象可分配单个对象或对象数组只能分配原始字节块
配套释放delete / delete[]free

在C++编程中,通常推荐使用newdelete来管理动态内存,因为它更符合C++的面向对象特性,能够正确处理对象的生命周期。只有在与C语言代码交互、需要分配原始内存块或进行特殊内存管理时,才考虑使用mallocfree

17、如果我只有一个2G的物理内存,new一个4G的对象,能实现吗?为什么?

在现代操作系统中,通常情况下是可以实现的,但这涉及到**虚拟内存(Virtual Memory)**的概念。

为什么可以实现?

  1. 虚拟内存机制:现代操作系统都实现了虚拟内存管理。每个进程都有自己独立的虚拟地址空间(例如,在64位系统上通常是256TB或更多),这个虚拟地址空间远大于实际的物理内存。当程序请求内存时,操作系统分配的是虚拟地址空间,而不是直接分配物理内存。
  2. 按需分配物理内存:操作系统并不会在new操作时立即将所有4GB的物理内存分配给进程。它只是在进程的虚拟地址空间中预留了4GB的区域,并建立了虚拟地址到物理地址的映射关系。只有当程序实际访问这些虚拟地址时(例如,对这4GB内存进行读写操作),操作系统才会通过**页错误(Page Fault)机制,将所需的虚拟页面映射到物理内存中的某个页面,或者从磁盘上的交换空间(Swap Space)**中加载数据到物理内存。
  3. 交换空间(Swap Space):如果物理内存不足以容纳所有活跃的虚拟页面,操作系统会将不常用的物理页面内容暂时存储到硬盘上的交换空间中,从而腾出物理内存供当前活跃的程序使用。当程序再次访问被换出的页面时,操作系统会将其从交换空间重新加载到物理内存。

潜在的问题和限制:

尽管可以new一个大于物理内存的对象,但这并不意味着程序可以高效运行,或者说总是能成功:

  • 性能下降:如果程序频繁访问这4GB内存,而物理内存只有2GB,会导致大量的页面置换(Paging/Swapping)操作,即数据在物理内存和硬盘交换空间之间频繁移动。硬盘的读写速度远低于内存,这将导致程序性能急剧下降,系统变得非常缓慢,这种现象被称为“颠簸”(Thrashing)
  • 交换空间限制:如果系统没有足够的交换空间,或者交换空间也耗尽了,那么即使有虚拟内存机制,操作系统也无法为新的内存请求提供支持,此时new操作可能会失败,抛出std::bad_alloc异常。
  • 系统资源耗尽:即使有足够的交换空间,过度的页面置换也会占用大量的CPU时间进行内存管理,影响整个系统的响应速度。

结论

在有虚拟内存和足够交换空间的现代操作系统中,new一个4GB的对象在2GB物理内存的机器上是可能成功的。然而,这通常会导致严重的性能问题,甚至可能耗尽系统资源导致程序崩溃。因此,在实际开发中,仍然需要关注程序的内存使用量,尽量避免分配远超物理内存的巨大对象,或者优化内存访问模式以减少页面置换。

18、两个线程交替打印字符串,一个打印"1234",一个打印”abcd",要求打印输出为"1a2b3c4d"这种类型?

这是一个经典的线程同步问题,可以使用多种同步机制来实现,例如互斥量(Mutex)条件变量(Condition Variable),或者信号量(Semaphore)。这里我们使用互斥量和条件变量来实现。

核心思路

  1. 共享资源:两个线程需要共享一个打印的“轮次”或“状态”变量,以决定当前哪个线程可以打印。
  2. 互斥访问:使用互斥量保护共享状态变量和打印操作,确保同一时间只有一个线程能修改状态或进行打印。
  3. 条件等待/通知:使用条件变量让线程在不满足打印条件时等待,并在满足条件时被另一个线程唤醒。

实现步骤

  1. 定义一个互斥量std::mutex
  2. 定义两个条件变量std::condition_variable,一个用于线程A等待,一个用于线程B等待。
  3. 定义一个共享的turn变量(例如int类型),表示当前轮到哪个线程打印(例如0代表线程A,1代表线程B)。
  4. 线程A在打印前,检查turn是否为0。如果不是,则在条件变量上等待。如果是,则打印字符,然后将turn设置为1,并通知线程B的条件变量。
  5. 线程B在打印前,检查turn是否为1。如果不是,则在条件变量上等待。如果是,则打印字符,然后将turn设置为0,并通知线程A的条件变量。
  6. 循环这个过程,直到所有字符打印完毕。

下面是C++11及更高版本中,使用std::mutexstd::condition_variable的实现示例:

cpp
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>

// 共享资源
std::mutex mtx; // 互斥量,保护共享数据
std::condition_variable cv_char; // 条件变量,用于字符线程等待
std::condition_variable cv_num;  // 条件变量,用于数字线程等待
int turn = 0; // 0 for num_thread, 1 for char_thread

void num_thread_func(const std::string& s)
{
    for (char c : s)
    {
        std::unique_lock<std::mutex> lock(mtx);
        // 等待,直到轮到数字线程打印
        cv_num.wait(lock, []{ return turn == 0; });

        std::cout << c;
        turn = 1; // 切换到字符线程
        cv_char.notify_one(); // 通知字符线程
    }
}

void char_thread_func(const std::string& s)
{
    for (char c : s)
    {
        std::unique_lock<std::mutex> lock(mtx);
        // 等待,直到轮到字符线程打印
        cv_char.wait(lock, []{ return turn == 1; });

        std::cout << c;
        turn = 0; // 切换到数字线程
        cv_num.notify_one(); // 通知数字线程
    }
}

int main()
{
    std::string s1 = "1234";
    std::string s2 = "abcd";

    std::thread t1(num_thread_func, s1);
    std::thread t2(char_thread_func, s2);

    t1.join();
    t2.join();

    std::cout << std::endl; // 打印完后换行

    return 0;
}

代码解释

  1. std::mutex mtx;:创建一个互斥量,用于保护对turn变量和std::cout的访问,确保线程安全。
  2. std::condition_variable cv_char;std::condition_variable cv_num;:创建两个条件变量。cv_num用于数字线程等待,cv_char用于字符线程等待。
  3. int turn = 0;:一个共享变量,表示当前轮到哪个线程执行。初始值为0,表示数字线程先开始。
  4. num_thread_funcchar_thread_func
    • 每个线程函数都遍历其对应的字符串。
    • std::unique_lock<std::mutex> lock(mtx);:在进入临界区前获取互斥量的锁。std::unique_lock是RAII(Resource Acquisition Is Initialization)风格的锁,它会在作用域结束时自动释放锁。
    • cv_num.wait(lock, []{ return turn == 0; }); (在num_thread_func中):数字线程会在这里等待。wait函数会原子性地释放锁lock,并将当前线程置于等待状态。当turn == 0的条件满足时,或者被notify_one唤醒时,线程会重新尝试获取锁,并在条件满足后继续执行。如果条件不满足,它会继续等待。
    • std::cout << c;:打印当前字符。
    • turn = 1; (在num_thread_func中):将turn设置为1,表示下一轮轮到字符线程。
    • cv_char.notify_one(); (在num_thread_func中):通知一个正在cv_char上等待的线程(即字符线程)可以尝试获取锁并检查条件了。
    • char_thread_func的逻辑与num_thread_func类似,只是等待的条件和通知的对象相反。
  5. main函数
    • 创建两个std::thread对象,分别运行num_thread_funcchar_thread_func
    • t1.join();t2.join();:等待两个线程执行完毕,确保主线程在子线程结束前不会退出。

这种机制确保了两个线程能够严格按照预期的顺序交替执行,从而实现“1a2b3c4d”的输出。