Sự khác biệt giữa biến con trỏ và biến tham chiếu trong C ++ là gì?


3263

Tôi biết tài liệu tham khảo là cú pháp đường, vì vậy mã dễ đọc và viết hơn.

Nhưng sự khác biệt là gì?


100
Tôi nghĩ rằng điểm 2 phải là "Một con trỏ được phép là NULL nhưng tham chiếu thì không. Chỉ có mã không đúng mới có thể tạo tham chiếu NULL và hành vi của nó không được xác định."
Đánh dấu tiền chuộc

19
Con trỏ chỉ là một loại đối tượng khác, và giống như bất kỳ đối tượng nào trong C ++, chúng có thể là một biến. Mặt khác, các tham chiếu không bao giờ là đối tượng, chỉ là các biến.
Kerrek SB

19
Điều này biên dịch mà không có cảnh báo: int &x = *(int*)0;trên gcc. Tham khảo thực sự có thể trỏ đến NULL.
Calmarius

20
tham chiếu là một bí danh biến
Khaled.K

20
Tôi thích cách câu đầu tiên là một ngụy biện hoàn toàn. Tài liệu tham khảo có ngữ nghĩa riêng của họ.
Các cuộc đua nhẹ nhàng trong quỹ đạo

Câu trả lời:


1708
  1. Một con trỏ có thể được gán lại:

    int x = 5;
    int y = 6;
    int *p;
    p = &x;
    p = &y;
    *p = 10;
    assert(x == 5);
    assert(y == 10);

    Một tham chiếu không thể, và phải được chỉ định khi khởi tạo:

    int x = 5;
    int y = 6;
    int &r = x;
  2. Một con trỏ có địa chỉ bộ nhớ và kích thước của nó trên ngăn xếp (4 byte trên x86), trong khi một tham chiếu chia sẻ cùng một địa chỉ bộ nhớ (với biến ban đầu) nhưng cũng chiếm một khoảng trống trên ngăn xếp. Vì một tham chiếu có cùng địa chỉ với chính biến ban đầu, nên có thể nghĩ rằng một tham chiếu là một tên khác cho cùng một biến. Lưu ý: Những gì một con trỏ trỏ đến có thể trên ngăn xếp hoặc đống. Ditto một tài liệu tham khảo. Yêu cầu của tôi trong tuyên bố này không phải là một con trỏ phải trỏ đến ngăn xếp. Một con trỏ chỉ là một biến chứa một địa chỉ bộ nhớ. Biến này là trên ngăn xếp. Vì một tham chiếu có không gian riêng trên ngăn xếp và vì địa chỉ giống với biến mà nó tham chiếu. Thêm vào stack vs heap. Điều này ngụ ý rằng có một địa chỉ thực sự của một tham chiếu mà trình biên dịch sẽ không cho bạn biết.

    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    assert(p == p2);
  3. Bạn có thể có con trỏ tới con trỏ tới con trỏ cung cấp thêm mức độ gián tiếp. Trong khi các tài liệu tham khảo chỉ cung cấp một mức độ gián tiếp.

    int x = 0;
    int y = 0;
    int *p = &x;
    int *q = &y;
    int **pp = &p;
    pp = &q;//*pp = q
    **pp = 4;
    assert(y == 4);
    assert(x == 0);
  4. Một con trỏ có thể được chỉ định nullptrtrực tiếp, trong khi tham chiếu không thể. Nếu bạn cố gắng đủ, và bạn biết làm thế nào, bạn có thể tạo địa chỉ của một tài liệu tham khảo nullptr. Tương tự như vậy, nếu bạn cố gắng hết sức, bạn có thể có một tham chiếu đến một con trỏ và sau đó tham chiếu đó có thể chứa nullptr.

    int *p = nullptr;
    int &r = nullptr; <--- compiling error
    int &r = *p;  <--- likely no compiling error, especially if the nullptr is hidden behind a function call, yet it refers to a non-existent int at address 0
  5. Con trỏ có thể lặp qua một mảng; bạn có thể sử dụng ++để đi đến mục tiếp theo mà con trỏ đang trỏ tới và + 4đi đến phần tử thứ 5. Đây không phải là vấn đề kích thước của đối tượng mà con trỏ trỏ tới.

  6. Một con trỏ cần được hủy *đăng ký để truy cập vào vị trí bộ nhớ mà nó trỏ tới, trong khi tham chiếu có thể được sử dụng trực tiếp. Một con trỏ tới một lớp / struct sử dụng ->để truy cập các thành viên của nó trong khi một tham chiếu sử dụng a ..

  7. Tài liệu tham khảo không thể được nhồi vào một mảng, trong khi con trỏ có thể được (Được đề cập bởi người dùng @litb)

  8. Tài liệu tham khảo Const có thể được ràng buộc với thời gian. Con trỏ không thể (không phải không có một số điều hướng):

    const int &x = int(12); //legal C++
    int *y = &int(12); //illegal to dereference a temporary.

    Điều này làm cho const&an toàn hơn để sử dụng trong danh sách đối số và vv.


23
... nhưng NULL không được xác định. Ví dụ: bạn không thể kiểm tra nếu tham chiếu là NULL (ví dụ: & ref == NULL).
Pat Notz

69
Số 2 không đúng. Một tài liệu tham khảo không chỉ đơn giản là "tên khác cho cùng một biến." Các tham chiếu có thể được truyền cho các hàm, được lưu trữ trong các lớp, v.v ... theo cách rất giống với con trỏ. Chúng tồn tại độc lập với các biến mà chúng trỏ đến.
Công viên Derek

31
Brian, ngăn xếp không liên quan. Tài liệu tham khảo và con trỏ không phải mất không gian trên ngăn xếp. Cả hai đều có thể được phân bổ trên đống.
Công viên Derek

22
Brian, thực tế là một biến (trong trường hợp này là một con trỏ hoặc tham chiếu) yêu cầu không gian không có nghĩa là nó yêu cầu không gian trên ngăn xếp. Con trỏ và tham chiếu có thể không chỉ trỏ đến heap, chúng thực sự có thể được phân bổ trên heap.
Công viên Derek

38
Một khác biệt quan trọng khác: các tài liệu tham khảo không thể được nhồi vào một mảng
Johannes Schaub - litb

384

Tham chiếu C ++ là gì ( dành cho lập trình viên C )

Một tham chiếu có thể được coi là một con trỏ không đổi (không bị nhầm lẫn với một con trỏ đến một giá trị không đổi!) Với tính năng tự động, tức là trình biên dịch sẽ áp dụng *toán tử cho bạn.

Tất cả các tham chiếu phải được khởi tạo với giá trị không null hoặc quá trình biên dịch sẽ thất bại. Không thể lấy địa chỉ của tham chiếu - toán tử địa chỉ sẽ trả về địa chỉ của giá trị được tham chiếu thay vào đó - cũng không thể thực hiện các phép đo đối với các tham chiếu.

Các lập trình viên C có thể không thích các tham chiếu C ++ vì nó sẽ không còn rõ ràng khi xảy ra tình trạng gián tiếp hoặc nếu một đối số được truyền bằng giá trị hoặc bằng con trỏ mà không nhìn vào chữ ký hàm.

Các lập trình viên C ++ có thể không thích sử dụng các con trỏ vì chúng được coi là không an toàn - mặc dù các tài liệu tham khảo không thực sự an toàn hơn các con trỏ liên tục ngoại trừ trong các trường hợp tầm thường nhất - thiếu sự tiện lợi của việc tự động chuyển hướng và mang một ý nghĩa ngữ nghĩa khác.

Hãy xem xét các tuyên bố sau từ C ++ FAQ :

Mặc dù một tham chiếu thường được thực hiện sử dụng một địa chỉ trong ngôn ngữ lắp ráp cơ bản, xin vui lòng không nghĩ đến một tài liệu tham khảo như một con trỏ tìm hài hước đến một đối tượng. Một tài liệu tham khảo đối tượng. Nó không phải là một con trỏ tới đối tượng, cũng không phải là bản sao của đối tượng. Nó đối tượng.

Nhưng nếu một tài liệu tham khảo thực sự là đối tượng, làm thế nào có thể có các tài liệu tham khảo lơ lửng? Trong các ngôn ngữ không được quản lý, các tham chiếu không thể 'an toàn' hơn con trỏ - nói chung không có cách nào đáng tin cậy các giá trị bí danh trên các ranh giới phạm vi!

Tại sao tôi coi tài liệu tham khảo C ++ hữu ích

Xuất thân từ một nền C, C ++ tài liệu tham khảo có thể trông giống như một khái niệm hơi ngớ ngẩn, nhưng chúng ta vẫn nên sử dụng chúng thay vì con trỏ nếu có thể: Tự động gián tiếp thuận tiện, và tài liệu tham khảo trở nên đặc biệt hữu ích khi giao dịch với RAII - nhưng không phải vì bất kỳ an toàn nhận thức lợi thế, nhưng thay vì họ làm cho việc viết mã thành ngữ bớt khó xử.

RAII là một trong những khái niệm trung tâm của C ++, nhưng nó tương tác không tầm thường với ngữ nghĩa sao chép. Vượt qua các đối tượng bằng cách tham chiếu sẽ tránh được các vấn đề này vì không liên quan đến sao chép. Nếu các tài liệu tham khảo không có trong ngôn ngữ, thay vào đó, bạn phải sử dụng các con trỏ, điều này sẽ khó sử dụng hơn, do đó vi phạm nguyên tắc thiết kế ngôn ngữ rằng giải pháp thực hành tốt nhất nên dễ dàng hơn các giải pháp thay thế.


17
@kriss: Không, bạn cũng có thể nhận được một tham chiếu lơ lửng bằng cách trả về một biến tự động bằng cách tham chiếu.
Ben Voigt

12
@kriss: Trình biên dịch hầu như không thể phát hiện ra trong trường hợp chung. Hãy xem xét một hàm thành viên trả về một tham chiếu đến một biến thành viên lớp: điều đó an toàn và không bị cấm bởi trình biên dịch. Sau đó, một người gọi có một phiên bản tự động của lớp đó, gọi hàm thành viên đó và trả về tham chiếu. Presto: tài liệu tham khảo lơ lửng. Và vâng, nó sẽ gây rắc rối, @kriss: đó là quan điểm của tôi. Nhiều người cho rằng một lợi thế của tài liệu tham khảo so với con trỏ là tài liệu tham khảo luôn hợp lệ, nhưng thực tế không phải vậy.
Ben Voigt

