C++开发面经

时间:2024-11-07 15:01:52

0.每次必问(无标准答案)

0.1 自我介绍

  •  学长/学姐,你好,我叫XXX, 今年大四在校生,来自XXX,学的专业是计算机科学与技术专业,熟悉c++编程,数据结构,算法,mysql,linux操作系统和常用命令,QT开发,git工具,了解python,C#,HTML,在校做过3个项目,一个是高并发内存池,一个是负载均衡项目,一个自主HTTP服务器,
    家庭情况:我来自四川省内江市的一个农业家庭,家里世代务农,双亲健在,还有一哥哥在银行上班,
    个人爱好:听音乐、打游戏,看电影、看小说,
    最近的个人规划:没有考研的打算,想尽快的找到工作,缓解家庭负担,

0.2 个人规划 

  • 最近的个人规划:没有考研的打算,想尽快的找到工作,缓解家庭负担,
    更想在公司实习提升技术实力, 承担更多的业务线 

0.3 谈谈对公司的了解

  • 需要去官网了解公司的产品和业务

0.4 个人的优点/缺点

  • 优点:做人真诚,做事认真负责,勤奋,善于学习自我感兴趣的知识和事物,守时守信,时间观念比较强
  • 缺点: 平时不主动锻炼身体,语言表达本事还有点欠缺

0.5 与同事合作沟通的能力怎么样

  • 还不错,但是还有点欠缺

0.6 平时是在哪里学习的 

  • 有****,github 
  • 有b站,,慕课网,刷题一般用leetcode

0.7  期望薪资

  • "作为应届生, 愿意服从公司的安排"
  • 8K - 10k 

 0.9 如何看待加班 

  • "作为一个新人, 应该多花时间熟悉公司的业务, 学习技术, 工作时间多长都不重要".

 0.10 为啥不考研

  • 想尽快的找到工作,缓解家庭负担
  • 而且对于 IT 行业, 在工作中结合实际的商业项目对自己的技术提升更大.

0.12 转语言学JAVE可以吗

  • 虽然我主要使用的是c++,但是我觉得语言间都是互通的,即不存在很大的转换的成本。
  • 此外,我觉得贵司的业务与具体的开发方向和我个人的兴趣非常的契合最后,我觉得,代码之外,做工程的能力也很重要。所以是没有问题的

1. 笔试中与C++语言相关

1.1 -1的源码 反码 补码

  • 源码:1000 0000 0000 0000 0000 0000 0000 0001
  • 反码:1111 1111 1111 1111 1111 1111  1111 1110 
  • 补码:1111 1111 1111 1111 1111 1111  1111 1111

1.2 static关键字,在函数中声明静态变量和在类中声明静态变量的区别

  • 在函数中时,改变了变量的生命周期和作用域,使其只能在当前文件中访问
  • 在类中时只能在类外进行定义和初始化,且不需要使用private、public、protected 访问规则
    被类的所有对象所共享

1.3 声明和定义的区别

  • 声明是告诉编译器名字的存在

  • 而定义是为名字分配内存并实现其功能

  • 在使用变量或函数之前,必须先进行声明或定义。 

1.4 数组和链表的区别

  •  数组支持随机访问,链表不支持
  • 但是链表的插入删除操作效率高,数组的插入删除效率低

 1.5 最小堆的插入删除查找的时间复杂度

  • 插入O(logN),删除O(logN),查找O(1)

1.6 归并排序

void my_sort(vector<int>& nums,int l,int r){
        if(l==r){
            return;
        }
        int mid = (l+r)>>1;
        my_sort(nums,l,mid);
        my_sort(nums,mid+1,r);
        int begin1 = l,end1 = mid;
        int begin2 = mid+1,end2 = r;
        vector<int> tmp;
        while(begin1 <= end1 && begin2 <= end2){
            if(nums[begin1] < nums[begin2]){
                tmp.push_back(nums[begin1++]);
            }
            else{
                tmp.push_back(nums[begin2++]);
            }
        }
        while(begin1 <= end1){
            tmp.push_back(nums[begin1++]);
        }
        while(begin2 <= end2){
            tmp.push_back(nums[begin2++]);
        }
        // 拷贝回去
        for(int i = 0;i < tmp.size();i++){
            nums[l+i] = tmp[i];
        }
    }
  •  memcpy(目的地址,源地址,字节数)

