第五章:函数 (Funktionen)
1. 课程概览
- 函数的定义与调用
- 参数传递方式:值传递、指针传递、引用传递
- 数组作为参数
vector作为参数- 返回引用
- 指针与引用的选择
2. 函数的定义与调用
- 目的:将代码分解为可重用的模块,避免重复,提高代码可读性和可维护性。
- 语法:
cpp Rückgabetyp Funktionsname(Parameterliste) { // 函数体 // ... return Wert; // 如果返回类型不是 void } - 示例:
```cpp // 定义一个计算平方的函数 int square(int x) { return x * x; }
int main() { int a = 3; cout << square(a) << endl; // 调用函数,输出 9 return 0; } ```
3. 参数传递方式
3.1 值传递 (Übergabe eines Wertes)
- 机制:将实参的副本传递给函数。函数内部对参数的修改不会影响原始变量。
- 特点:安全,但大对象的复制效率低。
- 示例:
```cpp void tausche_wert(int a, int b) { int h = a; a = b; b = h; cout << "Fkt.:" << a << " " << b << endl; // 输出: Fkt.: 11 3 }
int main() { int a = 3, b = 11; cout << "main:" << a << " " << b << endl; // 输出: main: 3 11 tausche_wert(a, b); cout << "main:" << a << " " << b << endl; // 输出: main: 3 11 (未改变) } ```
- 结论:此方法无法交换
main函数中的a和b。
3.2 指针传递 (Übergabe eines Zeigers)
- 机制:将变量的地址(指针)传递给函数。函数通过解引用
*操作符来访问和修改原始变量。 - 特点:可以修改原始数据,效率高(只传递地址)。
- 示例:
```cpp void tausche_ptr(int pa, int pb) { int h = pa; pa = pb; pb = h; cout << "Fkt.:" << pa << " " << pb << endl; // 输出: Fkt.: 11 3 }
int main() { int a = 3, b = 11; cout << "main:" << a << " " << b << endl; // 输出: main: 3 11 tausche_ptr(&a, &b); // 传递 a 和 b 的地址 cout << "main:" << a << " " << b << endl; // 输出: main: 11 3 (已交换) } ```
- 结论:通过传递指针,成功交换了
main函数中的a和b。
3.3 引用传递 (Übergabe einer Referenz)
- 机制:将变量的别名(引用)传递给函数。函数内部对参数的操作直接作用于原始变量。
- 特点:语法简洁,避免了指针的复杂性,可以修改原始数据。
- 示例:
```cpp void tausche_ref(int& a, int& b) { int h = a; a = b; b = h; cout << "Fkt.:" << a << " " << b << endl; // 输出: Fkt.: 11 3 }
int main() { int a = 3, b = 11; cout << "main:" << a << " " << b << endl; // 输出: main: 3 11 tausche_ref(a, b); // 传递变量本身,函数参数是其别名 cout << "main:" << a << " " << b << endl; // 输出: main: 11 3 (已交换) } ```
- 结论:引用传递同样能成功交换变量,且调用语法更直观。
4. 数组作为函数参数
- 核心事实:数组名在作为参数传递时,会自动退化为指向其首元素的指针。
- 问题:函数内部无法知道数组的实际大小,容易导致越界访问。
4.1 问题示例
void inkrement(int b[5]) { // [5] 是误导性的!编译器会忽略
for(int k = 0; k < 5; k++)
b[k]++;
cout << "sizeof(b):" << sizeof(b) << endl; // 输出: 8 (指针大小,非数组大小)
}
int main(){
int a[5] = {2, 4, 6, 8, 10};
cout << "sizeof(a):" << sizeof(a) << endl; // 输出: 20 (数组实际大小)
inkrement(a); // a 被当作指针传递
for(int i = 0; i < 5; i++)
cout << a[i] << endl; // 输出: 3 5 7 9 11 (a 被修改了)
}
sizeof(b)返回的是指针的大小(64 位系统上通常为 8 字节),而不是数组的大小。
4.2 正确做法:传递大小
- 建议:将数组的大小作为额外的参数传递给函数。
- 代码:
```cpp void inkrement(int b[], int const n) { // 或 int* b for(int k = 0; k < n; k++) b[k]++; }
int main(){ int constexpr n = 5; // 使用 constexpr 确保大小在编译时确定 int a[n] = {2, 4, 6, 8, 10}; inkrement(a, n); // 传递数组和大小 for(int i = 0; i < n; i++) cout << a[i] << endl; // 输出: 3 5 7 9 11 } ```
- 优点:避免了硬编码的数组大小,减少了越界错误的风险。
4.3 二维数组作为参数
- 规则:传递二维数组时,列数必须作为常量指定。
- 原因:编译器需要列数来计算元素的地址。
- 示例:
```cpp void inkrement(int const zeilen, int b[][3]) { // 列数 3 必须指定 for(int i = 0; i < zeilen; i++) for(int j = 0; j < 3; j++) b[i][j]++; }
int main(){ int a[][3] = {{2, 4, 6}, {9, 7, 5}}; inkrement(2, a); // 传递行数和数组 // 输出: 3 5 7 // 10 8 6 } ```
5. vector 作为函数参数
- 问题:如果直接传递
vector,会复制整个容器,效率低下且无法修改原vector。 - 解决方案:使用引用传递。
- 示例:
```cpp // 错误:值传递,复制整个 vector void plusEinsAusgabe(vector v) { for(int i = 0; i < v.size(); ++i) { ++v[i]; // 修改的是副本 cout << v[i] << " "; } cout << endl; } // v 在此处被销毁 // 输出: 2 3 4 5 6 // 原 vector 未变
// 正确:引用传递,可修改原 vector void plusEinsAusgabe(vector& v) { for(int i = 0; i < v.size(); ++i) { ++v[i]; // 直接修改原 vector cout << v[i] << " "; } cout << endl; } // 输出: 2 3 4 5 6 // 原 vector 被修改为 {2,3,4,5,6}
// 最佳:常量引用传递,不修改原 vector void ausgabe(const vector& v) { for(int i : v) { cout << i << " "; // 可读,不可写 } cout << endl; } ```
6. 返回引用 (Rückgabe einer Referenz)
- 用途:实现链式调用,或需要返回一个可修改的对象。
- 示例:
```cpp struct Konto { char name[20]; double wert; };
// 返回更富有账户的引用 Konto& reicher(Konto& k1, Konto& k2) { if(k1.wert > k2.wert) return k1; return k2; }
int main(){ Konto Anton{"Anton", 100}; Konto Berta{"Berta", 200}; cout << reicher(Anton, Berta).name << " hat mehr Geld" << endl; // 输出: Berta hat mehr Geld } ```
- 严重警告:绝对不要返回局部变量的引用!
cpp Konto& verdoppeln(Konto& k) { Konto lokal = k; // 局部变量 lokal.wert *= 2; return lokal; // 错误!返回对已销毁对象的引用 // 导致未定义行为! } lokal在函数结束时被销毁,返回的引用指向无效内存。
7. 指针 vs. 引用
| 特性 | 指针 (Zeiger) | 引用 (Referenz) |
|---|---|---|
| 初始化 | 可以先声明后赋值 | 必须在声明时初始化 |
| 重新赋值 | 可以改变指向(pa = &y;) |
一旦绑定,不能改变绑定的对象 |
| 层级 | 可以有指针的指针(int**) |
只有一层,不能有“引用的引用” |
| 空值 | 可以为 nullptr |
必须绑定到一个有效的对象 |
| 本质 | 存储地址的变量 | 变量的别名 |
建议:尽可能使用引用。当无法使用引用时(如需要 nullptr 或重新赋值),再使用指针。
示例对比:
// 使用引用
void square(int& x) { x = x * x; }
// 使用指针
void square(int* px) { *px = *px * *px; }
// 引用更简洁安全
8. 附录:确定性有限自动机 (DEA)
- 目的:设计一个自动机来检查不带括号的正整数算术表达式(如
3+1-)的正确性。 - 状态:
S = {-1, 0, 1, 2} - 起始状态:
s0 = 0 - 接受状态:
F = {1, 2} - 输入字母表:
Σ = {Z, 0, op} Z = {1,2,3,4,5,6,7,8,9}op = {+, -}-
转移函数
δ: | δ | Z | op | 0 | |---|---|---|---| | -1 | -1 | -1 | -1 | -1 | | 0 | 1 | -1 | 2 | | 1 | 1 | 0 | 1 | | 2 | -1 | 0 | -1 | -
示例:
30-0 - 起始状态
0 - 读
3(Z) → 状态1 - 读
0(0) → 状态1 - 读
-(op) → 状态0 - 读
0(0) → 状态2 - 状态
2属于F→ 接受该字符串。
—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com