C++中等价于java.lang.Object x = new Foo()的语句是什么?

8
什么是C++中 java.lang.Object x = new Foo() 的等效代码?
4个回答

22
没有C++中的等效物,尝试用C++编写Java是毫无意义的。话虽如此,我将从尽可能模仿任务特征和精神的角度来处理这个问题。我建议的每种方法都有缺点和限制。前两种方法并不真正符合C++的习惯用法,但了解它们非常重要,以便看到最后两种方法解决的问题。 1. C风格的void指针。 让我从最基本且最不实用的void指针开始:
void* foo = new Foo();

您可以使用new运算符将任何内容分配给void指针,因为new、放置new等操作始终返回void指针。这样做的缺点很明显:丢失所指对象的类型信息。首先,C++缺乏反射或任何询问对象的手段。您需要将类型信息记在脑海中,并使用来回转换进行强制转换才能实际使用它。由于没有类型安全的方法从void指针转换,可能会发生一些有趣的事情。

如果这是函数的返回类型:

void* foo = some_function( _arg0 );

任何使用你的代码的作者都需要弄清楚应该发生什么。不幸的是,他们认为应该发生的事情和你作为作者认为从函数中返回的应该发生的事情经常非常不同。
2. C语言风格的联合
如果你想限制自己只能使用N种支持的类型而不是java.lang.Object可以处理的无限类型,则有unions。这些可以在相同的内存空间上保存一组预定义的值类型,只要它们是POD数据类型。联合缺少两个非常重要的东西:知道分配了哪个值的能力和保存非POD类型的能力。这完全排除了它们与具有任何功能(例如std::string)的任何对象一起使用的可能性。
为了澄清上面实际意味着什么:
union myType{
    int a;
    char b[4];
};

如果我将“myType”的实例中的“b”部分的第一个字符设置为某个值,那么我也将该int的第一个字节设置为相同的值。在C++中,这些仅对内存黑客和极低级别的编程(嵌入式等)非常有用。它们不是惯用的C++。
3. 精通Boost::Any
现在,如果您真正想要一个“我可以持有任何东西”,那么请使用Boost::Any。它可以容纳任何对象,而不破坏很多有用的类型信息。Boost文档在其目的方面陈述得更好。摘自Any的介绍部分:
引用: 有时需要通用类型(指一般而言,而不是基于模板的编程):真正可变的变量,适应许多其他更具体类型的值,而不是C++的正常严格和静态类型。
想想Any解决了许多与void指针相关的问题,例如丢失关于包含对象的信息以及安全地转换为正确类型的能力。
4. 精通Boost::Variant Boost::Variant解决了联合的相同类型问题,而不会丢失对象信息。此外,它可以与非POD类型对象一起使用。正如文档所述,它最好:
典型的解决方案采用动态分配对象,然后通过一个通用基类类型(通常是虚基类Hen01或更危险的void*)对其进行操作。具体类型的对象可以通过多态下转换结构(例如dynamic_cast、boost::any_cast等)来检索。
然而,这种解决方案的缺点非常明显,原因如下:
1. 无法在编译时检测到下转换错误。因此,对下转换结构的不正确使用将导致仅在运行时可检测到的错误。 2. 新的具体类型的添加可能会被忽略。如果向层次结构中添加新的具体类型,则现有的下转换代码将继续按原样工作,完全忽略新类型。因此,程序员必须手动定位和修改许多位置的代码,这经常导致难以找到的运行时错误。

我认为你需要检查一下你的语法。 - CB Bailey
1
就个人而言,我认为这并不足以与给定的Java代码匹配,被描述为“等效”。在Java中,您可以安全地从“Object”转换为其他内容,并在转换无效时进行恢复。正如您所述,在C++中没有办法从“void*”动态转换。 - CB Bailey
1
@Fred,@wheaties:我完全不同意这个观点。首先,没有C++的等效物,因为C++中并非所有内容都是对象。如果这是一个常见问题解答,就需要明确说明。 - John Dibling
1
其次,如果你想在标准的C++中实现这个功能,你会首先实现一个抽象基类,然后派生出其他类。这样做可以与所提问的问题进行最直接的对比。但是这一点甚至没有提到。 - John Dibling
1
@rodrigob:我的意思是,当your_pointer的类型为void*时,这样做是非法的。 - CB Bailey
显示剩余6条评论

10

在C++中,没有直接的等价于 java.lang.Object x = new Foo(),因为并不是所有的东西都是对象。但是根据你想要使用的方式,你可以实现相同的目标。

