تارا فایل

پاورپوینت برنامه سازی پیشرفته


بسم الله الرحمن الرحیم
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 ''
short d = 0; // initializes d to the short int 0
int n = 0; // initializes n to the int 0
unsigned u = 0; // initializes u to the unsigned int 0
float x = 0; // initializes x to the float 0.0
double z = 0; // initializes z to the double 0.0
401

مقدار صفر معناهای گوناگونی دارد. وقتی برای اشیای عددی به کار رود، به معنای عدد صفر است. وقتی برای اشیای کاراکتری به کار رود، به معنای کاراکتر تهی یا NUL است. NUL معادل کاراکتر '' نیز هست. وقتی مقدار صفر برای اشاره گر ها به کار رود، به معنای «هیچ چیز» یا NULL است. NULL یک کلمه کلیدی است و کامپایلر آن را می شناسد. هنگامی که مقدار NULL یا صفر در یک اشاره گر قرار می گیرد، آن اشاره گر به خانه 0x0 در حافظه اشاره دارد. این خانه حافظه، یک خانه استثنایی است که قابل پردازش نیست. نه می توان آن خانه را مقداریابی کرد و نه می توان مقداری را درون آن قرار داد. به همین دلیل به NULL «هیچ چیز» می گویند.
402

وقتی اشاره گری را بدون استفاده از new اعلان می کنیم، خوب است که ابتدا آن را NULL کنیم تا مقدار زباله آن پاک شود. اما همیشه باید به خاطر داشته باشیم که اشاره گر NULL را نباید مقداریابی نماییم:
int* p = 0; // p points to NULL
*p = 22; // ERROR: cannot dereference the NULL pointer
پس خوب است هنگام مقداریابی اشاره گرها، احتیاط کرده و بررسی کنیم که آن اشاره گر NULL نباشد:
if (p) *p = 22; // O.K.
حالا دستور *p=22; وقتی اجرا می شود که p صفر نباشد. می دانید که شرط بالا معادل شرط زیر است:
if (p != NULL) *p = 22;
403

اشاره گر ها را نمی توان نادیده گرفت.
آن ها سرعت پردازش را زیاد می کنند و کدنویسی را کم.
با استفاده از اشاره گرها می توان به بهترین شکل از حافظه استفاده کرد.
با به کارگیری اشاره گرها می توان اشیایی پیچیده تر و کارآمدتر ساخت.
404

پایان جلسه هفتم
405

جلسه هشتم
« رشته های کاراکتری و فایل ها در ++C استاندارد»
406

مروری بر اشاره گرها
رشته های کاراکتری در C
ورودی /خروجی رشته های کاراکتری
چند تابع عضو cin و cout
توابع کاراکتری C استاندارد
آرایه ای از رشته ها
توابع استاندارد رشته های کاراکتری
آنچه در این جلسه می خوانید
›››
407

رشته های کاراکتری در C++ استاندارد
نگاهی دقیق تر به تبادل داده ها
ورودی قالب بندی نشده
نوع string در ++C استاندارد
فایل ها

هدف کلی: آشنایی با کلاس ها و اصول اولیه به کارگیری آن ها.
408

هدف کلی:

معرفی رشته های کاراکتری به سبک c و c++ و نحوه ایجاد و دست کاری آن ها و همچنین نحوه استفاده از فایل های متنی برای ذخیره سازی و بازیابی اطلاعات.
409

هدف های رفتاری: انتظار می رود پس از پایان این جلسه بتوانید:
– رشته های کاراکتری به سبک C استاندارد را ایجاد نمایید.
– توابع معرفی شده عضو cin و cout را شناخته و وظیفه هر یک را شرح دهید.
– رشته های کاراکتری به سبک C++ استاندارد را ایجاد نمایید.
– مفهوم «ورودی قالب بندی شده» و «ورودی قالب بندی نشده» را دانسته و هر کدام را در مکان های مناسب به کار ببرید.
– نوع string را شناخته و رشته هایی از این نوع ایجاد کنید و با استفاده از توابع خاص، این رشته ها را دست کاری نمایید.
– اطلاعات کاراکتری و رشته ای را در یک فایل متنی نوشته یا از آن بخوانید.
410

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

یک اشاره گر متغیری است که حاوی یک آدرس از حافظه می باشد. نوع این متغیر از نوع مقداری است که در آن آدرس ذخیره شده. با استفاده از عملگر ارجاع & می توان آدرس یک شی را پیدا کرد.
همچنین با استفاده از عملگر مقداریابی * می توانیم مقداری که در یک آدرس قرار دارد را مشخص کنیم. به تعاریف زیر نگاه کنید:
مروری بر اشاره گرها:
int n = 44;
int* p = &n;
412

رشته های کاراکتری در C
در زبان C++ یک «رشته کاراکتری» آرایه ای از کاراکترهاست که این آرایه دارای ویژگی مهم زیر است:
1- یک بخش اضافی در انتهای آرایه وجود دارد که مقدار آن، کاراکتر NUL یعنی '‘ است. پس تعداد کل کاراکترها در آرایه همیشه یکی بیشتر از طول رشته است.
2 – رشته کاراکتری را می توان با لیترال رشته ای به طور مستقیم مقدارگذاری کرد مثل:
char str[] = "string";
توجه کنید که این آرایه هفت عنصر دارد:
's' و 't' و 'r' و 'i' و 'n' و 'g' و ''
413

3– کل یک رشته کاراکتری را می توان مثل یک متغیر معمولی چاپ کرد. مثل:
cout << str;
در این صورت، همه کاراکترهای درون رشته کاراکتری str یکی یکی به خروجی می روند تا وقتی که به کاراکتر انتهایی NUL برخورد شود.
4 – یک رشته کاراکتری را می توان مثل یک متغیر معمولی از ورودی دریافت کرد مثل:
cin >> str;
در این صورت، همه کاراکترهای وارد شده یکی یکی درون str جای می گیرند تا وقتی که به یک فضای خالی در کاراکترهای ورودی برخورد شود. برنامه نویس باید مطمئن باشد که آرایه str برای دریافت همه کاراکترهای وارد شده جا دارد.
414

5 – توابع تعریف شده در سرفایل <cstring> را می توانیم برای دست کاری رشته های کاراکتری به کار بگیریم. این توابع عبارتند از:
تابع طول رشته strlen()
توابع کپی رشته strcpy() و strncpy()
توابع الصاق رشته ها strcat() و strncat()
توابع مقایسه رشته ها strcmp() و strncmp()
و تابع استخراج نشانه strtok() .
415

رشته های کاراکتری با کاراکتر NUL خاتمه می یابند
برنامه کوچک زیر نشان می دهد که کاراکتر '' به رشته های کاراکتری الصاق می شود:

int main()
{ char s[] = "ABCD";
for (int i = 0; i < 5; i++)
cout << "s[" << i << "] = '" << s[i] << "'n";
}
416

رشته کاراکتری s دارای پنج عضو است که عضو پنجم، کاراکتر '' می باشد. تصویر خروجی این مطلب را تایید می نماید.
وقتی کاراکتر '' به cout فرستاده می شود، هیچ چیز چاپ نمی شود. حتی جای خالی هم چاپ نمی شود.
خط آخر خروجی، عضو پنجم را نشان می دهد که میان دو علامت آپستروف هیچ چیزی چاپ نشده.
417

ورودی /خروجی رشته های کاراکتری:
در C++ به چند روش می توان رشته های کاراکتری را دریافت کرده یا نمایش داد.
یک راه استفاده از عملگرهای کلاس string است که در بخش های بعدی به آن خواهیم پرداخت.
روش دیگر، استفاده از توابع کمکی است که آن را در ادامه شرح می دهیم.
418

مثال 2-8 روش ساده دریافت و نمایش رشته های کاراکتری:
در برنامه زیر یک رشته کاراکتری به طول 79 کاراکتر اعلان شده و کلماتی که از ورودی خوانده می شود در آن رشته قرار می گیرد:
int main()
{ char word[80];
do
{ cin >> word;
if (*word) cout << "t"" << word << ""n";
} while (*word);
}
419

چند تابع عضو cin و cout
به cin شی ء فرآیند ورودی می گویند. این شی شامل توابع زیر است:
همه این توابع شامل پیشوند cin هستند زیرا آن ها عضوی از cin می باشند. به cout شیء فرآیند خروجی می گویند. این شی نیز شامل تابع cout.put() است. نحوه کاربرد هر یک از این توابع عضو را در ادامه خواهیم دید.
فراخوانی cin.getline(str,n); باعث می شود که n کاراکتر به درون str خوانده شود و مابقی کاراکترهای وارد شده نادیده گرفته می شوند.

cin.getline()
cin.get()
cin.ignore()
cin.putback()
cin.peek()
420

با دو پارامتر cin.getline() تابع
این برنامه ورودی را خط به خط به خروجی می فرستد:
int main()
{ char line[80];
do
{ cin.getline(line,80);
if (*line) cout << "t[" << line << "]n";
} while (*line);
}
421