4
@kriss: Không, một tham chiếu vào một đối tượng có thời lượng lưu trữ tự động rất khác với một đối tượng tạm thời. Dù sao, tôi chỉ cung cấp một ví dụ ngược lại cho tuyên bố của bạn rằng bạn chỉ có thể nhận được một tham chiếu không hợp lệ bằng cách hủy bỏ một con trỏ không hợp lệ. Christoph là chính xác - tài liệu tham khảo không an toàn hơn con trỏ, một chương trình chỉ sử dụng tài liệu tham khảo vẫn có thể phá vỡ sự an toàn của loại.
Ben Voigt

7
Tài liệu tham khảo không phải là một loại con trỏ. Chúng là một tên mới cho một đối tượng hiện có.
catphive

18
@catphive: đúng nếu bạn đi theo ngữ nghĩa ngôn ngữ, không đúng nếu bạn thực sự nhìn vào việc thực hiện; C ++ là ngôn ngữ 'ma thuật' hơn nhiều so với C, và nếu bạn loại bỏ ma thuật khỏi các tài liệu tham khảo, bạn sẽ kết thúc bằng một con trỏ
Christoph

191

Nếu bạn muốn thực sự phạm tội, có một điều bạn có thể làm với một tham chiếu mà bạn không thể làm với một con trỏ: kéo dài tuổi thọ của một đối tượng tạm thời. Trong C ++ nếu bạn liên kết một tham chiếu const với một đối tượng tạm thời, thời gian tồn tại của đối tượng đó sẽ trở thành thời gian tồn tại của tham chiếu.

std::string s1 = "123";
std::string s2 = "456";

std::string s3_copy = s1 + s2;
const std::string& s3_reference = s1 + s2;

Trong ví dụ này, s3_copy sao chép đối tượng tạm thời là kết quả của phép nối. Trong khi đó s3 numference về bản chất trở thành đối tượng tạm thời. Nó thực sự là một tham chiếu đến một đối tượng tạm thời hiện có cùng thời gian với tham chiếu.

Nếu bạn thử điều này mà không có constnó sẽ không biên dịch được. Bạn không thể liên kết một tham chiếu không phải là một đối tượng tạm thời, và bạn cũng không thể lấy địa chỉ của nó cho vấn đề đó.


5
Nhưng trường hợp sử dụng cho việc này là gì?
Ahmad Mushtaq

20
Chà, s3_copy sẽ tạo tạm thời và sau đó sao chép cấu trúc nó vào s3_copy trong khi s3 numference trực tiếp sử dụng tạm thời. Sau đó, để thực sự mô phạm, bạn cần xem Tối ưu hóa giá trị trả về, theo đó trình biên dịch được phép bỏ qua việc xây dựng bản sao trong trường hợp đầu tiên.
Matt Giá

6
@digitalSurgeon: Phép thuật ở đó khá mạnh. Tuổi thọ của đối tượng được kéo dài bởi thực tế của const &ràng buộc và chỉ khi tham chiếu vượt ra khỏi phạm vi thì hàm hủy của loại tham chiếu thực tế (so với loại tham chiếu, có thể là cơ sở) mới được gọi. Vì nó là một tài liệu tham khảo, không có lát cắt sẽ diễn ra ở giữa.
David Rodríguez - dribeas

9
Cập nhật cho C ++ 11: câu cuối cùng nên đọc "Bạn có thể không ràng buộc một giá trị trái tham chiếu không const để tạm thời" bởi vì bạn có thể ràng buộc một tổ chức phi const rvalue tham chiếu đến một tạm thời, và nó có hành vi tương tự đời-mở rộng.
Oktalist

4
@AhmadMushtaq: Việc sử dụng chính của lớp này là các lớp dẫn xuất . Nếu không có sự kế thừa liên quan, bạn cũng có thể sử dụng ngữ nghĩa giá trị, sẽ rẻ hoặc miễn phí do RVO / di chuyển xây dựng. Nhưng nếu bạn có Animal x = fast ? getHare() : getTortoise()thì xsẽ phải đối mặt với vấn đề cắt cổ điển, trong khi Animal& x = ...sẽ làm việc một cách chính xác.
Arthur Tacca

128

Ngoài đường cú pháp, một tham chiếu là một constcon trỏ ( không phải con trỏ đến a const). Bạn phải thiết lập những gì nó đề cập đến khi bạn khai báo biến tham chiếu và bạn không thể thay đổi nó sau này.

Cập nhật: bây giờ tôi nghĩ về nó nhiều hơn, có một sự khác biệt quan trọng.

Mục tiêu của con trỏ const có thể được thay thế bằng cách lấy địa chỉ của nó và sử dụng cast const.

Mục tiêu của tài liệu tham khảo không thể được thay thế bằng bất kỳ cách nào thiếu UB.

Điều này sẽ cho phép trình biên dịch thực hiện tối ưu hóa nhiều hơn trên một tài liệu tham khảo.


8
Tôi nghĩ rằng đây là câu trả lời tốt nhất cho đến nay. Những người khác nói về các tài liệu tham khảo và con trỏ như họ là những con thú khác nhau và sau đó trình bày cách họ khác nhau trong hành vi. Nó không làm cho mọi thứ dễ dàng hơn imho. Tôi luôn hiểu các tài liệu tham khảo là một T* constloại đường cú pháp khác nhau (điều đó xảy ra để loại bỏ rất nhiều * và & từ mã của bạn).
Carlo Wood

2
"Mục tiêu của con trỏ const có thể được thay thế bằng cách lấy địa chỉ của nó và sử dụng cast const." Làm như vậy là hành vi không xác định. Xem stackoverflow.com/questions/25209838/ cấp để biết chi tiết.
thức

1
Cố gắng thay đổi tham chiếu của một tham chiếu hoặc giá trị của một con trỏ const (hoặc bất kỳ vô hướng const) là bất đẳng thức. Những gì bạn có thể làm: xóa một trình độ const đã được thêm bằng chuyển đổi ngầm định: int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci);là OK.
tò mò

1
Sự khác biệt ở đây là UB so với nghĩa đen là không thể. Không có cú pháp nào trong C ++ sẽ cho phép bạn thay đổi điểm tham chiếu tại.

Không phải là không thể, khó hơn, bạn chỉ có thể truy cập vào vùng nhớ của con trỏ đang mô hình hóa tham chiếu đó và thay đổi nội dung của nó. Điều đó chắc chắn có thể được thực hiện.
Nicolas Bousquet

126

Trái với ý kiến ​​phổ biến, có thể có một tài liệu tham khảo đó là NULL.

int * p = NULL;
int & r = *p;
r = 1;  // crash! (if you're lucky)

Cấp, nó khó hơn nhiều để làm với một tài liệu tham khảo - nhưng nếu bạn quản lý nó, bạn sẽ xé tóc ra để cố gắng tìm nó. Tài liệu tham khảo vốn không an toàn trong C ++!

Về mặt kỹ thuật, đây là một tài liệu tham khảo không hợp lệ , không phải là tài liệu tham khảo null. C ++ không hỗ trợ các tham chiếu null dưới dạng khái niệm như bạn có thể tìm thấy trong các ngôn ngữ khác. Có nhiều loại tài liệu tham khảo không hợp lệ là tốt. Bất kỳ tham chiếu không hợp lệ nào cũng làm tăng bóng ma của hành vi không xác định , giống như sử dụng một con trỏ không hợp lệ.

Lỗi thực tế nằm ở phần hội thảo của con trỏ NULL, trước khi gán cho tham chiếu. Nhưng tôi không biết về bất kỳ trình biên dịch nào sẽ tạo ra bất kỳ lỗi nào trong điều kiện đó - lỗi sẽ lan truyền đến một điểm xa hơn trong mã. Đó là những gì làm cho vấn đề này rất ngấm ngầm. Hầu hết thời gian, nếu bạn bỏ qua một con trỏ NULL, bạn sẽ gặp sự cố ngay tại vị trí đó và không cần phải gỡ lỗi nhiều để tìm ra nó.

Ví dụ của tôi ở trên là ngắn và có kế hoạch. Đây là một ví dụ thực tế hơn.

class MyClass
{
    ...
    virtual void DoSomething(int,int,int,int,int);
};

void Foo(const MyClass & bar)
{
    ...
    bar.DoSomething(i1,i2,i3,i4,i5);  // crash occurs here due to memory access violation - obvious why?
}

MyClass * GetInstance()
{
    if (somecondition)
        return NULL;
    ...
}

MyClass * p = GetInstance();
Foo(*p);

Tôi muốn nhắc lại rằng cách duy nhất để có được một tham chiếu null là thông qua mã không đúng định dạng và một khi bạn có nó, bạn sẽ nhận được hành vi không xác định. Nó không bao giờ có ý nghĩa để kiểm tra một tài liệu tham khảo null; ví dụ bạn có thể thử if(&bar==NULL)...nhưng trình biên dịch có thể tối ưu hóa câu lệnh tồn tại! Một tham chiếu hợp lệ không bao giờ có thể là NULL vì vậy theo quan điểm của người biên dịch, so sánh luôn luôn sai và có thể loại bỏ ifmệnh đề dưới dạng mã chết - đây là bản chất của hành vi không xác định.

Cách thích hợp để tránh xa rắc rối là tránh hội thảo con trỏ NULL để tạo tham chiếu. Đây là một cách tự động để thực hiện điều này.

template<typename T>
T& deref(T* p)
{
    if (p == NULL)
        throw std::invalid_argument(std::string("NULL reference"));
    return *p;
}

MyClass * p = GetInstance();
Foo(deref(p));

Để có cái nhìn sâu sắc hơn về vấn đề này từ một người có kỹ năng viết tốt hơn, hãy xem Tài liệu tham khảo Null từ Jim Hyslop và Herb Sutter.

Đối với một ví dụ khác về sự nguy hiểm của việc hủy bỏ hội thảo, một con trỏ null xem Phơi bày hành vi không xác định khi cố gắng chuyển mã sang nền tảng khác của Raymond Chen.


63
Các mã trong câu hỏi có chứa hành vi không xác định. Về mặt kỹ thuật, bạn không thể làm bất cứ điều gì với một con trỏ null ngoại trừ đặt nó và so sánh nó. Khi chương trình của bạn gọi hành vi không xác định, nó có thể làm bất cứ điều gì, kể cả có vẻ hoạt động chính xác cho đến khi bạn đưa ra bản demo cho ông chủ lớn.
KeithB

