C++ primer plus学习笔记 --- 第7章、C++的编程模块

7.1、复习函数的基本知识

函数是编程中一个重要的概念,它将一段代码作为一个单独的单元进行封装,以便重复使用、简化程序结构、提高代码的可读性和可维护性等。

C++ 中的函数由函数头函数体组成。函数头定义了函数的返回值类型、函数名和参数列表,函数体则包含了一系列的语句和计算逻辑。在函数调用时,程序会跳转到函数体并执行其中的语句,返回结果后再跳回到原来的位置。

C++ 中的函数可以有返回值也可以没有,可以有参数也可以没有。其中,参数可以是基本类型(如整数、浮点数、字符等)或自定义类型(如结构体、类等)。函数也可以被重载,即定义多个同名函数,但参数(类型或数量)应该不同,这样可以有效提高代码的可复用性。

函数的声明和定义是两个不同的概念。函数的声明通常包含函数的返回值类型、函数名和参数列表,没有函数体。而函数的定义包含了函数的声明和函数体。在使用函数之前,需要先声明或定义函数,否则编译器将无法找到该函数的定义。函数的定义应该放在函数的声明之后,否则编译器将会报错。

在 C++ 中,有些函数属于 C++ 标准库,例如输入输出等,需要使用相应的头文件进行引用才能正常使用。同时,我们也可以自己编写函数,定义自己的函数,在程序中进行调用。

7.1.1、定义函数

在 C++ 中,我们可以使用 function_name() 的语法定义函数,其中 function_name 为函数名,括号中可以包含函数的参数列表。在函数定义中,我们需要指定函数的返回值类型、函数名和参数列表,以及函数体中的具体操作。

下面是一个用于计算两个整数之和的函数示例:

#include 

int sum(int a, int b) {
    int result = a + b;
    return result;
}

int main() {
    int x = 1;
    int y = 2;
    int z = sum(x, y); // 调用 sum() 函数,计算 x 和 y 的和,结果赋值给 z
    std::cout << "The sum of " << x << " and " << y << " is " << z << std::endl;
    return 0;
}

在上面的示例代码中,我们定义了一个 sum() 函数,它接受两个整数参数 a 和 b,返回它们的和。在 main() 函数中,我们定义了两个变量 x 和 y,并将它们作为参数调用 sum() 函数,将结果赋值给变量 z。最后,我们使用 std::cout 输出了计算结果。

需要注意的是,在函数声明中,我们必须指定函数的返回值类型和参数列表,确保程序可以正确调用函数。在函数体中,我们可以使用 return 语句返回函数的结果,也可以不返回任何值,这样的函数称为 void 类型函数。

7.1.2、函数原型和函数调用

在 C++ 中,如果在函数被调用之前没有进行函数声明或函数定义,编译器将无法识别该函数并抛出错误。因此,我们可以在函数被调用之前进行函数声明,这样编译器就能识别函数并进行编译。

函数声明也叫函数原型,它包含函数的返回值类型、函数名和参数列表,但没有函数体。在函数声明中,我们使用分号 ; 结尾来表示函数声明的结束。

下面是一个函数声明的示例:

int max(int a, int b);

在上面的示例中,我们声明了一个名为 max() 的函数,它接受两个整数参数,返回这两个整数的最大值。在 main() 函数中,我们可以使用该函数进行计算。

函数调用的语法是 function_name(arguments),其中 function_name 为函数名,arguments 为函数的实参(即调用函数时传递给函数的值)。在进行函数调用时,首先计算实参的值,然后将这些值传递给函数,在函数体中执行操作,并返回结果(如果有)。

下面是一个函数调用的示例:

#include 

int max(int a, int b);

int main() {
    int x = 1;
    int y = 2;
    int z = max(x, y); // 调用 max() 函数,计算 x 和 y 的最大值,结果赋值给 z
    std::cout << "The max of " << x << " and " << y << " is " << z << std::endl;
    return 0;
}

int max(int a, int b) {
    int result = a > b ? a : b;
    return result;
}

在上面的示例代码中,我们定义了 max() 函数,它接受两个整数参数,返回这两个整数的最大值。在 main() 函数中,我们调用 max() 函数计算 x 和 y 的最大值,将结果赋值给变量 z,并将结果输出到控制台。

7.2、函数参数和按值传递

在 C++ 中,函数常常需要接收参数,并对其进行操作。函数参数是指在函数调用时传递给函数的值,它可以是基本数据类型、数组、指针、结构体或其他复合类型等。

函数参数可以按值传递、按指针传递或按引用传递。按值传递是指将参数的值复制给函数的形式参数,这样函数内对形参的操作不会改变实参的值。按指针传递是指将参数的地址传递给函数的形式参数,通过指针可以对实参进行操作。按引用传递是指将参数的引用传递给函数的形式参数,通过引用也可以对实参进行操作,同时也减少了复制参数值的开销。

下面是一个按值传递的示例:

#include 

void swap(int x, int y); // 声明函数

int main() {
    int a = 1;
    int b = 2;
    std::cout << "Before swap: a = " << a << ", b = " << b << std::endl;
    swap(a, b); // 调用 swap() 函数,交换 a 和 b 的值
    std::cout << "After swap: a = " << a << ", b = " << b << std::endl;
    return 0;
}

void swap(int x, int y) { // 定义函数
    int t = x;
    x = y;
    y = t;
}

在上面的示例中,我们定义了一个 swap() 函数,用于交换两个整数的值。在 main() 函数中,我们定义了两个整数 a 和 b,并将它们作为参数调用 swap() 函数。我们期望该函数可以通过对 x 和 y 的值进行交换来改变 a 和 b 的值,但事实上并没有实现这个功能。原因是按值传递会将实参的值复制给函数的形参,因此函数内对形参的操作不会对实参造成影响。

为了解决这个问题,我们可以使用按引用传递的方式,将参数的引用传递给函数的形式参数,这样函数内对形参的操作就会也会对实参产生影响。

7.2.1、多个参数

在函数中,可以同时传递多个参数。我们只需要在函数声明或函数定义中列出需要的所有参数及其类型,并在函数调用时传递相应的实参即可。

