2010-03-22 74 views
18

最近在一個我是教學助理的課程中出現了這個問題。我們正在教導學生如何使用C++複製構造函數,而最初教授java的學生詢問您是否可以從另一個構造函數調用構造函數。我知道答案是否定的,因爲他們在課堂上使用迂腐的旗幟作爲他們的代碼,而舊標準對此沒有支持。我#2和其他網站發現這個使用new (this)建議僞造比如如下使用new(this)重新使用構造函數

class MyClass 
{ 
    private: 
     int * storedValue; 
    public: 
     MyClass(int initialValue = 0) 
     { 
      storedValue = new int(initialValue); 
     } 

     ~ MyClass() 
     { 
      delete storedValue; 
     } 

     MyClass(const MyClass &b) 
     { 
      new (this) MyClass(*(b.storedValue)); 
     } 

     int value() { 
      return *storedValue; 
     } 
}; 

這是非常簡單的代碼,顯然不通過重用構造保存任何代碼,但它只是一個例子。

我的問題是,如果這是甚至標準兼容,並且是否有任何邊緣情況下應該考慮,以防止這是聲碼?

編輯:我要指出,這對我來說很危險,但畢竟是從上看我真的不明白它比知道它如何能走壞越多。我只是想確保如果學生問到我可以指導他們爲什麼可以或不應該這樣做。爲了所有實際目的,我已經建議他們使用共享初始化方法。這是一個教學問題,而不是一個實際的項目。

回答

16

C++ 0x將引入語法以允許構造函數調用其他構造函數。

到那時,new(this)的一些案件,但不是全部。特別是,一旦在構造函數中,你的基類已經完全構建完成了。通過new(this)重建重新調用基礎構造函數而不調用基類析構函數,因此如果基類不期望這種類型的駭客問題,那麼可能會出現問題 - 而且它們可能不是。

爲清楚起見,舉個例子:

class Base 
{ 
public: 
    char *ptr; 
    MyFile file; 
    std::vector vect; 
    Base() 
    { 
     ptr = new char[1000]; 
     file.open("some_file"); 
    } 
    ~Base() 
    { 
     delete [] ptr; 
     file.close(); 
    } 
}; 

class Derived : Base 
{ 
    Derived(Foo foo) 
    { 
    } 
    Derived(Bar bar) 
    { 
     printf(ptr...); // ptr in base is already valid 
     new (this) Derived(bar.foo); // ptr re-allocated, original not deleted 
     //Base.file opened twice, not closed 
     // vect is who-knows-what 
     // etc 
    } 
} 

或者像他們說的「歡鬧隨之而來的」

3

除非你想調用父類的構造函數,否則我會建議創建一個私有的初始化方法。沒有理由不能在您的構造函數中調用共享初始化程序。

+1

這是h我通常做我的代碼。使用我上面描述的方法對我來說似乎很危險,但我不想告訴學生「不這樣做,因爲我不喜歡它的外觀」。但是,在實踐中,這是我通常處理這種情況的方式,也是我向學生們提出的第一個建議。 – 2010-03-22 20:40:14

1

,如果你有一個這樣的構造函數這不起作用:

class MyClass { 
public: 
    MyClass(const std::string & PathToFile) 
    : m_File(PathToFile.c_str()) 
    { 
    } 
private: 
    std::ifstream m_File; 
} 

原始的參數無法恢復,所以你不能調用從拷貝構造函數,構造函數。

+1

