深度剖析C语言的各种指针

该文章详解了C语言中指针的概念与用法,对初学者了解C语言中指针这一重要概念有很大的帮助,其中变量的定义与使用一节尤为有价值。要点如下:
1. 指针是个变量,本质代表了一个地址。
2. 变量拆解遵循由近到远,从左到右的原则。
3. C语言中,变量怎么使用就怎么定义(指的是书写格式上保持一致)。

指针简述

在C语言中,指针就是一个地址,不管是什么类型的指针,它都代表了一个地址,指针本身的值是一个整形的数(长度跟系统和硬件有关,这里认为指针长度是32位)。

不同的指针类型代表着什么?

指针的类型是对于指针指向的内容来说的,比如char类型的指针代表其指向的地址内容是char类型的,指定类型是为了在取数据的时候知道取多少个字节。比如一个指针名为ptr,假设指向0xFFFF0000,如下图所示:

指针
如果指针是char类型的,也就是char *ptr = 0xFFFF0000;如果我们取ptr所指向的值,就是0x01,依次类推,int类型的会取出4个字节的数据,struct类型的则与struct的长度有关。

指针的类型本质上就是方便编译器知道在取用指针指向的内容时应该取多少个字节的数据

指针&数组名

指针和数组不同的地方是:指针是变量,它有自己的存储空间(理解为普通变量即可,只不过指该变量的值是个指针),数组名没有存储空间。所以指针本身是可以进行加减运算的,但是数组名本身不可以进行加减运算。

/* 假设 "abc" 字串常量存放在从 0xFFFF0000 开始的地址空间 */
char str[4] = "abc";
char *ptr = "abc";

str++; //错误,不能够进行自加减运算。str 应该理解为一个地址常量,值为 0xFFFF0000
ptr++; //正确,ptr本身的值变为 0xFFFF0001。ptr 是个变量,有自己的存储空间,可以使用
// printf("%d", (int)ptr); 来打印出ptr变量的存放地址
ptr与str两者的关系如下图所示(ptr的存放地址是随便写的):

指针&数组

指针函数与函数指针

指针函数形如 void (*proc)(void)
函数指针形如 void *proc(void)

  • 从外观上来看,它们两者是函数名与前面的*号加不加括号的区别
  • 从含义上来看,一个是个前者本质是个指针,此指针指向一个参数为void,返回值为void的函数;后者是个函数,其返回值是一个指针

先看一段代码:

#include <stdio.h>
void (*uart)(void);
unsigned char test_str[] = "This is a test for a pointer of proc!";

void *uartx(void)
{
    return test_str;
}

void uart0(void)
{
   printf("I am uart0\n");
}

void set_handler(void (*handler)(void))
{
   uart = handler;
}

int main(int argc, char **argv)
{   
   unsigned char *replace_str;

   set_handler(uart0);
   uart();
   replace_str = (unsigned char *)uartx();
   printf("%s\n",replace_str);

   return 0;
}

对应于上面代码片的

void (*uart)(void);
void *uartx(void)

函数指针

void set_handler(void (*handler)(void))
{
   uart = handler;
}

本函数的参数是一个函数指针,函数体是将传入的函数地址赋值给uart这一函数指针(函数名本身就是地址,也可以看做是指针)
在main函数里面可以看到相关的调用

set_handler(uart0);
uart();

这两句中第一句传递函数指针,第二句是通过调用uart来调用uart0这个函数,uart本身就是指针,指向uart0函数的地址,这两句打印出来“I am uart0”
并且我们发现,形如

(*uart)();
(**uart)();
(*uart0)();
(**uart0)();

这样的语句也都可以正确的编译运行,由此可知函数名本身也许就可以看做一个指向自己的地址(为方便理解),对函数名取地址还是函数名的地址值

指针函数

如下代码所示

void *uartx(void)
{
    return test_str;
}

这段代码虽指明了void类型,但是还有返回值,编译器也未报错,此处大概可以看作是void *类型的指针,也即无类型指针,倘若此处改为int或者long就会报错,可见无类型指针与其余类型的指针可以兼容。
此时已经构造一个指针函数,名为uartx,返回值为无类型指针,下面对函数进行调用

replace_str = (unsigned char *)uartx();
printf("%s\n",replace_str);

改变replace_str的属性也可以得到正确的结果,表明返回值确实是void 类型的,倘若将uartx改为返回unsigned char指针,replace_str改为long型指针就会报错,但是若uartx是void 就不会报告任何信息

void指针

void指针可用于不同类型指针参量传递,由上面的例子就可以看出,如果不确定函数返回的是什么类型的值,就可以把返回值设为void指针类型,由此可以达到某些特殊的目的,但是大部分时候不建议这样子编写函数,因为这样会造成函数返回值或者变量值的类型变得不确定,会为阅读代码带来巨大困难,有些时候也会导致bug的产生,此时又不好找到原因,因此除了必须的特殊情况,void类型的指针尽量需要少用

