当前位置: 首页 > news >正文

4.22tx视频后台开发一面

总时长大概在一个小时,主要提问C++、操作系统、计网以及数据库等方面,最后两个算法编程题。

一上来先介绍项目

Linux下的mybash命令处理器和内存池

mybash可以再总结归纳一下,一上来有点紧张没有条理

内存池是用边界标识法写的,大概介绍了一下,问了知不知道伙伴系统,讲一下

提问了linux下的内存管理

Linux 下的内存管理是一个复杂而重要的系统功能,以下是其相关介绍:

  • 内核空间
    • 内核空间是操作系统内核运行的区域,它拥有最高的权限,可以访问系统的所有资源。
    • 内核空间占用了一部分物理内存,用于存储内核代码、数据结构以及内核态下的各种进程和线程。
    • 内核空间中的内存管理由内核负责,它通过一系列的数据结构和算法来分配、释放和管理内存,以确保内核的稳定运行和系统资源的有效利用。
  • 用户空间
    • 用户空间是用户进程运行的区域,每个用户进程都有自己独立的用户空间。
    • 用户空间的大小通常由操作系统和硬件平台决定,例如,在 32 位的 Linux 系统中,用户空间一般为 3GB,而在 64 位系统中,用户空间则可以达到更大的范围。
    • 用户空间中的内存管理由用户进程自己负责,进程可以通过系统调用请求操作系统分配内存,也可以使用标准的 C 库函数如mallocfree等来管理内存。

内存管理的主要功能

  • 内存分配与回收:负责为进程分配内存空间,确保进程有足够的内存来运行。当进程结束或不再需要某些内存时,及时回收这些内存,以便重新分配给其他进程,提高内存利用率。
  • 地址转换:将进程使用的虚拟地址转换为物理地址。每个进程都有自己独立的虚拟地址空间,通过地址转换机制,使得不同进程的虚拟地址可以映射到不同的物理内存区域,保证进程之间的内存相互隔离,提高系统的稳定性和安全性。
  • 内存保护:防止进程非法访问或修改其他进程的内存空间,以及保护操作系统自身的内存区域不被用户进程随意篡改。通过设置内存访问权限,如只读、可写、可执行等,确保内存的正确使用。
  • 内存共享:允许不同进程共享同一块物理内存区域,以实现进程间通信和数据共享,提高内存使用效率,减少内存浪费。
  • 缓存管理:利用内存作为缓存,加速对磁盘等慢速存储设备的数据访问。例如,将经常访问的文件数据缓存到内存中,下次访问时可以直接从内存中读取,大大提高了访问速度。

内存管理的实现方式

  • 虚拟内存:Linux 通过虚拟内存技术为每个进程提供了一个独立的、连续的虚拟地址空间。虚拟地址空间被划分为多个页,每页大小通常为 4KB 或更大。物理内存也被划分为同样大小的页帧。通过页表等数据结构,将虚拟页映射到物理页帧上。当进程访问的虚拟地址不在物理内存中时,会触发缺页中断,操作系统将相应的页面从磁盘交换到物理内存中。
  • 内存分配算法:Linux 采用了多种内存分配算法,如伙伴系统算法和 slab 分配器。伙伴系统算法主要用于管理较大的内存块分配,它将内存空间按照不同的大小级别进行划分,当需要分配内存时,从合适的级别中查找空闲块并进行分配。slab 分配器则主要用于为内核对象分配小内存,它预先分配一些内存缓冲区,并将其划分为多个小的对象空间,当需要创建内核对象时,直接从相应的 slab 缓冲区中分配空间,提高了分配效率。
  • 内存回收机制:当物理内存不足时,Linux 会启动内存回收机制。通过扫描内存中的页面,将不常用的页面交换到磁盘上的交换空间中,以释放物理内存供其他进程使用。同时,也会根据页面的使用频率和最近访问时间等因素,选择合适的页面进行回收。

内存管理相关的命令和工具

  • free 命令:用于查看系统内存的使用情况,包括总内存、已使用内存、空闲内存、缓冲区和缓存使用的内存等信息。
  • top 命令:可以实时显示系统中各个进程的资源占用情况,包括内存占用、CPU 使用率等,帮助用户了解系统中内存消耗较大的进程。
  • vmstat 命令:提供了关于系统虚拟内存、进程、CPU 等方面的统计信息,对于分析内存性能和系统整体运行状况有很大帮助。
  • strace 命令:可以跟踪进程的系统调用,查看进程对内存的分配、释放等操作,有助于调试内存相关的问题。

C/C++

1.C和C++的区别

C 和 C++ 均为广泛应用的编程语言,二者存在诸多不同,下面为你详细介绍:

1. 语言范式

  • C 语言:属于面向过程的编程语言,其核心在于对过程的抽象,把大问题分解成若干小问题,每个小问题由一个函数来解决。程序的执行过程就是一系列函数的调用过程。

c

#include <stdio.h>// 函数用于计算两个整数的和
int add(int a, int b) {return a + b;
}int main() {int result = add(3, 5);printf("The result is: %d\n", result);return 0;
}
  • C++ 语言:是一种支持多种编程范式的语言,涵盖面向过程、面向对象、泛型编程等。面向对象编程将数据和操作数据的方法封装成类,通过类的实例(对象)来实现程序功能。

cpp

#include <iostream>// 定义一个类
class Calculator {
public:int add(int a, int b) {return a + b;}
};int main() {Calculator calc;int result = calc.add(3, 5);std::cout << "The result is: " << result << std::endl;return 0;
}

2. 输入输出方式

  • C 语言:主要使用标准输入输出函数,如scanfprintf

c

#include <stdio.h>int main() {int num;printf("Please enter a number: ");scanf("%d", &num);printf("You entered: %d\n", num);return 0;
}
  • C++ 语言:采用流对象cincout进行输入输出操作,使用起来更加方便,且类型安全。

cpp

#include <iostream>int main() {int num;std::cout << "Please enter a number: ";std::cin >> num;std::cout << "You entered: " << num << std::endl;return 0;
}

3. 内存管理

  • C 语言:使用malloccallocrealloc进行内存分配,使用free释放内存。

c

#include <stdio.h>
#include <stdlib.h>int main() {int *arr = (int *)malloc(5 * sizeof(int));if (arr == NULL) {printf("Memory allocation failed\n");return 1;}for (int i = 0; i < 5; i++) {arr[i] = i;}for (int i = 0; i < 5; i++) {printf("%d ", arr[i]);}free(arr);return 0;
}
  • C++ 语言:除了可以使用 C 语言的内存管理方式,还引入了newdelete运算符。new用于动态分配内存,delete用于释放内存。

cpp

#include <iostream>int main() {int *arr = new int[5];for (int i = 0; i < 5; i++) {arr[i] = i;}for (int i = 0; i < 5; i++) {std::cout << arr[i] << " ";}delete[] arr;return 0;
}

4. 函数重载

  • C 语言:不支持函数重载,即不能定义同名但参数不同的函数。
  • C++ 语言:支持函数重载,允许定义多个同名但参数列表不同的函数,编译器会根据调用时的实参类型和数量来选择合适的函数。

cpp

#include <iostream>// 函数重载示例
int add(int a, int b) {return a + b;
}double add(double a, double b) {return a + b;
}int main() {int intResult = add(3, 5);double doubleResult = add(3.2, 5.6);std::cout << "Integer result: " << intResult << std::endl;std::cout << "Double result: " << doubleResult << std::endl;return 0;
}

