-
智能指针实现原理
智能指针是一种封装了原生指针的类,通过引用计数或所有权机制来管理动态分配的内存资源。它的核心原理是利用 RAII(Resource Acquisition Is Initialization)技术,确保在智能指针对象生命周期结束时自动释放所管理的资源。常见的智能指针包括std::unique_ptr
、std::shared_ptr
和std::weak_ptr
。 -
智能指针,里面的计数器何时会改变
对于std::shared_ptr
,计数器会在以下情况下改变:- 当一个新的
shared_ptr
被创建并指向同一块内存时,引用计数加 1。 - 当一个
shared_ptr
被销毁或重置时,引用计数减 1。 - 当引用计数减到 0 时,自动释放所管理的资源。
- 当一个新的
-
智能指针和管理的对象分别在哪个区
- 智能指针本身是一个栈区对象,它的生命周期由栈的规则管理。
- 智能指针所管理的资源位于堆区,通过智能指针的析构函数自动释放。
-
面向对象的特性:多态原理
多态是指通过基类的指针或引用调用派生类的重写函数,从而实现不同行为的能力。多态的原理依赖于虚函数表和动态绑定:- 虚函数表(vtable):每个包含虚函数的类都有一个虚函数表,存储虚函数的地址。
- 动态绑定:在运行时根据对象的实际类型调用相应的虚函数。
-
介绍一下虚函数,虚函数怎么实现的
虚函数是用于实现多态的成员函数,通过virtual
关键字声明。虚函数的实现依赖于虚函数表(vtable):- 每个包含虚函数的类都有一个虚函数表,存储虚函数的地址。
- 对象中包含一个指向虚函数表的指针(vptr),在运行时根据 vptr 找到正确的函数地址。
-
多态和继承在什么情况下使用
- 继承:当需要复用已有类的代码或表示“is-a”关系时使用。
- 多态:当需要通过基类接口调用派生类的不同实现时使用,通常与继承结合。
-
除了多态和继承还有什么面向对象方法
- 封装:隐藏对象的内部实现细节,提供公共接口。
- 抽象:定义接口或基类,隐藏具体实现。
- 组合:通过将对象作为成员变量来实现代码复用。
-
C++内存分布。什么样的数据在栈区,什么样的在堆区
- 栈区:局部变量、函数参数、函数返回地址等,生命周期由作用域管理。
- 堆区:动态分配的内存(如
new
、malloc
分配的内存),生命周期由程序员管理。 - 全局/静态区:全局变量和静态变量。
- 常量区:存储常量字符串等。
- 代码区:存储程序的二进制代码。
-
C++内存管理(RAII啥的)
RAII(Resource Acquisition Is Initialization)是 C++ 的核心内存管理技术,通过对象的生命周期管理资源:- 在构造函数中获取资源(如分配内存)。
- 在析构函数中释放资源(如释放内存)。
- 智能指针是 RAII 的典型应用。
-
C++从源程序到可执行程序的过程
- 预处理:处理宏、头文件等,生成
.i
文件。 - 编译:将预处理后的代码编译成汇编代码,生成
.s
文件。 - 汇编:将汇编代码转换成机器码,生成
.o
文件。 - 链接:将多个目标文件链接成可执行文件。
- 预处理:处理宏、头文件等,生成
-
一个对象=另一个对象会发生什么(赋值构造函数)
如果对象已经存在,赋值操作会调用赋值运算符函数(operator=
)。如果没有显式定义赋值运算符,编译器会生成默认的赋值运算符,执行成员变量的逐字节复制(浅拷贝)。对于动态资源管理,通常需要自定义赋值运算符以支持深拷贝。 -
C++11的智能指针有哪些。
weak_ptr
的使用场景。什么情况下会产生循环引用- C++11 的智能指针包括
std::unique_ptr
、std::shared_ptr
和std::weak_ptr
。 weak_ptr
的使用场景:用于解决shared_ptr
的循环引用问题。weak_ptr
不会增加引用计数,只是观察资源,避免循环引用导致的内存泄漏。- 循环引用:当两个或多个
shared_ptr
相互引用时,它们的引用计数永远不会减到 0,导致内存无法释放。
- C++11 的智能指针包括
-
多进程
fork
后不同进程会共享哪些资源fork
后,子进程会复制父进程的地址空间,包括代码段、数据段、堆、栈等。- 共享的资源包括:
- 打开的文件描述符。
- 信号处理函数。
- 进程的某些属性(如用户 ID、组 ID)。
- 不共享的资源包括:
- 进程的地址空间(虽然是复制,但独立修改)。
- 进程 ID(PID)。
-
多线程里线程的同步方式有哪些
- 互斥锁(
std::mutex
):保护共享资源,防止数据竞争。 - 条件变量(
std::condition_variable
):用于线程间通信,等待特定条件满足。 - 信号量(
semaphore
):控制对共享资源的访问数量。 - 读写锁(
std::shared_mutex
):允许多个读操作,但写操作独占。 - 原子操作(
std::atomic
):确保操作的原子性。
- 互斥锁(
-
sizeof
是在编译期还是在运行期确定
sizeof
是在编译期确定的,它返回类型或对象的大小,编译器会根据类型信息直接计算。 -
函数重载的机制。重载是在编译期还是在运行期确定
- 函数重载的机制:通过函数名相同但参数列表不同(参数类型、数量或顺序)来实现。
- 重载是在编译期确定的,编译器根据调用时的参数类型选择最匹配的函数。
-
指针常量和常量指针
- 指针常量(
int* const p
):指针本身是常量,不能修改指针的指向,但可以修改指向的值。 - 常量指针(
const int* p
):指向的值是常量,不能修改值,但可以修改指针的指向。
- 指针常量(
-
vector
的原理,怎么扩容vector
是一个动态数组,底层是连续的内存空间。- 扩容机制:当元素数量超过当前容量时,
vector
会分配一块更大的内存(通常是原容量的 2 倍),将原有元素拷贝到新内存中,并释放旧内存。
-
介绍一下
const
const
用于定义常量,表示值不可修改。- 可以修饰变量、函数参数、成员函数等。
- 例如:
const int a = 10;
:a
是常量,不能修改。void func(const int& x)
:x
是常量引用,不能修改x
的值。int getValue() const
:成员函数是常量函数,不能修改对象的成员变量。
-
引用和指针的区别
- 引用:
- 是变量的别名,必须初始化。
- 不能为空,不能重新绑定到其他变量。
- 语法更简洁,使用
.
访问成员。
- 指针:
- 存储变量的地址,可以为空。
- 可以重新指向其他变量。
- 使用
*
和->
访问成员。
- 引用:
-
C++新特性知道哪些
- C++11:
auto
、nullptr
、智能指针、范围for
循环、lambda
表达式、std::thread
等。 - C++14:泛型
lambda
、constexpr
增强等。 - C++17:结构化绑定、
std::optional
、std::variant
、std::filesystem
等。 - C++20:概念(
concepts
)、范围库(ranges
)、协程(coroutines
)等。
- C++11:
-
类型转换
- C++ 提供了四种类型转换操作符:
static_cast
:用于基本类型转换、父子类指针转换。dynamic_cast
:用于多态类型转换,运行时检查。const_cast
:去除const
或volatile
属性。reinterpret_cast
:用于低级别的类型转换,如指针和整数之间的转换。
- C++ 提供了四种类型转换操作符:
-
RAII基于什么实现的(生命周期、作用域、构造析构)
RAII(Resource Acquisition Is Initialization)基于对象的生命周期和作用域实现:- 在对象的构造函数中获取资源(如分配内存、打开文件)。
- 在对象的析构函数中释放资源(如释放内存、关闭文件)。
- 利用栈对象超出作用域时自动析构的特性,确保资源被正确释放。
-
手撕:
Unique_ptr
,控制权转移(移动语义);手撕:类继承,堆/栈上分别代码实现多态-
Unique_ptr
实现:template<typename T> class UniquePtr { public: UniquePtr(T* ptr = nullptr) : ptr_(ptr) {} ~UniquePtr() { delete ptr_; } UniquePtr(const UniquePtr&) = delete; // 禁止拷贝构造 UniquePtr& operator=(const UniquePtr&) = delete; // 禁止拷贝赋值 UniquePtr(UniquePtr&& other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; } UniquePtr& operator=(UniquePtr&& 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_; };
-
类继承,堆/栈上实现多态:
class Base { public: virtual void print() { std::cout << "Base" << std::endl; } virtual ~Base() = default; }; class Derived : public Base { public: void print() override { std::cout << "Derived" << std::endl; } }; // 栈上多态 Base base; Derived derived; Base* ptr = &derived; ptr->print(); // 输出 "Derived" // 堆上多态 Base* heapPtr = new Derived(); heapPtr->print(); // 输出 "Derived" delete heapPtr;
-
-
unique_ptr
和shared_ptr
区别unique_ptr
:独占所有权,不能拷贝,只能通过移动语义转移所有权。shared_ptr
:共享所有权,通过引用计数管理资源,支持拷贝和赋值。
-
右值引用
- 右值引用(
T&&
)用于绑定临时对象或即将销毁的对象,支持移动语义和完美转发。 - 示例:
void func(int&& x) { std::cout << x << std::endl; } func(10); // 10 是右值
- 右值引用(
-
函数参数可不可以传右值
可以,通过右值引用参数传递右值。例如:void func(int&& x) { std::cout << x << std::endl; } func(10); // 10 是右值
-
参考 C/C++ 堆栈实现自己的堆栈。要求:不能用 STL 容器
class MyStack { public: MyStack(int capacity) : capacity_(capacity), size_(0) { data_ = new int[capacity_]; } ~MyStack() { delete[] data_; } void push(int value) { if (size_ >= capacity_) throw std::overflow_error("Stack is full"); data_[size_++] = value; } int pop() { if (size_ <= 0) throw std::underflow_error("Stack is empty"); return data_[--size_]; } int top() const { if (size_ <= 0) throw std::underflow_error("Stack is empty"); return data_[size_ - 1]; } bool empty() const { return size_ == 0; } int size() const { return size_; } private: int* data_; int capacity_; int size_; };
-
STL 容器了解吗?底层如何实现:
vector
数组,map
红黑树,红黑树的实现vector
:动态数组,底层是连续内存空间,支持随机访问。map
:基于红黑树实现,保证键值有序,插入、删除、查找时间复杂度为 O(log n)。- 红黑树:一种自平衡二叉查找树,满足以下性质:
- 每个节点是红色或黑色。
- 根节点是黑色。
- 每个叶子节点(NIL)是黑色。
- 红色节点的子节点必须是黑色。
- 从任一节点到其每个叶子的路径包含相同数目的黑色节点。
-
完美转发介绍一下。去掉
std::forward
会怎样?- 完美转发:通过
std::forward
将参数以原始类型(左值或右值)传递给其他函数,保持值类别不变。 - 去掉
std::forward
:可能导致参数被当作左值处理,失去移动语义,性能下降。
- 完美转发:通过
-
介绍一下
unique_lock
和lock_guard
区别?lock_guard
:简单的 RAII 锁,构造时加锁,析构时解锁,不支持手动控制。unique_lock
:更灵活的锁,支持手动加锁、解锁,可以转移所有权,支持延迟加锁。
-
C 代码中引用 C++ 代码有时候会报错为什么?
- C++ 支持函数重载,编译器会对函数名进行修饰(name mangling),导致 C 编译器无法识别。
- 解决方法:在 C++ 代码中使用
extern "C"
声明,禁止名称修饰。
-
静态多态有什么?虚函数原理:虚表是什么时候建立的?为什么要把析构函数设置成虚函数?
- 静态多态:通过函数重载和模板实现,编译时确定。
- 虚函数原理:通过虚函数表(vtable)实现动态绑定,虚表在编译时建立,运行时使用。
- 析构函数设置为虚函数:确保派生类对象通过基类指针删除时,调用正确的析构函数,避免资源泄漏。
-
map
为啥用红黑树不用 AVL 树?(几乎所有面试都问了map
和unordered_map
区别)- 红黑树:插入、删除、查找的时间复杂度均为 O(log n),且插入和删除时旋转操作较少,性能更优。
- AVL 树:虽然查找更快,但插入和删除时旋转操作较多,性能较差。
map
和unordered_map
区别:map
:基于红黑树,键值有序,查找时间复杂度 O(log n)。unordered_map
:基于哈希表,键值无序,查找时间复杂度 O(1)。
-
inline
失效场景- 函数体过大:编译器可能忽略
inline
建议。 - 递归函数:无法内联。
- 函数指针调用:无法内联。
- 虚函数:动态绑定,无法内联。
- 编译器优化设置:某些优化级别可能忽略
inline
。
- 函数体过大:编译器可能忽略
-
C++ 中
struct
和class
区别- 默认访问权限:
struct
的成员默认是public
。class
的成员默认是private
。
- 其他:
- 除此之外,
struct
和class
的功能几乎完全相同,都可以包含成员函数、继承、多态等。
- 除此之外,
- 默认访问权限:
-
如何防止一个头文件 include 多次
使用头文件保护宏(Header Guard):#ifndef MY_HEADER_H #define MY_HEADER_H // 头文件内容 #endif // MY_HEADER_H
或者使用
#pragma once
(非标准但广泛支持):#pragma once // 头文件内容
-
lambda 表达式的理解,它可以捕获哪些类型
- Lambda 表达式是一种匿名函数,可以捕获外部变量。
- 捕获方式:
[=]
:以值捕获所有外部变量。[&]
:以引用捕获所有外部变量。[x, &y]
:以值捕获x
,以引用捕获y
。[this]
:捕获当前对象的指针。[]
:不捕获任何变量。
-
友元
friend
介绍friend
关键字用于声明友元函数或友元类,使其可以访问类的私有成员。- 示例:
class MyClass { private: int data; friend void friendFunction(MyClass& obj); }; void friendFunction(MyClass& obj) { obj.data = 10; // 可以访问私有成员 }
-
move
函数std::move
用于将对象转换为右值,支持移动语义。- 示例:
std::string str1 = "Hello"; std::string str2 = std::move(str1); // str1 的资源被移动到 str2
-
模版类的作用
- 模板类用于实现通用类,支持多种数据类型。
- 示例:
template<typename T> class Box { public: T value; Box(T v) : value(v) {} }; Box<int> intBox(10); Box<std::string> strBox("Hello");
-
模版和泛型的区别
- C++ 模板:编译时生成代码,支持多种类型,功能强大但复杂。
- Java/C# 泛型:运行时实现,类型擦除,功能受限但简单。
-
内存管理:C++ 的
new
和malloc
的区别new
:- 是 C++ 操作符,调用构造函数。
- 返回类型安全指针,不需要类型转换。
- 支持异常处理。
malloc
:- 是 C 标准库函数,不调用构造函数。
- 返回
void*
,需要类型转换。 - 不支持异常处理。
-
new
可以重载吗,可以改写new
函数吗- 可以重载全局
new
和类特定的new
。 - 示例:
void* operator new(size_t size) { std::cout << "Custom new" << std::endl; return malloc(size); }
- 可以重载全局
-
C++ 中的
map
和unordered_map
的区别和使用场景map
:- 基于红黑树实现,键值有序。
- 插入、删除、查找时间复杂度为 O(log n)。
- 适用于需要有序键值的场景。
unordered_map
:- 基于哈希表实现,键值无序。
- 插入、删除、查找时间复杂度为 O(1)。
- 适用于需要快速查找且不关心顺序的场景。
-
他们是线程安全的吗
map
和unordered_map
都不是线程安全的。- 如果多线程访问,需要手动加锁(如
std::mutex
)。
-
C++ 标准库里优先队列是怎么实现的?
std::priority_queue
是基于堆(默认是大顶堆)实现的。- 底层容器默认是
std::vector
。 - 主要操作:
push
:插入元素,时间复杂度 O(log n)。pop
:删除堆顶元素,时间复杂度 O(log n)。top
:访问堆顶元素,时间复杂度 O(1)。
- 示例:
std::priority_queue<int> pq; pq.push(10); pq.push(5); pq.push(20); std::cout << pq.top(); // 输出 20
-
GCC 编译的过程
GCC 编译过程分为以下步骤:- 预处理:处理宏、头文件等,生成
.i
文件。gcc -E source.c -o source.i
- 编译:将预处理后的代码编译成汇编代码,生成
.s
文件。gcc -S source.i -o source.s
- 汇编:将汇编代码转换成机器码,生成
.o
文件。gcc -c source.s -o source.o
- 链接:将多个目标文件链接成可执行文件。
gcc source.o -o executable
- 预处理:处理宏、头文件等,生成
-
C++ Coroutine
C++20 引入了协程(Coroutine),用于简化异步编程。协程是一种可以暂停和恢复的函数,核心关键字包括:co_await
:暂停协程,等待异步操作完成。co_return
:返回协程的结果。co_yield
:生成一个值并暂停协程。
示例:
#include <coroutine> #include <iostream> struct MyCoroutine { struct promise_type { MyCoroutine get_return_object() { return {}; } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; }; MyCoroutine my_coroutine() { std::cout << "Coroutine started" << std::endl; co_await std::suspend_always{}; std::cout << "Coroutine resumed" << std::endl; } int main() { auto coro = my_coroutine(); // 手动恢复协程 coro.coro.resume(); }
-
extern "C"
有什么作用
extern "C"
用于在 C++ 代码中声明 C 语言的函数或变量,防止 C++ 编译器对函数名进行名称修饰(name mangling)。
示例:extern "C" { void my_function(int x); }
-
C++
memory_order
/ ELF 文件格式 / 中断对于操作系统的作用memory_order
:用于指定原子操作的内存顺序(如memory_order_relaxed
、memory_order_acquire
等)。- ELF 文件格式:可执行和可链接格式(Executable and Linkable Format),用于存储可执行文件、目标文件和共享库。
- 中断对于操作系统的作用:中断是操作系统处理异步事件的核心机制,用于响应硬件事件(如键盘输入、时钟中断等)和软件事件(如系统调用)。
-
C++ 的符号表
符号表是编译器生成的一种数据结构,用于存储程序中定义的符号(如函数名、变量名)及其相关信息(如类型、地址)。在链接阶段,符号表用于解析符号引用。 -
C++ 的单元测试
单元测试是对代码的最小单元(如函数、类)进行测试,常用单元测试框架包括:- Google Test:
示例:#include <gtest/gtest.h> int add(int a, int b) { return a + b; } TEST(AddTest, PositiveNumbers) { EXPECT_EQ(add(1, 2), 3); } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
- Catch2:
示例:#define CATCH_CONFIG_MAIN #include <catch2/catch.hpp> int add(int a, int b) { return a + b; } TEST_CASE("Add function", "[add]") { REQUIRE(add(1, 2) == 3); }
- Boost.Test:
示例:#define BOOST_TEST_MODULE AddTest #include <boost/test/unit_test.hpp> int add(int a, int b) { return a + b; } BOOST_AUTO_TEST_CASE(AddTest) { BOOST_CHECK_EQUAL(add(1, 2), 3); }
- Google Test:
-
如果 new 了之后出了问题直接 return。会导致内存泄漏。怎么办
可以使用智能指针(如
std::unique_ptr
或std::shared_ptr
)来管理动态分配的内存,确保在异常或提前返回时自动释放资源。例如:void func() { std::unique_ptr<int> ptr(new int(10)); if (/* 出问题 */) return; // 自动释放内存 }
或者使用 RAII 技术,将资源封装在对象中。
C++面试题(自用)
最新推荐文章于 2025-04-20 21:31:56 发布