【C++面试系列】基础c++/STL题单

这是一份为大部分会用到C++的岗位的求职者的题单。


一、C++基础概念

1. C和C++的主要区别是什么?

  • 解析: C++是C的超集,但增加了面向对象编程(类、继承、多态)、泛型编程(模板)、异常处理、命名空间、函数重载、引用等特性。C是过程式语言,而C++是多范式语言。
  • 注解: 面试官问这个是为了确认你对C++的定位有基本了解。可以补充说明“C++不是纯粹的OOP语言,它支持多种编程风格”。

2. const#define 定义常量有什么区别?

  • 解析
    • #define 是预处理指令,进行简单的文本替换,没有类型检查,不占用内存(在符号表中)。
    • const 是编译器处理的常量,有类型检查,会占用内存(可能在只读数据区或被优化掉)。const常量可以被调试,而#define的宏在调试时已经被替换。
  • 注解: 强调类型安全可调试性是现代C++更推荐使用const的原因。

3. static 关键字有哪些用法?

  • 解析: 这是一个高频考点。
    • 在函数/文件内:修饰局部变量,改变其生命周期,使其在程序整个生命周期内存在,且只初始化一次。
    • 在全局变量/函数前:改变其链接性,使其仅在当前文件内可见(内部链接)。
    • 在类中
      • 修饰成员变量:该变量属于类本身,所有对象共享一份数据。
      • 修饰成员函数:该函数属于类本身,不能访问类的非静态成员(没有this指针)。
  • 注解: 可以按作用域(函数、文件、类)来分类回答,显得条理清晰。

4. sizeofstrlen 的区别?

  • 解析
    • sizeof运算符,在编译时计算数据类型或对象所占的内存大小(字节数)。对于字符串,它包含末尾的\0
    • strlen库函数,在运行时计算字符串的实际长度(字符数),遇到\0即停止,不包含\0
  • 注解: 举例说明:char str[] = "hello";sizeof(str) 是6(5个字符+\0),strlen(str) 是5。

二、面向对象编程(OOP)

1. C++面向对象的三大特性是什么?

  • 解析封装、继承、多态
    • 封装: 将数据和操作数据的函数绑定在一起,并对外部隐藏实现细节。
    • 继承: 允许新类(派生类)从现有类(基类)获取属性和行为,实现代码复用。
    • 多态: 允许不同类的对象对同一消息做出不同的响应。主要通过虚函数继承实现。
  • 注解: 不要只背名字,要能简要解释每个特性的目的。

2. 什么是构造函数和析构函数?它们可以是虚函数吗?

  • 解析
    • 构造函数: 在对象创建时自动调用,用于初始化对象。不能是虚函数,因为在调用构造函数时,对象的虚表指针(vptr)还未正确初始化。
    • 析构函数: 在对象销毁时自动调用,用于清理资源。基类的析构函数必须是虚函数(除非类不被继承),否则通过基类指针删除派生类对象时,只会调用基类的析构函数,导致派生类的资源泄漏(派生类对象析构不完全)。
  • 注解: “基类析构函数必须为虚函数”是面试必考点,务必理解其背后的原因。

3. 什么是虚函数?它的实现原理是什么?

  • 解析: 使用virtual关键字声明的成员函数,用于实现运行时多态。
  • 原理虚函数表(vtable)虚表指针(vptr)
    1. 编译器会为每个包含虚函数的类创建一个虚函数表(vtable),表中存放该类所有虚函数的函数指针。
    2. 每个该类对象内部都有一个隐藏的虚表指针(vptr),指向该类的vtable。
    3. 当通过基类指针调用虚函数时,程序通过对象的vptr找到对应的vtable,再从vtable中找到正确的函数地址进行调用。
  • 注解: 这是理解C++多态机制的基石,必须掌握。可以画图辅助说明。

