چگونه یک تابع main خوب در زبان برنامه نویسی c ایجاد کنیم؟

تابع main در زبان برنامه نویسی c یک تابع بسیار مهم است، در این مطلب درباره تابع Main در زبان برنامه نویسی قدرتمند سی صحبت خواهیم کرد.

 چگونه یک تابع main خوب در زبان برنامه نویسی c ایجاد کنیم؟

قبل از آن که بخواهیم درباره نوشتن یک تابع main خوب در سی صحبت کنیم بهتر است مقدمه ای را درباره این موضوع بیاوریم و سپس درباره تابع main و سایر بخش های فایلی که تابع main در آن قرار دارد صحبت کنیم.

من می دانم که پایتون و جاوا اسکریپت زبان هایی هستند که امروزه بسیار مورد استفاده قرار می گیرند ولی آن ها را نمی توان از لحاظ سازگاری و سرعت با زبان برنامه نویسی سی مقایسه کرد، این زبان یک زبان قدرتمند و مختصر می باشد که برای افرادی که نیازمند سرعت بالا هستند می تواند بسیار مفید باشد، اگر شما به دنبال امنیت شغلی خود در آینده هستید و قصد دارید برنامه نویسی را از صفر شروع کنید یادگیری این زبان می تواند برای شما مفید باشد. من در این مقاله قصد دارم به شما آموزش دهم که چگونه یک فایل C و تابع main آن را ساختار بندی کنید و یک تابع main مناسب را برای برنامه خود بنویسید.
یک برنامه خسته کننده ولی صحیح در C به همراه تابع main

یک برنامه که در سی نوشته شده است با تابع main کار خود را شروع می کند که معمولا تابع main در یک فایل با نام main.c نگهداری می شود:

/* main.c */

int main(int argc, char *argv[]) {


}



این برنامه ساده از شروع تابع main آن کامپایل می شود اما کار خاصی را انجام نمی دهد:


$ gcc main.c

$ ./a.out -o foo -vv

$

 تابع main


تابع main یکتا است

تابع main اولین تابع از برنامه شما است که زمانی که برنامه شما شروع به اجرا شدن می کند اجرا می شود، اولین تابع تابع ()start می باشد که معمولا توسط کتابخانه ران تایم سی فراهم می شود و زمانی که برنامه شما شروع به کامپایل شدن می کند به برنامه متصل می شود. جزئیات این کار وابسته به کامپایلر و سیستم عامل شما دارد، به همین علت من قصد دارم در این مطلب از آن چشم پوشی کنم.

تابع main در C دارای دو آرگومان می باشد که به صورت سنتی آنها را argc و argv می نامند و همینطور تابع main یک عدد از نوع int را نیز باز می گرداند. اکثر محیط های یونیکس انتظار دارند که این تابع در صورت اجرا شدن به موفقیت عدد 0 و در غیر این صورت عدد 1- را باز گرداند.

Argc در تابع main در واقع مخفف argument count می باشد که طول آرایه آرگومان را باز می گرداند و همینطور argv نیز مخفف argument vector میباشد که آرایه ای از کاراکترهای پوینتر را باز می گرداند.

Argv در واقع یک بازنویسی نشانه گذاری شده از خط فرمان می باشد که برنامه شما را فراخوانی می کند. به عنوان مثال در مثال بالا argv شامل لیستی از رشته های زیر خواهد بود:

argv = [ "/path/to/a.out", "-o", "foo", "-vv" ];

بردار آرگومان یا همان argument vector به صورت تضمینی همواره حداقل شامل یک رشته می باشد که آن را با نام argv[0] در تابع main می شناسند.

 تابع main


آشنایی با ساختار یک تابع main در c

زمانی که من یک پروژه C را شروع می کنم ساختار آن به شکل زیر می باشد:

/* main.c */

/* 0 copyright/licensing */

/* 1 includes */

/* 2 defines */

/* 3 external declarations */

