// repàs de classes amb Qt, sense finestres

Classes en C++, amb Qt al terminal

Objectes, constructors i destructors, pila i munt, herència i composició, i un primer QObject: tot el que fa falta per llegir sense por el codi d'un giny de Qt.

El que escrius (main.cpp)
#include <QCoreApplication>
#include <QDebug>
#include "nas.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Nas n1, n2;
    Nas *p;

    n1.llargada = 23;
    n2.llargada = 15;
    p = new Nas;
    p->llargada = 60;

    qDebug() << "n1:" << n1.llargada << "mm";
    qDebug() << "n2:" << n2.llargada << "mm";
    qDebug() << "p:" << p->llargada << "mm";
    delete p;
    qDebug() << "Fi de main";

    return 0;
}
El que veus al terminal
Nas creat: 40 mm
Nas creat: 40 mm
Nas creat: 40 mm
n1: 23 mm
n2: 15 mm
p: 60 mm
Nas destruït: 60 mm
Fi de main
Nas destruït: 15 mm
Nas destruït: 23 mm

Un projecte de consola

Aquí no hi ha finestres, ni Designer, ni QApplication: només el mòdul Core de Qt i el terminal. Així es veu només el llenguatge (classes, memòria, herència) sense res que el tapi. Tot el que aprenguis aquí és el que suposa la pàgina introQt.html.

El primer programa

main.cpp
#include <QCoreApplication>
#include <QDebug>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QString missatge = "Hola, DAM 2";

    qDebug() << missatge;

    return 0;
}
El que escriu el terminal
"Hola, DAM 2"
  • QCoreApplication és la versió sense finestres de QApplication. Viu al mòdul Core.
  • QString és la cadena de Qt (amb suport d'accents, Unicode i moltes funcions).
  • qDebug() escriu al terminal, i és el que farem servir per veure què passa.
  • No hi ha a.exec(): aquest programa fa la seva feina i acaba. Amb exec() es quedaria esperant esdeveniments, com faran els programes amb finestres d'introQt.html; aquí no fa falta.

El CMakeLists.txt d'un projecte de consola

cmake_minimum_required(VERSION 3.19)
project(nas LANGUAGES CXX)

find_package(Qt6 6.5 REQUIRED COMPONENTS Core)

qt_standard_project_setup()

qt_add_executable(nas
    main.cpp
    nas.cpp
    nas.h
)

target_link_libraries(nas PRIVATE Qt::Core)
  • Només Core: no hi ha Widgets. Els projectes antics amb .pro (qmake) escrivien QT -= gui per dir el mateix.
  • Cada classe nova són dos fitxers (nas.cpp i nas.h) que s'han de llistar a qt_add_executable. Si t'oblides del .cpp, el projecte compila i falla en enllaçar (mira la taula d'errors).
  • qt_standard_project_setup() només és imprescindible quan hi ha classes amb Q_OBJECT (l'últim bloc): activa moc.

qDebug o QTextStream?

Va a…TextQuan
qDebug() << sstderruna QString surt entre cometesper depurar
qDebug().noquote() << sstderrsense cometesper depurar, més net
QTextStream out(stdout)stdouttal qualquan el programa ha d'escriure resultats

Ho veuràs al pas a pas «Hola, terminal»: la sortida estàndard es pot redirigir (> fitxer) o encadenar (|), i els missatges de depuració, no. Al Creator els dos surten a Application Output.

Classe i objecte

Una classe és el disseny (com és qualsevol nas); un objecte és un exemplar concret (el nas de la Marta). Cada objecte té la seva pròpia còpia dels atributs. Farem servir una classe Nas que anirà creixent al llarg de la pàgina.

Una classe amb un atribut

main.cpp
#include <QCoreApplication>
#include <QDebug>
#include "nas.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Nas n1, n2;

    n1.llargada = 23;
    n2.llargada = 15;

    qDebug() << "n1:" << n1.llargada << "mm";
    qDebug() << "n2:" << n2.llargada << "mm";

    return 0;
}
El que escriu el terminal
n1: 23 mm
n2: 15 mm
nas.h
#ifndef NAS_H
#define NAS_H

class Nas
{
public:
    int llargada;    // atribut (variable membre), en mm
};

#endif // NAS_H
  • Nas n1, n2; crea dos objectes. Cadascun té el seu llargada: canviar el de n1 no toca el de n2.
  • public: vol dir que qualsevol pot llegir i escriure l'atribut amb objecte.atribut. Al bloc «private i const» ho tancarem.
  • Sense constructor, l'atribut val qualsevol cosa fins que se li assigni un valor (mira la primera línia del pas a pas: llargada = ?).

Capçalera i implementació

Cada classe es reparteix en dos fitxers: el .h diu què hi ha (la declaració) i el .cpp diu com funciona (la definició). Els dos porten el nom de la classe en minúscula.

main.cpp
#include <QCoreApplication>
#include <QDebug>
#include "nas.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Nas n1, n2, n3;

    n1.llargada = 23;
    n2.llargada = 15;

    qDebug() << "n1:" << n1.llargada << "mm";
    qDebug() << "n2:" << n2.llargada << "mm";
    qDebug() << "n3:" << n3.llargada << "mm";

    return 0;
}
El que escriu el terminal
n1: 23 mm
n2: 15 mm
n3: 40 mm
nas.h
#ifndef NAS_H
#define NAS_H

class Nas
{
public:
    Nas();           // constructor: mateix nom que la classe, sense tipus de retorn

    int llargada;    // atribut (variable membre), en mm
};

#endif // NAS_H
nas.cpp
#include "nas.h"

Nas::Nas()
{
    llargada = 40;   // valor inicial de qualsevol Nas nou
}
  • Les tres línies #ifndef … #define … #endif són les include guards: fan que la capçalera es llegeixi només una vegada, encara que arribi per camins diferents. Sense elles, moltes classes no compilen (mira Si surt un error).
  • Nas::Nas() defineix el constructor: una funció amb el nom de la classe i sense tipus de retorn, que C++ crida sola cada cop que es crea un objecte. El Nas:: davant vol dir «de la classe Nas».

Pila, munt i destructor

Un objecte pot viure en dos llocs, i de quin dels dos sigui depèn de qui l'ha de destruir. És el bloc més important de la pàgina: és el que et cal per entendre per què un giny de Qt fa new i delete.

Pila (stack)Munt (heap)
Com es creaNas n(51);Nas *p = new Nas(96);
Com s'accedeixn.llargadap->llargada (amb un apuntador, ->)
Quan es destrueixsol, quan s'acaba el blocnomés amb delete p;
Si t'oblides…fuita de memòria, sense cap missatge d'error

Un apuntador és una variable que guarda l'adreça d'un objecte, no l'objecte. Per això, al pas a pas, els objectes del munt surten a la columna de la dreta i el apuntador de la pila diu → @1.

El destructor, i qui es destrueix quan

main.cpp
#include <QCoreApplication>
#include <QDebug>
#include "nas.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Nas n1, n2;
    Nas *p;

    n1.llargada = 23;
    n2.llargada = 15;
    p = new Nas;
    p->llargada = 60;

    qDebug() << "n1:" << n1.llargada << "mm";
    qDebug() << "n2:" << n2.llargada << "mm";
    qDebug() << "p:" << p->llargada << "mm";
    delete p;
    qDebug() << "Fi de main";

    return 0;
}
El que escriu el terminal
Nas creat: 40 mm
Nas creat: 40 mm
Nas creat: 40 mm
n1: 23 mm
n2: 15 mm
p: 60 mm
Nas destruït: 60 mm
Fi de main
Nas destruït: 15 mm
Nas destruït: 23 mm
nas.h
#ifndef NAS_H
#define NAS_H

