Apakah ada manfaat lewat pointer dari melewati referensi di C ++?

225

Apa manfaat lewat pointer dari melewati referensi di C ++?

Akhir-akhir ini, saya telah melihat sejumlah contoh yang memilih lewat argumen fungsi oleh pointer daripada lewat referensi. Apakah ada manfaatnya melakukan ini?

Contoh:

func(SPRITE *x);

dengan panggilan

func(&mySprite);

vs.

func(SPRITE &x);

dengan panggilan

func(mySprite);
Matt Pascoe
sumber
Jangan lupa newuntuk membuat pointer dan masalah kepemilikan yang dihasilkan.
Martin York

Jawaban:

216

Pointer dapat menerima parameter NULL, parameter referensi tidak bisa. Jika ada kemungkinan Anda ingin meneruskan "tidak ada objek", maka gunakan pointer daripada referensi.

Selain itu, lewat penunjuk memungkinkan Anda melihat secara eksplisit di situs panggilan apakah objek dilewatkan oleh nilai atau dengan referensi:

// Is mySprite passed by value or by reference?  You can't tell 
// without looking at the definition of func()
func(mySprite);

// func2 passes "by pointer" - no need to look up function definition
func2(&mySprite);
Adam Rosenfield
sumber
18
Jawaban tidak lengkap. Menggunakan pointer tidak akan mengesahkan penggunaan objek sementara / dipromosikan, atau penggunaan objek runcing sebagai objek seperti tumpukan. Dan itu akan menyarankan bahwa argumennya bisa NULL ketika, sebagian besar waktu, nilai NULL harus dilarang. Baca jawaban litb untuk jawaban yang lengkap.
paercebal
Panggilan fungsi kedua digunakan untuk dianotasi func2 passes by reference. Sementara saya menghargai bahwa Anda bermaksud melewati "dengan referensi" dari perspektif level tinggi, diimplementasikan dengan melewatkan pointer pada perspektif level kode, ini sangat membingungkan (lihat stackoverflow.com/questions/13382356/… ).
Lightness Races in Orbit
Saya hanya tidak membeli ini. Ya, Anda memasukkan pointer, jadi karena itu harus menjadi parameter output, karena apa yang menunjuk tidak bisa menjadi const?
deworde
Bukankah kita memiliki referensi kelulusan ini dalam C? Saya menggunakan codeblock (mingw) versi terbaru dan dalam memilih proyek C. Masih lewat referensi (func (int & a)) berfungsi. Atau apakah tersedia di C99 atau C11 dan seterusnya?
Jon Wheelock
1
@JonWheelock: Tidak, C tidak memiliki referensi sama sekali. func(int& a)tidak valid C dalam versi standar apa pun. Anda mungkin mengkompilasi file Anda sebagai C ++ secara tidak sengaja.
Adam Rosenfield
245

Melewati dengan pointer

  • Penelepon harus mengambil alamat -> tidak transparan
  • Nilai 0 dapat diberikan dengan rata-rata nothing. Ini dapat digunakan untuk memberikan argumen opsional.

Lewati dengan referensi

  • Penelepon hanya melewati objek -> transparan. Harus digunakan untuk overloading operator, karena overloading untuk tipe pointer tidak dimungkinkan (pointer adalah tipe builtin). Jadi Anda tidak bisa string s = &str1 + &str2;menggunakan pointer.
  • Tidak ada 0 nilai yang mungkin -> Fungsi yang dipanggil tidak perlu memeriksa mereka
  • Referensi ke const juga menerima temporaries void f(const T& t); ... f(T(a, b, c));:, pointer tidak dapat digunakan seperti itu karena Anda tidak dapat mengambil alamat sementara.
  • Last but not least, referensi lebih mudah digunakan -> lebih sedikit kesempatan untuk bug.
