0
点赞
收藏
分享

微信扫一扫

深入浅出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;
}

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

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

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,三个成员都不会挨着存储。

  1. 当相邻成员的类型不同时,不同的编译器有不同的实现方案, 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,三个成员按照各自的类型存储(与不指定位宽时的存储方式相同)。

  1. 如果成员之间穿插着非位域成员,那么不会进行压缩。

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 类型的指针。

 

举报

相关推荐

0 条评论