后端开发面经系列 -- 美团C++后端开发一面

美团C++后端开发一面

公众号:阿Q技术站

八股

1、指针和引用的区别,常引用了解吗,简单介绍下?

  1. 指针(Pointer):
    • 指针是一个变量,其值为另一个变量的地址。
    • 通过指针,我们可以直接访问和操作内存中的数据。
    • 指针使用解引用操作符*来访问其指向的变量的内容。
    • 指针在内存管理和数据结构中非常有用,但需要小心使用,以避免出现错误和安全问题。
  2. 引用(Reference):
    • 引用是变量的别名,它提供了一个已存在变量的新名称。
    • 引用在创建时必须初始化,并且在其生命周期内不能改变绑定的对象。
    • 引用在语法上类似于指针,但在语义上更接近于常规变量。
    • 引用通常用于函数参数传递和返回值,可以避免指针可能引发的一些问题,比如空指针异常。
  3. 常量引用(Const Reference):
    • 常量引用是指被声明为常量的引用。
    • 常量引用的主要特点是不能通过该引用修改其引用的值,但可以通过其他途径修改。
    • 常量引用通常用于传递函数参数,以确保被引用的值不会被意外修改。

2、说下多态,多态的作用和使用场景?

  1. 多态的作用:
    • 简化代码:通过多态,可以使用统一的接口来处理不同类的对象,从而简化了代码的逻辑。
    • 提高灵活性:可以在不改变代码结构的情况下,轻松地添加新的类和方法。
    • 降低耦合度:不同类之间通过接口进行交互,而不是直接依赖于具体的实现,从而降低了类之间的耦合度。
    • 可扩展性:在需要添加新的功能时,可以通过扩展现有的类和接口来实现,而不需要修改现有的代码。
  2. 多态的使用场景:
    • 继承关系中:多态通常与继承结合使用。通过继承,子类可以重写父类的方法,并且可以根据实际对象的类型调用相应的方法。
    • 方法重写(Override):子类可以重写父类的方法,当调用这些方法时,根据实际对象的类型会调用相应的重写方法。
    • 抽象类和接口:抽象类和接口定义了统一的方法接口,具体的子类可以根据需要实现这些方法,从而实现多态。

示例(C++):

#include <iostream>
using namespace std;

// 基类
class Animal {
public:
    virtual void makeSound() {
        cout << "Animal makes a sound" << endl;
    }
};

