内存泄漏: c++
内存泄漏通常为在代码中申请了一块内存后因为各类缘由在使用完成后没去释放这块内存,操做系统这个时候认为这块内存还在被应用程序使用(由于程序没去释放),因而这一块内存对于程序来讲已经没有用了(也不会去用),对于系统来讲也没有释放出来,这样的一件事情成为内存泄漏。 算法
对于现代的操做系统而言,一个程序是运行在独立的进程空间中的,当这个进程结束后,操做系统将回收这个进程申请的全部内存,也就是说,当进程结束后,该进程泄漏的内存会被回收 (无论你是否是泄漏的都回收了) 编程
至于后果,对于运行在通常用户这边的应用程序来讲,因为运行的时间不长,结束后会被操做系统整个回收,通常不会形成不良影响(尽管如此,仍是要尽量作到没有内存泄漏);而对于服务来讲,好比跑在服务器上的程序,会长时间长时间的运行,若是有内存泄漏的代码,会在运行中不断积累泄漏的内存,最后占满服务器的全部可用内存,致使宕机。 windows
内存满了后再次申请内存会报错,或者在最后几回申请的时候发生内存溢出。 设计模式
野指针: 数组
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时若是未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不肯定的变量,此时去解引用就是去访问了一个不肯定的地址,因此结果是不可知的。 安全
任何指针变量刚被建立时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。因此,指针变量在建立的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。若是没有初始化,编译器会报错" 'point' may be uninitializedin the function "。 服务器
有时指针在free或delete后未赋值 NULL,便会令人觉得是合法的。别看free和delete的名字(尤为是delete),它们只是把指针所指的内存给释放掉,但并无把指针自己干掉。此时指针指向的就是"垃圾"内存。释放后的指针应当即将指针置为NULL,防止产生"野指针"。 微信
多线程和单线程的区别: 网络
何时单线程快何时多线程快:对于处理时间短的服务或者启动频率高的要用单线程,相反用多线程!
一亿个数用多线程找出其中的质数:一个线程负责一部分数的求解。好比10个线程就同时操做求是不是质数。
推荐我看Unix环境编程和Unix网络编程
继承的机制和实际应用场景,
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 容器的扩容过程是很是耗时的,而且当容器进行扩容后,以前和该容器相关的全部指针、迭代器以及引用都会失效。所以在使用 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 容器就会扩容。 |
说一下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有多少个核。毕竟协程虽然是一个特殊的函数,但仍然是一个函数。一个线程内能够运行多个函数,但这些函数都是串行运行的。当一个协程运行时,其它协程必须挂起。
进程、线程、协程的对比
上下文切换
管程
1、 管程的概念
1. 管程能够看作一个软件模块,它是将共享的变量和对于这些共享变量的操做封装起来,造成一个具备必定接口的功能模块,进程能够调用管程来实现进程级别的并发控制。
2. 进程只能互斥得使用管程,即当一个进程使用管程时,另外一个进程必须等待。当一个进程使用完管程后,它必须释放管程并唤醒等待管程的某一个进程。
3. 在管程入口处的等待队列称为入口等待队列,因为进程会执行唤醒操做,所以可能有多个等待使用管程的队列,这样的队列称为紧急队列,它的优先级高于等待队列。
2、 管程的特征
1. 模块化。
管程是一个基本的软件模块,能够被单独编译。
2. 抽象数据类型。
管程中封装了数据及对于数据的操做,这点有点像面向对象编程语言中的类。
3. 信息隐藏。
管程外的进程或其余软件模块只能经过管程对外的接口来访问管程提供的操做,管程内部的实现细节对外界是透明的。
4. 使用的互斥性。
任何一个时刻,管程只能由一个进程使用。进入管程时的互斥由编译器负责完成。
3、 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++的空类,编译器会加入哪些默认的成员函数
·默认构造函数和拷贝构造函数
·析构函数
·赋值函数(赋值运算符)
·取值函数
**即便程序没定义任何成员,编译器也会插入以上的函数!
注意:构造函数能够被重载,能够多个,能够带参数;
析构函数只有一个,不能被重载,不带参数
而默认构造函数没有参数,它什么也不作。当没有重载无参构造函数时,
A a就是经过默认构造函数来建立一个对象
下面代码为构造函数重载的实现
class A
{
int m_i;
Public:
A()
{
Cout<<"无参构造函数"<<endl;
}
A(int i):m_i(i) {} //初始化列表
}
class A
{
int m_i;
Public:
A()
{
Cout<<"无参构造函数"<<endl;
}
A(int i):m_i(i) {} //初始化列表
}
拷贝构造函数是C++独有的,它是一种特殊的构造函数,用基于同一类的一个对象构造和初始化另外一个对象。
当没有重载拷贝构造函数时,经过默认拷贝构造函数来建立一个对象
A a;
A b(a);
A b=a; 都是拷贝构造函数来建立对象b
强调:这里b对象是不存在的,是用a 对象来构造和初始化b的!!
先说下何时拷贝构造函数会被调用:
在C++中,3种对象须要复制,此时拷贝构造函数会被调用
1)一个对象以值传递的方式传入函数体
2)一个对象以值传递的方式从函数返回
3)一个对象须要经过另外一个对象进行初始化
何时编译器会生成默认的拷贝构造函数:
1)若是用户没有自定义拷贝构造函数,而且在代码中使用到了拷贝构造函数,编译器就会生成默认的拷贝构造函数。但若是用户定义了拷贝构造函数,编译器就不在生成。
2)若是用户定义了一个构造函数,但不是拷贝构造函数,而此时代码中又用到了拷贝构造函数,那编译器也会生成默认的拷贝构造函数。
由于系统提供的默认拷贝构造函数工做方式是内存拷贝,也就是浅拷贝。若是对象中用到了须要手动释放的对象,则会出现问题,这时就要手动重载拷贝构造函数,实现深拷贝。
下面说说深拷贝与浅拷贝:
浅拷贝:若是复制的对象中引用了一个外部内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象指向同一个外部内容,就是浅拷贝。(指针虽然复制了,但所指向的空间内容并无复制,而是由两个对象共用,两个对象不独立,删除空间存在)
深拷贝:若是在复制这个对象的时候为新对象制做了外部对象的独立复制,就是深拷贝。
拷贝构造函数重载声明以下:
A (const A&other)
下面为拷贝构造函数的实现:
class A
{
int m_i
A(const A& other):m_i(other.m_i)
{
Cout<<"拷贝构造函数"<<endl;
}
}
class A
{
int m_i
A(const A& other):m_i(other.m_i)
{
Cout<<"拷贝构造函数"<<endl;
}
}
当一个类的对象向该类的另外一个对象赋值时,就会用到该类的赋值函数。
当没有重载赋值函数(赋值运算符)时,经过默认赋值函数来进行赋值操做
A a;
A b;
b=a;
强调:这里a,b对象是已经存在的,是用a 对象来赋值给b的!!
赋值运算的重载声明以下:
A& operator = (const A& other)
一般你们会对拷贝构造函数和赋值函数混淆,这儿仔细比较二者的区别:
1)拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行赋值操做。
class A;
A a;
A b=a; //调用拷贝构造函数(b不存在)
A c(a) ; //调用拷贝构造函数
/****/
class A;
A a;
A b;
b = a ; //调用赋值函数(b存在)
2)通常来讲在数据成员包含指针对象的时候,须要考虑两种不一样的处理需求:一种是复制指针对象,另外一种是引用指针对象。拷贝构造函数大多数状况下是复制,而赋值函数是引用对象
3)实现不同。拷贝构造函数首先是一个构造函数,它调用时候是经过参数的对象初始化产生一个对象。赋值函数则是把一个新的对象赋值给一个原有的对象,因此若是原来的对象中有内存分配要先把内存释放掉,并且还要检察一下两个对象是否是同一个对象,若是是,不作任何操做,直接返回。(这些要点会在下面的String实现代码中体现)
!!!若是不想写拷贝构造函数和赋值函数,又不容许别人使用编译器生成的缺省函数,最简单的办法是将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。如:
class A
{
private:
A(const A& a); //私有拷贝构造函数
A& operate=(const A& a); //私有赋值函数
}
若是程序这样写就会出错:
A a;
A b(a); //调用了私有拷贝构造函数,编译出错
A b;
b=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协议
客户端请求:
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