10장 포인터와 동적 배열

2023. 11. 26. 13:41프로그래밍 공부/OOP

포인터(pointer): 컴퓨터의 메모리를 더 잘 제어할 수 있게 해주는 구조물

동적으로 할당된 배열: 프로그램을 쓸 때 고정되는 것이 아니라 프로그램이 실행되는 동안 크기가 결정되는 배열

 

10.1 포인터

포인터(ponter): 변수의 메모리 주소

컴퓨터의 메모리가 번호가 매겨진 메모리 위치들(바이트라고 함)로 나뉜다.

변수들이 인접한 메모리 위치들의 시퀀스로 구현된다.

또한 C++ 시스템이 이러한 메모리 주소들을 변수의 이름으로 사용하는 경우도 있다.

예시)

변수가 세 개의 메모리 위치들로 구현되는 경우

이러한 메모리 위치들 중 첫 번째 위치의 주소가

해당 변수의 이름으로 사용되는 경우도 있다.

예를 들어, 변수가 call-by-reference 인수로 사용될 때

호출 함수에 전달되는 것은

변수의 식별자 이름이 아니라 이 주소이다.

이런 식으로 변수의 이름을 짓는 데 사용되는 주소는

변수에 "포인팅"이라고 생각할 수 있기 때문에 포인터라고 부른다.

변수의 이름이 무엇인지 알려주는 것이 아니라

변수가 어디에 있는지 알려줌으로써

변수에 대한 주소가 "포인팅"된다.


당신은 이미 여러 가지 상황에서 포인터를 사용하고 있다.

1) 함수 호출에서 변수가 call-by-reference 인수일 때,

함수는 이 인수 변수를 변수에 대한 포인터 형태로 부여한다.

2) 배열은 첫 번째 배열 요소에 포인터를 부여함으로써 함수에 부여된다.

(당시에 우리는 이 포인터를 메모리 주소라고 불렀지만 그것은 포인터와 같은 것이다.)

이것들은 포인터를 위한 두 가지 강력한 용도이지만 C++ 시스템에 의해 자동으로 처리된다.

이 장에서는 포인터를 조작하기 위해 시스템에 의존하는 대신

포인터를 직접 조작하는 프로그램을 작성하는 방법을 보여준다.

 

포인터 변수

포인터 변수 선언

Type_Name 유형의 다른 변수에 대한 포인터를 보유할 수 있는 변수는

변수 이름의 맨 앞에 별표를 두는 것을 제외하고는

Type_Name 유형의 변수를 선언하는 방식과 유사하게 선언된다.

SYNTAX
Type_Name *Variable_Name1, *Variable_Name2,...;
EXAMPLE
double *pointer1, *pointer2;

 

예시)
변수 p1 및 p2를 선언하여 int형의 변수에 대한 포인터를 고정할 수 있다.

또한 int형의 두 개의 일반 변수 v1 및 v2를 선언한다:
int *p1, *p2, v1, v2;
각 포인터 변수 앞에는 별표가 있어야 한다.

이전 선언에서 두 번째 별표를 생략하면 p2는 포인터 변수가 아니라 일반적인 int형 변수가 됩니다.
포인터와 포인터 변수에 대해 이야기할 때

보통 주소(address)를 말하기보다는 포인팅(pointing)을 말한다.

p1과 같은 포인터 변수가 v1과 같은 변수의 주소를 포함할 때

포인터 변수는

변수 v1을 가리키거나 (point to the variable v1)

변수 v1을 가리키는 포인터(a pointer to the variable v1)

라고 한다.

 

주소 및 숫자
포인터는 주소이고, 주소는 정수이지만, 포인터는 정수가 아니다.

C++는 포인터를 주소로 사용해야 하며, 숫자로는 사용하지 말아야 한다고 주장한다.

포인터는 int형 또는 다른 숫자 형식의 값이 아니다.

일반적으로 포인터를 int 형의 변수에 저장할 수 없다.

시도하면 대부분의 C++ 컴파일러가 오류 메시지나 경고 메시지를 줄 것이다.
또한 포인터에 대해 일반적인 산술 연산을 수행할 수 없다.

 

& 연산자

이전에 선언한 p1이나 p2와 같은 포인터 변수는 v1이나 v2와 같은 변수에 대한 포인터를 포함할 수 있다.

연산자 &를 사용하여 변수의 주소를 결정하고

해당 주소를 포인터 변수에 할당할 수 있다.

예를 들어 다음은 변수 p1을 변수 v1을 가리키는 포인터와 동일하게 설정한다:
p1 = & v1;

 

* 연산자
이제 v1을 참조하는 두 가지 방법이 있다:

v1이라고 부르거나 "p1로 가리킨 변수"라고 부를 수 있다.

C++에서 "p1로 가리킨 변수"라고 말하는 방법은 *p1이다.

 

역참조 연산자
이것은 우리가 p1을 선언할 때 사용했던 것과 동일한 별표이지만, 

별표를 이렇게 사용하면 역참조 연산자(dereferencing operator)라고 하며,

포인터 변수를 역참조(dereferenced)라고 한다.

예시)

v1 = 0;
p1 = &v1;
*p1 = 42;
cout << v1 << endl;
cout << *p1 << endl;

출력 화면)
42
42
p1이 v1을 가리키는 포인터를 포함하는 한,

v1과 *p1은 동일한 변수를 가리킨다.

그래서 *p1을 42와 같게 설정하면

v1도 42와 같게 설정된다.


변수의 주소를 구하는 데 사용되는 기호 &는

call-by-reference 파라미터를 지정하기 위해 함수 선언에서 사용하는 기호와 동일하다.

우연이 아니다.

call-by-reference 인수는 호출 함수에 인수의 주소를 부여함으로써 구현된다는 것을 기억하라.

따라서 기호 &의 이 두 가지 사용은 완전히 동일한 것은 아니지만 매우 밀접한 관련이 있다.
한 포인터 변수의 값을 다른 포인터 변수에 할당할 수 있다.

예시)

p1이 여전히 v1을 가리키고 있다면,

다음은 p2를 설정하여 그것도 가리키게 할 것이다
v1:
p2 = p1;
v1의 값을 변경하지 않은 경우

다음과 같은 경우에도 42가 화면에 출력된다:
cout << *p2;
혼동하지 않도록 주의하자
p1 = p2;
그리고.
*p1 = *p2;

 

포인터 유형
C++가 포인터 유형을 지정하는 방법에는 약간의 불일치(또는 적어도 혼동 가능성이 있음)가 있다.

만약 유형이 int인 매개변수를 사용하려면, 다음 예제와 같이 유형이 int*로 쓰여진다:
void manipulate Pointer(int* p);
동일한 포인터 유형의 변수를 선언하려면 다음 예제와 같이 *가 변수와 함께 사용된다:
int *p1, *p2;
실제로 컴파일러는 *가 int에 붙든 변수 이름에 붙든 상관하지 않으므로 

다음도 컴파일러가 받아들이며 동일한 의미를 갖다:
void manipulatePointer(int *p); //승인되었지만 친절하지 않다.
int* p1, *p2; //수용되지만 위험한다.
그러나 우리는 첫 번째 버전이 더 명확하다고 생각한다.

특히 변수를 선언할 때 각 포인터 변수마다 하나의 *가 있어야 한다.

 

 

* 및 & 연산자
역참조 연산자(dereferencing operator): 포인터 변수가 가리키는 변수를 생성하는 포인터 변수 앞의 * 연산자

연산자의 주소(addressof operator): 해당 변수의 주소를 생성하는 일반 변수 앞에 있는 & 연산자

즉, 해당 변수를 가리키는 포인터를 생성한다.
예를 들어, 다음과 같은 선언을 고려한다
double *p, v;
다음은 p가 변수 v를 가리키도록 p의 값을 설정한다:
p = & v;
*p는 p가 가리키는 변수를 생성하므로 이전 할당 후에 *p와 v는 동일한 변수를 나타낸다.

예를 들어 v라는 이름을 명시적으로 사용하지 않더라도 다음은 v의 값을 9.99로 설정한다:
*p = 9.99;

 

별표를 추가할 때

당신은 포인터 p1과 p2를 다루는 것이 아니라

포인터가 가리키는 변수를 다루는 것이다.

이것은 그림 10.1에 나타나 있는데,

변수는 상자로 표시되고 변수의 값은 상자 안에 적혀 있다.

숫자는 중요하지 않기 때문에 우리는 포인터 변수에 실제 숫자 주소를 표시하지 않았다.

중요한 것은 숫자가 어떤 특정 변수의 주소라는 것이다.

그래서 우리는 실제 주소의 숫자를 사용하기보다는

단지 그 주소를 가진 변수를 가리키는 화살표로 주소를 표시했을 뿐이다.

 

=에 사용되는 포인터 변수
만약 p1과 p2가 포인터 변수라면, 문장

p1 = p2;

는 p1의 값을 변경하여 p2의 메모리 주소가 되도록 한다. 

이를 생각하는 일반적인 방법은

할당이 p1을 변경하여 현재 p2가 가리키는 것과 동일한 것을 가리킨다는 것이다.

 

디스플레이 10.1 포인터 변수를 가진 할당 연산자의 사용

p1 = p2;

 

*p1 = *p2;

 

new

포인터는 변수의 이름을 지정할 식별자가 없어도 프로그램에서 변수를 조작할 수 있다.

연산자 new: 변수의 이름으로 사용할 식별자가 없는 변수를 만들 수 있다.

이러한 이름 없는 변수는 포인터를 통해 참조된다.

예시)

다음은 int 타입의 새로운 변수를 만들고

포인터 변수 p1을 이 새로운 변수의 주소와 동일하게 설정한다

(즉, p1은 이 새로운 이름 없는 변수를 가리킨다):
p1 = new int;
이 새로운 이름 없는 변수를 *p1이라고 할 수 있다.

(즉, p1이 가리키는 변수로).

이 이름 없는 변수를 사용하여 int라는 다른 변수를 사용하여 수행할 수 있다.

예시)

키보드에서 이 이름 없는 변수로 int의 값을 읽고

값에 7을 더한 다음

이 새 값을 출력한다:
cin >> *p1;
*p1 = *p1 + 7;
cout << *p1;

 

동적 변수
new 연산자: 새로운 이름 없는 변수를 만들고 이 새로운 변수를 가리키는 포인터를 반환한다.

new 연산자의 이름을 따서 이 새로운 변수의 유형을 지정한다.

new 연산자를 사용하여 생성된 변수는 프로그램이 실행되는 동안 생성되고 소멸된다.

동적으로 할당된 변수(dyamically allocated variable) or 동적 변수(dynamic variable): new 연산자를 사용하여 생성된 변수

디스플레이 10.2의 프로그램은 포인터와 동적 변수에 대한 몇 가지 간단한 연산을 보여준다.

