0
点赞
收藏
分享

微信扫一扫

Windows Edge浏览器兼容性问题诊断与修复策略详解

eelq 04-03 10:30 阅读 1

目录

1.引言

2.分析

2.1.在C语言中定义一个全局变量,但不初始化,它默认的值是多少?

2.2.-2的源码,反码、补码分别是什么?-3呢?它的公式是什么?

2.3.一个数组有n个元素,找到前k个最大的?

2.4.简历上写了双指针和二分查找算法,能否具体讲一下思路?

2.5.Linux中是怎样用C语言实现面向对象的封装、继承、多态的?

2.6.C++中的class和C语言中的struct有什么区别?

2.7.UDP数据包的最大长度是多少?为什么?

2.8.TCP通信中,read函数的返回值返回0代表什么?

2.9.写一个while(1)的死循环,一直使用malloc申请空间,程序会崩溃吗?

2.10.进程退出时,假设存在没有释放的资源,此时应该怎么做?

2.11.你使用过这个项目和Google的TCmalloc项目做过对比吗?

2.12.项目中是怎么解决内碎片和外碎片问题的?

2.13.项目中如果用户忘记释放内存或多次释放内存,有没有做特殊处理?


1.引言

        在过去的几年里,互联网行业经历了一系列的调整和变革,许多企业都缩减了招聘规模。前两天腾讯放出了5000个实习岗位,网上评论已炸锅。回暖了?互联网又行了?腾讯前两年秋招基本上都没怎么招,今天放出这么多岗位,也可能意味着互联网行业的景气度有所回升。

        今儿给大家分享一下这个面经。

2.分析

2.1.在C语言中定义一个全局变量,但不初始化,它默认的值是多少?

        在 C 语言中,如果定义一个全局变量但不进行显式初始化,那么该全局变量会被默认初始化为零值。具体的默认值取决于变量的类型:

  • 对于整型(int、char、long 等)和枚举类型,在未显式初始化时,默认值为 0。

  • 对于浮点型(float、double 等),默认值为 0.0。

  • 对于指针类型,默认值为 NULL。

2.2.-2的源码,反码、补码分别是什么?-3呢?它的公式是什么?

计算方法如下:

  1. 原码:正数的原码就是其本身,负数的原码是将其绝对值的原码的符号位取反。

  2. 反码:正数的反码就是其本身,负数的反码是将其绝对值的原码的除符号位外的其他位取反。

  3. 补码:正数的补码就是其本身,负数的补码是将其绝对值的原码取反后加 1。

对于正数,补码等于原码。

对于负数,补码等于反码加 1。

-2 的二进制表示:

  • 原码:1000 0010

  • 反码:1111 1101

  • 补码:1111 1110

-3 的二进制表示:

  • 原码:1000 0011

  • 反码:1111 1100

  • 补码:1111 1101

2.3.一个数组有n个元素,找到前k个最大的?

方法一:使用堆

  1. 建立最小堆:首先将数组的前 k 个元素建立一个包含 k 个元素的最小堆。

  2. 遍历数组:从第 k+1 个元素开始遍历数组,对于每个元素,如果比堆顶元素大,则将堆顶元素替换为当前元素,并对堆进行一次调整,使得堆顶元素仍然是堆中的最小值。

  3. 返回结果:遍历完成后,堆中的元素就是前 k 个最大的元素。

方法二:使用快速排序的思想

  1. 快速选择算法:可以使用类似快速排序的思想,选取一个枢轴元素,将数组分为两部分,一部分比枢轴元素大,一部分比枢轴元素小。如果枢轴元素的位置恰好是 k,则找到了前 k 个最大的元素。如果枢轴元素的位置小于 k,则在右侧部分继续查找。如果枢轴元素的位置大于 k,则在左侧部分继续查找。

  2. 优化:在快速选择算法中,可以对快速排序进行一些优化,例如随机选择枢轴元素,或者使用三数取中法选择枢轴元素,以提高算法的效率。

