结构体概述:
有时候我们需要将不同类型的数据组合成一个有机的整体,如:一个学生有学号、姓名、性别、年龄、地址等属性.显然单独定义以上变量比较繁琐,数据不便于管理,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
写一个函数,判断一个字符串是否为另外一个字符串旋转之后的字符串.
例如:给定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自己给自己追加
#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;
}
#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;
}
#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变成控制行就可以了
方法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;
}