فصل هفتم
مباحث تکمیلی
دامنه شناسهها
متغیرهای محلی متغیرهایی هستند که داخل یک بلوک (مثلاً بدنه یک تابع) تعریف میشوند و خارج از بلوکی که در آن واقعاند قابلدسترسی نیستند. همین قاعده در مورد ثابتها نیز درست است. بهطورکلی، هر بلوکی میتواند شامل تعریف متغیر و ثابت باشد. برای مثال دستور if زیر شامل بلوکی است که متغیر n در آن تعریف شده است.
نظیر هر متغیر محلی دیگر n نمیتواند توسط دستورالعملهای خارج از بلوک فوق که در آن تعریف شده است مورد دسترسی و استفاده واقع شود. اگر همه مکانهایی که از آنها شناسه قابلدسترسی مجاز است در نظر بگیریم، میتوانیم این مکانها را دامنه دید یا دامنه دسترسی و قلمرو یا بهطور خلاصه دامنه بنامیم.
C++ سه دسته دامنه برای شناسهها تعریف میکند:
1 . دامنه کلاس (Class Scope)
جزئیات بحث راجع به کلاسها و دامنه کلاس را به فصل 9 موکول میکنیم.
2 ـ دامنه محلی (Local Scope)
دامنه یک شناسه که داخل یک بلوک تعریف شده باشد و از نقطه تعریف تا پایان بلوک گسترش مییابد.
3 ـ دامنه سراسری (Global or file Scope)
دامنه یک شناسه که خارج همه توابع و کلاسها تعریفشده باشد و از نقطه تعریف تا پایان همه فایلهای برنامه را شامل میشود.
نام توابع دامنه سراسری دارد، وقتی تابعی تعریف شد، هر تابع دیگری در برنامه میتواند از آن تابع استفاده کند. در C++ مفهومی به نام تابع محلی وجود ندارد یعنی نمیتوان تعریف تابعی را در داخل تابعی دیگر محصور کرد.
متغیرهای سراسری و ثابتها، خارج همه توابع تعریف میشوند. در قطعه کد ذیل، gamma متغیری سراسری است و توابع SomeFunc, main میتوانند به آن دسترسی داشته باشند.
اگر تابعی شناسهای محلی هم نام با شناسه سراسری تعریف میکند، شناسه محلی در تابع اولویت بالاتری داشته و توسط تابع بکار گرفته میشود و از شناسه سراسری در محدوده تابع صرفنظر میشود. این اصل را پنهانسازی نام یا اولویت نام میخوانند.
اولویت نام (Name Precedence) اولویتی که شناسه محلی در تابع بر شناسه سراسری همنام دارد. این اولویت در کلیه ارجاعاتی که تابع انجام میدهد برقرار است. نام دیگر آن پنهانسازی نام است.
مثالی از اولویت نام در زیر آورده شده است.
در این مثال تابع SomeFunc به ثابت سراسری a و متغیرهای سراسری b و c دسترسی دارد و درعینحال متغیرهای محلی b و c را نیز تعریف کرده است. لذا خروجی چنین است.
در ارجاعات تابع، متغیر محلی b بر متغیر سراسری b اولویت دارد. پارامتر صوری c نیز دسترسی به متغیر سراسری c را مسدود کرده است. پارامتر صوری از این لحاظ نظیر متغیر محلی عمل میکند.
همانگونه که پیشازاین توضیح دادیم، متغیرهایی که داخل یک تابع تعریف میشوند با پایان اجرای آن تابع نابود میشوند و فضای حافظه آنها آزاد میشود. ممکن است در مواردی نیاز داشته باشیم که مقدار یک متغیر پس از اتمام تابع حفظ شود و در فراخوانیهای بعدی نیز مورداستفاده قرار گیرد. در چنین مواردی میتوان از متغیر استاتیک استفاده کرد. متغیر استاتیک متغیری است که حافظه اختصاص داده شده به آن تا پایان برنامه در اختیارش باقی میماند. همه متغیرهای سراسری بهصورت پیشفرض استاتیک هستند.
میتوان از کلمه رزرو شده static برای تعریف متغیرهای استاتیک استفاده کرد. در این حال عمر متغیر تا پایان برنامه ادامه مییابد و از یک فراخوانی به فراخوانی دیگر مقدار آن از میان نمیرود. به برنامه ذیل توجه کنید.
خروجی برنامه چنین است:
محدوده متغیرها
هر یک از نوع دادههای عددی، محدوده مشخصی از اعداد را میتوانند ذخیره کنند که اصطلاحاً بهعنوان دامنه اعداد یا بازه مجاز اعداد شناخته میشود. جدول 7-1 دامنه مقادیر نوع دادههای صحیح را نشان میدهد. یادآوری میشود که کلمه رزرو شده unsigned قبل از هر یک از نوعها معرف مجموعهای از اعداد غیر منفی است که از صفر شروع میشوند و تا حداکثر مقدار مجاز در ماشین ادامه مییابند.
جدول 7-1: دامنه مقادیر نوع دادههای صحیح
C++ فایل سرفصلی به نام climit.h فراهم کرده است که از روی آن میتوان حداکثر و حداقلهای فوق را برای هر ماشینی پیدا کرد. این فایل سرفصل ثابتهای CHAR_MAX و CHAR_MIN و SHRT_MAX و SHRT_MIN و INT_MIN و INT_MAX و LONG_MAX و LONG_ MIN را تعریف میکند. حداقل نوعهای unsigned صفر بوده و حداکثر آنها با UCHAR_MAX ، USHRT_MAX و UINT_MAX و ULONG_MAX تعریف شدهاند. برای اینکه این مقادیر مشخص را در ماشین خود بیابید، برنامه ذیل را بعد از تکمیل کردن اجرا کنید:
محدوده مقادیر برای دادههای اعشاری نیز در جدول 7-2 نشان داده شده است.
جدول 7-2: دامنه مقادیر نوع دادههای اعشاری
فایل سرفصل استاندارد cfloat.h ثابتهای FLT_MAX و FLT_MIN و DBL_MIN و LDBL_MAX و LDBL_MIN را در بر دارد. با نوشتن برنامه کوچکی میتوانید مقادیر ثابتهای فوق را برای ماشین خود چاپ کنید.
برخی عملگرهای دیگر C++
علاوه بر عملگرهایی که در فصل 2 معرفی شدند، مجموعه دیگری از عملگرها در C++ ارائه شده است که در جدول زیر نمایش داده شدهاند.
نوع ساده تعریفشده توسط کاربر
C++ امکان تعریف نوع داده را به کاربر میدهد. در این بخش نشان میدهیم که چگونه نوع ساده متعلق به خود را تعریف کنیم. برای این منظور از دستور typedef استفاده میکنیم. ساختار دستوری آن چنین است:
برای مثال فرض کنید میخواهیم نوع داده جدیدی را برای نمره تعریف کنیم. برای این منظور از typedef بهصورت زیر بهعنوان یک مترادف از double استفاده میکنیم.
درواقع typedef نوع داده جدیدی ایجاد نمیکند. بلکه مترادفی برای انواع داده موجود تعریف میکند. این امکان در برخی موارد بسیار مفید خواهد بود.
نوع داده مجموعه ترتیبی
C++ به کاربر این امکان را میدهد که نوع داده سادهای را بهصورت مجموعه با ذکر کلیه عناصر آن مجموعه تعریف کند. به مثال زیر توجه کنید:
نام این نوع داده جدید Days است که مقادیر آن داخل آکولاد لیست شدهاند. این نوع را لیست ترتیبی مینامند. عناصر آن به ترتیب لیست میشوند، یعنی SUN قبل MON و MON قبل TUE میآید و الیآخر، به عبارتی ترتیب آنها مهم است.
پشت پرده، لیستهای ترتیبی بهصورت خودکار با اعداد صحیح متناظر میشوند، اولین عضو 0 ، دومی 1 و سومی 2 و بقیه هم به همین ترتیب متناظر میشوند. تعریف فوق شبیه تعریف زیر است.
این امکان وجود دارد که مقادیر 0 و 1 و 2 و... مذکور را که بهخودیخود در نظر گرفته میشوند، عوض کرد مثلاً میتوان نوشت:
این نحوه استفاده بهندرت اتفاق میافتد.
اکنون میتوانیم از نوع داده Days که آن را تعریف کردیم متغیری به نام someDay تعریف کنیم:
ساختار دستوری enum چنین است:
و ساختار دستوری شمارنده چنین است:
که در آن ConstIntExpression یک عبارت صحیح یا یک ثابت بانام یا واقعی است. شناسههای مورداستفاده در شمارندهها از قوانین مربوط به شناسهها پیروی میکنند. مثلاً تعریف زیر:
درست نیست. زیرا شناسهها نمیتوانند با رقم شروع شوند. تعریف زیر:
مجاز نیست، زیرا شمارندهها شناسه نیستند. دو تعریف زیر را در نظر بگیرید:
این دو تعریف بهطور مستقل مجازند. ولی اگر باهم در نظر گرفته شوند، اولی مجاز است و دومی مجاز نیست. شناسههایی با یک دامنه باید یکتا باشند. CORN نمیتواند دو بار تعریف شود.
انتساب
فرض کنید به متغیر someDay که در بالا تعریف شد بخواهیم مقدار MON را نسبت دهیم مینویسیم:
توجه کنید که اگرچه میدانیم مقدارMON در لایههای زیرین 1 است، لیکن نمیتوانیم بنویسیم:
به این قانون توجه کنید: تغییر ضمنی نوع از شمارشی به صحیح درست است و برعکس آن درست نیست. بهعبارتدیگر کامپایلر برای دستور انتساب 1 به someDay خطا میدهد. زیرا تبدیل ضمنی صحیح به داده شمارشی تعریفنشده است.
افزایش
میتوانیم someDay را که اکنون با مقدار MON مقداردهی اولیه شده است افزایش دهیم.
توجه کنید که از تبدیل نوع صریح استفاده کردهایم. دلیل این کار آن است که عبارت زیر با پیام خطای کامپایلر مواجه میشود.
سمت راست عبارت فوق درست است. زیرا با تبدیل ضمنی شمارشی به صحیح someDay برابر 1 در نظر گرفته میشود و 1 به آن اضافه میشود. حاصل سمت راست 2 است، ولی به خاطر قانون انتساب نمیتوانیم آن را به someDay نسبت دهیم. برای انجام این کار از تبدیل صریح نوع استفاده میکنیم که در بالا بیان شد.
رکوردها
اگرچه آرایه ساختمان داده بسیار مفیدی هست، اما از آن تنها زمانی میتوان استفاده کرد که عناصر داده همگی از یک نوع باشند. در این فصل بخش نوع داده ساختیافته به نام رکورد را معرفی میکنیم که در آن لزومی ندارد همه عناصر از یک نوع داده باشند.
دستهبندی گروهی از دادههای مرتبط به هم، صرفنظر از نوع آنها، با رکوردها امکانپذیر است. هر عنصر در رکورد یک مشخصه (field) رکورد نامیده میشود و نام خاص خود را دارد. در C++ نوع داده رکورد یا ساختار غالباً طبق چهارچوب دستوری زیر تعریف میشود:
که در آن TypeName شناسهای برای نام¬گذاری رکورد است. بهعلاوه، از چارچوب دستوری ملاحظه میشود که کلمه رزرو شده struct که مخفف structure و به معنی ساختار است در C++ برای تعریف رکورد بکار میرود. کلمه رکورد و ساختار در بعضی از کتابها بجای هم بکار میروند. ازآنجاکه کلمه ساختار در علوم کامپیوتر مفاهیم دیگری نیز دارد، در اینجا برای رفع هرگونه ابهام به جای واژه ساختار از واژه رکورد یا کلمه رزرو شده struct استفاده میکنیم.
لیست اعضاء نظیر دنبالهای از تعریف متغیرها هستند. دقت کنید که تعریف struct معرفی یک نوع جدید است و باید متغیرها از این نوع تعریف شوند تا کامپایلر به آنها فضا اختصاص دهد. بهعنوانمثال دانشجویان یک کلاس را در نظر بگیرید. میخواهیم نام، نام خانوادگی، میانگین نمره برنامهنویسی، معدل نمره کوئیزها و نمره امتحان نهایی و میانگین نهایی نمره دانشجویان را در مقیاس A, B, C, D, F نگهداری کنیم:
توجه کنید، در این مثال، تعریف یک struct با علامت ; ویرگول نقطه پایان میپذیرد. تاکنون گفته بودیم که علامت ; را بعد از آکولاد سمت راست عبارت مرکب قرار ندهید. در اینجا لیست اعضاء در تعریف struct بهعنوان یک عبارت مرکب در نظر گرفته نمیشود. آکولادها جزئی از چهارچوب دستوری میباشند. تعریف struct، نظیر همه تعریفها در C++ باید به علامت ; ختم شود. نام عضوهای رکورد StudentRec به شرح زیر است:
توجه کنید هر نام عضو از یک نوع داده ساده است. firstName و lastName از نوع داده NameString هستند که خود آرایهای از کاراکترها است. gpa از نوع اعشاری است و سه متغیر دیگر از نوع int است و آخرین متغیر یعنی courseGrade از نوع داده شمارشی است که مقدار آن یکی از مقادیر F , D , C , B , A است. تا زمانی که متغیری از نوع StudentRec تعریف نشود به هیچیک از متغیرهای بالا محلی از حافظه اختصاص داده نخواهد شد. StudentRec صرفاً یک چهارچوب و الگو برای این رکورد است (شکل 7-1 را ببینید). متغیرهای FirstStudent و Student متغیرهای از نوع StudentRec میباشند.
عضوهای یک متغیر struct با دادن نام متغیر، سپس یک نقطه و در ادامه نام عضو، قابلدسترسی میباشند. این عبارت انتخابکننده عضو نامیده میشود. ساختار دستوری آن برابر است با:
همچنان که کروشهها ([ ]) برای انتخاب مؤلفههای خاص یک آرایه بکار میروند، علامت نقطه برای انتخاب مؤلفههای یک رکورد بکار میرود. برای دسترسی به میانگین نمره firstStudent مینویسیم:
برای دسترسی به نمرد امتحانی نهایی دانشجو، مینویسیم:
با عناصر یک رکورد که با انتخابکننده عضو در دسترس قرار میگیرند، همانگونه برخورد میشود که با هر متغیر دیگری رفتار میشود. بهعبارتدیگر عناصر یک رکورد میتوانند در عبارت انتساب و یا ارسال بهعنوان یک پارامتر به یک تابع مورداستفاده قرار گیرند. در شکل 7-2 متغیری به نام student از نوع رکورد فوقالذکر را نشان میدهد که با پردازشهایی که قبلاً صورت گرفته است مقادیری در عناصر رکورد ذخیره شدهاند.
برای اینکه نحوه استفاده از انتخابکننده عضو را ببینیم، فرض کنید برای متغیر student بخواهیم نمره برنامهنویسی و نمره کوئیز و نمره امتحان نهایی را با هم جمع کنیم و بر اساس آن یک نمره حرفی به دانشجوی فوق بدهیم. نمره امتحان نهایی از طریق صفحهکلید خوانده میشود ولی دو نمره دیگر قبلاً در حافظه نشانده شدهاند.
همچنان که با عملگر >> میتوان مقداری را در یک عنصر مشخص از آرایهای وارد کرد، میتوان مقداری را در یک عضو یک رکورد نیز وارد نمود. دستور
مقداری را از صفحهکلید خوانده و آن را در متغیر finalExam که عضوی از رکورد student است قرار میدهد. نظیر آرایهها که عمل خواندن جمعی همه عناصر آرایه بهیکباره امکانپذیر نیست. در مورد رکوردها نیز باید عمل خواندن برای تکتک اعضای رکورد بهطور جداگانه انجام شود.
دستورات پیشرفته شرطی و حلقه
در این بخش چهار دستور نهچندان اساسی جدید را معرفی مینماید که در بعضی حالات کار برنامهنویسی را سهولت میبخشند. دستور Switch نوشتن ساختارهای انتخابی چند شاخه را آسان میکند.دستور Do-While پیادهسازی انواع معینی از حلقهها را آسان میسازند. دو دستور Break و Continue نیز دستورهای کنترلی هستند که در حلقهها و انتخابها مورداستفاده قرار میگیرند و پیادهسازی بعضی از حالات را سادهتر میکنند.
این فصل پنج دستور نهچندان اساسی جدید را معرفی مینماید که در بعضی حالات کار برنامهنویسی را سهولت میبخشند. دستور Switch نوشتن ساختارهای انتخابی چند شاخه را آسان میکند.دستورهای For و Do-While پیادهسازی انواع معینی از حلقهها را آسان میسازند. دو دستور Break و Continue نیز دستورهای کنترلی هستند که در حلقهها و انتخابها مورداستفاده قرار میگیرند و پیادهسازی بعضی از حالات را سادهتر میکنند.
دستور Switch
دستور Switch یک ساختار کنترلی است که با آن تعداد دلخواه شرط را میتوان پیادهسازی کرد. بهعبارتدیگر، یک ساختار کنترلی برای شرط¬های چند شاخه است. دستور Switch شبیه دستورهای if تودرتو است. مقدار عبارت switch عبارتی که مقدار آن با برچسبی که به یکی از حالات زده شده است مقایسه میگردد و شاخهای را که کنترل به آن منتقل و اجرا میشود تعیین میکند. برای مثال به دستور ذیل نگاه کنید:
در این مثال عبارت switch ، letter است. معنی دستور بالا چنین است: اگر letter برابر 'X' باشد Statement1 را اجرا کن و از دستور switch خارج شو و با Statement5 ادامه بده. اگر letter برابر 'L' یا 'M' باشد Statement2 را اجرا کن و با Statement5 ادامه بده. اگر letter برابر 'S' باشد Statement3 را اجرا کن و با Statement5 ادامه بده. اگر letter هیچیک از کاراکترهای یاد شده نبود Statement4 را اجرا کن و با Statement5 ادامه بده. عبارت break سبب خروج بلافاصله از ساختار Switch میشود. بعداً بررسی خواهیم کرد که در صورت حذف break چه اتفاقی میافتد.
ساختار دستوری برای دستور Switch چنین است:
عبارت دستور switch ، IntergralExpression است که از نوع صحیح (char ، short ، int ، long ) یا enum است. برچسب اختیاری SwitchLabel قبل از یک دستور ظاهر میشود و ساختار دستوری آن چنین است:
ملاحظه میشود که برچسب دستور Switch ، case یا default است. عبارت ConstantExpression یک عبارت صحیح است که باید یک ثابت بانام یا یک مقدار صحیح باشد. مثالهای ذیل نمونههایی از عبارت ثابت فوق میباشند که در آنها CLASS_SIZE ثابت بانام از نوع صحیح است.
نوع داده عبارت ConstanExpression در صورت لزوم بهطور ضمنی تبدیل میشود تا با عبارت switch ازلحاظ نوع مطابقت پیدا کند.
در مثال ابتدای بحث که مقدار letter مورد آزمون واقع شد حالات مختلف عبارتند از:
همانطور که در مثال مشهود است چند دستور میتواند در یک حالت قرار بگیرد. ثابت هر case در هر دستور switch تنها یکبار میتواند ظاهر شود. بهعلاوه فقط یک برچسب default در دستور switch مجاز است.
جریان کنترل درون یک ساختار switch به شرح زیر است: نخست عبارت switch ارزیابی و محاسبه میشود. اگر این مقدار با یکی از ثابتها در برچسب case یکسان باشد. کنترل به دستور بعدازآن case منتقل میشود. ازآنجا کنترل بهطور متوالی دستورات را اجرا میکند تا به یک دستور break یا به انتهای دستور switch برسد. اگر عبارت switch با هیچیک از حالات یکسان نباشد کنترل در صورت وجود برچسب default به دستور بعدازآن منتقل میشود و در غیر این صورت کلیه دستورات داخل بدنه switch صرفنظر شده و کنترل به اولین دستور بعد از بدنه switch منتقل میشود.
دستور switch ذیل پیام مناسبی بر اساس نمره دانشجو چاپ میکند (grade از نوع char است)
توجه کنید که break آخر قسمت F لازم نیست. اما برنامهنویسان ترجیح میدهند که آن را بیاورند. یک دلیل برای این کار این است که در صورت وجود break ، درج یک case جدید در انتهای ساختار switch آسانتر میشود.
اگر grade هیچیک از کاراکترهای مجاز نباشد هیچیک از دستورات switch اجرا نمیشوند. بهتر است برای غیر از حالات 'A' و 'B' و 'C' و'D' و 'F' یک برچسب default در نظر بگیریم.
دستور Do-While
دستور Do-While ساختار کنترل حلقهای است که در آن شرط حلقه در پایان حلقه مورد آزمون و تست قرار میگیرد. این ساختار اجرای حداقل یک بار حلقه را تضمین میکند. ساختار دستوری این دستور چنین است:
توجه کنید که دستور Do-While به نقطه ویرگول ; ختم میشود. همچنان که در C++ معمول است. Statement میتواند یک دستور یا یک بلوک باشد.
اجرای دستورات بین do و while تا زمانی که در انتهای حلقه Expression غیر صفر (TRUE) باشد ادامه پیدا میکند.
ذیلاً دستورهای حلقه While و Do-While را مقایسه میکنیم:
بهعنوانمثال؛ فرض کنید برنامهای نیاز به خواندن سن شخصی بهصورت محاورهای داشته باشد. فرض برنامه بر این است که سن مثبت باشد. حلقههای ذیل این اطمینان را میدهند که مقدار ورودی قبل از آنکه برنامه وارد مرحله دیگری بشود مثبت است.
حل با While
حل با Do-While
توجه کنید که در حل Do-While لازم نیست که پرامت و عمل خواندن دو بار ظاهر شوند ـ یکبار قبل از حلقه و یکبار داخل آن ـ اما مقدار ورودی را دو بار مورد امتحان قرار میدهد.
دستورهای Break و Continue
دستور break که آن را با دستور switch معرفی کردیم میتواند در حلقهها نیز بکار گرفته شود. دستور break سبب خروج فوری از داخلیترین حلقه میشود. کلمه داخلیترین را به این خاطر بکار بردهایم که اگر حلقهای در حلقه دیگری محصور شده باشد و شامل دستور break باشد دستور break سبب خروج از حلقه داخلی میشود و نه از حلقه خارجی. به مثال زیر که معمولیترین استفاده دستور break را نشان میدهد توجه کنید. فرض کنید میخواهیم 10 بار دو عدد صحیح را وارد کنیم و بعد از اطمینان از درستی داده جمع جذر هر دو را حساب کنیم. برای بررسی درستی داده، فرض کنید که اولین عدد باید از 100 کمتر و دومین عدد از 50 بزرگتر باشد. بهعلاوه بعد از هر ورودی میخواهیم حالت جریان ورودی برای EOF را نیز امتحان کنیم. حلقه ذیل شامل دستور break این وظیفه را انجام میدهد. (فرض کنید که متغیر TRUE قبلاً با مقدار 1 مقداردهی شده باشد).
حلقه بالا شامل سه نقطه خروج مشخص است. بعضی با این سبک برنامهنویسی که در تضاد با فلسفه یک نقطه ورود و یک نقطه خروج است مخالفاند. اگر بخواهیم رضایت آنان را جلب کنیم، برنامه را بدون break بهصورت زیر بازنویسی میکنیم:
از مقایسه دو قطعه کد فوق ملاحظه میشود که در کد دوم هدف اصلی برنامه که محاسبه جذر مجموع دو عدد است. در میان انبوهی از if و elseها گم شده است. بهعلاوه کد دوم به لحاظ دنبال کردن برنامه از کد اول مشکلتر است. ازاینرو میتوان معیاری برای استفاده از break در حلقهها به دست آورد. اگر استفاده از break ما را از تعداد زیادی if بینیاز میکند بهتر است از آن استفاده شود.
دستور دیگری که جریان کنترل در یک برنامه C++ را تغییر میدهد دستور Continue است. این دستور فقط در حلقهها مجاز است و تکرار جاری یا فعلی حلقه را متوقف میکند (توجه کنید تکرار جاری و نه همه حلقه).این دستور سبب میشود که کنترل به انتهای حلقه منتقل شود و تکرار بعدی آغاز شود. در مثال زیر میخواهیم تنها اعداد مثبت از فایل ورودی را پردازش کنیم.
اگر inputVal کوچکتر یا مساوی صفر باشد، کنترل به انتهای حلقه منتقل میشود. متغیر dataCount به اندازه یک واحد افزایش مییابد و آزمون حلقه قبل از تکرار بعدی برگزار میشود.
کد فوق را بدون استفاده از Continue نیز میتوانیم به ترتیب زیر بنویسیم:
اطمینان حاصل کنید که تفاوت continue و break را متوجه شدهاید، معنی دستور continue چنین است «تکرار جاری حلقه را رها کن و به تکرار بعدی برو» معنی دستور break چنین است: «فوراً از کل حلقه خارج شو».