腾讯wxg后台开发面经

文章目录

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;
}

在这里插入图片描述

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值