study of C
综述
- 文献摘录
- C其他展开
- extern 变量 & 函数
- 指针
- 指针和数组
- const的含义
- const与指针结合
- 使用Typeof提升可读性
- 为什么要封装成函数
- 全部功能写在main里有什么坏处
- 封装函数时会出现什么困难
- 宏
备注 每日一更 +代表更新 -代表还在占坑中(没有内容暂待更新) 更新内容:
- C其他展开
-
- 数组
文献摘录
摘自 你必须知道的495个C语言问题
QA
Q:如何生成“半全局变量” (部分源文件中部分函数可以访问的变量) A: C语言办不到。但是实在要做的话:
- 为一个库或相关函数的集合中的所有函数和全局变量加一个唯一的前缀。并用文字警告,使用该集合的用户不能定义和使用文档列出的公用符合意外的任何带有前缀的其他符号。
- 使用下划线命名。
作用域
标识声明的有效区域:
- 函数
- 文件
- 块
- 原型
命名空间
- 行标(label,goto的目的地)
- 标签(tag,struct,union和enum的名称<-这3种命名空间不相互独立)
- struct/union的成员
- 普通标识符(函数,变量,类型定义,枚举常量)
链接类型
- 外部链接(全局,非静态变量和函数)
- 内部链接(仅限于文件作用域内的静态函数和变量)
- 无链接(举办变量和类型定义(typedef)名称,枚举常量
C其他展开
数组
在创建一个数组的时,有时需要去创建2维甚至是多维的数组,但是往往需要动态的生成。那么可以给出一个简单的方法。 一下例子是用c++ 实现,但是思路相同。主要的思路就是,通过创建一长串的一维数组,模拟二维数组的操作,比如你要访问相应的数组长度,可以使用数组的计算方法,在一维数组中快速的找到相应的元素,同时减少了循环的次数。(如果动态实现,则会减少一个*。 在编程中如果条件允许,可以使用这种方式提高编程效率。(因为C,c++ 本身并不是真的存在高于一维的数据结构。都是模拟的进行操作。(即,都是开辟线性的内存块,我不知道这种编程方式是否会有真正的性能提升。虽然减少了一层的循环。)为了不要成为优化神教, 还是需要对具体情况进行具体的分析。
int intNumber;
cin >> intNumber;
int *intArray = new int[intNumber * intNumber];
for (int i = 0; i != (intNumber * intNumber); i++)
{
cin >> intArray[i];
}
for (int i = 0,j = 1; i != (intNumber * intNumber); i++, j++)
{
cout << intArray[i] << " ";
if (j == intNumber)
{
cout << endl;
// 为什么这里是0?读者自己思考
j = 0;
}
}
结构体比较
不能使用 == 和 != 比较结构体,简单的按字节比较的方法会遇到结构体中没有使用的内存空间(洞)的随机内容和失败。这里的洞是用来补位,以便后续成员对齐的 如果需要比较,需要自己写函数,按域比较。
未定义
// bad code 1
a[i] = i++;
// bad code 2
int i = 7;
printf("%d\n", i++ * i++);
诸如此类代码是未定义的,千万不要使用。
int f()
int h()
int g()
f() + (g() * h())
如上代码也不会改变函数的调用顺序。同理:
(i++) * (i++)
有没有括号都是未定义的。
短路
可以假定: 一旦&&和|| 左边 的表达式已经决定了整个表达式的结果,那么右边的表达式一定不会被求职
序列点
序列点是一个时间点,所有的副作用都应该保证结束。 在上一个和下一个序列点之间,一个对象所保存的值至多只能被表达式修改 一次 ,而且只有在确定将要保存的值的时候才能访问之前的一个值。 C语言标准提及的序列点包括:
- 完整的表达式尾部(表达式语句完成。并且该表达式不是其他表达式的子语句存在)
-
&&、 、?: 、, 操作处 - 函数调用时(参数已经被求值完毕,函数在被实际调用之前)
声明和定义
C系列的语言中,要严格分清,定义和声明的区别。其实也很简单:
// 这是声明
int f();
// 上面还有很多的code
// 这是定义
int f()
{
return 0;
}
// 这是申明和定义
int f()
{
return 0;
}
C语言中,一般来说,定义只能一次。在同一作用空间之中 在希望多个源文件中共享变量或函数的时候,需要确保声明和定义的一致性,(如果有同名的函数或者是变量,在编译的时候不会报错,但是在链接的时候会link失败的错误。这时应该检查是否有变量重名 ) 在.h 和.c文件中,其实相关的处理没有区别。仅仅是在使用的时候,以便编译器检查定义和声明的一致性。 如果需要编译器检查声明的一致性,一定要把全局变量放在头文件中。 永远不要把外部函数的原型放在.C文件中,函数的定义发生改变,很容易忘记修改函数原型
extern 变量 & 函数
extern标识这个变量在外部进行了声明。 外部指仍在同一工程下,但是不属于同一文件之中。
// A.c
#include <stdio.h>
#include "B.c"
extern int i;
int main()
{
// 输出6
printf("&d", i);
return 0;
}
// B.c
int i = 6;
实际上,extern在c和c++之中的处理不同。具体的可以单纯将其理解为,改变量已经在别处定义了。
指针
顾名思义,指针作为一个“特殊的类型”,在初学者中成为了一个十分头疼的问题。 但是我觉得还是可以一探究竟的。 指针可以做为不同的一种数据类型而存在。 比如
// 这里声明一个变量为i, 为int类型
int i;
// 这里声明一个指针类型p, 指向int类型
int *p;
这里要明确2个概念,指向空间的内存,和指针本身所占用的内存。 可以将指针理解为去寻找打开秘密门的钥匙。所指向的类型作为门的类型。虽然统称为钥匙[指针] 但是打开的门[指向类型]不同。门的类型有不同,所以[指向类型]的内存空间大小也有不同。 但指针的内存依然保持着都为4位的这个特性。
指针的关键之处在于,要将“指针类型” 这个概念分开,化为
* 指针类型
* 指针所指向的类型
这2种情况来看。 先声明几个指针放着做例子:
// 指向类型是int
int *ptr;
// 指向类型是char
char *ptr;
// 指向(指向int类型的指针)
int **ptr;
// 指向 int () [3]
int (*ptr)[3];
// 指向 int *() [4]
int *(*ptr)[4];
通过上面可以观察到,指针所指向的类型仅仅把*ptr部分提取出来剩下的类型就是这个指针所指向的类型。
再通过将括号转换为变量A来确定真实的指向类型。 所以
int (*ptr)[3];
/* ↓ */
int A [3];
这样看来所指向的数据类型就一目了然了。
补充-转换任意进制
#include <stdlib.h>
#include <stdio.h>
int main()
{
int number = 12345;
char string[25];
itoa(number, string, 2);
printf("integer = %d string = %s\n", number, string);
return 0;
}
指针就是去访问一段变量的地址。那么对此可能会产生一些疑问,为什么要用指针去访问呢? 比如,有这么一段代码。
int main
{
i = 5;
// your code
}
你想要去将i修改成7。那么你会怎么做?是不是会理所应到的想到如下代码
int main
{
int i = 5;
i = 7;
}
简单方便。是不是? 但是如果告诉你i是这样的。
void f(/*your code*/)
{
// your code
}
int main
{
int i = 5;
}
那么在此时。你应该怎么去修改i的值呢?
void f(int i)
{
i = 7;
}
这样的代码能够实现你想要的功能么?
此时你在运行中,你就会发现不管你怎么设值,在main中的值一直都会是5,不会做出任何的更改。
这是为什么呢?其实很简单,在发生函数调用的时候,会出现传值,还是传地址的考虑。
传值
在C语言中,是将一个变量即上文中的i产生一个副本,即复制一份传送给那个函数。 这是当时为了便于封装函数而采取的设计。即你在函数中对变量的任何操作都不会反应到该变量本身。 这对数学计算的时候是一个非常好的设计,你不需要关系经过了一轮计算后的值会不会被意外的更改,程序会帮你保证不会更改变量自身的值。
传地址
你可能想知道,如果要修改变量的本身那么应该怎么做呢? 那就是传地址的重要性了,地址和指针息息相关,函数通过地址,找到真正的变量,然后通过相应的指针类型,将地址解析出值,然后进行计算。 (说句题外话,数组一定是传地址的,并且因为C语言设计比较懒,在函数传递的时候,并没有传递真正的数组,而是单纯的将函数头的首地址传入。那么,其实这个数组到底有多长呢?)
你可能还有一个疑问,一个int也好,double也好,不是都是占用内存块么?一个指针按理说保存的是一个值,难道保存的是111110-111111这样的么? 当然不可能啊。其实指针只保存第一个地址,即一个类型所占用的第一个内存单元。那么怎么取得正确的值,那就是看指针自身的类型了。比如。
int *
类型的指针,就会在它保存的内存单元往后切4块。然后将内存中的值抽出。如果使用强制转换,你会发现你会得到很多奇怪的值哦。可以自己进行测试。
指针和数组
指针和数组的关系,千丝万缕。因为C语言的懒操作,对于数组这个东西,其实是由一个指针所维护的,其不保存数组长度在每个数组的类型之中,所以会在传参后丢失数组的长度。 sizeof实在编译时发生作用,在数组传递后,C内部只保存数组的头指针。此时在函数中很容易出现越位的错误。(并且越位后,如果不对其做取指,赋值操作,很难发现这个错误编译器因为不知道长度,不会检查此错误)所以要非常小心的使用数组。
如果要传递数组进行操作时,有如下3种方式:
- 定义数组的文件中,声明,定义并初始化一个变量,用以保存数组长度。
// file1.c:
int array[] = {1, 2, 3};
int array_size = sizeof(array) / array[0];
// file2.c
extern int array[];
extern int array_size;
- 使用#define确定数组的长度
// file.h
#define ARRAYSZ 3
// file1.c
#include "file.h"
int array[ARRAYSZ];
// file2.c
#include "file.h"
extern int array[ARRAYSZ];
- 在数组的最后的一个元素放入一个 “哨兵值” (通常0, -1, NULL)
// file1.c
int array[] = {1, 2, 3, -1};
// file2.c
extern int array[];
如果数组已经被初始化,使用2的话就不太好。
虽然C中的数组是由指针假扮的,但是仍然满足C语言的定义匹配规则。即数组和指针是不同的,所以你不能写如下代码:
// file1.c
char a[6];
// file2.c
extern char *a;
这时是类型不匹配的。 你需要将代码改成:
extern char a[6];
数组之坑
为什么说C语言的数组是坑呢?这也是为什么上文所提到的数组长度的计算方法,只有在申明了数组的同一函数内部中才能使用的原因。 可能会很困惑。为什么呢?
因为C语言的数组只有一个指针。 这个仔细想想就会知道答案,为什么在函数内部可以取到数组的长度,转递参数以后,数组的长度就没有了呢?其实这个很简单,脱离了该数组,数组的长度就消失了。这也是为什么在C99之前,要求你声明的数组长度一定是一个常量的原因。 即数组不像以后的类型,里面即保存了数组的头指针,还保存了数组的长度。
C语言只保存了数组的头指针。并在__其他的地方假装__保存了数组的长度。如果在某一函数内部声明了一个数组,这个数组的长度并不保存在这个数组的内部。只有在你去取指的时候(sizeof时),编译器会将这个假装的值给出,让你得到数组的长度。 但是。如果传递参数给出到下一个函数,那这个假装的值就没法给出了。数组就只有一个孤零零的指针,需要考程序员手工维护不要越界。 往往在学生的代码中会出现这种代码:
void f(const int *array_a)
{
// 遍历
for(int i = 0; i != 3/*←*/; i++)
{
// 有问题么?
}
}
int main()
{
int a[] = {1,2,3};
f(a);
return 0;
}
乍一看,毫无问题,其实问题很大,比如,那个3(魔法数),怎么蹦出来的?! 请解释。你可能回答,啊,数组长度是3啊。但是,这是你自己写的代码,以后工作,或者稍微大一点的项目,你不可能自己一个人完成,那么就牵扯到了多人协作,多人工作。那。如果这个数组是你要调用他人写的代码中的数组时,你怎么知道这个数组有多长? 所以现在比较流行的解决办法是:具体可以参加 __part3 指针__下的内容
void f(const int *array_a, unsigned int size)
{
// 遍历
for(int i = 0; i != size; i++)
{
}
}
来获取这个数组的长度。 简单的一个数组稍微展开一些就会有非常多的地方要学呢。
指针和假的字符串
// Q1
char *p = "asdadada";
p[1] = 'S';
printf("%s\n", p);
// Q2
char a[] = "adadasda";
char *q = a;
q[1] = 'S';
printf("%s\n", q);
这段程序的运行结果是什么呢? 是否会被迷惑住,以为都会修改了这个值。其实答案是只有Q2成功修改了该值。 这是为什么呢?
- Q1 是用数组作为初始值,只是多了一个结尾的数组,与其他数组作无异。
- Q2 将改字符串转换为一个无名的静态字符串数组。可能存储在只读内存之中。这将导致它不会被修改。(这里指向的是无名数组的第一个元素)。
空指针
0
原来将设定0的指针称之为空指针。如下:
char *p = 0;
判断指针为空可以简单的记做:
if(p)
//
可以直接使用空指针的情况请确保在如下之一:
- 初始化
- 赋值
- 比较(判断开始,或结尾)
- 固定参数的函数调用且在作用域中有函数原型
必须要显示的类型转换的:
- 函数调用, 作用域内没有原型
- 变参函数调用中的可变参数
NULL
是一个宏, 其值是一个空指针。(原先是为了避免魔法数0的出现) NULL只能用作指针, 并且NULL和0完全等价。上记的注意都是一样的。 同时,编译器还是根据上下文的来进行处理NULL,仍然需要对NULL进行相应的转换。
总结
简单来说,保证如下2点即可
- 当源码中需要空指针常量时,用0或NULL
- 如果在函数中调用0,或者NULL时,要把它转换成被调用函数需要的指针类型
好玩的
printf("%c\n","helloworld"[5]);
printf("%c\n", 5["helloworld"]);
结果是一样的。
const的含义
首先明确, const是告诉程序员这是一个常量。嗯。在C中,const是假的。
不信?可以试试如下代码: 根据 大佬 __←大佬的博客__的测试, 如下代码仍然存在着一些问题。 同时,如果该代码是在__全局变量__中,会产生error。__不能__通过编译。
const int i = 12;
int *p = &i;
// 修改了i
*p = 5;
VS2017中,该代码修改了const所声明的变量。但在mac的gcc中,i的值并未发生改变,不过当你去获取i地址上的值时,发现已经改变了。
// code
// i 的值为 12
printf("%d", i);
int *q = &i;
// *q 的值为5
printf("%d", *q);
这是为什么呢? 根据爆栈 的数据:
When it does optimisation, the compiler presumably loads 12 into a register and doesn't bother to load it again when it needs to access a for the printf because it "knows" that a can't change.
所以是一个很神奇的未定义行为。
展开
应该去写可以移植的代码,一个好的可移植的代码应该遵守:
可移植代码
- 只使用已知的特性
- 不突破编译器的限制
- 不使用,依赖任何的未定义行为
不可移植代码
由编译器定义的代码,是由编译器决定的如何采取行动
烂代码
- 有未定义行为的
- 没有遵守约束条件的(常量表达式不被赋值或修改,变量和常量的值应该在其所在的范围内)
- 其他可以导致程序出现异常或增加读代码人的劳动的
综上,其实在C编译器中其实不能很好的做到对const声明变量的保护,gcc的保护行为也是未定义的,不能假象某一个编译器的实现去类推其他操作系统或者代码。远离未定义 是否修改了const也是程序员应该关心的问题呢。(←别指望编译器了。)
所以,这个大前提一定要明白,清楚。const是告诉程序员这是一个常量。所以你要自己去保证不要去乱改const里所保存的值。(出了问题就是你的锅)
const的含义就是: 告诉你这是一个常量,你不要改。
当然你希望这个常量不能被改,理所应当给它设个初始值。这是常识。什么时候用const也不必说。 但是在新手中常犯的错误有:
\\ 不理解const的是假的含义,但知道程序中不能存在魔法数的规范:
const int i = 1;
\\ 会报错
\\ C99 后不报错,成为新的规则。具体情况请参阅自己编译器所支持的语言版本。
int arrA[i] = { 1 };
const与指针结合
const int n = 5;
int const m = 10;
效果等价。 但是如果和指针相互结合在一起。就会有很大的不同。
const int *p;
int const *q;
这2个所指向的类型是什么呢? 根据上文所说,去掉__*变量名__所剩下的就是所指向的类型,那么这两个所剩下的是:
const int
int const
由根据上文所说,const和int的位置交换所代表的含义相同。所以这2个变量所指向的类型也是相同的,指向const int类型,所以这个p和q是可以随便指向其他的const int类型的,甚至int类型(你关了warming的话)
为了避免误解,请将变量名自己规定为const在前。(←正常人的想法)
那么我现在其实想声明一个不可以改变的指针怎么办?
int n = 12;
int *const r = &n;
上面的代码片段才是正确的写法。用正确的写法能够最大的减少误解, 程序是给人读的!
再按照之前的原则拆分一下。*变量名,哎?变量名中间有const,那就是指针本身不可改。剩下了int,那就是指向int类型。
下面给出一些栗子。(举个栗子,请脑补)
char **p;
const char **p2;
char *const *p3;
const char *const *p4;
char **const p5;
const char **const p6;
char *const *const p7;
const char *const* const p8;
方法学会了么? 以第4个为例,
- 首先抽出 *const p4, 证明这是一个const 指针
- 剩下的为const char *A
- 所以这是一个const指针指向[ 一个指向const char 类型的指针 ]
答案
- p1是指向char类型的指针的指针;
- p2是指向const char类型的指针的指针;
- p3是指向char类型的const指针;
- p4是指向const char类型的const指针;
- p5是指向char类型的指针的const指针;
- p6是指向const char类型的指针的const指针;
- p7是指向char类型const指针的const指针;
- p8是指向const char类型的const指针的const指针。
使用Typeof提升可读性
良好的使用typedef可以使你的代码具有更好的可读性。 比如:
typedef char * PCHAR;
// 声明了2个指向字符的指针 p,q
PCHAR p,q;
//类比如下代码
char *p, q;
最后一行代码读懂了么?乍一看是也是声明了2个指针。其实是声明了一个指针和一个char类型的变量。所以要分清指针的*表述的意思。
与#define的区别
- typedef 遵守作用域规则。而#define是全局的,全部替换,并且不能正确的处理指针类型。
- 但是#define 可以使用#ifedf。
所以要根据实际情况进行选择。
命名方式展开
推荐采用:
char *p;
这样(星号靠右)的命名方式,这样你才能理解到,这个指针是针对于p的。即,只有char,int等类型,和一个指针类型__*__, 前面的类型仅仅是用来标识,从内存中读出几位字节而已。这样理解就不会出现如上的声明方式,而产生误解。或者采用 typedef 来讲其进行别名转换。
具体的好处可以参见如下:
typedef char *a;
typedef a b();
typedef b *c;
typedef c d();
typedef d *e;
e var[10];
是不是一下子就晕了。 咱们来慢慢梳理一下。 由后往前看。
- 这是一个 1.数组。 (1类型是e)
- 这是一个 2.指针类型的 1.数组。 (2指向的类型是d)
- 这是一个 3.指向函数的 2.指针类型的 1.数组。 (3函数返回的类型是c)
- 这是一个 4.指向有返回值的 3.指向函数的 2.指针类型的 1.数组。 (4指向的类型是b)
- 这是一个 4.指向返回值 5.是指针的 3.指向函数的 2.指针类型的 1.数组。 (5函数返回的类型是a)
- 这是一个 4.指向返回值 5.是指针指向 6.返回值是指向char类型的 3.指向函数的 2.指针类型的 1.数组。 (6指向的类型是char)
是不是一下子就懂了呢。 然后以此类推。 初次使用typedef可能会晕,还是要多多练习。 同时,在使用typedef时,注意。
// 普通声明变量
int a;
// typedef申明变量别名
typedef int a;
typedef在保证原来申明语法不变的前提下,将原变量的命名,作为空格之前的__东西__作为它的别名。 这点要在以后的函数指针中要理解透彻。
为什么要封装成函数
封装成函数可以:
- 提高可读性
- 提高健壮性
- 在修改函数的时候更加方便
- 维护程序的时候能够更好的去修改代码
- 适合项目中多人开发
但同时也有一些不算缺点的问题:
- 封装函数反而降低维护性
- 封装函数产生技术困难
- 封装函数没有意思
下面针对几点进行说明:
// 使用函数
int addValueFive(int a)
{
return a += 5;
}
// 不使用函数
int main()
{
int i = 21;
// 其他code
i += 5;
return 0;
}
可读性更高,阅读他人代码的时候,可以跟读小说一样将代码的含义阅读出来。而不必去关心其余不相关的问题,比如这里的值是否正确,是否真的是要去加5而不是误输入等其他问题。
当然封装函数的时候要掌握一个度,不需要将所有的功能全部拆分成块。毕竟在调用函数的时候也会产生相应的开销。在需要特殊优化的地方。这里应该也是需要被最后考虑的问题。
其次在初学者学习的时候,容易犯下如此的错误:
// wrong code 1
void changeValue1(int i)
{
i += 5;
printf("%d", i);
}
// wrong code 2
void changeValue2()
{
int i = 21;
i += 5;
printf("%d", i);
}
int main()
{
int i1 = 21;
int i2 = 21;
changeValue1(i1);
changeValue2();
return 0;
}
乍一看,上述2个函数都完成了将i增加5的功能,并有效的输出了变换后的值,但是其实并没有真正的实现这一个功能,对于第一个代码来说,仅仅是对形参增加了5,对main函数自身的i并没有更改,而第二个代码。根本没有将功能封装成函数。这是初学者容易犯的错误。其实对于第二种代码片段,更像是其他语言所拥有的命名空间,偶尔在C语言中也可以如此使用,但是一定要确保自己的函数是正确的,并且自己知道这么做的后果是什么再去这样使用。
全部功能写在main里有什么坏处
初学者会烦的错误就是一般会将所有部分全都写在main函数中,这样做其实也有一定的好处。
- 不需要考虑同名变量的作用域
- 不需要考虑用指针去修改变量
- 数组可以直接获取到长度
- 思维直线式-符合初学者思维
嗯,有这么多优点呢。这些优点就是你以后要努力克服不去触及的坑呢。如果不把坑填平,无论如何都不可能学会C的。甚至其他语言。 上文提到过,程序的可读性这个问题,在你面对上百上千行代码的时候,而且全部揉在main中,这个程序的可读性基本是0,过了几个月后,开发者本身可能都不能理解其中的逻辑依赖关系,再三强调代码是给人读的所以一定要将代码抽成函数。 抽出函数的时候,才能更好的提高自己的知识水平。 全部都写在main里。总结来说会使你的代码越来越烂而已。无他。
小提示:
\\获取数组长度 只在申明了数组的同一函数内部中使用
\\ 数组类型是int
int size = sizeof(array) / sizeof(int);
\\ 或使用如下方法
int size = sizeof(array) / sizeof(array[0]);
封装函数时会出现什么困难
- 程序封装函数后,传参出现问题,主要是由数组导致的(丢失了长度),产生数组越界或者其他bug
- 程序需要对多个变量进行更改,而返回值只有一个。需要更好的设计对整体操作进行拆分
宏
#define
使用宏可以完成很多非常有用的功能,比如如下代码:
__VA_ARGS__
// C99支持
#define showlist(...) puts(#__VA_ARGS__)
// puts("")
showlist();
// puts("1,\"x\", int")
showlist(1, "x", int);
此时,应用:不需要输入”“\等符号直接生成表示地址的字符串。
#include <stdio.h>
#include <stdlib.h>
#define showlist(...) put_file_name(#__VA_ARGS__)
char file_name[100] ={0};
char* put_file_name(const char * file)
{
int i = 0;
while((*file++) != '\0')
{
file_name[i] = *(file - 1);
i++;
if(i == 98)
break;
}
file_name[i + 1] = '\0';
return file_name;
}
int main()
{
char *file;
int j = 0;
// 不需要写转义字符,直接书写地址。
file = showlist("D:\code\cplus\TestGCC\Test\bin\Debug");
puts(file);
system("pause");
return 0;
}
使用宏还可以装作模板来生成代码:(摘抄自手册)
#include <stdio.h>
//make function factory and use it
#define FUNCTION(name, a) int fun_##name(int x) { return (a)*x;}
FUNCTION(quadruple, 4)
FUNCTION(double, 2)
#undef FUNCTION
#define FUNCTION 34
#define OUTPUT(a) puts( #a )
int main(void)
{
printf("quadruple(13): %d\n", fun_quadruple(13) );
printf("double(21): %d\n", fun_double(21) );
printf("%d\n", FUNCTION);
OUTPUT(million); //note the lack of quotes
}
其他问题点
malloc 和 free
free 掉malloc 开辟出来的内存时,是全部都会被free 掉. 因为malloc 在开辟内存的时候,在内存之前添加了header.