/* 4 typedefs */

/* 5 global variable declarations */

/* 6 function prototypes */


int main(int argc, char *argv[]) {

/* 7 command-line parsing */

}


/* 8 function declarations */

من درباره هر یک از این بخش ها به جز بخش صفرم به صورت مفصل صحبت خواهم کرد، اگر شما مجبور هستید که بخشی را به صورت کپی رایت در کد خود قرار دهید باید در این بخش ذکر کنید.

نکته دیگری که من درباره آن صحبت نخواهم کرد اضافه کردن کامنت ها به کد شما می باشد.

"Comments lie."

یک برنامه نویس خوب همواره باید کامنت ها را به کد خود اضافه کند.

به جای استفاده از کامنت شما می توانید نام های مناسبی را بر روی توابع و همینطور متغیرهای خود قرار دهید تا خواندن کد شما راحت تر شود.

 تابع main


یک نکته بسیار مهم

با توجه به تنبلی ذاتی برنامه نویسان شما با اضافه کردن کامنت ها می توانید میزان نگهداری از کد خود را دوبرابر کنید و در آینده ارتباط بهتری با آن برقرار کنید. توجه داشته باشید که اگر کد خود را تغییر دهید باید کامنت ها را نیز به روز رسانی کرده و یا گسترش دهید و هر کاری که در کد خود انجام می دهید را به صورت کامل توصیف کنید. اگر بخشی از کد خود را به صورت کامل توضیح دهید حتما این کار را انجام دهید، شاید این کار اندکی از شما وقت بگیرد اما توجه داشته باشید که در سال های بعد زمانی که قصد تغییر کد خود را داشتید می توانید به راحتی این کار را با مطالعه کامنت ها انجام دهید.

 تابع main


آشنایی با بخش includes در تابع main و ساختار برنامه c

اولین چیزی که من به فایلی که تابع main در آن قرار دارد یعنی فایل main.c اضافه می کنم عبارت است از include ها که برای کتابخانه های استاندارد c مورد استفاده قرار می گیرد و همینطور متغیرهایی که می خواهم از آنها در طول برنامه استفاده کنم. کتابخانه های استاندارد c عملیات های گوناگونی را برای شما انجام می دهند، فایل های هدر که در این بخش ها قرار می گیرند به شما توضیح می دهند که این کتابخانه ها چه کارهایی را می توانند برای شما انجام دهند.

رشته #include یک پیش پردازش در زبان برنامه نویسی c می باشد که باعث می شود تا فایل رجیستر به صورت کامل در فایل برنامه شما قرار گیرد، فایل های هدر در زبان برنامه نویسی سی معمولا با پسوند .h به نمایش گذاشته می شوند و نباید حاوی هیچ گونه کد اجرایی باشند. این فایل ها تنها شامل تعریف ها، نوع داده ها و متغیرهای اضافی می باشند، رشته <header.h> به پیش پردازش یا cpp زبان سی می گوید که به دنبال فایل هایی که در مسیر system-defined header دارای پسوند .h دارند باشد.

/* main.c */

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <libgen.h>

#include <errno.h>

#include <string.h>

#include <getopt.h>

#include <sys/types.h>

 تابع main


آشنایی با بخش Defines

/* main.c */

<...>


#define OPTSTR "vi:o:f:h"

#define USAGE_FMT  "%s [-v] [-f hex flag] [-i inputfile] [-o output file] [-h]"

#define ERR_FOPEN_INPUT  "fopen(input, r)"

#define ERR_FOPEN_OUTPUT "fopen(output, w)"

#define ERR_DO_THE_NEEDFUL "do_the_needful blew up"

#define DEFAULT_PROGNAME "george"

در حال حاضر این بخش خیلی مهم نیست اما تعریف OPTSTR بخشی است که برنامه اعلام می کند کدام خط فرمان تغییر می کند، USAGE_FMT یک تعریف در فرمت printf می باشد که امروزه از آن به جای تابع ()usage استفاده می کنند. علاوه بر این من دوست دارم که رشته های ثابت را به تحت عنوان تعریف در این بخش از کد بیاورم، جمع کردن تمامی این ثوابت در یک بخش باعث می شود تا در صورت تمایل به تغییر آنها بتوانید به راحتی این کار را انجام دهید.

نکته آخری که باید درباره این بخش بدانید این است که برای نام گذاری define# ها سعی کنید حتما از حروف بزرگ استفاده کنید تا تفاوت آنها با سایر متغیرها مشهود باشد. شما می توانید کلمات را در کنار یکدیگر استفاده کنید و در صورت لزوم می توانید آنها را با استفاده از یک _ از یکدیگر جدا کنید.

 تابع main


تعاریف اضافه

/* main.c */

<...>


extern int errno;

extern char *optarg;

extern int opterr, optind;

یک تعریف اضافه در واقع آن اسم را به namespace همان فایل وارد می کند و اجازه می دهد برنامه به صورت مستقیم به آن دسترسی داشته باشد، در این جا ما سه تعریف برای اعداد از نوع int و همینطور یک کاراکتر پویینتر آورده ایم، متغیرهای پیشفرض توسط تابع ()getopt استفاده می شوند و errno مورد استفاده قرار می گیرد تا به عنوان یک کانال ارتباطی بین کتابخانه های استاندارد زبان برنامه نویسی c تعیین کند که چرا یک تابع با شکست رو به رو می شود.

 تابع main


آشنایی با بخش Typedef

یکی دیگر از بخش های فایلی که تابع main در آن قرار دارد یعنی فایل main.c می باشد که به شکل زیر است:

/* main.c */

<...>


typedef struct {

int           verbose;

uint32_t      flags;

FILE         *input;

FILE         *output;

} options_t;

بعد از تعاریف اضافی من قصد دارم این بخش را تعریف کنم که برای ساختارها و شمارش ها به کار می رود، من به شدت ترجیح می دهم که به آن یک پسوند –t اضافه کنم تا نشان دهم که یک نوع داده می باشد. در این مثال من options-t را به عنوان یک ساختار تعریف کردم که شامل 4 عضو می باشد، زبان برنامه نویسی c یک زبان برنامه نویسی whitespace-neutral می باشد، بنابراین من از یک whitespace استفاده می کنم تا نام فیلد مورد نظر را در ستون مربوطه نمایش دهم. برای این که در تابع main نشان دهم که این یک پوینتر می باشد به آن یک ستاره نیز اضافه می کنم.

 تابع main


تعریف متغیرهای کلی

/* main.c */

<...>


int dumb_global_variable = -11;

متغیرهای کلی یک ایده بسیار بد هستند که شما نباید هرگز از آن ها استفاده کنید، اما اگر شما مجبور به استفاده از یک متغیر کلی هستید باید آن را در این قسمت تعریف کنید و مطمئن باشید که به آنها مقدار پیش فرض داده می شود، اما همواره این نکته را به خاطر داشته باشید که سعی کنید به هیچ وجه از متغیرهای کلی استفاده نکنید.

 تابع main

بخش Function prototypes

/* main.c */

<...>


void usage(char *progname, int opt);

int  do_the_needful(options_t *options);

زمانی که شما قصد دارید یک تابع بنویسید باید آن را بعد از تابع main اضافه کنید نه قبل از آن، کامپایلرهای اولیه زبان c از یک استراتژی single-pass استفاده می کردند که به این معنا است که هر نمادی که در برنامه مورد استفاده قرار می گیرد باید قبل از آن تعریف شده باشد. کامپایلرهای امروزی اما این چنین نیستند و قبل از انجام عملیات کامپایل یک مجموعه کامل از نمادهای استفاده شده در کد را ایجاد می کنند، بنابراین تعریف توابع بعد از تابع main هیچگونه مشکلی را ایجاد نمی کند.

 تابع main


