본문 바로가기
Computer Science/C++

[C++] 배열과 포인터(포인터의 개념,포인터의 연산, 메모리의 동적 할당)

by BrickSky 2023. 11. 10.

1) 포인터의 개념


주소값의 이해

데이터의 주소값이란 해당 데이터가 저장된 메모리의 시작 주소를 의미한다.

C++에서는 이러한 주소값을 1바이트 크기의 메모리 공간으로 나누어 이해할 수 있는데, int형 데이터는 4바이트의 크기를 갖지만, int형 데이터의 주소값은 시작 주소 1바이트 만을 갖는 것이다.

포인터란?

포인터란 메모리의 주소값을 저장하는 변수이며, 포인터 변수라고도 부른다.

char형 변수는 문자를 저장하고, int형 변수는 정수를 저장하는 것처럼 포인터는 주소값을 저장하는 것이다.

int n = 100;   // 변수의 선언
int *ptr = &n; // 포인터의 선언

 

그림에서 알 수 있듯, 포인터 변수는 n의 시작 주소를 가리키는 것을 알 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

포인터 연산자: 주소 연산자(&)

주소 연산자는 변수의 이름 앞에 사용하여 해당 변수의 주소값을 반환한다.

& 기호를 사용한다.

 

포인터 연산자: 참조 연산자(*)

참조 연산자는 포인터의 이름이나 주소 앞에 사용하여, 포인터에 저장된 주소에 저장되어 있는 값을 반환한다.

 

포인터 선언

타입 포인터이름;

타입이란 포인터가 가리키고자 하는 변수의 타입을 명시하는 것이다.

포인터 이름은 포인터가 선언된 후에 포인터에 접근하기 위해 사용되며 포인터를 선언할 때 참조연산자(*)의 앞뒤 공백은 신경쓰지 않아도 된다.

 

포인터의 동시 선언

두개의 int형 포인터를 선언하고 싶은 경우 각각의 포인터 변수 이름 앞에 참조연산자(*)를 각각 사용해야 한다.

잘못된 예제
int* ptr1, ptr2;
맞는 예제
int *ptr1, *ptr2;

 

포인터의 선언과 초기화

포인터를 선언한 후 참조 연산자(*)를 사용하기 전에 포인터는 반드시 초기화되어야 한다.

초기화하지 않은 채로 참조 연산자를 사용하면, 어딘지 알 수 없는 메모리 장소에 값을 저장하는 꼴이 된다.

타입* 포인터이름 = &변수이름;
또는
타입* 포인터이름 = &주소값;

 

포인터의 참조

포인터는 참조연산자(*)를 사용하여 참조할 수 있다.

 

아래의 예제는 포인터의 주소값과 함께 포인터가 가리키고 있는 주소값의 데이터를 참조하는 예제이다.

int x = 7;            // 변수의 선언
int *ptr = &x;      // 포인터의 선언
int **pptr = &ptr; // 포인터의 참조

int num1 = 1234;
double num2 = 3.14;
 
int* ptr_num1 = &num1;
double* ptr_num2 = &num2;

① cout << "포인터의 크기는 " << sizeof(ptr_num1) << "입니다." << endl;
② cout << "포인터 ptr_num1가 가리키고 있는 주소값은 " << ptr_num1 << "입니다." << endl;
③ cout << "포인터 ptr_num1가 가리키고 있는 주소에 저장된 값은 " << *ptr_num1 << "입니다." << endl;

cout << "포인터 ptr_num2가 가리키고 있는 주소값은 " << ptr_num2 << "입니다." << endl;
cout << "포인터 ptr_num2가 가리키고 있는 주소에 저장된 값은 " << *ptr_num2 << "입니다.";
포인터의 크기는 8입니다.
포인터 ptr_num1가 가리키고 있는 주소값은 0x7fff789fab54입니다.
포인터 ptr_num1가 가리키고 있는 주소에 저장된 값은 1234입니다.

포인터 ptr_num2가 가리키고 있는 주소값은 0x7fff789fab58입니다.
포인터 ptr_num2가 가리키고 있는 주소에 저장된 값은 3.14입니다.

위의 예제에서 ①번은 sizeof 연산자를 활용하여 포인터 변수의 크기를 구한 것이다.

②번과 ③번은 포인터가 가리키는 변수의 타입에 따라 포인터의 타입도 같이 바꿔주고 있다.

포인터의 타입은 참조 연산자를 통해 값을 참조할 때, 참조할 메모리의 크기를 알려주는 역할을 하기 때문이다.

 

 

2) 포인터 연산


포인터 연산

포인터는 값을 증가시키거나 감소시키는 등 제한된 연산만을 할 수 있다.

포인터끼리의 덧셈, 곱셈, 나눗셈은 아무런 의미가 없다.

포인터끼리의 뺄셈은 두 포인터 사이의 상대적인 거리를 나타낸다.

포인터에 정수를 더하거나 뺄 수는 있지만, 실수와의 연산은 불가능하다.

포인터끼리 대입하거나 비교할 수 있다.

 

타입별 포인터 연산

포인터 연산에서 포인터 연산 이후 각각의 포인터가 가리키고 있는 주소는 포인터의 타입에 따라 달라진다.

증가 폭은 포인터 변수가 가리키는 변수의 타입의 크기와 같다.

예를 들어, int형 포인터의 증가폭은 int형 타입의 크기인 4바이트만큼 증가하게 된다.

마찬가지로 포인터의 뺄셈에서도 동일하게 적용된다.]

 

포인터와 배열의 관계

배열의 이름은 그 이름을 변경할 수 없는 상수라는 점을 제외하면 포인터와 같다.

