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

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


文章目录

  • 嵌入式C语言学习进阶系列文章
    • 一、指针
      • 1.1 指针的本质
      • 1.2 复杂指针声明
      • 1.3 指针类型与运算
      • 1.4 指针与数组关系
    • 二、指针与结构体
    • 三、二级指针
      • 3.1 修改指针变量的值
      • 3.2 二维指针和指针数组
      • 3.3 二级指针和二维数组
    • 四、函数指针
    • 五、重新认识void

一、指针

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中可以做自增自减运算。

数组存储与指针学习笔记(三)指针与数组相关推荐

  1. 梓益C语言学习笔记之指针

    梓益C语言学习笔记之指针 一.32位平台下,地址是32位,所以指针变量占32位,共4个字节 二.内存单元的地址即为指针,存放指针的变量称为指针变量,故:"指针"是指地址,是常量,& ...

  2. SurfaceFlinger学习笔记(三)之SurfaceFlinger进程

    概述 本系列是基于android Q 即android10 SurfaceFlinger学习笔记(一)应用启动流程 SurfaceFlinger学习笔记(二)之Surface SurfaceFling ...

  3. 【AngularJs学习笔记三】Grunt任务管理器

    为什么80%的码农都做不了架构师?>>>    #0 系列目录# AngularJs学习笔记 [AngularJs学习笔记一]Bower解决js的依赖管理 [AngularJs学习笔 ...

  4. python3常用模块_Python学习笔记三(常用模块)

    Python 学习笔记三 (常用模块) 1.os模块 os模块包装了不同操作系统的通用接口,使用户在不同操作系统下,可以使用相同的函数接口,返回相同结构的结果. os.name:返回当前操作系统名称( ...

  5. Mongodb使用学习笔记(三)

    文章目录 Mongodb使用学习笔记(三) 1. MongoDB 更新文档 1.1 update() 1.2 save() Mongodb使用学习笔记(三) 1. MongoDB 更新文档 参考:Mo ...

  6. K8S 学习笔记三 核心技术 Helm nfs prometheus grafana 高可用集群部署 容器部署流程

    K8S 学习笔记三 核心技术 2.13 Helm 2.13.1 Helm 引入 2.13.2 使用 Helm 可以解决哪些问题 2.13.3 Helm 概述 2.13.4 Helm 的 3 个重要概念 ...

  7. GEE (Google Earth Engine)最基础代码学习笔记三

    GEE (Google Earth Engine)代码学习笔记三 本次学习核心为:将JavaScript objects and primitives放入Earth Engine 容器传到服务器,并处 ...

  8. OpenCasCade学习笔记(三):加载显示STEP格式图片,并实现平移、缩放和旋转操作

    OpenCasCade学习笔记(三):加载显示STEP格式图片,并实现平移.缩放和旋转操作 C3DWidget.h #pragma once#include <QtWidgets/QApplic ...

  9. Hive学习笔记三之函数操作

    文章目录 5 函数 5.1 系统内置函数 5.2 常用内置函数 5.2.1 空字段赋值 5.2.2 CASE WHEN THEN ELSE END(类似于java中的switch case) 5.2. ...

最新文章

  1. TextMate 通用快捷键
  2. 设计模式之_Strategy_04
  3. python if写在return 后面_python中return如何写
  4. C#中的泛型和泛型集合
  5. mysql索引三个字段查询两个字段_mysql中关于关联索引的问题——对a,b,c三个字段建立联合索引,那么查询时使用其中的2个作为查询条件,是否还会走索引?...
  6. 注册机patch起什么作用_电机滑环起什么作用?
  7. 直播丨Oracle 12cR2 ADG LGWR Library Cache案例分享
  8. matlab简单分析频域滤波和时域滤波
  9. 4.大数据架构详解:从数据获取到深度学习 --- 数据获取
  10. 个人计算机预防勒索病毒,避免电脑中勒索病毒的方法
  11. C++:Socket:WSAStartup和WSADATA
  12. vue脚手架安装很慢_vue-cli3脚手架安装
  13. 软件测试面试题:所有的软件缺陷都能修复吗?所有的软件缺陷都要修复吗?
  14. 试验Windows Embedded Standard 7 Service Pack 1 Evaluation Edition
  15. keras调试的正确打开方式: 一句话让你把tensorflow当pytorch用
  16. 人工智能到底是什么?AI基础概念深度解析
  17. html网页生成动态地图
  18. 广州Java工程师平均薪资_【广州恒生电子工资】java开发工程师待遇-看准网
  19. 计算机信息系统集成一级和二级资质认证通过企业名单
  20. 简单说下YOLOv1、v2、v3、v4各自的特点与发展史

热门文章

  1. 第一颗国产 四端口 LVDS 至 HDMI2.0,带音频 芯片 LT2611UX
  2. 校园垃圾回收处理APP
  3. 错误: LogSet 已指定,NSIS_CONFIG_LOG 未定义。
  4. 宁波市第三届网络安全大赛-WriteUp(Misc)
  5. 想做新媒体运营有前途吗
  6. 正则表达式——验证数字
  7. 思科 计算机网络 第10章测试考试 答案
  8. linux mysql木马_Linux系统木马后门查杀方法
  9. phpcms做的网站总是被植入木马后门快照劫持
  10. Sum of a column in an SQL query statistics table