zl程序教程

您现在的位置是:首页 >  后端

当前栏目

[C语言]结构体进阶与枚举联合

C语言 结构 进阶 联合 枚举
2023-06-13 09:18:27 时间

结构体进阶与枚举联合::

结构体进阶:

结构体类型的声明

结构体概述:

有时候我们需要将不同类型的数据组合成一个有机的整体,如:一个学生有学号、姓名、性别、年龄、地址等属性.显然单独定义以上变量比较繁琐,数据不便于管理,C语言中给出了另一种构造数据类型——结构体.

结构体的声明:

struct tag
{
    member-list;
}variable-list;
例如:
struct Stu
{
    char name[20];
    int age;
    char sex[5];
    char ID[20];
};
struct Stu
{
	char name[20];
	int age;
}s1,s2;   
s1和s2是struct Stu类型的全局变量

结构体的特殊声明:匿名结构体类型

在声明结构体的时候,可以不完全的声明.
例如:匿名结构体
struct
{
	char name[20];
	int age;
}s1;匿名结构体类型只能用一次
匿名结构体类型的特点
特点:即使两个结构体成员都相同 编译器也会把两个声明当成完全不同的两个结构体
struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}a[20],* p;
int main()
{
	struct Stu s3;  s3是局部变量
	p = &x;  err编译器认为匿名结构体是两个完全不同的两个结构体
	return 0;
}

结构的自引用

错误写法:
struct Node
{
	int date;
	struct Node next;
};这种写法无法计算结点大小:即sizeof(struct Node)无法计算
正确写法:
struct Node
{
	int date;
	struct Node* next;
};
结构体的自引用只能包含结构体成员对应的指针
错误写法:
typedef struct
{
	int data;
	Node* next;
}Node;typedef只能对已然存在的数据类型进行重命名 而此代码中产生了先有鸡还是先有蛋的问题
不要把匿名结构体类型和自引用相结合
正确写法:
typedef struct Node
{
	int data;
	struct Node* next;
}Node;
typedef struct Node
{
	int data;
	struct Node* next;
}* linklist;等价于 typedef struct Node* linklist

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

定义结构体变量的方式:

1.先声明结构体类型,再定义结构体变量.

2.在声明类型的同时定义变量.

3.直接定义结构体类型变量(无变量名). 

结构体类型和结构体变量的关系:

结构体类型:指定了一个结构体类型,它相当于一个模型,但其中并无具体数据,系统对之也不分配实际内存单元.

结构体变量:系统根据结构体类型(内部成员状况)为之分配空间.

结构体类型的定义
struct stu
{
	char name[50];
	int age;
};
先定义类型,再定义变量(常用)
struct stu s1 = { "mike", 18 };
定义类型同时定义变量
struct stu2
{
	char name[50];
	int age;
}s2 = { "lily", 22 };
struct
{
	char name[50];
	int age;
}s3 = { "yuri", 25 };
结构体变量的定义和初始化
struct Point
{
	int x;
	int y;
}p1 = { 2,3 };
struct score
{
	int n;
	char ch;
};
struct Stu
{
	char name[20];
	int age;
	struct score s;
};
int main()
{
	struct Point p2 = { 3,4 };
	struct Stu s1 = { "张三","20" ,{100,'q'} };
	printf("%s %d %d %c\n", s1.name, s1.age, s1.s.n, s1.s.ch);
	return 0;
}

结构体成员的使用:

#include<stdio.h>
#include<string.h>
结构体类型的定义
struct stu
{
	char name[50];
	int age;
};

int main()
{
	struct stu s1;
	如果是普通变量,通过点运算符操作结构体成员
	strcpy(s1.name, "abc");
	s1.age = 18;
	printf("s1.name = %s, s1.age = %d\n", s1.name, s1.age);
	如果是指针变量,通过->操作结构体成员
	strcpy((&s1)->name, "test");
	(&s1)->age = 22;
	printf("(&s1)->name = %s, (&s1)->age = %d\n", (&s1)->name, (&s1)->age);
	return 0;
}

