Konverzní a kopírovací konstruktory, konverzní funkce

C++ Objektově orientované programování Konverzní a kopírovací konstruktory, konverzní funkce

Jednou z priorit tříd jazyka C++ je umožnit vytvářet datové typy, se kterými je možno zacházet co nejvíce jako se základními typy, tedy co nejpřirozeněji. To nám umožňuje několik různých technik, mezi než patří konverzní a kopírovací konstruktory a konverzní funkce. Řekněme, že chceme vytvořit vlastní třídu String pro práci s textovými řetězci. Tato třída bude obsahovat pointer na pole znaků, které bude reprezentovat náš řetězec, bezparametrický konstruktor, který pouze alokuje paměť, destruktor pro dealokaci paměti, metodu Set pro nastavení řetězce, metodu Get pro získání řetězce a metodu Index pro přístup k jednotlivým znakům řetězce. Můžeme implementovat i metodu pro zjištění délky řetězce. Naše třída bude prozatím vypadat nějak takhle:

class String
{
  private:
    char* str;

  public:
    String(){str = new char[1]; str[0] = '\0';}  //bezparametrický konstruktor alokující paměť pro jeden prvek typu char
    ~String(){delete [] str;}  //destruktor dealokující paměť
    void Set(char*);  //metoda pro nastavení řetězce
    char* Get(){return str;}  //metoda pro získání řetězce
    char & Index(int i){return str[i];}  //metoda vracející referenci na konkrétní znak řetězce
    int GetLength(){return strlen(str);}  //metoda vracející délku řetězce pomocí standardní funkce strlen
};

void String::Set(char* s)
{
  delete [] str;  //nejdříve musíme dealokovat paměť, na které je náš aktuální řetětzec
  int len =  strlen(s);  //pro zjištění délky použijeme standardní funkci strlen
  str = new char[len + 1];  //následně musíme alokovat novou paměť pro nový řetězec, nesmíme zapomenout přidat jeden prvek pro nulový znak
  for(int i = 0; i < len + 1; i++)  //pak stačí překopírovat jednotlivé znaky do našeho nového řetězce
    str[i] = s[i];
}

...

String str;  //deklarace objektu typu String
str.Set("Hello world!");  //nastavení řetězce na "Hello world!"
cout<<str.Get();  //vypsání řetězce
cout<<str.Index(4);  //vypsání 5. znaku, tedy 'o'
cout<<str.GetLength();  //vypsání délky řetězce
str.Index(5) = '-';  //změna 6. znaku, tedy mezery, na pomlčku

Kopírovací konstruktor

Naše třída String je nyní ve stavu použitelnosti, ovšem co kdybychom chtěli inicializovat objekt naší třídy String, třeba v parametru nějaké funkce nebo uložit objekt této třídy do nějaké kolekce. Uvažujme následující dva případy:

void function(String s){...}  //funkce přebírající parametr typu String
...
String jmeno;  //deklarace Stringu
str.Set("Hello!");  //nastavení na "Hello!"
...
String str = jmeno;  //první případ - inicializace deklarovaného objektu objektem jmeno
function(jmeno);  //druhý případ - inicializace objektu s v parametru funkce

Toto samozřejmě bude nějak fungovat, ovšem ne tak, jak bychom potřebovali. O inicializaci objektu objektem té samé třídy se stará takzvaný kopírovací konstruktor. Je to jednoduše konstruktor, který přebírá pouze jeden parametr, a to referenci na konstantní objekt vlastní třídy, tedy const type &, kde type je název naší třídy. V tomto případě kopírovací konstruktor vypadá takto: String(const String &). Tento konstruktor se zavolá vždy při inicializování nově vytvořeného objektu nějakým jiným objektem té samé třídy a je jedno jestli se jedná o inicializaci operátorem = při deklaraci, inicializaci při předávání objektu funkci nebo inicializaci při vytváření objektu operátorem new. Kopírovací konstruktor je implicitně nastaven tak, že provádí mělkou kopii objektu. To znamená, že pouze překopíruje jednotlivé datové položky třídy. V naší třídě tedy pouze kopíruje pointer str objektu, kterým inicializujeme do objektu inicializovaného. Pokud tedy v našem kódu předáme objekt jmeno funkci function, pointer v objektu, který má funkce v parametru, bude ukazovat na ten samý řetězec v paměti. To nemůžeme dopustit, protože ve chvíli kdy tato funkce skončí, zavolá se destruktor tohoto parametrového objektu a dealokuje nám paměť, kde je náš řetězec. Na tento řetězec ale pořád ukazuje pointer v objektu jmeno, který jsme funkci předali. Pointer v objektu jmeno, tedy náhle ukazuje na neplatná data. Toto chování naštěstí můžeme změnit, a to tak, že jednoduše definujeme vlastní kopírovací konstruktor. Mezi veřejné položky třídy pouze přidáme tento řádek:

String(const String &);  //deklarace kopírovacího konstruktoru

Pak musíme konstruktor definovat tak, aby dělal přesně to co chceme. V našem případě pravděpodobně budeme chtít, aby v nově vytvořeném objektu přiřadil pointeru str dostatek paměti pro nový řetězec a následně řetězec do této paměti překopíroval.

String::String(const String & s)
{
  int len = strlen(s.str);  //zjištění délky řetězce v objektu s
  str = new char[len + 1];  //alokace paměti
  for(int i = 0; i < len + 1; i++)  //překopírování znaků z řetězce v objketu s, do pole na které ukazuje str
    str[i] = s.str[i];
}

Program od teď bude při inicializaci vždy volat tento konstruktor a jako parametr mu předá objekt, kterým inicializujeme. Pokud nyní provedeme toto...

String jmeno;
str.Set("Hello!");
...
String str = jmeno;
function(jmeno);

...všechno bude v pořádku. Do nových objektů se nezkopíruje pouze pointer, ale vytvoří se nový řetězec v nově alokovaném bloku paměti.

Konverzní konstruktory

Někdy může být vhodné, aby objekt bylo možné inicializovat nejen objektem té samé třídy, ale i nějakým jiným datovým typem. Například je dosti žádoucí, abychom objekty naší třidy String mohli inicializovat řetězcovou konstantou nebo řetězcem ve stylu C. Tedy potřebujeme, aby bylo možné objekt inicializovat proměnnou typu char*. K tomu poslouží takzvaný konverzní konstruktor. Ten se vytváří stejně jako konstruktor kopírovací, jen jako parametr musí přebírat proměnnou daného typu, pro nějž je konverze určena. Tedy pokud chceme, abychom mohli inicializovat objekt třídy String proměnnou typu char*, přidáme mezi veřejné položky třídy tento konstruktor:

String(char*); //deklarace konverzního konstruktoru pro typ char*

Konstruktor definujeme tak, aby řetězec překopíroval do nově vytvořeného objektu.

String::String(char* s)
{
  int len = strlen(s);  //zjištění délky řetězce
  str = new char[len + 1];  //alokace paměti
  for(int i = 0; i < len + 1; i++)  //překopírování řetězce
    str[i] = s[i];
}

Nyní můžeme bez problému použít následující zápisy:

void function(String s){...}
...
char array_str[] = "Hello!"  //řetězec jako pole znaků
char* ptr_str = "Hi!";  //to samé, pouze pomocí pointeru

String s1 = array_str;  //možné, array_str je bráno jako char*
String s2 = ptr_str  //také možné
String s3 = "Hello world!";  //také možné, řetězcová konstanta vrací char*
function(array_str);
function(ptr_str);          //toto všechno bude také fungovat
function("Hello world!");

cout<<s3.Get();  //můžeme otestovat

Konverzních konstruktorů můžeme mít libovolné množství, můžeme si udělat třeba konstruktory pro typy int a double, abychom mohli vkládat číselné hodnoty. Pokud chceme, aby konstruktor nemohl být použit pro konverzi, napíšeme před něj v deklaraci klíčové slovo explicit.

explicit String(int);
...
void function(String s){...}
...
function(10);  //toto nebude možné

Konverzní funkce

Konverzní funkce slouží k přetypování objektu třídy na libovolný datový typ. V našem případě se opět nabízí typ char*, abychom mohli například objekty třídy String dosadit do funkce přebírající parametr typu char*. Konverzní funkci deklarujeme mezi veřejnými položkami třídy takto:

operator char*();

Klíčové slovo operator určuje, že jde o konverzní funkci, char* je zde vlastně návratový typ. Mohli bychom funkci definovat rovnou při deklaraci, ukáži ale, jak definovat takovouto funkci mimo třídu. Stačí, když funkce vrátí pointer na náš řetězec v paměti.

String::operator char*()
{
  return str;
}

Nyní můžeme jakýkoliv objekt třídy String přetypovat na char*.

