เหตุใดฉันจึงควรใช้ตัวชี้แทนวัตถุเอง


1602

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

Object *myObject = new Object;

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

Object myObject;

หรือแทนที่จะใช้ฟังก์ชั่นสมมติว่าtestFunc()เป็นแบบนี้:

myObject.testFunc();

เราต้องเขียน:

myObject->testFunc();

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


405
ขอชื่นชมคุณสำหรับการซักถามการปฏิบัตินี้มากกว่าเพียงแค่ติดตามมัน พอยน์เตอร์ส่วนใหญ่ใช้บ่อยเกินไป
Luchian Grigore

120
หากคุณไม่เห็นเหตุผลในการใช้พอยน์เตอร์ ชอบวัตถุ ชอบวัตถุก่อน unique_ptr ก่อน shared_ptr ก่อนตัวชี้ดิบ
stefan

113
หมายเหตุ: ใน java ทุกอย่าง (ยกเว้นประเภทพื้นฐาน) เป็นตัวชี้ ดังนั้นคุณควรถามสิ่งที่ตรงกันข้าม: ทำไมฉันต้องมีวัตถุอย่างง่าย?
Karoly Horvath

119
โปรดสังเกตว่าใน Java ตัวชี้จะถูกซ่อนด้วยไวยากรณ์ ใน C ++ ความแตกต่างระหว่างตัวชี้และตัวที่ไม่ใช่ตัวชี้ถูกทำให้ชัดเจนในรหัส Java ใช้พอยน์เตอร์ทุกที่
Daniel Martín

216
ปิดกว้างเกินไปหรือ อย่างจริงจัง? โปรดคนทราบว่าวิธี Java ++ นี้ของการเขียนโปรแกรมเป็นเรื่องธรรมดามากและเป็นหนึ่งในปัญหาที่สำคัญที่สุดใน C ++ ชุมชน ควรได้รับการปฏิบัติอย่างจริงจัง
Manu343726

คำตอบ:


1573

มันโชคร้ายมากที่คุณเห็นการจัดสรรแบบไดนามิกบ่อยครั้ง นั่นแสดงให้เห็นว่ามีโปรแกรมเมอร์ C ++ ที่ไม่ดีเท่าไหร่

ในความรู้สึกคุณมีสองคำถามรวมเป็นหนึ่ง สิ่งแรกคือเมื่อใดที่เราควรใช้การจัดสรรแบบไดนามิก (โดยใช้new) ที่สองคือเมื่อเราควรใช้ตัวชี้?

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

การจัดสรรแบบไดนามิก

ในคำถามของคุณคุณได้สาธิตวิธีการสร้างวัตถุสองวิธี ความแตกต่างที่สำคัญคือระยะเวลาการเก็บข้อมูลของวัตถุ เมื่อทำObject myObject;ภายในบล็อกวัตถุจะถูกสร้างขึ้นด้วยระยะเวลาการจัดเก็บอัตโนมัติซึ่งหมายความว่ามันจะถูกทำลายโดยอัตโนมัติเมื่อมันออกนอกขอบเขต เมื่อคุณทำเช่นnew Object()วัตถุที่มีระยะเวลาการจัดเก็บข้อมูลแบบไดนามิกซึ่งหมายความว่ามันคงมีชีวิตอยู่จนกว่าคุณจะdeleteมัน คุณควรใช้ระยะเวลาเก็บข้อมูลแบบไดนามิกเมื่อคุณต้องการเท่านั้น นั่นคือคุณควรเสมอต้องการสร้างวัตถุที่มีระยะเวลาการจัดเก็บอัตโนมัติเมื่อคุณสามารถ

สองสถานการณ์หลักที่คุณอาจต้องการการจัดสรรแบบไดนามิก:

  1. คุณต้องการวัตถุที่จะอยู่ได้นานกว่าขอบเขตปัจจุบัน - วัตถุนั้นในตำแหน่งหน่วยความจำเฉพาะนั้นไม่ใช่สำเนาของมัน หากคุณไม่เป็นไรด้วยการคัดลอก / ย้ายวัตถุ (ส่วนใหญ่เวลาที่คุณควรจะเป็น) คุณควรชอบวัตถุอัตโนมัติ
  2. คุณต้องจัดสรรหน่วยความจำจำนวนมากซึ่งอาจเติมสแต็กได้ง่าย มันจะดีถ้าเราไม่ต้องกังวลกับเรื่องนี้ (ส่วนใหญ่คุณไม่ควรจะต้อง) เพราะมันอยู่นอกขอบเขตของ C ++ จริงๆ แต่น่าเสียดายที่เราต้องจัดการกับความเป็นจริงของระบบ เรากำลังพัฒนาเพื่อ

เมื่อคุณต้องการการจัดสรรแบบไดนามิกจริงๆคุณควรสรุปในตัวชี้สมาร์ทหรือประเภทอื่น ๆ ที่ดำเนินการRAII (เช่นคอนเทนเนอร์มาตรฐาน) ตัวชี้สมาร์ทให้ความหมายความเป็นเจ้าของของวัตถุที่จัดสรรแบบไดนามิก ลองดูstd::unique_ptrและstd::shared_ptrตัวอย่างเช่น หากคุณใช้อย่างเหมาะสมคุณสามารถหลีกเลี่ยงการจัดการหน่วยความจำของคุณเองได้เกือบทั้งหมด (ดูRule of Zero )

ตัวชี้

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

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

  2. คุณจำเป็นต้องมีความแตกต่าง คุณสามารถเรียกใช้ฟังก์ชัน polymorphically เท่านั้น (นั่นคือตามประเภทไดนามิกของวัตถุ) ผ่านตัวชี้หรือการอ้างอิงไปยังวัตถุ หากนั่นเป็นพฤติกรรมที่คุณต้องการคุณต้องใช้พอยน์เตอร์หรือการอ้างอิง ควรใช้การอ้างอิงอีกครั้ง

  3. คุณต้องการแสดงว่าวัตถุนั้นเป็นทางเลือกโดยอนุญาตให้nullptrส่งผ่านเมื่อวัตถุถูกละเว้น หากเป็นข้อโต้แย้งคุณควรใช้อาร์กิวเมนต์เริ่มต้นหรือฟังก์ชันเกินพิกัด มิฉะนั้นคุณควรใช้ประเภทที่ห่อหุ้มพฤติกรรมนี้เช่นstd::optional(แนะนำใน C ++ 17 - ด้วยการใช้มาตรฐาน C ++ ก่อนหน้านี้boost::optional)

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

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


