目录
ROS工作空间简介
元功能包:功能包的组织者
Launch文件:源文件的组织者
功能包/源文件/launch文件组织工具
Launch中的if-else
环境变量赋值标签
Launch文件中的替换标签
环境参数中值的提取标签(带默认值)
环境参数中值的提取标签(不带默认值)
功能包绝对路径替换标签
工作空间下绝对路径替换标签
随机名称生成标签
待输入参数标签(不可对输入参数进行处理)
待输入参数标签(可对输入参数进行处理)
对于ROS中的文件结构,我们重点关注src部分,我们经过了解ROS的文件结构知道:src目录下可以包含多个功能包,假设我们需要使用机器人导航模块,但是这个模块中包含着地图、定位、路径规划...等不同的功能包,它们的逻辑关系如下:
如果我要获取“导航功能”,我需要一个一个按照功能包之间的依赖关系启动功能包,这样有些太麻烦了。在Linux系统中为了更方便的组织工程项目(这里针对的是项目文件,即功能包),出现了“元功能包”的概念。这个是一个“虚包”,就是这个功能包的src目录下没有源文件,因此自身不会实现专属功能,其功能的实现完全依赖于其他的功能包,起到一个组织功能包的作用。
我们以导航模块中的元功能包为例:
上述图片中显示了导航模块的所有功能包,其中navigation功能包为元功能包(metapackage),元功能包中由于没有src目录因此无需添加任何依赖项,因为这个功能包没有自己的专属功能,它的功能是借助其他的功能包的功能来实现的。元功能包有两个文件即可:
一个是package.xml文件:用于声明元功能包所依赖的其他功能包;另一个是CMakelist.txt文件:用于指定功能包之间的依赖关系。
CMakelist.txt(其他无用部分一定要删除,以防报错):
cmake_minimum_required(VERSION 3.0.2)
project(navigation)
find_package(catkin REQUIRED)
catkin_metapackage() // 只需添加此条内容即可
package.xml:
amcl
base_local_planner
carrot_planner
clear_costmap_recovery
costmap_2d
dwa_local_planner
fake_localization
global_planner
map_server
move_base
move_base_msgs
move_slow_and_clear
navfn
nav_core
rotate_recovery
voxel_grid
// 表征:这个功能包为元功能包
其中,最后
那说完了如何组织功能包,我们谈谈如何组织一个功能包内的众多源文件。如果我们一个节点一个节点的启动,这样耗时耗力。Launch可以帮助我们解决这个难题,下面解释以下launch文件的编写(以ROS系统自带的turtle节点为例):
① 节点启动标签
注意:因为ROS中采用多线程,因此节点的运行不会按照节点在launch中排列顺序进行。三个主要参数含义如下所示:
pkg:功能包的名称;
type:节点本来的名称,这个名称和节点所在.cpp源文件的文件名一致;
name:节点重映射的名称,相当于在系统中给节点所在源文件改了个名字;
在launch文件中,都是如下形式:
...
结果如下:
其实,我们如果认为给很多节点取名太麻烦,可以使用name=”$(anon node_name)”标签在节点node_name名称之后加一些随机数,使得该节点名称在整个catkin编译项目中唯一:
输出结果如下:
其实几乎所有的标签都可以作为
1. 意外关闭后自动启动的子级标签
Respawn=true|false表示:如果节点意外关闭是否重新启动。
2. 在指定机器上启动节点的子级标签
我们知道ROS是一个分布式系统,也就是说它的各个部分可以分布在不同的机器上,我们要在一个机器上远程启动另一个机器上的节点可以用这个标签来实现。首先,我们先要明确几个参数:
1)name="machine-name":给机器取得名字;
2)address="blah.willowgarage.com":网络地址/机器的主机名;
3)env-loader="/opt/ros/fuerte/env.sh":指定远程机器上的环境文件。环境文件必须是一个shell脚本,它设置所有必需的环境变量,然后对提供的参数运行exec。有关示例文件,请参阅ROS Fuerte的debian安装时提供的env.sh;
4)default="true|false|never" (optional):Sets this machine as the default to assign nodes to. The default setting only applies to nodes defined later in the same scope. NOTE: if there are no default machines, the local machine is used. You can prevent a machine from being chosen by setting default="never", in which case the machine can only be explicitly assigned;
5)user="username" (optional):登录机器的SSH用户名;
6)password="passwhat"(strongly discouraged):SSH密码。强烈建议您配置SSH密钥和SSH代理,以便您可以使用证书代替登录;
7)timeout="10.0" (optional):远程启动节点最长允许的时间。
示例如下:(先配置machine信息,在使用
运行该内容所在的launch文件可以“启动名为foo的机器上的footalker节点”。
详细请见:roslaunch/XML/machine - ROS Wikihttp://wiki.ros.org/roslaunch/XML/machine
3. 节点延时启动的子级标签
节点延迟启动的子级标签,一般结合节点重启动是使能标签respawn(如果节点异常退出运行,那么该节点会被重新启动)一起使用:
Respawn_delay=”delay_time”:表示节点延迟启动delay_time秒。
4. “如果XXX节点结束运行(XXX节点被杀死),则所有节点都停止运行”的子级标签
这个标签表明了这个节点的重要性:
Required=true|false一般用在非常重要的节点(一旦这个节点停止运行则整个系统都会受影响)上。一旦这个节点进程结束(这个节点被杀死了),整个系统的所有节点都会停止运行,例如雷达探测节点一旦停止运行,那么整个无人驾驶的汽车上的所有节点就必须停止运行,否则就极有可能会引发事故。
5. 给节点名称添加前缀(给节点添加命名空间)的子级标签
运行launch文件之后,我们用rosnode list会发现:
乌龟节点的话题从/my_node变为了/hello/my_node。我们在启动roslaunch发现我们的键盘已经控制不了小乌龟了,因为我们在给节点添加namespace的同时,节点的所有属性包括topic也都被置于namespace之下:
turtlesim_node订阅的是/hello/turtle1/cmd_vel话题,而turtle_teleop_key发布的则是/turtle1/cmd_vel话题,topic都不同server和client不可能正常通信,因此小乌龟不再受键盘控制。
6. 指明输出位置的子级标签
turtle_teleop_key发布的消息可以输出值控制台,output=”screen”|”log”,消息的输出有两种方式:控制台输出/日志输出。
7. 指定节点工作的当前工作目录的子级标签
Cwd全称为current working directory,cwd=”ROS_HOME”|”node”。如果cwd=”node”,则该节点的工作目录将被设置为与该节点的可执行文件相同的目录;否则cwd=”ROS_HOME”,则该节点的工作目录详见:ROS/EnvironmentVariables - ROS Wikihttp://wiki.ros.org/ROS/EnvironmentVariables
8. 向节点传递参数的子级标签
向add_two_ints_client节点传递两个参数a和b,其实就如同我们运行如下命令:
rosrun beginner_tutorials add_two_ints_client a_value b_value
9. 私有参数清楚子级标签:clear_params
我们有时会碰到一下的情况:我们使用名为foo的节点发布参数param01:
但是因为我不小心写错了参数名称,正确的参数名称应为var01:
这就会导致,参数服务器中属于可执行名称为foo的节点的私有参数有两个:var、var01。
但是我不想再要var参数了,想删去,我们可以用以下两种方法:
也可以直接使用clear_params子级标签在launch文件启动该节点之前,清楚参数服务器中所有属于该节点的私有参数:
10. 输出方式子级标签:output=“log|screen”,output默认为log
output有两种选项:screen和log。当output="screen"时,输出信息和error信息会输出至屏幕上;当output="log"时,error信息会被输出至屏幕上,但是输出信息会被存放进ROS_HOME环境变量指定的路径下的log日志文件中。
rosnetic@coolboy:~/桌面/turtle_test$ roslaunch rosbag_package node.launch
... logging to /home/rosnetic/.ros/log/6ea41d1e-a050-11ec-aad6-b1cc52f1bc44/roslaunch-coolboy-12454.log
Checking log directory for disk usage. This may take a while.
Press Ctrl-C to interrupt
Done checking log file disk usage. Usage is <1GB.
started roslaunch server http://coolboy:32795/
SUMMARY
========
PARAMETERS
* /rosdistro: noetic
* /rosversion: 1.15.14
NODES
/
node (rosbag_package/node)
ROS_MASTER_URI=http://localhost:11311
process[node-1]: started with pid [12472]
上述输出数据中,我们可以知道log文件被输出到的路径为:
/home/rosnetic/.ros/log/6ea41d1e-a050-11ec-aad6-b1cc52f1bc44/roslaunch-coolboy-12454.log
其实我们还可以使用如下命令行工具查看log日志存放路径:
rosnetic@coolboy:~$ roslaunch-logs
/home/rosnetic/桌面/turtle_test/log/6ea41d1e-a050-11ec-aad6-b1cc52f1bc44
具体日志文件相关设置请参考:
ROS日志系统部分设置_Nicholasoooo的博客-CSDN博客_ros日志设置这里写自定义目录标题ROS日志设置$ROS_ROOT/config/rosconsole.config中的配置及说明ROS_LOG_DIR环境变量ROSCONSOLE_FORMAT环境变量ROS日志设置$ROS_ROOT/config/rosconsole.config中的配置及说明log4j.logger.ros.pkg=INFO,stdout,pkg_pro# pkg是ros中的package名;stdout指的是标准输出;pkg_pro是别名,后面的配置可以使用此名称来定义他的属性,此行代码代https://blog.csdn.net/Nicholasoooo/article/details/11792061511. launch-prefix子级标签,暂时还不太清楚其用途:
roslaunch/Tutorials/Roslaunch Nodes in Valgrind or GDB - ROS Wikihttp://wiki.ros.org/roslaunch/Tutorials/Roslaunch%20Nodes%20in%20Valgrind%20or%20GDB
12. if分支判断子标签:if="true|false"
我们可以对输入的bool类型的参数进行判断,来确定此节点是否应该被启动:
也可以直接使逻辑值为true或者false:
if对于bool类型的判断不区分大小写,我们可以输入true或者TRUE、false或者False等等,如果我们想要输入的bool类型的变量被系统判断为string字符串,需要使用强制类型转换符像"!!str true"一样对true这类bool类型的变量进行强制转换 :
命令行输入:roslaunch package01 setup.launch a="!!str true"
launch文件:
param01参数会被自动推导为string类型的参数,并且值为“true”字符串,此时“true”字符串不在代表逻辑值1了!
13. 节点工作空间设置子标签:cwd="node_save_file_path"
关于节点的工作空间详见:
ros launch中的节点工作空间路径_bluewhalerobot的博客-CSDN博客_roslaunch 当前路径原链接在launch文件中启动节点,那么这个节点的工作目录是什么呢?在node程序中如果创建一个文件,这个文件的默认位置在哪里? 在launch中node节点有一个cwd参数,按照文档的解释,这个参数可以为ROS_HOME或node。当设置为ROS_HOME时,cwd为ROS_HOME参数指定的位置。当设置为node时就是对应node所在的位置。但是如果你echo $ROS_HOME时会发现这个值https://blog.csdn.net/bluewhalerobot/article/details/79147243其实,我们都知道可执行节点文件其实就是一个.cpp源文件,当我们在这个.cpp源文件中进行可以生成文件的操作(比如:消息的录制就可以生成.bag文件)时,节点的工作空间就是ROS导出这些在节点中生成的文件的地方。
② 参数设置标签
我们设置的是global全局参数,我们也可以结合
这样的话我们设置了/my_key/var1私有命名空间下的参数,我们使用rosparam list可以看到如下部分参数:
param标签的子标签如下所示:
③ 参数打包输入输出删除的标签
从.yaml文件中读取参数:
使用rosparam list读取出参数如下所示:
除此之外,当我们将.yaml参数文件中的参数导入参数服务器时,我们还可以给这些参数添加namespace命名空间:
结果如下:
变量全都加上的前缀,即命名空间/hello/…,.yaml格式的书写格式如下所示:
ROS中yaml文件编写格式_超级霸霸强的博客-CSDN博客https://blog.csdn.net/weixin_45590473/article/details/123250193将参数打包输入进.yaml文件中:
这样做理应可以将该功能包的所有参数导入到了我们指定的.yaml文件中,但是我们得到的结果确是:
Input.yaml文件中啥变量也没有,我们导入了个寂寞,这是因为rosparam是launch文件命令中优先级最高的,不管你将rosparam命令放在哪里,launch文件总是先执行参数的导入/导出/删除操作。我们要想导入参数必须另建一个.launch文件,在使用上述launch文件启动完所有节点之后,在另一个launch文件中执行该功能包参数的导出操作:
此时,input.yaml文件中数据为:
Rosparam不仅可以从.yaml导出导入参数,也可以直接删除参数服务器中的参数:
使用上述代码:删除了“/hello/n1”这个参数:
使用上述代码:删除了“hello”命名空间下的所有参数:
如果啥子级标签也没有,那就删除这个包下的所有参数:
结果如下:
Rosparam可以直接向参数服务器传递数组类型的参数:
[1, 2, 3, 4]
我们调用rosparam get a_list命令行命令发现:
A_list参数被参数服务器解析为int类型的数组。也可以用另一种方式向参数服务器传递多个且类型的不同的参数:
[1, 2, 3, 4]
a: 9
b: "hello"
c: [1,2,3,4]
还可以使用此方式向参数服务器传递带有命名空间的多个不同类型的参数:
[1, 2, 3, 4]
a: 9
b: "hello"
c: [1,2,3,4]
此外,value标签的写法有两种形式:
[1, 2, 3, 4]
这两种形式都是等价的,都可以得到:
其实,这就是param和rosparam标签的最主要的区别:
Param标签:向参数服务器传递/修改(覆盖原来的参数就是修改)一个参数,且没有删除操作;
Rosparam标签:向参数服务器传递/修改/删除多个参数、从.yaml文件中加载参数或者将功能包的参数导入.yaml文件当中。
除此之外,我们介绍完rosparam常用的子标签,再来介绍一下rosparam中比较陌生的标签——subst_value子标签,该标签用于“是否允许在yaml文件中使用launch文件中的替换标签”:
# launch文件中的rosparam标签
# yaml中的内容
KeyValueStr: "$(optenv ROS_NAMESPACE NULL)" #string
就像这样,$(optenv ROS_NAMESPACE NULL)是一个launch文章中的子标签,含义为“如果ROS_NAMESPACE环境变量存在,那么KeyValueStr就被赋予环境变量中存储的内容,如果该环境变量不存在,则KeyValueStr变量就被赋予字符串“NULL” ”。在这里由于我们没有设置命名空间,因此KeyValueStr中的内容为空。
先不要着急,文章的后半部分会为您详细的讲解launch文章中替换子标签的含义及用途:roslaunch/XML - ROS Wikihttp://wiki.ros.org/roslaunch/XML#substitution_args如果subst_value="false",则“$(optenv ROS_NAMESPACE NULL)”会被解析为字符串送给KeyValueStr变量,此时KeyValueStr=$(optenv ROS_NAMESPACE NULL)
④ 参数统一管理的标签
$(arg car_width)
Name:
a: 9
b: "hello"
c: $(arg car_width)
Arg标签一般常和param一起使用(不可以和 rosparam标签一起使用),其中$(arg arg_name)代表着
其中default和value标签只能存在一个,不可以共存!
除此之外,我们使用default而不使用value标签的原因就是“我们使用default时,我们不通过命令行传参那么$(arg arg_name)就是default_value;当我们通过命令行传参时,我们传递的参数会覆盖$(arg arg_name)中默认参数值default_value”,这样我们传参就变得非常灵活了。如下所示,我们使用命令行传递参数:
命令行传参格式为:arg_name:=set_value。Launch文件如下:
我们在命令行调用rosparam get var命令:
但是当你将arg标签和rosparam标签组合在一起使用,则不会得到你想要的结果:
我们在命令行调用rosparam get a_list命令:
我们想要的是一个数字”2”,但是这个变量却被赋值了字符串。
⑤ 改topic名称的标签
Remap标签的格式为:
Remap的实质:
我们通过restopic info topic_name可以得知由topic:/turtle1/cmd_vel映射而来的topic:/new_topic其实本质上是相同的,只是名称不同。Remap标签的作用在于:
Remap的前提是topic1和topic2的数据类型相同,仅仅是名称不同而已。这样的话两个话题重映射到new_topic话题上就可以使得通信双方建立联系。切记:topic重映射是产生一个与其数据类型相同仅仅是名称不同的新话题,不是修改原来的话题!
⑥ 节点组织标签
Name:
a: 9
b: "hello"
c: [1,2,3,4]
结果就是给被
对于节点的属性(例如:节点订阅/发布的话题)来说:
用于确定是否在启动前删除指定命名空间下的所有参数,和前面的
⑦ 启动其他launch文件的标签
Include标签下有两个子级标签arg和加载环境变量的env(用于向被引用的launch中传入参数和环境变量的值),我们一般用arg来传递参数,被该launch加载的launch文件如下所示:
Name:
a: 9
b: "hello"
c: [1,2,3,4]
被加载的launch文件需要在命令行输入参数才可以运行,我们在
Include标签还有一个子标签,非常有用。他就是pass_all_args:这个标签表示我是否需要将该launch文件中所有使用arg设置的参数全部加载到使用include标签包含的launch当中:
我这样做的效果和上一个代码一样,但是我如果include很多launch,只要pass_all_args="true",那么该launch中的所有参数都会自动作为“被include的launch”的参数,这样我们再也不用在include标签下添加那么多arg子级标签用于给每个include的launch添加参数了,这看起来是多么美妙呀!
Note:我们可以使用“include标签+子级标签“来将所有launch文件按照逻辑启动顺序封装在一个launch文件当中,这样我们只需启动一个launch即可以启动整个功能包的全部节点。
功能包的组织工具 |
元功能包 |
节点文件(.cpp源文件)的组织工具 |
Launch文件 |
Launch文件的组织工具 |
Launch文件 |
一层一层的管理,文件组织形式如下所示:
一层一层的管理,文件组织形式如下所示:
其实在每个标签中都有用于if逻辑判断的子级标签:
[1, 2, 3, 4]
if=”true|false”,if后面的参数只能是bool类型,并且if=”true”就会执行该句语句。与之相反的是unless,译为“除非…”,格式与if相同:unless=”true|false”,如果unless=”true”则相当于if=”false”的作用:
[1, 2, 3, 4]
如果我们用于逻辑块的if判断,我们可以将if/unless和
[1, 2, 3, 4]
Name:
a: 9
b: "hello"
c: [1,2,3,4]
这样就将多条逻辑语句同时包含在if之下,相当于C++中if{…}语句中的“以大括号为边界的逻辑块”。
标签调用格式:
标签作用:给环境变量赋初值,ROS中环境变量详解如下所示:
(ros//EnvironmentVariables)ros环境变量_w383117613的专栏-CSDN博客_ros 环境变量https://blog.csdn.net/w383117613/article/details/46860417调用示例:
我们通过设置ROS_NAMESPACE来设置节点的命名空间,输出结果如下所示:
启动的turtle1节点的话题全部加上了hello前缀:
启动的turtle1节点的参数全部加上了前缀:
Launch文件中使用
结果如下所示:
结果ROS_PACKAGE_PATH中的值并没有发生改变。我们只能使用
结果如下所示:
我们看到我们的两次赋值行为,在实现逻辑上相当于如下的代码编写形式:
分别在两个不同的命名空间中分别启动了turtlesim功能包下的turtlesim_node节点。以上实例坚定了说明了使用
但是我们可以在不同节点的私有空间中赋予环境变量ROS_NAMESPACE初值:
标签格式:$(optenv var_name default_value)
标签作用:提取环境变量中的数值
调用示例:
当HELLO环境变量存在时,将HELLO环境变量中的内容赋值给环境变量ROS_NAMESPACE,否则将环境变量ROS_NAMESPACE置为hello。
运行结果如下所示:
$(openv var_name default_value)不仅可以用于给环境变量赋值标签
运行结果如下所示:
注意:第二个参数后面的所有内容代表着默认值default_value。
标签格式:$(env var_name)
标签作用:提取环境变量中的数值赋给参数或者环境变量
运行结果如下所示:
运行结果如下所示:
我们看到turtlesim功能包下的turtlesim_node节点上报的参数全部加上了前缀。
该标签不仅可以用于
运行结果如下所示:
标签格式:$(find package_name)
使用示例:
从功能包tf2_turtle工作目录下的启动launch文件夹中的setupGUI.launch文件。
下面是test1.launch调用setupGUI.launch文件的代码,并且两个launch文件在一个文件夹之中:
$(dirname)代表“test1.launch文件所在工作空间的绝对地址,即test1.launch文件所在文件夹的绝对地址”,由于test1.launch文件和setupGUI.launch文件在一个文件夹下,因此setupGUI.launch文件的路径可以写为“$(dirname)/setupGUI.launch”。
标签格式:$(nano name_base)
使用示例:
上述代码生成的节点名称格式为“foo_随机数”,这样可以保证一个节点同时启动多次。
标签格式:$(args arg_name)
使用示例:
1. 参数设置之命令行传参:
运行结果如下所示:
2. 节点参数设置之命令行传参:
命令行需输入:
roslaunch beginner_tutorials launch_file.launch a:=1 b:=5
roslaunch命令最后需要跟上两个参数a和b,传参形式为”param_name:=param_value”。
roslaunch这个指令后面可以跟[-optionalArg]可选参数:
osnetic@coolboy:~$ roslaunch --h
Usage: roslaunch [options] [package] [arg_name:=value...]
roslaunch [options] [...] [arg_name:=value...]
If is a single dash ('-'), launch XML is read from standard input.
Options:
-h, --help show this help message and exit
--files Print list files loaded by launch file, including
launch file itself
--args=NODE_NAME Print command-line arguments for node
--nodes Print list of node names in launch file
--find-node=NODE_NAME
Find launch file that node is defined in
-c NAME, --child=NAME
Run as child service 'NAME'. Required with -u
--local Do not launch remote nodes
--screen Force output of all local nodes to screen
--required Force all nodes to be required
--log Force output of all local nodes to log
-u URI, --server_uri=URI
URI of server. Required with -c
--run_id=RUN_ID run_id of session. Required with -c
--wait wait for master to start before launching
-p PORT, --port=PORT master port. Only valid if master is launched
--core Launch core services only
--pid=PID_FN write the roslaunch pid to filename
-v verbose printing
--no-summary hide summary printing
--dump-params Dump parameters of all roslaunch files to stdout
--skip-log-check skip check size of log folder
--ros-args Display command-line arguments for this launch file
--disable-title Disable setting of terminal title
-w NUM_WORKERS, --numworkers=NUM_WORKERS
override number of worker threads. Only valid for core
services.
-t TIMEOUT, --timeout=TIMEOUT
override the socket connection timeout (in seconds).
Only valid for core services.
--master-logger-level=MASTER_LOGGER_LEVEL
set rosmaster.master logger level ('debug', 'info',
'warn', 'error', 'fatal')
--sigint-timeout=SIGINT_TIMEOUT
the SIGINT timeout used when killing nodes (in
seconds).
--sigterm-timeout=SIGTERM_TIMEOUT
the SIGTERM timeout used when killing nodes if SIGINT
does not stop the node (in seconds).
最后还有一个
其实,test标签中的这些参数和node标签中的参数有一一对应的关系:
对于可执行名称为node01的节点进行测试:
启动可执行名称为node01的节点:
test标签的含义是:检查package01功能包下可执行节点名称为node01的节点在10.0秒之内能否正常响应,如若超过10.0秒还未响应,那么说明节点启动失败,进而导致test测试失败。
test标签和node标签一样,有自己的子级标签:
roslaunch/XML/test - ROS Wikihttp://wiki.ros.org/roslaunch/XML/test其中test标签所必须具有的三要素:
setup.launch中的内容如下所示:
当我们运行"roslaunch package01 setup.launch"时,结果如下所示:
rosnetic@coolboy:~/桌面/turtle_test$ roslaunch rosbag_package node.launch
... logging to /home/rosnetic/.ros/log/6ea41d1e-a050-11ec-aad6-b1cc52f1bc44/roslaunch-coolboy-21931.log
Checking log directory for disk usage. This may take a while.
Press Ctrl-C to interrupt
Done checking log file disk usage. Usage is <1GB.
started roslaunch server http://coolboy:34157/
SUMMARY
========
PARAMETERS
* /rosdistro: noetic
* /rosversion: 1.15.14
NODES
ROS_MASTER_URI=http://localhost:11311
No processes to monitor
shutting down processing monitor...
... shutting down processing monitor complete
我们可以看到检测无异常,说明节点在第一次启动时就已被成功启动。
标签格式:$(eval expression)
使用示例:
1. 参数值为传入参数经计算得到的结果:
也可以隐式(间接获取my_foo的值)传参:
使用了eval标签中的子标签arg(‘param_name’),切记一定要用单引号并且子标签为arg(该标签只接受一个待输入参数)。
运行结果如下所示:
2. 也可以在eval中使用其他标签(但必须将双引号改为单引号)
注意:一定要在””双引号内使用’’单引号,单引号表示由双引号包起来的次级表达式;其次,不同数据类型的参数不可以混用!
运行结果如下所示: