深入浅出C语言:(十一)C 语言结构体

目录

一、什么是结构体?

1、结构体的简述

2、结构体变量

3、成员的获取和赋值

二、结构体数组

三、结构体指针(指向结构体的指针)

1、结构体指针的基本用法

2、获取结构体成员

3、结构体指针作为函数参数

四、C 语言枚举类型(enum 关键字)

五、C 语言共用体(union 关键字)

1、共用体也是一种自定义类型,可以通过它来创建变量

2、共用体的应用

六、C 语言位域(位段)

1、位域的基本用法

2、位域的存储

3、无名位域

七、C 语言 typedef 的用法

1、typedef给int、char等类型定义别名

2、typedef给数组类型定义别名

3、为结构体类型定义别名

4、为指针类型定义别名

5、函数指针类型定义别名

6、指针示例

7、typedef 和 #define 的区别


       C 语言结构体(Struct)从本质上讲是一种自定义的数据类型,只不过这种数据类型比较复杂,是由 int、 char、 float等基本类型组成的。

一、什么是结构体?

1、结构体的简述

在 C 语言中,可以使用结构体Struct) 来存放一组不同类型的数据。结构体的定义形式为:

struct 结构体名
{
结构体所包含的变量或数组
};

结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员。

struct stu
{
char *name;   //姓名
int num;      //学号
int age;      //年龄
char group;   //所在学习小组
float score;  //成绩
};

2、结构体变量

既然结构体是一种数据类型,那么就可以用它来定义变量。例如:

struct stu stu1, stu2;

定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字 struct 不能少。
另一种方式:

struct stu
{
char *name;   姓名
int num;      学号
int age;      年龄
char group;   所在学习小组
float score;  成绩
} stu1, stu2;


将变量放在结构体定义的最后即可。

如果只需要 stu1、 stu2 两个变量,后面不需要使用结构体名定义其他变量,那么在定义时也可以不给出结构体名,如下所示:

struct{   没有写 stu
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2;

3、成员的获取和赋值

结构体使用点号 . 获取单个成员。获取结构体成员的一般格式为:结构体变量名.成员名;
通过这种方式可以获取成员的值,也可以给成员赋值
 

#include <stdio.h>
int main()
{
	
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1;

//给结构体成员赋值
stu1.name = "Tom";
stu1.num = 12;
stu1.age = 18;
stu1.group = 'A';
stu1.score = 136.5;
//读取结构体成员的值
printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f! \n", stu1.name, stu1.num, stu1.age,stu1.group, stu1.score);

return 0;
}

除了可以对成员进行逐一赋值,也可以在定义时整体赋值,例如:

struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1, stu2 = { "Tom", 12, 18, 'A', 136.5 }

不过整体赋值仅限于定义结构体变量的时候,在使用过程中只能对成员逐一赋值,这和数组的赋值非常类似。
       需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。

二、结构体数组

       所谓结构体数组,是指数组中的每个元素都是一个结构体。在实际应用中, C 语言结构体数组常被用来表示一个拥有相同数据结构的群体,比如一个班的学生、一个车间的职工等。

在 C 语言中,定义结构体数组和定义结构体变量的方式类似,请看下面的例子:

struct stu{
char *name;   姓名
int num;      学号
int age;      年龄
char group;   所在小组
float score;  成绩
}class[5];

结构体数组在定义的同时也可以初始化,例如:

struct stu{
char *name;   姓名
int num;      学号
int age;      年龄
char group;   所在小组
float score;  成绩
}class[5] = {
{"Li ping", 5, 18, 'C', 145.0},
{"Zhang ping", 4, 19, 'A', 130.5},
{"He fang", 1, 18, 'A', 148.5},
{"Cheng ling", 2, 17, 'F', 139.0},
{"Wang ming", 3, 17, 'B', 144.5}
};

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

struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
}class[] = {
{"Li ping", 5, 18, 'C', 145.0},
{"Zhang ping", 4, 19, 'A', 130.5},
{"He fang", 1, 18, 'A', 148.5},
{"Cheng ling", 2, 17, 'F', 139.0},
{"Wang ming", 3, 17, 'B', 144.5}
};

结构体数组的使用也很简单:

#include <stdio.h>
 
struct Books
{
   char  *title;
   char  *author;
   char  *subject;
   int   book_id;
} book[2]= {
            {"C 语言", "RUNOOB", "编程语言", 123456},
            {"C 言",   "BBBBBB", "语言编程", 456123}
			};
int main(void)
{
    book[1].author = "sumjess";
	book[1].book_id=666;
	book[0].subject="指针";
	book[0].title="C语言"; 

    printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book[0].title, book[1].author, book[0].subject, book[1].book_id);
return 0;
}

三、结构体指针(指向结构体的指针)

1、结构体指针的基本用法

