Algebra Liniara Numerica. Aplicatii ale Poo In C++
~ Prefață ~
Programarea orientată pe obiecte este un concept întâlnit sub diverse denumiri, printre care: programare obiectuală, programare orientată spre obiect, sau, pe scurt, OOP: Object Oriented Programming.
Dintre stilurile de programare contemporane, ea favorizează cel mai mult apropierea de lumea reală.
O dorință mai veche a programatorilor a fost aceea de a utiliza soft deja scris. În acest fel, există posibilitatea ca programatorii să devină cu mult mai eficienți atunci când au de scris o aplicație,
Metoda de a refolosi munca altora nu este nouă. Zeci de ani, singura posibilitate de refolosire a soft-ului a fost dată de utilizarea subprogramelor. Au apărut colecții de subprograme grupate pe teme. Chiar în C++ există o serie de fișiere antet care permit apelarea multor funcții specializate. Programarea orientată pe obiect prezintă avantaje cu mult mai mari decât utilizarea subprogramelor. Cunoștințele dobândite în acest moment nu ne permit nici măcar o enumerare a acestora.
O aplicație a POO este Programarea Vizuală. Orice apare pe ecranul nostru atunci când folosim Windows, este realizat cu ajutorul OOP. Fereastra este un obiect, meniul atașat ferestrei este un alt obiect, butoanele sunt obiecte, bara cu instrumente este un obiect ș.a.m.d.
Astăzi este imposibil să realizăm un program cerut pe piață fără ajutorul OOP. Doar să ne imaginăm ce ar însemna ca programatorul să scrie secvența prin care se desenează o fereastră, de câte ori programul său o va folosi. Ar fi o muncă titanică și inutilă.
În această lucrare, prezint în prima parte conceptele generale ale Programării orientate pe obiecte, precum și o evoluție a limbajelor de programare până în prezent, particularizând apoi pentru Programarea în C++. Definesc noțiunea de obiect care stă la baza acestui tip de programare și continui cu noțiunea de clasă, care este un tip abstract. Cele câteva caracteristici alese, cum ar fi clase și funcții prietene, constructori și destructori ai unei clase, supraîncărcarea operatorilor, însoțite de câteva exemple, sunt doar o mică parte din ce înseamnă cu adevărat Modul de programare în C++. În capitolul 2 al lucrării, încep aplicațiile. Și, am ales eu două probleme ce fac referire la geometria analitică. Fiecare din probleme are clasa ei reprezentativă: Punct2D, pentru un punct dat în plan de 2 coordonate și Vector3D pentru un vector din spațiu. La fiecare, am realizat câteva operații, astfel încât să scot în evidență posibilitatea de a folosi mai multe metode pentru același rezultat, dar într-o scriere sau un apel mai elegant. Și aici, fac referire în mod special la funcțiile de supraîncărcare a operatorilor.
În ultima parte, realizez o aplicație de algebră liniară și anume, dintre metodele directe de rezolvare a unui sistem liniar de ecuații Ax=b, am ales metoda lui Gauss cu pivotare parțială. Pentru acest program, am folosit si o bibliotecă personală, Vecmat.h, unde sunt implementate clasele vector și matrix, împreună cu metodele lor.
Introducere în universul obiectelor
Programul (informatic) este definit în mod tradițional ca fiind transpunerea într-un limbaj de programare a algoritmului de rezolvare a unei probleme. Se spune pe bună dreptate că, în terminologie informatică, nimic nu este mai ușor și totodată mai greu de definit decât noțiunea de obiect.
O definiție aproximativă pentru obiect ar fi aceea de entitate cu o existență concretă sau abstractă, caracterizată printr-un identificator (nume, cod sau ceva similar), o serie de proprietăți și care execută sau suportă anumite acțiuni.
Ce legătură au obiectele din lumea reală cu programele informatice? Ei bine, orice program încearcă să soluționeze o problemă decupată din realitate, deci implicând sub anumite forme obiecte. Orientarea-obiect se poate defini ca fiind modelarea software-ului prin tehnicile de dezvoltare care facilitează construirea de sisteme complexe din componente individuale.
Dintr-o perspectivă istorică, geneza și evoluția limbajelor de programare, ca și a tehnicilor de programare s-a legat aproape întotdeauna de evoluția sistemelor de calcul. Se cunosc mai multe generații de limbaje de programare, pornind de la cele de nivel scăzut (codmașină) și ajungând la limbajele de generația a patra. La început (adică timp de vreo două decenii și jumătate) conceperea de programe era apanajul specialiștilor (care trebuiau să cunoască amănunțit hardware-ul pe care lucrau). Pe măsură însă ce s-au construit calculatoare mai puternice și mai accesibile (din punct de vedere al prețului), limbajele de programare s-au orientat mai mult către utilizatorul obișnuit și activitatea de programare a putut fi abordată de un cerc mai larg de persoane. De exemplu, prima versiune a limbajului BASIC (Beginner’s Allpurpose Symbolic Instruction Code) a fost scrisă de Kurz și Kemeny, de la Dartmouth College, SUA, în 1965 pentru familiarizarea studenților cu lucrul la calculator. Această versiune cuprindea doar 12 instrucțiuni care erau cuvinte din limba engleză (INPUT, PRINT etc.). Problema accesului la calculator era însă una spinoasă, timpul de prelucrare a unui mainframe din acea vreme fiind costisitor (se folosea sistemul time-sharing).
Două sunt paradigmele sau stilurile de programare care au marcat evoluția programării calculatoarelor ca fenomen: paradigma procedurală și cea declarativă (figura 1).
Fig. 1 – Evoluția limbajelor de programare (după Giumale, C., Limbajele de programare la sfârșit de secol, o barieră de cultură profesională, în PCReport nr. 8/1997)
La sfârșitul anilor ’60 s-a instaurat așa-zisa “criză a programării”: multitudinea situațiilor “de pe teren” solicitau programatori tot mai mulți și tot mai bine pregătiți, precum și multe sute de ore-om de muncă. Criza aceasta persistă și în zilele noastre, deși s-au căutat multe soluții pentru depășirea ei.
În domeniul tehnicilor de programare, un loc important îl ocupă încă programarea
structurată, printre ai cărei promotori îi cităm pe E. Djikstra (cunoscut în anii 70 și prin înfierările aduse limbajelor de generația a treia), O. Dahl, F. Knuth. Programarea structurată se bazează pe principiul “divide et impera”, pe separarea activității de proiectare a algoritmilor de codarea lor, a introdus structurile de control și a reprezentat un pas însemnat în contracararea “crizei programării”.
Curente mai noi includ programarea bazată pe evenimente (proprie mediilor cu interfață grafică – Windows, MacOS), programarea vizuală și programarea orientată pe obiecte. În cele mai multe dintre mediile moderne de dezvoltare a aplicațiilor (Visual Basic, Delphi, Visual C++ ș.a.), coexistă elemente din programarea obiectuală, cea vizuală și cea “evenimențială”.
Majoritatea conceptelor care se regăsesc astăzi în programarea orientată obiect au fost inventate în ’60, însă limbajele orientate obiect au ajuns în prim plan în anii ’80, când au avut loc două evenimente semnificative: publicarea în revista Byte (august 1981) a unui articol cu largă audiență descriind limbajul Smalltalk și prima conferință având în vedere limbajele de programare orientate obiect în Portland, Oregon – 1986.
Toate limbajele de programare dau posibilitatea abstractizării: limbajul de asamblare este o “ușoară” abstractizare a mașinii pentru care este proiectat. În programarea “clasică” (folosind limbajele de generația a treia, mai precis cele imperative), abstractizarea se întâlnește pe larg, dar ea implică totuși judecarea problemei în termeni de structură a calculatorului, adică ținând cont de arhitectura von Neumann (vom numi în continuare calculatorul “spațiul soluțiilor”, în opoziție cu lumea reală, care este “spațiul problemei”). Rolul programatorului, simbolizat în figura 2, este de a realiza o “mapare” cât mai fidelă a spațiului soluțiilor pe spațiul problemei, operație care nu întotdeauna reprezintă un succes total, ea depinzând de flexibilitatea limbajului folosit și de abilitatea programatorului. De aceea, programele–sursă sunt deseori dificil de înțeles, greu de întreținut și de modificat ulterior.
Fig. 2. Dilema programatorului
Drumul spre programarea orientată pe obiecte am putea spune că a presupus parcurgerea mai multor etape, bazate în fapt pe evoluția modului de abstractizare a „spațiului problemei”:
1. Programarea procedurală în care abstractizarea presupunea elaborarea funcțiilor sub formă de proceduri;
2. Programare modulară (Modula-2, ADA) care a deschis calea abstractizării datelor. Modulele furnizau o sintactică pentru descompunerea programelor în componente mai mult sau mai puțin independente. În acest context, pentru a exprima mai bine semantica unui modul, a fost introdus conceptul de tip de date abstract, care oferea un set de servicii complet pentru datele încapsulate, având ca sarcină și păstrarea integrității acestora.
3. Programarea orientată pe obiecte care oferea o cale superioară de organizare a
tipurilor, oferind și posibilități de reutilizare îmbunătățită a acestora. În fapt, conceptele deosebite introduse de POO înseamnă încapsulare, polimorfism și moștenire.
Prin urmare, abordarea OO oferă programatorului instrumente pentru reprezentarea elementelor din spațiul problemei într-un mod cât mai natural. Această reprezentare este suficient de generală pentru ca programatorul să nu fie limitat la un anumit tip de problemă. Elementele din “spațiul problemei” sunt referite ca “obiecte”. Programul poate să se adapteze la complexitatea problemei adăugând noi (tipuri de) obiecte. Descrierea problemei se face în termenii problemei și nu în termenii soluției: se specifică mai mult ce decât cum trebuie făcut.
Caracteristicile de bază ale obiectelor și lucrului cu obiecte
Alan Kay, considerat după unii părintele programării orientate obiect, a identificat
următoarele caracteristici ca fundamentale în POO:
1. “Orice” poate fi considerat obiect – Everything is an object.
2. Prelucrările sunt efectuate de către obiecte comunicând între ele: unele cerând altora să execute anumite acțiuni. Obiectele comunică trimițând și recepționând mesaje. Un mesaj este o cerere pentru o acțiune, însoțit eventual de anumite argumente care ar putea fi necesare pentru a duce la bun sfârșit o sarcină.
3. Fiecare obiect are propria sa memorie, care constă din alte obiecte.
4. Fiecare obiect este o instanță a unei clase. O clasă grupează obiectele similare.
5. Clasa este “repository”-ul comportamentului asociat unui obiect. Adică, toate obiectele care sunt instanțe ale aceleași clase pot efectua aceleași acțiuni.
6. Clasele sunt organizate într-o structură ierarhică arborescentă (de regulă cu o singură rădăcină) numită ierarhie de moștenire. Memoria și comportamentul asociat cu instanțele unei clase este pus automat la dipoziția oricărei clase descendentă în structura arborescentă.
Prin urmare caracteristicile de bază ale orientării-obiect sunt:
• paradigma obiect-mesaj;
• tipurile abstracte de date;
• moștenirea;
• polimorfismul;
• identitatea obiectelor.
Paradigma obiect-mesaj
Universul computațional al acestei paradigme este construit din obiecte; fiecare obiect răspunde la o colecție determinată de mesaje.
În programarea procedurală, rutinele sau funcțiile se apelează unele pe altele pentru a returna anumite date sau pentru a modifica niște parametri de intrare. În modelul obiect-mesaj, în loc de data elementară, avem un obiect capabil să proceseze cereri cunoscute ca mesaje. Aceste mesaje:
• pot cere obiectului să efectueze “ceva” și să returneze un rezultat;
• pot modifica starea obiectului (adică pot modifica valorile unor proprietăți ale
obiectului).
Acțiunea (execuția) în programarea orientată-obiect este inițiată prin transmitarea unui mesaj către un agent (un obiect) responsabil de efectuarea respectivei acțiuni. Mesajul codifică cererea unei acțiuni și este însoțit de informații adiționale (argumente) necesare pentru a duce la bun sfârșit acțiunea indicată. Receptorul este obiectul către care este trimis mesajul. Dacă receptorul acceptăe a deschis calea abstractizării datelor. Modulele furnizau o sintactică pentru descompunerea programelor în componente mai mult sau mai puțin independente. În acest context, pentru a exprima mai bine semantica unui modul, a fost introdus conceptul de tip de date abstract, care oferea un set de servicii complet pentru datele încapsulate, având ca sarcină și păstrarea integrității acestora.
3. Programarea orientată pe obiecte care oferea o cale superioară de organizare a
tipurilor, oferind și posibilități de reutilizare îmbunătățită a acestora. În fapt, conceptele deosebite introduse de POO înseamnă încapsulare, polimorfism și moștenire.
Prin urmare, abordarea OO oferă programatorului instrumente pentru reprezentarea elementelor din spațiul problemei într-un mod cât mai natural. Această reprezentare este suficient de generală pentru ca programatorul să nu fie limitat la un anumit tip de problemă. Elementele din “spațiul problemei” sunt referite ca “obiecte”. Programul poate să se adapteze la complexitatea problemei adăugând noi (tipuri de) obiecte. Descrierea problemei se face în termenii problemei și nu în termenii soluției: se specifică mai mult ce decât cum trebuie făcut.
Caracteristicile de bază ale obiectelor și lucrului cu obiecte
Alan Kay, considerat după unii părintele programării orientate obiect, a identificat
următoarele caracteristici ca fundamentale în POO:
1. “Orice” poate fi considerat obiect – Everything is an object.
2. Prelucrările sunt efectuate de către obiecte comunicând între ele: unele cerând altora să execute anumite acțiuni. Obiectele comunică trimițând și recepționând mesaje. Un mesaj este o cerere pentru o acțiune, însoțit eventual de anumite argumente care ar putea fi necesare pentru a duce la bun sfârșit o sarcină.
3. Fiecare obiect are propria sa memorie, care constă din alte obiecte.
4. Fiecare obiect este o instanță a unei clase. O clasă grupează obiectele similare.
5. Clasa este “repository”-ul comportamentului asociat unui obiect. Adică, toate obiectele care sunt instanțe ale aceleași clase pot efectua aceleași acțiuni.
6. Clasele sunt organizate într-o structură ierarhică arborescentă (de regulă cu o singură rădăcină) numită ierarhie de moștenire. Memoria și comportamentul asociat cu instanțele unei clase este pus automat la dipoziția oricărei clase descendentă în structura arborescentă.
Prin urmare caracteristicile de bază ale orientării-obiect sunt:
• paradigma obiect-mesaj;
• tipurile abstracte de date;
• moștenirea;
• polimorfismul;
• identitatea obiectelor.
Paradigma obiect-mesaj
Universul computațional al acestei paradigme este construit din obiecte; fiecare obiect răspunde la o colecție determinată de mesaje.
În programarea procedurală, rutinele sau funcțiile se apelează unele pe altele pentru a returna anumite date sau pentru a modifica niște parametri de intrare. În modelul obiect-mesaj, în loc de data elementară, avem un obiect capabil să proceseze cereri cunoscute ca mesaje. Aceste mesaje:
• pot cere obiectului să efectueze “ceva” și să returneze un rezultat;
• pot modifica starea obiectului (adică pot modifica valorile unor proprietăți ale
obiectului).
Acțiunea (execuția) în programarea orientată-obiect este inițiată prin transmitarea unui mesaj către un agent (un obiect) responsabil de efectuarea respectivei acțiuni. Mesajul codifică cererea unei acțiuni și este însoțit de informații adiționale (argumente) necesare pentru a duce la bun sfârșit acțiunea indicată. Receptorul este obiectul către care este trimis mesajul. Dacă receptorul acceptă mesajul, acceptă implicit și responsabilitatea de a efectua acțiunea indicată. Ca răspuns la mesaj receptorul va executa o anumită cerere pentru a satisface cererea. Un principiu esențial al acestei paradigme este ascunderea informației vizavi de transmiterea mesajelor, cu alte cuvinte clientul care trimite cererea nu are nevoie (și uneori nici voie) să cunoască mijloacele efective prin care este onorată cererea.
Din perspectiva abstractizării, ascunderea informației (information hiding) reprezintă omisiunea intenționată a detaliilor în dezvoltarea reprezentării unei abstracțiuni. Principiul ascunderii informației se regăsește și în programarea prin limbajele convenționale (programarea structurată) sub forma apelurilor de proceduri (sau funcții). Există însă două diferențe: în primul rând apelul unei proceduri nu desemnează în mod explicit un receptor, iar în al doilea rând în programarea orientată-obiect interpretarea mesajului (metoda folosită pentru a răspunde cererii transmise) este determinată de receptor, putând varia astfel prin raportarea la receptori diferiți. De obicei, receptorul efectiv al unui mesaj dat nu este cunoscut decât în momentul execuției (la runtime), așa încât determinarea metodei care ar trebui invocată nu este cunoscută până la acel moment. Această situație este cunoscută sub numele de legarea întârziată (late binding) între mesaj (numele funcției sau procedurii) și fragmentul de cod (metoda) folosită pentru a răspunde cererii exprimate prin mesajul dat, în contradicție cu legarea în momentul compilării (early-binding sau compile-time binding sau link-binding) a numelui cu fragmentul de cod din apelul clasic de proceduri.
Tipuri abstracte de date
Multe limbaje (chiar procedurale) dau posibilitatea utilizării unor tipuri de date definite de utilizator. Dar programatorii au simțit nevoia introducerii unor noi tipuri, care să înglobeze pe lângă valori (care dau starea) și comportamentul și să ofere o interfață pentru accesarea stării și comportamentului. Faptul că utilizatorul se servește exclusiv de această interfață și percepe tipul de dată respectiv ca pe o “cutie neagră" a sugerat termenul de tip abstract de dată (pe scurt, TAD).
Tipurile abstracte de date definesc atât structura obiectului (aparența), cât și comportamentul său (care mesaje îi sunt aplicabile). Se ascunde astfel reprezentarea internă a obiectului de mediul înconjurător și se protejează algoritmii interni care implementează comportamentul obiectului. Termenul de “ascunderea informației” este cunoscut în teorie sub denumirea de încapsulare. Prin urmare principiul ascunderii informației oferă posibilitatea obținerii unor abstracțiuni cu stare și comportament autonom.
Limbajele care implementează tipurile abstracte de date oferă construcții pentru definirea structurilor de date și totodată a operațiilor utilizate pentru manipularea instanțelor acestor structuri de date. În plus, manipularea instanțelor unui anumit tip are loc numai prin operațiile asociate tipului respectiv.
Exemple de TAD: modulul (limbajul Modula-2), clusterul (limbajul CLU), pachetul (limbajul Ada), clasa (C++, Smalltalk, FoxPro), modulul-clasă (Visual Basic).
Interfață și implementare
În domeniul dezvoltării aplicațiilor software, pentru a desemna distincția dintre aspectele funcționale (ce trebuie făcut, ce se poate obține sau ce se așteaptă ca rezultat) și apectele procedurale (cum se realizează) ale unei sarcini anume, se folosesc termenii interfață și implementare. O interfață descrie ce este desemnat să facă un sistem. Aceasta reprezintă viziunea externă, viziunea pe care trebuie să o înțeleagă utilizatorii. Interfața nu precizează nimic cu privire la modul în care ar trebui efectuată respectiva sarcină. Așa încât pentru a-și dobândi funcționalitatea desemnată (descriptiv), o interfață trebuie să-și găsească o implementare corespunzătoare care întregește abstracțiunea luată în considerare.
Distincția dintre interfață și implementare nu numai că simplifică înțelegerea unei
probleme la un nivel de abstractizare mai înalt (din moment ce specificația descriptivă a unei interfețe este mult mai simplă decât descrierea oricărei implementări particulare) ci deschide și posibilitatea interschimbării componentelor software.
Faptul că interfețele descriu serviciile oferite de o componentă software fără a descrie tehnicile folosite pentru implementarea lor conduce la formalizarea unei viziuni a serviciilor în descrierea sistemelor.
Clase
Clasa este conceptul cel mai des utilizat pentru implementarea tipurilor abstracte de date în limbajele OO.
Aristotel a fost probabil primul care a început un studiu serios al noțiunii de clasă sau tip. El vorbea despre "clasa peștilor" și "clasa păsărilor", pentru a încadra obiecte unice în categorii de obiecte care au caracteristici comune.
Cuvintele clasă și tip sunt adesea folosite ca sinonime.
O definiție minimală a clasei include:
• numele clasei;
• operațiile externe pentru manipularea instanțelor clasei (adică metodele) – referite ca interfață, plus codul care implementează metodele clasei: într-un cuvânt, implementare (din exterior, metodele sunt accesibile prin semnătura lor);
• reprezentarea internă, care păstrează valorile diferitelor stări ale instanțelor unei clase, cu alte cuvinte atributele sau proprietățile; valorile acestora variază pentru fiecare instanță a clasei.
Iată o reprezentare în pseudocod:
clasa Geometrie
variabile:
Arie
Perimetru
metode:
CalculPerimetruTriunghi()
metoda CalculPerimetruTriunghi (Latura1, Latura2, Latura3)
Perimetru=Latura1+Latura2+Latura3
Orice clasă trebuie să ofere un mecanism de creare a unor noi instanțe (obiecte) și de distrugere a celor existente și nefolosite (constructori și destructori de obiecte). Deși valorile variabilelor pot diferi de la o instanță a clasei la alta, toate instanțele partajează codul care implementează interfața. În concordanță cu mecanismul încapsulării, variabilele de instanță private nu pot fi accesate decât prin intermediul unor funcții explicit declarate ca publice.
Compunerea
Compunerea este o tehnică esențială în crearea unor structuri complexe pornind de la părți simple. Ideea este să se înceapă de la câteva forme primitive după care să se formuleze regulile după care formele existente să se combine pentru a obține noi forme. Cheia este ca prin compoziție să se poată revalorifica oricând atât noile forme obținute până la un moment dat cât și formele primitive originale. Un exemplu clasic al utilizării compoziției este modul în care prin intermediul bibliotecilor interfețelor grafice se obține modul de afișare a ferestrelor.
Declararea câmpurilor/membrilor/variabilelor din definiția unei clase, ca fiind obiecte de tipuri implementate de alte clase, nu desemnează altceva decât un mecanism de compunere.
Containere și extensii ale claselor
Extensia unei clase corespunde tuturor obiectelor din acea clasă care au fost create și nu au fost (încă) distruse.
Un container poate fi definit ca o clasă generică, ale cărei instanțe sunt colecții de
obiecte, manipulabile prin operații asociate. Pot fi subclase precum Set, Bag (multiset) sau List, fiecare cu metode specifice de acces, ca de exemplu reuniunea seturilor sau enumerarea obiectelor din colecție.
Putem sumariza astfel avantajele tipurilor abstracte de date:
• mai bună conceptualizare și modelare a lumii reale; creșterea capacității de reprezentare și de înțelegere; categorizarea obiectelor pornind de la structuri și trăsături de comportament comune;
• creșterea robusteții sistemului informatic. Dacă limbajul de programare este tipizat, se poate evita apariția unor erori la execuție. Restricțiile de integritate impuse datelor și operațiilor ajută la menținerea consistenței datelor și corectitudinii programelor;
• sporirea performanței, prin optimizări în momentul compilării;
• mai buna reflectare a semanticii datelor;
• separarea implementării de specificație (interfață) – modificarea implementării nu va afecta interfața publică a tipului abstract de dată;
• posibilitatea de a realiza sisteme extensibile, prin intermediul reutilizării.
Moștenirea
Pe lângă modelarea cât mai fidelă a realității, POO tinde de asemenea să realizeze
extensibilitatea și reutilizabilitatea software-ului. Tehnicile prin care acestea sunt posibile sunt compunerea (definită mai înainte) și moștenirea. Moștenirea permite construcția de noi obiecte sau module de program – de exemplu, a unor clase – pe baza unei ierarhii de module existente. Aceasta elimină nevoia reproiectării și rescrierii de programe “de la zero”.
Prin urmare moștenirea oferă posibilitatea organizării abstracțiunilor (tipurilor) existente în ierarhii de clase specializate incremental.
Din punctul de vedere al abstractizării compunerea și moștenirea au efecte similare: se elimină anumite detalii, concentrarea rămând asupra unor mai puține caracteristici care ar trebui implementate la nivelul următor.
Diferența dintre compunere și moștenire rezidă din faptul că nivelul de abstractizare mai specializat este reprezentativ pentru nivelul de abstractizare mai general în cazul moștenirii. Adică printre caracteristicile nivelului specializat se regăsesc și toate caracteristicile nivelului mai general (excepțiile desemnând suprascrierile unor trăsături din clasele de bază). Prin compunere se împrumută numai funcționalitate, prin moștenire se poate împrumuta eventual și implementarea.
Noile clase pot moșteni atât comportamentul (metode, funcții) cât și reprezentarea
(variabile, atribute) de la clasele existente.
Moștenirea își are rădăcinile în cercetările legate de inteligența artificială. În 1968, Ross Quillian a prezentat modelul teoretic al memoriei asociative (unul dintre primele modele de rețea semantică). O rețea semantică se compune din noduri care reprezintă obiecte și din legături reprezentând relații.
În limbajele de programare OO vorbim despre moștenirea de clasă (se creează o clasă derivată pe seama unei/unor clase părinte sau superclase ori clase de bază – cu alte cuvinte, prin specializare).
Ex.: clasa de bază este comunitatea academică, iar clasele derivate sunt studenti și profesori.
Să reținem că moștenirea are două aspecte: structural și comportamental.
Avantaje ale moștenirii:
• oferă un model natural pentru organizarea informației);
• permite partajarea codului (implementării) și interfeței;
• permite claselor să fie definite pe baza ierarhiilor existente și nu de la zero.
Polimorfismul
Polimorfismul este un alt concept-cheie al POO, în strânsă relație cu moștenirea și cu ierarhiile de clase. A fost definit inițial de Christopher Strachey (1967).
Polimorfismul se definește ca fiind capacitatea unei metode de a se aplica obiectelor din diferite clase.
Polimorfismul ad-hoc se referă la faptul că identificatorul unei proceduri sau metode desemnează mai mul decât o singură procedură (fragment de cod).
Polimorfism pur se referă la caracteristica unei funcții singulare de a putea fi executată cu argumente de tipuri de diferite.
În practică se simte adesea necesitatea redefinirii unei metode pentru a-i da o implementare specifică pentru clasa derivată.
Această implementare va ține cont și de proprietățile locale (nemoștenite) ale clasei. Deci operația redefinită va avea același nume cu cea din clasa de bază, dar implementare diferită în clasa derivată. Despre redefinire sau suprascriere am vorbit mai sus, iar unii autori numesc suprascrierea polimorfism ad-hoc.
Prin mecanismul de legare întârziată (late binding), la momentul execuției se alege corpul (implementarea) adecvat(ă) metodei. În programul-client, în etapa codării (scrierii programului), este suficientă invocarea numelui metodei. Prin aceasta se elimină nevoia modificării programelor utilizatoare de obiecte, în cazul apariției de clase derivate.
Legarea întârziată reprezintă mecanismul prin care se pot adăuga noi tipuri în ierarhia de clase, fără a modifica programele de bază în care pot fi implicate în prelucrări.
După cum am precizat la un moment dat, în programarea orientată-obiect interpretarea mesajelor se poate realiza diferit, funcție de tipurile diferite de obiect care ar putea recepționa acel mesaj. Prin urmare, comportamentul rezultat și răspunsul obținut depind de obiectul receptor. Legarea acestei caracteristici de mecanismul de moștenire duce la situații în care aceeași metodă (definită inițial pentru un anume tip, cu anumite argumente) va conduce la rezultate diferite. Prin moștenire o aceeași metodă va fi disponibilă tuturor claselor care derivă din clasa în care aceasta a fost definită inițial. Însă funcție de natura lor, anumite subclase pot rescrie metoda respectivă ca implementare, lăsând însă nemodificată semnătura ei (restricția de moștenire). Prin urmare același mesaj poate determina funcție de obiectul care-l recepționează comportamente distincte. Astfel de comportamente se numesc polimorfice.
Avantajele polimorfismului sunt evidente și reprezintă un alt atu al programării obiectuale față de programarea clasică (procedurală).
Identitatea obiectelor
Dacă tipurile abstracte de date și mecanismul moștenirii modelează și organizează obiectele din punct de vedere al tipurilor sau claselor, identitatea obiectelor le organizează în spațiul manipulat de un program OO.
Identitatea este proprietatea unui obiect de a se distinge de toate celelalte. Cea mai uzitată tehnică de identificare a obiectelor în limbajele de programare este de a le înzestra cu nume definite de utilizator.
În programarea “clasică” nu există suport pentru identitatea obiectelor, atât timp cât se poate face referire la variabile fără ca acestea să fie legate de obiecte (putem invoca o variabilă Arie sau o variabilă Perimetru). În acest caz trebuie specificat dacă ele (variabilele) se referă sau nu la același obiect. În limbajele OO, acest dezavantaj nu mai apare, datorită introducerii operatorului “= =”, care testează identitatea a 2 obiecte și nu egalitatea din punct de vedere al conținutului, ca în cazul operatorului “=”.
1.3. Clase și obiecte în C++
În C++, clasele formează baza programării orientate pe obiecte. Caracteristic este faptul că o clasă este folosită pentru a defini natura unui obiect. De fapt, în C++ clasa este unitatea de bază pentru încapsulare (mecanismul prin care datele –variabilele și funcțiile – metodele sunt plasate împreună într-o unică structură). Se face distincție între clasă, care este tipul abstract și obiectul propriu-zis, care este instanțierea clasei respective.
Generalități
Clasele sunt create utilizându-se cuvântul cheie class. O declarare a unei clase definește un nou tip care unește cod și date. Acest nou tip este apoi folosit pentru a declara obiecte din acea clasă. De aceea, o clasă este o abstractizare logică, dar un obiect are o existență fizică. Cu alte cuvinte, un obiect este un exemplar (o instanță) al unei clase. O declarare de clasă este similară sintactic cu cea a unei structuri.
Forma generală completă a unei declarații de clasă care nu moștenește nici o altă clasă este:
class nume-clasă {
date și funcții particulare
specificator de acces:
date și funcții
specificator de acces:
date și funcții
.
.
.
specificator de acces:
date și funcții
lista de obiecte;
}
Lista de obiecte este opțională. Dacă există, ea declară obiecte din acea clasă. Aici, specificator de acces este unul dintre aceste trei cuvinte-cheie din C++:
public
private
protected
Implicit, funcțiile și datele declarate într-o clasă sunt proprii acelei clase și doar membrii săi pot să aibă acces la ele. Totuși, folosind specificatorul de acces public, permitem funcțiilor sau datelor să fie accesibile altor secțiuni ale programului. O dată utilizat un specificator de acces, efectul său durează până când ori se întâlnește altul, ori se ajunge la sfârșitul declarației pentru clasă. Pentru a ne reîntoarce la declararea particulară, putem folosi specificatorul de acces private. Specificatorul protected este necesar când este implicată moștenirea.
Într-o declarare de clasă putem modifica specificatorul de acces ori de câte ori dorim. Adică, putem trece la public pentru unele declarații și apoi să revenim la private.
Funcțiile care sunt declarate într-o clasă sunt numite funcții membre. Ele pot să aibă acces la orice element al clasei din care fac parte. Aceasta include toate elementele de tip private. Variabilele care sunt membre ale unei clase sunt numite variabile membre sau date membre. În general, orice element al unei clase este numit membru al acelei clase. Există câteva restricții care se aplică membrilor clasei. O variabilă membru care nu este de tip static nu poate să aibă o inițializare. Nici un membru nu poate fi un obiect al clasei care se declară.
În general, trebuie să facem membrii unei clase să fie particulari acelei clase. Aceasa este o parte a modului în care se realizează încapsularea. Totuși, pot exista situații în care vom avea nevoie să facem una sau mai multe variabile publice.
(De exemplu, poate fi necesar ca o variabilă folosită intens să fie accesibilă global, pentru a obține timpi de rulare mai mici.) Când o variabilă este public, ea poate fi accesată direct de oricare secțiune a programului. Sintaxa pentru a avea acces la o dată membru public este aceeași ca pentru apelarea unei funcții: specificăm numele obiectului, operatorul punct și numele variabilei.
Exemplu:
#include <iostream.h>
class clasamea{
public:
int i,j,k; //accesibile întregului program
};
main()
{
clasamea a, b;
a.i=100; //accesul direct la i,j,k
a.j=4;
a.k=a.i*a.j;
cout<<a.k;
}
1.3.2. Structuri și clase
C++ a crescut rolul structurii standard din C la acela al unui mod alternativ, de specificare a unei clase. De fapt, singura diferență dintre o clasă și o struct este aceea că, implicit, toți membrii unei structuri sunt publici, iar cei ai unei clase sunt particulari. În toate celelalte privințe structurile și clasele sunt echivalente. Aceasta înseamnă că în C++, o structură definește, de asemenea, un tip de clasă. Din acest motiv, obiectele de acel tip pot fi declarate folosind doar numele generic al structurii, fără să fie precedat de cuvântul cheie struct.
1.3.3. Funcții prietene
Este posibil să permitem unei funcții care nu este membru să aibă acces la membrii particulari ai clasei folosind un friend(prieten). O funcție friend are acces la membrii private și protected ai clasei căreia îi este prietenă. Pentru a declara o funcție friend, includem prototipul ei în acea clasă, precedat de cuvântul cheie friend.
Exemplu:
#include <iostream.h>
class clasamea {
int a,b;
public:
friend int sum(clasamea x);
void da_ab(int i, int j);
};
void clasamea::da_ab(int i, int j)
{
a=i;
b=j;
}
//sum() nu este o funcție membru a nici unei clase.
int sum(clasamea x)
{
//deoarece sum() este funcție prietenă pentru clasamea, ea poate avea acces direct la a și b
return x.a+x.b;
}
main()
{
clasamea n;
n.da_ab(3,4);
cout<<sum(n);
}
Chiar dacă nu s-a câștigat nimic, există anumite condiții în care funcțiile friend sunt într-adevăr valoroase. Prietenii pot să fie folositori când supraîncărcăm anumite tipuri de operatori. Alt motiv este că, în unele cazuri, două sau mai multe clase pot conține membri care sunt corelați cu alte secțiuni ale programului. Alte secțiuni ale programului pot să dorească să știe dacă mesajul a fost afișat înainte de a fi fost scris pe ecran, astfel încât să nu se suprapună, din greșeală, alt mesaj. Chiar dacă putem crea funcții membre în fiecare clasă care să returneze o valoare ce indică dacă un mesaj este afectiv, aceasta înseamnă un efort în plus la verificarea condiției (două apelări ale funcției în loc de una). Dacă acea condiție trebuie verificată des, munca în plus poate să nu fie acceptabilă. Dar, utilizând pentru fiecare clasă o funcție friend, este posibil să verificăm starea fiecărui obiect apelând doar această singură funcție. De aceea, în asemenea situații, o funcție friend permite să creăm un cod mai eficient.
Există două restricții importante care se aplică funcțiilor friend. Prima, o clasă derivată nu poate moșteni funcții friend. A doua, o funcție friend nu poate avea un specificator de clasă de memorare. Aceasta înseamnă că ea nu poate fi declarată ca fiind static sau extern.
1.3.4. Clase prietene
Este posibil ca o clasă sa fie friend pentru alta. Când este așa, clasa friend are acces la numele particulare definite în cadrul celeilalte. Aceste nume particulare pot include lucruri ca nume de tipuri și enumerări de constante.
Este important să înțelegem că atunci când o clasă este friend pentru o alta, ea are acces doar la numele definite în interiorul celeilalte. Ea nu moștenește cealaltă clasă. Mai precis, membrii primei clase nu devin membri ai clasei friend.
class matrix ;
class vector {
friend matrix ;
private:
int size ;
double *vect ;
int range(int) ;
public:
…
}
class matrix {
private:
int nrows, ncols ;
vector **mat ;
int range(int) ;
public:
matrix(int,int) ;
void print();
…
}
void matrix::print()
{ int i, j ;
cout << "MATRIX PRINT \n" ;
for ( i = 0 ; i < nrows ; i++) {
for ( j = 0 ; j < ncols ; j++)
cout << mat[i]->vect[j] << " " ;
cout << endl ;
}
}
}
1.3.5. Funcții inline
Există o caracteristică importantă în C++, numită funcții inline, care este folosită uzual împreună cu clasele.
În C++ putem crea funcții scurte care nu sunt apelate efectiv; în loc de aceasta, codul lor se dezvoltă în interior, în fiecare punct în care sunt numite. Pentru a determina ca o funcție să se dezvolte inline în loc să fie apelată, precedăm definirea ei cu cuvântul cheie inline. De exemplu, în programul de mai jos, funcția max() este dezvoltată inline în loc să fie apelată.
#include <iostream.h>
inline int max(int a, int b)
{
return a>b ? a:b;
}
void main()
{
cout<<max(10,20);
cout<<” “<<max(99,98);
}
În ceea ce privește compilatorul, programul precedent este echivalent cu acesta:
#include <iostream.h>
void main()
{
cout<<(10>20 ? 10:20);
cout<<” “<<(99>98 ? 99:98);
}
Motivul pentru care funcțiile inline sunt o facilitate în plus în C++ este că ele permit să creăm coduri foarte eficiente. De vreme ce pentru clase este tipic ca, deseori, să solicite executarea frecventă a funcțiilor de interfață(care asigură accesul la datele particulare), eficiența acestor funcții este o cerință esențială în C++. Știm că la fiecare apelare a unei funcții se generează o cantitate de muncă suplimentară prin mecanismul de apelare și returnare. În mod normal, când este apelată o funcție, argumentele sunt puse în memoria stivă și sunt salvate mai multe registre și apoi rememorate când funcția se returnează. Problema este că aceste instrucțiuni iau timp. Dar când o funcție se dezvoltă inline, nu mai apare nici una dintre aceste operații. Chiar dacă dezvoltarea inline a funcțiilor poate determina timpi de rulare mai scurți, deseori rezultă dimensiuni mai mari de coduri. Din acest motiv, este bine să inducem inline doar funcții mici și care vor avea un impact mare asupra performanțelor programului nostru.
1.3.6. Funcții constructor și destructor
1.3.6.1. Funcții constructor implicite
În absența altor definiții, clasei i se atașează în mod implicit o metodă specială, numită constructor, care are rolul de a permite declararea obiectelor. În acest fel, se alocă spațiul de memorie necesar obiectelor declarate. Un asemenea constructor are următoarele caracteristici:
– nu are parametri formali;
– consecința absenței parametrilor formali este dată de faptul că nu este permisă inițializarea la declarare a datelor membru;
– nu se generează, în cazul în care clasa are atașat alt constructor, fără parametri.
1.3.6.2. Funcții constructor date de utilizator
Putem înzestra clasa cu o metodă constructor proprie, respectând însă anumite reguli suplimentare, comparativ cu o metodă obișnuită:
– O metodă constructor are întotdeauna numele clasei din care face parte. Ea este apelată automat la declararea obiectelor.
– Un constructor este o funcție fără tip. Cu toate acestea, în dreptul tipului nu se trece cuvântul cheie void.
O clasă poate avea mai mulți constructori. Aceștia însă trebuie să difere prin numărul și tipul parametrilor formali. Problema se reduce la apelul mai multor funcții(metode) care au același nume, dar parametri formali diferiți, adică a supraîncărcării funcțiilor.
Când transmitem argumente către funcțiile constructor, acestea sunt folosite pentru a contribui la inițializarea unui obiect atunci când este creat. Pentru a crea un constructor cu parametri, îi adăugăm simplu parametri în modul în care am face-o pentru orice altă funcție. Funcția este în așa fel creată încât parametrii sunt folosiți pentru a inițializa obiectul. Funcțiile constructor cu parametri sunt foarte folosite deoarece ele permit să evităm să facem o apelare în plus a funcției doar pentru a inițializa una sau mai multe variabile dintr-un obiect. Fiecare apelare a funcției pe care o putem evita face programul mai eficient.
Exemplu: Înzestrăm clasa complex cu 3 constructori, unul fără parametri, altul cu un parametru și ultimul cu 2 parametri.
class complex {
private:
double re ;
double im ;
public:
complex() { re = 0 ; im = 0 ; }
complex(double x) {re=1; im=x; }
complex (double x, double y = 0)
{ re = x ; im = y ; }
Main()
{complex z1(3,4), z2(5), z3;
z3=z1;}
Declarația z3=z1 pare fără sens. În realitate, există două tipuri de constructori: unul care asigură alocarea și inițializarea obiectelor, altul de copiere, care permite atribuirea datelor unui obiect altuia. Orice obiecte este înzestrat cu un constructor implicit de copiere, ce realizează o copiere „bit cu bit” a datelor membru. Avem însă și posibilitatea de a scrie constructorul de copiere.
complex(complex &v) {re=v.re;im=v.im;}
Observăm că parametrul unui constructor de copiere este chiar de tipul clasei din care face parte constructorul respectiv.
1.3.6.3. Funcții destructor
Dezalocarea obiectelor se face prin destructori. Destructorul se generează automat, dacă nu a fost definit. Numele său conicide cu cel al clasei și e precedat de „~”. Nu are parametri formali. O clasă are un singur destructor. Destructorul se apelează automat atunci când viața unui obiect încetează.
1.3.6.4. Când sunt executați constructorii și destructorii
Ca regulă generală, un constructor de obiecte este apelat la declararea obiectului, iar un destructor de obiecte este apelat când este distrus obiectul. Prezentăm acum momentul exact al apariției acestor evenimente.
O funcție constructor de obiecte este executată când este întâlnită instrucțiunea de declarare a obiectului. Mai mult, când două sau mai multe obiecte sunt declarate în aceeași instrucțiune, constructorii sunt apelați în ordinea în care sunt ele întâlnite, de la stânga la dreapta. Funcțiile destructor pentru obiecte locale sunt executate în ordine inversă față de cele constructor.
Funcțiile constructor pentru obiectele globale sunt executate înaintea lui main(). Constructorii globali din același fișier sunt executați în ordine, de la stânga la dreapta și de sus în jos. Nu putem ști ordinea execuției constructorilor globali împrăștiați prin mai multe fișiere. Destructorii globali se execută în ordinea inversă după ce se încheie main().
1.3.7. Membrii de tip static ai claselor
Atât funcțiile membre cât și datele membre ale unei clase pot fi declarate static.
1.3.7.1. Membri statici de tip date
Când precedăm o declarație a unei variabile membru cu static, îi spunem compilatorului că va exista doar o copie a acelei variabile și va fi folosită de către toate obiectele clasei. Spre deosebire de membrii tip date obișnuiți, nu sunt create copii individuale ale variabilelor membre static pentru fiecare obiect. Nu are importanță câte obiecte de acel tip de clasă sunt create, va exista doar o singură copie a membrilor date de tip static. De aceea, toate obiectele acelei clase folosesc aceeași variabilă. Când este creat primul obiect, toate variabilele de tip static sunt inițializate cu zero.
Când declarăm o dată membru ca fiind static într-o clasă, nu o definim. Cu alte cuvinte, nu îi alocăm memorie. (În limbajul C++, o declarație descrie ceva. O definire face ca ceva să existe.) În schimb, trebuie să dăm o definire globală a membrilor de tip date static undeva, în afara clasei. Aceasta se face redeclarând variabila static folosind operatorul de specificare a domeniului pentru a preciza clasa căreia îi aparține; ca urmare, se va aloca memorie pentru variabilă. (Să ne amintim că o declarare de class este o simplă construcție logică ce nu are suport material.)
Prin convenție, versiunile vechi de C++ nu impun a doua declarație a unei variabile membru static. Dar această convenție a dat naștere la numeroare inconveniente grave și a fost eliminată acum câțiva ani. Chiar și așa, putem încă să mai găsim coduri vechi de C++ care nu redeclară variabilele membre static. În aceste cazuri, va fi necesar să adăugăm definirile cerute.
O variabilă membru static există înainte de a fi creat orice obiect din acea clasă.
În general, când programul nostru are acces la un membru static independent de un obiect, trebuie să îl precizăm folosind numele clasei al cărei membru este.
Una dintre cele mai obișnuite utilizări ale variabilelor membre static este de a asigura controlul accesului la unele resurse comune. De exemplu, putem crea mai multe obiecte, fiecare dintre ele trebuind să scrie într-un anumit fișier de pe disc. Este evident, totuși, că doar unui singur obiect îi este permis să scrie într-un fișier, într-un anumit moment. În acest caz, este bine să declarăm o variabilă static care să indice când este folosit fișierul și când este liber.
Folosind variabilele membre static, virtual ar trebui să nu mai avem nevoie de nici o variabilă globală. Problema variabilelor globale pentru OOP este aceea că ele încalcă, aproape întotdeauna, principiul încapsulării.
1.3.7.2. Funcții membre statice
Funcțiile membre pot fi, de asemenea, declarate ca static. Există mai multe restricții relativ la acestea. În primul rând, ele pot să aibă acces doar la alți membri de tip static ai clasei, și, bineînțeles, la funcțiile și datele globale. În al doilea rând, ele nu pot avea un pointer de tip this. În al treilea rând, nu poate exista o versiune static și una ne-static ale aceleași funcții.
De fapt, funcțiile membre static au aplicații limitate, dar o bună utilizare a lor este aceea că ele pot „preinițializa” datele particulare de tip static, înainte de crearea efectivă a vreunui obiect.
1.3.8. Clase imbricate
Este posibil să definim o clasă în cadrul alteia. Procedând astfel, creăm clase imbricate. Deoarece o declarare a unei class definește, de fapt, un domeniu de influență, o clasă imbricată este validă doar în interiorul clasei ce o conține. Clasele imbricate sunt folosite rareori. Datorită flexibilității și puterii mecanismului de moștenire, practic nu este nevoie de clase imbricate.
1.3.9. Clase locale
O clasă poate fi definită în interiorul unei funcții. Când o clasă este declarată într-o funcție, ea este cunoscută doar acelei funcții și necunoscută in afara ei.
Claselor locale li se aplică mai multe restricții. Prima, toate funcțiile membre trebuie definite în interiorul declarației pentru class. Clasa locală nu poate să folosească sau să aibă acces la variabilele locale așe funcției în care este declarată (cu excepția variabilelor locale de tip static declarate în interiorul funcției). În interiorul unei clase locale nu poate fi declarată nici o variabilă de tip static. Din cauza acestor restricții, clasele locale nu sunt uzuale în programarea în C++.
Supraîncărcarea operatorilor
Presupunem că avem clasa vector3d și vrem să compunem cei doi vectori de componente (x,y,z). Unul din vectori este obiectul curent(cel prin care se apelează funcția), iar celălalt este transmis prin valoare. Iată cum arată clasa după adăugarea metodei prodscalar:
class vector3d
{ private:
double x,y,z;
public:
vector3d();
vector3d(double,double, double);
vector3d(const vector3d &);
vector3d compunere(vector3d v)
{ vector3d temp;
temp.x=x+v.x;
temp.y=y+v.y;
temp.z=z+v.z;
return temp;
}
}
void main()
{
vector3d u(3,4,6), v(2,5,7);
vector3d t=u.compunere(v);
cout<<"\n vectorul suma este: "<<t.getx()<<" "<<t.gety()<<" "<<t.getz();
}
Observăm că apelul metodei nu este elegant, fiind depărtat de cel cunoscut în matematică. Avem însă și o altă posibilitate și anume utilizarea funcțiilor operator.
Funcția de mai sus (compunere) se va numi de această dată operator+ . Cuvântul operator este cuvânt cheie. Prin urmare, prototipul și definiția devin:
vector3d operator+(vector3d&, vector3d&);
vector3d operator+(vector3d& u, vector3d &v)
{
return vector3d(u.x + v.x, u.y+v.y, u.z+v.z) ;
}
În acest fel, funcția poate fi apelată astfel:
t=u+v;
cout<<"\n vectorul suma este: "<<t.getx()<<" "<<t.gety()<<" "<<t.getz();
unde getx(), gety(), getz() returnează x,y, respectiv z.
Funcțiile obținute prin supraîncărcarea operatorilor au aceeași prioritate, asocitiavitate și n-aritate ca operatorii respectivi. Astfel, dacă operatorul este unar și funcția este unară, deci nu are parametri. Dacă operatorul este binar și funcția este binară. Aceasta înseamnă că are un singur parametru.
Prezentăm în cele ce urmează supraîncărcarea operatorului [].
Vom lua exemplul unei clase vector. Dorim ca data membru să fie privată.
class vector {
private:
int size ;
double *vect ;
public:
vector(int) ;
vector(const vector&) ;
void scrie(double, double);
double afiseaza(double);
…}
void vector::scrie(double i, double x)
{
vect[i]= x;
};
double vector::afiseaza(double i)
{
return vect[i];
};
void main()
{ clrscr();
int n;
cout<<"dati dim vector: ";
cin>>n;
vector a(n);
a.scrie(5,3); //componenta 5 ia valoarea 3
cout<<a.afiseaza(5); //afișează valoarea componentei 5, adică 3.
}
Observăm că accesul la componentele vectorului se face greoi. Suntem tentați să avem un acces de genul a[3]. Aceasta conduce la ideea supraîncărcării operatorului “[]”.
Astfel, vom avea:
class vector {
private:
int size ;
double *vect ;
public:
vector(int) ;
vector(const vector&) ;
double& operator[] (int i) { return vect[i]; }
void main()
{ clrscr();
int n;
cout<<"dati dim vector: ";
cin>>n;
vector a(n);
a[3]=5;
cout<<endl<<a[3];
}
Și vom obține același efect ca mai sus.
. Punctul și probleme asupra punctului
Noțiuni introductive
2.1.1.1 Despre coordonate:
În geometria analitică, poziția unui punct în plan se fixează cu ajutorul unei perechi de numere reale, obținute astfel. Luăm în planul dat două drepte perpendiculare ce formează sistemul de coordonate cartezian XOY. Abscisa și ordonata punctului sunt coordonatele sale.
x=abscisa punctului,
y=ordonata punctului
2.1.1.2. Probleme asupra punctului:
1. Distanța între două puncte funcție de coordonatele acestora.
Fie punctele și.
Distanța d dintre cele două puncte.
Din figură, observăm că
2. Condiția de coliniaritate a 3 puncte:
Fie punctele , și .
Ecuația dreptei prin punctele șieste: =0.
E suficient ca punctul A să aparțină dreptei BC, deci să verifice ecuația dreptei.
Condiția devine:
2.1.2 Implementare în C++
Cu datele de mai sus, construim în cele ce urmează o clasă Punct2D.
Aceasta va avea datele membru private, 3 constructori, dintre care unul de copiere, o metodă a clasei de calculare a distanței între două puncte, precum și o metodă friend, o metodă de calculare a determinantului specific pentru condiția de coliniaritate a 3 puncte, două funcții care returnează componenta x și respectiv componenta y a punctului, două metode în care componentele x, respectiv y iau anumite valori date de utilizator și, în sfârșit, două funcții de supraîncărcare a operatorilor „<<”, „>>” (prima pentru a afișa ușor un punct, iar a doua pentru a citi) și „-”(care returnează simetricul unui punct față de origine).
#include <iostream.h>
#include<stdlib.h>
#include<math.h>
void error(char *) ;
class Punct2D{
private:
double x,y;
public:
Punct2D();
Punct2D(double,double);
Punct2D(const Punct2D &pct);
friend double Dist2Puncte(Punct2D pct1,Punct2D pct2);
double Dist2Puncte(Punct2D pct1);
double Determ(Punct2D pct2, Punct2D pct3);
double GetX() {return x;}
double GetY() {return y;}
void PutX(double a){this->x=a;}
void PutY(double b){this->y=b;}
friend ostream& operator<<(ostream&, Punct2D&) ;
friend istream& operator >> (istream&, Punct2D&) ;
Punct2D operator-() ;
};
//Constructor de inițializare cu parametri
Punct2D::Punct2D(double a,double b)
{
x=a;
y=b;
}
//Constructor de copiere
Punct2D::Punct2D(const Punct2D &pct)
{
this->x=pct.x;
this->y=pct.y;
}
//Constructor de inițializare fără parametri
Punct2D::Punct2D()
{
x=0;
y=0;
}
//Metodă friend de calculare a distanței între punctele pct1 și pct2
double Dist2Puncte(Punct2D pct1,Punct2D pct2)
{
double dist;
dist=sqrt((pct2.x-pct1.x)*(pct2.x-pct1.x)+(pct2.y-pct1.y)*(pct2.y-pct1.y));
return dist;
}
//Metodă a clasei de calculare a distanței între două puncte, dintre care unul este cel curent, indicat de pointerul this
double Punct2D::Dist2Puncte(Punct2D pct1)
{
double dist;
dist=sqrt((this->x-pct1.x)*(this->x-pct1.x)+(this->y-pct1.y)*(this->y-pct1.y));
return dist;
}
//Calcularea deteminantului pentru a stabili daca 3 puncte sunt sau nu coliniare
double Punct2D::Determ(Punct2D pct2, Punct2D pct3)
{
double det;
det=this->x*pct2.y+pct2.x*pct3.y+this->y*pct3.x-pct3.x*pct2.y-pct3.y*this->x-pct2.x*this->y;
return det;
}
//Supraîncărcarea operatorului „<<”
ostream& operator << (ostream &s, Punct2D &z)
{
s << "(" << z.x << ", " << z.y << ")" ;
return s ;
}
//Supraîncărcarea operatorului „>>”
istream& operator >> (istream &s, Punct2D &z)
{
s >> z.x >> z.y ;
return s ;
}
Punct2D Punct2D::operator-()
{
return Punct2D(-x,-y);
}
void error (char *msg)
{ cerr << "\n Error …\n" ;
cerr << msg ;
cerr << "\nSTOP PROGRAM\n" ;
exit(1) ;
}
//În programul principal, utilizăm toate metodele din clasă și vedem că se obține același rezultat. Bineînțeles, la citire și afișare, pare mai elegantă folosirea funcțiilor de supraîncărcare.
void main()
{
Punct2D a,b;
double x1,x2;
cout<<"Dati coordonatele punctului a: ";
cin>>x1>>x2;
a.PutX(x1);a.PutY(x2);
cout<<"Punctul a("<<a.GetX()<<" , "<<a.GetY()<<")."<<endl;
//Citirea și afișarea unui punct folosind operatorii supraîncărcați: „>>” și „<<”
cout<<"Dati coordonatele punctului b: ";
cin>>b;
cout<<"Punctul b"<<b<<endl;
//Sau, mai simplu, putem da direct punctele, folosindu-ne de constructorul de inițializare.
Punct2D p2(3,-7),p1(-2,0),p3(6,-5);
cout<<"\nPunctul p1"<<p1<<endl;
cout<<"Punctul p2"<<p2<<endl;
cout<<"Punctul p3"<<p3<<endl;
double determinant=p1.Determ(p2,p3);
if (determinant==0)cout<<"Punctele sunt coliniare"<<endl;
else cout<<"Punctele nu sunt coliniare"<<endl;
//Calculăm distanța dintre p1 și p2 apelând ambele metode.
double d=Dist2Puncte(p1,p2);
cout<<"Distanta dintre p1 si p2 este = "<<d<<'\n';
double dista=p1.Dist2Puncte(p2);
cout<<"Distanta dintre p1 si p2 este= "<<dista<<'\n';
cout<<"Simetricul punctului p1 fata de origine este: "<<-p1;
cout<<"\nSimetricul punctului p2 fata de origine este: "<<-p2;
}
Dacă dăm punctele p1, p2, p3 de mai sus, ele nu sunt coliniare. Un exemplu de coliniaritate ar fi p1(1,6), p2(-3,2), p3(2,7).
Vectori în spațiu
2.2.1. Noțiuni generale
Fie A, B două puncte din spațiu. Perechea ordonată (A,B) se numește bipunct de origine A și extremitate B.
Bipunctul (A,B) este un reprezentant al vectorului . Pentru , (AB) este dreapta determinată de punctele A și B, numită dreapta suport a bipunctului. Aceasta definește direcția vectorului , iar sensul este cel al deplasării prin care accedem de la punctul A la punctul B. Modulul sau norma vectorului este distanța de la A la B și se notează .
Vectorul este reprezentat de bipunctul (A,B) cu A=B.
Opusul vectorului este . Acesta are aceeași direcție, același modul, dar este de sens contrar.
Trei vectori se numesc coplanari dacă există astfel încât sau sau .
Reper. Bază.
Fie vectorii necoplanari și O un punct oarecare, iar A, B, C trei punte astfel încât . Vom considera dreapta d=(OC), dotată cu reperul și planul dotat cu reperul . Un punct M se proiectează pe planul , paralel cu d în punctul M1, iar pe dreapta d, paralel cu planul în punctul M2. patrulaterul este un paralelogram, deci , astfel încât și , deci rezultă că există un triplet de numere reale astfel încât .
Un triplet de vectori , astfel încât nu sunt coplanari, se numește bază.
este un reper cartezian al spațiului, punctul O este numit originea reperului și este o bază.
Dacă (OA), (OB), (OC) sunt perpendiculare două câte două reperul este ortogonal. Dacă în plus, , spunem că reperul este ortonormal.
(x,y,z) este tripletul coordonatelor punctului M în reperul sau ale vectorului în baza . Un punct M de coordonate (x,y,z) se notează M(x,y,z). În mod analog, vectorul de coordonate se notează .
Produsul scalar a doi vectori în spațiu, notat , se definește ca în plan: unghiul dintre cei doi vectori.
Doi vectori sunt ortogonali dacă și numai dacă .
Expresia produsului scalar într-o bază ortonormală.
Fie o bază ortonormală și , .
Avem și
Deci, .
.
2.2.2 Implementare în C++
Având aceste noțiuni, realizăm programul în C++, pornind de la o clasă vector3d, care va fi de fapt un vector de 3 componente. Lucrăm într-un bază ortonormală. Avem 4 constructori, vom calcula norma unui vector, produsul scalar a doi vectori, precum și suma lor. Citirea și afișarea se face similar ca la clasa precedentă.
#include <iostream.h>
#include <conio.h>
#include<math.h>
#define newline cout << endl
class vector3d
{ private:
double x,y,z;
public:
vector3d();
vector3d(double,double, double);
vector3d(const vector3d &);
vector3d compunere(vector3d);
friend vector3d compunere(vector3d, vector3d);
friend vector3d operator+(vector3d&, vector3d&) ;
double prodscalar(vector3d);
friend double normavec(vector3d);
friend double operator*(vector3d&, vector3d&);
void putx(double a) { x=a;};
void puty(double a) { y=a;};
void putz(double a) { z=a;};
double getx(){return x;};
double gety(){return y;};
double getz(){return z;};
vector3d prodvect(vector3d u);
friend ostream& operator<<(ostream&, vector3d&) ;
friend istream& operator>>(istream&, vector3d&) ;
};
//Constructor de inițializare fără parametri
vector3d::vector3d()
{
x=0;
y=0;
z=0;
}
vector3d::vector3d(double a, double b, double c)
{ x=a;
y=b;
z=c;
}
//Constructor de copiere
vector3d::vector3d(const vector3d &v)
{
x=v.x;
y=v.y;
z=v.z;
}
//Metodă a clasei de compunere a doi vectori
vector3d vector3d::compunere(vector3d v)
{
vector3d temp;
temp.x=x+v.x;
temp.y=y+v.y;
temp.z=z+v.z;
return temp;
}
//Metodă friend de compunere a doi vectori
vector3d compunere(vector3d u, vector3d v)
{
vector3d temp;
temp.x=u.x+v.x;
temp.y=u.y+v.y;
temp.z=u.z+v.z;
return temp;
}
//Realizăm tot compunerea a doi vectori, dar folosind supraîncărcarea operatorului “+”
vector3d operator+(vector3d& u, vector3d &v)
{
return vector3d(u.x + v.x, u.y+v.y, u.z+v.z) ;
}
double vector3d::prodscalar(vector3d v)
{
return x*v.x+y*v.y+z*v.z;
}
double operator*(vector3d &v, vector3d &u)
{ return u.x*v.x+u.y*v.y+u.z*v.z;
}
double normavec(vector3d u)
{ return sqrt(u*u);
}
ostream& operator<<(ostream &s, vector3d &u)
{
s << "(" << u.x << ", " << u.y << ","<<u.z<<")" ;
return s ;
}
istream& operator>>(istream &s, vector3d &u)
{
s >> u.x >> u.y>>u.z ;
return s ;
}
void main()
{ clrscr();
double a,b,c;
vector3d u,v;
cout<<"dati componentele vectorului u: ";
cin>>a>>b>>c;
u.putx(a);u.puty(b);u.putz(c);
cout<<"u("<<u.getx()<<","<<u.gety()<<","<<u.getz()<<")"<<endl;
cout<<"Dati componentele vectorului v: ";
cin>>v;
cout<<"Vectorul v"<<v<<endl;
//vector3d u(2,3,5),v(2,1,3) ;
cout<<"Suma celor doi vectori este: "<<u+v<<endl;
cout<<"Produsul scalar a celor doi vectori este: "<<u*v<<endl;
cout<<"Produsul scalar a celor 2 vectori este: "<<u.prodscalar(v)<<endl;
cout<<"Norma vectorului u este: "<<normavec(u);
}
Dând de la tastatură vectorii u(2,3,5) și v(2,1,3), vom obține:
Există două metode practice de determinare a soluției unui sistem : directe, prin care soluția exactă se obține într-un număr finit de operații aritmetice (dacă facem abstracție de erorile de rotunjire) și iterative, pentru care soluția sistemului x este limita unui șir de vectori , pentru (în acest caz, oprirea se face când s-a ajuns la precizia dorită).
Metode directe de rezolvare a sistemelor liniare:
Metoda lui Gauss (a eliminării)
Considerente generale
Pornim de la o matrice A pătratică, nesingulară și b vector nenul.
Ideea metodei este de a elimina succesiv necunoscutele astfel încât matricea coeficienților A să fie adusă în formă superior triunghiulară.
O matrice este superior triunghiulară dacă , pentru i>j.
O matrice este inferior triunghiulară dacă , pentru i<j.
Pas 1.
Eliminarea necunoscutei din ecuațiile de rang 2,3,…,n astfel încât prima coloană a matricei coeficienților A să fie adusă în formă superior triunghiulară.
Pas 2.
Eliminarea necunoscutei din ecuațiile de rang 3,…,n astfel încât a doua coloană a matricei coeficienților A să fie adusă în formă superior triunghiulară, fără a afecta forma primei coloane a matricei A.
…
Pas k. ()
Eliminarea nescunoscutei din ecuațiile de rang p+1,…,n astfel încât matricea coeficienților A să fie adusă în formă superior triunghiulară, fără a afecta forma coloanelor 1,…,p-1.
Înainte de primul pas, sistemul are următoarea formă:
Pasul 1. Eliminăm necunoscuta din ecuațiile de rang 2,3,…,n astfel încât să anulăm (), în felul următor.
Păstrăm prima ecuație, iar ecuațiile i, le transformăm astfel:
sau , . Deci rezultă .
Pasul 2.
Eliminăm necunoscuta din ecuațiile de rang 3,…,n astfel încât (obținut la pasul 1) să devină 0, pentru .
După pasul 1, sistemul devine:
Păstrăm primele două ecuații neschimbate și se transformă ecuațiile i, astfel:
Deci =0, .
Pasul p ().
Eliminăm necunoscuta din ecuațiile de rang p+1,…,n astfel încât să aducem coloanele p în formă superior triunghiulară.
Păstrăm primele p ecuații și transformăm ecuațiile i, .
.
Algoritmul are n-1 pași. Pentru a putea aplica acest algoritm, avem nevoie ca:
Aceste elemente se numesc pivoți.
Pentru a evita împărțirea la 0 sau la numere foarte mici se introduce pivotarea parțială.
Pivotarea parțială înseamnă schimbarea ordinii ecuațiilor sistemului astfel încât pivotul la pasul p să fie cel mai mare coeficient al necunoscutei .
Astfel:
Pasul 1.
Vom calcula întâi maximul dintre coeficienții lui în modul.
Elementul pivot va fi .
Dacă atunci schimbăm ecuația 1 cu ecuația și ecuațiile i devin: .
Pasul2.
Calculăm maximul dintre coeficienții lui în modul.
Elementul pivot va fi .
Dacă atunci schimbăm ecuația 2 cu ecuația și ecuațiile i devin: .
…
Pasul p. ()
Calculăm maximul dintre coeficienții lui în modul.
Elementul pivot va fi .
Dacă atunci schimbăm ecuația p cu ecuația și ecuațiile i devin: .
După ce am adus matricea la forma superior triunghiulară, obținem de fapt următorul sistem:
Din ultima ecuație scoatem și apoi înlocuim în ecuația precedentă pentru a afla și tot așa până ajungem la .
Implementare în C++
În programul care urmează, la fiecare pas se reactualizează coeficienții matricei, deci considerăm sistemul final ca fiind de forma:
Astfel vom avea , ,… de unde scoatem formula pentru : .
Observăm căpoate fi scrisă ca un produs vectorial a doi vectori.
Astfel suma poate fi produs vectorial dintre A[i] și x.
A[i]=. Inițial, x=(0,0,…,0). După ce am calculat , x=(0,0,…, ).
Pentru i=n-1, A[n-1]=.Produsul lor scalar este exact . Și deoarece vectorul x are multe elemente 0, pentru a simplifica operația, facem înmulțirea componentelor începând de la indicele i+1 până la n.
Ținem cont că lucrăm în C++, deci numerotarea indicilor pentru vectori și matrici pornește de la 0. Astfel vom avea un sistem de ecuații de la 0 la n-1.
În continuare implementăm în C++ metoda lui Gauss cu pivotare parțială.
Lucrăm cu o bibliotecă personală numită vecmat.h, pe care o includem la începutul programului și care conține, pe lângă definirea clasele vector și matrix de care avem nevoie cu metodele specifice, și câteva metode pe care nu le folosim în programul principal.
Matricea A este matricea coeficienților, iar vectorul b este vectorul termenilor liberi.
#include "gauss0.h"
const double tol = 1.0E-7 ;
//Determin elementul pivot de pe coloana j: coeficientul maxim al necunoscutei
double pivot (matrix &A, vector &b, int j)
{
int n = A.getsize() ;
int i = j ;
double t = 0.0F ;
for ( int k = j ; k < n ; k++) {
double akj = fabs(A[k][j]) ;
if ( akj > t ) {
t = akj ;
i = k ;
}
}
// Dacă am găsit un element diferit de cel de la care am plecat, mai mare decât acesta, atunci fac interschimbarea între liniile respective și între cele două componente ale vectorului liber b
if ( i > j ) {
A.swap(j,i) ;
b.swap(j,i) ;
}
return A[j][j] ; //returnez elementul pivot
}
//Calculăm produsul scalar dintre doi vectori, de la poziția k1 până la poziția k2.
double dotprod(vector &u, vector &v, int k1, int k2)
{
double sum = 0.0F ;
for ( int i = k1 ; i <= k2 ; i++)
sum += u[i] * v[i] ;
return sum ;
}
//Aducem matricea la formă superior triunghiulară.
void triang(matrix &A, vector &b)
{
int n = A.getsize() ;
for ( int j = 0 ; j < n-1 ; j++) {
double d = pivot(A,b,j) ;
//Dacă în urma pivotării, elemental pivot găsit este 0, algoritmul se oprește.
if (fabs(d) < tol)
error( /*ZERO DETERMINANT*/ "Pivot mic") ;
for ( int i = j+1 ; i < n ; i++) {
double temp = A[i][j] / d ;
for ( int k = j+1 ; k < n ; k++)
A[i][k] -= temp*A[j][k] ;
b[i] -= temp * b[j] ;
}
}
}
//Calculez soluțiile sistemului, păstrate în vectorul x
void backst(matrix &A, vector &b, vector &x)
{
int n = A.getsize() ;
double d = A[n-1][n-1] ;
if (fabs(d) < tol)
error("DIAGONAL ELEMENT TOO SMALL in BACKST") ;
x[n-1] = b[n-1] / d ;
for ( int i = n-2 ; i >= 0 ; i–)
x[i] = (b[i] – dotprod(A[i],x,i+1,n-1)) / A[i][i] ;
}
void gauss (matrix &A, vector &b, vector &x)
{
triang(A,b) ;
backst(A,b,x) ;
}
void main()
{
int i, j, n,p ;
cout << "dimensiunea sistemului : " ;
cin >> n ;
matrix A(n) ;
vector b(n), x(n) ;
for ( i = 0 ; i < n ; i++ )
{
cout << "linia " << i << endl ;
for ( j = 0 ; j < n ; j++ )
{
cout << j << " : " ;
cin >> A[i][j] ;
}
}
//Vectorul b trebuie să fie nenul
do{
p=0;
for(i=0;i<n;i++){
cout << "Dati b("<<i<<"): ";
cin >> b[i] ;
if (b[i]==0) p+=1;}
}
while (p==n);
cout<<endl;
A.print();
gauss(A,b,x) ;
cout << "\nsolutia sistemului\n" ;
x.print() ;
}
ifndef VECMAT
#define VECMAT
void error(char *) ;
class matrix ;
class vector {
friend matrix ;
private:
int size ;
double *vect ;
int range(int) ;
public:
vector(int) ;
vector(const double*, int) ;
vector(const vector&) ;
~vector() { delete vect ; }
double& operator[] (int i) { return vect[range(i)]; }
int getsize() { return size; }
void print() ;
void swap(int, int) ;
friend double norm_l2 (vector&) ;
friend double norm_spec (vector&) ;
friend double norm_inf (vector&) ;
friend double norm_l1 (vector&) ;
} ;
class matrix {
private:
int nrows, ncols ;
vector **mat ;
int range(int) ;
public:
matrix(int,int) ;
matrix(int) ;
matrix(const matrix&) ;
~matrix() ;
vector& operator[](int i)
{ return *mat[range(i)] ; }
matrix& operator+() { return *this ;}
int getnrows() { return nrows ; }
int getncols() { return ncols ; }
int getsize() ; // square matrix only !
void print() ;
void swap(int,int) ;
} ;
inline int vector::range (int i)
{ if (i < 0 || i >= size)
error ("INDEX OUT OF RANGE in VECTOR::operator[]") ;
return i ;
}
int matrix::range (int i)
{ if (i < 0 || i >= nrows)
error ("INDEX OUT OF RANGE in MATRIX::operator[]") ;
return i ;
}
#endif /* VECMAT */
#include <iostream.h>
#include <iomanip.h>
#include <math.h>
#include <stdlib.h>
#include <conio.h>
vector::vector(int n)
{ size = n ;
vect = new double [size] ;
if (!vect)
error ("ALLOCATION FAILURE in ::vector(int)") ;
for (int i = 0; i < size; i++)
vect[i] = 0 ;
}
vector::vector (const double *a, int n)
{ size = n ;
vect = new double [size] ;
if (!vect)
error ("ALLOCATION FAILURE in ::vector(const double*, int)") ;
for (int i = 0; i < size; i++)
vect[i] = a[i] ;
}
vector::vector (const vector &v)
{ size = v.size ;
vect = new double [size] ;
if (!vect)
error ("ALLOCATION FAILURE in ::vector(const vector&)") ;
for (int i = 0; i < size; i++)
vect[i] = v.vect[i] ;
}
void vector::print ()
{ int n = size ;
cout << "VECTOR::PRINT\n" ;
for (int i=0; i < n; i++)
cout << i << " : " << vect[i]
<< endl ;
}
void vector::swap (int i, int j)
{ double temp ;
cout << "VECTOR::SWAP\n" ;
temp = vect[range(i)] ;
vect[i] = vect[range(j)] ;
vect[j] = temp ;
}
double norm_spec (vector &v)
{ int n = v.size ;
const double eps = 1.0E-15 ;
double t, vi, x ;
t = fabs(v.vect[0]) ;
for (int i = 1; i < n; i++) {
vi = fabs(v.vect[i]) ;
if (t < eps) t = vi ;
else
{ x = vi / t ;
t *= sqrt(1.0 + x*x) ;
}
}
return t ;
}
double norm_l2 (vector &v)
{ int n = v.size ;
double t, vi ;
t = 0.0F ;
for (int i = 0; i < n; i++) {
vi = fabs(v.vect[i]) ;
t += vi*vi ;
}
return sqrt(t) ;
}
double norm_inf (vector &v)
{ int n = v.size ;
double t, vi ;
t = 0.0F ;
for (int i = 0; i < n; i++) {
vi = fabs(v.vect[i]) ;
if (vi > t) t = vi ;
}
return t ;
}
double norm_l1 (vector &v)
{ int n = v.size ;
double t ;
t = 0.0F ;
for (int i = 0; i < n; i++)
t += fabs(v.vect[i]) ;
return t ;
}
int matrix::getsize()
{ if ( nrows != ncols )
error("MATRIX::getsize() ERROR – not a square matrix") ;
return nrows ;
}
matrix::matrix (int m, int n)
{ int i ;
nrows = m ;
ncols = n ;
mat = new vector* [nrows] ;
if ( !mat ) error("ROW ALLOCATION FAILURE IN MATRIX") ;
for (i=0 ; i < nrows ; i++) {
mat[i] = new vector(ncols) ;
if ( !mat[i] ) error("COLUMN ALLOCATION FAILURE IN MATRIX") ;
}
}
matrix::matrix (int n)
{ int i ;
nrows = ncols = n ;
mat = new vector* [nrows] ;
if ( !mat ) error("ROW ALLOCATION FAILURE IN MATRIX") ;
for (i=0 ; i < nrows ; i++) {
mat[i] = new vector(ncols) ;
if ( !mat[i] ) error("COLUMN ALLOCATION FAILURE IN MATRIX") ;
}
}
matrix::matrix (const matrix &A)
{ int i ;
nrows = A.nrows ;
ncols = A.ncols ;
mat = new vector* [nrows] ;
if ( !mat ) error("ROW ALLOCATION FAILURE IN MATRIX") ;
for (i=0 ; i < nrows ; i++) {
mat[i] = new vector(ncols) ;
if ( !mat[i] ) error("COLUMN ALLOCATION FAILURE IN MATRIX") ;
}
for (i=0 ; i < nrows ; i++)
*mat[i] = *A.mat[i] ;
}
matrix::~matrix()
{
for (int i = nrows ; i > 0 ; i–)
delete mat[i-1] ;
delete mat ;
}
void matrix::print()
{ int i, j ;
cout << "MATRIX PRINT\n" ;
for ( i = 0 ; i < nrows ; i++) {
for ( j = 0 ; j < ncols ; j++)
cout << mat[i]->vect[j] << " " ;
cout << endl ;
}
}
void matrix::swap (int i, int j)
{ vector *temp ;
cout<<"Matrix::SWAP\n";
temp = mat[range(i)] ;
mat[i] = mat[range(j)] ;
mat[j] = temp ;
}
void error (char *msg)
{ cerr << "\nRuntime Error …\n" ;
cerr << msg ;
cerr << "\nSTOP PROGRAM\n" ;
exit(1) ;
}
Dând matricea și vectorul liber , trebuie să obținem soluția x=(1,-1,2)
Dacă dăm vectorul b astfel încât să fie nul, ni se va cere să introducem iar componentele sale.
~Bibliografie~
Tudor Sorin, Informatică, Manual pentru clasa a VI-a, Varianta C++, Editura L&S INFOMAT București.
Herbert Schildt, C++ manual complet, editura Teora
Daniela Oprescu, Liana Bejan Ienulescu, Viorica Pătrașcu, Informatică, Manual pentru clasa a XI-a, varianta C++, Editura NICULESCU ABC SRL, București 2002
Gabriela Tănase, Algebră liniară numerică curs
Constantin Ilioi, Analiză numerică, partea I, Metode numerice de rezolvare a problemelor finit dimensionale, pentru uzul studenților, 1982
Viorel Arnăutu, Tehnici de programare curs
Alexandru Myller, Geometrie analitică, Editura didactică și pedagogică, București 1972
Copyright Notice
© Licențiada.org respectă drepturile de proprietate intelectuală și așteaptă ca toți utilizatorii să facă același lucru. Dacă consideri că un conținut de pe site încalcă drepturile tale de autor, te rugăm să trimiți o notificare DMCA.
Acest articol: Algebra Liniara Numerica. Aplicatii ale Poo In C++ (ID: 149030)
Dacă considerați că acest conținut vă încalcă drepturile de autor, vă rugăm să depuneți o cerere pe pagina noastră Copyright Takedown.
