腾讯音乐-社交后台开发岗

一面

C++方面:
  • C++的虚函数是怎么实现的?
  • 虚函数的用法:虚函数可以让派生类重写基类的成员函数实现多态。虚函数实现多态的机制,严格来说是动态多态,是在出现运行的时候实现的。
  • 虚函数的实现原理:每个虚函数都会有一个与之对应的虚函数表,该虚函数表的实质是一个函数指针数组,存放的是每一个对象的虚函数入口地址。对于一个派生类来说,他会继承基类的虚函数表同时增加自己的虚函数入口地址。如果派生类重写了基类的虚函数的话,那么继承过来的虚函数入口地址将被派生类的重写虚函数入口地址替代。那么在程序运行时会发生动态绑定,将父类指针绑定到实例化的对象实现多态。
  • 通过以下方式来调用派生类的方法:首先基类方法必须为virtual修饰的虚函数,派生类继承基类。其次必须通过基类的指针或引用,指向或引用派生类的对象,并访问派生类方法
  • 虚表是存在哪里的?是类对象中吗,为什么?
    C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区。同一类的不同对象共用一张虚函数表,不存在所谓的逻辑上共用,物理上不同的问题。
Go方面:
  • 以下代码会输出什么?(示例1答对了,示例2答错了)
    考察点:在对切片进行append扩容后,因为会重新分配新的地址空间,所以append之后的操作都是在新的地址空间上进行的!
		// 示例1:
		array1 := []int{1, 2, 3, 4}
		array2 := array1
		array1[0] = 888
		array1 = append(array1, 999)

		fmt.Println("array1: ", array1) // 888 2 3 4 999
		fmt.Println("array2: ", array2) // 888 2 3 4
		// 因为array1在append扩容前先执行了下标值修改的操作,此时的array2和array1还是共用同一份地址空间的,所以 array1[0] = 888 会影响到array2的值
		
		
		
		// 示例2:如果把 array[0] = 888 移动到最后呢?
		array1 := []int{1, 2, 3, 4}
		array2 := array1
		array1 = append(array1, 999)
		array1[0] = 888

		fmt.Println("array1: ", array1) // 888 2 3 4 999
		fmt.Println("array2: ", array2) // 1 2 3 4
		// 因为 array2 := array1 一开始就赋值了,而之后array1在append扩容后,其重新分配了地址空间,然后array1在新分配的地址空间上进行下标操作,不会影响到array2的值



		// 示例3:
		array1 := []int{1, 2, 3, 4}
		array2 := &array1                                   // 指针赋值
		array3 := array1                                    // 直接赋值
		fmt.Printf("%p %p %p\n\n", array1, *array2, array3) // 起初:array1 = *array2 = array3

		// 对array2写操作
		(*array2)[0] = 555                                  // 指针赋值,要先*解引用,再对数组进行下标操作
		fmt.Println("array1-1: ", array1)                   // 555 2 3 4
		fmt.Println("array2-1: ", array2)                   // 555 2 3 4
		fmt.Println("array3-1: ", array3)                   // 555 2 3 4
		fmt.Printf("%p %p %p\n\n", array1, *array2, array3) // array1 = *array2 = array3

		// 对array3写操作
		array3[0] = 777
		fmt.Println("array1-2: ", array1)                   // 777 2 3 4
		fmt.Println("array2-2: ", array2)                   // 777 2 3 4
		fmt.Println("array3-2: ", array3)                   // 777 2 3 4
		fmt.Printf("%p %p %p\n\n", array1, *array2, array3) // array1 = *array2 = array3

		// 对array1进行append操作
		array1 = append(array1, 999)
		fmt.Println("array1-3: ", array1)                   // 777 2 3 4 999
		fmt.Println("array2-3: ", array2)                   // 777 2 3 4 999
		fmt.Println("array3-3: ", array3)                   // 777 2 3 4
		fmt.Printf("%p %p %p\n\n", array1, *array2, array3) // array1 = *array2 != array3:此时因为array1发生扩容,array1、*array2会申请新的地址空间

		// 起初array1、*array2、array3三者地址都一致
		// array2 := &array1:因为*array2是对array1的指针方式赋值(这俩切片地址一致),所以只要对*array2或array1其中之一进行append扩容,就会重新分配地址空间,彼此互相影响,且array1的结果始终和*array2一致
		// array3 := array1:而array3是对array1的直接赋值,起初他俩地址一致结果也一致,但是在对array1或*array2进行append扩容后,array1和*array2就会重新分配地址空间,而array3的地址空间则保持不变
		// 总结:在对切片进行append扩容后,因为会重新分配新的地址空间,所以append之后的操作都是在新的地址空间上进行的!
  • C++ map 和 Go map 有什么区别?
    C++ map底层是红黑树实现的(具体底层原理不是很清楚),Go map底层是hash链表实现的,然后讲述了下Go map的底层原理。
  • 如果Go map的冲突链表太长了,如何优化查询效率呢?
    可以通过增加多级索引链表,来加快查询速度,类似于Redis zset底层的跳表实现原理,可参考:Redis zset 底层数据结构之跳表
  • Redis hash,zset 数据类型底层是如何实现的?
  • Redis 是如何处理多个网络请求的?
    epoll 网络io多路复用模型
  • Go map是否是线程安全的?如何更好的实现线程安全的map?
    对于如何实现线程安全的map,我讲解了下sync.Map的底层实现方式。具体可参考文章:Golang sync.Map 原理(两个map实现 读写分离、适用读多写少场景)

    sync.Map 的实现原理可概括为:
    • 通过 read 和 dirty 两个字段实现数据的读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上
    • 读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty
    • 读取 read 并不需要加锁,而读或写 dirty 则需要加锁
    • 另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据更新到 read 中(触发条件:misses=len(dirty)
操作系统:
  • 对进程、线程、协程的理解?

  • 进程能互相访问对方的地址吗?(我回答说 IPC-进程间通信)

  • 进程切换、线程切换、协程切换,三者区别?为什么进程切换比线程切换代价大,效率低?

    • 线程切换比协程切换开销大的原因
      上下文切换代价小: Goroutine 上下文切换只涉及到三个寄存器(SP/ PC / DX)的值修改;而线程的上下文切换则需要涉及模式切换(从用户态切换到内核态)、以及 16 个寄存器、PC、SP…等寄存器的刷新;
      注:PC(程序计数器 - program counter)、SP(堆栈指针 - stack pointer)、DX(数据寄存器)
    • 进程切换比线程切换开销大的原因:关键在于进程切换涉及到TLB的失效及更新,线程不涉及。
      更多可参考:为什么进程切换比线程切换代价大,效率低?【TLB:页表缓存/快表】
  • 进程地址空间结构?
    代码段、数据段、BSS-未初始化段、堆、栈、文件内存映射区、进程参数及其系统变量…

  • 用户空间和内核空间通信的几种方式?
    具体过程是先把数据从用户空间读到内核空间,经过内核处理后再把数据拷贝回用户空间,并从内核态切换到用户态。更多参考:Linux用户空间与内核空间交互的几种方式

    • 通过系统函数调用(syscall)
      • get_uer/set_user :获取用户空间指定数据/将数据放到内核指定位置
      • copy_from_user/ copy_to_user:从用户空间复制数据到内核空间 / 从内核复制数据到用户空间
    • 虚拟文件系统
    • 文件内存映射mmap
    • 信号
      • 中断(interrupt)
  • C语言中通过指针访问内存地址空间,可能会发生什么?
    可能会访问到未分配的内存空间,导致段错误

  • 动态链接和静态链接?编译后的产物有什么区别?
    静态链接的过程就已经把要链接的内容已经链接到了生成的可执行文件中,就算你在去把静态库删除也不会影响可执行程序的执行;而动态链接这个过程却没有把内容链接进去,而是在执行的过程中,再去找要链接的内容,生成的可执行文件中并没有要链接的内容,所以当你删除动态库时,可执行程序就不能运行动态链接生成的可执行文件要比静态链接生成的文件要小一些
    另外:
    1、静态链接库执行速度比动态链接库快。(执行过程不需要找链接的内容)
    2、动态链接库更节省空间。(可执行文件未写入要链接的内容)

网络:
  • 拥塞控制和流量控制的作用?

  • 三次握手要等待几个RTT?
    单独的三次握手需要等待1.5个RTT,参考 TCP三次握手(含常见面试题)详解,另外:

    • 第三次握手不携带数据时的HTTP请求:经典的TCP三次握手,从握手开始,到握手完成,直到客户端请求的数据从服务器返回为止,需要:
      1.5 RTT(三次握手) + 1 RTT(数据往返) = 2.5 RTT
    • 第三次握手携带数据时的HTTP请求:一般来说,比较积极的TCP在第三次握手时,就已经顺便携带了数据请求,需要的RTT将减小为:
      1 RTT(前两次握手) + 1 RTT(第三次握手 + http数据往返) = 2 RTT
  • 第三次握手能否携带数据?为什么第三次握手可以携带数据,而第一次和第二次握手都不可以?
    第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。

在这里插入图片描述

  • 三次握手能否变为两次握手?
    不行,因为三次握手保证了双方的 发送/接收 通道的功能正常。因为TCP是面向连接且可靠的流式传输,所以要保证双方的收发功能正常,这样才能保证双发能够可靠的传输数据。

  • 如何查找具体是哪个进程连接到了远程数据库服务?
    登录远程数据库使用 show processlist ;命令查看当前连接进数据库的Host连接信息(ip:port);然后回到跳板机通过netstat -anp | grep 具体端口号即可查看到具体连接的进程信息。
    在这里插入图片描述
    而如果知道启动命令时,可以通过 ps -ef | grep 具体启动命令来找到对应的进程。

  • Go程序占用内存过多,排查思路?
    Go pprof工具,go tool pprof xxxtoplist
    另外在排查cpu占用过高的问题时,可通过 top + Go的调试工具dlv,具体可参考:go程序cpu过高问题排查方法

  • 算法题:215. 数组中的第K个最大元素
    思路:小顶堆(时间/空间复杂度更优)/大顶堆,具体可参考:堆排序在topK场景题中的应用及原理

  • 算法题:利用协程并发处理 doSomething() 操作

    • 注意点:
      • 方式1 不通过channel实现:多个协程直接累加 res += val 时,需要加 mutex 锁互斥处理
      • 方式2 通过无缓冲的channel实现:需要另起协程取channel中的数据并进行累加和计算
      • 方式3 通过有缓冲的channel实现:与方式2类似,不过方式2更优~
func doSomething(number int) (int, error) {
	// do something
	return number * number, nil
}

// sum(dosomethng(numbers[i]))
func doSomethingBatch(numbers []int) (int, error) {
	res := 0
	var wg sync.WaitGroup

	ch := make(chan int)
	defer close(ch)
	// defer func() {
	// 	close(ch)
	// }()

	for i := 0; i < len(numbers); i++ {
		wg.Add(1)
		go func(i int) {
			subRes, err := doSomething(numbers[i])
			if err != nil {
				// 错误处理
			}

			ch <- subRes
			wg.Done()
		}(i)
	}

	go func() {
		for {
			if val, ok := <-ch; ok {
				res += val
			}
		}
	}()

	wg.Wait() // 阻塞

	return res, nil
}

结果

可惜的是,虽然自我感觉面试问题回答对了7、8成,面试官也很有耐心,会跟我讨论问题,但最后面试还是挂了。

面试官的反馈说:其他的表现都还可以的,只是对操作系统底层的了解不够深入,比如:

  • 进程切换开销比线程大的本质原因:进程切换时存在TLB的失效及更新,而线程不存在…
  • 动态链接和静态链接我也回答的不是很准确

这些问题之前也确实了解的不是很彻底,过不了也确实是自身问题。

一时间变得迷茫了,总觉得面试要考察的问题点太多了,真的很难面面俱到,有时候也不知所措…每当自己觉得面试表现还不错时,结果就是给我泼了一盆凉水,让我清醒下来…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值