تجزیه خط فرمان

/* main.c */

<...>


int main(int argc, char *argv[]) {

   int opt;

   options_t options = { 0, 0x0, stdin, stdout };


   opterr = 0;


   while ((opt = getopt(argc, argv, OPTSTR)) != EOF)

     switch(opt) {

         case 'i':

            if (!(options.input = fopen(optarg, "r")) ){

               perror(ERR_FOPEN_INPUT);

               exit(EXIT_FAILURE);

               /* NOTREACHED */

            }

            break;


         case 'o':

            if (!(options.output = fopen(optarg, "w")) ){

               perror(ERR_FOPEN_OUTPUT);

               exit(EXIT_FAILURE);

               /* NOTREACHED */

            }

            break;

            

         case 'f':

            options.flags = (uint32_t )strtoul(optarg, NULL, 16);

             break;


         case 'v':

            options.verbose += 1;

            break;


         case 'h':

         default:

            usage(basename(argv[0]), opt);

            /* NOTREACHED */

            break;

     }


   if (do_the_needful(&options) != EXIT_SUCCESS) {

     perror(ERR_DO_THE_NEEDFUL);

     exit(EXIT_FAILURE);

     /* NOTREACHED */

   }


   return EXIT_SUCCESS;

}

شاید این مثال کمی طولانی باشد، به صورت کلی می توان گفت هدف از تابع main جمع آوری آرگومان های مورد نیاز کاربر می باشد، اعتبار سنجی ورودی ها و ارسال این آرگومان ها به توابع مختلف که از این آرگومان ها استفاده می کنند هدف اصلی از نوشتن تابع main می باشد. این مثال یک متغیر را نشان می دهد که با مقدار پیش فرض مقداردهی شده است و خط فرمان را تجزیه می کند و در صورت نیاز گزینه ها را به روز رسانی می کند.

 تابع main


تمرکز اصلی  تابع main

تمرکز اصلی تابع main بر روی یک حلقه while می باشد که از ()getopt استفاده می کند و از طریق argv به دنبال گزینه های خط فرمان و آرگومان های آنها می باشد. تعریف OPTISTAR که در یک فایل است به عنوان یک قالب عمل می کند که رفتار تابع ()getopt را مدیریت می کند. متغیر opt یک کاراکتر را از هر خط فرمانی دریافت می کند و برنامه پاسخ مناسبی به تشخیص گزینه خط فرمان که در دستور سوییچ اتفاق افتاده است ، می دهد.

اگر شما به این مقاله توجه کرده باشید ممکن است این سوال برای شما پیش آمده باشد که چرا opt به عنوان یک عدد 32 بیتی تعریف شده است در حالی که انتظار می رفت به عنوان یک عدد 8 بیتی تعریف شود؟ به نظر می رسد که ()getopt یک متغیر int را باز می گرداند که زمانی که به انتهای argv می رسد مقدار آن منفی است.

 تابع main


نکاتی مهم درباره تابع main

زمانی که یک خط فرمان شناخته شده تشخیص داده می شود رفتار خاصی در قبال آن صورت می گیرد، بعضی از گزینه ها دارای یک آرگومان هستند که در OPTSTR شناخته می شود، زمانی که یک گزینه دارای یک آرگومان باشد string بعدی در argv با استفاده از متغیر اضافی opt برای برنامه در دسترس خواهد بود. من از optarg برای باز کردن فایل ها برای خواندن، نوشتن یا تبدیل یک آرگومان خط فرمان از یک string یا متغیر عددی int استفاده می کنم.

در این جا چند نکته برای استایل وجود دارد:

-          Opterr را با مقدار اولیه 0 مقداردهی کنید که از انتشار یک ؟  توسط getopt جلوگیری کند.

-          از exit(EXIT_FAILURE) یا exit(EXIT_SUCCESS) در وسط تابع main استفاده کنید.