5. 类和对象

  • C 语言:没有类和对象的概念,主要通过结构体来组织数据。结构体只能包含数据成员,不能包含成员函数。

c

#include <stdio.h>// 定义一个结构体
struct Point {int x;int y;
};void printPoint(struct Point p) {printf("(%d, %d)\n", p.x, p.y);
}int main() {struct Point p = {3, 5};printPoint(p);return 0;
}
  • C++ 语言:引入了类和对象的概念,类是一种用户自定义的数据类型,它可以包含数据成员和成员函数。

cpp

#include <iostream>// 定义一个类
class Point {
private:int x;int y;
public:Point(int x, int y) : x(x), y(y) {}void print() {std::cout << "(" << x << ", " << y << ")" << std::endl;}
};int main() {Point p(3, 5);p.print();return 0;
}

6. 异常处理

  • C 语言:没有内置的异常处理机制,通常通过返回错误码来处理错误。

c

#include <stdio.h>
#include <stdlib.h>int divide(int a, int b, int *result) {if (b == 0) {return -1; // 返回错误码}*result = a / b;return 0;
}int main() {int a = 10, b = 0, result;if (divide(a, b, &result) == -1) {printf("Error: division by zero\n");} else {printf("Result: %d\n", result);}return 0;
}
  • C++ 语言:提供了异常处理机制,使用trycatchthrow关键字来捕获和处理异常。

cpp

#include <iostream>int divide(int a, int b) {if (b == 0) {throw "Division by zero!"; // 抛出异常}return a / b;
}int main() {int a = 10, b = 0;try {int result = divide(a, b);std::cout << "Result: " << result << std::endl;} catch (const char* msg) {std::cerr << "Error: " << msg << std::endl;}return 0;
}

7. 模板

  • C 语言:不支持模板。
  • C++ 语言:支持模板,模板是泛型编程的基础,它允许编写与类型无关的代码。例如,标准模板库(STL)就是基于模板实现的。

cpp

#include <iostream>// 模板函数示例
template <typename T>
T max(T a, T b) {return (a > b) ? a : b;
}int main() {int intMax = max(3, 5);double doubleMax = max(3.2, 5.6);std::cout << "Integer max: " << intMax << std::endl;std::cout << "Double max: " << doubleMax << std::endl;return 0;
}

综上所述,C++ 是 C 语言的扩展,在 C 语言的基础上增加了许多新特性,使得代码更加灵活、可维护和可扩展。但 C 语言由于其简洁性和高效性,在嵌入式系统、操作系统等领域仍然有广泛的应用。

2.C++优势体现在哪些方面

性能高效

  • 接近硬件:C++ 能直接操作内存、寄存器等硬件资源,通过指针可对内存进行精确控制,减少不必要的内存开销。例如在开发操作系统内核时,需要直接管理内存分配和释放,C++ 可以满足这种对硬件的直接操作需求,从而实现高效的系统性能。
  • 执行速度快:C++ 代码编译后生成的机器码执行效率高,在需要处理大量数据和复杂计算的场景中,如金融领域的高频交易系统,需要在短时间内完成大量的交易数据处理和计算,C++ 的高性能可以保证系统的实时性和响应速度。

功能强大

  • 支持多种编程范式:C++ 支持面向过程、面向对象、泛型编程和函数式编程等多种编程范式。例如,在开发一个游戏引擎时,可以使用面向对象编程来设计游戏中的角色、场景等类,使用泛型编程来实现通用的数据结构和算法,提高代码的复用性和可维护性。
  • 丰富的标准库:C++ 标准库提供了丰富的容器(如 vectormap 等)、算法(如 sortfind 等)和输入输出流等功能,开发者可以直接使用这些库来提高开发效率。例如,在开发一个文件管理系统时,可以使用标准库中的文件流来进行文件的读写操作,使用容器来存储文件信息。

可移植性强

  • 跨平台开发:C++ 代码可以在不同的操作系统(如 Windows、Linux、macOS 等)和硬件平台上进行编译和运行,只需要根据不同的平台进行适当的调整和优化。例如,一款跨平台的办公软件可以使用 C++ 开发,在不同的操作系统上都能提供一致的用户体验。
  • 代码复用:C++ 的代码可以在不同的项目中进行复用,通过将通用的功能封装成库,可以在多个项目中共享和使用,减少开发成本和时间。

应用广泛

  • 系统软件:C++ 常用于开发操作系统、编译器、数据库管理系统等系统软件。例如,Windows 操作系统、GCC 编译器等都是使用 C++ 开发的。
  • 游戏开发:许多大型游戏引擎(如 Unreal Engine、Unity 等)都使用 C++ 作为主要的开发语言,因为 C++ 可以提供高性能的图形渲染和物理模拟等功能。
  • 嵌入式系统:在嵌入式系统开发中,C++ 可以直接操作硬件资源,满足嵌入式系统对性能和实时性的要求。例如,智能家居设备、汽车电子系统等都广泛使用 C++ 进行开发。

社区支持和生态系统

  • 庞大的开发者社区:C++ 拥有庞大的开发者社区,开发者可以在社区中分享经验、交流技术、解决问题。例如,Stack Overflow 等技术论坛上有大量关于 C++ 的问题和解决方案。
  • 丰富的第三方库和工具:有许多优秀的第三方库和工具可供 C++ 开发者使用,如 Boost 库、OpenCV 库等,这些库可以扩展 C++ 的功能,提高开发效率。

3.C++引用和指针的区别

在C++ 中,引用和指针有以下区别:

定义与性质

- 引用:是一个已存在变量的别名,它在定义时必须初始化,并且一旦初始化后就不能再引用其他变量。例如, int a = 5; int& ref = a; , ref 就是 a 的引用。

- 指针:是一个变量,其值是另一个变量的地址。指针可以在定义后再赋值,也可以指向不同的变量。例如, int a = 5; int* ptr = &a; , ptr 指向 a 的地址,之后可以让 ptr 指向其他 int 类型的变量。

操作符

- 引用:使用时就像使用普通变量一样,直接访问其引用的变量的值,不需要使用特殊的操作符来解引用。例如, ref = 10;  就是直接修改 a 的值。

- 指针:访问指针所指向的变量的值时,需要使用 * 操作符进行解引用。例如, *ptr = 10;  才能修改 a 的值。

内存分配

- 引用:不占用额外的内存空间来存储地址信息,它只是被引用变量的一个别名

- 指针:本身需要占用一定的内存空间来存储所指向变量的地址,在32位系统中通常占4个字节,在64位系统中通常占8个字节。

空值

- 引用:不能指向空值,必须始终引用一个有效的对象。

- 指针:可以赋值为 nullptr ,表示不指向任何有效的对象。在使用指针之前,通常需要检查它是否为 nullptr ,以避免空指针引用错误。

应用场景

- 引用:常用于函数参数传递和返回值,以避免对象的拷贝,提高效率。例如,函数 void func(int& num) ,传递的是变量的引用,在函数中对 num 的修改会影响到原变量。

- 指针:在需要动态分配内存、实现数据结构(如链表、树)以及进行底层内存操作等场景中广泛使用。例如, int* arr = new int[10];  通过指针动态分配了一个包含10个 int 类型元素的数组。

4.指针和数组的区别

概念

- 指针:是一个变量,用于存储内存地址,通过该地址可以间接访问和操作所指向的内存空间中的数据。

- 数组:是一种复合数据类型,它由多个相同类型的元素组成,这些元素在内存中连续存储,通过数组名和下标来访问各个元素。

定义与初始化

