c++(7`数据大小、内存管理和分配, optional)
数据大小
不同位系统,long和指针的字节数可能不同
内存五大分区
- 内核空间:是系统内部的区域,不可直接读写
- 堆区:存放动态生命周期对象,完全由程序员控制
- 效率较低:由于new/delete会造成内存空间不连续,使程序效率降低(寻址),堆的机制是很复杂的,它比栈的效率要低
- 空间大:对于32位系统,可达2^32 = 4G
- 栈区:存储自动生命周期对象
- 效率高:执行效率很高
- 空间小:大约只有2MB
- 静态区/数据段:存储静态生命周期对象,分为BSS段(未初始化的)和DATA段(已初始化的)
- 常量区/代码段:存储程序指令、函数编译后的可执行的二进制代码、只读常量,也是静态生命周期对象
size_t
size_t是一种数据类型,是和平台相关的无符号整数类型,在32位系统上,是4字节,在64位系统上,是8字节
使用场景:表示字节数量(sizeof),对象数量(size,strlen)
优点:有跨平台优势
1
2
3
4
const size_t huge_size = sizeof(……); //内存大小
int arr[5000000000];//对象数量
for (size_t i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) { //循环计数
}
展开
假设是32位CPU,size_t表示的范围是2^32 == 0——4294967295(索引)
按字编址,地址总线的位数是32,即寻址范围共2^32字节 == 4,294,967,296 字节(个数) == 4G
一个对象字节数不可超过4,294,967,296,size_t恰好可以表示
而一个对象最小为一字节,显然最多不会超过4,294,967,296个
sizeof
1
size_t arrLength = sizeof(arr) / sizeof(arr[0]);
展开
sizeof()是运算符:
- 以类型,变量,表达式求值结果,函数作为参数
- 返回字节大小
- 编译时计算
- 当用sizeof计算C风格字符串长度时,返回的长度大小包括’\0’终止符
- 数组 元素类型字节数 * 元素数量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 对齐访问:单次内存操作
┌───┬───┬───┬───┐
│ B │ B │ B │ B │ // 一次读取4字节
└───┴───┴───┴───┘
// 非对齐访问:需要多次操作
┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐
│ │ B │ B │ B │ │ B │ │ │ │ // 两次读取+数据拼接
└───┴───┴───┴───┘ └───┴───┴───┴───┘
// 情况A:非对齐紧凑排列
地址: 0x1001 0x1002 0x1003 0x1004 | 0x1005 0x1006 0x1007 0x1008 | 0x1009 0x100A 0x100B 0x100C
读取第一个int: 需要访问0x1000-0x1003和0x1004-0x1007两个内存块
读取第二个int: 需要访问0x1004-0x1007和0x1008-0x100B两个内存块
读取第三个int: 需要访问0x1008-0x100B和0x100C-0x100F两个内存块
总内存访问: 6次
// 情况B:对齐排列(有填充)
地址: 0x1000 0x1001 0x1002 0x1003 | [填充] | 0x1008 0x1009 0x100A 0x100B | [填充] | 0x1010 0x1011 0x1012 0x1013
读取第一个int: 只需访问0x1000-0x1003(1次)
读取第二个int: 只需访问0x1008-0x100B(1次)
读取第三个int: 只需访问0x1010-0x1013(1次)
总内存访问: 3次
展开
- 内存对齐:
- 是指数据在内存中的存储地址必须是某个值的整数倍,而不是一个接一个地存放
- 字长决定一次寻址的大小(32位CPU:通常4字节,64位CPU:通常8字节),如果数据非内存对齐,就需要多次寻址,降低效率
自定义类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
struct stru { char i; //start address is 0 int m; //start address is 4 char n; //start address is 8 }; //sizeof(stu) 12 //嵌套结构体 struct stru1 //被展开的结构体 { short i; //start address is 0 struct //展开后的结构体 { char c; //start address is 4 int j; //start address is 8 } tt; int k; //start address is 12 }; //sizeof(stru1) 16 //结构体包含数组 struct array { float f; //start address is 0 char p; //start address is 4 int arr[3]; //start address is 8 }; //sizeof(array) 20
展开
- 类
- 对齐原则:
- 偏移量:成员地址相对结构体地址的偏移,成员偏移量 == 上一个成员偏移量 + 上一个成员大小
- 结构体大小等于最后一个成员的偏移量加上最后一个成员的大小,第一个成员的偏移量是0,当前成员偏移量必须是当前成员类型的整数倍(内存对齐)
- 最后结构体也要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍
- 嵌套结构体
- 展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍
- 结构体大小必须是所有成员大小的整数倍,这里所有成员计算的是展开后的成员,而不是将嵌套的结构体当做一个整体
- 结构体包含数组
- 和处理嵌套结构体一样
- 对齐原则:
- 类
strlen
1
2
char str[] = "Hello, World!";
size_t length = strlen(str);
展开
strlen(…)是函数,
- strlen(const char* str) ,参数为“……” C风格字符串,
- 返回的长度大小不包括’\0’终止符,
- 运行时计算
- 参数必须是字符型指针(char*)
- 用于返回字符串的实际长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Test1()
{
char p[] = "hello";//5,6
cout << "p: " << p << " " << strlen(p) << " " << sizeof(p) << endl;
char p1[] = "hello\0";//5,7,‘\0’是一个char
cout << "p1: " << p1 << " " << strlen(p1) << " " << sizeof(p1) << endl;
char p2[] = "hello\\0";//7,8,第一个\是转义\0的,所以它不算字符,它会让后面的\0变为普通的字符
cout << "p2: " << p2 << " " << strlen(p2) << " " << sizeof(p2) << endl;
char p3[] = "hello\\\0";//6,8,第一个\只转义了第二个\,使得第二个\不能再转义后面
cout << "p3: " << p3 << " " << strlen(p3) << " " << sizeof(p3) << endl;
char p4[] = "hel\0lo";//3,7
cout << "p4: " << p4 << " " << strlen(p4) << " " << sizeof(p4) << endl;
char p5[] = "hel\\0lo";//7,8
cout << "p5: " << p5 << " " << strlen(p5) << " " << sizeof(p5) << endl;
}
展开
注意:strlen 计算的是字符串的实际长度,遇到\0(空字符)即停止;sizeof 计算整个字符串所占内存字节数的大小,当然\0也要+1计算
memcpy
按字节内存拷贝(memory copy),仅支持POD类型
void *memcpy(void *str1, const void *str2, size_t n)
从str2所指向的内存区域复制n个字节到str1所指向的内存区域
num:
- num < src大小 数据不完整,但安全
- num > src大小 读取越界,未定义行为
- num < desc大小 正常拷贝,有剩余空间
- num > desc大小 缓冲区溢出,可能崩溃
当拷贝C风格字符串,需要strlen+1 / sizeof,否则不包含\0,在使用字符串时将出现错误
使用场景:快速将 基本数据类型/数组/POD结构体/C风格字符串/内存块 复制
memset
void *memset(void *str, int c, size_t n)
将一段内存区域设置为指定的值,通常用于初始化数组或结构体,它按字节赋值
将str所指向的内存区域的前n个字节,设置为c
由于按字节赋值,常用的c值:0/-1(结果和c值一样),0x3f设置为无穷大
使用场景:将数组/POD结构体 初始化为0/1/无穷大
allocator
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <memory>
std::allocator<int> alloc;
int* ptr = alloc.allocate(5); // 分配5个int的空间
for (int i = 0; i < 5; i++) {// 在指定位置构造对象
alloc.construct(ptr + i, i * 10);
}
for (int i = 0; i < 5; i++) {// 销毁对象(不释放内存)
alloc.destroy(ptr + i);
}
alloc.deallocate(ptr, 5);// 释放内存
MyClass* memory = alloc.allocate(1);
alloc.construct(memory, MyClass());
展开
分配器类模板:提供高级管理内存的方式,将内存分配和构造分离开来
malloc、free、calloc、realloc
1
2
3
4
5
void* malloc(size_t size);
int* p = (int*)malloc(40);
void* memory = malloc(sizeof(MyClass));
MyClass* obj = new(memory) MyClass(42, "hello");//定位new
展开
malloc:C 风格内存分配,在堆上,如果开辟成功,则返回内存指针,如果失败,返回nullptr指针
参数是字节数量,返回无类型的指针
在使用时应强制转换为具体类型
普通new:内存分配和构造,绑定在一起
定位new:不会分配新内存,它只是在已经存在的内存地址上调用构造函数
1
2
3
void free (void* ptr);
free(p);//避免内存泄漏
p = NULL;//避免空悬指针
展开
需要用free释放内存,参数是指针,指向的必须要是动态分配的内存
1
void* calloc(size_t num, size_t size);
展开
calloc:为num个size字节的元素开辟一块空间,并且把空间的每个字节初始化为0
1
void* realloc(void* ptr, size_t size);
展开
realloc: 对动态开辟内存大小的调整,ptr指向被调整的旧内存起始地址,size调整后的新大小,返回调整后的新内存起始地址
原有空间之后有足够空间开辟新空间,那么紧挨着开辟,并返回旧地址
原有空间之后没有足够空间开辟新空间,那么开辟新的空间,拷贝数据,释放旧空间,返回新地址
optional
- 作用:
- 我们会遇到空值的情况:容器没有任何元素,指针没有指向任何对象,类对象没有初始成员为有效值
- 对于这些情况,通常会写额外的代码来判断其有效性
- 特殊值:-1,INT_MAX……但是这些值可能也有意义(数据规定未知)
- 函数返回值:bool(true/false)/ 指针(有值/nullptr) 来表示结果是否有意义,但这样比较隐晦,可能忘记判断
- try-catch块来处理,这样让代码冗余,降低可读性
- std::optional<T>解决这类问题(它不具有值含义只会表示是否有效,optional作为返回值包装更明确,不会太多冗余),可以理解为存储对象的包装器
- 使用场景:表示一个可能不存在的值
1
2
3
4
5
6
7
8
9
10
11
12
13
//空初始化(存储对象没有有效值)
std::optional<int> emptyInt;//(对象,左值)
std::optional<double> emptyDouble = std::nullopt;//空值
//使用值来初始化
std::optional<int> intOpt{10};
//auto推断
std::optional intOptDeduced{10.0};
//使用make_optional
auto doubleOpt = std::make_optional(10.0);
//使用in_place占位符
std::optional<std::vector<int>> vectorOpt{std::in_place, {1, 2, 3}};
//其它optional对象
auto optCopied = vectorOpt;
展开
- 初始化方式:
- 转换构造函数(模板成员函数 + 万能引用)(不属于特殊成员函数)
- 如果一个函数有拷贝构造和移动构造,相当于重载了多个版本,区分调用谁根据初始值,如果是右值,则调用移动构造,如果是左值调用拷贝构造
- 转换构造函数构造时允许类型转换(当U可以转换为T时),对于拷贝构造和移动构造只能接受相同类型(optional<T>),并且不再需要构造对象(参数中临时构造也算)和拷贝构造的两步操作,而变为了直接调用转换构造
有时需要in_place / make_optional 对optional对象构造
1 2 3 4
std::optional<tSampleClass> sample{tSampleClass()}; std::optional<tSampleClass> opt{std::in_place}; auto opt = std::make_optional<tSampleClass>();
展开
- optional的存储对象, 需要调用构造函数 / 构造函数有个多个参数时
- 第一种
- tSampleClass()构造一个临时对象(右值)
- 由于tSampleClass&&类型和optional<tSampleClass>类型不直接匹配,因此首先调用转换构造,U&&被推导为U&&(左值)
- move后调用移动构造(因为tSampleClass()构造一个临时对象,我们不会再使用它,资源转移比拷贝的性能要好)
- 第二种(性能更好)
- 直接在optional内部构造,不需要创建临时对象,不需要调用转换构造,不需要移动构造
- 第一种
- 存储对象不支持拷贝和移动(=delete的):
- 第一种方式,需要对对象移动资源的操作,可能为new (storage) tSampleClass(std::move(value));这样形式,由于不支持移动,因此发生错误
- optional的存储对象, 需要调用构造函数 / 构造函数有个多个参数时
- 转换构造函数(模板成员函数 + 万能引用)(不属于特殊成员函数)
- 操作:
- 作为条件语句的表达式,可以隐式转换为boolean类型,表示当前是否有有效值
- 作为返回值:如果函数失败,返回nullopt表示没有有效返回值,否则返回计算值
- 像指针一样:解引用:返回存储对象的引用,箭头运算符:访问存储对象的成员
- value:返回存储对象的值
- value_or(defaultValue)有有效值时返回有效值,否则返回默认值
- 修改存储对象值:std::optional<tStudent> optStudent;构造空的opt没有存储对象,optStudent.emplace(“Bob”);构造存储对象,相当于optStudent = tStudent{“Bob”};,optStudent.emplace(“Steve”)”Bob”对象析构,构造”Steve”
- 销毁存储对象:reset
- 允许使用关系运算符,比较的是存储对象的值
- optional对象比原始对象将占有更多的内存空间