-          /* NOTREACHED */ یک دستور کمکی است که می توانید از آن نیز استفاده کنید.

-          از دستور  EXIT_SUCCESS برای بازگشت در انتهای توابعی که مقدار Int باز می گردانند استفاده کنید.

-          عملیات تبدیل داده ها را به صورت صریح انجام دهید.

اگر برنامه با موفقیت کامپایل شود چیزی شبیه به کد زیر تولید می شود:

$ ./a.out -h

a.out [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]

تعریف توابع

یکی دیگر از نکاتی که در تابع main و یا هر تابع دیگری باید به آن توجه داشته باشید تعریف توابع می باشد.

/* main.c */

<...>


void usage(char *progname, int opt) {

 fprintf(stderr, USAGE_FMT, progname?progname:DEFAULT_PROGNAME);

 exit(EXIT_FAILURE);

 /* NOTREACHED */

}


int do_the_needful(options_t *options) {


 if (!options) {

   errno = EINVAL;

   return EXIT_FAILURE;

 }


 if (!options->input || !options->output) {

   errno = ENOENT;

   return EXIT_FAILURE;

 }


 /* XXX do needful stuff */


 return EXIT_SUCCESS;

}

در این مثال تابع ()do_the_needful یک پوینتر را در یک ساختار options_t structure دریافت می کند، من در تابع main تعیین کرده ام که این پوینتر نباید مقدار NULL بگیرد. اگر تست با موفقیت صورت نگیرد EXIT_FAILURE تابع را با تغییر مقدار متغیر کلی errno  به یک کد مربوط به ارور باز می گرداند، تابع ()perror می تواند توسط فراخوانی کننده مورد استفاده قرار گیرد تا پیغام های خطا را با زبان انسانی برای ما چاپ کند.

توجه داشته باشید که توابع از جمله تابع main همواره باید مقدار ورودی خود را اعتبار سنجی کنند، اگر اعتبارسنجی کامل هزینه بر است،  سعی کنید حداقل یک بار آن را انجام دهید و داده های معتبر را به عنوان داده های تغییر ناپذیر در نظر بگیرید. استفاده از تابع اعتبارسنجی ()usage می تواند آرگومان را با یک تخصیص شرطی در فراخوانی ()fprintf اعتبار سنجی کند.

کلاس بزرگی از ارورها که من در این جا سعی کرده ام از آنها جلوگیری کنم استفاده از پوینتر با مقدار اولیه Null می باشد، استفاده از این پوینتر باعث می شود تا سیستم عامل یک سینگال ویژه برای پردازش من ارسال کند که نام این سیگنال SYSSEGV و باعث نابودی و اتمام برنامه می شود. آخرین چیزی که کاربران می خواهند در برنامه شما ببینند اتمام برنامه با استفاده از سیگنال SYSSEGV می باشد. بنابراین بهتر است که شما یک مدیریت کننده برای قسمت Null بودن پوینتر یا در خود تابع یا در تابع main در نظر بگیرید.

 تابع main


نکاتی مهم درباره توابع و همینطور تابع main

برخی از افراد ممکن است درباره این که یک تابع دارای چند دستور بازگشتی باشد اعتراض داشته باشند، آنها درباره این اعتراض خود بحث تداوم جریان های کنترلی را بیان می کنند. اگر بخواهم با شما صادق باشم باید بگویم که زمانی که یک دستور در وسط تابع main را هر تابع دیگری دچار ارور می شود بهتر است که دستور بازگشتی در همان مرحله تابع را به اتمام برساند، نوشتن دستورهای if مختلف برای تابع main یا هر تابع دیگری فقط برای یک دستور بازگشتی به هیچ وجه نمی تواند ایده مناسبی باشد.

در انتها باید گفت که اگر شما یک تابع نوشته اید که دارای 4 آرگومان و یا بیشتر است سعی کنید آرگومان ها را در یک ساختار قرار دهید و سپس آن را به تابع ارسال کنید. این کار باعث می شود تا کار کردن با تابع آسان تر شود.

 تابع main