- 指针:定义时需要指定所指向的数据类型,如 int* p; 。初始化可以将其指向已存在的变量,如 int a = 5; int* p = &a; ,也可以初始化为 nullptr ,即 int* p = nullptr; 。

- 数组:定义时需要指定数组的类型和大小,如 int arr[5]; 。初始化可以采用静态初始化,如 int arr[5] = {1, 2, 3, 4, 5}; ,也可以在定义后逐个元素赋值。

内存分配

- 指针:本身占用一定的内存空间来存储地址,其指向的内存空间可以通过 new 运算符动态分配,如 int* p = new int; ,也可以指向静态分配或栈上的变量。

- 数组:数组的内存是在定义时静态分配在栈上,或使用 new 动态分配在堆上。例如, int* arr = new int[10]; 在堆上分配了一个包含10个 int 类型元素的数组。

访问方式

- 指针:通过解引用操作符 * 来访问所指向的变量,如 *p = 10; 。也可以通过指针的算术运算来访问连续内存空间中的数据,如 p++ 指向下一个同类型的内存地址。

- 数组:通过数组名和下标来访问元素,如 arr[2] = 10; 。数组名在表达式中会隐式转换为指向数组首元素的指针,因此也可以使用指针的方式访问数组元素,如 *(arr + 2) = 10; 。

sizeof 操作符

- 指针:在32位系统中, sizeof 一个指针通常为4字节,在64位系统中通常为8字节,与指针所指向的数据类型无关。

- 数组: sizeof 数组得到的是整个数组所占用的内存空间大小,单位是字节。例如, int arr[5]; , sizeof(arr) 的值为 20 (假设 int 类型占4字节)。

可变性

- 指针:指针的值(即所指向的地址)可以改变,使其指向不同的内存空间。例如, int a = 5, b = 6; int* p = &a; p = &b; ,指针 p 先指向 a ,后指向 b 。

- 数组:数组名代表数组的首地址,是一个常量指针,其值不能被修改。例如, int arr[5]; arr++; 是错误的操作。

虽然在某些情况下指针和数组有相似的表现,但它们在概念、内存管理、操作特性等方面存在明显的区别。

5.内联函数和普通函数的区别

在C++ 中,内联函数和普通函数有以下区别:

函数调用机制

- 普通函数:调用时会产生函数调用的开销,包括保存现场、传递参数、跳转到函数地址执行代码以及返回结果后恢复现场等操作。

- 内联函数:在编译时,编译器会将内联函数的代码直接嵌入到调用处,避免了函数调用的开销,提高了程序的执行效率。

代码膨胀

- 普通函数:无论被调用多少次,在目标代码中只有一份函数代码。

- 内联函数:由于代码会被嵌入到每个调用处,如果内联函数体较大且被频繁调用,可能会导致目标代码体积增大,即代码膨胀。

定义位置

- 普通函数:可以在源文件的任何位置定义,只要在调用前进行声明即可。

- 内联函数:通常建议将内联函数的定义放在头文件中,因为编译器需要在编译调用该函数的代码时看到内联函数的定义,以便进行代码嵌入。

适用场景

- 普通函数:适用于代码逻辑复杂、函数体较大、不频繁调用的情况。

- 内联函数:适用于函数体简单、执行速度要求高、被频繁调用的情况,如一些简单的访问器函数、小型的辅助函数等。

编译器处理

- 普通函数:编译器按照正常的函数调用规则进行编译和链接。

- 内联函数:编译器会根据内联函数的定义和编译优化策略,决定是否将其真正展开为内联代码。即使函数被声明为内联,编译器也可能因为某些原因(如函数体过于复杂)而不进行内联展开。

6.内联函数和普通函数的使用场景有哪些

C++中内联函数和普通函数的使用场景分别如下:

内联函数

- 频繁调用的小函数:例如获取对象属性的访问器函数,像 getX()   getY() 等,将它们定义为内联函数可以避免函数调用开销,提高程序执行效率。

- 用于替代宏定义:宏定义存在一些问题,如没有类型检查、可能导致代码可读性差等。内联函数可以在提供类似宏的代码嵌入效果的同时,进行类型检查,增强代码的安全性和可读性。

普通函数

- 复杂逻辑函数:当函数体包含大量的代码逻辑、循环、条件判断等复杂操作时,使用普通函数可以使代码结构更清晰,避免内联带来的代码膨胀问题。

- 不频繁调用的函数:对于不经常被调用的函数,函数调用的开销相对来说影响较小,使用普通函数可以保持代码的简洁性和可维护性。

- 需要动态链接的函数:在一些动态链接库的场景中,普通函数更适合,因为内联函数的代码嵌入特性可能会导致在不同模块间的链接出现问题。

7.C++的多态以及优缺点

C++ 多态是指在不同的情况下,一个函数或对象可以表现出不同的行为。C++ 多态主要有以下两种形式:

编译时多态(静态多态)

- 函数重载:在同一个作用域内,可以有多个同名函数,但函数的参数列表(参数个数、类型或顺序)不同。编译器会根据调用时传递的参数类型和个数来确定调用哪个函数。例如:

int add(int a, int b) {

    return a + b;

}

double add(double a, double b) {

    return a + b;

}

- 模板:通过模板可以定义通用的函数或类,在编译时根据实际使用的类型生成具体的代码。例如,下面是一个模板函数,用于交换两个变量的值:

template <typename T>

void swap(T& a, T& b) {

    T temp = a;

    a = b;

    b = temp;

}

运行时多态(动态多态)

- 虚函数:在基类中使用 virtual 关键字声明的函数,在派生类中可以重写。通过基类指针或引用调用虚函数时,会根据对象的实际类型来决定调用哪个派生类的重写版本。例如:

class Shape {

public:

    virtual double area() const {

        return 0.0;

    }

};

class Circle : public Shape {

public:

    Circle(double r) : radius(r) {}

    double area() const override {

        return 3.14159 * radius * radius;

    }

private:

    double radius;

};

在上述代码中, Shape 类中的 area 函数是虚函数, Circle 类重写了 area 函数。当使用 Shape 类的指针或引用调用 area 函数时,如果指向的是 Circle 对象,就会调用 Circle 类中重写的 area 函数,实现运行时多态。

优点

- 提高代码的可维护性和可扩展性:当需要添加新的功能或修改现有功能时,只需要在相应的派生类中进行修改或添加,而不需要修改大量的调用代码。例如,在图形绘制系统中,添加新的图形类型,只需增加一个派生类并重写相关的绘制函数,不影响其他图形的绘制代码。

- 增强代码的复用性:通过多态,不同的派生类可以共享基类的接口,实现代码的复用。如多个不同类型的文件操作类,都可以继承自一个基类,共享一些通用的文件打开、关闭等操作代码,同时各自实现特定的读写功能。

- 实现灵活的程序设计:多态允许在运行时根据对象的实际类型来决定执行的操作,使程序具有更高的灵活性和动态性。例如,在游戏开发中,根据不同的角色类型,在运行时调用不同的行为函数。

缺点

- 增加程序的复杂性:多态涉及到虚函数、继承等概念,使得程序的结构和执行流程变得复杂,增加了理解和调试的难度。例如,在多层继承结构中,查找虚函数的调用路径可能会比较复杂。

- 可能导致性能开销:运行时多态通过虚函数表来实现,在调用虚函数时需要额外的查找虚函数表的操作,会带来一定的性能开销。虽然现代编译器会对此进行一些优化,但在一些对性能要求极高的场景下,可能会产生影响。

- 对代码可读性有一定影响:多态使得代码中的函数调用可能在运行时才能确定具体执行的代码,这可能会影响代码的可读性,尤其是对于不熟悉多态机制的开发者来说。

