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

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

Իմ երրորդ զրույցը նվիրված է C ծրագրավորման լեզվի ֆունկցիաներին։ Այս անգամ ես կպատմեմ նոր ֆունկցիաների հայտարարման ու սահմանման մասին, ֆունկցիայի արգումենտների ու վերադարձրած արժեքի մասին։

Ինչպես և մաթեմատիկայում, որտեղից եկել է ֆունկցիա տերմինը, C լեզվում էլ ֆունկցիաները նախատեսված են տվյալների մի բազմությունը մեկ այլ բազմության արտապատկերելու համար։ Օրինակ, նախորդ զրույցում հիշատակված sqrt ֆունկցիան արգումենտում սպասում է double տիպի արժեք և վերադարձնում է այդ արժեքի երկրորդ աստիճանի արմատը՝ նորից double տիպի արժեք։ Կարող եմ ասել, որ sqrt ֆունկցիան double արժեքների բազմությունն արտապատկերում է նույն double արժեքների բազմությանը։ Մաթեմատիկորեն այդ փաստը սովորաբար գրառվում է հետևյալ կերպ․

sqrt : double ↦ double

Իսկ C լեզվով այս նույն հայտարարությունը գրառվում է այսպես․

double sqrt( double );

Մեկ այլ օրինակ է նորից նախորդ զրույցում հիշատակված atan2 ֆունկցիան։ Այն արգումենտում սպասում է երկու double թվեր՝ կետի կոորդինատները և վերադարձնում է աբսցիսների առանցքի և կոորդինատների սկզբնակետով ու տրված կետով անցնող ուղղի կազմած անկյունը՝ նորից double թիվ՝ (−π, π] միջակայքից։ Մաթեմատիկական գրառումը հետևյալն է․

atan2 : double ⨯ double ↦ double

C լեզվով գրառումն էլ այսպիսինն է․

double atan2( double, double );

Եվ այսպես, C ծրագրավորման լեզվում ֆունկցիայի հայտարարությունն ունի երեք բաղադրիչ. վերադարձվող արժեքի տիպ, ֆունկցիայի անուն և պարամետրերի տիպերի ցուցակ։ Վերադարձվող արժեքի տիպը և պարամետրերի տիպն իրար հետ կոչվում է ֆունկցիայի տիպ։

Երբեմն ֆունկցիայի տիպ են անվանում վրա վերադարձրած արժեքի տիպը։ Բնականաբար դա սխալ է, որովհետև միայն վերադարձվող արժեքի տիպը չի կարող միարժեքորեն բնորոշել ֆունկցիան։

Ինչպես փոփոխականների դեպքում, այնպես էլ ֆունկցիաներն է պետք օգտագործումից առաջ հայտարարել։ Բայց C լեզվում հայտարարելուց բացի պետք է նաև սահմանել ֆունկցիաների վարքը՝ տալ այն գործողությունները, որ ֆունկցիան կատարում է որոշման տիրույթն արժեքների տիրույթին արտապատկերելու համար։ Ֆունկցիայի վարքը սահմանող բլոկը կոչվում է ֆունկցիայի մարմին և որոշվում է վերնագրից հետո գրված ու {, } փակագծերի մեջ առնված հրամանների հաջորդականությամբ։ Օրինակ, նախորդ զրույցում դեկարտյան կոորդինատներից բևեռային կոորդինատները ստանալու համար օգտագործեցի երկու բանաձև՝ կետի շառավիղը և ազիմուտը հաշվող կանոնները։ Կարող եմ սահմանել, օրինակ, radius ֆունկցիան հետևյալ կերպ․

/* շառավղի հաշվումը դեկարտյան կոորդինատներով */
double radius( double x, double y )
{
  return sqrt( x * x + y * y );
}

