음악, 삶, 개발

17. Vector and Free Store 본문

개발 공부/Principles And Practice Using C++

17. Vector and Free Store

Lee_____ 2020. 7. 24. 04:18

17.1 Introduction 

 

가장 많이 쓰이는 C++ 의 container 는 vector 이다.

vector 에 대해 간략히 정리하면 아래와 같다.

  • 주어진 type 의 element 로 sequence 를 만들수있다.

  • index 로 element 에 접근할수있다.

  • push_back() 함수로 element 를 추가하여 vector 를 확장할수있다.

  • size() 함수로 element 의 숫자를 알수있다.

  • type-safe container 이다.

C++ 에서는 또다른  container 인 string, list, map 등이 있다. (20. 에서 다룸)

하지만, 컴퓨터 메모리는 우리가 사용하는 이런 type 들을 support 하지앟는다.

(다르게 말하면 컴퓨터 메모리는 type 이 뭔지 모른다)

컴퓨터 하드웨어가 support 하는것은 bytes 의 sequence 이다.

push_back() 같은 함수가 뭔지도 모르며, 

컴퓨터가 알고있는 어떻게 byte 를 읽고 쓰는지 (read & write) 뿐이다.

이 챕터와 다음 챕터까지, 우리는 C++ 가 제공하는 기본적인 도구들을 통해

vector 를 어떻게 만드는지 설명할것이다.

이 과정속에서, 우리는 유용한 concept 과 프로그래밍 테크닉,

C++ 의 기능을 활용해 어떻게 code 를 작성해나가는지 배울것이다.

우리는 vector 를 통해 widely 하게 쓰일수있는 여러 프로그래밍 기술들을 배울수있을것이다.

 

Chapter 별로 무슨 내용을 다룰지 간략히 소개하겠다.

 

  • Chapter 17 : 메모리를 다루는법, free store (heap), pointer, cast, reference 
  • Chapter 18 : vector 를 copy 하기, array, pointer
  • Chapter 19 : out-of-range error 다루른 법, template

17.2 vector basics

 

간단한 vector 를 코드로 만들어보고, 그림으로 나타내보자.

vector<double>age(4); // double type 에 4개의 요소를 가진 vector

age[0] = 0.33;
age[1] = 22.0;
age[2] = 27.2;
age[3] = 54.2;

vector 의 내부


17.3 Memory, addresses, and pointers

 

컴퓨터의 memory 는 byte 의 sequence 다. 

메모리안에서 위치를 나타내는 숫자를 address 라고 한다.

address 는 일종의 int 값이라고 볼수있다.

메모리의 첫번째 byte 의 주소는 0, 그 다음 주소는 1, 이렇게 계속해서 더해진다.

그림으로 표현하면 아래와 같다.

메모리

우리가 메모리에 집어넣는 모든것은 address 가 있다.

아래와 같은 코드를 작성했다고 해보자.

int myNum = 17;

이 코드는, int 크기의 메모리를 어딘가에 만들고 value 17 을  그 메모리로 집어넣는다.

이때 address (주소값) 을 쥐고있는 객체를 pointer 라고 한다.

만약 int형의 주소를 쥐고있을수있는 type 을 "pointer to int", int pointer, int* (기보법) 라고 한다.

코드로 작성하면 아래와같다.

int myNum = 17;
int* ptr = &myNum; 

"address of ~ (~ 의 주소)" 를 나타내는 연산자는 & 이다.

&은 객체의 주소값을 얻기위해 사용된다.

위의 코드에서 myNum 의 주소가 4096 이라면, ptr 는 4096 을 쥐게된다.

 

pointer

각 type 은 해당하는 pointer type 을 갖는다.

int x = 17;
int* pi = &x; 

double e = 2.71828;
double* pd = &e;

만약 포인터가 가리키는 객체의 값을 알고있다면, "contents of" 연산자 * 을 사용한다.

예제.

int x = 17;
int* pi = &x;
int valueOfOjbectIndicatedByPiPointer = *pi;

* 연산자는 포인터가 가리키는 객체의 값을 바꾸는데에도 사용할수있다.

예제.

int x = 17;
int* pi = &x;

*pi = 27; // *pi 가 가리키는 x 의 값을 27로 변경.

주의해야할점은 pointer 값을 int 로 출력할수있다고 해도,

pointer 는 int 가 아니다.

예제.

int x = 17;
int* pi = &x;