8.纯虚函数

纯虚函数是在基类中声明的虚函数,它没有函数体,只有函数声明,用于规范派生类的行为。以下是关于纯虚函数的详细介绍:

定义格式

在函数声明后面加上 = 0 ,例如:

class AbstractClass {

public:

    virtual void pureVirtualFunction() = 0;

};

特点

- 必须在派生类中实现:包含纯虚函数的类是抽象类,不能实例化对象。派生类必须实现基类中的纯虚函数,否则派生类也会成为抽象类。

- 作为接口规范:纯虚函数定义了一组派生类必须遵循的接口,使得不同派生类具有统一的行为模式。

作用

- 实现多态性:通过在派生类中重写纯虚函数,实现运行时多态。当使用基类指针或引用调用纯虚函数时,会根据对象的实际类型调用相应派生类的实现。

- 设计抽象基类:用于定义抽象基类,抽象基类提供了一种通用的类型框架,强调派生类应具备的功能,而不关注具体实现。

例如,在一个图形绘制系统中,可以定义一个抽象基类 Shape ,其中包含纯虚函数 draw ,不同的图形类如 Circle 、 Rectangle 等作为派生类,各自实现 draw 函数来绘制具体的图形。这样,通过基类指针或引用就可以根据实际对象类型调用不同图形的绘制函数,实现多态性。

9.什么函数不能是虚函数

以下几种函数不能是虚函数:

- 非类成员函数:虚函数是类的成员函数,用于实现类的多态性,普通的全局函数或静态函数不属于类的成员,不能定义为虚函数。

- 构造函数:构造函数用于创建对象并初始化其成员变量,在对象创建时被调用。虚函数的调用需要通过对象的虚函数表来实现,而在构造函数执行时,对象还未完全初始化,虚函数表也未正确设置,所以构造函数不能是虚函数。

- 静态成员函数:静态成员函数属于类,而不属于类的某个对象,它不依赖于对象的状态,没有 this 指针。虚函数的调用与对象的动态类型相关,所以静态成员函数不能是虚函数。

- 内联函数:内联函数是在编译时将函数体插入到调用处,以减少函数调用开销。虚函数是在运行时根据对象的实际类型来确定调用的函数版本,这与内联函数的编译时展开机制相冲突。不过,内联函数可以调用虚函数。

- 友元函数:友元函数不是类的成员函数,它可以访问类的私有和保护成员,但不具有多态性的特点,因此不能定义为虚函数。

10.普通函数可以是虚函数吗

普通函数不能是虚函数。虚函数是类的成员函数,具有特定的多态性机制,需要通过类的对象或指针来调用,并且在运行时根据对象的实际类型确定具体调用的函数版本。而普通函数不属于类的成员,没有关联的类对象和虚函数表等多态实现机制,因此不能被声明为虚函数。

11.普通函数和虚函数的区别是什么

普通函数和虚函数有以下区别:

定义与声明

- 普通函数:在类内或类外直接定义,在类外定义时需指明所属类(如果是类成员函数)。

- 虚函数:在基类中使用 virtual 关键字声明,派生类中重写时也可加 virtual (非必需,但建议加上)。

调用机制

- 普通函数:根据函数名和参数在编译时确定调用的具体函数,属于静态绑定。

- 虚函数:通过基类指针或引用调用时,在运行时根据对象的实际类型确定调用的函数版本,是动态绑定。

多态性

- 普通函数:不支持多态,同名函数仅靠函数重载实现不同功能,但本质上是不同的函数,根据参数类型和个数选择调用。

- 虚函数:是实现多态的关键,派生类可重写虚函数,以实现不同的行为,在运行时根据对象的动态类型来调用相应的函数。

函数覆盖与隐藏

- 普通函数:在派生类中定义与基类同名但参数不同的函数,会隐藏基类中的函数,而不是覆盖。

- 虚函数:派生类中重写虚函数要求函数名、参数列表、返回值类型(协变返回类型除外)都相同,否则编译错误。

性能

- 普通函数:调用速度快,因为编译时就确定了调用地址,无额外开销。

- 虚函数:因运行时要通过虚函数表查找函数地址,有一定性能开销,但现代编译器会进行优化。

应用场景

- 普通函数:适用于功能明确且固定,不需要在运行时根据不同情况改变行为的场景。

- 虚函数:在设计具有多种不同实现的抽象行为,需要根据对象的实际类型动态选择行为时使用。如图形绘制系统中不同图形的绘制方法。

操作系统

1.linux多线程和多进程的区别

Linux中多进程和多线程有以下区别:

资源分配

- 多进程:每个进程都有独立的地址空间、代码段、数据段和堆栈等资源,进程间的资源相互隔离,一个进程的崩溃一般不会影响其他进程。

- 多线程:同一进程内的多个线程共享进程的地址空间、代码段、数据段等资源,但每个线程有自己独立的堆栈和寄存器。

通信方式

- 多进程:通信相对复杂,需要使用专门的进程间通信(IPC)机制,如管道、消息队列、共享内存、信号量等。

- 多线程:通信较为方便,可直接通过共享变量进行数据交换,但需注意线程同步问题,以避免数据冲突。

切换开销

- 多进程:进程切换时,需要保存和恢复进程的上下文信息,包括寄存器的值、内存映射等,开销较大。

- 多线程:线程切换只需保存和恢复少量的寄存器和堆栈信息,切换开销相对较小,能更高效地进行并发执行。

编程难度

- 多进程:编程相对简单,各进程独立性强,逻辑清晰,但实现进程间通信和同步较为复杂。

- 多线程:编程时要考虑线程安全问题,避免多个线程同时访问共享资源导致数据不一致,编程难度较大。

适用场景

- 多进程:适用于需要高度独立性和稳定性的任务,如服务器守护进程、数据库管理系统等,每个子进程可以独立处理不同的请求,互不干扰。

- 多线程:适合于对响应速度要求高、需要频繁切换任务的场景,如图形界面应用程序、网络服务器中的并发处理等,能充分利用多核处理器的优势,提高程序的运行效率。

2.线程的公有和私有

在Linux线程中,有些资源是线程私有的,而有些则是线程间公有的(共享的),以下是具体介绍:

线程私有资源

- 线程ID:每个线程都有唯一的标识符,用于在进程内区分不同的线程。

- 栈空间:线程有自己独立的栈,用于存储局部变量、函数调用参数和返回地址等,不同线程的栈空间相互独立,不会相互干扰。

- 寄存器:包括程序计数器、栈指针等,保存线程的运行状态和上下文信息,保证线程被调度时能正确恢复执行。

- errno变量:用于记录系统调用的错误码,每个线程都有自己的errno,避免一个线程的错误影响其他线程对错误的判断。

线程公有(共享)资源

- 进程地址空间:包括代码段、数据段、堆等,所有线程都在同一个进程地址空间内运行,可以访问相同的程序代码和全局数据。

- 文件描述符:进程打开的文件描述符在所有线程间共享,多个线程可以操作同一个文件。

- 信号处理:进程的信号处理机制是共享的,一个线程对信号的处理可能会影响到其他线程。

- 共享库:进程加载的共享库在所有线程间共享,节省内存空间,提高程序运行效率。

3.进程通信方式有哪些以及优缺点

Linux中进程通信方式主要有以下几种:

管道

- 匿名管道:是一种半双工的通信方式,只能在具有亲缘关系的进程之间使用,通常用于父子进程间通信。数据只能单向流动,一端写入,一端读出。