当一个指针变量指向结构体时,我们就称它为结构体指针。 C 语言结构体指针的定义形式一般为:

struct 结构体名 *变量名;
//结构体
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 };
//结构体指针
struct stu *pstu = &stu1;

也可以在定义结构体的同时定义结构体指针

struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;

注意,结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&,所以给 pstu 赋值只能写作:

struct stu *pstu = &stu1;

而不能写作:

struct stu *pstu = stu1;

结构体和结构体变量是两个不同的概念:结构体是一种数据类型,是一种创建变量的模板,编译器不会为它分配内存空间,就像 int、 float、 char 这些关键字本身不占用内存一样结构体变量才包含实实在在的数据,才需要内存来存储下面的写法是错误的,不可能去取一个结构体名的地址,也不能将它赋值给其他变量:

  • struct stu *pstu = &stu;
  • struct stu *pstu = stu;
     

2、获取结构体成员

通过结构体指针可以获取结构体成员,一般形式为:

(*pointer).memberName

或

pointer->memberName

->可以通过结构体指针直接取得结构体成员;这也是->在 C 语言中的唯一用途。

#include <stdio.h>

int main()
{
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;//*pstu = &stu1可以换成 *pstu,不写 = &stu1

//读取结构体成员的值
printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f! \n", (*pstu).name, (*pstu).num,(*pstu).age, (*pstu).group, (*pstu).score);
printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f! \n", pstu->name, pstu->num, pstu->age,pstu->group, pstu->score);

return 0;

}

运行结果:
Tom 的学号是 12,年龄是 18,在 A 组,今年的成绩是 136.5!
Tom 的学号是 12,年龄是 18,在 A 组,今年的成绩是 136.5!

3、结构体指针作为函数参数

        结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。

#include <stdio.h>

struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
}stus[] = {
{"Li ping", 5, 18, 'C', 145.0},
{"Zhang ping", 4, 19, 'A', 130.5},
{"He fang", 1, 18, 'A', 148.5},
{"Cheng ling", 2, 17, 'F', 139.0},
{"Wang ming", 3, 17, 'B', 144.5}
};

void average(struct stu *ps, int len);

int main()
{
int len = sizeof(stus) / sizeof(struct stu);
average(stus, len);
return 0;
}

void average(struct stu *ps, int len)
{
int i, num_140 = 0;
float average, sum = 0;

for(i=0; i<len; i++)
{
sum += (ps + i) -> score;   //ps为数组的地址
if((ps + i)->score < 140) num_140++;
}

printf("sum=%.2f\naverage=%.2f\nnum_140=%d\n", sum, sum/5, num_140);
}

四、C 语言枚举类型(enum 关键字)

C 语言提供了一种枚举(Enum)类型,能够列出所有可能的取值,并给它们取一个名字。
枚举类型的定义形式为:

enum typeName{ valueName1, valueName2, valueName3, ...... };

例如,列出一个星期有几天:

enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun };

      可以看到,我们仅仅给出了名字,却没有给出名字对应的值,这是因为枚举值默认从 0 开始,往后逐个加 1(递增);也就是说, week 中的 Mon、 Tues ...... Sun 对应的值分别为 0、 1 ...... 6。
我们也可以给每个名字都指定一个值

enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };

更为简单的方法是只给第一个名字指定值:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };

枚举是一种类型,通过它可以定义枚举变量

enum week a, b, c;

也可以在定义枚举类型的同时定义变量

enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a, b, c;
有了枚举变量,就可以把列表中的值赋给它:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
enum week a = Mon, b = Wed, c = Sat;

或者:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a = Mon, b = Wed, c = Sat;

【示例】判断用户输入的是星期几。

#include <stdio.h>

int main()
{
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day;

scanf("%d", &day);

switch(day)
{
case Mon: puts("Monday"); break;
case Tues: puts("Tuesday"); break;
case Wed: puts("Wednesday"); break;
case Thurs: puts("Thursday"); break;
case Fri: puts("Friday"); break;
case Sat: puts("Saturday"); break;
case Sun: puts("Sunday"); break;
default: puts("Error!");
}

return 0;
}

需要注意的两点是:

  1. 枚举列表中的 Mon、 Tues、 Wed 这些标识符的作用范围是全局的(严格来说是 main() 函数内部),不能再定义与它们名字相同的变量。
  2. Mon、 Tues、 Wed 等都是常量不能对它们赋值,只能将它们的值赋给其他的变量。枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。
     

五、C 语言共用体(union 关键字)

       通过前面的讲解,我们知道结构体(Struct)是一种构造类型或复杂类型,它可以包含多个类型不同的成员。在 C 语言中,还有另外一种和结构体非常类似的语法,叫做共用体(Union) ,它的定义格式为:

union 共用体名{
成员列表
};

       结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员

       结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉

1、共用体也是一种自定义类型,可以通过它来创建变量

union data{
int n;
char ch;
double f;
};
union data a, b, c;

上面是先定义共用体,再创建变量,也可以在定义共用体的同时创建变量

union data{
int n;
char ch;
double f;
} a, b, c;

如果不再定义新的变量,也可以将共用体的名字省略:

union{
int n;
char ch;
double f;
} a, b, c;

        共用体 data 中,成员 f 占用的内存最多,为 8 个字节,所以 data 类型的变量(也就是 a、 b、 c)也占用 8 个字节的内存,请看下面的演示:
 

#include <stdio.h>

union data
{
int n;
char ch;
short m;
};

int main()
{
union data a;
printf("%d, %d\n", sizeof(a), sizeof(union data) );
a.n = 0x40;
printf("%X, %c, %hX\n", a.n, a.ch, a.m);
a.ch = '9';
printf("%X, %c, %hX\n", a.n, a.ch, a.m);
a.m = 0x2059;
printf("%X, %c, %hX\n", a.n, a.ch, a.m);
a.n = 0x3E25AD54;
printf("%X, %c, %hX\n", a.n, a.ch, a.m);

return 0;
}

运行结果:
4,  4
40, @, 40
39, 9, 39
2059, Y, 2059
3E25AD54, T, AD54

这段代码不但验证了共用体的长度,还说明共用体成员之间会相互影响,修改一个成员的值会影响其他成员。

2、共用体的应用

        共用体在一般的编程中应用较少,在单片机中应用较多。
 

六、C 语言位域(位段)

1、位域的基本用法

       有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。例如开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。正是基于这种考虑, C 语言又提供了一种叫做位域的数据结构。
在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域。 请看下面的例子:

struct bs
{
unsigned m;
unsigned n: 4;
unsigned char ch: 6;
};

       : 后面的数字用来限定成员变量占用的位数。成员 m 没有限制,根据数据类型即可推算出它占用 4 个字节(Byte)的内存。成员 n、 ch 被 后面的数字限制,不能再根据数据类型计算长度,它们分别占用 4、 6 位(Bit)

#include <stdio.h>

int main()
{
struct bs{
unsigned m;
unsigned n: 4;
unsigned char ch: 6;
} a = { 0xad, 0xE, '$'};

//第一次输出
printf("%#x, %#x, %c\n", a.m, a.n, a.ch);

//更改值后再次输出
a.m = 0xb8901c;
a.n = 0x2d;
a.ch = 'z';
printf("%#x, %#x, %c\n", a.m, a.n, a.ch);

return 0;
}

运行结果:
0xad, 0xe, $
0xb8901c, 0xd, :


第一次输出时, n、 ch 的值分别是 0xE、 0x24('$' 对应的 ASCII 码为 0x24),换算成二进制是1110、 10 0100,都没有超出限定的位数,能够正常输出。
第二次输出时, n、 ch 的值变为 0x2d、 0x7a('z' 对应的 ASCII 码为 0x7a),换算成二进制分别是10 1101、 1111010,都超出了限定的位数。超出部分被直接截去,剩下 1101、 11 1010,换算成十六进制为0xd、 0x3a(0x3a 对应的字符是 :)

       C 语言标准规定,位域的宽度不能超过它所依附的数据类型的长度。通俗地讲,成员变量都是有类型的,这个类型限制了成员变量的最大长度, : 后面的数字不能超过这个长度
       C 语言标准还规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、 signed int 和unsigned int(int 默认就是 signed int);到了 C99, _Bool 也被支持了。

2、位域的存储

位域的具体存储规则如下:

  1. 当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。
    #include <stdio.h>
    
    int main()
    {
    struct bs{
    unsigned m: 6;
    unsigned n: 12;
    unsigned p: 4;
    };
    
    printf("%d\n", sizeof(struct bs));
    
    return 0;
    }
    
    运行结果:
    4
    
    
    
    m、 n、 p 的类型都是 unsigned int, sizeof 的结果为 4 个字节(Byte),也即 32 个位(Bit)。 m、 n、 p 的位宽之和为 6+12+4 = 22,小于 32,所以它们会挨着存储,中间没有缝隙。
    
    如果将成员 m 的位宽改为 22,那么输出结果将会是 8,因为 22+12 = 34,大于 32, n 会从新的位置开始存储,相对 m 的偏移量是 sizeof(unsigned int),也即 4 个字节。
    
    如果再将成员 p 的位宽也改为 22,那么输出结果将会是 12,三个成员都不会挨着存储。

     

  2. 当相邻成员的类型不同时,不同的编译器有不同的实现方案, GCC 会压缩存储,而 VC/VS 不会。·

    #include <stdio.h>
    
    int main()
    {
    struct bs{
    unsigned m: 12;
    unsigned char ch: 4;
    unsigned p: 4;
    };
    
    printf("%d\n", sizeof(struct bs));
    
    return 0;
    }
    
    在 GCC 下的运行结果为 4,三个成员挨着存储;在 VC/VS 下的运行结果为 12,三个成员按照各自的类型存储(与不指定位宽时的存储方式相同)。

     

  3. 如果成员之间穿插着非位域成员,那么不会进行压缩。
    struct bs{
    unsigned m: 12;
    unsigned ch;
    unsigned p: 4;
    };
    
    在各个编译器下 sizeof 的结果都是 12。

           通过上面的分析,我们发现位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用&获取位域成员的地址是没有意义的, C 语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号。
     

