C —— 结构体

在C语言中,结构体(struct)指的是一种数据结构,是C语言中聚合数据类型(aggregate data type)的一类。结构体可以被声明为变量、指针或数组等,用以实现较复杂的数据结构。结构体同时也是一些元素的集合,这些元素称为结构体的成员(member),且这些成员可以为不同的类型,成员一般用名字访问。
—— 摘自百度百科

声明与定义

在 C 语言提供的基本数据类型中,只能表示单一的类型的数据类型,如果需要将多个数据类型组合成为一个整体的作为新的类型的话,就需要使用到结构体。 在表达式 int a = 10;中,变量 a 被定义为了 int 类型,无论如何,变量 a 都表示的是一个数。而实际运用中,需要依赖变量某个变量来表示更多的内容。

例如,需要表示某个人的信息,包括姓名,年龄,体重等。那每个信息可以使用一个基本数据类型来表示。如下代码所示:

#include<stdio.h>

int main() {
	char name[50] = "tom";
	unsigned int age = 27;
	double weight = 57.9;
	printf("name is %s, age is %d, weight is %f\n", name, age, weight);
	return 0;
}

执行结果:

name is tom, age is 27, weight is 57.900000

但是作为某个人的信息,它们应该是一个整体。这时候可以使用结构体定义一个新的类型,这个类型将包含需要表示的某个人的信息的基本类型。如下代码所示:

/**
 * 人
 */
struct Person {
	/**
	 * 姓名
	 */
	char name[50];
	/**
	 * 年龄
	 */
	unsigned int age;
	/**
	 * 体重
	 */
	double weight;
};

这里定义了一个 Person 类型,该类型包含了 C 提供的三个基本类型。而作为整体,该类型将表示某个人的数据。

结构体的定义语法:

struct 结构体名称 {
	member-list
}

定义好的结构体与基本数据类型的使用方式是一样的。如下所示:

char name[50];
unsigned int age;
double weight;
struct Person man;

或者可以在声明的时候为其赋予初始值:

char name[50] = "Tom";
unsigned int age = 27;
double weight = 57.9;
struct Person man = {"Mark", 28, 60.2};

也可以在声明之后,使用运算符 . 来为结构体变量赋值,完整代码如下所示:

#include<stdio.h>
#include<string.h>

/**
 * 人
 */
struct Person {
	/**
	 * 姓名
	 */
	char name[50];
	/**
	 * 年龄
	 */
	unsigned int age;
	/**
	 * 体重
	 */
	double weight;
};

int main() {
	// 声明普通的变量
	char name[50];
	unsigned int age;
	double weight;
	// 为普通的变量赋值
	strcpy(name, "Tom");
	age = 27;
	weight = 57.9;
	// 输出变量的值
	printf("name is %s, age is %d, weight is %f\n", name, age, weight);
	
	// 声明结构体变量
	struct Person man;
	// 为结构体变量的内部成员赋值
	strcpy(man.name, "Mark");
	man.age = 28;
	man.weight = 60.2;
	// 输出结构体变量的内部成员的值
	printf("name is %s, age is %d, weight is %f\n", man.name, man.age, man.weight);
	return 0;
}

执行结果:

name is Tom, age is 27, weight is 57.900000
name is Mark, age is 28, weight is 60.200000

可以将结构体看作是将多个基础数据类型的变量进行打包的容器,使得这些只能表示单一信息的单元组合成一个整体,无论是取值还是储值都需要通过变量 man 使用运算符 . 来操作。在阅读代码的时候,可以将运算符 . 理解为汉字 ,比如 man.age = 28 可以理解为 man 的 age 是 28 <==> man's age is 28。也加强了代码的可读性。

当然,结构体的内部成员除了基础数据类型之外,还可以是指针变量,枚举变量和其他结构体,例如:

/**
 * 性别
 */
enum Gender {Girl, Boy};

/**
 * 成绩
 */
struct Score {
	/**
	 * 语文成绩
	 */
	int Chinese;
	/**
	 * 数学成绩
	 */
	int Math;
	/**
	 * 英语成绩
	 */
	int English;
};
/**
 * 人
 */
struct Person {
	/**
	 * 姓名
	 */
	char name[50];
	/**
	 * 年龄
	 */
	unsigned int age;
	/**
	 * 性别
	 */
	enum Gender gender;
	/**
	 * 体重
	 */
	double weight;
	/**
	 * 成绩
	 */
	struct Score score;
};

