自定义类型篇

1.结构体

1.1结构体的概念

  • 数组是一系列相同类型元素的集合
  • 结构体是一系列类型可能不同的元素的集合
    比如我们想描述一个人,仅仅用C语言中的类型是无法准确描述的,这就需要我们自定义一个类型

1.2结构体的声明

struct Student    
{
	char name[20];//姓名
	int age;//年龄
	char sex[5];//性别
	float score;//分数
};//分号不能缺失

自定义类型篇_第1张图片

1.3特殊的声明

struct
{
	char name[20];
	int age;
	char sex[5];
	float score;
}stu2;

这种没有结构体标签的声明叫做匿名结构体;这种结构体只能使用一次,也就是说,只能使用变量列表中的变量,不能用匿名结构体去创建新的变量

struct    
{
	char a;    
	int b;    
}s;//匿名结构体变量s    

struct    
{
	char a;    
	int b;    
}* p;//匿名结构体指针变量p    

int main()    
{
	p = &s;    

	return 0;    
}

上述代码是否正确?
编译器会给我们警告,p和&s的类型不兼容
所以,即使两个匿名结构体的成员变量一样,编译器也会认为两个结构体是不同的类型

1.4结构体的自引用

一个结构体中包含一个自身的指针,叫做结构体的自引用

struct Node
{
	int val;
	struct Node* n;
};

错误的自引用:

typedef struct Node                                             
{
	int val;      
	Node* n;      
	//typedef是将整个结构体重命名为Node   
	//而结构体还没创建出来就使用了Node,因此这种自引用方式是错误的   
}Node;      

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

struct Student                                                     
{
	char name[20];                                                     
	int age;                                                     
	char sex[5];                                                     
	float score;                                                     
};

int main()                                                     
{
	struct Student stu1;//定义                                                     
	struct Student stu2 = { "baiyahua",19,"男",20.2 };//初始化
	printf("%s %d %s %.2f", stu2.name, stu2.age, stu2.sex, stu2.score)
	
	return 0;              
}

1.6结构体内存对齐

1.6.1结构体内存大小的计算

一个结构体的大小该怎么计算呢?是我们想的那样,将每个结构体成员的大小相加吗?

实际上,结构体的成员在分配内存时,需要遵守内存对齐规则,该规则如下:

  1. 第一个成员要在地址偏移量为0的地址处
  2. 之后的每个成员的位置都必须是对齐数的整数倍
    • 对齐数 = 编译器默认对齐数与该成员的大小中的较小值
    • VS的默认对齐数是8
    • Linux中没有对齐数的概念,所以对齐数就是成员本身的大小
  3. 结构体的总大小必须是最大对齐数的整数倍
  4. 对于嵌套的结构体,需要对齐到自己最大对齐数的整数倍处;整个结构体的大小是最大对齐数的整数倍
//练习1
struct S1
{
	char c1;
	int i;
	char c2;
};

//练习2
struct S2
{
	char c1;
	char c2;
	int i;
};

//练习3
struct S3
{
	double d;
	char c;
	int i;
};

//练习4-结构体嵌套问题
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

int main()
{
	printf("%zd\n", sizeof(struct S1));// 12
	printf("%zd\n", sizeof(struct S2));// 8
	printf("%zd\n", sizeof(struct S3));// 16
	printf("%zd\n", sizeof(struct S4));// 32

	return 0;
}

练习1:自定义类型篇_第2张图片
练习2和练习3同理

练习4:自定义类型篇_第3张图片

1.6.2 为什么要内存对齐

  1. 平台原因:

    不是所有的平台都能访问任意地址处的内容的;某些平台只能访问特定地址处的内容

  2. 性能原因:

    有时为了访问没有对齐的内存,处理器需要做两次处理,而对齐了的内存,处理器只需做一次处理

    自定义类型篇_第4张图片

可以看到,为了内存对齐,会相应的浪费一些空间,那怎么能在内存对齐的情况下,尽量减小空间的浪费呢?

