C语言--说说指针

174 阅读5分钟
原文链接: zhuanlan.zhihu.com

指针,可以说是C语言的核心。

C语言能写应用、写系统、写编译器、写服务器、写算法,几乎没有不能写的软件,就在于指针。C语言的几乎等同于汇编的运行效率(写的不好的汇编,效率还不如C),也来自于对指针的合理使用。

指针,囊括了C的几乎全部的灵活性,也囊括了C的大部分的BUG。

搞清楚了指针,也就掌握了C语言。

“简单就是美”的C语言,没有那么多的设计模式和面向对象的概念,不就是指针嘛。还真就是指针。


1,什么是指针

指针,是标示内存地址的一个"无符号整数",其位数就是标示内存地址所需的位数。

一般32位机上是32位,一般64位机也是64位。与C中的long类型位数一样,后来C标准增加了uintptr_t/intptr_t类型。

在代码中更多的是用void*/char*/int*等,其中void*使用广泛,多被C写成的某些框架代码用来指代用户自定义结构的指针。例如,pthread_create的第三个参数,就是指向线程函数的函数指针,其参数和返回值都是void*:void* (*thread_func)(void*)。

2,指针所含的信息量

指针所含的信息量,在于其位数。(别的类型也是,毕竟数字就是有位数这个概念,只不过计算机是二进制,日常生活是十进制)

如果把指针赋值给一个>=其位数的整数,不会有信息损失,再把它强制转为对应类型的指针时,可以正确使用。当然,在这期间指针指向的数据结构不能被释放,否则就成野指针了。

在一些不支持指针的语言,与C语言对接时,经常使用这种技巧。

例如java的JNI机制,某个类的部分成员函数(native函数)需要用C实现,而且在C中还需要保存一个标示上下文的数据结构x_ctx_t,那么就可以这么实现x_ctx_t的open/close函数:

typedef struct {} x_ctx_t;

jlong x_ctx_open()

{

x_ctx_t* ctx = malloc(sizeof(x_ctx_t));

......

return (jlong)ctx;

}

void x_ctx_close(jlong obj)

{

x_ctx_t* ctx = (x_ctx_t*)obj;

......

}

而在对应的java类中:

class X {

private long nativeObj = 0;

X() {

nativeObj = nativeOpen();

}

void close() { nativeClose(nativeObj); }

private static native long nativeOpen();

private static native void nativeClose(long obj);

};

只要java的long类型>=C的指针的位数,那么就可以这么干。在现在流行的PC或者移动端平台上,这么搞还是可以的。Android代码中有大量的类似代码。


随着32位机以及64位机的流行,变量的内存地址也大多按至少4字节对齐,即指针的值是4的倍数。这就导致了一个事实,指针的值的最低2位是0,从而可以用来把它改成1来存储一些信息,以最大限度的利用内存!在Nginx中Igor Syseov就是这么干的。

3,指针的运算

指针的增减,是按照其指向的数据类型的所需字节数来增减的。

int a[5] = {0, 1, 2, 3, 4};

int* p = a;

p += 2;

这时候p指向a[2]。


如果需要手工来计算字节数,那么就把指针转成char*或者uint8_t*,它们指向的数据类型是单字节的,可以保证指针增加后所指的位置相对于初始位置的偏移量正好是"你计算出来的字节数"。

char b[16];

如果需要在b + 4的位置填充一个32位的整数,可以这么写:

*(int32_t*)(b + 4) = 1000;

4,函数指针

函数指针,是C中灵活性的一个高度体现,是C风格的"面向对象思想"的基础之一,另一个是结构体。

一般C风格的对象是一个代表数据结构的结构体,然后加一个代表操作集合的包含函数指针的结构体:

typedef struct x_s x_t;

typedef struct x_ops_s x_ops_t;

struct x_s {

x_ops_t* ops;

void* priv;

...

};

struct x_ops_s {

const char* type;

int (*open)(x_t* x);

int (*close)(x_t* x);

int (*read)(x_t* x, char* buf, int size);

int (*write)(x_t* x, const char* buf, int size);

...

};

其中x_s就相当于"基类",x_ops_s中的type字段用来标示不同"子类",其中的open/close等函数指针就是该"子类"的对应"虚函数",x_s中的priv指针用来存储"子类私有的数据"。

当根据type类型的字符串查找到对应的x_ops_t结构体并设置到x_s的ops字段后,这个机制就生效了。然后就可以利用"基类指针"p通过p->ops->read()来读数据。

Linux内核的设备驱动和网络协议栈中有大量类似代码,Nginx和ffmpeg中也有类似代码。

其中ffmpeg针对不同协议和文件格式的demux,针对H264、H265、AAC、OPUS等不同的音视频编码协议的编解码,也是用的类似方式。


还有标准库的qsort函数,也需要用户传入一个比较数据大小的函数指针,其定义为:

int (*compar)(const void*, const void*);


char*是指针,函数指针也是指针,都是标示内存地址的整数,他们所占的字节数一样。

所以,可以把上面的x_ops_t结构体看作一个"指针数组",虽然它的各个指针的类型不同,不是真正的"指针数组",但仅仅就按索引访问来说,它就相当于一个数组。

运用OOP思想,它就相当于C++的"虚函数表"。

OOP是一种思想,而不仅仅是一种语言。


5,类型,即是浮云,又不是浮云

C的强制类型转换,可以把各种类型互相转换,然后再转换回去,所以类型是浮云。

但转换时,必须清楚的知道是否有因位数不够导致的数据丢失,以及因符号扩展或零扩展导致的数据填充错误

如果搞错了,那么类型还真不是浮云。

最后一个例子:不用union关键字,来判断大小端序。

uint32_t a = 0x12345678;

uint8_t b = *(uint8_t*)&a;

打印b,看是0x12还是0x78。