结构体数组:

#include <stdio.h>
统计学生成绩
struct stu
{
	int num;
	char name[20];
	char sex;
	float score;
};

int main()
{
	定义一个含有5个元素的结构体数组并将其初始化
	struct stu boy[5] = {
		{ 101, "Li ping", 'M', 45 },			
		{ 102, "Zhang ping", 'M', 62.5 },
		{ 103, "He fang", 'F', 92.5 },
		{ 104, "Cheng ling", 'F', 87 },
		{ 105, "Wang ming", 'M', 58 }};
	int i = 0;
	int c = 0;
	float ave, s = 0;
	for (i = 0; i < 5; i++)
	{
		s += boy[i].score;	计算总分
		if (boy[i].score < 60)
		{
			c += 1;		统计不及格人的分数
		}
	}
	printf("s=%f\n", s);打印总分数
	ave = s / 5;					计算平均分数
	printf("average=%f\ncount=%d\n\n", ave, c); 打印平均分与不及格人数
	for (i = 0; i < 5; i++)
	{
		printf(" name=%s,  score=%f\n", boy[i].name, boy[i].score);
        printf(" name=%s,  score=%f\n", (boy+i)->name, (boy+i)->score);
	}
	return 0;
}

结构体嵌套结构体:

#include <stdio.h>
struct person
{
	char name[20];
	char sex;
};
struct stu
{
	int id;
	struct person info;
};
int main()
{
	struct stu s[2] = { 1, "lily", 'F', 2, "yuri", 'M' };
	int i = 0;
	for (i = 0; i < 2; i++)
	{
		printf("id = %d\tinfo.name=%s\tinfo.sex=%c\n", s[i].id, s[i].info.name, s[i].info.sex);
	}
	return 0;
}

结构体赋值:

#include<stdio.h>
#include<string.h>
结构体类型的定义
struct stu
{
	char name[50];
	int age;
};
int main()
{
	struct stu s1;

	如果是普通变量,通过点运算符操作结构体成员
	strcpy(s1.name, "abc");
	s1.age = 18;
	printf("s1.name = %s, s1.age = %d\n", s1.name, s1.age);
	相同类型的两个结构体变量,可以相互赋值
	把s1成员变量的值拷贝给s2成员变量的内存
	s1和s2只是成员变量的值一样而已,它们还是没有关系的两个变量
	struct stu s2 = s1;
    memcpy(&s2, &s1, sizeof(s1));
	printf("s2.name = %s, s2.age = %d\n", s2.name, s2.age);
	return 0;
}

指向普通结构体变量的指针

#include<stdio.h>
结构体类型的定义
struct stu
{
	char name[50];
	int age;
};
int main()
{
	struct stu s1 = { "lily", 18 };
	如果是指针变量,通过->操作结构体成员
	struct stu *p = &s1;
	printf("p->name = %s, p->age=%d\n", p->name, p->age);
	printf("(*p).name = %s, (*p).age=%d\n",  (*p).name,  (*p).age);
	return 0;
}

堆区结构体变量

#include<stdio.h>
#include <string.h>
#include <stdlib.h>
结构体类型的定义
struct stu
{
	char name[50];
	int age;
};
int main()
{
	struct stu *p = NULL;

	p = (struct stu *)malloc(sizeof(struct  stu));
    如果是指针变量,通过->操作结构体成员
	strcpy(p->name, "test");
	p->age = 22;
	printf("p->name = %s, p->age=%d\n", p->name, p->age);
	printf("(*p).name = %s, (*p).age=%d\n", (*p).name,  (*p).age);
	free(p);
	p = NULL;
	return 0;
}

结构体嵌套一级指针

#include<stdio.h>
#include <string.h>
#include <stdlib.h>
结构体类型的定义
struct stu
{
	char *name; 一级指针
	int age;
};
int main()
{
	struct stu *p = NULL;
	p = (struct stu *)malloc(sizeof(struct  stu));
	p->name = malloc(strlen("test") + 1);
	strcpy(p->name, "test");
	p->age = 22;
	printf("p->name = %s, p->age=%d\n", p->name, p->age);
	printf("(*p).name = %s, (*p).age=%d\n", (*p).name, (*p).age);
	if (p->name != NULL)
	{
		free(p->name);
		p->name = NULL;
	}
	if (p != NULL)
	{
		free(p);
		p = NULL;
	}
    	return 0;
}

结构体内存对齐

计算结构体的大小与介绍offsetof:

struct S1
{
	char c1;
	int i;
	char c2;
};12byte
struct S2
{
	char c1;
	char c2;
	int i;
};8byte
struct S3
{
	double d;
	char c;
	int i;
};16byte
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};32byte
offsetof 宏 功能:返回一个结构体成员在这个类型创建变量中的偏移量
offsetof(type,member) 
头文件:#include<stddef.h>
#include<stddef.h>
#include<stdio.h>
struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("&zu\n", sizeof(struct S1));
	printf("%zu\n", offsetof(struct S1, c1));
	printf("%zu\n", offsetof(struct S1, i));
	printf("%zu\n", offsetof(struct S1, c2));
	return 0;
}

结构体内存对齐的规则:

1.第一个成员在与结构体变量偏移量为0的地址处.

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处.

   对齐数 = 编译器默认的一个对齐数与该成员大小的较小值.

   VS中的默认对齐数为8.

3.结构体总大小最大默认对齐数的整数倍,其他编译器上没有默认对齐数 所以其他编译器的默认对

齐数就是自身大小.

4.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍.

5.如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处 ,结构体的总大小就是所

有的最大对齐数(含嵌套结构体的对齐数)的整数倍.

6.计算技巧:要开辟空间上一个的值+数据类型大小=开辟终止数.

结构体内存对齐存在的原因:

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

2.性能原因:数据结构(尤其是栈)应该尽可能的在自然边界上对齐,处理器需要两次进行内存访问,而对其的内存只需要进行一次内存访问.

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

我们在设计结构体的时候既满足结构体对齐又节省空间的方法:让占用空间小的成员尽量集中到一起.

修改默认对齐数:

#include <stdio.h>
#pragma pack(8)设置默认对齐数为8
struct S1
{
 char c1;
 int i;
 char c2;
};
#pragma pack()取消设置的默认对齐数,还原为默认对齐数
#pragma pack(1)设置默认对齐数为1
struct S2
{
 char c1;
 int i;
 char c2;
};
#pragma pack()取消设置的默认对齐数,还原为默认对齐数
int main()
{
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));
    return 0;
}

结构体传参

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\n", ss.num);
}
void print2(const struct S* ps)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ps->data[i]);
	}
	printf("%d\n", ps->num);
}
int main()
{
	struct S s = { {1,2,3},100 };
	print1(s);传值调用
	print(&s);传址调用
	return 0;
}
优选print2函数 函数在传参时 参数是需要压栈的 会有时间和空间上的系统开销
如果传递一个结构体对象的时候 结构体过大 参数压栈的系统开销过大 会导致性能的下降

结构体普通变量做函数参数:

#include<stdio.h>
#include <string.h>
结构体类型的定义
struct stu
{
	char name[50];
	int age;
};
函数参数为结构体普通变量
void set_stu(struct stu tmp)
{
	strcpy(tmp.name, "mike");
	tmp.age = 18;
	printf("tmp.name = %s, tmp.age = %d\n", tmp.name, tmp.age);
}
int main()
{
	struct stu s = { 0 };
	set_stu(s); 值传递
	printf("s.name = %s, s.age = %d\n", s.name, s.age);
	return 0;
}

