[C언어] 포인터와 배열! 함께 이해하기

김민정·2024년 8월 13일
0
post-thumbnail

Chapter 13. 포인터와 배열! 함께 이해하기

이번 chapter에서는 "배열의 이름도 포인터다", '배열이름의 포인터 형'을 이해하는데 중점을 두었다.

13-1 "포인터와 배열의 관계"

배열의 이름은 포인터이고, 그 값을 바꿀 수 없는 '상수 형태의 포인터'이다.
다음 예제를 보자.

#include <stdio.h>
int main()
{
	int arr[3] = { 0, 1, 2 };
	printf("배열의 이름: %p \n", arr);
	printf("첫 번째 요소: %p \n", &arr[0]);
	printf("두 번째 요소: %p \n", &arr[1]);
	printf("세 번째 요소: %p \n\n", &arr[2]);
	// 각 배열의 주소값은 4byte 차이가 난다! (int형 배열이기 때문에)
	// arr = &arr[i];	// 이 문장은 컴파일 에러를 일으킨다.
    return 0;
}

> 출력
배열의 이름: 0000002E4BAFF6D8
첫 번째 요소: 0000002E4BAFF6D8
두 번째 요소: 0000002E4BAFF6DC
세 번째 요소: 0000002E4BAFF6E0

예제와 위 사진의 할당된 배열의 구조를 통해 "모든 배열요소가 메모리 공간에 나란히 할당된다"라는 것을 알 수 있다.
또한, 배열의 이름은 대입 연산자의 피연산자가 될 수 없으므로(값의 저장이 불가능하므로) "배열의 이름은 배열의 시작 주소 값을 의미하며, 그 형태는 값의 저장이 불가능한 상수이다."

<배열 이름과 포인터 변수의 비교>

포인터 변수나 배열의 이름 둘 다 이름이 존재하며, 특정 메모리 공간의 주소 값을 지닌다.
다만, 포인터 변수는 이름 자체로 변수지만, 배열의 이름은 가리키는 대상의 변경이 불가능한 상수다.
즉, 배열의 이름은 상수 형태의 포인터이다. 포인터 상수라고도 한다.

*연산

*연산은 1차원 배열이름의 포인터 형과 배열이름을 대상으로 하는 연산이다.
1차원 배열이름의 포인터 형은 배열의 이름이 가리키는 대상을 기준으로 결정된다.
예를 들어 int arr1[5]의 경우 배열이름 arr1이 가리키는 것은 첫 번째 배열요소인 int형 변수므로 arr1은 int형 포인터(int *)가 된다.
double arr2[7]의 경우 배열이름 arr2가 가리키는 것은 첫 번째 배열요소인 double형 변수이므로 arr2는 double형 포인터(double *)가 된다.

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

	printf("%d %g \n", *arr1, *arr2);
	*arr1 += 100;
	*arr2 += 120.5;
	printf("%d %g \n\n", arr1[0], arr2[0]);
    return 0;
}

> 출력
1 1.1
101 121.6

위 예제에서도 볼 수 있든 포인터 변수는 배열의 이름처럼 사용할 수 있다.
따라서, 포인터 변수로 할 수 있는 연산은 배열의 이름으로도 할 수 있고, 배열의 이름으로 할 수 있는 연산은 포인터 변수로도 할 수 있다.

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

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

> 출력
15 15
25 25
35 35
15 15

사실 이렇게 포인터 변수를 배열의 이름처럼 사용하고 배열의 이름을 포인터처럼 사용하는 경우도 거의 없지만 개념을 알기 위해 알아두면 좋다.


13-2 "포인터 연산"

포인터를 대상으로 메모리의 접근을 위한 *연산 이외에 증가 및 감소 연산도 가능하다. 그리고 이 연산의 결과 중요하다.

포인터를 대상으로 하는 증가 및 감소 연산

#include <stdio.h>
int main()
{
	int num1 = 10;
    double num2 = 20.2;
	int* ptr1 = &num1;
	double* ptr2 = &num2;

	printf("ptr1 + 1: %p ptr1 + 2: %p\n", ptr1 + 1, ptr1 + 2);
    //	ptr1 + 1: 0000000000000014 (+4) ptr1 + 2: 0000000000000018 (+8)
	printf("ptr2 + 1: %p ptr2 + 2: %p\n", ptr2 + 1, ptr2 + 2);
    // 	ptr2 + 1: 0000000000000018 (+8) ptr2 + 2: 0000000000000020 (+16)

	printf("ptr1 (origin): %p ptr2 (origin): %p \n", ptr1, ptr2);
    //	ptr1(origin): 0000000000000010 ptr2(origin): 0000000000000010
	ptr1++;
	ptr2++;

	printf("ptr1++ : %p ptr2++ : %p \n\n", ptr1, ptr2)
    return 0;
}