디스플레이 10.3은 디스플레이 10.2의 프로그램 작동을 그래프로 보여준다.

 

new 연산자를 사용하여 클래스 유형의 동적 변수를 만들 때 클래스에 대한 생성자가 호출된다.

사용할 생성자를 지정하지 않으면 기본 생성자가 호출된다.

예를 들어 다음은 기본 생성자를 호출한다:
SomeClass *classPtr;
classPtr = new SomeClass; //Calls default constructor.

생성자 인수를 포함하는 경우 다음과 같이 다른 생성자를 호출할 수 있다:
classPtr = new SomeClass(32.0, 17);
//Calls SomeClass(double, int).

유사한 표기법을 사용하면 다음과 같이 비클래스 유형의 동적 변수를 초기화할 수 있다:
double *dPtr;
dPtr = new double(98.6); // Initializes *dPtr to 98.6.

 

디스플레이 10.2 포인터 조작 기초

//Program to demonstrate pointers and dynamic variables.
#include <iostream>
using namespace std;

int main( )
{
	int *p1, *p2;
    
	p1 = new int;
	*p1 = 42;
	p2 = p1;
	cout << "*p1 == " << *p1 << endl;
	cout << "*p2 == " << *p2 << endl;
    
	*p2 = 53;
	cout << "*p1 == " << *p1 << endl;
	cout << "*p2 == " << *p2 << endl;
    
	p1 = new int;
	*p1 = 88;
	cout << "*p1 == " << *p1 << endl;
	cout << "*p2 == " << *p2 << endl;
    
	cout << "Hope you got the point of this example!\n";    
	return 0;
}

 

샘플 대화 상자

*p1 == 42
*p2 == 42
*p1 == 53
*p2 == 53
*p1 == 88
*p2 == 53
Hope you got the point of this example!

 

디스플레이 10.3 디스플레이 10.2에 대한 설명

(a) int *p1, *p2;

 

(b) p1 = new int;

 

(c) *p1 = 42;

 

 

(d) p2 = p1;

 

(e) *p2 = 53;

 

(f) p1 = new int;

 

(g) *p1 = 88;

 

 

new 연산자
새 연산자는 지정된 유형의 새 동적 변수를 만들고 이 새 변수를 가리키는 포인터를 반환한다.

예시)

다음은 MyType의 새 동적 변수를 만들고 포인터 변수 p를 이 새 변수를 가리킨다:
MyType *p;
p = new MyType;
유형이 클래스 유형인 경우 새로 생성된 동적 변수에 대해 기본 생성자가 호출된다.

예시)

인수를 포함하여 다른 생성자를 지정할 수 있다:
MyType *mtPtr;
mtPtr = new MyType(32.0, 17); // calls MyType(double, int);
유사한 표기법을 사용하면 다음과 같이 비클래스 유형의 동적 변수를 초기화할 수 있다:
int *n;
n = new int(17); // initializes *n to 17
이전 C++ 컴파일러에서는 새 변수를 생성할 메모리가 부족하면

NULL이라는 특수 포인터를 반환했다.

C++ 표준은 새 변수를 생성할 메모리가 부족하면

기본적으로 new 연산자는 프로그램을 종료한다.

 

기본 메모리 관리

freestore 또는 heap

메모리의 특별한 영역인 freestore(프리스토어) 또는 힙은 동적으로 할당된 변수에 대해 예약되어 있다. 

프로그램에 의해 생성된 임의의 새로운 동적 변수는 freestore의 메모리 중 일부를 소비한다. 

만약 당신의 프로그램이 너무 많은 동적 변수를 생성한다면, 

그것은 freestore의 모든 메모리를 소비할 것이다. 

만일 이런 일이 발생한다면, new에 대한 추가 호출은 실패할 것이다. 

당신이 freestore의 모든 메모리를 다 소진한 후 

new를 사용할 때 어떤 일이 일어나는지는 컴파일러의 최신 상태에 따라 달라질 것이다. 

이전의 C++ 컴파일러에서는 

새로운 변수를 생성하기에 사용 가능한 메모리가 부족하면 NULL이라는 특별한 값을 반환했다. 

당신에게 새로운 C++ 표준을 완전히 따르는 컴파일러가 있다면, 

새로운 변수를 생성하기에 사용 가능한 메모리가 부족하면, 새로운 연산자는 프로그램을 종료한다. 

18장에서는 새로운 프로그램이 freestore를 소진할 때 

중단 이외의 다른 작업을 수행할 수 있도록 프로그램을 구성하는 방법에 대해 설명한다. 
이전 컴파일러가 있는 경우,

new 호출이 new 호출에 의해 NULL이 반환되었는지 테스트하여 new 호출이 성공했는지 확인할 수 있다.

예시)

다음 코드는 새로운 동적 변수를 생성하려는 시도가 성공했는지 확인하기 위해 테스트한다.

new 호출이 원하는 동적 변수를 생성하지 못하면 프로그램은 오류 메시지와 함께 종료된다:

int *p;
p = new int;
if (p == NULL)
{
    cout << "Error: Insufficient memory.\n";
    exit(1);
}
//If new succeeded, the program continues from here.

(이 코드는 exit을 사용하므로 헤더 파일 <cstdlib> 또는 일부 구현에서는 

P<stdlib.h>가 있는 라이브러리에 대한 include 명령어가 필요하다.)

 

NULL은 0이다
상수 NULL은 실제로 숫자 0이지만

포인터 변수에 할당할 수 있는 특수 목적 값을 의미한다는 것을 분명히 하기 위해

NULL로 생각하고 철자를 쓰는 것이 좋다.

NULL의 다른 용도에 대해서는 이 책의 뒷부분에서 설명하겠다.
식별자 NULL의 정의는 <iostream> 및 <cstddef>와 같은 여러 표준 라이브러리에 있으므로 

NULL을 사용할 때는 <iostream> 또는 <cstddef>(또는 다른 적합한 라이브러리)에 포함 명령어를 사용해야 한다.

 

우리가 말했듯이 NULL은 실제로 숫자 0에 불과한다.

NULL의 정의는 NULL을 0으로 대체하는 C++ 전처리기에 의해 처리된다.

따라서 컴파일러는 실제로 "NULL"을 보지 않으므로 네임스페이스 문제가 없으므로

NULL에 대한 지시어 사용이 필요하지 않다.
(NULL 포인터를 C 문자열을 종료하는 데 사용되는 null 문자 '\0'과 혼동하지 말자.

둘은 서로 다르다. 하나는 정수 0이고 다른 하나는 문자 '\0'이다.)

 

최신 컴파일러들은 새로운 동적 변수가 생성되었는지 확인하기 위해

이전의 명시적인 검사를 필요로 하지 않다.

새로운 컴파일러들의 경우,

새로운 것에 대한 호출이 원하는 동적 변수를 생성하지 못하면

프로그램은 자동으로 오류 메시지와 함께 종료된다.
그러나 컴파일러를 사용할 경우 이전 검사를 수행하면 

아무런 문제가 발생하지 않으며 프로그램을 더 쉽게 휴대할 수 있다.

 

NULL
NULL은 특별한 상수 포인터 값으로, 그렇지 않으면 값이 없을 포인터 변수에 값을 부여하는 데 사용된다.

NULL은 어떤 유형의 포인터 변수에도 할당될 수 있다.

식별자 NULL은 <iostream>을 포함한 여러 라이브러리에 정의되어 있다.

(상수 NULL은 실제로 정수 0이다.)

 

delete

프리스토어의 크기는 C++를 구현할 때마다 다르다.

일반적으로 크기가 크며 적당한 프로그램은 프리스토어의 모든 메모리를 사용하지 않을 것이다.

그러나 적당한 프로그램에서도 더 이상 필요하지 않은 프리스토어 메모리를 재활용하는 것이 좋은 방법이다.

프로그램에서 더 이상 동적 변수가 필요하지 않으면

해당 동적 변수가 사용한 메모리를 프리스토어 관리자에게 반환하여

다른 동적 변수를 만드는 데 메모리를 재활용할 수 있다.

삭제 연산자는 동적 변수를 제거하고 동적 변수가 사용한 메모리를

프리스토어 관리자에게 반환하여 메모리를 재사용할 수 있도록 한다.

p가 동적 변수를 가리키는 포인터 변수라고 가정하자.

다음은 p가 가리키는 동적 변수를 파괴하고

동적 변수가 사용한 메모리를 프리스토어 관리자에게 반환하여 재사용할 것이다:

delete p;

 

삭제 연산자
delete 연산자는 동적 변수를 제거하고 동적 변수가 사용한 메모리를 freestore에 반환한다.

그러면 메모리를 재사용하여 새로운 동적 변수를 만들 수 있다. 

예시)

포인터 변수 p가 가리키는 동적 변수를 제거하는 것은 다음과 같다:
delete p;
삭제 호출 후 p와 같이 포인터 변수의 값이 정의되지 않는다.
(이 장 뒷부분에서 설명하는 약간 다른 버전의 delete는 동적으로 할당된 변수가 배열인 경우에 사용된다.)

 

함정: 댕글링 포인터

포인터 변수에 delete를 적용하면 포인터 변수가 가리키는 동적 변수가 소멸된다.

그 시점에서 포인터 변수의 값은 정의되지 않으므로 어디를 가리키는지 알 수 없다.

게다가 다른 포인터 변수가 소멸된 동적 변수를 가리키고 있다면 이 다른 포인터 변수도 정의되지 않다.

댕글링 포인터(dangling pointer): 정의 되지 않은 포인터 변수

만약 p가 댕글링 포인터이고 프로그램이 역참조 연산자 *를 p에 적용하면

(식 *p를 생성하는) 결과는 예측할 수 없고 보통 참담하다.

참조 취소 연산자를 적용하기 전에 역참조 연산자 *를 적용하면 결과는 예측할 수 없고 보통 참담하다
* 포인터 변수에 대해 포인터 변수가 어떤 변수를 가리키는지 확인해야 한다.
C++에는 포인터 변수가 댕글링 포인터인지 여부를 확인할 수 있는 테스트가 내장되어 있지 않습니다.
댕글링 포인터를 피하는 한 가지 방법은 임의의 댕글링 포인터 변수을 NULL과 같게 설정하는 것이다.

그러면 포인터 변수에 역참조 연산자 *를 적용하기 전에 포인터 변수가 NULL과 동일한지 테스트할 수 있다.

이 기법을 사용하면 모든 댕글링 포인터를 NULL과 동일하게 설정하는 코드에 따라 삭제하는 호출을 수행한다.

delete 호출에 사용된 포인터 변수 하나 외에 다른 포인터 변수들도 댕글링 포인터가 될 수 있으므로

모든 댕글링 포인터를 NULL로 설정해야 한다.