下面是一个使用多个参数的示例:

#include 

void print_sum(int a, int b, int c); // 函数声明

int main() {
    int x = 1;
    int y = 2;
    int z = 3;
    print_sum(x, y, z); // 调用 print_sum() 函数,计算并输出三个数的和
    return 0;
}

void print_sum(int a, int b, int c) { // 函数定义
    int sum = a + b + c;
    std::cout << "The sum of " << a << ", " << b << " and " << c << " is " << sum << std::endl;
}

在上面的示例中,我们定义了一个 print_sum() 函数,它接受三个整数参数,将它们的和输出到控制台。在 main() 函数中,我们定义了三个整数 xyz,并将它们作为参数调用 print_sum() 函数。

7.2.2、另一个接受两个参数的函数

下面是另一个接受两个参数的函数的示例,它使用按值传递的方式:

#include 

int add(int x, int y);

int main() {
    int a = 1;
    int b = 2;
    int result = add(a, b);
    std::cout << a << " + " << b << " = " << result << std::endl;
    return 0;
}

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

在上面的示例中,我们定义了一个 add() 函数,它接受两个整数参数,返回它们的和。在 main() 函数中,我们定义了两个整数 a 和 b,并将它们作为参数调用 add() 函数。函数会将 a 和 b 的值复制给形参 x 和 y,然后计算它们的和并返回结果,该结果被赋值给变量 result,最后输出到控制台。

需要注意的是,由于参数是按值传递的,因此函数内对形参的操作不会影响变量 a 和 b 的值。如果我们希望函数内对形参的操作能够影响实参的值,可以使用按引用传递方式或按指针传递方式。

7.3、函数和数组

在 C++ 中,函数可以对数组进行操作。数组作为函数参数时,可以按值传递、按指针传递或按引用传递。

下面是一个函数对数组进行求和的示例,使用按值传递的方式:

#include 

int sum(int arr[], int size); // 声明函数

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    int size = sizeof(nums) / sizeof(int);
    int total = sum(nums, size); // 调用 sum() 函数,计算数组元素的和
    std::cout << "The sum of the array is " << total << std::endl;
    return 0;
}

int sum(int arr[], int size) { // 定义函数
    int result = 0;
    for (int i = 0; i < size; i++) {
        result += arr[i];
    }
    return result;
}

在上面的示例中,我们定义了一个 sum() 函数,它接受一个整型数组和数组的大小作为参数,返回数组元素的和。在 main() 函数中,我们定义了一个五元素整型数组 nums,并计算它的大小。然后我们调用 sum() 函数,将数组作为参数传递给该函数,并将函数的返回值赋值给变量 total,最后输出到控制台。

由于数组是以指针形式传递给函数的,因此函数内对数组元素的修改也会影响外部的数组。若使用按值传递方式,函数会默认复制一份数组并进行操作,因此函数内对形参的修改也不会影响外部的数组。

7.3.1、函数如何使用指针来处理数组

函数使用指针处理数组时,会将数组首元素的地址传递给函数,通过指针对数组进行操作。

下面是一个使用指针处理数组的示例:

#include 

void reverse(int* arr, int size); // 声明函数

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    int size = sizeof(nums) / sizeof(int);
    reverse(nums, size); // 调用 reverse() 函数,反转数组
    for (int i = 0; i < size; i++) {
        std::cout << nums[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}

void reverse(int* arr, int size) { // 定义函数
    int left = 0;
    int right = size - 1;
    while (left < right) {
        int temp = arr[left];
        arr[left] = arr[right];
        arr[right] = temp;
        left++;
        right--;
    }
}

在上面的示例中,我们定义了一个 reverse() 函数,它接受一个整型指针和数组大小作为参数,用于反转数组。在 main() 函数中,我们定义了一个五元素整型数组 nums,并计算它的大小。然后我们调用 reverse() 函数,将数组的首元素地址作为参数传递给该函数,通过指针对数组进行反转,最后输出反转后的数组。

需要注意的是,在函数参数中 * variable 形式的语句中,* 运算符表示 variable 是一个指向  类型的指针。使用指针可实现数组的各种操作,如遍历、排序、查找、插入等。

7.3.2、将数组作为参数意味着什么

将数组作为函数参数传递意味着函数可以对数组进行操作。在 C++ 中,数组名本身就是一个指向数组首元素的指针。函数可以接受指向数组首元素的指针作为参数,并用指针对数组进行操作。这意味着函数可以修改数组元素的值,也可以对数组进行其他操作,如排序、查找等。

当函数接受数组作为参数时,需要注意以下几点:

  • 数组大小需要作为参数传递给函数,否则函数无法知道数组的大小。
  • 数组作为参数时,可以按值传递、按指针传递或按引用传递。按值传递会复制一份数组,占用更多内存,但不会影响原数组。按指针传递或按引用传递可以节省内存,但会影响原数组。
  • 当使用指针或引用传递数组时,需要注意函数是否具有修改原数组的逻辑,要避免无意中修改了原数组的值。

在数组的操作中,使用指针可以更方便地对数组进行遍历和操作,需要时也可以尝试使用引用传递来降低指针的使用难度。

7.3.3、更多数组函数示例

下面是一些常见的数组函数示例:

求平均值

double avg(int arr[], int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += arr[i];
    }
    return static_cast(sum) / size;
}

在上面的示例中,avg() 函数接受一个整型数组和数组大小作为参数,计算数组元素的平均值并返回。由于整型除以整型会 truncated 后变为整型,因此需要将分子强制转换为 double 类型。

查找元素

int find(int arr[], int size, int target) {
    for (int i = 0; i < size; i++) {
        if (arr[i] == target) {
            return i;
        }
    }
    return -1;
}

在上面的示例中,find() 函数接受一个整型数组、数组大小和目标元素作为参数,查找目标元素在数组中的位置。如果找到目标元素,则返回其下标;否则,返回 -1。

冒泡排序

