c ढेर बनाम ढेर पर सी में "कक्षाएं" बनाना?




struct stack (10)

चूंकि कोई फ़ंक्शन केवल आवंटित स्ट्रक्चर को वापस कर सकता है यदि इसमें अन्य आवंटित structs के लिए कोई पॉइंटर्स नहीं है। यदि इसमें केवल साधारण ऑब्जेक्ट्स (int, bool, floats, chars और उनमें से सरणी हैं लेकिन कोई सूचक नहीं है ) तो आप इसे स्टैक पर आवंटित कर सकते हैं। लेकिन आपको पता होना चाहिए कि यदि आप इसे वापस कर देते हैं, तो इसकी प्रतिलिपि बनाई जाएगी। यदि आप पॉइंटर्स को अन्य structs पर अनुमति देना चाहते हैं, या प्रतिलिपि से बचना चाहते हैं तो ढेर का उपयोग करें।

लेकिन यदि आप एक शीर्ष स्तर इकाई में संरचना बना सकते हैं और केवल इसे फ़ंक्शंस में उपयोग कर सकते हैं और इसे कभी वापस नहीं कर सकते हैं, तो स्टैक उचित है

जब भी मैं एक सी "कक्षा" देखता हूं (किसी भी संरचना का उपयोग उन कार्यों तक पहुंचने के लिए किया जाता है जो पहले तर्क के रूप में पॉइंटर लेते हैं) मैं उन्हें इस तरह कार्यान्वित करता हूं:

typedef struct
{
    int member_a;
    float member_b;
} CClass;

CClass* CClass_create();
void CClass_destroy(CClass *self);
void CClass_someFunction(CClass *self, ...);
...

और इस मामले में CClass_create हमेशा यह स्मृति की malloc और उस पर एक सूचक देता है।

जब भी मैं सी ++ में अनावश्यक रूप से new आ जाता हूं, तो आमतौर पर यह सी ++ प्रोग्रामर पागल ड्राइव करता है, लेकिन यह अभ्यास सी में स्वीकार्य लगता है। क्या देता है? क्या कुछ कारण है कि ढेर-आवंटित संरचना "कक्षाएं" इतनी आम क्यों हैं?


क्या आपका सवाल है "क्यों सी में गतिशील रूप से स्मृति आवंटित करना सामान्य है और सी ++ में यह नहीं है"?

सी ++ में ऐसी जगहें हैं जो नई अनावश्यक बनाती हैं। प्रतिलिपि, चाल और सामान्य रचनाकार, विनाशक, मानक पुस्तकालय, आवंटक।

लेकिन सी में आप इसके चारों ओर नहीं मिल सकते हैं।


यह बहुत सारे जवाब पैदा करता है क्योंकि यह कुछ हद तक राय आधारित है । फिर भी मैं यह बताना चाहता हूं कि मैं ढेर पर आवंटित अपने "सी ऑब्जेक्ट्स" को व्यक्तिगत रूप से क्यों पसंद करता हूं। इसका कारण है कि मेरे खेतों में सभी छिपे हुए (बोलें: निजी ) उपभोग करने वाले कोड से। इसे एक अपारदर्शी सूचक कहा जाता है। अभ्यास में, इसका मतलब है कि आपकी हेडर फ़ाइल उपयोग में struct को परिभाषित नहीं करती है, यह केवल इसे घोषित करती है। प्रत्यक्ष परिणाम के रूप में, उपभोग करने वाले कोड को struct के आकार को नहीं पता हो सकता है और इसलिए आवंटन आवंटन असंभव हो जाता है।

लाभ यह है कि: उपभोग करने वाला कोड कभी भी struct की परिभाषा पर निर्भर नहीं हो सकता है, इसका मतलब यह है कि आप असंभव है कि आप किसी भी तरह से बाहर की struct के सामग्रियों की सामग्री प्रस्तुत करते हैं और जब आप struct बदलते हैं तो आप उपभोग कोड के अनावश्यक पुनर्मूल्यांकन से बचते हैं।

खेतों को private घोषित करके पहला मुद्दा सी ++ में संबोधित किया जाता है। लेकिन आपकी class की परिभाषा अभी भी उन सभी संकलन इकाइयों में आयात की जाती है जो इसका उपयोग करते हैं, जिससे उन्हें पुन: संकलित करना आवश्यक होता है, भले ही केवल आपके private सदस्य बदलते हैं। अक्सर सी ++ में उपयोग किया जाने वाला समाधान pimpl पैटर्न है: सभी निजी सदस्यों को दूसरी struct (या: class ) में रखें जो केवल कार्यान्वयन फ़ाइल में परिभाषित है। बेशक, यह आपके pimpl को ढेर पर आवंटित करने की आवश्यकता है।

