函数——C++的编程模块
1. 复习函数的基本知识
1.1 什么是函数?
函数是一段有名字的、可重复使用的代码块,它完成特定的任务。
为什么要学函数?
想象你写了一个程序,需要在10个不同的地方计算两个数的和。如果不使用函数,你就得把加法代码复制粘贴10次。一旦发现计算逻辑有误,你得修改10个地方。函数就是为了解决这个问题——把代码"打包"起来,需要的时候"叫名字"就能用。
通俗案例:
把函数想象成微波炉。你不需要知道微波炉内部怎么加热,你只需要:
- 放东西进去(输入)
- 按按钮(调用函数)
- 拿出热好的食物(得到结果)
1.2 定义函数
一个完整的函数定义包含以下部分:
返回值类型 函数名(参数列表)
{
函数体; // 具体执行的代码
return 返回值; // 将结果送回去
}
具体案例:
// 定义一个函数:计算两个整数的和
int add(int a, int b)
{
int sum = a + b; // 函数体:具体做加法
return sum; // 把结果返回给调用者
}
int main()
{
int result = add(3, 5); // 调用函数,result 得到 8
return 0;
}
拆解说明:
| 部分 | 内容 | 含义 |
|---|---|---|
int | 返回值类型 | 这个函数最终会"交还"一个整数 |
add | 函数名 | 给这段代码起的名字 |
int a, int b | 参数列表 | 调用时需要传入的两个整数 |
return sum; | 返回语句 | 把计算结果送回去 |
没有返回值的情况:
如果函数只是执行某个动作,不需要返回结果,可以用 void:
void greet() // void 表示"不返回任何值"
{
cout << "你好!" << endl;
// 不需要 return 语句
}
1.3 函数原型和函数调用
函数原型(声明)
函数原型就是告诉编译器:“有这么一个函数存在,它长这样”,但不写出具体实现。
// 函数原型(放在 main 函数前面)
int add(int a, int b); // 以分号结尾,没有函数体
int main()
{
int result = add(3, 5); // 编译器已经知道 add 存在,放心调用
return 0;
}
// 函数定义(放在 main 函数后面)
int add(int a, int b)
{
return a + b;
}
为什么要写函数原型?
C++ 编译器是从上往下读代码的。如果 main() 在前面调用了 add(),但 add() 的定义在 main() 后面,编译器读到 main() 时还不认识 add(),就会报错。函数原型就是提前告诉编译器:“别担心,后面会有这个函数的定义。”
小技巧: 函数原型的参数名可以省略,只保留类型:
int add(int, int); // 完全合法,编译器只关心参数类型
函数调用
int result = add(3, 5); // 这就是函数调用
add是函数名3和5是实参(实际传入的值)- 函数定义中的
a和b是形参(形式上的参数名)
实参与形参的关系:
调用 add(3, 5) 时,相当于执行了 int a = 3; int b = 5;,把实参的值复制给了形参。
1.4 函数参数和按值传递
"按值传递"的意思是:函数收到的是实参的一份拷贝,不是实参本身。
void triple(int x)
{
x = x * 3; // 修改的是拷贝,不影响原来的值
}
int main()
{
int num = 5;
triple(num);
cout << num << endl; // 输出 5,不是 15!
return 0;
}
通俗解释:
就像你给朋友发了一张照片的复印件。朋友在复印件上乱涂乱画,不影响你手里的原件。函数接收参数也是这样——它拿到的是副本。
这就是为什么有些时候需要"按引用传递"(后面会详细讲),因为按值传递无法修改原始数据。
2. 多个参数
函数可以接受任意数量的参数,只要在参数列表中依次列出即可。
// 三个参数的函数:计算三个数的平均值
double average(double a, double b, double c)
{
return (a + b + c) / 3.0;
}
int main()
{
double result = average(80.0, 90.0, 75.0);
cout << "平均分是:" << result << endl; // 输出 81.6667
return 0;
}
注意事项:
- 参数之间用逗号隔开
- 每个参数都需要单独声明类型
- 不能写成
average(double a, b, c),这样会报错 - 传入参数的个数和类型必须和函数定义匹配
3. 另一个接受两个参数的函数——实际应用场景
// 判断一个数是否在指定范围内
bool inRange(int value, int max)
{
if (value >= 0 && value <= max)
return true;
else
return false;
}
int main()
{
int score = 85;
if (inRange(score, 100))
cout << "成绩有效" << endl;
else
cout << "成绩无效" << endl;
return 0;
}
为什么要学多参数函数?
现实中的任务很少只涉及一个数据。比如:
- 判断日期是否合法(需要年、月、日三个参数)
- 计算矩形面积(需要长和宽两个参数)
- 计算折扣后的价格(需要原价和折扣率两个参数)
4. 函数和数组
4.1 函数如何使用指针来处理数组
核心概念: 当你把数组传给函数时,函数实际接收到的不是整个数组的副本,而是数组首元素的地址(指针)。
// 计算数组中所有元素的总和
int sumArray(int arr[], int size)
{
int total = 0;
for (int i = 0; i < size; i++)
{
total += arr[i];
}
return total;
}
int main()
{
int scores[] = {80, 90, 75, 85, 95};
int size = 5;
int total = sumArray(scores, size);
cout << "总分是:" << total << endl; // 输出 425
return 0;
}
为什么数组传给函数不会被复制?
想象一个班有100个学生的成绩存在数组里。如果每次都复制整个数组给函数,那内存开销太大了。所以 C++ 的做法是:只传一个地址,告诉函数"数据在那里,你自己去看"。
这就是为什么函数定义中写 int arr[] 看起来像数组,但本质上它是一个指针。
4.2 将数组作为参数意味着什么
重要事实:
- 数组作为参数传递时,总是按地址传递(不是按值传递)
- 函数中对数组元素的修改会直接影响原数组
- 所以必须额外传一个
size参数,因为函数不知道数组有多长
// 将数组中所有元素加倍
void doubleArray(int arr[], int size)
{
for (int i = 0; i < size; i++)
{
arr[i] *= 2; // 直接修改原数组!
}
}
int main()
{
int nums[] = {1, 2, 3, 4, 5};
doubleArray(nums, 5);
for (int i = 0; i < 5; i++)
cout << nums[i] << " "; // 输出 2 4 6 8 10
return 0;
}
4.3 更多数组函数示例
// 找出数组中的最大值
int findMax(int arr[], int size)
{
int maxVal = arr[0];
for (int i = 1; i < size; i++)
{
if (arr[i] > maxVal)
maxVal = arr[i];
}
return maxVal;
}
// 统计数组中某个值出现的次数
int countValue(int arr[], int size, int target)
{
int count = 0;
for (int i = 0; i < size; i++)
{
if (arr[i] == target)
count++;
}
return count;
}
4.4 使用数组区间的函数
数组区间用指向第一个元素的指针和指向超尾的指针来表示:
// 使用指针区间来计算总和
int sumRange(int *begin, int *end)
{
int total = 0;
for (int *p = begin; p < end; p++) // p 从 begin 走到 end(不含)
{
total += *p;
}
return total;
}
int main()
{
int arr[] = {10, 20, 30, 40, 50};
// 计算所有元素的和
int total1 = sumRange(arr, arr + 5);
cout << "全部总和:" << total1 << endl; // 150
// 只计算前3个元素的和
int total2 = sumRange(arr, arr + 3);
cout << "前3个总和:" << total2 << endl; // 60
// 计算第2到第4个元素的和
int total3 = sumRange(arr + 1, arr + 4);
cout << "第2到第4个总和:" << total3 << endl; // 90
return 0;
}
什么是"超尾"?
arr + 5 指向的是数组最后一个元素之后的那个位置(不存在的位置)。它就像书签一样标记"到这里就结束了",但并不指向实际的数据。
5. 指针和 const
5.1 两种保护方式
C++ 提供了两种使用 const 保护数据的方式:
// 方式1:const 指针 —— 指针指向的值不能改,但指针可以指向别处
const int *p = &num1;
// *p = 10; // 错误!不能通过 p 修改值
p = &num2; // 正确!可以让 p 指向别的地方
// 方式2:指针本身是 const —— 指针不能指向别处,但可以修改指向的值
int * const p2 = &num1;
*p2 = 10; // 正确!可以修改值
// p2 = &num2; // 错误!不能让 p2 指向别的地方
// 方式3:两者都不能改
const int * const p3 = &num1;
// *p3 = 10; // 错误
// p3 = &num2; // 错误
通俗比喻:
| 情况 | 比喻 |
|---|---|
const int *p | 你拿到了一个地址,可以去看别人家,但不能动别人家的东西 |
int * const p | 你只能去你指定的那一家,但到了那家你可以随便动 |
const int * const p | 你只能去一家,而且不能动任何东西 |
为什么要学这个?
函数参数使用 const 指针可以防止意外修改数据,同时避免复制整个数组的开销:
// 用 const 保护数组不被修改
void printArray(const int arr[], int size)
{
for (int i = 0; i < size; i++)
{
cout << arr[i] << " ";
// arr[i] = 0; // 错误!编译器会阻止你修改
}
}
6. 函数和二维数组
二维数组传给函数时,必须指定列数(但行数可以省略):
// 打印二维数组
void print2D(int arr[][4], int rows) // 列数 4 必须写
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < 4; j++)
{
cout << arr[i][j] << "\t";
}
cout << endl;
}
}
int main()
{
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
print2D(matrix, 3);
return 0;
}
为什么必须指定列数?
二维数组在内存中是一行接一行连续存放的。编译器需要知道每行有多少列,才能正确地用 arr[i][j] 定位到正确的元素。如果不知道列数,编译器不知道第二行从哪里开始。
7. 函数和 C-风格字符串
7.1 将 C-风格字符串作为参数
// 计算字符串长度(不用 strlen 的手动版)
int myStrlen(const char str[])
{
int count = 0;
while (str[count] != '\0') // 到结束符为止
{
count++;
}
return count;
}
int main()
{
char greeting[] = "Hello";
cout << "长度是:" << myStrlen(greeting) << endl; // 5
return 0;
}
注意: 字符串以 '\0'(空字符)结尾,所以不需要额外传长度参数,函数自己就能数到结尾。
7.2 返回 C-风格字符串的函数
// 拼接两个字符串(手动实现,不用 strcat)
char* concatStrings(const char str1[], const char str2[])
{
static char result[100]; // 用 static,函数结束后不会被销毁
int i = 0, j = 0;
// 先复制第一个字符串
while (str1[i] != '\0')
{
result[i] = str1[i];
i++;
}
// 再追加第二个字符串
while (str2[j] != '\0')
{
result[i] = str2[j];
i++;
j++;
}
result[i] = '\0'; // 别忘了加结束符!
return result;
}
注意事项:
- 返回局部数组是危险的(函数结束后局部变量会被销毁),所以这里用了
static - 字符串操作一定要记得处理
'\0'结束符
8. 函数和结构
8.1 传递和返回结构
结构体(struct)可以像普通变量一样传递给函数和从函数返回:
struct Student
{
char name[20];
int age;
double score;
};
// 显示学生信息
void displayStudent(Student s)
{
cout << "姓名:" << s.name << endl;
cout << "年龄:" << s.age << endl;
cout << "成绩:" << s.score << endl;
}
// 返回一个学生结构
Student createStudent()
{
Student s;
strcpy(s.name, "小明");
s.age = 18;
s.score = 95.5;
return s;
}
int main()
{
Student stu = createStudent(); // 用函数创建结构
displayStudent(stu); // 传结构给函数
return 0;
}
8.2 另一个处理结构的函数示例
struct Point
{
double x;
double y;
};
// 计算两点之间的距离
double distance(Point p1, Point p2)
{
double dx = p1.x - p2.x;
double dy = p1.y - p2.y;
return sqrt(dx * dx + dy * dy);
}
int main()
{
Point a = {0.0, 0.0};
Point b = {3.0, 4.0};
cout << "两点距离:" << distance(a, b) << endl; // 5
return 0;
}
8.3 传递结构的地址
当结构体比较大时,传整个结构的拷贝会浪费内存和时间。可以传递结构的地址,效率更高:
// 传地址版本——高效,不复制整个结构
void updateScore(Student *s, double newScore)
{
s->score = newScore; // 用 -> 操作符通过指针访问成员
}
int main()
{
Student stu = {"小红", 17, 88.0};
updateScore(&stu, 95.0); // 传入地址
cout << "更新后的成绩:" << stu.score << endl; // 95.0
return 0;
}
为什么要传地址?
假设 Student 结构有 1000 个字节,每次调用函数都复制一份就太浪费了。传地址只需要传 4 或 8 个字节(指针的大小),快得多。
传值 vs 传地址 对比:
传值 f(Student s) | 传地址 f(Student *s) | |
|---|---|---|
| 安全性 | 安全(改不了原数据) | 可能修改原数据 |
| 效率 | 慢(复制整个结构) | 快(只传地址) |
| 使用场景 | 只需要读取数据 | 需要修改数据或结构很大时 |
9. 函数和 string 对象
string 是 C++ 的字符串类,比 C-风格字符串方便得多:
#include <string>
using namespace std;
// string 作为参数
void printString(const string &s) // 用引用传递,避免复制
{
cout << "字符串内容:" << s << endl;
cout << "长度:" << s.length() << endl;
}
// 返回 string
string makeGreeting(const string &name)
{
return "你好," + name + "!欢迎来到C++世界。";
}
// 修改 string
void toUpperCase(string &s)
{
for (int i = 0; i < s.length(); i++)
{
if (s[i] >= 'a' && s[i] <= 'z')
s[i] = s[i] - 32; // 转成大写
}
}
int main()
{
string msg = makeGreeting("小明");
printString(msg);
string text = "hello world";
toUpperCase(text);
cout << text << endl; // HELLO WORLD
return 0;
}
string vs C-风格字符串:
| 特性 | C-风格字符串 | string 对象 |
|---|---|---|
| 拼接 | 手动处理 \0,容易出错 | 直接用 + |
| 长度 | 需要 strlen() | 直接用 .length() |
| 复制 | strcpy() | 直接用 = |
| 比较 | 不能直接用 == | 可以直接用 == |
| 安全性 | 容易越界 | 自动管理内存 |
10. 函数与 array 对象
C++11 引入的 std::array 是对原生数组的安全封装:
#include <array>
using namespace std;
// array 作为参数
void printArray(const array<double, 5> &arr)
{
for (int i = 0; i < arr.size(); i++) // .size() 获取大小
{
cout << arr[i] << " ";
}
cout << endl;
}
// 返回 array
array<double, 5> fillArray()
{
array<double, 5> result;
for (int i = 0; i < 5; i++)
{
result[i] = (i + 1) * 1.5;
}
return result;
}
// 计算平均值
double average(const array<double, 5> &arr)
{
double sum = 0;
for (int i = 0; i < arr.size(); i++)
sum += arr[i];
return sum / arr.size();
}
int main()
{
array<double, 5> nums = fillArray();
printArray(nums);
cout << "平均值:" << average(nums) << endl;
return 0;
}
array 的优势:
- 知道自己的大小(
.size()),不需要额外传 size 参数 - 可以直接赋值和比较
- 不会越界(有边界检查)
11. 递归
11.1 什么是递归?
递归就是函数自己调用自己。就像你站在两面镜子中间,会看到无限个自己的倒影。
通俗案例——剥洋葱:
想象你在问"我今年几岁了?“,答案取决于"我爸爸几岁了?”,而你爸爸的答案又取决于"我爷爷几岁了?",直到问到一个不需要再问的人(比如你爷爷自己知道答案)。这个"不需要再问"的点就是递归的终止条件。
11.2 包含一个递归调用的递归
经典例子:计算阶乘
5! = 5 × 4 × 3 × 2 × 1 = 120
int factorial(int n)
{
if (n <= 1) // 终止条件(最关键!)
return 1;
return n * factorial(n - 1); // 自己调用自己
}
int main()
{
cout << "5! = " << factorial(5) << endl; // 120
return 0;
}
调用过程详解:
factorial(5)
→ 5 * factorial(4)
→ 5 * (4 * factorial(3))
→ 5 * (4 * (3 * factorial(2)))
→ 5 * (4 * (3 * (2 * factorial(1))))
→ 5 * (4 * (3 * (2 * 1))) // 触发终止条件
= 5 * (4 * (3 * 2))
= 5 * (4 * 6)
= 5 * 24
= 120
递归的两个必要条件:
- 终止条件(base case):告诉函数什么时候停下来
- 递归调用:每次调用都让问题变得更小,逐渐逼近终止条件
警告: 如果忘记终止条件,函数会无限调用自己,最终导致栈溢出(stack overflow),程序崩溃!
再举一个简单的例子——倒数:
void countdown(int n)
{
if (n <= 0) // 终止条件
{
cout << "发射!" << endl;
return;
}
cout << n << "..." << endl;
countdown(n - 1); // 递归调用
}
int main()
{
countdown(3); // 输出:3... 2... 1... 发射!
return 0;
}
11.3 包含多个递归调用的递归
经典例子:斐波那契数列
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, …
每个数等于前两个数之和。
int fibonacci(int n)
{
if (n <= 1) // 终止条件
return 1;
return fibonacci(n - 1) + fibonacci(n - 2); // 两个递归调用
}
int main()
{
for (int i = 0; i < 8; i++)
cout << fibonacci(i) << " ";
// 输出:1 1 2 3 5 8 13 21
return 0;
}
调用过程(以 fibonacci(4) 为例):
fibonacci(4)
→ fibonacci(3) + fibonacci(2)
→ (fibonacci(2) + fibonacci(1)) + (fibonacci(1) + fibonacci(0))
→ ((fibonacci(1) + fibonacci(0)) + 1) + (1 + 1)
→ ((1 + 1) + 1) + (1 + 1)
= 5
注意: 多个递归调用时,计算量会急剧增长(指数级)。对于较大的 n 值,效率会很低。
11.4 递归 vs 循环
| 递归 | 循环 | |
|---|---|---|
| 代码 | 通常更简洁 | 通常更长 |
| 效率 | 有额外开销(函数调用) | 更高效 |
| 可读性 | 某些问题更直观 | 某些问题更直观 |
| 内存 | 每次调用占用栈空间 | 不额外占栈空间 |
什么时候用递归?
- 问题本身有递归结构(如树的遍历、分治算法)
- 用循环写反而更复杂的情况
- 简单的递归(如阶承)可以改写成循环,通常更推荐
12. 函数指针
12.1 函数指针的基础知识
函数也有内存地址!函数指针就是指向函数的指针,它存储的是函数在内存中的入口地址。
通俗比喻:
你可以把函数想象成电话号码。函数名就是电话簿上的名字,函数指针就是你自己手写的号码。你可以通过号码(指针)来打电话(调用函数),而不一定非要通过电话簿(函数名)。
// 声明一个函数指针
// 指向"接受两个 int 参数,返回 int"的函数
int (*pf)(int, int); // pf 是函数指针的名字
// 让它指向 add 函数
pf = add; // 注意:不加括号和参数
// 通过函数指针调用函数
int result = pf(3, 5); // 等同于 add(3, 5)
函数指针的声明规则:
返回值类型 (*指针名)(参数类型列表);
关键区别:
int *f(int); // 这是一个函数声明:f 是一个返回 int* 的函数
int (*f)(int); // 这是一个函数指针:f 是指向函数的指针
// 注意括号的位置!括号让 * 和 f 结合,变成"指针"
12.2 函数指针示例
#include <iostream>
using namespace std;
// 三个数学运算函数
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
// 一个通用的"计算器"函数
// 它不知道具体做什么运算,只负责调用传入的函数指针
int calculate(int a, int b, int (*operation)(int, int))
{
return operation(a, b);
}
int main()
{
cout << "加法:" << calculate(10, 5, add) << endl; // 15
cout << "减法:" << calculate(10, 5, subtract) << endl; // 5
cout << "乘法:" << calculate(10, 5, multiply) << endl; // 50
return 0;
}
为什么要学函数指针?
- 实现"策略模式":同一个函数可以执行不同的逻辑(如上面的计算器)
- 回调函数:让别人注册一段代码,在特定时候执行
- 作为参数传递:可以把"行为"作为参数传给其他函数
12.3 深入探讨函数指针
函数指针可以用在数组中,实现查表调用:
#include <iostream>
using namespace std;
double square(double x) { return x * x; }
double cube(double x) { return x * x * x; }
double absolute(double x) { return x < 0 ? -x : x; }
int main()
{
// 函数指针数组
double (*funcs[])(double) = {square, cube, absolute};
double value = -3.0;
const char *names[] = {"平方", "立方", "绝对值"};
for (int i = 0; i < 3; i++)
{
cout << names[i] << "(" << value << ") = "
<< funcs[i](value) << endl;
}
// 输出:
// 平方(-3) = 9
// 立方(-3) = -27
// 绝对值(-3) = 3
return 0;
}
12.4 使用 typedef 简化
函数指针的语法比较难看,用 typedef(或 C++11 的 using)可以简化:
// 原始写法——难看
int (*pf)(int, int);
// 用 typedef 简化
typedef int (*BinaryOp)(int, int);
BinaryOp pf1 = add;
BinaryOp pf2 = subtract;
// 用 using 简化(C++11,推荐)
using UnaryOp = double(*)(double);
UnaryOp op1 = square;
UnaryOp op2 = cube;
// 实际使用
cout << pf1(10, 5) << endl; // 15
cout << op1(3.0) << endl; // 9
使用 typedef 后,代码可读性大幅提升:
| 写法 | 可读性 |
|---|---|
int (*pf)(int, int) | 差,一眼看不出是什么 |
BinaryOp pf | 好,一眼就知道是"二元运算符" |
总结:各知识点速查表
| 知识点 | 核心要点 | 典型用途 |
|---|---|---|
| 函数定义 | 返回类型 + 函数名 + 参数 + 函数体 | 封装可复用的代码逻辑 |
| 函数原型 | 提前声明函数签名,放在 main 之前 | 允许函数在 main 之后定义 |
| 按值传递 | 函数收到的是参数的副本 | 简单数据的传递 |
| 数组参数 | 传的是地址,函数可以修改原数组 | 处理批量数据 |
| const 指针 | 防止函数意外修改数据 | 保护数据安全性 |
| 二维数组参数 | 列数必须指定 | 处理矩阵数据 |
| 字符串参数 | 以 \0 判断结束 | 字符串处理 |
| 结构参数 | 可传值(拷贝)或传地址(高效) | 传递复杂数据组合 |
| string 参数 | 用引用传递最高效 | 字符串操作 |
| array 参数 | 自带大小信息,更安全 | 固定大小的数组操作 |
| 递归 | 函数调用自身,必须有终止条件 | 阶乘、斐波那契、树遍历 |
| 函数指针 | 指向函数的指针,可动态选择调用哪个函数 | 回调机制、策略模式 |
练习建议
- 基础练习: 写函数实现数组求和、求最大值、求平均值
- 字符串练习: 写函数实现字符串反转、查找字符、字符串比较
- 结构练习: 定义学生结构,写函数实现排序和查找
- 递归练习: 用递归实现求和 1+2+…+n,以及用递归反转数字(如 1234 → 4321)
- 函数指针练习: 写一个通用的数组处理函数,通过函数指针传入不同的处理逻辑(排序规则、过滤条件等)
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq_45746668/article/details/166644500




