结构体

  无情的搬砖机器= =

一、关于c语言的结构体

  首先我们为什么要用到结构体,我们都已经学了很多intchar…等类型,还学到了同类型元素构成的数组;以及取上述类型的指针,在一些小应用可以灵活使用。然而,在我们实际应用中,每一种变量进行一次声明,再结合起来显然是不太实际的,类如一位学生的信息管理,他可能有,姓名(char),学号(int)成绩(float)等多种数据。如果把这些数据分别单独定义,就会特别松散、复杂,难以规划,因此我们需要把一些相关的变量组合起来,以一个整体形式对对象进行描述,这就是结构体的好处。

二、结构体小知识

  1. 只有结构体变量才分配地址,而结构体的声明是不分配空间的;
  2. 结构体声明,包括了结构体中各成员的声明,因此也不分配空间;
  3. c语言中的结构体不能直接进行强制转换,只有结构体指针才能进行强制转换
  4. 相同类型的成员是可以声明在同一类型下的
    1
    2
    3
    4
    5
    6
    7
    struct Student
    {
    int number,age;//int型学号和年龄
    char name[20],sex;//char类型姓名和性别
    float score;
    };
    //最后的分号不要忘了

总结:声明结构体类型仅仅是声明了一个类型,系统并不为之分配内存,就如同系统不会为类型 int 分配内存一样。只有当使用这个类型定义了变量时,系统才会为变量分配内存。所以在声明结构体类型的时候,不可以对里面的变量进行初始化。

结构体类型的变量的本质:结构体类型的变量,本质上是一个变形的数组

  • 不同点:结构体不要求元素类型一样,要用的时候,不是以数组下标,而是特定指向(.->)该成员来 处理(例如赋值);
  • 相同点:
    • 结构体和数组在定义的时候不进行初始化赋初值,则后面就不能全部赋初值,需逐个赋值;
    • 进行 数组、结构体变量 初始化的时候,不能跳过前面成员变量,而直接给后面成员赋初值,但是可以只赋初值前面几个;
    • 进行 数组、结构体变量 初始化的时候,对于未初始化(后半段)的数据:如果是数值型,则会自动赋值为 0 ;如果是字符型,会自动赋初值为 NULL ,即\0 ;即不足的元素补以默认值;
    • 函数的传入参数(形参)是结构体、数组,均采用指针传递的方式
      1
      2
      3
      4
      5
      6
      7
      8
      9
      typedef struct 
      {
      char name[20];
      char sex;
      int number;
      }Student;
      Student stu1={"zhaozixuan",'M'};

      int str[10]={1};//这里只是把str的第一个元素赋值为1,其他元素默认为0

  这里要强调的一点是, “变量赋值” 和 “变量初始化(赋初值)”不是一回事!给(全局)变量初始化,定义时后跟等号,等号后面是初始化值。赋初值只会在定义的时候进行赋初始化值,其余地方都是赋值

赋初值 和 赋值 的区别

  1. 赋值运算,函数体外是不允许的;而赋初值没有该要求,可在函数体外定义赋初值。
  2. 赋初值,可以初始化所有成员;赋值,只能对逐个成员进行赋值,无法一次性对全体成员进行赋值。(举例:数组、结构体)

三、结构体变量的定义和引用

3.1 结构体类型的变量

  在编译时,结构体的声明并不分配存储空间;声明结构体类型仅仅是声明了一个类型,系统并不为之分配内存,就如同系统不会为类型 int 分配内存一样;只有当使用这个类型定义了变量时,系统才会为变量分配内存。所以在声明结构体类型的时候,不可以对里面的变量进行初始化。

1
2
3
4
5
6
7
8
9
 struct Book
{
char title[20];//一个字符串表示的titile 题目
char author[20];//一个字符串表示的author作者
float value;//价格表示
};//这里只是声明 结构体类型

struct Book book1,book2;//结构体变量的定义 分配空间
book1.value;//引用结构体变量

  定义结构体变量以后,系统就会为其分配内存单元,比如book1和book2在内存中占44个字节(20+20+4)具体的长度你可以在你的编译器中使用sizeof关键字分别求出来。

