مبانی کامپیوتر و برنامه سازی فصل نهم : توابع
9 توابع در مبحث الگوریتمها راجع به مزایای استفاده از زیرالگوریتمها صحبت کردیم. در برنامهنویسی نیز مباحث مشابهی در مورد زیربرنامهها وجود دارد. هنگامی که یک برنامه بیش از حد بزرگ می شود، برنامهنویسی و مدیریت آن مشکل می گردد و ممکن است از عهده یک نفر خارج باشد. در چنین شرایطی، برنامه را به چندین بخش کوچکتر (زیربرنامه) تقسیم کرده و هریک را بعهده یک برنامهنویس می گذاریم. هر قسمت به تنهایی نوشته شده و رفع اشکال می گردد. سپس این زیربرنامهها در کنار یکدیگر قرار گرفته و تشکیل برنامه اصلی را می دهند. اینکار نه تنها در تقسیم کار به ما کمک می کند، بلکه باعث می شود خوانایی برنامه نیز بالاتر رفته و اشکالزدایی آن ساده تر گردد. علاوه براین می توان یک زیربرنامه را یکبار نوشت، و سپس در برنامه های مختلف از آن استفاده کرد که به این خاصیت، قابلیت استفاده مجدد از نرم افزار گفته می شود.
9 توابع در زبان C به هر زیربرنامه، یک تابع گفته می شود. 9 توابع در زبان C به هر زیربرنامه، یک تابع گفته می شود. یک تابع، تکه برنامه ای است که داده یا داده هایی را بعنوان ورودی دریافت، و داده یا داده هایی را بعنوان خروجی باز می گرداند. در زبان C، هر برنامه از یک یا چند تابع تشکیل می گردد، که یکی از آنها باید بنام main نامیده گردد و برنامه از این تابع شروع خواهد گردید. تابع main می تواند سایر توابع را فراخوانی نماید و هر یک از این توابع نیز می توانند به نوبه خود، توابع دیگر را فراخوانی نمایند. نکته جالب اینجاست که تابع فراخواننده نیازی به دانستن نحوه کار تابعی که فراخوانی می کند، ندارد و تنها باید از نحوه فراخوانی و مقدار خروجی آن آگاه باشد. این نحوه پنهانسازی جزئیات پیاده سازی، نقش بسیار مهمی در مهندسی نرم افزار دارد. تاکنون از توابع کتابخانه های استاندارد C استفاده نموده ایم. کتابخانه استاندارد C مجموعه ای از توابع و نوع دادهها است که برای انجام عملیاتی که عموما مورد نیاز برنامهنویسان است، طراحی شده و همراه کامپایلر در اختیار برنامهنویسان قرار داده شده است. بعنوان مثال توابع ورودی/خروجی مانند printf و scanf و یا توابع ریاضی مانند sqrt و sin که توسط بسیاری از برنامهنویسان مورد استفاده قرار می گیرند. اما ازطرف دیگر، برنامهنویس نیز می تواند توابع مورد نیاز خود را تعریف کرده و از آنها در برنامه خود استفاده نماید. به این دسته از توابع، توابع کاربر می گوییم.
1-9 توابع کاربر بکار گیری توابع شامل دو قسمت است: 1-9 توابع کاربر بکار گیری توابع شامل دو قسمت است: تعریف تابع استفاده از تابع (فراخوانی تابع) ما در مثالهای قبلی تابع main را در برنامه های خود تعریف می کردیم. تعریف توابع دیگر نیز بطور مشابه می باشد، که جزئیات آن را بررسی خواهیم کرد. البته تابع اصلی بطور اتوماتیک در ابتدای اجرای برنامه فراخوانی می گردد و برنامهنویس صریحا آن را احضار نمی کند، ولی سایر توابع باید از داخل تابع دیگری (از جمله main) بطور صریح فراخوانی گردند. البته ما قبلا توابعی همچون scanf و printf را فراخوانی کرده ایم، اما جزئیات مربوط به نحوه فراخوانی را در قسمت بعدی بررسی می نماییم.
1-1-9 تعریف تابع قالب کلی تعریف تابع بصورت زیر است: 1-1-9 تعریف تابع قالب کلی تعریف تابع بصورت زیر است: <return-type> <function-name> (<param-type param-name> , … ) { <local variable definitions> ; <statements> ; } در این قسمت هرگونه متغیری که برای انجام وظایف محوله به تابع مورد نیاز باشد، تعریف می گردد. این متغیرها نیز همانند پارامترهای تابع، محلی محسوب می شوند و در سایر توابع شناخته شده نیستند. با شروع هر تابع، این متغیرها بطور اتوماتیک ایجاد شده و پس از خاتمه آن از بین می روند. پس از نام تابع و در داخل پرانتز، لیست پارامترهای تابع قرار می گیرد. این لیست شامل تعریف تعدادی پارامتر است که با کاما ',' از یکدیگر جدا شده اند. تعریف هر پارامتر شامل نوع و سپس نام پارامتر می باشد. پارامترها در حقیقت رابط بین تابع احضار کننده و تابع احضار شونده هستند. در این قسمت، دستورات تشکیل دهنده بدنه تابع که وظایف مورد نظر برنامهنویس را انجام می دهند، قرار می گیرند. نوع مقدار بازگشتی : نوع داده مقداری که توسط تابع بازگشت داده می شود را نشان می دهد. نام تابع در حقیقت یک شناسه است که از همان قوانین نامگذاری مربوط به شناسهها تبعیت می کند. یک تابع توسط نام خود فراخوانی می گردد.
1-1-9 تعریف تابع مقدار بازگشتی تابع توسط دستور return به تابع فراخواننده برگشت داده می شود. برای این کار کافی است به شکل زیر عمل نماییم: return (<exp>) ; و یا return <exp>; چنانچه تابع مقدار بازگشتی نداشته باشد، از کلمه کلیدی void بجای نوع مقدار بازگشتی استفاده می نماییم. دراینصورت، دستور return نیاز به مقدار بازگشتی ندارد. البته چنانچه تابع بیش از یک مقدار بازگشتی داشته باشد، باید از تکنیکهای گفته شده در قسمت بعد استفاده نماییم. لازم به ذکر است که در قسمت دستورات تابع باید یک (یا چند) دستور return داشته باشیم که کنترل را به تابع فراخواننده باز گرداند. البته درمورد توابعی که مقداری را باز نمی گردانند، درصورت عدم وجود دستور return، کنترل پس از رسیدن به انتهای تابع یعنی } بطور خودکار به تابع فراخواننده باز می گردد.
1-1-9 تعریف تابع نکته مهم دیگر مکان تعریف توابع در یک برنامه C است. 1-1-9 تعریف تابع نکته مهم دیگر مکان تعریف توابع در یک برنامه C است. یک برنامه C می تواند دارای یک یا چند تابع باشد که یکی از آنها باید حتما main نامیده شود و همانطور که قبلا نیز گفته شد، اجرای برنامه از این تابع آغاز می گردد. توابع می توانند به هر ترتیبی تعریف شوند، اما معمولا تابع main در آخر توابع دیگر تعریف می گردد؛ گرچه این مسئله اجباری نیست. توابع باید بصورت پشت سر هم تعریف گردند و برخلاف بعضی از زبانهای دیگر، نمی توان یک تابع را در داخل تابع دیگر تعریف کرد. بعبارت دیگر، کلیه توابع در یک سطح قرار دارند و هیچ تابعی، شامل تابع دیگر نمی باشد.
2-1-9 فراخوانی توابع برای فراخوانی یک تابع باید از نام آن بعلاوه لیست آرگومانهای متناسب با پارامترهای تابع استفاده کرد. نکته مهم آنستکه باید تعداد، ترتیب و نوع آرگومانهای ارسالی با پارامترهای متناظرشان در تعریف تابع، منطبق باشد. در غیراینصورت ممکن است خطای نحوی و یا حتی خطای منطقی رخ دهد. هنگامیکه یک تابع فراخوانی می گردد، اجرای تابع فراخواننده بطور موقت متوقف شده و کنترل اجرا به تابع فراخوانی شده منتقل می گردد. پس از اتمام تابع فراخوانی شده و اجرای دستور return توسط آن، کنترل اجرا به تابع فراخواننده بازگشته و اجرا را از دستور بعدی، از سر می گیرد.
2-1-9 فراخوانی توابع چنانچه تابع هیچ مقداری را بازنگرداند، می توان آن را بصورت یک دستور مستقل فراخوانی کرد. بعنوان مثال : clrscr() ; اما توابعی که یک مقدار خروجی را باز می گردانند، می توان در یک عبارت نسبت دهی یا محاسباتی نیز بکاربرد. بعنوان مثال می توان تابع sqrt (که یک عدد را دریافت و جذر آن را باز می گرداند) را بصورت زیر استفاده کرد : a = sqrt(10) ; a = 2 * sqrt(b) + c ; نکته مهم آنستکه چنانچه فراخوانی تابع توسط مقدار باشد، آنگاه می توان بجای یک متغیر یا یک ثابت، یک عبارت محاسباتی را نیز به تابع ارسال کرد. بعنوان مثال فراخوانی زیر مجاز است: a = sqrt(2*b+8) ; در اینحالت، ابتدا عبارت محاسباتی ارزیابی شده و سپس مقدار آن بعنوان آرگومان به تابع ارسال می گردد. البته درصورتیکه فراخوانی توسط ارجاع باشد، فقط یک متغیر می تواند به تابع ارسال گردد و عبارت محاسباتی و یا حتی یک ثابت به تنهایی نیز مورد قبول نخواهد بود.
2-1-9 فراخوانی توابع void main() { int a, b; a = 5 ; b = square(a) ; 2-1-9 فراخوانی توابع void main() { int a, b; a = 5 ; b = square(a) ; printf(“a=%d and b=%d”,a,b) } a : 5 x : 5 int square(int x) { return(x * x) ; } b : 25 return 25 a = 5 and b = 25
2-9 چند نمونه از توابع مثال 1) برنامه ای بنویسید که مقدار ترکیب n به k را محاسبه نماید. #include <stdio.h> long int factorial(int number) { int i; long int f = 1L ; for (i=1; i<=number; i++) f *= i ; return(f) ; } void main() { int n, k ; long int result; printf("please enter n and k : "); scanf("%d %d", &n, &k) ; result = factorial(n) / ( factorial(k) * factorial(n-k) ) ; printf("result = %ld", result) ;
2-9 چند نمونه از توابع مثال 2) برنامه ای بنویسید که تعدادی عدد را دریافت و سپس حداکثر آنها را محاسبه و چاپ نماید. برای اینکار از تابعی بنام getMax که دو عدد را دریافت و حداکثر آنها را باز می گرداند، استفاده نمایید. #include <stdio.h> #include <values.h> int getMax(int a, int b) { if (a>b) return(a); else return(b) ; } void main() { int i, n, max, number ; printf("Please enter n: "); scanf("%d",&n); max = -MAXINT-1; for (i=0; i<n; i++) { printf("Please enter number: "); scanf("%d", &number); max = getMax(number, max); printf("Maximum is : %d", max);
2-9 چند نمونه از توابع مثال 3) تابعی بنویسید که یک خط 40 تایی از علامت * را چاپ نماید. سپس با استفاده از آن برنامهای بنویسید که ابتدا پیام Hello و سپس خط جداکننده فوق را چاپ کند. #include <stdio.h> void starLine() { int i; for (i=0; i<40; i++) printf("*"); printf("\n"); } void main() { printf("Hello\n"); starLine() ;
2-9 چند نمونه از توابع البته درچنین مواردی، بهتر است برنامهنویس تابع رسم خط را بصورت کلی تری بنویسد؛ بطوریکه در سایر برنامهها نیز بتواند از آن استفاده نماید. مثلا چنانچه تابع را بگونهای بنویسیم که کاراکتر جداکننده و تعداد آن را بعنوان ورودی دریافت کند، حالت کلی تری پیدا خواهد کرد. #include <stdio.h> void separatorLine(char sep,int n) { int i; for (i=0; i<n; i++) printf("%c",sep); printf(“\n”); } void main() { printf("Hello\n"); separatorLine('-' ,60) ;
3-9 نمونه اولیه توابع همانطور که قبلا گفته شد، توابع میتوانند به هر ترتیبی تعریف شوند، ولی معمولا تابع main در انتها قرار میگیرد. اما اگر بخواهیم دقیقتر صحبت کنیم، تعریف هر تابع باید قبل از فراخوانی آن صورت پذیرد. چنانچه تابعی قبل از آنکه تعریف شود، فراخوانی گردد؛ یک خطای کامپایل رخ خواهد داد. اما میتوان این خطا را با استفاده از نمونه اولیه تابع (prototype) که به آن پیش تعریف نیز گفته میشود، از بین برد. یک نمونه اولیه تابع، شامل نام تابع، نوع دادهای که باز میگرداند و همچنین تعداد و نوع پارامترهای تابع است. برای مثال، نمونه اولیه تابع factorial بصورت زیر است: long int factorial(int) ; این نمونه اولیه میگوید که تابع factorial یک آرگومان از نوع int دریافت و یک مقدار از نوع long int باز میگرداند. علامت ; انتهای دستور نشان میدهد که این فقط یک نمونه اولیه بوده و شامل تعریف بدنه تابع نمیباشد. نمونه اولیه یک تابع حتما باید با تعریف آن (که در قسمتهای بعدی برنامه آمده است) یکسان باشد و گرنه یک خطای کامپایل رخ خواهد داد. کامپایلر از نمونه اولیه تابع برای بررسی درستی نحوه فراخوانی تابع (از نظر تعداد و نوع آرگومانها) استفاده میکند.
4-9 انواع فراخوانی توابع بطورکلی، به دو روش میتوان یک تابع را فراخوانی کرد: فراخوانی توسط مقدار (Call by Value) فراخوانی توسط ارجاع (Call by Reference)
1-4-9 فراخوانی توسط مقدار در فراخوانی توسط مقدار، آرگومانهای ارسالی توسط تابع فراخواننده، در پارامترهای متناظر تابع فراخوانی شده، کپی میگردند. بنابراین تابع فراخوانی شده عملیات خود را برروی یک کپی از آرگومانهای ارسالی انجام میدهد. درنتیجه، در صورت انجام هرگونه تغییری برروی این کپی توسط تابع فراخوانی شده، متغیر اصلی در تابع فراخواننده تغییر نخواهد کرد. مزیت این نوع فراخوانی در این است که میتوان بدون هیچ نگرانی از تغییر ناخواسته متغیرها، آنها را به هر تابعی ارسال کرد، چرا که فقط یک کپی از آنها به تابع ارسال میشود. در زبان C، در حالت عادی فراخوانی توسط مقدار صورت میپذیرد. برای روشن شدن موضوع به مثال اسلاید بعدی توجه کنید.
1-4-9 فراخوانی توسط مقدار void test(int a, int b) { 1-4-9 فراخوانی توسط مقدار void test(int a, int b) { printf("Function test : a=%d and b=%d \n",a,b); a ++ ; b *= 2; } void main() { int x, y; x = 10; y = 8; printf("Function main : x=%d and y=%d \n",x,y); test(x,y) ; Function main : x=10 and y=8 Function test : a=10 and b=8 Function test : a=11 and b=16
1-4-9 فراخوانی توسط مقدار void main() { … void test(int a, int b) { … 1-4-9 فراخوانی توسط مقدار void main() { … test(x,y) ; } void test(int a, int b) { … } x 10 x و y در متغیرهای a و b کپی می شوند a 10 11 y 8 b 8 16 a و b تغییر کرده اند، اما متغیرهای اصلی x و y بدون تغییر مانده اند.
2-4-9 فراخوانی توسط ارجاع در این روش فراخوانی، خود آرگومانهای اصلی (بجای یک کپی از آنها) به تابع فراخوانی شده ارسال می گردد. در نتیجه، هرگونه تغییری در پارامترهای تابع فراخوانی شده، مقدار آرگومانهای اصلی در تابع فراخوانی کننده را نیز تغییر خواهد داد. در C اولیه، تنها راه فراخوانی توابع توسط ارجاع، استفاده از متغیرهای اشاره گر بود که در فصول بعدی این روش بررسی خواهد گردید. اما در استاندارد جدید C، می توان این کار را به روش ساده تر ی و با استفاده از متغیرهای ارجاعی انجام داد. یک متغیر ارجاعی، در حقیقت یک نام مترادف و یا یک جایگزین برای یک متغیر دیگر است. برای تعریف یک متغیر ارجاعی از علامت & پس از نوع متغیر مورد ارجاع استفاده می کنیم. بعنوان مثال به نمونه زیر توجه کنید: int a = 10; int &r = a; از این پس متغیر r، نام دیگری برای متغیر a محسوب می شود و هرگونه تغییری در r، باعث تغییر a نیز خواهد شد.
2-4-9 فراخوانی توسط ارجاع #include <stdio.h> void main() { 2-4-9 فراخوانی توسط ارجاع #include <stdio.h> void main() { int a = 10; int &r = a; r ++; printf("a=%d and r=%d \n",a,r); a ++; } a = 11 and r = 11 a = 12 and r = 12
2-4-9 فراخوانی توسط ارجاع برای ارسال آرگومانها توسط ارجاع، کافی است پارامترهای تابع مورد نظر را بصورت متغیر ارجاعی تعریف نماییم. در اینصورت، این پارامترها در حقیقت یک ارجاع به آرگومانهای ارسالی خواهند بود و در نتیجه هرگونه تغییری در آنها، آرگومانهای اصلی در تابع فراخوانی کننده را نیز تغییر خواهد داد. بعبارت بهتر، در این روش بجای آنکه یک کپی از آرگومانها در پارامترها قرار گیرد، خود آرگومانها به تابع فراخوانی شده ارسال خواهند شد. بعنوان مثال، همان تابع قسمت قبلی را که بصورت فراخوانی توسط مقدار عمل می کرد، مجددا به روش فراخوانی با ارجاع باز نویسی می کنیم تا تفاوت این دو مشخص گردد.
2-4-9 فراخوانی توسط ارجاع void test(int &a, int &b) { 2-4-9 فراخوانی توسط ارجاع void test(int &a, int &b) { printf("Function test : a=%d and b=%d \n",a,b); a ++ ; b *= 2; } void main() { int x, y; x = 10; y = 8; printf("Function main : x=%d and y=%d \n",x,y); test(x,y) ; Function main : x=10 and y=8 Function test : a=10 and b=8 Function test : a=11 and b=16 Function main : x=11 and y=16
2-4-9 فراخوانی توسط ارجاع void main() { … test(x,y) ; } 2-4-9 فراخوانی توسط ارجاع void main() { … test(x,y) ; } void test(int &a, int &b) { … } x 10 11 y a 8 16 b
2-4-9 فراخوانی توسط ارجاع اکنون سوال اصلی این است که در چه مواردی از فراخوانی توسط ارجاع استفاده کنیم؟ فراخوانی توسط ارجاع در دو مورد کاربرد دارد، که هریک را با ذکر یک مثال توضیح می دهیم. می دانیم که هر تابع فقط می تواند یک مقدار را توسط دستور return باز گرداند. در مواردی که تابع باید بیش از یک مقدار را باز گرداند، تنها راه استفاده از فراخوانی توسط ارجاع است. بدین صورت که تعدادی پارامتر خروجی را بصورت ارجاعی به تابع ارسال می کنیم، و تابع پس از انجام محاسبات، خروجی را در این متغیرها قرار داده و باز می گرداند. از آنجا که پارامترها توسط ارجاع ارسال شده اند، تغییرات اعمال شده توسط تابع (قرار دادن مقدار خروجی در آنها) به تابع فراخواننده منتقل خواهد شد.
2-4-9 فراخوانی توسط ارجاع مثال 5) برنامه ای بنویسید که ضرایب یک معادله درجه دوم را دریافت و ریشه های آن را محاسبه وچاپ نماید. #include <stdio.h> #include <math.h> int equation(int a, int b, int c, float &x1, float &x2) { float delta; if (a==0) return(0) ; // معادله جواب ندارد delta = b*b – 4*a*c ; if (delta < 0) return(0) ; // معادله جواب ندارد else if (delta == 0) { // معادله یک جواب دارد x1 = -b / (2*a) ; return(1) ; } else { // معادله دو جواب دارد delta = sqrt(delta) ; x1 = (-b + delta) / (2*a) ; x2 = (-b – delta) / (2*a) ; return(2) ;
2-4-9 فراخوانی توسط ارجاع void main() { 2-4-9 فراخوانی توسط ارجاع void main() { int resultNo, coef1, coef2, coef3; float result1,result2 ; printf("Please enter coefficients: "); scanf("%d %d %d", &coef1, &coef2, &coef3); resultNo = equation(coef1,coef2,coef3,result1,result2); if (resultNo == 0) printf("There is no answer! \n"); else if (resultNo==1) printf("There is 1 answer, x=%f \n", result1); else printf("There are 2 answers, x1=%f and x2=%f \n", result1, result2); } Please enter coefficients : 3 -7 2 There are two answers, x1 = 2.000000 and x2 = 0.333333
2-4-9 فراخوانی توسط ارجاع کاربرد دوم در مواردی است که آرگومان ارسالی به تابع، همزمان نقش ورودی و خروجی را دارا باشد. یعنی آرگومان علاوه بر آنکه مقداری را به تابع ارسال می کند، ممکن است مقداری را به تابع فراخواننده نیز بازگرداند. مثال زیر موضوع را روشن تر میکند. مثال 6) برنامه ای بنویسید که دو عدد را از کاربر دریافت و با استفاده از یک تابع، مقدار آن دو را جابجا نموده و حاصل را چاپ نماید. به حل این مثال در اسلاید بعدی توجه کنید.
2-4-9 فراخوانی توسط ارجاع #include <stdio.h> 2-4-9 فراخوانی توسط ارجاع #include <stdio.h> void swap(int &a, int &b) { int temp; temp = a; a = b; b = temp; } void main() { int x, y; printf("enter x : "); scanf("%d", &x); printf("enter y : "); scanf("%d", &y); printf("x = %d and y = %d \n", x, y); swap(x,y); printf("now x = %d and y = %d \n", x, y); enter x : 8 enter y : 12 x = 8 and y = 12 now x = 12 and y = 8
5-9 حوزه شناخت متغیر یکی از مسائل مهم در مورد متغیرها، حوزه شناخت (Scope) متغیر است که تعیین می نماید متغیر در چه قسمتهایی از برنامه شناخته شده است و در چه قسمتهایی قابل استفاده نیست. بطور کلی متغیرها به دو دسته تقسیم می شوند: متغیرهای محلی (local variable): متغیرهایی هستند که در داخل یک بلاک {} تعریف شدهاند و فقط در محدوده همان بلاک شناخته شده هستند. نمونه این دسته از متغیرها، متغیرهای محلی توابع هستند که در داخل بلاک مربوط به تابع تعریف می شوند و فقط در همان تابع شناخته شده هستند. البته دو تابع مختلف می توانند دارای متغیرهای همنام باشند، که در اینصورت این دو متغیر مجزا بوده و هیچ ارتباطی به یکدیگر ندارند. لازم به ذکر است که پارامترهای یک تابع نیز جزو متغیرهای محلی آن تابع محسوب می گردند. نکته دیگر اینکه گرچه معمولا متغیرهای محلی در داخل تابع تعریف می شوند، اما هر بلوک دلخواه می تواند دارای متغیرهای محلی باشد. مثلا یک دستور if مرکب می تواند در داخل بلوک خود، متغیرهای محلی را تعریف کند که فقط در داخل همان بلوک شناخته شده باشند (به مثال بعدی مراجعه کنید). متغیرهای سراسری (global variable): متغیرهای هستند که در خارج کلیه بلوکها (و توابع از جمله main) تعریف شدهاند و در کل توابع برنامه (در حقیقت کل فایل مربوط به برنامه) شناخته شده و قابل استفاده می باشند. از آنجا که کلیه توابع برنامه به این متغیرها دسترسی دارند، هرگونه تغییری در این متغیرها توسط یکی از توابع، در سایر توابع نیز قابل رویت خواهد بود.
5-9 حوزه شناخت متغیر int a ; void main() { int b; a = 10 ; b = 5 ; 5-9 حوزه شناخت متغیر متغیر سراسری int a ; void main() { int b; a = 10 ; b = 5 ; if (a>0) { int c ; c = 8; …. } void test() { c = 5; a = 20; متغیر محلی برای main متغیر سراسری a در اینجا قابل دسترسی است به متغیر محلی b نیز میتوان دسترسی داشت متغیر محلی برای بلوک if در این قسمت علاوه بر متغیر محلی c، به متغیرهای a و b نیز میتوان دسترسی پیدا کرد. در این قسمت به متغیرهای a و b دسترسی داریم، اما دیگر نمیتوان به متغیر محلی c مربوط به بلوک if دسترسی پیدا کرد. متغیر محلی برای test این متغیر ارتباطی با متغیر c در تابع main ندارد در این قسمت نه تنها میتوان به متغیر محلی c دسترسی داشت، بلکه متغیر سراسری a نیز قابل استفاده است. اما به متغیرهای محلی سایر توابع، مانند متغیر b دسترسی نداریم.
5-9 حوزه شناخت متغیر نکته مهم آنستکه درصورتیکه یک تابع دارای یک متغیر محلی همنام با یک متغیر سراسری باشد، در اینصورت هرگونه ارجاع به این نام مشترک، به متغیر محلی رجوع خواهد کرد (به مثال قسمت بعدی رجوع شود). چه موقع از متغیر های سراسری استفاده کنیم؟ متغیرهای سراسری هنگامی مفید هستند که یک داده بین چندین تابع بصورت مشترک استفاده شود. در این حالت نیازی به ارسال متغیرهای مشترک از طریق پارامترها نمی باشد. اما متاسفانه از آنجا که متغیرهای سراسری در کلیه توابع در دسترس هستند، ممکن است بصورت ناخواسته دچار تغییر شوند. علاوه براین اشکال زدایی آنها نیز بسیار مشکل است، چرا که محل بروز خطا مشخص نیست و هریک از توابع ممکن است مقدار متغیر را تغییر داده باشند. بنابراین در برنامهنویسی ساختیافته توصیه می گردد تا حد ممکن از متغیرهای سراسری استفاده نکنید.
6-9 ردههای ذخیره سازی رده ذخیره سازی یک متغیر، مدت زمان حضور آن را در برنامه تعیین مینماید. بعبارت دیگر، رده ذخیره سازی تعیین مینماید یک متغیر چه موقع بوجود میآید و چه زمانی از بین میرود. در هنگام تعریف یک متغیر، باید به همراه نوع آن، رده ذخیره سازی آن را نیز مشخص کرد. چنانچه اینکار صورت نپذیرد، کامپایلر از رده ذخیره سازی پیش فرض استفاده خواهد کرد. از آنجایی که در کلیه مثالهایی که تاکنون ذکر شده اند، رده ذخیره سازی بطور صریح مشخص نشده بود، همگی از رده پیش فرض در نظر گرفته شده اند. بطور کلی دو رده ذخیره سازی برای متغیرها وجود دارد: رده ذخیره سازی اتوماتیک رده ذخیره سازی ایستا
1-6-9 رده ذخیره سازی اتوماتیک 1-6-9 رده ذخیره سازی اتوماتیک متغیرهای متعلق به رده ذخیره سازی اتوماتیک، هنگام ورود به بلوکی که این متغیرها در آن اعلان شده اند، ایجاد شده و در طول اجرای این بلوک در حافظه وجود دارند؛ به محض خاتمه بلوک، این متغیرها نیز از بین رفته و حافظه آنها پس گرفته میشود. متغیرهای محلی (شامل پارامترهای توابع)، معمولا از این رده میباشند. بعنوان مثال، متغیرهای محلی یک تابع، به محض فراخوانی تابع ایجاد میشوند و در حین اجرای تابع در حافظه حضور دارند. با پایان یافتن اجرای تابع، این متغیرها نیز از بین میروند. این مسئله باعث میشود که صرفه جویی قابل توجهی در حافظه داشته باشیم. چرا که هرگاه به متغیری نیاز داریم ایجاد شده و با پایان یافتن کار نیز حافظه آن آزاد میشود. برای تعریف یک متغیر اتوماتیک، باید از کلمه کلیدی auto، قبل از مشخصه نوع متغیر، استفاده نماییم. بعنوان مثال به نمونه زیر دقت نمایید : void test(int a, int b) { auto int i; float k; … // دستورات تابع }
1-6-9 رده ذخیره سازی اتوماتیک 1-6-9 رده ذخیره سازی اتوماتیک نوع دیگری از متغیرهای اتوماتیک نیز وجود دارد. همانطور که میدانید کلیه متغیرهای برنامه در حافظه اصلی کامپیوتر (RAM) ذخیره میگردند. اما پردازنده اصلی (CPU) برای پردازش دادهها باید ابتدا آنها را از حافظه اصلی به یک حافظه داخلی بنام ثبات یا Register منتقل نماید. گرچه سرعت دسترسی به حافظه اصلی بسیار بالا است، اما بهرحال مدتی زمان صرف این انتقال میشود. پس از آن پردازنده میتواند با سرعت بسیار بالا عملیات را برروی ثباتهای داخلی خود انجام داده و حاصل را مجددا به حافظه اصلی منتقل نماید. حال اگر متغیری داشته باشیم که عملیات زیادی بر روی آن انجام شود، مانند متغیرهای شمارنده حلقه، میتوانیم از کامپایلر بخواهیم که آن را بجای حافظه اصلی، بطور مستقیم در داخل یکی از ثباتهای پردازنده ذخیره نماید. با این کار دیگر نیازی به عملیات انتقال نیست و سرعت بنحو قابل ملاحظهای افزایش مییابد. برای اینکار کافی است که از کلمه کلیدی register استفاده نماییم. بعنوان مثال در تابع قبلی میتوانستیم متغیر i را بصورت زیر تعریف نماییم: register int i; با این وجود دقت کنید که تعداد ثباتهای پردازنده محدود است و کامپایلر فقط درصورتی این درخواست را قبول مینماید که ثبات خالی وجود داشته باشد؛ در غیراینصورت متغیر را در حافظه اصلی ذخیره خواهد کرد(اما هیچ خطایی اعلام نخواهد کرد). همانطور که گفته شد فقط از متغیرهای بسیار پرکاربرد، بخصوص متغیرهایی که در داخل یک حلقه استفاده میشوند، بعنوان متغیر ثبات استفاده نمایید.
2-6-9 رده ذخیره سازی ایستا متغیرهای متعلق به رده ذخیره سازی ایستا، از ابتدای آغاز برنامه ایجاد میشوند و تا پایان برنامه نیز در حافظه حضور دارند. متغیرهای سراسری به این دسته متعلق هستند. با شروع اجرای برنامه به متغیرهای سراسری حافظه تخصیص داده میشود. پس از آن کلیه توابع قادر به دیدن و تغییر مقدار آنها هستند، اما نمیتوانند حافظه تخصیص یافته به این متغیرها را بازپس بگیرند. در پایان و پس از خاتمه تابع main، حافظه تخصیص یافته به این متغیرها بازپس گرفته میشود. اما علاوه بر متغیرهای سراسری، متغیرهای محلی نیز میتوانند از رده ذخیره سازی ایستا تعریف شوند. اگر یک متغیر محلی، بصورت ایستا تعریف گردد؛ فقط یکبار و آن هم در شروع اجرای برنامه ایجاد شده و مقدار اولیه خواهد گرفت(البته درصورتیکه به آن مقدار اولیه داده باشیم). پس از آن، با هر بار اجرای تابع، قادر به دسترسی به این متغیر خواهیم بود(چرا که یک متغیر محلی است) ؛ اما با خاتمه تابع این متغیر از بین نرفته و مقدار آن تا فراخوانی بعدی تابع حفظ خواهد شد. متغیرهای محلی ایستا هنگامی از بین میروند که برنامه اصلی خاتمه یابد. از این متغیرها هنگامی استفاده میشود که بخواهیم مقدار یک متغیر محلی در فراخوانیهای متوالی یک تابع حفظ شود(البته بدون اینکه سایر توابع به آن دسترسی داشته باشند، درغیراینصورت آن را بصورت سراسری تعریف میکردیم). برای تعریف یک متغیر محلی از رده ذخیره سازی ایستا، از کلمه کلیدی static استفاده میشود. البته استفاده از این کلمه الزامی است، چرا که متغیرهای محلی بطور پیش فرض از رده ذخیره سازی اتوماتیک درنظر گرفته میشوند.
2-6-9 رده ذخیره سازی ایستا #include <stdio.h> 2-6-9 رده ذخیره سازی ایستا #include <stdio.h> void computeSum(int number) { int autoSum = 0; static int staticSum = 0; autoSum += number; staticSum += number ; printf("autoSum = %d and staticSum = %d \n",autoSum,staticSum); } void main() { int i; for (i=1; i<=5; i++) computeSum(i) ; number 1 3 4 2 5 autoSum 1 4 3 5 2 staticSum 10 15 3 6 1 i 3 4 1 5 2
2-6-9 رده ذخیره سازی ایستا نوع دیگری از رده ذخیره سازی ایستا، ردهextern است. این رده هنگامی بکار می رود که برنامه در چندین فایل مختلف قرار داشته باشد. در برنامه های بزرگ که دارای توابع متعددی هستند، برای جلوگیری از بزرگ و پیچیده شدن بیش از حد فایل برنامه، آن را بطور منطقی به چندین فایل تقسیم می کنند. بدین صورت که کلیه توابع و داده های مرتبط با یکدیگر را در یک فایل قرار می دهند. سپس هر فایل بصورت مجزا کامپایل شده و درنهایت تمامی آنها با یکدیگر پیوند (link) خورده و تشکیل فایل اجرایی نهایی را می دهند. این روش باعث مدیریت بهتر پروژه های بزرگ می شود. اما مشکل هنگامی است که بخواهیم یک متغیر سراسری را در توابع موجود در چند فایل مختلف، مورد استفاده قرار دهیم. مسلما این متغیر سراسری باید در یکی از فایلها تعریف شود. اما در مورد سایر فایلها چه باید بکنیم؟ متاسفانه هریک از دو راه زیر منجر به شکست می شود: اگر متغیر را در سایر فایلها بدون تعریف مجدد استفاده نماییم، کامپایلر اعلام خطا کرده و متغیر را نخواهد شناخت. اگر متغیر را در سایر فایلها نیز تعریف مجدد نماییم، کامپایلر اعلام خطا نخواهد کرد، اما پیوند زننده(linker) در هنگام ترکیب فایلها با هم متوجه تعریف چند متغیر با نام یکسان شده و اعلام خطا خواهد کرد. تنها راه حل آن است که این متغیر را در یک فایل تعریف کرده و سپس در سایر فایلها آن را بعنوان یک متغیر خارجی (extern) اعلان (و نه تعریف) نماییم. مشخصه extern به کامپایلر اعلان می کند که این متغیر در جای دیگری (معمولا یک فایل دیگر) تعریف شده است و بنابراین می تواند بدون اعلان خطا از آن استفاده کند؛ اما این مشخصه باعث تعریف مجدد متغیر نمی گردد. برای تعریف متغیر به شکل خارجی بصورت زیر عمل می کنیم: extern int a;
2-6-9 رده ذخیره سازی ایستا File1.C File2.C int k = 0; extern int k; 2-6-9 رده ذخیره سازی ایستا int k = 0; void F1() { …. k ++; // k دسترسی به متغیر سراسری … } void main() { File1.C extern int k; void F2() { … k -- ; // دسترسی به متغیر سراسری مشترک } void F3() { File2.C
2-6-9 رده ذخیره سازی ایستا x in function main is 1 2-6-9 رده ذخیره سازی ایستا #include <stdio.h> int x = 1; // global variable x void a() { int x = 10 ; // local automatic variable x printf("x in function a is %d \n",x); x ++; } void b() { static int x = 20 ; // local static variable x printf("x in function b is %d \n",x); void c() { printf("x in function c is %d \n",x); void main() { printf("x in function main is %d \n",x); a() ; b() ; c() ; x in function main is 1 x in function a is 10 x in function a is 11 x in function b is 20 x in function b is 21 x in function c is 1 x in function c is 2 x in function main is 2 x in function b is 22 x in function c is 3 x in function main is 3
7-9 مقادیر پیش فرض برای پارامترها 7-9 مقادیر پیش فرض برای پارامترها در بسیاری از موارد، توابعی داریم که دارای تعداد زیادی پارامتر هستند که در هربار فراخوانی باید آرگومانهای متناظر با هریک را به تابع ارسال کرد. چنانچه تعداد آرگومانهای ارسالی، با تعداد پارامترها یکسان نباشد، یک خطای کامپایل ایجاد می گردد. اما در بعضی موارد، مقادیر بعضی از این پارامترها در اکثر موارد مشخص است و فقط در شرایط خاص تغییر می کند. بعنوان مثال فرض کنید تابعی نوشته اید که یک پنجره را مکان موردنظر ترسیم میکند. پارامترهای متداول برای چنین تابعی عبارتند از: مختصات شروع، رنگ زمینه، رنگ متن، نوع حاشیه (یک خطی، دوخطی و ...) اما فرض کنیم پنجره های متداول در برنامه ما دارای رنگ زمینه آبی و رنگ متن سفید با حاشیه دوخطی هستند. در اینصورت در اکثر موارد بجز اطلاعات مربوط به مختصات پنجره، بقیه اطلاعات بصورت تکراری ارسال می گردند. در چنین مواردی می توان از پارامترهای پیش فرض استفاده نمود. چنانچه یک پارامتر از تابع دارای مقدار پیش فرض باشد، آنگاه تابع فراخواننده می تواند هیچ آرگومانی متناظر با این پارامتر ارسال ننماید. در اینصورت تابع فراخوانده شده از مقدار پیش فرض برای آن پارامتر استفاده می نماید. برای تعیین مقدار پیش فرض برای پارامتر، کافی است که در هنگام تعریف پارامتر با استفاده از عملگر نسبت دهی (=) مقدار پیش فرض را به پارامتر نسبت دهیم.
7-9 مقادیر پیش فرض برای پارامترها 7-9 مقادیر پیش فرض برای پارامترها #include <stdio.h> int sum(int a, int b=0, int c=0) { return(a+b+c); } void main() { printf("sum(5,10,20) = %d \n", sum(5,10,20) ); printf("sum(5,10) = %d \n", sum(5,10) ); printf("sum(5) = %d \n", sum(5) ); a 5 b 10 c 15 sum(5,10,20) = 35 sum(5,10) = 15 sum(5) = 5
7-9 مقادیر پیش فرض برای پارامترها 7-9 مقادیر پیش فرض برای پارامترها البته در نحوه استفاده از پارامترهای پیش فرض دو محدودیت وجود دارد: تعریف پارامترهای پیش فرض حتما باید از سمت راست ترین پارامتر آغاز شده و به سمت چپ ادامه یابد. بعنوان نمونه در مثال قبل چنانچه پارامتر c مقدار پیش فرض نداشته باشد، پارامتر b نیز قادر به اعلام مقدار پیش فرض نیست. علاوه براین پارامتر a نیز فقط درصورتی می تواند مقدار پیش فرض داشته باشد که پارامترهای b و c هر دو دارای مقدار پیش فرض باشند. یعنی اعلان زیر خطا می باشد: int sum(int a=0, int b, int c=0) // compile error در هنگام فراخوانی یک تابع که دارای پارامترهای پیش فرض است، آرگومانهای ارسالی از چپ به راست به پارامترها اختصاص می یابند. یعنی در صورتیکه هنگام فراخوانی تابع sum دو آرگومان به آن ارسال شود، اولی به a و دومی به b تخصیص خواهد یافت. تحت هیچ شرایطی نمی توان بدون اینکه مقداری برای b ارسال شود، آرگومانی را برای c ارسال نمود.
8-9 سربارگذاری توابع در برخی موارد، تابعی داریم که یک وظیفه خاص را برای چندین نوع داده مختلف انجام می دهد. بعنوان مثال تابع max را درنظر بگیرید که دو داده را بعنوان ورودی دریافت و حداکثر آنها را باز می گرداند. چنانچه این تابع را برای دو ورودی از نوع عدد صحیح بنویسیم، آنگاه برای ورودی های اعشاری درست عمل نخواهد کرد. بنابراین ممکن است برنامهنویس ترجیح دهد دو نسخه از این تابع داشته باشد: یکی برای اعداد صحیح و یکی برای اعداد اعشاری. اما از آنجا که نمی توان دو شناسه همنام تعریف کرد، بنابراین ممکن است مجبور شویم از دو نام مجزا استفاده نماییم، مانند intMax برای اعداد صحیح و doubleMax برای اعداد اعشاری. خوشبختانه با استفاده از سربار گذاری توابع، می توان این مشکل را حل کرد. با استفاده از سربار گذاری، می توان توابعی تعریف کرد که دارای نام یکسان باشند، ولی لیست پارامترهای ورودی آنها متفاوت باشد. در اینصورت در هنگام فراخوانی تابع، زبان C با توجه به نوع آرگومانهای ارسالی، تابع مناسب را انتخاب کرده و فراخوانی می نماید. تفاوت در لیست پارامترهای ورودی توابع سربارگذاری شده، می تواند شامل یک یا هر دو مورد زیر باشد: تفاوت در تعداد پارامترها تفاوت در نوع داده یک یا چند پارامتر نکته آخر این که سربار گذاری نمی تواند بر اساس نوع داده خروجی انجام شود.
8-9 سربارگذاری توابع void main() { 8-9 سربارگذاری توابع int max(int a, int b) { if (a > b) return(a); else return(b); } double max(double a, double b) { if (a > b) return(a); else return(b); } int max(int a, int b, int c) { if (a > b) if (a > c) return(a); else return(c); else if (b > c) return(b); } void main() { printf("max(8, 10) = %d \n", max(8, 10) ); printf("max(4.23, 3.712) = %f \n", max(4.23, 3.712) ); printf("max(15, 3, 20) = %d \n", max(15, 3, 20) ); } max(8, 10) = 10 max(4.23, 3.712) = 4.23 max(15, 3, 20) = 20
9-9 الگوهای تابعی الگوهای تابعی، در حقیقت شکل پیشرفته تری از سربارگذاری هستند. اگر به تابع max در قسمت قبل دقت کنید، این تابع یک بار برای نوع داده int و یک بار نیز برای نوع داده double نوشته شده است، اما الگوریتم هر دو تابع یکسان است. در چنین مواردی می توان بجای نوشتن چندین تابع سربارگذاری شده با الگوریتم یکسان، تنها یک الگوی تابعی نوشت و ساخت توابع با نوع داده های مختلف را بعهده کامپایلر نهاد. برای روشن شدن موضوع، بحث را با یک مثال ادامه می دهیم. تابع swap را در نظر بگیرید که یک دو متغیر را دریافت و مقادیر آنها را جابجا می کند. این تابع دارای کاربردهای متعددی بوده و ممکن است با نوع داده های مختلفی مورد استفاده قرار گیرد. یک روش سربارگذاری تابع swap برای انواع داده های مورد نیاز است. اما روش بهتر استفاده از یک الگو است. تعریف الگو با استفاده از کلمه کلیدی template صورت می پذیرد و پس از آن لیست پارامترهای الگو در داخل علامت <> قرار می گیرند. پارامترهای الگو، مشابه پارامترهای تابع هستند، با این تفاوت که بجای اینکه حاوی مقدار باشند، حاوی نوع داده هستند. در هنگام استفاده از یک الگو، نوع داده مورد نظر از طریق این پارامترها به الگو ارسال می شود. برای تعریف هر پارامتر الگو، از کلمه کلیدی class بعلاوه یک شناسه که نماینده نوع داده است، استفاده می شود. در اینجا class به معنای هر نوع داده دلخواه است. پس از کلمه کلیدی template و لیست پارامترها، تعریف تابع مربوط به الگو شروع می شود که همانند سایر توابع است؛ با این تفاوت که می توان از پارامترهای الگو، همانند نوع داده های عادی در تعریف متغیرهای تابع استفاده نمود.
9-9 الگوهای تابعی template <class T> void swap(T &a, T &b) { 9-9 الگوهای تابعی template <class T> void swap(T &a, T &b) { T temp; temp = a; a = b; b = temp ; } void swap(int &a, int &b) { int temp; temp = a; a = b; b = temp ; } void swap(double &a, double &b) { double temp; temp = a; a = b; b = temp ; } void swap(char &a, char &b) { char temp; temp = a; a = b; b = temp ; } void main() { int a=5 , b=10; double c = 1.1 , d = 2.2 ; char e = 'a' , f = 'b' ; swap(a,b); swap(c,d); swap(e,f) ; printf("a = %d and b = %d \n", a, b); printf("c = %f and d = %f \n", c, d); printf("e = %c and f = %c \n", e, f); }
10-9 توابع درون برنامه ای فراخوانی یک تابع دارای سربار زمانی و حافظه ای می باشد. با فراخوانی هر تابع باید برای متغیرهای محلی و پارامترهای آن حافظه تخصیص داده شود، آرگومانهای ارسالی در پارامترهای تابع کپی شوند و ... که همگی این موارد باعث صرف زمان و حافظه می شوند. به همین دلیل برای انجام عملیات کوتاه، استفاده از تابع ایده چندان خوبی نیست؛ چراکه گرچه خوانایی برنامه بالا می رود، اما برای انجام یک کار کوچک، هزینه زیادی به سیستم تحمیل می گردد. برای رفع این مشکل، زبان C از توابع درون برنامه ای یا inline استفاده می کند. اگر قبل از شروع تعریف تابع (یعنی قبل از نوع مقدار بازگشتی) از کلمه کلیدی inline استفاده شود، به کامپایلر توصیه می شود که هر فراخوانی تابع مورد نظر را با یک کپی صریح از کد تابع جایگزین نماید، تا دیگر نیازی به صرف زمان برای فراخوانی تابع نباشد. البته استفاده از inline ، گرچه باعث بالا رفتن سرعت اجرای برنامه می شود، اما حجم آن را بالا می برد؛ چرا که بجای هر فراخوانی تابع، یک کپی از آن را قرار می دهد. در نتیجه استفاده از این مشخصه، فقط در توابع کوچک قابل قبول است. در حقیقت C از این مشخصه برای توابع بزرگ صرفنظر می کند.
10-9 توابع درون برنامه ای inline int square(int n) { return (n*n) ; } 10-9 توابع درون برنامه ای inline int square(int n) { return (n*n) ; } void main() { int a, b ; a = square(5) ; b = square(2*a+3) ; } void main() { int a, b ; a = (5) * (5) ; b = (2*a+3) * (2*a+3) ; } کامپایلر
11-9 توابع بازگشتی یکی از مهمترین مفاهیم علم کامپیوتر، توابع بازگشتی هستند که کاربرد بسیار زیادی در حل مسائل دارند. توابع بازگشتی، توابعی هستند که خود را مجددا فراخوانی می کنند. این توابع به دو دسته تقسیم می شوند: توابع بازگشتی مستقیم : تابعی مانند A، خودش را فراخوانی نماید. توابع بازگشتی غیر مستقیم : تابعی مانند A، تابع دیگری مانند B را فراخوانی نماید و سپس B مجددا تابع A را فراخوانی نماید. اما چرا یک تابع باید خودش را فراخوانی نماید؟ در بعضی مسائل (که عموما مسائل پیچیده ای نیز هستند)، می توان یک نمونه از مسئله را با استفاده از نمونه ساده تر یا کوچکتری از همان مسئله حل کرد. بنابراین، تابع می تواند برای حل مسئله بزرگتر، نسخه جدیدی از خودش را برای حل مسئله کوچکتر فراخوانی نماید و سپس با استفاده از نتایج بدست آمده، مسئله بزرگ را حل نماید. مسلم است که تابعی که برای حل مسئله کوچک فراخوانی شده است نیز از همین مکانیزم استفاده کرده و مجددا خودش را برای حل یک مسئله کوچکتر فراخوانی خواهد کرد. اما این فراخوانیها تا چه زمانی ادامه پیدا می کند؟ هر مسئله بازگشتی دارای یک حالت پایه است که حل آن بدیهی بوده و نیازی به حل یک مسئله کوچکتر ندارد. بنابراین تابع بازگشتی حتما باید دارای یک شرط برای بررسی حالت پایه باشد، که به آن شرط توقف گفته می شود. هنگامی که تابع به حالت پایه می رسد، فراخوانی بازگشتی را متوقف کرده و حل بدیهی مسئله را برای فراخوانی قبلی تابع باز می گرداند. سپس این بازگرداندن مقادیر رو به عقب تکرار می شود تا هنگامی که به نسخه اولیه تابع برسد و در نهایت جواب نهایی به تابع اصلی بازگردانده شود.
11-9 توابع بازگشتی اکنون برای روشن شدن موضوع به بررسی یک مثال کلاسیک در زمینه توابع بازگشتی، یعنی محاسبه فاکتوریال می پردازیم. قبلا نحوه محاسبه فاکتوریال یک عدد را بصورت زیر بیان کردیم : n! = 1 × 2 × … × n تابع مربوط به فاکتوریال نیز با استفاده از یک حلقه تکرار که اعداد 1 تا n را در یکدیگر ضرب می کرد، نوشته شد. اما اگر به تعریف فاکتوریال دقت کنیم، درمی یابیم که فاکتوریال عددی مانند 5 را می توان از ضرب عدد 5 در فاکتوریال 4 بدست آورد. بعبارت بهتر 5! = 5 × 4! استدلال مشابهی برای 4! برقرار است. بطور کلی می توان گفت: همانطور که دیده می شود، حالت پایه در این مسئله، حالت n=0 است؛ چرا که در این حالت حل مسئله بدیهی بوده و جواب برابر 1 است.
11-9 توابع بازگشتی تابع بازگشتی فاکتوریال بصورت زیر نوشته می شود: 11-9 توابع بازگشتی تابع بازگشتی فاکتوریال بصورت زیر نوشته می شود: long int factorial(int n) { if (n ==0) return(1) ; else return(n * factorial(n-1)); } همانطور که دیده می شود، توابع باز گشتی بسیار ساده و کوچک می باشند، اما درک نحوه کار آنها کمی مشکل است. قبل از توضیح نحوه کار این تابع، توجه کنید که هنگامی که یک تابع خودش را فراخوانی می کند، یک نسخه جدید از آن تابع بوجود می آید. بدین معنا که یک نسخه جدید از کلیه پارامترها و متغیرهای محلی تابع ایجاد می شود و عملیات برروی این متغیرهای جدید انجام می شود(بدون اینکه تغییری در متغیرهای فراخوانی قبلی ایجاد شود). البته برای صرفه جویی در حافظه، نسخه جدیدی از دستورات برنامه ایجاد نمی شود.
11-9 توابع بازگشتی factorial(4) = ? 24 long int factorial (4) { 11-9 توابع بازگشتی long int factorial (4) { if (4 ==0) return(1) ; else return(4 * factorial(3)); } long int factorial (3) { if (3 ==0) return(1) ; else return(3 * factorial(2)); } 6 2 long int factorial (2) { if (2 ==0) return(1) ; else return(2 * factorial(1)); } factorial(4) = ? 24 1 long int factorial (0) { if (0 ==0) return(1) ; else return(0 * factorial(-1)); } long int factorial (1) { if (1 ==0) return(1) ; else return(1 * factorial(0)); } 1
11-9 توابع بازگشتی n = 4 factorial(4) return (24) n = 3 factorial(3) 11-9 توابع بازگشتی n = 4 factorial(4) return (24) n = 3 factorial(3) return (6) n = 2 factorial(2) return (2) n = 1 factorial(1) return (1) n = 0 factorial(0) return (1)
11-9 توابع بازگشتی مثال 7) تابعی بنویسید که ترکیب n به k را محاسبه نماید. int combination(int n, int k) { if (k==0 || n==k) return(1); else return( combination(n-1,k-1) + combination(n-1,k) ) ; }
11-9 توابع بازگشتی مثال 8) تابعی بنویسید که nامین عدد فیبوناچی را باز گرداند. دنباله فیبو ناچی بصورت زیر تعریف می شود: 1 1 2 3 5 8 13 21 … int fibonacci(int n) { if (n==1 || n==2) return(1); else return( fibonacci(n-1) + fibonacci(n-2) ); }
11-9 توابع بازگشتی مثال 9) تابعی بنویسید که دو عدد را دریافت و بزرگترین مقسوم علیه مشترک آن دو را بازگرداند. int gcd(int x, int y) { if (y == 0) return(x); else return( gcd(y, x%y) ); }
1-11-9 مقایسه توابع بازگشتی و تکراری 1-11-9 مقایسه توابع بازگشتی و تکراری همانطور که قبلا دیده شد، الگوریتم فاکتوریال را می توان به دوصورت تکراری (با حلقه تکرار) و بازگشتی نوشت. آیا این مسئله برای سایر الگوریتمهای بازگشتی نیز برقرار است؟ جواب مثبت است. هر الگوریتم بازگشتی را می توان بصورت غیر بازگشتی یا تکراری نیز نوشت، گرچه ممکن است راه حل آن پیچیده تر و وضوح آن نیز کمتر باشد. اما کدام روش بهتر است؟ در الگوریتمهای بازگشتی، با هربار فراخوانی تابع یک نسخه جدید از متغیرها ایجاد می شود. به همین دلیل معمولا الگوریتمهای بازگشتی حافظه بیشتری را نسبت به الگوریتمهای تکراری مصرف می نمایند. علاوه براین الگوریتمهای بازگشتی معمولا از نظر زمان اجرا نیز کندتر هستند. دلیل این مسئله سربار ناشی از فراخوانی مکرر تابع است. با توجه به این نکات منفی، چرا از توابع بازگشتی استفاده کنیم؟ تنها هنگامی از توابع بازگشتی استفاده نمایید که طبیعت مساله بگونه ای باشد که توسط روشهای بازگشتی راحتتر حل شود و الگوریتم حاصل ساده تر و واضحتر باشد. در بسیاری از مسائل، روش بازگشتی بسیار ساده و آسان بوده و درک و اشکالزدایی آن بسیار ساده است، درحالیکه راه حل غیربازگشتی به راحتی به ذهن نمی رسد و یا بسیار پیچیده است. اما در مواردی که کارایی اهمیت زیادی دارد، هرگز از بازگشت استفاده ننمایید.