结构体作为参数

因为结构体类型中包含多个数据类型的成员,以结构体作为函数参数能减少函数的参数列表的长度,使得代码更加简洁。示例如下:

#include<stdio.h>

enum Gender {Girl=0, Boy=1};

/**
 * 人
 */
struct Person {
	/**
	 * 姓名
	 */
	char name[50];
	/**
	 * 年龄
	 */
	unsigned int age;
	/**
	 * 性别
	 */
	enum Gender gender;
	/**
	 * 体重
	 */
	double weight;
};

/**
 * 打印 Person 结构体的成员变量
 */ 
void print(struct Person person) {
	char *gender = person.gender ? "Boy" : "Girl";
	printf("Person{name:%s, age:%d, gender:%s, weight:%f}\n", person.name, person.age, gender, person.weight);
	
}

int main() {
	struct Person man = {"Mary", 16, Girl, 48.2};
	print(man);
	return 0;
}

执行结果:

Person{name:Mary, age:16, gender:Boy, weight:48.200000}

如果结构体中的成员变量较多,使用这种方式传递数据相比较把所有的成员变量列举到形参列表中而言显得更加方便简洁。

指向结构体的指针

同样的,声明一个指向结构体的指针,也能作为函数参数进行数据的传递。但是作为指针变量,需要访问结构体中的成员变量时,需要使用 -> 运算符。如下所示:

#include<stdio.h>

enum Gender {Girl=0, Boy=1};

/**
 * 人
 */
struct Person {
	/**
	 * 姓名
	 */
	char name[50];
	/**
	 * 年龄
	 */
	unsigned int age;
	/**
	 * 性别
	 */
	enum Gender gender;
	/**
	 * 体重
	 */
	double weight;
};

/**
 * 打印 Person 结构体的成员变量
 */ 
void print(struct Person *p) {
	char *gender = p->gender ? "Boy" : "Girl";
	printf("Person{name:%s, age:%d, gender:%s, weight:%f}\n", p->name, p->age, gender, p->weight);
}

int main() {
	struct Person man = {"Mary", 16, Girl, 48.2};
	struct Person *p = &man;
	printf("this person's name is %s\n", p->name);
	print(p);
	return 0;
}

执行结果:

this person's name is Mary
Person{name:Mary, age:16, gender:Girl, weight:48.200000}

结构体占用的内存空间大小

使用 sizeof 能够量出某个变量或者类型的占用空间的字节数,那结构体占用多大的内存空间呢?首先来看一段代码:

#include<stdio.h>

int main() {
	printf("sizeof(char) is %d\n", sizeof(char));
	printf("sizeof(int) is %d\n", sizeof(int));
	printf("sizeof(double) is %d\n", sizeof(double));
	printf("========================\n");
	
	struct A{int a; int b;};
	printf("sizeof(A) is %d\n", sizeof(struct A));
	
	struct B{int a; double b;};
	printf("sizeof(B) is %d\n", sizeof(struct B));
	
	struct C{double a; double b;};
	printf("sizeof(C) is %d\n", sizeof(struct C));
	
	struct D{double a; double b; int c;};
	printf("sizeof(D) is %d\n", sizeof(struct D));
	
	struct E{double a; double b; int c; int d;};
	printf("sizeof(E) is %d\n", sizeof(struct E));
	
	struct F{double a; double b; int c; int d; char e;};
	printf("sizeof(F) is %d\n", sizeof(struct F));
	
	return 0;
}

执行结果:

sizeof(char) is 1
sizeof(int) is 4
sizeof(double) is 8
========================
sizeof(A) is 8
sizeof(B) is 16
sizeof(C) is 16
sizeof(D) is 24
sizeof(E) is 24
sizeof(F) is 32

结构体 A, C, E 的大小正好是所有的内部成员的 size 之和。而其他的结构体的 size 都比内部成员的 size 的和还要大些。由此可知:结构体占用的内存空间大小不是简单的几个类型的 size 的和

从结果上看,结构体的 size 貌似是以 8 为单位扩张的。例如当成员中已经有两个 double 成员的时候,再增加一个 int 成员,就将自身扩张 8 个字节,于是 size 为 24。