结构体指针变量做函数参数:

#include<stdio.h>
#include <string.h>
结构体类型的定义
struct stu
{
	char name[50];
	int age;
};
函数参数为结构体指针变量
void set_stu_pro(struct stu *tmp)
{
	strcpy(tmp->name, "mike");
	tmp->age = 18;
}
int main()
{
	struct stu s = { 0 };
	set_stu_pro(&s); 地址传递
	printf("s.name = %s, s.age = %d\n", s.name, s.age);
	return 0;
}

结构体数组名做函数参数:

#include<stdio.h>
结构体类型的定义
struct stu
{
	char name[50];
	int age;
};
//void set_stu_pro(struct stu tmp[100], int n)
//void set_stu_pro(struct stu tmp[], int n)
void set_stu_pro(struct stu *tmp, int n)
{
	int i = 0;
	for (i = 0; i < n; i++)
	{
		sprintf(tmp->name, "name%d%d%d", i, i, i);
		tmp->age = 20 + i;
		tmp++;
	}
}
int main()
{
	struct stu s[3] = { 0 };
	int i = 0;
	int n = sizeof(s) / sizeof(s[0]);
	set_stu_pro(s, n); 数组名传递
	for (i = 0; i < n; i++)
	{
		printf("%s, %d\n", s[i].name, s[i].age);
	}
	return 0;
}

const修饰结构体指针形参变量:

结构体类型的定义
struct stu
{
	char name[50];
	int age;
};
void fun1(struct stu * const p)
{
	p = NULL; err
	p->age = 10; ok
}
void fun2(struct stu const*  p)
void fun2(const struct stu *  p)
{
	p = NULL; ok
	p->age = 10; err
}
void fun3(const struct stu * const p)
{
	p = NULL; err
	p->age = 10; err
}

结构体实现位段

位段的定义:

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

1.位段的成员必须是int、unsigned int、signed int、char.

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

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};8byte
位段是用来节省空间的
注:一般情况下位段的成员是同一类型的