9
đánh dấu có một đối số hợp lệ. đối số mà một con trỏ có thể là NULL và bạn phải kiểm tra cũng không có thực: nếu bạn nói một hàm yêu cầu không phải NULL, thì người gọi phải thực hiện điều đó. Vì vậy, nếu người gọi không anh ta đang gọi hành vi không xác định. giống như nhãn hiệu đã làm với tài liệu tham khảo xấu
Johannes Schaub - litb

13
Mô tả là sai. Mã này có thể hoặc không thể tạo một tham chiếu là NULL. Hành vi của nó là không xác định. Nó có thể tạo ra một tài liệu tham khảo hoàn toàn hợp lệ. Nó có thể không tạo ra bất kỳ tài liệu tham khảo nào cả.
David Schwartz

7
@David Schwartz, nếu tôi nói về cách mọi thứ phải hoạt động theo tiêu chuẩn, bạn sẽ đúng. Nhưng đó không phải là điều tôi đang nói - Tôi đang nói về hành vi được quan sát thực tế với một trình biên dịch rất phổ biến và ngoại suy dựa trên kiến ​​thức của tôi về trình biên dịch điển hình và kiến ​​trúc CPU cho những gì có thể sẽ xảy ra. Nếu bạn tin rằng tài liệu tham khảo vượt trội hơn con trỏ bởi vì chúng an toàn hơn và không cho rằng tài liệu tham khảo có thể xấu, bạn sẽ gặp phải một vấn đề đơn giản vào một ngày nào đó giống như tôi.
Đánh dấu tiền chuộc

6
Dereferences một con trỏ null là sai. Bất kỳ chương trình nào làm điều đó, thậm chí để khởi tạo một tài liệu tham khảo là sai. Nếu bạn đang khởi tạo một tham chiếu từ một con trỏ, bạn phải luôn kiểm tra xem con trỏ đó có hợp lệ không. Ngay cả khi điều này thành công, đối tượng cơ bản có thể bị xóa bất cứ lúc nào để lại tham chiếu để tham chiếu đến đối tượng không tồn tại, phải không? Những gì bạn đang nói là những thứ tốt. Tôi nghĩ rằng vấn đề thực sự ở đây là tham chiếu KHÔNG cần phải được kiểm tra "tính không" khi bạn nhìn thấy một và con trỏ tối thiểu phải được khẳng định.
t0rakka

115

Bạn đã quên phần quan trọng nhất:

truy cập thành viên với con trỏ sử dụng ->
quyền truy cập thành viên với tham chiếu sử dụng.

foo.barrõ ràng là vượt trội so với foo->barcùng một cách mà vi rõ ràng là vượt trội so với Emacs :-)


4
@Orion Edwards> truy cập thành viên với các con trỏ sử dụng - >> truy cập thành viên với các tham chiếu sử dụng. Điều này không đúng 100%. Bạn có thể có một tham chiếu đến một con trỏ. Trong trường hợp này, bạn sẽ truy cập các thành viên của con trỏ được tham chiếu bằng cách sử dụng -> struct Node {Node * next; }; Nút * đầu tiên; // p là một tham chiếu đến một con trỏ void foo (Node * & p) {p-> next = first; } Nút * bar = Nút mới; foo (thanh); - OP: Bạn có quen thuộc với các khái niệm về giá trị và giá trị không?

3
Con trỏ thông minh có cả hai. (các phương thức trên lớp con trỏ thông minh) và -> (các phương thức trên kiểu cơ bản).
JBRWilkinson

1
@ user6105 Tuyên bố Orion Edwards thực sự đúng 100%. "truy cập các thành viên của [con trỏ khử tham chiếu" Một con trỏ không có bất kỳ thành viên nào. Đối tượng mà con trỏ đề cập đến có các thành viên và truy cập vào đó chính xác là những gì ->cung cấp cho các tham chiếu đến các con trỏ, giống như với chính con trỏ.
Max Truxa

1
tại sao lại như vậy .->có liên quan đến vi và emacs :)
artm

10
@artM - đó là một trò đùa và có lẽ không có ý nghĩa với những người nói tiếng Anh không phải là người bản xứ. Lời xin lỗi của tôi. Để giải thích, liệu vi có tốt hơn emacs hay không là hoàn toàn chủ quan. Một số người cho rằng vi vượt trội hơn nhiều, và những người khác lại nghĩ ngược lại. Tương tự, tôi nghĩ rằng sử dụng .tốt hơn so với sử dụng ->, nhưng cũng giống như vi vs emacs, nó hoàn toàn chủ quan và bạn không thể chứng minh bất cứ điều gì
Orion Edwards

74

Tài liệu tham khảo rất giống với con trỏ, nhưng chúng được chế tạo đặc biệt để hữu ích cho việc tối ưu hóa trình biên dịch.

  • Các tham chiếu được thiết kế sao cho trình biên dịch dễ dàng hơn trong việc theo dõi bí danh tham chiếu nào biến. Hai tính năng chính rất quan trọng: không có "số học tham chiếu" và không phân công lại các tài liệu tham khảo. Chúng cho phép trình biên dịch tìm ra tham chiếu bí danh nào biến trong thời gian biên dịch.
  • Các tham chiếu được phép tham chiếu đến các biến không có địa chỉ bộ nhớ, chẳng hạn như các biến mà trình biên dịch chọn để đưa vào các thanh ghi. Nếu bạn lấy địa chỉ của một biến cục bộ, trình biên dịch sẽ rất khó để đưa nó vào một thanh ghi.

Ví dụ:

void maybeModify(int& x); // may modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // This function is designed to do something particularly troublesome
    // for optimizers. It will constantly call maybeModify on array[0] while
    // adding array[1] to array[2]..array[size-1]. There's no real reason to
    // do this, other than to demonstrate the power of references.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(array[0]);
        array[i] += array[1];
    }
}

Một trình biên dịch tối ưu hóa có thể nhận ra rằng chúng ta đang truy cập [0] và [1] khá nhiều. Rất thích tối ưu hóa thuật toán để:

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Do the same thing as above, but instead of accessing array[1]
    // all the time, access it once and store the result in a register,
    // which is much faster to do arithmetic with.
    register int a0 = a[0];
    register int a1 = a[1]; // access a[1] once
    for (int i = 2; i < (int)size; i++) {
        maybeModify(a0); // Give maybeModify a reference to a register
        array[i] += a1;  // Use the saved register value over and over
    }
    a[0] = a0; // Store the modified a[0] back into the array
}

Để thực hiện tối ưu hóa như vậy, cần phải chứng minh rằng không có gì có thể thay đổi mảng [1] trong suốt cuộc gọi. Điều này là khá dễ dàng để làm. i không bao giờ nhỏ hơn 2, vì vậy mảng [i] không bao giờ có thể tham chiếu đến mảng [1]. mayModify () được cho a0 làm tham chiếu (mảng răng cưa [0]). Do không có số học "tham chiếu", trình biên dịch chỉ cần chứng minh rằng mayModify không bao giờ lấy địa chỉ của x và nó đã chứng minh rằng không có gì thay đổi mảng [1].

Nó cũng phải chứng minh rằng không có cách nào một cuộc gọi trong tương lai có thể đọc / ghi [0] trong khi chúng tôi có một bản sao đăng ký tạm thời của nó trong a0. Điều này thường không quan trọng để chứng minh, bởi vì trong nhiều trường hợp, rõ ràng là tài liệu tham khảo không bao giờ được lưu trữ trong một cấu trúc vĩnh viễn như một thể hiện của lớp.

Bây giờ làm điều tương tự với con trỏ

void maybeModify(int* x); // May modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Same operation, only now with pointers, making the
    // optimization trickier.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(&(array[0]));
        array[i] += array[1];
    }
}

Hành vi là như nhau; chỉ bây giờ khó khăn hơn nhiều để chứng minh rằng mayModify không bao giờ sửa đổi mảng [1], bởi vì chúng tôi đã cho nó một con trỏ; Con mèo ra khỏi cái túi xách. Bây giờ nó phải thực hiện một bằng chứng khó khăn hơn nhiều: một phân tích tĩnh về mayModify để chứng minh rằng nó không bao giờ ghi vào & x + 1. Nó cũng phải chứng minh rằng nó không bao giờ lưu một con trỏ có thể tham chiếu đến mảng [0], chỉ là như là khó khăn

Các trình biên dịch hiện đại ngày càng tốt hơn trong phân tích tĩnh, nhưng thật tuyệt khi giúp chúng ra và sử dụng các tài liệu tham khảo.

Tất nhiên, việc tối ưu hóa thông minh như vậy, trình biên dịch thực sự sẽ biến các tham chiếu thành con trỏ khi cần.

EDIT: Năm năm sau khi đăng câu trả lời này, tôi đã tìm thấy một sự khác biệt kỹ thuật thực tế nơi các tài liệu tham khảo khác với chỉ một cách nhìn khác nhau về cùng một khái niệm địa chỉ. Tài liệu tham khảo có thể sửa đổi tuổi thọ của các đối tượng tạm thời theo cách mà con trỏ không thể.

F createF(int argument);

void extending()
{
    const F& ref = createF(5);
    std::cout << ref.getArgument() << std::endl;
};

Thông thường các đối tượng tạm thời như đối tượng được tạo bởi lệnh gọi createF(5)sẽ bị hủy ở cuối biểu thức. Tuy nhiên, bằng cách ràng buộc đối tượng đó với một tham chiếu, refC ++ sẽ kéo dài tuổi thọ của đối tượng tạm thời đó cho đến khi refvượt ra khỏi phạm vi.


Đúng, cơ thể phải được nhìn thấy. Tuy nhiên, việc xác định rằng maybeModifykhông lấy địa chỉ của bất kỳ thứ gì liên quan đến xthực chất dễ dàng hơn việc chứng minh rằng một loạt các số học con trỏ không xảy ra.
Cort Ammon

Tôi tin rằng trình tối ưu hóa đã thực hiện rằng "một loạt các con trỏ không xảy ra" kiểm tra các lý do khác.
Ben Voigt