class Nas
{
public:
    Nas();           // constructor
    ~Nas();          // destructor: es crida quan l'objecte desapareix

    int llargada;
};

#endif // NAS_H
nas.cpp
#include "nas.h"

#include <QDebug>

Nas::Nas()
{
    llargada = 40;
    qDebug() << "Nas creat:" << llargada << "mm";
}

Nas::~Nas()
{
    qDebug() << "Nas destruït:" << llargada << "mm";
}
  • El destructor (~Nas(), declarat a nas.h i definit a nas.cpp) és el contrari del constructor: C++ el crida quan l'objecte desapareix. Aquí només escriu un missatge, però és on s'alliberen els recursos.
  • Fixa't en l'ordre del terminal: el delete p; de main.cpp s'executa a l'instant (surt «Nas destruït: 60 mm» abans de «Fi de main»), però els objectes de la pila, n1 i n2, es destrueixen a l'acabar main, en ordre invers al de creació (primer n2, després n1).
  • Si esborres la línia delete p; de main.cpp, el «Nas destruït: 60 mm» no surt mai: és una fuita de memòria. Al pas a pas «Destructor, new i delete», tria delete p: no i ho veuràs.

Constructors sobrecarregats i llista d'inicialització

main.cpp
#include <QCoreApplication>
#include <QDebug>
#include "nas.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Nas n1, n2(51);
    Nas *p;

    p = new Nas(96);

    qDebug() << "n1:" << n1.llargada << "mm";
    qDebug() << "n2:" << n2.llargada << "mm";
    qDebug() << "p:" << p->llargada << "mm";
    delete p;

    return 0;
}
El que escriu el terminal
Nas creat: 40 mm
Nas creat: 51 mm
Nas creat: 96 mm
n1: 40 mm
n2: 51 mm
p: 96 mm
Nas destruït: 96 mm
Nas destruït: 51 mm
Nas destruït: 40 mm
nas.h
#ifndef NAS_H
#define NAS_H

class Nas
{
public:
    Nas();           // constructor per defecte
    Nas(int mm);     // constructor sobrecarregat: rep la llargada
    ~Nas();

    int llargada;
};

#endif // NAS_H
nas.cpp
#include "nas.h"

#include <QDebug>

Nas::Nas() : llargada(40)
{
    qDebug() << "Nas creat:" << llargada << "mm";
}

Nas::Nas(int mm) : llargada(mm)
{
    qDebug() << "Nas creat:" << llargada << "mm";
}

Nas::~Nas()
{
    qDebug() << "Nas destruït:" << llargada << "mm";
}
  • Una classe pot tenir diversos constructors amb el mateix nom i arguments diferents (sobrecàrrega). C++ tria el que encaixa amb el que escrius: Nas n1; o Nas n2(51);.
  • : llargada(40) és la llista d'inicialització: dona el valor a l'atribut abans d'entrar al cos de la funció. És la forma preferida, i la que fa servir el Creator a tots els seus constructors.
  • Una alternativa és un sol constructor amb un valor per defecte: Nas(int mm = 40);.

Per què això et servirà a Qt

Quan facis un projecte amb finestres, el Creator et generarà una classe amb un atribut Ui::Marcador *ui;: al constructor fa ui(new Ui::Marcador) i al destructor delete ui;. Exactament el mateix patró d'aquest bloc: un objecte creat amb new que algú ha de destruir. Si t'oblidessis del delete ui, ningú et diria res.

private, getters i const

