機器人操作系統(tǒng)ROS:從入門到放棄(四) C++類,命名空間,模版,CMakeLists介紹

由于下一講要講到怎么在類中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ù)就行了.

  1. 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)容.

  1. ...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_msgsInt8, 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> astd::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ā)布/接收消息

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容