अंतिम अद्यतन:2026-10-12
C++ RAII संसाधन प्रबंधन¶
0.प्रस्तावना¶
C भाषा में मेमोरी आवंटन के तीन प्रकार हैं: स्टैटिक, ऑटोमैटिक तथा डायनामिक। स्टैटिक वेरिएबल सोर्स फ़ाइल में अंतर्निहित स्थिरांक होते हैं; चूँकि उनका आकार ज्ञात होता है और वे कभी नहीं बदलते, वे उतने रोचक नहीं। ऑटोमैटिक आवंटन को स्टैक आवंटन माना जा सकता है — जब कोई लेक्सिकल ब्लॉक प्रवेश करता है तब स्पेस आवंटित होता है, और ब्लॉक से निकलते ही मुक्त हो जाता है। इसकी सबसे महत्वपूर्ण विशेषता इसी से सीधे जुड़ी है। C99 से पहले, ऑटोमैटिक आवंटित वेरिएबल का आकार संकलन के समय ज्ञात होना आवश्यक था। इसका अर्थ है — कोई भी string, list, map तथा इनसे व्युत्पन्न कोई भी स्ट्रक्चर अवश्य ही heap की डायनामिक मेमोरी में रहना चाहिए।
प्रोग्रामर चार मूल ऑपरेशनों से स्पष्ट रूप से डायनामिक मेमोरी आवंटित तथा मुक्त करते हैं: malloc, realloc, calloc तथा free। पहले दो कोई आरंभीकरण नहीं करते — मेमोरी में कचरा हो सकता है। free को छोड़कर शेष सभी विफल हो सकते हैं। ऐसी स्थिति में वे एक null पॉइंटर लौटाते हैं, जिस तक पहुँच अपरिभाषित व्यवहार (undefined behavior) है; सर्वोत्तम स्थिति में आपका प्रोग्राम क्रैश होगा। निकृष्ट स्थिति में आपका प्रोग्राम कुछ समय काम करता दिखेगा, क्रैश होने से पहले कचरा डेटा प्रोसेस करता हुआ।
उदाहरण के लिए:
int main() {
char *str = (char *) malloc(7);
strcpy(str, "toptal");
printf("char array = \"%s\" @ %u\n", str, str);
str = (char *) realloc(str, 11);
strcat(str, ".com");
printf("char array = \"%s\" @ %u\n", str, str);
free(str);
return(0);
}
आउटपुट:
भले ही कोड बहुत सरल है, इसमें पहले से ही एक एंटी-पैटर्न तथा एक संदिग्ध निर्णय शामिल है। वास्तविक जीवन में आपको बाइट्स की संख्या सीधे नहीं लिखनी चाहिए, बल्कि sizeof फंक्शन का उपयोग करना चाहिए। इसी प्रकार, हमने char* array को आवश्यक string आकार के ठीक दोगुने (null terminator के लिए string लंबाई से एक अधिक) आवंटित किया — जो काफी महँगा ऑपरेशन है। अधिक जटिल प्रोग्राम संभवतः एक बड़ा string बफ़र बनाएगा, जिससे string का आकार बढ़ सके।
1.RAII का आविष्कार: एक नई आशा¶
सभी हाथ से किए प्रबंधन — कम से कम कहें तो — अप्रिय हैं। 80 के दशक के मध्य में, Bjarne Stroustrup ने अपनी नई भाषा C++ के लिए एक नया प्रतिमान गढ़ा। उन्होंने इसे “संसाधन का अर्जन ही आरंभीकरण है” कहा, जिसकी मूल अंतर्दृष्टि इस प्रकार है: निर्दिष्ट किया जा सकता है कि ऑब्जेक्ट में कंस्ट्रक्टर तथा डिस्ट्रक्टर हों, जिन्हें उचित समय पर कंपाइलर स्वतः कॉल करता है — इससे दिए गए ऑब्जेक्ट की मेमोरी प्रबंधित करने की अधिक सुविधाजनक विधि मिलती है। आवश्यकता अनुसार, यह तकनीक मेमोरी न होने वाले संसाधनों के लिए भी उपयोगी है।
इसका अर्थ है — ऊपर का उदाहरण C++ में अधिक संक्षिप्त हो जाता है:
int main() {
std::string str = std::string ("toptal");
std::cout << "string object: " << str << " @ " << &str << "\n";
str += ".com";
std::cout << "string object: " << str << " @ " << &str << "\n";
return(0);
}
आउटपुट:
उपरोक्त उदाहरण में हमने कोई हाथ से मेमोरी प्रबंधन नहीं किया! string ऑब्जेक्ट बना, ओवरलोडेड मेथड कॉल हुआ, और फंक्शन से निकलते ही स्वतः नष्ट हो गया। दुर्भाग्य से, यही सरलता अन्य समस्याएँ भी जन्म देती है। एक उदाहरण विस्तार से देखिए:
vector<string> read_lines_from_file(string &file_name) {
vector<string> lines;
string line;
ifstream file_handle (file_name.c_str());
while (file_handle.good() && !file_handle.eof() && file_handle.peek()!=EOF) {
getline(file_handle, line);
lines.push_back(line);
}
file_handle.close();
return lines;
}
int main(int argc, char* argv[]) {
// get file name from the first argument
string file_name (argv[1]);
int count = read_lines_from_file(file_name).size();
cout << "File " << file_name << " contains " << count << " lines.";
return 0;
}
आउटपुट:
यह बहुत सरल दिखता है। vector भरता है, लौटाया जाता है तथा उपयोग होता है। तथापि, प्रदर्शन से जुड़े कुशल प्रोग्रामर के रूप में इसमें कुछ बातें चुभती हैं: return कथन में, value semantics के उपयोग के कारण, vector नष्ट होने से ठीक पहले एक नए vector में कॉपी हो जाता है।
आधुनिक C++ में अब यह कठोर आवश्यकता नहीं रही। C++11 ने move semantics की संकल्पना प्रस्तुत की, जिसमें स्रोत को वैध (ताकि उसे ठीक से नष्ट किया जा सके) परंतु अनिर्दिष्ट अवस्था में रखा जाता है। कंपाइलर के लिए, रिटर्न कॉल ऐसी स्थिति है जिसे move semantics द्वारा अनुकूलित करना सर्वाधिक सरल है, क्योंकि वह जानता है कि किसी और पहुँच से ठीक पहले स्रोत नष्ट हो जाएगा। तथापि, इस उदाहरण का उद्देश्य यह समझाना है कि लोगों ने 80 के दशक के अंत तथा 90 के दशक के आरंभ में ढेर सारी कचरा-संग्रह वाली भाषाएँ क्यों बनाईं — जब C++ की move semantics उपलब्ध नहीं थी।
बड़ी मात्रा में डेटा वाली फ़ाइलों के लिए यह महँगा सिद्ध हो सकता है। इसे अनुकूलित कर केवल एक पॉइंटर लौटाते हैं। सिंटैक्स कुछ बदला गया है, शेष कोड वही है:
vector<string> * read_lines_from_file(string &file_name) {
vector<string> * lines;
string line;
ifstream file_handle (file_name.c_str());
while (file_handle.good() && !file_handle.eof() && file_handle.peek()!=EOF) {
getline(file_handle, line);
lines->push_back(line);
}
file_handle.close();
return lines;
}
int main(int argc, char* argv[]) {
// get file name from the first argument
string file_name (argv[1]);
int count = read_lines_from_file(file_name).size();
cout << "File " << file_name << " contains " << count << " lines.";
return 0;
}
आउटपुट:
प्रोग्राम क्रैश! हमें बस उपरोक्त lines के लिए मेमोरी आवंटित करनी है:
अब यह चल जाएगा!
दुर्भाग्य से, भले ही यह सिद्ध दिखता है, इसमें अब भी एक दोष है: यह मेमोरी लीक करता है। C++ में, heap की ओर इशारा करने वाला पॉइंटर आवश्यकता समाप्त होने पर हाथ से delete करना आवश्यक है; अन्यथा अंतिम पॉइंटर के स्कोप से बाहर जाते ही वह मेमोरी अनुपयोगी हो जाती है, और प्रोसेस समाप्त होकर ऑपरेटिंग सिस्टम द्वारा प्रबंधित होने तक उबारी नहीं जा सकती। आधुनिक C++ की मानक शैली यहाँ unique_ptr उपयोग करती है, जो वांछित व्यवहार कार्यान्वित करता है — पॉइंटर स्कोप से बाहर जाते ही वह इशारा किए ऑब्जेक्ट को delete कर देता है। तथापि, यह व्यवहार C++11 में ही भाषा का भाग बना।
यहाँ, C++11 से पहले के सिंटैक्स का सीधा उपयोग किया जा सकता है — बस main में परिवर्तन करना है:
vector<string> * read_lines_from_file(string &file_name) {
vector<string> * lines = new vector<string>;
string line;
ifstream file_handle (file_name.c_str());
while (file_handle.good() && !file_handle.eof() && file_handle.peek()!=EOF) {
getline(file_handle, line);
lines->push_back(line);
}
file_handle.close();
return lines;
}
int main(int argc, char* argv[]) {
// get file name from the first argument
string file_name (argv[1]);
vector<string> * file_lines = read_lines_from_file(file_name);
int count = file_lines->size();
delete file_lines;
cout << "File " << file_name << " contains " << count << " lines.";
return 0;
}
मेमोरी का आवंटन तथा मुक्तिकरण हाथ से।
दुर्भाग्य से, जैसे ही प्रोग्राम उपरोक्त दायरे से आगे बढ़ता है, यह निर्णय करना कि पॉइंटर कब और कहाँ delete होना चाहिए, तेज़ी से कठिन हो जाता है। जब कोई फंक्शन पॉइंटर लौटाता है — क्या अब वह आपका है? काम पूरा होने पर आप उसे स्वयं delete करें, या वह किसी ऐसे डेटा स्ट्रक्चर का भाग है जिसे आगे एक साथ मुक्त किया जाएगा? एक ओर चूक हुई तो memory leak; दूसरी ओर चूक हुई तो आपने उस डेटा स्ट्रक्चर तथा संभावित अन्य स्ट्रक्चरों को बिगाड़ दिया, क्योंकि वे अब अमान्य पॉइंटर को dereference करने का प्रयास करते हैं।
2.“कचरा-संग्राहक उपयोग करो, flyboy!”¶
कचरा-संग्राहक कोई नई तकनीक नहीं है। इन्हें John McCarthy ने 1959 में Lisp के लिए आविष्कृत किया। 1980 में, Smalltalk-80 के आगमन के साथ कचरा-संग्रह मुख्यधारा बनने लगा। परंतु 1990 का दशक इस तकनीक की वास्तविक प्रौढ़ता था: 1990 से 2000 के बीच अनेक भाषाएँ जारी हुईं, सभी एक न एक प्रकार के कचरा-संग्रह का उपयोग करती थीं — Haskell, Python, Lua, Java, JavaScript, Ruby, OCaml तथा C# सर्वाधिक प्रसिद्ध हैं।
कचरा-संग्रह क्या है? संक्षेप में, यह हाथ से की जाने वाली मेमोरी प्रबंधन को स्वचालित करने वाली तकनीकों का समूह है। यह प्रायः हाथ से मेमोरी प्रबंधन वाली भाषाओं (जैसे C तथा C++) के लिए लाइब्रेरी के रूप में दिया जाता है, परंतु ऐसी भाषाओं में अधिक प्रचलित है जिन्हें इसकी आवश्यकता होती है। सबसे बड़ा लाभ — प्रोग्रामर को मेमोरी की चिंता ही नहीं करनी पड़ती। सब कुछ abstract हो जाता है। उदाहरण के लिए, ऊपर की फ़ाइल-पढ़ने वाली कोड के समतुल्य Python इस प्रकार है:
def read_lines_from_file(file_name):
lines = []
with open(file_name) as fp:
for line in fp:
lines.append(line)
return lines
if __name__ == '__main__':
import sys
file_name = sys.argv[1]
count = len(read_lines_from_file(file_name))
print("File {} contains {} lines.".format(file_name, count))
lines की array पहली बार उसे असाइन करते समय अस्तित्व में आती है, और बिना कॉपी किए कॉलर स्कोप में लौटा दी जाती है। समय अनिश्चित होने के कारण, वह स्कोप से बाहर जाने के बाद किसी समय कचरा-संग्राहक द्वारा साफ़ कर दी जाती है। रोचक बात यह है कि Python में, गैर-मेमोरी संसाधनों के लिए RAII अभिजात शैली (idiomatic) नहीं है। अनुमति है — हम with ब्लॉक के बजाय सीधे fp = open(file_name) लिखकर GC से सफ़ाई करवा सकते हैं; परंतु अनुशंसित पैटर्न है कि जहाँ संभव हो context manager उपयोग करें, ताकि उन्हें निश्चित समय पर मुक्त किया जा सके।
भले ही मेमोरी प्रबंधन सरल हो गया, इसका बड़ा मूल्य चुकाना पड़ता है। reference-counting कचरा-संग्रह में, सभी वेरिएबल असाइनमेंट तथा स्कोप-निकास पर reference अद्यतन की थोड़ी लागत आती है। mark-and-sweep तंत्र में, GC द्वारा मेमोरी साफ़ करते समय पूरे प्रोग्राम का निष्पादन अप्रत्याशित अंतराल पर रुक जाता है — इसे प्रायः “संसार-रोक” (stop-the-world) घटना कहते हैं। दोनों तंत्र एक साथ उपयोग करने वाले Python जैसे कार्यान्वयन दोनों दंड भुगताते हैं। ये समस्याएँ प्रदर्शन महत्वपूर्ण हो या real-time अनुप्रयोग चाहिए — ऐसी स्थितियों में कचरा-संग्रह भाषाओं की उपयोगिता घटा देती हैं। नीचे के खिलौना प्रोग्राम पर भी वास्तविक प्रदर्शन-गिरावट देखी जा सकती है:
$ make cpp && time ./c++ makefile
g++ -o c++ c++.cpp
File makefile contains 38 lines.
real 0m0.016s
user 0m0.000s
sys 0m0.015s
$ time python3 python3.py makefile
File makefile contains 38 lines.
real 0m0.041s
user 0m0.015s
sys 0m0.015s
Python संस्करण का वास्तविक समय लगभग C++ संस्करण का तीन गुना है। भले ही यह पूरा अंतर कचरा-संग्रह के श्रेय पर नहीं लिखा जा सकता, फिर भी यह उल्लेखनीय है।
3.स्वामित्व (ownership): RAII का जागरण¶
हम जानते हैं कि ऑब्जेक्ट का जीवनकाल उसके स्कोप से निर्धारित होता है। परंतु कभी-कभी हमें एक ऐसा ऑब्जेक्ट बनाना ज़रूरी या उपयोगी होता है जो उस स्कोप से स्वतंत्र रहे जिसमें उसे बनाया गया। C++ में ऐसे ऑब्जेक्ट बनाने के लिए new ऑपरेटर उपयोग होता है; ऑब्जेक्ट नष्ट करने के लिए delete ऑपरेटर उपयोग किया जा सकता है। new ऑपरेटर से बने ऑब्जेक्ट डायनामिक रूप से आवंटित होते हैं — अर्थात् डायनामिक मेमोरी (heap या free store) में। अतः new से बना ऑब्जेक्ट तब तक रहता है जब तक delete द्वारा स्पष्ट रूप से नष्ट न किया जाए।
new तथा delete उपयोग करते समय होने वाली कुछ त्रुटियाँ:
-
ऑब्जेक्ट (या मेमोरी) लीक: new से ऑब्जेक्ट आवंटित किया, पर उसे delete करना भूल गए।
-
समय से पहले deletion (या dangling reference): ऑब्जेक्ट की ओर इशारा करने वाला दूसरा पॉइंटर था, ऑब्जेक्ट delete कर दिया, पर अन्य पॉइंटर्स अभी भी उसे रेफर कर रहे हैं।
-
दोहरा deletion: एक ही ऑब्जेक्ट को दो बार delete करने का प्रयास।
सामान्यतः स्कोप-वेरिएबल ही प्राथमिकता रखते हैं। परंतु RAII को new तथा delete के विकल्प के रूप में उपयोग किया जा सकता है, ताकि ऑब्जेक्ट स्कोप से स्वतंत्र रह सके। इस तकनीक में heap पर आवंटित ऑब्जेक्ट का पॉइंटर लेकर उसे एक handle/manager ऑब्जेक्ट में रखा जाता है। बाद वाले में एक डिस्ट्रक्टर होता है जो उस ऑब्जेक्ट के विनाश की ज़िम्मेदारी लेता है। इससे यह सुनिश्चित होता है कि ऑब्जेक्ट उस तक पहुँचना चाहने वाले प्रत्येक फंक्शन के लिए उपलब्ध रहे, और handle ऑब्जेक्ट के जीवनकाल समाप्त होते ही वह नष्ट हो जाए — बिना किसी स्पष्ट सफ़ाई के।
C++ स्टैंडर्ड लाइब्रेरी से RAII उपयोग करने वाले उदाहरण हैं std::string तथा std::vector।
यह कोड देखिए:
void fn(const std::string& str)
{
std::vector<char> vec;
for (auto c : str)
vec.push_back(c);
// do something
}
जब vector बनाया जाता है और उसमें एलिमेंट्स डाले जाते हैं, तब आपको ऐसे एलिमेंट्स की मेमोरी आवंटन-मुक्ति की चिंता नहीं करनी पड़ती। vector अपने heap पर एलिमेंट्स के लिए new से स्पेस आवंटित करता है, तथा delete से उसे मुक्त करता है। vector के उपयोगकर्ता के रूप में आपको कार्यान्वयन विवरण से कोई सरोकार नहीं, और आप भरोसा रखते हैं कि vector लीक नहीं करेगा। इस स्थिति में, vector उसके एलिमेंट्स का handle ऑब्जेक्ट है।
स्टैंडर्ड लाइब्रेरी में RAII के अन्य उदाहरण हैं std::shared_ptr, std::unique_ptr तथा std::lock_guard।
इस तकनीक का एक और नाम SBRM है — Scope-Bound Resource Management का संक्षिप्त रूप।
अब हम उपरोक्त फ़ाइल-पढ़ने के उदाहरण को संशोधित करते हैं:
#include <iostream>
#include <vector>
#include <cstring>
#include <fstream>
#include <bits/unique_ptr.h>
using namespace std;
unique_ptr<vector<string>> read_lines_from_file(string &file_name) {
unique_ptr<vector<string>> lines(new vector<string>);
string line;
ifstream file_handle (file_name.c_str());
while (file_handle.good() && !file_handle.eof() && file_handle.peek()!=EOF) {
getline(file_handle, line);
lines->push_back(line);
}
file_handle.close();
return lines;
}
int main(int argc, char* argv[]) {
// get file name from the first argument
string file_name (argv[1]);
int count = read_lines_from_file(file_name).get()->size();
cout << "File " << file_name << " contains " << count << " lines.";
return 0;
}
4.अंत में जाकर आप RAII की वास्तविक शक्ति समझते हैं।¶
कंपाइलर के आविष्कार से ही, हाथ से की जाने वाली मेमोरी प्रबंधन प्रोग्रामरों का वह दुःस्वप्न रहा है जिससे बचने के उपाय वे ढूँढते रहे हैं। RAII एक आशाजनक पैटर्न है, परंतु कुछ अजीब workaround के बिना यह heap-आवंटित ऑब्जेक्ट्स पर काम ही नहीं करता, इसलिए C++ में यह प्रभावित रहा। इसीलिए 90 के दशक में कचरा-संग्रह वाली भाषाओं का विस्फोट हुआ — उद्देश्य था प्रोग्रामर का जीवन सुखद बनाना, चाहे प्रदर्शन की कीमत पर ही सही।
अंत में, RAII का सारांश:
-
संसाधन डिस्ट्रक्टर में मुक्त किया जाता है
-
क्लास का इंस्टेंस स्टैक-आवंटित होता है
- संसाधन कंस्ट्रक्टर में अर्जित किया जाता है।
RAII का अर्थ है “संसाधन का अर्जन ही आरंभीकरण है”।
सामान्य उदाहरण:
-
फ़ाइल ऑपरेशन
-
स्मार्ट पॉइंटर
-
म्यूटेक्स (mutex)
5.संदर्भ लेख¶
1. https://www.toptal.com/software/eliminating-garbage-collector#remote-developer-job
संबंधित विषय¶
- C++ डिफ़ॉल्ट कंस्ट्रक्टर से बचना | C++那些事
- C++ क्लास इनिशियलाइज़र लिस्ट इडियम | C++那些事
- C++ में फ़ंक्शन टेम्पलेट की जगह क्लास टेम्पलेट | C++那些事
- C++ copy-and-swap इडियम | C++那些事
- C++ डिपेंडेंसी इंजेक्शन | C++那些事
- C++ enum class स्ट्रॉन्ग टाइप्ड एनम | C++那些事
- C++ Header-Only तथा कार्यान्वयन का पृथक्करण | C++那些事
- C++ लेज़ी इवैल्यूएशन इडियम | C++那些事
- C++ लुकअप टेबल इडियम | C++那些事
- C++ मेंबर पॉइंटर तथा टाइप जानकारी | C++那些事