Gọi constructor trong c ++ mà không cần mới


142

Tôi thường thấy rằng mọi người tạo các đối tượng trong C ++ bằng cách sử dụng

Thing myThing("asdf");

Thay vì điều này:

Thing myThing = Thing("asdf");

Điều này dường như hoạt động (sử dụng gcc), ít nhất là miễn là không có mẫu liên quan. Câu hỏi của tôi bây giờ, dòng đầu tiên có đúng không và nếu có thì tôi có nên sử dụng nó không?


25
Hoặc là hình thức là không có mới.
Daniel Daranas

13
Biểu mẫu thứ hai sẽ sử dụng hàm tạo sao chép nên không, chúng không tương đương.
Edward Strange

Tôi đã chơi một chút với nó, cách đầu tiên đôi khi dường như thất bại khi các mẫu được sử dụng với các hàm tạo không tham số ..
Nils

1
Ồ và tôi đã nhận được huy hiệu "Câu hỏi hay" cho điều đó, thật đáng xấu hổ!
Nils

Câu trả lời:


153

Cả hai dòng trên thực tế đều đúng nhưng làm những điều khác biệt tinh tế.

Dòng đầu tiên tạo một đối tượng mới trên ngăn xếp bằng cách gọi hàm tạo của định dạng Thing(const char*).

Cái thứ hai phức tạp hơn một chút. Nó cơ bản làm như sau

  1. Tạo một đối tượng kiểu Thingbằng cách sử dụng hàm tạoThing(const char*)
  2. Tạo một đối tượng kiểu Thingbằng cách sử dụng hàm tạoThing(const Thing&)
  3. Gọi ~Thing()đối tượng được tạo ở bước # 1

7
Tôi đoán các loại hành động này được tối ưu hóa và do đó, không khác biệt đáng kể về khía cạnh hiệu suất.
M. Williams

14
Tôi không nghĩ rằng các bước của bạn là hoàn toàn đúng. Thing myThing = Thing(...)không sử dụng toán tử gán, nó vẫn được tạo bản sao giống như nói Thing myThing(Thing(...))và không liên quan đến cấu trúc mặc định Thing(chỉnh sửa: bài đăng sau đó đã được sửa)
AshleyBrain

1
Vì vậy, bạn có thể nói rằng dòng thứ hai là không chính xác, vì nó lãng phí tài nguyên mà không có lý do rõ ràng. Tất nhiên, có thể việc tạo ra phiên bản đầu tiên là có chủ ý đối với một số tác dụng phụ, nhưng điều đó thậm chí còn tồi tệ hơn (về mặt phong cách).
MK.

3
Không, @Jared, nó không được bảo đảm. Nhưng ngay cả khi trình biên dịch chọn thực hiện tối ưu hóa đó, thì hàm tạo sao chép vẫn cần có thể truy cập được (nghĩa là không được bảo vệ hoặc riêng tư), ngay cả khi nó không được thực hiện hoặc được gọi.
Rob Kennedy

3
Có vẻ như bản sao có thể bị xóa ngay cả khi hàm tạo sao chép có tác dụng phụ - xem câu trả lời của tôi: stackoverflow.com/questions/2722879/ Kẻ
Douglas Leeder

31

Tôi giả sử với dòng thứ hai bạn thực sự có nghĩa là:

Thing *thing = new Thing("uiae");

đó sẽ là cách tiêu chuẩn để tạo các đối tượng động mới (cần thiết cho liên kết động và đa hình) và lưu địa chỉ của chúng vào một con trỏ. Mã của bạn thực hiện những gì JaredPar đã mô tả, cụ thể là tạo hai đối tượng (một đối tượng đã vượt qua a const char*, đối tượng khác đã vượt qua a const Thing&) và sau đó gọi hàm hủy ( ~Thing()) trên đối tượng đầu tiên (đối tượng const char*).

Ngược lại, điều này:

Thing thing("uiae");

tạo một đối tượng tĩnh được hủy tự động khi thoát khỏi phạm vi hiện tại.


1
Thật không may, đó thực sự là cách phổ biến nhất để tạo các đối tượng động mới thay vì sử dụng auto_ptr, unique_ptr hoặc có liên quan.
Fred Nurk

3
Câu hỏi của OP là chính xác, câu trả lời này hoàn toàn liên quan đến một vấn đề khác (xem câu trả lời của @ JaredPar)
Silmathoron

21

Trình biên dịch có thể tối ưu hóa hình thức thứ hai thành hình thức đầu tiên, nhưng nó không phải.

#include <iostream>

class A
{
    public:
        A() { std::cerr << "Empty constructor" << std::endl; }
        A(const A&) { std::cerr << "Copy constructor" << std::endl; }
        A(const char* str) { std::cerr << "char constructor: " << str << std::endl; }
        ~A() { std::cerr << "destructor" << std::endl; }
};

void direct()
{
    std::cerr << std::endl << "TEST: " << __FUNCTION__ << std::endl;
    A a(__FUNCTION__);
    static_cast<void>(a); // avoid warnings about unused variables
}

void assignment()
{
    std::cerr << std::endl << "TEST: " << __FUNCTION__ << std::endl;
    A a = A(__FUNCTION__);
    static_cast<void>(a); // avoid warnings about unused variables
}

void prove_copy_constructor_is_called()
{
    std::cerr << std::endl << "TEST: " << __FUNCTION__ << std::endl;
    A a(__FUNCTION__);
    A b = a;
    static_cast<void>(b); // avoid warnings about unused variables
}

