از مصائب ورودی گرفتنِ رشته در C

زبان C خوبی‌های مختلفی داره، ولی به نظر من کار با رشته‌ٔ آسان جزوشان نیست. سی زبان سطح پایین و محبوبیست که بسیاری از برنامه‌های گنو/لینوکسی با آن نوشته شدند، سیستم‌عامل‌ها مدیون این زبان قدرتمند هستند و بسیاری از حرفه‌ای‌ها یادگیری سی را به عنوان اولین زبان شروع برنامه‌نویسی به تازه‌کارها پیشنهاد می‌دهند.

زبان سی، تایپ مستقل برای رشته ندارد و برای هندل کردن رشته، و برنامه‌نویسان سی برای اینکار از پوینتر به کاراکتر استفاده می‌کند. در صورتی که با char* و 0\ آخر رشته (در سی) آشنایی ندارید، توصیه می‌شود این لینک را مطالعه کنید چون در ادامه بحث به آن‌ها نیاز داریم.




و اما ورودی گرفتن

در سی ما از scanf برای ورودی گرفتن استفاده می‌کنیم، با استفاده از % مشخص می‌کنیم که چه مدل متغیری را می‌خواهیم از ورودی بخوانیم مثلا برای عدد دسیمال خواندن، d% می‌گذاریم. برای نمونه:

int num;                                                                
scanf("%d",&num);

برای ورودی گرفتن تایپ‌های مخلتف d% را با حرف‌های مختلفی جایگزین می‌کنیم مثلا f و lf برای ممیز شناور و c برای کاراکتر و نهایتا s% برای رشته. (برای آموزش ورودی گرفتن در سی این مطلب را بخوانید)

مثال ورودی گرفتن رشته:

char* buff = (char*) malloc(10*sizeof(char) );                          
scanf("%s", buff);                                                      
printf("you enterred <<%s>>\n",buff);

در این مثال یک رشته به طول حداکثر ۹ را به درستی ورودی می‌گیریم و در buff ذخیره می‌کنیم و نهایتا چاپ می‌کنیم. اما اینجاست که مشکل شروع می شود. چه تضمینی وجود دارد که طول رشته کم‌تر از ۱۰ باشد؟ اگر کاربر تصمیم گرفت که ۲۰ کاراکتر وارد کند تکلیف برنامه ما چیست؟ خب در اینجا buffer پر می‌شود و بیش‌تر هم توی مموری نوشته می‌شود و برنامه undefined behavior نشان می‌دهد. ممکن است سیستم عامل برنامه را ببندد یا صرفا آن خط برنامه اجرا نشود. در سیستم‌عامل‌های مختلف اتفاقات متفاوتی می‌افتد. نقل قول معروفی وجود دارد که می‌گوید

عملکرد درست برنامه را به قضا و قدر نسپارید.


باید همواره فکر همه حالات را بکنیم. مثلا چه می‌تواند کرد؟ می‌توانیم buffer را اینقدر بزرگ بسازیم که مطمئن شویم در عمر یک موجود زنده امکان ندارد اینقدر کاراکتر وارد شود! ولی اینکار از نظر مموری بهینه نیست و اصلا عقلی هم نیست.

اینجاست که باید از قابلیت‌های دیگر زبان سی استفاده کنیم.




آشنایی با gets

با scanf توانستیم یک رشته که شامل فاصله نبود را ورودی بگیریم. مثلا اگر کاربر وارد کند: salam roozbeh برنامه فقط کلمه اول یعنی salam را ورودی می‌گیرد. اگر بخواهیم تا انتهای خط را ورودی بگیریم به ما gets پیشنهاد می‌شود. (البته الان دیگر پیشنهاد نمی‌شود که در ادامه می‌بینیم چرا!)

برای استفاده از gets می‌توانستیم از کدی مشابه زیر استفاده کنیم:

char buf[MAX]; // max is defined as length of array                                                                                                                                
printf("Enter a string: ");     
gets(buf);                                                
printf("string is: %s\n", buf);

با اینکار رشته ورودی هرچقدر باشد را ورودی می‌گیرد و در buf می‌ریزد و به سرریز هم اهمیتی نمی‌دهد.

چرا گفتم "می‌توانستیم"؟ چون به دلیل مشکلات فراوانی که به وجود می‌آورد در نسخه‌های جدید سی حذف شده‌است!

راه‌حل: fgets

خدا گر ز حکمت ببندد دری، ز رحمت گشاید در دیگری! بالاخره وقتی gets حذف شده باید جایگزین امنی برایش وجود داشته باشد. جایگزین امن gets، تابع fgets است که با یک تغییر، از buffer overflow جلوگیری می‌کند. این تابع علاوه بر گرفتن آرایه‌ٔ مقصد، فایل ورودی (مثلا stdin) و از همه مهم‌تر، حداکثر طول مجاز برای خواندن از ورودی و نوشتن در بافر را می‌گیرد. مثلا:

char buf[MAX];                                                              
fgets(buf, MAX, stdin);                                                     
printf("string is: %s\n", buf);

این تابع امن است!

برای مطالعه‌ٔ بیش‌تر در زمینه gets و fgets از این لینک استفاده کنید.


بهترین‌راه: getline

اما آخرین راه ورودی گرفتن استفاده از getline است. خوبی این تابع این است که در صورتی که فضای کافی در خانه مربوطه وجود نداشته باشد به کمک realloc فضای بیشتری اختصاص می‌دهد و امن است.

با کدی مشابه زیر می‌توانیم یک خط را ورودی بگیریم:

int bytes_read;
unsigned long int size = 10;
char *string;
printf ("Please enter a string: ");
string = (char *) malloc (size);
bytes_read = getline (&string, &size, stdin);
if (bytes_read == -1)
      puts ("ERROR!");
else{
     puts ("You entered the following string: ");
     puts (string);
     printf ("\nCurrent size for string block: %d", bytes_read);
}


منابع

https://www.programiz.com/c-programming/c-strings

قسمت How to read a line of text



https://stackoverflow.com/questions/1621394/how-to-prevent-scanf-causing-a-buffer-overflow-in-c


https://www.geeksforgeeks.org/fgets-gets-c-language/


https://www.studymite.com/blog/strings-in-c