从零开始的嵌入式生活:指针详解

一、指针的概念

1.1 数据存储--地址

计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。

4G 内存中每个字节的编号(以十六进制表示):

0x00->0x01->0x02->........->0xFFFFFFFFE->0xFFFFFFFF

我们将内存中字节的编号称为地址(Address)或指针(Pointer)。地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。

#include <stdio.h>

int main(void)
{
    int a = 50;
    char str[20] = "gec-edu";

    //&a 获取a变量空间的地址  str获取数组空间的地址
    printf("%#X, %#X", &a, str);


    return 0;
}

输出地址:

0XFFFFCBFC, 0XFFFFCBE0

%#X表示以十六进制形式输出,并附带前缀0X。a 是一个变量,用来存放整数,需要在前面加&来获得它的地址;str 本身就表示字符串的首地址,不需要加&。

c语言还有一个%p专门用来以十六进制形式输出地址,不过 %p 的输出格式并不统一,有的编译器带0x前缀,有的不带。

1.2 一切都是地址

C语言用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供 CPU 使用。

数据和代码都以二进制的形式存储在内存中,计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据。

CPU 只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。如果程序不小心出错,或者开发者有意为之,在 CPU 要写入数据时给它一个代码区域的地址,就会发生内存访问错误。这种内存访问错误会被硬件和操作系统拦截,强制程序崩溃,程序员没有挽救的机会。

CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。

假设变量 a、b、c 在内存中的地址分别是 0X1000、0X2000、0X3000,那么加法运算c = a + b;将会被转换成类似下面的形式:0X3000 = (0X1000) + (0X2000);

( )表示取值操作,整个表达式的意思是,取出地址 0X1000 和 0X2000 上的值,将它们相加,把相加的结果赋值给地址为 0X3000 的内存

变量名和函数名为我们提供了方便,让我们在编写代码的过程中可以使用易于阅读和理解的英文字符串,不用直接面对二进制地址。

需要注意的是,虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为变量名表示的是数据本身(变量地址:&a),而函数名、字符串名和数组名表示的是代码块或数据块的首地址。

二、一级指针

2.1 指针概念

数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针(地址),我们就称它为指针变量。

在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。指针变量的值就是某份数据的地址,这样的一份数据可以是数组、字符串、函数,也可以是另外的一个普通变量或指针变量。

现在假设有一个 char 类型的变量 c,它存储了字符 'K'(ASCII码为十进制数 75),并占用了地址为 0X11A 的内存(地址通常用十六进制表示)。另外有一个指针变量 p,它的值为 0X11A,正好等于变量 c 的地址,这种情况我们就称 p 指向了 c,或者说 p 是指向变量 c 的指针。

2.2 定义一级指针变量

定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*,格式为:datatype *name;

或者 datatype *name = addr;    addr地址,初始化为NULL,或者为&变量、数组名、函数名

*表示这是一个指针变量,datatype表示该指针变量所指向的数据的类型 。例如:int *p1;

p1 是一个指向 int 类型数据的指针变量,至于 p1 究竟指向哪一份数据,应该由赋予它的值决定。再如:int a = 100;   int *p_a = &a;

在定义指针变量 p_a 的同时对它进行初始化,并将变量 a 的地址赋予它,此时 p_a 就指向了 a。值得注意的是,p_a 需要的一个地址,a 前面必须要加取地址符&,否则是不对的。

和普通变量一样,指针变量也可以被多次写入,只要你想,随时都能够改变指针变量的值,请看下面的代码:

float a=99.5, b = 10.6;
char c = '@', d = '#';

float *p1 = &a;
char *p2 = &c;

p1 = &b;   //给指针变量赋值 ,只能赋地址
p2 = &d;

*是一个特殊符号,表明一个变量是指针变量,定义 p1、p2 时必须带*。而给 p1、p2 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*。

指针变量也可以连续定义,例如: int *a, *b, *c;      a、b、c 的类型都是 int*

注意每个变量前面都要带*。如果写成:int *a, b, c 形式,那么只有 a 是指针变量,b、c 都是类型为 int 的普通变量。

2.3 通过指针变量取得数据

指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:

*pointer; //*指针变量

这里的*称为指针运算符(也称为指针解引脚),用来取得某个地址上的数据,请看下面的例子:

#include <stdio.h>

int main(void)
{

    int a = 15;
    int *p = &a;  //*p表示定义里标识为指针变量

    //*p 指针运算,指针解引用   *p  ==  a
    printf("a value:%d, *p value:%d\n", a, *p);

    if(*p == a)
    {
        printf("*p 是对等的哦!\n");
    }
    
    return 0;
}

运行结果:

a value:15, *p value:15
*p 是对等的哦!