C լեզվի return հրամանը ֆունկցիայից արժեք վերադարձնող գործողությունն է։ Այն իր կիրառման կետում ավարտում է ֆունկցիայի կատարումը և ծրագրի ղեկավարումը փոխանցում է տվյալ ֆունկցիայի կանչի կետին հաջորդող հրամանին։ Յուրաքանչյուր ֆունկցիա պարտավոր է ունենալ գոնե մեկ return հրաման և այդ հրամանի արգումենտն էլ հենց ֆունկցիայի արժեքն է։

radius ֆունկցիայի պես կարող եմ սահմանել նաև azimuth ֆունկցիան․

/* ազիմուտի հաշվումը դեկարտյան կոորդինատներով */
double azimuth( double x, double y )
{
  return atan2( y, x );
}

Այս օրինակներից երևում է, որ, ի տարբերություն ֆունկցիայի հայտարարության, ֆունկցիայի սահմանման ժամանակ վերնագրում պետք է գրել ոչ թե պարամետրերի տիպերի ցուցակը, այլ պարամետրերի հայտարարությունների ցուցակը։ Սակայն, եթե ծրագրում x, y և z փոփոխականները թույլատրելի է հայտարարել մեկ հրամանով՝

float x, y, z;

ապա ֆունկցիայի սահմանման ժամանակ այդպիսի բան գրել չի կարելի․

? int f( int x, y, z )
? {
?   return x + y + z;
? }

Ամեն մի պարամետրի անունից առաջ պետք է գրել իր տիպը։ Այսպես․

int f( int x, int y, int z )
{
  return x + y + z;
}

Կարծում եմ արդեն պարզ է, որ C լեզվի ֆունկցիաները հնարավորություն են տալիս ամբողջությամբ մոդելավորել ֆունկցիայի մաթեմատիկական գաղափարը։ Բայց ծրագրավորման գործում հաճախ հանդիպում են դեպքեր, երբ մեզ չի հետաքրքրում ֆունկցիայի վերադարձրած արժեքը, կամ ֆունկցիան ընդհանրապես վերադարձվող արժեք չունի։ Այդ դեպքերում C լեզուն առաջարկում է ֆունկցիայի վերադարձվող արժեքի տիպը նշել void ծառայողական բառով։ Օրինակ, կարող եմ սահամանել մի ֆունկցիա, որն stdout հոսքին է արտածում իմ անունը.

void my_name()
{
  puts( "Ընկ. Ա. Բադալյան" );
}

Այս ֆունկցիան արգումենտներ չի ստանում և արժեք չի վերադարձնում. այն պարզապես կատարում է նախապես որոշված ինչ-որ գործ։ Հաճախ void վերադարձրած արժեքի տիպ ունեցող ֆունկցիային անվանում են պրոցեդուրա։

scanf ֆունկցիայի մասին պատմելիս ասացի, որ այն, ըստ իր առաջին արգումենտում տրված ֆորմատի, կարդում է արժեքներ և գրում է ֆորմատավորման հրահանգներին համապատասխան երկրորդ և հաջորդ արգումենտներով տրված հասցեներում։ Հիմա ուզում եմ պատմել, թե ինչպես են օգտագործվում փոփոխականների հասցեները։

Եթե ուզում եմ ծրագրում հայտարարել, որ պատրաստվում եմ a0 փոփոխականին վերագրել ամբողջ թվեր, իսկ a1 փոփոխականին՝ նիշեր, ապա պետք է գրեմ.

int a0 = 777;
char a1 = 'A';

Եթե ուզում եմ հայտարարել, որ p0 փոփոխականին վերագրելու եմ ամբողջաթիվ փոփոխականի հասցե, իսկ p1 փոփոխականին՝ նիշային փոփոխականի հասցե, ապա պետք է գրեմ․

int* p0 = &a0;   /* p0 ցուցիչը արժեքավորել a0-ի հասցեով */
char* p1 = &a1;  /* p1 ցուցիչը արժեքավորել a1-ի հասցեով */

