C语言学习要领
指针,可以说是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();
}
c语言如何去学
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。