Johannes Schaub - litb
sumber
7
Melewati pointer juga memunculkan 'Apakah kepemilikan ditransfer atau tidak?' pertanyaan. Ini bukan masalahnya dengan referensi.
Frerich Raabe
43
Saya tidak setuju dengan "lebih sedikit kesempatan untuk bug". Ketika memeriksa situs panggilan dan pembaca melihat "foo (& s)" segera jelas bahwa s dapat dimodifikasi. Ketika Anda membaca "foo (s)" sama sekali tidak jelas apakah s dapat dimodifikasi. Ini adalah sumber utama bug. Mungkin ada sedikit peluang dari kelas bug tertentu, tetapi secara keseluruhan, memberikan referensi adalah sumber bug yang sangat besar.
William Pursell
25
Apa yang Anda maksud dengan "transparan"?
Gbert90
2
@ Gbert90, jika Anda melihat foo (& a) di situs panggilan, Anda tahu foo () menggunakan tipe pointer. Jika Anda melihat foo (a), Anda tidak tahu apakah itu membutuhkan referensi.
Michael J. Davenport
3
@ MichaelJ.Davenport - dalam penjelasan Anda, Anda menyarankan "transparan" untuk berarti sesuatu di sepanjang baris "jelas bahwa penelepon melewati sebuah pointer, tetapi tidak jelas bahwa penelepon memberikan referensi". Dalam posting Johannes, ia mengatakan "Lewati dengan pointer - Caller harus mengambil alamat -> tidak transparan" dan "Lewati dengan referensi - Caller hanya melewati objek -> transparan" - yang hampir berlawanan dengan apa yang Anda katakan . Saya pikir pertanyaan Gbert90 "Apa yang Anda maksud dengan" transparan "" masih valid.
Happy Green Kid Naps
66

Saya suka alasannya dengan artikel dari "cplusplus.com:"

  1. Lewati nilai ketika fungsi tidak ingin memodifikasi parameter dan nilainya mudah untuk disalin (ints, doubles, char, bool, dll ... tipe sederhana. Std :: string, std :: vector, dan semua STL lainnya wadah BUKAN jenis sederhana.)

  2. Lewati oleh pointer const ketika nilainya mahal untuk disalin DAN fungsi tersebut tidak ingin mengubah nilai yang menunjuk ke AND NULL adalah nilai yang valid dan diharapkan yang ditangani oleh fungsi tersebut.

  3. Lewati oleh pointer non-const ketika nilainya mahal untuk menyalin DAN fungsi ingin mengubah nilai menunjuk ke DAN NULL adalah nilai, valid yang diharapkan bahwa fungsi menangani.

  4. Lewati referensi const ketika nilainya mahal untuk disalin DAN fungsi tidak ingin mengubah nilai yang dirujuk DAN NULL tidak akan menjadi nilai yang valid jika pointer digunakan sebagai gantinya.

  5. Lewati referensi non-cont ketika nilainya mahal untuk menyalin DAN fungsi ingin mengubah nilai yang dirujuk ke AND NULL tidak akan menjadi nilai yang valid jika pointer digunakan sebagai gantinya.

  6. Saat menulis fungsi templat, tidak ada jawaban yang jelas karena ada beberapa pertukaran yang perlu dipertimbangkan yang berada di luar cakupan diskusi ini, tetapi cukuplah untuk mengatakan bahwa sebagian besar fungsi templat mengambil parameternya berdasarkan nilai atau referensi (konst) , namun karena sintaks iterator mirip dengan pointer (asterisk to "dereference"), setiap fungsi templat yang mengharapkan iterator sebagai argumen juga akan secara default menerima pointer juga (dan tidak memeriksa NULL karena konsep iterator NULL memiliki sintaks yang berbeda ).

http://www.cplusplus.com/articles/z6vU7k9E/

Apa yang saya ambil dari ini adalah bahwa perbedaan utama antara memilih untuk menggunakan pointer atau parameter referensi adalah jika NULL adalah nilai yang dapat diterima. Itu dia.

Apakah nilainya input, output, dapat dimodifikasi dll. Harus dalam dokumentasi / komentar tentang fungsi, setelah semua.

R. Navega
sumber
Ya, bagi saya persyaratan terkait NULL adalah masalah utama di sini. Terima kasih untuk mengutip ..
binaryguy
62

Allen Holub "Cukup Rope untuk Menembak Diri Anda di Kaki" daftar 2 aturan berikut:

120. Reference arguments should always be `const`
121. Never use references as outputs, use pointers

