Skapa, kopiera och destruera klassobjekt

Relevanta dokument
TDDC76 - Programmering och Datastrukturer

TDIU01 Programmering i C++

Operatoröverlagring. endast operatorsymboler definierade i C++ kan överlagras = += -= *= /= %= ^= &= = <<= >>= < > <= >= ==!= && > ->*, [ ] ( )

Innehåll. Konstruktorer vid arv Regler för basklassens konstruktor. Konstruktorer vid arv. Konstruktorer vid arv. Konstruktorer vid arv

TDDC76 PoD OH Föreläsning C Härledda klasser

Innehåll. 1 Kort om dynamisk polymorfism. 2 Arv i C++ 3 Multipelt arv. 4 Något om statisk polymorfism. class Container {

Dynamisk bindning och polymorfism

Lektionsuppgifter. TDDI14 Objektorienterad programmering. Lektionsplanering Lektion Lektion Lektion

Introduktion till arv

Programmering i C++ EDA623 Arv. EDA623 (Föreläsning 6) HT / 42

Del2 Klasser, medlemmar och arv Ämnesområden denna föreläsning:

Programmering i C++ EDA623 Objektorienterad programutveckling. EDA623 (Föreläsning 5) HT / 33

Klasser. Kapitel 2. Kapitel 2 - Klasser, medlemmar och arv. Klasser. Klasser Medlemmar Arv

Objektorientering - Arv och polymorfi. Eric Elfving Institutionen för datavetenskap

Innehåll. Typomvandlingar (casting) Implicita Typomvandlingar. Typomvandlingar (casting) Implicita Typomvandlingar

TDDC76 - Programmering och Datastrukturer

Introduktion. Klasser. TDP004 Objektorienterad Programmering Fö 2 Objektorientering grunder

Programsystem konstruktion med C++ (2D1387) Innehåll. övning 2 klasser och arv

Kommentarer till Person

Operatoröverlagring. 1 Definition och deklaration av operatorfunktioner. 2 Anrop av operatorfunktioner

Innehåll. Användardefinierade typer. Användardefinierade typer Kategorier. Konstruktorer. Konstruktorer Två sätt att skriva initiering av medlemmar

Innehåll. Pekaren this Självreferens. Klasser Resurshantering, representation. Överlagring av operatorer. Överlagring av operatorer

TDDI14 Objektorienterad programmering

Lite om felhantering och Exceptions Mer om variabler och parametrar Fält (eng array) och klassen ArrayList.

KLASSER. Inkapsling Abstrakt datatyp Public och private. Klassmedlemmar Datamedlemmar Exempel Funktionsmedlemmar

Programsystemkonstruktion med C++: Övning 2. Karl Palmskog september 2010

Objektorienterad programmering Föreläsning 12. Copyright Mahmud Al Hakim

allokeras på stacken dynamiskt new delete

Minneshantering. Minneshantering. Minneshantering. Undvik pekare

Innehåll. Pekare Exempel

Innehåll. Pekare Exempel

TDDC76 - Programmering och Datastrukturer

Kapitel 6 - Undantag

DD2387 Programsystemkonstruktion med C++ Tentamen 1 Torsdag 7 januari 2016, 14:00-18:00

Trädtraversering. Binärt träd. Trädtraversering djupet-först. Trädtraversering bredden-först

Idag. Javas datatyper, arrayer, referenssemantik. Arv, polymorfi, typregler, typkonvertering. Tänker inte säga nåt om det som är likadant som i C.

Nedan skapar vi klassen Person innehållande datamedlemmar för förnamn, efternamn, ålder, längd och vikt:

TDDC76 PoD OH Föreläsning C Klasser

Klassmedlemmar. Klasser. Konstruktion av en enskild, icke-trivial klass. Klassen String funktionalitet

Programmering i C++ EDA623 Mer om klasser. EDA623 (Föreläsning 6) HT / 26

Inledande programmering med C# (1DV402) Tärningarna ska kastas

Java, klasser, objekt (Skansholm: Kapitel 2)

Föreläsning 5 (6) Metoder. Metoder Deklarera. Metoder. Parametrar Returvärden Överlagring Konstruktorer Statiska metoder tostring() metoden javadoc

Övriga byggstenar. Övriga byggstenar. Några tips under programutveckling. Beroenden Pekare till funktioner Typkonvertering

Kopiering av objekt i Java

Klasser. Konstruktion av en enskild, icke-trivial klass

Klasser. Klassen String funktionalitet. Klassen String funktionalitet, forts. Klassen String funktionalitet, forts. Vad ska man kunna göra?

5 Arv och dynamisk bindning FIGUR

Föreläsning 5-6 Innehåll. Exempel på program med objekt. Exempel: kvadratobjekt. Objekt. Skapa och använda objekt Skriva egna klasser

DAT043 - Föreläsning 7

F8: Typkonvertering i C++

Föreläsning 5-6 Innehåll

OOP Objekt-orienterad programmering

Arv: Fordonsexempel. Arv. Arv: fordonsexempel (forts) Arv: Ett exempel. En klassdefinition class A extends B {... }

Tentamen i TDP004 Objektorienterad Programmering Lösningsförslag

Kapitel 3. Synlighet. Kapitel 3 - Klassanvändning, operatorer och pekare. Synlighet

TDIU20 - Objektorienterad programmering i c++ - föreläsning 6

Grundläggande programmering, STS 1, VT Sven Sandberg. Föreläsning 14

Tillämpad programmering

TDDC76 Programmering och datastrukturer

C++ Objektorientering - Klasser. Eric Elfving

Tommy Färnqvist, IDA, Linköpings universitet

Objektorienterad programmering

Övningar Dag 2 En första klass

3. Minneshantering - (copy)konstruktorn, destruktorn och tilldelningsoperatorn

Synlighet. Namespace Scope-operatorn Klasser Vänner

Del3 Klassanvändning, operatorer och pekare Ämnesområden denna föreläsning:

Objekt och klasser - Introduktion. Objekt. SparKonto.java 2. SparKonto.java 1. Konton.java. Ett objekt har: Ett bankkonto

public och private Obs: private inte skyddar mot access från andra objekt i samma klass.

C++ Objektorientering - Klasser. Eric Elfving Institutionen för datavetenskap

Objekt och klasser - Introduktion

Malmö högskola 2007/2008 Teknik och samhälle

Exempelvis pekarvariabler blir då lättare korrekt deklarerade och initierade.

TDDC30. Objektorienterad programmering i Java, datastrukturer och algoritmer. Föreläsning 2 Jonas Lindgren, Institutionen för Datavetenskap, LiU

F8 - Arv. ID1004 Objektorienterad programmering Fredrik Kilander

UML. Klassdiagr. Abstraktion. Relationer. Överskugg. Överlagr. Aktivitetsdiagram Typomv. Typomv. Klassdiagr. Abstraktion. Relationer.

Innehåll. Typomvandlingar (casting) Implicita Typomvandlingar. Typomvandlingar (type casts) Explicita, namngivna typomvandlingar (C++-11)

Innehåll. 1 Typdeklarationer och typomvandling 2 Resurshantering. 3 Objektorientering, kort repetition. 4 Klasser

1 Klasser och objektorientering Vad är objektorientering?

Polymorfi. Objektorienterad och komponentbaserad programmering

Arv. Fundamental objekt-orienterad teknik. arv i Java modifieraren protected Lägga till och modifiera metoder med hjälp av arv Klass hierarkier

Uppgiften är att beskriva en kvadrat i ett Java program. En första version av programmet skulle kunna se ut så här:

Problemet. Vi har sett att vi kan ersätta de metoder vi ärver från överklassen med egen funktionalitet (polymorfism)

Innehåll. dynamisk bindning. och programmering CRC) u Arv, polymorfi och

