久违的键盘声,熟悉的思绪,仿佛时间在这一刻凝固。距离我上一次敲击键盘写下文字,已不知过了多少个日夜。但文字的魅力就在于,它总能跨越时间的长河,将我们的心灵再次相连。今天,我带着满心的感慨与新的故事,重新坐到了屏幕前。让我们一起,再次启程,探索文字的奥秘。
-
(一)理解指针
-
1. 内存和地址
-
1.1 内存
- 在讲内存之前,我们想一个生活案例:
- 假设有⼀栋宿舍楼,把你放在楼⾥,楼上有100个房间,但是房间没有编号,你的⼀个朋友来找你玩,如果想找到你,就得挨个房⼦去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:
-
1 ⼀楼:101,102,103...
2 ⼆楼:201,202,203....
3 ... - 有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。
-
我们知道计算上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那我们电脑上内存是8GB/16GB/32GB等,那这些内存空间如何⾼效的管理呢?
其实也是把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩取1个字节。 -
1 bit - ⽐特位 1byte = 8bit
2 byte - 字节 1KB = 1024byte
3 KB 1MB = 1024KB
4 MB 1GB = 1024MB
5 GB 1TB = 1024GB
6 TB 1PB = 1024TB
7 PB -
1.2 地址
-
1.2.1 取地址操作符(&)
- 在c语言中,创建变量就是向内存申请空间,比如:
⽐如,上述的代码就是创建了整型变量a,内存中申请4个字节,⽤于存放整数10,其中每个字节都有地址,上图中4个字节的地址分别是:
1 0x000000ECAB90FA34
2 0x000000ECAB90FA35
3 0x000000ECAB90FA36
4 0x000000ECAB90FA37
那我们如何能得到a的地址呢?
这⾥就得学习⼀个操作符(&)-取地址操作符
#include<stdio.h>
int main()
{
int a = 10;
printf("%p\n", &a);
return 0;
}
按照我画的例子,最后打印的值是:000000ECAB90FA34 (&a是取出所占四个字节中地址较小字节的地址)
1.3 指针变量和解引用操作符
1.3.1 指针变量
我们通过取地址操作符(&)拿到的地址是⼀个数值,⽐如:0x000000ECAB90FA34 ,这个数值有时候也是需要存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在指针变量中。
比如:
#include<stdio.h>
int main()
{
int a = 10;int *ps = &a;
return 0;
}
指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。
注意:
如:
int a = 50;
int* ps = &a;
这⾥pa左边写的是 int* , * 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向的是整型(int)类型的对象。
1.3.2 解引用操作符
在现实⽣活中,我们使⽤地址要找到⼀个房间,在房间⾥可以拿去或者存放物品。
C语⾔中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这⾥必须学习⼀个操作符叫解引⽤操作符(*)。
1 #include <stdio.h>
23 int main()
4 {
5 int a = 100;
6 int* pa = &a;
7 *pa = 0;
8 return 0;
9 }
上⾯代码中第7⾏就使⽤了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间,*pa其实就是a变量了;所以*pa=0,这个操作符是把a改成了0.
1.3.3 指针变量的大小
32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。
如果指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变的⼤⼩就是8个字节。
如下代码:
注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。
1.3.4 指针变量类型的意义
指针变量的⼤⼩和类型⽆关,只要是指针变量,在同⼀个平台下,⼤⼩都是⼀样的。
指针类型是有特殊意义的,我们接下来继续学习。
(一)指针的解引用
我们可以看到,int* ps会将n的4个字节全部改为0,但是char* pr只是将n的第⼀个字节改为0。
结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。
当然,肯定会有同学问,为什么前面地址位数那么多,而这就8位呢?(这就是x86环境和x64环境的区别了),大家看下图就自然知道了。
2 、 指针运算
2.1指针+-整数
咱们先来看一段代码以及他们的运行情况:
我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤。
其次,因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素,即:
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));}
return 0;
}在上述代码中,arr[ i ] == *(ps+i)
2.2 指针-指针
指针-指针等于整数(指针--指针=|整数|)【前提条件:两指针指向同一空间】
指针减指针运算的前提是两个指针必须指向同一块连续的内存空间,通常是同一个数组或者是通过动态内存分配获得的连续内存块。这样的运算得到的结果是两个指针之间相隔的元素个数的绝对值. 如果两个指针指向的内存空间不连续,则指针减运算的结果是未定义的,这可能导致程序崩溃或产生不可预测的行为
#include <stdio.h>
int strlen_m(char* s)
{
char* p = s;
while (*p != '\0')
p++;
return p - s;
}
int main()
{
printf("%d\n", strlen_m("abcdrfjhg"));
return 0;
}
上述代码为指针—指针简易用法之一。
2.3 指针比大小
指针比大小其实就是比较地址大小,需特别注意两点:
(一)、类型一致性:两个指针比较大小时,它们必须是相同类型的指针,或者至少是指向兼容类型的数据的指针。
(二)地址空间:指针必须指向同一个数据段或地址空间中的内存地址,否则比较结果是未定义的
(二)指针类型
指针的类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:
(1)int*ptr;//指针的类型是int*
(2)char*ptr;//指针的类型是char*
(3)int**ptr;//指针的类型是int**
(4)int(*ptr)[3];//指针的类型是int(*)[3]
(5)int*(*ptr)[4];//指针的类型是int*(*)[4]
怎么样?找出指针的类型的方法是不是很简单?
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如:
(1)int*ptr; //指针所指向的类型是int
(2)char*ptr; //指针所指向的的类型是char
(3)int**ptr; //指针所指向的的类型是int*
(4)int(*ptr)[8]; //指针所指向的的类型是int()[3]
(5)int*(*ptr)[4]; //指针所指向的的类型是int*()[4]
在指针的算术运算中,指针所指向的类型有很大的作用。
指针的类型(即指针本身的类型)和指针所指向的类型是两个概念,大家学指针必须弄清楚他两的区别,不然容易晕。
-
1、基本数据类型指针
- 基本数据类型指针的声明格式为 数据类型 *指针变量名;。例如,声明一个指向整数的指针可以写作 int *ptr;。初始化指针时,可以将其设置为指向具体的变量地址,或者使用 NULL 表示指针当前不指向任何有效内存地址。
-
int* ptr;
char* ptr;
float* ptr;
double* ptr;
-
(一)枚举指针
-
枚举指针是c语言中的一种数据类型,它可以枚举出一组特定的值并赋予它们一个符号名。通常用于叙述程序中的状态或者选项。
-
枚举指针定义格式为:enum 枚举类型名{枚举1 , 枚举2,.....};
-
枚举指针可以用于判断、比较和赋值等操作,常与switch语句一起使用。如下代码
-
#include <stdio.h>
typedef enum {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
} Weekday;int main() {
Weekday d = MONDAY; // Assigning Wednesday to the variable d
Weekday* pa = &d;
*pa = SUNDAY;
printf("Today is: %d\n", d);
return 0;
}//最终输出结果是Today is: 6 -
2、数组指针和指针数组
-
2.1指针数组
-
我们类⽐⼀下,整型数组,是存放整型的数组,字符数组是存放字符的数组。那么指针数组就是存放指针的数组。如下图:
-
巧妙运用指针数组实现二维数组,如下代码:
-
#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数组中
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;
}//输出结果如下:
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7 -
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。
上述的代码模拟出⼆维数组的效果,实际上并⾮完全是⼆维数组,因为每⼀⾏并⾮是连续的。 -
2.2 数组指针
- 我们学习了指针数组,指针数组是⼀种数组,数组中存放的是地址(指针)。
- 那么数组指针变量是指针变量?还是数组?
答案是:指针变量。 - 整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。
- 那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
-
大家可以分辨一下下列哪一个是数组指针
-
int *p1[10];
int (*p2)[10];
//p2是指针,指针指向的是数组,数组有10个元素,每个元素类型是int,即p2是指向数组的指针,p1[10]则是存放指针的指针数组。
-
解释:p2先和*结合,说明p是⼀个指针变量变量,然后指着指向的是⼀个⼤⼩为10个整型的数组。所以p2是⼀个指针,指向⼀个数组,叫数组指针。
这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。 -
我们先来写一段简单的数组指针代码:
-
说明&arr和p是完全一致的
-
3、函数指针和函数指针数组
-
3.1函数指针
- 根据前⾯学习整型指针,数组指针的时候,我们的类⽐关系,我们不难得出结论:函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的。而肯定有同学有疑问:函数是否有地址呢,我们不妨做一个测试:
- 结果相信大家也看到了,最终函数地址被打印了出来,函数名就是函数的地址,也可以通过 &函数名的方式得到函数的地址
- 和之前讲的相似,我们要将函数地址储存起来就要用到函数指针,函数指针的变量写法其实和数组指针相似,如下:
-
int sub( int x , int y)
{
return x - y;)
int (*ps)(int x , int y) = ⊂
int (*ps)(int , int) = sub; //x和y省略或者加上都可以的
-
我们再来看一段函数指针的简易用法:
-
如图所示,因为函数名就是代表函数地址,所以函数指针其实与函数名等价,所以其解引用符号(*)可以省略的。
-
函数指针扩展:
-
(*(void (*)()) 0);
-
解释:此代码是一个函数调用,【1.把0强制类型转换为一个函数地址,这个函数无参数,返回类型是void型,去调用0地址的函数】
-
3.2函数指针数组
-
数组是⼀个存放相同类型数据的存储空间,我们已经学习了指针数组
-
要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
-
int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3]; -
最终是parr1才是函数指针数组的正确表示形式,parr1 先和 [ ] 结合,说明parr1是数组,数组的内容是int (*)() 类型的函数指针。
-
4、指向指针的指针(多级指针)
-
4.1多级指针原理
指针的本质就是一个普通变量,它的值表示的是一个内存地址,这个地址中可能存放了其它变量。那么二级指针其实也是一个普通的变量,这个变量中同样也存放了一个内存地址,而这个内存地址是一个指针变量的地址。例如:
int a = 100;
int *p1 = &a;
int **p2 = &p1;
-
如果我们需要修改a的值除了直接对a赋值之外,还可以通过*p来实现,即:
*p1 = 2;
经过这样的操作之后,a的值就被修改为了2。
如果我们要修改p2的值,想要使其指向b,也是有两种方法,可以直接修改p的值,也可以使用它的二级指针*p2来修改p的值,即:
p1= &a;
*p2 = &a; -
这两种方法的结果是一样的。同理,对于多级针指来说,*()就是修改其指向地址的变量内容。关于三级指针、四级指针或更高级指针来讲原理都是一样的
-
当然,本章旨在介绍各指针,其用法众多例如:链表等,本章就不再赘述,
-
5、结构体指针
-
结构体指针大家可以看这位博主介绍较为详细:
原文链接:https://blog.csdn.net/lipengyu1363658871/article/details/113974032 -
6、通用指针(void指针)
-
在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进⾏指针的+-整数和解引⽤的运算。
-
这⾥我们可以看到, void* 类型的指针可以接收不同类型的地址,但是⽆法直接进⾏指针运算。
-
⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得⼀个函数来处理多种类型的数据。
-
7、空指针和野指针
-
7.1野指针
-
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
-
7.1.2野指针成因
-
1. 指针未初始化
-
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}2 指针越界访问
-
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i <= 11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针*(p++) = i;
}
return 0;
}3. 指针指向的空间释放
-
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}7.1.2如何规避野指针
-
1、指针初始化
- 如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL。NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。
-
#include <stdio.h>
int main()
{
int num = 10;
int* p1 = #
int* p2 = NULL;return 0;
}2、指针越界访问
- ⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
-
3、避免返回局部变量地址
- 如造成野指针的第3个例⼦,不要返回局部变量的地址。
-
7.2、空指针
-
空指针:是指没有指向任何一个存储单元的指针。当我们需要用到指针,但不确定指针指向何处时使用。
-
int* p1=0; //0是唯一不必转换就可以赋值给指针的数据,在ASCII编码中,编号为0的字符就是空
int* p2=NULL; //NULL是一个宏定义,起作用和0相同
int x=10;
int* p=NULL;//指针指向空
p=&x;
在编程时,一般也是先将指针初始化为空,再对其进行赋值操作。
记住,掌握指针不是终点,而是通往更深层次编程艺术的起点。它教会我们如何更高效地管理资源,如何在复杂系统中游刃有余。但更重要的是,它让我们懂得,每一个0和1的背后,都是对逻辑与创造的不懈追求
-
今天中秋,祝各位中秋快乐,岁岁年年,学业有成!感觉这篇文章对朋友你有帮助的话,帮忙点个三连