代码环境:
Dev C++
?问题思考:
把上门的字母与下面相同的字母相连,线不能相交,不能碰到框。(文章末尾有答案)不要觉得不可能,请先思考思考。
一、数组
C语言中的数组是存储相同类型数据的集合,数组的元素可以通过下标访问。
- 数组的声明
数组声明时,必须指定数组的类型和大小。例如:
int arr[5]; // 声明一个包含5个整数的数组
char str[20]; // 声明一个包含20个字符的字符数组
- 数组的初始化
数组可以在声明时进行初始化。可以通过直接指定值来初始化数组:
int arr[5] = {1, 2, 3, 4, 5}; // 初始化数组
如果元素少于数组的大小,剩余的元素会被默认初始化为0:
int arr[5] = {1, 2}; // arr = {1, 2, 0, 0, 0}
- 数组的访问
数组元素通过下标访问,下标从0开始。例如:
int arr[5] = {1, 2, 3, 4, 5};
printf("%d", arr[2]); // 输出3
- 多维数组
C语言支持多维数组。二维数组(常用于矩阵)是最常见的多维数组:
int matrix[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
访问二维数组时,可以用两个下标:
printf("%d", matrix[1][2]); // 输出6
- 数组作为函数参数
数组可以作为函数参数传递。当传递数组时,实际上传递的是数组的指针,而不是数组的副本。例如:
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
}
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printArray(arr, 5); // 调用函数并传递数组
}
- 数组与指针的关系
在C语言中,数组名本质上是一个指向数组首元素的指针。因此,数组和指针有很多相似之处:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
printf("%d", *(ptr + 2)); // 输出3
- 动态数组
C语言没有内建的动态数组功能,但可以通过malloc
和free
函数来手动管理内存,创建和销毁动态数组。例如:
int *arr = (int *)malloc(5 * sizeof(int)); // 动态申请内存
arr[0] = 1;
arr[1] = 2;
// 其他操作
free(arr); // 释放内存
- 常见操作
- 数组长度:C语言没有内建的函数来获取数组的长度,但可以通过
sizeof
操作符计算数组的大小:
int arr[5];
int len = sizeof(arr) / sizeof(arr[0]); // 计算数组的长度
- 数组遍历:可以使用
for
循环遍历数组:
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
二、函数
C语言中的函数是执行特定任务的代码块,通过调用函数可以实现代码的复用。
- 函数的基本概念
函数是一段可以被反复调用的代码。函数的定义通常包含返回类型、函数名称、参数列表和函数体。
return_type function_name(parameter_list) {
// 函数体
// 执行任务
return return_value; // 如果有返回值
}
- 函数的声明
在使用函数之前,通常需要声明函数。函数声明告诉编译器函数的返回类型、名称以及参数类型。函数声明通常放在源文件的顶部或头文件中。
int add(int, int); // 函数声明
- 函数的定义
函数定义包括函数的返回类型、名称、参数及函数体。函数体包含执行任务的代码。
int add(int a, int b) {
return a + b; // 函数实现
}
- 函数的调用
在程序的其他部分,调用函数来执行定义的任务。调用时需要提供必要的参数(如果有)。
int result = add(3, 5); // 调用函数
printf("%d", result); // 输出 8
- 函数的返回类型
函数可以返回一个值,也可以不返回值。常见的返回类型有int
,float
,char
,void
等。
- 返回值类型:函数定义时,返回类型指定了函数返回数据的类型。
int multiply(int a, int b) {
return a * b; // 返回int类型
}
- void函数:如果函数没有返回值,使用
void
作为返回类型。
void printMessage() {
printf("Hello, World!");
}
- 参数传递
C语言中有两种方式传递参数:值传递和地址传递。
- 值传递:在函数调用时,实参的值被复制到形参中,函数内部对形参的修改不会影响实参。
void addFive(int num) {
num = num + 5;
}
int main() {
int n = 10;
addFive(n); // n不会改变,仍为10
}
- 地址传递(引用传递):使用指针将变量的地址传递给函数,函数可以通过指针修改实际参数。
void addFive(int *num) {
*num = *num + 5;
}
int main() {
int n = 10;
addFive(&n); // n变为15
}
- 递归函数
递归是指一个函数在其定义中调用自身。递归通常用于解决可以分解为子问题的问题,如阶乘、斐波那契数列等。
int factorial(int n) {
if (n == 0) return 1; // 递归基准条件
else return n * factorial(n - 1); // 递归调用
}
- 函数的作用域与生命周期
- 局部变量:在函数内部声明的变量,作用范围仅限于该函数。局部变量在函数调用时创建,返回后销毁。
- 全局变量:在函数外部声明的变量,整个程序范围内都可访问。全局变量在程序运行期间一直存在。
int globalVar = 100; // 全局变量
void foo() {
int localVar = 50; // 局部变量
printf("%d", localVar);
}
- 函数指针
函数指针是指向函数的指针。通过函数指针,可以在运行时动态决定调用哪个函数,常用于回调函数和事件驱动编程。
#include <stdio.h>
void greet() {
printf("Hello!");
}
int main() {
void (*func_ptr)() = greet; // 声明函数指针
func_ptr(); // 通过函数指针调用函数
return 0;
}
- 内存管理与函数
C语言中的函数并不会自动处理内存的分配和释放,程序员必须显式地管理内存。常用的内存管理函数有malloc
,free
,calloc
,realloc
等。
#include <stdlib.h>
int* createArray(int size) {
int *arr = (int*)malloc(size * sizeof(int)); // 动态分配内存
return arr;
}
int main() {
int *arr = createArray(10); // 分配10个整数的内存
free(arr); // 释放内存
return 0;
}
- 可变参数函数
C语言允许函数接收可变数量的参数,这类函数一般使用stdarg.h
库。常见的例子是printf
函数。
#include <stdarg.h>
#include <stdio.h>
void printNumbers(int num, ...) {
va_list args;
va_start(args, num);
for (int i = 0; i < num; i++) {
printf("%d ", va_arg(args, int)); // 获取下一个参数
}
va_end(args);
}
int main() {
printNumbers(3, 10, 20, 30); // 输出: 10 20 30
}
- 函数的调用约定
不同的编译器和平台可能使用不同的调用约定。常见的调用约定有cdecl
,stdcall
等,它们定义了如何传递参数、如何清理堆栈等。
三、指针
C语言中的指针是一个非常重要的概念,它允许程序直接操作内存。掌握指针的使用对于理解C语言的内存管理、函数调用、数组等方面非常有帮助。
- 指针的基本概念
指针是一个变量,它存储的是另一个变量的地址。指针允许我们通过地址来访问和操作内存中的数据。
- 声明指针:指针的声明需要在类型后加上一个星号(
*
)来表示该变量是一个指针。
int *ptr; // 声明一个指向int类型的指针
- 指针变量:指针变量存储的是另一个变量的地址,而不是值本身。
- 指针的初始化与赋值
指针必须被初始化才能使用,通常通过取地址符(&
)来获取变量的地址。
int num = 10;
int *ptr = # // 指针ptr指向num的地址
- 间接访问(解引用)
通过指针可以访问其指向的变量。这个过程称为解引用,通过星号(*
)操作符来进行。
int num = 10;
int *ptr = # // ptr存储num的地址
printf("%d", *ptr); // 输出num的值,即10
- 指针与变量
指针本身是一个变量,它保存了另一个变量的地址。通过指针可以修改指向的变量的值。
int num = 10;
int *ptr = #
*ptr = 20; // 通过指针修改num的值
printf("%d", num); // 输出20
- 指针与数组
数组名本质上是一个指向数组首元素的指针,因此可以通过指针操作数组。
int arr[3] = {1, 2, 3};
int *ptr = arr;
printf("%d", *(ptr + 1)); // 输出2,ptr指向arr[0],*(ptr + 1)指向arr[1]
- 指针运算
指针支持一些基本的运算,例如指针加减法。指针加法会根据指针所指向的数据类型自动调整步长。例如,int *ptr
加1,实际上会跳过一个int
的空间(通常是4字节)。
int arr[3] = {1, 2, 3};
int *ptr = arr;
ptr++; // 指针ptr移动到arr[1]
printf("%d", *ptr); // 输出2
- 空指针
空指针是指不指向任何有效内存位置的指针。它常用于初始化指针变量,以避免它指向不确定的内存地址。空指针常用的值是NULL
。
int *ptr = NULL; // 空指针
if (ptr == NULL) {
printf("指针为空\n");
}
- 指向指针的指针(多级指针)
指针本身可以指向另一个指针,这称为多级指针。常见的如二级指针、三级指针等。
int num = 10;
int *ptr = #
int **ptr2 = &ptr; // ptr2指向ptr,ptr指向num
printf("%d", **ptr2); // 输出10
- 指针与函数
指针在函数中的使用非常广泛,常用于传递大块数据(如数组),或者通过指针修改函数外部的变量值。
- 指针作为函数参数:传递指针到函数,允许在函数内修改外部变量。
void increment(int *ptr) {
(*ptr)++;
}
int main() {
int num = 10;
increment(&num); // 传递num的地址
printf("%d", num); // 输出11
}
- 返回指针的函数:函数可以返回指向局部变量的指针,但这样做是危险的,因为局部变量在函数返回后会被销毁。
int* foo() {
int num = 10;
return # // 返回局部变量的地址,不建议这么做
}
- 指针数组
指针数组是一个数组,其中每个元素都是指针。例如,可以创建一个指向多个字符串的数组。
char *arr[] = {"Hello", "World"};
printf("%s", arr[1]); // 输出 "World"
- 常量指针与指针常量
- 常量指针:指向的内容不能被修改,但指针本身可以指向其他地址。
int num = 10;
int *const ptr = # // 常量指针
*ptr = 20; // 允许修改ptr指向的内容
ptr = &num2; // 错误,无法修改指针的地址
- 指针常量:指针本身不能修改,但可以通过指针修改它所指向的内容。
int num = 10;
const int *ptr = # // 指针常量
*ptr = 20; // 错误,无法修改ptr指向的内容
ptr = &num2; // 允许修改指针的地址
- 指针与动态内存分配
C语言通过malloc
,calloc
,realloc
, 和free
来动态分配和释放内存。使用指针操作动态分配的内存。
#include <stdlib.h>
int *ptr = (int *)malloc(10 * sizeof(int)); // 动态分配内存
if (ptr != NULL) {
ptr[0] = 5;
printf("%d", ptr[0]);
free(ptr); // 释放内存
}
指针的内存布局
指针类型的大小通常是固定的,依赖于平台(例如,32位系统上通常是4字节,64位系统上通常是8字节)。指针本身只存储地址信息。指针的安全性
指针操作不当可能导致以下问题:
- 野指针:指向无效或未初始化的内存区域,可能导致程序崩溃或数据丢失。
- 内存泄漏:动态分配的内存未释放,导致内存浪费。
- 缓冲区溢出:对指针操作时,超出内存边界,可能会破坏数据或导致程序异常。
因此,在使用指针时,务必注意指针的初始化、使用和内存的正确管理。
四、结构体和共同体
C语言中的结构体(struct
)和共用体(union
)是用于存储不同类型数据的复合数据类型,它们具有各自的特点和用途。
- 结构体(
struct
)
结构体是一个用户自定义的数据类型,它允许将不同类型的数据组合在一起。每个数据成员称为结构体的字段或成员。
1.1 结构体的定义
结构体通过struct
关键字定义,并且可以包含不同类型的成员。
struct Person {
char name[50];
int age;
float height;
};
1.2 结构体的声明与初始化
结构体可以通过结构体名来声明变量,并且可以通过指定字段名来初始化。
struct Person person1; // 声明结构体变量
// 初始化结构体变量
struct Person person2 = {"John", 25, 175.5};
结构体变量可以直接通过点(.
)运算符访问其成员:
printf("Name: %s\n", person2.name);
printf("Age: %d\n", person2.age);
1.3 结构体指针
结构体变量也可以通过指针来访问。通过结构体指针访问成员时,使用箭头(->
)运算符。
struct Person *ptr = &person2;
printf("Name: %s\n", ptr->name); // 使用箭头运算符访问结构体成员
1.4 结构体作为函数参数
结构体可以作为函数的参数传递。通常有两种方式:
- 按值传递:传递结构体的副本。
- 按指针传递:传递结构体的地址,可以修改结构体的内容。
// 按值传递
void printPerson(struct Person p) {
printf("Name: %s\n", p.name);
printf("Age: %d\n", p.age);
}
// 按指针传递
void updateAge(struct Person *p) {
p->age = 30;
}
1.5 结构体的大小
结构体的大小取决于它的成员以及内存对齐要求。内存对齐是为了提高处理器访问数据的效率,通常结构体的每个成员按照最大类型对齐方式来分配内存。
printf("Size of Person: %zu bytes\n", sizeof(struct Person));
- 共用体(
union
)
共用体是一个特殊的结构体,所有成员共享同一块内存区域,因此共用体的大小是其最大成员的大小。共用体中的每个成员都可以存储值,但任何时刻只能有一个成员被使用。
2.1 共用体的定义
共用体通过union
关键字定义,它的成员共享同一块内存。
union Data {
int i;
float f;
char str[20];
};
2.2 共用体的声明与初始化
共用体声明后,它会分配足够大的内存空间来存储最大的成员。
union Data data1;
data1.i = 10;
printf("i: %d\n", data1.i); // 输出i: 10
在同一个时间,只能访问一个成员。如果你修改一个成员的值,其他成员的值可能会被覆盖。
union Data data2;
data2.f = 3.14;
printf("f: %.2f\n", data2.f); // 输出f: 3.14
data2.i = 100;
printf("i: %d\n", data2.i); // 输出i: 100,str被覆盖
2.3 共用体的大小
共用体的大小等于其最大成员的大小,因为所有成员共享同一块内存区域。
printf("Size of Data: %zu bytes\n", sizeof(union Data));
- 结构体与共用体的区别
- 内存分配:结构体的每个成员都有独立的内存空间,而共用体的所有成员共享同一块内存区域。
- 大小:结构体的大小是所有成员大小之和,而共用体的大小是其最大成员的大小。
- 使用场景:结构体用于需要存储多个不同类型的数据并且这些数据同时有效的场景,而共用体用于在同一时间只需要存储一个成员的值的场景,节省内存空间。
- 结构体与共用体的结合使用
可以在结构体中嵌套共用体,也可以在共用体中嵌套结构体。这种方式常用于实现具有多种类型数据的复合数据结构。
struct Mixed {
int x;
union {
int i;
float f;
} data;
};
struct Mixed example;
example.x = 5;
example.data.i = 10; // 或者example.data.f = 3.14;
- 位域(Bit-fields)
结构体中的位域是通过指定成员的位数来控制内存的精确分配。位域成员通常用于需要节省内存的场景。
struct Person {
unsigned int age : 7; // 7位来表示年龄
unsigned int gender : 1; // 1位来表示性别
};
位域成员的总和不能超过一个int
类型的大小。位域可以指定某个数据成员占用的位数,通常用于硬件寄存器映射等场景。
- 结构体指针和共用体指针
指向结构体和共用体的指针操作是相似的。可以使用指针访问结构体或共用体的成员,但指向结构体的指针和指向共用体的指针在访问时有所不同。
struct Person *ptr = &person1;
ptr->age = 30; // 修改结构体成员
union Data *ptr2 = &data1;
ptr2->i = 100; // 修改共用体成员
- 结构体与共用体的使用场景
- 结构体:结构体广泛用于需要同时存储多个不同类型数据的场景。例如,存储学生信息(姓名、年龄、成绩等)。
- 共用体:共用体适用于节省内存的场景,特别是当不同数据成员不会同时使用时。例如,用于处理不同类型的网络数据包,其中每个数据包类型的字段是不同的。
总结
- 结构体适合存储多个不同类型的变量,并且每个成员可以独立地存储值。
- 共用体用于节省内存,多个成员共享同一块内存空间,但同一时刻只能存储一个成员的值。
- 位域提供了精细的内存控制,通常用于低级编程和硬件编程。
五、文件
在C语言中,文件操作是非常重要的一部分,它使得程序能够与外部存储设备(如硬盘)进行数据交换。C语言提供了一些标准库函数,用于打开、读写、关闭文件等操作。
文件的基本概念
C语言中的文件操作通过文件指针来实现。文件指针是指向文件的指针,它包含了文件的信息(如文件的位置、状态等)。C语言标准库提供了一些函数来操作文件。文件操作的基本流程
在C语言中,文件操作的一般步骤如下:打开文件
进行文件读写操作
关闭文件
文件指针
文件指针是指向文件的指针,C语言中通过FILE
类型来定义文件指针。使用标准库函数打开文件时,会返回一个文件指针。
FILE *file;
- 打开文件
在C语言中,使用fopen
函数打开文件,fopen
函数返回一个指向文件的指针,打开文件的模式指定了文件操作的类型(如读、写、追加等)。
4.1 fopen
函数
FILE *fopen(const char *filename, const char *mode);
filename
:文件的名称(包括路径)。mode
:打开文件的模式,表示文件的访问权限。
常用的文件打开模式:
"r"
:以只读方式打开文件,文件必须存在。"w"
:以写入方式打开文件,如果文件存在则覆盖,不存在则创建。"a"
:以追加方式打开文件,如果文件不存在则创建。"rb"
:以二进制模式打开文件进行只读操作。"wb"
:以二进制模式打开文件进行写入操作。"r+"
:以读写方式打开文件,文件必须存在。"w+"
:以读写方式打开文件,如果文件存在则覆盖,不存在则创建。"a+"
:以读写方式打开文件,文件不存在则创建,文件指针移到文件末尾。
- 关闭文件
文件操作完成后,使用fclose
函数关闭文件。关闭文件时,文件指针不再有效。
int fclose(FILE *file);
- 文件读取
文件读取操作常用的函数包括fgetc
、fgets
、fread
等。
6.1 fgetc
函数
fgetc
用于读取一个字符,并返回该字符。如果文件结尾则返回EOF
(End Of File)。
int fgetc(FILE *file);
6.2 fgets
函数
fgets
用于从文件中读取一行数据,直到遇到换行符或文件结尾。fgets
会在读取的字符串末尾添加\0
。
char *fgets(char *str, int num, FILE *file);
str
:读取的字符串存储位置。num
:要读取的最大字符数。file
:文件指针。
6.3 fread
函数
fread
用于从文件中读取指定大小的数据块,通常用于二进制文件的读取。
size_t fread(void *ptr, size_t size, size_t count, FILE *file);
ptr
:指向存储读取数据的内存区域。size
:每个数据块的字节数。count
:要读取的数据块数量。
- 文件写入
文件写入操作常用的函数包括fputc
、fputs
、fprintf
、fwrite
等。
7.1 fputc
函数
fputc
用于将一个字符写入文件。
int fputc(int char, FILE *file);
7.2 fputs
函数
fputs
用于将字符串写入文件。
int fputs(const char *str, FILE *file);
7.3 fprintf
函数
fprintf
用于格式化输出到文件,类似于printf
,但输出目标是文件。
int fprintf(FILE *file, const char *format, ...);
7.4 fwrite
函数
fwrite
用于将内存中的数据块写入文件,通常用于二进制文件的写入。
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *file);
- 文件位置指针操作
文件指针用于记录文件中当前操作的位置。可以使用fseek
、ftell
和rewind
函数进行文件指针操作。
8.1 fseek
函数
fseek
用于设置文件指针的位置。
int fseek(FILE *file, long offset, int whence);
offset
:文件指针偏移量。whence
:指定偏移的基准位置(SEEK_SET
、SEEK_CUR
、SEEK_END
)。
8.2 ftell
函数
ftell
返回文件指针的当前位置。
long ftell(FILE *file);
8.3 rewind
函数
rewind
将文件指针移动到文件的开头。
void rewind(FILE *file);
- 文件错误处理
在文件操作过程中,可能会发生错误。C语言提供了以下函数来检测和处理文件错误。
9.1 feof
函数
feof
用于检测文件是否已到达结尾。
int feof(FILE *file);
- 返回值:非零值表示已到达文件末尾,零表示未到达文件末尾。
9.2 ferror
函数
ferror
用于检测文件是否发生错误。
int ferror(FILE *file);
- 返回值:非零值表示发生错误,零表示没有错误。
文件的读取与写入模式总结
| 操作模式 | 说明 |
|----------|-----------------------------|
|"r"
| 只读模式 |
|"w"
| 写模式,覆盖原文件或创建新文件 |
|"a"
| 追加模式 |
|"r+"
| 读写模式 |
|"w+"
| 读写模式,覆盖原文件或创建新文件 |
|"a+"
| 读写模式,追加内容 |
|"rb"
| 以二进制只读模式打开 |
|"wb"
| 以二进制写模式打开 |
|"ab"
| 以二进制追加模式打开 |
|"r+b"
| 以二进制读写模式打开 |常见的文件操作示例
示例1:读取文件内容并输出
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
printf("File not found!\n");
return 1;
}
char ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}
fclose(file);
return 0;
}
示例2:将字符串写入文件
#include <stdio.h>
int main() {
FILE *file = fopen("output.txt", "w");
if (file == NULL) {
printf("Error opening file!\n");
return 1;
}
fputs("Hello, world!\n", file);
fclose(file);
return 0;
}
C语言提供了强大的文件操作功能,能够帮助程序与外部存储设备进行数据交换。通过熟悉和掌握文件打开、读取、写入、关闭、指针操作和错误处理等基本操作,可以高效地进行文件操作。文件操作涉及的函数和模式非常丰富,可以满足各种需求。
问题答案:
你应该想到了吧?