댕글링 포인터를 계속 추적하여 NULL로 설정하거나 역참조되지 않도록 하는 것은 프로그래머가 결정할 일이다.

 

동적 변수 및 자동 변수

동적 변수(또는 동적으로 할당된 변수): 새로운 연산자로 생성된 변수. 프로그램이 실행되는 동안 생성되고 소멸된다.

지역 변수: 함수 정의 내에 선언된 변수. 특정한 동적 특성을 갖지만 동적 변수라고 부르지는 않는다.

변수가 함수에 대해 로컬인 경우, 함수가 호출될 때 C++ 시스템에 의해 생성되고 함수 호출이 완료되면 소멸된다.

프로그램의 main 부분은 실제로 main이라고 불리는 함수에 불과하므로,

이는 프로그램의 main 부분에 선언된 변수에도 해당된다.

(main에 대한 호출은 프로그램이 끝날 때까지 끝나지 않으므로,

main에 선언된 변수는 프로그램이 끝날 때까지 소멸되지 않지만,

지역 변수를 처리하는 메커니즘은 다른 함수와 마찬가지이다.)

이러한 지역 변수들은 동적 특성이 자동으로 제어되므로 자동 변수라고 불리기도 한다.

자신이 선언된 함수가 호출될 때 자동으로 생성되고 함수 호출이 종료될 때 자동으로 소멸된다.
전역(global) 변수: 외부 메인을 포함하여 어떤 함수나 클래스 정의 밖에 선언된 변수

이러한 글로벌 변수는 동적 변수나 자동 변수와는 대조적으로 진정으로 정적이기 때문에 정적으로 할당된 변수라고 불리기도 한다.

 

팁: 포인터 유형 정의

각 포인터 변수 앞에 별표를 둘 필요 없이 포인터 변수를 다른 변수처럼 선언할 수 있도록 포인터 유형 이름을 정의할 수 있다.
예시)

다음은 int 변수에 대한 포인터를 포함하는 포인터 변수의 유형인 IntPtr이라는 유형을 정의한다:
typedef int* IntPtr;
따라서 다음 두 개의 포인터 변수 선언은 동등하다:
IntPtr p;
그리고.
int *p;

 

typedef
typeef를 사용하여 모든 유형 이름 또는 정의에 대한 별칭을 정의할 수 있다.

예시)

다음은 Kykmers라는 유형 이름을 double로 정의하는 것과 동일한 의미로 정의한다:
typedef double Kilometers;
이 유형을 정의한 후에는 다음과 같이 double형의 변수를 정의할 수 있다:
Kilometers distance;
이런 식으로 기존 유형의 이름을 변경하는 것이 유용할 수 있다.

그러나 typedef는 포인터 변수의 유형을 정의하는 데 주로 사용된다.
typedef는 새로운 type을 생성하는 것이 아니라 단순히 type 정의에 대한 별칭임을 명심해야 한다.

예) 이전의 Kilometers 정의에서 Kilometers 유형 변수를 double형 매개변수로 대체할 수 있다.
Kilometers와 double은 같은 유형의 두 이름이다.

 

포인터 유형 이름 사용의 이점
이전에 정의된 IntPtr과 같이 정의된 포인터 유형 이름을 사용하면 두 가지 이점이 있다.

1. 별표를 생략하는 실수를 방지한다.

p1과 p2를 포인터로 사용하려고 하면 다음은 실수라는 점을 기억하라:
int *p1, p2;
p2에서 *가 빠졌기 때문에 변수 p2는 포인터 변수가 아닌 그냥 일반적인 int형 변수이다.

혼동해서 int에 *를 붙이면 문제는 같지만 더 알아차리기 어렵다.

C++를 사용하면 int와 같은 유형 이름에 *를 붙일 수 있으므로 다음은 합법적이다:
int* p1, p2;

이것은 합법적이기는 하지만, 오해의 소지가 있다.

p1과 p2가 모두 포인터 변수인 것처럼 보이지만,

사실 p1만 포인터 변수이고, p2는 일반적인 int 변수이다.

C++ 컴파일러에 관한 한,

식별자 int에 붙어 있는 *도 식별자 p1에 붙어 있을 수 있다.

p1과 p2를 모두 포인터 변수로 선언하는 한 가지 올바른 방법은
int *p1, *p2;
p1과 p2를 모두 포인터 변수로 선언하는 방법은 다음과 같이 정의된 유형 이름 IntPtr을 사용하는 것이다:
IntPtr p1, p2;
2. 포인터 변수에 대한 참조별 매개변수를 사용하여 함수를 정의할 때 나타난다.

정의된 포인터 타입 이름이 없으면

함수의 선언에 *와 &를 모두 포함해야 하며, 세부 사항이 혼동될 수 있다.

포인터 타입에 대한 형식 이름을 사용할 경우

포인터 타입에 대한 call-by-reference 매개변수는 복잡한 문제가 없다.

다른 참조별 매개변수를 정의할 때와 마찬가지로

정의된 포인터 타입에 대한 call-by-reference 매개변수를 정의한다.

다음 예제는 다음과 같다:

void sampleFunction(IntPtr& pointerVariable);

 

유형 정의
유형 정의에 이름을 할당한 다음 유형 이름을 사용하여 변수를 선언할 수 있다.

이 작업은 typedef 키워드를 사용하여 수행된다.

이러한 유형 정의는 일반적으로 프로그램의 본문 외부와 다른 함수의 본문 외부, 일반적으로 파일 시작 부근에 배치된다.

이렇게 하면 typedef는 전역적이고 전체 프로그램에서 사용할 수 있다.

여기 예제와 같이 유형 정의를 사용하여 포인터 유형의 이름을 정의한다.
SYNTAX
typedef Known_Type_Definition New_Type_Name;
EXAMPLE
typedef int* IntPtr;
그런 다음 다음 다음 예제와 같이 유형 이름 IntPtr을 사용하여 유형 int의 동적 변수에 대한 포인터를 선언할 수 있다:
IntPtr pointer1, pointer2;

 

함정: call-by-value 매개변수로서의 포인터

call-by-value 매개변수가 포인터 형식일 때, 그 동작은 때때로 미묘하고 귀찮을 수 있다.

그림 10.4의 함수 호출을 생각해 보라.

함수 sneaky의 매개변수 temp는 call-by-value 매개변수이므로 지역 변수이다.

함수가 호출되면 temp의 값이 인수 p의 값으로 설정되고 함수 본체가 실행된다.

temp는 로컬 변수이므로 함수 sneaky 밖으로 나가면 안 된다.

특히 포인터 변수 p의 값은 변경되어서는 안 된다.

그러나 예제 대화를 보면 포인터 변수 p의 값이 변경된 것처럼 보인다.

함수 sneaky를 호출하기 전에는 *p의 값이 77이었고

sneaky를 호출한 후에는 *p의 값이 99이다.

무슨 일이 일어났을까?


이 상황은 디스플레이 10.5에 도식화되어 있다.

샘플 대화에서 p가 변경된 것처럼 보일 수 있지만

함수 호출에 의해 p의 값이 snicky로 변경되지는 않았다.

포인터 p는 p의 포인터 값과 p가 가리키는 곳에 저장된 값 두 가지가 관련되어 있다.

그러나 p의 값은 포인터(즉, 메모리 주소)이다.
sneaky로의 호출 후 변수 p는 동일한 포인터 값(즉, 동일한 메모리 주소)을 포함한다.

sneaky로의 호출은 p가 가리키는 변수의 값을 변경시켰지만 p의 값 자체는 변경하지 않았다.
매개 변수유형이 포인터 유형의 멤버 변수가 있는 클래스 또는 구조 유형인 경우 

클래스 유형의 호출 단위 인수에서도 동일한 종류의 놀라운 변경 사항이 발생할 수 있다.

그러나 클래스 유형의 경우 이 장에서 나중에 설명하는 것처럼

복사 생성자를 정의하여 이러한 놀라운 변경 사항을 피할 수 있다.

 

디스플레이 10.4 call-by-value 포인터 매개변수

//Program to demonstrate the way call-by-value parameters
//behave with pointer arguments.
#include <iostream>
using namespace std;

typedef int* IntPointer;

void sneaky(IntPointer temp);

int main( )
{
	IntPointer p;
    
	p = new int;
	*p = 77;    
	cout << "Before call to function *p == "
	     << *p << endl;
         
	sneaky(p);
    
	cout << "After call to function *p == "
	     << *p << endl;
         
	return 0;
}
void sneaky(IntPointer temp)
{
	*temp = 99;
	cout << "Inside function call *temp == "
	     << *temp << endl;
}

 

샘플 대화 상자

Before call to function *p == 77
Inside function call *temp == 99
After call to function *p == 99

 

포인터에 사용

17장에서는 포인터를 사용하여 여러 가지 유용한 데이터 구조를 만드는 방법에 대해 설명한다.

이 장에서는 포인터를 사용하는 방법, 즉 참조 배열에 대한 한 가지 방법,

특히 동적으로 할당된 배열이라고 하는 배열을 만들고 참조하는 방법에 대해서만 설명한다.

10.2절에서는 동적으로 할당된 배열에 대해 설명한다.

 

디스플레이 10.5 함수 호출 sneaky(p);

1. sneaky 호출 전:

 

2. temp에 대해 p값이 연결되어 있다:

 

3. *temp로 변경:

 

4. sneaky 호출 후:

 

10.2 동적 배열

이 절에서는 배열 변수가 실제로는 포인터 변수임을 알게 될 것이다.

배열이 동적으로 할당된 프로그램을 쓰는 방법도 알아볼 것이다.

동적으로 할당된 배열은 프로그램을 쓸 때 크기가 지정되지 않고

프로그램이 실행되는 동안 결정되는 배열이다.

 

배열 변수 및 포인터 변수

5장에서는 배열이 메모리에 어떻게 유지되는지 설명했다.

그때 배열에 대해서 메모리 주소와 관련해서 논의했다.

하지만 메모리 주소는 포인터이다.

배열 변수는 배열의 첫 번째 색인 변수를 가리키는 일종의 포인터 변수이다.

예시)

다음 두 개의 변수 선언이 주어지면 p와 a는 모두 포인터 변수이다:
int a[10];
typedef int* IntPtr;
IntPtr p;
a와 p가 모두 포인터 변수라는 사실은 디스플레이 10.6에 나타나 있다.

a는 type int의 변수(즉, 변수 a[0])를 가리키는 포인터이므로,

a의 값은 다음과 같이 포인터 변수 p에 할당될 수 있다:
p = a;

 

이 할당 후에 p는 a가 가리키는 것과 같은 메모리 위치를 가리킨다.

따라서 p[0], p[1], ... p[9]는 인덱싱된 변수 a[0], a[1], ... a[9]를 나타낸다.

