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


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

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


Tóm tắt từ các câu trả lời và các liên kết dưới đây:

  1. Một con trỏ có thể được gán lại bất kỳ số lần nào trong khi một tham chiếu không thể được gán lại sau khi ràng buộc.
  2. Con trỏ có thể trỏ đến hư không (NULL), trong khi một tham chiếu luôn đề cập đến một đối tượng.
  3. Bạn không thể lấy địa chỉ của một tham chiếu như bạn có thể với con trỏ.
  4. Không có "số học tham chiếu" (nhưng bạn có thể lấy địa chỉ của một đối tượng được trỏ bởi một tham chiếu và làm số học con trỏ trên nó như trong &obj + 5).

Để làm rõ một quan niệm sai lầm:

Tiêu chuẩn C ++ rất cẩn thận để tránh ra lệnh một trình biên dịch có thể   thực hiện các tham chiếu, nhưng mọi trình biên dịch C ++ đều thực hiện   tham chiếu dưới dạng con trỏ. Đó là, một tuyên bố như:

int &ri = i;

nếu nó không được tối ưu hoàn toàn, phân bổ cùng một lượng bộ nhớ   làm con trỏ và đặt địa chỉ   của i vào bộ nhớ đó.

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

Như một quy luật chung,

  • Sử dụng các tham chiếu trong các tham số hàm và kiểu trả về để cung cấp các giao diện hữu ích và tự tạo tài liệu.
  • Sử dụng con trỏ để triển khai các thuật toán và cấu trúc dữ liệu.

Thú vị đọc:


2625
2017-09-11 20:03


gốc


Tôi nghĩ rằng điểm 2 nên là "Một con trỏ được phép là NULL nhưng một tham chiếu không phải là. Chỉ có mã sai có thể tạo ra một tham chiếu NULL và hành vi của nó là không xác định." - Mark Ransom
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. Tài liệu tham khảo mặt khác không bao giờ là đối tượng, chỉ có biến. - Kerrek SB
Điều này biên dịch mà không có cảnh báo: int &x = *(int*)0; trên gcc. Tham chiếu thực sự có thể trỏ đến NULL. - Calmarius
tham chiếu là một bí danh biến - Khaled.K
Tôi thích câu đầu tiên là tổng số sai lầm như thế nào. Tài liệu tham khảo có ngữ nghĩa riêng của họ. - Lightness Races in Orbit


Các câu trả lời:


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

    Không thể tham chiếu 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ỉ và kích thước bộ nhớ riêng 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 gốc) nhưng cũng chiếm một số không gian trên ngăn xếp. Vì tham chiếu có cùng địa chỉ với biến ban đầu, nên an toàn khi nghĩ đến tham chiếu dưới dạng tên khác cho cùng một biến. Lưu ý: Những gì một con trỏ trỏ đến có thể được trên stack hoặc đống. Ditto một tham chiếu. 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 nằm trên ngăn xếp. Vì 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ông tin thêm về 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ỏ đến con trỏ đến con trỏ cung cấp thêm mức độ vô hướng. Trong khi đó, tài liệu tham khảo chỉ cung cấp một mức độ không xác định.

    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. Con trỏ có thể được gán nullptr trực tiếp, trong khi tham chiếu thì không thể. Nếu bạn cố gắng hết sức, và bạn biết làm thế nào, bạn có thể làm cho đị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 ++để chuyển đến mục tiếp theo mà con trỏ trỏ tới và + 4 để đi đến phần tử thứ 5. Điều này là không có vấn đề gì kích thước đối tượng là con trỏ trỏ đến.

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

  7. Một con trỏ là một biến chứa một địa chỉ bộ nhớ. Bất kể tham chiếu được thực hiện như thế nào, tham chiếu có cùng địa chỉ bộ nhớ với mục mà nó tham chiếu.

  8. 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ập bởi người dùng @litb)

  9. Tham chiếu Const có thể bị ràng buộc vào thời gian. Con trỏ không thể (không phải không có một số hướng dẫn):

    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à v.v.


1376
2018-02-27 21:26



