Tôi nên thích con trỏ hoặc tài liệu tham khảo trong dữ liệu thành viên?


138

Đây là một ví dụ đơn giản để minh họa cho câu hỏi:

class A {};

class B
{
    B(A& a) : a(a) {}
    A& a;
};

class C
{
    C() : b(a) {} 
    A a;
    B b; 
};

Vì vậy, B chịu trách nhiệm cập nhật một phần của C. Tôi đã chạy mã thông qua lint và nó đã nói về thành viên tham chiếu: lint # 1725 . Điều này nói về việc chăm sóc bản sao mặc định và bài tập đủ công bằng, nhưng bản sao và bài tập mặc định cũng không tốt với con trỏ, vì vậy có rất ít lợi thế ở đó.

Tôi luôn cố gắng sử dụng các tài liệu tham khảo nơi tôi có thể kể từ khi con trỏ trần giới thiệu không chắc chắn về người chịu trách nhiệm xóa con trỏ đó. Tôi thích nhúng các đối tượng theo giá trị nhưng nếu tôi cần một con trỏ, tôi sử dụng auto_ptr trong dữ liệu thành viên của lớp sở hữu con trỏ và truyền đối tượng xung quanh làm tham chiếu.

Tôi thường chỉ sử dụng một con trỏ trong dữ liệu thành viên khi con trỏ có thể là null hoặc có thể thay đổi. Có bất kỳ lý do nào khác để thích con trỏ hơn các tham chiếu cho các thành viên dữ liệu không?

Có đúng không khi nói rằng một đối tượng có chứa một tham chiếu không nên được gán, vì một tham chiếu không nên được thay đổi một khi được khởi tạo?


"nhưng sao chép và gán mặc định cũng không tốt với con trỏ": Điều này không giống nhau: Một con trỏ có thể được thay đổi bất cứ khi nào bạn muốn nếu nó không phải là const. Một tài liệu tham khảo thường là const! (Bạn sẽ thấy điều này nếu bạn cố gắng thay đổi thành viên của mình thành "A & const a;". Trình biên dịch (ít nhất là GCC) sẽ cảnh báo bạn rằng dù sao thì tham chiếu vẫn không có từ khóa "const".
mmmmmmmm

Vấn đề chính với điều này là nếu ai đó làm B b (A ()), bạn sẽ bị lừa bởi vì bạn đang kết thúc với một tài liệu tham khảo lơ lửng.
đăng nhập

Câu trả lời:


68

Tránh các thành viên tham chiếu, vì họ hạn chế việc triển khai một lớp có thể làm gì (bao gồm, như bạn đã đề cập, ngăn chặn việc triển khai toán tử gán) và không cung cấp lợi ích nào cho những gì lớp có thể cung cấp.

Các vấn đề mẫu:

  • bạn buộc phải khởi tạo tham chiếu trong danh sách trình khởi tạo của mỗi nhà xây dựng: không có cách nào để đưa yếu tố khởi tạo này vào một chức năng khác ( cho đến khi C ++ 0x, dù sao cũng chỉnh sửa: C ++ hiện đã ủy quyền cho các nhà xây dựng )
  • tài liệu tham khảo không thể được phục hồi hoặc là null. Đây có thể là một lợi thế, nhưng nếu mã cần thay đổi để cho phép rebinding hoặc cho thành viên là null, tất cả các cách sử dụng của thành viên cần phải thay đổi
  • Không giống như các thành viên con trỏ, các tham chiếu không thể dễ dàng được thay thế bằng các con trỏ hoặc trình lặp thông minh vì việc tái cấu trúc có thể yêu cầu
  • Bất cứ khi nào một tham chiếu được sử dụng, nó trông giống như kiểu giá trị ( .toán tử, v.v.), nhưng hoạt động như một con trỏ (có thể treo lủng lẳng) - ví dụ: Google Style Guide không khuyến khích nó

65
Tất cả những điều bạn đã đề cập là những điều tốt để tránh, vì vậy nếu tài liệu tham khảo giúp trong việc này - chúng tốt và không xấu. Danh sách khởi tạo là nơi tốt nhất để khởi tạo dữ liệu. Rất thường xuyên bạn phải ẩn toán tử gán, với các tham chiếu bạn không phải. "Không thể phục hồi" - tốt, sử dụng lại các biến là một ý tưởng tồi.
Mykola Golubyev

4
@Mykola: Tôi đồng ý với bạn. Tôi thích các thành viên được khởi tạo trong danh sách khởi tạo, tôi tránh các con trỏ null và chắc chắn sẽ không tốt khi một biến thay đổi ý nghĩa của nó. Trường hợp ý kiến ​​của chúng tôi khác nhau là tôi không cần hoặc không muốn trình biên dịch thực thi điều này cho tôi. Nó không làm cho các lớp dễ viết hơn, tôi đã không gặp phải các lỗi trong lĩnh vực mà nó sẽ bắt được và đôi khi tôi đánh giá cao sự linh hoạt mà tôi có được từ mã luôn sử dụng các thành viên con trỏ (con trỏ thông minh khi thích hợp).
James Hopkin

Tôi nghĩ rằng không phải ẩn bài tập là một đối số không có thật vì bạn không phải ẩn bài tập nếu lớp có chứa một con trỏ mà nó không chịu trách nhiệm xóa bằng mọi cách - mặc định sẽ làm. Tôi nghĩ rằng đây là câu trả lời tốt nhất vì nó đưa ra lý do chính đáng tại sao con trỏ nên được ưu tiên hơn các tham chiếu trong dữ liệu thành viên.
markh44

4
James, không phải nó làm cho mã dễ viết hơn mà nó làm cho mã dễ đọc hơn. Với một tham chiếu là một thành viên dữ liệu, bạn không bao giờ phải tự hỏi liệu nó có thể là null tại điểm sử dụng hay không. Điều này có nghĩa là bạn có thể xem mã với ít ngữ cảnh hơn.
Len Holgate

4
Nó làm cho việc kiểm tra đơn vị và chế nhạo trở nên khó khăn hơn nhiều , gần như không thể phụ thuộc vào số lượng được sử dụng, kết hợp với 'ảnh hưởng đến vụ nổ đồ thị' là IMHO đủ lý do để không bao giờ sử dụng chúng.
Chris Huang-Leaver

155

Quy tắc riêng của tôi:

  • Sử dụng một thành viên tham chiếu khi bạn muốn cuộc sống của đối tượng của mình phụ thuộc vào cuộc sống của các đối tượng khác : đó là một cách rõ ràng để nói rằng bạn không cho phép đối tượng tồn tại mà không có một thể hiện hợp lệ của lớp khác - vì không chuyển nhượng và nghĩa vụ để có được khởi tạo tham chiếu thông qua hàm tạo. Đó là một cách tốt để thiết kế lớp của bạn mà không giả sử bất kỳ điều gì về trường hợp đó là thành viên hay không thuộc lớp khác. Bạn chỉ cho rằng cuộc sống của họ được liên kết trực tiếp với các trường hợp khác. Nó cho phép bạn thay đổi sau này cách bạn sử dụng thể hiện lớp của mình (với phiên bản mới, như một thể hiện cục bộ, như một thành viên lớp, được tạo bởi một nhóm bộ nhớ trong trình quản lý, v.v.)
  • Sử dụng con trỏ trong các trường hợp khác : Khi bạn muốn thay đổi thành viên sau này, hãy sử dụng một con trỏ hoặc con trỏ const để chắc chắn chỉ đọc thể hiện nhọn. Nếu loại đó được coi là có thể sao chép, bạn không thể sử dụng tài liệu tham khảo nào. Đôi khi bạn cũng cần khởi tạo thành viên sau một lệnh gọi hàm đặc biệt (init () chẳng hạn) và sau đó bạn đơn giản không có lựa chọn nào khác ngoài sử dụng một con trỏ. NHƯNG: sử dụng các xác nhận trong tất cả các chức năng thành viên của bạn để nhanh chóng phát hiện trạng thái con trỏ sai!
  • Trong trường hợp bạn muốn tuổi thọ của đối tượng phụ thuộc vào tuổi thọ của đối tượng bên ngoài và bạn cũng cần loại đó có thể sao chép được, sau đó sử dụng các thành viên con trỏ nhưng đối số tham chiếu trong hàm tạo Theo cách bạn đang chỉ ra khi xây dựng rằng thời gian tồn tại của đối tượng này phụ thuộc trên đời của đối số NHƯNG việc sử dụng các con trỏ vẫn có thể sao chép được. Miễn là các thành viên này chỉ được thay đổi bằng bản sao và loại của bạn không có hàm tạo mặc định, loại sẽ hoàn thành cả hai mục tiêu.

1
Tôi nghĩ rằng của bạn và Mykola là câu trả lời chính xác và thúc đẩy lập trình con trỏ (đọc ít con trỏ).
amit

37

Đối tượng hiếm khi nên cho phép gán và các công cụ khác như so sánh. Nếu bạn xem xét một số mô hình kinh doanh với các đối tượng như 'Bộ phận', 'Nhân viên', 'Giám đốc', thật khó để tưởng tượng một trường hợp khi một nhân viên sẽ được chỉ định cho người khác.

Vì vậy, đối với các đối tượng kinh doanh, rất tốt để mô tả các mối quan hệ một-một và một-nhiều với tư cách là tài liệu tham khảo và không phải là con trỏ.

Và có lẽ việc mô tả mối quan hệ một hoặc không là một con trỏ là ổn.

Vì vậy, không 'chúng ta không thể chỉ định' sau đó yếu tố.
Rất nhiều lập trình viên chỉ quen với con trỏ và đó là lý do tại sao họ sẽ tìm thấy bất kỳ đối số nào để tránh sử dụng tham chiếu.

Có một con trỏ là thành viên sẽ buộc bạn hoặc thành viên trong nhóm của bạn kiểm tra con trỏ nhiều lần trước khi sử dụng, với nhận xét "chỉ trong trường hợp". Nếu một con trỏ có thể bằng 0 thì con trỏ có thể được sử dụng như một loại cờ, điều này rất tệ, vì mọi đối tượng đều phải đóng vai trò riêng của nó.


2
+1: Tương tự tôi đã trải nghiệm. Chỉ một số lớp lưu trữ dữ liệu chung mới cần gánemtn và copy-c'tor. Và đối với các khung đối tượng kinh doanh cấp cao hơn, cần có các kỹ thuật sao chép khác để xử lý các công cụ như "không sao chép các trường duy nhất" và "thêm vào cha mẹ khác", v.v. Vì vậy, bạn sử dụng khung công tác cấp cao để sao chép các đối tượng kinh doanh của mình và không cho phép các bài tập cấp thấp.
mmmmmmmm

2
+1: Một số điểm của thỏa thuận: các thành viên tham chiếu ngăn toán tử gán được xác định không phải là một đối số quan trọng. Khi bạn nêu chính xác một cách khác nhau, không phải tất cả các đối tượng đều có giá trị ngữ nghĩa. Tôi cũng đồng ý rằng chúng tôi không muốn 'nếu (p)' phân tán khắp mã mà không có lý do hợp lý. Nhưng cách tiếp cận đúng với điều đó là thông qua các bất biến của lớp: một lớp được xác định rõ sẽ không còn chỗ để nghi ngờ về việc liệu các thành viên có thể là null hay không. Nếu bất kỳ con trỏ nào có thể là null trong mã, tôi mong rằng nó sẽ được nhận xét tốt.
James Hopkin

@JamesHopkin Tôi đồng ý rằng các lớp được thiết kế tốt có thể sử dụng con trỏ một cách hiệu quả. Bên cạnh việc bảo vệ chống lại NULL, các tham chiếu cũng đảm bảo rằng tham chiếu của bạn đã được khởi tạo (một con trỏ không cần phải khởi tạo, vì vậy nó có thể trỏ đến bất kỳ thứ gì khác). Tài liệu tham khảo cũng cho bạn biết về quyền sở hữu - cụ thể là đối tượng không sở hữu thành viên. Nếu bạn luôn sử dụng tài liệu tham khảo cho các thành viên tổng hợp, bạn sẽ có độ tin cậy rất cao rằng các thành viên con trỏ là đối tượng tổng hợp (mặc dù không có nhiều trường hợp thành viên hỗn hợp được biểu thị bằng một con trỏ).
weberc2


6

Trong một vài trường hợp quan trọng, đơn giản là không cần thiết. Đây thường là các hàm bao thuật toán nhẹ, tạo điều kiện cho việc tính toán mà không cần rời khỏi phạm vi. Các đối tượng như vậy là ứng cử viên chính cho các thành viên tham chiếu vì bạn có thể chắc chắn rằng họ luôn giữ một tham chiếu hợp lệ và không bao giờ cần phải sao chép.

Trong các trường hợp như vậy, đảm bảo làm cho toán tử gán (và thường là hàm tạo sao chép) không thể sử dụng được (bằng cách kế thừa từ boost::noncopyablehoặc khai báo chúng ở chế độ riêng tư).

Tuy nhiên, như pts người dùng đã nhận xét, điều tương tự không đúng với hầu hết các đối tượng khác. Ở đây, sử dụng các thành viên tham chiếu có thể là một vấn đề lớn và thường nên tránh.


6

Vì mọi người dường như đang đưa ra các quy tắc chung, tôi sẽ đưa ra hai:

  • Không bao giờ, sử dụng tài liệu tham khảo như các thành viên lớp. Tôi chưa bao giờ làm như vậy trong mã của riêng tôi (ngoại trừ để chứng minh với bản thân rằng tôi đã đúng trong quy tắc này) và không thể tưởng tượng được trường hợp tôi sẽ làm như vậy. Các ngữ nghĩa là quá khó hiểu, và nó thực sự không phải là tài liệu tham khảo được thiết kế cho.

  • Luôn luôn, luôn luôn, sử dụng các tham chiếu khi truyền tham số cho các hàm, ngoại trừ các loại cơ bản hoặc khi thuật toán yêu cầu một bản sao.

Các quy tắc này là đơn giản, và đã giúp tôi đứng vững. Tôi để lại quy tắc sử dụng con trỏ thông minh (nhưng xin vui lòng, không phải auto_ptr) làm thành viên lớp cho người khác.


2
Đừng hoàn toàn đồng ý về điều đầu tiên, nhưng sự không đồng ý không phải là vấn đề để coi trọng lý do. +1
David Rodríguez - dribeas

(Mặc dù chúng tôi dường như đang mất lập luận này với các cử tri giỏi của SO)
James Hopkin

2
Tôi đã bỏ phiếu vì "Không bao giờ, không bao giờ sử dụng tài liệu tham khảo với tư cách là thành viên trong lớp" và những lời biện minh sau đây không phù hợp với tôi. Như đã giải thích trong câu trả lời của tôi (và nhận xét về câu trả lời hàng đầu) và từ kinh nghiệm của riêng tôi, có những trường hợp đơn giản là một điều tốt. Tôi đã nghĩ như bạn cho đến khi tôi được thuê trong một công ty nơi họ đang sử dụng nó một cách tốt và điều đó cho tôi thấy rõ rằng có những trường hợp nó giúp ích. Trong thực tế, tôi nghĩ rằng vấn đề thực sự liên quan nhiều hơn đến cú pháp và hàm ý ẩn khiến việc sử dụng các tài liệu tham khảo khi các thành viên yêu cầu lập trình viên hiểu những gì anh ta đang làm.
Klaim

1
Neil> Tôi đồng ý nhưng đó không phải là "ý kiến" khiến tôi bỏ phiếu, đó là khẳng định "không bao giờ". Vì tranh luận ở đây dường như không hữu ích dù sao tôi sẽ hủy bỏ phiếu bầu của mình.
Klaim

2
Câu trả lời này có thể được cải thiện bằng cách giải thích những gì về ngữ nghĩa thành viên tham khảo là khó hiểu. Cơ sở lý luận này rất quan trọng bởi vì, trên mặt của nó, các con trỏ có vẻ mơ hồ hơn về mặt ngữ nghĩa (chúng có thể không được khởi tạo và chúng không cho bạn biết gì về quyền sở hữu đối tượng cơ bản).
weberc2

4

Có đối với: Có đúng không khi nói rằng một đối tượng chứa tham chiếu không nên được gán, vì tham chiếu không nên được thay đổi sau khi khởi tạo?

Quy tắc ngón tay cái của tôi cho các thành viên dữ liệu:

  • không bao giờ sử dụng một tài liệu tham khảo, bởi vì nó ngăn chặn sự phân công
  • nếu lớp của bạn chịu trách nhiệm xóa, hãy sử dụng boost 'scoped_ptr (an toàn hơn auto_ptr)
  • mặt khác, sử dụng một con trỏ hoặc con trỏ const

2
Tôi thậm chí không thể đặt tên cho một trường hợp khi tôi cần một sự phân công của đối tượng kinh doanh.
Mykola Golubyev

1
+1: Tôi chỉ cần thêm cẩn thận với các thành viên auto_ptr - bất kỳ lớp nào có họ đều phải có sự phân công và sao chép xây dựng được xác định rõ ràng (những người được tạo rất khó có thể là những gì được yêu cầu)
James Hopkin

auto_ptr cũng ngăn chặn sự phân công (hợp lý).

2
@Mykola: Tôi cũng đã trải nghiệm rằng 98% đối tượng kinh doanh của tôi không cần chuyển nhượng hoặc sao chép. Tôi ngăn chặn điều này bằng một macro nhỏ mà tôi thêm vào các tiêu đề của các lớp đó làm cho các bản sao và toán tử sao chép = () không có triển khai. Vì vậy, trong 98% các đối tượng tôi cũng sử dụng tài liệu tham khảo và nó an toàn.
mmmmmmmm

1
Các tham chiếu như các thành viên có thể được sử dụng để nói rõ rằng tuổi thọ của thể hiện của lớp phụ thuộc trực tiếp vào vòng đời của các thể hiện của các lớp khác (hoặc giống nhau). "Không bao giờ sử dụng một tham chiếu, bởi vì nó ngăn chặn sự gán" là giả định rằng tất cả các lớp phải cho phép gán. Nó giống như giả sử rằng tất cả các lớp phải cho phép hoạt động sao chép ...
Klaim

2

Tôi thường chỉ sử dụng một con trỏ trong dữ liệu thành viên khi con trỏ có thể là null hoặc có thể thay đổi. Có bất kỳ lý do nào khác để thích con trỏ hơn các tham chiếu cho các thành viên dữ liệu không?

Đúng. Khả năng đọc mã của bạn. Một con trỏ làm cho rõ ràng hơn rằng thành viên là một tham chiếu (trớ trêu thay :)), và không phải là một đối tượng có chứa, bởi vì khi bạn sử dụng nó, bạn phải hủy tham chiếu nó. Tôi biết một số người nghĩ rằng đó là lỗi thời, nhưng tôi vẫn nghĩ rằng nó chỉ đơn giản là ngăn ngừa sự nhầm lẫn và sai lầm.


1
Lakos cũng lập luận cho điều này trong "Thiết kế phần mềm C ++ quy mô lớn".
Johan Kotlinski

Tôi tin rằng Code Complete cũng ủng hộ điều này và tôi không chống lại nó. Nó không quá hấp dẫn mặc dù ...
markh44

2

Tôi khuyên các thành viên dữ liệu tham khảo vì bạn không bao giờ biết ai sẽ xuất phát từ lớp của bạn và họ có thể muốn làm gì. Họ có thể không muốn sử dụng đối tượng được tham chiếu, nhưng là một tài liệu tham khảo mà bạn đã buộc họ phải cung cấp một đối tượng hợp lệ. Tôi đã làm điều này với bản thân mình đủ để ngừng sử dụng các thành viên dữ liệu tham chiếu.


0

Nếu tôi hiểu chính xác câu hỏi ...

Tham chiếu dưới dạng tham số hàm thay vì con trỏ: Như bạn đã chỉ ra, một con trỏ không làm rõ ai là người sở hữu việc dọn dẹp / khởi tạo con trỏ. Thích một điểm được chia sẻ khi bạn muốn một con trỏ, đó là một phần của C ++ 11 và yếu_ptr khi tính hợp lệ của dữ liệu không được đảm bảo trong suốt thời gian của lớp chấp nhận dữ liệu được trỏ đến.; cũng là một phần của C ++ 11. Sử dụng tham chiếu làm tham số hàm đảm bảo rằng tham chiếu không rỗng. Bạn phải phá vỡ các tính năng ngôn ngữ để khắc phục điều này và chúng tôi không quan tâm đến các lập trình viên pháo lỏng lẻo.

Tham chiếu biến aa thành viên: Như trên liên quan đến tính hợp lệ của dữ liệu. Điều này đảm bảo chỉ vào dữ liệu, sau đó được tham chiếu, là hợp lệ.

Việc chuyển trách nhiệm về tính hợp lệ của biến sang một điểm trước đó trong mã không chỉ dọn sạch mã sau này (lớp A trong ví dụ của bạn), mà còn làm cho người đó sử dụng rõ ràng.

Trong ví dụ của bạn, điều này hơi khó hiểu (tôi thực sự cố gắng tìm cách triển khai rõ ràng hơn), A được B sử dụng được bảo đảm trọn đời của B, vì B là thành viên của A, vì vậy một tài liệu tham khảo củng cố điều này và là (có lẽ) rõ ràng hơn.

Và chỉ trong trường hợp (đó là một trình duyệt có khả năng thấp vì nó sẽ không có ý nghĩa gì trong ngữ cảnh mã của bạn), một cách khác, để sử dụng một tham số không tham chiếu, không con trỏ A, sẽ sao chép A và làm cho mô hình trở nên vô dụng - Tôi thực sự không nghĩ rằng bạn có nghĩa là một thay thế.

Ngoài ra, điều này cũng đảm bảo bạn không thể thay đổi dữ liệu được tham chiếu, nơi con trỏ có thể được sửa đổi. Một con trỏ const sẽ hoạt động, chỉ khi tham chiếu / con trỏ tới dữ liệu không thể thay đổi.

Con trỏ rất hữu ích nếu tham số A cho B không được đảm bảo được đặt hoặc có thể được gán lại. Và đôi khi, một con trỏ yếu chỉ quá dài cho việc triển khai và một số người tốt không biết điểm yếu là gì, hoặc không thích chúng.

Xé ra câu trả lời này, làm ơn :)

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.