指针与底层硬件联系紧密,使用指针可操作数据的地址,实现数据的间接访问
问题:这好好的一个变量,我定义完之后为啥不用它名字直接访问呢?我非要用间接访问,这不没事找事吗?
为什么需要指针?
- 指针的使用使得不同区域的代码可以轻易的共享内存数据。当然你也可以通过数据的复制达到相同的效果,但是这样往往效率不太好,因为诸如结构体等大型数据,占用的字节数多,复制很消耗性能。但使用指针就可以很好的避免这个问题,因为任何类型的指针占用的字节数都是一样的(根据平台不同,有4字节或者8字节或者其他可能)
- 指针使得一些复杂的链接性的数据结构的构建成为可能,比如链表,链式二叉树等等
- 有些操作必须使用指针。如操作申请的堆内存
- C语言中的一切函数调用中,实参传递给形参的机理都是“按值传递(pass by value)”,如果我们要在函数中修改被传递过来的对象,就必须通过这个对象的指针来完成
定义指针
用来保存 指针(地址) 的变量,就是指针变量。如果指针变量p1保存了变量 num的地址,则就说:p1指向了变量num
指针即指针变量,用于存放其他数据单元(变量/数组/结构体/函数等)的首地址。若指针存放了某个数据单元的首地址,则这个指针指向了这个数据单元,若指针存放的值是0,则这个指针为空指针
16位系统:x=2,32位系统:x=4,64位系统:x=8
在系统中,指针占用的字节数(即指针的大小)是固定的。这是因为指针的大小主要由CPU的架构和操作系统的内存管理模式决定!因为指针的作用就是把地址表示出来
不同系统的int占的字节数确实可能会变化!在C语言中,int类型的大小可以在不同的编译器和硬件平台上有所不同
误区:
所以我们应该把*放在p前面,这样可以更好区分数据类型
int *p,p1; //定义一个指向int*型数据的指针和一个int类型的p1
实际上int*是一个整体和其他数据类型一样!!!
这是因为C的语法规定了在声明多个变量时,只有第一个变量前的类型修饰符(如*
表示指针)会应用到所有后面声明的变量上,除非这些变量被明确地指定了不同的类型或修饰符。
在C语言中,定义变量时,在变量名前写一个 * 星号,这个变量就变成了对应变量类型的指针变量。必要时要加( ) 来避免优先级的问题
int (* parr )[3]; //parr是一个指向【包含3个int元素的数组】的指针变量
指针的操作
取地址:既然有了指针变量,那就得让他保存其它变量的地址,使用& 运算符取得一个变量的地址
int add(int a , int b)
{
return a + b;
}
int main(void)
{
int num = 97;
float score = 10.00F;
int arr[3] = {1,2,3};
//-----------------------
int* p_num = #
float* p_score = &score;
int (*p_arr)[3] = &arr;
int (*fp_add)(int ,int ) = &add; //p_add是指向函数add的函数指针
return 0;
}
特殊的情况,他们并不一定需要使用&取地址:
- 数组名的值就是这个数组的第一个元素的地址。
- 函数名的值就是这个函数的地址。
- 字符串字面值常量作为右值时,就是这个字符串对应的字符数组的名称,也就是这个字符串在内存中的地址。
#include <stdio.h>
int main() {
int arr[10] = {0};
printf("%p\n", (void*)arr); // 打印数组的地址,等同于&arr[0]
printf("%p\n", (void*)&arr[0]); // 显式地获取第一个元素的地址
}
数组与指针
数组名就是指针变量!
数组是一些相同数据类型的变量组成的集合,其数组名即为指向该数据类型的指针。数组的定义等效于申请内存、定义指针和初始化
char c[ ] = {0x33, 0x34, 0x35};
利用下标引用数组数据也等效于指针取内容:
#include <iostream>
using namespace::std;
int main()
{
char c[] = { 0x33, 0x34, 0x35 };
if (c[0]==*c)
{
cout << "true\n";
}
if (c[1] == *(c+1))
{
cout << "true";
}
}
注意事项
在对指针取内容之前,一定要确保指针指在了合法的位置,否则将会导致程序出现不可预知的错误 !!!
引用空指针:
尝试访问或修改空指针所指向的内存是未定义行为,通常会导致程序崩溃
int *ptr = NULL;
*ptr = 10; // 错误:解引用空指针
未初始化的指针:
未初始化的指针可能包含任何值,包括NULL或垃圾值。如果假设这样的指针不是NULL并尝试使用它,可能会导致不可预测的行为
int *ptr;
*ptr = 20; // 错误:ptr可能不是NULL,但也不是有效的内存地址
忘记检查返回值:
某些函数(如动态内存分配函数malloc
、calloc
和realloc
)在失败时会返回NULL。如果忘记检查这些函数的返回值,可能会错误地解引用一个空指针
int *ptr = malloc(10 * sizeof(int));
if (!ptr) {
// 处理错误
}
// 如果省略了上面的if语句,直接使用ptr可能会导致问题
错误的指针算术:
对空指针进行算术运算(如递增或递减)在C语言中是未定义的。虽然大多数现代编译器会将这些操作视为无操作(即空指针保持不变),但依赖这种未定义行为是不安全的
int *ptr = NULL;
ptr++; // 错误:对空指针进行算术运算
错误的比较:
虽然将指针与NULL进行比较是常见的做法,但错误地将指针与整数0或错误的指针值进行比较可能会导致问题
int *ptr = NULL;
if (ptr == 0) { // 正确,但不如使用NULL直观
// ...
}
if (ptr == (void*)0) { // 冗余且可能令人困惑
// ...
}
野指针:
野指针是指向已经被释放的内存的指针。虽然野指针本身不一定是NULL,但使用野指针(尤其是假设它指向有效内存)的行为与解引用空指针一样危险。
多重释放:
尝试释放一个已经被释放的指针(即使它之后被设置为NULL)也是错误的。这可能导致堆损坏或程序崩溃。
指针的应
指针的应用
传递参数
使用指针传递大容量的参数,主函数和子函数使用的是同一套数据,避免了参数传递过程中的数据复制,提高了运行效率,减少了内存占用。
使用指针传递输出参数,利用主函数和子函数使用同一套数据的特性,实现数据的返回,可实现多返回值函数的设计
直接访问物理地址下的数据
访问硬件指定内存下的数据,如设备ID号等
将复杂格式的数据转换为字节,方便通信与存储
传递返回值
将模块内的公有部分返回,让主函数持有模块的“句柄”,便于程序对指定对象的操作