Skip to content

C++课程 6:深入理解字符串、数组、指针与函数

本节课我们将深入探讨 C++ 中几个至关重要的概念:字符串、数组、指针以及函数。这些是 C++ 编程的基石,理解它们对于编写高效、安全的代码至关重要。

一、深入理解字符串常量

在 C++ 中,字符串常量是由双引号 " 括起来的一系列字符。虽然看起来简单,但其背后涉及到内存管理和字符编码等概念。

  • 字符与字符串的本质:

    • 在 C++ 中,单个字符用单引号 ' 包裹,例如 'A'
    • 字符串是由多个字符组成的序列,以空字符 \0 结尾。 \0 被称为尾零空终止符,它是一个特殊的非打印字符,用于标记字符串的结束位置。这是 C 风格字符串的约定。
  • '\0'' ' 的区别:

    • '\0' (尾零): 是一个 ASCII 码为 0 的字符。它不是字符串内容的一部分,而是作为字符串结束的标记。当我们使用 strlen 等函数计算字符串长度时,不会将 \0 计算在内。
    • ' ' (空格字符): 是一个普通的字符,其 ASCII 码为 32。空格是字符串内容的一部分,会被 strlen 等函数计算在内。
  • 空字符串 "": 表示一个不包含任何字符的字符串,但它仍然包含一个尾零 \0。因此,空字符串的长度为 0。

  • 空数组的意义: 虽然可以定义一个空的数组,例如 char empty_arr[0];,但在实际应用中很少见,因为它无法存储任何元素。它可能在某些特定的底层编程或模板编程中会用到。

  • 数组名与指针的紧密联系:

    • 数组名作为常量指针: 在大多数情况下,数组名可以隐式转换为指向数组第一个元素的常量指针。这意味着你不能修改数组名指向的地址,但可以通过数组名来访问和修改数组元素的值。
    • 赋值给指针变量: 可以将数组名赋值给相同数据类型的指针变量,这样指针就指向了数组的起始位置。

示例代码及详细解释:

cpp
#include <iostream>
#include <cstring> // 包含 strlen 函数的头文件

int main() {
  char str1[] = "Hello"; // 编译器会自动在 "Hello" 后面添加 '\0'
  char str2[] = "";      // 空字符串,实际上是 {'\0'}
  char str3[10];        // 定义一个可以存储 9 个字符的字符数组,留一个位置给 '\0'
  char* p_str = str1;   // 指针 p_str 指向 str1 的第一个元素 'H'

  std::cout << "字符串 str1 的内容: " << str1 << std::endl;
  std::cout << "字符串 str1 的长度 (不包含 '\\0'): " << strlen(str1) << std::endl; // 输出 5
  std::cout << "str1[0] 的地址: " << static_cast<void*>(&str1[0]) << std::endl; // 强制转换为 void* 以打印地址
  std::cout << "p_str 指向的地址: " << static_cast<void*>(p_str) << std::endl;    // 两者地址相同

  // 注意:直接打印字符数组或字符指针,会输出整个字符串直到 '\0'
  std::cout << "str2 的内容: " << str2 << std::endl;
  std::cout << "字符串 str2 的长度: " << strlen(str2) << std::endl; // 输出 0

  return 0;
}

关键点总结:

  • 字符串常量以 \0 结尾。
  • 空字符串 "" 包含一个 \0
  • 数组名在很多情况下可以视为指向数组首元素的常量指针。

二、深入理解二维数组与行指针

二维数组可以看作是数组的数组,理解其内存布局和访问方式至关重要。

  • 二维数组的内存模型:

    • 在内存中,二维数组的元素是连续存储的,按行优先的方式排列。例如,int matrix[2][3] 在内存中会依次存储 matrix[0][0], matrix[0][1], matrix[0][2], matrix[1][0], matrix[1][1], matrix[1][2]
  • 行指针的概念:

    • 二维数组的数组名 (matrix) 在表达式中使用时,会decay(退化)成指向其第一个(也就是第一个一维数组)的指针。这个指针的类型是“指向包含 列数元素类型 的数组”的指针。在 int matrix[2][3] 的例子中,matrix 的类型可以看作是指向 int [3] 的指针。
    • 这种指向一维数组的指针被称为行指针数组指针
  • 指针数组 vs. 数组指针:

    • 指针数组: 是一个数组,其元素都是指针。例如:int *ptr_arr[5]; 表示一个包含 5 个 int* 类型元素的数组,每个元素可以指向一个整数。
    • 数组指针: 是一个指针,它指向一个数组。例如:int (*arr_ptr)[3]; 表示一个指向包含 3 个整数的数组的指针。注意括号 () 的必要性,它决定了运算的优先级。

示例代码及详细解释:

