上一课标准答案:
m.speed = 100.0Joint是我们自己通过struct定义出来的新类型
Joint joints[6];
表示创建一个长度为 6 的 Joint 数组,也就是可以保存 6 个关节结构体。
joints[2].angle = 45.0;
表示把数组中第 3 个关节的 angle 成员设置为 45.0。
注意还是那个规则:
joints[0] → 第1个关节
joints[1] → 第2个关节
joints[2] → 第3个关节
第 22 课:const ------ 不允许随便修改的数据
这一课非常重要。
以后你看真实 C++ 工程代码,会大量看到:
const
尤其是:
const double
const int
const std::string&
const Joint&
先不急着背。
我们从最简单的问题开始:
有些变量,我创建以后根本不希望别人修改,怎么办?
1. 普通变量可以修改
例如:
double pi = 3.14159;
pi = 5.0;
这是合法的。
因为 pi 是普通变量。
最开始:
pi = 3.14159
后来:
pi = 5.0
C++ 不会阻止你。
但问题是:
圆周率本来就不应该随便改。
这时候可以使用:
const
2. 第一个 const
写:
const double PI = 3.14159;
可以理解成:
创建一个
double数据PI,但以后不允许修改它。
所以:
PI = 5.0;
会报错。
因为你已经告诉 C++:
PI是只读的。
3. const 怎么理解?
英文:
constant
有:
常量、不变
的意思。
所以现阶段可以把:
const
理解成:
这个东西不允许被修改。
例如:
const int joint_count = 6;
表示:
joint_count是 6,并且程序后面不应该再改变它。
4. 为什么这很有用?
假设你的机械臂永远是 6 自由度。
可以写:
const int JOINT_COUNT = 6;
以后:
for (int i = 0; i < JOINT_COUNT; i++)
{
}
比直接到处写:
6
更容易理解。
因为你一看:
JOINT_COUNT
就知道:
这是关节数量。
5. 为什么很多常量名字写成大写?
例如:
const int JOINT_COUNT = 6;
或者:
const double MAX_ANGLE = 90.0;
全大写并不是 C++ 强制要求。
这是很多工程中的命名习惯:
一看到大写,就知道这个东西可能是常量。
比如:
const double PI = 3.14159;
const double MAX_SPEED = 100.0;
const int JOINT_COUNT = 6;
不过以后你也会看到不同团队采用不同命名风格。
6. 机械臂限位例子
例如:
const double MAX_ANGLE = 90.0;
const double MIN_ANGLE = -90.0;
double angle = 45.0;
if (angle >= MIN_ANGLE && angle <= MAX_ANGLE)
{
std::cout << "Valid" << std::endl;
}
这样比:
if (angle >= -90.0 && angle <= 90.0)
更容易看懂。
因为:
MIN_ANGLE
MAX_ANGLE
直接说明了这些数字的意义。
7. 常量也有类型
例如:
const int a = 5;
这是:
不允许修改的
int
而:
const double angle = 30.5;
是:
不允许修改的
double
所以 const 并不是一种新的数据类型。
更准确地说,它是在告诉 C++:
对这个变量增加"不可修改"的限制。
8. const 和函数参数有什么关系?
现在进入一个非常重要的地方。
假设我们有:
struct Joint
{
double angle;
double speed;
};
然后写函数:
void printJoint(Joint joint)
{
std::cout << joint.angle << std::endl;
}
这里:
Joint joint
是值传递。
也就是说:
复制一份 Joint 进去。
如果 Joint 很大,这样每次调用都复制,可能有点浪费。
所以我们之前学过引用:
void printJoint(Joint& joint)
这就不会复制。
9. 但是引用有一个风险
看:
void printJoint(Joint& joint)
{
joint.angle = 999.0;
}
因为:
Joint& joint
是原对象的引用。
所以函数可以直接修改外面的关节。
例如:
Joint j;
j.angle = 30.0;
printJoint(j);
函数执行后:
j.angle = 999
但这个函数明明叫:
printJoint
我们本意只是:
打印数据。
根本不希望它修改。
怎么办?
10. const Joint&
这时候就可以写:
void printJoint(const Joint& joint)
{
std::cout << joint.angle << std::endl;
}
这句非常重要。
我们一点点拆。
11. 先看 Joint& joint
你已经学过:
Joint& joint
表示:
joint是外部 Joint 对象的引用。
没有复制。
12. 再加上 const
const Joint& joint
表示:
引用外面的 Joint,但不允许通过这个
joint去修改它。
你可以理解成:
只读引用。
这四个字非常重要。
13. 所以这句怎么读?
void printJoint(const Joint& joint)
可以翻译成:
printJoint接收一个Joint的只读引用。
它有两个优点:
不复制对象
+
不允许函数乱改对象
这就是为什么真实 C++ 工程里非常喜欢这种写法。
14. 对比三种方式
第一种:值传递
void printJoint(Joint joint)
意思:
复制一份。
优点:
不会改到外面。
缺点:
需要复制。
第二种:普通引用
void printJoint(Joint& joint)
意思:
不复制,直接引用原对象。
优点:
高效,而且可以修改。
缺点:
函数可能改坏外部数据。
第三种:const 引用
void printJoint(const Joint& joint)
意思:
不复制,但只允许读取。
这通常特别适合:
函数只需要看数据,不需要修改数据。
15. 用文件类比
假设你有一份机器人参数文件。
值传递
像:
复印一份文件给别人。
他改复印件,不影响原件。
引用
像:
把原件直接交给别人。
他可以直接涂改原件。
const 引用
像:
把原件放进透明保护套,只允许看,不允许修改。
这个类比非常好记。
16. 完整例子
#include <iostream>
struct Joint
{
double angle;
double speed;
};
void printJoint(const Joint& joint)
{
std::cout << joint.angle << std::endl;
std::cout << joint.speed << std::endl;
}
int main()
{
Joint j;
j.angle = 30.0;
j.speed = 5.0;
printJoint(j);
return 0;
}
这里调用:
printJoint(j);
不会复制完整 j。
函数只是通过引用查看它。
但是函数里面不能写:
joint.angle = 100;
因为:
joint
是 const。
17. 如果硬要修改会怎样?
例如:
void printJoint(const Joint& joint)
{
joint.angle = 100.0;
}
编译器会报错。
因为你前面已经承诺:
const Joint& joint
是只读的。
这其实是在帮你防止 Bug。
18. 普通基础类型还需要 const& 吗?
例如:
void printNumber(const int& number)
理论上可以。
但对于:
int
double
bool
char
这类很小的数据,复制成本很低。
很多时候直接:
void printNumber(int number)
就够了。
所以你以后经常会看到:
void setAngle(double angle)
而不是:
void setAngle(const double& angle)
19. 但 string、vector、struct、class 很常见
例如:
void printName(const std::string& name)
或者:
void processData(const std::vector<double>& data)
或者:
void printJoint(const Joint& joint)
因为这些对象可能比一个普通 double 大得多。
所以:
不复制 + 不修改
是非常常见的工程需求。
20. 你以后会大量见到这种函数
例如:
void printAngles(const std::vector<double>& angles)
拆开:
std::vector<double>
↓
一个 double vector
&
↓
引用,不复制
const
↓
只读,不能修改
整句:
函数接收一个只读的
double vector引用。
21. 一个机器人轨迹例子
假设:
std::vector<double> trajectory =
{
10.0,
20.0,
30.0
};
函数:
void printTrajectory(const std::vector<double>& trajectory)
{
for (int i = 0; i < trajectory.size(); i++)
{
std::cout << trajectory[i] << std::endl;
}
}
它只负责看数据、打印数据。
所以很适合:
const std::vector<double>&
因为我们根本不需要改它。
22. 那如果函数就是需要修改呢?
那就不要加 const。
例如:
void setZero(Joint& joint)
{
joint.angle = 0.0;
}
因为我们的目的就是:
修改原来的
joint
所以这里必须允许修改。
不能写:
void setZero(const Joint& joint)
否则:
joint.angle = 0.0;
会被禁止。
23. 所以怎么选?
现在可以先这样判断。
如果函数:
需要修改外部对象
用:
Joint&
例如:
void resetJoint(Joint& joint)
如果函数:
只需要读取外部对象,不需要修改
优先考虑:
const Joint&
例如:
void printJoint(const Joint& joint)
如果参数只是一个简单数字:
int
double
bool
通常直接值传递即可:
void setSpeed(double speed)
24. 再看一个非常典型的机器人函数
bool isJointSafe(const Joint& joint)
{
return joint.angle >= -90.0 &&
joint.angle <= 90.0;
}
这句现在你已经可以完整拆出来:
bool
↓
函数最后返回 true/false
isJointSafe
↓
函数名
const Joint& joint
↓
接收一个 Joint,只读引用,不复制
return ...
↓
判断关节角度是否合法
这已经很像真实工程 C++ 代码了。
25. const 还能和指针结合
以后你会见到:
const int* p
以及:
int* const p
甚至:
const int* const p
但现在先不学。
因为 const + 指针 的位置不同,含义会变化,很容易一下绕晕。
我们后面会专门开一课讲。
26. 本课最核心的三个写法
普通变量:
double angle = 30.0;
可以修改。
常量:
const double MAX_ANGLE = 90.0;
不允许修改。
只读引用:
const Joint& joint
不复制外部 Joint,而且不允许通过这个引用修改它。
本课小练习
第 1 题
const int JOINT_COUNT = 6;
后面能不能写:
JOINT_COUNT = 7;
为什么?
第 2 题
下面函数:
void printJoint(const Joint& joint)
里面能不能写:
joint.angle = 100.0;
第 3 题
一句话解释:
const Joint& joint
是什么意思。
第 4 题
下面两个函数分别更适合哪种场景?
void printJoint(const Joint& joint)
void resetJoint(Joint& joint)
下一课我们进入:
第 23 课:class ------ 从 struct 走向真正的面向对象
你会开始理解为什么真实机器人程序里会出现:
class Robot
{
public:
void connect();
void moveJoint();
void stop();
private:
double joint_angle;
};
也会第一次真正理解:
robot.connect();
robot.stop();
到底是什么。