C++合适的指针成员初始化

4

我是一个C++的新手,之前从事Java相关工作。我的类原型设置了两个私有指针对象成员。

class DriveController : Controller {

public:

DriveController(
    int8_t portTL_, int8_t portTR_, int8_t portBL_, int8_t portBR_, 
    double wheelSize_, double baseSize_);

private:
// Internal chassis controller
okapi::ChassisControllerIntegrated *chassisController;
okapi::AsyncMotionProfileController *chassisMotionProfiler;

现在,在这个类的构造函数中,我使用API提供的工厂设计模式初始化这两个变量。这是唯一真正初始化这些类的方法。
DriveController::DriveController(
    int8_t portTL_, int8_t portTR_, int8_t portBL_, int8_t portBR_, 
    double wheelSize, double baseSize) 
{
    // Initialize port definitions
    portTL = portTL_;
    portTR = portTR_;
    portBL = portBL_;
    portBR = portBR_;

    // Create chassis
    auto chassis = okapi::ChassisControllerFactory::create(
        {portTL, portBL}, // Left motors
        {portTR, portBR}, // Right motors
        okapi::AbstractMotor::gearset::red, // torque gearset
        {wheelSize, baseSize} // wheel radius, base width
    );
    chassisController = &chassis;

    auto profiler = okapi::AsyncControllerFactory::motionProfile(
        1.0, 2.0, 10.0, *chassisController);
    chassisMotionProfiler = &profiler;
  }

我知道我在这里分配内存时做错了什么,因为当我尝试访问稍后调用的函数中的这些成员指针时,程序会出现“内存权限错误”。我正在考虑使用unique_ptr来存储对象,因为它们管理生命周期很好,但由于创建对象的唯一方法是通过工厂初始化器,我还没有找到一个好的方法来构造unique_ptr。
那么,初始化这些指针成员的正确方法是什么?

6
“内存许可错误”是因为chassisprofiler在范围结束时(构造函数)被销毁。当您尝试稍后访问它们的内存时,就没有对象了。至于初始化指针成员,除了您的Java背景外,是否有其他原因?如果没有,就不要使用指针,应该可以正常工作。 - me'
是的,我知道它们会被销毁,但有没有一种方法可以延长它们的生命周期?就像Java一样,有没有一种方法可以初始化指针成员,使其在类实例的生命周期内持久存在?如果不使用指针,那么应该使用什么来存储类中的对象?非常感谢! - user2300851
另外,我忘了提到,“ChassisControllerIntegrated”在复制时会提示由于它被隐式删除,我无法将chassis设置为工厂创建。 - user2300851
2
这里有两件奇怪的事情:首先,一个工厂返回对象而不是使用unique_ptr或类似的东西。然后,如果工厂返回了一个unique_ptr,你可以将其存储在类中。但是,如果工厂无法修复,你可以简单地使用一个普通实例而不是对象,并使用工厂产品初始化它。是的,这意味着需要复制,因此是值得怀疑的,但是现在工厂本身就有问题,因为它返回副本。 - Ulrich Eckhardt
2个回答

1
为了使您的指针与DriverController对象一样长寿,您可以使用std::unique_ptr而不是裸指针。
至于chassisController的构建,由于它不可复制,这个问题的解决方案可以使用C++17复制省略。
chassisController = std::unique_ptr<okapi::ChassisControllerIntegrated> { new okapi::ChassisControllerIntegrated( okapi::ChassisControllerFactory::create( ...) )};

关于性能分析器也是同样的情况。

无论如何,正如其他人所评论的,以及另一个工厂正在使用引用/值而不是指针,您可能更好地将控制器和性能分析器存储为值。但是为了将它们存储为值,您必须在构造函数的初始化程序列表中对它们进行初始化,如下:

DriveController::DriveController(
    int8_t portTL_, int8_t portTR_, int8_t portBL_, int8_t portBR_, 
    double wheelSize, double baseSize):
    // Initialize port definitions
    portTL{ portTL_},
    portTR{ portTR_},
    portBL{ portBL_},
    portBR{ portBR_},