int i = pi; // error : can't assign an int* to an int
pi = 7; // error : can't assign an int to an int*

이와 유사하게, 서로 다른 type 을 가리키는 pointer 를 assign 할수없다.

int x = 17;
int* pi = &x;

char* pc = pi; // error : can't assign an int* to a char*
pi = pc; // error : can't assign a char* to an int*

만약 int 를 pointer 로 바꾼다던가, pointer 를 다른 type 의 pointer 로 바꾸려한다면, reinterpret_cast (17.8 설명) 를 사용한다.

 

이렇게 메모리에 대해 low-level 관점에서 설명한 이유는,

모든 코드가 high-level 이 될수는 없기때문이다.

하지만 우리는 언제나 가장 highest-level 의 abstraction 을 추구해야한다.


17.3.1 The sizeof operator

 

sizeof(type명) 함수로 각 type 이 메모리에서 차지하는 크기를 확인할수있다.

void main() {
	
    std::cout << sizeof(char) << std::endl; // 1 - 1 byte 의미
    std::cout<< sizeof(int*) << std::endl; 

}

여기서 vector 와 관련해서 착각하기쉬운것이 있다.

vector<int> v {1, 2, 3, 4};

void main() {
	
    std::cout << sizeof(v) << std::endl;  //  20 bytes, vector type 의 크기를 알려줌
    std::cout << v.size() << std:: endl;  //  4, vector 가 가지고있는 element 의 갯수를 알려줌.

}

sizeof() 와 .size() 를 혼동해서는 안된다.


17.4 Free store and pointers

 

우리가 C++ 프로그램을 실행하면, compiler 는 memory 공간을 확보한다.

각 공간의 명칭과 무엇이 그 안에 담기는지 설명하면 아래와 같다.

  • 우리의 code 를 위한 공간 : code storage (또는 text storage)

  • global variable 을 위한 공간 : static

  • local variable 을 위한 공간 : stack 

  • 다른 사용을 위해 남겨진 잔여 공간 : free store (또는 free 또는 heap)

그림으로 표현하면 아래와 같다.

memory layout

free store 에 new 연사자를 통해 공간을 확보할수있다.

double* p = new double[4]; // 4 개의 double 을 free 에 할당함.

위의 코드는, C++ 의 run-time 시스템에게 free store 에 4개의 double 을 할당하고,

첫번째 double 을 위한 pointer 를 return 한다.

그림으로 표현하면 아래와 같다.

 

free store 와 pointer

new 연산자는 생성된 객체의 pointer 를 return 하지만, 

array 같이 여러개의 객체를 생성했다면 그들중 첫번째 객체의 pointer 를 return 한다.

new 를 사용해 객체를 만들때 type 이 서로 일치해야한다.

아래와 같이 할수없다.

char* q = new double[4]; // error : double* assigned to char*

17.4.1 Free-store allocation

  • 할당된 memory 의 주소값을 나타내는 pointer 를 return 한다.

  • pointer 의 값은 메모리의 첫번째 byte 의 address 다.

  • pointer 는 정해진 type 의 객체를 가리킨다.

  • pointer 는 자신이 가리키는 element 가 얼마나 많은지 모른다.

new 연산자는 각각의 element 또는 array 를 둘다 할당할수있다.

int* pi = new int; // 한 개
int* qi = new int[4]; // 4 개

위의 코드를 그림으로 하면..


17.4.2 Access through pointers

 

우리는 pointer 앞에 * 을 붙이거나 [ ] 연산자를 사용하여 객체의 값을 read 혹은 write 할수있다.

/* read */

double* p = new double[4];
double x  = *p; // 첫번째 객체의 값을 읽는다.
double x2 = p[0] // *p 의 사용과 완전히 동일하다.

/* write */

*p = 7.7;
p[2] = 9.9;  //  포인터 p 가 가리키는 3번째 객체에 9.9를 대입한다

* 연산자를  "contents of" 연산자 또는, "dereference (역참조)" 연산자라고 한다.

* 연산자와 달리, [ ] 연산자는 memory 를 객체의 sequence 로 접근한다.

 

이게 전부다. 어려운 메카니즘따위는 없다.


17.4.3 Ranges

 

pointer 사용의 주요한 문제점은, 

포인터 자신이 가리키는 것이 몇개의 element 인지 모른다는것이다.

따라서 아래와 같은 문제가 발생할수있다.

double *pd = new double[3];