其它的复杂指针的用法

下面列举了一些比较复杂的指针变量定义,在非必要的情况下为了代码便于阅读还是不要这样去定义变量的好

int (*daytab)[13];   //一个指针,指针指向一个拥有13个int类型值的数组
int *daytab[13];  //一个指针数组,共13个指针,都指向int类型
char (*(*x())[])();  //x是一个指针函数,返回一个指针,该指针一个一位数组,数组元素为指针,每个指针都指向返回char类型的函数
char (*(*x[3])())[5];   //一个指针数组,共有3项,每项都指向一个函数指针,并且该函数返回指向拥有5个char类型项的数组的指针

上述指针应用实在是太过复杂,不是逻辑特别清晰的在编写程序的时候千万要避免这种写法,否则会痛不欲生的

如何看懂复杂的变量定义

同普通变量一样,在C语言中变量的定义遵循一条原则,就是定义与使用的书写格式保持一致。比如:

char str[10]; // 定义
str[0] = 'a'; // 使用,可以看到两者的结构都是一个名字然后加一个[],这就是所谓的定义
// 与使用格式保持一致
char *ptr;  // 定义
*ptr = 'a'; // 使用,都是 *ptr 格式

另外一条,变量定义的拆解遵循由近到远,先右后左的原则进行,这样一来我们去分析一个比较复杂的定义就有据可循了,试着拆解下面一个指针的定义:

int **ptr[10];
int **ptr[10][10];
int (*foo)(char);
int (*foo[10])(char);
  • 第一个定义拆解:由近到远,先右后左
    ptr[10]表明ptr是一个长度为十个单元的数组,左边的第一个表示数组中的元素是指针,左边第二个表示数组元素的指针指向另一个指针变量,最前面的int表示指向的指针变量是int类型的。合起来,ptr是一个有10个成员的数组,数组中的元素是指针,指针指向另一个类型是int的指针。(第二个自己拆解,注意:[]运算符比*运算符优先级高)
  • 第四个定义拆解:由近到远,先右后左
    foo[10]表明foo是一个拥有10个成员的数组,左边的*表明数组成员是指针类型,右边的(char)表明指针指向一个只有一个char类型参数的函数,最左边的int表明函数的返回值是int类型。合起来,foo是一个有10个成员的数组,数组元素指向一个参数为char返回值为int的函数。

指向函数的数组实例

先看一段代码:

int start(void);
int pause(void);
int stop(void);

int (*foo[10])(void) = {start, pause, stop};

int start(void)
{
    printf("start gouliguojiashengsiyi\n");
    return (0);
}

int pause(void)
{
    printf("pause >O< \n");
    return (0);
}

int stop(void)
{
    printf("stop qiyinhuofuqubizhi\n");
}

int main(int argc, char *argv[])
{
    foo[0]();
    foo[1]();
    foo[2]();

    return (0);
}

原则:变量的定义与使用在格式上是保持一致的

上面定义了一个有10个指向参数为空返回值为int的函数的指针数组,我们可以称foo就是一个函数指针数组。main函数里面的foo0;就代表调用了start这个函数。内存排布如下图所示(地址均为随便写的,虚构,但是不妨碍理解):
函数指针数组

foo[0]本身只是代表一个地址,加上()之后就变成了一个函数,我们可以把()理解为一个操作符,该操作符的作用就是设置PC指针跳转到某个地址执行代码,有需要的时候设置函数的参数。当我们知道一个函数的地址之后(假设是0xFFFF0010),我们可以直接用把该地址强制转换为函数类型达到函数调用的作用(int (*0xFFFF0010)(void))();这个也是遵循怎么使用就怎么定义的原则。

总结

其实C语言中很多东西都是非常容易理解的,但是由于包裹了系统这层厚厚的外壳,导致不那么容易理解,这个时候需要透过现象看本质,最笨的方法是写单片机或者SOC的裸机代码,然后反汇编,对比一下反汇编之后的代码与C语言代码,很多疑惑或者误解都会在瞬间烟消云散。

有时候要理解C语言中的一些东西,首先就要搞清楚“它是什么”,比如指针,指针实质上就是一个变量,这个变量和普通变量没什么大的不同,如果紧紧的抓住指针是变量这一根源,那么指针理解起来也就不是十分的困难了。


如果觉得本文章不错,请关注微信公众号-YellowMax多多支持,查看更多文章

欢迎转发、关注、点赞一波

微信公众号

猜你喜欢

转载自blog.csdn.net/u013904227/article/details/71600627