- 命名管道:FIFO,有名管道,它可以在不相关的进程之间进行通信。在文件系统中有对应的节点,通过文件路径来标识,克服了匿名管道只能在亲缘进程间通信的限制。

信号

信号是一种用于通知进程发生了某种事件的异步通信机制。进程可以通过信号来响应系统事件、用户操作或其他进程发送的信号,如 SIGINT (中断信号)、 SIGKILL (终止信号)等。

消息队列

消息队列是一个消息的链表,存放在内核中,由消息队列标识符来标识。进程可以向消息队列中发送消息,也可以从消息队列中读取消息,不同进程通过消息队列进行数据交换。

共享内存

共享内存允许不同进程访问同一块内存区域,是最快的进程间通信方式。多个进程可以直接读写共享内存中的数据,无需进行数据复制,但需要通过信号量等机制来实现同步和互斥访问。

信号量

信号量主要用于进程间的同步和互斥,它是一个计数器,用于控制多个进程对共享资源的访问。通过对信号量的操作(P操作和V操作)来实现进程的同步和互斥控制。

套接字

套接字可用于不同主机间的进程通信,也可用于同一主机上不同进程间的通信。它提供了一种通用的网络编程接口,支持多种协议,如TCP和UDP。

用户:

优缺点

豆包:

以下是Linux中各种进程通信方式的优缺点:

管道

- 优点:简单易用,对于具有亲缘关系的进程间通信方便,内核自动进行同步管理。

- 缺点:匿名管道只能用于亲缘进程,半双工通信限制了数据传输方向,且管道容量有限。

信号

- 优点:可以快速响应系统事件或异常情况,实现异步通知,开销较小。

- 缺点:信号能传递的信息有限,通常只是一个信号值,难以传递复杂数据,且处理信号的代码可能会打断正常的程序流程,导致程序逻辑复杂。

消息队列

- 优点:消息有类型,接收方可以根据类型有选择地接收消息,适用于不同类型数据的异步通信。

- 缺点:消息的发送和接收有一定的开销,且消息队列的大小有限制,若处理不及时可能导致消息丢失。

共享内存

- 优点:通信效率高,数据传输速度快,因为进程直接访问共享内存,无需数据复制。

- 缺点:需要程序员自己处理同步和互斥问题,否则容易出现数据不一致的情况,编程复杂度较高。

信号量

- 优点:能有效实现进程间的同步和互斥,控制对共享资源的访问,确保数据的一致性。

- 缺点:只能进行简单的计数和同步控制,不能直接传输数据,且使用不当容易导致死锁。

套接字

- 优点:功能强大,可用于不同主机间的进程通信,支持多种网络协议,适用于分布式系统。

- 缺点:对于本地进程通信,相比其他一些方式,开销较大,编程相对复杂,需要处理网络连接、数据传输的可靠性等问题。

4.线程同步方式

在Linux中,线程同步的方式有多种,以下是一些常见的方式:

互斥锁

- 原理:通过对共享资源进行锁定,确保在同一时刻只有一个线程能够访问该资源。

- 使用方法:使用 pthread_mutex_t 类型的变量来表示互斥锁,通过 pthread_mutex_lock 函数来获取锁, pthread_mutex_unlock 函数来释放锁。

- 优点:简单高效,适用于对共享资源的短时间独占访问。

- 缺点:如果一个线程在持有锁的情况下发生阻塞,可能会导致其他线程长时间等待。

条件变量

- 原理:允许线程在满足特定条件时才进行相应的操作,通常与互斥锁配合使用。

- 使用方法:定义 pthread_cond_t 类型的条件变量,使用 pthread_cond_wait 函数让线程等待条件满足, pthread_cond_signal 或 pthread_cond_broadcast 函数用于唤醒等待的线程。

- 优点:能让线程在条件不满足时阻塞,避免无效的忙等待,提高资源利用率。

- 缺点:需要正确地处理条件判断和信号发送,否则可能导致线程无法正确唤醒或出现竞态条件。

读写锁

- 原理:允许多个线程同时读取共享资源,但在写操作时需要独占资源。

- 使用方法:使用 pthread_rwlock_t 类型的变量表示读写锁,通过 pthread_rwlock_rdlock 获取读锁, pthread_rwlock_wrlock 获取写锁, pthread_rwlock_unlock 释放锁。

- 优点:适合于读操作远多于写操作的场景,能提高并发访问效率。

- 缺点:实现相对复杂,对读写操作的顺序和时机有一定要求。

信号量

- 原理:通过一个计数器来控制对共享资源的访问,当计数器大于0时,线程可以获取资源并将计数器减1;当计数器为0时,线程需要等待。

- 使用方法:使用 sem_t 类型的变量表示信号量,通过 sem_wait 函数获取信号量, sem_post 函数释放信号量。

- 优点:可以控制多个线程对多个共享资源的访问,具有较高的灵活性。

- 缺点:如果信号量的初始值设置不当或操作不正确,可能导致死锁或资源浪费。

5.死锁

死锁是指多个进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。以下是关于死锁的一些关键信息:

产生死锁的原因

- 资源竞争:多个进程或线程同时请求有限的资源,如打印机、内存等,当资源分配不当时,就可能导致死锁。

- 进程推进顺序不当:进程的执行顺序不合理,也可能引发死锁。例如,进程A先获取了资源R1,然后请求资源R2,而进程B先获取了资源R2,又请求资源R1,此时两个进程就会相互等待对方释放资源,从而产生死锁。

产生死锁的必要条件

- 互斥条件:资源在某一时刻只能被一个进程或线程所占用。

- 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而新请求的资源被其他进程占用,导致该进程阻塞,但它仍保持着已获得的资源。

- 不可剥夺条件:进程所获得的资源在未使用完之前,不能被其他进程强行剥夺,只能由该进程自己释放。

- 循环等待条件:存在一个进程或线程的循环链,链中的每个进程或线程都在等待下一个进程或线程所占用的资源。

死锁的预防

- 破坏互斥条件:使资源可同时被多个进程访问,但有些资源本身不支持这种方式,如打印机,所以该方法可行性有限。

- 破坏请求与保持条件:要求进程在开始执行前一次性申请所需的全部资源,或者在申请新资源时,先释放已持有的资源。

- 破坏不可剥夺条件:允许系统对资源进行剥夺,当一个进程申请的资源被其他进程占用时,可将占用资源的进程暂停,剥夺其资源分配给请求进程。

- 破坏循环等待条件:对资源进行编号,规定进程必须按照编号递增的顺序申请资源,这样就不会形成循环等待。

死锁的避免

- 银行家算法是一种经典的死锁避免算法。该算法通过对系统资源的分配情况进行监控和预测,判断每个进程的资源请求是否安全,只有在系统处于安全状态时才满足进程的资源请求,从而避免死锁的发生。

死锁的检测与解除

- 检测:通过资源分配图等方法来检测系统是否存在死锁。资源分配图中节点表示进程和资源,边表示进程对资源的请求和分配关系,若图中存在环,则可能存在死锁。

- 解除:检测到死锁后,可通过撤销某些进程、剥夺某些进程的资源或让进程回退到某个安全状态等方法来解除死锁。

计算机网络

1.IO复用

网络I/O复用是一种在网络编程中用于同时监控多个文件描述符(通常是套接字)状态变化的技术,以实现高效的网络通信。以下是关于它的详细介绍:

基本概念

- 应用程序通过系统调用,将多个文件描述符注册到内核中的I/O复用模块,然后阻塞等待这些文件描述符上有事件发生,如可读、可写或异常等。当有事件发生时,内核会通知应用程序,应用程序再对相应的文件描述符进行处理。