假设 a 的地址是 0X1000,p 指向 a 后,p 本身的值(空间)也会变为 0X1000,*p 表示获取地址 0X1000 上的数据,也即变量 a 的值。从运行结果看,*p 和 a 是等价的。

CPU 读写数据必须要知道数据在内存中的地址,普通变量和指针变量都是地址的助记符,虽然通过 *p 和 a 获取到的数据一样,但它们的运行过程稍有不同:a 只需要一次运算就能够取得数据,而 *p 要经过两次运算,多了一层“间接”。

程序被编译和链接后,a、p 被替换成相应的地址。使用 *p 的话,要先通过地址 0XF0A0 取得变量 p 本身的值(0x1000),这个值是变量 a 的地址,然后再通过这个值取得变量 a 的数据,前后共有两次运算;而使用 a 的话,可以通过地址 0X1000 直接取得它的数据,只需要一步运算。

也就是说,使用指针是间接获取数据,使用变量名是直接获取数据,前者比后者的代价要高。

指针除了可以获取内存上的数据,也可以修改内存上的数据,例如:

#include <stdio.h>

int main(void)
{

    int a = 15, b = 99, c = 222;
    int *p = &a;  //指针变量初始化

    *p = b;  //通过指针变量修改内存上的数据
    c = *p;  //通过指针变量获取内存上的值,并赋值给c

    printf("a:%d,b:%d,c:%d, *p:%d\n", a, b, c , *p);

    return 0;
}

运行结果:

99, 99, 99, 99

 

*p 代表的是 a 中的数据,它等价于 a,可以将另外的一份数据赋值给它,也可以将它赋值给另外的一个变量。

*在不同的场景下有不同的作用:*可以用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;使用指针变量时在前面加*表示获取指针指向的数据,或者说表示的是指针指向的数据本身。

2.4 关于 * 和 & 的谜题

假设有一个 int 类型的变量 a,pa 是指向它的指针,那么*&a和&*pa分别是什么意思呢?

*&a可以理解为*(&a),&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 *pa),绕来绕去,又回到了原点,*&a仍然等价于 a。

&*pa可以理解为&(*pa),*pa表示取得 pa 指向的数据(等价于 a),&(*pa)表示数据的地址(等价于 &a),所以&*pa等价于 pa( == &a)。

2.5 对星号*的总结

星号*主要有三种用途:

  • 表示乘法,例如int a = 3, b = 5, c;  c = a * b;,这是最容易理解的。
  • 表示定义一个指针变量,以和普通变量区分开,例如int a = 100;  int *p = &a;。
  • 表示获取指针指向的数据,是一种间接操作,例如int a, b, *p = &a;  *p = 100;  b = *p;
2.6 C语言指针变量的运算 

指针变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行部分运算,例如加法、减法、比较等,请看下面的代码:

#include <stdio.h>
int main(void)
{
    int    a = 10,   *pa = &a, *paa = &a;
    double b = 99.9, *pb = &b;
    char   c = '@',  *pc = &c;
    //最初的值
    printf("&a=%p, &b=%p, &c=%p\n", &a, &b, &c);
    printf("pa=%p, pb=%p, pc=%p\n", pa, pb, pc);

    printf("-----------------------------------\n");
    //加法运算
    pa++; pb++; pc++;
    printf("pa=%p, pb=%p, pc=%p\n", pa, pb, pc);

     printf("-----------------------------------\n");
    //减法运算
    pa -= 2; pb -= 2; pc -= 2;
    printf("pa=%p, pb=%p, pc=%p\n", pa, pb, pc);\

    printf("-----------------------------------\n");
    //比较运算
    if(pa == paa){
        printf("*paa:%d\n", *paa);
    }else{
        printf("*pa:%d\n", *pa);
    }
    return 0;
}

运行结果:

&a=0xffffcbdc, &b=0xffffcbd0, &c=0xffffcbcf
pa=0xffffcbdc, pb=0xffffcbd0, pc=0xffffcbcf
-----------------------------------
pa=0xffffcbe0, pb=0xffffcbd8, pc=0xffffcbd0
-----------------------------------
pa=0xffffcbd8, pb=0xffffcbc8, pc=0xffffcbce
-----------------------------------
*pa:0

从运算结果可以看出:pa、pb、pc 每次加 1,它们的地址分别增加 4、8、1,正好是 int、double、char 类型的长度;减 2 时,地址分别减少 8、16、2,正好是 int、double、char 类型长度的 2 倍。

这很奇怪,指针变量加减运算的结果跟数据类型的长度有关,而不是简单地加 1 或减 1,这是为什么呢?

