知识分享小能手头像
关注
C++ 学习教程,从入门到精通,函数——C++的编程模块(7)封面图

C++ 学习教程,从入门到精通,函数——C++的编程模块(7)

函数——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

递归的两个必要条件:

  1. 终止条件(base case):告诉函数什么时候停下来
  2. 递归调用:每次调用都让问题变得更小,逐渐逼近终止条件

警告: 如果忘记终止条件,函数会无限调用自己,最终导致栈溢出(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;
}

为什么要学函数指针?

  1. 实现"策略模式":同一个函数可以执行不同的逻辑(如上面的计算器)
  2. 回调函数:让别人注册一段代码,在特定时候执行
  3. 作为参数传递:可以把"行为"作为参数传给其他函数

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. 字符串练习: 写函数实现字符串反转、查找字符、字符串比较
  3. 结构练习: 定义学生结构,写函数实现排序和查找
  4. 递归练习: 用递归实现求和 1+2+…+n,以及用递归反转数字(如 1234 → 4321)
  5. 函数指针练习: 写一个通用的数组处理函数,通过函数指针传入不同的处理逻辑(排序规则、过滤条件等)

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_45746668/article/details/166644500

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--