循环
循环(loop)是重复执行其他语句(循环体)的一种语句。在C语言中,每个循环都有一个控制表达式(controlling expression)。每次执行循环体(循环重复一次)时都要对控制表达式求值。如果表达式为真(即值不为零),那么继续执行循环。C语言提供了3种重复语句,即while语句、do语句和for语句,下面我们一起来学习一下循环的关键知识和相关示例。
while语句
在C语言中,while
语句是一种循环控制结构,它允许你重复执行一段代码,直到给定的条件不再满足。这种循环特别有用,当你不确定需要迭代多少次时,只要条件为真(值为非零),循环就会继续执行。下面是while
循环的基本语法:
while (条件) {
// 要执行的代码块
}
这里的“条件”是一个表达式,编译器会在每次循环开始前评估它。如果条件为真(即,结果不为零),代码块就会被执行。一旦条件为假(结果为零),循环就会停止,程序流程会继续执行while
循环之后的代码。
举个例子,使用while
循环来计算从1到10的和:
#include <stdio.h>
int main(void) {
int sum = 0;
int i = 1;
while (i <= 10) {
sum += i; // 将i的值加到sum上
i++; // 增加i的值,准备下一次循环
}
printf("Sum from 1 to 10 is: %d\n", sum);
return 0;
}
在这个例子中,while
循环会一直执行,直到变量i
的值超过10。每次循环,都会把i
的当前值加到sum
上,然后i
自增1。
实际上还有一些特殊的使用方法,无限循环就是其中之一。如果控制表达式的值始终非零,while语句将无法终止。事实上,C程序员有时故意用非零常量作为控制表达式来构造无限循环。除非循环体中含有跳出循环控制的语句(break、goto、return)或者调用了导致程序终止的函数,否则上述形式的while语句将永远执行下去。
while
循环是C语言中实现迭代的核心机制之一,它简单而强大,但也需要谨慎使用,以避免造成无限循环,即条件永远为真,导致程序无法继续执行下去
do语句
do
…while
语句是另一种循环控制结构,它与while
语句相似,但有一点关键的区别:do
…while
循环至少会执行一次,因为条件检查位于循环的末尾,先执行do里的内容,再去while里判断。这意味着不管条件是否为真,循环体内的代码都会先执行一次,然后才评估条件以决定是否继续执行循环。
下面是do
…while
循环的基本语法:
do {
// 要执行的代码块
} while (条件);
这里的“条件”是一个表达式,与while
循环中的条件相同。只要条件为真(即,结果不为零),循环就会继续执行。如果条件为假(结果为零),循环就会停止。
举个例子,使用do
…while
循环来提示用户输入一个正数:
#include <stdio.h>
int main(void) {
int number;
do {
printf("Please enter a positive number: ");
scanf("%d", &number);
} while (number <= 0);
printf("You entered: %d\n", number);
return 0;
}
在这个例子中,程序会提示用户输入一个数,然后检查这个数是否为正。如果输入的数不是正数(小于或等于0),循环会再次执行,继续提示用户输入,直到用户输入一个正数为止。
do
…while
循环特别适用于至少需要执行一次的循环体,或者当循环的继续执行不完全取决于循环开始前的条件,而是与循环体内的操作结果有关的情况。
for语句
for
语句作为功能最强大的一种循环,是一种基于计数的循环控制结构,它允许你指定一个初始化表达式、一个循环继续的条件,以及一个在每次循环迭代后执行的表达式。for
循环通常用于你知道需要迭代固定次数的情况。
下面是for
循环的基本语法:
for (初始化表达式; 条件表达式; 增量表达式) {
// 要执行的代码块
}
- 初始化表达式:在循环开始前执行一次,通常用于设置循环计数器的初始值。
- 条件表达式:在每次循环迭代前评估。如果结果为真(非零),则执行循环体内的代码块。如果结果为假(零),则循环终止。
- 增量表达式:在每次循环迭代结束时执行,通常用于更新循环计数器的值。
举个例子,使用for
循环来打印0到9的数字:
#include <stdio.h>
int main(void) {
int i;
for (i = 0; i < 10; i++) {
printf("%d ", i);
}
printf("\n");
return 0;
}
在这个例子中,i
是从0开始的计数器,每次循环后i
的值会增加1(i++
)。循环会继续执行,直到i
的值达到10。
for
循环非常适合于当你需要对一系列值进行迭代操作时,例如遍历数组或执行固定次数的重复任务。由于for
循环的结构清晰,它也有助于提高代码的可读性。
多个初始化表达式
如果你想在 for
循环的初始化阶段同时初始化多个变量,可以使用逗号表达式:
for (int i = 0, j = 10; i < 10; i++, j--) {
// 循环体,这里i从0开始递增,j从10开始递减
}
更新表达式中的多个自增操作
如果你想在每次循环迭代结束时同时更新多个变量,可以在更新表达式中使用逗号表达式:
for (int i = 0; i < 10; i++, j++) {
// 循环体,这里i和j每次迭代后都会递增
int j = 5; // 循环体内j的局部变量,不影响外层的j
}
多个初始化和更新表达式
如果需要同时在初始化和更新阶段使用多个表达式,可以这样做:
for (int i = 0, j = 10; i < 5; i++, j--) {
// 循环体
}
使用逗号表达式进行复杂的更新操作
有时候,你可能想要在更新表达式中执行更复杂的操作,比如同时自增和执行函数调用:
for (int i = 0; i < 10; i++, process(i)) {
// 循环体
}
void process(int value) {
// 对value进行处理
}
在这个例子中,每次循环迭代结束时,变量 i
会增加,同时调用 process
函数。
注意事项
- 逗号表达式的结果是最后一个表达式的值。在使用逗号表达式时,要确保最后一个表达式是你期望的结果。
- 逗号表达式可以提高代码的紧凑性,但过度使用可能会降低代码的可读性。因此,应该在提高代码简洁性和保持代码清晰性之间找到平衡。
- 在使用逗号表达式时,要确保所有表达式都是有效的,并且它们执行的顺序是从左到右。
退出循环
前面我们学习了循环的三种用法,但是程序执行到某一特定条件后,往往是要终止循环的。有些时候也会需要在循环中间设置退出点,甚至可能需要对循环设置多个退出点break语句可以用于有上述这些需求的循环中。在C语言中,break
、continue
和 goto
语句都是控制程序流程的语句,它们在循环或条件分支中非常有用。下面是对这些语句的详细说明:
break 语句
break
语句用于立即终止包含它的最内层循环或 switch
语句。当执行到 break
时,程序会跳出当前的循环体或 switch
语句,继续执行后面的代码。
for (int i = 0; i < 10; i++) {
if (i == 5) {
break; // 当 i 等于 5 时,终止循环
}
printf("%d ", i);
}
// 输出: 0 1 2 3 4
continue 语句
continue
语句用于跳过当前循环的剩余部分,并立即开始下一次迭代。它只影响当前的迭代,不会导致整个循环的终止。
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue; // 跳过偶数,不执行下面的打印语句
}
printf("%d ", i);
}
// 输出: 1 3 5 7 9
goto 语句
goto
语句允许程序跳转到程序中预先定义的标签位置。由于 goto
可以跳出多层循环或条件分支,使用不当会导致程序难以理解和维护,因此通常不推荐使用。
#include <stdio.h>
int main() {
int i, j;
label:
for (i = 0; i < 3; i++) {
for (j = 0; j < 3; j++) {
if (i == 1 && j == 1) {
goto end; // 跳转到 end 标签
}
printf("%d ", i * j);
}
printf("\n");
}
end:
printf("Jumped out of the nested loops.\n");
return 0;
}
// 输出:
// 0 0 0
// 0 0 0
// Jumped out of the nested loops.c
使用场景和最佳实践
- break 语句:当需要在满足特定条件时立即退出循环时使用。它适用于循环中需要提前终止的情况。
- continue 语句:当需要跳过当前迭代的剩余部分,但希望继续执行循环的下一次迭代时使用。它适用于需要根据条件部分执行循环体的情况。
- goto 语句:尽管
goto
提供了强大的跳转能力,但由于它可能导致代码难以理解和维护,因此应尽量避免使用,除非在特定情况下(如错误处理或复杂的循环结构)没有更好的替代方案。
在实际编程中,推荐使用结构化控制语句(如 if
、else
、for
、while
等)来提高代码的可读性和可维护性。如果确实需要使用 goto
,应该确保跳转逻辑清晰,并且尽量避免跳过重要的代码块,如资源释放或错误处理。
空语句
在C语言中,空语句(也称为空表达式或空操作)是一个不执行任何操作的语句,其语法形式为一个分号(;
)。空语句通常用于循环或条件分支语句中,作为占位符或当需要一个语句但不需要执行任何操作时使用。
语法
空语句的语法非常简单,如下所示:
;
使用场景
-
循环体为空:当你需要创建一个循环,但当前不需要在循环体中执行任何操作时,可以使用空语句作为循环体。
for (int i = 0; i < 10; i++) { ; // 空循环体,不执行任何操作 }
-
条件分支的默认行为:在某些条件分支中,可能某些分支不需要执行任何操作,此时可以使用空语句。
if (condition) { // 条件满足时执行的代码 } else { ; // 条件不满足时不执行任何操作 }
-
宏定义:在宏定义中,有时需要一个语句作为宏的结束,但又不想执行任何操作,空语句可以作为占位符。
#define EMPTY_STATEMENT do { } while (0)
-
循环迭代的占位符:在某些算法中,可能需要一个循环来等待某个条件成立,但又不想在每次迭代中执行任何操作。
while (!conditionMet) { ; // 等待条件成立,不执行任何操作 }
-
语法要求:在某些情况下,语法要求必须有一个语句,即使不需要执行任何操作。
注意事项
- 虽然空语句不执行任何操作,但它仍然是一个有效的语句,因此在需要语句的地方使用空语句是合法的。
- 过度使用空语句可能会使代码的意图不明确,因此在可能的情况下,应该尽量避免使用空语句,或者使用注释来说明为什么这里没有执行任何操作。
- 在某些情况下,使用空语句可能不如使用
continue
或break
语句来得清晰和直观。
基本类型
整数类型
C语言提供了多种整数类型,用于存储不同范围的整数值。以下是C语言中常见的整数类型:
int
- 基本的整数类型,通常用于存储整数。short
-short int
的缩写,表示短整型,占用的内存空间比int
少。long
-long int
的缩写,表示长整型,占用的内存空间比int
多。long long
- 表示更长的整型,占用的内存空间比long int
多。
除了这些基本类型,C语言还提供了有符号和无符号的变体:
- 有符号类型:可以存储正数、负数和零。
- 无符号类型:只能存储非负数(正数和零)。
具体类型如下:
signed int
- 有符号的int
。unsigned int
- 无符号的int
。signed short
或signed short int
- 有符号的short
。unsigned short
或unsigned short int
- 无符号的short
。signed long
或signed long int
- 有符号的long
。unsigned long
或unsigned long int
- 无符号的long
。signed long long
或signed long long int
- 有符号的long long
。unsigned long long
或unsigned long long int
- 无符号的long long
。
大小和范围
每种整数类型占用的内存大小和可以表示的数值范围可能会根据编译器和平台的不同而有所变化,但通常:
char
:通常是8位,范围是 -128 到 127(有符号)或 0 到 255(无符号)。short
:通常是16位。int
:通常是16位或32位。long
:通常是32位或64位。long long
:通常是64位。
例如,对于一个标准的32位系统,int
通常是32位,范围大约是 -2,147,483,648 到 2,147,483,647(有符号)或 0 到 4,294,967,295(无符号)。long
可能是32位,与 int
相同,也可能是64位。long long
通常是64位,范围大约是 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807(有符号)或 0 到 18,446,744,073,709,551,615(无符号)。
使用建议
- 根据需要存储的数值范围选择合适的整数类型。
- 如果不确定具体的数值范围,可以使用
int
作为默认选择。 - 如果需要存储非常大的整数,可以使用
long long
。 - 如果存储的值不会是负数,可以考虑使用无符号类型,这样可以增加可以表示的数值范围。
- 使用
sizeof()
函数可以检查特定类型在特定平台上的占用字节数。
了解这些整数类型及其特性对于编写有效和高效的C语言程序非常重要。
浮点类型
浮点类型主要用于存储带有小数的数值,它们提供了不同的精度和数值范围。以下是C语言中定义的浮点类型:
float
- 单精度浮点数,通常占用4个字节(32位),提供大约6到7位的十进制数字精度。double
- 双精度浮点数,通常占用8个字节(64位),提供大约15到16位的十进制数字精度。long double
- 扩展精度浮点数,占用的字节数可能比double
更多(通常是12字节或16字节),提供更高的精度和更大的数值范围。
浮点数的表示
浮点数在内存中的表示通常基于IEEE 754标准,该标准定义了浮点数的存储方式,包括:
- 符号位(Sign bit):1位,确定数值的正负。
- 指数位(Exponent bits):用于存储数值的范围。
- 尾数位(Fraction/Mantissa bits):用于存储数值的精度。
浮点数的特性
- 精度:浮点数可以表示非常精确的小数值,但这种精度是有限的,并且受到存储位数的限制。
- 范围:浮点数可以表示非常大或非常小的数值,具体范围取决于其类型(
float
、double
或long double
)。 - 舍入误差:由于浮点数的表示精度有限,某些小数值可能无法精确表示,导致舍入误差。
- 特殊值:包括正无穷大、负无穷大和NaN(Not a Number)。
使用浮点数时的注意事项
- 避免等值比较:由于浮点数的舍入误差,直接比较两个浮点数是否相等可能不总是正确的。
- 使用epsilon:在需要比较两个浮点数是否足够接近时,可以定义一个小的阈值(epsilon),如果两个数的差的绝对值小于这个阈值,则认为它们相等。
- 数值稳定性:在进行数值计算时,应该考虑数值稳定性,避免由于舍入误差导致的计算错误。
浮点数的初始化
浮点数可以使用以下方式进行初始化:
float f = 3.14159f; // 显式指定为float类型
double d = 3.14159;
long double ld = 3.14159L; // 显式指定为long double类型
浮点数的格式化输出
使用 printf
函数时,可以使用以下格式化说明符来输出浮点数:
%f
:输出float
或double
类型的浮点数。%Lf
:输出long double
类型的浮点数(需要与long double
类型的变量一起使用)。
例如:
printf("The value of pi is: %f\n", 3.14159);
了解浮点数的特性和使用注意事项对于编写涉及实数计算的C语言程序非常重要。
字符类型
字符类型用于存储单个字符,其基本类型是 char
。char
类型的大小通常是一个字符,具体取决于编译器和平台,但通常是8位(1字节)。以下是一些与字符类型相关的要点:
-
存储范围:标准的
char
类型可以存储的值的范围是 -128 到 127(有符号)或 0 到 255(无符号)。这取决于是否定义了_CHAR_UNSIGNED
或使用了unsigned char
。 -
无符号字符类型:
unsigned char
可以存储的值的范围是 0 到 255。它不能存储负数。 -
字符常量:在C语言中,字符常量用单引号括起来,例如
'A'
、'a'
、'1'
、' '
(空格)和'\0'
(空字符,即字符串结束符)。 -
转义序列:C语言支持多种转义序列,例如:
\\
表示反斜杠字符本身。\'
表示单引号。\"
表示双引号。\n
表示换行符。\t
表示制表符。\b
表示退格符。\0
或\0
表示空字符。
-
ASCII值:在C语言中,字符常量通常表示其ASCII值。例如,
'A'
的ASCII值是65,'a'
是97。 -
字符数组和字符串:
char
类型的数组可以用于存储字符串,字符串是以空字符'\0'
结尾的字符序列。例如:char str[] = "Hello, World!";
这里
str
是一个字符数组,存储了字符串 “Hello, World!”,末尾跟着一个空字符。 -
字符的输入和输出:
- 使用
scanf()
函数读取字符:scanf(" %c", &c);
- 使用
printf()
函数打印字符:printf("%c", c);
- 使用
-
字符类型提升:在算术表达式中,
char
类型的值会提升为int
类型,然后再进行计算。 -
字符类型的大小:可以使用
sizeof
操作符来确定char
类型在特定平台上的大小。 -
宽字符和多字节字符:C语言还支持宽字符(
wchar_t
)和多字节字符(通过使用多字节编码,如UTF-8),这些类型用于表示更广泛的字符集。
类型转换
类型转换是将一个类型的值转换成另一个类型的值的过程。类型转换分为两种:隐式类型转换(也称为自动类型转换)和显式类型转换(也称为强制类型转换)。
隐式类型转换(自动类型转换)
隐式类型转换是由编译器自动执行的,通常发生在不同数据类型运算时,以确保运算能够进行。转换规则如下:
- 从较小范围的类型到较大范围的类型:例如,
char
或short int
转换为int
。 - 浮点数到整数:例如,
float
或double
转换为int
,小数部分将被截断。 - 整数到浮点数:例如,
int
转换为float
或double
,数值将被近似表示。
隐式类型转换的例子:
int a = 10;
double b = a; // 隐式将int转换为double
显式类型转换(强制类型转换)
显式类型转换是由程序员明确指定的,需要使用圆括号将目标类型括起来,放在要转换的值前面。这种转换可以是安全的,也可以是不安全的,取决于转换的类型和值。
显式类型转换的例子:
double a = 3.14159;
int b = (int)a; // 显式将double转换为int,小数部分被截断
类型转换的注意事项:
- 精度损失:将浮点数转换为整数时,小数部分将被丢弃,可能导致精度损失。
- 数值溢出:将一个较大范围的数值转换为较小范围的类型时,可能会发生溢出。
- 符号问题:在将有符号类型转换为无符号类型时,如果数值是负数,其行为是未定义的。
- 指针和整数之间的转换:可以将指针转换为整型,也可以将整型转换为指针,但这种转换的使用需要谨慎,以避免未定义行为。
- 可移植性问题:不同类型的大小和表示可能因编译器和平台而异,因此在进行类型转换时,需要考虑代码的可移植性。
正确的类型转换实践:
- 仅在确实需要时才进行类型转换。
- 了解不同类型之间的范围和表示,以避免不可预料的结果。
- 在进行可能不安全的转换时,使用条件语句检查值的范围,以确保安全。
- 在编写需要高度可移植性的代码时,避免依赖特定平台的类型特性。
类型定义
typedef
关键字用于为现有的数据类型创建一个新的名称,这个过程称为类型定义或类型别名。使用 typedef
可以提高代码的可读性和可维护性。下面是 typedef
的基本用法:
基本语法
typedef existing_type new_type_name;
这里的 existing_type
是已经存在的数据类型,new_type_name
是为这个类型定义的新名称。
示例
- 为基本类型定义新名称:
typedef int Integer;
Integer myInteger = 10; // 使用新类型名称Integer
- 为结构体定义新名称:
typedef struct {
int x;
int y;
} Point;
Point myPoint; // 创建一个Point类型的变量myPoint
- 为指针类型定义新名称:
typedef int *IntegerPointer;
IntegerPointer ptr = &myInteger; // ptr是一个指向Integer的指针
- 为函数指针定义新名称:
typedef int (*FunctionPointer)(int, int);
FunctionPointer fp = add; // 假设add是一个接受两个int参数并返回int的函数
- 为数组类型定义新名称:
typedef int ArrayType[10];
ArrayType myArray; // myArray是一个包含10个int元素的数组
- 为枚举类型定义新名称:
typedef enum { RED, GREEN, BLUE } Color;
Color myColor = GREEN; // 使用新类型名称Color
注意事项
typedef
创建的是别名,不是新类型。因此,使用typedef
定义的类型与原始类型在内存中的表现是相同的。typedef
可以提高代码的可读性,特别是在处理复杂的类型时,如结构体或函数指针。typedef
可以嵌套使用,为更复杂的类型定义别名。- 在头文件中使用
typedef
可以避免在多个源文件中重复定义相同的类型。 typedef
应该谨慎使用,以避免混淆。例如,避免为基本类型(如int
或float
)定义过多别名,因为这可能会使代码难以理解。
类型定义是C语言中一个非常有用的工具,它允许程序员为复杂的或常用的数据类型创建易于理解和使用的别名。
sizeof运算符
sizeof
运算符在C语言中用于确定变量或类型在内存中的大小(以字节为单位)。它是编译时运算符,意味着编译器在编译期间就计算出结果,而不是在程序运行时。
基本用法
- 获取类型的大小:
int size = sizeof(int);
- 获取变量的大小:
int var;
int size = sizeof(var);
- 获取数组单个元素的大小:
int arr[10];
int size = sizeof(arr[0]); // 获取数组单个元素的大小
- 获取数组的总大小:
int arr[10];
int size = sizeof(arr); // 获取整个数组的大小
- 获取指针类型的大小:
int *p;
int size = sizeof(p); // 获取指针的大小,而不是它指向的数据的大小
- 获取结构体的大小:
struct MyStruct {
int a;
char b;
};
int size = sizeof(struct MyStruct);
注意事项
sizeof
运算符的结果是一个size_t
类型的值,这是一个无符号整数类型,定义在<stddef.h>
头文件中。sizeof
可以应用于类型名,而不需要实例化该类型的对象,例如sizeof(int)
。sizeof
运算符不会计算数组中元素的个数,它只返回数组单个元素的大小,例如sizeof(int[10])
将返回10 * sizeof(int)
。sizeof
运算符可以用于任何数据类型,包括基本数据类型、结构体、联合体、枚举和指针。sizeof
运算符的结果通常在编译时就已知,但当应用于动态分配的内存(如malloc
分配的内存)时,它将返回分配的总字节数,而不是指针变量的大小。
示例
#include <stdio.h>
int main() {
int i = 100;
char c = 'a';
double d;
int arr[10];
struct MyStruct {
int a;
char b;
} myStruct;
printf("Size of i: %zu bytes\n", sizeof(i));
printf("Size of c: %zu bytes\n", sizeof(c));
printf("Size of d: %zu bytes\n", sizeof(d));
printf("Size of arr: %zu bytes\n", sizeof(arr));
printf("Size of myStruct: %zu bytes\n", sizeof(myStruct));
return 0;
}
在这个示例中,%zu
是 size_t
类型的格式化输出说明符。使用 sizeof
运算符可以方便地获取不同类型和变量在内存中的大小,这对于内存管理和数据结构的设计非常有用。
案例
下面是一个简单的C语言程序示例,它结合了基本数据类型、类型转换、sizeof
运算符以及一些简单的输入输出操作。这个程序将提示用户输入一个整数和两个浮点数,然后计算整数的平方、浮点数的和以及它们的平均值,并将结果打印到控制台。
#include <stdio.h>
int main() {
// 定义基本数据类型变量
int integer;
float float1, float2;
// 打印提示信息
printf("Please enter an integer: ");
scanf("%d", &integer);
// 使用sizeof运算符
printf("Size of integer: %zu bytes\n", sizeof(integer));
// 提示用户输入两个浮点数
printf("Please enter two floating point numbers: ");
scanf("%f %f", &float1, &float2);
// 使用sizeof运算符
printf("Size of float1: %zu bytes\n", sizeof(float1));
printf("Size of float2: %zu bytes\n", sizeof(float2));
// 计算整数的平方
int square = integer * integer;
// 计算浮点数的和与平均值
float sum = float1 + float2;
float average = sum / 2;
// 打印结果
printf("The square of %d is %d\n", integer, square);
printf("The sum of %f and %f is %f\n", float1, float2, sum);
printf("The average of %f and %f is %f\n", float1, float2, average);
// 演示类型转换
float integerValue = (float)integer; // 将整数转换为浮点数
printf("Integer converted to float: %f\n", integerValue);
return 0;
}
这个程序首先包含了 stdio.h
头文件,这是因为我们需要使用 printf
和 scanf
函数。然后定义了三个变量:一个 int
类型的变量 integer
,两个 float
类型的变量 float1
和 float2
。
程序接着提示用户输入一个整数和两个浮点数。输入的整数用于计算其平方,而两个浮点数用于计算它们的和与平均值。
程序中使用了 sizeof
运算符来打印 integer
、float1
和 float2
变量的大小。这有助于理解不同数据类型在内存中所占用的空间。
最后,程序演示了类型转换,将整数 integer
转换为浮点数 integerValue
并打印出来。
这个基本的C语言程序案例,涵盖了变量声明、用户输入、计算、类型转换和 sizeof
运算符的使用,,通过学习这个案例,能够帮助我们更快的掌握C语言的基本类型等相关知识。
数组
一维数组
一维数组是C语言中处理集合数据的基本工具,掌握其使用方法对于编写有效和高效的C程序至关重要。
一维数组多用于存储相同类型的多个元素。数组中的每个元素可以通过索引(下标)访问,索引从0开始。以下是一维数组的一些关键特性和用法:
声明一维数组
要声明一个一维数组,你需要指定元素的类型和数组的名称,后面跟着方括号中的数组大小。例如,声明一个包含10个整数的数组:
int myArray[10];
初始化一维数组
在声明数组的同时,你可以初始化数组元素:
int myArray[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
如果数组的所有元素都是同一类型,并且你希望它们初始化为0,你可以只指定数组的大小:
int myArray[10] = {0}; // 所有元素初始化为0
编译器会自动将剩余的元素初始化为0。
访问数组元素
使用索引来访问数组中的元素,索引从0开始:
int firstElement = myArray[0]; // 获取第一个元素
myArray[1] = 10; // 设置第二个元素的值为10
数组的大小
你可以使用 sizeof
运算符来获取数组的大小(以字节为单位):
size_t arraySize = sizeof(myArray);
如果你只需要知道数组的元素个数,可以使用以下方法:
int elementCount = sizeof(myArray) / sizeof(myArray[0]);
遍历一维数组
通常使用循环来遍历数组中的所有元素:
for (int i = 0; i < sizeof(myArray) / sizeof(myArray[0]); ++i) {
printf("Element at index %d: %d\n", i, myArray[i]);
}
多维数组与一维数组的关系
多维数组本质上是一维数组的扩展。例如,二维数组实际上是以一维数组为元素的数组,可以通过以下方式声明和初始化:
int my2DArray[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
一维数组作为函数参数
数组名作为函数参数时,它会被转换为指向数组第一个元素的指针:
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int myArray[5] = {1, 2, 3, 4, 5};
printArray(myArray, 5);
return 0;
}
一维数组的使用场景:
- 存储和管理数据集合:例如,一个班级的学生成绩。
- 实现简单的数据结构:如栈(stack)和队列(queue)。
- 算法实现:排序算法(如冒泡排序、选择排序)、搜索算法(如线性搜索)等。
- 函数参数传递:用于传递多个参数,或将数组作为参数传递给函数。
- 字符串处理:在C语言中,字符串通常以字符数组的形式存储,以空字符(
'\0'
)结尾。
高级用法:
-
变长数组(VLA):C99标准引入了变长数组,其大小可以在运行时确定。
int size = 100; int vla[size];
注意:并非所有编译器都支持VLA。
-
指针和数组的关系:数组名在大多数情况下可以作为指向数组首元素的指针使用。
int arr[10]; int *ptr = arr; // ptr是指向arr第一个元素的指针
-
指针数组:数组的元素是指针,可以存储多个指向不同数据的指针。
int *ptrArray[10]; int a = 5, b = 10; ptrArray[0] = &a; ptrArray[1] = &b;
-
函数返回数组:虽然C语言的函数不能直接返回数组,但可以返回指向数组的指针。
int* createArray(int size) { int *arr = malloc(size * sizeof(int)); // 初始化数组... return arr; }
-
数组作为结构体成员:可以将数组包含在结构体中,以创建更复杂的数据结构。
注意事项:
- 数组越界:访问数组时,确保索引在有效范围内,避免数组越界。
- 内存分配:当使用动态内存分配(如
malloc
)创建数组时,必须手动释放内存以避免内存泄漏。 - 数组初始化:静态存储期的数组(如全局数组)如果不显式初始化,其元素的值是未定义的。
- 数组作为函数参数:数组作为函数参数时,实际上是通过指向数组第一个元素的指针传递的,因此函数无法直接获取数组的大小。
- 数组复制:直接使用赋值操作符复制数组(如
arr1 = arr2;
)只复制了指针,而不是数组内容。要复制数组内容,需要使用memcpy
函数或手动复制每个元素。 - 多维数组:虽然多维数组是一维数组的扩展,但它们的索引和内存布局可能更复杂,需要仔细处理。
- 数组的类型安全:C语言的数组不是类型安全的,因此在使用数组时应确保不违反类型规则。
二维数组
二维数组在C语言中是多维数组的一种,通常用于存储矩阵或表格形式的数据。它实际上是一个数组的数组,也就是说,每个元素本身也是一个数组。这使得二维数组在处理需要按行和列组织的数据时非常有用。
声明二维数组
二维数组的声明需要指定列数和行数,以及元素的数据类型,二维数组和矩阵差不多。例如,声明一个3行4列的整数二维数组:
int myArray[3][4];
初始化二维数组
在声明二维数组的同时,你可以初始化它,以下是一个三行四列的二维数组:
int myArray[3][4] = {
{0, 1, 2, 3}, // 第一行
{4, 5, 6, 7}, // 第二行
{8, 9, 10, 11} // 第三行
};
如果所有元素都初始化为同一值,可以简化为:
int myArray[3][4] = {
{0} // 所有元素初始化为0
};
访问二维数组元素
使用两个索引来访问二维数组中的元素,第一个索引表示行,第二个索引表示列:
int firstElement = myArray[0][1]; // 获取第一行第二列的元素,即1
myArray[1][2] = 20; // 设置第二行第三列的元素为20
获取二维数组的大小
由于二维数组本质上是一维数组,所以 sizeof
运算符只能直接给出整个二维数组的大小,而不是行或列的大小:
size_t arraySize = sizeof(myArray);
int rows = sizeof(myArray) / sizeof(myArray[0][0]); // 行数
int cols = sizeof(myArray[0]) / sizeof(myArray[0][0]); // 列数
遍历二维数组
通常使用嵌套循环来遍历二维数组:
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("Element at [%d][%d]: %d\n", i, j, myArray[i][j]);
}
}
高级用法
-
指针的指针:可以将二维数组视为指针的指针,这使得你可以更灵活地操作数组:
int (*ptr)[4] = myArray; // ptr是指向含有4个int元素的数组的指针 (*(ptr + 1))[2] = 15; // 等同于 myArray[1][2] = 15;
-
传递给函数:二维数组可以作为参数传递给函数,但通常需要指定列数:
void processArray(int arr[][4], int rows) { // ... }
-
动态分配:二维数组可以动态分配,但通常需要分配行指针数组,然后为每行分配空间:
int **dynamicArray = malloc(rows * sizeof(int*)); for (int i = 0; i < rows; i++) { dynamicArray[i] = malloc(cols * sizeof(int)); }
注意事项
- 二维数组的列数必须是已知的,因为它决定了每个行数组的大小。
- 行数可以在运行时确定,特别是当使用动态分配时。
- 在使用完动态分配的二维数组后,需要逐行释放内存,然后释放行指针数组本身。
- 二维数组作为函数参数时,通常需要指定每行的列数,以便函数知道每行的大小。
二维数组是处理表格数据的强大工具,但使用时需要注意内存分配和释放,以及正确地传递数组尺寸信息。
多维数组
除了一维数组和二维数组,C语言中还存在多维数组,二维数组也属于多维数组。多维数组是C语言中一种强大的数据结构,用于按多个维度组织数据。最常见的多维数组是二维数组,但C语言实际上支持更高维度的数组,如三维数组或更高。多维数组在形式上可以看作是数组的数组,即数组的每个元素本身又是一个数组。
声明多维数组
多维数组的声明和二维数组是差不多的,相同的是,都需要指定每个维度的大小和数组的元素类型。例如,声明一个3x4x5的三维整型数组:
int my3DArray[3][4][5];
初始化多维数组
多维数组的初始化需要为每个维度的每个元素赋值。
下面初始化了一个具有三个四行五列数组元素的数组:
int my3DArray[3][4][5] = {
{
{0, 1, 2, 3, 4}, // 第一个3维数组
{5, 6, 7, 8, 9},
{10, 11, 12, 13, 14},
{15, 16, 17, 18, 19}
},
// ... 其他维度的初始化
};
访问多维数组元素
使用多个索引来访问多维数组中的元素,每个索引对应一个维度:
int element = my3DArray[2][1][3]; // 访问第三行第二列第五个元素
获取多维数组的大小
获取多维数组的总大小和每个维度的大小可以使用 sizeof
运算符:
size_t totalSize = sizeof(my3DArray);
size_t size1 = totalSize / sizeof(my3DArray[0]);
size_t size2 = sizeof(my3DArray[0]) / sizeof(my3DArray[0][0]);
size_t size3 = sizeof(my3DArray[0][0]) / sizeof(my3DArray[0][0][0]);
遍历多维数组
遍历多维数组需要使用多层嵌套循环,每个循环对应一个维度:
for (int i = 0; i < size1; i++) {
for (int j = 0; j < size2; j++) {
for (int k = 0; k < size3; k++) {
printf("Element at [%d][%d][%d]: %d\n", i, j, k, my3DArray[i][j][k]);
}
}
}
高级用法
- 指针数组:多维数组视为指针数组,其中每个元素都是指向下一个维度的指针,这意味着你可以操作指向数组的指针。
复制int *arrayOfPointers[3]; // 一个指向int的指针数组,有3个元素
for (int i = 0; i < 3; i++) {
arrayOfPointers[i] = malloc(4 * sizeof(int)); // 为每个数组分配内存
for (int j = 0; j < 4; j++) {
arrayOfPointers[i][j] = i * 4 + j; // 初始化数组
}
}
// 使用指针数组
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("Element at [%d][%d]: %d\n", i, j, arrayOfPointers[i][j]);
}
}
// 释放内存
for (int i = 0; i < 3; i++) {
free(arrayOfPointers[i]);
}
- 动态分配:多维数组可以动态分配,通常需要为每个维度分配指针数组,然后为每个子数组分配内存。
复制int **dynamicArray = malloc(3 * sizeof(int*)); // 为行指针分配内存
for (int i = 0; i < 3; i++) {
dynamicArray[i] = malloc(4 * sizeof(int)); // 为每行分配内存
for (int j = 0; j < 4; j++) {
dynamicArray[i][j] = i * 4 + j; // 初始化数组
}
}
// 使用动态分配的二维数组
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("Element at [%d][%d]: %d\n", i, j, dynamicArray[i][j]);
}
}
// 释放内存
for (int i = 0; i < 3; i++) {
free(dynamicArray[i]);
}
free(dynamicArray);
- 传递多维数组到函数:当你想要将多维数组作为参数传递给函数时,你可以传递一个指向数组的指针和数组的维度
void processArray(int *array, int dim1, int dim2) {
for (int i = 0; i < dim1; i++) {
for (int j = 0; j < dim2; j++) {
// 处理array[i][j]
printf("Element at [%d][%d]: %d\n", i, j, *((array + i * dim2) + j));
}
}
}
int main() {
int myArray[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
processArray(&myArray[0][0], 3, 4);
return 0;
}
- 使用多维数组作为结构体成员:多维数组可以作为结构体的成员,这使得你可以创建包含多维数据的复杂数据结构。
struct Matrix {
int rows;
int cols;
int data[]; // 声明一个灵活的数组成员
};
// 动态分配一个矩阵结构体
struct Matrix *matrix = malloc(sizeof(struct Matrix) + 3 * 4 * sizeof(int));
matrix->rows = 3;
matrix->cols = 4;
for (int i = 0; i < 3 * 4; i++) {
matrix->data[i] = i; // 初始化数组
}
// 使用矩阵
for (int i = 0; i < matrix->rows; i++) {
for (int j = 0; j < matrix->cols; j++) {
printf("Element at [%d][%d]: %d\n", i, j, matrix->data[i * matrix->cols + j]);
}
}
// 释放内存
free(matrix);
注意事项
- 内存分配:对于动态分配的多维数组,需要确保正确地释放所有分配的内存,以避免内存泄漏。
- 数组越界:在使用多维数组时,要确保所有维度的索引都在有效范围内。
- 性能考虑:多维数组的遍历和操作可能涉及复杂的循环结构,这可能影响程序的性能。
- 可读性:多维数组的使用可能会使代码的可读性降低,特别是在处理高维度数组时。
多维数组在科学计算、图形处理和游戏开发等领域中非常有用,但需要仔细设计和实现以确保代码的正确性和效率。
函数
在C语言中,函数是执行特定任务的代码块,具有以下特点:
- 模块化:函数将代码组织成模块,每个模块负责一个特定的功能。
- 重用性:函数可以在程序的多个地方重复调用,避免重复编写相同的代码。
- 参数化:函数可以接受参数,使得功能具有通用性。
- 返回值:函数可以返回一个值,将计算结果传递给调用者。
函数的基本结构
一个基本的函数包括:
- 返回类型:函数执行完毕后返回的值的类型。
- 函数名:唯一标识函数的名称。
- 参数列表:函数接收的输入值,可以是零个或多个。
- 函数体:包含实际执行代码的大括号
{}
。
声明和定义函数
在C语言中,你需要先声明函数,然后定义它。声明提供了函数的原型,即返回类型、函数名和参数列表。定义提供了函数的实现。
// 函数声明
int add(int a, int b);
// 函数定义
int add(int a, int b) {
return a + b;
}
调用函数
使用函数名和参数列表来调用函数:
int sum = add(5, 10);
参数传递
C语言通过值传递参数。这意味着当参数传递给函数时,实际上是将参数值的副本传递给了函数。
返回值
函数可以使用 return
语句返回一个值。当 return
被执行时,函数立即终止,并把控制权和返回值传递给调用者。
无返回值的函数
如果函数不需要返回任何值,可以使用 void
作为返回类型:
void printHello() {
printf("Hello, World!\n");
}
高级用法
-
指针参数:通过指针传递参数可以改变原始数据或传递大型数据结构。
void swap(int *x, int *y) { //将x和y的指针传入 int temp = *x; *x = *y; *y = temp; }
-
数组参数:数组作为参数时,实际上是通过指向数组第一个元素的指针传递。
void printArray(int arr[], int size) { for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } printf("\n"); }
-
函数指针:函数指针可以指向函数,允许动态调用函数。
void (*functionPtr)() = printHello; functionPtr(); // 调用printHello函数
-
递归:函数可以调用自身,这称为递归。
int factorial(int n) { if (n == 0) return 1; return n * factorial(n - 1); }
-
变长参数列表:使用
<stdarg.h>
可以定义接受可变数量参数的函数。#include <stdarg.h> // 包含可变参数列表处理所需的头文件 double average(int count, ...) { // 定义一个名为average的函数,它接收一个整数count和可变数量的参数 va_list args; // 定义一个va_list类型的变量,用于访问可变参数列表中的参数 double sum = 0.0; // 初始化一个double类型的变量sum,用于累加参数的总和,并将其初始值设为0.0 va_start(args, count); // 初始化args,设置其指向可变参数列表的第一个参数,count是最后一个固定参数的名称,告诉编译器 args 应该从哪个位置开始遍历参数。 for (int i = 0; i < count; i++) { // 循环count次,遍历所有参数 sum += va_arg(args, double); // 从args获取下一个参数,并将其强制转换为double类型,然后累加到sum中 } va_end(args); // 结束可变参数列表的使用,清理args return sum / count; // 计算平均值并返回结果 }
-
内联函数:使用
inline
关键字可以建议编译器在调用点展开函数,以减少函数调用的开销。inline int max(int a, int b) { return a > b ? a : b; }
7.在数组参数声明中使用static:tatic
关键字在数组参数声明中的使用并不常见,因为 static
主要用来修饰变量和函数,改变它们的存储类别和作用域。然而,static
可以用于数组参数的特定上下文中,尤其是在函数的局部变量中。
-
静态局部数组:在函数内部声明一个静态局部数组,这个数组的生命周期将贯穿整个程序的运行期,而不是仅限于函数调用的持续时间。
-
void function() { static int localArray[10]; // 静态局部数组 // ... 使用 localArray ... }
-
使用
static
修饰局部变量时,会改变局部变量的存储位置,从而使得局部变量的生命周期变长。这同样适用于局部数组。 -
防止数组内容被意外修改:由于静态变量只会被初始化一次,使用
static
可以防止数组在函数多次调用时被意外修改。 -
在多文件环境中控制数组作用域:在多文件的程序中,使用
static
可以限制数组的作用域,使其只在定义它的文件内可见。 -
模拟全局数组:在某些情况下,可能需要一个在函数间共享的数组,但又不希望它成为真正的全局变量。使用
static
可以在不违反封装性原则的情况下实现这一点。 -
数组参数的默认值:虽然
static
不能直接用于函数参数以提供默认值,但可以结合使用static
和函数返回值来模拟这种行为。
8.复合字面量(Compound Literals)是一种在运行时构造临时匿名对象的表达式,它允许你在声明的同时初始化一个数组或结构体。复合字面量通常用于需要立即创建一个初始化的对象,但又不想或不需要给它一个明确的标识符(名称)的场景。
复合字面量的语法如下:
(type){initializer}
这里 type
是对象的类型,initializer
是用花括号 {}
包围的初始化值。
复合字面量用于数组:
int arr[] = (int[]){1, 2, 3, 4, 5};
在这个例子中,我们创建了一个包含5个整数的数组,并使用复合字面量进行初始化。
复合字面量用于结构体:
struct Point {
int x;
int y;
};
struct Point p = (struct Point){10, 20};
注意事项
- 确保函数的声明与定义匹配。
- 注意参数的传递方式,特别是指针参数。
- 避免在函数中产生副作用,除非是故意的。
- 对于大型程序,合理组织函数,避免过长或过于复杂的函数体。
程序结构
C语言程序的结构通常包括以下几个关键部分:
- 预处理器指令:
- 使用
#include
指令来包含标准库或用户定义的头文件,提供程序所需的函数原型、宏定义和类型定义。
- 使用
- 定义常量和宏:
- 使用
#define
指令来定义常量和宏。
- 使用
- 函数原型声明:
- 在程序的开始部分声明将要使用的函数原型,以便编译器知道函数的返回类型、名称和参数列表。
- 包含标准库:
- 根据需要包含C标准库的头文件,例如
<stdio.h>
用于输入输出函数,<stdlib.h>
用于标准库函数等。
- 根据需要包含C标准库的头文件,例如
- 主函数:
int main(void)
或int main(int argc, char *argv[])
是程序的入口点,argc
和argv[]
可以处理命令行参数。
- 程序逻辑:
- 程序的主要逻辑,包括变量声明、函数调用、循环、条件判断等。
- 函数定义:
- 程序中使用的函数的具体实现。
- 局部变量(Local Variable):
- 定义在函数内部的变量。
- 只在该函数的作用域内可见。
- 当函数调用结束后,局部变量的生命周期也随之结束。
- 外部变量(External Variable):
- 定义在所有函数外部的全局变量。
- 可以在程序的任何部分被访问(除非被限制)。
- 生命周期贯穿整个程序。
- 程序块(Block):
- 由花括号
{}
包围的代码段,可以包含变量声明和语句。 - 程序块可以嵌套,内层程序块的作用域限定在其花括号内。
- 在程序块中声明的变量是局部变量。
- 由花括号
- 作用域(Scope):
- 指变量或常量可以被访问的代码区域。
- 作用域可以是全局的(整个程序)或局部的(在特定程序块内)。
- 局部变量的作用域限于声明它们的程序块。
- 输入输出:
- 使用
printf
、scanf
等函数进行输入输出操作。
- 使用
- 内存管理:
- 如果程序使用动态内存分配,需要使用
malloc
、calloc
、realloc
和free
等函数。
- 如果程序使用动态内存分配,需要使用
- 错误处理:
- 检查函数调用是否成功,并在必要时处理错误。
- 返回值:
main
函数返回一个整数,通常返回0表示成功,非0表示错误。
- 注释:
- 程序中应包含注释来解释关键代码段的功能。
示例程序结构:
#include <stdio.h> // 预处理器指令,包含标准输入输出库
#include <stdlib.h> // 包含标准库函数,如malloc和free
#define MAX_SIZE 100 // 定义常量
// 函数原型声明
void printWelcomeMessage(void);
int calculateSum(int numbers[], int size);
int main(int argc, char *argv[]) { // 主函数入口
if (argc > 1) {
printf("Number of arguments: %d\n", argc - 1);
}
printWelcomeMessage(); // 调用函数
int numbers[MAX_SIZE];
int size = 0;
// 假设这里填充numbers数组和size变量
int sum = calculateSum(numbers, size); // 调用函数计算总和
printf("The sum is: %d\n", sum);
return 0; // 正常退出
}
// 函数定义
void printWelcomeMessage(void) {
printf("Welcome to the program!\n");
}
int calculateSum(int numbers[], int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += numbers[i];
}
return sum;
}
这个简单示例展示了C语言程序的基本结构,包括预处理指令、函数原型声明、主函数、函数定义和输入输出。在实际编程中,程序可能会更复杂,包含多个文件和模块。