C语言进阶之路--精讲深入剖析指针(从入门到全面)

目录

前言:判断指针

1 基础类型

2 复杂类型

一 、详解指针

1 指针的类型

2 指针所指向的类型 

3 指针的值(指针所指向的内存区/地址)

4 指针本身所占据的内存区 

二、野指针 

1 概念

2 产生原因 

3 避免野指针的基本原则 

三、运算符&和* 

四、指针表达式 

五、指针与数组的关系 

六、指针与结构类型的关系 

七、指针与函数的关系 

八、指针数组 


   嗨嗨大家好呀~今天我来分享有关于指针的相关知识,你们在刚学的时候是不是有些懵懵的状态呢?那么本期博主的分享就是针对于小白或是对指针还没有真正了解的小伙伴们,一定要看完哦!接下来让我们一起来探索其中的奥秘吧!

注意:前言是基础!一定要看!一定要看!认真看!!!


前言:判断指针

   想要了解指针,多多少少会出现一些比较复杂的类型,所以在这里先介绍一下如何完全理解一个复杂类型,要理解复杂类型其实很简单,一个类型里会出现很多运算符,它们也像普通的表达式一样,有优先级,其优先级和运算优先级一样,所以我大概总结了一下相应的原则:从变量名处起,根据运算符优先级结合,一步一步进行分析。下面让我们先从简单的类型开始慢慢分析吧:

1 基础类型

  •  int a:// 这是一个普通的整型变量。
  •  int* a :// 我们从 a 开始往前分析:首先 a 与 * 结合,这就说明了 a 是一个指针;然后再与 int 结合,说明该指针所指向内容的类型是整(int)型。所以 a 是一个返回整型数据的指针
  •  int a[5]: // 首先从 a 开始,先与[ ]结合,说明 a 是一个数组,然后与 int 结合,说明数组里的元素是整型的,所以 a 是一个由整型数据组成的数组
  •  int* a[5]:// 首先 a 与 [ ] 结合,这是因为 [ ] 的优先级比 * 高,这是 a 是一个数组,然后再与 * 结合,说明数组里的元素是指针类型,最后再与 int 结合,说明该指针所指向内容的类型是整(int)型。所以 a 是一个由返回整型数据的指针所组成的数组
  •  int (*a)[3]: // 这里的()是为了改变优先级, a 先与 * 结合,说明 a 是一个指针;然后再与 [ ] 结合,说明指针所指向的内容是一个数组,然后再与 int 结合,说明数组里的元素是整型的。所以 a  是一个指向由整型数据组成的数组的指针

我们掌握以上的基础后,来进一步研究复杂一点的类型:

2 复杂类型

  •  int **a:// 首先从 a 开始,先与 * 结合,说是 a 是一个指针,然后再与 * 结合,说明指针所指向的元素是指针,然后再与 int 结合,说明该指针所指向的元素是整型数据。由于更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑二级指针。
  •  int a(int):// 从 a 处起,先与 () 结合,说明 a 是一个函数,然后进入 () 里分析,说明该函数有一个整型变量的参数,然后再与外面的 int 结合,说明函数的返回值是一个整型数据
  •  int (*a)(int):// 从 a 处开始,先与指针结合,说明 a 是一个指针,然后与 () 结合,说明指针指向的是一个函数,然后再与 () 里的 int 结合,说明函数有一个 int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以 a 是一个指向有一个整型参数且返回类型为整型的函数的指针
  •  int *(*a(int))[3]:// 我们从 a 开始进行分析,先与 () 结合,说明 a 是一个函数,然后进入 () 里面,与 int 结合,说明函数有一个整型变量参数,然后再与外面的 * 结合,说明函数返回的是一个指针,然后到最外面一层,先与 [ ] 结合,说明返回的指针指向的是一个数组,然后再与 * 结合,说明数组里的元素是指针,然后再与 int 结合,说明指针指向的内容是整型数据。所以 a 是一个参数为一个整型数据且返回一个指向由整型指针变量组成的数组的指针变量的函数

到这里常见的指针类型基本讲完了,相信大家理解了这几个类型,在脑海中对指针内容有了新的认知,上面的这些类型足以供我们日常使用了。现在我们往下进行新的内容:

