C语言数据结构—数组(cpu内存与指针)
此文章讲述了数组的基本原理,数组与内存间的操作,内存与指针的操作,指针内存数组三者之间的操作。有很多地方还未很完善,写此,日后补。
目录
一:一维数组(二维待续.....)
1. 索引(为什么数组要从下标0开始?)
1.1 数组索引地址示例
1.2 数组与数组指针的等价性
2. 数组为何可以通过索引来随机访问元素
3. 初始化
4. 数组在内存中的存储
4.1 内存分配规则
4.2 内存布局示例
4.3 数组名本质
4.4 访问越界
5. 数组的遍历
6. 字符串数组 (char)
二:cpu与内存(运行)
1. 三线
2. cpu读写流程
2.1 关键硬件总线的作用
2.2 核心步骤
2. 3 内存地址与数据存储示例
3. 系统与内存地址
3.1、32位与64位系统的核心区别
3.2. 地址总线与内存地址范围的关系
3.2.1. 地址总线的作用
3.2.2. 示例
3.2.3 内存地址的本质
三:初始指针
1. 指针与指针变量
1.1 指针
1.2 指针变量
1. 指针变量的本质
2. 指针变量的内存模型
3. 指针的指针(双重指针)
4. 我之前的误解
5. 总结
2. 指针的运算
3. 特殊指针
1. 野指针
2. 空指针
3. 野指针与空指针的对比
4. 数组的指针
5. 指针数组
6.数组的指针与指针数组总结:
一:一维数组(二维待续.....)
在c语言中,数组可以存储一个固定大小的相同数据元素的顺序集合,数组是用来存储一系列数据,但是往往被认为是一系列相同元素的变量。
1. 索引(为什么数组要从下标0开始?)
1. 数组在内存中是连续存储的,每个元素占用的内存大小相同。
2. 索引从0开始的设计,使得计算元素地址的公式可以简化为:元素地址 = 基地址 + 索引 × 元素大小
3. 在C语言中,数组名本质是指向首元素的指针(
arr
等价于&arr[0]
),索引操作arr[i]
等价于*(arr + i)
。4. 在C语言中,数组的 基地址(Base Address) 是数组第一个元素(即索引为0的元素)的地址,可通过
&arr[0]
或直接使用数组名arr
获取
1.1 数组索引地址示例
int arr[3] = {10, 20, 30};
// 内存布局(假设基地址为0x1000,int占4字节):
// arr[0] → 0x1000 + 0*4 = 0x1000
// arr[1] → 0x1000 + 1*4 = 0x1004
// arr[2] → 0x1000 + 2*4 = 0x1008
1.2 数组与数组指针的等价性
int arr[3] = {10, 20, 30};
int *p = arr; // p指向arr[0]
printf("%d\n", p[1]); // 输出20
printf("%d\n", *(p+1)); // 输出20(等价于p[1])
2. 数组为何可以通过索引来随机访问元素
3. 初始化
此处的知识点:
1. sizeof(array)返回的是字节数,(sizeof(array) / sizeof(array[0]))返回的元素个数。
2. define指的是宏定义,相当于一个全局的变量,length是宏定义,可以理解为一个函数。
3. 全局数组默认有初始值,必须初始化。
4. 初始化,详细看代码。
#include <stdio.h>//宏表达式
#define LENGTH(array) (sizeof(array)/sizeof(array[0]))//全局数组
int my_array[5];//声明未初始化,全局数组 元素有默认值
int you_array[3]={5,7,9};//声明并给元素赋值,初始化int main(int argc, char const *argv[])
{//数组遍历for (int i = 0; i < LENGTH(you_array); i++){printf("you_array[%d]=%d ",i,you_array[i]);}printf("\n");for (int i = 0; i < LENGTH(my_array); i++){printf("my_array[%d]=%d ",i,my_array[i]);}printf("\n");//1.完全初始化,给每一个元素都赋值//局部数组,是没有默认值的,必须要初始化防止脏数据double she_array[5]={9.9,7.7,8.8,6.6,3.3};for (int i = 0; i < LENGTH(she_array); i++){printf("she_array[%d]=%lf ",i,she_array[i]);}printf("\n");//2.未完全初始化double he_array[10]={1.1,2.2,3.3};for (int i = 0; i < LENGTH(he_array); i++){printf("he_array[%d]=%lf ",i,he_array[i]);}printf("\n");//3.省略数组大小,数组大小由初始化确定double our_array[]={1.1,2.2,4.2,5.6,5.7,5.8,5.9,5.8};for (int i = 0; i < LENGTH(our_array); i++){printf("our_array[%d]=%lf ",i,our_array[i]);}printf("\n");return 0;
}
4. 数组在内存中的存储
数组是 连续内存块 中存储的相同类型元素的集合,其内存模型是理解C语言内存操作的核心基础。
4.1 内存分配规则
特性 描述 连续存储 所有元素在内存中按顺序连续存放(逻辑相邻 ↔ 物理相邻) 固定大小 数组长度在声明时确定,无法动态扩展(静态数组) 元素类型一致 所有元素类型相同,占用相同内存大小(如 int
占4字节,char
占1字节)4.2 内存布局示例
一维数组:
int arr[3] = {10, 20, 30};
int arr[3] = {10, 20, 30};. /* 地址:0x1000 0x1004 0x1008 值: 10 20 30*/
4.3 数组名本质
数组名是指向基地址的常量指针:
int arr[3]; arr == &arr[0]; // 成立,数组名是首元素地址
4.4 访问越界
未定义行为:访问超出数组范围的索引(如
arr[5]
),可能导致程序崩溃或数据污染。示例:
int arr[3] = {1, 2, 3}; arr[5] = 10; // 可能覆盖其他变量的内存!
5. 数组的遍历
与java不同,sizeof是返回的字节数,所以使用宏定义一个返回数,
#define LENGTH(array) (sizeof(array) / sizeof(array[0]))
#include <stdio.h>// 宏表达式
//sizeof(array)返回的是字节数,(sizeof(array) / sizeof(array[0]))返回的元素个数
//define指的是宏定义,相当于一个全局的变量,length是宏定义,可以理解为一个函数
#define LENGTH(array) (sizeof(array) / sizeof(array[0]))// 全局数组,默认有初始值
int my_array[5] = {0, 1, 2, 3, 4};
int main(int argc, char const *argv[])
{// sizeof 返回的是字节数for (int i = 0; i < LENGTH(my_array); i++){printf("%d\n", my_array[i]);}return 0;
}
6. 字符串数组 (char)
1. #include <string.h>String标准库,包含了string的很多操作,可以瞅瞅
C 标准库 – <string.h> | 菜鸟教程
#include <stdio.h>
#include <string.h>
//宏表达式
#define LENGTH(array) (sizeof(array)/sizeof(array[0]))/*
在C语言中,字符串实际上是使用空字符 \0 结尾的一维字符数组,\0标记字符串结束,但是\0并不是空字符,\0被称为结束符
*/int main(int argc, char const *argv[])
{//1.char数组 char cs[5]={'a','b','c','d','e'};for (int i = 0; i < LENGTH(cs); i++){printf("cs[%d]=%c ",i,cs[i]);}printf("\n");//2.字符串char ds[5]={'a','b','c','d','\0'};//\0结束符//专门打印字符串 stringprintf("%s\n",ds);//3.字符串 stringchar es[]="askdhjkjashdlkhklajshdkjhiasdichikchkljchkljh";//自动在末尾添加\0 结束printf("%s\n",es);//string.h 头文件,提供了操作字符串的函数和变量/*string .h 头文件定义了一个变量类型、一个宏和各种操作字符数组的函数。<string.h> 是 C 标准库中的一个头文件,提供了一组用于处理字符串和内存块的函数。这些函数涵盖了字符串复制、连接、比较、搜索和内存操作等。*//*size_t strlen(const char *str)计算字符串 str 的长度,直到空结束字符,但不包括空结束字符。*/unsigned long size=strlen(es);printf("size=%lu,length=%lu\n",size,LENGTH(es));//45 46/*char *strcpy(char *dest, const char *src)把 src 所指向的字符串复制到 dest。*/char fs[10]="jjj";char gs[50]="abcdefghijklmn";//strcpy(fs,gs);//专门字符串函数puts(fs);puts(gs);/*char *strcat(char *dest, const char *src)把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。*/strcat(fs,gs);puts(fs);puts(gs);printf("size=%lu,length=%lu\n",strlen(fs),LENGTH(fs));return 0;
}
二:cpu与内存(运行)
1. 三线
在计算机中,cpu不能直接与磁盘交互,只能通过三线进行与内存交互
1. 地址总线
地址总线的宽度决定了cpu的寻址能力(64为每次可以读8个字节,32则为4byte)
2. 数据总线
控制总线,决定了cpu对其控件的控制能力以及控制方式;
3. 控制线
数据总线的宽度决定了,每次传输的数据量(64 位总线一次可传输 8 字节)。
2. cpu读写流程
2.1 关键硬件总线的作用
cpu在内存中读取一个int3,操作流程
1. cpu通过地址总线,在内存中找到数据3的地址;(地址总线)
2. 然后通过控制总线知道该操作是读还是写;(控制总线)
3. 通过数据总线,把数据3传输到cpu中; (数据总线)
总线类型 功能 示例参数 地址总线 传输目标内存地址(决定 CPU 寻址能力) 32 位总线 → 4GB 寻址 数据总线 传输实际数据(决定单次传输数据量) 64 位总线 → 8字节/次 控制总线 传输控制信号(读/写、中断、时钟同步等) Read/Write 信号 2.2 核心步骤
地址总线定位内存地址
CPU 通过 地址总线 发送目标数据的 内存地址(假设数据
3
存储在地址0x1000
)。地址总线宽度(如 32 位)决定了 CPU 可寻址的内存范围(32 位地址总线 → 最大 4GB 内存)。
控制总线发送读操作信号
CPU 通过 控制总线 发送 读信号(Read Signal),通知内存控制器当前操作为读取数据。
内存控制器响应请求
内存控制器根据地址总线的地址定位到物理内存位置,并将数据从存储单元中取出。
数据总线传输数据到 CPU
内存通过 数据总线 将数据
3
(假设为int
类型,占 4 字节)传输到 CPU 的寄存器(如EAX
)。数据总线宽度(如 64 位)决定了每次传输的数据量(64 位总线一次可传输 8 字节)。
CPU 接收并处理数据
CPU 将接收到的数据存储在寄存器中,供后续指令使用(如运算或写入其他地址)。
2. 3 内存地址与数据存储示例
假设
int
类型占 4 字节,数据3
的二进制表示为:
00000000 00000000 00000000 00000011
(小端模式存储)。
内存地址 存储的二进制数据(1字节) 说明 0x1000
00000011
数据最低有效字节 0x1001
00000000
0x1002
00000000
0x1003
00000000
数据最高有效字节
3. 系统与内存地址
内存读写以byte(字节)为基本的数据储存单位,是一种物理单位;
而bit是信息量的衡量单位,是一种数学单位;
3.1、32位与64位系统的核心区别
位数 定义 核心能力 32位 CPU寄存器和数据总线宽度为32位(4字节) 一次最多处理32位二进制数据,最大寻址空间为4GB(2³²字节) 64位 CPU寄存器和数据总线宽度为64位(8字节) 一次最多处理64位二进制数据,理论最大寻址空间为16EB(2⁶⁴字节,约1.8×10¹⁹GB)
3.2. 地址总线与内存地址范围的关系
3.2.1. 地址总线的作用
地址总线是CPU用于发送内存地址的物理通道,其 位数(宽度) 决定了CPU能访问的最大内存地址数量。
公式:
可寻址内存空间 = 2<sup>地址总线位数</sup>(单位:字节)3.2.2. 示例
地址总线位数 最大内存地址范围 换算为十进制 实际应用 32位 0x00000000 ~ 0xFFFFFFFF 4,294,967,296 B 4GB内存(32位系统上限) 64位 0x0000000000000000 ~ ... 18,446,744,073,709,551,616 B 16EB(理论值,远超当前硬件支持
3.2.3 内存地址的本质
内存地址是系统为每个字节分配的 唯一编号,用于定位内存中的存储单元。
地址长度由地址总线位数决定:
32位地址总线:每个地址为32位二进制数(如
0x1A3F
)。64位地址总线:每个地址为64位二进制数(如
0x7FFF12345678
)。
三:初始指针
1. 指针与指针变量
1.1 指针
C语言中的指针:
1. 指针也就是内存地址(内存地址是每个字节的标识),指针变量是用来存放内存地址的变量,
2. 基地址(首地址):平时获取的地址就是该数据在内存中的数据的首个字节的地址,基地址或者首地址
一个int类型,占用四个字节,指针为其基地址
第一个字节
0000 0000
二个字节
0000 0000
三个字节
0000 0000
第四个字节
0000 0011
地址 0✖ff....f1 地址 0✖ff....f2 地址 0✖ff....f3 地址 0✖ff....f4
重难点:
1. 不同类型指针不能赋值,地址可以转换。
2. 解引用:通过指针来获取到数据或者操作数据。
3. 注意指针所匹配的符号。
代码示例:#include <stdio.h> int main(int argc, char const *argv[]) {/*指针声名并赋值:type:*pointer name = 地址;type为指针的类型,*代表该变量是一个指针,指针占用的字节数与操作系统有关32:占用4节64:占用8节*/// 1. 声名指针变量int *ip;int i = 23;ip = &i;printf("ip=%p\n", ip);// 输出ip=000000000061FE14printf("占用字节数:%lu\n", sizeof(ip));// 输出 占用字节数:8// 3.声明指针并复赋值int *ip2 = &ip;printf("ip2=%p\n", ip2);int i2 = 888;// 重新赋值ip2 = &i2;printf("ip=%p\n", ip2);int i3 = 885;ip2 = &i3;printf("ip=%p\n", ip2);// 4.其他类型的指针// 5.不同类型的指针是不能赋值,(地址是可以互换,因为在内存中内存地址是一样的,地址值不可以因为由类型决定)// 6.解引用 通过指针来获取到数据或者操作数据// 4.其他类型的指针char c = 'A';char *cp = &c;printf("cp=%p\n", cp);printf("cp指向的值:%c\n", *cp);// 通过指针修改数字*cp = 'c';printf("cp=%p\n", cp);return 0; }
1.2 指针变量
1. 指针变量的本质
指针变量是存储 内存地址 的变量,其核心作用是通过地址间接访问内存中的数据。
特性 描述 内存占用 指针变量本身占用内存空间(32位系统为4字节,64位系统为8字节) 存储内容 内存地址(如 0x7ffd4c3a8b2c
)操作方式 通过 *
运算符解引用(访问指向的内存数据)2. 指针变量的内存模型
假设在 32位系统 中定义一个
int
类型指针:int a = 10; // 假设a的地址为0x1000 int *p = &a; // p存储0x1000,p自身地址为0x2000
内存布局:
地址 存储内容 说明 0x1000 10 变量 a
的值0x2000 0x1000 指针变量 p
的值(地址)
p
的大小:4字节(32位系统)。
p
的地址:&p
是0x2000
,即指针变量自身在内存中的位置。3. 指针的指针(双重指针)
指针的指针(如
int **pp
)是存储 另一个指针变量地址 的变量,同样占用内存空间。int a = 10; // 假设a的地址为0x1000 int *p = &a; // p的值为0x1000,p的地址为0x2000 int **pp = &p; // pp的值为0x2000,pp的地址为0x3000
内存布局:
地址 存储内容 说明 0x1000 10 变量 a
的值0x2000 0x1000 指针变量 p
的值0x3000 0x2000 双重指针 pp
的值
pp
的大小:4字节(32位系统)或8字节(64位系统),与普通指针变量相同。
pp
的地址:&pp
是0x3000
,即双重指针自身在内存中的位置。4. 我之前的误解
概念 正确理解 常见误解 指针变量 存储内存地址的变量,占用内存空间(大小由系统位数决定) 误认为指针本身不占内存 指针的指针 存储另一个指针变量的地址,同样占用内存空间 误认为双重指针不占内存 内存地址 系统为每个内存单元分配的编号,不占用额外存储空间 混淆内存地址与指针变量的存储空间 5. 总结
核心结论 说明 指针变量占用内存 存储地址需要内存空间(32位系统4字节,64位系统8字节) 指针的指针也占用内存 双重指针存储的是指针变量的地址,大小与普通指针相同 内存地址是编号,不占存储空间 地址是内存单元的标识符,但存储地址的变量(指针)需要占用内存 理解指针的关键:区分 内存地址(系统分配的编号)和 指针变量(存储地址的变量,占用内存空间)。
2. 指针的运算
指针的运算:
指针的加法:指针加法,以当前指针类型为一个单位
指针的减法:指针减法,以当前指针类型为一个单位
注意:
不同同类型,不能获取前后的值。
#include <stdio.h>
int main(int argc, char const *argv[])
{/*指针的运算指针的加法:指针的减法:*/int i1 = 1;int i2 = 2;int i3 = 3;printf("&i1=%p,&i2=%p,&i3=%p\n", &i1, &i2, &i3);// 数据类型必须一致// 指针加法(有错误)int *ip1 = &i1;printf("ip1=%p,ip1+1=%p,*ip1=%d,*(ip1+1)=%d\n", ip1, ip1 + 1, *ip1, *(ip1 + 1)); //+1向前移动一个单位长度,即四个字节,由int决定printf("ip1=%p,ip1+2=%p,*(ip1+2)=%p\n", ip1, ip1 + 2, *(ip1 + 2)); //+2向前移动二个单位长度// 指针减法,同上return 0;
}
3. 特殊指针
1. 野指针
野指针----指针变量中的值为非法内存地址,野指针不是空指针,是指向不可用的内存地址的指针
1.1局部指针变量没有初始化
2.2指针所指向的变量在指针使用之前被销毁
2.3旨使用已经释放过的指针
2.4进行错误的指针运算
2.5进行的错误的强制类型转换
// 野指针,int *p; // 在内存占用八个字节,局部指针变量未赋值printf("p=%p\n", p);
2. 空指针
空指针----在声名变量的时候,如果没有确切的地址可以被赋值,为指针变量赋null。值为null为空指针
2.1null指针是一个顶底在标准库值为零的常量
2.2null 地址为0✖0
2.3大多数操作系统,程序不允许访问地址为0的内存,该内存是操作系统保留的。
2.4内存地址为0是非常重要的
int *p2 = NULL;printf("p2=%p\n", p2);if (p2 == NULL){printf("我是NULL指针....\n");}else{printf("我不是NULL指针....\n");}
3. 野指针与空指针的对比
注意:c语言无法判断一个指针所保存的地址是否合法
特性 野指针 空指针 定义 指向无效内存地址的指针 明确不指向任何地址的指针 安全性 危险,可能导致未定义行为 安全,显式表示无指向 初始化 未初始化或指向已释放内存 显式初始化为 NULL
或nullptr
检测方法 无法直接检测,需代码逻辑保证 可通过 if (p == NULL)
检测典型场景 释放后未置空、返回局部变量地址 初始化未分配的指针、函数错误返回值
野指针:需通过代码规范避免(如释放后置空、避免返回局部地址)。
空指针:使用
NULL
(C)或nullptr
(C++)显式表示无指向,提升代码安全性。最佳实践:
指针声明时立即初始化(如
int *p = NULL;
)。动态内存释放后立即置空。
C++中优先使用
nullptr
替代NULL
。
4. 数组的指针
数组array是一系列具有相同类型的数据的集合,每一份数据叫做一个数据元素(element),
1. 数组中所有元素在内存中是连续排列的,整个数组占用的是一块内存。
2. 定义数组是,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的index索引为0的元素,
3. 在C语言中 index索引为0的元素的地址为数组的首地址。
4. 数组名的本意是表示整个数组,也就是表示多份数据的集合,但是在使用过程中经常为转5. 换为指向数组index索引为0的元素的指针。
6. 所以说可以“认为”数组名是一个指针,但是数组名和数组首地址并不总是等价。
#include <stdio.h>
//宏表达式
#define LENGTH(array) (sizeof(array)/sizeof(array[0]))/*
数组array是一系列具有相同类型的数据的集合,每一份数据叫做一个数据元素(element),
数组中所有元素在内存中是连续排列的,整个数组占用的是一块内存。
定义数组是,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的index索引为0的元素,
在C语言中 index索引为0的元素的地址为数组的首地址。
数组名的本意是表示整个数组,也就是表示多份数据的集合,但是在使用过程中经常为转换为指向数组index索引为0的元素的指针。
所以说可以“认为”数组名是一个指针,但是数组名和数组首地址并不总是等价。
*/int main(int argc, char const *argv[])
{int arr[]={5,88,99,707,101};//数组的指针int (*p1)[]=&arr;//指向的整块数组所在的内存 printf("p1=%p\n",p1);//数组中元素的指针int *p2=&arr[0];//第一个元素的指针printf("p2=%p\n",p2);//不是一个类型//p1=p2;p2=&arr[1];//第二个元素的指针printf("p2=%p\n",p2);//遍历printf("数组名操作:\n");for (int i = 0; i < LENGTH(arr); i++){ //数组名操作printf("arr[%d]=%d",i,arr[i]);}printf("\n数组指针操作:\n");for (int i = 0; i < LENGTH(arr); i++){ //数组指针操作printf("(*p1)[%d]=%d,地址:%p ",i,(*p1)[i],&(*p1)[i]);}printf("\n");int msg[]={15,188,199,1707,1101}; //数组名不能更改指向,但是数组指针可以//arr=msg;p1=&msg;for (int i = 0; i < LENGTH(msg); i++){ //数组指针操作printf("(*p1)[%d]=%d,地址:%p ",i,(*p1)[i],&(*p1)[i]);}printf("\n");return 0;
}
5. 指针数组
指针数组:
int (*P)[10];数组的指针
int *p[];指针的数组
#include <stdio.h>
int main(int argc, char const *argv[])
{/*指针数组:int (*P)[10];数组的指针int *p[];指针的数组*/int a = 1;int b = 2;int c = 3;int arr[3] = {a, b, c}; // int类型的数组int *arr2[3] = { &a,&b,&c } // int类型的指针数组return 0;
}
6.数组的指针与指针数组总结:
特性 指针数组 数组指针 本质 数组,元素为指针 指针,指向整个数组 声明语法 int *arr[N];
int (*p)[N];
内存占用 每个元素占用指针大小(4/8字节) 单个指针变量大小(4/8字节) 访问元素方式 *arr[i]
或arr[i][j]
(二维场景)(*p)[i]
或p[i][j]
典型用途 存储多个独立地址(如字符串数组) 操作多维数组或动态分配的连续内存块
指针数组:数组元素为指针,用于管理多个独立数据块的地址(如字符串数组)。
数组指针:指针指向整个数组,适合操作多维数组或动态分配的连续内存。
语法核心:
int *arr[N]
是指针数组,int (*p)[N]
是数组指针。