2.4.简历上写了双指针和二分查找算法,能否具体讲一下思路?

双指针

双指针算法是指在数组或链表等数据结构中,使用两个指针分别指向不同位置,通过移动这两个指针来解决问题的一种算法。

思路:

  1. 设定两个指针,一般初始化为数组或链表的头部或尾部。

  2. 根据问题要求,移动指针并进行相应的处理,直至两个指针相遇或达到特定条件。

应用场景:

  1. 数组中的两数之和:通过双指针遍历数组,找到两个数使它们的和等于目标值。

  2. 链表中的环检测:使用快慢指针,快指针每次移动两步,慢指针每次移动一步,如果存在环,两个指针最终会相遇。

  3. 归并两个有序数组:使用双指针分别指向两个数组的开头,比较两个指针所指元素的大小,将较小的元素放入新数组中。

二分查找

二分查找算法是一种在有序数组中查找目标值的高效算法。

思路:

  1. 设定左右边界指针,一般初始化为数组的起始和结束位置。

  2. 每次取中间位置的值与目标值比较,如果相等则返回,如果小于目标值,则在右半部分继续查找,如果大于目标值,则在左半部分继续查找。

  3. 重复以上步骤,直到找到目标值或左右边界指针相遇。

应用场景:

  1. 有序数组中的查找:通过二分查找可以在 O(logn) 的时间复杂度内找到目标值。

  2. 旋转数组的查找:可以先找到旋转点,然后再在两个有序子数组中分别进行二分查找。

2.5.Linux中是怎样用C语言实现面向对象的封装、继承、多态的?

在 Linux 中使用 C 语言实现面向对象的封装、继承和多态需要一些技巧,因为 C 语言本身并不直接支持面向对象的特性。可以通过结构体和函数指针等特性来模拟实现面向对象的概念。

封装

可以使用结构体来封装数据,并通过函数来操作这些数据,实现封装的效果。

#include <stdio.h>

// 定义一个封装数据的结构体
typedef struct {
    int data; // 数据成员
} Encapsulation;

// 设置数据的函数
void setData(Encapsulation *obj, int value) {
    obj->data = value;
}

// 获取数据的函数
int getData(Encapsulation *obj) {
    return obj->data;
}

int main() {
    Encapsulation obj; // 创建一个 Encapsulation 类型的对象
    setData(&obj, 42); // 设置数据为 42
    printf("%d\n", getData(&obj)); // 打印数据
    return 0;
}

结构体 Encapsulation 封装了一个整数 data,并通过 setData 和 getData 函数对其进行操作,实现了数据的封装。

继承

可以通过在子类中包含父类的结构体来实现继承的效果。

#include <stdio.h>

// 父类结构体
typedef struct {
    int data; // 父类数据成员
} Parent;

// 子类结构体,包含了父类的结构体
typedef struct {
    Parent parent; // 包含父类对象
    int childData; // 子类自己的数据成员
} Child;

// 设置子类数据的函数
void setChildData(Child *obj, int value) {
    obj->childData = value;
}

int main() {
    Child obj; // 创建一个 Child 类型的对象
    obj.parent.data = 42; // 设置父类数据
    setChildData(&obj, 24); // 设置子类数据
    printf("%d %d\n", obj.parent.data, obj.childData); // 打印父类数据和子类数据
    return 0;
}

子类 Child 包含了父类 Parent 的结构体,并且可以通过操作子类对象来操作父类对象,实现了继承的效果。

多态

可以通过函数指针来实现多态的效果

#include <stdio.h>

// 定义一个包含函数指针的结构体,实现多态
typedef struct {
    int data; // 数据成员
    void (*print)(void *); // 函数指针
} Polymorphism;

// 打印整数的函数
void printInt(void *obj) {
    printf("%d\n", ((Polymorphism *)obj)->data);
}