배열에 사용된 사각형 괄호 표기법은 포인터 변수가 메모리의 배열을 가리키는 한 포인터 변수에 적용된다.

이전 할당 후에는 식별자 p를 배열 식별자인 것처럼 취급할 수 있다.

식별자 a를 포인터 변수인 것처럼 취급할 수도 있지만 중요한 예약이 하나 있다
배열 변수에서 포인터 값을 변경한다.

포인터 변수 p2에 값이 있으면 다음이 합법이라고 생각할 수 있지만 그렇지 않다:
a = p2; //ILLEGAL. a에 다른 주소를 할당할 수 없다.

 

이 할당이 작동하지 않는 근본적인 이유는 

배열 변수가 int*의 형식이 아니지만 그 유형이 int*의 const 버전이기 때문이다.

배열 변수는 a와 같이 const라는 수식어를 가진 포인터 변수이므로 값을 변경할 수 없다.

 

디스플레이 10.6 배열과 포인터 변수

//Program to demonstrate that an array variable is a kind of pointer
//variable.
#include <iostream>
using namespace std;
typedef int* IntPtr;
int main( )
{
	IntPtr p;
	int a[10];
	int index;
    
	for (index = 0; index < 10; index++)
		a[index] = index;
        
	p = a; //Note that changes to the array p are also changes to the array a.
    
	for (index = 0; index < 10; index++)
		cout << p[index] << " ";
	cout << endl;
    
	for (index = 0; index < 10; index++)
		p[index] = p[index] + 1;
        
	for (index = 0; index < 10; index++)
		cout << a[index] << " ";
	cout << endl;
    
	return 0;
}

 

샘플 대화 상자

0 1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9 10

 

동적 배열 생성 및 사용

5장에서 배운 배열 종류의 문제점은 프로그램을 작성할 때 배열의 크기를 지정해야 하지만 

프로그램을 실행하기 전에는 배열의 크기를 알 수 없다는 것이다.

예시)

배열에는 학생 식별 번호 목록이 있을 수 있지만

프로그램을 실행할 때마다 클래스의 크기가 다를 수 있다.

 

지금까지 사용한 배열 종류를 사용하면

배열에 필요한 최대 크기를 추정해야 하며 그 크기가 충분히 클 것으로 기대한다.

이와 관련된 두 가지 문제점이 있다.

1. 너무 낮게 추정할 수도 있고 모든 상황에서 프로그램이 작동하지 않을 수도 있다.

2. 배열에 사용되지 않는 위치가 많을 수 있으므로 컴퓨터 메모리가 낭비될 수 있다.

 

동적으로 할당된 배열을 사용하면 배열에 동적으로 할당된다.
따라서 이러한 문제를 방지한다.

프로그램에서 학생 식별 번호에 동적으로 할당된 배열을 사용하는 경우,

학생 수를 프로그램에 입력으로 입력하고

동적으로 할당된 배열을 학생 수와 정확히 동일한 크기로 만들 수 있다.

 

동적 배열 만들기
동적으로 할당된 배열은 new 연산자를 사용하여 만들어진다.

동적으로 할당된 배열을 만들고 사용하는 방법은 놀라울 정도로 간단한다.

배열 변수는 포인터 변수이므로 새로운 연산자를 사용하여 배열인 동적으로 할당된 변수를 만들 수 있고,

이러한 동적으로 할당된 배열을 마치 보통의 배열인 것처럼 취급할 수 있다.

예제)

다음은 double형인 열 개의 배열 요소로 동적으로 할당된 배열 변수를 만든다:

typedef double* DoublePtr;
DoublePtr d;
d = new double[10];

동적으로 할당된 다른 종류의 원소 배열을 구하려면 단순히 double을 원하는 종류로 바꾸어라. 

특히 double을 구조형이나 클래스형으로 바꿀 수 있다. 

동적으로 할당된 다른 크기의 배열 변수를 구하려면 단순히 10을 원하는 크기로 바꾸어라.


이 예제에 관해서는 덜 명확한 것들도 많이 있다.

1. 동적으로 할당된 배열의 포인터에 사용하는 포인터 타입은

배열의 단일 요소에 사용하는 포인터 타입과 같다.

예시) double형의 요소들로 이루어진 배열의 포인터 타입은

double형의 단순 변수에 사용하는 포인터 타입과 같다.

배열의 포인터는 실제로 배열의 첫 번째 인덱싱된 변수에 대한 포인터이다.

앞의 예제에서 전체는 10개의 인덱싱된 변수가 있는 배열이 생성되고

포인터 d는 이 10개의 인덱싱된 변수 중 첫 번째 변수를 가리킨다.
2. new를 호출할 때 동적으로 할당된 배열의 크기는 유형 뒤에 대괄호로 표시되는데, 이 예에서는 double형이다.

이것은 컴퓨터가 동적 배열을 위해 얼마나 많은 저장소를 예약해야 하는지를 알려준다.

만약 이 예에서 대괄호와 10을 생략한다면,

컴퓨터는 double형의 색인이 있는 열 개의 변수 배열 대신에

double형의 한 변수에만 충분한 저장소를 할당할 것이다.


디스플레이 10.7은 동적으로 할당된 배열의 사용을 보여주는 프로그램을 포함한다.

프로그램은 동적으로 할당된 배열에 저장된 숫자의 목록을 검색한다.
배열의 크기는 프로그램을 실행할 때 결정된다.

사용자에게 숫자가 몇 개가 될 것인지를 묻고

새로운 연산자는 그 크기의 배열을 동적으로 할당한다.

동적 배열의 크기는 arraySize 변수로 제공된다.
동적 배열의 크기는 상수로 주어질 필요가 없다.

그것은 디스플레이 10.7과 같이 프로그램을 실행할 때 값이 결정되는 변수로 주어질 수 있다.

 

디스플레이 10.7 동적으로 할당된 배열

//Searches a list of numbers entered at the keyboard.
#include <iostream>
using namespace std;

typedef int* IntPtr;

void fillArray( int a[], int size); //Ordinary array parameters
//Precondition: size is the size of the array a.
//Postcondition: a[0] through a[size-1] have been
//filled with values read from the keyboard.

int search( int a[], int size, int target); //Ordinary array parameters
//Precondition: size is the size of the array a.
//The array elements a[0] through a[size-1] have values.
//If target is in the array, returns the first index of target.
//If target is not in the array, returns -1.

int main( )
{
	cout << "This program searches a list of numbers.\n";
    
	int arraySize;
	cout << "How many numbers will be on the list? ";
	cin >> arraySize;    
	IntPtr a;
	a = new int[arraySize];
    //The dynamic array a is used like an ordinary array.
	fillArray(a, arraySize);
    
	int target;
	cout << "Enter a value to search for: ";
	cin >> target;    
	int location = search(a, arraySize, target);
	if (location == -1)
		cout << target << " is not in the array.\n";
	else
		cout << target << " is element " << location << " in the
		array.\n";

	delete [] a;

	return 0;
}
//Uses the library <iostream>:
void fillArray( int a[], int size)
{
	cout << "Enter " << size << " integers.\n";
	for ( int index = 0; index < size; index++)
		cin >> a[index];
}

int search( int a[ ], int size, int target)
{
	int index = 0;
	while ((a[index] != target) && (index < size))
		index++;
	if (index == size) //if target is not in a.
		index = -1;
	return index;
}

 

샘플 대화 상자

This program searches a list of numbers.
How many numbers will be on the list? 5
Enter 5 integers.
1 2 3 4 5
Enter a value to search for: 3
3 is element 2 in the array.

 

delete[]

디스플레이 10.7의 a가 가리키는 동적으로 할당된 배열을 파괴하는 delete문을 주목하라.

어쨌든 프로그램이 끝나려 하기 때문에 이 delete문은 필요하지 않았지만

프로그램이 다른 일을 계속한다면

당신은 이러한 delete문을 원해서

이 동적으로 할당된 배열이 사용하는 메모리를 프리스토어 관리자에게 반환할 것이다.

동적으로 할당된 배열의 삭제문은 앞에서 본 delete문과 비슷하지만

동적으로 할당된 배열에서는 다음과 같은 빈 대괄호 쌍을 포함해야 한다:
delete [] a;


대괄호는 C++에게 동적으로 할당된 배열 변수가 제거되고 있음을 알려주기 때문에, 

시스템은 배열의 크기를 확인하고 그만큼 많은 색인 변수를 제거한다.

대괄호를 생략하면 배열 전체가 제거되는 것은 아니다.
예를들면,
delete a;

이것은 합법적이지는 않지만, 대부분의 컴파일러들은 이것을 감지하지 못한다.

C++ 표준은 이것을 할 때 발생하는 일이 "정의되지 않았다"고 말한다.

즉, 컴파일러 작성자는 (사용자가 아닌 컴파일러 작성자에게) 편리한 모든 작업을 수행하도록 할 수 있다.

 

다음과 같은 작업이 할당된 메모리를 삭제할 때는 항상

delete [] arrayPtr;

구문을 사용하라. 
arrayPtr = new MyType[37];
또한 delete문에서 대괄호의 위치를 기록한다 
delete [] arrayPtr; //Correct
delete arrayPtr[]; //ILLEGAL!
디스플레이 10.7의 포인터 a와 같은 포인터를 사용하여

new에 대한 호출과 함께 동적으로 할당된 배열을 만든다.

new에 대한 호출 후에는 이 포인터 변수에 다른 포인터 값을 할당하면 안 된다.

왜냐하면 동적 배열의 메모리가 삭제할 호출과 함께 프리스토어 관리자에게 반환될 때

시스템을 혼란스럽게 할 수 있기 때문이다.
동적으로 할당된 배열은 새로운 변수와 포인터 변수를 사용하여 생성된다.

동적으로 할당된 배열을 사용하여 프로그램이 끝나면

삭제할 호출과 함께 배열 메모리를 프리스토어 관리자에게 반환해야 한다.

그 외에는 다른 배열과 마찬가지로 동적으로 할당된 배열을 사용할 수 있다.

 

예: 배열을 반환하는 함수

C++에서 배열 유형은 함수의 반환 유형으로 허용되지 않다. 예를 들어, 다음은 불법이다:
iint [] someFunction( ); //ILLEGAL
이와 유사한 함수를 만들려면 배열 베이스 유형으로 포인터를 반환하고 배열의 포인터 지점을 가져야 한다.

따라서 함수 선언은 다음과 같다:
int* someFunction( ); //Legal
배열에 포인터를 반환하는 함수의 예는 디스플레이 10.8에 나와 있다.

 

디스플레이 10.8 배열로 포인터 반환하기

#include <iostream>
using namespace std;

int* doubler( int a[], int size);
//Precondition; size is the size of the array a.
//All indexed variables of a have values.
//Returns: a pointer to an array of the same size as a in which
//each indexed variable is double the corresponding element in a.

