谈谈c指针的本质

想了很久最终还是写了篇关于c指针的文章。

那么指针是干嘛的?指针只是用来保存地址的并且可以间接引用内存,仅此而已没有什么高深的东西。
那么地址又是什么东西,内存的标号。
内存标号又是啥,一个无符号数,用来表示内存的位置用的。

一个经典的c指针入门的程序

#include <stdio.h>

int main(int argc,char *argv[])
{
    int a = 10;
    int *p = &a;
    printf("a = %d\n",a);
    
    *p = 20;
    printf("a = %d\n",a);
    
    return 0;
}

懂了,int 变量可以让int *改,float可以让float改,…好像没什么问题

回到一开始的话题,指针是干嘛的,保存地址的,那么我用unsigned long 就不能保存吗

比如直接这样

int a = 10;
unsigned long p = &a;

好像这样写会报警告或者错误。嗯,我们强转一下

int a = 10;
unsigned long p = (unsigned long)&a;

OK,这样子就没错了。但是我们让p保存这个地址有什么用呢?

那么我们平时用指针保存地址有什么用?没错,间接引用。间接引用怎么玩?没错使用’*’

那么我们也用‘*’

int a = 10;
unsigned long p = (unsigned long)&a;
*p = 20;

这样写好像不太对劲。没有哪个编译器会通过,怎么办?就是想要改这个内存的值

嗯,我们再强转一下

int a = 10;
unsigned long p = (unsigned long)&a;
*(int *)p = 20;

嗯?是不是感觉没什么意义,转回来又转回去

为什么我们要转成unsigned long 会不会有什么问题?

我们知道地址无符号,32位系统的最大寻址范围是2^32-1,64位同理。所以在32位是4字节,64位是8字节。那么我们用unsigned long来保存显然很完美,不会有什么溢出。

那么指针呢?也是一样,毕竟指针是用来保存地址的,所以指针内存也是32位4字节,64位8字节。那么也意味着指针和unsigned long 之际可以互相转换没有任何错误

上面的案例可以表现什么呢?在不会溢出的情况下用什么类型去保存一个地址其实是无所谓的。只要内存大小够就行了,但是要间接引用那么必须要用指针类型。

ok。既然指针类型的大小用一样,并且都是保存地址用的,那么有什么区别

比如我们这么写

int a = 10;

int *i_p = &a;
*i_p = 20;

short *s_p = (short *)i_p;
*s_p = 20;

char *c_p = (char *)i_p;
*c_p = 20;

没错,虽然引用的内存地址是同一个,但是他们直接操作内存的大小是用区别的

那么还有什么区别?

#include <stdio.h>

int main(int argc,char *argv[])
{
    char  *p1 = NULL;
    short *p2 = NULL;
    int   *p3 = NULL;
    
    printf("p1 = %p\n",p1+1);
	printf("p2 = %p\n",p2+1);
    printf("p3 = %p\n",p3+1);
    
    return 0;
}

都是加一,那么输出啥? 1、2、4

为什么呢?很简单,指针有步长啊。步长怎么算?直接sizeof(*type)

步长这东西用的太多了。比如常用的数组,

int a[3];
*a = 1;
*(a + 1) = 2;
*(a + 2) = 3;

比如还可以方便初始化。

例如要分配内存的时候

struct data
{
    int a;
    int *b;
    int c;
};

场景是我要分配struct data的内存还要给b成员分配内存

一般来说怎么做?

struct data *p = (struct data*)malloc(sizeof(struct data));
p->b = (int *)malloc(sizeof(int));

好像没毛病,但可以这么做

struct data *p = (struct data*)malloc(sizeof(struct data) + sizeof(int));
p->b = (int *)(p + 1);

分配内存的时候多分配4字节,然后利用指针的步长给b成员赋值。有啥好处,少调一次malloc,也少一次free,甚至不用考虑哪个内存先释放和后释放。

总结一下:

指针:

  • 指针是保存地址的
  • 指针和unsigned long之间可以互相转换
  • 指针可以间接引用
  • 指针有步长

指针之间的转换可以干嘛

比如我写一个这东西

char buf[1024];

这是啥?字符数组啊。

但是你可以这么玩

struct data
{
    int a;
    int b;
    int c;
};

char buf[1024];

struct data *pd = (struct data *)buf;
int *pi = (int *)buf;
short ps = (short *)buf;

对的,指针之间任意转换没有任何问题。但是随着转换这个字符数组的意义也发生了改变。