"Tài liệu tham khảo rất giống với con trỏ" - về mặt ngữ nghĩa, trong bối cảnh thích hợp - nhưng về mặt mã được tạo, chỉ trong một số triển khai và không thông qua bất kỳ định nghĩa / yêu cầu nào. Tôi biết bạn đã chỉ ra điều này và tôi không đồng ý với bất kỳ bài đăng nào của bạn về mặt thực tế, nhưng chúng tôi có quá nhiều vấn đề với những người đọc quá nhiều vào các mô tả tốc ký như 'các tài liệu tham khảo giống như / thường được thực hiện như con trỏ' .
gạch dưới

Tôi có cảm giác rằng ai đó đã gắn cờ sai vì đã bình luận lỗi thời dọc theo dòng void maybeModify(int& x) { 1[&x]++; }mà các ý kiến ​​khác ở trên đang thảo luận
Ben Voigt

69

Trên thực tế, một tài liệu tham khảo không thực sự giống như một con trỏ.

Trình biên dịch giữ "tham chiếu" đến các biến, liên kết tên với địa chỉ bộ nhớ; đó là công việc của nó để dịch bất kỳ tên biến thành địa chỉ bộ nhớ khi biên dịch.

Khi bạn tạo một tham chiếu, bạn chỉ nói với trình biên dịch rằng bạn gán tên khác cho biến con trỏ; đó là lý do tại sao các tham chiếu không thể "trỏ đến null", bởi vì một biến không thể và không thể.

Con trỏ là các biến; chúng chứa địa chỉ của một số biến khác hoặc có thể là null. Điều quan trọng là một con trỏ có một giá trị, trong khi một tham chiếu chỉ có một biến mà nó đang tham chiếu.

Bây giờ một số giải thích về mã thực:

int a = 0;
int& b = a;

Ở đây bạn không tạo ra một biến khác trỏ đến a; bạn chỉ cần thêm một tên khác vào nội dung bộ nhớ giữ giá trị của a. Bộ nhớ này hiện có hai tên, ab, nó có thể được xử lý bằng tên này.

void increment(int& n)
{
    n = n + 1;
}

int a;
increment(a);

Khi gọi một hàm, trình biên dịch thường tạo các không gian bộ nhớ cho các đối số được sao chép vào. Chữ ký hàm xác định các khoảng trắng sẽ được tạo và đặt tên nên được sử dụng cho các khoảng trắng này. Khai báo một tham số làm tham chiếu chỉ cho trình biên dịch sử dụng không gian bộ nhớ biến đầu vào thay vì phân bổ một không gian bộ nhớ mới trong khi gọi phương thức. Có vẻ lạ khi nói rằng hàm của bạn sẽ trực tiếp thao tác một biến được khai báo trong phạm vi gọi, nhưng hãy nhớ rằng khi thực thi mã được biên dịch, không còn phạm vi nữa; chỉ có bộ nhớ phẳng đơn giản và mã chức năng của bạn có thể thao tác bất kỳ biến nào.

Bây giờ có thể có một số trường hợp trình biên dịch của bạn không thể biết được tham chiếu khi biên dịch, như khi sử dụng biến ngoài. Vì vậy, một tham chiếu có thể hoặc không thể được thực hiện như một con trỏ trong mã bên dưới. Nhưng trong các ví dụ tôi đã đưa cho bạn, rất có thể nó sẽ không được thực hiện bằng một con trỏ.


2
Một tham chiếu là một tham chiếu đến giá trị l, không nhất thiết phải là một biến. Do đó, nó gần với một con trỏ hơn là một bí danh thực sự (một cấu trúc thời gian biên dịch). Ví dụ về các biểu thức có thể được tham chiếu là * p hoặc thậm chí * p ++

5
Đúng vậy, tôi chỉ chỉ ra một thực tế là một tham chiếu có thể không phải lúc nào cũng đẩy một biến mới trên ngăn xếp theo cách mà một con trỏ mới sẽ làm.
Vincent Robert

1
@VincentRobert: Nó sẽ hoạt động giống như một con trỏ ... nếu hàm được nội tuyến, cả tham chiếu và con trỏ sẽ được tối ưu hóa đi. Nếu có lệnh gọi hàm, địa chỉ của đối tượng sẽ cần được truyền cho hàm.
Ben Voigt

1
int * p = NULL; int & r = * p; tham chiếu trỏ đến NULL; if (r) {} -> boOm;)
sree

2
Sự tập trung này vào giai đoạn biên dịch có vẻ tốt, cho đến khi bạn nhớ rằng các tham chiếu có thể được truyền xung quanh trong thời gian chạy, tại thời điểm đó, răng cưa tĩnh đi ra khỏi cửa sổ. (Và sau đó, các tham chiếu thường được triển khai dưới dạng con trỏ, nhưng tiêu chuẩn không yêu cầu phương pháp này.)
underscore_d

45

Một tài liệu tham khảo không bao giờ có thể được NULL.


10
Xem câu trả lời của Mark Ransom cho một ví dụ ngược lại. Đây là huyền thoại thường được khẳng định nhất về các tài liệu tham khảo, nhưng nó là một huyền thoại. Bảo đảm duy nhất mà bạn có theo tiêu chuẩn là, bạn ngay lập tức có UB khi bạn có tham chiếu NULL. Nhưng đó là giống như nói "Chiếc xe này an toàn, nó không bao giờ có thể ra khỏi đường. (Chúng tôi không chịu trách nhiệm cho những gì có thể xảy ra nếu bạn lái nó ra khỏi đường. Dù sao nó cũng có thể phát nổ."
cmaster - phục hồi monica

17
@cmaster: Trong một chương trình hợp lệ , tham chiếu không thể là null. Nhưng một con trỏ có thể. Đây không phải là một huyền thoại, đây là một sự thật.
dùng541686

8
@Mehrdad Có, các chương trình hợp lệ ở lại trên đường. Nhưng không có rào cản giao thông để thực thi rằng chương trình của bạn thực sự làm. Phần lớn của con đường thực sự là thiếu dấu hiệu. Vì vậy, cực kỳ dễ dàng để ra đường vào ban đêm. Và điều rất quan trọng để gỡ lỗi các lỗi như vậy mà bạn biết điều này có thể xảy ra: tham chiếu null có thể lan truyền trước khi nó làm hỏng chương trình của bạn, giống như một con trỏ null có thể. Và khi nó có mã như void Foo::bar() { virtual_baz(); }segfaults đó. Nếu bạn không biết rằng các tài liệu tham khảo có thể là null, bạn không thể theo dõi null trở lại nguồn gốc của nó.
cmaster - phục hồi monica

4
int * p = NULL; int & r = * p; tham chiếu trỏ đến NULL; if (r) {} -> boOm;) -
sree

10
@sree int &r=*p;là hành vi không xác định. Vào thời điểm đó, bạn không có một "tài liệu tham khảo trỏ đến NULL," bạn có một chương trình mà không còn có thể được lập luận về ở tất cả .
cdhowie

35

Trong khi cả tham chiếu và con trỏ được sử dụng để gián tiếp truy cập một giá trị khác, có hai điểm khác biệt quan trọng giữa tham chiếu và con trỏ. Đầu tiên là một tham chiếu luôn đề cập đến một đối tượng: Đó là lỗi khi xác định tham chiếu mà không khởi tạo nó. Hành vi của sự phân công là sự khác biệt quan trọng thứ hai: Việc gán cho một tham chiếu thay đổi đối tượng mà tham chiếu bị ràng buộc; nó không rebind tham chiếu đến đối tượng khác. Sau khi khởi tạo, một tham chiếu luôn đề cập đến cùng một đối tượng cơ bản.

Hãy xem xét hai đoạn chương trình này. Đầu tiên, chúng ta gán một con trỏ cho một con trỏ khác:

int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2;    // pi now points to ival2

Sau khi gán, ival, đối tượng được giải quyết bằng pi vẫn không thay đổi. Việc gán thay đổi giá trị của pi, làm cho nó trỏ đến một đối tượng khác. Bây giờ hãy xem xét một chương trình tương tự gán hai tham chiếu:

int &ri = ival, &ri2 = ival2;
ri = ri2;    // assigns ival2 to ival

Nhiệm vụ này thay đổi ival, giá trị được tham chiếu bởi ri và không phải là chính tham chiếu. Sau khi gán, hai tham chiếu vẫn tham chiếu đến các đối tượng ban đầu của chúng và giá trị của các đối tượng đó cũng giống nhau.


"một tài liệu tham khảo luôn đề cập đến một đối tượng" hoàn toàn sai
Ben Voigt

32

Có một sự khác biệt về ngữ nghĩa có thể xuất hiện bí truyền nếu bạn không quen với việc học ngôn ngữ máy tính theo kiểu trừu tượng hoặc thậm chí học thuật.

Ở cấp độ cao nhất, ý tưởng của các tài liệu tham khảo là chúng là các "bí danh" trong suốt. Máy tính của bạn có thể sử dụng một địa chỉ để làm cho chúng hoạt động, nhưng bạn không cần phải lo lắng về điều đó: bạn phải nghĩ chúng là "chỉ một tên khác" cho một đối tượng hiện có và cú pháp phản ánh điều đó. Chúng chặt chẽ hơn con trỏ để trình biên dịch của bạn có thể cảnh báo bạn một cách đáng tin cậy hơn khi bạn sắp tạo một tham chiếu lơ lửng, hơn là khi bạn sắp tạo một con trỏ lơ lửng.

Ngoài ra, tất nhiên có một số khác biệt thực tế giữa con trỏ và tài liệu tham khảo. Cú pháp để sử dụng chúng rõ ràng là khác nhau và bạn không thể "ngồi lại" các tham chiếu, có các tham chiếu đến hư vô hoặc có các con trỏ tới các tham chiếu.


27

Tham chiếu là bí danh cho một biến khác trong khi con trỏ giữ địa chỉ bộ nhớ của biến. Các tham chiếu thường được sử dụng làm tham số hàm để đối tượng được truyền không phải là bản sao mà là chính đối tượng.

    void fun(int &a, int &b); // A common usage of references.
    int a = 0;
    int &b = a; // b is an alias for a. Not so common to use. 

20

Không quan trọng là nó chiếm bao nhiêu dung lượng vì bạn thực sự không thể thấy bất kỳ tác dụng phụ nào (không thực thi mã) của bất kỳ không gian nào nó sẽ chiếm.

Mặt khác, một sự khác biệt chính giữa các tham chiếu và các con trỏ là các tạm thời được gán cho các tham chiếu const sống cho đến khi tham chiếu const vượt quá phạm vi.