Deixar els atributs public és còmode i perillós: qualsevol línia del programa els pot deixar en un estat impossible. La solució és amagar-los i obrir-hi només una porta que vigili.

Getter i setter

main.cpp
#include <QCoreApplication>
#include <QDebug>
#include "nas.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Nas n1, n2(60);

    n1.setLlargada(150);

    qDebug() << "n1:" << n1.llargada() << "mm";
    qDebug() << "n2:" << n2.llargada() << "mm";

    return 0;
}
El que escriu el terminal
Nas creat: 40 mm
Nas creat: 60 mm
n1: 100 mm
n2: 60 mm
Nas destruït: 60 mm
Nas destruït: 100 mm
nas.h
#ifndef NAS_H
#define NAS_H

class Nas
{
public:
    Nas();
    Nas(int mm);
    ~Nas();

    int llargada() const;      // getter: només llegeix
    void setLlargada(int mm);       // setter: comprova i modifica

private:
    int m_llargada;                 // ja no es pot tocar des de fora
};

#endif // NAS_H
nas.cpp
#include "nas.h"

#include <QDebug>

Nas::Nas() : m_llargada(40)
{
    qDebug() << "Nas creat:" << m_llargada << "mm";
}

Nas::Nas(int mm) : m_llargada(0)
{
    setLlargada(mm);
    qDebug() << "Nas creat:" << m_llargada << "mm";
}

Nas::~Nas()
{
    qDebug() << "Nas destruït:" << m_llargada << "mm";
}

int Nas::llargada() const
{
    return m_llargada;
}

void Nas::setLlargada(int mm)
{
    if (mm < 0)
        mm = 0;
    if (mm > 100)
        mm = 100;
    m_llargada = mm;
}
  • private vol dir que només la pròpia classe hi pot accedir. Des de main, n1.m_llargada = 5; ja no compila (mira la taula d'errors).
  • El getter llegeix; el setter escriu i pot validar: aquí, la llargada es queda entre 0 i 100. A main.cpp es demana setLlargada(150) i el terminal mostra n1: 100 mm: el setter ho ha limitat. Sense aquesta porta, ningú no ho podria garantir.
  • El constructor amb argument reutilitza el setter, perquè la comprovació sigui la mateixa.
  • Noms com els de Qt: el getter porta el nom de la propietat (llargada()), el setter porta set davant (setLlargada(int)), i l'atribut s'amaga darrere d'un prefix (m_llargada). Qt fa exactament això (value() / setValue()), i és el que veuràs a introQt.

const: les promeses del compilador

La paraula const surt en dos llocs diferents d'aquest exemple, i vol dir dues coses relacionades:

main.cpp
#include <QCoreApplication>
#include <QDebug>
#include "nas.h"

void mostra(const Nas &n)
{
    qDebug() << "Es mostra un nas de" << n.llargada() << "mm";
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Nas n(60);

    mostra(n);
    qDebug() << "Fi de main";

    return 0;
}
El que escriu el terminal
Nas creat: 60 mm
Es mostra un nas de 60 mm
Fi de main
Nas destruït: 60 mm
nas.h
#ifndef NAS_H
#define NAS_H

class Nas
{
public:
    Nas();
    Nas(int mm);
    ~Nas();

    int llargada() const;      // getter: només llegeix
    void setLlargada(int mm);       // setter: comprova i modifica

private:
    int m_llargada;                 // ja no es pot tocar des de fora
};

#endif // NAS_H
Altres fitxers del projecte: nas.cpp
nas.cpp
#include "nas.h"

#include <QDebug>

Nas::Nas() : m_llargada(40)
{
    qDebug() << "Nas creat:" << m_llargada << "mm";
}

Nas::Nas(int mm) : m_llargada(0)
{
    setLlargada(mm);
    qDebug() << "Nas creat:" << m_llargada << "mm";
}

Nas::~Nas()
{
    qDebug() << "Nas destruït:" << m_llargada << "mm";
}

int Nas::llargada() const
{
    return m_llargada;
}

void Nas::setLlargada(int mm)
{
    if (mm < 0)
        mm = 0;
    if (mm > 100)
        mm = 100;
    m_llargada = mm;
}
  • Al final d'una funció membre (int llargada() const;, a nas.h, i int Nas::llargada() const a nas.cpp): és una promesa que aquesta funció no modifica l'objecte. El compilador la vigila: si dins de llargada() escrius m_llargada = 0;, no compila. Cal escriure-la igual al .h i al .cpp. Un getter sempre l'ha de portar.
  • A un paràmetre (const Nas &n, a la funció mostra de main.cpp): el & és una referència (el mateix objecte, sense fer-ne una còpia) i el const vol dir que mostra no el podrà modificar.
  • Què s'hi pot cridar, doncs? Si el paràmetre és const, només se li poden cridar les funcions que també prometen no modificar-lo: les funcions membre const. A l'exemple, mostra crida n.llargada(), que porta const a nas.h: per això compila. n.setLlargada(5) no compilaria, perquè el setter sí que modifica.
  • Si al getter li treus el const, el compilador diu error: passing 'const Nas' as 'this' argument discards qualifiers: «estàs cridant una funció que no promet respectar l'objecte sobre un objecte que has jurat no tocar». Ho pots provar al pas a pas «const i passar objectes», amb getter: sense const.
  • Si passes l'objecte per valor (Nas n), C++ en fa una còpia: la còpia es destrueix al final de la funció, i el seu destructor sí que escriu «Nas destruït». Al pas a pas, amb mostra(: per valor, hi surt un «destruït» sense el seu «creat». Per això a Qt tot es passa com a const QString &.

Herència

Una classe filla és una classe base amb més coses. Un nas és una part de la cara, i una boca també: les dues tenen un nom, i cadascuna té els seus atributs propis. La base guarda el que tenen en comú.

PartCara és la base; Nas i Boca hi hereten

main.cpp
#include <QCoreApplication>
#include <QDebug>
#include "nas.h"
#include "boca.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Nas n(60);
    Boca b(45);

    qDebug().noquote() << n.nom() << "i" << b.nom();

    return 0;
}
El que escriu el terminal
PartCara creada: nas
Nas creat: 60 mm
PartCara creada: boca
Boca creada: 45 mm
nas i boca
Boca destruïda: 45 mm
PartCara destruïda: boca
Nas destruït: 60 mm
PartCara destruïda: nas
partcara.h
#ifndef PARTCARA_H
#define PARTCARA_H

