C语言之玩转指针(初阶)

简介: 现在,我们终于来到了指针——C语言的精髓所在,当然也是C语言的核心所在。其重要程度就不必多说了。在C语言中,指针提供了动态操控内存的机制,强化了对数据结构的支持,且实现了访问硬件的功能。但是指针的这种能力以及其灵活性是有代价的,它很难掌握。不过也不要担心,本文将以循序渐进的方式带大家来初步的学习指针。

1.什么是指针以及指针和指针类型。

什么是指针?从根本上讲,指针(pointer)是一个值为内存地址的变量。如int类型变量的值为整数、char类型变量的值为字符、而指针变量的值是地址。

在计算机科学中,指针式编程语言的一个对象,利用地址,它的值直接指向存在电脑存储器中另一个地方的值,通过地址可以找到所需的变量单元,可以说地址指向该变量单元,总之将地址形象化的称为"指针",在将来我们可以通过它找到以它为地址的内存单元。


在真正讲解指针之前,我们有必要了解一下内存。


下面来看一个简单的代码

1.png

在我们初始化变量a时,即int a = 10,此时此刻在内存中开辟了一块空间,这里我们对变量a取出它的地址,可以使用&操作符,之后把a的地址放到变量p中去,此时变量p就是一个指针变量。

简单来说就是任何一个值,只要将其放在指针变量中去,那么这个值就会被当作地址来处理。


总结:指针就是变量,是用来存放地址的变量。(存放在地址中的值都会被当作地址处理)


下面来看各种类型指针的大小:

printf("%d\n",sizeof(int*));
printf("%d\n",sizeof(char*));
printf("%d\n",sizeof(double*));
printf("%d\n",sizeof(short*));


结果如下:

2.png

既然指针大小在这里都是8个字节,那我们为什么还区分那么多种类型的指针呢?例如:整型指针,字符型指针等等。具体如下代码:


int a=0x11223344;
int* pa=&a;
char* pc=&a;
printf("%p\n",pa);//结果为0000003F986FF504
printf("%p\n",pc);//结果为0000003F986FF504
那是不是就说明指针类型就没有意义呢,答案是否定的,它当然有意义,请看下面代码

3.png

当我们对指针变量pa解引用操作完成后(即*pa=0)发现变量a变成了0先记住这里,下面来看第二段代码:

4.png

注意看原来内存中的 44 33 22 11(4个字节),现在经过char*解引用后变成了

00 33 22 11,仅仅更改了1个字节;而经过之前的int*解引用后变成了00 00 00 00更改了4个字节。

所以,当类型发生变化时,我们对其解引用所产生的结果时不一样的,这是指针类型带来的区别之一,也是其意义之一。

当使用整型指针(即int*)进行解引用操作时,我们操作了4个字节后由44 33 22 11变成了00 00 00 00;但是如果是字符指针(即char*)只能操作1个字节,由原来的44 33 22 11变成了00 33 22 11。故指针类型意义一:指针类型决定了指针在解引用操作时能够访问空间的大小。即:


/

int* p; --*p能够访问4个字节
double* p;  --*p能够访问8个字节
char* p;  --*p能够访问1个字节
short* p;  --*p能够访问2个字节

下面来看一段代码:


在这里我们可以知道指针类型决定了指针类型+1能走多远(即决定了指针的步长)。这也就是指针类型的意义二:指针类型决定了指针向前或向后走一步有多大。


总结:指针类型的意义:1.指针类型决定了对指针解引用时有多大的访问权限(即能操作几个字节)。2.指针类型决定了指针向前或向后走一步有多大。

在明白了指针类型的意义后,下面我们来看一段代码:

int arr[10]={0};
for(int i=0;i<10;i++)
{
  *(p+i)=1;
}

6.png

当我们把其中的int* p=arr;改为char* p=arr;后,我们再来看一下区别,请看:

7.png

这里我们很好看出区别:这里的char* =arr;相当于把之前的int* p=arr;中的两个半整型更改为了1。其根本原因是因为char*每次只能访问1个字节,而int*每次能访问4个字节。


以上就是对指针的基本认识。


2.野指针

  • 概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)


2.1野指针成因

1.局部变量未初始化


#include<stdio.h>
int main()
{
  //未初始化的指针变量
  int *p;//局部变量未初始化,默认为随机值,此时程序会出问题
  *p=20;
  return 0;
}

2.指针越界访问

