Salut, Habr. Si vous connaissez la réponse à la question dans le titre, félicitations, vous n'avez pas besoin de cet article. Il s'adresse aux débutants en programmation, comme moi, qui ne peuvent pas toujours comprendre indépendamment toutes les subtilités du C ++ et d'autres langages typés, et s'ils le peuvent, il vaut mieux apprendre des erreurs des autres de toute façon.
Dans cet article, je ne répondrai pas seulement à la question "
Pourquoi avons-nous besoin de fonctions virtuelles en C ++ ", mais je donnerai un exemple de ma pratique. Pour une réponse brève, vous pouvez vous tourner vers les moteurs de recherche qui produisent quelque chose comme ceci: "
Les fonctions virtuelles sont nécessaires pour fournir le polymorphisme - l'une des trois baleines OOP. Grâce à elles, la machine elle-même peut déterminer le type d'objet par pointeur, sans charger le programmeur avec cette tâche. " D'accord, mais la question «pourquoi» demeure, bien que cela signifie maintenant un peu différent: «
Pourquoi compter sur la machine, consacrer plus de temps et de mémoire, si vous pouvez podcoder le pointeur vous-même, car le type d'objet auquel il se réfère est presque toujours connu? » En effet, le casting à première vue laisse les fonctions virtuelles sans travail, et c'est ce qui provoque des idées fausses et un mauvais code. Dans les petits projets, la perte est invisible, mais, comme vous le verrez bientôt, avec la croissance du programme, les castes augmentent le référencement dans une progression presque géométrique.
Tout d'abord, rappelons où les castes et les fonctions virtuelles peuvent être nécessaires. Un type est perdu lorsqu'un objet déclaré avec le type A se voit allouer une nouvelle opération pour allouer de la mémoire à un objet de type B compatible avec le type A, généralement hérité de A. Le plus souvent, l'objet n'est pas un, mais un tableau entier. Un tableau de pointeurs du même type, chacun attendant l'attribution d'une zone mémoire avec des objets de types complètement différents. Voici un exemple que nous allons considérer.
Je ne vais pas traîner longtemps, la tâche était la suivante: sur la base d'un document balisé avec le langage de balisage hypertexte Markedit (vous pouvez le lire
ici ), construire un arbre d'analyse et créer un fichier contenant le même document en balisage HTML. Ma solution consiste en trois routines séquentielles: analyser le texte source en jetons, construire une arborescence de syntaxe à partir de jetons et construire un document HTML sur sa base. Nous sommes intéressés par la deuxième partie.
Le fait est que les nœuds de l'arborescence de destination ont différents types (section, paragraphe, nœud de texte, lien, note de bas de page, etc.), mais pour les nœuds parents, les pointeurs vers les nœuds enfants sont stockés dans le tableau, et ont donc un type - Node.
L'analyseur lui-même, sous une forme simplifiée, fonctionne comme ceci: il crée la «racine» de l'arbre de syntaxe d'
arbre avec le type
racine , déclare un pointeur
open_node du type général
Node , auquel est immédiatement attribuée l'adresse d'
arbre , et la variable
type du
type énuméré
Node_type , puis la boucle démarre,
itérant sur les jetons dès le premier au dernier. À chaque itération, le type du nœud ouvert
open_node est d'abord entré dans la variable type (les types sous forme d'énumération sont stockés dans la structure du nœud), suivi de l'
instruction switch , qui vérifie le type du prochain jeton (les types de jetons sont déjà soigneusement fournis par le lexeur). Dans chaque branche du commutateur, une autre branche est présentée qui vérifie la variable de
type , où, comme nous le rappelons, le type du nœud ouvert est contenu. En fonction de sa valeur, différentes actions sont effectuées, par exemple: ajouter une liste de nœuds d'un certain type à un nœud ouvert, ouvrir un autre nœud d'un certain type dans un nœud ouvert et transmettre son adresse à
open_node , fermer le nœud ouvert,
lever une exception. Applicable au sujet de l'article, nous nous intéressons au deuxième exemple. Chaque nœud ouvert (et généralement chaque nœud pouvant être ouvert) contient déjà un tableau de pointeurs vers des nœuds de type
Node . Par conséquent, lorsque nous ouvrons un nouveau nœud dans un nœud ouvert (nous attribuons la zone de mémoire pour un objet d'un autre type au pointeur de tableau suivant), pour un analyseur sémantique C ++, il reste une instance de type
Node sans acquérir de nouveaux champs et méthodes. Un pointeur est désormais affecté à la variable
open_node , sans perdre le type de
Node . Mais comment travailler avec un pointeur d'un type
Node général lorsque vous devez appeler une méthode, par exemple un paragraphe? Par exemple,
open_bold () , qui ouvre un nœud de police en gras dedans? Après tout,
open_bold () est déclaré et défini comme une méthode de la classe
Paragraph , et
Node n'en
est pas du tout conscient. De plus,
open_node est également déclaré comme un pointeur vers
Node , et il doit accepter les méthodes de tous les types de nœuds d'ouverture.
Il y a deux solutions ici: l'évidente et la bonne. Pour un débutant, il est
évident que static_cast et les fonctions virtuelles ont raison. Regardons d'abord une branche de l'analyseur de commutateur écrit en utilisant la première méthode:
case Lexer::BOLD_START: { if (type == Node::ROOT) { open_node = tree->open_section(); open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::SECTION) { open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->open_bold(); else if (type == Node::TITLE) open_node = static_cast<Title*>(open_node)->open_bold(); else if (type == Node::QUOTE) open_node = static_cast<Quote*>(open_node)->open_bold(); else if (type == Node::UNORDERED_LIST) { open_node = static_cast<Unordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::ORDERED_LIST) { open_node = static_cast<Ordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::LINK) open_node = static_cast<Link*>(open_node)->open_bold(); else
Pas mal. Et maintenant, je ne vais pas le faire glisser pendant longtemps, je vais montrer la même section de code écrite à l'aide de fonctions virtuelles:
case Lexer::BOLD_START: { if (type == Node::ROOT) { open_node = tree->open_section(); open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else if (type == Node::SECTION) { open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else if (type == Node::UNORDERED_LIST) { open_node = open_node->close(); while (open_node->get_type() != Node::SECTION) open_node = open_node->close(); open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else
Le gain est évident, mais en avons-nous vraiment besoin? Après tout, vous devez déclarer dans la classe
Node toutes les méthodes de toutes les classes dérivées comme virtuelles et les implémenter d'une manière ou d'une autre dans chaque classe dérivée. La réponse est oui, en effet. Il n'y a pas tellement de méthodes spécifiquement dans ce programme (29), et leur implémentation dans des classes dérivées qui ne leur sont pas liées se compose d'une seule ligne:
throw string ("error!"); . Vous pouvez activer le mode créatif et proposer une ligne unique pour chaque levée d'exception. Mais le plus important - en raison de la réduction du code, le nombre d'erreurs a diminué. La diffusion est l'une des causes les plus importantes d'erreurs dans le code. Parce qu'après avoir appliqué
static_cast, le compilateur arrête de jurer si la classe appelée est contenue dans la classe donnée. Pendant ce temps, différentes classes peuvent contenir différentes méthodes portant le même nom. Dans mon cas, 6 était caché dans le code !!! erreurs, tandis que l'un d'eux a été dupliqué dans plusieurs branches de commutateur. Le voici:
else if (type == Node:: open_node = static_cast<Title*>(open_node)->open_italic();
Ensuite, sous les spoilers, j'apporte la liste complète des première et deuxième versions de l'analyseur.
Analyseur avec casting Root * Parser::parse (const Lexer &lexer) { Node * open_node(tree); Node::Node_type type; for (unsigned long i(0), len(lexer.count()); i < len; i++) { type = open_node->get_type(); if (type == Node::CITE || type == Node::TEXT || type == Node::NEWLINE || type == Node::NOTIFICATION || type == Node::IMAGE) throw string("error!"); switch (lexer[i].type) { case Lexer::NEWLINE: { if (type == Node::ROOT || type == Node::SECTION) ; else if (type == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->add_text("\n"); else if (type == Node::TITLE) open_node = static_cast<Title*>(open_node)->add_text("\n"); else if (type == Node::QUOTE) open_node = static_cast<Quote*>(open_node)->add_text("\n"); else if (type == Node::UNORDERED_LIST) { open_node = static_cast<Unordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } } else if (type == Node::ORDERED_LIST) { open_node = static_cast<Ordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } } else if (type == Node::LINK) { open_node = static_cast<Link*>(open_node)->add_text(lexer[i].lexeme); } else
Root * Parser::parse (const Lexer &lexer) { Node * open_node(tree); Node::Node_type type; for (unsigned long i(0), len(lexer.count()); i < len; i++) { type = open_node->get_type(); if (type == Node::CITE || type == Node::TEXT || type == Node::NEWLINE || type == Node::NOTIFICATION || type == Node::IMAGE) throw string("error!"); switch (lexer[i].type) { case Lexer::NEWLINE: { if (type == Node::ROOT || type == Node::SECTION) ; else if (type == Node::PARAGRAPH || type == Node::TITLE || type == Node::QUOTE || type == Node::TITLE || type == Node::QUOTE) open_node = open_node->add_text("\n"); else if (type == Node::UNORDERED_LIST || type == Node::ORDERED_LIST) { open_node = open_node->close(); while (open_node->get_type() != Node::SECTION) open_node = open_node->close(); } else
1357 487 – , !
: ? , ? – . :
– 538 .
– 1174 .
, 636 – . ? . , , , . – ,
static_cast dynamic_cast , . ?