void bubble_sort(int arr[], int size) {
    for (int i = 0; i < size - 1; i++) {
        for (int j = 0; j < size - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

在上面的示例中,bubble_sort() 函数接受一个整型数组和数组大小作为参数,使用冒泡排序算法对数组进行排序。冒泡排序算法的基本思想是比较相邻的元素,如果它们的顺序不对,则交换它们,重复这个过程直到排序完成。

7.3.4、使用数组区间的函数

使用数组区间的函数是针对一段连续的数组元素进行操作的函数。在 C++ 中,可以使用指向数组首元素和尾元素的指针来表示数组区间。

下面是一个使用数组区间的函数示例,用于对数组进行子区间的反转:

#include 

void reverse(int* begin, int* end); // 声明函数

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    int size = sizeof(nums) / sizeof(int);
    reverse(nums + 1, nums + 4); // 反转 nums[1] 到 nums[3] 的子区间
    for (int i = 0; i < size; i++) {
        std::cout << nums[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}

void reverse(int* begin, int* end) { // 定义函数
    while (begin < end) {
        int temp = *begin;
        *begin = *end;
        *end = temp;
        begin++;
        end--;
    }
}

在上面的示例中,我们定义了一个 reverse() 函数,它接受一个整型指针数组区间作为参数,用于反转数组的子区间。在 main() 函数中,我们定义了一个五元素整型数组 nums,并计算它的大小。然后我们调用 reverse() 函数,将数组指向 nums[1] 和 nums[4] 的指针作为参数传递给该函数,通过指针反转nums[1]到nums[3]的元素,最后输出反转后的数组。

需要注意的是,使用数组区间操作时,需要传递指向数组区间的指针,而不是数组本身。此外,数组区间不一定要从数组的开头或结尾开始,并且数组区间可能为空。

7.3.5、指针和const

指针和 const 是 C++ 语言中非常重要的概念,常常结合使用。

指针是一个指向某种类型(如 int 或 char)变量的地址的变量。声明指针时,需要指定指向的变量类型。用指针可以访问或修改指向变量的值。

const 关键字用于限制变量的值不可修改,也可以用于指向常量的指针或指向常量的引用。声明指向常量的指针时,指针所指向的变量不能更改,但指针本身可以改变指向其他常量的地址。

int x = 1;
const int y = 2;
int *p1 = &x;       // 指向 int 变量的指针
const int *p2 = &y; // 指向 const int 变量的指针
int * const p3 = &x; // 指向 int 变量的 const 指针
const int * const p4 = &y; // 指向 const int 变量的 const 指针

在上面的示例中,声明了四个指针,分别指向 x 和 y 变量,使用 const 关键字将 y 定义为常量。p1 是指向 x 的指针,可以修改 x 的值,而 p2 是指向 y 的指针,因为 y 是常量所以不能修改其值。p3 是一个指向 x 的常指针,即不能通过 p3 修改其所指向的 x 变量地址,但可以改变其所指向的 x 变量的值。p4 是指向常量的常指针,不能修改指针所指向的地址和地址上的值。

7.4、函数和二维数组

函数和二维数组在 C++ 中的使用是很普遍的。二维数组是由多个一维数组组成的,每个一维数组称为一个行,数组中的行按顺序排列形成一个矩阵。

在 C++ 中,可以用指向数组首行的指针来表示一个二维数组。也可以将二维数组看作一个一维数组,其中元素是一维数组。

下面是一些示例展示了如何在函数中使用二维数组:

传递二维数组给函数

#include 

const int rows = 2;
const int cols = 3;

void print_array(int arr[][cols], int rows);

int main() {
    int arr[rows][cols] = {{1,2,3},{4,5,6}};
    print_array(arr, rows);
    return 0;
}

void print_array(int arr[][cols], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            std::cout << arr[i][j] << " ";
        }
        std::cout << std::endl;
    }
}

在上面的示例中,我们定义了一个二维数组 arr,其中包含两个行和三个列。然后定义了 print_array() 函数来打印数组元素,函数接受一个二维数组和行的数量作为参数。在主函数中将 arr 数组和 rows 传递给函数进行打印。

返回二维数组

#include 

const int rows = 2;
const int cols = 3;

int** make_array(int rows, int cols);

int main() {
    int** arr = make_array(rows, cols);
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            std::cout << arr[i][j] << " ";
        }
        std::cout << std::endl;
    }
    // 释放内存
    for (int i = 0; i < rows; i++) {
        delete[] arr[i];
    }
    delete[] arr;
    return 0;
}

int** make_array(int rows, int cols) {
    int** arr = new int*[rows];
    for (int i = 0; i < rows; i++) {
        arr[i] = new int[cols];
        for(int j = 0; j < cols; j++) {
            arr[i][j] = i * cols + j;
        }
    }
    return arr;
}

上面的示例展示了如何在函数中返回一个二维数组。make_array() 函数接受行和列的数量作为参数,创建一个二维数组并返回指向它的指针。然后在主函数中打印二维数组的所有元素,并释放动态分配的内存。

需要注意的是,在使用动态内存分配时,需要在使用完后显式地释放内存,以避免内存泄漏。

7.5、函数和二维数组

函数和二维数组在 C++ 中的使用是很普遍的。二维数组是由多个一维数组组成的,每个一维数组称为一个行,数组中的行按顺序排列形成一个矩阵。

在 C++ 中,可以用指向数组首行的指针来表示一个二维数组。也可以将二维数组看作一个一维数组,其中元素是一维数组。

下面是一些示例展示了如何在函数中使用二维数组:

传递二维数组给函数

#include 

const int rows = 2;
const int cols = 3;

void print_array(int arr[][cols], int rows);

int main() {
    int arr[rows][cols] = {{1,2,3},{4,5,6}};
    print_array(arr, rows);
    return 0;
}

void print_array(int arr[][cols], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            std::cout << arr[i][j] << " ";
        }
        std::cout << std::endl;
    }
}

在上面的示例中,我们定义了一个二维数组 arr,其中包含两个行和三个列。然后定义了 print_array() 函数来打印数组元素,函数接受一个二维数组和行的数量作为参数。在主函数中将 arr 数组和 rows 传递给函数进行打印。

返回二维数组

#include 

const int rows = 2;
const int cols = 3;

int** make_array(int rows, int cols);

