C++细节知识for面试

发布于:2025-03-29 ⋅ 阅读:(30) ⋅ 点赞:(0)

1. linux上C++程序可用的栈和堆大小分别是多少,为什么栈大小小于堆?

1. 栈(Stack)大小

栈默认为8MB,可修改。

为什么是这个大小

  • 安全性:限制栈大小可防止无限递归或过深的函数调用导致内存耗尽。
  • 多线程优化:每个线程的栈独立,较小的默认值避免内存浪费(线程数多时,总内存消耗可能激增)。

2. 堆(Heap)大小

  • 默认限制

    • 堆的大小受限于系统的虚拟内存地址空间物理内存+交换空间(Swap)​
    • 在64位系统上,理论最大值为 ​128 TB​(Linux内核默认配置),实际受物理资源和进程地址空间限制。
    • 在32位系统上,通常最大为 ​3 GB​(受限于用户空间地址范围)。
  • 为什么是这个大小

  • 动态分配灵活性:堆用于动态内存分配,需支持程序运行时按需扩展。
  • 操作系统虚拟内存管理:64位系统地址空间极大,但实际分配取决于物理内存和Swap。

2. 构造函数和析构函数可以声明为inline吗,为什么?

1. 语法可行性

  • 可以声明为 inline:C++标准允许构造函数和析构函数声明为 inline

  • 隐式 inline:在类定义内部直接实现的构造函数和析构函数,默认会被编译器视为 inline,无需显式声明。

  • 显式 inline:在类外定义时,需显式添加 inline 关键字。

2. 最佳实践

  1. 优先隐式 inline:在类定义内直接实现简单的构造函数/析构函数。
  2. 避免复杂逻辑:若构造/析构函数涉及动态内存、虚函数或异常,避免内联。

总结

  • 可以声明为 inline:语法支持且对简单场景有效。
  • 需谨慎使用:复杂逻辑或涉及虚函数时,内联可能适得其反。
  • 依赖编译器决策:最终是否内联由编译器优化策略决定。

3. 函数作用域结束后,变量的析构,是有谁来进行的?

在 C++ 中,​函数作用域结束后,变量的析构是由编译器自动插入的代码触发的

编译器如何实现自动析构?

  1. 代码插入:编译器在作用域结束处(如 } 前)插入析构函数调用。
  2. 异常安全:即使作用域因异常提前退出,编译器仍会插入析构代码(利用栈展开机制)。
  3. 逆序析构:保证对象按创建的逆序析构,避免依赖问题。

4. struct和class的区别?

struct 和 class 的内存对齐规则是完全相同的,唯一的区别在于默认的访问控制权限(struct 默认 publicclass 默认 private)。

5. atomic, mutex底层实现?

1. std::atomic 的底层实现

std::atomic 用于实现无锁(lock-free)或低竞争(low-contention)的原子操作,其性能远高于互斥锁。

​**(1) 硬件支持**
  • 原子指令:直接使用 CPU 提供的原子指令,例如:
    • x86 架构LOCK 前缀指令(如 LOCK CMPXCHG 实现 CAS)。
    • ARM 架构LDREX/STREX 指令(Load-Exclusive/Store-Exclusive)。
  • 内存屏障(Memory Barriers)​:保证内存操作的顺序性,例如 std::memory_order 相关的屏障指令。

2. std::mutex 的底层实现

std::mutex 是互斥锁,用于保护临界区,其实现依赖操作系统内核的调度。

​**(1) Linux 实现(基于 pthread_mutex_t)​**
  • 轻量级锁(Futex)​:快速用户空间互斥锁(Fast Userspace Mutex)。
    • 无竞争时:完全在用户空间通过原子操作(如 CAS)完成加锁/解锁。
    • 有竞争时:通过系统调用(futex_waitfutex_wake)挂起或唤醒线程。
  • 锁类型
    • 普通锁(PTHREAD_MUTEX_DEFAULT)​:可能死锁,无错误检查。
    • 递归锁:允许同一线程多次加锁。
    • 自适应锁:在竞争激烈时退化为内核态锁。