实际上,结构体的 size 在扩张的时候是取决于 C 中的 #pragma pack(n) 指令。缺省情况下,n 的值为8。这也就是该例中扩张 8 个字节的原因。因为 n 的合法的数值分别是1、2、4、8、16。若将该例中增加该指令并设置 n 的值为 1,结构体就会以 1 位单位进行扩张。执行结果如下:

sizeof(char) is 1
sizeof(int) is 4
sizeof(double) is 8
========================
sizeof(A) is 8
sizeof(B) is 12
sizeof(C) is 16
sizeof(D) is 20
sizeof(E) is 24
sizeof(F) is 25

这样就能看到结构体的 size 就正好是各个成员的 size 之和了。

编译器中提供了#pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:

  1. 如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式
  2. 如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。

结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;否则必须为n的倍数。

—— 摘自百度百科

位域

除了使用指令能约束结构体的 size 之外,使用位域也能约束某个成员变量占用的内存大小,进而调整 结构体的 size。在结构内声明位域的形式如下:

struct 结构体标识符
{
  type [member_name] : width ;
};

在使用位域的时候,需要注意以下几个问题:

  1. 成员类型只能为 int(整型),unsigned int(无符号整型),signed int(有符号整型) 三种类型,决定了如何解释位域的值。
  2. 位域中位的数量。宽度必须小于或等于指定类型的位宽度。

示例代码如下:

#include<stdio.h>

int main() {
	
	struct A { int a:1; int b:1; };	
	printf("sizeof(A) is %d\n", sizeof(struct A));
	
	return 0;
}

执行结果:

sizeof(A) is 4

因为结构体 A 内部成员使用的 int 型,其 size 为 4,也就是 32 个 bit 位。所以位域的宽度不能超过 int 的宽度,也就是不能超过 32。在本例中,两个 int 类型的位域宽度值都是 1 ,在实际存储中,它只占用 1 个 bit 位,所以结构体的 size 就是一个 int 类型的 size。换句话将,结构体中所有 int 类型的成员的位域值之和不超过 32 的话,结构体 A 的 size 就为 4。如下例所示:

#include<stdio.h>

int main() {
	
	struct A {
		int a0:1; int a1:1; int a2:1; int a3:1; int a4:1;
		int a5:1; int a6:1; int a7:1; int a8:1; int a9:1;
		
		int b0:1; int b1:1; int b2:1; int b3:1; int b4:1;
		int b5:1; int b6:1; int b7:1; int b8:1; int b9:1;
		
		int c0:1; int c1:1; int c2:1; int c3:1; int c4:1;
		int c5:1; int c6:1; int c7:1; int c8:1; int c9:1;
		
		int d0:1; int d1:1;
	};
	
	printf("sizeof(A) is %d\n", sizeof(struct A));
	
	return 0;
}

执行结果:

sizeof(A) is 4

该例中结构体 A 的成员变量有 32 个,但是结构体 A 的 size 仍然是 4,因为没有超出一个 int 的位宽。如果在增加一个相同的成员,则超出了一个 int 的位宽,其 size 就会扩张为 8。

结构体小案例

结构体能够表示很多场景的实体,比如订单,商品等。在程序中,如果需要使用某个数据类型来表示一个订单信息,或者一个商品信息。使用结构体是一个不错的选择。

下面是一个模拟了一个简单的订单实体的数据小案例。本例中,声明了三个结构体,一个枚举类型;运用随机数并结合之前的文章的内容。具体代码如下所示:

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>

// 定义存货量的上限值
#define INVENTORY_QUANTITY 9

/**
 * 支付方式枚举
 */
enum PayWay {Alipay, WeChatPay, ApplePay, UnionPay, CreditCard};

/**
 * 商品详情
 */
struct Goods {
	/**
	 * 商品名称
	 */
	char name[50];
	/**
	 * 商品价格
	 */
	double price;
	/**
	 * 商品折扣
	 */
	unsigned short int discount;
};

/**
 * 订单详情
 */
struct OrderDetail {
	/**
	 * 该商品购买的数量
	 */
	int quantity;
	/**
	 * 小计
	 */
	double subtotalAmount;
	/**
	 * 该商品的详细信息
	 */
	struct Goods goods;
};

/**
 * 订单
 */