// 派生类
class Dog : public Animal {
public:
    void makeSound() override {
        cout << "Dog barks" << endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() override {
        cout << "Cat meows" << endl;
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->makeSound();  // 输出:Dog barks
    animal2->makeSound();  // 输出:Cat meows

    delete animal1;
    delete animal2;

    return 0;
}

3、构造函数和析构函数可以是虚函数吗?

  1. 虚构造函数:
    • C++中并没有虚构造函数的概念,因为构造函数在对象创建时被调用,但在对象构造之前虚函数表还没有建立,因此构造函数不能是虚函数。换句话说,在构造函数中使用虚函数是没有意义的,因为在构造函数执行期间,对象的类型是不完整的,虚函数机制无法正常工作。
  2. 虚析构函数:
    • 虚析构函数是允许的,它允许在继承关系中正确地释放资源。如果一个类可能会被其他类继承,且通过基类指针删除派生类对象时,通常应该将析构函数声明为虚函数,以确保正确地调用派生类的析构函数。
    • 虚析构函数应该声明为virtual并且在基类中实现。这样,当通过基类指针删除派生类对象时,会首先调用派生类的析构函数,然后再调用基类的析构函数,确保资源得到正确释放。

示例(C++):

class Base {
public:
    virtual ~Base() { // 虚析构函数
        cout << "Base destructor" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() override { // 虚析构函数的重写
        cout << "Derived destructor" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr; // 输出:Derived destructor Base destructor
    return 0;
}

4、C++中内存区域分布是怎样的?

  1. 栈(Stack):

    • 栈是由编译器自动管理的,用于存储局部变量、函数参数、函数返回地址等。栈内存的分配和释放是自动进行的,当函数调用结束时,其在栈上分配的内存会自动被释放。
    • 栈内存的特点是大小固定,且分配释放速度快,但是生命周期短暂,不能用于存储动态分配的内存。
  2. 堆(Heap):

    • 堆是由程序员手动管理的,用于存储动态分配的内存。堆内存的分配和释放需要程序员显式地调用相关函数(如newdelete)来进行操作。
    • 堆内存的特点是大小不固定,分配释放速度较慢,但是生命周期可以很长,适合存储动态数据结构(如链表、树等)。
  3. 全局/静态存储区:

    • 全局变量和静态变量存储在这个区域中,它们在程序运行期间始终存在,直到程序结束才会被销毁。
    • 全局变量存储在程序的全局作用域中,静态变量可以在函数内部定义,也可以在全局作用域中定义,但是它们都具有静态生存期。
  4. 常量存储区:

    常量数据存储在这个区域中,包括字符串常量和其他类型的常量。这些常量在程序运行期间不可修改。

  5. 代码区:

    • 代码区存储程序的机器代码,包括所有的可执行代码和只读数据(如常量字符串)。
    • 代码区通常是只读的,不允许写入操作,用于存储程序的执行指令。

5、拷贝构造函数介绍下,如何用?

拷贝构造函数是在C++中用于创建一个对象的副本的特殊构造函数。当使用一个对象来初始化同类的另一个对象时,拷贝构造函数会被调用。拷贝构造函数通常有以下两种形式:

  1. 默认拷贝构造函数:

    如果你没有显式地定义一个类的拷贝构造函数,编译器会为你生成一个默认的拷贝构造函数。默认的拷贝构造函数会按照成员变量的顺序逐个进行拷贝构造,对于指针类型的成员变量,会进行浅拷贝(复制指针本身,而不是指针指向的对象)。

  2. 自定义拷贝构造函数:

    如果你需要实现自定义的拷贝行为,可以显式地定义一个拷贝构造函数。自定义的拷贝构造函数通常以引用方式接受一个同类对象作为参数,并且通常需要在函数体内实现对成员变量的深度拷贝(复制指针指向的对象)。

拷贝构造函数的用法:

  • 当你需要将一个对象的值传递给另一个对象时,会调用拷贝构造函数。例如,通过值传递方式传递对象给函数时,或者在函数中返回对象时,都会调用拷贝构造函数。
  • 当你需要创建一个对象的副本时,也会调用拷贝构造函数。例如,通过一个对象初始化另一个对象,或者将一个对象作为另一个对象的参数传递给构造函数时,都会调用拷贝构造函数。

示例(C++):

#include <iostream>
using namespace std;

class MyString {
private:
    char* buffer;

public:
    // 自定义拷贝构造函数
    MyString(const MyString& other) {
        // 深度拷贝 buffer
        int length = strlen(other.buffer);
        this->buffer = new char[length + 1];
        strcpy(this->buffer, other.buffer);
        cout << "Custom copy constructor called" << endl;
    }

    // 构造函数
    MyString(const char* input) {
        int length = strlen(input);
        this->buffer = new char[length + 1];
        strcpy(this->buffer, input);
    }

    // 析构函数
    ~MyString() {
        delete[] this->buffer;
    }

    // 输出字符串
    void display() {
        cout << buffer << endl;
    }
};

int main() {
    MyString str1("Hello");
    MyString str2 = str1;  // 调用拷贝构造函数
    str2.display();  // 输出:Hello

    return 0;
}

6、浅拷贝和深拷贝区别?

  1. 浅拷贝:
    • 浅拷贝是指将一个对象的成员变量逐个复制到另一个对象中。对于基本数据类型的成员变量,会直接进行值复制;对于指针类型的成员变量,只会复制指针的值,而不会复制指针所指向的对象。
    • 浅拷贝只复制了对象的表面结构,而没有复制指针所指向的动态分配的内存,因此两个对象共享同一块内存,容易造成悬空指针和内存泄漏等问题。
  2. 深拷贝:
    • 深拷贝是指将一个对象的所有成员变量以及指针所指向的对象都复制到另一个对象中。这意味着会递归地复制指针所指向的对象,而不是简单地复制指针的值。
    • 深拷贝会创建一个新的对象及其拷贝,两个对象各自拥有独立的内存空间,不会相互影响。因此,深拷贝通常需要对指针所指向的对象进行动态内存分配,并在拷贝时进行相应的内存管理。

区别总结:

  • 浅拷贝只复制对象的表面结构,对于指针成员变量只复制指针的值,不复制指针所指向的对象;深拷贝会递归地复制对象的所有内容,包括指针所指向的对象。
  • 浅拷贝容易造成对象间的数据共享和悬空指针问题;深拷贝能够完整地复制对象及其所有内容,不会出现共享和悬空指针问题。

示例:

#include <iostream>
#include <cstring>
using namespace std;

class DeepCopyExample {
private:
    char* buffer;

public:
    // 深拷贝构造函数
    DeepCopyExample(const DeepCopyExample& other) {
        int length = strlen(other.buffer);
        this->buffer = new char[length + 1];
        strcpy(this->buffer, other.buffer);
    }

    // 析构函数
    ~DeepCopyExample() {
        delete[] this->buffer;
    }

    // 输出字符串
    void display() {
        cout << buffer << endl;
    }

    // 设置字符串
    void setBuffer(const char* input) {
        int length = strlen(input);
        this->buffer = new char[length + 1];
        strcpy(this->buffer, input);
    }
};

int main() {
    DeepCopyExample obj1;
    obj1.setBuffer("Hello");

    DeepCopyExample obj2 = obj1;  // 深拷贝
    obj1.display();  // 输出:Hello
    obj2.display();  // 输出:Hello

    obj1.setBuffer("World");  // 修改 obj1 的 buffer
    obj1.display();  // 输出:World
    obj2.display();  // 输出:Hello(obj2 的 buffer 不受 obj1 的影响)

    return 0;
}

7、虚拟地址了解吗?

虚拟地址是指在计算机系统中由操作系统管理的一种抽象地址空间,它使得每个进程都拥有独立的地址空间,从而实现了进程间的隔离和保护。虚拟地址与物理地址相对应,物理地址是指内存中实际的存储位置,而虚拟地址是进程在访问内存时使用的地址。

虚拟地址的特点和作用如下:

  1. 隔离和保护:

    • 每个进程都拥有独立的虚拟地址空间,使得不同进程的内存访问彼此隔离,互不干扰。
    • 操作系统通过虚拟地址空间的管理,实现了对进程的保护,防止进程越界访问其他进程的内存空间或者操作系统内核的关键数据结构。
  2. 地址映射:

    • 虚拟地址需要通过地址映射机制才能转换为物理地址,这个过程由硬件的内存管理单元(MMU)和操作系统共同完成。
    • 操作系统会维护一个虚拟地址到物理地址的映射关系,通过这个映射关系,操作系统可以将进程的虚拟地址映射到实际的物理内存地址。
  3. 虚拟内存:

    • 虚拟地址的使用使得操作系统可以实现虚拟内存技术,这是一种将部分数据存储在磁盘上的技术,可以扩展系统的可用内存空间。
    • 当进程访问的数据不在物理内存中时,操作系统会将需要的数据从磁盘加载到内存,并更新虚拟地址到物理地址的映射关系。
  4. 虚拟地址空间布局:

    虚拟地址空间通常被划分为多个区域,包括代码区、数据区、堆区、栈区等。不同区域有不同的用途和访问权限。

8、虚拟内存作用(进程隔离,内存连续,mmap),优势和缺点?

  1. 作用:
    • 进程隔离:每个进程拥有独立的虚拟地址空间,使得不同进程的内存访问彼此隔离,互不干扰。
    • 内存连续性:虚拟内存可以使得物理内存的分配更加灵活,不需要连续的物理内存空间,从而解决了内存碎片问题。
    • 内存映射文件(mmap):虚拟内存可以将文件映射到进程的地址空间,从而实现文件的读写操作,这种技术常用于文件映射到内存进行快速访问,如内存映射文件、共享内存等。
  2. 优势:
    • 提高内存利用率:虚拟内存使得操作系统可以将实际内存和硬盘空间结合起来使用,从而提高了内存的利用率。
    • 简化内存管理:虚拟内存通过地址映射机制,使得操作系统可以更加灵活地管理内存,简化了内存管理的复杂度。
    • 提供了更大的地址空间:虚拟内存使得每个进程可以拥有更大的地址空间,从而支持更大的程序和数据。
  3. 缺点:
    • 性能开销:虚拟内存需要硬盘等外部存储设备作为支撑,因此在访问速度上会有一定的性能开销。
    • 额外的复杂性:虚拟内存增加了操作系统的复杂性,包括地址映射、页表管理、页面置换等方面的实现。
    • 容易产生页错误:由于虚拟内存需要将部分数据存储在硬盘上,因此在访问时可能会产生页错误,需要操作系统进行页面置换操作,影响了访问速度。

9、七层网络模型,每层的作用?

  1. 物理层:
    • 物理层负责在物理媒体上传输原始比特流,主要关注的是传输媒体、数据传输速率、接口标准等物理细节。
    • 物理层的作用是将比特流转换为电信号、光信号或者其他物理形式的信号,并负责在网络设备之间进行数据的传输。
  2. 数据链路层:
    • 数据链路层负责通过物理链路传输数据帧,处理帧的开始和结束标记,以及错误检测和纠正。
    • 数据链路层的作用是确保相邻节点之间的可靠数据传输,通过帧的确认和重传机制来实现数据的可靠性。
  3. 网络层:
    • 网络层负责在多个网络之间进行路由选择和数据转发,实现了不同网络之间的通信。
    • 网络层的作用是实现端到端的数据传输,通过路由选择算法确定数据传输的路径,并负责将数据包从源主机传输到目标主机。
  4. 传输层:
    • 传输层负责端到端的数据传输,提供了数据传输的可靠性和顺序性,同时也负责流量控制和拥塞控制。
    • 传输层的作用是建立端到端的连接,并且确保数据的可靠传输,同时通过流量控制和拥塞控制来管理网络的流量。
  5. 会话层:
    • 会话层负责建立、管理和终止会话连接,提供了数据交换的逻辑通路。
    • 会话层的作用是在通信节点之间建立会话连接,并负责会话的管理和维护,以实现数据的可靠传输和有效管理。
  6. 表示层:
    • 表示层负责数据的格式转换、加密和解密、数据压缩和解压缩等功能,以便于不同系统之间的数据交换。
    • 表示层的作用是将应用层的数据格式转换为网络能够识别和传输的格式,同时也负责数据的安全性和可靠性。
  7. 应用层:
    • 应用层是最高层,负责为用户提供网络服务和接口,包括各种网络应用程序和协议。
    • 应用层的作用是为用户提供各种网络服务,如电子邮件、文件传输、远程登录等,同时也负责处理应用层协议的交互和管理。

10、TCP三次握手四次挥手的具体过程,为什么不能是三次挥手?

三次握手:

  1. 第一步(SYN):

    客户端发送一个带有 SYN(同步)标志的数据包给服务器,并进入 SYN_SENT 状态,表示客户端请求建立连接。

  2. 第二步(SYN + ACK):

    服务器收到 SYN 数据包后,如果同意建立连接,则会发送一个带有 SYN/ACK 标志的数据包给客户端,并进入 SYN_RCVD 状态。

  3. 第三步(ACK):

    客户端收到服务器的 SYN/ACK 数据包后,会发送一个带有 ACK 标志的数据包给服务器,表示确认连接建立,双方可以开始通信。

四次挥手:

  1. 第一步(FIN):

    客户端发送一个带有 FIN(结束)标志的数据包给服务器,表示客户端希望关闭连接,进入 FIN_WAIT_1 状态。

  2. 第二步(ACK):

    服务器收到客户端的 FIN 数据包后,发送一个带有 ACK 标志的数据包给客户端,表示收到了关闭请求,但仍然允许数据传输,服务器进入 CLOSE_WAIT 状态,客户端进入 FIN_WAIT_2 状态。

  3. 第三步(FIN):

    服务器完成数据传输后,发送一个带有 FIN 标志的数据包给客户端,表示服务器也准备关闭连接,服务器进入 LAST_ACK 状态。

  4. 第四步(ACK):

    客户端收到服务器的 FIN 数据包后,发送一个带有 ACK 标志的数据包给服务器,表示确认收到了关闭请求,客户端进入 TIME_WAIT 状态。

为什么不能是三次挥手?

三次握手建立连接时,客户端向服务器发送 SYN 数据包,服务器收到后回复 SYN/ACK 数据包,客户端再回复一个 ACK 数据包,这样双方就确认了连接的建立。但是在关闭连接时,为了保证数据的可靠传输,需要在双方都确认关闭之后才能断开连接。如果使用三次挥手,可能会出现以下问题:

  • 客户端发送 FIN 数据包后,服务器收到并发送 ACK 数据包,表示接受了关闭请求,但此时服务器可能还有数据需要传输。
  • 如果服务器在发送 ACK 数据包后立即关闭连接,客户端可能会因为网络延迟而没有收到服务器的所有数据。
  • 如果服务器在发送 ACK 数据包后等待一段时间再关闭连接,客户端可能会在此期间再次发送数据,导致服务器无法正确处理。

11、UDP和TCP的区别?

  1. 连接性:
    • TCP是一种面向连接的协议,通过三次握手建立连接,并且在传输数据时保证数据的可靠性和顺序性。
    • UDP是一种无连接的协议,不需要建立连接,也不会保证数据的可靠性和顺序性,数据包的发送和接收是独立的。
  2. 数据可靠性:
    • TCP保证数据的可靠性,通过序号、确认和重传机制来确保数据的正确传输和顺序到达,适用于需要可靠传输的应用。
    • UDP不保证数据的可靠性,数据包发送后不会进行确认和重传,适用于实时性要求高、数据丢失可以容忍的应用。
  3. 传输效率:
    • TCP的传输效率相对较低,因为它需要进行连接的建立、数据的确认和重传等操作,这些都会增加传输的延迟和开销。
    • UDP的传输效率相对较高,因为它不需要进行连接的建立和数据的确认,数据包的发送和接收是独立的,适用于实时性要求高的应用。
  4. 应用场景:
    • TCP适用于对数据可靠性要求较高的应用,如文件传输、网页访问等,这些应用需要保证数据的完整性和正确性。
    • UDP适用于对实时性要求较高的应用,如视频直播、音频通话等,这些应用对数据的实时性要求高,可以容忍一定的数据丢失。

12、进程和线程的区别?

  1. 定义:
    • 进程是程序在执行过程中的一个实例,它包含了程序的代码、数据和执行环境等资源,是操作系统进行资源分配和调度的基本单位。
    • 线程是进程内的一个独立执行流,是操作系统进行调度的基本单位,一个进程可以包含多个线程,它们共享进程的资源。
  2. 资源拥有:
    • 进程拥有独立的地址空间、文件描述符、信号处理器、优先级等资源,各个进程之间的资源是相互独立的。
    • 线程共享所属进程的地址空间和其他资源,如打开的文件、信号处理器等,线程之间可以方便地共享数据和通信。
  3. 调度和切换开销:
    • 进程的切换开销较大,因为它涉及到地址空间的切换、文件描述符的重定向等操作。
    • 线程的切换开销相对较小,因为它们共享了进程的地址空间等资源,切换时只需要保存和恢复线程的上下文即可。
  4. 并发性:
    • 进程是操作系统进行资源分配和调度的基本单位,不同进程之间的执行是并发的,它们各自拥有独立的地址空间和资源。
    • 线程是进程内的执行流,不同线程之间的执行是并发的,它们共享了进程的资源,可以方便地进行通信和数据共享。
  5. 创建和销毁开销:
    • 创建和销毁进程的开销较大,涉及到内存分配、资源初始化和清理等操作。
    • 创建和销毁线程的开销较小,因为它们共享了进程的资源,不需要额外的内存分配和资源初始化。

13、网页请求中get和post区别?

  1. 请求方式:
    • GET:通常用于获取数据,通过 URL 提交数据,数据在 URL 中可见,以键值对的形式出现在 URL 的查询字符串中,如 http://example.com/resource?param1=value1&param2=value2
    • POST:通常用于提交数据,数据在请求体中,不会显示在 URL 中,适用于提交表单数据等需要保密的情况。
  2. 数据传输:
    • GET:数据以查询字符串的形式附加在 URL 后面,长度有限制,一般不适用于传输大量数据。
    • POST:数据放在请求体中,没有长度限制,适用于传输大量数据。
  3. 安全性:
    • GET:由于数据在 URL 中可见,不适合传输敏感信息,如密码等。
    • POST:数据在请求体中,相对于 GET 更安全,适合传输敏感信息。
  4. 幂等性:
    • GET:幂等,多次请求相同的 URL,结果应该是一样的。
    • POST:不幂等,多次提交相同的数据,可能会产生不同的结果。
  5. 使用场景:
    • GET:适用于获取资源,如查看网页、下载文件等。
    • POST:适用于提交数据,如提交表单、上传文件等。

算法题

1、给n个区间(左闭右开,n <= 1e5),值域在1e9内,现在所有区间会合并,输出合并后的结果?

如有3个区间[1,3),[2,4),[4,6] => [1,4),[4,6]

LeetCode 56题

思路:

  1. 排序区间列表:

    首先,需要对给定的区间列表按照起点进行排序,以便后续合并操作。

  2. 初始化结果数组:

    初始化一个空的结果数组merged,用于存储合并后的区间。

  3. 依次合并区间:

    • 接下来,我们从第二个区间开始,依次与merged中的最后一个区间进行比较。
    • 如果当前区间的起点在merged中最后一个区间的终点之后(不相交),则将当前区间加入merged中。
    • 如果当前区间的起点在merged中最后一个区间的终点之前(相交),则更新merged中最后一个区间的终点为两者的最大值。
  4. 返回合并后的结果:

    最后,返回合并后的结果数组merged即可。

参考代码:

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:
    // 定义区间类型
    typedef vector<int> Interval;

    // 比较函数,用于区间按照起点排序
    static bool compareInterval(const Interval &a, const Interval &b) {
        return a[0] < b[0];
    }

    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        // 对区间列表按照起点进行排序
        sort(intervals.begin(), intervals.end(), compareInterval);

        // 初始化结果数组
        vector<vector<int>> merged;
        merged.push_back(intervals[0]);

        // 依次合并区间
        for (int i = 1; i < intervals.size(); i++) {
            if (intervals[i][0] > merged.back()[1]) {
                // 不相交,直接加入
                merged.push_back(intervals[i]);
            } else {
                // 相交,更新终点
                merged.back()[1] = max(merged.back()[1], intervals[i][1]);
            }
        }

        // 返回合并后的结果
        return merged;
    }
};

int main() {
    // 测试示例
    vector<vector<int>> intervals = {{1, 3}, {2, 6}, {8, 10}, {15, 18}};
    Solution sol;
    vector<vector<int>> result = sol.merge(intervals);

    // 输出合并后的结果
    for (auto interval : result) {
        cout << "[" << interval[0] << ", " << interval[1] << "] ";
    }
    cout << endl;

    return 0;
}
  • 33
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值