C语言基础之【指针】(上)
往期《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操作向后加过的字节数。
*
:表示这是一个指针变量。
指针变量名
:指针变量的名称。int *p; // 定义一个指向整型的指针变量p float *q; // 定义一个指向浮点型的指针变量q char *r; // 定义一个指向字符型的指针变量r
注意:由于指针变量定义的语法中
数据类型
和指针变量名
之间的*
该怎么放?C语言中并没有明确规定。所以:
//指针变量定义的语法:(以下的都可以) 数据类型* 指针变量名; ----->Windows平台常用 数据类型 *指针变量名; ----->Linux平台常用 数据类型 * 指针变量名; ----->几乎没人这么写 数据类型*指针变量名; ----->几乎没人这么写
指针变量初始化的语法:
指针变量名 = &变量名;
指针变量在定义后,通常需要初始化为某个变量的地址,可以使用
&
运算符获取变量的地址。int a = 10; int *p = &a; // 将指针变量p初始化为变量a的地址
指针变量的使用:
取地址操作(&)
:使用&运算符获取
变量的地址
int a = 10; int *p = &a; // p存储变量a的地址
注意:
&可以取得一个变量在内存中的地址,但是不能取寄存器变量。
因为寄存器变量不在内存里,而在CPU里面,所以是没有地址的。
解引用操作(*)
:使用*运算符访问
指针所指向的内存中的数据
int a = 10; int *p = &a; printf("%d", *p); // 输出10
代码示例:通过指针间接修改变量的值
#include <stdio.h> 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
指针的类型与解引用
代码示例:
#include <stdio.h> #include <stdlib.h> 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; }
输出:
*p = 0000000012345678 *p = 12345678 p = 00000022016FF5A4
代码分析:
变量声明与初始化:
int a = 0x12345678;
- 声明了一个整型变量
a
并将其初始化为十六进制值0x12345678
int *p = &a;
- 声明了一个指向整型的指针
p
,并将其初始化为变量a
的地址打印指针指向的值:
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*
是为了确保类型的匹配
和代码的可移植性
代码示例:
short a = 0x12345678; short* p = &a; printf("*p = %p\n", *p); printf("*p = %x\n", *p); // 打印 a 的值 printf("p = %p\n", (void*)p); // 打印 p 的地址
代码片段分析:
short a = 0x12345678;
short
类型通常占用 2 字节(16 位)0x12345678
是一个 4 字节的十六进制值,赋值给short
类型时会发生截断
,只有最低的 2 字节(0x5678
)会被存储到a
中。所以:
short* p 解引用时读取 2 字节数据
输出:
*p = 0000000000005678 *p = 5678 p = 0000001988EFFB04
代码示例:
char a = 0x12345678; char* p = &a; printf("*p = %p\n", *p); printf("*p = %x\n", *p); // 打印 a 的值 printf("p = %p\n", (void*)p); // 打印 p 的地址
代码片段分析:
char a = 0x12345678;
char
类型通常占用 1 字节(8 位)0x12345678
是一个 4 字节的十六进制值,赋值给char
类型时会发生截断
,只有最低的 1 字节(0x78
)会被存储到a
中。所以:
char* p 解引用时读取 1 字节数据
输出:
*p = 0000000000000078 *p = 78 p = 0000009FC08FF574
指针大小
指针的大小
:指针变量本身占用的内存字节数。
指针的大小与它所指向的数据类型无关,而是取决于系统的架构和编译器的实现
- 在32位系统中,指针的大小通常为4字节
- 在64位系统中,指针的大小通常为8字节
指针大小与系统架构的关系:
32位系统
- 地址总线宽度为32位,可以寻址的内存空间为 ( 2 32 ) (2^{32}) (232) 字节(即:4GB)
- 指针的大小通常为4字节。
64位系统
- 地址总线宽度为64位,可以寻址的内存空间为 ( 2 64 ) (2^{64}) (264)字节(即:16EB)
- 指针的大小通常为8字节。
获取指针大小的语法:
sizeof(指针变量);
#include <stdio.h> 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位系统)
int指针的大小: 4字节 char指针的大小: 4字节 double指针的大小: 4字节
输出结果(64位系统)
int指针的大小: 8字节 char指针的大小: 8字节 double指针的大小: 8字节
总结:无论指针指向的是
int
、char
、double
还是其他类型,指针的大小都是相同的。
#include <stdio.h> 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位系统)
int指针的大小: 4字节 int指针的指针的大小: 4字节
输出结果(64位系统)
int指针的大小: 8字节 int指针的指针的大小: 8字节
总结:多级指针(指向指针的指针)的大小与普通指针相同。
空指针
空指针(NULL Pointer)
:不指向任何有效的内存地址的指针。
- 在C语言中,空指针通常用宏
NULL
表示,其值为0
空指针的用途:
初始化指针:
在定义指针时
,如果暂时不知道指针应该指向哪里,可以先将其初始化为NULL
,以表明它不指向任何有效数据。
- 例如:
int *p = NULL;
判断指针是否有效:
在使用指针之前
,可以通过判断指针是否为NULL来确定它是否指向了一个有效的内存地址,从而避免对空指针进行操作导致的错误。#include <stdio.h> int main() { int *p = NULL; //初始化指针 if (p == NULL) //判断指针是否有效 { printf("p是一个空指针\n"); } return 0; }
输出:
p是一个空指针
注意事项:
解引用空指针会导致程序崩溃
- 因为空指针不指向任何有效内存,所以对空指针进行解引用操作是非法的,会导致程序出现错误。
int *p = NULL; *p = 10; // 错误:解引用空指针 //这样的代码是错误的,因为它试图向一个不存在的内存地址写入数据。
野指针
野指针(Dangling Pointer)
:指向无效内存地址的指针。
野指针产生原因:
定义指针时未初始化
指针变量定义后未赋值,其值是随机的。
int *p; // 未初始化 *p = 10; // 错误:p是野指针 //或者有一部分人会这样作: int p2 = 1000; // 错误:p2是野指针 //这样手动为指针赋内存地址的行为,绝大多数情况下会导致该指针为野指针
释放内存后未置空指针
动态分配的内存释放后,指针仍然指向该内存地址。
int *p = (int *)malloc(sizeof(int)); free(p); // 释放了指针指向的内存,但是未将指针置空 *p = 10; // 错误:p是野指针
返回局部变量的指针
局部变量的内存在被调函数执行结束后就被释放掉了
所以:
禁止返回局部变量的地址
int *getPointer() { int a = 10; return &a; //返回局部变量的地址,但是局部变量a的内存在函数执行结束后就被释放掉了 } int main() { int *p = getPointer(); *p = 20; // 错误:p是野指针 return 0; }
野指针的危害:
访问
野指针指向的内存可能导致程序崩溃
(段错误)修改
野指针指向的内存可能导致数据损坏
或安全漏洞
如何避免野指针:
初始化指针
定义指针时初始化为
NULL
int *p = NULL;
释放内存后置空指针
释放动态分配的内存后,将指针置为
NULL
int *p = (int *)malloc(sizeof(int)); free(p); p = NULL; // 置空指针
避免返回局部变量的地址
- 不要返回函数内局部变量的地址。
空指针与野指针的对比:
特性 | 空指针(NULL Pointer) | 野指针(Dangling Pointer) |
---|---|---|
定义 | 指针不指向任何内存地址 | 指针指向无效的内存地址 |
产生原因 | 显式初始化为NULL |
定义指针时未初始化 释放内存后未置空指针 返回局部变量的地址 |
解引用后果 | 程序崩溃 | 程序崩溃或数据损坏 |
如何避免 | 初始化指针为NULL |
定义指针时初始化指针 释放内存后置空指针 避免返回局部变量地址 |
代码示例:空指针
#include <stdio.h> int main() { int* p = NULL; // 定义一个空指针 if (p == NULL) { printf("p是一个空指针\n"); } // *p = 10; // 错误:解引用空指针会导致程序崩溃 return 0; }
代码示例:野指针
#include <stdio.h> #include <stdlib.h> 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 *指针变量名;
void *p; // 定义一个万能指针p
万能指针的特点:
可以指向任意类型的数据
void *
可以指向int
、float
、char
、结构体等任意类型的数据。int a = 10; float b = 3.14; void *p; p = &a; // 指向int类型 p = &b; // 指向float类型
不能直接解引用
由于
void *
没有类型信息,编译器无法确定它指向的数据类型,因此不能直接解引用。int a = 10; void *p = &a; // *p = 20; // 错误:不能直接解引用void指针 int *q = (int *)p; // 将void指针转换为int指针 *q = 20; // 解引用并修改值
不能直接指针算术运算
void *
不能直接进行指针算术运算,因为编译器无法确定指针的步长。int arr[] = {1, 2, 3}; void *p = arr; // p++; // 错误:void指针不能进行算术运算 int *q = (int *)p; q++; // 正确:int指针可以进行算术运算
总结:在使用 void * 时,需要将其转换为具体类型的指针,才能
解引用
或进行指针算术运算
代码示例:展示
void *
的使用#include <stdio.h> #include <stdlib.h> // 打印任意类型的数据 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; }
输出:
int: 10 float: 3.14 char: A
const修饰的指针变量
const关键字
:用于定义常量
或限制变量
的修改。
const
修饰指针的基本形式:
指向常量的指针
:指针指向的数据是常量,不能通过指针修改数据。
语法:
const 数据类型 *指针变量名;
语法:
数据类型 const *指针变量名;
const int *p; // p是一个指向常量的指针,不能通过p修改数据 int const *p; // p是一个指向常量的指针,不能通过p修改数据
常量指针
:指针本身是常量,不能修改指针的指向。
语法:
数据类型 *const 指针变量名;
int *const p; // p是一个常量指针,不能修改p的指向
指向常量的常量指针
:指针指向的数据是常量,且指针本身也是常量。
语法:
const 数据类型 *const 指针变量名;
const int *const p; // p是一个指向常量的常量指针
指向常量的指针
的特点:
指针指向的数据
是常量,不能通过指针修改数据。指针本身
可以修改,指向其他地址。#include <stdio.h> 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; }
输出:
a的值: 10 修改后a的值: 20 b的值: 30
常量指针
的特点:
指针本身
是常量,不能修改指针的指向。指针指向的数据
可以修改。#include <stdio.h> 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; }
输出:
a的值: 10 修改后a的值: 20
指向常量的常量指针
的特点:
指针指向的数据
是常量,不能通过指针修改数据。指针本身
也是常量,不能修改指针的指向。#include <stdio.h> 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; }
输出:
a的值: 10 修改后a的值: 20
const 修饰指针的常见用法:
1.保护函数参数
使用
const
修饰指针参数,防止函数内部修改数据。void printArray(const int *arr, int n) { for (int i = 0; i < n; i++) { printf("%d ", arr[i]); } printf("\n"); }
2.定义常量字符串
使用
const
修饰指针,定义常量字符串。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++全栈开发
的知识,如果你感兴趣请多多关注博主。