การสร้างอินสแตนซ์วัตถุ C ++


113

ฉันเป็นโปรแกรมเมอร์ C ที่พยายามเข้าใจ C ++ บทช่วยสอนจำนวนมากแสดงให้เห็นถึงการสร้างอินสแตนซ์อ็อบเจ็กต์โดยใช้ข้อมูลโค้ดเช่น:

Dog* sparky = new Dog();

ซึ่งหมายความว่าในภายหลังคุณจะทำ:

delete sparky;

ซึ่งสมเหตุสมผล ตอนนี้ในกรณีที่ไม่จำเป็นต้องมีการจัดสรรหน่วยความจำแบบไดนามิกมีเหตุผลใดที่จะใช้สิ่งที่กล่าวมาแทน

Dog sparky;

และปล่อยให้ผู้ทำลายถูกเรียกเมื่อประกายไฟออกไปนอกขอบเขต?

ขอบคุณ!

คำตอบ:


166

ในทางตรงกันข้ามคุณควรชอบการจัดสรรสแต็กเสมอเท่าที่ตามหลักทั่วไปคุณไม่ควรมีใหม่ / ลบในรหัสผู้ใช้ของคุณ

อย่างที่คุณพูดเมื่อมีการประกาศตัวแปรบนสแตกตัวทำลายของมันจะถูกเรียกโดยอัตโนมัติเมื่อมันอยู่นอกขอบเขตซึ่งเป็นเครื่องมือหลักของคุณในการติดตามอายุการใช้งานของทรัพยากรและหลีกเลี่ยงการรั่วไหล

ดังนั้นโดยทั่วไปทุกครั้งที่คุณต้องจัดสรรทรัพยากรไม่ว่าจะเป็นหน่วยความจำ (โดยการเรียกใหม่) ตัวจัดการไฟล์ซ็อกเก็ตหรือสิ่งอื่นใดให้รวมไว้ในคลาสที่ตัวสร้างได้รับทรัพยากรและผู้ทำลายจะปล่อยทรัพยากรนั้น จากนั้นคุณสามารถสร้างออบเจ็กต์ประเภทนั้นบนสแต็กและคุณรับประกันได้ว่าทรัพยากรของคุณจะได้รับอิสระเมื่ออยู่นอกขอบเขต ด้วยวิธีนี้คุณไม่ต้องติดตามคู่ใหม่ / ลบทุกที่เพื่อให้แน่ใจว่าคุณจะหลีกเลี่ยงการรั่วไหลของหน่วยความจำ

ชื่อที่พบบ่อยที่สุดสำหรับสำนวนนี้คือRAII

นอกจากนี้ให้ดูคลาสตัวชี้อัจฉริยะที่ใช้ในการรวมตัวชี้ผลลัพธ์ในกรณีที่หายากเมื่อคุณต้องจัดสรรบางสิ่งด้วยสิ่งใหม่ภายนอกวัตถุ RAII เฉพาะ คุณส่งตัวชี้ไปยังสมาร์ทพอยน์เตอร์แทนซึ่งจะติดตามอายุการใช้งานของมันเช่นโดยการนับอ้างอิงและเรียกตัวทำลายเมื่อการอ้างอิงล่าสุดอยู่นอกขอบเขต ไลบรารีมาตรฐานมีstd::unique_ptrไว้สำหรับการจัดการตามขอบเขตอย่างง่ายและstd::shared_ptrซึ่งอ้างอิงการนับเพื่อใช้การเป็นเจ้าของร่วม

แบบฝึกหัดจำนวนมากแสดงให้เห็นถึงการสร้างอินสแตนซ์อ็อบเจ็กต์โดยใช้ข้อมูลโค้ดเช่น ...

ดังนั้นสิ่งที่คุณค้นพบก็คือบทช่วยสอนส่วนใหญ่ดูด ;) บทช่วยสอนส่วนใหญ่จะสอนวิธีปฏิบัติ C ++ ที่ไม่ดีให้กับคุณรวมถึงการเรียกใหม่ / ลบเพื่อสร้างตัวแปรเมื่อไม่จำเป็นและทำให้คุณมีเวลาติดตามตลอดอายุการใช้งานของการจัดสรรของคุณ


พอยน์เตอร์ดิบมีประโยชน์เมื่อคุณต้องการความหมายเหมือน auto_ptr (โอนความเป็นเจ้าของ) แต่ยังคงการดำเนินการแลกเปลี่ยนแบบไม่ขว้างและไม่ต้องการค่าใช้จ่ายในการนับการอ้างอิง อาจมีขอบ แต่มีประโยชน์
Greg Rogers