pd[2] = 2.2;
pd[4] = 4.4; // 5번째 element 가 있어? 3개밖에 안만들었는데?
pd[-3] = -3.3; // -3 이라는 index 가 있음???

위의 코드를 인간이 보았을때는, 잘못되었다는것을 알지만 compiler 는 모른다.

compiler 는 pointer 의 주소값을 일일히 추적하지않는다.

우리의 코드가 충분히 많은 양의 memory 를 할당했다면, 

pd[-3] 같은 말도 안되는 접근이 가능해지는것이다.

 

이게 가능해지기도 한다...

pd[-3] 이나 pd[4] 가 사용될때 무슨 일이 생길지 우리는 알수없다.

우리가 의도한것이 아니었음은 매우 분명하다.

이러한 사용이, "내 프로그램이 왜 crash 되는지 알수가 없어" 등에 알수없는 문제를 발생시킨다.

이런 out-of-rage 의 접근과 사용은 프로그램과 상관없는 예측불가능한 이상한 문제들을 야기시킬것이다.

이러한 코드의 문제점은, 문제가 발생하기전까지 긴 시간이 걸리고 문제의 근원을 찾기가 너무나 힘들어진다는것이다.

우리는 이런 out-of-range 가 절대 일어나지않는 코드를 작성해야한다.

이것이 우리가 new 를 이용해 memory 를 할당하기보다 vector 를 사용하는 가장 필수적인 이유다.

vector 는 자기자신의 size 를 알고있고, 이것은 out-of-rage access 를 쉽게 막을수있게 해준다.

 

포인터를 사용했을때 out-of-range 가되는 또다른 예를 보자.

double* p = new double; // 1개의 double 할당
double* q = new double[1000];  // 1000개의 double 할당

q[700] = 7.7; // fine
q = p; // 이제 q 가 p 와 같은 객체를 가리킴
double d = q[700]; // out of  range access!

q 는 자기만의 double 이 있었지만, 어느순간 p 가 가리키는곳을 함께 가리키기 시작...

언제나 이런 pointer 의 out-of-range 문제는 pointer 가 자기가 가리키는 객체의 size 를 모르기때문이다.

(이런 문제들을 조금 해결해주는 smart pointer 등은 차후에 배울것이다.)

하지만 pointer 를 이해하는것은 다른 이들의 수많은 코드를 이해하는데 필수적이다.


17.4.4 Initialization

 

원칙은 아래와 같다.

  • 객체는 사용되기전 초기화 되어야한다.

  • pointer 는 초기화 되어야한다.

  • pointer 가 가리키는 객체또한, 이미 초기화되었어야한다. (가리키기전에)

아래와 같은 코드를 절대 절대 절대 작성하지말라.

double* p; // 문제점 시작.
*p0 = 7.0;

이런 코드가 언젠가 out-of-range 가 가져다주는 동일한 문제를 야기시킬것이다.

(내 프로그램이 미스터리하게 crash 가 되요!!!)

엄청나게 많은 수의 "C-Style" 또는 old C++ style 의 프로그램들이 가질수있는 심각한 문제점은 언제나

아래의 접근 방식을 통해 야기된다.

  • 초기화되지않은 pointer 를 통한 접근

  • out-of-rage 접근

이런 bug 를 잡으려고 하는 노력보다, 애초에 차단하려는 노력이 훨씬 훨씬 중요하다.

(이런 bug 를 잡는것은 너무 너무 힘들다)

 

new 로 할당된 메모리는 bulit-in type 을 사용할때 초기화되지않는다.

아래와 같이 초기화할수있다.

double* p1 = new double {5.5};
double* p2 = new double[5] {0, 1, 2, 3, 4}; // 초기화 list
double* p3 = new double[] {0, 1, 2, 3, 4};  // 초기화 list 를 제공했기때문에, [] 로 비워놓을수있다.

/* 절대 하지말것 */
double * d1; // 초기화 안됨
double* d2 = new double; // 초기화안된 double 을 위해 memory 할당
double* d3 = new double[5]; // 5개의 초기화안된 double 을 위해 memory 할당

언제나 우리는 초기화 되지않은 객체에 대해 걱정해야하며, 

우리가 그들을 사용하기이전에 value 를 줄것을 명확히해야한다.

compiler 가 초기화 값을 알아서 제공해주는 경우가 있는데, 절대 이것에 의존하면 안된다.

