음악, 삶, 개발
17. Vector and Free Store 본문
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;
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 을 쥐게된다.
각 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)
그림으로 표현하면 아래와 같다.
free store 에 new 연사자를 통해 공간을 확보할수있다.
double* p = new double[4]; // 4 개의 double 을 free 에 할당함.
위의 코드는, C++ 의 run-time 시스템에게 free store 에 4개의 double 을 할당하고,
첫번째 double 을 위한 pointer 를 return 한다.
그림으로 표현하면 아래와 같다.
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!
언제나 이런 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 가 필요하다.
이유는 아래와 같다.
-
class 가 virtual 함수를 가지고있다는것은, base class 로 사용된다는것을 의미한다.
-
base class 의 파생 class 가 new 를 사용해 객체를 생성할 가능성이 있다.
-
파생 class 의 객체가 new 로 할당되었다면, 자신의 base 를 가리키는 pointer 를 통해 manipulate 된다면
-
자신의 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가지 선택지가 있다.
-
value 를 함수 argument 로 넘기고 결과를 return 한다.
-
pointer를 함수 argument 로 넘긴다.
-
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
- Why do we need data structures with varying numbers of elements?
- What four kinds of storage do we have for a typical program?
- What is the free store? What other name is commonly used for it? What operators support it?
- What is a dereference operator and why do we need one?
- What is an address? How are memory addresses manipulated in C++?
- What information about a pointed-to object does a pointer have? What useful information does it lack?
- What can a pointer point to?
- What is a leak?
- What is a resource?
- How can we initialize a pointer?
- What is a null pointer? When do we need to use one?
- When do we need a pointer instead of a reference or a named object?
- What is a destructor? When do we want one?
- When do we want a virtual destructor?
- How are destructors for members called?
- What is cast? When do we need to use one?
- How do we access a member of a class through a pointer?
- What is a doubly-linked list?
- 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*