... nhưng dereferencing NULL là không 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
Số 2 là không phải thật. Một tham chiếu không đơn giản là "một tên khác cho cùng một biến". Tài liệu tham khảo có thể được chuyển đến các hàm, được lưu trữ trong các lớp, vv 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. - Derek Park
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 cấp phát trên heap. - Derek Park
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) đòi hỏi không gian không phải có nghĩa là nó đòi hỏi không gian trên ngăn xếp. Con trỏ và tham chiếu có thể không chỉ điểm đến heap, họ thực sự có thể được phân bổ trên đống. - Derek Park
khác quan trọng khác: tài liệu tham khảo không thể được nhồi vào một mảng - Johannes Schaub - litb


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

A tài liệu tham khảo có thể được coi là một con trỏ liên tục (không được nhầm lẫn với một con trỏ đến một giá trị không đổi!) với tự động indirection, tức là trình biên dịch sẽ áp dụng * nhà điều hành cho bạn.

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

Các lập trình viên C có thể không thích tài liệu tham khảo C ++ vì nó sẽ không còn rõ ràng khi không xác định xảy ra hoặc nếu một đối số được truyền theo giá trị hoặc bởi 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 con trỏ vì chúng được coi là không an toàn - mặc dù tài liệu tham khảo không thực sự an toàn hơn so với con trỏ liên tục ngoại trừ trong những trường hợp nhỏ nhất - thiếu sự tiện lợi của tự động chuyển hướng và mang ý nghĩa ngữ nghĩa khác.

Xem xét tuyên bố sau từ Câu hỏi thường gặp về C ++:

Mặc dù tham chiếu thường được triển khai bằng địa chỉ trong   ngôn ngữ lắp ráp cơ bản, làm ơn không phải nghĩ về một tham chiếu như một   con trỏ tìm kiếm thú vị cho một đối tượng. Một tài liệu tham khảo  đối tượng. Nó là   không phải là một con trỏ đến đối tượng, cũng không phải là một bản sao của đối tượng. Nó  các   vật.

Nhưng nếu một tham chiếu có thật không là đối tượng, làm thế nào có thể có tham chiếu lơ lửng? Trong các ngôn ngữ không được quản lý, không thể cho các tham chiếu trở nên an toàn hơn so với các con trỏ - thường không chỉ là một cách đá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 xem xét các tham chiếu C ++ hữu ích

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

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 trivially với sao chép ngữ nghĩa. Việc truyền các đối tượng theo tham chiếu tránh các vấn đề này vì không có sự sao chép nào. Nếu tài liệu tham khảo không có trong ngôn ngữ, bạn sẽ phải sử dụng con trỏ để sử dụng, do đó vi phạm nguyên tắc thiết kế ngôn ngữ là 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ế.


301
2017-09-11 21:43



@ 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ả lại một biến tự động bằng cách tham chiếu. - Ben Voigt
@ kriss: Nó hầu như không thể cho một trình biên dịch để phát hiện 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: đó là an toàn và không nên bị cấm bởi trình biên dịch. Sau đó, một người gọi có một cá thể tự động của lớp đó, gọi hàm thành viên đó và trả về tham chiếu. Presto: tham khảo lơ lửng. Và có, nó sẽ gây rắc rối, @ kriss: đó là quan điểm của tôi. Nhiều người cho rằng lợi thế của các tham chiếu trên con trỏ là các tham chiếu luôn hợp lệ, nhưng nó không phải như vậy. - Ben Voigt
@ kriss: Không, tham chiếu đến đối tượng của thời lượng lưu trữ tự động rất khác với đối tượng tạm thời. Dù sao, tôi đã chỉ cung cấp một ví dụ truy cập vào tuyên bố của bạn rằng bạn chỉ có thể nhận được một tài liệu tham khảo không hợp lệ bởi dereferencing một con trỏ không hợp lệ. Christoph là đúng - tài liệu tham khảo không an toàn hơn con trỏ, một chương trình sử dụng tài liệu tham khảo độc quyền vẫn có thể phá vỡ an toàn loại. - Ben Voigt
Tài liệu tham khảo không phải là một loại con trỏ. Họ là một tên mới cho một đối tượng hiện có. - catphive
@catphive: true 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à một ngôn ngữ “huyền diệu” hơn nhiều so với C, và nếu bạn loại bỏ ma thuật khỏi các tham chiếu, bạn sẽ kết thúc bằng một con trỏ - Christoph