운영체제마다, 또는 다른 compiler 마다 다른 값을 줄수있고, 

이것은 나의 프로그램이 환겸마다 다르게 작동할수있다는것을 의미한다.

 

built-in type 이 아닌, 사용자 정의 type (class, struct, enum) 등을 사용할때,

우리가 만든 class 가 default constructor 를 가지고있다면 아래와 같이 작성할수있다.

Mine* mine = new Mine; // default constructor 에 의해 초기화
Mine* mine2 = new Mine[17]; // default constructor 에 의해 17개가 초기화

근데 우리가 만든 class 가 constructor 는 가지고있지만, default constructor 는 가지고있지않을때는

우리는 아래와 같이 명시적으로 (explicitly) 초기화해야한다.

Mine* mine = new Mine; // error : no default constructor
Mine* mine2 = new Mine{13}; // OK : initialized to Mine{13}
Mine* mine3 = new Mine[4]; // error : no default constructor
Mine* mine4 = new  Mine[4] {0, 1, 2, 3, 4 } // OK 

new 를 위해 긴 초기화 list 를 작성하는것은 말이 안되지만,

소수의 element 일 경우에는 편리하며 자주 사용된다.

내가 놓친 부분 : default constructor 와 그냥 constructor 에 대해, 따로 공부해야한다.

17.4.5 The null pointer

 

nullptr 는 말그대로 아무것도 가리키지않는 pointer 이다.

pointer 를 초기화할수있는 다른 pointer 가 없을 경우, nullptr 을 사용한다.

double* p = nullptr;

nullptr 는 pointer 가 valid 한지 확인할때 사용할수도있다.

if (p != nullptr) // if (p) 와 같다

 

하지만 위와 같은 경우 if(p) 를 사용하는것이 더 직관적이고 짧으며 일반적이다.


17.4.6 Free-store deallocation

 

new 연산자는 free store 에 메모리를 할당한다.

컴퓨터 메모리는 한정적이기때문에, 우리가 사용을 끝냈다면 다시 free store 에 반납(return) 해야한다.

이렇게 함으로써 free store 는 반납된 메모리를 다른 new 에 의해 재사용할수있는것이다.

큰 프로그램을 개발할때는 메모리를 free 하는것이 매우 필수적이다.

memory leak : 메모리를 할당해놓고 free 하지않음.

free 하지않은 코드를 보자.

double* cal(int resSize, int max) {
	
    double* p = new double[max];
	double* res = new double[resSize];
    
    return res;

}

double* r = calc(100, 1000);

위와 같은 코드는, calc() 이 호출될때마다 메모리 leak 이 발생한다.

delete : 메모리를 free store 로 반환해주는 연산자

delete 하는 코드를 보자.

double* calc(int resSize, int max) {

    double* p = new double[max];
    double* res = new double[resSize];

    delete[] p; // 더 이상 필요없으니 삭제. (free)

    return res;

}

double* r = calc(100, 1000);

delete[] r; // 더 이상 필요없으니 삭제. (free)

delete 를 사용하는 2가지 형태가 있다.

delete p; // new 에 의해 생성된 단일 객체를 위한 메모리 해제
delete[] p; // new 에 의해 생성된 객체의 배열을 위한 메모리 해제

하지만, 프로그래머가 아래와 같은 실수를 할수있다.

int* p = new int { 5 };

delete p; // fine

delete p; // error

이렇게 delete 를 2번했을때 나타날수있는 문제는 2가지다.

  • 첫번째 delete 후, free-store manager 는 내부 data 구조를 바꿨고, 따라서 delete p 는 다시 excute 될수없다.

  • 첫번째 delete 후, free-store manager 는 반환된 memory 를 재활용하기위해 다른 객체에게 주었고,

  • 우리는 2번째 delete 로 그 다른 객체를 지웠다, 이는 프로그램의 error 를 만들것이다.

위의 문제는 실제 프로그램에서 흔히 일어나는 일들이다.

반면에 nullptr 을 여러번 삭제하는것은 전혀 문제가 되지않는다. 애초에 가리키는 객체가 없기때문이다.

int* p = nullptr;

delete p; // fine
delete p; // fine

지금 이 챕터를 읽는 독자들은 나에게 질문할것이다.

"왜 메모리 해제를 귀찮게 신경써야하나요? 컴파일러가 자동으로 알아서 해주면안돼? 인간이 일일히 할 필요없이..."

