Có lợi ích gì khi chuyển bằng con trỏ qua chuyển qua tham chiếu trong C ++ không?


225

Những lợi ích của việc chuyển bằng con trỏ qua chuyển qua tham chiếu trong C ++ là gì?

Gần đây, tôi đã thấy một số ví dụ chọn truyền các đối số hàm bằng con trỏ thay vì chuyển qua tham chiếu. Có lợi ích để làm điều này?

Thí dụ:

func(SPRITE *x);

với một cuộc gọi của

func(&mySprite);

so với

func(SPRITE &x);

với một cuộc gọi của

func(mySprite);

Đừng quên newtạo con trỏ và các vấn đề về quyền sở hữu.
Martin York

Câu trả lời:


216

Một con trỏ có thể nhận được một tham số NULL, một tham số không thể. Nếu có cơ hội bạn có thể muốn vượt qua "không có đối tượng", thì hãy sử dụng một con trỏ thay vì tham chiếu.

Ngoài ra, chuyển qua con trỏ cho phép bạn nhìn rõ ràng tại trang web cuộc gọi cho dù đối tượng được truyền theo giá trị hoặc bằng tham chiếu:

// 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);

18
Câu trả lời không đầy đủ. Sử dụng con trỏ sẽ không cho phép sử dụng các đối tượng tạm thời / được thăng cấp, cũng không sử dụng đối tượng nhọn làm đối tượng giống như ngăn xếp. Và nó sẽ gợi ý rằng đối số có thể là NULL khi, hầu hết thời gian, giá trị NULL nên bị cấm. Đọc câu trả lời của litb để có câu trả lời hoàn chỉnh.
paercebal

Cuộc gọi chức năng thứ hai được sử dụng để được chú thích func2 passes by reference. Mặc dù tôi đánh giá cao rằng bạn có nghĩa là nó vượt qua "bằng cách tham chiếu" từ phối cảnh cấp cao, được thực hiện bằng cách chuyển một con trỏ ở góc độ mã, điều này rất khó hiểu (xem stackoverflow.com/questions/13382356/ .).
Các cuộc đua nhẹ nhàng trong quỹ đạo

Tôi không mua cái này. Có, bạn truyền vào một con trỏ, do đó, nó phải là một tham số đầu ra, bởi vì những gì được chỉ ra không thể là const?
deworde

Chúng ta không có tài liệu tham khảo này trong C? Tôi đang sử dụng codeblock (mingw) phiên bản mới nhất và trong đó chọn dự án C. Vẫn chuyển qua tham chiếu (func (int & a)) hoạt động. Hoặc nó có sẵn trong C99 hoặc C11 trở đi?
Jon Wheelock

1
@JonWheelock: Không, C hoàn toàn không có tài liệu tham khảo. func(int& a)C không hợp lệ trong bất kỳ phiên bản nào của tiêu chuẩn. Bạn có thể đang biên dịch các tệp của bạn dưới dạng C ++ một cách tình cờ.
Adam Rosenfield

245

Đi qua con trỏ

  • Người gọi phải lấy địa chỉ -> không minh bạch
  • Giá trị 0 có thể được cung cấp có nghĩa nothing. Điều này có thể được sử dụng để cung cấp các đối số tùy chọn.

Đi qua tham khảo

  • Người gọi chỉ cần vượt qua đối tượng -> trong suốt. Phải được sử dụng cho quá tải toán tử, vì quá tải cho các loại con trỏ là không thể (con trỏ là loại dựng sẵn). Vì vậy, bạn không thể làm string s = &str1 + &str2; sử dụng con trỏ.
  • Không có giá trị 0 nào -> Hàm được gọi không phải kiểm tra chúng
  • Tham chiếu đến const cũng chấp nhận tạm thời: void f(const T& t); ... f(T(a, b, c)); , con trỏ không thể được sử dụng như thế vì bạn không thể lấy địa chỉ tạm thời.
  • Cuối cùng nhưng không kém phần quan trọng, các tài liệu tham khảo dễ sử dụng hơn -> ít cơ hội hơn cho các lỗi.

7
Đi qua con trỏ cũng làm tăng 'Quyền sở hữu có được chuyển giao hay không?' câu hỏi Đây không phải là trường hợp với tài liệu tham khảo.
Frerich Raabe

