常见面试题目

常见面试题目

static 在C语言中的作用?什么是静态变量?存储在什么地方?有什么特性?

作用:

  1. 使局部变量持久化,生命周期持续到程序结束;
  2. 限制全局变量和函数的作用域在文件内部,使其不可被其他文件访问;
  3. 存储在数据段中(既不是堆也不是栈,堆用于动态分配的内存,栈用于局部变量)

静态局部变量:保存整个程序执行期间内的值;虽然函数被多次调用,但是其值在多次调用函数时都不会丢失;在程序执行时,只初始化一次,如果没有明确初始化,默认会初始化为0;多次调用函数,并不会初始化多次;

静态全局变量:限制变量的作用域在本文件内,对该文件的所有函数都看见,但是不能被其他文件引用。

静态函数:规定一个函数只能在一个文件中使用,不能被其他文件引用。可以防止项目模块间命名冲突的问题;(和静态全局变量类似,都是降低作用范围)


局部变量、全局变量等的内存位置。

程序将内存划分为不同的区域:

  1. 代码段:保存静态代码(包括编译之后的机器码),可以被多个进程复用;
  2. 数据段:存储全局变量和静态变量,.data段存放已经初始化的全局和静态变量,.bss段存放未初始化的全局和静态变量;
  3. 堆:用于动态分配的内存(malloc等),需要明确释放(free
  4. 栈:函数调用、存储局部变量、返回地址、传递给函数的参数、上下文信息等;

局部变量存储在栈,全局变量存储在堆;


源代码文件到可执行文件发生了什么?GCC起什么作用?

步骤:

  1. 预处理:将预处理器处理预处理指令(比如#define#include#if#ifdef等)
  2. 编译:进行词法和语法分析,生成抽象语法树,然后优化,生成对应的汇编代码;
  3. 汇编:将汇编代码转换为二进制的机器码;
  4. 链接:将多个目标文件和库文件连接成一个完整的可执行文件;并堆不同目标文件中的符号(变量和函数)进行分配地址,并解释所有符号的引用关系;

GCC编译器负责在这些阶段中执行编译任务;GCC将这些步骤组合起来,帮助我们只用一条简单的GCC组合编译语句就实现了整个过程;GCC就像工厂,各个步骤就像车间,我们将代码给GCC,GCC将代码分配到各个车间,然后将可执行文件返回;


排序算法你知道哪些?讲一讲各种排序算法的实现思路。快速排序怎么实现?各种排序算法的复杂度比较。

  1. 冒泡排序(Bubble Sort):通过比较相邻的元素并交换它们的位置,每一轮将最大的元素移动到最后。重复这个过程直到整个数组排序完成。平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  2. 插入排序(Insertion Sort):将数组分为有序和无序两部分,每次从无序部分取出一个元素,插入到有序部分的正确位置。平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  3. 选择排序(Selection Sort):每次从未排序的部分中找到最小(或最大)的元素,将其放到已排序部分的末尾。平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  4. **快速排序(Quick Sort):选择一个基准元素,将小于基准的元素放在左边,大于基准的元素放在右边,然后递归地对左右子数组进行排序。**平均时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),最坏情况下为 O ( n 2 ) O(n^2) O(n2)
  5. 归并排序(Merge Sort):将数组分成两个子数组,递归地对每个子数组进行排序,然后将两个有序的子数组合并成一个有序数组。平均时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
  6. 堆排序(Heap Sort):将数组构建为一个二叉堆,然后反复从堆顶取出最大(或最小)元素,并将其放入已排序部分的末尾。平均时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
  7. 希尔排序(Shell Sort):将数组分成多个子序列并进行插入排序,然后逐步缩小子序列的间隔,最终完成排序。(分区间快排)
  8. 计数排序(Counting Sort):统计数组中每个元素出现的次数,然后根据统计结果重构有序数组。
  9. 桶排序(Bucket Sort):将元素分配到不同的桶中,每个桶内部再使用其他排序算法(如插入排序)进行排序,然后按照桶的顺序合并桶内的元素。
  10. 基数排序(Radix Sort):按照元素的位数进行排序,先按照最低位进行排序,然后依次按照更高位进行排序,直到最高位。

IO多路复用中的select、epoll的区别。

select:遍历文件描述符、文件描述符上限1024,适用于文件描述符数量少的情况;

epoll:无需遍历或者轮询文件描述符,内核支持的事件通知机制;文件描述符没有数量上限,并且返回就绪文件描述符列表,无需遍历所有文件描述符;、epoll 会将就绪文件描述符加入链表等待。利用 epoll_wait(),直接获取就绪的文件描述符集,不必遍历整个集合。适用于文件描述符数量多,并且活跃事件占比少的情况;


C++的三大特性:封装、继承、多态

  1. 封装(Encapsulation):将数据和操作数据的函数封装成一个独立的类。封装通过数据隐藏(成员变量通常声明为私有)和提供公有接口来实现。这样做的目的是为了限制对类成员的直接访问,保护内部数据的完整性
  2. 继承(Inheritance):通过继承,可以创建一个新类,并发挥已有类的功能(基类)。新类(派生类)会复制基类的方法和属性,并且可以添加自己的方法和属性。继承使代码重用成为可能,同时支持多态性,让基类指针或引用可以指向派生类对象
  3. 多态性(Polymorphism):多态性允许一种接口有不同实现。举例来说,基类指针或引用可以指向不同派生类对象,并且在调用方法时表现出不同个性。可以通过重载或重写(覆盖)实现多态。重写通常需要基类函数标记为 virtual

虚函数,纯虚函数和抽象类

虚函数(Virtual Function):带有 virtual (/ˈvɜːtʃuəl/)关键字的成员函数称为虚函数。虚函数支持运行时多态。基类的函数被声明为虚函数时,可以在派生类中重写该函数。这时通过基类指针或引用来调用时,依然会调用派生类中的实现。如:

class Base {
public:
    virtual void func() { std::cout << "Base func\n"; }
};

class Derived : public Base {
public:
    void func() override { std::cout << "Derived func\n"; }
};

纯虚函数(Pure Virtual Function):纯虚函数是虚函数的一种,它没有实现,只提供接口。本质上是为派生类定义一个通用的接口,让派生类提供具体实现。在基类中使用 = 0 描述。例如:

class Shape {
public:
    virtual void draw() const = 0; // 纯虚函数
};

抽象类是有一个或多个纯虚函数的类,不能直接实例化对象。派生类需要实现这些纯虚函数才能成为完整的类并生成对象。

class Shape {
public:
    virtual void draw() const = 0;
};

class Circle : public Shape {
public:
    void draw() const override { std::cout << "Drawing Circle\n"; }
};

虚函数表,虚函数指针

“虚函数表”(Virtual Table)和“虚函数指针”(Virtual Pointer,通常简称为vptr)是用于实现动态多态性(也称为运行时多态性)的一种机制。

虚函数表是一种由编译器在程序编译时生成的内部数据结构。它是一个指针数组,其中每个指针都指向对应类的某个虚函数的实际实现代码。

每个存在虚函数的类(包括基类和派生类)都有自己的虚函数表。虚函数表包含该类所有虚函数的指针列表,它们指向最适合当前类的(自身定义的或继承自父类的)虚函数的实现。

虚函数表使得编译器能够在运行时通过动态绑定来确定需要调用哪个派生类的虚函数,从而实现多态性。

虚函数指针是一个指针,指向类的虚函数表。编译器为每个使用虚函数的对象(类实例)插入一个这类指针。通过vptr,函数调用者可以在运行时找到对象对应的虚函数表,从而根据实际类型的虚函数表查找到对象实际实现的虚函数,这种查找是通过表查询完成的。

工作机制:

当创建一个对象时,如果它的类包含虚函数,编译器会为对象插入一个vptr,并将其指向相应类的vtable。当通过基类的指针或引用调用虚函数时,编译器根据该对象的vptr查询到实际要调用的函数实现。这种通过对象的vptr查询vtable来调用函数的方法使得编译器能够在运行时根据实际类确定要执行的函数。


重载和重写(覆盖)

函数重载(Function Overloading):在同一作用域内有两个或多个名称相同但参数不同的函数称为函数重载。编译器会根据参数的数量和类型选择适当的函数。

void func(int x);
void func(double y);
void func(int x, double y);

数重写(函数覆盖):子类对基类的虚函数进行实现。两者的函数签名必须相同,子类的实现会覆盖基类的实现。这是运行时多态的基础。需标记基类函数为 virtual,在子类中用 override 标记重写实现。

class Base {
public:
    virtual void func() { std::cout << "Base func\n"; }
};

class Derived : public Base {
public:
    void func() override { std::cout << "Derived func\n"; }
};

SOLID原则、讲一下里氏替换原则

“子类型必须能够替换其基类型”。 换句话说,代码中的基类对象可以无缝地替换为子类对象,但不会改变程序的正确行为。

遵循 LSP,在继承关系中:

  1. 子类应该实现父类的所有接口,包括方法和属性。
  2. 子类可以扩展父类,但不能修改父类的功能。具体来说,子类中的覆盖方法应该与它们所覆盖的方法具有相同的语义。
  3. 子类不应该引入超出父类所假设的约束、前提。

STL中的容器:vector和list的区别,map的底层以及时间复杂度,map和set的区别。

vector和list的区别

vector的底层实现是数组,list的底层实现是链表;在元素插入、删除、获取元素效率方面二者区别明显;

元素插入:vector插入一个元素时,插入位置后面的元素都要向后移动一个单位,效率很低,平均时间复杂度为O(n);list插入一个元素,只需要改变被插入元素前的指针和插入元素的指针,时间复杂度为O(1);

元素删除:vector删除元素同样需要移动大量元素效率很低,而list无需移动大量元素,效率很高;

获取元素:vector可以按秩访问,即按照数组下标访问元素,可以很方便获取到某个元素,时间复杂度为O(1);但是list需要从头开始遍历才能访问到目标元素,所以平均时间复杂度为O(n);

对于动态操作多的情况(插入、删除),使用链表;对于静态操作多的情况(查找)使用vector;

map的底层和时间复杂度

map并不是容器,而是关联容器,用来存储键值对,其底层基于平衡二叉树(一般是红黑树),使得他能高效的查找、删除、插入;红黑树是一种平衡二叉搜索树,维持树的高度为 O ( l o g n ) O(logn) O(logn)

插入、查找、删除、访问特定位置的时间复杂度均为 O ( l o g n ) O(logn) O(logn),遍历的时间复杂度为 O ( n ) O(n) O(n),并且使用中序遍历可以将键升序输出(二叉搜索树特性);

红黑树的每个节点存储着键值对,树的大小大致为 ( O ( n ) O(n) O(n) ),因此 map 的空间复杂度为 ( O ( n ) O(n) O(n) ),因为树的高度为 ( O ( l o g n ) O(log n) O(logn)​ ),并且红黑规则要求树的平衡性,防止出现极端情况。

set集合

set是一种集合容器,它存储的元素是唯一的值,没有键值对的概念。set中的元素按照某种规则自动排序,因此可以按照值的顺序访问元素。

底层原理和map一致,都是基于红黑树;但是set没有键值对的概念,只有值;并且值唯一;


构造函数有几种:默认构造(无参)、有参、拷贝、移动构造,在什么情况下使用什么构造函数?

默认构造函数:不接受任何参数的构造函数,无需自己创建,在没有构造函数时,编译器自动创建一个默认无参构造函数;

有参构造函数:接受一个或者多个参数,通常用于初始化对象的成员变量;

拷贝构造函数:接受同类对象的常引用作为参数,并生成拷贝,用于对在已有对象的基础上创建新对象时,例如对象传参、复制等情况;编译器会自动合成这种构造函数;

class Example {
public:
    Example(const Example& other) {
        // 拷贝构造函数
    }
};

Example e1;
Example e2(e1); // 调用拷贝构造函数

移动构造函数:

  • 接受一个同类对象的右值引用(右值引用&&)作为参数。
  • 通过移动资源而不是拷贝它们来构造新对象。这能极大提高效率,特别是处理大型数据时。
  • 编译器从 C++11 起会自动合成移动构造函数,但如果类定义了自己的拷贝构造、拷贝赋值、移动赋值函数或析构函数,那么编译器将不再自动合成移动构造函数。
class Example {
public:
    Example(Example&& other) noexcept {
        // 移动构造函数
    }
};

Example e(Example{}); // 调用移动构造函数
  1. 默认无参构造函数:这个构造函数创建一个对象,没有参数传递给它。它使用类中定义的默认值来初始化对象的成员变量。
  2. 有参构造函数:这个构造函数接受一个或多个参数,用于初始化对象的成员变量。参数的类型和数量根据类的定义而定。
  3. 拷贝构造函数:这个构造函数接受一个同类型的对象作为参数,并创建一个新的对象,其成员变量与参数对象相同。拷贝构造函数通常用于在创建对象时进行深拷贝,以避免浅拷贝可能引发的问题。
  4. 移动构造函数:这个构造函数接受一个右值引用(&&)作为参数,用于创建一个新的对象。移动构造函数通常用于在转移资源所有权的情况下,提高性能和效率。

浅拷贝引发的问题:

  1. 资源共享:如果使用浅拷贝创建一个新对象,新对象和原对象会共享相同的资源,这意味着它们指向相同的内存地址。这可能导致在一个对象释放资源后,另一个对象仍然引用已释放的资源,从而导致悬空指针或内存泄漏。
  2. 对象析构问题:当使用浅拷贝时,原对象和新对象的析构函数都会尝试释放相同的资源。这可能导致多次释放同一块内存,引发内存错误或崩溃。
  3. 数据一致性问题:如果一个对象对资源进行了修改,而另一个对象也引用了相同的资源,那么它们之间的数据可能会不一致。这可能导致程序逻辑错误或意外的行为。

为了避免这些问题,最好使用深拷贝来创建新对象,这样每个对象都有自己独立的副本资源;


左值引用和右值引用

左值(Lvalue)是指具有标识符的表达式,可以放在赋值操作符的左边。它表示一个具名的内存位置,可以被访问和修改。例如,变量、数组元素和对象成员都是左值。

右值(Rvalue)是指不能被赋值的表达式,通常是临时的、无法取址的值。它表示一个临时的结果,不具有持久性。例如,字面量、临时对象和返回右值的函数调用都是右值。

左值引用(Lvalue reference)是指一个引用类型,它可以绑定到一个左值。使用左值引用可以对左值进行操作,如修改其值或获取其地址。左值引用使用&符号进行声明。

右值引用(Rvalue reference)是指一个引用类型,它可以绑定到一个右值。使用右值引用主要用于移动语义和临时对象的优化。右值引用使用&&符号进行声明。

// 左值引用
int x = 5; // x是一个左值
int& lref = x; // lref是对x的左值引用

lref = 10; // 修改x的值
int* ptr = &lref; // 获取x的地址

// 右值引用
int&& rref = 42; // rref是一个对右值的引用
int&& sum = x + 5; // 表达式x + 5是一个右值,sum是对它的引用

int* ptr = &rref; // 错误!不能获取右值的地址

std::string&& str = std::move(myString); // 使用std::move将左值转换为右值引用,用于移动语义

常量指针和指针常量

常量指针(const pointer)是指一个指针,它指向的对象是常量,不可通过该指针修改对象的值。指针本身是可变的,可以指向其他对象,但通过常量指针无法修改指向对象的值。

指针常量(pointer to const)是指一个指针,它本身是常量,不可通过该指针修改指向的对象。指针本身的值是固定的,但可以通过其他指针修改指向的对象的值。

// 常量指针(可以修改指针指向,但是不能修改指向对象的常量值)
int x = 5;
int y = 10;
const int* ptr = &x; // 声明一个常量指针,指向x

// 通过常量指针无法修改指向对象的值
*ptr = 7; // 错误!不能通过常量指针修改对象的值

ptr = &y; // 可以改变指针的指向

// 指针常量(可以修改指向对象的常量值,但是不能修改指针指向)
int x = 5;
int y = 10;
int* const ptr = &x; // 声明一个指针常量,指向x

// 通过指针常量可以修改指向对象的值
*ptr = 7; // 通过指针常量修改对象的值

ptr = &y; // 错误!不能改变指针常量的值

// 指向指针常量的常量指针(即无法修改常量值,也无法修改指针值)
int x = 5;
const int* const ptr = &x;

主要看const的作用域,const作用域它之间相连的右边的数据类型;如果右边是一个指针,则是常量指针,如果右边是一个常量,则是指针常量;


完美转发

完美转发(perfect forwarding)是一种特性,它允许将函数参数以原始的形式转发给其他函数,保持被转发参数的值类别不变。它是C++11引入的一项功能,通过使用引用折叠和右值引用来实现。

完美转发的目的是解决函数模板中参数类型推导的问题。在传递函数参数时,如果直接使用引用作为参数类型,会导致传递的参数类型被强制转换为引用类型,丧失了原始参数的值类别(左值或右值)。

使用完美转发,我们可以在函数模板中保留原始参数的值类别,并将其转发给其他函数。这样,被转发参数的值类别不会改变,同时可以避免不必要的拷贝或移动操作,提高效率。

template <typename T>
void foo(T&& arg) {
    bar(std::forward<T>(arg));
}

void bar(int& x) {
    std::cout << "Lvalue reference: " << x << std::endl;
}

void bar(int&& x) {
    std::cout << "Rvalue reference: " << x << std::endl;
}

int main() {
    int x = 5;
    foo(x); // 调用bar(int&)
    foo(10); // 调用bar(int&&) // 传入左值和右值调用不同的函数,以往都是调用参数为int的函数
    return 0;
}

函数模板foo使用了完美转发。通过使用std::forward函数,将参数arg以原始的形式转发给函数bar。根据参数的值类别,会调用相应的函数重载,保持参数的值类别不变。

通过完美转发,我们可以达到传递参数的目的,并保留原始参数的值类别,提高代码的灵活性和性能。

需要注意的是,完美转发要求转发函数的参数是模板类型,并且使用到了引用折叠和右值引用的特性。


OSI七层模型、TCP、IP分别在哪一层,TCP的三次握手。

七层模型自底向上分别是物理层、数据链路层、网络层、传输层、会话层(建立会话)、表示层(数据格式转换、加密)、应用层;

TCP在传输层、IP在网络层;

TCP的三次握手:

  1. 客户端发送握手请求,SYN=1,ACK=0,并且包含客户端初始序列号;
  2. 服务端接收到握手请求,发送SYN=1,ACK=1确认报文,确认号为客户端序列号加1,并且包含服务器初始序列号;
  3. 客户端收到服务器的确认报文,发送ACK=1、确认号为服务器序列号加1的报文,确认收到服务器的确认报文;
  4. 服务器收到客户端的报文,三次握手成功,建立连接;

三次握手保证了双方都有收发数据的能力;


从浏览器输入URL到显示页面发生了什么?

  1. 输入的URL包括几个部分,协议、域名、请求的文件路径、附加信息;拆解URL;
  2. 根据域名调用DNS协议获取到目标IP地址;(浏览器缓存、本机缓存、本地DNS域名服务器、根域名服务器、顶级域名服务器、权威域名服务器)
  3. 建立TCP连接;
  4. 建立TLS/SSL连接;
  5. 发送HTTP请求报文;
  6. 服务器处理报文并返回HTTP响应报文;
  7. 浏览器收到响应报文,根据报文内容渲染页面,然后显示页面;

IPC(进程通信)机制举例说明、socket返回的是什么?

进程通信的几种方式:

  1. 共享内存:内核空间中开辟一块进程可以共同访问的内存空间;
  2. 匿名管道:父进程和子进程间通信的方式,本质是读写一个文件,数据在管道中以字节流的方式进行传输;
  3. 命名管道:所有进程之间都可以通信,不需要非要父子进程;
  4. 消息队列:进程将信息挂载到消息队列上,其他进程可以在消息队列上取信息;一般是先进先出结构,可以存储多个不同优先级的信息;
  5. 信号:一个进程通知另一个进程的机制;不能传输大量信息;
  6. 套接字Socket:端到端的传输;可以是不同主机上的进程之间通信,也可以是同一个主机上的不同进程通信;支持多种协议,包括TCP、UDP;
  7. 信号量:提供一种进程之间同步机制;通过信号量,多个进程可以实现简单的协作和同步;

操作系统中,一切都是文件;所以Socket返回的本质是套接字文件描述符;


什么是回调函数?有什么作用?

回调函数是一种通过函数指针调用的函数,允许你将一个函数作为参数传递给另一个函数,并在特定事件发生时调用该函数。它广泛应用于各种编程任务中,包括异步编程、事件处理、数据处理等。

异步编程中:回调函数常用于处理异步操作的完成通知或结果返回。当一个耗时的操作完成后,系统会调用预先注册的回调函数来处理操作的结果。这样可以避免阻塞程序的执行,提高程序的响应性。


C++的内存管理机制。

C++使用手动内存管理机制,即开发人员需要自己负责分配和释放内存。这是通过使用newdelete运算符来实现的。

  1. 动态内存分配:使用new运算符可以在堆上动态地分配内存,并返回指向分配内存的指针。例如:
int* ptr = new int; // 在堆上分配一个整数的内存,并将其地址赋值给指针ptr
  1. 动态内存释放:使用delete运算符可以释放之前通过new分配的内存。例如:
delete ptr; // 释放ptr指向的内存

需要注意的是,使用new分配的内存必须通过相应的delete进行释放,以避免内存泄漏。

此外,C++还提供了数组的动态内存分配和释放方式:

int* arr = new int[5]; // 在堆上分配一个包含5个整数的数组,并将其地址赋值给指针arr

delete[] arr; // 释放arr指向的数组内存

除了手动内存管理,C++也提供了智能指针(如std::unique_ptrstd::shared_ptrstd::weak_ptr)来帮助管理内存。智能指针可以自动在适当的时候释放内存,避免了手动释放内存的繁琐和潜在的错误。

例如,使用std::unique_ptr可以管理动态分配的单个对象的内存:

std::unique_ptr<int> ptr = std::make_unique<int>(); // 在堆上分配一个整数的内存,并使用unique_ptr进行管理

// 不需要显式调用delete,当unique_ptr超出作用域时会自动释放内存

智能指针是C++中更安全和更方便的内存管理工具,可以减少内存泄漏和悬空指针的风险。

总的来说,C++的内存管理机制需要开发人员手动分配和释放内存。这需要谨慎地管理内存,以避免内存泄漏和使用无效的指针。同时,也可以使用智能指针等工具来简化内存管理的操作。


extern关键字有什么作用?define和typedef等关键字有什么作用?区别是什么?

extern关键字的作用是用于声明外部变量或函数。它告诉编译器,被声明的变量或函数并不是在当前文件或作用域内定义的,而是在其他文件或作用域内定义的。这样,编译器将在链接阶段将它们与实际定义进行关联。

例如,如果在一个源文件中声明了一个全局变量,但该变量的定义实际上在另一个源文件中,可以使用extern关键字来声明该变量。

// file1.cpp
int globalVariable = 10;

// file2.cpp
extern int globalVariable; // 声明外部变量

int main() {
    // 使用外部变量
    int x = globalVariable;
    // ...
    return 0;
}

在上述示例中,extern int globalVariable;声明了一个外部变量,告诉编译器该变量的定义在其他地方。这样,我们可以在main函数中使用该外部变量。

相比之下,#definetypedef是预处理器指令,而不是关键字。

  • #define用于定义预处理器宏。它将一个标识符与一个常量值或一段代码片段进行替换。预处理器在编译之前会进行宏的文本替换。例如:
#define PI 3.14159

double area = PI * radius * radius;

在上述示例中,#define PI 3.14159定义了一个名为PI的宏,它会在编译时被替换为3.14159。这样,我们可以在代码中使用PI来表示圆周率。

  • typedef用于为数据类型创建别名。它可以使代码更具可读性,并且可以简化复杂的类型声明。例如:
typedef int Age;

Age myAge = 25;

在上述示例中,typedef int Age;int类型创建了一个别名Age。这样,我们可以使用Age作为int类型的同义词。

总的来说,extern关键字用于声明外部变量或函数,#define用于定义预处理器宏,typedef用于为数据类型创建别名。它们的功能和用途不同,但都在C++中起到了重要的作用。


知道哪些设计模式,单例模式如何实现?

单例模式是一种创建对象的设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。在单例模式中,有两种常见的实现方式:饿汉式和懒汉式。

  1. 饿汉式(Eager Initialization):
    • 在程序启动时就创建单例对象,并在全局访问点直接返回该对象。
    • 优点是实现简单,线程安全,不会因为多线程环境而引起竞争条件。
    • 缺点是在程序启动时就创建对象可能会增加启动时间和内存占用。

以下是饿汉式的示例实现:

class Singleton {
private:
    static Singleton* instance;
    Singleton() {}  // 私有构造函数

public:
    static Singleton* getInstance() {
        return instance;
    }
};

Singleton* Singleton::instance = new Singleton();
  1. 懒汉式(Lazy Initialization):
    • 在首次使用时创建单例对象,延迟初始化。
    • 优点是在不使用单例对象时不会创建,可以节省内存。
    • 缺点是需要考虑线程安全性,可能需要采用加锁等机制来保证线程安全。

以下是懒汉式的示例实现:

class Singleton {
private:
    static Singleton* instance;
    Singleton() {}  // 私有构造函数

public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};

Singleton* Singleton::instance = nullptr;

需要注意的是,在多线程环境中使用懒汉式时,需要考虑线程安全性。可以使用互斥锁、双重检查锁或使用C++11的原子操作等方式来确保线程安全。

无论是饿汉式还是懒汉式,单例模式都可以确保一个类只有一个实例,并提供全局访问点来获取该实例。选择使用哪种方式取决于具体的需求和场景。


Linux常用命令

  1. ls:列出当前目录下的文件和子目录。
  2. cd:切换到指定的目录。
  3. pwd:显示当前工作目录的路径。
  4. mkdir:创建一个新的目录。
  5. rm:删除文件或目录。
  6. cp:复制文件或目录。
  7. mv:移动文件或目录,也可以用于重命名文件。
  8. cat:显示文件的内容。
  9. grep:在文件中搜索指定的文本模式。
  10. chmod:修改文件或目录的权限。
  11. chown:更改文件或目录的所有者。
  12. chgrp:更改文件或目录的所属组。
  13. find:在文件系统中查找文件或目录。
  14. tar:创建或提取归档文件。
  15. gzip:压缩文件。
  16. gunzip:解压缩文件。
  17. ssh:通过SSH协议连接到远程主机。
  18. scp:通过SSH协议复制文件到远程主机。
  19. wget:从网络上下载文件。
  20. top:显示系统中运行的进程和资源使用情况。

可以通过在终端中输入man命令加上命令名来查看命令的详细用法和选项。

例如,要查看ls命令的帮助文档,可以运行man ls命令。这将显示关于ls命令的详细信息,包括用法、选项和示例。


define和const哪一个好?

#defineconst都可以用于定义常量,但它们有一些区别和适用的场景。

#define是C/C++预处理器的指令,它用于在编译之前进行简单的文本替换。#define定义的常量没有类型,只是简单地将标识符替换为预定义的文本。这意味着#define可以用于定义常量、宏和条件编译等。但是,它没有作用域,它的定义在整个程序中都有效,可能会导致潜在的命名冲突和错误使用。

#define PI 3.14159
#define MAX(a, b) ((a) > (b) ? (a) : (b))

const是C++语言的关键字,用于定义具有类型和作用域的常量。const定义的常量具有类型安全性,可以进行类型检查和编译器优化。它还可以定义在函数作用域中,具有更小的作用范围,并且可以使用命名空间和类进行限定。

const double PI = 3.14159;
const int MAX = 100;

使用const常量通常被认为是更好的选择,因为它提供了类型安全性和更好的可读性。它还避免了可能发生的#define宏带来的一些问题,如副作用、优先级问题和命名冲突。

然而,在某些情况下,#define宏可能更加灵活,特别是在需要进行复杂的宏替换或条件编译时。#define宏还可以用于定义跨平台的常量和条件编译选项。

综上所述,const常量通常是更好的选择,因为它提供了类型安全性和更好的作用域限定。但是,根据具体的需求和场景,#define宏也可以是一种合适的选择。


面向对象和面向过程对比。

面向对象编程(OOP)和面向过程编程(Procedural Programming)是两种不同的编程范式。

面向对象编程强调将问题分解为对象的集合,每个对象都具有自己的状态和行为,并通过彼此之间的消息传递进行交互。OOP的主要概念包括封装、继承和多态。在面向对象编程中,程序的设计和实现主要围绕对象的抽象、类的定义和对象之间的关系展开。

面向过程编程则更加关注问题的步骤和过程,将问题分解为一系列的操作和函数。它将程序看作是一系列的过程或函数的集合,这些过程依次被调用以完成特定的任务。面向过程编程通常将数据和操作分离,并通过传递参数来传递数据和返回结果。

下面是面向对象编程和面向过程编程的一些对比:

  1. 抽象:面向对象编程通过类和对象的抽象,将问题分解为更小的可管理的单元。面向过程编程则更关注问题的过程和步骤。
  2. 封装和信息隐藏:面向对象编程通过将数据和行为封装在对象中,实现了信息隐藏和模块化。面向过程编程则将数据和操作分开,可能会暴露更多的实现细节。
  3. 继承和多态:面向对象编程通过继承和多态实现了代码的重用和灵活性。面向过程编程通常需要通过复制和粘贴代码来实现代码的重用。
  4. 可维护性和扩展性:面向对象编程通过封装、继承和多态提供了更好的可维护性和扩展性。面向过程编程可能会导致代码的重复和紧耦合,使得维护和扩展更加困难。
  5. 设计思路:面向对象编程更注重问题的分析和设计,关注问题的本质和对象之间的关系。面向过程编程更注重问题的流程和步骤,关注问题的执行过程。

面向对象编程和面向过程编程都有自己的优点和适用场景。面向对象编程通常更适合大型和复杂的项目,可以提供更好的可维护性、可扩展性和代码重用性。面向过程编程则更适合简单和直接的问题,可以提供更高的执行效率和控制。

在实际开发中,根据具体的需求和项目的规模,可以选择适合的编程范式或将二者结合使用,以获得最佳的结果。


设计一个单链表,包括增删改查,以及成员变量,构造函数等

简单的单链表,包括增删改查操作以及相关的成员变量和构造函数。

#include <iostream>

// 定义单链表节点
class Node {
public:
    int data; // 节点数据
    Node* next; // 指向下一个节点的指针

    // 构造函数
    Node(int value) {
        data = value;
        next = nullptr;
    }
};

// 定义单链表
class LinkedList {
private:
    Node* head; // 链表头指针

public:
    // 构造函数
    LinkedList() {
        head = nullptr;
    }

    // 插入节点到链表末尾
    void insert(int value) {
        Node* newNode = new Node(value);

        if (head == nullptr) {
            head = newNode;
        } else {
            Node* current = head;
            while (current->next != nullptr) {
                current = current->next;
            }
            current->next = newNode;
        }
    }

    // 删除指定值的节点
    void remove(int value) {
        if (head == nullptr) {
            return;
        }

        if (head->data == value) {
            Node* temp = head;
            head = head->next;
            delete temp;
            return;
        }

        Node* current = head;
        while (current->next != nullptr) {
            if (current->next->data == value) {
                Node* temp = current->next;
                current->next = current->next->next;
                delete temp;
                return;
            }
            current = current->next;
        }
    }

    // 修改指定位置的节点值
    void modify(int position, int value) {
        Node* current = head;
        int index = 0;
        while好的,我将为你设计一个简单的单链表,包括增加节点、删除节点、修改节点和查找节点的功能,并添加一些常用的成员变量和构造函数。

首先,我们定义一个节点类 `Node`,它包含一个数据成员 `data` 和一个指向下一个节点的指针 `next`:

```cpp
class Node {
public:
    int data;
    Node* next;

    Node(int value) {
        data = value;
        next = nullptr;
    }
};

定义一个单链表类 LinkedList,它包含一个指向头节点的指针 head

class LinkedList {
private:
    Node* head;

public:
    LinkedList() {
        head = nullptr;
    }

    // 增加节点
    void insert(int value) {
        Node* newNode = new Node(value);

        if (head == nullptr) {
            head = newNode;
        } else {
            Node* current = head;
            while (current->next != nullptr) {
                current = current->next;
            }
            current->next = newNode;
        }
    }

    // 删除节点
    void remove(int value) {
        if (head == nullptr) {
            return;
        }

        if (head->data == value) {
            Node* temp = head;
            head = head->next;
            delete temp;
        } else {
            Node* current = head;
            while (current->next != nullptr && current->next->data != value) {
                current = current->next;
            }

            if (current->next != nullptr) {
                Node* temp = current->next;
                current->next = current->next->next;
                delete temp;
            }
        }
    }

    // 修改节点
    void modify(int oldValue, int newValue) {
        Node* current = head;
        while (current != nullptr) {
            if (current->data == oldValue) {
                current->data = newValue;
                return;
            }
            current = current->next;
        }
    }

    // 查找节点
    Node* search(int value) {
        Node* current = head;
        while (current != nullptr) {
            if (current->data == value) {
                return current;
            }
            current = current->next;
        }
        return nullptr;
    }
};

可以使用这个简单的单链表类进行节点的增加、删除、修改和查找操作。例如:

LinkedList list;

list.insert(5);
list.insert(10);
list.insert(15);

list.remove(10);

list.modify(5, 7);

Node* node = list.search(15);
if (node != nullptr) {
    // 找到节点
} else {
    // 节点不存在
}

引用和指针的区别

引用和指针是C++中用于处理内存和变量的两种不同的机制。

  1. 定义和初始化:指针是一个变量,存储的是另一个变量的地址。可以使用*运算符来间接访问指针指向的变量。指针需要使用&运算符来获取变量的地址,并使用*运算符来访问指针指向的值。引用是已存在的变量的别名,它必须在定义时进行初始化,并且不能重新绑定到其他变量。

  2. 空值:指针可以为空,即指向空地址(nullptr)。引用必须始终引用一个有效的对象,不能引用空值。

  3. 重新赋值:指针可以被重新赋值,可以指向不同的变量。引用一旦绑定到一个变量,就不能再绑定到其他变量,它始终引用同一个对象。

  4. 空间占用:指针本身占用一定的内存空间,而引用只是变量的别名,并不占用额外的内存空间。

  5. 空间操作:指针可以进行算术运算,例如指针加法和减法,以及指针的比较。引用没有这些操作,它只是变量的别名。

  6. 语法:指针使用*作为解引用运算符,&作为取地址运算符。引用没有特殊的运算符,它直接使用变量名来表示引用。

引用和指针都可以用于传递参数、修改变量和实现数据结构等。选择使用引用还是指针取决于具体的需求和情况。引用通常更容易使用和理解,可以提高代码的可读性和可维护性。指针在某些情况下更灵活,例如需要动态分配内存或需要进行空值检查等情况。


malloc、free、new、delete的区别

mallocfree是C语言中用于动态内存分配和释放的函数,而newdelete是C++中的操作符,用于对象的动态内存分配和释放。下面是它们之间的一些区别:

  1. 语法:mallocfree是函数,需要使用函数调用的语法来调用它们,如malloc(size)free(ptr)。而newdelete是操作符,使用类似于语法糖的形式,如new Typedelete ptr

  2. 类型安全性:mallocfree在使用时需要手动进行类型转换,因为它们返回的是void*指针,需要将其转换为正确的指针类型。而newdelete是类型安全的,它们会根据所需的类型进行动态内存分配和释放,无需手动指定类型。

  3. 构造函数和析构函数的调用:new在进行内存分配后会自动调用对象的构造函数来初始化对象,而delete释放内存之前会自动调用对象的析构函数来进行资源清理。这使得newdelete更适合用于动态对象的创建和销毁,而mallocfree则更适合用于分配和释放原始的内存块。

  4. 内存分配失败处理:malloc在内存分配失败时会返回NULL指针,需要手动检查返回值来处理错误情况。而new在内存分配失败时会抛出std::bad_alloc异常,可以使用异常处理机制来处理错误情况。

  5. 数组的分配和释放:mallocfree可以用于分配和释放数组的内存块。而new[]delete[]是用于分配和释放数组对象的内存,它们会自动调用数组元素的构造函数和析构函数。

总的来说,mallocfree是C语言的函数,适用于动态内存分配和释放;而newdelete是C++的操作符,适用于动态对象的创建和销毁,具有更好的类型安全性和自动调用构造函数和析构函数的功能。在C++中更推荐使用newdelete来进行动态内存分配和释放,尤其是在涉及到对象的情况下。


git的使用

Git是一个分布式版本控制系统,广泛用于管理和跟踪软件开发项目的代码。下面是一些常见的Git使用场景和命令:

  1. 初始化仓库:使用git init命令在一个目录中初始化一个新的Git仓库。

  2. 克隆仓库:使用git clone <repository>命令克隆一个远程Git仓库到本地。

  3. 添加文件:使用git add <file>命令将文件添加到暂存区。

  4. 提交更改:使用git commit -m "<message>"命令将暂存区中的更改提交到本地仓库。

  5. 查看状态:使用git status命令查看当前工作区和暂存区的状态。

  6. 查看提交历史:使用git log命令查看仓库的提交历史。

  7. 创建分支:使用git branch <branchname>命令创建一个新的分支。

  8. 切换分支:使用git checkout <branchname>命令切换到指定的分支。

  9. 合并分支:使用git merge <branchname>命令将指定分支的更改合并到当前分支。

  10. 推送更改:使用git push <remote> <branchname>命令将本地仓库的更改推送到远程仓库。

  11. 拉取更改:使用git pull <remote> <branchname>命令从远程仓库拉取最新的更改到本地仓库。

  12. 解决冲突:当合并分支或拉取更改时,如果出现冲突,需要手动解决冲突并提交更改。


C++结构体和C语言结构体有什么不同?

  1. 默认访问修饰符:在C语言中,结构体的成员默认是公共的(public),而在C++中,默认是私有的(private)。
  2. 成员函数:C++结构体可以包含成员函数,这使得结构体可以具有更多的行为和功能。C语言结构体只能包含成员变量。
  3. 继承:C++结构体可以通过继承来扩展其功能,从其他结构体或类继承成员变量和函数。C语言结构体不支持继承。
  4. 构造函数和析构函数:C++结构体可以定义构造函数和析构函数,用于初始化和清理结构体的成员。C语言结构体不支持构造函数和析构函数。
  5. 默认初始化:C++结构体可以通过在定义时调用默认构造函数进行默认初始化。C语言结构体没有默认初始化机制。
struct Person {
    // 成员变量
    std::string name;
    int age;

    // 构造函数
    Person(const std::string& n, int a) : name(n), age(a) {}

    // 成员函数
    void sayHello() {
        std::cout << "Hello, my name is " << name << " and I am " << age << " years old." << std::endl;
    }
};

int main() {
    // 创建结构体对象
    Person person("John", 25);

    // 调用成员函数
    person.sayHello();

    return 0;
}

函数中传指针、传引用和传常量的区别

  1. 传递指针(Passing Pointers):
    • 通过指针传递参数时,函数接收指针的副本,可以通过指针间接访问和修改原始数据。
    • 函数可以修改指针指向的数据,包括修改原始数据的值。
    • 指针可以为空(指向空地址),需要在函数中进行空指针检查。
  2. 传递引用(Passing References):
    • 通过引用传递参数时,函数接收引用的别名,可以直接访问和修改原始数据。
    • 函数可以修改原始数据的值,修改会直接反映在原始数据上。
    • 引用不能为空,必须始终引用一个有效的对象。
  3. 传递常量(Passing Constants):
    • 通过常量传递参数时,函数接收常量的副本,不能直接修改原始数据。
    • 函数只能读取常量的值,不能修改原始数据。
    • 常量传递可以用于保护数据的完整性和防止意外修改。
void modifyByPointer(int* ptr) {
    *ptr = 100; // 修改原始数据
}

void modifyByReference(int& ref) {
    ref = 200; // 修改原始数据
}

void readConstant(const int value) {
    // value = 300; // 编译错误,不能修改常量
}

int main() {
    int num = 0;

    modifyByPointer(&num); // 通过指针修改数据
    std::cout << num << std::endl; // 输出:100

    modifyByReference(num); // 通过引用修改数据
    std::cout << num << std::endl; // 输出:200

    readConstant(num); // 通过常量传递数据
    std::cout << num << std::endl; // 输出:200,数据未被修改

    return 0;
}

注意传指针虽然能修改原数据,但必须先解引用;


socket进程间通信,如果要传大文件,如何做?

  1. 分块传输:将大文件划分为较小的块,分块传输可以减少单个传输过程中的内存占用和网络传输的负载。发送端将每个块逐个发送给接收端,接收端接收到每个块后再进行组装。
  2. 流式传输:使用流式传输的方式,不需要将整个文件划分为块,而是一边读取文件的内容,一边通过Socket流逐个发送给接收端,接收端则一边接收数据,一边写入到目标文件中。
  3. 压缩传输:在传输过程中对文件进行压缩,可以减小文件的大小,从而减少传输时间和网络负载。发送端在发送之前将文件进行压缩,接收端在接收到数据后进行解压缩。
  4. 断点续传:如果传输过程中出现网络中断或其他异常情况,可以通过记录传输的起始位置和已传输的数据量,以及在接收端进行校验和验证,实现断点续传的功能。这样即使传输中断,可以从上次中断的位置继续传输,避免重新传输整个文件。
  5. 并行传输:使用多个线程或进程同时传输文件的不同部分,可以提高传输速度。发送端将文件划分为多个部分,并使用多个Socket同时传输,接收端也使用相应的多个Socket接收数据并进行合并。
  6. 使用高性能网络库或协议:选择适用于大文件传输的高性能网络库或协议,如TCP、UDP或专门用于大文件传输的协议(如HTTP或FTP),以提供更高的传输速度和效率

如何保证并发性?

  1. 互斥锁(Mutex):使用互斥锁可以确保在同一时间只有一个线程或进程可以访问共享资源。通过在访问共享资源之前获取互斥锁,并在访问结束后释放锁,可以避免多个线程或进程同时修改共享资源导致的数据竞争和不一致性。
  2. 读写锁(Read-Write Lock):读写锁允许多个线程或进程同时读取共享资源,但只允许一个线程或进程进行写操作。这种机制可以提高并发性能,因为读操作通常比写操作更频繁。
  3. 原子操作(Atomic Operations):原子操作是指不可被中断的操作,可以确保在并发执行的情况下对共享资源的操作是原子的。原子操作可以避免数据竞争和不一致性,常见的原子操作有原子变量、原子加减操作等。
  4. 条件变量(Condition Variable):条件变量用于线程或进程之间的同步和通信。一个线程或进程可以在某个条件满足时等待,另一个线程或进程在条件满足时通过条件变量唤醒等待的线程或进程。条件变量常用于实现生产者-消费者模式等并发设计模式。
  5. 无锁数据结构(Lock-Free Data Structures):无锁数据结构是一种无需使用互斥锁的数据结构设计,通过使用原子操作和其他并发控制手段来确保数据的一致性。无锁数据结构可以提高并发性能,但也更复杂和容易出错。
  6. 并发算法和并发编程模型:采用合适的并发算法和并发编程模型,如并行计算、消息传递、任务并发等,可以有效地提高程序的并发性能和可扩展性。

非对称加密是什么?

HTTPS就使用了非对称加密和对称加密;

非对称加密就是加密和解密使用的不是相同的密钥,而是一个有关联的密钥;具体来说,服务器有一个公钥和一个私钥,公钥加密的数据可以使用私钥解密,私钥加密的数据可以用公钥解密;

在HTTPS中,服务器将公钥发送给客户端,客户端用公钥对会话密钥进行加密,然后发送给服务器,服务器用私钥解密获取到了会话密钥,之后就可以使用会话密钥来对称加密通话信息;


单核单线程CPU上使用多线程的意义是什么

防止大任务一直占用CPU把小任务饿死;

在单核单线程CPU上使用多线程可能并没有直接的性能优势,因为在单线程环境下,CPU只能按顺序执行指令,无法同时执行多个线程。然而,使用多线程在单核单线程CPU上仍然有一些意义和优势:

  1. 并发性和响应性:多线程可以模拟并发执行,即使在单核单线程CPU上,也可以使得程序在逻辑上同时执行多个任务。这样可以提高程序的响应性,使用户感觉到程序具有更好的交互性和实时性。

  2. 阻塞和等待:在单线程环境中,当程序遇到阻塞操作(如等待IO完成)时,整个程序会停止执行,直到阻塞操作完成。使用多线程可以将这些阻塞操作放在独立的线程中,使得其他线程可以继续执行,提高了程序的效率和吞吐量。

  3. 并行任务处理:尽管单核单线程CPU无法真正同时执行多个线程,但在某些情况下,可以利用多线程来并行处理一些独立的任务。例如,可以将计算密集型的任务划分为多个子任务,在多个线程中并行处理,从而加速整体的计算过程。

  4. 异步编程:使用多线程可以实现异步编程模型,通过将一些耗时的操作放在后台线程中执行,使得主线程可以继续执行其他任务,提高了程序的并发性和效率。

尽管在单核单线程CPU上使用多线程可以增强程序的并发性、提高响应性、处理阻塞和等待、并行处理独立任务等。然而,在多线程编程中需要注意线程间的同步与互斥,以避免数据竞争和其他并发问题的出现。


大端存储和小端存储(大高低,小高高)

大端存储就是高字节在内存低地址,低字节存储在内存高地址;

小端存储就是低字节存储在内存低地址,高字节存储在内存高地址;

网络传输统一使用大端存储,所以要把信息发送到网络上要将小端存储变成大端存储;


如果使用未知关键字,在哪个编译环节报错?对常量赋值在哪报错?

在C++中,如果使用了未知的关键字,编译器会在编译阶段报错。编译器在解析源代码时会对语法进行分析,如果遇到未知的关键字或标识符,编译器将无法识别其含义,因此会产生编译错误。

常量修改也会在编译阶段进行报错;


死锁的原因

死锁的原因一般有多个,比如非抢占式申请资源,资源占有之后不会主动释放,资源一次只能被一个进程获取,进程之间形成环形等待链;

即:互相等待、资源独占、不可剥夺资源、持有并等待;(抢不走、不给放、我要你的,你要我的,只有一个)


进程和线程

进程:有独立的内存空间和资源,是运行中的程序;上下文切换开销大;

线程:没有独立的内存空间,共享进程的内存空间;是轻量级的进程,有自己独立的栈、程序计数器和局部变量,但是堆空间共享;


数组和指针的区别

  1. 类型:数组是一种数据类型,用于存储一组相同类型的元素。指针是一个变量,用于存储一个变量的地址。
  2. 大小:数组在定义时需要指定固定的大小,而指针没有固定大小。
  3. 内存分配:数组在定义时会在内存中分配一块连续的内存空间来存储元素。指针只是存储一个地址,可以通过它来访问内存中的数据,但是指针本身不会分配内存。
  4. 访问:数组可以直接通过索引访问元素,使用[]运算符。指针需要使用解引用运算符*来访问指针指向的值。
  5. 可变性:数组的大小是固定的,无法改变。指针可以指向不同的地址,可以使用指针进行地址的偏移和修改。
  6. 传递给函数:数组在函数中传递时,会进行数组的拷贝或者数组的指针传递。指针可以传递给函数,函数可以直接操作指针指向的数据。

内存泄露

内存泄漏是指在程序中动态分配的内存没有被正确释放或回收的情况。这意味着在程序执行过程中,分配的内存不再被使用,但没有被释放,导致内存的浪费和不可用。

内存泄漏通常发生在以下情况下:

  1. 忘记释放动态分配的内存:当使用newmalloc等操作符分配内存时,必须使用deletefree等操作符释放内存。如果忘记释放分配的内存,就会导致内存泄漏。

  2. 异常情况下的内存泄漏:如果在异常处理过程中没有正确释放分配的内存,就会导致内存泄漏。因此,在编写异常处理代码时,应该确保释放所有动态分配的内存。

  3. 循环引用:在使用垃圾回收机制的语言中,如果对象之间存在循环引用,而没有被正确处理,就会导致内存泄漏。垃圾回收机制无法回收被循环引用的对象。

内存泄漏可能会导致程序运行时的内存占用不断增加,最终耗尽可用内存,导致程序崩溃或性能下降。为了避免内存泄漏,应该遵循以下几点:

  1. 在动态分配内存后,确保在不再需要时正确释放内存。

  2. 在异常处理代码中,确保释放所有动态分配的内存。

  3. 避免循环引用,特别是在使用垃圾回收机制的语言中。

  4. 使用内存管理工具和编程实践,如智能指针和RAII(资源获取即初始化)等,可以减少内存泄漏的风险。


局部变量和全局变量可以同名吗

局部变量和全局变量可以同名。在C++中,局部变量是在特定的作用域内定义的变量,而全局变量是在全局范围内定义的变量。当局部变量和全局变量同名时,根据作用域的规则,局部变量将会覆盖全局变量。


如果我sizeof一个纯虚类和sizeof一个空类有什么区别吗,他们的大小分别是多少?

在C++中,sizeof运算符用于获取对象或类型的大小(字节数)。对于纯虚类和空类,它们的大小是不同的。

  1. 纯虚类(Abstract Class):纯虚类是包含至少一个纯虚函数的类,它无法实例化对象。纯虚函数是通过在函数声明中使用virtual关键字并在函数体内使用= 0来声明的。纯虚类通常被用作基类,派生类必须实现纯虚函数才能被实例化。纯虚类的大小通常为一个指针大小(通常是4或8字节),这是因为纯虚类本身不包含任何成员变量,只有一个虚函数表指针(vptr)。
  2. 空类(Empty Class):空类是没有任何成员变量和成员函数的类。空类的大小通常为1字节(某些编译器可能会额外添加对齐信息,导致大小不为1字节)。即使空类不包含任何成员,编译器为每个对象保留至少一个字节的空间,以确保每个对象在内存中具有唯一的地址。

请注意,空类的大小为1字节是C++标准的要求,但编译器可能会对空类进行优化,将其大小优化为0字节。这种优化是编译器特定的行为,不是C++标准规定的。


malloc你觉得会返回什么东西呢,这个地址是物理地址还是虚拟地址呢?

malloc函数在C中用于动态分配内存空间,并返回指向分配内存的起始地址的指针。malloc返回的是指向分配内存块的虚拟地址,而不是物理地址。

虚拟地址是程序中使用的地址空间,它是由操作系统管理和映射到物理内存的。虚拟地址是相对于进程的地址空间的,每个进程都有自己独立的虚拟地址空间。操作系统负责将虚拟地址映射到物理内存的实际地址。


你对操作系统的内存分配的原理知道吗,比如说你通过malloc去申请一个内存,这个操作系统的处理方式是怎么样的呢?

当程序通过malloc函数向操作系统请求内存时,操作系统会根据其内存管理的原理来处理这个请求。以下是一般情况下操作系统的内存分配处理方式:

  1. 内存管理单元:操作系统维护一个内存管理单元,负责跟踪和管理整个系统的内存使用情况。这个管理单元会记录哪些内存块是空闲的,哪些是已被分配的。

  2. 内存分配算法:操作系统使用不同的内存分配算法来满足malloc请求。常见的算法包括首次适应、最佳适应和最坏适应等。

  • 首次适应:从空闲内存块链表中找到第一个满足大小要求的内存块。
  • 最佳适应:从空闲内存块链表中找到大小最合适的内存块。
  • 最坏适应:从空闲内存块链表中找到大小最大的内存块。
  1. 内存分配:当操作系统接收到malloc请求时,它会查找可用内存块链表,找到一个大小足够的空闲内存块。然后,操作系统会将这个内存块标记为已分配,并返回给程序一个指向这个内存块的虚拟地址。

  2. 内存释放:当程序通过free函数释放动态分配的内存时,操作系统会将这个内存块标记为未分配,并将其返回给空闲内存块链表,以供后续的malloc请求使用。


说一说你了解的多继承?多继承中可能会遇到的问题,然后怎么处理

多继承是一种面向对象编程中的特性,允许一个类从多个父类中继承属性和方法。在C++中,类可以通过使用逗号分隔的方式同时继承多个父类。

多继承的优点是可以使类具有多个不同父类的特性,增加了代码的灵活性和重用性。然而,多继承也可能引发一些问题,其中最常见的是以下两个问题:

  1. 菱形继承(Diamond Inheritance):当一个类从两个不同的父类继承,而这两个父类又有一个共同的父类时,就会出现菱形继承。这样就会导致派生类中存在两份间接继承的共同基类,并且可能会引发二义性问题,即同名成员在派生类中存在冲突。

  2. 命名冲突:当多个父类中存在同名的成员函数或成员变量时,派生类在访问这些同名成员时可能会发生命名冲突。这会导致编译错误,因为编译器无法确定使用哪个同名成员。

为了解决多继承中可能遇到的问题,可以采取以下几种处理方式:

  1. 使用虚继承(Virtual Inheritance):通过在继承关系中使用虚继承,可以解决菱形继承导致的二义性问题。虚继承使得在派生类中只保留一份共同基类的实例。

  2. 重定义同名成员函数:如果多个父类中存在同名的成员函数,派生类可以通过在自己的类中重新定义该成员函数来解决命名冲突。使用作用域解析运算符::可以指定要调用的父类版本。

  3. 命名空间(Namespace):将同名的成员函数或成员变量放置在不同的命名空间中,以避免命名冲突。

  4. 组合而非继承:在某些情况下,可以通过组合(Composition)而非继承的方式来解决多继承带来的问题。通过将多个类的对象作为成员变量组合到一个类中,可以实现类似于多继承的功能。


介绍一下菱形继承,菱形继承会有什么问题,然后怎么解决菱形继承带来的问题

菱形继承是一种在多继承中可能出现的情况,它指的是一个派生类同时继承了两个不同的父类,而这两个父类又有一个共同的基类。

用一个例子来说明菱形继承的情况。假设有一个基类Animal,有两个派生类:Mammal(哺乳动物)和Bird(鸟类)。现在我们希望定义一个派生类Bat(蝙蝠),它既是哺乳动物又是鸟类。这样,Bat类会同时继承Mammal和Bird两个父类,而Mammal和Bird又都继承自Animal基类。

菱形继承可能会导致以下问题:

  1. 冗余数据:由于Bat类同时继承了Mammal和Bird,而这两个类又都继承了Animal,所以在Bat中将会有两份Animal类的成员数据。这样会浪费内存空间。
  2. 二义性:如果在Animal类中定义了一个名为eat()的成员函数,并且Mammal和Bird类都分别重写了这个函数,那么在Bat类中将会存在两个同名的eat()函数。这样会导致调用eat()函数时产生二义性,编译器无法确定使用哪个版本的函数。

为了解决菱形继承带来的问题,可以采取以下几种方式:

  1. 使用虚继承(Virtual Inheritance):在Mammal和Bird与Animal之间的继承关系中,使用虚继承可以避免菱形继承带来的冗余数据。通过在继承声明中加上关键字virtual,如class Mammal : virtual public Animal,可以确保在继承链中只有一份Animal的实例。
  2. 虚继承后重定义:当使用虚继承后,如果在Bat类中需要访问Animal的成员函数,可以选择在Bat类中重新定义这些函数,以解决二义性问题。
  3. 使用虚函数:在Animal类中定义虚函数,并在Mammal和Bird类中重写这些虚函数,这样在Bat类中就可以通过调用这些虚函数来避免二义性。

C++的整个过程,从编辑到运行的过程是什么样的?注释是在这个过程中哪个过程处理的?

  1. 编写源代码:程序员使用文本编辑器编写C++源代码文件,代码文件通常以.cpp为扩展名。

  2. 预处理(Preprocessing):使用预处理器(如C++预处理器)对源代码进行预处理。预处理器执行以下操作:

    • 文件包含(File Inclusion):根据#include指令将其他文件的内容包含到当前文件中。
    • 宏替换(Macro Substitution):将代码中的宏名称替换为相应的值或代码片段。
    • 条件编译(Conditional Compilation):根据预定义的宏的状态来选择性地编译代码块。
    • 注释处理(Comment Handling):删除代码中的注释。
    • 符号常量定义(Symbol Constant Definition):定义符号常量以便在代码中使用。
  3. 编译(Compilation):使用C++编译器(如GCC或Clang),将预处理过的源代码文件编译成汇编语言代码或目标代码文件。编译过程包括以下步骤:

    • 词法分析(Lexical Analysis):将源代码分解成词法单元(tokens),如关键字、标识符、运算符等。
    • 语法分析(Syntax Analysis):根据语法规则检查词法单元的组合是否符合语法结构。
    • 语义分析(Semantic Analysis):对语法正确的代码进行语义检查,并生成中间表示(如抽象语法树)。
    • 代码生成(Code Generation):将中间表示转换为机器可执行的目标代码。
  4. 链接(Linking):将编译生成的目标代码文件与所需的库文件进行链接,生成最终的可执行文件。链接过程包括以下步骤:

    • 符号解析(Symbol Resolution):将目标文件中的符号引用与库文件中的符号定义进行匹配。
    • 重定位(Relocation):将目标文件中的相对地址转换为绝对地址,以便正确地在内存中加载代码和数据。
    • 符号表生成(Symbol Table Generation):生成用于在运行时查找符号地址的符号表。
  5. 运行程序:将生成的可执行文件加载到内存中,并由操作系统启动执行。操作系统会为程序分配资源,如内存空间、打开文件等,并按照程序指令逐行执行,直到程序结束或遇到异常。

注释是在预处理(预编译阶段)进行删除;


#ifdef 和 #ifndef的好处是啥?

#ifdef#ifndef是C和C++预处理器中的条件编译指令,用于在编译时根据条件来选择性地包含或排除代码。

#ifdef指令用于检查一个宏是否已经被定义,如果该宏已经定义,则执行后续的代码块。#ifndef指令则是检查一个宏是否未被定义,如果该宏未被定义,则执行后续的代码块。

这两个指令的好处是:

  1. 条件编译:#ifdef#ifndef允许在编译时根据宏的定义与否来决定是否编译特定的代码块。这样可以根据不同的条件来选择性地包含或排除代码,从而实现代码的灵活性和可配置性。

  2. 避免重复包含:#ifndef常用于防止头文件的重复包含。通过在头文件的开头使用#ifndef指令,可以确保只有当该头文件没有被包含过时才进行包含,从而避免重复定义变量、函数或类等问题。

下面是一个简单的示例,展示了#ifdef#ifndef的使用:

#ifndef MY_HEADER_H
#define MY_HEADER_H

// 在这里定义头文件的内容

#endif

在上述示例中,MY_HEADER_H是一个自定义的宏。如果在编译过程中该宏未被定义,则会执行#ifndef后的代码块,也就是定义头文件的内容。如果MY_HEADER_H已经被定义过了,则会跳过#ifndef后的代码块,从而避免重复包含头文件。

通过合理使用#ifdef#ifndef,可以在编译时根据条件灵活地控制代码的包含与排除,提高代码的可维护性和可配置性。


可不可以无限递归,在内存无限大的时候?

在理论上,如果有足够大的内存空间,是可以进行无限递归的。每次递归调用都会在内存中创建一个新的栈帧,保存函数的局部变量和返回地址等信息。当递归调用结束时,栈帧会被销毁,从而释放内存。

然而,在实际情况下,内存是有限的,并不可能拥有无限大的内存。当递归调用的层数过多时,会导致栈空间的溢出,即栈溢出(stack overflow)。栈溢出会导致程序异常终止,并可能引发内存错误或崩溃。


一般的析构函数是否需要添加virtual修饰,什么情况下需要加,什么情况下不需要加,为什么?

在一般情况下,析构函数不需要添加virtual修饰符。只有在使用多态(Polymorphism)的情况下,才需要将析构函数声明为虚函数。

当基类指针指向派生类对象时,如果基类的析构函数不是虚函数,那么当通过基类指针调用delete释放内存时,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类资源无法正确释放,造成内存泄漏或其他问题。

通过将基类的析构函数声明为虚函数,可以实现动态绑定。这意味着通过基类指针调用delete释放内存时,会根据指针指向的实际派生类对象来调用相应的析构函数,确保正确释放派生类资源。

因此,当存在继承关系并且通过基类指针来管理派生类对象时,应该将基类的析构函数声明为虚函数。这样可以确保在删除对象时,正确调用派生类的析构函数。

需要注意的是,将析构函数声明为虚函数会引入虚函数表(vtable)的开销,增加了内存和运行时的开销。在没有多态需求的情况下,将析构函数声明为虚函数是不必要的。

总结起来,一般情况下,析构函数不需要添加virtual修饰符。只有在存在继承关系并且需要通过基类指针管理派生类对象时,才需要将析构函数声明为虚函数,以确保正确释放派生类资源。


析构函数和构造函数能不能是虚函数?

构造不可以,析构一般写成虚函数;析构写成虚函数可以在多态中实现通过基类指针调用delete释放内存时,根据指针指向的派生类来调用相应的析构函数,而不是总是调用基类的析构函数;

在构造函数中使用虚函数是没有意义的,因为在对象创建的过程中,对象的类型已经确定,且虚函数的动态绑定是通过对象的虚函数表来实现的,而在构造函数中,对象的虚函数表还没有被构造完成,因此无法进行动态绑定。

而在析构函数中使用虚函数是有意义的,特别是在使用基类指针或引用管理派生类对象时。当通过基类指针或引用删除对象时,如果析构函数是虚函数,那么会根据实际对象的类型来调用正确的析构函数,确保正确释放派生类的资源。这是因为在对象销毁的过程中,对象的类型是已知的,虚函数表也是完整的,可以进行动态绑定。


头文件是怎么操作的?

头文件是一种包含了函数声明、类定义、宏定义等内容的文件,通常以.h.hpp为扩展名。头文件的主要作用是在不同的源文件之间共享代码,提供代码的模块化和可重用性。

在使用头文件时,有两种常见的操作方式:

  1. 包含头文件:通过使用预处理器指令#include,可以将头文件的内容包含到当前源文件中。例如,#include "myheader.h"会将名为myheader.h的头文件包含到当前源文件中。包含头文件可以让编译器在编译时能够识别并使用头文件中的声明、定义和宏定义。

  2. 防止重复包含:由于头文件可能被多个源文件包含,为了避免重复定义变量、函数或类等问题,通常在头文件的开头使用条件编译指令。常见的方式是使用#ifndef#define来创建一个预处理器宏,并在文件的结尾使用#endif来结束条件编译区块。这样,当头文件被多次包含时,只有第一次会真正包含,后续的包含都会被跳过,避免重复定义。

例如,下面是一个头文件的示例:

#ifndef MYHEADER_H
#define MYHEADER_H

// 在这里定义头文件的内容,包括函数声明、类定义、宏定义等

#endif

在源文件中,通过使用#include "myheader.h"来包含该头文件,就可以在源文件中使用头文件中定义的函数、类和宏。

使用头文件可以提高代码的可维护性和可重用性,将代码模块化,并使不同的源文件之间可以共享代码。头文件的设计应该遵循良好的编程实践,避免过多的依赖和冗余的引用,以确保代码的清晰性和可扩展性。


你说说操作系统的概念中最重要的几个模块,比如文件管理模块。

以下是其中几个重要的模块:

  1. 文件管理模块:文件管理是操作系统的核心功能之一,负责管理计算机系统中的文件和文件系统。它包括文件的创建、读取、写入、删除以及对文件进行组织和保护的操作。文件管理模块还负责文件的存储分配、磁盘空间管理、文件访问控制和文件共享等功能。

  2. 进程管理模块:进程管理是操作系统中的另一个重要模块,负责管理和控制系统中的进程。它包括进程的创建、调度、同步、通信和终止等操作。进程管理模块还负责为进程分配和管理系统资源,如内存、处理器时间、文件和输入/输出等。

  3. 存储管理模块:存储管理是操作系统中负责管理计算机内存的模块。它负责将内存分配给进程、跟踪空闲内存块、处理内存碎片和实现虚拟内存等功能。存储管理模块还负责内存保护、内存映射和页面置换等操作。

  4. 设备管理模块:设备管理是操作系统中负责管理计算机硬件设备的模块。它包括设备的分配、控制和调度等操作。设备管理模块还负责处理中断、处理设备故障和提供设备驱动程序等功能。

除了上述模块外,还有其他一些重要的操作系统模块,如用户界面模块、网络管理模块、安全管理模块等,它们共同协作以提供操作系统的各种功能和服务。


C语言为什么不能重载?

C语言不支持函数重载的主要原因是它的函数调用机制是通过函数名来确定函数的入口地址,而不考虑函数的参数类型和个数。当存在多个同名函数时,编译器无法根据函数名来区分它们,从而无法确定要调用的具体函数。


预编译的过程?

预编译是编译过程的第一个阶段,它由预处理器(Preprocessor)执行。预编译的主要任务是对源代码进行一系列的预处理操作,以生成最终编译所需的代码。

预编译的过程包括以下几个步骤:

  1. 文件包含(File Inclusion):预处理器根据#include指令将其他文件的内容包含到当前文件中。这样可以将代码模块化,并提供代码的组织和复用。

  2. 宏替换(Macro Substitution):预处理器会根据预定义的宏定义,将代码中的宏名称替换为相应的值或代码片段。宏替换可以简化代码,提高代码的重用性和可维护性。

  3. 条件编译(Conditional Compilation):预处理器支持通过条件判断来选择性地编译特定的代码块。使用#ifdef#ifndef#if等条件编译指令,可以根据预定义的宏的状态来控制代码的编译。

  4. 注释处理(Comment Handling):预处理器会删除代码中的注释,以减小编译后的代码大小。

  5. 符号常量定义(Symbol Constant Definition):预处理器支持使用#define指令定义符号常量,以便在代码中使用。符号常量的使用可以提高代码的可读性和可维护性。

  6. 条件表达式求值(Conditional Expression Evaluation):预处理器会对条件表达式进行求值,以确定哪些代码块应该被编译。

预编译的结果是生成了经过预处理操作的代码,这些代码将作为编译器的输入。预编译的目的是为了提供更灵活、可配置和可重用的代码,并为后续的编译过程做准备。


多态的实现机制是什么,假设让你用C语言模拟出来,你会怎么写?

多态的实现机制是通过虚函数(Virtual Function)和动态绑定(Dynamic Binding)来实现的。在面向对象编程中,通过基类指针或引用调用派生类对象的函数时,会根据实际对象的类型来动态地绑定到相应的函数。

在C语言中,没有直接支持虚函数和动态绑定的机制,但可以通过一些手动的方式来模拟实现多态的效果。以下是一种可能的实现方式:

  1. 创建函数指针表:为基类和派生类定义一个函数指针表,其中包含了类中所有的虚函数的指针。

  2. 在基类中定义虚函数指针表:在基类中定义一个虚函数指针表,该表存储了所有虚函数的指针,并且根据派生类的实际类型来指向相应的函数。

  3. 在派生类中实现虚函数:在派生类中重写基类的虚函数,并将派生类的函数指针赋值给虚函数指针表中对应的位置。

  4. 手动进行动态绑定:在实际调用时,通过基类指针或引用获取到对象的虚函数指针表,然后根据虚函数的索引调用相应的函数。

下面是一个简单的示例代码,展示了用C语言模拟实现多态的方法:

#include <stdio.h>

// 基类
typedef struct {
    void (*virtual_func)();
} Base;

// 派生类1
typedef struct {
    Base base;
} Derived1;

// 派生类2
typedef struct {
    Base base;
} Derived2;

// 定义虚函数
void base_virtual_func(Base *obj) {
    printf("Base virtual function\n");
}

void derived1_virtual_func(Derived1 *obj) {
    printf("Derived1 virtual function\n");
}

void derived2_virtual_func(Derived2 *obj) {
    printf("Derived2 virtual function\n");
}

int main() {
    Base *baseObj;
    Derived1 derived1Obj;
    Derived2 derived2Obj;

    // 设置虚函数指针
    baseObj = (Base *)&derived1Obj;
    baseObj->virtual_func = (void (*)())derived1_virtual_func;

    // 调用虚函数
    baseObj->virtual_func();

    // 修改虚函数指针
    baseObj = (Base *)&derived2Obj;
    baseObj->virtual_func = (void (*)())derived2_virtual_func;

    // 调用虚函数
    baseObj->virtual_func();

    return 0;
}

这个示例代码中,通过在基类和派生类中定义函数指针表,并手动进行函数指针的赋值和调用,实现了类似多态的效果。但需要注意的是,这种手动模拟的方式较为繁琐,容易出错,并且不具备C++中编译器提供的类型检查和自动绑定的优势。因此,如果需要实现多态的功能,建议使用C++或其他支持多态的高级编程语言。


在内核态两个进程操作一个device有什么方法可以处理进程间同步的问题呢?(进程同步实现,即进程通信)

  1. 互斥锁(Mutex):使用互斥锁可以确保在同一时间只有一个进程可以访问设备。进程在访问设备之前,需要先获取互斥锁,如果锁已经被其他进程持有,则当前进程会被阻塞,直到锁被释放为止。
  2. 信号量(Semaphore):信号量提供了一种计数机制,可以用来控制多个进程对设备的访问。进程在访问设备之前,需要通过信号量进行申请,如果信号量的计数大于0,则表示设备可用,进程可以继续执行访问操作;如果计数为0,则表示设备被其他进程占用,当前进程会被阻塞,直到有其他进程释放信号量。
  3. 读写锁(Read-Write Lock):读写锁允许多个进程同时读取设备的内容,但只允许一个进程进行写操作。读写锁可以提高并发性能,适用于读操作频繁的场景。
  4. 条件变量(Condition Variable):条件变量用于在某个特定条件满足时唤醒等待的进程。可以使用条件变量来实现进程间的通信和同步,例如,一个进程等待设备上的某个事件发生,另一个进程在事件发生时通过条件变量唤醒等待的进程。
  5. 管道(Pipe)或消息队列(Message Queue):管道或消息队列可以用于进程间的通信和同步。一个进程可以将消息发送到管道或消息队列,另一个进程可以从中接收消息,并根据消息来进行相应的操作。

智能指针

智能指针是一种C++中的工具,用于管理动态分配的内存,并在不再需要时自动释放内存,从而避免内存泄漏。它们提供了一种自动化的内存管理方式,可以减轻手动管理指针带来的负担。

在传统的C++中,我们使用原始指针(Raw Pointer)来管理动态分配的内存。然而,原始指针容易导致内存泄漏、空悬指针和二次释放等问题。为了解决这些问题,C++标准库提供了三种智能指针:unique_ptrshared_ptrweak_ptr

  1. unique_ptr

    • unique_ptr是一种独占式智能指针,它拥有对动态分配的对象的唯一所有权。
    • unique_ptr超出其作用域或被显式释放时,会自动释放其所管理的对象的内存。
    • unique_ptr不能被复制,但可以通过std::move来转移所有权。
    • unique_ptr通常用于管理单个对象的动态内存。
  2. shared_ptr

    • shared_ptr是一种共享式智能指针,它允许多个shared_ptr共享对同一对象的所有权。
    • shared_ptr使用引用计数来跟踪有多少个shared_ptr指向同一对象,在最后一个共享指针被销毁时释放内存。(如果要删除时,并不会立即删除,而是看要被删除的指针是否是该对象的最后一个共享指针,如果是,才会删除对象)
    • shared_ptr可以通过std::make_shared来创建,也可以使用std::shared_ptr构造函数来创建。
    • shared_ptr的使用需要注意避免循环引用,可能导致内存泄漏。
  3. weak_ptr

    • weak_ptr是一种弱引用智能指针,它指向由shared_ptr管理的对象,但不增加引用计数。
    • weak_ptr可以用来解决shared_ptr的循环引用问题,它不会导致循环引用而造成内存泄漏。
    • weak_ptr可以通过shared_ptrlock()函数来获取一个指向对象的shared_ptr

智能指针通过封装原始指针,并提供自动内存管理的功能,大大简化了内存管理的工作。它们提供了更安全、更方便的内存管理方式,并减少了内存泄漏和悬空指针等错误的发生。在实际编程中,推荐使用智能指针来管理动态内存,以提高代码的可靠性和可维护性。

  1. unique_ptr的示例:
#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr(new int(5));

    std::cout << *ptr << std::endl;  // 输出:5(指针要解引用才能访问值)

    // unique_ptr不能被复制,但可以通过std::move来转移所有权
    std::unique_ptr<int> ptr2 = std::move(ptr);

    if (ptr) {
        std::cout << "ptr is not null" << std::endl;
    } else {
        std::cout << "ptr is null" << std::endl;
    }

    std::cout << *ptr2 << std::endl;  // 输出:5

    // unique_ptr会在超出作用域时自动释放内存
    return 0;
}

在上述示例中,我们使用unique_ptr来管理动态分配的int对象。unique_ptr拥有对对象的唯一所有权,当unique_ptr超出作用域时,会自动释放内存。

  1. shared_ptr的示例:
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor" << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destructor" << std::endl;
    }
};

int main() {
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
    std::shared_ptr<MyClass> ptr2 = ptr1;  // 共享所有权

    std::cout << "Number of shared pointers: " << ptr1.use_count() << std::endl;  // 输出:2

    ptr1.reset();  // 释放ptr1所管理的对象

    std::cout << "Number of shared pointers: " << ptr2.use_count() << std::endl;  // 输出:1

    // shared_ptr会在最后一个共享指针被销毁时释放内存
    return 0;
}

在上述示例中,我们使用shared_ptr来管理MyClass对象。shared_ptr允许多个shared_ptr共享对同一对象的所有权,使用引用计数来跟踪共享的数量。当最后一个shared_ptr被销毁时,会释放内存。

  1. weak_ptr的示例:
#include <iostream>
#include <memory>

class MyClass;

std::shared_ptr<MyClass> globalPtr;

class MyClass {
public:
    std::weak_ptr<MyClass> weakPtr;

    ~MyClass() {
        std::cout << "MyClass destructor" << std::endl;
    }
};

int main() {
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
    std::shared_ptr<MyClass> ptr2 = std::make_shared<MyClass>();

    ptr1->weakPtr = ptr2;  // 使用weak_ptr来解决循环引用问题
    ptr2->weakPtr = ptr1;

    globalPtr = ptr1;  // 全局变量持有shared_ptr的引用

    std::cout << "Number of shared pointers to MyClass: " << ptr1.use_count() << std::endl;  // 输出:2

    if (auto sharedPtr = ptr1->weakPtr.lock()) {
        std::cout << "Accessing shared_ptr from weak_ptr" << std::endl;
        // 对象仍然存在,可以通过weak_ptr获取shared_ptr并使用
    } else {
        std::cout << "Object no longer exists" << std::endl;
    }

    // weak_ptr不会增加引用计数,不会阻止对象的销毁

    return 0;
}

在上述示例中,我们使用weak_ptr解决了两个对象之间的循环引用问题。weak_ptr不会增加引用计数,当最后一个shared_ptr被销毁时,对象会被正确地释放。

这些示例展示了智能指针的使用方式以及它们在内存管理中的作用。通过使用智能指针,我们可以避免手动释放内存、防止内存泄漏和悬空指针等问题,提高代码的可靠性和可维护性。


同步和异步

在计算机编程中,同步(Synchronous)和异步(Asynchronous)是用来描述不同的执行模式或通信方式。

同步指的是程序按照顺序依次执行,每个操作需要等待上一个操作完成后才能进行。在同步模式下,程序会阻塞等待结果返回,直到得到结果后再继续执行下一个操作。

异步则是指程序在发起一个操作后,不需要等待其完成,而是可以继续执行后续的操作。在异步模式下,程序不会阻塞等待结果,而是通过回调函数、事件触发或轮询等方式来处理操作的完成。

举个例子来说明:

假设有一个下载文件的操作,同步方式下,程序会发起下载请求后一直等待,直到文件下载完成后才能进行下一步操作。而在异步方式下,程序可以发起下载请求后继续执行其他操作,然后在下载完成后通过回调函数或事件来处理下载结果。

同步和异步的选择取决于具体的需求和场景。同步方式简单直观,但可能会导致程序阻塞,特别是在处理大量并发请求时。异步方式可以提高程序的并发性和响应性,但对于编程和调试来说可能更加复杂。

需要根据具体情况来选择同步或异步模式,以达到最佳的性能和用户体验。


反问面试官

我想问问贵公司主要做什么业务?工作内容?

我想问问在贵公司的这个工作节奏大概是什么样子呢?比如说每天什么时候上班,什么时候下班呢 ?

你觉得在贵公司工作,如果满分是10分的话你会给到多少的幸福指数 ?

我想问一下我作为一个应届生进入到贵公司,贵公司会如何做一个培养呢?


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OutlierLi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值