가능하다. 이것을 garbage collection 이라고 한다.

하지만 이런 자동 garbage collection 이 모든 종류의 프로그램에 ideal 한건 아니다.

당신이 정말 필요하다면, C++ program 에 garbage collector 를 plug 할수있다.

좋은 garbage collector 들을 https://www.stroustrup.com/C++.html 에서 구할수있다.

하지만 이 책에서는 당신 스스로 garbage 를 다루는 법에 대해서 알려줄것이다. (편하게, 효율적으로)

 

"언제 메모리를 누수하지않는게 중요한가요?"

 

영원히(forever) 돌아가는 프로그램은 memory leak 을 감당할수없다.

예를 들어, 운영체제, 대부분의 embeded systems이다.

library 또한 메모리 leak 을 하면 안된다, 왜냐면 누군가가 memory leak 을 하면안되는 시스템에 사용할수도있기에.

우리가 쓰는 Windows 같은 운영체제는 프로그램이 끝났을때 자동으로 deallocation 하지만,

leak 을 하지않으려고 노력하는것이 좋다.


17.5 Destructors

 

꽤 긴데 요약을 하면,

vector 내의 destructor 가 memory 를 free 해주도록 설계되어있기때문에,

new delete 보다 훨씬 낫다는 내용이다.

iostream 또한 buffer 를 flush 를 해줄때 destructor 를 내부적으로 사용한다고 한다.

따라서, resource 를 own 하는 모든 class 는 destructor 가 필요하다.


17.5.1 Generated destructors

 

class 의 멤버로 destructor 가 있다면, 이 member 를 포함하는 객체가 파괴될때

destructor 가 호출된다.

객체가 파괴되고 destructor 가 호출되는 경우는 2가지이다.

  • 객체가 생성된후 scope { } 를 벗어났을때

  • delete keyword 에 의해.


17.5.2 Destructors and free store

 

Destructor 의 사용은 효율적인 C++ 프로그래밍 테크닉의 근간이 된다.

 

  • class 의 객체가 작동하는데 필요한 resource 는 constructor 에 의해 얻어진다.

  • 객체의 lifetime(수명) 동안, 객체는 resource 를 release 하고 새로운 resource 를 얻는다.

  • 객체의 lifetime 이 끝날때, destructor 는 이 객체에 의해 소유(own) 되었던 모든 resource 를 해제한다.

class 가 virtual 함수를 가지고있다면, virtual destructor 가 필요하다.

이유는 아래와 같다.

  1.  class 가 virtual 함수를 가지고있다는것은, base class 로 사용된다는것을 의미한다.

  2.  base class 의 파생 class 가 new 를 사용해 객체를 생성할 가능성이 있다.

  3.  파생 class 의 객체가 new 로 할당되었다면, 자신의 base 를 가리키는 pointer 를 통해 manipulate 된다면

  4.  자신의 base 를 가리키는 pointer 를 통해 delete 될 가능성이 있다.

중요한점은, destructor 는 delete 에 의해 암시적으로, 우회적으로 (implicitly, indirectly) 호출된다는것이다.


17.6 Access to elements

 

생략.


17.7 Pointers to class objects

 

constructor 에서 바깥에서 사용되는 new 를 naked new 라고 한다.

naked new 의 사용은 new 로 생성된 객체를 delete 하는것을 프로그래머가 잊어버릴수있는 문제를 야기한다.

객체를 delete 하는 좋은 전략이 따로 있지않는한,

new 는 constructor 에서, delete 는 destructor 에서 사용하는것이 best practice 이다.

 

객체의 member 를 접근하는 방법은 2가지가 있다.

  • 객체일때 : .(dot) 연산자
  • pointer 일때 : -> (arrow) 연산자

위의 연산자를 member access operator 라고 한다.

 

예를 보자.

/* 객체일때 */

vector v(4);
int x = v.size();

/* 포인터일때 */
vector* p = new vector(4);
int x = p->size();

built-in type 은 member 를 가지고있지않기때문에 member access operator를 사용할수없다.


17.8 Messing with types : void* and casts

 

pointer 와 free-store 에 할당된 array를 사용한다는것은,

우리가 하드웨어에 굉장히 가까이 있음을 의미한다.

우리가 pointer 에게 하는 초기화, assignment, *, [] 같은 연산은 직접적으로 machine에게 연결된다.

가끔 type saftey 하지않는 연산을 불가피하게 해야할때가 있다. (다른 언어와의 interact 를 시도한다던가)

