文章目录
- 1、C++ 编译器是如何解析 decltype 和 auto 关键字的?
- 2、vector的resize和reserve有什么区别?
- 3. vector是分配在堆还是栈,sizeof(vector)返回什么值?
- 4. unorderedmap中插入一个元素,它原来的iterater还有效吗?
- 5. map中插入一个元素,它原来的iterater还有效吗?
- 6. shared_ptr是不是线程安全的,为什么?
- 7. 把uniqueptr move到sharedptr会发生什么? (高频)
- 8. weak_ptr的作用是什么?它与shared_ptr的区别是什么?
- 9、HTTP/2和HTTP/1.1的主要区别是什么?
- 10. 使用 std::function 和 std::bind 绑定一个类成员函数,并调用它。
- 11. 如何重载[]操作符? 写一个例子
- 12. 假设你要实现一个线程安全的队列,其中 push 和 pop 需要支持多个线程并发访问,如何设计?请实现该数据结构
- 13. 代码实现:给定一个数组,找出数组中重复的数。
- 14. 代码实现:反转链表
- 15. 代码实现:实现一个线程安全的单例模式
1、C++ 编译器是如何解析 decltype 和 auto 关键字的?
在 C++ 中,decltype 和 auto 关键字用于类型推导,但它们的解析方式不同
auto 关键字的解析
auto 在编译期会进行类型推导,通常基于初始化表达式的类型进行推导。auto 也是编译期类型推导的关键字,但它的行为更像是一个“占位符”,由初始化表达式来决定类型
a.普通变量:
int x = 10;
auto y = x; // y 的类型为 int
b. 指针和引用:
int a = 10;
int& ra = a;
auto b = ra; // b 的类型是 int(不会保留引用)
注意:auto 不会保留引用,需要显式加上 auto&:
auto& c = ra; // c 的类型是 int&
c.数组和 auto:
int arr[3] = {1, 2, 3};
auto p = arr; // p 的类型是 int*,不是 int[3]
d.const 修饰符:
const int x = 42;
auto y = x; // y 的类型是 int(丢失 const)
auto& z = x; // z 的类型是 const int&
e.模板中的 auto:
template <typename T>
void func(T val) { } // 传值时会发生类型退化(decay)
int x = 42;
func(x); // T 被推导为 int
decltype 关键字的解析
decltype 主要用于获取表达式的类型,但不会对其进行类型退化(decay)
a.普通变量:
int x = 10;
decltype(x) y; // y 的类型是 int
b.表达式的类型:
int a = 10, b = 20;
decltype(a + b) c; // c 的类型是 int
c.引用的处理:
int x = 10;
int& ref = x;
decltype(ref) y = x; // y 的类型是 int&
d. decltype(auto)用法:
decltype(auto) 结合 decltype 和 auto 的特点,保留所有修饰符:
int x = 10;
int& ref = x;
decltype(auto) z = ref; // z 的类型是 int&
2、vector的resize和reserve有什么区别?
resize:调整容器的大小(元素数量)
#include <iostream>
#include <vector>
using namespace std;
int main() {
std::vector<int> v = {1, 2, 3};
v.resize(5); // 扩展到5个元素,默认填充 0(int 默认构造值)
for (int i : v) std::cout << i << " ";
// 输出: 1 2 3 0 0
cout << endl;
v.resize(2); // 缩小到 2 个元素
for (int i : v) std::cout << i << " ";
// 输出: 1 2
cout << endl;
v.resize(4, 9); // 扩展到 4 个元素,新元素填充 9
for (int i : v) std::cout << i << " ";
// 输出: 1 2 9 9
cout << endl;
return 0;
}
reserve:预留容量(内存空间)
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.reserve(5); // 预分配容量为 5
std::cout << "Capacity: " << v.capacity() << ", Size: " << v.size() << std::endl;
// 输出: Capacity: 5, Size: 0
v.push_back(10);
std::cout << "Capacity: " << v.capacity() << ", Size: " << v.size() << std::endl;
// 输出: Capacity: 5, Size: 1
return 0;
}
二者关键区别
3. vector是分配在堆还是栈,sizeof(vector)返回什么值?
vector是分配在堆还是栈?
std::vector 本身是一个类模板,它的对象(即vector实例)通常分配在栈上,但它管理的元素数据是动态分配在堆上的。
具体来说:
- vector对象本身:当你声明一个std::vector变量(比如std::vector v;),这个对象通常是在栈上创建的。它包含一些内部成员(如指向数据的指针、容量大小、元素个数等),这些成员占用固定大小的内存,存储在栈上。
- 元素数据:vector通过动态内存分配(通常使用new或分配器)在堆上存储实际的元素。当你调用push_back或resize等操作时,vector会在堆上分配或重新分配内存来存储这些元素。
一句话:vector对象在栈上,但它管理的元素在堆上。
sizeof(vector)返回什么值?
4. unorderedmap中插入一个元素,它原来的iterater还有效吗?
在 std::unordered_map 中插入一个元素后,原有的迭代器通常仍然有效,但有一个重要的例外:如果插入导致哈希表重新分配内存(rehash),那么所有已有迭代器都会失效。
具体原因解释
示例代码
#include <iostream>
#include <unordered_map>
int main() {
std::unordered_map<int, std::string> umap;
umap[1] = "one"; // 插入第一个元素
auto it = umap.find(1); // 获取迭代器
std::cout << "Before insert: " << it->second << "\n"; // 输出 "one"
umap[2] = "two"; // 插入第二个元素
// 检查迭代器是否仍然有效
std::cout << "After insert: " << it->second << "\n"; // 通常仍输出 "one"
// 假设大量插入导致 rehash
for (int i = 3; i < 60000000; ++i) {
umap[i] = std::to_string(i);
}
// 此时 it 可能已失效,使用它会导致未定义行为
// 不要这样做!
// std::cout << it->second << "\n";
// 检查迭代器是否仍然有效
if (it != umap.end()) {
std::cout << it->second << "\n"; // 此时 it 可能已失效,检查是否有效
} else {
std::cout << "Iterator is invalid after rehash.\n"; // 输出迭代器失效的提示
}
return 0;
}
5. map中插入一个元素,它原来的iterater还有效吗?
在 std::map 中插入一个元素后,原有的迭代器始终保持有效。
详细解释
示例代码
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> myMap;
myMap[1] = "one"; // 插入键 1
auto it = myMap.find(1); // 获取键 1 的迭代器
std::cout << "Before insert: " << it->second << "\n"; // 输出 "one"
myMap[2] = "two"; // 插入键 2
std::cout << "After insert: " << it->second << "\n"; // 仍然输出 "one",迭代器有效
// 插入更多元素
myMap[3] = "three";
myMap[0] = "zero";
std::cout << "After more inserts: " << it->second << "\n"; // 仍然有效,输出 "one"
return 0;
}
注意事项
规律
6. shared_ptr是不是线程安全的,为什么?
原因
#include <memory>
#include <thread>
//函数
void threadFunc(std::shared_ptr<int> sp) {
// 拷贝 sp,增加引用计数
std::shared_ptr<int> local = sp;
// local 离开作用域,减少引用计数
}
int main() {
auto sp = std::make_shared<int>(42);
std::thread t1(threadFunc, sp);
std::thread t2(threadFunc, sp);
t1.join();
t2.join();
// sp 的引用计数安全更新,最终销毁
return 0;
}
这里两个线程同时拷贝和销毁 sp,引用计数的变化是线程安全的,最终对象在引用计数为 0 时正确销毁。
#include <memory>
#include <thread>
void threadFunc(std::shared_ptr<int>& sp) {
sp = std::make_shared<int>(10); // 修改 sp
}
int main() {
auto sp = std::make_shared<int>(42);
std::thread t1(threadFunc, std::ref(sp));
std::thread t2(threadFunc, std::ref(sp));
t1.join();
t2.join();
// 未定义行为:两个线程同时修改 sp
return 0;
}
这里两个线程同时对 sp 赋值,会导致数据竞争,结果不可预测。
修改后
#include <memory>
#include <thread>
#include <mutex>
std::mutex mtx;
void threadFunc(std::shared_ptr<int>& sp) {
std::lock_guard<std::mutex> lock(mtx); // 确保在修改 sp 时加锁
sp = std::make_shared<int>(10); // 修改 sp
}
int main() {
auto sp = std::make_shared<int>(42);
std::thread t1(threadFunc, std::ref(sp));
std::thread t2(threadFunc, std::ref(sp));
t1.join();
t2.join();
return 0;
}
7. 把uniqueptr move到sharedptr会发生什么? (高频)
将 std::unique_ptr 移动到 std::shared_ptr 是安全的,且 std::shared_ptr 会接管资源的生命周期,而原 std::unique_ptr 变为空。
示例代码
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> uptr = std::make_unique<int>(10);
std::shared_ptr<int> sptr = std::move(uptr); // 资源转移
std::cout << "uptr is " << (uptr ? "not empty" : "empty") << std::endl;
std::cout << "sptr use count: " << sptr.use_count() << std::endl;
return 0;
}
注意事项
8. weak_ptr的作用是什么?它与shared_ptr的区别是什么?
std::weak_ptr 的作用
使用 std::weak_ptr 防止循环引用
如果两个对象相互持有 std::shared_ptr,则引用计数不会归零,导致内存泄漏。使用 std::weak_ptr 可以解决这个问题。
#include <iostream>
#include <memory>
struct B;
struct A {
std::shared_ptr<B> ptr;
~A() { std::cout << "A destroyed\n"; }
};
struct B {
std::shared_ptr<A> ptr;
~B() { std::cout << "B destroyed\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->ptr = b; // A 拥有 B
b->ptr = a; // B 拥有 A(循环引用)
//问题是:a 和 b 互相持有 shared_ptr,
//即使 main() 结束,它们的 use_count 仍大于 0,导致内存泄漏。
return 0; // 资源无法释放,导致内存泄漏
}
使用 std::weak_ptr 解决循环引用
#include <iostream>
#include <memory>
struct B;
struct A {
std::weak_ptr<B> ptr; // 改为 weak_ptr,避免循环引用
~A() { std::cout << "A destroyed\n"; }
};
struct B {
std::shared_ptr<A> ptr;
~B() { std::cout << "B destroyed\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->ptr = b; // A 持有 B(弱引用,不增加计数)
b->ptr = a; // B 持有 A(共享所有权)
return 0; // 资源正常释放,不会内存泄漏
}
在 main() 函数的结尾,a 和 b 都超出了作用域,shared_ptr 的引用计数会被减少。
由于 A 中的 ptr 是 std::weak_ptr,它不会增加 B 的引用计数,所以 B 可以正常销毁。
当 B 被销毁时,A 也可以被销毁,因为 B 不再持有 A 的共享所有权。
std::weak_ptr 的 lock() 方法
由于 weak_ptr 不能直接访问对象,如果想访问对象,需要使用 .lock() 方法将其转换为 shared_ptr:
std::weak_ptr<int> wptr = sptr; // sptr 是一个 std::shared_ptr<int>
if (auto spt = wptr.lock()) { // 获取 std::shared_ptr<int>
std::cout << "Value: " << *spt << std::endl;
} else {
std::cout << "Object has been destroyed" << std::endl;
}
9、HTTP/2和HTTP/1.1的主要区别是什么?
HTTP 基本概念
HTTP 是超文本传输协议,也就是HyperText Transfer Protocol。
HTTP 是一种在计算机世界里专门在 两点 之间 传输 文字、图片、音频、视频等 超文本 数据的 约定和规范。
HTTP/2 vs HTTP/1.1 的主要区别
并发传输, HTTP/2 就很⽜逼了,引出了 Stream 概念,多个 Stream 复⽤在⼀条 TCP 连接。
从上图可以看到,1 个 TCP 连接包含多个 Stream,Stream ⾥可以包含 1 个或多个
Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成。Message ⾥包含⼀条或者多个 Frame,Frame 是 HTTP/2 最⼩单位,以⼆进制压缩格式存放 HTTP/1 中的内容(头部和包体)。
HTTP/2 的关键改进
示例:HTTP/1.1 vs HTTP/2 传输过程
HTTP/1.1:
客户端:请求 index.html
服务器:返回 index.html
客户端:请求 style.css
服务器:返回 style.css
客户端:请求 script.js
服务器:返回 script.js
每个请求是串行的,导致阻塞。
HTTP/2:
客户端:请求 index.html、style.css、script.js(同时)
服务器:返回 index.html、style.css、script.js(同时)
多个请求并行进行,减少延迟。
10. 使用 std::function 和 std::bind 绑定一个类成员函数,并调用它。
代码示例
#include <iostream>
#include <functional> // 包含 std::function 和 std::bind
class MyClass {
public:
// 一个成员函数,接受两个参数
void print(int x, const std::string& s) {
std::cout << "x = " << x << ", s = " << s << std::endl;
}
};
int main() {
MyClass obj; // 创建一个 MyClass 对象
// 使用 std::function 声明一个函数对象
std::function<void(int, const std::string&)> func;
// 使用 std::bind 绑定 MyClass 的成员函数 print
// std::placeholders::_1 和 _2 分别表示第一个和第二个参数的占位符
/*
&MyClass::print:表示要绑定的成员函数。&MyClass::print 是指向 MyClass 类成员函数 print 的指针。
&obj:表示 print 函数的第一个参数(this 指针),即 obj 是调用 print 成员函数的对象。因此,&obj 是 print 函数的隐式 this 指针。
std::placeholders::_1 和 std::placeholders::_2:占位符用于表示待绑定的参数。这些占位符将被调用时的实际参数替代。_1 对应第一个参数,_2 对应第二个参数。
*/
func = std::bind(&MyClass::print, &obj, std::placeholders::_1, std::placeholders::_2);
// 调用绑定的函数
func(42, "Hello, World!"); // 预期输出: x = 42, s = Hello, World!
return 0;
}
面试中的可能追问和应对
1. “为什么用 std::bind 而不用 lambda?”
- 回答:
- std::bind 和 lambda 都可以实现类似功能,但 std::bind 是 C++11 引入的工具,专门为绑定函数和参数设计,语法更简洁,尤其在需要占位符时。
- 用 lambda 也可以,比如:
std::function<void(int, const std::string&)> func = [&obj](int x, const std::string& s) {
obj.print(x, s);
};
- 但 lambda 更灵活,适合复杂逻辑,std::bind 更适合简单绑定。
11. 如何重载[]操作符? 写一个例子
重载 [] 操作符,就是说可以让你自定义类对象使用方括号访问数据(例如 obj[index])。
实现方式
示例代码
#include <iostream>
#include <stdexcept>
using namespace std;
class MyArray {
private:
int* data; // 动态分配的数组
int size; // 数组大小
public:
// 构造函数
MyArray(int s) : size(s) {
data = new int[size];
for (int i = 0; i < size; i++) {
data[i] = 0; // 初始化
}
}
// 非 const 版本:支持读写
int& operator[](int index) {
if (index < 0 || index >= size) {
throw out_of_range("Index out of bounds");
}
return data[index];
}
// const 版本:仅支持读取
const int& operator[](int index) const {
if (index < 0 || index >= size) {
throw out_of_range("Index out of bounds");
}
return data[index];
}
// 析构函数
~MyArray() {
delete[] data;
}
};
int main() {
MyArray arr(3);
// 测试读写
arr[1] = 10; // 使用非 const 版本
cout << arr[1] << endl; // 输出 10
// 测试 const 对象
const MyArray constArr(3);
cout << constArr[1] << endl; // 使用 const 版本,输出 0
// 测试越界
try {
arr[5] = 100;
} catch (const out_of_range& e) {
// e.what() 是 std::exception 类(out_of_range 是从 std::exception 继承的)中的一个成员函数,用来返回异常的描述信息。
cout << e.what() << endl; // 输出 "Index out of bounds"
}
return 0;
}
面试官也可能问到的要点
12. 假设你要实现一个线程安全的队列,其中 push 和 pop 需要支持多个线程并发访问,如何设计?请实现该数据结构
基于 mutex 和 condition_variable 的线程安全队列
#include <iostream>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
//模版
template <typename T>
class ThreadSafeQueue {
private:
std::queue<T> queue;
std::mutex mtx; //锁
std::condition_variable cv; //条件变量
public:
void push(T value) {
std::lock_guard<std::mutex> lock(mtx);
queue.push(value);
cv.notify_one(); // 通知等待的线程
}
T pop() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] { return !queue.empty(); }); // 等待直到队列非空
T value = queue.front();
queue.pop();
return value;
}
};
//生产者
void producer(ThreadSafeQueue<int>& tsq) {
for (int i = 1; i <= 5; ++i) {
tsq.push(i);
std::cout << "Produced: " << i << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
//消费者
void consumer(ThreadSafeQueue<int>& tsq) {
for (int i = 1; i <= 5; ++i) {
int value = tsq.pop();
std::cout << "Consumed: " << value << "\n";
}
}
int main() {
ThreadSafeQueue<int> tsq;
std::thread t1(producer, std::ref(tsq));
std::thread t2(consumer, std::ref(tsq));
t1.join();
t2.join();
return 0;
}
13. 代码实现:给定一个数组,找出数组中重复的数。
使用 unordered_set 存储已经访问过的元素,如果遇到重复元素,则直接返回。
count() 是 C++ 标准库中的 std::unordered_set 类的成员函数。它用于检查容器中是否存在某个元素。
#include <iostream>
#include <vector>
#include <unordered_set>
void findDuplicates(const std::vector<int>& nums) {
std::unordered_set<int> st;
for(const int& num: nums) {
if(st.count(num)) {
std::cout << "重复的数: " << num << std::endl;
} else {
st.insert(num);
}
}
}
int main() {
std::vector<int> nums = {4, 3, 2, 7, 8, 2, 3, 1};
findDuplicates(nums);
return 0;
}
优点:
- 线性时间复杂度 O(n),适用于大规模数据。
- 使用 unordered_set 提高查找效率。
14. 代码实现:反转链表
就是Leetcode—206.反转链表【简单】,我的博客提供了一题多解
给定一个单链表的头节点,反转整个链表并返回新的头节点。
示例:输入 1->2->3->4->5->NULL,输出 5->4->3->2->1->NULL。
解题思路
代码实现
#include <iostream>
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* nextTemp = curr->next; // 保存下一个节点
curr->next = prev; // 反转指针
prev = curr; // 前进
curr = nextTemp;
}
return prev; // 新的头节点
}
// 测试代码
int main() {
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
head->next->next->next = new ListNode(4);
ListNode* newHead = reverseList(head);
ListNode* curr = newHead;
while (curr) {
std::cout << curr->val << " ";
curr = curr->next;
}
return 0;
}
说明:
- 时间复杂度:O(n),n 是链表长度。
- 空间复杂度:O(1),只用了几个指针。
15. 代码实现:实现一个线程安全的单例模式
#include <iostream>
#include <mutex>
class Singleton {
private:
static Singleton* instance;
static std::mutex mtx;
Singleton() { std::cout << "Singleton created\n"; } // 私有构造函数
public:
Singleton(const Singleton&) = delete; // 禁止拷贝
Singleton& operator=(const Singleton&) = delete; // 禁止赋值
static Singleton* getInstance() {
std::lock_guard<std::mutex> lock(mtx); // 线程安全
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};
// 静态成员初始化
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
// 测试代码
int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
std::cout << "s1: " << s1 << ", s2: " << s2 << std::endl; // 地址相同
return 0;
}
说明:
- 使用 std::mutex 和 std::lock_guard 确保线程安全。
- C++11 后可以用 static 局部变量实现更简洁的线程安全单例(Meyers’ Singleton),但这里展示传统方式。
最优解——使用C++11标准的局部静态变量
#include <iostream>
#include <mutex>
class Singleton {
private:
Singleton() { std::cout << "Singleton created\n"; } // 私有构造函数
public:
// 禁止拷贝
Singleton(const Singleton&) = delete;
// 禁止赋值
Singleton& operator=(const Singleton&) = delete;
// 静态局部变量,线程安全
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
};
int main() {
Singleton& s1 = Singleton::getInstance();
Singleton& s2 = Singleton::getInstance();
std::cout << "s1: " << &s1 << ", s2: " << &s2 << std::endl; // 地址相同
return 0;
}
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!