数组存储与指针学习笔记(三)指针与数组

嵌入式C语言学习进阶系列文章

GUN C编译器拓展语法学习笔记(一)GNU C特殊语法部分详解
GUN C编译器拓展语法学习笔记(二)属性声明
GUN C编译器拓展语法学习笔记(三)内联函数、内建函数与可变参数宏
数组存储与指针学习笔记(一)数据类型与存储、数据对齐、数据移植、typedef
数组存储与指针学习笔记(二)枚举类型、常量与变量



一、指针

1.1 指针的本质

  内存一般可分为静态内存和动态内存,一个程序被加载到内存运行时,代码段和数据段就属于静态内存,而堆栈则属于动态内存。

  • 静态内存的特点是内存中各个变量的地址在编译期间就确定了,在程序运行期间不再改变。
  • 动态内存中变量的地址在程序运行期间是不固定的,如函数的局部变量,如果这个函数多次被调用运行,那么每次运行都要在栈上随机分配一个栈帧空间;
      指针的原始初衷用途,其实就是访问一片匿名的动态内存。通过指针我们可以直接读写指定的内存。通变量一般采用直接寻址,既可当左值,又可当右值;而指针变量一般采用间接寻址。当指针变量通过间接寻址时,其又等价为一个普通变量(下面代码中的*p与a是等价的),既可当左值,又可当右值。

1.2 复杂指针声明

  声明一个指针,其实就是声明一个指针的类型。指针类型一般可以分为三大类。

  • 函数指针:void(*fp)(int,int)。
  • 对象指针:char*、int*、long*、struct xx*。
  • void指针:一般作为通用指针,作为函数的参数。
      函数指针,顾名思义,指针指向一个函数,指针变量存储的是函数的入口地址。当指针指向不同类型的数据时,我们称这种指针为对象指针。void
    指针既不属于对象指针,也不属于函数指针。

  和指针相关的运算符主要包括以下几种。

  • 指针声明:int*
  • 取址运算符:&
  • 间接访问运算符:*
  • 自增自减运算符:++、--
  • 成员选择运算符:.、->
  • 其他运算符:[]、()
      优先级按照从高到低的顺序依次为:[]、()、.、->、++、--、*、&
    在这里插入图片描述

  对于这种复杂的指针声明,我们可以借助“左右法则”来分析:首先从最里面的圆括号(未定义标识符)看起,先往右看,再往左看,每当遇到圆括号时,就应该掉转阅读方向。一旦解析完圆括号里所有的东西,就跳出圆括号。重复这个过程,直到整个声明解析完毕。
  我们再去分析上面声明语句中的最后一个指针声明:首先从最里面的圆括号看起,f是一个指针,整个指针表达式因此也就定了性。这条语句声明的是一个指针。然后往右看,是一个参数列表,说明该指针的类型是一个函数指针。再向左看,是一个符号,说明该指针指向的函数的返回值是一个指针。此时括号里的东西解析完毕,跳出圆括号,继续重复这个过程。往右看是一个数组,再往左看是int*,与下面类似。在这里插入图片描述
  把以上分析综合就可以得出最后的分析结果:这个复杂的指针声明相当于定义一个函数指针,该指针指向一个函数,这个函数的类型形参为(int),返回值是一个指向指针数组的指针,指针数组中的元素类型为int*。

在这里插入图片描述

1.3 指针类型与运算

  类型就是一组数值和对这些数值相关操作的集合。指针也是如此,不同的指针类型会有不同的运算操作。指针变量p通常用来指向一个不同类型的变量,每个变量类型不同,在内存中所占空间大小不同,p++也就被转换为不同的数值运算,运算结果也各不相同。但有一点是相同的,p++总是指向下一个元素的地址。为了存储变量的地址,指针变量本身也得有一个存储空间。
  加减运算:两个指针也可以直接相减,但前提是指针类型要一致,而且只能相减,不能相加,相减的结果表示两个指针在内存中的距离。两个指针相减的结果以数据类型的长度sizeof(type)为单位,而非以字节为单位。
  关系运算:两个指针可以比较大小,但比较的前提是指针类型必须相同,指针关系运算一般用在同一个数组或链表中,不同的比较结果代表不同的含义。

  • p < q:指针p所指的数在q所指数据的前面。
  • p > q:指针p所指的数在q所指数据的后面。
  • p == q:p和q指向同一个数据。
  • p != q:p和q指向不同的数据。

