其他项目问题

为什么服务器要以root身份启动,不能以root身份运行

为什么要以 root 启动?

绑定低编号端口(如 80、443)。
配置共享内存、设备文件、调整资源等需要的高权限操作。

为什么不能以 root 运行?

潜在漏洞可能导致系统被完全攻破。
不符合最小权限原则。
服务隔离无法实现,其他服务可能受到影响。
意外操作可能导致灾难性的后果。
如何解决这个问题?

启动时以 root 完成特权操作,然后立刻降级为普通用户运行。
使用 setuid() 和 setgid() 等 Linux 用户管理接口实现特权降级。

字节面试准备

字节面试准备

1. C++ 基础

(1) 面向对象的三个特性

C++ 是一种支持面向对象编程的语言,其面向对象编程的三个核心特性是:封装(Encapsulation)、继承(Inheritance)、多态(Polymorphism)。

  • 封装
    封装是将数据(成员变量)和操作这些数据的代码(成员函数)绑定在一起,形成一个整体(类)。
    优点:

    • 限制对内部成员的访问,保护数据隐私(通过 publicprotectedprivate 访问权限控制关键字)。
    • 提高代码的可维护性和复用性。
  • 继承
    继承是一种用于实现代码复用和建立层次化关系的机制。子类可以继承父类的属性和行为。
    优点:

    • 重用父类代码。
    • 可以通过父类指针或引用操作多态对象。
  • 多态
    多态是允许通过基类引用或指针访问派生类对象,并调用派生类的重写方法。基于动态绑定(Run-time Polymorphism)的多态主要通过虚函数实现。
    优点:

    • 提高代码的扩展性和灵活性,可以使用统一的接口处理不同类型的对象。

(2) 多态和虚函数的底层实现

  • 多态的实现

    • 多态的核心是通过 虚函数表(Virtual Table)和虚函数表指针实现的。
    • 编译器会在类中生成一个指向虚函数表的指针,称为 vptr
    • 虚函数表是一个数组,存储了该类的所有虚函数的函数指针。
    • 在运行时,通过 vptr 指向的虚函数表,根据实际的动态类型调用对应的方法。
  • 虚函数的底层实现

    • 类中有虚函数时,类会生成一个虚函数表(存储虚函数指针)。
    • 每个对象的内存布局中会加入一个指向虚函数表的指针(vptr)。
    • 通过 vptr 和虚函数表在运行时找到并调用具体的函数。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base {
public:
virtual void display() { std::cout << "Base" << std::endl; }
};

class Derived : public Base {
public:
void display() override { std::cout << "Derived" << std::endl; }
};

int main() {
Base* obj = new Derived();
obj->display(); // 执行 Derived::display()
}

区别场景:带虚函数与不带虚函数

  • 如果类中没有虚函数,那么对象内存只存储成员变量。
  • 带虚函数的类,内存中会存储额外的虚函数表指针。
  • 因此,带虚函数的对象的内存占用会比没有虚函数的对象多。

(3) 多继承的特殊情况(菱形继承问题)

场景:如何区分多继承中调用的同名成员和方法?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
class A {
public:
int x;
void func() { std::cout << "A::func()" << std::endl; }
};

class B : public A {
public:
void func() { std::cout << "B::func()" << std::endl; }
};

class C : public A {
public:
void func() { std::cout << "C::func()" << std::endl; }
};

class D : public B, public C {};

int main() {
D obj;
// 调用 B::func 或 C::func?
obj.B::func(); // 通过类名加作用域区分
obj.C::func();
return 0;
}

菱形继承问题的解决方式:

  • 普通继承会导致 A 的成员在 BC 中各有一份(两份副本)。
  • 使用 虚继承 可以解决该问题,确保 A 的成员在多继承的子类中只有一份。
1
2
3
4
class A { public: int x; };
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

D 类中只有一份 A::x


3. 内存

(1) malloc 和 new 的区别

  1. malloc

    • 只分配内存,不调用构造函数。
    • 返回 void* 需要强制类型转换。
    • 需要 free 手动释放。
  2. new

    • 分配内存并自动调用构造函数初始化对象。
    • 返回目标类型的指针,无需强转。
    • 使用 delete 释放内存并调用析构函数。

