810 likes | 1.08k Views
فصل هشتم. برنامه نویس شی گرا. تعریف شی گرایی چند ریختی ( polymorphism ) خاصیت ارث بری پشته ( stack ) ایجاد شی ارث بری سازنده ها و نابود کننده ها توابع دوست کلاس های دوست توابع سازنده پارامتر دار توابع سازنده یک پارامتری. عضوهای static کلاسهای تودرتو کلاس های محلی
E N D
فصل هشتم برنامه نویس شی گرا
تعریف شی گرایی چند ریختی (polymorphism) خاصیت ارث بری پشته (stack) ایجاد شی ارث بری سازنده ها و نابود کننده ها توابع دوست کلاس های دوست توابع سازنده پارامتر دار توابع سازنده یک پارامتری عضوهای static کلاسهای تودرتو کلاس های محلی استفاده از object ها بعنوان پارامترهای تابع برگشت اشیاء انتساب اشیاء آرایه اشیاء اشاره گر به اشیاء اشاره گر this توابع مجازی و پلی مرفیسم فهرست مطالب فصل هشتم
تعریف شی گرایی برنامه نويسي شئ گرا يا oop يك روش جديد برنامه نويسي ميباشد كه در آن از ويژگي ساختيافته همراه با چند ويژگيهاي قوي جديد استفاده ميشود. زبان برنامه نويسي C++ امكان استفاده از oop را به راحتي فراهم مينمايد.
تعریف شی گرایی در oop ، بطور كلي مساله به تعدادي زيرگروه قطعات مربوط بهم شكسته ميشود كه براي هر زير گروه code و data تهيه شده و نهايتاً اين زيرگروهها تبديل به unit ها يا واحدهائي ميشود كه objects (يا اشياء) ناميده ميشوند.
نکته مهم : تمام زبانهاي برنامه نويسي شيگرا داراي سه خصوصيت مشترك زير ميباشند : الف: encapsulation (محصورسازي) ب: polymorphism (چندريختي)ج:inheritance(ارث بري)
محصورسازی (Encapsulation ) محصورسازي مكانيزمي است كه code و data را بهم وصل نموده و هر دوي آنها را از استفادههاي غيرمجاز مصون نگه ميدارد. شي يك مؤلفه منطقي است كه data و code را محصور نموده و code باعث دستكاري و پردازش data ميشود.
polymorphism (چند ريختي) چند ريختي مشخصهاي است كه بيك وسيله امكان ميدهد كه باتعدادي از سيستمها يا عميات يا دستگاهها، مورد استفاده قرار گيرد.
inheritance (ارث بري) ارث بري فرآيندي است كه بوسيله آن يك شي (object) ميتواند خاصيتهاي شي ديگري را دارا شود.
نکته: • در بحث شيگرايي به دستهها «کلاس» ميگويند و به نمونههاي هر کلاس «شي» گفته ميشود. • مشخصات هر شي را «صفت» مينامند و به رفتارهاي هر شي «متد» ميگويند. • برنامهنويسي شيگرا بر سه ستون استوار است: • الف. بستهبندي:يعني اين که دادههاي مرتبط، با هم ترکيب شوند و جزييات پيادهسازي مخفي شود. • ب. وراثت: در دنياي واقعي، وراثت به اين معناست که يک شي وقتي متولد ميشود، خصوصيات و ويژگيهايي را از والد خود به همراه دارد. • امتياز وراثت در اين است که از کدهاي مشترک استفاده ميشود و علاوه بر اين که ميتوان از کدهاي قبلي استفاده مجدد کرد، در زمان نيز صرفهجويي شده و استحکام منطقي برنامه هم افزايش مييابد. • ج. چند ريختي:که به آن چندشکلي هم ميگويند به معناي يک چيز بودن و چند شکل داشتن است. چندريختي بيشتر در وراثت معنا پيدا ميکند.
اعلان كلاسها • کد زير اعلان يک کلاس را نشان ميدهد. class Ratio {public: void assign(int, int); void print(); private: int num, den; }; • اعلان کلاس با کلمۀ کليدي class شروع ميشودسپس نام کلاس ميآيد.
اعلان کلاس (ادامه...) • اعلان اعضاي کلاس درون يک بلوک انجام ميشود و سرانجام يک سميکولن بعد از بلوک نشان ميدهد که اعلان کلاس پايان يافته است. عبارت public و عبارت private . هر عضوي که ذيل عبارت public اعلان شود، يک «عضو عمومي» محسوب ميشود و هر عضوي که ذيل عبارت private اعلان شود، يک «عضو خصوصي» محسوب ميشود.
نکته : فقط توابع عضو ميتوانند به متغيرهاي عضو از نوع private دسترسي داشته باشند. بايستي توجه داشت كه اگر نوع عضوي مشخص نگردد آن عضو به صورت اتوماتيك privateمی باشد. Private
تعریف تابع داخل کلاس نام تابع :: اسم کلاس نوع بازگشتی (پارامترها) } {
مثال 1: تعریف یک کلاس class myclass { public: int a,b; float getasfloat(); void inverseit(); int add(myclass tmp); };
تعریف توابع داخل کلاس (ادامه مثال قبل) float myclass::getasfloat() { return (a * b); } void myclass::inverseit() { int temp; temp = a; a =b; b= temp; } intmyclass::add(myclass temp) { return (temp.a + temp.b); }
ادامه مثال قبل (نحوه استفاده از توابع درون یک کلاس در برنامه اصلی) int main() { myclass n1,n2; n1.a = 10; n1.b = 20; n2.a = 30; n2.b = 40; cout<<n1.getasfloat()<<endl; cout<<n2.add(n2)<<endl; n1.inverseit(); cout<<n1.a<<endl<<n1.b; return 0; }
سازنده ها: • وظيفۀ تابع سازنده اين است که حافظۀ لازم را براي شيء جديد تخصيص داده و آن را مقداردهي نمايد و با اجراي وظايفي که در تابع سازنده منظور شده، شيء جديد را براي استفاده آماده کند. • هر کلاس ميتواند چندين سازنده داشته باشد. در حقيقت تابع سازنده ميتواند چندشکلي داشته باشد. سازندهها، از طريق فهرست پارامترهاي متفاوت از يکديگر تفکيک ميشوند. به مثال بعدي نگاه کنيد.
مثال: تعریف سه سازنده برای کلاس myclass(سازنده ها از نوع ساده می باشند) class myclass { public: myclass() {num1 =1;num2=0;} myclass(int y) {num1=y;num2=0;} myclass(inty,int z){num1 = y;num2=z;} void print() {cout<<num1<<endl<<num2;} private: int num1,num2; }; int main() { myclass x, y(4), z(13,4); x.print(); y.print(); z.print(); return 0; }
فهرست مقداردهی: • سارنده ها اغلب به غیر از مقداردهی داده های عضو یک شی، کار دیگری انجام نمی دهند. به همین دلیل در C++ یک واحد دستوری مخصوص پیش بینی شده که تولید سازنده را تسهیل می نماید. این واحد دستوری فهرست مقداردهی نام دارد. • در این حالت دستورالعمل های جایگزینی که قبلا در بدنه تابع سازنده قرار داشتند، اکنون درون فهرست مقداردهی جای داده شده اند. فهرست مقداردهی با یک علامت : شروع می شود. بدنه تابع سازنده نیز در انتها می آید.
مثال: تعریف سه سازنده برای کلاس myclass(سازنده ها از نوع فهرست مقدار دهی می باشند) class myclass { public: myclass() : num1(0),num2(1) {} myclass(int n) : num1(n),num2(0){} myclass(int n,int m): num1(n),num2(m){} void print() {cout<<num1<<endl<<num2<<endl;} private: int num1,num2; }; int main() { myclass x,y(4),z(13,4); x.print(); y.print(); z.print(); return 0; }
توابع سازنده پارامتردار امكان انتقال آرگومانها به توابع سازنده وجود دارد. معمولاً از اين آرگومانها براي initialize نمودن شي در زمان ايجاد آن استفاده ميگردد. در اسلاید بعد مثالی آورده شده است.
مثال : تعریف سازنده ای برای کلاس myclass با استفاده از تابع پارامتردار #include <iostream.h> #include <conio.h> class myclass { int x, y; public : myclass(inti, int j) {x = i; y=j; } void show( ) {cout << x << endl << y; } }; int main( ) { myclassobj( 3 , 5); clrscr( ); obj.show( ); return 0; }
توابع سازنده يك پارامتريدر این گونه از سازنده ها می توان شی را با انتساب یک مقدار اولیه ایجاد نمود. #include <iostream.h> #include <conio.h> class myclass{ int x; public: myclass(inti) {x=i;} intgetx( ) {return x;} }; int main( ) { clrscr( ); myclassobj=126; // منتقل كنiرا به 126 cout << obj.getx( ); return 0 ; }
توابع دستیابی • داده های عضو یک کلاس معمولا به صورت خصوصی اعلان می شوند تا دستیابی به آنها محدود باشد اما همین امر باعث می شود که نتوانیم در مواقع لزوم به آنها دسترسی داشته باشیم. برای حل این مشکل از توابعی با عنوان توابع دستیابی استفاده می کنیم. • تابع دستیابی یک تابع عمومی عضو کلاس است و به همین دلیل اجازه دسترسی به اعضای داده ای خصوصی را دارد. از طرفی توابع دستیابی را طوری تعریف می کنند که فقط مقدار اعضای داده ای را برگرداند. ولی آن ها را تغییر ندهد. به بیان ساده تر با استفاده از توابع دستیابی فقط می توان اعضای داده ای خصوصی را خواند ولی نمی توان آن ها را دست کاری کرد.
مثال: کاربرد توابع دستیابی class myclass { public: myclass (int n=0,int d=1) : num(n),den(d){} int numerator() {return num;} intdenomerator() {return den;} private: intnum,den; }; int main() { myclass x(22,6); cout<<x.numerator()<<" "<<x.denomerator(); return 0; } اینجا توابع numerator و denumerator مقادیر عضو خصوصی num و den را بر می گردانند.
توابع عضو خصوصی: • گاهی اوقات توابع عضو کلاس را به صورت خصوصی تعریف می کنیم . واضح است چنین تابعی از داخل برنامه اصلی به هیچ عنوان قابل دستیابی نیست. این تابع فقط می تواند توسط سایر توابع عضو کلاس دستیابی داشته باشد. به چنین تابعی یک تابع سودمند محلی(utility function) می گوییم.
مثال: class myclass { public: myclass(int n=0, int d=1):num(n),den(d){} void print() {cout<<num<<endl<<den;} void printconv() {cout<<tofloat()<<endl;} private: int num,den; double tofloat(); }; double myclass::tofloat() { return num/den; } int main() { myclass x(22,6); x.print();x.printconv(); return 0; } نکته: توابعی که فقط به انجام وظیفه سایر توابع کمک می کنند و در برنامه اصلی هیچ کاربردی ندارند بهتر است به صورت خصوصی اعلان شوند تا از دسترس سایرین در امان بمانند.
نابود کننده: • وقتی که یک شی ایجاد می شود، تابع سازنده به طور خودکار برای ساختن آن فراخوانی می شود. وقتی که شی به پایان زندگی اش برسد، تابع عضو دیگری به طور خودکار فراخوانی می شود تا نابود کردن آن شی را مدیریت کند. این تابع عضو، نابود کننده نامیده می شود. • سازنده وظیفه دارد تا منابع را برای شی تخصیص دهد و نابود کننده وظیفه دارد آن منابع را آزاد کند. • هر کلاس فقط یک نابود کننده دارد. نام تابع نابود کننده باید هم نام کلاس مربوطه باشد با این تفاوت که علامت نقیض ~ به ابتدای آن اضافه شده باشد.
مثال: نحوه فراخوانی سازنده هنگام ایجاد یک شی و فراخوانی نابود کننده هنگام از بین رفتن شی class myclass { public: myclass() {cout<<"object is born"<<endl;} ~myclass(){cout<<"object dies"<<endl;} private: intnum,den; }; int main() { {myclass x; cout<<"now x is alive"<<endl; } //end of scope for x cout<<"now between blocks"<<endl; {myclass y; cout<<"now y is alive"<<endl; } return 0; }
اشیای ثابت: • اگر قرار است شیی بسازید که در طول اجرای برنامه هیچ گاه تغییر نمی کند، بهتر است منطقی رفتار کنید و آن شی را به شکل ثابت اعلان نمایید. اعلان های زیر چند ثابت را می دهند: Const char = ‘ ‘; Const intmax_int = 3424; • نکته: در مورد اشیای ثابت یک محدودیت وجود دارد: کامپایلر اجازه نمی دهد که توابع را برای اشیا ثابت فراخوانی کنید . • توابع عضوی که می خواهیم با اشیای ثابت کار کنند را باید به صورت const و به شکل زیر تعریف کنیم. • Void print() const{cout<<num<<endl<<den<<endl;} • فراخوانی: • Const myclass s(22,7); • S.print();
پشته (stack) پشته ساختاري است كه داراي خاصيت last in first out ميباشد.پشته فضاي پيوسته در حافظه اشغال مينمايد.عملياتي كــه روي پشته انجام ميشوند عبارتند از : الف: push، كه باعث ميشود يك عنصر وارد پشته شده. ب:pop، كه باعث ميشود يك عنصر از پشته خارج گردد.
ايجاد شي (object) بمنظور ايجاد يك شي بايستي از كلمة رزروشده class استفاده نمود. class از نظر ظاهر شبيه ساختار يا struct ميباشد. پشته را بعنوان يك object ميتوان در نظر گرفت كه data آن شامل يك آرايه و يك tos ، و عملياتي كه روي اين object انجام ميشود عبارتست از push،initialize ، pop كردن پشته. در اسلاید بعد مثالی از نحوه ایجاد شی آورده شده است.
مثال : بدين معني است كه stck و tos بوسيله توابعي كه عضو object نباشند غير قابل دسترسي هستند. و اين يكي از روشهاي محصور سازي اقلام دادههاست. #define SIZE 100 // this creates the class stack. class stack { private : int stck[SIZE]; int tos; public: void init( ); void push(int i); int pop( ); }; بدين معني است كه بوسيله ساير قطعات برنامه قابل دسترسي ميباشد.
نحوه تعریف تابع عضو یک کلاس void stack : : push(int i) { if(tos = = SIZE ) { cout << stack is full.; return; } stck[tos]= i ; tos ++ ; }; عملگر: : مشخص مينمايد كه تابع متعلق به كدام object ميباشد. عملگر : : عملگر scope resolution ناميده ميشود.
برنامه کامل stack : int stack : : pop( ) { if(tos = = 0) { cout << stack underflow. ; return 0 ; } tos - - ; return stck[tos]; } int main( ) { stack st1, st2; // create two objects st1. init( ); st2.init( ); st1.push(1); st2.push(2); st1.push(3); st2.push(4); cout << st1.pop( ) << endl; cout << st1.pop( ) << endl; cout << st2. pop( ) << endl; cout << st2. pop( ) << endl; return 0; } #include <iostream.h> #define SIZE 100 // this creates the class stack. class stack { int stck[SIZE]; int tos; public: void init(int i); int pop( ); void push(int i); }; void stack : : init( ) { tos = 0 ; } void stack : : push(int i) { if(tos = = size) { cout << stack is full. ; return ; } stck[tos] = i ; tos ++ ; }
اشاره گر به اشیا • می توان اشاره گر به اشیای کلاس داشته باشیم. ازآنجا که یک کلاس می تواند اشیای داده ای متنوع و متفاوتی داشته باشد اشاره گر به اشیا بسیار سودمند و مفید است. #include <iostream> using namespace std; class pointer { public: int data; }; int main() { pointer* p = new pointer; (*p).data = 22; cout<<(*p).data<<endl; cout<<p->data; return 0; } حتما باید هنگام استفاده از *p آن را درون پرانتز قرار دهید زیرا عملگر انتخاب عضو (.) تقدم بالاتری نسبت به عملگر مقدار یابی (*) دارد.
عضو داده ای ایستا • زمانی که شیی ساخته می شود، آن شی مستقل از اشیای دیگر داده های عضو خاص خودش را دارد. گاهی لازم است که مقدار یک عضو داده ای در همه اشیا یکسان باشد. اگر این عضو مفروض در همه اشیا تکرار شود، هم از کارایی برنامه می کاهد و هم حافظه را تلف می کند. در چنین مواقعی بهتر است آن عضو را به عنوان یک عضو ایستا اعلان کنیم. • عضو ایستا عضوی است که فقط یک نمونه از آن ایجاد می شود . همه اشیا از همان نمونه مشترک استفاده می نمایند. • ایجاد: با استفاده از کلمه کلیدی static در شروع اعلان متغیر می توانیم آن متغیر را به صورت ایستا اعلان نماییم. یک متغیر ایستا را فقط باید به طور مستقیم و مستقل از اشیا مقداردهی نمود.
مثال: تعریف عضو ایستا به نام count class X { public: static int n; }; int X::n = 0; • نکته:خط آخر از کد بالا نشان می دهد که متغیرهای ایستا را باید به طور مستقیم و مستقل از اشیا مقداردهی نمود. • نکته: متغیرهای ایستا به طور پیش فرض با صفر مقداردهی اولیه می شوند.
مثال: تغییر مقدار عضو ایستا class widget { public: widget(){++count;} ~widget() {--count;} static int count; }; int widget::count = 0; int main() { widget w,x; cout<<"now there are"<<w.count<<"widget"<<endl; { widget w,x,y,z; cout<<"now is"<<w.count<<"widegt"<<endl; } cout<<"now is "<<w.count<<"widget"<<endl; widget y; cout<<"now is"<<w.count<<"widget"<<endl; return 0; }
عضوهاي static اگر عضو دادهاي بصورت static اعلان گردد اين بدين معني است كه كامپيلر فقط يك كپي از متغير مذكور را نگهداري نموده و تمام object ها بايستي بصورت اشتراكي از آن كپي استفاده نمايند.براي اينكار ميبايستي از كلمه static قبل از اعلان عضو استفاده نمود. در اسلاید بعد مثالی آورده شده است.
مثالی دیگر از عضو ایستا #include <iostream.h> class shared{ static int a; int b; public : void set(inti, int j) {a=i; b=j; } void show( ); }; int shared :: a ; // define a void shared :: show( ) { cout << static a: << a << endl; cout << nonstatic b: << b << endl; } int main( ) { shared x,y; x.set(1,1); // set a to 1 x.show( ); y.set(4,4); // change a to 4 y.show( ); x.show( ); return 0; }
یک عضو داده ای ایستا و خصوصی • از آنجا که عضو داده ای ایستا عضوی از کلاس است، می توانیم آن را به شکل یک عضو خصوصی نیز اعلان کنیم. • در این وضعیت از طریق تابع دسترسی می توان به مقدار عضو خصوصی دسترسی پیدا نمود. • در این صورت متغیر ایستا درون خود کلاس جای می گیرد نه درون اشیا.
مثال: استفاده از عضو ایستا به صورت خصوصی class widget { public: widget(){++count;} ~widget() {--count;} intnumwidget(){return count;} private: static int count; }; int widget::count = 0; int main() { widget w,x; cout<<"now there are"<<w.numwidget()<<"widget"<<endl; { widget w,x,y,z; cout<<"now is"<<w.numwidget()<<"widegt"<<endl; } cout<<"now is "<<w.numwidget()<<"widget"<<endl; widget y; cout<<"now is"<<w.numwidget()<<"widget"<<endl; return 0; }
تابع عضو ایستا • گرچه متغیرهای ایستا یک عضو ایستا هستند ولی برای خواندن آن حتما باید از یک شی موجود استفاده می کنیم. این باعث می شود که همیشه مجبور شویم مواظب باشیم تا عضو ایستا از طریق یک شی که الان موجود است فراخوانی شود. • نکته بعدی آن است که اگر هیچ شی موجود نباشد نمی توانیم عضو ایستای Count را دستیابی کنیم. • جهت رفع دو ایرادی که ذکر شد لازم است تا تابع دستیابی کننده را نیز به شکل ایستا تعریف کنیم.
مثال: class myclass { public: myclass() {++count;} ~myclass(){--count;} static int num() {return count;} private: static int count; }; intmyclass::count = 0; int main() { myclassw,x; cout<<"now there are"<<myclass::num()<<"widget"<<endl; { myclassw,x,y,z; cout<<"now is"<<myclass::num()<<"widegt"<<endl; } cout<<"now is "<<myclass::num()<<"widget"<<endl; myclass y; cout<<"now is"<<myclass::num()<<"widget"<<endl; return 0; } نکته: وقتی تابع num به صورت ایستا تعریف می شود، از اشیای کلاس مستقل شده و برای فراخوانی آن نیازی به یک شی موجود نیست و می توان با کد myclass::num() به شکل مستقیم آن را فراخوانی کرد.
ارث بری: • وراثت یک روش جهت ایجاد کلاس جدید از روی کلاس قبلی است. در دنیای واقعی وراثت به این معنا است که هر فرزندی خواص عمومی را از والد خود به ارث می گیرد مثل: رنگ مو، چشم و رفتارها ..... در دنیای واقعی نیز وراثت به همین معنا است یعنی کلاس جدید را طوری تعریف کنیم که اعضای کلاس دیگر را به کار بگیرد. فرض کنید کلاس x قبلا تعریف شده و وجود دارد. نحوه کلی برای وراثت کلاس y از کلاس x به شکل زیرا است:
ارث بری: Class y: public x { }; • در این حالت می گوییم کلاس Y از کلاس x مشتق شده است. • عبارت public بعد از علامت کولن به معنای این است که کلاس y فقط به اعضای عمومی کلاس x دسترسی دارد. به کلاس x کلاس اصلی یا کلاس والد یا کلاس پایه می گوییم . به کلاس y زیر کلاس یا کلاس فرزند یا کلاس مشتق شده می گوییم. • کلاس فرزند می تواند علاوه بر اعضای به ارث برده شده، اعضای عمومی و خصوصی خاص خود را د اشته باشد.
مثال: کلاس Y فرزند کلاس X است و تنها به اعضای عمومی کلاس X دسترسی دارد. class X { public: int a; private: int b; }; class Y:public X { public: int c; private: int d; };
مثال: ارث بری و استفاده در برنامه اصلی #include <iostream> using namespace std; class X { public: X(inti=0,char G='M') : id(i), p(G){} void printID() {cout<<id;} void printGen(){cout<<p;} private : char name; char p; int id; }; class Y:public X { public: Y(inti,charp,int s) : X(i,p), uni("azad"), cource(s){} void printuni() {cout<<uni;} void printcource() {cout<<cource;} private: char *uni; intcource; }; int main() { Y student(879900088,'F',14); cout<<"the ID number is"<<"----->"<<endl; student.printID(); cout<<endl<<"Gender is"<<"----->"<<endl; student.printGen(); cout<<endl<<"uni is"<<" "<<endl; student.printuni(); cout<<endl<<"the Cource is"<<"----->"<<endl; student.printcource(); return 0; }
مثالی دیگر از ارث بری: class building { int rooms; int floors; int area; public: void set_rooms(int num); int get_rooms( ); void set_floors(int num); int get_floors( ); void set_area(int num); int get_area( ); }; در روبروobject اي بنام ساختمان يا building تعريف گرديده است. هر ساختمان داراي تعدادي اطاق، تعدادي طبقه و سطح زير بنا نيز ميباشد. از طرف ديگرتوابعي كه براي شي تعريف شده: حال object ديگري بنام house تعريف مينمائيم كه نه تنها داراي تمام اعضاي شي building ميباشد بلكه دارای دو اقلام داده اضافي و چهار تابع اضافي ميباشد. دراينجا عملاً شي house از شي building ارث ميبرد : // house is derived from building class house : public building { int bedrooms; int baths; public: void set_bedrooms(int num); int get_bedrooms( ); void set_baths(int num); int get_baths( ); };