由于下一講要講到怎么在類中pub和sub消息.那么考慮到有些同學(xué)對類不甚熟悉.我們稍微回顧一下.但關(guān)于類網(wǎng)上一查其實一大堆東西,而且都是從入門講起.所以我這兒肯定不會重復(fù)書寫那些內(nèi)容.要介紹的幾個東西,其實本來要用得好的話蠻復(fù)雜,我們只會涉及到皮毛,重心會放在和之前的代碼比較,以了解之前我們之前三講的很多語法為什么可以那么寫.
比如上一講的geometry_msgs::PoseStamped的對象msg包含成員變量header和pose,heaer包含成員變量stamp等,為什么我們就可以使用msg.header.stamp這種語法來獲取類型為time的變量?
再比如std_msgs::Int8這種語法怎么來的,中間那個::表示什么意思,以及它前后的std_msgs和In8有什么區(qū)別.
再比如我們定義ROS publisher時
ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);
為什么通過<std_msgs::String>這種語法來定義要發(fā)送的對象?
這三個比如分別涉及到類,命名空間和模版.對語法熟悉或者不想究其所以然的同學(xué)可以跳過這一章直接進入下一章的講在類中pub和sub消息.
這一講假設(shè)大家對函數(shù),參數(shù),循環(huán)等最基本的C++的東西已經(jīng)掌握了.如果這些不清楚那么用C++操作ROS確實不太合適哈哈.
類(class)
同樣類的作用和意義我就不詳細闡釋了,網(wǎng)上一抓一大把,他們的基本意義大家可以上網(wǎng)搜索.簡單來講,定義了類之后我們可以創(chuàng)建它的對象.對類和其對象直接操作是c++最重要的東西之一.直接開始例子.打開一個terminal,輸入下面的內(nèi)容
mkdir ~/C++Test
cd C++Test
mkdir classTest
mkdir namespaceTest
mkdir templateTest
咱們創(chuàng)建一個叫C++Test的文件夾,再創(chuàng)建三個用于測試三種東西的子文件夾.之后,在classTest文件夾下創(chuàng)建一個叫classBasic.cpp的文件和一個叫CMakeLists.txt的文件.在classBasic.cpp中輸入下面內(nèi)容.
#include <iostream>
class poorPhd{
public:
/*define constructor*/
poorPhd(){
std::cout<<"we create a poor phd class"<<std::endl;
}
/*public member variable*/
int hairNumber = 100;
/*public member function*/
int getGirlFriendNumber(){
return girlFriendNumber;
}
private:
/*private member variable*/
int girlFriendNumber = 0;
};
int main(){
/*define the object*/
poorPhd phd;//will use constructor function
/*call the public memberfunction*/
std::cout<<"girlFriendnNumber is "<<phd.getGirlFriendNumber()<<std::endl;
/*change tha value of member variale*/
phd.hairNumber = 101;
/*call the member variable*/
std::cout<<"hairNumber is "<<phd.hairNumber<<std::endl;
/*define class pointer*/
poorPhd *phdPointer;
/*assign the pointer to an object*/
phdPointer = &phd;
/*call the member variable*/
std::cout<<"use pointer, hair number is "<<phdPointer->hairNumber<<std::endl;
}
逐行解說.
1:#include<> 包含頭文件,這樣可以使用std::cout<<...std::endl;
2:class poorPhd 定義了一個叫poorPhd的類.類后跟這宗括號{}.宗括號中的內(nèi)容為類的內(nèi)容.
3:public 加冒號之后的內(nèi)容,即為公有.公有范圍內(nèi)定義的函數(shù)為公有成員函數(shù),變量為公有成員變量.
4:poorPhd(). 這個函數(shù)稱為構(gòu)造函數(shù)(constructor function).在類創(chuàng)建時,會自動調(diào)用.構(gòu)造函數(shù)的名字和類的名字必須一樣并且沒有返回值.
5:int hairNumber = 100. 定義了一個int類型公有成員變量,賦值100.
6:int getGirlFriendNumber(). 定義了一個返回值為int的函數(shù),該函數(shù)會返回私有成員變量girlFriendNumber.
7:private加冒號之后的內(nèi)容,即為私有.私有范圍內(nèi)定義的函數(shù)為私有成員函數(shù),變量為私有成員變量.
8: int girlFriendNumnber=0. 定義了一個int類型的私有成員變量girlFriendNumber并賦值為0
main函數(shù)中
9: poorPhd phd 創(chuàng)建了一個類的對象(object),名字叫phd.每一個類,要想實際被使用,都需要創(chuàng)建一個對象.對象會擁有之前我們在類中定義的所有東西.所謂擁有,即是可以調(diào)用他們.對象的數(shù)量是沒有限制的,并且他們之間不會干擾.你還可以用類似方法創(chuàng)建一個名字加abc的對象,它也會擁有poorPhd這個類的全部東西.
對象在創(chuàng)建時,會自動調(diào)用構(gòu)造函數(shù).
10:std::cout....phd.getGirlFriendNumber()<<std::endl;
類對象調(diào)用成員函數(shù)或者成員變量的方法是對象名.成員.公有成員可以在類的定義外使用這種方式直接調(diào)用,私有成員是不可以被直接調(diào)用的.所以如果我們使用phd.girlFriendNumber就會報錯.因為在類外,不可以直接調(diào)用私有成員變量.那有時候我們?nèi)匀幌肟吹交蛘咝薷乃接谐蓡T變量怎么辦呢?那么我們可以寫類似于這個gerGirlFriend的公有成員函數(shù).公有成員函數(shù)定義在類中,所以它可以使用私有成員變量,并把變量的值作為返回值,這樣我們就得到可私有成員變量的值.
為什么要分私有公有呢?有時候我們寫了一個類,并不想其中所有東西都被使用者使用,比如我們有了造車相關(guān)技術(shù),所有這些技術(shù)和在一起,就是類.具體實現(xiàn),就是我們造了一輛輛車子.每一輛車子就稱為對象.每一輛車子都有相同的內(nèi)容,但是他們互不干擾.我們只想用戶了解剎車,油門等東西.并不想用戶了解車子內(nèi)部構(gòu)造.那剎車油門在這兒就是公有,而車子內(nèi)部構(gòu)造就是私有.如果用戶實在想獲取內(nèi)部構(gòu)造,用戶可以去汽車銷售店了解些相關(guān)資料,銷售店就相當于咱們寫的那個get...函數(shù)接口,架起用戶和類私有成員友誼的橋梁.當然,如果有些內(nèi)容特別私密,我們并不想用戶了解它的相關(guān)資料,就不寫那個get...函數(shù)就行了.
- phd.hairNumber = 101;
為公有成員變量賦值101.
12.std::cout<<...phd.hairNumber...
調(diào)用公有成員并print出來.
13.poorPhd *phdPointer 創(chuàng)建一個類的指針.類的指針被創(chuàng)建時不會調(diào)用構(gòu)造函數(shù).它需要指向一個對象.
14.phdPointer = &phd 剛才創(chuàng)建的對象的地址賦值給指針,這個指針就有了phd對象的所有內(nèi)容.
-
...phdPointer->hairNumber...類指針調(diào)用類的成員的唯一不同之處就是使用指針名->成員調(diào)用而不是對象名.成員調(diào)用.
和之前寫的ROS代碼的聯(lián)系: 之前我們定義過std_msgs::Int8 msg,msg即是類Int8的對象.我們通過查看roswiki http://docs.ros.org/api/std_msgs/html/msg/Int8.html 得知Int8包含類型為int8的成員變量data,所以我們通過msg.data使用這個成員.
寫好文件后退出保存,打開之前建立的CMakeLists.txt文件.輸入以下內(nèi)容.
project(class_test)
cmake_minimum_required(VERSION 2.8)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAG} -std=c++11 -Wall")
add_executable(classBasic classBasic.cpp)
這基本上算是一個最簡單的CMakeLists.txt文件了.CMakeLists.txt是用來編譯C++文件的.
第一行表明了項目名稱.
第二行輸入CMake使用的最小版本號,一般是2.8以及以上.
第三行設(shè)定編譯器.使用c++11.雖然我們的項目沒用到c++11但是考慮到如今c++已經(jīng)被普遍使用了,所以最好加上.我們在ROS的CMakeLists里注釋過這個內(nèi)容add_compile_options(-std=c++11)達到的也是使用c++11編譯的效果.
第四行指定要編譯的文件.要編譯的文件是classBasic.cpp,編譯后的可執(zhí)行文件名字叫classBasic.
寫完上面的內(nèi)容后,保存退出.
在terminal中cd 到classTest這個文件夾輸入下面的內(nèi)容
mkdir build
cd build
cmake ..
make
第一二行命令創(chuàng)建一個叫build的文件夾并進入
第三行命令使用是使用cmake命令并通過..表示使用上一個文件夾的CMakeLists.txt.執(zhí)行這行命令之后我們寫的CMakeLists就會產(chǎn)生一系列的文件在build中,其中一個是Makefile.其他的這里不作介紹
第四行命令是使用makefile.makefile的作用就是直接編譯你在CMakeLists里設(shè)定好的文件了.
建立一個build文件夾不是必須的但是推薦,因為你看到build里有一系列編譯CMakeLists.txt里產(chǎn)生的文件,你以后要刪除或者修改他們會比較方便,不至于和其他文件混在一起.
執(zhí)行完上面的命令后,你會看到多了一個叫classBasic的文件沒有后綴,這就是我們的可執(zhí)行二進制文件了.使用./classBasic執(zhí)行后得到下面的輸出
we create a poor phd class
girlFriendnNumber is 0
hairNumber is 101
use pointer, hair number is 101
請對應(yīng)源代碼一行行查看輸出為何如此.
上一章我們說過有這樣一段話msgs_header.stamp調(diào)用stamp,stamp.sec調(diào)用sec得到epoch的時間,那么msgs_header.stamp.sec就可以獲取當前的時間,秒為單位.寫段話之前我們創(chuàng)建了Header的對象msg_header,并通過ros wiki知道了該對象包含數(shù)據(jù)成員stamp,stamp包含數(shù)據(jù)成員sec,然后我們我們可以用這種msg_header.stamp.sec來調(diào)用sec這個數(shù)據(jù)成員.這種數(shù)據(jù)之間看起來的連續(xù)性具體是怎么實現(xiàn)的呢?
咱們在之前創(chuàng)建的classTest文件夾下再創(chuàng)建一個新的文件叫 classBasic2.cpp.并輸入下面的內(nèi)容.
#include <iostream>
class poorPhd{
public:
/*define constructor*/
poorPhd(){
std::cout<<"we create a poor phd class"<<std::endl;
}
/*public member variable*/
int hairNumber = 100;
/*public member function*/
int getGirlFriendNumber(){
return girlFriendNumber;
}
private:
/*private member variable*/
int girlFriendNumber = 0;
};
class master1 {
public:
/*define constructor*/
master1(){
std::cout<<"we create a master class"<<std::endl;
}
/*member variable*/
poorPhd future;
};
int main(){
/*define the object*/
master1 mStudent1;
/*use inheritance*/
std::cout<<"hairNumber of master student 1 is "<<mStudent1.future.hairNumber<<std::endl;
}
poorPhd類和上一個文件完全一樣,我們新添加了一個類叫master1.master1同樣有一個構(gòu)造函數(shù).另外它有一個成員變量,這個成員變量是poorPhd類型的對象future.那么在main函數(shù)中,定義了master1的對象mStudent1.咱們就可以用mStudent1.future調(diào)用變量future,再由于future是poorPhd類型的變量,所以可以用future.hairNumber調(diào)用hairNumber.連在一起就可以通過定義msater1的對象卻最終調(diào)用了poorPhd的成員變量了.
保存退出后,在CMakeLists.txt中添加下面的內(nèi)容.
add_executable(classBasic2 classBasic2.cpp)
terminal中進入classTest/build文件加輸入
cmake ..
make
這時候就多了一個二進制文件classBasic2,執(zhí)行該二進制文件你會看到
we create a poor phd class
we create a master class
hairNumber of master student 1 is 100
從這個輸入可以看出,創(chuàng)建master1的對象mStudent1的時候c++會首先初始化它的成員變量,所以咱們先得到的是create a poor phd class,之后再調(diào)用了構(gòu)造函數(shù).
類還有很多很多的內(nèi)容,就靠大家自己取學(xué)習(xí)了,咱們這兒只是簡單地介紹了和前面的代碼聯(lián)系的部分.
命名空間(namespace)
你肯定使用過命名空間,基本上每一個寫c++的人都會用過using namespace std這條語句.這條語句代表使用命名空間std.達到的效果是,例如你要使用cout語句在屏幕上打印什么東西,如果沒有std,你需要輸入的是
std::cout<<"....."<<std::endl;
如果你使用了using namespace std這條語句,那么你就只需要下面的內(nèi)容打印語句
cout<<"...."<<endl;
但是你如果沒寫過大型程序的話,可能沒有機會自己寫過命名空間.命名空間一般是用來避免重命名的.大型的庫里面一般定義了很多類,無數(shù)的函數(shù).不同的大型的庫之間很可能會有函數(shù)甚至類的命名重復(fù),這會造成很大的麻煩.
namespace的命名語法也很簡單
namespace name{
//內(nèi)容
}
下面這個程序簡單地展示了兩個命名空間里定義相同名字的類,并分別使用兩個類的簡單程序.
#include <iostream>
/*define a phd namespace*/
namespace phd {
/*define a student class in phd namespace*/
class student{
public:
student(){
std::cout<<"create a student class in phd namespace"<<std::endl;
}
int graduateYear = 5;
int hairNumber = 100;
};
}
/*define a master namespace*/
namespace master{
/*define a student class in master namespace*/
class student{
public:
student(){
std::cout<<"create a student class in master namespace"<<std::endl;
}
int graduateYear = 2;
int hairNumber = 10000;
};
}
int main(){
/*create an object of student class, in phd namespace*/
phd::student phdStudent;
/*create an object of student class, in master namespace*/
master::student masterStudent;
std::cout<<"phd normally graduate in "<<phdStudent.graduateYear<<" years"<<std::endl;
std::cout<<"master normally graduate in "<<masterStudent.graduateYear<<" years"<<std::endl;
}
上面的這個程序定義了兩個命名空間,一個叫phd,一個叫master,這兩個命名空間擁有一個類,類名都叫student.
定義命名空間中的類的對象的方法是命名空間名::類名 對象名.::被稱為作用域符號(scope resolution operator).在main函數(shù)中我們定義了phd命名空間下的student類的對象phdStudent和master命名空間下的類student的對象masterStudenrt. 后面的兩行各自輸出了成員變量graduateYear.
在我們之前的ros程序中,遇到了兩個命名空間,一個是std_msgs,另一個是geometry_msgs.Int8, Float64等都是std_msgs這個命名空間下的類,PoseStamped等是geometry_msgs這個命名空間下的類.
回到上面的程序我們在定義完phd這個命名空間后,可以使用using namespace phd,這樣在main函數(shù)中我們可以不使用phd::來定義一個phd下的student類的對象,直接student phdStudent即可.同樣,如果我們添加using namespace master,我們也可以直接使用student masterStudent來定義msater命名空間下student類的對象.
但是如果在程序中同時添加了
using namespace phd;
using namespace master;
這時候你在main函數(shù)中寫student object_name就肯定會報錯.因為電腦無法知道你要使用的student類是屬于哪個命名空間的.所以一般為了圖方便,在我們確定沒有類名會重復(fù)時,我們添加using namespace ...這一行在定義完頭文件之后,這樣我們就可以省去在定義類時一直使用namespace_name::類名這種格式命名.但是有些時候如果兩個庫很有可能有相同的類名,就不要使用using namespace ...,不然很有可能造成程序的誤解.
寫好上面的程序后和咱們寫classBasic.cpp的過程完全一樣的步驟,創(chuàng)建CMakeLists.txt和一個build文件夾進行編譯.
可能有的讀者會問那如果命名空間的名字都重復(fù)了呢?你就刪掉其中一個程序把 = = ....
同樣,命名空間有的是學(xué)問,有興趣的同學(xué)自行研究.
模版(Template)
模版這個東西,你如果是c++的使用者,那必定也接觸過.為什么這么說呢?當你定義一個std::vector的時候,你就已經(jīng)使用了模版了.但是你可能沒自己寫過模版(這種情況好像和namespace有點相似).
模版是為了避免重復(fù)定義同樣功能的函數(shù)而開發(fā)的.
打個比方,你現(xiàn)在要實現(xiàn)平方一個數(shù)的函數(shù).很簡單,類似于下面這樣
#include <iostream>
int square(int a){
return a*a;
}
int main(){
double x = 5.3;
std::cout<<"the square of "<<x <<" is "<<square(x)<<std::endl;
}
這個程序有個很明顯的缺點,編寫函數(shù)或者使用變量時,都必須先指定類型,由于c++函數(shù)形參類型和返回值已經(jīng)指定為int類型了,你只能傳int類型進去,如果傳double類型的變量進去,變量會被強制轉(zhuǎn)換截斷為int類型.而且只能return整型的變量.所以你只能得到25.
基本的解決方法是函數(shù)的重載,即我可以命名相同的函數(shù)但是變量類型或者個數(shù)不同以實現(xiàn)對不同輸入的處理.類似于下面這樣
#include <iostream>
int square(int a){
return a*a;
}
double suqare(double a ){
return a*a;
}
int main(){
double x = 5.3;
std::cout<<"the square of "<<x <<" is "<<square(x)<<std::endl;
}
這樣調(diào)用square(x)時會自動匹配形參相同的函數(shù).我們可以得到5.3的平方.但是可以想象,如果我有很多不同類型的變量要傳入,我就得寫好多不同的除了變量類型不同,其他的一模一樣的函數(shù)了!有沒有一種方法,形參什么類型都是可以的呢?
模版應(yīng)運而生.模版的定義方式是
template <typename T>
或者
template <class T>
定義完之后后面緊跟要實現(xiàn)的函數(shù)或者是類.這個class不是我們之前理解的那種class了.這兒的class和typename作用完全一樣,表示定義了一個新的類型T.這個新的類型具體是什么不知道,要等我們具體使用時程序根據(jù)傳入的類型自行判斷.
咱們先上代碼,實現(xiàn)數(shù)字平方相同的功能.
#include <iostream>
template <typename T>
T square(T a){
return a*a;
}
int main(){
double x = 5.3;
std::cout<<"square of "<<x<<" is "<<square(x)<<std::endl;
}
現(xiàn)在你無論傳什么類型的數(shù)據(jù)進去,都會得到它的平方.sqaure指定的函數(shù)形參和返回值類型都為T.可以這樣理解,現(xiàn)在當我們傳入一個double類型的變量時,T就會自動變成double,傳入int時,T就自動變?yōu)閕nt.
下面來一個稍微復(fù)雜一點的例子的.實現(xiàn)兩個向量的相加(好像也不怎么復(fù)雜...). 向量在c++里是不能直接相加的.我們定義向量時要指定向量元素的類型.比如std::vector<int> a,std::vector<double> b等.和上一個例子一樣,為了避免傳入重載函數(shù),我們使用模版.代碼如下
#include <iostream>
#include <vector>
template <typename T, typename U>
U addVector(T vec1, U vec2){
U result;
if(vec1.size()!=vec2.size()){
std::cout<<"cannot add two vector, they must be the same length. Return a null vector"<<std::endl;
return result;
}
for(int i = 0; i<vec1.size(); i++){
result.push_back(vec1[i]+vec2[i]);
}
return result;
}
int main(){
std::vector<int> vec1 = {1,2,3};
std::vector<double> vec2 = {4.0,5.0,6.0};
auto addVec = addVector(vec1,vec2);
for(auto i:addVec)
std::cout<<i<<",";
std::cout<<std::endl;
}
我們的tempalte定義了兩個類型,一個叫U,一個叫T.為什么要定義兩個呢?因為前面說過模板定義的具體類型在使用時確定的,在主函數(shù)中我們要加兩個vector,一個是int類型的,作為第一個參數(shù)傳入addVector,那么T就會是std::vector<int>,而第二個參數(shù)是double類型的向量,作為第二個參數(shù)傳入函數(shù)后U就會相當于std::vector<double>,函數(shù)返回的類型也是U.
程序主函數(shù)第三行使用了auto這個關(guān)鍵字.使用c++11編譯才可使用auto.這個是很有用的關(guān)鍵字.a(chǎn)uto會自動分配被它定義的對象的類型,根據(jù)賦值的變量的類型.a(chǎn)ddVector返回的是U,在這個程序里也就是std::vector<double>了.那么auto會自動讓addVec稱為dpuble類型的vector.
主函數(shù)第四行的for循環(huán)采用的是有別于我們常用的for循環(huán)的形式.
for(auto i:addVec)
其中i:addVec的作用是把addVec中的元素依次賦值給i,這就要求i的類型得和addVec中的元素的類型相同,不過有auto的幫助,我們也就不用管這么多了,把i的類型定義為auto,那么程序會自動讓i成為addVec中要賦值給i的元素的類型,這兒也就是double了.
說了這么多,還沒到我們最初想講的,那就是類似于std::vector<int>和我們使用ros的時候定義的advertise<std_msgs::String>這種類型的語法是怎么來的?首先根據(jù)命名空間那兒的學(xué)習(xí)我們知道std肯定是代表命名空間的名字了,vector是一個類,而<int>則來源于模版.如果我們使用模版定義了一個類,則會出現(xiàn)類似的內(nèi)容.還是用簡單的square函數(shù)來舉例.我們來建立一個簡單的sqaure類.
#include <iostream>
template <typename T>
class square{
public:
T a;
/*constructor function will store _a*_a as public member a*/
square(T _a){
a = _a*_a;
}
};
int main(){
double x = 5.5;
square<double> test(x);
std::cout<<"the square of "<<x<<" is "<<test.a<<std::endl;
}
在聲明了模版之后緊接著我們聲明了一個類,類的公有成員函數(shù)是一個類型為T的值a.主函數(shù)中,在我們聲明模版下定義的類的對象時,我們需要在<>之中表明T的類型.再這之后才能定義對象.即普通的類的對象的定義格式如下
類名 對象名(構(gòu)造函數(shù)參數(shù))
模版下的類的對象定義的格式就是
類名<模版變量類型> 對象名(構(gòu)造函數(shù)參數(shù))
main函數(shù)第二行的這種定義方法,就類似于我們std::vector<int> ABC這種定義方法了,后者多的不過是在命名空間下定義了模版.然后再在模版下定義類.
總結(jié)
這一講我們粗略地涉及到c++中幾個簡單又龐雜的系統(tǒng),類,命名空間和模版,在我們平常使用的語法中多多少少都出現(xiàn)過他們的影子.只是我們自己不經(jīng)常定義罷了.學(xué)會使用他們對建立龐雜的代碼系統(tǒng)很有幫助.我們還介紹了最簡要的CMakeLists的需要包含的內(nèi)容.下一講咱們回到ROS.在這一講的基礎(chǔ)之上,講解在ros的類中發(fā)布/接收消息