2
นี่เป็นคำตอบที่ถูกต้อง แต่เหตุผลที่ฉันไม่เคยติดนิสัยในการสร้างวัตถุบนสแต็กเป็นเพราะมันไม่เคยชัดเจนว่าวัตถุนั้นจะใหญ่แค่ไหน คุณเพียงแค่ขอข้อยกเว้นสแต็กล้น
dviljoen

3
Greg: แน่นอน แต่อย่างที่คุณพูดกรณีขอบ โดยทั่วไปควรหลีกเลี่ยงคำแนะนำที่ดีที่สุด แต่พวกเขาอยู่ในภาษาด้วยเหตุผลไม่ปฏิเสธสิ่งนั้น :) dviljoen: หากวัตถุมีขนาดใหญ่คุณจะรวมวัตถุนั้นไว้ในวัตถุ RAII ซึ่งสามารถจัดสรรบนสแต็กและมีตัวชี้ไปยังข้อมูลที่จัดสรรด้วยฮีป
jalf

3
@dviljoen: ไม่ฉันไม่. คอมไพเลอร์ C ++ ไม่ขยายวัตถุโดยไม่จำเป็น สิ่งที่เลวร้ายที่สุดที่คุณจะเห็นโดยทั่วไปคือการปัดเศษขึ้นเป็นผลคูณที่ใกล้ที่สุดของสี่ไบต์ โดยปกติแล้วคลาสที่มีตัวชี้จะใช้พื้นที่มากพอ ๆ กับตัวชี้ดังนั้นคุณจึงไม่ต้องเสียค่าใช้จ่ายใด ๆ ในการใช้งานสแต็ก
ม.ค.

20

แม้ว่าการมีสิ่งต่างๆในสแต็กอาจเป็นข้อดีในแง่ของการจัดสรรและการพ้นอัตโนมัติ แต่ก็มีข้อเสียอยู่บ้าง

  1. คุณอาจไม่ต้องการจัดสรรวัตถุขนาดใหญ่บน Stack

  2. ส่งแบบไดนามิก! พิจารณารหัสนี้:

#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;
}

สิ่งนี้จะพิมพ์ "B" มาดูกันว่าจะเกิดอะไรขึ้นเมื่อใช้ Stack:

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

สิ่งนี้จะพิมพ์ "A" ซึ่งอาจไม่เข้าใจง่ายสำหรับผู้ที่คุ้นเคยกับ Java หรือภาษาเชิงวัตถุอื่น ๆ เหตุผลก็คือคุณไม่มีตัวชี้ไปที่อินสแตนซ์Bอีกต่อไป แต่เป็นตัวอย่างของBถูกสร้างและคัดลอกไปยังตัวแปรประเภทaA

บางสิ่งอาจเกิดขึ้นโดยไม่ได้ตั้งใจโดยเฉพาะอย่างยิ่งเมื่อคุณยังใหม่กับ C ++ ใน C คุณมีพอยน์เตอร์ของคุณนั่นแหล่ะ คุณรู้วิธีใช้และพวกเขาก็ทำเช่นเดียวกันเสมอ ใน C ++ ไม่เป็นเช่นนั้น ลองนึกดูว่าจะเกิดอะไรขึ้นเมื่อคุณใช้ในตัวอย่างนี้เป็นอาร์กิวเมนต์สำหรับวิธีการสิ่งต่าง ๆ จะซับซ้อนมากขึ้นและมันสร้างความแตกต่างอย่างมากหากaเป็นประเภทAหรือA*หรือแม้กระทั่งA&(call-by-reference) การผสมผสานหลายอย่างเป็นไปได้และพวกเขาทั้งหมดมีพฤติกรรมที่แตกต่างกัน


2
-1: คนที่ไม่สามารถเข้าใจความหมายเชิงคุณค่าและความจริงง่ายๆที่ว่าพหุนามต้องการการอ้างอิง / ตัวชี้ (เพื่อหลีกเลี่ยงการแบ่งวัตถุ) ไม่ก่อให้เกิดปัญหาใด ๆ กับภาษา พลังของ c ++ ไม่ควรถือเป็นข้อเสียเปรียบเพียงเพราะชาวบ้านบางคนไม่สามารถเรียนรู้กฎพื้นฐานของมันได้
underscore_d

ขอบคุณสำหรับหัวขึ้น. ฉันมีปัญหาที่คล้ายกันกับวิธีการใช้วัตถุแทนตัวชี้หรือการอ้างอิงและมันเป็นเรื่องยุ่ง วัตถุมีตัวชี้อยู่ภายในและถูกลบเร็วเกินไปเนื่องจากการเผชิญปัญหานี้
BoBoDev

@underscore_d ฉันเห็นด้วย; ควรลบย่อหน้าสุดท้ายของคำตอบนี้ อย่าใช้ความจริงที่ว่าฉันแก้ไขคำตอบนี้เพื่อหมายความว่าฉันเห็นด้วยกับมัน ฉันไม่ต้องการให้มีการอ่านข้อผิดพลาดในนั้น
TamaMcGlinn