Arv. Objektorienterad och komponentbaserad programmering

Del2 Klasser, medlemmar och arv Ämnesområden denna föreläsning:

Kort om klasser och objekt En introduktion till GUI-programmering i Java

Arv bakgrund (kap. 9)

TDDC76 - Programmering och Datastrukturer

Initiering och tilldelning

Inkapsling tumregler. Åtkomstmodifikatorer, instantiering, referenser, identitet och ekvivalens, samt klassvariabler. public och private

Objektorienterade programmeringsspråk. Objektorienterade språk. Den objekt-orienterade modellen. Jämför med icke-oo

TENTAMEN CD5250. Objektorienterad programutveckling med C++, 5p. Datum: , Tid: 14:00-19:00

"Är en"-relation. "Har en"-relation. Arv. Seminarium 2 Relevanta uppgifter. I exemplet Boll från förra föreläsningen gällde

Classes och Interfaces, Objects och References, Initialization

Programsystemkonstruktion med C++

1. Klass med en dynamiskt allokerad variabel, definitionsfilen-del Klass med en dynamiskt allokerad variabel, inkluderingsfilen.

Tentamen EDAF30 Programmering i C++

(Man brukar säga att) Java är... Denna föreläsning. Kompilering av Java. Historik: Java. enkelt. baserat på C/C++ Allmänt om Java

