పాయింటర్లు, అర్రేస్, Double Pointers & కాల్బ్యాక్స్
ఒక సాహసోపేతమైన నిధుల వేటను (Treasure Hunt) ఊహించుకోండి. మీరు అడవిలో మొదటి చెక్క పెట్టెను కనుగొని తెరిచారు. కానీ లోపల బంగారం ఉండదు; దానికి బదులుగా "నిధి ఉన్న రెండవ పెట్టె ఏ చెట్టు కింద ఉందో తెలిపే మ్యాప్" కనిపిస్తుంది (సింగిల్ పాయింటర్ *ptr). ఇప్పుడు ఆ మ్యాప్ను మీ చేతికి ఇచ్చిన గైడ్ బుక్ మీ వద్ద ఉందనుకుందాం — ఆ గైడ్ బుక్ నేరుగా నిధిని చూపించదు; అది మ్యాప్ ఉన్న పెట్టెను చూపిస్తుంది, ఆ మ్యాప్ అసలైన నిధిని చూపిస్తుంది! ఇది కంప్యూటర్ ప్రోగ్రామింగ్లోని డబుల్ పాయింటర్ (Pointer to Pointer - **ptr)!
ఇంతటితో ఆగకుండా, C లాంగ్వేజ్లోని అత్యంత అద్భుతమైన అంతర్గత రహస్యం మరొకటి ఉంది: మనం నిత్యం వాడే arr[i] అనేది అసలు C కంపైలర్ దృష్టిలో అర్రేనే కాదు! అది కేవలం ఒక తీపి పైపూత (Syntactic Sugar) మాత్రమే! కంపైలర్ దానిని తెరవెనుక నేరుగా *(arr + i) అనే పాయింటర్ అరిథ్మెటిక్గా మారుస్తుంది! అందుకే C లో arr[2] అని రాసినా లేదా 2[arr] అని రాసినా కంపైలర్ ఏమాత్రం తడబడకుండా ఒకే ఫలితాన్ని ఇస్తుంది!
ఈ పదహారవ అధ్యాయంతో మనం Phase 04 (అర్రేస్, స్ట్రింగ్స్ & పాయింటర్లు) ను విజయవంతంగా ముగించబోతున్నాం. ఇందులో అర్రే-పాయింటర్ ఈక్వివలెన్స్, డబుల్ పాయింటర్లు, టెర్మినల్ కమాండ్ లైన్ ఆర్గుమెంట్స్ (argc, argv), మరియు ఫంక్షన్లను వేరియబుల్స్లా పాస్ చేసే అడ్వాన్స్డ్ ఫంక్షన్ పాయింటర్లు & కాల్బ్యాక్స్ (Function Pointers & Callbacks) ను సమగ్రంగా మాస్టర్ చేద్దాం!
అడ్వాన్స్డ్ C ప్రోగ్రామింగ్లో పాయింటర్లు మరియు అర్రేల సమన్వయం అనేది సాఫ్ట్వేర్ ఆర్కిటెక్ట్లకు అత్యంత కీలకమైన ఆయుధం. ఈ అధ్యాయంలో కవర్ చేయబడే ప్రధానాంశాలు:
- అర్రే-పాయింటర్ అంతర్గత బంధం:
arr[i] == *(arr + i)గణిత సమానత్వం మరియుi[arr]వింత ప్రవర్తన. - అర్రే వర్సెస్ పాయింటర్ కీలక తేడాలు: కాన్స్టాంట్ పాయింటర్ రీ-అసైన్మెంట్ పరిమితులు మరియు
sizeofవ్యత్యాసాలు. - అర్రే ఆఫ్ పాయింటర్స్ (Array of Pointers -
int *arr[]): పాయింటర్ల వరుస మరియు ర్యాగ్డ్ అర్రేస్ (Ragged Arrays). - డబుల్ పాయింటర్లు (Double Pointers -
**ptr): పాయింటర్ యొక్క మెమరీ అడ్రస్ను పట్టుకునే లెవల్-2 ఇండైరెక్షన్. - ఫంక్షన్లలో పాయింటర్ మోడిఫికేషన్: ఫంక్షన్ లోపల కాలర్ యొక్క పాయింటర్ను మార్చడానికి డబుల్ పాయింటర్లు ఎందుకు తప్పనిసరి?
- కమాండ్ లైన్ ఆర్గుమెంట్స్ (Command-Line Arguments):
argcమరియుargv(అర్రే ఆఫ్ స్ట్రింగ్స్char**) విశ్లేషణ. - సమగ్ర తులనాత్మక పట్టిక:
*arr[](అర్రే ఆఫ్ పాయింటర్స్) vs(*arr)[](పాయింటర్ టు యాన్ అర్రే). - ఫంక్షన్ పాయింటర్లు (Function Pointers): కోడ్ సెగ్మెంట్లో ఫంక్షన్ల మెమరీ అడ్రస్లు మరియు సింటాక్స్ డీకోడింగ్.
- కాల్బ్యాక్ ఫంక్షన్స్ (Callback Architecture): లైబ్రరీలకు ఫంక్షన్లను పాస్ చేయడం మరియు C స్టాండర్డ్
qsort()ఇంజిన్. - డైనమిక్ 2D అర్రేస్ పరిచయం: డబుల్ పాయింటర్లతో రన్టైమ్ గ్రిడ్ నిర్మాణం.
- సమగ్ర రియల్ వరల్డ్ ప్రాజెక్ట్: ప్లగ్-అండ్-ప్లే ఫంక్షన్ పాయింటర్ మ్యాథమెటికల్ క్యాలిక్యులేటర్ ఇంజిన్.
- బిగినర్స్ చేసే టాప్ 5 అడ్వాన్స్డ్ తప్పులు: బ్రాకెట్స్ మిస్సింగ్, డబుల్ పాయింటర్ అన్ఇనిషియలైజ్డ్ సెగ్ఫాల్ట్స్.
- స్వీయ మూల్యాంకన క్విజ్: 5 లోతైన విశ్లేషణాత్మక ప్రశ్నలు & గేట్ ఎగ్జామ్ కాన్సెప్ట్స్.
C లాంగ్వేజ్లో ఒక ప్రాథమిక సూత్రం ఉంది: "An array name evaluates to the address of its first element." (అర్రే పేరు అనగా దాని మొదటి ఎలిమెంట్ యొక్క బేస్ అడ్రస్ &arr[0]). మీరు స్క్వేర్ బ్రాకెట్లతో రాసే ఇండెక్సింగ్ సింటాక్స్ కంపైలర్ కోసం కేవలం ఒక పాయింటర్ అరిథ్మెటిక్ మాత్రమే:
కూడిక అనేది స్థిత్యంతర ధర్మాన్ని (Commutative Property: $A + B = B + A$) పాటిస్తుంది కాబట్టి:
$$*( ext{arr} + i) iff *(i + ext{arr}) iff i[ ext{arr}]$$#include <stdio.h>
int main() {
int arr[4] = {10, 20, 30, 40};
// ఈ 4 పద్ధతులు పూర్తిగా ఒకే ఒక్క విలువను ఇస్తాయి!
printf("1. సాధారణ ఇండెక్సింగ్ arr[2] = %d\n", arr[2]);
printf("2. పాయింటర్ నోటేషన్ *(arr + 2) = %d\n", *(arr + 2));
printf("3. తిరగరాసిన పాయింటర్ *(2 + arr) = %d\n", *(2 + arr));
printf("4. విచిత్రమైన ఇండెక్సింగ్ 2[arr] = %d (పూర్తిగా చెల్లుతుంది!)\n", 2[arr]);
return 0;
}
2[arr] అని కోడ్ ఇస్తే భయపడకండి! అది arr[2] తో సమానం. అయితే ప్రొడక్షన్ కోడ్లో రీడబిలిటీ కోసం ఎల్లప్పుడూ ప్రామాణిక arr[i] మాత్రమే వాడాలి.
అర్రే పేరు పాయింటర్లా ప్రవర్తించినప్పటికీ, అర్రే మరియు పాయింటర్ ఒకటి కావు! వాటి మధ్య రెండు భారీ సాంకేతిక వ్యత్యాసాలు ఉన్నాయి:
1. కాన్స్టాంట్ పాయింటర్ (Re-assignment):
పాయింటర్ అనేది ఒక సాధారణ వేరియబుల్, దానిని వేరొక అడ్రస్కి మార్చవచ్చు (ptr = &y;). కానీ అర్రే పేరు ఒక స్థిరమైన కాన్స్టాంట్ పాయింటర్! దానిని వేరొక అడ్రస్కి మార్చలేము (arr = ptr; // ❌ COMPILE ERROR!).
2. sizeof ఆపరేటర్ ప్రవర్తన:
int arr[5]; పై sizeof(arr) లెక్కిస్తే మొత్తం 20 బైట్లు (5 * 4) వస్తుంది. కానీ int *ptr = arr; పై sizeof(ptr) లెక్కిస్తే కేవలం 8 బైట్లు (పాయింటర్ సైజు) మాత్రమే వస్తుంది!
సాధారణ అర్రే సంఖ్యలను దాచుకుంటే, అర్రే ఆఫ్ పాయింటర్స్ మెమరీ చిరునామాలను (Pointers) ఒక వరుసలో నిల్వ చేస్తుంది. వేర్వేరు పొడవులు గల స్ట్రింగ్స్ను మెమరీ వేస్ట్ కాకుండా నిల్వ చేయడానికి ఇది అద్భుతంగా పనిచేస్తుంది:
#include <stdio.h>
int main() {
// 4 స్ట్రింగ్ పాయింటర్ల అర్రే (Array of 4 char pointers)
const char *districts[] = {
"హైదరాబాద్",
"విశాఖపట్నం",
"విజయవాడ",
"వరంగల్"
};
printf("=== అర్రే ఆఫ్ పాయింటర్స్ డిస్ప్లే ===\n");
for (int i = 0; i < 4; i++) {
printf("జిల్లా #%d: %s (అడ్రస్: %p)\n", i + 1, districts[i], (void*)districts[i]);
}
return 0;
}
ఒక పాయింటర్ కూడా RAM లో ఉండే సాధారణ వేరియబుల్ మాత్రమే కాబట్టి దానికి కూడా ఒక మెమరీ అడ్రస్ ఉంటుంది. ఆ పాయింటర్ యొక్క అడ్రస్ను పట్టుకునే పాయింటర్ను డబుల్ పాయింటర్ (**ptr) అంటారు.
డబుల్ పాయింటర్ ఎప్పుడు అవసరం?
ఒక ఫంక్షన్ లోపల సాధారణ వేరియబుల్ విలువను మార్చడానికి మనం సింగిల్ పాయింటర్ (&x) పాస్ చేస్తాం కదా? అదే విధంగా, ఒక ఫంక్షన్ లోపల ఒక పాయింటర్ పట్టుకున్న అడ్రస్నే మార్చాల్సి వస్తే, ఆ పాయింటర్ యొక్క అడ్రస్ను (డబుల్ పాయింటర్ &ptr) పాస్ చేయాలి!
#include <stdio.h>
int numA = 100;
int numB = 200;
// ఫంక్షన్ లోపల పాయింటర్ను numA నుండి numB కి మారుస్తున్నాం
void changeTarget(int **doublePtr) {
*doublePtr = &numB; // కాలర్ యొక్క పాయింటర్ అడ్రస్ మార్చబడింది!
}
int main() {
int *myPtr = &numA;
printf("ఫంక్షన్ కాల్ కి ముందు: *myPtr = %d (numA)\n", *myPtr);
changeTarget(&myPtr); // పాయింటర్ అడ్రస్ (&myPtr) ని పాస్ చేస్తున్నాం
printf("ఫంక్షన్ కాల్ తర్వాత : *myPtr = %d (numB కి మారింది!)\n", *myPtr);
return 0;
}
మనం టెర్మినల్లో gcc main.c -o app అని కమాండ్స్ రన్ చేస్తాం కదా? ఇక్కడ main.c, -o, app అనేవి కమాండ్ లైన్ ఆర్గుమెంట్స్. C ప్రోగ్రామ్లు టెర్మినల్ నుండి ఇన్పుట్ అందుకోవడానికి main() ఫంక్షన్కు రెండు పారామీటర్లు ఉంటాయి:
int argc(Argument Count): టెర్మినల్ నుండి పాస్ చేసిన మొత్తం పదాల సంఖ్య (ప్రోగ్రామ్ పేరుతో సహా, ఎల్లప్పుడూ ≥ 1).char *argv[](Argument Vector): ఆ పదాల స్ట్రింగ్ పాయింటర్ల అర్రే (డబుల్ పాయింటర్char**).
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("=== కమాండ్ లైన్ ఆర్గుమెంట్స్ అనలిటిక్స్ ===\n");
printf("మొత్తం ఆర్గుమెంట్ల సంఖ్య (argc) = %d\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
C ఇంటర్వ్యూలలో అడిగే అత్యంత కఠినమైన బ్రాకెట్ ప్రిసిడెన్స్ ప్రశ్న:
| డిక్లరేషన్ | సాంకేతిక అర్థం | మెమరీ స్వభావం | ప్రధాన ఉపయోగం |
|---|---|---|---|
int *arr[5]; |
Array of 5 Pointers (5 పాయింటర్ల అర్రే). | 5 వేర్వేరు మెమరీ అడ్రస్లను భద్రపరుస్తుంది (5 × 8 = 40 బైట్లు). | విభిన్న పొడవులు గల స్ట్రింగ్స్, ర్యాగ్డ్ అర్రేస్. |
int (*arr)[5]; |
Pointer to an Array of 5 ints (5 ఎలిమెంట్స్ గల అర్రేను పాయింట్ చేసే ఒకే ఒక్క పాయింటర్). | కేవలం ఒకే ఒక్క పాయింటర్ (8 బైట్లు). | 2D మ్యాట్రిక్స్లను ఫంక్షన్లకు పాస్ చేసేటప్పుడు అడ్డు వరుసలను పాయింట్ చేయడానికి. |
కంప్యూటర్ RAM లో కేవలం వేరియబుల్స్ మాత్రమే కాదు; మనం రాసిన ఫంక్షన్ల మెషీన్ కోడ్ కూడా ఒక మెమరీ అడ్రస్ లోనే నిల్వ ఉంటుంది (Code Segment / Text Segment)! ఆ ఫంక్షన్ యొక్క మెమరీ చిరునామాను పట్టుకునే పాయింటర్ను ఫంక్షన్ పాయింటర్ (Function Pointer) అంటారు!
ఫంక్షన్ పాయింటర్ సింటాక్స్ అనాటమీ:
ఉదాహరణ: int (*operationPtr)(int, int);
#include <stdio.h>
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
int main() {
// ఫంక్షన్ పాయింటర్ డిక్లరేషన్
int (*mathFunc)(int, int);
// 1. add ఫంక్షన్ అడ్రస్ ని అసైన్ చేస్తున్నాం
mathFunc = add;
printf("add ఫంక్షన్ మెమరీ అడ్రస్: %p\n", (void*)mathFunc);
printf("mathFunc(10, 20) ఫలితం = %d\n", mathFunc(10, 20));
// 2. రన్టైమ్లో multiply ఫంక్షన్కి మారుస్తున్నాం!
mathFunc = multiply;
printf("multiply మెమరీ అడ్రస్ : %p\n", (void*)mathFunc);
printf("mathFunc(10, 20) ఫలితం = %d\n", mathFunc(10, 20));
return 0;
}
ఒక ఫంక్షన్కు మరొక ఫంక్షన్ను పారామీటర్గా పంపడాన్ని కాల్బ్యాక్ (Callback) అంటారు. C స్టాండర్డ్ లైబ్రరీలోని ప్రసిద్ధ qsort() అల్గారిథమ్ దీనికి అత్యుత్తమ ఉదాహరణ. సంఖ్యలను అసెండింగ్ లేదా డిసెండింగ్ ఆర్డర్లో ఎలా సార్ట్ చేయాలో మనం ఒక కాల్బ్యాక్ ఫంక్షన్ ద్వారా qsort కు అందిస్తాం:
#include <stdio.h>
#include <stdlib.h>
// qsort కోసం కంపారేటర్ కాల్బ్యాక్ ఫంక్షన్
int compareAscending(const void *a, const void *b) {
int valA = *(const int*)a;
int valB = *(const int*)b;
return valA - valB; // పాజిటివ్ అయితే స్వాప్ చేస్తుంది
}
int main() {
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr) / sizeof(arr[0]);
printf("సార్టింగ్ చేయడానికి ముందు: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
// qsort కు compareAscending ఫంక్షన్ పాయింటర్ను కాల్బ్యాక్గా పాస్ చేస్తున్నాం
qsort(arr, n, sizeof(int), compareAscending);
printf("సార్టింగ్ చేసిన తర్వాత : ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
return 0;
}
సాధారణ 2D అర్రేలలో రోస్ మరియు కాలమ్స్ సంఖ్య కంపైల్-టైమ్లోనే స్థిరంగా ఉండాలి. కానీ డబుల్ పాయింటర్లు (int **matrix) మరియు malloc() ఉపయోగించి యూజర్ కోరిన సైజుతో రన్టైమ్లో ఫ్లెక్సిబుల్ 2D గ్రిడ్లను నిర్మించవచ్చు. దీనిని మనం Phase 05 లో డైనమిక్ మెమరీతో కలిపి మరింత లోతుగా నేర్చుకుంటాం.
ఇప్పటివరకు నేర్చుకున్న ఫంక్షన్ పాయింటర్లు మరియు కాల్బ్యాక్స్ ఉపయోగించి ఒక ఎక్స్టెన్సిబుల్ ప్లగ్-అండ్-ప్లే మ్యాథమెటికల్ కంప్యూట్ ఇంజిన్ (Compute Engine) నిర్మిద్దాం. కోర్ ఇంజిన్ కోడ్ను మార్చకుండానే భవిష్యత్తులో కొత్త ఆపరేషన్లను జోడించవచ్చు:
#include <stdio.h>
// ఫంక్షన్ పాయింటర్ టైప్డెఫ్ (Function Signature)
typedef double (*MathOperation)(double, double);
// ఆపరేషన్లు
double opAdd(double a, double b) { return a + b; }
double opSubtract(double a, double b) { return a - b; }
double opPower(double base, double exp) {
double res = 1.0;
for (int i = 0; i < (int)exp; i++) res *= base;
return res;
}
// కోర్ ఎగ్జిక్యూటర్ ఇంజిన్ (కాల్బ్యాక్ ద్వారా రన్ అవుతుంది)
void executeOperation(const char *opName, MathOperation func, double x, double y) {
double result = func(x, y); // ఫంక్షన్ పాయింటర్ ద్వారా ఇన్వొకేషన్!
printf("⚙️ [%-10s] ఇన్పుట్: (%.1f, %.1f) ➔ ఫలితం = %.2f\n", opName, x, y, result);
}
int main() {
printf("====================================================\n");
printf(" మనా ప్లగ్-అండ్-ప్లే కంప్యూట్ ఇంజిన్ (2026 Engine)\n");
printf("====================================================\n");
// రన్టైమ్లో వేర్వేరు ఫంక్షన్లను కాల్బ్యాక్లుగా పంపుతున్నాం
executeOperation("కూడిక", opAdd, 45.0, 15.0);
executeOperation("తీసివేత", opSubtract, 100.0, 35.0);
executeOperation("ఘాతం (Power)", opPower, 2.0, 8.0);
printf("====================================================\n");
return 0;
}
బ్రాకెట్లు లేకపోతే అది అర్రే ఆఫ్ పాయింటర్స్ అవుతుంది; బ్రాకెట్లు ఉంటే పాయింటర్ టు యాన్ అర్రే అవుతుంది. ఇది భారీ కంపైలేషన్ ఎర్రర్లను ఇస్తుంది.
int **p; **p = 5; అని రాస్తే లెవల్ 1 అడ్రస్ ఖాళీగా ఉండి సెగ్మెంటేషన్ ఫాల్ట్ వస్తుంది.
arr = ptr; అని రాయకూడదు. అర్రే బేస్ అడ్రస్ స్థిరమైనది (Non-modifiable lvalue).
ఫంక్షన్ పాయింటర్
(int, int) ఆశిస్తుంటే (double, double) ఫంక్షన్ను అసైన్ చేస్తే కంపైలర్ టైప్ ఎర్రర్ ఇస్తుంది.
యూజర్ ఆర్గుమెంట్స్ ఇవ్వకుండా ప్రోగ్రామ్ రన్ చేస్తే
argv[1] నల్ అవుతుంది. if (argc > 1) తనిఖీ తప్పనిసరి.
ఈ పాఠంలోని లోతైన అంశాలను క్రింది 5 ప్రాక్టికల్ ప్రశ్నల ద్వారా పరీక్షించుకోండి:
❓ ప్రశ్న 1: C లో arr[i] మరియు *(arr + i) మధ్య వ్యత్యాసం ఏమిటి?
arr[i] అనేది కంపైలర్ కోసం *(arr + i) గా రూపాంతరం చెందే సింటాక్స్ షుగర్ మాత్రమే.
❓ ప్రశ్న 2: డబుల్ పాయింటర్ (**ptr) యొక్క పరిమాణం (sizeof) ఎంత ఉంటుంది?
❓ ప్రశ్న 3: కమాండ్ లైన్ లో argc కనీస విలువ ఎల్లప్పుడూ ఎంత ఉంటుంది?
./app) మొదటి ఆర్గుమెంట్ argv[0] గా పరిగణించబడుతుంది.
❓ ప్రశ్న 4: ఫంక్షన్ పాయింటర్లు (Function Pointers) RAM లో ఏ సెగ్మెంట్లోని చిరునామాను పట్టుకుంటాయి?
❓ ప్రశ్న 5: 'int *a[10]' మరియు 'int (*b)[10]' ల మధ్య ప్రధాన వ్యత్యాసం ఏమిటి?
a అనేది 10 పాయింటర్ల అర్రే (Array of Pointers); b అనేది 10 ఎలిమెంట్స్ గల అర్రేను పాయింట్ చేసే ఒకే ఒక్క పాయింటర్ (Pointer to Array).
పైన వివరించిన ఫంక్షన్ పాయింటర్ కాల్బ్యాక్స్ ఇంజిన్ మరియు డబుల్ పాయింటర్ కోడ్లను మన ఆన్లైన్ C కంపైలర్లో ఓపెన్ చేసి స్వయంగా రన్ చేయండి. కొత్త మ్యాథ్ ఫంక్షన్లను (ఉదా: మాడ్యులో, వర్గమూలం) ప్లగ్-ఇన్ చేసి టెస్ట్ చేయండి!
📌 Phase 04 ముగింపు సారాంశం (Phase 04 Wrap-Up):
- డేటా స్ట్రక్చర్స్ & మెమరీ విజయం: అర్రేలు, 2D Matrices (Ch 13), స్ట్రింగ్స్, నల్ టెర్మినేటర్ (Ch 14), పాయింటర్లు, అరిథ్మెటిక్ (Ch 15), మరియు డబుల్ పాయింటర్లు & ఫంక్షన్ పాయింటర్లు (Ch 16) లపై సంపూర్ణ పట్టు సాధించారు!
- arr[i]: అంతర్గతంగా
*(arr + i)అనే పాయింటర్ అరిథ్మెటిక్. - డబుల్ పాయింటర్: మరొక పాయింటర్ యొక్క మెమరీ అడ్రస్ను నిల్వ చేసే లెవల్-2 పాయింటర్.
- ఫంక్షన్ పాయింటర్లు: రన్టైమ్లో ఫంక్షన్లను డైనమిక్గా మార్చడానికి మరియు కాల్బ్యాక్ ఆర్కిటెక్చర్ నిర్మించడానికి ఉపయోగపడతాయి.