#include <QString>

class PartCara
{
public:
    PartCara(const QString &nom);
    ~PartCara();
    QString nom() const;

protected:
    QString m_nom;     // protected: els fills el poden veure, el món de fora no
};

#endif // PARTCARA_H
partcara.cpp
#include "partcara.h"

#include <QDebug>

PartCara::PartCara(const QString &nom) : m_nom(nom)
{
    qDebug().noquote() << "PartCara creada:" << m_nom;
}

PartCara::~PartCara()
{
    qDebug().noquote() << "PartCara destruïda:" << m_nom;
}

QString PartCara::nom() const
{
    return m_nom;
}
nas.h
#ifndef NAS_H
#define NAS_H

#include "partcara.h"

class Nas : public PartCara
{
public:
    Nas(int mm);
    ~Nas();

private:
    int m_llargada;
};

#endif // NAS_H
nas.cpp
#include "nas.h"

#include <QDebug>

Nas::Nas(int mm) : PartCara("nas"), m_llargada(mm)
{
    qDebug() << "Nas creat:" << m_llargada << "mm";
}

Nas::~Nas()
{
    qDebug() << "Nas destruït:" << m_llargada << "mm";
}
Altres fitxers del projecte: boca.h, boca.cpp
boca.h
#ifndef BOCA_H
#define BOCA_H

#include "partcara.h"

class Boca : public PartCara
{
public:
    Boca(int mm);
    ~Boca();

private:
    int m_amplada;
};

#endif // BOCA_H
boca.cpp
#include "boca.h"

#include <QDebug>

Boca::Boca(int mm) : PartCara("boca"), m_amplada(mm)
{
    qDebug() << "Boca creada:" << m_amplada << "mm";
}

Boca::~Boca()
{
    qDebug() << "Boca destruïda:" << m_amplada << "mm";
}
  • class Nas : public PartCara (a nas.h): Nas hereta tot el que és públic i protected de PartCara.
  • protected és a mig camí entre public i private: el veuen la classe i les seves filles, però no el món de fora. Per això main.cpp no pot fer n.m_nom, i necessita el getter nom().
  • La base es construeix sempre primer. La llista d'inicialització de Nas comença per PartCara("nas"): així el constructor de la base rep el nom. Destrucció: el contrari, primer el fill i després la base.
  • Què fa el const de QString PartCara::nom() const? És el mateix const de la promesa del bloc anterior: nom() només llegeix m_nom i no modifica l'objecte. Com a getter, sempre el porta. El const es repeteix a partcara.h (QString nom() const;) i a partcara.cpp. Que hi sigui permet cridar nom() també sobre un objecte const (per exemple, a través d'un const PartCara &), i la funció retorna una còpia de la QString (barata: Qt no copia les dades fins que es modifiquen).

virtual i override

El problema. Un apuntador a la base (PartCara *) pot apuntar a un Nas o a una Boca. Quan crides parts[i]->descripcio(), quina versió s'ha d'executar: la de PartCara (pel tipus del apuntador) o la del Nas o la Boca (pel tipus de l'objecte que hi ha darrere)? Per defecte, C++ tria pel tipus del apuntador. Amb virtual, tria pel tipus de l'objecte real.

main.cpp
#include <QCoreApplication>
#include <QDebug>
#include "nas.h"
#include "boca.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    PartCara *parts[2];
    int i;

    parts[0] = new Nas(60);
    parts[1] = new Boca(45);

    for (i = 0; i < 2; i++)
        qDebug().noquote() << parts[i]->descripcio();

    for (i = 0; i < 2; i++)
        delete parts[i];

    return 0;
}
El que escriu el terminal
PartCara creada: nas
Nas creat: 60 mm
PartCara creada: boca
Boca creada: 45 mm
nas de 60 mm
boca de 45 mm d'amplada
Nas destruït: 60 mm
PartCara destruïda: nas
Boca destruïda: 45 mm
PartCara destruïda: boca
partcara.h
#ifndef PARTCARA_H
#define PARTCARA_H

#include <QString>

class PartCara
{
public:
    PartCara(const QString &nom);
    virtual ~PartCara();
    virtual QString descripcio() const;
    QString nom() const;

protected:
    QString m_nom;     // protected: els fills el poden veure, el món de fora no
};

#endif // PARTCARA_H
nas.h
#ifndef NAS_H
#define NAS_H

#include "partcara.h"

class Nas : public PartCara
{
public:
    Nas(int mm);
    ~Nas();
    QString descripcio() const override;

private:
    int m_llargada;
};

#endif // NAS_H
Altres fitxers del projecte: partcara.cpp, nas.cpp, boca.h, boca.cpp
partcara.cpp
#include "partcara.h"

#include <QDebug>

PartCara::PartCara(const QString &nom) : m_nom(nom)
{
    qDebug().noquote() << "PartCara creada:" << m_nom;
}

