往期《C语言基础系列》回顾:
链接:
C语言基础之【C语言概述】
C语言基础之【数据类型】(上)
C语言基础之【数据类型】(下)
C语言基础之【运算符与表达式】
C语言基础之【程序流程结构】
C语言基础之【数组和字符串】(上)
C语言基础之【数组和字符串】(下)
C语言基础之【函数】
概述内存 内存(Memory):是计算机系统中用于存储数据和指令的关键硬件组件。
内存是程序运行的基石,所有正在执行的程序和数据都需要加载到内存中才能被CPU处理。
内存的特点:
速度快:相较于外部存储设备如硬盘、光盘等,内存的读写速度极快。 内存作为CPU与硬盘之间的桥梁,提供快速数据访问。临时存储:内存用于暂时存储正在运行的程序和数据。 当计算机断电后,内存中存储的数据会立即丢失,这是与硬盘等外部存储设备最显著的区别之一。与 CPU 直接交互:内存是计算机中唯一能与 CPU 直接进行数据交互的部件。 CPU 可以直接从内存中读取指令和数据,进行运算处理后再将结果写回内存。可扩展性:计算机的内存具有一定的可扩展性。 用户可以通过添加内存模块来增加内存容量,从而提升计算机的性能。 内存的分类:
RAM(随机存取存储器):可读写,用于临时存储数据。 如:程序运行时变量ROM(只读存储器):只读,用于存储固件。 如:BIOS(数据不可修改)物理存储器和存储地址空间 物理存储器:是指计算机系统中实际存在的存储硬件,数据和程序的物理存储介质。
如:内存条(RAM)、硬盘、固态硬盘(SSD)等。 物理存储器的分类:
主存储器(内存):用于存储正在运行的程序和数据。 直接与CPU交互速度快,容量有限辅助存储器(外存):用于存储需要长期存储的数据。 速度较慢,容量大高速缓存(Cache):用于加快数据访问的速度。 位于CPU和主存之间分为L1、L2、L3缓存,速度依次递减,容量依次递增 存储地址空间:是指计算机系统中用于存储数据的地址范围。
存储地址空间可以分为 物理地址空间 和 逻辑地址空间:
物理地址空间:是对物理存储器中每个存储单元进行编号的地址范围,与物理存储器的实际地址相对应。
逻辑地址空间:是程序和进程所使用的地址空间,也称为虚拟地址空间。
内存地址 内存单元:是计算机内存系统中的基本存储单位,用于存储数据的最小物理单位。
它就像一个小格子,每个格子都有自己的编号(地址),可以存放特定数量的数据。
在计算机内存里,众多这样的内存单元组合在一起,形成了能够存储大量数据的内存空间。
内存地址:是计算机系统中用于标识和访问内存中特定存储单元的唯一编号。
将内存抽象成一个很大的一维字符数组编码就是对内存的每一个字节分配一个32位或64位的编号(与32位或者64位处理器相关)这个内存编号我们称之为内存地址 内存地址的作用:
程序:通过内存地址访问数据或指令。操作系统和硬件:通过内存地址管理内存资源。 内存地址的表示:
内存地址的表示通常是十六进制
如:0x1000 内存地址的范围由系统的位数决定
如:32位系统的地址范围为0x00000000到0xFFFFFFFF指针和指针变量 如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号)
指针的实质就是内存“地址”。指针就是地址,地址就是指针
在这里插入图片描述 指针的作用:
直接访问内存:允许程序直接操作内存中的数据,能够更灵活地对数据进行处理。
如:在需要高效地处理大量数据或对内存进行精细管理的场景中,指针可以让程序员直接定位到特定的内存区域,进行数据的读取、写入或修改等操作。 实现数据结构:是构建各种复杂数据结构的基础。
如:链表、树、图等。通过指针,这些数据结构中的节点可以相互链接,形成特定的逻辑结构,方便对数据进行组织和操作。 函数间数据传递:在函数之间传递数据时,指针可以起到高效传递数据的作用。
如:对于大型数据结构或对象,传递指针比传递数据本身更加高效,因为只需要传递一个地址值,而不需要复制大量的数据,从而节省了内存空间和数据传输时间。指针基础知识指针变量的定义和使用
指针变量定义的语法:数据类型 *指针变量名;
数据类型:指针所指向的变量的类型 (如:int、float、char等)
决定了从指针存储的地址开始向后读取的字节数。决定了指针进行+1操作向后加过的字节数。 *:表示这是一个指针变量。
指针变量名:指针变量的名称。
代码语言:javascript复制int *p; // 定义一个指向整型的指针变量p
float *q; // 定义一个指向浮点型的指针变量q
char *r; // 定义一个指向字符型的指针变量r注意:由于指针变量定义的语法中数据类型和指针变量名之间的*该怎么放?C语言中并没有明确规定。
所以:
代码语言:javascript复制//指针变量定义的语法:(以下的都可以)
数据类型* 指针变量名; ----->Windows平台常用
数据类型 *指针变量名; ----->Linux平台常用
数据类型 * 指针变量名; ----->几乎没人这么写
数据类型*指针变量名; ----->几乎没人这么写指针变量初始化的语法:指针变量名 = &变量名;
指针变量在定义后,通常需要初始化为某个变量的地址,可以使用&运算符获取变量的地址。
代码语言:javascript复制int a = 10;
int *p = &a; // 将指针变量p初始化为变量a的地址
指针变量的使用:
取地址操作(&):使用&运算符获取变量的地址
代码语言:javascript复制int a = 10;
int *p = &a; // p存储变量a的地址注意:
&可以取得一个变量在内存中的地址,但是不能取寄存器变量。
因为寄存器变量不在内存里,而在CPU里面,所以是没有地址的。
解引用操作(*):使用*运算符访问指针所指向的内存中的数据
代码语言:javascript复制int a = 10;
int *p = &a;
printf("%d", *p); // 输出10代码示例:通过指针间接修改变量的值
代码语言:javascript复制#include
int main()
{
int a = 10; // 定义一个整型变量a
int *p = &a; // 定义一个指针变量p,并将其初始化为变量a的地址
printf("修改前,a的值: %d\n", a); // 输出a的初始值
//通过指针间接修改变量的值
*p = 20; // 通过指针p修改变量a的值
printf("修改后,a的值: %d\n", a); // 输出修改后的a的值
return 0;
}输出:
修改前,a的值: 10
修改后,a的值: 20
指针的类型与解引用
代码示例:
代码语言:javascript复制#include
#include
int main(void)
{
int a = 0x12345678;
int* p = &a;
printf("*p = %p\n", *p);
printf("*p = %x\n", *p); // 打印 a 的值
printf("p = %p\n", (void*)p); // 打印 p 的地址
system("pause");
return EXIT_SUCCESS;
}输出:
代码语言:javascript复制*p = 0000000012345678
*p = 12345678
p = 00000022016FF5A4代码分析:
变量声明与初始化:
int a = 0x12345678; 声明了一个整型变量 a 并将其初始化为十六进制值 0x12345678int *p = &a; 声明了一个指向整型的指针 p,并将其初始化为变量 a 的地址打印指针指向的值:
代码语言:javascript复制printf("*p = %p\n", *p); //err%p:是用于**打印指针地址**的格式说明符。*p:是解引用指针,得到的是 a 的值,而不是地址。正确的用法应该是 :
printf("*p = %x\n", *p); 来打印 a 的十六进制值。
printf("p = %p\n", (void*)p); 来打印指针 p 的地址。 %p 期望的参数类型是 void*,即指向 void 的指针,所以int* 类型的指针 p 需要强制转换为 void*类型,以便与 %p 格式说明符匹配。这是因为 %p 被设计为可以打印任何类型的指针地址,而 void* 是一种通用指针类型,可以指向任何数据类型。总结:强制转换为 void* 是为了确保类型的匹配和代码的可移植性
代码示例:
代码语言:javascript复制short a = 0x12345678;
short* p = &a;
printf("*p = %p\n", *p);
printf("*p = %x\n", *p); // 打印 a 的值
printf("p = %p\n", (void*)p); // 打印 p 的地址代码片段分析:
代码语言:javascript复制short a = 0x12345678;short 类型通常占用 2 字节(16 位)0x12345678 是一个 4 字节的十六进制值,赋值给 short 类型时会发生 截断,只有最低的 2 字节(0x5678)会被存储到 a 中。所以:short* p 解引用时读取 2 字节数据
输出:
代码语言:javascript复制*p = 0000000000005678
*p = 5678
p = 0000001988EFFB04代码示例:
代码语言:javascript复制char a = 0x12345678;
char* p = &a;
printf("*p = %p\n", *p);
printf("*p = %x\n", *p); // 打印 a 的值
printf("p = %p\n", (void*)p); // 打印 p 的地址代码片段分析:
代码语言:javascript复制char a = 0x12345678;char 类型通常占用 1 字节(8 位)0x12345678 是一个 4 字节的十六进制值,赋值给 char 类型时会发生 截断,只有最低的 1 字节(0x78)会被存储到 a 中。所以:char* p 解引用时读取 1 字节数据
输出:
代码语言:javascript复制*p = 0000000000000078
*p = 78
p = 0000009FC08FF574指针大小 指针的大小:指针变量本身占用的内存字节数。
指针的大小与它所指向的数据类型无关,而是取决于系统的架构和编译器的实现
在32位系统中,指针的大小通常为4字节在64位系统中,指针的大小通常为8字节 指针大小与系统架构的关系:
32位系统
地址总线宽度为32位,可以寻址的内存空间为 (2^{32}) 字节(即:4GB)
指针的大小通常为4字节。 64位系统
地址总线宽度为64位,可以寻址的内存空间为(2^{64})字节(即:16EB)
指针的大小通常为8字节。
获取指针大小的语法:sizeof(指针变量);
代码语言:javascript复制#include
int main()
{
int a = 10;
char b = 'A';
double c = 3.14;
int* p = &a;
char* q = &b;
double* r = &c;
printf("int指针的大小: %zu字节\n", sizeof(p));
printf("char指针的大小: %zu字节\n", sizeof(q));
printf("double指针的大小: %zu字节\n", sizeof(r));
return 0;
}输出结果(32位系统)
代码语言:javascript复制int指针的大小: 4字节
char指针的大小: 4字节
double指针的大小: 4字节输出结果(64位系统)
代码语言:javascript复制int指针的大小: 8字节
char指针的大小: 8字节
double指针的大小: 8字节总结:无论指针指向的是int、char、double还是其他类型,指针的大小都是相同的。
代码语言:javascript复制#include
int main()
{
int a = 10;
int* p = &a;
int** pp = &p;
printf("int指针的大小: %zu字节\n", sizeof(p));
printf("int指针的指针的大小: %zu字节\n", sizeof(pp));
return 0;
}输出结果(32位系统)
代码语言:javascript复制int指针的大小: 4字节
int指针的指针的大小: 4字节输出结果(64位系统)
代码语言:javascript复制int指针的大小: 8字节
int指针的指针的大小: 8字节总结:多级指针(指向指针的指针)的大小与普通指针相同。
空指针
空指针(NULL Pointer):不指向任何有效的内存地址的指针。
在C语言中,空指针通常用宏NULL表示,其值为0空指针的用途:
初始化指针:在定义指针时,如果暂时不知道指针应该指向哪里,可以先将其初始化为NULL,以表明它不指向任何有效数据。
例如:int *p = NULL; 判断指针是否有效:在使用指针之前,可以通过判断指针是否为NULL来确定它是否指向了一个有效的内存地址,从而避免对空指针进行操作导致的错误。
代码语言:javascript复制#include
int main()
{
int *p = NULL; //初始化指针
if (p == NULL) //判断指针是否有效
{
printf("p是一个空指针\n");
}
return 0;
}输出:
p是一个空指针
注意事项:解引用空指针会导致程序崩溃
因为空指针不指向任何有效内存,所以对空指针进行解引用操作是非法的,会导致程序出现错误。代码语言:javascript复制int *p = NULL;
*p = 10; // 错误:解引用空指针
//这样的代码是错误的,因为它试图向一个不存在的内存地址写入数据。野指针
野指针(Dangling Pointer):指向无效内存地址的指针。
野指针产生原因:
定义指针时未初始化
指针变量定义后未赋值,其值是随机的。
代码语言:javascript复制int *p; // 未初始化
*p = 10; // 错误:p是野指针
//或者有一部分人会这样作:
int p2 = 1000; // 错误:p2是野指针
//这样手动为指针赋内存地址的行为,绝大多数情况下会导致该指针为野指针释放内存后未置空指针
动态分配的内存释放后,指针仍然指向该内存地址。
代码语言:javascript复制int *p = (int *)malloc(sizeof(int));
free(p); // 释放了指针指向的内存,但是未将指针置空
*p = 10; // 错误:p是野指针返回局部变量的指针
局部变量的内存在被调函数执行结束后就被释放掉了
所以:禁止返回局部变量的地址
代码语言:javascript复制int *getPointer()
{
int a = 10;
return &a;
//返回局部变量的地址,但是局部变量a的内存在函数执行结束后就被释放掉了
}
int main()
{
int *p = getPointer();
*p = 20; // 错误:p是野指针
return 0;
}野指针的危害:
访问野指针指向的内存可能导致程序崩溃(段错误)修改野指针指向的内存可能导致数据损坏或安全漏洞如何避免野指针:
初始化指针
定义指针时初始化为NULL
代码语言:javascript复制int *p = NULL;释放内存后置空指针
释放动态分配的内存后,将指针置为NULL
代码语言:javascript复制int *p = (int *)malloc(sizeof(int));
free(p);
p = NULL; // 置空指针避免返回局部变量的地址
不要返回函数内局部变量的地址。空指针与野指针的对比:
特性
空指针(NULL Pointer)
野指针(Dangling Pointer)
定义
指针不指向任何内存地址
指针指向无效的内存地址
产生原因
显式初始化为NULL
定义指针时未初始化释放内存后未置空指针返回局部变量的地址
解引用后果
程序崩溃
程序崩溃或数据损坏
如何避免
初始化指针为NULL
定义指针时初始化指针释放内存后置空指针避免返回局部变量地址
代码示例:空指针
代码语言:javascript复制#include
int main()
{
int* p = NULL; // 定义一个空指针
if (p == NULL)
{
printf("p是一个空指针\n");
}
// *p = 10; // 错误:解引用空指针会导致程序崩溃
return 0;
}代码示例:野指针
代码语言:javascript复制#include
#include
int main()
{
int* p = (int*)malloc(sizeof(int)); // 动态分配内存
*p = 10;
printf("p指向的值: %d\n", *p);
free(p); // 释放内存
// p现在是野指针
// *p = 20; // 错误:解引用野指针会导致未定义行为
p = NULL; // 置空指针,避免成为野指针
return 0;
}万能指针
void * :是一种通用指针类型,可以指向任意数据类型的内存地址,因此也称为 万能指针 或 泛型指针
万能指针定义的语法:void *指针变量名;
代码语言:javascript复制void *p; // 定义一个万能指针p
万能指针的特点:
可以指向任意类型的数据
void * 可以指向 int、float、char、结构体等任意类型的数据。
代码语言:javascript复制int a = 10;
float b = 3.14;
void *p;
p = &a; // 指向int类型
p = &b; // 指向float类型不能直接解引用
由于 void * 没有类型信息,编译器无法确定它指向的数据类型,因此不能直接解引用。
代码语言:javascript复制int a = 10;
void *p = &a;
// *p = 20; // 错误:不能直接解引用void指针
int *q = (int *)p; // 将void指针转换为int指针
*q = 20; // 解引用并修改值不能直接指针算术运算
void * 不能直接进行指针算术运算,因为编译器无法确定指针的步长。
代码语言:javascript复制int arr[] = {1, 2, 3};
void *p = arr;
// p++; // 错误:void指针不能进行算术运算
int *q = (int *)p;
q++; // 正确:int指针可以进行算术运算总结:在使用 void * 时,需要将其转换为具体类型的指针,才能解引用或进行指针算术运算
代码示例:展示 void * 的使用
代码语言:javascript复制#include
#include
// 打印任意类型的数据
void printValue(void* data, char type)
{
switch (type)
{
case 'i':
printf("int: %d\n", *(int*)data);
break;
case 'f':
printf("float: %.2f\n", *(float*)data);
break;
case 'c':
printf("char: %c\n", *(char*)data);
break;
default:
printf("Unknown type\n");
}
}
int main()
{
int a = 10;
float b = 3.14;
char c = 'A';
printValue(&a, 'i'); // 打印int类型
printValue(&b, 'f'); // 打印float类型
printValue(&c, 'c'); // 打印char类型
return 0;
}输出:
代码语言:javascript复制int: 10
float: 3.14
char: Aconst修饰的指针变量
const关键字:用于定义常量或限制变量的修改。
const 修饰指针的基本形式:
指向常量的指针:指针指向的数据是常量,不能通过指针修改数据。
语法:const 数据类型 *指针变量名;
语法:数据类型 const *指针变量名;
代码语言:javascript复制const int *p; // p是一个指向常量的指针,不能通过p修改数据
int const *p; // p是一个指向常量的指针,不能通过p修改数据常量指针:指针本身是常量,不能修改指针的指向。
语法:数据类型 *const 指针变量名;
代码语言:javascript复制int *const p; // p是一个常量指针,不能修改p的指向指向常量的常量指针:指针指向的数据是常量,且指针本身也是常量。
语法:const 数据类型 *const 指针变量名;
代码语言:javascript复制const int *const p; // p是一个指向常量的常量指针
指向常量的指针的特点:
指针指向的数据是常量,不能通过指针修改数据。指针本身可以修改,指向其他地址。代码语言:javascript复制#include
int main()
{
int a = 10;
const int *p = &a; // p是一个指向常量的指针
printf("a的值: %d\n", *p);
// *p = 20; // 错误:不能通过p修改a的值
a = 20; // 正确:可以直接修改a的值
printf("修改后a的值: %d\n", *p);
int b = 30;
p = &b; // 正确:可以修改p的指向
printf("b的值: %d\n", *p);
return 0;
}输出:
代码语言:javascript复制a的值: 10
修改后a的值: 20
b的值: 30常量指针的特点:
指针本身是常量,不能修改指针的指向。指针指向的数据可以修改。代码语言:javascript复制#include
int main()
{
int a = 10;
int *const p = &a; // p是一个常量指针
printf("a的值: %d\n", *p);
*p = 20; // 正确:可以通过p修改a的值
printf("修改后a的值: %d\n", a);
int b = 30;
// p = &b; // 错误:不能修改p的指向
return 0;
}输出:
代码语言:javascript复制a的值: 10
修改后a的值: 20指向常量的常量指针的特点:
指针指向的数据是常量,不能通过指针修改数据。指针本身也是常量,不能修改指针的指向。代码语言:javascript复制#include
int main()
{
int a = 10;
const int *const p = &a; // p是一个指向常量的常量指针
printf("a的值: %d\n", *p);
// *p = 20; // 错误:不能通过p修改a的值
a = 20; // 正确:可以直接修改a的值
printf("修改后a的值: %d\n", *p);
int b = 30;
// p = &b; // 错误:不能修改p的指向
return 0;
}输出:
代码语言:javascript复制a的值: 10
修改后a的值: 20
const 修饰指针的常见用法:
1.保护函数参数
使用 const 修饰指针参数,防止函数内部修改数据。
代码语言:javascript复制void printArray(const int *arr, int n)
{
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}2.定义常量字符串
使用 const 修饰指针,定义常量字符串。
代码语言:javascript复制const char *str = "Hello, World!";
// str[0] = 'h'; // 错误:不能修改常量字符串关于 const 修饰指针的三种形式的对比表格:
形式
语法
特点
示例
指向常量的指针
const 数据类型 *指针变量名;
不能通过指针修改数据可以修改指针的指向
const int *p;p = &a;// *p = 20; // 错误
常量指针
数据类型 *const 指针变量名;
可以通过指针修改数据不能修改指针的指向
int *const p = &a;*p = 20;// p = &b; // 错误
指向常量的常量指针
const 数据类型 *const 指针变量名;
不能通过指针修改数据不能修改指针的指向
const int *const p = &a;// *p = 20; // 错误// p = &b; // 错误
👨💻 博主正在持续更新C语言基础系列中。
❤️ 如果你觉得内容还不错,请多多点赞。
⭐️ 如果你觉得对你有帮助,请多多收藏。(防止以后找不到了)
👨👩👧👦 C语言基础系列 持续更新中~,后续分享内容主要涉及 C++全栈开发 的知识,如果你感兴趣请多多关注博主。