Lập trình đối tượng C ++


113

Tôi là một lập trình viên C đang cố gắng hiểu C ++. Nhiều hướng dẫn chứng minh việc tạo đối tượng bằng cách sử dụng một đoạn mã như:

Dog* sparky = new Dog();

điều này ngụ ý rằng sau này bạn sẽ làm:

delete sparky;

điều đó có ý nghĩa. Bây giờ, trong trường hợp cấp phát bộ nhớ động là không cần thiết, có lý do gì để sử dụng các điều trên thay vì

Dog sparky;

và để hàm hủy được gọi sau khi sparky ra khỏi phạm vi?

Cảm ơn!

Câu trả lời:


166

Ngược lại, bạn nên luôn thích phân bổ ngăn xếp, đến mức theo nguyên tắc chung, bạn không bao giờ nên có mới / xóa trong mã người dùng của mình.

Như bạn nói, khi biến được khai báo trên ngăn xếp, trình hủy của nó sẽ tự động được gọi khi nó vượt ra khỏi phạm vi, đây là công cụ chính của bạn để theo dõi thời gian tồn tại của tài nguyên và tránh rò rỉ.

Vì vậy, nói chung, mỗi khi bạn cần cấp phát một tài nguyên, cho dù đó là bộ nhớ (bằng cách gọi mới), các xử lý tệp, ổ cắm hoặc bất kỳ thứ gì khác, hãy bọc nó trong một lớp mà hàm tạo thu được tài nguyên và trình hủy giải phóng nó. Sau đó, bạn có thể tạo một đối tượng thuộc loại đó trên ngăn xếp và bạn được đảm bảo rằng tài nguyên của bạn sẽ được giải phóng khi nó vượt ra khỏi phạm vi. Bằng cách đó, bạn không phải theo dõi các cặp mới / xóa của mình ở khắp mọi nơi để đảm bảo tránh rò rỉ bộ nhớ.

Tên phổ biến nhất của thành ngữ này là RAII

Ngoài ra, hãy xem xét các lớp con trỏ thông minh được sử dụng để bọc các con trỏ kết quả trong những trường hợp hiếm hoi khi bạn phải cấp phát một thứ gì đó mới bên ngoài một đối tượng RAII chuyên dụng. Thay vào đó, bạn chuyển con trỏ tới một con trỏ thông minh, con trỏ này sau đó theo dõi thời gian tồn tại của nó, ví dụ bằng cách đếm tham chiếu và gọi hàm hủy khi tham chiếu cuối cùng vượt ra khỏi phạm vi. Thư viện chuẩn std::unique_ptrdành cho việc quản lý dựa trên phạm vi đơn giản và std::shared_ptrcó chức năng đếm tham chiếu để thực hiện quyền sở hữu chung.

Nhiều hướng dẫn chứng minh việc tạo đối tượng bằng cách sử dụng một đoạn mã như ...

Vì vậy, những gì bạn đã phát hiện ra là hầu hết các hướng dẫn đều tệ. ;) Hầu hết các hướng dẫn đều dạy bạn các phương pháp C ++ tệ hại, bao gồm gọi new / delete để tạo các biến khi không cần thiết và giúp bạn mất nhiều thời gian theo dõi thời gian tồn tại của các phân bổ của mình.


Con trỏ thô hữu ích khi bạn muốn ngữ nghĩa giống auto_ptr (chuyển quyền sở hữu), nhưng vẫn giữ nguyên hoạt động hoán đổi không ném và không muốn tính chi phí tham chiếu. Vỏ cạnh có thể, nhưng hữu ích.
Greg Rogers

2
Đây là một câu trả lời chính xác, nhưng lý do tôi không bao giờ có thói quen tạo các đối tượng trên ngăn xếp là vì nó không bao giờ hoàn toàn rõ ràng đối tượng đó sẽ lớn như thế nào. Bạn chỉ yêu cầu một ngoại lệ tràn ngăn xếp.
dviljoen

