文章目录

  • 前言
  • 函数基础
  • 1. 定义函数
  • 2. 调用函数
  • 3. 返回值
  • 函数参数
  • 1. 值传递 (Pass by Value)
  • 2. 引用传递 (Pass by Reference)
  • 3. 指针传递 (Pass by Pointer)
  • 默认参数值
  • 基本概念
  • 示例
  • 使用场景和注意事项
  • 函数重载
  • 函数重载的基本原则
  • 示例
  • 使用场景和注意事项
  • 递归函数
  • 基本概念
  • 示例:计算阶乘
  • 使用场景和注意事项
  • 内联函数
  • 基本概念
  • 示例
  • 使用场景和注意事项
  • Lambda表达式
  • 基本概念
  • 示例
  • 捕获方式详解
  • 1. 按值捕获 `[=]`
  • 2. 按引用捕获 `[&]`
  • 3. 显式捕获
  • 4. 默认捕获与显式混合
  • 5. 捕获 `this` 指针
  • 使用场景和注意事项
  • 函数模板
  • 基本概念
  • 示例
  • 使用场景和注意事项
  • 总结
  • 接下继续先把c++基础知识搞完,然后整点提高版,有点深度的!!!
  • 有兴趣的朋友,点个赞,加个关注啊



前言

本文主要探讨C++中的函数、模板以及Lambda表达式等。


函数基础

当然,让我们详细介绍一下C++中函数的基础概念:

1. 定义函数

函数定义指定了函数要执行的操作。在C++中,函数的定义包括几个关键部分:

  • 返回类型:指明函数返回的数据类型。如果函数不返回任何值,则使用 void 类型。
  • 函数名:函数的唯一标识符,用于调用函数。
  • 参数列表:括号内的参数列表,指定传递给函数的变量。如果函数不接受任何参数,则此处为空。
  • 函数体:大括号 {} 内的代码块,定义了函数的执行操作。

例如,一个简单的函数定义可以是:

int add(int x, int y) {
    return x + y;
}

这个函数接受两个整数参数,并返回它们的和。

2. 调用函数

一旦定义了函数,就可以在程序的其他部分调用它。调用函数意味着告诉程序执行该函数的代码。例如,使用上面定义的 add 函数:

int main() {
    int result = add(5, 3);
    std::cout << "The sum is: " << result << std::endl;
    return 0;
}

在这里,add(5, 3) 调用了 add 函数,并把返回的结果赋值给 result 变量。

3. 返回值

函数可以返回一个值,该值可以是任何数据类型。在函数定义中指定了返回类型,函数体中的 return 语句用于返回值。如果函数的返回类型为 void,则表示该函数不返回任何值。例如,在上面的 add 函数中,return x + y; 语句返回两个参数的和。

代码示例:

#include <iostream>

// 函数定义
int add(int x, int y) {
    return x + y; // 返回两个参数的和
}

int main() {
    int a = 5;
    int b = 3;

    // 调用函数并接收返回值
    int sum = add(a, b);

    // 输出结果
    std::cout << "The sum of " << a << " and " << b << " is: " << sum << std::endl;

    return 0;
}

在这个例子中:

  1. 首先包含了 iostream 库,以便使用输入输出功能。
  2. 定义了一个名为 add 的函数,它接受两个整数参数 xy,并返回它们的和。这是通过 return x + y; 这行代码实现的。
  3. main 函数中,定义了两个整数 ab,并将它们作为参数传递给 add 函数。
  4. add 函数的返回值被赋值给变量 sum
  5. 最后,使用 std::cout 输出计算结果。

函数参数

在C++中,函数参数可以通过三种主要方式传递:值传递引用传递指针传递。每种方式对参数的处理和对函数外部变量的影响有所不同。

1. 值传递 (Pass by Value)

  • 概念值传递时,函数接收参数的副本。在函数内部对参数的任何修改都不会影响原始数据
  • 特点:安全且简单,但可能会导致额外的内存和时间开销(特别是对于大型结构体或类)。
  • 示例
void modify(int x) {
    x = 10; // 只修改了副本
}

int main() {
    int a = 5;
    modify(a);
    // 'a' 仍然是 5,因为 'modify' 只修改了它的副本
}

2. 引用传递 (Pass by Reference)

  • 概念:通过引用传递参数,函数接收的是参数的引用(或别名)。在函数内部对参数的任何修改都会直接影响原始数据。
  • 特点:可以避免复制大型数据结构,同时允许函数修改调用者的数据。
  • 示例
void modify(int &x) {
    x = 10; // 直接修改原始数据
}

int main() {
    int a = 5;
    modify(a);
    // 'a' 现在是 10,因为 'modify' 修改了它的原始数据
}

3. 指针传递 (Pass by Pointer)

  • 概念:通过指针传递参数,函数接收的是参数的地址。通过这个地址,函数可以修改原始数据。
  • 特点:和引用传递类似,允许修改原始数据。但使用指针更加灵活,因为它可以接收空值(nullptr),从而表示“没有有效数据”。
  • 示例
void modify(int *x) {
    if (x != nullptr) {
        *x = 10; // 通过指针修改原始数据
    }
}

int main() {
    int a = 5;
    modify(&a);
    // 'a' 现在是 10,因为 'modify' 修改了它的原始数据
}

默认参数值

在C++中,默认参数是一个非常实用的特性,可以在定义函数时为一个或多个参数指定默认值。当调用这个函数而没有为这些参数提供值时,就会使用这些默认值。这样可以增加函数的灵活性,并减少在不同情况下需要编写的重载函数数量。

基本概念

  • 定义默认参数:在函数声明时,可以为一个或多个参数指定默认值。
  • 调用含默认参数的函数:在调用函数时,可以省略那些有默认值的参数,也可以提供新的值来覆盖默认值。
  • 注意事项:默认参数只能从右向左定义,即如果一个参数有默认值,那么它右边的所有参数也必须有默认值。

示例

假设有一个函数,用于计算两个数的和,其中第二个数有一个默认值:

#include <iostream>

// 函数声明,带有一个默认参数
int add(int x, int y = 10) {
    return x + y;
}

int main() {
    std::cout << "Add 5 and 3: " << add(5, 3) << std::endl; // 输出 8
    std::cout << "Add 5 and default (10): " << add(5) << std::endl; // 输出 15,使用默认参数值

    return 0;
}

在这个例子中:

  • add 函数有两个参数,xyy 被赋予了默认值 10。
  • 当调用 add(5, 3) 时,使用的是提供的值 3。
  • 当调用 add(5) 时,省略了第二个参数,因此自动使用默认值 10。

使用场景和注意事项

  • 使用场景:默认参数非常适合于那些有“典型值”的参数,这些典型值在大多数调用中都不会改变。
  • 注意事项
  • 默认参数应在函数声明中定义,而不是在函数定义中(如果函数声明和定义是分开的)。
  • 如果函数在多个地方声明(如在不同的头文件中),那么默认参数只应在一个地方声明。
  • 默认参数的值是在函数被调用时确定的,而不是在函数被定义时。

函数重载

函数重载允许使用相同的函数名来定义多个函数,只要这些函数的参数列表(参数的类型、数量或顺序)不同。这增加了代码的可读性和灵活性,因为你可以为相似的操作使用相同的函数名。

函数重载的基本原则

  • 不同的参数列表:函数可以被重载,前提是它们的参数列表不同。这种差异可以通过改变参数的数量、类型或参数的顺序来实现。
  • 返回类型不是重载的依据:仅仅返回类型不同并不足以构成函数重载。即使返回类型不同,参数列表也必须有所不同。
  • 调用时的匹配:当调用一个重载的函数时,编译器会根据提供的参数类型和数量来决定使用哪个函数版本。

示例

以下是一个函数重载的示例,使用相同的函数名(print)来处理不同类型或数量的参数:

#include <iostream>

// 打印整数
void print(int i) {
    std::cout << "Printing int: " << i << std::endl;
}

// 打印浮点数
void print(double f) {
    std::cout << "Printing float: " << f << std::endl;
}

// 打印两个整数
void print(int i, int j) {
    std::cout << "Printing two ints: " << i << ", " << j << std::endl;
}

int main() {
    print(5);       // 调用 print(int)
    print(5.5);     // 调用 print(double)
    print(5, 10);   // 调用 print(int, int)

    return 0;
}

在这个例子中,print 函数被重载了三次:一次用于打印一个整数,一次用于打印一个浮点数,还有一次用于同时打印两个整数。

使用场景和注意事项

  • 使用场景:函数重载非常适合于那些执行相似操作但参数类型或数量不同的场景。
  • 注意事项
  • 重载的函数应具有明确的、不会引起混淆的参数列表。
  • 使用重载时要小心,确保每个函数版本的行为都是清晰和预期的,以避免在调用时产生歧义。
  • 过度使用重载可能会导致代码难以理解和维护,特别是当函数具有多个参数时。