43
Tôi không đồng ý với "ít cơ hội cho lỗi". Khi kiểm tra trang web cuộc gọi và người đọc thấy "foo (& s)", ngay lập tức rõ ràng rằng s có thể được sửa đổi. Khi bạn đọc "foo (s)" thì không rõ ràng nếu s có thể được sửa đổi. Đây là một nguồn chính của lỗi. Có lẽ ít có khả năng xảy ra một loại lỗi nhất định, nhưng nhìn chung, việc chuyển qua tham chiếu là một nguồn lỗi rất lớn.
William Pursell

25
Bạn có ý nghĩa gì bởi "minh bạch"?
Gbert90

2
@ Gbert90, nếu bạn thấy foo (& a) tại trang web cuộc gọi, bạn biết foo () có một loại con trỏ. Nếu bạn thấy foo (a), bạn không biết liệu nó có tham chiếu hay không.
Michael J. Davenport

3
@ MichaelJ.Davenport - trong phần giải thích của bạn, bạn đề xuất "minh bạch" có nghĩa là một cái gì đó dọc theo dòng chữ "hiển nhiên rằng người gọi đang truyền một con trỏ, nhưng không rõ ràng rằng người gọi đang chuyển một tham chiếu". Trong bài đăng của anh ấy, anh ấy nói "Đi qua con trỏ - Người gọi phải lấy địa chỉ -> không minh bạch" và "Chuyển qua tham chiếu - Người gọi chỉ chuyển đối tượng -> trong suốt" - gần như trái ngược với những gì bạn nói . Tôi nghĩ câu hỏi của Gbert90 "Ý của bạn là gì" minh bạch "vẫn còn hiệu lực.
Giấc ngủ trẻ em xanh vui vẻ

66

Tôi thích lý do bởi một bài viết từ "cplusplus.com:"

  1. Truyền theo giá trị khi hàm không muốn sửa đổi tham số và giá trị dễ sao chép (ints, double, char, bool, v.v ... các loại đơn giản. Std :: string, std :: vector và tất cả các STL khác container KHÔNG phải là loại đơn giản.)

  2. Chuyển qua con trỏ const khi giá trị đắt để sao chép VÀ hàm không muốn sửa đổi giá trị được trỏ đến AND NULL là giá trị hợp lệ, được mong đợi mà hàm xử lý.

  3. Truyền bằng con trỏ không const khi giá trị đắt để sao chép VÀ hàm muốn sửa đổi giá trị được trỏ đến AND NULL là giá trị được mong đợi, hợp lệ mà hàm xử lý.

  4. Chuyển qua tham chiếu const khi giá trị đắt để sao chép VÀ hàm không muốn sửa đổi giá trị được đề cập VÀ NULL sẽ không phải là giá trị hợp lệ nếu sử dụng con trỏ thay thế.

  5. Chuyển qua tham chiếu không liên quan khi giá trị đắt để sao chép VÀ hàm muốn sửa đổi giá trị được đề cập VÀ NULL sẽ không phải là giá trị hợp lệ nếu sử dụng con trỏ thay thế.

  6. Khi viết các hàm mẫu, không có câu trả lời rõ ràng vì có một vài sự đánh đổi để xem xét nằm ngoài phạm vi của cuộc thảo luận này, nhưng đủ để nói rằng hầu hết các hàm mẫu lấy tham số của chúng theo giá trị hoặc tham chiếu (const) , tuy nhiên vì cú pháp của trình lặp tương tự như cú pháp của con trỏ (dấu hoa thị cho "dereference"), nên bất kỳ hàm mẫu nào mong đợi các trình lặp như là đối số cũng sẽ mặc định chấp nhận con trỏ (và không kiểm tra NULL vì khái niệm Trình lặp NULL có cú pháp khác ).

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

Điều tôi nhận được từ điều này là sự khác biệt chính giữa việc chọn sử dụng một con trỏ hoặc tham số tham chiếu là nếu NULL là một giá trị chấp nhận được. Đó là nó.

Cho dù giá trị là đầu vào, đầu ra, có thể sửa đổi, vv nên được đưa vào tài liệu / nhận xét về chức năng.


Vâng, đối với tôi các điều khoản liên quan đến NULL là mối quan tâm chính ở đây. Thx để trích dẫn ..
binaryguy

62

"Đủ dây để tự bắn vào chân" của Allen Holub liệt kê 2 quy tắc sau:

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

Ông liệt kê một số lý do tại sao các tài liệu tham khảo đã được thêm vào C ++:

  • chúng là cần thiết để xác định các hàm tạo sao chép
  • chúng là cần thiết cho quá tải nhà điều hành
  • const tài liệu tham khảo cho phép bạn có ngữ nghĩa pass-by-value trong khi tránh một bản sao

