Զրույցներ C ծրագրավորման լեզվի մասին

Զրույց յոթերորդ

Դժվար կլիներ կառուցել քիչ թե շատ պիտանի ծրագրեր, եթե C լեզուն թույլ տար օգտագործել միայն ավտոմատ և ստատիկ փոփոխականներ կամ զանգվածներ (որոնց զբաղեցրած հիշողության չափը որոշվում է կոմպիլյացիայի ժամանակ)։ Ավելի հաճախ են հանդիպում այն դեպքերը, երբ անհրաժեշտ հիշողության չափը հայտնի է դառնում ծրագրի կատարման ընթացքում։

Օրինակ, ինչպե՞ս գրել ծրագիր, որն օգտագործողից պահանջում է ներմուծել իրական թվերի հաջորդականություն, ապա արտածում է այդ նույն թվերը՝ հակառակ կարգով։ Վատագույն դեպքն այն է, որ հայտարարել հնարավորինս մեծ զանգված և նրա մեջ լցնել թվերը։ Բայց միշտ հնարավոր է, որ օգտագործողի խելքին փչի ներմուծել ավելի շատ թվեր, քան զանգվածի չափն է։ Ավելի լավ տարբերակն այն է, որ ծրագրում սահմանված զանգվածն ունենա փոփոխվող, չֆիքսված չափ։

C լեզվի ստանդարտ գրադարանի stdlib.h ֆայլում հայտարարված են դինամիկ հիշողության հետ աշխատող calloc, realloc, malloc և free ֆունկցիաները։ Առաջին երեքը նախատեսված են համակարգից նոր հիշողության տիրույթ առանձնացնելու համար, իսկ չորրորդը՝ առանձնացված հիշողության տիրույթը համակարգին վերադարձնելու համար։

Եվ այսպես, գրում եմ մի ծրագիր, որը ներմուծման ստանդարտ հոսքից կարդում է իրական թվերի հաջորդականություն, այնուհետև, հենց որ հանդիպում է EOF կոդը, այդ թվերն արտածում է ներմուծվածին հակառակ կարգով։

Թվերը ժամանակավորապես պահելու համար կօգտագործեմ numbers դինամիկ զանգվածը, որի ընթացիկ չափը ցույց է տալու size ամբողջ թիվը։ Սկզբի համար size փոփոխականին կտամ 8 արժեքը։

int size = 8;
double* numbers = calloc( size, sizeof(double) );

calloc ֆունկցիայի առաջին արգումենտը տարրերի քանակն է, իսկ երկրորդը՝ մեկ տարրի չափը։ Ֆունկցիան վերադարձնում է զրոներով արժեքավորված հիշողության տիրույթ՝ տրված քանակով և տրված չափի տարրերի համար։

Հետո ուզում եմ կազմակերպել նախապայմանով ցիկլ, որը կկարդա թվերը, քանի դեռ չի հանդիպել EOF կոդը։ C լեզվում նախապայմանով ցիկլը կազմակերպվում է while հրամանով։

while( ⟨կրկնման պայման⟩ )
  ⟨մարմին⟩

Քանի դեռ ճշմարիտ է ⟨կրկնման պայման⟩-ը կատարվում են ⟨մարմին⟩ բլոկի հրամանները։

Հետո while հրամանով կազմակերպեմ մի անվերջ ցիկլ, որի մարմնում կարդալու եմ թվերը։ Այդ անվերջ ցիկլն ավարտվելու է այն դեպքում, երբ scanf ֆունկցիան վերադարձնում է EOF (end of file) արժեքը։

int count = 0; /* ներմուծված թվերի հաշվիչ */
while( EOF != scanf( "%lf", &numbers[count] ) )
  if( ++count == size ) {
    size *= 2;
    numbers = realloc( numbers, size * sizeof(double) );
  }

