腾讯实习一面面经

216 阅读4分钟

1. 三道编程

(1)ip地址转整数

#include<iostream>
#include<string.h>
#include<math.h>
using namespace std;

int main()
{
	char ip[] = "192.168.1.0";
	int i = 0;
	unsigned int ip_int = 0;
	char* ptr;
	char* p = strtok_s(ip, ".",&ptr);
	int a[4] = { 0 };
	
	for (; i < 4 && p != nullptr; i++)
	{
		a[i] = atoi(p);
		p = strtok_s(NULL, ".", &ptr);
	}
	for (i = 0; i < 4; i++)
	{
		if (a[i] > 0 && a[i] < 255)
		{
			ip_int += a[i] * pow(256, 3-i);
		}
	}
	cout << ip_int << endl;
}

运行结果: image.png

(2)有一链表的头节点head,请将所给链表按照val变量的大小升序排列并返回排序后的链表。 结构如下:

struct ListNode
{
    int val;
    ListNode* next;
}

思路: 对链表自顶向下归并排序的过程如下。

  • 找到链表的中点,以中点为分界,将链表拆分成两个子链表。寻找链表的中点可以使用快慢指针的做法,快指针每次移动 22 步,慢指针每次移动 11 步,当快指针到达链表末尾时,慢指针指向的链表节点即为链表的中点。

  • 对两个子链表分别排序。

  • 将两个排序后的子链表合并,得到完整的排序后的链表。

上述过程可以通过递归实现。递归的终止条件是链表的节点个数小于或等于1,即当链表为空或者链表只包含 1个节点时,不需要对链表进行拆分和排序。

代码实现:

//合并
ListNode* Merge(ListNode* l1,ListNode* l2)
    {
        ListNode* l = new ListNode(0);
        ListNode* head = l;
        while(l1 != nullptr && l2 != nullptr)
        {
            if(l1->val < l2->val)
            {
                l->next = l1;
                l1 = l1->next;
            }
            else
            {
                l->next = l2;
                l2 = l2->next;
            }
            l = l->next;
        }
        l->next = l1 ? l1 : l2;
        return head->next;
    }
    //递归
    ListNode* MergeSort(ListNode* head,ListNode* tail)
    {
        //递归终止条件
        if(head == nullptr)
        {
            return head;
        }
        if(head->next == tail)
        {
            head->next = nullptr;
            return head;
        }

        ListNode* slow = head;
        ListNode* fast = head;
        while(fast != tail && fast->next != tail)
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        return Merge(MergeSort(head,slow),MergeSort(slow,tail));
    }
    ListNode* sortList(ListNode* head) {
        return MergeSort(head,nullptr);
    }

(3)多线程交替输出数字

给定n个线程,不用任何原子类型或者锁,使其交替输出0-m的数字

打印一定要先打印,修改flag条件一定要最后修改。防止一修改flag,其他线程就通过了if条件

#include <iostream>
#include <thread>
#include <vector>

using namespace std;

// num_thread个线程输出
// 1-n   0-m
int flag = 1;
int num = 0;
const int num_thread = 3;
const int num_max = 7;

// 线程函数
void print(int tid) {
	while (true) {
		if (tid == flag) {
			// 打印一定要先打印,修改flag条件一定要最后修改。防止一修改flag,其他线程就通过了if条件
			cout << tid << " " << num << endl;
			num = (num + 1) % (num_max + 1);            // 0 ~ num_max
			flag = flag % num_thread + 1;         // 1 ~ num_thread
		}
	}
}

int main() {
	vector<thread> vec;
	for (int i = 1; i <= num_thread; i++) {
		vec.push_back(thread(print, i));
	}
	for (thread& t : vec) {
		t.join();
	}
	return 0;
}

image.png

只打印一次 0~num_max

#include <iostream>
#include <thread>
#include <vector>

using namespace std;

// num_thread个线程输出
// 1-n   0-m
int flag = 1;
int num = 0;
const int num_thread = 4;
const int num_max = 7;

// 线程函数
void print(int tid) {
	while (true) {
		if (num == num_max + 1) {
			return;
		}
		if (tid == flag) {
			if (num == num_max) {
				cout << tid << " " << num << endl;
				// 这里修改num,让num超过num_max,表示可以退出程序了,让其他两个线程退出
				num++;
				// 不修改flag,其他两个线程就不会进入if(tid == flag)
				return;
			}
			cout << tid << " " << num << endl;
			num = (num + 1) % (num_max + 1);            // 0 ~ num_max 
			flag = flag % num_thread + 1;               // 1 ~ num_thread
		}
	}
}

