[모던C++입문] 1.8 배열, 포인터, 레퍼런스

짜장범벅·2022년 6월 4일
0

모던CPP입문

목록 보기
1/11

1.8 배열, 포인터, 레퍼런스

1.8.2 포인터

초기화하지 않은 포인터에는 무작위 값을 할당하기 때문에, 초기화하지 않은 포인터를 사용하면 오류가 발생할 수 있다. 따라서 포인터가 무언가를 가리키고 있지 않다는 것을 명시적으로 알려주려면 다음과 같이 설정해야 한다.

int* ip = nullptr;
int* ip2{};

위와 같은 방법은 c++ 11이상에서 좋은 방법이다. 반면,

int* ip3 = 0;
int* ip4 = NULL;

은 c++ 11 이상에서는 좋지 않다. 주소값 0은 절대 사용되지 않기 때문에 포인터가 비어있다는 것을 잘 알려주지만 int 0으로 함수 오버로딩에서 모호함을 유발할 수 있다. 또한 NULL 은 단지 전처리 과정에서 0 으로 정의되어있기 때문에 같은 이유로 좋지 않다.

1.8.3 스마트 포인터

c++ 11에 새로운 스마트 포인터 3가지를 도입했다. c++ 03에 있던 auto_ptr 은 더이상 사용하면 안 된다. 만약 c++ 11 이상 사용할 수 없다면 Boost에 있는 스마트 포인터로 대체해야 한다.

1.8.3.1 unique_ptr

이 포인터는 참조한 데이터의 고유 소유권을 나타낸다.

  1 #include <memory>
  2 #include <iostream>
  3 
  4 int main()
  5 {
  6     std::unique_ptr<double> dp{new double};
  7 
  8     *dp = 7;
  9 
 10     std::cout << *dp << std::endl; //expected 7
 11 
 12     //double d;
 13     //unique_ptr<doubledd{d}; //expected error. Because d is not allocated dinamically.
 14 
 15     std::unique_ptr<doubledp2{move(dp)}; //delete dp and move the value of dp to dp2 uniquely.
 16 
 17     if (dp == nullptr)
 18     {
 19         std::cout << "dp is nullptr." << std::endl;
 20     }
 21 
 22     std::cout << "dp2 : " << *dp2 << std::endl;
 23 
 24     std::unique_ptr<doubledp3;
 25     dp3 = move(dp2); //delete dp2 and move the value of dp2 to dp3 uniquely.
 26 
 27     if (dp2 == nullptr)
 28     {
 29         std::cout << "dp2 is nullptr." << std::endl;
 30     }
 31 
 32     std::cout << "dp3 : " << *dp3 << std::endl;
 33 
 34     return 0;
 35 }
7
dp is nullptr.
dp2 : 7
dp2 is nullptr.
dp3 : 7

10: 결과로 dp를 출력했을 때 7이 콘솔 창에 출력된다. 또한 main 함수가 종료될 때, 자동으로 변수 dp에 할당된 메모리에 가서 할당을 해제할 것이다.

15: dpdp2로 move하는 과정에서 dpnullptr이 됨을 알 수 있다. dpnullptr가 되면서 고유 소유권이 유지된다.

  1 #include <memory>
  2 #include <iostream>
  3 
  4 int main()
  5 {
  6     std::unique_ptr<double[]da{new double[3]};
  7 
  8     for (int i=0; i<3; ++i) //available for-loop
  9     {
 10         da[i] = i;
 11     }
 12 
 13     for (int i=0; i<3; ++i)
 14     {
 15         std::cout << "i : " << i << ", da[i] : " << da[i] << std::endl;
 16     }
 17 
 18 #if 0
 19     for (int j=0; j<3; ++j) //NOT available for-loop due to * operator 
 20     {
 21         *(da+j) = j;
 22     }
 23 #endif
 24     
 25     return 0;
 26 }
i : 0, da[i] : 0
i : 1, da[i] : 1
i : 2, da[i] : 2

1.8.3.2 shared_ptr

shared_ptr은 여러 파티에서 공통으로 메모리를 관리한다. 또한 shared_ptr가 더 이상 데이터를 참조하지 않는 즉시 메모리를 자동으로 해제한다. 이는 복잡한 데이터 구조의 경우 매우 편리하다. 모든 스레드가 스레드에 대한 접근이 끝나면 메모리를 자동으로 해제하기 때문이다.

