接上一节:文件输入输出工作原理和实例详解
结构体struct类型是C语言中的一种核心数据类型,也是C语言编程围绕的对象,联合union类型和枚举enum类型和结构体类型有着相似的语法结构,在类型声明上极为相似,形如struct/union/enum tagName,这里的tagName是一种标记名称,和前面的关键字组合才是一个数据类型,下面详细介绍这三种C语言特别的数据类型。
一、结构体数据类型(struct)
结构体(structure)的关键字是struct ,C语言的结构体主要的作用是:封装数据,也可以封装函数,它和OOP中的class相似,实际上使用结构体也可以实现class的使用方式。
1、创建结构体
(1)结构体声明
结构体的形式和联合或枚举有着相似的形式,第一步首先是类型声明,对这个的理解很重要,结构体声明实质是创建一种新的数据类型,仅仅是一个类型声明,没有创建变量,所以也没有实际占用内存空间。结构体声明和联合或枚举一样,是一种数据模板,OOP中的class类就是一种数据模板。
类型声明语法形如:struct+可选标记名{结构体成员列表};声明的结尾要加分号,结构体的类型名由struct和可选标记名组成,成员列表使用一般变量声明的形式,可在函数内部或外部进行声明。
结构体类型名需要两个标识符组成而成,可以使用typedef简化类型声明,这个看个人习惯,过多使用typedef可能会造成难于辨认数据类型,本人习惯使用全名的形式,下面是代码示例:
// 结构体类型声明:用于创建一个新的数据类型
// 语法:struct 结构体类型名(可选){成员列表};
// [struct User]是一个类型名和int、float这些类型一样
// 函数外部声明结构体类型
// 仅仅是创建一个新的类型,并没有分配到实际的内存空间
int; // 结构体类型声明类似于int;
double; // 类似于double;
struct User{
unsigned int age;
char *username;
};
// 使用typedef简化结构体类型名
typedef struct{ // 匿名形式
char *title;
} Rain;
// 或者使用
typedef struct User USER; // USER是struct User的类型别名
void print_11_01(void){
// 在函数内部声明一个结构体类型
struct Book{
char *title;
};
USER rain = {.age = 9, .username = "Elastic Search"};
}
(2)定义结构体变量
形如struct
user才是一个完整的结构体数据类型,创建变量的语法为struct user root,定义结构体变量即分配相应的内存空间,结构体变量类似于一个可以存储不同数据类型的数组。
定义结构体变量可以使用两种方式:在创建结构体类型时声明变量、使用匿名的结构体声明变量。
初始化结构体变量和数组类似,有两种方式:和数组初始化列表类似,使用逗号分隔,C99和C11支持另一种方式:使用指定初始化器,支持在初始化列表中任意位置初始化,相同成员初始化,后面的值覆盖掉前面的。
注意,结构体变量和简单类型的变量是类似的,可以使用任意存储类别的数据初始化,初始化静态结构体变量必须使用常量表达式。
访问结构体成员有两种方式,普通结构体变量使用.点运算符,例如obj.name,另一种方式,使用结构体指针,访问成员数据使用->箭头运算符,例如pt->name,示例代码如下:
// 定义结构体变量,结构体名Post是可选的,但是如果要重用该类型,则需要名字
struct Post{
char title[64];
char author[32];
char content[256];
} post1; // 1、声明结构体类型的时候同时创建变量
// 2、使用标准形式定义结构体变量
struct Post post2; // 该变量中的数据为初始化,是未知的
void print_11_02(void){
// 初始化结构体变量
// 1、使用初始化列表
struct Post post3 = {
"A half of bottle of wood",
"half of a yellow sun",
"Staying in the fog"
};
// 2、使用指定初始化器
struct Post post4 = {
.title = "Happy to death",
.author = "Carol",
.content = "Hugging and jumping"
};
// 3、默认初始化
struct Post post5; // 结构体的成员进行默认初始化
// 访问结构体变量中的成员数据
// 1、结构体变量访问使用点运算符
strcpy(post4.title, "The Price of Salt");
printf("%s\n", post4.title);
printf("%s\n", post4.author);
printf("%s\n", post4.content);
putchar('\n');
// 2、结构体指针访问使用箭头运算符->
struct Post * pst = &post3;
strcpy(pst->title, "The Old Man And The Sea");
strcpy(pst->author, "Hemingway");
printf("%s\n", pst->title);
printf("%s\n", pst->author);
printf("%s\n", pst->content);
}
2、复合结构体
(1)结构体数组
结构体和普通变量一样,结构体名不是指针,推荐使用指针方式操作结构体,指针比使用数据变量对象更方便更有效率。声明结构体数组和一般类型的数组也是一样的,主要是还要理解struct user这个才是结构体的完全类型名,使用结构体数组时候要注意,数组变量其数据对象默认存储在栈内存中,栈空间有限,大结构体数组会占据过多的栈内存,推荐使用malloc分配动态内存空间。
访问数组中的结构体成员同样也是基本的两种方式:数据对象方式访问点操作符,以及数组指针方式箭头操作符,示例代码如下:
// 定义结构体变量,结构体名Post是可选的,但是如果要重用该类型,则需要名字
struct Post{
char title[64];
char author[32];
char content[256];
} post1; // 1、声明结构体类型的时候同时创建变量
// 2、使用标准形式定义结构体变量
struct Post post2; // 该变量中的数据为初始化,是未知的
void print_11_02(void){
// 初始化结构体变量
// 1、使用初始化列表
struct Post post3 = {
"A half of bottle of wood",
"half of a yellow sun",
"Staying in the fog"
};
// 2、使用指定初始化器
struct Post post4 = {
.title = "Happy to death",
.author = "Carol",
.content = "Hugging and jumping"
};
// 3、默认初始化
struct Post post5; // 结构体的成员进行默认初始化
// 访问结构体变量中的成员数据
// 1、结构体变量访问使用点运算符
strcpy(post4.title, "The Price of Salt");
printf("%s\n", post4.title);
printf("%s\n", post4.author);
printf("%s\n", post4.content);
putchar('\n');
// 2、结构体指针访问使用箭头运算符->
struct Post * pst = &post3;
strcpy(pst->title, "The Old Man And The Sea");
strcpy(pst->author, "Hemingway");
printf("%s\n", pst->title);
printf("%s\n", pst->author);
printf("%s\n", pst->content);
}
(2)结构体嵌套
结构体嵌套在C语言中显得尤为重要,C语言特别重视数据结构算法,在数据结构和算法中经常会用到结构体嵌套,结构体嵌套有如下三种情形:
A嵌套其它结构体B:数据对象的嵌套形式,A直接嵌套B,但是B需要先于A声明;指针方式,必要时需要先进行结构体类型声明。
结构体A/B互相声明:使用指针方式,必要时需要先进行结构体类型声明。
A嵌套自身结构体A:使用指针方式,必要时需要先进行结构体类型声明。
下面是结构体嵌套的代码实例:
// 结构体的嵌套要注意,所有的标识符都要求先声明后使用,例如
//typedef struct P{
// struct P p; // 错误,因为struct P此时还没确定好内存空间
// PT pp; // 错误,PT标识符之前还没声明
//} PT;
// 结构体的声明或别名声明的方式要尽量简洁
// 1、一个结构体嵌套其它结构体,A嵌套B,B需要先于A声明(指针方式可不用)
struct B{
char *name;
};
struct A{
char *name;
struct B b; // 数据对象方式
struct B *bp; // 指针方式
};
// 2、结构体互相嵌套,使用指针方式,必要时进行结构体类型声明
// 类型声明,告诉编译器存在该类型,让编译器在本文件寻找真实的类型模板
// 也可以不书写类型声明,但书写会更清晰,所以使用一个结构体的顺序可以是这样:类型声明 -> 类型模板声明 -> 类型使用
struct C;
struct D;
struct D{
char *name;
struct C *cp;
};
struct C{
char *name;
struct D *dp;
};
// 3、结构体嵌套自身,不能使用数据对象的方式,使用指针的方式
// 结构体嵌套自身并不是嵌套自己,意思是嵌套和自己数据类型相同的变量
// 在数据结构中经常用到,要理解好这种嵌套的层次关系
struct E;
struct E{
char *name;
// struct E e; // 错误,编译器不能确定e的内存空间
struct E *ep; // 嵌套自身的数据类型变量
};
void print_11_04(void){
struct C c;
struct D d;
c.name = "JavaScript";
c.dp = &d;
d.name = "Python";
d.cp = &c;
printf("%s\n", c.name);
printf("%s\n", c.dp->name);
}
// 必要时需要预先进行各结构体的类型声明
// 类型声明是告诉编译器存在指定的数据类型,结构体的数据类型全名是struct struct_name
struct user; // 或typedef struct user U;
struct post; // 或typedef struct post P;
// 结构体嵌套
struct user{
unsigned int age;
char *username;
struct user *friend;
// 使用struct user friend定义变量错误,因为编译器在给结构体分配内存的时候无法确定friend的内存大小
// 使用指针可以,因为指针是一个unsigned int类型的地址,一般都是占4字节
// 结构体嵌套推荐使用指针方式进行嵌套
struct post *post; // 变量名post可以和结构体名同名,因为结构体的全名应该是struct post
};
struct post{
char *title;
char *content;
// struct user u; // 可以使用此方式,因为user先于post声明
};
void print_11_05(void){
struct post post = {.title = "Half of Yellow Sun", .content = "Rain or Cloud, it doesn't matter."};
struct user friend = {22, "Carol", NULL, NULL};
struct user user;
user.age = 99;
user.username = "Hemingway";
user.friend = &friend;
user.post = &post;
printf("%s\n", user.username);
printf("%u\n", user.age);
printf("%s\n", user.friend->username);
}
3、结构体与函数
(1)结构体与函数参数
在函数参数中传递结构体成员数据和之前说明的普通类型数一样,传递结构体地址,则复制结构体指针,处理效率更高,可以使用const限制修改数据,传递结构体数据对象,则会复制原结构体数据,处理效率低,但默认不会修改原始结构体数据,实例代码如下:
// 函数参数传递与结构体
struct box{
float width;
float height;
};
// 1、传递结构体成员数据,这里是复制值传递
float get_area(float width, float height);
// 2、传递结构体数据对象本身,复制本身数据对象本身
void print_box_info(struct box);
// 3、传递结构体指针,复制指针值传递
void print_box(const struct box *);
void print_11_06(void){
struct box box;
box.width = 12.6f;
box.height = 4.6f;
printf("address: %#x\n", &box); // 0x28fea4
print_box_info(box);
float area = get_area(box.width, box.height);
printf("area: %.2f\n", area);
print_box(&box);
}
float get_area(float width, float height){
return width * height;
}
void print_box_info(struct box box){
printf("width: %f\n", box.width);
printf("height: %f\n", box.height);
printf("address: %#x\n", &box); // 0x28fe80
}
void print_box(const struct box *bp){
printf("width: %f\n", bp->width);
printf("height: %f\n", bp->height);
printf("address: %#x\n", bp); // 0x28fea4
}
(2)结构体赋值
结构体数据对象之间的赋值是通过值复制的形式,赋值后是两个不同的数据对象,示例代码如下:
// 结构体赋值
// 结构体数据对象互相赋值是通过值复制的形式,赋值后产生的是两个数据相同,但内存不同的对象
struct data{
int count;
char name[16];
};
void print_11_07(void){
struct data data1;
struct data data2;
data1.count = 33;
strcpy(data1.name, "Actually");
data2 = data1; // 值复制,分别复制每个成员的数据值
printf("struct address: %#x %#x\n", &data1, &data2); // 地址不同
printf("count address: %#x %#x\n", &data1.count, &data2.count); // 地址不同
printf("name address: %#x %#x\n", data1.name, data2.name); // 地址不同
}
(3)函数返回值与结构体
函数的返回值在根本上还是值复制,对于普通变量,直接复制数据对象返回,对于指针,复制指针值返回,C++中有引用,引用是变量的别名,返回时复制变量变量名返回,示例代码如下:
// 函数返回值与结构体
struct book{
char *title;
char *charactor;
};
struct book create_book();
struct book * buy_book();
void print_11_08(void){
struct book book1 = create_book();
printf("book address: %#x\n", &book1); // 0x28fea8
printf("%s\n", book1.title);
printf("%s\n", book1.charactor);
putchar('\n');
struct book *book2 = buy_book();
printf("book address: %#x\n", book2); // 0x9f1680
printf("%s\n", book2->title);
printf("%s\n", book2->charactor);
free(book2);
}
struct book create_book(){
struct book book;
book.title = "The Price of Salt";
book.charactor = "Carol";
printf("book address: %#x\n", &book); // 0x28fe78
return book; // 复制结构体数据对象返回给主调函数
}
struct book * buy_book(){
// struct book book;
// book.title = "The Price of Salt";
// book.charactor = "Carol";
// return &book;
// 以上方式错误,因为book数据对象默认保存在栈内存中,执行完buy_book函数就会出栈销毁,返回的数据对象已不存在
// 使用malloc可以保证函数返回数据对象依然存在,但是返回的指针变量仍然是复制返回给主调函数
struct book *pbook = (struct book *)malloc(1 * sizeof(struct book));
pbook->title = "The Old Man And The Sea";
pbook->charactor = "Santiago";
printf("book address: %#x\n", pbook); // 0x9f1680
return pbook;
}
(4)结构体与字符串
字符串的表示形式可以使用字符数组和字符指针,之前已经说了这两种形式的区别,程序中出现的字符串字面量会被保存在静态区中,该字符常量不允许修改。对于字符数组的形式实际上是从静态区中复制对应的字符常量到栈区中,这时该字符实际可修改,但是指针不可更改。字符指针的形式,是复制静态区字符常量的地址,其指针值可改变,但是数据不可更改,实例代码如下:
// 结构体与字符串
struct dog{
char *name; // 一个指针,存储的一个4字节空间的地址值
char description[256]; // 一个字符数组,具有256字节的存储空间,每字节空间地址已固定
};
void print_11_09(void){
char *value1 = "String"; // 将静态区中的字符串"String"的地址复制给value1,value1保存的值可更改
char value2[16] = "String"; // 将静态区中的字符串"String"值复制给value2,value2的地址值不可更改
// value2 = value1; // 错误,因为value2存储的地址已经固定
struct dog dog;
dog.name = "Pure";
// dog.description = "A pure dog called Pure";
// 实质是将字符串的地址值复制给description,但是description保存的地址值已经固定了,不能更改
// 此时应该使用strcpy复制字符串值给对应的字符数组空间
strcpy(dog.description, "A pure dog called Pure");
}
(5)结构体复合字面量
C99支持结构体字面量,和数组字面量是类似的,下面是具体的实例:
// 结构体复合字面量
struct song{
char *name;
float price;
};
void play_song(struct song);
void record_song(const struct song *);
void print_11_10(void){
// 结构体字面量(struct name){成员数据}
struct song song1 = (struct song){"shall we talk", 10.5f};
play_song((struct song){"Ten Years", 11.99f});
record_song(&(struct song){"Comes and Goes", 9.99f});
}
void play_song(struct song song){
printf("playing: %s %f\n", song.name, song.price);
}
void record_song(const struct song *sp){
printf("recording: %s %f\n", sp->name, sp->price);
}
(6)结构体的伸缩数组成员
C99支持伸缩数组成员,结构体伸缩数组的特点是:该数组不会立即存在,即默认未分配内存空间,不计入结构体的内存空间,需要手动使用malloc进行额外分配,可以使用该数组创建合适的数据结构。
伸缩数组成员需要满足:结构体至少有一个数据成员;伸缩数组成员是最后一个成员;数组方括号为空[]。
伸缩数组成员的限制有:这种结构体变量之间不能进行赋值或拷贝(但是可以使用memcpy);不能作为值传递(例如函数参数);不能作为结构体成员或数组成员,实例代码如下:
// C99结构体可伸缩数组成员
struct clothes{
char *name; // 4字节空间大小,至少有一个结构体成员
int button[]; // 默认未分配空间,最后一个结构体成员
};
void print_11_11(void){
printf("%u\n", sizeof(struct clothes)); // 结构体空间默认为4字节
struct clothes *pc = (struct clothes *)malloc(sizeof(struct clothes) + 2 * sizeof(int));
printf("%u\n", sizeof(struct clothes)); // 伸缩数组成员不计入结构体的内存空间
pc->name = "Size";
pc->button[0] = 99;
pc->button[1] = 88;
printf("%u\n", sizeof(*pc)); // 4字节
printf("%d\n", pc->button[0]);
}
(7)C11匿名结构体
匿名结构体指的是在结构体中直接定义的结构体,例如struct fam{ struct{char *name} },访问时直接使用外层结构体访问匿名结构体成员,例如fam.name,示例代码如下:
// 定义匿名结构体
struct root{
char *username;
struct{ // 匿名结构体
char *avator;
char *password;
};
};
void print_11_12(void){
struct root *proot = (struct root *)malloc(sizeof(struct root));
proot->username = "Normal";
proot->avator = "Monster"; // 直接访问匿名结构体成员
proot->password = "123456";
printf("%s\n", proot->username);
printf("%s\n", proot->avator);
printf("%s\n", proot->password);
}
(8)结构体与内存
将结构体按二进制保存进文件时,不同系统器存储是不同的,不同编译器设置其存储也不尽相同。另外,结构体在内存空间上不是理所当然按顺序存储的,编译器会将结构体中的数据进行字节对齐,默认按照最大字节数(数据成员)的倍数对齐,示例代码如下:
// 结构体的字节对齐:按照结构体成员最大字节数的倍数对齐:如果最小对齐数可以保存所需数据则使用
struct node{
int a; // 4 8(4+1+2=7,使用8字节存储)
char b; // 1
short c; // 2
};
void print_11_13(void){
printf("%d\n", sizeof(struct node)); // 8字节
struct node *node = (struct node *)malloc(sizeof(struct node));
node->a = 11;
node->b = 'B';
node->c = 6;
printf("%d\n", *((int *)node));
printf("%c\n", *(char *)((int *)node + 1));
printf("%hd\n", *(short *)((char *)((int *)node + 1) + 2));
}
二、联合类型(union)
联合类型的作用是:多种不同类型的数据根据不同的状态而存在,即不同的状态对应不同的数据形式。
声明联合类型使用union关键字,和结构体类似,联合类型和下面讨论的枚举类型很多使用方式和结构体都是类似的。其中要注意,联合类型变量的字节大小为最大成员的字节大小,一次只能存取一个成员,初始化同样可以使用另一个联合变量进行赋值、使用初始化列表和指定初始化器,成员访问同样有变量方式和指针方式。和匿名结构一样,也有匿名联合,访问上同样相似,实例代码如下:
// union联合类型
// 联合类型声明
union rand{
int age;
char name[32]; // 32字节
double size;
};
// 联合类型的字节大小为:最大字节数的成员
// 联合变量一次只能存取一个成员
struct U{
int status; // 根据status使用struct P p或struct O o
union {
struct P{int age;} p;
struct O{char *name;} o;
};
};
void print_11_14(void){
printf("%u\n", sizeof(union rand)); // 输出:32
union rand rand; // 创建联合类型变量
rand.size = 3.14;
rand.age = 88;
printf("%.2f\n", rand.size);
union rand rand1 = {55}; // age = 55 // 使用初始化列表
printf("%d\n", rand1.age);
rand1 = rand; // 使用其它联合类型变量赋值
printf("%.2f\n", rand1.size);
union rand rand2 = {.name = "high voice"}; // 使用指定初始化器
union rand *pr = &rand2; // 使用指针访问数据,使用箭头运算符
printf("%s\n", pr->name);
}
三、枚举类型(enum)
枚举的类型的作用主要是提高程序的可读性,它实际是一个int型数组,但是比使用单纯的int值更可友好。枚举变量同样是一个int型值,称为枚举符,其成员都是使用标记符号表示,可进行++或—操作,但是C++不允许此操作。枚举变量值默认从0开始,可进行显示赋值,示例代码如下:
// enum枚举类型
// 枚举类型使用符号名称来表示成员变量/整型变量(类似于常规的整型数组)
// 声明一个枚举类型,每个枚举成员符号成为一个int型常量
// 虽然枚举常量是一个int型变量,但是使用时还是不要使用int字面量赋值
enum background{
// 显式赋值
red = 9, green, blue
};
void print_11_15(void){
enum background color = blue;
printf("%d\n", color);
printf("%d\n", --color);
printf("%d\n", --color);
}
C语言中有两种命名空间(即名称空间),一种为变量的命名空间,另一种是标记的命名空间,在同一个作用域中,这另种命名空间是区分开的,其中标记命名就是结构标记、联合标记和枚举标记,同一个作用域中允许出现标记名和变量名同名,在上面的代码中都有标记名和变量名同名的情况,但是这种在C++中是不允许的。
typedef定义类型别名
typedef和#define类型,同样可以定义类型的别名,但是typedef只能用于定义类型别名,并且由编译器处理,而不是预处理器,受作用域影响,一个作用域中的typedef定义只能用于其所在的作用域,#define和typedef定义类型别名的实例代码如下:
// typedef定义类型别名
// 使用typedef和#define定义类型别名
struct lang{
int id;
char *name;
};
typedef struct lang Lang;
#define LANG struct lang
typedef unsigned char byte;
#define BYTE unsigned char
typedef unsigned int m_size;
#define SIZE unsigned int
typedef void (Func)(void); // 函数类型别名
typedef int * (*PFArray[10])(int, char*); // 函数指针数组
void print_11_16(void){
SIZE a = 8;
byte b = 99;
printf("%u\n", a);
printf("%hu\n", b);
}