常用的I/O复用函数

- select:能同时监控多个文件描述符集合的读写和异常事件。它通过 fd_set 结构体来表示文件描述符集合,应用程序需要先将感兴趣的文件描述符添加到集合中,然后调用 select 函数。 select 函数会阻塞直到有文件描述符上有事件发生或超时。其缺点是能监控的文件描述符数量有限,通常受限于系统的 FD_SETSIZE 常量,而且每次调用 select 都需要将文件描述符集合从用户空间复制到内核空间,效率较低。

- poll:与 select 类似,但使用 pollfd 结构体数组来表示文件描述符集合,它没有文件描述符数量的限制。不过, poll 同样需要在每次调用时将数组从用户空间复制到内核空间,并且当文件描述符数量较多时,遍历数组判断事件的开销也较大。

- epoll:是Linux下高效的I/O复用机制。它通过 epoll_create 创建一个 epoll 实例,然后使用 epoll_ctl 函数向 epoll 实例中添加、修改或删除要监控的文件描述符及其感兴趣的事件。当调用 epoll_wait 函数时,它会阻塞等待事件发生,内核会将发生事件的文件描述符列表返回给应用程序,应用程序只需处理这些有事件的文件描述符即可,大大提高了效率。 epoll 适用于处理大量并发连接的场景。

优点

- 提高资源利用率:可以在一个线程中同时处理多个网络连接,避免为每个连接创建一个单独的线程,减少了线程创建和上下文切换的开销。

- 实现并发处理:能同时监听多个文件描述符的事件,及时响应不同连接上的读写请求,实现对多个网络连接的并发处理,提高服务器的并发处理能力。

应用场景

- 网络服务器:广泛应用于各种网络服务器程序,如Web服务器、邮件服务器等,用于处理大量客户端的连接请求和数据传输。

- 实时应用:在一些对实时性要求较高的应用中,如即时通讯、在线游戏等,能够及时响应多个客户端的操作,保证应用的实时性和流畅性。

2.IO复用的优缺点是什么

网络I/O复用除了上述提到的提高资源利用率和实现并发处理等优点外,还有一些其他优点和缺点,具体如下:

优点

- 响应性好:能及时响应多个文件描述符上的事件,对于有实时数据处理需求的场景,如实时监控系统,可快速处理接收到的数据,提高系统的响应速度。

- 编程相对简单:相比于多线程或多进程模型,使用I/O复用可以在一个线程或进程中处理多个连接,减少了多线程编程中复杂的同步和资源管理问题,使代码结构更清晰,更易于理解和维护。

缺点

- 事件处理复杂:当有大量文件描述符同时产生事件时,处理这些事件的逻辑可能会变得复杂,需要仔细处理不同类型的事件和对应的操作,否则容易出现逻辑错误。

- 不适用于所有场景:对于一些特定的场景,如每个连接都需要进行长时间的复杂计算,使用I/O复用可能无法充分发挥其优势,因为单个线程在处理这些计算时可能会阻塞,导致其他连接的事件不能及时处理。此时,结合多线程或多进程模型可能更合适。

- 可移植性问题:不同操作系统对I/O复用的实现和支持有所不同,例如 epoll 是Linux特有的机制,在其他操作系统上可能需要使用不同的技术或进行适配,这会影响代码的可移植性。

3.tcp的滑动窗口

TCP的滑动窗口是一种流量控制和数据传输优化机制,以下是其相关介绍:

基本原理

- 发送方和接收方都有一个缓存区用于存储数据,同时各自维护一个滑动窗口。窗口大小表示在无需等待对方确认的情况下,发送方可以发送的数据量。接收方通过在确认消息中告知发送方自己的窗口大小,来控制发送方的发送速度,确保接收方有足够的缓存空间来处理数据,避免数据丢失。

窗口的滑动

- 当发送方收到接收方对已发送数据的确认信息后,发送窗口会向前滑动,允许发送更多的数据。例如,发送方发送了序号为1 - 1000的字节数据,接收方成功接收并确认后,发送方的窗口就可以向前滑动,假设窗口大小为2000,那么就可以接着发送序号为1001 - 3000的字节数据。

流量控制

- 接收方根据自身的缓存情况和处理能力,动态调整窗口大小并告知发送方。如果接收方处理速度变慢,缓存空间快满了,就会减小窗口大小,让发送方降低发送速度;当接收方处理完数据,缓存空间空闲增多时,又会增大窗口大小,允许发送方发送更多数据。

与拥塞控制的关系

- 滑动窗口主要用于流量控制,关注接收方的接收能力;而拥塞控制则主要考虑网络的拥塞状况,防止网络过载。在实际的TCP传输中,二者通常结合使用。发送方的发送窗口大小会取拥塞窗口和接收方通告窗口中的较小值,以同时适应网络状况和接收方的接收能力。

优点

- 提高传输效率,允许发送方在等待确认的同时继续发送多个数据包,充分利用网络带宽。

- 实现流量控制,避免接收方因处理能力不足而丢失数据,保证数据传输的可靠性。

缺点

- 窗口大小的设置需要根据网络和接收方的情况进行合理调整,否则可能导致性能下降。如果窗口设置过大,可能会加重网络拥塞;设置过小,则无法充分利用网络带宽。

- 增加了协议的复杂性,需要额外的机制来维护窗口的状态、处理确认消息等,增加了TCP实现和维护的难度。

4.tcp的滑动窗口是如何进行流量控制的

TCP滑动窗口通过以下方式进行流量控制:

接收方通告窗口大小

接收方在发送给发送方的确认(ACK)报文中,会包含一个字段来告知发送方自己当前的接收窗口大小,这个窗口大小表示接收方还有多少缓存空间可以用来接收数据。例如,接收方的缓存总大小为1000字节,已经接收并处理了200字节的数据,那么此时它可以在ACK报文中通告窗口大小为800字节,告诉发送方最多可以再发送800字节的数据。

发送方根据通告窗口调整发送速度

发送方接收到接收方通告的窗口大小后,会根据这个值来控制自己的发送速度。发送方不会发送超过接收方通告窗口大小的数据量。比如接收方通告窗口大小为500字节,发送方当前有1000字节的数据要发送,那么发送方只会发送前500字节的数据,然后等待接收方的进一步确认和窗口更新信息。

窗口的动态调整

在数据传输过程中,接收方会根据自身缓存的使用情况和数据处理速度,动态地调整通告窗口的大小。如果接收方处理数据的速度加快,缓存空间变得更充裕,它会增大通告窗口,允许发送方发送更多的数据;反之,如果接收方处理速度变慢,缓存快满了,就会减小通告窗口,让发送方降低发送速度。例如,接收方原本通告窗口大小为500字节,在处理完一部分数据后,缓存空间增加,于是在下次ACK报文中将通告窗口大小调整为800字节,发送方收到后就可以根据新的窗口大小发送更多数据。

通过这种接收方反馈窗口大小,发送方根据反馈调整发送数据量的机制,TCP滑动窗口能够有效地实现流量控制,确保数据发送速度与接收方的处理能力相匹配,避免数据丢失和网络拥塞。

5.拥塞控制的方法

常见的TCP拥塞控制方法有以下几种:

慢启动

- 发送方初始时将拥塞窗口大小设置为一个较小的值,通常为一个最大段长度(MSS)。然后,每收到一个对新数据的确认,就将拥塞窗口大小增加一个MSS。这样,拥塞窗口会以指数增长的方式快速增加,以探测网络的可用带宽。