Föreläsning 4. Klass. Klassdeklaration. Klasser Och Objekt

Transkript:

LINKÖPINGS TEKNISKA HÖGSKOLA Institutionen för datavetenskap Avdelningen för Programvara och system Tommy Olsson 2013-08-12 Skapa, kopiera och destruera klassobjekt Detta dokument tar upp viktiga, grundläggande saker att tänka på då man konstruerar klasser. Initiera datamedlemmar i samma ordning som de deklareras Datamedlemmar initieras alltid i den ordning som de deklareras i klassdefinitionen. Datamedlemmar av grundläggande typ eller pekare har dock ingen defaultinitiering. Skriv alltid initierare i konstruktorer i samma ordning som medlemmarna deklareras, för att överensstämma med den ordningsom de kommer att initieras. Man undviker då irriterande varningar från kompilatorn. Exempel (felaktigt!): class Employee Employee(const string& firstname, const string& lastname) : firstname_name}, lastname_lastname}, email_firstname_ +. + lastname_ + @liu.se } } private: string email_; string firstname_; string lastname_; Eftersom email_ är deklarerad före firstname_ och lastname_ kommer email_ att initieras först och då använda de ännu ej initierade medlemmarna firstname_ och lastname_. Om definitionen för konstruktorn skrivs separat blir det ännu svårare att observera felet. Exekveringsfel kan bli följden. I C++11 kan man initiera datamedlemmar redan i deras deklaration, vilket man bör utnyttja. Om det även finns en initierare i en konstruktor är det inget problem, i så fall är det konstruktorns initiering som gäller. Detta är speciellt intressant för klasser med flera konstruktorer och olika sätt att initiera objekt. Anm. Anledningen till att designbeslutet att medlemmar initieras i deklarationsordning är att säkerställa en unik destrueringsordning. I annat fall skulle destruktorn behöva kunna destruera medlemmarna i olika ordning, beroende på vilken konstruktor som skapat ett objekt. Den extra bokföring som det skulle kräva vore inte acceptabel. Det bästa är om man kan undvika att en medlems initiering är beroende av andra medlemmars initiering. I exemplet ovan är problemet enkelt att rätta till, antingen deklareras email_ efter de andra eller så kan man använd parametrarna för att initiera email_ och, i det senare fallet, skriva initierarna i samma ordning som medlemmarna deklareras. Vid härledning gäller samma regel, basklassubobjekt initieras i samma ordning som basklasserna deklareras och basklassubobjekt initieras före de vanliga datamedlemmarna. Använd hellre initiering än tilldelning i konstruktorer Klassmedlemmar av klasstyp defaultinitieras. Om man inte initierar sådana medlemmar och använder tilldelning i stället, enligt följande class Employee Employee(const string& firstname, const string& lastname) firstname_ = firstname; lastname_ = lastname; email_ = firstname_ +. + lastname_ + @liu.se ; } 1 (6)

