이번 chapter에서는 "배열의 이름도 포인터다", '배열이름의 포인터 형'을 이해하는데 중점을 두었다.
배열의 이름은 포인터이고, 그 값을 바꿀 수 없는 '상수 형태의 포인터'이다.
다음 예제를 보자.
#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
사실 이렇게 포인터 변수를 배열의 이름처럼 사용하고 배열의 이름을 포인터처럼 사용하는 경우도 거의 없지만 개념을 알기 위해 알아두면 좋다.
포인터를 대상으로 메모리의 접근을 위한 *연산
이외에 증가 및 감소 연산도 가능하다. 그리고 이 연산의 결과 중요하다.
#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
위 예제처럼 포인터의 초기화는 적절한 초기화가 아니다. 하지만 연산 결과를 보기 좋게 확인하기 위해 진행했다.
포인터를 대상으로 하는 증감연산의 결과는 다음과 같다.
따라서 다음 예제과 같은 배열접근이 가능하다.
#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]
위 네줄 코드는 모두 동일한 결과다.
마지막에 널 문자가 삽입되는 문자열의 선언방식에는 두 가지가 있다.
하나는 앞서 설명한 배열을 이용하는 방식이고,
다른 하나는 char형 포인터 변수를 이용하는 방식이다.
두 방식의 차이는 무엇일까?
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 함수는 문자열을 통째로 전달 받는 것이 아닌 문자열의 주소 값을 전달받는 함수라는 것을 알 수 있다.
포인터 배열
이란? 포인터 변수로 이뤄진, 그래서 주소 값의 저장이 가능한 배열을 말한다.
포인터 배열의 선언방식은 다음과 같다.
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는 포인터 변수, 배열이름 포인터 등 이름이 헷갈리기 시작한다.
오랜만에 보니 나도 다시 헷갈리는,,, ㅋㅋㅋㅋ
처음 공부할 때는 윤성우의 프로그래밍 스터디그룹 카페에 있는 강좌를 보면서 이해하는 것을 추천한다!
윤성우 강사님 및 저자님의 열띤 강의를 들을 수 있다.👍'
(카페라 카페 가입은 필수다.)
지치지 말고 다음 공부도 궈궈...!!!