面试专栏:http://t.csdnimg.cn/0i089
目录
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。
对于正数,补码等于原码。
对于负数,补码等于反码加 1。
-2 的二进制表示:
-
原码:1000 0010
-
反码:1111 1101
-
补码:1111 1110
-3 的二进制表示:
-
原码:1000 0011
-
反码:1111 1100
-
补码:1111 1101
2.3.一个数组有n个元素,找到前k个最大的?
方法一:使用堆
-
建立最小堆:首先将数组的前 k 个元素建立一个包含 k 个元素的最小堆。
-
遍历数组:从第 k+1 个元素开始遍历数组,对于每个元素,如果比堆顶元素大,则将堆顶元素替换为当前元素,并对堆进行一次调整,使得堆顶元素仍然是堆中的最小值。
-
返回结果:遍历完成后,堆中的元素就是前 k 个最大的元素。
方法二:使用快速排序的思想
-
快速选择算法:可以使用类似快速排序的思想,选取一个枢轴元素,将数组分为两部分,一部分比枢轴元素大,一部分比枢轴元素小。如果枢轴元素的位置恰好是 k,则找到了前 k 个最大的元素。如果枢轴元素的位置小于 k,则在右侧部分继续查找。如果枢轴元素的位置大于 k,则在左侧部分继续查找。
-
优化:在快速选择算法中,可以对快速排序进行一些优化,例如随机选择枢轴元素,或者使用三数取中法选择枢轴元素,以提高算法的效率。
2.4.简历上写了双指针和二分查找算法,能否具体讲一下思路?
双指针
双指针算法是指在数组或链表等数据结构中,使用两个指针分别指向不同位置,通过移动这两个指针来解决问题的一种算法。
思路:
-
设定两个指针,一般初始化为数组或链表的头部或尾部。
-
根据问题要求,移动指针并进行相应的处理,直至两个指针相遇或达到特定条件。
应用场景:
-
数组中的两数之和:通过双指针遍历数组,找到两个数使它们的和等于目标值。
-
链表中的环检测:使用快慢指针,快指针每次移动两步,慢指针每次移动一步,如果存在环,两个指针最终会相遇。
-
归并两个有序数组:使用双指针分别指向两个数组的开头,比较两个指针所指元素的大小,将较小的元素放入新数组中。
二分查找
二分查找算法是一种在有序数组中查找目标值的高效算法。
思路:
-
设定左右边界指针,一般初始化为数组的起始和结束位置。
-
每次取中间位置的值与目标值比较,如果相等则返回,如果小于目标值,则在右半部分继续查找,如果大于目标值,则在左半部分继续查找。
-
重复以上步骤,直到找到目标值或左右边界指针相遇。
应用场景:
-
有序数组中的查找:通过二分查找可以在 O(logn) 的时间复杂度内找到目标值。
-
旋转数组的查找:可以先找到旋转点,然后再在两个有序子数组中分别进行二分查找。
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有什么区别?
-
访问权限:
-
在 C++ 中,
class
的默认成员访问权限是private
,而struct
的默认成员访问权限是public
。 -
这意味着,如果不显式指定成员的访问权限,使用
class
定义的成员默认是private
的,而使用struct
定义的成员默认是public
的。
-
-
默认继承权限:
-
在 C++ 中,
class
默认继承权限是private
,而struct
默认继承权限是public
。 -
这意味着,如果不显式指定继承关系,使用
class
继承的成员默认是private
继承,而使用struct
继承的成员默认是public
继承。
-
-
成员函数:
-
在 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 数据包的最大长度受到以下因素的限制:
-
IPv4 和 IPv6 的数据报长度限制:IPv4 数据报的最大长度是 65535 字节,IPv6 数据报的最大长度是 65535 字节。其中,IPv4 头部占用 20 字节,IPv6 头部占用 40 字节,因此 UDP 数据报的数据部分最大长度为 65535 - 20 = 65515 字节(IPv4)或 65535 - 40 = 65515 字节(IPv6)。
-
UDP 头部长度:UDP 头部占用 8 个字节,包含源端口、目的端口、长度和校验和字段。
-
网络传输的 MTU(最大传输单元):MTU 是网络中能够传输的最大数据包长度。不同网络类型(如以太网、Wi-Fi 等)的 MTU 不同,通常以太网的 MTU 是 1500 字节。如果 UDP 数据包超过了 MTU,就会被分片发送,增加了网络传输的开销和复杂性。
2.8.TCP通信中,read函数的返回值返回0代表什么?
TCP 通信中,read
函数用于从已连接的 TCP 套接字中读取数据。当read
函数返回 0 时,表示对端已关闭连接,即对端发送了一个正常的连接关闭请求。这种情况通常称为“对端正常关闭连接”。
详细讲解如下:
-
对端正常关闭连接:当对端主动调用
close
关闭连接时,TCP 协议会发送一个正常的连接关闭请求,告知本端关闭连接。当本端的read
函数读取到这个关闭请求时,返回值为 0,表示对端已经关闭连接。 -
调用
read
函数返回值的含义:调用read
函数后,返回值表示实际读取到的字节数。如果返回值为 0,表示对端已经关闭连接,没有数据可读取了。 -
处理对端关闭连接:当
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
来申请内存空间,程序最终会因为内存耗尽而崩溃。这种情况称为内存泄漏,即程序在动态分配内存后没有释放该内存,导致程序持续占用内存直至耗尽系统资源。
详细讲解:
-
内存泄漏原因:每次调用
malloc
函数分配内存后,如果没有调用free
函数释放该内存,这块内存就会一直被程序占用,直到程序结束或系统资源耗尽。 -
死循环中的内存泄漏:在
while(1)
的死循环中不断调用malloc
分配内存,但没有释放,会导致程序持续占用内存,最终导致内存耗尽。 -
程序崩溃:当系统内存耗尽时,操作系统无法满足程序对内存的需求,会导致程序崩溃。这种情况在不同操作系统下表现形式可能不同,例如 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.项目中如果用户忘记释放内存或多次释放内存,有没有做特殊处理?
总的来说,问的东西不是一些简单的概念,需要对目标足够熟悉才能回答上来。