拥塞避免

- 当拥塞窗口大小达到慢启动阈值时,进入拥塞避免阶段。此时,发送方每收到一个确认,不再是成倍增加拥塞窗口,而是线性增加,通常每次增加一个MSS除以拥塞窗口大小的值。通过这种较为缓慢的增长方式,避免网络出现拥塞。

快重传

- 接收方如果发现有失序的数据包,会立即发送重复确认给发送方,而不是等到自己的缓存填满或者定时器超时。发送方只要收到三个相同的重复确认,就认为相应的数据包丢失了,会立即重传该数据包,而不需要等到定时器超时,从而加快了数据包的重传速度,提高了网络的吞吐量。

快恢复

- 快重传后,执行快恢复算法。发送方将慢启动阈值设置为当前拥塞窗口大小的一半,同时将拥塞窗口大小设置为慢启动阈值加上3倍的MSS。然后进入拥塞避免阶段,继续线性增加拥塞窗口,而不是像慢启动那样从较小的值开始重新增长,这样可以更快地恢复网络传输能力,减少数据传输的延迟。

数据库

1.关系型数据库和非关系型数据库

关系型数据库和非关系型数据库是两种不同类型的数据库管理系统,它们在数据模型、存储结构、查询方式等方面存在差异,以下是具体介绍:

数据模型

- 关系型数据库:以表格形式存储数据,由行和列组成,支持事务处理和约束,如外键关联等,通过SQL语言进行操作,数据一致性高。

- 非关系型数据库:数据模型更加灵活多样,包括键值对、文档、列族、图等形式,支持高并发读写,易于横向扩展,适用于处理非结构化或半结构化数据。

存储结构

- 关系型数据库:数据通常存储在固定格式的表中,表之间通过外键等关系进行关联。数据存储在硬盘等存储设备上,以页为单位进行读写,索引用于加快数据的查询速度。

- 非关系型数据库:根据不同的类型有不同的存储方式。例如,键值对数据库将数据以键值对的形式存储在内存或硬盘中;文档数据库以文档的形式存储数据,数据格式通常为JSON或BSON等。

查询方式

- 关系型数据库:使用SQL语言进行查询,具有强大的查询功能,可以进行复杂的关联查询、分组查询等。

- 非关系型数据库:查询方式因类型而异。例如,键值对数据库通过键来获取值;文档数据库可以使用特定的查询语言或API来查询文档中的数据。

适用场景

- 关系型数据库:适用于对数据一致性要求高、有复杂事务处理需求、数据结构固定且关系明确的场景,如银行系统、电子商务系统中的订单处理等。

- 非关系型数据库:适用于高并发读写、数据量巨大、数据结构不固定或半结构化的场景,如社交媒体平台的用户生成内容存储、实时数据处理等。

代表产品

- 关系型数据库:有MySQL、Oracle、SQL Server等。

- 非关系型数据库:包括Redis(键值对数据库)、MongoDB(文档数据库)、Cassandra(列族数据库)、Neo4j(图数据库)等。

2.什么是事务

在数据库管理系统中,事务是一个不可分割的工作单元,它包含了一系列的数据库操作,这些操作要么全部成功执行,要么全部不执行,以保证数据库的一致性。例如,在银行转账操作中,从一个账户扣除一定金额并将相同金额存入另一个账户,这两个操作必须作为一个事务来处理,以确保资金的准确性和一致性。

事务的特性

- 原子性:事务中的所有操作要么全部执行成功,要么全部回滚,就像一个原子一样不可分割。

- 一致性:事务执行前后,数据库的完整性约束不会被破坏,数据从一个一致状态转换到另一个一致状态。

- 隔离性:多个事务并发执行时,每个事务都感觉不到其他事务的存在,它们之间相互隔离,互不干扰。

- 持久性:一旦事务被提交,它对数据库所做的修改就会永久保存,即使系统出现故障也不会丢失。

事务的操作

- 开始事务:标志着事务的开始,后续的数据库操作将被纳入该事务的范围。

- 提交事务:表示事务中的所有操作都已成功完成,将对数据库的修改永久保存。

- 回滚事务:当事务中的某个操作出现错误或满足特定条件时,将事务回滚到初始状态,撤销事务中已经执行的操作,使数据库恢复到事务开始前的状态。

3.事务的acid特性

事务的ACID特性是指原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),以下是具体解释:

- 原子性:事务是一个不可分割的工作单元,事务中的所有操作要么全部成功执行,要么全部不执行。例如,在一个银行转账事务中,从账户A扣除金额和向账户B添加金额这两个操作必须同时成功或者同时失败,不能出现只执行了其中一个操作的情况。

- 一致性:事务执行前后,数据库的完整性约束保持不变,数据从一个一致状态转换到另一个一致状态。例如,在转账事务中,转账前后,两个账户的总金额应该保持不变,不能因为转账操作导致数据不一致。

- 隔离性:多个事务并发执行时,每个事务都感觉不到其他事务的存在,它们之间相互隔离,互不干扰。例如,当多个用户同时进行转账操作时,每个用户的事务应该是相互独立的,不会出现一个事务的操作影响到另一个事务的情况。

- 持久性:一旦事务被提交,它对数据库所做的修改就会永久保存,即使系统出现故障也不会丢失。例如,在转账事务提交后,即使数据库服务器突然断电,重新启动后,转账的结果也应该是有效的,账户的余额已经被正确更新。

4.MySQL锁机制

MySQL的锁机制是用于控制并发访问数据库时的数据一致性和完整性的重要手段,主要有以下几种类型:

按锁的粒度分类

- 表级锁:对整个表进行锁定。开销小,加锁快,但并发度低。例如,当使用  LOCK TABLES table_name READ  语句对表进行读锁定后,其他事务可以对该表进行并发读,但不能进行写操作;若使用  LOCK TABLES table_name WRITE  进行写锁定,那么其他事务对该表的读和写操作都将被阻塞。

- 行级锁:仅对表中的某一行数据进行锁定。并发度高,但开销大,加锁慢。InnoDB存储引擎支持行级锁,如执行  UPDATE table_name SET column = value WHERE condition  语句时,如果  condition  条件能精准定位到某一行,MySQL会使用行级锁,只锁定被更新的行,不影响其他行的并发操作。

- 页级锁:介于表级锁和行级锁之间,锁定粒度为数据页。其并发度和开销也介于两者之间。

按锁的类型分类

- 共享锁(读锁):允许事务对数据进行读取操作,多个事务可以同时获取同一数据的共享锁,实现并发读。例如,一个事务执行  SELECT ... LOCK IN SHARE MODE  语句获取共享锁后,其他事务也可以对相同数据获取共享锁进行读取,但不能获取排他锁进行写入操作。

- 排他锁(写锁):只允许一个事务对数据进行写入操作,并且在排他锁释放之前,其他事务不能对该数据获取任何类型的锁。例如,执行  UPDATE   DELETE  等写操作时,MySQL会自动获取排他锁,以保证数据的一致性和完整性。

MySQL通过锁机制来协调并发事务对数据的访问,不同的存储引擎支持的锁机制有所不同,用户可以根据具体的业务需求选择合适的锁类型和粒度,以平衡并发性能和数据一致性。

5.数据库的索引

索引是数据库中一种用于提高数据查询效率的数据结构。以下是关于索引的详细介绍:

索引的作用

