C语言学习教程(六)

C语言学习教程(六):本系列教程第17-20章。

17-C Structures

C 数组允许你定义可存储相同类型数据项的变量,结构体(structure)是 C 编程中可用的另一种用户自定义的数据类型,它允许你组合不同类型数据项的变量。

结构体通常用于表示一条记录,假设你想记录图书馆中书本的动态信息。你可能需要跟踪每本书的以下属性:

  • Title
  • Author
  • Subject(主题)
  • Book ID

(1)定义结构体

要定义结构体,必须使用struct语句。 struct 语句定义了一个包含多个成员的新的数据类型,struct 语句的格式如下:

1
2
3
4
5
6
struct [structure tag]{
    member definition;
    member definition;
    ...
    member definition;
} [one or more structure variables];

或者简写为:

1
2
3
struct [structure tag]{
	member-list;
} variable-list;

structure tag(结构体标签)是可选的。

member definition(成员定义)或写为member-list,它是标准的普通变量定义,比如int i;或者float f;,或者其他有效的变量定义。

one or more structure variables(结构体变量名)或写为variable-list,定义在结构体的末尾,最后一个分号之前,你可以指定一个或多个结构体变量名称。

下面是声明 Books 结构体的方式:

1
2
3
4
5
6
struct Books{
	char title[50];
    char author[50];
    char subject[100];
    int book_id;
} book;

你可以使用struct关键字来定义结构体。需要注意的是,在定义结构体时,structure tagmember-listvariable-list这三部分至少要出现两个。下面是几个定义实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//1.声明一个拥有3个成员的结构体数据类型simple。
//成员分别为整型的a,字符型的b和双精度的c。(常用方式)

struct simple{
    int a;
    char b;
    double c;
};

//声明数据类型为simple的结构体变量test1、test2、test3。
struct simple test1, test2[20], *test3; 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//2.声明一个拥有3个成员的结构体数据类型。
//成员分别为整型的a,字符型的b和双精度的c。
//需要注意,该结构体没有structure tag,直接声明了这个结构体就是结构体变量test。

struct 
{
	int a;
    char b;
    double c;
} test;

(2)结构体变量的初始化与使用

和其它类型变量一样,结构体变量可以在定义时指定初始值。

要访问任意一个结构体成员,我们需要使用成员访问运算符.,成员访问运算符被编码为结构体变量名称和我们希望访问的结构成员之间的句点。

下面的实例演示了结构体的用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

//定义并初始化数据类型为Books的结构体变量book。
struct Books{
	char title[50];
	char author[50];
	char subject[100];
	int book_id;
} book = {"C programing", "Nuha Ali", "C Programming Tutorial", 6495407};

int main(){
	//输出book的信息
	printf("Book title : %s\n", book.title);
	printf("Book author : %s\n", book.author);
	printf("Book subject : %s\n", book.subject);
	printf("Book book_id : %d\n", book.book_id);

	return 0;
}

运行结果:

1
2
3
4
5
6
$ gcc -o test1 test1.c
$ ./test1
Book title : C programing
Book author : Nuha Ali
Book subject : C Programming Tutorial
Book book_id : 6495407

下面是使用结构体变量的另一个示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <string.h>


//声明一个拥有4个成员的结构体数据类型Books
struct Books{
	char title[50];
	char author[50];
	char subject[100];
	int book_id;
};


