自定义类型:结构体----初学者笔记

目录

1. 结构体类型的声明

1.1 结构体类型的简单介绍和声明

1.1.1 结构的声明

1.1.2 特殊的声明

1.1.3 结构的自引用

2. 结构体变量的创建和初始化

3. 结构成员访问操作符

4. 结构体内存对⻬

4.1 对齐规则

4.2 练习

4.2.1 练习1

4.2.2 练习2

4.3 为什么存在内存对⻬?

4.4 修改默认对齐数

5. 结构体传参

6. 结构体实现位段

6.1 什么是位段

6.2 位段的内存分配

6.3 位段的跨平台问题

6.4 位段使⽤的注意事项


1. 结构体类型的声明

1.1 结构体类型的简单介绍和声明


首先,结构体分为内置类型和自定义类型2种,我们今天介绍的就是自定义类型的结构体。
当内置类型不能满足需求的时候,我们就用自定义类型的结构体,例如:我们要描述一本书,不能只通过一个变量来描述,需要知道书的书名、作者、价格、类型等等

1.1.1 结构的声明

而这就需要自定义类型的结构体来实现,结构体里面包含了各种类型的数据,用来描述一个复杂对象的各种属性。

struct book
{
	char name1[20];//作者名
	char name2[20];//书名
	int price;//价格
	//......
};

1.1.2 特殊的声明

我们有时候会看见这种匿名结构体类型
请看接下来的代码:

struct
{
	char a;
	int c;
	float d;
}s = { 0 };
struct
{
	char a;
	int c;
	float d;
}*ps;
int main()
{
	ps = &s;
	return 0;
}
//在上⾯代码的基础上,下⾯的代码合法吗?
ps= &s;

警告:
编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。

1.1.3 结构的自引用

大家觉得下面的代码可以运行成功吗?

struct Node
{
 int data;//存储数据--4个字节
 struct Node next;//下一个节点的地址--->指针类型变量4or8个字节
};
int main()
{
 struct Node n;
 return 0;
}

在给出答案之前,我们先来了解一下数据结构

自定义类型:结构体----初学者笔记_第1张图片


我们发现不可以,这是为什么呢?
我们可以假想一下:房子里面要放下一个大小一样的房子和另外的东西,这是不可能的
方法:将一个数据结构分成2部分,一半存数据,一半存放下一个节点的地址,这样就可以存储结构体指针了,也就可以结构自引用了

自定义类型:结构体----初学者笔记_第2张图片

struct Node
{
	int data;//存储数据--4个字节
	struct Node* next;//下一个节点的地址--->指针类型变量4or8个字节
};
int main()
{
	struct Node n;
	return 0;
}

自定义类型:结构体----初学者笔记_第3张图片

这样问题就解决了

2. 结构体变量的创建和初始化

初始化很简单,请看代码

struct Stu
{
  char name[20];
  int age;
  float score;
}s1,s2;//全局变量
struct Stu
{
  char name[20];
  int age;
  float score;
};

int main
{
  struct Stu s1,s2,s3;//局部变量
  return 0;
}

3. 结构成员访问操作符

结构体访问成员有2种方法:
1.结构体变量.成员变量名
2.结构体指针—>成员变量名

请看代码:

struct Stu
{
	char name[20];
	int age;
	float score;
}s1 = { "zhangsan",22,35.0f };

int main()
{
	struct Stu s2 ={"lisi",19,88.5f };
    struct Stu s3 = { "wangwu",39,76.0f };//按照顺序
    struct Stu s4 = { .age = 33,.name = "xiaofang",.score = 38.5f };//不按顺序
    printf("%s %d %lf\n", s1.name, s1.age, s1.score);
    printf("%s %d %lf\n", s2.name, s2.age, s2.score);
    struct Stu* ps = &s4;
    printf("%s %d %lf\n", ps->name, ps->age, ps->score);
    return 0;
}

4. 结构体内存对⻬

我们先看下面的代码,你觉得输出的是什么?

int main()
{
struct S1
{
 char c1;//1个字节
 int I;//4个字节
 char c2;//1个字节
};
printf("%d\n", sizeof(struct S1));

struct S2
{
 char c1;
 char c2;
 int i;
};
printf("%d\n", sizeof(struct S2));
return 0;
}


结果和你想的一样吗?
为什么成员一样,顺序不一样,存放的数据类型和变量也一样,但是结果不一样呢?
为什么开辟的空间不一样?s1和s2到底是怎么分配内存空间的?这里就涉及到了内存对齐!!!

4.1 对齐规则

1. 结构体的第⼀个成员对⻬到相对结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数 = 编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值
- VS中默认的值为8
- Linux中没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数(不包括默认对齐数),所有对⻬数中最⼤的)的整数倍
4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍

那我们来看看刚刚的s1和s2是怎么存储的

