C ++ के साथ अभ्यास में, RAII क्या है , स्मार्ट पॉइंटर्स क्या हैं , ये प्रोग्राम में कैसे लागू किए जाते हैं और स्मार्ट पॉइंटर्स के साथ RAII का उपयोग करने के क्या फायदे हैं?
C ++ के साथ अभ्यास में, RAII क्या है , स्मार्ट पॉइंटर्स क्या हैं , ये प्रोग्राम में कैसे लागू किए जाते हैं और स्मार्ट पॉइंटर्स के साथ RAII का उपयोग करने के क्या फायदे हैं?
जवाबों:
RAII का एक सरल (और शायद अधिक उपयोग किया जाने वाला) उदाहरण एक फ़ाइल वर्ग है। RAII के बिना, कोड कुछ इस तरह दिख सकता है:
File file("/path/to/file");
// Do stuff with file
file.close();
दूसरे शब्दों में, हमें यह सुनिश्चित करना चाहिए कि जब हम इसके साथ समाप्त कर लें, तो हम फ़ाइल को बंद कर दें। इसकी दो कमियां हैं - सबसे पहले, जहाँ भी हम फ़ाइल का उपयोग करते हैं, हमें फ़ाइल :: close () कहा जाता है - यदि हम ऐसा करना भूल जाते हैं, तो हमें उस फ़ाइल पर अधिक समय तक रोकना पड़ता है, जब हमें उसकी आवश्यकता होती है। दूसरी समस्या यह है कि यदि हम फ़ाइल को बंद करने से पहले एक अपवाद को फेंक देते हैं तो क्या होगा?
जावा अंत में क्लॉज का उपयोग करके दूसरी समस्या हल करता है:
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
या जावा 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
यह जावा में नहीं किया जा सकता है क्योंकि ऑब्जेक्ट नष्ट होने की कोई गारंटी नहीं है, इसलिए हम गारंटी नहीं दे सकते कि फाइल जैसे संसाधन को कब मुक्त किया जाएगा।
स्मार्ट पॉइंटर्स पर - बहुत समय, हम बस स्टैक पर ऑब्जेक्ट बनाते हैं। उदाहरण के लिए (और दूसरे उत्तर से एक उदाहरण चुराना):
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;
}
तो, इसमें गलत क्या है? खैर, वापसी का प्रकार एसटीडी :: स्ट्रिंग है - तो इसका मतलब है कि हम मूल्य से लौट रहे हैं। इसका मतलब यह है कि हम str कॉपी करते हैं और वास्तव में कॉपी वापस करते हैं। यह महंगा हो सकता है, और हम इसे कॉपी करने की लागत से बचना चाहते हैं। इसलिए, हम संदर्भ या सूचक द्वारा लौटने के विचार के साथ आ सकते हैं।
std::string* foo() {
std::string str;
// Do cool things to or using str
return &str;
}
दुर्भाग्य से, यह कोड काम नहीं करता है। हम स्ट्राइक पर एक पॉइंटर लौटा रहे हैं - लेकिन स्टैक पर स्ट्रेट बनाया गया था, इसलिए हम फू () से बाहर निकलते ही हटा दिए जाते हैं। दूसरे शब्दों में, जब तक कॉलर को पॉइंटर मिलता है, तब तक यह बेकार होता है (और यकीनन बेकार से बदतर होता है क्योंकि इसे इस्तेमाल करने से हर तरह की फनी त्रुटियाँ हो सकती हैं)
तो, समाधान क्या है? हम नए का उपयोग कर ढेर पर बना सकते हैं - इस तरह, जब फू () पूरा हो गया है, तो तार नष्ट नहीं होगा।
std::string* foo() {
std::string* str = new std::string();
// Do cool things to or using str
return str;
}
बेशक, यह समाधान भी सही नहीं है। कारण यह है कि हमने स्ट्रगल बनाया है, लेकिन हम इसे कभी नहीं हटाते हैं। यह एक बहुत छोटे कार्यक्रम में समस्या नहीं हो सकती है, लेकिन सामान्य तौर पर, हम यह सुनिश्चित करना चाहते हैं कि हम इसे हटा दें। हम बस यह कह सकते हैं कि कॉल करने वाले को एक बार उसके साथ समाप्त होने के बाद ऑब्जेक्ट को हटाना होगा। नकारात्मक पक्ष यह है कि कॉल करने वाले को मेमोरी का प्रबंधन करना पड़ता है, जो अतिरिक्त जटिलता जोड़ता है, और यह गलत हो सकता है, जिससे मेमोरी रिसाव हो सकता है अर्थात ऑब्जेक्ट को हटाना नहीं है, भले ही अब इसकी आवश्यकता न हो।
यह वह जगह है जहाँ स्मार्ट पॉइंटर्स आते हैं। निम्नलिखित उदाहरण शेयर्ड_एप्ट्र का उपयोग करता है - मेरा सुझाव है कि आप विभिन्न प्रकार के स्मार्ट पॉइंटर्स को देखें जो आप वास्तव में उपयोग करना चाहते हैं।
shared_ptr<std::string> foo() {
shared_ptr<std::string> str = new std::string();
// Do cool things to or using str
return str;
}
अब, SHAR_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 दोनों को फाइल करने के लिए एक पॉइंटर पास कर सकते हैं:
void setLog(const Foo & foo, const Bar & bar) {
File* file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
लेकिन फिर फाइल हटाने के लिए कौन जिम्मेदार है? यदि न तो फ़ाइल को हटाएं, तो हमारे पास स्मृति और संसाधन रिसाव दोनों हैं। हमें नहीं पता कि फू या बार पहले फाइल के साथ खत्म होगा या नहीं, इसलिए हम यह उम्मीद भी नहीं कर सकते हैं कि वह फाइल को खुद डिलीट कर दे। उदाहरण के लिए, अगर f फ़ाइल को हटाने से पहले बार के साथ समाप्त हो गया है, तो बार में अब एक अमान्य पॉइंटर है।
इसलिए, जैसा कि आपने अनुमान लगाया होगा, हम हमारी मदद करने के लिए स्मार्ट पॉइंटर्स का उपयोग कर सकते हैं।
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);
}
अब, फ़ाइल को हटाने के बारे में किसी को भी चिंता करने की आवश्यकता नहीं है - एक बार जब फू और बार दोनों समाप्त हो गए हैं और अब फ़ाइल के लिए कोई संदर्भ नहीं है (शायद फू और बार नष्ट होने के कारण), फ़ाइल स्वचालित रूप से हटा दी जाएगी।
RAII यह एक सरल लेकिन भयानक अवधारणा का एक अजीब नाम है। बेहतर है स्कोप बाउंड रिसोर्स मैनेजमेंट (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);
}
जैसा कि आप देख रहे हैं कि कई तरीके हैं जिनसे हम हलकान हो सकते हैं। विचार यह है कि हम संसाधन प्रबंधन को एक वर्ग में बदल देते हैं। इसकी वस्तु का आरंभिक संसाधन ("संसाधन अधिग्रहण प्रारंभिक है") प्राप्त करता है। जिस समय हम ब्लॉक (ब्लॉक स्कोप) से बाहर निकलते हैं, संसाधन फिर से मुक्त हो जाता है।
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
जब वे जिस संसाधन के दायरे से बाहर जाते हैं। कुछ स्मार्ट पॉइंटर्स, जैसे कि share_ptr आपको उन्हें एक तथाकथित डीलेटर बताने की अनुमति देता है, जिसका उपयोग इसके बजाय किया जाता है delete
। उदाहरण के लिए, आपको विंडो हैंडल, नियमित अभिव्यक्ति संसाधनों और अन्य मनमाने सामानों को प्रबंधित करने की अनुमति देता है, जब तक कि आप सही डिलेटर के बारे में साझा नहीं करते।
विभिन्न उद्देश्यों के लिए अलग-अलग स्मार्ट पॉइंटर्स हैं:
कोड:
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.
जैसा कि आप देखते हैं, प्लॉट-सोर्स (फ़ंक्शन एफएक्स) साझा किया जाता है, लेकिन हर एक की एक अलग प्रविष्टि है, जिस पर हम रंग सेट करते हैं। एक कमजोर_ वर्ग है जिसका उपयोग तब किया जाता है जब कोड को स्मार्ट पॉइंटर के स्वामित्व वाले संसाधन को संदर्भित करने की आवश्यकता होती है, लेकिन संसाधन के स्वामी होने की आवश्यकता नहीं होती है। कच्चे पॉइंटर पास करने के बजाय, आपको फिर एक कमजोर_प्रतिमा बनाना चाहिए। यह एक अपवाद को फेंक देगा जब यह नोटिस करता है कि आप संसाधन को कमजोर_प्रात अभिगम पथ द्वारा एक्सेस करने का प्रयास करते हैं, हालांकि संसाधन साझा करने के बाद भी कोई साझा_पार्ट नहीं है।
unique_ptr
, और sort
इसी तरह बदला भी जाएगा।
RAII यह सुनिश्चित करने के लिए डिज़ाइन प्रतिमान है कि चर अपने कंस्ट्रक्टरों में सभी आवश्यक आरंभीकरण को संभालते हैं और सभी को उनके तंतुओं में सफाई की आवश्यकता होती है। यह एक ही कदम के लिए सभी इनिशियलाइज़ेशन और क्लीनअप को कम करता है।
C ++ को RAII की आवश्यकता नहीं है, लेकिन यह तेजी से स्वीकार किया जाता है कि RAII विधियों का उपयोग करने से अधिक मजबूत कोड का उत्पादन होगा।
C ++ में RAII उपयोगी होने का कारण यह है कि C ++ आंतरिक रूप से चर के निर्माण और विनाश का प्रबंधन करता है क्योंकि वे प्रवेश करते हैं और गुंजाइश छोड़ते हैं, चाहे सामान्य कोड प्रवाह के माध्यम से या स्टैक अनइंडिंग के माध्यम से एक अपवाद द्वारा ट्रिगर किया गया हो। यह C ++ में एक फ्रीबी है।
इन तंत्रों के लिए सभी आरंभीकरण और सफाई को बांधने से, आपको यह सुनिश्चित किया जाता है कि C ++ आपके लिए भी इस काम का ध्यान रखेगा।
C ++ में RAII के बारे में बात करना आमतौर पर स्मार्ट पॉइंटर्स की चर्चा की ओर जाता है, क्योंकि सफाई के लिए पॉइंटर्स विशेष रूप से नाजुक होते हैं। मॉलोक या नए से अधिग्रहित ढेर-आवंटित मेमोरी का प्रबंधन करते समय, यह आमतौर पर प्रोग्रामर की जिम्मेदारी होती है कि पॉइंटर को नष्ट करने से पहले उस मेमोरी को मुक्त या हटा दें। स्मार्ट संकेत आरएआईआई दर्शन का उपयोग यह सुनिश्चित करने के लिए करेंगे कि ढेर आवंटित वस्तुएं किसी भी समय नष्ट हो जाएं क्योंकि सूचक चर नष्ट हो जाता है।
स्मार्ट पॉइंटर RAII की भिन्नता है। RAII का अर्थ है संसाधन का अधिग्रहण इनिशियलाइज़ेशन। स्मार्ट पॉइंटर उपयोग से पहले एक संसाधन (मेमोरी) प्राप्त करता है और फिर इसे एक विध्वंसक में स्वचालित रूप से फेंक देता है। दो चीजें होती हैं:
उदाहरण के लिए, एक और उदाहरण नेटवर्क सॉकेट RAII है। इस मामले में:
अब, जैसा कि आप देख सकते हैं, RAII ज्यादातर मामलों में एक बहुत ही उपयोगी उपकरण है क्योंकि यह लोगों को ढलने में मदद करता है।
C ++ स्मार्ट पॉइंटर्स के स्रोत नेट पर लाखों में हैं, जिनमें मेरे ऊपर प्रतिक्रियाएं भी शामिल हैं।
Boost में Boost.Interprocess की साझा मेमोरी के लिए इनमें से कई शामिल हैं । यह स्मृति प्रबंधन को बहुत सरल करता है, विशेष रूप से सिरदर्द-उत्पीड़क स्थितियों में जैसे कि जब आपके पास एक ही डेटा संरचना को साझा करने वाली 5 प्रक्रियाएं होती हैं: जब हर कोई स्मृति के साथ काम करता है, तो आप चाहते हैं कि यह स्वचालित रूप से मुक्त हो जाए और वहां बैठने की कोशिश न करें। जो delete
स्मृति के एक टुकड़े पर कॉल करने के लिए ज़िम्मेदार होना चाहिए , ऐसा न हो कि आप स्मृति रिसाव के साथ समाप्त हो जाएं, या एक संकेतक जो दो बार गलती से मुक्त हो जाए और पूरे ढेर को दूषित कर सके।
शून्य फू () { std :: string bar; // // यहाँ और अधिक कोड // }
कोई भी बात नहीं है, क्योंकि फू () फ़ंक्शन के दायरे को पीछे छोड़ दिया गया है, बार को ठीक से हटाया जा रहा है।
आंतरिक रूप से std :: string कार्यान्वयन अक्सर संदर्भ गिने बिंदुओं का उपयोग करते हैं। इसलिए आंतरिक स्ट्रिंग को केवल तब कॉपी करने की आवश्यकता होती है जब स्ट्रिंग्स की एक कॉपी बदल जाती है। इसलिए एक संदर्भ जो स्मार्ट पॉइंटर गिना जाता है, केवल आवश्यक होने पर कुछ कॉपी करना संभव बनाता है।
इसके अलावा, आंतरिक संदर्भ की गिनती यह संभव बनाती है कि आंतरिक स्ट्रिंग की प्रतिलिपि की आवश्यकता नहीं होने पर मेमोरी ठीक से हटा दी जाएगी।