cpp
#include <iostream>

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

  // matrix 是一个指向包含 3 个 int 的数组的指针 (行指针)
  int (*p_row)[3] = matrix;

  std::cout << "二维数组 matrix 的首地址: " << static_cast<void*>(matrix) << std::endl;
  std::cout << "行指针 p_row 的值 (指向第一行): " << static_cast<void*>(p_row) << std::endl; // 与 matrix 地址相同
  std::cout << "matrix[0] 的地址: " << static_cast<void*>(matrix[0]) << std::endl; // 第一行的首地址,与 matrix 相同

  // p_row + 1  移动到下一行(移动了 3 个 int 的大小)
  std::cout << "p_row + 1 的值 (指向第二行): " << static_cast<void*>(p_row + 1) << std::endl;
  std::cout << "matrix[1] 的地址: " << static_cast<void*>(matrix[1]) << std::endl; // 第二行的首地址

  // 使用行指针访问二维数组元素
  // *(p_row + 1)  解引用行指针,得到第二行的首地址,类型是 int*
  // *(p_row + 1) + 2  在第二行的首地址上偏移 2 个 int 的位置,指向 matrix[1][2]
  // *(*(p_row + 1) + 2)  解引用,得到 matrix[1][2] 的值
  std::cout << "matrix[1][2] 的值为: " << *(*(p_row + 1) + 2) << std::endl;

  return 0;
}

关键点总结:

  • 二维数组在内存中按行优先存储。
  • 数组名可以退化为指向第一行的行指针。
  • 理解指针数组和数组指针的区别。

三、函数与指针的深度应用

指针在函数中扮演着重要的角色,可以用于传递数据、返回结果,甚至传递函数本身。

  • 指针函数:返回指针的函数
    • 指针函数的返回值是一个指针类型。这使得函数可以返回动态分配的内存地址,或者指向在函数外部定义的变量的地址。
    • 需要注意内存管理,避免返回指向局部变量的指针,因为局部变量在函数执行结束后会被销毁。

示例代码:

cpp
#include <iostream>

// 指针函数:返回动态分配的 int 数组
int* createArray(int size) {
  int* arr = new int[size];
  for (int i = 0; i < size; ++i) {
    arr[i] = i * 2;
  }
  return arr; // 返回指向动态分配内存的指针
}

int main() {
  int* myArray = createArray(5);
  if (myArray != nullptr) {
    std::cout << "动态数组的元素: ";
    for (int i = 0; i < 5; ++i) {
      std::cout << myArray[i] << " ";
    }
    std::cout << std::endl;
    delete[] myArray; // 记得释放动态分配的内存
    myArray = nullptr;
  }
  return 0;
}
  • 函数指针:指向函数的指针
    • 函数指针存储的是函数的入口地址。通过函数指针,我们可以间接地调用函数,可以将函数作为参数传递给其他函数,或者存储在一组函数列表中。
    • 函数指针的声明需要指定函数的返回类型和参数列表。

示例代码及详细解释:

cpp
#include <iostream>

// 一个简单的加法函数
int add(int a, int b) {
  return a + b;
}

// 一个使用函数指针作为参数的函数
void executeOperation(int a, int b, int (*operation)(int, int)) {
  std::cout << "执行结果: " << operation(a, b) << std::endl;
}

int main() {
  // 声明一个指向返回 int,接受两个 int 参数的函数的指针
  int (*funcPtr)(int, int);

  // 将 add 函数的地址赋值给 funcPtr
  funcPtr = add;

  // 通过函数指针调用 add 函数
  int result = funcPtr(5, 3);
  std::cout << "通过函数指针调用 add: " << result << std::endl;

  // 将 add 函数作为参数传递给 executeOperation 函数
  executeOperation(10, 5, add);

  return 0;
}

关键点总结:

  • 指针函数返回指针,需要注意内存管理。
  • 函数指针可以存储函数的地址,实现间接调用和函数作为参数传递。

四、函数的重载:提高代码的灵活性

函数重载允许在同一个作用域内定义多个同名函数,只要它们的参数列表不同(参数的类型、数量或顺序不同)。

  • 重载的原理: 编译器会根据函数调用时提供的参数类型和数量,自动匹配最合适的重载版本。
  • 重载的优势: 提高了代码的可读性和可维护性,可以使用相同的函数名执行相似但针对不同数据类型的操作。

示例代码及详细解释:

cpp
#include <iostream>

// 重载的 sum 函数,处理两个整数
int sum(int a, int b) {
  std::cout << "调用 sum(int, int)" << std::endl;
  return a + b;
}

// 重载的 sum 函数,处理三个整数
int sum(int a, int b, int c) {
  std::cout << "调用 sum(int, int, int)" << std::endl;
  return a + b + c;
}

// 重载的 sum 函数,处理两个 double 类型
double sum(double a, double b) {
  std::cout << "调用 sum(double, double)" << std::endl;
  return a + b;
}

int main() {
  std::cout << "sum(2, 3) = " << sum(2, 3) << std::endl;      // 调用第一个 sum 函数
  std::cout << "sum(2, 3, 4) = " << sum(2, 3, 4) << std::endl; // 调用第二个 sum 函数
  std::cout << "sum(2.5, 3.5) = " << sum(2.5, 3.5) << std::endl; // 调用第三个 sum 函数
  return 0;
}

