想了很久最终还是写了篇关于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;
}
虽然可读性不是很好,但是也完全可行。一个变量的具体意义在于编码者,不是看变量的声明。
总结:
主要理解到任何类型声明的变量皆是内存即可,一块内存用来存什么完全看编码者,但是要先搞定编译器所以会有各种各样的强转。