4. 重载(Overload)、覆盖(Override)、隐藏(Hiding)的区别?

  • 解析
    • 重载: 同一作用域内,函数名相同但参数列表不同(类型、个数、顺序)。与返回值、virtual无关。
    • 覆盖/重写: 发生在派生类和基类之间。派生类重新定义基类的虚函数,要求函数名、参数列表、返回值都必须相同(协变返回值除外)。
    • 隐藏: 派生类的函数屏蔽了基类的同名函数(无论是否为虚函数)。只要函数名相同,且不构成覆盖,就会发生隐藏。
  • 注解: 这是一个极易混淆的概念,可以通过代码示例来区分。

三、内存管理

1. new/deletemalloc/free 的区别?

  • 解析
    特性new/deletemalloc/free
    语言C++运算符C库函数
    内存大小编译器自动计算需手动指定字节数
    返回值返回确切类型指针返回void*,需强制转换
    失败行为抛出bad_alloc异常返回NULL
    构造/析构会调用构造函数和析构函数不会调用
    重载可以重载不可重载
  • 注解: 核心区别在于new/delete会管理对象的生命周期(调用构造和析构),而malloc/free只负责分配和释放raw memory

2. 什么是内存泄漏?如何避免?

  • 解析: 程序在运行中未能释放不再使用的内存,导致可用内存逐渐减少,最终可能耗尽。
  • 避免方法
    • 遵循RAII原则:使用智能指针(std::unique_ptr, std::shared_ptr)管理资源。
    • 谁申请,谁释放:确保成对使用new/deletenew[]/delete[]
    • 使用容器替代手动管理:如std::vector替代动态数组。
  • 注解: 现代C++中,“几乎永远不要使用裸指针和new/delete 是避免内存泄漏的最佳实践。

3. 什么是智能指针?std::unique_ptrstd::shared_ptr的区别?

  • 解析: 智能指针是RAII理念的实践者,用对象来管理裸指针,在其析构时自动释放内存。
    • std::unique_ptr独占所有权。同一时间只能有一个unique_ptr指向一个对象。无法被复制,只能被移动(std::move)。
    • std::shared_ptr共享所有权。通过引用计数机制,记录有多少个shared_ptr指向同一对象。计数为0时,对象被销毁。
    • std::weak_ptr: 伴随shared_ptr使用,是弱引用。它不增加引用计数,用于解决shared_ptr的循环引用问题。
  • 注解: 这是现代C++内存管理的核心,必须熟练掌握其用法和区别。

四、STL(标准模板库)

1. 说说你常用的STL容器及其底层实现和时间复杂度。

  • 解析
    容器底层实现插入/删除(平均)查找(平均)
    vector动态数组尾部O(1),头部/中部O(n)O(n)
    list/forward_list双向/单向链表O(1)(已知位置)O(n)
    deque分段连续空间头尾O(1)O(n)
    map/set红黑树O(log n)O(log n)
    unordered_map/unordered_set哈希表O(1)(最理想)O(1)(最理想)
  • 注解: 要能根据应用场景(频繁查找、频繁头尾插入、需要有序性)选择合适的容器。

2. vector的底层原理和扩容机制?

  • 解析
    • 原理: 使用一段连续的动态分配数组来存储元素。
    • 扩容机制: 当当前容量(capacity())不足以容纳新元素时,会申请一块更大的新内存(通常是原大小的1.5或2倍,取决于编译器实现),将旧数据拷贝或移动到新内存,然后释放旧内存。
  • 注解: 理解size()capacity()的区别。频繁扩容会导致性能开销,如果知道元素大概数量,可以用reserve()预分配空间。

3. mapunordered_map 的区别?如何选择?

  • 解析
    特性mapunordered_map
    底层结构红黑树(平衡二叉搜索树)哈希表
    元素顺序按键排序,有序无序
    操作时间复杂度O(log n)平均O(1),最差O(n)
    是否需要哈希函数
  • 选择: 如果需要元素有序,或者对遍历顺序有要求,用map。如果只需要高效的查找速度,且不关心顺序,用unordered_map

五、高级与C++11/14/17新特性

