C语言自定义类型——结构体

        内置类型并不能满足在复杂条件下的使用,比如对人的描述:年龄,身高,电话等信息,C语言提供了自定义类型:结构体、联合体、枚举

        一、结构体

        1.1、结构体基本定义

        结构是一个或多个变量的集合,这些变量称为成员变量,这些变量可以是不同的类型

        而数组是一组相同类型元素的集合,结构的每个成员可以是不同类型的变量,这点要注意区分下。

        1.2、结构体声明

        struct 是关键字;

        tag是结构标记,是我们想要的名字,一般表示某种信息,比如描述一个人,people;

         {}中的member - list是成员列表,可以有一个或多个成员,类型可以不同;

        variable - list是变量列表。

//	struct tag
//	{
//		member - list;
//	}variable - list;
// struct 是关键字,tag是结构体标签,
// {}中的member - list是成员列表,
// variable - list是变量列表。

        (1)  这里只是创造了一个类型struct people,类似于int、double等是类型。

struct people
{
	//成员列表可以放描述某人的属性信息,可以放多个成员
	char name[15];
	int age;
};	//注意这里有分号
//这里只是创造了一个类型 struct people
//比如int double 是类型

        (2) 这里的代码在创建struct people类型的同时,创建两个结构体变量p1和p2,并且是全局变量。

struct people
{
	char name[15];
	int age;
}p1, p2;	//创建类型的同时,创建两个变量p1和p2
		//这里的p1和p2是利用创建的类型struct people,创建了两个结构体变量
		//p1和p2是全局变量

       (3)结构体创建的全局变量和局部变量。

struct people
{
	char name[15];
	int age;
}p1, p2;	//创建类型的同时,创建两个结构体变量p1和p2
		    //p1和p2是全局变量

int main()
{
	struct book
	{
		char name[20];
		float price;
		int number;
	}b1, b2;			//b1和b2是局部变量

	struct people p3;	//有了类型之后,使用struct people类型创建结构体变量p3,p3是局部变量

	return 0;
}

1.3、结构体特殊声明

        (1)创建结构体类型省略结构体标记时,称为匿名结构体类型,此时结构体变量无法创建。因为结构体标记表示声明一种新的结构体类型,如果省略,结构体类型变量无法创建。

//匿名结构体类型
struct 
{
	char name[15];
	int age;
}s1,s2;
//创建匿名结构体类型时创建两个变量s1和s2,
// 只能使用s1和s2
//后面不能再通过结构体类型创建变量,比如:struct s3,创建s3这是不可以的。

        (2)两个匿名结构体成员一致,但是编译器会将两个声明当成完全不同的两个类型。

struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}a[20], * p;
//匿名结构体定义一个数组a,数组a中可以放20个数据,
//定义了p,p是结构体指针。  *和匿名结构体类型结合起来称为结构体指针

int main()
{
	//两个匿名结构体放置的成员一样,但是编译器会把上面的两个声明当成完全不同的两个类型。
	p = &x;		//是非法的。

	return 0;
}

1.4、结构体自引用

       (1)在结构体内部包含自己是错误的,会造成死循环。而通过在结构体成员中包含一个同类型的结构体指针,是允许的,可以达到自引用的效果。

struct Node
{
	int data;
	struct Node next;	//结构体中包含自己类型是错误的
};

int main()
{
	int sz = sizeof(struct Node);	//err

	return 0;
}

//通过在结构体成员里包含一个同类型的结构体指针,自引用
struct Node
{
	int data;
	struct Node* next;	//结构体中指针能够找到自己同类型的节点,自引用。
};

        (2) 结构体类型太长,可以用typedef对其进行重命名,注意要先有结构体类型才能重命名。

//err,要先有结构体类型,才能重命名
typedef struct
{
	int data;
	Node* next;
}Node;