83
"คุณต้องการวัตถุเพื่ออยู่ได้นานกว่าขอบเขตปัจจุบัน" - หมายเหตุเพิ่มเติมเกี่ยวกับเรื่องนี้: มีหลายกรณีที่ดูเหมือนว่าคุณต้องการวัตถุที่อยู่ได้นานกว่าขอบเขตปัจจุบัน แต่คุณไม่ต้องการ หากคุณวางวัตถุของคุณในเวกเตอร์ตัวอย่างวัตถุจะถูกคัดลอก (หรือย้าย) ลงในเวกเตอร์และวัตถุดั้งเดิมนั้นปลอดภัยที่จะทำลายเมื่อขอบเขตสิ้นสุดลง

25
จำไว้ว่า s / copy / move / ในหลาย ๆ ที่ตอนนี้ การกลับมาของวัตถุนั้นไม่ได้หมายความถึงการเคลื่อนไหว นอกจากนี้คุณควรทราบว่าการเข้าถึงวัตถุผ่านตัวชี้เป็นมุมฉากกับวิธีการสร้าง
Puppy

15
ฉันพลาดการอ้างอิงที่ชัดเจนถึง RAII ในคำตอบนี้ C ++ คือทั้งหมด (เกือบทั้งหมด) เกี่ยวกับการจัดการทรัพยากรและ RAII เป็นวิธีการทำบน C ++ (และปัญหาหลักที่พอยน์เตอร์ดิบสร้างขึ้น: Breaking RAII)
Manu343726

11
พอยน์เตอร์อัจฉริยะมีอยู่ก่อน C ++ 11 เช่น boost :: shared_ptr และ boost :: scoped_ptr โครงการอื่น ๆ มีความเท่าเทียมกัน คุณไม่สามารถรับความหมายของการย้ายและการมอบหมายของ std :: auto_ptr นั้นมีข้อบกพร่องดังนั้น C ++ 11 จะปรับปรุงสิ่งต่าง ๆ แต่คำแนะนำยังดี (และ nitpick เศร้าก็ไม่มากพอที่จะมีการเข้าถึงC ++ 11 คอมไพเลอร์ก็จำเป็นที่คอมไพเลอร์ทั้งหมดที่คุณอาจจะต้องการรหัสของคุณในการทำงานด้วยการสนับสนุน C ++ 11. ใช่, Oracle Solaris สตูดิโอ, ฉัน มองดูคุณ)
armb

7
@ MDMoore313 คุณสามารถเขียนObject myObject(param1, etc...)
user000001

173

มีหลายกรณีการใช้งานสำหรับตัวชี้

พฤติกรรมที่หลากหลาย สำหรับประเภท polymorphic พอยน์เตอร์ (หรือการอ้างอิง) ถูกใช้เพื่อหลีกเลี่ยงการแบ่ง:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

ความหมายอ้างอิงและหลีกเลี่ยงการคัดลอก สำหรับชนิดที่ไม่ใช่ polymorphic ตัวชี้ (หรือการอ้างอิง) จะหลีกเลี่ยงการคัดลอกวัตถุที่มีราคาแพง

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

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

การเข้าซื้อกิจการทรัพยากร การสร้างตัวชี้ไปยังทรัพยากรโดยใช้newโอเปอเรเตอร์เป็นการต่อต้านแบบใน c ++ สมัยใหม่ ใช้คลาสทรัพยากรพิเศษ (หนึ่งในคอนเทนเนอร์มาตรฐาน) หรือตัวชี้สมาร์ท ( std::unique_ptr<>หรือstd::shared_ptr<>) พิจารณา:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

เมื่อเทียบกับ

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

ตัวชี้แบบดิบควรใช้เป็น "มุมมอง" เท่านั้นและไม่ควรเกี่ยวข้องกับการเป็นเจ้าของไม่ว่าจะเป็นการสร้างโดยตรงหรือโดยปริยายผ่านค่าส่งคืน ดูเพิ่มเติมQ & A นี้จาก C ++ คำถามที่พบบ่อย

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


17
"การสร้างตัวชี้ไปยังทรัพยากรที่ใช้ประกอบการใหม่ที่เป็นรูปแบบการป้องกัน"ผมคิดว่าคุณยังสามารถเพิ่มศักยภาพในการที่จะมีตัวชี้ดิบสิ่งที่ตัวเองเป็นรูปแบบการป้องกัน ไม่เพียง แต่การสร้าง แต่ส่งผ่านพอยน์เตอร์พอยน์เตอร์เป็นอาร์กิวเมนต์หรือค่าส่งคืนที่บ่งบอกถึงการโอนความเป็นเจ้าของ IMHO เลิกใช้แล้วตั้งแต่unique_ptr/ ย้ายอรรถศาสตร์
dyp

1
@dyp tnx อัปเดตและอ้างอิงคำถามที่พบบ่อย C ++ ในหัวข้อนี้
TemplateRex

4
การใช้สมาร์ทพอยน์เตอร์ทุกหนทุกแห่งเป็นการต่อต้านรูปแบบ มีกรณีพิเศษไม่กี่กรณีที่สามารถนำมาใช้ได้ แต่ส่วนใหญ่แล้วเหตุผลเดียวกันกับการโต้แย้งสำหรับการจัดสรรแบบไดนามิก (อายุการใช้งานตามอำเภอใจ) โต้แย้งกับตัวชี้สมาร์ทปกติเช่นกัน
James Kanze

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