PartCara::~PartCara()
{
    qDebug().noquote() << "PartCara destruïda:" << m_nom;
}

QString PartCara::descripcio() const
{
    return "part de la cara";
}

QString PartCara::nom() const
{
    return m_nom;
}
nas.cpp
#include "nas.h"

#include <QDebug>

Nas::Nas(int mm) : PartCara("nas"), m_llargada(mm)
{
    qDebug() << "Nas creat:" << m_llargada << "mm";
}

Nas::~Nas()
{
    qDebug() << "Nas destruït:" << m_llargada << "mm";
}

QString Nas::descripcio() const
{
    return "nas de " + QString::number(m_llargada) + " mm";
}
boca.h
#ifndef BOCA_H
#define BOCA_H

#include "partcara.h"

class Boca : public PartCara
{
public:
    Boca(int mm);
    ~Boca();
    QString descripcio() const override;

private:
    int m_amplada;
};

#endif // BOCA_H
boca.cpp
#include "boca.h"

#include <QDebug>

Boca::Boca(int mm) : PartCara("boca"), m_amplada(mm)
{
    qDebug() << "Boca creada:" << m_amplada << "mm";
}

Boca::~Boca()
{
    qDebug() << "Boca destruïda:" << m_amplada << "mm";
}

QString Boca::descripcio() const
{
    return "boca de " + QString::number(m_amplada) + " mm d'amplada";
}

Les tres línies clau, on són i què fan:

FitxerLíniaQuè vol dir
partcara.hvirtual QString descripcio() const;A la base. virtual: «aquesta funció es pot substituir a les filles, i es triarà per l'objecte real». const: la promesa de no modificar l'objecte (com a nom()). Són dues coses independents: virtual decideix quina versió es crida; const decideix què pot fer la funció.
nas.hQString descripcio() const override;A la filla. override: «aquesta funció substitueix la virtual de la base». El compilador comprova que a la base n'hi hagi una exactament igual: mateix nom, mateixos arguments i mateix const. No cal repetir virtual: la funció ja és virtual per herència. Si el const no coincideix, o no hi ha cap virtual a la base, l'override dona un error (mira a sota).
partcara.hvirtual ~PartCara();El destructor de la base és virtual. Sense això, delete parts[i] (que és un PartCara *) només cridaria ~PartCara(), i ~Nas() no s'executaria mai: allò que haguessis de tancar o alliberar al Nas es perdria, sense cap avís. Regla: si una classe té alguna funció virtual, el destructor també ho ha de ser. Al Nas, ~Nas() no porta virtual ni override: ja n'hereta el caràcter virtual.
Què canviesQuè surt
virtual a la base i override a les fillesnas de 60 mm i boca de 45 mm d'amplada
sense virtual ni overridepart de la cara dues vegades: C++ tria pel tipus del apuntador
sense virtual a la base, amb override a la fillano compila: marked 'override', but does not override
virtual QString descripcio() const a la base i QString descripcio() override a la filla (sense const)no compila: error: 'QString Nas::descripcio()' marked 'override', but does not override
  • override és opcional, però molt recomanable. Sense ell, una errada al nom, als arguments o al const no donaria cap error: la filla tindria una funció nova amb un nom semblant, i la base seguiria cridant la seva. Amb override, el compilador t'ho diu.
  • Prova-ho. Al pas a pas «virtual i override», canvia descripcio() és a no, sense override (surt «part de la cara») o a no, amb override (error), i canvia destructor virtual a no per veure què desapareix del terminal. Els passos assenyalen les línies virtual de partcara.h i override de nas.h.
  • Això és el que faràs a introQt_02.html quan escriguis void keyPressEvent(QKeyEvent *event) override; en una classe que hereta de QWidget: substitueixes una funció virtual de Qt.

Composició: una cara un nas

L'herència és «és un». Però una Cara no és un nas: un nas i una boca. Això és composició, i és el que veuràs a introQt.html quan un giny en contingui dos més.

Per valor: els components viuen dins de la cara

main.cpp
#include <QCoreApplication>
#include <QDebug>
#include "cara.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Cara c(60, 45);

    qDebug() << "Fi de main";

    return 0;
}
El que escriu el terminal
PartCara creada: nas
Nas creat: 60 mm
PartCara creada: boca
Boca creada: 45 mm
Cara creada
Fi de main
Cara destruïda
Boca destruïda: 45 mm
PartCara destruïda: boca
Nas destruït: 60 mm
PartCara destruïda: nas
cara.h
#ifndef CARA_H
#define CARA_H

#include "nas.h"
#include "boca.h"

class Cara
{
public:
    Cara(int mmNas, int mmBoca);
    ~Cara();

private:
    Nas m_nas;       // un Nas de veritat, dins de cada Cara
    Boca m_boca;
};

#endif // CARA_H
cara.cpp
#include "cara.h"

#include <QDebug>

Cara::Cara(int mmNas, int mmBoca) : m_nas(mmNas), m_boca(mmBoca)
{
    qDebug() << "Cara creada";
}

Cara::~Cara()
{
    qDebug() << "Cara destruïda";
}
Altres fitxers del projecte: partcara.h, partcara.cpp, nas.h, nas.cpp, boca.h, boca.cpp
partcara.h
#ifndef PARTCARA_H
#define PARTCARA_H

#include <QString>

class PartCara
{
public:
    PartCara(const QString &nom);
    virtual ~PartCara();
    QString nom() const;

protected:
    QString m_nom;     // protected: els fills el poden veure, el món de fora no
};