이럴때 우리는 2가지가 필요하다.

  • 메모리안에 객체의 type 을 몰라도, 메모리를 가리킬수있는 pointer

  • compiler 에게 pointer 가 가리키는 memory가  어떤 type 인지 알려주는 연산

이때 type void* 가 필요하다.

void* : compiler 가 type 을 모르는 memory 를 가리키는 pointer

 

우리는 서로 type 을 모르는 code 사이에서 주소값을 넘기려할때 void* 를 사용한다.

이때 그냥 void 와 다른점은, 그냥 void 는 객체가 될수없으며 아무것도 return 하지않는 함수앞에만 사용할수있다.

void v; // error : there are no objects of type void
void* pv1 = new int; // OK : int* converts to void*
void* pv2 = new double[10]; // OK : double* converts to void*

우리는 compiler 에게 void* 가 무엇을 가리키는지 말해줘야한다.

코드의 예를 보자.

void* pv2 = pv; // copying is ok! (copying is what void* is for)
double* pd = pv; // error : cannot convert void* to double*
*pv = 7; // error : cannot dereference a void*
            // 우리는 *pv 가 뭘 가리키는 객체의 type 을 알수없다.
pv[2] = 9; // error : cannot subscript a void*
int* pi = static_cast<int*>(pv); // OK : explicit conversion

static_cast 는 서로 연관있는 type 을 변환할때 사용한다. ex) void* <-> double*

하지만 static_cast 의 사용은 매우 위험하며, 정말 절대로 필요한 상황일때만 사용하라.

당신은 아마도 사용할 일이 거의 없을것이다. (혹은 전혀)

static_cast 가 하는 연산을 explicit type conversion (명시적 타입 변환) 또는 cast 라고 한다.

C++ 는 static_cast 보다 더 위험한 2가지 case 방식을 제공한다.

  • reinterpret_cast : 연관성없는 type 간에 변환을 함. (int <-> double*)
  • const_cast : 객체의 const (상수성) 을 제거함

하지만 당신이 cast 를 사용하고자할때 아래와같이 자신에게 질문해보라.

(cast 를 어떻게든 사용하지않으려해야하기때문에)

  • cast 없이 code 를 작성할수있는 방법은 없는지?
  • 프로그램의 부분을 cast 가 필요없도록 redesign 할수없는지?

당신이 다른 사람의 code 와 협업하지않는이상, 언제나 길은 있다.

cast 를 사용하지않을수있는 코드를 제발 작성하라.


17.9 Pointers and references

 

reference 는 자동으로 dereference 해주는 불변의 pointer, 또는 객체의 또다른 name 으로 볼수있다.

pointer 와 reference 의 차이점은 아래와 같다.

 

  • pointer 에 값을 대입하면, pointer 가 가리키는 객체의 값이 바뀌는게 아니라, pointer 값이 바뀐다. (가리키는 객체가 바뀜)
  • pointer 를 얻기위해서는 new 나 & 가 필요하다.
  • pointer 가 가리키는 객체에 접근하기위해서 * 나 [ ] 을 사용해야한다.
  • reference 에 값을 대입하면 reference 가 가리키는 객체의 (참조하는) 값이 바뀐다.
  • reference 는 초기화된 후, 다른 객체를 가리키게 (참조하게) 할수없다.
  • reference 를 다른 refernece 객체에 대입하면, deep copy 를 수행한다. (pointer 는 가리키는 객체가 바뀜)
  • null pointer 를 조심하라. (nullptr)

pointer 의 예.

 

int x = 10;              
int* p = &x;        // & 를 통해 x 의 주소값을 p 에 대입했다.
*p = 7;             // *p 를 통해 x 에 7 을 대입했다.
int x2 = *p;        // *p 를 통해 x 의 값 10 을 x2 에 대입했다.
int* p2 = &x2;      // & 를 통해 x2 의 주소값을 p2 에 대입했다.
p2 = p;             // p 가 가리키는 객체를 p2 도 가리킨다.
p = &x2;            // p 가 가리키는 객체는 x 에서 x2 가 되었다.

reference 의 예.