一 、详解指针

   指针,是C语言中的一个重要概念及其特点,也是掌握C语言比较困难的部分。指针也就是内存地址,指针变量是用来存放内存地址的变量,在同一CPU构架下,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。 为方便大家理解举例如下图:

   指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。要搞清一个指针需要搞清指针的四方面的内容:指针的类型指针所指向的类型指针的值或者叫指针所指向的内存区指针本身所占据的内存区。下面我们来详细地说明:

1 指针的类型

   从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。

首先举第一个例子:

//我们来看这几种类型:

  int*ptr; 
  char*ptr; 
  int**ptr; 
  int(*ptr)[3]; 
  int*(*ptr)[4];
  • int*ptr:// 指针的类型是int*
  • char*ptr:// 指针的类型是char*
  • int**ptr:// 指针的类型是int**
  • int(*ptr)[3]:// 指针的类型是int(*)[3]
  • int*(*ptr)[4]:// 指针的类型是int*(*)[4] 

如此看来,找出指针的类型是不是非常容易? 

2 指针所指向的类型 

   当你通过指针来访问指针所指向的内存块时,指针所指向的类型决定了编译器将把那片内存块里的内容当做什么来看待。

   从语法上看,你只需把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。

例如:

  • int*ptr: // 指针所指向的类型是int
  • char*ptr:// 指针所指向的的类型是char
  • int**ptr:// 指针所指向的的类型是int*
  • int(*ptr)[3]:// 指针所指向的的类型是int()[3]
  • int*(*ptr)[4]:// 指针所指向的的类型是int*()[4]

   指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C语言越来越熟悉时,你会发现,把与指针搅和在一起的“类型"这个概念分成"指针的类型"和"指针所指向的类型"两个概念,是精通指针的关键点。 

注:千万不要把两个概念混淆!!

3 指针的值(指针所指向的内存区/地址)

   指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。

   指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX 为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在第一个例子中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。

所以在未来,每遇到一个指针,都应该问问自己:

这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?(重点)

4 指针本身所占据的内存区 

   在32 位机器上,所有类型的指针的值都是一个32位整数,因为32 位机器上内存地址全都是32 位长,是4个字节,所以指针变量的大小也是4个字节;同理,在64位机器上,所有类型的指针的值都是一个64位整数,因为64位机器上内存地址全都是64位长,是8个字节,所以指针变量的大小也是8个字节。

现在我们来举第二个例子:

char a[15];
int* ptr=(int*)a;
//这里对a进行了强制类型转换,但并不会改变a本身的类型
ptr++;

在上面的例子中,指针ptr 的类型是int*,它指向的类型是int,它被初始化为指向整型变量a。接下来的第3句(ptr++)中,指针 ptr 被加了1,编译器是这样处理的:它把指针ptr 的值加上了sizeof(int),在32位机器上,是被加上了4,因为在32位机器上,int 占4 个字节。因为地址是用字节做单位的,所以 ptr 所指向的地址由原来的变量 a 的地址向高地址方向增加了4 个字节。由于char 类型的长度是1个字节,所以,原来 ptr 是指向数组 a 的第 0 号单元开始的4个字节,此时指向了数组a 中从第4 号单元开始的四个字节。 用文字表达或许难以理解,下面来画图表达:

二、野指针 

1 概念

   野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。

这里要注意:

  •   指针变量中的值是非法内存地址,进而形成野指针
  •    野指针不是NULL指针,是指向不可用内存地址的指针
  •    NULL指针并无危害,很好判断,也很好调试
  •    C语言中无法判断一个指针所保存的地址是否合法

2 产生原因 

  •  指针变量未初始化

任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所  以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。如果没有初始化,编译器会报错“ ‘point’ may be uninitializedin the function ”。

  • 指针释放后之后未置空

有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。

  • 指针操作超越变量作用域

不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

结论:

  (1)局部指针变量没有初始化

  (2)指针所指向的变量在指针之前被销毁

  (3)使用已经释放过的指针

  (4)进行了错误指针运算

  (5)进行了错误的强制类型转换

