今天来讨论一下C中的内存管理。
记得上周在饭桌上和同事讨论C语言的崛起时,讲到了内存管理方面
我说所有指针使用前都必须初始化,结构体中的成员指针也是一样
有人反驳说,不是吧,以前做二叉树算法时,他的左右孩子指针使用时难道有初始化吗
那时我不知怎么的想不出理由,虽然我还是坚信要初始化的
过了几天这位同事说他试了一下,结构体中的成员指针不经过初始化是可以用(左子树和右子树指针)
那时在忙着整理文档,没在意
今天抽空调了一下,结论是,还是需要初始化的。
而且,不写代码你是不知道原因的(也许是对着电脑久了IQ和记性严重下跌吧)
测试代码如下
- #include
- #include
- #include
- struct student{
- char *name;
- int score;
- struct student* next;
- }stu,*stu1;
- int main(){
- stu.name = (char*)malloc(sizeof(char)); /*1.结构体成员指针需要初始化*/
- strcpy(stu.name,"Jimy");
- stu.score = 99;
- stu1 = (struct student*)malloc(sizeof(struct student));/*2.结构体指针需要初始化*/
- stu1->name = (char*)malloc(sizeof(char));/*3.结构体指针的成员指针同样需要初始化*/
- stu.next = stu1;
- strcpy(stu1->name,"Lucy");
- stu1->score = 98;
- stu1->next = NULL;
- printf("name %s, score %d \n ",stu.name, stu.score);
- printf("name %s, score %d \n ",stu1->name, stu1->score);
- free(stu1);
- return 0;
- }
plaincopy
- #include
- #include
- #include
- struct student{
- char *name;
- int score;
- struct student* next;
- }stu,*stu1;
- int main(){
- stu.name = (char*)malloc(sizeof(char)); /*1.结构体成员指针需要初始化*/
- strcpy(stu.name,"Jimy");
- stu.score = 99;
- stu1 = (struct student*)malloc(sizeof(struct
- student));/*2.结构体指针需要初始化*/
- stu1->name =
- (char*)malloc(sizeof(char));/*3.结构体指针的成员指针同样需要初始化*/
- stu.next = stu1;
- strcpy(stu1->name,"Lucy");
- stu1->score = 98;
- stu1->next = NULL;
- printf("name %s, score %d \n ",stu.name, stu.score);
- printf("name %s, score %d \n ",stu1->name, stu1->score);
- free(stu1);
- return 0;
- }
写测试代码的过程中我明白了,同事所说的二叉树遍历算法中所用的左子树和右子树指针不需要初始化,其实是这样的,左子树和右子树指向的必须是二叉树节点类型的结构体指针(你填一个长度相同的指针也可以),而该结构体指针是需要初始化的(见注释2),也就是并没有通过malloc来分配内存,而是将另一个指针的值赋给它
顿时觉得挺无语的,确实,看了很多大学里的教材,对于二叉树的遍历等算法定义的结构体无非是以下形式
- struct node{
- int data;
- struct node* lchild, rchild;
- };
plaincopy
- struct node{
- int data;
- struct node* lchild, rchild;
- };
使用时都直接的
- struct node* root;
- root = (struct node*)malloc(sizeof(struct node));
- root->data = 3;
- struct node* nlchild;
- nlchild = (struct node*)malloc(sizeof(struct node));
- root->lchild = nlchild;
- nlchild->data = 2;
- struct node* nrchild;
- nlrchild = (struct node*)malloc(sizeof(struct node));
- root->rchild = nrchild;
- nrchild->data = 4;
plaincopy
- struct node* root;
- root = (struct node*)malloc(sizeof(struct node));
- root->data = 3;
- struct node* nlchild;
- nlchild = (struct node*)malloc(sizeof(struct node));
- root->lchild = nlchild;
- nlchild->data = 2;
- struct node* nrchild;
- nlrchild = (struct node*)malloc(sizeof(struct node));
- root->rchild = nrchild;
- nrchild->data = 4;
这样子给人造成一种错觉好像结构体成员指针是不用初始化的。
可是,只要是指针,要使用它前就必须保证指针变量的值是一个有效的值;否则,它指向的内存一定是垃圾数据!
C语言的内存管理很重要,集魄力和麻烦于一身,看你自己的心态如何了。如果你积极的面对,你正在控制一切;如果你觉得烦躁,你正不得不控制一切。C仍旧是博大精深的语言,信C哥!
/*附加:仍旧是指针*/
- stu1 = (struct student*)malloc(sizeof(struct student));/*2.结构体指针需要初始化*/
plaincopy
- stu1 = (struct student*)malloc(sizeof(struct
- udent));/*2.结构体指针需要初始化*/
这一句可能会有人把sizeof里边也填成struct student*
可以理解这样的行为,因为stu本来就是struct student*,可是这样子你就没有为结构体分配足够的内存,使用中会因为内存错误同样报错的。
当然,仅仅为结构体指针分配内存还不够,结构体成员指针仍然需要分配内存,如下
- stu1->name = (char*)malloc(sizeof(char));
自己在用结构体指针的时候遇到的引用问题,网上找的一段文字觉得挺不错的,可能对大家有帮助。
在使用结构体指针变量的时候,往往容易犯一个“低级”错误。即定义一个结构体指针变量后就直接对结构体指针变量所指向的结构体成员进行操作,从而产生一些莫名其妙的错误。我们必须要给结构体指针变量赋予一个有效的结构体变量地址,才能正常操作结构体指针变量。比如:
struct UART{
int a;
uchar b;
}
main()
{
struct UART *p;
p->a = 0xXXX;
p->b = 0xXX;
printf("%i,%c",p->b,p->a);
}
这个程序输出的值将是不可预知的,因为“在程序中只是定义了一个结构体指针变量,并没有给该结构体指针变量赋一个有效值,因此该结构体变量所指向的地址将不确定,从而不能得到预期结果”
应该改为:
struct UART{
int a;
uchar b;
}
main()
{
struct UART *p;
struct UART dd;
p = ⅆ //这句一定要有,否则将出现不可预知的问题
p->a = 0xXXX;
p->b = 0xXX;
printf("%i,%c",p->b,p->a);
}
C/C++中
结构体(struct)知识点强化 为了进一部的学习结构体这一重要的知识点,我们今天来学习一下链表结构。
结构体可以看做是一种自定义的数据类型,它还有一个很重要的特性,就是结构体可以相互嵌套使用,但也是有条件的,结构体可以包含结构体指针,但绝对不能在结构体中包含结构体变量。
struct test
{
char name[10];
float socre;
test *next;
};//这样是正确的!
struct test
{
char name[10];
float socre;
test next;
};//这样是错误的!
利用结构体的这点特殊特性,我们就可以自己生成一个环环相套的一种射线结构,一个指向另一个。
链表的学习不像想象的那么那么容易,很多人学习到这里的时候都会碰到困难,很多人也因此而放弃了学习,在这里我说,一定不能放弃,对应它的学习我们要进行分解式学习,方法很重要,理解需要时间,不必要把自己逼迫的那么紧,学习前你也得做一些最基本的准备工作,你必须具备对堆内存的基本知识的了解,还有就是对结构体的基本认识,有了这两个重要的条件,再进行分解式学习就可以比较轻松的掌握这一节内容的难点。
下面我们给出一个完整的创建链表的程序,不管看的懂看不懂希望读者先认真看一下,想一想,看不懂没有关系,因为我下面会有分解式的教程,但之前的基本思考一定要做,要不即使我分解了你也是无从理解的。
代码如下,我在重要部分做了注解:
#include
using namespace std;
struct test
{
char name[10];
float socre;
test *next;
};
test *head;//创建一个全局的引导进入链表的指针
test *create()
{
test *ls;//节点指针
test *le;//链尾指针
ls = new test;//把ls指向动态开辟的堆内存地址
cin>>ls->name>>ls->socre;
head=NULL;//进入的时候先不设置head指针指向任何地址,因为不知道是否一上来就输入null跳出程序
le=ls;//把链尾指针设置成刚刚动态开辟的堆内存地址,用于等下设置le->next,也就是下一个节点的位置
while(strcmp(ls->name,"null")!=0)//创建循环条件为ls->name的值不是null,用于循环添加节点
{
if(head==NULL)//判断是否是第一次进入循环
{
head=ls;//如果是第一次进入循环,那么把引导进入链表的指针指向第一次动态开辟的堆内存地址
}
else
{
le->next=ls;//如果不是第一次进入那么就把上一次的链尾指针的le->next指向上一次循环结束前动态创建的堆内存地址
}
le=ls;//设置链尾指针为当前循环中的节点指针,用于下一次进入循环的时候把上一次的节点的next指向上一次循环结束前动态创建的堆内存地址
ls=new test;//为下一个节点在堆内存中动态开辟空间
cin>>ls->name>>ls->socre;
}
le->next=NULL;//把链尾指针的next设置为空,因为不管如何循环总是要结束的,设置为空才能够在循环显链表的时候不至于死循环
delete ls;//当结束的时候最后一个动态开辟的内存是无效的,所以必须清除掉
return head;//返回链首指针
}
void showl(test *head)
{
cout<<"链首指针:"< <
while(head)//以内存指向为null为条件循环显示先前输入的内容
{
cout< name<<"|"< socre<
head=head->next;
}
}
void main()
{
showl(create());
cin.get();
cin.get();
}
上面的代码我们是要达到一个目的:就是要存储你输入的人名和他们的得分,并且以链状结构把它们组合成一个链状结构。
程序种有两个组成部分
test *create()
和 void showl(test *head)
这两个函数,create是用来创建链表的 ,showl是用来显示链表的。
create函数的返回类型是一个结构体指针,在程序调用的时候我们用了showl(create());,而不用引用的目的原因是引导指针是一个全局指针变量,我们不能在showl()内改变它,因为showl()函数内有一个移动操作head=head->next;,如果是引用的话我们就破坏了head指针的位置,以至于我们再也无法找会首地址的位置了。
下面我们来分解整个程序,以一个初学者的思想来思考整个程序,由浅入深的逐步解释。
首先,我们写这个程序,要考虑到由于是一个链表结构,我们不可能知道它的大小到底是多大,这个问题我们可以用动态开辟堆内存来解决,因为堆内存在程序结束前始终是有效的,不受函数栈空间生命期的限制,但要注意的是我们必须有一个指针变量来存储这一链状结构的进入地址,而在函数内部来建立这一指针变量显然是不合适的,因为函数一旦退出,这个指针变量也随之失效,所以我们在程序的开始声明了一个全局指针变量。
test *head;//创建一个全局的引导进入链表的指针
好解决了这两个问题,我们接下去思考
有输入就必然有输出,由于输出函数和输入函数是相对独立的,为了不断测试程序的正确性好调试我们先写好输出函数和main函数捏的调用,创建函数我们先约定好名为create。
我们先写出如下的代码:
#include
using namespace std;
struct test
{
char name[10];
float socre;
test *next;
};
test *head;//创建一个全局的引导进入链表的指针
test *create()
{
return head;//返回链首指针
}
void showl(test *head)
{
cout<<"链首指针:"< <
while(head)//以内存指向为null为条件循环显示先前输入的内容
{
cout< name<<"|"< socre<
head=head->next;
}
}
void main()
{
showl(create());
cin.get();
cin.get();
}
程序写到这里,基本形态已经出来,输入和调用我们已经有了。
下面我们来解决输入问题,链表的实现我们是通过循环输入来实现的,既然是循环我们就一定得考虑终止循环的条件,避免死循环和无效循环的发生。
在create()函数内部我们先写成这样:
test *create()
{
test *ls;//节点指针
test *le;//链尾指针
ls = new test;//把ls指向动态开辟的堆内存地址
cin>>ls->name>>ls->socre;
head=NULL;//进入的时候先不设置head指针指向任何地址,因为不知道是否一上来就输入null跳出程序
le=ls;//把链尾指针设置成刚刚动态开辟的堆内存地址,用于等下设置le->next,也就是下一个节点的位置
le->next=NULL;//把链尾指针的next设置为空,因为不管如何循环总是要结束的,设置为空才能够在循环显链表的时候不至于死循环
delete ls;//当结束的时候最后一个动态开辟的内存是无效的,所以必须清除掉
return head;//返回链首指针
}
在循环创建之前我们必须考虑一个都不输入的情况。
程序一单进入create函数我们首先必然要创建一个节点,我们先创建一个节点指针,后把者个节点指针指向到动态开辟的test类型的动态内存地址位置上。
test *ls;
ls = new test;
程序既然是循环输入,而结构成员test *next又是用来存储下一个接点的内存地址的,每次循环我们又要动态创建一个新的内存空间,所以我们必须要有一个指针来存储上一次循环动态开辟的内存地址,于是就有了
test *le;
接下来在进入循环前我们要创建链表的第一个节点,第一个节点必然是在循环外创建,于是就有了
cin>>ls->name>>ls->socre;
程序执行者的情况是位置的,所以我们必然要考虑,一上来就不想继续运行程序的情况,所以我们一开始先把head引导指针设置为不指向任何地址也就是
head=NULL;
为了符合le也就是链尾指针的设计思路,我们在循环前一定要保存刚刚动态开辟的内存地址,好在下一次循环的时候设置上一个节点中的next成员指向,于是我们便有了:
le=ls;