1.7 关于malloc的几个常见没有释放内存的错误

  1.  对空指针进行解引用操

  2. 对开辟出来的空间的越界访问

  3. 内存使用完,没有进行释放(内存泄漏)

  4. 释放内存时,指针没有指向原来空间的首地址(释放开辟的空间的部分内存)

  5. 非动态开辟的内存进行释放

  6. 对同一块内存的多次释放

1.8 const关键字

  • const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。
  •  const 修饰函数参数,使得传递过来的函数参数的值不能改变
  • const 修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。

1.9 extern 关键字

  •  extern外部变量: 叫做外部声明,被extern修饰的变量,会告诉编译器这个变量的定义需要再其他文件中查找 

1.10 条件编译

  • #ifdef #else #endif

  • #ifndef #else #endif 

  • #if #elif #else #endif

1.11 模拟实现strcpy

char* my_strcpy(char* des, const char* sou)
{
	assert(des && sou);
	char* ret = des;
	while (*des++ = *sou++)
	{
		;
	}
	return ret;
}

 1.12 模拟string的构造,赋值,拷贝构造

class my_string {
	my_string(const char* str = "") {
		_size = strlen(str);//初始时
		_capacity = _size;
		// 多开辟一个空间为了存'\0'
		_str = new char[_capacity + 1];// 为存储字符串开辟空间
		strcpy(_str, str);
	}
	my_string(const my_string& s)
		:_str(new char[s._capacity+1])
		,_size(0)
		,_capacity(0)
	{
		// 实现深拷贝
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
	}
	my_string& operator=(const my_string& s)
	{
		// 解决自己跟自己赋值
		if (this != &s) {
			// 重新申请一段空间
			delete[] _str;
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}
		return *this;
	}
private:
	char* _str;// 存储字符串
	size_t _size;//记录字符串有效长度
	size_t _capacity;// 记录字符串当前的容量

};

 1.13 vector容器的push_back接口实现

//尾插数据
void push_back(const T& x)
{
	if (_finish == _endofstorage) //判断是否需要增容
	{
		size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //将容量扩大为原来的两倍
		reserve(newcapacity); //增容
	}
	*_finish = x; //尾插数据
	_finish++; //_finish指针后移
}
  • 首先vector的实现需要3个东西
  • iterator _start; //指向容器的头
  • iterator _finish; //指向有效数据的尾
  • iterator _endofstorage; //指向容器的尾 

1.14 找出不大于n的最大质数 (sqrt优化被除数) 

bool isPrimeNum(int i)
{
    // 使用sqrt缩短范围
    int j = 0;
    for (j = 2; j < sqrt(i) + 1; j++) {
        if (i % j == 0) {
            return false;
        }
    }
    return true;
}
void function(int n)
{
    if (n < 2) {
        cout << "不存在" << endl;
    }
    // 从n n-1 n-2到 0遍历
    for (int i = n; i >= 2; i--) {
        if (isPrimeNum(i)) {
            cout << "不大于N的最大质数是: " << i << endl;
            return;
        }
    }
}
  • 使用sqrt的理由:被除数求sqrt后往往都比较小,如果不是质数能更早遇到可除数(这里偏向于数学逻辑)
  • 如果是O(n^2)的暴力循环,内循环  从小到大好

 1.15 1000个数范围是[0,999],有2个相同的数,请设计算法找出来

#include <bitset>
int function(vector<int>&v)
{
    bitset<1024*8>bt;
    for (auto e : v) {
        if (bt.test(e)) {
            return e;
        }
        bt.set(e);
    }
    return 0;
}