با سه پارامتر cin.getlineتابع()
برنامه زیر، متن ورودی را جمله به جمله تفکیک می نماید:
int main()
{ char clause[20];
do
{ cin.getline(clause, 20, ',');
if (*clause) cout << "t[" << clause << "]n";
} while (*clause);
}

422

تابع cin.get()
این برنامه تعداد حرف 'e' در جریان ورودی را شمارش می کند. تا وقتی cin.get(ch) کاراکترها را با موفقیت به درون ch می خواند، حلقه ادامه می یابد:
int main()
{ char ch;
int count = 0;
while (cin.get(ch))
if (ch = = 'e') ++count;
cout << count << " e's were counted.n";
}
423

تابع cout.put()
برنامه زیر، اولین حرف از هر کلمه ورودی را به حرف بزرگ تبدیل کرده و آن را مجددا در خروجی چاپ می کند:
int main()
{ char ch, pre = '';
while (cin.get(ch))
{ if (pre = = ' ' || pre = = 'n')
cout.put(char(toupper(ch)));
else cout.put(ch);
pre = ch;
}
}
424

cin.ignore() و cin.putback() توابع

با استفاده از برنامه زیر، تابعی آزمایش می شود که این تابع اعداد صحیح را از ورودی استخراج می کند:
int nextInt();
int main()
{ int m = nextInt(), n = nextInt();
cin.ignore(80,'n'); // ignore rest of input line
cout << m << " + " << n << " = " << m+n << endl;
}
int nextInt()
{ char ch;
int n;
while (cin.get(ch))
if (ch >= '0' && ch <= '9') // next character is a digit
{ cin.putback(ch); // put it back so it can be
cin >> n; // read as a complite int
break;
}
return n;
}

425

تابع cin.peek()
int nextInt()
{ char ch;
int n;
while (ch = cin.peek())
if (ch >= '0' && ch <= '9')
{ cin >> n;
break;
}
else cin.get(ch);
return n;
}
این نسخه از تابع nextInt() معادل آن است که در مثال قبلی بود:
426

توابع کاراکتری C استاندارد
در مثال 6-8 به تابعtoupper() اشاره شد. این فقط یکی از توابعی است که برای دست کاری کاراکترها استفاده می شود. سایر توابعی که در سرفایل <ctype.h> یا <cctype> تعریف شده به شرح زیر است:
427

428

429

توجه کنید که همه توابع فوق یک پارامتر از نوع int دریافت می کنند و یک مقدار int را برمی گردانند. علت این است که نوع char در اصل یک نوع صحیح است. در عمل وقتی توابع فوق را به کار می برند، یک مقدار char به تابع می فرستند و مقدار بازگشتی را نیز در یک char ذخیره می کنند. به همین خاطر این توابع را به عنوان «توابع کاراکتری» در نظر می گیریم.
430

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

char name[5][20];

این آرایه در اصل پنج عضو دارد که هر عضو می تواند بیست کاراکتر داشته باشد. اگر آرایه فوق را با تعریف رشته های کاراکتری مقایسه کنیم، نتیجه این می شود که آرایه بالا یک آرایه پنج عنصری است که هر عنصر آن یک رشته کاراکتری بیست حرفی است. این آرایه را می توانیم به شکل مقابل تصور کنیم.
431

از طریق name[0] و name[1] و name[2] و name[3] و name[4] می توانیم به هر یک از رشته های کاراکتری در آرایه بالا دسترسی داشته باشیم. یعنی آرایه name گرچه به صورت یک آرایه دوبعدی اعلان شده لیکن به صورت یک آرایه یک بعدی با آن رفتار می شود.
432

آرایه ای از رشته های کاراکتری
برنامه زیر چند رشته کاراکتری را از ورودی می خواند و آن ها را در یک آرایه ذخیره کرده و سپس مقادیر آن آرایه را چاپ می کند:
int main()
{ char name[5][20];
int count=0;
cout << "Enter at most 4 names with at most 19 characters:n";
while (cin.getline(name[count++], 20)) ;
–count;
cout << "The names are:n";
for (int i=0; i<count; i++)
cout << "t" << i << ". [" << name[i] << "]" << endl;
}
433

یک آرایه رشته ای پویا
این برنامه نشان می دهد که چگونه می توان از کاراکتر '$' به عنوان کاراکتر نگهبان در تابع getline() استفاده کرد. مثال زیر تقریبا معادل مثال 9-9 است. برنامه زیر مجموعه ای از اسامی را می خواند، طوری که هر اسم روی یک خط نوشته می شود و هر اسم با کاراکتر 'n' پایان می یابد. این اسامی در آرایه name ذخیره می شوند. سپس نام های ذخیره شده در آرایه name چاپ می شوند:
434

int main()
{ char buffer[80];
cin.getline(buffer,80,'$');
char* name[4];
name[0] = buffer;
int count = 0;
for (char* p=buffer; *p ! ''; p++)
if (*p == 'n')
{ *p = ''; // end name[count]
name[++count] = p+1; // begin next name
}
cout << "The names are:n";
for (int i=0; i<count; i++)
cout << "t" << i << . [" << name[i] << "]" << endl;
}
435

مقداردهی یک آرایه رشته ای
این برنامه هم آرایه رشته ای name را مقداردهی کرده و سپس مقادیر آن را چاپ می نماید:
int main()
{char* name[]= { "Mostafa Chamran", "Mehdi Zeinoddin", "Ebrahim Hemmat" };
cout << "The names are:n";
for (int i = 0; i < 3; i++)
cout << "t" << i << ". [" << name[i] << "]"
<< endl;
}
436

توابع استاندارد رشته های کاراکتری:
سرفایل <cstring> که به آن «کتابخانه رشته های کاراکتری» هم می گویند، شامل خانواده توابعی است که برای دست کاری رشته های کاراکتری خیلی مفیدند.
مثال بعدی ساده ترین آن ها یعنی تابع طول رشته را نشان می دهد. این تابع، طول یک رشته کاراکتری ارسال شده به آن (یعنی تعداد کاراکترهای آن رشته) را برمی گرداند.
437

تابع strlen():
برنامه زیر یک برنامه آزمون ساده برای تابع strlen() است.
وقتی strlen(s) فراخوانی می شود، تعداد کاراکترهای درون رشته s که قبل از کاراکتر NUL قرار گرفته اند، بازگشت داده می شود:
#include <cstring>
int main()
{ char s[] = "ABCDEFG";
cout << "strlen(" << s << ") = " << strlen(s) << endl;
cout << "strlen("") = " << strlen("") << endl;
char buffer[80];
cout << "Enter string: "; cin >> buffer;
cout << "strlen(" << buffer << ") = " << strlen(buffer) << endl;
}
438

توابع strrchr(), strchr(), strstr():
برنامه زیر، مکان یابی یک کاراکتر یا زیررشته خاص را در رشته کاراکتری s نشان می دهد:
#include <cstring>
int main()
{ char s[] = "The Mississippi is a long river.";
cout << "s = "" << s << ""n";
char* p = strchr(s, ' ');
cout << "strchr(s, ' ') points to s[" << p – s << "].n";
p = strchr(s, 'e');
cout << "strchr(s, 'e') points to s[" << p – s << "].n";
p = strrchr(s, 'e');
cout << "strrchr(s, 'e') points to s[" << p – s << "].n";
p = strstr(s, "is");
cout << "strstr(s, "is") points to s[" << p – s
<< "].n";
p = strstr(s, "isi");
cout << "strstr(s, "is") points to s[" << p – s
<< "].n";
if (p == NULL) cout << "strstr(s, "isi") returns
NULLn";
}
439

تابع strcpy():
برنامه زیر نشان می دهد که فراخوانی strcpy(s1, s2) چه تاثیری دارد:
#include <iostream>
#include <cstring>
int main()
{ char s1[] = "ABCDEFG";
char s2[] = "XYZ";
cout << "Before strcpy(s1,s2):n";
cout << "ts1 = [" << s1 << "], length = " << strlen(s1)
<< endl;
cout << "ts2 = [" << s2 << "], length = " << strlen(s2)
<< endl;
strcpy(s1,s2);
cout << "After strcpy(s1,s2):n";
cout << "ts1 = [" << s1 << "], length = " << strlen(s1)
<< endl;
cout << "ts2 = [" << s2 << "], length = " << strlen(s2)
<< endl;
}

440

تابع :strncpy()
برنامه زیر بررسی می کند که فراخوانیstrncpy(s1, s2, n) چه اثری دارد:
int main()
{ char s1[] = "ABCDEFG";
char s2[] = "XYZ";
cout << "Before strncpy(s1,s2,2):n";
cout << "ts1 = [" << s1 << "], length = " << strlen(s1)
<< endl;
cout << "ts2 = [" << s2 << "], length = " << strlen(s2)
<< endl;
strncpy(s1,s2,2);
cout << "After strncpy(s1,s2,2):n";
cout << "ts1 = [" << s1 << "], length = " << strlen(s1)
<< endl;
cout << "ts2 = [" << s2 << "], length = " << strlen(s2)
<< endl;
}
441