我们在定义结构体时,让占用空间小的成员尽量集中在一起,就比如上面的S1和S2

对于S1,两个char类型的变量分开了;对于S2,两个char类型的变量在一起,其结构体的大小比S1小

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

1.7 修改默认对齐数

对于不同的编译器,默认对齐数是不同的,而我们也可以通过代码的形式修改默认对齐数

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

int main()
{
    printf("%d\n", sizeof(struct S2));// 6

    return 0;
}

1.8 结构体传参

struct S
{
    int arr[1000];
    char c;
};

void print1(struct S s)
{
    printf("%d %d %d %d %d %c\n", s.arr[0], s.arr[1], s.arr[2], s.c);
}

void print2(const struct S* s)
{
    printf("%d %d %d %d %d %c\n", s->arr[0], s->arr[1], s->arr[2], s->c);
}

int main()
{
    struct S s = { {1,2,3},'a' };
    print1(s);//传值调用
    print2(&s);//传址调用

    return 0;
}

对于上面的两种调用方式,都能完成我们的任务,大家觉得哪种好?

我们的结构体大小可能是很大的,这种情况下使用传值调用,还得再开辟一块一样大的空间,对于栈帧的消耗是非常大的;而使用传址调用只需要开辟一个指针大小的空间,大大降低了时间和空间

并且对参数进行const修饰,也能避免结构体内容被修改的情况

因此在对结构体进行传参时,推荐使用传址调用

2.位段


2.1位段的概念

位段与结构体的定义相似,但又有所不同:

  1. 位段的成员类型必须都是char,unsigned char,int,unsigned int中的一种
  2. 位段的成员变量后要加上冒号和数字

2.2位段的定义

struct A
{
	char _a : 3;
	char _b : 5;
	char _c : 2;
};

2.3位段的大小

int main()
{
	printf("%zd\n", sizeof(struct A));// 2

	return 0;
}

位段成员变量的内存开辟是按需所取,即先开辟一个类型的空间,如果不够,再往下开辟

对于上面的位段A,先开辟1字节的空间,其中有3bit位给了_a,还剩5bit;正好够_b,就把剩余的5bit给了_b;再另外开辟1字节,其中2bit位给了_c,所以位段A的大小就是2字节

但是位段的大小计算还存在一些问题:

  1. 分配给成员变量bit位时是从左往右分配还是从右往左分配,这是未确定的
  2. 剩余的bit位不够下一个变量存放时,是先用完剩余的bit位,再开辟新的空间,还是直接舍弃掉剩余的bit位,直接开辟新的空间,这也是未确定的

不同的平台,上面的规则需要我们去验证;对于VS,我们可以验证一下符合上面的哪种规则

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("%zd\n", sizeof(s));// 3

	return 0;
}

我们假设在VS中,内存分配时是从右往左分配的;并且当剩余的bit位不够下一个变量存放时,会舍弃剩余的bit位,开辟新的空间存放

如果真的如我们假设所想,那么结构体的大小就是3字节,内存中的数据以十六进制打印出来就是62,03,04
自定义类型篇_第5张图片

经过验证,确实如我们所想,在VS环境下,位段的内存分配规则是从右往左使用空间;剩余空间不够存放变量时,会另外开辟新的空间

2.4位段的跨平台问题

  1. int,char被当作有符号还是无符号类型是不确定的
  2. 位段中的成员变量最大位的数目是不确定的(16位机器下最大位数是16,32位机器下最大位数是32);假设在32位机器下的变量大小为28,那么移植到16位机器下就会出问题
  3. 内存分配是从右往左还是从左往右是不确定的
  4. 剩余位数无法容纳下一个变量时,是用完剩余的位数再开辟空间,还是浪费剩余的位数直接开辟空间,这是不确定的

总的来说,位段与结构体相比,能节约空间,但存在不少的跨平台问题

2.5位段的应用

