OVERVIEW
- Q&AGroup1
- 一、语言基础
- 1.C语言
- (1)含参数的宏与函数的不同点
- (2)sizeof与strlen的区别
- (3)大/小端
- (4)strcpy与memcpy的区别
- (5)extern与static的区别
- (6)断言
- 2.C与C++
- (1)C++语言的特点?🚩
- (2)C语言和C++的区别?🚩
- (3)C++中 struct 和 class 的区别?🚩🚩🚩🚩
- (4)C++结构体和C结构体的区别?🚩🚩🚩🚩
- (5)static 在C和C++中的区别?
- (6)static关键字的作用?🚩🚩🚩🚩
- (7)静态变量什么时候初始化?🚩🚩🚩🚩
- (8)静态局部变量,全局变量,局部变量的特点,以及使用场景?
- (9)C++作用域与生命周期?🚩🚩🚩
- (10)string.h与cstring的区别?
- (11)动态数组与静态数组区别?
- (12)include头文件的顺序以及双引号""和尖括号<>的区别?🚩
- (13)导入C函数的关键字是什么,C++编译时和C有什么不同?🚩🚩🚩
- (14)代码到可执行二进制文件的过程?🚩🚩🚩🚩
- (15)静态链接和动态链接有什么区别? 🚩🚩🚩🚩
- 3.地址与指针
- (1)使用指针需要注意什么? 🚩🚩🚩🚩
- (2)指针和引用的区别?
- (3)指针和数组的区别?🚩🚩
- (4)C++有几种传值方式,之间的区别是什么?🚩🚩🚩🚩
- (5)指针传递与引用传递的区别?🚩🚩🚩🚩
- (6)什么是函数指针,如何定义函数指针,有什么使用场景?🚩
- (7)nullptr调用成员函数可以吗?为什么?🚩🚩🚩
- (8)什么是野指针,怎么产生的,如何避免?🚩🚩
- (9)空指针与野指针的区别?🚩🚩
- (10)野指针通常出现的情况?🚩🚩🚩🚩
- (11)如果规避野指针?🚩🚩🚩🚩
- (12)什么是函数地址?🚩🚩
- (13)函数指针和指针函数的区别?🚩🚩🚩
- (14)内联函数和函数的区别,内联函数的作用?🚩🚩🚩🚩
- (15)内联函数和宏函数的区别?🚩🚩🚩
- (16)const和define的区别?🚩🚩🚩
- (17)define与inline的区别?🚩🚩
- (18)什么是const常函数,有什么作用?🚩🚩🚩
- (19)const * 和 *const的区别?🚩🚩🚩🚩
- (20)const int *a, int const *a, const int a, int *const a, const int *const a什么特点?🚩🚩🚩🚩
- (21)运算符i++和++i的区别?🚩
- (22)i++ 是原子操作吗?🚩
- 4.内存分配
- (1)内存对齐的使用场景?🚩🚩🚩🚩🚩
- (2)内存分配方式?🚩🚩🚩
- (3)程序有哪些section,分别的作用?🚩🚩
- (4)初始化为0的全局变量在bss还是data?🚩🚩🚩
- (5)堆与自由存储的联系与区别?🚩🚩🚩
- (6)堆和栈的区别?🚩🚩🚩🚩
- (7)关键字、操作符与函数的区别?
- (8)new和malloc的区别,各自底层实现原理?🚩🚩🚩🚩
- (9)new与delete操作符分配/释放对象内存时会经历的步骤🚩🚩🚩🚩
- (10)malloc与new的底层实现?🚩
- (11)malloc和局部变量分配在堆还是栈?🚩🚩🚩
- (12)常见的内存错误及其对策?🚩🚩
- (13)什么是内存泄露,内存泄露怎么检测?🚩🚩
- (14)简述一下atomoic内存顺序?
- 5.面向对象
- (1)什么是面向对象?
- (2)面向对象的三大特征?
- (3)构造函数有几种,分别什么作用?🚩🚩🚩🚩
- (4)类对象的初始化顺序,有多重继承情况下的顺序?🚩🚩🚩🚩
- (5)向上转型和向下转型?🚩
- (6)拷贝构造函数的参数是什么传递方式,为什么?🚩🚩🚩🚩
- (7)简述拷贝赋值和移动赋值?🚩🚩🚩🚩
- (8)拷贝构造函数的调用时机?
- (9)简述深拷贝和浅拷贝,如何实现深拷贝?🚩🚩🚩🚩
- (10)类继承时,派生类对不同关键字修饰的基类方法的访问权限?🚩🚩🚩🚩
- (11)类内可以定义引用数据成员吗?🚩🚩🚩
- (12)简述移动构造函数,什么库用到了这个函数?🚩
- 6.多态性与虚函数
- (1)多态的实现原理
- (2)虚函数的实现原理
- (3)静态多态与动态多态?🚩🚩🚩
- (4)重载与重写的区别?🚩🚩🚩🚩
- (5)重载和重写是如何实现的?🚩🚩🚩🚩
- (6)重载/重写/隐藏?
- (7)C 语言如何实现 C++ 语言中的重载?🚩
- (8)菱形继承问题如何解决?🚩🚩🚩
- (9)什么是虚继承解决什么问题?如何实现?🚩🚩🚩🚩
- (10)构造函数中的能不能调用虚方法?
- (11)纯虚函数可以被实例化吗?🚩
- (12)虚基类可以被实例化吗?🚩🚩🚩🚩
- (13)虚函数和纯虚函数如何实现?🚩🚩🚩🚩
- (14)虚函数与纯虚函数的区别?🚩🚩🚩🚩
- (15)虚析构函数的作用?🚩🚩🚩🚩
- (16)虚析构函数的作用?🚩🚩🚩🚩
- (17)哪些函数不能被声明为虚函数??🚩🚩🚩🚩
- (18)虚函数表里存放的内容是什么时候写进去的?
- (19)如何理解抽象类?🚩🚩
- 7.函数模板与类模板
- (1)模板类是在什么时候实现的?
- (2)仿函数有什么作用?🚩🚩🚩
- (3)解释下C++ 中类模板和模板类的区别?🚩🚩
- 8.STL
- (1)STL 的基本组成部分?
- (2)STL 中常见的容器,并介绍一下实现原理?
- (3)介绍一下 STL 的空间配置器(allocator)?
- (4)STL 容器用过哪些,查找的时间复杂度是多少,为什么?
- (5)迭代器什么时候会失效?
- (6)迭代器的作用,有指针为何还要迭代器?
- (7) 迭代器是怎么删除元素的?
- (8)resize 和 reserve 的区别?
- (9)容器动态链接可能产生的问题?
- (10)vector 和 list 的区别,分别适用于什么场景?🚩🚩
- (11)简述 vector 的实现原理?🚩🚩
- (12)map hashtable deque list 的实现原理?
- (13)vector和list如果删除末尾的元素,其指针和迭代器如何变化?若删除的是中间的元素呢?🚩🚩
- (14)vector 与 list 具体是怎么实现的?常见操作的时间复杂度是多少?🚩
- (15)vector的insert与emplace
- (16)vector的resize
- (17)vector的截取操作
- (18)map 和 unordered_map 的区别?底层实现?🚩🚩
- (19)map 的实现原理?
- (20)map 和 set 有什么区别,分别又是怎么实现的?🚩🚩
- (21)push_back 和 emplace_back 的区别?🚩🚩
- 8.智能指针⭐
- (1)C++ 中智能指针和指针的区别是什么?
- (2)C++中的智能指针有哪些?分别解决的问题以及区别?
- (3)C++ 中智能指针的特点?
- (4)weak_ptr 能不能知道对象计数为 0,为什么?
- (5)weak_ptr 如何解决 shared_ptr 的循环引用问题?
- (6)share_ptr 怎么知道跟它共享对象的指针释放了?
- (7)智能指针及其实现,shared_ptr 线程安全性,原理?
- (8)智能指针有没有内存泄露的情况?
- 9.C++11多线程
- (1)C++11多线程中的几种锁?🚩🚩🚩🚩
- 10.新特性
- (1)C++11 的新特性有哪些?
- (2)C++11 中 Lambda 新特性?
- (3)NULL与nullptr的区别?
- (4)C++ 11 中 auto 的具体用法?
- (5)C++11 中四种类型转换?
- (6)C++ 右值引用与转移语义?
- (7)C++11 中的可变参数模板新特性?
- (8)左值与右值?🚩🚩🚩🚩
- (9)左值引用和右值引用?🚩🚩🚩🚩
- 11.Qt相关
- (1)Qt的核心机制
- (2)关于回调机制
- (3)信号与槽机制原理
- (4)信号与槽 与 回调函数的区别?
- (5)信号槽机制的优势与不足?
- 二、算法与数据结构
- 1.链表⭐
- (1)数组和链表的区别?
- (2)手写代码:反转链表?⭐⭐⭐
- (3)手写代码:合并两个有序链表?⭐⭐⭐
- (4)手写代码:判断一个链表是否为回文链表,给出思路?⭐⭐⭐
- (5)手写代码:如何判断两个单向链表是否相交?⭐⭐⭐
- (6)手写代码:数组中是否有重复数字?⭐⭐⭐
- 2.堆/栈/队列⭐
- (1)栈和堆的区别,以及为什么栈要快?
- (2)什么是栈溢出,并举个简单例子导致栈溢出?
- (3)手写代码:两个栈实现一个队列?
- (4)请你说一说小根堆特点?
- 3.红黑树⭐
- (1)红黑树介绍一下?
- (2)红黑树和AVL树的定义与特点,以及二者区别?
- (3)map底层为什么用红黑树实现?
- (4)map和unordered_map的底层实现?
- (5)map和unordered_map优点和缺点?
- (6)epoll怎么实现的?
- (7)介绍一下B+树?
- (8)Top(K)问题?
- (9)介绍C++两种map?
- (10)
- (11)手写代码:二叉树的层序遍历并输出?
- (12)手写代码:二叉树序列化反序列化
- (13)哈夫曼编码?
- 4.十大排序⭐
- (1)快排的优化?
- (2)请你来手写一下快排的代码?
- 5.哈希
- 6.动态规划⭐
- (1)请你手写代码:最长公共连续子序列?
- (2)手写代码:求一个字符串最长回文子串?
- (3) 手写代码:查找最长回文子串?
- 7.字符串⭐
- 8.分治/贪心
- 9.高级算法
- 三、操作系统
- 1.其他
- (1)程序启动的过程?🚩
- 1.Linux相关
- (1)Linux中查看进程运行状态的指令、查看内存使用情况的指令、tar解压文件的参数?
- (2)文件权限怎么修改?
- (3)说说常用的Linux命令?
- (4)软链接和硬链接的区别?
- (5)静态库和动态库怎么制作及如何使用,区别是什么?
- (6)GDB常见的调试命令,什么是条件断点,多进程下如何调试?
- (7)什么是大端小端,如何判断大端小端?
- (8)Linux零拷贝的原理?
- 2.内存管理
- (1)操作系统如何申请以及管理内存的?
- (2)什么是页表,为什么要有?
- (3)操作系统中的缺页中断
- (4)虚拟内存分布,什么时候会由用户态陷入内核态?
- (5)简述虚拟内存和物理内存,为什么要用虚拟内存,好处是什么?
- (6)虚拟地址到物理地址怎么映射的?
- 3.进程管理
- (1)进程调度算法有哪些?
- (2)简述Linux系统态与用户态,什么时候会进入系统态?
- (3)简述LRU算法及其实现方式?
- (4)一个线程占多大内存?
- (2)Linux进程调度算法及策略有哪些?
- (3)请你说说并发和并行?
- 4.进程和线程的概念和区别⭐
- 5.线程同步⭐
- 6.进程的通信方式⭐
- 7.死锁
- 8.网络与系统编程
- (1)epoll和select的区别,epoll为什么高效?
- (2)epoll为什么高效
- (2)多路IO复用技术有哪些,区别是什么?
- (3)简述socket中select,epoll的使用场景和区别,epoll水平触发与边缘触发的区别?
- (4)说说Reactor、Proactor模式?
- (5)简述同步与异步的区别,阻塞与非阻塞的区别?
- (6)BIO、NIO有什么区别?
- (7)介绍一下5种IO模型?
- (8)socket网络编程中客户端和服务端用到哪些函数?
- 四、计算机网络
- 1.网络通信模型
- (1)简述静态路由和动态路由?
- (2)路由协议有哪些,都是如何更新的?
- (3)简述域名解析过程,本机如何干预域名解析?
- (4)简述 DNS 查询服务器的基本流程是什么?DNS 劫持是什么?
- (5)简述网关的作用是什么,同一网段的主机如何通信?
- (6) MAC地址和IP地址分别有什么作用?
- (7)简述网络七层参考模型,以及每一层的作用?
- 2.TCP⭐
- (1)TCP 三次握手和四次挥手的过程?
- (2)TCP 连接和关闭的具体步骤?
- (3)TCP 2次握手行不行?为什么要3次?
- (5)TCP 连接 和 关闭的状态转移?
- (6)TCP 慢启动?
- (7)TCP 如何保证有序?
- (8)TCP 常见的拥塞控制算法有哪些?
- (9)TCP 超时重传?
- (10)TCP 可靠性保证?
- **检验和**
- **序列号/确认应答**
- **超时重传**
- **最大消息长度**
- **滑动窗口控制**
- **拥塞控制**
- (11)TCP 滑动窗口以及重传机制?
- (12)滑动窗口过小怎么办?
- (13)如果三次握手时候每次握手信息对方没收到会怎么样,分情况介绍?
- (14)TCP 的 TIME_WAIT,为什么需要有这个状态?
- (15)什么是 MSL,为什么客户端连接要等待2MSL的时间才能完全关闭?
- (17)什么是 TCP 粘包和拆包?
- (19)TCP 的 keepalive,以及和 HTTP 的 keepalive 的区别?
- (20)TCP 协议的延迟 ACK 和累计应答?
- (21)TCP 如何加速一个大文件的传输?
- (22)服务器怎么判断客户端断开了连接?
- (23)端到端,点到点的区别?
- (24)浏览器从输入 URL 到展现页面的全过程?
- 3.UDP⭐
- (1)从系统层面上,UDP 如何保证尽量可靠?🚩🚩🚩
- (2) TCP 和 UDP 的区别,它们的头部结构是什么样的?🚩🚩🚩
- (3)TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?🚩🚩🚩
- 4.HTTP与HTTPS
- (1)HTTP 和 HTTPS 的区别?🚩
- (2)HTTP 中的 referer 头的作用?🚩🚩
- (3)HTTP 的请求方法有哪些?🚩🚩🚩
- (4)HTTP 1.0,1.1,2.0 的主要区别?
- (5)HTTP 常见的响应状态码及其含义?🚩🚩🚩🚩
- (6)说说 GET请求和 POST 请求的区别?🚩🚩🚩🚩
- (7)Cookie 和 Session 的关系和区别是什么?🚩🚩🚩
- (8)HTTPS 的加密与认证过程?🚩
- 五、数据库
- 1.SQL
- 2.索引
- 3.事务
- 4.锁
- 5.优化
- 6.其他
- 六、设计模式
- 1.单例模式
- (1)说说什么是单例设计模式,如何实现?
- (2)简述单例设计模式的懒汉式和饿汉式,如何保证线程安全?
- 2.工厂模式
- (1)请说说工厂设计模式,如何实现,以及它的优点?
- 3.装饰器模式
- (1)请说说装饰器计模式,以及它的优缺点?
- 4.观察者模式
- 七、面试者提问
- 1.职位的由来?
- 2.面临的挑战?
- 3.如何考评?
- 4.关于您?
- 5.未来的憧憬?
Q&AGroup1
服务器开发需要以下几个方面的知识:
- 操作系统和网络基础知识:了解操作系统的基本原理和网络协议的工作原理,对高并发服务器的开发有很大帮助。
- 多线程和锁机制:了解多线程编程和锁机制,能够编写高效的线程安全代码,同时防止多线程竞争问题的发生。
- 数据库和缓存:了解数据库和缓存的基本原理和应用场景,能够设计和优化高性能的数据库和缓存方案。
- 消息队列:了解消息队列的基本原理和应用场景,能够应对高并发场景下的流量峰值和突发请求。
- 高并发服务器框架:了解高并发服务器框架的设计和实现,例如Netty、Node.js等,能够使用这些框架开发高性能的服务器应用。
- 分布式系统和负载均衡:了解分布式系统和负载均衡的基本原理和实现方式,能够设计和实现高可用性的分布式系统。
学习高并发服务器开发,需要对以上的知识有深入的理解和掌握,并进行不断的实践和优化。可以通过阅读相关的书籍和论文、参加相关的课程和培训、参与开源项目等方式进行学习和实践。同时,可以积极参加相关技术社区和论坛,与行业内的专家和开发者进行交流和学习。
-
基础语法:包括变量、数据类型、运算符、控制流程语句等。
-
面向对象编程:包括类、继承、多态、虚函数等概念和应用。
-
模板编程:包括函数模板、类模板、元编程等概念和应用。
-
标准库:包括STL容器、算法、迭代器、流、文件等的使用。
-
异常处理:包括异常的概念、处理方式、异常安全性等。
-
并发编程:包括多线程、互斥量、条件变量、原子操作等。
-
内存管理:包括动态内存分配、智能指针、内存泄漏、内存安全等。
-
性能优化:包括算法优化、编译器优化、代码结构优化、内存优化等。
-
操作系统相关:包括进程、线程、同步机制、IO模型等。
-
高级应用:包括网络编程、图形界面、游戏开发、嵌入式开发、数据科学等
-
C++基础知识
- 变量和数据类型
- 运算符
- 控制语句(if-else、for、while、switch)
- 函数
- 数组
- 指针
- 内存管理(动态分配内存、指针运算、内存泄漏和悬空指针)
- 引用
- 类型转换
- 文件操作
-
C++面向对象编程
- 封装
- 继承
- 多态
- 抽象类和纯虚函数
- 虚函数和虚表
- 模板类和模板函数
- 智能指针
- STL(容器、迭代器、算法)
-
C++高级特性
- 异常处理
- RAII
- 普通函数和Lambda表达式
- 函数对象和函数指针
- 常量表达式和constexpr函数
- 右值引用和移动语义
- 模板元编程
-
C++标准库
- I/O流库
- 字符串处理库
- 数学库
- 随机数库
- 时间库
- 正则表达式库
- 文件系统库
-
并发编程
- 线程和线程池
- 互斥量和信号量
- 条件变量和读写锁
- 原子操作和无锁编程
- 并发编程模型(生产者-消费者、线程池、MapReduce等)
-
C++网络编程
- socket编程
- TCP/IP协议
- HTTP协议
- WebSocket协议
- libcurl库
- Boost.Asio库
一、语言基础
1.C语言
(1)含参数的宏与函数的不同点
区别 | 含参数的宏 | 函数 |
---|---|---|
运算方式 | 函数调用时,先求出实参表达式的值,然后带入形参。 | 使用带参的宏只是进行简单的字符替换。 |
作用时期不同 | 函数调用是在程序运行时进行的,分配临时的内存单元; | 而宏替换则是在编译时进行的,在展开时不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。 |
类型检查 | 对函数中的实参和形参都要定义类型,二者的类型数量要求一致; | 宏没有类型检查,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。 |
返回结果 | 调用函数只可得到一个返回值 | 而用宏可以得到几个结果。 |
程序增长 | 函数调用不使源程序变长。 | 使用宏时,宏替换后源程序增长 |
占用程序运行时间 | 函数调用占运行时间(分配单元、保留现场、值传递、返回)。 | 宏替换不占运行时间,只占编译时间; |
(2)sizeof与strlen的区别
区别 | sizeof | strlen |
---|---|---|
性质不同 | sizeof是关键字(保留字),就是已被C语言本身使用,不能作其它用途使用的字 | strlen是C语言标准库函数 |
计算时期不同 | 编译器在编译时就计算出了sizeof的结果 | 而strlen函数必须在运行时才能计算出来。 |
计算结果不同 | sizeof计算的是数据类型占内存的大小 | strlen计算的是字符串实际的长度,strlen只能测量字符串 计算字符串 str 的长度,直到空结束字符,但不包括空结束字符 |
(3)大/小端
- 大端模式:是指数据的高字节保存在内存的低地址中;
- 小端模式:是指数据的高字节保存在内存的高地址中;
(4)strcpy与memcpy的区别
区别 | strcpy | memcpy |
---|---|---|
复制内容不同 | strcpy只能复制字符串 | memcpy可以复制任意内容,例如字符数组、整型、结构体、类等 |
复制方法不同 | strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,如果空间不够,就会引起踩内存(访问不应该访问的内存地址)。 | memcpy则是根据其第3个参数决定复制的长度。 |
用途不同 | 通常在复制字符串时用strcpy | 需要复制其他类型数据时则一般用memcpy,由于字符串是以“\0”结尾的,所以对于在数据中包含“\0”的数据只能用memcpy |
(5)extern与static的区别
static:
- 作用于局部变量时:称为静态局部变量,只有在该函数第一次调用时才对其分配空间和初始化。在函数调用结束时,不对该变量的内存进行释放值仍然保留。这也是与自动变量的区别。
- 作用于全局变量时:称为静态全局变量,表示该变量是私有的,只能在该文件使用,不能通过extern关键字对其引用。
- 作用于函数变量时:称为静态函数,表示该函数是私有的,只能在本文件中使用,不能通过extern关键字对其引用。
extern:
- 本文件:定义在本文件下面的全局变量,想要在上面使用时需要使用extern关键字对其声明
- 其他文件:定义在其他文件的全局变量想要在本文件使用时,若该变量未被
static
修饰时,可通过extern
关键字在本文件对其声明,即可使用。
(6)断言
#include <assert.h>
assert(src != NULL);//断言 括号内部成立上面事情不发生,否则报错
作用:解决预防性编程的问题,
例如参数传入一个指针为NULL时,程序就会奔溃时,我们可以增加assert来防御这种问题。在联调中assert会显示崩溃的信息,加快联调速度,也能对参数问题进行判断。assert只能在debug版起作用,发布版不生效。综上所述assert就是预防性编程一个重要的宏,能加快联调速度。
2.C与C++
(1)C++语言的特点?🚩
- C++在C语言基础上引入了面对对象的机制,同时也兼容C语言
- C++有三大特性(1)封装。(2)继承。(3)多态
- C++语言编写出的程序结构清晰、易于扩充,程序可读性好
- C++生成的代码质量高,运行效率高,仅比汇编语言慢10%~20%
- C++更加安全,增加const常量、引用、四类cast转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)、智能指针、try—catch等
- C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL(Standard Template Library)
- 同时,C++是不断在发展的语言。C++后续版本更是发展了不少新特性,如C++11中引入了nullptr、auto变量、Lambda匿名函数、右值引用、智能指针
(2)C语言和C++的区别?🚩
- C语言是C++的子集,C++可以很好兼容C语言,但是C++又有很多新特性。
- C++是面对对象的编程语言;C语言是面对过程的编程语言。
- C语言有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄露等。而C++对此增加了不少新特性来改善安全性,如const常量、引用、cast转换、智能指针、try—catch等等;
- C++可复用性高、引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL。
- C++的STL库相对于C语言的函数库更灵活、更通用。
(3)C++中 struct 和 class 的区别?🚩🚩🚩🚩
-
struct 一般用于描述一个数据结构集合,而 class 是对一个对象数据的封装
-
struct 中默认的访问控制权限是 public 的,而 class 中默认的访问控制权限是 private 的
-
在继承关系中,struct 默认是公有继承,而 class 是私有继承
-
class 关键字可以用于定义模板参数,就像 typename,而 struct 不能用于定义模板参数
template<typename T, typename Y>//可以把typename换成class int Func(const T &t, const Y &y) { //TODO }
(4)C++结构体和C结构体的区别?🚩🚩🚩🚩
C++中的struct是对C中的struct的扩充,它们在声明时的区别如下:
- C的结构体内不允许有函数存在,C++允许有内部成员函数,且允许该函数是虚函数
- C的结构体对内部成员变量的访问权限只能是public,而C++允许public,protected,private三种
- C语言的结构体是不可以继承的,C++的结构体是可以从其他的结构体或者类继承过来的
- C中使用结构体需要加上struct关键字,或者对结构体使用typedef取别名,而 C++ 中可以省略 struct 关键字直接使用
struct | C | C++ |
---|---|---|
成员函数 | 不能有 | 可以 |
静态成员 | 不能有 | 可以 |
访问控制 | 默认public不可修改 | 默认public/private/protected |
继承关系 | 不可以继承 | 可从类或者其他结构体继承 |
初始化 | 不能直接初始化数据成员 | 可以 |
(5)static 在C和C++中的区别?
- 在C和C++都可以作用于局部变量叫静态局部变量,在函数调用时,只有在该函数第一次调用时才对其分配空间和初始化。在函数调用结束时,不对该变量的内存进行释放值仍然保留。
- 在C和C++中都可以作用于全局变量和全局函数,表示该变量或者函数是私有的,只能在该文件使用。不能通过extern关键字对其引用。
- 修饰成员变量时,所有的对象都只维持一份拷贝,可以实现不同对象间的数据共享;
- 不能在类内部初始化,一般在类外部初始化,并且初始化时不不加 static
- 修饰成员函数时,该函数不接受 this 指针,只能访问类的静态成员;
- 不需要实例化对象即可访问;
(6)static关键字的作用?🚩🚩🚩🚩
作用 | 具体说明 |
---|---|
1.定义全局静态变量和局部静态变量 | 在变量前面加上static关键字。初始化的静态变量会在数据段分配内存,未初始化的静态变量会在BSS段分配内存。直到程序结束,静态变量始终会维持前值。只不过全局静态变量和局部静态变量的作用域不一样 |
2.定义为静态变量 | 静态变量只能在本源文件中使用 |
3.定义静态函数 | 在函数返回类型前加上static关键字,函数即被定义为静态函数。静态函数只能在本源文件中使用 |
4.定义类中的静态成员变量 | 使用静态数据成员,它既可以被当成全局变量那样去存储,但又被隐藏在类的内部。类中的static静态数据成员拥有一块单独的存储区,而不管创建了多少个该类的对象。所有这些对象的静态数据成员都共享这一块静态存储空间 |
5.定义类中的静态成员函数 | 与静态成员变量类似,类里面同样可以定义静态成员函数。只需要在函数前加上关键字static即可。如静态成员函数也是类的一部分,而不是对象的一部分。所有这些对象的静态数据成员都共享这一块静态存储空间 |
- 当调用一个对象的非静态成员函数时,系统会把该对象的起始地址赋给成员函数的this指针。
- 而静态成员函数不属于任何一个对象,因此C++规定静态成员函数没有this指针(划重点,面试题常考)。既然它没有指向某一对象,也就无法对一个对象中的非静态成员进行访问
(7)静态变量什么时候初始化?🚩🚩🚩🚩
- 对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。
- 而C++标准规定:全局或静态对象当且仅当对象首次用到时才进行构造。
- 静态变量都在静态存储区,所以下次调用函数的时候还是能取到原来的值
- 静态全局变量、静态局部变量都在静态存储区,直到程序结束才会回收内存。类静态成员变量在静态存储区,当超出类作用域时回收内存。
(8)静态局部变量,全局变量,局部变量的特点,以及使用场景?
(9)C++作用域与生命周期?🚩🚩🚩
C++里作用域可分为6种:全局,局部,类,语句,命名空间和文件作用域
- 全局变量:全局作用域,可以通过extern作用于其他非定义的源文件
- 静态全局变量 :全局作用域+文件作用域,所以无法在其他文件中使用。
- 静态局部变量 :局部作用域,只被初始化一次,直到程序结束。
- 局部作用域,比如函数的参数,函数内的局部变量等等
- 类静态成员变量:类作用域
所在空间考虑:除了局部变量在栈上外,其他都在静态存储区。因为静态变量都在静态存储区,所以下次调用函数的时候还是能取到原来的值。
生命周期:局部变量在栈上,出了作用域就回收内存。而全局变量、静态全局变量、静态局部变量都在静态存储区,直到程序结束才会回收内存。
(10)string.h与cstring的区别?
(11)动态数组与静态数组区别?
(12)include头文件的顺序以及双引号""和尖括号<>的区别?🚩
- 尖括号<>的头文件是系统文件,双引号""的头文件是自定义文件
- 编译器预处理阶段查找头文件的路径不一样
- 使用尖括号<>的头文件的查找路径:编译器设置的头文件路径–>系统变量
- 使用双引号""的头文件的查找路径:当前头文件目录–>编译器设置的头文件路径–>系统变量
(13)导入C函数的关键字是什么,C++编译时和C有什么不同?🚩🚩🚩
-
在C++中,导入C函数的关键字是extern,表达形式为extern “C”,主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的
-
由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;
而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
(14)代码到可执行二进制文件的过程?🚩🚩🚩🚩
-
C++和C语言类似,一个C++程序从源码到执行文件,有四个过程,预编译、编译、汇编、链接。
-
预编译:
- 将所有的#define删除,并且展开所有的宏定义
- 处理所有的条件预编译指令,如#if、#ifdef
- 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置
- 过滤所有的注释
- 添加行号和文件名标识
-
编译:
- 词法分析:将源代码的字符序列分割成一系列的记号
- 语法分析:对记号进行语法分析,产生语法树
- 语义分析:判断表达式是否有意义
- 代码优化
- 目标代码生成:生成汇编代码
- 目标代码优化
-
汇编:这个过程主要是将汇编代码转变成机器可以执行的指令
-
链接:将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序
-
静态链接:是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀
-
动态链接:是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息。所以当你删除动态库时,可执行程序就不能运行。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀
-
(15)静态链接和动态链接有什么区别? 🚩🚩🚩🚩
- 静态链接:在编译链接时直接将需要的执行代码拷贝到调⽤用处;
- 优点:程序在发布时不需要依赖库,可以独立执行
- 缺点:程序的体积会相对较大,⽽而且如果静态库更更新之后,所有可执行文件需要重新链接;
- 动态链接:在编译时不直接拷贝执行代码,而是通过记录一系列符号和参数,在程序运⾏行行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行行到指定代码时,在共享执行内存中寻找已经加载的动态库可执⾏代码,实现运行时链接;
- 多个程序可以共享同一个动态库,节省资源
- 由于运行时加载,可能影响程序的前期执行性能
3.地址与指针
(1)使用指针需要注意什么? 🚩🚩🚩🚩
- 定义指针时,先初始化为NULL。
- 用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
- 不要忘记为数组和动态内存赋初值,防止将未被初始化的内存作为右值使用。
- 避免数字或指针的下标越界。
- 动态内存的申请与释放必须配对,防止内存泄漏。
- 用free或delete释放了内存之后,立即将指针设置为NULL,防止出现野指针
(2)指针和引用的区别?
- 指针:指针变量是用于记录地址编号,可以通过解引用的方式*来找到指针指向的内存,不论指向数据的数据类型,指针变量在占用的字节空间大小不变4/8字节
- 引用:实际上是给变量起别名,引用必须初始化且完成后不可修改(本质是一个常量指针)
- 由于其本质为常量指针,占用空间大小与指针相同
- 引用的出现是为了简化指针的使用
- 联系:引用本质上是一个常量指针
区别 | 指针 | 引用 |
---|---|---|
空间 | 指针是地址,有存储空间 | 就是别名,无存储空间 |
多级 | 指针可以有多级 | 引用只能是一级 |
是否为NULL | 指针可以指向NULL | 引用不可以为NULL |
初始化 | 指针可以在定义的时候不初始化 | 引用必须在定义的时候初始化 |
改变指向 | 指针初始化之后可以再改变指向 | 引用初始化后不可以再改变指向 |
空间大小 | sizeof 的运算结果不同,指针就是4/8字节 | 引用是被引⽤用对象的大⼩ |
自增运算 | 指针使用自增运算是指针指向向后偏移一个存储单元 | 引用自增是将对应的值+1 |
类型检查 | 无 | 引用比指针多了类型检查 |
判空操作 | 指针作为函数参数时,指针需要检查是否为空 | 引用作为函数参数时,引用不需要见检查是否为空 |
(3)指针和数组的区别?🚩🚩
- 数组是用于储存多个相同类型数据的集合。 数组名是首元素的地址。
- 指针相当于一个变量,它存放的是其它变量在内存中的地址。 指针指向了内存的首地址。
区别 | 数组 | 指针 |
---|---|---|
1.存储方式 | 数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的,数组的存储空间,不是在静态区就是在栈上 | 指针很灵活,它可以指向任意类型的数据。指针的存储空间不能确定。 |
2.所占存储空间的内存大小 | 数组所占存储空间的内存大小:sizeof(数组名)/sizeof(数据类型) | 在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8 |
(4)C++有几种传值方式,之间的区别是什么?🚩🚩🚩🚩
传参方式有这三种:值传递、引用传递、指针传递
- 值传递:形参即使在函数体内值发生变化,也不会影响实参的值;
- 引用传递:形参在函数体内值发生变化,会影响实参的值;
- 指针传递:在指针指向没有发生改变的前提下,形参在函数体内值发生变化,会影响实参的值;
(5)指针传递与引用传递的区别?🚩🚩🚩🚩
-
指针:变量,独立,可变,可空,替身,无类型检查
指针从本质上讲是一个变量,变量的值是另一个变量的地址,指针在逻辑上是独立的,它可以被改变的,包括指针变量的值(所指向的地址)和指针变量的值对应的内存中的数据(所指向地址中所存放的数据)
-
引用:别名,依赖,不变,非空,本体,有类型检查
引用从本质上讲是一个别名,是另一个变量的同义词,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化(先有这个变量,这个实物,这个实物才能有别名),而且其引用的对象在其整个生命周期中不能被改变,即自始至终只能依附于同一个变量(初始化的时候代表的是谁的别名,就一直是谁的别名,不能变)
-
区别:
指针参数传递本质上是值传递,它所传递的是一个地址值。
引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。
引用传递和指针传递是不同的,任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。
(6)什么是函数指针,如何定义函数指针,有什么使用场景?🚩
-
函数指针就是指向函数的指针变量。每个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。
int func(int a); int (*f)(int a); f = &func;
-
函数指针的应用场景:回调callback,
我们调用别人提供的 API函数应用程序编程接口称为Call;如果别人的库里面调用我们的函数,就叫Callback
(7)nullptr调用成员函数可以吗?为什么?🚩🚩🚩
可以,因为在编译时对象就绑定了函数地址,和指针空不空没关系。
//给出实例
class animal{
public:
void sleep(){ cout << "animal sleep" << endl; }
void breathe(){ cout << "animal breathe haha" << endl; }
};
class fish :public animal{
public:
void breathe(){ cout << "fish bubble" << endl; }
};
int main(){
animal *pAn=nullptr;
pAn->breathe(); // 输出:animal breathe haha
fish *pFish = nullptr;
pFish->breathe(); // 输出:fish bubble
return 0;
}
(8)什么是野指针,怎么产生的,如何避免?🚩🚩
野指针:就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
产生原因:释放内存后指针不及时置空,依然指向原来的内存,那么可能出现非法访问的错误。
避免方法:
- 初始化置NULL
- 申请内存后判空
- 指针释放后置NULL
- 使用智能指针
(9)空指针与野指针的区别?🚩🚩
-
空指针:空指针是一个指向空地址的指针,指针没有指向任何有效的内存地址,可以通过将指针初始化为null或0来创建空指针。空指针通常用于指示指针没有指向任何对象。
如若你尝试打印指向空的指针的地址值,这时你会发现输出的结果为0x00000000,表示不指向任何有效空间。
-
野指针:野指针是一个指向未知或无效地址的指针,指针指向一个没有分配的内存地址。或指向一个已经释放的内存地址。野指针通常是由于未初始化指针或者指针越界引起的,其使用是非法的。最为常见的产生野指针的情况有三种:
-
区别在于:
- 区别在于,空指针是一个有效的指针,只是它没有指向任何有效的内存地址,因此可以安全地使用。
- 而野指针是一个无效的指针,因为它指向未知或已释放的内存地址,使用野指针可能会导致不可预测的结果,包括程序崩溃、数据损坏等问题。
- 因此,在编程中应尽可能避免使用野指针,而应该始终确保指针指向有效的内存地址或者为空指针。
- 同时,在使用指针之前应该始终将其初始化为null或0,以避免指向无效地址。
(10)野指针通常出现的情况?🚩🚩🚩🚩
- 未初始化指针:如果一个指针没有被初始化或者赋值为null或0,那么它将成为一个野指针,指向一个未知的内存地址。
- 指针越界:如果一个指针指向了一个数组或其他数据结构的末尾或之外,那么它可能会指向一个未知的内存地址,成为野指针。
- 释放后未置空的指针:如果一个指针指向一个已经释放的内存块,但是未将该指针置为null或0,那么它仍然是一个野指针。
- 指针操作错误:如果对指针进行错误的操作,比如将指针加上或减去错误的值,或者将指针强制转换成错误的类型,那么也可能会导致它成为野指针。
总的来说,野指针是由于程序员疏忽或者代码逻辑错误引起的,因此在编程时应该避免这种错误,以确保程序的正确性和可靠性。可以通过正确地初始化和管理指针,以及使用安全的编程实践来避免野指针的出现。
(11)如果规避野指针?🚩🚩🚩🚩
- 初始化指针:在声明指针时,尽可能在声明的同时将指针初始化为null或0。这样可以确保指针指向一个已知的值,避免成为野指针。
- 指针赋值和释放:在对指针进行赋值和释放时,应该注意确保指针指向的内存块的有效性。如果指针指向的内存块已经释放,应该立即将指针置为null或0。
- 指针范围:在编写代码时,应该注意指针的范围,确保指针指向的内存块在指针范围内是有效的。避免使用指向超出范围的内存地址的指针。
- .检查指针:在使用指针时,应该对指针进行检查,确保它指向的内存地址是有效的。可以使用if语句或者断言来检查指针。
- 使用智能指针:智能指针是一种可以自动管理内存的指针,可以帮助避免野指针的出现。使用智能指针可以有效地避免指针释放后未置空的情况。
(12)什么是函数地址?🚩🚩
- 变量是存放在内存中的,所有变量是有地址的,只要是存放在内存中的二进制数就会有一个内存的地址,
- 那函数是什么呢?函数是由一些运行的语句组成的,程序运行的时候就会把函数中的语句调用到内存中去,那么函数代码在内存中开始的那个内存空间的地址就是函数的地址。
- 至于函数中用的变量的地址并不是放在代码区的,一般都放在另外的两个地方,其实地址上是有很大的差值的
(13)函数指针和指针函数的区别?🚩🚩🚩
不同点 | 指针函数 | 函数指针 |
---|---|---|
1.定义不同 | 指针函数本质是一个函数,其返回值为指针 | 函数指针本质是一个指针,其指向一个函数 |
2.写法不同 | int *fun(int x,int y); | int (*fun)(int x,int y); |
3.使用不同 |
//指针函数示例
typedef struct _Data{
int a;
int b;
}Data;
//指针函数
Data* f(int a,int b){
Data * data = new Data;
//...
return data;
}
int main(){
//调用指针函数
Data * myData = f(4,5);
//Data * myData = static_cast<Data*>(f(4,5));
//...
}
//函数指针示例
int add(int x,int y){
return x+y;
}
//函数指针
int (*fun)(int x,int y);
//赋值
fun = add;
//调用
cout << "(*fun)(1,2) = " << (*fun)(1,2);
//输出结果
//(*fun)(1,2) = 3
(14)内联函数和函数的区别,内联函数的作用?🚩🚩🚩🚩
内联函数的作用:是将内联函数的调用表达式用内联函数体来替换。避免函数调用的开销
- 内联函数比普通函数多了关键字inline
- 内联函数避免了函数调用的开销;普通函数有调用的开销
- 普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址
- 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句
(15)内联函数和宏函数的区别?🚩🚩🚩
区别 | 宏定义 | 内联函数 |
---|---|---|
1.是否为函数 | 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程提高了效率 | 而内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身 |
2.工作方式不同 | 宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 | 而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率 |
3.是否有类型检查 | 宏定义是没有类型检查的,无论对还是错都是直接替换; | 而内联函数在编译的时候,编译器会检查参数的类型和数量是否正确, |
4.编译方式不同 | 内联函数是在编译时被展开的 | 内联函数是在编译时被展开的 |
注:类型检查指,验证接收的是否为合适的数据类型以及赋值是否合乎类型要求。
总的来说,内联函数相对于宏函数更加安全、灵活、易于调试和维护。但是,内联函数在函数体过长时会增加代码的大小,从而降低程序的性能。因此,在实际编程中,需要根据具体情况选择合适的方式来提高程序的执行效率。
(16)const和define的区别?🚩🚩🚩
const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:
区别 | const | define |
---|---|---|
编译器处理方式 | const生效于编译的阶段确定其值的大小 | define生效于预处理阶段。 |
类型检查 | const定义的常量是带类型的, | define定义的常量不带类型,因此define定义的常量不利于类型检查。宏只作替换,不做计算,不做表达式求解。 |
内存空间 | const定义的常量,在C语言中是存储在内存中(静态存储区)、需要额外的内存空间的,在程序运行过程中内存中只有一个拷贝。 | define定义的常量,运行时是直接的操作数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大。 |
总结来说const比define更加安全,
(17)define与inline的区别?🚩🚩
相同点:拿空间换时间,提高程序的执行效率
区别 | define | inline |
---|---|---|
展开时机 | 内联函数在编译时展开 | 宏是由预处理器对宏进行展开 |
类型检查 | 内联函数会检查参数类型,所以内联函数更安全 | 宏定义不检查函数参数 |
是否为函数 | inline是函数 | 宏不是函数 |
设置的内联函数编译器不一定会满足要求,这取决于函数大小或者函数是否调用自身 | 宏在定义时要小心处理宏参数,一般情况是把参数用括弧括起来 |
(18)什么是const常函数,有什么作用?🚩🚩🚩
常函数:类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。
- 在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加const,而对于改变数据成员的成员函数不能加 const。
- 所以 const 关键字对成员函数的行为作了更明确的限定:有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。
- 除此之外,在类的成员函数后面加 const 还有什么好处呢?那就是常量对象可以调用 const 成员函数,而不能调用非const修饰的函数。正如非const类型的数据可以给const类型的变量赋值一样,反之则不成立。
(19)const * 和 *const的区别?🚩🚩🚩🚩
- int const* a;是常量指针(指向常量的指针):a指针所指向的内存里的值不变,即(*a)不变
- int* const a; 是指针常量(指针是常量):a指针所指向的内存地址不变,即a不变
(20)const int *a, int const *a, const int a, int *const a, const int *const a什么特点?🚩🚩🚩🚩
(21)运算符i++和++i的区别?🚩
不同点 | 前置(++i) | 后置(i++) |
---|---|---|
赋值顺序不同 | ++i 是先加后赋值 | i++ 是先赋值后加 |
效率不同 | 前置++不产生临时对象 | 后置++中tmp是一个临时对象,会造成一次构造函数和一次析构函数的额外开销 |
是否作为左值 | i++不能作为左值 | 而++i 可以作为左值 |
相同点:两者都不是原子操作(不会被线程调度机制打断的操作)
(22)i++ 是原子操作吗?🚩
- 原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束。中间不会有任何 context switch(切换到另一个线程)
- 对于单处理器单核系统来说,只要保证操作指令序列不被打断即可实现原子操作
- 对于简单的原子操作,cpu实现上会提供单条指令,比如INC和XCHG
- 对于复杂的原子操作,需要包含多条指令。执行过程中,出现上下文切换行为,比如任务切换,中断处理等。这里的行为会影响原子操作的原子性。因此需要自旋锁spinlock来保证操作指令序列不会在执行的中途受干扰
4.内存分配
知识易错点:
- 空类占用内存空间:1字节
- explicit作用:关闭函数的类型自动转换(防止隐式转换)
- 当初始化列表时,被初始化的顺序是声明是的顺序不是列表顺序。
- final关键字用处:当前我这个类就是最终类,我不想让别的类再继承我自己。
(1)内存对齐的使用场景?🚩🚩🚩🚩🚩
我的博客总结:https://blog.csdn.net/weixin_49167174/article/details/130046439
-
什么是内存对齐:计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。
-
为什么要进行内存对齐?
为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,
设置了内存对齐的,类型数据只能存放在按照对齐规则的内存中,处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作(比如剔除不要的数据),提高了效率。
-
内存对齐的规则?
- 结构体的对齐值是其成员的最大对齐值
- 编译器可以设置一个最大对齐值。#pragma pack(1)
- 成员方法对类占用内存没有任何影响
- 内存占用大小与类成员属性的先后顺序有关,与编译器内存对齐的设置有关
- 成员函数放在代码区,数据主要放在栈区和堆区,静态/全局区以及文字常量区也有
- 普通函数:不占用内存。
- 虚函数:要占用4个字节(32位系统)或8个字节(64位系统),用来指定虚函数的虚拟函数表的入口地址。
-
内存对齐的使用场景?
- 在客户端与服务端交互、进行网络通讯时,当一个类的结构被序列化出来之后,有内存对齐与无内存对齐所占用的字节数时不同的。
- 如果客户端做了内存对齐,而服务端没有做内存对齐,肯能会则将导致通信协议解析失败,
- 另外在客户端与服务端之间的通讯内容多了2byte,网络包大小增加并且资源浪费
-
内存对齐应用与3种数据类型:struct、class、union
- 数据成员对齐规则:结构(struct)或联合(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始
- 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储
- 尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的"最宽基本类型成员"的整数倍。不足的要补齐。
- sizeof(union),以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地址。
在C++中,内存分成5个区:堆、栈、全局存储区、常量存储区、代码区(自由存储区)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u5Kdq6ax-1693106684783)(https://s2.loli.net/2023/07/21/E4CsR9h6bxBPS5g.png)]
(2)内存分配方式?🚩🚩🚩
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5WfiPoFS-1693106684785)(https://s2.loli.net/2023/08/26/7xUIEAKLpYqDJMC.png)]
内存总共分为五大分区:栈区,堆区,全局静态区,常量文本区,程序代码区。
- 栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放
- 堆,就是那些由new分配的内存块,一般一个new就要对应一个delete
- 全局/静态存储区:全局变量和静态变量被分配到同一块内存中
- 常量存储区:这是一块比较特殊的存储区,里面存放的是常量,不允许修改
- 代码区(自由存储区),就是那些由malloc等分配的内存块,和堆是十分相似的,不过是用free来结束自己的生命
- 从生存周期来说:栈区是程序运行时分配结束就释放。堆区是程序员主动分配和释放。全局静态区是程序运行前分配,程序结束释放。常量文本区也是程序运行前分配,程序结束释放。
- 从管理角度来说:只有堆区是由程序员自己来管理,其他四个区都是由系统来管理的。
- 从可能产生的问题来说:栈区可能会造成栈溢出(1、深度递归,栈帧太多 2、只分配不释放,栈耗尽)。堆区可能造成内存泄漏(就是申请空间,然后忘记释放了),野指针(指向了一块已经被释放的空间),内存碎片。
- 从内存大小来说:栈区大小m,堆区大小和内存有关。最大可1G。其他三个区都很小。
- 从效率来说:栈区比堆区效率高。
- 代码区:存放程序体的二进制代码。比如我们写的函数,都是在代码区的。
(3)程序有哪些section,分别的作用?🚩🚩
从低地址到高地址,一个程序由代码段、数据段、BSS段、堆、共享区、栈等组成。
- 代码段:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
- 数据段:存放程序中已初始化的全局变量和静态变量的一块内存区域。
- BSS段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
- 可执行程序在运行时又会多出两个区域:堆区和栈区
- 堆区:动态申请内存用,堆从低地址向高地址增长。由new分配的内存块,其释放由程序员控制。(一个new对应一个delete)
- 栈区:存储局部变量、函数参数值。栈从高地址向低地址增长,是一块连续的空间。是那些编译器在需要时分配,在不需要时自动清除的存储区。
- 位于堆和栈之间还有一块共享区:
(4)初始化为0的全局变量在bss还是data?🚩🚩🚩
- BSS段通常用来存放程序中未初始化的或者初始化为0的全局变量,和静态变量的一块内存区域。
- BSS段特点是可读写的,在程序执行之前BSS段会自动清0。
(5)堆与自由存储的联系与区别?🚩🚩🚩
(1)malloc申请的内存在堆上,使用free释放。new申请的内存在自由存储区,用delete释放
(2)堆(heap)是c语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当程序运行时调用malloc()时就会从中分配,调用free可把内存交换。而自由存储区是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认用堆来实现自由存储区,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来实现,这时由new运算符分配的对象,说它在堆上也对,说它在自由存储区也对。
总结:
- 堆是c语言和操作系统的术语,是操作系统维护的一块内存。自由存储是C++中通过new和delete动态分配和释放对象的抽象概念
- new所申请的内存区域在C++中称为自由存储区,编译器用malloc和free实现new和delete操作符时,new申请的内存可以说是在堆上
- 堆和自由内存区有相同之处,但并不等价
(6)堆和栈的区别?🚩🚩🚩🚩
- 堆栈空间分配不同:栈由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等;堆一般由程序员分配及释放
- 堆栈缓存方式不同:栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放;堆则是存放在二级缓存中,速度要慢些
- 堆栈数据结构不同:堆类似数组结构;栈类似栈结构,先进后出FILO
(7)关键字、操作符与函数的区别?
- 关键字:所谓关键字就是已被:语言本身使用, 不能作其它用途使用的字
- 操作符:操作符是在表达式中用于连接不同对象的运算符,不同的操作符指定了不同的运算方式
- 可以把操作符理解为语言内置的,最基础的函数,不可代替的函数!
- 操作符本质上也是函数。只是操作符是编译器需要进行进一步解释。
- 函数:函数是一组一起执行一个任务的语句
- 函数与操作符的区别:
- 运算符只能重载不能自定义,函数的名字随便起只要是个标识符就行;但运算符不行
- 函数本身有一段代码,程序执行时,遇到函数时,会先将函数的参数入栈,再跳到函数的代码来运行。而操作符则是在本地直接运算
(8)new和malloc的区别,各自底层实现原理?🚩🚩🚩🚩
区别 | new | malloc |
---|---|---|
性质不同 | new是操作符 | malloc是函数 |
是否调用析构与构造函数函数 | 在调用时先分配内存,再调用构造函数,释放时调用析构函数 | malloc没构造函数与析构函数 |
申请内存大小是否给定 | new会调用构造函数,不用指定申请内存的大小, | malloc需要给定申请内存的大小 |
返回类型 | new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。 | malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型 |
是否能被重载 | new可以被重载 | malloc不行 |
内存分配失败的返回值 | new发生错误抛出异常,更直接和安全 | malloc返回null |
申请的内存所在位置不同 | new操作符从自由存储区(free store)上为对象动态分配内存空间 | malloc函数从堆上动态分配内存 |
(9)new与delete操作符分配/释放对象内存时会经历的步骤🚩🚩🚩🚩
new操作符:
- 调用
operatornew
函数分配一块足够大的原始未命名的内存空间以便存储特定类型的对象。 - 编译器运行相应的构造函数以构造对象,并为其传入初值。
- 对象构造完成后,返回一个指向该对象的指针
delete操作符:
- 调用对象的析构函数
- 编译器调用operator delete函数释放内存空间
new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构,而malloc则不会。
(10)malloc与new的底层实现?🚩
malloc:
- 当开辟的空间小于 128K 时,调用 brk函数;当开辟的空间大于 128K 时,调用mmap
- malloc采用的是内存池的管理方式,以减少内存碎片:先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快
- 采用隐式链表将所有空闲块记录,每一个空闲块记录了一个未分配的、连续的内存地址。
new:
- 创建一个新的对象
- 将构造函数的作用域赋值给这个新的对象(因此this指向了这个新的对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
(11)malloc和局部变量分配在堆还是栈?🚩🚩🚩
- malloc是在堆上分配内存,需要程序员自己回收内存;
- 局部变量是在栈中分配内存,超过作用域就自动回收
(12)常见的内存错误及其对策?🚩🚩
常见的内存错误:
- 内存分配未成功,却使用了它
- 内存分配虽然成功,但是尚未初始化就引用它
- 内存分配成功并且已经初始化,但操作越过了内存的边界
- 忘记了释放内存,造成内存泄露
- 释放了内存却继续使用它
应对策略:
- 定义指针时,先初始化为NULL
- 用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存
- 不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用
- 避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作
- 动态内存的申请与释放必须配对,防止内存泄漏
- 用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”
- 使用智能指针
(13)什么是内存泄露,内存泄露怎么检测?🚩🚩
内存泄露是什么?:
就是申请了一块内存空间,使用完毕后没有释放掉。
- new和malloc申请资源使用后,没有用delete和free释放;
- 子类继承父类时,父类析构函数不是虚函数。
- Windows句柄资源使用后没有释放
如何检测?:
- 使用了内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉(好的编码习惯)
- 将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表
- 使用智能指针
- 一些常见的工具插件,如ccmalloc、Dmalloc、Leaky、Valgrind等等
(14)简述一下atomoic内存顺序?
有六个内存顺序选项可应用于对原子类型的操作:
- memory_order_relaxed:在原子类型上的操作以自由序列执行,没有任何同步关系,仅对此操作要求原子性。
- memory_order_consume:memory_order_consume只会对其标识的对象保证该对象存储先行于那些需要加载该对象的操作。
- memory_order_acquire:使用memory_order_acquire的原子操作,当前线程的读写操作都不能重排到此操作之前。
- memory_order_release:使用memory_order_release的原子操作,当前线程的读写操作都不能重排到此操作之后。
- memory_order_acq_rel:memory_order_acq_rel在此内存顺序的读-改-写操作既是获得加载又是释放操作。没有操作能够从此操作之后被重排到此操作之前,也没有操作能够从此操作之前被重排到此操作之后。
- memory_order_seq_cst:memory_order_seq_cst比std::memory_order_acq_rel更为严格。memory_order_seq_cst不仅是一个"获取释放"内存顺序,它还会对所有拥有此标签的内存操作建立一个单独全序。
5.面向对象
(1)什么是面向对象?
面向对象是一种编程思想,把一切东西看成是一个个对象,把这些对象拥有的属性变量和操作这些属性变量的函数打包成一个类来表示。
- 面向过程:根据业务逻辑从上到下写代码
- 面向对象:将数据与函数绑定到一起,进行封装,这样能够更快速的开发程序,减少了重复代码的重写过程。
(2)面向对象的三大特征?
面向对象四大特点:抽象、封装、继承、多态
- 抽象:对具体问题进行概括,抽出一类对象的公共性质(数据抽象、行为抽象)并加以描述。
- 封装:将抽象得到的数据与行为相结合,形成一个有机的整体类,其中数据和函数都是类的成员。
- 继承:允许在原有类特性的基础上,进行更具体、更详细的说明。
- private继承
- protected继承
- public继承
- 多态:广义上是指一段程序能够处理多种类型对象的能力,主要包括4种形式实现:
- 强制多态:将一种类型的数据转换成另一种类型的数据来实现(数据类型转换),
- 重载多态:指为同一个名字赋予不同含义(函数重载、运算符重载),
- 类型参数化多态:模板是C++实现参数化多态的工具(分为函数模板与类模板)
- 包含多态:虚函数实现包含多态
(3)构造函数有几种,分别什么作用?🚩🚩🚩🚩
构造函数可以分为4类:默认构造函数、初始化构造函数、拷贝构造函数、移动构造函数。
-
默认构造函数和初始化构造函数:在定义类的对象的时候,完成对象的初始化工作(有了有参的构造了,编译器就不提供默认的构造函数)。
-
拷贝构造函数:赋值构造函数默认实现的是值拷贝(浅拷贝)
-
移动构造函数:用于将其他类型的变量,隐式转换为本类对象
//将int类型的r转换为Student类型的对象,对象的age为r,num为1004. Student(int r) { int num=1004; int age= r; }
(4)类对象的初始化顺序,有多重继承情况下的顺序?🚩🚩🚩🚩
- 创建派生类的对象,基类的构造函数优先被调用(也优先于派生类里的成员类)
- 如果类里面有成员类,成员类的构造函数优先被调用;(也优先于该类本身的构造函数)
- 基类构造函数如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序而不是它们在成员初始化表中的顺序;
- 成员类对象构造函数如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序而不是它们出现在成员初始化表中的顺序;
- 综上可以得出,初始化顺序:
- 父类构造函数–>成员类对象构造函数–>自身构造函数
- 其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序。
- 析构顺序和构造顺序相反。
(5)向上转型和向下转型?🚩
- 子类转换为父类:向上转型,使用dynamic_cast<type_id>(expression),这种转换相对来说比较安全不会有数据的丢失;
- 父类转换为子类:向下转型,可以使用强制转换,这种转换时不安全的,会导致数据的丢失,原因是父类的指针或者引用的内存中可能不包含子类的成员的内存。
(6)拷贝构造函数的参数是什么传递方式,为什么?🚩🚩🚩🚩
- 拷贝构造函数的参数必须使用引用传递
- 如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式,而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。
- 需要澄清的是,传指针其实也是传值,如果上面的拷贝构造函数写成CClass(const CClass* c_class),也是不行的。
- 事实上,只有传引用不是传值外,其他所有的传递方式都是传值。
(7)简述拷贝赋值和移动赋值?🚩🚩🚩🚩
- 拷贝赋值是通过拷贝构造函数来赋值,在创建对象时,使用同一类中之前创建的对象来初始化新创建的对象。
- 移动赋值是通过移动构造函数来赋值,二者的主要区别在于:
- 拷贝构造函数的形参是一个左值引用,而移动构造函数的形参是一个右值引用;
- 拷贝构造函数完成的是整个对象或变量的拷贝,
- 而移动构造函数是生成一个指针指向源对象或变量的地址,接管源对象的内存,相对于大量数据的拷贝节省时间和内存空间。
(8)拷贝构造函数的调用时机?
调用时机:拷贝构造函数在对象需要进行拷贝时会被调用,对象拷贝包含以下几种情况:
- 使用已创建的对象来初始化一个新对象
- 使用值传递的方式来给函数参数进行传值
- 使用返回值的方式来返回局部变量
浅拷贝:只做简单的值拷贝,如果内存开辟在堆区,析构时会发生重复释放内存的情况。
深拷贝:为了解决浅拷贝的问题,在堆区另外申请内存空间,进行拷贝操作,需要自定义拷贝构造函数。
(9)简述深拷贝和浅拷贝,如何实现深拷贝?🚩🚩🚩🚩
-
浅拷贝:将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的。
-
深拷贝:拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的。这样不会出现指针悬挂问题。
两个指针先后去调用析构函数,分别释放自己所指向的位置。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。
-
深拷贝的实现:赋值运算符的重载传统实现:
这种方法解决了我们的指针悬挂问题,通过不断的开空间让不同的指针指向不同的内存,以防止同一块内存被释放两次的问题。
string( const string& s ) {
//_str = s._str;
_str = new char[strlen(s._str) + 1];
strcpy_s( _str, strlen(s._str) + 1, s._str );
}
string &operator=(const string& s) {
if (this != &s) {
//this->_str = s._str;
delete[] _str;
this->_str = new char[strlen(s._str) + 1];
strcpy_s(this->_str, strlen(s._str) + 1, s._str);
}
return *this;
}
(10)类继承时,派生类对不同关键字修饰的基类方法的访问权限?🚩🚩🚩🚩
类中的成员可以分为三种类型,分别为public成员、protected成员、public成员。类中可以直接访问自己类的public、protected、private成员,但类对象只能访问自己类的public成员。
-
public继承:
- 父类中public权限的成员,到子类中依然是public权限
- 都可访问
- 父类中protected权限的成员,到子类中依然是protected权限
- protected权限在子类继承中可以访问,在类外无法访问
- 父类中private权限的成员,子类中访问不到unreachable报错
- private权限在子类继承中无法访问,在类外无法访问
- 父类中public权限的成员,到子类中依然是public权限
-
protected继承:
- 父类中public权限的成员,到子类中变为protected权限
- public权限在子类继承中可以访问,在类外无法访问
- 父类中protected权限的成员,到子类中依然是protected权限
- protected权限在子类继承中可以访问,在类外无法访问
- 父类中private权限的成员,子类中访问不到unreachable报错
- private权限在子类继承中可以访问,在类外无法访问
- 父类中public权限的成员,到子类中变为protected权限
-
private继承:
- 父类中public权限的成员,到子类中变为private权限
- private权限在子类继承中可以访问,在类外无法访问
- 父类中protected权限的成员,到子类中变为private权限
- private权限在子类继承中可以访问,在类外无法访问
- 父类中private权限的成员,子类中访问不到unreachable报错
- private权限在子类继承中可以访问,在类外无法访问
- 父类中public权限的成员,到子类中变为private权限
-
public继承:
- 派生类可以访问基类的public、protected成员,不可以访问基类的private成员;
- 派生类对象可以访问基类的public成员,不可以访问基类的protected、private成员。
-
protected继承:
- 派生类可以访问基类的public、protected成员,不可以访问基类的private成员;
- 派生类对象不可以访问基类的public、protected、private成员。
-
private继承:
- 派生类可以访问基类的public、protected成员,不可以访问基类的private成员;
- 派生类对象不可以访问基类的public、protected、private成员。
(11)类内可以定义引用数据成员吗?🚩🚩🚩
c++类内可以定义引用成员变量,但要遵循以下三个规则:
- 不能用默认构造函数初始化,必须提供构造函数来初始化引用成员变量。否则会造成引用未初始化的错误。
- 不能在构造函数里初始化,必须在初始化列表中进行初始化。
- 构造函数的形参也必须是引用类型。
(12)简述移动构造函数,什么库用到了这个函数?🚩
C++11中新增了移动构造函数。
- 与拷贝类似,移动也使用一个对象的值设置另一个对象的值。
- 但是又与拷贝不同的是,移动实现的是对象值真实的转移(源对象到目的对象):源对象将丢失其内容,其内容将被目的对象占有。
- 移动操作的发生的时候,是当移动值的对象是未命名的对象的时候。这里未命名的对象就是那些临时变量,甚至都不会有名称。典型的未命名对象就是函数的返回值或者类型转换的对象。使用临时对象的值初始化另一个对象值,不会要求对对象的复制:因为临时对象不会有其它使用,因而,它的值可以被移动到目的对象。
- 做到这些,就要使用移动构造函数和移动赋值:当使用一个临时变量对对象进行构造初始化的时候,调用移动构造函数。类似的,使用未命名的变量的值赋给一个对象时,调用移动赋值操作。
移动操作的概念对对象管理它们使用的存储空间很有用的,
- 诸如对象使用new和delete分配内存的时候。在这类对象中,拷贝和移动是不同的操作:
- 从A拷贝到B意味着,B分配了新内存,A的整个内容被拷贝到为B分配的新内存上。
- 而从A移动到B意味着分配给A的内存转移给了B,没有分配新的内存,它仅仅包含简单地拷贝指针。
6.多态性与虚函数
(1)多态的实现原理
多态通过虚函数实现,虚函数通过虚函数表实现,
(2)虚函数的实现原理
(3)静态多态与动态多态?🚩🚩🚩
多态:多态是通过虚函数实现的,是一种通过动态绑定实现对不同的类调用不同的函数接口。由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定。
多态分为静态多态和动态多态:
-
静态多态(重载、模板):
- 编译器在编译期间完成的,
- 在编译时期就可以通过函数名和参数确定需要调用那个函数(函数重载),
- 如果有对应的函数就调用,没有则在编译时报错,
-
动态多态(虚函数、纯虚函数、虚析构函数、虚函数表):
- 要实现动态多态需要几个条件即动态绑定条件:
- 虚函数:基类中必须有虚函数,在派生类中必须重写虚函数。
- 通过基类类型的指针或引用来调用虚函数。
- 基类中返回值是基类类型的引用或指针,在派生类中返回值为派生类类型的引用或指针
-
关于动态绑定:
在使用基类的引用/指针调用虚函数时,就会发生动态绑定。所谓动态绑定,就是在运行时,虚函数会根据绑定对象的实际类型,选择调用函数的版本。
- 每一个类会为所有虚函数数维护一个虚函数表,并且有一个指针指向这个表的首地址,这个虚函数表记录着所有的虚函数的入口地址。在动态绑定时,编译器会给基类指针返回一个正确的函数入口地址
- 首先会找到函数的索引,里display索引是0
- 然后编译器会做一个替换(*(p->vptr)[0]),找到p指针的函数入口地址
- 程序运行后会执行语句,完成函数的调用
- 对于不同的虚函数,仅仅就是索引的不同
(4)重载与重写的区别?🚩🚩🚩🚩
- 重写:在派生类中存在重新定义基类的函数,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类对象调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类(虚基类)中被重写的函数必须有virtual修饰(与java中提供的接口类似)。
- 重载:具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
(5)重载和重写是如何实现的?🚩🚩🚩🚩
- 重载:C++利用命名倾轧(name mangling)技术,来改函数名,区分参数不同的同名函数。命名倾轧是在编译阶段完成的。
- 重写:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
- 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
- 存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。、
- 多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
- 重写用虚函数来实现,结合动态绑定。
- 纯虚函数是虚函数再加上 = 0;(
virtual 返回值类型 函数名(参数列表) = 0;
) - 抽象类是指包括至少一个纯虚函数的类(存续函数的buff叠加)。
(6)重载/重写/隐藏?
- 重载:作用域相同、函数名相同、不同的函数参数
- 函数名相同,通过不同参数类型or参数个数or参数顺序来重载函数,需要满足都在同一个作用域下。
- 重写:父类的方法,子类重写,要求父类的该方法必须是虚函数或者纯虚函数virtual
- 是指派生类重新实现基类的成员函数(虚函数)
- 函数名相同,参数列表相同
- 函数位于不同作用域下
- 基类函数是虚函数
- 隐藏:父类的方法,子类重写,要求父类的该方法不能被virtual修饰
- 是指派生类的成员函数隐藏了与其同名的基类成员函数
- case1:派生类的函数与基类函数同名、参数列表相同,如果基类函数没有virtual关键字,基类的函数在派生类中将被隐藏
- case2:派生类的函数与基类函数同名、参数列表不同,基类的函数在派生类中将被隐藏。
(7)C 语言如何实现 C++ 语言中的重载?🚩
c语言中不允许有同名函数,因为编译时函数命名是一样的,不像c++会添加参数类型和返回类型作为函数编译后的名称,进而实现重载。
如果要用c语言显现函数重载,可通过以下方式来实现:
- 使用函数指针来实现,重载的函数不能使用同名称,只是类似的实现了函数重载功能
- 函数使用可变参数,方式如打开文件open函数
- gcc有内置函数,程序使用编译函数可以实现函数重载
(8)菱形继承问题如何解决?🚩🚩🚩
假设类B、类C都继承了相同的类A,另外我们还有类D,类D通过多重继承机制继承了类B和类C。
继承关系的形状类似于菱形称为菱形继承,菱形继承存在的问题是多重继承导致数据重复冗余、浪费资源,可以继承基类Base时添加virtual
关键字利用虚继承可以解决:如果继承基类时用virtual来标注,C++会保证在子类对象中只有一个基类的子对象会被创建,解决重复冗余。
(9)什么是虚继承解决什么问题?如何实现?🚩🚩🚩🚩
虚继承是解决C++多重继承问题(菱形继承问题)的一种手段,从不同途径继承同一基类,会在子类中存在多份拷贝。
这将存在两个问题:
- 其一,浪费存储空间;第二,存在二义性问题,
- 通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是:将基类指针指向继承类中的基类对象的地址,
- 但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性。虚继承可以解决多种继承前面提到的两个问题
虚基表:存放相对偏移量,用来找虚基类
(10)构造函数中的能不能调用虚方法?
- 从语法上讲,调用完全没有问题,但是从效果上看,往往不能达到需要的目的,故不要在构造函数中调用虚方法,
- 派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型
- 同样,进入基类析构函数时,对象也是基类类型
- 所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。
(11)纯虚函数可以被实例化吗?🚩
- 纯虚函数不可以实例化,但是可以用其派生类实例化。
- 虚函数的原理采用 vtable。类中含有纯虚函数时,其vtable 不完全,有个空位。纯虚函数在类的vftable表中对应的表项被赋值为0。也就是指向一个不存在的函数。
- 由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象。
- 纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性。定义纯虚函数是为了实现一个接口,用来规范派生类的行为,也即规范继承这个类的程序员必须实现这个函数。
(12)虚基类可以被实例化吗?🚩🚩🚩🚩
-
在被继承的类前面加上virtual关键字,这时被继承的类称为虚基类
class A class B1:public virtual A; class B2:public virtual A;
-
虚继承的类可以被实例化
class Animal {/* ... */ }; class Tiger : virtual public Animal { /* ... */ }; class Lion : virtual public Animal { /* ... */ }; int main( ) { Liger lg; /*既然我们已经在Tiger和Lion类的定义中声明了"virtual"关键字,于是下面的代码编译OK */ int weight = lg.getWeight(); }
(13)虚函数和纯虚函数如何实现?🚩🚩🚩🚩
关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。
如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。非虚函数总是在编译时根据调用该函数的对象,引用或指针的类型而确定。
C++中的虚函数的作用主要是实现了多态的机制。
- 虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。
- 如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定或指针所指向的对象所属类型定义的版本。
- 虚函数必须是基类的非静态成员函数。
- 虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。
- 这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
纯虚函数是在基类中声明的虚函数,在基类中实现纯虚函数的方法是在函数原型后加“=0” virtualvoid GetName() =0。它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。
- 在很多情况下,基类本身生成对象是不合情理的,为了解决上述问题,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性,所以用户不能创建类的实例,只能创建它的派生类的实例。
- 将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的函数绝不会调用。
- 声明了纯虚函数的类是一个抽象类。
- 定义纯虚函数的目的在于,使派生类只是继承函数的接口,让所有的派生类对象都可以执行纯虚函数的动作。所以类纯虚函数的声明就是在告诉子类的设计者,必须提供一个纯虚函数的实现,但没有给出具体实现。
(14)虚函数与纯虚函数的区别?🚩🚩🚩🚩
区别 | 虚函数 | 纯虚函数 |
---|---|---|
定义位置 | 而只含有虚函数的类不能被称为抽象类。 | 含有纯虚函数的类被称为抽象类,虚函数和纯虚函数可以定义在同一个类中 |
是否可被是直接使用 or 必须重载再使用 | 虚函数可以被直接使用,也可以被子类重载以后, | 而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。 |
定义方式不同 | 虚函数的定义形式:virtual{}; | 纯虚函数的定义形式:virtual { } = 0; |
- 在虚函数和纯虚函数的定义中不能有static标识符,原因很简单被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。
(15)虚析构函数的作用?🚩🚩🚩🚩
- 虚析构函数,是将基类的析构函数声明前加上virtual关键字修饰
- 虚析构函数的主要作用是防止内存泄露:
- 定义一个基类的指针p,在delete p时,如果基类的析构函数是虚函数,这时只会看p所赋值的对象,
- 如果p赋值的对象是派生类的对象,就会调用派生类的析构函数(毫无疑问,在这之前也会先调用基类的构造函数,在调用派生类的构造函数,然后调用派生类的析构函数,基类的析构函数)
- 如果p赋值的对象是基类的对象,就会调用基类的析构函数,这样就不会造成内存泄露。
- 如果基类的析构函数不是虚函数,在delete p时,调用析构函数时,只会看指针的数据类型,而不会去看赋值的对象,这样子类的析构函数就不会被调用,进而造成内存泄露。
- 定义一个基类的指针p,在delete p时,如果基类的析构函数是虚函数,这时只会看p所赋值的对象,
(16)虚析构函数的作用?🚩🚩🚩🚩
虚析构作用:使用父类指针释放子类对象时可以让子类的析构函数和父类的析构函数同时被调用到。
虚析构:释放基类指针时可以释放掉子类的空间,防止内存泄漏。将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。如果基类的析构函数不是虚函数,在特定情况下会导致派生来无法被析构。
- 用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构
- 用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。
- C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。
(17)哪些函数不能被声明为虚函数??🚩🚩🚩🚩
常见的不不能声明为虚函数的有:
普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。
-
为什么C++不支持普通函数为虚函数?
普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时绑定函数。
-
为什么C++不支持构造函数为虚函数?
这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。
-
为什么C++不支持内联成员函数为虚函数?
内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。
-
为什么C++不支持静态成员函数为虚函数?
静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态绑定的必要性。
静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别
-
为什么C++不支持友元函数为虚函数?
因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
(18)虚函数表里存放的内容是什么时候写进去的?
-
虚函数表是一个存储虚函数地址的数组,以NULL结尾。
虚表(vftable)在编译阶段生成,对象内存空间开辟以后,写入对象中的 vfptr,然后调用构造函数。即:虚表在构造函数之前写入。
-
除了在构造函数之前写入之外,我们还需要考虑到虚表的二次写入机制,
通过此机制让每个对象的虚表指针都能准确的指向到自己类的虚表,为实现动多态提供支持。
(19)如何理解抽象类?🚩🚩
抽象类的定义如下:有纯虚函数的类就叫做抽象类。
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,
抽象类有如下几个特点:
- 抽象类只能用作其他类的基类,不能建立抽象类对象。
- 抽象类不能用作参数类型、函数返回类型或显式转换的类型。
- 可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。
7.函数模板与类模板
(1)模板类是在什么时候实现的?
-
模板实例化:模板的实例化分为显示实例化和隐式实例化,
前者是研发人员明确的告诉模板应该使用什么样的类型去生成具体的类或函数,
后者是在编译的过程中由编译器来决定使用什么类型来实例化一个模板不管是显示实例化或隐式实例化,最终生成的类或函数完全是按照模板的定义来实现的
-
模板具体化:
当模板使用某种类型类型实例化后生成的类或函数不能满足需要时,可以考虑对模板进行具体化。
具体化时可以修改原模板的定义,当使用该类型时,按照具体化后的定义实现,具体化相当于对某种类型进行特殊处理。
(2)仿函数有什么作用?🚩🚩🚩
仿函数(functor)又称为函数对象(function object)是一个能行使函数功能的类。
- 函数对象是指,需要重载函数调用操作符
()
的类。 - 函数对象(仿函数)的本质是一个类,而不是函数。
- 函数对象特点:
- 函数对象在使用时,可以像普通函数一样具有传递参数和返回值。
- 函数对象与普通函数不同,函数对象可以有自己的状态。
- 函数对象可以作为参数传递。
class Func{
public:
void operator() (const string& str) const {
cout << str << endl;
}
};
Func myFunc;
myFunc("helloworld!");
>>>helloworld!
STL中已经提供了的一些仿函数,这些仿函数所产生的对象、用法和一般函数完全相同,使用内建函数对象时需要提前引入头文件#include<functional>
主要包括:算术仿函数、关系仿函数、逻辑仿函数
plus、minus、multiplies、divides、modulus、negate
equal_to、not_equal_to、greater、greater_equal、less、less_equal
logical_and、logical_or、logical_not
class MyConpare{
public:
bool operator()(int num1 ,int num2){
return num1 > num2;
}
};
void main(){
//1.自主实现仿函数,实现sort降序排序
vector<int> v1;
v1.push_back(40);
v1.push_back(10);
v1.push_back(50);
v1.push_back(30);
v1.push_back(20);
sort(v1.begin(), v1.end(), MyConpare());
//2.使用内建函数对象(仿函数),实现sort降序排序
vector<int> v2;
v2.push_back(80);
v2.push_back(60);
v2.push_back(100);
v2.push_back(90);
v2.push_back(70);
sort(v2.begin(), v2.end(), greater<int>());
return 0;
}
(3)解释下C++ 中类模板和模板类的区别?🚩🚩
- 类模板是模板的定义,不是一个实实在在的类,定义中用到通用类型参数
- 模板类是实实在在的类定义,是类模板的实例化。类定义中参数被实际类型所代替。
- 类模板的类型参数可以有一个或多个,每个类型前面都必须加class,如template <class T1,class T2>class someclass{…};在定义对象时分别代入实际的类型名,如 someclass<int,double> obj;
- 和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。
- 模板可以有层次,一个类模板可以作为基类,派生出派生模板类。
8.STL
(1)STL 的基本组成部分?
STL由6部分组成:容器(Container)、算法(Algorithm)、 迭代器(Iterator)、仿函数(Function object)、适配器(Adaptor)、空间配制器(Allocator),容器和算法通过迭代器可以进行无缝地连接。
标准模板库STL主要由6大组成部分:
-
容器(Container)
是一种数据结构, 如list, vector, 和deques,以模板类的方法提供。为了访问容器中的数据,可以使用由容器类输出的迭代器。
-
算法(Algorithm)
是用来操作容器中的数据的模板函数。例如,STL用sort()来对一 个vector中的数据进行排序,用find()来搜索一个list中的对象, 函数本身与他们操作的数据的结构和类型无关,因此他们可以用于从简单数组到高度复杂容器的任何数据结构上。
-
迭代器(Iterator)
提供了访问容器中对象的方法。例如,可以使用一对迭代器指定list或vector中的一定范围的对象。 迭代器就如同一个指针。事实上,C++ 的指针也是一种迭代器。 但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符方法的类对象;
-
仿函数(Function object)
仿函数又称之为函数对象, 其实就是重载了操作符的struct,没有什么特别的地方。
-
适配器(Adaptor)
简单的说就是一种接口类,专门用来修改现有类的接口,提供一中新的接口;或调用现有的函数来实现所需要的功能。主要包括3中适配器Container Adaptor、Iterator Adaptor、Function Adaptor。
-
空间配制器(Allocator)
为STL提供空间配置的系统。其中主要工作包括两部分:
(1)对象的创建与销毁;
(2)内存的获取与释放。
(2)STL 中常见的容器,并介绍一下实现原理?
-
顺序容器
容器并非排序的,元素的插入位置同元素的值无关。包含vector、deque、list,具体实现原理如下:
(1)vector 头文件
<vector>
动态数组,元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
(2)deque 头文件
<deque>
双向队列,元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于vector)。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
(3)list 头文件
<list>
双向链表,元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。
-
关联式容器
元素是排序的;插入任何元素都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现。包含set、multiset、map、multimap,具体实现原理如下:
(1)set/multiset 头文件
<set>
set 即集合。set中不允许相同元素,multiset中允许存在相同元素。
(2)map/multimap 头文件
<map>
map与set的不同在于map中存放的元素有且仅有两个成员变量,一个名为first,另一个名为second, map根据first值对元素从小到大排序,并可快速地根据first来检索元素。
**注意:**map同multimap的不同在于是否允许相同first值的元素。
-
容器适配器
封装了一些基本的容器,使之具备了新的函数功能,比如把deque封装一下变为一个具有stack功能的数据结构。这新得到的数据结构就叫适配器。包含stack,queue,priority_queue,具体实现原理如下:
(1)stack 头文件
<stack>
栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项)。后进先出。
(2)queue 头文件
<queue>
队列。插入只可以在尾部进行,删除、检索和修改只允许从头部进行。先进先出。
(3)priority_queue 头文件
<queue>
优先级队列。内部维持某种有序,然后确保优先级最高的元素总是位于头部。最高优先级元素总是第一个出列。
(3)介绍一下 STL 的空间配置器(allocator)?
程序包括数据结构和相应的算法,而数据结构作为存储数据的组织形式,与内存空间有着密切的联系。
在C++ STL中,空间配置器便是用来实现内存空间分配的工具(一般是内存,也可以是硬盘等空间),他与容器联系紧密,每一种容器的空间分配都是通过空间分配器alloctor实现的。
(4)STL 容器用过哪些,查找的时间复杂度是多少,为什么?
STL中常用的容器有vector、deque、list、map、set、multimap、multiset、unordered_map、unordered_set等。容器底层实现方式及时间复杂度分别如下:
-
vector
采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为:
插入: O(N)
查看: O(1)
删除: O(N)
-
deque
采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为:
插入: O(N)
查看: O(1)
删除: O(N)
-
list
采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为:
插入: O(1)
查看: O(N)
删除: O(1)
-
map、set、multimap、multiset
上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为:
插入: O(logN)
查看: O(logN)
删除: O(logN)
-
unordered_map、unordered_set、unordered_multimap、 unordered_multiset
上述四种容器采用哈希表实现,不同操作的时间复杂度为: 插入: O(1),最坏情况O(N)
查看: O(1),最坏情况O(N)
删除: O(1),最坏情况O(N)
**注意:**容器的时间复杂度取决于其底层实现方式。
(5)迭代器什么时候会失效?
用过,常用容器迭代器失效情形如下。
- 对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。
- 对于关联容器map,set来说,使用了erase后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
- 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。
(6)迭代器的作用,有指针为何还要迭代器?
-
迭代器的作用
(1)用于指向顺序容器和关联容器中的元素
(2)通过迭代器可以读取它指向的元素
(3)通过非const迭代器还可以修改其指向的元素
-
迭代器和指针的区别
**迭代器不是指针,是类模板,表现的像指针。**他只是模拟了指针的一些功能,重载了指针的一些操作符,–>、++、–等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。
-
迭代器产生的原因
Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
(7) 迭代器是怎么删除元素的?
这是主要考察迭代器失效的问题。
- 对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器;
- 对于关联容器map,set来说,使用了erase后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可;
- 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。
(8)resize 和 reserve 的区别?
-
首先必须弄清楚两个概念:
(1)capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素的个数。还不能通过下标等访问,因为此时容器中还没有创建任何对象。
(2)size:指的是此时容器中实际的元素个数。可以通过下标访问0-(size-1)范围内的对象。
-
resize和reserve区别主要有以下几点:
(1)resize既分配了空间,也创建了对象;reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert()或push_back()等创建对象。
(2)resize既修改capacity大小,也修改size大小;reserve只修改capacity大小,不修改size大小。
(3)两者的形参个数不一样。 resize带两个参数,一个表示容器大小,一个表示初始值(默认为0);reserve只带一个参数,表示容器预留的大小。
(9)容器动态链接可能产生的问题?
-
可能产生 的问题
容器是一种动态分配内存空间的一个变量集合类型变量。在一般的程序函数里,局部容器,参数传递容器,参数传递容器的引用,参数传递容器指针都是可以正常运行的,而在动态链接库函数内部使用容器也是没有问题的,但是给动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题。
-
产生问题的原因 容器和动态链接库相互支持不够好,动态链接库函数中使用容器时,参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏问题。
(10)vector 和 list 的区别,分别适用于什么场景?🚩🚩
vector和list区别在于底层实现机理不同,因而特性和适用场景也有所不同。
vector:一维数组
特点:元素在内存连续存放,动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后内存也不会释放。
优点:和数组类似开辟一段连续的空间,并且支持随机访问,所以它的查找效率高其时间复杂度O(1)。
缺点:由于开辟一段连续的空间,所以插入删除会需要对数据进行移动比较麻烦,时间复杂度O(n),另外当空间不足时还需要进行扩容。
list:双向链表
特点:元素在堆中存放,每个元素都是存放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问。
优点:底层实现是循环双链表,当对大量数据进行插入删除时,其时间复杂度O(1)。
缺点:底层没有连续的空间,只能通过指针来访问,所以查找数据需要遍历其时间复杂度O(n),没有提供[]操作符的重载。
应用场景
vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。
(11)简述 vector 的实现原理?🚩🚩
新增元素
Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,再插入新增的元素。
插入新的数据分在最后插入push_back和通过迭代器在任何位置插入,
这里说一下通过迭代器插入,通过迭代器与第一个元素的距离知道要插入的位置,即int index=iter-begin()。这个元素后面的所有元素都向后移动一个位置,在空出来的位置上存入新增的元素。
删除元素
删除和新增差不多,也分两种,删除最后一个元素pop_back和通过迭代器删除任意一个元素erase(iter)。
通过迭代器删除还是先找到要删除元素的位置,即int index=iter-begin();这个位置后面的每个元素都想前移动一个元素的位置。同时我们知道erase不释放内存只初始化成默认值。
删除全部元素clear:只是循环调用了erase,所以删除全部元素的时候,不释放内存。内存是在析构函数中释放的。
迭代器iteraotr
迭代器iteraotr是STL的一个重要组成部分,通过iterator可以很方便的存储集合中的元素.STL为每个集合都写了一个迭代器, 迭代器其实是对一个指针的包装,实现一些常用的方法,如++,–,!=,==,*,->等, 通过这些方法可以找到当前元素或是别的元素. vector是STL集合中比较特殊的一个,因为vector中的每个元素都是连续的,所以在自己实现vector的时候可以用指针代替。
(12)map hashtable deque list 的实现原理?
map、hashtable、deque、list实现机理分别为红黑树、函数映射、双向队列、双向链表,他们的特性分别如下:
-
map实现原理
map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。
-
hashtable(也称散列表,直译作哈希表)实现原理
hashtable采用了函数映射的思想记录的存储位置与记录的关键字关联起来,从而能够很快速地进行查找。这决定了哈希表特殊的数据结构,它同数组、链表以及二叉排序树等相比较有很明显的区别,它能够快速定位到想要查找的记录,而不是与表中存在的记录的关键字进行比较来进行查找。
-
deque实现原理
deque内部实现的是一个双向队列。元素在内存连续存放。随机存取任何元素都在常数时间完成(仅次于vector)。所有适用于vector的操作都适用于deque。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
-
list实现原理
list内部实现的是一个双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。无成员函数,给定一个下标i,访问第i个元素的内容,只能从头部挨个遍历到第i个元素。
(13)vector和list如果删除末尾的元素,其指针和迭代器如何变化?若删除的是中间的元素呢?🚩🚩
-
迭代器和指针之间的区别
**迭代器不是指针,是类模板,表现的像指针。**他只是模拟了指针的一些功能,重载了指针的一些操作符,–>、++、–等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。
-
vector和list特性
vector特性 动态数组。元素在内存连续存放。随机存取任何元素都在常数时间完成。在尾端增删元素具有较大的性能(大部分情况下是常数时间)。
list特性 双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。
-
vector增删元素
对于vector而言,删除某个元素以后,该元素后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。
-
list增删元素
对于list而言,删除某个元素,只有“指向被删除元素”的那个迭代器失效,其它迭代器不受任何影响。
(14)vector 与 list 具体是怎么实现的?常见操作的时间复杂度是多少?🚩
-
vector 一维数组(元素在内存连续存放)
是动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后,内存也不会释放;如果新增大小当前大小时才会重新分配内存。
扩容方式:
a. 倍放开辟三倍的内存
b. 旧的数据拷贝到新的内存
c. 释放旧的内存
d. 指向新内存
-
list 双向链表(元素存放在堆中)
元素存放在堆中,每个元素都是放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点,使得它的随机存取变得非常没有效率,因此它没有提供[ ]操作符的重载。但是由于链表的特点,它可以很有效的支持任意地方的删除和插入操作。
特点:
a. 随机访问不方便
b. 删除插入操作方便
-
常见时间复杂度
(1)vector插入、查找、删除时间复杂度分别为:O(n)、O(1)、O(n);
(2)list插入、查找、删除时间复杂度分别为:O(1)、O(n)、O(1)。
(15)vector的insert与emplace
(16)vector的resize
(17)vector的截取操作
(18)map 和 unordered_map 的区别?底层实现?🚩🚩
map和unordered_map的区别在于他们的实现基理不同。
-
map实现机理
map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),
- 红黑树有自动排序的功能,因此map内部所有元素都是有序的,
- 红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。
- map中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。
-
unordered_map实现机理
unordered_map内部实现了一个哈希表(也叫散列表),通过把关键码值映射到Hash表中一个位置来访问记录,查找时间复杂度可达O(1),其中在海量数据处理中有着广泛应用。因此,元素的排列顺序是无序的。
(19)map 的实现原理?
map是关联式容器,它们的底层容器都是红黑树。map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
-
map的特性如下
(1)map以RBTree作为底层容器;
(2)所有元素都是键+值存在;
(3)不允许键重复;
(4)所有元素是通过键进行自动排序的;
(5)map的键是不能修改的,但是其键对应的值是可以修改的。
(20)map 和 set 有什么区别,分别又是怎么实现的?🚩🚩
-
set是一种关联式容器,其特性如下:
(1)set以RBTree作为底层容器
(2)所得元素的只有key没有value,value就是key
(3)不允许出现键值重复
(4)所有的元素都会被自动排序
(5)不能通过迭代器来改变set的值,因为set的值就是键,set的迭代器是const的
-
map和set一样是关联式容器,其特性如下:
(1)map以RBTree作为底层容器
(2)所有元素都是键+值存在
(3)不允许键重复
(4)所有元素是通过键进行自动排序的
(5)map的键是不能修改的,但是其键对应的值是可以修改的
综上所述,map和set底层实现都是红黑树;map和set的区别在于map的值不作为键,键和值是分开的。
(21)push_back 和 emplace_back 的区别?🚩🚩
- 如果要将一个临时变量push到容器的末尾,
- push_back()需要先构造临时对象,再将这个对象拷贝到容器的末尾,
- 而emplace_back()则直接在容器的末尾构造对象,这样就省去了拷贝的过程
8.智能指针⭐
(1)C++ 中智能指针和指针的区别是什么?
(2)C++中的智能指针有哪些?分别解决的问题以及区别?
(3)C++ 中智能指针的特点?
(4)weak_ptr 能不能知道对象计数为 0,为什么?
(5)weak_ptr 如何解决 shared_ptr 的循环引用问题?
(6)share_ptr 怎么知道跟它共享对象的指针释放了?
(7)智能指针及其实现,shared_ptr 线程安全性,原理?
(8)智能指针有没有内存泄露的情况?
9.C++11多线程
(1)C++11多线程中的几种锁?🚩🚩🚩🚩
- 互斥锁:互斥锁用于控制多个线程对他们之间共享资源互斥访问的一个信号量,也就是说是为了避免多个线程在某一时刻同时操作一个共享资源。任何是一个线程都要使用互斥锁互斥访问任务队列,以避免多个线程同时访问任务队列以发生错乱。std::mutex
- 一次只能一个线程拥有互斥锁,其他线程只有等待
- 互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒
- 为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换
- 实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线程投入睡眠中
- 条件锁:条件锁就是所谓的条件变量,某一个线程因为某个条件未满足时可以使用条件变量使改程序处于阻塞状态。一旦条件满足以信号量的方式唤醒一个因为该条件而被阻塞的线程。最为常见就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。std::condition_variable
- 条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件(互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定)
- 互斥锁是线程间互斥的机制,条件变量则是同步机制。
- 自旋锁:互斥锁是是一种sleep-waiting的锁,自旋锁是一种busy-waiting的锁。与互斥锁不同(处理器不会因为线程阻塞而空闲着),如果发现临界资源已经被占用,则其会一直不断地循环检查锁是否可用。直到获取到这个自旋锁为止。
- 如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。
- 如果别的线程长时期占有锁,那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高
- 读写锁:计算机中某些数据被多个进程共享,对数据库的操作有两种:一种是读操作,就是从数据库中读取数据不会修改数据库中内容;另一种就是写操作,写操作会修改数据库中存放的数据。因此可以得到我们允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。boost::shared_lock
- 多个读者可以同时进行读
- 写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
- 写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
- 递归锁:一般而言,锁的功能与性能成反比,不过我们一般不使用递归锁std::recursive_mutex
10.新特性
(1)C++11 的新特性有哪些?
(2)C++11 中 Lambda 新特性?
(3)NULL与nullptr的区别?
(4)C++ 11 中 auto 的具体用法?
(5)C++11 中四种类型转换?
(6)C++ 右值引用与转移语义?
(7)C++11 中的可变参数模板新特性?
(8)左值与右值?🚩🚩🚩🚩
-
左值指既能够出现在等号左边,也能出现在等号右边的变量。
左值是可寻址的变量,有持久性;
-
右值则是只能出现在等号右边的变量。
右值一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的。
-
左值和右值主要的区别之一是左值可以被修改,而右值不能。
int a; // a 为左值
a = 3; // 3 为右值
(9)左值引用和右值引用?🚩🚩🚩🚩
- 左值引用:引用一个对象
- 右值引用:就是必须绑定到右值的引用,C++11中右值引用可以实现移动语义,通过 && 获得右值引用
- 右值引用和相关的移动语义是C++11标准中引入的最强大的特性之一,通过std::move()可以避免无谓的复制,提高程序性能。
int x = 6; // x是左值,6是右值
int &y = x; // 左值引用,y引用x
int &z1 = x * 6; // 错误,x*6是一个右值
const int &z2 = x * 6; // 正确,可以将一个const引用绑定到一个右值
int &&z3 = x * 6; // 正确,右值引用
int &&z4 = x; // 错误,x是一个左值
11.Qt相关
(1)Qt的核心机制
Qt的核心机制包括:元对象系统、属性系统、信号与槽
元对象系统:
元对象系统(meta-object)提供了用于内部对象通讯的信号与槽(signals & slots)机制,运行时类型信息,以及动态属性系统(dynamic property system),整个元对象系统基于三个东西建立:
- QObject类为所有对象提供了一个基类,只要继承此类,创建出的对象便可以使用元对象系统
- 在声明类时,将Q_OBJECT宏放置于类的私有区域就可以在类中使能元对象特性,诸如动态属性,信号,以及槽。
- 元对象编译器(Meta-Object Compiler,缩写moc),为每个QObject的子类提供必要的代码去实现元对象特性。可以认为Qt对C++进行了一些拓展,moc则是负责将这些拓展语法翻译成原生的C++语法,之后交给C++编译器去编译。
moc工具读取c++源文件。如果它找到一个或多个包含Q_OBJECT宏的类声明,它会生成另一个c++源文件,其中包含每个类的元对象代码。生成的源文件要么#include到类的源文件中,要么(更常见的情况)编译并链接到类的实现。
属性系统:
如同很多编译器厂商提供的编译器一样,Qt也提供了一个精妙的属性系统。
然而,作为一个独立于编译器和架构的库,Qt不依赖于诸如__property或[property]这样的非标准的编译器特性。Qt的这套属性系统特性可以用于任何Qt支持的编译器与架构。它基于元对象系统(Meta-Object System),这套系统同时也提供信号与槽机制用于对象间通讯。
- 成员变量是一个“内”概念,反映的是类的结构构成。属性是一个“外”概念,反映的是类的逻辑意义。
- 成员变量没有读写权限控制,而属性可以指定为只读或只写,或可读可写。
- 成员变量不对读出作任何后处理,不对写入作任何预处理,而属性则可以。
- public成员变量可以视为一个可读可写、没有任何预处理或后处理的属性。 而private成员变量由于外部不可见,与属性“外”的特性不相符,所以不能视为属性。
- 虽然大多数情况下,属性会由某个或某些成员变量来表示,但属性与成员变量没有必然的对应关系, 比如与非门的 output 属性,就没有一个所谓的 $output 成员变量与之对应。 属性 与成员变量的 区别与联系 及属性修饰词理解
信号与槽:信号与槽机制是Qt的核心特性,也是与其他框架最大的不同之处,Qt的元对象系统使得信号与槽机制得以实现。
- 信号与槽用于对象之间的通讯,
- 在Qt中使用了信号与槽机制代替了回调机制。
- 关于信号:信号将会在特定的事件出现时被发出,Qt的控件预定义了很多信号,当然也可以继承这些控件以定义自己的子类,然后添加自己的信号。
- 关于槽:槽是在响应特定信号时会被调用的方法,Qt的控件预定义了很多槽,但通常的做法是继承控件以生成自己的子类,然后添加自己的槽,这样就可以自行处理感兴趣的信号。
(2)关于回调机制
(3)信号与槽机制原理
信号与槽的具体流程,可以将信号和槽理解成 命令-执行,即信号就是命令,槽就是执行命令。
- moc查找头文件中的signals,slots,标记出信号和槽。
- 将信号槽信息存储到类静态变量staticMetaObject中,并且按声明顺序进行存放,建立索引。
- 当发现有connect连接时,将信号槽的索引信息放到一个map中,彼此配对。
- 当调用emit时,调用信号函数,并且传递发送信号的对象指针,元对象指针,信号索引,参数列表到active函数
- 通过active函数找到在map中找到所有与信号对应的槽索引
- 根据槽索引找到槽函数,执行槽函数。
参考文章:https://blog.csdn.net/ddllrrbb/article/details/88374350
(4)信号与槽 与 回调函数的区别?
(5)信号槽机制的优势与不足?
优点:
缺点:
二、算法与数据结构
1.链表⭐
(1)数组和链表的区别?
(2)手写代码:反转链表?⭐⭐⭐
(3)手写代码:合并两个有序链表?⭐⭐⭐
(4)手写代码:判断一个链表是否为回文链表,给出思路?⭐⭐⭐
(5)手写代码:如何判断两个单向链表是否相交?⭐⭐⭐
(6)手写代码:数组中是否有重复数字?⭐⭐⭐
一个长度为N的整形数组,数组中每个元素的取值范围是[0,n-1],判断该数组否有重复的数,请说一下你的思路并手写代码?
2.堆/栈/队列⭐
(1)栈和堆的区别,以及为什么栈要快?
(2)什么是栈溢出,并举个简单例子导致栈溢出?
(3)手写代码:两个栈实现一个队列?
(4)请你说一说小根堆特点?
3.红黑树⭐
(1)红黑树介绍一下?
(2)红黑树和AVL树的定义与特点,以及二者区别?
浅析红黑树(RBTree)原理及实现_芮小谭的博客-CSDN博客
(3)map底层为什么用红黑树实现?
(4)map和unordered_map的底层实现?
(5)map和unordered_map优点和缺点?
(6)epoll怎么实现的?
(7)介绍一下B+树?
(8)Top(K)问题?
(9)介绍C++两种map?
(10)
(11)手写代码:二叉树的层序遍历并输出?
(12)手写代码:二叉树序列化反序列化
(13)哈夫曼编码?
4.十大排序⭐
注:堆雪差炮击,统计快归西
(1)快排的优化?
(2)请你来手写一下快排的代码?
请问求第k大的数的方法以及各自的复杂度是怎样的,另外追问一下,当有相同元素时,还可以使用什么不同的方法求第k大的元素?
请你来介绍一下各种排序算法及时间复杂度?
请你说一说你知道的排序算法及其复杂度?
请问海量数据如何去取最大的k个?
请问快排的时间复杂度最差是多少?什么时候时间最差?
请问稳定排序哪几种?
请你介绍一下快排算法;以及什么是稳定性排序,快排是稳定性的吗;快排算法最差情况推导公式?
5.哈希
请你来说一说hash表的实现,包括STL中的哈希桶长度常数?
请你回答一下hash表如何rehash,以及怎么处理其中保存的资源?
请你说一下哈希表的桶个数为什么是质数,合数有何不妥?
请你说一下解决hash冲突的方法?
请你说一说哈希冲突的解决方法?
6.动态规划⭐
(1)请你手写代码:最长公共连续子序列?
(2)手写代码:求一个字符串最长回文子串?
(3) 手写代码:查找最长回文子串?
7.字符串⭐
给你一个字符串,找出第一个不重复的字符,如“abbbabcd”,则第一个不重复就是c?
8.分治/贪心
9.高级算法
请问加密方法都有哪些?
什么是LRU缓存?
请你说一说洗牌算法?
三、操作系统
1.其他
(1)程序启动的过程?🚩
- 操作系统首先创建相应的进程并分配私有的进程空间,然后操作系统的加载器负责把可执行文件的数据段和代码段映射到进程的虚拟内存空间中。
- 加载器读入可执行程序的导入符号表,根据这些符号表可以查找出该可执行程序的所有依赖的动态链接库。
- 加载器针对该程序的每一个动态链接库调用LoadLibrary
- (1)查找对应的动态库文件,加载器为该动态链接库确定一个合适的基地址
- (2)加载器读取该动态链接库的导入符号表和导出符号表,比较应用程序要求的导入符号是否匹配该库的导出符号
- (3)针对该库的导入符号表,查找对应的依赖的动态链接库,如有跳转,则跳到3
- (4)调用该动态链接库的初始化函数
- 初始化应用程序的全局变量,对于全局对象自动调用构造函数。
- 进入应用程序入口点函数开始执行。
1.Linux相关
(1)Linux中查看进程运行状态的指令、查看内存使用情况的指令、tar解压文件的参数?
- 查看进程运行状态的指令:ps命令。“ps -aux | grep PID”,用来查看某PID进程状态
- 查看内存使用情况的指令:free命令。“free -m”,命令查看内存使用情况。
- tar解压文件的参数:
(2)文件权限怎么修改?
Linux文件的基本权限就有九个,分别是owner/group/others三种身份各有自己的read/write/execute权限
修改权限指令:chmod
举例:文件的权限字符为 -rwxrwxrwx 时,这九个权限是三个三个一组。其中,我们可以使用数字来代表各个权限。
各权限的分数对照如下:
r | w | x |
---|---|---|
4 | 2 | 1 |
每种身份(owner/group/others)各自的三个权限(r/w/x)分数是需要累加的,
例如当权限为: [-rwxrwx—] ,则分数是:
owner = rwx = 4+2+1 = 7
group = rwx = 4+2+1 = 7
others= — = 0+0+0 = 0
所以我们设定权限的变更时,该文件的权限数字就是770!变更权限的指令chmod的语法是这样的:
(3)说说常用的Linux命令?
- cd命令:用于切换当前目录
- ls命令:查看当前文件与目录
- grep命令:该命令常用于分析一行的信息,若当中有我们所需要的信息,就将该行显示出来,该命令通常与管道命令一起使用,用于对一些命令的输出进行筛选加工。
- cp命令:复制命令
- mv命令:移动文件或文件夹命令
- rm命令:删除文件或文件夹命令
- ps命令:查看进程情况
- kill命令:向进程发送终止信号
- tar命令:对文件进行打包,调用gzip或bzip对文件进行压缩或解压
- cat命令:查看文件内容,与less、more功能相似
- top命令:可以查看操作系统的信息,如进程、CPU占用率、内存信息等
- pwd命令:命令用于显示工作目录。
(4)软链接和硬链接的区别?
-
定义不同
软链接又叫符号链接,这个文件包含了另一个文件的路径名。可以是任意文件或目录,可以链接不同文件系统的文件。
硬链接就是一个文件的一个或多个文件名。把文件名和计算机文件系统使用的节点号链接起来。因此我们可以用多个文件名与同一个文件进行链接,这些文件名可以在同一目录或不同目录。
-
限制不同
硬链接只能对已存在的文件进行创建,不能交叉文件系统进行硬链接的创建;
软链接可对不存在的文件或目录创建软链接;可交叉文件系统;
-
创建方式不同
硬链接不能对目录进行创建,只可对文件创建;
软链接可对文件或目录创建;
-
影响不同
删除一个硬链接文件并不影响其他有相同 inode 号的文件。
删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接被称为死链接(即 dangling link,若被指向路径文件被重新创建,死链接可恢复为正常的软链接)。
(5)静态库和动态库怎么制作及如何使用,区别是什么?
- 静态库代码装载的速度快,执行速度略比动态库快。
- 动态库更加节省内存,可执行文件体积比静态库小很多。
- 静态库是在编译时加载,动态库是在运行时加载。
- 生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。
(6)GDB常见的调试命令,什么是条件断点,多进程下如何调试?
GDB调试:gdb调试的是可执行文件,在gcc编译时加入 -g ,告诉gcc在编译时加入调试信息,这样gdb才能调试这个被编译的文件 gcc -g tesst.c -o test
GDB命令格式:
-
quit:退出gdb,结束调试
-
list:查看程序源代码
list 5,10:显示5到10行的代码
list test.c:5, 10: 显示源文件5到10行的代码,在调试多个文件时使用
list get_sum: 显示get_sum函数周围的代码
list test,c get_sum: 显示源文件get_sum函数周围的代码,在调试多个文件时使用
-
reverse-search:字符串用来从当前行向前查找第一个匹配的字符串
-
run:程序开始执行
-
help list/all:查看帮助信息
-
break:设置断点
break 7:在第七行设置断点
break get_sum:以函数名设置断点
break 行号或者函数名 if 条件:以条件表达式设置断点
-
watch 条件表达式:条件表达式发生改变时程序就会停下来
-
next:继续执行下一条语句 ,会把函数当作一条语句执行
-
step:继续执行下一条语句,会跟踪进入函数,一次一条的执行函数内的代码
**条件断点:**break if 条件 以条件表达式设置断点
**多进程下如何调试:**用set follow-fork-mode child 调试子进程
或者set follow-fork-mode parent 调试父进程
-
在进行网络通信时是否需要进行字节序转换?
相同字节序的平台在进行网络通信时可以不进行字节序转换,但是跨平台进行网络数据通信时必须进行字节序转换。
原因如下:网络协议规定接收到得第一个字节是高字节,存放到低地址,所以发送时会首先去低地址取数据的高字节。小端模式的多字节数据在存放时,低地址存放的是低字节,而被发送方网络协议函数发送时会首先去低地址取数据(想要取高字节,真正取得是低字节),接收方网络协议函数接收时会将接收到的第一个字节存放到低地址(想要接收高字节,真正接收的是低字节),所以最后双方都正确的收发了数据。而相同平台进行通信时,如果双方都进行转换最后虽然能够正确收发数据,但是所做的转换是没有意义的,造成资源的浪费。而不同平台进行通信时必须进行转换,不转换会造成错误的收发数据,字节序转换函数会根据当前平台的存储模式做出相应正确的转换,如果当前平台是大端,则直接返回不进行转换,如果当前平台是小端,会将接收到得网络字节序进行转换。
-
网络字节序
网络上传输的数据都是字节流,对于一个多字节数值,在进行网络传输的时候,先传递哪个字节?也就是说,当接收端收到第一个字节的时候,它将这个字节作为高位字节还是低位字节处理,是一个比较有意义的问题; UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节(即:高位字节存放在低地址处);由此可见,多字节数值在发送之前,在内存中因该是以大端法存放的; 所以说,网络字节序是大端字节序; 比如,我们经过网络发送整型数值0x12345678时,在80X86平台中,它是以小端发存放的,在发送之前需要使用系统提供的字节序转换函数htonl()将其转换成大端法存放的数值;
(7)什么是大端小端,如何判断大端小端?
小端模式:低的有效字节存储在低的存储器地址。小端一般为主机字节序;常用的X86结构是小端模式。很多的ARM,DSP都为小端模式。
大端模式:高的有效字节存储在低的存储器地址。大端为网络字节序;KEIL C51则为大端模式。
有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
如何判断:我们可以根据联合体来判断系统是大端还是小端。因为联合体变量总是从低地址存储。
(8)Linux零拷贝的原理?
2.内存管理
(1)操作系统如何申请以及管理内存的?
操作系统如何管理内存:
-
物理内存:物理内存有四个层次,分别是寄存器、高速缓存、主存、磁盘。
寄存器:速度最快、量少、价格贵。
高速缓存:次之。
主存:再次之。
磁盘:速度最慢、量多、价格便宜。
操作系统会对物理内存进行管理,有一个部分称为内存管理器(memory manager),它的主要工作是有效的管理内存,记录哪些内存是正在使用的,在进程需要时分配内存以及在进程完成时回收内存。
-
虚拟内存:操作系统为每一个进程分配一个独立的地址空间,但是虚拟内存。虚拟内存与物理内存存在映射关系,通过页表寻址完成虚拟地址和物理地址的转换。
操作系统如何申请内存:
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:*brk和mmap
(2)什么是页表,为什么要有?
(3)操作系统中的缺页中断
(4)虚拟内存分布,什么时候会由用户态陷入内核态?
(5)简述虚拟内存和物理内存,为什么要用虚拟内存,好处是什么?
(6)虚拟地址到物理地址怎么映射的?
说堆栈溢出是什么,会怎么样?
简述操作系统中malloc的实现原理?
说说进程空间从高位到低位都有些什么?
32位系统能访问4GB以上的内存吗?
3.进程管理
(1)进程调度算法有哪些?
- 先来先服务调度算法
- 短作业(进程)优先调度算法
- 高优先级优先调度算法
- 时间片轮转法
- 多级反馈队列调度算法
(2)简述Linux系统态与用户态,什么时候会进入系统态?
参考回答
- 内核态与用户态:内核态(系统态)与用户态是操作系统的两种运行级别。内核态拥有最高权限,可以访问所有系统指令;用户态则只能访问一部分指令。
- 什么时候进入内核态:共有三种方式:a、系统调用。b、异常。c、设备中断。其中,系统调用是主动的,另外两种是被动的。
- 为什么区分内核态与用户态:在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。所以区分内核态与用户态主要是出于安全的考虑。
(3)简述LRU算法及其实现方式?
-
LRU算法:LRU算法用于缓存淘汰。思路是将缓存中最近最少使用的对象删除掉
-
实现方式:利用链表和hashmap。
当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。
在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。
(4)一个线程占多大内存?
一个linux的线程大概占8M内存。
linux的栈是通过缺页来分配内存的,不是所有栈地址空间都分配了内存。因此,8M是最大消耗,实际的内存消耗只会略大于实际需要的内存(内部损耗,每个在4k以内)。
(2)Linux进程调度算法及策略有哪些?
(3)请你说说并发和并行?
4.进程和线程的概念和区别⭐
说说进程、线程、协程是什么,区别是什么?
有了进程,为什么还要有线程?
说说多线程和多进程的不同?
多线程和单线程有什么区别,多线程编程要注意什么,多线程加锁需要注意什么?
5.线程同步⭐
说说进程同步的方式?
请你说说Linux的fork的作用?
互斥量能不能在进程中使用?
说说sleep和wait的区别?
1、sleep是一个延时函数,让进程或线程进入休眠。
休眠完毕后继续运行。在linux下面,sleep函数的参数是秒,而windows下面sleep的函数参数是毫秒
2、wait是父进程回收子进程PCB资源的一个系统调用。
进程一旦调用了wait函数,就立即阻塞自己本身,然后由wait函数自动分析当前进程的某个子进程是否已经退出,当找到一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞,直到有一个出现为止。
区别:
1、sleep是一个延时函数,让进程或线程进入休眠。休眠完毕后继续运行。
2、wait是父进程回收子进程PCB(Process Control Block)资源的一个系统调用
说说线程池的设计思路,线程池中线程的数量由什么确定?
进程和线程相比,为什么慢?
6.进程的通信方式⭐
管道、信号量、消息队列、共享内存、套接字、剪切板、油槽
请你说说什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程?
请你说说什么是守护进程,如何实现?
说说进程通信的方式有哪些?
说说进程有多少种状态?
进程通信中的管道实现原理是什么?
简述mmap的原理和使用场景?
协程是轻量级线程,轻量级表现在哪里?
说说常见信号有哪些,表示什么含义?
说说线程间通信的方式有哪些?
说说线程同步方式有哪些?
说说什么是信号量,有什么作用?
进程、线程的中断切换的过程是怎样的?
请你说说线程有哪些状态,相互之间怎么转换?
7.死锁
说说什么是死锁,产生的条件,如何解决?
单核机器上写多线程程序,是否要考虑加锁,为什么?
简述互斥锁的机制,互斥锁与读写的区别?
简述自旋锁和互斥锁的使用场景?
8.网络与系统编程
(1)epoll和select的区别,epoll为什么高效?
select的特点:
- 文件描述符受限1024,随着文件描述符增多,其效率会呈现线性降低。
- select中会有频繁的数据拷贝(用户态与内核态之间上下文切换,数据拷贝)
- 需要遍历文件描述符集合来确定谁就绪(时间花销很大),且在内核中也需要依次遍历。
注:select的出现具有跨时代的意义,其将原先并发的机制由多线程、多进程的形式改为单个进程就可以实现并发管理。
epoll的特点:
总结:poll特点
- 文件描述符不受限,因为其底层使用的是链表
- poll中也会有频繁的数据拷贝,
- 其效率会呈现线性降低。
- poll中引入的了event的概念。
epoll与select的区别:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-whHo0LFC-1693106684788)(https://s2.loli.net/2023/07/21/OY7PDNuFUbGRWns.png)]
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;而epoll保证了每个fd在整个过程中只会拷贝一次(利用mmap实现)。
- 每次调用select都需要在内核遍历传递进来的所有fd;而epoll只需要轮询一次fd集合,同时查看就绪链表中有没有就绪的fd就可以了。
- select支持的文件描述符数量太小了,默认是1024;而epoll没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048。
(2)epoll为什么高效
- select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。
- select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把当前进程往设备等待队列中挂一次,而epoll只要一次拷贝,而且把当前进程往等待队列上挂也只挂一次,这也能节省不少的开销。
(2)多路IO复用技术有哪些,区别是什么?
-
select,poll,epoll都是IO多路复用的机制,I/O多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知应用程序进行相应的读写操作。
-
区别:
(1)poll与select不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。
(2)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。
(3)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把当前进程往设备等待队列中挂一次,而epoll只要一次拷贝,而且把当前进程往等待队列上挂也只挂一次,这也能节省不少的开销。
(3)简述socket中select,epoll的使用场景和区别,epoll水平触发与边缘触发的区别?
-
select,epoll的使用场景:都是IO多路复用的机制,应用于高并发的网络编程的场景。I/O多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知应用程序进行相应的读写操作。
-
select,epoll的区别:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;而epoll保证了每个fd在整个过程中只会拷贝一次。
(2)每次调用select都需要在内核遍历传递进来的所有fd;而epoll只需要轮询一次fd集合,同时查看就绪链表中有没有就绪的fd就可以了。
(3)select支持的文件描述符数量太小了,默认是1024;而epoll没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048。
-
epoll水平触发与边缘触发的区别
LT模式(水平触发)下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作;
而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。
(4)说说Reactor、Proactor模式?
在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
-
Reactor模式:Reactor模式应用于同步I/O的场景。Reactor中读操作的具体步骤如下:
读取操作:
(1)应用程序注册读就需事件和相关联的事件处理器
(2)事件分离器等待事件的发生
(3)当发生读就需事件的时候,事件分离器调用第一步注册的事件处理器
(4)事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理
-
Proactor模式:Proactor模式应用于异步I/O的场景。Proactor中读操作的具体步骤如下:
(1)应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
(2)事件分离器等待读取操作完成事件
(3)在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。
(4)事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。
-
区别:从上面可以看出,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要用户再自己接收数据,直接使用就可以了,操作系统会将数据从内核拷贝到用户区。
(5)简述同步与异步的区别,阻塞与非阻塞的区别?
-
同步与异步的区别:
同步:是所有的操作都做完,才返回给用户结果。即写完数据库之后,再响应用户,用户体验不好。
异步:不用等所有操作都做完,就响应用户请求。即先响应用户请求,然后慢慢去写数据库,用户体验较好。
-
阻塞与非阻塞的区别:
阻塞:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
非阻塞:非阻塞等待,每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。
(6)BIO、NIO有什么区别?
BIO(Blocking I/O):阻塞IO。调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
NIO(New I/O):同时支持阻塞与非阻塞模式,NIO的做法是叫一个线程不断的轮询每个IO的状态,看看是否有IO的状态发生了改变,从而进行下一步的操作。
(7)介绍一下5种IO模型?
- 阻塞IO:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
- 非阻塞IO:非阻塞等待,每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。
- 信号驱动IO:Linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进程收到SIGIO信号,然后处理IO事件。
- IO多路复用:Linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检查。知道有数据可读或可写时,才真正调用IO操作函数。
- 异步IO:Linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。用户可以直接去使用数据。
(8)socket网络编程中客户端和服务端用到哪些函数?
-
服务器端函数:
(1)socket创建一个套接字
(2)bind绑定ip和port
(3)listen使套接字变为可以被动链接
(4)accept等待客户端的链接
(5)write/read接收发送数据
(6)close关闭连接
-
客户端函数:
(1)创建一个socket,用函数socket()
(2)bind绑定ip和port
(3)连接服务器,用函数connect()
(4)收发数据,用函数send()和recv(),或read()和write()
(5)close关闭连接
四、计算机网络
1.网络通信模型
(1)简述静态路由和动态路由?
- 静态路由是由系统管理员设计与构建的路由表规定的路由。适用于网关数量有限的场合,且网络拓朴结构不经常变化的网络。其缺点是不能动态地适用网络状况的变化,当网络状况变化后必须由网络管理员修改路由表。
- 动态路由是由路由选择协议而动态构建的,路由协议之间通过交换各自所拥有的路由信息实时更新路由表的内容。动态路由可以自动学习网络的拓朴结构,并更新路由表。其缺点是路由广播更新信息将占据大量的网络带宽。
(2)路由协议有哪些,都是如何更新的?
-
路由可分为静态&动态路由。静态路由由管理员手动维护;动态路由由路由协议自动维护。
路由选择算法的必要步骤:
1)向其它路由器传递路由信息;
2)接收其它路由器的路由信息;
3)根据收到的路由信息计算出到每个目的网络的最优路径,并由此生成路由选择表;
4)根据网络拓扑的变化及时的做出反应,调整路由生成新的路由选择表,同时把拓扑变化以路由 信息的形式向其它路由器宣告。
两种主要算法:距离向量法(Distance Vector Routing)和链路状态算法(Link-State Routing)。
由此可分为距离矢量(如:RIP、IGRP、EIGRP)&链路状态路由协议(如:OSPF、IS-IS)。 路由协议是路由器之间实现路由信息共享的一种机制,它允许路由器之间相互交换和维护各 自的路由表。当一台路由器的路由表由于某种原因发生变化时,它需要及时地将这一变化通 知与之相连接的其他路由器,以保证数据的正确传递。路由协议不承担网络上终端用户之间 的数据传输任务。
-
1)RIP 路由协议:RIP 协议最初是为 Xerox 网络系统的 Xerox parc 通用协议而设计的,是 Internet 中常用的 路由协议。RIP 采用距离向量算法,即路由器根据距离选择路由,所以也称为距离向量协议。 路由器收集所有可到达目的地的不同路径,并且保存有关到达每个目的地的最少站点数的路 径信息,除到达目的地的最佳路径外,任何其它信息均予以丢弃。同时路由器也把所收集的 路由信息用 RIP 协议通知相邻的其它路由器。这样,正确的路由信息逐渐扩散到了全网。RIP 使用非常广泛,它简单、可靠,便于配置。但是 RIP 只适用于小型的同构网络,因 为它允许的最大站点数为 15,任何超过 15 个站点的目的地均被标记为不可达。而且 RIP 每 隔 30s 一次的路由信息广播也是造成网络的广播风暴的重要原因之一。
2)OSPF 路由协议:0SPF 是一种基于链路状态的路由协议,需要每个路由器向其同一管理域的所有其它路 由器发送链路状态广播信息。在 OSPF 的链路状态广播中包括所有接口信息、所有的量度和 其它一些变量。利用 0SPF 的路由器首先必须收集有关的链路状态信息,并根据一定的算法 计算出到每个节点的最短路径。而基于距离向量的路由协议仅向其邻接路由器发送有关路由 更新信息。与 RIP 不同,OSPF 将一个自治域再划分为区,相应地即有两种类型的路由选择方式: 当源和目的地在同一区时,采用区内路由选择;当源和目的地在不同区时,则采用区间路由 选择。这就大大减少了网络开销,并增加了网络的稳定性。当一个区内的路由器出了故障时 并不影响自治域内其它区路由器的正常工作,这也给网络的管理、维护带来方便。
3)BGP 和 BGP4 路由协议:BGP 是为 TCP/IP 互联网设计的外部网关协议,用于多个自治域之间。它既不是基于纯 粹的链路状态算法,也不是基于纯粹的距离向量算法。它的主要功能是与其它自治域的 BGP 交换网络可达信息。各个自治域可以运行不同的内部网关协议。BGP 更新信息包括网络号/ 自治域路径的成对信息。自治域路径包括到达某个特定网络须经过的自治域串,这些更新信 息通过 TCP 传送出去,以保证传输的可靠性。为了满足 Internet 日益扩大的需要,BGP 还在不断地发展。在最新的 BGP4 中,还可以 将相似路由合并为一条路由。
4)IGRP 和 EIGRP 协议:EIGRP 和早期的 IGRP 协议都是由 Cisco 发明,是基于距离向量算法的动态路由协议。 EIGRP(Enhanced Interior Gateway Routing Protocol)是增强版的 IGRP 协议。它属于动态内部网 关路由协议,仍然使用矢量-距离算法。但它的实现比 IGRP 已经有很大改进,其收敛特性 和操作效率比 IGRP 有显著的提高。它的收敛特性是基于 DUAL ( Distributed Update Algorithm ) 算法的。DUAL 算法使得路径 在路由计算中根本不可能形成环路。它的收敛时间可以与已存在的其他任何路由协议相匹敌
Enhanced IGRP 与其它路由选择协议之间主要区别包括:收敛宽速(Fast Convergence)、 支持变长子网掩模(Subnet Mask)、局部更新和多网络层协议。执行 Enhanced IGRP 的路由 器存储了所有其相邻路由表,以便于它能快速利用各种选择路径(Alternate Routes)。如果没有合适路径,Enhanced IGRP 查询其邻居以获取所需路径。直到找到合适路径,EnhancedIGRP 查询才会终止,否则一直持续下去。
EIGRP 不作周期性更新。取而代之,当路径度量标准改变时,Enhanced IGRP 只发送局 部更新(Partial Updates)信息。局部更新信息的传输自动受到限制,从而使得只有那些需 要信息的路由器才会更新。基于以上这两种性能,因此 Enhanced IGRP 损耗的带宽比 IGRP 少得多。
(3)简述域名解析过程,本机如何干预域名解析?
-
(1)在浏览器中输入www.qq.com域名,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。
(2)如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。
(3)如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,首先会找TCP/IP参数中设置的首选DNS服务器,在此我们叫它本地DNS服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。
(4)如果要查询的域名,不由本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性。
(5)如果本地DNS服务器本地区域文件与缓存解析都失效,则根据本地DNS服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地DNS就把请求发至13台根DNS,根DNS服务器收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后,将会联系负责.com域的这台服务器。这台负责.com域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址(qq.com)给本地DNS服务器。当本地DNS服务器收到这个地址后,就会找qq.com域服务器,重复上面的动作,进行查询,直至找到www.qq.com主机。
(6)如果用的是转发模式,此DNS服务器就会把请求转发至上一级DNS服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根DNS或把转请求转至上上级,以此循环。不管是本地DNS服务器用是是转发,还是根提示,最后都是把结果返回给本地DNS服务器,由此DNS服务器再返回给客户机。
从客户端到本地DNS服务器是属于递归查询,而DNS服务器之间就是的交互查询就是迭代查询
-
通过修改本机host来干预域名解析,
(4)简述 DNS 查询服务器的基本流程是什么?DNS 劫持是什么?
-
打开浏览器,输入一个域名。比如输入www.163.com,这时,你使用的电脑会发出一个DNS请求到本地DNS服务器。本地DNS服务器一般都是你的网络接入服务器商提供,比如中国电信,中国移动。
DNS请求到达本地DNS服务器之后,本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果。如果没有,本地DNS服务器还要向DNS根服务器进行查询。
根DNS服务器没有记录具体的域名和IP地址的对应关系,而是告诉本地DNS服务器,你可以到域服务器上去继续查询,并给出域服务器的地址。
本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。
最后,本地DNS服务器向域名的解析服务器发出请求,这时就能收到一个域名和IP地址对应关系,本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。
-
DNS劫持就是通过劫持了DNS服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致对该域名的访问由原IP地址转入到修改后的指定IP,其结果就是对特定的网址不能访问或访问的是假网址,从而实现窃取资料或者破坏原有正常服务的目的。DNS劫持通过篡改DNS服务器上的数据返回给用户一个错误的查询结果来实现的。
DNS劫持症状:在某些地区的用户在成功连接宽带后,首次打开任何页面都指向ISP提供的“电信互联星空”、“网通黄页广告”等内容页面。还有就是曾经出现过用户访问Google域名的时候出现了百度的网站。这些都属于DNS劫持。
(5)简述网关的作用是什么,同一网段的主机如何通信?
-
网关即网络中的关卡,我们的互联网是一个一个的局域网、城域网、等连接起来的,在连接点上就是一个一个网络的关卡,即我们的网关,他是保证网络互连的,翻译和转换,使得不同的网络体系能够进行。
-
网内通信,即通信双方都位处同一网段中,数据传输无需经过路由器(或三层交换机),即可由本网段自主完成。
假设发送主机的ARP表中并无目的主机对应的表项,则发送主机会以目的主机IP地址为内容,广播ARP请求以期获知目的主机MAC地址,并通过交换机(除到达端口之外的所有端口发送,即洪泛(Flooding))向全网段主机转发,而只有目的主机接收到此ARP请求后会将自己的MAC地址和IP地址装入ARP应答后将其回复给发送主机,发送主机接收到此ARP应答后,从中提取目的主机的MAC地址,并在其ARP表中建立目的主机的对应表项(IP地址到MAC地址的映射),之后即可向目的主机发送数据,将待发送数据封装成帧,并通过二层设备(如交换机)转发至本网段内的目的主机,自此完成通信。
(6) MAC地址和IP地址分别有什么作用?
- IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。而MAC地址,指的是物理地址,用来定义网络设备的位置。
- IP地址的分配是根据网络的拓扑结构,而不是根据谁制造了网络设置。若将高效的路由选择方案建立在设备制造商的基础上而不是网络所处的拓朴位置基础上,这种方案是不可行的。
- 当存在一个附加层的地址寻址时,设备更易于移动和维修。例如,如果一个以太网卡坏了,可以被更换,而无须取得一个新的IP地址。如果一个IP主机从一个网络移到另一个网络,可以给它一个新的IP地址,而无须换一个新的网卡。
- 无论是局域网,还是广域网中的计算机之间的通信,最终都表现为将数据包从某种形式的链路上的初始节点出发,从一个节点传递到另一个节点,最终传送到目的节点。数据包在这些节点之间的移动都是由ARP(Address Resolution Protocol:地址解析协议)负责将IP地址映射到MAC地址上来完成的。
(7)简述网络七层参考模型,以及每一层的作用?
- 物理层:定义物理设备的标准,主要对物理连接方式,电气特性,机械特性等制定统一标准。
- 数据链路层:主要是对物理层传输的比特流包装,检测保证数据传输的可靠性,将物理层接收的数据进行MAC(媒体访问控制)地址的封装和解封装
- 网络层:控制子网的运行,如逻辑编址,分组传输,路由选择
- 传输层:定义一些传输数据的协议和端口。
- 会话层:负责在网络中的两节点建立,维持和终止通信。
- 表示层:确保一个系统的应用层发送的消息可以被另一个系统的应用层读取,编码转换,数据解析,管理数据的解密和加密。
- 应用层:文件传输,文件管理,电子邮件的信息处理
OSI七层模型 | 功能 | 对应的网络协议 | TCP/IP四层概念模型 |
---|---|---|---|
应用层 | 文件传输,文件管理,电子邮件的信息处理 | HTTP、TFTP, FTP, NFS, WAIS、SMTP | 应用层 |
表示层 | 确保一个系统的应用层发送的消息可以被另一个系统的应用层读取,编码转换,数据解析,管理数据的解密和加密。 | Telnet, Rlogin, SNMP, Gopher | 应用层 |
会话层 | 负责在网络中的两节点建立,维持和终止通信。 | SMTP, DNS | 应用层 |
传输层 | 定义一些传输数据的协议和端口。 | TCP, UDP | 传输层 |
网络层 | 控制子网的运行,如逻辑编址,分组传输,路由选择 | IP, ICMP, ARP, RARP, AKP, UUCP | 网络层 |
数据链路层 | 主要是对物理层传输的比特流包装,检测保证数据传输的可靠性,将物理层接收的数据进行MAC(媒体访问控制)地址的封装和解封装 | FDDI, Ethernet, Arpanet, PDN, SLIP, PPP,STP。HDLC,SDLC,帧中继 | 数据链路层 |
物理层 | 定义物理设备的标准,主要对物理连接方式,电气特性,机械特性等制定统一标准。 | IEEE 802.1A, IEEE 802.2到IEEE 802. | 数据链路层 |
2.TCP⭐
(1)TCP 三次握手和四次挥手的过程?
三次握手:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B69GP0pb-1693106684788)(https://s2.loli.net/2023/07/21/CzKN71gwJXTMSL6.png)]
- 第一次握手:建立连接时,客户端向服务器发送SYN包(seq=x),请求建立连接,等待确认
- 第二次握手:服务端收到客户端的SYN包,回一个ACK包(ACK=x+1)确认收到,同时发送一个SYN包(seq=y)给客户端
- 第三次握手:客户端收到SYN+ACK包,再回一个ACK包(ACK=y+1)告诉服务端已经收到
- 三次握手完成,成功建立连接,开始传输数据
四次挥手:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ECgKZrZp-1693106684789)(https://s2.loli.net/2023/07/21/RbiKruFBQWpl1GL.png)]
- 客户端发送FIN包(FIN=1)给服务端,告诉它自己的数据已经发送完毕,请求终止连接,此时客户端不发送数据,但还能接收数据
- 服务端收到FIN包,回一个ACK包给客户端告诉它已经收到包了,此时还没有断开socket连接,而是等待剩下的数据传输完毕
- 服务端等待数据传输完毕后,向客户端发送FIN包,表明可以断开连接
- 客户端收到后,回一个ACK包表明确认收到,等待一段时间,确保服务端不再有数据发过来,然后彻底断开连接
(2)TCP 连接和关闭的具体步骤?
TCP通过三次握手建立链接:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TaWIqrln-1693106684789)(https://s2.loli.net/2023/07/21/6zIfcWVyiQbMDEa.png)]
- 第一次握手:建立连接时,客户端向服务器发送SYN包(seq=x),请求建立连接,等待确认
- 第二次握手:服务端收到客户端的SYN包,回一个ACK包(ACK=x+1)确认收到,同时发送一个SYN包(seq=y)给客户端
- 第三次握手:客户端收到SYN+ACK包,再回一个ACK包(ACK=y+1)告诉服务端已经收到
- 三次握手完成,成功建立连接,开始传输数据
TCP通过四次挥手关闭链接:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bQosu8Bj-1693106684792)(https://s2.loli.net/2023/07/21/WmOypPzDe2osvJQ.png)]
- 客户端发送FIN包(FIN=1)给服务端,告诉它自己的数据已经发送完毕,请求终止连接,此时客户端不发送数据,但还能接收数据
- 服务端收到FIN包,回一个ACK包给客户端告诉它已经收到包了,此时还没有断开socket连接,而是等待剩下的数据传输完毕
- 服务端等待数据传输完毕后,向客户端发送FIN包,表明可以断开连接
- 客户端收到后,回一个ACK包表明确认收到,等待一段时间,确保服务端不再有数据发过来,然后彻底断开连接
(3)TCP 2次握手行不行?为什么要3次?
- 为了实现可靠数据传输, TCP 协议的通信双方都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。
- 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤。
- 如果只是两次握手,至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认。
(5)TCP 连接 和 关闭的状态转移?
(6)TCP 慢启动?
- 慢启动(Slow Start),是传输控制协议(TCP)使用的一种阻塞控制机制。慢启动也叫做指数增长期。慢启动是指每次TCP接收窗口收到确认时都会增长。增加的大小就是已确认段的数目。这种情况一直保持到要么没有收到一些段,要么窗口大小到达预先定义的阈值。如果发生丢失事件,TCP就认为这是网络阻塞,就会采取措施减轻网络拥挤。一旦发生丢失事件或者到达阈值,TCP就会进入线性增长阶段。这时,每经过一个RTT窗口增长一个段。
(7)TCP 如何保证有序?
-
主机每次发送数据时,TCP就给每个数据包分配一个序列号并且在一个特定的时间内等待接收主机对分配的这个序列号进行确认,如果发送主机在一个特定时间内没有收到接收主机的确认,则发送主机会重传此数据包。接收主机利用序列号对接收的数据进行确认,以便检测对方发送的数据是否有丢失或者乱序等,接收主机一旦收到已经顺序化的数据,它就将这些数据按正确的顺序重组成数据流并传递到高层进行处理。
-
具体步骤如下:
(1)为了保证数据包的可靠传递,发送方必须把已发送的数据包保留在缓冲区;
(2)并为每个已发送的数据包启动一个超时定时器;
(3)如在定时器超时之前收到了对方发来的应答信息(可能是对本包的应答,也可以是对本包后续包的应答),则释放该数据包占用的缓冲区;
(4)否则,重传该数据包,直到收到应答或重传次数超过规定的最大次数为止。
(5)接收方收到数据包后,先进行CRC校验,如果正确则把数据交给上层协议,然后给发送方发送一个累计应答包,表明该数据已收到,如果接收方正好也有数据要发给发送方,应答包也可方在数据包中捎带过去。
(8)TCP 常见的拥塞控制算法有哪些?
-
TCP Tahoe/Reno
最初的实现,包括慢启动、拥塞避免两个部分。基于重传超时(retransmission timeout/RTO)和重复确认为条件判断是否发生了丢包。两者的区别在于:Tahoe算法下如果收到三次重复确认,就进入快重传立即重发丢失的数据包,同时将慢启动阈值设置为当前拥塞窗口的一半,将拥塞窗口设置为1MSS,进入慢启动状态;而Reno算法如果收到三次重复确认,就进入快重传,但不进入慢启动状态,而是直接将拥塞窗口减半,进入拥塞控制阶段,这称为“快恢复”。
而Tahoe和Reno算法在出现RTO时的措施一致,都是将拥塞窗口降为1个MSS,然后进入慢启动阶段。
-
TCP BBR(Bottleneck Bandwidth and Round-trip propagation time)
BBR是由Google设计,于2016年发布的拥塞算法。以往大部分拥塞算法是基于丢包来作为降低传输速率的信号,而BBR则基于模型主动探测。该算法使用网络最近出站数据分组当时的最大带宽和往返时间来建立网络的显式模型。数据包传输的每个累积或选择性确认用于生成记录在数据包传输过程和确认返回期间的时间内所传送数据量的采样率。该算法认为随着网络接口控制器逐渐进入千兆速度时,分组丢失不应该被认为是识别拥塞的主要决定因素,所以基于模型的拥塞控制算法能有更高的吞吐量和更低的延迟,可以用BBR来替代其他流行的拥塞算法,例如CUBIC。
(9)TCP 超时重传?
TCP可靠性中最重要的一个机制是处理数据超时和重传。TCP协议要求在发送端每发送一个报文段,就启动一个定时器并等待确认信息;接收端成功接收新数据后返回确认信息。若在定时器超时前数据未能被确认,TCP就认为报文段中的数据已丢失或损坏,需要对报文段中的数据重新组织和重传。
(10)TCP 可靠性保证?
TCP主要提供了检验和、序列号/确认应答、超时重传、最大消息长度、滑动窗口控制等方法实现了可靠性传输。
检验和
通过检验和的方式,接收端可以检测出来数据是否有差错和异常,假如有差错就会直接丢弃TCP段,重新发送。TCP在计算检验和时,会在TCP首部加上一个12字节的伪首部。检验和总共计算3部分:TCP首部、TCP数据、TCP伪首部
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-STTGDTHm-1693106684793)(https://s2.loli.net/2023/07/21/CR5unz1gtfKGU2a.png)]
序列号/确认应答
-
这个机制类似于问答的形式。比如在课堂上老师会问你“明白了吗?”,假如你没有隔一段时间没有回应或者你说不明白,那么老师就会重新讲一遍。其实计算机的确认应答机制也是一样的,发送端发送信息给接收端,接收端会回应一个包,这个包就是应答包。
-
上述过程中,只要发送端有一个包传输,接收端没有回应确认包(ACK包),都会重发。或者接收端的应答包,发送端没有收到也会重发数据。这就可以保证数据的完整性。
超时重传
- 超时重传是指发送出去的数据包到接收到确认包之间的时间,如果超过了这个时间会被认为是丢包了,需要重传。那么我们该如何确认这个时间值呢?
- 我们知道,一来一回的时间总是差不多的,都会有一个类似于平均值的概念。比如发送一个包到接收端收到这个包一共是0.5s,然后接收端回发一个确认包给发送端也要0.5s,这样的两个时间就是RTT(往返时间)。然后可能由于网络原因的问题,时间会有偏差,称为抖动(方差)。
- 从上面的介绍来看,超时重传的时间大概是比往返时间+抖动值还要稍大的时间。
- 但是在重发的过程中,假如一个包经过多次的重发也没有收到对端的确认包,那么就会认为接收端异常,强制关闭连接。并且通知应用通信异常强行终止。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5VrRX8Om-1693106684794)(https://s2.loli.net/2023/07/21/pD7AWUVsNuwM5jS.png)]
最大消息长度
在建立TCP连接的时候,双方约定一个最大的长度(MSS)作为发送的单位,重传的时候也是以这个单位来进行重传。理想的情况下是该长度的数据刚好不被网络层分块。
滑动窗口控制
我们上面提到的超时重传的机制存在效率低下的问题,发送一个包到发送下一个包要经过一段时间才可以。所以我们就想着能不能不用等待确认包就发送下一个数据包呢?这就提出了一个滑动窗口的概念。
窗口的大小就是在无需等待确认包的情况下,发送端还能发送的最大数据量。这个机制的实现就是使用了大量的缓冲区,通过对多个段进行确认应答的功能。通过下一次的确认包可以判断接收端是否已经接收到了数据,如果已经接收了就从缓冲区里面删除数据。
在窗口之外的数据就是还未发送的和对端已经收到的数据。那么发送端是怎么样判断接收端有没有接收到数据呢?或者怎么知道需要重发的数据有哪些呢?通过下面这个图就知道了。
拥塞控制
- 窗口控制解决了 两台主机之间因传送速率而可能引起的丢包问题,在一方面保证了TCP数据传送的可靠性。然而如果网络非常拥堵,此时再发送数据就会加重网络负担,那么发送的数据段很可能超过了最大生存时间也没有到达接收方,就会产生丢包问题。为此TCP引入慢启动机制,先发出少量数据,就像探路一样,先摸清当前的网络拥堵状态后,再决定按照多大的速度传送数据。
- 发送开始时定义拥塞窗口大小为1;每次收到一个ACK应答,拥塞窗口加1;而在每次发送数据时,发送窗口取拥塞窗口与接送段接收窗口最小者。
- 慢启动:在启动初期以指数增长方式增长;设置一个慢启动的阈值,当以指数增长达到阈值时就停止指数增长,按照线性增长方式增加至拥塞窗口;线性增长达到网络拥塞时立即把拥塞窗口置回1,进行新一轮的“慢启动”,同时新一轮的阈值变为原来的一半。
(11)TCP 滑动窗口以及重传机制?
-
滑动窗口协议是传输层进行流控的一种措施,接收方通过通告发送方自己的窗口大小,从而控制发送方的发送速度,从而达到防止发送方发送速度过快而导致自己被淹没的目的。
TCP的滑动窗口解决了端到端的流量控制问题,允许接受方对传输进行限制,直到它拥有足够的缓冲空间来容纳更多的数据。
-
TCP在发送数据时会设置一个计时器,若到计时器超时仍未收到数据确认信息,则会引发相应的超时或基于计时器的重传操作,计时器超时称为重传超时(RTO) 。另一种方式的重传称为快速重传,通常发生在没有延时的情况下。若TCP累积确认无法返回新的ACK,或者当ACK包含的选择确认信息(SACK)表明出现失序报文时,快速重传会推断出现丢包,需要重传。
(12)滑动窗口过小怎么办?
- 我们可以假设窗口的大小是1,也是就每次只能发送一个数据,并且发送方只有接受方对这个数据进行确认了以后才能发送下一个数据。如果说窗口过小,那么当传输比较大的数据的时候需要不停的对数据进行确认,这个时候就会造成很大的延迟。
(13)如果三次握手时候每次握手信息对方没收到会怎么样,分情况介绍?
-
如果第一次握手消息丢失,那么请求方不会得到ack消息,超时后进行重传
-
如果第二次握手消息丢失,那么请求方不会得到ack消息,超时后进行重传
-
如果第三次握手消息丢失,那么Server 端该TCP连接的状态为SYN_RECV,并且会根据 TCP的超时重传机制,会等待3秒、6秒、12秒后重新发送SYN+ACK包,以便Client重新发送ACK包。而Server重发SYN+ACK包的次数,可以设置/proc/sys/net/ipv4/tcp_synack_retries修改,默认值为5.如果重发指定次数之后,仍然未收到 client 的ACK应答,那么一段时间后,Server自动关闭这个连接。
client 一般是通过 connect() 函数来连接服务器的,而connect()是在 TCP的三次握手的第二次握手完成后就成功返回值。也就是说 client 在接收到 SYN+ACK包,它的TCP连接状态就为 established (已连接),表示该连接已经建立。那么如果 第三次握手中的ACK包丢失的情况下,Client 向 server端发送数据,Server端将以 RST包响应,方能感知到Server的错误。
(14)TCP 的 TIME_WAIT,为什么需要有这个状态?
-
TIME_WAIT状态也成为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。
对一个具体实现所给定的MSL值,处理的原则是:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。
这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。
-
理论上,四个报文都发送完毕,就可以直接进入CLOSE状态了,但是可能网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文
(15)什么是 MSL,为什么客户端连接要等待2MSL的时间才能完全关闭?
- MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
- 为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。最后客户端和服务器都能正常的关闭。假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。
-
两个理由:
-
保证客户端发送的最后一个ACK报文段能够到达服务端。
这个ACK报文段有可能丢失,使得处于LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认,服务端超时重传FIN+ACK报文段,而客户端能在2MSL时间内收到这个重传的FIN+ACK报文段,接着客户端重传一次确认,重新启动2MSL计时器,最后客户端和服务端都进入到CLOSED状态,若客户端在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到服务端重传的FIN+ACK报文段,所以不会再发送一次确认报文段,则服务端无法正常进入到CLOSED状态。
-
防止“已失效的连接请求报文段”出现在本连接中。
客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
-
(17)什么是 TCP 粘包和拆包?
- TCP是个“流”协议,所谓流,就是没有界限的一串数据。大家可以想想河里的流水,是连成一片的,其间并没有分界线。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。
(19)TCP 的 keepalive,以及和 HTTP 的 keepalive 的区别?
-
HTTP Keep-Alive
在http早期,每个http请求都要求打开一个tpc socket连接,并且使用一次之后就断开这个tcp连接。使用keep-alive可以改善这种状态,即在一次TCP连接中可以持续发送多份数据而不会断开连接。通过使用keep-alive机制,可以减少tcp连接建立次数,也意味着可以减少TIME_WAIT状态连接,以此提高性能和提高httpd服务器的吞吐率(更少的tcp连接意味着更少的系统内核调用,socket的accept()和close()调用)。但是,keep-alive并不是免费的午餐,长时间的tcp连接容易导致系统资源无效占用。配置不当的keep-alive,有时比重复利用连接带来的损失还更大。所以,正确地设置keep-alive timeout时间非常重要。
-
TCP KEEPALIVE
链接建立之后,如果应用程序或者上层协议一直不发送数据,或者隔很长时间才发送一次数据,当链接很久没有数据报文传输时如何去确定对方还在线,到底是掉线了还是确实没有数据传输,链接还需不需要保持,这种情况在TCP协议设计中是需要考虑到的。TCP协议通过一种巧妙的方式去解决这个问题,当超过一段时间之后,TCP自动发送一个数据为空的报文给对方,如果对方回应了这个报文,说明对方还在线,链接可以继续保持,如果对方没有报文返回,并且重试了多次之后则认为链接丢失,没有必要保持链接。
-
TCP的keepalive机制和HTTP的keep-alive机制是说的完全不同的两个东西,tcp的keepalive是在ESTABLISH状态的时候,双方如何检测连接的可用行。而http的keep-alive说的是如何避免进行重复的TCP三次握手和四次挥手的环节。
(20)TCP 协议的延迟 ACK 和累计应答?
- 延迟应答指的是:TCP在接收到对端的报文后,并不会立即发送ack,而是等待一段时间发送ack,以便将ack和要发送的数据一块发送。当然ack不能无限延长,否则对端会认为包超时而造成报文重传。linux采用动态调节算法来确定延时的时间。
- 累计应答指的是:为了保证顺序性,每一个包都有一个ID(序号),在建立连接的时候,会商定起始的ID是多少,然后按照ID一个个发送。而为了保证不丢包,对应发送的包都要进行应答,但不是一个个应答,而是会应答某个之前的ID,该模式称为累计应答
(21)TCP 如何加速一个大文件的传输?
- 建连优化:TCP 在建立连接时,如果丢包,会进入重试,重试时间是 1s、2s、4s、8s 的指数递增间隔,缩短定时器可以让 TCP 在丢包环境建连时间更快,非常适用于高并发短连接的业务场景。
- 平滑发包:在 RTT 内均匀发包,规避微分时间内的流量突发,尽量避免瞬间拥塞
- 丢包预判:有些网络的丢包是有规律性的,例如每隔一段时间出现一次丢包,例如每次丢包都连续丢几个等,如果程序能自动发现这个规律(有些不明显),就可以针对性提前多发数据,减少重传时间、提高有效发包率。
- RTO 探测:若始终收不到 ACK 报文,则需要触发 RTO 定时器。RTO 定时器一般都时间非常长,会浪费很多等待时间,而且一旦 RTO,CWND 就会骤降(标准 TCP),因此利用 Probe 提前与 RTO 去试探,可以规避由于 ACK 报文丢失而导致的速度下降问题。
- 带宽评估:通过单位时间内收到的 ACK 或 SACK 信息可以得知客户端有效接收速率,通过这个速率可以更合理的控制发包速度。
- 带宽争抢:有些场景(例如合租)是大家互相挤占带宽的,假如你和室友各 1Mbps 的速度看电影,会把 2Mbps 出口占满,而如果一共有 3 个人看,则每人只能分到 1/3。若此时你的流量流量达到 2Mbps,而他俩还都是 1Mbps,则你至少仍可以分到 2/(2+1+1) * 2Mbps = 1Mbps 的 50% 的带宽,甚至更多,代价就是服务器侧的出口流量加大,增加成本。(TCP 优化的本质就是用带宽换用户体验感)
(22)服务器怎么判断客户端断开了连接?
- 检测连接是否丢失的方法大致有两种:keepalive和heart-beat
- (tcp内部机制)采用keepalive,它会先要求此连接一定时间没有活动(一般是几个小时),然后发出数据段,经过多次尝试后(每次尝试之间也有时间间隔),如果仍没有响应,则判断连接中断。可想而知,整个周期需要很长的时间。
- (应用层实现)一个简单的heart-beat实现一般测试连接是否中断采用的时间间隔都比较短,可以很快的决定连接是否中断。并且,由于是在应用层实现,因为可以自行决定当判断连接中断后应该采取的行为,而keepalive在判断连接失败后只会将连接丢弃。
(23)端到端,点到点的区别?
-
端到端通信是针对传输层来说的,传输层为网络中的主机提供端到端的通信。因为无论tcp还是udp协议,都要负责把上层交付的数据从发送端传输到接收端,不论其中间跨越多少节点。只不过tcp比较可靠而udp不可靠而已。所以称之为端到端,也就是从发送端到接收端。
它是一个网络连接,指的是在数据传输之前,在发送端与接收端之间(忽略中间有多少设备)为数据的传输建立一条链路,链路建立以后,发送端就可以发送数据,知道数据发送完毕,接收端确认接收成功。 也就是说在数据传输之前,先为数据的传输开辟一条通道,然后在进行传输。从发送端发出数据到接收端接收完毕,结束。
端到端通信建立在点到点通信的基础之上,它是由一段段的点到点通信信道构成的,是比点到点通信更高一级的通信方式,完成应用程序(进程)之间的通信。
端到端的优点:
链路建立之后,发送端知道接收端一定能收到,而且经过中间交换设备时不需要进行存储转发,因此传输延迟小。
端到端传输的缺点:
(1)直到接收端收到数据为止,发送端的设备一直要参与传输。如果整个传输的延迟很长,那么对发送端的设备造成很大的浪费。
(2)如果接收设备关机或故障,那么端到端传输不可能实现。
-
点到点通信是针对数据链路层或网络层来说的,因为数据链路层只负责直接相连的两个节点之间的通信,一个节点的数据链路层接受ip层数据并封装之后,就把数据帧从链路上发送到与其相邻的下一个节点。 点对点是基于MAC地址和或者IP地址,是指一个设备发数据给与该这边直接连接的其他设备,这台设备又在合适的时候将数据传递给与它相连的下一个设备,通过一台一台直接相连的设备把数据传递到接收端。
直接相连的节点对等实体的通信叫点到点通信。它只提供一台机器到另一台机器之间的通信,不会涉及到程序或进程的概念。同时点到点通信并不能保证数据传输的可靠性,也不能说明源主机与目的主机之间是哪两个进程在通信。
由物理层、数据链路层和网络层组成的通信子网为网络环境中的主机提供点到点的服务
点到点的优点:
(1)发送端设备送出数据后,它的任务已经完成,不需要参与整个传输过程,这样不会浪费发送端设备的资源。
(2)即使接收端设备关机或故障,点到点传输也可以采用存储转发技术进行缓冲。
点到点的缺点:
点到点传输的缺点是发送端发出数据后,不知道接收端能否收到或何时能收到数据。
在一个网络系统的不同分层中,可能用到端到端传输,也可能用到点到点传输。如Internet网,IP及以下各层采用点到点传输,4层以上采用端到端传输。
(24)浏览器从输入 URL 到展现页面的全过程?
- 输入地址
- 2、浏览器查找域名的 IP 地址
- 3、浏览器向 web 服务器发送一个 HTTP 请求
- 4、服务器的永久重定向响应
- 6、服务器处理请求
- 7、服务器返回一个 HTTP 响应
- 8、浏览器显示 HTML
- 9、浏览器发送请求获取嵌入在 HTML 中的资源(如图片、音频、视频、CSS、JS等等)
3.UDP⭐
(1)从系统层面上,UDP 如何保证尽量可靠?🚩🚩🚩
- UDP仅提供了最基本的数据传输功能,至于传输时连接的建立和断开、传输可靠性的保证这些UDP统统不关心,而是把这些问题抛给了UDP上层的应用层程序去处理,自己仅提供传输层协议的最基本功能
- 最简单的方式是在应用层模仿传输层TCP的可靠性传输。下面不考虑拥塞处理,可靠UDP的简单设计。
- 添加seq/ack机制,确保数据发送到对端
- 添加发送和接收缓冲区,主要是用户超时重传。
- 添加超时重传机制。
(2) TCP 和 UDP 的区别,它们的头部结构是什么样的?🚩🚩🚩
区别 | TCP | UDP |
---|---|---|
1.是否有建立连接 | TCP协议是有连接的,意思是开始传输实际数据之前TCP的客户端和服务器端必须通过三次握手建立连接,会话结束之后也要结束连接。 | 而UDP是无连接的 |
2.保证数据按序发送 | TCP协议保证数据按序发送,按序到达,提供超时重传来保证可靠性, | 但是UDP不保证按序到达,甚至不保证到达,只是努力交付,即便是按序发送的序列也不保证按序送到。 |
3.协议首部字节数不同 | TCP协议所需资源多,TCP首部需20个字节(不算可选项) | UDP首部字段只需8个字节 |
4.流量控制和拥塞控制 | TCP有流量控制和拥塞控制 | UDP没有,网络拥堵不会影响发送端的发送速率 |
5.一对一或多对多 | TCP是一对一的连接 | 而UDP则可以支持一对一,多对多,一对多的通信 |
6.面向服务不同 | TCP面向的是字节流的服务 | UDP面向的是报文的服务 |
/*TCP头定义,共20个字节*/
typedef struct _TCP_HEADER {
short m_sSourPort; // 源端口号16bit
short m_sDestPort; // 目的端口号16bit
unsigned int m_uiSequNum; // 序列号32bit
unsigned int m_uiAcknowledgeNum; // 确认号32bit
short m_sHeaderLenAndFlag; // 前4位:TCP头长度;中6位:保留;后6位:标志位
short m_sWindowSize; // 窗口大小16bit
short m_sCheckSum; // 检验和16bit
short m_surgentPointer; // 紧急数据偏移量16bit
}__attribute__((packed))TCP_HEADER, *PTCP_HEADER;
/*
TCP头中的选项定义
kind(8bit)+Length(8bit,整个选项的长度,包含前两部分) +内容(如果有的话)
KIND =
1表示 无操作NOP,无后面的部分
2表示 maximum segment 后面的LENGTH就是maximum segment选项的长度(以byte为单位,1+1+内容部分长度)
3表示 windows scale 后面的LENGTH就是 windows scale选项的长度(以byte为单位,1+1+内容部分长度)
4表示 SACK permitted LENGTH为2,没有内容部分
5表示这是一个SACK包 LENGTH为2,没有内容部分
8表示时间戳,LENGTH为10,含8个字节的时间戳 */
typedef struct _TCP_OPTIONS {
char m_ckind;
char m_cLength;
char m_cContext[32];
}__attribute__((packed))TCP_OPTIONS, *PTCP_OPTIONS;
//UDP头部结构如下
/*UDP头定义,共8个字节*/
typedef struct _UDP_HEADER {
unsigned short m_usSourPort; // 源端口号16bit
unsigned short m_usDestPort; // 目的端口号16bit
unsigned short m_usLength; // 数据包长度16bit
unsigned short m_usCheckSum; // 校验和16bit
}__attribute__((packed))UDP_HEADER, *PUDP_HEADER;
(3)TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?🚩🚩🚩
TCP和UDP协议都是传输层协议。二者的区别主要有:
- 基于连接vs无连接
- TCP是面向连接的协议。
- UDP是无连接的协议。UDP更加适合消息的多播发布,从单个点向多个点传输消息。
- 可靠性
- TCP提供交付保证,传输过程中丢失,将会重发。
- UDP是不可靠的,不提供任何交付保证。(网游和视频的丢包情况)
- 有序性
- TCP保证了消息的有序性,即使到达客户端顺序不同,TCP也会排序。
- UDP不提供有序性保证。
- 数据边界
- TCP不保存数据边界:虽然TCP也将在收集所有字节之后生成一个完整的消息,但是这些信息在传给传输给接受端之前将储存在TCP缓冲区,以确保更好的使用网络带宽
- UDP保证:在UDP中,数据包单独发送的,只有当他们到达时,才会再次集成。包有明确的界限来哪些包已经收到,这意味着在消息发送后,在接收器接口将会有一个读操作,来生成一个完整的消息
- 速度
- TCP速度慢
- UDP速度快。应用在在线视频媒体,电视广播和多人在线游戏
- 发送消耗
- TCP是重量级。
- UDP是轻量级:因为UDP传输的信息中不承担任何间接创造连接,保证交货或秩序的的信息。这也反映在用于报头大小
- 报头大小
- TCP头大,一个TCP数据包报头的大小是20字节:TCP报头中包含序列号,ACK号,数据偏移量,保留,控制位,窗口,紧急指针,可选项,填充项,校验位,源端口和目的端口。
- UDP数据报报头是8个字节:而UDP报头只包含长度,源端口号,目的端口,和校验和
- 拥塞或流控制
- TCP有流量控制:在任何用户数据可以被发送之前,TCP需要三数据包来设置一个套接字连接。TCP处理的可靠性和拥塞控制。
- UDP不能进行流量控制。
- 应用
- 由于TCP提供可靠交付和有序性的保证,它是最适合需要高可靠并且对传输时间要求不高的应用。
- UDP是更适合的应用程序需要快速,高效的传输的应用,如游戏
- UDP是无状态的性质,在服务器端需要对大量客户端产生的少量请求进行应答的应用中是非常有用的
- 在实践中,TCP被用于金融领域,如FIX协议是一种基于TCP的协议,而UDP是大量使用在游戏和娱乐场所。
- 上层使用的协议
- 基于TCP协议的:Telnet,FTP以及SMTP协议。
- 基于UDP协议的:DHCP、DNS、SNMP、TFTP、BOOTP。
4.HTTP与HTTPS
(1)HTTP 和 HTTPS 的区别?🚩
HTTP:是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议。http的连接很简单,是无状态的.
HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。是建立一个信息安全通道,来保证数据传输的安全、确认网站的真实性。
HTTP与HTTPS的区别:
- http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
- HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
- http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
- https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
SSL是一种安全套接层协议,是Web浏览器与Web服务器之间安全交换信息的协议,提供两个基本的安全服务:鉴别与保密。
SSL协议的三个特性:
1、保密:在握手协议中定义了会话密钥后,所有的消息都被加密;
2、鉴别:可选的客户端认证,和强制的服务器端认证;
3、完整性:传送的消息包括消息完整性检查。
(2)HTTP 中的 referer 头的作用?🚩🚩
HTTP Referer是header的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器该网页是从哪个页面链接过来的,服务器因此可以获得一些信息用于处理。
- 防盗链:假如在www.google.com里有一个
www.baidu.com
链接,那么点击进入这个www.baidu.com
,它的header信息里就有:Referer= http://www.google.com - 只允许我本身的网站访问本身的图片服务器,假如域是www.google.com,那么图片服务器每次取到Referer来判断一下域名是不是www.google.com,如果是就继续访问不是就拦截(gitee图床)
- 防止恶意请求:比如静态请求是*.html结尾的,动态请求是*.shtml,那么由此可以这么用,所有的*.shtml请求,必须Referer为我自己的网站。
(3)HTTP 的请求方法有哪些?🚩🚩🚩
- GET: 用于请求访问已经被URI(统一资源标识符)识别的资源,可以通过URL传参给服务器
- POST:用于传输信息给服务器,主要功能与GET方法类似,但一般推荐使用POST方式。
- PUT: 传输文件,报文主体中包含文件内容,保存到对应URI位置。
- DELETE:删除文件,与PUT方法相反,删除对应URI位置的文件。
- HEAD: 获得报文首部,与GET方法类似,只是不返回报文主体,一般用于验证URI是否有效。
OPTIONS:查询相应URI支持的HTTP方法。
(4)HTTP 1.0,1.1,2.0 的主要区别?
- http/1.0:
- 默认不支持长连接,需要设置keep-alive参数指定
- 强缓存expired、协商缓存last-modified\if-modified-since 有一定的缺陷
- http 1.1:
- 默认长连接,http请求可以复用Tcp连接,但是同一时间只能对应一个http请求(http请求在一个Tcp中是串行的)
- 增加了强缓存cache-control、协商缓存etag\if-none-match 是对http/1 缓存的优化
- http/2.0:
- 多路复用,一个Tcp中多个http请求是并行的 (雪碧图、多域名散列等优化手段http/2中将变得多余)
- 二进制格式编码传输
- 使用HPACK算法做header压缩
(5)HTTP 常见的响应状态码及其含义?🚩🚩🚩🚩
- 1XX : 信息类状态码(表示接收请求状态处理)
- 2XX : 成功状态码(表示请求正常处理完毕)
- 3XX : 重定向(表示需要进行附加操作,已完成请求)
- 4XX : 客户端错误(表示服务器无法处理请求)
- 5XX : 服务器错误状态码(表示服务器处理请求的时候出错)
200 : 从状态码发出的请求被服务器正常处理。
204 : 服务器接收的请求已成功处理,但在返回的响应报文中不含实体的主体部分【即没有内容】。
206 : 部分的内容(如:客户端进行了范围请求,但是服务器成功执行了这部分的干请求)。
301 : 跳转,代表永久性重定向(请求的资源已被分配了新的URI,以后已使用资源,现在设置了URI)。
302 : 临时性重定向(请求的资源已经分配了新的URI,希望用户本次能够使用新的URI来进行访问)。
303 : 由于请求对应的资源存在的另一个URI(因使用get方法,定向获取请求的资源)。
304 : 客户端发送附带条件的请求时,服务器端允许请求访问资源,但因发生请求未满足条件的情况后,直接返回了304
307 : 临时重定向【该状态码与302有着相同的含义】。
400 : 请求报文中存在语法错误(当错误方式时,需修改请求的内容后,再次发送请求)。
401 : 发送的请求需要有通过HTTP认证的认证信息。
403 : 对请求资源的访问被服务器拒绝了。
404 : 服务器上无法找到请求的资源。
500 : 服务器端在执行请求时发生了错误。
503 : 服务器暂时处于超负载或者是正在进行停机维护,现在无法处理请求。
(6)说说 GET请求和 POST 请求的区别?🚩🚩🚩🚩
- GET请求在URL中传送的参数是有长度限制的,而POST没有。
- GET参数通过URL传递,POST放在Request body中
- GET请求只能进行url编码,而POST支持多种编码方式
- GET产生的URL地址可以被Bookmark,而POST不可以
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留
- GET请求会被浏览器主动cache,而POST不会,除非手动设置
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息
- GET在浏览器回退时是无害的,而POST会再次提交请求
(7)Cookie 和 Session 的关系和区别是什么?🚩🚩🚩
Cookie与Session都是会话的一种方式。它们的典型使用场景比如“购物车”,当你点击下单按钮时,服务端并不清楚具体用户的具体操作,为了标识并跟踪该用户,了解购物车中有几样物品,服务端通过为该用户创建Cookie/Session来获取这些信息。
- cookie数据存放在客户的浏览器上,session数据放在服务器上
- cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗 考虑到安全应当使用session
- session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能 考虑到减轻服务器性能方面,应当使用COOKIE
- 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie
(8)HTTPS 的加密与认证过程?🚩
加密过程:
- 客户端在浏览器中输入一个https网址,然后连接到server的443端口
- 采用https协议的server必须有一套数字证书(一套公钥和密钥)
- 首先server将证书\公钥传送到客户端
- 客户端解析证书,验证成功则生成一个随机数\私钥,并用证书将该随机数加密后传回server,
- server用密钥解密后获得这个随机值,然后将要传输的信息和私钥通过某种算法混合在一起(加密)传到客户端
- 客户端用之前的生成的随机数(私钥)解密服务器端传来的信息。
认证过程:
- 首先浏览器会从内置的证书列表中索引,找到服务器下发证书对应的机构
- 如果没有找到,此时就会提示用户该证书是不是由权威机构颁发,是不可信任的。
- 如果查到了对应的机构,则取出该机构颁发的公钥。
- 用机构的证书公钥解密得到证书的内容和证书签名,内容包括网站的网址、网站的公钥、证书的有效期等。
- 浏览器会先验证证书签名的合法性。签名通过后,浏览器验证证书记录的网址是否和当前网址是一致的,不一致会提示用户。如果网址一致会检查证书有效期,证书过期了也会提示用户。这些都通过认证时,浏览器就可以安全使用证书中的网站公钥了。
五、数据库
1.SQL
介绍一下数据库分页
介绍一下SQL中的聚合函数
表跟表是怎么关联的?
说一说你对外连接的了解?
说一说数据库的左连接和右连接?
SQL中怎么将行转成列?
谈谈你对SQL注入的理解?
将一张表的部分数据更新到另一张表,该如何操作呢?
WHERE和HAVING有什么区别?
WHERE和HAVING有什么区别?
2.索引
说一说你对MySQL索引的理解?
索引有哪几种?
如何创建及保存MySQL的索引?
MySQL怎么判断要不要加索引?
只要创建了索引,就一定会走索引吗?
如何判断数据库的索引有没有生效?
如何评估一个索引创建的是否合理?
索引是越多越好吗?
数据库索引失效了怎么办?
所有的字段都适合创建索引吗?
说一说索引的实现原理?
介绍一下数据库索引的重构过程?
MySQL的索引为什么用B+树?
联合索引的存储结构是什么,它的有效方式是什么?
MySQL的Hash索引和B树索引有什么区别?
聚簇索引和非聚簇索引有什么区别?
什么是联合索引?
select in语句中如何使用索引?
模糊查询语句中如何使用索引?
3.事务
说一说你对数据库事务的了解?
事务有哪几种类型,它们之间有什么区别?
MySQL的ACID特性分别是怎么实现的?
谈谈MySQL的事务隔离级别?
MySQL的事务隔离级别是怎么实现的?
事务可以嵌套吗?
如何实现可重复读?
如何解决幻读问题?
MySQL事务如何回滚?
4.锁
了解数据库的锁吗?
介绍一下间隙锁?
InnoDB中行级锁是怎么实现的?
数据库在什么情况下会发生死锁?
说说数据库死锁的解决办法
5.优化
说一说你对数据库优化的理解?
该如何优化MySQL的查询?
怎样插入数据才能更高效?
表中包含几千万条数据该怎么办?
MySQL的慢查询优化有了解吗?
说一说你对explain的了解?
explain关注什么?
6.其他
介绍一下数据库设计的三大范式?
说一说你对MySQL引擎的了解?
说一说你对redo log、undo log、binlog的了解?
谈谈你对MVCC的了解?
MySQL主从同步是如何实现的?
六、设计模式
1.单例模式
(1)说说什么是单例设计模式,如何实现?
-
单例模式定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
那么我们就必须保证:
(1)该类不能被复制。
(2)该类不能被公开的创造。
那么对于C++来说,它的构造函数,拷贝构造函数和赋值函数都不能被公开调用。
-
单例模式实现方式
单例模式通常有两种模式,分别为懒汉式单例和饿汉式单例。两种模式实现方式分别如下:
(1)懒汉式设计模式实现方式(2种)
a. 静态指针 + 用到时初始化
b. 局部静态变量
(2)饿汉式设计模式(2种)
a. 直接定义静态对象
b. 静态指针 + 类外初始化时new空间实现
(2)简述单例设计模式的懒汉式和饿汉式,如何保证线程安全?
2.工厂模式
(1)请说说工厂设计模式,如何实现,以及它的优点?
-
工厂设计模式的定义
定义一个创建对象的接口,让子类决定实例化哪个类,而对象的创建统一交由工厂去生产,有良好的封装性,既做到了解耦,也保证了最少知识原则。
-
工厂设计模式分类
工厂模式属于创建型模式,大致可以分为三类,简单工厂模式、工厂方法模式、抽象工厂模式。听上去差不多,都是工厂模式。下面一个个介绍:
(1)简单工厂模式
它的主要特点是需要在工厂类中做判断,从而创造相应的产品。当增加新的产品时,就需要修改工厂类。
有一家生产处理器核的厂家,它只有一个工厂,能够生产两种型号的处理器核。客户需要什么样的处理器核,一定要显示地告诉生产工厂。下面给出一种实现方案
简单工厂模式可以根据需求,动态生成使用者所需类的对象,而使用者不用去知道怎么创建对象,使得各个模块各司其职,降低了系统的耦合性。
**缺点:**就是要增加新的核类型时,就需要修改工厂类。这就违反了开放封闭原则:软件实体(类、模块、函数)可以扩展,但是不可修改。
(2)工厂方法模式
所谓工厂方法模式,是指定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。
**举例:**这家生产处理器核的产家赚了不少钱,于是决定再开设一个工厂专门用来生产B型号的单核,而原来的工厂专门用来生产A型号的单核。这时,客户要做的是找好工厂,比如要A型号的核,就找A工厂要;否则找B工厂要,不再需要告诉工厂具体要什么型号的处理器核了。下面给出一个实现方案:
优点: 扩展性好,符合了开闭原则,新增一种产品时,只需增加改对应的产品类和对应的工厂子类即可。
**缺点:**每增加一种产品,就需要增加一个对象的工厂。如果这家公司发展迅速,推出了很多新的处理器核,那么就要开设相应的新工厂。在C++实现中,就是要定义一个个的工厂类。显然,相比简单工厂模式,工厂方法模式需要更多的类定义。
(3)抽象工厂模式
**举例:**这家公司的技术不断进步,不仅可以生产单核处理器,也能生产多核处理器。现在简单工厂模式和工厂方法模式都鞭长莫及。抽象工厂模式登场了。它的定义为提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。具体这样应用,这家公司还是开设两个工厂,一个专门用来生产A型号的单核多核处理器,而另一个工厂专门用来生产B型号的单核多核处理器,下面给出实现的代码:
优点: 工厂抽象类创建了多个类型的产品,当有需求时,可以创建相关产品子类和子工厂类来获取。
缺点: 扩展新种类产品时困难。抽象工厂模式需要我们在工厂抽象类中提前确定了可能需要的产品种类,以满足不同型号的多种产品的需求。但是如果我们需要的产品种类并没有在工厂抽象类中提前确定,那我们就需要去修改工厂抽象类了,而一旦修改了工厂抽象类,那么所有的工厂子类也需要修改,这样显然扩展不方便。
3.装饰器模式
(1)请说说装饰器计模式,以及它的优缺点?
-
装饰器计模式的定义
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。
-
优点
(1)装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用;
(2)通过使用不用装饰类及这些装饰类的排列组合,可以实现不同效果;
(3)装饰器模式完全遵守开闭原则。
-
缺点
装饰模式会增加许多子类,过度使用会增加程序得复杂性。
-
装饰模式的结构与实现
通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰模式的目标。下面来分析其基本结构和实现方法。
装饰模式主要包含以下角色:
(1)抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
(2)具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
(3)抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
(4)具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
装饰模式的结构图如下图所示:
4.观察者模式
请观察者设计模式,如何实现?
七、面试者提问
1.职位的由来?
- 我所应聘的职位是咱们公司现有职位还是新职位?如果是一个新职位,咱们公司是出于何种考虑设立这样一个职位的呢?如果是现有职位,前任雇员因为什么原因离开的?
- 前任有哪些做得好的地方,咱们希望接任者继续保持?有哪些希望改进的地方?
点评:了解职位由来是做好该职位的前提,务必要了解。此外,建议用“咱们公司”来代替“贵公司”的说法,以拉近彼此距离。
2.面临的挑战?
- 您觉得市面上哪些成功的公司可以为我们所借鉴?
- 咱们团队准备怎样迎接今年市场的挑战?
3.如何考评?
- 如果我得到这份工作,我如何能在绩效评估时评为最佳员工?你们希望下一年我在这个岗位上取得哪些关键成就?
- 我的试用期内,我做到哪几件事会使得你们认为聘用我是一个很棒的决定?
- 我未来老板如何对我进行绩效考评?我应该如何从绩效考评过程中学会为公司创造最大价值?
点评:详细了解考核职位的方式,才是面试官最想听到的问题。它不仅体现了你对自己认真负责的态度,也让招聘方感受到了你严谨的思维方式。
4.关于您?
- 您能站在过来人的角度给我些关于如何在这个行业持续发展的建议吗?
- 能告诉我您做到今天这个层次的秘诀吗?
点评:面试环节不仅仅是双方交换信息的过程,也是彼此拉近距离的机会。适时与面试官(也可能是你未来的老板)主动示好,会增加面试的印象分。
5.未来的憧憬?
- 咱们公司一年后将取得什么样的成就?五年后呢?
- 您觉得市面上哪些成功的公司可以为我们所借鉴?
点评:关注公司的未来,也就是关心自己的未来。职业目标的设立不能只是一句空话,要借助平台的未来逐步实现。最后一定要了解一下公司或者部门未来的规划