3
Greg: Chắc chắn. Nhưng như bạn nói, một trường hợp cạnh. Nói chung, tốt nhất nên tránh các con trỏ. Nhưng họ sử dụng ngôn ngữ là có lý do, không thể phủ nhận điều đó. :) dviljoen: Nếu đối tượng lớn, bạn bọc nó trong một đối tượng RAII, đối tượng này có thể được cấp phát trên ngăn xếp và chứa một con trỏ đến dữ liệu được cấp phát theo đống.
jalf 02/12/08

3
@dviljoen: Không, tôi không. Các trình biên dịch C ++ không làm phình các đối tượng một cách không cần thiết. Điều tồi tệ nhất mà bạn sẽ thấy thường là nó được làm tròn đến bội số gần nhất của bốn byte. Thông thường, một lớp chứa một con trỏ sẽ chiếm nhiều không gian như chính con trỏ, vì vậy bạn không tốn kém gì khi sử dụng ngăn xếp.
jalf

20

Mặc dù có những thứ trên ngăn xếp có thể là một lợi thế về mặt phân bổ và giải phóng tự động, nó có một số nhược điểm.

  1. Bạn có thể không muốn phân bổ các đối tượng lớn trên Ngăn xếp.

  2. Công văn động! Hãy xem xét mã này:

#include <iostream>

class A {
public:
  virtual void f();
  virtual ~A() {}
};

class B : public A {
public:
  virtual void f();
};

void A::f() {cout << "A";}
void B::f() {cout << "B";}

int main(void) {
  A *a = new B();
  a->f();
  delete a;
  return 0;
}

Điều này sẽ in "B". Bây giờ chúng ta hãy xem điều gì sẽ xảy ra khi sử dụng Stack:

int main(void) {
  A a = B();
  a.f();
  return 0;
}

Điều này sẽ in ra "A", có thể không trực quan đối với những người đã quen thuộc với Java hoặc các ngôn ngữ hướng đối tượng khác. Lý do là bạn không có con trỏ đến một phiên bản Bnào nữa. Thay vào đó, một thể hiện của Bđược tạo và sao chép vào abiến kiểu A.

Một số điều có thể xảy ra không theo chủ ý, đặc biệt là khi bạn mới làm quen với C ++. Trong C, bạn có con trỏ của bạn và đó là nó. Bạn biết cách sử dụng chúng và chúng LUÔN LUÔN giống nhau. Trong C ++ thì không phải như vậy. Chỉ cần tưởng tượng những gì sẽ xảy ra, khi bạn sử dụng trong ví dụ này như một tham số cho một phương pháp - mọi thứ trở nên phức tạp hơn và nó làm cho một sự khác biệt rất lớn nếu alà loại Ahoặc A*hoặc thậm chí A&(gọi-by-reference). Có thể có nhiều kết hợp và tất cả chúng đều hoạt động khác nhau.


2
-1: mọi người không thể hiểu ngữ nghĩa giá trị và thực tế đơn giản là tính đa hình cần tham chiếu / con trỏ (để tránh cắt đối tượng) không tạo nên bất kỳ loại vấn đề nào với ngôn ngữ. Sức mạnh của c ++ không nên được coi là một nhược điểm chỉ vì một số dân gian không thể học các quy tắc cơ bản của nó.
underscore_d

Cảm ơn cho những người đứng đầu lên. Tôi đã gặp vấn đề tương tự với phương thức lấy đối tượng thay vì con trỏ hoặc tham chiếu, và nó thật rắc rối. Đối tượng có con trỏ bên trong và chúng đã bị xóa quá sớm vì cách đối phó này.
BoBoDev

@underscore_d Tôi đồng ý; đoạn cuối của câu trả lời này nên được loại bỏ. Đừng coi thực tế là tôi đã chỉnh sửa câu trả lời này có nghĩa là tôi đồng ý với nó; Tôi chỉ không muốn những sai sót trong đó được đọc.
TamaMcGlinn