位段在网络中应用的比较多,比如IP数据包;日常生活中,我们用微信向他人发送了一条信息,别人是怎么接收到我们的信息的?实际上,信息还会被各种东西包装,形成数据包;数据包中有发送人的IP地址,接收人的IP地址…其中有些数据很小,是完全不需要char或int来存储,这时就用到我们的位段了,如果用结构体存储,数据包就会比较大,在网络中流动的也就慢自定义类型篇_第6张图片

3.枚举


生活中,某些事物的取值是有限个,能被一一列举出来,比如三原色,一周的天数…这时就可以考虑使用枚举

枚举,就是一一列举

3.1 枚举的定义

//三原色
enum Color
{
	GREEN,
	RED,
	BLUE
};

当然,成员是可以赋值的,如果不赋值,默认从0开始递增;一个成员被赋值,下面没赋值成员的值递增

int main()
{
	printf("%d\n", GREEN);// 0
	printf("%d\n", RED);// 1
	printf("%d\n", BLUE);// 2

	return 0;
}
//三原色
enum Color
{
	GREEN=3,
	RED,
	BLUE
};


int main()
{
	printf("%d\n", GREEN);// 3
	printf("%d\n", RED);// 4
	printf("%d\n", BLUE);// 5

	return 0;
}

3.2枚举的使用

枚举中的成员都是常量,定义完我们就可以直接拿来使用

另外,我们最好拿枚举常量给枚举变量赋值,如果给枚举常量赋值其他类型,有些编译器是不会报错的,但这样枚举就失去了意义,要赋整型值,为什么不直接定义整型变量呢?况且,用枚举常量给枚举变量赋值能大大增加代码的可读性,一看就知道变量的含义是什么

enum Color
{
	GREEN,
	RED,
	BLUE
};

int main()
{
	enum Color color = GREEN;
	printf("%d\n", color);// 0

	color = RED;
	printf("%d\n", color);// 1

	//不好的写法
	color = 5;
	printf("%d\n", color);// 5

	return 0;
}

为什么使用枚举?我们用#define也能定义常量

#define GREEN 0
#define RED 1
#define BLUE 2

int main()
{
	printf("%d\n", GREEN);// 0
	printf("%d\n", RED);// 1
	printf("%d\n", BLUE);// 2

	return 0;
}

枚举的优点:

  1. 增加代码的可读性和可维护性
  2. 便于调试,因为预处理在编译期间会完成常量的替换
  3. 枚举定义的变量有类型检查,比#define更加严谨
  4. 使用方便,一次定义多个变量

4.联合体


联合体,顾名思义,它的特征是成员公用同一块空间

4.1 联合体的定义

//联合体的定义
union S
{
	int a;
	char b;
};

int main()
{
	printf("%zd\n", sizeof(union S));// 4

	return 0;
}

怎么确定联合体的成员公用一块空间呢?我们可以通过成员的地址来验证

自定义类型篇_第7张图片

可以发现,联合体s的地址,联合体的成员a,b的地址,三者是一样的,也就是说变量b和a使用了同一块空间

题目:利用联合体判断当前编译器是大端字节序存储还是小端字节序存储

union S
{
	int a;
	char b;
}s;

int main()
{
	s.a = 0x00000001;
	if (s.b == 0x00)
		printf("大端\n");
	else
		printf("小端\n");

	return 0;
}

自定义类型篇_第8张图片

4.2 联合体大小的计算

  • 联合体的大小至少是最大成员的大小
  • 当最大成员的大小不是对齐数的整数倍时,就要对齐到对齐数的整数倍处
union Un1
{
	char c[5];
	int i;
};

union Un2
{
	short c[7];
	int i;
};

int main()
{
	//下面输出的结果是什么?
	printf("%d\n", sizeof(union Un1));// 8
	printf("%d\n", sizeof(union Un2));// 16

	return 0;
}

关于自定义类型就讲到这!

你可能感兴趣的:(c语言)