目录
1.前言
C++首先是C语言的超集,大部分C语言代码可以直接在C++中编译运行,并且C++继承了C的许多特性,包括数据类型、运算符、控制结构等。
但C是主要面向过程而C++是主要面向对象的,即C++主要使用"对象"来设计软件和应用程序。在C++中可以创建类和对象并使用它们来模拟现实世界的情况,这是C所不具备的。同时,C++具有更多数据类型,包括类、结构体、联合体等,此外C++还支持自定义数据类型。除此之外C++还在函数重载,运算符重载,内存管理,异常处理方面具有新功能和特性。
2.基本语法
2.1数据类型
数据类型主要分为三大类整型、浮点型、指针,在整型中,有字符、短整型、长整型,它们都分为有符号和无符号两种版本,对于各自有着不同的范围。
基本声明
int: 整数类型
float: 单精度浮点数
double: 双精度浮点数
char: 字符类型
bool: 布尔类型(true 或 false)
int myInteger = 10;
float myFloat = 3.14f;
double myDouble = 2.71828;
char myChar = 'A';
bool myBool = true;
int* myPointer;
int myArray[10];
int* myPointer = nullptr; // 初始化指针为nullptr,表示它不指向任何有效的内存地址
typedef关键字和#define
typedef
和#define
都是 C 和 C++ 中用于定义类型别名的预处理指令,它们可以让你为现有的类型创建一个新的名称,这有助于简化代码、提高可读性,并在某些情况下提供类型安全。不过,它们之间有一些重要的区别。
typedef
是 C 语言的一个关键字,用于为现有的类型定义一个新的名称。它创建的是一个真正的类型别名,因此编译器在编译时会知道这个新名称代表的实际类型。typedef int Integer; Integer myVariable = 10; // 这里的 Integer 实际上就是 int
#define
是 C 和 C++ 的预处理器指令,它用于定义宏。虽然它也可以用于创建类型别名,但这种方式实际上只是简单的文本替换,预处理器会在编译前将所有的宏名替换为对应的值或表达式。因此,使用#define
创建的类型别名在编译时并不被视为真正的类型#define Integer int Integer myVariable = 10; // 在编译前,这里的 Integer 会被替换为 int
枚举类型
枚举类型的定义通常使用enum
关键字,后面跟着枚举类型的名称,以及用花括号{}
括起来的一组枚举常量。每个枚举常量都有一个与之关联的整数值,如果没有显式地给枚举常量赋值,那么它们的值会按照定义的顺序递增,默认从0开始。
enum Color {
RED,
GREEN,
BLUE
};
在这个例子中,RED
、GREEN
和BLUE
是枚举常量,它们分别被隐式地赋值为0、1和2。
变量储存类型
在C语言中共有三个地方可以储存变量:普通内存、运行时堆栈、硬件寄存器。在代码块之外声明的变量总是储存在静态内存中,在代码块内部声明的变量自动储存在堆栈之中,自动销毁。而关键字register可以用于自动变量的声明,提示计算机应该储存在硬件寄存器之中。
链接属性
static关键字:static关键字有两种用法,第一种是在代码块内部时,它表示声明一个变量的声明周期是整个程序的执行期间,而不是仅在函数调用期间。并且这样的变量只会被初始化一次,并且在函数调用结束后不会被销毁;第二种是在代码块外部,static用于限制变量的连接作用域,使得变量只在定义它的文件内部可见。
extern
关键字:用于声明一个变量,该变量的定义在别的文件中。它告诉编译器变量的定义存在于其它地方而不是在当前源文件中// 在一个头文件中 extern int globalVariable; // 声明一个外部变量 // 在另一个源文件中 int globalVariable = 100; // 定义外部变量
2.2语句
空语句
空语句本身只包含一个分号,虽然它本身并不执行任何任务,但有时还是有用,它所适用的场合就说语法要求出现一条完整的语句,但并不需要它执行任何任务。
控制语句
控制语句用于控制程序的执行流程,如条件语句、循环语句、和跳转语句。
条件语句
if (x > 0) {
// 如果x大于0,执行这里的代码块
} else {
// 否则,执行这里的代码块
}
循环语句
while (x < 10) {
// 当x小于10时,反复执行这里的代码块
x++;
}
for (int i = 0; i < 5; i++) {
// 初始化i为0,当i小于5时执行循环体,每次循环后i自增1
}
跳转语句
break; // 跳出最近的循环或switch语句
continue; // 跳过当前循环的剩余部分,开始下一次迭代
goto label; // 无条件跳转到程序中标记为label的位置(通常不推荐使用)
2.3操作符和表达式
C语言包含多种操作符,这些操作符用于执行各种操作,如算术运算、比较、逻辑运算、位运算等。表达式是由操作符和操作数组成的,用于计算一个值或执行一个操作。
以下是一些C语言中常见的操作符和表达式的概述:
算术操作符和表达式
- 加法 (
+
):a + b
- 减法 (
-
):a - b
- 乘法 (
*
):a * b
- 除法 (
/
):a / b
- 取模 (
%
):a % b
// 返回a除以b的余数 - 自增 (
++
):a++
或++a
// 将a的值加1 - 自减 (
--
):a--
或--a
// 将a的值减1
关系操作符和表达式
- 小于 (
<
):a < b
- 大于 (
>
):a > b
- 小于等于 (
<=
):a <= b
- 大于等于 (
>=
):a >= b
- 等于 (
==
):a == b
- 不等于 (
!=
):a != b
逻辑操作符和表达式
- 逻辑与 (
&&
):a && b
// 两者都为真时返回真 - 逻辑或 (
||
):a || b
// 至少有一个为真时返回真 - 逻辑非 (
!
):!a
// a为假时返回真,a为真时返回假
位操作符和表达式
- 按位与 (
&
):a & b
- 按位或 (
|
):a | b
- 按位异或 (
^
):a ^ b
- 按位非 (
~
):~a
- 左移 (
<<
):a << b
// 将a的二进制表示向左移动b位 - 右移 (
>>
):a >> b
// 将a的二进制表示向右移动b位
赋值操作符和表达式
- 赋值 (
=
):a = b
// 将b的值赋给a - 加等 (
+=
):a += b
// 等价于a = a + b
- 减等 (
-=
):a -= b
// 等价于a = a - b
- 乘等 (
*=
):a *= b
// 等价于a = a * b
- 除等 (
/=
):a /= b
// 等价于a = a / b
- 模等 (
%=
):a %= b
// 等价于a = a % b
- 左移等 (
<<=
):a <<= b
// 等价于a = a << b
- 右移等 (
>>=
):a >>= b
// 等价于a = a >> b
- 按位与等 (
&=
):a &= b
// 等价于a = a & b
- 按位或等 (
|=
):a |= b
// 等价于a = a | b
- 按位异或等 (
^=
):a ^= b
// 等价于a = a ^ b
条件(三元)操作符和表达式
- 条件操作符 (
? :
):expression1 ? expression2 : expression3
// 如果expression1为真,则结果为expression2,否则为expression3
其他操作符
- 逗号操作符 (
,
):expression1, expression2
// 顺序执行两个表达式,并返回第二个表达式的值 - 取地址 (
&
):&variable
// 返回变量的地址 - 取内容 (
*
):*pointer
// 返回指针指向的值 - sizeof (
sizeof
):sizeof(variable_or_type)
// 返回变量或类型所占用的字节数
表达式求值
表达式的求值遵循运算符优先级和结合性规则。例如,乘法和除法运算符的优先级高于加法和减法运算符,而逻辑运算符的优先级通常低于算术运算符。可以使用括号来改变默认的求值顺序。了解并正确使用这些操作符和表达式是编写有效和高效的C语言程序的关键。
2.4指针
指针定义
首先, 指针是一个变量,它存储的是另一个变量的内存地址。通过指针,我们可以间接地访问和操作那个变量。想象一下,指针就像一个箭头,指向了另一个变量的位置,让我们能够找到并操作那个变量。
具体还是用代码来体现
#include <stdio.h>
int main() {
// 定义一个整数变量
int a = 5;
// 定义一个指向整数的指针变量,并初始化为变量a的地址
int *p = &a;
// 输出变量a的值
printf("The value of a: %d\n", a);
// 通过指针p修改变量a的值
*p = 10;
// 再次输出变量a的值,可以看到它已经被修改
printf("The value of a after modification through pointer: %d\n", a);
// 输出指针p所指向的地址
printf("The address stored in pointer p: %p\n", (void *)p);
// 输出变量a的地址
printf("The address of variable a: %p\n", (void *)&a);
// 验证指针p确实指向变量a
if (p == &a) {
printf("Pointer p points to variable a.\n");
}
return 0;
}
在这个程序中,我们首先定义了一个整数变量
a
并初始化为5。然后,我们定义了一个指向整数的指针p
,并将a
的地址赋给它。通过*p
,我们可以访问或修改a
的值。在程序中,我们通过指针p
将a
的值从5修改为10。此外,我们还输出了指针p
所存储的地址,以及变量a
的地址,以验证指针p
确实指向了变量a
。最后,我们用一个简单的if
语句来确认这一点。
取值和取址操作符
取值操作符用于获取指针指向的值。当它被应用在一个指针上时,它会返回指针指向的内存位置上的值。例如:
int a = 10; int *p = &a; // p指向a的地址 int value = *p; // 使用取值操作符获取p指向的值,此时value等于10
取址操作符用于获取变量的内存地址。当它被应用在一个变量上时,它会返回该变量在内存中的地址。
int a = 10; int *p = &a; // 使用取址操作符获取a的地址,并将地址赋给指针p
二级指针
所谓二级指针,即是指针的指针,是一个指向指针的指针,指针变量是一个变量,那么内存中也有属于它的地址,则我们可以声明一个指针来指向它的值。
牛刀小试
#include <stdio.h>
int main() {
// 定义一个整数变量
int a = 42;
// 定义一个指向整数的指针
int *p = &a;
// 定义一个指向整数指针的指针(指针的指针)
int **pp = &p;
// 使用指针的指针访问整数变量
printf("The value of a: %d\n", **pp);
// 修改指针所指向的整数值
*p = 50;
// 再次使用指针的指针访问修改后的整数变量
printf("The value of a after modification: %d\n", **pp);
// 输出指针p的地址
printf("The address of pointer p: %p\n", (void *)p);
// 输出指针的指针pp所指向的地址,即指针p的地址
printf("The address stored in pointer-to-pointer pp: %p\n", (void *)pp);
// 输出指针的指针pp所指向的指针p所指向的地址,即变量a的地址
printf("The address pointed to by the pointer pointed to by pp: %p\n", (void *)*pp);
// 输出变量a的地址,验证上面的地址是否一致
printf("The address of variable a: %p\n", (void *)&a);
return 0;
}
在这个例子中,
pp
是一个指针的指针,它存储了p
的地址。通过**pp
,我们可以间接地访问a
的值。当我们修改*p
的值时,**pp
也会反映出这个变化,因为它们都指向同一个变量a
。指针的指针在函数参数中特别有用,特别是当你需要传递一个指针给函数,并且该函数需要修改这个指针的指向时。例如,你可以使用指针的指针来交换两个指针的地址。
2.5函数
函数定义
首先,函数是一个代码块,它执行特定的任务并可能返回一个值。函数使得代码更易于理解、重用和维护。
函数一般形式如下
return_type function_name(parameter list) {
body of the function
}
return_type
:函数返回的数据类型。如果函数不返回任何值,则使用关键字void
。function_name
:函数的名称。parameter list
:函数参数列表,由逗号分隔。
一个返回整数的函数
int add(int a, int b) {
int sum = a + b;
return sum;
}
不返回任何值的函数
void displayMessage() {
printf("Hello, world!\n");
}
函数调用
int result = add(5, 3);
printf("The result is: %d\n", result);
函数的类型
用户定义的函数、库函数(printf,scanf等)、回调函数(作为参数传递给其它函数的函数,在需要时被调用)
函数的参数传递
在C和C++中,参数通常是通过值传递的。这意味着当你将一个值传递给函数时,实际上是传递了该值的一个副本,原始值保持不变。如果你想修改原始值,你需要使用指针。
#include <stdio.h> // 函数声明,使用值传递 void increment(int x); int main() { int a = 5; // 调用increment函数,传递变量a的值 increment(a); // 输出变量a的值,预期输出仍然是5,因为increment函数内部修改了x的副本 printf("a = %d\n", a); return 0; } // 函数定义 void increment(int x) { // 这里修改的是x的副本,而不是main函数中的a x = x + 1; printf("Inside function, x = %d\n", x); }
输出结果将是
Inside function, x = 6, a = 5
指针传递
当使用指针传递时,函数接收的是变量的地址,而不是变量的副本。这样,函数就可以通过指针来访问和修改原始变量的值。
#include <stdio.h> // 函数声明,使用指针传递 void increment(int *p); int main() { int a = 5; // 调用increment函数,传递变量a的地址 increment(&a); // 输出变量a的值,预期输出是6,因为increment函数通过指针修改了原始变量a的值 printf("a = %d\n", a); return 0; } // 函数定义 void increment(int *p) { // 这里通过指针p直接访问和修改main函数中的a *p = *p + 1; printf("Inside function, *p = %d\n", *p); }
输出结果
Inside function, *p = 6 a = 6
引用传递
C语言本身不支持引用传递,这是C++的一个特性。在C++中,引用传递允许函数直接修改调用函数中变量的值。
#include <iostream> // 函数声明,使用引用传递 void increment(int& ref); int main() { int a = 5; // 调用increment函数,传递变量a的引用 increment(a); // 输出变量a的值,预期输出是6,因为increment函数通过引用直接修改了原始变量a的值 std::cout << "a = " << a << std::endl; return 0; } // 函数定义 void increment(int& ref) { // 这里通过引用ref直接访问和修改main函数中的a ref = ref + 1; std::cout << "Inside function, ref = " << ref << std::endl; }
输出结果
Inside function, ref = 6 a = 6
在这个C++例子中,
increment
函数通过引用ref
修改了main
函数中变量a
的值。注意,在C++中,使用引用传递时,函数参数前需要加上&
符号。
递归
递归是指一个函数在其定义中直接或间接调用自身的方法。递归的基本思想是将一个复杂的问题分解为与原问题相似的、规模较小的问题来求解。
递归通常用来解决以下问题:
- 分治问题:将一个大问题分解为若干个小问题,然后逐个解决这些小问题,最后将它们的解决方案合并起来得到大问题的解决方案。这类问题往往具有天然的递归结构,例如汉诺塔、斐波那契数列等。
- 树形结构问题:树形结构问题通常具有递归性质,因为树形结构可以自然地分解为子树。例如,遍历二叉树、计算树的深度等。
- 回溯问题:回溯算法是一种通过搜索所有可能的候选解来找出所有解的算法。当候选解被确认不是一个解时,回溯算法会通过在上一步进行一些变化来丢弃该解,即“回溯”。这类问题也可以使用递归来实现,例如八皇后问题、图的着色问题等。
牛刀小试
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
在这个例子中,factorial
函数通过递归调用自身来计算 n
的阶乘。当 n
等于 0 时,递归终止并返回 1。否则,函数返回 n
乘以 n-1
的阶乘,从而实现了阶乘的计算。
用递归时需要注意的问题:
- 递归必须有明确的终止条件,否则会导致无限递归,最终导致栈内存溢出。
- 递归的层次不宜过深,否则可能导致栈空间不足,引发栈溢出错误。
- 递归算法通常具有较高的时间复杂度,因此在处理大规模数据时可能不太适用。
2.6数组
数组是C和C++编程中一种非常重要的数据结构,它允许我们存储相同类型的多个元素在一个连续的内存块中。数组中的每个元素都可以通过其索引来访问,索引通常从0开始,到数组长度减1。
数组的定义
在C和C++中,数组可以通过以下方式定义:
// 定义一个包含5个整数的数组
int myArray[5];
// 初始化一个包含5个整数的数组
int initializedArray[5] = {1, 2, 3, 4, 5};
// 初始化一个包含部分整数的数组,其余元素将自动初始化为0
int partiallyInitializedArray[5] = {1, 2}; // 等价于 {1, 2, 0, 0, 0}
// C++11及以后版本中,可以使用列表初始化语法
int listInitializedArray[5] = {1, 2, 3, 4, 5};
// 定义一个字符数组(也就是字符串)
char str[10] = "Hello"; // 字符串"Hello"加上一个空字符'\0'
访问数组元素
int value = myArray[2]; // 访问数组中的第三个元素(索引为2)
myArray[2] = 10; // 将数组中的第三个元素设置为10
数组的长度是固定的,一旦定义后不能更改。在C语言中,如果你想知道一个数组的长度,你需要自己保存这个信息,或者使用sizeof
操作符:
int length = sizeof(myArray) / sizeof(myArray[0]); // 计算数组长度
数组与指针
数组名的本质:在大多数情况下,数组名其实可以看做是一个指向数组第一个元素的常量指针。例如,有一个int arr[10],那么arr就是一个指向int类型的指针,它指向arr[0]。通过这层关系,我们可以通过指针算术来遍历数组。
指针与数组索引:使用指针访问数组元素与使用数组索引访问元素在功能上是等效的。例如,ptr[i]
与arr[i]
是等价的,它们都访问数组中索引为i
的元素。但大多数情况来说,指针的效率高于索引的效率。
数组作为函数参数:当数组作为函数参数传递时,它实际上已经退化为指针。这以为着在函数内部,你无法知道原始数组的大学,因此在函数内部处理数组时,通常需要指针。下面是传递数组给函数时,声明函数的方法
1、声明一个指向数组第一个元素的指针,指针的类型应该与数组元素的类型相匹配。
void printArray(int* arr, int size) {
for (int i = 0; i < size; ++i) {
printf("%d ", arr[i]);
}
printf("\n");
}
#调用这个函数时,你可以传递数组名和数组的大小
int myArray[] = {1, 2, 3, 4, 5};
printArray(myArray, sizeof(myArray) / sizeof(myArray[0]));
2、使用数组形参,虽然不推荐这样做,因为这会导致函数形参大小不固定,但技术上你仍然可以在函数原型中声明一个固定大小的数组。然而,这种做法限制了函数只能接受特定大小的数组,而且实际上仍然是通过指针来传递的。
void printArray(int arr[], int size) {
// 与上面的函数相同
}
3、 在C++中,你可以使用模板来编写一个能够接受任意大小数组的函数。
template<typename T, std::size_t N>
void printArray(const T (&arr)[N]) {
for (std::size_t i = 0; i < N; ++i) {
std::cout << arr[i] << ' ';
}
std::cout << '\n';
}
数组与指针区别:数组在创建时,编译器将为数组声明的元素数量保留内存空间,然后再创建数组名,它的值是一个常量,指向第一个元素的位置。当声明一个指针时,编译器只为指针本身保留内存空间,它并不为任何整型值分配内存空间。
多维数组
C语言中的多维数组是一个非常重要的概念,它允许你存储和操作二维或更高维度的数据集合。多维数组可以看作是数组的数组,也就是说,它的每个元素都是一个数组。二维数组可以看作是一个表格,它包含多行和多列。每行和每列的大小在声明时都需要指定。
二维数组:
int matrix[3][4]; // 声明一个3行4列的整数二维数组
matrix[0][0] = 1; // 设置第一行第一列的元素为1
matrix[1][2] = 5; // 设置第二行第三列的元素为5
// ... 以此类推
在这个例子中,
matrix
是一个3x4的数组,也就是说,它有3行和4列。你可以通过两个索引来访问它的元素:第一个索引表示行,第二个索引表示列。此外,在C语言中,二维数组matrix[3][4]
的索引是从0开始的,而不是从1开始。因此,matrix[3][4]
实际上是一个无效的数组访问,因为它超出了数组的边界。
多维数组: 多维数组的概念可以扩展到三维或更高维度。例如,一个三维数组可以看作是一个立方体,它有多个“层”或“页面”,每个页面都是一个二维数组。
int cube[2][3][4]; // 声明一个2层3行4列的整数三维数组
cube[0][1][2] = 20; // 设置第一层第二行第三列的元素为20
// ... 以此类推
在这个例子中,cube
是一个2x3x4的数组,它有2个“页面”,每个页面是一个3x4的二维数组。
初始化多维数组 :
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
指针:在多维数组中,数组名实际上是一个指向数组第一个元素的指针。对于二维数组,数组名是一个指向一维数组的指针,这些一维数组又包含指向实际元素的指针。理解这一点对于掌握多维数组和指针的交互非常有帮助。
指针数组和数组指针:在C语言中,指针数组(array of pointers)和数组指针(pointer to array)是两个不同的概念,尽管它们的名称可能听起来相似,但它们的用途和内存布局是不同的。
指针数组是一个包含多个指针的数组,每个指针可以指向不同的内存位置或对象。指针数组通常用于存储指向不同类型对象或同类型对象的多个指针。
// 示例:一个包含3个整数指针的指针数组 int *ptr_array[3]; // 初始化指针数组 int a = 10; int b = 20; int c = 30; ptr_array[0] = &a; ptr_array[1] = &b; ptr_array[2] = &c; // 通过指针数组访问整数值 printf("%d\n", *ptr_array[0]); // 输出:10 printf("%d\n", *ptr_array[1]); // 输出:20 printf("%d\n", *ptr_array[2]); // 输出:30
数组指针是一个指向数组的指针,它本身不是一个数组,而是指向一个固定大小的数组。数组指针通常用于函数参数中,以传递数组而不仅仅是数组的首元素地址。
// 示例:一个指向包含3个整数的数组的指针 int (*array_ptr)[3]; // 初始化数组指针 int my_array[3] = {1, 2, 3}; array_ptr = &my_array; // 通过数组指针访问数组元素 printf("%d\n", (*array_ptr)[0]); // 输出:1 printf("%d\n", (*array_ptr)[1]); // 输出:2 printf("%d\n", (*array_ptr)[2]); // 输出:3
2.7结构体和联合
C语言中的结构体(struct)和联合(union)是两种重要的复合数据类型,它们允许我们将多个不同类型的数据封装成一个单独的数据类型。
结构体(struct)
结构体是一种复合数据类型,可以包含不同类型的数据项。这些数据项可以是基本数据类型(如 int、float、char 等),也可以是其他结构体类型。结构体提供了一种将多个相关的数据项组合成一个单独实体的方式。
定义结构体
在C语言中,你可以使用 struct
关键字来定义结构体。例如:
struct Student {
char name[50];
int age;
float score;
};
在这个例子中,我们定义了一个标签(结构体名称)为 Student
的结构体,它包含了一个字符数组 name
(用于存储学生的名字),一个整数 age
(用于存储学生的年龄),和一个浮点数 score
(用于存储学生的分数)。
使用结构体:通过声明结构体变量来使用结构体,通常有两种主要方式:第一种是在结构体定义时就声明,如
struct Student {
char name[50];
int age;
float score;
} student1, student2; // 在定义结构体时声明变量
第二种是在其他位置使用结构体类型来声明
struct Student student1;
结构体初始化 :
struct Student student1 = {"张三", 20, 90.5};
结构体的引用(访问):
访问结构体变量的成员需要使用点运算符(.)例如:
printf("%s is %d years old and has a score of %.2f\n", student1.name, student1.age, student1.score);
typedef重命名结构体
为了简化结构体的使用,可以使用 typedef
为结构体类型定义一个新名称,这样在声明变量时就不需要每次都使用 struct
关键字,这也是编程中常用的方法。例如:
typedef struct Student {
char name[50];
int age;
float score;
} Student_t; // 使用typedef重命名结构体
// 现在可以直接使用Student_t来声明变量,而不需要写struct Student
Student_t student5;
结构体的自引用
结构体不能直接包含自己类型的成员,因为这将导致无限大小的递归定义。但是,结构体可以包含指向自己类型的指针。这通常用于实现链表、树等数据结构。例如:
struct Node {
int data;
struct Node* next; // 指向下一个节点的指针
};
联合(union)
联合也是一种复合数据类型,但它与结构体有所不同。在联合中,所有的成员都占用同一块内存区域,也就是说,联合的大小是其最大成员的大小。在任何时候,联合中只有一个成员是有效的。
定义联合:使用union关键字
union Data {
int i;
float f;
char str[20];
};
使用联合
我们可以像使用结构体一样使用联合。但是需要注意的是,由于联合的所有成员都占用同一块内存,因此当你给联合的一个成员赋值时,其他成员的值可能会发生变化,因为它们的内存位置是重叠的。
union Data data;
data.i = 10; // data 的 i 成员被赋值为 10
printf("%f\n", data.f); // 输出 data 的 f 成员的值,可能是不确定的,因为 f 和 i 占用同一块内存
在这个例子中,虽然我们没有直接给
data.f
赋值,但是由于f
和i
占用同一块内存,所以data.f
的值可能会受到data.i
赋值的影响。总的来说,结构体和联合都是C语言中非常有用的复合数据类型,它们允许我们将多个相关的数据项组合在一起,从而更方便地管理和操作这些数据。