int main()
{
	int a[] = {1, 2, 3, 4, 5};
	int* b;
    
	b = doubler(a, 5);
    
	int i;
	cout << "Array a:\n";
	for (i = 0; i < 5; i++)
		cout << a[i] << " ";
	cout << endl;
	cout << "Array b:\n";
	for (i = 0; i < 5; i++)
		cout << b[i] << " ";
	cout << endl;
    
	//This call to delete is not really
	//needed since the program is ending,
	//but in another context it could be
	//important to include this delete.
	delete[] b;
	return 0;
}

int* doubler( int a[], int size)
{
	int* temp = new int[size];
    
	for ( int i =0; i < size; i++)
		temp[i] = 2*a[i];
        
	return temp;
}

프로그램이 종료되기 때문에 이 삭제 호출은 실제로 필요하지 않지만 다른 상황에서는 이 삭제를 포함하는 것이 중요할 수 있다.

 

샘플 대화 상자

Array a:
1 2 3 4 5
Array b:
2 4 6 8 10

 

포인터 산술

포인터에 대해 일종의 산술을 수행할 수 있지만, 

주소에 대한 산술이지 숫자에 대한 산술은 아니다.

예를 들어 프로그램에 다음 코드가 포함되어 있다고 가정해 보겠다:
typedef double* DoublePtr;
DoublePtr d;
d = new double[10];

 

숫자 아닌 주소
이 문장 뒤에 d는 색인화된 변수 d[0]의 주소를 포함한다.

식 d + 1은 d[1]의 주소로 평가하고, d + 2는 d[2]의 주소로 평가한다.

주의할 점은 d의 값이 주소이고 주소가 숫자이긴 하지만,

d + 1은 단순히 d의 숫자에 1을 더하는 것이 아니라는 것이다.

만약 유형 더블의 변수에 8바이트가 필요하고 d에 주소 2000이 포함되어 있다면,

d + 1은 메모리 주소 2008로 평가한다.

물론 double형은 다른 유형으로 대체할 수 있으며,

그 유형에 대한 변수 단위로 포인터 추가가 이동한다.
이 포인터 산술은 배열을 조작하는 다른 방법을 제공한다.

예시)

arraySize가 d로 가리킨 동적으로 할당된 배열의 크기라면 다음은 동적 배열의 내용을 출력한다:
for ( int i = 0; i < arraySize; i++)
    cout << *(d + i)<< " ";
앞의 내용은 다음과 같다:
for ( int i = 0; i < arraySize; i++)
   cout << d[i] << " ";
당신은 포인터의 곱셈이나 나눗셈을 할 수 없다.

당신이 할 수 있는 일은 포인터에 정수를 더하거나, 포인터에서 정수를 빼거나,

또는 같은 종류의 포인터 두 개를 빼는 것뿐이다.

당신이 두 개의 포인터를 뺄 때, 결과는 두 주소 사이의 인덱스 변수의 개수이다.

두 개의 포인터 값을 뺄 때, 이 값들은 반드시 같은 배열을 가리켜야 한다

한 배열을 가리키는 포인터를 다른 배열로 가리키는 다른 포인터에서 빼는 것은 거의 의미가 없다.

 

++와 --
증가와 감소 연산자인 ++와 --를 사용하여 포인터 연산을 수행할 수도 있다.

예를 들어, d++는 d의 값을 진행하여 다음 인덱싱된 변수의 주소를 포함하고,

d--는 이전 인덱싱된 변수의 주소를 포함하도록 변경된다.

 

다차원 동적 배열

다차원의 동적 배열(multidimensional array): 배열의 배열 또는 배열의 배열의 배열 등등

예시)

2차원의 동적 배열 = 배열의 배열

정수의 2차원 배열을 만들려면

먼저 1차원의 int 배열에 대한 유형인 int* 유형의 포인터의 1차원 동적 배열을 만든다.

그런 다음 배열의 각 요소에 대해 int의 동적 배열을 작성한다.
유형 정의는 배열을 똑바로 유지하는 데 도움이 될 수 있다.

다음은 일반적인 1차원 동적 int 배열의 변수 유형이다:
typedef int* IntArrayPtr;
3x4개의 int 배열을 얻으려면 기본 유형이 IntArrayPtr인 배열을 사용해야 한다. 예를 들어,
IntArrayPtr *m = new IntArrayPtr[3];
이것은 3개의 포인터의 배열이며, 각각의 포인터는 동적인 int 배열의 이름을 다음과 같이 지정할 수 있다:
for ( int i = 0; i < 3; i++)
    m[i] = new int[4];
결과로 나온 배열 m은 3x4 동적 배열이다.

이를 설명하기 위한 간단한 프로그램이 디스플레이 10.9에 나와 있다.

 

Dynamic Array 사용 방법
■ 포인터 유형 정의 : 배열의 요소와 같은 유형의 변수에 대한 포인터 유형을 정의한다.

예를 들어 동적 배열이 이중 배열인 경우 다음을 사용할 수 있다:
typeef double* DoubleArrayPtr;
■ 포인터 변수 선언: 이 정의된 유형의 포인터 변수를 선언한다.

포인터 변수는 메모리에서 동적으로 할당된 배열을 가리키며 동적 배열의 이름으로 사용된다.
DoubleArrayPtr a;
(또는 정의된 포인터 유형 없이 double *a;를 사용한다.).
■ 새로 부르기: 새 연산자를 사용하여 동적 배열을 만든다:
a = new double[arraySize];
동적 배열의 크기는 앞의 예에서와 같이 대괄호로 표시된다.

크기는 int 변수 또는 다른 int 식을 사용하여 표시할 수 있다.

앞의 예에서 arraySize는 프로그램이 실행되는 동안 값이 결정되는 int 유형의 변수가 될 수 있다.
■ 일반 배열처럼 사용: a와 같은 포인터 변수는 일반 배열처럼 사용된다.

예를 들어, 색인화된 변수는 일반적인 방식으로 작성된다: a[0], a[1] 등.

포인터 변수에는 다른 포인터 값이 할당되지 않아야 하지만 배열 변수처럼 사용되어야 한다.
■ 동적으로 할당된 배열 변수로 프로그램이 완료되면 

포인터 변수와 함께 삭제 및 빈 대괄호를 사용하여 동적 배열을 제거하고 

해당 배열이 차지하는 저장소를 무료 저장소 관리자에게 반환하여 재사용한다. 예를 들어,
delete [] a;

 

delete[]

디스플레이 10.9에서 삭제를 사용하는 것을 주의하라.

동적 배열 m은 배열의 배열이므로

13번과 14번 행의 for 루프에서 new로 생성된 배열 각각은

[]를 삭제하는 호출과 함께 프리스토어 관리자에게 반환되어야 하며,

그러면 배열 m 자체는 []를 삭제하는 다른 호출과 함께 프리스토어 관리자에게 반환되어야 한다.

배열을 생성한 new로 호출할 때마다 []를 삭제하는 호출이 있어야 한다.

([]를 삭제하는 호출 직후에 프로그램이 종료되므로

[]를 삭제하는 호출을 안전하게 생략할 수 있지만 사용 방법을 설명하고 싶다.)

 

 

디스플레이 10.9 이차원 동적 배열

#include <iostream>
using namespace std;

typedef int* IntArrayPtr;

int main( )
{
	int d1, d2;
	cout << "Enter the row and column dimensions of the array:\n"
	cin >> d1 >> d2;
    
	IntArrayPtr *m = new IntArrayPtr[d1];
	int i, j;
	for (i = 0; i < d1; i++)
	m[i] = new int[d2];
	//m is now a d1-by-d2 array.
    
	cout << "Enter " << d1 << " rows of "
	     << d2 << " integers each:\n";
	for (i = 0; i < d1; i++)
		for (j = 0; j < d2; j++)
			cin >> m[i][j];
            
	cout << "Echoing the two-dimensional array:\n";    
	for (i = 0; i < d1; i++)
	{
		for (j = 0; j < d2; j++)
			cout << m[i][j] << " ";
		cout << endl;
	}

	for (i = 0; i < d1; i++)
		delete[] m[i];
	delete[] m;
    
	return 0;
}

배열을 만든 new 호출에 대해 delete[]할 호출이 하나씩 있어야 한다.
(delete[]하기 위한 호출은 프로그램이 종료되기 때문에 실제로 필요한 것은 아니지만,

다른 맥락에서 이러한 호출을 포함시키는 것이 중요할 수 있다.)

 

샘플 대화 상자

Enter the row and column dimensions of the array:
3 4
Enter 3 rows of 4 integers each:
1 2 3 4
5 6 7 8
9 0 1 2
Echoing the two-dimensional array:
1 2 3 4
5 6 7 8
9 0 1 2

 

10.3 클래스, 포인터 및 동적 배열

-> 연산자

C++에는 구조체나 클래스의 멤버를 지정하는 표기법을 간단히 하기 위해 

포인터와 함께 사용할 수 있는 연산자가 있다.

-> 연산자는 화살표 연산자(arrow operator)로

역참조 연산자 *와 점 연산자의 동작을 결합하여 주어진 포인터이다.

가리키는 동적 구조체나 클래스 객체의 멤버를 지정한다.

예시) 
struct Record
{
    int number;
    char grade;
};
다음은 Record 유형의 동적으로 할당된 변수를 만들고

동적 구조 변수의 멤버 변수를 2001 및 'A'로 설정한다.
Record *p;
p = new Record;
p->number = 2001;
p->grade = 'A';

 

표기법 p->grade 와 (*p).grade가 같은 의미를 갖는다.

그러나 첫번째가 더 편리하고 거의 항상 사용되는 표기법이다.

 

this 포인터

클래스에 대한 멤버 함수를 정의할 때 호출 객체를 참조하기를 원할 때가 있다.

this 포인터: 호출 객체를 가리키는 미리 정의된 포인터

예를 들어, 다음과 같은 클래스를 생각해 보자:
class Sample
{
public:
    ...
    void showStuff( ) const;
    ...
private:
    int stuff;
    ...
};
멤버 함수를 정의하는 다음 두 가지 방법은 showStuff와 같다:
void Sample::showStuff( ) const
{
    cout << stuff;
}
//좋은 스타일은 아니지만 이 포인터는 다음과 같다:
void Sample::showStuff( )
{
    cout << this->stuff;
}
this는 호출 객체의 이름이 아니라 호출 객체를 가리키는 포인터의 이름임을 유의하라.

this 포인터는 값을 변경할 수 없으며 항상 호출 객체를 가리킨다.
앞에서 언급했듯이, 당신은 보통 이것을 포인터로 쓸 필요가 없다.

그러나 몇몇 상황에서는 편리하다.