3. 对比与选择

特性 std::atomic std::mutex
实现基础 硬件原子指令 + 可能的锁模拟 操作系统内核机制(Futex/CRITICAL_SECTION)
性能 无锁时极快(纳秒级) 无竞争时快(约 20-50 ns),有竞争时较慢
适用场景 简单原子操作(计数器、标志位) 复杂临界区(需保护多步操作)
内存开销 通常较小(与数据类型对齐) 较大(需存储锁状态和等待队列)
线程阻塞 无(自旋或原子操作) 可能阻塞(进入内核等待)

6. 线程的挂起和执行在用户态还是内核态?

1. 两种情况

  • 内核级线程:挂起和执行由内核态管理,是现代操作系统的默认选择(如Linux的pthread)。
  • 协程:完全在用户态实现,适用于高并发但需结合多线程利用多核。

2. 内核级线程(Kernel-Level Threads, KLT)​

  • 管理方式:由操作系统内核直接支持,每个线程是内核调度的基本单位(如Linux的pthread)。
  • 挂起与执行
    • 内核态操作:线程的创建、销毁、调度(挂起/恢复)需通过系统调用,由内核完成。
    • 内核感知:内核直接管理线程状态(就绪、运行、阻塞等)。
  • 优点
    • 并行性:线程可分配到不同CPU核心并行执行。
    • 阻塞隔离:一个线程阻塞不会影响同一进程内其他线程。
  • 缺点
    • 切换开销大(需切换到内核态)。
    • 线程数量受内核限制。

3. 协程(Coroutine)——用户态的轻量级并发

  • 管理方式:完全在用户态由程序或运行时库(如Golang的goroutine)控制。
  • 挂起与执行
    • 用户态切换:协程主动让出(yield)或恢复(resume),不依赖内核调度。
    • 非抢占式:协程需显式让出CPU,通常与事件循环(如epoll)配合。
  • 适用场景:高并发I/O密集型任务(如网络服务器)。

7. 谈一谈new/delete和malloc/free的区别和联系?

1. 核心区别

特性 new/delete (C++ 运算符) malloc/free (C 标准库函数)
语法与类型安全 是运算符,无需类型转换(自动匹配类型) 是函数,需显式类型转换(返回 void*
构造函数/析构函数 调用构造函数(new)和析构函数(delete 不调用构造函数/析构函数
内存大小计算 自动根据类型计算内存大小(如 new int 需手动计算(如 malloc(sizeof(int)*n)
异常处理 失败时抛出 std::bad_alloc 异常 失败时返回 NULL(需手动检查)
内存来源 自由存储区(free store)​分配 堆(heap)​分配
重载支持 可重载类的 operator new/delete 不可重载
内存对齐 自动满足类型的对齐要求 需手动处理对齐(如 aligned_alloc
多态支持 支持(通过虚析构函数正确释放派生类对象) 不支持(需手动管理派生类内存)
扩展功能 支持 placement new(在指定内存构造对象) 不支持
与 C++ 特性结合 兼容智能指针(如 std::unique_ptr 需额外封装才能安全使用

8. 解决内存泄漏?

**(1) 使用 Valgrind 检测泄露**
valgrind --leak-check=full --show-leak-kinds=all ./your_program
  • 输出示例
    ==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
    ==12345==    by 0x123456: main (main.c:10)
**(2) 分析代码**

定位到泄露位置后,检查以下常见原因:

  • 忘记释放内存malloc/new 未配对 free/delete
  • 异常路径未释放:如 return 或 throw 前未释放资源。
  • 循环引用​(智能指针):std::shared_ptr 循环引用导致无法自动释放。
​**(3) 修复并验证**
  • 修复代码:添加释放逻辑或使用 RAII(如 std::unique_ptr)。
  • 重新测试:重复步骤 1 确保泄露消失