c++ 레퍼런스 언어 - C ++에서 포인터 변수와 참조 변수의 차이점은 무엇입니까?



15 Answers

C ++ 참고 자료 ( C 프로그래머 용 )

참조상수 포인터 (상수 값에 대한 포인터와 혼동해서는 안됩니다!)와 자동 간접 지정으로 생각할 수 있습니다. 즉, 컴파일러는 * 연산자를 적용합니다.

모든 참조는 널이 아닌 값으로 초기화되어야하며 그렇지 않으면 컴파일이 실패합니다. 참조 주소를 얻는 것도 불가능합니다. 대신 주소 연산자가 참조 된 값의 주소를 반환합니다. 참조에 대해 연산을 수행 할 수도 없습니다.

C 프로그래머는 간접적 인 일이 발생하거나 인수가 함수 시그니처를 보지 않고 값이나 포인터로 전달되는 경우 더 이상 명확하지 않으므로 C ++ 참조를 싫어할 수 있습니다.

C ++ 프로그래머는 포인터가 안전하지 않은 것으로 간주하기 때문에 포인터를 사용하는 것을 싫어할 수 있습니다. 비록 사소한 경우를 제외하고는 참조가 항상 포인터보다 안전하지는 않지만 자동 간접 지정의 편의성이없고 다른 의미 론적 의미를 지닙니다.

C ++ FAQ 에서 다음 문장을 고려하십시오.

참조가 기본 어셈블리 언어의 주소를 사용하여 구현 되더라도 참조가 객체에 대한 재미있는 포인터라고 생각하지 마십시오 . 참조 객체입니다. 객체에 대한 포인터가 아니며 객체의 복사본도 아닙니다. 그것은 대상입니다.

그러나 참조가 실제로 대상 일 경우 참조가 매달릴 수 있습니까? 관리되지 않는 언어에서는 참조가 포인터보다 '더 안전'하지 못합니다. 일반적으로 범위 경계에서 값을 신뢰할 수있게 별칭을 지정하는 방법이 아닙니다.

왜 내가 C ++ 참조가 유용하다고 생각 하는가?

C 배경에서 나온 C ++ 참조는 다소 어리석은 개념처럼 보이지만 가능하면 포인터 대신 자동 참조가 사용되어야합니다. 자동 간접 참조가 편리하며 참조가 RAII 다룰 때 특히 유용합니다. 장점 이라기보다는 관용구 코드를 덜 어색하게 만든다.

RAII는 C ++의 핵심 개념 중 하나이지만, 의미를 복사하는 것과는 거의 상호 작용하지 않습니다. 참조로 개체를 전달하면 복사가 필요 없으므로 이러한 문제를 피할 수 있습니다. 언어에서 참조가 없으면 대신 포인터를 사용해야하므로 사용하기가 번거롭므로 대안보다 모범 사례 솔루션이 쉬워야한다는 언어 디자인 원칙을 위반합니다.

타입 이중

나는 참조가 문법적인 설탕이라는 것을 안다. 그래서 코드는 읽고 쓰는 것이 더 쉽다.

그러나 차이점은 무엇입니까?

아래 답변 및 링크 요약 :

  1. 바인딩 후 참조를 다시 할당 할 수 없으면 포인터를 여러 번 다시 할당 할 수 있습니다.
  2. 포인터는 아무데도 가리킬 수 없으며 ( NULL ), 참조는 항상 객체를 나타냅니다.
  3. 포인터로 할 수있는 것처럼 참조 주소를 가져올 수 없습니다.
  4. "참조 연산"은 없습니다 (그러나 참조로 가리키는 객체의 주소를 가져 와서 &obj + 5 에서처럼 포인터 연산을 할 수 있습니다).

오해를 명확히하기 위해 :

C ++ 표준은 컴파일러가 참조를 구현하는 방법을 피하기 위해 매우 신중하지만 모든 C ++ 컴파일러는 참조를 포인터로 구현합니다. 즉, 다음과 같은 선언입니다.

