低到高的内存地址增长
程序代码区、常量区、全局数据区(全局变量、静态变量)(已初始化、未初始化(不占可执行文件内存布局))、堆(向上增长)、动态链接库、栈(向下增长)
C++类对象内存模型
编译器将成员变量和成员函数分开存储,分别为每个对象的成员变量分配内存,但是所有对象都共享同一段函数代码。对象的大小只受成员变量的影响。
泛型技术
1.多态
使用父类型别的指针指向其子类的实例,通过父类的指针调用实际子类的成员函数,让父类的指针有“多种形态”。
2.泛型技术
- 使用不变的代码来实现可变的算法
- 模板技术,RTTI技术,虚函数技术
- 要么编译时决议的多态,模板
- 要么运行时决议的多态,虚函数表
虚拟内存
程序给出的地址看做是一种虚拟地址(Virtual Address),然后通过某些映射的方法,将这个虚拟地址转换成实际的物理地址。妥善地控制这个虚拟地址到物理地址的映射过程。
优点
- 程序运行时使用固定的内存地址,不同程序的地址空间相互隔离。
- 提高内存使用效率,且操作系统能够更加灵活地控制与硬盘换入换出的粒度
内存分页
Paging对虚拟地址空间和物理地址空间进行分割和映射,以减小换入换出的粒度,提高程序运行效率
- 当程序运行时,只需要将必要的数据从磁盘读取到内存,暂时用不到的数据先留在磁盘中,什么时候用到什么时候读取。
- 当物理内存不足时,只需要将原来程序的部分数据写入磁盘,腾出足够的空间即可,不用把整个程序都写入磁盘。
页表
定位到数据所在的页,以及它在页内的偏移(也就是距离页开头的字节数),就能够转换为物理地址
-
1G= 1024M = 1024*1024 K,1K = 1024
-
页表
- 页通过页面编号寻址,记录页面编号的内存数组称为页表
-
一级页表
- 页大小是固定的,一般是4KB
- 4GB(4KB*1M)虚拟内存共有1M个页面,
- 页面编号需要4字节即4B存储,所以页表大小4MB(1M*4B)
-
一级页表寻址方案,
- 虚拟地址大小是4字节、32位
- 高20位作为页表数组的下标,低12位作为页内偏移
-
二级页表
- 页表大小是4MB,共记录了1M个页面编号,将页表拆分成1K个小页表,每个小页表占据4KB内存
- 小页表在对应的物理页有编号,编号需要4B/4个字节
- 每个物理页有1K个小页表需要1K个编号
- 页目录:记录小页表编号的数组,数组大小4KB(1K*4B)
-
二级页表寻址方案
- 虚拟地址分割为三分部,高10位作为页目录中元素的下标,中间10位作为页表中元素的下标,最后12位作为页内偏移
-
三级页表同理
分级的优点
- 明显优点是,如果程序占用的内存较少,分散的小页表的个数就会远远少于1024个,只会占用很少的一部分存储空间(远远小于4M)
声明和定义
声明
- 告诉编译器,名字已经预定,被匹配到一块内存上,可能是在别的地方定义的。声明可以出现多次
定义
- (编译器)创建一个对象,为这个对象分配一块内存,并给它取上一个名字
volatile关键字
The as-if rule
Allows any and all code transformations that do not change the observable behavior of the program
volatile说明变量可能会被意想不到地改变,提示编译器不要优化成从寄存器读取而是每次从内存读取。
使用场景
- 中断程序会访问该变量
- 多线程共享某一个变量,变量可能被另一个线程修改
- 嵌入式中要访问某个设备顺序赋值,却被编译器优化成执行最后一个赋值
误解:无法解决多线程并发问题
- 即使通过volatile保证编译中指令不被优化
- cpu执行指令也可能会优化
- 在A线程中两个变量没有依赖关系,而在在B线程中有,即使加上volatile也无法防止导致A线程中对两个变量的行为被优化,导致B线程的依赖关系被打乱
常见问题
- const也有可能被程序之外的因素改变,可以使用volatile
- 指针可能被中断程序修改指向,可以使用volatile
- 指针ptr指向一个volatile变量,通过指针求平方时应该只能读取1次,如果读取两次可能两次之间会发生数值改变
atomic
atomic_flag原子bool类型
- 只有默认构造函数,拷贝构造函数已被禁用,不能被拷贝
- 需要使用ATOMIC_FLAG_INIT宏定义初始化,保证处于clear状态
- test_and_set()函数检查标志,被设置过返回true,未被设置则设置它并返回false
- test_and_set()操作是原子的
- clear()清除 std::atomic_flag 对象的标志位,即设置 atomic_flag 的值为 false
- 多线程中可以实现lock free,提高竞态访问效率
static关键字
1.面向过程两大作用
- 隐藏,将全局变量或者函数限制在当前模块,对其他模块隐藏。
- 保持变量内容持久化,函数调用结束不会被销毁。
1.1静态变量
在全局数据区,默认初始化为0。
- 静态全局变量,声明只在当前模块可见、对其他模块隐藏、不可extern,其他模块可以定义同名变量
- 静态局部变量,程序执行到声明处首次初始化,后面再调用函数不再初始化,局部作用域
- 静态函数,声明只在当前模块可见、对其他模块隐藏、不可extern,其他模块可以定义同名函数
2.面向对象
静态成员与对象无关,故静态成员不可访问普通成员、普通成员可以访问静态成员
2.1静态数据成员
- 只有一份拷贝、所有对象共享
- 在全局数据区,因为定义时需要分配空间,只能在类中声明,在类外定义不能加static、类外定义后即分配空间,没有实例也可以访问
- 访问权限同普通数据成员
- 不在全局名字空间,不会与其他全局名字冲突,在类名字空间通过private可以实现隐藏
2.2静态成员函数
- 不与任何对象联系,不具有this指针,所以无法访问对象普通数据成员和成员函数
3.静态变量何时初始化
- 静态变量存储在虚拟地址空间的数据段和bss段
- C语言在编译器分配内存直接初始化
- C++由于引入对象,
- 对象生成必须调用构造函数
- 编译期间简单分配内存还不够
- 所以只有在首次用到时才进行构造(初始化)
- 所以C++可以使用变量对局部静态变量初始化
4.循环中静态变量如何知道已经初始化
- C语言编译器直接跳过初始化语句,应为编译期间已经分配内存且初始化
- C++编译器会在编译器分配静态变量内存后,
- 在当前静态局部变量地址附近分配一块空间,记录变量是否经过初始化
- 每个编译器的flag位置分配方式不太一样
const关键字
const对象定义时必须初始化
常量成员函数
- 声明定义处都要加上 const
- 不能修改类
class Test
{
public:
void func() const;
};
void Test::func() const
{
intValue = 100;//错误,不能修改类的数据成员
}
常指针
- 指针常量,char *const p; 指针本身是一个常量
- 常量指针,char const* p; 指向的是一个常量类型
常变量
-
const变量只在本文件中有作用,不同文件可以定义同名const变量,原因在于文本替换
-
文件中定义了const对象要在其他文件使用,需要在声明和定义处加上extern
-
#define 和 常变量区别
- 宏定义是简单的文本替换
- const常量虽然在使用时会被替换(除了取地址),但具有作用域、且有类型检查,在debug的时候是以变量形式呈现而不是文本
inline关键字
解决频繁调用的小函数大量消耗栈空间(栈内存)的问题,如for循环里调用一个小函数。
- 递归函数、包含复杂结构switch、while(开销更大)等函数不能用inline
- 虚函数不要用inline,virtual意味着”等待,直到执行时期再确定应该调用哪一个函数“,inline却意味着”在编译阶段
- 构造函数和析构函数不要inline?,可能偷偷执行可基类或成员对象的构造、析构函数
- inline只是对编译器建议,是否内联不一定
- 内联函数的定义要放在头文件,否则编译时可能无法在文件找到内联函数定义,除非每个文件都定义改函数
- 内联函数定义时必须使用inline关键字,声明时不要,inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。
- 类内定义的成员函数会被建议为inline
C++类对象的内存模型
-
空类和真空类的占据1字节无意义内存,简单类的内存只有普通成员数据
-
普通继承类的内存中,子类成员在父类之后
-
复杂多继承的内存排列
-
带虚函数的空类,内存只有一个虚函数表指针,占4字节
-
继承带虚函数的的类
-
继承带虚函数的的类,且子类有新的虚函数
虚表只有一张,不因为增加新的虚函数而多出另一张,新的虚函数的指针在复制的虚表的后面
-
纯虚函数,纯虚函数的类不可实例化,但其派生类必须实现纯虚函数,即继承了虚表
-
虚函数类的多继承,会有不止一张虚表
虚函数表
-
子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的
-
任何妄图使用父类指针想调用子类中的**未覆盖父类的成员函数**的行为都会被编译器视为非法
-
父类型的指针访问子类自己的虚函数,通过指针的方式访问虚函数表来达到违反C++语义的行为
-
访问non-public****的虚函数,父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表
-
虚函数表是类对象之间共享的,而非每个对象都保存了一份
-
单一的一般继承,被overwrite的虚函数在虚函数表中得到更新,以下示意图里都是虚函数
-
多重继承,子类只overwrite了父类的f()函数,以下示意图全部是虚函数
-
- 每一个父类都有自己的虚表
- 子类的成员函数被放到了第一个父类的表中
- 内存布局中,其父类布局依次按声明顺序排列
虚函数表中,函数的位置怎么确定
虚函数调用和普通函数调用,效率谁快
虚指针是怎么实现的
菱形继承与虚继承
-
多继承的菱形继承有二义性问题、浪费内存空间,虚继承使得最终派生类只保留一份虚基类成员,操作上的麻烦是必须在虚派生的真实需求出现前完成虚派生的操作
-
base_ios、istream、ostream、iostream
-
最终派生类的构造函数会调用虚基类的构造函数,派生类忽略不会,这样虚基类只被初始化一次
-
虚继承通过 虚基类表指针 和 虚表 实现,__虚基类表指针__占据对象内存空间,虚表 记录着 虚基类表指针 距离 虚基类 的 偏移量
重载、重写、隐藏
-
隐藏,派生类的函数屏蔽了与其同名的基类函数
- 函数名相同
- 参数不同时,不被重写
- 参数相同时没有virtual关键字时,不被重写
-
重载overload
- 同一作用域声明的
- 不同参数列表(类型,个数,顺序)的同名函数
- 不关心函数返回类型
-
重写override,多态
- 不同作用域,派生类与基类
- 相同参数列表的同名函数
- 基类函数必须有 virtual 关键字,不能有 static
- static成员没有this指针,而虚函数必须要用对象调用
- static function都是静态决议的,virtual function 是动态决议的
- 访问权限可以不同
构造函数和析构函数
构造函数不能为虚函数
- 存储空间角度:虚函数对应于vtable,vtable存在于对象的内存空间,构造函数是虚函数则需要vtable调用,但对象未实例化,没有内存空间没有vtable
- 使用角度:虚函数用于多态,而构造函数适用于创建对象时自动调用,不会被父类指针调用,不存在多态使用情况
构造函数不能为static
- 静态static成员没有this指针,而虚函数必须要用对象调用
- static function都是静态决议的,virtual function 是动态决议的
析构函数在派生类申请了资源的情况下必须是虚函数
- 基类指针指向了派生类对象,而基类中的析构函数是非virtual的,在delete的时候只会调用基类的析构函数,而不会调用派生类的析构函数,造成内存泄漏
构造函数调用顺序
- 基类构造函数、对象成员构造函数、派生类本身的构造函数
析构函数调用顺序
- 派生类本身的析构函数、对象成员析构函数、基类析构函数(与构造顺序正好相反)
构造函数抛出异常
- C++在执行构造函数过程中产生异常时不是一个完整的对象,是不会调用对象的析构函数的,而仅仅清理和释放产生异常前的那些C++管理的变量空间等,之后就把异常抛给程序员处理
- 如果有成员对象被构造成功则改成员对象会调用析构函数
析构函数不允许抛出异常
- 如果析构函数抛出异常,将直接导致当前执行线程异常终止!如果是主线程中发生析构异常,程序立即退出!
动态内存
new和malloc的区别
- new 和 delete是操作符(可以被重载),malloc和free是库函数有出栈入栈操作(可以被覆盖)
- 返回类型安全性
- new返回的是对象类型指针,严格匹配,类型安全
- malloc返回的是void*需要强制转换
- new依据类型分配内存大小,malloc需要手动指出大小
- 分配失败
- new抛出bac_alloc异常
- malloc返回NULL
- 构造函数析构函数
- new分配内存
- 分配一块足够大的,原始的,未命名的内存空间
- 编译器运行相应的构造函数以构造对象
- 构造完成后,返回一个指向该对象的指针
- delete释放内存
- 调用对象的析构函数
- 释放内存空间
- malloc只会分配一块原始的内存空间
- new分配内存
- new在重载时可以调用malloc
free的如何知道释放的内存多大
- malloc分配内存的时候会在内存块的地址前面的4(linux)/16(win)个字节出存放内存块的大小
- free依据首地址之前的大小释放
堆栈的区别,适用场景以及注意事项 为什么要用堆
- 区别
- 堆是由低地址向高地址扩展,栈是由高地址向低地址扩展
- 栈空间小,堆可申请较大空间(受限于有效虚拟内存大小)
- 栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等
- 函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内存(eip),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(ebp),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是调用函数的局部变量,注意静态变量是存放在数据段或者BSS段,不入栈的
- 堆,堆顶使用一个字节的空间来存放堆的大小,具体存放内容是由程序员来填充
- 堆中的内存需要手动申请和手动释放,栈中内存是由OS自动申请和自动释放
- 堆中频繁调用malloc和free,会产生内存碎片,降低程序效率;而栈由于其先进后出的特性,不会
- 堆的分配效率较低,而栈的分配效率较高
- 栈是操作系统提供的数据结构,专门的寄存器存储栈的地址,专门的指令执行压栈和入栈;
- 堆是由C/C++函数库提供的,分配、释放、合并内存的算法机制复杂
- 为什么用堆
- 程序运行时才知道对象需要多少内存空间,对象的生存期不固定
- 而堆的内存分配空间大可以自由分配、控制权在程序员手里
**栈的注意事项**
*不要在函数内部定义过大的局部变量,线程堆栈空间不够导致 COREDUMP*
*函数参数传递尽量使用引用和指针,不然消耗空间*
*函数调用栈不可过深,特别是递归*
*内存越界*
***堆的注意事项***
*内存越界,导致段错误、脏数据*
*内存泄漏,没有释放、重复申请*
*内存碎片,频繁的申请、释放小块的内存可能会造成申请和释放的不协调,导致很多已释放内存无法使用*
*重复释放,同一块内存多次释放。 可能造成段错误*
*野指针,释放完内存后指向他的指针要及时设置为空*
,否则可能有重复释放内存之类的操作
malloc原理
池化技术:内存池、连接池、线程池、对象池,每次申请该资源都有较大的开销,不如提前过量的资源,然后自己管理,以备不时之需。
内存池的研究重点是对已申请到的内存的管理
-
管理方式:
- 链表、位图、对象池,
- malloc多种方式复合,不同大小内存块不同措施
-
链表管理
- 内存块结构
- next 是指针,指向下一个内存块,used 用来表示当前内存块是否已被使用[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qzat09ou-1587027693494)(C:\Users\cqlia\AppData\Roaming\Typora\typora-user-images\1583414519193.png)]
- malloc() 会把第一个空闲区域拆分成两部分,一部分交给程序使用,剩下的部分任然空闲
- free() 会将几个连续的空闲区域合并为一个
重复释放内存
- 释放内存后,没有指针抹零,指针在其它部分对同一块内存进行释放,导致对这块内存的写操作,内存有可能已经被分配做其他用途,可能影响了程序行为
- 无论是free还是delete都会导致程序运行异常
STL
为什么要设计迭代器
- 迭代器是抽象设计概念,提供一种可以依序访问某聚合物元素的的方法,不需要暴露聚合物内部实现细节
- 保证对所有容器的遍历方式是一样的,不用关心容器类型
- 保证容器与算法的分开实现,容器负责数据存储,算法只需要通过迭代器来访问容器。迭代器是容器和算法的通用接口
- 同一的访问方式可以避免冗余的的代码和意外错误
allocator
- 介绍
- new的机制是将内存分配和对象构造组合在一起,delete也是将对象析构和内存释放组合在一起。
- 容器中需要内存池
- allocator将内存分配和对象构造分离,事先得到大块内存,然后真正需要时就在这块内存上创建对象
- 常用函数
- allocator a 定义了一个名为a的allocator对象
- a.allocator(n) ,分配一段原始的、未构造的内存
- a.deallocate(p,n) 释放T*指针p地址开始的内存
- a.construct(p,args) 在p指向的原始内存上构建对象
- a.destory§ 对p指向的对象执行析构函数
- uninitialized_copy(b,e,b2) 、uninitialized_copy_n(b,n,b2)
- uninitialized_fill(b,e,t) 、uninitalized_fiil_n(b,n,t)
迭代器失效
- 数组型数据结构(vector、deque)
- 元素是分配在连续的内存中
- insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效
- **insert(iter)、erase(iter)返回下一个有效的迭代器
- 链表型数据结构(list)
- 使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.
- *erase(iter)返回下一个迭代器
- erase(iter++)直接推导下一个迭代器
- 树形数据结构(map、set)
- 红黑树来存储数据
- 插入不会使得任何迭代器失效
- 删除运算使指向删除位置的迭代器失效
- erase操作返回void
- erase(iter++)的方式删除迭代器
list与vector区别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q88rvItI-1587027693500)(C:\Users\cqlia\AppData\Roaming\Typora\typora-user-images\1583734959969.png)]
顺序容器
vector
https://blog.csdn.net/BillCYJ/article/details/78801834
vector与array
- array是静态空间,一旦配置了就不能改变
- vector是动态空间,随着元素的加入,自行扩充空间
空间配置策略
- 使用allocator(空间配置器)进行内存管理
- insert或push_back等增加元素的操作时,如果此时动态数组的内存不够用,就要动态的重新分配,一般是当前大小的两倍,把原数组的内容拷贝过去
- 访问速度同一般数组,只有在重新分配发生时,其性能才会下降
- 重新分配空间后,所有迭代器失效
插入元素
- 首先将旧的vector的插入点之前的元素复制到新空间
- 再将新增元素填入新空间
- 最后将旧vector的插入点之后的元素复制到新空间
关联容器-有序/无序
有序
map/set/multimap/multiset
map、set全是红黑树实现
无序
unordered_map/unordered_set/unordered_multimap/unordered_multiset
哈希桶实现
unordered_map 和 map的区别
- 有序性
- map有序
- unordered_map无序
- 底层数据结构
- map底层为红黑树,查找插入O(logN)
- unordered_map底层是闭散列的哈希桶,查找插入为O(1),性能更优
- insert操作,map比unordered_map慢,但稳定
- unordered_map的erase操作会缩容,导致元素重新映射
- 关系比较符
- map的使用<关系比较符
- unordered_map使用“==”关系比较,通过hash值方式比较,自定义类型需要重载==操作符
- unordered_map遍历顺序与输入顺序不一定相同
map用find和[]的区别
[]:将关键码作为下标去执行查找,并返回对应的值;如果不存在这个关键码,就将一个具有该关键码和值类型的默认值的项插入这个map
find:用关键码执行查找,找到了返回该位置的迭代器;如果不存在这个关键码,就返回尾迭代器。
lamda表达式
对于只使用一次的函数对象类,创建匿名的函数对象,直接在使用它的地方定义
[=/&] (int x, int y) -> bool {return x%10 < y%10; }
//=表示值传递
//&表示引用传递
智能指针
智能指针(smart pointer)是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露(利用自动调用类的析构函数来释放内存)。
- shared_ptr允许多个指针指向同一个对像
- unique_ptr则“独占”所指向的对象
- weak_ptr伴随类,它是一种弱引用,指向shared_ptr所管理的对象
shared_ptr
- 最安全的分配和使用动态内存的方法就是调用一个名为标准库函数make_shared,在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
- 每个shared_ptr都有一个关联的计数器,通常称其为引用计数,计数器变为0,它就会自动释放自己所管理的对象。
- shared_ptr类会自动销毁此对象,它是通过另一个特殊的成员函数-析构函数完成销毁工作的
unique_ptr
- 只能有一个unique_ptr指向一个给定对象
- unique_ptr不支持普通的拷贝或赋值操作
- reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针
- release会切断unique_ptr和它原来管理的的对象间的联系,返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值
- 例外:可以拷贝或赋值一个将要被销毁的unique_ptr.最常见的例子是从函数返回一个unique_ptr.
weak_ptr
- weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放
- 没有其他操作,lock返回一个指向共享对象的shared_ptr,如果不存在,lock将返回一个空指针
scoped_ptr
- 和weak_ptr的区别在于,有”++”“–”以及“*”“->”这些操作
智能指针的原因
- 手动malloc/new出来的资源,容易忘记free/delete
- 程序中途抛出异常,无法释放资源
循环引用问题
两个对象内部的shared_ptr成员相互指向对方,由一方改为weak_ptr解决
条件变量
https://www.jianshu.com/p/c1dfa1d40f53
- 使用生产者使用主动通知的办法告知消费者需要处理消息,防止消费者不断加锁解锁轮询造成无谓的cpu消耗
- 消费者被唤醒的可能不是被notify唤醒的,需要判断队列中是否有消息,没有则继续wait睡眠
- 条件变量和互斥锁一起使用比只使用互斥锁好很好多
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
std::deque<int> q;
std::mutex mu;
std::condition_variable cond;
void function_1() {
int count = 10;
while (count > 0) {
std::unique_lock<std::mutex> locker(mu);
q.push_front(count);
locker.unlock();
cond.notify_one(); // Notify one waiting thread, if there is one.
std::this_thread::sleep_for(std::chrono::seconds(1));
count--;
}
}
void function_2() {
int data = 0;
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu);
while(q.empty())
cond.wait(locker); // Unlock mu and wait to be notified
data = q.back();
q.pop_back();
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
}
}
int main() {
std::thread t1(function_1);
std::thread t2(function_2);
t1.join();
t2.join();
return 0;
}
- wait()函数会先调用互斥锁的unlock()函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作
- 所以互斥锁要用unique_lock ,提供了lock和unlocker接口
- wait被唤醒需要判断是否为假醒,若是假醒需要继续wait
函数
函数指针和函数名
- 函数名和函数指针都是函数指针,一个是地址常量(函数的入口地址)、一个变量
- 函数和函数指针的调用可以不需要解引用,使用函数名赋值的时候可以不需要取地址
- 函数指针可以作为变量传递,作为参数,比如回调中可以使用
函数对象
- 重载了函数调用操作符(operator())的类对象
- 优点
- 携带附加信息
区别
struct、class的区别
- 默认访问控制,struct是public,class是private
- class 继承默认是 private 继承,而 struct 继承默认是 public 继承
- class这个关键字还可用于定义模板参数,struct不行
- {}初始化
- 没有构造函数时,即使有成员函数,struct可以使用{}初始化
- class不行
指针和引用的区别
- 有const指针,没有const引用
- 指针可以有多级,引用只能是一级
- 指针的值可以为空,引用的值不能为NULL
- 引用在进行初始化后就不会再改变
- sizeof,指针是指针的大小,引用时对象的大小
- 使用场景
- 尽可能使用引用,不需要“重新指向”时,引用一般优先于指针被选用
- 相同点
- 都是地址的概念,指针存储内存地址,引用是内存块的别名
memcpy与memmove的区别
当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的,memcpy不保证拷贝的结果的正确
-
覆盖情况1,没有问题
-
覆盖情况2,有问题
-
memmove的解决办法是,判断
**if**(s_dst>s_src && (s_src+n>s_dst))
,成立的话从尾开始倒过来拷贝,不成立从投开始拷贝
内存对齐
概念
操作内存的起始地址,能够被操作的内存大小整除。否则可能需要两个地址总线读操作。
限制
编译器只会以2的次幂去分割内存,1、2、4、8、16
规则
- 设默认对齐单位为n(一般为4、8)
- 实际对齐单位
- 成员:n、成员大小的最小值
- 内嵌struct:n、最大成员大小的最小值
- 整体struct:n、最大成员大小的最小值
- 成员的地址首地址偏移量必须为实际对齐单位的整数倍
- 结构体整体大小必须为实际对齐单位的整数倍
补漏
函数的参数在底层存在哪里
(R0-R3)
重载如何实现
编译器在编译.cpp文件中当根据函数形参的类型和顺序会对函数进行重命名
C++中使用C函数的注意事项
- 原因:C和C++编译器对编译函数符号的生成规则是不一样,因为重载
- 解决:让被调用的C代码按照C的编译规则生成
- extern “C”
- 注意点:
- c允许void指针隐式转换为其他类型,C++不允许需要显式转换
- new 和 class 在C中不是保留字
如何防止类被继承
-
要点
- 虚继承的最终派生类在生成对象时,是虚基类自己调用构造函数,而不是通过派生类
- 将虚基类的构造函数设置为private就不会生成对象了,它的派生类类也无法生成对象
- 将派生类设置为虚基类的友元,派生类就可以访问被设为private的构造函数
class A; class Assistant//虚基类 { private: friend A; //派生类设为虚基类友元,突破private限制 Assistant(){}; ~Assistant(){}; }; class A : public virtual Assistant//派生类虚继承 { public: A(){}; ~A(){}; }; class B : public A { public: B(){}; ~B(){}; }; int main(int argc, char* argv[]) { A a; // 可以构造 B b; // 不能构造 return 0; }
#### sizeof(1==1)?
32位机中,c++ bool 1字节,c int 4字节
C是C++的子集吗
不是,C的部分特性C++并没有,C++只是继承了C的语法和大部分特性
整数在计算机里面是怎么存储的?补码还是源码?正数和负数的区别
- 正数的原码、反码、补码都是一样的
- 负数才有实际意义的反码和补码
- 反码是原码除符号数外取反
- 补码是反码加1
对象移动
右值引用
- 可以取地址的、有名字的就是左值
- 常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化
- 不能取地址的、没有名字的就是右值。右值是临时的,是即将销毁的
- 将亡值
- 将要被移动的对象(移为他用)
- 将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配
- 纯右值
- 临时变量值
- 字面量值
- 将亡值
- **右值值引用通常不能绑定到任何的左值,**要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值
int i = 42;
int &r = i; //对
int &&rr = i; //错,右值不是字面常量,也不是临时对象
int &r2 = i * 24; //错,不能把临时对象赋给普通引用
const int &r3 = i * 13; //对,可以把临时对象赋给引用常量
int &&rr2 = i * 2; //对,右值是临时对象
注意!
int &&rr3 = 42; //对,右值是字面值常量
int &&rr4 = rr3; //错,绑定右值引用的变量rr3仍是左值,即rr3是正常的变量。
标准库move函数
强行右值,move算是一个移动构造函数
int a = 12;
int &&b = std::move(a) //move函数告诉编译器,要把这个左值a当右值处理
移动构造函数
- 类似对应的拷贝操作,但它从给定对象窃取资源而不是拷贝资源。
- 移动构造函数和拷贝构造函数的唯一区别就是它的引用是右值引用。
- 一旦资源完成移动,原对象必须不再指向被移动的资源,所有权已归属新对象。
StrVec::StrVec(StrVec &&s) noexcept : elements(s.elements),
first_free(s.first_free), cap(s.cap) //noexcept表示不抛出异常
//noexcept:声明和定义不抛出异常的移动构造函数和移动赋值函数都要显式
//指定noexcept,否则系统会使用拷贝操作。
{
//上面的列表初始化就移动好了,注意第一个参数是非const右值引用
//接下来的话保证s进入这样的状态-对其进行析构函数是安全的
s.elements = s.first_free = s.cap = nullptr;
}
移动赋值运算符
从给定对象窃取资源而不是拷贝资源
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
if(this != &rhs) //检测,不是自赋值再进行下面步骤,是自赋值直接返回
{
free(); //因为它要接管rhs,原来的内存就不用了。
//从rhs窃取资源
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
//将rhs置于可析构状态
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
模板类型推导
auto关键字
-
auto是类型指示符,编译期间根据后面的表达式推导出具体类型。所以用auto声明的变量必须初始化
-
注意点
- 函数参数、模板参数不可声明为auto
- 只是占位符,不是类型,不能用于类型转换
- 不能sizeof或者typeid
- 同一auto序列的变量必须推导为同一类型
四种cast转换
C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
1、const_cast
用于将const变量转为非const
2、static_cast
用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
3、dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
向上转换:指的是子类向基类的转换
向下转换:指的是基类向子类的转换
它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
4、reinterpret_cast
几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
5、为什么不使用C的强制转换?
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
//将rhs置于可析构状态
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
## 模板类型推导
#### auto关键字
- auto是类型指示符,编译期间根据后面的表达式推导出具体类型。**所以用auto声明的变量必须初始化**
- 注意点
- 函数参数、模板参数不可声明为auto
- 只是占位符,不是类型,不能用于类型转换
- 不能sizeof或者typeid
- 同一auto序列的变量必须推导为同一类型
## 四种cast转换
C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
1、const_cast
用于将const变量转为非const
2、static_cast
用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
3、dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
向上转换:指的是子类向基类的转换
向下转换:指的是基类向子类的转换
它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
4、reinterpret_cast
几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
5、为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错