关键点总结:

  • 函数重载通过不同的参数列表区分同名函数。
  • 编译器根据参数匹配调用相应的重载版本。

五、函数的默认参数值:简化函数调用

C++ 允许在函数声明时为参数指定默认值。当调用函数时,如果没有为带有默认值的参数提供实参,则会使用默认值。

  • 默认参数的规则:
    • 默认参数必须从参数列表的右侧开始定义。也就是说,如果一个参数有默认值,那么它右边的所有参数都必须有默认值。
    • 默认参数值在函数声明中指定,而不是在函数定义中(虽然在定义中也可以指定,但不推荐)。

示例代码及详细解释:

cpp
#include <iostream>

// 计算矩形面积,默认宽度为 1
int calculateArea(int length, int width = 1) {
  std::cout << "计算长度为 " << length << ", 宽度为 " << width << " 的矩形面积" << std::endl;
  return length * width;
}

// 打印信息,默认打印次数为 1
void printMessage(const std::string& message, int times = 1) {
  for (int i = 0; i < times; ++i) {
    std::cout << message << std::endl;
  }
}

int main() {
  std::cout << "面积1: " << calculateArea(5) << std::endl;      // 使用默认宽度 1
  std::cout << "面积2: " << calculateArea(5,```markdown
  std::cout << "面积2: " << calculateArea(5, 3) << std::endl;    // 传递了宽度参数

  printMessage("Hello");                             // 使用默认打印次数 1
  printMessage("World", 3);                         // 传递了打印次数

  return 0;
}

关键点总结:

  • 默认参数在函数声明时指定。
  • 调用函数时可以省略有默认值的参数。
  • 默认参数必须从右向左定义。

六、内联函数:提升程序性能

内联函数是一种编译器优化技术,用于减少函数调用的开销。

  • 内联的原理: 当编译器遇到内联函数的调用时,会尝试将函数体的代码直接插入到调用处,而不是进行实际的函数调用过程(压栈、跳转、返回等)。这类似于宏展开,但内联函数是类型安全的。
  • inline 关键字: 使用 inline 关键字建议编译器将函数内联,但这只是一个请求,编译器可以选择忽略。通常,编译器会考虑函数的复杂度和大小来决定是否内联。
  • 适用场景:
    • 短小、频繁调用的函数是内联的理想选择,例如简单的 getter/setter 方法、小的计算函数等。
    • 不适合内联的情况包括包含循环、递归、复杂控制流的函数,以及函数体过于庞大的函数。过度内联可能导致代码膨胀,反而降低性能。

示例代码及详细解释:

cpp
#include <iostream>

// 内联函数:计算两个整数的最大值
inline int max(int a, int b) {
  return a > b ? a : b;
}

// 内联函数:计算平方
inline double square(double x) {
  return x * x;
}

int main() {
  int x = 10, y = 5;
  // 编译器可能会将 max(x, y) 的代码直接替换到这里
  std::cout << "最大值: " << max(x, y) << std::endl;

  double num = 3.5;
  // 编译器可能会将 square(num) 的代码直接替换到这里
  std::cout << "平方: " << square(num) << std::endl;

  return 0;
}

内联函数的注意事项:

  • 声明和定义: 通常,内联函数的声明和定义应该放在同一个头文件中,以便编译器在编译调用处时能够看到函数体。
  • inline 的建议性: inline 只是对编译器的建议,编译器最终决定是否内联。
  • 调试难度: 内联函数可能使调试更加困难,因为代码在编译后被展开,单步调试时可能看不到独立的函数调用。

总结:

本节课我们深入学习了 C++ 中关于字符串常量、二维数组与行指针、函数与指针的应用、函数的重载、默认参数值以及内联函数等重要概念。这些知识是构建复杂 C++ 程序的基石,理解和熟练运用它们对于编写高效、可维护的代码至关重要。在接下来的学习中,我们将继续探索更多 C++ 的强大特性。

作业:

请根据 main 函数中的测试代码实现sum, product, calculate 三个函数:

cpp
#include <stdio.h>

// 在这里补充三个函数的实现

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

  // 计算数组元素的和
  int sum_result = calculate(arr, len, sum);
  printf("Sum: %d\n", sum_result);

  // 计算数组元素的积
  int product_result = calculate(arr, len, product);
  printf("Product: %d\n", product_result);

  return 0;
}

参考实现如下:

c++
// 计算数组元素的和
int sum(int arr[], int len) {
  int total = 0;
  for (int i = 0; i < len; i++) {
    total += arr[i];
  }
  return total;
}

// 计算数组元素的积
int product(int arr[], int len) {
  int result = 1;
  for (int i = 0; i < len; i++) {
    result *= arr[i];
  }
  return result;
}

// 使用函数指针进行计算
int calculate(int arr[], int len, int (*func)(int *, int)) {
  return func(arr, len);
}