private: string email_; string firstname_; string lastname_; kommer defaultinitiering ändå att göras motsvarande följande: class Employee Employee(const string& firstname, const string& lastname) : email_}, firstname_}, lastname_} // Tilldelningarna enligt exemplet ovan } private: // Deklarationerna enligt exemplet ovan Medlemmarna initieras alltså av defaultkonstruktorn för string och tilldelas sedan med kopieringstilldelningsoperatorn för string. Vanligtvis behöver en tilldelningsoperator göra mer än en konstruktor, eftersom den opererar på ett redan existerande objekt. Förutom att initerare ofta ger bättre läsbarhet finns det alltså även en effektivitetsaspekt för medlemmar med defaultinitiering. Medlemmar som är konstanter eller referenser måste initieras. Det är inte tillåtet att använda tilldelning för sådana medlemmar, i analogi med att man i ett block inte kan deklarera en konstant, eller en referens, utan att initiera den och senare tilldela den ett värde. Då en konstruktor ska allokera resurser, till exempel dynamiskt minne (new), är det vanligtvis bättre eller nödvändigt att göra det i kroppen för konstruktorn. Om en resursallokering misslyckas kan till exempel andra, redan erhållna resutser, behöva återlämnas och det kan visa sig omöjligt att hantera om man allokerar resurser i samband med att initierare utförs. Basklassdestruktorer bör vara public och virtual eller protected och ej virtual När man härleder klasser är ofta syftet att använda objekten polymorft och definitivt så om det finns vanliga medlemsfunktioner som deklareras virtual. Det finns två huvudalternativ då det gäller hur en destruktor i en klass som är basklass bör deklareras. Om objekt ska kunna deallokeras via en basklasspekare måste basklassens destruktor vara public, för att det ska vara tillåtet (pekarens typ, den statiska typen, avgör), och virtual för att korrekt destruktor ska anropas initialt, dvs destruktorn för den dynamiska typen (objektets typ). class Base virtual ~Base(); class Derived : public Base ~Derived(); // Implicit virtual, liksom även den kompilatorgenererade hade varit Base* bp = new Derived; delete bp; // OK, ~Base() är public; virtual medför att ~Derived() anropas I annat fall kan basklassens destruktor vara protected och inte virtual. 2 (6)

