C++面试题(自用)

  1. 智能指针实现原理
    智能指针是一种封装了原生指针的类,通过引用计数或所有权机制来管理动态分配的内存资源。它的核心原理是利用 RAII(Resource Acquisition Is Initialization)技术,确保在智能指针对象生命周期结束时自动释放所管理的资源。常见的智能指针包括 std::unique_ptrstd::shared_ptrstd::weak_ptr

  2. 智能指针,里面的计数器何时会改变
    对于 std::shared_ptr,计数器会在以下情况下改变:

    • 当一个新的 shared_ptr 被创建并指向同一块内存时,引用计数加 1。
    • 当一个 shared_ptr 被销毁或重置时,引用计数减 1。
    • 当引用计数减到 0 时,自动释放所管理的资源。
  3. 智能指针和管理的对象分别在哪个区

    • 智能指针本身是一个栈区对象,它的生命周期由栈的规则管理。
    • 智能指针所管理的资源位于堆区,通过智能指针的析构函数自动释放。
  4. 面向对象的特性:多态原理
    多态是指通过基类的指针或引用调用派生类的重写函数,从而实现不同行为的能力。多态的原理依赖于虚函数表和动态绑定:

    • 虚函数表(vtable):每个包含虚函数的类都有一个虚函数表,存储虚函数的地址。
    • 动态绑定:在运行时根据对象的实际类型调用相应的虚函数。
  5. 介绍一下虚函数,虚函数怎么实现的
    虚函数是用于实现多态的成员函数,通过 virtual 关键字声明。虚函数的实现依赖于虚函数表(vtable):

    • 每个包含虚函数的类都有一个虚函数表,存储虚函数的地址。
    • 对象中包含一个指向虚函数表的指针(vptr),在运行时根据 vptr 找到正确的函数地址。
  6. 多态和继承在什么情况下使用

    • 继承:当需要复用已有类的代码或表示“is-a”关系时使用。
    • 多态:当需要通过基类接口调用派生类的不同实现时使用,通常与继承结合。
  7. 除了多态和继承还有什么面向对象方法

    • 封装:隐藏对象的内部实现细节,提供公共接口。
    • 抽象:定义接口或基类,隐藏具体实现。
    • 组合:通过将对象作为成员变量来实现代码复用。
  8. C++内存分布。什么样的数据在栈区,什么样的在堆区

    • 栈区:局部变量、函数参数、函数返回地址等,生命周期由作用域管理。
    • 堆区:动态分配的内存(如 newmalloc 分配的内存),生命周期由程序员管理。
    • 全局/静态区:全局变量和静态变量。
    • 常量区:存储常量字符串等。
    • 代码区:存储程序的二进制代码。
  9. C++内存管理(RAII啥的)
    RAII(Resource Acquisition Is Initialization)是 C++ 的核心内存管理技术,通过对象的生命周期管理资源:

    • 在构造函数中获取资源(如分配内存)。
    • 在析构函数中释放资源(如释放内存)。
    • 智能指针是 RAII 的典型应用。
  10. C++从源程序到可执行程序的过程

    • 预处理:处理宏、头文件等,生成 .i 文件。
    • 编译:将预处理后的代码编译成汇编代码,生成 .s 文件。
    • 汇编:将汇编代码转换成机器码,生成 .o 文件。
    • 链接:将多个目标文件链接成可执行文件。
  11. 一个对象=另一个对象会发生什么(赋值构造函数)
    如果对象已经存在,赋值操作会调用赋值运算符函数(operator=)。如果没有显式定义赋值运算符,编译器会生成默认的赋值运算符,执行成员变量的逐字节复制(浅拷贝)。对于动态资源管理,通常需要自定义赋值运算符以支持深拷贝。

  12. C++11的智能指针有哪些。weak_ptr的使用场景。什么情况下会产生循环引用

    • C++11 的智能指针包括 std::unique_ptrstd::shared_ptrstd::weak_ptr
    • weak_ptr 的使用场景:用于解决 shared_ptr 的循环引用问题。weak_ptr 不会增加引用计数,只是观察资源,避免循环引用导致的内存泄漏。
    • 循环引用:当两个或多个 shared_ptr 相互引用时,它们的引用计数永远不会减到 0,导致内存无法释放。
  13. 多进程 fork 后不同进程会共享哪些资源

    • fork 后,子进程会复制父进程的地址空间,包括代码段、数据段、堆、栈等。
    • 共享的资源包括:
      • 打开的文件描述符。
      • 信号处理函数。
      • 进程的某些属性(如用户 ID、组 ID)。
    • 不共享的资源包括:
      • 进程的地址空间(虽然是复制,但独立修改)。
      • 进程 ID(PID)。
  14. 多线程里线程的同步方式有哪些

    • 互斥锁(std::mutex):保护共享资源,防止数据竞争。
    • 条件变量(std::condition_variable):用于线程间通信,等待特定条件满足。
    • 信号量(semaphore):控制对共享资源的访问数量。
    • 读写锁(std::shared_mutex):允许多个读操作,但写操作独占。
    • 原子操作(std::atomic):确保操作的原子性。
  15. sizeof 是在编译期还是在运行期确定
    sizeof 是在编译期确定的,它返回类型或对象的大小,编译器会根据类型信息直接计算。

  16. 函数重载的机制。重载是在编译期还是在运行期确定

    • 函数重载的机制:通过函数名相同但参数列表不同(参数类型、数量或顺序)来实现。
    • 重载是在编译期确定的,编译器根据调用时的参数类型选择最匹配的函数。
  17. 指针常量和常量指针

    • 指针常量(int* const p):指针本身是常量,不能修改指针的指向,但可以修改指向的值。
    • 常量指针(const int* p):指向的值是常量,不能修改值,但可以修改指针的指向。
  18. vector 的原理,怎么扩容

    • vector 是一个动态数组,底层是连续的内存空间。
    • 扩容机制:当元素数量超过当前容量时,vector 会分配一块更大的内存(通常是原容量的 2 倍),将原有元素拷贝到新内存中,并释放旧内存。
  19. 介绍一下 const

    • const 用于定义常量,表示值不可修改。
    • 可以修饰变量、函数参数、成员函数等。
    • 例如:
      • const int a = 10;a 是常量,不能修改。
      • void func(const int& x)x 是常量引用,不能修改 x 的值。
      • int getValue() const:成员函数是常量函数,不能修改对象的成员变量。
  20. 引用和指针的区别

    • 引用:
      • 是变量的别名,必须初始化。
      • 不能为空,不能重新绑定到其他变量。
      • 语法更简洁,使用 . 访问成员。
    • 指针:
      • 存储变量的地址,可以为空。
      • 可以重新指向其他变量。
      • 使用 *-> 访问成员。
  21. C++新特性知道哪些

    • C++11:autonullptr、智能指针、范围 for 循环、lambda 表达式、std::thread 等。
    • C++14:泛型 lambdaconstexpr 增强等。
    • C++17:结构化绑定、std::optionalstd::variantstd::filesystem 等。
    • C++20:概念(concepts)、范围库(ranges)、协程(coroutines)等。
  22. 类型转换

    • C++ 提供了四种类型转换操作符:
      • static_cast:用于基本类型转换、父子类指针转换。
      • dynamic_cast:用于多态类型转换,运行时检查。
      • const_cast:去除 constvolatile 属性。
      • reinterpret_cast:用于低级别的类型转换,如指针和整数之间的转换。
  23. RAII基于什么实现的(生命周期、作用域、构造析构)
    RAII(Resource Acquisition Is Initialization)基于对象的生命周期和作用域实现:

    • 在对象的构造函数中获取资源(如分配内存、打开文件)。
    • 在对象的析构函数中释放资源(如释放内存、关闭文件)。
    • 利用栈对象超出作用域时自动析构的特性,确保资源被正确释放。
  24. 手撕:Unique_ptr,控制权转移(移动语义);手撕:类继承,堆/栈上分别代码实现多态

    • Unique_ptr 实现:

      template<typename T>
      class UniquePtr {
      public:
          UniquePtr(T* ptr = nullptr) : ptr_(ptr) {}
          ~UniquePtr() { delete ptr_; }
          UniquePtr(const UniquePtr&) = delete; // 禁止拷贝构造
          UniquePtr& operator=(const UniquePtr&) = delete; // 禁止拷贝赋值
          UniquePtr(UniquePtr&& other) noexcept : ptr_(other.ptr_) {
              other.ptr_ = nullptr;
          }
          UniquePtr& operator=(UniquePtr&& other) noexcept {
              if (this != &other) {
                  delete ptr_;
                  ptr_ = other.ptr_;
                  other.ptr_ = nullptr;
              }
              return *this;
          }
          T* get() const { return ptr_; }
          T& operator*() const { return *ptr_; }
          T* operator->() const { return ptr_; }
      private:
          T* ptr_;
      };
      
    • 类继承,堆/栈上实现多态:

      class Base {
      public:
          virtual void print() { std::cout << "Base" << std::endl; }
          virtual ~Base() = default;
      };
      
      class Derived : public Base {
      public:
          void print() override { std::cout << "Derived" << std::endl; }
      };
      
      // 栈上多态
      Base base;
      Derived derived;
      Base* ptr = &derived;
      ptr->print(); // 输出 "Derived"
      
      // 堆上多态
      Base* heapPtr = new Derived();
      heapPtr->print(); // 输出 "Derived"
      delete heapPtr;
      
  25. unique_ptrshared_ptr 区别

    • unique_ptr:独占所有权,不能拷贝,只能通过移动语义转移所有权。
    • shared_ptr:共享所有权,通过引用计数管理资源,支持拷贝和赋值。
  26. 右值引用

    • 右值引用(T&&)用于绑定临时对象或即将销毁的对象,支持移动语义和完美转发。
    • 示例:
      void func(int&& x) { std::cout << x << std::endl; }
      func(10); // 10 是右值
      
  27. 函数参数可不可以传右值
    可以,通过右值引用参数传递右值。例如:

    void func(int&& x) { std::cout << x << std::endl; }
    func(10); // 10 是右值
    
  28. 参考 C/C++ 堆栈实现自己的堆栈。要求:不能用 STL 容器

    class MyStack {
    public:
        MyStack(int capacity) : capacity_(capacity), size_(0) {
            data_ = new int[capacity_];
        }
        ~MyStack() { delete[] data_; }
        void push(int value) {
            if (size_ >= capacity_) throw std::overflow_error("Stack is full");
            data_[size_++] = value;
        }
        int pop() {
            if (size_ <= 0) throw std::underflow_error("Stack is empty");
            return data_[--size_];
        }
        int top() const {
            if (size_ <= 0) throw std::underflow_error("Stack is empty");
            return data_[size_ - 1];
        }
        bool empty() const { return size_ == 0; }
        int size() const { return size_; }
    private:
        int* data_;
        int capacity_;
        int size_;
    };
    
  29. STL 容器了解吗?底层如何实现:vector 数组,map 红黑树,红黑树的实现

    • vector:动态数组,底层是连续内存空间,支持随机访问。
    • map:基于红黑树实现,保证键值有序,插入、删除、查找时间复杂度为 O(log n)。
    • 红黑树:一种自平衡二叉查找树,满足以下性质:
      1. 每个节点是红色或黑色。
      2. 根节点是黑色。
      3. 每个叶子节点(NIL)是黑色。
      4. 红色节点的子节点必须是黑色。
      5. 从任一节点到其每个叶子的路径包含相同数目的黑色节点。
  30. 完美转发介绍一下。去掉 std::forward 会怎样?

    • 完美转发:通过 std::forward 将参数以原始类型(左值或右值)传递给其他函数,保持值类别不变。
    • 去掉 std::forward:可能导致参数被当作左值处理,失去移动语义,性能下降。
  31. 介绍一下 unique_locklock_guard 区别?

    • lock_guard:简单的 RAII 锁,构造时加锁,析构时解锁,不支持手动控制。
    • unique_lock:更灵活的锁,支持手动加锁、解锁,可以转移所有权,支持延迟加锁。
  32. C 代码中引用 C++ 代码有时候会报错为什么?

    • C++ 支持函数重载,编译器会对函数名进行修饰(name mangling),导致 C 编译器无法识别。
    • 解决方法:在 C++ 代码中使用 extern "C" 声明,禁止名称修饰。
  33. 静态多态有什么?虚函数原理:虚表是什么时候建立的?为什么要把析构函数设置成虚函数?

    • 静态多态:通过函数重载和模板实现,编译时确定。
    • 虚函数原理:通过虚函数表(vtable)实现动态绑定,虚表在编译时建立,运行时使用。
    • 析构函数设置为虚函数:确保派生类对象通过基类指针删除时,调用正确的析构函数,避免资源泄漏。
  34. map 为啥用红黑树不用 AVL 树?(几乎所有面试都问了 mapunordered_map 区别)

    • 红黑树:插入、删除、查找的时间复杂度均为 O(log n),且插入和删除时旋转操作较少,性能更优。
    • AVL 树:虽然查找更快,但插入和删除时旋转操作较多,性能较差。
    • mapunordered_map 区别:
      • map:基于红黑树,键值有序,查找时间复杂度 O(log n)。
      • unordered_map:基于哈希表,键值无序,查找时间复杂度 O(1)。
  35. inline 失效场景

    • 函数体过大:编译器可能忽略 inline 建议。
    • 递归函数:无法内联。
    • 函数指针调用:无法内联。
    • 虚函数:动态绑定,无法内联。
    • 编译器优化设置:某些优化级别可能忽略 inline
  36. C++ 中 structclass 区别

    • 默认访问权限:
      • struct 的成员默认是 public
      • class 的成员默认是 private
    • 其他:
      • 除此之外,structclass 的功能几乎完全相同,都可以包含成员函数、继承、多态等。
  37. 如何防止一个头文件 include 多次
    使用头文件保护宏(Header Guard):

    #ifndef MY_HEADER_H
    #define MY_HEADER_H
    // 头文件内容
    #endif // MY_HEADER_H
    

    或者使用 #pragma once(非标准但广泛支持):

    #pragma once
    // 头文件内容
    
  38. lambda 表达式的理解,它可以捕获哪些类型

    • Lambda 表达式是一种匿名函数,可以捕获外部变量。
    • 捕获方式:
      • [=]:以值捕获所有外部变量。
      • [&]:以引用捕获所有外部变量。
      • [x, &y]:以值捕获 x,以引用捕获 y
      • [this]:捕获当前对象的指针。
      • []:不捕获任何变量。
  39. 友元 friend 介绍

    • friend 关键字用于声明友元函数或友元类,使其可以访问类的私有成员。
    • 示例:
      class MyClass {
      private:
          int data;
          friend void friendFunction(MyClass& obj);
      };
      
      void friendFunction(MyClass& obj) {
          obj.data = 10; // 可以访问私有成员
      }
      
  40. move 函数

    • std::move 用于将对象转换为右值,支持移动语义。
    • 示例:
      std::string str1 = "Hello";
      std::string str2 = std::move(str1); // str1 的资源被移动到 str2
      
  41. 模版类的作用

    • 模板类用于实现通用类,支持多种数据类型。
    • 示例:
      template<typename T>
      class Box {
      public:
          T value;
          Box(T v) : value(v) {}
      };
      
      Box<int> intBox(10);
      Box<std::string> strBox("Hello");
      
  42. 模版和泛型的区别

    • C++ 模板:编译时生成代码,支持多种类型,功能强大但复杂。
    • Java/C# 泛型:运行时实现,类型擦除,功能受限但简单。
  43. 内存管理:C++ 的 newmalloc 的区别

    • new
      • 是 C++ 操作符,调用构造函数。
      • 返回类型安全指针,不需要类型转换。
      • 支持异常处理。
    • malloc
      • 是 C 标准库函数,不调用构造函数。
      • 返回 void*,需要类型转换。
      • 不支持异常处理。
  44. new 可以重载吗,可以改写 new 函数吗

    • 可以重载全局 new 和类特定的 new
    • 示例:
      void* operator new(size_t size) {
          std::cout << "Custom new" << std::endl;
          return malloc(size);
      }
      
  45. C++ 中的 mapunordered_map 的区别和使用场景

    • map
      • 基于红黑树实现,键值有序。
      • 插入、删除、查找时间复杂度为 O(log n)。
      • 适用于需要有序键值的场景。
    • unordered_map
      • 基于哈希表实现,键值无序。
      • 插入、删除、查找时间复杂度为 O(1)。
      • 适用于需要快速查找且不关心顺序的场景。
  46. 他们是线程安全的吗

    • mapunordered_map 都不是线程安全的。
    • 如果多线程访问,需要手动加锁(如 std::mutex)。
  47. C++ 标准库里优先队列是怎么实现的?

    • std::priority_queue 是基于堆(默认是大顶堆)实现的。
    • 底层容器默认是 std::vector
    • 主要操作:
      • push:插入元素,时间复杂度 O(log n)。
      • pop:删除堆顶元素,时间复杂度 O(log n)。
      • top:访问堆顶元素,时间复杂度 O(1)。
    • 示例:
      std::priority_queue<int> pq;
      pq.push(10);
      pq.push(5);
      pq.push(20);
      std::cout << pq.top(); // 输出 20
      
  48. GCC 编译的过程
    GCC 编译过程分为以下步骤:

    1. 预处理:处理宏、头文件等,生成 .i 文件。
      gcc -E source.c -o source.i
      
    2. 编译:将预处理后的代码编译成汇编代码,生成 .s 文件。
      gcc -S source.i -o source.s
      
    3. 汇编:将汇编代码转换成机器码,生成 .o 文件。
      gcc -c source.s -o source.o
      
    4. 链接:将多个目标文件链接成可执行文件。
      gcc source.o -o executable
      
  49. C++ Coroutine
    C++20 引入了协程(Coroutine),用于简化异步编程。协程是一种可以暂停和恢复的函数,核心关键字包括:

    • co_await:暂停协程,等待异步操作完成。
    • co_return:返回协程的结果。
    • co_yield:生成一个值并暂停协程。
      示例:
    #include <coroutine>
    #include <iostream>
    
    struct MyCoroutine {
        struct promise_type {
            MyCoroutine get_return_object() { return {}; }
            std::suspend_always initial_suspend() { return {}; }
            std::suspend_always final_suspend() noexcept { return {}; }
            void return_void() {}
            void unhandled_exception() {}
        };
    };
    
    MyCoroutine my_coroutine() {
        std::cout << "Coroutine started" << std::endl;
        co_await std::suspend_always{};
        std::cout << "Coroutine resumed" << std::endl;
    }
    
    int main() {
        auto coro = my_coroutine();
        // 手动恢复协程
        coro.coro.resume();
    }
    
  50. extern "C" 有什么作用
    extern "C" 用于在 C++ 代码中声明 C 语言的函数或变量,防止 C++ 编译器对函数名进行名称修饰(name mangling)。
    示例:

    extern "C" {
        void my_function(int x);
    }
    
  51. C++ memory_order / ELF 文件格式 / 中断对于操作系统的作用

    • memory_order:用于指定原子操作的内存顺序(如 memory_order_relaxedmemory_order_acquire 等)。
    • ELF 文件格式:可执行和可链接格式(Executable and Linkable Format),用于存储可执行文件、目标文件和共享库。
    • 中断对于操作系统的作用:中断是操作系统处理异步事件的核心机制,用于响应硬件事件(如键盘输入、时钟中断等)和软件事件(如系统调用)。
  52. C++ 的符号表
    符号表是编译器生成的一种数据结构,用于存储程序中定义的符号(如函数名、变量名)及其相关信息(如类型、地址)。在链接阶段,符号表用于解析符号引用。

  53. C++ 的单元测试
    单元测试是对代码的最小单元(如函数、类)进行测试,常用单元测试框架包括:

    • Google Test
      示例:
      #include <gtest/gtest.h>
      
      int add(int a, int b) {
          return a + b;
      }
      
      TEST(AddTest, PositiveNumbers) {
          EXPECT_EQ(add(1, 2), 3);
      }
      
      int main(int argc, char **argv) {
          ::testing::InitGoogleTest(&argc, argv);
          return RUN_ALL_TESTS();
      }
      
    • Catch2
      示例:
      #define CATCH_CONFIG_MAIN
      #include <catch2/catch.hpp>
      
      int add(int a, int b) {
          return a + b;
      }
      
      TEST_CASE("Add function", "[add]") {
          REQUIRE(add(1, 2) == 3);
      }
      
    • Boost.Test
      示例:
      #define BOOST_TEST_MODULE AddTest
      #include <boost/test/unit_test.hpp>
      
      int add(int a, int b) {
          return a + b;
      }
      
      BOOST_AUTO_TEST_CASE(AddTest) {
          BOOST_CHECK_EQUAL(add(1, 2), 3);
      }
      
  54. 如果 new 了之后出了问题直接 return。会导致内存泄漏。怎么办

    可以使用智能指针(如 std::unique_ptrstd::shared_ptr)来管理动态分配的内存,确保在异常或提前返回时自动释放资源。例如:

    void func() {
        std::unique_ptr<int> ptr(new int(10));
        if (/* 出问题 */) return; // 自动释放内存
    }
    

    或者使用 RAII 技术,将资源封装在对象中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值