基础数据类型
1、ASCII是8bit,Unicode是16bit。
2、计算机存储小数时使用的是浮点数(floating-point numbers)。
符号位:1位,表示数值的正负。
指数位:8位,用于存储指数部分,采用偏移表示法。
尾数位:23位,也称为有效数字或小数部分,用于存储数值的精度。
单精度浮点数的有效数字精度约为6-7位。
FP16是1位符号位、5位指数位和10位尾数位。BF16是1位符号位、8位指数位和7位指数位。BF16比FP16表达的范围更广,但是精确度低。
定点数可能表示整数也可能表示小数。表示小数时就是将浮点数根据小数点位置(如小数点位于第k位和第k+1位,缩放因子就是2的k次方)扩大缩放因子倍后再取整得到的数据。
int16 a = 10;
float b = (float)a;
b = 10.0。
Int16转浮点型时,是将原始值直接转化为浮点类型,值大小基本不变。
但是
int16 a[2] = {0x1234, 0x5678};
Float* b = (float*)a;
*b = 0x12345678;
通过指针转化时,*b会从a地址起读取4个字节的内存数据,会导致数完全不同。
3、auto主要用于for(auto a:vect)循环访问容器变量和auto a = func()声明返回值类型。
4、size_t
主要用于表示数组、内存块和对象的大小。由于在不同平台位宽不同,可移植性较强,同时可以避免比较时类型不匹配。
5、decltype(a*b) c 将c的数据类型声明为和a*b一样的数据类型。
6、具体触发隐式类型转换的情况包括:
-
赋值操作:当将一个类型的值赋给另一个类型的变量时,可能会发生类型转换。
-
表达式操作:当表达式中涉及到不同类型的操作数时,编译器会根据隐式类型转换规则进行必要的转换。
-
函数调用:当调用函数时,实参的类型可能需要转换为形参的类型,这同样可以通过隐式类型转换或显式类型转换来完成。
-
算术运算:不同类型的算术运算可能需要将操作数转换为同一类型,以便进行运算。
-
类继承关系中的多态:使用基类指针或引用调用派生类对象的成员函数时,会进行派生类到基类的类型转换。
显式数据类型转化:静态转化(static_cast):基本数据类型转化;动态转化(dynamic_cast):有继承关系的相互转化;重解释转化(reinterpret_cast):任何类型转化,只简单进行二进制位重解释(安全性较低);常量转化(const_cast):非常量到常量的转化。
总之,类型转换在C++中是一个常见且重要的概念,编译器会根据需要自动进行隐式类型转换,而显式类型转换则由程序员明确指定。
7、用static声明局部变量时,使变量成为静态的局部变量,改变变量的生命周期,即编译时就为变量分配内存,直到程序退出才释放存储单元。由static修饰的全局变量和函数其作用域被限制在本文件内,无法通过extern在其他文件中引用。
8、volatile uint a 可以保证编译器每次都从内存中获取a的最新值,而不是编译器中的缓存值。
9、引用只能被赋值,不能被操作,如加减乘除。
数据结构
1、联合体内存共享,size和最大成员size相同。enum_x {a,b,c};
2、在现代C++中,更推荐使用 struct A {}
的方式定义结构体,因为它更加直观和符合C++的风格。结构体存储原则:原则一:结构体中元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每一个元素放置到内存中时,它都会认为内存是以它自己的大小来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始(以结构体变量首地址为0计算)。原则二:在经过第一原则分析后,检查计算出的存储单元是否为所有元素中最宽的元素的长度的整数倍,是,则结束;若不是,则补齐为它的整数倍。
结构体中可以通过构造函数初始化结构体的成员变量。
struct FinTable {
uint64_t fin0_c : 32;
uint64_t fin1_c : 32;
FinTable() { fin0_c = 0; fin1_c = 0; }
};
如果联合体包含带有构造函数的结构体,则需要在联合体中通过联合体的初始化列表FinTableUnion:fin_table() 调用结构体的构造函数FinTable,确保结构体中的变量被初始化。
union FinTableUnion{
FinTable fin_table;
FinTableUnion:Fin_table() {}
} ;
结构体和类的区别:
1、类型不同:结构是一种值类型,而类是引用类型。值类型用于存储数据的值,引用类型用于存储对实际数据的引用。那么结构体就是当成值来使用的,类则通过引用来对实际数据操作。
2、存储不同:结构使用栈存储,而类使用堆存储。栈的空间相对较小。但是存储在栈中的数据访问效率相对较高。堆的空间相对较大。但是存储在堆中的数据的访问效率相对较低。
3、作用不同:类是反映现实事物的一种抽象,而结构体的作用只是一种包含了具体不同类别数据的一种包装,结构体不具备类的继承多态特性。
4、初始化不同:类可以在声明的时候初始化,结构不能在声明的时候初始化(不能在结构中初始化字段),否则报错。
Struct A{
Int s;
}
A a; a.s;
Struct A{
Static int s;
}
A::s
::访问的是结构体中的静态成员,静态成员也可以赋值。
3、枚举
enum A
{
a = 1,
b = 2
};
4、在计算机编程里,堆和栈是内存管理中的两个重要概念,二者存在诸多区别:
内存分配方式
- 栈:由操作系统自动进行分配和释放。在函数调用时,系统会自动为函数的局部变量、参数等在栈上分配内存;当函数执行结束,这些内存会被自动释放。
- 堆:由程序员手动进行内存的分配和释放。在程序运行过程中,若需要动态分配内存,就可使用特定的函数(如在 C 语言里是
malloc
、calloc
等,在 C++ 中是new
运算符)在堆上申请内存;使用完毕后,要使用相应的函数(C 语言里是free
,C++ 中是delete
运算符)释放内存。
内存空间的大小
- 栈:内存空间相对较小。栈的大小一般在编译时就已确定,不同操作系统和编译器对栈的大小有不同的限制,通常为几兆字节。
- 堆:内存空间较大。堆的大小受限于系统的物理内存和虚拟内存,只要系统还有可用的内存,就可以在堆上进行内存分配。
数据存储的特点
- 栈:数据存储遵循后进先出(LIFO)的原则。新的数据会被压入栈顶,而在释放时,会从栈顶开始依次释放。这种特性使得栈的操作效率较高,因为只需移动栈指针即可完成内存的分配和释放。栈指针是从高地址向低地址增长的。
- 堆:数据存储是无序的。在堆上分配的内存块可以在任意位置,并且这些内存块之间可能存在碎片。这就导致在堆上进行内存分配和释放时,需要进行复杂的内存管理操作,以确保内存的有效利用。
数据访问的效率
- 栈:数据访问效率高。由于栈上的数据存储是连续的,并且栈指针的移动非常快速,因此对栈上数据的访问速度很快。
- 堆:数据访问效率相对较低。在堆上进行内存分配时,需要进行内存的查找和管理,这会增加额外的开销;而且堆上的数据存储可能不连续,这也会影响数据的访问速度。
数据的生命周期
- 栈:数据的生命周期与函数的调用和返回密切相关。函数内的局部变量在函数调用时被创建,存储在栈上;当函数执行结束,这些变量的内存会被自动释放,其生命周期也就结束了。
- 堆:数据的生命周期由程序员控制。通过手动分配的堆内存,在没有被显式释放之前,会一直存在于内存中,即使分配该内存的函数已经执行结束。
内存碎片问题
- 栈:一般不会产生内存碎片。因为栈的内存分配和释放是按照后进先出的顺序进行的,内存的使用是连续的,不会出现内存空间不连续的情况。
- 堆:容易产生内存碎片。由于堆上的内存分配和释放是随机的,多次分配和释放内存后,会导致内存空间被分割成许多小块,这些小块之间可能存在无法利用的空闲内存,从而产生内存碎片。
存储结构
1、queue的相关用法:
#include<queue>
queue<类型> que;
que.empty():判断队列是否为空,如果队列空则返回真。
que.push(元素或者结构体):在末尾加入一个元素
que.front():返回第一个元素
que.back():返回最后一个元素
que.pop():删除第一个元素
que.size():返回队列中元素的个数
2、vector是大小可变的数组,采用连续的存储空间,可以采用下标对vector的元素进行访问,和数组一样高效,但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。
用法:声明:vector<int> vec;
插入:末尾添加元素: vec.push_back();
任意位置插入元素: vec.insert();
任意位置删除元素: vec.erase(); 删除首元素:vec.erase(vec.begin()); vec.pop_front();
获取任意位置元素: vec.at(n);
c.erase(b,e):从c中删除迭代器对b和e所表示的范围中的元素.
迭代器:vector<int>::iterator it;
for (it = vec.begin(); it != vec.end(); it++)
也可以通过for(int num:vec)遍历vec中的元素。
vec.reserve(M)为vec提前申请了M大小的内存空间,但是不改变vec的size,也不初始化vec中的元素。vec.resize(M)修改了vec的size,同时初始化了vec中的元素为0。
vec.push_back(func())会把func函数返回的临时对象赋值给移动构造函数创造的对象后压入vec中。当需要把返回值压入vector中时,适合用explace_back()函数。
sort(vec.begin(),vec.end(),func)会按照func中的规则对vec中的元素进行排序。
vector<int> a(2);初始化vector大小。
4、array和数组的元素个数,在声明时都已经固定。array更安全,有成员函数fill()用于初始化。用法:std::array<int8_t, num> ram1_ = {0};ram1_.data()传递地址,ram[i]访问元素。
5、list:双向链表,是一种常见的线性数据结构,相比于queue,可以在中间通过insert插入和erase删除元素。双向链表是单链表的扩展,每个节点除了有一个指向下一节点的指针外,还有一个指向前一个节点的指针。循环链表则是一种链表中最后一个节点的指针指向第一个节点,形成一个环的结构。这两种链表结构在某些应用场景下可以提供更高效的遍历和操作。链表的性能分析通常涉及插入、删除和查找操作的时间复杂度。链表的插入和删除操作通常具有O(1)的时间复杂度,但查找操作的时间复杂度为O(n)。优化链表性能的方法包括使用双向链表和循环链表等。
std::list<int> test_list;
auto it = test_list.find(test_list.begin(), test_list.end(),3);//查找value为3的元素
test_list.insert(it,5);//在元素3之前插入5
list常见函数:empty();insert();push_back();erase();front();end();at();find()。
5、树是一种非常重要的非线性数据结构,它是由节点(或称作顶点)组成,每个节点有零个或多个子节点,并且没有形成闭环的路径。树的分类有很多种,包括普通树、二叉树、平衡树(如AVL树)、堆树等。二叉树是每个节点最多有两个子节点的树结构。它是树结构中的一种特殊情况,具有很多独特的性质和操作方法。二叉树的遍历是按照一定的顺序访问树中的所有节点,常见的遍历方法包括前序遍历、中序遍历和后序遍历。线索二叉树是对二叉树进行的一种改进,它通过在节点之间添加线索(额外的指针)来避免使用递归遍历,从而提高遍历效率。平衡二叉树(如AVL树)是一种特殊的二叉搜索树,它能够保持树的平衡,使得树的高度保持在一定的范围内,从而保证操作的高效性。
图:图是由顶点集合及顶点间的关系(边)组成的集合。图分为有向图和无向图,以及根据边的权重分为加权图和无权图。图的遍历是指按照某种顺序访问图中的所有顶点。图的搜索算法主要有深度优先搜索(DFS)和广度优先搜索(BFS)。
map:图或者平衡二叉树。
平衡二叉树:有序,O(logn)。用法:std::map<key,value> map; map[key] = a;(赋值) b = map[key](获取key对应的元素);插入相同key元素时,更新value。主要用于有序存储和范围查找。
unordered_map:无序图,基于哈希表实现,无序、O(1)<x<O(n)。用法:std::unordered_map<key,value> un_map; un_map[key] = a; b = un_map[key]。插入相同key元素时,更新value。un_map.find(key)查找key对应的value。主要用于快速查找和插入。
6、set用于存储有序键值,提供了成员函数 lower_bound
和 upper_bound
,可以方便地进行范围查找操作。key具有唯一性,插入重复key元素时会插入失败。可以用它来去掉重复元素,只保留一个key。
7、multiset是一种可以包含重复元素的有序数据结构,与普通集合(set)不同,普通集合中每个元素最多出现一次。统计元素出现次数:在需要统计某些元素出现频率的场景中,multiset非常有用。例如,统计文本中单词的出现次数,或者在一个数据集中统计各种物品的数量。维护元素多重性:当需要考虑元素重复出现的情况时,multiset提供了直接的数据结构支持。std::unordered_set
是 C++ 标准库中的无序集合容器,它基于哈希表实现,插入和查找操作的平均时间复杂度为 O(1)。当需要频繁进行查找操作,且不关心字符串的顺序时,使用 std::unordered_set
是个不错的选择。