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语言是一把双刃剑。