본문 바로가기
C언어

C언어 - 포인터와 배열

by 소프트웨어 학부생의 개발 도전기 2023. 7. 12.

저번 포스팅까지는 포인터와 관련해서 기초적인 지식에 대해서 공부하였다.

이번 포스팅에서는 포인터와 배열의 관계에 대해 한번 알아보겠다.

 

포인터와 배열은 서로 떼려야 뗄 수 없는 관계이다!  왜 그런지 이번 포스팅을 통해 한 번 공부해 보자.

이번 글에서는 포인터와 배열의 관계, 포인터 연산에 대해서 한번 글을 작성해 보겠다.

 

1. 배열의 이름은 무엇을 의미하는가?

과연 포인터와 배열은 어떤 관계에 있을까?

한 번 저번 포인터 내용 복습 겸 아래의 코드를 살펴보자.

#include<stdio.h>
int main(void){
	int num1 = 10;
	int num2 = 30;
	int* ptr;
	ptr = &num1;

	*ptr += 30;
	printf("num1 : %d, *ptr = %d\n", num1, *ptr);

	ptr = &num2;
	*ptr -= 20;
	printf("num2 : %d, *ptr = %d\n", num2, *ptr);

	return 0;
}

저번 글에서 포인터에 대해서 잘 이해하였다면 위의 코드는 간단하다.

int형 포인터 변수 ptr을 하나 선언하고 한 번은 num1을 가리키고 num1변수의 메모리공간에 접근해서 값을 수정하고, 

그다음에 num1과의 참조관계를 끊고 num2를 가리켜서 num2의 메모리 공간에 접근해서 값을 바꾸거나 수정하는 

간단한 코드이다.

 

여기서 ptr 이 가리키는 대상을 변경하고 있다.

그러면 ptr이 가리키는 대상을 변경할 수 있는 이유는 무엇인가?

바로 ptr은 변수이기 때문에 가리키는 대상을 변경할 수 있다.

 

그렇다면 배열은 어떠한가?

만약에 내가 int arr [3];이라는 배열을 하나 선언했다고 가정해 보자.

배열은 첫 번째 요소의 주소값을 지니는 상수이다.

arr이라는 배열은 배열의 첫번째 요소를 가리키기 때문에 상수이다.

 

정리하자면

ptr  변수  -->  *ptr  -->  ptr이라는 포인터 변수에 int형 포인터라는 정보가 담겨있다.(int형 변수를 가리키는 포인터)

arr  상수  -->  *arr  -->  마찬가지로 int형 포인터라는 정보가 담겨있다.

하지만, 위의 ptr과 arr의 차이점은 상수냐 변수냐 이 차이점이 있다.

 

 

ptr이 int형 포인터인 건 알겠지만, arr도 왜 int형 포인터인가?

바로 배열의 이름이 첫 번째 요소를 가리키고 있기 때문에 int형 포인터이다.

예를 들어 설명해 보겠다.

 

int num = 10;
int* ptr = &num;

위와 같이 코드를 작성했다고 가정해 보자. 이렇게
되면 ptr 이 num의 주소를 저장하고 num을 가리키게 된다. 이때 num 변수는 메모리에 4바이트의 공간을 차지한다. ptr은 num이 저장된 메모리 공간의 첫 번째 바이트를 가리키는 이유는, 포인터가 해당 변수의 값을 참조하거나 변경할 때 메모리에서 어디서부터 읽어야 하는지를 알려주기 위해서이다.

 

따라서 비슷한 논리로 배열도 마찬가지로 배열의 이름은 시작 주소 값을 의미하는(배열의 첫 번째 요소를 가리키는) 포인터이다.

 

검증을 위해 다음 코드를 살펴보자.

#include<stdio.h>
int main(void) {
	int arr[3] = { 1,2,3 };
	printf("배열의 이름 : %p\n", arr);
	printf("배열의 첫 번째 요소 : %p\n", &arr[0]);
	printf("배열의 두 번째 요소 : %p\n", &arr[1]);
	printf("배열의 세 번째 요소 : %p\n", &arr[2]);
	return 0;
}

출력결과▼

배열의 이름 출력결과와 첫 번째 요소의 주소값이 같다.