2
@TemplateRex ที่ดูเหมือนว่าโง่เล็กน้อยที่hun(b)ยังต้องมีความรู้เกี่ยวกับลายเซ็นเว้นแต่ว่าคุณจะสบายดีที่ไม่ทราบว่าคุณได้พิมพ์ผิดไปจนกว่าจะมีการคอมไพล์ ในขณะที่ปัญหาการอ้างอิงมักจะไม่ได้รับการรวบรวมในเวลารวบรวมและจะใช้ความพยายามมากขึ้นในการแก้ปัญหาถ้าคุณกำลังตรวจสอบลายเซ็นเพื่อให้แน่ใจว่าข้อโต้แย้งนั้นถูกต้องคุณจะสามารถดูว่าข้อโต้แย้งใด ๆ ดังนั้นบิตอ้างอิงจะกลายเป็นสิ่งที่ไม่มีปัญหา (โดยเฉพาะอย่างยิ่งเมื่อใช้ IDE หรือโปรแกรมแก้ไขข้อความที่แสดงลายเซ็นของฟังก์ชั่นที่เลือก) นอกจากนี้const&.
JAB

130

มีคำตอบที่ดีมากสำหรับคำถามนี้รวมถึงกรณีการใช้งานที่สำคัญของการประกาศล่วงหน้า, polymorphism เป็นต้น แต่ฉันรู้สึกว่าส่วนหนึ่งของ "วิญญาณ" ของคำถามของคุณไม่ได้รับคำตอบ - นั่นคือความหมายของไวยากรณ์ที่แตกต่างกันทั่วทั้ง Java และ C ++

ลองตรวจสอบสถานการณ์เปรียบเทียบทั้งสองภาษา:

Java:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

สิ่งที่ใกล้เคียงที่สุดกับสิ่งนี้คือ:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

มาดูวิธี C ++ ทางเลือก:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

วิธีที่ดีที่สุดในการคิดว่ามันคือ - มากหรือน้อย - Java (โดยปริยาย) จัดการตัวชี้ไปยังวัตถุในขณะที่ C ++ อาจจัดการตัวชี้ไปยังวัตถุหรือวัตถุเอง มีข้อยกเว้นสำหรับสิ่งนี้ - ตัวอย่างเช่นหากคุณประกาศประเภท Java "ดั้งเดิม" เป็นค่าจริงที่คัดลอกไม่ใช่ตัวชี้ ดังนั้น,

Java:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

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