1.4 指针与数组关系

  从用法上看来:

  • 数组名作为函数参数时相当于一个指针地址。
  • 数组和指针一样,都可以通过间接运算符*访问。
  • 数组和指针一样,都可以使用下标运算符[]访问。

  从C语言语法上看来,数组与指针的访问方式、主要用途都不相同。
在这里插入图片描述

  1. 下标运算符[]
      下标运算符[]是数组用来访问数组元素的运算符,而间接访问运算符则是指针用来访问内存的运算符。按理而言,数组和指针是两个不同的东西,为啥运算符可以混用呢?原因如下:C语言对下标运算符的访问,是通过转化为指针来实现的。
    在这里插入图片描述
      当我们对一个数组a[n]通过下标访问时,编译器会将其转换为
    (a+n)的形式,数组名a代表的是数组首元素的地址,相当于一个指针常量。
    在这里插入图片描述

  2. 数组名本质
      当数组作为函数参数时,传递的是一个地址,此时数组名相当于一个常量指针。数组名其实也存在隐式转换,在不同的场合代表不同的意义。当我们使用数组名声明一个数组,或者使用数组名和sizeof、取址运算符&结合使用时,数组名表示的是数组类型。在其他情况下,数组名都是一个右值,表示数组首元素的地址,但是可以与间接访问运算符*构成一个左值表达式。如下面的程序所示。
    在这里插入图片描述  程序运行结果如下。
    在这里插入图片描述  了解了数组名在不同场合代表的类型及其隐式转换,也就明白了我们为什么不能直接为数组赋值,只能在初始化的时候赋值。

  3. 指针数组与数组指针
       指针数组本质上是一个数组,数组里的每一个元素存放的不是普通的数据,而是一个地址;数组指针本质上是一个指针,只不过这个指针指向的数据类型是一个数组。通过数组或指针,使用下标运算符或间接访问运算符都可以灵活实现对数组元素的访问。而指针数组的本质还是一个数组,只不过数组元素的数据类型比较特殊,是一个指针,我们按照数组的正常访问方式访问即可。

二、指针与结构体

   结构体则是由一组不同类型的数据组成的集合,我们可以通过成员访问运算符.去访问各个成员,也可以通过指针间接访问运算符-> 去访问各个成员。与指针、结构体相关的运算符如下。

  • 成员访问运算符:.
  • 成员间接访问运算符:->
  • 结构体成员取址:&stu.num
  • 结构体成员自增自减:++stu.num、stu.num++
  • 间接访问运算符:*stu.p

   举个例子。
在这里插入图片描述   程序运行结果如下。
在这里插入图片描述在这里插入图片描述
   访问结构体的成员有两种方法:直接成员访问和间接成员访问,对应的运算符分别为stu.num和p->num。构体是一个标量,当结构体作为函数的参数或者返回值时,传递的是整个结构体所有成员的值,这一点和数组是不同的,数组名作为参数时传递的仅仅是一个地址。在实际编程中,当需要结构体传参时,我们一般都使用结构体指针来实现,直接传一个地址就可以,简单高效。

三、二级指针

   指针变量主要用来存储一块内存的地址,然后通过间接访问运算符*去访问这块内存,对这块内存进行读写操作。指针变量可以保存任意类型变量的地址:数组、结构体、函数甚至另一个指针变量的地址。当一个指针变量保存的是另一个指针变量的地址时,我们称该指针是指向指针的指针,或者叫二级指针。举个例子:
在这里插入图片描述
   程序的运行结果如下。
在这里插入图片描述
   访问a变量所绑定的这块内存空间有以下三种方法。

  • 通过变量名a直接访问。
  • 通过一级指针p和间接访问运算符*间接访问。
  • 通过二级指针pp和间接访问运算符**间接访问。
       二级指针解决场景。
  • 修改指针变量的值。
  • 指针数组传参。
  • 操作二维数组。

3.1 修改指针变量的值

   函数的参数传递是值传递,传递的是变量的副本,函数形参的改变并不会改变实参的值。通过一级指针,我们可以修改一个普通变量的值。如果想修改一个指针变量的值,则可以通过二级指针来完成。
在这里插入图片描述
  程序运行结果如下。
在这里插入图片描述
  在上面的程序中,如果我们想通过change3()函数改变指针变量p的值,则只能将change3()函数的参数设计为二级指针形式,把指针变量的地址&p作为实参传递给函数,change3() 函数就可以根据p的内存地址来修改p的值了。