以 a 和 pa 为例,a 的类型为 int,占用 4 个字节,pa 是指向 a 的指针,刚开始的时候,pa 指向 a 的开头,通过 *pa 读取数据时,从 pa 指向的位置向后移动 4 个字节,把这 4 个字节的内容作为要获取的数据,这 4 个字节也正好是变量 a 占用的内存。

我们知道,数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加 1 就表示指向下一个元素,减 1 就表示指向上一个元素,这样指针的加减运算就具有了现实的意义,也就是说指针在数组里才有意义。

不过C语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,我们往往不进行加减运算,虽然编译器并不会报错,但这样做没有意义,因为不知道它后面指向的是什么数据。

2.7 关于指针变量长度问题

指针变量所占的空间多少?在32位系统里,指针变量所占的空间大小永远是4,在64位系统,指针变量所占的空间大小永远是:8。指针变量空间大小与所指向的类型无关,只与系统对地址分配有空,32位系统,分配给地址空间大小为:4字节。64位系统,分配给地址空间大小为:8字节。

64位系统下:

#include <stdio.h>

int main(void)
{

    char  *p1;
    short *p2;
    int   *p3;
    long  *p4;
    float *p5;
    double *p6;


    printf("指向char 类型指针长度:%d\n", sizeof(p1));
    printf("指向short 类型指针长度:%d\n", sizeof(p2));
    printf("指向int 类型指针长度:%d\n", sizeof(p3));
    printf("指向long 类型指针长度:%d\n", sizeof(p4));
    printf("指向float 类型指针长度:%d\n", sizeof(p5));
    printf("指向double 类型指针长度:%d\n", sizeof(p6));

    return 0;
}

输出:

指向char 类型指针长度:8
指向short 类型指针长度:8
指向int 类型指针长度:8
指向long 类型指针长度:8
指向float 类型指针长度:8
指向double 类型指针长度:8
2.8 C语言指针变量作为函数参数

在C语言中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。

像数组、字符串、动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些数据集合。

有的时候,对于整数、小数、字符等基本类型数据的操作也必须要借助指针,一个典型的例子就是交换两个变量的值。

先看一个经典的错误案例:

#include <stdio.h>

void swap(int x, int y)
{
    int temp;

    temp = x;
    x    = y;
    y    = temp;

}


int main(void)
{
    int a = 66, b =99;
    //这是一个函数
    swap(a, b);
    printf("a:%d, b:%d\n", a, b);
    return 0;
}


运行结果:

a:66, b:99

从结果可以看出,a、b 的值并没有发生改变,交换失败。这是因为 swap() 函数内部的x、y 和 main() 函数内部的 a、b 是不同的变量,占用不同的内存,swap() 交换的是它内部 x、y 的值,不会影响它外部(main() 内部) a、b 的值。

改用指针变量作参数后就很容易解决上面的问题:

#include <stdio.h>

void swap(int *p1, int *p2)
{
    int temp;

    printf("p1:%p, p2:%p\n", p1, p2);

    temp = *p1;
    *p1  = *p2;
    *p2  = temp;

}

int main(void)
{
    int a = 66, b =99;


    printf("&a:%p, &b:%p\n", &a, &b);
    //这是一个函数
    swap(&a, &b);

    printf("a:%d, b:%d\n", a, b);

    return 0;
}

输出:

&a:0xffffcbfc, &b:0xffffcbf8
p1:0xffffcbfc, p2:0xffffcbf8
a:99, b:66

调用 swap() 函数时,将变量 a、b 的地址分别赋值给 p1、p2,这样 *p1、*p2 代表的就是变量 a、b 本身,交换 *p1、*p2 的值也就是交换 a、b 的值。函数运行结束后虽然会将 p1、p2 销毁,但它对外部 a、b 造成的影响是“持久化”的,不会随着函数的结束而“恢复原样”。

需要注意的是临时变量 temp,它的作用特别重要,因为执行*p1 = *p2;语句后 a 的值会被 b 的值覆盖,如果不先将 a 的值保存起来以后就找不到了。

这就好比拿来一瓶可乐和一瓶雪碧,要想把可乐倒进雪碧瓶、把雪碧倒进可乐瓶里面,就必须先找一个杯子,将两者之一先倒进杯子里面,再从杯子倒进瓶子里面。这里的杯子,就是一个“临时变量”,虽然只是倒倒手,但是也不可或缺。

2.9 用数组作函数参数

数组是一系列数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,必须传递数组指针。下面的例子定义了一个函数 max(),用来查找数组中值最大的元素:

在函数内,使用p[i]表示数组的元素,是其中的一种表达方式。

#include <stdio.h>