นำจุดกลับบ้าน - Object * object = new Object()โครงสร้างเป็นจริงสิ่งที่ใกล้เคียงที่สุดกับความหมาย Java ทั่วไป (หรือ C # สำหรับเรื่องนั้น)


7
Object2 is now "dead": ฉันคิดว่าคุณหมายถึงmyObject1หรือแม่นยำthe object pointed to by myObject1กว่า
Clément

2
แน่นอน! เปลี่ยนโฉมใหม่เล็กน้อย
Gerasimos R

2
Object object1 = new Object(); Object object2 = new Object();รหัสไม่ดีมาก ตัวสร้างวัตถุใหม่หรือตัวที่สองที่สองอาจโยนและตอนนี้ object1 จะรั่วไหล หากคุณใช้ raw news คุณควรทำการล้อมรอบnewวัตถุ ed ใน RAII wrappers โดยเร็ว
PSkocik

8
แน่นอนมันจะเป็นถ้านี่เป็นรายการและไม่มีอะไรเกิดขึ้นรอบ ๆ โชคดีที่นี่เป็นเพียงตัวอย่างคำอธิบายที่แสดงให้เห็นว่าตัวชี้ใน C ++ ทำงานอย่างไรและหนึ่งในไม่กี่แห่งที่วัตถุ RAII ไม่สามารถทดแทนตัวชี้แบบดิบได้คือการศึกษาและเรียนรู้เกี่ยวกับตัวชี้แบบดิบ ...
Gerasimos R

80

อีกเหตุผลที่ดีที่จะใช้ตัวชี้จะเป็นสำหรับการประกาศไปข้างหน้า ในโครงการขนาดใหญ่พอที่พวกเขาสามารถเร่งเวลาในการรวบรวมได้


7
นี่คือการเพิ่มการผสมผสานของข้อมูลที่มีประโยชน์จริงๆดีใจที่คุณตอบมัน!
TemplateRex

3
std :: shared_ptr <T> ทำงานร่วมกับการประกาศไปข้างหน้าของ T. (std :: unique_ptr <T> ไม่ได้ )
berkus

13
@berkus: การทำงานกับการประกาศไปข้างหน้าของstd::unique_ptr<T> Tคุณเพียงแค่ต้องทำให้แน่ใจว่าเมื่อตัวทำลายของการstd::unique_ptr<T>เรียกTนั้นเป็นชนิดที่สมบูรณ์ โดยทั่วไปหมายถึงคลาสของคุณที่มีตัวstd::unique_ptr<T>ประกาศ destructor ในไฟล์ส่วนหัวและใช้ในไฟล์ cpp (แม้ว่าการนำไปใช้จะว่างเปล่าก็ตาม)
David Stone

โมดูลจะแก้ไขปัญหานี้หรือไม่
เทรเวอร์ Hickey

@TrevorHickey ความคิดเห็นเก่า ๆ ที่ฉันรู้ แต่จะตอบมันต่อไป โมดูลจะไม่ลบการพึ่งพา แต่ควรรวมถึงการพึ่งพาถูกมากเกือบฟรีในแง่ของต้นทุนประสิทธิภาพ นอกจากนี้หากการเร่งความเร็วทั่วไปจากโมดูลจะเพียงพอที่จะทำให้เวลาในการรวบรวมของคุณอยู่ในช่วงที่ยอมรับได้ก็จะไม่มีปัญหาอีกต่อไป
Aidiakapi

79

คำนำ

Java ไม่เหมือนกับ C ++ ตรงกันข้ามกับ hype เครื่อง Java hype ต้องการให้คุณเชื่อว่าเนื่องจาก Java มี C ++ เหมือนวากยสัมพันธ์จึงมีความคล้ายคลึงกับภาษา ไม่มีอะไรเพิ่มเติมจากความจริง ข้อมูลที่ผิดนี้เป็นส่วนหนึ่งของเหตุผลที่โปรแกรมเมอร์ Java ไปที่ C ++ และใช้ไวยากรณ์เหมือน Java โดยไม่เข้าใจความหมายของรหัส

ต่อไปเราไป

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

ไปในทางตรงกันข้ามจริง ฮีปนั้นช้ากว่ากองมากเนื่องจากกองซ้อนนั้นง่ายมากเมื่อเทียบกับกอง ตัวแปรหน่วยเก็บข้อมูลอัตโนมัติ (รู้จักกันในชื่อตัวแปรสแต็ก) มี destructors ที่เรียกว่าเมื่อพวกเขาออกจากขอบเขต ตัวอย่างเช่น:

{
    std::string s;
}
// s is destroyed here

ในทางตรงกันข้ามถ้าคุณใช้ตัวชี้การจัดสรรแบบไดนามิก destructor นั้นจะต้องถูกเรียกด้วยตนเอง deleteเรียกสิ่งนี้ว่า destructor

{
    std::string* s = new std::string;
}
delete s; // destructor called

สิ่งนี้ไม่เกี่ยวข้องกับnewไวยากรณ์ที่แพร่หลายใน C # และ Java พวกเขาจะใช้เพื่อวัตถุประสงค์ที่แตกต่างอย่างสิ้นเชิง

ประโยชน์ของการจัดสรรแบบไดนามิก

1. คุณไม่จำเป็นต้องรู้ขนาดของอาร์เรย์ล่วงหน้า

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

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

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

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

หมายเหตุด้านข้าง : หนึ่งข้อผิดพลาดที่ผู้เริ่มต้นทำคือการใช้อาร์เรย์ความยาวผันแปร นี่คือส่วนขยาย GNU และหนึ่งใน Clang เพราะสะท้อนส่วนขยายของ GCC จำนวนมาก ดังนั้นสิ่งต่อไปนี้ int arr[n]ไม่ควรเชื่อถือ

เนื่องจากฮีปมีขนาดใหญ่กว่าสแต็กมากจึงสามารถจัดสรร / จัดสรรใหม่หน่วยความจำได้มากตามที่เขา / เธอต้องการในขณะที่สแต็กมีข้อ จำกัด

2. อาร์เรย์ไม่ใช่ตัวชี้

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

คุณไม่สามารถกำหนดให้กับอาร์เรย์ได้ แต่เริ่มต้นได้เท่านั้น ตัวอย่างเช่น:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

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

3. ความแตกต่าง

Java และ C # มีสิ่งอำนวยความสะดวกที่ช่วยให้คุณสามารถจัดการกับวัตถุได้เช่นใช้asคำหลัก ดังนั้นหากใครบางคนต้องการที่จะปฏิบัติต่อEntityวัตถุเป็นPlayerวัตถุใคร ๆ ก็สามารถทำได้Player player = Entity as Player;สิ่งนี้มีประโยชน์มากถ้าคุณตั้งใจจะเรียกใช้ฟังก์ชั่นบนคอนเทนเนอร์ที่เป็นเนื้อเดียวกันซึ่งควรใช้กับประเภทที่ระบุเท่านั้น การทำงานสามารถทำได้ในลักษณะคล้ายกันด้านล่าง:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

ดังนั้นถ้าสามเหลี่ยมมีฟังก์ชั่นหมุนมันจะเป็นข้อผิดพลาดของคอมไพเลอร์ถ้าคุณพยายามเรียกมันบนทุกวัตถุของคลาส ใช้dynamic_castคุณสามารถจำลองasคำหลัก เพื่อความชัดเจนหากการร่ายไม่สำเร็จจะส่งคืนพอยน์เตอร์ที่ไม่ถูกต้อง ดังนั้นจึง!testเป็นชวเลขที่สำคัญสำหรับการตรวจสอบว่าtestเป็นโมฆะหรือตัวชี้ที่ไม่ถูกต้องซึ่งหมายถึงการโยนล้มเหลว

ประโยชน์ของตัวแปรอัตโนมัติ

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

  • มันเป็นข้อผิดพลาดได้ง่าย การจัดสรรหน่วยความจำด้วยตนเองเป็นสิ่งที่อันตรายและคุณมีแนวโน้มที่จะรั่วไหล หากคุณไม่ชำนาญในการใช้ดีบักเกอร์หรือvalgrind(เครื่องมือรั่วหน่วยความจำ) คุณอาจดึงผมออกจากหัว โชคดีที่ RAII สำนวนและตัวชี้อัจฉริยะช่วยบรรเทาปัญหานี้ได้เล็กน้อย แต่คุณต้องคุ้นเคยกับการฝึกฝนเช่น The Rule Of Three และ Rule Of Five มันเป็นข้อมูลจำนวนมากที่ต้องทำและผู้เริ่มต้นที่ไม่รู้หรือไม่สนใจจะตกหลุมพรางนี้

  • มันไม่จำเป็น แตกต่างจาก Java และ C # ที่มีการใช้newคำสำคัญทุกที่ใน C ++ คุณควรใช้เฉพาะเมื่อคุณต้องการ วลีทั่วไปจะไปทุกอย่างดูเหมือนว่าเป็นตอกถ้าคุณมีค้อน ในขณะที่ผู้เริ่มต้นที่เริ่มต้นด้วย C ++ จะกลัวพอยน์เตอร์และเรียนรู้การใช้ตัวแปรสแต็กตามนิสัย, Java และ C # โปรแกรมเมอร์เริ่มโดยใช้พอยน์เตอร์โดยไม่เข้าใจ! นั่นคือก้าวเท้าที่ผิดไป คุณต้องละทิ้งทุกสิ่งที่คุณรู้เพราะไวยากรณ์เป็นสิ่งหนึ่งการเรียนรู้ภาษาก็เป็นอีกสิ่งหนึ่ง

1. (N) RVO - Aka, (ตั้งชื่อ) การเพิ่มประสิทธิภาพมูลค่าส่งคืน

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

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


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

4
"เครื่อง Java hype อยากให้คุณเชื่อ" - อาจจะเป็นในปี 1997 แต่ตอนนี้เป็นแบบสมัยไม่มีแรงจูงใจในการเปรียบเทียบ Java กับ C ++ ในปี 2014
Matt R

15
คำถามเก่า ๆ แต่ในส่วนของรหัส{ std::string* s = new std::string; } delete s; // destructor called.... แน่นอนว่ามันdeleteจะไม่ทำงานเพราะคอมไพเลอร์จะไม่รู้sอีกต่อไป
badger5000

2
ฉันไม่ได้ให้ -1 แต่ฉันไม่เห็นด้วยกับข้อความเปิดที่เขียนไว้ ก่อนอื่นฉันไม่เห็นด้วยมี "hype" - อาจมีรอบ Y2K แต่ตอนนี้ทั้งสองภาษาเข้าใจกันดี สองฉันจะยืนยันว่าพวกเขาค่อนข้างคล้ายกัน - C ++ เป็นลูกของ C แต่งงานกับ Simula, Java เพิ่ม Virtual Machine, Garbage Collector และ HEAVILY ลดคุณสมบัติและ C # streamlines และแนะนำคุณสมบัติที่ขาดหายไปของ Java ใช่มันทำให้รูปแบบและการใช้งานที่ถูกต้องแตกต่างกันอย่างมาก แต่ก็มีประโยชน์ในการทำความเข้าใจโครงสร้างพื้นฐานทั่วไป / การออกแบบเพื่อให้สามารถเห็นความแตกต่าง
Gerasimos R

1
@ James Matta: แน่นอนว่าคุณจำได้ว่าหน่วยความจำนั้นเป็นหน่วยความจำและทั้งคู่ถูกจัดสรรจากหน่วยความจำกายภาพเดียวกัน แต่สิ่งหนึ่งที่ต้องพิจารณาคือมันเป็นเรื่องธรรมดามากที่จะได้รับประสิทธิภาพการทำงานที่ดีกว่า หรืออย่างน้อยก็ระดับสูงสุด - มีโอกาสสูงมากที่จะ "ร้อน" ในแคชในขณะที่ฟังก์ชั่นเข้าและออกในขณะที่ฮีปไม่มีประโยชน์ดังกล่าวดังนั้นหากคุณกำลังไล่ล่าตัวชี้ในฮีปคุณอาจได้รับแคชจำนวนมาก คุณอาจจะไม่ได้อยู่ในกอง แต่ "สุ่ม" ทั้งหมดนี้โดยปกติแล้วจะชอบสแต็ก
Gerasimos R

23

C ++ ให้คุณสามวิธีในการส่งผ่านวัตถุ: โดยตัวชี้โดยการอ้างอิงและตามค่า Java จำกัด คุณไว้ที่หนึ่ง (ข้อยกเว้นเพียงอย่างเดียวคือประเภทดั้งเดิมเช่น int, บูลีน ฯลฯ ) ถ้าคุณต้องการใช้ C ++ ไม่ใช่แค่ของเล่นแปลก ๆ คุณควรทำความรู้จักกับความแตกต่างระหว่างสามวิธีนี้

Java อ้างว่าไม่มีปัญหาเช่น 'ใครและเมื่อไรควรทำลายสิ่งนี้?' คำตอบคือ: The Garbage Collector, Great และ Awful อย่างไรก็ตามมันไม่สามารถป้องกันการรั่วไหลของหน่วยความจำได้ 100% (ใช่จาวาสามารถรั่วหน่วยความจำได้ ) จริงๆแล้ว GC ให้ความรู้สึกผิด ๆ กับความปลอดภัย ยิ่งเอสยูวีของคุณใหญ่ขึ้นเท่าไหร่คุณก็จะไปยังศูนย์อพยพได้นานขึ้นเท่านั้น

C ++ ให้คุณแบบตัวต่อตัวด้วยการจัดการวงจรชีวิตของวัตถุ มีวิธีจัดการกับสิ่งนั้น (สมาร์ทชี้ครอบครัว QObject ใน Qt และอื่น ๆ ) แต่ไม่มีของพวกเขาสามารถนำมาใช้ในไฟและลืม 'ลักษณะเช่น GC: คุณควรเสมอเก็บไว้ในการจัดการหน่วยความจำใจ ไม่เพียง แต่คุณต้องใส่ใจในการทำลายวัตถุเท่านั้นคุณต้องหลีกเลี่ยงการทำลายวัตถุเดียวกันมากกว่าหนึ่งครั้ง

ยังไม่กลัวเหรอ? Ok: การอ้างอิงแบบวนรอบ - จัดการมันด้วยตัวเองมนุษย์ และจำไว้ว่า: ฆ่าแต่ละวัตถุอย่างแม่นยำครั้งเดียวเรารัน C ++ ไม่ชอบคนที่ยุ่งกับศพทิ้งคนตายไว้คนเดียว

ดังนั้นกลับไปที่คำถามของคุณ

เมื่อคุณส่งวัตถุไปตามค่าไม่ใช่โดยตัวชี้หรือโดยการอ้างอิงคุณคัดลอกวัตถุ (วัตถุทั้งหมดไม่ว่าจะเป็นสองไบต์หรือดัมพ์ฐานข้อมูลขนาดใหญ่ - คุณฉลาดพอที่จะหลีกเลี่ยงสิ่งหลัง คุณ?) ทุกครั้งที่คุณทำ '=' และในการเข้าถึงสมาชิกของวัตถุคุณใช้ '.' (dot)

เมื่อคุณส่งวัตถุโดยตัวชี้คุณคัดลอกเพียงไม่กี่ไบต์ (4 ในระบบ 32 บิต, 8 ในคน 64- บิต) คือ - ที่อยู่ของวัตถุนี้ และเพื่อแสดงให้ทุกคนเห็นคุณต้องใช้โอเปอร์เรเตอร์ '->' แฟนซีนี้เมื่อคุณเข้าถึงสมาชิก หรือคุณสามารถใช้การรวมกันของ '*' และ '.'

เมื่อคุณใช้การอ้างอิงคุณจะได้รับตัวชี้ที่อ้างว่าเป็นค่า มันเป็นตัวชี้ แต่คุณเข้าถึงสมาชิกผ่าน '.'

และเพื่อทำให้ใจของคุณอีกครั้ง: เมื่อคุณประกาศตัวแปรหลายตัวคั่นด้วยเครื่องหมายจุลภาคแล้ว (ดูมือ):

  • ประเภทมอบให้กับทุกคน
  • ค่า / ตัวชี้ / ตัวอ้างอิงอ้างอิงเป็นรายบุคคล

ตัวอย่าง:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one

1
std::auto_ptrเลิกใช้แล้วโปรดอย่าใช้
Neil

2
ค่อนข้างแน่ใจว่าคุณไม่สามารถมีการอ้างอิงในฐานะสมาชิกโดยไม่ต้องจัดทำคอนสตรัคเตอร์ด้วยรายการเริ่มต้นที่มีตัวแปรอ้างอิง (การอ้างอิงจะต้องเริ่มต้นทันทีแม้กระทั่งตัวคอนสตรัคเตอร์ก็สายเกินไปที่จะวางมัน IIRC)
cHao

20

ใน C ++ วัตถุที่จัดสรรในสแต็ก (โดยใช้Object object;คำสั่งภายในบล็อก) จะอยู่ภายในขอบเขตที่ประกาศไว้เท่านั้นเมื่อบล็อกของรหัสเสร็จสิ้นการดำเนินการวัตถุที่ประกาศจะถูกทำลาย ในขณะที่ถ้าคุณจัดสรรหน่วยความจำบนกองใช้พวกเขายังคงมีชีวิตอยู่ในกองจนกว่าคุณจะเรียกObject* obj = new Object()delete obj

ฉันจะสร้างวัตถุบนกองเมื่อฉันต้องการใช้วัตถุไม่เพียง แต่ในบล็อกของรหัสที่ประกาศ / จัดสรรมัน


6
Object objไม่ได้อยู่ในสแต็กเสมอ - ตัวอย่างเช่น globals หรือตัวแปรสมาชิก
สิบสี่

2
@LightnessRacesinOrbit ฉันพูดถึงเฉพาะเกี่ยวกับวัตถุที่จัดสรรในบล็อกไม่ใช่เกี่ยวกับตัวแปรโกลบอลและสมาชิก สิ่งที่มันไม่ชัดเจนตอนนี้แก้ไข - เพิ่ม "ภายในบล็อก" ในคำตอบ หวังว่าข้อมูลไม่ผิดพลาดในขณะนี้ :)
คาร์ทิค Kalyanasundaram

