بسم الله الرحمن الرحیم
1
برنامه سازی پیشرفته
2
زبان C یک زبان همه منظوره است. دستورالعمل های این زبان بسیار شبیه عبارات جبری و نحو آن شبیه جملات انگلیسی می باشد. این امر سبب می شود که C یک زبان سطح بالا باشد که برنامه نویسی در آن آسان است ›››
مقدمه:
3
++C که از نسل C است، تمام ویژگی های C را به ارث برده است. اما برتری فنی دیگری هم دارد: C++ اکنون «شی گرا» است. می توان با استفاده از این خاصیت، برنامه های شی گرا تولید نمود. برنامه های شی گرا منظم و ساخت یافته اند، قابل روزآمد کردن اند، به سهولت تغییر و بهبود می یابند و قابلیت اطمینان و پایداری بیشتری دارند.
4
اهم مطالب این کتاب :
جلسه سوم: «انتخاب»
جلسه دوم: «انواع اصلی»
جلسه پنجم: «توابع»
جلسه چهارم: ‹‹تکرار»
جلسه اول: «مقدمات برنامه نویسی با C++»
جلسه ششم: « آرایه ها»
5
جلسه نهم: «شیئ گرایی»
جلسه هشتم: «رشته های کاراکتری و فایل ها در ++Cاستاندارد»
جلسه دهم: «سربارگذاری عملگرها»
جلسه هفتم: «اشاره گرها و ارجاع ها»
جلسه یازدهم: «ترکیب و وراثت»
6
جلسه اول
مقدمات برنامه نویسی با C++
7
آنچه در این جلسه می خوانید:
1- چرا C++ ؟
2- تاریخچه C++
3- آماده سازی مقدمات
4- شروع کار با C++
5- عملگر خروجی
6- لیترال ها و کاراکترها
7- متغیرها و تعریف آن ها
8- مقداردهی اولیه به متغیرها
9- ثابت ها
10- عملگر ورودی
8
هدف کلی:
آشنایی با تاریخچه و مزایای زبان برنامه نویسی C++ و بیان مفاهیم بنیادی شی گرایی و عناصر مهم برنامه های C++
9
هدف های رفتاری:
انتظار می رود پس از پایان این جلسه بتوانید:
– مزایای زبان C++ را بر زبان های مشابه ذکر کرده و تفاوت آن را با زبان C بیان کنید.
– شرح مختصری از روند پیشرفت زبان های برنامه نویسی را بیان کرده و مشکلات هر دوره را به اختصار شرح دهید.
– مزایای شی گرایی در تولید نرم افزار را برشمارید.
– اصول سه گانه شی گرایی را نام برده و هر یک را به اختصار شرح دهید.
>>
10
– قالب کلی برنامه های C++ را بشناسید و بتوانید برنامه های کوچک را نوشته و آزمایش کنید.
– عملگر ورودی و خروجی را در C++ شناخته و از آن ها در برنامه ها استفاده کنید.
– نحوه اعلان متغیرها و شیوه مقداردهی به آن ها را بدانید.
– سه موجودیت «لیترال»، «کاراکتر» و «عدد» را شناخته و فرق بین آن ها را شرح دهید.
– علت و شیوه های افزودن توضیح به کد برنامه را شرح دهید.
– علت و شیوه معرفی ثابت ها در برنامه را شرح دهید.
11
مقدمه
در دهه 1970 در آزمایشگاه های بل زبانی به نام C ایجاد شد. انحصار این زبان در اختیار شرکت بل بود تا این که در سال 1978 توسط Kernighan و Richie شرح کاملی از این زبان منتشر شد و به سرعت نظر برنامه نویسان حرفه ای را جلب نمود.
هنگامی که بحث شی گرایی و مزایای آن در جهان نرم افزار رونق یافت، زبان C که قابلیت شی گرایی نداشت ناقص به نظر می رسید تا این که در اوایل دهه 1980 دوباره شرکت بل دست به کار شد و Bjarne Stroustrup زبان C++ را طراحی نمود
12
C++ ترکیبی از دو زبان C و Simula بود و قابلیت های شی گرایی نیز داشت. از آن زمان به بعد شرکت های زیادی کامپایلرهایی برای C++ طراحی کردند. این امر سبب شد تفاوت هایی بین نسخه های مختلف این زبان به وجود بیاید و از قابلیت سازگاری و انتقال آن کاسته شود.
به همین دلیل در سال 1998 زبان C++ توسط موسسه استانداردهای ملی آمریکا (ANSI) به شکل استاندارد و یک پارچه در آمد.
13
1- چرا C++ ؟
زبان C یک زبان همه منظوره است
در این زبان عملگر هایی تعبیه شده که برنامه نویسی سطح پایین و به زبان ماشین را نیز امکان پذیر می سازد
چون C عملگرهای فراوانی دارد، کد منبع برنامه ها در این زبان بسیار کوتاه است
14
برنامه مقصدی که توسط کامپایلرهای C ساخته می شود بسیار فشرده تر و کم حجم تر از برنامه های مشابه در سایر زبان ها است.
C++ که از نسل C است، تمام ویژگی های جذاب C را به ارث برده است .
و سرانجام آخرین دلیل استفاده از C++ ورود به دنیای C# است.
– زبان C برای اجرای بسیاری از دستوراتش از توابع کتابخانه ای استفاده می کند و بیشتر خصوصیات وابسته به سخت افزار را به این توابع واگذار می نماید.
15
2- تاریخچه C++
در دهه 1970 در آزمایشگاه های بل زبانی به نام C ایجاد شد. انحصار این زبان در اختیار شرکت بل بود تا این که در سال 1978 توسط Kernighan و Richie شرح کاملی از این زبان منتشر شد و به سرعت نظر برنامه نویسان حرفه ای را جلب نمود. هنگامی که بحث شی گرایی و مزایای آن در جهان نرم افزار رونق یافت، زبان C که قابلیت شی گرایی نداشت ناقص به نظر می رسید تا این که در اوایل دهه 1980 دوباره شرکت بل دست به کار شد و Bjarne Stroustrup زبان C++ را طراحی نمود.
16
C++ ترکیبی از دو زبان C و Simula بود و قابلیت های شی گرایی نیز داشت از آن زمان به بعد شرکت های زیادی کامپایلرهایی برای C++ طراحی کردند. این امر سبب شد تفاوت هایی بین نسخه های مختلف این زبان به وجود بیاید و از قابلیت سازگاری و انتقال آن کاسته شود. به همین دلیل در سال 1998 زبان C++ توسط موسسه استانداردهای ملی آمریکا (ANSI) به شکل استاندارد و یک پارچه در آمد. کامپایلرهای کنونی به این استاندارد پایبندند. کتاب حاضر نیز بر مبنای همین استاندارد نگارش یافته است.
17
3- آماده سازی مقدمات
یک «برنامه» دستورالعمل های متوالی است که می تواند توسط یک رایانه اجرا شود. برای نوشتن و اجرای هر برنامه به یک «ویرایش گر متن» و یک «کامپایلر» احتیاج داریم.
بسته Visual C++ محصول شرکت میکروسافت و بسته C++ Builder محصول شرکت بورلند نمونه های جالبی از محیط مجتمع تولید برای زبان C++ به شمار می روند.
18
4- شروع کار با C++
C++ نسبت به حروف «حساس به حالت» است یعنی A و a را یکی نمی داند
اولین برنامه ای که می نویسیم به محض تولد، به شما سلام می کند و عبارت "Hello, my programmer!" را نمایش می دهد:
#include <iostream>
int main()
{ std::cout << "Hello, my programmer!n" ;
return 0;
}
مثال : اولین برنامه
19
اولین خط از کد بالا یک «راهنمای پیش پردازنده» است. راهنمای پیش پردازنده شامل اجزای زیر است:
1- کاراکتر # که نشان می دهد این خط، یک راهنمای پیش پردازنده است. این کاراکتر باید در ابتدای همه خطوط راهنمای پیش پردازنده باشد.
2- عبارت include
3- نام یک «فایل کتابخانه ای» که میان دو علامت <> محصور شده است.
20
خط دوم برنامه نیز باید در همه برنامه های C++ وجود داشته باشد.
این خط به کامپایلر می گوید که «بدنه اصلی برنامه» از کجا شروع می شود. این خط دارای اجزای زیر است:
1 – عبارت int که یک نوع عددی در C++ است.
2 – عبارت main که به آن «تابع اصلی» در C++ می گویند.
3 – دو پرانتز () که نشان می دهد عبارت main یک «تابع» است.
هر برنامه فقط باید یک تابع main() داشته باشد .
21
سه خط آخر برنامه، «بدنه اصلی برنامه» را تشکیل می دهند.
دستورات برنامه از خط سوم شروع شده است.
دستور خط سوم با علامت سمیکولن ; پایان یافته است.
22
توضیح
توضیح، متنی است که به منظور راهنمایی و درک بهتر به برنامه اضافه می شود و تاثیری در اجرای برنامه ندارد. . کامپایلر توضیحات برنامه را قبل از اجرا حذف می کند.
استفاده از توضیح سبب می شود که سایر افراد کد برنامه شما را راحت تر درک کنند.
23
به دو صورت می توانیم به برنامه های C++ توضیحات اضافه کنیم:
1 – با استفاده از دو علامت اسلش // : هر متنی که بعد از دو علامت اسلش بیاید تا پایان همان سطر یک توضیح تلقی می شود .
2 – با استفاده از حالت C : هر متنی که با علامت /* شروع شود و با علامت */ پایان یابد یک توضیح تلقی می شود.
24
5- عملگر خروجی
علامت << عملگر خروجی در C++ نام دارد (به آن عملگر درج نیز می گویند).
یک «عملگر» چیزی است که عملیاتی را روی یک یا چند شی انجام می دهد. عملگر خروجی، مقادیر موجود در سمت راستش را به خروجی سمت چپش می فرستد.
به این ترتیب دستور
cout<< 66 ;
مقدار 66 را به خروجی cout می فرستد که cout معمولا به صفحه نمایش اشاره دارد. در نتیجه مقدار 66 روی صفحه نمایش درج می شود.
25
6 -لیترال ها و کاراکترها
یک «لیترال» رشته ای از حروف، ارقام یا علایم چاپی است که میان دو علامت نقل قول " " محصور شده باشد.
یک «کاراکتر» یک حرف، رقم یا علامت قابل چاپ است که میان دونشانه ' ' محصور شده باشد. پس 'w' و '!' و '1' هر کدام یک کاراکتر است.
به تفاوت سه موجودیت «عدد» و «کاراکتر» و «لیترال رشته ای» دقت کنید: 6 یک عدد است، '6' یک کاراکتر است و "6" یک لیترال رشته ای است.
26
7 – متغیرها و تعریف آن ها:
«متغیر» مکانی در حافظه است که چهار مشخصه دارد: نام، نوع، مقدار، آدرس. وقتی متغیری را تعریف می کنیم، ابتدا با توجه به نوع متغیر، آدرسی از حافظه در نظر گرفته می شود، سپس به آن آدرس یک نام تعلق می گیرد.
27
در C++ قبل از این که بتوانیم از متغیری استفاده کنیم، باید آن را اعلان نماییم.
نحو اعلان یک متغیر
type name initializer
عبارت type نوع متغیر را مشخص می کند. نوع متغیر به کامپایلر اطلاع می دهد که این متغیر چه مقادیری می تواند داشته باشد و چه اعمالی می توان روی آن انجام داد.
28
name initializer
عبارت name نام متغیر را نشان می دهد. این نام حداکثر می تواند 31 کاراکتر باشد، نباید با عدد شروع شود، علایم ریاضی نداشته باشد و همچنین «کلمه کلیدی» نیز نباشد.
عبارت initializer عبارت «مقداردهی اولیه» نام دارد. با استفاده از این عبارت می توان مقدار اولیه ای در متغیر مورد نظر قرار داد.
مقداردهی اولیه
دستور زیر تعریف یک متغیر صحیح را نشان می دهد:
int n = 50;
29
8 – مقداردهی اولیه به متغیرها
در بسیاری از موارد بهتر است متغیرها را در همان محلی که اعلان می شوند مقداردهی کنیم. استفاده از متغیرهای مقداردهی نشده ممکن است باعث ایجاد دردسرهایی شود.
دردسر متغیرهای مقداردهی نشده وقتی بزرگ تر می شود که سعی کنیم متغیر مقداردهی نشده را در یک محاسبه به کار ببریم. مثلا اگر x را که مقداردهی نشده در عبارت y = x + 5; به کار ببریم، حاصل y غیر قابل پیش بینی خواهد بود. برای اجتناب از چنین مشکلاتی عاقلانه است که متغیرها را همیشه هنگام تعریف، مقداردهی کنیم.
مثال:
int x=45;
int y=0;
30
9- ثابت ها
در بعضی از برنامه ها از متغیری استفاده می کنیم که فقط یک بار لازم است آن را مقداردهی کنیم و سپس مقدار آن متغیر در سراسر برنامه بدون تغییر باقی می ماند. مثلا در یک برنامه محاسبات ریاضی، متغیری به نام PI تعریف می کنیم و آن را با 3.14 مقداردهی می کنیم و می خواهیم که مقدار این متغیر در سراسر برنامه ثابت بماند. در چنین حالاتی از «ثابت ها» استفاده می کنیم.
یک ثابت، یک نوع متغیر است که فقط یک بار مقداردهی می شود و سپس تغییر دادن مقدار آن در ادامه برنامه ممکن نیست.
تعریف ثابت ها مانند تعریف متغیرهاست با این تفاوت که کلمه کلیدی const به ابتدای تعریف اضافه می شود.
31
int main()
{ // defines constants; has no output:
const char BEEP ='b';
const int MAXINT=2147483647;
const float DEGREE=23.53;
const double PI=3.14159265358979323846
return 0;
}
برنامه فوق خروجی ندارد:
مثال تعریف ثابت ها:
32
10 – عملگر ورودی
برای این که بتوانیم هنگام اجرای برنامه مقادیری را وارد کنیم از عملگر ورودی >> استفاده می کنیم.
استفاده از دستور ورودی به شکل زیر است:
cin >> variable;
variable نام یک متغیر است.
33
مثال 10 – 1 استفاده از عملگر ورودی
برنامه زیر یک عدد از کاربر گرفته و همان عدد را دوباره در خروجی نمایش می دهد:
int main()
{ // reads an integer from input:
int m;
cout << "Enter a number: ";
cin >> m;
cout << "your number is: " << m << endl;
return 0;
}
Enter a number: 52
your number is: 52
34
عملگر ورودی نیز مانند عملگر خروجی به شکل جریانی رفتار می کند. یعنی همان طور که در عملگر خروجی می توانستیم چند عبارت را با استفاده از چند عملگر << به صورت پشت سر هم چاپ کنیم، در عملگر ورودی نیز می توانیم با استفاده از چند عملگر >> چند مقدار را به صورت پشت سر هم دریافت کنیم. مثلا با استفاده از دستور:
cin >> x >> y >> z;
سه مقدار x و y و z به ترتیب از ورودی دریافت می شوند. برای این کار باید بین هر ورودی یک فضای خالی (space) بگذارید و پس از تایپ کردن همه ورودی ها، کلید enter را بفشارید. آخرین مثال جلسه، این موضوع را بهتر نشان می دهد.
35
مثال 11 – 1 چند ورودی روی یک خط
برنامه زیر مانند مثال 10 – 2 است با این تفاوت که سه عدد را از ورودی گرفته و همان اعداد را دوباره در خروجی نمایش می دهد:
int main()
{ // reads 3 integers from input:
int q, r, s;
cout << "Enter three numbers: ";
cin >> q >> r >> s;
cout << "your numbers are: << q << ", " << r
<< ", " << s << endl;
return 0;
}
Enter three numbers: 35 70 9
your numbers are: 35, 70, 9
36
پایان جلسه اول
37
جلسه دوم
«انواع اصلی»
38
آنچه در این جلسه می خوانید:
1- انواع داده عددی
2- متغیر عدد صحیح
3- محاسبات اعداد صحیح
4- عملگرهای افزایشی و کاهشی
5- عملگرهای مقدارگذاری مرکب
6- انواع ممیز شناور
›››
39
7- تعریف متغیر ممیز شناور
8 – شکل علمی مقادیر ممیز شناور
9- نوع بولین bool
10- نوع کاراکتری char
11- نوع شمارشی enum
12- تبدیل نوع، گسترش نوع
›››
40
13- برخی از خطاهای برنامه نویسی
14 – سرریزی عددی
15- خطای گرد کردن
16- حوزه متغیرها
41
هدف کلی:
معرفی انواع متغییرها و نحوه به کارگیری آن ها در برنامه های C++
هدف های رفتاری:
انتظار می رود پس از پایان این جلسه بتوانید:
– انواع عددی صحیح در C++ را نام ببرید و متغیرهایی از این نوع ها را در برنامه ها به کار ببرید.
– انواع عددی ممیز شناور در C++ را نام ببرید و متغیرهایی از این نوع ها را در برنامه ها به کار ببرید.
– نوع بولین را تعریف کرده و متغیرهایی از این نوع را در برنامه ها به کار ببرید.
>>>
42
– نوع شمارشی را شناخته و متغیرهایی از این نوع را در برنامه ها به کار ببرید.
– مفاهیم «تبدیل نوع» و «گسترش نوع» را شناخته و انواع مختلف را به یکدیگر تبدیل نمایید.
– علت خطاهای «سرریزی عددی» و «گردکردن» را دانسته و بتوانید محل وقوع آن ها را کشف کنید.
– عملگرهای حسابی و افزایشی و کاهشی و مقدارگذاری مرکب را در برنامه ها به کار ببرید.
43
ما در زندگی روزمره از داده های مختلفی استفاده می کنیم: اعداد ، تصاویر، نوشته ها یا حروف الفبا، صداها، بوها و … . با پردازش این داده ها می توانیم تصمیماتی اتخاذ کنیم، عکس العمل هایی نشان دهیم و مساله ای را حل کنیم. رایانه ها نیز قرار است همین کار را انجام دهند. یعنی داده هایی را بگیرند، آن ها را به شکلی که ما تعیین می کنیم پردازش کنند و در نتیجه اطلاعات مورد نیازمان را استخراج کنند.
مقدمه
44
1- انواع داده عددی
در C++ دو نوع اصلی داده وجود دارد: «نوع صحیح» و «نوع ممیز شناور». همه انواع دیگر از روی این دو ساخته می شوند (به شکل زیر دقت کنید).
45
نوع صحیح
نوع صحیح برای نگهداری اعداد صحیح (اعداد 0 و 1 و 2 و …) استفاده می شود. این اعداد بیشتر برای شمارش به کار می روند و دامنه محدودی دارند.
46
47
نوع ممیز شناور برای نگهداری اعداد اعشاری استفاده می شود. اعداد اعشاری بیشتر برای اندازه گیری دقیق به کار می روند و دامنه بزرگ تری دارند. یک عدد اعشاری مثل 352/187 را می توان به شکل 10×7352/18 یا 102×87352/1 یا1-10×52/1873یا2-10×2/18735 و یا … نوشت.
به این ترتیب با کم و زیاد کردن توان عدد 10 ممیز عدد نیز جابه جا می شود. به همین دلیل است که به اعداد اعشاری «اعداد ممیز شناور» می گویند.
48
2- متغیر عدد صحیح
C++ شش نوع متغیر عدد صحیح دارد تفاوت این شش نوع مربوط به میزان حافظه مورد استفاده و محدوده مقادیری است که هر کدام می توانند داشته باشند.
این میزان حافظه مورد استفاده و محدوده مقادیر، بستگی زیادی به سخت افزار و همچنین سیستم عامل دارد. یعنی ممکن است روی یک رایانه، نوع int دو بایت از حافظه را اشغال کند در حالی که روی رایانه ای از نوع دیگر نوع int به چهار بایت حافظه نیاز داشته باشد.
49
وقتی برنامه ای می نویسید، توجه داشته باشید که از نوع صحیح مناسب استفاده کنید تا هم برنامه دچار خطا نشود و هم حافظه سیستم را هدر ندهید.
50
3 -محاسبات اعداد صحیح
C++ مانند اغلب زبان های برنامه نویسی برای محاسبات از عملگرهای جمع (+) ، تفریق (-) ، ضرب (*) ، تقسیم (/) و باقیمانده (%) استفاده می کند.
51
4 – عملگرهای افزایشی و کاهشی
C++ برای دستکاری مقدار متغیرهای صحیح، دو عملگر جالب دیگر دارد:
عملگر ++ :
مقدار یک متغیر را یک واحد افزایش می دهد.
عملگر — :
مقدار یک متغیر را یک واحد کاهش می دهد.
اما هر کدام از این عملگرها دو شکل متفاوت دارند: شکل «پیشوندی» و شکل «پسوندی».
52
در شکل پیشوندی، عملگر قبل از نام متغیر می آید مثل ++m یا –n . در شکل پسوندی، عملگر بعد از نام متغیر می آید مثل m++ یا n– .
در شکل پیشوندی ابتدا متغیر، متناسب با عملگر، افزایش یا کاهش می یابد و پس از آن مقدار متغیر برای محاسبات دیگر استفاده می شود.
در شکل پسوندی ابتدا مقدار متغیر در محاسبات به کار می رود و پس از آن مقدار متغیر یک واحد افزایش یا کاهش می یابد.
53
5 – عملگرهای مقدارگذاری مرکب
C++ عملگرهای دیگری دارد که مقدارگذاری در متغیرها را تسهیل می نمایند. مثلا با استفاده از عملگر += می توانیم هشت واحد به m اضافه کنیم اما با دستور کوتاه تر: m += 8;
دستور بالا معادل دستور m = m + 8; است با این تفاوت که کوتاه تر است. به عملگر += «عملگر مرکب» می گویند زیرا ترکیبی از عملگرهای + و = می باشد
54
5- عملگرهای مقدارگذاری مرکب
قبلا از عملگر = برای مقدارگذاری در متغیرها استفاده کردیم. C++ عملگرهای دیگری دارد که مقدارگذاری در متغیرها را تسهیل می نمایند.
عملگر مرکب در C++ عبارتند از: += و -= و *= و /= و =%
55
نحوه عمل این عملگرها به شکل زیر است:
m += 8; → m = m + 8;
m -= 8; → m = m – 8;
m *= 8; → m = m * 8;
m /= 8; →m = m / 8;
m %= 8; →m = m % 8;
56
6 – انواع ممیز شناور
عدد ممیز شناور به بیان ساده همان عدد اعشاری است. عددی مثل 123.45 یک عدد اعشاری است. برای این که مقدار این عدد در رایانه ذخیره شود، ابتدا باید به شکل دودویی تبدیل شود:
123.45 = 1111011.01110012
اکنون برای مشخص نمودن محل اعشار در عدد، تمام رقم ها را به سمت راست ممیز منتقل می کنیم. البته با هر جابجایی ممیز، عدد حاصل باید در توانی از 2 ضرب شود:
123.45 = 0.11110110111001× 27
به مقدار 11110110111001 «مانتیس عدد» و به 7 که توان روی دو است، «نمای عدد» گفته می شود.
57
درC++ سه نوع ممیز شناور وجود دارد:
نوع long double از هشت یا ده یا دوازده یا شانزده بایت برای نگهداری عدد استفاده می کند.
معمولا نوع float از چهار بایت برای نگهداری عدد استفاده می کند.
نوع double از هشت بایت برای نگهداری عدد استفاده می کند.
58
جدول تخصیص حافظه برای متغییر های ممیز شناور
59
7 – تعریف متغیر ممیز شناور
تعریف متغیر ممیز شناور مانند تعریف متغیر صحیح است. با این تفاوت که از کلمه کلیدی float یا double برای مشخص نمودن نوع متغیر استفاده می کنیم.
مثال:
float x;
double x,y=0;
تفاوت نوع float با نوع double در این است که نوع double دو برابر float از حافظه استفاده می کند. پس نوع double دقتی بسیار بیشتر از float دارد. به همین دلیل محاسبات double وقت گیرتر از محاسبات float است.
60
اعداد ممیز شناور به دو صورت در ورودی و خروجی نشان داده می شوند: به شکل «ساده» و به شکل «علمی».
8- شکل علمی مقادیر ممیز شناور
2- علمی
1.234567×104
1- ساده
12345.67
مشخص است که شکل علمی برای نشان دادن اعداد خیلی کوچک و همچنین اعداد خیلی بزرگ، کارآیی بیشتری دارد.
61
نوع bool یک نوع صحیح است که متغیرهای این نوع فقط می توانند مقدار true یا false داشته باشند. true به معنی درست و false به معنی نادرست است.
اما این مقادیر در اصل به صورت 1 و 0 درون رایانه ذخیره می شوند: 1 برای true و 0 برای false.
9 – نوع بولین bool
62
10- نوع کاراکتری char
یک کاراکتر یک حرف، رقم یا نشانه است که یک شماره منحصر به فرد دارد. به عبارت عامیانه، هر کلیدی که روی صفحه کلید خود می بینید یک کاراکتر را نشان می دهد.
مثلا هر یک از حروف 'A' تا 'Z' و 'a' تا 'z' و هر یک از اعداد '0' تا '9' و یا نشانه های '~' تا '+' روی صفحه کلید را یک کاراکتر می نامند.
63
برای تعریف متغیری از نوع کاراکتر از کلمه کلیدی char استفاده می کنیم. یک کاراکتر باید درون دو علامت آپستروف (') محصور شده باشد. پس 'A' یک کاراکتر است؛ همچنین'8' یک کاراکتر است اما 8 یک کاراکتر نیست بلکه یک عدد صحیح است .
مثال:
char c ='A';
64
11 – نوع شمارشی enum
یک نوع شمارشی یک نوع صحیح است که توسط کاربر مشخص می شود. نحو تعریف یک نوع شمارشی به شکل زیر است:
enum typename{enumerator-list}
که enum کلمه ای کلیدی است، typename نام نوع جدید است که کاربر مشخص می کند و enumerator-list مجموعه مقادیری است که این نوع جدید می تواند داشته باشد.
65
به عنوان مثال به تعریف زیر دقت کنید:
enum Day{SAT,SUN,MON,TUE,WED,THU,FRI}
حالا Day یک نوع جدید است و متغیرهایی که از این نوع تعریف می شوند می توانند یکی از مقادیر SAT و SUN و MON و TUE و WED و THU و FRI را داشته باشند:
Day day1,day2;
day1 = MON;
day2 = THU;
وقتی نوع جدید Day و محدوده مقادیرش را تعیین کردیم، می توانیم متغیرهایی از این نوع جدید بسازیم. در کد بالا متغیرهای day1 و day2 از نوع Day تعریف شده اند. آنگاه day1 با مقدار MON و day2 با مقدار THU مقداردهی شده است.
66
مقادیر SAT و SUN و … هر چند که به همین شکل به کار می روند اما در رایانه به شکل اعداد صحیح 0 و 1 و 2 و … ذخیره می شوند. به همین دلیل است که به هر یک از مقادیر SAT و SUN و … یک شمارشگر می گویند.
می توان مقادیر صحیح دلخواهی را به شمارشگرها نسبت داد:
enum Day{SAT=1,SUN=2,MON=4,TUE=8,WED=16,THU=32,FRI=64}
اگر فقط بعضی از شمارشگرها مقداردهی شوند، آنگاه سایر شمارشگرها که مقداردهی نشده اند مقادیر متوالی بعدی را خواهند گرفت:
enum Day{SAT=1,SUN,MON,TUE,WED,THU,FRI}
دستور بالا مقادیر 1 تا 7 را به ترتیب به روزهای هفته تخصیص خواهد داد.
همچنین دو یا چند شمارشگر در یک فهرست می توانند مقادیر یکسانی داشته باشند:
enum Answer{NO=0,FALSE=0,YES=1,TRUE=1,OK=1}
67
نام شمارشگر باید معتبر باشد:
یعنی:
1- کلمه کلیدی نباشد.
2- با عدد شروع نشود.
3- نشانه های ریاضی نیز نداشته باشد.
1 – برای نام ثابت ها از حروف بزرگ استفاده کنید
2 – اولین حرف از نام نوع شمارشی را با حرف بزرگ بنویسید.
3 – در هر جای دیگر از حروف کوچک استفاده کنید.
نحوه انتخاب نام شمارشگرها آزاد است اما بیشتر برنامه نویسان از توافق زیر در برنامه هایشان استفاده می کنند:
68
آخر این که نام شمارشگرها نباید به عنوان نام متغیرهای دیگر در جاهای دیگر برنامه استفاده شود. مثلا:
enum Score{A,B,C,D}
float B;
char c;
در تعریف های بالا B و C را نباید به عنوان نام متغیرهای دیگر به کار برد زیرا این نام ها در نوع شمارشی Score به کار رفته است .
شمارشگرهای هم نام نباید در محدوده های مشترک استفاده شوند. برای مثال تعریف های زیر را در نظر بگیرید:
enum Score{A,B,C,D}
enum Group{AB,B,BC}
دو تعریف بالا غیرمجاز است زیرا شمارشگر B در هر دو تعریف Score و Group آمده است.
69
انواع شمارشی برای تولید کد «خود مستند» به کار می روند، یعنی کدی که به راحتی درک شود و نیاز به توضیحات اضافی نداشته باشد.
مثلا تعاریف زیر خودمستند هستند زیرا به راحتی نام و نوع کاربرد و محدوده مقادیرشان درک می شود:
enum Color{RED,GREEN,BLUE,BLACK,ORANGE}
enum Time{SECOND,MINUTE,HOUR}
enum Date{DAY,MONTH,YEAR}
enum Language{C,DELPHI,JAVA,PERL}
enum Gender{MALE,FEMALE}
70
در محاسباتی که چند نوع متغیر وجود دارد، جواب همیشه به شکل متغیری است که دقت بالاتری دارد. یعنی اگر یک عدد صحیح را با یک عدد ممیز شناور جمع ببندیم، پاسخ به شکل ممیز شناور است به این عمل گسترش نوع می گویند.
12 – تبدیل نوع، گسترش نوع
برای این که مقدار یک متغیر از نوع ممیز شناور را به نوع صحیح تبدیل کنیم از عبارت int() استفاده می کنیم به این عمل تبدیل نوع گفته می شود
71
مثال تبدیل نوع:
این برنامه، یک نوع double را به نوع int تبدیل می کند:
int main()
{ // casts a double value as an int:
double v=1234.987;
int n;
n = int(v);
cout << "v = " << v << ", n = " << n << endl;
return 0;
}
مثال های زیر تبدیل نوع و گسترش نوع را نشان می دهند.
مثال گسترش نوع
برنامه زیر یک عدد صحیح را با یک عدد ممیز شناور جمع می کند:
int main()
{ // adds an int value with a double value:
int n = 22;
double p = 3.1415;
p += n;
cout << "p = " << p << ", n = " << n << endl;
return 0;
}
72
«خطای زمان کامپایل»
این قبیل خطاها که اغلب خطاهای نحوی هستند ، توسط کامپایلر کشف می شوند و به راحتی می توان آن ها را رفع نمود.
«خطای زمان اجرا»
کشف اینگونه خطاها به راحتی ممکن نیست و کامپایلر نیز چیزی راجع به آن نمی داند. برخی از خطاهای زمان اجرا سبب می شوند که برنامه به طور کامل متوقف شود و از کار بیفتد.
13 – برخی از خطاهای برنامه نویسی
73
یک متغیر هر قدر هم که گنجایش داشته باشد، بالاخره مقداری هست که از گنجایش آن متغیر بیشتر باشد. اگر سعی کنیم در یک متغیر مقداری قرار دهیم که از گنجایش آن متغیر فراتر باشد، متغیر «سرریز» می شود،در چنین حالتی می گوییم که خطای سرریزی رخ داده است.
14- سرریزی عددی
74
مثال 12 – 2 سرریزی عدد صحیح
این برنامه به طور مکرر n را در 1000 ضرب می کند تا سرانجام سرریز شود:
int main()
{ //prints n until it overflows:
int n =1000;
cout << "n = " << n << endl;
n *= 1000; // multiplies n by 1000
cout << "n = " << n << endl;
n *= 1000; // multiplies n by 1000
cout << " n = " << n << endl;
n *= 1000; // multiplies n by 1000
cout << " n = " << n << endl;
return 0;
}
وقتی یک عدد صحیح سرریز شود، عدد سرریز شده به یک مقدار منفی «گردانیده» می شود اما وقتی یک عدد ممیز شناور سرریز شود، نماد inf به معنای بی نهایت را به دست می دهد.
75
15 – خطای گرد کردن
خطای گرد کردن نوع دیگری از خطاست که اغلب وقتی رایانه ها روی اعداد حقیقی محاسبه می کنند، رخ می دهد. برای مثال عدد 1/3ممکن است به صورت 0.333333 ذخیره شود که دقیقا معادل 1/3 نیست .
این خطا از آن جا ناشی می شود که اعدادی مثل 1/3 مقدار دقیق ندارند و رایانه نمی تواند این مقدار را پیدا کند، پس نزدیک ترین عدد قابل محاسبه را به جای چنین اعدادی منظور می کند.
«هیچ گاه از متغیر ممیز شناور برای مقایسه برابری استفاده نکنید» زیرا در متغیرهای ممیز شناور خطای گرد کردن سبب می شود که پاسخ با آن چه مورد نظر شماست متفاوت باشد.
76
16 – حوزه متغیرها
انتخاب نام های نامفهوم یا ناقص سبب کاهش خوانایی برنامه و افزایش خطاهای برنامه نویسی می شود. استفاده از متغیرها در حوزه نامناسب هم سبب بروز خطاهایی می شود. «حوزه متغیر» محدوده ای است که یک متغیر خاص اجازه دارد در آن محدوده به کار رود یا فراخوانی شود.
اصطلاح «بلوک» در C++ واژه مناسبی است که می توان به وسیله آن حوزه متغیر را مشخص نمود. یک بلوک برنامه، قسمتی از برنامه است که درون یک جفت علامت کروشه { } محدود شده است.
77
حوزه یک متغیر از محل اعلان آن شروع می شود و تا پایان همان بلوک ادامه می یابد. خارج از آن بلوک نمی توان به متغیر دسترسی داشت. همچنین قبل از این که متغیر اعلان شود نمی توان آن را استفاده نمود.
می توانیم در یک برنامه، چند متغیر متفاوت با یک نام داشته باشیم به شرطی که در حوزه های مشترک نباشند.
78
پایان جلسه دوم
79
جلسه سوم
«انتخاب»
80
آنچه در این جلسه می خوانید:
1- دستور if
2- دستور if..else
3- عملگرهای مقایسه ای
4- بلوک های دستورالعمل
5- شرط های مرکب
6- ارزیابی میانبری
›››
81
7- عبارات منطقی
8 – دستور های انتخاب تودرتو
9- ساختار else if
10- دستورالعمل switch
11- عملگر عبارت شرطی
12- کلمات کلیدی
82
هدف کلی:
شناخت انواع دستورالعمل های انتخاب و شیوه به کارگیری هر یک
هدف های رفتاری:
انتظار می رود پس از پایان این جلسه بتوانید:
– نحو دستور if را شناخته و آن را در برنامه ها به کار ببرید.
– نحو دستور if..else را شناخته و آن را در برنامه ها به کار ببرید.
– از ساختار else..if در تصمیم گیری های پیچیده استفاده کنید.
– نحو دستور switch را شناخته و خطای «تله سقوط» را تشخیص دهید.
– بلوک دستورالعمل را تعریف کنید.
– عملگرهای مقایسه ای و عملگر عبارت شرطی را در دستورات شرطی به کار ببرید.
– از شرط های مرکب استفاده کرده و ارزیابی میانبری را شرح دهید.
– «کلمه کلیدی» را تعریف کنید.
>>>
83
همه برنامه هایی که در دو جلسه اول بیان شد، به شکل ترتیبی اجرا می شوند، یعنی دستورات برنامه به ترتیب از بالا به پایین و هر کدام دقیقا یک بار اجرا می شوند. در این جلسه نشان داده می شود چگونه از دستورالعمل های انتخاب1 جهت انعطاف پذیری بیشتر برنامه استفاده کنیم. همچنین در این جلسه انواع صحیح که در C++ وجود دارد بیشتر بررسی می گردد.
مقدمه
84
دستور if موجب می شود برنامه به شکل شرطی اجرا شود. نحو آن به گونه زیر است :
If (condition) statement;
Condition که شرط نامیده می شود یک عبارت صحیح است (عبارتی که با یک مقدار صحیح برآورد می شود) و statement می تواند هر فرمان قابل اجرا باشد. Statement وقتی اجرا خواهد شد که condition مقدار غیر صفر داشته باشد. دقت کنید که شرط باید درون پرانتز قرار داده شود.
دستور if
85
2- دستور if..else
دستور if..else موجب می شود بسته به این که شرط درست باشد یا خیر، یکی از دو دستورالعمل فرعی اجرا گردد. نحو این دستور به شکل زیر است:
if (condition) statement1;
else statement2;
condition همان شرط مساله است که یک عبارت صحیح می باشد و statement1 و statement2 فرمان های قابل اجرا هستند. اگر مقدار شرط، غیر صفر باشد، statement1 اجرا خواهد شد وگرنه statement2 اجرا می شود.
86
int main()
{ int n, d;
cout << " Enter two positive integers: ";
cin >> n >> d;
if (n%d) cout << n << " is not divisible by " << d << endl;
else cout << n << " is divisible by " << d << endl;
}
مثال یک آزمون قابلیت تقسیم
87
4- عملگرهای مقایسه ای
در C++ شش عملگر مقایسه ای وجود دارد: < و > و <= و >= و == و != . هر یک از این شش عملگر به شکل زیر به کار می روند:
x < y // است y کوچکتر از x
x > y // است y بزرگتر از x
x <= y // است y کوچکتر یا مساوی x
x >= y // است y بزرگ تر یا مساوی x
x == y // است y مساوی با x
x != y // نیست y مساوی با x
88
این ها می توانند برای مقایسه مقدار عبارات با هر نوع ترتیبی استفاده شوند. عبارت حاصل به عنوان یک شرط تفسیر می شود. مقدار این شرط صفر است اگر شرط نادرست باشد و غیر صفر است اگر شرط درست باشد. برای نمونه، عبارت 7*8<6*5 برابر با صفر ارزیابی می شود، به این معنی که این شرط نادرست است.
89
2- متغیر عدد صحیح
C++ شش نوع متغیر عدد صحیح دارد تفاوت این شش نوع مربوط به میزان حافظه مورد استفاده و محدوده مقادیری است که هر کدام می توانند داشته باشند.
این میزان حافظه مورد استفاده و محدوده مقادیر، بستگی زیادی به سخت افزار و همچنین سیستم عامل دارد. یعنی ممکن است روی یک رایانه، نوع int دو بایت از حافظه را اشغال کند در حالی که روی رایانه ای از نوع دیگر نوع int به چهار بایت حافظه نیاز داشته باشد.
90
مثلا دستور x = 33; مقدار 33 را در x قرار می دهد ولی دستور x == 33; بررسی می کند که آیا مقدار x با 33 برابر است یا خیر. درک این تفاوت اهمیت زیادی دارد.
دقت کنید که در ++C عملگر جایگزینی با عملگر برابری فرق دارد. عملگر جایگزینی یک مساوی تکی " = " است ولی عملگر برابری، دو مساوی " = = " است.
91
4- بلوک های دستورالعمل
یک بلوک دستورالعمل زنجیره ای از دستورالعمل هاست که درون براکت {} محصور شده، مانند :
{ int temp=x;
x = y;
y = temp;
}
در برنامه های ++C یک بلوک دستورالعمل مانند یک دستورالعمل تکی است.
92
int main()
{ int x, y;
cout << "Enter two integers: ";
cin >> x >> y;
if (x > y) { int temp = x;
x = y;
y = temp;
} //swap x and y
cout << x << " <= " << y << endl;
}
مثال : یک بلوک دستورالعمل درون یک دستور if
این برنامه دو عدد صحیح را گرفته و به ترتیب بزرگ تری، آن ها را چاپ می کند:
93
int main()
{ int n=44;
cout << "n = " << n << endl;
{ int n;
cout << "Enter an integer: ";
cin >> n;
cout << "n = " << n << endl; }
{ cout << " n = " << n << endl; }
{ int n;
cout << "n = " << n << endl; }
cout << "n = " << n << endl; }
94
5 – شرط های مرکب
شرط هایی مانند n%d و x>=y می توانند به صورت یک شرط مرکب با هم ترکیب شوند. این کار با استفاده ازعملگرهای منطقی && (and) و || (or) و ! (not) صورت می پذیرد. این عملگرها به شکل زیر تعریف می شوند:
p && q درست است اگر و تنها اگر هم p و هم q هر دو درست باشند
p || q نادرست است اگر و تنها اگر هم p و هم q هر دو نادرست باشند
!pدرست است اگر و تنها اگر p نادرست باشد
برای مثال(n%d || x>=y) نادرست است اگر و تنها اگر n%d برابر صفر و x کوچک تر از y باشد.
95
سه عملگر منطقی && (and) و || (or) و ! (not) معمولا با استفاده از جداول درستی به گونه زیر بیان می شوند:
طبق جدول های فوق اگر p درست و q نادرست باشد، عبارت p&&q نادرست و عبارت p||q درست است.
96
6- ارزیابی میانبری
عملگرهای && و || به دو عملوند نیاز دارندتا مقایسه را روی آن دو انجام دهند. جداول درستی نشان می دهد که p&&q نادرست است اگر p نادرست باشد. در این حالت دیگر نیازی نیست که q بررسی شود. همچنین p||q درست است اگر p درست باشد و در این حالت هم نیازی نیست که q بررسی شود. در هر دو حالت گفته شده، با ارزیابی عملوند اول به سرعت نتیجه معلوم می شود. این کار ارزیابی میانبری نامیده می شود.
شرط های مرکب که از && و || استفاده می کنند عملوند دوم را بررسی نمی کنند مگر این که لازم باشد.
97
7- عبارات منطقی
یک عبارت منطقی شرطی است که یا درست است یا نادرست. قبلا دیدیم که عبارات منطقی با مقادیر صحیح ارزیابی می شوند. مقدار صفر به معنای نادرست و هر مقدار غیر صفر به معنای درست است. به عبارات منطقی «عبارات بولی» هم می گویند.
98
چون همه مقادیر صحیح ناصفر به معنای درست تفسیر می شوند، عبارات منطقی اغلب تغییر قیافه می دهند. برای مثال دستور
if (n) cout << "n is not zero";
وقتی n غیر صفر است عبارت n is not zero را چاپ می کند زیرا عبارت منطقی (n) وقتی مقدار n غیر صفر است به عنوان درست تفسیر می گردد.
99
کد زیر را نگاه کنید:
if (n%d) cout << "n is not a multiple of d";
دستور خروجی فقط وقتی که n%d ناصفر است اجرا می گردد و n%d وقتی ناصفر است که n بر d بخش پذیر نباشد. گاهی ممکن است فراموش کنیم که عبارات منطقی مقادیر صحیح دارند و این فراموشی باعث ایجاد نتایج غیر منتظره و نامتعارف شود.
100
یک خطای منطقی دیگر، این برنامه خطادار است:
int main()
{ int n1, n2, n3;
cout << "Enter three integers: ";
cin >> n1 >> n2 >> n3;
if (n1 >= n2 >= n3) cout << "max = " << n1;
}
منشا خطا در برنامه بالا این اصل است که عبارات منطقی مقدارهای عددی دارند.
101
دستورهای انتخاب می توانند مانند دستورالعمل های مرکب به کار روند. به این صورت که یک دستور انتخاب می تواند درون دستور انتخاب دیگر استفاده شود. به این روش، جملات تودرتو می گویند.
8- دستور های انتخاب تودرتو
102
مثال 12-3 دستور های انتخاب تودرتو
این برنامه همان اثر مثال 10-3 را دارد:
int main()
{ int n, d;
cout << "Enter two positive integers: ";
cin >> n >> d;
if (d != 0)
if (n%d = = 0) cout << d << " divides " << n << endl;
else cout << d << " does not divide " << n << endl;
else cout << d << " does not divide " << n << endl;
}
در برنامه بالا، دستور if..else دوم درون دستور if..else اول قرار گرفته است .
وقتی دستور if..else به شکل تو در تو به کار می رود، کامپایلر از قانون زیر جهت تجزیه این دستورالعمل مرکب استفاده می کند:
« هر else با آخرین if تنها جفت می شود.»
103
9- ساختار else if
دستور if..else تودرتو، اغلب برای بررسی مجموعه ای از حالت های متناوب یا موازی به کار می رود. در این حالات فقط عبارت else شامل دستور if بعدی خواهد بود. این قبیل کدها را معمولا با ساختار else ifمی سازند.
104
استفاده از ساختار else if برای مشخص کردن محدوده نمره
برنامه زیر یک نمره امتحان را به درجه حرفی معادل تبدیل می کند:
int main()
{ int score;
cout << "Enter your test score: "; cin >> score;
if (score > 100) cout << "Error: that score is out of range.";
else if (score >= 90) cout << "Your grade is an A." << endl;
else if (score >= 80) cout << "Your grade is a B." << endl;
else if (score >= 70) cout << "Your grade is a C." << endl;
else if (score >= 60) cout << "Your grade is a D." << endl;
else if (score >= 0) cout << "Your grade is an F." << endl;
else cout << "Error: that score is out of range.";
}
105
10- دستورالعمل switch
دستور switch می تواند به جای ساختار else if برای بررسی مجموعه ای از حالت های متناوب و موازی به کار رود. نحو دستور switch به شکل زیر است:
switch (expression)
{ case constant1: statementlist1;
case constant2: statementlist2;
case constant3: statementlist3;
:
:
case constantN: statementlistN;
default: statementlist0;
}
106
این دستور ابتدا expression را برآورد می کند و سپس میان ثابت های case به دنبال مقدار آن می گردد. اگر مقدار مربوطه از میان ثابت های فهرست شده یافت شد، دستور statementlist مقابل آن case اجرا می شود. اگر مقدار مورد نظر میان caseها یافت نشد و عبارت default وجود داشت، دستور statementlist مقابل آن اجرا می شود.
عبارتdefault یک عبارت اختیاری است. یعنی می توانیم در دستور switch آن را قید نکنیم. expression باید به شکل یک نوع صحیح ارزیابی شود و constantها باید ثابت های صحیح باشند.
107
لازم است در انتهای هر case دستور break قرار بگیرد. بدون این دستور، اجرای برنامه پس از این که case مربوطه را اجرا کرد از دستور switch خارج نمی شود، بلکه همه caseهای زیرین را هم خط به خط می پیماید و دستورات مقابل آن ها را اجرا می کند. به این اتفاق، تله سقوط می گویند.
case constant1: statementlist1;break;
108
عملگر عبارت شرطی یکی از امکاناتی است که جهت اختصار در کدنویسی تدارک دیده شده است. این عملگر را می توانیم به جای دستور if..else به کار ببریم. این عملگر از نشانه های ? و : به شکل زیر استفاده می کند:
condition ? expression1 : expression2;
11- عملگر عبارت شرطی
در این عملگر ابتدا شرط condition بررسی می شود. اگر این شرط درست بود، حاصل کل عبارت برابر با expression1 می شود و اگر شرط نادرست بود، حاصل کل عبارت برابر با expression2 می شود.
109
مثلا در دستور انتساب زیر:
min = ( x<y ? x : y );
اگر x<y باشد مقدار x را درون min قرار می دهد و اگر x<y نباشد مقدار y را درون min قرار می دهد. یعنی به همین سادگی و اختصار، مقدار کمینه x و y درون متغیر min قرار می گیرد.
110
اکنون با کلماتی مثل if و case و float آشنا شدیم. دانستیم که این کلمات برای C++ معانی خاصی دارند. از این کلمات نمی توان به عنوان نام یک متغیر یا هر منظور دیگری استفاده کرد و فقط باید برای انجام همان کار خاص استفاده شوند. مثلا کلمه float فقط باید برای معرفی یک نوع اعشاری به کار رود.
12- کلمات کلیدی
یک کلمه کلیدی در یک زبان برنامه نویسی کلمه ای است که از قبل تعریف شده و برای هدف مشخصی منظور شده است.
111
C++ استاندارد اکنون شامل 74 کلمه کلیدی است:
112
113
114
یک کلمه رزرو شده کلمه ای است که یک دستور خاص از آن زبان را نشان می دهد. کلمه کلیدی if و else کلمات رزرو شده هستند.
دو نوع کلمه کلیدی وجود دارد:
1- کلمه های رزرو شده
2- شناسه های استاندارد.
یک شناسه استاندارد کلمه ای است که یک نوع داده استاندارد از زبان را مشخص می کند. کلمات کلیدی bool و int شناسه های استاندارد هستند
115
پایان جلسه سوم
116
جلسه چهارم
«تکرار»
117
آنچه در این جلسه می خوانید:
1- دستور while
2- خاتمه دادن به یک حلقه
3- دستور do..while
4- دستور for
5- دستور break
6- دستور continue
7- دستور goto
8- تولید اعداد شبه تصادفی
118
هدف های رفتاری:
انتظار می رود پس از مطالعه این جلسه بتوانید:
– نحو دستورwhile را شناخته و از آن برای ایجاد حلقه استفاده کنید.
– نحو دستور do..while را شناخته و تفاوت آن با دستور while را بیان کنید.
– نحو دستور for را شناخته و با استفاده از آن حلقه های گوناگون بسازید.
– حلقه های فوق را به یکدیگر تبدیل کنید.
– علت استفاده از «دستورات پرش» را ذکر کرده و تفاوت سه دستور break و continue و goto را بیان کنید.
– اهمیت اعداد تصادفی را بیان کرده و نحوه تولید «اعداد شبه تصادفی» را بدانید.
هدف کلی:
شناخت انواع ساختارهای تکرار و نحو آن ها و تبدیل آن ها به یکدیگر.
119
مقدمه
تکرار، اجرای پی در پی یک دستور یا بلوکی از دستورالعمل ها در یک برنامه است. با استفاده از تکرار می توانیم کنترل برنامه را مجبور کنیم تا به خطوط قبلی برگردد و آن ها را دوباره اجرا نماید.
C++ دارای سه دستور تکرار است: دستور while، دستور do_while و دستور for. دستور های تکرار به علت طبیعت چرخه مانندشان ، حلقه نیز نامیده می شوند.
120
1- دستور while
نحو دستور while به شکل زیر است:
while (condition) statement;
به جای condition، یک شرط قرار می گیرد و به جای statement دستوری که باید تکرار شود قرار می گیرد. اگر مقدار شرط، صفر(یعنی نادرست) باشد، statement نادیده گرفته می شود و برنامه به اولین دستور بعد از while پرش می کند. اگر مقدار شرط ناصفر(یعنی درست) باشد، statement اجرا شده و دوباره مقدار شرط بررسی می شود. این تکرار آن قدر ادامه می یابد تا این که مقدار شرط صفر شود.
121
مثال 1-4 محاسبه حاصل جمع اعداد صحیح متوالی با حلقه while
این برنامه مقدار 1 + 2 + 3 + … + n را برای عدد ورودی n محاسبه می کند:
int main()
{ int n, i=1;
cout << "Enter a positive integer: ";
cin >> n;
long sum=0;
while (i <= n)
sum += i++;
cout << "The sum of the first " << n << " integers is "
<< sum;
}
122
int main()
{ int n, i=1;
cout << "Enter a positive integer: ";
cin >> n;
long sum=0;
while (true)
{ if (i > n) break;
sum += i++;
}
cout << "The sum of the first " << n << " integers is " << sum;
}
2- خاتمه دادن به یک حلقه
قبلا دیدیم که چگونه دستور break برای کنترل دستورالعمل switch استفاده می شود (به مثال 17-4 نگاه کنید). از دستور break برای پایان دادن به حلقه ها نیز می توان استفاده کرد.
یکی از مزیت های دستور break این است که فورا حلقه را خاتمه می دهد بدون این که مابقی دستورهای درون حلقه اجرا شوند.
123
* مثال 4-4 اعداد فیبوناچی
اعداد فیبوناچی F0, F1, F2, F3, … به شکل بازگشتی توسط معادله های زیر تعریف می شوند:
F0 = 0 , F1 = 1 , Fn = Fn-1 + Fn-2
مثلا برای n=2 داریم:
F2 = F2-1 + F2-2 = F1 + F0 = 0 + 1 = 1
یا برای n=3 داریم:
F3 = F3-1 + F3-2 = F2 + F1 = 1 + 1 = 2
و برای n=4 داریم:
F4 = F4-1 + F4-2 = F3 + F2 = 2 + 1 = 3
124
برنامه زیر، همه اعداد فیبوناچی را تا یک محدوده مشخص که از ورودی دریافت می شود، محاسبه و چاپ می کند:
int main()
{ long bound;
cout << "Enter a positive integer: ";
cin >> bound;
cout << "Fibonacci numbers < " << bound << ":n0, 1";
long f0=0, f1=1;
while (true)
{ long f2 = f0 + f1;
if (f2 > bound) break;
cout << ", " << f2;
f0 = f1;
f1 = f2;}
}
Enter a positive integer: 1000
Fibonacci numbers < 1000:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987
125
int main()
{ long bound;
cout << "Enter a positive integer: ";
cin >> bound;
cout << "Fibonacci numbers < " << bound << ":n0, 1";
long f0=0, f1=1;
while (true)
{ long f2 = f0 + f1;
if (f2 > bound) exit(0);
cout << ", " << f2;
f0 = f1;
f1 = f2; }
}
برنامه نویسان ترجیح می دهند از break برای خاتمه دادن به حلقه های نامتناهی استفاده کنند زیرا قابلیت انعطاف بیشتری دارد.
مثال5-4 استفاده از تابع exit(0)
تابع exit(0) روش دیگری برای خاتمه دادن به یک حلقه است. هرچند که این تابع بلافاصله اجرای کل برنامه را پایان می دهد:
126
متوقف کردن یک حلقه نامتناهی :
با فشردن کلیدهای Ctrl+C سیستم عامل یک برنامه را به اجبار خاتمه می دهد. کلید Ctrl را پایین نگه داشته و کلید C روی صفحه کلید خود را فشار دهید تا برنامه فعلی خاتمه پیدا کند.
127
3- دستور do..while
ساختار do..while روش دیگری برای ساختن حلقه است. نحو آن به صورت زیر است:
do statement while (condition);
به جای condition یک شرط قرار می گیرد و به جای statement دستور یا بلوکی قرار می گیرد که قرار است تکرار شود.
این دستور ابتدا statement را اجرا می کند و سپس شرط condition را بررسی می کند. اگر شرط درست بود حلقه دوباره تکرار می شود وگرنه حلقه پایان می یابد.
128
دستور do..while مانند دستور while است. با این فرق که شرط کنترل حلقه به جای این که در ابتدای حلقه ارزیابی گردد، در انتهای حلقه ارزیابی می شود.
یعنی هر متغیر کنترلی به جای این که قبل از شروع حلقه تنظیم شود، می تواند درون آن تنظیم گردد.
نتیجه دیگر این است که حلقه do..while همیشه بدون توجه به مقدار شرط کنترل، لااقل یک بار اجرا می شود اما حلقه while می تواند اصلا اجرا نشود.
129
مثال 7-4 محاسبه حاصل جمع اعداد صحیح متوالی با حلقه do..while
این برنامه همان تاثیر مثال 1-5 را دارد:
int main()
{ int n, i=0;
cout << "Enter a positive integer: ";
cin >> n;
long sum=0;
do
sum += i++;
while (i <= n);
cout << "The sum of the first " << n << " integers is " << sum;
}
130
* مثال 8-4 اعداد فاکتوریال
اعداد فاکتوریال 0! و 1! و 2! و 3! و … با استفاده از رابطه های بازگشتی زیر تعریف می شوند:
0! = 1 , n! = n(n-1)!
برای مثال، به ازای n = 1 در معادله دوم داریم:
1! = 1((1-1)!) = 1(0!) = 1(1) = 1
همچنین برای n = 2 داریم:
2! = 2((2-1)!) = 2(1!) = 2(1) = 2
و به ازای n = 3 داریم:
3! = 3((3-1)!) = 3(2!) = 3(2) = 6
131
برنامه زیر همه اعداد فاکتوریال را که از عدد داده شده کوچک ترند، چاپ می کند:
int main()
{ long bound;
cout << "Enter a positive integer: ";
cin >> bound;
cout << "Factorial numbers < " << bound << ":n1";
long f=1, i=1;
do
{ cout << ", " << f;
f *= ++i;
}
while (f < bound);
}
132
نحو دستورالعمل for به صورت زیر است:
for (initialization; condition; update) statement;
سه قسمت داخل پرانتز، حلقه را کنترل می کنند.
4 – دستور for
عبارت initialization برای اعلان یا مقداردهی اولیه به متغیر کنترل حلقه استفاده می شود.این عبارت اولین عبارتی است که ارزیابی می شود پیش از این که نوبت به تکرارها برسد.
عبارت condition برای تعیین این که آیا حلقه باید تکرار شود یا خیر به کار می رود. یعنی این عبارت، شرط کنترل حلقه است. اگر این شرط درست باشد دستور statement اجرا می شود.
عبارت updateبرای پیش بردن متغیر کنترل حلقه به کار می رود. این عبارت پس از اجرای statement ارزیابی می گردد.
133
بنابراین زنجیره وقایعی که تکرار را ایجاد می کنند عبارتند از:
1 – ارزیابی عبارت initialization
2 – بررسی شرط condition . اگر نادرست باشد، حلقه خاتمه می یابد.
3 – اجرای statement
4 – ارزیابی عبارت update
5 – تکرار گام های 2 تا 4
عبارت های initialization و condition و updateعبارت های اختیاری هستند. یعنی می توانیم آن ها را در حلقه ذکر نکنیم.
134
مثال 9-4 استفاده از حلقه for برای محاسبه مجموع اعداد صحیح متوالی
این برنامه همان تاثیر مثال 1-5 را دارد:
int main()
{ int n;
cout << "Enter a positive integer: ";
cin >> n;
long sum=0;
for (int i=1; i <= n; i++)
sum += I;
cout << "The sum of the first " << n << " integers is " << sum;
}
در C++ استاندارد وقتی یک متغیر کنترل درون یک حلقه for اعلان می شود (مانند i در مثال بالا) حوزه آن متغیر به همان حلقه for محدود می گردد. یعنی آن متغیر نمی تواند بیرون از آن حلقه استفاده شود.
نتیجه دیگر این است که می توان از نام مشابهی در خارج از حلقه for برای یک متغیر دیگر استفاده نمود.
135
مثال 12-4 یک حلقه for نزولی
برنامه زیر ده عدد صحیح مثبت را به ترتیب نزولی چاپ می کند:
int main()
{ for (int i=10; i > 0; i–)
cout << " " << i;
}
136
مثال 15-4 بیشتر از یک متغیر کنترل در حلقه for
حلقه for در برنامه زیر دو متغیر کنترل دارد:
int main()
{ for (int m=95, n=11, m%n > 0; m -= 3, n++)
cout << m << "%" << n << " = " << m%n << endl;
}
137
مثال 16-4 حلقه های for تودرتو
برنامه زیر یک جدول ضرب چاپ می کند:
#include <iomanip>
#include <iostream>
int main()
{ for (int x=1; x <= 10; x++)
{ for (int y=1; y <= 10; y++)
cout << setw(4) << x*y;
cout << endl;
}
}
138
دستور break یک دستور آشناست. قبلا از آن برای خاتمه دادن به دستور switch و همچنین حلقه های while و do..while استفاده کرده ایم. از این دستور برای خاتمه دادن به حلقه for نیز می توانیم استفاده کنیم.
دستور break در هر جایی درون حلقه می تواند جا بگیرد و در همان جا حلقه را خاتمه دهد.
5- دستور break
وقتی دستور break درون حلقه های تودرتو استفاده شود، فقط روی حلقه ای که مستقیما درون آن قرار گرفته تاثیر می گذارد.
حلقه های بیرونی بدون هیچ تغییری ادامه می یابند.
139
6- دستور continue
دستور break بقیه دستورهای درون بلوک حلقه را نادیده گرفته و به اولین دستور بیرون حلقه پرش می کند. دستور continue نیز شبیه همین است اما به جای این که حلقه را خاتمه دهد، اجرا را به تکرار بعدی حلقه منتقل می کند.
این دستور، ادامه چرخه فعلی را لغو کرده و اجرای دور بعدی حلقه را آغاز می کند.
140
مثال 19-4 استفاده از دستورهای break و continue
این برنامه کوچک، دستورهای break و continue را شرح می دهد:
int main()
{ int n = 1;
char c;
for( ; ;n++ )
{ cout << "nLoop no: " << n << endl;
cout << "Continue? <y|n> ";
cin >> c;
if (c = = 'y') continue;
break;
}
cout << "nTotal of loops: " << n;
}
141
دستورgoto نوع دیگری از دستورهای پرش است. مقصد این پرش توسط یک برچسب معین می شود.
برچسب شناسه ای است که جلوی آن علامت کولن( : ) می آید و جلوی یک دستور دیگر قرار می گیرد.
یک مزیت دستور goto این است که با استفاده از آن می توان از همه حلقه های تودرتو خارج شد و به مکان دلخواهی در برنامه پرش نمود.
7- دستور goto
142
مثال 20-4 استفاده از دستور goto برای خارج شدن از حلقه های تودرتو
int main()
{ const int N=5;
for (int i=0; i<N; i++)
{ for (int j=0; j<N; j++)
{ for (int k=0; k<N; k++)
if (i+j+k>N) goto esc;
else cout << i+j+k << " ";
cout << "* ";
}
esc: cout << "." << endl;
}
}
143
یکی از کاربردهای بسیار مهم رایانه ها، «شبیه سازی» سیستم های دنیای واقعی است. تحقیقات و توسعه های بسیار پیشرفته به این راهکار خیلی وابسته است. به وسیله شبیه سازی می توانیم رفتار سیستم های مختلف را مطالعه کنیم بدون این که لازم باشد واقعا آن ها را پیاده سازی نماییم. در شبیه سازی نیاز است «اعداد تصادفی» توسط رایانه ها تولید شود تا نادانسته های دنیای واقعی مدل سازی شود.
8- تولید اعداد شبه تصادفی
144
رایانه ها «ثابت کار» هستند یعنی با دادن داده های مشابه به رایانه های مشابه، همیشه خروجی یکسان تولید می شود. با وجود این می توان اعدادی تولید کرد که به ظاهر تصادفی هستند؛ اعدادی که به طور یکنواخت در یک محدوده خاص گسترده اند و برای هیچ کدام الگوی مشخصی وجود ندارد. چنین اعدادی را «اعداد شبه تصادفی» می نامیم.
145
مثال 22-4 تولید اعداد شبه تصادفی
این برنامه از تابع rand() برای تولید اعداد شبه تصادفی استفاده می کند:
#include<cstdlib>//defines the rand() and RAND_MAX
#include <iostream>
int main()
{ // prints pseudo-random numbers:
for (int i = 0; i < 8; i++)
cout << rand() << endl;
cout << "RAND_MAX = " << RAND_MAX << endl;
}
هر بار که برنامه بالا اجرا شود، رایانه هشت عدد صحیح unsigned تولید می کند که به طور یکنواخت در فاصله 0 تا RAND_MAX گسترده شده اند. RAND_MAX در این رایانه برابر با 2,147,483,647 است.
146
هر عدد شبه تصادفی از روی عدد قبلی خود ساخته می شود.
اولین عدد شبه تصادفی از روی یک مقدار داخلی که «هسته» گفته می شود ایجاد می گردد.
هر دفعه که برنامه اجرا شود، هسته با یک مقدار پیش فرض بارگذاری می شود.
برای حذف این اثر نامطلوب که از تصادفی بودن اعداد می کاهد، می توانیم با استفاده از تابع ()srand خودمان مقدار هسته را انتخاب کنیم.
147
#include <cstdlib> // defines the rand() and srand()
#include <iostream>
int main()
{ // prints pseudo-random numbers:
unsigned seed;
cout << "Enter seed: ";
cin >> seed;
srand(seed); // initializes the seed
for (int i = 0; i < 8; i++)
cout << rand() << endl;
}
مثال 23-4 کارگذاری هسته به طور محاوره ای
این برنامه مانند برنامه مثال 22-4 است بجز این که می توان هسته تولیدکننده اعداد تصادفی را به شکل محاوره ای وارد نمود:
148
پایان جلسه چهارم
149
جلسه پنجم
« توابع»
150
آنچه در این جلسه می خوانید:
1- توابع کتابخانه ای C++ استاندارد
2- توابع ساخت کاربر
3- برنامه آزمون
4- اعلان ها و تعاریف تابع
5- کامپایل جداگانه توابع
6- متغیرهای محلی، توابع محلی
›››
151
7- تابع void
8 – توابع بولی
9- توابع ورودی/خروجی (I/O)
10- ارسال به طریق ارجاع (آدرس)
11- ارسال از طریق ارجاع ثابت
12-توابع بی واسطه
›››
152
13- چندشکلی توابع
14- تابع main()
15- آرگومان های پیش فرض
153
هدف کلی:
شناخت و معرفی توابع و مزایای استفاده از تابع در برنامه ها
هدف های رفتاری:
انتظار می رود پس از پایان این جلسه بتوانید:
– اهمیت توابع و مزیت استفاده از آن ها را بیان کنید.
– «اعلان» و «تعریف» تابع را بدانید و خودتان توابعی را ایجاد کنید.
– «برنامه آزمون» را تعریف کرده و دلیل استفاده از آن را بیان نمایید.
– مفهوم «آرگومان» را بدانید.
– تفاوت ارسال به طریق «ارجاع» و ارسال به طریق «مقدار» و ارسال به طریق «ارجاع ثابت» را بیان کنید و شکل استفاده از هر یک را بدانید.
›››
154
– «تابع بی واسطه» را شناخته و نحوه معرفی آن را بدانید.
– چندشکلی توابع را تعریف کنید و شیوه آن را بدانید.
– طریقه به کارگیری آرگومان های پیش فرض را بدانید.
– فرق بین تابع void با سایر توابع را بدانید.
155
1-مقدمه
برنامه های واقعی و تجاری بسیار بزرگ تر از برنامه هایی هستند که تاکنون بررسی کردیم. برای این که برنامه های بزرگ قابل مدیریت باشند، برنامه نویسان این برنامه ها را به زیربرنامه هایی بخش بندی می کنند. این زیربرنامه ها «تابع» نامیده می شوند. توابع را می توان به طور جداگانه کامپایل و آزمایش نمود و در برنامه های مختلف دوباره از آن ها استفاده کرد.
156
2- توابع کتابخانه ای C++ استاندارد
«کتابخانه C++ استاندارد» مجموعه ای است که شامل توابع از پیش تعریف شده و سایر عناصر برنامه است . این توابع و عناصر از طریق «سرفایل ها» قابل دستیابی اند.
قبلا برخی از آن ها را استفاده کرده ایم : ثابت INT_MAX که در <climits> تعریف شده ، تابع ()sqrt که در <cmath> تعریف شده است و… .
157
تابع جذر sqrt()
ریشه دوم یک عدد مثبت ، جذر آن عدد است.
تابع مانند یک برنامه کامل، دارای روند ورودی – پردازش – خروجی است هرچند که پردازش، مرحله ای پنهان است. یعنی نمی دانیم که تابع روی عدد 2 چه اعمالی انجام می دهد که 41421/1 حاصل می شود.
158
برنامه ساده زیر، تابع از پیش تعریف شده جذر را به کار می گیرد:
#include <cmath> // defines the sqrt() function
#include <iostream> // defines the cout object using namespace std;
int main()
{ //tests the sqrt() function:
for (int x=0; x < 6; x++)
cout << "t" << x << "t" << sqrt(x) << endl;
}
برای اجرای یک تابع مانند تابع sqrt() کافی است نام آن تابع به صورت یک متغیر در دستورالعمل مورد نظر استفاده شود، مانند زیر:
y=sqrt(x);
159
این کار «فراخوانی تابع» یا «احضار تابع» گفته می شود. بنابراین وقتی کد sqrt(x) اجرا شود، تابع sqrt() فراخوانی می گردد. عبارت x درون پرانتز «آرگومان» یا «پارامتر واقعی» فراخوانی نامیده می شود. در چنین حالتی می گوییم که x توسط «مقدار» به تابع فرستاده می شود. لذا وقتی x=3 است، با اجرای کد sqrt(x) تابع sqrt() فراخوانی شده و مقدار 3 به آن فرستاده می شود. تابع مذکور نیز حاصل 1.73205 را به عنوان پاسخ برمی گرداند…
160
… این فرایند در نمودار زیر نشان داده شده.
3
1.73205
x
y
Main()
double
int
Sqrt()
3
1.73205
متغیرهای x و y در تابع main() تعریف شده اند. مقدار x که برابر با 3 است به تابع sqrt() فرستاده می شود و این تابع مقدار 1.73205 را به تابع main() برمی گرداند. جعبه ای که تابع sqrt() را نشان می دهد به رنگ تیره است، به این معنا که فرایند داخلی و نحوه کار آن قابل رویت نیست.
161
مثال 2-5 آزمایش یک رابطه مثلثاتی
این برنامه هم از سرفایل <cmath> استفاده می کند. هدف این است که صحت رابطه Sin2x=2SinxCosx به شکل تجربی بررسی شود.
int main()
{ for (float x=0; x < 2; x += 0.2)
cout << x << "tt" << sin(2*x) << "t“
<< 2*sin(x)*cos(x) << endl;
}
162
0 0 0
0.2 0.389418 0.389418
0.4 0.717356 0.717356
0.6 0.932039 0.932039
0.8 0.999574 0.999574
1 0.909297 0.909297
1.2 0.675463 0.675463
1.4 0.334988 0.334988
1.6 -0.0583744 -0.0583744
1.8 -0.442521 -0.442521
برنامه مقدار x را در ستون اول، مقدار Sin2x را در ستون دوم و مقدار 2SinxCosx را در ستون سوم چاپ می کند.
خروجی برنامه:
خروجی نشان می دهد که برای هر مقدار آزمایشی x، مقدار Sin2x با مقدار 2SinxCosx برابر است.
163
بیشتر توابع معروف ریاضی که در ماشین حساب ها هم وجود دارد در سرفایل <cmath> تعریف شده است. بعضی از این توابع در جدول زیر نشان داده شده:
164
165
توجه داشته باشید که هر تابع ریاضی یک مقدار از نوع double را برمی گرداند. اگر یک نوع صحیح به تابع فرستاده شود، قبل از این که تابع آن را پردازش کند، مقدارش را به نوع double ارتقا می دهد.
166
بعضی از سرفایل های کتابخانه C++ استاندارد که کاربرد بیشتری دارند در جدول زیر آمده است:
این سرفایل ها از کتابخانه C استاندارد گرفته شده اند. استفاده از آن ها شبیه استفاده از سرفایل های C++ استاندارد (مانند <iostream> ) است. برای مثال اگر بخواهیم تابع اعداد تصادفی rand() را از سرفایل <cstdlib> به کار ببریم، باید دستور پیش پردازنده زیر را به ابتدای فایل برنامه اصلی اضافه کنیم:
#include <cstdlib>
167
3- توابع ساخت کاربر
گرچه توابع بسیار متنوعی در کتابخانه C++ استاندارد وجود دارد ولی این توابع برای بیشتر وظایف برنامه نویسی کافی نیستند. علاوه بر این برنامه نویسان دوست دارند خودشان بتوانند توابعی را بسازند و استفاده نمایند.
168
مثال 3-5 تابع cube()
یک مثال ساده از توابع ساخت کاربر:
int cube(int x)
{ // returns cube of x:
return x*x*x;
}
این تابع، مکعب یک عدد صحیح ارسالی به آن را برمی گرداند. بنابراین فراخوانی cube(2) مقدار 8 را برمی گرداند.
169
نوع بازگشتی تابع cube() که در بالا تعریف شد، int است. نام آن cube می باشد و یک پارامتر از نوع int به نام x دارد. یعنی تابع cube() یک مقدار از نوع int می گیرد و پاسخی از نوع int تحویل می دهد.
بدنه تابع، یک بلوک کد است که در ادامه عنوان آن می آید. بدنه شامل دستوراتی است که باید انجام شود تا نتیجه مورد نظر به دست آید. بدنه شامل دستور return است که پاسخ نهایی را به مکان فراخوانی تابع برمی گرداند.
یک تابع ساخت کاربر دو قسمت دارد:
1-عنوان 2- بدنه.
عنوان یک تابع به صورت زیر است:
(فهرست پارامترها) نام نوع بازگشتی
مثال:
int cube(int x)
{
… بدنه تابع
}
170
دستور return دو وظیفه عمده دارد. اول این که اجرای تابع را خاتمه می دهد و دوم این که مقدار نهایی را به برنامه فراخوان باز می گرداند. دستور return به شکل زیر استفاده می شود:
return expression;
به جای expression هر عبارتی قرار می گیرد که بتوان مقدار آن را به یک متغیر تخصیص داد. نوع آن عبارت باید با نوع بازگشتی تابع یکی باشد.
عبارت int main() که در همه برنامه ها استفاده کرده ایم یک تابع به نام «تابع اصلی» را تعریف می کند. نوع بازگشتی این تابع از نوع int است. نام آن main است و فهرست پارامترهای آن خالی است؛ یعنی هیچ پارامتری ندارد.
171
وقتی یک تابع مورد نیاز را ایجاد کردید، فورا باید آن تابع را با یک برنامه ساده امتحان کنید. چنین برنامه ای برنامه آزمون نامیده می شود.
4- برنامه آزمون
برنامه آزمون یک برنامه موقتی است که باید «سریع و کثیف» باشد؛
یعنی:
لازم نیست در آن تمام ظرافت های برنامه نویسی – مثل پیغام های خروجی، برچسب ها و راهنماهای خوانا – را لحاظ کنید.
تنها هدف این برنامه، امتحان کردن تابع و بررسی صحت کار آن است.
172
مثال 4-5 یک برنامه آزمون برای تابع cube()
کد زیر شامل تابع cube() و برنامه آزمون آن است:
int cube(int x)
{ // returns cube of x:
return x*x*x;
}
int main()
{ // tests the cube() function:
int n=1;
while (n != 0)
{ cin >> n;
cout << "tcube(" << n << ") = " << cube(n) << endl; }}
برنامه حاضر اعداد صحیح را از ورودی می گیرد و مکعب آن ها را چاپ می کند تا این که کاربر مقدار 0 را وارد کند.
173
هر عدد صحیحی که خوانده می شود، با استفاده از کد cube(n) به تابع cube() فرستاده می شود. مقدار بازگشتی از تابع، جایگزین عبارت cube(n) گشته و با استفاده از cout در خروجی چاپ می شود.
می توان رابطه بین تابع main() و تابع cube() را شبیه این شکل تصور نمود:
دقت کنید که تابع cube() در بالای تابع main() تعریف شده زیرا قبل از این که تابعcube() در تابع main() به کار رود، کامپایلر C++ باید در باره آن اطلاع حاصل کند.
174
مثال 5-5 یک برنامه آزمون برای تابع max()
تابع زیر دو پارامتر دارد. این تابع از دو مقدار فرستاده شده به آن، مقدار بزرگ تر را برمی گرداند:
int max(int x, int y)
{ // returns larger of the two given integers:
int z;
z = (x > y) ? x : y ;
return z;
}
int main()
{ int m, n;
do
{ cin >> m >> n;
cout << "tmax(" << m << "," << n << ") = " << max(m,n) << endl; }
while (m != 0);}
175
توابع می توانند بیش از یک دستور return داشته باشند. مثلا تابع max() را مانند این نیز می توانستیم بنویسیم:
int max(int x, int y)
{ // returns larger of the two given integers:
if (x < y) return y;
else return x;
}
در این کد هر دستور return که زودتر اجرا شود مقدار مربوطه اش را بازگشت داده و تابع را خاتمه می دهد.
دستور return نوعی دستور پرش است (شبیه دستور break ) زیرا اجرا را به بیرون از تابع هدایت می کند. اگرچه معمولا return در انتهای تابع قرار می گیرد، می توان آن را در هر نقطه دیگری از تابع قرار داد.
176
5- اعلان ها و تعاریف تابع
به دو روش میتوان توابع را تعریف نمود:
1-توابع قبل از تابع main() به طور کامل با بدنه مربوطه آورده شوند.
2-راه دیگری که بیشتر رواج دارد این گونه است که ابتدا تابع اعلان شود، سپس متن برنامه اصلیmain() بیاید، پس از آن تعریف کامل تابع قرار بگیرد.
177
اعلان تابع با تعریف تابع تفاوت دارد.
اعلان تابع، فقط عنوان تابع است که یک سمیکولن در انتهای آن قرار دارد.
تعریف تابع، متن کامل تابع است که هم شامل عنوان است و هم شامل بدنه.
اعلان تابع شبیه اعلان متغیرهاست.
یک متغیر قبل از این که به کار گرفته شود باید اعلان شود. تابع هم همین طور است با این فرق که متغیر را در هر جایی از برنامه می توان اعلان کرد اما تابع را باید قبل از برنامه اصلی اعلان نمود.
178
همین ها برای کامپایلر کافی است تا بتواند کامپایل برنامه را آغاز کند. سپس در زمان اجرا به تعریف بدنه تابع نیز احتیاج می شود که این بدنه در انتهای برنامه و پس از تابع main() قرار می گیرد.
در اعلان تابع فقط بیان می شود که نوع بازگشتی تابع چیست، نام تابع چیست و نوع پارامترهای تابع چیست.
179
فرق بین «آرگومان» و «پارامتر» :
پارامترها متغیرهایی هستند که در فهرست پارامتر یک تابع نام برده می شوند.
پارامترها متغیرهای محلی برای تابع محسوب می شوند؛ یعنی فقط در طول اجرای تابع وجود دارند.
آرگومان ها متغیرهایی هستند که از برنامه اصلی به تابع فرستاده می شوند.
180
int max(int,int);
int main()
{ int m, n;
do
{ cin >> m >> n;
cout << "tmax(" << m << "," << n << ") = "
<< max(m,n) << endl; }
while (m != 0);}
int max(int x, int y)
{ if (x < y) return y;
else return x;}
مثال 6-5 تابعmax() با اعلان جدا از تعریف آن
این برنامه همان برنامه آزمون تابع max() در مثال 5-6 است. اما این جا اعلان تابع بالای تابع اصلی ظاهر شده و تعریف تابع بعد از برنامه اصلی آمده است:
توجه کنید که پارامترهای x و y در بخش عنوان تعریف تابع آمده اند (طبق معمول) ولی در اعلان تابع وجود ندارند.
181
اغلب این طور است که تعریف و بدنه توابع در فایل های جداگانه ای قرار می گیرد. این فایل ها به طور مستقل کامپایل1 می شوند و سپس به برنامه اصلی که آن توابع را به کار می گیرد الصاق2 می شوند.
6- کامپایل جداگانه توابع
توابع کتابخانه C++ استاندارد به همین شکل پیاده سازی شده اند و هنگامی که یکی از آن توابع را در برنامه هایتان به کار می برید باید با دستور راهنمای پیش پردازنده، فایل آن توابع را به برنامه تان ضمیمه کنید.
این کار چند مزیت دارد:
182
1- اولین مزیت «مخفی سازی اطلاعات» است.
2-مزیت دیگر این است که توابع مورد نیاز را می توان قبل از این که برنامه اصلی نوشته شود، جداگانه آزمایش نمود.
3-سومین مزیت این است که در هر زمانی به راحتی می توان تعریف توابع را عوض کرد بدون این که لازم باشد برنامه اصلی تغییر یابد.
4-چهارمین مزیت هم این است که می توانید یک بار یک تابع را کامپایل و ذخیره کنید و از آن پس در برنامه های مختلفی از همان تابع استفاده ببرید.
183
تابع max() را به خاطر بیاورید. برای این که این تابع را در فایل جداگانه ای قرار دهیم، تعریف آن را در فایلی به نام max.cpp ذخیره می کنیم. فایل max.cpp شامل کد زیر است:
184
حال کافی است عبارت:#include <test.cpp> را به اول برنامه اصلی وقبل ازmain() اضافه کنیم:
#include <test.cpp>
int main()
{ // tests the max() function:
int m, n;
do
{ cin >> m >> n;
cout << "tmax(" << m << "," << n << ") = "
<< max(m,n) << endl;
}
while (m != 0);}
185
نحوه کامپایل کردن فایل ها و الصاق آن ها به یکدیگر به نوع سیستم عامل و نوع کامپایلر بستگی دارد. در سیستم عامل ویندوز معمولا توابع را در فایل هایی از نوع DLL کامپایل و ذخیره می کنند و سپس این فایل را در برنامه اصلی احضار می نمایند. فایل های DLL را به دو طریق ایستا و پویا می توان مورد استفاده قرار داد. برای آشنایی بیشتر با فایل های DLL به مرجع ویندوز و کامپایلرهای C++ مراجعه کنید.
186
6- متغیرهای محلی، توابع محلی
متغیر محلی، متغیری است که در داخل یک بلوک اعلان گردد. این گونه متغیرها فقط در داخل همان بلوکی که اعلان می شوند قابل دستیابی هستند.
چون بدنه تابع، خودش یک بلوک است پس متغیرهای اعلان شده در یک تابع متغیرهای محلی برای آن تابع هستند.
این متغیرها فقط تا وقتی که تابع در حال کار است وجود دارند.
پارامترهای تابع نیز متغیرهای محلی محسوب می شوند.
187
* مثال 7-5 تابع فاکتوریل
اعداد فاکتوریل را در مثال 8-5 دیدیم. فاکتوریل عدد صحیح n برابر است با:
n! = n(n-1)(n-2)..(3)(2)(1)
تابع زیر، فاکتوریل عدد n را محاسبه می کند :
long fact(int n)
{ //returns n! = n*(n-1)*(n-2)*…*(2)*(1)
if (n < 0) return 0;
int f = 1;
while (n > 1)
f *= n–;
return f;
}
این تابع دو متغیر محلی دارد: n و f پارامتر n یک متغیر محلی است زیرا در فهرست پارامترهای تابع اعلان شده و متغیر f نیز محلی است زیرا درون بدنه تابع اعلان شده است.
188
همان گونه که متغیرها می توانند محلی باشند، توابع نیز می توانند محلی باشند.
یک تابع محلی تابعی است که درون یک تابع دیگر به کار رود. با استفاده از چند تابع ساده و ترکیب آن ها می توان توابع پیچیده تری ساخت. به مثال زیر نگاه کنید.
تابع محلی
در ریاضیات، تابع جایگشت را با p(n,k) نشان می دهند. این تابع بیان می کند که به چند طریق می توان k عنصر دلخواه از یک مجموعه n عنصری را کنار یکدیگر قرار داد. برای این محاسبه از رابطه زیر استفاده می شود:
189
پس به 12 طریق می توانیم دو عنصر دلخواه از یک مجموعه چهار عنصری را کنار هم بچینیم. برای دو عنصر از مجموعه {1, 2, 3, 4} حالت های ممکن عبارت است از:
12, 13, 14, 21, 23, 24, 31, 32, 34, 41, 42, 43
کد زیر تابع جایگشت را پیاده سازی می کند:
long perm(int n, int k)
{// returns P(n,k), the number of the permutations of k from n:
if (n < 0) || k < 0 || k > n) return 0;
return fact(n)/fact(n-k);
}
این تابع، خود از تابع دیگری که همان تابع فاکتوریل است استفاده کرده است.
شرط به کار رفته در دستور if برای محدود کردن حالت های غیر ممکن استفاده شده است. در این حالت ها، تابع مقدار 0 را برمی گرداند تا نشان دهد که یک ورودی اشتباه وجود داشته است.
190
برنامه آزمون برای تابع perm() در ادامه آمده است:
long perm(int,int);
// returns P(n,k), the number of permutations of k from n:
int main()
{ // tests the perm() function:
for (int i = -1; i < 8; i++)
{ for (int j= -1; j <= i+1; j++)
cout << " " << perm(i,j);
cout << endl; }
}
0 0
0 1 0
0 1 1 0
0 1 2 2 0
0 1 3 6 6 0
0 1 4 12 24 24 0
0 1 5 20 60 120 120 0
0 1 6 30 120 360 720 720 0
0 1 7 42 210 840 2520 5040 5040 0
191
7- تابع void
لازم نیست یک تابع حتما مقداری را برگرداند. در C++ برای مشخص کردن چنین توابعی از کلمه کلیدی void به عنوان نوع بازگشتی تابع استفاده می کنند
یک تابع void تابعی است که هیچ مقدار بازگشتی ندارد.
از آن جا که یک تابع void مقداری را برنمی گرداند، نیازی به دستور return نیست ولی اگر قرار باشد این دستور را در تابع void قرار دهیم، باید آن را به شکل تنها استفاده کنیم بدون این که بعد از کلمه return هیچ چیز دیگری بیاید:
return;
در این حالت دستور return فقط تابع را خاتمه می دهد.
192
8- توابع بولی
در بسیاری از اوقات لازم است در برنامه، شرطی بررسی شود.
اگر بررسی این شرط به دستورات زیادی نیاز داشته باشد، بهتر است که یک تابع این بررسی را انجام دهد. این کار مخصوصا هنگامی که از حلقه ها استفاده می شود بسیار مفید است.
توابع بولی فقط دو مقدار را برمی گردانند: true یا false .
اسم توابع بولی را معمولا به شکل سوالی انتخاب می کنند زیرا توابع بولی همیشه به یک سوال مفروض پاسخ بلی یا خیر می دهند.
193
مثال 10-5 تابعی که اول بودن اعداد را بررسی می کند
کد زیر یک تابع بولی است که تشخیص می دهد آیا عدد صحیح ارسال شده به آن، اول است یا خیر:
bool isPrime(int n)
{ // returns true if n is prime, false otherwise:
float sqrtn = sqrt(n);
if (n < 2) return false; // 0 and 1 are not primes
if (n < 4) return true; // 2 and 3 are the first primes
if (n%2 == 0) return false; // 2 is the only even prime
for (int d=3; d <= sqrtn; d += 2)
if (n%d == 0) return false; // n has a nontrivial divisor
return true; // n has no nontrivial divisors
}
194
9- توابع ورودی/خروجی (I/O)
بخش هایی از برنامه که به جزییات دست و پا گیر می پردازد و خیلی به هدف اصلی برنامه مربوط نیست را می توان به توابع سپرد. در چنین شرایطی سودمندی توابع محسوس تر می شود.
فرض کنید نرم افزاری برای سیستم آموزشی دانشگاه طراحی کرده اید که سوابق تحصیلی دانشجویان را نگه می دارد. در این نرم افزار لازم است که سن دانشجو به عنوان یکی از اطلاعات پرونده دانشجو وارد شود. اگر وظیفه دریافت سن را به عهده یک تابع بگذارید، می توانید جزییاتی از قبیل کنترل ورودی معتبر، یافتن سن از روی تاریخ تولد و … را در این تابع پیاده سازی کنید بدون این که از مسیر برنامه اصلی منحرف شوید.
195
قبلا نمونه ای از توابع خروجی را دیدیم. تابع PrintDate() در مثال 9-5 هیچ چیزی به برنامه اصلی برنمی گرداند و فقط برای چاپ نتایج به کار می رود.
این تابع نمونه ای از توابع خروجی است؛ یعنی توابعی که فقط برای چاپ نتایج به کار می روند و هیچ مقدار بازگشتی ندارند.
توابع ورودی نیز به همین روش کار می کنند اما در جهت معکوس. یعنی توابع ورودی فقط برای دریافت ورودی و ارسال آن به برنامه اصلی به کار می روند و هیچ پارامتری ندارند.
مثال بعد یک تابع ورودی را نشان می دهد.
196
مثال 11-5 تابعی برای دریافت سن کاربر
تابع ساده زیر، سن کاربر را درخواست می کند و مقدار دریافت شده را به برنامه اصلی می فرستد. این تابع تقریبا هوشمند است و هر عدد صحیح ورودی غیر منطقی را رد می کند و به طور مکرر درخواست ورودی معتبر می کند تا این که یک عدد صحیح در محدوده 7 تا 120 دریافت دارد:
int age()
{ // prompts the user to input his/her age and returns that value:
int n;
while (true)
{ cout << "How old are you: ";
cin >> n;
if (n < 0) cout << "atYour age could not
be negative.";
else if (n > 120) cout << "atYou could not
be over 120.";
else return n;
cout << "ntTry again.n";
}
}
197
یک برنامه آزمون و خروجی حاصل از آن در ادامه آمده است:
int age()
int main()
{ // tests the age() function:
int a = age();
cout << "nYou are " << a << " years old.n";
}
How old are you? 125
You could not be over 120
Try again.
How old are you? -3
Your age could not be negative
Try again.
How old are you? 99
You are 99 years old.
198
تا این لحظه تمام پارامترهایی که در توابع دیدیم به طریق مقدار ارسال شده اند. یعنی ابتدا مقدار متغیری که در فراخوانی تابع ذکر شده برآورد می شود و سپس این مقدار به پارامترهای محلی تابع فرستاده می شود.
مثلا در فراخوانی cube(x) ابتدا مقدار x برآورد شده و سپس این مقدار به متغیر محلی n در تابع فرستاده می شود و پس از آن تابع کار خویش را آغاز می کند. در طی اجرای تابع ممکن است مقدار n تغییر کند اما چون n محلی است هیچ تغییری روی مقدار x نمی گذارد.
199
پس خود x به تابع نمی رود بلکه مقدار آن درون تابع کپی می شود.
تغییر دادن این مقدار کپی شده درون تابع هیچ تاثیری بر x اصلی ندارد. به این ترتیب تابع می تواند مقدار x را بخواند اما نمی تواند مقدار x را تغییر دهد.
به همین دلیل به x یک پارامتر «فقط خواندنی» می گویند.
وقتی ارسال به وسیله مقدار باشد، هنگام فراخوانی تابع می توان از عبارات استفاده کرد.
مثلا تابع cube() را می توان به صورتcube(2*x-3) فراخوانی کرد یا به شکل cube(2*sqrt(x)-cube(3)) فراخوانی نمود. در هر یک از این حالات، عبارت درون پرانتز به شکل یک مقدار تکی برآورد شده و حاصل آن مقدار به تابع فرستاده می شود.
200
10- ارسال به طریق ارجاع (آدرس)
ارسال به طریق مقدار باعث می شود که متغیرهای برنامه اصلی از تغییرات ناخواسته در توابع مصون بمانند.
اما گاهی اوقات عمدا می خواهیم این اتفاق رخ دهد. یعنی می خواهیم که تابع بتواند محتویات متغیر فرستاده شده به آن را دست کاری کند. در این حالت از ارسال به طریق ارجاع استفاده می کنیم.
201
برای این که مشخص کنیم یک پارامتر به طریق ارجاع ارسال می شود، علامت را به نوع پارامتر در فهرست پارامترهای تابع اضافه می کنیم. این باعث می شود که تابع به جای این که یک کپی محلی از آن آرگومان ایجاد کند، خود آرگومان محلی را به کار بگیرد.
به این ترتیب تابع هم می تواند مقدار آرگومان فرستاده شده را بخواند و هم می تواند مقدار آن را تغییر دهد. در این حالت آن پارامتر یک پارامتر «خواندنی-نوشتنی» خواهد بود.
&
202
* مثال 12-5 تابع swap()
تابع کوچک زیر در مرتب کردن داده ها کاربرد فراوان دارد:
void swap(float& x, float& y)
{ // exchanges the values of x and y:
float temp = x;
x = y;
y = temp;
}
هر تغییری که روی پارامتر خواندنی-نوشتنی در تابع صورت گیرد به طور مستقیم روی متغیر برنامه اصلی اعمال می شود. به مثال زیر نگاه کنید.
هدف این تابع جابجا کردن دو عنصری است که به آن فرستاده می شوند. برای این منظور پارامترهای x و y به صورت پارامترهای ارجاع تعریف شده اند:
float& x, float& y
203
عملگر ارجاع & موجب می شود که به جای x و y آرگومان های ارسالی قرار بگیرند. برنامه آزمون و اجرای آزمایشی آن در زیر آمده است:
void swap(float&, float&)
// exchanges the values of x and y:
int main()
{ // tests the swap() function:
float a = 55.5, b = 88.8;
cout << "a = " << a << ", b = " << b << endl;
swap(a,b);
cout << "a = " << a << ", b = " << b << endl;
}
a = 55.5, b = 88.8
a = 88.8, b = 55.5
204
وقتی فراخوانی swap(a,b) اجرا می شود، x به a اشاره می کند و y به b. سپس متغیر محلی temp اعلان می شود و مقدار x (که همان a است) درون آن قرار می گیرد. پس از آن مقدار y (که همان b است) درون x (یعنی a) قرار می گیرد و آنگاه مقدار temp درون y (یعنی b) قرار داده می شود. نتیجه نهایی این است که مقادیر a و b با یکدیگر جابجا می شوند. شکل مقابل نشان می دهد که چطور این جابجایی رخ می دهد:
هنگام فراخوانی تابع swap(a,b)
بعد از بازگشت
swap()
205
به اعلان تابع swap() دقت کنید:
void swap(float&, float&)
این اعلان شامل عملگر ارجاع & برای هر پارامتر است .
برنامه نویسان c عادت دارند که عملگر ارجاع & را به عنوان پیشوند نام متغیر استفاده کنند (مثلfloat &x) در C++ فرض می کنیم عملگر ارجاع & پسوند نوع است (مثل float& x)
به هر حال کامپایلر هیچ فرقی بین این دو اعلان نمی گذارد و شکل نوشتن عملگر ارجاع کاملا اختیاری و سلیقه ای است.
206
مثال 13-5 ارسال به طریق مقدار و ارسال به طریق ارجاع
این برنامه، تفاوت بین ارسال به طریق مقدار و ارسال به طریق ارجاع را نشان می دهد:
void f(int,int&);
int main()
{ int a = 22, b = 44;
cout << "a = " << a << ", b = " << b << endl;
f(a,b);
cout << "a = " << a << ", b = " << b << endl;
f(2*a-3,b);
cout << "a = " << a << ", b = " << b << endl;}
void f(int x , int& y)
{ x = 88;
y = 99;}
a = 22, b = 44
a = 22, b = 99
a = 22, b = 99
تابع f() دو پارامتر دارد که اولی به طریق مقدار و دومی به طریق ارجاع ارسال می شود. فراخوانی f(a,b) باعث می شود که a از طریق مقدار به x ارسال شود و b از طریق ارجاع به y فرستاده شود.
207
شکل زیر نحوه کار تابع f() را نشان می دهد.
208
در جدول زیر خلاصه تفاوت های بین ارسال از طریق مقدار و ارسال از طریق ارجاع آمده است.
209
یکی از مواقعی که پارامترهای ارجاع مورد نیاز هستند جایی است که تابع باید بیش از یک مقدار را بازگرداند.
دستور return فقط می تواند یک مقدار را برگرداند.
بنابراین اگر باید بیش از یک مقدار برگشت داده شود، این کار را پارامترهای ارجاع انجام می دهند.
210
* مثال 14-5 بازگشت بیشتر از یک مقدار
تابع زیر از طریق دو پارامتر ارجاع، دو مقدار را بازمی گرداند: area و circumference (محیط و مساحت ) برای دایره ای که شعاع آن عدد مفروض r است:
void ComputeCircle(double& area, double& circumference, double r)
{ // returns the area and circumference of a circle with radius r:
const double PI = 3.141592653589793;
area = PI*r*r;
circumference = 2*PI*r;
}
211
برنامه آزمون تابع فوق و یک اجرای آزمایشی آن در شکل زیر نشان داده شده است:
void ComputerCircle(double&, double&, double);
// returns the area and circumference of a circle with radius r;
int main()
{ // tests the ComputeCircle() function:
double r, a, c;
cout << "Enter radius: ";
cin >> r;
ComputeCircle(a, c, r);
cout << "area = " << a << ", circumference = "
<< c << endl;}
212
12- ارسال از طریق ارجاع ثابت
ارسال پارامترها به طریق ارجاع دو خاصیت مهم دارد:
اول این که تابع می تواند روی آرگومان واقعی تغییراتی بدهد
دوم این که از اشغال بی مورد حافظه جلوگیری می شود.
روش دیگری نیز برای ارسال آرگومان وجود دارد:
ارسال از طریق ارجاع ثابت. این روش مانند ارسال از طریق ارجاع است با این فرق که تابع نمی تواند محتویات پارامتر ارجاع را دست کاری نماید و فقط اجازه خواندن آن را دارد.
برای این که پارامتری را از نوع ارجاع ثابت اعلان کنیم باید عبارت const را به ابتدای اعلان آن اضافه نماییم.
213
مثال 15-5 ارسال از طریق ارجاع ثابت
سه طریقه ارسال پارامتر در تابع زیر به کار رفته است:
void f(int x, int& y, const int& z)
{ x += z;
y += z;
cout << "x = " << x << ", y = " << y << ", z = "
<< z << endl;
}
در تابع فوق اولین پارامتر یعنی x از طریق مقدار ارسال می شود، دومین پارامتر یعنی y از طریق ارجاع و سومین پارامتر نیز از طریق ارجاع ثابت.
214
برنامه آزمون و یک اجرای آزمایشی از مثال قبل:
void f(int, int&, const int&);
int main()
{ // tests the f() function:
int a = 22, b = 33, c = 44;
cout << "a = " << a << ", b = " << b << ", c = "
<< c << endl;
f(a,b,c);
cout << "a = " << a << ", b = " << b << ", c = "
<< c << endl;
f(2*a-3,b,c);
cout << "a = " << a << ", b = " << b << ", c = "
<< c << endl;
}
a = 22, b = 33, c = 44
x = 66, y = 77, z = 44
a = 22, b = 77, c = 44
x = 85, y = 121, z = 44
a = 22, b = 121, c = 44
تابع فوق پارامترهای x و y را می تواند تغییر دهد ولی قادر نیست پارامتر z را تغییر دهد. تغییراتی که روی x صورت می گیرد اثری روی آرگومان a نخواهد داشت زیرا a از طریق مقدار به تابع ارسال شده. تغییراتی که روی y صورت می گیرد روی آرگومان b هم تاثیر می گذارد زیرا b از طریق ارجاع به تابع فرستاده شده.
215
ارسال به طریق ارجاع ثابت بیشتر برای توابعی استفاده می شود که عناصر بزرگ را ویرایش می کنند مثل آرایه ها یا نمونه کلاس ها که در جلسه های بعدی توضیح آن ها آمده است. عناصری که از انواع اصلی هستند (مثل int یا float) به طریق مقدار ارسال می شوند به شرطی که قرار نباشد تابع محتویات آن ها را دست کاری کند.
216
13- توابع بی واسطه
تابعی که به شکل بی واسطه تعریف می شود، ظاهری شبیه به توابع معمولی دارد با این فرق که عبارت inline در اعلان و تعریف آن قید شده است.
مثال 16-5 تابع cube() به شکل بی واسطه
این همان تابع cube() مثال 3-5 است :
inline int cube(int x)
{ // returns cube of x:
return x*x*x;
}
تنها تفاوت این است که کلمه کلیدی inline در ابتدای عنوان تابع ذکر شده. این عبارت به کامپایلر می گوید که در برنامه به جای cube(n) کد واقعی (n)*(n)*(n) را قرار دهد.
217
. به برنامه آزمون زیر نگاه کنید:
int main()
{ // tests the cube() function:
cout << cube(4) << endl;
int x, y;
cin >> x;
y = cube(2*x-3);}
این برنامه هنگام کامپایل به شکل زیر درمی آید، گویی اصلا تابعی وجود نداشته:
int main()
{ // tests the cube() function:
cout << (4) * (4) * (4) << endl;
int x, y;
cin >> x;
y = (2*x-3) * (2*x-3) * (2*x-3);}
وقتی کامپایلر کد واقعی تابع را جایگزین فراخوانی آن می کند، می گوییم که تابع بی واسطه، باز می شود.
احتیاط:
استفاده از توابع بی واسطه می تواند اثرات منفی داشته باشد. مثلا اگر یک تابع بی واسطه دارای 40 خط کد باشد و این تابع در 26 نقطه مختلف از برنامه اصلی فراخوانی شود، هنگام کامپایل بیش از هزار خط کد به برنامه اصلی افزوده می شود. همچنین تابع بی واسطه می تواند قابلیت انتقال برنامه شما را روی سیستم های مختلف کاهش دهد.
218
14- چندشکلی توابع
در C++ می توانیم چند تابع داشته باشیم که همگی یک نام دارند. در این حالت می گوییم که تابع مذکور، چندشکلی دارد. شرط این کار آن است که فهرست پارامترهای این توابع با یکدیگر تفاوت داشته باشد. یعنی تعداد پارامترها متفاوت باشد یا دست کم یکی از پارامترهای متناظر هم نوع نباشند.
219
مثال 17-5 چندشکلی تابع max()
در مثال 3-5 تابع max() را تعریف کردیم. حالا توابع دیگری با همان نام ولی شکلی متفاوت تعریف می کنیم و همه را در یک برنامه به کار می گیریم:
int max(int, int);
int max(int, int, int);
int max(double, double);
int main()
{ cout << max(99,77) << " " << max(55,66,33) << " " << max(44.4,88.8);
}
220
int max(int x, int y)
{ // returns the maximum of the two given integers:
return (x > y ? x : y);
}
int max(int x, int y, int z)
{ // returns the maximum of the three given integers:
int m = (x > y ? x : y); // m = max(x , y)
return ( z > m ? z : m);
}
int max(double x, double y)
{ // return the maximum of the two given doubles:
return (x>y ? x : y);
}
221
در این برنامه سه تابع با نام max() تعریف شده است.
وقتی تابع max() در جایی از برنامه فراخوانی می شود، کامپایلر فهرست آرگومان آن را بررسی می کند تا بفهمد که کدام نسخه از max باید احضار شود.
مثلا در اولین فراخوانی تابع max() دو آرگومان int ارسال شده، پس نسخه ای که دو پارامتر int در فهرست پارامترهایش دارد فراخوانی می شود. اگر این نسخه وجود نداشته باشد، کامپایلر intها را به double ارتقا می دهد و سپس نسخه ای که دو پارامتر double دارد را فرا می خواند.
222
14- تابع main()
برنامه هایی که تا کنون نوشتیم همه دارای تابعی به نام main() هستند.
منطق C++ این طور است که هر برنامه باید دارای تابعی به نام main() باشد.
در حقیقت هر برنامه کامل، از یک تابع main() به همراه توابع دیگر تشکیل شده است که هر یک از این توابع به شکل مستقیم یا غیر مستقیم از درون تابع main() فراخوانی می شوند.
223
خود برنامه با فراخوانی تابع main() شروع می شود.
چون این تابع یک نوع بازگشتی int دارد، منطقی است که بلوک تابع main() شامل دستور return 0; باشد هرچند که در برخی از کامپایلرهای C++ این خط اجباری نیست و می توان آن را ذکر نکرد.
مقدار صحیحی که با دستور return به سیستم عامل برمی گردد باید تعداد خطاها را شمارش کند. مقدار پیش فرض آن 0 است به این معنا که برنامه بدون خطا پایان گرفته است.
با استفاده از دستور return می توانیم برنامه را به طور غیرمعمول خاتمه دهیم.
224
مثال 18-5 استفاده از دستور return برای پایان دادن به یک برنامه
int main()
{ // prints the quotient of two input integers:
int n, d;
cout << "Enter two integers: ";
cin >> n >> d;
if (d = = 0) return 0;
cout << n << "/" << d << " = " << n/d << endl;
}
Enter two integers: 99 17
99/17 = 5
دستور return تابع فعلی را خاتمه می دهد و کنترل را به فراخواننده بازمی گرداند. به همین دلیل است که اجرای دستور return در تابع main() کل برنامه را خاتمه می دهد.
225
چهار روش وجود دارد که بتوانیم برنامه را به شکل غیرمعمول (یعنی قبل از این که اجرا به پایان بلوک اصلی برسد) خاتمه دهیم:
1 – استفاده از دستور return
2 – فراخوانی تابع exit()
3 – فراخوانی تابع abort()
4 – ایجاد یک حالت استثنا
این تابع در سرفایل <cstdlib> تعریف شده است. تابع exit() برای خاتمه دادن به کل برنامه در هر تابعی غیر از تابع main() مفید است.
226
مثال 19-5 استفاده از تابع exit() برای پایان دادن به برنامه
#include <cstdlib> // defines the exit() function
#include <iostream> // defines thi cin and cout objects
using namespace std;
double reciprocal(double x);
int main()
{ double x;
cin >> x;
cout << reciprocal(x);
}
double reciprocal(double x)1 – Exception
{ // returns the reciprocal of x:
if (x = = 0) exit(1); // terminate the program
return 1.0/x; }
دراین برنامه اگر کاربر عدد 0 را وارد کند، تابع reciprocal() خاتمه می یابد و برنامه بدون هیچ مقدار چاپی به پایان می رسد
227
15- آرگومان های پیش فرض
در C++ می توان تعداد آرگومان های یک تابع را در زمان اجرا به دلخواه تغییر داد.
این امر با استفاده از آرگومان های اختیاری و مقادیر پیش فرض امکان پذیر است.
برای این که به یک پارامتر مقدار پیش فرض بدهیم باید آن مقدار را در فهرست پارامترهای تابع و جلوی پارامتر مربوطه به همراه علامت مساوی درج کنیم. به این ترتیب اگر هنگام فراخوانی تابع، آن آرگومان را ذکر نکنیم، مقدار پیش فرض آن در محاسبات تابع استفاده می شود. به همین خاطر به این گونه آرگومان ها، آرگومان اختیاری می گویند.
228
double p(double, double, double=0, double=0, double=0);
int main()
{ // tests the p() function:
double x = 2.0003;
cout << "p(x,7) = " << p(x,7) << endl;
cout << "p(x,7,6) = " << p(x,7,6) << endl;
cout << "p(x,7,6,5) = " << p(x,7,6,5) << endl;
cout << "p(x,7,6,5,4) = " << p(x,7,6,5,4) << endl;
}
double p(double x, double a0, double a1=0, double a2=0, double a3=0)
{ // returns a0 + a1*x + a2*x^2 + a3*x^3:
return a0 + (a1 + (a2 + a3*x)*x)*x;
}
مثال 20-5 آرگومان های پیش فرض
برنامه زیر حاصل چند جمله ای درجه سوم را پیدا می کند. برای محاسبه این مقدار از الگوریتم هورنر استفاده شده. به این شکل که برای کارایی بیشتر، محاسبه به صورت دسته بندی می شود:
p(x,7) = 7
p(x,7,6) = 19.0018
p(x,7,6,5) = 39.00781 – Default
p(x,7,6,5,4) = 71.0222
229
دقت کنید که پارامترهایی که مقدار پیش فرض دارند باید در فهرست پارامترهای تابع بعد از همه پارامترهای اجباری قید شوند مثل:
void f( int a, int b, int c=4, int d=7, int e=3); // OK
void g(int a, int b=2, int c=4, int d, int e=3); // ERROR
همچنین هنگام فراخوانی تابع، آرگومان های ذکر شده به ترتیب از چپ به راست تخصیص می یابند و پارامترهای بعدی با مقدار پیش فرض پر می شوند.
مثلا در تابع p() که در بالا قید شد، فراخوانی p(8.0,7,6) باعث می شود که پارامتر x مقدار 8.0 را بگیرد سپس پارامتر a0 مقدار 7 را بگیرد و سپس پارامتر a1 مقدار 6 را بگیرد. پارامترهای a2 و a3 مقدار پیش فرض شان را خواهند داشت. این ترتیب را نمی توانیم به هم بزنیم. مثلا نمی توانیم تابع را طوری فرا بخوانیم که پارامترهای x و a0 و a3 مستقیما مقدار بگیرند ولی پارامترهای a1 و a2 مقدار پیش فرض شان را داشته باشند.
230
پایان جلسه پنجم
231
جلسه ششم
«آرایه ها»
232
1- پردازش آرایه ها
2- مقداردهی آرایه ها
3- ایندکس بیرون از حدود آرایه
4- ارسال آرایه به تابع
5- الگوریتم جستجوی خطی
6- مرتب سازی حبابی
7- الگوریتم جستجوی دودویی
آنچه در این جلسه می خوانید:
›››
233
8- استفاده از انواع شمارشی در آرایه
9- تعریف انواع
10 -آرایه های چند بعدی
234
هدف کلی:
شناخت و معرفی آرایه ها و مزیت و طریقه به کارگیری آن ها
هدف های رفتاری:
انتظار می رود پس از پایان این جلسه بتوانید:
– علت استفاده از آرایه ها را بدانید و بتوانید آن ها را در برنامه ها به کار ببرید.
– آرایه های «یک بعدی» و «چندبعدی» را تعریف کنید.
– مفهوم «ایندکس» را بدانید و خطای «اثر همسایگی» را تعریف و شناسایی کنید.
– طریقه ارسال آرایه به توابع را بدانید.
– «جستجوی خطی» و «جستجوی دودویی» را به اختصار شرح دهید.
– «مرتب سازی حبابی» را به اختصار شرح دهید.
235
مقدمه:
در برنامه هایی که داده های فراوانی را پردازش می کنند استفاده از متغیرهای معمولی کار عاقلانه ای نیست زیرا در بسیاری از این برنامه ها «پردازش دسته ای» صورت می گیرد به این معنی که مجموعه ای از داده های مرتبط با هم در حافظه قرار داده می شود و پس از پردازش، کل این مجموعه از حافظه خارج می شود و مجموعه بعدی در حافظه بارگذاری می شود. اگر قرار باشد برای این کار از متغیرهای معمولی استفاده شود بیشتر وقت برنامه نویس صرف پر و خالی کردن انبوهی از متغیرها می شود. به همین دلیل در بیشتر زبان های برنامه نویسی «آرایه ها» تدارک دیده شده اند.
آرایه را می توان متغیری تصور کرد که یک نام دارد ولی چندین مقدار را به طور هم زمان نگهداری می نماید.
236
یک آرایه، یک زنجیره از متغیرهایی است که همه از یک نوع هستند.
به این متغیرها «اعضای آرایه» می گویند.
هر عضو آرایه با یک شماره مشخص می شود که به این شماره «ایندکس» یا «زیرنویس» می گویند
عناصر یک آرایه در خانه های پشت سر هم در حافظه ذخیره می شوند. به این ترتیب آرایه را می توان بخشی از حافظه تصور کرد که این بخش خود به قسمت های مساوی تقسیم شده و هر قسمت به یک عنصر تعلق دارد.
237
شکل مقابل آرایه a که پنج عنصر دارد را نشان می دهد.
عنصر a[0] حاوی مقدار 17.5 و عنصر a[1] حاوی 19.0 و عنصر a[4] حاوی مقدار 18.0 است.
238
2- پردازش آرایه ها
آرایه ها را می توان مثل متغیرهای معمولی تعریف و استفاده کرد. با این تفاوت که آرایه یک متغیر مرکب است و برای دستیابی به هر یک از خانه های آن باید از ایندکس استفاده نمود.
مثال 1-6 دستیابی مستقیم به عناصر آرایه
برنامه ساده زیر یک آرایه سه عنصری را تعریف می کند و سپس مقادیری را در آن قرار داده و سرانجام این مقادیر را چاپ می کند:
int main()
{ int a[3];
a[2] = 55;
a[0] = 11;
a[1] = 33;
cout << "a[0] = " << a[0] << endl;
cout << "a[1] = " << a[1] << andl;
cout << "a[2] = " << a[2] << endl;
}
a[0] = 11
a[1] = 33
a[2] = 55
239
نحو کلی برای اعلان آرایه به شکل زیر است:
type array_name[array_size];
عبارت type نوع عناصر آرایه را مشخص می کند. array_name نام آرایه است .
array_size تعداد عناصر آرایه را نشان می دهد. این مقدار باید یک عدد ثابت صحیح باشد و حتما باید داخل کروشه [] قرار بگیرد.
240
در C++ می توانیم یک آرایه را با استفاده از فهرست مقداردهی، اعلان و مقدارگذاری کنیم:
float a[] = {22.2,44.4,66.6};
به این ترتیب مقادیر داخل فهرست به همان ترتیبی که چیده شده اند درون عناصر آرایه قرار می گیرند. اندازه آرایه نیز برابر با تعداد عناصر موجود در فهرست خواهد بود.
پس همین خط مختصر، آرایه ای از نوع float و با نام a و با تعداد سه عنصر اعلان کرده و هر سه عنصر را با مقدارهای درون فهرست، مقداردهی می کند.
3- مقداردهی آرایه ها
241
مثال 3-6 مقداردهی آرایه با استفاده از فهرست مقداردهی
برنامه زیر، آرایه a را مقداردهی کرده و سپس مقدار هر عنصر را چاپ می کند:
int main()
{ float a[] = { 22.2, 44.4, 66.6 };
int size = sizeof(a)/sizeof(float);
for (int i=0; i<size; i++)
cout << "ta[" << i << "] = " << a[i] << endl;
}
a[0] = 22.2
a[1] = 44.4
a[2] = 66.6
242
هنگام استفاده از فهرست مقداردهی برای اعلان آرایه، می توانیم تعداد عناصر آرایه را هم به طور صریح ذکر کنیم. در این صورت اگر تعداد عناصر ذکر شده از تعداد عناصر موجود در فهرست مقداردهی بیشتر باشد، خانه های بعدی با مقدار صفر پر می شوند:
float a[7] = { 55.5, 66.6, 77.7 };
دقت کنید که تعداد مقادیر موجود در فهرست مقداردهی نباید از تعداد عناصر آرایه بیشتر باشد:
float a[3] = { 22.2, 44.4, 66.6, 88.8 }; // ERROR: too many values!
243
یک آرایه را می توانیم به طور کامل با صفر مقداردهی اولیه کنیم. برای مثال سه اعلان زیر با هم برابرند:
float a[ ] = { 0, 0, 0, 0, 0, 0, 0, 0, 0 };
float a[9] = { 0, 0 };
float a[9] = { 0, 0, 0, 0, 0, 0, 0, 0, 0 };
اما مطلب فوق اصلا به این معنی نیست که از فهرست مقداردهی استفاده نشود.
درست مثل یک متغیر معمولی، اگر یک آرایه مقداردهی اولیه نشود، عناصر آن حاوی مقادیر زباله خواهد بود.
244
مثال 5-6 یک آرایه مقداردهی نشده
برنامه زیر، آرایه a را اعلان می کند ولی مقداردهی نمی کند. با وجود این، مقادیر موجود در آن را چاپ می کند:
int main()
{ const int SIZE=4; // defines the size N for 4 elements
float a[SIZE]; // declares the array's elements as float
for (int i=0; i<SIZE; i++)
cout << "ta[" << i << "] = " << a[i] << endl;
}
a[0] = 6.01838e-39
a[1] = 9.36651e-39
a[2] = 6.00363e-39
a[3] = 0
245
آرایه ها را می توان با استفاده از عملگر جایگزینی مقداردهی کرد اما نمی توان مقدار آن ها را به یکدیگر تخصیص داد:
float a[7] = { 22.2, 44.4, 66.6 };
float b[7] = { 33.3, 55.5, 77.7 };
b = a; // ERROR: arrays cannot be assigned!
همچنین نمی توانیم یک آرایه را به طور مستقیم برای مقداردهی به آرایه دیگر استفاده کنیم :
float a[7] = { 22.2, 44.4, 66.6 };
float b[7] = a; // ERROR: arrays cannot be used as nitializers!
246
4- ایندکس بیرون از حدود آرایه
در بعضی از زبان های برنامه نویسی ، ایندکس آرایه نمی تواند از محدوده تعریف شده برای آن بیشتر باشد. برای مثال در پاسکال اگر آرایه a با تعداد پنج عنصر تعریف شده باشد و آنگاه a[7] دستیابی شود، برنامه از کار می افتد. این سیستم حفاظتی در C++ وجود ندارد. مثال بعدی نشان می دهد که ایندکس یک آرایه هنگام دستیابی می تواند بیشتر از عناصر تعریف شده برای آن باشد و باز هم بدون این که خطایی گرفته شود، برنامه ادامه یابد.
247
مثال 6-6 تجاوز ایندکس آرایه از محدوده تعریف شده برای آن
برنامه زیر یک خطای زمان اجرا دارد؛ به بخشی از حافظه دستیابی می کند که از محدوده آرایه بیرون است:
in main()
{ const int SIZE=4;
float a[SIZE} = { 33.3, 44.4, 55.5, 66.6 };
for (int i=0; i<7; i++) //ERROR: index is out of bounds!
cout << "ta[" << i << "] = " << a[i] << endl;
}
a[0] = 33.3
a[1] = 44.4
a[2] = 55.5
a[3] = 66.6
a[4] = 5.60519e-45
a[5] = 6.01888e-39
a[6] = 6.01889e-39
آرایه ای که در این برنامه تعریف شده، چهار عنصر دارد ولی تلاش می شود به هفت عنصر دستیابی شود. سه مقدار آخر واقعا جزو آرایه نیستند و فقط سلول هایی از حافظه اند که دقیقا بعد از عنصر چهارم آرایه قرار گرفته اند. این سلول ها دارای مقدار زباله هستند.
248
* مثال 7-6 اثر همسایگی
برنامه زیر از ایندکس خارج از محدوده استفاده می کند و این باعث می شود که مقدار یک متغیر به طور ناخواسته تغییر کند:
int main()
{ const int SIZE=4;
float a[] = { 22.2, 44.4, 66.6 };
float x=11.1;
cout << "x = " << x << endl;
a[3] = 88.8; // ERROR: index is out of bounds!
cout << "x = " << x << endl;
}
x = 88.8
249
متغیر x بعد از آرایه a اعلان شده، پس یک سلول چهاربایتی بلافاصله بعد از دوازده بایت آرایه به آن تخصیص می یابد. بنابراین وقتی برنامه تلاش می کند مقدار 88.8 را در a[3] قرار دهد (که جزو آرایه نیست) این مقدار به شکل ناخواسته در x قرار می گیرد. شکل مقابل نشان می دهد چطور این اتفاق در حافظه رخ می دهد.
a
0 22.2
1 44.4
2 66.6
x 88.8
22.2
44.4
66.6
88.8
این خطا یکی از وحشت ناک ترین خطاهای زمان اجراست زیرا ممکن است اصلا نتوانیم منبع خطا را کشف کنیم. حتی ممکن است به این روش داده های برنامه های دیگری که در حال کارند را خراب کنیم و این باعث ایجاد اختلال در کل سیستم شود. به این خطا «اثر همسایگی» می گویند. این وظیفه برنامه نویس است که تضمین کند ایندکس آرایه هیچ گاه از محدوده آن خارج نشود.
مثال بعدی نوع دیگری از خطای زمان اجرا را نشان می دهد: وقتی ایندکس آرایه بیش از حد بزرگ باشد.
250
مثال 8-6 ایجاد استثنای مدیریت نشده
برنامه زیر از کار می افتد زیرا ایندکس آرایه خیلی بزرگ است:
int main()
{ const int SIZE=4;
float a[] = { 22.2, 44.4, 66.6 };
float x=11.1;
cout << "x = " << x << endl;
a[3333] =88.8;//ERROR: index is out of bounds!
cout << "x = " << x << endl;
}
251
وقتی این برنامه روی رایانه ای با سیستم عامل ویندوز اجرا شود، یک صفحه هشدار که در شکل نشان داده شده روی صفحه ظاهر می شود.
این پنجره بیان می کند که برنامه تلاش دارد به نشانی 0040108e از حافظه دستیابی کند. این مکان خارج از حافظه تخصیصی است که برای این برنامه منظور شده، بنابراین سیستم عامل برنامه را متوقف می کند.
252
خطایی که در مثال 8-6 بیان شده یک «استثنای مدیریت نشده» نامیده می شود زیرا کدی وجود ندارد که به این استثنا پاسخ دهد.
در C++ می توانیم کدهایی به برنامه اضافه کنیم که هنگام رخ دادن حالت های استثنا، از توقف برنامه جلوگیری کند. به این کدها «پردازش گر استثنا» می گویند.
پردازش گر استثنا
253
5- ارسال آرایه به تابع
کد float a[]; که آرایه a را اعلان می کند دو چیز را به کامپایلر می گوید:
1- این که نام آرایه a است
2- عناصر آرایه از نوع float هستند.
سمبل a نشانی حافظه آرایه را ذخیره می کند. لازم نیست تعداد عناصر آرایه به کامپایلر گفته شود زیرا از روی نشانی موجود در a می توان عناصر را بازیابی نمود. به همین طریق می توان یک آرایه را به تابع ارسال کرد. یعنی فقط نوع آرایه و نشانی حافظه آن به عنوان پارامتر به تابع فرستاده می شود.
254
مثال 9-6 ارسال آرایه به تابعی که مجموع عناصر آرایه را برمی گرداند
int sum(int[],int);
int main()
{ int a[] = { 11, 33, 55, 77 };
int size = sizeof(a)/sizeof(int);
cout << "sum(a,size) = " << sum(a,size) << endl;}
int sum(int a[], int n)
{ int sum=0;
for (int i=0; i<n; i++)
sum += a[i];
return sum;
}
فهرست پارامتر تابع فوق به شکل (int a[], int n) است به این معنا که این تابع یک آرایه از نوع int و یک متغیر از نوع int دریافت می کند.
به اعلان این تابع در بالای تابع main() نگاه کنید. نام پارامترها حذف شده است.
255
هنگام فراخوانی تابع نیز از عبارت sum(a,size) استفاده شده که فقط نام آرایه به تابع ارسال شده.
نام آرایه در حقیقت نشانی اولین عنصر آرایه است (یعنی a[0])
تابع از این نشانی برای دستیابی به عناصر آرایه استفاده می کند. همچنین تابع می تواند با استفاده از این نشانی، محتویات عناصر آرایه را دست کاری کند.
پس ارسال آرایه به تابع شبیه ارسال متغیر به طریق ارجاع است. به مثال بعدی دقت کنید.
256
مثال 10-6 توابع ورودی و خروجی برای یک آرایه در این برنامه از تابع read() استفاده می شود تا مقادیری به داخل آرایه وارد شود. سپس با استفاده از تابع print() مقادیر داخل آرایه چاپ می شوند:
void read(int[],int&;)
void print(int[],int);
int main()
{ const int MAXSIZE=100;
int a[MAXSIZE]={0}, size;
read(a,size);
cout << "The array has " << size << " elements: ";
print(a,size);
}
Enter integers. Terminate with 0:
a[0]: 11
a[1]: 22
a[2]: 33
a[3]: 44
a[4]: 0
The array has 4 elements: 11 22 33 44
257
void read(int a[], int& n)
{ cout << "Enter integers. Terminate with 0:n";
n = 0;
do
{ cout << "a[" << n << "]: ";
cin >> a[n];
{ while (a[n++] !=0 && n < MAXSIZE);
–n; // don't count the 0
}
258
void print(int a[], int n)
{ for (int i=0; i<n; i++)
cout << a[i] << " ";
}
چون n یک متغیر است، برای این که تابع read() بتواند مقدار آن را تغییر دهد این متغیر باید به شکل ارجاع ارسال شود.
همچنین برای این که تابع مذکور بتواند مقادیر داخل آرایه a را تغییر دهد، آرایه نیز باید به طریق ارجاع ارسال شود، اما ارجاع آرایه ها کمی متفاوت است.
259
در C++ توابع قادر نیستند تعداد عناصر آرایه ارسالی را تشخیص دهند. بنابراین به منظور ارسال آرایه ها به تابع از سه مشخصه استفاده می شود:
1 – آدرس اولین خانه آرایه
2 – تعداد عناصر آرایه
3 – نوع عناصر آرایه
تابع با استفاده از این سه عنصر می تواند به تک تک اعضای آرایه دستیابی کند.
260
آدرس اولین خانه آرایه، همان نام آرایه است.
پس وقتی نام آرایه را به تابع بفرستیم آدرس اولین خانه را به تابع فرستاده ایم.
نوع آرایه نیز در تعریف تابع اعلان می شود.
بنابراین با این دو مقدار، تابع می تواند به آرایه دسترسی داشته باشد.
261
برنامه زیر، آدرس ذخیره شده در نام آرایه و مقدار موجود در آن خانه را چاپ می کند:
int main()
{ int a[] = { 22, 44, 66, 88 };
cout << "a = " << a << endl; // the address of a[0]
cout << "a[0] = " << a[0]; // the value of a[0]
}
مثال 11-6 آدرس اولین خانه آرایه و مقدار درون آن
a = 0x0064fdec
a[0] = 22
این برنامه تلاش می کند که به طور مستقیم مقدار a را چاپ کند. نتیجه چاپ a این است که یک آدرس به شکل شانزده دهی چاپ می شود. این همان آدرس اولین خانه آرایه است. یعنی درون نام a آدرس اولین عنصر آرایه قرار گرفته. خروجی نیز نشان می دهد که a آدرس اولین عنصر را دارد و a[0] مقدار اولین عنصر را.
262
6- الگوریتم جستجوی خطی
آرایه ها بیشتر برای پردازش یک زنجیره از داده ها به کار می روند.
اغلب لازم است که بررسی شود آیا یک مقدار خاص درون یک آرایه موجود است یا خیر. ساده ترین راه این است که از اولین عنصر آرایه شروع کنیم و یکی یکی همه عناصر آرایه را جستجو نماییم تا بفهمیم که مقدار مورد نظر در کدام عنصر قرار گرفته. به این روش «جستجوی خطی» می گویند.
263
int index(int,int[],int);
int main()
{ int a[] = { 22, 44, 66, 88, 44, 66, 55};
cout << "index(44,a,7) = " << index(44,a,7) << endl;
cout << "index(50,a,7) = " << index(50,a,7) << endl;
}
int index(int x, int a[], int n)
{ for (int i=0; i<n; i++)
if (a[i] == x) return i;
return n; // x not found
}
مثال 12-6 جستجوی خطی
برنامه زیر تابعی را آزمایش می کند که در این تابع از روش جستجوی خطی برای یافتن یک مقدار خاص استفاده شده:
index(44,a,7) = 1
index(40,a,7) = 7
264
تابع index() سه پارامتر دارد:
پارامتر x مقداری است که قرار است جستجو شود،
پارامتر a آرایه ای است که باید در آن جستجو صورت گیرد
و پارامتر n هم ایندکس عنصری است که مقدار مورد نظر در آن پیدا شده است.
در این تابع با استفاده از حلقه for عناصر آرایه a پیمایش شده و مقدار هر عنصر با x مقایسه می شود. اگر این مقدار با x برابر باشد، ایندکس آن عنصر بازگردانده شده و تابع خاتمه می یابد.
265
اگر مقدار x در هیچ یک از عناصر آرایه موجود نباشد، مقداری خارج از ایندکس آرایه بازگردانده می شود که به این معناست که مقدار x در آرایه a موجود نیست.
در اولین اجرای آزمایشی، مشخص شده که مقدار 44 در a[1] واقع است و در اجرای آزمایشی دوم مشخص شده که مقدار 40 در آرایه a موجود نیست (یعنی مقدار 44 در a[7] واقع است و از آن جا که آرایه a فقط تا a[6] عنصر دارد، مقدار 7 نشان می دهد که 40 در آرایه موجود نیست).
266
7- مرتب سازی حبابی
«مرتب سازی حبابی» یکی از ساده ترین الگوریتم های مرتب سازی است.
در این روش، آرایه چندین مرتبه پویش می شود و در هر مرتبه بزرگ ترین عنصر موجود به سمت بالا هدایت می شود و سپس محدوده مرتب سازی برای مرتبه بعدی یکی کاسته می شود.
در پایان همه پویش ها، آرایه مرتب شده است.
267
طریقه یافتن بزرگ ترین عنصر و انتقال آن به بالای عناصر دیگر به این شکل است
اولین عنصر آرایه با عنصر دوم مقایسه می شود.
اگر عنصر اول بزرگ تر بود، جای این دو با هم عوض می شود.
سپس عنصر دوم با عنصر سوم مقایسه می شود.
اگر عنصر دوم بزرگ تر بود، جای این دو با هم عوض می شود
و به همین ترتیب مقایسه و جابجایی زوج های همسایه ادامه می یابد تا وقتی به انتهای آرایه رسیدیم، بزرگ ترین عضو آرایه در خانه انتهایی قرار خواهد گرفت.
در این حالت محدوده جستجو یکی کاسته می شود
و دوباره زوج های کناری یکی یکی مقایسه می شوند تا عدد بزرگ تر بعدی به مکان بالای محدوده منتقل شود. این پویش ادامه می یابد تا این که وقتی محدوده جستجو به عنصر اول محدود شد، آرایه مرتب شده است.
268
مثال 13-6 مرتب سازی
برنامه زیر تابعی را آزمایش می کند که این تابع با استفاده از مرتب سازی حبابی یک آرایه را مرتب می نماید:
void print(float[],int);
void sort(float[],int);
int main()
{float a[]={55.5,22.2,99.9,66.6,44.4,88.8,33.3, 77.7};
print(a,8);
sort(a,8);
print(a,8);
}
55.5, 22.2, 99.9, 66.6, 44.4, 88.8, 33.3, 77.7
22.2, 33.3, 44.4, 55.5, 66.6, 77.7, 88.8, 99.9
269
void sort(float a[], int n)
{ // bubble sort:
for (int i=1; i<n; i++)
// bubble up max{a[0..n-i]}:
for (int j=0; j<n-i; j++)
if (a[j] > a[j+1]) swap (a[j],a[j+1]);
//INVARIANT: a[n-1-i..n-1] is sorted
}
270
تابع sort() از دو حلقه تودرتو استفاده می کند.
1- حلقه for داخلی زوج های همسایه را با هم مقایسه می کند و اگر آن ها خارج از ترتیب باشند، جای آن دو را با هم عوض می کند.
وقتی for داخلی به پایان رسید، بزرگ ترین عنصر موجود در محدوده فعلی به انتهای آن هدایت شده است.
2-سپس حلقه for بیرونی محدوده جستجو را یکی کم می کند و دوباره for داخلی را راه می اندازد تا بزرگ ترین عنصر بعدی به سمت بالای آرایه هدایت شود.
271
8- الگوریتم جستجوی دودویی
در روش جستجوی دودویی به یک آرایه مرتب نیاز است.
هنگام جستجو آرایه از وسط به دو بخش بالایی و پایینی تقسیم می شود.
مقدار مورد جستجو با آخرین عنصر بخش پایینی مقایسه می شود.
اگر این عنصر کوچک تر از مقدار جستجو بود، مورد جستجو در بخش پایینی وجود ندارد و باید در بخش بالایی به دنبال آن گشت.
272
دوباره بخش بالایی به دو بخش تقسیم می گردد و گام های بالا تکرار می شود.
سرانجام محدوده جستجو به یک عنصر محدود می شود که یا آن عنصر با مورد جستجو برابر است و عنصر مذکور یافت شده و یا این که آن عنصر با مورد جستجو برابر نیست و لذا مورد جستجو در آرایه وجود ندارد.
این روش پیچیده تر از روش جستجوی خطی است اما در عوض بسیار سریع تر به جواب می رسیم.
273
مثال 14-6 جستجوی دودویی
برنامه آزمون زیر با برنامه آزمون مثال 12-6 یکی است اما تابعی که در زیر آمده از روش جستجوی دودویی برای یافتن مقدار درون آرایه استفاده می کند:
int index(int, int[],int);
int main()
{ int a[] = { 22, 33, 44, 55, 66, 77, 88 };
cout << "index(44,a,7) = " << index(44,a,7) << endl;
cout << "index(60,a,7) = " << index(60,a,7) << endl;
}
274
int index(int x, int a[], int n)
{ // PRECONDITION: a[0] <= a[1] <= … <= a[n-1];
// binary search:
int lo=0, hi=n-1, i;
while (lo <= hi)
{ i = (lo + hi)/2; // the average of lo and hi
if (a[i] == x) return i;
if (a[i] < x) lo = i+1; // continue search in a[i+1..hi]
else hi = i-1; // continue search in a[0..i-1]
}
return n; // x was not found in a[0..n-1]
}
index(44,a,7) = 2
index(60,a,7) = 7
275
برای این که بفهمیم تابع چطور کار می کند، فراخوانی index(44,a,7) را دنبال می کنیم.
وقتی حلقه شروع می شود، x=44 و n=7 و lo=0 و hi=6 است.
ابتدا i مقدار (0+6)/2 = 3 را می گیرد.پس عنصر a[i] عنصر وسط آرایه a[0..6] است. مقدار a[3] برابر با 55 است که از مقدار x بزرگ تر است. پس x در نیمه بالایی نیست و جستجو در نیمه پایینی ادامه می یابد. لذا hi با i-1 یعنی 2 مقداردهی می شود و حلقه تکرار می گردد.
276
حالا hi=2 و lo=0 است و دوباره عنصر وسط آرایه a[0..2] یعنی a[1] با x مقایسه می شود. a[1] برابر با 33 است که کوچک تر از x می باشد. پس این دفعه lo برابر با i+1 یعنی 2 می شود.
در سومین دور حلقه، hi=2 و lo=2 است. پس عنصر وسط آرایه a[2..2] که همان a[2] است با x مقایسه می شود. a[2] برابر با 44 است که با x برابر است. پس مقدار 2 بازگشت داده می شود؛ یعنی x مورد نظر در a[2] وجود دارد.
277
278
حال فراخوانی index(60,a,7) را دنبال می کنیم. وقتی حلقه شروع می شود، x=60 و n=7 و lo=0 و hi=6 است. عنصر وسط آرایه a[0..6] عنصر a[3]=55 است که از x کوچک تر است. پس lo برابر با i+1=4 می شود و حلقه دوباره تکرار می شود. این دفعه hi=6 و lo=4 است . عنصر وسط آرایه a[4..6] عنصر a[5]=77 است که بزرگ تر از x می باشد. پس hi به i-1=4 تغییر می یابد و دوباره حلقه تکرار می شود. این بار hi=4 و lo=4 است و عنصر وسط آرایه a[4..4] عنصر a[4]=66 است که بزرگ تر از x می باشد. لذا hi به i-1=3 کاهش می یابد.
279
اکنون شرط حلقه غلط می شود زیرا hi<lo است. بنابراین تابع مقدار 7 را برمی گرداند یعنی عنصر مورد نظر در آرایه موجود نیست.
280
در تابع فوق هر بار که حلقه تکرار می شود، محدوده جستجو 50% کوچک تر می شود. در آرایه n عنصری، روش جستجوی دودویی حداکثر به مقایسه نیاز دارد تا به پاسخ برسد.
حال آن که در روش جستجوی خطی به n مقایسه نیاز است.
281
تفاوتهای جستجوی دودویی و خطی
جستجوی دودویی سریع تر از جستجوی خطی است.
دومین تفاوت در این است که اگر چند عنصر دارای مقادیر یکسانی باشند، آنگاه جستجوی خطی همیشه کوچک ترین ایندکس را برمی گرداند ولی در مورد جستجوی دودویی نمی توان گفت که کدام ایندکس بازگردانده می شود.
سومین فرق در این است که جستجوی دودویی فقط روی آرایه های مرتب کارایی دارد و اگر آرایه ای مرتب نباشد، جستجوی دودویی پاسخ غلط می دهد ولی جستجوی خطی همیشه پاسخ صحیح خواهد داد.
282
* مثال 15-6 مشخص کردن این که آیا آرایه مرتب است یا خیر
برنامه زیر یک تابع بولی را آزمایش می کند. این تابع مشخص می نماید که آیا آرایه داده شده غیر نزولی است یا خیر:
bool isNondecreasing(int a[], int n);
int main()
{ int a[] = { 22, 44, 66, 88, 44, 66, 55 };
cout<<"isNondecreasing(a,4) = " << isNondecreasing(a,4)<< endl;
cout<<"isNondecreasing(a,7) = " << isNondecreasing(a,7) << endl;
}
283
bool isNondecreasing(int a[], int n)
{ // returns true iff a[0] <= a[1] <= … <= a[n-1]:
for (int i=1; i<n; i++)
if (a[i]<a[i-1]) return false;
return true;
}
isNondecreasing(a,4) = 1
isNondecreasing(a,7) = 0
284
این تابع یک بار کل آرایه را پیمایش کرده و زوج های a[i-1] و a[i] را مقایسه می کند.
اگر زوجی یافت شود که در آن a[i]<a[i-1] باشد، مقدار false را بر می گرداند به این معنی که آرایه مرتب نیست.
ببینید که مقادیر true و false به شکل اعداد 1 و 0 در خروجی چاپ می شوند زیرا مقادیر بولی در حقیقت به شکل اعداد صحیح در حافظه ذخیره می شوند.
285
اگر پیش شرط مثال 14-6 یعنی مرتب بودن آرایه رعایت نشود، جستجوی دودویی پاسخ درستی نمی دهد. به این منظور ابتدا باید این پیش شرط بررسی شود.
با استفاده از تابع assert() می توان اجرای یک برنامه را به یک شرط وابسته کرد.
این تابع یک آرگومان بولی می پذیرد. اگر مقدار آرگومان false باشد، برنامه را خاتمه داده و موضوع را به سیستم عامل گزارش می کند. اگر مقدار آرگومان true باشد، برنامه بدون تغییر ادامه می یابد.
تابع asset() در سرفایل <cassert> تعریف شده است.
286
مثال 16-6 استفاده از تابع assert() برای رعایت کردن یک پیش شرط
برنامه زیر نسخه بهبودیافته ای از تابع search() مثال 14-6 را آزمایش می کند. در این نسخه، از تابع isNonDecreasing() مثال 15-6 استفاده شده تا مشخص شود آرایه مرتب است یا خیر.
نتیجه این تابع به تابع assert() ارسال می گردد تا اگر آرایه مرتب نباشد برنامه به بیراهه نرود.
287
#include <cassert> // defines the assert() function
#include <iostream> // defines the cout object
using namespace std;
int index(int x, int a[], int n);
int main()
{ int a[] = { 22, 33, 44, 55, 66, 77, 88, 60 };
cout<<"index(44,a,7) = " << index(44,a,7) << endl;
cout<<"index(44,a,8) = " << index(44,a,8) << endl;
cout<<"index(60,a,8) = " << index(60,a,8) << endl;
}
288
bool isNondecreasing(int a[], int n);
int index(int x, int a[], int n)
{ assert(isNondecreasing(a,n));
int lo=0, hi=n-1, i;
while (lo <= hi)
{ i = (lo + hi)/2;
if (a[i] == x) return i;
if (a[i] < x) lo = i+1;
else hi = i-1; }
return n;
}
index(44,a,7) = 2
289
آرایه a[] که در این برنامه استفاده شده کاملا مرتب نیست اما هفت عنصر اول آن مرتب است. بنابراین در فراخوانی index(44,a,7) تابع بولی مقدار true را به assert() ارسال می کند و برنامه ادمه می یابد.
اما در دومین فراخوانی index(44,a,8) باعث می شود که تابع isNondecreasing() مقدار false را به تابع assert() ارسال کند که در این صورت برنامه متوقف می شود و ویندوز پنجره هشدار مقابل را نمایش می دهد.
290
9- استفاده از انواع شمارشی در آرایه
انواع شمارشی در جلسه دوم توضیح داده شده اند.
با استفاده از انواع شمارشی نیز می توان آرایه ها را پردازش نمود.
مثال 17-7 شمارش با استفاده از روزهای هفته
این برنامه یک آرایه به نام high[] با هفت عنصرازنوعfloat تعریف می کند که هر عنصر حداکثر دما در یک روز هفته را نشان می دهد:
int main()
{ enum Day { SUN, MON, TUE, WED, THU, FRI, SAT };
float high[SAT+1] = {28.6, 29.1, 29.9, 31.3, 30.4, 32.0, 30.7};
for (int day = SUN; day <= SAT; day++)
cout << "The high temperature for day " << day << " was "<< high[day] << endl;
}
The high temperature for day 0 was 28.6
The high temperature for day 1 was 29.1
The high temperature for day 2 was 29.9
The high temperature for day 3 was 31.3
The high temperature for day 4 was 30.4
The high temperature for day 5 was 32.0
The high temperature for day 6 was 30.7
291
به خاطر بیاورید که انواع شمارشی به شکل مقادیر عددی ذخیره می شوند.
اندازه آرایه، SAT+1 است زیرا SAT مقدار صحیح 6 را دارد و آرایه به هفت عنصر نیازمند است. متغیر day از نوع int است پس می توان مقادیر Day را به آن تخصیص داد. استفاده از انواع شمارشی در برخی از برنامه ها باعث می شود که کد برنامه «خود استناد» شود. مثلا در مثال 17-6 کنترل حلقه به شکل
for (int day = SUN; day <= SAT; day++)
باعث می شود که هر بیننده ای حلقه for بالا را به خوبی درک کند.
292
10- تعریف انواع
انواع شمارشی یکی از راه هایی است که کاربر می تواند نوع ساخت خودش را تعریف کند. برای مثال دستور زیر :
enum Color{ RED,ORANGE,YELLOW, GREEN, BLUE, VIOLET };
یک نوع جدید به نام Color تعریف می کند که متغیرهایی از این نوع می توانند مقادیر RED یا ORANGE یا YELLOW یا GREEN یا BLUE یا VIOLET را داشته باشند. پس با استفاده از این نوع می توان متغیرهایی به شکل زیر تعریف نمود:
Color shirt = BLUE;
Color car[] = { GREEN, RED, BLUE, RED };
Floatwavelength[VIOLET+1]={420,480,530,570,600,620};
در این جا shirt متغیری از نوع Color است و با مقدار BLUE مقداردهی شده. car یک آرایه چهار عنصری است و مقدار عناصر آن به ترتیب GREEN و RED و BLUE و RED می باشد. همچنین wavelength آرایه ای از نوع float است که دارای VIOLET+1 عنصر یعنی 5+1=6 عنصر است.
293
در C++ می توان نام انواع استاندارد را تغییر داد.
کلمه کلیدی typedef یک نام مستعار برای یک نوع استاندارد موجود تعریف می کند.
نحو استفاده از آن به شکل زیر است:
typedef type alias;
که type یک نوع استاندارد و alias نام مستعار برای آن است .
294
برای مثال کسانی که با پاسکال برنامه می نویسند به جای نوع long از عبارت Integer استفاده می کنند و به جای نوع double از عبارت Real استفاده می نمایند. این افراد می توانند به شکل زیر از نام مستعار استفاده کنند:
typedef long Integer;
typedef double Real;
و پس از آن کدهای زیر معتبر خواهند بود:
Integer n = 22;
const Real PI = 3.141592653589793;
Integer frequency[64];
اگر دستور typedef را به شکل زیر بکار ببریم می توانیم آرایه ها را بدون علامت براکت تعریف کنیم:
typedef element-type alias[];
مثل تعریف زیر :
typedef float sequence[];
سپس می توانیم آرایه a را به شکل زیر اعلان کنیم:
sequence a = {55.5, 22.2, 99.9};
295
دستور typedef نوع جدیدی را اعلان نمی کند، بلکه فقط به یک نوع موجود نام مستعاری را نسبت می دهد.
مثال بعدی نحوه به کارگیری typedef را نشان می دهد.
برنامه زیر همان برنامه مثال 13-6 است با این فرق که از typedef استفاده شده تا بتوان از نام مستعار sequrnce به عنوان یک نوع استفاده کرد. سپس این نوع در فهرست پارامترها و اعلان a در تابع main() به کار رفته است:
296
typedef float Sequence[];
void sort(Sequence,int);
void print(Sequence,int);
int main()
{ Sequence a = {55.5, 22.2, 99.9, 66.6, 44.4, 88.8, 33.3, 77.7};
print(a,8);
sort(a,8);
print(a,8);
}
297
void sort(Sequence a, int n)
{ for (int i=n-1; i>0; i–)
for (int j=0; j<i; j++)
if (a[j] > a[j+1]) swap(a[j],a[j+1]);
}
دوباره به دستور typedef نگاه کنید:
typedef float Seguence[];
علامت براکت ها [] نشان می دهند که هر چیزی که از نوع Sequence تعریف شود، یک آرایه است و عبارت float نیز بیان می کند که این آرایه از نوع float است.
298
11- آرایه های چند بعدی
همه آرایه هایی که تاکنون تعریف کردیم، یک بعدی هستند، خطی هستند، رشته ای هستند.
می توانیم آرایه ای تعریف کنیم که از نوع آرایه باشد، یعنی هر خانه از آن آرایه، خود یک آرایه باشد.
به این قبیل آرایه ها، آرایه های چندبعدی می گوییم.
یک آرایه دو بعدی آرایه ای است که هر خانه از آن، خود یک آرایه یک بعدی باشد.
یک آرایه سه بعدی آرایه ای است که هر خانه از آن یک آرایه دو بعدی باشد.
299
دستور int a[5]; آرایه ای با پنج عنصر از نوع int تعریف می کند. این یک آرایه یک بعدی است.
دستور int a[3][5]; آرایه ای با سه عنصر تعریف می کند که هر عنصر، خود یک آرایه پنج عنصری از نوع int است. این یک آرایه دو بعدی است که در مجموع پانزده عضو دارد.
دستور int a[2][3][5]; آرایه ای با دو عنصر تعریف می کند که هر عنصر، سه آرایه است که هر آرایه پنج عضو از نوع int دارد. این یک آرایه سه بعدی است که در مجموع سی عضو دارد.
شکل دستیابی به عناصر در آرایه های چند بعدی مانند آرایه های یک بعدی است. مثلا دستور
a[1][2][3] = 99;
مقدار 99 را در عنصری قرار می دهد که ایندکس آن عنصر(1,2,3) است.
آرایه های چند بعدی مثل آرایه های یک بعدی به توابع فرستاده می شوند با این تفاوت که هنگام اعلان و تعریف تابع مربوطه، باید تعداد عناصر بُعد دوم تا بُعد آخر حتما ذکر شود.
300
مثال 19-6 نوشتن و خواندن یک آرایه دو بعدی
برنامه زیر نشان می دهد که یک آرایه دوبعدی چگونه پردازش می شود:
void read(int a[][5]);
void print(int a[][5]);
int main()
{ int a[3][5];
read(a);
print(a);
}
301
void read(int a[][5])
{ cout << "Enter 15 integers, 5 per row:n";
for (int i=0; i<3; i++)
{ cout << "ROW " << i << ": ";
for (int j=0; j<5; j++)
cin >> a[i][j];
}
302
void print(const int a[][5])
{ for (int i=0; i<3; i++)
{ for (int j=0; j<5; j++)
cout << " " << a[i][j];
cout << endl;
}
}
303
Enter 15 integers, 5 per row:
row 0: 44 77 33 11 44
row 1: 60 50 30 90 70
row 2: 65 25 45 45 55
44 77 33 11 44
60 50 30 90 70
65 25 45 45 55
دقت کنید که در فهرست پارامترهای توابع بالا، بعد اول نامشخص است اما بعد دوم مشخص شده. علت هم این است که آرایه دو بعدی a[][] در حقیقت آرایه ای یک بعدی از سه آرایه پنج عنصری است. کامپایلر نیاز ندارد بداند که چه تعداد از این آرایه های پنج عنصری موجود است، اما باید بداند که آن ها پنج عنصری هستند.
304
وقتی یک آرایه چند بعدی به تابع ارسال می شود، بُعد اول مشخص نیست اما همه ابعاد دیگر باید مشخص باشند.
مثال 20-6 پردازش یک آرایه دوبعدی از نمرات امتحانی
const NUM_STUDENTS = 3;
const NUM_QUIZZES = 5;
typedef int Score[NUM_STUDENTS][NUM_QUIZZES];
void read(Score);
void printQuizAverages(Score);
void printClassAverages(Score);
305
int main()
{ Score score;
cout << "Enter " << NUM_QUIZZES
<< " quiz scores for each student:n";
read(score);
cout << "The quiz averages are:n";
printQuizAverages(score);
cout << "The class averages are:n";
printClassAverages(score);}
306
void read(Score score)
{ for (int s=0; s<NUM_STUDENTS; s++)
{ cout << "Student " << s << ": ";
for(int q=0; q<NUM_QUIZZES; q++)
cin >> score[s][q];
}
}
307
void printQuizAverages(Score score)
{ for (int s=0; s<NUM_STUDENTS; s++)
{ float sum = 0.0;
for (int q=0; q<NUM_QUIZZES; q++)
sum += score[s][q];
cout << "tStudent " << s << ": " << sum/NUM_QUIZZES
<< endl;
}}
308
void printClassAverages(Score score)
{ for (int q=0; q<NUM_QUIZZES; q++)
{ float sum = 0.0;
for (int s=0; s<NUM_STUDENTS; s++)
sum += score[s][q];
cout << "tQuiz " << q << ": " << sum/NUM_STUDENTS << endl;
}
}
309
Enter 5 quiz scores for each student:
student 0: 8 7 9 8 9
student 1: 9 9 9 9 8
student 2: 5 6 7 8 9
The quize averages are:
student 0: 8.2
student 1: 8.8
student 2: 7
The class averages are:
Quiz 0: 7.33333
Quiz 1: 7.33333
Quiz 2: 8.33333
Quiz 3: 8.33333
Quiz 4: 8.66667
در برنامه فوق با استفاده از دستور typedef برای آرایه های دوبعدی 3*5 نام مستعار Score انتخاب شده. این باعث می شود که توابع خواناتر باشند. هر تابع از دو حلقه for تودرتو استفاده کرده که حلقه بیرونی، بعد اول را پیمایش می کند و حلقه درونی بعد دوم را پیمایش می نماید.
تابع printQuizAverages() میانگین هر سطر از نمرات را محاسبه و چاپ می نماید و تابع printClassAverages() میانگین هر ستون از نمره ها را چاپ می کند.
310
مثال 21-6 پردازش یک آرایه سه بعدی
این برنامه تعداد صفرها را در یک آرایه سه بعدی می شمارد:
int numZeros(int a[][4][3], int n1, int n2, int n3);
int main()
{ int a[2][4][3]={{{5,0,2}, {0,0,9},{4,1,0},{7,7,7} }, { {3,0,0}, {8,5,0}, {0,0,0}, {2,0,9} } };
cout << "This array has " << numZeros(a,2,4,3)
<< " zeros:n";
}
311
int numZeros(int a[][4][3], int n1, int n2, int n3)
{ int count = 0;
for (int i = 0; i < n1; i++)
for (int j = 0; j < n2; j++)
for (int k = 0; k < n3; k++)
if (a[i][j][k] == 0) ++count;
return count;
}
This array has 11 zeros:
312
توجه کنید که آرایه چگونه مقداردهی شده است. این قالب مقداردهی به خوبی نمایان می کند که آرایه مذکور یک آرایه دو عنصری است که هر عنصر، خود یک آرایه چهار عضوی است که هر عضو شامل آرایه ای سه عنصری می باشد. پس این آرایه در مجموع 24 عنصر دارد. آرایه مذکور را به شکل زیر نیز می توانیم مقداردهی کنیم:
int a[2][4][3]={5,0,2,0,0,9,4,1,0,7,7,7,3,0,0,8,5,0,0,0,0,2,0,9};
و یا مانند این :
int a[2][4][3] = {{5,0,2,0,0,9,4,1,0,7,7,7},{3,0,0,8,5,0,0,0,0,2,0,9}};
هر سه این قالب ها برای کامپایلر یک مفهوم را دارند اما با نگاه کردن به دو قالب اخیر به سختی می توان فهمید که کدام عنصر از آرایه، کدام مقدار را خواهد داشت.
313
پایان جلسه ششم
314
جلسه هفتم
«اشاره گرها و ارجاع ها »
315
آنچه در این جلسه می خوانید:
1- عملگر ارجاع
2- ارجاع ها
3- اشاره گرها
4- مقداریابی
5- چپ مقدارها، راست مقداره
6- بازگشت از نوع ارجاع
7- آرایه ها و اشاره گرها
›››
316
8- عملگر new
9- عملگر delete
10- آرایه های پویا
11- اشاره گر ثابت
12- آرایه ای از اشاره گرها
13- اشاره گری به اشاره گر دیگر
14- اشاره گر به توابع
15- NUL و NULL
317
هدف کلی:
آشنایی با اشاره گرها و نحوه کار با آدرس های حافظه
هدف های رفتاری:
انتظار می رود پس از پایان این جلسه بتوانید:
– «ارجاع» را تعریف کنید و با استفاده از عملگر ارجاع به متغیرها دستیابی کنید.
– «اشاره گر» را بشناسید و بتوانید اشاره گرهایی به انواع مختلف ایجاد کرده و آن ها را مقداریابی کنید.
»»»
318
– «چپ مقدارها» و «راست مقدارها» را تعریف کرده و آن ها را از یکدیگر تمیز دهید.
– بازگشت هایی از نوع ارجاع ایجاد نمایید.
– طریقه استفاده از عملگرهای new و delete و وظیفه هر یک را بدانید.
– «آرایه های پویا» را تعریف کرده و مزیت آن ها را نسبت به آرایه های ایستا ذکر کنید.
– آرایه های پویا را در برنامه هایتان ایجاد کرده و مدیریت نمایید.
– تفاوت بین NUL و NULL را توضیح دهید.
319
1- مقدمه
حافظه رایانه را می توان به صورت یک آرایه بزرگ در نظر گرفت. برای مثال رایانه ای با 256 مگابایت RAM در حقیقت حاوی آرایه ای به اندازه 268،435،456 (=228) خانه است که اندازه هر خانه یک بایت است.
این خانه ها دارای ایندکس صفر تا 268،435،455 هستند. به ایندکس هر بایت، آدرس حافظه آن می گویند.
320
آدرس های حافظه را با اعداد شانزده دهی نشان می دهند. پس رایانه مذکور دارای محدوده آدرس 0x00000000 تا 0x0fffffff می باشد.
هر وقت که متغیری را اعلان می کنیم، سه ویژگی اساسی به آن متغیر نسبت داده می شود: «نوع متغیر» و «نام متغیر» و «آدرس حافظه» آن.
مثلا اعلان int n; نوع int و نام n و آدرس چند خانه از حافظه که مقدار n در آن قرار می گیرد را به یکدیگر مرتبط می سازد. فرض کنید آدرس این متغیر 0x0050cdc0 است. بنابراین می توانیم n را مانند شکل مقابل مجسم کنیم:
321
خود متغیر به شکل جعبه نمایش داده شده. نام متغیر، n، در بالای جعبه است و آدرس متغیر در سمت چپ جعبه و نوع متغیر، int، در زیر جعبه نشان داده شده. در بیشتر رایانه ها نوع int چهار بایت از حافظه را اشغال می نماید. بنابراین همان طور که در شکل مقابل نشان داده شده است، متغیر n یک بلوک چهاربایتی از حافظه را اشغال می کند که شامل بایت های 0x0050cdc0 تا 0x0050cdc3 است. توجه کنید که آدرس شی، آدرس اولین بایت از بلوکی است که شی در آن جا ذخیره شده.
322
0x0050cdb8
0x0050cdb9
0x0050cdc0
0x0050cdc1
0x0050cdc2
0x0050cdc3
0x0050cdc4
0x0050cdc5
32
32
0x0050cdc0
n
int
اگر متغیر فوق به شکل int n=32; مقداردهی اولیه شود، آنگاه بلوک حافظه به شکل زیر خواهد بود. مقدار 32 در چهار بایتی که برای آن متغیر منظور شده ذخیره می شود.
323
2- عملگر ارجاع
در C++ برای بدست آوردن آدرس یک متغیر می توان از عملگر ارجاع1 & استفاده نمود. به این عملگر «علمگر آدرس» نیز می گویند. عبارت &n آدرس متغیر n را به دست می دهد.
int main()
{ int n=44;
cout << " n = " << n << endl;
cout << "&n = " << &n << endl;
}
n = 44
&n = 0x00c9fdc3
324
خروجی نشان می دهد که آدرس n در این اجرا برابر با 0x00c9fdc3 است. می توان فهمید که این مقدار باید یک آدرس باشد زیرا به شکل شانزده دهی نمایش داده شده. اعداد شانزده دهی را از روی علامت 0x می توان تشخیص داد. معادل دهدهی عدد بالا مقدار 13,237,699 می باشد.
325
3- ارجاع ها
یک «ارجاع» یک اسم مستعار یا واژه مترادف برای متغیر دیگر است.
نحو اعلان یک ارجاع به شکل زیر است:
type& ref_name = var_name;
type نوع متغیر است، ref_name نام مستعار است و var_name نام متغیری است که می خواهیم برای آن نام مستعار بسازیم. برای مثال در اعلان :
int& rn=n; // r is a synonym for n
rn یک ارجاع یا نام مستعار برای n است. البته n باید قبلا اعلان شده باشد.
326
مثال 2-7 استفاده از ارجاع ها
در برنامه زیر rn به عنوان یک ارجاع به n اعلان می شود:
int main()
{ int n=44;
int& rn=n; // rn is a synonym for n
cout << "n = " << n << ", rn = " << rn << endl;
–n;
cout << "n = " << n << ", rn = " << rn << endl;
rn *= 2;
cout << "n = " << n << ", rn = " << rn << endl;
}
n = 44, rn = 44
n = 43, rn = 43
n = 86, rn = 86
n و rn نام های متفاوتی برای یک متغیر است. این دو همیشه مقدار یکسانی دارند. اگر n کاسته شود، rn نیز کاسته شده و اگر rn افزایش یابد، n نیز افزایش یافته است.
327
همانند ثابت ها، ارجاع ها باید هنگام اعلان مقداردهی اولیه شوند با این تفاوت که مقدار اولیه یک ارجاع، یک متغیر است نه یک لیترال. بنابراین کد زیر اشتباه است:
int& rn=44; // ERROR: 44 is not a variable;
گرچه برخی از کامپایلرها ممکن است دستور بالا را مجاز بدانند ولی با نشان دادن یک هشدار اعلام می کنند که یک متغیر موقتی ایجاد شده تا rn به حافظه آن متغیر، ارجاع داشته باشد.
328
درست است که ارجاع با یک متغیر مقداردهی می شود، اما ارجاع به خودی خود یک متغیر نیست.
یک متغیر، فضای ذخیره سازی و نشانی مستقل دارد، حال آن که ارجاع از فضای ذخیره سازی و نشانی متغیر دیگری بهره می برد.
329
* مثال 3-7 ارجاع ها متغیرهای مستقل نیستند
int main()
{ int n=44;
int& rn=n; // rn is a synonym for n
cout << " &n = " << &n << ", &rn = " << &rn << endl;
int& rn2=n; // rn2 is another synonym for n
int& rn3=rn; // rn3 is another synonym for n
cout << "&rn2 = " << &rn2 << ", &rn3 = " << &rn3 << endl;
}
&n = 0x0064fde4, &rn = 0x0064fde4
&rn2 = 0x0064fde4, &rn3 = 0x0064fde4
330
در برنامه فوق فقط یک شی وجود دارد و آن هم n است. rn و rn2 و rn3 ارجاع هایی به n هستند. خروجی نیز تایید می کند که آدرس rn و rn2 و rn3 با آدرس n یکی است.
یک شی می تواند چند ارجاع داشته باشد.
ارجاع ها بیشتر برای ساختن پارامترهای ارجاع در توابع به کار می روند. تابع می تواند مقدار یک آرگومان را که به طریق ارجاع ارسال شده تغییر دهد زیرا آرگومان اصلی و پارامتر ارجاع هر دو یک شی هستند.
تنها فرق این است که دامنه پارامتر ارجاع به همان تابع محدود شده است.
331
4- اشاره گرها
می دانیم که اعداد صحیح را باید در متغیری از نوع int نگهداری کنیم و اعداد اعشاری را در متغیرهایی از نوع float.
به همین ترتیب کاراکترها را باید در متغیرهایی از نوع char نگهداریم و مقدارهای منطقی را در متغیرهایی از نوع bool.
اما آدرس حافظه را در چه نوع متغیری باید قرار دهیم؟
332
عملگر ارجاع & آدرس حافظه یک متغیر موجود را به دست می دهد. می توان این آدرس را در متغیر دیگری ذخیره نمود.
متغیری که یک آدرس در آن ذخیره می شود اشاره گر نامیده می شود.
برای این که یک اشاره گر اعلان کنیم، ابتدا باید مشخص کنیم که آدرس چه نوع داده ای قرار است در آن ذخیره شود. سپس از عملگر اشاره * استفاده می کنیم تا اشاره گر را اعلان کنیم.
333
برای مثال دستور :
float* px;
اشاره گری به نام px اعلان می کند که این اشاره گر، آدرس متغیرهایی از نوع float را نگهداری می نماید. به طور کلی برای اعلان یک اشاره گر از نحو زیر استفاده می کنیم:
type* pointername;
که type نوع متغیرهایی است که این اشاره گر آدرس آن ها را نگهداری می کند و pointername نام اشاره گر است.
آدرس یک شی از نوع int را فقط می توان در اشاره گری از نوع int* ذخیره کرد و آدرس یک شی از نوع float را فقط می توان در اشاره گری از نوع float* ذخیره نمود. دقت کنید که یک اشاره گر، یک متغیر مستقل است.
334
* مثال 4-7 به کارگیری اشاره گرها
برنامه زیر یک متغیر از نوع int به نام n و یک اشاره گر از نوع int* به نام pn را اعلان می کند:
int main()
{ int n=44;
cout << "n = " << n << ", &n = " << &n << endl;
int* pn=&n; // pn holds the address of n
cout << " pn = " << pn << endl;
cout << "&pn = " << &pn << endl;}
n = 44, &n = 0x0064fddc
pn = 0x0064fddc
&pn = 0x0064fde0
335
متغیر n با مقدار 44 مقداردهی شده و آدرس آن 0x0064fddc می باشد. اشاره گر pn با مقدار &n یعنی آدرس n مقداردهی شده. پس مقدار درون pn برابر با 0x0064fddc است (خط دوم خروجی این موضوع را تایید می کند) .
336
اما pn یک متغیر مستقل است و آدرس مستقلی دارد. &pn آدرس pn را به دست می دهد. خط سوم خروجی ثابت می کند که متغیر pn مستقل از متغیر n است. تصویر زیر به درک بهتر این موضوع کمک می کند. در این تصویر ویژگی های مهم n و pn نشان داده شده. pn یک اشاره گر به n است و n مقدار 44 دارد.
وقتی می گوییم «pn به n اشاره می کند» یعنی درون pn آدرس n قرار دارد.
44
n
int
وقتی می گوییم «pn به n اشاره می کند» یعنی درون pn آدرس n قرار دارد.
337
5-مقداریابی
فرض کنید n دارای مقدار 22 باشد و pn اشاره گری به n باشد. با این حساب باید بتوان از طریق pn به مقدار 22 رسید. با استفاده از * می توان مقداری که اشاره گر به آن اشاره دارد را به دست آورد.
به این کار مقداریابی اشاره گر می گوییم.
338
مثال 5-7 مقداریابی یک اشاره گر
این برنامه همان برنامه مثال 4-7 است. فقط یک خط کد بیشتر دارد:
int main()
{ int n=44;
cout << "n = " << n << ", &n = " << &n << endl;
int* pn=&n; // pn holds the address of n
cout << " pn = " << pn << endl;
cout << "&pn = " << &pn << endl;
cout << "*pn = " << *pn << endl;
}
n = 44, &n = 0x0064fdcc
pn = 0x0064fdcc
&pn = 0x0064fdd0
*pn = 44
ظاهرا *pn یک اسم مستعار برای n است زیرا هر دو یک مقدار دارند.
339
* مثال 6-7 اشاره گری به اشاره گرها
این کد ادامه ساختار برنامه مثال 4-7 است:
int main()
{ int n=44;
cout << " n = " << n << endl;
cout << " &n = " << &n << endl;
int* pn=&n; // pn holds the address of n
cout << " pn = " << pn << endl;
cout << " &pn = " << &pn << endl;
cout << " *pn = " << *pn << endl;
int** ppn=&pn; // ppn holds the address of pn
cout << " ppn = " << ppn << endl;
cout << " &ppn = " << &ppn << endl;
cout << " *ppn = " << *ppn << endl;
cout << "**ppn = " << **ppn << endl;
}
یک اشاره گر به هر چیزی می تواند اشاره کند، حتی به یک اشاره گر دیگر. به مثال زیر دقت کنید.
340
n = 44
&n = 0x0064fd78
pn = 0x0064fd78
&pn = 0x0064fd7c
*pn = 44
ppn = 0x0064fd7c
&ppn = 0x0064fd80
*ppn = 0x0064fd78
**ppn = 44
44
n
int
int**
ppn
در برنامه بالا متغیر n از نوع int تعریف شده. pn اشاره گری است که به n اشاره دارد. پس نوع pn باید int* باشد. ppn اشاره گری است که به pn اشاره می کند. پس نوع ppn باید int** باشد. همچنین چون ppn به pn اشاره دارد، پس *ppn مقدار pn را نشان می دهد و چون pn به n اشاره دارد، پس *pn مقدار n را می دهد.
341
عملگر مقداریابی * و عملگر ارجاع & معکوس یکدیگر رفتار می کنند. اگر این دو را با هم ترکیب کنیم، یکدیگر را خنثی می نمایند. اگر n یک متغیر باشد، &n آدرس آن متغیر است. از طرفی با استفاده از عملگر * می توان مقداری که در آدرس &n قرار گرفته را به دست آورد. بنابراین *&n برابر با خود n خواهد بود. همچنین اگر p یک اشاره گر باشد، *p مقداری که p به آن اشاره دارد را می دهد. از طرفی با استفاده از عملگر & می توانیم آدرس چیزی که در *p قرار گرفته را بدست آوریم. پس &*p برابر با خود p خواهد بود. ترتیب قرارگرفتن این عملگرها مهم است. یعنی *&n با &*n برابر نیست. علت این امر را توضیح دهید.
342
عملگر * دو کاربرد دارد. اگر پسوندِ یک نوع باشد (مثل int*) یک اشاره گر به آن نوع را تعریف می کند و اگر پیشوندِ یک اشاره گر باشد (مثل *p) آنگاه مقداری که p به آن اشاره می کند را برمی گرداند. عملگر & نیز دو کاربرد دارد. اگر پسوند یک نوع باشد (مثل int&) یک نام مستعار تعریف می کند و اگر پیشوند یک متغیر باشد (مثل &n) آدرس آن متغیر را می دهد.
343
6- چپ مقدارها، راست مقدارها
یک دستور جایگزینی دو بخش دارد: بخشی که در سمت چپ علامت جایگزینی قرار می گیرد و بخشی که در سمت راست علامت جایگزینی قرار می گیرد. مثلا دستور n = 55; متغیر n در سمت چپ قرار گرفته و مقدار 55 در سمت راست. این دستور را نمی توان به شکل 55 = n; نوشت زیرا مقدار 55 یک ثابت است و نمی تواند مقدار بگیرد. پس هنگام استفاده از عملگر جایگزینی باید دقت کنیم که چه چیزی را در سمت چپ قرار بدهیم و چه چیزی را در سمت راست.
344
چیزهایی که می توانند در سمت چپ جایگزینی قرار بگیرند «چپ مقدار» خوانده می شوند و چیزهایی که می توانند در سمت راست جایگزینی قرار بگیرند «راست مقدار» نامیده می شوند. متغیرها (و به طور کلی اشیا) چپ مقدار هستند و لیترال ها (مثل 15 و "ABC") راست مقدار هستند.
345
یک ثابت در ابتدا به شکل یک چپ مقدار نمایان می شود:
const int MAX = 65535; // MAX is an lvalue
اما از آن پس دیگر نمی توان به عنوان چپ مقدار از آن ها استفاده کرد:
MAX = 21024; // ERROR: MAX is constant
به این گونه چپ مقدارها، چپ مقدارهای «تغییر ناپذیر» گفته می شود. مثل آرایه ها:
int a[] = {1,2,3}; // O.K
a[] = {1,2,3}; // ERROR
346
مابقی چپ مقدارها که می توان آن ها را تغییر داد، چپ مقدارهای «تغییر پذیر» نامیده می شوند. هنگام اعلان یک ارجاع به یک چپ مقدار نیاز داریم:
int& r = n; // O.K. n is an lvalue
اما اعلان های زیر غیرمعتبرند زیرا هیچ کدام چپ مقدار نیستند:
int& r = 44; // ERROR: 44 is not an lvalue
int& r = n++; // ERROR: n++ is not an lvalue
int& r = cube(n); // ERROR: cube(n) is not an lvalue1 – L_values 2- R_values
یک تابع، چپ مقدار نیست اما اگر نوع بازگشتی آن یک ارجاع باشد، می توان تابع را به یک چپ مقدار تبدیل کرد.
347
7- بازگشت از نوع ارجاع
در بحث توابع، ارسال از طریق مقدار و ارسال از طریق ارجاع را دیدیم. این دو شیوه تبادل در مورد بازگشت از تابع نیز صدق می کند:
بازگشت از طریق مقدار و بازگشت از طریق ارجاع. توابعی که تاکنون دیدیم بازگشت به طریق مقدار داشتند. یعنی همیشه یک مقدار به فراخواننده برمی گشت. می توانیم تابع را طوری تعریف کنیم که به جای مقدار، یک ارجاع را بازگشت دهد. مثلا به جای این که مقدار m را بازگشت دهد، یک ارجاع به m را بازگشت دهد.
348
وقتی بازگشت به طریق مقدار باشد، تابع یک راست مقدار خواهد بود زیرا مقدارها لیترال هستند و لیترال ها راست مقدارند. به این ترتیب تابع را فقط در سمت راست یک جایگزینی می توان به کار برد مثل:
m = f();
وقتی بازگشت به طریق ارجاع باشد، تابع یک چپ مقدار خواهد بود زیرا ارجاع ها چپ مقدار هستند. در این حالت تابع را می توان در سمت چپ یک جایگزینی قرار داد مثل :
f() = m;
349
برای این که نوع بازگشتی تابع را به ارجاع تبدیل کنیم کافی است عملگر ارجاع را به عنوان پسوند نوع بازگشتی درج کنیم.
* مثال 7-8 بازگشت از نوع ارجاع
int& max(int& m, int& n)
{ return (m > n ? m : n);}
int main()
{ int m = 44, n = 22;
cout << m << ", " << n << ", " << max(m,n) << endl;
max(m,n) = 55;
cout << m << ", " << n << ", " << max(m,n) << endl;
}
44, 22, 44
55, 22, 55
350
تابع max() از بین m و n مقدار بزرگ تر را پیدا کرده و سپس ارجاعی به آن را باز می گرداند.
بنابراین اگر m از n بزرگ تر باشد، تابع max(m,n) آدرس m را برمی گرداند.
پس وقتی می نویسیم max(m,n) = 55; مقدار 55 در حقیقت درون متغیر m قرار می گیرد (اگر m>n باشد).
به بیانی ساده، فراخوانی max(m,n) خود m را بر می گرداند نه مقدار آن را.
351
اخطار:
وقتی یک تابع پایان می یابد، متغیرهای محلی آن نابود می شوند. پس هیچ وقت ارجاعی به یک متغیر محلی بازگشت ندهید زیرا وقتی کار تابع تمام شد، آدرس متغیرهای محلی اش غیر معتبر می شود و ارجاع بازگشت داده شده ممکن است به یک مقدار غیر معتبر اشاره داشته باشد. تابع max() در مثال بالا یک ارجاع به m یا n را بر می گرداند. چون m و n خودشان به طریق ارجاع ارسال شده اند، پس محلی نیستند و بازگرداندن ارجاعی به آن ها خللی در برنامه وارد نمی کند.
352
به اعلان تابع max() دقت کنید:
int& max(int& m, int& n)
نوع بازگشتی آن با استفاده از عملگر ارجاع & به شکل یک ارجاع درآمده است.
مثال 9-7 به کارگیری یک تابع به عنوان عملگر زیرنویس آرایه
353
float& component(float* v, int k)
{ return v[k-1];}
int main()
{ float v[4];
for (int k = 1; k <= 4; k++)
component(v,k) = 1.0/k;
for (int i = 0; i < 4; i++)
cout << "v[" << i << "] = " << v[i] << endl;
}
v[0] = 1
v[1] = 0.5
v[2] = 0.333333
v[3] = 0.25
354
تابع component() باعث می شود که ایندکس آرایه v از «شماره گذاری از صفر» به «شماره گذاری از یک» تغییر کند. بنابراین component(v,3) معادل v[2] است. این کار از طریق بازگشت از طریق ارجاع ممکن شده است.
355
8- آرایه ها و اشاره گرها
گرچه اشاره گرها از انواع عددی صحیح نیستند اما بعضی از اعمال حسابی را می توان روی اشاره گرها انجام داد. حاصل این می شود که اشاره گر به خانه دیگری از حافظه اشاره می کند. اشاره گرها را می توان مثل اعداد صحیح افزایش و یا کاهش داد و می توان یک عدد صحیح را به آن ها اضافه نمود یا از آن کم کرد. البته میزان افزایش یا کاهش اشاره گر بستگی به نوع داده ای دارد که اشاره گر به آن اشاره دارد.
مثال 10-7 پیمایش آرایه با استفاده از اشاره گر
این مثال نشان می دهد که چگونه می توان از اشاره گر برای پیمایش یک آرایه استفاده نمود:
356
int main()
{ const int SIZE = 3;
short a[SIZE] = {22, 33, 44};
cout << "a = " << a << endl;
cout << "sizeof(short) = " << sizeof(short) << endl;
short* end = a + SIZE; // converts SIZE to offset 6
short sum = 0;
for (short* p = a; p < end; p++)
{ sum += *p;
cout << "t p = " << p;
cout << "t *p = " << *p;
cout << "t sum = " << sum << endl;
}
cout << "end = " << end << endl;
}
a = 0x3fffd1a
sizeof(short) = 2
p = 0x3fffd1a *p = 22 sum = 22
p = 0x3fffd1c *p = 33 sum = 55
p = 0x3fffd1e *p = 44 sum = 99
end = 0x3fffd20
357
این مثال نشان می دهد که هر گاه یک اشاره گر افزایش یابد، مقدار آن به اندازه تعداد بایت های شیئی که به آن اشاره می کند، افزایش می یابد. مثلا اگر p اشاره گری به double باشد و sizeof(double) برابر با هشت بایت باشد، هر گاه که p یک واحد افزایش یابد، اشاره گر p هشت بایت به پیش می رود.
358
مثلا کد زیر :
float a[8];
float* p = a; // p points to a[0]
++p; // increases the value of p by sizeof(float)
359
اگر floatها 4 بایت را اشغال کنند آنگاه ++p مقدار درون p را 4 بایت افزایش می دهد و p += 5; مقدار درون p را 20 بایت افزایش می دهد. با استفاده از خاصیت مذکور می توان آرایه را پیمایش نمود: یک اشاره گر را با آدرس اولین عنصر آرایه مقداردهی کنید، سپس اشاره گر را پی در پی افزایش دهید. هر افزایش سبب می شود که اشاره گر به عنصر بعدی آرایه اشاره کند. یعنی اشاره گری که به این نحو به کار گرفته شود مثل ایندکس آرایه عمل می کند.
360
همچنین با استفاده از اشاره گر می توانیم مستقیما به عنصر مورد نظر در آرایه دستیابی کنیم:
float* p = a; // p points to a[0]
p += 5; // now p points to a[5]
یک نکته ظریف در ارتباط با آرایه ها و اشاره گرها وجود دارد: اگر اشاره گر را بیش از ایندکس آرایه افزایش دهیم، ممکن است به بخش هایی از حافظه برویم که هنوز تخصیص داده نشده اند یا برای کارهای دیگر تخصیص یافته اند. تغییر دادن مقدار این بخش ها باعث بروز خطا در برنامه و کل سیستم می شود. همیشه باید مراقب این خطر باشید.
361
کد زیر نشان می دهد که چطور این اتفاق رخ می دهد.
float a[8];
float* p = a[7]; // points to last element in the array
++p; //now p points to memory past last element!
*p = 22.2; // TROUBLE!
مثال بعدی نشان می دهد که ارتباط تنگاتنگی بین آرایه ها و اشاره گرها وجود دارد. نام آرایه در حقیقت یک اشاره گر ثابت (const) به اولین عنصر آرایه است. همچنین خواهیم دید که اشاره گرها را مانند هر متغیر دیگری می توان با هم مقایسه نمود.
362
* مثال 11-7 پیمایش عناصر آرایه از طریق آدرس
int main()
{ short a[] = {22, 33, 44, 55, 66};
cout << "a = " << a << ", *a = " << *a << endl;
for (short* p = a; p < a +5; p++)
cout << "p = " << p << ", *p = " << *p << endl;
}
a = 0x3fffd08, *a = 22
p = 0x3fffd08, *p = 22
p = 0x3fffd0a, *p = 33
p = 0x3fffd0c, *p = 44
p = 0x3fffd0e, *p = 55
p = 0x3fffd10, *p = 66
p = 0x3fffd12, *p = 77
363
در نگاه اول ، a و p مانند هم هستند: هر دو به نوع short اشاره می کنند و هر دو دارای مقدار 0x3fffd08 هستند. اما a یک اشاره گر ثابت است و نمی تواند افزایش یابد تا آرایه پیمایش شود. پس به جای آن p را افزایش می دهیم تا آرایه را پیمایش کنیم. شرط (p < a+5) حلقه را خاتمه می دهد. a+5 به شکل زیر ارزیابی می شود:
0x3fffd08 + 5*sizeof(short) = 0x3fffd08 + 5*2 = 0x3fffd08 + 0xa = 0x3fffd12
پس حلقه تا زمانی که p < 0x3fffd12 باشد ادامه می یابد.
364
عملگر زیرنویس [] مثل عملگر مقداریابی * رفتار می کند. هر دوی این ها می توانند به عناصر آرایه دسترسی مستقیم داشته باشند.
a[0] == *a
a[1] == *(a + 1)
a[2] == *(a + 2)
…
…
پس با استفاده از کد زیر نیز می توان آرایه را پیمایش نمود:
for (int i = 0; i < 8; i++)
cout << *(a + i) << endl;
365
مثال 12-7 مقایسه الگو
در این مثال، تابع loc() در میان n1 عنصر اول آرایه a1 به دنبال n2 عنصر اول آرایه a2 می گردد. اگر پیدا شد، یک اشاره گر به درون a1 برمی گرداند که a2 از آن جا شروع می شود وگرنه اشاره گر NULL را برمی گرداند.
short* loc(short* a1, short* a2, int n1, int n2)
{ short* end1 = a1 + n1;
for (short* p1 = a1; p1 <end1; p1++)
if (*p1 == *a2)
{ for (int j = 0; j < n2; j++)
if (p1[j] != a2[j]) break;
if (j == n2) return p1;
}
return 0;
}
366
int main()
{ short a1[9] = {11, 11, 11, 11, 11, 22, 33, 44, 55};
short a2[5] = {11, 11, 11, 22, 33};
cout << "Array a1 begins at locationt" << a1 << endl;
cout << "Array a2 begins at locationt" << a2 << endl;
short* p = loc(a1, a2, 9, 5);
if (p)
{ cout << "Array a2 found at locationt" << p << endl;
for (int i = 0; i < 5; i++)
cout << "t" << &p[i] << ": " << p[i] << "t"
<< &a2[i] << ": " << a2[i] << endl; }
else cout << "Not found.n";}
367
Array a1 begins at location 0x3fffd12
Array a2 begins at location 0x3fffd08
Array a2 found at location 0x3fffd16
0x3fffd16: 11 0x3fffd08: 11
0x3fffd18: 11 0x3fffd0a: 11
0x3fffd1a: 11 0x3fffd0c: 11
0x3fffd1c: 22 0x3fffd0e: 22
0x3fffd1e: 33 0x3fffd10: 33
368
9-7 عملگر new
وقتی یک اشاره گر شبیه این اعلان شود:
float* p; // p is a pointer to a float
یک فضای چهاربایتی به p تخصیص داده می شود (معمولا sizeof(float) چهار بایت است). حالا p ایجاد شده است اما به هیچ جایی اشاره نمی کند زیرا هنوز آدرسی درون آن قرار نگرفته. به چنین اشاره گری اشاره گر سرگردان می گویند. اگر سعی کنیم یک اشاره گر سرگردان را مقداریابی یا ارجاع کنیم با خطا مواجه می شویم.
369
مثلا دستور:
*p = 3.14159; // ERROR: no storage has been allocated for *P
خطاست. زیرا p به هیچ آدرسی اشاره نمی کند و سیستم عامل نمی داند که مقدار 3.14159 را کجا ذخیره کند. برای رفع این مشکل می توان اشاره گرها را هنگام اعلان، مقداردهی کرد:
float x = 0; // x cintains the value 0
float* p = &x // now p points to x
*p = 3.14159; // O.K. assigns this value to address that p points to
370
در این حالت می توان به *p دستیابی داشت زیرا حالا p به x اشاره می کند و آدرس آن را دارد. راه حل دیگر این است که یک آدرس اختصاصی ایجاد شود و درون p قرار بگیرد. بدین ترتیب p از سرگردانی خارج می شود. این کار با استفاده از عملگر new صورت می پذیرد:
float* p;
p = new float; // allocates storage for 1 float
*p = 3.14159; // O.K. assigns this value to that storage
دقت کنید که عملگر new فقط خود p را مقداردهی می کند نه آدرسی که p به آن اشاره می کند. می توانیم سه خط فوق را با هم ترکیب کرده و به شکل یک دستور بنویسیم:
float* p = new float(3.141459);
371
با این دستور، اشاره گر p از نوع float* تعریف می شود و سپس یک بلوک خالی از نوع float منظور شده و آدرس آن به p تخصیص می یابد و همچنین مقدار 3.14159 در آن آدرس قرار می گیرد. اگر عملگر new نتواند خانه خالی در حافظه پیدا کند، مقدار صفر را برمی گرداند. اشاره گری که این چنین باشد، «اشاره گر تهی» یا NULL می نامند.
372
با استفاده از کد هوشمند زیر می توانیم مراقب باشیم که اشاره گر تهی ایجاد نشود:
double* p = new double;
if (p == 0) abort(); // allocator failed: insufficent memory
else *p = 3.141592658979324;
در این قطعه کد، هرگاه اشاره گری تهی ایجاد شد، تابع abort() فراخوانی شده و این دستور لغو می شود.
373
تاکنون دانستیم که به دو طریق می توان یک متغیر را ایجاد و مقداردهی کرد. روش اول:
float x = 3.14159; // allocates named memory
و روش دوم:
float* p = new float(3.14159); // allocates unnamed memory
در حالت اول، حافظه مورد نیاز برای x هنگام کامپایل تخصیص می یابد. در حالت دوم حافظه مورد نیاز در زمان اجرا و به یک شیء بی نام تخصیص می یابد که با استفاده از *p قابل دستیابی است.
374
10- عملگر delete
عملگر delete عملی برخلاف عملگر new دارد. کارش این است که حافظه اشغال شده را آزاد کند. وقتی حافظه ای آزاد شود، سیستم عامل می تواند از آن برای کارهای دیگر یا حتی تخصیص های جدید استفاده کند. عملگر delete را تنها روی اشاره گرهایی می توان به کار برد که با دستور new ایجاد شده اند. وقتی حافظه یک اشاره گر آزاد شد، دیگر نمی توان به آن دستیابی نمود مگر این که دوباره این حافظه تخصیص یابد:
float* p = new float(3.14159);
delete p; // deallocates q
*p = 2.71828; // ERROR: q has been deallocated
375
وقتی اشاره گر p در کد بالا آزاد شود، حافظه ای که توسط new به آن تخصیص یافته بود، آزاد شده و به میزان sizeof(float) به حافظه آزاد اضافه می شود. وقتی اشاره گری آزاد شد، به هیچ چیزی اشاره نمی کند؛ مثل متغیری که مقداردهی نشده. به این اشاره گر، اشاره گر سرگردان می گویند.
اشاره گر به یک شیء ثابت را نمی توان آزاد کرد:
const int* p = new int;
delete p; // ERROR: cannot delete pointer to const objects
علت این است که «ثابت ها نمی توانند تغییر کنند».
376
اگر متغیری را صریحا اعلان کرده اید و سپس اشاره گری به آن نسبت داده اید، از عملگر delete استفاده نکنید. این کار باعث اشتباه غیر عمدی زیر می شود:
float x =3.14159; // x contains the value 3.14159
float* p = &x; // p contains the address of x
delete p; // WARNING: this will make x free
کد بالا باعث می شود که حافظه تخصیص یافته برای x آزاد شود. این اشتباه را به سختی می توان تشخیص داد و اشکال زدایی کرد.
377
11- آرایه های پویا
نام آرایه در حقیقت یک اشاره گر ثابت است که در زمان کامپایل ، ایجاد و تخصیص داده می شود:
float a[20]; //a is a const pointer to a block of 20 floats
float* const p = new float[20]; // so is p
هم a و هم p اشاره گرهای ثابتی هستند که به بلوکی حاوی 20 متغیر float اشاره دارند. به اعلان a بسته بندی ایستا1 می گویند زیرا این کد باعث می شود که حافظه مورد نیاز برای a در زمان کامپایل تخصیص داده شود. وقی برنامه اجرا شود، به هر حال حافظه مربوطه تخصیص خواهد یافت حتی اگر از آن هیچ استفاده ای نشود.
378
می توانیم با استفاده از اشاره گر، آرایه فوق را طوری تعریف کنیم که حافظه مورد نیاز آن فقط در زمان اجرا تخصیص یابد:
float* p = new float[20];
دستور بالا، 20 خانه خالی حافظه از نوع float را در اختیار گذاشته و اشاره گر p را به خانه اول آن نسبت می دهد. به این آرایه، «آرایه پویا2» می گویند. به این طرز ایجاد اشیا بسته بندی پویا3 یا «بسته بندی زمان جرا» می گویند.
379
آرایه ایستای a و آرایه پویای p را با یکدیگر مقایسه کنید. آرایه ایستای a در زمان کامپایل ایجاد می شود و تا پایان اجرای برنامه، حافظه تخصیصی به آن مشغول می ماند. ولی آرایه پویای p در زمان اجرا و هر جا که لازم شد ایجاد می شود و پس از اتمام کار نیز می توان با عملگر delete حافظه تخصیصی به آن را آزاد کرد:
delete [] p;
برای آزاد کردن آرایه پویای p براکت ها [] قبل از نام p باید حتما قید شوند زیرا p به یک آرایه اشاره دارد.
380
مثال 15-7 استفاده از آرایه های پویا
تابع get() در برنامه زیر یک آرایه پویا ایجاد می کند:
void get(double*& a, int& n)
{ cout << "Enter number of items: "; cin >> n;
a = new double[n];
cout << "Enter " << n << " items, one per line:n";
for (int i = 0; i < n; i++)
{ cout << "t" << i+1 << ": ";
cin >> a[i];
}}
void print(double* a, int n)
{ for (int i = 0; i < n; i++)
cout << a[i] << " " ;
cout << endl;
}
381
int main()
{ double* a;// a is simply an unallocated pointer
int n;
get(a,n); // now a is an array of n doubles print(a,n);
delete [] a;// now a is simply an unallocated pointer again
get(a,n); // now a is an array of n doubles print(a,n);
}
382
Enter number of items: 4
Enter 4 items, one per line:
1: 44.4
2: 77.7
3: 22.2
4: 88.8
44.4 77.7 22.2 88.8
Enter number of items: 2
Enter 2 items, one per line:
1: 3.33
2: 9.99
3.33 9.99
383
12- اشاره گر ثابت
«اشاره گر به یک ثابت» با «اشاره گر ثابت» تفاوت دارد. این تفاوت در قالب مثال زیر نشان داده شده است.
مثال 16-7 اشاره گرهای ثابت و اشاره گرهایی به ثابت ها
در این کد چهار اشاره گر اعلان شده. اشاره گر p، اشاره گر ثابت cp، اشاره به یک ثابت pc، اشاره گر ثابت به یک ثابت cpc :
384
int n = 44; // an int
int* p = &n; // a pointer to an int
++(*p); // OK: increments int *p
++p; // OK: increments pointer p
int* const cp = &n; // a const pointer to an int
++(*cp); // OK: increments int *cp
++cp; // illegal: pointer cp is const
const int k = 88; // a const int
const int * pc = &k; // a pointer to a const int
++(*pc); // illegal: int *pc is const
++pc; // OK: increments pointer pc
const int* const cpc = &k; // a const pointer to a const int
++(*cpc); // illegal: int *pc is const
++cpc; // illegal: pointer cpc is const
385
اشاره گر p اشاره گری به متغیر n است. هم خود p قابل افزایش است (++p) و هم مقداری که p به آن اشاره می کند قابل افزایش است (++(*P)). اشاره گر cp یک اشاره گر ثابت است. یعنی آدرسی که در cp است قابل تغییر نیست ولی مقداری که در آن آدرس است را می توان دست کاری کرد. اشاره گر pc اشاره گری است که به آدرس یک ثابت اشاره دارد. خود pc را می توان تغییر داد ولی مقداری که pc به آن اشاره دارد قابل تغییر نیست. در آخر هم cpc یک اشاره گر ثابت به یک شیء ثابت است. نه مقدار cpc قابل تغییر است و نه مقداری که آدرس آن در cpc است.
386
13- آرایه ای از اشاره گرها
می توانیم آرایه ای تعریف کنیم که اعضای آن از نوع اشاره گر باشند. مثلا دستور:
float* p[4];
آرایه p را با چهار عنصر از نوع float* (یعنی اشاره گری به float) اعلان می کند. عناصر این آرایه را مثل اشاره گر های معمولی می توان مقداردهی کرد:
p[0] = new float(3.14159);
p[1] = new float(1.19);
387
این آرایه را می توانیم شبیه شکل مقابل مجسم کنیم:
مثال بعد نشان می دهد که آرایه ای از اشاره گرها به چه دردی می خورد. از این آرایه می توان برای مرتب کردن یک فهرست نامرتب به روش حبابی استفاده کرد. به جای این که خود عناصر جابجا شوند، اشاره گرهای آن ها جابجا می شوند.
3.14159
double
0
1
2
3
p
388
مثال 17-7 مرتب سازی حبابی غیرمستقیم
void sort(float* p[], int n)
{ float* temp;
for (int i = 1; i < n; i++)
for (int j = 0; j < n-i; j++)
if (*p[j] > *p[j+1])
{ temp = p[j];
p[j] = p[j+1];
p[j+1] = temp;
}
}
389
تابع sort() آرایه ای از اشاره گرها را می گیرد. سپس درون حلقه های تودرتوی for بررسی می کند که آیا مقادیری که اشاره گرهای مجاور به آن ها اشاره دارند، مرتب هستند یا نه. اگر مرتب نبودند، جای اشاره گرهای آن ها را با هم عوض می کند. در پایان به جای این که یک فهرست مرتب داشته باشیم، آرایه ای داریم که اشاره گرهای درون آن به ترتیب قرار گرفته اند.
390
14-7 اشاره گری به اشاره گر دیگر
یک اشاره گر می تواند به اشاره گر دیگری اشاره کند. مثلا:
char c = 't';
char* pc = &c;
char** ppc = &pc;
char*** pppc = &ppc;
***pppc = 'w'; // changes value of c to 'w'
حالا pc اشاره گری به متغیر کاراکتری c است. ppc اشاره گری به اشاره گر pc است و اشاره گر pppc هم به اشاره گر ppc اشاره دارد. مثل شکل مقابل:
391
با این وجود می توان با اشاره گر pppc مستقیما به متغیر c رسید.
392
15- اشاره گر به توابع
این بخش ممکن است کمی عجیب به نظر برسد. حقیقت این است که نام یک تابع مثل نام یک آرایه، یک اشاره گر ثابت است. نام تابع، آدرسی از حافظه را نشان می دهد که کدهای درون تابع در آن قسمت جای گرفته اند. پس بنابر قسمت قبل اگر اشاره گری به تابع اعلان کنیم، در اصل اشاره گری به اشاره گر دیگر تعریف کرده ایم. اما این تعریف، نحو متفاوتی دارد:
int f(int); // declares function f
int (*pf)(int); // declares function pointer pf
pf = &f; // assigns address of f to pf
393
اشاره گر pf همراه با * درون پرانتز قرار گرفته، یعنی این که pf اشاره گری به یک تابع است. بعد از آن یک int هم درون پرانتز آمده است، به این معنی که تابعی که pf به آن اشاره می نماید، پارامتری از نوع int دارد. اشاره گر pf را می توانیم به شکل زیر تصور کنیم:
394
فایده اشاره گر به توابع این است که به این طریق می توانیم توابع مرکب بسازیم. یعنی می توانیم یک تابع را به عنوان آرگومان به تابع دیگر ارسال کنیم! این کار با استفاده از اشاره گر به تابع امکان پذیر است.
395
مثال 18-7 تابع مرکب جمع
تابع sum() در این مثال دو پارامتر دارد: اشاره گر تابع pf و عدد صحیح n :
int sum(int (*)(int), int);
int square(int);
int cube(int);
int main()
{ cout << sum(square,4) << endl; // 1 + 4 + 9 + 16
cout << sum(cube,4) << endl; //1 + 8 + 27 + 64
}
396
تابع sum() یک پارامتر غیر معمول دارد. نام تابع دیگری به عنوان آرگومان به آن ارسال شده. هنگامی که sum(square,4) فراخوانی شود، مقدار square(1)+square(2)+square(3)+square(4) بازگشت داده می شود. چونsquare(k) مقدار k*k را برمی گرداند، فراخوانی sum(square,4) مقدار 1+4+9+16=30 را محاسبه نموده و بازمی گرداند. تعریف توابع و خروجی آزمایشی به شکل زیر است:
397
int sum(int (*pf)(int k), int n)
{ // returns the sum f(0) + f(1) + f(2) + … + f(n-1):
int s = 0;
for (int i = 1; i <= n; i++)
s += (*pf)(i);
return s;
}
int square(int k)
{ return k*k;
}
int cube(int k)
{ return k*k*k;
}
30
100
398
pf در فهرست پارامترهای تابع sum() یک اشاره گر به تابع است. اشاره گر به تابعی که آن تابع پارامتری از نوع int دارد و مقداری از نوع int را برمی گرداند. k در تابع sum اصلا استفاده نشده اما حتما باید قید شود تا کامپایلر بفهمد که pf به تابعی اشاره دارد که پارامتری از نوع int دارد. عبارت (*pf)(i) معادل با square(i) یا cube(i) خواهد بود، بسته به این که کدام یک از این دو تابع به عنوان آرگومان به sum() ارسال شوند.
399
نام تابع، آدرس شروع تابع را دارد. پس square آدرس شروع تابع square() را دارد. بنابراین وقتی تابع sum() به شکل sum(square,4) فراخوانی شود، آدرسی که درون square است به اشاره گر pf فرستاده می شود. با استفاده از عبارت (*pf)(i) مقدار i به آرگومان تابعی فرستاده می شود که pf به آن اشاره دارد.
400
16- NUL و NULL
ثابت صفر (0) از نوع int است اما این مقدار را به هر نوع بنیادی دیگر می توان تخصیص داد:
char c = 0; // initializes c to the char '