typedef struct Node
{
	int data;
	struct Node* next;
}Node;
//创建结构体类型struct Node的同时,对struct Node重命名为Node,
//以后: struct Node n1;   和   Node n1;  这两种写法是一样的

        (3)同样,可以定义结构体指针。

        ①可以在定义结构体类型的同时,创建一个结构体指针并重命名;

        ② 也可以先创建一个结构体,再创建结构体指针,重命名。

//1、定义了一个结构体指针的同时,重命名为linklist
typedef struct Node
{
	int data;
	struct Node* next;
}*linklist;

//2、定义一个结构体类型
 struct Node
{
	int data;
	struct Node* next;
};
//重命名结构体指针为linklist
typedef struct Node* linklist;

1.5、结构体变量的定义和初始化

       (1)结构体初始化

         ① 可以在创建结构体类型的同时,创建一个结构体类型的变量,并对其初始化。

         ② 也可以利用结构体类型创建一个变量,在对其初始化。

struct point
{
	int x;
	int y;
}p1 = {2,3},p2;	//创建struct point类型的同时,创建一个struct point类型的变量p1和p2,
				//创建p1变量的同时对p1初始化。
	
int main()
{
	struct point p3 = { 3,4 };	//对p3初始化
	return 0;
}

       (2)结构体嵌套初始化。

struct score
{
	int k;
	char ch;
};

//结构体嵌套
struct student
{
	char name[20];
	int age;
	struct score s;
};

int main()
{
	struct student s1 = { "lee",22,{99,'*'} };    //创建变量并初始化
	printf("%s\n", s1.name);
	printf("%c\n", s1.s.ch);
	printf("%s %d %d %c", s1.name, s1.age, s1.s.k, s1.s.ch);
	return 0;
}

1.6、结构体内存对齐

        在能够对结构体使用时,还有一个问题:结构体的大小是多少?

        在计算结构体大小之前,需要了解结构体内存对齐的规则:

        下面通过例子介绍对齐规则。

1.6.1、计算结构体大小-1

①对于struct S1,第一个成员  “c1”  在偏移量为0的地址处,c1的大小为1byte;

②第二个成员  “i”  的大小为4byte,默认对齐数是8。对齐数是默认对齐数与成员大小的最小值,所以  “i”  对齐数是4,所以地址要对齐到偏移量是4的整数倍处,偏移量为1、2、3的位置不是4的整数倍,所以不能存储在这些偏移量处,偏移量为4的位置是对齐数4的整数倍,可以放置,“i”   大小为4byte,所以  “i”  可以存放在偏移量为4处。

③c2的大小为1byte,默认对齐数是8,所以c2的对齐数是1,所以c2的地址要对齐到偏移量是1的整数倍处,偏移量为8的位置是1的整数倍(任意整数都是1的整数倍),所以可以存储在此处,c2的大小为1byte,占用一个字节

结构体的总大小为最大对齐数的整数倍,这里的最大对齐数是每个成员的对齐数进行比较,选择其中对齐数最大的一个。

c1对齐数为1,i 对齐数为4,c2对齐数为1,所以结构体的最大对齐数为4,总大小为4的整数倍,可以是4、8、12、16…

⑤所以最终结构体大小为12byte,其中黑色部分是未使用的内存,即浪费掉的内存。

另外可以通过offsetof计算偏移量,来验证上述分析中的偏移量是否正确。

offsetof的第一个参数是结构体类型,第二个参数是要确定偏移量的结构体成员。

struct S1
{
	char c1;	//1byte
	int i;		//4byte
	char c2;	//1byte
};

int main()
{
	struct S1 s1;	//为s1开辟空间,空间大小?

	printf("%d\n", sizeof(struct S1));

	//offsetof
	printf("%d\n", offsetof(struct S1, c1));	//0
	printf("%d\n", offsetof(struct S1, i));		//4
	printf("%d\n", offsetof(struct S1, c2));	//8

	return 0;
}
 1.6.2、计算结构体大小-2

第一个成员  c1  偏移量为0,大小为1byte,对齐数为1;

第二个成员  c2  大小为1,默认对齐数为8,所以c2的对齐数为1,偏移量为1的整数倍,任意整数都是1的整数倍。

