C++课程 6:深入理解字符串、数组、指针与函数
本节课我们将深入探讨 C++ 中几个至关重要的概念:字符串、数组、指针以及函数。这些是 C++ 编程的基石,理解它们对于编写高效、安全的代码至关重要。
一、深入理解字符串常量
在 C++ 中,字符串常量是由双引号 "
括起来的一系列字符。虽然看起来简单,但其背后涉及到内存管理和字符编码等概念。
字符与字符串的本质:
- 在 C++ 中,单个字符用单引号
'
包裹,例如'A'
。 - 字符串是由多个字符组成的序列,以空字符
\0
结尾。\0
被称为尾零或空终止符,它是一个特殊的非打印字符,用于标记字符串的结束位置。这是 C 风格字符串的约定。
- 在 C++ 中,单个字符用单引号
'\0'
和' '
的区别:'\0'
(尾零): 是一个 ASCII 码为 0 的字符。它不是字符串内容的一部分,而是作为字符串结束的标记。当我们使用strlen
等函数计算字符串长度时,不会将\0
计算在内。' '
(空格字符): 是一个普通的字符,其 ASCII 码为 32。空格是字符串内容的一部分,会被strlen
等函数计算在内。
空字符串
""
: 表示一个不包含任何字符的字符串,但它仍然包含一个尾零\0
。因此,空字符串的长度为 0。空数组的意义: 虽然可以定义一个空的数组,例如
char empty_arr[0];
,但在实际应用中很少见,因为它无法存储任何元素。它可能在某些特定的底层编程或模板编程中会用到。数组名与指针的紧密联系:
- 数组名作为常量指针: 在大多数情况下,数组名可以隐式转换为指向数组第一个元素的常量指针。这意味着你不能修改数组名指向的地址,但可以通过数组名来访问和修改数组元素的值。
- 赋值给指针变量: 可以将数组名赋值给相同数据类型的指针变量,这样指针就指向了数组的起始位置。
示例代码及详细解释:
#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 个整数的数组的指针。注意括号()
的必要性,它决定了运算的优先级。
- 指针数组: 是一个数组,其元素都是指针。例如:
示例代码及详细解释:
#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;
}
关键点总结:
- 二维数组在内存中按行优先存储。
- 数组名可以退化为指向第一行的行指针。
- 理解指针数组和数组指针的区别。
三、函数与指针的深度应用
指针在函数中扮演着重要的角色,可以用于传递数据、返回结果,甚至传递函数本身。
- 指针函数:返回指针的函数
- 指针函数的返回值是一个指针类型。这使得函数可以返回动态分配的内存地址,或者指向在函数外部定义的变量的地址。
- 需要注意内存管理,避免返回指向局部变量的指针,因为局部变量在函数执行结束后会被销毁。
示例代码:
#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;
}
- 函数指针:指向函数的指针
- 函数指针存储的是函数的入口地址。通过函数指针,我们可以间接地调用函数,可以将函数作为参数传递给其他函数,或者存储在一组函数列表中。
- 函数指针的声明需要指定函数的返回类型和参数列表。
示例代码及详细解释:
#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;
}
关键点总结:
- 指针函数返回指针,需要注意内存管理。
- 函数指针可以存储函数的地址,实现间接调用和函数作为参数传递。
四、函数的重载:提高代码的灵活性
函数重载允许在同一个作用域内定义多个同名函数,只要它们的参数列表不同(参数的类型、数量或顺序不同)。
- 重载的原理: 编译器会根据函数调用时提供的参数类型和数量,自动匹配最合适的重载版本。
- 重载的优势: 提高了代码的可读性和可维护性,可以使用相同的函数名执行相似但针对不同数据类型的操作。
示例代码及详细解释:
#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++ 允许在函数声明时为参数指定默认值。当调用函数时,如果没有为带有默认值的参数提供实参,则会使用默认值。
- 默认参数的规则:
- 默认参数必须从参数列表的右侧开始定义。也就是说,如果一个参数有默认值,那么它右边的所有参数都必须有默认值。
- 默认参数值在函数声明中指定,而不是在函数定义中(虽然在定义中也可以指定,但不推荐)。
示例代码及详细解释:
#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 方法、小的计算函数等。
- 不适合内联的情况包括包含循环、递归、复杂控制流的函数,以及函数体过于庞大的函数。过度内联可能导致代码膨胀,反而降低性能。
示例代码及详细解释:
#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 三个函数:
#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;
}
参考实现如下:
// 计算数组元素的和
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);
}