Je suis nouveau dans la programmation C++, mais j'ai de l'expérience en Java. J'ai besoin de conseils sur la manière de passer des objets à des fonctions en C++.
Dois-je passer des pointeurs, des références ou des valeurs non-pointeur et non-référence? Je me souviens que dans Java, il n’existait pas de tels problèmes puisque nous passions simplement à la variable qui contient la référence aux objets.
Ce serait formidable si vous pouviez également expliquer où utiliser chacune de ces options.
Passer par valeur , sauf quand
const
reference ,const
lvalue ,const
référence ou non.)Passer par le pointeur n'est pratiquement jamais conseillé. Les paramètres facultatifs sont mieux exprimés sous la forme d'un std::optional
(boost::optional
pour les anciennes bibliothèques std), et l'aliasing est bien défini par référence.
La sémantique des mouvements de C++ 11 rend le passage et le retour en valeur beaucoup plus attrayants, même pour des objets complexes.
Passer des arguments par const
reference , sauf lorsque
const
reference NULL
/0
/nullptr
à la place; applique la règle précédente pour déterminer si vous devez passer par un pointeur sur un argument const
(ici, "passe par valeur" est appelé "passe par copie", car passer par valeur crée toujours une copie en C++ 03)
Il y a plus que cela, mais ces quelques règles de débutant vous mèneront assez loin.
Il existe certaines différences dans les conventions d'appel en C++ et Java. En C++, techniquement, il n'y a que deux conventions: pass-by-value et pass-by-reference, avec une littérature incluant une troisième convention de pass-by-pointeur (qui correspond en fait à une valeur passe d'un type de pointeur). En plus de cela, vous pouvez ajouter une const-ness au type de l'argument, améliorant ainsi la sémantique.
passage par référence
Passer par référence signifie que la fonction recevra conceptuellement votre instance d'objet et non une copie de celle-ci. La référence est conceptuellement un alias de l'objet utilisé dans le contexte de l'appel et ne peut pas être null. Toutes les opérations effectuées dans la fonction s'appliquent à l'objet en dehors de la fonction. Cette convention n'est pas disponible dans Java ou C.
passage par valeur (et passage par pointeur)
Le compilateur générera une copie de l'objet dans le contexte de l'appel et utilisera cette copie dans la fonction. Toutes les opérations effectuées dans la fonction sont effectuées sur la copie et non sur l'élément externe. C'est la convention pour les types primitifs en Java.
Une version spéciale de celui-ci passe un pointeur (adresse-de l'objet) dans une fonction. La fonction reçoit le pointeur et toutes les opérations appliquées au pointeur lui-même sont appliquées à la copie (pointeur). Par contre, les opérations appliquées au pointeur déréférencé s’appliqueront à l’instance d’objet située à cet emplacement mémoire. La fonction peut avoir des effets secondaires. L'effet d'utiliser le paramètre point par point d'un pointeur sur l'objet permettra à la fonction interne de modifier les valeurs externes, comme avec le paramètre passe-à-référence, et autorisera également des valeurs facultatives (passer un pointeur null).
C'est la convention utilisée en C lorsqu'une fonction doit modifier une variable externe et celle utilisée dans Java avec des types de référence: la référence est copiée, mais l'objet référé est le même: modifications de la référence/pointeur ne sont pas visibles en dehors de la fonction, mais les modifications apportées à la mémoire pointée le sont.
Ajout de const à l'équation
En C++, vous pouvez attribuer une constance aux objets lors de la définition de variables, de pointeurs et de références à différents niveaux. Vous pouvez déclarer une variable constante, vous pouvez déclarer une référence à une instance constante et définir tous les pointeurs sur des objets constants, des pointeurs constants sur des objets modifiables et des pointeurs constants sur des éléments constants. Inversement, dans Java, vous ne pouvez définir qu'un seul niveau de constante (mot-clé final): celui de la variable (instance pour les types primitifs, référence pour les types de référence), mais vous ne pouvez pas définir une référence à un élément immuable. (sauf si la classe elle-même est immuable).
Ceci est largement utilisé dans les conventions d'appel C++. Lorsque les objets sont petits, vous pouvez les transmettre par valeur. Le compilateur générera une copie, mais cette copie n’est pas une opération coûteuse. Pour tout autre type, si la fonction ne change pas l'objet, vous pouvez passer une référence à une instance constante (généralement appelée référence constante) du type. Cela ne copiera pas l'objet, mais le transmettra à la fonction. Mais en même temps, le compilateur garantira que l'objet n'est pas modifié dans la fonction.
Règles de base
Voici quelques règles de base à suivre:
Il existe d’autres petits écarts par rapport à ces règles, dont le premier concerne la gestion de la propriété d’un objet. Lorsqu'un objet est alloué dynamiquement avec new, il doit être désalloué avec delete (ou ses versions []). L'objet ou la fonction responsable de la destruction de l'objet est considéré comme le propriétaire de la ressource. Lorsqu'un objet alloué dynamiquement est créé dans un morceau de code, mais que la propriété est transférée à un élément différent, cela se fait généralement avec une sémantique passe par pointeur ou, si possible, avec des pointeurs intelligents.
note latérale
Il est important d'insister sur l'importance de la différence entre les références C++ et Java. En C++, les références sont conceptuellement l'instance de l'objet et non un accesseur. L’exemple le plus simple est l’implémentation d’une fonction de swap:
// C++
class Type; // defined somewhere before, with the appropriate operations
void swap( Type & a, Type & b ) {
Type tmp = a;
a = b;
b = tmp;
}
int main() {
Type a, b;
Type old_a = a, old_b = b;
swap( a, b );
assert( a == old_b );
assert( b == old_a );
}
La fonction swap ci-dessus change ses deux arguments en utilisant des références. Le code le plus proche en Java:
public class C {
// ...
public static void swap( C a, C b ) {
C tmp = a;
a = b;
b = tmp;
}
public static void main( String args[] ) {
C a = new C();
C b = new C();
C old_a = a;
C old_b = b;
swap( a, b );
// a and b remain unchanged a==old_a, and b==old_b
}
}
La version Java du code modifiera les copies des références en interne, mais ne modifiera pas les objets réels en externe. Les références Java sont des pointeurs C sans arithmétique de pointeur qui sont passés par valeur dans des fonctions.
Il y a plusieurs cas à considérer.
void modifies(T ¶m);
// vs
void modifies(T *param);
Ce cas concerne principalement le style: voulez-vous que le code ressemble à call (obj) ou call (& obj ) ? Cependant, il y a deux points où la différence est importante: le cas optionnel, ci-dessous, et vous souhaitez utiliser une référence pour surcharger les opérateurs.
void modifies(T *param=0); // default value optional, too
// vs
void modifies();
void modifies(T ¶m);
void uses(T const ¶m);
// vs
void uses(T param);
C'est le cas intéressant. La règle empirique est que les types "bon marché à copier" sont passés par valeur - ce sont généralement des types petits (mais pas toujours) - tandis que d'autres sont passés par const ref. Cependant, si vous avez besoin de faire une copie dans votre fonction malgré tout, vous devez passer par valeur . (Oui, cela expose un peu les détails de l'implémentation. C'est le C++. )
void uses(T const *param=0); // default value optional, too
// vs
void uses();
void uses(T const ¶m); // or optional(T param)
Il y a la moindre différence entre toutes les situations, alors choisissez celle qui vous simplifie la vie.
void f(T);
void f(T const);
Ces déclarations sont en fait exactement la même fonction ! Lors du passage de valeur, const est purement un détail d'implémentation. Essayez le:
void f(int);
void f(int const) { /* implements above function, not an overload */ }
typedef void NC(int); // typedefing function types
typedef void C(int const);
NC *nc = &f; // nc is a function pointer
C *c = nc; // C and NC are identical types
void func (vector v)
Transmettez les variables par valeur lorsque la fonction nécessite une isolation complète de l'environnement, c'est-à-dire pour empêcher la fonction de modifier la variable d'origine, ainsi que pour empêcher d'autres threads de modifier sa valeur pendant l'exécution de la fonction.
L'inconvénient est les cycles du processeur et la mémoire supplémentaire utilisée pour copier l'objet.
void func (const vector& v);
Ce formulaire émule le comportement passage par valeur tout en supprimant le temps système nécessaire à la copie. La fonction obtient un accès en lecture à l'objet d'origine, mais ne peut pas modifier sa valeur.
L'inconvénient est la sécurité des threads: toute modification apportée à l'objet d'origine par un autre thread apparaîtra dans la fonction pendant son exécution.
void func (vector& v)
Utilisez ceci lorsque la fonction doit écrire une valeur dans la variable, qui sera finalement utilisée par l'appelant.
Tout comme le cas de référence const, ce n'est pas thread-safe.
void func (const vector* vp);
Fonctionnellement identique à passer par référence const à l'exception de la syntaxe différente, plus le fait que la fonction appelante peut passer le pointeur NULL pour indiquer qu'elle n'a pas de données valides à transmettre.
Pas thread-safe.
void func (vector* vp);
Similaire à la référence non-const. L'appelant définit généralement la variable sur NULL lorsque la fonction n'est pas censée écrire une valeur. Cette convention est visible dans de nombreuses API glibc. Exemple:
void func (string* str, /* ... */) {
if (str != NULL) {
*str = some_value; // assign to *str only if it's non-null
}
}
Juste comme tous passent par référence/pointeur, pas thread-safe.
Puisque personne n’a mentionné que j’ajoute dessus, lorsque vous transmettez un objet à une fonction en c ++, le constructeur de copie par défaut de l’objet est appelé si vous n’en avez pas qui crée un clone de l’objet et le passe ensuite à la méthode. Lorsque vous modifiez les valeurs d'objet qui se répercuteront sur la copie de l'objet au lieu de l'objet d'origine, le problème se pose en c ++. Par conséquent, si vous définissez tous les attributs de classe comme des pointeurs, les constructeurs de copie copient les adresses du attributs de pointeur, donc lorsque les invocations de méthode sur l'objet manipulant les valeurs stockées dans les adresses d'attributs de pointeur, les modifications sont également répercutées sur l'objet d'origine qui est transmis en tant que paramètre, de sorte qu'il peut se comporter de la même manière que Java n'oubliez pas que tous vos attributs de classe doivent être des pointeurs, vous devez également changer les valeurs des pointeurs, cela sera beaucoup plus clair avec l'explication de code.
Class CPlusPlusJavaFunctionality {
public:
CPlusPlusJavaFunctionality(){
attribute = new int;
*attribute = value;
}
void setValue(int value){
*attribute = value;
}
void getValue(){
return *attribute;
}
~ CPlusPlusJavaFuncitonality(){
delete(attribute);
}
private:
int *attribute;
}
void changeObjectAttribute(CPlusPlusJavaFunctionality obj, int value){
int* prt = obj.attribute;
*ptr = value;
}
int main(){
CPlusPlusJavaFunctionality obj;
obj.setValue(10);
cout<< obj.getValue(); //output: 10
changeObjectAttribute(obj, 15);
cout<< obj.getValue(); //output: 15
}
Mais ce n’est pas une bonne idée car vous finirez par écrire beaucoup de code impliquant des pointeurs, sujets aux fuites de mémoire et n’oubliez pas d’appeler des destructeurs. Et pour éviter cela, c ++ a des constructeurs de copie où vous créerez une nouvelle mémoire lorsque les objets contenant des pointeurs seront passés à des arguments de fonction qui cesseront de manipuler les données des objets, Java passe par valeur et valeur est référence, de sorte ne nécessite pas de constructeur de copie.