目 录CONTENT

文章目录

C++ 部分

TalentQ
2025-08-27 / 0 评论 / 0 点赞 / 5 阅读 / 0 字

值传递、引用传递、指针传递的区别

值传递:将实参的值复制一份,传递给形参,函数内部对形参的修改不会影响到外部。安全,但是有一次值拷贝,效率低,适合基本数据类型和小型对象。

引用传递:函数行参是实参的别名,传递的是变量本身,函数内部对形参的修改会影响到外部。没有拷贝效率高,适用于需要在函数内部修改外部变量的场景。

指针传递:传递实参的地址,函数通过指针访问或修改原始数据。实参能够被修改。可以传递空指针,需要注意检查。适合数组等需要动态管理内存的场景。

引用传递较为简单,适合简单场景下使用提高效率;指针传递更灵活,更适合底层操作。

new/delete 和 malloc/free 的差别

差异项

new/delete

malloc/free

语法

C++专用,适用于对象和基本类型

C标准库函数,适用于基本类型和结构体

构造和析构

new分配内存并调用对象的构造函数;

delete释放内存并调用对象的析构函数;

malloc只分配原始内存,不调用构造函数;

free只释放原始内存,不调用析构函数;

返回值

new返回正确类型的指针

malloc返回void*,需要强制类型转换

运算符重载

C++允许重载new和delete,实现自定义内存分配策略

不支持重载

异常处理

new分配失败时抛出 std::bad_alloc 异常(除非使用new(nothrow))

malloc分配失败返回inullptr,需要手动检查

底层实现

底层通常调用mallc/free或操作系统API。但会额外处理对象的构造/析构、类型信息等

底层直接调用C运行库的堆管理函数(如brk、sbrk、mmap等),只负责原始内存分配和释放

栈和堆的区别

差异项

分配方式

由编译器自动分配和释放。用于存储函数的局部变量、参数等

由程序员手动分配和释放(如new/delete 和 malloc/free)。用于动态分配内存

生命周期

变量的生命周期随作用域(如函数、代码快)自动开始和结束,离开作用域后自动释放

内存的生命周期由程序员控制,分配后直到显式释放或程序结束才释放

分配效率

速度快,只需要移动栈指针

速度慢,需要复杂的内存管理(查找空闲块、合并、碎片处理等)

内存大小

较小,几百KB到几MB,受限于操作系统

较大,可以分配较多内存,受限于物理内存和操作系统

访问方式

通过变量名直接访问

通过指针间接访问

内存管理风险

自动管理,几乎不会内存泄漏,但可能发生栈溢出(如递归过深)

需手动管理,容易出现内存泄漏、野指针等问题

内存对齐为什么重要

提高访问效率:大多数现代CPU在访问对齐的数据时速度更快。例如,访问对齐的4字节整型数据只需要一次总线操作,而非对齐则可能需要多次操作。对齐的数据能更好地利用CPU缓存和内存总线。

跨平台兼容:保证代码在不同硬件和平台下的正确性与安全性。

支持高性能计算:高性能硬件和指令集通常要求数据严格对齐,否则无法利用硬件加速。

多态的实现原理

多态分为:

静态多态(编译时多态):如函数重载、模板、运算符重载等,编译期决定调用关系。

动态多态(运行时多态):如虚函数、接口继承等,运行期决定调用关系。

主要讲解运行时多态。运行时多态依赖于虚函数表(vtable)和虚指针(vptr)。

1 虚函数表(vtable)

当类中声明了虚函数时,编译器为该类生成一个虚函数表。虚函数表是一个存储虚函数地址的数组,每个虚函数对应一个入口。

2 虚指针(vptr)

每个包含虚函数的对象内部都会有一个隐藏的虚指针(vptr),指向该对象所属类的虚函数表。

3 运行时绑定

当通过基类指针或引用调用虚函数时,程序会根据对象的虚指针找到虚函数表,从而调用实际的函数实现。

拷贝构造函数和移动构造函数什么时候触发

拷贝构造函数触发时机

1 用一个对象初始化另一个同类型对象:MyClass b = a;

2 对象作为值参数传递给函数:func(a);

3 函数返回值为对象类型,即按值返回;

MyClass create() {
  MyClass temp;
  return temp;  // 可能会调用拷贝构造(在C++11后,这里会被优化成移动构造)
}
MyClass b = create();

4 显式调用拷贝构造函数:MyClass b(a);

移动构造函数触发时机

1 用右值对象初始化另一个对象:MyClass b = std::move(a);

2 对象作为右值参数传递给函数:func(std::move(a));

3 函数返回值为对象类型;

MyClass create() {
  MyClass temp;
  return temp;  // 可能会调用拷贝构造(在C++11后,这里会被优化成移动构造)
}
MyClass b = create();

4 显式调用移动构造函数:MyClass b(std::move(a));

RAII的思想和理解

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是C++中重要的一种编程思想,实现自动管理资源。

核心思想是 “资源的生命周期绑定到对象的生命周期”,具体而言就是:

在对象创建(初始化)时获取资源(如内存、文件句柄、锁等);在对象销毁(析构)时自动释放资源。

