写在前面
由于学习ROS2需要比较好的c++基础,因此在开始ros之前,先对一些c++的常用概念进行的复习,以更好地看懂其源码
我会大量使用C语言和python来类比c++的特性,以辅助理解
博客中部分程序来自赵虚左老师的ROS2教程
核心概念
类,是c++区别于c的最大概念,并围绕着类增加了非常多的方法和上层库,因此,本文章围绕着类,对各个概念进行联系和辨析
其他概念
- 基类中的虚函数,可以在派生类中被重写,重写时,可以使用override关键字显式标识,以说明该函数是对某个虚函数的重写(这不是必要行为,但十分推荐加上)
- 纯虚函数在基类中不声明函数体,但要加上=0,以说明这是一个纯虚函数
- 在类的方法中,用&和&&说明该方法的引用限定,左值引用限定&,右值引用限定&&,变量和常量等具有持久性的对象属于左值,临时对象等即将被销毁的对象属于右值
- 类中的非静态方法不能被直接调用,需要实例化后才能通过实例调用
- 命名空间和类中的public类型的static属性效果类似,区别在于同一命名空间可以在不同位置被多次声明,在编译时,它们会被合成为同一个命名空间,但是类不允许被重复定义;命名空间主要用于解决变量重名问题
具体程序举例1
下面的程序用到了私有继承和const常量成员函数
#include <iostream>
#include <list>
// 基类:提供基础操作
class List {
public:
void push_back(int val) { data.push_back(val); }
void pop_back() { data.pop_back(); }
int back() const { return data.back(); }
bool empty() const { return data.empty(); }
private:
std::list<int> data;
};
// 派生类:私有继承 List,仅复用其实现
class Stack : private List {
public:
// 栈接口(封装 List 的操作)
void push(int val) { List::push_back(val); } // 调用基类方法
void pop() { List::pop_back(); }
int top() const { return List::back(); }
bool empty() const { return List::empty(); }
};
int main() {
Stack s;
s.push(1);
s.push(2);
std::cout << "Top: " << s.top() << std::endl; // 输出 2
s.pop();
std::cout << "Top after pop: " << s.top() << std::endl; // 输出 1
std::cout << "Is empty? " << s.empty() << std::endl; // 输出 0(false)
// 以下调用会编译报错:基类 public 成员在派生类中变为 private
// s.push_back(3); // 错误!无法访问基类 List 的 public 方法
const Stack ss;
// 以下程序也会报错
// ss.push(1); // const对象只能调用const方法,但push是普通方法
}
类的派生
无论如何继承,,派生类的实例不能直接访问基类的 private 成员 (包括属性和方法),但可以通过调用基类提供的公有接口 (如 public 或 protected 方法)间接访问或修改基类的私有属性。
换而言之,基类通过public成员定义了其private成员的访问或修改方式,在基类之外的域,不允许直接修改private
-
公有继承(public 继承)
基类的 public 成员在派生类中仍为 public,protected 成员保持 protected,而 private 成员不可访问。
派生类的对象可以直接访问基类的 public 成员,派生类的子类可以访问基类的 public 和 protected 成员 -
私有继承(private 继承)
基类的所有成员(无论原访问权限)在派生类中均变为 private。
派生类的对象无法直接访问基类的任何成员,派生类的子类也无法继承这些成员的访问权限
若未显式指定继承方式,默认使用私有继承 -
保护继承(protected 继承)
基类的 public 成员在派生类中变为 protected,protected 成员保持 protected,而 private 成员不可访问。
派生类的对象无法直接访问基类成员,但派生类的子类可以访问这些成员(此时它们为 protected)
使用场景
- public 继承 :最常用,表示“是一个”(is-a)关系,符合面向对象设计原则(如“猫是动物”)
- private 继承 :表示“实现复用”,派生类仅借用基类的实现,不暴露其接口(如“汽车使用引擎”)。
- protected 继承 :较少使用,用于限制基类接口仅对派生类的子类可见。
常量成员函数
const对象或方法只能调用const对象或方法
在上面的程序中,ss可以调用.top(),但是不能调用.push()
具体程序举例2
用node派生类创建新ros节点,主要用到了成员初始化列表、模板函数、this指针
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
// 3-1.创建发布方;
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
// 3-2.创建定时器;
timer_ = this->create_wall_timer(500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
void timer_callback()
{
// 3-3.组织消息并发布。
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "发布的消息:'%s'", message.data.c_str());
publisher_->publish(message);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
};
类的初始化列表
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
}
private:
size_t count_;
};
如果转换成python,就是这样
# 基类 Node
class Node:
def __init__(self, name: str):
self.name = name
# 派生类 MinimalPublisher
class MinimalPublisher(Node):
def __init__(self):
super().__init__("minimal_publisher") # 调用基类构造函数
self.count = 0 # 成员变量初始化
二者效果类似,虽说不算完全等价,但是可以辅助理解
进一步的,除了默认初始化,我们还可以使用参数完成初始化
class Node {
public:
explicit Node(const std::string& name) : name_(name) {}
private:
std::string name_;
};
// 派生
class MinimalPublisher : public Node {
public:
MinimalPublisher(const std::string& a, int b)
: Node(a), // 调用基类构造函数,传入参数 a
count_(b) // 初始化派生类成员变量 count_
{}
private:
int count_;
};
模板函数
典型案例:交换两个同类型数据的值
// 定义一个函数模板
template <typename T> // 表示T是一个模板类型,在这里起到占位作用,遵循命名规范即可
void swap(T& a, T& b) { // 说明a和b都是T类型的
T temp = a;
a = b;
b = temp;
}
int main(void)
{
int a=1, b=2;
float c=3.3, d=4.4;
swap(a, b); // 隐式自动推导类型
swap<float>(c, d); // 显式说明参数类型,两种方法均可
}
此外,这里还用到了引用传递的方式(在变量后面直接加&),用引用传递比指针传递更简洁、直观,且避免了空指针等潜在问题
再往create_publisher的更深一层的实现看,它在Node类中定义接口,在Node外完成了函数的具体实现,用到了函数模板的默认参数,大体上类似于下面的程序
class Node{
public:
template <
typename star,
typename light = float,
typename T = int>
int create_publisher(const light& aaa, T bbb);
private:
int ids = 0;
};
template <
typename star,
typename light,
typename T>
int Node::create_publisher(const light& aaa, T bbb){
star num = 3;
std::cout << typeid(num).name() << std::endl;
std::cout << typeid(aaa).name() << std::endl;
std::cout << typeid(bbb).name() << std::endl;
std::cout << aaa << "has been changed! " << bbb << std::endl;
return 1;
}
int main(void)
{
Node cat;
char test_a = 5;
float test_b = 2.2;
b = cat.create_publisher<double>(test_b, test_a);
std::cout << b << std::endl;
}
虽然在Node中的定义用到了三个模板参数,但是由于默认参数的存在,使其在被使用时,最少只需要确定一个参数类型即可
注意: 有默认值的模板参数,必须放在无默认值的模板参数后面,以避免混淆
this指针
功能基本等价于python中的self,
虽然细节上不太一样,比如this可以用->直接调用父类的public对象,但是self不行,需要使用super().才可以,但是大体上是一样的
在程序示例中,用this指针调用了create_publisher方法和create_wall_timer方法,其查找逻辑为:
1. 编译器首先会在子类的作用域中查找该方法。
2. 如果子类中没有该方法的定义,则会在其直接父类中查找。
3. 如果父类也没有定义该方法,编译器会继续向上查找其父类的父类,直到找到最顶层的基类。
4. 如果在整个继承链中都没有找到该方法,编译器将报错,提示找不到该方法 。
因此,在示例程序中,由于Node的派生类MinimalPublisher没有对create_publisher等方法进行重写,因此,this调用的是父类Node中的具体实现