3、无名位域

       位域成员可以没有名称,只给出数据类型和位宽,如下所示:

struct bs{
int m: 12;
int : 20; //该位域成员不能使用
int n: 4;
};

       无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。上面的例子中,如果没有位宽为 20 的无名成员, m、 n 将会挨着存储, sizeof(struct bs) 的结果为 4;有了这 20位作为填充, m、 n 将分开存储, sizeof(struct bs) 的结果为 8。

七、C 语言 typedef 的用法

C 语言允许为一个数据类型起一个新的别名,就像给人起“绰号”一样。


起别名的目的不是为了提高程序运行效率,而是为了编码方便。例如有一个结构体的名字是 stu,要想定义一个结
构体变量就得这样写:

struct stu stu1;

struct 看起来就是多余的,但不写又会报错。如果为 struct stu 起了一个别名 STU,书写起来就简单了:

STU stu1;


这种写法更加简练,意义也非常明确,不管是在标准头文件中还是以后的编程实践中,都会大量使用这种别名。

使用关键字 typedef 可以为类型起一个新的别名。 typedef 的用法一般为:

typedef oldName newName;

1、typedef给int、char等类型定义别名

typedef int INTEGER;
INTEGER a, b;
a = 1;
b = 2;

INTEGER a, b;等效于 int a, b;

2、typedef给数组类型定义别名

typedef char ARRAY20[20];

表示 ARRAY20 是类型 char [20]的别名。它是一个长度为 20 的数组类型。接着可以用 ARRAY20 定义数组:

ARRAY20 a1, a2, s1, s2;

它等价于:

char a1[20], a2[20], s1[20], s2[20];

注意,数组也是有类型的。例如 char a1[20];定义了一个数组 a1,它的类型就是 char [20]。

3、为结构体类型定义别名

typedef struct stu
{
char name[20];
int age;
char sex;
} STU;

STU 是 struct stu 的别名,可以用 STU 定义结构体变量:

STU body1,body2;

它等价于:
struct stu body1, body2;

4、为指针类型定义别名

typedef int (*PTR_TO_ARR)[4];

表示 PTR_TO_ARR 是类型 int * [4]的别名,它是一个二维数组指针类型。接着可以使用 PTR_TO_ARR 定义二维数组指针:

PTR_TO_ARR p1, p2;

5、函数指针类型定义别名

typedef int (*PTR_TO_FUNC)(int, int);

PTR_TO_FUNC pfunc;

6、指针示例

#include <stdio.h>

typedef char (*PTR_TO_ARR)[30];
typedef int (*PTR_TO_FUNC)(int, int);

int max(int a, int b)
{
return a>b ? a : b;
}

char str[3][30] = {
"http://c.biancheng.net",
"C语言中文网",
"C-Language"
};

int main()
{
PTR_TO_ARR parr = str;
PTR_TO_FUNC pfunc = max;
int i;
printf("max: %d\n", (*pfunc)(10, 20));
for(i=0; i<3; i++){
printf("str[%d]: %s\n", i, *(parr+i));
}

return 0;
}


运行结果:
max: 20
str[0]: http://c.biancheng.net
str[1]: C 语言中文网
str[2]: C-Language

       需要强调的是, typedef 是赋予现有类型一个新的名字,而不是创建新的类型。为了“见名知意”,请尽量使用含义明确的标识符,并且尽量大写。

7、typedef 和 #define 的区别

1) 可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做。如下所示:

#define INTERGE int
unsigned INTERGE n;     没问题

typedef int INTERGE;
unsigned INTERGE n;     错误,不能在 INTERGE 前面添加 unsigned

2) 在连续定义几个变量的时候, typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证。例如:

#define PTR_INT int *
PTR_INT p1, p2;

经过宏替换以后,第二行变为:

int *p1, p2;

这使得 p1、 p2 成为不同的类型: p1 是指向 int 类型的指针, p2 是 int 类型。
相反,在下面的代码中:

typedef int * PTR_INT
PTR_INT p1, p2;

p1、 p2 类型相同,它们都是指向 int 类型的指针。

 

  • 8
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值