Dia mencantumkan beberapa alasan mengapa referensi ditambahkan ke C ++:

  • mereka perlu mendefinisikan konstruktor salinan
  • mereka diperlukan untuk kelebihan operator
  • const referensi memungkinkan Anda memiliki semantik nilai per nilai sambil menghindari salinan

Poin utamanya adalah bahwa referensi tidak boleh digunakan sebagai parameter 'output' karena di situs panggilan tidak ada indikasi apakah parameter tersebut merupakan referensi atau parameter nilai. Jadi aturannya adalah hanya menggunakan constreferensi sebagai argumen.

Secara pribadi, saya pikir ini adalah aturan praktis yang baik karena membuatnya lebih jelas ketika suatu parameter adalah parameter keluaran atau tidak. Namun, sementara saya pribadi setuju dengan ini secara umum, saya membiarkan diri saya terpengaruh oleh pendapat orang lain di tim saya jika mereka memperdebatkan parameter output sebagai referensi (beberapa pengembang sangat menyukainya).

Michael Burr
sumber
8
Pendapat saya dalam argumen itu adalah bahwa jika nama fungsi membuatnya benar-benar jelas, tanpa memeriksa dokumen, bahwa param akan diubah, maka referensi non-const OK. Jadi secara pribadi saya akan mengizinkan "getDetails (DetailStruct & result)". Pointer di sana meningkatkan kemungkinan buruk dari input NULL.
Steve Jessop
3
Ini menyesatkan. Bahkan jika beberapa tidak suka referensi, mereka adalah bagian penting dari bahasa dan harus digunakan sebagai itu. Alur penalaran ini seperti mengatakan jangan menggunakan templat, Anda selalu dapat menggunakan wadah kosong * untuk menyimpan jenis apa pun. Bacalah jawaban dengan litb.
David Rodríguez - dribeas
4
Saya tidak melihat bagaimana ini menyesatkan - ada kalanya diperlukan referensi, dan ada kalanya praktik terbaik mungkin menyarankan untuk tidak menggunakannya bahkan jika Anda bisa. Hal yang sama dapat dikatakan untuk fitur bahasa apa pun - pewarisan, teman non-anggota, kelebihan operator, MI, dll ...
Michael Burr
Ngomong-ngomong, saya setuju bahwa jawaban litb sangat baik, dan tentu saja lebih komprehensif daripada yang ini - saya baru saja memilih untuk fokus pada membahas alasan untuk menghindari menggunakan referensi sebagai parameter output.
Michael Burr
1
Aturan ini digunakan di google c ++ style guide: google-styleguide.googlecode.com/svn/trunk/…
Anton Daneyko
9

Klarifikasi ke posting sebelumnya:


Referensi BUKAN jaminan mendapatkan pointer non-null. (Meskipun kita sering memperlakukan mereka seperti itu.)

Walaupun kode tersebut sangat buruk, seperti saat membawa Anda keluar di balik kode buruk woodshed , yang berikut ini akan dikompilasi & dijalankan: (Setidaknya di bawah kompiler saya.)

bool test( int & a)
{
  return (&a) == (int *) NULL;
}

int
main()
{
  int * i = (int *)NULL;
  cout << ( test(*i) ) << endl;
};

Masalah sebenarnya yang saya miliki dengan referensi ada pada programmer lain, untuk selanjutnya disebut IDIOTS , yang mengalokasikan dalam konstruktor, membatalkan alokasi dalam destruktor, dan gagal memasok copy constructor atau operator = ().

Tiba-tiba ada perbedaan antara foo (BAR bar) dan foo (BAR & bar) . (Operasi salin bitwise otomatis dipanggil. Deallokasi di destructor dipanggil dua kali.)

Untungnya kompiler modern akan mengambil alokasi ganda ini dari pointer yang sama. 15 tahun yang lalu, mereka tidak melakukannya. (Di bawah gcc / g ++, gunakan setenv MALLOC_CHECK_ 0 untuk meninjau kembali cara lama.) Menghasilkan, di bawah DEC UNIX, dalam memori yang sama dialokasikan ke dua objek yang berbeda. Banyak kesenangan debugging di sana ...


Lebih praktis:

  • Referensi menyembunyikan bahwa Anda mengubah data yang disimpan di tempat lain.
  • Sangat mudah untuk membingungkan Referensi dengan objek yang disalin.
  • Pointer membuatnya jelas!