int main() {
	vector<thread> vec;
	for (int i = 1; i <= num_thread; i++) {
		vec.push_back(thread(print, i));
	}
	for (thread& t : vec) {
		t.join();
	}
	return 0;
}

image.png

2. I/O复用有几种,说下优缺点?

参考博客:blog.csdn.net/qq_41721746/article/details/124234462

3. 虚函数的优缺点

  • 优点:在有可能成为父类时,虚函数可以被同名子类函数覆盖,安全;基类的析构函数有时必须实现成虚函数,就是在基类的指针(引用)指向堆上new出来的派生类对象的时候,delete pb(基类的指针),它调用析构函数的时候,必须发生动态绑定,否则会导致派生类的析构函数无法调用。
  • 缺点:一个类里面定义了虚函数,那么编译阶段,编译器给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要储存的内容是RTTI指针和虚函数地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区,一个类里面定义了虚函数,那么这个类定义的对象,其运行时,内存中开始部分,多存储一个vfptr虚函数指针,指向相应类型的虚函数表vftable,需要额外的一点点运行时间(不过现在机子很快,这些时间可以无视)

4. 介绍单例模式

一个类不管创建多少次对象,都只能得到一个该对象的实例。常用到的比如日志模块、数据库模块。实现方法:

  • 构造函数私有化
  • 获取类的唯一实例对象的接口方法(static方法)
  • 删除默认的拷贝构造和赋值重载(单例模式只允许一个实例)

单例模式分为饿汉式单例模式和懒汉式单例模式

  • 饿汉式单例模式:还没有获取实例对象,实例对象就已经产生了。线程安全的,但是获取在软件启动的时候,并没有使用到这个对象,然而这个对象已经产生,启动时间长,程序启动就得加载,比较浪费资源。
  • 懒汉式单例模式:唯一的实例对象,直到第一次获取它的时候才产生。将唯一的实例对象定义为指针,该指针初始化为nullptr(静态变量在类外初始化),当第一次调用时,new()一个对象给指针,后续调用时,直接返回该指针,是线程安全的,也不浪费资源。

5. 介绍malloc、new和delete、free区别(问的是底层实现还是区别忘了,就说下区别把)

malloc和free,称作C的库函数;new和delete,称作运算符

(1)malloc和new的区别

  • malloc按字节开辟内存的;new开辟内存时需要指定类型 new int[10],所以malloc开辟内存返回的是void* operator new->int*
  • malloc只负责开辟空间,new不仅仅有malloc的功能,还可以进行数据初始化
  • malloc开辟内存失败返回nullptr指针;new抛出的是bad_alloc类型的异常

(2)free和delete的区别

delete:是先调用析构函数,再free,单个不加[],数组加[]。

free:函数调用,传起始地址。

  • delete 是操作符,而 free 是函数;

  • delete 用于释放 new 分配的空间,free 有用释放 malloc 分配的空间;

  • free 不会调用对象的析构函数,而 delete 会调用对象的析构函数;

  • 调用 free 之前需要检查要释放的指针是否为 NULL,使用 delete 释放内存则不需要检查指针是否为 NULL;

6. 进程、线程和协程的区别

进程:一个正在运行的程序,是一个动态的概念。是操作系统进行分配的基本单位,有自己独立的运行空间,进程创建先消耗资源大,切换开销大

线程:进程内部的一条执行路径(序列)。是CPU调度的基本单位,共享进程中的地址空间,线程创建消耗资源较小,切换开销小

协程:协程是一种比线程更加轻量级的存在。协程完全由程序所控制(在用户态执行),带来的好处是性能大幅度的提升。

一个操作系统中可以有多个进程;一个进程可以有多个线程;同理,一个线程可以有多个协程。

线程和进程的区别

◼ 进程是资源分配的最小单位,线程是 CPU 调度的最小单位

◼ 进程有自己的独立地址空间,线程共享进程中的地址空间

◼ 进程的创建消耗资源大,线程的创建相对较小

◼ 进程的切换开销大,线程的切换开销相对较小

◼ 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

◼ 进程的并发性较低,线程的并发性较高

线程和协程的区别如下:

 1. 线程是操作系统的资源,线程的创建、切换、停止等都非常消耗资源,而创建协程不需要调用操作系统的功能,编程语言自身就能完成,所以协程也被称为用户态线程,协程比线程轻量很多;

 2. 线程在多核环境下是能做到真正意义上的并行,而协程是为并发而产生的;

 3. 一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行;

 4. 线程进程都是同步机制,而协程则是异步;

 5. 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力;

 6. 操作系统对于线程开辟数量限制在千的级别,而协程可以达到上万的级别。