递归函数

在C++中,递归函数是一种调用自身的函数。递归可以用来解决那些可以分解为相似子问题的问题,尤其是在处理数据结构(如树和图)或进行算法操作(如排序和搜索)时非常有用。

基本概念

  • 递归定义:一个函数在其定义中调用自己。
  • 基本情况:每个递归函数都应该有一个或多个不涉及递归调用的退出条件,用于终止递归。
  • 递归情况:递归函数的部分,其中函数调用自身来解决子问题。

示例:计算阶乘

一个典型的递归函数示例是计算整数的阶乘。阶乘 n! 定义为从 1 到 n 的所有整数的乘积,且 0! = 1。

#include <iostream>

// 递归函数来计算阶乘
int factorial(int n) {
    if (n <= 1) {
        return 1; // 基本情况:0! = 1! = 1
    } else {
        return n * factorial(n - 1); // 递归情况
    }
}

int main() {
    int num = 5;
    std::cout << "Factorial of " << num << " is " << factorial(num) << std::endl;
    return 0;
}

在这个例子中,factorial 函数调用自己来计算较小整数的阶乘,直到达到退出条件(n <= 1)。

使用场景和注意事项

  • 使用场景:递归非常适合于处理那些可以自然分解为更小、更简单子问题的任务,如树的遍历、某些数学问题等。
  • 注意事项
  • 确保递归有退出条件,以避免无限递归。
  • 递归函数可能会消耗大量内存,尤其是深度递归的情况。
  • 在某些情况下,使用循环可能比递归更高效。

内联函数

内联函数是C++提供的一个特性,用于优化小型、频繁调用的函数。当声明一个函数为内联时,编译器会尝试将该函数的所有调用直接替换为函数本身的代码。这样可以减少函数调用的开销,尤其是在循环或频繁调用的场景中。

基本概念

  • 定义内联函数:通过在函数声明前添加关键字 inline 来定义内联函数。
  • 工作原理:编译器会在每个调用点将内联函数替换为相应的函数代码。
  • 使用场景:通常用于小型函数,如简单的访问器或修改器,这些函数只有几行代码。

示例

下面是一个内联函数的示例:

#include <iostream>

inline int max(int x, int y) {
    return (x > y) ? x : y; // 返回 x 和 y 中的最大值
}

int main() {
    std::cout << "Max of 10 and 20 is " << max(10, 20) << std::endl;
    return 0;
}

在这个例子中,max 函数被定义为内联。这意味着在调用 max(10, 20) 时,编译器可能会直接用 (10 > 20) ? 10 : 20 来替换这个调用,从而减少一个函数调用的开销。

使用场景和注意事项

  • 使用场景:对于非常简短、频繁调用的函数,如简单的数学运算、条件判断或访问器函数。
  • 注意事项
  • 内联仅是对编译器的一个请求,编译器可以选择忽略这个请求。
  • 内联大型函数或递归函数可能导致代码膨胀(增加了程序的大小)。
  • 过度使用内联可能会降低程序的整体性能。
  • 内联函数的定义必须在调用之前或与调用在同一个文件中,以便编译器可以在编译时进行替换。

Lambda表达式

Lambda表达式是C++11及其后续版本中引入的一项功能,它允许在代码中定义匿名函数。Lambda表达式是一种便捷的编写内联函数的方式,尤其是在需要作为参数传递给算法或其他函数时。

基本概念

  • 匿名函数:Lambda表达式本质上是一个没有名称的函数。
  • 语法:通常形式为 [capture](parameters) -> return_type { body },其中大部分元素可以根据需要省略。
  • 捕获列表:Lambda表达式通过捕获列表来访问它所在作用域中的变量。
  • 参数和返回类型:和普通函数一样,Lambda可以有参数和返回类型。如果Lambda体只包含一个返回语句,或者返回类型是 void,则可以省略返回类型说明。

示例

