[笔记]指针与结构体
结构体(structure)与数组都是属于派生的数据类型(derived data type)数组的每一个元素都是相同的数据类型,而结构体则是由多个相同或不同数据类型的成员所组成的集合体。
指向结构体变量的指针
我们用一段代码说明指向结构体变量的指针(pointer to structure variable),以及如何访问结构体变量的成员
#include<stdio.h>
#include<stdlib.h>
int main()
{
struct employee
{
char id[7];
char name[20];
int salary;
};
//声明一个结构体,拥有三个成员变量
struct employee manager = { "D54321","Peter",35000 };
//定义一个结构体变量,并为结构体变量依次赋初值
struct employee* p = &manager;
//定义一个结构体类型指针,指向结构体变量
printf("&p=%x\n",&p);
//十六进制输出指针p的地址
printf("p=%x\n", p);
//十六进制输出manager的首地址
printf("&manager=%x\n", &manager);
printf("&manager.id=%x\n", &manager.id);
printf("manager.name=%x\n", manager.name);
printf("&manager.salary=%x\n\n\n", &manager.salary);
printf("<<使用.运算符取得结构体内的值>>\n");
printf("manager.id=%s\n", manager.id);
printf("manager.name=%s\n", manager.name);
printf("manager.salary=%d\n\n\n", manager.salary);
printf("<<使用->运算符取得结构体内的值>>\n");
printf("p->id=%s\n", p->id);
printf("p->name=%s\n", p->name);
printf("p->salary=%d\n\n\n", p->salary);
printf("<<使用(*).运算符取到结构体内的值>>\n");
printf("(*p).id=%s\n", (*p).id);
printf("(*p).name=%s\n", (*p).name);
printf("(*p).salary=%d\n\n\n", (*p).salary);
system("pause");
return 0;
}
输出结果:
从输出结果得知,ptr指向结构体,manager的地址(30fd30),也是此结构体第一个成员(id)的地址(&manager.id),我们用图来解释下:
同时,也告诉了我们如何打印出结构体中每一成员的数据。假如是一般的变量,则一运算符(.)处理,如:manager.id,假如是指针变量,则使用->处理,如p->id
也可以使用().来处理,如(p).id,但比较麻烦,所以这一种比较少用
容易出错点:
在指针和结构体结合的时候,较易出错的地方,我们来剖析一下,避免做题的时候遇到错误,先看代码:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
int main()
{
struct student
{
char name[20];
int score;
};
struct student st1, * st2;
st2 = (struct student*)malloc(sizeof(struct student));
//为结构体指针分配一段结构体大小的内存空间。
printf("请输入您的姓名:\n");
scanf("%s", st1.name);
printf("请输入您的C语言分数:\n");
scanf("%d", &st1.score);
printf("\n请输入您对象对的姓名:\n");
scanf("%s", st2->name);
printf("请输入您对象的C语言分数:\n");
scanf("%d", &st2->score);
printf("\n您的姓名:%s,您的C语言分数:%d\n", st1.name, st1.score);
printf("\n您对象的姓名:%s,您对象的C语言分数:%d\n", st2->name, st2->score);
free(st2);
//释放st2所管理(指向)的内存
printf("\n您对象的姓名:%s,您对象的C语言分数:%d\n", st2->name, st2->score);
printf("\n您的姓名:%s,您的C语言分数:%d\n", st1.name, st1.score);
system("pause");
return 0;
}
运行结果:
由于程序便没有将结构体变量指定给结构体指针变量,所以必须用malloc函数,分配内存给结构体指针st2
当给st2分配好该结构体空间的时候,就可以指定数据给该指针。
malloc原型存放于<stdio.h>头文件内,其语法如下:
- malloc(bytes数目);
除了malloc函数可用来动态分配内存外,还有calloc函数,其语法如下:
- calloc(分配次数,每一次的bytes数目);
使用malloc与calloc函数,分配st2所需要的内存,如下:
- st2=(struct student*)malloc(sizeof(struct student));
- st2=(struct student*)ccalloc(1,sizeof(struct student));
上述表示st2指针指向刚分配的结构体,并将此内存分割给name与score两个成员,如图:
需要注意的是,malloc与calloc函数均须类型转换,让它符合st2的数据类型(struct students*),这样才能正确的分配其内存
free为释放st2所指向的内存,清空它所指向的内存地址内的数据,但是这篇内存还是属于st2管理,可以实现删数据与改数据
在加了这段代码后,我们来看看运行结果:
我们可以看到,在释放内存后,内存内的数据就为空了,但我们依旧可以指针去添加st2结构体内的数据
我们利用一段代码来说明:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
int main()
{
struct student
{
char firstname[20];
char* lastname;
int socre;
};
struct student st1, * st2;
printf("请输入第一位同学的姓:");
scanf("%s", st1.firstname);//不可以指定一个字符串给它
st1.lastname = "小轩"; //不可以使用scanf输入函数给它
printf("%s\n", st1.lastname);
printf("请输入第一位同学的分数:\n");
scanf("%d", &st1.socre);
st2 = (struct student*)malloc(sizeof(struct student));
printf("请输入第二位同学的姓名:");
scanf("%s", st2->firstname);
st2->lastname = "小C";
printf("%s\n", st2->lastname);
printf("请输入第二位同学的分数:\n");
scanf("%d", &st2->socre);
printf("\n%s%s的C语言分数为:%d\n", st1.firstname, st1.lastname, st1.socre);
printf("%s%s的C语言分数为:%d\n", st2->firstname, st2->lastname, st2->socre);
system("pause");
return 0;
}
输出结果:
这段代码中要注意几点:
- 程序中student结构体的成员fitstname是一个数组,由于它是指针常量,在初定义时并未载入数据,则不可重定义,就不可以将一个字符串指定给它
如果要指定一个字符串给它的话,必须在定义结构体变量时并给其赋值
- 而lastname是一个指针变量,目前也没有指向任何内存,则不可以使用scanf函数来输入数据
- 如果要输入的话需要使用malloc函数为他分配一段内存
结构体指针与自增运算符
结构体指针与自增运算符(++)结合时的用法,要注意的是++的作用段是值还是地址,我们来看一段代码
#include<stdio.h>
#include<stdlib.h>
int main()
{
struct student
{
char* name;
int score;
};
struct student st = { "Axuan",97 };
struct student* st2;
st2 = &st;
printf("st2->name=%s\n", st2->name);
printf("*st2->name=%c\n", *st2->name);
printf("*st2->name++=%c\n",*st2->name++);
printf("*st2->name=%c\n",*st2->name);
printf("st2->score=%d\n",st2->score);
printf("st2->score++=%d\n",(st2->score)++);
printf("st2->score=%d\n",st2->score);
}
运行结果:
结构体的声明与定义我们用图表示出来
st2->name
此语句相当于(*st2).name,以%s输出结果为Axuan
*st2->name
由于->的运算符优先级高于,所以该表达式等价于(st2->name),得到的结果是字符A
*st2->name++
表示*(st2->name)++,由于*和++的运算优先级相同,而且其结合性是由右向左,所以此处的++作用于st2->name,因此,得到B字符之后,将name的指针移到r的地址,用图表示为:
st2->name
验证上述name的指针是否指向下一字符,就是r
st2->score
取出score结构体变量的值,为97
(st2->score)++
这里的++作用于st2->score,先取值后自增,为97
st2->score
验证上一语句,结果为98
我们再看一段代码:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
int main()
{
struct student
{
char name[20];
int score;
};
struct student st = { "Xiaoxuan",97 };
struct student* st2;
st2 = &st;
printf("st2->name=%s\n", st2->name);
printf("*st2->name=%c\n", *(st2->name));
printf("++*(st2->name)=%c\n",++*(st2->name));
printf("st2->name=%s\n", st2->name);
printf("*st2->name=%c\n",*st2->name);
printf("st2->score=%d\n",st2->score);
printf("st2->score++=%d\n",(st2->score)++);
printf("st2->score=%d\n",st2->score);
system("pause");
return 0;
}
相比于上一段代码,在student结构体内,将name从指针变量改为了指针常量
在
前置自增++*(st2->name),先将st2指向st结构体内name的首地址,也就是Xiaoxuan字符串首字符X的地址,再结合外面的*符号取到X,在自增,输出的为字符格式的Y
因为指针变量是不可以修改静态数据区的内容的,区别如下:
char *name = "Xiaoxuan";
"Xiaoxuan"保存在文字常量区,该数据不能修改,默认有只读属性.
由指针name指向. 不能通过指针name来修改此值.
char name[20] = "Xiaoxuan";
"Xiaoxuan"保存在栈空间数组里. 数组名为name, 数组名为数组的首地址.
可以通过name[i]='Y', 或*(name+i)='Y'的形式来修改数组内容.
而st2是指向st的首地址,也就是name的首地址,如果当name为指针变量的时候,st2可以运算但是必须遵从name的规则,如果name存储的值在栈内,即可通过指针走向修改指向地址内的值,但是若name存储的值在静态数据区内,则只有只读属性,具体请移步:指针与内存笔记
结构体与函数:
将一结构体传送给一函数,就像传送数组一般,只要将结构体的地址作为实际参数传送给形式参数即可,因为是传址调用,这该形式参数为实际参数拷贝过来的地址,需要配合*取值符号,换句话来说,形参是指向结构体变量的指针。
我们来看一段代码:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
void passOrdown(struct student*);//声明函数,返回类型为无类型,参数为student结构体类型指针
void output(struct student*);//声明函数,返回类型为无类型,参数为student结构体类型指针
struct student
{
//定义全局变量结构体,拥有三个成员变量
char name[20];
int score;
char* passdown;//为char*类型,未分配内存
};
int main()
{
struct student stu;//定义一结构体变量
printf("请输入您的姓名:\n");
scanf("%s", stu.name);//name为数组类型,指针常量,stu空间内已有属于name的空间
printf("请输入您的C语言分数;\n");
scanf("%d", &stu.score);
passOrdown(&stu);//将stu的首地址传入到passOrdown函数内
output(&stu);//将stu的首地址传入到output函数内
getchar(NULL);//清空缓冲区
return 0;
}
void passOrdown(struct student* p)
{
if (p->score >= 60)
{
p->passdown = "PASS";
}
else
{
p->passdown = "DOWN";
}
}
void output(struct student* q)
{
int i;
printf("\n\n%10s %10s %20s\n", "Name","Score","Pass or Down");
for (i = 0; i <= 42; i++)
{
printf("=");
}
printf("\n");
printf("%10s %10d %20s\n", q->name, q->score, q->passdown);
}
输出结果:
程序将实际参数stu的地址拷贝传给形式参数p和q后,将以这些结构体变量指针取得结构体成员
若要表示一班同学的成绩,则可使用结构体数组(structure array)
我们用一段代码来讲解:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<conio.h>
void passOrdown(struct student*);
void output(struct student*);
struct student
{
char name[20];
int score;
char* passdown;
};
int main()
{
struct student stu[3];//定义一个结构体类型的数组,数组内的每一元素都为结构体类型
int i;
for (i = 0; i < 3; i++)
{
printf("请输入第#%d位同学的姓名:\n",i+1);
scanf("%s", stu[i].name);
printf("请输入第#%d位同学的学分:\n",i+1);
scanf("%d", &stu[i].score);
printf("\n");
}
passOrdown(stu);
output(stu);
getchar(NULL);
return 0;
}
void passOrdown(struct student* p)
{
for (int i = 0; i < 3; i++)
{
if (p[i].score >= 60)
{
p[i].passdown="PASS";
}
else
{
p[i].passdown="DOWN";
}
}
}
void output(struct student* q)
{
int i;
printf("\n\n%10s %10s %20s\n", "NAME", "SCORE", "PASS or DOWN");
for (i = 0; i <= 42; i++)
{
printf("=");
}
printf("\n");
for (i = 0; i < 3; i++)
{
printf("%10s %10d %20s\n", q->name, q->score, q->passdown);
q++;
}
}
与上例代码不同的是,程序定义了一个结构体数组stu[3],里面包含了3个结构体数据类型的变量
利用循环给name和score赋值。注意,passdown为指针变量,且它为被分配内存,只可指定一片内存给它管理,就不能通过scanf 函数输入值
同样也是将实际参数数组名(stu)传入给了实参p,q,这时形参p,q均指向了stu[0]的地址。我们用图来表示一下:
这时p,q自增是跨越了整个结构体字节长度,指向下一个结构体数组元素
输出结果:
自引用结构体
自引用结构体(self*referenced structure)表示在结构体中有结构体成员的数据类型也是此结构体的数据类型,这是重点!
我们先看看容易出错的地方:
这种声明是错误的,因为A也是一个s1类型的结构体变量,这样会形成一个无限嵌套,系统不能计算结构体的长度。
而这种是正确的,声明了一个结构体指针类型的成员,它可以指向同类型的结构体。
而当结构体成员为结构体指针的时候,可让该指针指向同类型的结构体,我们用一段代码来解释:
#include<stdio.h>
#include<stdlib.h>
#include<conio.h>
int main()
{
struct node//声明一个局部结构体
{
char *name;
int score;
struct node* next;//结构体内声明一个结构体指针
};
struct node* p, * q;//定义两个结构体变量,为指针类型
p = ( struct node* )malloc(sizeof(struct node));//为指针p分配内存,强制转换为结构体指针类型
p->name = "Axuan";
p->score = 100;
p->next = NULL;//将p指向结构体中的next指针,让next指针指向空
q = (struct node*)malloc(sizeof(struct node));
q->name = "Bxuan";
q->score = 70;
q->next = p;//将q指向结构体中的next指针,将next赋值位p的地址
printf("%s %10d\n", q->name, q->score);
printf("%s %10d", q->next->name, q->next->score);
getchar();
return 0;
}
这段代码以malloc函数,动态分配内存空间给p与q结构体变量指针,分别指定结构体项目中的值后,将p->next指向NULL,q->指向p,如图所示:
因为程序将q.next指向了p的首地址,所以q->next->name其实就是p->name
我试着解释一下:
一、q是一结构体类型指针,先用malloc动态分配一struct student大小的内存,再将此片内存交给指针q管理,q的值就是这篇内存的首地址
二、与指向结构体变量其实是意义大致相同,可以把结构体变量的首地址看做动态分配的首地址。
三、在struct student类型的结构体中,拥有一个struct student *next,student结构体类型的指针变量,在q管理的这片内存中,next被指向p
四、p也用malloc分配了一片相对应的内存,这时p的值就为这篇内存的首地址,而通过q->next->就可以调用p结构体中的元素,因为next的值等价于p的值
我们可以将上例有关链表节点的输出,以一个循环完成,看代码:
#include<stdio.h>
#include<stdlib.h>
#include<conio.h>
int main()
{
struct student
{
char* name;
int score;
struct student* next;//内嵌一个struct student类型的指针
};//定义一个局部结构体,拥有三个参数
struct student* q, * p, * r, * current;//定义四个结构体类型的指针变量
p = (struct student*)malloc(sizeof(struct student));
p->name = "Axuan";
p->score = 99;
q = (struct student*)malloc(sizeof(struct student));
q->name = "Bxuan";
q->score = 95;
r = (struct student*)malloc(sizeof(struct student));
r->name = "Cxuan";
r->score = 92;
p->next = q;
q->next = r;
r->next = NULL;
printf("我的三个同学的班级信息:\n");
current = p;
while (current != NULL)
{
printf("名字:%10s 学分:%20d\n", current->name, current->score);
current = current->next;
}
getchar();
return 0;
}
我们来简单解释一下相连关系,p所管理的内存中,成员next存储的是q所管理内存的首地址,以此类推
- current->p
- p.next->q
- q.next->r
- r.next-NULL
那么这时头指针就是current,就像一个火车一样,火车头为current,第一节车厢为p,车厢内存的就是p所管理内存的成员,而p的地址就是车厢的编号。
运行结果:
此程序while循环,将链表数据一一打印出,注意,循环的判断条件是current!=NULL
也就是当current所指向的内存中,成员next指向为空的时候停止循环,若不为空,这将成员next所存储的值赋给current
若表达式为真,则说明链表中还有数据,所以将current移到下一个节点
有了基本概念,我们来看下一段代码:
#include<stdio.h>
#include<stdlib.h>
#include<conio.h>
int main()
{
struct student
{
char* name;
int score;
struct student* next;
};//定义一个局部结构体,拥有三个成员
//成员三为一个结构体类型的指针变量
struct student st[] = {
{"Axuan",97,st + 1},
{"Bxuan", 95, st + 2},
{"Cxuan",92,st}
};//定义一个结构体类型的数组,并赋值
struct student* p = &st;//定义一个结构体指针变量p,它指向结构体数组的首地址
printf("st[0].name=%16s\n", st[0].name);
printf("(*st).name=%16s\n", (*st).name);
printf("p.name=%20s\n", p->name);
printf("st[1].next->name=%10s\n", st[1].next->name);
printf("st[1].next->score=%9d\n", st[1].next->score);
printf("++(p->name)=%15s\n", ++(p->name));
printf("++p->name=%17s\n", ++p->name);
printf("p->next->name=%13s\n", p->next->name);
printf("p->next->score=%12d\n", p->next->score);
system("pause");
return 0;
}
运行结果:
我们先用图解释一下结构体中的迭代关系
我们来剖析下这段代码中的表达式:
st[0]的name,先取到st数组中的首地址,进入首地址后取到成员name的首地址,以%s输出,结果为Axuan
(*st).name,进入st的地址,取到成员name的值,等价于st[0],结果为Axuan
p.name, 由于p指向的是st的,p内存储的也是st的首地址,那么通过p也能访问st,输出的值也为Axuan
st[1].next->name,取到st+1地址内的成员next,next指向的是r,这段语句等价于q.name,输出的值为Cxuan
st[1].next->score,等同于上句,取到的也是指针r所管理内存中成员score的值,为92
++(p->name)先结合括号内,取到p所指向st中成员name的值,为字符串Axuan的首地址,自增得到x的地址,%s输出为xuan
++p->name,由于->优先级最大,等同于上一语句,也是p指向st中name的值,这时name的值已自增为x的地址,再自增得到u的地址,则输出为uan
p->next->name,p指向st所管理内存中成员next,next存储的是st[1]的首地址,访问到st[1]后取到成员name的值,为Bxuan首字符B的地址,以字符串格式输出,为Bxuan
p->next->name,等同于上一句,访问到st[1]后,取到成员score的值为95
我们再来个扩展性:看代码
#include<stdio.h>
#include<stdlib.h>
#include<conio.h>
int main()
{
struct student
{
char* name;
int score;
struct student* next;
};//定义一个局部结构体,拥有三个成员
//成员三为一个结构体类型的指针变量
struct student st[] = {
{"Axuan",97,st + 1},
{"Bxuan", 95, st + 2},
{"Cxuan",92,st}
};//定义一个结构体类型的数组,并赋值
struct student* p[] = { st,st + 1,st + 2 };
printf("(*p)->name=%s\n", (*p)->name);
printf("(**p).name=%s\n\n", (**p).name);
printf("(*p)->score=%d\n", (*p)->score);
printf("p[0]->score=%d\n\n", p[0]->score);
printf("(*p)->next->name=%s\n", (*p)->next->name);
printf("(*p)->next->next->name=%s\n\n", (*p)->next->next->name);
printf("(++(*p)->next)->name=%s\n", (++(*p)->next)->name);
printf("(*p)->next->score=%d\n", (*p)->next->score);
system("pause");
return 0;
}
多了啥?多了个指针数组,里面存的是每一个st结构体的首地址
我们先来看看输出结果:
再看看它的剖析图
再来看看它的表达式:
(*p)->name
访问p指向的结构体st,取到成员name,字符串Auan的首地址,以%s输出为Axuan
(**p).name
同上,这是另外一种表达方式,因为p为以指针数组,需要两个*才能访问到指向的结构体内
(*p)->score
取到p中首元素为st的首地址,再取到score的值,为90
p[0]->score
同上,结果也为90
(*p)->next->name
先取到p访问的st结构体中取到next的值为st[1]的首地址,再取到name的值为Bxuan的首地址,以%s输出为Bxuan
(*p)->next->next->name
先取到p访问的st结构体中取到next的值为st[1]的首地址,再取到st[1]中next的值为st[2]的首地址,再取到st[2]中name的值,为Cxuan
(++(*p)->next)->name
此处++作用于(*p)->next,则next的值为st+1,再自增则为st+2的地址,取到st+2中的name值为Cxuan
(*p)->next->score,这时p还是指向st,但st中的next经过自增后指向st[2],取到st[2]中的score值为92
总结
到这里,结构体与指针就学习完毕了,结构体与指针的关系有:
- 结构体类型的指针变量,它可以指向同类型的结构体变量
- 内嵌结构体指针,可以实现结构体嵌套与自引用
- 结构体成员有指针类型,可以指向同类型的变量
- 指向一个结构体变量的指针,也可以指向结构体变量的任何一个成员
- 当结构体变量类型为指针时,需要使用->做指向成员运算
- 若结构体指针未指向任何一个变量,则需要为其动态分配同类型的内存
- 当结构体做函数参数时候,需要使用指针进行传址调用
具体的疑问将会在问题演练中浮出,摸透结构体与指针的关系对于数据结构链表类型是一个铺垫。