Quan điểm chính của ông là các tham chiếu không nên được sử dụng làm tham số 'đầu ra' bởi vì tại trang web cuộc gọi không có dấu hiệu nào cho biết tham số đó là tham chiếu hay tham số giá trị. Vì vậy, quy tắc của ông là chỉ sử dụngconst tài liệu tham khảo như là đối số.

Cá nhân, tôi nghĩ rằng đây là một quy tắc tốt vì nó làm cho nó rõ ràng hơn khi một tham số là một tham số đầu ra hay không. Tuy nhiên, trong khi cá nhân tôi đồng ý với điều này nói chung, tôi cho phép bản thân bị ảnh hưởng bởi ý kiến ​​của những người khác trong nhóm của mình nếu họ tranh luận về các tham số đầu ra là tài liệu tham khảo (một số nhà phát triển thích chúng).


8
Quan điểm của tôi trong lập luận đó là nếu tên hàm làm cho nó hoàn toàn rõ ràng, mà không kiểm tra các tài liệu, rằng param sẽ được sửa đổi, thì một tham chiếu không phải là hằng. Vì vậy, cá nhân tôi cho phép "getDetails (Chi tiết & kết quả)". Một con trỏ ở đó làm tăng khả năng xấu của đầu vào NULL.
Steve Jessop

3
Điều này là sai lệch. Ngay cả khi một số người không thích tài liệu tham khảo, chúng là một phần quan trọng của ngôn ngữ và nên được sử dụng như vậy. Dòng lý luận này giống như nói rằng không sử dụng các mẫu bạn luôn có thể sử dụng các thùng chứa void * để lưu trữ bất kỳ loại nào. Đọc câu trả lời của litb.
David Rodríguez - dribeas

4
Tôi không thấy điều này gây hiểu lầm như thế nào - có những lúc cần phải tham khảo và có những lúc thực hành tốt nhất có thể đề nghị không sử dụng chúng ngay cả khi bạn có thể. Điều tương tự cũng có thể nói đối với bất kỳ tính năng nào của ngôn ngữ - kế thừa, bạn bè không phải thành viên, quá tải nhà điều hành, MI, v.v ...
Michael Burr

Nhân tiện, tôi đồng ý rằng câu trả lời của litb là rất tốt, và chắc chắn là toàn diện hơn câu trả lời này - tôi chỉ chọn tập trung thảo luận về một lý do để tránh sử dụng tài liệu tham khảo làm tham số đầu ra.
Michael Burr

1
Quy tắc này được sử dụng trong hướng dẫn về phong cách google c ++: google-styleguide.googlecode.com/svn/trunk/ Kẻ
Anton Daneyko

9

Làm rõ các bài viết trước:


Tài liệu tham khảo KHÔNG là sự đảm bảo để có được một con trỏ không null. (Mặc dù chúng ta thường đối xử với họ như vậy.)

Trong khi mã xấu khủng khiếp, như đưa bạn ra đằng sau mã xấu bị khắc gỗ , phần sau đây sẽ biên dịch và chạy: (Ít nhất là dưới trình biên dịch của tôi.)

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

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

Vấn đề thực sự tôi gặp phải với các tài liệu tham khảo nằm ở các lập trình viên khác, từ đó được gọi là IDIOTS , người phân bổ trong hàm tạo, phân bổ trong hàm hủy và không cung cấp hàm tạo hoặc toán tử sao chép = ().

Đột nhiên, có một thế giới khác biệt giữa foo (thanh BAR)foo (BAR & bar) . (Hoạt động sao chép bitwise tự động được gọi. Giao dịch trong hàm hủy được gọi hai lần.)

Rất may các trình biên dịch hiện đại sẽ nhận được sự phân chia kép này của cùng một con trỏ. 15 năm trước, họ đã không làm thế. (Trong gcc / g ++, sử dụng setenv MALLOC_CHECK_ 0 để xem lại các cách cũ.) Kết quả, theo DEC UNIX, trong cùng một bộ nhớ được phân bổ cho hai đối tượng khác nhau. Có rất nhiều trò vui gỡ lỗi ở đó ...


Thực tế hơn:

  • Tài liệu tham khảo ẩn rằng bạn đang thay đổi dữ liệu được lưu trữ ở một nơi khác.
  • Thật dễ dàng để nhầm lẫn một Tài liệu tham khảo với một đối tượng Sao chép.
  • Con trỏ làm cho nó rõ ràng!