this 포인터가 일반적으로 사용되는 한 곳은 할당 연산자인 =를 오버로딩시키는 것인데, 이것은 다음에 논의한다.

this 포인터가 호출 객체를 가리키기 때문에, this를 정적 멤버 함수의 정의에 사용할 수 없다.

정적 멤버 함수는 보통 this가 가리키는 포인터를 가리키는 호출 객체가 없다.

 

할당 연산자 오버로딩하기

이 책에서 우리는 보통 할당 연산자를 void 함수인 것처럼 사용한다.

그러나 미리 정의된 할당 연산자는 일부 특수한 용도를 허용하는 참조를 반환한다.


미리 정의된 할당 연산자를 사용하면 다음과 같이 할당 연산자를 연결할 수 있다:

a = b = c;

a = (b = c)

를 의미한다.

첫 번째 연산인 b = c는 새로운 버전의 b를 반환한다.

따라서 a = b = c;

의 동작은 a 뿐만 아니라 b도 c와 동일하게 설정하는 것이다.

할당 연산자의 오버로드된 버전을

이러한 방식으로 사용할 수 있도록 하려면

할당 연산자를 정의해야 한다.

곧 보게 되겠지만 이 포인터를 사용하면 이러한 작업을 수행할 수 있다.

그러나 할당 연산자가 왼쪽에 있는 유형의 것을 반환해야 하지만 참조를 반환할 필요는 없다.

할당 연산자를 사용하면 참조가 반환되는 이유를 설명할 수 있다.
미리 정의된 할당 연산자가 참조를 반환하는 이유는

다음과 같이 반환된 값으로 멤버 함수를 호출할 수 있기 때문이다
(a = b).f( );
여기서 f는 멤버 함수이다.

할당 연산자의 오버로딩 버전이 멤버 함수를 이런 식으로 호출할 수 있도록 하려면

참조를 반환하도록 해야 한다.

참조를 반환해야 하는 강력한 이유는 아니다.

그러나 참조를 반환하는 것은 전통적인 방법이며

단순히 값을 반환하는 것보다 참조를 반환하는 것이 훨씬 어렵지 않다.

 

예를 들어 다음 클래스(사전 정의된 클래스 문자열로 쉽게 처리되지 않는 일부 특수 문자열 처리에 사용될 수 있음)를 고려한다:
class StringClass
{
public:
    ...
    void someProcessing( );
    ...
    StringClass& operator=( const StringClass& rtSide);
    ...
private:
    char *a; //Dynamic array for characters in the string
    int capacity; //size of dynamic array a
    int length; //Number of characters in a
};

= 는 멤버여야 한다

할당 연산자를 오버로딩시키면 클래스의 멤버여야 하며 클래스의 친구가 될 수 없다.

그래서 이전 정의에 연산자에 대한 매개 변수가 하나만 있는 것이다.

예를 들어 다음을 생각해 보라:

s1 = s2; //s1 and s2 in the class StringClass

 

=에게 객체 호출
이전 호출에서 s1은 호출 객체이고 s2는 멤버 연산자 =에게 인수이다.
오버로드된 할당 연산자의 다음 정의는 s1 = s2 = s3와 같은 할당 체인에서 사용할 수 있으며 

다음과 같이 멤버 함수를 호출하는 데 사용할 수 있다:
(s1 = s2).someProcessing( );


오버로딩된 할당 연산자의 정의에서는

this 포인터를 사용하여 = 부호(호출 개체)의 왼쪽에 있는 개체를 반환한다:

//This version does not work in all cases.
StringClass& StringClass::operator=( const StringClass& rtSide)
{
    capacity = rtSide.capacity;
    length = rtSide.length;
    delete [] a;
    a = new char[capacity];
    for ( int i = 0; i < length; i++)
        a[i] = rtSide.a[i];
    return * this;
}

 

이 버전은 다음과 같이 할당 연산자의 양쪽에 동일한 객체가 있는 할당에서 사용할 때 문제가 있다:
s = s;
이 할당이 실행되면 다음 문이 실행된다:
delete [] a;
그러나 호출 객체는 s이므로 delete [] s.a;를 의미한다;
그러면 포인터 s.a가 정의되지 않는다.

할당 연산자가 객체를 손상시켰으며 이 프로그램 실행은 아마도 실패했을 것이다.
많은 클래스에서 동일한 객체가 할당 연산자의 양쪽에 있을 때

할당 연산자를 오버로딩시키는 명확한 정의가 올바르게 작동하지 않는다.

항상 이 경우를 확인하고 오버로딩된 할당 연산자에 대한 정의를 작성하도록 주의해야 한다.
오버로딩된 할당 연산자에 대한 첫 번째 정의에서 발생한 문제를 피하기 위해 

this 포인터를 사용하여 다음과 같이 이 특수한 경우를 테스트할 수 있다:

//Final version with bug fixed:
StringClass& StringClass::operator=(const StringClass& rtSide)
{
    if ( this == &rtSide)
    //if the right side is the same as the left side
    {
        return * this;
    }
    else
    {
        capacity = rtSide.capacity;
        length = rtSide.length;
        delete [] a;
        a = new char[capacity];

        for ( int i = 0; i < length; i++)
            a[i] = rtSide.a[i];
        return * this;
    }
}

오버로딩된 할당 연산자가 있는 완전한 예는 다음 프로그래밍 예제에 나와 있다.

 

디스플레이 10.10 동적 배열 멤버를 가진 클래스 정의

//Objects of this class are partially filled arrays of doubles.
class PFArrayD
{
public:
	PFArrayD( );
	//Initializes with a capacity of 50.
    
	PFArrayD( int capacityValue);
    
	PFArrayD( const PFArrayD& pfaObject); //Copy constructor
    
	void addElement( double element);
	//Precondition: The array is not full.
	//Postcondition: The element has been added.
    
	bool full( ) const { return (capacity == used); }
	//Returns true if the array is full, false otherwise.
    
	int getCapacity( ) const { return capacity; }
    
	int getNumberUsed( ) const { return used; }
    
	void emptyArray( ){ used = 0; }
	//Empties the array.
    
	double& operator[]( int index);
	//Read and change access to elements 0 through numberUsed - 1.
    
	PFArrayD& operator =( const PFArrayD& rightSide); //Overloaded assignment
    
	~PFArrayD( ); //Destructor
private:
	double *a; //For an array of doubles
	int capacity; //For the size of the array
	int used; //For the number of array positions currently in use
};

 

디스플레이 10.11 PFARrayD 클래스에 대한 멤버 함수 정의

//These are the definitions for the member functions for the class
//PFArrayD.
//They require the following include and using directives:
//#include <iostream>
//using std::cout;

PFArrayD::PFArrayD( ) :capacity(50), used(0)
{
	a = new double[capacity];
}

PFArrayD::PFArrayD( int size) :capacity(size), used(0)
{
	a = new double[capacity];
}

PFArrayD::PFArrayD( const PFArrayD& pfaObject)
 :capacity(pfaObject.getCapacity( )), used(pfaObject.getNumberUsed( ))
{
	a = new double[capacity];
	for ( int i = 0; i < used; i++)
		a[i] = pfaObject.a[i];
}

void PFArrayD::addElement( double element)
{
	if (used >= capacity)
	{
		cout << "Attempt to exceed capacity in PFArrayD.\n";
		exit(0);
	}
	a[used] = element;
	used++;
}

double& PFArrayD::operator[]( int index)
{
	if (index >= used)
	{
		cout << "Illegal index in PFArrayD.\n";
		exit(0);
	}
	return a[index];
}

PFArrayD& PFArrayD::operator =( const PFArrayD& rightSide)
{
	//Note that this also checks for the case of having the same
	//object on both sides of the assignment operator.
	if (capacity != rightSide.capacity)
	{
		delete [] a;
		a = new double[rightSide.capacity];
	}
    
	capacity = rightSide.capacity;
	used = rightSide.used;
	for ( int i = 0; i < used; i++)
		a[i] = rightSide.a[i];
        
	return * this;
}

PFArrayD::~PFArrayD( )
{
	delete [] a;
}

if(capacity != rightSide.capacity)

이것은 할당 연산자의 양쪽에 동일한 개체가 있는 경우도 확인한다.

 

디스플레이 10.12 PFArrayD 시연 프로그램

//Program to demonstrate the class PFArrayD
#include <iostream>
using namespace std;

class PFArrayD
{
	<The rest of the class definition is the same as in Display 10.10.>
};

void testPFArrayD( );
//Conducts one test of the class PFArrayD.

int main( )
{
	cout << "This program tests the class PFArrayD.\n";    
	char ans;    
	do
	{
		testPFArrayD( );
		cout << "Test again? (y/n) ";
		cin >> ans;
	} while ((ans == 'y') || (ans == 'Y'));
    
	return 0;
}

<The definitions of the member functions for the class PFArrayD go here.>
void testPFArrayD( )
{
	int cap;
	cout << "Enter capacity of this super array: ";
	cin >> cap;
	PFArrayD temp(cap);
    
	cout << "Enter up to " << cap << " nonnegative numbers.\n";
	cout << "Place a negative number at the end.\n";
    
	double next;
	cin >> next;
	while ((next >= 0) && (!temp.full( )))
	{
		temp.addElement(next);
		cin >> next;
	}
    
	cout << "You entered the following "
	     << temp.getNumberUsed( ) << " numbers:\n";
	int index;
	int count = temp.getNumberUsed( );    
	for (index = 0; index < count; index++)
		cout << temp[index] << " ";
	cout << endl;
	cout << "(plus a sentinel value.)\n";
}

11장의 섹션 11.1에서는 

이 긴 파일을 대략 디스플레이 10.10, 10.11에 해당하는 세 개의 짧은 파일로 분할하는 방법을 보여주며 

디스플레이 10.10 및 10.11의 코드 없이 이 파일을 표시한다.

 

샘플 대화 상자

This program tests the class PFArrayD.
Enter capacity of this super array: 10
Enter up to 10 nonnegative numbers.
Place a negative number at the end.
1.1
2.2
3.3
4.4
-1

You entered the following 4 numbers:
1.1 2.2 3.3 4.4
(plus a sentinel value.)
Test again? (y/n) n

 

 

예: 부분적으로 채워진 배열을 위한 클래스

디스플레이 10.10과 10.11의 클래스 PFArrayD는 부분적으로 채워진 이중 배열에 대한 클래스이다.

디스플레이 10.12의 시연 프로그램과 같이

클래스 PFArrayD의 객체는 일반적인 배열과 마찬가지로 대괄호를 사용하여 접근할 수 있지만,

그 객체는 또한 배열이 얼마나 사용되고 있는지를 자동으로 추적한다.
따라서 부분적으로 채워진 배열과 같은 기능을 한다.

멤버 함수 getNumberUseed는 사용된 배열 위치의 수를 반환하므로