int main() {
    int** arr = make_array(rows, cols);
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            std::cout << arr[i][j] << " ";
        }
        std::cout << std::endl;
    }
    // 释放内存
    for (int i = 0; i < rows; i++) {
        delete[] arr[i];
    }
    delete[] arr;
    return 0;
}

int** make_array(int rows, int cols) {
    int** arr = new int*[rows];
    for (int i = 0; i < rows; i++) {
        arr[i] = new int[cols];
        for(int j = 0; j < cols; j++) {
            arr[i][j] = i * cols + j;
        }
    }
    return arr;
}

上面的示例展示了如何在函数中返回一个二维数组。make_array() 函数接受行和列的数量作为参数,创建一个二维数组并返回指向它的指针。然后在主函数中打印二维数组的所有元素,并释放动态分配的内存。

需要注意的是,在使用动态内存分配时,需要在使用完后显式地释放内存,以避免内存泄漏。

7.5.1、将C风格字符串作为参数的函数

在 C++ 中,字符串可以使用不同的类型表示,包括 char 数组,string 类型和字符串指针。

使用 char 数组表示的字符串称为 C 风格字符串,也叫做以 ‘\0’ 结尾的字符串。由于 C 风格字符串是一个字符数组,可以通过指针访问它。

下面是一个使用 C 风格字符串作为参数的函数示例,用于计算字符串的字符数:

#include 

int strlen(const char* str);

int main() {
    const char* str = "Hello, World!";
    int len = strlen(str);
    std::cout << "The length of string \"" << str << "\" is " << len << std::endl;
    return 0;
}

int strlen(const char* str) {
    int len = 0;
    while (*str != '\0') {
        len++;
        str++;
    }
    return len;
}

在上面的示例中,我们定义了一个 strlen() 函数,函数接受一个 C 风格字符串,用于计算字符串的长度,即字符数。 main() 函数中定义了一个字符串指针 str 指向 C 风格字符串 “Hello, World!”,然后将它传递给 strlen() 函数进行计算,并打印出长度。在 strlen() 函数中,我们使用了一个指向字符串 str 的指针,通过遍历字符串并增加计数器来计算字符串的长度。

需要注意的是,在使用 C 风格字符串时,要确保字符串以字符 ‘\0’ 结尾,以防止访问超过数组的边界。此外,C 风格字符串不能随意修改,而且其长度是固定的,在字符串中添加字符或删除字符时需要修改整个字符串。

7.5.2、返回C风格字符串的函数

在 C++ 中,如果需要在函数中返回一个字符串,可以使用 char 数组或字符串指针。如果使用 char 数组,需要保证数组大小足够容纳返回的字符串,而字符串指针需要注意内存泄漏的问题。

下面是一个返回 C 风格字符串的函数示例,用于将字符串中所有字符转为大写,并返回结果:

#include 
#include 
#include  // 大小写转换函数的头文件

char* to_upper(const char* str);

int main() {
    const char* str = "hello, world!";
    char* upper_str = to_upper(str);
    std::cout << "The original string: " << str << std::endl;
    std::cout << "The upper case string: " << upper_str << std::endl;
    delete[] upper_str; // 释放内存
    return 0;
}

char* to_upper(const char* str) {
    int len = strlen(str);
    char* upper_str = new char[len + 1]; // 分配存储单元
    std::strcpy(upper_str, str); // 复制原字符串
    for (int i = 0; i < len; i++) {
        upper_str[i] = std::toupper(upper_str[i]); // 转换为大写
    }
    upper_str[len] = '\0'; // 添加字符串结尾
    return upper_str;
}

在上面的示例中,我们定义了一个 to_upper() 函数,函数接受一个 C 风格字符串,用于将字符串中的所有字符转换为大写,并返回结果字符串。在 main() 函数中,我们定义了一个字符串指针 upper_str 用于存储 to_upper() 函数的返回结果。然后,我们将其打印出来,并在使用完后释放内存。在 to_upper() 函数中,我们首先分配足够存储转换后字符串的空间,并复制原始字符串。然后,我们使用 std::toupper() 函数将字符串中的所有字符转换为大写,并在字符串末尾添加 ‘\0’ 作为字符串结尾。

需要注意的是,在 to_upper() 函数中,我们使用动态分配的内存来存储转换后的字符串,并在返回结果之前添加了字符串的结尾字符。此外,我们在使用完转换后的字符串后需要显式地释放内存,以避免内存泄漏。

7.6、函数和结构

函数和结构体是 C++ 中两个非常重要的概念。函数指代一段完成特定功能的代码,而结构体是一种自定义数据类型,可以包含多个不同数据类型的成员。

在 C++ 中,可以在结构体中定义函数,这被称为结构体内部的函数或成员函数。成员函数可以访问结构体的成员变量,以及调用其它函数。

下面是一个示例,展示了如何在结构体中定义成员函数:

#include 

struct Rectangle {
    int width;
    int height;
    int area() {
        return width * height;
    }
};

int main() {
    Rectangle r = {5, 10};
    std::cout << "Rectangle width: " << r.width << std::endl;
    std::cout << "Rectangle height: " << r.height << std::endl;
    std::cout << "Rectangle area: " << r.area() << std::endl;
    return 0;
}

在上面的示例中,我们定义了一个名为 Rectangle 的结构体,并在其中定义了一个成员函数 area(),用于计算矩形的面积。在 main() 函数中,我们创建了一个名为 r 的结构体变量,设置了其 width 和 height 成员变量,并使用 area() 函数计算了矩形的面积。最后,我们将矩形的属性和面积输出到控制台。

需要注意的是,成员函数的定义方式与普通函数不同,其定义方式为 返回值类型 结构体名::成员函数名() { 函数体 },其中 :: 用于表示函数属于结构体 Rectangle。在成员函数中,可以通过 this 指针来访问当前对象的成员变量,例如 this->width 和 this->height。在调用成员函数时,可以使用点操作符 .,例如 r.area(),其中 r 是结构体变量。

除了成员函数,结构体还可以包括成员变量、构造函数和析构函数等。结构体可以像类一样实现面向对象的编程,并提供更好的封装和抽象能力。

7.6.1、传递和返回结构

在 C++ 中,可以传递结构体作为函数的参数,并返回结构体作为函数的结果。这对于处理复杂的数据结构和对象非常有用。

