ในทางปฏิบัติกับ C ++ RAIIคืออะไรพอยน์เตอร์อัจฉริยะคืออะไรสิ่งเหล่านี้นำไปใช้ในโปรแกรมได้อย่างไรและประโยชน์ของการใช้ RAII กับพอยน์เตอร์อัจฉริยะคืออะไร
ในทางปฏิบัติกับ C ++ RAIIคืออะไรพอยน์เตอร์อัจฉริยะคืออะไรสิ่งเหล่านี้นำไปใช้ในโปรแกรมได้อย่างไรและประโยชน์ของการใช้ RAII กับพอยน์เตอร์อัจฉริยะคืออะไร
คำตอบ:
ตัวอย่างง่ายๆ (และอาจใช้มากเกินไป) ของ RAII คือคลาสไฟล์ หากไม่มี RAII รหัสอาจมีลักษณะเช่นนี้:
File file("/path/to/file");
// Do stuff with file
file.close();
กล่าวอีกนัยหนึ่งเราต้องทำให้แน่ใจว่าเราปิดไฟล์เมื่อเราทำเสร็จแล้ว สิ่งนี้มีข้อเสียสองประการประการแรกไม่ว่าเราจะใช้ไฟล์ที่ไหนเราจะต้องเรียกว่าไฟล์ :: ปิด () - หากเราลืมทำสิ่งนี้เราจะถือไฟล์ไว้นานกว่าที่เราต้องการ ปัญหาที่สองคือเกิดอะไรขึ้นถ้ามีข้อผิดพลาดเกิดขึ้นก่อนที่เราจะปิดไฟล์
Java แก้ปัญหาที่สองโดยใช้ประโยคสุดท้าย:
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
หรือตั้งแต่ Java 7 คำสั่งลองกับทรัพยากร:
try (File file = new File("/path/to/file")) {
// Do stuff with file
}
C ++ แก้ปัญหาทั้งสองอย่างโดยใช้ RAII - นั่นคือปิดไฟล์ในตัวทำลายของไฟล์ ตราบใดที่วัตถุไฟล์ถูกทำลายในเวลาที่เหมาะสม (ซึ่งควรจะเป็นต่อไป) การปิดไฟล์จะได้รับการดูแลจากเรา ดังนั้นรหัสของเราตอนนี้ดูเหมือน:
File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us
สิ่งนี้ไม่สามารถทำได้ใน Java เนื่องจากไม่มีการรับประกันเมื่อวัตถุจะถูกทำลายดังนั้นเราจึงไม่สามารถรับประกันได้ว่าเมื่อใดที่ทรัพยากรเช่นไฟล์จะถูกปลดปล่อย
บนพอยน์เตอร์อัจฉริยะ - บ่อยครั้งที่เราเพิ่งสร้างวัตถุบนสแต็ก เช่น (และขโมยตัวอย่างจากคำตอบอื่น):
void foo() {
std::string str;
// Do cool things to or using str
}
มันใช้งานได้ดี - แต่ถ้าเราต้องการคืนค่า STR? เราสามารถเขียนสิ่งนี้:
std::string foo() {
std::string str;
// Do cool things to or using str
return str;
}
ดังนั้นมีอะไรผิดปกติกับที่? ประเภทการส่งคืนคือ std :: string - ดังนั้นจึงหมายความว่าเราคืนค่า ซึ่งหมายความว่าเราคัดลอก str และคืนค่าสำเนาจริง สิ่งนี้อาจมีราคาแพงและเราอาจต้องการหลีกเลี่ยงค่าใช้จ่ายในการคัดลอก ดังนั้นเราอาจเกิดความคิดในการกลับมาโดยการอ้างอิงหรือโดยตัวชี้
std::string* foo() {
std::string str;
// Do cool things to or using str
return &str;
}
น่าเสียดายที่รหัสนี้ใช้ไม่ได้ เรากำลังส่งคืนพอยน์เตอร์ไปยัง str - แต่ str ถูกสร้างขึ้นบนสแต็กดังนั้นเราจะถูกลบเมื่อเราออกจาก foo () กล่าวอีกนัยหนึ่งตามเวลาที่ผู้เรียกได้รับตัวชี้มันก็ไร้ประโยชน์ (และเลวร้ายยิ่งกว่าไร้ประโยชน์เนื่องจากการใช้มันอาจทำให้เกิดข้อผิดพลาดขี้ขลาด)
ดังนั้นทางออกคืออะไร? เราสามารถสร้าง str บน heap โดยใช้ new - วิธีนี้เมื่อ foo () เสร็จสมบูรณ์ str จะไม่ถูกทำลาย
std::string* foo() {
std::string* str = new std::string();
// Do cool things to or using str
return str;
}
แน่นอนวิธีนี้ไม่ได้สมบูรณ์แบบเช่นกัน เหตุผลก็คือเราได้สร้าง str แต่เราไม่เคยลบเลย นี่อาจไม่ใช่ปัญหาในโปรแกรมขนาดเล็กมาก แต่โดยทั่วไปเราต้องการให้แน่ใจว่าเราลบมัน เราสามารถพูดได้ว่าผู้โทรต้องลบวัตถุเมื่อเสร็จแล้ว ข้อเสียคือผู้โทรต้องจัดการหน่วยความจำซึ่งเพิ่มความซับซ้อนมากขึ้นและอาจผิดพลาดทำให้หน่วยความจำรั่วเช่นไม่ลบวัตถุแม้ว่าจะไม่จำเป็นต้องใช้อีกต่อไป
นี่คือที่มาของสมาร์ทพอยน์เตอร์ตัวอย่างต่อไปนี้ใช้ shared_ptr - ฉันแนะนำให้คุณดูพอยน์เตอร์สมาร์ทประเภทต่าง ๆ เพื่อเรียนรู้สิ่งที่คุณต้องการใช้จริง
shared_ptr<std::string> foo() {
shared_ptr<std::string> str = new std::string();
// Do cool things to or using str
return str;
}
ตอนนี้ shared_ptr จะนับจำนวนการอ้างอิงถึง str ตัวอย่างเช่น
shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;
ตอนนี้มีสองการอ้างอิงถึงสตริงเดียวกัน เมื่อไม่มีการอ้างอิงไปยัง str แล้วจะถูกลบทิ้ง คุณไม่ต้องกังวลเกี่ยวกับการลบด้วยตัวเองอีกต่อไป
การแก้ไขอย่างรวดเร็ว: เนื่องจากความคิดเห็นบางส่วนชี้ให้เห็นตัวอย่างนี้ไม่เหมาะสำหรับ (อย่างน้อย!) ด้วยเหตุผลสองประการ ประการแรกเนื่องจากการใช้งานสตริงการคัดลอกสตริงจึงมีราคาไม่แพง ประการที่สองเนื่องจากสิ่งที่รู้จักกันในชื่อการปรับค่าส่งคืนที่มีชื่อการส่งคืนโดยค่าอาจไม่แพงเนื่องจากคอมไพเลอร์สามารถทำสิ่งที่ชาญฉลาดเพื่อเร่งความเร็วของสิ่งต่างๆ
ดังนั้นลองทำตัวอย่างอื่นโดยใช้คลาสไฟล์ของเรา
สมมติว่าเราต้องการใช้ไฟล์เป็นบันทึก หมายความว่าเราต้องการเปิดไฟล์ของเราในโหมดต่อท้ายเท่านั้น:
File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log
ตอนนี้ให้ตั้งไฟล์ของเราเป็นบันทึกสำหรับวัตถุอื่นสองสามอย่าง:
void setLog(const Foo & foo, const Bar & bar) {
File file("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
น่าเสียดายที่ตัวอย่างนี้จบลงอย่างน่ากลัว - ไฟล์จะถูกปิดทันทีที่วิธีนี้สิ้นสุดซึ่งหมายความว่าตอนนี้ foo และ bar มีไฟล์บันทึกที่ไม่ถูกต้อง เราสามารถสร้างไฟล์บนฮีปและส่งต่อตัวชี้ไปยังไฟล์ไปที่ foo และ bar:
void setLog(const Foo & foo, const Bar & bar) {
File* file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
แต่ใครจะเป็นผู้รับผิดชอบในการลบไฟล์? หากไม่ลบไฟล์เราก็มีทั้งหน่วยความจำและทรัพยากรรั่วไหล เราไม่ทราบว่า foo หรือ bar จะเสร็จสิ้นไฟล์ก่อนดังนั้นเราจึงไม่สามารถคาดหวังว่าจะลบไฟล์เอง ตัวอย่างเช่นหาก foo ลบไฟล์ก่อนที่แถบจะเสร็จสิ้นแถบตอนนี้จะมีตัวชี้ที่ไม่ถูกต้อง
ดังนั้นตามที่คุณอาจเดาได้เราสามารถใช้ตัวชี้อัจฉริยะเพื่อช่วยเราออก
void setLog(const Foo & foo, const Bar & bar) {
shared_ptr<File> file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
ตอนนี้ไม่มีใครต้องกังวลเกี่ยวกับการลบไฟล์ - เมื่อทั้ง foo และ bar เสร็จสิ้นแล้วและไม่มีการอ้างอิงถึงไฟล์อีกต่อไป (อาจเป็นเพราะ foo และ bar ถูกทำลาย) ไฟล์จะถูกลบโดยอัตโนมัติ
RAIIนี่เป็นชื่อที่แปลกสำหรับแนวคิดที่เรียบง่าย แต่ยอดเยี่ยม Better ชื่อขอบเขตการจัดการทรัพยากร (SBRM) แนวคิดคือบ่อยครั้งที่คุณจัดสรรทรัพยากรที่จุดเริ่มต้นของบล็อกและต้องปล่อยออกที่ทางออกของบล็อก การออกจากบล็อกสามารถเกิดขึ้นได้โดยการควบคุมการไหลปกติกระโดดออกจากบล็อกและแม้กระทั่งโดยการยกเว้น เพื่อครอบคลุมกรณีเหล่านี้รหัสจะซับซ้อนและซ้ำซ้อน
เพียงแค่ทำตัวอย่างโดยไม่ใช้ SBRM:
void o_really() {
resource * r = allocate_resource();
try {
// something, which could throw. ...
} catch(...) {
deallocate_resource(r);
throw;
}
if(...) { return; } // oops, forgot to deallocate
deallocate_resource(r);
}
อย่างที่คุณเห็นมีหลายวิธีที่เราสามารถรับ pwned แนวคิดคือเราสรุปการจัดการทรัพยากรเป็นคลาส การเริ่มต้นของวัตถุนั้นได้รับทรัพยากร ("การได้มาซึ่งทรัพยากรเป็นการเริ่มต้น") ในขณะที่เราออกจากบล็อก (ขอบเขตบล็อก) ทรัพยากรจะถูกปลดปล่อยอีกครั้ง
struct resource_holder {
resource_holder() {
r = allocate_resource();
}
~resource_holder() {
deallocate_resource(r);
}
resource * r;
};
void o_really() {
resource_holder r;
// something, which could throw. ...
if(...) { return; }
}
นั่นเป็นเรื่องดีถ้าคุณมีคลาสของตัวเองซึ่งไม่ได้มีไว้เพื่อวัตถุประสงค์ในการจัดสรร / จัดสรรคืนทรัพยากรเท่านั้น การจัดสรรจะเป็นข้อกังวลเพิ่มเติมเพื่อให้งานเสร็จ แต่ทันทีที่คุณต้องการจัดสรร / จัดสรรคืนทรัพยากรดังกล่าวข้างต้นจะไม่สามารถใช้ได้ คุณต้องเขียนคลาสการห่อสำหรับทรัพยากรทุกประเภทที่คุณได้รับ เพื่อให้ง่ายขึ้นตัวชี้อัจฉริยะช่วยให้คุณสามารถดำเนินการดังกล่าวโดยอัตโนมัติ:
shared_ptr<Entry> create_entry(Parameters p) {
shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
return e;
}
โดยปกติตัวชี้สมาร์ทจะล้อมรอบใหม่ / ลบที่เพิ่งเกิดขึ้นdelete
เมื่อทรัพยากรที่พวกเขาเป็นเจ้าของออกไปนอกขอบเขต บางตัวชี้สมาร์ทเช่น shared_ptr ช่วยให้คุณสามารถที่จะบอกพวกเขา Deleter delete
ที่เรียกว่าซึ่งถูกนำมาใช้แทน ที่ช่วยให้คุณสามารถจัดการหน้าต่างจัดการทรัพยากรนิพจน์ปกติและสิ่งอื่น ๆ โดยพลการตราบใดที่คุณบอก shared_ptr เกี่ยวกับ deleter ที่เหมาะสม
มีตัวชี้สมาร์ทที่แตกต่างกันสำหรับวัตถุประสงค์ที่แตกต่างกัน:
รหัส:
unique_ptr<plot_src> p(new plot_src); // now, p owns
unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing.
unique_ptr<plot_src> v(u); // error, trying to copy u
vector<unique_ptr<plot_src>> pv;
pv.emplace_back(new plot_src);
pv.emplace_back(new plot_src);
ซึ่งแตกต่างจาก auto_ptr, unique_ptr สามารถใส่ลงในคอนเทนเนอร์ได้เนื่องจากคอนเทนเนอร์จะสามารถเก็บประเภทที่ไม่สามารถคัดลอกได้ (แต่เคลื่อนย้ายได้) เช่นสตรีมและ unique_ptr
รหัส:
void do_something() {
scoped_ptr<pipe> sp(new pipe);
// do something here...
} // when going out of scope, sp will delete the pointer automatically.
รหัส:
shared_ptr<plot_src> p(new plot_src(&fx));
plot1->add(p)->setColor("#00FF00");
plot2->add(p)->setColor("#FF0000");
// if p now goes out of scope, the src won't be freed, as both plot1 and
// plot2 both still have references.
อย่างที่คุณเห็นพล็อตที่มา (ฟังก์ชั่น fx) จะถูกแชร์ แต่แต่ละรายการมีรายการแยกต่างหากซึ่งเรากำหนดสี มีคลาส weak_ptr ซึ่งใช้เมื่อรหัสจำเป็นต้องอ้างถึงทรัพยากรที่เป็นของตัวชี้สมาร์ท แต่ไม่จำเป็นต้องเป็นเจ้าของทรัพยากร แทนที่จะส่งตัวชี้แบบดิบคุณควรสร้าง weak_ptr มันจะโยนข้อยกเว้นเมื่อสังเกตเห็นว่าคุณพยายามเข้าถึงทรัพยากรโดยใช้เส้นทางการเข้าถึงที่อ่อนแอแม้ว่าจะไม่มี shared_ptr ที่เป็นเจ้าของทรัพยากรอีกต่อไป
unique_ptr
และsort
จะถูกเปลี่ยนเช่นกัน
RAII เป็นกระบวนทัศน์การออกแบบเพื่อให้แน่ใจว่าตัวแปรจัดการกับการเริ่มต้นทั้งหมดที่จำเป็นในการก่อสร้างของพวกเขาและการทำความสะอาดที่จำเป็นใน destructors ของพวกเขาทั้งหมด ซึ่งจะช่วยลดการเริ่มต้นและการล้างข้อมูลทั้งหมดในขั้นตอนเดียว
C ++ ไม่ต้องการ RAII แต่เป็นที่ยอมรับมากขึ้นว่าการใช้วิธี RAII จะสร้างรหัสที่มีประสิทธิภาพมากขึ้น
เหตุผลที่ RAII มีประโยชน์ใน C ++ ก็คือ C ++ นั้นจะจัดการการสร้างและการทำลายตัวแปรในขณะที่พวกมันเข้าและออกจากขอบเขตไม่ว่าจะเป็นการไหลของรหัสปกติหรือผ่านสแต็ก นั่นคือ freebie ใน C ++
ด้วยการคาดเดาการเริ่มต้นและการล้างข้อมูลให้กับกลไกเหล่านี้คุณจะมั่นใจได้ว่า C ++ จะดูแลงานนี้ให้คุณเช่นกัน
การพูดเกี่ยวกับ RAII ใน C ++ มักจะนำไปสู่การอภิปรายของพอยน์เตอร์อัจฉริยะเนื่องจากพอยน์เตอร์นั้นบอบบางโดยเฉพาะเมื่อทำความสะอาด เมื่อจัดการหน่วยความจำที่จัดสรรฮีปที่ได้รับจาก malloc หรือใหม่โดยทั่วไปแล้วมันเป็นความรับผิดชอบของโปรแกรมเมอร์ที่จะทำให้หน่วยความจำว่างหรือลบหน่วยความจำก่อนที่ตัวชี้จะถูกทำลาย พอยน์เตอร์อัจฉริยะจะใช้ปรัชญา RAII เพื่อให้แน่ใจว่าวัตถุที่จัดสรรฮีปถูกทำลายเมื่อใดก็ตามที่ตัวแปรตัวชี้ถูกทำลาย
ตัวชี้สมาร์ทเป็นรูปแบบของ RAII RAII หมายถึงการได้มาซึ่งทรัพยากรเป็นการเริ่มต้น ตัวชี้สมาร์ทได้รับทรัพยากร (หน่วยความจำ) ก่อนการใช้งานแล้วโยนมันออกไปโดยอัตโนมัติใน destructor มีสองสิ่งเกิดขึ้น:
ตัวอย่างเช่นอีกตัวอย่างหนึ่งคือซ็อกเก็ตเครือข่าย RAII ในกรณีนี้:
ตอนนี้อย่างที่คุณเห็น RAII เป็นเครื่องมือที่มีประโยชน์มากในกรณีส่วนใหญ่ที่ช่วยให้ผู้คนได้รับการวาง
แหล่งที่มาของสมาร์ทพอยน์เตอร์ C ++ นั้นมีอยู่เป็นล้าน ๆ รายการทั่วโลก
Boost มีจำนวนของสิ่งเหล่านี้รวมถึงที่อยู่ในBoost.Interprocessสำหรับหน่วยความจำที่ใช้ร่วมกัน มันช่วยลดความยุ่งยากในการจัดการหน่วยความจำโดยเฉพาะอย่างยิ่งในสถานการณ์ที่ก่อให้เกิดอาการปวดหัวเช่นเมื่อคุณมี 5 กระบวนการที่แชร์โครงสร้างข้อมูลเดียวกัน: เมื่อทุกคนทำกับหน่วยความจำจำนวนมากคุณต้องการให้มันเป็นอิสระโดยอัตโนมัติ ผู้ที่ควรรับผิดชอบในการเรียกdelete
ใช้หน่วยความจำอันธพาลว่าคุณจะจบลงด้วยการรั่วไหลของหน่วยความจำหรือตัวชี้ที่เป็นอิสระผิดสองครั้งและอาจทำให้ทั้งกองเสียหาย
โมฆะ foo () { std :: แถบสตริง; // // รหัสเพิ่มเติมที่นี่ // }
ไม่ว่าจะเกิดอะไรขึ้นแถบจะถูกลบอย่างถูกต้องเมื่อขอบเขตของฟังก์ชั่น foo () ถูกทิ้งไว้ข้างหลัง
การใช้งานสตริงภายใน :: มักจะใช้ตัวชี้นับการอ้างอิง ดังนั้นสตริงภายในจะต้องคัดลอกเมื่อหนึ่งในสำเนาของสตริงเปลี่ยนไป ดังนั้นการอ้างอิงสมาร์ทพอยน์เตอร์นับจึงเป็นไปได้ที่จะคัดลอกบางสิ่งเมื่อจำเป็น
นอกจากนี้การนับการอ้างอิงภายในทำให้เป็นไปได้ว่าหน่วยความจำจะถูกลบอย่างถูกต้องเมื่อไม่จำเป็นต้องคัดลอกสตริงภายใน