int main(){
    //声明数据类型为Books的结构体变量Book1和Book2。
	struct Books Book1;
	struct Books Book2;

	//设置Book1和Book2的基本信息
	strcpy(Book1.title, "C programing");
	strcpy(Book1.author, "Nuha Ali");
	strcpy(Book1.subject, "C Programming Tutorial");
	Book1.book_id = 6495407;

	strcpy(Book2.title, "Telecom Billing");
	strcpy(Book2.author, "Zara Ali");
	strcpy(Book2.subject, "Telecom Billing Tutorial");
	Book2.book_id = 6495700;


	//输出Book1和Book2的信息
	printf("Book 1 title : %s\n", Book1.title);
	printf("Book 1 author : %s\n", Book1.author);
	printf("Book 1 subject : %s\n", Book1.subject);
	printf("Book 1 book_id : %d\n\n", Book1.book_id);

	printf("Book 2 title : %s\n", Book2.title);
	printf("Book 2 author : %s\n", Book2.author);
	printf("Book 2 subject : %s\n", Book2.subject);
	printf("Book 2 book_id : %d\n", Book2.book_id);

	return 0;
}

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ gcc -o test2 test2.c
$ ./test2
Book 1 title : C programing
Book 1 author : Nuha Ali
Book 1 subject : C Programming Tutorial
Book 1 book_id : 6495407

Book 2 title : Telecom Billing
Book 2 author : Zara Ali
Book 2 subject : Telecom Billing Tutorial
Book 2 book_id : 6495700

(3)结构体作为函数参数

你可以通过与传递任何其他变量或指针非常相似的方式将结构作为函数参数传递。

下面是具体的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <string.h>

struct Books{
	char title[50];
	char author[50];
	char subject[100];
	int book_id;
};

void printBook(struct Books Book){
	printf("Book title : %s\n", Book.title);
	printf("Book author : %s\n", Book.author);
	printf("Book subject : %s\n", Book.subject);
	printf("Book book_id : %d\n\n", Book.book_id);
}

int main(){
	//声明数据类型为Books的结构体变量Book1和Book2。
	struct Books Book1;
	struct Books Book2;

	//设置Book1和Book2的基本信息
	strcpy(Book1.title, "C programing");
	strcpy(Book1.author, "Nuha Ali");
	strcpy(Book1.subject, "C Programming Tutorial");
	Book1.book_id = 6495407;

	strcpy(Book2.title, "Telecom Billing");
	strcpy(Book2.author, "Zara Ali");
	strcpy(Book2.subject, "Telecom Billing Tutorial");
	Book2.book_id = 6495700;


	//输出Book1和Book2的信息
	printBook(Book1);
	printBook(Book2);

	return 0;
}

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ gcc -o test3 test3.c
$ ./test3
Book title : C programing
Book author : Nuha Ali
Book subject : C Programming Tutorial
Book book_id : 6495407

Book title : Telecom Billing
Book author : Zara Ali
Book subject : Telecom Billing Tutorial
Book book_id : 6495700

(4)指向结构体的指针

你可以定义指向结构体的指针变量,方式与定义指向其他类型变量的指针相似,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//声明一个拥有4个成员的结构体数据类型Books
struct Books{
	char title[50];
	char author[50];
	char subject[100];
	int book_id;
};

//声明数据类型为Books的结构体指针变量struct_pointer。
struct Books *struct_pointer

现在,你可以在上述定义的指针变量中存储结构体变量的地址。为了查找结构变量的地址,需要把&运算符放在结构体名称的前面,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//声明一个拥有4个成员的结构体数据类型Books
struct Books{
	char title[50];
	char author[50];
	char subject[100];
	int book_id;
};

//声明数据类型为Books的结构体变量Book1。
struct Books Book1;
//设置Book1的基本信息
strcpy(Book1.title, "C programing");
strcpy(Book1.author, "Nuha Ali");
strcpy(Book1.subject, "C Programming Tutorial");
Book1.book_id = 6495407;


//声明数据类型为Books的结构体指针变量struct_pointer。
struct Books *struct_pointer
 
//存储结构体变量Book1的地址
struct_pointer = &Book1;

为了使用指向该结构的指针访问结构的成员,你必须使用->运算符,如下所示:

1
2
3
4
struct_pointer->title;
struct_pointer->author;
struct_pointer->subject;
struct_pointer->book_id;

让我们使用结构指针来重写上面的实例,这将有助于您理解结构体指针的概念:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <string.h>

struct Books{
	char title[50];
	char author[50];
	char subject[100];
	int book_id;
};