(2) 只分配内存或只调用构造函数

  1. 只分配内存(不调用构造函数):

    • 可使用 operator new
      1
      void* ptr = ::operator new(sizeof(MyClass)); // 只分配内存
  2. 只调用构造函数(对象已分配内存):

    • 可通过 placement new
      1
      MyClass* obj = new(ptr) MyClass(args); // 在已有内存 `ptr` 上构造对象

operator new:用来分配原始内存,不涉及对象的构造。
operator delete:用来释放原始内存,不涉及对象的析构。
普通的 new 调用等价于:

1
2
MyClass* obj = static_cast<MyClass*>(::operator new(sizeof(MyClass))); // 分配内存
new (obj) MyClass(); // 构造对象

普通的 delete 调用等价于:

1
2
obj->~MyClass();             // 调用析构函数
::operator delete(obj); // 释放内存

注意:new不能重载,只有operator new才能重载

(3) sizeof 结构体的对齐规则

C++ 中结构体内存对齐主要受到以下因素影响:

  1. 每个成员变量的对齐方式由编译器决定(通常与成员类型的大小相关)。
  2. 结构体的总大小必须是最大对齐成员的倍数。

示例:

1
2
3
4
5
6
7
8
9
10
struct S1 {
char A; // 1 字节
char B; // 1 字节
int C; // 4 字节
};
struct S2 {
char A; // 1 字节
int C; // 4 字节
char B; // 1 字节
};
  • 对于 S1,内存布局为:A + B + padding + C(总大小 8)
  • 对于 S2,内存布局为:A + padding + C + B + padding(总大小 12)

4. STL 和智能指针

(1) 智能指针概览

C++ 标准库提供了三种智能指针:

  1. std::unique_ptr: 独占所有权,不可复制。
  2. std::shared_ptr: 共享所有权,使用引用计数。
  3. std::weak_ptr: 使用弱引用,依赖 shared_ptr,避免循环引用。

(2) std::shared_ptr 的实际应用

  • 场景:共享资源的生命周期管理(如线程池中的任务对象)。
  • 示例:
    1
    2
    3
    4
    std::shared_ptr<int> p = std::make_shared<int>(10);
    {
    std::shared_ptr<int> q = p; // 引用计数+1
    } // q 离开作用域,引用计数-1

使用智能指针(如 std::shared_ptr)的优点

自动化的内存管理,避免内存泄漏和悬垂指针。简化代码逻辑,减少人工管理内存的复杂性。更容易与标准容器(如 std::vector)配合使用。不使用智能指针是可以的,

但代价是:

更容易引入 bug,例如内存泄漏、悬垂指针问题。
增加代码维护难度,并且可能需要大量的单元测试来覆盖所有的边界情况。
难以保证代码的健壮性,特别是在复杂的资源关系中。

RAII是什么

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种资源管理的编程惯用法,是 C++ 中处理资源管理的重要设计理念。它通过将资源的生命周期与对象的生命周期绑定来确保资源得到正确的分配和释放,从而避免资源泄漏。

核心思想

  1. 资源绑定到对象的生命周期

    • 当一个对象在栈上或通过堆分配时,与该对象相关联的资源(例如内存、文件句柄、锁、网络连接等)也随之初始化。
    • 当该对象离开作用域(或者析构)时,资源会被自动释放。
  2. 依靠析构函数来释放资源

    • C++ 中,当一个对象生命周期结束时,其析构函数会被自动调用。因此,可以利用析构函数自动释放资源,而无需手动释放。
    • 通过 RAII,资源分配后不用担心异常、早退或其他复杂逻辑干扰正确释放。

RAII 的优点

  1. 自动化资源管理:用户不需要手动释放资源,不用担心遗漏或错误。
  2. 异常安全:即使代码中间发生异常,析构函数仍然会自动释放资源。
  3. 代码简洁:减少了显式的资源释放代码,避免了冗余或错误。

应用场景

RAII 在内存分配、文件操作、线程锁、数据库连接等场景广泛应用。例如:

  1. 智能指针
    • std::unique_ptr:独占资源,适合动态内存管理。
    • std::shared_ptr:共享资源,适合多个对象共享动态内存。
  2. 互斥锁
    • std::lock_guard<std::mutex>std::unique_lock
  3. 文件与流对象
    • C++ 的 IO 流类如 std::ifstreamstd::ofstream
  4. 其他自定义的资源管理类
    • 如管理数据库连接、网络套接字、句柄等。

总结