3 避免野指针的基本原则 

  •   (1)绝不返回局部变量和局部数组的地址
  •   (2)任何变量在定义后必须0初始化
  •   (3)字符数组必须确认0结束符后才能成为字符串
  •   (4)任何使用与内存操作相关的函数必须指定长度信息

三、运算符&和* 

这里&是取地址运算符,*是间接运算符。

&a 的运算结果是一个指针,指针的类型是a 的类型加个*,指针所指向的类型是a 的类型,指针所指向的地址嘛,那就是a 的地址。

*p 的运算结果就五花八门了。总之*p 的结果是p 所指向的东西,这个东西有这些特点:它的类型是p 指向的类型,它所占用的地址是p所指向的地址。

 下面用代码和注释进行举例:


int a=12; int b; int *p; int **ptr; 

p=&a; 
//&a的结果是一个指针,类型是int*,指向的类型是int,指向的地址是a 的地址

*p=24; 
//*p 的结果,在这里它的类型是int,它所占用的地址是p所指向的地址,显然,*p 就是变量a

ptr=&p; 
//&p 的结果是个指针,该指针的类型是p 的类型加个*, 
//在这里是int **。该指针所指向的类型是p 的类型,这里是int*。
//该指针所指向的地址就是指针p 自己的地址。 

*ptr=&b; 
//*ptr 是个指针,&b 的结果也是个指针,且这两个指针 
//的类型和所指向的类型是一样的,所以用&b 来给*ptr 赋值就没有问题了。 

**ptr=34; 
//*ptr 的结果是ptr 所指向的东西,在这里是一个指针, 
//对这个指针再做一次*运算,结果是一个int 类型的变量。

四、指针表达式 

如果一个表达式的结果是一个指针,那么这个表达式就叫指针表式。

下面是举一些指针表达式的例子:

int a,b; 
int array[10]; 
int *pa; 
pa=&a; //&a 是一个指针表达式。 
int **ptr=&pa; //&pa 也是一个指针表达式。 
*ptr=&b; //*ptr 和&b 都是指针表达式。 
pa=array; 
pa++; //这也是指针表达式。

char *arr[20]; 
char **parr=arr; //如果把arr 看作指针的话,arr 也是指针表达式 
char *str; 
str=*parr; //*parr 是指针表达式 
str=*(parr+1); //*(parr+1)是指针表达式 
str=*(parr+2); //*(parr+2)是指针表达式

 因为指针表达式的结果是一个指针,所以指针表达式也具有指针所具有的四个要素:指针的类型,指针所指向的类型,指针指向的内存区,指针自身占据的内存。

五、指针与数组的关系 

数组的数组名可以看作一个指针,下面举例来看:

int arr[10]={0,1,2,3,4,5,6,7,8,9},value; 
value=arr[0]; //也可写成:value=*arr; 
value=arr[3]; //也可写成:value=*(arr+3); 
value=arr[4]; //也可写成:value=*(arr+4);

   上例中,一般而言数组名 arr 代表数组本身,类型是 int[10],但如果把 arr 看做指针的话,它指向数组的第 0 个单元,类型是 int* 所指向的类型是数组单元的类型即 int 。因此 *arr 等于0 就一点也不奇怪了。同理,arr+3 是一个指向数组第3 个单元的指针,所以*(arr+3)等于3。其它依此类推。 

我们再举另外一个例子:

char *str[3]={ 

     "Hello,thisisasample!", 

     "Hi,goodmorning.", 

     "Helloworld" 

}; 
char s[80]; 
strcpy(s,str[0]); //也可写成strcpy(s,*str); 
strcpy(s,str[1]); //也可写成strcpy(s,*(str+1)); 
strcpy(s,str[2]); //也可写成strcpy(s,*(str+2));

上例中,str 是一个含有三个元素的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。把指针数组名str 当作一个指针的话,它指向数组的第0 号单元,它的类型是char **,它指向的类型是char *。

*str 也是一个指针,它的类型是char *,它所指向的类型是char,它指向的地址是字符串"Hello,thisisasample!"的第一个字符的地址,即'H'的地址。注意:字符串相当于是一个数组,在内存中以数组的形式储存,只不过字符串是一个数组常量,内容不可改变。如果看成指针的话,他既是常量指针,也是指针常量。