Ví dụ:

class scope_test
{
public:
    ~scope_test() { printf("scope_test done!\n"); }
};

...

{
    const scope_test &test= scope_test();
    printf("in scope\n");
}

sẽ in:

in scope
scope_test done!

Đây là cơ chế ngôn ngữ cho phép ScopeGuard hoạt động.


1
Bạn không thể lấy địa chỉ của một tài liệu tham khảo, nhưng điều đó không có nghĩa là họ không chiếm dung lượng. Tối ưu hóa chặn, họ chắc chắn có thể.
Các cuộc đua nhẹ nhàng trong quỹ đạo

2
Mặc dù tác động, "Một tham chiếu trên ngăn xếp không chiếm bất kỳ khoảng trống nào" là hoàn toàn sai.
Các cuộc đua nhẹ nhàng trong quỹ đạo

1
@Tomalak, tốt, điều đó cũng phụ thuộc vào trình biên dịch. Nhưng vâng, nói rằng đó là một chút khó hiểu. Tôi cho rằng sẽ bớt khó hiểu hơn nếu chỉ loại bỏ điều đó.
MSN

1
Trong bất kỳ trường hợp cụ thể nào, nó có thể hoặc không. Vì vậy, "nó không" như một khẳng định phân loại là sai. Đó là những gì tôi đang nói. :) [Tôi không thể nhớ những gì tiêu chuẩn nói về vấn đề này; các quy tắc của các thành viên tham chiếu có thể truyền đạt một quy tắc chung về "tài liệu tham khảo có thể chiếm không gian", nhưng tôi không có bản sao của tiêu chuẩn với tôi ở đây trên bãi biển: D]
Các cuộc đua Lightness trong Orbit

20

Điều này dựa trên hướng dẫn . Những gì được viết làm cho nó rõ ràng hơn:

>>> The address that locates a variable within memory is
    what we call a reference to that variable. (5th paragraph at page 63)

>>> The variable that stores the reference to another
    variable is what we call a pointer. (3rd paragraph at page 64)

Đơn giản chỉ cần nhớ rằng,

>>> reference stands for memory location
>>> pointer is a reference container (Maybe because we will use it for
several times, it is better to remember that reference.)

Hơn nữa, như chúng ta có thể đề cập đến hầu hết mọi hướng dẫn về con trỏ, con trỏ là một đối tượng được hỗ trợ bởi số học con trỏ, làm cho con trỏ giống với một mảng.

Nhìn vào tuyên bố sau đây,

int Tom(0);
int & alias_Tom = Tom;

alias_Tomcó thể được hiểu là một alias of a variable(khác với typedef, đó là alias of a type) Tom. Bạn cũng có thể quên thuật ngữ của tuyên bố đó là tạo tài liệu tham khảo Tom.


1
Và nếu một lớp có một biến tham chiếu, nó sẽ được khởi tạo với nullptr hoặc một đối tượng hợp lệ trong danh sách khởi tạo.
Giải quyết sai lầm

1
Các từ ngữ trong câu trả lời này là quá khó hiểu để nó được sử dụng thực sự nhiều. Ngoài ra, @Misgevolution, bạn có nghiêm túc giới thiệu với độc giả để khởi tạo một tài liệu tham khảo với a nullptrkhông? Bạn đã thực sự đọc bất kỳ phần nào khác của chủ đề này, hoặc ...?
gạch dưới

1
Xấu của tôi, xin lỗi vì điều ngu ngốc mà tôi nói Lúc đó tôi đã bị thiếu ngủ. 'Khởi tạo với nullptr' là hoàn toàn sai.
Giải quyết sai lầm

19

Một tham chiếu không phải là một tên khác được đặt cho một số bộ nhớ. Đó là một con trỏ bất biến được tự động tham chiếu khi sử dụng. Về cơ bản, nó sôi sùng sục xuống:

int& j = i;

Nó trở thành nội bộ

int* const j = &i;

13
Đây không phải là những gì mà Tiêu chuẩn C ++ nói và trình biên dịch không bắt buộc phải thực hiện các tham chiếu theo cách được mô tả bởi câu trả lời của bạn.
jogojapan

@jogojapan: Bất kỳ cách nào hợp lệ để trình biên dịch C ++ thực hiện tham chiếu cũng là một cách hợp lệ để nó thực hiện một constcon trỏ. Tính linh hoạt đó không chứng minh rằng có sự khác biệt giữa tham chiếu và con trỏ.
Ben Voigt

2
@BenVoigt Có thể đúng là bất kỳ triển khai hợp lệ nào của một cái cũng là một triển khai hợp lệ của cái kia, nhưng điều đó không tuân theo một cách rõ ràng từ các định nghĩa của hai khái niệm này. Một câu trả lời tốt sẽ bắt đầu từ các định nghĩa, và chứng minh tại sao tuyên bố về hai người cuối cùng giống nhau là đúng. Câu trả lời này dường như là một số loại nhận xét về một số câu trả lời khác.
jogojapan

Một tham chiếu một tên khác được đặt cho một đối tượng. Trình biên dịch được phép có bất kỳ loại triển khai nào, miễn là bạn không thể biết được sự khác biệt, điều này được gọi là quy tắc "như thể nếu". Phần quan trọng ở đây là bạn không thể phân biệt được. Nếu bạn có thể phát hiện ra rằng một con trỏ không có bộ lưu trữ, trình biên dịch bị lỗi. Nếu bạn có thể phát hiện ra rằng một tham chiếu không có bộ lưu trữ, trình biên dịch vẫn tuân thủ.
sp2danny

18

Câu trả lời trực tiếp

Một tài liệu tham khảo trong C ++ là gì? Một số trường hợp cụ thể của loại không phải là loại đối tượng .

Một con trỏ trong C ++ là gì? Một số trường hợp cụ thể của loại đó là một loại đối tượng .

Từ định nghĩa ISO C ++ của loại đối tượng :

Một loại đối tượng là một (có thể cv- đủ tiêu chuẩn) không phải là một loại chức năng, không phải là một loại tham chiếu và không cv void.

Có thể cần biết, loại đối tượng là một loại cấp cao nhất của vũ trụ loại trong C ++. Tham khảo cũng là một thể loại cấp cao nhất. Nhưng con trỏ thì không.

Con trỏ và tài liệu tham khảo được đề cập cùng nhau trong bối cảnh của loại hợp chất . Điều này về cơ bản là do bản chất của cú pháp khai báo được kế thừa từ (và mở rộng) C, không có tham chiếu. (Bên cạnh đó, có nhiều hơn một loại khai báo tham chiếu kể từ C ++ 11, trong khi các con trỏ vẫn "unityped":& +&& vs *..) Vì vậy, việc phác thảo một ngôn ngữ cụ thể bằng "phần mở rộng" với kiểu C tương tự trong ngữ cảnh này là hơi hợp lý . (Tôi vẫn sẽ lập luận rằng cú pháp của người khai báo làm lãng phí tính biểu cảm cú pháp rất nhiều , khiến cả người dùng và người thực hiện đều bực bội. Vì vậy, tất cả đều không đủ điều kiện để hợpthiết kế ngôn ngữ mới. Đây là một chủ đề hoàn toàn khác về thiết kế PL. , Tuy nhiên.)

Mặt khác, điều quan trọng là các con trỏ có thể đủ điều kiện là một loại cụ thể với các tham chiếu cùng nhau. Họ chỉ đơn giản là chia sẻ quá ít thuộc tính chung bên cạnh tính tương tự cú pháp, do đó không cần phải đặt chúng cùng nhau trong hầu hết các trường hợp.

Lưu ý các câu trên chỉ đề cập đến "con trỏ" và "tham chiếu" dưới dạng các loại. Có một số câu hỏi quan tâm về trường hợp của họ (như các biến). Cũng có quá nhiều quan niệm sai lầm.

Sự khác biệt của các danh mục cấp cao nhất có thể tiết lộ nhiều khác biệt cụ thể không liên quan trực tiếp đến con trỏ:

  • Các loại đối tượng có thể có cvvòng loại cấp cao nhất . Tài liệu tham khảo không thể.
  • Biến thể của các loại đối tượng chiếm lưu trữ theo máy trừu tượng ngữ nghĩa . Tài liệu tham khảo không cần thiết lưu trữ (xem phần về quan niệm sai lầm dưới đây để biết chi tiết).
  • ...

Một vài quy tắc đặc biệt hơn về tài liệu tham khảo:

  • Khai báo hợp chất là hạn chế hơn về tài liệu tham khảo.
  • Tài liệu tham khảo có thể sụp đổ .
    • Các quy tắc đặc biệt về &&các tham số (như "tham chiếu chuyển tiếp") dựa trên thu gọn tham chiếu trong khi khấu trừ tham số mẫu cho phép "chuyển tiếp hoàn hảo" các tham số.
  • Tài liệu tham khảo có các quy tắc đặc biệt trong khởi tạo. Thời gian tồn tại của biến được khai báo là kiểu tham chiếu có thể khác với các đối tượng thông thường thông qua phần mở rộng.
    • BTW, một vài bối cảnh khác như khởi tạo liên quan đến std::initializer_listmột số quy tắc tương tự kéo dài tuổi thọ tham chiếu. Nó là một con giun khác.
  • ...

Những quan niệm sai lầm

Cú pháp đặc biệt

Tôi biết tài liệu tham khảo là cú pháp đường, vì vậy mã dễ đọc và viết hơn.

Về mặt kỹ thuật, điều này hoàn toàn sai. Tài liệu tham khảo không phải là cú pháp của bất kỳ tính năng nào khác trong C ++, bởi vì chúng không thể được thay thế chính xác bằng các tính năng khác mà không có bất kỳ sự khác biệt về ngữ nghĩa.

(Tương tự như vậy, lambda-biểu s là không cú pháp đường của bất kỳ tính năng khác trong C ++ vì nó không thể được mô phỏng một cách chính xác với tính chất "không xác định" như thứ tự khai báo các biến bị bắt , có thể là quan trọng bởi vì trình tự khởi tạo các biến như vậy có thể có ý nghĩa.)

C ++ chỉ có một vài loại đường cú pháp theo nghĩa chặt chẽ này. Một trường hợp là (được kế thừa từ C) toán tử tích hợp (không quá tải) [], được xác định chính xác có cùng thuộc tính ngữ nghĩa của các dạng kết hợp cụ thể so với toán tử đơn *và toán tử tích hợp+ .

Lưu trữ

Vì vậy, cả một con trỏ và một tham chiếu đều sử dụng cùng một lượng bộ nhớ.

Các tuyên bố trên chỉ đơn giản là sai. Để tránh những quan niệm sai lầm như vậy, thay vào đó, hãy xem các quy tắc ISO C ++:

Từ [intro.object] / 1 :

... Một vật thể chiếm một vùng lưu trữ trong thời kỳ xây dựng, trong suốt vòng đời của nó và trong thời kỳ hủy diệt. ...

Từ [dcl.ref] / 4 :

Không xác định được liệu tham chiếu có yêu cầu lưu trữ hay không.

Lưu ý đây là ngữ nghĩa thuộc tính .

Thực dụng

Ngay cả những con trỏ không đủ điều kiện để được đặt cùng với các tham chiếu theo nghĩa của thiết kế ngôn ngữ, vẫn có một số đối số khiến nó trở nên gây tranh cãi khi đưa ra lựa chọn giữa chúng trong một số bối cảnh khác, ví dụ, khi đưa ra lựa chọn về các loại tham số.

Nhưng đây không phải là toàn bộ câu chuyện. Ý tôi là, có nhiều thứ hơn con trỏ so với tài liệu tham khảo mà bạn phải xem xét.

Nếu bạn không phải dính vào những lựa chọn quá cụ thể như vậy, trong hầu hết các trường hợp, câu trả lời rất ngắn gọn: bạn không cần phải sử dụng con trỏ, vì vậy bạn không cần . Con trỏ thường đủ tệ vì chúng ngụ ý quá nhiều điều bạn không mong đợi và chúng sẽ dựa vào quá nhiều giả định ngầm làm suy yếu khả năng duy trì và (thậm chí) tính di động của mã. Không cần thiết phải dựa vào con trỏ chắc chắn là một phong cách xấu và nên tránh theo nghĩa của C ++ hiện đại. Xem xét lại mục đích của bạn và cuối cùng bạn sẽ thấy rằng con trỏ là tính năng của các loại cuối cùng trong hầu hết các trường hợp.

  • Đôi khi các quy tắc ngôn ngữ rõ ràng yêu cầu các loại cụ thể được sử dụng. Nếu bạn muốn sử dụng các tính năng này, hãy tuân thủ các quy tắc.
    • Các hàm tạo sao chép yêu cầu các loại cv - &tham chiếu cụ thể làm kiểu tham số thứ nhất. (Và thường thì nênconst đủ tiêu chuẩn.)
    • Các constructor di chuyển yêu cầu các loại cv - &&tham chiếu cụ thể làm kiểu tham số thứ nhất. (Và thường không nên có vòng loại.)
    • Quá tải cụ thể của các nhà khai thác yêu cầu các loại tham chiếu hoặc không tham chiếu. Ví dụ:
      • Quá tải operator=vì các hàm thành viên đặc biệt yêu cầu các loại tham chiếu tương tự như tham số thứ nhất của các hàm tạo sao chép / di chuyển.
      • Postfix ++yêu cầu giả int.
      • ...
  • Nếu bạn biết pass-by-value (nghĩa là sử dụng các loại không tham chiếu) là đủ, hãy sử dụng trực tiếp, đặc biệt khi sử dụng một triển khai hỗ trợ bản sao chép bắt buộc C ++ 17. ( Cảnh báo : Tuy nhiên, để suy luận thấu đáo về sự cần thiết có thể rất phức tạp .)
  • Nếu bạn muốn vận hành một số tay cầm có quyền sở hữu, hãy sử dụng các con trỏ thông minh như unique_ptrshared_ptr(hoặc thậm chí với các tay cầm homebrew nếu bạn yêu cầu chúng mờ đục ), thay vì con trỏ thô.
  • Nếu bạn đang thực hiện một số lần lặp trên một phạm vi, hãy sử dụng các trình lặp (hoặc một số phạm vi chưa được thư viện chuẩn cung cấp), thay vì các con trỏ thô trừ khi bạn tin chắc rằng các con trỏ thô sẽ làm tốt hơn (ví dụ như phụ thuộc ít tiêu đề hơn) các trường hợp.
  • Nếu bạn biết pass-by-value là đủ và bạn muốn một số ngữ nghĩa nullable rõ ràng, hãy sử dụng trình bao bọc như std::optional, thay vì con trỏ thô.
  • Nếu bạn biết pass-by-value không lý tưởng vì những lý do trên và bạn không muốn ngữ nghĩa vô hiệu, hãy sử dụng {lvalue, rvalue, Forward} -references.
  • Ngay cả khi bạn muốn có ngữ nghĩa như con trỏ truyền thống, thường có một cái gì đó phù hợp hơn, như observer_ptrtrong Thư viện cơ bản TS.

Các ngoại lệ duy nhất không thể được xử lý trong ngôn ngữ hiện tại:

  • Khi bạn đang thực hiện các con trỏ thông minh ở trên, bạn có thể phải đối phó với các con trỏ thô.
  • Các thói quen tương tác ngôn ngữ cụ thể đòi hỏi con trỏ, như operator new. (Tuy nhiên, cv - void*vẫn khá khác biệt và an toàn hơn so với các con trỏ đối tượng thông thường vì nó loại trừ các trường hợp con trỏ bất ngờ trừ khi bạn đang dựa vào một số phần mở rộng không tuân thủ void*như của GNU.)
  • Con trỏ hàm có thể được chuyển đổi từ biểu thức lambda mà không cần chụp, trong khi tham chiếu hàm không thể. Bạn phải sử dụng các con trỏ hàm trong mã không chung cho các trường hợp như vậy, thậm chí bạn cố tình không muốn các giá trị nullable.

Vì vậy, trong thực tế, câu trả lời rất rõ ràng: khi nghi ngờ, hãy tránh con trỏ . Bạn phải sử dụng con trỏ chỉ khi có những lý do rất rõ ràng rằng không có gì khác phù hợp hơn. Ngoại trừ một vài trường hợp đặc biệt được đề cập ở trên, các lựa chọn như vậy hầu như không hoàn toàn là C ++ - cụ thể (nhưng có khả năng là ngôn ngữ cụ thể - cụ thể). Những trường hợp như vậy có thể là:

  • Bạn phải phục vụ các API kiểu cũ (C).
  • Bạn phải đáp ứng các yêu cầu ABI của việc triển khai C ++ cụ thể.
  • Bạn phải tương tác trong thời gian chạy với các triển khai ngôn ngữ khác nhau (bao gồm các hội đồng khác nhau, thời gian chạy ngôn ngữ và FFI của một số ngôn ngữ máy khách cấp cao) dựa trên các giả định về việc triển khai cụ thể.
  • Bạn phải cải thiện hiệu quả của bản dịch (biên dịch & liên kết) trong một số trường hợp cực đoan.
  • Bạn phải tránh biểu tượng phình to trong một số trường hợp cực đoan.

Ngôn ngữ trung lập hãy cẩn thận

Nếu bạn đến để xem câu hỏi thông qua một số kết quả tìm kiếm của Google (không cụ thể đối với C ++) , đây rất có thể là địa điểm sai.

Tài liệu tham khảo trong C ++ là khá "kỳ quặc", vì nó về cơ bản không phải là hạng nhất: họ sẽ được đối xử như các đối tượng hoặc các chức năng được gọi để họ không có cơ hội để hỗ trợ một số hoạt động hạng nhất như là toán hạng trái của các toán tử truy cập thành viên độc lập với loại đối tượng được đề cập. Các ngôn ngữ khác có thể có hoặc không có các hạn chế tương tự đối với tài liệu tham khảo của họ.

