C语言编程主要操作的对象就是指针

指针从哪里来

指针就是表示内存存储区域的一组数值,使用%p格式化字符串。

Linux系统会为程序维护两个临时变量存储位置:栈、堆。栈的空间少,栈通常在用户更高的地址空间处分配,通常有数M字节的大小,堆一般比栈要更大一点,一般会达到几十甚至是数百M字节。

对于较小的变量,使用int、char、double等定义符号,可以直接在栈中建立空间。

#include <stdio.h>

int main(){
    int a = -1;
    int b[4] = {0, 1, 2, 3};
    int c = 4;
    
    printf("id(a):%p\n", &a);
    printf("id(b[0]):%p\nid(b[3]):%p\n", &b[0], &b[3]);
    printf("id(c):%p\n", &c);
    
    int* d = &b[4];
    printf("id(d[0]):%p\nid(d[3]):%p",&d[0], &d[3]);
    
    return 0;
}

函数退出时C编译器会从栈中“弹出”所有变量,清空整个栈,因此防止了栈上变量的内存泄漏。

值得注意的是,如果获取了指向栈上变量的指针,并且将它用于传参或从函数返回,接收它的函数会产生“段错误”。因为实际的数据被弹出而消失,指针依旧指向被释放的栈区域。

栈容量有限,很容易溢出,因此对于较大的数据结构,比如结构体,尽可能在堆上开辟空间。

malloc、calloc等初始化函数就是Linux为程序进程开辟堆内存空间的函数。这些初始化动作返回的指针,都是指向目标内存区域的起始位置。

一块内存空间一旦使用完毕,应该立即调用free函数释放空间。否则,函数退出后,指针变量在栈上随即被注销。但是指针所指向的内存空间仍然在系统注册为“正在使用”。这就造成了,系统无法再度分配该空间,而进程也没有指针操纵该空间,就成了内存泄漏

使用free释放内存空间之后,该指针仍然会指向原来的地址,成了“野指针”,容易造成危险。为了避免,这种情况的发生,应该在调用free之后,立即将指针置为NULL。

指针有什么用

指针可以用于四个最基本的操作:

  • 向OS申请一块内存,并且用指针处理它。这包括字符串、结构体等等。
  • 通过指针向函数传递大块的内存(比如很大的结构体),这样不必把全部数据都传递进去。
  • 获取函数的地址用于动态调用,可以向其他函数传递函数指针,从而实现callback回调机制。
  • 对一块内存做复杂的搜索,比如,转换网络套接字中的字节,或者解析文件。

指针的使用

  • type *ptr,type类型的指针,名为ptr。
  • *ptr,指针ptr所指向位置的值。
  • *(ptr + i),(ptr所指向位置加上i)的值。以字节为单位的话,应该是ptr所指向的位置再加上sizeof(type) * i。
  • &thing,变量thing的地址。
  • type *ptr = &thing,type类型的名为ptr的指针,其值设置为thing的地址,也就是“新建一个指向thing的指针,thing的类型是type”。
  • ptr++,自增ptr指向的位置,相当于*(ptr + 1)
  • int (*ptr) (int, double),新建一个函数指针,这个指针所指向的函数:需要两个参数——一个int类型,一个double类型;返回一个int类型的值。
  • typedef int (*ptr) (int, double),干脆定义了一类函数类型。然后使用ptr p;来新建一个ptr类型函数指针p。
  • (*ptr)((int) x, (double) y),调用ptr所指向的函数,并传入合法参数。

指针和数组

指针并不是数组,即使C允许以一些相同的方法来使用它们。例如,对于一个数组age[]的指针cur_age,调用sizeof(cur_age)会得到指针的大小,而不是它指向数组的大小。如果想得到整个数组的大小,应该使用数组的名称age。

除了sizeof、&操作和声明之外,数组名称都会被编译器推导为指向其首个元素的指针。对于这些情况,不要用“是”这个词,而是要用“推导”。

一点题外话

提到了栈,那就再说说这一块的编程难点。

函数的栈是非常灵活的内存区,应该竭力避免与栈相关的bug:

  • 不要隐藏某个变量:把重要的变量定义在一个小的局部函数里。这可能会产生一些隐蔽的bug,你认为你改变了某个变量但实际上没有。因为,局部函数里的变量,在函数退出后会全部从栈里弹出。
  • 避免过多的全局变量,尤其是跨越多个文件。如果必须的话,要使用读写器函数——一个专门用来读写全局变量的函数。这并不适用于const定义的常量,因为它们是只读的。
  • 在不清楚的情况下,应该把变量放在堆上。不要依赖于栈的语义,或者指定区域,而是要直接使用malloc创建它。
  • 不要在函数内部定义静态变量。它们并不有用,而且当你想要使你的代码运行在多线程环境时,会有很大的隐患。对于良好的全局变量,它们也非常难于寻找。
  • 避免复用函数参数,特别是搞不清楚仅仅想要复用它还是希望修改它的调用者版本。

参考资料

延伸阅读