void function(char* s){...}  //funkce přebírající parametr typu char*
...
String str = "Hello world!";
function(str);  //toto bude fungovat, funkce dostane v parametru pointer na řetězec v objektu str
cout<<str;  //můžeme objekt i takto vypsat, přetypuje se na char*

Pokud máme konverzních funkcí více, může být někdy konverze nejednoznačná. Pak musíme použít explicitní přetypování. Například, když přidáme ještě takovouto konverzní funkci:

operator int(){...}  //nezáleží na tom, co funkce dělá

Příkaz cout<<str nebude fungovat, kompilátor neví, jestli má str přetypovat na int nebo na char*. Musíme mu to tedy říci.

cout<<(char*)str;

Metodu Get můžeme z třídy klidně odstranit. Aktuální třídu String přikládám jako zdrojový kód. Berte na vědomí, že blok třídy by měl být v hlavičkovém souboru oddělen od definic metod, ale to pro tento příklad není podstatné. Do funkce main si můžete napsat libovolný kód.


 

Stáhnout

Staženo 224x (937 B)
Aplikace je včetně zdrojových kódů v jazyce C++

 

  Aktivity (1)

Článek pro vás napsal Lukáš Hruda (Luckin)
Avatar
...

Jak se ti líbí článek?
Celkem (1 hlasů) :
55555


 


Miniatura
Následující článek
Šablony funkcí

 

 

Komentáře

Avatar
michal
Redaktor
Avatar
michal:

to se bude hodit do jednoho programu :)

 
Odpovědět 3.7.2013 11:58
Avatar
Odpovídá na michal
Lukáš Hruda (Luckin):

Tohle se hodí do hodně programů :D Ještě tu pak chci popsat přetěžování operátorů, což na tuhle kapitolu víceméně navazuje a tyhle možnosti ještě dost rozšiřuje. Teď ale nějak nebyl čas, snad se k tomu dostanu.

 
Odpovědět 3.7.2013 12:10
Avatar
michal
Redaktor
Avatar
michal:

to jo ale nemohl sem to najít nikde :D

 
Odpovědět 3.7.2013 12:11
Avatar
radian1
Člen
Avatar
Odpovídá na michal
radian1:

LOL michal, copak ty neznáš string (<cstring>)? Tohleto je hoodně ořezaný odvar té třídy.

 
Odpovědět 4.1.2014 20:39
Avatar
Samuel Bachar:

Čauko .

Mám otázku na Kopirovací konstruktor .
Nemám predstavu ako mám vykonať inicializáciu objektu v parametru funkce .

function(jmeno); //druhý případ - inicializace objektu s v parametru funkce

Skúšal som rôzne spôsoby ale zatial mi ani jeden nevyšiel niekde robim trivialnu chybu .

 
Odpovědět 21.10.2015 16:23
Avatar
Lukáš Hruda (Luckin):

Objekt uvnitř té funkce je jenom kopie toho objektu str, který jsi deklaroval v main. Cokoliv s ním uvnitř té funkce uděláš se v tom původním objektu v main nijak neprojeví. Proto se tomu říká kopírovací konstruktor, protože provádí kopii.

 
Odpovědět 21.10.2015 22:41
Avatar
Milan Křepelka
Redaktor
Avatar
Milan Křepelka:

Jen tak jsem zabruslil. C++ už jsem delší dobu zapudil. Nicméně opravdu je potřeba takhle kopírovat znak po znaku. Jestli si pamatuju dobře, tak C(++) mají poměrně silné funkce pro práce s pamětí( memcpy?). Možná by byly lehce efektivnejší.

 
Odpovědět 21.10.2015 23:28
Avatar
Martin Dráb
Redaktor
Avatar
Odpovídá na Milan Křepelka
Martin Dráb:

Ano, memcpy (strcpy) by tady byla určitě vhodnější. Některé předkladače dokážou takovýhle cyklus na memcpy převést v rámci optimalizací (Intel), ale i tak bychom na nich všechno nechávat nemuseli (podobně memset()).

Ono ta třída String má trochu více problémů, takže je skutečně jen ukázková. Třea taková metoda Get() je dost nebezpečná, protože ukazatel jí vrácený se stává neplatný po volání metody Set(), případně délka řetězce se počítá pokaždé při volání GetLength().

Odpovědět 22.10.2015 0:00
2 + 2 = 5 for extremely large values of 2
Avatar
 
Odpovědět 22.10.2015 13:04
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 9 zpráv z 9.