int main() {
    Polymorphism obj; // 创建一个 Polymorphism 类型的对象
    obj.data = 42; // 设置数据
    obj.print = printInt; // 设置函数指针为打印整数的函数
    obj.print(&obj); // 调用函数指针打印数据
    return 0;
}

Polymorphism 结构体包含了一个函数指针 print,可以指向不同的打印函数,实现了多态的效果。

2.6.C++中的class和C语言中的struct有什么区别?

  1. 访问权限:

    • 在 C++ 中,class 的默认成员访问权限是 private,而 struct 的默认成员访问权限是 public

    • 这意味着,如果不显式指定成员的访问权限,使用 class 定义的成员默认是 private 的,而使用 struct 定义的成员默认是 public 的。

  2. 默认继承权限:

    • 在 C++ 中,class 默认继承权限是 private,而 struct 默认继承权限是 public

    • 这意味着,如果不显式指定继承关系,使用 class 继承的成员默认是 private 继承,而使用 struct 继承的成员默认是 public 继承。

  3. 成员函数:

    • 在 C++ 中,class 中的成员函数默认是 private 的,而 struct 中的成员函数默认是 public 的。

    • 这意味着,在 class 中定义的成员函数需要通过 public 成员函数来访问,而在 struct 中定义的成员函数可以直接访问。

参考代码:

// C++中的class
class MyClass {
private:
    int privateMember;

public:
    void publicMethod() {
        // 可以直接访问privateMember
        privateMember = 10;
    }
};

// C语言中的struct
struct MyStruct {
    int publicMember;

private:
    void privateMethod() {
        // 不能在外部直接访问privateMethod
    }

public:
    void publicMethod() {
        // 可以直接访问publicMember
        publicMember = 20;
        // 不能在外部直接访问privateMethod
    }
};

2.7.UDP数据包的最大长度是多少?为什么?

UDP 数据包的最大长度是 65507 字节(64KB - 8 字节 UDP 头 - 20 字节 IP 头),这是由于 UDP 协议的设计和底层网络的限制所决定的。

UDP(用户数据报协议)是一种无连接、不可靠的传输协议,它将数据作为数据报发送,不提供可靠性保证和流量控制。UDP 的设计目标之一是尽可能简单和高效,因此没有像 TCP 那样的拥塞控制、重传机制等复杂特性。

UDP 数据包的最大长度受到以下因素的限制:

  1. IPv4 和 IPv6 的数据报长度限制:IPv4 数据报的最大长度是 65535 字节,IPv6 数据报的最大长度是 65535 字节。其中,IPv4 头部占用 20 字节,IPv6 头部占用 40 字节,因此 UDP 数据报的数据部分最大长度为 65535 - 20 = 65515 字节(IPv4)或 65535 - 40 = 65515 字节(IPv6)。

  2. UDP 头部长度:UDP 头部占用 8 个字节,包含源端口、目的端口、长度和校验和字段。

  3. 网络传输的 MTU(最大传输单元):MTU 是网络中能够传输的最大数据包长度。不同网络类型(如以太网、Wi-Fi 等)的 MTU 不同,通常以太网的 MTU 是 1500 字节。如果 UDP 数据包超过了 MTU,就会被分片发送,增加了网络传输的开销和复杂性。

2.8.TCP通信中,read函数的返回值返回0代表什么?

TCP 通信中,read 函数用于从已连接的 TCP 套接字中读取数据。当read函数返回 0 时,表示对端已关闭连接,即对端发送了一个正常的连接关闭请求。这种情况通常称为“对端正常关闭连接”。

详细讲解如下:

  1. 对端正常关闭连接:当对端主动调用 close 关闭连接时,TCP 协议会发送一个正常的连接关闭请求,告知本端关闭连接。当本端的 read 函数读取到这个关闭请求时,返回值为 0,表示对端已经关闭连接。

  2. 调用 read 函数返回值的含义:调用 read 函数后,返回值表示实际读取到的字节数。如果返回值为 0,表示对端已经关闭连接,没有数据可读取了。

  3. 处理对端关闭连接:当 read 函数返回 0 时,表示对端已经关闭连接,本端应当做相应的清理工作,如关闭本端的套接字,并释放资源。