class Base // Bör inte finnas några publika konstruktorer fristående Base-objekt kan inte destrueras protected: ~Base(); class Derived : public Base ~Derived(); Base b; Derived d; // Fel, ~Base() är inte public, objekt kan inte skapas // OK, ~Derived() är public (anropar ~Base som är protected) Om en klass är både basklass och ska kunna användas som en konkret klass för att skapa fristående objekt måste dock destruktorn vara public. En kompilatorgenererad destruktor är public och inte virtual, vilket inte passar på något av de två alternativen ovan. Det innebär att för härledda klasser måste vanligtvis en destruktor deklareras. Destruktorer ska aldrig misslyckas Destruktorer, funktioner som frigör resurser (till exempel delete) och funktioner som byter innehåll på två objekt (swap-funktioner i många fall), ska aldrig behöva misslyckas. En destruktor ska aldrig kasta undantag, noexcept. Glöm inte kopiering Det är lätt att glömma bort kopiering eftersom kompilatorn genererar kopieringskonstruktorn och kopieringstilldelningsoperatorn, om man inte deklarerar dem själv. Det finns i princip tre alternativ: Använd de kompilatorgenererade versionerna om de fungerar. Skriv både kopieringskonstruktorn och kopieringstilldelningsoperatorn om klassen ska ha kopiering men de kompilatorgenererade versionerna inte fungerar. Eliminera båda genom att deklarera dem deleted. Flyttversioner (se nedan) genereras inte. class T private: T(const T&) = delete; T& operator=(const T&) = delete; Att eliminera kopiering medför en del begränsningar, till exempel att sådana objekt inte kan lagras i en standardcontainer men det skulle man antagligen i vilket fall inte vilja. Flyttsemantik (move-semantik) Normalt bör en klass, om den har kopieringskonstruktor och kopieringstilldelningsoperator även, ha flyttkonstruktor (move-konstruktor) och flyttilldelningsoperator (move-tilldelningsoperator). T(T&&); T& operator=(t&&); // Flyttkonstruktor för T // Flyttilldelningsoperator för T En flyttkonstruktor flyttar innehållet från källobjektet till destinationsobjektet och nollställer sedan källobjektet. En flyttilldelningsoperator flyttar innehållet från högeroperanden till vänsteroperanden och nollställer sedan normalt källobjektet. 3 (6)

Kopiera och destruera konsekvent Om man definierar någon av kopieringskonstruktorn, kopieringstilldelningsoperatorn eller destruktorn för en klass behöver man antagligen definiera även de övriga, eftersom dessa tre är relaterade inbördes: om man har en egen kopieringskonstruktor eller kopieringstilldelningsoperator ska man antagligen ha även den andra, eftersom båda bör ha likartade egenskaper om man eliminerar kopieringskonstruktorn eller kopieringstilldelningsoperatorn ska man antagligen eliminera båda, eftersom kopiering med någon av dessa antagligen inte ska tillåtas om man har egna kopieringsfunktioner behöver man antagligen ha en egen destruktor om man har en egen destruktor behöver man antagligen ha egen kopiering eller eliminera kopiering tillåts kopiering bör antagligen även möjligheten att flytta objekt finnas Om kompilatorgenererade versioner fungerar är dessa att föredra framför egendefinierade, eftersom de kompilatorgenererade är triviala, vilket kan ha stor betydelse vid exempelvis optimering. Se upp med slicing Slicing av objekt uppstår när ett objekt av subklasstyp omvandlas till ett objekt av basklasstyp. Detta är en är lömsk omvandling eftersom den är osynlig och automatisk. Ett scenario kan se ut enligt följande. class Base Base(const Base&); virtual ~Base(); class Derived : public Base void fun(base b); // Kopieringskonstruktorn för Base gör kopian vid värdeöverföringen Derived d; fun(d); // Funktionen fun anropas med d, ett objekt av subtyp till Base Programmerarens avsikt är att funktionen fun() ska operera på objekt av typen Base och subtyper polymorft men glömde & efter Base i deklarationen av parametern b. När fun anropas med ett objekt av typen Derived, kommer kopieringskonstruktorn för Base att initiera b med en slajsad kopia av d. Avsikten var att en referensöverföring skulle ske, vilket inte påverkar det refererade objektet. Om man vill tillåta slicing under kontrollerade former kan man deklarera kopieringskonstruktorn explicit. Anropet av funktionen fun() ovan ger då ett kompileringsfel, eftersom användningen av kopieringskonstruktorn inte är explicit vid parameteröverföringen. Genom att deklarera kopieringskonstruktorn explicit elimineras dock all värdeöverföring, vilket bör vara förbehållet specialfall. class Base explicit Base(const Base&); void fun(const Base& rb) Base b(rb); // explicit användning av kopieringskonstruktorn för Base }. 4 (6)