第三个成员  i  的大小为4,默认对齐数是8,所以 i 的对齐数为4,偏移量为4的整数倍,可以是4、8、12、16...。

结构体的总大小为最大对齐数的整数倍,这里的最大对齐数是每个成员进行比较,选择其中对齐数最大的一个,比如 c1 对齐数为1,c2 对齐数为1,i 对齐数为4,所以结构体的最大对齐数为4,总大小为4的整数倍,可以是4、8、12、16…

 

最终s2的大小为8byte。

 1.6.2、计算结构体大小-3-存在结构体嵌套的情况

先分析S3的情况

 

d偏移量为0,d的大小为8byte,默认对齐数是8,所以d的对齐数为8;

c的大小为1,默认对齐数为8,所以c的对齐数为1,偏移量为1的整数倍;

i的大小为4byte,默认对齐数是8,所以i的对齐数是4,偏移量为4的整数倍,

 结构体的总大小为最大对齐数的整数倍,也就是8的整数倍。

所以最终S3的大小为16byte。

 

对于S4来说,嵌套了结构体。

如果嵌套了结构体,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包括嵌套结构体的对齐数)的整数倍。

拿s3举例,也就是说嵌套的结构体的对齐数为自己成员的最大对齐数,s3里面成员的最大对齐数是8,所以s3的对齐数为8 。

对于S4来说:

c1偏移量为0,c1的大小为1byte;

S3的大小为16byte,s3的最大对齐数为8,默认对齐数是8,所以s3的对齐数是8,偏移量是8的倍数;

d的大小为8byte,默认对齐数是8,所以d的对齐数是8,偏移量为8的整数倍,

 

结构体的整体大小就是所有最大对齐数(包括嵌套结构体的对齐数)的整数倍。

S4中,最大对齐数为8,所以结构体大小是8的整数倍。

最终,S4的大小为32byte。

 1.6.4、结构体内存对齐的原因

1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总体来说: 结构体的内存对齐是拿空间来换取时间的做法

因此在设计结构体的时候,既要满足对齐,又要节省空间,所以在允许的情况下,让占用空间小的成员尽量集中在一起,在一定程度上节省空间。

比如1.6.1中和1.6.2中两个例子,让占用空间小的c1和c2集中在一起。

1.7、修改默认对齐数

默认对齐数是可以修改的,使用#pragma 这个预处理指令,可以改变默认对齐数。

注意:将默认对齐数修改、使用之后,将默认对齐数在改为vs的默认对齐数。

//将默认对齐数改为4
#pragma pack(4)
struct S
{
	int i;
	double d;
};

//vs默认对齐数是8,在()中不填数字,默认vs当前对齐数
#pragma pack()

//默认对齐数改为1,意味着不对齐,因为任何数都是1的整数倍
#pragma pack(1)
struct S1
{
	char c;
	int i;
};

#pragma pack()

int main()
{
	printf("%d\n", sizeof(struct S));
	printf("%d\n", sizeof(struct S1));

	return 0;
}

 1.8、结构体传参

结构体传参有两种形式,一种是传结构体(传值),另一种传结构体地址(传址);相应的,在接收结构体也有两种形式:结构体和指针。

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象(传值调用时)的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的 下降。

因此结构体传参的时候,要传结构体的地址(传址调用)。

struct S
{
	int data[1000];
	int num;
};

void print1(struct S ss)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ss.data[i]);
	}
	printf("%d ", ss.num);
}

void print2(const struct S* ps)
{
	int i = 0;
	for(i = 0; i < 3; i++)
	{
		printf("%d ", ps->data[i]);
	}
	printf("%d ", ps->num);
}

int main()
{
	struct S s1 = { {1,2,3},100 };
	print1(s1);		//传值调用,形参是对实参的一份临时拷贝,会创建一份独立空间存放s1
					//
	print2(&s1);	//传址调用,传递地址,地址是4byte(32位)或者8byte(64位)
					//节省空间,提高效率
					//为了防止传址调用,值被修改,加const
	return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值