위의 예제에서 보듯이 배열의 이름은 배열의 시작 주소 값을 의미하는 포인터임을 증명했다. 

단순히 주소 값이 아닌 포인터인 이유는 메모리 접근에 사용되는 * 연산이 가능하기 때문이다.

배열 요소간 주소 값의 크기는 4바이트임을 알 수 있다(모든 요소가 붙어있다는 의미)
배열 이름과 포인터 변수의 비교

(사진 출처 : 윤성우 열혈 C프로그래밍)

 

2. 1차원 배열 이름의 포인터 형

1차원 배열 이름의 포인터 형 결정하는 방법은 다음과 같다.

1) 배열의 이름이 가리키는 변수의 자료형을 근거로 판단

2) int형 변수를 가리키면 int* 형

3) double형 변수를 가리키면 double* 형

 

아래의 코드를 한번 살펴보자

#include<stdio.h>
int main(void) {
	int arr1[3] = { 1,2,3 };
	double arr2[3] = { 1.1,2.2,3.3 };

	pritnf("%d %g\n", *arr1, *arr2);
	*arr1 += 100;
	*arr2 += 120.5;

	printf("%d %g\n", arr1[0], arr2[0]);
		
	return 0; 
}

출력결과▼

arr1 같은 경우는 주소값이 있고, 가리키는 대상에 대한 정보가 int형 포인터이므로 * 연산의 결과로 4바이트 형태로 메모리 공간에 정수를 저장한다.

arr2 같은 경우는 주소값이 있고, 가리키는 대상에 대한 정보가 double형 포인터이므로 * 연산의 결과로 8바이트 형태로 메모리 공간에 실수를 저장한다.

 

 

3. 포인터를 배열의 이름처럼 사용가능하다.

위에서 한번 배열이 단순히 주소값이 아닌 포인터인 이유가 메모리 접근에 사용되는 * 연산이 가능하다고 했다.

그러면 포인터도 마찬가지로 배열의 이름처럼 사용가능한가?

다음 아래의 코드를 한 번 살펴보자

#include<stdio.h>
int main(void) {
	int arr[3] = { 15,25,35 };
	int* ptr = &arr[0];  //int* ptr = arr; 와 동일한 문장

	printf("%d %d\n", ptr[0], arr[0]);
	printf("%d %d\n", ptr[1], arr[1]);
	printf("%d %d\n", ptr[2], arr[2]);
	printf("%d %d\n", ptr[3], arr[3]);
	printf("%d %d\n", *ptr, *arr);
	return 0;
}

출력결과▼

실제로 포인터 변수 ptr을 대상으로 ptr [0], ptr [1], ptr [2]와 같은 방식으로 메모리 공간에 접근이 가능하다.

 

지금까지 배운 내용을 다시 한번 정리해 보겠다.

 

1. 배열의 이름 + 포인터변수 --->  포인터!

2. 단, 배열의 이름은 상수이고 포인터 변수는 변수이다.

3. 배열의 이름은 왜 포인터인가? -> 주소값이 존재하고 가리키는 대상에 대한 정보가 있기 때문이다.

 

 

4. 포인터 연산

포인터도 마찬가지로 포인터 변수에 저장된 값을 대상으로 하는 증가 및 감소연산을 진행할 수 있다.(곱셈, 나눗셈 등등은 불가) 아래의 코드도 마찬가지로 포인터 연산의 일종이다. 다음 코드를 살펴보자.

#include<stdio.h>
int main(void) {
	int* ptr1 = 0x0010;   //3,4번 라인이 적절하지 않은 초기화지만 예시를 위해 이렇게 초기화해보았다.
	double* ptr2 = 0x0010;

	printf("%p %p\n", ptr1 + 1, ptr1 + 2);
	printf("%p %p\n", ptr2 + 1, ptr2 + 2);
	printf("%p %p\n", ptr1, ptr2);
	ptr1++;
	ptr2++;
	printf("%p %p\n", ptr1, ptr2);

	return 0;
}

출력결과▼

위와 같은 예시를 통해 다음과 같은 사실을 알 수 있다.

 

■int형 포인터 변수 대상의 증가 및 감소 연산 시 sizeof(int)의 크기만큼 값이 증가 및 감소한다. 

