C
内存
malloc 的底层实现
malloc(size) 的核心功能是在 堆(heap)区域分配 size 字节的内存,并返回指向该内存块的指针。如果分配失败,返回 NULL
- 检查请求大小:
- 如果
size == 0,通常返回NULL或最小可分配单元。 - 可能会对
size进行对齐(比如 8 字节或 16 字节对齐)。
- 如果
- 在空闲链表查找合适的块:
- 先查看 空闲链表(free list)是否有合适的块。
- 如果找到 大小合适 的块,则直接分配。
- 如果找到的块 比所需的稍大,可能会进行 切割(split)。
- 请求新的内存(如果没有合适的空闲块):
- 如果空闲链表找不到合适的块,就向 操作系统请求新的内存。
- 通过
sbrk()或mmap()来扩展堆区。
- 更新管理结构:
- 记录已分配的块(用于
free)。 - 维护空闲块的信息(用于后续分配)。
- 记录已分配的块(用于
在1G内存的计算机中能否malloc(1.2G)?为什么?
malloc() 只是在用户态申请虚拟地址空间,并不立即分配物理内存。
如果虚拟地址空间足够(且有 swap 空间),malloc(1.2G) 可能会成功,但可能会导致 swap 频繁交换,严重影响性能。
32位系统中,如果已经有进程占用大量空间,malloc(1.2G) 可能因 地址空间不足 而失败。
数组到底存放在哪里
数组的存放位置取决于 数组的定义方式 和 存储类别。在 C/C++ 语言中,数组可能存放在 栈(Stack)、堆(Heap)、数据段(Data/BSS)、代码段(Text) 等不同的内存区域。
- 栈(Stack)——局部数组,栈内存是 自动分配和释放 的,由系统管理
- 堆(Heap)——动态分配的数组,手动管理,必须
free(arr)释放,否则会 内存泄漏 - 数据段(Data/BSS)——全局或静态数组,
- global:
int global_arr[10]; // BSS 段(未初始化) - local:
static int static_arr[10]; // BSS 段(未初始化) - local:
static int static_arr2[10] = {1, 2, 3}; // Data 段(已初始化)
- global:
- 代码段(Text)——字符串常量
C++
左值右值
Ai
在C++中,表达式的值类别(value category)是一个重要的概念,主要用于区分对象的生命周期、是否可以修改等特性。C++11引入了更细致的值类别划分,包括:
1. 左值(lvalue, Left Value)
- 可以出现在赋值号(
=)的左侧- 通常具有持久的存储
- 可以获取地址(即可以使用
&操作符)示例:
int a = 10; // 变量 a 是左值 int& ref = a; // 可以使用左值引用绑定2. 右值(rvalue, Right Value)
- 不能出现在赋值号的左侧
- 通常是临时对象,生命周期短
- 不能获取地址(即
&操作符不能用于纯右值)示例:
int a = 5; int b = a + 10; // 表达式 `a + 10` 产生的 15 是一个右值右值进一步分为:
(1) 纯右值(prvalue, Pure rvalue)
- 纯计算结果,不关联具体对象,不能获取地址。
- 常见的纯右值:字面量、临时对象、算术表达式结果。
示例:
int x = 5 + 3; // `5 + 3` 是 prvalue std::string("hello"); // 这是一个临时的 std::string 对象,属于 prvalue(2) 亡值(xvalue, Expiring value)
- 过渡对象,即将被销毁的对象,但仍然可以移动资源。
- 常见的亡值:使用
std::move转换后的对象,返回临时对象的函数调用。示例:
std::string foo() { return std::string("hello"); } std::string s = std::move(foo()); // `std::move(foo())` 是 xvalue
3. 区分值类别
值类别 是否可修改 是否可获取地址 是否有名字 例子 lvalue ✅ ✅ ✅ int a;a = 10;prvalue ❌ ❌ ❌ 5 + 3,std::string("hello")xvalue ❌(可移动) ✅ ❌ std::move(a),foo()(返回右值引用的情况)
4. 应用
左值引用(
T&) 只能绑定到左值:int a = 10; int& ref = a; // OK int& ref2 = 5; // ❌错误,5 是 prvalue右值引用(
T&&) 只能绑定到右值:int&& rref = 5; // OK, 5 是 prvalue int&& rref2 = std::move(a); // OK, a 被转换为 xvalue
5. 总结
- 左值(lvalue):有名字、可获取地址、可修改。
- 纯右值(prvalue):不能获取地址,纯计算结果。
- 亡值(xvalue):可获取地址,即将销毁但可移动。
- 右值(rvalue) 是 prvalue 和 xvalue 的统称。
这些概念对于**移动语义、完美转发(perfect forwarding)**等高级C++技术至关重要。
什么是引用?左、右值引用?
Ai
在C++中,引用(Reference)是为已存在的变量创建的别名。通过引用,可以使用不同的名称访问同一个变量。引用在函数参数传递、返回值以及重载运算符等场景中广泛应用。
1. 左值引用(Lvalue Reference)
左值引用是对左值的引用,使用单个
&符号表示。左值是指在表达式中具有持久存储的对象,例如变量、数组元素等。特点:
- 可以绑定到左值。
- 允许通过引用修改所指对象的值。
示例:
int a = 10; int& ref = a; // ref是a的左值引用 ref = 20; // 修改a的值为20在上述代码中,
ref是a的左值引用,对ref的修改会直接影响a的值。2. 右值引用(Rvalue Reference)
右值引用是对右值的引用,使用
&&符号表示。右值是指在表达式中不具有持久存储的临时对象,例如字面量、临时对象等。特点:
- 可以绑定到右值(临时对象)。
- 主要用于实现移动语义,以避免不必要的拷贝,提高程序性能。
示例:
std::string str1 = "Hello"; std::string str2 = std::move(str1); // str1被移动,str2接管其资源在上述代码中,
std::move将str1转换为右值,使其可以绑定到右值引用,从而将str1的资源移动到str2,避免了深拷贝操作。3. 常量左值引用(Const Lvalue Reference)
常量左值引用(
const T&)可以绑定到左值和右值,但通过该引用无法修改所指对象的值。示例:
const int& ref1 = 10; // 绑定到右值 int x = 5; const int& ref2 = x; // 绑定到左值这种特性使得在函数参数传递时,可以使用常量左值引用来避免不必要的拷贝,同时保证参数不会被修改。
4. 应用场景
函数参数传递:
- 使用左值引用传递参数,可以在函数内部修改原始变量的值。
- 使用常量左值引用传递参数,可以避免拷贝,同时保护参数不被修改。
- 使用右值引用传递参数,可以实现移动语义,避免不必要的拷贝,提高性能。
移动构造函数和移动赋值运算符:
- 通过右值引用,可以定义移动构造函数和移动赋值运算符,实现资源的高效转移,避免深拷贝。
完美转发(Perfect Forwarding):
- 结合右值引用和模板,可以实现完美转发,将参数完美地传递给其他函数,保持参数的左右值属性。
5. 注意事项
**右值引用只能绑定到右值。**如果尝试将右值引用绑定到左值,会导致编译错误。要将左值转换为右值,可以使用
std::move。**具名的右值引用是左值。**即使变量声明为右值引用类型,但在表达式中,它仍然是左值。如果需要将其作为右值使用,需要显式地使用
std::move进行转换。通过理解和正确使用左值引用、右值引用及其相关概念,可以编写出高效且健壮的C++程序。
多态
虚函数和纯虚函数的区别
- 相同:在基类中声明的,使用关键字
virtual。它可以在派生类中被override - 虚函数
- 可以有具体的实现,可以在派生类中不重写
- 基类可以实例化
- 允许通过基类指针或引用来调用派生类中的重写函数,从而实现动态绑定
- 纯虚函数
- 没有具体实现(
= 0) - 基类不能实例化
- 纯虚函数必须在派生类中被重写
- 没有具体实现(
内存
malloc + free VS new + delete
malloc + free | new + delete | |
|---|---|---|
| 语言 | C 语言库函数 | C++ 操作符 |
| 行为 | 调用对象的构造函数和析构函数,支持对象的初始化和销毁; 支持 type 和真数组 | 仅分配和释放原始内存 |
| 内存分配 | 分配对象或对象数组的内存,并初始化。 | 分配一块未初始化的内存空间 |
| 返回 | 对应对象的指针,类型明确 | void*, 需要强制类型转换 |
| 内存释放 | 调用对象的析构函数来销毁对象(数组使用 delete[] | 仅释放内存 |
| 其他 | 类型安全、面向对象、速度较慢、能抛异常 | 类型无关、原始分配、性能更高、错误返回 NULL 指针 |
指针
智能指针的种类
智能指针的实现
unique_ptr- 独占所有权的智能指针,它确保同一时间内只有一个指向对象
- 移动转移所有权,不允许复制
shared_ptr- 允许多个指针共享同一个对象所有权
- 通过引用计数机制跟踪指向同一个对象的
shared_ptr实例数量- 引用计数降为零时,自动释放资源
- 引用计数操作是线程安全的
weak_ptr- 弱引用指针,可访问
shared_ptr管理的对象,但不增加引用计数
- 弱引用指针,可访问
Types
如何避免编译器进行的隐式类型转换: explicit
C++ 有几种类型转换
- 隐式转换
- 显示转换
- C 风格转换
- 不会对这种潜在的错误进行严格的检查
double -> int可能使用截断
static_cast: 类型安全的转换,适用于基本数据类型之间、类层次结构中的转换- Eg. 在有继承关系的类之间进行向上转型;
int -> double double -> int:static_cast<int>(d);double* -> int*会报错
- Eg. 在有继承关系的类之间进行向上转型;
dynamic_cast: 多态类型转换,通常用于将基类指针或引用转换为派生类指针或引用- Eg. 有继承关系的类之间的向下转型
- 转换失败将会返回
nullptr/ 异常
const_cast: 添加或删除变量的const属性- Eg.
const int ci = 10; int* pi = const_cast<int*>(&ci); // 去掉const属性
- Eg.
reinterpret_cast: 低级别的类型转换。将一个类型的**指针(或引用)**转换为另一个类型的指针(或引用),但不会对指针所指向的数据进行任何检查。double -> int不改变内存内容
- C 风格转换
STL
C++ vector 底层实现
std::move 原理
- 将传入的左值转换为右值引用
Misc
struct 和 class 的区别
struct- 默认成员访问权限:
public - 默认继承权限:
public - 用于 数据结构、POD(Plain Old Data)类型,通常 不包含复杂方法。
- 默认成员访问权限:
class- 默认成员访问权限:
private - 默认继承权限:
private,外部不能直接访问 - 用于 面向对象编程(OOP),用于 封装成员变量和方法。
- 默认成员访问权限:
extern "C" 的作用
用于 C++ 代码调用 C 语言代码,或 让 C 语言调用 C++ 代码。通过 extern "C" 告诉编译器,该函数应按 C 语言规则编译,不进行名称修饰
const 常量和 #define 的区别(编译阶段、安全性、内存占用等)
Ai
对比项 const#define编译阶段 编译期处理,有类型检查 预处理阶段,简单文本替换 作用域 遵循 C++ 作用域规则 无作用域,全局替换 安全性 有类型检查,防止错误 无类型检查,容易出错 内存占用 可能优化为单个存储 可能导致重复展开 可调试性 变量有名字,调试器可见 预处理后消失,调试器不可见 是否能修改 不可修改,防止错误 只是文本替换,容易被意外改动 ✅
const适用场景
- 需要 类型检查,防止错误。
- 需要 节省内存,减少重复定义。
- 需要 可调试性,在调试器中可见。
✅
#define适用场景
- 定义 宏函数,如
#define SQUARE(x) ((x)*(x))。- 定义 条件编译(
#ifdef/#ifndef)。- 定义 跨文件的编译开关。