RAII 的核心就是将资源的生命周期绑定到对象的生命周期,通过构造函数和析构函数确保资源的正确分配和释放。这种机制有效避免了手动管理资源时的各种问题(如资源泄漏、重复释放等),是 C++ 中非常重要的编程思想,尤其在异常安全和代码简洁性方面效果显著。

面试中,如果被问到 RAII,你可以直接引用 C++ 智能指针、文件流或锁管理类作为例子展开解释。进一步可以说明 RAII 提升了程序的安全性及简洁性,使 C++ 的资源管理更加工程化和易用化。

5. 操作系统

(1) 并发与并行的区别

  • 并发(Concurrency):多个任务在逻辑上同时进行,但实际上可能是按时间片交替执行。
  • 并行(Parallelism):多个任务在物理上真正同时运行(需要多核 CPU 支持)。

(2) 最大并发线程池设计

  • 关键点:
    1. 使用任务队列管理任务。
    2. 通过信号量或条件变量限制线程并发数量。
    3. 动态创建、销毁线程降低资源消耗(即线程复用)。

6. 网络

(1) HTTPS 的通信过程

  1. 客户端发送请求,包括支持的协议版本、加密套件等。
  2. 服务器发送证书,客户端验证证书的合法性。
  3. TLS 握手,双方协商对称密钥(通过非对称加密交换密钥)。
  4. 加密通信,之后的所有数据使用对称密钥加密。

7. 项目挑战

Buffer 的思想

  • 临时缓存用于解决数据处理速度的差异(如网络接收速度慢于处理速度)。
  • **自动增长:**通过动态分配内存扩容实现,常用 指数增长(2 倍增长) 策略;也可以按照具体业务需要调整。

智能指针

智能指针

unique_ptr

作用域指针,不能复制

栈分配指针,当死亡时,自动释放所管理的内存,无需显示调用delete

1
2
3
std::unique_ptr<int> p1 = std::make_unique<int>(10); // 管理动态分配的内存
// std::unique_ptr<int> p2 = p1; // 错误!unique_ptr不支持拷贝
std::unique_ptr<int> p2 = std::move(p1); // 通过 std::move 转移所有权

只能显示调用构造函数,因为其构造函数有explicit关键字,没有了构造函数的隐式转换

最好的调用还是使用make_unique会捕获异常,不会产生悬空指针问题

shared_ptr

追踪引用计数,如果引用为0,则释放内存

需要分配内存用于计数

1
shared_ptr<Entity> p = make_shared<Entity>();

作用主要包括:

  1. 自动管理动态分配的对象,避免手动调用 delete
  2. 支持共享所有权,让多个 shared_ptr 可以安全地访问同一对象。
  3. 借助引用计数机制,实现对象的生命周期控制,当最后一个 shared_ptr 销毁后,自行释放资源。

unique_ptr:独占所有权,不支持多个指针管理同一个对象;更轻量且不存在循环引用问题。

weak_ptr:用于观察 shared_ptr 管理的对象,不增加引用计数;主要用于辅助 shared_ptr,避免循环引用的问题。

std::weak_ptr

std::weak_ptr 是 C++11 引入的一种智能指针,和 std::shared_ptr 一起使用,用于避免 循环引用 问题,同时提供了一种对 std::shared_ptr 所管理对象的弱引用(non-owning reference)。它不改变所管理对象的引用计数。

  • 循环引用问题
    在使用 std::shared_ptr 时,如果两个对象互相以 shared_ptr 引用彼此,会导致内存泄漏,因为它们的引用计数无法递减到 0。
    • shared_ptr 通过引用计数管理对象的生命周期,当引用计数为 0 时,自动释放对象。
    • 如果存在循环引用,两个对象会始终持有对方,这样它们的引用计数永远不会减为 0,因此无法释放内存。
  • 非拥有性的弱引用
    有时候,一个对象只需要 “观察” 对另一个对象的引用,而无需控制它的生命周期。这时使用 std::weak_ptr 是更合理的选择。

std::weak_ptr 提供了一种临时、不影响生命周期的引用,从而解决了上述问题。

总结

std::weak_ptr 的主要使用场景包括:

  1. 解决 std::shared_ptr 的循环引用问题
  2. 跨组件之间的非拥有性引用,例如缓存对象的管理。
  3. 事件监听器或回调函数,避免悬垂指针的产生
  4. 在弱引用需求场景下提供更加灵活的资源管理,而不是一味增加强引用计数。

