C语言 - 从入门到选购内存
前言
C语言是一种通用的、过程化的计算机编程语言,由1972年开发。它以其简洁、效率和灵活性而闻名。
特点:
结构化编程:C语言支持结构化编程,包括函数、循环和条件语句。
指针:C语言允许使用指针,直接访问内存地址。
低级访问:C语言提供对底层硬件的低级访问,使其适合于操作系统和嵌入式系统编程。
可移植性:C语言代码可以在各种平台上编译和执行,具有很强的可移植性。
效率:C语言编译后的代码高效紧凑,适合资源受限的系统。
变量与类型
C语言是一种静态类型语言,这意呈着所有变量在编译时必须指定数据类型。与诸如Python、JavaScript、PHP等解释型语言不同,C语言要求在声明变量时明确其类型。变量名可以包含字母、数字和下划线,但不能以数字开头。在C中,变量可以在声明时初始化。一旦变量被声明,其值可以随时被更新,但新值必须与原始类型匹配。例如,尝试将浮点数赋值给整型变量会导致编译警告,并且浮点数会被转换为整数。C语言支持多种内置数据类型,包括int、char、short、long、float、double和long double等。
整数
C语言提供了多种整数类型:char、int、short和long,以适应不同的使用场景和存储需求。char类型通常用于存储ASCII字符,但也可以存储-128到127之间的小整数,占用至少一个字节。int类型是最常用的整数类型,占用至少两个字节。short类型也占用至少两个字节,而long类型至少占用四个字节。这些类型的具体大小和存储范围依赖于编译器实现和系统架构,但有一定的保证:short不会比int长,long至少与int一样长。ANSI C规范为每种类型设定了最小值范围。在不同的硬件平台上,如Arduino开发板,这些类型的大小和范围可能会有所不同,例如Arduino Uno上的int是两个字节,而在Arduino MKR 1010上是四个字节,显示出不同板子之间的显著差异。无论哪种Arduino开发板,short都是两个字节,long是四个字节,其范围分别保持不变。
无符号整数
在C语言中,给基本的整数数据类型(如char、int、short、long)前面加上”unsigned”关键字,可以让这些类型只能表示正数和零,而不能表示负数。这样,这些数据类型的值的范围就从0开始,能表示的最大数值也相应增大。
比如,使用unsigned char后,它能表示的数值范围是0到至少255;
unsigned int能表示的范围是0到至少65,535;
unsigned short的范围是0到至少65,535;
unsigned long的范围是0到至少4,294,967,295。
这种特性在需要确保数据始终为非负的情况下非常有用。
溢出的问题
在处理C语言中的整数类型时,确保数值不超出其定义的范围是非常重要的。如果一个数值超出了其类型所能表示的最大范围,结果会发生”溢出”。在C语言中,这种溢出并不会导致错误或异常,而是会按照一种特定的方式”环绕”回到其类型的最小值附近。
例如,对于一个unsigned char
类型,其能表示的数值范围是0到255。如果你有一个值为255的unsigned char
变量,并对其加1,结果会变成0,因为它超出了unsigned char
能表示的最大值,所以就环绕回到了最小值。同样,如果你有一个值为255的unsigned char
变量,并给它加上10,结果会变成9,因为加上10后数值达到了265,超出了unsigned char
的范围,所以它会从0开始继续数,直到9。
这种行为称为”模运算”或”取模环绕”,是因为当数值超过类型所能表示的最大值时,它会从该类型所能表示的最小值开始,继续计数,直到达到超出部分的等效值。因此,在编写程序时,特别是涉及到可能会增加或减少数值的操作时,非常重要的一点是要考虑到这种溢出行为,确保程序的逻辑能够正确处理或避免溢出情况。
例如:
1 |
|
当处理有符号的整数类型并且数值超出了该类型所能表示的范围时,C语言的行为是未定义的。这意味着程序可能会表现出不可预测的结果。例如,如果你有一个char
类型的变量(在大多数系统上,它是有符号的,并且能表示的范围大约是-128到127),并且给它赋予超出这个范围的值,你将无法确定程序会如何响应。
1 | include <stdio.h> |
一个值为127的char
类型变量被加上了10,理论上这会超出char
类型所能表示的最大值。然而,打印出的结果是一个非常大的无符号整数(4294967177),这是因为在打印时使用了%u
格式说明符,它是用于无符号整数的。由于char
被解释为一个非常大的无符号整数,这个数实际上是一个溢出结果的表现。
这种未定义的行为强调了在编程时需要特别注意变量的类型和可能的值范围。C语言不会自动检查类型溢出或下溢,因此程序员需要通过逻辑检查和适当的编程实践来避免这种情况。例如,可以通过在进行数学运算之前检查值是否会超出范围来预防溢出。此外,了解并使用合适的数据类型(如使用更大的整数类型或无符号类型)也可以帮助减少这种风险。
浮点数
浮点类型在C语言中用于表示可以有小数部分的数字,这包括了正数、负数以及分数。与整数不同,浮点数能表示的数值范围更广,并且能表示整数不能表示的数值,比如非整数值。浮点数通常以小数点形式或是科学计数法(例如1.29e-3或-2.3e+5)来表示,后者表示数值乘以10的指定次幂。
C语言提供了三种基本的浮点类型:
float:这是最小的浮点类型,通常用32位比特来实现,能够表示的范围至少在10^-37到10^+37之间。在32位系统中,float的精度大约是7位十进制数字。
double:比float类型能表示的数值范围更广,通常用64位比特来实现。double提供的精度更高,大约是15位十进制数字,适合需要更高数值精度的计算。
long double:提供比double还要大的数值范围和更高的精度,通常用80位比特来实现。long double的精度和数值范围最大,适合对精度要求极高的计算场景。
不同的C语言实现和不同的硬件平台可能会有不同的表示方式。例如,在现代的Mac上,float是用32位表示,double用64位表示,而long double可能用80位表示。浮点数的具体实现(包括精度和能表示的数值范围)依赖于具体的编译器和硬件,但任何C语言的实现都必须满足以上提到的最小要求。浮点数在处理科学计算、图形处理和其他需要非整数数值的领域中非常重要。
你可以通过下面的代码来比较大小
1 |
|
常量
在C语言中,常量是一种一旦被定义就不能改变值的特殊类型。声明常量的方法有两种主要形式:
使用
const
关键字:这种方法的声明方式与变量相似,但需要在声明前加上const
关键字,并且在声明时必须初始化常量。例如,const int age = 37;
定义了一个整型常量age
,其值为37。一般来说,按照惯例,常量的名字会全部大写,如const int AGE = 37;
,这样做主要是为了提高代码的可读性,使得常量和变量容易区分。使用
#define
预处理器指令:这种方式不需要指定类型,也不使用等号=
,并且末尾不加分号。例如,#define AGE 37
定义了一个常量AGE
,其值为37。#define
方式定义的常量在预处理阶段就被处理了,编译器会在编译之前将所有AGE
的实例替换为37。这意味着它在编译时不占用内存,并且类型是根据使用的上下文来推断的。
无论使用哪种方法定义常量,其命名规则与变量相同:可以包含字母(无论大小写)、数字和下划线,但不能以数字开头。例如,AGE
和Age10
都是有效的常量名,而1AGE
则是无效的。
总的来说,常量在C语言中是一种很有用的特性,能够提高程序的可读性和维护性,同时帮助避免硬编码的值被意外改变,从而增加代码的稳定性。
运算符
C语言提供了一系列丰富的运算符,用于在程序中执行各种数据操作。这些运算符可以分为以下几个主要类别:
算术运算符:用于执行基本的数学计算,如加法(
+
)、减法(-
)、乘法(*
)、除法(/
)和取模(%
)。取模运算符用于求两个数相除的余数。比较运算符:用于比较两个值之间的关系,包括等于(
==
)、不等于(!=
)、大于(>
)、小于(<
)、大于等于(>=
)、小于等于(<=
)。逻辑运算符:用于组合或修改条件表达式,包括逻辑与(
&&
)、逻辑或(||
)和逻辑非(!
)。复合赋值运算符:将赋值操作与另一种操作(如加法或乘法)结合在一起,例如加等于(
+=
)、减等于(-=
)、乘等于(*=
)、除等于(/=
)等。位运算符:直接对整数类型的操作数的二进制位进行操作。包括位与(
&
)、位或(|
)、位异或(^
)、位非(~
)、左移(<<
)和右移(>>
)。指针运算符:用于直接操作内存地址。包括取地址(
&
)和解引用(*
)运算符。取地址运算符用于获取变量的内存地址,而解引用运算符用于访问指针指向的值。结构运算符:用于访问结构体成员。包括成员访问(
.
)和成员通过指针访问(->
)运算符。混合运算符:这不是一个官方的分类,但可以用来指那些不易归入其他类别的运算符,如逗号(
,
)运算符,它用于在表达式中分隔多个操作。
理解和熟练使用这些运算符对于编写有效和高效的C程序至关重要。每种运算符都有其特定的用途和操作规则,合理使用这些运算符可以帮助解决各种编程问题。
算术运算符
二元运算符:
- 就像数学中的符号,需要两个数字才能工作。
- 例如:加法 (+)、减法 (-)、乘法 (*) 和除法 (/)。
操作符 | 名字 | 示例 |
---|---|---|
= |
赋值 | a = b |
+ |
加 | a + b |
- |
减 | a - b |
* |
乘 | a * b |
/ |
除 | a / b |
% |
取模 | a % b |
一元运算符:
- 只需要一个数字来工作。
- 例如:正号 (+) 和负号 (-)。
- 自增 (++) 和自减 (–) 运算符:
- 让数字增加或减少 1。
- ++a 表示先增加 1 再使用 a 的值,而 a++ 表示先使用 a 的值再增加 1。
运算符 | 名字 | 示例 |
---|---|---|
+ |
一元加 | +a |
- |
一元减 | -a |
++ |
自增 | a++ or ++a |
-- |
自减 | a-- or --a |
比较运算符
运算符 | 名字 | 示例 |
---|---|---|
== |
相等 | a == b |
!= |
不相等 | a != b |
> |
大于 | a > b |
< |
小于 | a < b |
>= |
大于等于 | a >= b |
<= |
小于等于 | a <= b |
逻辑运算符
!
非(例如:!a
)&&
与(例如:a && b
)||
或(例如:a || b
)
复合赋值运算符
运算符 | 名字 | 示例 |
---|---|---|
+= |
加且赋值 | a += b |
-= |
减且赋值 | a -= b |
*= |
乘且赋值 | a *= b |
/= |
除且赋值 | a /= b |
%= |
求模且赋值 | a %= b |
三目运算符
- C 语言中唯一需要三个操作数的运算符。
- 就像一个简短的 if/else 语句。
- 格式:
<条件> ? <表达式> : <表达式>
- 例如:
a ? b : c
表示:如果a
为 true,执行b
,否则执行c
。 - 优点:比 if/else 语句更简洁,可以内联在表达式中。
示例:
1 | int a = 10; |
在这个示例中,c
将被赋值为 a
(因为 a
大于 b
)。
sizeof运算符:
返回给定操作数的大小(以字节为单位)。
- 可以传入变量或类型。
示例:
1 | int a; |
其他用法:
- 检查数组的大小:
sizeof(数组名) / sizeof(数组元素类型)
- 获取结构体或联合体的成员数量:
sizeof(结构体或联合体名称)
- 检查指针的大小:
sizeof(指针变量)
运算符优先级
决定了在表达式中先执行哪些运算。优先级较高的运算符会先执行。
C 语言运算符优先级(从高到低):
- 括号
- 一元运算符(例如:+、-、++、–)
- 乘法和除法
- 加法和减法
- 移位运算符(例如:<<、>>)
- 关系运算符(例如:==、!=、<、>)
- 逻辑运算符(例如:&&、||、!)
- 赋值运算符(例如:=、+=、-=)
- 逗号运算符(,)
示例:
1 | a + b * c |
在这个表达式中,乘法运算符 (*) 的优先级高于加法运算符 (+),因此会先执行乘法,再执行加法。
使用括号控制优先级:
可以使用括号来改变运算符的优先级。括号内的表达式会优先执行。
示例:
1 | (a + b) * c |
在这个表达式中,括号强制先执行加法,然后再执行乘法。
条件语句
允许程序根据不同的条件执行不同的代码块。
C 语言中有两种主要的条件语句:
1. if/else 语句
- 检查一个条件是否为真。
- 如果条件为真,则执行 if 块中的代码。
- 如果条件为假,则执行 else 块中的代码(可选)。
格式:
1 | if (condition) { |
示例:
1 | int age = 18; |
2. switch 语句
- 检查一个变量的值是否等于多个给定的值之一。
- 如果变量的值与某个给定值匹配,则执行相应的代码块。
格式:
1 | switch (variable) { |
示例:
1 | char grade = 'A'; |
循环
C 语言中的循环允许程序重复执行一段代码,直到某个条件满足。
1. for 循环
- 使用一个计数器变量来控制循环的次数。
- 计数器变量在每次循环中都会更新。
格式:
1 | for (initialization; condition; increment/decrement) { |
示例:
1 | for (int i = 0; i < 10; i++) { |
2. while 循环
- 只要给定的条件为真,就会一直执行循环体。
格式:
1 | while (condition) { |
示例:
1 | int i = 0; |
3. do-while 循环
- 与 while 循环类似,但会先执行循环体,然后再检查条件。
格式:
1 | do { |
示例:
1 | int i = 0; |
区别:
- for 循环最适合当你知道循环的执行次数时。
- while 循环最适合当你不确定循环的执行次数时。
- do-while 循环最适合当你希望循环体至少执行一次时。
使用 break 跳出循环
break
语句会立即终止当前循环,并继续执行循环后的代码。
语法:
1 | break; |
示例:
1 | for (int i = 0; i < 10; i++) { |
在这个示例中,当 i
等于 5 时,break
语句会跳出循环。因此,循环只打印数字 0 到 4。
注意:
break
语句只能用于循环内部。- 它会跳出最内层的循环。
- 如果需要跳出嵌套循环,可以使用嵌套的
break
语句。
数组
数组是一种数据结构,它可以存储多个相同类型的值。
C 语言中数组的特点:
- 数组中的每个元素都有一个唯一的索引(从 0 开始)。
- 数组中的所有元素必须具有相同的类型。
- 数组的大小在编译时确定,并且在程序运行时不能改变。
声明数组:
1 | type array_name[size]; |
其中:
type
是数组元素的数据类型。array_name
是数组的名称。size
是数组的大小(元素的数量)。
示例:
1 | int numbers[5]; // 声明一个包含 5 个整数的数组 |
访问数组元素:
可以使用索引来访问数组元素。
1 | array_name[index] |
其中:
array_name
是数组的名称。index
是要访问的元素的索引。
示例:
1 | numbers[0] = 10; // 将数组中的第一个元素设置为 10 |
数组的优点:
- 便于存储和管理相关数据。
- 访问数组元素的速度很快,因为它们在内存中是连续存储的。
字符串
字符串在 C 语言中是一种特殊类型的数组,它存储一组字符。
特点:
- 字符串以一个空字符(’\0’)结尾。空字符表示字符串的结束。
- 字符串数组的大小必须足够大,以容纳所有字符和空字符。
声明字符串:
1 | char string_name[size]; |
其中:
string_name
是字符串的名称。size
是字符串的大小(字符的数量 + 1,用于空字符)。
示例:
1 | char name[10]; // 声明一个可以容纳 9 个字符和一个空字符的字符串 |
初始化字符串:
字符串可以在声明时使用字符串文字初始化:
1 | char name[] = "John Doe"; // 声明并初始化一个字符串 |
访问字符串字符:
可以使用索引来访问字符串中的字符:
1 | string_name[index] |
其中:
string_name
是字符串的名称。index
是要访问的字符的索引。
示例:
1 | printf("%c", name[0]); // 打印字符串中的第一个字符 |
字符串的优点:
- 便于存储和操作文本数据。
- 可以使用标准库函数轻松地处理字符串。
指针
指针很难,但是别怕,指针是 C 语言中一个强大的特性,它允许你直接操作内存地址。
理解指针:
- 指针是一个变量,它存储另一个变量的内存地址。
- 指针的值是一个地址,指向被指向的变量。
声明指针:
1 | type *pointer_name; |
其中:
type
是被指向变量的数据类型。pointer_name
是指针的名称。
示例:
1 | int *ptr; // 声明一个指向整数的指针 |
使用指针:
- 使用取地址运算符 (&) 获取变量的地址:
&variable
- 使用解引用运算符 (*) 来访问被指向的变量:
*pointer
示例:
1 | int a = 10; |
指针的优点:
- 提供对内存的低级访问。
- 允许高效的数据结构和算法。
- 在某些情况下可以提高性能。
指针的注意事项:
- 指针可以指向无效的内存地址,导致程序崩溃。
- 使用指针需要小心,以避免内存泄漏和数据损坏。
函数
函数是 C 语言中将代码组织成可重用块的主要机制。
理解函数:
- 函数是一组执行特定任务的语句。
- 函数可以接收输入(称为参数)并返回输出(称为返回值)。
声明函数:
1 | return_type function_name(parameter_list) { |
其中:
return_type
是函数返回的值的数据类型。function_name
是函数的名称。parameter_list
是函数接收的参数列表。
示例:
1 | int sum(int a, int b) { |
调用函数:
1 | int result = sum(5, 10); // 调用 sum 函数并存储结果 |
函数的优点:
- 代码可重用性:函数可以根据需要多次调用。
- 模块化:函数将代码组织成易于管理的块。
- 可测试性:函数可以独立于程序的其他部分进行测试。
函数的注意事项:
- 函数必须在使用前声明。
- 函数的参数和返回值类型必须匹配其声明。
- 函数调用会产生开销,因此应谨慎使用。
输入与输出
输入/输出 (I/O) 操作允许程序与外部世界进行交互。
C 语言中的 I/O:
- C 语言本身不提供 I/O 功能。
- I/O 功能由 C 标准库中的
stdio.h
头文件提供。
常用的 I/O 函数:
- 输入:
scanf
:从标准输入读取格式化数据。
- 输出:
printf
:以格式化方式向标准输出写入数据。fprintf
:以格式化方式向文件写入数据。
示例:
1 |
|
注意事项:
- 打开文件时要小心,以避免内存泄漏。
- 使用
fflush
函数刷新输出缓冲区,以确保数据立即写入文件。
变量作用域
变量作用域定义了变量在程序中可用的范围。
C 语言中的变量作用域:
- 全局变量:
- 在函数外部声明。
- 在整个程序中可用。
- 局部变量:
- 在函数内部声明。
- 只在声明它们的函数中可用。
示例:
1 | // 全局变量 |
在这个示例中,global_variable
是一个全局变量,可以在 some_function
中使用。但是,local_variable
是一个局部变量,只能在 some_function
中使用。
作用域规则:
- 内部作用域中的变量覆盖外部作用域中的同名变量。
- 变量只能在声明它们的作用域内使用。
- 全局变量在程序启动时初始化,而局部变量在函数调用时初始化。
静态变量
静态变量是在函数内部使用 static
关键字声明的变量。
特点:
- 在没有初始化的情况下,静态变量会被初始化为 0。
- 静态变量在函数调用之间保留其值。
- 即使函数返回,静态变量也不会被销毁。
示例:
1 | void some_function() { |
在这个示例中,counter
是一个静态变量。每次调用 some_function
时,counter
都会增加 1。
优点:
- 允许函数在调用之间记住信息。
- 可以用于实现计数器、累加器和其他需要在函数调用之间保留状态的变量。
注意事项:
- 静态变量只在声明它们的函数中可见。
- 使用静态变量时要小心,因为它们可能会导致意外的行为,例如函数间共享意外的状态。
类型定义
类型定义(typedef
)允许你创建新的数据类型,这些类型是现有类型的别名。
语法:
1 | typedef existing_type new_type_name; |
示例:
1 | typedef int my_int; |
现在,my_int
可以像 int
一样使用:
1 | my_int a = 10; |
优点:
- 提高代码可读性和可维护性。
- 允许你创建特定于应用程序的自定义类型。
- 可以简化复杂数据类型的声明。
注意事项:
- 类型定义不会创建新的数据类型,而是创建别名。
- 类型定义只在声明它们的文件中有效。
枚举类型
枚举类型(enum
)允许你创建具有固定且有限值的自定义类型。
语法:
1 | enum enum_name { |
示例:
1 | enum colors { |
现在,colors
类型可以像这样使用:
1 | enum colors color = RED; |
优点:
- 提高代码可读性和可维护性。
- 确保变量只能取预定义的值。
- 可以简化复杂数据类型的声明。
注意事项:
- 枚举类型中的值从 0 开始自动编号,除非显式指定。
- 枚举类型只在声明它们的文件中有效。
结构体
结构体(struct
)允许你创建包含不同类型数据的复合数据类型。
语法:
1 | struct struct_name { |
示例:
1 | struct person { |
现在,person
类型可以像这样使用:
1 | struct person john; |
优点:
- 组织和存储相关数据。
- 简化复杂数据类型的声明。
- 提高代码可读性和可维护性。
注意事项:
- 结构体中的成员可以通过点运算符 (.) 访问。
- 结构体是按值传递的,这意味着对结构体变量的更改不会影响原始结构体。
命令行参数
命令行参数允许你从命令行向 C 程序传递数据。
获取命令行参数:
1 | int main(int argc, char *argv[]) { |
示例:
1 | int main(int argc, char *argv[]) { |
运行:
1 | ./program argument1 argument2 |
输出:
1 | 第一个命令行参数:argument1 |
注意事项:
argc
始终至少为 1,因为它包括程序名称。argv[0]
是程序名称。- 命令行参数以字符串形式传递。
头文件
头文件(.h
)用于在多个源文件中共享函数、变量和类型定义。
优点:
- 模块化:将代码组织成更小的、可管理的块。
- 代码重用:允许在多个源文件中使用相同的代码。
- 可维护性:更容易更新和维护代码。
使用头文件:
- 包含头文件:使用
#include
指令将头文件包含到源文件中:
1 |
- 声明函数和变量:在头文件中声明函数和变量,以便可以在其他源文件中使用它们:
1 | // header.h |
- 定义函数和变量:在源文件中定义函数和变量:
1 | // source.c |
注意事项:
- 头文件通常以
.h
为扩展名。 - 头文件应该只包含声明,而不应该包含定义。
- 避免在头文件中使用
#define
,因为这可能会导致编译器错误。
预处理器
预处理器是在编译器处理源代码之前对源代码进行处理的程序。
功能:
- 宏定义:使用
#define
预处理器指令定义宏,就像常量一样。 - 条件编译:使用
#ifdef
、#ifndef
、#if
、#else
和#endif
预处理器指令根据条件编译代码块。 - 文件包含:使用
#include
预处理器指令将头文件包含到源文件中。 - 其他功能:还提供其他功能,如错误处理和调试。
示例:
1 |
|
优点:
- 简化代码:宏定义可以简化重复的代码。
- 条件编译:允许根据条件编译不同的代码块。
- 模块化:通过包含头文件实现代码模块化。
注意事项:
- 预处理器指令不是 C 语言的一部分,而是编译器处理源代码之前执行的。
- 使用预处理器时要小心,因为滥用它可能会导致难以调试的代码。
条件编译
允许根据条件编译代码块。
语法:
1 |
|
示例:
1 |
|
优点:
- 根据条件编译不同的代码块。
- 允许在不同的环境(例如调试和发布)中编译代码。
- 提高代码的可维护性。
注意事项:
- 条件编译指令不是 C 语言的一部分,而是编译器处理源代码之前执行的。
- 使用条件编译时要小心,因为滥用它可能会导致难以调试的代码。
符号常量
符号常量(也被称为宏)使用 #define
预处理器指令定义。
语法:
1 |
示例:
1 |
优点:
- 简化代码:符号常量可以简化重复的代码。
- 提高可读性:符号常量使代码更易于理解。
- 提高可维护性:符号常量更容易更新和维护。
注意事项:
- 符号常量不是变量,它们的值在编译时确定。
- 使用符号常量时要小心,因为滥用它可能会导致难以调试的代码。
#ifdef
预处理器指令检查某个符号常量或宏是否被定义过。
语法:
1 |
|
示例:
1 |
|
优点:
- 根据符号常量或宏是否被定义来编译不同的代码块。
- 允许在不同的环境(例如调试和发布)中编译代码。
- 提高代码的可维护性。
注意事项:
#ifdef
指令不是 C 语言的一部分,而是编译器处理源代码之前执行的。- 使用
#ifdef
时要小心,因为滥用它可能会导致难以调试的代码。
你可以使用的预定义的符号常量
预处理器还定义了很多你可以直接使用的符号常量,它们的名字的前后有两个下划线作为标识,包括:
__LINE__
代表源代码文件中的当前行__FILE__
代表文件的名字__DATE__
表示编译日期,格式为Mmm gg aaaa
__TIME__
表示编译实践,格式为hh:mm:ss
结语
先挖个坑,只写了基础内容,具体的例子或实例什么时候想起来了,再详细补充。