int y = 10;
int& r = y;  // & 은 type옆에 붙여야함.
r = 7;  // r 을 통해 y 에 7을 대입했다. (* 필요없음)
int y2 = r; // r 을 통해 y 를 읽고 y2 에 대입하였다. (* 필요없음)
int& r2 = y2; // y2 의 참조를 r2 에 대입하였다.
r2 = r; // r2 가 가리키는 (참조하는) y2 의 값에 r 이 가리키는 (참조하는) y 를 대입하였다.
r  = &y2; // error :  우리는 reference 의 주소값을 초기화후 변경할수없다.

위의 코드처럼,

reference 는 한번 초기화되고나면, 다른 객체를 가리킬수없기때문에,

다른 객체를 가리키고자한다면 pointer 를 사용하라.

reference 와 pointer 는 둘다 memory 주소를 사용하지만, 둘은 서로 memory 주소를 다르게 사용한다.


17.9.1 Pointer and reference parameters

 

우리가 variable 의 값을 함수에서 바꾸고자할때 3가지 선택지가 있다.

  1. value 를 함수 argument 로 넘기고 결과를 return 한다.

  2. pointer를 함수 argument 로 넘긴다.

  3. reference 를 함수 argument 로 넘긴다.

코드로 표현하면 아래와같다.

int valueArg(int x) { return x + 1;}
int pointerArg(int* p) { ++*p; }
int referenceArg(int& r) { ++r; }

가장 선호해야할 스타일은 return 하는것이다. (가장 코드상으로 명확하고, error 를 최소화한다.)

만약 large object 를 return 해야한다면 추후 배울 move constructor 를 사용할수있다. (18.3.4 에 나옴)

 

그럼 pointer vs reference 중에는 뭐가 더 좋은 방법일까?

불행히도, 함수가 하려는 목적에 따라 둘 다 적절히 사용할줄 알아야한다.

어떻게 적절히 사용할수있는지 대답을 해주마.

  • 작은 객체를 compute 할거라면 : pass-by-value

  • nullptr 이 들어와도 작동할수있는 함수라면 : pass-by-pointer (단, nullptr 를 테스트할것을 잊지말아야한다)

  • 이외에는, pass-by-reference


17.9.2 Pointers, references, and inheritance

 

14.3 에서 우리는 파생 class 에 관해 배웠다.

파생 class 의 객체는 base class 의 객체가 필요한곳에 사용될수있다.

pointer 와 reference 에도 마찬가지 사용법을 적용할수있다.

base class 객체를 요구하는곳에 파생 class 의 객체를 사용하는것은

객체 지향 프로그래밍에서 아주 중요한 기술이다. (14.3-4 다시 참고할것)

 

예제.

void roate(Shape* s, int n);

Shape* p = new Circle { Point{100, 100}, 40};
Circle c { Point{200, 200}, 50};
rotate(p, 35);
rotate(&c, 45);

void rotate(Shape& s, int n);

Shape& r = c;
rotate(r, 55);
rotate(*p, 65);
rotate(c, 75);

17.9.3 And example: lists

17.9.4 List operations

17.9.5 List use

17.10 The this pointer

17.10.1 More link use

 

std::list 와 this 에 대한 이야기인데, 추후 사용이 필요하면 그때가서 검색해서 배우자.

 


Review

 

  1. Why do we need data structures with varying numbers of elements?
  2. What four kinds of storage do we have for a typical program?
  3. What is the free store? What other name is commonly used for it? What operators support it?
  4. What is a dereference operator and why do we need one?
  5. What is an address? How are memory addresses manipulated in C++?
  6. What information about a pointed-to object does a pointer have? What useful information does it lack?
  7. What can a pointer point to?
  8. What is a leak?
  9. What is a resource?
  10. How can we initialize a pointer?
  11. What is a null pointer? When do we need to use one?
  12. When do we need a pointer instead of a reference or a named object?
  13. What is a destructor? When do we want one?
  14. When do we want a virtual destructor?
  15. How are destructors for members called?
  16. What is cast? When do we need to use one?
  17. How do we access a member of a class through a pointer?
  18. What is a doubly-linked list?
  19. What is this and when do we need to use it?

Terms

 

  • address
  • address of : &
  • allocation
  • cast
  • container
  • contents of : *
  • deallocation
  • delete
  • delete[]
  • dereference
  • destructor
  • free store
  • link
  • list
  • member access : ->
  • member destructor
  • memory
  • memory leak
  • new
  • null pointer
  • nullptr
  • pointer
  • range
  • resource leak
  • subscripting
  • subscript : [ ]
  • this
  • type conversion
  • virtual destructor
  • void*