C డైనమిక్ మెమరీ కేటాయింపు (malloc, calloc, realloc, free) & Memory Leaks
హలో C ప్రోగ్రామర్! 👋 రన్టైమ్లో మెమరీని మీ చేతుల్లోకి తీసుకునే సమయం వచ్చింది!
ఇప్పటివరకు మనం రాసిన అన్ని ప్రోగ్రామ్లలో వేరియబుల్స్ మరియు అర్రేల సైజును కోడ్ రాసేప్పుడే (Compile-Time) నిర్ణయించాం.
ఉదాహరణకు int arr[100]; అని డిక్లేర్ చేస్తే, కంపైలర్ ఖచ్చితంగా 100 పూర్ణాంకాల మెమరీని స్టాక్ (Stack) లో రిజర్వ్ చేస్తుంది.
కానీ వాస్తవ ప్రపంచంలో యూజర్ 5 నంబర్లు మాత్రమే ఎంటర్ చేయవచ్చు—అప్పుడు మిగిలిన 95 స్లాట్లు వ్యర్థం అవుతాయి! ఒకవేళ యూజర్ 101 నంబర్లు ఎంటర్ చేయాలనుకుంటే?
ప్రోగ్రామ్ బఫర్ ఓవర్ఫ్లో అయి క్రాష్ అవుతుంది!
ఈ తీవ్రమైన సమస్యకు C భాష అందించిన అత్యంత శక్తివంతమైన పరిష్కారమే డైనమిక్ మెమరీ కేటాయింపు (Dynamic Memory Allocation - DMA).
ప్రోగ్రామ్ రన్ అవుతున్న సమయంలోనే (Run-time), ఎంత మెమరీ అవసరమో ఆపరేటింగ్ సిస్టమ్ హీప్ (Heap) పూల్ నుంచి అడిగి తీసుకోవడం, పని పూర్తయ్యాక తిరిగి అప్పగించడం
C లో డెవలపర్కు ఉన్న విశిష్ట హక్కు. ఈ 18వ అధ్యాయంలో malloc(), calloc(), realloc(), free(),
మెమరీ లీక్లు (Memory Leaks), డ్యాంగ్లింగ్ పాయింటర్లు మరియు వాస్తవ డేటాబేస్ ఆర్కిటెక్చర్ను లోతుగా నేర్చుకుందాం!
1 Stack vs Heap: కంప్యూటర్ మెమరీ ఆర్కిటెక్చర్ & స్టాటిక్ కేటాయింపు పరిమితులు
C ప్రోగ్రామ్ మెమరీని ఎలా ఉపయోగిస్తుందో అర్థం చేసుకోవడానికి, ఆపరేటింగ్ సిస్టమ్ మన ప్రోగ్రామ్కు కేటాయించే RAM విభజనను పరిశీలించాలి. ఒక C ప్రోగ్రామ్ ఎగ్జిక్యూట్ అయ్యేటప్పుడు RAM ప్రధానంగా నాలుగు విభాగాలుగా విభజించబడుతుంది:
| మెమరీ విభాగం (Segment) | ఎవరు నిర్వహిస్తారు? | జీవితకాలం (Lifetime) | ఉద్దేశం / స్టోర్ అయ్యే అంశాలు |
|---|---|---|---|
| 1. Text / Code Segment | OS లోడర్ (Read-Only) | ప్రోగ్రామ్ పూర్తయ్యే వరకు | కంపైల్ చేయబడిన బైనరీ మెషిన్ ఇన్స్ట్రక్షన్లు (CPU కోడ్). ఇక్కడ డేటాను రాయలేం. |
| 2. Data & BSS Segments | OS & Compiler | ప్రోగ్రామ్ పూర్తయ్యే వరకు | గ్లోబల్ మరియు స్టాటిక్ వేరియబుల్స్. Initialized డేటా సెగ్మెంట్లో, Uninitialized BSS సెగ్మెంట్లో ఉంటాయి. |
| 3. Stack Memory | CPU / కంపైలర్ (ఆటోమేటిక్) | ఫంక్షన్ ఎగ్జిక్యూషన్ ముగిసే వరకు | ఫంక్షన్ కాల్ ఫ్రేమ్స్, లోకల్ వేరియబుల్స్, ఫంక్షన్ పారామీటర్లు. LIFO ఆర్డర్లో వేగంగా పనిచేస్తుంది. సైజ్ పరిమితం (~1MB to 8MB). |
| 4. Heap Memory | ప్రోగ్రామర్ మాన్యువల్ నియంత్రణ | free() చేసే వరకు లేదా ప్రోగ్రామ్ ముగిసే వరకు |
డైనమిక్ కేటాయింపులు (malloc/calloc). భారీ మెమరీ పూల్ (గిగాబైట్ల కొద్దీ RAM అందుబాటులో ఉంటుంది). |
కంప్యూటర్ RAM ఆర్కిటెక్చర్ డయాగ్రమ్:
│ STACK (స్టాక్ మెమరీ) │
│ (ఫంక్షన్ కాల్స్, లోకల్ వేరియబుల్స్ - ⬇️ క్రిందికి పెరుగుతుంది) │
├────────────────────────────────────────────────────────┤
│ ⬇️ │
│ │
│ ఖాళీ మెమరీ స్థలం (Free Memory Gap) │
│ │
│ ⬆️ │
├────────────────────────────────────────────────────────┤
│ HEAP (హీప్ మెమరీ పూల్) │
│ (malloc / calloc కేటాయింపులు - ⬆️ పైకి పెరుగుతుంది) │
├────────────────────────────────────────────────────────┤
│ BSS Segment (Uninitialized Globals/Statics) │
├────────────────────────────────────────────────────────┤
│ Data Segment (Initialized Globals/Statics) │
├────────────────────────────────────────────────────────┤
Low Memory (0x00000000) │ Text Segment (మెషిన్ కోడ్ - Read Only) │
└────────────────────────────────────────────────────────┘
స్టాక్ లోకల్ వేరియబుల్స్ ఫంక్షన్ పూర్తవగానే ఆటోమేటిక్గా నాశనం అవుతాయి. కానీ ఒక ఫంక్షన్లో సృష్టించిన డేటా ఇతర ఫంక్షన్లలో కూడా బ్రతికి ఉండాలన్నా, లేదా రన్టైమ్లో యూజర్ కోరిన సైజులో మాత్రమే ఖచ్చితంగా మెమరీని రిజర్వ్ చేయాలన్నా హీప్ మెమరీ (Heap) ఒక్కటే ఏకైక మార్గం!
2 <stdlib.h> హెడర్ ఫైల్ మరియు డైనమిక్ మెమరీ బేసిక్స్
C భాషలో డైనమిక్ మెమరీని కేటాయించడానికి మరియు విడుదల చేయడానికి కావాల్సిన అన్ని ఫంక్షన్లు <stdlib.h>
(Standard Library) హెడర్ ఫైల్లో నిర్వచించబడ్డాయి. అందువల్ల DMA వాడే ప్రతి ప్రోగ్రామ్ ప్రారంభంలో #include <stdlib.h> తప్పనిసరిగా చేర్చాలి.
C స్టాండర్డ్ లైబ్రరీ మనకు నాలుగు కీలకమైన ఫంక్షన్లను అందిస్తుంది:
malloc()— Memory Allocation: ఇచ్చిన బైట్ల పరిమాణంలో ఒకే బ్లాక్ మెమరీని కేటాయిస్తుంది (కానీ ఇనిషియలైజ్ చేయదు).calloc()— Contiguous Allocation: సమాన పరిమాణంలో ఉన్న బహుళ ఎలిమెంట్లకు మెమరీని కేటాయించి, ప్రతి బైట్నూ సున్నా (0) చేస్తుంది.realloc()— Re-Allocation: ఇప్పటికే కేటాయించిన మెమరీ బ్లాక్ సైజును రన్టైమ్లో పెంచుతుంది లేదా తగ్గిస్తుంది.free()— Deallocation: హీప్లో తీసుకున్న మెమరీని తిరిగి ఆపరేటింగ్ సిస్టమ్కు విడుదల చేస్తుంది.
పాయింటర్లు మరియు void* (జెనెరిక్ పాయింటర్) పాత్ర:
హీప్ మెమరీలో స్టోర్ అయ్యే వేరియబుల్స్కు పేర్లు (identifiers) ఉండవు! కేవలం వాటి మెమరీ చిరునామా (Address) మాత్రమే ఉంటుంది.
అందుకే DMA ఫంక్షన్లన్నీ void* (Generic Pointer) ను రిటర్న్ చేస్తాయి.
void* అంటే ఏ డేటా టైప్కైనా మారగల ఒక సార్వత్రిక చిరునామా. మనం దానిని అవసరమైన టైప్లోకి టైప్కాస్ట్ చేసుకోవచ్చు.
ఎప్పుడూ
malloc(20) అని హార్డ్కోడ్ చేయకండి! ఎందుకంటే 32-bit ఆర్కిటెక్చర్లో int 4 బైట్లు ఉండవచ్చు, కొన్ని సిస్టమ్లలో పాయింటర్లు 4 లేదా 8 బైట్లు ఉండవచ్చు.
ఎల్లప్పుడూ sizeof(int) * n లేదా sizeof(float) * n అని రాయడమే సీనియర్ సాఫ్ట్వేర్ ఇంజనీర్ల ప్రామాణిక పద్ధతి!
3 malloc(): బైట్ల కేటాయింపు మరియు గార్బేజ్ వాల్యూస్ (Garbage Data)
malloc() అనేది డైనమిక్ మెమరీ కోసం అత్యంత ప్రాథమిక ఫంక్షన్. దీని ఫంక్షన్ ప్రోటోటైప్ క్రింది విధంగా ఉంటుంది:
void* malloc(size_t size);
ఇక్కడ size అంటే హీప్లో మనకు కావాల్సిన మొత్తం బైట్ల సంఖ్య.
malloc() హీప్లో వరుసగా ఖాళీగా ఉన్న మెమరీ లొకేషన్లను గుర్తించి, మొదటి బైట్ చిరునామాను void* పాయింటర్గా రిటర్న్ చేస్తుంది.
ఒకవేళ సిస్టమ్లో తగినంత ర్యామ్ లేకపోతే అది NULL ను రిటర్న్ చేస్తుంది.
malloc() కేటాయించిన మెమరీలో గతంలో వేరే ప్రాసెస్లు ఉపయోగించి వదిలేసిన చెత్త విలువలు (Garbage Values) అలాగే ఉంటాయి.
మనం స్పష్టంగా విలువలను అసైన్ చేసే వరకు లేదా లూప్తో రీడ్ చేసే వరకు ఆ మెమరీ సురక్షితం కాదు!
malloc() ప్రాక్టికల్ ఉదాహరణ: యూజర్ కోరిన సైజులో డైనమిక్ అర్రే
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("ఎన్ని ఎలిమెంట్లు స్టోర్ చేయాలనుకుంటున్నారు? ");
if (scanf("%d", &n) != 1 || n <= 0) {
printf("చెల్లుబాటు అయ్యే సంఖ్య ఇవ్వండి!\n");
return 1;
}
// హీప్ మెమరీలో n పూర్ణాంకాలకు సరిపడా స్థలాన్ని కేటాయించడం
int *arr = (int *)malloc(n * sizeof(int));
// మెమరీ కేటాయింపు విజయవంతమైందో లేదో తనిఖీ చేయడం
if (arr == NULL) {
printf("ఎర్రర్: హీప్ మెమరీ కేటాయింపు విఫలమైంది!\n");
return 1;
}
printf("\n--- malloc() కేటాయించిన ప్రారంభ గార్బేజ్ డేటా ---\n");
for (int i = 0; i < n; i++) {
printf("arr[%d] = %d (Garbage)\n", i, arr[i]);
}
printf("\nఇప్పుడు %d విలువలను ఎంటర్ చేయండి:\n", n);
for (int i = 0; i < n; i++) {
printf("ఎలిమెంట్ %d: ", i + 1);
scanf("%d", &arr[i]);
}
printf("\nమీరు ఎంటర్ చేసిన విలువల మొత్తం (Sum):\n");
long long sum = 0;
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
sum += arr[i];
}
printf("\nమొత్తం సగటు = %.2f\n", (double)sum / n);
// వాడిన తర్వాత మెమరీని OS కి విడుదల చేయడం
free(arr);
arr = NULL; // Dangling pointer నివారించడానికి
printf("\nమెమరీ విజయవంతంగా విడుదల చేయబడింది!\n");
return 0;
}
4 calloc(): జీరో ఇనిషియలైజేషన్తో మెమరీ కేటాయింపు (Zero Initialization)
calloc() (Contiguous Allocation) అనేది malloc() కు ఒక సురక్షితమైన ప్రత్యామ్నాయం. దీని ప్రోటోటైప్:
void* calloc(size_t num, size_t size);
calloc() రెండు పారామీటర్లను తీసుకుంటుంది:
num: మనకు కావాల్సిన ఎలిమెంట్ల సంఖ్య.size: ప్రతి ఎలిమెంట్ యొక్క బైట్ల పరిమాణం (ఉదా:sizeof(float)).
మొత్తం కేటాయించబడే పరిమాణం = num * size బైట్లు.
calloc() యొక్క సూపర్ పవర్ ఏమిటంటే: ఇది కేటాయించిన ప్రతి బైట్నూ జీరో (0) చేస్తుంది (Zero Initialization).
అందువల్ల ఇందులో ఎలాంటి గార్బేజ్ విలువలు ఉండవు!
calloc() లైవ్ ఎగ్జాంపుల్: ఫ్రీక్వెన్సీ కౌంటర్ మరియు క్లీన్ బఫర్
#include <stdio.h>
#include <stdlib.h>
int main() {
int size = 5;
// calloc ద్వారా 5 పూర్ణాంకాల మెమరీని కేటాయింపు
int *clean_arr = (int *)calloc(size, sizeof(int));
if (clean_arr == NULL) {
printf("మెమరీ కేటాయింపు విఫలమైంది!\n");
return 1;
}
printf("calloc() కేటాయించిన ఎలిమెంట్ల ప్రారంభ విలువలు:\n");
for (int i = 0; i < size; i++) {
// ఇక్కడ ప్రతి ఎలిమెంట్ ఖచ్చితంగా 0 గానే ఉంటుంది
printf("clean_arr[%d] = %d\n", i, clean_arr[i]);
}
// ఇప్పుడు విలువలను అప్డేట్ చేయడం
for (int i = 0; i < size; i++) {
clean_arr[i] = (i + 1) * 10;
}
printf("\nఅప్డేట్ చేసిన తర్వాత విలువలు:\n");
for (int i = 0; i < size; i++) {
printf("clean_arr[%d] = %d\n", i, clean_arr[i]);
}
free(clean_arr);
clean_arr = NULL;
return 0;
}
5 malloc() vs calloc(): పోలిక మరియు పర్ఫార్మెన్స్ విశ్లేషణ
ఇంటర్వ్యూలలో మరియు ప్రొడక్షన్ సిస్టమ్ డిజైన్లో తరచుగా అడిగే ప్రశ్న: "మనం ఎప్పుడు malloc() వాడాలి, ఎప్పుడు calloc() వాడాలి?"
క్రింది పోలిక పట్టిక ద్వారా వాటి అంతర్గత వ్యత్యాసాలను స్పష్టంగా పరిశీలిద్దాం:
| ఫీచర్ / అంశం | malloc() | calloc() |
|---|---|---|
| పారామీటర్ల సంఖ్య | కేవలం 1 పారామీటర్: malloc(total_bytes) |
2 పారామీటర్లు: calloc(num_elements, element_size) |
| ఇనిషియలైజేషన్ | ఏమీ చేయదు; పాత Garbage Values అలాగే ఉంటాయి. | కేటాయించిన ప్రతి బిట్ / బైట్నూ 0 చేస్తుంది. |
| వేగం / పర్ఫార్మెన్స్ | వేగవంతమైనది (Faster), ఎందుకంటే మెమరీని సున్నా చేసే అదనపు పని ఉండదు. | స్వల్పంగా నిదానం (Slightly Slower), ఎందుకంటే కేటాయించిన మెమరీని జీరో చేయడానికి CPU సైకిల్స్ పడతాయి. |
| పూర్ణాంకాల ఓవర్ఫ్లో భద్రత | malloc(n * size) లో గుణకారం జరిగేటప్పుడు Integer Overflow ప్రమాదం ఉంది. |
ఆధునిక C లైబ్రరీలలో calloc గుణకార ఓవర్ఫ్లోను స్వయంగా తనిఖీ చేసి NULL ఇస్తుంది. |
| ఉత్తమ వినియోగ సందర్భం | వెంటనే యూజర్ ఇన్పుట్ ద్వారా లేదా ఫైల్ నుండి మొత్తం డేటాను ఓవర్రైట్ చేస్తున్నప్పుడు. | కౌంటింగ్ అర్రేలు, ఫ్రీక్వెన్సీ మ్యాప్స్, గ్రాఫ్ అడ్జసెన్సీ మ్యాట్రిక్స్, సున్నా డిఫాల్ట్ కావాల్సిన చోట. |
మీరు వెంటనే ఆ అర్రేలోకి వేరే ఫైల్ నుండి లేదా లూప్ ద్వారా డేటాను నింపబోతుంటే,
calloc() వాడితే సమయం వృధా అవుతుంది (ముందు సున్నా చేయడం, వెంటనే వేరే నంబర్ రాయడం).
అటువంటి సందర్భాలలో malloc() వాడటమే గరిష్ట వేగాన్ని అందిస్తుంది!
6 NULL Pointer Check: మెమరీ వైఫల్యాలు & డిఫెన్సివ్ ప్రోగ్రామింగ్
చాలామంది ప్రారంభ ప్రోగ్రామర్లు చేసే ఘోరమైన తప్పు: malloc() పిలవగానే కంప్యూటర్ మెమరీ ఇచ్చేసిందని భావించి నేరుగా యాక్సెస్ చేయడం!
కానీ కంప్యూటర్లో RAM అనంతమైనది కాదు. సిస్టమ్లో తగినంత మెమరీ లేనప్పుడు లేదా OS లిమిట్స్ మించినప్పుడు malloc() లేదా calloc()
NULL పాయింటర్ను రిటర్న్ చేస్తాయి.
ఒకవేళ మీరు NULL పాయింటర్ను డీరెఫరెన్స్ చేస్తే (అంటే *ptr = 10; లేదా ptr[0] = 5; అని రాస్తే),
ఆపరేటింగ్ సిస్టమ్ తక్షణమే మీ ప్రోగ్రామ్ను Segmentation Fault (SIGSEGV) తో చంపేస్తుంది!
డిఫెన్సివ్ ప్రోగ్రామింగ్ గోల్డెన్ ప్యాటర్న్:
#include <stdio.h>
#include <stdlib.h>
int main() {
// ఉద్దేశపూర్వకంగా అసాధ్యమైన భారీ మెమరీని అడగడం (ఉదా: 1000 Terabytes)
size_t huge_size = (size_t)1024 * 1024 * 1024 * 1000ULL;
printf("భారీ మెమరీ కేటాయింపుకు ప్రయత్నిస్తున్నాం...\n");
int *ptr = (int *)malloc(huge_size);
// పాయింటర్ చెక్ తప్పనిసరి!
if (ptr == NULL) {
fprintf(stderr, "క్రిటికల్ ఎర్రర్: సిస్టమ్లో తగినంత ర్యామ్ లేదు! మెమరీ కేటాయింపు విఫలమైంది.\n");
// సిస్టమ్ క్రాష్ అవ్వకుండా గౌరవప్రదంగా నిష్క్రమించడం
return EXIT_FAILURE;
}
// కేటాయింపు విజయవంతమైతేనే ఇక్కడకు వస్తుంది
ptr[0] = 100;
printf("మొదటి ఎలిమెంట్: %d\n", ptr[0]);
free(ptr);
ptr = NULL;
return EXIT_SUCCESS;
}
7 free(): మెమరీని ఆపరేటింగ్ సిస్టమ్కు తిరిగి అప్పగించడం
పైథాన్, జావా లేదా జావాస్క్రిప్ట్ వంటి భాషలలో ఆటోమేటిక్ గార్బేజ్ కలెక్టర్ (Garbage Collector) ఉంటుంది. అవి వాడని ఆబ్జెక్టులను అవే డిలీట్ చేసుకుంటాయి. కానీ C భాషలో గార్బేజ్ కలెక్టర్ ఉండదు! C లో ప్రతి బైట్ మెమరీ ప్రోగ్రామర్ బాధ్యత.
హీప్ నుండి తీసుకున్న మెమరీ బ్లాక్తో పని పూర్తయిన వెంటనే దానిని free() ఫంక్షన్ ద్వారా ఆపరేటింగ్ సిస్టమ్కు అప్పగించాలి:
void free(void* ptr);
free() నియమాలు & గోల్డెన్ రూల్స్:
- రూల్ 1: కేవలం
malloc,calloc, లేదాreallocద్వారా వచ్చిన పాయింటర్లను మాత్రమేfree()చేయాలి. స్టాక్ వేరియబుల్స్ను (ఉదా:int x = 10; free(&x);) ఫ్రీ చేస్తే ప్రోగ్రామ్ క్రాష్ అవుతుంది! - రూల్ 2:
free(NULL)చేయడం సురక్షితం. C స్టాండర్డ్ ప్రకారం NULL పాయింటర్ను ఫ్రీ చేస్తే ఎలాంటి ఎర్రర్ రాదు, ఏమీ జరగదు. - రూల్ 3: ఫ్రీ చేసిన వెంటనే ఆ పాయింటర్ను
NULLచేయాలి:free(ptr); ptr = NULL;. దీనివల్ల పొరపాటున పాత అడ్రస్ను మళ్లీ వాడే ప్రమాదం తప్పుతుంది.
8 realloc(): సైజును డైనమిక్గా మార్చడం (Dynamic Resizing)
ఒకసారి మెమరీ కేటాయించిన తర్వాత, ప్రోగ్రామ్ నడుస్తున్నప్పుడు ఆ స్థలం చాలకపోతే లేదా చాలా ఎక్కువగా మిగిలిపోతే దాని పరిమాణాన్ని మార్చడానికి
realloc() (Re-Allocation) ను ఉపయోగిస్తాం.
void* realloc(void* ptr, size_t new_size);
realloc() తెరవెనుక ఎలా పనిచేస్తుంది? (3 విభిన్న దృశ్యాలు):
- In-Place Expansion: ప్రస్తుత మెమరీ బ్లాక్ పక్కనే తగినంత ఖాళీ స్థలం ఉంటే, అదే బ్లాక్ పరిమాణాన్ని పెంచుతుంది. పాయింటర్ చిరునామా మారదు!
- New Block Migration: పక్కన ఖాళీ లేకపోతే, హీప్లో వేరొక చోట కొత్తగా
new_sizeస్థలాన్ని సృష్టించి, పాత డేటాను స్వయంగా కొత్త ప్రదేశానికి కాపీ చేసి, పాత బ్లాక్ను ఆటోమేటిక్గా ఫ్రీ చేసి, కొత్త పాయింటర్ను రిటర్న్ చేస్తుంది! - Failure: కొత్త సైజుకు సరిపడా ర్యామ్ లేకపోతే,
NULLరిటర్న్ చేస్తుంది. కానీ ముఖ్యమైన విషయం: పాత బ్లాక్ చెక్కుచెదరకుండా అలాగే ఉంటుంది.
ఎప్పుడూ
ptr = realloc(ptr, new_size); అని రాయకండి! ఒకవేళ రీఎలొకేషన్ విఫలమై NULL వస్తే, మీ పాత ptr చిరునామా కూడా పోతుంది!
దీనివల్ల పాత మెమరీ హీప్లో అనాథగా మిగిలిపోయి మెమరీ లీక్ ఏర్పడుతుంది. ఎల్లప్పుడూ తాత్కాలిక పాయింటర్ (temp) ను వాడండి!
realloc() సరైన మరియు సురక్షితమైన వినియోగ కోడ్:
#include <stdio.h>
#include <stdlib.h>
int main() {
int initial_cap = 3;
int *arr = (int *)malloc(initial_cap * sizeof(int));
if (arr == NULL) {
printf("ప్రారంభ మెమరీ విఫలమైంది!\n");
return 1;
}
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
printf("ప్రారంభ అర్రే (సైజు: %d): %d, %d, %d\n", initial_cap, arr[0], arr[1], arr[2]);
printf("ప్రారంభ పాయింటర్ చిరునామా: %p\n", (void*)arr);
// ఇప్పుడు సైజును 3 నుండి 6 కి పెంచాలనుకుంటున్నాం
int new_cap = 6;
int *temp = (int *)realloc(arr, new_cap * sizeof(int));
if (temp == NULL) {
printf("రీసైజింగ్ విఫలమైంది! పాత డేటా అలాగే ఉంది.\n");
free(arr);
return 1;
}
// రీసైజింగ్ విజయవంతమైంది కాబట్టి పాయింటర్ను అప్డేట్ చేయాలి
arr = temp;
printf("రీసైజ్ తర్వాత పాయింటర్ చిరునామా: %p\n", (void*)arr);
// కొత్తగా జోడించిన స్థానాల్లో విలువలు రాయడం
arr[3] = 40;
arr[4] = 50;
arr[5] = 60;
printf("\nవిస్తరించిన అర్రే లోని అన్ని 6 ఎలిమెంట్లు:\n");
for (int i = 0; i < new_cap; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
free(arr);
arr = NULL;
return 0;
}
9 Memory Leaks (మెమరీ లీక్లు) & Valgrind డీబగ్గింగ్ కాన్సెప్ట్
మెమరీ లీక్ (Memory Leak) అంటే ఏమిటి? హీప్ మెమరీలో malloc లేదా calloc ద్వారా మెమరీని కేటాయించిన తర్వాత,
ఆ మెమరీని free() చేయకుండానే దాన్ని సూచించే పాయింటర్ను కోల్పోవడాన్ని మెమరీ లీక్ అంటారు.
మెమరీ లీక్ ఎందుకు అంత ప్రమాదకరం?
మీరు రాసే చిన్న కన్సోల్ ప్రోగ్రామ్ ముగియగానే OS మొత్తం మెమరీని క్లీన్ చేస్తుంది. కానీ నిరంతరం 24/7 నడిచే వెబ్ సర్వర్లు, డేటాబేస్లు, ఆపరేటింగ్ సిస్టమ్ కెర్నల్స్ లేదా ఎంబెడెడ్ డివైస్లలో ప్రతి రిక్వెస్ట్కు కేవలం 1 KB మెమరీ లీక్ అయినా, కొన్ని రోజుల్లోనే మొత్తం RAM అయిపోయి సర్వర్ పూర్తిగా క్రాష్ అవుతుంది (Out of Memory - OOM Crash)!
మెమరీ లీక్ ఏర్పడే 2 సాధారణ మార్గాలు:
int *ptr = (int *)malloc(100 * sizeof(int)); // బ్లాక్ A కేటాయించబడింది
ptr = (int *)malloc(200 * sizeof(int)); // బ్లాక్ B కేటాయించబడింది!
// 🚨 బ్లాక్ A చిరునామా శాశ్వతంగా పోయింది! దానిని ఇక ఎవరూ free చేయలేరు!
// లీక్ దృశ్యం 2: ఫంక్షన్ లోకల్ పాయింటర్లను ఫ్రీ చేయకుండా బయటకు రావడం
void do_calculation() {
int *data = (int *)malloc(500 * sizeof(int));
// లెక్కింపు పూర్తయింది... కానీ free(data) రాయడం మర్చిపోయారు!
return; // ఫంక్షన్ ముగియగానే స్టాక్ లోని 'data' పోతుంది, కానీ హీప్ బ్లాక్ అలాగే ఉండిపోతుంది!
}
రియల్ వరల్డ్ లైనక్స్ / యూనిక్స్ ఎన్విరాన్మెంట్లో డెవలపర్లు మెమరీ లీక్లను పట్టుకోవడానికి Valgrind టూల్ వాడతారు:
valgrind --leak-check=full ./my_programఅలాగే ఆధునిక GCC / Clang కంపైలర్లలో
-fsanitize=address -g ఫ్లాగ్తో కంపైల్ చేస్తే, రన్టైమ్లో లీక్ అయిన ప్రతి లైన్ నంబర్ను టెర్మినల్లో చూపిస్తుంది!
10 Dangling Pointers, Wild Pointers & Double Free లోపాలు
C భాషలో పాయింటర్లు మరియు డైనమిక్ మెమరీతో పనిచేసేటప్పుడు ఎదురయ్యే మూడు అత్యంత ప్రమాదకరమైన బగ్స్:
1. Dangling Pointer (వేలాడే పాయింటర్):
ఒక మెమరీ బ్లాక్ను free(ptr) చేసిన తర్వాత కూడా ఆ పాయింటర్ పాత మెమరీ అడ్రస్ను గుర్తుపెట్టుకుని ఉంటుంది.
ఆ విడుదల చేయబడిన ప్రదేశాన్ని యాక్సెస్ చేయడానికి ప్రయత్నించే పాయింటర్ను Dangling Pointer అంటారు.
దీనిని చదవడం లేదా రాయడం తీవ్రమైన భద్రతా లోపాలకు (Use-After-Free Vulnerability) దారితీస్తుంది.
2. Double Free Error (రెండుసార్లు ఫ్రీ చేయడం):
ఒకే మెమరీ బ్లాక్పై రెండుసార్లు free() పిలిస్తే, ఆపరేటింగ్ సిస్టమ్ హీప్ మేనేజర్లోని మెటాడేటా పాడైపోతుంది.
కంపైలర్ వెంటనే "double free or corruption (fasttop)" అని ప్రోగ్రామ్ను అబార్ట్ చేస్తుంది.
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int));
*ptr = 42;
printf("విలువ: %d\n", *ptr);
// మెమరీని ఫ్రీ చేయడం
free(ptr);
// 🚨 ఇక్కడ ptr ఒక Dangling Pointer!
// ఒకవేళ మనం ptr = NULL చేయకపోతే:
// free(ptr); // 💥 CRASH: Double free error!
// సరైన రక్షణ మార్గం:
ptr = NULL;
// ఇప్పుడు free(ptr) పిలిచినా ఏ ప్రమాదం ఉండదు (free(NULL) is harmless)
free(ptr);
printf("సురక్షితంగా రక్షించబడింది!\n");
return 0;
}
11 2D డైనమిక్ అర్రేలు (Dynamic 2D Arrays on Heap)
రన్టైమ్లో యూజర్ కోరిన వరుసలు (Rows) మరియు నిలువు వరుసలు (Columns) తో 2D మ్యాట్రిక్స్ను హీప్లో సృష్టించడానికి రెండు విధానాలు ఉన్నాయి:
విధానం 1: Array of Pointers (పాయింటర్ల అర్రే - డబుల్ పాయింటర్ int**)
ముందుగా Rows సంఖ్యలో పాయింటర్లను (int*) కేటాయించి, ఆపై ప్రతి రో కోసం విడిగా malloc పిలుస్తాం.
దీని ప్రయోజనం ఏమిటంటే, ప్రతి వరుసకు వేర్వేరు సంఖ్యలో కాలమ్స్ (Jagged Array) కేటాయించవచ్చు!
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 3, cols = 4;
// 1. రో పాయింటర్ల కోసం మెమరీ కేటాయింపు (int* అర్రే)
int **matrix = (int **)malloc(rows * sizeof(int *));
if (matrix == NULL) return 1;
// 2. ప్రతి రో కోసం విడిగా కాలమ్స్ కేటాయించడం
for (int i = 0; i < rows; i++) {
matrix[i] = (int *)malloc(cols * sizeof(int));
if (matrix[i] == NULL) return 1;
}
// 3. విలువలను నింపడం (i * 10 + j)
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i][j] = (i + 1) * 10 + (j + 1);
}
}
// 4. మ్యాట్రిక్స్ను డిస్ప్లే చేయడం
printf("--- 2D డైనమిక్ మ్యాట్రిక్స్ (%d x %d) ---\n", rows, cols);
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%4d ", matrix[i][j]);
}
printf("\n");
}
// 5. డీ-ఎలొకేషన్: రివర్స్ ఆర్డర్లో ఫ్రీ చేయాలి!
for (int i = 0; i < rows; i++) {
free(matrix[i]); // ప్రతి వరుసను ఫ్రీ చేయడం
}
free(matrix); // ప్రధాన పాయింటర్ను ఫ్రీ చేయడం
matrix = NULL;
printf("\n2D మ్యాట్రిక్స్ మెమరీ పూర్తిగా క్లీన్ చేయబడింది!\n");
return 0;
}
12 రియల్-వరల్డ్ ప్రాజెక్ట్: డైనమిక్ రీసైజబుల్ స్టూడెంట్ రికార్డ్స్ మేనేజర్
ఇప్పుడు మనం నేర్చుకున్న struct, malloc, realloc, free మరియు డిఫెన్సివ్ ప్రోగ్రామింగ్
అంశాలన్నింటినీ కలిపి ఒక పూర్తి స్థాయి డైనమిక్ స్టూడెంట్ డేటాబేస్ (Auto-Growing Vector / ArrayList) ను నిర్మిద్దాం.
డేటాబేస్ ప్రారంభ కెపాసిటీ 2 మాత్రమే ఉంటుంది. యూజర్ కొత్త స్టూడెంట్లను జోడిస్తూ కెపాసిటీ నిండిపోగానే, ప్రోగ్రామ్ ఆటోమేటిక్గా
realloc() ద్వారా కెపాసిటీని రెట్టింపు (2 ➔ 4 ➔ 8) చేసుకుంటుంది. యూజర్ నిష్క్రమించగానే మొత్తం మెమరీని సురక్షితంగా క్లీన్ చేస్తుంది!
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int id;
char name[50];
float gpa;
} Student;
typedef struct {
Student *data; // డైనమిక్ అర్రే పాయింటర్
int size; // ప్రస్తుతం ఉన్న విద్యార్థుల సంఖ్య
int capacity; // గరిష్ట కెపాసిటీ
} StudentDatabase;
// డేటాబేస్ను ప్రారంభించడం
void init_db(StudentDatabase *db, int initial_capacity) {
db->data = (Student *)malloc(initial_capacity * sizeof(Student));
if (db->data == NULL) {
printf("డేటాబేస్ మెమరీ కేటాయింపు విఫలమైంది!\n");
exit(1);
}
db->size = 0;
db->capacity = initial_capacity;
printf("[System]: డేటాబేస్ ప్రారంభమైంది. కెపాసిటీ = %d\n", db->capacity);
}
// విద్యార్థిని జోడించడం (ఆటో-గ్రోయింగ్ లాజిక్)
void add_student(StudentDatabase *db, int id, const char *name, float gpa) {
// కెపాసిటీ నిండిపోతే రెట్టింపు చేయడం
if (db->size == db->capacity) {
int new_capacity = db->capacity * 2;
printf("\n⚠️ [Auto-Grow]: కెపాసిటీ నిండింది! %d నుండి %d కి విస్తరిస్తున్నాం...\n", db->capacity, new_capacity);
Student *temp = (Student *)realloc(db->data, new_capacity * sizeof(Student));
if (temp == NULL) {
printf("ఎర్రర్: మెమరీ విస్తరణ విఫలమైంది!\n");
return;
}
db->data = temp;
db->capacity = new_capacity;
}
// కొత్త రికార్డును చొప్పించడం
db->data[db->size].id = id;
strncpy(db->data[db->size].name, name, sizeof(db->data[db->size].name) - 1);
db->data[db->size].name[sizeof(db->data[db->size].name) - 1] = '\0';
db->data[db->size].gpa = gpa;
db->size++;
printf("✅ విద్యార్థి జోడించబడ్డారు: %s (ID: %d) [ప్రస్తుతం: %d/%d]\n", name, id, db->size, db->capacity);
}
// అన్ని రికార్డులను చూపించడం
void print_db(const StudentDatabase *db) {
printf("\n================ విద్యార్థుల డేటాబేస్ ================\n");
printf("%-6s %-20s %-8s\n", "ID", "పేరు", "GPA");
printf("----------------------------------------------------\n");
for (int i = 0; i < db->size; i++) {
printf("%-6d %-20s %-8.2f\n", db->data[i].id, db->data[i].name, db->data[i].gpa);
}
printf("====================================================\n\n");
}
// మెమరీని విడుదల చేయడం
void free_db(StudentDatabase *db) {
if (db->data != NULL) {
free(db->data);
db->data = NULL;
}
db->size = 0;
db->capacity = 0;
printf("[System]: డేటాబేస్ మెమరీ పూర్తిగా క్లీన్ చేయబడింది (No Memory Leaks).\n");
}
int main() {
StudentDatabase db;
init_db(&db, 2); // కేవలం 2 సామర్థ్యంతో ప్రారంభం
// 4 గురు విద్యార్థులను చేర్చడం (ఇది ఆటో-గ్రోత్ ట్రిగ్గర్ చేస్తుంది)
add_student(&db, 101, "శ్రీకాంత్ శర్మ", 9.4);
add_student(&db, 102, "భవాని వర్మ", 8.9);
add_student(&db, 103, "రాకేష్ నాయుడు", 9.1);
add_student(&db, 104, "అనూష రెడ్డి", 9.7);
print_db(&db);
free_db(&db);
return 0;
}
🚀 లైవ్ C కంపైలర్లో రన్ చేసి చూడండి!
డైనమిక్ మెమరీ ఎలా రిజర్వ్ అవుతుందో మరియు realloc ఎలా ఆటో-గ్రోత్ చేస్తుందో మన ఆన్లైన్ C కంపైలర్లో స్వయంగా పరీక్షించండి!
Online C Compiler ఓపెన్ చేయండి ➔13 సారాంశం, ఇంటర్వ్యూ ప్రశ్నలు & సెల్ఫ్-అసెస్మెంట్ క్విజ్
కీలక సారాంశం (Key Takeaways):
- Stack vs Heap: స్టాక్ వేగవంతమైనది కానీ పరిమిత సైజు కలిగి ఉంటుంది; హీప్ భారీగా ఉండి ప్రోగ్రామర్ నియంత్రణలో ఉంటుంది.
- malloc(size): బైట్లను కేటాయిస్తుంది కానీ ఇనిషియలైజ్ చేయదు (గార్బేజ్ విలువలు ఉంటాయి).
- calloc(n, size): ఎలిమెంట్లను కేటాయించి ప్రతి బిట్ను సున్నా (0) చేస్తుంది.
- realloc(ptr, new_size): సైజును రన్టైమ్లో తగ్గిస్తుంది లేదా పెంచుతుంది. ఎల్లప్పుడూ తాత్కాలిక పాయింటర్తో సురక్షితంగా వాడాలి.
- free(ptr): వాడిన తర్వాత మెమరీని తిరిగి విడుదల చేయాలి, వెంటనే
ptr = NULL;చేయాలి. - Memory Leak: పాయింటర్ను ఫ్రీ చేయకుండా రిఫరెన్స్ కోల్పోతే ఏర్పడుతుంది; సర్వర్లకు అత్యంత హానికరం.
టాప్ 5 C డైనమిక్ మెమరీ ఇంటర్వ్యూ ప్రశ్నలు:
- Q1: malloc(0) కాల్ చేస్తే C లో ఏమి జరుగుతుంది?
జవాబు: ఇది ఇంప్లిమెంటేషన్ డిఫైన్డ్. C స్టాండర్డ్ ప్రకారం ఇదిNULLఅయినా రిటర్న్ చేయవచ్చు లేదా డీరెఫరెన్స్ చేయకూడని ఒక చెల్లుబాటు అయ్యే నాన్-నల్ పాయింటర్ను అయినా రిటర్న్ చేయవచ్చు. అయితే దానిని కూడా ఖచ్చితంగాfree()చేయాలి! - Q2: free() కి కేటాయించిన సైజు ఎలా తెలుస్తుంది? మనం సైజ్ ఇవ్వట్లేదు కదా?
జవాబు: చాలా ఆసక్తికరమైన ప్రశ్న!mallocమెమరీని ఇచ్చేటప్పుడు ఆ బ్లాక్ కంటే ముందు కొన్ని బైట్ల స్థలంలో హెడర్ / మెటాడేటా (Block Header) ను రాస్తుంది. అందులో ఆ బ్లాక్ సైజు మరియు ఫ్రీ ఫ్లాగ్స్ ఉంటాయి.free(ptr)ఆ హెడర్ను చదివి ఎంత మెమరీ క్లీన్ చేయాలో తెలుసుకుంటుంది! - Q3: స్టాక్ ఓవర్ఫ్లో (Stack Overflow) మరియు హీప్ ఓవర్ఫ్లో (Heap Overflow) మధ్య తేడా ఏమిటి?
జవాబు: రికార్షన్ అనంతంగా జరగడం వల్ల లేదా స్టాక్లో భారీ అర్రేలను కేటాయించడం వల్ల స్టాక్ పరిమితి దాటిపోవడాన్ని స్టాక్ ఓవర్ఫ్లో అంటారు. హీప్లో కేటాయించిన బౌండరీని దాటి పక్కనున్న మెమరీ బ్లాక్లోకి డేటాను రాయడాన్ని హీప్ ఓవర్ఫ్లో అంటారు. - Q4: realloc(ptr, 0) కాల్ చేస్తే ఏమి జరుగుతుంది?
జవాబు: ఇదిfree(ptr)తో సమానంగా పనిచేసి మెమరీని విడుదల చేసి NULL రిటర్న్ చేస్తుంది (C23 స్టాండర్డ్లో దీనిపై మరింత కఠినమైన నియమాలు వచ్చాయి). - Q5: మెమరీ లీక్ను గుర్తించే పాపులర్ టూల్స్ ఏవి?
జవాబు: లైనక్స్ సిస్టమ్లలో Valgrind, కంపైలర్లలో AddressSanitizer (ASan), విండోస్లో CRT Debug Library వాడతారు.
ఇంటరాక్టివ్ సెల్ఫ్-అసెస్మెంట్ క్విజ్ (Self-Assessment Quiz):
❓ ప్రశ్న 1: calloc(10, sizeof(int)) మరియు malloc(10 * sizeof(int)) మధ్య ప్రధాన వ్యత్యాసం ఏమిటి?
calloc() కేటాయించిన మెమరీలోని ప్రతి బైట్ను సున్నా (0) గా ఇనిషియలైజ్ చేస్తుంది, కాబట్టి అందులో గార్బేజ్ విలువలు ఉండవు.
కానీ malloc() మెమరీని క్లీన్ చేయదు, అందులో పాత చెత్త విలువలు (Garbage values) ఉంటాయి.
❓ ప్రశ్న 2: ఒక పాయింటర్ను free(ptr) చేసిన తర్వాత వెంటనే *ptr = 25; అని రాస్తే ఏమి జరుగుతుంది?
ptr ఇప్పుడు ఒక Dangling Pointer.
OS ఆ మెమరీని వేరే ప్రాసెస్కు ఇచ్చి ఉంటే ప్రోగ్రామ్ క్రాష్ (Segmentation Fault) అవుతుంది, లేదా సైలెంట్గా డేటాను కరప్ట్ చేస్తుంది.
❓ ప్రశ్న 3: ptr = realloc(ptr, new_size); అని రాయడం ఎందుకు చెడు ప్రోగ్రామింగ్ పద్ధతి?
realloc() విఫలమైతే, అది NULL రిటర్న్ చేస్తుంది.
అప్పుడు పాత మెమరీ చిరునామా కూడా NULL తో ఓవర్రైట్ అయిపోయి శాశ్వతంగా మెమరీ లీక్ (Memory Leak) అవుతుంది. ఎప్పుడూ తాత్కాలిక పాయింటర్ (temp) వాడాలి.
❓ ప్రశ్న 4: int a = 10; int *p = &a; free(p); — ఈ కోడ్ రన్ చేస్తే ఏమి జరుగుతుంది?
a అనేది స్టాక్ మెమరీలో ఉన్న లోకల్ వేరియబుల్.
free() ని కేవలం హీప్ మెమరీ (malloc/calloc/realloc) పాయింటర్లపై మాత్రమే పిలవాలి.
❓ ప్రశ్న 5: ఒక ప్రోగ్రామ్లో free(NULL); అని రాస్తే ఎర్రర్ వస్తుందా?
free(NULL) పూర్తిగా సురక్షితమైనది మరియు ఏ ఆపరేషన్ చేయకుండా సైలెంట్గా రిటర్న్ అవుతుంది.
అందుకే free(ptr); ptr = NULL; చేయడం ఉత్తమమైన భద్రతా పద్ధతి.