Mr.Ree
sumber
16
itu bukan masalah fungsi atau referensi. Anda melanggar aturan bahasa. mendereferensi pointer nol dengan sendirinya sudah perilaku yang tidak terdefinisi. "Referensi BUKAN jaminan mendapatkan pointer non-null.": Standar itu sendiri mengatakan mereka. cara lain merupakan perilaku yang tidak terdefinisi.
Johannes Schaub - litb
1
Saya setuju dengan litb. Meskipun benar, kode yang Anda tunjukkan kepada kami lebih banyak sabotase daripada yang lainnya. Ada beberapa cara untuk menyabotase apa pun, termasuk notasi "referensi" dan "penunjuk".
paercebal
1
Saya memang mengatakan itu "membawa Anda keluar di balik kode buruk gudang kayu"! Dalam nada yang sama, Anda juga dapat memiliki i = FOO baru; hapus saya; test (* i); Terjadi penggantungan / referensi yang lain (sayangnya umum).
Mr.Ree
1
Ini sebenarnya bukan dereferencing NULL yang menjadi masalah, melainkan MENGGUNAKAN objek dereferensi (null). Dengan demikian, benar-benar tidak ada perbedaan (selain sintaksis) antara pointer dan referensi dari perspektif implementasi bahasa. Pengguna yang memiliki harapan berbeda.
Mr.Ree
2
Terlepas dari apa yang Anda lakukan dengan referensi yang dikembalikan, saat Anda mengatakannya *i, program Anda memiliki perilaku yang tidak jelas. Sebagai contoh, kompiler dapat melihat kode ini dan menganggap "OK, kode ini memiliki perilaku yang tidak terdefinisi di semua jalur kode, sehingga seluruh fungsi ini tidak dapat dijangkau." Maka akan diasumsikan bahwa semua cabang yang mengarah ke fungsi ini tidak diambil. Ini adalah optimasi yang dilakukan secara rutin.
David Stone
5

Sebagian besar jawaban di sini gagal mengatasi ambiguitas yang melekat dalam memiliki pointer mentah dalam tanda tangan fungsi, dalam hal mengekspresikan niat. Masalahnya adalah sebagai berikut:

  • Penelepon tidak tahu apakah penunjuk menunjuk ke objek tunggal, atau ke awal "array" objek.

  • Penelepon tidak tahu apakah pointer "memiliki" memori yang ditunjuknya. Yaitu apakah fungsi tersebut harus membebaskan memori atau tidak. ( foo(new int)- Apakah ini kebocoran memori?).

  • Penelepon tidak tahu apakah nullptrbisa dengan aman masuk ke dalam fungsi.

Semua masalah ini diselesaikan dengan referensi:

  • Referensi selalu merujuk ke satu objek.

  • Referensi tidak pernah memiliki ingatan yang mereka rujuk, mereka hanyalah pandangan ke ingatan.

  • Referensi tidak boleh nol.

Ini menjadikan referensi kandidat yang jauh lebih baik untuk penggunaan umum. Namun, referensi tidak sempurna - ada beberapa masalah besar yang perlu dipertimbangkan.

  • Tidak ada tipuan eksplisit. Ini bukan masalah dengan pointer mentah, karena kita harus menggunakan &operator untuk menunjukkan bahwa kita memang melewati pointer. Sebagai contoh, int a = 5; foo(a);Tidak jelas sama sekali di sini bahwa a sedang disahkan oleh referensi dan dapat dimodifikasi.
  • Nullability. Kelemahan pointer ini juga bisa menjadi kekuatan, ketika kita benar - benar ingin referensi kita menjadi nol. Melihat sebagai std::optional<T&>tidak valid (untuk alasan yang baik), petunjuk memberi kami nullability yang Anda inginkan.

Jadi sepertinya ketika kita menginginkan referensi yang dapat dibatalkan dengan tipuan eksplisit, kita harus meraih T*hak? Salah!

Abstraksi

Dalam keputus-asaan kita untuk nullability, kita dapat meraih T*, dan mengabaikan semua kekurangan dan ambiguitas semantik yang disebutkan sebelumnya. Sebagai gantinya, kita harus meraih apa yang paling baik dilakukan C ++: abstraksi. Jika kita hanya menulis kelas yang membungkus sebuah pointer, kita memperoleh ekspresif, serta nullability dan tipuan eksplisit.