難道你不會在java的例子中有同樣的問題嗎? (即一個接受另一個MyClass的java構造函數在MyClass沒有String的情況下無法調用帶String的構造函數 – tony 2010-03-22 20:41:05

+0

你是對的,它也不會在那裏工作,猜測這讓我在旁邊的例子 – 2010-03-22 23:36:30

11

成員和基類將在進入構造函數體之前被初始化,然後在調用第二個構造函數時再次初始化。一般來說,這會導致內存泄漏並可能導致未定義的行爲。

所以答案是「不,這不是聲音代碼」。

+0

So這會爲基類提供額外的內存分配嗎?我假設它在你執行'new(this)Something()'時重新使用當前分配的空間。如果這個假設是錯誤的,那麼這是一個明顯的問題上面的代碼,謝謝 – 2010-03-22 20:45:27

+0

啊@。tony的回答有助於澄清你的意思,我現在明白了。 – 2010-03-22 20:46:27

+0

如果基類分配了什麼東西 class Base {char * ptr; Base(){ptr = new char [1000];}}; – tony 2010-03-22 20:46:51

5

這裏是C++的常見問題有什麼看法,在question,「燦類的一個構造函數調用另一個相同類的構造函數來初始化這個對象?「:

順便說一句,不要試圖通過放置新實現這個。有些人認爲他們可以在Foo::Foo(char)的身體內說new(this) Foo(x, int(x)+7)。然而這是壞的,壞的,壞的。請不要寫信給我,並告訴我它似乎工作在您的特定版本的特定編譯器上;這不好。構造函數在幕後做了一些小奇蹟,但那些糟糕的技巧在這些部分構造的位上執行。拒絕吧。

1

由於確切的代碼是寫的,它應該工作 - 雖然我無法想象爲什麼你會寫這樣的代碼。特別是,它取決於所有指針只用於引用單個int的事實。既然如此,爲什麼他們不把一個int 放在這個對象中,而不是使用一個指針來動態地分配int呢?總之,他們有什麼很長,效率低,但不顯著不同:

class MyClass { 
    int v; 
public: 
    MyClass(int init) : v(init) {} 
    int value() { return v; } 
}; 

不幸的是,分你嘗試從指針一些真正的用途(例如,分配不同數量的不同對象的內存)他們在放置新退出工作時使用的「技巧」 - 完全取決於每個對象都分配完全相同數量的內存的事實。既然你在每個分配中限制了完全相同的分配,爲什麼把這個分配放在堆中,而不是使它成爲對象本身的一部分呢?

說實話,的情況下它纔有意義。然而,我唯一能想到的就是分配空間很大,你在堆棧空間比堆棧空間多得多的環境中運行。

該代碼有效,但它僅在相當狹窄的特定情況下才有用。它並不像我推薦的那樣,成爲如何做事的例子。

+0

「但沒有顯着不同」 - 好吧,他的版本有一個錯誤的賦值運算符,而你的版本沒有。所以有一些區別;-) – 2010-03-22 22:51:34

+0

@Steve:好的,我會接受的。我可能應該說「與他顯然打算做的沒有明顯不同」。 – 2010-03-23 01:46:03

0

在我看來,即使在派生類的構造函數中也可以安全地使用new(this),如果你知道自己在做什麼。你只需要確保你的基類有一個虛構構造器(對於它的基類,一直沿着鏈)。例如:

#include <stdio.h> 
#include <new> 

struct Dummy {}; 

struct print 
{ 
    print(const char *message)     { fputs(message, stdout); } 
    print(const char *format, int arg1)   { printf(format, arg1); } 
    print(const char *format, int arg1, int arg2) { printf(format, arg1, arg2); } 
}; 
struct print2 : public print 
{ 
    print2(const char *message)     : print(message) {} 
    print2(const char *format, int arg1)   : print(format, arg1) {} 
    print2(const char *format, int arg1, int arg2) : print(format, arg1, arg2) {} 
}; 

class foo : public print 
{ 
    int *n; 
public: 
    foo(Dummy) : print("foo::foo(Dummy) {}\n") {} 
    foo() : print("foo::foo() : n(new int) {}\n"), n(new int) {} 
    foo(int n) : print("foo::foo(int n=%d) : n(new int(n)) {}\n", n), n(new int(n)) {} 
    int Get() const { return *n; } 
    ~foo() 
    { 
     printf("foo::~foo() { delete n; }\n"); 
     delete n; 
    } 
}; 

class bar : public print2, public foo 
{ 
public: 
    bar(int x, int y) : print2("bar::bar(int x=%d, int y=%d) : foo(x*y) {}\n", x, y), foo(x*y) {} 
    bar(int n) : print2("bar::bar(int n=%d) : foo(Dummy()) { new(this) bar(n, n); }\n", n), foo(Dummy()) 
    { 
     __assume(this); // without this, MSVC++ compiles two extra instructions checking if this==NULL and skipping the constructor call if it does 
     new(this) bar(n, n); 
    } 
    ~bar() 
    { 
     printf("bar::~bar() {}\n"); 
    } 
}; 

void main() 
{ 
    printf("bar z(4);\n"); 
    bar z(4); 
    printf("z.Get() == %d\n", z.Get()); 
} 

輸出:

bar z(4); 
bar::bar(int n=4) : foo(Dummy()) { new(this) bar(n, n); } 
foo::foo(Dummy) {} 
bar::bar(int x=4, int y=4) : foo(x*y) {} 
foo::foo(int n=16) : n(new int(n)) {} 
z.Get() == 16 
bar::~bar() {} 
foo::~foo() { delete n; } 