3.2 二维指针和指针数组

  指针数组本质上还是一个数组,只不过每个数组元素都是一个指针而已。当数组作为函数的参数时,对于一维数组来说,数组名会隐式转换为数组首元素的地址,即一级指针。当指针数组作为函数参数时,数组名也会隐式转换为首元素的地址,即指针的地址——二级指针。当数组作为函数参数时,其可以匹配的形参形式如下。
在这里插入图片描述

3.3 二级指针和二维数组

  一维数组的数组元素类型为int,我们称这个一维数组为整型数组;一维数组的数组元素类型为结构体,我们称这个数组为结构体数组。如果一维数组的数组元素还是一个数组,则我们不能称之为数组型数组,而一般称之为二维数组。C语言是把二维数组看成一个特殊的一维数组来处理的:每个元素都是一个一维数组。我们可以通过一级指针去操作一维数组,那么如何通过二级指针操作二维数组呢?
在这里插入图片描述  上面代码中有两条赋值语句,第一条p赋值语句没有问题,指针p指向一维数组首元素的地址,数组元素的类型为int,指针p的类型为int*,两者是匹配的,程序编译正常。而第二句二级指针pp的赋值,编译器在编译时会发出警告:类型不兼容。
在这里插入图片描述
  第二句的赋值语句等效为pp=&b[0],b[0]代表什么呢?C语言是把二维数组当成一维数组来处理的,二维数组b[3][5]其实就是一个一维数组b[3],该一维数组中的每一个数组元素是一个长度为5的一维数组int c[5]。如果你想把数组名b直接赋值给指针变量pp,那么指针变量的类型必须为int(*p)[5]这种类型。为了解决这个问题,有以下两种解法。

  • 指针数组方式访问

在这里插入图片描述

  • 二级指针方式访问
    在这里插入图片描述在这里插入图片描述
  • 一级指针方式访问
    在这里插入图片描述
      注意:不同的指针类型执行自增操作时,实际偏移的地址是不一样的。在使用指针操作数组时,无论操作一维数组,还是二维数组,程序员都必须时刻记住的一点就是:你定义的指针类型不同,操作数组的方式也不同。牢记这点,并熟练掌握与指针相关的声明和运算符的优先级,才能够把指针用得得心应手。

四、函数指针

  函数指针用来指向一个函数,一般我们会定义一个函数指针变量来保存函数的入口地址。
在这里插入图片描述
  调用示例如下:
在这里插入图片描述在这里插入图片描述
  我们可以通过函数名+函数调用运算符()去调用一个函数。函数名的本质其实就是指向函数的指针常量,即函数的入口地址。在fp=func;语句中,函数名会通过隐式转换,转换成fp=&func;的形式。当我们通过指针调用函数时,(*fp)()间接访问其实就等效为fp()表达式。无论是间接访问,还是多次间接访问,如下所示,它们的效果其实都是一样的,都等效为fp()。
在这里插入图片描述
  区分:指针函数指函数的类型,即函数的返回值是一个指针,除此之外和普通函数无异,就不再赘述了。指针函数的声明方式如下。
在这里插入图片描述

五、重新认识void

  void*指针,void关键字在C语言中被大量使用,大家对它既熟悉又陌生。void其实也是一种类型,只不过它比较特殊:无数值,无运算。

  • void经常用来修饰函数的返回类型,表明函数无返回值。
  • void作为函数的参数时,表明函数无参数。
  • void*指针可以指向任意数据类型,任意类型指针可以直接赋值给void*指针,不需要强制类型转换。
  • void*指针赋值给其他类型指针时,需要强制类型转换。任意类型的指针转换为void*,再转换为原来的类型时,都不会发生数据丢失,值也不会发生改变。
    在这里插入图片描述
      void*指针主要用来作为函数的参数,表示函数的参数可以是任意指针类型。当函数的返回类型为void*时,返回的指针可以指向任意数据类型。C标准库中很多函数原型中都使用了void*指针。
    在这里插入图片描述
      malloc()函数返回的指针类型为void*,因此在将malloc()函数返回的地址赋值给一个指针变量时,一般要做强制类型转换。void作为一种指针类型,除了修饰函数原型,一般不参与具体的指针运算。我们不能使用间接访问运算符访问void*,不能对void*做下标运算,但是在GNU C中可以做自增自减运算。

猜你喜欢

转载自blog.csdn.net/qq_41866091/article/details/130600804
今日推荐