#endif // PARTCARA_H
partcara.cpp
#include "partcara.h"

#include <QDebug>

PartCara::PartCara(const QString &nom) : m_nom(nom)
{
    qDebug().noquote() << "PartCara creada:" << m_nom;
}

PartCara::~PartCara()
{
    qDebug().noquote() << "PartCara destruïda:" << m_nom;
}

QString PartCara::nom() const
{
    return m_nom;
}
nas.h
#ifndef NAS_H
#define NAS_H

#include "partcara.h"

class Nas : public PartCara
{
public:
    Nas(int mm);
    ~Nas();

private:
    int m_llargada;
};

#endif // NAS_H
nas.cpp
#include "nas.h"

#include <QDebug>

Nas::Nas(int mm) : PartCara("nas"), m_llargada(mm)
{
    qDebug() << "Nas creat:" << m_llargada << "mm";
}

Nas::~Nas()
{
    qDebug() << "Nas destruït:" << m_llargada << "mm";
}
boca.h
#ifndef BOCA_H
#define BOCA_H

#include "partcara.h"

class Boca : public PartCara
{
public:
    Boca(int mm);
    ~Boca();

private:
    int m_amplada;
};

#endif // BOCA_H
boca.cpp
#include "boca.h"

#include <QDebug>

Boca::Boca(int mm) : PartCara("boca"), m_amplada(mm)
{
    qDebug() << "Boca creada:" << m_amplada << "mm";
}

Boca::~Boca()
{
    qDebug() << "Boca destruïda:" << m_amplada << "mm";
}
  • Els membres es construeixen abans que el cos del constructor de Cara, i en l'ordre en què es declaren a la classe (m_nas, després m_boca), encara que la llista d'inicialització els escrigui en un altre ordre.
  • La destrucció és exactament la contrària: primer el cos del destructor de Cara, i després els membres en ordre invers.

Per apuntador: com fa Qt amb el ui

main.cpp
#include <QCoreApplication>
#include <QDebug>
#include "cara.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Cara c(60, 45);

    qDebug() << "Fi de main";

    return 0;
}
El que escriu el terminal
PartCara creada: nas
Nas creat: 60 mm
PartCara creada: boca
Boca creada: 45 mm
Cara creada
Fi de main
Cara destruïda
Boca destruïda: 45 mm
PartCara destruïda: boca
Nas destruït: 60 mm
PartCara destruïda: nas
cara.h
#ifndef CARA_H
#define CARA_H

class Nas;       // declaració anticipada: no cal el .h complet
class Boca;

class Cara
{
public:
    Cara(int mmNas, int mmBoca);
    ~Cara();

private:
    Nas *m_nas;      // només un apuntador: l'objecte es crea amb new
    Boca *m_boca;
};

#endif // CARA_H
cara.cpp
#include "cara.h"
#include "nas.h"
#include "boca.h"

#include <QDebug>

Cara::Cara(int mmNas, int mmBoca)
    : m_nas(new Nas(mmNas))
    , m_boca(new Boca(mmBoca))
{
    qDebug() << "Cara creada";
}

Cara::~Cara()
{
    qDebug() << "Cara destruïda";
    delete m_boca;
    delete m_nas;
}
Altres fitxers del projecte: partcara.h, partcara.cpp, nas.h, nas.cpp, boca.h, boca.cpp
partcara.h
#ifndef PARTCARA_H
#define PARTCARA_H

#include <QString>

class PartCara
{
public:
    PartCara(const QString &nom);
    virtual ~PartCara();
    QString nom() const;

protected:
    QString m_nom;     // protected: els fills el poden veure, el món de fora no
};

#endif // PARTCARA_H
partcara.cpp
#include "partcara.h"

#include <QDebug>

PartCara::PartCara(const QString &nom) : m_nom(nom)
{
    qDebug().noquote() << "PartCara creada:" << m_nom;
}

PartCara::~PartCara()
{
    qDebug().noquote() << "PartCara destruïda:" << m_nom;
}

QString PartCara::nom() const
{
    return m_nom;
}
nas.h
#ifndef NAS_H
#define NAS_H

#include "partcara.h"

class Nas : public PartCara
{
public:
    Nas(int mm);
    ~Nas();

private:
    int m_llargada;
};

#endif // NAS_H
nas.cpp
#include "nas.h"

#include <QDebug>

Nas::Nas(int mm) : PartCara("nas"), m_llargada(mm)
{
    qDebug() << "Nas creat:" << m_llargada << "mm";
}

Nas::~Nas()
{
    qDebug() << "Nas destruït:" << m_llargada << "mm";
}
boca.h
#ifndef BOCA_H
#define BOCA_H

#include "partcara.h"

class Boca : public PartCara
{
public:
    Boca(int mm);
    ~Boca();

private:
    int m_amplada;
};

#endif // BOCA_H
boca.cpp
#include "boca.h"

#include <QDebug>

Boca::Boca(int mm) : PartCara("boca"), m_amplada(mm)
{
    qDebug() << "Boca creada:" << m_amplada << "mm";
}

Boca::~Boca()
{
    qDebug() << "Boca destruïda:" << m_amplada << "mm";
}
  • Ara els components són apuntadors: es creen amb new a la llista d'inicialització i s'esborren amb delete al destructor, en l'ordre invers al de creació. Destruir un apuntador no destrueix allò a què apunta.
  • class Nas; és una declaració anticipada: al .h només cal saber que existeix una classe amb aquest nom, perquè un apuntador ocupa el mateix tingui la classe la mida que tingui. El #include complet va al .cpp. És exactament el que fa namespace Ui { class Marcador; } al marcador.h que et generarà el Creator quan facis un projecte amb finestres.