struct Order {
	/**
	 * 总计
	 */
	double totalAmount;
	/**
	 * 订单支付方式
	 */
	enum PayWay payway;
	/**
	 * 订单详情
	 */
	struct OrderDetail details[100];
};

int main() {
	
	// 定义多个商品集合用于后面模拟存货
	struct Goods stocks[INVENTORY_QUANTITY] = {
		{"键盘", 199.0, 9},
		{"鼠标", 129.5, 8},
		{"电源", 109.0, 10},
		{"音响", 699.0, 9},
		{"耳机", 169.0, 10},
		{"插板", 19.0, 10},
		{"电脑", 7899.0, 10},
		{"手机", 4999.0, 10},
		{"平板", 299.0, 10}
	};		
	// 使用随机数模拟购物需要的数据
	int count = 0;
	int indexs[INVENTORY_QUANTITY];
	struct Order order = {0.0, Alipay};
	{
		// 初始化已购清单的索引
		for(int k = 0; k < INVENTORY_QUANTITY; k++) {
			indexs[k] = -1;
		}
		// 使用当前时间作来初始化随机函数种子
		srand((unsigned)time(NULL));
		// 随机一个数字,这个数字不能超过 stocks 的长度
		count = rand() % INVENTORY_QUANTITY;
		for(int i = 0; i < count; i++) {
			// 随机一个商品
			int index = -1, exist = 0;
			while (index < 0) {
				index = rand() % INVENTORY_QUANTITY;
				for(int j = 0; j < INVENTORY_QUANTITY; j++) {
					if(indexs[j] == index) {
						exist = 1;
						break;
					}
				}
				if(exist == 1) {
					// 已经在买过该商品, 重新选择新的下标
					exist = 0;
					index = -1;
				}
			}
			for(int k = 0; k < INVENTORY_QUANTITY; k++) {
				if(indexs[k] < 0) {
					indexs[k] = index;
					break;
				}
			}
			struct Goods goods = stocks[index];
			// 随机一个数量,表示购买该商品的数量
			int quantity = rand() % 50;
			// 计算小计
			double subtotalAmount = quantity * goods.price * goods.discount / 10.0;
			order.totalAmount += subtotalAmount;
			// 生成一个订单详情信息
			struct OrderDetail detail = {quantity, subtotalAmount, goods};
			order.details[i] = detail;
		}
	}
	// 打印订单信息
	if(count > 0) {
		printf("\n==========================================================\n");
		{
			char way[11] = "";
			switch(order.payway) {
				case Alipay: strcpy(way, "支付宝"); break;
				case WeChatPay: strcpy(way, "微信支付"); break;
				case ApplePay: strcpy(way, "苹果支付"); break;
				case UnionPay: strcpy(way, "银联支付"); break;
				case CreditCard: strcpy(way, "信用卡支付"); break;
			}
			printf("订单总额:%f, 支付方式:%s", order.totalAmount, way);
		}
		printf("\n----------------------------------------------------------\n");
		printf("序号 \t 商品 \t 数量 \t 单价 \t\t 小计.\n");
		for(int i = 0; i < count; i++) {
			struct OrderDetail detail = order.details[i];
			char *name = detail.goods.name;
			int quantity = detail.quantity;
			double price = detail.goods.price;
			int discount = detail.goods.discount;
			double subtotalAmount = detail.subtotalAmount;
			printf(" %d. \t %s \t %d \t %.2f \t %.2f\n", (i + 1), name, quantity, price, subtotalAmount);
			if(discount > 0 && discount < 10) {
				double subPrice = price * discount / 10.0;
				printf(" \t \t \t 折后价 -> %.2f\n", subPrice);
			}
		}
		printf("\n==========================================================\n");
	} else {
		printf("没有查询到有效的订单信息");
	}
	
	return 0;
}

执行结果:


==========================================================
订单总额:53539.800000, 支付方式:支付宝
----------------------------------------------------------
序号     商品    数量    单价            小计.
 1.      鼠标    44      129.50          4558.40
                         折后价 -> 103.60
 2.      音响    25      699.00          15727.50
                         折后价 -> 629.10
 3.      键盘    19      199.00          3402.90
                         折后价 -> 179.10
 4.      平板    15      299.00          4485.00
 5.      插板    13      19.00   247.00
 6.      电源    47      109.00          5123.00
 7.      手机    4       4999.00         19996.00

==========================================================

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值