> 출력
ptr1 + 1: 0000000000000014 ptr1 + 2: 0000000000000018
ptr2 + 1: 0000000000000018 ptr2 + 2: 0000000000000020
ptr1 (origin): 0000000000000010 ptr2 (origin): 0000000000000010
ptr1++ : 0000000000000014 ptr2++ : 0000000000000018

위 예제처럼 포인터의 초기화는 적절한 초기화가 아니다. 하지만 연산 결과를 보기 좋게 확인하기 위해 진행했다.
포인터를 대상으로 하는 증감연산의 결과는 다음과 같다.

  • type형 포인터를 대상으로 n 증가 : n x sizeof(자료형)의 크기만큼 증감

따라서 다음 예제과 같은 배열접근이 가능하다.

#include <stdio.h>
int main()
{
	int Arr1[3] = { 11, 22, 33 };
	int* ptr3 = Arr1;
	printf("%d %d %d\n", *ptr3, *(ptr3 + 1), *(ptr3 + 2));	// 각 ptr3[0], ptr3[1], ptr3[2]의 값을 가리켜 값 출력. (위치가 변한건 x)

	printf("%d ", *ptr3); ptr3++;	// ptr3[0] 출력 이후 ptr3[0] 가리키는 곳이 → ptr3[1] 위치로 이동.
	printf("%d ", *ptr3); ptr3++;
	printf("%d ", *ptr3); ptr3--;	// 출력 이후 감소.
	printf("%d ", *ptr3); ptr3--;
	printf("%d ", *ptr3); printf("\n\n");
    return 0;
}
    
> 출력
11 22 33
11 22 33 22 11

*ptr, *(ptr+1), *(ptr+2)의 참조결과 출력 시 Arr1[0], Arr1[1], Arr1[2]에 저장된 요소가 출력된다.

그렇다면 아래 두 연산의 차이를 알 수 있을까?

*(++ptr) = 20;	// ptr에 저장된 값 자체를 변경
*(ptr+1) = 20;	// ptr에 저장된 값은 변경되지 않음

⭐따라서 arr[i] == *(arr+i)라는 중요한 결론을 얻을 수 있다.⭐

*(ptr+0), *(ptr+1), *(ptr+2)
ptr[0], ptr[1], ptr[2]
*(arr+0), *(arr+1), *(arr+2)
arr[0], arr[1], arr[2]

위 네줄 코드는 모두 동일한 결과다.


13-3 "상수 형태의 문자열을 가리키는 포인터"

마지막에 널 문자가 삽입되는 문자열의 선언방식에는 두 가지가 있다.
하나는 앞서 설명한 배열을 이용하는 방식이고,
다른 하나는 char형 포인터 변수를 이용하는 방식이다.

두 가지 형태의 문자열 표현

  1. 배열을 기반으로 하는 문자열의 선언 👉 변수 형태의 문자열 선언
    문자열의 일부를 변경 가능. 변수 성향의 문자열
    ex) char str1[ ] = "My String"; // 배열의 길이는 자동을 계산됨.
  2. 포인터 기반으로 하는 문자열의 선언 👉 char형 포인터로 선언
    문자열의 첫 번째 문자 주소 값이 반환되고 그 반환 값이 포인터 변수에 저장. 상수 성향의 문자열
    ex) char * str2 = "Your String";

두 방식의 차이는 무엇일까?

str1은 그 자체로 문자열 전체를 저장하는 배열, str2는 메모리상에 자동으로 저장된 문자열의 첫 번째 문자를 단순히 가리키고만 있는 포인터 변수.
따라서, 배열이름 str1은 계속해서 문자 M이 저장된 위치를 가리키는 상태이지만, 포인터 변수 str2는 다른 위치를 가리킬 수 있다.
하지만, 배열이름 str1은 상수형태의 포인터이기 때문에 가리키는 대상을 변경할 순 없지만 배열을 대상으로 값의 변경은 가능하다. 따라서 변수 형태의 문자열이라고 한다.
포인터 변수 str2은 가리키는 대상은 변경 가능하지만 가리키는 문자열의 내용 변경은 불가능 하다. 다라서 상수 형태의 문자열이라 한다.

#include <stdio.h>
int main()
{
	char str1[] = "My String";	// 변수 상태의 문자열
	char* str2 = "Your String";	// 상수 상태의 문자열
	printf("%s %s \n", str1, str2);

	str2 = "Our String";	// 가리키는 대상 변경.
	printf("%s %s \n", str1, str2);

	str1[0] = 'X';	// 변경 가능.
	printf("%s \n", str1);
	str2[0] = 'X';	// 변경 불가.
	printf("%s \n\n", str2);	//	위 명령에서 반영이 안되기 때문에 출력 안됨.
    
    return 0;
}