下面是一个示例,展示了如何传递和返回结构体:

#include 

struct Point {
    int x;
    int y;
};

void printPoint(Point p) {
    std::cout << "Point(" << p.x << ", " << p.y << ")" << std::endl;
}

Point addPoints(Point p1, Point p2) {
    Point result;
    result.x = p1.x + p2.x;
    result.y = p1.y + p2.y;
    return result;
}

int main() {
    Point p1 = {3, 5};
    Point p2 = {7, 2};
    Point p3 = addPoints(p1, p2);
    printPoint(p1);
    printPoint(p2);
    printPoint(p3);
    return 0;
}

在上面的示例中,我们定义了一个结构体 Point,表示二维平面上的点。我们还定义了两个函数,printPoint() 和 addPoints(),用于打印点和将两个点相加。在 main() 函数中,我们创建了两个点 p1 和 p2,并将它们传递给 addPoints() 函数。该函数将这两个点相加,并返回一个新的点 p3。然后,我们在控制台上打印了三个点的坐标。

需要注意的是,在向函数传递结构体时,结构体的成员将被复制到函数的参数中。如果结构体很大,则可能会影响程序的性能。如果需要避免这种情况,可以将结构体作为指针或引用传递。

在返回结构体时,可以将其作为函数的结果返回。在函数中,可以使用 return 语句返回结构体,就像返回任何其他类型的值一样。需要注意的是,在返回结构体时,函数不能返回指向自动变量或局部变量的指针。

7.6.2、另一个处理结构的函数示例

下面是一个示例,展示了如何在函数中操作结构:

#include 

struct Person {
    std::string name;
    int age;
};

void printPerson(Person p) {
    std::cout << "Name: " << p.name << ", Age: " << p.age << std::endl;
}

void increaseAge(Person& p, int amount) {
    p.age += amount;
}

Person createPerson(std::string name, int age) {
    Person p;
    p.name = name;
    p.age = age;
    return p;
}

int main() {
    Person p1 = {"Alice", 25};
    Person p2 = createPerson("Bob", 30);
    printPerson(p1);
    printPerson(p2);
    increaseAge(p1, 5);
    increaseAge(p2, 5);
    printPerson(p1);
    printPerson(p2);
    return 0;
}

在上面的示例中,我们定义了一个结构体 Person,表示一个人。该结构体包括两个成员变量,name 表示姓名,age 表示年龄。我们还定义了三个函数,用于打印人的信息、增加人的年龄和创建新的人结构。

在 main() 函数中,我们创建了两个人结构 p1 和 p2,并使用 printPerson() 函数打印其信息。然后,我们使用 increaseAge() 函数将两个人的年龄增加了 5 岁,并再次使用 printPerson() 函数打印其信息。

在上面的示例中,使用了引用参数,可以避免将结构体复制到函数中。另外,我们也使用了结构体作为函数的返回值,在 createPerson() 函数中,我们创建了一个新的 Person 结构体,并将其作为函数结果返回。

需要注意的是,在定义结构体时,可以使用访问控制符 publicprivate 和 protected 来限制成员变量的访问范围。默认情况下,结构体中的成员变量是公共的。此外,结构体中也可以定义成员函数,用于操作结构体的数据。

7.6.3、传递结构的地址

在一些情况下,当结构体较大或需要修改结构体成员时,可以通过传递结构体的地址来提高程序的效率。这样可以避免将整个结构体复制到函数中,而是传递一个指向结构体的指针或引用。

下面是一个示例,展示了如何传递结构体的地址:

#include 

struct Rectangle {
    int width;
    int height;
};

void printRectangle(Rectangle* r) {
    std::cout << "Width: " << r->width << ", Height: " << r->height << std::endl;
}

void setRectangleSize(Rectangle* r, int w, int h) {
    r->width = w;
    r->height = h;
}

Rectangle* createRectangle(int w, int h) {
    Rectangle* r = new Rectangle;
    r->width = w;
    r->height = h;
    return r;
}

int main() {
    Rectangle* r1 = createRectangle(5, 10);
    Rectangle r2 = {3, 8};
    printRectangle(r1);
    printRectangle(&r2);
    setRectangleSize(r1, 7, 14);
    setRectangleSize(&r2, 4, 10);
    printRectangle(r1);
    printRectangle(&r2);
    delete r1;
    return 0;
}

在上面的示例中,我们定义了一个结构体 Rectangle,表示矩形的宽度和高度。我们还定义了三个函数,用于打印矩形的信息、设置矩形的大小和创建新的矩形结构。

在 main() 函数中,我们使用 createRectangle() 函数创建了一个矩形结构体的指针 r1,并创建了一个矩形结构体 r2。通过传递指针或引用,我们在 printRectangle() 和 setRectangleSize() 函数中操作了这两个矩形结构体。最后,我们使用 delete 关键字释放了 r1 指向的内存空间。

需要注意的是,当使用指针或引用传递结构体时,需要使用 -> 运算符来访问结构体的成员,而不是使用 . 运算符。此外,使用指针或引用时,需要确保指针或引用变量指向的结构体是有效的,否则可能会导致程序崩溃或内存泄漏。

7.7、函数与string对象

在 C++ 中,可以将 string 对象作为函数的参数和返回值。string 类在标准库头文件  中定义,并提供了丰富的字符串操作函数和运算符。

下面是一个示例,展示了如何在函数中操作 string 对象:

#include 
#include 

void printString(std::string s) {
    std::cout << s << std::endl;
}

std::string concatStrings(std::string s1, std::string s2) {
    return s1 + s2;
}

int main() {
    std::string s1 = "Hello";
    std::string s2 = "world!";
    std::string s3 = concatStrings(s1, s2);
    printString(s1);
    printString(s2);
    printString(s3);
    return 0;
}

在上面的示例中,我们定义了两个函数 printString() 和 concatStrings(),分别用于打印字符串和将两个字符串拼接起来。在 main() 函数中,我们创建了两个 string 对象 s1 和 s2,并将它们传递给 concatStrings() 函数,该函数将这两个字符串相加,并返回一个新的字符串 s3。然后,我们在控制台上打印了三个字符串。