16
Đó không phải là vấn đề của chức năng hoặc tài liệu tham khảo. bạn đang phá vỡ quy tắc ngôn ngữ. tự hủy bỏ một con trỏ null là hành vi chưa được xác định. "Tài liệu tham khảo KHÔNG phải là sự đảm bảo để có được một con trỏ không null.": Bản thân tiêu chuẩn nói rằng chúng là. những cách khác cấu thành hành vi không xác định.
Julian Schaub - litb

1
Tôi đồng ý với litb. Mặc dù đúng, mã bạn đang hiển thị cho chúng tôi bị phá hoại nhiều hơn bất kỳ thứ gì khác. Có nhiều cách để phá hoại bất cứ điều gì, bao gồm cả các ký hiệu "tham chiếu" và "con trỏ".
paercebal

1
Tôi đã nói rằng đó là "đưa bạn ra đằng sau mã xấu"! Trong cùng một hướng, bạn cũng có thể có i = new FOO; xóa i; kiểm tra (* i); Một con trỏ khác (không may phổ biến) lơ lửng / tham chiếu xảy ra.
Mr.Ree

1
Nó thực sự không dereferencing NULL đó là vấn đề, mà là SỬ DỤNG rằng dereferenced (null) đối tượng. Như vậy, thực sự không có sự khác biệt (ngoài cú pháp) giữa các con trỏ và các tham chiếu từ góc độ thực hiện ngôn ngữ. Đó là những người dùng có những kỳ vọng khác nhau.
Mr.Ree

2
Bất kể những gì bạn làm với tham chiếu được trả về, thời điểm bạn nói *i, chương trình của bạn có hành vi không xác định. Chẳng hạn, trình biên dịch có thể thấy mã này và giả sử "OK, mã này có hành vi không xác định trong tất cả các đường dẫn mã, vì vậy toàn bộ chức năng này phải không thể truy cập được." Sau đó, nó sẽ cho rằng tất cả các nhánh dẫn đến chức năng này không được thực hiện. Đây là một tối ưu hóa được thực hiện thường xuyên.
David Stone

5

Hầu hết các câu trả lời ở đây không giải quyết được sự mơ hồ vốn có trong việc có một con trỏ thô trong chữ ký hàm, về mặt thể hiện ý định. Các vấn đề như sau:

  • Người gọi không biết liệu con trỏ trỏ đến một đối tượng hay bắt đầu một "mảng" của các đối tượng.

  • Người gọi không biết liệu con trỏ có "sở hữu" bộ nhớ mà nó trỏ tới hay không. IE, có hay không chức năng sẽ giải phóng bộ nhớ. ( foo(new int)- Đây có phải là rò rỉ bộ nhớ không?).

  • Người gọi không biết liệu nullptrcó thể được truyền an toàn vào chức năng hay không.

Tất cả những vấn đề này được giải quyết bằng cách tham khảo:

  • Tài liệu tham khảo luôn đề cập đến một đối tượng duy nhất.

  • Tài liệu tham khảo không bao giờ sở hữu bộ nhớ mà chúng đề cập đến, chúng chỉ đơn thuần là một cái nhìn vào bộ nhớ.

  • Tài liệu tham khảo không thể rỗng.

Điều này làm cho tài liệu tham khảo một ứng cử viên tốt hơn nhiều cho sử dụng chung. Tuy nhiên, tài liệu tham khảo không hoàn hảo - có một vài vấn đề lớn cần xem xét.

  • Không có quyết định rõ ràng. Đây không phải là vấn đề với một con trỏ thô, vì chúng ta phải sử dụng &toán tử để cho thấy rằng chúng ta thực sự đang vượt qua một con trỏ. Ví dụ, int a = 5; foo(a);không rõ ràng ở đây rằng a đang được thông qua tham chiếu và có thể được sửa đổi.
  • Tính không ổn định. Điểm yếu này của con trỏ cũng có thể là một điểm mạnh, khi chúng ta thực sự muốn các tài liệu tham khảo của mình là null. Xem như std::optional<T&>không hợp lệ (vì lý do chính đáng), con trỏ cung cấp cho chúng tôi tính vô hiệu mà bạn muốn.

Vì vậy, có vẻ như khi chúng ta muốn một tài liệu tham khảo vô giá trị với sự gián tiếp rõ ràng, chúng ta nên đạt được một T*quyền? Sai lầm!

