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;
}QCoreApplicationés la versió sense finestres deQApplication. 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. Ambexec()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 haWidgets. Els projectes antics amb.pro(qmake) escrivienQT -= guiper dir el mateix. - Cada classe nova són dos fitxers (
nas.cppinas.h) que s'han de llistar aqt_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 ambQ_OBJECT(l'últim bloc): activamoc.
qDebug o QTextStream?
| Va a… | Text | Quan | |
|---|---|---|---|
qDebug() << s | stderr | una QString surt entre cometes | per depurar |
qDebug().noquote() << s | stderr | sense cometes | per depurar, més net |
QTextStream out(stdout) | stdout | tal qual | quan 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;
}nas.h#ifndef NAS_H
#define NAS_H
class Nas
{
public:
int llargada; // atribut (variable membre), en mm
};
#endif // NAS_HNas n1, n2;crea dos objectes. Cadascun té el seullargada: canviar el den1no toca el den2.public:vol dir que qualsevol pot llegir i escriure l'atribut ambobjecte.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;
}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_Hnas.cpp#include "nas.h"
Nas::Nas()
{
llargada = 40; // valor inicial de qualsevol Nas nou
}- Les tres línies
#ifndef … #define … #endifsó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. ElNas::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 crea | Nas n(51); | Nas *p = new Nas(96); |
| Com s'accedeix | n.llargada | p->llargada (amb un apuntador, ->) |
| Quan es destrueix | sol, quan s'acaba el bloc | nomé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;
}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_Hnas.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 anas.hi definit anas.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;demain.cpps'executa a l'instant (surt «Nas destruït: 60 mm» abans de «Fi de main»), però els objectes de la pila,n1in2, es destrueixen a l'acabarmain, en ordre invers al de creació (primern2, desprésn1). - Si esborres la línia
delete p;demain.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;
}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_Hnas.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;oNas 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;
}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_Hnas.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;
}privatevol dir que només la pròpia classe hi pot accedir. Des demain,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.cppes demanasetLlargada(150)i el terminal mostran1: 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 portasetdavant (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;
}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_HAltres 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;, anas.h, iint Nas::llargada() constanas.cpp): és una promesa que aquesta funció no modifica l'objecte. El compilador la vigila: si dins dellargada()escriusm_llargada = 0;, no compila. Cal escriure-la igual al.hi al.cpp. Un getter sempre l'ha de portar. - A un paràmetre (
const Nas &n, a la funciómostrademain.cpp): el&és una referència (el mateix objecte, sense fer-ne una còpia) i elconstvol dir quemostrano 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 membreconst. A l'exemple,mostracridan.llargada(), que portaconstanas.h: per això compila.n.setLlargada(5)no compilaria, perquè el setter sí que modifica. - Si al getter li treus el
const, el compilador diuerror: 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 aconst 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;
}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_Hpartcara.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_Hnas.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_Hboca.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(anas.h):Nashereta tot el que és públic iprotecteddePartCara.protectedés a mig camí entrepubliciprivate: el veuen la classe i les seves filles, però no el món de fora. Per aixòmain.cppno pot fern.m_nom, i necessita el getternom().- La base es construeix sempre primer. La llista d'inicialització de
Nascomença perPartCara("nas"): així el constructor de la base rep el nom. Destrucció: el contrari, primer el fill i després la base. - Què fa el
constdeQString PartCara::nom() const? És el mateixconstde la promesa del bloc anterior:nom()només llegeixm_nomi no modifica l'objecte. Com a getter, sempre el porta. Elconstes repeteix apartcara.h(QString nom() const;) i apartcara.cpp. Que hi sigui permet cridarnom()també sobre un objecteconst(per exemple, a través d'unconst PartCara &), i la funció retorna una còpia de laQString(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;
}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_Hnas.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_HAltres 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_Hboca.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:
| Fitxer | Línia | Què vol dir |
|---|---|---|
partcara.h | virtual 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.h | QString 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.h | virtual ~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è canvies | Què surt |
|---|---|
virtual a la base i override a les filles | nas de 60 mm i boca de 45 mm d'amplada |
sense virtual ni override | part de la cara dues vegades: C++ tria pel tipus del apuntador |
sense virtual a la base, amb override a la filla | no 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 alconstno donaria cap error: la filla tindria una funció nova amb un nom semblant, i la base seguiria cridant la seva. Amboverride, 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
virtualdepartcara.hioverridedenas.h. - Això és el que faràs a introQt_02.html quan escriguis
void keyPressEvent(QKeyEvent *event) override;en una classe que hereta deQWidget: substitueixes una funció virtual de Qt.
Composició: una cara té un nas
L'herència és «és un». Però una Cara no és un nas: té 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;
}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_Hcara.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_Hpartcara.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_Hnas.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_Hboca.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ésm_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;
}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_Hcara.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_Hpartcara.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_Hnas.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_Hboca.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
newa la llista d'inicialització i s'esborren ambdeleteal destructor, en l'ordre invers al de creació. Destruir un apuntador no destrueix allò a què apunta. class Nas;és una declaració anticipada: al.hnomés cal saber que existeix una classe amb aquest nom, perquè un apuntador ocupa el mateix tingui la classe la mida que tingui. El#includecomplet va al.cpp. És exactament el que fanamespace Ui { class Marcador; }almarcador.hque 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= nullptrper defecte al.h.: QWidget(parent): la base es construeix primer i rep el pare (herència, comPartCara("nas"))., ui(new Ui::Marcador): un membre apuntador creat ambnewa 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;
}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_Hnas.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_HAltres 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_Hboca.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: almainnomés hi hadelete c, i els «Nas destruït» i «Boca destruïda» surten sols.- Per això a Qt es fa tant
newsensedelete: les finestres d'introQt fanaddWidgetper 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:islots:(anas.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(...)(anas.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è
mocgeneri el codi dels senyals, calqt_standard_project_setup()alCMakeLists.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è veus | Quan | Què vol dir |
|---|
Els errors que no diuen res
- Un
newsensedelete: 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.
■ 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.
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.
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.
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.
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.