Nếu bạn muốn thực sự là một phần nhỏ, có một thứ 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 đó 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 bản chất s3_reference 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 mà bây giờ có cùng tuổi thọ như tham chiếu.

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


152
2017-09-11 21:06



nhưng whats trường hợp sử dụng cho điều này? - Ahmad Mushtaq
Vâng, s3_copy sẽ tạo ra một tạm thời và sau đó sao chép xây dựng nó vào s3_copy trong khi s3_reference trực tiếp sử dụng tạm thời. Sau đó, để được thực sự pedantic bạn cần phải nhìn vào Tối ưu hóa giá trị gia tăng theo đó trình biên dịch được phép elide việc xây dựng bản sao trong trường hợp đầu tiên. - Matt Price
@ digitalSurgeon: Phép thuật có 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 đi ra khỏi phạm vi destructor của thực tế loại tham chiếu (so với loại tham chiếu, có thể là cơ sở) được gọi. Vì nó là một tham chiếu, không có lát cắt sẽ diễn ra ở giữa. - David Rodríguez - dribeas
Cập nhật cho C ++ 11: câu cuối cùng nên đọc "Bạn không thể liên kết một tham chiếu không phải const lvalue tạm thời" bởi vì bạn có thể ràng buộc một không const rvalue tham chiếu đến tạm thời và có cùng hành vi kéo dài suốt đời. - Oktalist
@AhmadMushtaq: Việc sử dụng chính này là lớp học có nguồn gốc. Nếu không có thừa kế liên quan, bạn cũng có thể sử dụng ngữ nghĩa giá trị, mà sẽ được giá 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() sau đó x sẽ đối mặt với vấn đề cắt cổ điển, trong khi Animal& x = ... sẽ hoạt động chính xác. - Arthur Tacca


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

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

Cấp, nó là khó khăn hơn để 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 thấy nó. Tham chiếu là không phải vốn đã an toàn trong C ++!

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

Lỗi thực tế là trong dereferencing của con trỏ NULL, trước khi gán cho một 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 trên điều kiện đó - lỗi sẽ lan truyền đến một điểm nữa trong mã. Đó là điều làm cho vấn đề này trở nên xảo quyệt. Hầu hết thời gian, nếu bạn dereference một con trỏ NULL, bạn sụp đổ ngay tại chỗ đó và nó không mất nhiều gỡ lỗi để con số nó ra.

Ví dụ của tôi ở trên là ngắn và contrived. Đâ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 đang nhận được hành vi không xác định. Nó không bao giờ có ý nghĩa để kiểm tra một tham chiếu 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 tuyên bố ra khỏi sự tồn tại! Một tham chiếu hợp lệ không bao giờ có thể là NULL vì vậy từ quan điểm của trình biên dịch so sánh luôn luôn là sai, và nó là miễn phí để loại bỏ if mệnh đề là 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 rắc rối là tránh dereferencing 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 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 Tham khảo Null từ Jim Hyslop và Herb Sutter.

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


104
2017-09-11 20:07



Mã trong câu hỏi 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ừ thiết lập nó, và so sánh nó. Một 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ì, bao gồm cả xuất hiện để hoạt động chính xác cho đến khi bạn đang đưa ra một bản demo cho các ông chủ lớn. - KeithB
đánh dấu có đối số hợp lệ. lập luận rằng một con trỏ có thể là NULL và bạn phải kiểm tra là không thực sự hoặc: nếu bạn nói một hàm yêu cầu không NULL, thì người gọi phải làm đ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ư dấu đã làm với tham chiếu xấu - Johannes Schaub - litb
Mô tả là sai. Mã này có thể hoặc không thể tạo 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 tham chiếu hoàn toàn hợp lệ. Nó có thể không tạo ra bất kỳ tham chiếu nào cả. - David Schwartz
@ David Schwartz, nếu tôi đang nói về cách mọi thứ phải làm việc theo tiêu chuẩn, bạn sẽ chính xác. Nhưng đó là không phải những gì tôi đang nói về - Tôi đang nói về hành vi 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ề các trình biên dịch điển hình và các kiến ​​trúc CPU cho những gì sẽ có lẽ xảy ra. Nếu bạn cho 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 xem xét tham chiếu có thể xấu, bạn sẽ bị bối rối bởi một vấn đề đơn giản vào một ngày nào đó giống như tôi. - Mark Ransom
Dereferencing 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 tham chiếu là sai. Nếu bạn đang khởi tạo một tham chiếu từ một con trỏ, bạn nên luôn luôn kiểm tra xem con trỏ có hợp lệ hay 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à thứ tốt. Tôi nghĩ rằng vấn đề thực sự ở đây là tài liệu tham khảo KHÔNG cần phải được kiểm tra cho "nullness" khi bạn nhìn thấy một và con trỏ nên được, ở mức tối thiểu, khẳng định. - t0rakka


Ngoài đường cú pháp, tham chiếu là const con trỏ (không phải trỏ đến một 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ó một số chi tiết, có một sự khác biệt quan trọng.

Một 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 một const cast.

Không thể thay thế mục tiêu của tham chiếu bằng bất kỳ cách nào ngắn của UB.

Điều này sẽ cho phép trình biên dịch tối ưu hóa nhiều hơn trên một tham chiếu.


97
2017-09-11 22:10



Tôi nghĩ đây là câu trả lời tốt nhất cho đến nay. Những người khác nói về tài liệu tham khảo và con trỏ như họ là những con thú khác nhau và sau đó đặt ra cách chúng khác nhau trong hành vi. Nó không làm mọi thứ dễ dàng hơn. Tôi luôn hiểu tài liệu tham khảo như là một T* const với đường cú pháp khác nhau (điều đó xảy ra để loại bỏ rất nhiều * và & khỏi mã của bạn). - Carlo Wood
tưởng tượng cố gắng làm cout << 42 << "hello" << endl không có tài liệu tham khảo - pm100
Câu trả lời ngắn và tốt nhất - Kad
"Một 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 một const cast." Làm như vậy là hành vi không xác định. Xem stackoverflow.com/questions/25209838/… để biết chi tiết. - dgnuff
Cố gắng thay đổi hoặc là 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ình đẳng bất hợp pháp. Những gì bạn có thể làm: loại bỏ một chứng chỉ const đã được thêm vào bằng cách chuyển đổi ngầm: int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci); OK. - curiousguy


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

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

foo.bar Là thông suốt vượt trội hơn foo->bar theo cách tương tự vi Là thông suốt vượt trội hơn Emacs :-)


96
2017-09-19 12:23



@Orion Edwards> truy cập thành viên bằng con trỏ sử dụng ->> truy cập thành viên có sử dụng tham chiếu. Đ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 vào các thành viên của con trỏ de-referenced bằng cách sử dụng -> struct Node {Node * next; }; Nút * đầu tiên; // p là tham chiếu đến con trỏ void foo (Node * & p) {p-> next = first; } Node * bar = new Node; foo (bar); - OP: Bạn có quen thuộc với các khái niệm về rvalues ​​và lvalues?
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
@ user6105 Orion Edwards tuyên bố thực sự là 100% đúng. "truy cập các thành viên của [the] de-referenced pointer" 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à quyền truy cập vào các đối tượng đó chính xác là những gì -> cung cấp các tham chiếu đến con trỏ, giống như con trỏ. - Max Truxa
tại sao vậy . và -> có liên quan đến vi và emacs :) - artm
@artM - đó là một trò đùa, và có lẽ không có ý nghĩa với người nói tiếng Anh không phải là người bản địa. Lời xin lỗi của tôi. Để giải thích, liệu vi là tốt hơn emacs là hoàn toàn chủ quan. Một số người nghĩ rằng vi là cao cấp hơn, và những người khác nghĩ ngược lại chính xác. Tương tự, tôi nghĩ sử dụng . tốt hơn là 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


