####0x05-C语言指针(Volume-2)

内存的使用的那些事儿

你一直以为你操作的是真实物理内存,实际上并不是,你操作的只是操作系统为你分配的资格虚拟地址,但这并不意味着我们可以无限使用内存,那内存卖那么贵干嘛,实际上存储数据的还是物理内存,只不过在操作系统这个中介的介入情况下,不同程序窗口(可以是相同程序)可以共享使用同一块内存区域,一旦某个傻大个程序的使用让物理内存不足了,我们就会把某些没用到的数据写到你的硬盘上去,之后再使用时,从硬盘读回。这个特性会导致什么呢?假设你在Windows上使用了多窗口,打开了两个相同的程序:

...
int  stay_here;
char tran_to_int[100];
printf("Address: %p\n", &stay_here);

fgets(tran_to_int, sizeof(tran_to_int), stdin);
sscanf(tran_to_int, "%d", &stay_here);

for(;;)
{
    printf("%d\n", stay_here);
    getchar();
    ++stay_here;
}
...

对此程序(引用前桥和弥的例子),每敲击一次回车,值加1。当你同时打开两个该程序时,你会发现,两个程序的stay_here都是在同一个地址,但对它进行分别操作时,产生的结果是独立的!这在某一方面验证了虚拟地址的合理性。虚拟地址的意义就在于,即使一个程序出现了错误,导致所在内存完蛋了,也不会影响到其他进程。对于程序中部的两个读取语句,是一种理解C语言输入流本质的好例子,建议查询用法,这里稍微解释一下:

  • 通俗地说,fgets将输入流中由调用起,stdin输入的东西存入起始地址为tran_to_int的地方,并且最多读取sizeof(tran_to_int)个,并在后方sscanf函数中将刚才读入的数据按照%d的格式存入stay_here,这就是C语言一直在强调的流概念的意义所在,这两个语句组合看起来也就是读取一个数据这么简单,但是我们要知道一个问题,一个关于scanf的问题

      scanf("%d", &stay_here);
    

这个语句将会读取键盘输入,直到回车之前的所有数据,什么意思?就是回车会留在输入流中,被下一个输入读取或者丢弃。这就有可能会影响我们的程序,产生意料之外的结果。而使用上当两句组合则不会。

#####函数与函数指针的那些事 事实上,函数名出现在赋值符号右边就代表着函数的地址

int function(int argc){ /*...*/
}
...
int (*p_fun)(int) = function;
int (*p_fuc)(int) = &function;//和上一句意义一致

上述代码即声明并初始化了函数指针,p_fun的类型是指向一个返回值是int类型,参数是int类型的函数的指针

p_fun(11);
(*p_fun)(11);
function(11);

上述三个代码的意义也相同,同样我们也能使用函数指针数组这个概念

int (*p_func_arr[])(int) = {func1, func2,};

其中func1,func2都是返回值为int参数为int的函数,接着我们能像数组索引一样使用这个函数了。

Tips: 我们总是忽略函数声明,这并不是什么好事。

  • 在C语言中,因为编译器并不会对有没有函数声明过分深究,甚至还会放纵,当然这并不包含内联函数(inline),因为它本身就只在本文件可用。

  • 比如,当我们在某个地方调用了一个函数,但是并没有声明它:

      CallWithoutDeclare(100); //参数100为 int 型
    

那么,C编译器就会推测,这个使用了int型参数的函数,一定是有一个int型的参数列表,一旦函数定义中的参数列表与之不符合,将会导致参数信息传递错误(编译器永远坚信自己是对的!),我们知道C语言是强类型语言,一旦类型不正确,会导致许多意想不到的结果(往往是Bug)发生。

  • 对函数指针的调用同样如此

#####C语言中malloc的那些事儿 我们常常见到这种写法:

int* pointer = (int*)malloc(sizeof(int));

这有什么奇怪的吗?看下面这个例子:

int* pointer_2 = malloc(sizeof(int));