다음 샘플 코드와 같이 for문에서 사용할 수 있다:
PFArrayD stuff(cap); //cap is an int variable.
<some code to fill object stuff with elements.>
for ( int index = 0; index < stuff.getNumberUsed( ); index++)
    cout << stuff[index] << " ";


클래스 PFArrayD의 객체는 동적 배열을 멤버 변수로 갖는다.

이 멤버 변수 배열은 요소들을 저장한다.

동적 배열 멤버 변수는 실제로 포인터 변수이다.

각 생성자에서 이 멤버 변수는 동적 배열을 가리키도록 설정된다.

또한 int형의 멤버 변수는 두 가지가 있다.

멤버 변수 용량은 동적 배열의 크기를 기록하고

사용된 멤버 변수는 지금까지 채워진 배열 위치의 수를 기록한다.

부분적으로 채워진 배열의 관례와 같이 요소들을 순서대로 채워야 한다.


클래스 PFArrayD의 개체는 부분적으로 채워진 double형의 배열로 사용될 수 있다.

일반적인 double형의 배열이나 동적인 double형의 배열에 비해 몇 가지 장점이 있다.

1. 표준 배열과 달리 이 배열은 잘못된 배열 인덱스를 사용할 경우 오류 메시지를 제공한다.

2. 또한 클래스 PFArrayD의 개체는 배열의 사용 정도를 추적하기 위해 추가 int 변수를 필요로 하지 않다.

클래스 PFArrayD의 객체는 type double의 값을 저장하기 위해서만 작동한다.

16장에서 템플릿을 논의할 때,

어떤 타입에서도 작동하는 템플릿 클래스로 정의를 변환하는 것이 쉽다는 것을 알게 되겠지만,

일단 double형의 요소를 저장하는 것으로 만족하겠다.
3. 클래스 PFArrayD의 정의에서 대부분의 세부 사항은 이전에 다룬 항목만 사용하지만, 

복사 생성자, 파괴자, 할당 연산자의 오버로딩의 세 가지 항목이 새로 추가된다.

다음으로 오버로딩된 할당 연산자를 설명하고

다음 두 하위 섹션에서 복사 생성자와 소멸자에 대해 논의한다.

 

할당 연산자를 오버로딩하려는 이유를 확인하려면

할당 연산자의 오버로딩이 디스플레이 10.10 및 10.11에서 누락되었다고 가정한다.
그런 다음 list1과 list2가 다음과 같이 선언된다고 가정한다:
PFArrayD list1(10), list2(20);


list2에 list2.addElement의 호출과 함께 숫자 목록이 주어진 경우 

할당 연산자의 오버로딩이 없다고 가정하더라도

다음 할당 문은 여전히 정의되어 있지만 그 의미가 원하는 것이 아닐 수 있다:
list1 = list2;


할당 연산자의 오버로드가 없으면 기본적으로 미리 정의된 할당 연산자가 사용된다.

이 미리 정의된 버전의 할당 연산자는 평소와 같이

list2의 각 멤버 변수의 값을 list1의 해당 멤버 변수에 복사한다.

따라서 list1.a의 값은 list2.a와 같고,

list1.capacity의 값은 list2.capacity와 같으며,

list1.used의 값은 list2.used와 같도록 변경된다.

 

그러나 문제가 발생할 수 있다.
멤버 변수 list1.a는 포인터를 포함하고, 

할당문은 이 포인터를 list2.a와 같은 값으로 설정한다.

따라서 list1.a와 list2.a 둘 다 메모리에서 같은 위치를 가리킨다.

따라서 array list1.a를 변경하면 array list2.a도 변경된다.

마찬가지로 array list2.a를 변경하면 array list1.a도 변경된다.

이것은 우리가 보통 원하는 것이 아니다.

우리는 보통 할당 연산자가 오른쪽에 있는 것을 완전히 독립적으로 복사하기를 원한다.

 

이것을 해결하는 방법은 할당 연산자를 오버로딩시켜서

클래스 PFArrayD의 객체에 대해 우리가 원하는 것을 수행하는 것이다.

이것은 우리가 디스플레이 10.10과 10.11에서 수행한 것이다.

디스플레이 10.11의 오버로딩 할당 연산자의 정의를 다음에 재현한다:

PFArrayD& PFArrayD::operator =( const PFArrayD& rightSide)
{
    if (capacity != rightSide.capacity)
    {
        delete [] a;
        a = new double[rightSide.capacity];
    }
    capacity = rightSide.capacity;
    used = rightSide.used;
    for ( int i = 0; i < used; i++)
        a[i] = rightSide.a[i];
    return * this;
}

 

할당 연산자를 오버로딩할 때 클래스의 멤버여야 한다.

클래스의 friend가 될 수 없다.

그래서 이전 정의에 매개 변수가 하나만 있는 것이다.

예를 들어 다음을 생각해 보라:
list1 = list2;
이전 호출에서 list1은 호출 객체이고 

list2는 멤버 연산자 =에게 인수이다.


두 객체의 공간(capacity)이 같은지 확인하기 위해

두 객체의 공간이 같은지 확인한다.

만약 공간이 같지 않다면

왼쪽 부분의 배열 변수 a가 delete를 통해 삭제되고,

적당한 용량의 새 배열이 새 배열로 생성된다.

그러면 할당 연산자의 왼쪽 부분에 있는 객체가 정확한 크기의 배열을 가질 수 있지만,

매우 중요한 다른 일을 수행한다.

할당 연산자의 양쪽에서 같은 객체가 발생하면,

멤버 변수 a에 의해 명명된 배열이 delete 호출과 함께 삭제되지 않도록 보장한다.

이것이 왜 중요한지 알아보기 위해,

오버로딩 할당 연산자에 대한 다음의 대안적이고 간단한 정의를 생각해 보자:

//This version has a bug:
PFArrayD& PFArrayD::operator =( const PFArrayD& rightSide)
{
    delete [] a;
    a = new double[rightSide.capacity];
    capacity = rightSide.capacity;
    used = rightSide.used;
    for ( int i = 0; i < used; i++)
        a[i] = rightSide.a[i];
    return * this;
}

이 버전은 다음과 같이 할당 연산자의 양쪽에 동일한 개체가 있는 할당에서 사용할 때 문제가 있다:
my list = my list;
이 할당이 실행되면 첫 번째로 실행되는 구문은
delete [] a;
하지만 호출 개체는 myList이므로, 이것은
delete [] myList.a;
그러면 포인터 myList.a가 정의되지 않는다.

할당 연산자가 개체 myList를 손상시켰다.

이 문제는 디스플레이 10.11에서 지정한 오버로딩 할당 연산자의 정의에서는 발생할 수 없다.

 

얕은 복사 및 깊은 복사
얕은 복사(shallow copy)

오버로드된 할당 연산자 또는 복사본 생성자를 정의할 때 

코드가 한 개체에서 다른 개체로 멤버 변수의 내용을 간단히 복사한다.

기본 할당 연산자와 기본 복사본 생성자는 얕은 복사본을 수행한다.

관련된 포인터나 동적으로 할당된 데이터가 없으면 이 작업은 정상적으로 수행된다.

깊은 복사(deep copy)

일부 멤버 변수가 동적 배열 이름을 지정하거나 다른 동적 구조를 가리킨다면

각 멤버 변수가 가리키는 내용의 복사본을 만들어서

개별적이지만 동일한 복사본을 얻도록 한다.

할당 연산자를 오버로드하거나 복사본 생성자를 정의할 때 일반적으로 수행하는 작업이다.

 

소멸자

동적으로 할당된 변수들은

프로그램이 적절한 delete 호출을 하지 않는 한 소멸되지 않는다는 문제점을 가지고 있다.

동적 변수가 지역 포인터 변수를 사용하여 만들어졌고

함수 호출의 마지막에 지역 포인터 변수가 소멸되더라도

delete 호출이 없는 한 동적 변수는 유지된다.

delete 호출로 동적 변수를 제거하지 않으면

동적 변수는 계속 메모리 공간을 차지하게 되어

프리스토어 관리자의 메모리를 모두 사용하여

프로그램이 중단될 수 있다.

또한 동적 변수가 클래스의 구현 세부 정보에 포함되어 있으면

클래스를 사용하는 프로그래머가 동적 변수에 대해 알지 못해

delete 호출을 수행할 것으로 예상할 수 없다.

실제로 데이터 멤버는 일반적으로 private 멤버이므로

프로그래머가 필요한 포인터 변수에 접근할 수 없으므로

이러한 포인터 변수로 삭제 호출을 할 수 없다.

이 문제를 처리하기 위해 C++는 소멸자(destructor)라는 특수한 종류의 멤버 함수를 가지고 있다.

 

소멸자
소멸자(destructor): 클래스의 객체가 범위를 벗어날 때 자동으로 호출되는 멤버 함수

만약 당신의 프로그램이 소멸자가 있는 클래스의 객체의 이름을 짓는 지역 변수를 포함하고 있다면,

함수 호출이 끝나면, 소멸자는 자동으로 호출될 것이다.

소멸자가 올바르게 정의되면,

소멸자는 객체에 의해 생성된 모든 동적으로 할당된 변수를 제거하기 위해 delete를 호출할 것이다.
이것은 삭제하기 위해 한 번의 호출로 수행될 수도 있고

삭제하기 위해 여러 번의 호출이 필요할 수도 있다.
소멸자가 다른 정리 세부 정보도 수행하기를 원할 수 있지만

재사용을 위해 freestore 관리자에게 메모리를 반환하는 것이 소멸자의 주요 작업이다.

 

소멸자 이름
구성원 함수 ~PFArrayD는 그림 10.10에 표시된 클래스 PFArrayD의 소멸자이다.

생성자처럼, 소멸자는 항상 자신이 구성원으로 있는 클래스와 같은 이름을 갖지만,

소멸자는 이름의 첫머리에 tilde 기호인 ~를 갖는다.

생성자처럼, 소멸자는 반환되는 값에 대한 유형이 없으며,

형식 공백도 없다.

소멸자는 매개 변수가 없다.

따라서 클래스는 오직 하나의 소멸자만 가질 수 있고,

클래스에 대해 소멸자를 오버로딩시킬 수 없다.

그렇지 않으면, 소멸자는 다른 구성원 함수와 마찬가지로 정의된다.


Display 10.11에 주어진 destructor ~PFArrayD의 정의에 주목하자.

~PFArrayD는 멤버 포인터 변수 a가 가리키는 동적으로 할당된 배열을 제거하기 위해 delete를 호출한다.

Display 10.12에 나타난 샘플 프로그램의 함수 테스트 PFArrayD를 다시 본다.

로컬 변수 temp는 동적 배열을 포함한다
멤버 변수 temp.a.로 가리킨다.

만약 이 클래스에 소멸자가 없다면,