1.16 C++STL哈希底层是什么

  • 哈希表使用哈希函数将元素映射到一个桶中,每个桶中存储一个链表或者红黑树,哈希表的插入,删除,查找等操作的平均时间复杂度是O(1),但是最坏情况下可能达到O(N);

 1.17 如何解决hash冲突

  • 链地址法: 对于相同的哈希值,使用链表进行挂起
  • 再哈希法:提供多个哈希函数,如果第一个哈希函数计算出来的key的哈希值冲突了,则使用第二个哈希函数计算key的哈希值
  • 建立公共溢出区: 将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
  • 开放定址法:将冲突的hash值不停的映射成新的哈希值,直到不冲突为止
    • 线性探测再散列(向后面不断遍历)
    • 平方探测再散列(平方跳动)
    • 伪随机探测再散列(随机跳动)

1.18 map底层实现 

  • map是关联容器,底层使用红黑树作为底层数据结构实现,而红黑树是一种自平衡的二叉搜索树,可以保持元素的有序,并且插入,删除,查找等操作的时间复杂度都是O(logN)

1.19 各大排序时间复杂度

  • 插入,希尔都是O(N^2)
  • 堆是O(N*logN),选择O(N^2)
  • 冒泡是O(N^2),快速排序O(N*logN)
  • 归并是O(N*logN);

1.20 各大排序应用场景

  • 插入排序:小规模,部分有序的数据
  • 插入排序: 中小规模数据
  • 堆排序:任意规模的数据
  • 选择排序: 小规模数据,实际效率较低
  • 冒泡排序:小规模数据,实际效率较低
  • 快速排序:任意规模的数据

1.21 各个容器应用场景

  •  vector常用于需要频繁添加或删除元素尾部的情况
  • list 适用于频繁插入或删除元素
  • map unordered_map常用于字典或配置项存储。
  • set unordered_set适用于去重或集合运算 
  • stack和queue: 只允许在一端进行插入和删除

1.22 include<> 和 include"" 的区别

  • 第一种是:用于引用系统头文件,在系统目录中查找
  • 第二种是:引用用户头文件,在源项目路径中查找

1.23  说一下二级指针,以及使用场景

  • 二级指针是指向一级指针的地址,
  • 使用场景: a.需要对一级指针进行修改时,b.或传递的是指针数组时

1.24 野指针,以及如何避免

  • 野指针: 不确定指向地址空间的指针,和未进行初始化的指针
  • 指针定义时进行初始化,动态分配内存后,必须及时手动释放内存

1.25 说一下结构体对齐 

  • 内存对齐:编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中

内存对齐的原则:

  • 结构体变量的首地址 = min(最宽基本类型大小,对齐基数)的整除
  • 其他成员的地址偏移量 = min(成员大小,对齐基数)的整数倍 + 填充字节
  • 总大小 = min(最宽基本类型大小,对齐基数) + 填充字节

 1.26 代码编译过程

  • 预处理:头文件的展开,宏替换,去注释
  • 编译:把C/C++语言变成汇编代码;
  • 汇编:通过汇编变成以.o结尾的目标二进制文件(不可执行)
  • 链接:多个目标文件连接库进行链接的,从而生成可执行的程序 .exe 文件。

1.27 代码如何判断大小端 

