పాయింటర్లు & రిఫరెన్సెస్ (Pointers & References)
C++ ప్రోగ్రామింగ్ లాంగ్వేజ్కు ప్రపంచంలోనే అసమానమైన వేగం, సామర్థ్యం మరియు సిస్టమ్-లెవల్ పవర్ లభించడానికి ప్రధాన మూలస్తంభం: హార్డ్వేర్ మెమరీపై ప్రత్యక్ష నియంత్రణ (Direct Memory Control). ఇతర ఉన్నత స్థాయి ప్రోగ్రామింగ్ భాషలు (Java, Python, C# వంటివి) కంప్యూటర్ మెమరీని వర్చువల్ మెషీన్ మరియు గార్బేజ్ కలెక్టర్ వెనుక దాచిపెడితే, C++ ప్రోగ్రామర్కు కంప్యూటర్ ర్యామ్ (RAM) లోని ప్రతి ఒక్క బైట్ను నేరుగా యాక్సెస్ చేసే పూర్తి స్వేచ్ఛను ఇస్తుంది.
ఆ అద్భుతమైన స్వేచ్ఛను మరియు శక్తిని మనకు అందించే సాధనాలే పాయింటర్లు (Pointers) మరియు రిఫరెన్సెస్ (References). పాయింటర్లు మెమరీ చిరునామాలను (Hexadecimal Memory Addresses) నిల్వ చేస్తే, రిఫరెన్సెస్ మెమరీలో ఇప్పటికే ఉన్న డేటాకు సురక్షితమైన, వేగవంతమైన మారుపేర్లను (Aliases) అందిస్తాయి. సాఫ్ట్వేర్ ఇంజనీరింగ్లో గరిష్ట పనితీరు (Zero-Cost Abstractions) సాధించడానికి ఇవి అత్యంత కీలకమైన భావనలు.
ఈ సమగ్ర మాస్టర్క్లాస్లో మనం మెమరీ ఆర్కిటెక్చర్, 64-bit వర్చువల్ అడ్రస్ స్పేస్, Pointer Arithmetic, Modern C++ nullptr యొక్క టైప్ సేఫ్టీ, References vs Pointers అంతర్గత తేడాలు, const కరెక్ట్నెస్, మరియు సున్నా ఓవర్హెడ్తో పనిచేసే "Enterprise High-Performance Zero-Copy Binary Packet Parser" ప్రాజెక్ట్ను లైవ్గా నిర్మించి నేర్చుకుందాం!
1 కంప్యూటర్ మెమరీ ఆర్కిటెక్చర్ & హెక్సాడెసిమల్ అడ్రస్లు (RAM & Byte Addressing)
పాయింటర్లను లోతుగా అర్థం చేసుకోవడానికి ముందు, కంప్యూటర్ మెమరీ (Random Access Memory - RAM) ఎలా నిర్మించబడిందో తెలుసుకోవడం తప్పనిసరి. కంప్యూటర్ సైన్స్లో ర్యామ్ (RAM) అనేది ఒక భారీ వీధి వంటిది. ఆ వీధిలో కోట్లాది ఇళ్ళు వరుసగా ఉంటాయి. ప్రతి ఇంటికి ఒక ప్రత్యేకమైన నంబర్ (House Number) ఉన్నట్లే, RAM లో ప్రతి ఒక్క బైట్ (1 Byte = 8 Bits) కు ఒక ప్రత్యేకమైన మెమరీ చిరునామా (Memory Address) ఉంటుంది.
మనం ప్రోగ్రామ్లో ఒక వేరియబుల్ను సృష్టించినప్పుడు, కంపైలర్ మరియు ఆపరేటింగ్ సిస్టమ్ కలిసి ఆ వేరియబుల్ డేటా టైప్కు తగినన్ని బైట్లను RAM లో కేటాయిస్తాయి:
char: 1 బైట్ మెమరీని తీసుకుంటుంది.int: సాధారణంగా 4 బైట్ల మెమరీని వరుసగా ఆక్రమిస్తుంది.double: 8 బైట్ల మెమరీని తీసుకుంటుంది.
ఈ మెమరీ చిరునామాలను కంప్యూటర్ సాధారణంగా హెక్సాడెసిమల్ (Hexadecimal - Base 16) సంఖ్యల రూపంలో సూచిస్తుంది (ఉదాహరణకు: 0x7ffee4b29c14). '0x' అనే ఉపసర్గ (prefix) ఇది హెక్సాడెసిమల్ సంఖ్య అని తెలియజేస్తుంది.
| Memory Address (Hex) | Variable Name | Stored Value (Binary/Data) |
+-------------------------+------------------+-----------------------------+
| 0x7ffd9a10 | int score | [ Byte 0: 0x64 ] (Total 100)|
| 0x7ffd9a11 | (int continuation| [ Byte 1: 0x00 ] |
| 0x7ffd9a12 | 4 contiguous | [ Byte 2: 0x00 ] |
| 0x7ffd9a13 | bytes) | [ Byte 3: 0x00 ] |
| 0x7ffd9a14 | char grade | 'A' (ASCII 65 = 0x41) |
| 0x7ffd9a18 (Aligned) | double salary | 8 Bytes Floating Point Data |
+-------------------------+------------------+-----------------------------+
C++ లో ఏదైనా ఒక వేరియబుల్ యొక్క మెమరీ చిరునామాను తెలుసుకోవడానికి మనం Address-of Operator (&) ను ఉపయోగిస్తాం.
#include <iostream>
int main() {
int age = 25;
double price = 999.50;
char rank = 'A';
std::cout << "===== C++ Memory Addresses (RAM Examination) =====\n";
std::cout << "age విలువ: " << age << " | RAM Address (&age): " << &age << "\n";
std::cout << "price విలువ: " << price << " | RAM Address (&price): " << &price << "\n";
// char వేరియబుల్ అడ్రస్ ప్రింట్ చేయాలంటే void* కు కాస్ట్ చేయాలి
std::cout << "rank విలువ: " << rank << " | RAM Address (&rank): " << (void*)&rank << "\n";
std::cout << "\nage ఆక్రమించిన సైజ్: " << sizeof(age) << " Bytes\n";
std::cout << "price ఆక్రమించిన సైజ్: " << sizeof(price) << " Bytes\n";
return 0;
}
&rank ను నేరుగా std::cout కు ఇస్తే, కంపైలర్ దానిని C-style string గా భావించి నల్-టెర్మినేటర్ (\0) వచ్చే వరకు ప్రింట్ చేయడానికి ప్రయత్నిస్తుంది. అందుకే ఒక సింగిల్ క్యారెక్టర్ మెమరీ అడ్రస్ను చూడాలంటే (void*)&rank లేదా static_cast<const void*>(&rank) అని స్పష్టంగా చెప్పాలి!
2 పాయింటర్ డిక్లరేషన్ & డీరిఫరెన్సింగ్ (Pointer Declaration & Dereferencing *)
పాయింటర్ (Pointer) అంటే ఏమిటి? చాలా సరళంగా చెప్పాలంటే: వేరొక వేరియబుల్ యొక్క మెమరీ చిరునామాను (Memory Address) తనలో భద్రపరచుకునే ప్రత్యేకమైన వేరియబుల్ను పాయింటర్ అంటారు.
పాయింటర్ను డిక్లేర్ చేయడానికి మనం డేటా టైప్ తర్వాత ఒక నక్షత్ర గుర్తు (* - Asterisk) ను ఉంచుతాం:
int* ptr = &age; // age యొక్క మెమరీ అడ్రస్ను ptr అనే పాయింటర్ హోల్డ్ చేస్తుంది
ఇక్కడ రెండు ముఖ్యమైన ఆపరేటర్లు గుర్తుంచుకోవాలి:
&(Address-of Operator): ఒక వేరియబుల్ ఉన్న మెమరీ అడ్రస్ను అందిస్తుంది.*(Dereference Operator లేదా Indirection Operator): ఒక పాయింటర్ చూపిస్తున్న మెమరీ అడ్రస్లోకి వెళ్లి, అక్కడ ఉన్న అసలైన విలువను చదవడానికి (Read) లేదా మార్చడానికి (Write / Mutate) ఉపయోగపడుతుంది.
#include <iostream>
int main() {
int target = 42;
int* ptr = ⌖ // ptr లో target యొక్క మెమరీ చిరునామా భద్రపరచబడింది
std::cout << "target అసలు విలువ: " << target << "\n";
std::cout << "target మెమరీ అడ్రస్ (&target): " << &target << "\n";
std::cout << "ptr లో భద్రపరిచిన అడ్రస్: " << ptr << "\n";
std::cout << "ptr చూపిస్తున్న విలువ (*ptr - Dereferencing): " << *ptr << "\n";
std::cout << "ptr స్వంత మెమరీ అడ్రస్ (&ptr): " << &ptr << "\n";
std::cout << "\n--- డీరిఫరెన్సింగ్ ద్వారా విలువను మార్చడం ---\n";
*ptr = 999; // ptr ఉన్న అడ్రస్లోని విలువను 999 గా మారుస్తున్నాం!
std::cout << "*ptr = 999 అమలు చేసిన తర్వాత target విలువ: " << target << "\n";
return 0;
}
పై ఉదాహరణను గమనించండి: *ptr = 999; అని రాసినప్పుడు మనం నేరుగా target = 999; అని రాయలేదు! కానీ ptr ఆ అడ్రస్ను పాయింట్ చేస్తున్నందున, ఆ మెమరీ లొకేషన్లోని డేటా మారిపోయింది. ఇదే పాయింటర్ల యొక్క అసలైన మ్యాజిక్.
3 పాయింటర్ సైజ్ & 64-bit వర్సెస్ 32-bit ఆర్కిటెక్చర్ (Hardware Alignment)
సాఫ్ట్వేర్ ఇంటర్వ్యూలలో ఎక్కువగా అడిగే ఒక గమ్మత్తైన ప్రశ్న: "C++ లో char* పాయింటర్ పెద్దదా? లేక double* పాయింటర్ పెద్దదా? లేక 10,000 బైట్లు ఉన్న స్ట్రక్చర్ పాయింటర్ MyBigStruct* పెద్దదా?"
నిజం: డేటా టైప్తో సంబంధం లేకుండా కంప్యూటర్లోని అన్ని పాయింటర్ల సైజ్ ఖచ్చితంగా సమానం!
ఎందుకు? ఎందుకంటే పాయింటర్ అనేది డేటాను స్టోర్ చేసుకోదు; అది కేవలం మెమరీ అడ్రస్ (చిరునామా సంఖ్య) ను మాత్రమే భద్రపరుస్తుంది. ఒక నగరంలో పూరిగుడిసె చిరునామా రాయడానికైనా, 100 అంతస్తుల ఆకాశహర్మ్యం చిరునామా రాయడానికైనా ఒకే సైజు అడ్రస్ ప్లేట్ పడుతుంది కదా! అలాగే:
- 64-bit Systems (x86_64 / ARM64): ప్రతి పాయింటర్ ఖచ్చితంగా 8 బైట్లు (64 bits) ఉంటుంది. దీని ద్వారా కంప్యూటర్ $2^{64}$ బైట్ల (16 ఎక్సాబైట్ల) వర్చువల్ మెమరీని అడ్రస్ చేయగలదు.
- 32-bit Systems (x86): ప్రతి పాయింటర్ 4 బైట్లు (32 bits) ఉంటుంది. గరిష్టంగా 4 GB RAM మాత్రమే అడ్రస్ చేయగలదు.
#include <iostream>
struct HeavyPayload {
char data[10000];
double matrices[50][50];
};
int main() {
char c = 'Z';
int i = 100;
double d = 3.14159;
HeavyPayload hp;
char* pChar = &c;
int* pInt = &i;
double* pDouble = &d;
HeavyPayload* pHp = &hp;
std::cout << "===== CPU Architecture & Pointer Sizes =====\n";
std::cout << "sizeof(char*): " << sizeof(pChar) << " Bytes\n";
std::cout << "sizeof(int*): " << sizeof(pInt) << " Bytes\n";
std::cout << "sizeof(double*): " << sizeof(pDouble) << " Bytes\n";
std::cout << "sizeof(HeavyPayload*): " << sizeof(pHp) << " Bytes\n";
std::cout << "\nఅసలు డేటా సైజులు (Pointed Values):\n";
std::cout << "sizeof(char): " << sizeof(*pChar) << " Byte\n";
std::cout << "sizeof(HeavyPayload): " << sizeof(*pHp) << " Bytes\n";
return 0;
}
4 పాయింటర్ అరిథ్మెటిక్ (Pointer Arithmetic: +, -, ++, ptrdiff_t)
పాయింటర్లపై మనం సాధారణ సంఖ్యల వలె గణిత కార్యకలాపాలు (Arithmetic Operations) చేయవచ్చు, కానీ ఇవి సాధారణ గణితం కంటే చాలా భిన్నంగా పనిచేస్తాయి.
ఒక పాయింటర్కు ptr + 1 అని కూడినప్పుడు, దానికి 1 బైట్ కలవదు! బదులుగా ఆ పాయింటర్ సూచిస్తున్న డేటా టైప్ సైజు ఎంతైతే ఉందో (sizeof(T)), అన్ని బైట్లు ముందుకు కదులుతుంది!
ఉదాహరణకు int* p అనేది 0x1000 వద్ద ఉంటే, p + 1 అనేది 0x1004 అవుతుంది (ఎందుకంటే sizeof(int) = 4). అదే double* d వద్ద d + 1 చేస్తే 8 బైట్లు పెరిగి 0x1008 అవుతుంది.
| ఆపరేషన్ | అర్థం & అంతర్గత సమీకరణం | ఫలితం (Result) |
|---|---|---|
ptr + n |
ptr + (n * sizeof(*ptr)) |
మెమరీలో 'n' ఎలిమెంట్స్ ముందుకు వెళ్లే కొత్త అడ్రస్ |
ptr - n |
ptr - (n * sizeof(*ptr)) |
మెమరీలో 'n' ఎలిమెంట్స్ వెనక్కి వచ్చే కొత్త అడ్రస్ |
ptr++ లేదా ++ptr |
తర్వాతి ఎలిమెంట్ అడ్రస్కు పాయింటర్ను జంప్ చేస్తుంది | ఇటరేటర్ వలె అర్రేలలో వేగంగా ట్రావర్స్ చేయవచ్చు |
ptr2 - ptr1 |
రెండు పాయింటర్ల మధ్య దూరం: (ptr2 - ptr1) / sizeof(T) |
మధ్యలో ఉన్న ఎలిమెంట్ల సంఖ్య (Type: ptrdiff_t) |
#include <iostream>
#include <cstddef> // ptrdiff_t కొరకు
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int* p = numbers; // numbers[0] అడ్రస్ కు సమానం
std::cout << "===== Pointer Arithmetic Demonstration =====\n";
std::cout << "మొదటి ఎలిమెంట్: " << *p << " (అడ్రస్: " << p << ")\n";
p++; // తర్వాతి int కు జంప్ (4 బైట్లు ముందుకు)
std::cout << "p++ తర్వాత ఎలిమెంట్: " << *p << " (అడ్రస్: " << p << ")\n";
p += 2; // మరో 2 స్థానాలు ముందుకు
std::cout << "p += 2 తర్వాత ఎలిమెంట్: " << *p << " (అడ్రస్: " << p << ")\n";
int* start = &numbers[0];
int* end = &numbers[4];
ptrdiff_t distance = end - start;
std::cout << "\nరెండు పాయింటర్ల మధ్య దూరం (end - start): " << distance << " ఎలిమెంట్స్\n";
return 0;
}
5 Modern C++ nullptr వర్సెస్ సాంప్రదాయ NULL మరియు 0
ఒక పాయింటర్ ప్రస్తుతం ఏ సరైన మెమరీ లొకేషన్ను సూచించడం లేదని చెప్పడానికి పూర్వం C భాషలో NULL లేదా కేవలం 0 వాడేవారు. కానీ సాంప్రదాయ NULL అనేది ఆధునిక C++ లో అనేక భద్రతా లోపాలకు, బగ్స్కు కారణమైంది.
ఎందుకు? ఎందుకంటే C లో #define NULL 0 లేదా ((void*)0) గా డిఫైన్ చేయబడింది. అంటే NULL అనేది ప్రాథమికంగా ఒక పూర్ణాంకం (Integer: 0)!
ఆలోచించండి: మీ కోడ్లో ఈ క్రింది రెండు ఫంక్షన్లు ఉన్నాయనుకుందాం:
void display(int x);void display(int* ptr);ఇప్పుడు మీరు
display(NULL); అని పిలిస్తే ఏ ఫంక్షన్ రన్ అవుతుంది? కంపైలర్ NULL ను 0 గా భావించి display(int) ను పిలుస్తుంది! ఇది ప్రోగ్రామర్ ఉద్దేశానికి పూర్తి విరుద్ధం మరియు తీవ్రమైన రన్టైమ్ లోపాలకు దారితీస్తుంది.
ఈ లోపాన్ని శాశ్వతంగా పరిష్కరించడానికి Modern C++11 లో nullptr అనే సరికొత్త కీవర్డ్ ప్రవేశపెట్టబడింది.
nullptrఅనేది ఒక పూర్ణాంకం కాదు! దీని డేటా టైప్std::nullptr_t.- ఇది కేవలం పాయింటర్లతో మాత్రమే టైప్-సేఫ్గా సంభాషిస్తుంది. ఏ ఇంటిజర్తోనూ కన్వర్ట్ కాదు.
- అందువల్ల
display(nullptr);అని పిలిచినప్పుడు కంపైలర్ ఏ సంకోచం లేకుండా 100% ఖచ్చితత్వంతోdisplay(int*)నే పిలుస్తుంది!
#include <iostream>
void inspect(int value) {
std::cout << "-> inspect(int) పిలువబడింది: " << value << "\n";
}
void inspect(int* ptr) {
if (ptr == nullptr) {
std::cout << "-> inspect(int*) పిలువబడింది: [Safe NULLPTR Pointer]\n";
} else {
std::cout << "-> inspect(int*) పిలువబడింది: వాల్యూ = " << *ptr << "\n";
}
}
int main() {
std::cout << "===== Modern C++ nullptr Type Safety =====\n";
inspect(100); // inspect(int) పిలుస్తుంది
inspect(nullptr); // ఖచ్చితంగా inspect(int*) నే పిలుస్తుంది!
int* safePtr = nullptr;
if (safePtr != nullptr) {
std::cout << *safePtr << "\n";
} else {
std::cout << "safePtr సురక్షితంగా nullptr వద్ద ఉంది. క్రాష్ నివారించబడింది!\n";
}
return 0;
}
6 C++ రిఫరెన్సెస్ (References: T& — The Safe Alias)
పాయింటర్లు చాలా శక్తివంతమైనవే అయినప్పటికీ, వాటితో పనిచేసేటప్పుడు సింటాక్స్ కాస్త సంక్లిష్టంగా (*, &, ->) ఉంటుంది మరియు తప్పుడు అడ్రస్లను పాయింట్ చేసే ప్రమాదం ఉంటుంది. దీని కోసమే C++ మనకు రిఫరెన్సెస్ (References) అనే అద్భుతమైన ఫీచర్ను అందించింది.
రిఫరెన్స్ అంటే ఏమిటి? మెమరీలో ఇప్పటికే ఉన్న ఒక వేరియబుల్కు మనం ఇచ్చే మరొక అధికారిక మారుపేరు (Alias / Nickname) నే రిఫరెన్స్ అంటారు!
రిఫరెన్స్ డిక్లేర్ చేయడానికి వేరియబుల్ టైప్ పక్కన & గుర్తును వాడతాం:
int original = 50;
int& ref = original; // ref అనేది original కు మారుపేరు
రిఫరెన్స్ యొక్క 3 అచంచలమైన సూత్రాలు (Three Unbreakable Rules of References):
- తప్పనిసరి ఇనిషియలైజేషన్ (Must Be Initialized): రిఫరెన్స్ను సృష్టించేప్పుడే అది ఏ వేరియబుల్కు మారుపేరో చెప్పాలి. ఖాళీగా
int& ref;అని వదిలేస్తే కంపైలేషన్ ఎర్రర్ వస్తుంది. - ఎప్పటికీ Null కాలేదు (Cannot Be Null): రిఫరెన్స్ ఎల్లప్పుడూ ఒక నిజమైన వాల్యూకే కట్టుబడి ఉంటుంది.
nullptrరిఫరెన్స్ అనేది ఉండదు. - రీ-బైండింగ్ సాధ్యం కాదు (Cannot Be Reseated): ఒకసారి ఒక వేరియబుల్కు కనెక్ట్ అయిన తర్వాత, దానిని వేరొక వేరియబుల్కు మార్చలేం.
ref = other;అని రాస్తేotherయొక్క విలువoriginalలోకి కాపీ అవుతుంది తప్ప రిఫరెన్స్ మారదు!
#include <iostream>
int main() {
int bankBalance = 50000;
int& alias = bankBalance; // alias అనేది bankBalance యొక్క ప్రత్యక్ష మారుపేరు
std::cout << "bankBalance: " << bankBalance << " (అడ్రస్: " << &bankBalance << ")\n";
std::cout << "alias: " << alias << " (అడ్రస్: " << &alias << ")\n";
std::cout << "\n--- alias ద్వారా విలువను సవరించడం ---\n";
alias += 15000; // alias లో మార్పు చేస్తే నేరుగా bankBalance మారుతుంది!
std::cout << "కొత్త bankBalance: " << bankBalance << "\n";
std::cout << "గమనించండి: &bankBalance మరియు &alias ఖచ్చితంగా ఒకే అడ్రస్!\n";
return 0;
}
T* const (Constant Pointer) గానే ఇంప్లిమెంట్ చేస్తుంది! కానీ ప్రోగ్రామర్కు * రాయాల్సిన అవసరం లేకుండా, ఒక సాధారణ వేరియబుల్ వలె క్లీన్ సింటాక్స్తో ఉపయోగపడేలా C++ ఈ అద్భుతమైన సింటాక్టిక్ షుగర్ను అందించింది.
7 పాయింటర్లు వర్సెస్ రిఫరెన్సెస్ సమగ్ర పోలిక పట్టిక (Pointers vs References Matrix)
C++ ప్రోగ్రామర్లు నిత్యం ఎదుర్కొనే సందేహం: ఫంక్షన్లలో మరియు క్లాసుల్లో ఎప్పుడు పాయింటర్ వాడాలి? ఎప్పుడు రిఫరెన్స్ వాడాలి? క్రింది సమగ్ర పోలిక పట్టిక ద్వారా వీటి మధ్య ఉన్న 8 కీలక వ్యత్యాసాలను గ్రహించండి:
| ఫీచర్ / అంశం | పాయింటర్లు (Pointers: T*) |
రిఫరెన్సులు (References: T&) |
|---|---|---|
| నిర్వచనం (Definition) | మెమరీ అడ్రస్ను దాచుకునే ఒక స్వతంత్ర వేరియబుల్ | ఇప్పటికే ఉన్న ఒక ఆబ్జెక్టుకు ఇచ్చే మారుపేరు (Alias) |
| ఇనిషియలైజేషన్ (Initialization) | తప్పనిసరి కాదు (డిక్లేర్ చేసి తర్వాత అడ్రస్ ఇవ్వవచ్చు) | సృష్టించే సమయంలోనే తప్పనిసరిగా ఇనిషియలైజ్ చేయాలి |
| Nullability | nullptr ను సురక్షితంగా హోల్డ్ చేయగలదు |
ఎప్పటికీ null కాలేదు (ఎల్లప్పుడూ నిజమైన ఆబ్జెక్ట్ ఉండాలి) |
| రీ-అసైన్మెంట్ (Rebinding) | ఎప్పుడైనా వేరొక అడ్రస్ను సూచించేలా మార్చవచ్చు | ఒకసారి బైండ్ అయ్యాక వేరొక ఆబ్జెక్ట్కు మార్చలేం |
| సింటాక్స్ (Access Syntax) | విలువను పొందడానికి *ptr లేదా ptr->member వాడాలి |
సాధారణ వేరియబుల్ వలె ref లేదా ref.member వాడవచ్చు |
| గణితం (Arithmetic) | Pointer Arithmetic (++, --, +, -) సాధ్యం |
అంకగణితం ఉండదు (ref++ అంటే ఆబ్జెక్ట్ వాల్యూ పెరుగుతుంది) |
| స్వంత మెమరీ అడ్రస్ | పాయింటర్కు స్టాక్లో స్వంత ప్రత్యేక అడ్రస్ (8 బైట్లు) ఉంటుంది | రిఫరెన్స్కు ప్రత్యేక అడ్రస్ ఉండదు (అసలు ఆబ్జెక్ట్ అడ్రసే) |
| గోల్డెన్ యూజ్ కేస్ | ఆప్షనల్ వాల్యూస్ (Nullable), డైనమిక్ మెమరీ, డేటా స్ట్రక్చర్స్ | ఫంక్షన్ పారామీటర్లు (Zero-Copy Pass-by-Reference), ఆపరేటర్ ఓవర్లోడింగ్ |
8 const కరెక్ట్నెస్: పాయింటర్లు & రిఫరెన్సులతో const మ్యాట్రిక్స్
C++ లో const కీవర్డ్ మరియు పాయింటర్ల కలయిక చాలా మంది డెవలపర్లను తికమక పెడుతుంది. const int* అంటే ఏమిటి? int* const అంటే ఏమిటి? ఈ గందరగోళానికి ఒకే ఒక్క సింపుల్ ట్రిక్ ఉంది: కుడి నుండి ఎడమకు చదవండి (Read from Right to Left)!
1. const int* ptr (లేదా int const* ptr):
ptr is a pointer to a CONST INT.
-> మెమరీలోని విలువను మార్చలేరు (*ptr = 50 ERROR), కానీ పాయింటర్ వేరే అడ్రస్కు మారవచ్చు (ptr = &other OK).
2. int* const ptr:
ptr is a CONST POINTER to an int.
-> పాయింటర్ అడ్రస్ మారదు (ptr = &other ERROR), కానీ లోపలి విలువను మార్చవచ్చు (*ptr = 50 OK).
3. const int* const ptr:
ptr is a CONST POINTER to a CONST INT.
-> అడ్రస్ మారదు, లోపలి విలువనూ మార్చలేరు (పూర్తి రీడ్-ఓన్లీ లాక్).
4. const T& ref (Const Reference):
ref is a reference to a CONST object.
-> అత్యంత ప్రాచుర్యం పొందిన C++ పద్ధతి. సున్నా కాపీతో రీడ్-ఓన్లీ యాక్సెస్ ఇస్తుంది!
#include <iostream>
int main() {
int a = 10;
int b = 20;
// 1. Pointer to CONST data
const int* p1 = &a;
// *p1 = 15; // కంపైలర్ ఎర్రర్! డేటా రీడ్-ఓన్లీ
p1 = &b; // సవ్యమైనది! వేరే అడ్రస్ను సూచించవచ్చు
// 2. CONST Pointer to mutable data
int* const p2 = &a;
*p2 = 99; // సవ్యమైనది! విలువను సవరించవచ్చు
// p2 = &b; // కంపైలర్ ఎర్రర్! పాయింటర్ అడ్రస్ మార్చలేము
// 3. CONST Pointer to CONST data
const int* const p3 = &a;
// *p3 = 100; // కంపైలర్ ఎర్రర్!
// p3 = &b; // కంపైలర్ ఎర్రర్!
std::cout << "a యొక్క తాజా విలువ: " << a << "\n";
std::cout << "*p1 (b చూపిస్తోంది): " << *p1 << "\n";
return 0;
}
9 Pass-by-Value vs Pass-by-Pointer vs Pass-by-Reference (హై-పెర్ఫార్మెన్స్ ఫంక్షన్ కాల్స్)
C++ లో ఒక ఫంక్షన్కు డేటాను పంపేటప్పుడు (Function Arguments Passing) ఎంచుకునే విధానం అప్లికేషన్ యొక్క మెమరీ మరియు వేగాన్ని శాసిస్తుంది. ప్రధానంగా మూడు విధానాలు ఉన్నాయి:
-
Pass-by-Value: వేరియబుల్ యొక్క పూర్తి కాపీని స్టాక్ మెమరీలో డూప్లికేట్ చేస్తుంది.
int,doubleవంటి చిన్న ప్రాథమిక రకాలకు ఇది వేగవంతమైనదే, కానీ 10 లక్షల రికార్డులున్నstd::vectorను పాస్ చేస్తే విపరీతమైన మెమరీ కాపీయింగ్ జరిగి సిస్టమ్ స్లో అయిపోతుంది. -
Pass-by-Pointer: కేవలం 8 బైట్ల మెమరీ అడ్రస్ను మాత్రమే పాస్ చేస్తుంది. అసలైన వేరియబుల్ను ఫంక్షన్ లోపల సవరించవచ్చు. కానీ ఫంక్షన్ లోపల పాయింటర్
nullptrకాదా అని ఎల్లప్పుడూ చెక్ చేసుకోవాలి. -
Pass-by-Reference (
T&లేదాconst T&): Modern C++ ప్రపంచంలో బంగారు ప్రమాణం (Gold Standard). ఎటువంటి మెమరీ కాపీ జరగదు (Zero-Copy), మరియుnullptrవచ్చే ప్రమాదం అస్సలు ఉండదు! ఒకవేళ ఫంక్షన్ కేవలం డేటాను చదవడానికి మాత్రమే అయితేconst T&వాడటం సర్వశ్రేష్టం.
#include <iostream>
#include <string>
// 1. Pass by Value (పూర్తి కాపీ అవుతుంది - స్లో)
void modifyByValue(int x) {
x += 100;
}
// 2. Pass by Pointer (అడ్రస్ పాస్ అవుతుంది - అసలు విలువ మారుతుంది)
void modifyByPointer(int* ptr) {
if (ptr != nullptr) {
*ptr += 100;
}
}
// 3. Pass by Reference (మారుపేరు - క్లీన్ & ఫాస్ట్)
void modifyByReference(int& ref) {
ref += 100;
}
// 4. Pass by Const Reference (భారీ ఆబ్జెక్టులకు జీరో-కాపీ రీడ్-ఓన్లీ)
void printBigData(const std::string& hugeText) {
std::cout << "టెక్స్ట్ సైజు: " << hugeText.length() << " అక్షరాలు (Zero-Copy!)\n";
}
int main() {
int val = 10;
modifyByValue(val);
std::cout << "modifyByValue తర్వాత: " << val << " (మార్పు లేదు - 10)\n";
modifyByPointer(&val);
std::cout << "modifyByPointer తర్వాత: " << val << " (మారింది - 110)\n";
modifyByReference(val);
std::cout << "modifyByReference తర్వాత: " << val << " (మారింది - 210)\n";
std::string report = "Our Compiler Modern C++ Masterclass 2026";
printBigData(report);
return 0;
}
10 వాయిడ్ పాయింటర్లు (void*) & మోడరన్ టైప్ కాస్టింగ్ (Generic Memory Handling)
ఒక పాయింటర్కు నిర్దిష్టమైన డేటా టైప్ లేకుండా, కంప్యూటర్ ర్యామ్ లోని ఏ డేటా టైప్ అడ్రస్నైనా భద్రపరచడానికి ఉపయోగపడే జెనరిక్ పాయింటర్నే void* (Void Pointer లేదా Generic Pointer) అంటారు.
C భాషలోని malloc, memcpy వంటి లో-లెవల్ మెమరీ ఫంక్షన్లు మెమరీని కేటాయించడానికి void* నే వాడతాయి.
void* ను మనం నేరుగా డీరిఫరెన్స్ చేయలేము (*voidPtr చెల్లదు)! ఎందుకంటే కంపైలర్కు ఆ మెమరీ లొకేషన్లో ఎన్ని బైట్లు చదవాలో (1 బైటా, 4 బైట్లా, లేక 8 బైట్లా) తెలియదు. దానిని చదవాలంటే ముందుగా సరైన టైప్లోకి కాస్ట్ (Cast) చేయాలి.
పాత C-Style కాస్టింగ్ (int*)vptr సురక్షితం కాదు. Modern C++ లో మనం static_cast లేదా లో-లెవల్ హార్డ్వేర్ కోసం reinterpret_cast ను వాడతాం.
#include <iostream>
enum class DataType { INT, DOUBLE, CHAR };
void printGeneric(void* ptr, DataType type) {
if (ptr == nullptr) return;
switch (type) {
case DataType::INT: {
int* intPtr = static_cast<int*>(ptr);
std::cout << "Generic Int వాల్యూ: " << *intPtr << "\n";
break;
}
case DataType::DOUBLE: {
double* dblPtr = static_cast<double*>(ptr);
std::cout << "Generic Double వాల్యూ: " << *dblPtr << "\n";
break;
}
case DataType::CHAR: {
char* chrPtr = static_cast<char*>(ptr);
std::cout << "Generic Char వాల్యూ: " << *chrPtr << "\n";
break;
}
}
}
int main() {
int num = 404;
double pi = 3.14159;
char code = 'K';
printGeneric(&num, DataType::INT);
printGeneric(&pi, DataType::DOUBLE);
printGeneric(&code, DataType::CHAR);
return 0;
}
11 పాయింటర్లతో వచ్చే ప్రమాదాలు & బగ్స్ నివారణ (Pitfalls, Dangling Pointers & Best Practices)
"గొప్ప శక్తీతో పాటు గొప్ప బాధ్యత కూడా వస్తుంది" (With great power comes great responsibility). పాయింటర్లు కంప్యూటర్ మెమరీపై అపరిమిత అధికారాన్ని ఇస్తాయి, కానీ అజాగ్రత్తగా వాడితే సాఫ్ట్వేర్ క్రాష్ అవ్వడం లేదా సెక్యూరిటీ హ్యాకింగ్కు గురయ్యే ప్రమాదం ఉంది. క్రింది 4 ప్రధాన సమస్యలను మరియు వాటి నివారణను తప్పక తెలుసుకోవాలి:
-
Wild Pointers (వైల్డ్ పాయింటర్లు): పాయింటర్ను డిక్లేర్ చేసి ఏ అడ్రస్ ఇవ్వకుండా వదిలేస్తే (
int* p;), అది రాండమ్ గార్బేజ్ మెమరీ అడ్రస్ను సూచిస్తుంది. దానిని డీరిఫరెన్స్ చేస్తే అప్లికేషన్ వెంటనే క్రాష్ (Segmentation Fault) అవుతుంది.
పరిష్కారం: ఎల్లప్పుడూ డిక్లేర్ చేసేటప్పుడేnullptrతో ఇనిషియలైజ్ చేయండి:int* p = nullptr; -
Dangling Pointers (డాంగ్లింగ్ పాయింటర్లు): ఒక వేరియబుల్ నాశనమైన తర్వాత (ఉదాహరణకు ఒక ఫంక్షన్ ముగిసి లోకల్ స్టాక్ వేరియబుల్ నశించినప్పుడు), ఆ పాత అడ్రస్ను పాయింటర్ ఇంకా చూపిస్తూ ఉంటే దానిని డాంగ్లింగ్ పాయింటర్ అంటారు.
పరిష్కారం: ఫంక్షన్ లోకల్ వేరియబుల్స్ యొక్క అడ్రస్లను ఎప్పుడూ రిటర్న్ చేయవద్దు! -
Null Pointer Dereference:
nullptrవద్ద ఉన్న పాయింటర్ను డీరిఫరెన్స్ చేయడానికి ప్రయత్నించడం.
పరిష్కారం: పాయింటర్ను వాడే ముందుif (ptr != nullptr)అని చెక్ చేసుకోవడం. - Memory Leaks: డైనమిక్ మెమరీని అలొకేట్ చేసి దాన్ని సరిగ్గా ఫ్రీ చేయకపోవడం (దీని గురించి మనం తర్వాతి చాప్టర్ 12 లో Smart Pointers ద్వారా లోతుగా నేర్చుకుంటాం).
#include <iostream>
// ప్రమాదకరమైన కోడ్ - ఫంక్షన్ లోకల్ స్టాక్ అడ్రస్ రిటర్న్ చేయడం తప్పు!
int* dangerousFunction() {
int localVal = 500;
return &localVal; // ⚠️ WARNING: localVal ఫంక్షన్ ముగియగానే నశిస్తుంది!
}
int main() {
int* badPtr = dangerousFunction();
// *badPtr ని యాక్సెస్ చేయడం Undefined Behavior! మెమరీ కరప్ట్ కావచ్చు.
std::cout << "డాంగ్లింగ్ పాయింటర్ను నివారించడానికి Modern C++ లో స్మార్ట్ పాయింటర్లు లేదా వాల్యూ రిటర్న్స్ వాడాలి!\n";
return 0;
}
12 రియల్-వరల్డ్ ప్రాజెక్ట్: Zero-Copy Binary Network Packet Parser Engine
నిజ జీవితంలో గేమ్ ఇంజిన్లు, నెట్వర్క్ రౌటర్లు (Cisco Switches), ఆటోమోటివ్ CAN-Bus సిస్టమ్స్ మరియు హై-ఫ్రీక్వెన్సీ స్టాక్ ఎక్స్ఛేంజ్ ఇంజిన్లు సెకనుకు లక్షలాది బైనరీ డేటా ప్యాకెట్లను ప్రాసెస్ చేస్తాయి. అక్కడ స్ట్రింగ్ కాపీలు చేయడం లేదా భారీ ఆబ్జెక్టులను సృష్టించడం వల్ల తీవ్రమైన లేటెన్సీ (Latency) ఏర్పడుతుంది.
అటువంటి సమయాల్లో ఇంజనీర్లు పాయింటర్ అరిథ్మెటిక్, మెమరీ కాస్టింగ్ మరియు కాన్స్ట్ రిఫరెన్సులను ఉపయోగించి సున్నా కాపీ (Zero-Copy) పద్ధతిలో రా-బైట్ల నుండి నేరుగా డేటాను వెలికితీస్తారు.
ఈ ప్రాజెక్ట్లో మనం ఒక హై-పెర్ఫార్మెన్స్ బైనరీ టెలిమెట్రీ ప్యాకెట్ పార్సర్ ను నిర్మిస్తున్నాం. ఇది 1024-బైట్ల రా-బఫర్ నుండి హెక్సాడెసిమల్ మ్యాజిక్ నంబర్, ప్యాకెట్ సీక్వెన్స్, సెన్సార్ ఐడీ, ఉష్ణోగ్రత మరియు చెక్సమ్ను మెకానికల్ సింపతీతో పార్స్ చేస్తుంది:
#include <iostream>
#include <cstdint>
#include <cstring>
#include <iomanip>
// హార్డ్వేర్ నెట్వర్క్ ప్యాకెట్ హెడర్ లేఅవుట్ (Packed Struct)
#pragma pack(push, 1)
struct TelemetryPacketHeader {
uint16_t magic; // 0xABCD (2 Bytes)
uint32_t packetSeq; // సీక్వెన్స్ నంబర్ (4 Bytes)
uint16_t sensorId; // సెన్సార్ ఐడీ (2 Bytes)
float temperature; // సెన్సార్ ఉష్ణోగ్రత (4 Bytes)
uint32_t payloadLength; // పేలోడ్ పొడవు (4 Bytes)
};
#pragma pack(pop)
// పాయింటర్లు & రిఫరెన్సులతో జీరో-కాపీ ప్యాకెట్ అనలైజర్ క్లాస్
class BinaryStreamParser {
public:
static void parseStream(const uint8_t* rawBuffer, size_t totalBytes) {
std::cout << "====================================================================\n";
std::cout << "📡 [HIGH-FREQUENCY ZERO-COPY BINARY PACKET PARSER ENGINE]\n";
std::cout << "మొత్తం స్వీకరించిన బైట్లు: " << totalBytes << " Bytes\n";
std::cout << "====================================================================\n";
const uint8_t* currentPtr = rawBuffer;
const uint8_t* endPtr = rawBuffer + totalBytes;
size_t packetCount = 0;
while (currentPtr + sizeof(TelemetryPacketHeader) <= endPtr) {
// జీరో కాపీ: మెమరీ బఫర్ను నేరుగా Struct పాయింటర్గా రీఇంటర్ప్రెట్ చేస్తున్నాం!
const TelemetryPacketHeader* header = reinterpret_cast<const TelemetryPacketHeader*>(currentPtr);
// Magic Byte చెక్ (0xABCD)
if (header->magic != 0xABCD) {
std::cout << "❌ చెల్లని మ్యాజిక్ బైట్: " << std::hex << header->magic << ". స్ట్రీమ్ సింక్ లోపం!\n";
break;
}
packetCount++;
std::cout << "\n[ప్యాకెట్ #" << packetCount << " పరిశీలన]:\n";
std::cout << " - మ్యాజిక్ ఐడెంటిఫైయర్ : 0x" << std::hex << header->magic << std::dec << "\n";
std::cout << " - ప్యాకెట్ సీక్వెన్స్ : " << header->packetSeq << "\n";
std::cout << " - సెన్సార్ ఐడీ : 0x" << std::hex << header->sensorId << std::dec << "\n";
std::cout << " - కొలిచిన ఉష్ణోగ్రత : " << std::fixed << std::setprecision(2) << header->temperature << " °C\n";
std::cout << " - పేలోడ్ పరిమాణం : " << header->payloadLength << " Bytes\n";
// పాయింటర్ అరిథ్మెటిక్ ద్వారా పేలోడ్ లొకేషన్కు కదలడం
const uint8_t* payloadPtr = currentPtr + sizeof(TelemetryPacketHeader);
std::cout << " - పేలోడ్ టెక్స్ట్ డేటా: \"";
for (uint32_t i = 0; i < header->payloadLength; ++i) {
std::cout << static_cast<char>(*(payloadPtr + i));
}
std::cout << "\"\n";
// తర్వాతి ప్యాకెట్కు పాయింటర్ను జంప్ చేయడం (Header Size + Payload Length)
currentPtr += sizeof(TelemetryPacketHeader) + header->payloadLength;
}
std::cout << "\n====================================================================\n";
std::cout << "✅ మొత్తం " << packetCount << " టెలిమెట్రీ ప్యాకెట్లు సున్నా మెమరీ కాపీలతో విజయవంతంగా పార్స్ చేయబడ్డాయి!\n";
std::cout << "====================================================================\n";
}
};
int main() {
// నెట్వర్క్ సాకెట్ నుండి వచ్చినట్లుగా ఒక సిమ్యులేటెడ్ బైనరీ స్ట్రీమ్ బఫర్
uint8_t networkBuffer[512] = {0};
uint8_t* writePtr = networkBuffer;
// ప్యాకెట్ 1 రాత (ISRO Rocket Telemetry 1)
TelemetryPacketHeader p1;
p1.magic = 0xABCD;
p1.packetSeq = 1001;
p1.sensorId = 0x55AA;
p1.temperature = 48.75f;
const char* msg1 = "STAGE-1 BOOSTER IGNITION NORMAL";
p1.payloadLength = static_cast<uint32_t>(std::strlen(msg1));
std::memcpy(writePtr, &p1, sizeof(TelemetryPacketHeader));
writePtr += sizeof(TelemetryPacketHeader);
std::memcpy(writePtr, msg1, p1.payloadLength);
writePtr += p1.payloadLength;
// ప్యాకెట్ 2 రాత (ISRO Rocket Telemetry 2)
TelemetryPacketHeader p2;
p2.magic = 0xABCD;
p2.packetSeq = 1002;
p2.sensorId = 0x55AB;
p2.temperature = 74.20f;
const char* msg2 = "MAX-Q VELOCITY 1250 M/S CRITICAL";
p2.payloadLength = static_cast<uint32_t>(std::strlen(msg2));
std::memcpy(writePtr, &p2, sizeof(TelemetryPacketHeader));
writePtr += sizeof(TelemetryPacketHeader);
std::memcpy(writePtr, msg2, p2.payloadLength);
writePtr += p2.payloadLength;
size_t totalBytesWritten = writePtr - networkBuffer;
// పార్సర్ కాల్ (Const Pointer గా పాస్ చేస్తున్నాం)
BinaryStreamParser::parseStream(networkBuffer, totalBytesWritten);
return 0;
}
13 సారాంశం & ఇంటరాక్టివ్ క్విజ్ (Summary & Self-Check Quizzes)
📌 ఈ అధ్యాయంలో నేర్చుకున్న ప్రధాన అంశాలు:
- మెమరీ ఆర్కిటెక్చర్: RAM లోని ప్రతి బైట్కు ఒక ప్రత్యేక హెక్సాడెసిమల్ అడ్రస్ ఉంటుంది.
&ఆపరేటర్ వేరియబుల్ అడ్రస్ను ఇస్తుంది. - పాయింటర్లు: వేరొక వేరియబుల్ మెమరీ అడ్రస్ను భద్రపరిచే వేరియబుల్.
*ptr(Dereferencing) ద్వారా ఆ అడ్రస్లోని డేటాను చదవవచ్చు, మార్చవచ్చు. - పాయింటర్ సైజులు: 64-bit సిస్టమ్లో అన్ని పాయింటర్లు (char*, int*, struct*) ఖచ్చితంగా 8 బైట్లే ఉంటాయి.
- Pointer Arithmetic:
ptr + 1అంటే 1 బైట్ కాదు,sizeof(T)బైట్లు ముందుకు కదులుతుంది. రెండు పాయింటర్ల మధ్య దూరంptrdiff_t. - Modern nullptr: పాత C-style
NULL(0) వల్ల వచ్చే టైప్ అంబిగ్యుటీని పరిష్కరించే టైప్-సేఫ్ పాయింటర్ లిటరల్nullptr. - రిఫరెన్సులు (T&): ఇప్పటికే ఉన్న ఆబ్జెక్టుకు సురక్షితమైన మారుపేరు. ఇది ఎప్పటికీ null కాదు మరియు రీ-బైండ్ చేయలేం.
- Const Correctness: కుడి నుండి ఎడమకు చదివి
const int*(Pointer to const) మరియుint* const(Const pointer) మధ్య తేడాను గుర్తించాలి. - జీరో-కాపీ పెర్ఫార్మెన్స్: భారీ ఆబ్జెక్టులను ఫంక్షన్లకు పాస్ చేయడానికి
const T&ప్రపంచవ్యాప్త బెస్ట్ ప్రాక్టీస్.
🧠 మీ అవగాహనను పరీక్షించుకోండి (5 Interactive Quizzes):
ప్రశ్న 1: 64-bit ఆర్కిటెక్చర్లో char* మరియు double* పాయింటర్ల సైజులు ఎంత ఉంటాయి?
వివరణ: పాయింటర్ అనేది అది సూచించే డేటా సైజును బట్టి మారదు. పాయింటర్ కేవలం కంప్యూటర్ మెమరీ అడ్రస్ను మాత్రమే స్టోర్ చేస్తుంది. 64-bit CPU ఆర్కిటెక్చర్లో ప్రతి మెమరీ అడ్రస్ 64 బిట్స్ (8 బైట్లు) పొడవు ఉంటుంది.
ప్రశ్న 2: int* p = (int*)0x1000; ఉన్నప్పుడు, p + 3 యొక్క కొత్త మెమరీ అడ్రస్ విలువ ఎంత అవుతుంది?
వివరణ: Pointer Arithmetic సమీకరణం ప్రకారం:
New_Address = Current_Address + (3 * sizeof(int)). sizeof(int) = 4 బైట్లు కాబట్టి, $3 \times 4 = 12$ బైట్లు. డెసిమల్ 12 హెక్సాడెసిమల్ లో 'C' అవుతుంది. కాబట్టి 0x1000 + 0x000C = 0x100C.
ప్రశ్న 3: Modern C++ లో సాంప్రదాయ NULL లేదా 0 కు బదులుగా nullptr ను ఎందుకు వాడాలి?
వివరణ:
NULL అనేది కేవలం 0 అనే పూర్ణాంకం (Integer). అందువల్ల foo(int) మరియు foo(int*) రెండూ ఉన్నప్పుడు foo(NULL) పిలిస్తే foo(int) కాల్ అయ్యే తీవ్రమైన లోపం ఉండేది. nullptr ప్రత్యేక టైప్ std::nullptr_t కలిగి ఉండి కేవలం పాయింటర్లతో మాత్రమే టైప్-సేఫ్గా సరిపోలుతుంది.
ప్రశ్న 4: const int* const ptr = &x; అంటే ఏమిటి? ఇందులో ఏది మార్చవచ్చు, ఏది మార్చలేము?
వివరణ: కుడి నుండి ఎడమకు చదివితే:
ptr అనేది ఒక కాన్స్టంట్ పాయింటర్ (అందువల్ల ptr = &y; చేయడం నిషిద్ధం), అది ఒక కాన్స్టంట్ పూర్ణాంకాన్ని సూచిస్తుంది (అందువల్ల *ptr = 50; చేయడం నిషిద్ధం). ఇది పూర్తి రీడ్-ఓన్లీ లాక్.
ప్రశ్న 5: C++ లో ఒక ఫంక్షన్ నుండి లోకల్ స్టాక్ వేరియబుల్ యొక్క అడ్రస్ను (Pointer) రిటర్న్ చేస్తే ఏమవుతుంది?
వివరణ: ఫంక్షన్ లోపల సృష్టించబడిన లోకల్ వేరియబుల్స్ స్టాక్ ఫ్రేమ్లో ఉంటాయి. ఫంక్షన్ ముగియగానే (Return కాగానే) ఆ స్టాక్ ఫ్రేమ్ పూర్తిగా డిస్ట్రాయ్ అయిపోతుంది. నశించిపోయిన ఆ అడ్రస్ను కాలర్ యాక్సెస్ చేయడానికి ప్రయత్నిస్తే మెమరీ కరప్షన్ లేదా ప్రోగ్రామ్ క్రాష్ అవుతుంది.
ఈ ప్రాజెక్ట్ను లైవ్గా మన కంపైలర్లో రన్ చేయండి!
ఎటువంటి లోకల్ సెటప్ లేకుండా, బ్రౌజర్లోనే నేరుగా Modern C++ (GCC 13 / C++20) కంపైలర్లో ఈ కోడ్ను ఎగ్జిక్యూట్ చేసి రన్టైమ్ అవుట్పుట్ను తనిఖీ చేయండి.