Trên thực tế, một tham chiếu không thực sự giống như một con trỏ.

Trình biên dịch giữ "tham chiếu" cho 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 nào thành địa chỉ bộ nhớ khi biên dịch.

Khi bạn tạo một tham chiếu, bạn chỉ cho trình biên dịch biết rằng bạn gán một tên khác cho biến con trỏ; đó là lý do tại sao các tham chiếu không thể "trỏ tới null", bởi vì một biến không thể là, 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ố lời giải thích về mã thực:

int a = 0;
int& b = a;

Ở đây bạn không tạo 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, a và b, và nó có thể được giải quyết bằng cách sử dụng một trong hai tên.

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 ra 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 cần được tạo và cung cấp tên cần được sử dụng cho các khoảng trắng này. Khai báo một tham số như một tham chiếu chỉ yêu cầu 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ó phạm vi nào khác; 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 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ã cơ bản. Nhưng trong các ví dụ tôi đưa cho bạn, nó rất có thể sẽ không được thực hiện với một con trỏ.


57
2017-09-01 03:44



Tham chiếu là tham chiếu đến giá trị l, không nhất thiết là một biến. Do đó, nó gần giống với một con trỏ hơn là một bí danh thực (một cấu trúc 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 ++ - Arkadiy
Phải, tôi chỉ đang 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 con trỏ mới sẽ. - Vincent Robert
@VincentRobert: Nó sẽ hoạt động giống như một con trỏ ... nếu hàm được inlined, cả tham chiếu và con trỏ sẽ được tối ưu hóa. Nếu có một cuộc gọi hàm, địa chỉ của đối tượng sẽ cần được chuyển đến hàm. - Ben Voigt
int * p = NULL; int & r = * p; tham chiếu trỏ tới NULL; if (r) {} -> boOm;) - sree
Điều này tập trung vào giai đoạn biên dịch có vẻ tốt đẹp, cho đến khi bạn nhớ rằng tài liệu tham khảo có thể được thông qua xung quanh lúc thời gian chạy, tại đó điểm bí danh tĩnh đi ra ngoài cửa sổ. (Và sau đó, tài liệu tham khảo thông thường thực hiện như con trỏ, nhưng tiêu chuẩn không yêu cầu phương pháp này.) - underscore_d


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

  • Tài liệu tham khảo được thiết kế sao cho trình biên dịch dễ dàng hơn để theo dõi các bí danh tham chiếu có biến nào. Hai tính năng chính rất quan trọng: không có "số học tham chiếu" và không gán lại các tham chiếu. Điều này cho phép trình biên dịch tìm ra tham chiếu bí danh mà các biến tại thời gian biên dịch.
  • 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 thanh ghi. Nếu bạn lấy địa chỉ của một biến cục bộ, rất khó cho trình biên dịch đưa nó vào sổ đăng ký.

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

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 một [0] và một [1] khá nhiều. Nó 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 một tối ưu hóa như vậy, nó cần 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 khá dễ làm. tô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]. có lẽModify () được gán a0 làm tham chiếu (mảng răng cưa [0]). Vì không có số học "tham chiếu", trình biên dịch chỉ cần chứng minh rằng có thểModify 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 một [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 đáng để chứng minh, bởi vì trong nhiều trường hợp rõ ràng là tham chiếu không bao giờ được lưu trữ trong một cấu trúc cố định như một cá thể 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ờ nó là khó khăn hơn nhiều để chứng minh rằng có thểModify 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 làm bằng chứng khó khăn hơn nhiều: một phân tích tĩnh của mayModify để chứng minh nó không bao giờ viết lên & x + 1. Nó cũng phải chứng minh rằng nó không bao giờ tiết kiệm được một con trỏ có thể tham chiếu đến mảng [0], mà chỉ là khôn lanh.

Các trình biên dịch hiện đại ngày càng trở nên tốt hơn ở phân tích tĩnh, nhưng nó luôn luôn là tốt đẹp để giúp họ ra ngoài và sử dụng tài liệu tham khảo.

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


50
2017-09-11 20:12



Đúng, cơ thể không cần phải nhìn thấy được. Tuy nhiên, xác định rằng maybeModify không lấy địa chỉ của bất cứ điều gì liên quan đến x là đáng kể dễ dàng hơn 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 đã làm điều đó "một loạt các con trỏ số học không xảy ra" kiểm tra cho một loạt các lý do khác. - Ben Voigt
"Tham chiếu rất giống với con trỏ" - ngữ nghĩa, trong ngữ cảnh thích hợp - nhưng xét về mã được tạo ra, 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 trong điều kiện 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ả ngắn gọn như 'tham chiếu như / thường được thực hiện như con trỏ' . - underscore_d
Tôi có cảm giác rằng một người nào đó đã gắn cờ sai là một nhận xét lỗi thời dọc theo các dòng void maybeModify(int& x) { 1[&x]++; }, các nhận xét khác ở trên đang thảo luận - Ben Voigt


Một tham chiếu không bao giờ có thể NULL.


32
2018-05-20 19:26



Xem câu trả lời của Mark Ransom để biết ví dụ phản biện. Đây là huyền thoại thường được khẳng định nhất về tài liệu tham khảo, nhưng đó là một huyền thoại. Việc đảm bảo 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ó một tham chiếu NULL. Nhưng điều đó cũng giống như nói rằng "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 về những gì có thể xảy ra nếu bạn lái nó ra khỏi đường đi. Nó có thể nổ tung.)" - cmaster
@cmaster: Trong một chương trình hợp lệ, một tham chiếu không được rỗng. Nhưng một con trỏ có thể. Đây không phải là một huyền thoại, đây là một thực tế. - Mehrdad
@Mehrdad Có, các chương trình hợp lệ vẫn ở trên đường. Nhưng không có rào cản giao thông để thực thi chương trình của bạn thực sự. Phần lớn đường thực sự bị mất dấu. Vì vậy, nó rất dễ dàng để có được ra khỏi đường vào ban đêm. Và nó là rất quan trọng để gỡ 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ào bạn có mã như void Foo::bar() { virtual_baz(); } mà segfaults. Nếu bạn không biết rằng các tham chiếu có thể là null, bạn không thể theo dõi null trở về nguồn gốc của nó. - cmaster
int * p = NULL; int & r = * p; tham chiếu trỏ tới NULL; if (r) {} -> boOm;) - - sree
@sree int &r=*p; là hành vi không xác định. Tại thời điểm đó, bạn không có "tham chiếu trỏ tới NULL", bạn có một chương trình không còn có thể được lý luận về ở tất cả. - cdhowie


Trong khi cả hai tham chiếu và con trỏ được sử dụng để gián tiếp truy cập vào một giá trị khác, thì có hai sự 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à một lỗi để định nghĩa một tham chiếu mà không khởi tạo nó. Hành vi của nhiệm vụ là sự khác biệt quan trọng thứ hai: Việc gán cho một tham chiếu sẽ 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. Khi được khởi tạo, 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 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 chỉ định bởi pi vẫn không thay đổi. Nhiệm vụ 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

Bài tập này thay đổi ival, giá trị được tham chiếu bởi ri và không phải là 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 đó bây giờ cũng giống nhau.


30
2017-10-29 17:17



"một tham chiếu luôn đề cập đến một đối tượng" là hoàn toàn sai - Ben Voigt


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

Ở cấp độ cao nhất, ý tưởng tham chiếu là chúng là "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 phải lo lắng về điều đó: bạn phải nghĩ chúng là "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 các 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, có một số khác biệt thực tế giữa con trỏ và tham chiếu. Cú pháp để sử dụng chúng rõ ràng là khác nhau, và bạn không thể "tham chiếu lại" tài liệu tham khảo, có tham chiếu đến hư vô, hoặc có con trỏ để tham khảo.


24
2018-01-01 17:45