int &ri = i;

완전히 최적화되지 않은 경우 포인터와 동일한 양의 저장소를 할당하고 해당 주소를 해당 저장소에 저장합니다.

따라서 포인터와 참조는 모두 동일한 양의 메모리를 사용합니다.

일반적으로,

  • 유용하고 자체적으로 문서화 할 수있는 인터페이스를 제공하기 위해 함수 매개 변수와 반환 유형에서 참조를 사용하십시오.
  • 알고리즘과 데이터 구조를 구현하기위한 포인터를 사용하십시오.

재미있는 읽기 :




대중적인 의견과는 달리, NULL 인 참조를 가질 수 있습니다.

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

허락하신다면 참고 문헌을 사용하는 것이 훨씬 더 어렵습니다. 그러나 그것을 관리하면 머리를 찢어서 찾으려고합니다. 참조는 본질적으로 C ++에서 안전 하지 않습니다 !

기술적으로 이는 null 참조가 아닌 잘못된 참조입니다. C ++은 다른 언어에서 볼 수있는 것처럼 null 참조를 개념으로 지원하지 않습니다. 다른 종류의 잘못된 참조도 있습니다. 유효하지 않은 포인터를 사용하는 것과 마찬가지로 잘못된 참조는 정의되지 않은 동작 의 유령을 발생시킵니다.

실제 오류는 참조에 할당되기 전에 NULL 포인터의 역 참조에 있습니다. 하지만 그 조건에 오류를 생성 할 컴파일러에 대해서는 알지 못합니다. 오류는 코드의 한 지점까지 전파됩니다. 이것이이 문제를 너무 교활하게 만드는 이유입니다. 대부분의 경우 NULL 포인터를 역 참조하면 해당 지점에서 충돌이 발생하고 디버깅을 많이하지 않아도됩니다.

위의 예는 짧고 고안된 것입니다. 다음은 좀 더 실제적인 예입니다.

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

null 참조가 잘못된 형식의 코드를 통해 얻을 수있는 유일한 방법은 일단 정의되지 않은 동작이 발생했다는 것을 반복하고 싶습니다. null 참조를 확인하는 것은 결코 의미가 없습니다 . 예를 들어 if(&bar==NULL)... 시도해 볼 수는 있지만 if(&bar==NULL)... 컴파일러가 문을 최적화하지 못할 수도 있습니다! 유효한 참조는 NULL이 될 수 없으므로 컴파일러의 뷰에서 비교가 항상 거짓이며 if 절을 불필요한 코드로 제거 할 if 있습니다. 이것은 정의되지 않은 동작의 본질입니다.

문제를 해결하는 적절한 방법은 참조를 생성하기 위해 NULL 포인터를 역 참조하는 것을 피하는 것입니다. 이를 위해 자동화 된 방법이 있습니다.

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

더 나은 작문 기술을 가진 사람의이 문제에 대한 더 오래된 예를 보려면 Jim Hyslop 및 Herb Sutter의 Null 참조참조 하십시오.

NULL 포인터 역 참조의 위험에 대한 또 다른 예는 레이몬드 첸 (Raymond Chen)이 다른 플랫폼 으로 코드를 포팅하려고 시도 할 때 정의되지 않은 동작 노출 입니다.




구문 당과는 별도로 참조는 const 포인터입니다 ( const 포인터가 아닙니다 ). 참조 변수를 선언 할 때 참조하는 내용을 설정해야하며 나중에 변경할 수 없습니다.

업데이트 : 이제 생각해 보면 더 중요한 차이가 있습니다.

const 포인터의 대상은 주소를 가져 와서 const 캐스트를 사용하여 바꿀 수 있습니다.

참조의 대상은 UB가 부족한 어떤 방법 으로든 대체 할 수 없습니다.

