玩转C语言指针~

距离我学习C语言也有四年之久了,我不禁想到,自己是否真的了解和会完C语言指针,以下是我对C语言指针的理解(如有纰漏,请指出

我做了codewars上的 C Puzzle: Pointer Monster,其中运用到了一些关于指针相关的内容,我感觉这是一道学习指针和数组和函数之间的较好的一个例子

特别是不用编译器去算指针,数组,偏移后所得出来了的最后的答案,我觉得对理解数组和指针帮助还是有一定的帮助的

题目链接

数组和指针

一重声明

什么是指针呢?

学过计算机组成原理就知道,哦,这个是个间接寻址~

比如声明一个指针 int* x,在其中 int* 是 x 的类型,x的值是一个 int*

我们可以使用 *x 去访问 这块地址的值!

有间接寻找那味儿了

什么是数组呢?

我们知道,内存被分为了一系列连续的小块,对于每一个小块,我们给它们取了个编号

这样,我们就有了我们的地址这个概念

那么,我们的指针是指向一个地址的,它可以支持加减操作,每次操作对应着移动到下一个块去

那么,我们是不是可以使用 *(x + 1) 来访问 下一个地址的值呢?

显然是可以的

那么数组是个什么东西呢?

首先,数组给的内存肯定是先分配好的,不然会段错误。

其次,我眼中的数组其实就是一个 int * const (这个指针是不能够被改变的

当然,无论是指针还是数组,他都可以写成一个乖乖的形式:

int* t;
int arr[2];

1[t] = 1;
1[arr] = 2;

上述操作是可以的哦

大概是因为1[t] 在编译时会被翻译为 1+t 吧~

二重声明

那么,二重指针呢?

套娃嘛。

二重指针指向的是一个int*

那么我们的指针就应该长成 int* *

那么,二维数组呢?

同理,我们的二维数组呢?

应该长成 int[][]

二重数组的地址分布是什么样呢?

那么,指向数组的指针呢?

注意,这是指向数组的指针,是一个指针,元素是数组

int (*)[k] (k代表数组长度,得指定,数组和指针还是有区别的)

那么,指向指针的数组呢?

注意,这是指向指针的数组,是一个数组,元素是指针

int *[k] (k 代表数组长度)

螺旋法则

看到这里,可能开始模糊起来了,可能就读不清楚是个什么类型了
简单过一下螺旋法则吧~

玩转C语言指针~_第1张图片

  1. 从我们需要判定的标识符开始,顺时针画圈,遇到如下符号时,用对应的语义替换:
  • [x][] => 容量为 x 的数组或数组
  • (type1,type2...) => 接收 type1type2… 的函数,返回值为(待定)
  • * => 指向(类型待定)的指针
  1. 重复上面的步骤直到语句中所有符号都被遍历过。
  2. 始终优先解析括号括起来的部分。
                    +-------+
                     | +-+   |
                     | ^ |   |
                int *arr[7];
                 ^   ^   |   |
                 |   +---+   |
                 +-----------+
  • arr 这个需要被判定的对象出发。
  • 螺旋路径 遇到的是 [ 左方括号 ->arr 是一个尺寸为 7 的数组。
  • 继续旋转,遇到 * -> 所以 arr 是一个尺寸为 7 的数组,元素为指针。
  • 继续,遇到 ; -> 结束。
  • 再继续,遇到 int -> 所以 arr 是一个尺寸为 7 的数组,数组元素: int 类型的指针。

下面自己分析一个复杂的吧~

                      +-----------------------------+
                      |                  +---+      |
                      |  +---+           |+-+|      |
                      |  ^   |           |^ ||      |
                void (*signal(int, void (*fp)(int)))(int);
                 ^    ^      |      ^    ^  ||      |
                 |    +------+      |    +--+|      |
                 |                  +--------+      |
                 +----------------------------------+

三重声明

接下来,可能要变得有趣起来了

因为指针和数组,一重时,最多两种情况,而二维时,我们则有4中情况了

我们可以来枚举一下:

设:用指针代表 A ,数组代表 B 一重 : A 、 B 二重 : A A 、 A B 、 B A 、 B B 三重: A A A 、 A B A 、 B A A 、 B B A 、 A A B 、 A B B 、 B A B 、 B B B 设:用指针代表 A, 数组代表B \\ 一重: A、B \\ 二重: AA、AB、BA、BB \\ 三重:AAA、ABA、BAA、BBA、AAB、ABB、BAB、BBB 设:用指针代表A,数组代表B一重:AB二重:AAABBABB三重:AAAABABAABBAAABABBBABBBB

三重理论上排列出来的组合还是蛮多的,可能有这么多种,但实际不一定,这里就介绍四种

第一种是 指向 数组的指针 的指针

int(**)[k] 由于 int(*(*))[k]中的(*)没必要,可以省略

第二种是 声明 数组的指针 的数组

int(*)[k][k] 加一维即可

第三种是 指向 指针的数组 的指针

int *(*)[k] 嵌套一个进去

第四种是 声明 指针的数组 的数组

int (*[k])[k] 加一个维度

示例

int main() {
#define k 7
  using A = int *;
  using B = int[k];
  using AA = int **;
  using BB = int[k][k];
  using BA = int(*)[k];
  using AB = int *[k];
  using BAA = int(**)[k];
  using BAB = int(*)[k][k];
  using ABA = int *(*)[k];
  using ABB = int (*[k])[k];
  A a;
  B b;
  
  AA aa;
  BB bb;
  BA ba = &b;
  AB ab = {a};

  BAA baa = &ba;
  BAB bab = {&bb};
  ABA aba = &ab;
  ABB abb = {ba};

  return 0;
}

没有毛病可以通过。

只要按照这样套娃,100层我都套的出来,不过那样的代码味道一定蛮大的~

可以看出C语言中的这种类型属实不好认。。。

所以,后续我们有了 typedef 和 using(C++)来补救

让我们的心智负担降低了不少

加入函数指针

那么,上续说的很简单,可是我们遗漏掉了一个东西,函数

形如void (*signal(int, void (*fp)(int)))(int);这种比较 容易 读得懂的东西来说。

还有更多烦人的东西呢

来试试?

普通函数函数指针

我们的函数声明长这个样儿

int add(int x, int y) {}

我们的函数指针类型长什么样子呢?

简单,我们把指针塞到变量的位置嘛!

int (*)(int, int), 好,现在它长这个样子了

还是比较好理解的,用螺旋法则。

加入数组

现在,让我们来升级一下我们的函数指针

现在我们要定义如下东西:

声明 函数指针的 数组

int (*[k])(int, int) 嗯,现在开始犯迷糊了

记住,记住,螺旋法则看待,我们先是一个数组

用的时候,大概长这个样子

int (*w[k])(int, int) = {add};

指向 函数指针的 指针

int (**)(int, int) 这个简单!加一个指针就是

接下来,还要继续么。

回想我们刚才,采用枚举的方式,构造了一系列复杂的指针和数组

通过枚举的方式,对于函数指针也是一样的可行道理

懒得写了,太多了,枚举不过来

复杂案例

现在,我们来写一个比较复杂的函数,就用我们刚才的那几个组成

using BAA = int(**)[k];
using BAB = int(*)[k][k];
using ABA = int *(*)[k];
using ABB = int(*[k])[k];
using F = BAA(ABB, BAB, ABA);

现在函数样式已经定下来了

大抵上是

int(**)[k] fun(int(*[k])[k], int(*)[k][k], int *(*)[k]) {
  return any;   
}

当然,上述声明放到编译器中去编译是不会通过的

tips : 貌似C语言中并不能返回一个数组,可以返回值

那么,现在我们考虑如何把函数写出来

我们先看返回值:

int(**)[k]

我们若是要定义一个变量如何定义?

即为

int (**a)[k]

那么,现在换做我的函数呢?

我们可以等价的替换掉,即:

int(**fun(int(*[k])[k], int(*)[k][k], int *(*)[k]))[k]

把他看作一个变量名一样放进去

现在我们再给参数加上变量名,就可以得到我们的函数

int (**fun(int (*a[k])[k], int (*b)[k][k], int *(*c)[k]))[k] { 
  return a; 
}

那么,我们的函数指针呢?

不要着急,仔细看:

替换的规律,我们把fun替换为一个 (*) 就可以办到这一件事情

using T = int(**(*)(int(**)[k], int(*)[k][k], int *(*)[k]))[k];

好,现在写出来了,如何,还是比较复杂吧

当然,还要更复杂的,我可以返回一个函数数组指针,参数也是函数数组指针,还要指针数组和数组指针

总结

数组和指针是非常灵活的东西,我们的C/C++程序员,尽可能用好指针

最好使用typedef和using来简化我们的操作

现代语言,比如golang、rust这种现在都已经采用后置类型的方式了,就我而已,我任务后置类型是比c/c++这种形式要来的好的

且可以保持统一的风格:

var xxx int
var yyy double

fun func(a int, b double) int {}

当然,C++也可以做到一点保持统一风格

auto fun(int a, int b) -> decltype(a + b) {

}

auto func(int x) -> double {

}

但不可忘记这些东西,举个例子:

你不理解函数指针,你就搞不懂 c++ 中的 function 是如何实现的,有没有其他实现方式

C++的类型萃取更是离不开类型的分辨

甚至你可以使用 模板来给类型添加指针这样的操作

模板再编译期是可以执行的,所以基本上没有损耗一般

比如:

using std::add_pointer;
add_pointer<add_pointer<int>::type[7]>::type t;

就很容易的达到了 指向 数组指针的 指针

最后,附上我开始说的那道题的代码,感受一下吧~

#include 

char a[4][7] = {"Common", "Point", "Boost", "Better"};
char (*b[4])[7] = {a+3, a+1, a, a+2};

char (*(*C(void))[4])[7]
{
    return &b;
}

char (**d(void))[7]
{
    return C()[1] - 3;
}

char buf[256];

char *pointer_monster(char (**f(void))[7])
{
    int len;
    
    len  = sprintf(buf, "%s", *f()[0]);
    len += sprintf(buf + len, "%s ", *((**f)()-1)[0]+4);
    len += sprintf(buf + len, "%s", (*f())[0]-4);
    len += sprintf(buf + len, "%s", f()[1][2]+3);
    len += sprintf(buf + len, "%s", *((**f)()-1)[0]+4);
    return buf;
}

[author: amjieker]

你可能感兴趣的:(札记,c语言,算法,c++)