这是当时春招实习面试的时候总结的C++面试笔记,拿了腾讯、美团、CVTE、阿里的offer,最后去了阿里实习,一共三万字,建议慢慢看
1.预处理器
C/C++的预处理器其实就是一个词法(而不是语法)预处理器,其主要完成文本替换、宏展开以及删除注释等,完成这些操作之后,将会获得真正地**“源代码”**。
常见的include语句即是一个预处理器命名,在预处理器中它将所有的头文件包含进来。(该步骤的文件扩展名为****.i)
2.编译器
在这一步骤,将.i文件翻译为.s**,得到**汇编程序语言,值得注意的是所有的编译器输出的汇编语言都是同一种语法。
注:内联函数就是在这一环节“膨胀”进源码的,它的作用即在于:不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处,适用于功能简单,规模较小又使用频繁的函数。递归函数无法内联处理,内联函数不能有循环体,switch语句,不能进行异常接口声明。仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
3.汇编器
将**.s****翻译成机器语言指令**,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o中(把汇编语言翻译成机器语言的过程)。
4.链接器
链接(ld):gcc会到系统默认的搜索路径”/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去。
函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,**但在运行时也就不再需要库文件了。**其后缀名一般为”.a”。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,**而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。**动态库一般后缀名为”.so”,如前面所述的libc.so.6就是动态库。gcc在编译时默认使用动态库。
https://www.jianshu.com/p/1bab86143f1c
1、静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib 中的指令都全部被直接包含在最终生成的 EXE 文件中了,所以程序运行的时候不再需要其它的库文件。
但是若使用 DLL,该 DLL 不必被包含在最终 EXE 文件中,EXE 文件执行时可以**“动态”地引用和卸载这个与 EXE 独立的 DLL 文件**。静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
2、动态库就是在运行时需要调用其中的函数时,根据函数映射表找到该函数然后调入堆栈执行。如果在当前工程中有多处对dll文件中同一个函数的调用,那么执行时,这个函数只会留下一份拷贝。但是如果有多处对lib文件中同一个函数的调用,那么执行时,该函数将在当前程序的执行空间里留下多份拷贝,而且是一处调用就产生一份拷贝。
静态链接的优缺点
静态链接的缺点很明显,一是浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf()函数,则这多个程序中都含有printf.o,所以同一个目标文件都在内存存在多个副本;另一方面就是更新比较困难,因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
为什么会出现动态链接
动态链接出现的原因就是为了解决静态链接中提到的两个问题,一方面是空间浪费,另外一方面是更新困难。下面介绍一下如何解决这两个问题。
动态链接的优缺点
动态链接的优点显而易见,就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;另一个优点是,更新也比较方便,更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。但是动态链接也是有缺点的,因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。
源于一个问题:C++编译模式是怎样的?
简单来说就是**“事先声明”、“分别编译”、“事后链接”**的编译模式。
程序源代码无非就是变量和函数的总和,多个分开的文件分别编译自己的部分,在最后阶段进行互相链接,从而得到了一个可执行文件。
这是如何实现的呢?这就的提到声明和定义的区别。
简单地说,**“定义”就是将一个符号(函数和变量都是符号)完整描述:类型、参数、实现细节等;**而“声明”则简单很多:它只是“说明”有这样一个变量或者函数,但其内部是什么情况,**请等链接的时候我们再去找找。**从这就可以看出:一个函数或者变量可以被声明很多次,但是只能被定义一次!(这其实也就是头文件的好处,你想用这个函数,那就把我这个头文件给包含吧,最后链接的时候从其他目标文件去找就行了)
**到这又不得不提一点了,**头文件到底是个什么东西?
头文件其实跟源文件没什么区别,都是C++的源代码(因为在编译的时候头文件中的内容会被直接copy进cpp文件,但是有的时候头文件会互相包含,这可能就会造成在一份源码中copy两次同样的头文件,这也是为什么需要ifndef endif 或者#pragma once的用处*)。
PS:头文件相互包含总会有一个文件在另一个文件中被忽略。因为预处理时include是将包含的文件中的代码插入到当前代码里,文件是不能包含自己的**,如果相互包含编译器只能取舍一下,否则是不可能正常通过的。如果遇到这种情况就需要对头文件进行重构,修改其包含关系;
所以头文件里最好只放变量和函数的声明,而不能放它们的定义(如果多个函数都include定义,那么就会出错了)。
但!有三个例外!
其一,就是const/static可以在头文件的中定义,因为const/static默认为全局数据区,仅在当前文件有效,即使被多个文件包含也只会定义一次。
其二,就是内联函数的定义。内联函数和普通函数的区别在于编译阶段编译器需要知道内联函数的内部具体实现(才能够将其展开插入源代码),因此将内联函数放于头文件甚至有好处的。
其三,就是类的定义。程序在创建一个类对象的时候,编译器只有在类定义完全可见的情况下才能够对其进行布局(如内存分配、数据成员有哪些、函数接口有哪些),且也可以将函数成员的实现也放在头文件中,因为如果函数成员在类的定义体中被定义,那么就默认这个函数是内联的。
其实.cpp和.h文件名称没有任何直接关系,很多编译器都可以接受其他扩展名。
主要有两个功能:
一是,调用其他文件中外部定义的变量或函数(防止重定义);
二是,在C++中调用C语言代码,则编译器在编译fun这个函数名时按C的规则去翻译相应的函数名而不是C++的;
PS:extern “c” {}说明C++编译跟C编译有什么区别呢?
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像**_foo_int_int之类的名字**(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。
_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息****,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。
同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
一个全局变量的作用域默认是整个程序, 加了****static (即便加了extern,还是只能当前文件)或者加了 const(因为本身就是常量,无影响) 则是这个源文件。
如果在多个源文件 包含同一个名字的全局变量的定义,就会引起重定义。
因此要想在多个文件共用一个全局变量,我们只需在一个头文件里
声明(注意是声明)这个变量:extern int i;
然后在其中一个源文件只能是一个(通常就是头文件的同名源文件)里面 定义这个变量 int i =1**;**
注意不能用 i =1**;** 要用 int i =1**;**
最后在要使用这个变量的源文件(即其他源文件)里**#include** 头文件即可。
static修饰的变量有一个重要特点那就是:该变量限制在该源文件内;
如果将static定义在头文件中,多个程序同时包含该头文件,那么在编译的时候就会产生多个同名变量,且互不影响,所以可以是可以但不建议。如果想实现其他文件访问该变量,可以使用extern的方式;
变量的定义一般不放在头文件里,但可以把声明放在头文件里,供其他文件引用这个变量。
比如:在test.c文件中定义变量static int global = 0;
可以在头文件test.h中声明这个变量为:extern int global;
要使用这个变量的其他文件,只要包含test.h就可以了。
参考链接:
https://bbs.csdn.net/topics/80055779
https://www.cnblogs.com/lulululu/p/3693865.html
面向对象就是通过将需求要素转化为对象进行问题处理的一种思想。C++面向对象的特性可以总结为:封装、继承和多态。
封装:
封装就是将程序模块化,对象化,把具体事物的特性属性和通过这些属性来实现一些动作的具体方法放在一个类中。对象是封装的最基本单位。
继承:
继承是子类自动共享父类数据和方法的机制。父类的相关属性,可以被子类重复使用,而对于子类中需要用到的新的属性和方法,子类可以自己扩展。
多态:
多态包含了重载和重写。
静态变量都存放于全局数据区,都在程序退出时才销毁,两者唯一的区别就在于作用域不同,全局变量全局可见,而局部静态变量仅在局部区域可见。
作用域和生命周期是从两个不同的角度:时间和空间对变量进行描述。
作用域,即是该变量可被引用的范围;
生命周期即是该变量从初始化到销毁的时间;
一个程序的内存分为代码区、全局数据区、堆区、栈区,不同的内存区域,对应不同的生命周期。
函数指针是指向函数的指针,确切的说,是指向特定类型函数的指针(函数与函数指针 类型要匹配)
函数指针用来保存函数首地址,即可以通过该指针访问函数。函数指针相当于取别名。
函数指针可以指向一类函数,而不是一个函数,即可以重新赋值。
简单使用:
进阶使用:利用****typedef
typedef 返回类型(*新类型)(参数表)
指针函数是返回值为指针的函数,所以我们在main()中调用它时可以用一个同类型的指针来接收。
指针函数可以用来解决众多问题,如返回多个值的问题**。(见****“函数返回多个值的方法”**那篇文章)(见后续)
指针函数比函数指针更经常用到(哦?),一定要学会用
https://www.cnblogs.com/anwcq/p/c_zhizhenhanshu.html
这博客的例三很经典,涉及到了指针,数组指针,指针函数,二维数组的赋值,函数返回多个值,数组指针的自增与指针自增的区别。
int (*p)[4] = a;//定义一个指向a的指针变量p
括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。
[]的优先级高于*,( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针。
p指向数组 a 的开头,也即第 0 行;p+1前进一行,指向第 1 行。
参考链接:http://c.biancheng.net/view/2022.html
PS:如何获取函数多个返回值?
法一、需要的变量在函数外定义,利用引用传值在函数内修改;
法二、将需要的值打包为数组(相同数据类型)、或结构体(不同的类型),返回指针;
// ++i实现代码为:
int& operator++()
{
*this += 1;
return *this;
}
//i++实现代码为:
int operator++(int)
{
int temp = *this;
++*this;
return temp;
}
1、switch语句中如果case语句后没加break,就会一次执行下去。
2、switch语句的各常量表达式的值不能相同,但次序不影响执行结果(可以把default放最前面)。
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。
C语言的类型安全
C只在局部上下文中表现出类型安全,比如试图从一种结构体的指针转换成另一种结构体的指针时,编译器将会报告错误,除非使用显式类型转换。
C++的类型安全
如果C++使用得当,它将远比C更有类型安全性。相比于C,C++提供了一些新的机制保障类型安全:
(1)操作符new返回的指针类型严格与对象匹配,而不是void*;
(2)C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;
(3)引入const关键字代替#define constants,它是有类型、有作用域的,而#define constants只是简单的文本替换;
(4)一些**#define宏可被改写为inline函数**,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保证类型安全;
(5)C++提供了dynamic_cast关键字,使得转换过程更加安全,因为dynamic_cast比static_cast涉及更多具体的类型检查。
在C++****中可以取地址的、有名字的就是左值(内存中),反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)(寄存器中)。
左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型。左值引用是具名变量的别名,右值引用则是不具名变量的别名。
https://blog.csdn.net/qianyayun19921028/article/details/80875002
还没懂?
对左值和右值的一个最常见的误解是:等号左边的就是左值,等号右边的就是右值。左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象。
一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。
在C++11中,区别表达式是左值或右值可以做这样的总结:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
这就是完美转发***主要用作模板函数的参数类型推导
https://www.cnblogs.com/Braveliu/p/12235618.html
**std::move()****:**可以将左值强制转换为右值;(右值转左值?将右值赋值给一个新的变量即可)
那么为什么需要右值引用这个东西呢**?**
可以从寄存器直接取值,降低性能损耗。
**T&&就是C++11****之后的右值引用(看到它就是右值!)(一般用作转移构造函数),**如:int &&a = i+j;
**T&**是普通的左值引用;
1.long long ,nullptr(解除二义性),auto(编译时确定)
2.decltype():取出变量类型
3.constexptr等价于const
4.正则表达式(描述了一种字符串匹配的模式)
5.封装了多线程库如:mutex
6.for(auto d:v)
7.begin(v),end(v)
8.智能指针方面
9.可变参数模板:它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。
\10. 可变模版参数类:template< class… Types >class tuple;
11.右值引用:int &&i=10;将10这个值存到临时存储位置,再将临时存储位置的地址存给i
12.lambda:匿名函数:
auto func = [] () { cout << “hello,world”; };
func();
其中:[]函数开始,(填写函数参数),{函数主体};
13.新增容器
std::forward_list,和 std::list 的双向链表的实现不同,std::forward_list 使用单向链表进行实现,提供了 O(1) 复杂度的元素插入,不支持快速随机访问(这也是链表的特点),也是标准库容器中唯一一个不提供 size() 方法的容器。
当不需要双向迭代时,具有比 std::list 更高的空间利用率。
无序容器
std::unordered_map/std::unordered_multimap和 std::unordered_set/std::unordered_multiset。
底层都是hash表。
元组 std::tuple
std::array 保存在栈内存中,相比堆内存(vector对象在栈内存,其中有指向堆内存的指针)中的std::vector。
先说说内置数组:
int a[5];
int *b = new int[5];
前者时建立在栈内存,后者是建立在堆内存。
既然有了内置的数组,为什么还要引入array呢?
内置的数组有很多麻烦的地方,比如无法直接对象赋值,无**法直接拷贝(比如两个数组无法直接赋值)**等等,同时内置的数组又有很多比较难理解的地方,比如数组名是数组的起始地址等等。
简单来说,std::array除了有内置数组支持随机访问、效率高、存储大小固定等特点外,还支持迭代器访问、获取容量、获得原始指针等高级功能。而且它还不会退化成指针给开发人员造成困惑。
PS:定义array时,可以使用{}来直接初始化,也可以使用另外的array来构造,但不可以使用内置数组来构造。
#include
#include
int main(int argc, char const *argv[])
{
std::array a0 = {0, 1, 2, 3, 4}; //正确
std::array a1 = a0; //正确
int m = 5;
int b[m]; //正确,内置数组
std::array a2; //正确
std::array a3; //错误,array不可以用变量指定
std::array a4 = b; //错误,array不可以用数组指定
return 0;
}
他的一些内置函数参见:https://blog.csdn.net/qq_38410730/article/details/102802239
std::memory_order(可译为内存序,访存顺序)。
C++11 中规定了 **6 中访存次序(Memory Order)*只是针对原子变量的原子操作来说的!
建议阅读: 知乎 https://www.zhihu.com/question/24301047/answer/85844428
enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
};
上述内存序分为3类,顺序一致性模型(std::memory_order_seq_cst),Acquire-Release 模型(std::memory_order_consume, std::memory_order_acquire, std::memory_order_release, std::memory_order_acq_rel,)(获取/释放语义模型)和 Relax 模型(std::memory_order_relaxed)(宽松的内存序列化模型)。
memory_order_relaxed: 只保证当前操作的原子性,**不考虑线程间的同步,**其他线程可能读到新值,也可能读到旧值。
memory_order_release:(可以理解为 mutex 的 unlock 操作)
memory_order_acquire: (可以理解为 mutex 的 lock 操作)
memory_order_acq_rel:
对读取和写入施加 acquire-release 语义,无法被重排
可以看见其他线程施加 release 语义的所有写入,同时自己的 release 结束后所有写入对其他施加 acquire 语义的线程可见
memory_order_seq_cst:(顺序一致性)
如果是读取就是 acquire 语义,如果是写入就是 release 语义,如果是读取+写入就是 acquire-release 语义
同时会对所有使用此 memory order 的原子操作进行同步,所有线程看到的内存操作的顺序都是一样的,就像单个线程在执行所有线程的指令一样
通常情况下,默认使用 memory_order_seq_cst,所以你如果不确定怎么这些 memory order,就用这个。
std::atomic_flag是一个原子的布尔类型,可支持两种原子操作:
test_and_set, 如果atomic_flag对象被设置,则返回true; 如果atomic_flag对象未被设置,则设置之,返回false
clear. 清除atomic_flag对象
使用atomic_flag可实现mutex。
std::atomic对int, char, bool等数据结构进行原子性封装,在多线程环境中,对std::atomic对象的访问不会造成竞争-冒险。利用std::atomic可实现数据结构的无锁设计。
https://www.cnblogs.com/taiyang-li/p/5914331.html
所谓内存布局,即是研究以下的问题:
1* 类如何布局?
兼容C的struct:按照声明顺序对齐;
C++类的实例大小完全取决于其自身及基类的成员变量,成员函数不影响。
具体解释:https://blog.csdn.net/alidada_blog/article/details/81262152数据成员每一个类对象不同空间,但是函数是公用一份。
2* 成员变量如何访问?
3* 成员函数如何访问?
4* 所谓的“调整块”(adjuster thunk)是怎么回事?
5* 使用如下机制时,开销如何:
* 单继承、多重继承、虚继承
* 虚函数调用
* 强制转换到基类,或者强制转换到虚基类
* 异常处理
在计算大小(sizeof)的时候:
除了虚函数表指针、成员变量(非静态和常量),其他****的常量,静态,成员函数都不占类内存大小的。
参考以下博客,重要性依次降低:
这里有一道关于内存布局的题,贼妙!
https://blog.csdn.net/laozhong110/article/details/6402574
之所以拷贝到**&pca****,是将该指针自身的地址更改为CA的对象的地址;**
https://blog.twofei.com/496/
https://blog.csdn.net/fairyroad/article/details/6376620
https://www.cnblogs.com/noryes/p/6434245.html
https://www.cnblogs.com/QG-whz/p/4909359.html
https://www.cnblogs.com/mysky007/p/11042294.html
.txt(代码段) .data(全局静态已初始化变量) .bss(全局未初始化变量) heap(堆) stack(栈)
内存分段见下图:
https://blog.csdn.net/jirryzhang/article/details/79518408
让类的函数或者其他类能够访问该类的内部成员变量、函数();
右元在类内进行声明(无关public和private,它不是成员函数),在类外进行定义;
在内类声明的原因就在于,为了声明这个函数可以访问该类的私有成员。
https://blog.csdn.net/yiwanyuan2756/article/details/80437536
重载:同名函数参数(包括参数类型,个数与顺序)或返回值不同,注意返回值不能作为重载的标志。
覆盖:派生类覆盖基类函数(虚函数)
重写:派生类重写基类函数,但不覆盖(基类函数是虚函数的话就变成覆盖了)
重载和覆盖有何不同?
虚函数总是在派生类被改写,这种改写被称为“override”(覆盖)。
https://www.cnblogs.com/zbzb1/p/11527983.html
会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。
显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。
类对象被创建时,完成系统内存空间分配,并自动调用该构造函数->由构造函数完成成员的初始化工作。
析构函数的作用则恰恰相反,调用成员的析构函数,释放类对象的内存。
只在堆创建的基本思路:将构造或析构改成私有;
只在栈:将operator new delete重载私有;
1、在栈上分配就是把new、delete运算符重载为private属性。只有使用new运算符,对象才会被建立在堆上,因此只要限制new运算符就可以实现类对象只能建立在栈上。可以将new运算符重载为私有。
2、动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。
在堆上分配就是把构造、析构函数设为protected属性(时不能在类外直接访问构造函数),再用子类来动态创建对象或提供一个public的static函数来完成构造。)
以下这个博客的重点在于,C++内存机制的探讨:
https://blog.csdn.net/g5dsk/article/details/4775144
这是才是正解:
https://www.cnblogs.com/raichen/p/5808766.html
1、被类的所有的对象共有,不属于某一个对象。通过类名::就可以直接调用。
2、跟普通的成员函数比,没有隐藏的this指针作为参数。这一点可用于封装线程类。
3、静态成员函数只可以访问静态成员变量。
答:因为静态变量在main之前就已经在全局数据段产生的,它不应该去依赖类对象的生命周期。若是在类内初始化,说明需要等待该类实例化才初始化。因此在类外初始化,程序编译时就已完成。
https://www.cnblogs.com/sggggr/p/13570280.html
生命周期
静态成员变量从类被加载开始到类被卸载,一直存在;
普通成员变量只有在类创建对象后才开始存在,对象结束,它的生命期结束;
共享方式
静态成员变量是全类共享;普通成员变量是每个对象单独享用的;
定义位置
普通成员变量存储在栈或堆中,而静态成员变量存储在静态全局区;
初始化位置
普通成员变量在类中初始化;静态成员变量在类外初始化;
声明静态成员变量count,类内声明,类外初始化;
构造函数、拷贝、赋值,变量+1;析构变量-1;
拷贝(缺省、拷贝、赋值)、析构;
Empty(); // 缺省构造函数//
Empty( const Empty& ); // 拷贝构造函数//
~Empty(); // 析构函数//
Empty& operator=( const Empty& ); // 赋值运算符//
对于在函数体中初始化,是在所有的数据成员被分配内存空间(并初始化)后才进行的。列表初始化是给数据成员分配内存空间时就进行初始化。
前者是初始化、赋值;后者是初始化;少了一个步骤,当然更快;
类成员变量初始化时按照类中声明的顺序初始化的,而不是按照初始化列表的排序方式。
1、当类的数据成员中有指针类型,或存在动态内存分配时,默认的拷贝构造函数实现的只能是浅拷贝,浅拷贝会带来数据安全方面的隐患(如同一块内存的多次析构,任何一方变动都会影响到另一方)。
此时要实现正确的拷贝也就是深拷贝,必须自行编写拷贝构造函数。
2、注意:拷贝构造函数必须形式必须是:类名(类名&对象名),缺少&编译不通过(堆栈溢出)(因为重复套娃)
3、拷贝构造在三种情况下会被调用:
①用类的一个对象去初始化类的另一个新创建的对象;
②函数的形参是类对象,调用函数时(所以拷贝构造形参必须是引用类 型);
③函数的返回值是类的对象,函数执行完返回调用时(所以赋值函数用于连续赋值场合时返回值必须是引用类型);
4、A a = 10;
注意当A只有一个成员变量的时候是允许这么定义类的实例的。
CMyString& operator = (const CMyString& str);
2、那程序什么时候执行拷贝构造,什么时候执行的是运算符重载里的内容呢?
CMyString str2=str1; //执行的拷贝构造
CMyString str2(str1); //执行的拷贝构造
str2 = str1; //执行的运算符重载
可能会出现得问题的情况主要是由于:存在指针和内存分配。
#include
using namespace std;
class Student
{
private:
int num;
char *name;
public:
Student();
~Student();
};
Student::Student()
{
name = new char(20);
cout << "Student" << endl;
}
Student::~Student()
{
cout << "~Student " << (int)name << endl;
delete name;
name = NULL;
}
int main()
{
{// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2(s1);// 复制对象
}
system("pause");
return 0;
}
执行结果:调用一次构造函数,调用两次析构函数,两个对象的指针成员所指内存相同,这会导致什么问题呢?name指针被分配一次内存,但是程序结束时该内存却被释放了两次,会导致崩溃!
因此需要添加拷贝构造:
Student::Student(const Student &s)
{
name = new char(20);
memcpy(name, s.name, strlen(s.name));
cout << "copy Student" << endl;
}
https://blog.csdn.net/caoshangpa/article/details/79226270
是在写的时候(即改变字符串的时候)才会真正的开辟空间拷贝(深拷贝),如果只是对数据的读时,只会对数据进行浅拷贝。
写时拷贝:引用计数器的浅拷贝,又称延时拷贝:写时拷贝技术是通过"引用计数"实现的,在分配空间的时候多分配4个字节,用来记录有多少个指针指向块空间,当有新的指针指向这块空间时,引用计数加一,当要释放这块空间时,引用计数减一(假装释放),直到引用计数减为0时才真的释放掉这块空间。当有的指针要改变这块空间的值时,再为这个指针分配自己的空间(注意这时引用计数的变化,旧的空间的引用计数减一,新分配的空间引用计数加一)。
其实我们对写时拷贝并不陌生,Linux fork和STL string是比较典型的写时拷贝应用,本文只讨论STL string的写时拷贝。
string类的实现必然有个char成员变量,用以存放string的内容,写时拷贝针对的对象就是这个char成员变量。通过赋值或拷贝构造类操作,不管派生多少份string“副本”,每个“副本”的char成员都是指向相同的地址,也就是共享同一块内存,直到某个“副本”执行string写操作时,才会触发写时拷贝,拷贝一份新的内存空间出来,然后在新空间上执行写操作。显然,那些只读的“副本”节省了内存分配的时间和空间。
优秀博客:https://blog.csdn.net/qiansg123/article/details/80128063
移动构造函数
我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;
拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间;
移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。
1、**就近调用原则:**派生类的某一个对象调用某个接口,若本派生类有该接口的话就调用自己的,如果没有该接口,就会存在一个就近调用原则,如果父辈存在相关接口则优先调用父辈接口,如果父辈也不存在相关接口则调用祖辈接口。
2、调用过程中若不是直接(通过类域名)调用某一虚函数则会调用派生类对该接口的重写,若是直接调用某虚函数(通过类域名),则调用的就是该虚函数。
3、基类对象调用虚函数,若派生类对该虚类有重写则会优先调用该接口的重写。这句话是错误的!!!
基类对象(或指向基类对象的指针)调用虚函数是只会调用自己的,指针把基类指针指向派生类对象时才会调用派生类的。
4、注意派生类对象地址(指针)可以直接赋给基类指针,而基类赋给派生类必须显示转化。(看这个类引用他的成员会不会出错来区分)
5、构造函数是从最初的基类开始构造的,各个类的同名变量没有形成覆盖,都是单独的变量。
6、在C++的类继承中,建立对象时,首先调用基类的构造函数(不管基类构造函数是否带参数),然后在调用下一个派生类的构造函数,依次类推;析构对象时,其顺序正好与构造相反;
7、注意区别虚函数继承与虚继承(多重继承中特有的概念,虚拟基类是为解决多重继承而出现的)。
提示:增加中间层:虚继承、友元类;
https://www.cnblogs.com/wangpei0522/p/4460425.html
子类中对父类的虚函数进行了重写,那么利用基类指针就可以实现子类的动态调用。
1,如果以一个基类指针指向一个衍生类对象(派生类对象),那么经由该指针只能访问基础类定义的函数(静态连接),如果是存在虚函数,那就可以动态连接。
2,如果以一个衍生类指针指向一个基础类对象,必须先做强制转型动作(explicit cast),给程序员带来困扰。(一般不会这么去定义)
3,如果基础类和衍生类定义了相同名称的成员函数,那么通过对象指针调用成员函数时,到底调用那个函数要根据指针的原型来确定,而不是根据指针实际指向的对象类型确定。(基类和派生类之间的同名函数会被后者覆盖,而不存在重载,但可以显式指定调用)
虚拟函数就是为了对“如果你以一个基础类指针指向一个衍生类对象,那么通过该指针,你只能访问基础类定义的成员函数”这条规则反其道而行之的设计。
定义实例时会在构造函数中进行虚表的创建和虚表指针的初始化,每个对象调用的虚函数都是通过虚表指针来索引的。
虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数**,那么虚表中的地址就会改变,指向自身的虚函数实现**。如果派生类有自己的虚函数,那么虚表中就会添加该项。
派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
做一道题?
请问一下代码块的输出是啥?
#include
using namespace std;
class A
{
public:
virtual void foo()
{
cout << "A's foo()" << endl;
bar();
}
virtual void bar()
{
cout << "A's bar()" << endl;
}
};
class B: public A
{
public:
void foo()
{
cout << "B's foo()" << endl;
A::foo();
}
void bar()
{
cout << "B's bar()" << endl;
}
};
int main()
{
B bobj;
A *aptr = &bobj;
aptr->foo();
A aobj = *aptr; //转化为A类对象
aobj.foo();
}
先写下来再看答案:
https://blog.csdn.net/cxycj123/article/details/81700621
动态多态其实就是上述虚函数表中所提到的基类指针运行期间选择函数;
静态多态则是编译期间实现的,一是通过函数重载,二是通过函数模板;
https://www.cnblogs.com/lizhenghn/p/3667681.html
答:虚函数表在编译的时候就确定了,而类对象的虚函数指针vptr是在运行阶段确定的,这是实现多态的关键。(虚函数表示类所共有的,有点类似于static变量,存在全局数据区/常量区)
直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。
具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定(基类指针被撤销时,会先调用派生类的析构函数,再调用基类的析构函数。),因而只会调用基类的析构函数,而不会调用派生类的析构函数。
那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。
包括:虚函数表、虚函数、继承、多态、菱形继承、纯虚函数、虚继承。
虚函数表有什么内容?
http://c.biancheng.net/view/267.html
任何有虚函数的类及其派生类的对象都包含虚函数表(准确来说是虚函数表的地址,64位机即8个字节,且虚函数表位于对象指针的前八个字节)。
多态(动态绑定)即是通过基类指针所指向的对象的虚函数表来实现的:如果该基类指针指向的是基类对象,那就调用基类的虚函数表;如果该基类指针指向的是派生类,则调用派生类的虚函数表;
https://blog.csdn.net/primeprime/article/details/80776625
注:虚表是属于类的,而不是属于某个具体的对象,对象中仅有虚表的指针;
菱形继承即两个子类继承同一个父类,而另一个类同时继承这两个子类。这回出现二义性,D调用接口时应该调用哪一个?
可能出现模糊调用(“request for member ‘fun’ is ambiguous”)的问题,这时需要虚继承(virtual public),具体实现是:A和C中不再保存Base的具体内容,而是保存了一份偏移地址,所以在D调用fun()时,调用的就是Base的fun(),但对于A、C相同的变量名,D在调用时还是要利用域限定来处理。虚继承不同于虚函数,虚函数在C++中主要用于实现多态。
参考链接:
https://www.cnblogs.com/ybf-yyj/p/9641192.html
**纯虚拟函数(pure virtual):**virtual void myfunc ( ) =0;
纯虚拟函数不许定义其具体动作,它的存在只是为了在衍生类中被重新定义。只要是拥有纯虚拟函数的类,就是抽象类,它们是不能够被实例化的(只能被继承)。
所以到底有啥用啊?
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。(强制子类重写该函数)
如果一个继承类没有改写父类中的纯虚函数,那么他也是抽象类,也不能被实例化。
抽象类不能被实例化,不过我们可以拥有指向抽象类的指针,以便于操纵各个衍生类。
纯虚拟函数衍生下去仍然是纯虚拟函数,而且还可以省略掉关键字“virtual”。
虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。
假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。(先有鸡还是先有蛋的问题)
protected成员可以被派生类对象访问,不能被类外访问。
private继承有什么用?(只负责实现is-a,只想要父类的某些函数实现,见下例代码:)
class Timer{
public:
virtual void timeout(){ cout << __FUNCTION__ << endl;} //用于计算超时功能
};
class Widget: private Timer{ //private 继承
private: //这里也改private 或许比较好,如果是public接口,有可能不太好哦.客户误意味widget居然有超时!
virtual void timeout() {
Timer::timeout(); //调用父类的超时功能
cout << __FUNCTION__ << endl; //干自己的事
}
};
不可以,因为静态成员函数/变量没有this指针。
参考链接:
https://blog.csdn.net/leiming32/article/details/8619893
https://blog.csdn.net/weibo1230123/article/details/81980889
从上面几个图可以看出,new以及delete其实是基于malloc和free,其中分配内存的过程大致分为:
分配内存 构造函数 析构函数 销毁内存(且都需要手动释放<或者等待main函数结束全部销毁>):
即delete T[N]时,需要从空间的首地址前推四个字节开始释放(因为多分配了四个字节记录析构函数的次数)
注:malloc单纯申请内存而不会调用类的构造函数(因此需要显示地指明其指针类型),free单纯释放内存,不会调用类的析构函数;
https://www.cnblogs.com/huangfuyuan/p/9150362.html
https://www.cnblogs.com/huangfuyuan/p/9190371.html
sizeof总结:
它的功能是:返回一个对象或类型所占的内存字节数。
具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:
数组——编译时分配的数组空间大小;
指针——存储该****指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4);
类型——该类型所占的空间大小;
对象——对象的实际占用空间大小;
函数——函数的返回类型所占的空间大小。函数的返回类型不能是void。
注意:要特别留意sizeof一个指针与sizeof一个数组的区别。
见下例:
string str = "abcdefg";
string find = "de";
char find1[] ="de";
int len = sizeof(find);//就是固定的28(因为string是一个对象,类似于vector)
int len1 = sizeof(find1);//3,因为后面还有一个终止符
strlen****总结
strlen(…)是函数,要在运行时才能计算。参数必须是字符型指针(char*)。当数组名作为参数传入时,实际上数组就退化成指针了。
它的功能是:返回字符串的长度。该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符**’\0’。返回的长度大小不包括’\0’。**
1、sizeof是运算符,strlen是函数。 sizeof功能是返回一个对象或类型所占的内存字节数。strlen的功能是返回字符串的长度。
2、sizeof操作符的返回值类型是size_t****,strlen返回值类型是****int。
PS:size_t和int?
size_t在32位架构上是4字节,在64位架构上是8字节,在不同架构上进行编译时需要注意这个问题。而int在不同架构下都是4字节,与size_t不同;且int为带符号数,size_t为无符号数。
3、sizeof可以用类型、函数做参数,strlen只能用**char***做参数,且必须是以’’\0’'结尾。
注意:
1.sizeof后如果是类型必须加括弧,如果是变量名可以不加括弧。
2.sizeof用函数做参数,比如:
short f();
printf("%d\n", sizeof(f()));
输出的结果是sizeof(short),即2。
3.当适用于一个结构类型时或变量, sizeof 返回实际的大小, 当适用一静态地空间数组, sizeof 归还全部数组的尺寸。 sizeof 操作符不能返回动态地被分派了的数组或外部的数组的尺寸 。
4、数组做sizeof的参数不退化,传递给strlen就退化为指针。
5、大部分编译程序在编译的时候就把sizeof计算过了是类型或是变量的长度。
char str[20]=“0123456789”;
int a=strlen(str); //a=10;
int b=sizeof(str); //而b=20;
注意:数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址。
1.类的大小为类的非静态成员数据的类型大小之和,也就是说静态成员数据不计算在内。
2.普通成员函数与sizeof无关。
3.虚函数由于要维护在虚函数表,所以要占据一个指针大小,也就是4字节。
4.类的总大小也遵守类似class字节对齐的,调整规则。
5.空类的对象占据一个字节。
6.有关内存对齐的知识,见下;
注意:
1、只定义类但未实例化系统是不会为其分配存储空间的,sizeof(类类型)只不过是计算其所占内存大小。
2、一个类的实例所对应的内存空间中存放的第一个元素的位置是该类的第一个数据成员的存放位置。
补充:各种数据类型所占字节数:
1、int32位系统是4字节;64位操作系统64位,故占8个字节(但是自己的64为系统在vs下实测是占4个字节)。
2、char类型通常占据一个字节,对于用于扩展字符集的wchar_t类型,需要占据两个字节。
3、bool占据一个字节
4、float占据4个字节,double是float的两倍即8个字节。
在默认对齐方式下,结构体成员的内存分配满足下面三个条件
·结构体第一个成员的地址和结构体的首地址相同
·结构体每个成员地址相对于结构体首地址的偏移量(offset)是该成员大小的整数倍,如果不是则编译器会在成员之间添加填充字节(internal adding)。
·结构体总的大小要是其成员中最大size的整数倍,如果不是编译器会在其末尾添加填充字节(trailing padding)。 https://www.cnblogs.com/wangguchangqing/p/4853438.html
为什么需要内存对齐?
主要是为了实现快速寻址,提高效率,如int为4位,在32位CPU下,一次寻址是4个字节,如果int对象在地址6,就需要在0和4取两次,如果地址是4,那就只需要取一次。
strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
注意:
用memcpy给字符串赋值时应为memset(number,‘0’, n);
不能是memset(number,0,n);否则后面strlen(number)时的结果为1。
2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
1、函数功能和区别说明
strcpy():char strcpy(char dest,const char src),返回值为char ,便于链式访问,参数列表中dest为目标字符串,src为源字符串。
功能:将源字符串整体拷贝到目标字符串,包括字符串结束符“\0”,注意在使用时应该注意dest的空间应该足够放下src。
strncpy():char strncpy(char dest,const char *src,int count),与strcpy()不同的地方就是多了参数count,count为字符串src拷贝到字符串dest的字符个数,如果count给的数值大于src的长度,会在标字符串相应位置补上“\0”。如果count给的数值小于src的长度,那么只有len个字符被复制到dst中,注意!此时它的结果将不会以‘\0’字节结尾。
使用这个函数,尤其需要注意,不要出现count>strlen(dst)的情况,如果count>strlen(dst),那么会破坏dst后面的内存(即缓冲区溢出):strncpy是不负责检测count是否大于dst长度的。
2、代码实现
// strcpy()
char my_strcpy(char *dest, const char * src)
{
assert(dest&&src);
char * temp = dest;//这句不能少,否则找不到字符首部!!
while ((dest++ = src++)!=’\0’);
return temp;
}
//strncpy()
char my_strncpy(char dest, const char * src,int count)
{
assert(dest&&src);
char * temp = dest;
while (count--&&(dest++ = src++))
{;}
if(count>0)
{
while (count--)
{
dest++ = '\0';
}
}
return temp;
}
3、安全性
在安全性方面,显然strncpy要比strcpy安全得多,strcpy无法控制拷贝的长度,可能出现越界的问题,程序就会崩溃。而strncpy就控制了拷贝的字符数避免了这类问题,但是要注意的是dest依然要注意要有足够的空间存放src,而且src 和 dest 所指的内存区域不能重叠。
考虑内存重叠的strncpy
面试中经常会遇到让你写一个能够处理内存重叠的strncpy,标准库中的strncpy是不考虑内存重叠的,如果出现内存重叠,结果将是未定义的。如果内存重叠和src的长度小于len这两种情况同时出现,又如何处理?
代码博客:https://blog.csdn.net/sinat_30071459/article/details/72771137
C++中强制类型转换操作符有static_cast****、dynamic_cast、const_cast、****reinterpert_cast四个。
1.static_cast:
用法:static_cast < type-id > ( expression )
说明:该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。
用途:
情况1:**void指针->**其他类型指针
情况2:const转非const
情况3:多态向上转化,可以向下但不建议;
**PS:**https://blog.csdn.net/wanfustudio/article/details/1952963?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1.control
编译器隐式执行的任何类型转换都可以由static_cast来完成,比如int与float、double与char、enum与int之间的转换等。
当编译器隐式执行类型转换时,大多数的编译器都会给出一个警告(即常见的warning)。
有何作用?使用static_cast可以明确告诉编译器,这种损失精度的转换是在知情的情况下进行的,也可以让阅读程序的其他程序员明确你转换的目的而不是由于疏忽。把精度大的类型转换为精度小的类型,static_cast使用位截断进行处理。
使用static_cast可以找回存放在void*指针中的值。如:
static_cast也可以用在于基类与派生类指针或引用类型之间的转换。然而它不做运行时的检查,不如dynamic_cast安全。static_cast仅仅是依靠类型转换语句中提供的信息来进行转换,而dynamic_cast****则会遍历整个类继承体系进行类型检查,因此dynamic_cast在执行效率上比static_cast要差一些。
2.dynamic_cast
用法:dynamic_cast < type-id > ( expression )
说明:该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。
来源:为什么需要dynamic_cast****强制转换?
简单的说,当无法使用virtual****函数的时候。
PS:代码详见第二篇博客,很大的应用价值;
Base要有虚函数,否则会编译出错;static_cast则没有这个限制。这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见
3.reinpreter_cast
用法:reinpreter_cast (expression)
说明:type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
PS:基本什么都能转,但是可能出错,少用;
4.const_cast
用法:const_cast
说明:该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
参考博客:https://www.cnblogs.com/xiangtingshen/p/10851349.html
更为详细:https://blog.csdn.net/windgs_yf/article/details/88396056
补充:volitate
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去优化这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
而const值放在编译器符号表中,而没有访问内存,所以用volatile修饰const,就可以对其进行修改了。
http://www.zzvips.com/article/127340.html
标准库的六大组成部分
容器
分配器
适配器
仿函数
迭代器
算法
考虑小型区块造成的内存破碎问题,SGI设计了双层级配置器:
第一级直接使用allocate()调用malloc()、deallocate()调用free(),使用类似new_handler机制解决内存不足(抛出异常),配置无法满足的问题(如果在申请动态内存时找不到足够大的内存块,malloc 和new 将返回NULL 指针,宣告内存申请失败)。
第二级视情况使用不同的策略,当配置区块大于128bytes时,调用第一级配置器,当配置区块小于128bytes时,采用内存池的整理方式:配置器维护16个(128/8)自由链表,负责16种小型区块的此配置能力。内存池以malloc配置而得,如果内存不足转第一级配置器处理。
Operator new(PS:这是new的底层)和malloc:
Operator new其中调用的就是malloc,值得注意的是,malloc****实际分配的内存比你申请的更大,这是为了内存管理而设计的(分配内存不连续需要用header指针链接)。
基本思路是利用链表来管理。
顺序容器:数组(Array)、Vector、Deque(念dei ke):双向队列,两端皆可进可出(其内部其实是分块的内存buffer(线性空间),逻辑相邻的buffer利用指针相连(中控器map来管理));List**(链表)**:标准库是双向(环状)链表,前后向指针;**Forward-List:**单向链表;
关联容器:(associative)
Set/Multiset**:集合,大小排序,独一无二的数据(Multi可以重复),基本都是红黑树实现****;**
Map/MultiMap**:**映射表,也是红黑树实现;
UnorderedMap/Multimap**:**hash表,支持多对一,一般是Separate Chaining(分离链接法);
set或者map底层解析?
https://blog.csdn.net/chongzi_daima/article/details/107849493
容器分析
vector的size增长倍数是2或者1.5倍,根据编译器的不同倍数不同;
vector****内存成长方式可归结以下三步曲:
(1)另觅更大空间;
(2)将原数据复制过去;
(3)释放原空间三部曲。
PS;从上图可以看出,string的大小默认是28,且不会因为赋值而改变,这是为什么呢?
string的实现在各库中可能有所不同,但是在同一库中相同一点是,无论你的string里放多长的字符串,**它的****sizeof()**都是固定的,字符串所占的空间是从堆中动态分配的,与sizeof()无关。
有sizeof()为12****、32字节的库实现。通常,我们所用到的 string 类型一般都会是这样实现:
双向队列****deque,在内存中是分段连续(即在多个内存块中存放,利用指针连接这几个内存块),如果已分配的不够,那么就会在前或者后增加buffer。
**详见:**https://blog.csdn.net/u010710458/article/details/79540505
set/Multiset,try ··· catch(exception& p)···,进行异常处理;
map/Multimap,key-value;其实multiset跟哈希表其实本质一样,只不过set的key和value是合一的,即value就是key!另外,两者底层都是红黑树结构;
详见:https://blog.csdn.net/zhang_guyuan/article/details/62237971
Unordered set/multiset,采用分散链接法处理冲突的情况(即multi),如果分配内存不够,则扩大一倍,重新规划散列;
Unordered map/multimap,跟前者本质无区别;
Priority_queue(优先队列),在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。本质是一个堆实现的。可以实现升序或者降序。
https://blog.csdn.net/u011408355/article/details/47957481
vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。
当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间【vector内存增长机制】。
当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。
因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器会都失效了。
之所以不使用常量的增加操作,是考虑均摊时间。
举个例子,一共要vector中插入n个数,倍增因子为m,即每次变为以前的m倍,假定这n个数最后都需要内存扩容才装的下去,那么需要分配内存的次数为logm(n)(比如n=8,m=2,那么需要分配三次才能装下所有的东西)。
另外,要注意的是,每次内存分配之后,需要将之前的数据拷贝至新内存(这个移动操作也看做是push_back),第i次分配内存就需要拷贝m^i个元素,所以n个元素全部插入,涉及的元素插入次数为:
将m-1看成一个常量,那就是n的时间消耗,所以均摊下来,每个插入操作的时间复杂度为O(1)。
补充:等比数列的求和公式
那如果是增加指定值呢?比如增加m?
假定有n个元素,每次增加k个。第i次增加复制的数量为为:100i 。
所以最终的耗时为:
n 次 push_back 操作所花费的时间复杂度为O(n^2)。
那么均摊下来,每一个push_back的时间复杂度为O(n)。
这个问题似乎在于之前内存空间的重用。
如果倍增因子大于2,那么新分配的空间必然大于之前内存空间的总和,也就无法重新使用之前的那一段内存,这对缓存极不友好,可能会造成内存碎片。
从上图可见如果选用k=1.5,那么之前的内存空间就可以被使用了。
vector和list的区别?
vector数据结构
1、vector和数组类似,拥有一段连续的内存空间,因此能高效的进行随机存取(即[]操作符),时间复杂度为o(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。
2、另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝并释放掉旧空间。
list数据结构
list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n);但由于链表的特点,能高效地进行插入和删除。
总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;如果需要大量的插入和删除,而不关心随机存取,则应使用list。
map插入方式有几种?
1、用insert函数插入pair数据,
mapStudent.insert(pair
2、用insert函数插入value_type数据
mapStudent.insert(map
3、在insert函数中使用make_pair()函数
mapStudent.insert(make_pair(1, “student_one”));
4、用数组方式插入数据
mapStudent[1] = “student_one”;
注意:关联容器也是可以通过迭代器从头到尾遍历的。
1、unordered_map和map类似,都是存储的key-value的值,可以通过key快速索引到value。不同的是unordered_map不会根据key的大小进行排序,unordered_map的底层实现是哈希表,hash_table使用的开链法进行冲突避免。
2、存储时是根据key的hash值(并不是元素值value)判断元素是否相同,即unordered_map内部元素是无序的,而map中的元素是按照二叉搜索树存储,进行中序遍历会得到有序遍历。
3、注意无序容器unordered_map在存储上组织为一个桶,每个桶保存0个或多个元素(value),哈希函数是用来计算桶号。
4、对于自定义类型,使用时map的key需要重载运算符 <。而unordered_map需要定义hash_value哈希函数并且重载operator== (用来比较键值对相应的哈希值)。(这个在leetcode题中经常遇到)
1、他们的底层都是以红黑树的结构实现,因此插入删除等操作都在O(logn)时间内完成;(相比于平衡二叉树,红黑树删除操作时旋转次数更少)
2、实现map的红黑树的节点数据类型是key+value,而实现set的节点数据类型是value;
3、因为map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比较低;
4、四类容器仅仅只是在RBTree上进行了一层封装,map的键和值不同,每个键都有自己的值,键不能重复,但是值可以重复。multimap和multiset就在map和set的基础上,使他们的键可以重复,除此之外基本等同。
常用的解决冲突方法有以下四种:
链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存放在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
插入和查找以及删除操作消耗的时间会达到****O(n)
开放定址法:
这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址****p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
他不需要更多的空间,但是在最坏的情况下(例如所有输入数据都被map到了一个index上)的时间复杂度也会达到****O(n)。
再哈希法:
这种方法是同时构造多个不同的哈希函数:
Hi=RH1(key) i=1,2,…,k
当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
什么是一致性哈希?
一致性哈希目前主要应用于分布式缓存当中(或者是负载均衡)。
一致性哈希可以有效地解决分布式存储结构下动态增加和删除节点所带来的问题。我们简单举例说明一下:
首先,我们把全量的缓存空间当做一个环形存储结构。环形空间总共分成2^32个缓存区
如何让key和节点对应起来呢?很简单,每一个key的顺时针方向最近节点,就是key所归属的存储节点。
https://www.jianshu.com/p/49e3fbf41b9b
当使用一个容器的insert或者erase函数通过迭代器插入或删除元素**"可能"会导致迭代器失效**,因此我们为了避免危险,应该重新获取的新的有效的迭代器进行正确的操作。
迭代器失效的类型:
1.由于插入元素,使得容器元素整体“迁移”导致存放原容器元素的空间不再有效,从而使得指向原空间的迭代器失效。
2.由于删除元素使得某些元素次序发生变化使得原本指向某元素的迭代器不再指向希望指向的元素。
https://blog.csdn.net/weikangc/article/details/49762929
从概念上来讲:
指针,其实就是一个存储变量地址的变量;
引用,变量的别名,内存块的别名,编译器一般将其实现为const指针;
从实现特点来讲:
指针可以更改,引用不能更改;
指针可以指向数组,而引用无法绑定数组;
引用必须声明初始化;
又一个问题,指针传递和引用传递?
指针传递的本质是值传递,将指针存储的地址作为形参,若改变其中指针的地址,将不会影响外部;
而引用传递传入的虽然也有局部变量,但其中存储的是实参的地址,即对该局部变量的操作都间接寻址到了原本的实参;
又一个问题,指针的引用和指向引用的指针?
不存在指向引用的指针,对引用(引用不是对象)取地址,其实就是对引用的对象取地址;
这种语法是错误的:
int &p=v;
int&*q=&p;
这个也是错误的,
int **&rnum = &num1;//**这是不允许的 **无法从“**int ”转换为“int *&”
而指针的引用就简单了:
int v=0;
int *p=&v;
int *&q=p;
1、指针是一个实体,而引用仅是个别名;
2、引用必须被初始化,指针不必;引用只能在定义时被初始化一次,之后不可变;指针可以改变所指的对象;
3、可以有const指针,但是没有const引用;
4、“sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
5、指针和引用的自增(++)运算意义不一样;
6、程序为指针变量分配内存区域,而引用不需要分配内存区域(只是一个变量有了两个名字);
7、指针和引用作为函数参数进行传递时也不同。用指针传递参数,可以实现对实参进行改变的目的,但是在被调函数中同样要给形参分配存储单元(指针自身的四字节存储单元);在将引用作为函数参数进行传递时,实质上传递的是实参本身,而不是实参的一个拷贝,因此对形参的修改其实是对实参的修改。
8、不存在指向空值的引用必须确保引用是和一块合法的存储单元关联,但是存在指向空值的指针,即引用不能为空,指针可以为空;
**野指针:**指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针。
悬空指针:悬空指针是指针最初指向的内存已经被释放了的一种指针。避免悬空指针的方法:使用智能指针。
如何避免?
如何避免使用野指针? 好办!那就是养成初始化的习惯;
如何避免使用智能指针呢?迂回一下,使用智能指针?
指向函数的指针(函数指针) :int( *p )(int ,int);//p是指向函数的指针变量。指向的函数返回值类型为int型,参数类型为(int ,int)
使用形式为 p=function; result = (*p)(x, y);
返回指针值的函数(指针函数):类型名*函数名(参数表列)如int *a(int x,int y);
指向指针的指针,如char *(*p)或char**p ,注:指向指针的指针一般用它来指向指针数组的
参考:
https://blog.csdn.net/xu1105775448/article/details/80627371
为什么需要智能指针?
便于资源的管理,进行堆内存的分配和回收,防止造成内存泄露(如忘记delete或者catch异常之后忘记释放内存)。
智能指针的原理是什么?
简单来看,智能指针使用**基于RAII(资源获取即初始化)**思想对普通指针进行了封装,使其实际是一个对象,然表现地像一个指针;
有哪些智能指针?
C++11版本之后提供,包含在头文件**中,shared_ptr、unique_ptr、weak_ptr(C++98时还有一个auto_ptr****)**
shared_ptr
多个shared_ptr可以指向同一个指针,采用引用计数来实现指针的释放:使用一次计数+1,析构一次计数-1;
每个shared_ptr对象在内部指向两个内存位置:
1**、指向对象的指针**。
2**、用于控制引用计数数据的指针**。
智能指针里的计数器维护的是一个指针,指向的实际内存在堆上,不是栈上的
如何实现共享所有权的?
1、当新的 shared_ptr 对象与指针关联时,则在其构造函数中,将与此指针关联的引用计数增加****1。
2、当任何 shared_ptr 对象超出作用域时,则在其析构函数中,它将关联指针的引用计数减1。如果引用计数变为0,则表示没有其他 shared_ptr 对象与此内存关联,在这种情况下,它使用delete函数删除该内存。
另外值得注意的是,shared_ptr关联的对象必须是new****出来的堆空间的指针,不能够是栈空间的指针,否则会报错。
注:智能指针是一个模板类,不能将类直接赋给指针。
注2:https://blog.csdn.net/shaosunrise/article/details/85228823
注3:https://blog.csdn.net/qq_31904421/article/details/107708025
unique_ptr
https://blog.csdn.net/shaosunrise/article/details/85158249
独享被管理对象指针所有权的智能指针。其实现是通过包装一个原始指针,来负责其生命周期,在该unique_ptr对象被销毁时,调用析构函数释放关联指针的内存。
简单来说就是对普通指针做了一层封装,使其具有自动析构的功能。
unique_ptr无法复制,只能移动move(所有权),因为独享所有权,如果能复制就违背了这一前提(将拷贝赋值构造函数给禁用了)。
weak_ptr
https://www.jb51.net/article/188294.htm
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变引用计数,一旦最后一个指向对象的shared_ptr被销毁(它是一个若引用),对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。
循环引用解决方法:
弱指针用于专门解决shared_ptr循环引用的问题,weak_ptr不会修改引用计数,即其存在与否并不影响对象的引用计数器。
循环引用就是:两个对象互相使用一个shared_ptr成员变量指向对方,这样当离开各自作用域后内存也得不到释放从而引起内存泄露。
如何解决呢?
在其中一个类里使用weak_ptr,这是弱引用。
弱引用并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
其内部实现原理?
不过就是指向了shareedptr而已,但不会增加sharedptr的引用次数。
auto_ptr
https://blog.csdn.net/gatieme/article/details/50939155
已经被C++11废弃的智能指针,其简单的封装普通指针,使其可以在析构时自动释放资源;
跟unique_ptr有何不同?
前者支持拷贝和赋值,且完成操作之后所有权就被转移了。 前者不能作为容器对象,后者可以利用move()函数来实现作为容器对象。
参见:https://blog.csdn.net/weixin_40081916/article/details/79377564
auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr p2;
p2 = p1; //auto_ptr不会报错.
此时不会报错,p2剥夺了p1的所有权(p1=NULL了!),但是当程序运行时访问p1将会报错。所(问题)以auto_ptr的缺点是:存在潜在的内存崩溃问题!
手写智能指针实现
https://www.cnblogs.com/xiehongfeng100/p/4645555.html
模板T是指代任何类型(template 或者template ),在实际使用的时候需要对其类型指定才能正常使用,如类模板,但是函数模板是不需要指定的,因为在使用过程中,编译器可以根据调用参数的情况来推断出应该调用何种类型的对应函数。
PS:有关模板的特化(specoalization), 偏特化(partial specialization)
为什么需要模板?
为了减小重复编写相似代码的工作量,如函数功能一致,差别仅在于其中参数或返回值的类型不同。
C++中的模板分为:函数模板、类模板。
通常而言,并不是把模板编译成一个可以处理任何类型的单一实体;而是对于实例化模板参数的每种类型,都从模板产生一个不同的实体。这种用具体类型代替模板参数的过程叫做实例化,它产生了一个模板的实例。(即是说在编写代码的时候可能并不知道有些函数调用对不对,因此程序易读性较差)
于是,模板被编译了两次,分别发生在:
(1)实例化之前,先检查模板代码本身,查看语法是否正确;在这里会发现错误的语法,如遗漏分号等。
(2)在实例化期间,检查模板代码,查看是否所有的调用都有效。在这里会发现无效的调用,如该实例化类型不支持某些函数调用(该类型没有提供模板所需要使用到的操作)等。
模板可以让我们生成通用参数类型的函数,这个通用的函数能够接受任意类型参数,同样可以返回任意类型的值,这样就避免了对所有类型的函数进行重载。
如:
template
G_Type Plus(G_Type a, G_Type b)
{
G_Type Sum;
Sum = a + b;
return Sum;
}
上述只是声明了一种通用类型,可以增加任意个:
template
G_Type Plus(G_Type a, U_Type b)
{
G_Type Sum;
Sum = a + b;
return Sum;
}
类模板的实现,可以使类有通用类型的成员,不必在定义类的时候定义成员的类型,即类的实现不关注数据元素的具体类型,而只关注类所需要实现的功能。如:
//类模板实现
template
class Student
{
public:
Name1 m_Member1;
Name2 m_Member2;//成员函数定义
public:
void m_Add(Name1 a, Name2 b);//成员函数声明
};
//成员函数的外部实现
template
void Student
{
return a + b;
}
3.1 函数模板特化
函数模板在某种特定类型下的特定实现称为函数模板的特化。如下:
template
T add(T a, T b)
{
printf(“T add(T a, T b)\n”);
return a + b;
}
//上述函数不支持int*类型,所以要对传入指针类型参数进行特化。
template <>
int* add
{
printf(“T add(const int* pa, const int* pb)\n”);
*pa += *pb;
return pa;
}
int main(void)
{
int a = 8, b = 6;
add<>(&a, &b); //<>,编译器会自动推导数据类型为int*
return 0;
}
3.2 类模板特化
类模板特化分为部分特化与完全特化。模板的特殊化是当模板中的pattern有确定的类型时,模板有一个具体的实现。简单来说,部分特化是修改了参数的类型,而完全特化则是定死了参数的类型(int、char还是其他)。
a.部分特化
//原版的模板
template
class TestCls
{
public:
void add(T1 a, T2 b);
};
template
void TestCls
{
printf(“estCls
}
//部分特化
template
class TestCls
{
public:
void add(T a, T b);
};
template
void TestCls
{
printf(“TestCls
}
int main(void)
{
TestCls
//注意<>内不能为空,编译器对类模板不能进行类型推导。
t.add(5, 5);
return 0;
}
原先的设定是T1、T2两种类型,二者既可以是同一种类型,也可以是不同一种,特化后T1、T2只能是相同的一种类型****T。当我们定义的对象的T1、T2类型是一样的时候,编译器会选择特化后的类模板。
b.完全特化
**//完全特化,完全特化后"<>"**内为空
template <>
class TestCls
{
public:
void add(int a, char b)
{
printf(“TestCls
}
};
int main(void)
{
TestCls
t.add(5, 5);
return 0;
}
PS:模板这种类似宏(macro-like) 的功能,对多文件工程有一定的限制:函数或类模板的实现 (定义) 必须与原型声明在同一个文件中。也就是说我们不能再 将接口(interface)存储在单独的头文件中,而必须将接口和实现放在使用模板的同一个文件中。
来干嘛?实现在函数模板中获取迭代器的特性!
https://blog.csdn.net/Jiangtagong/article/details/108896943
对指针的赋值,仅仅是让基类指针_pB指向的子类对象的地地址。
而上面代码中无论赋值操作还是赋值构造时,只会处理成员变量,一个类对象里面的vptr永远不会变,永远都会指向所属类型的虚函数表。
PS:对象赋值只能对成员变量,不影响虚函数表(只有指针才能拿到)!
全局数组、变量在定义时默认初始化为0;
局部数组、变量在定义时默认初始化为随机值;
https://www.cnblogs.com/zhaocs/articles/13958302.html
noexcept可以用来修饰函数,在函数的后面加上noexcept,代表这个函数不会抛出异常,如果抛出异常程序就会终止。
https://www.cnblogs.com/sword03/p/10020344.html
https://www.cnblogs.com/moonz-wu/archive/2008/05/07/1186065.html
这个回答更好:
https://www.cnblogs.com/wangpei0522/p/4460425.html
主要是为了确定参数的个数方便:
https://blog.csdn.net/jiange_zh/article/details/47381597
https://blog.csdn.net/weiyayunerfendou/article/details/72805766
利用自建的结构实现对内存的分配(大小相符)和释放进行优化。
https://www.cnblogs.com/bangerlee/archive/2011/09/01/2161437.html
new是不可重载,完成内存分配和构造函数调用;
operator new可重载,只完成内存分配;
https://www.cnblogs.com/raichen/p/5808766.html
线程池简单来说就是提前申请一堆线程(资源),后续任务需要即分配,用完即放回线程池,基本步骤如下:
线程池有两个核心的概念,一个是任务队列,一个是工作线程队列。任务队列负责存放主线程需要处理的任务,工作线程队列其实是一个死循环,负责从任务队列中取出和运行任务,可以看成是一个生产者和多个消费者的模型。
任务队列相当于生产者,工作线程相当于消费者,当任务来临时,只有一个线程可以抢到。
参考代码:https://zhuanlan.zhihu.com/p/64739638
模板类代码:https://zhuanlan.zhihu.com/p/61464921
主要作用是禁止隐式转换。如将类的构造函数设置为explicit,那么编译器无法对其进行隐式转换,强烈建议观看链接:
https://zhuanlan.zhihu.com/p/137947734
或https://www.cnblogs.com/winnersun/archive/2011/07/16/2108440.html
x=y=5,最后x、y都是5;
||具有惰性,前面完成了就不会完成后面;
strlen计算字符串的长度,不包括末尾的’\0’,C语言字符串默认加上;
sizeof变量或者类型的大小(以字节为单位);
https://www.cnblogs.com/starrys/p/12664953.html
是的,但是否内联还要看编译器(C++primier);
如果是引用,那么就必须用初始化参数列表,并且不能有缺省构造;
const是运行期常量;constpexr主要是编译期常量:
https://blog.csdn.net/u012516419/article/details/105792962
https://www.cnblogs.com/Kernel001/p/7853441.html
相同点:
都和数组类似,利用下标访问;存储空间连续,可随机访问;
不同点:
array和vector可以赋值给另一个array、vector,但数组不行;
由于vector是动态变化的,因此在插入和删除时,需要考虑迭代器是否失效。
array和vector是自动释放的,而数组如果是new/malloc的需要先手动进行内存释放。
sizeof 计算array那就是个数乘以元素字节;
计算vector那就是vector的成员变量的大小(它是一个对象),跟编译器有关!
类型推断,是编译期间获得对应元素的数据类型。
对于内存空间的清理,由于申请时记录了其大小,因此无论使用delete还是delete[ ]都能将这片空间完整释放,而问题就出在析构函数的调用上,当使用delete时,仅仅调用了对象数组中第一个对象的析构函数,而使用delete [ ]的话,将会逐个调用析构函数。
常用的有,单例模式、工厂模式、观察者模式;
https://www.nowcoder.com/tutorial/93/f982cd252694499181bcf1bb83780cad
单例模式主要解决一个全局使用的类频繁的创建和销毁的问题。单例模式下可以确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
应用:日志系统、线程池;
工厂模式主要解决接口选择的问题。该模式下定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,使其创建过程延迟到子类进行。
应用:河长制算法中线程RiverThread;
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。如果多个观察者和client相互调用,那么可能会程序崩溃,需要一个array存储这些观察对象;
应用:类似于发布订阅模式(Publish/Subscribe)
对已经存在的某些类进行装饰,以此来扩展一些功能,从而动态的为一个对象增加新的功能,这种方法无需经过继承。
应用:https://www.cnblogs.com/of-fanruice/p/11565679.html
一个对象可以被多个装饰者装饰,这些装饰者拥有一个共同的超类。
https://leetcode-cn.com/problems/print-foobar-alternately/solution/c-3chong-fang-fa-yuan-zi-cao-zuo-tiao-ji-d2l9/
法一、利用一个原子变量bool来判断,打印出来之后就取反,而后另外一个线程开始打印,如此反复;
法二、互斥锁加条件变量,互斥锁主要是对一个bool值进行判断;
法三、信号量,初始化两个信号量,一个为1一个为0(1的就是先打印的),打印之后相反的信号量加一(post)
补充:
std::this_thread::yield() 的目的是避免一个线程频繁与其他线程争抢CPU时间片, 从而导致多线程处理性能下降.
std::this_thread::yield() 是让当前线程让渡出自己的CPU时间片(给其他线程使用)
std::this_thread::sleep_for() **是让当前休眠”指定的一段”**时间.
sleep_for()也可以起到 std::this_thread::yield()相似的作用, 但两者的使用目的是大不相同的。
https://blog.csdn.net/zzhongcy/article/details/85248597?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242
动态内存模型可理解为存储一致性模型,主要是从行为(behavioral)方面来看多个线程对同一个对象同时(读写)操作时(concurrency)所做的约束,动态内存模型理解起来稍微复杂一些,涉及了内存,Cache,CPU 各个层次的交互,尤其是在共享存储系统中,为了保证程序执行的正确性,就需要对访存事件施加严格的限制。
"大端"和"小端"表示多字节值的哪一端存储在该值的起始地址处;小端存储在起始地址处,即是小端字节序;大端存储在起始地址处,即是大端字节序。
UDP/TCP/IP协议规定:**把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;**而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节(即:高位字节存放在低地址处);由此可见,多字节数值在发送之前,在内存中因该是以大端法存放的;
不同CPU有不同的字节序类型,典型的使用小端存储的CPU有:Intel x86和ARM 典型的使用大端存储CPU有:Power PC、MIPS UNIX和HP-PA UNIX。
C/C++中有如下四个常用的转换函数,这四个函数在小端系统中生效,大端系统由于和网络字节序相同,所以无需转换。
htons —— 把unsigned short类型从主机序转成网络字节序
ntohs —— 把unsigned short类型从网络字节序转成主机序
htonl —— 把unsigned long类型从主机序转成网络字节序
ntohl —— 把unsigned long类型从网络字节序转成主机序
???C++20居然不是yield实现的?
nullptr是C++11版本中新加入的,它的出现是为了解决NULL表示空指针在C++中具有二义性的问题。
https://blog.csdn.net/qq_18108083/article/details/84346655
abort会发送SIGABORT****信号
调用exit后,程序会调用静态对象和全局对象的析构函数,但abort****什么析构函数都不会调用。
程序完全退出时,系统会释放所有未释放的内存和和其他资源。
整型与0的比较不用赘述,易错点在于布尔变量、浮点、指针变量与零值的比较。
PS:这个误差可以是1e-6等;
PS:有时候为了防止将 if (p == NULL)误写成 **if (p = NULL),而有意把p和NULL**颠倒。写成 if (NULL == p)****。(这是一个很好的规避风险的习惯)