C语言的指针
你好,这里是 Sunfor
这篇是我最近对于C语言指针的学习心得和错题整理
有任何错误欢迎指正,欢迎交流!
会持续更新,希望对你有所帮助,我们一起学习,一起进步
一、指针的概念
开始之前,我们先看一段顺口溜
指针指来又指去,地址不同指向异。
变量之间传递值,指针操作得心应手。
一级二级空间址,指针指向处处炫技。
指针操作需小心,否则bug如影随形。
1.内存和地址
数据在内存中读取处理后放回内存,为了高效管理空间;我们可以通过地址来查找
1.1内存
内存是计算机用来存储数据和指令的地方
它包括:
- 栈:用于存储函数的局部变量和调用信息,自动管理
- 堆:用于动态分配内存,程序员需要动手管理
- 数据段:存储全局变量和静态变量
- 代码段:存储程序的可执行代码
1.2地址
首先:
在生活中,我们的地址详细到家的门牌号,这样外卖才能准确地送到我们手里
我们联想一下:
为了高效地管理数据,我们也要将我们的内存进行编号,内存单元的编号也称为地址,C语言中给地址起名为指针
内存单元的编号 == 地址 == 指针
2.指针变量
2.1指针变量的概念
指针变量是用来存储内存地址的变量
2.2指针变量的大小
- 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
- 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
- 注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的
二、指针的意义
- 动态内存管理:可以动态分配和释放内存,灵活处理数据存储
- 高效的数据处理:通过指针,可以快速访问和修改数据,特别是在处理大数据结构或数组时
- 函数传参:指针允许通过引用传递参数,避免复制大量数据,提高性能
实现复杂数据结构:如链表、树等数据结构,指针是构建和操作这些结构的基础
三、指针类型
- 整型指针(int*)
- 字符型指针(char*)
- 浮点型指针(float*)
- 双精度浮点型指针(double*)
- 结构体指针(struct StructName*)
- 函数指针(void (*func_ptr)())
了解了这些指针类型之后,或许我们会想:为什么我们需要有指针类型呢?刚才不是说指针的大小与指针类型无关吗,那指针类型的意义是什么?
- 指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)
- 指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)
四、指针运算
我们在上面提到的指针类型对于指针运算的意义很大
我们举个例子来看看
#include<stdio.h>
int main()
{
int n = 10;
char* ptr1 = (char*)&n;
int* ptr2 = &n;
printf("%p\n", &n);
printf("%p\n", ptr1);
printf("%p\n", ptr1+1);
printf("%p\n", ptr2);
printf("%p\n", ptr2+1);
return 0;
}
运行结果如下
观察以上代码和运行结果,我们可以发现:
- char类型的指针+1 跳过一个字节
- int 类型的指针+1 跳过四个字节
1.指针 + - 整数
我们通过例子来了解一下
int main()
{
int arr[] = {10,20,30,40,50};
int* ptr = arr;
printf("初始值为:%d\n", *ptr);
printf("指针+1后:%d\n", *(ptr + 1));
printf("指针+2后:%d\n", *(ptr + 2));
printf("指针-3后:%d\n", *(ptr - 3));//这样可能会出现越界访问的问题
return 0;
}
运行结果如下
原始指针 ptr 指向 arr[0],然后 ptr - 3 实际上是指向 arr[0 - 3],即 arr[-3]
这种访问是越界的,不应该在正常情况下访问
正常情况下,未定义的行为可能导致程序崩溃或显示意外的值(如 0)
修改后的代码为:
int main()
{
int arr[] = { 10,20,30,40,50 };
int* ptr = arr;
printf("初始值为:%d\n", *ptr);
ptr = ptr + 1;
printf("指针+1后:%d\n", *ptr);
ptr = ptr + 2;
printf("指针+2后:%d\n", *ptr);
ptr = ptr - 3;
printf("指针-3后:%d\n", *ptr);
return 0;
}
运行结果为:
对比发现:这次的运行结果就是在原来移动过的基础上再移动,不会出现刚才那样未定义的情况了
2.指针 + - 指针
还是一样 通过例子来看
#include<stdio.h>
int main()
{
int arr[] = { 100,200,300,400 };
int* ptr1 = &arr[1];
int* ptr2 = &arr[3];
//计算指针的差值
ptrdiff_t diff = ptr2 - ptr1;
printf("指针差值为:%ld\n", diff);
//指针之间的相对关系
int* ptr3 = ptr1 + diff;
//指针移动后的值
printf("指针移动后的值为:%d", *ptr3);
return 0;
}
运行结果如下:
这里的 2 表示ptr2在ptr1的后面两位
400 是在指针移动后解引用的结果
3.指针的关系运算
我们还是通过例子来看
#include<stdio.h>
int main()
{
int arr[] = { 10,20,30,40,50 };
int* ptr1 = &arr[1];
int* ptr2 = &arr[3];
int* ptr3 = &arr[1];
if (*ptr1 == *ptr2)
{
printf("ptr1和ptr2指向同一位置\n");
}
else
{
printf("ptr1和ptr2不指向同一位置\n");
}
printf("\n");
if (*ptr1 == *ptr3)
{
printf("ptr1和ptr3指向同一位置\n");
}
else
{
printf("ptr1和ptr3不指向同一位置\n");
}
return 0;
}
运行结果如下
需要注意的是:
关系运算仅在同一数组或同一对象的元素之间有效
跨越不同数组或对象的指针是不可比较的
五、野指针
野指针是指未被初始化,或者已经释放的指针,它们指向一个不确定的内存位置。
我们甚至可以将野指针想象成一只野狗,野狗在街上徘徊,没人知道它的去向和行为。
它可能会去任何地方,做出不可预测的事情,甚至可能对周围的人和环境造成伤害
因此,我们需要小心处理指针,确保它们始终指向有效的内存位置
1.野指针的成因
知道什么是野指针之后,我们也需要知道为什么会形成野指针呢?
正如我上面所提到的,主要有三个原因:
- 指针未初始化
- 指针越界访问
- 指针指向的空间释放
1.1指针未初始化
我们还是通过例子来看
#include<stdio.h>
int main()
{
int* p;
*p = 50;
return 0;
}
上面的 *p 就是还未初始化的指针,系统默认给一个随机值
1.2指针越界访问
越界访问我们在之前举的例子中就有所提及,我们再举个例子来看看
#include<stdio.h>
int main()
{
int arr[10] = {0};
int* ptr = &arr[0];
int i = 0;
for (i = 0; i <= 12; i++)//超过数组所在范围
{
*(ptr++) = i;
}
return 0;
}
我们来看一下运行结果
1.3指针指向的空间释放
我们还是通过例子来看
#include <stdio.h>
#include <stdlib.h>
int main() {
// 动态分配内存
int* ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) {
perror("Failed to allocate memory");
return 1;
}
// 为分配的内存赋值
*ptr = 42;
printf("Value before free: %d\n", *ptr);
// 释放内存
free(ptr);
// 释放后 ptr 变成野指针,访问它会导致未定义行为
// 注意:下面的代码可能会导致程序崩溃或输出垃圾值
printf("Value after free: %d\n", *ptr);
// 对野指针进行操作后,最好将指针置为 NULL
ptr = NULL;
return 0;
}
运行结果如下
2.如何规避野指针
知道野指针形成的三个原因之后,我们就明白了该如何规避野指针
- 指针初始化
- 小心指针越界
- 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
六、指针的使用
了解了以上指针的类型和功能之后,更重要的是我们该如何使用指针呢?
1.取地址操作符、解引用操作符
首先我们要清楚两个概念
- 取地址操作符(&)
- 解引用操作符(*)
取地址操作符&
作用:获取变量的内存地址
& 操作符 用于返回一个变量的内存地址
int a = 10;
int* ptr = &a;//取地址操作符将变量a的地址赋值给指针ptr
解引用操作符*
作用:访问指针所指向的内存地址中的值
*操作符 用于获取指针所指向的地址中的值,也可以用来修改这个值
int a = 10;
int *ptr = &a;//取地址操作符
int b = *ptr;//解引用操作符,将指针ptr指向的值赋给b
*ptr = 20;//通过解引用操作符修改ptr指向的值
2.动态内存分布
指针在动态内存分配中扮演者重要的角色
主要通过动态内存管理函数来实现
我们还是通过例子来看
int main() {
int* ptr = (int*)malloc(sizeof(int));//使用 malloc 函数分配足够存储一个 int 类型的内存块
//检查内存分配是否成功
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
//通过指针访问和修改分配的内存
*ptr = 20;
printf("%d\n", *ptr);
//释放已分配的内存以防止内存泄漏
free(ptr);
return 0;
}
3.释放内存
通过上面的示例中我们也可以观察到释放内存可以帮助防止内存泄漏,内存泄漏发生时,程序不断分配内存未释放,导致系统资源耗尽
所以我们可以通过free()函数去释放不再使用的内存
七、指针VS数组
指针和数组在C语言中非常紧密地相关。
数组名可以被视为指向第一个元素的指针
指针可以用于访问和操作数组元素
1.数组名的理解
数组名就是数组首元素(第⼀个元素)的地址
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* ptr1 = &arr[0];
int* ptr2 = arr;
printf("arr[0] = %d\n", *ptr1);
printf("arr = %d\n", *ptr2);
return 0;
}
打印结果如下
但是我们也要注意有两个例外
- sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
- &数组名,这里的数组名表示整个数组,取出的是整个数组的地址
2.一维数组传参的本质
本质上数组传参传递的就是数组首元素的地址
因此,一维数组传参形参的部分可以写成数组的形式,也可以写成指针的形式
void test1(int arr[])//参数写成数组形式,本质上还是指针
{
printf("%zd\n", sizeof(arr));
}
void test2(int* arr)//参数写成指针形式
{
printf("%zd\n", sizeof(arr));//计算一个指针变量的大小
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
test1(arr);
test2(arr);
return 0;
}
运行结果如下
3.二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放再哪里呢?
这时候二级指针就该上场啦!
我们先通过一个例子来感受一下
int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa;
return 0;
}
结合图像来看是这样
*ppa通过对ppa中的地址解引用可以找到pa
*pa 通过对pa中的地址解引用可以找到a
4.指针数组
知道指针和数组的关系之后,我们或许会好奇何为指针数组,指针数组是指针还是数组呢?
或许我们可以通过之前所学习的数组的知识,来进行类比归纳
整型数组:存放整型的数组
字符数组:存放字符的数组
指针数组:存放指针的数组
我们再次用图像来感受一下
5.指针数组模拟二维数组
了解以上知识之后,我们或许可以尝试使用指针数组来模拟二维数组
在编写代码之前,我们要整理出大概的思路:
首先,我们可以定义三个一维数组
其次,定义一个指针数组(每个指针指向一个一维数组)
最后,遍历指针数值中的每个一维数组,并打印每一个元素
#include<stdio.h>
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* parr[3] = { arr1,arr2,arr3 };
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
运行结果如下
我们还是可以通过图片来直观感受一下
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数 组中的元素
以上就是关于C语言指针初阶的内容,进阶内容后续即将更新~