上一课标准答案:
result = 7- 返回值类型是
double;返回值是45.5 result = false
double error = calculateError(80.0, 50.0);
表示调用 calculateError 函数,把 80.0 传给目标值、50.0 传给当前值,函数计算后返回 30.0,最后把这个结果存进 error。
第 12 课:局部变量、作用域、变量生命周期
这一课非常重要。
因为以后你学:
函数
指针
引用
类
栈
动态内存
ROS2
都会碰到一个问题:
这个变量到底在哪里能用?什么时候还存在?
先别急着记专业名词,我们从直觉开始。
1. 先看一个奇怪的问题
void test()
{
int a = 5;
}
int main()
{
std::cout << a << std::endl;
return 0;
}
你可能觉得:
前面不是已经创建
a = 5了吗?
为什么 main() 里不能直接用?
因为这个:
int a = 5;
是在 test() 的花括号里面创建的。
它只属于:
test()
这块区域。
2. 用"房间"理解作用域
把函数想象成不同房间。
┌───────────────┐
│ test 房间 │
│ │
│ a = 5 │
│ │
└───────────────┘
┌───────────────┐
│ main 房间 │
│ │
│ │
└───────────────┘
a 放在 test 房间里。
所以:
test房间里的人能看到a
但是:
main房间里看不到a
3. 这就是"作用域"
专业一点的名字叫:
scope,作用域
意思就是:
一个变量可以被访问的代码范围。
例如:
void test()
{
int a = 5;
std::cout << a << std::endl;
}
这里可以使用:
a
因为还在:
{
...
}
这个范围里面。
4. 出了花括号,就不行了
void test()
{
int a = 5;
}
std::cout << a << std::endl;
这里:
a
已经离开它的作用范围。
所以程序会报错。
你可以先记:
在
{}里面创建的普通局部变量,一般只能在这对{}里面使用。
5. 什么叫局部变量?
例如:
void test()
{
int a = 5;
}
这里的:
a
叫:
局部变量 local variable
"局部"就是:
只属于某个局部范围。
6. main() 里的变量也是局部变量
例如:
int main()
{
int age = 20;
return 0;
}
age 也是局部变量。
它的作用域大致就是:
{
int age = 20;
return 0;
}
这一块。
不是只有普通函数里的变量才叫局部变量。
7. 不同函数可以有同名变量
看:
void test1()
{
int a = 5;
}
void test2()
{
int a = 10;
}
这样是可以的。
为什么?
因为它们在不同的"房间"。
test1 房间
a = 5
test2 房间
a = 10
它们不是同一个变量。
8. 这点特别重要
看:
void functionA()
{
int angle = 30;
}
void functionB()
{
int angle = 60;
}
虽然都叫:
angle
但实际上是两个不同的变量。
就像:
张三家有一个"杯子"
李四家也有一个"杯子"
名字一样,不代表是同一个东西。
9. 参数也是局部变量
还记得:
double calculateError(double target, double current)
{
return target - current;
}
这里:
target
current
其实也只在:
calculateError
函数内部有效。
它们也属于局部变量。
例如:
int main()
{
std::cout << target << std::endl;
}
这里不能直接使用 target。
因为:
target
属于 calculateError()。
10. 调用函数时会发生什么?
例如:
double calculateError(double target, double current)
{
double error = target - current;
return error;
}
然后:
calculateError(50.0, 20.0);
可以粗略想象成:
进入 calculateError 房间
target = 50
current = 20
error = 30
return 30
离开房间
函数结束后:
target
current
error
这些局部变量通常就结束它们的生命周期。
11. 什么叫"生命周期"?
作用域和生命周期不是完全一回事,但初学阶段先这样理解:
作用域:
在哪里能使用它?
生命周期:
它什么时候存在?
例如:
void test()
{
int a = 5;
}
调用:
test();
可以粗略理解为:
进入 test
↓
创建 a
↓
a = 5
↓
test 执行完
↓
a 的这次生命结束
12. 再次调用 test() 呢?
void test()
{
int a = 5;
a++;
std::cout << a << std::endl;
}
int main()
{
test();
test();
return 0;
}
第一次调用:
创建新的 a
a = 5
a++
a = 6
输出 6
函数结束
第二次调用:
重新创建一个新的 a
a = 5
a++
a = 6
输出 6
所以最终:
6
6
不是:
6
7
这一点非常重要。
13. 为什么不是 7?
因为普通局部变量:
int a = 5;
每次进入函数时都会重新创建。
第一次函数结束以后,那次的 a 已经结束了。
第二次:
又是一个新的
a
所以重新从:
5
开始。
14. if 里面也有自己的作用域
看:
int main()
{
if (true)
{
int a = 5;
std::cout << a << std::endl;
}
return 0;
}
这里:
a
只属于:
if
{
...
}
这一块。
所以:
int main()
{
if (true)
{
int a = 5;
}
std::cout << a << std::endl;
}
这里通常会报错。
因为已经出了 a 的作用域。
15. for 循环里的 i 呢?
你之前经常写:
for (int i = 0; i < 5; i++)
{
std::cout << i << std::endl;
}
这个:
i
一般只属于这个 for 循环。
所以:
for (int i = 0; i < 5; i++)
{
}
结束以后,再写:
std::cout << i << std::endl;
通常就不能用了。
16. 为什么这样设计?
因为如果所有变量在整个程序里都一直存在、到处都能用:
a
b
angle
speed
state
count
error
程序稍微复杂一点就会乱成一团。
函数内部自己管理自己的变量,反而更安全。
比如:
double calculateError(...)
{
double error;
}
另一个函数也可以有:
void checkMotor()
{
int error;
}
互相不冲突。
17. 机器人程序里为什么尤其重要?
假设以后你写:
double calculateJoint1Error()
{
double error = 10.0;
return error;
}
另一个函数:
double calculateJoint2Error()
{
double error = 20.0;
return error;
}
这里两个:
error
完全可以同时存在。
因为:
Joint1 函数里的 error
和:
Joint2 函数里的 error
不是同一个变量。
18. 但是外层变量可以在内层使用
看:
int main()
{
int a = 5;
if (true)
{
std::cout << a << std::endl;
}
return 0;
}
这里可以输出:
5
为什么?
因为:
a
在外层:
main
里面创建。
而:
if
在它里面。
可以粗略理解成:
main 大房间
┌────────────────────┐
│ a = 5 │
│ │
│ if 小房间 │
│ ┌─────────────┐ │
│ │ 可以看到 a │ │
│ └─────────────┘ │
└────────────────────┘
19. 但反过来不行
如果变量是在小房间创建:
int main()
{
if (true)
{
int a = 5;
}
std::cout << a << std::endl;
}
外面看不到。
可以想象:
main 大房间
┌────────────────────┐
│ │
│ if 小房间 │
│ ┌─────────────┐ │
│ │ a = 5 │ │
│ └─────────────┘ │
│ │
│ 外面看不到 a │
└────────────────────┘
20. 一个容易混淆的例子
看:
int main()
{
int a = 5;
if (true)
{
int a = 10;
std::cout << a << std::endl;
}
std::cout << a << std::endl;
return 0;
}
你可能会疑惑:
怎么能有两个
a?
可以。
因为内层又创建了一个新的局部变量:
int a = 10;
所以可以想成:
外层 a = 5
进入 if
↓
内层又创建 a = 10
↓
内层优先使用自己的 a
↓
输出 10
离开 if
↓
内层 a 消失
↓
重新看到外层 a = 5
↓
输出 5
最终:
10
5
21. 这叫"遮蔽"
专业名字叫:
variable shadowing,变量遮蔽
现在不用背英文。
只要知道:
内层创建了一个同名变量,会暂时把外层那个同名变量"挡住"。
但真实工程里:
通常不建议随便写很多同名变量。
因为很容易看晕。
22. 一个机械臂例子
#include <iostream>
double calculateError(double target, double current)
{
double error = target - current;
return error;
}
int main()
{
double target = 90.0;
double current = 60.0;
double error = calculateError(target, current);
std::cout << error << std::endl;
return 0;
}
这里其实存在两套变量。
main() 里面:
target
current
error
函数里面:
target
current
error
虽然名字一样,但它们属于不同作用域。
调用时只是:
main 的 target 值
↓
传给函数的 target
main 的 current 值
↓
传给函数的 current
不是同一个变量直接"搬进去"。
这个区别以后学值传递、引用、指针时会非常重要。
23. 现在先形成三个概念
局部变量
在某个函数或代码块内部创建的变量。
例如:
int a = 5;
作用域
变量:
在哪里可以被使用。
生命周期
变量:
从什么时候创建,到什么时候结束。
24. 本课最核心的一句话
以后看到:
{
int a = 5;
}
你就应该想到:
a属于这一层{}的局部范围,出了这个范围通常就不能再使用。
本课小练习
第 1 题
void test()
{
int a = 5;
}
int main()
{
std::cout << a << std::endl;
}
这段代码能正常使用 a 吗?为什么?
第 2 题
void test()
{
int a = 5;
a++;
std::cout << a << std::endl;
}
int main()
{
test();
test();
}
输出:
?
?
第 3 题
int main()
{
int a = 3;
if (true)
{
std::cout << a << std::endl;
}
}
能不能在 if 里面使用 a?
第 4 题
int main()
{
if (true)
{
int b = 10;
}
std::cout << b << std::endl;
}
这里为什么会有问题?