Ja pots llegir un constructor com els del Creator

Quan a introQt creïs una classe amb finestra, el Creator escriurà per tu un constructor com aquest (d'una classe d'exemple, Marcador). Ja tens tot el que cal per entendre'l:

Marcador::Marcador(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Marcador)
{
    ui->setupUi(this);
}

Marcador::~Marcador()
{
    delete ui;
}
  • Marcador::Marcador(QWidget *parent): un constructor amb un argument, i = nullptr per defecte al .h.
  • : QWidget(parent): la base es construeix primer i rep el pare (herència, com PartCara("nas")).
  • , ui(new Ui::Marcador): un membre apuntador creat amb new a la llista d'inicialització (composició per apuntador).
  • ~Marcador() { delete ui; }: el destructor allibera el que el constructor ha reservat.

Un primer QObject, sense finestres

Tots els ginys de Qt hereten de QObject, la classe base de Qt. Té dues coses que veuràs a introQt amb finestres i botons, i que aquí funcionen igual sense cap finestra: pare i fills, i senyals i slots.

Pare i fills

main.cpp
#include <QCoreApplication>
#include <QDebug>
#include "cara.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Cara *c = new Cara;

    c->nas()->setLlargada(70);
    c->nas()->setLlargada(70);

    delete c;

    return 0;
}
El que escriu el terminal
Nas creat: 60 mm
Boca creada: 45 mm
Cara creada
La cara veu que el nas ara fa 70 mm
Cara destruïda
Nas destruït: 70 mm
Boca destruïda: 45 mm
cara.cpp
#include "cara.h"

#include <QDebug>

Cara::Cara(QObject *parent)
    : QObject(parent)
    , m_nas(new Nas(60, this))
    , m_boca(new Boca(45, this))
{
    connect(m_nas, &Nas::llargadaCanviada, this, &Cara::elNasHaCanviat);
    qDebug() << "Cara creada";
}

Cara::~Cara()
{
    qDebug() << "Cara destruïda";
}

Nas *Cara::nas() const
{
    return m_nas;
}

void Cara::elNasHaCanviat(int mm)
{
    qDebug() << "La cara veu que el nas ara fa" << mm << "mm";
}
cara.h
#ifndef CARA_H
#define CARA_H

#include <QObject>
#include "nas.h"
#include "boca.h"

class Cara : public QObject
{
    Q_OBJECT

public:
    explicit Cara(QObject *parent = nullptr);
    ~Cara();

    Nas *nas() const;

private slots:
    void elNasHaCanviat(int mm);

private:
    Nas *m_nas;
    Boca *m_boca;
};

#endif // CARA_H
nas.h
#ifndef NAS_H
#define NAS_H

#include <QObject>

class Nas : public QObject
{
    Q_OBJECT

public:
    explicit Nas(int mm, QObject *parent = nullptr);
    ~Nas();

    int llargada() const;

public slots:
    void setLlargada(int mm);

signals:
    void llargadaCanviada(int mm);

private:
    int m_llargada;
};

#endif // NAS_H
Altres fitxers del projecte: nas.cpp, boca.h, boca.cpp
nas.cpp
#include "nas.h"

#include <QDebug>

Nas::Nas(int mm, QObject *parent) : QObject(parent), m_llargada(mm)
{
    qDebug() << "Nas creat:" << m_llargada << "mm";
}

Nas::~Nas()
{
    qDebug() << "Nas destruït:" << m_llargada << "mm";
}

int Nas::llargada() const
{
    return m_llargada;
}

void Nas::setLlargada(int mm)
{
    if (mm == m_llargada)
        return;
    m_llargada = mm;
    emit llargadaCanviada(m_llargada);
}
boca.h
#ifndef BOCA_H
#define BOCA_H

#include <QObject>

class Boca : public QObject
{
    Q_OBJECT

public:
    explicit Boca(int mm, QObject *parent = nullptr);
    ~Boca();

private:
    int m_amplada;
};

#endif // BOCA_H
boca.cpp
#include "boca.h"

#include <QDebug>

Boca::Boca(int mm, QObject *parent) : QObject(parent), m_amplada(mm)
{
    qDebug() << "Boca creada:" << m_amplada << "mm";
}

Boca::~Boca()
{
    qDebug() << "Boca destruïda:" << m_amplada << "mm";
}
  • new Nas(60, this): el segon argument és el pare. Quan el pare es destrueix, destrueix tots els seus fills: al main només hi ha delete c, i els «Nas destruït» i «Boca destruïda» surten sols.
  • Per això a Qt es fa tant new sense delete: les finestres d'introQt fan addWidget per posar un giny dins d'un altre, i això és, en el fons, posar-li un pare. Sense pare, els fills no es destrueixen (proveu-ho al pas a pas).

Senyals i slots

  • Q_OBJECT, signals: i slots: (a nas.h, dins de la targeta anterior) són les peces amb què Qt fa que un objecte n'avisi un altre. A introQt les faràs servir amb botons i camps; aquí es veuen sense cap finestra. emit llargadaCanviada(...) (a nas.cpp) avisa tots els que hi estiguin connectats.
  • Al constructor de Cara, connect(m_nas, &Nas::llargadaCanviada, this, &Cara::elNasHaCanviat) uneix el senyal amb el slot. Sense finestres ni bucle d'esdeveniments, l'emissió és una crida directa: el slot s'executa just en aquell moment.
  • El setter només emet si el valor canvia: és el patró que evita bucles.
  • Perquè moc generi el codi dels senyals, cal qt_standard_project_setup() al CMakeLists.txt.

