【实习面经】阿里+360+字节
阿里云二面:
内存泄漏:
内存泄漏一般为在代码中申请了一块内存后由于各种原因在使用完成后没去释放这块内存,操作系统这个时候认为这块内存还在被应用程序使用(因为程序没去释放),于是这一块内存对于程序来说已经没有用了(也不会去用),对于系统来说也没有释放出来,这样的一件事情成为内存泄漏。
对于现代的操作系统而言,一个程序是运行在独立的进程空间中的,当这个进程结束后,操作系统将回收这个进程申请的所有内存,也就是说,当进程结束后,该进程泄漏的内存会被回收 (不管你是不是泄漏的都回收了)
至于后果,对于运行在一般用户这边的应用程序来说,由于运行的时间不长,结束后会被操作系统整个回收,一般不会造成不良影响(尽管如此,还是要尽可能做到没有内存泄漏);而对于服务来说,比如跑在服务器上的程序,会长时间长时间的运行,如果有内存泄漏的代码,会在运行中不断积累泄漏的内存,最后占满服务器的所有可用内存,导致宕机。
内存满了后再次申请内存会报错,或者在最后几次申请的时候发生内存溢出。
野指针:
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。
指针变量未初始化
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。如果没有初始化,编译器会报错" 'point' may be uninitializedin the function "。
指针释放后之后未置空
有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是"垃圾"内存。释放后的指针应立即将指针置为NULL,防止产生"野指针"。
多线程和单线程的区别:
什么时候单线程快什么时候多线程快:对于处理时间短的服务或者启动频率高的要用单线程,相反用多线程!
一亿个数用多线程找出其中的质数:一个线程负责一部分数的求解。比如10个线程就同时操作求是否是质数。
推荐我看Unix环境编程和Unix网络编程
360一面:
继承的机制和实际应用场景,
static和const的实际应用场景,
问hash结构,哈希冲突:
键(key)经过hash函数得到的结果作为地址去存放当前的键值对(key-value)(hashmap的存值方式),但是却发现该地址已经有值了,就会产生冲突。这个冲突就是hash冲突了。
换句话说就是:如果两个不同对象的hashCode相同,这种现象称为hash冲突。
解决哈希冲突
有以下的方式可以解决哈希冲突:
开放定址法
再哈希法
链地址法
建立公共溢出区
开放定址法
这种方法的意思是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
线性探测再散列
当发生冲突的时候,顺序的查看下一个单元
二次(平方)探测再散列
当发生冲突的时候,在表的左右进行跳跃式探测
伪随机探测再散列
建立一个伪随机数发生器,并给一个随机数作为起点
再hash法
这种方式是同时构造多个哈希函数,当产生冲突时,计算另一个哈希函数的值。这种方法不易产生聚集,但增加了计算时间。
链地址法
将所有哈希地址相同的都链接在同一个链表中 ,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。hashmap就是用此方法解决冲突的。
建立一个公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
优缺点
开放散列(open hashing)/ 拉链法(针对桶链结构)
优点:
在总数频繁变动的时候可以节省开销,避免了动态调整;
记录存储在节点里,动态分布,避免了指针的开销
删除时候比较方便
缺点:
因为存储是动态的,所以在查询的时候跳转需要更多的时间的开销
在key-value可以预知,以及没有后续增改操作时候,封闭散列性能优于开放散列
不容易序列化
封闭散列(closed hashing)/ 开放定址法
优点:
容易序列化
如果可以预知数据总数,可以创建完美哈希数列
缺点:
存储的记录数目不能超过桶组数,在交互时候会非常麻烦
使用探测序列,计算时间成本过高
删除的时候比较麻烦
字节跳动新业务一面:
C++ map底层实现:
1.vector 底层数据结构为数组 ,支持快速随机访问
2.list 底层数据结构为双向链表,支持快速增删
3.deque 底层数据结构为一个中央控制器和多个缓冲区,支持首尾(中间不能)快速增删,也支持随机访问
deque是一个双端队列(double-ended queue),也是在堆中保存内容的.它的保存形式如下:
[堆1] --> [堆2] -->[堆3] --> ...
每个堆保存好几个元素,然后堆和堆之间有指针指向,看起来像是list和vector的结合品.
4.stack 底层一般用list或deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时
5.queue 底层一般用list或deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时
(stack和queue其实是适配器,而不叫容器,因为是对容器的再封装)
6.priority_queue 的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现
7.set 底层数据结构为红黑树,有序,不重复
8.multiset 底层数据结构为红黑树,有序,可重复
9.map 底层数据结构为红黑树,有序,不重复
10.multimap 底层数据结构为红 黑树,有序,可重复
11.hash_set 底层数据结构为hash表,无序,不重复
12.hash_multiset 底层数据结构为hash表,无序,可重复
13.hash_map 底层数据结构为hash表,无序,不重复
14.hash_multimap 底层数据结构为hash表,无序,可重复
15.unordered_map 与unordered_multimap底层数据结构
而unordered_map与unordered_multimap中key为无序排列,其底层实现为hash table,因此其查找时间复杂度理论上达到了O(n),之所以说理论上是因为在理想无碰撞的情况下,而真实情况未必如此。
16.unordered_set & unordered_multiset
与unordered_map & unordered_multimap相同,其底层实现为hash table;
字节跳动新业务三面:
就绪 运行 阻塞:进程调度:
四种进程间的状态转换:
1)进程的三种基本状态
进程在运行中不断地改变其运行状态。通常,一个进程必须具有以下三种基本状态:
就绪状态:
当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态就称为就绪状态;
执行状态:
当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态;
阻塞状态:
正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而进入阻塞状态。引起进程阻塞的事件有很多种,例如,等待I/O完成、申请缓冲区不能满足、等待信号等。
2)进程三种状态间的转换
一个进程在运行期间,不断地从一种状态转换到另一种状态,它可以多次处于就绪状态和执行状态,也可以多次处于阻塞状态。
A. 就绪—>执行
处于就绪状态的进程,当进程调度程序为之分配好了处理机后,该进程便由就绪状态转换为执行状态;
B. 执行—>就绪
处于执行状态的进程在其执行过程中,因分配给它的一个时间片已经用完而不得不让出处理机,于是进程从执行状态转换为就绪状态;
C. 执行—>阻塞
正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态变成阻塞状态;
D. 阻塞—>就绪
处于阻塞状态的进程,若其等待的事件已经发生,于是进程便从阻塞状态转变为就绪状态。
进程线程区别:线程可以独占内存吗?可以,线程的堆是共享的,栈是独占的
硬链接软链接:
一 建立软链接和硬链接的语法
软链接:ln -s 源文件 目标文件
硬链接:ln 源文件 目标文件
源文件:即你要对谁建立链接
二 什么是软链接和硬链接
1,软链接可以理解成快捷方式。它和windows下的快捷方式的作用是一样的。
2,硬链接等于cp -p 加 同步更新。(相当于给源文件加了一个智能指针,两个指针指向同一个源文件内容)
区别: 软链接文件的大小和创建时间和源文件不同。软链接文件只是维持了从软链接到源文件的指向关系(从jys.soft->jys可以看出),不是源文件的内容,大小不一样容易理解。
硬链接文件和源文件的大小和创建时间一样。硬链接文件的内容和源文件的内容一模一样,相当于copy了一份。
软链接像快捷方式,方便我们打开源文件,这一点在windows中深有体会,那硬链接有哪些应用呢?
在多用户的操作系统里,你写一个脚本,程序等,没有完成,保存后等下次有时间继续写,但是其他用户有可能将你未写完的东西当成垃圾清理掉(这里只是删除了一个指向源文件的指针,还有硬链接指针存在),这时,你对你的程序,脚本等做一个硬链接,利用硬链接的同步更新,就可以防止别人误删你的源文件了。
深拷贝浅拷贝:
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
vector 容器扩容的整个过程,和 realloc() 函数的实现方法类似,大致分为以下 4 个步骤:
- 分配一块大小是当前 vector 容量几倍的新存储空间。注意,多数 STL 版本中的 vector 容器,其容器都会以 2 的倍数增长,也就是说,每次 vector 容器扩容,它们的容量都会提高到之前的 2 倍;
- 将 vector 容器存储的所有元素,依照原有次序从旧的存储空间复制到新的存储空间中;
- 析构掉旧存储空间中存储的所有元素;
- 释放旧的存储空间。
通过以上分析不难看出,vector 容器的扩容过程是非常耗时的,并且当容器进行扩容后,之前和该容器相关的所有指针、迭代器以及引用都会失效。因此在使用 vector 容器过程中,我们应尽量避免执行不必要的扩容操作。
要实现这个目标,可以借助 vector 模板类中提供的 reserve() 成员方法。不过在讲解如何用 reserve() 方法避免 vector 容器进行不必要的扩容操作之前,vector 模板类中还提供有几个和 reserve() 功能类似的成员方法,很容易混淆,这里有必要为读者梳理一下,如表 1 所示。
表 1 vector模板类中功能类似的成员方法 | |
成员方法 | 功能 |
size() | 告诉我们当前 vector 容器中已经存有多少个元素,但仅通过此方法,无法得知 vector 容器有多少存储空间。 |
capacity() | 告诉我们当前 vector 容器总共可以容纳多少个元素。如果想知道当前 vector 容器有多少未被使用的存储空间,可以通过 capacity()-size() 得知。注意,如果 size() 和 capacity() 返回的值相同,则表明当前 vector 容器中没有可用存储空间了,这意味着,下一次向 vector 容器中添加新元素,将导致 vector 容器扩容。 |
resize(n) | 强制 vector 容器必须存储 n 个元素,注意,如果 n 比 size() 的返回值小,则容器尾部多出的元素将会被析构(删除);如果 n 比 size() 大,则 vector 会借助默认构造函数创建出更多的默认值元素,并将它们存储到容器末尾;如果 n 比 capacity() 的返回值还要大,则 vector 会先扩增,在添加一些默认值元素。 |
reserve(n) | 强制 vector 容器的容量至少为 n。注意,如果 n 比当前 vector 容器的容量小,则该方法什么也不会做;反之如果 n 比当前 vector 容器的容量大,则 vector 容器就会扩容。 |
360广告业务部二面
说一下Move函数:
C++11中,std::move存在于<utility>中,std::move函数可以很方便的将左值引用转换为右值引用(左值、右值、左值引用、右值引用等相关介绍可以参看:https://blog.csdn.net/xiaomucgwlmx/article/details/101346463)。实际上,std::move并不可以移动任何东西,唯一的功能就是上边说的将一个左值强制转化为右值引用,然后通过右值引用使用该值。
std::move函数原型如下:
// TEMPLATE FUNCTION move
template<class _Ty> inline
constexpr typename remove_reference<_Ty>::type&&
move(_Ty&& _Arg) _NOEXCEPT
{ // forward _Arg as movable
return (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));
}
这里,函数参数T&&是一个指向模板类型参数的右值引用,通过引用折叠,此参数可以与任何类型的实参匹配(可以传递左值或右值,这是std::move主要使用的两种场景)。
1,引用折叠规则:
X& + & => X&
X&& + & => X&
X& + && => X&
X&& + && => X&&
2,函数模板参数推导规则(右值引用参数部分):
当函数模板的模板参数为T而函数形参为T&&(右值引用)时适用本规则。
若实参为左值 U& ,则模板参数 T 应推导为引用类型 U& 。
(根据引用折叠规则, U& + && => U&, 而T&& ≡ U&,故T ≡ U& )
若实参为右值 U&& ,则模板参数 T 应推导为非引用类型 U 。
(根据引用折叠规则, U或U&& + && => U&&, 而T&& ≡ U&&,故T ≡ U或U&&,这里强制规定T ≡ U )
3,std::remove_reference为C++0x标准库中的元函数,其功能为去除类型中的引用。
std::remove_reference<U&>::type ≡ U
std::remove_reference<U&&>::type ≡ U
std::remove_reference<U>::type ≡ U
4,以下语法形式将把表达式 t 转换为T类型的右值(准确的说是无名右值引用,是右值的一种)
static_cast<T&&>(t)
5,无名的右值引用是右值
具名的右值引用是左值。
源码详细说明:
1,原型定义中的原理实现:
首先,函数参数T&&是一个指向模板类型参数的右值引用,通过引用折叠,此参数可以与任何类型的实参匹配(可以传递左值或右值,这是std::move主要使用的两种场景)。关于引用折叠如下:
公式一)X& &、X&& &、X& &&都折叠成X&,用于处理左值
string s("hello");
std::move(s) => std::move(string& &&) => 折叠后 std::move(string& )
此时:T的类型为string&
typename remove_reference<T>::type为string
整个std::move被实例化如下
string&& move(string& t) //t为左值,移动后不能在使用t
{
//通过static_cast将string&强制转换为string&&
return static_cast<string&&>(t);
}
公式二)X&& &&折叠成X&&,用于处理右值
std::move(string("hello")) => std::move(string&&)
//此时:T的类型为string
// remove_reference<T>::type为string
//整个std::move被实例如下
string&& move(string&& t) //t为右值
{
return static_cast<string&&>(t); //返回一个右值引用
}
简单来说,右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用.
②对于static_cast<>的使用注意:任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。
double d = 1;
void* p = &d;
double *dp = static_cast<double*> p; //正确
const char *cp = "hello";
char *q = static_cast<char*>(cp); //错误:static不能去掉const性质
static_cast<string>(cp); //正确
③对于remove_reference是通过类模板的部分特例化进行实现的,其实现代码如下
//原始的,最通用的版本
template <typename T> struct remove_reference{
typedef T type; //定义T的类型别名为type
};
//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{ typedef T type; }
template <class T> struct remove_reference<T&&> //右值引用
{ typedef T type; }
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a; //使用原版本,
remove_refrence<decltype(i)>::type b; //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type b; //右值引用特例版本
写一个shared_ptr:
字节跳动安全与风控一面:
char* a="aaaa";char a[]="aaa";的区别,转换成二进制后的区别?
两者区别如下:
一. "读" "写" 能力
char *a = "abcd"; 此时"abcd"存放在常量区。通过指针只可以访问字符串常量,而不可以改变它。
而char a[20] = "abcd"; 此时 "abcd"存放在栈。可以通过指针去访问和修改数组内容。
二. 赋值时刻
char *a = "abcd"; 是在编译时就确定了(因为为常量)。
而char a[20] = "abcd"; 在运行时确定
三. 存取效率
char *a = "abcd"; 存于静态存储区。在栈上的数组比指针所指向字符串快。因此慢
而char a[20] = "abcd"; 存于栈上。快
另外注意:
char a[] = "01234",虽然没有指明字符串的长度,但是此时系统已经开好了,就是大小为6-----'0' '1' '2' '3' '4' '5' '\0',(注意strlen(a)是不计'\0')
协程
对操作系统而言,线程是最小的执行单元,进程是最小的资源管理单元。无论是进程还是线程,都是由操作系统所管理的。
线程的状态
线程具有五种状态:初始化、可运行、运行中、阻塞、销毁
线程状态的转化关系
线程之间是如何进行协作的呢?
最经典的例子是生产者/消费者模式,即若干个生产者线程向队列中系欸如数据,若干个消费者线程从队列中消费数据。
生产者/消费者模式的性能问题是什么?
- 涉及到同步锁
- 涉及到线程阻塞状态和可运行状态之间的切换
- 涉及到线程上下文的切换
什么是协程呢?
协程(Coroutines)是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。(没有返回值的函数)
coroutine is suspendable, resumable subroutine.
协程是可暂停和恢复执行的过程(过程就是函数)
常规子程序(函数)和协程的区别
子程序执行完返回把控制权返还给调用这个子程序的上层,让上层继续往下执行,一层套一层,这就是层级调用。
特征:执行完毕才返回
不可中断
协程
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
特征:可以执行到一半先返回
可中断、挂起再次执行
可恢复状态
不同的语言对协程的实现方式多有不同,但是只要能够在单线程里实现协程的中断恢复这两个特征那么就是协程。
中断过程与调用子程序过程相似点是表面的,从本质上讲两者是完全不一样的。
两者的根本区别主要表现在服务时间与服务对象不一样上。首先,调用子程序过程发生的时间是已知和固定的,即在主程序中的调用指令(CALL)执行时发生主程序调用子程序,调用指令所在位置是已知和固定的。而中断过程发生的时间一般的随机的,CPU在执行某一主程序时收到中断源提出的中断申请时,就发生中断过程,而中断申请一般由硬件电路产生,申请提出时间是随机的(软中断发生时间是固定的),也可以说,调用子程序是程序设计者事先安排的,而执行中断服务程序是由系统工作环境随机决定的;其次,子程序完全为主程序服务的,两者属于主从关系,主程序需要子程序时就去调用子程序,并把调用结果带回主程序继续执行。而中断服务程序与主程序两者一般是无关的,不存在谁为谁服务的问题,两者是平行关系;第三,主程序调用子程序过程完全属于软件处理过程,不需要专门的硬件电路,而中断处理系统是一个软、硬件结合系统,需要专门的硬件电路才能完成中断处理的过程;第四,子程序嵌套可实现若干级,嵌套的最多级数由计算机内存开辟的堆栈大小限制,而中断嵌套级数主要由中断优先级数来决定,一般优先级数不会很大。
操作系统中的协程
协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。
协程不是进程也不是线程,而是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行。所以说,协程与进程、线程相比并不是一个维度的概念。
一个进程可以包含多个线程,一个线程也可以包含多个协程。简单来说,一个线程内可以由多个这样的特殊函数在运行,但是有一点必须明确的是,一个线程的多个协程的运行是串行的。如果是多核CPU,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内协程却绝对是串行的,无论CPU有多少个核。毕竟协程虽然是一个特殊的函数,但仍然是一个函数。一个线程内可以运行多个函数,但这些函数都是串行运行的。当一个协程运行时,其它协程必须挂起。
进程、线程、协程的对比
- 协程既不是进程也不是线程,协程仅仅是一个特殊的函数,协程它进程和进程不是一个维度的。
- 一个进程可以包含多个线程,一个线程可以包含多个协程。
- 一个线程内的多个协程虽然可以切换,但是多个协程是串行执行的,只能在一个线程内运行,没法利用CPU多核能力。
- 协程与进程一样,切换是存在上下文切换问题的。
上下文切换
- 进程的切换者是操作系统,切换时机是根据操作系统自己的切换策略,用户是无感知的。进程的切换内容包括页全局目录、内核栈、硬件上下文,切换内容保存在内存中。进程切换过程是由"用户态到内核态到用户态"的方式,切换效率低。
- 线程的切换者是操作系统,切换时机是根据操作系统自己的切换策略,用户无感知。线程的切换内容包括内核栈和硬件上下文。线程切换内容保存在内核栈中。线程切换过程是由"用户态到内核态到用户态",切换效率中等。
- 协程的切换者是用户(编程者或应用程序),切换时机是用户自己的程序所决定的。协程的切换内容是硬件上下文,切换内存保存在用户自己的变量(用户栈或堆)中。协程的切换过程只有用户态,即没有陷入内核态,因此切换效率高。
管程
一、 管程的概念
1. 管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
2. 进程只能互斥得使用管程,即当一个进程使用管程时,另一个进程必须等待。当一个进程使用完管程后,它必须释放管程并唤醒等待管程的某一个进程。
3. 在管程入口处的等待队列称为入口等待队列,由于进程会执行唤醒操作,因此可能有多个等待使用管程的队列,这样的队列称为紧急队列,它的优先级高于等待队列。
二、 管程的特征
1. 模块化。
管程是一个基本的软件模块,可以被单独编译。
2. 抽象数据类型。
管程中封装了数据及对于数据的操作,这点有点像面向对象编程语言中的类。
3. 信息隐藏。
管程外的进程或其他软件模块只能通过管程对外的接口来访问管程提供的操作,管程内部的实现细节对外界是透明的。
4. 使用的互斥性。
任何一个时刻,管程只能由一个进程使用。进入管程时的互斥由编译器负责完成。
三、 enter过程、leave过程、条件型变量c、wait(c) 、signal(c)
1. enter过程
一个进程进入管程前要提出申请,一般由管程提供一个外部过程--enter过程。如Monitor.enter()表示进程调用管程Monitor外部过程enter进入管程。
2. leave过程
当一个进程离开管程时,如果紧急队列不空,那么它就必须负责唤醒紧急队列中的一个进程,此时也由管程提供一个外部过程—leave过程,如Monitor.leave()表示进程调用管程Monitor外部过程leave离开管程。
3. 条件型变量c
条件型变量c实际上是一个指针,它指向一个等待该条件的PCB队列。如notfull表示缓冲区不满,如果缓冲区已满,那么将要在缓冲区写入数据的进程就要等待notfull,即wait(notfull)。相应的,如果一个进程在缓冲区读数据,当它读完一个数据后,要执行signal(notempty),表示已经释放了一个缓冲区单元。
4. wait(c)
wait(c)表示为进入管程的进程分配某种类型的资源,如果此时这种资源可用,那么进程使用,否则进程被阻塞,进入紧急队列。
5. signal(c)
signal(c)表示进入管程的进程使用的某种资源要释放,此时进程会唤醒由于等待这种资源而进入紧急队列中的第一个进程。
TCP报文中syn标志位除了申请连接还有什么用?
无其他作用。
https中的非对称加密用了什么算法,这个算法是怎么加密的
RSA加密?
1. RSA 签名验证
A和B分别具有自己的公钥和私钥。A知道自己的公私钥和B的公钥,B知道自己的公私钥和A的公钥匙。
流程如下:
A 方:
1. A利用hash算法对明文信息message进行加密得到hash(message),然后利用自己对私钥进行加密得到签名,如下
PrivateA(hash(message))=sign
2. 利用B的公钥对签名和message进行加密,如下:
PublicB(sign+message)=final
B 方:
1. 利用自己的私钥解密
PrivateB(final)=sign+message
2.利用A的公钥钥对签名进行解密
PublicA(sign)=hash(message)
3.利用与A相同对hash算法对message加密,比较与第二步是否相同。验证信息是否被篡改
字节跳动安全与风控二面:
C++ copy和=重载什么时候用
C++中一般创建对象,拷贝或赋值的方式有构造函数,拷贝构造函数,赋值函数这三种方法。下面就详细比较下三者之间的区别以及它们的具体实现
构造函数是一种特殊的类成员函数,是当创建一个类的对象时,它被调用来对类的数据成员进行初始化和分配内存。(构造函数的命名必须和类名完全相同)
而默认构造函数没有参数,它什么也不做。当没有重载无参构造函数时,
拷贝构造函数是C++独有的,它是一种特殊的构造函数,用基于同一类的一个对象构造和初始化另一个对象。
当没有重载拷贝构造函数时,通过默认拷贝构造函数来创建一个对象
强调:这里b对象是不存在的,是用a 对象来构造和初始化b的!!
1)如果用户没有自定义拷贝构造函数,并且在代码中使用到了拷贝构造函数,编译器就会生成默认的拷贝构造函数。但如果用户定义了拷贝构造函数,编译器就不在生成。
2)如果用户定义了一个构造函数,但不是拷贝构造函数,而此时代码中又用到了拷贝构造函数,那编译器也会生成默认的拷贝构造函数。
因为系统提供的默认拷贝构造函数工作方式是内存拷贝,也就是浅拷贝。如果对象中用到了需要手动释放的对象,则会出现问题,这时就要手动重载拷贝构造函数,实现深拷贝。
深拷贝:如果在复制这个对象的时候为新对象制作了外部对象的独立复制,就是深拷贝。
A(const A& other):m_i(other.m_i)
A(const A& other):m_i(other.m_i)
当一个类的对象向该类的另一个对象赋值时,就会用到该类的赋值函数。
当没有重载赋值函数(赋值运算符)时,通过默认赋值函数来进行赋值操作
强调:这里a,b对象是已经存在的,是用a 对象来赋值给b的!!
A& operator = (const A& other)
通常大家会对拷贝构造函数和赋值函数混淆,这儿仔细比较两者的区别:
1)拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行赋值操作。
2)一般来说在数据成员包含指针对象的时候,需要考虑两种不同的处理需求:一种是复制指针对象,另一种是引用指针对象。拷贝构造函数大多数情况下是复制,而赋值函数是引用对象
!!!如果不想写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,最简单的办法是将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。如:
A& operate=(const A& a); //私有赋值函数
所以如果类定义中有指针或引用变量或对象,为了避免潜在错误,最好重载拷贝构造函数和赋值函数。
下面以string类的实现为例,完整的写了普通构造函数,拷贝构造函数,赋值函数的实现。String类的基本实现见我另一篇博文。
String::String(const char* str) //普通构造函数
{
cout<<construct<<endl;
if(str==NULL) //如果str 为NULL,就存一个空字符串""
{
m_string=new char[1];
*m_string ='\0';
}
else
{
m_string= new char[strlen(str)+1] ; //分配空间
strcpy(m_string,str);
}
}
String::String(const String&other) //拷贝构造函数
{
cout<<"copy construct"<<endl;
m_string=new char[strlen(other.m_string)+1]; //分配空间并拷贝
strcpy(m_string,other.m_string);
}
String & String::operator=(const String& other) //赋值运算符
{
cout<<"operator =funtion"<<endl ;
if(this==&other) //如果对象和other是用一个对象,直接返回本身
{
return *this;
}
delete []m_string; //先释放原来的内存
m_string= new char[strlen(other.m_string)+1];
strcpy(m_string,other.m_string);
return * this;
}
String::String(const char* str) //普通构造函数
{
cout<<construct<<endl;
if(str==NULL) //如果str 为NULL,就存一个空字符串""
{
m_string=new char[1];
*m_string ='\0';
}
else
{
m_string= new char[strlen(str)+1] ; //分配空间
strcpy(m_string,str);
}
}
String::String(const String&other) //拷贝构造函数
{
cout<<"copy construct"<<endl;
m_string=new char[strlen(other.m_string)+1]; //分配空间并拷贝
strcpy(m_string,other.m_string);
}
String & String::operator=(const String& other) //赋值运算符
{
cout<<"operator =funtion"<<endl ;
if(this==&other) //如果对象和other是用一个对象,直接返回本身
{
return *this;
}
delete []m_string; //先释放原来的内存
m_string= new char[strlen(other.m_string)+1];
strcpy(m_string,other.m_string);
return * this;
}
一句话记住三者:对象不存在,且没用别的对象来初始化,就是调用了构造函数;
对象不存在,且用别的对象来初始化,就是拷贝构造函数(上面说了三种用它的情况!)
对象存在,用别的对象来给它赋值,就是赋值函数。
指针和引用的区别(更深一步,汇编层面)
首先是引用情形下的c++源码:
void add(int a, int b, int&c) {
c = a + b;
}
int main() {
int a = 1;
int b = 2;
int c = 0;
add(a, b, c);
}
下面是main对应的汇编码:
; 6 : int main() {
push ebp
mov ebp, esp
sub esp, 12 ;为该调用函数的栈空间预留12byte,用来存储局部变量a,b, c
; 7 : int a = 1;
mov DWORD PTR _a$[ebp], 1;初始化a _a$为a存储空间地址相对于ebp基址的偏移量
; 8 : int b = 2;
mov DWORD PTR _b$[ebp], 2;初始化b _b$为b存储空间地址相对于ebp基址的偏移量
; 9 : int c = 0;
mov DWORD PTR _c$[ebp], 0;初试化c _c$为c存储空间地址相对于ebp基址的偏移量
; 10 : add(a, b, c);
lea eax, DWORD PTR _c$[ebp]; 获取c存储空间相对于ebp基址的偏移量(即c存储单元的偏移地址),放在寄存器eax中
push eax;保存c存储空间的偏移量到堆栈中
mov ecx, DWORD PTR _b$[ebp];将b存储空间里面的值(即b的值)放在寄存器ecx中
push ecx;保存b存储空间的值到堆栈中
mov edx, DWORD PTR _a$[ebp];将a存储空间里面的值(即a的值)放在寄存器edx里面
push edx;保存a存储空间的到堆栈
;上面push eax push ecx push edx在栈里面存储了原来局部变量a,b,c的值,只不过对于c来说,存储的是c存储空间的偏移地址
;因此,对于a,b来说,也就是将他们的值得一份拷贝存了起来,也就是传值;而c只是存储了自己存储空间的偏移地址,也就是传地址
call ?add@@YAXHHAAH@Z ; 调用add函数,上面的语句已经为传递参数做好了准备
add esp, 12 ; 由于刚才为调用函数add传递参数进行了压栈,这里释放栈空间,即释放参数
;这就是为什么函数调用完成后局部变量和参数无效的原因,因为他们的空间被释放了
; 11 :
; 12 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
下面是函数add对应的汇编码:
; 1 : void add(int a, int b, int&c) {
push ebp
mov ebp, esp
; 2 : c = a + b;
mov eax, DWORD PTR _a$[ebp];取参数a的值到寄存器eax中
add eax, DWORD PTR _b$[ebp];取参数b的值与eax中a的值相加,结果放到eax中
mov ecx, DWORD PTR _c$[ebp];去c的偏移地址放到寄存器ecx中
mov DWORD PTR [ecx], eax;将eax中的结果写到由ecx指定的地址单元中去,即c所在存储单元
; 3 : }
pop ebp
ret 0
从上面可以看到,对于传值,c++确实传的是一份值拷贝,而对于引用,虽然是传值的形式,但是其实编译器内部传递的是值得地址
下面是指针的情形的c++源码:
void add(int a, int b, int* c) {
*c = a + b;
}
int main() {
int a = 1;
int b = 2;
int c = 0;
add(a, b, &c);
}
mian函数对应的汇编码:
; 6 : int main() {
push ebp
mov ebp, esp
sub esp, 12 ;
; 7 : int a = 1;
mov DWORD PTR _a$[ebp], 1
; 8 : int b = 2;
mov DWORD PTR _b$[ebp], 2
; 9 : int c = 0;
mov DWORD PTR _c$[ebp], 0
; 10 : add(a, b, &c);
lea eax, DWORD PTR _c$[ebp]
push eax
mov ecx, DWORD PTR _b$[ebp]
push ecx
mov edx, DWORD PTR _a$[ebp]
push edx
call ?add@@YAXHHPAH@Z ; add
add esp, 12 ;
; 11 :
; 12 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
add函数对应的汇编码:
; 1 : void add(int a, int b, int* c) {
push ebp
mov ebp, esp
; 2 : *c = a + b;
mov eax, DWORD PTR _a$[ebp]
add eax, DWORD PTR _b$[ebp]
mov ecx, DWORD PTR _c$[ebp]
mov DWORD PTR [ecx], eax
; 3 : }
pop ebp
ret 0
可以看到,指针和引用的汇编码一样,因此两者的作用也一样
字节跳动安全与风控三面:
界面监听然后修改数据的模式是什么设计模式?
用数组实现循环队列(给定长度)?
腾讯微信支付中心-事务开发部 一面:
用户态在什么情况下可以使用到内核态?
1.系统调用(来自应用)
2.中断(来自外设)异步
3.异常(来自错误应用)同步
讲讲http协议
- HTTP是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
- HTTP是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用适合的MIME-type内容类型。
- HTTP是无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
客户端请求:
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
服务端响应:
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain
char a[1G];
声明一个1G的字符串会发生什么?因为是放在栈中的,但是一般的栈都只有2M,所以最大也就char a[2077144] 再大就会报错
a[1G -1 ] = 'x';
这个语句会发生什么?这个更不可能发生了,要是在范围内还可以执行,1G真的太大了
string b(1G);
声明一个1G的string会发生什么?相当于构建一个string对象,调用构造函数是向内存中申请空间,也就是堆,这个最大空间很大,声明一个1G的对象毫无问题。
vector<char> c(16);
堆栈区别
线程模型
用户态
内核态
vector<char> c(16);
内存管理
虚拟内存 物理内存
ip 分片 重组
tcp 滑动窗口
慢启动算法
倒序合并两个链表
A->B->C
F->G->H
C->H->B->G->A->F