位段的内存分配:

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;
};3byte
char只涉及到一个字节 因此不考虑大小端的问题
int main()
{
	struct S s = { 0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	return 0;
}

位段的跨平台问题:

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

2.位段中最大位的数目是不确定的(16位机器最大16,32位机器最大32,写成27,在16位机器会出现问题)

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

4.当一个结构包含两个位段 第二个位段成员比较大,无法容纳第一个位段剩余位时,是舍弃剩余位还是利用 是不确定的.

总结:跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有不跨平台的问题. 位段的应用:

位段两道经典笔试题:

问:当A=2, B=3时,pointer分配( )个字节的空间
#define MAX_SIZE A+B
struct _Record_Struct
{
	unsigned char Env_Alarm_ID : 4;
	unsigned char Para1 : 2;
	unsigned char state;
	unsigned char avail : 1;
}*Env_Alarm_Record;
struct _Record_Struct* pointer = (struct _Record_Struct*)malloc(sizeof(struct _Record_Struct) * MAX_SIZE);
答案:9
下列代码的运行结果是:
int main()
{
	unsigned char puc[4];
	struct tagPIM
	{
		unsigned char ucPim1;
		unsigned char ucData0 : 1;
		unsigned char ucData1 : 2;
		unsigned char ucData2 : 3;
	}*pstPimData;
	pstPimData = (struct tagPIM*)puc;
	memset(puc, 0, 4);
	pstPimData->ucPim1 = 2;
	pstPimData->ucData0 = 3;
	pstPimData->ucData1 = 4;
	pstPimData->ucData2 = 5;
	printf("%02x %02x %02x %02x\n", puc[0], puc[1], puc[2], puc[3]);
	return 0;
}
程序的输出结果是:02 29 00 00

枚举:

枚举类型的定义

枚举:将变量的值一一列举出来,变量的值只限于列举出来的值的范围内.

enum 枚举名
{
    枚举值列表
};

枚举的性质:

在枚举值表中应列出所有可用值,也称为枚举元素.

枚举值是常量,不能在程序中用赋值语句再对它赋值.

枚举元素本身由系统定义了一个表示序号的数值从0开始顺序定义为0,1,2...

enum Day
{
	枚举常量
	Mon=1,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};
int main()
{
	enum Day d = Fri;
	printf("%d\n", Mon);
	printf("%d\n", Tues);
	printf("%d\n", Wed);
	return 0;
}

枚举的优点

1.增加代码的可读性和维护性.

2.和#define定义的标识符相比 枚举又类型检查 更加严谨.

3.防止了命名污染(封装).

4.与#define相比 便于调试.

5.使用方便 一次可以定义多个常量.

枚举的使用

枚举一般配合switch使用
enum Color
{
	red,
    blue,
    green,
    pink,
    yellow,
    black,
    white
};
int main()
{
	int value = 0;
	scanf("%d", &value);
	enum Color color;
	switch (value)
	{
	case red:
		printf("红色\n");
		break;
	case blue:
		printf("蓝色\n");
		break;
	case green:
		printf("绿色\n");
		break;
	case pink:
		printf("粉色\n");
		break;
	case yellow:
		printf("黄色\n");
		break;
	default:
		printf("输入错误\n");
		break;
	}
	return 0;
}

联合:

联合类型的定义

union Un
{
	int a;
	char c;
};
int main()
{
	union Un u;
	u.a = 0x11223344;
	u.c = 0x00;
	printf("%d\n", sizeof(u));
	printf("%p\n", &u);
	printf("%p\n", &(u.a));
	printf("%p\n", &(u.c));
	return 0;
}

联合的特点

联合的成员是共用一块空间的,这样一个联合变量的大小,至少是最大成员的大小.

判断当前机器是大端还是小端
代码1
int check_sys()
{
	int a = 1;
	return *(char*)&a;
}
代码2
int check_sys()
{
	union Un
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
}
代码3
匿名联合体
int check_sys()
{
	union 
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
}
int main()
{
	01 00 00 00 -小端
	00 00 00 01 -大端
	int ret = check_sys();
	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

联合大小的计算

联合的大小至少是最大成员的大小,当最大成员不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍.

联合一道经典笔试题:

下列代码的运行结果是:
#include<stdio.h>
int main()
{
    union
    {
        short k;
        char i[2];
    }*s, a;
    s = &a;
    s->i[0] = 0x39;
    s->i[1] = 0x38;
    printf("%x\n", a.k);
    return 0;
}
程序的输出结果是:38 39

C语言编程训练(牛客网)

1.字符串旋转结果

写一个函数,判断一个字符串是否为另外一个字符串旋转之后的字符串.
例如:给定s1 =AABCD和s2 = BCDAA,返回1
给定s1=abcd和s2=ACBD,返回0.
AABCD左旋一个字符得到ABCDA
AABCD左旋两个字符得到BCDAA
AABCD右旋一个字符得到DAABC
#include<stdio.h>
方法1
int is_left_move(char arr1[], char arr2[])
{
	int len = strlen(arr1);
	int i = 0;
	for (i = 0; i < len; i++)
	{
		char tmp = arr1[0];
		int j = 0;
		for (j = 0; j < len - 1; j++)
		{
			arr1[j] = arr1[j + 1];
		}
		arr1[len - 1] = tmp;
		if (strcmp(arr2, arr1) == 0)
			return 1;
	}
	return 0;
}
int main()
{
	char arr1[] = "abcdef";
	char arr2[] = "cdefab";
	int ret = is_left_move(arr1, arr2);
	if (ret == 1)
		printf("OK\n");
	else
		printf("No\n");
	return 0;
}
上述方法右旋思路相同
方法2:使用库函数
int is_left_move(char arr1[], char arr2[])
{
	int len1 = strlen(arr1);
	int len2 = strlen(arr2);
	if (len1 != len2)
		return 0;
	strncat(arr1,arr1, len1);
	char* ret = strstr(arr1, arr2);
	if (ret == NULL)
		return 0;
	else
		return 1;
}
int main()
{
	char arr1[20] = "abcdef";
	char arr2[] = "cdefab";
	int ret = is_left_move(arr1, arr2);
	if (ret == 1)
		printf("OK\n");
	else
		printf("No\n");
	return 0;
}
自己给自己追加之后 就会有旋转之后所有的可能性
尽量不要用strcat自己给自己追加

2.BC96-有序序列判断

#include <stdio.h>
int main()
{
    int n = 0;
    int arr[50] = { 0 };
    scanf("%d", &n);
    int i = 0;
    int flag1 = 0;
    int flag2 = 0;
    for (i = 0; i < n; i++)
    {
        scanf("%d", &arr[i]);
        if (i > 0)
        {
            if (arr[i] > arr[i - 1])
            {
                flag1 = 1;
            }
            else if (arr[i] < arr[i - 1])
            {
                flag2 = 1;
            }
            else
            {
                ;
            }
        }
    }
    if (flag1 + flag2 <= 1)
        printf("sorted\n");
    else
        printf("unsorted\n");
    return 0;
}

3.BC106-上三角矩阵判定

#include <stdio.h>
int main()
{
    int n = 0;
    scanf("%d", &n);
    int arr[n][n];//变长数组
    int i = 0;
    int j = 0;
    int flag = 1;//表示是上三角矩阵
    for (i = 0; i < n; i++)
    {
        for (j = 0; j < n; j++)
        {
            scanf("%d", &arr[i][j]);
        }
    }
    判断
    for (i = 0; i < n; i++)
    {
        for (j = 0; j < i; j++)
        {
            if (arr[i][j] != 0)
            {
                flag = 0;
                goto END;
            }
        }
    }
END:
    if (flag == 0)
        printf("NO\n");
    else
        printf("YES\n");
    return 0;
}

4.BC107—矩阵转置

#include <stdio.h>
int main()
{
    int n = 0;
    int m = 0;
    scanf("%d %d", &n, &m);
    int arr[10][10] = { 0 };
    int i = 0;
    int j = 0;
    for (i = 0; i < n; i++)
    {
        for (j = 0; j < m; j++)
        {
            scanf("%d", &arr[i][j]);
        }
    }
    i控制的列
    for (i = 0; i < m; i++)
    {
        j控制的行
        for (j = 0; j < n; j++)
        {
            printf("%d ", arr[j][i]);
        }
        printf("\n");
    }
    return 0;
}
在原二维数组的基础上进行矩阵逆置只要将原来控制行的i变成控制列
将原来控制列的j变成控制行就可以了

5.BC115-小乐乐与欧几里得                                                                    

方法1:
#include <stdio.h>
int main()
{
    int n = 0;
    int m = 0;
    while (scanf("%d %d", &n, &m) == 2)
    {
        int min = n < m ? n : m;
        int max = n > m ? n : m;
        int i = min;
        int j = max;
        while (1)
        {
            if (n % i == 0 && m % i == 0)
            {
                break;
            }
            i--;
        }
        i就是最大公约数
        while (1)
        {
            if (j % n == 0 && j % m == 0)
            {
                break;
            }
            j++;
        }
        j就是最小公倍数
        printf("%d\n", i + j);
    }
    return 0;
}
方法2:
#include <stdio.h>
int main()
{
    long n = 0;
    long m = 0;
    while (scanf("%ld %ld", &n, &m) == 2)
    {
        long i = n;
        long j = m;
        long r = 0;
        while (r = i % j)
        {
            i = j;
            j = r;
        }
        j就是最大公约数
        printf("%ld\n", m * n / j + j);
    }
    return 0;
}