Trừu tượng

Trong sự tuyệt vọng của chúng tôi về sự vô hiệu, chúng tôi có thể tiếp cận T*và chỉ cần bỏ qua tất cả những thiếu sót và sự mơ hồ ngữ nghĩa được liệt kê trước đó. Thay vào đó, chúng ta nên tiếp cận với những gì C ++ làm tốt nhất: một sự trừu tượng hóa. Nếu chúng ta chỉ đơn giản viết một lớp bao quanh một con trỏ, chúng ta sẽ có được tính biểu cảm, cũng như tính vô hiệu và không rõ ràng.

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;
};

Đây là giao diện đơn giản nhất tôi có thể nghĩ ra, nhưng nó thực hiện công việc một cách hiệu quả. Nó cho phép khởi tạo tham chiếu, kiểm tra xem một giá trị có tồn tại và truy cập giá trị đó không. Chúng ta có thể sử dụng nó như vậy:

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

Chúng tôi đã đạt được mục tiêu của chúng tôi! Bây giờ chúng ta hãy xem các lợi ích, so với con trỏ thô.

  • Giao diện hiển thị rõ ràng rằng tham chiếu chỉ nên tham chiếu đến một đối tượng.
  • Rõ ràng nó không sở hữu bộ nhớ mà nó đề cập đến, vì nó không có hàm hủy do người dùng định nghĩa và không có phương thức để xóa bộ nhớ.
  • Người gọi biết nullptrcó thể được thông qua, vì tác giả hàm rõ ràng đang yêu cầu mộtoptional_ref

Chúng ta có thể làm cho giao diện phức tạp hơn từ đây, chẳng hạn như thêm toán tử đẳng thức, giao diện đơn get_ormapgiao diện, một phương thức nhận giá trị hoặc ném ngoại lệ,constexpr hỗ trợ. Điều đó có thể được thực hiện bởi bạn.

Tóm lại, thay vì sử dụng các con trỏ thô, hãy suy luận về ý nghĩa của những con trỏ đó trong mã của bạn và tận dụng sự trừu tượng của thư viện tiêu chuẩn hoặc viết riêng của bạn. Điều này sẽ cải thiện mã của bạn đáng kể.


3

Không hẳn vậy. Trong nội bộ, chuyển qua tham chiếu được thực hiện bằng cách chủ yếu chuyển địa chỉ của đối tượng được tham chiếu. Vì vậy, thực sự không có bất kỳ lợi ích hiệu quả nào có được bằng cách vượt qua một con trỏ.

Đi qua tham chiếu có một lợi ích, tuy nhiên. Bạn được đảm bảo có một thể hiện của bất kỳ đối tượng / loại nào đang được truyền vào. Nếu bạn truyền vào một con trỏ, thì bạn có nguy cơ nhận được một con trỏ NULL. Bằng cách sử dụng tham chiếu qua, bạn đang đẩy một NULL ẩn - kiểm tra một cấp cho người gọi hàm của bạn.


1
Đó là cả một lợi thế và bất lợi. Nhiều API sử dụng các con trỏ NULL có nghĩa là một cái gì đó hữu ích (ví dụ: NULL timespec đợi mãi mãi, trong khi giá trị có nghĩa là chờ đợi lâu).
Greg Rogers

1
@Brian: Tôi không muốn chọn nit nhưng: Tôi sẽ không nói người ta được đảm bảo lấy một ví dụ khi nhận được tài liệu tham khảo. Các tham chiếu lơ lửng vẫn có thể nếu người gọi hàm hủy tham chiếu một con trỏ lơ lửng, mà callee không thể biết.
trả trước

đôi khi bạn thậm chí có thể đạt được hiệu suất bằng cách sử dụng tài liệu tham khảo, vì họ không cần phải có bất kỳ lưu trữ nào và không có bất kỳ địa chỉ nào được chỉ định cho chính họ. không yêu cầu
Julian Schaub - litb

Các chương trình chứa tham chiếu lơ lửng không hợp lệ C ++. Do đó, vâng, mã có thể cho rằng tất cả các tham chiếu là hợp lệ.
Konrad Rudolph

2
Tôi chắc chắn có thể hủy bỏ một con trỏ null và trình biên dịch sẽ không thể nói ... nếu trình biên dịch không thể nói nó là "C ++ không hợp lệ", nó có thực sự không hợp lệ không?
rmeador
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.