std::weak_ptr 的特点

  1. 不控制对象的生命周期
  2. 检测对象是否已销毁
    • 可以通过调用 weak_ptrexpired() 方法来检查被引用的对象是否已经销毁。
  3. 使用 lock() 转换为 shared_ptr
    • 如果需要安全地访问被引用的对象,可以调用 weak_ptrlock() 方法,返回一个临时的 shared_ptr。如果对象已销毁,lock() 会返回一个空指针。

假设我们有两个类 AB,它们通过 std::shared_ptr 互相引用。如果没有使用 weak_ptr,将会发生循环引用,导致内存泄漏。

示例代码(循环引用问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
std::shared_ptr<B> ptrB; // A 持有共享指针引用 B
~A() { std::cout << "A destroyed" << std::endl; }
};

class B {
public:
std::shared_ptr<A> ptrA; // B 持有共享指针引用 A
~B() { std::cout << "B destroyed" << std::endl; }
};

int main() {
// 创建循环引用
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->ptrB = b; // A 持有 B
b->ptrA = a; // B 持有 A

// 离开 main() 作用域时,A 和 B 的引用计数不会减到 0,导致内存泄漏
return 0;
}

运行结果:

1
# 没有输出,因为 `A` 和 `B` 无法正常析构,发生内存泄漏。

解决循环引用的正确做法
将其中一个引用改为 std::weak_ptr,避免两个对象互相增加引用计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class B; // 前向声明

class A {
public:
std::weak_ptr<B> ptrB; // 弱引用 B
~A() { std::cout << "A destroyed" << std::endl; }
};

class B {
public:
std::shared_ptr<A> ptrA; // 共享指针持有 A
~B() { std::cout << "B destroyed" << std::endl; }
};

int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->ptrB = b; // A 弱引用 B
b->ptrA = a; // B 持有 A

// 离开作用域时,A 和 B 都将正确析构
return 0;
}

运行结果:

1
2
B destroyed
A destroyed

通过将某一侧的引用改为 std::weak_ptr,打破了循环引用。


跨组件间的弱引用

如果某些对象之间并无强依赖关系,但仍需临时引用,则可以使用 std::weak_ptr

示例 1:缓存管理
在缓存系统中,如果一个对象的存在依赖于被缓存的内容,则可以使用 std::weak_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <memory>
#include <unordered_map>
#include <string>

class CachedObject {
public:
CachedObject(std::string name) : name(name) {
std::cout << name << " created" << std::endl;
}
~CachedObject() {
std::cout << name << " destroyed" << std::endl;
}
void printName() { std::cout << "Object name: " << name << std::endl; }

private:
std::string name;
};

int main() {
std::unordered_map<std::string, std::weak_ptr<CachedObject>> cache;

{
auto obj1 = std::make_shared<CachedObject>("Object1");
cache["key1"] = obj1;

auto obj2 = std::make_shared<CachedObject>("Object2");
cache["key2"] = obj2;

// 使用缓存中的对象
if (auto obj = cache["key1"].lock()) {
obj->printName(); // 输出:Object name: Object1
}
} // obj1 和 obj2 均超出作用域,被释放

// 尝试访问释放的对象
if (cache["key1"].expired()) {
std::cout << "Object1 no longer exists" << std::endl; // 输出
}

return 0;
}

运行结果:

1
2
3
4
5
6
Object1 created
Object2 created
Object name: Object1
Object2 destroyed
Object1 destroyed
Object1 no longer exists

事件回调(防止悬垂引用)

如果某类对象注册了一个事件监听器或回调函数,而监听器的生命周期可能比被观察的对象短,那么可以使用 std::weak_ptr 避免访问悬垂的指针。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <memory>
#include <functional>

class EventSource;

class Listener {
public:
Listener(std::shared_ptr<EventSource> source) : source(source) {}
void onEvent() {
if (auto src = source.lock()) { // 检查 source 是否仍有效
std::cout << "Event handled" << std::endl;
} else {
std::cout << "Source no longer exists" << std::endl;
}
}

private:
std::weak_ptr<EventSource> source;
};

class EventSource : public std::enable_shared_from_this<EventSource> {
public:
void fireEvent() {
if (listener) listener(); // 调用回调
}

void setListener(std::function<void()> callback) {
listener = callback;
}

private:
std::function<void()> listener;
};