    // Create chassis
    chassisController{ okapi::ChassisControllerFactory::create(
        {portTL, portBL}, // Left motors
        {portTR, portBR}, // Right motors
        okapi::AbstractMotor::gearset::red, // torque gearset
        {wheelSize, baseSize} // wheel radius, base width
    )},

    chassisMotionProfiler { okapi::AsyncControllerFactory::motionProfile(
        1.0, 2.0, 10.0, chassisController)}
  {
   // no need to do anything in the body
  }  

另一个非常重要的细节是,定义数据成员的顺序需要与构造函数中初始化的顺序相同,即由于我们使用 chassisController 来初始化 chassisMotionProfiler,因此需要在 chassisMotionProfiler 之前声明 chassisController


1
如果指针是必要的(而不仅仅是从使用Java中养成的习惯!),那么这就是正确的方法。绝对聪明的指针比原始指针更好!不过,我首先会质疑是否真的需要使用指针。 - Ulrich Eckhardt
@UlrichEckhardt 添加了如何将它们用作值的示例。 - Bob Bills

1
我要先说这段代码看起来非常像Java: 对象是“做事情的人”(控制器控制,配置文件进行配置)- 为什么不在需要时直接实现控制和配置呢?这可能会避免使用工厂的必要性。
但是忽略这一点,假设你确实需要这些点:
使用自定义删除器让你的工厂函数返回unique_ptr 正如评论中所提到的,你的工厂函数表现奇怪。它们似乎返回值类型为okapi::ChassisControllerIntegratedokapi::AsyncMotionProfileController(或可转换为这两种类型)- 一旦你获取它们的地址。但这意味着工厂函数总是返回相同的类型,这使得有工厂函数的意义失去了效果(工厂可以通过指向基类的指针返回某个层次结构内的任何类型的值)。如果是这种情况,那么,确实如@me'所说 - 当离开构造函数作用域时,创建的对象将被销毁。
如果你的工厂函数返回这两个类的指针,代码就可以工作,但这可能不是一个好主意,因为你需要正确地释放这两个指向对象的内存(甚至将它们发送到工厂函数以进行销毁)。
@BobBills提出了一种避免这种情况的方法,即将创建的两个指针包装在std::unique_ptr中。虽然这种方法可行,但只适用于可以朴素地释放它们的情况。
我建议您使工厂本身返回std::unique_ptr,并使用它们需要使用的特定删除函数。这样,您将真正不必担心删除 - 任何使用工厂的其他代码也不必担心。
构造函数代码将是:
DriveController::DriveController(
    int8_t portTL_, int8_t portTR_, int8_t portBL_, int8_t portBR_, 
    double wheelSize, double baseSize)
:
    portTL{ portTL_}, portTR{ portTR_},
    portBL{ portBL_}, portBR{ portBR_},

    chassisController { 
        okapi::ChassisControllerFactory::create(
            {portTL, portBL}, // Left motors
            {portTR, portBR}, // Right motors
            okapi::AbstractMotor::gearset::red, // torque gearset
            {wheelSize, baseSize} // wheel radius, base width
        )
    },

    chassisMotionProfiler { 
        okapi::AsyncControllerFactory::motionProfile(
        1.0, 2.0, 10.0, chassisController)
    }
{ }  

(与@BobBills的解决方案相同)- 好处在于可以安全地假定析构函数是平凡的:
DriveController::~DriveController() = default;

考虑非指针型替代方法

如果您的DeviceController代码可以提前确定所有不同类型的底盘控制器和配置文件控制器,您确实可以让工厂返回一个值 - std::variant,它可以保存任意几种固定类型中的单个值,例如std :: variant<int,double> 可以保存intdouble,但不能同时保存两者;并且它占用的存储空间略大于不同类型的最大存储空间。这样,您就可以完全避免使用指针,而DeviceController将具有底盘和配置文件控制器的非指针成员。

另一种避免使用指针的方法是使用std::any对两个成员控制器进行类型抹除: 如果工厂返回了这个,你将无法享受在基类上使用虚拟方法的好处,但如果你有知道应该获取哪种控制器类型的代码 - 它可以以类型安全的方式从std::any中获取。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接