> 출력
My String Your String
My String Our String
Xy String

위에서 글로 설명한 내용을 예제로 한번에 나타낼 수 있다.

어디서든 선언할 수 있는 상수 형태의 문자열

상수 형태의 문자열을 처리 과정은 어떻게 될까?
예를 들어 char * str = "Const String";라는 코드가 있다.
위에서도 잠깐 언급한 적 있지만 "Const String"이라는 문자열이 메모리 공간에 저장되고 이 메모리의 주소 값이 반환된다.
따라서 char * str = 0x1234;와 같이 문자열의 주소 값이 저장된다.

그렇다면 함수의 호출과정에서 선언되는 문자열은 어떨까?
printf("show your string");이라는 코드가 잇으면 큰따옴표로 묶여서 표현되는 문자열은 그 형태에 상관없이 메모리 공간에 저장된 후 그 주소 값이 반환된다.
따라서 위의 함수호출 문장도 메모리 공간에 문자열이 저장된 이후에 printf(0x1234);의 식으로 된다고 볼 수 있다.
printf 함수는 문자열을 통째로 전달 받는 것이 아닌 문자열의 주소 값을 전달받는 함수라는 것을 알 수 있다.


13-4 "포인터 변수로 이뤄진 배열: 포인터 배열"

포인터 배열이란?

포인터 배열이란? 포인터 변수로 이뤄진, 그래서 주소 값의 저장이 가능한 배열을 말한다.
포인터 배열의 선언방식은 다음과 같다.

int * arr1[20];		// 길이가 20인 int형 포인터 배열 arr1
double * arr2[30];	// 길이가 30인 double형 포인터 배열 arr2

포인터 배열의 선언은 기본 자료형 배열의 선언과 동일하다.
배열의 이름 앞에 배열요소의 자료형 정보를 선언하면 된다.

#include <stdio.h>
int main()
{
	int num1 = 10, num2 = 20, num3 = 30;
	int * arr[3] = { &num1, &num2, &num3 };

	printf("%d \n", *arr[0]);
	printf("%d \n", *arr[1]);
	printf("%d \n\n", *arr[2]);	
    // for ( int i=0;i<3;i++) printf("%d \n", *arr[i]);로 해도 됨.
	return 0;
}

> 출력
10
20
30

위 예제의 선언된 포인터 배열과 선언된 변수드르이 관계를 그림으로 나타내면 위와 같다.
포인터 배열도 기본 자료형 배열과 별반 다르지 않다.
단, 8~10행에서 볼 수 있듯 포인터 표시인 *을 생략하고 그냥 arr[0]으로 적으면 안된다. 배열(할당 메모리)을 잡고 값을 넣은게 아니기 때문이다.
arr[0]을 적게되면 출력이 해당 메모리의 주소 값이 출력된다.

문자열을 저장하는 포인터 배열

문자열 배열이란? 문자열의 주소 값을 저장할 수 있는 배열로, 사실상 char형 포인터 배열이다. ex) char * strArr[3]; // 길이가 3인 char형 포인터 배열

#include <stdio.h>
int main()
{
	char* strArr[3] = { "Simple", "String", "Array" };

	for (int i = 0; i < 3; i++)
		printf("%s \n", strArr[i]);
	
    // 추가 코드로 각 문자열이 나란히 메모리 주소에 저장된 걸 확인할 수 있다.
	for (int i = 0; i < 3; i++)
		printf("%p \n", strArr[i]);	

	return 0;
}

> 출력
Simple
String
Array
00007FF704ACAEA8
00007FF704ACAEB0
00007FF704ACAEB8

큰 따옴표로 묶여서 표현되는 문자열은 그 형태에 상관없이 메모리 공간에 저장된 후 그 주소 값이 반환된다. 따라서, 초기화 리스트에 선언된 문자열들은 메모리 공간에 저장되고, 그 위치에 저장된 문자열의 주소 값이 반환된다.

반환된 주소 값은 문자열의 첫 번째 문자의 주소 값이니, char형 포인터 배열에 저장 가능한 것이다.


<Review>

이번 chapter는 포인터 변수, 배열이름 포인터 등 이름이 헷갈리기 시작한다.
오랜만에 보니 나도 다시 헷갈리는,,, ㅋㅋㅋㅋ
처음 공부할 때는 윤성우의 프로그래밍 스터디그룹 카페에 있는 강좌를 보면서 이해하는 것을 추천한다!
윤성우 강사님 및 저자님의 열띤 강의를 들을 수 있다.👍'
(카페라 카페 가입은 필수다.)
지치지 말고 다음 공부도 궈궈...!!!

profile
백엔드 코린이😁

0개의 댓글

관련 채용 정보