تابع الصاق رشته :strcat()
برنامه زیر بررسی می کند که فراخوانی strcat(s1, s2) چه تاثیری دارد:
int main()
{ char s1[] = "ABCDEFG";
char s2[] = "XYZ";
cout << "Before strcat(s1,s2):n";
cout << "ts1 = [" << s1 << "], length = " << strlen(s1)
<< endl;
cout << "ts2 = [" << s2 << "], length = " << strlen(s2)
<< endl;
strcat(s1,s2);
cout << "After strcat(s1,s2):n";
cout << "ts1 = [" << s1 << "], length = " << strlen(s1)
<< endl;
cout << "ts2 = [" << s2 << "], length = " << strlen(s2)
<< endl;
}
442

رشته های کاراکتری در C++ استاندارد :
رشته های کاراکتری که تاکنون تشریح شد، در زبان C استفاده می شوند و البته بخش
مهمی از C++ نیز محسوب می شوند زیرا وسیله مفیدی برای پردازش سریع داده ها
هستند. اما این سرعت پردازش، هزینه ای هم دارد: خطر خطاهای زمان اجرا.
ین خطاها معمولا از این ناشی می شوند که فقط بر کاراکتر NUL به عنوان پایان رشته
تکیه می شود. C++ رشته های کاراکتری خاصی نیز دارد که امن تر و مطمئن تر هستند.
در این رشته ها، طول رشته نیز درون رشته ذخیره می شود و لذا فقط به کاراکتر NUL
برای مشخص نمودن انتهای رشته اکتفا نمی شود.
443

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

حالا ساز و کاری لازم است که از این کاراکترها یک مقدار صحیح بسازد و به برنامه تحویل دهد. همچنین وقتی قصد داریم یک عدد اعشاری را به خروجی بفرستیم، باید با استفاده از راه کاری، آن عدد اعشاری به کاراکترهایی تبدیل شود تا در خروجی نمایش یابد. C++ بر عهده دارند.
445

جریان ها این وظایف را در C++ بر عهده دارند. جریان ها شبیه پالایه ای هستند که داده ها را به کاراکتر تبدیل می کنند و کاراکترها را به داده هایی از یک نوع بنیادی تبدیل می نمایند. به طور کلی، ورودی ها و خروجی ها را یک کلاس جریان به نام stream کنترل می کند. این کلاس خود به زیرکلاس هایی تقسیم می شود:
446

شیء istream جریانی است که داده های مورد نیاز را از کاراکترهای وارد شده از صفحه کلید، فراهم می کند. شیء ostream جریانی است که داده های حاصل را به کاراکترهای خروجی قابل نمایش روی صفحه نمایش گر تبدیل می نماید. شیء ifstream جریانی است که داده های مورد نیاز را از داده های داخل یک فایل، فراهم می کند. شیء ofstream جریانی است که داده های حاصل را درون یک فایل ذخیره می نماید. این جریان ها و طریقه استفاده از آن ها را در ادامه خواهیم دید.
447

استفاده از عملگر بیرون کشی برای کنترل کردن یک حلقه :
int main()
{ int n;
while (cin >> n)
cout << "n = " << n << endl;
}
448

ورودی قالب بندی نشده :
سرفایل <iostream> توابع مختلفی برای ورودی دارد. این توابع برای وارد کردن کاراکترها و رشته های کاراکتری به کار می روند که کاراکترهای فضای سفید را نادیده نمی گیرند.
رایج ترین آن ها، تابع cin.get() برای دریافت یک کاراکتر تکی و تابع cin.getline() برای دریافت یک رشته کاراکتری است.
449

دریافت کاراکترها با استفاده از تابع :cin.get()
while (cin.get(c))
{ if (c >= 'a' && c <= 'z') c += 'A' – 'a'; // capitalize c
cout.put(c);
}
450

وارد کردن یک رشته کاراکتری به وسیله تابع :cin.getline()
برنامه زیر نشان می دهد که چطور می توان داده های متنی را خط به خط از ورودی خوانده و
درون یک آرایه رشته ای قرار داد:::
const int LEN=32; // maximum word length
const int SIZE=10; // array size
typedef char Name[LEN]; // defines Name to be a C_string type
int main()
{ Name martyr[SIZE]; // defines martyr to be an array of 10 names
int n=0;
while(cin.getline(martyr[n++], LEN) && n<SIZE)
;
–n;
for (int i=0; i<n; i++)
cout << 't' << i+1 << ". " << martyr[i] << endl;
}
451

نوع string در ++C استاندارد:
در C++ استاندارد نوع داده ای خاصی به نام string وجود دارد که مشخصات این نوع در سرفایل <string> تعریف شده است. برای آشنایی با این نوع جدید، از طریقه اعلان و مقداردهی آن شروع می کنیم. اشیایی که از نوع string هستند به چند طریق می توانند اعلان و مقداردهی شوند:
string s1; // s1 contains 0 characters
string s2 = "PNU University"; // s2 contains 14 characters
string s3(60, '*'); // s3 contains 60 asterisks
string s4 = s3; // s4 contains 60 asterisks
string s5(s2, 4, 2); // s5 is the 2-character string "Un"
452

استفاده از نوع string
کد زیر یک مجموعه کاراکتر را از ورودی می گیرد و سپس بعد از هر کاراکتر "E" یک علامت ویرگول ',' اضافه می نماید.
مثلا اگر عبارت "The SOFTWARE MOVEMENT is began"
وارد شود، برنامه زیر، آن را به جمله زیر تبدیل می کند:
The SOFTWARE, MOVE,ME,NT is began
متن برنامه این چنین است: string word;
int k;
while (cin >> word)
{ k = word.find("E") + 1;
if (k < word.length())
word.relace(k, 0, ",");
cout << word << ' ';
}
453

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

پردازش فایل در C++ بسیار شبیه تراکنش های معمولی ورودی و خروجی است زیرا این ها همه از اشیای جریان مشابهی بهره می برند. جریان fstream برای تراکنش برنامه با فایل ها به کار می رود. fstream نیز به دو زیرشاخه ifstream و ofstream تقسیم می شود.
جریان ifstream برای خواندن اطلاعات از یک فایل به کار می رود و جریان ofstream برای نوشتن اطلاعات درون یک فایل استفاده می شود.
455

فراموش نکنید که این جریان ها در سرفایل <fstream> تعریف شده اند.
پس باید دستور پیش پردازنده #include <fstream> را به ابتدای برنامه بیافزایید. سپس می توانید عناصری از نوع جریان فایل به شکل زیر تعریف کنید:
ifstream readfile("INPUT.TXT");
ofstream writefile("OUTPUT.TXT");
طبق کدهای فوق، readfile عنصری است که داده ها را از فایلی به نام INPUT.TXT می خواند و writefile نیز عنصری است که اطلاعاتی را در فایلی به نام OUTPUT.TXT می نویسد.
اکنون می توان با استفاده از عملگر >> داده ها را به درون readfile خواند و با عملگر << اطلاعات را درون writefile نوشت. به مثال زیر توجه کنید.
456

یک دفتر تلفن
برنامه زیر، چند نام و تلفن مربوط به هر یک را به ترتیب از کاربر دریافت کرده و در فایلی به نام PHONE.TXT ذخیره می کند. کاربر برای پایان دادن به ورودی باید عدد 0 را تایپ کند.
#include <fstream>
#include <iostream>
using namespace std;
int main()
{ ofstream phonefile("PHONE.TXT");
long number;
string name;
cout << "Enter a number for each name. (0 for quit): ";
for ( ; ; )
{ cout << "Number: ";
cin >> number;
if (number == 0) break;
phonefile << number << ' ';
cout << "Name: ";
cin >> name;
phonefile << name << ' ';
cout << endl;
}
}
457