उसमें जोड़ना: आधुनिक ओओपी भाषाएं (जैसे java या c# ) का मतलब ऑब्जेक्ट आवंटित करना है (और आम तौर पर यह तय करना है कि क्या यह ढेर या ढेर आंतरिक रूप से है) कॉलिंग कोड के बिना उनकी परिभाषा के बारे में जानना।


सी में कुछ चीजें हैं जो सी ++ प्रोग्रामर मानते हैं जैसे कि।

  1. सार्वजनिक और निजी विनिर्देशक
  2. रचनाकार और विनाशक

इस दृष्टिकोण का बड़ा लाभ यह है कि आप अपनी सी फाइल में संरचना को छुपा सकते हैं और अपने निर्माण और नष्ट कार्यों के साथ सही निर्माण और विनाश को मजबूर कर सकते हैं।

यदि आप अपनी .h फ़ाइल में संरचना का पर्दाफाश करते हैं, तो इसका अर्थ यह होगा कि उपयोगकर्ता सीधे सदस्यों तक पहुंच सकते हैं जो encapsulation को तोड़ता है। इसके अलावा बनाने के लिए मजबूर नहीं करना आपके ऑब्जेक्ट के गलत निर्माण की अनुमति देता है।


मैं "कंस्ट्रक्टर" को एक void CClass_create(CClass*);

यह संरचना का एक उदाहरण / संदर्भ वापस नहीं करेगा, लेकिन एक पर बुलाया जाएगा।

चाहे यह "ढेर" या गतिशील रूप से आवंटित किया गया हो, यह पूरी तरह से आपके उपयोग परिदृश्य आवश्यकताओं पर निर्भर करता है। हालांकि आप इसे आवंटित करते हैं, आप केवल पैरामीटर के रूप में आवंटित संरचना को पार कर CClass_create() कॉल CClass_create()

{
    CClass stk;
    CClass_create(&stk);

    CClass *dyn = malloc(sizeof(CClass));
    CClass_create(dyn);

    CClass_destroy(&stk); // the local object lifetime ends here, dyn lives on
}

// and later, assuming you kept track of dyn
CClass_destroy(dyn); // destructed
free(dyn); // deleted

बस एक स्थानीय (स्टैक पर आवंटित) के संदर्भ को वापस न करने के लिए सावधान रहें, क्योंकि यह यूबी है।

हालांकि आप इसे आवंटित करते हैं, आपको void CClass_destroy(CClass*); को कॉल करने की आवश्यकता होगी void CClass_destroy(CClass*); सही स्थान पर (उस वस्तु के जीवनकाल का अंत जो है), और यदि गतिशील रूप से आवंटित किया गया है, तो उस स्मृति को भी मुक्त करें।

आवंटन / विलोपन और निर्माण / विनाश के बीच अंतर, वे समान नहीं हैं (भले ही सी ++ में वे स्वचालित रूप से एक साथ मिल सकें)।


इसके अनेक कारण हैं।

  1. "अपारदर्शी" पॉइंटर्स का उपयोग करना
  2. विनाशकों की कमी
  3. एम्बेडेड सिस्टम (ढेर ओवरफ्लो समस्या)
  4. कंटेनर
  5. जड़ता
  6. "आलस्य"

आइए संक्षेप में उन पर चर्चा करें।

अपारदर्शी पॉइंटर्स के लिए , यह आपको कुछ ऐसा करने में सक्षम बनाता है:

struct CClass_;
typedef struct CClass_ CClass;
// the rest as in your example

इसलिए, उपयोगकर्ता को struct CClass_ की परिभाषा नहीं दिखाई struct CClass_ , struct CClass_ उसे परिवर्तन से इन्सुलेट किया जाता है और अन्य रोचक सामग्री को सक्षम बनाता है, जैसे विभिन्न प्लेटफ़ॉर्म के लिए कक्षा को अलग-अलग कार्यान्वित करना।

बेशक, यह CClass ढेर चर का उपयोग करने पर रोक लगाता है। लेकिन, ओटीओएच, कोई देख सकता है कि यह CClass ऑब्जेक्ट्स को स्थिर रूप से आवंटित करने पर रोक नहीं लगाता है (कुछ पूल से) - CClass_create द्वारा लौटाया गया है या शायद एक और फ़ंक्शन CClass_create_static जैसे।

CClass कमी - चूंकि सी कंपाइलर स्वचालित रूप से आपके CClass स्टैक ऑब्जेक्ट्स को नष्ट नहीं करेगा, आपको इसे स्वयं करने की आवश्यकता है (मैन्युअल रूप से विनाशक फ़ंक्शन को कॉल करना)। इसलिए, केवल एकमात्र लाभ यह तथ्य है कि आवंटन ढेर आवंटन से अधिक तेज़ आवंटन होता है। ओटीओएच, आपको ढेर का उपयोग करने की ज़रूरत नहीं है - आप पूल, या एक क्षेत्र, या ऐसी कुछ चीज़ों से आवंटित कर सकते हैं, और यह नीचे चर्चा की गई स्टैक आवंटन की संभावित समस्याओं के बिना, आवंटन के रूप में लगभग तेज हो सकता है।

एम्बेडेड सिस्टम - स्टैक एक "अनंत" संसाधन नहीं है, आपको पता है। निश्चित रूप से, आज के "नियमित" ओएस (POSIX, विंडोज़ ...) पर अधिकांश अनुप्रयोगों के लिए, यह लगभग है। लेकिन, एम्बेडेड सिस्टम पर, ढेर कुछ केबी के रूप में कम हो सकता है। यह चरम है, लेकिन यहां तक ​​कि "बड़े" एम्बेडेड सिस्टमों में स्टैक है जो एमबी में हैं। तो, अगर अधिक इस्तेमाल किया जाता है तो यह खत्म हो जाएगा। जब ऐसा होता है, तो ज्यादातर गारंटी नहीं होती है कि क्या होगा - AFAIK, सी और सी ++ दोनों में "अपरिभाषित व्यवहार" है। CClass_create() , CClass_create() जब आप स्मृति से बाहर होते हैं तो नल पॉइंटर वापस कर सकते हैं, और आप इसे संभाल सकते हैं।

कंटेनर - सी ++ उपयोगकर्ता स्टैक आवंटन जैसे उपयोगकर्ता, लेकिन, यदि आप स्टैक पर std::vector बनाते हैं, तो इसकी सामग्री आवंटित की जाएगी। आप निश्चित रूप से इसे ट्विक कर सकते हैं, लेकिन यह डिफ़ॉल्ट व्यवहार है, और यह लोगों को जीवन को इतना आसान बनाता है कि "कंटेनर के सभी सदस्यों को ढेर-आवंटित किया जाता है" ताकि यह पता लगाने की कोशिश की जा सके कि वे कैसे हैं।

जड़ता - ठीक है, ओओ स्मॉलटाक से आया था। सबकुछ गतिशील है, इसलिए, सी के लिए "प्राकृतिक" अनुवाद "ढेर पर सब कुछ डाल" है। तो, पहले उदाहरण इस तरह थे और उन्होंने कई वर्षों तक दूसरों को प्रेरित किया।

" आलस्य " - यदि आप जानते हैं कि आप केवल स्टैक ऑब्जेक्ट चाहते हैं, तो आपको कुछ चाहिए:

CClass CClass_make();
void CClass_deinit(CClass *me);

लेकिन, यदि आप दोनों ढेर और ढेर की अनुमति देना चाहते हैं, तो आपको यह जोड़ना होगा:

CClass *CClass_create();
void CClass_destroy(CClass *me);

यह कार्यान्वयनकर्ता के लिए अधिक काम है, लेकिन यह उपयोगकर्ता को भी भ्रमित कर रहा है। कोई थोड़ा अलग इंटरफेस बना सकता है, लेकिन यह इस तथ्य को नहीं बदलेगा कि आपको कार्यों के दो सेट चाहिए।

बेशक, "कंटेनर" कारण भी आंशिक रूप से "आलस्य" कारण है।


यदि किसी प्रकार की वस्तुओं की अधिकतम संख्या को एक साथ मौजूद होने की आवश्यकता होगी, तो सिस्टम को प्रत्येक "लाइव" उदाहरण के साथ कुछ करने में सक्षम होना चाहिए, और प्रश्न वाले आइटम बहुत अधिक पैसे नहीं लेते हैं, सर्वोत्तम दृष्टिकोण आमतौर पर न तो ढेर आवंटन और न ही आवंटन ढेर होता है, बल्कि "बनावट" और "नष्ट" विधियों के साथ-साथ एक स्थैतिक रूप से आवंटित सरणी भी होती है। किसी सरणी का उपयोग करना ऑब्जेक्ट्स की एक लिंक्ड सूची को बनाए रखने की आवश्यकता से बच जाएगा, और उस मामले को संभालना संभव होगा जहां ऑब्जेक्ट को तुरंत "नष्ट नहीं किया जा सकता है क्योंकि यह" व्यस्त "है (उदाहरण के लिए यदि डेटा किसी चैनल पर बाधा के माध्यम से आ रहा है या डीएमए जब उपयोगकर्ता कोड का फैसला करता है कि यह अब चैनल में दिलचस्पी नहीं लेता है और इसका निपटान करता है, तो उपयोगकर्ता कोड "लंबित होने पर निपटान" सेट कर सकता है और लंबित इंटरप्ट या डीएमए ओवरराइट स्टोरेज होने की चिंता किए बिना वापस लौटा सकता है जिसे अब आवंटित नहीं किया जाता है यह]।

फिक्स्ड-साइज्ड ऑब्जेक्ट्स के एक निश्चित आकार के पूल का उपयोग करके मिश्रित आकार के ढेर से भंडारण लेने से आवंटन और डी-आवंटन अधिक अनुमानित होता है। उन मामलों में दृष्टिकोण बहुत अच्छा नहीं है जहां मांग परिवर्तनीय है और वस्तुएं बहुत अधिक जगह लेती हैं (व्यक्तिगत रूप से या सामूहिक रूप से), लेकिन जब मांग अधिकतर सुसंगत होती है (उदाहरण के लिए किसी एप्लिकेशन को हर समय 12 वस्तुओं की आवश्यकता होती है, और कभी-कभी 3 तक की आवश्यकता होती है अधिक) यह वैकल्पिक दृष्टिकोण से काफी बेहतर काम कर सकता है। एक कमजोरी यह है कि किसी भी सेटअप को उस जगह पर किया जाना चाहिए जहां स्थिर बफर घोषित किया गया हो, या ग्राहकों में निष्पादन योग्य कोड द्वारा किया जाना चाहिए। क्लाइंट साइट पर चर-प्रारंभिक वाक्यविन्यास का उपयोग करने का कोई तरीका नहीं है।

संयोग से, इस दृष्टिकोण का उपयोग करते समय क्लाइंट कोड को कुछ भी पॉइंटर्स प्राप्त करने की आवश्यकता नहीं है। इसके बजाए, कोई भी आकार पूर्णांक सुविधाजनक का उपयोग कर संसाधनों की पहचान कर सकता है। इसके अतिरिक्त, यदि संसाधनों की संख्या को किसी int में बिट्स की संख्या से अधिक नहीं होना पड़ेगा, तो कुछ स्टेटस वेरिएबल एक बिट प्रति संसाधन का उपयोग करने में सहायक हो सकते हैं। उदाहरण के लिए, किसी के पास वेरिएबल timer_notifications (केवल इंटरप्ट हैंडलर के माध्यम से लिखे गए) और timer_acks (केवल मेनलाइन कोड के माध्यम से लिखे गए) हो सकते हैं और जब भी टाइमर एन सेवा चाहता है तो उस बिट एन (timer_notifications ^ timer_acks) को सेट किया जाएगा। इस तरह के दृष्टिकोण का उपयोग करने के लिए, कोड को प्रत्येक टाइमर के लिए एक चर पढ़ने के बजाय, किसी भी टाइमर को सेवा की आवश्यकता होती है, यह निर्धारित करने के लिए केवल दो चर पढ़ने की आवश्यकता होती है।


यह अजीब बात है कि आप इसे अक्सर देखते हैं। आप कुछ "आलसी" कोड के रूप में देख रहे होंगे।

सी में जिस तकनीक का आप वर्णन करते हैं वह आमतौर पर "अपारदर्शी" पुस्तकालय प्रकारों के लिए आरक्षित होता है, यानी संरचना प्रकार जिनकी परिभाषा जानबूझकर क्लाइंट के कोड के लिए अदृश्य हो जाती है। चूंकि ग्राहक ऐसी वस्तुओं की घोषणा नहीं कर सकता है, इसलिए मुहावरे को वास्तव में "छिपा" लाइब्रेरी कोड में गतिशील आवंटन पर होना है।

जब संरचना की परिभाषा को छिपाने की आवश्यकता नहीं होती है, तो आमतौर पर एक सामान्य सी मुहावरे निम्नानुसार दिखता है

typedef struct CClass
{
    int member_a;
    float member_b;
} CClass;

CClass* CClass_init(CClass* cclass);
void CClass_release(CClass* cclass);

फ़ंक्शन CClass_init *cclass ऑब्जेक्ट को प्रारंभ करता है और परिणामस्वरूप एक ही पॉइंटर देता है। यानी ऑब्जेक्ट के लिए स्मृति आवंटित करने का बोझ कॉलर पर रखा गया है और कॉलर इसे किसी भी तरह फिट कर सकता है

CClass cclass;
CClass_init(&cclass);
...
CClass_release(&cclass);

इस idiom का एक क्लासिक उदाहरण pthread_mutex_t साथ pthread_mutex_init और pthread_mutex_destroy

इस बीच, गैर-अपारदर्शी प्रकारों (जैसे कि आपके मूल कोड में) के लिए पूर्व तकनीक का उपयोग करना आम तौर पर एक संदिग्ध अभ्यास है। यह सी ++ में गतिशील स्मृति के मुफ्त उपयोग के रूप में बिल्कुल संदिग्ध है। यह काम करता है, लेकिन फिर, गतिशील स्मृति का उपयोग करते समय इसकी आवश्यकता नहीं होती है क्योंकि सी में सी ++ में है।


आम तौर पर, तथ्य यह है कि आप एक * देखते हैं इसका मतलब यह नहीं है कि यह malloc 'd है। उदाहरण के लिए, आपको static वैश्विक चर के लिए एक सूचक मिल सकता है; आपके मामले में, वास्तव में, CClass_destroy() कोई पैरामीटर नहीं लेता है जो मानता है कि यह पहले से ही वस्तु को नष्ट करने के बारे में कुछ जानकारी जानता है।

इसके अलावा, पॉइंटर्स, malloc डी, चाहे या नहीं, एकमात्र तरीका है जो आपको ऑब्जेक्ट को संशोधित करने की अनुमति देता है।

मुझे ढेर के बजाय ढेर का उपयोग करने के विशेष कारण नहीं दिखते हैं: आपको कम स्मृति का उपयोग नहीं मिलता है। हालांकि, इस तरह की "कक्षाओं" को शुरू करने के लिए जरूरी चीजों को निष्क्रिय / नष्ट कर दिया गया है क्योंकि अंतर्निहित डेटा संरचना में वास्तव में गतिशील डेटा शामिल होना आवश्यक है, इसलिए पॉइंटर्स का उपयोग।


यह वास्तव में सी ++ को "नया" बनाने में बहुत आसान है।

सिद्धांत रूप में, सी में इस वर्ग निर्माण पैटर्न का उपयोग सी ++ में "नया" का उपयोग करने के समान है, इसलिए कोई अंतर नहीं होना चाहिए। हालांकि, लोगों के बारे में सोचने के तरीके अलग-अलग होते हैं, इसलिए जिस तरह से लोग कोड पर प्रतिक्रिया करते हैं, वह अलग होता है।

सी में आपके लक्ष्यों को पूरा करने के लिए कंप्यूटर को सटीक परिचालनों के बारे में सोचना बहुत आम बात है। यह सार्वभौमिक नहीं है, लेकिन यह एक बहुत ही आम मानसिकता है। यह माना जाता है कि आपने मॉलोक / फ्री के लागत / लाभ विश्लेषण करने के लिए समय निकाला है।

सी ++ में, कोड की रेखाएं लिखना बहुत आसान हो गया है जो आपके लिए बहुत अच्छा काम करता है, बिना आपको इसे महसूस भी करता है। किसी के लिए कोड की एक पंक्ति लिखना आम बात है, और यह भी एहसास नहीं है कि यह 100 या 200 नए / हटाए जाने के लिए कॉल किया गया है! इसने एक बैकलैश का कारण बना दिया है, जहां सी ++ डेवलपर खबरों और हटावट पर कट्टरपंथी रूप से नाइटपिक करेगा, डर से कि उन्हें जगह पर गलती से बुलाया जा रहा है।

ये, ज़ाहिर है, सामान्यीकरण। किसी भी तरह से पूरे सी और सी ++ समुदाय इन molds फिट बैठते हैं। हालांकि, अगर आप ढेर पर चीजों को डालने के बजाए नए इस्तेमाल पर फ्लेक कर रहे हैं, तो यह मूल कारण हो सकता है।





heap