C语言与Linux复习笔记
一、Linux基础操作
(一)虚拟机与Ubuntu系统
- 虚拟机安装:提供安装包链接及安装文档,可据此完成虚拟机安装,如VMware Workstation等软件的安装。
- Ubuntu系统介绍:Ubuntu是Linux操作系统的一个版本,Linux是基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统,在嵌入式开发方面有优势。
- 目录结构:根目录类似Windows下的C盘;家目录类似桌面。系统中有诸多重要目录,如/bin存放常用命令,/etc存放系统管理配置文件等。
(二)操作终端
- 打开与关闭:可通过多种方式打开终端,如收藏夹按钮、快捷键等;关闭终端也有多种方法,如点击叉号、输入指令、使用快捷键等。
- 终端操作:包括字体缩放、文本复制粘贴、历史指令查找、清屏等操作。终端默认内容包含用户名、主机名、当前目录等信息,通过这些可判断当前用户和所在目录。
(三)基础指令
- 指令格式:指令名 -[选项] [参数],选项和参数可省略,但指令名必须有。
- 常用指令
- ls:用于显示目录内容,选项丰富,可展示文件详细信息,如文件类型、权限、大小等。
- cd:切换目录,可使用绝对路径、相对路径或特殊目录标识进行切换。
- mkdir/rmdir:mkdir用于创建目录,rmdir用于删除空目录, -p选项可实现级联操作。
- touch/rm:touch可修改文件时间属性或创建新文件,rm用于删除文件或目录, -r选项可删除目录及内容。
- cp/mv:cp用于复制文件或目录,mv用于移动或重命名文件、目录。
(四)VIM编辑器
- 模式:分为命令行模式、插入模式、底行模式。命令行模式用于整体操作,插入模式用于文本编辑,底行模式用于保存、查找、退出等操作。
- 操作命令:各模式下有不同的操作命令,如命令行模式下的复制(yy)、粘贴(p);底行模式下的保存(:w)、退出(:q)等。
(五)网络配置
- 网络模式:桥接模式下虚拟机与Windows使用各自网络;NAT模式下与主机共享外网IP;仅主机模式只能实现虚拟机与主机通信。
- 配置方法:通过虚拟网络编辑器进行配置,若出现网络图标消失等问题,可通过删除缓存文件、修改配置文件等方法解决。
二、C语言基础
(一)基本概念
- 编程语言发展:从机器语言到汇编语言,再到高级程序设计语言(面向过程和面向对象)。C语言属于面向过程的语言,在硬件操作和嵌入式开发中应用广泛。
- C语言配置:源文件以.c结尾,头文件以.h结尾。可设置代码片段补全,首个C程序包含预处理指令、函数定义等,需理解各部分含义。
- 数据类型:C语言是强类型语言,数据类型分为基本数据类型、构造数据类型、指针类型、空类型。需掌握基本数据类型的字节数、取值范围和输入输出格式。
- 常量与变量:常量在程序执行中不变,变量可更改。变量定义需指定存储类型、数据类型和变量名,且要符合命名规则。
(二)运算符与表达式
- 运算符分类:包括赋值、算术、关系、逻辑、自增自减等多种运算符,需掌握其功能、优先级和结合方向。
- 数据类型转换:自动类型转换发生在不同类型数据混合运算时,强制类型转换需手动使用类型转换符号。
(三)流程控制
- 顺序结构:程序按顺序执行,变量需先声明后使用。
- 选择结构:if结构用于范围和等值判断,switch结构用于等值判断,两者可嵌套使用。
- 循环结构:有while、do…while、for循环,需掌握循环三要素,循环中可使用break和continue控制循环流程。
三、C语言进阶
函数定义与分类
- 从定义角度分类
- 库函数:是系统提供的已经封装好的函数。在使用时,需要包含相应的头文件。例如,常用的输入输出函数
printf
、scanf
等包含在<stdio.h>
头文件中;字符处理函数isalpha
、isupper
等包含在<ctype.h>
头文件中;字符串处理函数strlen
、strcpy
等包含在<string.h>
头文件中。这些库函数为程序员提供了便捷的功能,减少了重复开发的工作量 。 - 用户自定义函数:当系统提供的库函数无法满足特定需求时,程序员可以自己定义函数。例如,在一个学生成绩管理系统中,可能需要自定义函数来计算学生的平均成绩、判断成绩等级等。自定义函数使得代码更具针对性和灵活性,能够更好地满足实际应用的需求。
- 库函数:是系统提供的已经封装好的函数。在使用时,需要包含相应的头文件。例如,常用的输入输出函数
- 从参数和返回值角度分类
- 有参有返回值函数:函数执行需要外部传入数据,并且执行结束后会返回一个结果给主调函数。例如,定义一个函数
int add(int a, int b)
用于计算两个整数的和并返回结果,在主调函数中可以这样使用:
#include <stdio.h> int add(int a, int b) {return a + b; } int main() {int num1 = 3, num2 = 5;int result = add(num1, num2);printf("两数之和为:%d\n", result);return 0; }
- 有参无返回值函数:该函数需要外部传入数据来启动执行,但执行结束后不会返回数据给主调函数。比如,定义一个函数
void printInfo(int num)
用于打印某个整数相关的信息:
#include <stdio.h> void printInfo(int num) {if (num % 2 == 0) {printf("%d 是偶数\n", num);} else {printf("%d 是奇数\n", num);} } int main() {int number = 7;printInfo(number);return 0; }
- 无参有返回值函数:调用函数时不需要传入参数,函数内部会生成数据并返回给主调函数。例如,定义一个函数
int getRandomNumber()
用于生成一个随机数并返回:
#include <stdio.h> #include <stdlib.h> #include <time.h> int getRandomNumber() {srand(time(0));return rand() % 100; // 返回0到99之间的随机数 } int main() {int randomNum = getRandomNumber();printf("随机数为:%d\n", randomNum);return 0; }
- 无参无返回值函数:既不需要外部传入数据,执行结束后也不返回数据,仅执行封装在函数内的语句块。比如,定义一个函数
void printHello()
用于打印"Hello":
#include <stdio.h> void printHello() {printf("Hello\n"); } int main() {printHello();return 0; }
- 有参有返回值函数:函数执行需要外部传入数据,并且执行结束后会返回一个结果给主调函数。例如,定义一个函数
函数参数与返回值
- 参数传递 - 值传递:当函数的实参是普通变量或表达式的值时,采用值传递方式。在值传递过程中,形式参数变量会在函数内部开辟新的内存空间,与实际参数变量的空间相互独立。因此,形参的改变不会影响实参的值。例如:
#include <stdio.h>
void swap(int m, int n) {int temp = m;m = n;n = temp;printf("swap 函数中 m = %d, n = %d\n", m, n);
}
int main() {int num1 = 520;int num2 = 1314;swap(num1, num2);printf("main 函数中 num1 = %d, num2 = %d\n", num1, num2);return 0;
}
上述代码中,swap
函数内对m
和n
的值进行了交换,但main
函数中的num1
和num2
的值并未改变。
2. 参数传递 - 地址传递:当主调函数传递的是变量的地址时,形参使用能接收地址的容器(如指针、数组名)进行接收,这种方式就是地址传递。在地址传递中,形参和实参指向同一个内存空间,所以形参对地址中数据的更改会影响到实参。例如:
#include <stdio.h>
void swap(int *m, int *n) {int temp = *m;*m = *n;*n = temp;printf("swap 函数中 *m = %d, *n = %d\n", *m, *n);
}
int main() {int num1 = 520;int num2 = 1314;swap(&num1, &num2);printf("main 函数中 num1 = %d, num2 = %d\n", num1, num2);return 0;
}
在这个例子中,swap
函数通过指针操作,成功交换了main
函数中num1
和num2
的值。
3. 函数返回值与return语句:函数的返回值类型由主调函数的需求决定。如果主调函数需要函数执行后的结果进行后续操作,那么被调函数应定义为有返回值函数;如果主调函数只需要被调函数执行某些操作,不需要返回结果,则可定义为无返回值函数。return
语句用于将函数的执行结果返回给主调函数,同时也会结束当前函数的执行。例如:
#include <stdio.h>
int calculate(int a, int b) {int result = a * b;return result;
}
int main() {int num1 = 4, num2 = 5;int product = calculate(num1, num2);printf("两数之积为:%d\n", product);return 0;
}
在calculate
函数中,return result
语句将计算结果返回给main
函数,main
函数可以使用这个返回值进行输出等操作。
递归函数
- 递归的定义与概念:递归是指一个函数通过直接或间接的形式调用自身的一种特殊函数调用方式。例如:
#include <stdio.h>
void story() {printf("从前有座山,山里有个庙,庙里有个老和尚给小和尚讲故事,故事里说:");story(); // 直接递归调用自身
}
int main() {story();return 0;
}
上述代码中,story
函数在函数体内直接调用了自身,这是直接递归的例子。若函数fun
调用函数hunc
,而hunc
又调用fun
,则属于间接递归。
2. 递归的条件与要素
- 递归出口:递归函数必须有递归出口,当满足某个条件时,递归函数不再继续调用自身,而是返回结果。递归出口是防止递归无限进行下去的关键。例如,计算阶乘的递归函数:
#include <stdio.h>
int factorial(int n) {if (n == 0 || n == 1) { // 递归出口return 1;} else {return n * factorial(n - 1); // 递归表达式}
}
int main() {int num = 5;int result = factorial(num);printf("%d 的阶乘为:%d\n", num, result);return 0;
}
在factorial
函数中,当n
等于0或1时,函数直接返回1,不再继续递归调用。
- 递归表达式:递归表达式用于描述如何将问题逐步分解为更小规模的相同问题。通过递归表达式,函数不断调用自身,处理更小的子问题,直到达到递归出口。在上述阶乘的例子中,n * factorial(n - 1)
就是递归表达式,它将计算n
的阶乘问题转化为计算n - 1
的阶乘问题。
3. 递归的应用场景:递归常用于解决可以分解为相似子问题的情况,例如计算阶乘、斐波那契数列、汉诺塔问题等。以斐波那契数列为例:
#include <stdio.h>
int fibonacci(int n) {if (n == 1 || n == 2) { // 递归出口return 1;} else {return fibonacci(n - 1) + fibonacci(n - 2); // 递归表达式}
}
int main() {int num = 7;int result = fibonacci(num);printf("斐波那契数列第 %d 项的值为:%d\n", num, result);return 0;
}
在这个例子中,通过递归函数fibonacci
计算斐波那契数列中第n
项的值,递归表达式fibonacci(n - 1) + fibonacci(n - 2)
将计算第n
项的问题分解为计算第n - 1
项和第n - 2
项的问题 。
数组
一、数组的基本概念
- 数组的定义与特性:数组是连续存储的多个相同数据类型的变量的集合。这意味着数组在内存中占据一段连续的存储空间,并且所有元素的数据类型必须一致。例如,
int arr[5];
定义了一个包含5个整型元素的数组,这5个元素在内存中依次排列,每个元素都是int
类型。数组属于构造数据类型,它能让我们通过一条语句定义多个相同类型的变量,方便对大量同类型数据进行管理和操作。 - 数组的分类
- 按维度分类:一维数组通过一个下标来访问元素,如
int arr[5];
;二维数组使用两个下标,类似矩阵的形式,有行和列的概念,如int arr[3][4];
可看作由3个一维数组组成,每个一维数组有4个元素。还有多维数组,但在实际应用中,二维以上的数组使用相对较少。 - 按数据类型分类:根据存储的数据类型不同,数组可分为整型数组(如
int arr[10];
)、实型数组(如float arr[5];
)、字符数组(如char arr[20];
)、结构体数组(如struct Student arr[3];
,其中struct Student
是自定义的结构体类型)、指针数组(如int *arr[5];
,数组元素为指针)等。不同类型的数组用于存储不同类型的数据,以满足各种编程需求。
- 按维度分类:一维数组通过一个下标来访问元素,如
二、一维数组
- 定义及初始化
- 定义格式:
数据类型 数组名[常量表达式];
,其中数据类型
指定数组元素的类型,数组名
是标识符,需符合命名规则,常量表达式
确定数组中元素的个数,即数组的长度。例如,int arr[5];
定义了一个长度为5的整型数组,数组元素为arr[0]
、arr[1]
、arr[2]
、arr[3]
、arr[4]
,注意数组元素的下标从0开始,所以最大下标为数组长度减1 。 - 初始化方式
- 全部初始化:在定义数组时,使用花括号包裹多个数据给数组初始化,且初始化值的个数与数组元素个数相同。例如,
int arr[5] = {23, 5, 1, 2, 4};
,此时arr[0] = 23
,arr[1] = 5
,以此类推。 - 部分初始化:初始化元素个数小于数组元素个数时,先将初始化的值赋给前面的变量,未初始化的部分自动用0补齐。如
int arr[5] = {23, 5, 1};
,则arr[0] = 23
,arr[1] = 5
,arr[2] = 1
,arr[3] = 0
,arr[4] = 0
。 - 特殊初始化:定义数组并初始化时,不给定数组长度,由初始化元素的个数确定数组长度。例如,
int arr[] = {23, 5, 1};
,此时数组arr
的长度为3。
- 全部初始化:在定义数组时,使用花括号包裹多个数据给数组初始化,且初始化值的个数与数组元素个数相同。例如,
- 定义格式:
- 相关操作
- 输入输出操作:不能直接使用数组名操作整个数组进行输入输出,需要通过循环遍历数组元素来实现。例如,输入8名学生的成绩并输出:
#include <stdio.h>
int main() {int score[8];for (int i = 0; i < 8; i++) {printf("请输入第%d个学生的成绩:", i + 1);scanf("%d", &score[i]);}printf("学生成绩分别是:");for (int i = 0; i < 8; i++) {printf("%d\t", score[i]);}printf("\n");return 0;
}
- **求和、均值与求最值**- **求和**:通过循环累加数组元素实现。例如,求`score`数组的总成绩:
int sum = 0;
for (int i = 0; i < 8; i++) {sum += score[i];
}
- **均值**:在求和的基础上,除以元素个数得到平均值。如`double avg = (double)sum / 8;`,需注意类型转换,将`sum`转换为`double`类型,以确保结果为小数。- **求最值**:常用擂台法,先将数组的某个元素(如第一个元素)设为最值,然后遍历数组,与其他元素比较并更新最值。例如,求最高分:
int max = score[0];
for (int i = 0; i < 8; i++) {if (max < score[i]) {max = score[i];}
}
- **逆置**:将数组首尾元素进行交换,实现数组逆置。例如,对`score`数组逆置:
for (int i = 0; i < 8 / 2; i++) {int temp = score[i];score[i] = score[8 - 1 - i];score[8 - 1 - i] = temp;
}
- **查找**:查找方式有存在性查找(找到一个符合条件的元素就停止查找)和统计性查找(统计符合条件的元素个数)。例如,统计性查找某个成绩在数组中出现的次数:
int value = 0;
printf("请输入您要查找的成绩:");
scanf("%d", &value);
int key = 0;
for (int i = 0; i < 8; i++) {if (value == score[i]) {key++;}
}
if (key == 0) {printf("查找失败\n");
} else {printf("您要查找的数据出现%d次\n", key);
}
- **排序**- **冒泡排序**:依次比较相邻元素,若顺序不对则交换,每一趟比较会使一个最大(或最小)的元素“冒泡”到数组末尾。例如,对数组`arr`进行升序冒泡排序:
int arr[] = {3, 5, 1, 6, 8, 7, 9};
int len = sizeof(arr) / sizeof(arr[0]);
for (int i = 1; i < len; i++) {for (int j = 0; j < len - i; j++) {if (arr[j] > arr[j + 1]) {int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}
}
- **选择排序**:每次从待排序序列中选择一个最大(或最小)值,放在已排序序列的最后。例如,对数组`arr`进行升序选择排序:
int arr[] = {3, 5, 1, 6, 8, 7, 9};
int len = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < len; i++) {int mini = i;for (int j = i; j < len; j++) {if (arr[j] < arr[mini]) {mini = j;}}if (mini != i) {int temp = arr[i];arr[i] = arr[mini];arr[mini] = temp;}
}
三、字符数组
- 定义及初始化
- 定义格式:
char 数组名[数组长度];
,用于存储字符数据。 - 初始化方式
- 以字符方式初始化:全部初始化如
char arr[5] = {'a', 'b', 'c', 'd', 'e'};
;部分初始化时,未初始化部分用'\0'
补齐,如char arr[5] = {'a', 'b', 'c'};
;特殊初始化如char arr[] = {'a', 'b', 'c', 'd', 'e'};
。 - 以字符串方式初始化:全部初始化
char arr[6] = {"hello"};
(等价于char arr[6] = "hello";
),数组长度需比字符串实际长度多1,以存储字符串结束标识'\0'
;部分初始化char arr[8] = {"hello"};
;特殊初始化char arr[] = {"hello"};
,此时数组实际长度为6(包含'\0'
),字符串实际长度为5。
- 以字符方式初始化:全部初始化如
- 定义格式:
- 字符及字符串的输入输出函数
printf
和scanf
:printf
和scanf
可用于格式化输出和输入字符及字符串。输入字符时用%c
,输入字符串时用%s
。但scanf
输入字符串时,遇到空格会结束一次输入,且可以连续输入多个字符串;printf
可以连续输出多个字符串,但不会自动换行。gets
和puts
:gets
用于从终端获取一个字符串,放入给定的字符数组容器中,可输入带空格的字符串,且每次只能输入一个字符串;puts
用于向终端输出一个字符串,并自带换行(自动将字符串的'\0'
换成'\n'
输出)。getchar
和putchar
:getchar
用于阻塞等待从终端获取一个字符数据,以返回值形式返回;putchar
用于向终端输出一个单字符。
- 字符及字符串处理函数
- 字符处理函数:包含在
<ctype.h>
头文件中,如isalnum
判断字符是否为字母或数字,isupper
判断是否为大写字母,tolower
将大写字母转换为小写字母等。 - 字符串处理函数:包含在
<string.h>
头文件中。例如,strcpy
用于字符串拷贝,strcmp
用于比较两个字符串大小,strcat
用于连接两个字符串,strlen
用于求字符串实际长度(不包含'\0'
) 。对字符串操作时,不能直接使用运算符,因为字符数组名是地址,需使用这些专门的字符串处理函数。
- 字符处理函数:包含在
四、二维数组
- 二维数组的引入与定义:当程序中需要管理多个一维数组时,使用二维数组更为方便。二维数组由两个下标表示,有行和列的概念。定义格式为
数据类型 数组名[行数][列数];
,例如int arr[3][4];
,从一维数组角度看,定义了3个一维数组,每个一维数组名为arr[0]
、arr[1]
、arr[2]
;从变量角度看,定义了12个变量,即arr[0][0]
到arr[2][3]
。 - 二维整型数组的初始化
- 按行全部初始化:将每一行的值放在一个花括号中,如
int arr[3][4] = {{1, 1, 1, 1}, {2, 2, 2, 2}, {3, 3, 3, 3}};
。 - 按行部分初始化:当前行未初始化部分用0补齐,如
int arr[3][4] = {{1, 1}, {}, {3}};
。 - 按分布方式全部初始化:每行元素无需花括号包裹,按顺序填充,如
int arr[3][4] = {1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3};
。 - 按分布方式部分初始化:数据先填满前面行,后面未初始化部分用0补齐,如
int arr[3][4] = {1, 1, 1, 1, 2, 2};
。 - 特殊初始化:定义二维数组并初始化时,第一维可省略,由初始化元素个数和每一行的列数确定行数(行数 = (元素个数 / 列数 向上取整)),但第二维不能省略,如
int arr[][4] = {1, 1, 1, 1, 2, 2, 2, 2, 3};
。
- 按行全部初始化:将每一行的值放在一个花括号中,如
- 二维数组的输入输出与操作
- 输入输出:通过两层循环遍历二维数组的每一个元素进行输入输出。例如,输入一个班级3个小组,每组4人的成绩并输出:
#include <stdio.h>
int main() {int score[3][4];for (int i = 0; i < 3; i++) {for (int j = 0; j < 4; j++) {printf("请输入第%d小组的第%d个学生的成绩:", i + 1, j + 1);scanf("%d", &score[i][j]);}printf("\n");}printf("该班级的学生分数如下:\n");for (int i = 0; i < 3; i++) {for (int j = 0; j < 4; j++) {printf("%d\t", score[i][j]);}printf("\n");}return 0;
}
- **相关操作**:包括整体求和、求最值,求每一行的和、最值,以及转置等操作。例如,整体求和:
int sum = 0;
for (int i = 0; i < 3; i++) {for (int j = 0; j < 4; j++) {sum += score[i][j];}
}
求每一行的和:
for (int i = 0; i < 3; i++) {int sumh = 0;for (int j = 0; j < 4; j++) {sumh += score[i][j];}printf("第%d个小组的总成绩为:%d\n", i + 1, sumh);
}
转置(将原数组的行和列互换):
int brr[4][3];
for (int i = 0; i < 4; i++) {for (int j = 0; j < 3; j++) {brr[i][j] = score[j][i];}
}
- 二维字符数组:用于存储多个字符串,定义格式为
char 数组名[行数][列数];
。例如,char str[3][8];
可看作定义了3个字符串变量str[0]
、str[1]
、str[2]
,也可看作定义了24个字符变量。初始化方式与二维整型数组类似,可使用字符填充或字符串填充。在输入输出及操作时,既可以按字符方式处理,也可以按字符串方式处理,对多个字符串排序时,需使用strcmp
函数进行比较,strcpy
函数进行交换。
指针
1. 指针变量
指针的基本概念
在计算机内存中,每个存储单元都有一个唯一的编号,这个编号就是内存地址,也被称为指针。指针变量则是一种特殊的变量,它专门用于存储这些内存地址。
指针变量的定义与初始化
- 定义格式:
数据类型 *指针变量名;
。这里的数据类型表示该指针所指向的变量的数据类型。例如:
int *p; // 定义一个指向整型变量的指针变量p
char *ch; // 定义一个指向字符型变量的指针变量ch
- 初始化:可以使用取地址运算符
&
来获取变量的地址,并将其赋值给指针变量。示例如下:
#include <stdio.h>
int main() {int num = 10;int *p = # // 初始化指针p,使其指向变量numprintf("变量num的地址:%p\n", &num);printf("指针p存储的地址:%p\n", p);return 0;
}
在上述代码中,&num
获取了变量num
的地址,然后将该地址赋值给指针变量p
。
指针的解引用
通过指针访问其所指向的变量的值,称为指针的解引用,使用*
运算符。例如:
#include <stdio.h>
int main() {int num = 10;int *p = #printf("变量num的值:%d\n", num);printf("通过指针p访问num的值:%d\n", *p); // *p表示解引用,获取p所指向的变量的值*p = 20; // 通过指针修改所指向变量的值printf("修改后变量num的值:%d\n", num);return 0;
}
2. 指针与数组、函数
指针与数组
- 指针指向数组:数组名代表数组首元素的地址。因此,可以将数组名赋值给指针,使指针指向数组的首元素。例如:
#include <stdio.h>
int main() {int arr[5] = {1, 2, 3, 4, 5};int *p = arr; // 指针p指向数组arr的首元素printf("数组首元素的值:%d\n", *p);return 0;
}
- 通过指针访问数组元素:指针可以进行算术运算,通过指针的移动来访问数组的不同元素。例如:
#include <stdio.h>
int main() {int arr[5] = {1, 2, 3, 4, 5};int *p = arr;for (int i = 0; i < 5; i++) {printf("arr[%d] = %d\n", i, *(p + i)); // p + i 指向数组的第i个元素}return 0;
}
- 数组名作为参数传递:当数组名作为函数参数传递时,实际上传递的是数组首元素的地址,属于地址传递。这意味着在函数内部对数组元素的修改会影响到原数组。示例如下:
#include <stdio.h>
void modifyArray(int *arr, int size) {for (int i = 0; i < size; i++) {arr[i] *= 2; // 修改数组元素的值}
}
int main() {int arr[5] = {1, 2, 3, 4, 5};modifyArray(arr, 5);for (int i = 0; i < 5; i++) {printf("arr[%d] = %d\n", i, arr[i]);}return 0;
}
指针与函数
- 指针作为函数参数:指针可以作为函数参数,用于接收普通变量的地址或数组。通过传递指针,可以在函数内部修改主调函数中变量的值。例如,交换两个整数的值:
#include <stdio.h>
void swap(int *a, int *b) {int temp = *a;*a = *b;*b = temp;
}
int main() {int num1 = 10, num2 = 20;printf("交换前:num1 = %d, num2 = %d\n", num1, num2);swap(&num1, &num2);printf("交换后:num1 = %d, num2 = %d\n", num1, num2);return 0;
}
3. 指针进阶
指针函数
- 定义:指针函数是返回值为指针的函数。其定义格式为:
数据类型 *函数名(参数列表);
。例如:
#include <stdio.h>
int *findMax(int *arr, int size) {int *max = arr;for (int i = 1; i < size; i++) {if (*(arr + i) > *max) {max = arr + i;}}return max;
}
int main() {int arr[5] = {1, 3, 5, 2, 4};int *p = findMax(arr, 5);printf("数组中的最大值:%d\n", *p);return 0;
}
在上述代码中,findMax
函数返回一个指向数组中最大值的指针。
函数指针
- 定义:函数指针是指向函数的指针变量。其定义格式为:
数据类型 (*指针变量名)(参数列表);
。例如:
#include <stdio.h>
int add(int a, int b) {return a + b;
}
int main() {int (*p)(int, int); // 定义一个函数指针pp = add; // 使指针p指向函数addint result = p(3, 5); // 通过函数指针调用函数printf("3 + 5 = %d\n", result);return 0;
}
函数指针常用于实现回调函数、函数表等。
数组指针
- 定义:数组指针是指向数组的指针。其定义格式为:
数据类型 (*指针变量名)[数组长度];
。例如:
#include <stdio.h>
int main() {int arr[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};int (*p)[4] = arr; // 定义一个数组指针p,指向一个包含4个元素的一维数组for (int i = 0; i < 3; i++) {for (int j = 0; j < 4; j++) {printf("%d ", *(*(p + i) + j));}printf("\n");}return 0;
}
指针数组
- 定义:指针数组是数组元素为指针的数组。其定义格式为:
数据类型 *数组名[数组长度];
。例如:
#include <stdio.h>
int main() {int a = 1, b = 2, c = 3;int *arr[3] = {&a, &b, &c}; // 定义一个指针数组arr,包含3个指向整型变量的指针for (int i = 0; i < 3; i++) {printf("%d ", *arr[i]);}printf("\n");return 0;
}
二级指针
- 定义:二级指针是指向指针的指针。其定义格式为:
数据类型 **指针变量名;
。例如:
#include <stdio.h>
int main() {int num = 10;int *p = #int **pp = &p; // 定义一个二级指针pp,指向指针pprintf("通过二级指针访问num的值:%d\n", **pp);return 0;
}
万能指针
- 定义:万能指针也称为空指针,其类型为
void *
。万能指针可以指向任意类型的数据,但在使用时需要进行显式的类型转换。例如:
#include <stdio.h>
int main() {int num = 10;void *p = # // 定义一个万能指针p,指向整型变量numint *ip = (int *)p; // 进行类型转换printf("通过万能指针访问num的值:%d\n", *ip);return 0;
}
综上所述,指针是C语言中非常重要且强大的特性,通过灵活运用指针,可以实现高效的数据处理和程序设计。但同时,指针的使用也需要谨慎,避免出现指针越界、野指针等问题。
(四)杂项
1. 预处理指令
文件包含
- 作用:将指定的头文件内容插入到当前源文件中。常见的有两种形式:
#include <文件名>
:用于包含系统提供的标准头文件,编译器会在系统指定的标准目录中查找该文件。例如,#include <stdio.h>
会包含标准输入输出库的头文件。#include "文件名"
:用于包含用户自定义的头文件,编译器先在当前源文件所在目录查找,若未找到则再到系统指定目录查找。例如,#include "myheader.h"
。
- 好处:避免代码重复编写,提高代码复用性,方便模块化开发。
宏定义
- 无参宏:
- 定义格式:
#define 宏名 替换文本
。例如,#define PI 3.14159
,在编译预处理时,源文件中所有的PI
都会被替换为3.14159
。 - 用途:定义常量,提高代码可读性和可维护性,方便修改。
- 定义格式:
- 带参宏:
- 定义格式:
#define 宏名(参数列表) 替换文本
。例如,#define ADD(a, b) ((a) + (b))
,使用ADD(3, 5)
时会被替换为((3) + (5))
。 - 用途:实现简单的函数功能,但与函数调用不同,宏展开是在编译预处理阶段,没有函数调用的开销。
- 定义格式:
条件编译
#ifdef
、#ifndef
、#endif
:#ifdef 宏名
:如果该宏已被定义,则编译#ifdef
到#endif
之间的代码。#ifndef 宏名
:如果该宏未被定义,则编译#ifndef
到#endif
之间的代码。常用于防止头文件重复包含,例如:
#ifndef MYHEADER_H
#define MYHEADER_H
// 头文件内容
#endif
#if
、#elif
、#else
、#endif
:根据条件表达式的值来决定编译哪些代码块,类似于if - else if - else
语句。例如:
#define VERSION 2
#if VERSION == 1// 版本1的代码
#elif VERSION == 2// 版本2的代码
#else// 其他版本的代码
#endif
2. 结构体与共用体
结构体
- 定义:
struct 结构体名 {数据类型 成员1;数据类型 成员2;// 可以有更多成员
};
例如:
struct Student {char name[20];int age;float score;
};
- 初始化:可以在定义结构体变量时进行初始化,例如:
struct Student stu = {"Tom", 20, 85.5};
- 使用:通过
.
运算符访问结构体成员,例如:
printf("Name: %s, Age: %d, Score: %.2f\n", stu.name, stu.age, stu.score);
共用体
- 定义:
union 共用体名 {数据类型 成员1;数据类型 成员2;// 可以有更多成员
};
例如:
union Data {int i;float f;char str[20];
};
- 特点:共用体的所有成员共享同一块内存空间,同一时间只能使用一个成员。例如:
union Data data;
data.i = 10;
printf("data.i: %d\n", data.i);
data.f = 3.14;
printf("data.f: %.2f\n", data.f);
在上述代码中,当给 data.f
赋值后,data.i
的值就不再有效。
3. 特殊关键字
const
- 作用:用于修饰变量,使其成为常量,一旦初始化后其值不能被修改。例如:
const int num = 10;
// num = 20; // 错误,不能修改常量的值
- 应用场景:保护数据不被意外修改,提高程序的安全性。
static
- 修饰局部变量:改变局部变量的生命周期,使其在程序运行期间只初始化一次,函数调用结束后不会销毁,下次调用函数时其值保留上次调用结束时的值。例如:
void func() {static int count = 0;count++;printf("Count: %d\n", count);
}
- 修饰全局变量和函数:改变全局变量和函数的作用域,使其只能在定义它们的源文件中使用,避免不同源文件之间的命名冲突。
extern
- 作用:用于声明在其他源文件中定义的全局变量或函数,告诉编译器该变量或函数在其他地方已经定义,从而可以在当前源文件中使用。例如,在
file1.c
中定义了全局变量int num = 10;
,在file2.c
中可以使用extern int num;
来声明并使用该变量。
typedef
- 作用:用于为已有的数据类型定义一个新的别名,提高代码的可读性和可维护性。例如:
typedef int Integer;
Integer a = 5; // 等价于 int a = 5;
还可以用于简化复杂的类型声明,如结构体类型:
typedef struct {char name[20];int age;
} Person;
Person p = {"Alice", 25};
enum
- 作用:用于定义枚举类型,将一组相关的常量值组合在一起。例如:
enum Weekday {MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY
};
枚举常量默认从 0 开始依次递增,也可以手动指定初始值。例如:
enum Color {RED = 1,GREEN = 2,BLUE = 3
};
可以使用枚举类型的变量来表示这些常量值,例如:
enum Weekday today = MONDAY;