#include<stdio.h>
int main()
{
  int a[10]={0};
  int* p=a;
  int i=0;
  for(i=0;i<=12;i++)
  {
  //当指针指向的范围超过数组a的范围时,p就是野指针
  *p++=1;//注意这里为后置++
  }
  return 0;
}

这就是数组越界导致的野指针问题。

3.指针指向的空间被释放

int* test()
{
  int a = 20;//在test()函数中,变量a是一个局部变量a,而局部变量a在出这个函数时就会别销毁
  return &a;//也就是说出来这个函数时,局部变量a会把之前所占据的空间还给内存,既然还给了内存,那么局部变量a就不能使用了。
}
//所以当我们再在主函数中利用*p去访问变量a的空间时,此时就是非法访问,因为这块空间已经不属于变量a了。
#include<stdio.h>
int main()
{
  int* p = test();
  printf("%d", *p);
  return 0;
}

8.png

这里也许程序会正常运行(错误很隐晦是编译器可能会认为正确),但是这段代码简直是大错特错。

如果大家还不明白,我给大家举一个生活中的例子:比如小明交了个女朋友叫小兰,但是好了一段时间后,小兰因为又找了个男朋友就把小明给踹了,又由于小明在与小兰交往的时候把小兰的电话号码(指针)记了下来,于是小明就每天给小兰打电话,这难道不是非法骚扰吗。


2.2如何规避野指针问题

1.指针初始化

例如:

#include<stdio.h>
int main()
{
  int a = 10;
  int* p = &a;
  //如果我们不知道该给指针变量p初始化谁时我们可以给它初始化成空指针NULL
  int* p = NULL;//NULL是用来初始化指针的,是用来给指针赋值的
  return 0;
}

2.小心指针越界

3.指针指向的空间如果释放,则我们把指针置为空,即NULL。

#include<stdio.h>
int main()
{
  int a = 10;
  int* pa = &a;
  *pa = 20;//将a改为20后,我们如果不想让指针指向其它地方时,或者指针指向的那块空间已经还给操作系统时,我们就可以把这个指针置为空指针。
  pa = NULL;
  return 0;
}


当指针pa被置为空指针时,倘若要强行访问指针pa所指向的空间,此时程序很可能会崩溃掉。

例如:

#include<stdio.h>
int main()
{
  int a = 10;
  int* pa = &a;
  pa = NULL;
  *pa = 50;
  return 0;
}


/

4.指针使用之前检查有效性。

3.指针运算

3.1指针±整数

下面拿代码进行演示:

9.png

倘若我们把p=p+1(或者p++)换成p+=2呢?我们看看会发生什么:

10.png

倘若用减的方式呢:

11.png

下面在举一个指针+-的例子:

12.png

上述代码的功能是这样的:

13.png


3.2指针-指针

指针-指针就是地址-地址,但是得到的是其中间的元素个数,请看:

14.png


如果改为&arr[0]-&arr[9];呢?请看:

15.png


所以说如果是小地址减去大地址的话,得出来的数的绝对值是其中间元素的个数。

我们说指针-指针一定是指向同一个数组中的空间,那我们能不能这样写呢:

16.png

这样的写法是大错特错的,这样做的话带来的结果是不可预知的。这样的代码实际上也没多大的意义和价值。


下面来看这段代码(求字符串长度的一种实现方式):

17.png


3.3指针的关系运算

我们先观察这两种代码的一个不同:

18.png


这两种代码看似相同,但是未来我们非要在这两种代码之间做选择的话,我们应该选择第一种的代码。实际上在大部分的编译器上是可以顺利完成任务的,然而我们应该避免写成第二种代码,因为标准并不保证它可行。

标准规定:允许指向数组元素的指针与指向最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

19.png


也就是说指针p1可以和指针p2进行比较,但是不能拿指针p1和指针p3进行比较。

所以说刚刚代码中的第一种写法是不存在用指针p1和指针p3进行比较的。


4.指针和数组

我们知道,数组名是首元素的地址。下面我们再次来简单的证明一下:

20.png

一般情况下,数组名的确是首元素的地址。但这里有两个例外:


  • &arr–即&数组名–这里的数组名arr不是首元素地址,而是整个元素的地址,即&arr取出的是整个数组的地址(数组首元素地址和整个数组地址下面再来详细介绍。)
  • sizeof(arr)–即sizeof(数组名)–此时的数组名arr表示的是整个数组–sizeof(数组名)计算的是整个数组的大小。


再来看一段代码:

21.png

看到这里有些读者多少会有一些疑惑,既然数组名代表着首元素的地址,而&数组名代表着整个数组的地址,那为什么第一个结果和第三个结果打印出来的地址是一样的呢?下面请再来看一段代码:

22.png

23.png


相信看到这里大家对整个数组的地址有了一个比较清晰的认识。

结论:1.数组名和数组首元素的地址是一样的。(数组名表示的就是数组首元素的地址)

   2.数组名表示的是首元素的地址时有两个例外:sizeof(数组名),此时的数组名表示的是整个数组;(&数组名)中的数组名表示的是整个数组,即(&数组名)取出来的是整个数组的地址。


既然我们可以把数组名当成一个地址存放到一个指针中,那我们使用指针来访问一个数组就成为了可能,这时我们就可以把数组和指针联系起来。

int arr[10] = { 0 };
int* p = arr;//此时数组arr就可以通过指针变量p来进行访问
//即数组是可以通过指针来访问


下面我们通过一段代码来把指针和数组联系起来:

24.png

我们可以发现&arr[i]取出来的地址跟我们的指针变量p作为首元素地址,再来通过p+i取出来的地址是一样的。

所以我们当然通过指针来访问数组了。

比如:

25.png

总结:我们可以通过指针来访问数组来进行一系列的操作。


5.二级指针

我们之前学过了一级指针,比如:

int a = 10;
int* pa = &a;//pa就是一级指针变量,int*就是一级指针变量类型

我们来看上述代码中的变量pa,它是一个一级指针变量,既然是一个变量的话,就需要在内存中开辟一块空间。那我们能不能&pa呢?即从内存中拿到pa这块空间的地址,我们如果要把从内存中拿到pa这块空间的地址存起来应该怎么做呢?假如说我们要把一级指针变量pa的地址存放到ppa中,我们可以这样做:int** ppa = &pa;这里的ppa就是一个二级指针变量,而int**就是二级指针变量类型。依次类推我们当然也可以把二级指针ppa的地址存放起来,即int*** pppa = &ppa;。那么四级指针、五级指针、六级指针等等我们都可以写出来。

上述中的代码是这样的:

int* pa = &a;
int** ppa = &pa;
int*** pppa = &ppa;

可以配合下面这张图来进行理解:

26.png


指针变量也是变量,既然是变量就会有地址,那指针变量的地址存放到哪里呢?这就是二级指针。


那二级指针怎么使用呢?还是以上面的代码为例,倘若我们想要通过二级指针变量ppa来打印变量a中的值10,我们可以这样:

27.png

我们可以这样理解上述代码:*ppa的意思是对ppa进行解引用操作来找到二级指针变量ppa指向的对象pa,在对*ppa进行解引用操作即**ppa就可以找到变量a中存储的值(10)了。

我们也可以通过二级指针变量ppa来改变变量a中的值,请看:

28.png

注意此时变量a中的值就变为了20。

所以二级指针只不过是用来存放一级指针变量地址的东西。


6.指针数组

指针数组,指针数组,那它当然是一个数组了,只不过数组中存放的是指针而已,所以大家在学指针数组时不要害怕😰,指针数组本质上就是一个数组,是一个存放指针的数组。

int a = 10;
int b = 20;
int c = 30;
int* pa = &a;
int* pb = &b;
int* pc = &c;

我们可以通过指针数组把变量a b c的地址存放到一起,即存放到一个指针数组中去。请看:

int a = 10;
int b = 20;
int c = 30;
int arr1[10];//arr1是一个存放整型的数组
int* arr2[3] = { &a,&b,&c };//arr2是一个存放整型指针的数组
5

我们也可以通过指针数组把变量a,变量b,变量c中的值打印出来。请看:

29.png

这里一定要注意,arr2[i]代表的是变量a,变量b,变量c的地址,所以我们还需要对其进行解引用操作,即*(arr2[i])才能找到变量a,变量b,变量c中的值,并将其打印。

30.png

最后,初阶部分的指针学习到这里就完全可以了。后面会给大家带来指针的进阶部分。

目录
相关文章
|
3月前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
62 0
|
1月前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
84 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
1月前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
54 9
|
1月前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
45 7
|
2月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
151 13
|
2月前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
2月前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
129 3
|
2月前
|
存储 C语言 开发者
C 语言指针与内存管理
C语言中的指针与内存管理是编程的核心概念。指针用于存储变量的内存地址,实现数据的间接访问和操作;内存管理涉及动态分配(如malloc、free函数)和释放内存,确保程序高效运行并避免内存泄漏。掌握这两者对于编写高质量的C语言程序至关重要。
62 11
|
2月前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
2月前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
44 1