这样可以确保资源的正确释放,避免资源泄露或重复释放等问题。

RAII通常通过构造函数和析构函数来实现:

构造函数负责获取和初始化资源;析构函数负责释放资源。

我们常见的智能指针(std::unique_ptr、std::shared_ptr)、锁管理(std::lock_guard、std::unique_lock)等都是遵循了RAII思想,自动管理资源,提高了代码的安全性、健壮性和可维护性。

vector 扩容机制

std::vector内部维护一个连续的动态数组,指针指向堆内存。涉及三个核心成员:起始指针、当前元素个数(size)、当前分配容量(capacity)。

vector 扩容只在 push_back / insert / resize 等导致 size==capacity 时触发:

  1. 重新分配更大的内存,通常是当前容量的2倍;

  2. 将原有元素拷贝或移动(如果支持则优先移动构造)到新数组;

  3. 释放旧的空间;

  4. 更新指针和容量信息;

扩容后旧迭代器、指针、引用全部失效。

如何避免频繁扩容?
reserve(n) 事先分配;
• 在已知元素数量时,用 vector<int> v; v.reserve(1000); 可一次性搞定。

模板如何在编译时期展开的

C++模板(template)是在编译时期展开的,这一过程成为模板的实例化(template instantiation)。当用具体类型调用模板时,编译器会为每种类型生成对应的代码。

  1. 语法分析阶段:编译器识别到模板的定义;

  2. 实例化阶段:遇到模板被具体类型调用时,编译器用该类型替换模板参数,生成具体的代码,即“展开”;

  3. 类型检查:编译器会检查实例化后的代码是否有语法和类型错误。

在编译时期展开,能保证类型安全,并且没有运行时开销。

C++ 11/14常见的新特性有哪些

C++11新特性:

1 自动类型推断(auto)

auto x = 10;  // x 自动推断为 int

2 基于范围的 for 循环

std::vector<int> v(1, 2, 3);
for (auto& elem : v) {
  std::cout << elem << std::endl;
}

3 右值引用和移动语义

void func(std::vector<int>&& v);  // 右值引用

4 智能指针(std::unique_ptr,std::share_ptr,std::weak_ptr)

std::unique_ptr<int> ptr(new int(5));

5 lambda表达式

auto add = [](int a, int b) { return a+ b; };

6 nullptr关键字

int* p nullptr;  // 替代NULL

7 constexpr常量表达式

// 如果x是编译时常量,则square(x)也是编译时常量
constexpr int square(int x) { return x * x; }

8 std::thread多线程库

std::thread t([](){ std::cout << "Hello\n"; });
t.join();

9 模板别名

template<typename T>
using Vec = std::vector<T>;

C++ 14新特性:

1 范型 lambda 捕获(lambda表达式可以auto推断参数类型)

auto add = [](auto a, auto b) { return a + b; };

2 变量模板

template<typename T>
constexpr T pi = T(3.1415926);

3 返回值类型推断(函数返回值类型可自动推断)

auto func() { return 123; }  // 返回值自动推断为 int

mutex、spinlock、condition_variable的区别

mutex是一种线程同步机制,用于保证同一时间只有一个线程能访问某个资源。当一个线程获得了mutex,其他线程必须等待,直到该线程释放mutex。等待期间,线程会被操作系统挂起,不再占用CPU。这种机制适合临界区较长、资源竞争激烈的场景。

spinlock(自旋锁)也是一种互斥锁,但它的等待方式不同。没有获得锁的线程不会被挂起,而是会不断地循环检查锁是否可用(“自旋”),直到获得锁。这样会持续占用CPU,适合临界区很短、锁持有时间很短的场景。自旋锁不适合长时间等待,否则会浪费大量的CPU资源。通常用原子操作(如std::atomic_flag)实现。

condition_variable(条件变量)用于线程之间的通信和同步。它允许线程在某个条件不满足时主动挂起等待,直到其他线程通知(notify_one()或notify_all())条件已经满足。常见于生产者-消费者模型或事件通知机制。等待期间,线程会被操作系统挂起,不占用CPU。条件变量通常需要搭配mutex使用来保护条件的检查和修改。

inline、宏、constexpr的区别

inline(内联函数)

inline用于修饰函数,建议编译器将函数代码在调用处展开,以减少函数调用的开销。

  • 主要用于短小、频繁调用的函数;

  • 是否真的内联由编译器决定,inline只是建议;

  • 有类型检查,能安全地处理参数和返回值。

宏定义

宏是预处理器提供的文本替换机制,用 #define 定义。

  • 在编译之期,宏会被原样替换为定义的内容;

  • 没有类型检查,容易出错,比如运算符优先级问题;

  • 常用于常量、简单的代码片段,但现代C++推荐使用constexpr或inline。

constexpr(常量表达式)

constexpr用于声明常量或函数,要求其值能在编译期间确定。

  • 用于定义编译期间的常量,提高效率;

  • 可以用于变量、函数、类成员等;

  • 有类型检查,语法安全,推荐使用。

0

评论区