이렇게하면 컴파일러가 참조에서 더 많은 최적화 작업을 수행 할 수 있습니다.




참조는 포인터와 매우 유사하지만 컴파일러 최적화에 도움이되도록 특별히 제작되었습니다.

  • 참조는 컴파일러가 어떤 참조 별칭을 추적하는지 쉽게 추적 할 수 있도록 설계되었습니다. "참조 연산"이없고 참조를 재 할당하지 않는 두 가지 주요 기능이 매우 중요합니다. 이것들은 컴파일러가 어떤 참조를 컴파일 할 때 어떤 변수에 알릴지를 알 수있게합니다.
  • 참조는 컴파일러가 레지스터에 넣는 것과 같이 메모리 주소가없는 변수를 참조 할 수 있습니다. 지역 변수의 주소를 취하면 컴파일러가 레지스터에 넣기가 매우 어렵습니다.

예로서:

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

최적화 컴파일러는 우리가 [0]과 [1] 꽤 많은 묶음에 접근하고 있다는 것을 깨닫게됩니다. 알고리즘을 최적화하여 다음을 수행하는 것이 좋습니다.

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
}

이러한 최적화를 수행하려면 호출 중에 배열 [1]을 변경할 수있는 것이 없음을 증명해야합니다. 이것은하기 쉽습니다. 나는 결코 2보다 작지 않으므로 array [i]는 배열 [1]을 결코 참조 할 수 없다. maybeModify ()는 a0을 참조로 제공합니다 (별칭 배열 [0]). "참조"연산이 없기 때문에 컴파일러는 maybeModify가 x의 주소를 얻지 못한다는 것을 증명해야하며 배열 [1]을 변경하지 않는다는 것이 증명되었습니다.

또한 a0에 임시 레지스터 복사본이있는 동안 향후 호출이 [0]을 읽고 쓸 수있는 방법이 없음을 증명해야합니다. 많은 경우에 참조가 클래스 인스턴스와 같은 영구 구조에 저장되지 않는다는 것이 명백하기 때문에 이것은 흔히 증명하기가 쉽지 않습니다.

이제 포인터로 같은 작업을 수행하십시오.

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

동작은 동일합니다. maybeModify가 배열 [1]을 수정하지 않는다는 것을 증명하는 것이 훨씬 어렵습니다. 왜냐하면 우리가 이미 포인터를 주었기 때문입니다. 고양이는 가방에서 나와요. 이제는 훨씬 더 어려운 증명을해야합니다 : xModule에 결코 쓰지 않는 것을 증명하는 maybeModify의 정적 분석 또한 배열 [0]을 참조 할 수있는 포인터를 절대로 저장하지 않아야한다는 것을 증명해야합니다. 까다로워.

현대 컴파일러는 정적 분석에서 더 좋아지고 있습니다.하지만 참조를 사용하고 도움을주는 것이 좋습니다.

물론, 영리한 최적화가 없다면, 컴파일러는 필요할 때 참조를 실제로 포인터로 바꿀 것입니다.

편집 : 5 년이 답변을 게시 한 후 참조가 동일한 주소 지정 개념을 보는 다른 방법보다 다른 실제 기술적 차이를 발견했습니다. 참조는 포인터가 할 수없는 방식으로 임시 객체의 수명을 수정할 수 있습니다.

F createF(int argument);

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

일반적으로 createF(5) 호출하여 생성 된 객체와 같은 임시 객체는 표현식의 끝에서 삭제됩니다. 그러나, 그 객체를 참조로 바인딩함으로써, C ++은 ref 가 범위를 벗어날 때까지 그 임시 객체의 수명을 연장 할 것입니다.