صبر کنید، شما گفتید از کامنت استفاده نکنیم؟

در تابع () do_the_needful نوع خاصی از کامنت را ایجاد کردیم که در واقع توضیحی برای کد نوشته شده نیست.

/* XXX do needful stuff */

زمانی که شما دوباره به این بخش از کد مراجعه می کنید علاقه مند هستید که بخش هایی از آن را دوباره بنویسید، من در این جا xxx قرار داده ام و بعد از آن کاری که باید در این جا انجام شود را نوشته ام، نکته بسیار مهمی که باید در این روش به آن دقت داشته باشید این است که به هیچ وجه از آن در کانتکس های دیگر برنامه خود و یا به عنوان نام متغیر و یا سایر تابع ها استفاده نکنید که این کار می تواند برای شما خساراتی را در کدتان به ارمغان بیاورد.

 تابع main


جمع آوری تمامی مطالبی که درباره تابع main و سایر بخش ها بیان کردیم

این برنامه را اگر اجرا و کامپایل کنید با وجود دارا بودن تابع main هیچ گونه کاری را انجام نمی دهد، اما شما حالا می توانید ساختار تجزیه و تحلیل خط فرمان برنامه های نوشته شده به زبان برنامه نویسی c را خودتان طراحی کنید:

/* main.c - the complete listing */


#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <libgen.h>

#include <errno.h>

#include <string.h>

#include <getopt.h>


#define OPTSTR "vi:o:f:h"

#define USAGE_FMT  "%s [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]"

#define ERR_FOPEN_INPUT  "fopen(input, r)"

#define ERR_FOPEN_OUTPUT "fopen(output, w)"

#define ERR_DO_THE_NEEDFUL "do_the_needful blew up"

#define DEFAULT_PROGNAME "george"


extern int errno;

extern char *optarg;

extern int opterr, optind;


typedef struct {

int           verbose;

uint32_t      flags;

FILE         *input;

FILE         *output;

} options_t;


int dumb_global_variable = -11;


void usage(char *progname, int opt);

int  do_the_needful(options_t *options);


int main(int argc, char *argv[]) {

   int opt;

   options_t options = { 0, 0x0, stdin, stdout };


   opterr = 0;


   while ((opt = getopt(argc, argv, OPTSTR)) != EOF)

     switch(opt) {

         case 'i':

            if (!(options.input = fopen(optarg, "r")) ){

               perror(ERR_FOPEN_INPUT);

               exit(EXIT_FAILURE);

               /* NOTREACHED */

            }

            break;


         case 'o':

            if (!(options.output = fopen(optarg, "w")) ){

               perror(ERR_FOPEN_OUTPUT);

               exit(EXIT_FAILURE);

               /* NOTREACHED */

            }

            break;

            

         case 'f':

            options.flags = (uint32_t )strtoul(optarg, NULL, 16);

            break;


         case 'v':

            options.verbose += 1;

            break;


         case 'h':

         default:

            usage(basename(argv[0]), opt);

            /* NOTREACHED */

            break;

     }


   if (do_the_needful(&options) != EXIT_SUCCESS) {

     perror(ERR_DO_THE_NEEDFUL);

     exit(EXIT_FAILURE);

     /* NOTREACHED */

   }


   return EXIT_SUCCESS;

}


void usage(char *progname, int opt) {

 fprintf(stderr, USAGE_FMT, progname?progname:DEFAULT_PROGNAME);

 exit(EXIT_FAILURE);

 /* NOTREACHED */

}


int do_the_needful(options_t *options) {


 if (!options) {

   errno = EINVAL;

   return EXIT_FAILURE;

 }


 if (!options->input || !options->output) {

   errno = ENOENT;

   return EXIT_FAILURE;

 }


 /* XXX do needful stuff */


 return EXIT_SUCCESS;

}