一、内存的使用
1.1 你创建的内存区域可能是脏的
当我们创建一个内存区域的时候,内存中的数据可能是乱七八糟的(可能是其他代码用过后遗留的数据),如下面一段代码:
int main(int argc, char *argv[]) { // 下面申请的20个字节的内存有可能被别人用过 char chs[20]; // 这个代码打印出来的可能就是乱码,因为printf的%s是“打印一直遇到' '”。 printf("%s ",chs); return 0; }
其运行结果是如下图所示的乱码,因为printf的%s是“打印一直遇到' '”。
1.2 解决脏内存区域的办法
那么,如何解决上面我们有可能会访问的脏内存区域呢?在C语言中,可以采用如下的两种方法:
(1)使用memset函数首先清理一下内存:
void *memset(void *s, int ch, unsigned n);
将s所指向的某一块内存中的每个字节的内容全部设置为ch指定的ASCII值,
块的大小由第三个参数指定,这个函数通常为新申请的内存做初始化工作,
其返回值为指向S的指针。
那么,我们可以使用memset函数来清理内存,即填充创建的这块内存区域:
int main(int argc, char *argv[]) { // 下面申请的20个字节的内存有可能被别人用过 char chs[20]; // 这个代码打印出来的可能就是乱码,因为printf的%s是“打印一直遇到' '”。 printf("%s ",chs); // memset内存初始化:memset(void *,要填充的数据,要填充的字节个数) memset(chs,0,sizeof(chs)); int i,len=sizeof(chs)/sizeof(char); for(i=0;i<len;i++) { printf("%d | ",chs[i]); } return 0; }
可以看到该代码的运行结果如下图所示,将20个字节都置为了0。
通常对于在堆空间中创建的内存区域,一般都会用到memset函数来清理。
(2)使用初始化填充0:
除了使用memset函数之外,另一种比较直接的方式就是在初始化时直接指定要填充的数据,如下面的代码:
int main(int argc, char *argv[]) { int i; char chs[20] = { 0 }; for(i=0;i<sizeof(chs)/sizeof(char);i++) { printf("%d | ",chs[i]); } printf(" "); int nums[10] = { 7, 5 }; for(i=0;i<sizeof(nums)/sizeof(int);i++) { printf("%d | ",nums[i]); } return 0; }
可以看到运行结果:对于int数组,如果不为其进行初始化,会默认填充0到内存区域。
二、结构体的使用
2.1 结构体的初始化
结构体其实就是一大块内存,我们可以对它进行格式化的存储和读取。如下代码所示,我们试着定义一个结构体:
struct _Person { char *name; int age; double height; }; int main(int argc, char *argv[]) { struct _Person p1; // 不初始化内存区域是脏的 printf("p1.age is %d ",p1.age); return 0; }
在main函数中,我们声明了一个刚刚定义的结构体p1,但是我们并没有进行初始化。这时,会出现上面所提到的脏内存区域的问题,如下所示:
如何解决呢,还是采用上面所说的两个办法:
(1)memset:
// 方法一:使用memset进行清理 memset(&p1,0,sizeof(struct _Person)); printf("p1.age is %d ",p1.age); p1.name = "周旭龙"; p1.age = 26; printf("Name : %s , Age : %d ",p1.name,p1.age); printf("p1.age is %d ",p1.age); printf("------------------------------ ");
注意这里第三个参数是 sizeof(struct _Person)
(2)初始化为0:
// 方法二:初始化 struct _Person p2 = { 0 }; p2.name = "刘德华"; p2.age = 60; printf("Name : %s , Age : %d ",p2.name,p2.age);
两块代码运行结果如下图所示:
第一行是未经清理的脏内存数据,第二部分是使用memset进行清理后再赋值的结果,第三部分是直接初始化后再赋值的结果。
2.2 包含指针的结构体大小
对于普通数据类型的结构体,计算结构体的的大小是件容易的事。但是,如果是有包含有指针的结构体呢?我想,很多跟我一样的菜鸟都会犯错。那么,我们来看看刚刚那个结构体的大小是多少吧?
struct _Person { char *name; // 指针为4个字节,地址(int) int age; // 4个字节 double height; // 8个字节 }; int main(int argc, char *argv[]) { struct _Person p1; printf("The size of p1 is %d ",sizeof(struct _Person)); return 0; }
通过sizeof函数计算该结构体的大小居然为16!没错,你没有看错!不是13,而是16。
那么,问题来了,为什么是16呢?原来,对于int、short等放到结构体中保存是占用对应的字节,但是对于char*等,则只是保存它的指针(地址)。所谓地址,就是一个数字,那么这里就是一个整形数字代表内存地址,因此,它占4个字节,4+4+8=16。
那么,问题又来了,假如我在main函数中,给name赋值了一个很长很长的字符串呢?
struct _Person p2 = { 0 }; p2.name = "刘德华刘德华刘德华刘德华刘德华刘德华刘德华刘德华刘德华刘德华";
我们再次通过sizeof计算大小,仍然是16!为什么呢,我们可以通过下面这张图来看看:
可以看到,无论我们为name赋值多么长的字符串,存储的永远只是一个指向具体字符串的指针,也就是一个地址(一个神奇的数字),结构体的大小不会因为具体指向的字符串的大小而变化。
2.3 使用typedef为结构体取别名
前面的代码中,我们每次使用结构体的时候都要声明struct _Person ,比如:
struct _Person p1={0}; sizeof(struct _Person );
这样显得比较麻烦,可以借助typedef来取一个别名:
typedef struct _Person { char *name; // 指针为4个字节,地址(int) int age; // 4个字节 double height; // 8个字节 } Person; int main(int argc, char *argv[]) { Person p = { 0 }; p.name = "陈冠希"; p.age = 34; printf("Name : %s , Age : %d ",p.name,p.age); return 0; }
看看,是不是清爽得多?
三、结构体的拷贝赋值问题
3.1 结构体的复制其实是“深拷贝”
在C语言中,结构体的复制其实就是将整体拷贝一份而不是将地址拷贝一份,这与在.NET中的深拷贝的概念是类似的(深拷贝和浅拷贝是.NET中一个比较重要的概念)。例如下面一段代码:
Person p1 = { 0 }; p1.name = "陈冠希"; p1.age = 34; // 下面的复制其实是拷贝了一份 Person p2 = p1; p1.age = 100; printf("p1.Name : %s , p1.Age : %d ",p1.name,p1.age); printf("p2.Name : %s , p2.Age : %d ",p2.name,p2.age); printf("Address : %d , %d ",&p1,&p2);
从下面的运行结果可以看出,即使我们在拷贝后改变了原p1的age,但p2的age仍为修改之前的值。最后,从两个结构体的内存地址可以看出,两个结构体是相互独立的内存空间(两块地址相隔了16个字节,刚好是该结构体的大小)。
3.2 如何实现结构体的“浅拷贝”
假如我们要在一个程序中多次引用某个结构体,而不是希望每次复制都拷贝一份新的,这样会增加内存使用量,也就是我们在.NET中时常提到的浅拷贝(拷贝的只是引用地址)。于是,这时我们就可以使用一个指向结构体的指针来实现。
Person* p3 = &p1; p1.age = 250; printf("p1.Name : %s , p1.Age : %d ",p1.name,p1.age); printf("p3.Name : %s , p3.Age : %d ",p3->name,p3->age); // 对于结构体指针,取成员要使用->而不是. printf("Address : %d , %d ",&p1,p3);
这里需要注意的就是,对于结构体指针,取成员要使用 -> 而不是 .
再来看看运行结果,发现两个地址一样,说明都是使用的同一块内存地址:
参考资料
如鹏网,《C语言也能干大事(第三版)》