C++ 笔记
Last updated on November 26, 2023 pm
[TOC]
Overview
https://www.cnblogs.com/Y1Focus/p/6707121.html
https://www.cnblogs.com/wuchanming/p/3992395.html?utm_source=tuicool&utm_medium=referral
https://blog.csdn.net/u010236550/article/details/12372319
What kind of optimization does const offer in C/C++?
微机原理
机器字长
机器字长 是指计算机进行一次整数运算所能处理的二进制数据的位数(整数运算即定点整数运算)。
机器字长也就是运算器进行定点数运算的字长,通常也是CPU内部数据通路的宽度。现在一般为32位即4个字节,也有64位和16位的。
算术类型的存储空间按照机器而定。一般,short类型为半个机器字长,int为一个机器字长,long为1或2个机器字长,float为一个机器字长,double为两个字长,long double用3或4个字长。C++标准规定的是每个算术类型的最小存储空间,但其并不阻止编译器用更大的存储空间。
如果要保证移植性,尽量用int16 int32 __int64吧,或者自己typedef int INT32一下。
字节序(大端小端)
对于一个由2个字节组成的16位整数,在内存中存储这两个字节有两种方法:一种是将低序字节存储在起始地址,这称为 小端(little-endian)字节序;另一种方法是将高序字节存储在起始地址,这称为 大端(big-endian)字节序。
- 大端:起始地址 存储 高字节
- 小端:起始地址 存储 低字节
基于X86平台的PC机是 小端字节序,而有的嵌入式平台则是 大端字节序。
C++基础
生命周期
全局对象在main开始前被创建,main退出后被销毁
静态对象在第一次进行作用域时被创建,在main退出后被销毁(若程序不进入其作用域,则不会被创建)
局部对象在进入作用域时被创建,在退出作用域时被销毁;作用域由{}定义,可以用构造函数和析构函数来追踪对象的生命周期
c++中的new和c中的malloc类似,如果你不delete掉这段申请的内存的话,它会一直存在直到进程结束后系统会回收掉这段资源;而如果你delete掉这段申请的内存,则这段申请到的内存的生命周期为从你new(申请一段内存)到你delete(释放掉这段内存)这段时间
内存分配
字节(内存)对齐、结构体对齐
有效对齐值N,即 数据自身对齐值 和 指定对齐值 中 小 的那个值,字节对齐 即 数据的存放的起始地址 满足:”存放起始地址%N=0”。
字节对齐有什么作用?
- 字节对齐的作用不仅是便于cpu快速访问,同时合理的利用字节对齐可以有效地节省存储空间。
- 对于32位机来说,4字节对齐能够使cpu访问速度提高,比如说一个long类型的变量,如果跨越了4字节边界存储,那么cpu要读取两次,这样效率就低了。但是在32位机中使用1字节或者2字节对齐,反而会使变量访问速度降低。所以这要考虑处理器类型,另外还得考虑编译器的类型。在vc中默认是4字节对齐的,GNU gcc 也是默认4字节对齐。
什么时候需要设置对齐
- 在设计不同CPU下的通信协议时,或者编写硬件驱动程序时寄存器的结构这两个地方都需要按一字节对齐。即使看起来本来就自然对齐的也要使其对齐,以免不同的编译器生成的代码不一样.
如果在编程的时候要考虑节约空间的话,那么我们只需要假定结构的首地址是0,然后各个变量按照上面的原则进行排列即可,基本的原则就是
- 把结构中的变量按照 类型大小从小到大声明,尽量减少中间的填补空间.
还有一种就是为了以空间换取时间的效率,我们显示的进行填补空间进行对齐,比如:有一种使用空间换时间做 法是显式的插入
reserved
成员, reserved成员对我们的程序没有什么意义,它只是起到填补空间以达到字节对齐的目的,当然即使不加这个成员通常编译器也会给我们自动填补对齐,我们自己加上它只是起到显式的提醒作用.1
2
3
4
5struct A{
char a;
char reserved[3]; //使用空间换时间
int b;
}
内存管理机制
从Code Segment到Stack的内存地址均位于用户空间中,其地址空间由低到高。其中:
Code Segment(代码段或Text Segment):中存放着程序的机器码和只读数据,可执行指令就是从这里取得的。如果可能,系统会安排相同程序的多个运行实体共享这些实例代码。这个段在内存中一般被标记为只读,任何对该区的写操作都会导致段错误(Segmentation Fault)。
Data Segment:中存放已初始化的全局或静态变量。
BSS:中存放未初始化的全局或静态变量。
Heap(堆):堆的大小并不固定,可动态扩张或缩减。其分配由malloc()、new()等这类实时内存分配函数来实现(brk函数也是从这里分配内存)。
Stack(栈):用来存储函数调用时的临时信息,如函数调用所传递的参数、函数的返回地址、函数的局部变量等。 在程序运行时由编译器在需要的时候分配,在不需要的时候自动清除。栈内存的申请和释放遵循LIFO(先进后出)。
malloc/free和new/delete的区别
共同点:都是从堆上申请空间,并且需要手动释放
不同点:
1.malloc和free是函数,new和delete是操作符
2.malloc申请的空间不能初始化,new可以初始化
3.malloc申请空间需要手动计算空间大小并传递,new只需在后面跟上空间类型
4.malloc的返回值是void*,使用时必须强转,new不需要,因为new后跟的是空间类型
5.malloc申请空间失败会返回NULL,因此使用时必须判空,而new不需要,但new需要捕获异常
6.malloc和free只会操作空间,而new和delete会调用构造和析构函数
内存泄露
数组指针、指针数组、二级指针
1 |
|
二维数组
二维数组 与 一维指针:二维数组元素地址赋值给一维指针,对元素进行访问时需要跨越n个整型数据的长度,即
p + i*N +j == &a[i][j]
二维数组 与 指针数组和数组指针:指向二维数组时,指针数组和数组指针访问数组元素时完全相同,但函数传参时,只能用数组指针
1 |
|
数组指针
int (*p)[n]
首先()优先级高,它是一个指针,指向一个整型数组。n为数组的长度,当p+1时需要跨越n个整型数据的长度,通常用来表示二维数组及二维数组的函数传参。是一个指针变量,占有内存中一个指针的存储空间
1 |
|
指针数组
int *p[n]
首先[]
优先级高,它是一个数组,前面为int*
,表示数组的元素为整型指针,也可表示为 二级指针int **p
, 这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a
;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量,可以用来存放变量地址。- 是多个指针变量,以数组的形式存储在内存中,占有多个指针的存储空间
1 |
|
智能指针
- 使用C++11解决内存泄露: C++11提供了智能指针,使用智能指针后不需要用户自己释放内存空间,一旦使用时对象超出了自己的生命周期,就会进行自动释放,从而有效解决了内存泄露的问题。
shared_ptr
std::shared_ptr
使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。
unique_ptr
独享所有权的智能指针
同一时间只能有一个智能指针对象指向某个内存
unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过
std::move
来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了如果希望只有一个智能指针管理资源或管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr
weak_ptr
弱引用的智能指针weak_ptr是用来监视shared_ptr的,不会使引用计数加一,它不管理shared_ptr内部的指针,主要是为了监视shared_ptr的生命周期,更像是shared_ptr的一个助手
weak_ptr没有重载运算符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中关联的资源是否存在
weak_ptr还可以用来返回this指针和解决循环引用的问题
1 |
|
1 |
|
类型转换
https://blog.csdn.net/tiandao2009/article/details/79842006
- 指针 Child to Base
- static_cast: 成功(Good)
- dynamic_cast: 成功,但有 运行检查 开销
- 指针 Base to Child
- static_cast: 允许,但 将返回一个指向不完整对象的指针,对这样一个指针的重新引用会导致运行时错误
- dynamic_cast: 允许,但有 运行检查 开销 (Good)
dynamic_cast
主要用于执行“安全的向下转型(safe downcasting)”,也就是说,要确定一个对象是否是一个继承体系中的一个特定类型。它是唯一不能用旧风格语法执行的强制转型,也是唯一可能有重大运行时代价的强制转型。
static_cast
可以被用于强制隐型转换(例如,non-const 对象转型为 const 对象,int 转型为 double,等等),它还可以用于很多这样的转换的反向转换(例如,void* 指针转型为有类型指针,基类指针转型为派生类指针),但是它不能将一个 const 对象转型为 non-const 对象(只有 const_cast 能做到),它最接近于C-style的转换。
const_cast
一般用于强制消除对象的常量性。它是唯一能做到这一点的 C++ 风格的强制转型。
reinterpret_cast
是特意用于底层的强制转型,导致实现依赖(implementation-dependent)(就是说,不可移植)的结果,例如,将一个指针转型为一个整数。这样的强制转型在底层代码以外应该极为罕见。
const_cast
主要用于添加或删除变量的const修饰符,主要是在有一个函数接受一个非常量指针参数时使用
C++类
const 修饰成员函数
- const在函数后面: 表示函数不可以修改这个类的成员变量
- const在函数前面: 用于描述返回值,表示返回一个常量
重载、继承、覆盖、隐藏
https://www.cnblogs.com/DannyShi/p/4593735.html
重载 overload
- 同一个类中
- 函数名字相同
- 函数参数必须不同
继承
覆盖(重写)override
- 不同的作用域(非别位于派生类和基类中)
- 函数名称相同,参数列表、返回值完全相同
- 基类函数必须是虚函数
- C++ 11中新增了final关键字,final修饰的虚函数不能被被重写
- 静态方法不能被重写,也就是static和virtual不能同时使用
隐藏(重定义)redefining
- 指派生类的成员函数遮蔽了与其同名的基类成员函数
- 函数名称相同,参数列表不同
- 不论有无virtual关键字
- 函数名称相同,参数列表相同
- 基类函数没有virtual关键字
虚函数 & 多态
纯虚函数是虚函数再加上= 0,抽象类是指包括至少一个纯虚函数的类
多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性
多态用虚函数来实现,结合 动态绑定
虚函数表
- 存在虚函数的类都有一个 一维的虚函数表 叫做 虚表。类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
ref:
https://zhuanlan.zhihu.com/p/41309205
https://blog.csdn.net/lixungogogo/article/details/51152214
析构函数 与 虚析构函数
一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。
虚析构函数:只有当一个类被定义为基类的时候,才会把析构函数写成虚析构函数。
如果我们不需要使用基类对派生类的对象操作时,我们也不必去定义虚析构函数,这样会增加系统的内存开销,当类里面有虚析构函数时,系统会为当前类分配一个虚函数表,里面存放虚函数指针,这样就会增加类的存储空间。
防止内存泄露,定义一个基类的指针p,在delete p时,如果基类的析构函数是虚函数,这时只会看p所赋值的对象,如果p赋值的对象是派生类的对象,就会调用派生类的析构函数;如果p赋值的对象是基类的对象,就会调用基类的析构函数,这样就不会造成内存泄露。
如果基类的析构函数不是虚函数,在delete p时,调用析构函数时,只会看指针的数据类型,而不会去看赋值的对象,这样就会造成内存泄露。
类中的默认函数
默认构造函数
默认析构函数
拷贝构造函数
拷贝赋值函数
移动构造函数
移动拷贝函数
拷贝构造函数
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。
如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。
1 |
|
浅拷贝与深拷贝
ref: https://cosmosning.github.io/2020/05/18/c-zhong-de-qian-kao-bei-he-shen-kao-bei/
浅拷贝: 会共享引用数据类型成员变量(指针指向同一个地址),而不共享原始数据类型的成员变量
深拷贝: 不会共享引用数据类型成员变量(它们的指针指向不同地址,但是拷贝后指针指向地址所存储的值是相等的),也不共享原始数据类型的成员变量
- 在 C++ 中可以自定义 复制构造函数、重载赋值运算符,实现深拷贝
浅拷贝
在 C++ 中,默认对象之间的拷贝(包括默认复制构造函数和默认赋值语句)是浅拷贝。
1 |
|
深拷贝
在 C++ 中可以自定义复制构造函数、重载赋值运算符,实现深拷贝。
1 |
|
C++11 (14, 17)
C++内置数组和array
array是C++11中新提出来的容器类型,与内置数组相比,array是一种更容易使用,更加安全的数组类型,可以用来替代内置数组。
array是数组的升级版,将数组正式纳入到容器的范畴。array在使用和性能上都要强于内置数组,对于一些固定大小的使用场景,可以用array来替代原先数组的工作。
- 数组、vector、array 在内存中都是使用连续内存
lambda
C++11的一大亮点就是引入了Lambda表达式。利用Lambda表达式,可以方便的定义和创建匿名函数。
1 |
|
在C++11之前,我们使用STL的sort函数,需要提供一个谓词函数。如果使用C++11的Lambda表达式,我们只需要传入一个匿名函数即可,方便简洁,而且代码的可读性也比旧式的做法好多了。
e.g.:
1 |
|
捕获外部变量
1 |
|
STL
vector
内存分配
1 |
|
- 对于
std::vector<T> vec;
vec在栈上(stack),而其中的元素T保存在堆上(heap); - 对于
std::vector<T>* Vec = new std::vector<T>();
vec和其中的元素T都保存在堆上; - 对于
std::vector<T*> vec;
vec在栈上(stack),而其中的元素T保存在堆上(heap);和第一种情况类似。
resize vs reserve
reserve
是容器预留空间,但并不真正创建元素对象,在创建对象之前,不能引用容器内的元素,因此当加入新的元素时,需要用push_back()
orinsert()
函数。resize
是改变容器的大小,并且创建对象,因此,调用这个函数之后,就可以引用容器内的对象了,因此当加入新的元素时,用operator[]
操作符,或者用迭代器来引用元素对象。
再者,两个函数的形式是有区别的,reserve
函数之后一个参数,即需要预留的容器的空间;resize
函数可以有两个参数,第一个参数是容器新的大小,第二个参数是要加入容器中的新元素,如果这个参数被省略,那么就调用元素对象的默认构造函数。
设计模式
https://www.cnblogs.com/wanggary/category/294620.html
https://www.cnblogs.com/cbf4life/tag/设计模式/