int main() {
std::shared_ptr<EventSource> source = std::make_shared<EventSource>();
{
auto listener = std::make_shared<Listener>(source);
source->setListener([listener]() { listener->onEvent(); });

source->fireEvent(); // 输出:Event handled
}

// Listener 已销毁,无悬垂引用
source->fireEvent(); // 不输出,Listener 已解除绑定

return 0;
}

红黑树

红黑树

基本规则:

隐藏的空节点是黑色

满足Avl树

根节点和叶子节点都是黑色

不能有连续两个红色节点

从根节点到叶子节点的每个路径上黑色节点数量都一致

插入:

插入节点是根节点;直接变黑

插入节点的叔叔是红色;叔父爷爷变色,爷爷变插入节点继续调整

插入节点的叔叔是黑 ;(LL,RL,LR,RL)旋转,然后变色

红黑树 - 定义, 插入, 构建_哔哩哔哩_bilibili

腾讯面试准备

算法和数据结构

刷题

C++的stl和新特性

常量指针和指针常量

1
const int *p = &a;//常量指针,指针指向常量,指针指向的变量不能通过指针修改,但是指针指向的值可以改变
1
int *const p = &a;//指针常量,是一个常量,不能改变指向的对象(地址),但是可以改变地址的内容

野指针和悬浮指针

野指针是指向已经被释放或者⽆效的内存地址的指针

悬浮指针是指向已经被销毁的对象的引⽤

区别就是一个是指针一个是引用

网络编程

网络编程的基本流程

(1) 服务端代码流程

1
2
3
4
5
6
1. socket()             // 创建一个套接字
2. bind() // 将套接字与特定 IP 地址和端口绑定
3. listen() // 开启监听,等待客户端连接
4. accept() // 相应客户端的连接请求,接受新的连接
5. send()/recv() // 用于和客户端通信(读取请求或者写入响应
6. close() // 关闭连接

(2) 客户端代码流程

1
2
3
4
1. socket()             // 创建一个套接字
2. connect() // 连接到服务端指定的 IP 地址和端口
3. send()/recv() // 用于发送请求或读取服务端的响应
4. close() // 关闭连接

调试命令,gdb/vscode

额外:linux操作系统的内存管理,文件系统,进程和线程调度

进程间的通信机制:信号量,条件变量,生产者消费者

异步通信:回调函数,生产者消费者(promise-future和消息队列)

同步通信:阻塞式调用,文件或网络操作

引用和指针的区别

1. 什么是引用和指针?

  1. 引用(Reference)

    • 引用是某个变量的别名,声明后与该变量绑定在一起,不能再绑定其他变量。
    • 本质上是一个语法糖,用更简洁的方式访问变量。

    示例代码:

    1
    2
    3
    int a = 10;
    int& ref = a; // 引用 `ref` 绑定到变量 `a`
    ref = 20; // 实际修改的是 `a`
  2. 指针(Pointer)

    • 指针是存储变量地址的一种特殊变量,通过指针可以间接访问或操作存储在内存地址上的值。
    • 指针可以指向不同的变量或内存单元。

    示例代码:

    1
    2
    3
    int a = 10;
    int* ptr = &a; // 指针 ptr 存储变量 a 的地址
    *ptr = 20; // 实际修改的是 a

2. 引用和指针的区别

特性 引用 指针
定义方式 使用 & 声明 使用 * 声明
是否可以为空 引用必须绑定到变量 指针可以是 nullptr 或空
绑定后是否可以更改 一旦绑定不能更改 指针可以重新指向其他变量
语法 直接使用,无需解引用符号 需要用 * 解引用
内存布局 编译器实现(可能是指针) 明确存储变量地址
需要初始化 声明时必须初始化 可以声明后再初始化
别名关系 是原变量的别名 独立的变量
运算 不支持运算 可以进行加减运算
灵活性 较低,限定性强 较高,可以动态分配内存

3. 区别详解

(1) 是否必须初始化

  • 引用:

    • 引用在声明的时候必须初始化,否则无法通过编译。
    1
    2
    int a = 10;
    int& ref; // 编译错误,引用必须绑定变量
  • 指针:

    • 指针声明后可以不初始化,但最好将其初始化(例如初始化为 nullptr),否则容易产生未定义行为。
    1
    2
    int* ptr;  // 未初始化,非法操作可能导致未定义行为
    int* ptr = nullptr; // 推荐初始化为 nullptr