@TamaMcGlinn đã đồng ý. Cảm ơn vì bản chỉnh sửa tốt. Tôi đã loại bỏ phần ý kiến.
UniversE

13

Chà, lý do sử dụng con trỏ sẽ giống hoàn toàn với lý do sử dụng con trỏ trong C được cấp phát bằng malloc: nếu bạn muốn đối tượng của mình tồn tại lâu hơn biến của bạn!

Thậm chí, chúng tôi khuyến nghị KHÔNG sử dụng toán tử mới nếu bạn có thể tránh nó. Đặc biệt nếu bạn sử dụng ngoại lệ. Nói chung, sẽ an toàn hơn nhiều nếu để trình biên dịch giải phóng các đối tượng của bạn.


13

Tôi đã thấy kiểu chống đối này từ những người không hoàn toàn hiểu được nhà điều hành & address-of. Nếu họ cần gọi một hàm với một con trỏ, họ sẽ luôn phân bổ trên heap để họ nhận được một con trỏ.

void FeedTheDog(Dog* hungryDog);

Dog* badDog = new Dog;
FeedTheDog(badDog);
delete badDog;

Dog goodDog;
FeedTheDog(&goodDog);

Ngoài ra: void FeedTheDog (Dog & inheritDog); Chó chó; FeedTheDog (chó);
Scott Langham

1
@ScottLangham đúng, nhưng đó không phải là điểm tôi đang cố gắng thực hiện. Đôi khi bạn đang gọi một hàm lấy một con trỏ NULL tùy chọn chẳng hạn hoặc có thể đó là một API hiện có không thể thay đổi được.
Mark Ransom vào

7

Hãy coi đống này như một bất động sản rất quan trọng và sử dụng nó một cách thận trọng. Quy tắc ngón tay cái cơ bản là sử dụng ngăn xếp bất cứ khi nào có thể và sử dụng đống bất cứ khi nào không còn cách nào khác. Bằng cách phân bổ các đối tượng trên ngăn xếp, bạn có thể nhận được nhiều lợi ích như:

(1). Bạn không cần phải lo lắng về việc tháo cuộn ngăn xếp trong trường hợp ngoại lệ

(2). Bạn không cần phải lo lắng về sự phân mảnh bộ nhớ do trình quản lý heap của bạn phân bổ nhiều dung lượng hơn mức cần thiết.


1
Cần có một số xem xét về kích thước của đối tượng. Stack có kích thước giới hạn, vì vậy các đối tượng rất lớn phải được cấp phát Heap.
Adam

1
@Adam Tôi nghĩ rằng Naveen vẫn đúng ở đây. Nếu bạn có thể đặt một vật lớn trên ngăn xếp, thì hãy làm điều đó, vì nó tốt hơn. Có rất nhiều lý do mà bạn có thể không làm được. Nhưng tôi nghĩ anh ấy đúng.
McKay

5

Lý do duy nhất tôi lo lắng là Dog hiện được phân bổ trên ngăn xếp, thay vì đống. Vì vậy, nếu Dog có kích thước megabyte, bạn có thể gặp sự cố,

Nếu bạn cần đi theo lộ trình mới / xóa, hãy cảnh giác với các trường hợp ngoại lệ. Và vì điều này, bạn nên sử dụng auto_ptr hoặc một trong những loại con trỏ thông minh tăng cường để quản lý thời gian tồn tại của đối tượng.


1