Tài liệu tham khảo trong C ++ có thể sẽ không bảo tồn ý nghĩa trên các ngôn ngữ khác nhau. Ví dụ, các tài liệu tham khảo nói chung không bao hàm các thuộc tính không hoàn chỉnh trên các giá trị như trong C ++, do đó, các giả định như vậy có thể không hoạt động trong một số ngôn ngữ khác (và bạn sẽ tìm thấy các bản mẫu tương đối dễ dàng, ví dụ Java, C #, ...).

Vẫn có thể có một số thuộc tính chung trong số các tài liệu tham khảo trong các ngôn ngữ lập trình khác nhau nói chung, nhưng hãy để nó cho một số câu hỏi khác trong SO.

(Một lưu ý phụ: câu hỏi có thể sớm hơn đáng kể so với bất kỳ ngôn ngữ "giống như C" nào có liên quan, như ALGOL 68 so với PL / I. )


17

Có thể tham chiếu đến một con trỏ trong C ++, nhưng điều ngược lại là không thể có nghĩa là một con trỏ tới một tham chiếu là không thể. Tham chiếu đến một con trỏ cung cấp một cú pháp sạch hơn để sửa đổi con trỏ. Nhìn vào ví dụ này:

#include<iostream>
using namespace std;

void swap(char * &str1, char * &str2)
{
  char *temp = str1;
  str1 = str2;
  str2 = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap(str1, str2);
  cout<<"str1 is "<<str1<<endl;
  cout<<"str2 is "<<str2<<endl;
  return 0;
}

Và xem xét phiên bản C của chương trình trên. Trong C, bạn phải sử dụng con trỏ tới con trỏ (nhiều cảm ứng) và điều này dẫn đến sự nhầm lẫn và chương trình có thể trông phức tạp.

#include<stdio.h>
/* Swaps strings by swapping pointers */
void swap1(char **str1_ptr, char **str2_ptr)
{
  char *temp = *str1_ptr;
  *str1_ptr = *str2_ptr;
  *str2_ptr = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap1(&str1, &str2);
  printf("str1 is %s, str2 is %s", str1, str2);
  return 0;
}

Truy cập vào phần sau để biết thêm thông tin về tham chiếu đến con trỏ:

Như tôi đã nói, một con trỏ đến một tham chiếu là không thể. Hãy thử chương trình sau:

#include <iostream>
using namespace std;

int main()
{
   int x = 10;
   int *ptr = &x;
   int &*ptr1 = ptr;
}

16

Tôi sử dụng tài liệu tham khảo trừ khi tôi cần một trong hai thứ sau:

  • Con trỏ Null có thể được sử dụng như một giá trị sentinel, thường là một cách rẻ tiền để tránh quá tải chức năng hoặc sử dụng bool.

  • Bạn có thể làm số học trên một con trỏ. Ví dụ,p += offset;


5
Bạn có thể viết &r + offsetnơi rđược tuyên bố là tham chiếu
MM

15

Có một sự khác biệt cơ bản giữa các con trỏ và các tham chiếu mà tôi không thấy ai đã đề cập: các tham chiếu cho phép ngữ nghĩa thông qua tham chiếu trong các đối số hàm. Con trỏ, mặc dù ban đầu nó không hiển thị: chúng chỉ cung cấp ngữ nghĩa truyền qua giá trị. Điều này đã được mô tả rất độc đáo trong bài viết này .

Trân trọng, & rzej


1
Tài liệu tham khảo và con trỏ đều là tay cầm. Cả hai đều cung cấp cho bạn ngữ nghĩa nơi đối tượng của bạn được truyền bằng tham chiếu, nhưng tay cầm được sao chép. Không khác nhau. (Có nhiều cách khác để xử lý quá, chẳng hạn như một khóa để tra cứu trong từ điển)
Ben Voigt

Tôi cũng đã từng nghĩ như thế này. Nhưng xem bài viết liên kết mô tả lý do tại sao nó không phải là như vậy.
Andrzej

2
@Andrzj: Đó chỉ là một phiên bản rất dài của một câu trong nhận xét của tôi: Tay cầm được sao chép.
Ben Voigt

Tôi cần giải thích thêm về điều này "Tay cầm được sao chép". Tôi hiểu một số ý tưởng cơ bản nhưng tôi nghĩ về mặt vật lý tham chiếu và con trỏ đều chỉ ra vị trí bộ nhớ của biến. Có phải nó giống như bí danh lưu trữ biến giá trị và cập nhật nó như giá trị của biến là thay đổi hay cái gì khác? Tôi là người mới và xin đừng đánh dấu nó là một câu hỏi ngu ngốc.
Asim

1
@Andrzej Sai. Trong cả hai trường hợp, việc vượt qua giá trị đang xảy ra. Tham chiếu được truyền theo giá trị và con trỏ được truyền theo giá trị. Nói cách khác gây nhầm lẫn cho người mới.
Miles Rout

14

Có nguy cơ thêm vào sự nhầm lẫn, tôi muốn đưa vào một số đầu vào, tôi chắc chắn rằng nó chủ yếu phụ thuộc vào cách trình biên dịch thực hiện các tham chiếu, nhưng trong trường hợp gcc, ý tưởng rằng một tham chiếu chỉ có thể trỏ đến một biến trên ngăn xếp không thực sự chính xác, lấy ví dụ này:

#include <iostream>
int main(int argc, char** argv) {
    // Create a string on the heap
    std::string *str_ptr = new std::string("THIS IS A STRING");
    // Dereference the string on the heap, and assign it to the reference
    std::string &str_ref = *str_ptr;
    // Not even a compiler warning! At least with gcc
    // Now lets try to print it's value!
    std::cout << str_ref << std::endl;
    // It works! Now lets print and compare actual memory addresses
    std::cout << str_ptr << " : " << &str_ref << std::endl;
    // Exactly the same, now remember to free the memory on the heap
    delete str_ptr;
}

Mà kết quả này:

THIS IS A STRING
0xbb2070 : 0xbb2070

Nếu bạn nhận thấy ngay cả các địa chỉ bộ nhớ hoàn toàn giống nhau, có nghĩa là tham chiếu được trỏ thành công vào một biến trên heap! Bây giờ nếu bạn thực sự muốn trở nên kỳ dị, điều này cũng hoạt động:

int main(int argc, char** argv) {
    // In the actual new declaration let immediately de-reference and assign it to the reference
    std::string &str_ref = *(new std::string("THIS IS A STRING"));
    // Once again, it works! (at least in gcc)
    std::cout << str_ref;
    // Once again it prints fine, however we have no pointer to the heap allocation, right? So how do we free the space we just ignorantly created?
    delete &str_ref;
    /*And, it works, because we are taking the memory address that the reference is
    storing, and deleting it, which is all a pointer is doing, just we have to specify
    the address with '&' whereas a pointer does that implicitly, this is sort of like
    calling delete &(*str_ptr); (which also compiles and runs fine).*/
}

Mà kết quả này:

THIS IS A STRING

Do đó, một tham chiếu IS là một con trỏ bên dưới mui xe, cả hai chỉ lưu trữ một địa chỉ bộ nhớ, trong đó địa chỉ được trỏ đến là không liên quan, bạn nghĩ điều gì sẽ xảy ra nếu tôi gọi std :: cout << str numf; SAU khi gọi xóa & str numf? Chà, rõ ràng là nó biên dịch tốt, nhưng gây ra lỗi phân đoạn khi chạy vì nó không còn trỏ đến một biến hợp lệ, về cơ bản chúng ta có một tham chiếu bị hỏng vẫn còn tồn tại (cho đến khi nó nằm ngoài phạm vi), nhưng vô dụng.

Nói cách khác, một tham chiếu không là gì ngoài một con trỏ có cơ chế con trỏ được trừu tượng hóa, làm cho nó an toàn hơn và dễ sử dụng hơn (không có toán học con trỏ ngẫu nhiên, không trộn lẫn '.' Và '->', v.v.), giả sử bạn đừng thử bất kỳ điều gì vô nghĩa như ví dụ của tôi ở trên;)

Bây giờ bất kể trình biên dịch xử lý các tham chiếu như thế nào, nó sẽ luôn có một loại con trỏ bên dưới mui xe, bởi vì một tham chiếu phải tham chiếu đến một biến cụ thể tại một địa chỉ bộ nhớ cụ thể để nó hoạt động như mong đợi, do đó không có xung quanh điều này (do đó thuật ngữ 'tham khảo').

Quy tắc chính duy nhất quan trọng cần nhớ với các tham chiếu là chúng phải được xác định tại thời điểm khai báo (ngoại trừ tham chiếu trong tiêu đề, trong trường hợp đó phải được xác định trong hàm tạo, sau khi đối tượng chứa trong đó là xây dựng đã quá muộn để định nghĩa nó).

Hãy nhớ rằng, các ví dụ của tôi ở trên chỉ là, các ví dụ minh họa một tham chiếu là gì, bạn sẽ không bao giờ muốn sử dụng một tài liệu tham khảo theo những cách đó! Để sử dụng đúng cách một tài liệu tham khảo, có rất nhiều câu trả lời ở đây đã đánh vào đầu


14

Một sự khác biệt nữa là bạn có thể có các con trỏ tới một kiểu void (và nó có nghĩa là con trỏ tới bất cứ thứ gì) nhưng các tham chiếu đến void bị cấm.

int a;
void * p = &a; // ok
void & p = a;  //  forbidden

Tôi không thể nói rằng tôi thực sự hài lòng với sự khác biệt đặc biệt này. Tôi rất thích nó sẽ được cho phép với tham chiếu ý nghĩa cho bất cứ điều gì có địa chỉ và hành vi tương tự cho các tài liệu tham khảo. Nó sẽ cho phép định nghĩa một số hàm thư viện C tương đương như memcpy bằng cách sử dụng các tham chiếu.


13

Ngoài ra, một tham chiếu là tham số cho hàm được nội tuyến có thể được xử lý khác với con trỏ.

void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
    int testptr=0;
    increment(&testptr);
}
void increftest()
{
    int testref=0;
    increment(testref);
}

Nhiều trình biên dịch khi nội tuyến phiên bản con trỏ sẽ thực sự buộc ghi vào bộ nhớ (chúng tôi đang lấy địa chỉ một cách rõ ràng). Tuy nhiên, họ sẽ để lại tham chiếu trong một thanh ghi tối ưu hơn.

Tất nhiên, đối với các hàm không được trỏ vào con trỏ và tham chiếu sẽ tạo cùng một mã và sẽ tốt hơn là truyền nội tại theo giá trị so với tham chiếu nếu chúng không được sửa đổi và trả về bởi hàm.


11

Một cách sử dụng tham chiếu thú vị khác là cung cấp một đối số mặc định của loại do người dùng xác định:

class UDT
{
public:
   UDT() : val_d(33) {};
   UDT(int val) : val_d(val) {};
   virtual ~UDT() {};
private:
   int val_d;
};

class UDT_Derived : public UDT
{
public:
   UDT_Derived() : UDT() {};
   virtual ~UDT_Derived() {};
};

class Behavior
{
public:
   Behavior(
      const UDT &udt = UDT()
   )  {};
};

int main()
{
   Behavior b; // take default

   UDT u(88);
   Behavior c(u);

   UDT_Derived ud;
   Behavior d(ud);

   return 1;
}

Hương vị mặc định sử dụng tham chiếu 'bind const cho khía cạnh tạm thời' của các tham chiếu.


11

Chương trình này có thể giúp hiểu được câu trả lời của câu hỏi. Đây là một chương trình đơn giản của một tham chiếu "j" và một con trỏ "ptr" trỏ đến biến "x".

#include<iostream>

using namespace std;

int main()
{
int *ptr=0, x=9; // pointer and variable declaration
ptr=&x; // pointer to variable "x"
int & j=x; // reference declaration; reference to variable "x"

cout << "x=" << x << endl;

cout << "&x=" << &x << endl;

cout << "j=" << j << endl;

cout << "&j=" << &j << endl;

cout << "*ptr=" << *ptr << endl;

cout << "ptr=" << ptr << endl;

cout << "&ptr=" << &ptr << endl;
    getch();
}

Chạy chương trình và xem đầu ra và bạn sẽ hiểu.

Ngoài ra, hãy dành 10 phút và xem video này: https://www.youtube.com/watch?v=rlJrrGV0iOg


11

Tôi cảm thấy như có một điểm khác chưa được đề cập ở đây.

Không giống như các con trỏ, các tham chiếu tương đương về mặt cú pháp với đối tượng mà chúng tham chiếu, tức là bất kỳ thao tác nào có thể được áp dụng cho một đối tượng đều hoạt động để tham chiếu và với cú pháp chính xác (tất nhiên ngoại lệ là khởi tạo).