PFArrayD를 테스트하기 위한 호출이 종료된 후에도,

동적 배열이 프로그램에 쓸모가 없더라도,

이 동적 배열은 여전히 메모리를 점유하고 있을 것이다.

 

소멸자
클래스의 소멸자: 클래스의 개체가 범위를 벗어날 때 자동으로 호출되는 클래스의 멤버 함수

이는 무엇보다도 클래스 유형의 개체가 함수의 로컬 변수인 경우

함수 호출이 종료되기 전 마지막 작업으로 소멸자가 자동으로 호출됨을 의미한다.

소멸자는 개체가 생성한 동적으로 할당된 변수를 제거하는 데 사용되므로

이러한 동적 변수가 차지하는 메모리는 재사용을 위해 freestore 관리자에게 반환된다.

소멸자는 다른 정리 작업도 수행할 수 있다.

소멸자의 이름은 tilde 기호, ~로 구성되어야 하며 그 뒤에 클래스의 이름이 와야 한다.


복사 생성자

복사 생성자(Copy Constructor): 클래스와 동일한 유형의 매개 변수 하나를 가지는 생성자

하나의 매개 변수는 call-by-reference 호출 매개 변수여야 하며,

일반적으로 매개 변수 앞에는 const 매개 변수가 있으므로 상수 매개 변수이다.
다른 모든 점에서 복사 생성자는 다른 생성자와 같은 방식으로 정의되며 다른 생성자처럼 사용할 수 있다.

예를 들어, 디스플레이 10.10에 정의된 클래스 PFArrayD를 사용하는 프로그램은 다음을 포함할 수 있다:
PFArrayD b(20);
for ( int i = 0; i < 20; i++)
    b.addElement(i);
PFArrayD temp(b); //Initialized by the copy constructor
객체 b: int형의 매개 변수를 가진 생성자로 초기화된다.
객체 temp: 생성자 유형 PFArrayD&의 인수를 가진 생성자에 의해 초기화된다.

이와 같이 사용되는 경우 다른 생성자와 마찬가지로 복사 생성자가 사용된다.
복사 생성자는 초기화되는 객체가 그것의 논법의 완전하고 독립적인 복사가 되도록 정의되어야 한다.

그래서 선언문에서
PFArrayD temp(b);
멤버 변수 temp.a는 단순히 b.a와 같은 값으로 설정해서는 안 된다.

그러면 같은 동적 배열을 가리키는 두 개의 포인터가 생성된다.

복사 생성자의 정의는 디스플레이 10.11에 나와 있다.

복사 생성자의 정의에서 새로운 동적 배열이 생성되고 하나의 동적 배열의 내용이 다른 동적 배열에 복사된다.

따라서 이전 선언에서는 배열의 멤버 변수가 b의 배열 멤버 변수와 다르도록 temp가 초기화된다.

두 개의 배열 멤버 변수 temp.a와 b.a는 같은 double형을 포함하지만

이들 배열의 멤버 변수 중 하나에 변경이 가해지면 다른 배열 멤버 변수에는 아무런 영향을 주지 않는다.

따라서 temp에 대한 변경은 b에 아무런 영향을 주지 않는다.

당신이 본 것처럼 복사 생성자도 다른 생성자와 마찬가지로 사용할 수 있다.
다른 특정한 상황에서는 복사 생성자를 자동으로 호출하기도 한다.

 

대략적으로 말하면 C++가 객체의 복사본을 만들어야 할 때마다 복사 생성자를 자동으로 호출한다.

특히 복사 생성자를 자동으로 호출하는 경우는 다음 세 가지 상황이다:

1. 클래스 객체가 선언되고 괄호 안에 주어진 동일한 유형의 다른 객체에 의해 초기화되는 경우

(다른 생성자와 마찬가지로 copy constructor를 사용하는 경우)
2. 함수가 클래스 유형의 값을 반환하는 경우.
3. 클래스 유형의 인수가 호출 단위 매개 변수에 대해 

"플러그인"될 때마다 복사 생성자는 "플러그인"이 의미하는 바를 정의한다.
클래스에 대한 복사 생성자를 정의하지 않으면 C++가 자동으로 복사 생성기를 생성한다.

그러나 이 기본 복사 생성자는 단순히 멤버 변수의 내용을 복사할 뿐

멤버 변수에 포인터나 동적 데이터가 있는 클래스에는 올바르게 작동하지 않는다.

따라서 클래스 멤버 변수에 포인터, 동적 배열 또는 기타 동적 데이터가 포함된 경우

클래스에 대한 복사 생성기를 정의해야한다.

 

복사 생성자가 필요한 이유
클래스 PFArrayD에 대한 복사 생성자를 정의하지 않았다면 어떤 일이 일어날지 알아보겠다.

클래스 PFArrayD의 정의에 복사 생성자를 포함하지 않았다고 가정하고,

예시)

함수 정의에서 call-by-value 매개 변수를 사용했다고 가정해 보겠다.
void showPFArrayD(PFArrayD parameter)
{
    cout << "The first value is: "
             << parameter[0] << endl;
}
함수 호출이 포함된 다음 코드를 생각해 보라:
PFArrayD sample(2);
sample.addElement(5.5);
sample.addElement(6.6);
showPFArrayD(sample);
cout << "After call: " << sample[0] << endl;
클래스 PFArrayD에 대해서는 복사 생성자가 정의되어 있지 않기 때문에 

클래스에는 단순히 멤버 변수의 내용을 복사하는 기본 복사 생성자가 있다.

그러면 다음과 같이 진행된다.

함수 호출이 실행되면 샘플의 값이 지역 변수 매개변수로 복사되므로

parameter.a가 sample.a와 동일하게 설정된다.

그러나 이들은 포인터 변수이므로

함수 호출 parameter.a와 sample.a이 동일한 동적 배열을 가리키는 동안,

이들은 포인터 변수이므로 다음과 같다:
           5.5, 6.6
sample.a parameter.a

함수 호출이 종료되면 PFArrayD에 대한 소멸자를 호출하여 

매개 변수가 사용한 메모리를 freestore 관리자로 반환하여 재사용할 수 있도록 한다.

소멸자의 정의는 다음과 같다:
delete [] a;
소멸자가 객체 매개변수와 함께 호출되므로, 이 문장은 다음과 같다
delete [] parameter.a;
그림을 다음과 같이 바꾼다:

Undefined

sample.a parameter.a

sample.a와 parameter.a는 같은 동적 배열을 가리키므로 

parameter.a를 삭제하는 것은 sample.a를 삭제하는 것과 같다.

따라서 sample.a는 프로그램이 다음 구문에 도달하면 정의되지 않다
cout << "After call : " << sample [0 ] << endl;
따라서 이 cout 문은 정의되지 않았다.

cout 문은 우연히 당신이 원하는 출력을 줄 수 있지만,

sample.a 가 정의되지 않았다는 사실은 곧 문제를 발생시킬 것이다.

한 가지 주요 문제는 객체 표본이 어떤 함수의 지역 변수일 때 발생한다.

이 경우 함수 호출이 종료되면 소멸자가 표본과 함께 호출된다.

그 소멸자 호출은 다음과 같다 
delete [] sample.a;
하지만 방금 본 것처럼 sample.a가 가리키는 동적 배열은 이미 한 번 삭제되었고, 

이제 시스템은 그것을 두 번째로 삭제하려고 한다.

동일한 동적 배열(또는 새로 만든 변수)을 삭제하기 위해

두 번 삭제를 호출하면 심각한 시스템 오류가 발생하여 프로그램이 중단될 수 있다.
만약 복사 생성자가 없다면 그렇게 될 것이다.

다행히도 클래스 PFArrayD에 대한 정의에 복사 생성자를 포함시켰기 때문에

다음 함수 호출이 실행되면 복사 생성자가 자동으로 호출된다:
showPFArrayD(sample);
복사 생성자는

parameter라는 이름의 call-by-value 매개 변수에 대한

인수 sample을 연결하는 것의 의미를 정의하므로

그림은 다음과 같다:
5.5, 6.6.   5.5, 6.6
sample.a parameter.a

따라서 parameter.a로 변경된 모든 변경은 인수 sample에 영향을 주지 않으며, 소멸자에 문제가 없다.

만약 소멸자가 parameter에 대해 호출되고 sample에 대해 호출되면,

소멸자에 대한 각 호출은 다른 동적 배열을 삭제한다.
어떤 함수가 클래스 유형의 값을 반환할 때 

복사 생성자를 자동으로 호출하여 반환문에 지정된 값을 복사한다.

복사 생성자가 없으면 call-by-value 매개변수에 대해 설명한 것과 비슷한 문제가 발생한다.
클래스 정의에 포인터와 new 연산자를 사용하여 동적으로 할당된 메모리가 포함된다면 복사 생성자를 포함해야 한다.

포인터나 동적으로 할당된 메모리를 포함하지 않는 클래스는 복사 생성자를 정의할 필요가 없다.
할당 연산자를 사용하여 한 객체를 다른 객체와 동일하게 설정하면 예상과 달리 복사 생성자가 호출되지 않는다.
그러나 기본 할당 연산자가 수행하는 작업이 마음에 들지 않으면 

디스플레이 10.10 및 10.11에서 수행한 것처럼 할당 연산자를 재정의할 수 있다.

 

복사 생성자
복사 생성자: 클래스와 같은 형식의 하나의 call-by-reference 파라미터를 가지는 생성자

하나의 매개변수는 call-by-reference 매개변수여야 한다.

보통 매개변수는 상수 매개변수이기도 한다.

즉, const 매개변수 수식어가 선행한다.

클래스에 대한 복사 생성자는 함수가 클래스 유형의 값을 반환할 때마다 자동으로 호출된다.

또한 클래스 유형의 호출별 매개변수에 대해 인수를 연결할 때마다 복사 생성자가 자동으로 호출된다.

복사 생성자도 다른 생성자와 같은 방식으로 사용할 수 있다.

포인터를 사용하는 클래스와 새 연산자는 복사 생성자를 가져야 한다.

 

빅3
전문가들은 복사 생성자, = 할당 연산자, 그리고 소멸자를 빅3라고 부르는데, 

이것들 중 하나라도 필요하면 세 가지가 모두 필요하다고 말한다.

이 중 하나라도 빠지면 컴파일러가 생성하지만,

생성된 항목이 원하는 대로 동작하지 않을 수 있다.

따라서 직접 정의하는 것이 좋다.

컴파일러가 생성하는 복사 생성자와 오버로딩된 = 할당 연산자는

모든 구성원 변수가 int나 double처럼 미리 정의된 유형이면 잘 작동한다.

포인터를 사용하는 클래스와 new 연산자의 경우에는

자신의 복사 생성자, 오버로딩된 = 및 소멸자를 정의하는 것이 가장 안전하다.