void printBook(struct Books *Book){
	printf("Book title : %s\n", Book->title);
	printf("Book author : %s\n", Book->author);
	printf("Book subject : %s\n", Book->subject);
	printf("Book book_id : %d\n\n", Book->book_id);
}

int main(){
	//声明数据类型为Books的结构体变量Book1和Book2。
	struct Books Book1;
	struct Books Book2;

	//设置Book1和Book2的基本信息
	strcpy(Book1.title, "C programing");
	strcpy(Book1.author, "Nuha Ali");
	strcpy(Book1.subject, "C Programming Tutorial");
	Book1.book_id = 6495407;

	strcpy(Book2.title, "Telecom Billing");
	strcpy(Book2.author, "Zara Ali");
	strcpy(Book2.subject, "Telecom Billing Tutorial");
	Book2.book_id = 6495700;


	//输出Book1和Book2的信息
	printBook(&Book1);
	printBook(&Book2);

	return 0;
}

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ gcc -o test4 test4.c
$ ./test4
Book title : C programing
Book author : Nuha Ali
Book subject : C Programming Tutorial
Book book_id : 6495407

Book title : Telecom Billing
Book author : Zara Ali
Book subject : Telecom Billing Tutorial
Book book_id : 6495700

18-C Unions

联合体(Union)是 C 语言中可用的一种特殊数据类型,它可让你存储不同的数据类型在相同的内存位置。你可以定义具有许多成员的联合体,但在任何给定时间下只有一个成员可以包含值。联合体提供了一种将同一内存位置用于多用途的有效方式。联合体有时也被称作共用体。

(1)定义联合体

要定义联合体,你必须使用与定义结构体非常相似的联合体语句。 union语句定义了一种新的数据类型。union语句的格式如下:

1
2
3
4
5
6
union [union tag]{
    member definition;
    member definition;
    ...
    member definition;
} [one or more union variables];

或者简写为:

1
2
3
union [union tag]{
	member-list;
} variable-list;

union tag(联合体标签)是可选的。

member definition(成员定义)或写为member-list,它是标准的普通变量定义,比如int i;或者float f;,或者其他有效的变量定义。

one or more structure variables(联合体变量名)或写为variable-list,定义在联合体的末尾,最后一个分号之前,你可以指定一个或多个联合体变量名称。

下面定义一个名称为 Data 的联合体类型,它包含三个成员 i、f 和 str:

1
2
3
4
5
union Data{
	int i;
    float f;
    char str[20];
} data;

现在,Data 数据类型的变量(比如上面的data变量)可以存储一个整数、一个浮点数,或者一个字符串。这意味着一个变量在相同的内存位置可以存储多个不同类型的数据。

你可以根据需要在一个联合体内使用任何内置的或者用户自定义的数据类型。

联合体占用的内存应足够存储联合体中最大的成员。例如,在上面的实例中,Data 数据类型的变量将占用 20 个字节的内存空间,因为在各个成员中,字符数组 str 所占用的空间是最大的。

下面的实例将显示上面的联合体占用的总内存大小:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>

union Data{
	int i;
	float f;
	char str[20];
};

int main(){

	//声明数据类型为Data的联合体变量data。
	union Data data;

	printf("Memory size occupied by data : %lu\n", sizeof(data));

	return 0;
}

%lu: long unsigned(无符号长整型/浮点数)

运行结果:

1
2
3
$ gcc -o test1 test1.c
$ ./test1
Memory size occupied by data : 20

(2)访问联合体成员

要访问联合体的任何成员,我们需要使用成员访问运算符.,成员访问运算符被编码为联合体变量名称和我们希望访问的联合体成员之间的句点。

下面的实例演示了联合体的用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>

union Data{
	int i;
	float f;
	char str[20];
};

int main(){

	//声明数据类型为Data的联合体变量data。
	union Data data;

	data.i = 10;
	data.f = 220.5;
	strcpy(data.str, "C Programing");

	printf("data.i : %d\n", data.i);
	printf("data.f : %f\n", data.f);
	printf("data.str : %s\n", data.str);

	return 0;
}