template <typename T>
struct optional_ref {
  optional_ref() : ptr(nullptr) {}
  optional_ref(T* t) : ptr(t) {}
  optional_ref(std::nullptr_t) : ptr(nullptr) {}

  T& get() const {
    return *ptr;
  }

  explicit operator bool() const {
    return bool(ptr);
  }

private:
  T* ptr;
};

Ini adalah antarmuka paling sederhana yang bisa saya buat, tapi itu berfungsi dengan efektif. Ini memungkinkan untuk menginisialisasi referensi, memeriksa apakah suatu nilai ada dan mengakses nilai tersebut. Kita bisa menggunakannya seperti ini:

void foo(optional_ref<int> x) {
  if (x) {
    auto y = x.get();
    // use y here
  }
}

int x = 5;
foo(&x); // explicit indirection here
foo(nullptr); // nullability

Kami telah mencapai tujuan kami! Sekarang mari kita lihat manfaatnya, dibandingkan dengan pointer mentah.

  • Antarmuka menunjukkan dengan jelas bahwa referensi hanya merujuk pada satu objek.
  • Jelas itu tidak memiliki memori yang dimaksud, karena tidak memiliki destruktor yang ditentukan pengguna dan tidak ada metode untuk menghapus memori.
  • Pemanggil tahu nullptrdapat diteruskan, karena fungsi penulis secara eksplisit memintaoptional_ref

Kita bisa membuat antarmuka lebih kompleks dari sini, seperti menambahkan operator kesetaraan, monadik get_ordan mapantarmuka, metode yang mendapatkan nilai atau melempar pengecualian, constexprdukungan. Itu bisa dilakukan oleh Anda.

Sebagai kesimpulan, alih-alih menggunakan pointer mentah, alasan tentang apa yang sebenarnya dimaksud pointer dalam kode Anda, dan baik memanfaatkan abstraksi perpustakaan standar atau menulis milik Anda. Ini akan meningkatkan kode Anda secara signifikan.

Tom VH
sumber
3

Tidak juga. Secara internal, melewati dengan referensi dilakukan dengan dasarnya melewati alamat objek yang direferensikan. Jadi, sebenarnya tidak ada keuntungan efisiensi yang bisa didapat dengan melewatkan sebuah pointer.

Melewati dengan referensi memang memiliki satu manfaat. Anda dijamin memiliki instance dari objek / tipe apa pun yang sedang dilewati. Jika Anda meneruskan sebuah pointer, maka Anda berisiko mengambil pointer NULL. Dengan menggunakan pass-by-reference, Anda mendorong pemeriksaan NULL implisit satu tingkat ke pemanggil fungsi Anda.

Brian
sumber
1
Itulah keuntungan dan kerugian. Banyak API menggunakan pointer NULL untuk mengartikan sesuatu yang bermanfaat (mis. NULL timespec menunggu selamanya, sementara nilainya berarti menunggu selama itu).
Greg Rogers
1
@ Brian: Saya tidak ingin menjadi orang yang suka mengambil risiko tetapi: Saya tidak akan mengatakan ada yang dijamin untuk mendapatkan contoh ketika mendapatkan referensi. Referensi yang menggantung masih dimungkinkan jika pemanggil suatu fungsi menghapus referensi dari sebuah pointer yang menggantung, yang tidak dapat diketahui oleh callee.
foraidt
kadang-kadang Anda bahkan dapat memperoleh kinerja dengan menggunakan referensi, karena mereka tidak perlu mengambil penyimpanan apa pun dan tidak memiliki alamat yang ditugaskan untuk diri mereka sendiri. tidak diperlukan tipuan.
Johannes Schaub - litb
Program yang berisi referensi yang menggantung tidak valid C ++. Oleh karena itu, ya, kode dapat mengasumsikan bahwa semua referensi valid.
Konrad Rudolph
2
Saya pasti dapat melakukan dereferensi pointer nol dan kompiler tidak akan dapat memberi tahu ... jika kompiler tidak dapat mengatakan itu "tidak valid C ++", apakah itu benar-benar tidak valid?
rmeador