自定义类型

目录

结构体

[① 声明](#① 声明)

[② 创建和初始化](#② 创建和初始化)

[③ 特殊声明](#③ 特殊声明)

[④ 自引用](#④ 自引用)

[⑤ 成员访问](#⑤ 成员访问)

[⑥ 传参](#⑥ 传参)

[⑦ 实现位段](#⑦ 实现位段)

概念

应用

注意事项

内存对齐

[① 对齐规则](#① 对齐规则)

[② 存在的原因](#② 存在的原因)

[③ 修改默认对齐数](#③ 修改默认对齐数)

[④ offsetof](#④ offsetof)

枚举

[① 定义](#① 定义)

[② 优点](#② 优点)

[③ 使用](#③ 使用)

联合体

[① 定义](#① 定义)

[② 特点](#② 特点)

[③ 大小](#③ 大小)

[④ 判断大小端](#④ 判断大小端)


结构体

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

① 声明

cpp 复制代码
struct tag
{
    //结构的成员可以是标量、数组、指针,甚至是其他结构体
    member-list;
}variable-list;

② 创建和初始化

cpp 复制代码
struct Point
{
    int x;
    int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值
struct Point p3 = {a, b};

struct Node
{
    int data;
    struct Point p;
    struct Node* next; 
}n1 = {10, {1,3}, NULL}; //结构体嵌套初始化
//结构体嵌套初始化
struct Node n2 = {20, {2, 6}, NULL};

struct Stu        //类型声明
{
    char name[20];//名字
    int age;      //年龄
};
//初始化:定义变量的同时赋初值
struct Stu s = {"tianci", 13};

int main()
{
 //按照结构体成员的顺序初始化
    struct Stu s1 = { "天赐", 20 };
    printf("name: %s\n", s1.name);
    printf("age : %d\n", s1.age);
 //按照指定的顺序初始化
    struct Stu s2 = { .age = 18, .name = "良缘" };
    printf("name: %s\n", s2.name);
    printf("age : %d\n", s2.age);
    return 0;
}

③ 特殊声明

cpp 复制代码
//在声明结构的时候,可以不完全的声明
//匿名结构体类型
struct
{
    int a;
    char b;
    float c;
}x;
struct
{
    int a;
    char b;
    float c;
}a[20], *p;

但是如果 p = &x;(将x地址赋给p),编译器会报出警告,因为编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。

匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。

④ 自引用

cpp 复制代码
//在结构中包含⼀个类型为该结构本⾝的成员
//定义⼀个链表的节点
struct Node
{
    int data;
    struct Node* next;
};

在刚开始结构体⾃引⽤使⽤的过程中,会容易出现问题

cpp 复制代码
struct Node
{
    int data;
    struct Node next;
};

typedef struct
{
    int data;
    Node* next;
}Node;

上述两段代码都出现了典型的错误

① 仔细分析,如果我要计算它的大小,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的⼤⼩就会⽆穷的⼤,是不合理的。

② 在结构体⾃引⽤使⽤的过程中,夹杂了 typedef 对匿名结构体类型重命名,因为Node是对前⾯的匿名结构体类型的重命名产⽣的,但是在匿名结构体内部提前使⽤Node类型来创建成员变量,所以这也是不合理的。

cpp 复制代码
//建议定义结构体不要使⽤匿名结构体
typedef struct Node
{
    int data;
    struct Node* next;
}Node;

⑤ 成员访问

cpp 复制代码
struct Stu
{
    char name[20];
    int age;
};

void print1(struct Stu ps1)
{
    printf("name = %s   age = %d\n", ps1.name, ps1.age);
}
void print2(struct Stu* ps2)
{
    printf("name = %s   age = %d\n", (*ps2).name, (*ps2).age);
    //使用结构体指针访问指向对象的成员
    printf("name = %s   age = %d\n", ps2->name, ps2->age);
}

int main()
{
    struct Stu s1 = {"tianci", 20};
    print(s1);//结构体传参
    struct Stu s2 = {"liangyuan", 21};
    print(&s2);//结构体地址传参
    return 0;
}

总结:

① 直接访问: 结构体变量访问成员用.

② 间接访问: 结构体指针访问成员用->

⑥ 传参

cpp 复制代码
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;
}

print1 和 print2 函数哪个好些?⾸选print2函数,因为函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。
结论:结构体传参的时候,要传结构体的地址。

⑦ 实现位段

结构体具有实现位段的能力

概念

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

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

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

cpp 复制代码
struct A
{
    int _a:2;
    int _b:5;
    int _c:10;
    int _d:30;
};
int main()
{
    //A是⼀个位段类型
    printf("%d\n", sizeof(struct A));
    return 0;
}

应用

下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通是有帮助的。

注意事项

位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。

另外位段的内存分配在不同平台分配的方式是不一样的,所以位段具有跨平台问题,大家感兴趣的可以去研究一下,这里就不做过多介绍了。

内存对齐

对于结构体的基本概念介绍完了,下面我们来计算一下结构体的大小。

结构体内存对齐是一个非常非常重要的知识点!!!热门考点!!!

① 对齐规则

① 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处。

② 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。

对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。

(VS 中默认的值为8,Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩)

③ 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。

④ 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

cpp 复制代码
struct S1
{
    double d;
    char c;
    int i;
};
printf("%d\n", sizeof(struct S1));

struct S2
{
    char c1;
    struct S1 s1;
    double d;
};
printf("%d\n", sizeof(struct S2));

大家可以算一算S2的大小然后跑一下代码来检验是否准确。

② 存在的原因

在结构体中明明内存对齐浪费空间,为什么还要这么做呢?

其实那是因为我要读取i,如果按照内存对⻬规则存储数据,假设一次从整数倍位置可以读取4byte,那么我只需要读取一次就完成了,而如果不按照内存对⻬规则存储数据,那就需要读取两次,并且还要将数据拼接在一起。

为什么存在内存对⻬?(参考大部分材料)

① 平台原因 (移植原因):

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

② 性能原因:

数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。

cpp 复制代码
struct S1
{
    char c1;
    int i;
    char c2;
};

struct S2
{
    char c1;
    char c2;
    int i;
};

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

所以我们在设计结构体的时候,尽量让占⽤空间⼩的成员尽量集中在⼀起,这样既可以满⾜对⻬,⼜可以节省空间!

③ 修改默认对齐数

结构体在对⻬⽅式不合适的时候,我们可以⾃⼰更改默认对⻬数。

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

cpp 复制代码
#pragma pack(1)//设置默认对⻬数为1
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
	printf("%d\n", sizeof(struct S));
    //输出结果为6
	return 0;
}

④ offsetof

offsetof是一个宏,用于计算结构体成员相较于结构体变量起始位置的偏移量,使用时需要包含<stddef.h>文件。

cpp 复制代码
struct S
{
	char c1;
	char c2;
	int i;
};
int main()
{
	printf("%zd\n", offsetof(struct S, c1));
	printf("%zd\n", offsetof(struct S, c2));
	printf("%zd\n", offsetof(struct S, i));
    //输出结果为0、1、4
}

枚举

① 定义

cpp 复制代码
enum Day//星期
{
    Mon,
    Tues,
    Wed,
    Thur,
    Fri,
    Sat,
    Sun
};
enum Sex//性别
{
    MALE = 1,
    FEMALE = 3,
    SECRET = 9
};

代码中定义的 enum Day , enum Sex , enum Color 都是枚举类型,{}中的内容是枚举类型的可能取值,也叫枚举常量 。

这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。

② 优点

可能会有人问,这不就是#define 定义常量的作用吗?

那么枚举肯定是有它的特别之处

① 增加代码的可读性和可维护性

② 和#define定义的标识符比较枚举有类型检查,更加严谨

③ 便于调试

④ 使用方便,一次可以定义多个常量

③ 使用

cpp 复制代码
enum Color//颜色
{
    RED=1,
    GREEN=2,
    BLUE=4
};
//这样赋值是错误的
//enum Color clr = 5;
//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异
enum Color clr = GREEN;

联合体

联合体类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)

① 定义

cpp 复制代码
//联合类型的声明
union Un
{
    char c;
    int i;
};
//联合变量的定义
union Un un;
//计算联合变量的大小
printf("%d\n", sizeof(un));
//输出结果为4

② 特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)

cpp 复制代码
union Un
{
    int i;
    char c;
};

union Un un;
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//输出结果一样

un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
//输出结果为11223355

③ 大小

计算联合体大小

① 联合的大小至少是最大成员的大小

② 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍

cpp 复制代码
union Un1
{
    char c[5];
    int i;
};
union Un2
{
    short c[7];
    int i;
};

printf("%d\n", sizeof(union Un1));
//输出结果为8
printf("%d\n", sizeof(union Un2));
//输出结果为16

④ 判断大小端

cpp 复制代码
union Un
{
	char c;
	int i;
};
int main()
{
	union Un un = { 0 };
	un.i = 1;
	if (un.c == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

本篇文章到这里就结束啦,自定义类型的内容介绍完啦,大家一定要理解结构体,因为后面还会大量接触接触体,特别是在数据结构阶段,另外文章中内存对齐是本章的重点!!!希望对大家有所帮助! 下篇文章见啦,希望大家多多来支持一下!

感谢大家的三连支持!

相关推荐
人才程序员1 小时前
QML z轴(z-order)前后层级
c语言·前端·c++·qt·软件工程·用户界面·界面
w(゚Д゚)w吓洗宝宝了1 小时前
C vs C++: 一场编程语言的演变与对比
c语言·开发语言·c++
爱吃西瓜的小菜鸡6 小时前
【C语言】抽空洗澡
c语言·开发语言·学习·算法
encoconut7 小时前
动态内存管理
c语言
人才程序员12 小时前
【无标题】
c语言·前端·c++·qt·软件工程·qml·界面
思麟呀14 小时前
在C语言基础上的C++第一章(基础的输入输出和动态内存开辟)
c语言·c++·学习
小林熬夜学编程16 小时前
【Linux网络编程】第十三弹---构建HTTP响应与请求处理系统:从HttpResponse到HttpServer的实战
linux·运维·服务器·c语言·网络·c++·http
misty youth16 小时前
学生信息管理系统
c语言·数据结构·算法·c#
人才程序员18 小时前
Qt Widgets、QML与Qt Quick
c语言·开发语言·c++·qt·ui·qml
LabVIEW开发18 小时前
LabVIEW中什么和C 语言指针类似?
c语言·开发语言·labview