在C++中,最接近java.lang.Object x = new Foo()的等价方式是使用抽象基类 (ABC)。ABC是一个被设计为其他类的基类的类。您可以通过至少提供一个纯虚拟成员函数来创建ABC,并且您可以使用以下语法指定它:

class Object
{
public:
  virtual int my_func() = 0; // The "= 0" means "pure virtual"
};

纯虚函数通常在基类中没有实现 (参见脚注*1)。无法创建抽象基类的实例:

int main()
{
  Object obj; // not possible because Object is an ABC
}
为了使用 ABC,您必须创建一个它的子类并实现派生类中的每个纯虚成员函数:
class Foo : public Object
{
public: 
  int my_func() { return 42; } // use of "virtual" is assumed here
};

现在,您可以创建一个Foo的实例,同时获取指向基类的指针:

int main()
{
  Object* my_obj = new Foo;
}

上面的代码使用智能指针等需要遵守通常的免责声明。为了清晰起见,我省略了此内容,但从现在开始我将使用 shared_ptr

您还可以获取到对 FooObject 引用,而无需担心slicing问题。

int main()
{
  Foo my_foo;
  Object& obj_ref = my_foo; // OK
}

关于析构函数和抽象基类(ABC)的重要提示。在实现ABC时,通常需要在基类中使用虚拟析构函数(脚注*2)。如果您不在基类中实现虚拟析构函数,则当您尝试通过基类指针delete一个对象时,会引发未定义行为,这是不好的。

   class Object
    {
    public:
      virtual int my_func() = 0;
    };
    class Foo : public Object
    {
    public: 
      int my_func() { return 42; } 
    };

    int main()
    {
      Object* obj = new Foo;
      delete obj;  // Undefined Behavior: Object has no virtual destructor
    }

基于我的实际经验,在实现ABC时,我发现唯一真正需要成为纯虚函数的成员函数是析构函数。我设计的ABC通常有许多虚方法不是纯虚的,然后只有一个虚析构函数。在我看来(可争议),这是设计ABC的好起点:将析构函数设为纯虚函数,在基类中保留最少量的非纯虚虚拟成员函数,并在基类中为纯虚析构函数提供实现。当按照这种方式进行设计时,你会发现你无法在实际代码中实现某些事情,这就是你要从这个设计中偏离的时候。


脚注:


*1 ) 基类可以在基类中为纯虚成员函数提供定义。但这不是常规做法,你可能这样做的原因有些超出了本文的范围。请注意,当你这样做时,标准中有一个特殊规则,规定你不能在声明中同时提供定义;它们必须分开写。例如:

class Object
{
public:
  virtual int my_funky_method() = 0;
  virtual bool is_this_ok() = 0 { return false; } // ERROR: Defn not allowed here
};

int Object::my_funky_method()
{
  return 43;
}

*2)有关虚析构函数的规则确实存在例外情况。本文不涉及这些内容,但更好的经验法则是:“基类的析构函数应该是公共且虚拟的,或者受保护且非虚拟的”。


4

没有等价物,因为Java从托管堆中分配对象,而C++在非托管内存中分配它们。Java中的对象由JVM跟踪自动垃圾回收使用标记和清除,而C++需要显式释放所有内存。

运行时环境根本不同,因为类似的语法看起来相似是一个陷阱。


C++并不要求显式释放所有内存,对象可以具有自动、静态或动态存储期。只有那些具有动态存储期的对象需要显式释放,而具有自动和静态存储期的对象的生命周期和内存是自动处理的。你甚至可以说delete x;显式销毁了对象,并且内存随之隐式释放。 - CB Bailey
@Charles - 谢谢你。C++中基于堆栈的RAII是另一个例子。我的回答是针对问题的背景(使用 new X; 创建对象)的,并且第二个句子在C ++行为的普遍性方面过于自信。 - Steve Townsend
Java使用标记-清除算法进行垃圾回收。我不确定是否应该使用引用计数这个术语,因为它会让人们认为它类似于Swift管理内存。 - AteszDude

-1
// Test this
namespace external
{
    template <class T>
    struct var
    {
    private:
        T *value;
    public:
        var(T *value)
        {
            this->value = value;
        };

        T operator &() {
            return this-> value;
        };
    };
}
#define var external::var<void *>
/**
  Use:
  var i = 0;
  cout << &i; // Output 0;
*/

感谢您提交答案。如果您能简要解释一下这个答案是如何解决原问题的,那么其他人更容易理解您的代码。 - Nick

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