哪个写法是正确的?两个都正确,这是为什么呢,这又要追求到远古C语言时期,在那个时候, void* 这个类型还没有出现的时候,malloc 返回的是 char* 的类型,于是那时的程序员在调用这个函数时总要加上强制类型转换,才能正确使用这个函数,但是在标准C出现之后,这个问题不再拥有,由于任何类型的指针都能与 void* 互相转换,并且C标准中并不赞同在不必要的地方使用强制类型转换,故而C语言中比较正统的写法是第二种。

题外话: C中的指针转换需要使用强制类型转换,而不能像第二种例子,但是C中有一种更好的内存分配方法,所以这个问题也不再是问题。

Tips:

  • C语言的三个函数malloc, calloc, realloc都是拥有很大风险的函数,在使用的时候务必记得对他们的结果进行校验,最好的办法还是对他们进行再包装,可以选择宏包装,也可以选择函数包装。
  • realloc函数是最为人诟病的一个函数,因为它的职能过于宽广,既能分配空间,也能释放空间,虽然看起来是一个好函数,但是有可能在不经意间会帮我们做一些意料之外的事情,例如多次释放空间。正确的做法就是,应该使用再包装阉割它的功能,使他只能进行扩展或者缩小堆内存块大小。

#####指针与结构体

typedef struct tag{
        int  value;
        long vari_store[1];
}vari_struct;

乍一看,似乎是一个很中规中矩的结构体

...
vari_struct  vari_1;
vari_struct* vari_p_1 = &vari_1;
vari_struct* vari_p_2 = malloc(sizeof(vari_struct))(

似乎都是这么用的,但总有那么一些人想出了一些奇怪的用法

int          what_spa_want = 10;
vari_struct* vari_p_3 = malloc(sizeof(vari_struct) + sizeof(long)*what_spa_want);

这么做是什么意思呢?这叫做可变长结构体,即便我们超出了结构体范围,只要在分配空间内,就不算越界。what_spa_want解释为你需要多大的空间,即在一个结构体大小之外还需要多少的空间,空间用来存储long类型,由于分配的内存是连续的,故可以直接使用数组vari_store直接索引。 而且由于C语言中,编译器并不对数组做越界检查,故对于一个有N个数的数组arr,表达式&arr[N]是被标准允许的行为,但是要记住arr[N]却是非法的。 这种用法并非是娱乐,而是成为了标准(C99)的一部分,运用到了实际中

#####对于内存的理解

在内存分配的过程中,我们使用 malloc 进行分配,用 free 进行释放,但这是我们理解中的分配与释放吗? 在调用 malloc 时,该函数或使用 brk() 或使用 mmap() 向操作系统申请一片内存,在使用时分配给需要的地方,与之对应的是 free,与我们硬盘删除东西一样,实际上:

int* value = malloc(sizeof(int)*5);
...
free(value);
printf("%d\n", value[0]);

代码中,为什么在 free 之后,我又继续使用这个内存呢?因为 free 只是将该内存标记上释放的标记,示意分配内存的函数,我可以使用,但并没有破坏当前内存中的内容,直到有操作对它进行写入。 这便引申出几个问题:

  • Bug更加难以发现,让我们假设,如果我们有两个指针p1,p2指向同一个内存,如果我们对其中某一个指针使用了 free(p1); 操作,却忘记了还有另一个指针指向它,那这就会导致很严重的安全隐患,而且这个隐患十分难以发现,原因在于这个Bug并不会在当时显露出来,而是有可能在未来的某个时刻,不经意的让你的程序崩溃。
  • 有可能会让某些问题更加简化,例如释放一个条条相连的链表域。

某些大哥提到说,free并不是什么都不做,而是将该段地址空间的前面一小部分置零 但是如果地址空间很长的话,依旧有误用的风险,希望大家还是警惕

实际上之所以库作者不让free操作将地址空间清空,有一部分原因是为了性能考虑,因为置零操作是一个消耗性能的行为,具体可以自行尝试,所谓双刃剑就在于此。

总的来说,还是那句话C语言是一把双刃剑。


书籍推荐