函数、指针
一、函数
1.1 函数概述
-
作用:提高代码的编写效率,实现对代码的重用
-
函数使用步骤
- 定义函数:理解为制作工具,工具只需要制作1次即可
- 调用函数:理解为使用工具
-
演示函数
c#include <stdio.h> // 定义函数 void func() { printf(" _ooOoo_ \n"); printf(" o8888888o \n"); printf(" 88 . 88 \n"); printf(" (| -_- |) \n"); printf(" O\\ = /O \n"); printf(" ____/`---'\\____ \n"); printf(" . ' \\| |// `. \n"); printf(" / \\||| : |||// \\ \n"); printf(" / _||||| -:- |||||- \\ \n"); printf(" | | \\\\\\ - /// | | \n"); printf(" | \\_| ''\\---/'' | | \n"); printf(" \\ .-\\__ `-` ___/-. / \n"); printf(" ___`. .' /--.--\\ `. . __ \n"); printf(" ."" '< `.___\\_<|>_/___.' >'"". \n"); printf(" | | : `- \\`.;`\\ _ /`;.`/ - ` : | | \n"); printf(" \\ \\ `-. \\_ __\\ /__ _/ .-` / / \n"); printf(" ======`-.____`-.___\\_____/___.-`____.-'====== \n"); printf(" `=---=' \n"); printf(" \n"); printf(" ............................................. \n"); printf(" 佛祖镇楼 BUG辟易 \n"); printf(" 佛曰: \n"); printf(" 写字楼里写字间,写字间里程序员; \n"); printf(" 程序人员写程序,又拿程序换酒钱。 \n"); printf(" 酒醒只在网上坐,酒醉还来网下眠; \n"); printf(" 酒醉酒醒日复日,网上网下年复年。 \n"); printf(" 但愿老死电脑间,不愿鞠躬老板前; \n"); printf(" 奔驰宝马贵者趣,公交自行程序员。 \n"); printf(" 别人笑我忒疯癫,我笑自己命太贱; \n"); printf(" 不见满街漂亮妹,哪个归得程序员?\n"); } int main() { // 函数调用 func(); func(); func(); return 0; }
1.2 无参无返回值
在 C 语言中,无参无返回值函数是指既不需要接收参数,也不返回任何结果的函数。
- void 关键字表示 "无类型",用于说明函数不需要返回值,且不接收参数
- 括号中的 void 明确表示该函数不接受任何参数(可省略,但建议写上以增强可读性)
c
#include <stdio.h>
/*
函数定义:
void 函数名() {
}
函数调用:
函数名();
需求:函数内部实现2个数相加
*/
// 函数定义
void my_add() {
int res = 1 + 2;
printf("res = %d\n", res);
}
int main() {
// 函数调用
my_add();
my_add();
return 0;
}
1.3 有参无返回值
-
函数参数的作用:增加函数的灵活性
-
可根据需求在调用函数时, 通过参数传入不同的数据
c
#include <stdio.h>
/*
函数定义: () 定义变量,这个变量叫形参,这个变量就是参数
void 函数名(形参1, 形参2, ......) {
}
函数调用:() 传递参数,这个参数,叫实参
函数名(实参1, 实参2, .........);
形参和实参关系:从左往右,实参和形参要一一对应传参
*/
// 函数定义
void my_add(int a, int b) {
int res = a + b;
printf("%d + %d = %d\n", a, b, res);
}
int main() {
// 函数调用
my_add(10, 20);
return 0;
}
1.4 有参有返回值
在 C 语言中,有参有返回值函数是指既需要接收参数(输入数据),又会返回一个结果(输出数据)的函数,这类函数能实现更灵活的逻辑处理,是实际开发中最常用的函数类型之一。
- 返回值类型:可以是任意基本数据类型(int、float、char等)或自定义类型
- 参数列表:用逗号分隔的多个参数,每个参数需指定类型和名称(形参)
- 返回值类型一致:return 语句后的表达式类型必须与函数声明的返回值类型一致,否则会导致编译错误或隐式类型转换。
- 多参数顺序:传入实参的顺序必须与形参声明顺序一致,否则会导致逻辑错误。
c
#include <stdio.h>
/*
函数定义: () 定义变量,这个变量叫形参,这个变量就是参数
类型 函数名(形参1, 形参2, ......) {
return 内容(要和函数定义的类型匹配);
}
函数调用:() 传递参数,这个参数,叫实参
匹配的类型 接收返回值的变量 = 函数名(实参1, 实参2, .........);
形参和实参关系:从左往右,实参和形参要一一对应传参
*/
// 函数定义
int my_add(int a, int b) {
int res = a + b;
return res;
}
int main() {
// 函数调用
int temp = my_add(10, 20);
printf("temp = %d\n", temp);
return 0;
}
1.4.1 返回值基本语法
- 函数返回值的作用:函数外部想使用函数内部的数据
c
#include <stdio.h>
// 需求:函数内,判断一个形参,奇偶数,> 0 才判断, <= 0 提示参数不符合
void f1(int num){
if (num <= 0){
printf("%d 不符合, 必须 >= 0\n", num);
}else{
if (num % 2 == 0){
printf("%d 是偶数\n", num);
}else{
printf("%d 是奇数\n", num);
}
}
}
void f2(int num) {
if (num <= 0) {
printf("%d 不符合, 必须 > 0\n", num);
return; // 提前结束函数
}
// 能执行到这,说明 num > 0
if (num % 2 == 0){
printf("%d 是偶数\n", num);
} else{
printf("%d 是奇数\n", num);
}
}
int main(){
f2(-10);
f2(5);
return 0;
}
1.4.2 返回值注意点
-
return只能用在函数里
-
return的作用是结束函数
-
函数内,return后面的代码不会执行
1.5 函数的声明
- 如果函数定义代码没有放在函数调用的前面,这时候需要先做函数的声明
- 所谓函数声明,相当于告诉编译器,函数是有定义的,在别的地方定义,以便使编译能正常进行
- 注意:一个函数只能被定义一次,但可以声明多次
c
#include <stdio.h>
/*
- 如果函数定义代码没有放在函数调用的前面,这时候需要先做函数的声明
- 所谓函数声明,相当于告诉编译器,函数是有定义的,在别的地方定义,以便使编译能正常进行
- 注意:一个函数只能被定义一次,但可以声明多次
*/
// 如果函数定义代码没有放在函数调用的前面,这时候需要先做函数的声明
// 所谓函数声明,相当于告诉编译器,函数是有定义的,在别的地方定义,以便使编译能正常进行
// 注意:一个函数只能被定义一次,但可以声明多次
int my_add(int a, int b);
int my_add(int, int); // 声明时,形参变量名,可以省略,定义时不可以
extern int my_add(int, int); // 可以加extern,加不加效果一样
int main() {
// 函数调用
int temp = my_add(10, 20); // 调用前,没有定义,需要声明
printf("temp = %d\n", temp);
return 0;
}
// 函数定义
int my_add(int a, int b) {
int res = a + b;
return res;
}
1.6 函数案例
- 自定义一个函数,返回2个整数的最大值
- 自定义一个函数,返回3个整数的最大值
c
#include <stdio.h>
// - 自定义一个函数,返回2个整数的最大值
int get_2num_max(int a, int b) {
return a > b ? a : b ;
}
// - 自定义一个函数,返回3个整数的最大值
int get_3num_max(int x, int y, int z) {
// 通过 get_2num_max 求 x, y 的最大值 max1
// 再通过 get_2num_max 求 max1 和 z 的最大值
int max1 = get_2num_max(x, y);
int max2 = get_2num_max(max1, z);
return max2;
}
int main() {
int temp;
// 2数最大值
temp = get_2num_max(33, 22);
printf("temp = %d\n", temp);
// 3数最大值
temp = get_3num_max(8, 7, 6);
printf("temp = %d\n", temp);
return 0;
}
1.7 局部和全局变量
1.7.1 局部变量
- 定义位置
- 函数体内部、代码块{}内部、循环控制语句中,如 for(int i=0; ...)
- 作用域(作用范围)
- 从其定义点 开始,到其所在的代码块 ({}) 结束为止。离开了这个范围,就无法访问该变量。
- 生命周期(存在时间)
- 当程序执行进入其作用域时,局部变量被创建(分配内存)。
- 当程序执行离开其作用域时,局部变量被销毁。
- 销毁意味着其占用的内存被系统回收,用户不再有权限访问这块内存。
- 没有初始化,值为随机数
c
#include <stdio.h>
/*
- **定义位置**
- 函数体内部、代码块{}内部、循环控制语句中,如 for(int i=0; ...)
- **作用域(作用范围)**
- 从其**定义点**开始,到其所在的**代码块 ({}) 结束**为止。离开了这个范围,就无法访问该变量。
- 生命周期(存在时间)
- 当程序执行**进入**其作用域时,局部变量被创建(分配内存)。
- 当程序执行**离开**其作用域时,局部变量被销毁。
- **销毁**意味着其占用的内存被系统回收,用户不再有权限访问这块内存。
- 没有初始化,值为随机数
*/
void func(int a, int b) {
int c;
}
int main() {
{
int d = 10;
}
// printf("d = %d\n", d); // err
for (int i = 0; i < 3; i++) {
}
// printf("i = %d\n", i); // err
int temp; // 没有初始化,值为随机数
printf("temp = %d\n", temp);
return 0;
}
1.7.2 全局变量
- 定义位置
- 在函数外部定义。通常位于源文件的顶部。
- 作用域(作用范围)
- 全局作用域 (Global Scope) 。从其定义点 开始,到整个文件结束为止。
- 生命周期
- 在程序启动时(通常在 main 函数执行之前)被创建和初始化。
- 在程序结束时才被销毁。
- 它的内存在程序的整个运行期间都持续存在。
- 没有初始化,值为0
c
#include <stdio.h>
/*
- **定义位置**
- 在函数**外部**定义。通常位于源文件的顶部。
- **作用域(作用范围)**
- **全局作用域 (Global Scope)**。从其**定义点**开始,到**整个文件结束**为止。
- **生命周期**
- 在程序**启动时**(通常在 main 函数执行之前)被创建和初始化。
- 在程序**结束时**才被销毁。
- 它的内存在程序的整个运行期间都**持续存在**。
- 没有初始化,值为0
*/
int g_num1; // 没有初始化,值为0
void func() {
extern int g_num2; // 全局变量的声明,extern不要省略
printf("g_num2 = %d\n", g_num2);
}
int g_num2 = 250; // 全局变量
int main() {
// 调用函数
func();
return 0;
}
1.8 多文件编程
在 C 语言开发中,多文件编程是将程序按功能拆分到多个源文件(.c)和头文件(.h)中的组织方式,能提高代码复用性、可维护性和团队协作效率。以下是其核心概念和实现方法: 18.1 文件分类与作用 多文件编程通常包含三类文件:
- 头文件(.h) 存放函数声明、宏定义、结构体 / 枚举类型定义、外部变量声明等。 作用:供其他文件引用,告知编译器 "这些内容的存在"。
- 源文件(.c) 存放函数实现、全局变量定义等具体逻辑。 每个 .c 文件可对应一个功能模块(如 uart.c 处理串口通信)。
- 主程序文件(如 main.c) 包含 main() 函数,作为程序入口,调用其他模块的功能。
18.2 核心规则 "声明与实现分离"
- 函数声明放在 .h 中,实现放在 .c 中。 例:math.h 声明 add() 函数,math.c 实现 add() 函数。
"头文件保护"
- 用 #ifndef 防止头文件被重复包含(避免编译错误)。
18.3 示例:实现一个简单的多文件程序 假设我们要实现一个 "计算器" 程序,拆分以下模块:
- 数学功能模块
math.h(头文件,声明函数)
c
#ifndef MATH_H // 头文件保护
#define MATH_H
// 声明加法函数
int add(int a, int b);
// 声明减法函数
int subtract(int a, int b);
#endif // MATH_H
math.c(源文件,实现函数)
c
#include "math.h" // 引用自身头文件(检查声明与实现是否一致)
// 实现加法
int add(int a, int b) {
return a + b;
}
// 实现减法
int subtract(int a, int b) {
return a - b;
}
- 主程序模块 main.c(程序入口)
c
#include <stdio.h>
#include "math.h" // 引用math模块的头文件,使用add()和subtract()
int main() {
int x = 10, y = 3;
// 调用math模块的函数
printf("%d + %d = %d\n", x, y, add(x, y)); // 输出:13
printf("%d - %d = %d\n", x, y, subtract(x, y)); // 输出:7
return 0;
}
18.4 编译与链接 多文件程序不能直接单个编译,需要编译所有 .c 文件并链接:
- 手动编译(以 GCC 为例)
c
# 编译所有.c文件为目标文件(.o)
gcc -c math.c -o math.o # 生成math.o
gcc -c main.c -o main.o # 生成main.o
# 链接目标文件为可执行程序
gcc math.o main.o -o calculator
# 运行程序
./calculator
- 用 IDE 自动编译 在 Keil、VS Code、Dev-C++ 等 IDE 中,只需将所有 .c 和 .h 文件添加到项目中,IDE 会自动处理编译和链接。
二、指针
- 学习一个新的类型,指针类型
2.1 基本语法
c
#include <stdio.h>
/*
- 指针也是一种数据类型,指针变量也是一种变量
- 指针变量指向谁,就把谁的地址赋值给指针变量
*/
int main() {
// 定义变量,变量类型为int
int a = 10;
// 定义变量,变量类型为指针类型,变量名为 p
// 是变量,就可以保存数据 类型匹配 int * 类型 保存 int的地址
int* p = &a; // 指针变量指向谁,就把谁的地址赋值给指针变量
printf("p = %p, &a = %p\n", p, &a);
// 可通过 * 间接操作 a 的内存
// p 和 *p 不是同一个内存空间
// *p 指指针所指向的内存,就是a
*p = 123;
printf("a = %d\n", a);
return 0;
}
2.1.1 指针变量的定义和使用
- 指针也是一种数据类型,指针变量也是一种变量
- 指针变量指向谁,就把谁的地址赋值给指针变量
c
#include <stdio.h>
/*
- 指针也是一种数据类型,指针变量也是一种变量
- 指针变量指向谁,就把谁的地址赋值给指针变量
*/
int main() {
// 定义变量,变量类型为int
// tom 外卖
int a = 10;
// a 指内存 &a 内存的地址 地址也叫指针
printf("a = %d, &a = %p\n", a, &a);
// 定义变量 p, 类型为 int* 匹配类型 int * 保存 int 地址
// 定义一个变量 保存 a 的地址 ,这个变量 地址变量 指针变量
// 指针也是一种数据类型,指针变量也是一种变量
int* p = &a; // 指针变量指向谁,就把谁的地址赋值给指针变量
printf("p = %p, &a = %p\n", p, &a);
// 指针的意义,间接操作内存
// p 和 *p 不是同一个内存
// *p 指 a, 指针所执行的内存
printf("*p = %d, a = %d\n", *p, a);
return 0;
}
2.1.2 通过指针间接修改变量的值
- 指针变量指向谁,就把谁的地址赋值给指针变量
- 通过 *指针变量 间接修改变量的值
c
#include <stdio.h>
int main() {
int a = 10;
// 变量p 保存 a 的地址,类型为 int *
int * p = &a; // 指针指向谁,就把谁的地址赋值给这个指针变量
// 通过 *p 间接修改a
*p = 123; // *p 指p所指向的内存,就是a
printf("a = %d\n", a);
int b = 5;
// &b 给p赋值
p = &b;
*p = 250; // *p 就是 b
printf("a = %d, b = %d\n", a, b);
return 0;
}
2.1.3 const修饰的指针变量
- 语法格式
C
int a = 10;
const int *p1 = &a; // 等价于 int const *p1 = &a;
int * const p2 = &a;
const int * const p3 = &a;
- a本身可以修改内容,不能通过指针间接修改a
- 从左往右看,跳过类型,看修饰哪个字符
- 如果是*, 说明指针指向的内存不能改变
- 如果是指针变量,说明指针的指向不能改变,指针的值不能修改
c
#include <stdio.h>
int main() {
int a = 10;
int b = 1;
const int *p1 = &a; // 等价于 int const *p1 = &a;
// *p1 = 123; // *p1 不能改
// p1 = &b; // ok
int * const p2 = &a;
// p2 = &b; // err
// *p2 = 123; // ok
const int * const p3 = &a;
// *p3 = 123; // err
// p3 = &b; // err
return 0;
}
2.1.4 指针大小
- 使用sizeof()测量指针的大小,得到的总是:4或8
- sizeof()测的是指针变量指向存储地址的大小
- 在32位平台,所有的指针(地址)都是32位(4字节)
- 在64位平台,所有的指针(地址)都是64位(8字节)
c
#include <stdio.h>
int main() {
char * a;
int * b;
double * c;
int ** d;
printf("%d, %d, %d, %d\n", sizeof(a), sizeof(b), sizeof(c), sizeof(d));
return 0;
}
2.1.5 指针步长
- 指针步长指的是通过指针进行递增或递减操作时,指针所指向的内存地址相对于当前地址的偏移量。
- 指针的步长取决于所指向的数据类型。
- 指针加n等于指针地址加上 n 个 sizeof(type) 的长度
- 指针减n等于指针地址减去 n 个 sizeof(type) 的长度
c
#include <stdio.h>
int main() {
char a;
char * pa = &a; // char * 指向 char
printf("pa = %p, pa + 1 = %p\n", pa, pa + 1); // +1
int b;
int * pb = &b; // int * 指向 int
printf("pb = %p, pb + 1 = %p\n", pb, pb + 1); // +4
double c;
double * pc = &c; // double * 指向 double
printf("pc = %p, pc + 1 = %p\n", pc, pc + 1); // +8
return 0;
}
2.1.6 野指针和空指针
概念 | 描述 | 危险性/安全性 |
---|---|---|
野指针 | 指向未知或非法内存的指针。 | 极度危险:解引用会导致程序崩溃。 |
空指针 | 指向地址0 (NULL)的指针,明确表示不指向任何对象。 | 安全:可通过判断是否为NULL来避免错误。 |
c
#include <stdio.h>
int main() {
int * p; // 指针变量
p = 0x11223344; // 内容,乱写的,内容不是系统分配的地址
// 操作p本身的内存没有问题
printf("p = %#x\n", p);
// 不能操作*p, 不能操作野指针所指向的内存
// *p = 123; // err
printf("111111111111111111111111\n");
// 赋值为NULL,说明指针变量p, 没有任何指向,可以用
int * p2 = NULL;
int a = 10;
if (p2 == NULL) {
p2 = &a;
}
return 0;
}
2.1.7 多级指针
- C语言允许有多级指针存在,在实际的程序中一级指针最常用,其次是二级指针。
c
#include <stdio.h>
int main() {
int a = 10;
int * p1 = &a; // int * 指向 int
int ** p2 = &p1; // int ** 指向 int *
int *** p3 = &p2; // int *** 指向 int **
printf("%d, %d, %d, %d\n", ***p3, **p2, *p1, a);
return 0;
}
2.2 指针和函数
2.2.1 函数参数传值
- 传值是指将参数的值拷贝一份传递给函数,函数内部对该参数的修改不会影响到原来的变量
c
#include <stdio.h>
// 通过函数交互2个实参的值
void swap(int a, int b) {
int temp;
temp = a;
a = b;
b = temp;
printf("函数内部:a = %d, b = %d\n", a, b);
}
int main() {
int a = 10;
int b = 20;
swap(a, b); // 变量本身传递,不是&传递,值传递
printf("函数外部:a = %d, b = %d\n", a, b);
return 0;
}
2.2.2 函数参数传址
- 传址是指将参数的地址传递给函数,函数内部可以通过该地址来访问原变量,并对其进行修改。
c
#include <stdio.h>
// 通过函数交互2个实参的值
void swap(int * p1, int * p2) {
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
printf("函数内部:*p1 = %d, *p2 = %d\n", *p1, *p2);
}
int main() {
int a = 10;
int b = 20;
swap(&a, &b); // 变量取地址传递,&传递,地址传递
printf("函数外部:a = %d, b = %d\n", a, b);
return 0;
}