3.2 结构体空洞

  用sizeof关键字求结构体长度时,返回的最大基本类型所占字节的整数倍;比方说我们上面求得的为44 为 float(4个字节)的整数倍,但是我们把title修改为title[22]; 这时正常长度为46 ,但是你会发现实际求得的为48(4的整数倍)。

这就涉及到结构体的存储:

  1. 结构体整体空间是占用空间最大的成员(的类型)所占字节数的整数倍;
  2. 结构体的每个成员相对结构体首地址的偏移量(offset)都是最大基本类型成员字节大小的整数倍(一般是int,4字节),如果不是编译器会自动补齐;

  在结构体分配空间时,如果结构体中出现4个字节(32位)及以上的变量时,给每个变量分配空间时都是按字对齐分配的(就是按4个字节,4个字节来分配);如果结构体中没有出现4个字节以上变量,则按半字对齐(按 2个字节,2个字节。。来分配)。
  结构体中的每一个模块在内存中并不是禁止排列存储的,而是上下对齐存储(字对齐或双字对齐等)。这种现象叫做内存对齐。这样做的目的是为了是处理器能够更快速的进行寻址,执行速度更快。以空间换取时间。
  看来鱼与熊掌还是不能兼得啊。既然是上下对齐的,那么并不是每个模块都能准确的填满一行的内存空间。那么没有被填满的内存空间就造成了空洞。
  这样的话,在查看结构体所占的空间时,就不能把每个模块所分别占的内存空间简单地(手动计算)相加。因为他们中间存在空洞。

关于偏移量,简单介绍下:

结构体偏移量指的是结构体变量中成员的地址和结构体变量首地址的差。即偏移字节数,结构体大小等于最后一个成员的偏移量加上他的大小;第一个成员的偏移量为0

1
2
3
4
5
6
struct S1
{
char a;
int b;
double c;
};

这里 char a 偏移量为 1 之后为 int b 因为偏移量 1 不为int(4)的整数倍,所以会自动补齐,而在 double c 时,偏移量为 8 是int(4)的整数倍,所以不用自动补齐,最后求得结构体得大小为 16。

四、结构体变量的初始化

  结构体的初始化有很多需要注意的地方,这里我们说明下,首先是几种初始化的方法。

Ps: 在对结构体变量初始化时,要对结构体成员一一赋值,不能跳过前面成员变量,而直接给后面成员赋初值,但是可以只赋值前面几个,对于后面未赋值的变量,如果是数值型,则会自动赋值为0;对于字符型,会自动赋初值为 NULL ,即 \0

4.1 定义时直接赋值(变量初始化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Student
{
char name[20];
char sex;
int number;
}stu1={"zhaozixuan",'M',12345};
//或者
struct Student
{
char name[20];
char sex;
int number;
};
struct Student stu1={"zhaozixuan",'M',12345};

注意: 字符为 ' ' ,字符串为 " "

4.2 定义结构体之后逐个赋值

1
2
3
4
5
6
7
//赋值操作均在函数内操作

stu1.name="王伟"
stu1.sex='M';
stu1.number=12305;
//也可用strcpy函数进行赋值
strcpy(stu1.name,"王伟");

4.3 定义之后任意赋值

1
2
3
4
5
struct Student stu1={
.name="Wang",
.number=12345,
.sex='W',
};//可以对任意变量赋值

  这样写的好处时不用按照顺序来进行初始化,而且可以对你想要赋值的变量直接进行赋值,而不想赋值的变量可以不用赋值。
  需要注意的是,如果在定义结构体变量的时候没有初始化,那么后面就不能全部一起初始化了。(数组性质)

4.4 typedef 说明结构体类型

  typedef 为一种数据类型定义一个新名字。这里的数据类型包括内部数据类型(int,char等)和自定义的数据类型(struct等)。

1
2
3
4
5
typedef struct tagMyStruct  //这里也可以不写tagMyStruct
{
int iNum;
long lLength;
}MyStruct;

  上面的tagMyStruct是标识符,MyStruct是变量类型(相当于 int , char 等)。

这语句实际上完成两个操作:

  1. 定义一个新的结构类型
1
2
3
4
5
struct tagMyStruct
{  
int iNum;
long lLength;
};

分析:tagMyStruct 称为“tag”,即“标签”,实际上是一个临时名字,不论是否有 typedef struct 这个关键字 和 tagMyStruct 一起,构成了这个结构类型,这个结构都存在。我们可以用 tagMyStruct varName 来定义变量;但要注意,使用 tagMyStruct varName 来定义变量是不对的,因为 struct tagMyStruct 合在一起才能表示一个结构类型。

  1. typedef为这个新的结构起了一个名字,叫MyStruct
1
typedef struct tagMyStruct MyStruct;

  因此,MyStruct实际上相当于 struct tagMyStruct ,我们可以使用 MyStruct varName 来定义变量。

1
2
3
4
5
typedef struct tagMyStruct
{
int iNum;
long lLength;
}MyStruct;

在C中,这个申明后申请结构变量的方法有两种:
(1) struct tagMyStruct 变量名(typedef声明时可省略tagMyStruct,省略后则无法使用该方法定义结构变量)
(2) MyStruct 变量名(一般采用该方法)

五、结构体变量的引用(结构体成员)

  1. 结构体类型 声明定义的是 普通变量,普通变量 访问成员时就用 .
  2. 结构体类型 声明定义的是 指针 ,指针 访问成员时就用 ->

Ps: 若使用指针对结构体成员进行访问,格式为:指针->成员名 等价于 (*指针).成员名

但是有几点需要注意:
(1) .是运算符,在所有运算符优先级中最高
(2)如果结构体的成员本身是一个结构体,则需要继续用.运算符,直到最低一级的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct 
{ char name[20];
char sex;
int number;
struct Date
{
int year;
int month;
int day;
}birthday;
}Student;

Student Stu1;//定义结构体变量

printf("%d",stu1.birthday);//这样子是错误的,因为birthday也是一个结构体变量
scanf("%d",&stu1.birthday.month);//正确

六、结构体数组及其初始化(重点)

  这里我们简单说下,具有相同类型的结构体变量组成数组就是结构体数组。反而言之,是指数组中的每个元素都是一个结构体。在实际应用中,结构体数组常被用来表示一个拥有相同数据结构的群体,比如一个班的学生、一个车间的职工等。

1
2
3
4
5
6
7
8
9
10
11
12
struct Student
{
char name[20];
char sex;
int number;
}stu1[5]={
{"zhaozixuan",'M',12345},
{"houxiaohong",'M',12306},
{"qxiaoxin",'W',12546},
{"wangwei",'M',14679},
{"yulongjiao",'W',17857}
};

当对结构体数组中全部元素赋值时,也可不给出数组长度,例如:

1
2
3
4
5
6
7
8
9
10
11
12
struct Student
{
char *name;//指针类型指向字符串
char sex;
int number;
}stu1[]={
{"zhaozixuan",'M',12345},
{"houxiaohong",'M',12306},
{"qxiaoxin",'W',12546},
{"wangwei",'M',14679},
{"yulongjiao",'W',17857}
};

Ps:在上面的Tip提到,结构体是一个变形的数组;结构体数组,其实就是变形的二元数组;数组的性质同样也是存在:结构体数组要在定义时就直接初始化赋值,不然后面再全部赋值是错误的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//错误的示例
struct Student stu1
stu1[3]={
{"zhaozixuan",'M',12345},
{"houxiaohong",'M',12306},
{"qxiaoxin",'W',12546}
};

//正确的示例
struct Student stu1[]={
{"zhaozixuan",'M',12345},
{"houxiaohong",'M',12306},
{"qxiaoxin",'W',12546}
};

数组初始化

1
2
3
4
5
6
7
8
9
10
11
//错误示范
char str[20];
str="I love you"; //这样会修改数组的地址,原因如下

//数组初始化后,地址分配是固定的,数组名是(符号)地址常量;常量没有(可写的)内存空间存你要赋的值
//因此数组名不能作为左值
//所以我们可以把str[i]当左值,而无法把str当左值。

//正确示范
char *str;
str="I love you";

  在第一条语句中 str 就已经被定义成数组,而在C99标准中不允许将字符串(实际上是一个指针变量)赋值给数组(左值),所以如果我们直接赋值是错误的。

注意区分声明时的初始化和普通的赋值语句!!!


赋值运算

  赋值运算,分为左值和右值。

  1. 左值:可以出现在赋值语句的左边或右边,它不光有值,还有一个存储地址;
  2. 右值:只能出现在赋值语句的右边,认为它只有一个值的大小,没有存储地址,只关心它的值(字符串常量则是存在字符常量区,只可读不可写)。

数组名不可能作为左值!数组名不是指针!

  那么数组名应该如何理解呢?用来存放数组的区域是一块在栈中静态分配的内存(非static),而数组名是这块内存的代表,它被定义为这块内存的首地址。这就说明了数组名是一个地址,而且,还是一个不可修改的常量,完整地说,就是一个地址常量。

  数组名跟枚举常量类似,都属于符号常量。数组名这个符号,就代表了那块内存的首地址。注意了!不是数组名这个符号的值是那块内存的首地址,而是数组名这个符号本身就代表了首地址这个地址值,它就是这个地址,这就是数组名属于符号常量的意义所在。

  由于数组名是一种符号常量,因此它是一个右值,而指针,作为变量,却是一个左值,一个右值永远都不会是左值,那么,数组名永远都不会是指针!

这里提供数组赋(字符串)值的3种方法:

  1. 定义数组时直接定义

    1
    char str[20] = "I love you";
  2. strcpy进行复制

    1
    2
    char str[20];
    strcpy(str,“I love you”);
  3. memset进行复制

    1
    2
    3
    //memset</code>语法介绍
    void *memset(void *s,int c,size_t n)
    //作用:将已开辟内存空间s的首n个字节的值设为值c。

    3.1 如果是字符类型数组的话,memset可以随便用,如下所示:

    1
    2
    char str[20];
    memset(str,'a',20);

    3.2 但是对于其他类型的数组,一般只用来清0或者填-1,如果是填充其他数据就会出错,如下所示:

    1
    2
    int str[10];
    memset(str,1,sizeof(str));//这样是错误的

    3.3 错误分析

    - <code>memset</code>在进行赋值时,是按字节为单位来进行赋值的,每次填充的数据长度为一个字节;
    - 对于其他类型的变量,比如int,占4个字节 所以sizeof(str)=40;
    - 用memset赋值时,将会对指向str地址的前40个字节进行赋值0x01(00000001) 的操作;**把0x00000000赋值4次0x01操作变为<code>0x01010101</code>**(错误主要原因);
    - 相当于给 “(所有)10个int” 进行了赋值<code>0x01010101</code>的操作 对应十进制的16843009,所以会出很大的错误。
  4. 用指针(注意内存分配方式)

    1
    2
    char *str;
    str = "I love you";

  这两句话的本质是, 在内存中开辟一段内存空间(字符常量区),把"I love you"放进这段内存空间,然后把这段内存空间的地址交给str,由于str是变量(栈 或 堆),所以给它赋值(地址,指向的内容)是合法的。


memset用法总结:如果是清零一个数组用memset还是很方便的;简单赋字符串值,用strcmp函数(或memset)就行。

七、结构体与指针

  指针指向的是变量所占内存的首地址,在结构体中,指针指向的是结构体变量的起始地址,当然也可指向结构体变量的元素。

7.1 指向结构体变量的指针

1
2
3
4
5
6
7
8
struct Student
{
char cName[20];
int number;
char csex;
}student1;
struct Student *p;
p=&student1;

简单来说以下三种形式是等价的

1
2
3
p->cName        //可以进行正常的运算
(*p).cName //这里的括号不能少,.运算符优先级最高
student1.cName

  这里需要注意的是,结构体访问成员方式和数组访问成员方式有些差别。

即使*p访问到了结构对象的第一个成员变量a,也不能保证*(p+1)就一定能访问到结构成员b。因为成员a和成员b之间可能会有若干填充字节,说不定*(pstr+1)就正好访问到了这些填充字节呢。这也证明了指针的灵活性。要是你的目的就是想看看各个结构成员之间到底有没有填充字节,嘿,这倒是个不错的方法。 (用sizeof作为偏移量访问下一个成员要注意一下结构体空洞吖!)

7.2 指向结构体数组的指针

  在我们想要用指针访问结构体数组的第n个数据时可以用

1
2
3
4
5
6
7
8
9
10
struct Student
{
char cName[20];
int number;
char csex;
};
struct Student stu1[5];
struct Student*p;
p=stu[n];
(++p).number//是指向了结构体数组下一个元素的地址

7.3 结构体成员是指针类型变量

1
2
3
4
5
6
struct Student
{
char* Name;//这样防止名字长短不一造成空间的浪费
int number;
char csex;
}student1;

在使用时可以很好地防止内存被浪费,但是注意在引用时一定要给指针变量分配地址,如果你不分配地址,结果可能是对的(野指针),但是Name会被分配到任意的一地址,指针为字符串分配任何内存存储空间具有不确定性,这样就存在潜在的危险。

1
2
3
4
5
6
7
8
struct Student
{
char* Name;
int number;
char csex;
}stu,*stu;

stu.name=(char*)malloc(sizeof(char));//内存初始化

所对应的指针类型结构体成员要相应初始化分配内存

1
2
3
4
5
6
7
8
struct Student
{
char* Name;
int number;
char csex;
}stu,*stu;
stu = (struct student*)malloc(sizeof(struct student));./*结构体指针初始化*/
stu->name = (char*)malloc(sizeof(char));/*结构体指针的成员指针同样需要初始化*/

  实际上,结构体指针、结构体成员指针就是指针,没有区别;只不过结构体成员指针容易被忽略初始化。而指针的初始化是很重要的,详情可以看 野指针一章的内容。

7.4 结构体嵌套的问题

结构体的自引用(self reference),就是在结构体内部,包含指向自身类型结构体的指针。
结构体的相互引用(mutual reference),就是说在多个结构体中,都包含指向其他结构体的指针。

7.4.1 自引用 结构体

不使用 typedef

  1. 错误的方式:
    1
    2
    3
    4
    struct tag_1{
    struct tag_1 A;
    int value;
    };

  这种声明是错误的,因为这种声明实际上是一个无限循环,成员A是一个结构体,A的内部还会有成员是结构体,依次下去,类似于永无出口的递归调用。在分配内存的时候,由于无限嵌套,也无法确定这个结构体的长度,所以这种方式是非法的。

  1. 正确的方式:(使用指针)
    1
    2
    3
    4
    struct tag_1{
    struct tag_1 *A;
    int value;
    };

  由于指针的长度是确定的(在32位机器上指针长度为4),所以编译器能够确定该结构体的长度

Ps:这个指针看似指向自身,其实不是,而是指向同一类型的不同结构。链表和树的数据结构就都使用到此技巧。自身的结构体指针指向下一节点或者下一子树的地址。


使用 typedef

  1. 错误的方式:
    1
    2
    3
    4
    typedef struct {
    int value;
    NODE *link;
    }NODE;

  这里的目的是使用typedef为结构体创建一个别名NODE。但是这里是错误的,因为此时还没定义完类型名,而在结构体内部引用了结构类型名,是非法的。

  1. 正确的方式:(使用不完全声明)有三种,差别不大,使用哪种都可以
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    typedef struct tag_1{
    int value;
    struct tag_1 *link;
    } NODE;

    //虽然 C 语言编译器完全支持这种做法,但不推荐使用以下的第2种
    typedef struct tag_2 NODE;
    struct tag_2{
    int value;
    NODE *link;
    };

    //建议使用以下的第3种
    struct tag_3{
    int value;
    struct tag_3 *link;
    };
    typedef struct tag_3 NODE;

7.4.2 相互引用 结构体

  1. 错误的方式:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    typedef struct tag_a{
    int value;
    B *bp;
    }A;

    typedef struct tag_b{
    int value;
    A *ap;
    }B;

  错误的原因和上面一样,这里类型B在定义之前 就被使用。

  1. 正确的方式:(使用不完全声明)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    //推荐使用第一种
    struct tag_a{
    struct tag_b *bp;
    int value;
    };
    struct tag_b{
    struct tag_a *ap;
    int value;
    };
    typedef struct tag_a A;
    typedef struct tag_b B;



    struct tag_a;
    struct tag_b;
    typedef struct tag_a A;
    typedef struct tag_b B;
    struct tag_a{
    struct tag_b *bp;
    int value;
    };
    struct tag_b{
    struct tag_a *ap;
    int value;
    };

7.5 结构体作为函数参数(形参)

将结构体传递给函数的方式有如下3种:

  1. 用结构体的单个成员作为函数参数,向函数传递结构体的单个成员(属于传值调用,不会影响相应的实参结构体的值)
  2. 用结构体变量做函数参数,向函数传递结构体完整结构(属于传值调用,不会影响相应的实参结构体的值)
  3. 用结构体指针或结构体数组作函数参数属于模拟按引用调用,会影响相应的实参结构体的值,向函数传递结构体地址,因为仅复制结构体首地址一个值给被调函数,相对于第二种方式,这种传递效率更高

补充:传值调用与模拟按引用调用(参数传递)

按值调用:将程序将函数调用语句中的实参的一份副本传给函数的形参
模拟按引用调用:指针作为函数的参数,虽然实际上也是传值给被调用函数,但是传给被调用函数的这个值不是变量的值,而是变量的地址,通过向被调用函数传递某个变量的地址值可以在被调函数中改变主调函数中这个变量的值,相当于模拟C++中的按引用调用因此称为模拟按引用调用

  使用结构体变量作为函数参数的时候,是采取传值调用,将结构体所占内存单元的内容全部传递给形参,并且形参必须也要是同类型的结构体变量,在使用时,会自动创建一个结构体变量作为原变量的副本,并且也需要占内存,效率较低。且无法修改实际的结构体变量中成员的值。

  如果用指针作为实参,传递给函数的形参,这时候传递的是结构体变量的地址,形参所指向的地址就是结构体变量的地址,这时候进行修改的话是可以修改的(这正是指针的精华所在)。


7.6 结构体的一些小技巧

7.6.1 互换结构体

在这里我们再提供几种互换两个结构体的方法:

  1. 结构体指针互换地址
  2. 直接互换值(同类型结构体)
  3. 比较笨的方法:用for循环互换
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    typedef struct Student
    {
    char cName[20];
    int number;
    char csex;
    }Student;


    Student student1={"Wang",12345,'W'};
    Student student2={"Zhao",54321,'M'};
    Student *stu1=&student1;
    Student *stu2=&student2;

    //法1
    Student *stu3;
    stu3=stu1;
    stu1=stu2;
    stu2=stu3;

    //法2
    struct stu student3;
    student3=student1;
    student1=student2;
    student2=student3;

7.6.2 meset的妙用

  最后提下memset清空结构体:

1
2
3
4
5
6
struct Student
{
char cName[20];
int number;
char csex;
}stu1;

一般情况下,清空str的方法:

1
2
3
  str.cName[0]='\0';
  str.csex='0';
  str.number=0;

但是我们用memset就非常方便:

1
memset(&str,0,sizeof(struct Student));

如果是数组,就是:

1
2
struct Student stu[10];
memset(stu,0,sizeof(struct Student)*10);

八、拓展(待写)

二叉树遍历算法
二叉树的二叉链表类型定义如下:

1
2
3
4
typedef struct btnode{
datatype data;
struct btnode *lchild,*rchild;
};

九、友情链接

多亏主要如下几位网友的资料
结构体嵌套中的问题
c语言结构体学习整理(结构体初始化,结构体指针)
结构体中定义函数指针
结构体(结构体嵌套、结构体指针、结构体参数传递)

-------------本文结束感谢您的阅读-------------