Ցիկլի հերթական իտերացիայում, count փոփոխականի արժեքը համեմատվում է զանգվածի ընթացիկ չափի հետ։ Եթե դրանք հավասար են, ուրեմն զանգվածի բոլոր դիրքերը զբաղված են և պետք է մեծացնել նրա չափը։ size *= 2 արտահայտությամբ, որը համարժեք է size = size * 2 արտահայտությանը, ես կրկնապատկում եմ զանգվածի ընթացիկ չափը որոշող փոփոխականի արժեքը։ Իսկ numbers զանգվածը ընդլայնում եմ realloc ֆունկցիայով։ Վերջինս ընդլայնում է malloc, calloc կամ realloc ֆունկցիաներով առանձնացված հիշողության տիրույթը՝ պահպանելով տվյալները։ Այն իր արգումենտում ստանում է ընդլայնվելիք հիշողության հասցեն և նոր չափը։

Երբ ներմուծման ցիկլը ավարտվում է (Linux համակարգրում EOF սիմվոլը ներմուծվում է ստեղնների Ctrl+D համադրմամբ), մեկ այլ while ցիկլով արտածում եմ numbers զանգվածի պարունակությունը։

while( --count >= 0 )
  printf( "%lf\n", numbers[count] );

Ամենավերջում պետք է ազատել ծրագրի աշխատանքի ընթացքում համակարգից վերցրած դինամիկ հիշողությունը։ free ֆունկցիան արգումենտում ստանում malloc, calloc կամ realloc ֆունկցիաներով առանձնացված հիշողության տիրույթի հասցեն և ազատում ու համակարգին է վերադարձնում այդ տիրույթը։

free( numbers );

Նկարագրված ծրագիրը գրառում եմ prog07a.c ֆայլում, կումպիլյացնում եմ ու գործարկում։ Հետո ներմուծում եմ 10 տարբեր թվեր ու սեղմում եմ Ctrl+D, որ նշանակում է ներմուծման ավարտ (EOF կոդն է)։ Ծրագիրն անմիջապես հակառակ կարգով արտածում է ներմուծված թվերը։

Որպեսզի համոզվեմ, որ free ֆունկցիան իսկապես համակարգին է վերադարձրել calloc և realloc ֆունկցիաներով ստացված հիշողության տիրույթը, կօգտագործեմ Valgrind ծրագիրը։ Ի թիվս այլ բաների, Valgrind-ը հնարավորություն է տալիս բացահայտել հիշողության կորուստները։

valgrind ./prog07a
...

Բացի calloc և realloc ֆունկցիաներից հիշողության նոր տիրույթ կարելի է առանձնացնել նաև malloc ֆունկցիայով։ Այն վերադարձնում է հիշողության տիրույթ, որի չափը ստանում է իր միակ արգումենտով։ Միակ տարբերությունն այն է, որ malloc ֆունկցիան զրոներով չի արժեքավորում առանձնացված տիրույթը։ Վերը բերված օրինակում calloc ֆունկցիայի կիրառությունը կարելի է փոխարինել malloc ֆունկցիայի հետևյալ կիրառությամբ.

double* numbers = malloc( size * sizeof(double) );

malloc ֆունկցիան ավելի հաճախ օգտագործվում է ստրուկտուրաների նմուշների դինամիկ ստեղծման համար։ Օրինակ, ենթադենք իրական թվեր պարունակող որոնման բինար ծառի հանգույցը սահմանված է որպես node ստրուկտուրա.

/* որոնման բինար ծառի հանգույց */
struct node {
  double data;        /* արժեքի դաշտ */
  struct node* left;  /* ձախ ենթածառ */
  struct node* right; /* աջ ենթածառ */
};

Սահմանեմ create_node ֆունկցիան, որը ստեղծում է նոր հանգույց, դրա data դաշտում գրում է տրված արժեքը և վերադարձնում է այդ հանգույցի ցուցիչը։

struct node* create_node( double val )
{
  struct node* no = malloc(sizeof(struct node));
  no->data = val; no->left = NULL; no->right = NULL;
  return no;
}

Եթե ինչ-որ պատճառով ձախողվում է պահանջվող չափի հիշողության տիրույթի առանձնացումը, ապա malloc, calloc և realloc ֆունկցիաները վերադարձնում են NULL արժեքը։ Եվ այս փաստը պետք է հաշվի առնել ու ծրագրերը լրացել համապատասխան ստուգումներով։ Օրինակ.

double* numbers = calloc( size, sizeof(double) );
if( numbers == NULL ) {
  puts( "Հնարավոր չէ հատկացնել պահանջվող հիշողությունը։" );
  exit( 1 );
}