Փոփոխականի հայտարարման այս նոր գրելաձևը, երբ տիպի անունից հետո գրված է * նիշը, հենց ցուցիչ փոփոխականի հայտարարման ձևն է (իսկ & գործողությունը փոփոխականի հասցեն վերցնելու գործողությունն է)։

Հիմա կարող եմ a0 և a1 փոփոխականներին արժեք վերագրել ոչ թե ուղղակիորեն, այլ նրանց հասցեի միջոցով։ Օրինակ այսպես․

*p0 = 888;  /* a0-ին վերագրել 888 */
*p1 = 'B';  /* a1-ին վերագրել 'B' */

Այս դեպքում արդեն * գործողությունը կոչվում է ապահասցեավորման գործողություն (ի հակադրություն & գործողության, որը հասցեավորման գործողությունն է)։

Հիմա, երբ արդեն գիտեմ, որ կարող եմ ֆունկցիային տալ փոփոխականի հասցեն և ֆունկցիան էլ կարող է իր հաշվարկած արժեքները գրել այս հասցեում, կսահմանեմ մի նոր ֆունկցիա, որը, ստանալով կետի դեկարտյան կոորդինատները, վերադարձնում է նույն այդ կետի բևեռային կոորդինատները։ Բայց վերադարձնում է ոչ թե որպես ֆունկցիայի արժեք, այլ հաշվարկած արժեքները գրում է տրված հասցեներում։

void polar( double x, double y, double* rho, double* phi )
{
  *rho = sqrt( x * x + y * y );
  *phi = atan2( y, x );
}

polar ֆունկցիան ստանում է երկու իրական թիվ, որոնք ֆունկցիայի արգումենտներն են, և իրական թվերի երկու ցուցիչներ, որոնք, ըստ էության, ֆունկցիայի վերադարձվող արժեքներն են։ Այս նոր ֆունկցիան օգտագործվում է ճիշտ այնպես, ինչպես օգտագործվում էր scanf ֆունկցիան՝ ստեղնաշարից կոորդինատների կարդալու ժամանակ․

double x = 0.0, y = 0.0;
scanf( "%lf,%lf", &x, &y );

double rho = 0.0, phi = 0.0;
polar( x, y, &rho, &phi );

Քանի որ polar ֆունկցիան արժեքները վերադարձնում է որպես ցուցիչ տրված արգումենտների միջոցով, պետք է սահմանման մեջ նշել, որ այս ֆունկցիան սովորական իմաստով վերադարձվող արժեք չունի և չի օգտագործելու return հրամանը։ Ես դա արել եմ ֆունկցիայի տիպը նշելով որպես void։

Ծրագրի առանձին, տրամաբանորեն անկախ հատվածները ֆունկցիաների տեսքով կազմակերպելով կարծես թե ստեղծում ենք խաղալիք կոնստրուկտորի մասնիկները, որոնցից հետո հավաքելու ենք պետք եղած օբյեկտը՝ ծրագիրը։ Բացի այդ, հնարավորություն ենք ստեղծում առանձին֊առանձին թեստավորել այդ հատվածները։

Ծրագրավորման C լեզուն թույլ է տալիս սահմանել ֆունկցիաներ՝ առանց նշելու դրանց պարամետրերի ճիշտ քանակը։ Այդպիսի ֆունկցիաների պարամետրերի ցուցակը պետք է բաղկացած լինի գոնե մեկ հայտարարված պարամետրից և բազմակետերից։ Օրինակ, արդեն հիշատակված scanf և printf ֆունկցիաների հայտարարությունը մոտավորապես հետևյալն է․

int scanf( const char* format, ... );
int printf( const char* format, ... );

Ստանդարտ գրադարանի stdarg.h ֆայլում հայտարարված են բոլոր այն անհրաժեշտ միջոցները, որոնց օգնությամբ սահմանվում են վարիադիկ ֆունկցիաները։

Ֆունկցիաների մասին առայժմ այսքանը։ Բայց սա դեռ ամբողջը չէ։