當然,你的運氣了,如果基類有常數*或引用成員(或者,如果你不能編輯包含基礎的文件類的聲明)。這將使它不可能在其中編寫一個虛構的構造函數 - 更不用說用「new(this)」,那麼你會將這些「常量」成員初始化兩次!這就是真正的事情,C++ 0x委託構造函數,真的可以派上用場。

請告訴我,如果還有關於此技術的其他內容仍然可能不安全或不可移植。 (編輯:我也意識到,也許在一個虛擬的類,虛擬函數表可能會被初始化兩次。這將是無害的,但效率低下,我需要嘗試一下,看看編譯代碼的樣子。 )

*如果您在基類中只有常量成員(並且沒有引用),那麼您並不完全沒有運氣。您可以確保所有常量成員的所有類都具有自己的虛構構造函數,以便基類的虛構構造函數可以依次調用它們。如果某些常量具有內置類型(如int),那麼您不幸運 - 這些將不可避免地被初始化(例如,常量int將被初始化爲零)。

編輯:

#include <stdio.h> 
#include <new> 

struct Dummy {}; 

struct print 
{ 
    print(const char *message)     { fputs(message, stdout); } 
    print(const char *format, int arg1)   { printf(format, arg1); } 
    print(const char *format, int arg1, int arg2) { printf(format, arg1, arg2); } 
}; 
struct print2 : public print 
{ 
    print2(const char *message)     : print(message) {} 
    print2(const char *format, int arg1)   : print(format, arg1) {} 
    print2(const char *format, int arg1, int arg2) : print(format, arg1, arg2) {} 
}; 

class FooBar : public print 
{ 
    int value; 
public: 
    FooBar() : print("FooBar::FooBar() : value(0x12345678) {}\n"), value(0x12345678) {} 
    FooBar(Dummy) : print("FooBar::FooBar(Dummy) {}\n") {} 
    int Get() const { return value; } 
}; 

class foo : public print 
{ 
    const FooBar j; 
    int *n; 
public: 
    foo(Dummy) : print("foo::foo(Dummy) : j(Dummy) {}\n"), j(Dummy()) {} 
    foo() : print("foo::foo() : n(new int), j() {}\n"), n(new int), j() {} 
    foo(int n) : print("foo::foo(int n=%d) : n(new int(n)), j() {}\n", n), n(new int(n)), j() {} 
    int Get() const { return *n; } 
    int GetJ() const { return j.Get(); } 
    ~foo() 
    { 
     printf("foo::~foo() { delete n; }\n"); 
     delete n; 
    } 
}; 

class bar : public print2, public foo 
{ 
public: 
    bar(int x, int y) : print2("bar::bar(int x=%d, int y=%d) : foo(x*y) {}\n", x, y), foo(x*y) {} 
    bar(int n) : print2("bar::bar(int n=%d) : foo(Dummy()) { new(this) bar(n, n); }\n", n), foo(Dummy()) 
    { 
     printf("GetJ() == 0x%X\n", GetJ()); 
     __assume(this); // without this, MSVC++ compiles two extra instructions checking if this==NULL and skipping the constructor call if it does 
     new(this) bar(n, n); 
    } 
    ~bar() 
    { 
     printf("bar::~bar() {}\n"); 
    } 
}; 

void main() 
{ 
    printf("bar z(4);\n"); 
    bar z(4); 
    printf("z.Get() == %d\n", z.Get()); 
    printf("z.GetJ() == 0x%X\n", z.GetJ()); 
} 

輸出:這裏是鏈接虛擬構造的一個例子,如果int值成爲類FooBar的內部const int的值將被打破

bar z(4); 
bar::bar(int n=4) : foo(Dummy()) { new(this) bar(n, n); } 
foo::foo(Dummy) : j(Dummy) {} 
FooBar::FooBar(Dummy) {} 
GetJ() == 0xCCCCCCCC 
bar::bar(int x=4, int y=4) : foo(x*y) {} 
foo::foo(int n=16) : n(new int(n)), j() {} 
FooBar::FooBar() : value(0x12345678) {} 
z.Get() == 16 
z.GetJ() == 0x12345678 
bar::~bar() {} 
foo::~foo() { delete n; } 

( 0xCCCCCCCC是什麼未初始化的內存在調試版本initalized。)

相關問題