@TamaMcGlinn เห็นด้วย ขอบคุณสำหรับการแก้ไขที่ดี ฉันลบส่วนความคิดเห็นออก
UniversE

13

เหตุผลในการใช้ตัวชี้จะเหมือนกันทุกประการกับเหตุผลที่ต้องใช้ตัวชี้ใน C ที่จัดสรรด้วย malloc: ถ้าคุณต้องการให้วัตถุของคุณอยู่ได้นานกว่าตัวแปรของคุณ!

ขอแนะนำอย่างยิ่งว่าอย่าใช้ตัวดำเนินการใหม่หากคุณสามารถหลีกเลี่ยงได้ โดยเฉพาะอย่างยิ่งถ้าคุณใช้ข้อยกเว้น โดยทั่วไปแล้วการปล่อยให้คอมไพเลอร์เป็นอิสระจากวัตถุของคุณจะปลอดภัยกว่ามาก


13

ฉันเคยเห็นรูปแบบการต่อต้านนี้จากผู้ที่ไม่ค่อยได้รับ & ที่อยู่ของผู้ให้บริการ หากพวกเขาต้องการเรียกใช้ฟังก์ชันด้วยตัวชี้พวกเขาจะจัดสรรบนฮีปเสมอเพื่อให้ได้ตัวชี้

void FeedTheDog(Dog* hungryDog);

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

Dog goodDog;
FeedTheDog(&goodDog);

อีกทางเลือกหนึ่ง: เป็นโมฆะ FeedTheDog (Dog & hungryDog); หมาหมา; FeedTheDog (สุนัข);
Scott Langham

1
@ScottLangham จริง แต่นั่นไม่ใช่ประเด็นที่ฉันพยายามทำ บางครั้งคุณกำลังเรียกใช้ฟังก์ชันที่ใช้ตัวชี้ NULL ซึ่งเป็นทางเลือกเช่นหรืออาจเป็น API ที่มีอยู่ซึ่งไม่สามารถเปลี่ยนแปลงได้
Mark Ransom

7

ถือว่าฮีปเป็นอสังหาริมทรัพย์ที่สำคัญมากและใช้อย่างรอบคอบ กฎพื้นฐานทั่วไปคือการใช้ stack เมื่อใดก็ตามที่เป็นไปได้และใช้ heap เมื่อใดก็ตามที่ไม่มีวิธีอื่น ด้วยการจัดสรรวัตถุบนสแต็กคุณจะได้รับประโยชน์มากมายเช่น:

(1) คุณไม่จำเป็นต้องกังวลเกี่ยวกับการคลายสแต็กในกรณีที่มีข้อยกเว้น

(2) คุณไม่จำเป็นต้องกังวลเกี่ยวกับการกระจายตัวของหน่วยความจำที่เกิดจากการจัดสรรพื้นที่มากเกินความจำเป็นโดยตัวจัดการฮีปของคุณ


1
ควรคำนึงถึงขนาดของวัตถุด้วย กองมีขนาด จำกัด ดังนั้นวัตถุที่มีขนาดใหญ่มากควรได้รับการจัดสรร Heap
อดัม

1
@Adam คิดว่า Naveen ยังถูกต้องตรงนี้ ถ้าคุณสามารถวางวัตถุขนาดใหญ่บนสแต็กได้ให้ทำเพราะมันดีกว่า มีหลายเหตุผลที่คุณอาจทำไม่ได้ แต่ฉันคิดว่าเขาถูกต้อง
McKay

5

เหตุผลเดียวที่ฉันกังวลคือตอนนี้ Dog ได้รับการจัดสรรให้อยู่ในสแต็กแทนที่จะเป็นฮีป ดังนั้นหาก Dog มีขนาดเมกะไบต์คุณอาจมีปัญหา

หากคุณจำเป็นต้องไปเส้นทางใหม่ / ลบโปรดระวังข้อยกเว้น และด้วยเหตุนี้คุณจึงควรใช้ auto_ptr หรือ boost smart pointer ประเภทหนึ่งเพื่อจัดการอายุการใช้งานของวัตถุ


1

