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. 最佳实践
- 优先隐式
inline
:在类定义内直接实现简单的构造函数/析构函数。 - 避免复杂逻辑:若构造/析构函数涉及动态内存、虚函数或异常,避免内联。
总结
- 可以声明为
inline
:语法支持且对简单场景有效。 - 需谨慎使用:复杂逻辑或涉及虚函数时,内联可能适得其反。
- 依赖编译器决策:最终是否内联由编译器优化策略决定。
3. 函数作用域结束后,变量的析构,是有谁来进行的?
在 C++ 中,函数作用域结束后,变量的析构是由编译器自动插入的代码触发的。
编译器如何实现自动析构?
- 代码插入:编译器在作用域结束处(如
}
前)插入析构函数调用。 - 异常安全:即使作用域因异常提前退出,编译器仍会插入析构代码(利用栈展开机制)。
- 逆序析构:保证对象按创建的逆序析构,避免依赖问题。
4. struct和class的区别?
struct
和 class
的内存对齐规则是完全相同的,唯一的区别在于默认的访问控制权限(struct
默认 public
,class
默认 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)。
- x86 架构:
- 内存屏障(Memory Barriers):保证内存操作的顺序性,例如
std::memory_order
相关的屏障指令。
2. std::mutex
的底层实现
std::mutex
是互斥锁,用于保护临界区,其实现依赖操作系统内核的调度。
**(1) Linux 实现(基于 pthread_mutex_t
)**
- 轻量级锁(Futex):快速用户空间互斥锁(Fast Userspace Mutex)。
- 无竞争时:完全在用户空间通过原子操作(如
CAS
)完成加锁/解锁。 - 有竞争时:通过系统调用(
futex_wait
,futex_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 确保泄露消失