Odpowiedź na pytanie: Nie ma żadnej różnicy, a tymbardziej żadnej, która powinna cię obchodzić z punktu widzenia programisty.
Co robią implementacje, spójrzmy (Windows7/MinGW, inne zachowują się tak samo/podobnie).
Przykładowy kodzik:
#include <iostream>
using namespace std;
void byref(int& ref) {
cout << ref << endl;
}
void byptr(int* ptr) {
cout << *ptr << endl;
}
int main(int argc, char** argv) {
int a{0};
byref(a);
byptr(&a);
return 0;
}
Dość prosty, tworzymy zmienną, dalej ją przekazujemy do funkcji przez referencje i przez wskaźnik.
Teraz kompilacja:
g++ test.cpp -o test.exe -std=c++11 -fdump-tree-all
Opcja -fdump-tree-all
zrzuci nam poszczególne etapy kompilacji w różnych plikach, nas interesuje ten z rozszerzeniem optimized
czyli kod już w miarę "gotowy".
Jak to wygląda:
;; Function void byref(int&) (_Z5byrefRi, funcdef_no=1137, decl_uid=25418, cgraph_uid=234)
void byref(int&) (int & ref)
{
struct basic_ostream & D.25570;
struct basic_ostream & D.25569;
int D.25568;
int _2;
struct basic_ostream & _3;
struct basic_ostream & _4;
<bb 2>:
_2 = *ref_1(D);
_3 = std::basic_ostream<char>::operator<< (&cout, _2);
_4 = _3;
std::basic_ostream<char>::operator<< (_4, endl);
return;
}
;; Function void byptr(int*) (_Z5byptrPi, funcdef_no=1138, decl_uid=25424, cgraph_uid=235)
void byptr(int*) (int * ptr)
{
struct basic_ostream & D.25615;
struct basic_ostream & D.25614;
int D.25613;
int _2;
struct basic_ostream & _3;
struct basic_ostream & _4;
<bb 2>:
_2 = *ptr_1(D);
_3 = std::basic_ostream<char>::operator<< (&cout, _2);
_4 = _3;
std::basic_ostream<char>::operator<< (_4, endl);
return;
}
;; Function int main(int, char**) (main, funcdef_no=1139, decl_uid=25428, cgraph_uid=236)
int main(int, char**) (int argc, char * * argv)
{
void * D.25643;
int a;
int D.25616;
int _1;
<bb 2>:
a = 0;
byref (&a);
<bb 3>:
byptr (&a);
<bb 4>:
_1 = 0;
a ={v} {CLOBBER};
<L1>:
return _1;
<L2>:
_2 = __builtin_eh_pointer (1);
__builtin_unwind_resume (_2);
}
;; Function void __tcf_0() (__tcf_0, funcdef_no=1164, decl_uid=25563, cgraph_uid=260)
void __tcf_0() ()
{
<bb 2>:
std::ios_base::Init::~Init (&__ioinit);
return;
}
;; Function void __static_initialization_and_destruction_0(int, int) (_Z41__static_initialization_and_destruction_0ii, funcdef_no=1163, decl_uid=25559, cgraph_uid=261)
void __static_initialization_and_destruction_0(int, int) (int __initialize_p, int __priority)
{
<bb 2>:
if (__initialize_p_1(D) == 1)
goto <bb 3>;
else
goto <bb 5>;
<bb 3>:
if (__priority_2(D) == 65535)
goto <bb 4>;
else
goto <bb 5>;
<bb 4>:
std::ios_base::Init::Init (&__ioinit);
atexit (__tcf_0);
<bb 5>:
return;
}
;; Function (static initializers for test.cpp) (_GLOBAL__sub_I__Z5byrefRi, funcdef_no=1165, decl_uid=25566, cgraph_uid=262)
(static initializers for test.cpp) ()
{
<bb 2>:
__static_initialization_and_destruction_0 (1, 65535);
return;
}
Na co zwrócić uwagę:
#Obie funkcji mają takie same ciała.
#Wywołania obu funkcji wyglądają identycznie.
Dalej, możemy sobie odpalić jakis disassembler i zobaczyć jak wygląda już skompilowane w pełni wywołanie funkcji. A no, identycznie w obu przypadkach.
mov [esp+24h+var_24], ebx
call sub_4013F0
mov [esp+24h+var_24], ebx
call sub_401470
Jak ktoś jeszcze nie dowierza to zachęcam do sprawdzenia implementacji tych funkcji (przez użycie cout
są one dość długie w asmie).