-
C++11
-
智能指针&shared_ptr 底层实现
unique_ptr: 管理的资源唯一的属于一个对象,但是支持将资源移动给其他unique_ptr对象。当拥有所有权的unique_ptr对象析构时,资源即被释放。
shared_ptr: 管理的资源被多个对象共享,内部采用引用计数跟踪所有者的个数。当最后一个所有者被析构时,资源即被释放。
weak_ptr: 与shared_ptr配合使用,虽然能访问资源但却不享有资源的所有权,不影响资源的引用计数。有可能资源已被释放,但weak_ptr仍然存在。因此每次访问资源时都需要判断资源是否有效。
shared_ptr内部是使用引用计数来记录托管指针被引用的次数,当托管指针的引用计数为0时会释放托管的内存
-
什么情况引用增加?
每个shared_ptr都会记录有多少个其他的shared_ptr指向相同的对象
auto p6 = make_shared<int> (200);
目前p6所指向的对象只有p6一个引用者auto p7(p6);
智能指针定义的初始化,p7和p6指向了相同的对象,此对象目前有两个引用者在如下情况下,所有指向这个对象的shared_ptr引用计数都会增加1:
-
move时增加吗?
如果**
sp3 =move(sp2)
****,** 相当于下图,sp2将对A(2)的控制权给了sp3,sp2不再指向A(2).如果**
sp3 =sp2
****,** 相当于下图,sp3也指向A(2). -
循环引用怎么办?
双方的 shared_ptr 强引用数量不会为0,所以不会自动释放内存,产生了内存泄漏。
- 使用weak_ptr
- weak_ptr 会对一个对象产生弱引用
- weak_ptr 可以指向对象解决 shared_ptr 的循环引用问题
- 使用weak_ptr
-
weak_ptr:
-
C++有什么机制防止内存泄漏。
智能指针,讲了shared_ptr、unique_ptr和weak_ptr的原理
-
-
8.右值引用的作用,移动构造函数如何实现
移动构造函数是一个特殊的构造函数,它能够从一个右值引用(rvalue reference)创建新的对象,而无需进行深拷贝(deep copy)
- std::move 和 std::forward 做了什么?
-
-
内存与底层相关
-
1.int,long在32位,64位中的长度
除了linux64是8,余下都是4
-
2.指针占几字节
32位4个字节 64位8个字节
-
5.堆和栈,分别存些什么,栈中存放函数中哪些变量,函数参数的入栈顺序
- stack段(栈空间):主要用于函数调用时存储临时变量的,这部分的内存是自动分配,自动释放的
- heap段(堆空间):主要用于动态分配,C语言中malloc和free操作堆内存
- 入栈顺序 右向左
-
内存对齐,作用,除了减少cpu访问次数还有吗?
一、硬件原因:加快CPU访问的速度 由于每一个数据都是对齐好的,CPU可以一次就能够将数据读取完成,虽然会有一些内存碎片,但从整个内存的大小来说,都不算什么,可以说是用空间换取时间的做法。 二、平台原因: 不是所有的硬件平台都可以访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些类型的数据,否则抛出硬件异常。
-
内存泄露有了解吗?
- 原因
- 指针重新赋值
- 错误的内存释放
- 返回值的不正确处理
- 内存分配后忘记free
- 原因
-
每个内存分配函数都应该有一个 free 函数与之对应,alloca 函数除外。 每次分配内存之后都应该及时进行初始化,可以结合 memset 函数进行初始化,calloc 函数除外。 每当向指针写入值时,都要确保对可用字节数和所写入的字节数进行交叉核对。 在对指针赋值前,一定要确保没有内存位置会变为孤立的。 每当释放结构化的元素(而该元素又包含指向态分配的内存位置的指针)时,都应先遍历子内存位置并从那里开始释放,然后再遍历回父节点。 始终正确处理返回动态分配的内存引用的函数返回值。
-
如果程序关闭了内存还泄露吗?
对于现代操作系统,泄露的内存会被操作系统自动释放,叫内存自动回收,否则的话依旧是泄漏的
-
那为什么程序员要手动释放内存呢?
原因1:如果程序存在内存泄漏,但恰好运行的操作系统可以帮你自动释放,那么短时间运行没问题。但是,如果移植到另一个没有内存自动回收功能的操作系统,怎么办?
原因2:大多数程序是服务端的守护进程,是一直运行的,如果存在内存泄漏,那么经过长时间的累计,会造成严重问题,程序会崩溃,操作系统的性能和稳定性也会受到很大影响。
-
-
**6.new除了分配内存还有什么用法,new重载有什么作用 **
-
C++类的大小 有虚函数的类呢?
-
内存布局
- stack段(栈空间):主要用于函数调用时存储临时变量的,这部分的内存是自动分配,自动释放的
- heap段(堆空间):主要用于动态分配,C语言中malloc和free操作堆内存
- bss段:存储未被初始化的全局变量,和data段一样都属于静态分配,在编译阶段就确定了大小,不释放
- data段:存储已被初始化的全局变量、常量
- text段:存储程序的二进制指令,即程序源码编译后的二进制代码
-
malloc 和 new 区别
1、属性 new和delete是C++关键字,需要编译器支持;malloc和free是库函数,需要头文件支持。
2、参数 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
3、返回类型 new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回**void * **,需要通过强制类型转换将void*指针转换成我们需要的类型。
4、自定义类型 new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
5、重载 C++允许自定义operator new 和 operator delete 函数控制动态内存的分配。
6、内存区域 new做两件事:分配内存和调用类的构造函数,delete是:调用类的析构函数和释放内存。而malloc和free只是分配和释放内存。
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
7、分配失败 new内存分配失败时,会抛出bac_alloc异常(要用try-catch)。malloc分配内存失败时返回NULL。
8、内存泄漏 内存泄漏对于new和malloc都能检测出来,而new可以指明是哪个文件的哪一行,malloc确不可以。
-
malloc 底层实现
-
malloc 申请内存一定是 size 大小吗?
不一定
在每个 arena 中,最基本的内存分配的单位是 malloc_chunk,我们简称 chunk。
glibc 会将相似大小的空闲内存块 chunk 都串起来。这样等下次用户再来分配的时候,先找到链表,然后就可以从链表中取下一个元素快速分配。这样的一个链表被称为一个 bin。
用户要分配内存的时候,malloc 函数就可以根据其大小,从合适的 bins 中查找合适的 chunk。
- 假如用户要申请 30 字节的内存,那就直接找到 32 字节这个 bin 链表,从链表头部摘下来一个 chunk 直接用。
- 假如用户要申请 500 字节的内存,那就找到 512 字节的 bin 链表,摘下来一个 chunk 使用
-
深拷贝和浅拷贝
-
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存(分支)。
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。 如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
-
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象,是“值”而不是“引用”(不是分支)
拷贝第一层级的对象属性或数组元素 递归拷贝所有层级的对象属性和数组元素 深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
-
-
函数调用参数传递时底层
-
函数调用中参数传递有传值、传指针和传参,它们有什么区别。
- 指针参数传递: 指针参数传递本质上是值传递,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。
- 引用参数传递 引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。。
从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。
-
STL
-
12.STL有哪些容器,map的类型,区别
-
效率
-
顺序容器
容器并非排序的,元素的插入位置同元素的值无关。包含vector、deque、list,具体实现原理如下:
-
(1)vector 头文件
动态数组。元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
-
vector 底层实现?
连续的线性内存空间,三个迭代器
-
vector 是类对象扩容时发生什么?没有构造函数的类型呢?
vector 容器扩容的过程需要经历以下 3 步:
- 完全弃用现有的内存空间,重新申请更大的内存空间;
- 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
- 最后将旧的内存空间释放。
-
vector的扩容机制优化方法
reverse() -
(2)deque 头文件
双向队列。元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于vector)。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
deque 是由一段一段的定量的连续空间构成。一旦有必要在 deque 前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在 deque 的头端或者尾端。Deque 最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。
-
(3)list 头文件
双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。
-
vector与list区别
(1)vector为存储的对象分配一块连续的地址空间 ,随机访问效率很高。但是 插入和删除需要移动大量的数据,效率较低。尤其当vector中存储 的对象较大,或者构造函数复杂,则在对现有的元素进行拷贝的时候会执行拷贝构造函数。 (2)list中的对象是离散的,随机访问需要遍历整个链表, 访问效率比vector低。但是在list中插入元素,尤其在首尾 插入,效率很高,只需要改变元素的指针。 (3)vector是单向的,而list是双向的;
(4)vector中的iterator在使用后就释放了,但是链表list不同,它的迭代器在使用后还可以继续用;链表特有的;
-
-
关联式容器
元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现。包含set、multiset、map、multimap,具体实现原理如下:
-
(1)set/multiset 头文件
set 即集合。set中不允许相同元素,multiset中允许存在相同元素。
-
(2)map/multimap 头文件
map与set的不同在于map中存放的元素有且仅有两个成员变,一个名为first,另一个名为second, map根据first值对元素从小到大排序,并可快速地根据first来检索元素。
注意:map同multimap的不同在于是否允许相同first值的元素。
-
map实现原理
map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。
-
unordered_map 怎么实现?
哈希表
-
哈希冲突怎么解决?
**开放地址法(也叫开放寻址法):**实际上就是当需要存储值时,对Key哈希之后,发现这个地址已经有值了,这时该怎么办?不能放在这个地址,不然之前的映射会被覆盖。这时对计算出来的地址进行一个探测再哈希,比如往后移动一个地址,如果没人占用,就用这个地址。如果超过最大长度,则可以对总长度取余。这里移动的地址是产生冲突时的增列序量。
**再哈希法:**在产生冲突之后,使用关键字的其他部分继续计算地址,如果还是有冲突,则继续使用其他部分再计算地址。这种方式的缺点是时间增加了。
**链地址法:**链地址法其实就是对Key通过哈希之后落在同一个地址上的值,做一个链表。其实在很多高级语言的实现当中,也是使用这种方式处理冲突的,我们会在后面着重学习这种方式。
**建立一个公共溢出区:**这种方式是建立一个公共溢出区,当地址存在冲突时,把新的地址放在公共溢出区里。
-
-
-
容器适配器
封装了一些基本的容器,使之具备了新的函数功能,比如把deque封装一下变为一个具有stack功能的数据结构。这新得到的数据结构就叫适配器。包含stack,queue,priority_queue,具体实现原理如下:
-
(1)stack 头文件
栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项)。后进先出。
-
(2)queue 头文件
队列。插入只可以在尾部进行,删除、检索和修改只允许从头部进行。先进先出。
-
(3)priority_queue 头文件
优先级队列。内部维持某种有序,然后确保优先级最高的元素总是位于头部。最高优先级元素总是第一个出列
-
-
-
vector的push_back()和emplace_back()的区别
emplace_back()
函数中,是支持直接将构造函数所需的参数传递过去,然后构建一个新的对象出来,然后填充到容器尾部的
-
-
数据结构
-
平衡二叉树 AVL
- 非叶子节点只能允许最多两个子节点存在。
- 每一个非叶子节点数据分布规则为左边的子节点小当前节点的值,右边的子节点大于当前节点的值(这里值是基于自己的算法规则而定的,比如 hash 值)。
总结平衡二叉树特点:
插入、删除和查找等操作的平均时间复杂度为O(log2n)
- 非叶子节点最多拥有两个子节点;
- 非叶子节值大于左边子节点、小于右边子节点;
- 树的左右两边的层级数相差不会大于 1;
- 没有值相等重复的节点;
-
调整
-
代码
-
public class AVLNode {
public int data;//保存节点数据
public int depth;//保存节点深度
public int balance;//是否平衡
public AVLNode parent;//指向父节点
public AVLNode left;//指向左子树
public AVLNode right;//指向右子树
public AVLNode(int data){
this.data = data;
depth = 1;
balance = 0;
left = null;
right = null;
}
}
public void insert(AVLNode root, int data){
//如果说插入的数据小于根节点,往左边递归插入
if (data < root.data){
if (root.left != null){
insert(root.left, data);
}else {
root.left = new AVLNode(data);
root.left.parent = root;
}
}
//如果说插入的数据小于根节点,往左边递归插入
else {
if (root.right != null){
insert(root.right, data);
}else {
root.right = new AVLNode(data);
root.right.parent = root;
}
}
//插入之后,计算平衡银子
root.balance = calcBalance(root);
// 左子树高,应该右旋
if (root.balance >= 2){
// 右孙高,先左旋
if (root.left.balance == -1){
left_rotate(root.left);
}
right_rotate(root);
}
// 右子树高,左旋
if (root.balance <= -2){
// 左孙高,先右旋
if (root.right.balance == 1){
right_rotate(root.right);
}
left_rotate(root);
}
//调整之后,重新计算平衡因子和树的深度
root.balance = calcBalance(root);
root.depth = calcDepth(root);
}
// 右旋
private void right_rotate(AVLNode p){
// 一次旋转涉及到的结点包括祖父,父亲,右儿子
AVLNode pParent = p.parent;
AVLNode pLeftSon = p.left;
AVLNode pRightGrandSon = pLeftSon.right;
// 左子变父
pLeftSon.parent = pParent;
if (pParent != null){
if (p == pParent.left){
pParent.left = pLeftSon;
}else if (p == pParent.right){
pParent.right = pLeftSon;
}
}
pLeftSon.right = p;
p.parent = pLeftSon;
// 右孙变左孙
p.left = pRightGrandSon;
if (pRightGrandSon != null){
pRightGrandSon.parent = p;
}
p.depth = calcDepth(p);
p.balance = calcBalance(p);
pLeftSon.depth = calcDepth(pLeftSon);
pLeftSon.balance = calcBalance(pLeftSon);
}
private void left_rotate(AVLNode p){
// 一次选择涉及到的结点包括祖父,父亲,左儿子
AVLNode pParent = p.parent;
AVLNode pRightSon = p.right;
AVLNode pLeftGrandSon = pRightSon.left;
// 右子变父
pRightSon.parent = pParent;
if (pParent != null){
if (p == pParent.right){
pParent.right = pRightSon;
}else if (p == pParent.left){
pParent.left = pRightSon;
}
}
pRightSon.left = p;
p.parent = pRightSon;
// 左孙变右孙
p.right = pLeftGrandSon;
if (pLeftGrandSon != null){
pLeftGrandSon.parent = p;
}
p.depth = calcDepth(p);
p.balance = calcBalance(p);
pRightSon.depth = calcDepth(pRightSon);
pRightSon.balance = calcBalance(pRightSon);
}
-
红黑树
红黑树的性质:
1.节点是红色或黑色。
2.根节点是黑色。
3.每个叶子节点都是黑色的空节点(NIL节点)。
4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
-
红黑树和AVL的异同。
1、红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。
2、平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
-
红黑树相比AVL有什么优势。
黑树属于平衡二叉树。它不严格是因为它不是严格控制左、右子树高度或节点数之差小于等于1,但红黑树高度依然是平均log(n),且最坏情况高度不会超过2log(n)。
- 红黑树能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。
- 任何不平衡都会在3次旋转之内解决。这一点是AVL所不具备的。
-
哈希表在什么情况下效率会很低
哈希表扩容时,会把所有元素重新哈希一遍。
-
单向链表怎么找中间位置
这里我们可以创建两个指针均指向链表的头位置,让两个指针一起向前走,一个一次走一步,一个一次走两步。当走两步的指针到达链表末尾时,另一个指针则到达链表的中间位置
-
循环右移链表
先取余,再移
-
有序链表合并
递归
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){
//递归条件
if(l1 == NULL) return l2;
if(l2 == NULL) return l1;
if(l1->val < l2->val){
l1->next = mergeTwoLists(l1->next,l2);
return l1;
}
else{
l2->next = mergeTwoLists(l1,l2->next);
return l2;
}
}
-
哈希表的查找时间复杂度
O(1)
-
常见算法
-
**罗列一下你了解的排序算法并介绍原理。
-
基于比较的排序算法理论最低的时间复杂度是多少
O(nlogn)
-
**什么是稳定的排序和不稳定的排序。**我一开始以为是指时间复杂度的稳定,考官提示我是原数组中有相同元素时的稳定和不稳定,我才答对。
-
快排如何优化?(随机数法,三数取中,加入插入排序,递归变为非递归等能说的也都说了)
-
**有没有用过一些其它的算法比如贪心、动态规划。**我说我以前参加过竞赛,这些算法包括一些图论数论的算法都有一定了解。
-
-
-
网络
-
14.TCP和UDP可以同时使用80端口吗
可以,他们的端口号是独立的
- 浏览器输入网址到呈现网页
- DNS解析——解析域名,获取对应的ip地址
- TCP连接——TCP三次握手
- 浏览器发送http请求
- 服务器处理请求并返回http报文
- 浏览器解析返回的数据并渲染页面
- 断开连接:TCP四次挥手
-
tcp和udp的差异
-
TCP是面向连接的,UDP是无连接的
-
TCP是可靠的,UDP是不可靠的
-
TCP是面向字节流的,UDP是面向数据报文的
-
TCP只支持点对点通信,UDP支持一对一,一对多,多对多
-
TCP报文首部20个字节,UDP首部8个字节
-
TCP有拥塞控制机制,UDP没有
-
TCP协议下双方发送接受缓冲区都有,UDP并无实际意义上的发送缓冲区,但是存在接受缓冲区
-
怎么让UDP可靠?
- 1、添加seq/ack机制,确保数据发送到对端
- 2、添加发送和接收缓冲区,主要是用户超时重传。
- 3、添加超时重传机制。
-
-
-
多态虚函数相关
-
7.析构函数为什么是虚函数,析构函数不是虚函数一定会造成内存泄漏吗
与构造函数不同,vptr已经完成初始化,析构函数可以声明为虚函数,且类有继承时,析构函数常常必须为虚函数。
不一定,但是继承关系下可能会内存泄漏
-
虚指针什么时候指向虚表?
编译期间
-
虚函数多态原理
- 当一个类中出现虚函数或着子类继承了虚函数时,就会在该类中产生一个虚函数表(virtual table),虚函数表实际上是一个函数指针数组(在有的编译器作用下是链表),里面的每一个元素对应指向该类中的某一个虚函数的指针。被该类声明的对象会包含一个虚函数表指针(virtual table pointer),指向该类的虚函数表的地址。
- 虚函数的调用过程: 当一个对象要调用到虚函数时,先将对象内存中的vptr指针(虚函数表指针)指向定义该类的vtbl(虚函数表),vtbl再寻找里面的指针指向想要调用的虚函数,从而完成虚函数的调用。
-
虚继承
(2)虚拟继承会给继承类添加一个虚基类指针(virtual base ptr 简称vbptr),其位于类虚函数指针后面,成员变量前面,若基类没有虚函数,则vbptr其位于继承类的最前端
-
模板偏特化
-
-
const相关
-
3.宏和const
(1) 编译器处理方式不同
define宏是在预处理阶段展开。
const常量是编译运行阶段使用。
(2) 类型和安全检查不同
define宏没有类型,不做任何类型检查,仅仅是展开。
const常量有具体的类型,在编译阶段会执行类型检查。
(3) 存储方式不同
define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。
const常量会在内存中分配(可以是堆中也可以是栈中)。
(4)const 可以节省空间,避免不必要的内存分配
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而 #define定义的常量在内存中有若干个拷贝。
(5) 提高了效率
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高
-
-
操作系统
-
16.进程和线程的区别,多线程有多少栈,无锁多线程如何协作
-
区别
- 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
- 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
- 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
- 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
- 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
-
多线程 栈
每个线程一个栈,每个进程一个堆
-
无锁多线程如何协作
-
-
线程之间怎么同步。
- 互斥锁(mutex)
- 条件变量(condition):条件变量被用来阻塞一个线程,当条件不满足时,线程会解开互斥锁,并等待条件发生变化。一旦其他线程改变了条件变量,将通知相应的阻塞线程,这些线程重新锁定互斥锁,然后执行后续代码,最后再解开互斥锁。
- 读写锁(reader-writer lock)
- 信号量(semphore)
-
原子操作。
不可中断的一个或者一系列操作, 也就是不会被线程调度机制打断的操作, 运行期间不会有任何的上下文切换(context switch).
-
19.矩形蛋糕有一个矩形空洞,如何一刀切成等量两部分
找两个对角线交点连线
-
虚拟内存,虚拟内存的好处
为进程隐藏了物理内存这一概念,为进程提供了更加简洁和易用的接口。这个中间层提供了三个重要的能力:
- 高效使用内存:VM将主存看成是存储在磁盘上的地址空间的高速缓存,主存中保存热的数据,根据需要在磁盘和主存之间传送数据;
- 简化内存管理:VM为每个进程提供了一致的地址空间,从而简化了链接、加载、内存共享等过程;
- 内存保护:保护每个进程的地址空间不被其他进程破坏。 下文详述这虚拟内存三项能力。
-
9.有了解过LINUX操作系统吗?它有什么优点? 10.Linux下I/O多路复用机制有哪些?区别是什么?(epoll,poll,select它们之间的区别)
-
-
其他
-
9.static_cast和dynamic_cast的区别,后者转化指针或引用失败时返回什么
- static_cast<类型>(变量表达式)
用于类层次结构中基类和派生类之间引用或指针的转换。
进行上行转换(把派生类的指针或引用转换成基类表示)是安全的。
进行下行转换(把基类的指针或引用转换成派生类表示),由于没有动态类型检查,不安全。
用于基本数据类型之间的转换
把空指针转换成目标类型的空指针
把任何类型的表达式转换成void类型
- dynamic_cast
用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)。
dynamic_cast< type_id >(expression)
type_id 必须是类的指针、类的引用或者void*。
主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
dynamic_cast只能用于含有虚函数的类;
dynamic_cast会先检查是否能转换成功,如果能则转换,不能则返回0。
-
10.C++编译链接的过程,链接的方式,多个程序共享一个动态库,其运行时加载几次,占用谁的内存
编译过程分为四个过程:编译(编译预处理、编译、优化),汇编,链接。
编译预处理:处理以 # 开头的指令;
编译、优化:将源码 .cpp 文件翻译成 .s 汇编代码;
汇编:将汇编代码 .s 翻译成机器指令 .o 文件;
链接:汇编程序生成的目标文件,即 .o 文件,并不会立即执行,因为可能会出现:.cpp 文件中的函数引用了另一个 .cpp文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序 .exe文件。
链接分为两种:
静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时。
静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中。 静态链接时,按目标文件+引用文件为基准存放,会造成大量的空间浪费。可以理解为a.o和b.o都用到了c.o文件,那么就会将c.o分别与a.o与b.o拷贝到一起,形成a.o、c.o一组,b.o、c.o一组。
动态链接:代码被放到动态链接库或共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间。 动态链接时,多个程序在执行时共享同一份副本。程序运行时a.o有用到c.o的时候,将c.o加入到内存,下次b.o要用到c.o直接从内存中链接。
二者的优缺点:
静态链接:浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难);优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容。 动态链接:节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要链接,相比静态链接会有一定的性能损失。
-
11.两个cpp中都定义int a会不会报错
不会
-
内联和宏定义在使用上的区别
相同点:
两者都是可以加快程序运行效率,使代码变得更加通用
不同点:
1.内联函数的调用是传参,宏定义只是简单的文本替换
2.内联函数可以在程序运行时调用,宏定义是在程序编译进行
**3.**内联函数有类型检测更加的安全,宏定义没有类型检测
4.内联函数在运行时可调式,宏定义不可以
5.内联函数可以访问类的成员变量,宏不可以
**6.**类中的成员函数是默认的内联函数
-
内联函数和普通调用比有什么优势
优点
- 当函数体较小的时候,内联可以令目标代码更加高效。inline函数在被调用处进行代码展开,省去了调用普通函数会产生的参数压栈、栈帧开辟与回收、结果返回等步骤,以此来提高程序运行速度。
缺点
- inline函数是以代码膨胀为代价来消除函数调用带来的开销的,意味着消耗更多的内存空间。
- inline函数在变更实现代码后需要重新链接。非内联函数则不需要。
- 内联是不能完全由程序控制。内联函数只是对编译器的建议,最终由编译器决定是否内联。
-
6.给你5000W数量级的数据,如何快速定位到我要找到的数据?(哈希表,B+树)
-
8.C++中提供的强制类型转换有哪些,有什么区别?
一、static_cast转换
a、用于类层次结构中基类和派生类之间指针或引用的转换
上行转换(派生类---->基类)是安全的;
下行转换(基类---->派生类)由于没有动态类型检查,所以是不安全的。
b、用于基本数据类型之间的转换,如把int转换为char,这种带来安全性问题由程序员来保证
c、把空指针转换成目标类型的空指针
d、把任何类型的表达式转为void类型
3.使用特点
a、主要执行非多态的转换操作,用于代替C中通常的转换操作
b、隐式转换都建议使用static_cast进行标明和替换
二、dynamic_cast转换
只有在派生类之间转换时才使用dynamic_cast,type-id必须是类指针,类引用或者void*。
a、基类必须要有虚函数,因为dynamic_cast是运行时类型检查,需要运行时类型信息,而这个信息是存储在类的虚函数表中,只有一个类定义了虚函数,才会有虚函数表(如果一个类没有虚函数,那么一般意义上,这个类的设计者也不想它成为一个基类)。
b、对于下行转换,dynamic_cast是安全的(当类型不一致时,转换过来的是空指针),而static_cast是不安全的(当类型不一致时,转换过来的是错误意义的指针,可能造成踩内存,非法访问等各种问题)
c、dynamic_cast还可以进行交叉转换
三、const_cast转换
a、常量指针转换为非常量指针,并且仍然指向原来的对象
b、常量引用被转换为非常量引用,并且仍然指向原来的对象
四、reinterpret_cast转换
a、reinterpret_cast是从底层对数据进行重新解释,依赖具体的平台,可移植性差
b、reinterpret_cast可以将整型转换为指针,也可以把指针转换为数组
c、reinterpret_cast可以在指针和引用里进行肆无忌惮的转换
-
怎么判断一个点在矩形内
(AB X AE ) * (CD X CE) >= 0 && (DA X DE ) * (BC X BE) >= 0 。
-
个人记录向,从wolai里面粘的,可能格式有点混乱,如有错误欢迎大佬评论区指正