自定义类型:结构体----初学者笔记_第4张图片


4.2 练习

4.2.1 练习1

struct S3
{
 double d;//8个字节
 char c;//1个字节
 int I;//4个字节
};
printf("%d\n", sizeof(struct S3));

自定义类型:结构体----初学者笔记_第5张图片


4.2.2 练习2

struct S3
{
 double d;//8个字节
 char c;//1个字节
 int I;//4个字节
};
printf("%d\n", sizeof(struct S3));

struct S4
{
 char c1;
 struct S3 s3;
 double d;
};
printf("%d\n", sizeof(struct S4));

自定义类型:结构体----初学者笔记_第6张图片

4.3 为什么存在内存对⻬?

简单来说:结构体的内存对⻬是拿空间来换取时间的做法。

那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到:

//例如:
struct S1
{
 char c1;
 int i;
 char c2;
};
struct S2
{
 char c1;
 char c2;
 int i;
};

S1 和 S2 类型的成员⼀模⼀样,但是 S1 和 S2 所占空间的⼤⼩有了⼀些区别。

4.4 修改默认对齐数

#pragma 这个预处理指令,可以改变编译器的默认对⻬数。

#include 
#pragma pack(1)//设置默认对⻬数为1
struct S
{
 char c1;
 int i;
 char c2;
};
#pragma pack()//取消设置的默认对⻬数,还原为默认

但是,我们一般设置成2的次方数,如果乱修改的话,可能适得其反

5. 结构体传参

结构体传参有2种方式,分别是传值调用和传址调用

struct S
{
 int data[1000];
 int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
 printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
 printf("%d\n", ps->num);
}
int main()
{
 print1(s); //传结构体
 print2(&s); //传地址
 return 0;
}

综合来看,还是传址调用比较好

6. 结构体实现位段

6.1 什么是位段

位段是基于结构体的,是可以节省空间的

位指的是二进制位​​​​​​​

位段的声明和结构是类似的,有两个不同:

1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。

2. 位段的成员名后边有⼀个冒号和⼀个数字

使用举例:

struct A
{
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};
struct B
{
 int _a;
 int _b; 
 int _c;
 int _d;
};
int main( )
{
 printf("%d\n",sizeof(struct A));
 printf("%d\n",sizeof(struct B));
 return 0;
}
//空间是如何开辟的?

后面的数字指的是变量所占的空间大小, 例如:2指的就是变量a占两个二进制位,
以此类推,总共要占到42个二进制位, 我们初步推测,struct A大概需要6个字节

但是通过运行我们发现,实际上需要占到8个字节,但是还是比第二种方法所占内存小

我们发现和猜测的不一样,这就得讲讲位段的内存空间是怎么分配的了

6.2 位段的内存分配

1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型

2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。

3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。

我们以接下来的代码来分析一下:

struct S
{
 char a:3;
 char b:4;
 char c:5;
 char d:4;
};
int main()
{
 struct S s = {0};
 //s.a = 10;
 //s.b = 12;
 //s.c = 3;
 //s.d = 4;
 printf("%d\n",sizeof(struct S));
 return 0;
}

先根据位段大小,开辟空间;再根据存储数据的二进制位读取相应空间位数,存放至内存空间(统一假设从右向左储存,剩余空位不够下一个使用就空着不使用),开辟新的内存空间

自定义类型:结构体----初学者笔记_第7张图片

下面,我们看看完整代码的内存存储是怎么样的

struct S
{
 char a:3;
 char b:4;
 char c:5;
 char d:4;
};
int main()
{
 struct S s = {0};
 s.a = 10;
 s.b = 12;
 s.c = 3;
 s.d = 4;
 printf("%d\n",sizeof(struct S));
 return 0;
}

为了方便观察,我们在内存中以16进制的形式查看自定义类型:结构体----初学者笔记_第8张图片
但是,这是为什么呢?

自定义类型:结构体----初学者笔记_第9张图片

6.3 位段的跨平台问题

1. int 位段被当成有符号数还是⽆符号数是不确定的。

2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。

3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。

总结:

跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

6.4 位段使⽤的注意事项

内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的

所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。

struct A
{
 int _a : 2;
 int _b : 5;
 int _c : 10;
 int _d : 30;
};
int main()
{
 struct A sa = {0};
 scanf("%d", &sa._b);//这是错误的
 
 //正确的⽰范
 int b = 0;
 scanf("%d", &b);
 sa._b = b;
 return 0;
}

本次的分享到这里就结束了!!!

PS:小江目前只是个新手小白。欢迎大家在评论区讨论哦!有问题也可以讨论的!

如果对你有帮助的话,记得点赞+收藏⭐️+关注➕

自定义类型:结构体----初学者笔记_第10张图片

你可能感兴趣的:(c语言,数据结构,自定义类型:结构体,笔记)