一、结构体
1、结构体基本概念
结构体属于用户自定义的数据类型,允许用户在同一个结构体变量中存储不同的类型的数据。
2、结构体的定义
(1)建立结构体类型的语法:
struct <结构体名> { <结构体成员列表> };
(2)定义结构体类型变量的三种方法:
①先声明结构体类型,再定义该类型的变量。
struct <结构体名> <变量名列表>;
②在声明类型的同时定义变量。
struct <结构体名>
{
<结构体成员列表>
}<变量名列表>;
③不指定类型名而直接定义结构体类型变量。(使用较少)
struct
{
<结构体成员列表>
}<变量名列表>;
(3)定义结构体类型时的关键字是struct,不可省略;创建结构体变量时,关键字struct可以省略。
①例1:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}s1,s2; //分号不能丢
int main()
{
Stu s3; //s1、s2是全局变量,s3是局部变量
return 0;
}
②例2:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p; //含有20个结构体类型元素的数组以及指向该结构体类型的指针
int main()
{
//p = &x; 虽然两个结构体成员相同,但是并不能算作同一类型,即使都是匿名
return 0;
}
3、结构体变量的初始化和访问
(1)在定义结构体变量时可以对它的成员初始化,初始化列表是用花括号括起来的一些数据(逗号分隔),这些数据依次赋给结构体变量中的各成员。
(2)可以访问结构体变量中成员的值,访问方式为:
<结构体变量名>.<成员名> 或 <结构体指针变量名>-><成员名>
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct Stu
{
char name[20]; //名字
int age; //年龄
char sex[10]; //性别
char id[15]; //学号
};
int main()
{
//利用结构体类型创建一个该类型的结构体变量
struct Stu s = { "张三", 20,"男","20180101" };
printf("name = %s age = %d sex = %s id = %s\n", s.name, s.age, s.sex, s.id);
struct Stu *ps = &s;
printf("name = %s age = %d sex = %s id = %s\n", ps->name, ps->age, ps->sex, ps -> id);
return 0;
}
4、定义结构体数组
(1)结构体数组的每个元素都是一个结构体类型的数据,它们都分别包括各个成员项。
(2)定义结构体数组的两种形式:
struct <结构体名>
{
<结构体成员列表>
}<数组名>[<数组长度>];
<结构体名> <数组名>[<数组长度>];
(3)对结构体数组初始化的形式与对普通数组初始化的形式类似,只是每个元素都是结构体常量,用花括号括起,花括号内的是赋给结构体元素各成员的值。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//结构体定义
struct student
{
//成员列表
char name[100];
int age;
int score;
};
int main() {
//结构体数组
struct student arr[3] =
{
{"张三",19,90},
{"李四", 18, 100},
{"王五",20,80}
};
arr[2].age = 21; //可以修改数组里某个结构体元素的某个成员
for (int i = 0; i < 3; i++)
{
printf("姓名:%s 年龄:%d\n", arr[i].name, arr[i].age);
}
return 0;
}
5、指向结构体变量的指针
(1)指针变量的基类型必须与结构体变量的类型相同。
(2)利用操作符“->”可以通过结构体指针访问结构体属性(也可以将结构体指针解引用,再用操作符“.”一样可以访问结构体属性)。
(3)定义结构体指针的两种形式:
struct <结构体名>
{
<结构体成员列表>
}*<指针名>;
<结构体名> *<指针名>;
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct Stu
{
char name[20];
int age;
};
void print(struct Stu* ps)
{
printf("name = %s age = %d\n", (*ps).name, (*ps).age);
//使用结构体指针访问指向对象的成员
printf("name = %s age = %d\n", ps->name, ps->age);
}
int main()
{
struct Stu s = { "zhangsan", 20 };
print(&s);//结构体地址传参
return 0;
}
6、指向结构体数组的指针
(1)指针变量的基类型必须与结构体变量的类型相同。
(2)可以用指针变量指向结构体数组的元素,和普通数组一样,只是指向结构体数组元素的指针还可以访问结构体元素的成员。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//结构体定义
struct student
{
//成员列表
char name[100];
int age;
int score;
};
int main() {
struct student stu[2] = { { "张三",19,90 }, { "张四",14,50 } };
//int * p = &s ;
struct student *p = stu; //指针指向数组首元素
printf("姓名:%s 年龄:%d\n", p->name, p->age);
p++; //指针指向下一个元素
printf("姓名:%s 年龄:%d\n", p->name, p->age);
return 0;
}
7、结构体嵌套结构体
(1)结构体中的成员可以是另一个结构体,在结构体中可以定义另一个结构体作为成员。
(2)需要注意的是,结构体中的成员类型不可以是结构体的本身类型(否则定义该结构体类型的变量时内存将会被无限开辟),不过可以是结构体本身类型的指针(结构体的自引用)。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = { 1, 2 };
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = { "zhangsan", 20 };//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化
struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化
int main()
{
return 0;
}
8、结构体做函数参数
(1)将结构体作为参数向函数中传递,其中传递方式有值传递和地址传递两种,如果不想修改主函数中的数据就用值传递,反之用地址传递。
(2)函数传参的时候,参数是需要压栈的,如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降,所以结构体传参的时候,尽量传结构体的地址。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
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;
}
9、结构体内存对齐
(1)结构体的存储结构并不是和预料中的一样,结构体中有不同类型的成员,比如一个结构体中有两个字符型变量(分别为1成员和2成员)和一个整型变量(3成员,认为它占4字节),那么这个结构体的存储结构应该如下右图所示,这虽然浪费了两个字节的空间,但是却能提升程序的运行效率,这种操作就是内存对齐。
(2)结构体的对齐规则:
①第一个成员在与结构体变量偏移量为0的地址处。
②其它成员变量要对齐到某个数字(对齐数)的整数倍的地址处,其中对齐数为编译器默认的一个对齐数与该成员大小的较小值(VS中默认的值为8)。
③结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
④如果是嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
(3)根据结构体的对齐规则,不难得出结构体的大小不仅与其成员类型有关,还与不同类型成员的排序有关,在设计结构体的时候,既要满足对齐,又要节省空间,让占用空间小的成员尽量集中在一起。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S1));//12
printf("%d\n", sizeof(struct S2));//8
printf("%d\n", sizeof(struct S3));//16
printf("%d\n", sizeof(struct S4));//32
return 0;
}
(4)编译器有一个默认的对齐数,而这个对齐数实际上可以进行修改,而且这个修改操作还是可逆的,具体见下面的代码。
#define _CRT_SECURE_NO_WARNINGS 1
#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));//12
printf("%d\n", sizeof(struct S2));//6
return 0;
}
10、柔性数组
(1)C99中,结构体中的最后一个元素允许是大小未知的数组,这就叫做柔性数组成员,其声明方式如下。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
typedef struct st_type //声明结构体的同时给它起别名(视个人爱好)
{
int i;
int a[0];//柔性数组成员 //如无法编译则可以改为int a[];
}type_a; //type_a是别名(不是变量名,而是类型名)
int main()
{
return 0;
}
(2)柔性数组的特点:
①结构体中的柔性数组成员前面必须至少一个其它成员。
②sizeof返回的这种结构体大小不包括柔性数组的内存。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//输出的是4
return 0;
}
(3)包含柔性数组成员的结构体应用malloc ()函数为其进行内存的动态分配,并且分配的内存应该大于结构体的大小,适应柔性数组的预期大小是其次,首先是能保证存储结构体的其它成员,剩余的空间则全部分配给柔性数组。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
//这样柔性数组成员a,相当于获得了100个整型元素的连续空间(不够就用realloc继续开辟)
p->i = 100;
for (i = 0; i < 100; i++)
{
p->a[i] = i;
}
free(p);
return 0;
}
二、位段
1、位段的声明
(1)位段的声明和结构体是类似的(比如给成员赋值的操作方法是相同的),但是它们又有两点不同:
①位段的成员必须是int、unsigned int、signed int或char(属于整型家族)类型。
②位段的成员名后边有一个冒号和一个数字,数字代表这个成员将占用多少个位(bit)的空间。
(2)举例(Visual Studio C++环境):
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct A
{
int _a : 2; //2个bit位,占1字节
int _b : 5; //5个bit位,占1字节
int _c : 10; //10个bit位,占2字节
int _d : 30; //30个bit位,占4字节
};
int main()
{
printf("%d\n", sizeof(struct A));//8
return 0;
}
2、位段的内存分配
(1)在VC中,位段在空间上是按照以4个字节(int型成员)或者1个字节(char型成员)为单位的方式来开辟的:
①假如有一个char型成员占用了1个字节的一部分,而这个字节剩下的空间还能容纳下一个char型成员,那么这两个char型成员可以共用1个字节,如果容不下,那么这个字节只属于前一个char型成员,需要为下一个char型成员分配1个新的字节。(甚至连续的最多8个char型成员可以共用1个字节,只要它们占用位数的总和不超过8位)
②对于int型成员,虽然是以4个字节为单位进行空间分配的,但是如果该成员占不满4个字节,则该成员将只占有它所用的字节的整个空间(如上例中的成员_a仅占1个字节),剩下的字节留给下一个成员,如果剩下的字节空间能容纳下一个成员,则暂时不用开辟新的4个字节,如果容不下,则为下一个int型成员分配4个新的字节。(甚至连续的最多4个int型成员可以共用4个字节,只要它们各自占用的位数都不超过8位)
(2)举例(Visual Studio C++环境):
①例1:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 }; //占3个字节
s.a = 10;
s.b = 12; //a、b共用1个字节
s.c = 3;
s.d = 4; //c、d各自用1个字节
printf("%d\n", sizeof(struct S));//3
return 0;
}
②例2:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct A
{
int _a : 2; //2个bit位,占1字节
int _b : 15; //15个bit位,占2字节
int _c : 30; //10个bit位,占4字节
int _d : 30; //30个bit位,占4字节
};
int main()
{
printf("%d\n", sizeof(struct A));//12
return 0;
}
3、位段的跨平台问题
(1)int位段被当成有符号数还是无符号数是不确定的。
(2)位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
(3)位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
(4)当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
三、枚举
1、概述
(1)如果一个变量只有几种可能的取值,则可以定义为枚举类型,所谓“枚举”就是把可能的值一一列举出来,变量的值只限于列举出来的值的范围中。
(2)被一一列举出来的值称为枚举元素或枚举常量,它们是用户指定的名字。
2、枚举类型的定义和使用
(1)声明枚举类型的一般形式为:
enum <枚举名>{<枚举元素列表>};
①enum是关键字,指出要定义的是枚举类型。
②枚举名是标识符,即由用户给出的具体枚举类型名。
③枚举元素表包含多个枚举值,它们用逗号分隔开,每个枚举值就是一个枚举常量,它有两种定义形式,如下所示。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
//这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值(后面未赋初值的从被赋初值一个开始一个一个递增1)
int main()
{
//以上定义的 enum Day , enum Sex , enum Color 都是枚举类型
//{ }中的内容是枚举类型的可能取值,也叫 枚举常量
return 0;
}
(2)关于enum类型的几点说明:
①一个enum类型实际上是int类型的一个子集,其每一个枚举值代表一个整数。
②n个枚举值全部未赋常量值时,它们从前至后分别与整数0、1、…、n-1对应。
③若第i个枚举值赋常量值为m,则其未赋常量值的后续枚举值分别与整数m+1、m+2、…对应,直到下一个赋了值的枚举值或结束,因此,为枚举值所赋的整型常量值应从前至后递增。
④只能拿枚举常量给枚举变量赋值或初始化,才不会出现类型的差异。
⑤定义枚举变量的方式和定义结构体变量的方式类似,只是关键字换为“enum”,这个关键字可以省略。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
enum Color//颜色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
enum Color clr = GREEN;
int main()
{
//clr = 2;
printf("%d", clr);//2
printf("%d", BLUE);//4
return 0;
}
四、联合(共用体)
1、概述
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,但与结构体不同的是,这些成员共用同一块空间(所以联合也叫共用体)。
2、共用体的声明与变量的定义
(1)共用体的声明形式:
union <共用体名>
{
<共用体成员列表>
};
(2)共用体变量的定义形式:
union <共用体名>
{
<共用体成员列表>
}<变量名列表>;
union <共用体名> <变量名列表>;
3、引用共用体变量的方式
(1)引用共用体变量的方式与引用结构体变量的方式类似,都可以使用操作符“.”对成员进行访问,不过虽然访问的是同一段内存空间,但是访问成员的类型不同,于是访问空间的大小以及对存储内容的解读方式也会有所差异(比如访问整型和字符型成员,访问空间的大小会不同;访问整型和浮点型成员,对存储内容的解读方式会不同)。
(2)因为共用体的成员共用同一片内存,所以对其中一个成员进行修改时,基本都会影响到其它成员。
(3)举例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
union Un
{
int i;
char c;
};
union Un un;
printf("%p\n", &(un));
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
printf("%x\n", un.c);
}
4、共用体的特点
(1)共用体的成员是共用同一块内存空间的,这样一个共用体变量的大小至少是最大成员的大小(因为共用体至少得有能力保存最大的那个成员)。
(2)当共用体最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
union Un1
{
char c[5];//占五个字节,按照char类型对齐,最大对齐数仍为8
int i;
};
union Un2
{
short c[7];//占十四个字节,按照short类型对齐,最大对齐数仍为8
int i;
};
printf("%d\n", sizeof(union Un1));//8
printf("%d\n", sizeof(union Un2));//16
}
五、案例——通讯录
1、main.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "contact.h"
void menu()
{
printf("***********************************************\n");
printf("******* 1. add 2. del ********\n");
printf("******* 3. search 4. modify ********\n");
printf("******* 5. show 6. sort ********\n");
printf("******* 0. exit ********\n");
printf("***********************************************\n");
}
int main()
{
int input = 0;
Contact contact;
InitContact(&contact);
do
{
system("cls");
menu();
printf("请输入您的选择:");
scanf("%d", &input);
switch (input)
{
case 1:
AddContact(&contact);
break;
case 2:
DelContact(&contact);
break;
case 3:
SearchContact(&contact);
break;
case 4:
ModifyContact(&contact);
break;
case 5:
ShowContact(&contact);
break;
case 6:
SortContact(&contact);
break;
case 0:
printf("正在退出系统!\n");
break;
default:
printf("输入有误!\n");
break;
}
system("pause");
} while (input);
return 0;
}
2、contact.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#define MAX_NAME 20
#define MAX_TEL 12
#define MAX_SEX 14
#define MAX_ADDR 30
typedef struct People
{
char name[MAX_NAME];
int age;
char telephone[MAX_TEL];
char sex[MAX_SEX];
char addr[MAX_ADDR];
}People;
typedef struct Contact
{
People data[100];
int count;
}Contact;
void InitContact(Contact* cp);
void AddContact(Contact* cp);
void DelContact(Contact* cp);
void SearchContact(const Contact* cp);
void ModifyContact(Contact* cp);
void ShowContact(const Contact* cp);
void SortContact(Contact* cp);
3、contact.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "contact.h"
void InitContact(Contact* cp)
{
assert(cp);
cp->count = 0;
}
void AddContact(Contact* cp)
{
assert(cp);
printf("请输入姓名:");
scanf("%s", cp->data[cp->count].name);
printf("请输入年龄:");
scanf("%d", &(cp->data[cp->count].age));
printf("请输入性别:");
scanf("%s", cp->data[cp->count].sex);
printf("请输入电话号码:");
scanf("%s", cp->data[cp->count].telephone);
printf("请输入地址:");
scanf("%s", cp->data[cp->count].addr);
cp->count++;
printf("添加成功!\n");
}
int FindName(const Contact* cp ,const char* name)
{
assert(cp);
int i = 0;
while (i < cp->count)
{
if (strcmp(cp->data[i].name, name) == 0)
{
return i;
}
i++;
}
return -1;
}
void DelContact(Contact* cp)
{
assert(cp);
char name[MAX_NAME];
if (cp->count == 0)
{
printf("通讯录为空!\n");
return;
}
printf("请输入需要删除联系人的姓名:");
scanf("%s", name);
int pos = FindName(cp, name);
if (pos == -1)
{
printf("未找到该联系人!\n");
}
else
{
while (pos < cp->count)
{
cp->data[pos] = cp->data[pos + 1];
pos++;
}
cp->count--;
printf("删除成功!\n");
}
}
void SearchContact(const Contact* cp)
{
assert(cp);
char name[MAX_NAME];
if (cp->count == 0)
{
printf("通讯录为空!\n");
return;
}
printf("请输入需要查找联系人的姓名:");
scanf("%s", name);
int pos = FindName(cp, name);
if (pos == -1)
{
printf("未找到该联系人!\n");
}
else
{
printf("%-20s\t %-5s\t %-15s\t %-13s\t %-30s\n",
"姓名", "年龄", "性别", "电话号码", "地址");
printf("%-20s\t %-5d\t %-15s\t %-13s\t %-30s\n",
cp->data[pos].name, cp->data[pos].age, cp->data[pos].sex,
cp->data[pos].telephone, cp->data[pos].addr);
}
}
void ShowContact(const Contact* cp)
{
assert(cp);
if (cp->count == 0)
{
printf("通讯录为空!\n");
return;
}
printf("%-20s\t %-5s\t %-15s\t %-13s\t %-30s\n",
"姓名", "年龄", "性别", "电话号码", "地址");
for (int i = 0; i < cp->count; i++)
{
printf("%-20s\t %-5d\t %-15s\t %-13s\t %-30s\n",
cp->data[i].name, cp->data[i].age, cp->data[i].sex,
cp->data[i].telephone, cp->data[i].addr);
}
}
void ModifyContact(Contact* cp)
{
assert(cp);
char name[MAX_NAME];
if (cp->count == 0)
{
printf("通讯录为空!\n");
return;
}
printf("请输入需要修改联系人的姓名:");
scanf("%s", name);
int pos = FindName(cp, name);
if (pos == -1)
{
printf("未找到该联系人!\n");
}
else
{
printf("请输入姓名:");
scanf("%s", cp->data[pos].name);
printf("请输入年龄:");
scanf("%d", &(cp->data[pos].age));
printf("请输入性别:");
scanf("%s", cp->data[pos].sex);
printf("请输入电话号码:");
scanf("%s", cp->data[pos].telephone);
printf("请输入地址:");
scanf("%s", cp->data[pos].addr);
printf("修改成功!");
}
}
int cmpName(const void* p1, const void* p2)
{
return strcmp(((People*)p1)->name, ((People*)p2)->name);
}
int cmpAge(const void* p1, const void* p2)
{
return (((People*)p1)->age - ((People*)p2)->age);
}
int cmpSex(const void* p1, const void* p2)
{
return strcmp(((People*)p1)->sex, ((People*)p2)->sex);
}
void SortContact(Contact* cp)
{
assert(cp);
if (cp->count == 0)
{
printf("通讯录为空!\n");
return;
}
int(*fun[3])(const void* p1, const void* p2) = { cmpName,cmpAge,cmpSex };
int i;
printf("输入0——按姓名排序;输入1——按年龄排序;输入2——按性别排序\n请输入:");
do
{
scanf("%d", &i);
if (!(i >= 0 && i <= 2))
printf("输入无效数字!请重新输入:");
} while (!(i >= 0 && i <= 2));
qsort(cp->data, cp->count, sizeof(People), fun[i]);
ShowContact(cp);
}