(2) 是否可以为空

  • 引用:

    • 引用不能指向空(nullptr),它必须始终绑定到有效的变量。
  • 指针:

    • 指针可以指向空内存区域(nullptr),表示它当前没有指向任何变量。

    示例:

    1
    2
    int* ptr = nullptr;  // 合法
    int& ref = nullptr; // 编译错误,引用必须绑定到变量

(3) 绑定后是否可以更改

  • 引用:

    • 引用一旦绑定到变量,就不能再绑定到其他变量,引用始终是它所绑定变量的别名。
    1
    2
    3
    int a = 10, b = 20;
    int& ref = a; // ref 绑定到 a
    ref = b; // 修改的是 a 的值,而不是重新绑定到 b
  • 指针:

    • 指针可以随时更改指向,可以指向其他变量或内存单元。
    1
    2
    3
    int a = 10, b = 20;
    int* ptr = &a; // ptr 指向 a
    ptr = &b; // ptr 改为指向 b

(4) 使用上的差异

  • 引用:

    • 如果将一变量赋值给引用,引用直接操作变量本身。
    1
    2
    3
    int a = 10;
    int& ref = a;
    ref = 20; // 改变的是 a,a 的值变为 20
  • 指针:

    • 要改变指针指向变量的值,需要解引用(*)。
    1
    2
    3
    4
    int a = 10, b = 20;
    int* ptr = &a; // 指针指向 a
    *ptr = 30; // 修改 a 的值为 30
    ptr = &b; // 改为指向 b

(5) 运算能力

  • 引用:
    • 引用不支持指针的算术运算,例如加减法。
  • 指针:
    • 指针可以进行算术运算,例如指针递增/递减,用于访问数组元素。
    1
    2
    3
    int arr[3] = {1, 2, 3};
    int* ptr = arr; // 指向数组的第一个元素
    ptr++; // 指向下一个元素

(6) 内存特性

  • 引用在编译器实现中,可能会转换为指针,但它对开发者是透明的。
  • 指针本质上是一个变量,存储的是某个地址,并且占用内存。

4. 引用与指针的适用场景

(1) 适用引用

  1. 函数参数传递

    • 使用引用避免拷贝实参,提高性能。
    • 常用于不需要修改参数的地方(const 引用)。
    1
    2
    3
    void print(const int& x) {
    std::cout << x << std::endl;
    }
  2. 返回值

    • 返回引用允许函数直接返回变量本身,而不是拷贝。
    1
    2
    3
    int& getValue(int& a) {
    return a;
    }
  3. 别名

    • 为变量创建更简化的别名。
    1
    2
    int a = 42;
    int& alias = a; // alias 是 a 的别名

(2) 适用指针

  1. 动态内存管理

    • 使用指针分配和释放动态内存。
    1
    2
    int* ptr = new int(10);
    delete ptr; // 手动释放内存
  2. 数组与迭代

    • 指针常用来访问数组元素。
    1
    2
    3
    4
    int arr[3] = {1, 2, 3};
    for (int* ptr = arr; ptr < arr + 3; ++ptr) {
    std::cout << *ptr << " ";
    }
  3. 数据结构

    • 指针是数据结构(如链表、树)的核心基础。
    1
    2
    3
    4
    struct Node {
    int data;
    Node* next;
    };
  4. 需要动态改变指向时

    • 指针可以灵活地改变指向,适合需要频繁切换指向的场景。

5. 总结对比