double형 포인터 변수 대상의 증가 및 감소 연산 시 sizeof(double)의 크기만큼 값이 증가 및 감소한다.

이것들을 일반화하게 되면

 

■type형 포인터 변수 대상의 증가 및 감소 연산 시 sizeof(type)의 크기만큼 값이 증가 및 감소한다.

 

 

그렇다면 앞서 말했듯이 배열도 포인터이므로 포인터를 대상으로 하는 증가 및 감소연산이 가능한가?

당연히 가능하다!

아래의 코드를 살펴보겠다.

#include<stdio.h>
int main(void) {
	int arr[3] = { 100,200,300 };
	int* ptr = &arr[0];  //int* ptr = arr; 과 같은 문장
	
	printf("%d %d %d\n", *ptr, *(ptr + 1), *(ptr + 2));
	printf("%d\n", *ptr); ptr++;  //printf함수 호출 후 ptr++ 실행
	printf("%d\n", *ptr); ptr++;
	printf("%d\n", *ptr); ptr--;
	printf("%d\n", *ptr); ptr--;
	printf("%d\n", *ptr);

	return 0;
}

출력결과▼

int형 포인터 변수를 대상으로 증감 연산을 하면 4Byte씩 증가 및 감소를 하니, int형 포인터 변수가 int형 배열을 가리키면, int형 포인터 변수의 값을 증가 및 감소시켜서 배열 요소에 순차적으로 접근이 가능하다.

위의 사진과 출력결과처럼 배열을 상대로 포인터연산을 진행하면 4Byte씩 크기가 증감한다는 것을 알 수 있다.

 

배열이름도 포인터이니, 포인터 변수를 이용한 배열의 접근방식을 배열의 이름에도 사용할 수 있다. 

그리고 배열의 이름을 이용한 접근방식도 포인터 변수를 대상으로 사용할 수 있다.

중요한 결론은 arr이 포인터 변수의 이름이건 배열의 이름이건 arr [i] == *(arr + i) 이렇게 사용할 수 있다는 것이다.

 

다음 코드를 한번 살펴보자

#include<stdio.h>
int main(void) {
	int arr[3] = { 11,22,33 };
	int* ptr = &arr[0];

	for (int k = 0; k < 3; k++) 
		printf("%d %d %d %d\n", arr[k], *(arr + k), ptr[k], *(ptr + k));

	/*printf("%d %d %d\n", *(ptr + 0), *(ptr + 1), *(ptr + 2));
	  printf("%d %d %d\n", ptr[0], ptr[1], ptr[2]);
	  printf("%d %d %d\n", *(arr + 0), *(arr + 1), *(arr + 2));
	  printf("%d %d %d\n", arr[0], arr[1], arr[2]);
	*/
	
	return 0;
}

출력결과▼

실제로 메모리 공간에 접근해서 계산할 때 *(arr + i) 이런 식으로 계산이 이루어진다.

중요한 것은 포인터 변수로 할 수 있는 일은 배열의 이름을 가지고도 할 수 있다.

 

배열을 선언하고 선언된 배열의 요소에 접근할 수 있도록(arr [0], arr [1], arr [2])와 같이 이러한 형태의 연산을 허용하고 마찬가지로 배열은 이름 그 자체가 포인터 이므로 (*(arr + 0), *(arr + 1), *(arr + 2))와 같이 이러한 형태의 연산도 허용한다.

 

오늘 배운 내용을 한 번 정리해 보겠다.

 

1. 배열과 포인터 변수 둘 다 포인터이다. 다만, 차이점이 있다면 배열은 상수이고, 포인터 변수는 변수라는 점이다.

2. 배열도 포인터처럼 사용 가능하고 포인터도 배열처럼 사용가능하다.

3. 배열을 상대로 포인터 연산 진행 시 sizeof(type)만큼 진행된다. 즉, 배열의 다음 요소를 가리키게 되는 것이다.

(포인터를 대상으로 하는 증가 및 감소연산도 마찬가지로 int형일 경우 sizeof(int)의 크기. 즉, 4Byte의 크기만큼 진행된다)

4. arr [i] == *(arr + i) 이 사실을 꼭 기억해 주기 바란다.

 

다음 글에서는 상수 형태의 문자열을 가리키는 포인터라는 주제에 대해서 들고 오겠다.