【C/C++】各种概念联系及辨析(ROS2基础)

写在前面

由于学习ROS2需要比较好的c++基础,因此在开始ros之前,先对一些c++的常用概念进行的复习,以更好地看懂其源码
我会大量使用C语言和python来类比c++的特性,以辅助理解

博客中部分程序来自赵虚左老师的ROS2教程

核心概念

类,是c++区别于c的最大概念,并围绕着类增加了非常多的方法和上层库,因此,本文章围绕着类,对各个概念进行联系和辨析

其他概念

  1. 基类中的虚函数,可以在派生类中被重写,重写时,可以使用override关键字显式标识,以说明该函数是对某个虚函数的重写(这不是必要行为,但十分推荐加上)
  2. 纯虚函数在基类中不声明函数体,但要加上=0,以说明这是一个纯虚函数
  3. 在类的方法中,用&和&&说明该方法的引用限定,左值引用限定&,右值引用限定&&,变量和常量等具有持久性的对象属于左值,临时对象等即将被销毁的对象属于右值
  4. 类中的非静态方法不能被直接调用,需要实例化后才能通过实例调用
  5. 命名空间和类中的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中的具体实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值