特征 引用 指针
需要初始化 必须初始化 可以先声明后初始化
是否可以为空 不可以为空 可以是空指针(nullptr
绑定是否可更改 绑定后不可更改 可通过重新赋值更改指向
语法复杂度 更简单 更复杂,需要使用 *
灵活性 受限 更灵活
常见场景 函数传参、别名 动态内存、复杂结构

引用更简单、更安全,适合大多数普通变量操作;指针更灵活,适合动态内存和复杂结构的场景。在实际开发中,应根据场景和需求选择适合的工具。

金山C++研发实习生面试准备

金山C++研发实习生面试准备

1.自我介绍
2.拷打项目

3.拷打八股:

4.多态的实现

5.静态多态和动态多态

静态多态是指在编译时多态,动态多态是指运行时多态

静态多态有函数重载(函数名相同,参数不同,返回值相同或不同),函数模板

动态多态(动态绑定):即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。

6.动态多态怎么实现的

动态多态

通过每个类的虚函数和动态绑定

派生类实现动态绑定

  • 先拷贝基类的虚函数表
  • 如果派生类重写了基类的某个虚函数,就用派生类的虚函数替换虚表同位置的基类虚函数
  • 跟上派生类自己的虚函数

指向基类的指针或引用

动态多态需要通过指向基类的 指针或引用 操作派生类对象。如果直接操作派生对象,则是静态绑定,而非动态绑定。

7.介绍一下虚函数表和虚指针

虚函数表和虚指针是由编译器生成并使用的,它们是实现 动态绑定 和运行时多态的核心。

虚函数表是一种由编译器生成的 隐藏数据结构,它是一个函数指针数组,用来存储类中所有虚函数的地址。简单来说,虚函数表就是一个指针数组,每个元素指向一个虚函数的具体实现。

虚指针是一个 隐藏成员变量,存在于每个包含虚函数的类的对象中。虚指针用于指向当前对象所属类的虚函数表。

8.说一下构造函数和析构函数在父类和子类的执行顺序

  • 构造函数顺序
    父类构造函数成员对象的构造函数子类构造函数
  • 析构函数顺序
    子类析构函数成员对象的析构函数父类析构函数

9.析构函数的作用

析构函数用于释放对象占用的资源(如内存、文件句柄等),在对象生命周期结束时自动调用,确保资源不会泄漏。

10.为什么析构函数通常定义为虚函数;不调用析构函数会怎样

  • 原因
    若父类指针指向子类对象,且父类析构函数非虚,则通过父类指针删除对象时,只会调用父类析构函数,导致子类资源泄漏。
  • 后果
    不调用析构函数会导致资源泄漏(如内存、文件句柄未释放)。

11.结构体为何通常要进行内存对齐;一般是几字节对齐

为了提高访问效率,为了时内存命中访问效率更高

对齐原则

结构体的内存对齐通常遵循以下原则:

  1. 每个成员变量的起始地址必须是其自身大小的整数倍
  2. 结构体本身的大小必须是其最大对齐系数(对齐要求)的整数倍

12.new/delete和malloc/free的区别

特性 new/delete malloc/free
语言特性支持 C++ 专有 C 风格(C 和 C++ 均支持)
类型安全 否(需要手动类型转换)
初始化支持 自动调用构造函数和析构函数 不调用,需要手动处理
异常处理 内存分配失败抛出 std::bad_alloc 内存分配失败返回 NULL
灵活性 适合对象管理和复杂场景 适合简单的POD或跨语言
效率 稍有额外开销(构造/析构函数调用) 相对高效

什么时候选择哪一个?

使用 new/delete

  • 在C++程序中,需要创建具有构造函数和析构函数的复杂对象。
  • 希望利用类型安全和异常处理机制,提高代码的健壮性。
  • 更偏向于面向对象的编程风格。

使用 malloc/free

  • 在C语言程序中,或者需要与 C 的库/代码接口。
  • 需要跨语言兼容时(如 C 和 C++ 混合开发)。
  • 只操作简单的内存(非对象)时,比如使用裸内存块或数组。

总结

  1. newdelete 是C++中的语法糖,功能更强大,更贴合现代编程需求。
  2. mallocfree 是底层内存管理工具,适合裸内存分配。
  3. 在C++开发中,通常推荐优先使用 newdelete,除非有特殊需要才选择 mallocfree

13.什么情况用mmap()

mmap() 是一个 POSIX 标准的内存映射函数,通常用于将文件或设备映射到进程虚拟内存空间中,使得我们可以通过内存访问文件内容,大大提高了 I/O 的效率。它的使用场景主要集中在对文件的高效访问、大文件处理、共享内存以及特殊的内存管理需求等。以下是 mmap() 的典型使用情况、优缺点以及示例解释。

1.高效文件访问,减少内核态和用户态之间的拷贝开销

2.处理大文件,将文件内容按需映射到虚拟内存中

3.共享内存(进程间通信)

  • mmap() 支持使用 MAP_SHARED 标志创建共享内存,多个进程可以操作同一内存区域进行通信。
  • 使用共享内存比使用消息队列、管道等传统 IPC 机制有更高的效率。

4.文件修改或写入(内存作为缓冲区)适用于需要高效更新大文件的场景

5.实现内存映射I/O

6.异步I/O和并行处理

使用 mmap() 的场景主要是权衡性能与开销,以下是具体选择标准:

  1. 适合用 mmap() 的场景
    • 需要频繁随机访问大文件。
    • 文件的内容按需加载,不需要一次性加载到内存。
    • 多进程需要共享文件或内存。
    • 操作设备文件或硬件寄存器。
    • 文件访问频繁,且希望减少用户态与内核态之间频繁的数据拷贝。
  2. 不太适合用 mmap() 的场景
    • 操作小文件,或者一次性读取整个文件,不需要复杂的随机访问。
    • 文件处理逻辑非常简单,而 mmap() 的复用性和管理开销反而适得其反。
    • 跨平台兼容需求非常高的场景。

14.动态链接库用的brk()还是mmap()

特性 brk() mmap()
内存范围 紧跟数据段末尾 任意空闲的虚拟地址空间
按需加载 不支持 支持
分页分配 不支持 支持,以页为单位分配和管理
碎片问题 容易引发堆碎片 避免堆碎片,内存管理灵活
共享内存 不支持 支持共享内存(MAP_SHARED 标志)
动态链接库加载 不适用 是动态链接库的默认选择

因此,动态链接库的加载和运行使用 mmap() 是最佳选择,它为动态链接库的按需加载、多进程共享、独立管理以及性能优化提供了完美支持。同时,brk() 由于其限制,已不适用于动态库加载。

15.动态链接和静态链接的区别

特性 静态链接 动态链接
生成的可执行文件体积 较大,库代码直接嵌入 较小,仅包含对动态库的引用
运行时依赖 不依赖外部库,所有代码都在一个文件中 依赖外部库,运行时需要动态加载
运行时性能 加载速度快,无需加载外部库 加载速度稍慢,需要动态加载符号解析
内存使用 每个程序独立占用一份库的内存 多个程序共享同一个库,节省内存
更新方式 更新程序需要重新编译并链接静态库 可以通过更新库文件,无需重新编译程序
跨平台兼容性 静态库只与编译平台相关,适合封装依赖 动态库可能与运行时环境、平台设置相关
调试难度 相对简单,因为库已集成进可执行文件 调试更复杂,可能需要检查多个动态库
符号冲突问题 不会出现符号冲突,代码已固定 有可能发生符号冲突,需要小心处理
磁盘存储 每个可执行文件都包含完整的库代码 动态库存储独立,节省磁盘空间
维护与分发 更新程序和库需要重新分发完整的可执行文件 库文件可以单独更新,减少分发工作量

16.红黑树特性;有哪些应用

  • 特性
    1. 每个节点是红或黑。
    2. 根节点和叶子节点(NIL)是黑。
    3. 红节点的子节点必为黑。
    4. 从任一节点到其叶子节点的路径包含相同数量的黑节点。
  • 应用
    C++ STL map/set、Java TreeMap、Linux 内核调度器等。

17.死锁的必要条件有哪些

  1. 互斥:资源只能被一个进程占用。
  2. 持有并等待:进程持有资源并等待其他资源。
  3. 不可抢占:资源只能由持有者主动释放。
  4. 循环等待:存在进程资源的环形等待链。

18.线程安全实现方式

  1. 互斥锁(Mutex):保证临界区代码原子性。
  2. 原子操作(Atomic):通过硬件支持的原子指令(如 CAS)。
  3. 无锁编程:如使用无锁数据结构。
  4. 线程局部存储(TLS):避免共享数据。
  5. 读写锁(Read-Write Lock):区分读/写操作。
  6. 条件变量(Condition Variable):线程间同步。

docker上传文件至容器

docker上传文件至容器

拿到容器ID

1
docker ps -a

将本地文件上传到容器的指定目录中

1
docker cp 本地文件路径 ID全称:容器路径
1
docker cp /home/stydent/EdgeGPT /usr/local/lib/python3.11/site-packages
1
sudo docker cp fbf56805d23a:/usr/local/lib/python3.11/site-packages ./site-packages

docker遇到的问题

拉取镜像失败是网络问题,先看网络

docker命令报错error during connect: Get http://2F2F.2Fpipe2Fdocker_engine/v1.36/containers/json: open//.

在清理docker时遇到

1
docker system prune

原因是docker无法挂载到wsl上

因为刚刚执行压缩命令占用了vhdx文件,不能把docker文件关联上

关机重启就好

1
2
3
4
5
6
7
8
9
 docker system prune
WARNING! This will remove:
- all stopped containers
- all networks not used by at least one container
- all dangling images
- unused build cache

Are you sure you want to continue? [y/N] y
Total reclaimed space: 0B