Pour ce qui est correcte, c'est facile : tu stocke dans les pointeurs l'adresse de variable du type pointé par le pointeur (sauf pour pc=&i, j'y reviens). Ou au contraire, tu stocke dans une variable, ce qui est pointé par un pointeur pointant vers un objet du même type que la variable que tu es en train d'écrire (où un type compatible).
Et ce n'est pas vrai pour les exemple faux.
Ce qui est important, c'est la compatibilité binaire. Quand tu stocke le poiteur vers un int (&i) dans un pointeur vers un char (pc) ce n'est pas un problème, car un pointeur vers un int est aussi un pointeur vers un char si on ne regarde ue les 8 premiers bits de l'int (qui sont ceux de poids faible en plus si les architectures standard).
Et après ça, si tu fait c=*pc; ou i=*pc; ça va marcher mais dans les deux cas, tu obtiendra la valeur qu'il y avait dans i modulo 256. De même, et ça explique ce que je viens de dire, tu peut écrire c=*pi; car *pi est un int et tu peut l'écrire dans un char, il va juste être tronqué. Quand tu fait *pc, même si dedans il y a &i, c'est un char qui est lu dans la mémoire et donc stocké dans i ou c dans mes exemple précédant (d'où la troncature, mais aussi l'autorisation de faire ça).
Ça ne marche pas avec pi=&f, car les représentations binaires de ces deux types ne sont pas compatibles. Idem pour le reste (mais dans les autres c'est parce que le niveau d'indirection n'est pas le même à gauche et à droite).
À noter que tout ça est vrai en C, ou un poiteur vers un int peut être converti en un pointeur vers autre chose sans que le compilateur ne sennerve trop. Mais par contre il ne permet pas de transformer un pointeur vers un type non pointeur en un pointeur vers un pointeur ou vice versa et de même entre n'importe quel niveau d'indirection différents.
En C++ tout ça est faux, il faut des conversions explicite pour le compilo entre différents type de pointeur.
P.S. je n'ai rien testé en écrivant cela, il est donc tout à fait possible qu'il y a est plein d'erreurs qui trainent.