ไม่มีเหตุผลที่จะใหม่ (บนฮีป) เมื่อคุณสามารถจัดสรรบนสแต็กได้ (เว้นแต่ด้วยเหตุผลบางประการที่คุณมีสแต็กขนาดเล็กและต้องการใช้ฮีป

คุณอาจต้องการพิจารณาใช้ shared_ptr (หรือตัวแปรตัวใดตัวหนึ่ง) จากไลบรารีมาตรฐานหากคุณต้องการจัดสรรบนฮีป ซึ่งจะจัดการลบให้คุณเมื่อการอ้างอิงถึง shared_ptr หมดไปจากการมีอยู่แล้ว


มีหลายเหตุผลเช่นดูฉันตอบ
dangerousdave

ฉันคิดว่าคุณอาจต้องการให้วัตถุอยู่ได้นานกว่าขอบเขตของฟังก์ชัน แต่ดูเหมือนว่า OP จะไม่แนะนำว่าเป็นสิ่งที่พวกเขาพยายามทำ
Scott Langham

0

มีเหตุผลเพิ่มเติมที่ไม่มีใครกล่าวถึงคือทำไมคุณถึงเลือกที่จะสร้างวัตถุของคุณแบบไดนามิก วัตถุแบบไดนามิกที่ใช้ฮีปช่วยให้คุณสามารถใช้ประโยชน์จากความหลากหลายได้


2
คุณสามารถทำงานกับวัตถุที่ใช้สแต็กได้หลายรูปแบบเช่นกันฉันไม่เห็นความแตกต่างระหว่างสแต็ก / และฮีปที่จัดสรรอ็อบเจ็กต์ในเรื่องนี้ เช่น: void MyFunc () {Dog dog; ฟีด (สุนัข); } void Feed (สัตว์ & สัตว์) {auto food = animal-> GetFavouriteFood (); PutFoodInBowl (อาหาร); } // ในตัวอย่างนี้ GetFavouriteFood อาจเป็นฟังก์ชันเสมือนจริงที่สัตว์แต่ละตัวจะแทนที่ด้วยการนำไปใช้งานของมันเอง สิ่งนี้จะทำงานได้หลายรูปแบบโดยไม่มีฮีปที่เกี่ยวข้อง
Scott Langham

-1: ความหลากหลายต้องการเพียงการอ้างอิงหรือตัวชี้ ไม่เชื่อเรื่องพระเจ้าโดยสิ้นเชิงกับระยะเวลาการจัดเก็บข้อมูลของอินสแตนซ์ต้นแบบ
underscore_d

@underscore_d "การใช้คลาสฐานสากลหมายถึงต้นทุน: อ็อบเจ็กต์ต้องได้รับการจัดสรรฮีปให้เป็นโพลีมอร์ฟิก" - Bjarne Stroustrup stroustrup.com/bs_faq2.html#object
dangerousdave

ฮ่า ๆ ฉันไม่สนใจหรอกว่า Stroustrup พูดด้วยตัวเอง แต่นั่นเป็นคำพูดที่น่าอายอย่างไม่น่าเชื่อสำหรับเขาเพราะเขาผิด มันไม่ยากที่จะทดสอบด้วยตัวคุณเองคุณก็รู้: สร้างตัวอย่าง DerivedA, DerivedB และ DerivedC บนสแต็ก สร้างอินสแตนซ์ตัวชี้ที่จัดสรรแบบสแต็กไปยังฐานและยืนยันว่าคุณสามารถนั่งได้ด้วย A / B / C และใช้มันหลายแบบ ใช้ความคิดเชิงวิพากษ์และการอ้างอิงมาตรฐานในการอ้างสิทธิ์แม้ว่าจะมาจากผู้เขียนภาษาต้นฉบับก็ตาม ที่นี่: stackoverflow.com/q/5894775/2757035
underscore_d

ด้วยวิธีนี้ฉันมีวัตถุที่มี 2 ตระกูลที่แยกจากกันซึ่งมีวัตถุโพลีมอร์ฟิกเกือบ 1,000 ชิ้นพร้อมระยะเวลาการจัดเก็บอัตโนมัติ ฉันสร้างอินสแตนซ์อ็อบเจ็กต์นี้บนสแต็กและโดยอ้างถึงสมาชิกเหล่านั้นผ่านการอ้างอิงหรือพอยน์เตอร์มันได้รับความสามารถทั้งหมดของความหลากหลายมากกว่า ไม่มีสิ่งใดในโปรแกรมของฉันที่จัดสรรแบบไดนามิก / ฮีป (นอกเหนือจากทรัพยากรที่เป็นของคลาสที่จัดสรรสแต็ก) และจะไม่เปลี่ยนแปลงความสามารถของอ็อบเจ็กต์ของฉันด้วย iota
underscore_d

-4

ฉันมีปัญหาเดียวกันใน Visual Studio คุณต้องใช้:

yourClass-> classMethod ();

ค่อนข้างมากกว่า:

yourClass.classMethod ();


3
สิ่งนี้ไม่ตอบคำถาม คุณควรสังเกตว่ามีการใช้ไวยากรณ์ที่แตกต่างกันเพื่อเข้าถึงวัตถุที่จัดสรรบนฮีป (ผ่านตัวชี้ไปยังวัตถุ) และวัตถุที่จัดสรรบนสแต็ก
Alexey Ivanov
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.