Kopiering av polymorfa objekt Ett sätt att kopiera polymorfa objekt (man känner inte statiskt till typen för objekt som ska kopieras) är att använda en virtuell kopieringsfunktion, som clone() nedan. class Base virtual Base* clone() const = 0; protected: Base(const Base&); class Derived : public Base virtual Derived* clone() const override return Derived(*this); } protected: Derived(const Derived& other) : Baseother} } Det gäller att samtliga konkreta subklasser överskuggar clone() med en egen version som gör en korrekt kopia av subklassen ifråga. Märk alltid sådana överskuggningar med override, så uppmanas kompilatorn att kontrollera att det är korrekt. Alla klasser bör ha en swap-funktion om det är meningsfullt En swap-funktion byter innehåll på två objekt. Den är, förutom att byta innehåll på två objekt, även användbar för att implementera andra operationer. Deklarera en medlem och en icke-medlem, som då används i stället för std::swap. class T void swap(t& other); // ska helst inte kasta undantag swap(t& a, T& b); Ett exempel på användning av swap är för att implementera tilldelningsoperatorer: T& operator=(const T& rhs) T temprhs); // skapa en kopia händer det dåliga saker händer det här swap(temp); // och byt med *this return *this; // temp:s destruktor tar hand om skräpet } Se även nästa stycke för en variation på detta tema. 5 (6)

Egendefinierad kopieringstilldelningsoperator När man ska skriva en egen kopieringstilldelningsoperator för en typ T bör den normalt deklareras på den klassiska formen T& operator=(const T& rhs); Deklarera inte tilldelningsoperatorer virtual, det kan göra att tilldelning mellan olika typer av objekt möjliggörs. Det är i så fall bättre att ha en vanligt funktion, till exempel virtual void assign(const T&); operator= ska inte returnera const T&. Det förhindrar förvisso att skriva kod som (x = y) = z, men även att objekt av typen T kan lagras i standardcontainrar, vilka kräver att tilldelningsoperatorn returnerar T&. Gör alltid kopieringstilldelningsoperatorn felsäker och gärna enligt den starka garantin, dvs att inget minne läcker och inga objekt hamnar i ett odefinierat tillstånd. Se till att alla tilldelningsoperator är självtilldelningssäkra. Det bästa sättet att göra det är att använda idiomet skapa en temporär/kopia och byt, se ovan, det gör operatorerna både starkt felsäkra och självtilldelningssäkra. Anropa basklassers tilldelningsoperatorer uttryckligen och tilldela alla datamedlemmar. Returnera alltid *this. Undvik att anropa virtuella funktioner i konstruktorer och destruktorer Virtuella funktioner beter sig inte virtuellt i konstruktorer och destruktorer. Om en basklasskonstruktor eller -destruktor skulle vilja använda en härledd klass får man tillgripa andra tekniker, eftersom sammansatta objekt konstrueras subobjekt för subobjekt, uppifrån och ner enligt klasshierarkin. class Base Base() memfun(); } virtual void memfun(); class Derived : public Base Derived() memfun(); } virtual void memfun(); Derived d; När objektet d ovan konstrueras utförs först konstruktorn för Base. Medan det sker är den dynamiska typen för objektet Base, inte Derived (this-pekaren har typen Base* const, inte Derived* const). Ett anrop av den virtuella funktionen memfun() i konstruktorn för Base kommer alltså att anropa Base::memfun(). Efter att konstruktorn för Base är avslutad utförs konstruktorn för Derived och i dess kropp har this-pekaren nu typen Derived* const. Att det är Base::memfun() som anropas i konstruktorn för Base är bra, eftersom medlemmarna som tillhör Derived ännu inte är initierade men inte det som skulle behövas. Medan konstruktorn för Base utförs finns inget sätt att avgöra om det är ett fristående objekt som håller på att konstrueras eller om det är ett subobjekt i ett större sammansatt objekt. I vissa fall behövs så kallad post-construction, vilket innebär att en virtuell funktion anropas direkt efter att det fullständiga objektet har konstruerats. 6 (6)