TDIU01 Programmering i C++ Föreläsning 6 - Klasser Eric Elfving, eric.elfving@liu.se Institutionen för datavetenskap (IDA) Avdelningen för Programvara och system (SaS)
Klasser När vi skapade vår lista gjorde vi det genom att definiera en pekare till en posttyp och fria funktioner som kunde manipulera en sådan pekare. Detta har dock vissa nackdelar: Eftersom användaren (programmeraren som skapar huvudprogrammet) vet att det är en pekare kan den manipulera den som en pekare. Eftersom vi måste ha en nollpekare från början (vårt sätt att se att det är en tom lista) kräver vi av användaren att pekaren sätts till noll. Om den inte nollställs ger det stora problem i vår kod. Dessa problem (och andra) kan vi lösa med klasser. En klass är en beskrivning av en datatyp. Genom att göra om vår lista till en klass kan vi dels dölja våra pekare (datarepresentation) samt få en bättre koppling mellan data och funktioner för att manipulera datat. Detta ska vi öva på under lektion och på lab.
Klasser En klass har två delar, en privat del och en publik del. I den publika delen lägger vi saker som vi vill att alla ska kunna komma åt, t.ex. metoder för att operera på vårt data. I den privata delen lägger vi det vi vill gömma för användaren, t.ex. datamedlemmar och hjälpfunktioner. class Klass public: Klass(); ~Klass(); void metod(); void const_metod() const; private: int data; ; Datamedlem void helper_function(); Metoder / medlemsfunktioner
Användning av klasser Vi kan se en klass som en beskrivning av en typ. Därför kan vi skapa en variabel (ett objekt) utifrån en klass precis som vi skapar en variabel av en grundläggande typ. För att sedan anropa en metod använder vi punktoperatorn.
Metoder En funktion som är med i en klass kallas metod eller medlemsfunktion. De fungerar i grunden som vanliga funktioner men har alltid tillgång till objektets datamedlemmar. I klassdefinitionen deklarerar man metoder precis som man gör med vanliga funktioner. Det finns dock ett specialfall; när man har en metod som inte ändrar på objektets datamedlemmar bör den deklareras som en const-metod. Då kan den användas på ett const-objekt. class Klass public: void set_value(int); int get_value() const; private: int data; void helper_function(); ;
Speciella metoder Det finns alltid minst fyra metoder för en klass: Konstruktor skapar ett objekt Destruktor tar bort ett objekt Kopieringskonstruktor och Kopieringstilldelningsoperator Om vi inte deklarerar någon av dessa själva kommer vi få en genererad av kompilatorn.
Konstruktor Konstruktorn är en speciell metod som finns för alla klasser. Den anropas när ett objekt skapas och används främst för att initiera datamedlemmar. Konstruktorn kan ta 0,1 eller flera parametrar, har samma namn som klassen och ingen returtyp. Klass() data = 0; Klass() :data(0) Klass(int d) data = d; Klass(int d) :data(d) Till höger ser vi användandet av en initieringslista. Den används för att initiera datamedlemmar Används såhär: Klass k; // Konstruktorn utan argument anropas Klass k2(2); // Konstruktorn med en parameter anropas
Destruktor Destruktorn anropas innan ett objekt förstörs. Dess huvudsakliga uppgift är att återlämna resurser som vår klass använt under dess livstid. Det vanligaste är att man återlämnar dynamisk allokerat minne men det kan också handla om att t.ex. stänga filer eller avsluta en nätverkskontakt. Om klassens datamedlemmar endast har enkla datatyper behövs sällan en egendefinierad destruktor, utan den kompilatorgenererade duger fint. class Klass public: Klass(); void metod(); void const_metod() const; private: int data; ; void helper_function(); class Klass2 public: Klass2(int d) Data = new int(d); ~Klass2() delete data; private: int * data; ;
Kopiering Det finns två sätt att kopiera objekt; med kopieringskonstruktor och med kopieringstilldelning. Kopieringskonstruktorn är en konstruktor med vilken man kopierar ett redan existerande objekt. Klass(const Klass & other) : data(other.data) Klass & operator=(const Klass &other) data = other.data; return *this; Kopieringstilldelningsoperatorn (tilldelning) används för att ändra värden på ett redan existerande objekt så att det blir en kopia av något annat objekt. int main() Klass k1(2); Klass k2(k1); Klass k3; k3 = k1; // // // // konstruktorn för heltal kopieringskonstruktor default konstruktor kopieringstilldelning
Kopiering, fortsättning Om man inte deklarerar kopieringskonstruktorn eller kopieringstilldelningsoperatorn kommer vi som sagt få en kompilatorgenererad. Men vad händer om vi inte vill tillåta kopiering? Vi kan förbjuda kopiering genom att deklarera båda metoderna i privata delen av vår klass. Därmed ser kompilatorn att de finns med men eftersom de ligger privat så kan inte användaren komma åt dem. Detta rekommenderar vi att ni gör för listklassen ni ska bygga för att slippa problem med grund kopiering.
Träd Nu gör vi om trädstrukturen vi hade förra föreläsningen till en klass. Till vänster är den gamla koden och till höger har vi den nya.
Träd header-filen (Tree.h) #include <iosfwd> class Tree public: #ifndef TREE_H #define TREE_H #include <iosfwd> typedef struct Tree_Node *Tree; void void bool void insert(int, Tree &); print(std::ostream &, Tree, int = 1); member(int, Tree); clear(tree &); #endif Tree() : root_(0) ~Tree(); void void bool void bool insert(int); print(std::ostream & = std::cout, int = 1) const; member(int) const; clear(); empty() const; private: // Dold kopieringskonstruktor och kopieringstilldelningsoperator // för att förhindra kopiering Tree(const Tree&); Tree &operator=(const Tree &); typedef struct Tree_Node * Sub_Tree; Sub_Tree root_; // rekursiva hjälpfunktioner void insert(int, Sub_Tree&); void print(ostream &, const Sub_Tree, int) const; bool member(int, const Sub_Tree) const; ;
Träd cc-filen (Tree.cc) #include <iostream> #include <iomanip> using namespace std; #include "Tree.h" struct Tree_Node int data; Tree left; Tree right; ; /* * create_leaf skapar en lövnod med det värde som skickas in */ void create_leaf(int data, Tree &tree) tree = new Tree_Node; tree >data = data; tree >left = 0; tree >right = 0; #include <iostream> #include <iomanip> using namespace std; #include "Tree.h" struct Tree_Node Tree_Node(int data, Tree_Node * left = 0, Tree_Node * right = 0) : data_(data), left_(left), right_(right) ~Tree_Node() delete delete left_ right_ left_; right_; = 0; = 0; int data_; Tree_Node * left_; Tree_Node * right_; ;
/* *insert stoppar in ett värde som ett löv i trädet. Om * värdet redan finns görs inget */ void insert(int data, Tree &tree) /* * insert stoppar in ett värde som ett löv i trädet. * Om värdet redan finns görs inget */ void Tree::insert(int data, Sub_Tree &tree) if (tree == 0) if (tree == 0) create_leaf(data,tree); tree = new Tree_Node(data); else if (tree >data_ > data) else if (tree >data > data) insert(data, tree >left_); insert(data, tree >left); else if (tree >data_ < data) else if (tree >data < data) insert(data, tree >right_); insert(data, tree >right); void Tree::insert(int data) insert(data, root_);
/* * print skriver ut trädet in order med höger delträd först * följt av noden och därefter vänster delträd. Detta gör att * man ser trädet rätt om man vrider skärmen (eller huvudet) 90 grader */ void print(ostream &os, Tree tree, int indent) if (tree == 0) return; void Tree::print(ostream &os, const Sub_Tree tree, int indent) const if (tree == 0) return; // Ett tomt träd ska inte skrivas ut if (tree >right!= 0) print(os,tree >right,indent+2); cout << setw(indent+1) << / << endl; if (tree >right_!= 0) // skriv ut höger subträd om det existerar cout << setw(indent) << tree >data << endl; cout << setw(indent) << tree >data_ << endl; // skriv ut nuvarande nod if (tree >left!= 0) cout << setw(indent+1) << \\ << endl; print(os,tree >left,indent+2); if (tree >left_!= 0) // skriv ut höger subträd om det existerar cout << setw(indent+1) << \\ << endl; print(os,tree >left_, indent+2); print(os,tree >right_, indent+2); cout << setw(indent+1) << / << endl; void Tree::print(ostream &os, int indent) const print(os, root_,indent);
/* * member returnerar true om värdet finns i trädet, annars false */ /* * member returnerar true om värdet finns i trädet, annars false */ bool member(int data, Tree tree) bool Tree::member(int data, const Sub_Tree tree) const if (tree == 0) return false; if (tree == 0) return false; else if (tree >data_ == data) return true; else if (tree >data_ == data) return true; else if (tree >data_ > data) return member(data, tree >left_); else if (tree >data_ > data) return member(data, tree >left_); return member(data, tree >right_); return member(data, tree >right_); bool Tree::member(int data) const return member(data, root_);
/* * clear tar bort ett träd rekursivt genom att först anropa clear * för subträden och sedan återlämna sig själv */ void clear(tree &t) if (t!= 0) clear(t >left); clear(t >right); delete t; t = 0; /* * clear behövs inte längre utan vi kan skapa en * destruktor istället. Om vi vill att användaren * ska få explicit tömma en lista kan det dock * vara bra att lämna kvar clear. */ Tree::~Tree() delete root_; root_ = 0; /* * empty undersöker om trädet är tomt eller ej. */ bool Tree::empty() const return root_ == 0;
Träd huvudprogrammet (tree_main.cc) #include <iostream> #include <iomanip> using namespace std; #include "Tree.h" int main() Tree tree = 0; for (int i = 0; i<5; ++i) insert(i,tree); #include <iostream> #include <iomanip> using namespace std; #include "Tree.h" int main() Tree tree; for (int i = 0; i<5; ++i) tree.insert(i); print(cout, tree); clear(tree); tree.print(); tree.clear(); if (tree == 0) cout << endl <<"Trädet är nu tomt" << endl << endl; if (tree.empty()) cout << endl <<"Trädet är nu tomt" << endl << endl; insert(5, tree); insert(2, tree); insert(1, tree); tree.insert(5); tree.insert(2); tree.insert(1); print(cout, tree); tree.print(cout); cout << boolalpha << member(0, tree) << endl; cout << boolalpha << tree.member(0) << endl;
Objektorientering???
www.liu.se