需要注意的是,在向函数传递字符串时,可以使用 const 关键字来防止函数修改字符串的内容。例如:

void printString(const std::string& s) {
    std::cout << s << std::endl;
}

这样,函数就不能修改传递进来的字符串。使用 const 引用也可以避免将整个字符串对象复制到函数中,提高程序的效率。

此外,string 类提供了许多与字符串有关的函数和运算符,例如:

  • length():返回字符串的长度。
  • find(str):返回字符串中字符串 str 第一次出现的位置。如果没有找到,返回 npos
  • substr(pos, len):返回从位置pos开始长度为 len 的子字符串。
  • replace(pos, len, str):用字符串 str 替换从位置 pos 开始长度为 len 的子字符串。
  • + 运算符:将两个字符串相连得到一个新的字符串。
  • == 和 != 运算符:比较两个字符串是否相等。

这些函数和运算符使得使用 string 类处理字符串变得非常方便。

7.8、函数与array对象

在 C++ 中,可以将 array 对象作为函数的参数和返回值。array 是一个固定大小的数组容器,定义在 array 头文件  中。

下面是一个示例,展示了如何在函数中操作 array 对象:

#include 
#include 

void printArray(std::array arr) {
    for (int i = 0; i < 5; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

std::array incrementArray(std::array arr) {
    for (int i = 0; i < 5; i++) {
        arr[i]++;
    }
    return arr;
}

int main() {
    std::array arr1 = {1, 2, 3, 4, 5};
    std::array arr2 = incrementArray(arr1);
    printArray(arr1);
    printArray(arr2);
    return 0;
}

在上面的示例中,我们定义了两个函数 printArray() 和 incrementArray(),分别用于打印数组和将数组中的每个元素加 1。在 main() 函数中,我们创建了一个 array 对象 arr1,并将其传递给 incrementArray() 函数,该函数将数组中的每个元素加 1,并返回一个新的 array 对象 arr2。然后,我们在控制台上打印了两个数组。

需要注意的是,在向函数传递 array 时,使用 const 关键字来防止函数修改数组的内容,例如:

void printArray(const std::array& arr) {
    for (int i = 0; i < 5; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

使用 const 引用也可以避免将整个 array 对象复制到函数中,提高程序的效率。

此外,array 还提供了许多与数组有关的函数,例如:

  • size():返回数组的大小。
  • at(i):返回数组中索引为 i 的元素,如果索引越界,将会抛出一个 out_of_range 异常。
  • front() 和 back():分别返回数组的第一个元素和最后一个元素。
  • fill(val):将数组中的所有元素都设置为 val

这些函数使得使用 array 类处理数组变得非常方便。

7.9、递归

递归是一种通过重复将问题分解为更小的子问题而解决问题的方法。递归函数是一种可以调用自身的函数,通常用于解决具有递归结构的问题。递归的核心思想是将一个大问题分解为更小的子问题,直到问题的规模足够小,可以直接求解。

递归函数通常包含两个部分:基本情况和递归情况。基本情况是指递归调用停止的条件,通常是问题的规模足够小,不需要再进行递归调用了,此时函数直接返回结果。递归情况是指在递归调用中需要继续分解问题的情况,通常是对函数的自身调用,将问题的规模缩小。

下面是一个求解斐波那契数列的递归函数的示例:

int fib(int n) {
    if (n == 0 || n == 1) { // 基本情况
        return n;
    } else { // 递归情况
        return fib(n-1) + fib(n-2);
    }
}

在上面的示例中,我们定义了一个求解斐波那契数列的递归函数 fib(),其基本情况是当 n 等于 0 或者 1 时直接返回 n,递归情况是根据斐波那契数列的递推公式 F(n) = F(n-1) + F(n-2) 对自身进行调用,将问题的规模缩小,直到问题的规模足够小,可以直接求解为止。

递归虽然是解决某些问题的有效方法,但也具有一些缺点。递归调用需要占用栈空间,如果递归层数太多,可能会导致栈溢出的错误。此外,递归调用的效率通常比较低,因为函数的调用和返回需要一定的时间开销。

因此,在使用递归解决问题时,需要仔细权衡其优缺点,确保其在解决问题时的效率、可靠性和正确性。

7.9.1、包含一个递归调用的递归

包含一个递归调用的递归函数通常被称为间接递归函数。这种递归函数与直接递归函数的区别在于,间接递归函数的递归调用不是直接调用自身,而是通过调用其他函数来间接调用自身。

一个简单的例子是两个函数 foo() 和 bar() 的互相递归调用:

void foo(int n) {
    if (n > 0) {
        bar(n-1);
        cout << n << " ";
    }
}

void bar(int n) {
    if (n > 0) {
        foo(n-1);
    }
}

在上面的例子中,函数 foo() 和 bar() 分别调用了彼此,形成了间接递归调用的关系。具体地, foo() 函数首先调用 bar(n-1),然后再输出 n;而 bar() 函数则首先调用 foo(n-1),然后不做任何操作。

间接递归函数与直接递归函数类似,都需要考虑基本情况和递归情况。在上面的例子中,基本情况是当 n 小于或等于 0 时结束递归,不再进行调用;而递归情况则是当 n 大于 0 时,先调用另一个函数进行递归,然后再进行当前函数的操作。

7.9.2、包含多个递归调用的递归

一个递归函数包含多个递归调用通常被称为复杂递归或混合递归。在复杂递归中,一个递归函数在递归调用中可能会调用自身多次,同时也会调用其他的递归函数。

以下是一个计算迷宫路径数的例子。在这个例子中,迷宫被表示成一个二维数组,0 表示空格,1 表示障碍物。求从迷宫左上角移动到右下角的路径条数。

const int N = 5;
int maze[N][N] = {{0,1,0,0,0},
                  {0,0,0,1,0},
                  {0,1,0,1,0},
                  {1,1,1,0,0},
                  {0,1,0,0,0}};

int countPaths(int x, int y) {
    if (x < 0 || x >= N || y < 0 || y >= N || maze[x][y] == 1) { // 判断是否越界或遇到障碍
        return 0;
    } else if (x == N-1 && y == N-1) { // 到达终点,返回1
        return 1;
    } else { // 递归情况,向四个方向继续寻找路径
        return countPaths(x+1, y) + countPaths(x-1, y) + countPaths(x, y+1) + countPaths(x, y-1);
    }
}

int main() {
    int paths = countPaths(0, 0);
    std::cout << "Paths: " << paths << std::endl;
    return 0;
}

在上面的例子中,我们定义了一个计算迷宫路径数的递归函数 countPaths()。该函数分别向右、向左、向下、向上四个方向递归调用自身,直到到达终点或遇到障碍物为止。最后,我们在 main() 函数中调用 countPaths() 函数并输出路径数。

需要注意的是,在这个例子中,当 maze[x][y] 等于 1 时表示该位置有障碍,不能通行。因此,递归调用时需要先判断当前位置是否越界或有障碍,否则会产生错误的路径计数。

7.10、函数指针

函数指针是指向函数的指针变量,可以用来存储函数的地址。与普通指针变量不同的是,函数指针指向的是函数,而不是变量。使用函数指针可以使得程序动态地调用不同的函数。

下面是一个简单的示例,展示了如何定义、初始化和使用函数指针:

#include 

// 定义一个函数指针类型
typedef void (*FunctionType)(int);

// 定义两个函数
void printInt(int n) {
    std::cout << "The value is: " << n << std::endl;
}

void printDouble(int n) {
    std::cout << "The value is: " << n * 2 << std::endl;
}

int main() {
    // 定义一个函数指针变量,并将其初始化为 printInt 函数的地址
    FunctionType fptr = &printInt;

    // 调用函数指针指向的函数
    (*fptr)(10);

    // 将函数指针变量的值改为 printDouble 函数的地址
    fptr = &printDouble;

    // 调用函数指针指向的函数
    (*fptr)(10);

    return 0;
}

在上面的示例中,我们首先定义了一个函数指针类型 FunctionType,其指向一个参数为整数类型的返回值为 void 的函数。然后,我们定义了两个不同的函数 printInt() 和 printDouble(),用于打印整数值和其两倍值。接着,在 main() 函数中,我们定义了一个函数指针变量 fptr 并将其初始化为 printInt 函数的地址。然后,我们调用了函数指针所指向的函数 (*fptr)(10)。接下来,我们将 fptr 的值改为 printDouble 函数的地址,并再次调用 (*fptr)(10)

需要注意的是,在使用函数指针时,需要指定函数指针所指向的函数的参数类型和返回值类型。在定义函数指针类型时,使用 typedef 关键字可以使得代码更加简洁易读。另外,可以使用 & 操作符获取函数的地址,并使用 * 操作符调用函数指针。

7.10.1、函数指针的基本知识

函数指针是指向函数的指针变量,指针存储的是该函数的起始地址,函数指针与普通的指针变量类似,都可以进行赋值、比较、运算等操作。函数指针可以用来动态调用函数,使得程序更加灵活和可扩展。

我们可以使用以下语法定义一个函数指针:

// 声明一个函数指针类型
typedef void (*FunctionType)(int arg1, double arg2);

// 声明一个函数,用于接受函数指针作为参数
void callFunction(FunctionType funcPtr, int arg1, double arg2) {
    funcPtr(arg1, arg2); // 调用函数指针指向的函数
}

// 定义一个函数,用于测试函数指针
void printIntDouble(int n, double d) {
    std::cout << "The value of n is " << n << ", and the value of d is " << d << std::endl;
}

int main() {
    // 定义一个函数指针变量
    FunctionType funcPtr = &printIntDouble;

    // 调用函数指针指向的函数
    funcPtr(10, 3.14);

    // 将函数指针传递给另一个函数,用于调用
    callFunction(funcPtr, 20, 2.718);

    return 0;
}

在上面的示例中,我们首先定义了一个函数指针类型 FunctionType,其指向一个参数为整数和双精度浮点数类型的返回值为 void 的函数。然后,我们定义了一个 callFunction 函数,用于接受一个函数指针作为参数并调用该函数指针。接下来,我们定义了一个 printIntDouble 函数,用于测试函数指针。在 main() 函数中,我们首先定义了一个函数指针变量 funcPtr 并将其初始化为 printIntDouble 函数的地址。然后,我们调用了函数指针所指向的函数 funcPtr(10, 3.14)。接着,我们将 funcPtr 传递给 callFunction 函数,并调用该函数进行测试。

需要注意的是,函数指针的类型与指向的函数的类型必须严格匹配,包括参数的类型和顺序、返回值的类型等。可以使用 typedef 关键字来定义函数指针类型,从而使代码更加简洁易读。在调用函数指针时,可以使用 * 操作符来调用指向的函数,也可以省略 * 操作符直接调用函数指针。

7.10.2、函数指针示例

使用函数指针来动态指定排序算法,代码更加灵活、可重用。使用函数指针对整型数组进行排序:

#include 
#include 

// 定义一个函数指针类型,用于比较两个整数
typedef bool (*CompareFunc)(int, int);

// 定义两个比较函数
bool ascending(int a, int b) {
    return a < b;
}

bool descending(int a, int b) {
    return a > b;
}

// 定义一个排序函数,使用函数指针动态指定比较函数
void sortArray(int* arr, int n, CompareFunc cmpFunc) {
    std::sort(arr, arr+n, cmpFunc);
}

int main() {
    int arr[] = {4, 1, 3, 2, 5};

    // 使用 ascending 函数进行升序排序
    sortArray(arr, 5, ascending);

    // 输出排序结果
    for (int i = 0; i < 5; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    // 使用 descending 函数进行降序排序
    sortArray(arr, 5, descending);

    // 输出排序结果
    for (int i = 0; i < 5; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    return 0;
}

在上面的示例中,我们首先定义了一个函数指针类型 CompareFunc,其指向一个参数为整数类型的返回值为 bool 的函数。然后,我们定义了两个比较函数 ascending 和 descending,用于升序和降序排序。接着,我们定义了一个 sortArray 函数,使用函数指针 cmpFunc 动态指定比较函数,从而对整型数组进行排序。最后,在 main() 函数中,我们首先使用 ascending 函数进行升序排序,然后使用 descending 函数进行降序排序,并输出排序结果。

需要注意的是,在使用函数指针时,需要指定函数指针所指向的函数的参数类型和返回值类型。在传递函数指针时,需要将函数名作为参数,并在函数名前加上 & 操作符,从而获取该函数的地址。另外,使用函数指针进行动态排序可以方便地实现各种排序算法的切换和重用。

7.10.3、深入探讨函数指针

函数指针是一个指向函数的指针变量。它可以指向任何匹配函数签名的函数,从而实现灵活的函数调用。在C++中,函数指针的语法比较简单:

返回类型 (* 指针变量名) (参数列表)

其中,指针变量名是可选的,参数列表和返回类型则分别对应函数的参数列表和返回类型。

接下来,我们来深入探讨函数指针的一些特性:

  1. 函数指针可以作为函数的参数传递

函数指针可以作为函数的参数,以便在函数内部调用该函数指针所指向的函数。下面是一个例子:

#include 

void foo(int (*f)(int, int), int a, int b) {
  std::cout << "Result: " << f(a, b) << std::endl;
}

int add(int a, int b) {
  return a + b;
}

int main() {
  foo(add, 2, 3); // output: Result: 5
  return 0;
}

在上面的例子中,我们使用一个函数指针 f 作为 foo 函数的参数。在 foo 函数内部,我们调用 f 指向的函数,并将 a 和 b 作为参数传递给该函数。最后,函数 add 返回结果 5 打印出来。

  1. 函数指针可以作为函数的返回值

函数指针也可以作为函数的返回值,以便在函数外部调用该函数指针所指向的函数。下面是一个例子:

#include 

typedef int (*FuncPtr)(int, int);

FuncPtr bar(bool useAdd) {
  if (useAdd) {
    return add;
  } else {
    return multiply;
  }
}

int add(int a, int b) {
  return a + b;
}

int multiply(int a, int b) {
  return a * b;
}

int main() {
  FuncPtr f = bar(true);
  std::cout << "Result: " << f(2, 3) << std::endl; // output: Result: 5
  
  f = bar(false);
  std::cout << "Result: " << f(2, 3) << std::endl; // output: Result: 6
  
  return 0;
}

在上面的例子中,我们定义了一个函数 bar,该函数的返回值是一个函数指针类型 FuncPtr。通过 useAdd 参数的值,函数 bar 动态返回一个指向 add 或 multiply 函数的指针。在 main 函数中,我们分别调用 bar 函数并获取 FuncPtr 类型的返回值,然后使用该函数指针进行函数调用,并输出结果。

  1. 函数指针也可以使用Lambda表达式

在C++11标准之后,我们可以使用Lambda表达式来定义函数对象。因此,函数指针也可以使用Lambda表达式来替代函数名。下面是一个例子:

#include 

using namespace std;

int operation(int a, int b, function f) {
    return f(a, b);
}

int main() {
    auto add = [](int a, int b){ return a + b; };   // 定义Lambda表达式
    auto multiply = [](int a, int b){ return a * b; };  // 定义Lambda表达式

    int result1 = operation(4, 2, add);           // 调用函数指针add
    int result2 = operation(4, 2, multiply);      // 调用函数指针multiply

    cout << result1 << endl;     // 输出6
    cout << result2 << endl;     // 输出8
    return 0;
}

在上面的例子中,我们首先使用Lambda表达式定义两个函数指针 add 和 multiply。然后,我们调用 operation 函数,并传递不同的函数指针作为参数。在 operation 函数内部,我们调用 f 指向的函数,并将 a 和 b 作为参数传递给该函数。

需要注意的是,在使用Lambda表达式定义函数指针时,需要使用 auto 关键字来自动推导函数指针类型。此外,由于Lambda表达式是一种函数对象,因此它也可以被传递到接受函数对象的函数中,比如 std::transform 等STL算法中。

总结:

函数指针是C++中一种强大的工具,它使得编程更加灵活和可重用。函数指针可以作为函数的参数和返回值,也可以使用Lambda表达式来表示函数对象。需要注意的是,在使用函数指针时,需要指定函数指针所指向的函数的参数类型和返回值类型。

7.10.4、使用typedef进行简化

在C++中,使用 typedef 关键字可以为类型定义一个新的名称。这个新的名称可以让我们更方便地使用这个类型。

对于函数指针类型来说,使用 typedef 可以简化函数指针的语法,让代码更加易读易写。下面是一个例子:

#include 

typedef int (*FuncPtr)(int, int);

int add(int a, int b) {
  return a + b;
}

int subtract(int a, int b) {
  return a - b;
}

int main() {
  FuncPtr f1 = add;
  FuncPtr f2 = subtract;

  std::cout << "Result of add: " << f1(2, 3) << std::endl;
  std::cout << "Result of subtract: " << f2(2, 3) << std::endl;

  return 0;
}

在上面的例子中,我们使用 typedef 关键字定义了一个名为 FuncPtr 的函数指针类型,该类型指向参数为两个整数,返回一个整数的函数。通过定义 FuncPtr 类型,我们可以避免反复写出函数指针的完整语法,并使代码更加简洁和易读。

需要注意的是,使用 typedef 关键字定义函数指针类型时,需要在类型名称和 (*) 之间留出一个空格,以避免产生歧义。此外,我们也可以使用 using 关键字来定义类型别名,其语法类似于 typedef。例如:

#include 

using FuncPtr = int (*)(int, int);

int add(int a, int b) {
  return a + b;
}

int subtract(int a, int b) {
  return a - b;
}

int main() {
  FuncPtr f1 = add;
  FuncPtr f2 = subtract;

  std::cout << "Result of add: " << f1(2, 3) << std::endl;
  std::cout << "Result of subtract: " << f2(2, 3) << std::endl;

  return 0;
}

上面的代码中,我们使用 using 关键字定义了一个名为 FuncPtr 的函数指针类型,该类型指向参数为两个整数,返回一个整数的函数。在使用 FuncPtr 类型时,我们可以直接使用别名而不需要在类型名称前添加 typedef 关键字。

你可能感兴趣的:(C++,c++,学习,笔记)