또한 공유 소유권을 갖기 때문에 원하는 만큼 자주 복사할 수 있다.

  1 #include <memory>
  2 #include <iostream>
  3 
  4 int main()
  5 {
  6     std::shared_ptr<doubledp4{new double}; //NOT recommanded way due to memory struture of shared_ptr.
  7     *dp4 = 6;
  8 
  9     std::shared_ptr<doubledp5 = dp4;
 10 
 11     std::cout << "dp4 : " << *dp4 << ", dp5 : " << *dp5 << std::endl;
 12 
 13     std::shared_ptr<doubledp6 = dp4;
 14     std::cout << "dp6 : " << *dp6 << std::endl;
 15 
 16     std::cout << "dp4.use_count() : " << dp4.use_count() << ", dp5.use_count() : " << dp5.use_count() << ", dp6.use_count() : " << dp6.use_    count() << std::endl;
 17 
 18     std::shared_ptr<doubledp7 = std::make_shared<double>(); //better way.
 19     *dp7 = 5;
 20     std::cout << "dp7 : " << *dp7 << std::endl;
 21 
 22     auto dp8 = std::make_shared<double>(); //best way.
 23     *dp8 = 4;
 24     std::cout << "dp8 : " << *dp8 << std::endl;
 25 
 26 
 27     return 0;
 28 }

6: new operator를 이용하면 dp4는 counter, manager 등 data 이외의 값들이 저장된 블록과 data가 저장된 블록을 임의로(대부분 다른 영역으로) 할당한다. 따라서 메모리 관리에 문제를 일으킬 수도 있다. 따라서 18 혹은 22번째 줄과 같이 std::make_shared를 이용해서 할당해야 한다.

16: shared_ptr의 멤버 함수인 use_count()shared_ptr이 가리키는 메모리가 몇 번의 참조를 받는지를 return하는 함수이다.

22: make_shared 는 명시적으로 shared_ptr을 return하기 때문에 auto를 사용하는 것이 휴먼 에러를 줄일 수 있는 방법이다.

1.8.4 레퍼런스

레퍼런스를 별칭으로 생각할 수 있다. 즉, 기존에 있는 개체 또는 하위 개체에 새로운 이름을 도입하는 것이다. 레퍼런스를 정의할 때 마다 포인터와는 달리 어떤 변수를 참조할 것인지를 직접 선언해야 한다. 나중에 다른 변수를 참조할 수는 없다.

 1 #include <iostream>
 2 
 3 int main()
 4 {
 5     int i = 5;
 6     int& j = i;
 7 
 8     std::cout << "i : " << i << ", j : " << j << std::endl;
 9 
10     i = 6;
11     std::cout << "i : " << i << ", j : " << j << std::endl;
12 
13     j = 7;
14     std::cout << "i : " << i << ", j : " << j << std::endl;
15 
16     return 0;
17 }
i : 5, j : 5
i : 6, j : 6
i : 7, j : 7

6: ji를 참조하기 때문에 ij가 항상 같은 값을 갖고 있다. 쉽게 말해 ji의 별칭이다.

1.8.5 포인터와 레퍼런스의 비교

레퍼런스의 특징은 아래와 같다.

  • 정의된 위치 참조
  • 초기화 필수
  • 메모리 누수 방지
  • 개체와 같은 표기법

반면 포인터의 특징은 아래와 같다.

  • 메모리 관리(할당을 의미하는 듯...)
  • 주소 계산
  • 컨테이너 만들기

1.8.6 오래된 데이터를 참조하지 마라!

double& square_ref(double d) {
  double s = d * d;
  return s;
}

위 코드에서는 square_ref 함수가 더이상 존재하지 않는 함수 내 지역 변수 s를 참조해서 리턴한다. square_ref 함수를 호출해서 사용하는 코드에서 s의 메모리가 남아있는지는 불확실하기 때문에 위 코드는 지양하는 것이 좋다.

따라서 동적으로 할당할 데이터나 함수를 호출하기 전의 데이터 또는 static 데이터의 포인터나 레퍼런스를 리턴하는 것이 좋다.

profile
큰일날 사람

0개의 댓글