下面是一个Lambda表达式的示例,它演示了如何定义和使用Lambda表达式:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    int factor = 2;

    // 使用Lambda表达式来操作每个元素
    std::for_each(numbers.begin(), numbers.end(), [factor](int &n) {
        n *= factor; // 每个元素乘以因子
    });

    // 输出结果
    for (int n : numbers) {
        std::cout << n << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个例子中,Lambda表达式被用于 std::for_each 算法中,对 numbers 向量的每个元素进行操作。Lambda以 [factor] 捕获外部变量 factor,使得在Lambda体内可以访问它。

捕获方式详解

Lambda表达式的捕获列表定义了Lambda体内可以访问的外部变量。根据捕获方式的不同,这些变量可以按值捕获、按引用捕获或以其他方式捕获。

1. 按值捕获 [=]

  • 行为:按值捕获时,Lambda表达式将外部变量捕获值捕获列表中,并且每个变量都是以其在Lambda创建时的值的副本捕获的。
  • 特点:捕获的副本在Lambda体内是常量,除非在参数列表后面加上mutable关键字。
  • 示例
int x = 10;
auto lambda = [=]() mutable { x = 42; }; // 'x' 是副本
lambda();
// 原始的 'x' 保持不变,仍为 10

2. 按引用捕获 [&]

  • 行为:按引用捕获时,Lambda表达式将外部变量捕获值捕获列表中,并且变量是以引用的方式捕获的。
  • 特点:Lambda体内对这些变量的任何修改都会影响到原始变量。
  • 示例
int x = 10;
auto lambda = [&]() { x = 42; }; // 'x' 是引用
lambda();
// 原始的 'x' 现在变为 42

3. 显式捕获

  • 行为:可以显式地按值或按引用捕获特定的变量。
  • 示例
int x = 10, y = 20;
auto lambda = [x, &y]() { /* 使用 'x' 的副本和 'y' 的引用 */ };

4. 默认捕获与显式混合

  • 行为:可以混合使用默认捕获方式(全部按值或全部按引用)与显式指定的方式。
  • 示例
int x = 10, y = 20;
auto lambda = [=, &y]() { /* 按值捕获除 'y' 外的所有变量,'y' 通过引用捕获 */ };
auto lambda2 = [&, x]() { /* 按引用捕获除 'x' 外的所有变量,'x' 通过值捕获 */ };

5. 捕获 this 指针

  • 行为:在类的成员方法中,可以捕获 this 指针来访问类的成员。
  • 示例
class Example {
    int value = 10;
public:
    void method() {
        auto lambda = [this]() { return value; };
    }
};

使用场景和注意事项

  • 使用场景:Lambda表达式非常适合用于需要临时函数对象的场景,如作为参数传递给算法,或者用于定义简单的回调函数等。
  • 注意事项
  • 捕获列表中的变量可以按值([=]),按引用([&]),或者混合([=, &foo])捕获。
  • Lambda表达式的类型是唯一的,不可命名的。如果需要类型,可以使用 auto 关键字或 std::function
  • 在捕获外部变量时要注意生命周期问题,确保Lambda使用时,它所捕获的变量仍然有效。

函数模板

函数模板是C++中实现泛型编程的一种强大工具。它允许编写独立于特定数据类型的代码。这意味着可以用同一段代码来处理不同类型的数据,无需为每种数据类型编写重复的函数。

基本概念

  • 泛型编程:函数模板是泛型编程的一部分。泛型编程的目标是使算法尽可能独立于使用的数据类型。
  • 模板定义:函数模板以 template 关键字开始,后跟模板参数列表,这些参数是类型或非类型参数。
  • 类型参数:代表一种数据类型,使用模板时,这些参数被特定的类型所替代。
  • 非类型参数:代表一个值而非类型,它可以是一个整数或者指向某种类型的指针等。

示例

下面是一个简单的函数模板示例,它定义了一个交换两个值的模板函数:

template <typename T>
void Swap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

int main() {
    int i = 10, j = 20;
    Swap(i, j); // 用于 int 类型

    double x = 10.5, y = 20.5;
    Swap(x, y); // 用于 double 类型

    return 0;
}

在这个例子中,Swap 函数模板可以用于任何数据类型。当用于 intdouble 或其他任何类型时,编译器会为每种类型生成一个特定的函数实例。

使用场景和注意事项

  • 使用场景:函数模板特别适用于算法,这些算法的基本操作(如比较、交换、复制等)独立于操作的数据类型。
  • 注意事项
  • 模板代码应保持简洁,以避免在实例化时产生过于复杂的代码。
  • 某些复杂的情况可能需要特化模板,这是模板编程中的一个高级特性,允许您为特定类型提供特殊的实现。
  • 在设计函数模板时,考虑类型安全和通用性。

总结

这篇主要从函数基础,参数传递,默认参数,函数重载,递归函数,内联函数,Lambda表达式,函数模板这几个方面简要介绍了一下c++函数相关的一些知识。

接下继续先把c++基础知识搞完,然后整点提高版,有点深度的!!!

有兴趣的朋友,点个赞,加个关注啊