20

แต่ฉันคิดไม่ออกว่าทำไมเราถึงใช้มันแบบนี้?

ฉันจะเปรียบเทียบการทำงานของฟังก์ชั่นภายในร่างกายถ้าคุณใช้:

Object myObject;

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

หากคุณเขียนเนื้อหาภายในของฟังก์ชัน:

 Object *myObject = new Object;

ดังนั้นอินสแตนซ์คลาสอ็อบเจ็กต์ที่ชี้โดยmyObjectจะไม่ถูกทำลายเมื่อฟังก์ชันสิ้นสุดลงและการจัดสรรอยู่บนฮีป

ตอนนี้ถ้าคุณเป็นโปรแกรมเมอร์ Java แล้วตัวอย่างที่สองจะใกล้เคียงกับการทำงานของการจัดสรรวัตถุภายใต้ java บรรทัดนี้Object *myObject = new Object;จะเทียบเท่ากับ Object myObject = new Object();Java: ความแตกต่างคือภายใต้ java myObject จะได้รับการรวบรวมขยะในขณะที่ภายใต้ c ++ จะไม่ได้รับการปลดปล่อยคุณต้องเรียก `ลบ myObject; ' มิฉะนั้นคุณจะแนะนำหน่วยความจำรั่ว

ตั้งแต่ c ++ 11 คุณสามารถใช้วิธีการจัดสรรแบบไดนามิกที่ปลอดภัย: new Objectโดยการเก็บค่าใน shared_ptr / unique_ptr

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

นอกจากนี้วัตถุมักถูกเก็บไว้ในภาชนะบรรจุเช่น map-s หรือ vector-s พวกมันจะจัดการอายุการใช้งานของวัตถุของคุณโดยอัตโนมัติ


1
then myObject will not get destroyed once function endsมันจะอย่างแน่นอน
การแข่งขัน Lightness ใน Orbit

6
ในกรณีตัวชี้myObjectจะยังคงถูกทำลายเช่นเดียวกับตัวแปรท้องถิ่นอื่น ๆ จะ ความแตกต่างคือว่าค่าของมันคือตัวชี้ไปยังวัตถุไม่ใช่วัตถุเองและการทำลายของตัวชี้ใบ้ไม่ส่งผลกระทบต่อจุดของมัน ดังนั้นวัตถุจะรอดจากการทำลายได้
cHao

แก้ไขปัญหาที่แน่นอนว่าตัวแปรท้องถิ่น (ที่มีตัวชี้) จะเป็นอิสระ - มันอยู่ในสแต็ก
marcinj

13

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

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


6

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

อีกสิ่งที่คุณพูดถึงในคำถามของคุณ:

Object *myObject = new Object;

มันทำงานยังไง? มันสร้างพอยน์เตอร์ของObjectประเภทจัดสรรหน่วยความจำให้พอดีกับวัตถุหนึ่งและเรียกตัวสร้างเริ่มต้นเสียงดีใช่มั้ย แต่จริงๆแล้วมันไม่ดีนักหากคุณจัดสรรหน่วยความจำแบบไดนามิก (ใช้คีย์เวิร์ดnew) คุณต้องเพิ่มหน่วยความจำด้วยตนเองนั่นหมายความว่าในรหัสที่คุณควรมี:

delete myObject;

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


และตอนนี้การแนะนำก็จบลงแล้วกลับไปตั้งคำถาม

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

ลองดูที่คุณมีstd::string(มันเป็นวัตถุ) และมันมีข้อมูลจำนวนมากเช่น XML ขนาดใหญ่ตอนนี้คุณต้องแยกวิเคราะห์มัน แต่สำหรับสิ่งที่คุณมีฟังก์ชั่นvoid foo(...)ซึ่งสามารถประกาศในรูปแบบที่แตกต่างกัน:

  1. void foo(std::string xml); ในกรณีนี้คุณจะคัดลอกข้อมูลทั้งหมดจากตัวแปรของคุณไปยังฟังก์ชั่นสแต็คมันใช้เวลาพอสมควรดังนั้นประสิทธิภาพของคุณจะต่ำ
  2. void foo(std::string* xml); ในกรณีนี้คุณจะส่งตัวชี้ไปยังวัตถุความเร็วเดียวกับsize_tตัวแปรส่งอย่างไรก็ตามการประกาศนี้มีข้อผิดพลาดเกิดขึ้นเนื่องจากคุณสามารถส่งNULLตัวชี้หรือตัวชี้ที่ไม่ถูกต้อง ตัวชี้มักใช้Cเนื่องจากไม่มีการอ้างอิง
  3. void foo(std::string& xml); ที่นี่คุณผ่านการอ้างอิงโดยทั่วไปมันเหมือนกับการผ่านตัวชี้ แต่คอมไพเลอร์ทำบางสิ่งและคุณไม่สามารถผ่านการอ้างอิงที่ไม่ถูกต้อง (อันที่จริงแล้วมันเป็นไปได้ที่จะสร้างสถานการณ์ที่มีการอ้างอิงที่ไม่ถูกต้อง
  4. void foo(const std::string* xml); นี่คือเช่นเดียวกับที่สองเพียงแค่ค่าตัวชี้ไม่สามารถเปลี่ยนแปลงได้
  5. void foo(const std::string& xml); ที่นี่เหมือนกับที่สาม แต่ไม่สามารถเปลี่ยนค่าวัตถุได้

ยิ่งไปกว่านั้นฉันต้องการพูดถึงคุณสามารถใช้ 5 วิธีเหล่านี้ในการส่งผ่านข้อมูลไม่ว่าคุณจะเลือกวิธีการจัดสรรแบบใด (ด้วยnewหรือปกติ )


อีกสิ่งหนึ่งที่พูดถึงเมื่อคุณสร้างวัตถุในแบบปกติคุณจัดสรรหน่วยความจำในกองซ้อน แต่ในขณะที่คุณสร้างวัตถุด้วยการnewจัดสรรฮีป มันเร็วกว่าในการจัดสรร stack แต่มันมีขนาดเล็กสำหรับอาร์เรย์ขนาดใหญ่ของข้อมูลดังนั้นถ้าคุณต้องการวัตถุขนาดใหญ่คุณควรใช้ heap เพราะคุณอาจได้ stack overflow แต่โดยปกติปัญหานี้จะแก้ไขโดยใช้คอนเทนเนอร์ STLและจดจำstd::stringยังเป็นตู้คอนเทนเนอร์บางคนลืมมัน :)


5

สมมติว่าคุณมีclass Aสิ่งนั้นclass Bเมื่อคุณต้องการเรียกใช้ฟังก์ชันclass Bนอกclass Aคุณจะได้รับตัวชี้ไปยังคลาสนี้และคุณสามารถทำสิ่งที่คุณต้องการและมันจะเปลี่ยนบริบทของclass Bในclass A

แต่ระวังด้วยวัตถุแบบไดนามิก


5

มีประโยชน์มากมายในการใช้พอยน์เตอร์กับวัตถุ -

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

3

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


3
Object *myObject = new Object;

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

Object myObject;

การทำเช่นนี้จะสร้างวัตถุ (myObject) ของประเภทอัตโนมัติ (บนสแต็ค) ที่จะถูกลบโดยอัตโนมัติเมื่อวัตถุ (myObject) ออกไปนอกขอบเขต


1

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

เพื่อตอบคำถามของคุณมันเป็นเพียงการตั้งค่าของคุณ ฉันชอบใช้ไวยากรณ์เหมือน Java


ตารางแฮช อาจจะอยู่ใน JVM บางแห่ง แต่ไม่ต้องนับ
Zan Lynx

JVM ที่มาพร้อมกับ Java คืออะไร แน่นอนว่าคุณสามารถนำสิ่งใดไปใช้คิดว่าเป็น JVM ที่ใช้พอยน์เตอร์โดยตรงหรือวิธีที่ใช้ตัวชี้ทางคณิตศาสตร์ นั่นคือการพูดว่า "คนไม่ตายจากโรคไข้หวัด" และได้รับคำตอบ "บางทีคนส่วนใหญ่ไม่ได้ แต่อย่าเชื่อ!" ฮ่า.
RioRicoRick

2
@RioRicoRick HotSpot ใช้การอ้างอิง Java เป็นตัวชี้แบบดั้งเดิมดูdocs.oracle.com/javase/7/docs/technotes/guides/vm/ …เท่าที่ฉันจะเห็น JRockit ก็ทำเช่นเดียวกัน ทั้งสองสนับสนุนการบีบอัด OOP แต่ไม่เคยใช้ตารางแฮช ผลกระทบด้านประสิทธิภาพอาจเป็นหายนะ นอกจากนี้ "มันเป็นเพียงการตั้งค่าของคุณ" ดูเหมือนจะบอกเป็นนัยว่าทั้งสองเป็นเพียงไวยากรณ์ที่แตกต่างกันสำหรับพฤติกรรมที่เทียบเท่าซึ่งแน่นอนว่าพวกเขาไม่ได้
Max Barraclough


0

กับคำแนะนำ ,

  • สามารถพูดคุยกับหน่วยความจำโดยตรง

  • สามารถป้องกันการรั่วไหลของหน่วยความจำจำนวนมากของโปรแกรมโดยจัดการพอยน์เตอร์


4
" ใน C ++ โดยใช้พอยน์เตอร์คุณสามารถสร้างตัวรวบรวมขยะที่กำหนดเองสำหรับโปรแกรมของคุณเอง " ซึ่งฟังดูเหมือนไอเดียที่แย่มาก
quant

0

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


0

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


0

ฉันจะรวมกรณีการใช้งานที่สำคัญของตัวชี้ เมื่อคุณกำลังเก็บวัตถุบางอย่างในชั้นฐาน แต่มันอาจเป็น polymorphic

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

ดังนั้นในกรณีนี้คุณไม่สามารถประกาศ bObj เป็นวัตถุโดยตรงคุณจะต้องมีตัวชี้


-5

"ความจำเป็นคือแม่ของการประดิษฐ์" ความแตกต่างที่สำคัญที่สุดที่ฉันต้องการชี้ให้เห็นคือผลลัพธ์ของประสบการณ์การเขียนโค้ดของฉันเอง บางครั้งคุณต้องส่งวัตถุไปยังฟังก์ชั่น ในกรณีนั้นถ้าวัตถุของคุณมีขนาดใหญ่มากจากนั้นส่งผ่านเป็นวัตถุจะคัดลอกสถานะของมัน (ซึ่งคุณอาจไม่ต้องการ .. และสามารถใหญ่เกินไป) จึงส่งผลให้ค่าใช้จ่ายในการคัดลอกวัตถุ. ขนาด 4 ไบต์ (สมมติว่า 32 บิต) เหตุผลอื่น ๆ ที่กล่าวถึงข้างต้นแล้ว ...


14
คุณควรจะชอบผ่านการอ้างอิง
bolov

2
ผมขอแนะนำให้ผ่านโดยคงอ้างอิงเช่นตัวแปรstd::string test;ที่เรามีvoid func(const std::string &) {}แต่ถ้าฟังก์ชั่นที่ต้องการเปลี่ยนการป้อนข้อมูลซึ่งในกรณีนี้ผมขอแนะนำให้ใช้ตัวชี้ (เพื่อให้ทุกคนที่อ่านรหัสไม่แจ้งให้ทราบล่วงหน้า&และมีความเข้าใจในการทำงานอาจมีการเปลี่ยนแปลงปัจจัยการผลิต)
Top- ปรมาจารย์

-7

มีคำตอบที่ยอดเยี่ยมมากมายอยู่แล้ว แต่ให้ฉันยกตัวอย่าง:

ฉันมีรายการระดับง่าย ๆ :

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

ฉันทำเวกเตอร์เพื่อจับมัดพวกมัน

std::vector<Item> inventory;

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

std::vector<Item *> inventory;

... และสร้างรายการสิ่งของล้านของฉันผ่านทางใหม่ การเปลี่ยนแปลงเดียวที่ฉันทำกับรหัสของฉันคือการใช้ตัวชี้ไปยังรายการยกเว้นลูปที่ฉันเพิ่มสำหรับการล้างหน่วยความจำในตอนท้าย โปรแกรมนั้นทำงานในเวลาไม่เกิน 40 วินาทีหรือดีกว่าการเพิ่มความเร็ว 10 เท่า แก้ไข: รหัสอยู่ที่http://pastebin.com/DK24SPeW ด้วยการเพิ่มประสิทธิภาพคอมไพเลอร์มันแสดงเพียง 3.4x เพิ่มขึ้นบนเครื่องที่ฉันเพิ่งทดสอบซึ่งยังคงเป็นจำนวนมาก


2
คุณเปรียบเทียบตัวชี้แล้วหรือคุณยังเปรียบเทียบวัตถุจริงหรือไม่? ฉันสงสัยอย่างมากว่าการอ้อมอีกระดับสามารถปรับปรุงประสิทธิภาพได้ โปรดระบุรหัส! หลังจากนั้นคุณทำความสะอาดอย่างถูกต้องหรือไม่?
ฟาน

1
@stefan ฉันเปรียบเทียบข้อมูล (โดยเฉพาะฟิลด์ชื่อ) ของวัตถุสำหรับการเรียงลำดับและการค้นหา ฉันทำความสะอาดอย่างถูกต้องตามที่ระบุไว้ในโพสต์แล้ว การเร่งความเร็วอาจเกิดจากปัจจัยสองประการ: 1) std :: vector push_back () คัดลอกวัตถุดังนั้นรุ่นของตัวชี้จึงจำเป็นต้องคัดลอกตัวชี้เดียวต่อวัตถุเท่านั้น สิ่งนี้มีผลกระทบหลายอย่างต่อประสิทธิภาพการทำงานเนื่องจากไม่เพียง แต่คัดลอกข้อมูลน้อยลงเท่านั้น
Darren

2
นี่คือรหัสที่แสดงถึงตัวอย่างของคุณ: การเรียงลำดับ รหัสตัวชี้นั้นเร็วกว่ารหัสที่ไม่ใช่ตัวชี้ 6% สำหรับการเรียงลำดับเพียงอย่างเดียว แต่โดยรวมแล้วจะช้ากว่ารหัสที่ไม่ใช่ตัวชี้ 10% ideone.com/G0c7zw
stefan

3
คำสำคัญ: push_back. แน่นอนสำเนานี้ คุณควรเข้ามาemplaceแทนที่เมื่อสร้างวัตถุของคุณ (เว้นแต่คุณต้องการให้แคชไว้ที่อื่น)
underscore_d

1
พอยเตอร์ของพอยน์เตอร์นั้นมักจะผิดเสมอไป โปรดอย่าแนะนำพวกเขาโดยไม่อธิบายรายละเอียดคำเตือนข้อดีข้อเสีย ดูเหมือนว่าคุณจะพบโปรหนึ่งตัวซึ่งเป็นผลมาจากตัวอย่างเคาน์เตอร์ที่มีคุณภาพต่ำและบิดเบือนความจริง
Lightness Races ใน Orbit
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.