Tot això, amb els ginys de veritat, és el tema d'introQt.html.

Si surt un error

Els errors de classes són molt sovint de compilació o d'enllaç, i el missatge diu on i per què. Tots els d'aquesta taula són reals: s'han provocat i copiat del compilador.

Què veusQuanQuè vol dir

Els errors que no diuen res

  • Un new sense delete: fuita de memòria. Només es nota perquè el destructor no escriu res.
  • Un destructor no virtual amb herència: el del fill no s'executa.
  • Un atribut sense inicialitzar: valor indeterminat.

Per això, mentre aprens, els constructors i destructors escriuen un missatge: així veus què passa que d'una altra manera seria invisible.

Tria un exemple i avança'l pas a pas. A l'esquerra veus el codi; a la dreta, la memòria (la pila i el munt, amb els objectes i els seus atributs) i el terminal. Les sortides estan copiades d'haver compilat i executat de veritat cada exemple (amb Qt 6.4), no inventades. Els camps de sobre el codi et deixen canviar els valors i provar els errors.

Codi (línia acabada d'executar)
Memòria
Terminal

■ stderr (qDebug) · ■ stdout (QTextStream) · ■ error de compilació · ■ avís

Set reptes

Cada repte va un pas més enllà que l'anterior i fa servir el que has vist al pas a pas. Cada targeta diu què necessites de la teoria i té dues pistes que pots obrir d'una en una. Són una proposta: ajusta'ls al que vulguis demanar.

1. Hola, terminal

Crea un projecte de consola que guardi el teu nom en una QString i l'escrigui de dues maneres: amb qDebug() sense cometes i amb QTextStream(stdout). Executa el programa des del terminal redirigint la sortida a un fitxer i mira què hi acaba.

Pista 1

Per a QTextStream cal #include <QTextStream>. El salt de línia és Qt::endl.

Pista 2

Al terminal, ./programa > sortida.txt només redirigeix stdout. Quina de les dues línies acabarà al fitxer i quina continuarà sortint per pantalla?

2. Un nas

Fes una classe Nas amb un atribut llargada i un constructor que la posi a 40. Crea tres objectes, canvia'n dos i escriu-ne els tres valors.

Necessites: classe i objecte
Pista 1

Necessites tres fitxers: nas.h, nas.cpp i main.cpp, tots llistats a qt_add_executable.

Pista 2

No t'oblidis de les include guards al .h. El tercer objecte, que no has canviat, ha de valer 40.

3. La cronologia dels objectes

Afegeix un destructor a Nas i fes que el constructor i el destructor escriguin un missatge. Escriu un main amb dos objectes a la pila, un creat amb new i un bloc { … } amb un altre objecte a dins. Abans d'executar-lo, escriu en un paper les línies que sortiran, i després compara.

Pista 1

Els objectes de la pila es destrueixen en ordre invers al de creació; el de new només amb delete.

Pista 2

Un bloc { } destrueix el que s'hi ha declarat en tancar-se la clau, no al final de main. I després, treu el delete i mira què deixa de sortir.

4. Encapsula el nas

Fes private l'atribut i afegeix llargada() i setLlargada(int), que limiti el valor entre 0 i 100. Intenta tocar l'atribut directament des de main i llegeix l'error. Afegeix una funció mostra(const Nas &) i comprova què passa si el getter no és const.

Necessites: private i const
Pista 1

Un atribut privat s'acostuma a escriure amb el prefix m_, perquè el nom net (llargada) es quedi per al getter.

Pista 2

El constructor amb argument pot cridar el setter, així la validació és la mateixa a tot arreu.

5. Les parts de la cara

Crea la classe base PartCara (amb un nom) i les filles Nas i Boca. Afegeix una funció descripcio() que cada filla escrigui a la seva manera i crida-la a través d'un vector de apuntadors a la base. Fes-ho amb i sense virtual, i amb i sense destructor virtual.

Pista 1

Les filles criden el constructor de la base a la seva llista d'inicialització: : PartCara("nas").

Pista 2

Fes que constructors i destructors escriguin un missatge: així veuràs, sense cap error, quins destructors deixen d'executar-se quan el de la base no és virtual.

6. Una cara

Fes una Cara que contingui un Nas i una Boca, primer per valor i després per apuntador. Escriu abans l'ordre de les línies del terminal i comprova-ho. Escriu la llista d'inicialització en l'ordre invers al de declaració i compila amb els avisos activats.

Necessites: composició
Pista 1

Amb membres per valor no cal cap delete. Amb apuntadors, sí, al destructor.

Pista 2

Amb apuntadors, a cara.h et basta una declaració anticipada (class Nas;), i el #include va a cara.cpp.

7. Un primer QObject

Fes que Nas i Cara heretin de QObject. El setter de Nas ha d'emetre llargadaCanviada només si el valor canvia, i Cara ha d'escoltar-lo amb un slot que escrigui un missatge. Crea els fills amb la cara com a pare i comprova que no cal cap delete per a ells.

Necessites: un primer QObject
Pista 1

Cal Q_OBJECT a cada classe i qt_standard_project_setup() al CMakeLists.txt.

Pista 2

El pare es passa com a últim argument del constructor: new Nas(60, this). Prova de treure el this i mira què deixa de sortir.

Entrena't: què escriurà el terminal?

Llegeix el programa sense executar-lo, escriu en un paper les línies que sortiran i després mostra la solució. Cada cop surt un programa diferent.


    

Troba l'error

Vuit casos curts, tots reals. Abans d'obrir cada targeta decideix: compila? enllaça? què fa en executar-lo? Després compara amb el que diu el compilador de veritat.