目录
一、X86 X64
常见误解澄清
二、栈和堆的区别
三、sizeof应用
1. 对数组名使用 sizeof
2. 对指针使用 sizeof
四、结构体的内存对齐
五、自增自减底层
六、引用与指针的区别
七、函数在什么情况下能用引用的方式返回地址?
可以安全返回引用的场景
❌ 绝对不能返回引用的场景
一、X86 X64
核心区别表
| 特性 | X86 (32位) | X64 (64位) |
|---|---|---|
| 虚拟地址空间 | 4 GB (2^32) | 16 EB (2^64),实际用 256 TB |
| 物理内存支持 | 最大 4 GB | 理论上 16 EB,实际主板支持几十到几百 GB |
| 指针大小 | 4 字节 | 8 字节 |
| 通用寄存器 | 8 个 (eax, ebx, ecx, edx, esi, edi, ebp, esp) | 16 个 (rax, rbx, ... + r8-r15) |
| 寄存器宽度 | 32 位 | 64 位 |
| 单次处理数据 | 4 字节 | 8 字节 |
| 程序兼容性 | 不能在 64 位系统运行 16 位程序 | 可运行 32 位程序(通过兼容层) |
为什么X86叫32位系统,X64叫64位系统呢?
因为X86,它的地址总线是32根,最多可以访问2的32次方字节,大约是4GB,而X64是地址总线是64根,最多访问2的64次方字节就是约等于16EB
它们俩的最本质区别就是内存的寻址能力。
在32位系统中。单个程序最多使用2到3GB。 在64位系统中。支持256 TB。或者更多。
他们俩还有一个差异是数据类型大小差异。
| 数据类型 | X86 (32位) | X64 (64位) |
|---|---|---|
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long | 4 | 8(Windows: 4) |
long long | 8 | 8 |
float | 4 | 4 |
double | 8 | 8 |
void*(指针) | 4 | 8 |
size_t | 4 | 8 |
特别注意:Linux 上long是 8 字节,Windows 上long是 4 字节(保持兼容性)
常见误解澄清
64位系统一定比32位快?
不一定。指针变大导致缓存压力增加,某些程序反而变慢
但更多寄存器和 64位运算有明显优势
64位系统不能运行32位程序?
可以,通过兼容层支持
long 在64位一定是8字节?
不!Windows 上 long 仍是4字节
可移植代码应使用
int32_t、int64_t等固定大小类型
二、栈和堆的区别
1.先了解内存布局
高地址 +------------------+ | 栈 (Stack) | ← 向下增长(向低地址) | (函数调用、局部变量) | +------------------+ | ↓ | | 空闲空间 | | ↑ | +------------------+ | 堆 (Heap) | ← 向上增长(向高地址) | (动态分配) | +------------------+ | 未初始化数据段 | | (BSS Segment) | +------------------+ | 已初始化数据段 | | (Data Segment) | +------------------+ | 代码段/只读区 | | (Text Segment) | +------------------+ 低地址
堆和栈的核心区别主要在他们的管理方式,分配速度和生命周期上
栈他是有编译器自动管理,堆是由程序员手动管理malloc free或者是new,delete。
栈他的分配速度非常快,只需要移动指针。 堆的分配速度比较慢,它是因为需要找空闲块,处理碎片。
栈他的生命周期是作用域结束之后自动去释放,堆是需要去手动释放。
栈的分配方向是从高地址向低地址向下增长。堆的分配方向是从低地址向高地址向上增长。
栈他主要存放的是局部变量函数参数返回地址,堆主要存放的是动态分配的对象,数据。
栈他有可能会有一些栈溢出的越界风险,堆的话它可能会有内存泄漏,野指针等越界风险。
int * r=&a;星号代表修饰符
三、sizeof应用
编译时确定:sizeof在编译时计算,不会执行括号内的表达式
int a = 10; sizeof(a++); // a++ 不会执行,a 仍然是 10
sizeof在处理数组和指针时有着本质的区别
重点!!!!!
sizeof是在编译时决定的,它只看变量的类型,不看变量里存了什么。
1. 对数组名使用sizeof
返回:整个数组占用的总字节数(数组长度 × 单个元素大小)
int arr[10]; // 10个int,每个4字节(32位系统) printf("%zu\n", sizeof(arr)); // 40 (10 * 4) char str[20]; // 20个char,每个1字节 printf("%zu\n", sizeof(str)); // 20 float nums[5]; // 5个float,每个4字节 printf("%zu\n", sizeof(nums)); // 202. 对指针使用sizeof
返回:指针变量本身的大小(固定值,与指向什么无关)
int *p1; char *p2; double *p3; int arr[10]; int *p4 = arr; printf("%zu\n", sizeof(p1)); // 8 (64位系统) 或 4 (32位系统) printf("%zu\n", sizeof(p2)); // 8 (同上) printf("%zu\n", sizeof(p3)); // 8 (同上) printf("%zu\n", sizeof(p4)); // 8 (指针,不是数组!)关键区别图:
int arr[10]; sizeof(arr) = 40 +----+----+----+----+----+----+----+----+----+----+ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +----+----+----+----+----+----+----+----+----+----+ <------------------ 40 字节 --------------------> int *p = arr; sizeof(p) = 8 +----------+ | 地址0x100| +----------+ <-- 8 字节 -->
解释:返回的是指针变量本身的大小,因为指针变量p它指向的是arr的数组元素的首地址,所以是八个字节
因为数组首元素地址的大小是固定的,在64位系统下。是8个字节,这和数组元素类型无关。因为地址本身就是一个指针,而指针的大小是由系统架构决定,而不由指向的数据类型决定。
地址 = 内存位置的编号 就像门牌号,不管房子里住的是人还是家具,门牌号本身都是同样大小
四、结构体的内存对齐
结构体的大小往往不是各成员大小的简单相加:
struct Example1 { char c; // 1字节 int i; // 4字节 }; // sizeof = 8(不是5) struct Example2 { char c; // 1字节 char d; // 1字节 int i; // 4字节 }; // sizeof = 8(不是6) struct Example3 { char c; // 1字节 short s; // 2字节 int i; // 4字节 }; // sizeof = 8(不是7) // 内存布局示例(Example1): // +----+---+---+---+----+----+----+----+ // | c | 填充 | i | // +----+---+---+---+----+----+----+----+ // 0 1 2 3 4 5 6 7对齐规则:
每个成员按自己的对齐要求存放
结构体大小是最大对齐数的整数倍
五、自增自减底层
*p++等价于*(p++)
由于后缀自增++的优先级高于解引用*,所以先执行p++,但p++返回的是p 的原值,然后对这个原值进行解引用。
| 表达式 | 效果 | 说明 |
|---|---|---|
*p++ | 先取*p的值,然后p自增 | 最常用 |
(*p)++ | 先取*p的值,然后该值自增 | 改变指向的内容 |
*++p | 先p自增,再取*p的值 | 先移动指针再取值 |
++*p | 先取*p的值,然后该值自增 | 同(*p)++ |
寄存器底层实现
int a = 5, b; b = ++a;
初始状态: a 地址: 0x1000, 值: 5 b 地址: 0x1004, 值: 未初始化 步骤1: 将 a 的值从内存加载到 CPU LOAD R1, [0x1000] ; R1 = 5 步骤2: CPU 计算加1 ADD R1, 1 ; R1 = 6 步骤3: 将新值写回 a 的内存地址 STORE [0x1000], R1 ; a = 6 步骤4: 将新值赋值给 b STORE [0x1004], R1 ; b = 6
b = ++a;底层是三步:
从内存读取
a计算
a+1并写回a将新值写入
b
大白话说
++a:将a的值取出来,存入临时内存空间中。然后对a进行自增,再将新的值写入b,结束。(先自增,再赋值)
a++:将a的值取出来。存入临时空间中,将a的值先写入b,再对a进行自增,结束。(先赋值,再自增)
++*p是另一个常见的指针操作组合,它的含义和*p++完全不同。
++*p等价于++(*p)
由于前缀自增++和解引用*的优先级相同,且结合性是从右向左,所以先计算*p,然后对结果进行自增。
六、引用与指针的区别
语法上
在语法上指针变量存储的是某个变量的地址或者实例的地址;引用存放的是变量或者实例的别名
程序为指针变量分配内存空间;不给引用分配内存空间
指针变量存在解引用一说,读取指针变量指向实例的内容;引用不存在解引用一说,访问实例的内容直接访问即可,引用就代表实例,引用的值就是实例的值
普通的指针变量可以存储其他实例的地址,也就是说指针变量可以改变指针指向的实例;而引用一旦代表了一个实例的别名,也就是说一旦初始化就不能修改,代表别的实例的别名
指针变量可以为空;引用不能为空,不能为空引用
作为形参时指针变量要验证他的全法性,即不能传入空指针;引用不需要验证,因为不存在空引用一说
sizeof获取的是指针的大小(地址编号的大小);针对引用获取的是引用代表的实例的大小
指针存在层级,一级指针,二级指针。。。;引用不存在层级,二级引用。。。
自增自减针对指针变量是对指针指向的地址进行操作,移动到下一个实例的位置;引用进行自增自减是针对引用指向的实例进行算数自增自减操作
本质上
引用是指针的语法层概念,引用的本质就是指针
int& ra=a; int*const ra=&a;
引用是自身为常性的指针
七、函数在什么情况下能用引用的方式返回地址?
坚决不能将局部变量的地址返回
// ❌ 错误:返回局部变量的引用 int& badFunc() { int local = 10; return local; // 编译警告!local 在函数结束时销毁 } // 调用者得到一个悬空引用(野引用) int& worseFunc() { int arr[10]; return arr[0]; // 同样错误 }为什么错误?
局部变量在栈上分配,函数结束时栈帧被回收
返回的引用指向已释放的内存(未定义行为)
坚决不能将(已死亡)局部对象以引用的方式返回:解决方式就是加static
// ❌ 错误:返回临时对象的引用 int& badFunc2() { return 10; // 10 是临时量,立即销毁 } string& badFunc3() { return string("hello"); // 临时 string 被销毁 }静态变量的生命周期:
程序启动时初始化(或首次调用时)
程序结束时才销毁
地址固定不变
可以安全返回引用的场景
静态局部变量(生命周期整个程序)
全局变量
类的成员变量(确保对象生命周期)
函数参数传入的引用(直接返回)
容器元素、数组元素(确保容器存在)
*this(成员函数返回自身)
❌ 绝对不能返回引用的场景
局部变量(包括局部数组)
临时对象
函数内创建的局部对象
局部智能指针管理的对象(离开作用域就释放)