C++에서는 배열의 이름을 포인터처럼 사용할 수 있을 뿐만 아니라, 포인터를 배열의 이름처럼 사용할 수도 있다.

int arr[3] = {10, 20, 30}; // 배열 선언
int* ptr_arr = arr; // 포인터에 배열의 이름을 대입함.

 
cout << "배열의 이름을 이용하여 배열 요소에 접근 : " << arr[0] << ", " << arr[1] << ", " << arr[2] << endl;
cout << " 포인터를 이용하여 배열 요소에 접근 : "
   << ptr_arr[0] << ", " << ptr_arr[1] << ", " << ptr_arr[2] << endl;

cout << "배열의 이름을 이용한 배열의 크기 계산 : " << sizeof(arr) << endl;
cout << " 포인터를 이용한 배열의 크기 계산 : " << sizeof(ptr_arr);
배열의 이름을 이용하여 배열 요소에 접근 : 10, 20, 30
     포인터를 이용하여 배열 요소에 접근 : 10, 20, 30

배열의 이름을 이용한 배열의 크기 계산 : 12
     포인터를 이용한 배열의 크기 계산 : 8

배열의 이름을 이용한 크기 계산에서는 배열의 크기가 int형 배열 요소 3개의 크기인 12바이트로 정상 출력된다. 하지만 포인터를 이용한 크기 계산에서는 배열의 크기가 아닌 포인터 변수 자체의 크기가 출력된다.

 

배열의 포인터 연산

앞선 에제와 달리 배열의 이름을 포인터처럼 사용하는 예제이다.

배열의 이름으로 포인터 연산을 진행하여 배열의 요소에 접근한다.

int arr[3] = {10, 20, 30}; // 배열 선언

cout << " 배열의 이름을 이용하여 배열 요소에 접근 : " << arr[0] << ", " << arr[1] << ", " << arr[2] << endl;
cout << "배열의 이름으로 포인터 연산을 해 배열 요소에 접근 : "
    << *(arr+0) << ", " << *(arr+1) << ", " << *(arr+2);
배열의 이름을 이용하여 배열 요소에 접근 : 10, 20, 30
배열의 이름으로 포인터 연산을 해 배열 요소에 접근 : 10, 20, 30

위의 코드를 통해 배열의 이름과 포인터 사이에는 다음과 같은 공식이 성립함을 알 수 있다.

arr이 배열의 이름이거나 포인터이고 n이 정수일 때,
arr[n] == *(arr + n)

 

 

3) 메모리의 동적 할당


데이터 영역과 스택 영역에 할당되는 메모리의 크기는 컴파일 타임에 미리 결정된다.

하지만 힙 영역의 크기는 프로그램이 실행되는 도중인 런타임에 사용자가 직접 결정한다. 이렇게 런 타임에 메모리를 할당받는 것을 메모리의 동적 할당이라고 한다.

 

포인터의 목적이 뭘까?

포인터의 가장 큰 목적은 런 타임에 이름 없는 메모리를 할당받아 포인터에 할당하여 할당받은 메모리에 접근하는 것이다.

 

new 연산자

C언어에서는 malloc()이나 calloc()을 활용하여 메모리의 동적 할당을 수행한다.

C++에서는 new 연산자를 통해 메모리의 동적 할당을 활용할 수 있다.

 

new 연산자란?

new 연산자는 자유 기억 공간(free store)라고 불리는 메모리 공간에 객체를 위한 메모리를 할당받는 것이다. 이때, new 연산자를 통해 할당받은 메모리는 따로 이름이 없으므로 해당 포인터로만 접근할 수 있다.

타입* 포인터이름 = new 타입;

앞에 있는 타입은 데이터에 맞는 포인터를 선언하기 위해 사용되며,

뒤에 있는 타입은 메모리의 종류를 지정하기 위해 사용된다.

만약 메모리의 부족으로 새로운 메모리를 만들지 못하게 된다면 new 연산자는 null 포인터를 반환한다.

 

delete 연산자

C언어에서는 free() 함수를 이용해 동적으로 할당받은 메모리를 운영체제로 반환한다.

C언어에서의 free()함수처럼 C++에서는 delete연산자를 통해 더 이상 사용하지 않는 메모리를 다시 메모리 공간에 돌려줄 수 있다.

delete 포인터이름;

 

아래의 코드는 런 타임시에 int형과 double형 데이터를 위한 메모리를 할당받고, delete 연산자를 이용해 더 이상 사용하지 않는 메모리를 반환하는 예제이다.

int* ptr_int = new int;
*ptr_int = 100;

double* ptr_double = new double;
*ptr_double = 100.123;

cout << "int형 숫자의 값은 " << *ptr_int << "입니다." << endl;
cout << "int형 숫자의 메모리 주소는 " << ptr_int << "입니다." << endl;

cout << "double형 숫자의 값은 " << *ptr_double << "입니다." << endl;
cout << "double형 숫자의 메모리 주소는 " << ptr_double << "입니다." << endl;

delete ptr_int;
delete ptr_double;

이때! new 연산자를 통해 생선한 메모리가 아닌 경우에는 delete연산자로 해제할 수 없다.

delete 연산자는 반드시 new 연산자를 통해 할당된 메모리를 해제할 때만 사용해야 한다.

'Computer Science > C++' 카테고리의 다른 글

[C++] 구조체  (0) 2023.11.13
[C++] 문자열  (1) 2023.11.12
[C++] 배열과 포인터(1차원 배열,다차원 배열)  (0) 2023.11.05
[C++] 제어문  (0) 2023.10.28
[C++] 연산자  (1) 2023.10.27