Không có lý do gì để tạo mới (trên đống) khi bạn có thể phân bổ trên ngăn xếp (trừ khi vì lý do nào đó bạn có một ngăn xếp nhỏ và muốn sử dụng đống.

Bạn có thể muốn cân nhắc sử dụng shared_ptr (hoặc một trong các biến thể của nó) từ thư viện chuẩn nếu bạn muốn phân bổ trên heap. Điều đó sẽ xử lý việc xóa cho bạn khi tất cả các tham chiếu đến shared_ptr đã hết tồn tại.


Có rất nhiều lý do, ví dụ, hãy xem tôi trả lời.
nguy hiểm vào

Tôi cho rằng bạn có thể muốn đối tượng tồn tại lâu hơn phạm vi của hàm, nhưng OP dường như không đề xuất đó là những gì họ đang cố gắng làm.
Scott Langham

0

Có một lý do bổ sung mà chưa ai đề cập đến, tại sao bạn có thể chọn tạo động đối tượng của mình. Các đối tượng động, dựa trên đống cho phép bạn sử dụng tính đa hình .


2
Bạn cũng có thể làm việc với các đối tượng dựa trên ngăn xếp một cách đa hình, tôi không thấy bất kỳ sự khác biệt nào giữa các đối tượng được phân bổ ngăn xếp / và heap về mặt này. Vd: void MyFunc () {Con chó; Nuôi chó); } void Feed (Animal & animal) {auto food = animal-> GetFavouriteFood (); PutFoodInBowl (thức ăn); } // Trong ví dụ này GetFavouriteFood có thể là một hàm ảo mà mỗi con vật ghi đè bằng cách triển khai riêng của nó. Điều này sẽ hoạt động đa hình mà không liên quan đến đống.
Scott Langham

-1: đa hình chỉ yêu cầu một tham chiếu hoặc con trỏ; hoàn toàn không xác định được thời lượng lưu trữ của phiên bản bên dưới.
underscore_d

@underscore_d "Sử dụng một lớp cơ sở phổ quát có nghĩa là tốn kém: Các đối tượng phải được phân bổ theo đống để có thể đa hình;" - Bjarne Stroustrup stroustrup.com/bs_faq2.html#object
dangerousdave

LOL, tôi không quan tâm liệu bản thân Stroustrup có nói điều đó hay không, nhưng đó là một câu nói vô cùng đáng xấu hổ đối với anh ấy, bởi vì anh ấy đã sai. Không khó để tự mình kiểm tra điều này, bạn biết đấy: khởi tạo một số DerivedA, DerivedB và DerivedC trên ngăn xếp; cũng tạo ra một con trỏ được phân bổ ngăn xếp tới Base và xác nhận rằng bạn có thể đặt nó với A / B / C và sử dụng chúng một cách đa hình. Áp dụng tư tưởng phê bình và tham chiếu Tiêu chuẩn cho các tuyên bố, ngay cả khi chúng là của tác giả gốc của ngôn ngữ. Đây: stackoverflow.com/q/5894775/2757035
underscore_d

Nói theo cách này, tôi có một đối tượng chứa 2 họ riêng biệt gần 1000 mỗi đối tượng đa hình, với thời lượng lưu trữ tự động. Tôi khởi tạo đối tượng này trên ngăn xếp và bằng cách tham chiếu đến các thành viên đó thông qua tham chiếu hoặc con trỏ, nó đạt được đầy đủ khả năng đa hình đối với chúng. Không có gì trong chương trình của tôi được phân bổ động / theo đống (ngoài tài nguyên thuộc sở hữu của các lớp được phân bổ theo ngăn xếp) và nó không thay đổi khả năng của đối tượng của tôi bởi một iota.
underscore_d

-4

Tôi đã gặp vấn đề tương tự trong Visual Studio. Bạn phải sử dụng:

yourClass-> classMethod ();

hơn là:

yourClass.classMethod ();


3
Điều này không trả lời câu hỏi. Bạn cần lưu ý rằng người ta phải sử dụng cú pháp khác nhau để truy cập đối tượng được phân bổ trên heap (thông qua con trỏ tới đối tượng) và đối tượng được cấp phát trên ngăn xếp.
Alexey Ivanov
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.