#include #include "complexe.h"
en utilisant l'espace de noms std ;
ostream& opérateur << (ostream& os, const complexe& x) { return os << '(' << real (x) << ',' << imag (x) << ')' }
int main() { complexe c1(2, 1); complexe c2(4, 0);
cout << c1 << endl; cout << c2 << endl;
cout << c1+c2 << endl; cout << c1-c2 << endl; cout << c1*c2 << endl;
cout << conj(c1) << endl; cout << norm(c1) << endl; cout << polaire(10,4) << endl;
cout << (c1 += c2) << endl;
cout << (c1 == c2) << endl; cout << (c1 != c2) << endl;
cout << (c2 - 2) << endl; cout << (5 + c2) << endl;
renvoie 0 ;
Comme le montre la figure ci-dessus, l'auto-affectation doit être vérifiée lors de la surcharge de l'opérateur d'affectation "=", pour les raisons suivantes :
S'il n'y a pas de détection d'auto-affectation, alors les m_data du propre objet seront libérés et le contenu pointé par m_data n'existera pas, il y aura donc des problèmes avec la copie.
Voici deux classes pour explication :
class complex
{
public:
complex (double r = 0, double i = 0)
: re (r), im (i)
{ }
complex& operator += (const complex&);
double real () const { return re; }
double imag () const { return im; }
private:
double re, im;
friend complex& __doapl (complex*,
const complex&);
};
class String
{
public:
String(const char* cstr = 0);
String(const String& str);
String& operator=(const String& str);
~String();
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
Après avoir créé ces deux objets, le compilateur (VC) alloue de la mémoire aux deux objets comme suit :
Les deux à gauche sont l'allocation de mémoire du compilateur du complexe de classes en mode débogage et en mode release. En mode débogage, le compilateur insère la partie d'information de taille tête et queue (partie rouge) 4*8 + 4 (partie grise) dans la mémoire de l'objet complexe. La partie verte est l'espace réellement occupé par l'objet complexe. il n'y a que 52 mots de section, mais le VC est aligné sur 16 octets, donc le multiple de 16 le plus proche de 52 est 64, et l'espace de 12 octets doit être comblé (partie du tampon cyan). Pour l'objet complexe dans la partie release, seuls l'en-tête d'information et la partie en-tête sont ajoutés. L’analyse de la classe string est fondamentalement la même.
Voyons ensuite comment le compilateur VC alloue les objets tableau :
De même, le compilateur ajoute des informations redondantes à l'objet. Pour les objets de classe complexe, puisque le tableau comporte trois objets, il y a 8 doubles. Ensuite, le compilateur insère "3" devant les trois objets complexes pour marquer le numéro des objets individuels. . La méthode d'analyse de la classe String est également similaire.
L'image ci-dessous illustre pourquoi la suppression d'un objet tableau nécessite la méthode delete[] :
Chaîne.h
#ifndef __MYSTRING__
#define __MYSTRING__
class String
{
public:
String(const char* cstr=0);
String(const String& str);
String& operator=(const String& str);
~String();
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
#include <cstring>
inline
String::String(const char* cstr)
{
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = ' ';
}
}
inline
String::~String()
{
delete[] m_data;
}
inline
String& String::operator=(const String& str)
{
if (this == &str)
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
#include <iostream>
using namespace std;
ostream& operator<<(ostream& os, const String& str)
{
os << str.get_c_str();
return os;
}
#endif
string_test.cpp
#include "string.h"
#include <iostream>
using namespace std;
int main()
{
String s1("hello");
String s2("world");
String s3(s2);
cout << s3 << endl;
s3 = s1;
cout << s3 << endl;
cout << s2 << endl;
cout << s1 << endl;
}
En C++, un constructeur à un paramètre (ou un constructeur à plusieurs paramètres avec des valeurs par défaut pour tous les paramètres sauf le premier paramètre) joue deux rôles. 1 est un constructeur, 2 est un opérateur de conversion de type par défaut et implicite.
Par conséquent, parfois, lorsque nous écrivons du code tel que AAA = XXX et que le type de XXX se trouve être le type de paramètre du constructeur AAA à paramètre unique, le compilateur appellera automatiquement ce constructeur pour créer un objet AAA.
Cela a l'air cool et très pratique. Mais dans certains cas (voir l'exemple faisant autorité ci-dessous), cela va à l'encontre de nos intentions initiales (celles des programmeurs). À ce stade, vous devez ajouter une modification explicite devant le constructeur pour spécifier que ce constructeur ne peut être appelé/utilisé qu'explicitement et ne peut pas être utilisé implicitement comme opérateur de conversion de type.
Le constructeur explicite est utilisé pour empêcher les conversions implicites. Veuillez regarder le code ci-dessous :
class Test1
{
public:
Test1(int n)
{
num=n;
}//普通构造函数
private:
int num;
};
class Test2
{
public:
explicit Test2(int n)
{
num=n;
}//explicit(显式)构造函数
private:
int num;
};
int main()
{
Test1 t1=12;//隐式调用其构造函数,成功
Test2 t2=12;//编译错误,不能隐式调用其构造函数
Test2 t2(12);//显式调用成功
return 0;
}
Le constructeur de Test1 prend un paramètre int et la ligne 23 du code sera implicitement convertie en appelant le constructeur de Test1. Le constructeur de Test2 est déclaré explicite, ce qui signifie que le constructeur ne peut pas être appelé via une conversion implicite, donc une erreur de compilation se produira à la ligne 24 du code.
Les constructeurs ordinaires peuvent être appelés implicitement. Le constructeur explicite ne peut être appelé qu'explicitement.
Lorsqu'une fraction doit être convertie en type double, la fonction double() est automatiquement appelée pour la conversion. Comme le montre la figure ci-dessus, le compilateur détermine que 4 est un entier lors de l'analyse du double d = 4 + f, puis continue de déterminer f. On observe que f fournit la fonction double(), puis exécute le double. () opération sur f, et calcule 0.6 , puis l'ajoute à 4, et enfin obtient le double type 4.6.
Une classe est définie dans la figure ci-dessus, appelée Fraction. L'opérateur "+" est surchargé dans la classe Lors de l'opération f+4, "4" est implicitement converti (constructeur) par le compilateur en un objet Fraction, puis passé. via Fraction L'opérateur "+" surchargé participe à l'opération.
Comme le montre la figure ci-dessus, la fonction double() est ajoutée à Fraction, qui divise les deux variables membres de Fraction, puis les convertit de force en type double et renvoie le résultat. Pendant le processus de surcharge f+4, le compilateur le fera. signaler une erreur. Vous pouvez effectuer l'analyse suivante :
1. Tout d'abord, 4 est implicitement converti (constructeur) en un objet Fraction, puis l'opérateur "+" surchargé est utilisé avec "f" pour renvoyer un objet Fraction ;
2. Tout d'abord, 4 est implicitement converti (constructeur) en un objet Fraction, puis une double opération est effectuée sur la paire via l'opérateur "+" surchargé et l'opération "f", et enfin un objet Fraction est renvoyé ;
3. . .
Le compilateur a donc au moins deux voies à suivre, ce qui crée une ambiguïté et signale une erreur. Comme le montre la figure ci-dessus, si le mot-clé explicite est ajouté avant le constructeur Franction, la conversion implicite sera annulée. Par conséquent, lors de l'exécution de d2 = f + 4, f appellera la fonction double pour convertir en 0,6, puis. ajoutez-le à 4 pour devenir 4.6 Puisque le constructeur annule la conversion de formule implicite, 4.6 ne peut pas être converti en Fraction, donc une erreur sera signalée.
La figure suivante montre une application des fonctions de surcharge et de conversion d'opérateurs en stl C++ :
L'image ci-dessous illustre très bien la structure interne et l'utilisation des pointeurs intelligents : Il y a trois points clés dans la syntaxe des pointeurs intelligents. Le premier est le pointeur externe enregistré, qui correspond à T* px dans l'image ci-dessus. lié au pointeur entrant sera effectué à la place du pointeur entrant ; la deuxième consiste à surcharger l'opérateur "*", à déréférencer et à renvoyer un objet pointé par le pointeur ; la troisième consiste à surcharger l'opérateur "->", à return Un pointeur correspondant à l'image ci-dessus est px.
Les itérateurs sont également un type de pointeurs intelligents. Il existe également les trois éléments de pointeurs intelligents mentionnés ci-dessus, qui correspondent à la police rouge et aux parties marquées en jaune dans la figure ci-dessous :
Les caractères de surcharge "*" et "->" de la surcharge de l'itérateur seront soigneusement analysés ci-dessous :
Créez un objet itérateur de liste, list::iterator ite ; la liste ici est utilisée pour enregistrer l'objet Foo, qui est la classe dans la définition du modèle de liste. T, Operator*() renvoie un objet (*node).data, le nœud est de type __link_type, mais __link_type est de type __list_node<T>* T voici Foo, donc le nœud est de type __list_node<Foo >*, donc ( *node).data obtient un objet de type Foo, et &(operator*()) obtient enfin une adresse de l'objet Foo, c'est-à-dire renvoie Foo* Un pointeur vers le type.
Comme vous pouvez le voir sur la figure ci-dessus, chaque foncteur est une classe qui surcharge l'opérateur "()", et devient alors un "foncteur". Il s'agit en fait d'une classe, mais elle semble avoir les propriétés d'une fonction. Chaque foncteur intègre en fait une classe étrange derrière lui, comme le montre la figure ci-dessous. Cette classe n'a pas besoin d'être explicitement déclarée manuellement par le programmeur. Le foncteur de la bibliothèque standard hérite également d'une classe étrange : le contenu de cette classe est montré dans la figure ci-dessous. Elle déclare simplement certaines choses, et il n'y a pas de variables ou de fonctions réelles. Le contenu spécifique sera discuté en STL.
Contrairement aux modèles de classe, les modèles de fonctions n'ont pas besoin de déclarer explicitement le type des paramètres entrants lors de leur utilisation, le compilateur en déduira automatiquement le type.
Les modèles de membres sont souvent utilisés dans la programmation générique. Afin d'avoir une meilleure évolutivité, prenons la figure ci-dessus comme exemple. T1 est souvent la classe de base de U1, et T2 est souvent la classe de base de U2. De cette manière, tant que les classes parents ou ancêtres de U1 et U2 transmises sont T1 et T2, l'héritage et le polymorphisme peuvent être intelligemment utilisés grâce à cette méthode, mais pas l'inverse. Cette méthode est beaucoup utilisée en STL :
Comme son nom l'indique, la partialisation d'un modèle fait référence à la spécification de types de données spécifiques dans le modèle, ce qui est différent de la généralisation : bien sûr, la partialisation d'un modèle a également des degrés, et des types partiels peuvent être spécifiés, ce qu'on appelle la spécialisation partielle :
Trop de contenu sera expliqué dans le cours C++11, je ne le présenterai donc ici que pour l'instant.
Trop de contenu sera expliqué dans le cours C++11, je ne le présenterai ici que pour l'instant.
Une référence peut être considérée comme un alias pour une variable référencée.
Comme le montre la figure ci-dessus, trois classes sont définies, A, B et C. B hérite de A et C hérite de B. Il existe deux fonctions virtuelles dans A, une dans B et une dans C. Le compilateur alloue l'objet a de A dans la mémoire comme le montre la figure ci-dessus. Il n'y a que deux variables membres m_data1 et m_data2 en même temps, puisque la classe A a des fonctions virtuelles, le compilateur allouera un espace à l'objet a. pour sauvegarder la table des fonctions virtuelles, cette table conserve l'adresse de la fonction virtuelle (fonction dynamique) de cette classe. Liaison avec état), puisque la classe A a deux fonctions virtuelles, il y a deux espaces (espaces jaunes et bleus) dans la table des fonctions virtuelles de a pointant respectivement vers A::vfunc1() et A::vfunc2(), b It ; est un objet de classe B, car la classe B remplace la fonction vfunc1() de la classe A. , donc la table des fonctions virtuelles de B (partie cyan) pointera vers B::vfunc1(), et B hérite de vfunc2() de la classe A, donc la table des fonctions virtuelles de B (partie bleue) pointera vers A de la classe parent A : :vfunc2 () fonction de même, c est un objet de classe C, représenté par La classe C réécrit la fonction vfunc1() de la classe parent, donc la table des fonctions virtuelles de C (partie jaune) pointera vers C::vfunc1(). En même temps, C hérite de vfunc2() de la superclasse A, donc la fonction virtuelle de B. Le tableau (partie bleue) pointera vers la fonction A::vfunc2(). Dans le même temps, la figure ci-dessus utilise également le code du langage C pour illustrer comment la couche inférieure du compilateur appelle ces fonctions. C'est l'essence du polymorphisme d'héritage orienté objet.
Le pointeur this peut en fait être considéré comme un pointeur pointant vers l'adresse mémoire de l'objet actuel, comme le montre la figure ci-dessus, puisqu'il existe des fonctions virtuelles dans la classe et la sous-classe de base, this->Serialize() sera lié dynamiquement, ce qui équivaut à (*(this- >vptr)[n])(this). Cela peut être compris en combinant le pointeur virtuel et la table de fonctions virtuelles dans la section précédente. Quant à la raison pour laquelle il est correct d'écrire ainsi à la fin, le résumé suivant l'expliquera.