运行结果:

1
2
3
4
5
$ gcc -o test2 test2.c
$ ./test2
data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programing

在这里,我们可以看到联合体的 if 成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。现在让我们再来看一个相同的实例,这次我们在同一时间只使用一个变量,这也演示了使用联合体的主要目的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>

union Data{
	int i;
	float f;
	char str[20];
};

int main(){

	union Data data;

	data.i = 10;
	printf("data.i : %d\n", data.i);

	data.f = 220.5;
	printf("data.f : %f\n", data.f);

	strcpy(data.str, "C Programing");
	printf("data.str : %s\n", data.str);

	return 0;
}

运行结果:

1
2
3
4
5
$ gcc -o test3 test3.c
$ ./test3
data.i : 10
data.f : 220.500000
data.str : C Programing

在这里,所有的成员都能完好输出,因为这里同一时间只用到了一个成员。

(3)结构体和联合体的区别

结构体和联合体最大的区别在于内存利用

结构体:

各成员各自拥有自己的内存,各自使用互不干涉,同一时间下是同时存在的,遵循内存对齐原则。一个struct变量的总长度等于所有成员的长度之和。

联合体:

各成员共用一块内存空间,并且同一时间只有一个成员可以得到这块内存的使用权(对该内存的读写),各成员共用一个内存的首地址。因此,联合体比结构体更节约内存。一个union变量的总长度至少能容纳最大的成员变量,而且要满足是所有成员变量类型大小的整数倍。

19-Bit Fields

(1)什么是位域

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个 TRUE/FALSE 时,只有 1 和 0 两种状态,用 1 bit二进位即可。为了节省存储空间,并使处理简便,C 语言提供了一种数据结构,称为位域(Bit Field)位段

假设您的 C 程序中包含许多 TRUE/FALSE 变量,这些变量分组在一个名为 status 的结构体中,如下所示:

1
2
3
4
5
struct
{
    unsigned int widthValidated;   //widthValidated(宽度验证)
    unsigned int heightValidated;  //heightValidated(高度验证)
} status;

上面的结构体需要占用 8 字节的内存空间。但在实际上,在每个变量中,我们只存储 1 或 0 来表示 TRUE 或 FALSE。在这种情况下,C 语言提供了一种更好的利用内存空间的方式。如果你在结构体内使用这样的变量,你可以定义变量的宽度来告诉编译器,你只使用这些大小的字节。例如,上面的结构体可以重写成:

1
2
3
4
5
struct  
{
    unsigned int widthValidated : 1;   //widthValidated(宽度验证)
    unsigned int heightValidated : 1;  //heightValidated(高度验证)
} status;

现在,上面的结构体中,status 变量将占用 4 个字节的内存空间,但是只有 2 bit被用来存储值。

如果您用了 32 个变量,每一个变量宽度为 1 bit,那么 status 结构体还将使用 4 个字节(32bit/8),但只要你再多用一个变量,如果使用了 33 个变量,那么它将分配内存的下一段来存储第 33 个变量,这个时候就开始使用 8 个字节(32bit + 1bit)。让我们看看下面的实例来理解这个概念:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <string.h>

struct
{
	unsigned int widthValidated;
	unsigned int heightValidated;
} status1;

struct  
{
	unsigned int widthValidated : 1;
	unsigned int heightValidated : 1;
} status2;

int main(){
	printf("Memory size occupied by status1 : %lu\n", sizeof(status1));
	printf("Memory size occupied by status2 : %lu\n", sizeof(status2));
	return 0;
}

运行结果:

1
2
3
4
$ gcc -o test1 test1.c
$ ./test1
Memory size occupied by status1 : 8
Memory size occupied by status2 : 4

(2)位域的定义

定义位域和定义结构体的格式相似:

1
2
3
4
struct
{
  type [member_name] : width ;
};

其中,位域的变量元素描述如下:

1
type [member_name] : width ;
元素 描述
type 只能为:int(整型),unsigned int(无符号整型),signed int(有符号整型) 三种类型,它决定了如何解释位域的值。
member_name 位域的名称。
width 位域中位的数量。宽度必须小于或等于指定类型的位宽度。

带有预定义宽度的变量被称为位域。位域可以存储多于 1 bit的数,例如,需要一个变量来存储从 0 到 7 的值,您可以定义一个宽度为 3 bit的位域,如下:

1
2
3
4
struct
{
	unsigned int age : 3;    
} Age;

3bit,最大可存放数字8(2^3=8)

上面的结构体定义告诉 C 编译器 age 变量将只使用 3 bit来存储值,如果尝试使用超过 3 bit,则无法完成。

让我们再来看一个示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <string.h>

struct 
{
	unsigned int age : 3;
} Age;

int main(){

	Age.age = 4;
	printf( "Sizeof(Age) : %lu\n", sizeof(Age) );  //成员之和小于32bit就是4Byte
	printf( "Age.age : %d\n", Age.age );

	Age.age = 7;
	printf( "Age.age : %d\n", Age.age );

	Age.age = 8;
	printf( "Age.age : %d\n", Age.age );	

	return 0;
}

当上面的代码被编译时,它会带有警告。

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ gcc -o test2 test2.c
test2.c:18:10: warning: implicit truncation from 'int' to bit-field changes value from 8 to 0 [-Wbitfield-constant-conversion]
        Age.age = 8;
                ^ ~
1 warning generated.

$ ./test2
Sizeof(Age) : 4
Age.age : 4
Age.age : 7
Age.age : 0

20-Typedef

(1)什么是typedef

C 编程语言提供了一个名为typedef的关键字,你可以使用它给一个数据类型一个新的名字。

以下是为 unsigned char 定义术语 BYTE 的示例:

1
typedef unsigned char BYTE;

在这个类型定义之后,标识符 BYTE 可作为数据类型 unsigned char 的缩写,例如:

1
BYTE b1, b2;

按照惯例,使用typedef定义的新名字会使用大写字母,以便提醒用户该数据类型名称是一个象征性的缩写,但是你也可以使用小写字母,如下:

1
typedef unsigned char byte;	

你也可以使用typedef来为用户自定义的数据类型取一个新的名字。例如,你可以对结构体使用typedef来定义一个新的数据类型的名字,然后使用这个新的数据类型来直接定义结构体变量,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <string.h>

typedef struct Books{
	char title[50];
	char author[50];
	char subject[100];
	int book_id;
} Book;

int main(){

	Book book; 

	strcpy(book.title, "C Programming");
	strcpy(book.author, "Nuha Ali");
	strcpy(book.subject, "C Programming Tutorial");
	book.book_id = 6495407;

	printf( "Book title : %s\n", book.title);
    printf( "Book author : %s\n", book.author);
    printf( "Book subject : %s\n", book.subject);
    printf( "Book book_id : %d\n", book.book_id);

	return 0;
}

运行结果:

1
2
3
4
5
6
$ gcc -o test1 test1.c
$ ./test1
Book title : C Programming
Book author : Nuha Ali
Book subject : C Programming Tutorial
Book book_id : 6495407

(2)typedef与#define

#define是 C 指令,用于为各种数据类型定义别名,与typedef类似,但是它们有以下几点不同:

  • typedef仅限于为数据类型定义新的别名,#define不仅可以为数据类型定义新的别名,也可以为数值定义别名,比如您可以定义 1 为 ONE。
  • typedef是由编译器执行解释的,#define语句是由预编译器进行处理的。

下面是#define的最简单的用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <stdio.h>

#define TRUE 1
#define FALSE 0

int main(){
	printf("Value of TRUE : %d\n", TRUE);
	printf("Value of FALSE : %d\n", FALSE);

	return 0;
}

运行结果:

1
2
3
4
$ gcc -o test2 test2.c
$ ./test2
Value of TRUE : 1
Value of FALSE : 0
updatedupdated2023-04-242023-04-24