1. 左值右值左值引用右值引用的区别?std::movestd::forward的作用?

  • 解析
    • 左值: 有标识符、可取地址的表达式(如变量、函数返回的左值引用)。
    • 右值: 临时对象,即将消亡的值,不可取地址(如字面量、函数返回的非引用值、std::move的结果)。
    • 左值引用T&,只能绑定到左值。
    • 右值引用T&&,只能绑定到右值。其核心目的是实现移动语义,避免不必要的深拷贝,提升性能。
    • std::move无条件将其参数转换为右值引用。它本身不移动任何东西,只是告诉编译器“这个对象可以被移动”。
    • std::forward有条件的转换,用于完美转发(Perfect Forwarding),在模板函数中保持参数的原始值类别(左值性或右值性)。
  • 注解: 这是现代C++性能优化的关键,也是难点。理解移动语义和完美转发是高级C++工程师的标志。

2. Lambda表达式的组成部分是什么?

  • 解析[捕获列表] (参数列表) -> 返回类型 { 函数体 }
    • 捕获列表 []: 指定如何捕获外部变量。
      • [=]:以值方式捕获所有外部变量。
      • [&]:以引用方式捕获所有外部变量。
      • [a, &b]:以值捕获a,以引用捕获b。
      • [this]:捕获当前类的this指针。
    • 其他部分与普通函数类似,参数列表和返回类型可省略。
  • 注解: 理解捕获列表是使用Lambda的关键,要特别注意按值捕获和按引用捕获的生命周期问题。

3. 什么是RAII?

  • 解析资源获取即初始化。其核心思想是:将资源(内存、文件句柄、锁等)的生命周期与对象的生命周期绑定
    • 构造函数中获取资源。
    • 析构函数中释放资源。
  • 注解: RAII是C++管理资源的根本大法。智能指针、std::fstreamstd::lock_guard等都是RAII的典型应用。它可以保证异常安全(Exception Safety)。

六、实战编程题

1. 实现一个单例模式(Singleton)。

class Singleton {
public:
    // 删除拷贝构造和赋值操作,确保唯一性
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton& getInstance() {
        static Singleton instance; // C++11保证局部静态变量初始化是线程安全的
        return instance;
    }

private:
    Singleton() {} // 构造函数私有化
    ~Singleton() {}
};

// 使用: Singleton& s = Singleton::getInstance();
  • 注解: 注意C++11后,局部静态变量初始化是线程安全的,因此这是最简洁优雅的实现(Meyers’ Singleton)。

2. 实现一个智能指针(如unique_ptr的简化版)。

template<typename T>
class SimpleUniquePtr {
public:
    explicit SimpleUniquePtr(T* ptr = nullptr) : ptr_(ptr) {}
    ~SimpleUniquePtr() { delete ptr_; }

    // 禁止拷贝
    SimpleUniquePtr(const SimpleUniquePtr&) = delete;
    SimpleUniquePtr& operator=(const SimpleUniquePtr&) = delete;

    // 允许移动
    SimpleUniquePtr(SimpleUniquePtr&& other) noexcept : ptr_(other.ptr_) {
        other.ptr_ = nullptr;
    }
    SimpleUniquePtr& operator=(SimpleUniquePtr&& 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_;
};
  • 注解: 这道题考察对资源管理、移动语义、模板、noexcept等的综合运用。

3. 反转一个单链表。

struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode *prev = nullptr;
        ListNode *curr = head;
        while (curr != nullptr) {
            ListNode *nextTemp = curr->next; // 保存下一个节点
            curr->next = prev;                // 反转指针
            prev = curr;                      // 移动prev
            curr = nextTemp;                  // 移动curr
        }
        return prev; // prev现在是新的头节点
    }
};
  • 注解: 经典的数据结构题,考察指针操作和逻辑思维能力。务必能在白板上清晰写出。

你们可以把想要的岗位可以发到评论区里面,我可以专门发篇文章,然后列一个题单解析或注解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值