那么字符数组本质是什么?不就是一块可读可写的内存块吗

那么c当中的任何类型就不是内存块了吗?对的任何类型都是

所以这么玩也没问题

int a;
char *p = (char *)&a;
p[0] = 0x11;
p[3] = 0x33;

当然要注意内存的大小了,别玩嗨了。

只是这么玩好像也没什么意思,刚刚说的是任何类型,对的包括指针。

让我们回想一下,我们如果要去改变指针的指向会怎么做?嗯,指针的指针。那么有没有必要呢?

那我们写个这样子的代码

#include <stdio.h>

int main(int argc,char *argv[])
{
    int a = 10;
    int b = 20;
    int *p = &a;
    unsigned long *pp = (unsigned long *)&p;
    
    printf("%d\n",*p);
    *pp = (unsigned long)&b;
    printf("%d\n",*p);
    
    return 0;
}

我们使用一级指针去改变一级指针的指向并且成功了。

这意味着我们就是用一级指针去改变二级、三级、四级。。。都没什么问题。

追根究底最关键的就是理解任何类型声明的变量都是内存,内存有两东西,一个是他的标号也就是地址,一个就是他保存的值。不管是指针也好还是什么类型也好本质都是一样的。

嗯,理解的差不多了,那我们再写这样的代码。

int ****n;

好,声明一个这样的变量。是不是有点害怕,想到什么奇奇怪怪的东西?

然后我们这么用

#include <stdio.h>

int main(int argc,char *argv[])
{
    int ****n = (int ****)10;
    int a[10] = {1,2,3,4};
    unsigned long i = 0;
    for(i = 0;i < (unsigned long)n;i++)
    {
        printf("%d ",a[i]);
    }
    printf("\n");
    
    return 0;
}

我们干了啥,把他当unsigned long用。所以我想说的是不要去看一个指针的声明而感到害怕,真正是要看他怎么去用。他只是一个内存,关键是看这内存存的是什么东西。

比如nginx源码中用到这样一个东西

struct ngx_cycle_s {
    void                  ****conf_ctx;
    ...
}

是不是又害怕了。

来看看这东西保存了什么

//  src/core/ngx_cycle.c:189
cycle->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *));

没错,他保存就只是一个指针数组罢了

我们继续看下去

// src/core/ngx_cycle.c:222
for (i = 0; cycle->modules[i]; i++) {
    if (cycle->modules[i]->type != NGX_CORE_MODULE) {
        continue;
    }

    module = cycle->modules[i]->ctx;

    if (module->create_conf) {
        rv = module->create_conf(cycle);
        if (rv == NULL) {
            ngx_destroy_pool(pool);
            return NULL;
        }
        cycle->conf_ctx[cycle->modules[i]->index] = rv;
    }
}

这个指针数组存的什么东西?create_conf的返回值。

ok我们找一个create_conf看看

typedef struct {
    ngx_array_t               pools;
} ngx_thread_pool_conf_t;

static void *
ngx_thread_pool_create_conf(ngx_cycle_t *cycle)
{
    ngx_thread_pool_conf_t  *tcf;

    tcf = ngx_pcalloc(cycle->pool, sizeof(ngx_thread_pool_conf_t));
    if (tcf == NULL) {
        return NULL;
    }

    if (ngx_array_init(&tcf->pools, cycle->pool, 4,
                       sizeof(ngx_thread_pool_t *))
        != NGX_OK)
    {
        return NULL;
    }

    return tcf;
}

是的这个还是返回的只是个数组地址。

大概是这样的

在这里插入图片描述

所以说看到一个类型的声明不要怕,像nginx这种四级指针只是一个用来做指针数组的玩意。至于这个指针数组的成员保存的是什么东西看后续代码就行。就算是声明int ****a,****a也不一定有会有指向的内存块

比如可以用一个unsigned long去保存二维数组

#include <stdio.h>
#include <stdlib.h>

int main(int argc,char *argv[])
{
    unsigned long a = 0;
    int i = 0;
    
    a = (unsigned long)malloc(sizeof(void *) * 4);
    for(i = 0;i < 4;i++) {
        *((unsigned long *)a + i) = (unsigned long)malloc(sizeof(int) * 4);
    }
    
    return 0;
}

虽然可读性不是很好,但是也完全可行。一个变量的具体意义在于编码者,不是看变量的声明。

总结:
主要理解到任何类型声明的变量皆是内存即可,一块内存用来存什么完全看编码者,但是要先搞定编译器所以会有各种各样的强转。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值