Mặc dù điều này có vẻ bề ngoài, tôi tin rằng tài sản này rất quan trọng đối với một số tính năng của C ++, ví dụ:

  • Mẫu . Do các tham số mẫu được gõ vịt, nên các thuộc tính cú pháp là một vấn đề quan trọng, do đó, thường thì cùng một mẫu có thể được sử dụng với cả TT&.
    (hoặc std::reference_wrapper<T>vẫn dựa vào một diễn viên ngầm định T&)
    Các mẫu bao gồm cả hai T&T&&thậm chí còn phổ biến hơn.

  • Giá trị . Xem xét câu lệnh str[0] = 'X';Không có tham chiếu, nó sẽ chỉ hoạt động cho chuỗi c ( char* str). Trả về ký tự theo tham chiếu cho phép các lớp do người dùng định nghĩa có cùng ký hiệu.

  • Sao chép các nhà xây dựng . Về mặt cú pháp, việc truyền các đối tượng để sao chép các hàm tạo và không trỏ đến các đối tượng là hợp lý. Nhưng không có cách nào để một hàm tạo sao chép lấy một đối tượng theo giá trị - nó sẽ dẫn đến một cuộc gọi đệ quy đến cùng một hàm tạo sao chép. Điều này để lại tài liệu tham khảo là lựa chọn duy nhất ở đây.

  • Vận hành quá tải . Với các tài liệu tham khảo, có thể giới thiệu sự gián tiếp cho một cuộc gọi toán tử - giả sử, operator+(const T& a, const T& b)trong khi vẫn giữ nguyên ký hiệu infix. Điều này cũng hoạt động cho các chức năng quá tải thường xuyên.

Những điểm này trao quyền cho một phần đáng kể của C ++ và thư viện chuẩn, vì vậy đây là một tài sản chính của tài liệu tham khảo.


" diễn viên ngầm " một diễn viên là một cấu trúc cú pháp, nó tồn tại trong ngữ pháp; một dàn diễn viên luôn rõ ràng
tò mò

9

Có một sự khác biệt phi kỹ thuật rất quan trọng giữa con trỏ và tham chiếu: Một đối số được truyền cho hàm bằng con trỏ hiển thị rõ hơn nhiều so với đối số được truyền cho hàm bởi tham chiếu không phải là const. Ví dụ:

void fn1(std::string s);
void fn2(const std::string& s);
void fn3(std::string& s);
void fn4(std::string* s);

void bar() {
    std::string x;
    fn1(x);  // Cannot modify x
    fn2(x);  // Cannot modify x (without const_cast)
    fn3(x);  // CAN modify x!
    fn4(&x); // Can modify x (but is obvious about it)
}

Quay lại C, một cuộc gọi trông giống như fn(x)chỉ có thể được chuyển qua giá trị, vì vậy nó chắc chắn không thể sửa đổi x; để sửa đổi một đối số bạn sẽ cần phải vượt qua một con trỏ fn(&x). Vì vậy, nếu một đối số không có trước một &bạn biết thì nó sẽ không được sửa đổi. (Điều ngược lại, &có nghĩa là đã sửa đổi, là không đúng vì đôi khi bạn sẽ phải chuyển các cấu trúc chỉ đọc lớn bằng constcon trỏ.)

Một số ý kiến ​​cho rằng đây là một tính năng hữu ích khi đọc mã, các tham số con trỏ phải luôn được sử dụng cho các tham số có thể sửa đổi thay vì không consttham chiếu, ngay cả khi hàm không bao giờ mong đợi a nullptr. Đó là, những người cho rằng chữ ký chức năng như fn3()trên không nên được cho phép. Hướng dẫn về phong cách C ++ của Google là một ví dụ về điều này.


8

Có thể một số ẩn dụ sẽ giúp; Trong ngữ cảnh của không gian màn hình máy tính để bàn của bạn -

  • Một tài liệu tham khảo yêu cầu bạn chỉ định một cửa sổ thực tế.
  • Một con trỏ yêu cầu vị trí của một phần không gian trên màn hình mà bạn đảm bảo nó sẽ chứa 0 hoặc nhiều phiên bản của loại cửa sổ đó.

6

Sự khác biệt giữa con trỏ và tham chiếu

Một con trỏ có thể được khởi tạo thành 0 và tham chiếu thì không. Trong thực tế, một tham chiếu cũng phải tham chiếu đến một đối tượng, nhưng một con trỏ có thể là con trỏ null:

int* p = 0;

Nhưng chúng ta không thể có int& p = 0;và cũng có int& p=5 ;.

Trong thực tế để làm điều đó đúng, chúng ta phải khai báo và định nghĩa một đối tượng trước tiên sau đó chúng ta có thể tạo một tham chiếu đến đối tượng đó, vì vậy việc thực hiện đúng mã trước đó sẽ là:

Int x = 0;
Int y = 5;
Int& p = x;
Int& p1 = y;

Một điểm quan trọng khác là chúng ta có thể thực hiện khai báo con trỏ mà không cần khởi tạo tuy nhiên không có điều gì có thể được thực hiện trong trường hợp tham chiếu phải tạo tham chiếu luôn cho biến hoặc đối tượng. Tuy nhiên, việc sử dụng một con trỏ như vậy có rủi ro vì vậy nhìn chung chúng tôi kiểm tra xem con trỏ có thực sự đang trỏ đến một cái gì đó hay không. Trong trường hợp tham chiếu, không cần kiểm tra như vậy, bởi vì chúng tôi đã biết rằng việc tham chiếu đến một đối tượng trong khi khai báo là bắt buộc.

Một điểm khác biệt nữa là con trỏ có thể trỏ đến một đối tượng khác tuy nhiên tham chiếu luôn được tham chiếu đến cùng một đối tượng, hãy lấy ví dụ này:

Int a = 6, b = 5;
Int& rf = a;

Cout << rf << endl; // The result we will get is 6, because rf is referencing to the value of a.

rf = b;
cout << a << endl; // The result will be 5 because the value of b now will be stored into the address of a so the former value of a will be erased

Một điểm khác: Khi chúng ta có một mẫu như mẫu STL, mẫu lớp đó sẽ luôn trả về một tham chiếu, không phải là một con trỏ, để dễ đọc hoặc gán giá trị mới bằng toán tử []:

Std ::vector<int>v(10); // Initialize a vector with 10 elements
V[5] = 5; // Writing the value 5 into the 6 element of our vector, so if the returned type of operator [] was a pointer and not a reference we should write this *v[5]=5, by making a reference we overwrite the element by using the assignment "="

1
Chúng ta vẫn có thể có const int& i = 0.
Revolver_Ocelot

1
Trong trường hợp này, tham chiếu sẽ chỉ được sử dụng trong đọc, chúng tôi không thể sửa đổi tham chiếu const này ngay cả khi sử dụng "const_cast" vì "const_cast" chỉ chấp nhận con trỏ không tham chiếu.
dhokar.w

1
const_cast hoạt động với các tài liệu tham khảo khá tốt: coliru.stacked-crooking.com/a/eebb454ab2cfd570
Revolver_Ocelot

1
bạn đang tạo một diễn viên để tham khảo không chọn một tài liệu tham khảo hãy thử điều này; const int & i =; const_cast <int> (i); tôi cố gắng vứt bỏ các hằng số của tham chiếu để có thể viết và gán giá trị mới cho tham chiếu nhưng điều này là không thể. hãy tập trung !!
dhokar.w

5

Sự khác biệt là biến con trỏ không hằng (không bị nhầm lẫn với con trỏ thành hằng số) có thể bị thay đổi tại một thời điểm trong khi thực hiện chương trình, yêu cầu các toán tử con trỏ được sử dụng (&, *), trong khi các tham chiếu có thể được đặt khi khởi tạo chỉ (đó là lý do tại sao bạn chỉ có thể đặt chúng trong danh sách trình khởi tạo của hàm tạo, chứ không phải bằng cách nào khác) và sử dụng ngữ nghĩa truy cập giá trị thông thường. Về cơ bản các tài liệu tham khảo đã được giới thiệu để cho phép hỗ trợ cho các nhà khai thác quá tải như tôi đã đọc trong một số cuốn sách rất cũ. Như ai đó đã nêu trong chủ đề này - con trỏ có thể được đặt thành 0 hoặc bất kỳ giá trị nào bạn muốn. 0 (NULL, nullptr) có nghĩa là con trỏ được khởi tạo không có gì. Đó là một lỗi để hủy bỏ con trỏ null. Nhưng trên thực tế, con trỏ có thể chứa một giá trị không trỏ đến một vị trí bộ nhớ chính xác. Đến lượt mình, các tham chiếu cố gắng không cho phép người dùng khởi tạo một tham chiếu đến một cái gì đó không thể được tham chiếu do thực tế là bạn luôn cung cấp giá trị của loại chính xác cho nó. Mặc dù có rất nhiều cách để biến tham chiếu được khởi tạo thành một vị trí bộ nhớ sai - tốt hơn là bạn không nên đào sâu điều này vào chi tiết. Ở cấp độ máy, cả con trỏ và tham chiếu đều hoạt động đồng đều - thông qua con trỏ. Hãy nói trong tài liệu tham khảo thiết yếu là cú pháp đường. tham chiếu rvalue khác với điều này - chúng là các đối tượng stack / heap tự nhiên. Mặc dù có rất nhiều cách để biến tham chiếu được khởi tạo thành một vị trí bộ nhớ sai - tốt hơn là bạn không nên đào sâu điều này vào chi tiết. Ở cấp độ máy, cả con trỏ và tham chiếu đều hoạt động đồng đều - thông qua con trỏ. Hãy nói trong tài liệu tham khảo thiết yếu là cú pháp đường. tham chiếu rvalue khác với điều này - chúng là các đối tượng stack / heap tự nhiên. Mặc dù có rất nhiều cách để biến tham chiếu được khởi tạo thành một vị trí bộ nhớ sai - tốt hơn là bạn không nên đào sâu điều này vào chi tiết. Ở cấp độ máy, cả con trỏ và tham chiếu đều hoạt động đồng đều - thông qua con trỏ. Hãy nói trong tài liệu tham khảo thiết yếu là cú pháp đường. tham chiếu rvalue khác với điều này - chúng là các đối tượng stack / heap tự nhiên.


4

nói một cách đơn giản, chúng ta có thể nói một tham chiếu là một tên thay thế cho một biến trong khi đó, một con trỏ là một biến chứa địa chỉ của một biến khác. ví dụ

int a = 20;
int &r = a;
r = 40;  /* now the value of a is changed to 40 */

int b =20;
int *ptr;
ptr = &b;  /*assigns address of b to ptr not the value */
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.