두 참조와 포인터는 다른 값에 간접적으로 액세스하는 데 사용되지만 참조와 포인터 사이에는 두 가지 중요한 차이점이 있습니다. 첫 번째는 참조가 항상 객체를 참조한다는 것입니다. 초기화하지 않고 참조를 정의하는 것은 오류입니다. 할당의 동작은 두 번째 중요한 차이입니다. 참조에 지정하면 참조가 바인딩되는 객체가 변경됩니다. 다른 오브젝트에 대한 참조를 리 바인드하지 않습니다. 초기화되면 참조는 항상 동일한 기본 객체를 참조합니다.

이 두 프로그램 단편을 고려하십시오. 먼저 포인터 하나를 다른 포인터에 할당합니다.

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

할당이 끝나면 pi에 의해 처리 된 객체는 변경되지 않습니다. 할당은 pi 값을 변경하여 다른 객체를 가리 킵니다. 이제 두 개의 참조를 지정하는 유사한 프로그램을 고려하십시오.

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

이 할당은 ri에 의해 참조 된 값인 ival을 변경하고 참조 자체는 변경하지 않습니다. 할당 후에는 두 개의 참조가 여전히 원래 객체를 참조하므로 해당 객체의 값도 이제는 동일합니다.




참조는 다른 변수의 별명이지만 포인터는 변수의 메모리 주소를 보유합니다. 참조는 일반적으로 함수 매개 변수로 사용되므로 전달 된 객체는 사본이 아니라 객체 자체입니다.

    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. 



이는 tutorial 기반으로합니다 . 쓰여진 것은 그것을 더 분명하게합니다 :

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

기억하기 만하면,

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

더구나 거의 모든 포인터 자습서를 참조 할 수 있으므로 포인터는 포인터를 배열과 비슷하게 만드는 포인터 산술에 의해 지원되는 객체입니다.

다음 진술을보고,

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

alias_Tomint로서 이해 될 수있다 alias of a variable(서로 다른 typedefalias of a type) Tom. 그것은 또한 그러한 진술의 용어를 잊어도 괜찮습니다의 참조를 만드는 것입니다 Tom.




C ++에서 포인터에 대한 참조가 가능하지만 그 반대가 불가능하다는 것은 참조에 대한 포인터가 불가능하다는 것을 의미합니다. 포인터에 대한 참조는 포인터를 수정하는 더 명확한 구문을 제공합니다. 이 예제를 보자.

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

그리고 위의 프로그램의 C 버전을 고려하십시오. C에서는 포인터에 대한 포인터 (다중 간접 참조)를 사용해야하며 혼동을 일으키고 프로그램이 복잡해 보일 수 있습니다.

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

포인터에 대한 자세한 내용은 다음을 참조하십시오.

앞에서 말한 것처럼 참조 포인터는 불가능합니다. 다음 프로그램을 사용해보십시오.

#include <iostream>
using namespace std;

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



나는 다음 중 하나가 필요하지 않으면 참조를 사용합니다.

  • 널 포인터는 센티널 값으로 사용할 수 있습니다. 종종 함수 오버로딩이나 bool 사용을 피하기위한 저렴한 방법입니다.

  • 포인터에 대해 산술 연산을 수행 할 수 있습니다. 예를 들어,p += offset;




또 다른 차이점은 void 타입에 대한 포인터를 가질 수 있다는 것입니다 (그리고 포인터는 무엇이든 의미합니다). 그러나 void에 대한 참조는 금지되어 있습니다.

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

나는이 특별한 차이에 대해 정말로 행복하다고 말할 수 없다. 나는 주소를 가진 어떤 것에도 의미의 참조를 허용하고 그렇지 않으면 참조를 위해 같은 행동을하는 것이 훨씬 더 좋을 것이다. 그것은 참조를 사용하여 memcpy와 같은 C 라이브러리 함수의 일부 동등한 것을 정의 할 수 있습니다.




또한 인라인 된 함수에 대한 매개 변수 인 참조는 포인터와 다르게 처리 될 수 있습니다.

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