代码

char buffer[1024];
int bytesRead = read(socket_fd, buffer, sizeof(buffer));
if (bytesRead == 0) {
    // 对端已关闭连接
    printf("对端已关闭连接\n");
    close(socket_fd);
    exit(0);
} else if (bytesRead < 0) {
    // 读取出错
    perror("读取出错");
    exit(1);
} else {
    // 处理读取到的数据
    printf("读取到 %d 字节数据\n", bytesRead);
    // ...
}

2.9.写一个while(1)的死循环,一直使用malloc申请空间,程序会崩溃吗?

        如果在一个 while(1) 的死循环中不断使用 malloc 来申请内存空间,程序最终会因为内存耗尽而崩溃。这种情况称为内存泄漏,即程序在动态分配内存后没有释放该内存,导致程序持续占用内存直至耗尽系统资源。

详细讲解:

  1. 内存泄漏原因:每次调用 malloc 函数分配内存后,如果没有调用 free 函数释放该内存,这块内存就会一直被程序占用,直到程序结束或系统资源耗尽。

  2. 死循环中的内存泄漏:在 while(1) 的死循环中不断调用 malloc 分配内存,但没有释放,会导致程序持续占用内存,最终导致内存耗尽。

  3. 程序崩溃:当系统内存耗尽时,操作系统无法满足程序对内存的需求,会导致程序崩溃。这种情况在不同操作系统下表现形式可能不同,例如 Windows 可能弹出内存不足的错误提示,而 Linux 下可能会直接导致程序崩溃。

代码

#include <stdlib.h>

int main() {
    while(1) {
        // 不断申请内存但不释放,导致内存泄漏
        int *ptr = (int *)malloc(sizeof(int));
        // 如果需要持续运行,应在适当的时候释放内存,如下所示
        // free(ptr);
    }
    return 0;
}

要避免内存泄漏,应该在动态分配内存后,在不需要使用该内存时调用 free 函数释放内存。

2.10.进程退出时,假设存在没有释放的资源,此时应该怎么做?

当一个进程退出时,如果存在没有释放的资源(如内存、文件描述符等),应该尽量避免资源泄漏,确保资源能够正确释放。

常见的处理方式:

1.显式释放资源:在进程退出之前,尽量显式释放所有未释放的资源,包括内存、文件描述符、网络连接等。例如,对于动态分配的内存,可以使用 free 函数释放;对于打开的文件描述符,可以使用 close 函数关闭文件。

2.注册退出处理函数:可以使用 atexit 函数注册一个退出处理函数,在进程退出时自动调用。在退出处理函数中可以释放资源。

3.利用操作系统的资源回收机制:在现代操作系统中,当一个进程退出时,操作系统会回收进程占用的所有资源,包括内存、文件描述符等。但是,依赖操作系统的资源回收机制并不是一个良好的实践,因为它可能导致资源泄漏或者资源释放的时机不确定。

4.检查内存泄漏:可以使用内存泄漏检测工具(如 Valgrind)来检查程序是否存在内存泄漏问题,及时修复。

代码:

#include <stdlib.h>

void cleanup() {
    // 释放资源的代码
}

int main() {
    // 注册退出处理函数
    atexit(cleanup);

    // 其他代码

    return 0;
}

2.11.你使用过这个项目和Google的TCmalloc项目做过对比吗?

2.12.项目中是怎么解决内碎片和外碎片问题的?

2.13.项目中如果用户忘记释放内存或多次释放内存,有没有做特殊处理?

总的来说,问的东西不是一些简单的概念,需要对目标足够熟悉才能回答上来。

举报

相关推荐

0 条评论