结构简介
什么是结构?为什么要用结构? ??编写代码时,最重要的步骤之一是选择表示数据的方法。在许多情况下,基本的变量类型和数组还步够,为此 C 提供了结构变量来表示复杂的数据。例如,对于一本书,可能需要书名、作者、价格等属性。
使用结构需要掌握的三个技巧:
- 为结构建立一个格式或样式;
- 声明一个合适该样式的变量;
- 访问结构变量的各个部分。
建立结构声明
结构声明描述了一个结构的组织布局。
struct book {
char title[100];
char author[80];
float value;
};
如上所示的声明描述了一个由两个字符数组和一个 float 类型变量组成的结构。注意,该声明仅仅只是描述了该结构由什么组成,并未创建实际的数据对象。 ??接下来分析一下结构声明的细节。 ??struct 是声明结构的关键字,该关键字表明跟在其后面的是一个结构。 ??book 是一个可选的标记,该标记表示结构的名称(该例中是 book)。关于标记,会在定义结构变量处讲解。 ??{ }; 花括号括起来的是结构成员列表,每个成员都是可以是任意一种 C 的数据类型,甚至可以是其他结构。注意,**右括号后面一定要有 ****;** !!
结构声明可以放在所有函数的外部,也可以放在一个函数定义的内部。如果把结构声明置于一个函数的内部,它的标记(如本例中的 book),只能限于该函数的内部使用。如果把结构声明置于函数的外部,那么该声明之后的所有函数都可以使用它的标记(如本例中的 book)。
结构变量的定义和初始化
定义结构变量
结构有两层含义,一层是上面讲过的结构布局,另一层则是这里要讲的定义结构变量。以上面声明的结构 book 为例,定义一个变量 library 的代码如下所示:
struct book library;
这行代码表明,用 book 结构来为 library 变量分配空间,包括了一个还有 100 个元素的 char 数组,一个包含 80 个元素的 char 数组,和一个 float 类型的变量。 ??在结构变量 library 的声明中,struct book 的作用相当于 int 或者 float 。和基本类型变量的声明一样,可以定义多个结构变量,甚至是结构指针。
struct book doyle, panshin, *ptbook;
从本质上看,book 结构声明创建了一个名为 struct book 的新类型。就计算机而言,struct book library; 是以下声明的简化:
struct book {
char title[100];
char author[80];
float value;
} library;
所以,建立结构声明的过程和定义结构变量的过程可以组合成一个步骤。组合后的结构声明和结构变量的定义可以不使用结构标记。
struct {
char title[100];
char author[80];
float value;
} library;
关于可选的结构标记是否可以省略的问题。 如果是只需要定义有限个变量,可以将结构声明和变量定义合并到一起,这样结构模板只会被使用一次,此时可以省略结构标记。然而,如果打算多次使用结构模板,就必须使用带标记的形式,或者使用 typedef (这里不做介绍)。
初始化结构变量
和定义基本数据类型变量一样,声明结构变量的同时可以对其进行初始化。 初始化结构变量使用花括号中括起来的初始化列表进行初始化,各初始化项用逗号分隔,也可以用结构体变量来初始化。 PS:使用结构体变量来初始化的变量不能作为全局变量。
struct book library = {
"The Pious Pirate and the Devious Damsel",
"Renee Vivotte",
1.95
};
struct book l1 = library;
结构成员的使用
对于结构变量来说,使用结构成员运算符 —— 点(.)访问结构中的成员。例如使用 library.value 访问 library 中的 value 部分。 ??对于指向结构变量的指针来说,使用 -> 运算符访问结构中的成员,或者(*指针变量).成员 。
struct book *ptbook = &library;
ptbook->value;
(*ptbook).value;
结构体的赋值
同一个结构体的变量之间可以直接赋值。
struct book b1 = {
"C Primer",
"Stephen Prata",
68.3
};
struct book b2 = b1;
但如果结构体的成员涉及到动态内存分配的话会有一些问题。
数组成员的赋值
其实在对结构体变量赋值时,会发现有一个很奇妙的事情:无法将一个数组直接赋值给另一个数组,但是如果结构体的成员中包含数组,在将一个结构体变量赋值给另一个结构体变量的时候,则会完成数组的赋值。
浅拷贝
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct book {
char *title;
char *author;
float value;
};
void show_book(const struct book* ptr_book) {
printf("This book\'s name is %s, the author is %s, the value is %lf.\n",
ptr_book->title, ptr_book->author, ptr_book->value);
printf("The struct value\'s address is %p.\n", ptr_book);
printf("The title\'s address is %p.\n", ptr_book->title);
printf("The author\'s address is %p.\n", ptr_book->author);
printf("-------------------------------------------------------------\n");
}
void free_book(struct book* ptr_b) {
if (ptr_b == NULL)
return;
if(ptr_b->title != NULL) {
free(ptr_b->title);
ptr_b->title = NULL;
}
if (ptr_b->author != NULL) {
free(ptr_b->author);
ptr_b->title = NULL;
}
}
int main(int argc, char** args) {
struct book b1 = {.value = 68.3};
b1.title = (char*) malloc(9 * sizeof (char));
strcpy(b1.title, "C Primer");
b1.author = (char*) malloc(13 * sizeof (char));
strcpy(b1.author, "Stephen Prata");
show_book(&b1);
struct book b2 = b1;
show_book(&b2);
strcpy(b2.author, "You Ka");
show_book(&b2);
show_book((&b1));
free_book(&b1);
return 0;
}
This book's name is C Primer, the author is Stephen Prata, the value is 68.3.
The struct value's address is 000000000061FE00.
The title's address is 0000000000A11420.
The author's address is 0000000000A11440.
-------------------------------------------------------------
This book's name is C Primer, the author is Stephen Prata, the value is 68.3.
The struct value's address is 000000000061FDE0.
The title's address is 0000000000A11420.
The author's address is 0000000000A11440.
-------------------------------------------------------------
This book's name is C Primer, the author is You Ka, the value is 68.3.
The struct value's address is 000000000061FDE0.
The title's address is 0000000000A11420.
The author's address is 0000000000A11440.
-------------------------------------------------------------
This book's name is C Primer, the author is You Ka, the value is 68.3.
The struct value's address is 000000000061FE00.
The title's address is 0000000000A11420.
The author's address is 0000000000A11440.
-------------------------------------------------------------
可以看到,b2 = b1 其实是将 b1 成员的值复制给 b2,b2 的 title 成员和 author 成员的值和 b1 一模一样,即只复制指针本身,而不复制指针指向的模板。因此结构体赋值,采用的类似于 memcpy() 这种形式。 ?
在结构体成员中有指针,并且会用到动态内存分配的情况下,就会产生一些问题。例如,在本例的结构体中就会存在一下问题:
- 同一地址多次使用 free 释放,导致程序崩溃。结构体变量 b1 和 b2 的 title 和 author 指针都指向一个 malloc 分配的地址,malloc 分配的地址是需要 free 释放的。如果使用 free 释放 b1 的 title 和 author ,此时 b2 的 title 和 author 就会成为野指针,后续使用 b2 时就会出现一些不可预见的问题。例如,free b1 之后,再用 free 释放 b2 就会造成程序崩溃。
- 修改某一个变量指针成员指向的内容,会导致其他变量指向的内容一起改变。结构体变量 b1 和 b2 的 title 和 author 指针都指向同一个地址,修改任意一个变量指向的内容,另一个变量的内容也会被修改。
- malloc 申请的内存没有被 free 释放,造成内存泄漏。如果在将 b1 赋值给 b2 之前,b2 的 title 和 author 指针已经指向一个 malloc 申请的内存,并且没有其他变量的成员指针指向该内存。如果直接将 b1 赋值给 b2 的话,会造成之前 b2 指向的内存没有被回收。
深拷贝
解决办法:不直接使用赋值运算符对结构体进行赋值,编写代码使两个结构体中指针地址不同,但是指向的内容一致。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct book {
char *title;
char *author;
float value;
};
void show_book(const struct book* ptr_book) {
printf("This book\'s name is %s, the author is %s, the value is %lf.\n",
ptr_book->title, ptr_book->author, ptr_book->value);
printf("The struct value\'s address is %p.\n", ptr_book);
printf("The title\'s address is %p.\n", ptr_book->title);
printf("The author\'s address is %p.\n", ptr_book->author);
printf("-------------------------------------------------------------\n");
}
void free_book(struct book* ptr_b) {
if (ptr_b == NULL)
return;
if(ptr_b->title != NULL) {
free(ptr_b->title);
ptr_b->title = NULL;
}
if (ptr_b->author != NULL) {
free(ptr_b->author);
ptr_b->title = NULL;
}
printf("Free Memory Success!\n");
}
void copy_book(struct book * dest, const struct book * src) {
if(dest != NULL)
free_book(dest);
dest->title = malloc(strlen(src->title) + 1);
dest->author = malloc(strlen(src->author) + 1);
strcpy(dest->title, src->title);
strcpy(dest->author, src->author);
dest->value = src->value;
}
int main(int argc, char** args) {
struct book b1 = {.value = 68.3};
b1.title = (char*) malloc(9 * sizeof (char));
strcpy(b1.title, "C Primer");
b1.author = (char*) malloc(13 * sizeof (char));
strcpy(b1.author, "Stephen Prata");
show_book(&b1);
struct book b2;
copy_book(&b2, &b1);
show_book(&b2);
strcpy(b2.author, "You Ka");
show_book(&b2);
show_book((&b1));
free_book(&b1);
free_book(&b2);
return 0;
}
D:\MyNote\C++\source\cmake-build-debug\C_Struct2.exe
This book's name is C Primer, the author is Stephen Prata, the value is 68.3.
The struct value's address is 000000000061FE00.
The title's address is 0000000000BA1420.
The author's address is 0000000000BA1440.
-------------------------------------------------------------
Free Memory Success!
This book's name is C Primer, the author is Stephen Prata, the value is 68.3.
The struct value's address is 000000000061FDE0.
The title's address is 0000000000BA1460.
The author's address is 0000000000BA1480.
-------------------------------------------------------------
This book's name is C Primer, the author is You Ka, the value is 68.3.
The struct value's address is 000000000061FDE0.
The title's address is 0000000000BA1460.
The author's address is 0000000000BA1480.
-------------------------------------------------------------
This book's name is C Primer, the author is Stephen Prata, the value is 68.3.
The struct value's address is 000000000061FE00.
The title's address is 0000000000BA1420.
The author's address is 0000000000BA1440.
-------------------------------------------------------------
Free Memory Success!
Free Memory Success!
结构体的大小(笔试考点)
之前学过的基本数据类型,如 char、int、double,它们的大小都是固定的,在 64 位系统中,char 占 1 字节,int 占 4 字节,double 占 8 字节。而结构体的大小并不固定,因为结构体的成员类型和数目是程序员自定义的。和基本数据类型一样,我们可以用 sizeof 这个运算符来获取结构体占据的内存大小。
struct Cookie
{
int a;
short b;
short c;
} cookie;
int main()
{
printf("%d\n", sizeof cookie);
return 0;
}
上面代码输出的结果是 8,正好是 1 个 int 类型,2 个 short 类型的大小之和。 那么结构体的大小是各成员大小之和吗?我们把 Cookie 稍微变化一下。
struct Cookie
{
int a;
short b;
char c;
} cookie;
将 Cookie 的第3个成员变为 char,运行程序发现输出的结果依旧是 8,而不是 4+3+1 = 7。由此可知结构体的大小并不是简单的各成员大小之和。 在计算结构体大小的时候,需要满足以下规则。
对齐原则
- 结构体成员的地址需要根据对齐数进行字节对齐。对齐数=编译器默认的对齐数与该成员类型的大小中的较小值。其中编辑器默认的对齐数由编辑器决定,Linux 环境下是 4,这个值也可以由程序员指定。
- 结构体总大小为其最大成员大小的整数倍。
示例1:编译器默认对齐数
注:以下程序是在默认对齐数为 4 的编译器下运行。如果是在像 VS 这种默认对齐数为 8 的编译器下运行结果会有所不同。
struct Cookie1
{
int a;
short b;
char c[10];
} cookie1;
struct Cookie2
{
short b;
int a;
char c[10];
} cookie2;
首先看 Cookie1 的大小。Cookie1 的成员中 int 占4字节,short 占2字节,char 占1字节,因此最大成员是占4字节的 int(对于数组,考虑数组元素的大小而不是数组的大小),因此最后计算的 Cookie1 的大小一定是4的倍数。对于第一个成员 a,占4字节。对于第二个成员 b,大小是2字节,小于编译器默认的4字节,因此 b 的对齐数为 2,而 b 前面的成员已经占据的内存大小为 4,是2字节的倍数,因此 b 不需要字节补齐,此时 Cookie1 的内存大小是 4+2=6 字节。对于第三个成员 c,对齐数是 1,前面成员以及占据的内存是 6,是对齐数1的倍数,因此不需要字节补齐。此时 Cookie1 的内存大小是 4+2+10=16,正好是其最大成员 a 大小的倍数,因此 Cookie1 的内存大小是 16。 再来看 Cookie2 的大小。Cookie2 的成员中依旧是 int 为最大成员,因此最后计算的 Cookie2 的大小也一定是 4 的倍数。对于第一个成员 b,占2字节。对于第二个成员 a,对齐数是 4,而 a 前面已占据的内存是 2,不是 4 的倍数,因此需要对前面的内存进行补齐,空出 2 字节空间,此时 Cookie2 的大小为 2(存储 b) + 2(空出) + 4(存储 a) = 8 字节。对于成员 c,不需要字节补齐。此时 Cookie2 的大小为 2+2+4+10 = 18,不是 4 的倍数,因此最后还需要空出 2 字节进行字节补齐,使得 Cookie2 的大小为 4 的倍数,故 Cookie2 的大小为 2+2(空出)+4+10+2(空出)=20。
struct Cookie3
{
int a;
double b;
short c;
} cookie3;
Cookie3 最大成员为 double 类型,因此 Cookie 的大小一定是 8 的倍数。第一个成员 a,占 4 字节内存。第二个成员 b,对齐数为 min(编译器默认对齐数(4), double 大小(8)),即 b 的对齐数为 4,而 b 前面已占据的内存大小为 4,不需要补齐字节。第三个成员 c,对齐数为 2,之前的成员占据的内存为 4 + 8 = 12,是 2 的倍数,不需要补齐。此时 Cookie3 的大小为 4+8+2 = 14,不是 8 的倍数,需要补齐2字节使其成为 8 的倍数,故 Cookie3 的大小为 16 字节。
示例2:程序员指定对齐数
可以通过 #param pack(n) 来指定 n 为编译器默认的对齐数。 PS:n 的值可以是 1、2、4、8。
#pragma pack(4)
struct Cookie3
{
int a;
double c;
short b;
} cookie3;
#pragma pack(8)
struct Cookie4
{
int a;
double c;
short b;
} cookie4;
为什么要字节对齐?
- 为了 CPU 访问数据的高效率。如果变量的地址不对齐,那么 CPU 读取结构体就需要对结构体成员进行重复的访问,然后组合得到数据。而如果变量在自然对齐位置上,则只要一次就可以取出数据。
- 在有的硬件平台中,计算机在内存读取数据时,只能在规定的地址处读数据,而不是内存中任意地址都是可以读取。因此字节对齐尤为重要。
为什么要结构体大小?
通过示例1,我们可以看到存储相同的成员的结构体,在结构体声明中成员的顺序不同,结构体的大小是不同的。了解了结构体大小的计算,我们可以在声明结构体的时候声明一个占内存最小的结构体,这样可以减少内存开销。 ?
指向结构的指针
为什么使用指向结构的指针?
- 指向结构的指针通常比结构本身容易操控。
- 一些早期的 C 视线中,结构不能作为参数传递给函数,但是可以传递指向结构的指针。
- 传递指针通常更有效率。
- 一些用于表示数据的结构中包含指向其他结构的指针。
向函数传递结构
和基本类型数据相同,可以向函数传递结构变量和指向结构的指针。
结构和结构指针作为函数参数的优缺点:
| 结构作为参数 | 指针作为参数 |
---|
优点 | 1. 函数处理的是原始数据的副本,保护了原始数组; | | 2. 代码风格更为清晰。 | 1. 无论是以前还是现在的实现都可以使用这种方法; | | 2. 执行起来很快,只需要传递一个地址。 | | | 缺点 | 1. 较老版本的实现可能无法处理这样的代码; | | 2. 传递结构浪费时间和存储空间。 | 无法保护原始数据,但 ANSI C 新增的 const 限定符解决了这个问题。 | |
结构和结构指针作为函数参数的选择: ??通常,我们为了追求效率会使用结构指针作为函数参数,如需防止原始数据被意外修改,可以使用 const 限定符。而按值传递结构是处理小型结构最常用的方法。
|