//数组名名字被做参数里,在函数在默认转化为指针,指针长度永远为:8
int max(int *p, int len)
{
    int max;
    
    int i;

    printf("指针长度:%d\n", sizeof(p));
    printf("intarr value:%p\n", p);
    printf("在函数当中打印数组\n");
    for(i=0; i<len; i++)
    {
        printf("p[%d] = %d\n", i, p[i]);  //在函数里,数组传过来的指针,可以:指针[下标] 来表示元素
    }
    //假设p[0]是最大值
    max = p[0];
    

    for(i=1; i<len; i++)
    {
        if(max < p[i])
        {
            max = p[i];
        }
    }


    return max;
}


int main(void)
{

    int nums[6], i, maxvalue;

    //按字节来算
    printf("数组长度:%d\n", sizeof(nums));


    printf("请输出数组的值:");

    for(i=0; i<6; i++)
    {
        scanf("%d", &nums[i]);
    }
    printf("数组地址:%p\n", nums);

    //数组名字就是指针
    maxvalue = max(nums, 6);

    printf("数组最大值:%d\n", maxvalue);

    return 0;
}

运行结果:

数组长度:24
请输出数组的值:25 41 36 85 41 22
数组地址:0xffffcbe0
指针长度:8
intarr value:0xffffcbe0
在函数当中打印数组
p[0] = 25
p[1] = 41
p[2] = 36
p[3] = 85
p[4] = 41
p[5] = 22
数组最大值:85

参数p 仅仅是一个数组指针,在函数内部无法通过这个指针获得数组长度,必须将数组长度作为函数参数传递到函数内部。数组 nums 的每个元素都是整数。

用数组做函数参数时,参数也能够以“真正”的数组形式给出。例如对于上面的 max() 函数,它的参数可以写成下面的形式:

int max(int *p, int len)
改
int max(int intarr[], int len)

int intArr[6]好像定义了一个拥有 6 个元素的数组,调用 max() 时可以将数组的所有元素“一股脑”传递进来。

int intArr[]虽然定义了一个数组,但没有指定数组长度,好像可以接受任意长度的数组。

实际上这两种形式的数组定义都是假象,不管是int *p还是int intArr[]都不会创建一个数组出来,编译器也不会为它们分配内存,实际的数组是不存在的,它们最终还是会转换为int *intArr这样的指针。这就意味着,两种形式都不能将数组的所有元素“一股脑”传递进来,大家还得规规矩矩使用数组指针。

需要强调的是,不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。

C语言为什么不允许直接传递数组的所有元素,而必须传递数组指针呢?

参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。

对于像 int、float、char 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行内存拷贝有可能是一个漫长的过程,会严重拖慢程序的效率,为了防止技艺不佳的程序员写出低效的代码,C语言没有从语法上支持数据集合的直接赋值。

三、二级指针

指针可以指向一份普通类型的数据,例如 int、double、char 等,也可以指向一份指针类型的数据,例如 int *、double *、char * 等。

如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。

假设有一个 int 类型的变量 a,p1是指向 a 的指针变量,p2 又是指向 p1 的指针变量:

int a = 100;
int *p1 = &a;
int **p2 = &p1;

指针变量也是一种变量,也会占用存储空间,也可以使用&获取它的地址。C语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号*。p1 是一级指针,指向普通类型的数据,定义时有一个*;p2 是二级指针,指向一级指针 p1,定义时有两个*。

如果我们希望再定义一个三级指针 p3,让它指向 p2,那么可以这样写:int ***p3 = &p2;

四级指针也是类似的道理:int ****p4 = &p3;

实际开发中使用一级指针和二级指针足以解决绝大部分问题。

例子:

#include <stdio.h>

int main(void)
{

    int a = 100;
    int *p1 = &a;
    int **p2 = &p1;

    printf("&a addr:%p\n",&a);
    printf("&p1 addr:%p\n",&p1);
    printf("&p2 addr:%p\n",&p2);

    printf("a变量空间的值: %d\n", a);
    printf("p1变量空间的值:%p\n", p1);
    printf("p2娈量空间的值:%p\n", p2);


    printf("a = %d, *p1 = %d, **p2 = %d\n", a, *p1, **p2);

    printf("*p2 = %p\n", *p2);


    return 0;
}

输出:

&a addr:0xffffcbfc
&p1 addr:0xffffcbf0
&p2 addr:0xffffcbe8
a变量空间的值: 100
p1变量空间的值:0xffffcbfc
p2娈量空间的值:0xffffcbf0
a = 100, *p1 = 100, **p2 = 100
*p2 = 0xffffcbfc

以二级指针 p2 为例来分析上面的代码。**p2等价于*(*p2))。*p2 得到的是 p1 的值(地址),也即变量a的地址;*(*p2) 得到的是a 的值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值