[笔记]指针与结构体

结构体(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;


}

输出结果:

img

从输出结果得知,ptr指向结构体,manager的地址(30fd30),也是此结构体第一个成员(id)的地址(&manager.id),我们用图来解释下:

img

同时,也告诉了我们如何打印出结构体中每一成员的数据。假如是一般的变量,则一运算符(.)处理,如: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;
}

运行结果:

img

由于程序便没有将结构体变量指定给结构体指针变量,所以必须用malloc函数,分配内存给结构体指针st2

img

当给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两个成员,如图:

img

需要注意的是,malloc与calloc函数均须类型转换,让它符合st2的数据类型(struct students*),这样才能正确的分配其内存

free为释放st2所指向的内存,清空它所指向的内存地址内的数据,但是这篇内存还是属于st2管理,可以实现删数据与改数据

img

在加了这段代码后,我们来看看运行结果:

img

我们可以看到,在释放内存后,内存内的数据就为空了,但我们依旧可以指针去添加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;

}

输出结果:

img

这段代码中要注意几点:

  • 程序中student结构体的成员fitstname是一个数组,由于它是指针常量,在初定义时并未载入数据,则不可重定义,就不可以将一个字符串指定给它

img

如果要指定一个字符串给它的话,必须在定义结构体变量时并给其赋值

img

  • 而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);




}

运行结果:

img

结构体的声明与定义我们用图表示出来

img

st2->name

此语句相当于(*st2).name,以%s输出结果为Axuan

*st2->name

由于->的运算符优先级高于,所以该表达式等价于(st2->name),得到的结果是字符A

*st2->name++

表示*(st2->name)++,由于*和++的运算优先级相同,而且其结合性是由右向左,所以此处的++作用于st2->name,因此,得到B字符之后,将name的指针移到r的地址,用图表示为:

img

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从指针变量改为了指针常量

img

前置自增++*(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);
}

输出结果:

img

程序将实际参数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]的地址。我们用图来表示一下:

img

这时p,q自增是跨越了整个结构体字节长度,指向下一个结构体数组元素

输出结果:

img

自引用结构体

自引用结构体(self*referenced structure)表示在结构体中有结构体成员的数据类型也是此结构体的数据类型,这是重点!

我们先看看容易出错的地方:

img

这种声明是错误的,因为A也是一个s1类型的结构体变量,这样会形成一个无限嵌套,系统不能计算结构体的长度。

img

而这种是正确的,声明了一个结构体指针类型的成员,它可以指向同类型的结构体。

而当结构体成员为结构体指针的时候,可让该指针指向同类型的结构体,我们用一段代码来解释:

#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,如图所示:

img

因为程序将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的地址就是车厢的编号。

运行结果:

img

此程序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;

}

运行结果:

img

我们先用图解释一下结构体中的迭代关系

img

我们来剖析下这段代码中的表达式:

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结构体的首地址

我们先来看看输出结果:

img

再看看它的剖析图

img

再来看看它的表达式:

(*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

总结

到这里,结构体与指针就学习完毕了,结构体与指针的关系有:

  • 结构体类型的指针变量,它可以指向同类型的结构体变量
  • 内嵌结构体指针,可以实现结构体嵌套与自引用
  • 结构体成员有指针类型,可以指向同类型的变量
  • 指向一个结构体变量的指针,也可以指向结构体变量的任何一个成员
  • 当结构体变量类型为指针时,需要使用->做指向成员运算
  • 若结构体指针未指向任何一个变量,则需要为其动态分配同类型的内存
  • 当结构体做函数参数时候,需要使用指针进行传址调用

具体的疑问将会在问题演练中浮出,摸透结构体与指针的关系对于数据结构链表类型是一个铺垫。

本文链接:

https://nullcode.fun/111.html
1 + 7 =
快来做第一个评论的人吧~