int main()
{
    direct();
    assignment();
    prove_copy_constructor_is_called();
    return 0;
}

Đầu ra từ gcc 4.4:

TEST: direct
char constructor: direct
destructor

TEST: assignment
char constructor: assignment
destructor

TEST: prove_copy_constructor_is_called
char constructor: prove_copy_constructor_is_called
Copy constructor
destructor
destructor

Mục đích của các diễn viên tĩnh để làm trống là gì?
Stephen Cross

1
@Stephen Tránh cảnh báo về các biến không sử dụng.
Douglas Leeder

10

Rất đơn giản, cả hai dòng đều tạo đối tượng trên ngăn xếp, thay vì trên heap như 'mới'. Dòng thứ hai thực sự liên quan đến một cuộc gọi thứ hai đến một nhà xây dựng sao chép, vì vậy nó nên được tránh (nó cũng cần được sửa chữa như được nêu trong các ý kiến). Bạn nên sử dụng ngăn xếp cho các đối tượng nhỏ càng nhiều càng tốt vì nó nhanh hơn, tuy nhiên nếu các đối tượng của bạn sẽ tồn tại lâu hơn khung ngăn xếp, thì rõ ràng đó là lựa chọn sai.


Đối với những người không quen thuộc với sự khác biệt giữa các đối tượng khởi tạo trên ngăn xếp trái ngược với heap (đó là sử dụng mới và không sử dụng mới ), đây là một chủ đề tốt.
edmqkk

2

Lý tưởng nhất, một trình biên dịch sẽ tối ưu hóa thứ hai, nhưng nó không bắt buộc. Đầu tiên là cách tốt nhất. Tuy nhiên, điều khá quan trọng để hiểu sự khác biệt giữa stack và heap trong C ++, vì bạn phải quản lý bộ nhớ heap của riêng mình.


Trình biên dịch có thể đảm bảo rằng hàm tạo sao chép không có tác dụng phụ (như I / O) không?
Stephen Cross

@Stephen - không thành vấn đề nếu nhà xây dựng sao chép thực hiện I / O - xem câu trả lời của tôi stackoverflow.com/questions/2722879/ chủ đề
Douglas Leeder

Ok, tôi thấy, trình biên dịch được phép biến biểu mẫu thứ hai thành dạng thứ nhất và do đó tránh được cuộc gọi đến hàm tạo sao chép.
Stephen Cross

2

Tôi đã chơi một chút với nó và cú pháp có vẻ khá lạ khi một hàm tạo không có đối số. Để tôi lấy một ví dụ:

#include <iostream> 

using namespace std;

class Thing
{
public:
    Thing();
};

Thing::Thing()
{
    cout << "Hi" << endl;
}

int main()
{
    //Thing myThing(); // Does not work
    Thing myThing; // Works

}

Vì vậy, chỉ cần viết Thing myThing w / o ngoặc thực sự gọi hàm tạo, trong khi Thing myThing () làm cho trình biên dịch bạn muốn tạo một con trỏ hàm hoặc một cái gì đó ?? !!


6
Đây là một sự mơ hồ cú pháp nổi tiếng trong C ++. Khi bạn viết "int rand ()", trình biên dịch không thể biết bạn có nghĩa là "tạo một int và khởi tạo mặc định" hay "khai báo hàm rand". Quy tắc là nó chọn cái sau bất cứ khi nào có thể.
jpalecek

1
Và đây, folks, là phân tích phật ý nhất .
Marc.2377

2

Nối vào câu trả lời của JaredPar

1 ctor thông thường, 2-function-like-ctor với đối tượng tạm thời.

Biên dịch nguồn này ở đâu đó tại đây http://melpon.org/wandbox/ với các trình biên dịch khác nhau

// turn off rvo for clang, gcc with '-fno-elide-constructors'

#include <stdio.h>
class Thing {
public:
    Thing(const char*){puts(__FUNCTION__ );}
    Thing(const Thing&){puts(__FUNCTION__ );}   
    ~Thing(){puts(__FUNCTION__);}
};
int main(int /*argc*/, const char** /*argv*/) {
    Thing myThing = Thing("asdf");
}

Và bạn sẽ thấy kết quả.

Từ ISO / IEC 14882 2003-10-15

8,5, phần 12

Công trình thứ 1, 2 của bạn được gọi là khởi tạo trực tiếp

12.1, phần 13

Một chuyển đổi loại ký hiệu chức năng (5.2.3) có thể được sử dụng để tạo các đối tượng mới của loại. [Lưu ý: Cú pháp trông giống như một cuộc gọi rõ ràng của nhà xây dựng. ] ... Một đối tượng được tạo theo cách này không được đặt tên. [Lưu ý: 12.2 mô tả thời gian tồn tại của các đối tượng tạm thời. ] [Lưu ý: các lệnh gọi hàm tạo rõ ràng không mang lại giá trị, xem 3.10. ]


Nơi để đọc về RVO:

12 Hàm thành viên đặc biệt / 12.8 Sao chép các đối tượng lớp / Phần 15

Khi các tiêu chí nhất định được đáp ứng, việc triển khai được phép bỏ qua cấu trúc sao chép của một đối tượng lớp, ngay cả khi hàm tạo sao chép và / hoặc hàm hủy cho đối tượng có tác dụng phụ .

Tắt nó với cờ trình biên dịch từ bình luận để xem hành vi sao chép như vậy)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.