جستجوی یک شماره در دفتر تلفن
این برنامه، فایل تولید شده توسط برنامه قبل را به کار می گیرد
و درون آن به دنبال یک شماره تلفن می گردد:
#include <fstream>
#include <iostream>
using namespace std;
int main()
{ ifstream phonefile("PHONE.TXT");
long number;
string name, searchname;
bool found=false;
cout << "Enter a name for findind it's phone number: ";
cin >> searchname;
cout << endl;
while (phonefile >> number)
{ phonefile >> name;
if (searchname == name)
{ cout << name << ' ' << number << endl;
found = true;
} if (!found) cout << searchname
<< " is not in this phonebook." << endl;
}

458

پایان جلسه هشتم
459

جلسه نهم
«شی گرایی»
460

آنچه در این جلسه می خوانید:
1- اعلان کلاس ها
2- سازنده ها
3- فهرست مقداردهی در سازنده ها
4- توابع دستیابی
5- توابع عضو خصوصی
6- سازنده کپی
›››
461

7- نابود کننده
8 – اشیای ثابت
9- اشاره گر به اشیا
10- اعضای داده ای ایستا
11- توابع عضو ایستا

462

هدف کلی :
آشنایی با کلاس ها و اصول اولیه به کارگیری آن ها.
463

هدف های رفتاری:
انتظار می رود پس از پایان این جلسه بتوانید:
– نحوه اعلان «کلاس ها» را بدانید و اعضای یک کلاس را برشمارید.
– تفاوت بین اعضای «عمومی» و «خصوصی» را شرح داده و نحوه اعلان هر کدام را بیان کنید.
– «تابع سازنده» را شناخته و وظیفه آن را شرح دهید.
– روش های گوناگون مقداردهی با استفاده از فهرست مقداردهی را بدانید.
– «تابع سازنده کپی» را معرفی کرده و وظیفه آن را شرح دهید.
– اعضای «ایستا» را تعریف کرده و نحوه اعلان و استفاده از آن ها را بدانید.
464

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

در بحث شی گرایی به دسته ها «کلاس» می گویند و به نمونه های هر کلاس «شی» گفته می شود.
مشخصات هر شی را «صفت» می نامند و به رفتارهای هر شی «متد» می گویند.
466

الف. بسته بندی: یعنی این که داده های مرتبط، با هم ترکیب شوند و جزییات پیاده سازی مخفی شود.
برنامه نویسی شی گرا بر سه ستون استوار است:
ب. وراثت: در دنیای واقعی، وراثت به این معناست که یک شی وقتی متولد می شود، خصوصیات و ویژگی هایی را از والد خود به همراه دارد .
467

امتیاز وراثت در این است که از کدهای مشترک استفاده می شود و علاوه بر این که می توان از کدهای قبلی استفاده مجدد کرد، در زمان نیز صرفه جویی شده و استحکام منطقی برنامه هم افزایش می یابد.
468

ج. چند ریختی: که به آن چندشکلی هم می گویند به معنای یک چیز بودن و چند شکل داشتن است. چندریختی بیشتر در وراثت معنا پیدا می کند.
469

اعلان کلاس ها
کد زیر اعلان یک کلاس را نشان می دهد.
class Ratio
{ public:
void assign(int, int);
viod print();
private:
int num, den;
};
اعلان کلاس با کلمه کلیدی class شروع می شودسپس نام کلاس می آید.

470

عبارت public و عبارت private . هر عضوی که ذیل عبارت public اعلان شود، یک «عضو عمومی» محسوب می شود و هر عضوی که ذیل عبارت private اعلان شود، یک «عضو خصوصی» محسوب می شود.
اعلان اعضای کلاس درون یک بلوک انجام می شود و سرانجام یک سمیکولن بعد از بلوک نشان می دهد که اعلان کلاس پایان یافته است.
471

سازنده ها
وظیفه تابع سازنده این است که حافظه لازم را برای شیء جدید تخصیص داده و آن را مقداردهی نماید و با اجرای وظایفی که در تابع سازنده منظور شده، شیء جدید را برای استفاده آماده کند.
هر کلاس می تواند چندین سازنده داشته باشد. در حقیقت تابع سازنده می تواند چندشکلی داشته باشد.
472

سازنده ها، از طریق فهرست پارامترهای متفاوت از یکدیگر تفکیک می شوند. به مثال بعدی نگاه کنید.
class Ratio
{ public:
Ratio() { num = 0; den = 1; }
Ratio(int n) { num = n; den = 1; }
Ratio(int n, int d) { num = n; den = d; }
void print() { cout << num << '/' << den; }
private:
int num, den;
};
مثال 5-9 افزودن چند تابع سازنده دیگر به کلاس Ratio
473

سومین سازنده نیز همان سازنده مثال 4-2 است.
این نسخه از کلاس Ratio سه سازنده دارد:
اولی هیچ پارامتری ندارد و شیء اعلان شده را با مقدار پیش فرض 0 و 1 مقداردهی می کند.
دومین سازنده یک پارامتر از نوع int دارد و شیء اعلان شده را طوری مقداردهی می کند که حاصل کسر با مقدار آن پارامتر برابر باشد.
474

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

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

به سومین سازنده در مثال 5-9 دقت کنید. این سازنده را می توانیم با استفاده از فهرست مقداردهی به شکل زیر خلاصه کنیم:
Ratio(int n, int d) : num(n), den(d) { }
مثال 6-9 استفاده از فهرست مقداردهی در کلاس Ratio
class Ratio
{ public:
Ratio() : num(0) , den(1) { }
Ratio(int n) : num(n) , den(1) { }
Ratio(int n, int d) : num(n), den(d) { }
private:
int num, den;
};
477

توابع دستیابی
داده های عضو یک کلاس معمولا به صورت خصوصی (private) اعلان می شوند تا دستیابی به آن ها محدود باشد اما همین امر باعث می شود که نتوانیم در مواقع لزوم به این داده ها دسترسی داشته باشیم. برای حل این مشکل از توابعی با عنوان توابع دستیابی استفاده می کنیم.
478

تابع دستیابی یک تابع عمومی عضو کلاس است و به همین دلیل اجازه دسترسی به اعضای داده ای خصوصی را دارد.
با استفاده از توابع دستیابی فقط می توان اعضای داده ای خصوصی را خواند ولی نمی توان آن ها را دست کاری کرد.
479

مثال 8-9 افزودن توابع دستیابی به کلاس Ratio
class Ratio
{ public:
Ratio(int n=0, int d=1) : num(n) , den(d) { }
int numerator() { return num; }
int denomerator() { return den; }
private:
int num, den;
};
در این جا توابع numerator() و denumerator() مقادیر موجود در داده های عضو خصوصی را نشان می دهند.
480

توابع عضو خصوصی
توابع عضو را گاهی می توانیم به شکل یک عضو خصوصی کلاس معرفی کنیم. واضح است که چنین تابعی از داخل برنامه اصلی به هیچ عنوان قابل دستیابی نیست. این تابع فقط می تواند توسط سایر توابع عضو کلاس دستیابی شود. به چنین تابعی یک تابع سودمند محلی می گوییم.
481

مثال 9-9 استفاده از توابع عضو خصوصی
class Ratio
{ public:
Ratio(int n=0, int d=1) : num(n), den(d) { }
void print() { cout << num << '/' << den << endl; }
void printconv() { cout << toFloat() << endl; } private:
int num, den;
double toFloat();
};
در برنامه زیر، کلاس Ratio دارای یک تابع عضو خصوصی به نام toFloat() است. وظیفهتابع مذکور این است که معادل ممیز شناور یک عدد کسری را برگرداند
این تابع فقط درون بدنه تابع عضو printconv() استفاده شده و به انجام وظیفه آن کمک می نماید و هیچ نقشی در برنامه اصلی ندارد.
482

سازنده کپی
می دانیم که به دو شیوه می توانیم متغیر جدیدی تعریف نماییم:
int x;
int x=k;

در روش اول متغیری به نام x از نوع int ایجاد می شود. در روش دوم هم همین کار انجام می گیرد با این تفاوت که پس از ایجاد x مقدار موجود در متغیر k که از قبل وجود داشته درون x کپی می شود. اصطلاحا x یک کپی از k است.
483

Ratio y(x);

کد بالا یک شی به نام y از نوع Ratio ایجاد می کند و تمام مشخصات شیء x را درون آن قرار می دهد. اگر در تعریف کلاس، سازنده کپی ذکر نشود (مثل همه کلاس های قبلی) به طور خودکار یک سازنده کپی پیش فرض به کلاس افزوده خواهد شد.
484

مثال 10-9 افزودن یک سازنده کپی به کلاس Ratio
{ public:
Ratio(int n=0, int d=1) : num(n), den(d) { }
Ratio(const Ratio& r) : num(r.num), den(r.den) { }
void print() { cout << num << '/' << den; }
private:
int num, den;
};
در مثال بالا، تابع سازنده کپی طوری تعریف شده که عنصرهای num و den از پارامتر r به درون عنصرهای متناظر در شیء جدید کپی شوند.
485

سازنده کپی در سه وضعیت فرا خوانده می شود:

1 – وقتی که یک شی هنگام اعلان از روی شیء دیگر کپی شود.

2 – وقتی که یک شی به وسیله مقدار به یک تابع ارسال شود.

3 – وقتی که یک شی به وسیله مقدار از یک تابع بازگشت داده شود .
486

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

487

مثال 12-9 افزودن یک نابودکننده به کلاس Ratio
class Ratio
{ public:
Ratio() { cout << "OBJECT IS BORN.n"; }
~Ratio() { cout << "OBJECT DIES.n"; }
private:
int num, den;
};
488

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

const char BLANK = ' ';
const int MAX_INT = 2147483647;
const double PI = 3.141592653589793;
void int(float a[], const int SIZE);
اشیا را نیز می توان با استفاده از عبارت const به صورت یک شیء ثابت اعلان کرد:

const Ratio PI(22,7);
489

اشاره گر به اشیا

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

490

مثال 13-9 استفاده از اشاره گر به اشیا
class X
{ public:
int data;
};
main()
{ X* p = new X;
(*p).data = 22; // equivalent to: p->data = 22;
cout << "(*p).data = " << (*p).data << " = " << p->data << endl;
p->data = 44;
cout << " p->data = " << (*p).data << " = " << p->data << endl;
}
491

در این مثال، p اشاره گری به شیء x است. پس *p یک شیء x است و (*p).data داده عضو آن شی را دستیابی می کند.
حتما باید هنگام استفاده از *p آن را درون پرانتز قرار دهید زیرا عملگر انتخاب عضو (.) تقدم بالاتری نسبت به عملگر مقداریابی (*) دارد.
اگر پرانتزها قید نشوند و فقط *p.data نوشته شود، کامپایلر این خط را به صورت *(p.data) تفسیر خواهد کرد که این باعث خطا می شود.
492

مثال بعدی اهمیت بیشتری دارد و کاربرد اشاره گر به اشیا را بهتر نشان می دهد.
مثال 14-9 فهرست های پیوندی با استفاده از کلاس Node
به کلاسی که در زیر اعلان شده دقت کنید:
class Node
{ public:
Node(int d, Node* p=0) : data(d), next(p) { }
int data;
Node* next;
};
493

عبارت بالا کلاسی به نام Node تعریف می کند که اشیای این کلاس دارای دو عضو داده ای هستند که یکی متغیری از نوع int است و دیگری یک اشاره گر از نوع همین کلاس. این کار واقعا ممکن است و باعث می شود بتوانیم یک شی را با استفاده از همین اشاره گر به شیء دیگر پیوند دهیم و یک زنجیره بسازیم.
494

اگر اشیای q و r و s از نوع Node باشند، می توانیم پیوند این سه شی را به صورت زیر مجسم کنیم:
495

به تابع سازنده نیز دقت کنید که چطور هر دو عضو داده ای شیء جدید را مقداردهی می کند.

int main()
{ int n;
Node* p;
Node* q=0;
while (cin >> n)
{ p = new Node(n, q);
q = p;
}
for ( ; p->next; p = p->next)
cout << p->data << " -> ";
cout << "*n";
}
496

شکل زیر روند اجرای برنامه را نشان می دهد.
Node* next
Node* next
Node* next
Node* next
Node* next
Node* next
الف – قبل از شروع حلقه
ب – پس از اولین تکرار حلقه
ج – پس از دومین تکرار حلقه
497

اعضای داده ای ایستا
هر وقت که شیئی از روی یک کلاس ساخته می شود، آن شی مستقل از اشیای دیگر، داده های عضو خاص خودش را دارد. گاهی لازم است که مقدار یک عضو داده ای در همه اشیا یکسان باشد. اگر این عضو مفروض در همه اشیا تکرار شود، هم از کارایی برنامه می کاهد و هم حافظه را تلف می کند. در چنین مواقعی بهتر است آن عضو را به عنوان یک عضو ایستا اعلان کنیم.
498

عضو ایستا عضوی است که فقط یک نمونه از آن ایجاد می شود و همه اشیا از همان نمونه مشترک استفاده می کنند. با استفاده از کلمه کلیدی static در شروع اعلان متغیر، می توانیم آن متغیر را به صورت ایستا اعلان نماییم. یک متغیر ایستا را فقط باید به طور مستقیم و مستقل از اشیا مقداردهی نمود. کد زیر نحوه اعلان و مقداردهی یک عضو داده ای ایستا را بیان می کند:
class X
{ public:
static int n; // declaration of n as a static data member
};
int X::n = 0; // definition of n
خط آخر نشان می دهد که متغیرهای ایستا را باید به طور مستقیم و مستقل از اشیا مقداردهی کرد.
499

متغیرهای ایستا به طور پیش فرض با صفر مقداردهی اولیه می شوند. بنابراین مقداردهی صریح به این گونه متغیرها ضروری نیست مگر این که بخواهید یک مقدار اولیه غیر صفر داشته باشید.
مثال 15-9 یک عضو داده ای ایستا
کد زیر، کلاسی به نام widget اعلان می کند که این کلاس یک عضو داده ای ایستا به نام count دارد. این عضو، تعداد اشیای widget که موجود هستند را نگه می دارد. هر وقت که یک شیء widget ساخته می شود، از طریق سازنده مقدار count یک واحد افزایش می یابد و هر زمان که یک شیء widget نابود می شود، از طریق نابودکننده مقدار count یک واحد کاهش می یابد:
500

class Widget
{ public:
Widget() { ++count; }
~Widget() { –count; }
static int count;
};
int Widget::count = 0;
main()
{ Widget w, x;
cout << "Now there are " << w.count << " widgets.n";
{ Widget w, x, y, z;
cout << "Now there are " << w.count << " widgets.n";
}
cout << "Now there are " << w.count << " widgets.n";
Widget y;
cout << "Now there are " << w.count << " widgets.n";
}
Now there are 2 widgets.
Now there are 6 widgets.
Now there are 2 widgets.
Now there are 3 widgets.
501

توجه کنید که چگونه چهار شیء widget درون بلوک داخلی ایجاد شده است. هنگامی که اجرای برنامه از آن بلوک خارج می شود، این اشیا نابود می شوند و لذا تعداد کل widgetها از 6 به 2 تقلیل می یابد.
یک عضو داده ای ایستا مثل یک متغیر معمولی است: فقط یک نمونه از آن موجود است بدون توجه به این که چه تعداد شی از آن کلاس موجود باشد. از آن جا که عضو داده ای ایستا عضوی از کلاس است، می توانیم آن را به شکل یک عضو خصوصی نیز اعلان کنیم.
502

* مثال 16-9 یک عضو داده ای ایستا و خصوصی
class Widget
{ public:
Widget() { ++count; }
~Widget() { –count; }
int numWidgets() { return count; }
private:
static int count;
};
503

int Widget::count = 0;
main()
{ Widget w, x;
cout << "Now there are " << w.numWidgets() << " widgets.n";
{ Widget w, x, y, z;
cout << "Now there are " << w.numWidgets() << " widgets.n";
}
cout << "Now there are " << w.numWidgets() << " widgets.n";
Widget y;
cout << "Now there are " << w.numWidgets() << " widgets.n";
}
504

این برنامه مانند مثال 15-9 کار می کند با این تفاوت که متغیر ایستای count به شکل یک عضو خصوصی اعلان شده و به همین دلیل به تابع دستیابی numWidgets() نیاز داریم تا بتوانیم درون برنامه اصلی به متغیر count دسترسی داشته باشیم. می توانیم کلاس Widget و اشیای x و y و w را مانند مقابل تصور کنیم:
505

12- توابع عضو ایستا
با دقت در مثال قبلی به دو ایراد بر می خوریم: اول این که گرچه متغیر count یک عضو ایستا است ولی برای خواندن آن حتما باید از یک شیء موجود استفاده کنیم. در مثال قبلی از شیء w برای خواندن آن استفاده کرده ایم. این باعث می شود که مجبور شویم همیشه مواظب باشیم عضو ایستای مفروض از طریق یک شی که الان موجود است فراخوانی شود.
506

* مثال 17-9 یک تابع عضو ایستا
کد زیر همان کد مثال قبلی است با این فرق که در این کد، تابع دستیابی کننده نیز به شکل ایستا اعلان شده است:
class Widget
{ public:
Widget() { ++count; }
~Widget() { –count; }
static int num() { return count; }
private:
static int count;
};
507

int Widget::count = 0;
int main()
{ cout << "Now there are " << Widget::num() << " widgets.n";
Widget w, x;
cout << "Now there are " << Widget::num() << " widgets.n";
{ Widget w, x, y, z;
cout << "Now there are " << Widget::num() << " widgets.n";
}
cout << "Now there are " << Widget::num() << " widgets.n";
Widget y;
cout << "Now there are " << Widget::num() << " widgets.n";
}
508

وقتی تابع num() به صورت ایستا تعریف شود، از اشیای کلاس مستقل می شود و برای فراخوانی آن نیازی به یک شیء موجود نیست و می توان با کد Widget::num() به شکل مستقیم آن را فراخوانی کرد.
509

پایان جلسه نهم
510

جلسه دهم
«سربارگذاری عملگرها »
511

1- توابع دوست
2- سربارگذاری عملگر جایگزینی (=)
3- اشاره گر this
4- سربارگذاری عملگرهای حسابی
5- سربارگذاری عملگرهای جایگزینی حسابی
6- سربارگذاری عملگرهای رابطه ای
7- سربارگذاری عملگرهای افزایشی و کاهشی
آنچه در این جلسه می خوانید:
512

هدف کلی:

بیان اهمیت سربارگذاری عملگرها برای یک کلاس و نحوه انجام این کار.
513

هدف های رفتاری: انتظار می رود پس از پایان این جلسه بتوانید:
– «سربارگذاری» را تعریف کرده و اهمیت آن را شرح دهید.
– «تابع دوست» را تعریف کنید و علت و اهمیت استفاده از چنین توابعی را بیان نمایید.
– اشاره گر this را بشناسید و علت استفاده از چنین اشاره گری را بیان نمایید.
– نحوه سربارگذاری عملگر جایگزینی را بیان کنید.
– نحوه سربارگذاری عملگرهای حسابی را بیان کنید.
– نحوه سربارگذاری عملگرهای جایگزینی حسابی را بیان کنید.
– نحوه سربارگذاری عملگرهای رابطه ای را بیان کنید.
– نحوه سربارگذاری عملگرهای افزایشی و کاهشی را بیان کنید.
514

مقدمه:
در C++ مجموعه ای از 45 عملگر مختلف وجود دارد که برای کارهای متنوعی استفاده می شوند. همه این عملگرها برای کار کردن با انواع بنیادی (مثل int و float و char) سازگاری دارند.
هنگامی که کلاسی را تعریف می کنیم، در حقیقت یک نوع جدید را به انواع موجود اضافه کرده ایم. ممکن است بخواهیم اشیای این کلاس را در محاسبات ریاضی به کار ببریم.
اما چون عملگرهای ریاضی (مثل + یا = یا *= ) چیزی راجع به اشیای کلاس جدید نمی دانند، نمی توانند به درستی کار کنند. C++ برای رفع این مشکل چاره اندیشیده و امکان سربارگذاری عملگرها را تدارک دیده است. سربارگذاری عملگرها به این معناست که به عملگرها تعاریف جدیدی اضافه کنیم تا بتوانند با اشیای کلاس مورد نظر به درستی کار کنند.
515

class Ratio
{ friend int numReturn(Ratio);
public:
Ratio();
~Ratio();
private:
int num, den;
}
int numReturn(Ratio r)
{ return r.num;
}
int main()
{ Ratio x(22, 7);1 – Friend function
cout << numReturn(x) << endl;
}

1- توابع دوست:
اعضایی از کلاس که به شکل خصوصی (private) اعلان می شوند فقط از داخل همان کلاس قابل دستیابی اند و از بیرون کلاس (درون بدنه اصلی) امکان دسترسی به آن ها نیست.
اما یک استثنا وجود دارد. تابع دوست تابعی است که عضو یک کلاس نیست اما اجازه دارد به اعضای خصوصی آن دسترسی داشته باشد.
به کد زیر نگاه کنید:
516

در بین عملگرهای گوناگون، عملگر جایگزینی شاید بیشترین کاربرد را داشته باشد.
هدف این عملگر، کپی کردن یک شی در شیء دیگر است.
مانند سازنده پیش فرض، سازنده کپی و نابودکننده، عملگر جایگزینی نیز به طور خودکار برای یک کلاس ایجاد می شود اما این تابع را می توانیم به شکل صریح درون کلاس اعلان نماییم.
2-سربارگذاری عملگر جایگزینی(=):
517

class Ratio
{ public:
Ratio(int = 0, int = 1);
Ratio(const Ratio&);
void operator=(const Ratio&);
private:
int num, den;
};
مثال : افزودن عملگر جایگزینی به کلاس:
کد زیر یک رابط کلاس برای Ratio است که شامل سازنده پیش فرض، سازنده کپی و عملگر جایگزینی می باشد:
518

به نحو اعلان عملگر جایگزینی دقت نمایید.
نام این تابع عضو، operator= است و فهرست آرگومان آن مانند سازنده کپی می باشد یعنی یک آرگومان منفرد دارد که از نوع همان کلاس است که به طریقه ارجاع ثابت ارسال می شود. عملگر جایگزینی را می توانیم به شکل زیر تعریف کنیم:
void Ratio::operator=(const Ratio& r)
{ num = r.num;
den = r.den;
}
کد فوق اعضای داده ای شیء r را به درون اعضای داده ای شیئی که مالک فراخوانی این عملگر است، کپی می کند.
519

در C++ می توانیم عملگر جایگزینی را به شکل زنجیره ای مثل زیر به کار ببریم:
x = y = z = 3.14;
3-اشاره گر :this
اجرای کد بالا از راست به چپ صورت می گیرد. یعنی ابتدا مقدار 3.14 درون z قرار می گیرد و سپس مقدار z درون y کپی می شود و سرانجام مقدار y درون x قرار داده می شود. عملگر جایگزینی که در مثال قبل ذکر شد، نمی تواند به شکل زنجیره ای به کار رود.
520

مثال 2-10 سربارگذاری عملگر جایگزینی به شکل صحیح :
class Ratio
{ public:
Ratio(int =0, int =1); // default constructor
Ratio(const Ratio&); // copy constructor
Ratio& operator=(const Ratio&); // assignment operator
// other declarations go here
private:
int num, den;
// other declarations go here
};
Ratio& Ratio::operator=(const Ratio& r)
{ num = r.num;
den = r.den;
return *this;
}
521

توجه داشته باشید که عمل جایگزینی با عمل مقداردهی تفاوت دارد، هر چند هر دو از عملگر یکسانی استفاده می کنند. مثلا در کد زیر:
Ratio x(22,7); // this is an initialization
Ratio y(x); // this is an initialization
Ratio z = x; // this is an initialization
Ratio w;
w = x; // this is an assignment
سه دستور اول، دستورات مقداردهی هستند ولی دستور آخر یک دستور جایگزینی است.
دستور مقداردهی، سازنده کپی را فرا می خواند ولی دستور جایگزینی عملگر جایگزینی را فراخوانی می کند.

522

4-سربارگذاری عملگرهای حسابی:
چهار عملگر حسابی + و – و * و / در همه زبان های برنامه نویسی وجود دارند و با همه انواع بنیادی به کار گرفته می شوند. قصد داریم سرباری را به این عملگرها اضافه کنیم تا بتوانیم با استفاده از آن ها، اشیای ساخت خودمان را در محاسبات ریاضی به کار ببریم.

عملگرهای حسابی به دو عملوند نیاز دارند. مثلا عملگر ضرب (*) در رابطه زیر:
z = x*y;

با توجه به رابطه فوق و آنچه در بخش قبلی گفتیم، عملگر ضرب سربارگذاری شده باید دو پارامتر از نوع یک کلاس و به طریق ارجاع ثابت بگیرد و یک مقدار بازگشتی از نوع همان کلاس داشته باشد. پس انتظار داریم قالب سربارگذاری عملگر ضرب برای کلاس Ratio به شکل زیر باشد:
Ratio operator*(Ratio x, Ratio y)
{ Ratio z(x.num*y.num, x.den*y.den);
return z;
}
523

اگر تابعی عضو کلاس نباشد، نمی تواند به اعضای خصوصی آن کلاس دستیابد. برای رفع این محدودیت ها، تابع سربارگذاری عملگر ضرب را باید به عنوان تابع دوست کلاس معرفی کنیم. لذا قالب کلی برای سربارگذاری عملگر ضرب درون کلاس مفروض T به شکل زیر است:
Class T
{ friend T operator*(const T&, const T&);
public:
// public members
private:
// private members
}
524

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

T operator*(const T& x, const T& y)
{ T z;
// required operations for z = x*y
return z;
}
در سربارگذاری عملگرهای حسابی + و – و / نیز از قالب های کلی فوق استفاده می کنیم با این تفاوت که در نام تابع سربارگذاری، به جای علامت ضرب * باید علامت عملگر مربوطه را قرار دهیم و دستورات بدنه تابع را نیز طبق نیاز تغییر دهیم.
525

class Ratio
{ friend Ratio operator*(const Ratio&, const Ratio&);
public:
Ratio(int = 0, int = 1);
Ratio(const Ratio&);
Ratio& operator=(const Ratio&);
// other declarations go here
private:
int num, den;
// other declarations go here
};
Ratio operator*(const Ratio& x, const Ratio& y)
{ Ratio z(x.num * y.num , x.den * y.den);
return z;
}
int main()
{ Ratio x(22,7) ,y(-3,8) ,z;
z = x; // assignment operator is called
z.print(); cout << endl;
x = y*z; // multiplication operator is called
x.print(); cout << endl;
}
مثال 3-10 سربارگذاری عملگر ضرب برای کلاس :Ratio
526

class T
{ public:
T& operator*=(const T&);
// other public members
private:
// private members
};
5-سربارگذاری عملگرهای جایگزینی حسابی:
به خاطر بیاورید که عملگرهای جایگزینی حسابی، ترکیبی از عملگر جایگزینی و یک عملگر حسابی دیگر است. مثلا عملگر *= ترکیبی از دو عمل ضرب * و سپس جایگزینی = است. نکته قابل توجه در عملگرهای جایگزینی حسابی این است که این عملگرها بر خلاف عملگرهای حسابی ساده، فقط یک عملوند دارند. پس تابع سربارگذاری عملگرهای جایگزینی حسابی بر خلاف عملگرهای حسابی، می تواند عضو کلاس باشد. سربارگذاری عملگرهای جایگزینی حسابی بسیار شبیه سربارگذاری عملگر جایگزینی است. قالب کلی برای سربارگذاری عملگر *= برای کلاس مفروض T به صورت زیر است:
527

استفاده از اشاره گر *this باعث می شود که بتوانیم عملگر *= را در یک رابطه زنجیره ای به کار ببریم. در C++ چهار عملگر جایگزینی حسابی += و -= و *= و /= وجود دارد. قالب کلی برای سربارگذاری همه این عملگرها به شکل قالب بالا است فقط در نام تابع به جای *= باید علامت عملگر مربوطه را ذکر کرد و دستورات بدنه تابع را نیز به تناسب، تغییر داد. مثال بعدی نشان می دهد که عملگر *= چگونه برای کلاس Ratio سربارگذاری شده است.
بدنه تابع سربارگذاری به قالب زیر است:
T& T::operator*=(const T& x)
{ // required operations
return *this;
}
528

class Ratio
{ public:
Ratio(int = 0, int = 1);
Ratio& operator=(const Ratio&);
Ratio& operator*=(const Ratio&);
// other declarations go here
private:
int num, den;
// other declarations go here
};
Ratio& Ratio::operator*=(const Ratio& r)
{ num = num*r.num;
den = den*r.den;
return *this;
}
مثال 4-10 کلاس Ratio با عملگر *= سربارگذاری شده:
بدیهی است که عملگر سربارگذاری شده جایگزینی حسابی، باید با عملگر سربارگذاری شده حسابی معادلش، نتیجه یکسانی داشته باشد. مثلا اگر x و y هر دو از کلاس Ratio باشند، آنگاه دو خط کد زیر باید نتیجه مشابهی داشته باشند:
x *= y;
x = x*y;
529

شش عملگر رابطه ای در C++ وجود دارد که عبارتند از: > و < و => و <= و == و != .
این عملگرها به همان روش عملگرهای حسابی،یعنی به شکل توابع دوست سربارگذاری می شوند. اما نوع بازگشتی شان فرق می کند.
6-سربارگذاری عملگرهای رابطه ای:
530

چون نوع بولین در حقیقت یک نوع عددی صحیح است، می توان به جای true مقدار 1 و به جای false مقدار 0 را قرار داد. به همین جهت نوع بازگشتی را برای توابع سربارگذاری عملگرهای رابطه ای، از نوع int قرار داده اند.
حاصل عبارتی که شامل عملگر رابطه ای باشد، همواره یک مقدار بولین است. یعنی اگر آن عبارت درست باشد، حاصل true است و اگر آن عبارت نادرست باشد، حاصل false است.
531

قالب کلی برای سربارگذاری عملگر رابطه ای == به شکل زیر است:
class T
{ friend int operator==(const T&, const T&);
public:
// public members
private:
// private members
}
532

همچنین قالب کلی تعریف بدنه این تابع به صورت زیر می باشد:
int operator==(const T& x,const T& y)
{ // required operations to finding result
return result;
}
که به جای result یک مقدار بولین یا یک عدد صحیح قرار می گیرد. سایر عملگرهای رابطه ای نیز از قالب بالا پیروی می کنند.
533

class Ratio
{ friend int operator==(const Ratio&, const Ratio&);
frined Ratio operator*(const Ratio&, const Ratio&);
// other declarations go here
public:
Ratio(int = 0, int = 1);
Ratio(const Ratio&);
Ratio& operator=(const Ratio&);
// other declarations go here
private:
int num, den;
// other declarations go here
};
int operator==(const Ratio& x, const Ratio& y)
{ return (x.num * y.den == y.num * x.den);
}
چون اشیای کلاس Ratio به صورت کسر هستند، بررسی تساوی x==y معادل بررسی است که برای بررسی این تساوی می توانیم مقدار (a*d==b*c) را بررسی کنیم. بدنه تابع سربارگذاری در مثال همین رابطه را بررسی می کند.
مثال 5-10 سربارگذاری عملگر تساوی (==) برای کلاس :Ratio
534

7-سربارگذاری عملگرهای افزایشی و کاهشی:
عملگر افزایشی ++ و کاهشی — هر کدام دو شکل دارند:
1- شکل پیشوندی.
2-شکل پسوندی.
هر کدام از این حالت ها را می توان سربارگذاری کرد.
535

قالب کلی برای سربارگذاری عملگر پیش افزایشی به شکل زیر است:
T T::operator++()
{ // required operations
return *this;
}
این جا هم از اشاره گر *this استفاده شده. علت هم این است که مشخص نیست چه چیزی باید بازگشت داده شود. به همین دلیل اشاره گر *this به کار رفته تا شیئی که عمل پیش افزایش روی آن صورت گرفته، بازگشت داده شود.
536

افزودن عملگر پیش افزایشی به کلاس :Ratio
اگر y یک شی از کلاس Ratio باشد و عبارت ++y ارزیابی گردد، مقدار 1 به y افزوده می شود اما چون y یک عدد کسری است، افزودن مقدار 1 به این کسر اثر متفاوتی دارد. فرض کنید y=22/7 باشد. حالا داریم:
537

افزودن عملگر پس افزایشی به کلاس Ratio:
در عبارت y = x++; از عملگر پس افزایشی استفاده کرده ایم. تابع این عملگر باید طوری تعریف شود که مقدار x را قبل از این که درون y قرار بگیرد، تغییر ندهد. می دانیم که اشاره گر *this به شیء جاری (مالک فراخوانی) اشاره دارد. کافی است مقدار این اشاره گر را در یک محل موقتی ذخیره کنیم و عمل افزایش را روی آن مقدار موقتی انجام داده و حاصل آن را بازگشت دهیم. به این ترتیب مقدار *this تغییری نمی کند و پس از شرکت در عمل جایگزینی، درون y قرار می گیرد:
class Ratio
{ public:
Ratio(int n=0, int d=1) : num(n) , den(d) { }
Ratio operator++(); //pre-increment
Ratio operator++(int); //post-increment
void print() { cout << num << '/' << den << endl; }
private:
int num, den;
};
int main()
{ Ratio x(22,7) , y = x++;
cout << "y = "; y.print();
cout << ", x = "; x.print();}
Ratio Ratio::operator++(int)
{ Ratio temp = *this;
num += den;
return temp;
}

538

عملگرهای پیش کاهشی و پس کاهشی نیز به همین شیوه عملگر های پیش افزایشی و پس افزایشی سربارگذاری می شوند. غیر از این ها، عملگرهای دیگری نیز مثل عملگر خروجی (<<) ، عملگر ورودی (>>) ، عملگر اندیس ([]) و عملگر تبدیل نیز وجود دارند که می توان آن ها را برای سازگاری برای کلاس های جدید سربارگذاری کرد.
539

پایان جلسه دهم
540

جلسه یازدهم
«ترکیب و وراثت»
541

مقدمه
ترکیب
وراثت
اعضای حفاظت شد
غلبه کردن بر وراثت
اشاره گرها در وراثت
توابع مجازی و چندریختی
نابودکننده مجازی <<<
«ترکیب و وراثت»
542

کلاس های پایه انتزاعی
پرسش های گزینه ای
پرسش های تشریحی
تمرین های برنامه نویسی
ضمیمه الف : پاسخ نامه پرسش های گزینه ای
ضمیمه ب:جدول اسکی
ضمیمه ج : کلمات کلیدی C++ استاندارد
ضمیمه د : عملگرهای C++ استاندارد
ضمیمه هـ : فهرست منابع و ماخذ
543

هدف کلی:
بیان اهمیت ترکیب و وراثت در شی گرایی و چگونگی انجام این کارها.
هدف های رفتاری:
انتظار می رود پس از پایان این جلسه بتوانید:
– علت استفاده از «ترکیب» و «وراثت» را در برنامه های شی گرا توضیح دهید.
– نحوه ترکیب دو یا چند کلاس را برای ایجاد کلاس جدید، بدانید.
– وراثت را تعریف کنید.
544

– «اعضای حفاظت شده کلاس» را تعریف کنید و تفاوت این اعضا با اعضای عمومی و خصوصی کلاس را شرح دهید.
– نحوه غلبه کردن بر وراثت را شرح دهید.
– «تابع مجازی» را تعریف کنید و علت استفاده از توابع مجازی را بدانید.
– «چندریختی» را تعریف کنید و شیوه پیاده سازی چندریختی در کلاس ها را بدانید.
– «کلاس پایه انتزاعی» را تعریف کنید و علت تعریف این کلاس ها را ذکر کنید.
545

مقدمه

اغلب اوقات برای ایجاد یک کلاس جدید، نیازی نیست که همه چیز از اول طراحی شود. می توانیم برای ایجاد کلاس مورد نظر، از تعاریف کلاس هایی که قبلا ساخته ایم، استفاده نماییم.
این باعث صرفه جویی در وقت و استحکام منطق برنامه می شود.
در شی گرایی به دو شیوه می توان این کار را انجام داد: ترکیب1 و وراثت2. در این جلسه خواهیم دید که چگونه و چه مواقعی می توانیم از این دو شیوه بهره ببریم.
546

ترکیب:
ترکیب کلاس ها (یا تجمیع کلاس ها) یعنی استفاده از یک یا چند کلاس دیگر در داخل تعریف یک کلاس جدید.
هنگامی که عضو داده ای کلاس جدید، شیئی از کلاس دیگر باشد، می گوییم که این کلاس جدید ترکیبی از سایر کلاس هاست.
به تعریف دو کلاس زیر نگاه کنید.
547

کلاس Date
کد زیر، کلاس Date را نشان می دهد که اشیای این کلاس برای نگهداری تاریخ استفاده می شوند.

class Date
{ public:
Date(int y=0, int m=0, int d=0) :
year(y), month(m), day(d) {};
void setDate(int y, int m, int d)
{ year = y; month = m; day = d; }
void getDate()
{ cin >> year >> month >> day ; }
void showDate()
{ cout << year << '/' << month << '/' << day ; }
private:
int year, month, day;
}
548

کلاس Book
کد زیر، کلاس Book را نشان می دهد که اشیای این کلاس برخی از مشخصات یک کتاب را نگهداری می کنند:
class Book
{ public:
Book(char* n = " ", int i = 0, int p = 0) :
name(n), id(i), page(p) { }
void printName() { cout << name; }
void printId() { cout << id; }
void printPage() { cout << page; }
private:
string name, author;
int id, page;
}
549

بهبود دادن کلاس Book
#include "Date.h"
class Book
{ public:
Book(char* n = " ", int i = 0, int p = 0) :
name(n), id(i), page(p) { }
void printName() { cout << name; }
void printId() { cout << id; }
void printPage() { cout << page; }
void setDOP(int y, int m, int d)
{ publish.setDate(y, m, d) ; }
void showDOP() { publish.showDate(); }
private:
string name, author;
int id, page;
Date publish;
}
550

وراثت :
وراثت روش دیگری برای ایجاد کلاس جدید از روی کلاس قبلی است. گاهی به وراثت «اشتقاق» نیز می گویند.
اگر از قبل با برنامه نویسی مبتنی بر پنجره ها آشنایی مختصر داشته باشید، احتمالا عبارت «کلاس مشتق شده» را فراوان دیده اید. این موضوع به خوبی اهمیت وراثت را آشکار می نماید.
551

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

غلبه کردن بر وراثت :
اگر Y زیر کلاسی از X باشد، آنگاه اشیای Y همه اعضای عمومی و حفاظت شده کلاس X را ارث می برند.
مثلا تمامی اشیای Ebook تابع دستیابی printName() از کلاس Book را به ارث می برند. به تابع printName() یک «عضو موروثی» می گوییم. گاهی لازم است یک نسخه محلی از عضو موروثی داشته باشیم.
یعنی کلاس فرزند، عضوی هم نام با عضو موروثی داشته باشد که مخصوص به خودش باشد و ارثی نباشد. برای مثال فرض کنید کلاس X یک عضو عمومی به نام p داشته باشد و کلاس Y زیر کلاس X باشد.
553

در این حالت اشیای کلاس Y عضو موروثی p را خواهند داشت. حال اگر یک عضو به همان نام p در زیرکلاس Y به شکل صریح اعلان کنیم، این عضو جدید، عضو موروثی هم نامش را مغلوب می کند.
به این عضو جدید، «عضو غالب» می گوییم. بنابراین اگر y1 یک شی از کلاس Y باشد، y1.p به عضو p غالب اشاره دارد نه به p موروثی.
البته هنوز هم می توان به p موروثی دسترسی داشت. عبارت y1.X::p به p موروثی دستیابی دارد.
554

هم می توان اعضای داده ای موروثی را مغلوب کرد و هم اعضای تابعی موروثی را.
یعنی اگر کلاس X دارای یک عضو تابعی عمومی به نام f() باشد و در زیرکلاس Y نیز تابع f() را به شکل صریح اعلان کنیم، آنگاه y1.f() به تابع غالب اشاره دارد و y1.X::f() به تابع موروثی اشاره دارد. در برخی از مراجع به توابع غالب override می گویند و داده های غالب را dominate می نامند. ما در این کتاب هر دو مفهوم را به عنوان اعضای غالب به کار می بریم. به مثال زیر نگاه کنید.
555

اعضای داده ای و تابعی غالب :
class X
{ public:
void f() { cout << "Now X::f() is runningn"; }
int a;
};
class Y : public X
{ public:
void f() { cout << "Now Y::f() is runningn"; }
// this f() overrides X::f()
int a;
// this a dominates X::a
};
556

سازنده ها و نابودکننده های والد:
class X
{ public:
X() { cout << "X::X() constructor executingn"; }
~X() { cout << "X::X() destructor executingn"; }
};
clas Y : public X
{ public:
Y() { cout << "Y::Y() constructor executingn"; }
~Y() { cout << "Y::Y() destructor executingn"; }
};
clas Z : public Y
{ public:
Z(int n) {cout << "Z::Z(int) constructor executingn";}
~Z() { cout << "Z::Z() destructor executingn"; }
};
int main()
{ Z z(44);
}
557

اشاره گرها در وراثت :
در شی گرایی خاصیت جالبی وجود دارد و آن این است که اگر p اشاره گری از نوع کلاس والد باشد، آنگاه p را می توان به هر فرزندی از آن کلاس نیز اشاره داد. به کد زیر نگاه کنید:
class X
{ public:
void f();
} class Y : public X // Y is a subclass of X
{ public:
void f();
}
int main()
{ X* p; // p is a pointer to objects of base class X
Y y;
p = &y; // p can also point to objects of subclass Y
}
558

اشاره گری از کلاس والد به شیئی از کلاس فرزند:
در برنامه زیر، کلاس Y زیرکلاسی از X است. هر دوی این کلاس ها دارای یک عضو تابعی به نام f() هستند و p اشاره گری از نوع X* تعریف شده:
class X
{ public:
void f() { cout << "X::f() executingn"; }
};
class Y : public X
{ public:
void f() { cout << "Y::f() executingn"; }
}
int main()
{ X x;
Y y;
X* p = &x;
p->f(); // invokes X::f() because p has type X*
p = &y;
p->f(); // invokes X::f() because p has type X*
}
559

توابع مجازی و چندریختی :
تابع مجازی تابعی است که با کلمه کلیدی virtual مشخص می شود. وقتی یک تابع به شکل مجازی اعلان می شود، یعنی در حداقل یکی از کلاس های فرزند نیز تابعی با همین نام وجود دارد. توابع مجازی امکان می دهند که هنگام استفاده از اشاره گرها، بتوانیم بدون در نظر گرفتن نوع اشاره گر، به توابع شیء جاری دستیابی کنیم. به مثال زیر دقت کنید.
560

استفاده از توابع مجازی:
class X
{ public:1 – Virtual function
virtual void f() { cout << "X::f() executingn"; }
};
class Y : public X
{ public:
void f() { cout << "Y::f() executingn"; }
}
int main()
{ X x;
Y y;
X* p = &x;
p->f(); // invokes X::f()
p = &y;
p->f(); // invokes Y::f()
}
561

چندریختی از طریق توابع مجازی :
سه کلاس زیر را در نظر بگیرید. بدون استفاده از توابع مجازی، برنامه آن طور که مورد انتظار است کار نمی کند:
class Book
{ public:
Book(char* s) { name = new char[strlen(s+1)];
strcpy(name, s);
}
void print() { cout << "Here is a book with name "
<< name << ".n";
}
protected:
char* name;
};
class Ebook : public Book
{ public:
Ebook(char* s, float g) : Book(s), size(g) {}
void print() { cout << "Here is an Ebook with name "
<< name << " and size "
<< size << " MB.n";
} private:
float size;
}
class Notebook : public Book
{ public:
Notebook(char* s, int n) : Book(s) , pages(n) {}
void print() { cout << "Here is a Notebook with name "
<< name << " and " << pages
<< " pages.n";
}
private:
int pages;
};
562

نابودکننده مجازی:
با توجه به تعریف توابع مجازی، به نظر می رسد که نمی توان توابع سازنده و نابودکننده را به شکل مجازی تعریف نمود زیرا سازنده ها و نابودگرها در کلاس های والد و فرزند، هم نام نیستند. در اصل، سازنده ها را نمی توان به شکل مجازی تعریف کرد اما نابودگرها قصه دیگری دارند.
مثال بعدی ایراد مهلکی را نشان می دهد که با مجازی کردن نابودگر، برطرف می شود.
563

حافظه گم شده :
به برنامه زیر دقت کنید:
class X
{ public:
x() { p = new int[2]; cout << "X(). "; }
~X() { delete [] p; cout << "~X().n" }
private:
int* p;
};
class Y : public X
{ public:
Y() { q = new int[1023]; cout << "Y() : Y::q = " << q
<< ". "; }
~Y() { delete [] q; cout << "~Y(). "; }
private:
int* q;
};
int main()
{ for (int i=0; i<8; i++)
{ X* r = new Y;
delete r;
}
}
564

کلاس های پایه انتزاعی :
در شی گرایی رسم بر این است که ساختار برنامه و کلاس ها را طوری طراحی کنند که بتوان آن ها را به شکل یک نمودار درختی شبیه زیر نشان داد:
565

پایان جلسه یازدهم
566


تعداد صفحات : 566 | فرمت فایل : ppt

بلافاصله بعد از پرداخت لینک دانلود فعال می شود