포인터 버전 1을 인라이닝 할 때 많은 컴파일러가 실제로 메모리에 쓰기를 강제합니다 (주소를 명시 적으로 취함). 그러나 그들은 더 최적 인 레지스터에 레퍼런스를 남겨 둘 것이다.

물론, 인라인되지 않은 함수의 경우 포인터와 참조는 동일한 코드를 생성하므로 수정되지 않고 함수에서 반환 된 경우 참조보다는 값으로 내장 함수를 전달하는 것이 좋습니다.




참조의 또 다른 흥미로운 사용법은 사용자 정의 형식의 기본 인수를 제공하는 것입니다.

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

디폴트 플레이버는 '참조에 대한 임시 속성에 대한 바인딩 참조'를 사용합니다.




어쩌면 어떤 은유가 도움이 될 것입니다. 데스크톱 화면 공간의 맥락에서 -

  • 참조에서는 실제 창을 지정해야합니다.
  • 포인터는 화면에 공간의 위치가 필요하다는 것을 나타냅니다.이 공간에는 0 개 이상의 해당 창 유형의 인스턴스가 포함됩니다.



차이점은 상수 포인터 변수와 혼동하지 않는 포인터가 아닌 변수는 프로그램 실행 중 일부 시간에 변경 될 수 있으며 포인터 의미가 사용되어야한다는 것입니다 (&, * 연산자). 반면에 참조는 초기화시 설정 될 수 있습니다 (생성자 이니셜 라이저 목록에서만 설정할 수 있지만 다른 방법으로는 설정할 수없는 이유입니다) 일반 값 액세스 의미를 사용합니다. 기본적으로 몇 가지 오래된 책을 읽었을 때 연산자 오버로드를 지원할 수 있도록 참조가 도입되었습니다. 누군가이 스레드에서 언급했듯이 포인터는 0이나 원하는 값으로 설정할 수 있습니다. 0 (NULL, nullptr)은 포인터가 아무것도 초기화되지 않았 음을 의미합니다. NULL 포인터를 역 참조하는 것은 오류입니다. 그러나 실제로 포인터에는 올바른 메모리 위치를 가리 키지 않는 값이 포함될 수 있습니다.차례 차례 참조는 사용자가 항상 올바른 유형의 rvalue를 제공하기 때문에 참조 할 수없는 항목에 대한 참조를 사용자가 초기화하지 못하게하려는 것입니다. 참조 변수를 잘못된 메모리 위치로 초기화하는 방법은 많이 있지만, 자세한 내용을 자세히 살펴 보지 않는 것이 좋습니다. 기계 수준에서 포인터와 참조는 모두 포인터를 통해 균일하게 작동합니다. 근본적인 참고 문헌에서 통사론 설탕이 있다고 가정 해 봅시다. rvalue 참조는 이것과 다릅니다. 자연스럽게 스택 / 힙 객체입니다.참조 변수를 잘못된 메모리 위치로 초기화하는 방법은 많이 있지만, 자세한 내용을 자세히 살펴 보지 않는 것이 좋습니다. 기계 수준에서 포인터와 참조는 모두 포인터를 통해 균일하게 작동합니다. 근본적인 참고 문헌에서 통사론 설탕이 있다고 가정 해 봅시다. rvalue 참조는 이것과 다릅니다. 자연스럽게 스택 / 힙 객체입니다.참조 변수를 잘못된 메모리 위치로 초기화하는 방법은 많이 있지만, 자세한 내용을 자세히 살펴 보지 않는 것이 좋습니다. 기계 수준에서 포인터와 참조는 모두 포인터를 통해 균일하게 작동합니다. 근본적인 참고 문헌에서 통사론 설탕이 있다고 가정 해 봅시다. rvalue 참조는 이것과 다릅니다. 자연스럽게 스택 / 힙 객체입니다.




나는 항상 this 규칙에 따라 C ++ 핵심 지침에서 결정합니다 .

"인자 없음"이 유효한 옵션 일 때 T &보다 T *를 선호합니다.




Related