​
int main() {
	int a = 1;
	char* p = (char*) & a;//char* -- 访问一个字节(第一个地址)
	if (*p == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}
 
​
  • 小端:低地址在低地址
  • 大端:低地址在高地址

1.28 用2个栈实现队列

class Solution
{
public:
	void push(int value) {
		in_st.push(value);
	}
	int pop() {
		int tmp = 0;
		if (!out_st.empty()) {
			tmp = out_st.top();
			out_st.pop();
		}
		else {
			// 转移数据
			while (!in_st.empty()) {
				out_st.push(in_st.top());
				in_st.pop();
			}
			tmp = out_st.top();
			out_st.pop();
		}
		return tmp;
	}
private:
	stack<int>in_st;
	stack<int>out_st;
};

1.29 用2个队列实现栈

class Solution
{
public:
	void push(int value) {
		if (q1.empty()) {
			q1.push(value);
		}
		else {
			q2.push(value);
		}
	}
	void pop() {
		if (q1.empty() && q2.empty()) {
			cout << "没有数据" << endl;
		}
		else if (q1.empty()) {
			while (q2.size() != 1) {
				q1.push(q2.front());
				q2.pop();
			}
			cout << q2.front() << endl;
		}
		else {
			while (q1.size() != 1) {
				q2.push(q1.front());
				q1.pop();
			}
			cout << q1.front() << endl;
		}
	}
private:
	queue<int>q1;
	queue<int>q2;
};

1.30 为什么vector遍历的速度比list快

  •  Vector在C++ STL中是一个动态数组,它实现了随机访问(即通过索引直接访问元素),这使得遍历速度通常比List(双向链表)更快。

  • List的遍历操作需要从头开始逐个节点访问,虽然插入和删除效率较高,但对于查找或迭代,由于其线性查找性质,速度相对较慢。

 1.31 模板的特化 与偏特化

  • 特化和函数重载有点类似,特化要和原来的模板产生不同,才能够区别并分开 

与QT技术相关

2.1 QT信号与槽函数

  • 信号源: 由那个控件发出的信号

  • 信号类型: 用户进行不同的操作,就可能会触发不同的信号

  • 信号处理的方式: 槽(slot) -> 函数,这个函数的本质就是一种回调函数(callback)

  • Qt中可以使用connect这样的函数,把一个信号和一个槽关联起来,后续只要信号触发了,Qt就会自动执行槽函数

  • 而在Qt中,一定是关联信号 和 槽 ,然后触发这个信号,顺序不能颠倒

3. 笔试中与Linux技术相关

3.1 TCP三次握手流程

  • 第一次握手:客户端向服务器发送的报文当中的SYN位被设置为1,表示请求与服务器建立连接

  • 第二次握手:服务器收到客户端发来的连接请求报文后,紧接着向客户端发起连接建立请求并对客户端发来的连接请求进行响应,此时服务器向客户端发送的报文当中的SYN位和ACK位均被设置为1

  • 第三次握手:客户端收到服务器发来的报文后,得知服务器收到了自己发送的连接建立请求,最后客户端再向服务器发来的报文进行响应

3.2 为什么TCP需要三次握手? 

  • 三次握手是验证双方通信信道的最小次数让能建立的连接尽快建立起来
  • 三次握手能够保证连接建立时的异常连接挂在客户端(风险转移)。

3.3 TPC四次挥手流程

  •  第一次挥手:客户端向服务器发送的报文当中的FIN位被设置为1表示请求与服务器断开连接。
  • 第二次挥手:服务器收到客户端发来的断开连接请求后对其进行响应
  • 第三次挥手:服务器收到客户端断开连接的请求,且已经没有数据需要发送给客户端的时候,服务器就会向客户端发起断开连接请求
  • 第四次挥手:客户端收到服务器发来的断开连接请求后对其进行响应

3.4 TCP为什么需要四次挥手?

  •  由于TCP是全双工的,建立连接的时候需要建立双方的连接断开连接时也同样如此 

3.5 Epoll的LT和ET的区别 

  • ET叫做边缘触发当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完; 
  • LT叫做水平触发 当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取; 

3.6 Linux上命名管道的创建接口

  • mkfifo

3.7 Linux上socket套接字编程,写一个TCP服务器需要那些接口

  • socket,bind,listen,accept,read,write

3.8 进程间通信方式

  • 管道,共享内存,消息队列,套接字,信号,信号量

3.9 说一下分页和分段

  •  分页和分段都是为了管理文本或数据的方式,但它们的目的和主要区别不同
  • 分页是将文本分成一页一页的,便于阅读和翻页
  • 分段是将文本或数据分成逻辑上相关的部分,以便组织和管理

3.10 信号了解吗

  • 本质是一种通知机制,由用户or操作系统通过发送一定的信号,通知进程,但不是需要立即处理,你可以在后续进行处理
  • 而在linux中查看信号的命令是:kill -l

3.11 信号处理的过程 

3.12 说一下 CPU 缓存 

  • CPU缓存即高速缓冲存储器,是位于CPU与主内存间的一种容量较小但速度很快的存储器。
  • 由于CPU的速度远高于主内存,CPU直接从内存中存取数据要等待一定时间周期
  • 而Cache正好保存着CPU刚用过或循环使用的一部分数据 ,则CPU在Cache中拿数据时,就可以减少CPU的等待时间,提高了系统的效率

3.13 介绍一下线程安全,如何保证?

  • 线程安全:a.不可变 b.绝对线程安全 c.相对线程安全 d.线程兼容 e.线程独立 

  • 互斥同步 保证同一时刻,只有一个线程在操作共享数据

  •  线程池

3.14 ARP 欺骗了解吗

  • ARP 欺骗(ARP spoofing)是一种网络攻击技术,通过发送伪造的 ARP
    数据包,让目标设备误以为攻击者是其网关或其它设备,从而达到欺骗目标设备的目的

3.15 动态链接和静态链接的区别?使用场景?

  • 动态链接将库中我要的方法的地址,填入我的可执行程序中,建立关联,节省资源
  •  静态链接将库中方法的实现,真的拷贝到我们的可执行程序中!但占用资源
  • 小型应用的开发用静态链接,而大型应用的开发用动态连接 

3.16 静态链接比动态链接快吗

  • 静态链接库的代码装载速度快,则执行的速度会比动态链接库更快一点

3.17  如何解决网络编程中大小端字节序不一致问题

  • 链接双方统一规定使用大端字节序
  • 调用htons()、ntohs()、htonl()、ntohl()这些函数对数据进行处理

3.18 less,more,cat区别

  • cat是一次性显示整个文件的内容,还适用于文件内容少的情况;

  • more提供分页功能,当内容较多,超过一页时,

  • less比more更强大,提供翻页,跳转,查找等命令。

3.19 查找文件的命令

  • which 查看可执行文件的位置

  • whereis 查看文件的位置

  • locate 配合数据库查看文件位置。

  • find 实际搜寻硬盘查询文件名称。

3.20 压缩/解压命令

  •  zip -r 名字.zip 要压缩的目录-r 一般都是递归的意思
    unzip 要解压的压缩包 -d 解压到的路径(如果目录不存在,就会创建)
  • tar -czvf 名字.tgz 要压缩的目录-v 这个选项是否显示压缩的过程
    tar -xzf 要解压的压缩包 -C 解压到的路径, (这里的路径必须要存在)

3.21 如何改变文件权限

  • chmod 644 code.c

3.22 如何改名文件所属者权限 

  • chown root code.c
  • chgrp root code.c

3.23 文件三种权限的作用

  •  想要进入目录的话必须要有x权限
  • 查看目录下面的文件列表必须要有r权限
  • 要在目录下创建文件或者目录必须要有w权限

3.24 边缘触发和水平触发

边缘触发

  • 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;

水平触发

  • 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;

3.25 线程间同步的方式 

  • 互斥锁 读写锁 信号量 条件变量 

3.26 进程间通信的方式

  • 管道,共享内存,消息队列,套接字,信号,信号量

3.27 谈谈线程与进程

  • 进程是资源调度的基本单位,运行一个可执行程序会创建一个或多个进程,进程就是运行起来的可执行程序

  • 线程是程序执行的基本单位,是轻量级的进程。每个进程中都有唯一的主线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束

  • 线程启动速度快,轻量级,系统开销小,使用有一定难度

3.28 Linux下最快的进程间通信方式是,为什么?

  • 共享内存
  • 因为拷贝次数最少,只有2次拷贝,输入文件到共享内存,共享内存到输出文件

3.29 Linux下gdb调试命令

  • i: 显示行号

  • b+行号:打断点 d+断点编号:删除断点 info b:查看断点

  • r: 运行 s:逐语句 n:逐过程

3.30 linux 中 grep 和 find 的区别

  •  grep是根据文件的内容进行查找
  • find是根据文件的属性进行查找

3.31 TCP和UDP的区别

  • TCP面向连接字节流,可靠,且只能点对点通信
  • UDP面向报文,不可靠,可以1对多,多对多通信

4.与git相关 

4.1 说一下 Git 指令 

  • git clone 克隆远端仓库
  • git add 添加本地文件到远端仓库
  • git push 同步到远端仓库