字节跳动一面【C++后端开发】
base : 深圳 岗位:C++后端开发 时间: 2024/8/30
文章目录
- 基本介绍
- C++语言
- 1. 堆栈内存是否连续,为什么?
- 2. int i=0; i++ ; 两个线程同时执行10000次,i最终的数值是多少?
- 3. 全局变量,是在堆区 还是栈区? 【这个纯属是挖坑】
- 4. 进程和线程的区别 详解推荐 :
- 5. 我们输入网址,打开浏览器这一过程,会发生什么情况? 【偏网络协议】 详解推荐:
- 6. 进程打开网页,和线程打开网页,有什么区别?
- 7. 什么叫死锁,死锁产生的条件是什么?
- 8. 读写锁,了解过吗? 【建议多写代码 ,看视频实例,我看网上太乱了,死记硬背不行】
- 9. DNS 网络解析,以及域名这些了解吗?【我个人觉得是,我当时第5个问题,没有回答好,才问的这个知识点】
- 场景题
- 1. 这两个变量的区别?
- 内联函数 和 宏定义的区别 ?
- 常见的数据结构有哪些?
- 代码题
- 第1题: 在32位机器中,这些变量占的内存是多少字节的?
- 第2题: 二叉树最大路径和 【深度遍历】
基本介绍
主要是介绍了本科,硕士的学校,以及项目和 科研经历
C++语言
1. 堆栈内存是否连续,为什么?
堆和栈是两种不同的内存管理方式。堆栈内存是否连续取决于具体的实现和平台,但一般来说:
栈内存:通常是连续的,因为栈是为函数调用分配的内存空间,栈帧(Stack Frame)会按照函数调用的顺序逐步压入栈中,并在函数返回时按相反的顺序弹出,因此栈内存通常是线性增长的,空间是连续分配的。
堆内存:通常不连续。堆内存是**为动态内存分配(如使用malloc或new)**而设计的,内存块的分配和释放是非线性的,堆内存管理器会在可用的内存块中寻找合适的块进行分配,因此可能会出现碎片化,导致堆内存分布不连续。
2. int i=0; i++ ; 两个线程同时执行10000次,i最终的数值是多少?
如果没有任何同步机制,如锁(Lock)或原子操作(Atomic Operation),最终的值是不确定的。这是因为i++不是一个原子操作,它实际上由三部分组成:读取i的值、将i的值加1、将结果写回i。在两个线程同时操作时,可能会出现竞争条件(Race Condition),导致其中一个线程的更新被覆盖。因此,i的最终值可能小于20000。
3. 全局变量,是在堆区 还是栈区? 【这个纯属是挖坑】
全局变量通常存储在静态存储区,也称为数据段(Data Segment),而不是堆区或栈区。数据段在程序加载时分配,并在程序的整个生命周期内保持不变。它与栈区、堆区分开管理
4. 进程和线程的区别 详解推荐 :
面试官:说说什么是进程?什么是线程?区别?
进程是操作系统分配资源的基本单位,每个进程有独立的地址空间、文件描述符、堆栈等资源。进程之间的通信需要使用进程间通信(IPC)机制,如管道、信号、共享内存等。
线程是进程内的一个执行单元,多个线程共享进程的资源,如代码段、数据段、文件描述符等。线程之间的通信更加快捷,因为它们共享同一进程的内存空间。
主要区别:进程是独立的,彼此隔离,切换进程开销较大;线程是共享资源的,切换线程的开销较小。
5. 我们输入网址,打开浏览器这一过程,会发生什么情况? 【偏网络协议】 详解推荐:
从浏览器输入 URL 到页面展示过程发生了什么?
当你在浏览器中输入网址并按下回车时,以下步骤会发生:
- DNS解析:
浏览器先检查本地缓存是否有该域名的IP地址。如果没有,会向DNS服务器发送查询请求,将域名解析为IP地址。
- 建立TCP连接:
使用获取的IP地址,浏览器会通过三次握手(TCP三次握手)建立与目标服务器的TCP连接。
- 发送HTTP请求:
连接建立后,浏览器向服务器发送一个HTTP请求,请求目标网页。
- 服务器响应:
服务器处理请求并返回相应的HTTP响应,通常包括HTML文档、CSS、JavaScript文件和图像等。
- 浏览器渲染:
浏览器接收响应数据,解析HTML文档,加载和执行CSS和JavaScript,最终渲染网页内容到屏幕上。
- 关闭连接:
在页面加载完成后,浏览器和服务器会通过四次挥手(TCP四次挥手)关闭连接。
6. 进程打开网页,和线程打开网页,有什么区别?
进程打开网页:如果一个进程负责打开网页,它将独立管理所有的资源,包括内存、文件句柄等。如果进程崩溃,所有资源都将被释放。比如,现代浏览器通常会为每个标签页创建一个独立的进程,以提高稳定性和安全性。
线程打开网页:如果一个线程负责打开网页,它将共享进程的资源。多个线程可以同时处理网页内容,例如渲染、加载资源等。线程之间的切换开销较小,但如果一个线程发生崩溃,可能会影响同一进程中的其他线程。
7. 什么叫死锁,死锁产生的条件是什么?
死锁是指在多线程或多进程环境中,两个或多个进程或线程因相互等待对方持有的资源而进入的一种无限等待状态。换句话说,所有涉及的进程或线程都在等待某个资源,而这些资源被其他进程或线程持有,因此所有进程或线程都无法继续执行。
死锁产生的四个必要条件(同时满足):
互斥条件(Mutual Exclusion):资源不能共享,只能由一个线程或进程占用。如果一个资源已经被占用,其他线程或进程必须等待。
占有并等待(Hold and Wait):一个线程或进程已经获得了某个资源,并且在等待其他资源的同时不释放它已经持有的资源。
不可剥夺条件(No Preemption):已分配的资源不能被强行剥夺,只有持有该资源的线程或进程可以主动释放它。
循环等待(Circular Wait):存在一个线程或进程的循环链,其中每个线程或进程都在等待链中的下一个线程或进程所持有的资源。
8. 读写锁,了解过吗? 【建议多写代码 ,看视频实例,我看网上太乱了,死记硬背不行】
读写锁(Read-Write Lock)是一种同步原语,用于在多线程环境下保护共享资源。它允许多个线程同时读取共享资源,但在有线程正在写入该资源时,其他线程不能读或写。这种锁的设计目的是优化在读操作多于写操作的场景下的性能。
读写锁的工作原理
读写锁通常提供两种操作:
读锁(共享锁):
允许多个线程同时持有读锁,从而可以同时读取共享资源。
只要没有线程持有写锁,读锁是可以被多个线程同时持有的。
当一个线程持有读锁时,其他线程仍然可以获得读锁,但无法获得写锁。
写锁(独占锁):
写锁是独占的,一次只能有一个线程持有写锁。
当一个线程持有写锁时,其他线程不能获得读锁或写锁。
写锁通常被用于修改共享资源,当写操作结束后,锁会被释放,其他线程才能继续读取或写入该资源。
读写锁的优势
读写锁的主要优势在于它能够在读操作多于写操作的情况下提高并发性能。相比于传统的互斥锁(Mutex),读写锁在以下场景中更具优势:
高读并发:在系统中有大量的读操作,且很少有写操作的情况下,读写锁允许多个读线程并发执行,而不需要像互斥锁那样每次都排队等待。
减少写锁等待时间:当写操作较少时,写线程可以在没有其他线程读或写时获得锁,从而迅速进行写操作。
读写锁的使用示例
以伪代码为例,描述如何使用读写锁:
ReadWriteLock lock;
// 读线程
lock.readLock(); // 获取读锁
// 执行读操作
lock.readUnlock(); // 释放读锁
// 写线程
lock.writeLock(); // 获取写锁
// 执行写操作
lock.writeUnlock(); // 释放写锁
注意事项
读写锁的选择:在读写锁设计中,必须仔细考虑读写锁的选择策略。某些实现可能会导致读线程“饥饿”(即读线程长时间无法获得锁),因为写线程总是被优先考虑。
适用场景:读写锁在读操作频繁而写操作较少的场景下表现良好,但在写操作频繁的情况下,性能可能不如简单的互斥锁好。
总结
读写锁是用于优化多线程环境下共享资源访问的一种锁机制。它允许在没有写操作的情况下多个线程同时读取数据,从而提高系统的并发性能。然而,在使用读写锁时,需要权衡读写频率、锁的实现方式以及可能的线程“饥饿”问题。
9. DNS 网络解析,以及域名这些了解吗?【我个人觉得是,我当时第5个问题,没有回答好,才问的这个知识点】
DNS(域名系统,Domain Name System)是互联网基础设施的重要组成部分,用于将人类可读的域名(如www.example.com)解析为计算机可以理解的IP地址(如192.0.2.1)。域名系统使得我们可以使用易记的名字访问互联网资源,而不需要记住复杂的数字IP地址。
DNS解析过程
当你在浏览器中输入一个域名并按下回车时,以下过程会发生:
- 本地缓存检查:
浏览器首先检查本地的DNS缓存,看看是否已经存储了该域名对应的IP地址。如果有,浏览器将直接使用该IP地址。
如果浏览器的缓存中没有记录,它会检查操作系统的DNS缓存。
- Hosts文件检查:
操作系统还会检查本地的hosts文件,该文件可以手动指定某些域名对应的IP地址。
- DNS查询过程:
如果本地缓存和hosts文件都没有找到对应的IP地址,操作系统会向配置的DNS服务器(通常是ISP提供的DNS服务器)发出查询请求。
递归查询:本地DNS服务器接收到查询请求后,会先检查自己的缓存,如果没有,则会递归地查询其他DNS服务器,直到找到正确的IP地址。查询过程可能涉及以下几个步骤:
根域名服务器:DNS服务器首先向根域名服务器请求,根服务器会返回顶级域(如.com、.net等)对应的权威DNS服务器地址。
顶级域名服务器(TLD Server):然后DNS服务器向顶级域名服务器发送请求,TLD服务器会返回该域名对应的权威DNS服务器地址。
权威DNS服务器:最终,DNS服务器向权威DNS服务器发送请求,权威服务器返回域名对应的IP地址。
- 返回IP地址:
DNS服务器将找到的IP地址返回给操作系统,操作系统将其提供给浏览器。
- 缓存IP地址:
该IP地址通常会被缓存,以便后续请求可以更快地处理(直到缓存失效为止)。
建立连接并访问网站:
浏览器使用解析到的IP地址,与目标服务器建立TCP连接(通过三次握手),然后发送HTTP或HTTPS请求,获取网页内容。
- 域名结构【有问到】
域名的结构是分层的,从右到左依次为顶级域、二级域、三级域等。以 www.example.com 为例:
.com:顶级域(TLD,Top-Level Domain),是最高级别的域名部分。
example:二级域(SLD,Second-Level Domain),通常由域名注册者自由选择。
www:三级域或子域(Subdomain),通常用于区分不同的服务或站点(如www、mail等)。
- 域名注册与管理
域名注册:用户可以通过域名注册商(如GoDaddy、Namecheap等)注册一个可用的域名。注册时,用户需要指定域名对应的DNS服务器(即NS记录),这些服务器负责域名的解析。
域名管理:域名注册后,用户可以在DNS服务器上配置不同的DNS记录,以管理域名和其对应的IP地址或服务。
- 常见问题与挑战
DNS缓存中毒(DNS Cache Poisoning):攻击者通过向DNS服务器注入错误的IP地址来劫持流量,使用户访问错误的网站。
DNS劫持:恶意或不安全的DNS服务器可能会返回错误的IP地址,导致用户被重定向到恶意网站。
DNS是互联网的基础之一,它的可靠性和安全性直接影响到用户访问网站和其他网络服务的体验和安全性。了解DNS的工作原理、记录类型和常见问题对于网络相关的开发、运维和安全工作都非常重要。
场景题
1. 这两个变量的区别?
int a =1
int main(){
int b=1;
}
内存分为拿几个区?
在典型的计算机系统中,内存通常分为以下几个主要区域(区段):
-
代码段(Code Segment):
也称为文本段(Text Segment),用于存储程序的机器代码,即程序的可执行指令。这个区域是只读的,防止程序意外修改指令。
数据段(Data Segment): -
静态区(Static Segment):
用于存储已初始化的全局变量和静态变量。变量在程序开始执行时就被分配内存,并在整个程序运行期间一直存在。 -
BSS段(Block Started by Symbol Segment):
用于存储未初始化的全局变量和静态变量。这部分内存在程序启动时会被初始化为0。 -
堆区(Heap Segment):
堆区用于动态分配内存(如使用malloc或new)。由程序员在运行时手动分配和释放,堆的大小通常是动态变化的。 -
栈区(Stack Segment):
栈区用于存储函数的局部变量、函数参数和返回地址。当函数调用时,会在栈区分配一个栈帧(Stack Frame),函数执行结束后栈帧会被释放。栈的大小是动态变化的,但受限于系统的栈大小限制。
那这变量 a 和 b 分别是在哪两个区? 【追问】
变量a:这是一个全局变量,生命周期从程序开始到结束。它被存储在**数据段(静态区)**中,因为它是一个已初始化的全局变量。
变量b:这是一个局部变量,它的作用域仅限于main函数内部。b被存储在栈区中。当main函数调用时,b会被分配到栈中;当main函数执行结束后,b的内存会被释放。
总结:
变量a:位于数据段(静态区)。
变量b:位于栈区。
内联函数 和 宏定义的区别 ?
常见的数据结构有哪些?
代码题
第1题: 在32位机器中,这些变量占的内存是多少字节的?
struct a{
int a ;
double b;
char c;
};
struct b{
int a;
char b;
double c;
char d;
}
struct b{
int a;
char b;
double c;
}
解题思路:
1、如果忘记具体的 字节,可以使用siezeof() 输出
2、最容易忘记的就是,对齐操作 下面是详解
在32位机器中,结构体中的变量内存对齐是一个重要的考虑因素。不同的编译器可能有不同的对齐方式,但通常会根据最大成员的字节数进行对齐。在32位系统中,int占用4个字节,double占用8个字节,char占用1个字节。
我们来看每个结构体占用的内存。
结构体 a:
struct a{
int a; // 4字节
double b; // 8字节
char c; // 1字节
};
所以说: 4 + 4 + 8+ 1 + 7 = 24
结构体 b:
struct b{
int a; // 4字节
char b; // 1字节
double c; // 8字节
char d; // 1字节
}
int a:
int 类型通常占用4个字节,并且按4字节对齐。
因此,a 占用偏移量0到3,共4个字节。
char b:
char 类型通常占用1个字节,不需要特殊的对齐。
b 放置在 int a 之后,占用偏移量4。
填充:
因为下一个成员 double c 需要按8字节对齐,而当前偏移量是5,因此编译器会在此处插入3个字节的填充,使得下一个成员 double c 从偏移量8开始。
double c:
double 类型占用8个字节,并且按8字节对齐。
c 放置在偏移量8到15,共8个字节。
char d:
char 类型占用1个字节,不需要特殊的对齐。
d 放置在 double c 之后,占用偏移量16。
最后的对齐处理:
结构体的总大小需要是最大对齐要求的整数倍。在这个例子中,最大对齐要求是8字节(因为 double 需要8字节对齐)。
目前,结构体的大小是17字节(0到16)。为了使总大小符合8字节对齐的要求,编译器会在最后再添加7个字节的填充,使得总大小达到24字节。
计算过程总结
int a 占用4字节(偏移量0到3)。
char b 占用1字节(偏移量4)。
填充3字节,使下一个double c 能从偏移量8开始。
double c 占用8字节(偏移量8到15)。
char d 占用1字节(偏移量16)。
填充7字节,使结构体总大小达到24字节(符合8字节对齐的要求)。
因此,结构体 b 的总大小为24个字节。这个大小是因为编译器为满足内存对齐要求,在某些成员之间和结构体末尾添加了填充字节。
int a占用4字节,放在偏移量0到3。
char b占用1字节,放在偏移量4。
double c占用8字节,放在偏移量8到15(为了满足double的8字节对齐要求,通常会在此之前有3个字节的填充)。
char d占用1字节,放在偏移量16。
总大小:24个字节。
所以: 4+ 1+ 3 + 8 + 1+ 7 = 24
结构体 c:
struct c{
int a; // 4字节
char b; // 1字节
double c; // 8字节
}
int a占用4字节,放在偏移量0到3。
char b占用1字节,放在偏移量4。
double c占用8字节,放在偏移量8到15(为了满足double的8字节对齐要求,通常会在此之前有3个字节的填充)。
总大小:16个字节。
总结:
结构体 a 占用 24 字节。
结构体 b 占用 24 字节。
结构体 c 占用 16 字节。
第2题: 二叉树最大路径和 【深度遍历】
root = {1,2,3}
最大路径是: 2-1-3,最大路径和是6
#include <iostream>
#include <algorithm>
using namespace std;
// 定义二叉树节点结构
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
int maxPathSum(TreeNode* root) {
int maxSum = INT_MIN;
maxGain(root, maxSum);
return maxSum;
}
private:
// 递归函数,计算从当前节点出发的最大路径和
int maxGain(TreeNode* node, int &maxSum) {
if (node == NULL) return 0;
// 递归计算左子树和右子树的最大贡献值
int leftGain = max(maxGain(node->left, maxSum), 0);
int rightGain = max(maxGain(node->right, maxSum), 0);
// 当前节点的最大路径和
int currentMaxPath = node->val + leftGain + rightGain;
// 更新全局最大路径和
maxSum = max(maxSum, currentMaxPath);
// 返回当前节点的最大贡献值
return node->val + max(leftGain, rightGain);
}
};
int main() {
// 创建二叉树 {1, 2, 3}
TreeNode* root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
Solution sol;
int result = sol.maxPathSum(root);
cout << "最大路径和是: " << result << endl;
return 0;
}