- 快速定位数据:就像书籍的目录,能让数据库快速找到所需数据的位置,减少数据查询时的磁盘 I/O 操作,从而显著提高查询性能。例如在一个有大量用户记录的表中,通过对“姓名”字段建立索引,查询特定姓名的用户时,数据库可以直接根据索引快速定位到相关记录,而无需全表扫描。

索引的类型

- B树索引:是最常见的索引类型,适用于全键值、键值范围和键前缀查找。其结构能有效平衡树的高度,保证查询效率稳定。

- 哈希索引:基于哈希函数实现,能快速定位数据,但只支持精确匹配查询,不适合范围查询。

索引的创建与维护

- 创建索引:使用  CREATE INDEX  语句,如  CREATE INDEX index_name ON table_name (column_name) ,可以在指定表的特定列上创建索引。

- 维护索引:数据库会自动维护索引,但在数据插入、更新和删除时,索引也需要相应更新,这会带来一定的性能开销。因此,要根据实际业务需求合理创建索引,避免过多索引导致维护成本过高。

虽然索引能提高查询性能,但也会占用额外的存储空间,并且会影响数据的插入、更新和删除操作的速度。所以在设计索引时,需要综合考虑数据库的性能需求和空间成本等因素。

6.为什么常用b+树做索引

B+树是一种常用于数据库索引的数据结构,它具有以下特点和优势:

结构特点

- 节点类型:B+树由根节点、内部节点和叶子节点组成。所有数据都存储在叶子节点中,叶子节点通过指针连接成有序链表,便于范围查询。

- 键值存储:内部节点只存储键值和指向子节点的指针,不存储数据记录,这样可以增大每个节点能存储的键值数量,从而减少树的高度,提高查询效率。

作为索引的优势

- 高效的范围查询:由于叶子节点是有序链表,在进行范围查询时,只需找到范围的起始和结束位置,然后沿着链表顺序读取即可,避免了大量的随机磁盘 I/O。

- 稳定性好:B+树通过动态调整节点的分裂和合并,保持树的平衡,使得查询时间复杂度稳定为 O(log_{n}),n为节点数。

- 支持并发操作:B+树的结构有利于实现并发控制,多个事务可以同时对不同的节点进行操作,提高数据库的并发性能。

在数据库中,使用B+树作为索引可以大大提高数据的查询效率,特别是对于范围查询和频繁的等值查询场景,能够快速定位到所需数据,减少磁盘访问次数,提升数据库的整体性能。

7.b+树和红黑树的区别

B+树和红黑树都是常见的数据结构,在数据库索引等场景中有着广泛应用,它们有以下区别:

结构特点

- B+树:是一种多路平衡查找树,节点可以有多个子节点,所有数据都在叶子节点,叶子节点之间通过链表相连。

- 红黑树:是一种二叉查找树,每个节点只有左右两个子节点,通过对节点进行红黑着色来保证树的平衡。

数据存储

- B+树:数据全部存储在叶子节点,非叶子节点仅用于索引,因此能存储更多的索引项,树的高度相对较低。

- 红黑树:每个节点都可以存储数据和键值,数据分布在整个树中。

查找效率

- B+树:由于树的高度较低,且叶子节点有序,在进行范围查询和等值查询时用户:

和红黑用户:

和红黑树的区别

豆包:

B+树和红黑树都是常见的数据结构,在数据库索引等场景中有着广泛应用,它们有以下区别:

结构特点

- B+树:是一种多路平衡查找树,节点可以有多个子节点,所有数据都在叶子节点,叶子节点之间通过链表相连。

- 红黑树:是一种二叉查找树,每个节点只有左右两个子节点,通过对节点进行红黑着色来保证树的平衡。

数据存储

- B+树:数据全部存储在叶子节点,非叶子节点仅用于索引,因此能存储更多的索引项,树的高度相对较低。

- 红黑树:每个节点都可以存储数据和键值,数据分布在整个树中。

查找效率

- B+树:由于树的高度较低,且叶子节点有序,在进行范围查询和等值查询时,通常具有更高的效率,查询时间复杂度为O(log_{n}),n为节点数。

- 红黑树:查找效率也比较高,时间复杂度为O(log_{n}),但在范围查询上相对B+树不够高效,需要遍历更多的节点。

应用场景

- B+树:适合用于数据库索引,特别是在大规模数据存储和频繁的范围查询、等值查询场景中表现出色。

- 红黑树:常用于内存中的数据结构,如Java的TreeMap、C++ 的map等,在需要快速插入、删除和查找的场景中应用广泛。

算法编程

1.删除字符串中出现的空格,包含首尾

//删除字符串中出现的空格,包含首尾
#include<iostream>
#include<string>
using namespace std;
string removesp(const string &str)
{string result = "";for (char c :str ){if (c != ' '){result += c;}}return result;}int main()
{string str = " du xin ran ";string newstr = removesp(str);cout << newstr << endl;return 0;
}

2.输入一个链表的头结点,从尾到头反过来打印出每个结点的值。 


链表结点定义如下: 
不带头结点的单链表

struct ListNode
{
    int m_nKey;
    ListNode* m_pNext;
};

使用栈

#include<iostream>
#include<stack>
using namespace std;
struct ListNode
{int m_nKey;ListNode* m_pNext;
};
void printLN(ListNode* head)
{stack<ListNode*>s;ListNode* p = head;//存入while (p != nullptr){s.push(p);p = p->m_pNext;}//打印while (!s.empty()){ListNode* node = s.top();cout << node->m_nKey << " ";s.pop();}
}
int main()
{ListNode* head = new ListNode{ 1,new ListNode{2,new ListNode{3,nullptr}} };printLN(head);return 0;
}

相关文章:

  • 【愚公系列】《Python网络爬虫从入门到精通》063-项目实战电商数据侦探(主窗体的数据展示)
  • 前端框架的“快闪“时代:我们该如何应对技术迭代的洪流?
  • 媒体关注:联易融聚焦AI+业务,重塑供应链金融生态
  • CAD在线查看免费,可以支持DWG/GLB/GLTF/doc/wps/pdf/psd/eml/zip, rar/MP3/MP4/svg/OBJ/FBX格式
  • 2025年数字媒体设计与文化交流国际会议 (DMACE 2025)
  • 【Redis】字符串类型List 常用命令详解
  • 基于 PaddleOCR对pdf文件中的文字提取
  • 分布式之易混淆概念
  • vue浅试(1)
  • EasyRTC音视频实时通话:打造高清低延迟的远程会议新生态
  • (51单片机)LCD显示温度(DS18B20教程)(LCD1602教程)(延时函数教程)(单总线教程)
  • 7. 深入Spring AI:刨析 Advisors 机制
  • C++中的算术转换、其他隐式类型转换和显示转换详解
  • 极验4滑块笔记:整理思路--填坑各种问题
  • Java--数组的应用
  • Linux随记(十七)
  • 辛格迪客户案例 | 浙江高跖医药委托生产质量管理协同(OWL MAH)项目
  • C语言实现堆(优先队列)详解
  • 【沉浸式求职学习day21】【常用类分享,完结!】
  • 使用 Vue Router 和 Vite 构建的自动路由生成系统
  • 涡虫首上太空,神舟二十号任务将开展3项生命科学实验
  • A股三大股指涨跌互现:黄金股再度走强,两市成交10900亿元
  • 马上评|遭强奸之后私刑报复,不属正当防卫
  • 人民日报刊文:美国滥施关税及中国反制措施的法理视角透析
  • “中国共产党的故事——习近平新时代中国特色社会主义思想在重庆的实践”重庆经贸推介会成功举办
  • “雷公”起诉人贩子王浩文案将开庭:索赔6元,“讨个公道”