Super question!
Pour clarifier, j'imagine que par type opaque, tu parles d'un type qui n'est manipule uniquement a travers un pointeur qui est retourne par une fonction et passe a des fonctions, mais jamais dereference expliitement. Quelquechose comme
struct monTypeAMoi;
monTypeAMoi* creerType();
void operationCool(monTypeAMoi*);
void desalloueType(monTypeAMoi*);
Ce genre de chose. (Si c'est pas ca, clarifie, je vais continuer en supposant que c'est de ca dont tu parles.)
La raison principal pour utiliser un type opaque est de separer clairement l'implementation de l'abstraction de l'objet de facon portable. C'est d'ailleurs la seule facon en C de fournir des fonctionnalite objet comme encapsulation. Et il y a deux buts principaux qui sont tous les deux des but de portabilites et d'abstraction.
1/ C'est une question de portabilite vers des bibliotheques tres differente. Et vraiment une implementation des interface java en C, ou des objets virtuel pure en C++.
L'exemple typique c'est l'exemple de FILE* dans la lib C.
FILE* c'est vraiment un type abstrait si tu y reflechit. C'est un type qui fournit une abstraction de fichier et qui fonctionne sur tout les systemes d'exploitation du monde. Ca permet meme d'acceder a des choses qui ne sont pas des fichiers, comme si c'etait des fichiers. Du moment que ca suit l'interfacage presente par la lib C standard.
Regardons different cas. Ca marche sur windows, mac, linux, freebsd, etc. De memoire il y a meme des abstractions pour nintendoDS qui expose les donnees comme des fichiers FILE* alors qu'il n'y a pas de systeme de fichier dans la machine.
2/ Ca permet de creer un type qui est gerer par une bibliotheque sans avoir a recompiler l'application.
Cette raison est utilise par CURL. Imagine que tu as une application qui est deja compile et qui utilise CURL. Mais la tu n'as pas le code de cette application. C'est tres commun en pratique: des jeux videos proprietaire, des solveurs (gurobi par exemple), des bases de donnee proprietaire. Une faille a ete identifie dans CURL et il faudrait mettre a jour la bibliotheque. Tu veux pouvoir mettre a jour la bibliotheque sans avoir besoin de recompiler l'application (parceque tu n'as pas le code.)
Si le type n'etait pas obscure tu pourrais avoir ca dans le header bibliotheque:
struct monTypeAMoi {
int champ1;
int champ2;
};
Et dans le code de l'application tu pourrais avoir:
struct monTypeAMoi b;
struct monTypeAMoi *arr = malloc(sizeof(struct monTypeAMoi)*sizearray);
//En pratique ca alloue 8*sizearray octets (en supposant int == 4 octets)
Maintenant si la nouvelle version de la bibliotheque change le type:
struct monTypeAMoi {
int champ1;
int champ2;
int champ3;
};
Maintenant l'objet fait 12 octets (+padding probablement). Ce qui fait que le code deja compile va allouer le mauvais nombre d'octet. "struct monTypeAMoi b;" va alloue 8 octets sur la pile au lieu de 12, et le malloc va creer un tableau trop petit. En pratique, tu ne vas pas pouvoir faire cette mise a jour.
Alors que si le code ne manipulait que des types opaques, le code ne ferait que des appels de fonctions sur des pointeurs. Tout ca ne change pas de taille et donc les allocations memoires qui sont faite par l'application deja compiles restent correcte. la taille des objets eux meme peuvent changer parceque le code responsable de choisir les tailles de ces choses la est aussi dans la bilbiothque et donc il est changer en meme temps.
Cette fonctionnalite, pouvoir changer la bibliothque sans recompiler, est particulierement importante dans des applications ou la bibliotheque est critique en terme de securite. Si tu as une faille dans libCURL ou libSSL, tu veux pouvoir changer la lib de la facon la plus simple possible. C'est aussi utile pour les applications de calcul haute performance, ou en fonction de la machine ou le code tourne, tu veux utiliser une lib de calcul differente. J'aide un collegue a faire du machine learning ces jours ci. Et la lib qui est utilise change dynamiquement en fonction de l'architecture sur laquelle on est. Si il y a GPU nVidia, ca charge la lib qui utilise cuda, si la machine utilise le cpu, ca utilise la lib OpenCL qui parallelise sur le CPU. Et tu peux utiliser le meme binaire sur deux machines differentes qui partagent le meme systeme de fichier.
C'est aussi utiliser par pas mal d'outils de debuggage pour intercepter les appels a malloc, brk, etc... pour fournir plus d'information au debuggage.