str+1 也是一个指针,它指向数组的第1 号单元,它的类型是char**,它指向的类型是char*。

*(str+1)也是一个指针,它的类型是char*,它所指向的类型是char,它指向"Hi,goodmorning."的第一个字符'H' 。

六、指针与结构类型的关系 

我们可以声明一个指向结构类型的指针。

struct MyStruct 
{
     int a; 
     int b; 
     int c; 
}; 

struct MyStruct s={20,30,40};
//声明了结构对象s,并把s 的成员初始化为20,30 和40。 

struct MyStruct *ptr=&s; 
//声明了一个指向结构对象s 的指针。它的类型是MyStruct *,它指向的类型是MyStruct。 

int *pstr=(int*)&s; 
//声明了一个指向结构对象s 的指针。但是pstr和它被指向的类型ptr是不同的。 

那么如何通过指针来访问s的三个成员变量? 

很简单:

ptr->a; // 指向运算符,或者可以使用 (*ptr).a ,但是我建议使用前者。

同理:ptr->b;ptr->c;

又请问怎样通过指针pstr 来访问s 的三个成员变量?

这里也很容易:

*pstr; // 访问了s 的成员a。

*(pstr+1);// 访问了s 的成员b。

*(pstr+2) ;// 访问了ss 的成员c。 

七、指针与函数的关系 

我们可以把一个指针声明成为一个指向函数的指针。
 

int fun1(char *,int);

int (*pfun1)(char *,int);

pfun1=fun1;

int a=(*pfun1)("abcdefg",7); 
//通过函数指针调用函数

可以把指针作为函数的形参。在函数调用语句中,可以用指针表达式来作为实参。

下面来举个例子:

int fun(char *); 
int a; 
char str[]="abcdefghijklmnq"; 
a=fun(str); 
int fun(char *s)
{ 
   int num=0;  
   for(int i=0;;) 
   {  
      num+=*s;
      s++; 
   } 
  
   return num;
} 

这个例子中的函数fun 统计一个字符串中各个字符的ASCII 码值之和。我们在前面已经说了,数组的名字也是一个指针。在函数调用中,当把str作为实参传递给形参s 后,实际是把str 的值传递给了s,s 所指向的地址就和str 所指向的地址一致,但是str 和s 各自占用各自的存储空间。在函数体内对s 进行自加1 运算,并不意味着同时对str 进行了自加1 运算。 

八、指针数组 

如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。指针数组的定义形式一般为:

dataType *arrayName[length];
//括号里面说明arrayName是一个数组,包含了length个元素
//括号外面说明每个元素的类型为dataType *

#include <stdio.h>
int main()
{
    int a = 16, b = 32, c = 100;

    //定义一个指针数组
    int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写成int *arr[]

    //定义一个指向指针数组的指针
    int **parr = arr;
    printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
    printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));
    return 0;
}

运行结果:

16,32,100

16,32,100 

  • arr 是一个指针数组,它包含了 3 个元素,每个元素都是一个指针,在定义 arr 的同时,我们使用变量 a、b、c 的地址对它进行了初始化,这和普通数组是多么地类似。
  • parr 是指向数组 arr 的指针,确切地说是指向 arr 第 0 个元素的指针,它的定义形式应该理解为int *(parr),括号中的表示 parr 是一个指针,括号外面的int *表示 parr 指向的数据的类型。arr 第 0 个元素的类型为 int *,所以在定义 parr 时要加两个 *。
  • 第一个 printf() 语句中,arr[i] 表示获取第 i 个元素的值,该元素是一个指针,还需要在前面增加一个 * 才能取得它指向的数据,也即 *arr[i] 的形式。
  • 第二个 printf() 语句中,parr+i 表示第 i 个元素的地址,*(parr+i) 表示获取第 i 个元素的值(该元素是一个指针),**(parr+i) 表示获取第 i 个元素指向的数据。 

   相信大家看到这里,已经对指针有了新的认知水平,希望本期分享能带给你们学习指针的方向。路还很长,一点一点来,或许我们走的很慢,但却从不后退。如果这篇文章对你们有所帮助,别忘记给博主一个三连哈~你的支持是我创作的最大动力!那我们下期再会啦!诸君加油,我们一起!

  • 26
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值