포인터와 배열의 관계
배열의 이름 자체는 포인터이다. 즉 메모리 블록의 시작 주소 값을 가지고 있고 출력도 할 수 있다.
단, 일반 포인터 변수는 주소 값의 변경이 가능하지만 배열의 이름은 주소 값을 변경할 수 없다. 그래서 배열의 이름을 상수 형태의 포인터라고도 하고, 포인터 상수라고 부르기도 한다.
int main(void)
{
itn arr[3] = {0,1,2};
printf("배열의 주소 값 : %p \n", arr); // %p는 주소 값의 출력에 사용되는 서식문자이다.
printf("배열의 첫번째 주소 값 : %p \n", &arr[0]);
printf("배열의 두번째 주소 값 : %p \n", &arr[1]);
printf("배열의 세번째 주소 값 : %p \n", &arr[2]);
//arr = &arr[i] // arr은 주소 값의 첫 번째 요소를 가리키기 때문에 컴파일 에러가 발생한다.
}
#출력 결과
배열의 주소값 : 0012FF50
배열의 첫번째 주소값: 0012FF50
배열의 두번째 주소값: 0012FF54
배열의 세번째 주소값: 0012FF58
포인터 연산
포인터를 대상으로 하는 증가 및 감소연산
포인터 변수를 대상으로 증가 및 감소 연산이 가능하다. 그런데 1+1 =2 처럼 단순한 정수 간의 덧셈, 뺄셈이 아니다.
포인터 변수마다 자료형을 설정할 수 있는데 이 자료형이 몇 바이트인지에 따라 그 바이트 수 만큼 현재 위치에서 메모리 블록을 이동한다. 예를 들어, int형 포인터 변수 1을 더하면 4가 증가하고 double형이면 1을 더했을 때 8이 증가한다. 또한 모든 배열요소가 메모리 공간에 '나란히' 할당된다.
#include <stdio.h>
int main() {
int i = 10;
double d = 20.0;
int* pi = &i;
double* pd = &d;
printf("증가 전:\n");
printf("pi = %p, pd = %p\n", (void*)pi, (void*)pd);
pi++; // int 포인터를 1 증가
pd++; // double 포인터를 1 증가
printf("증가 후:\n");
printf("pi = %p, pd = %p\n", (void*)pi, (void*)pd);
return 0;
}
증가 전:
pi = 0000009D0CAFF944, pd = 0000009D0CAFF9430
증가 후:
pi = 0000009D0CAFF948, pd = 0000009D0CAFF938
arr[i] ==*(arr+i)
한 단계 더 나아가 배열을 접근하는 포인터의 예시를 살펴보자. 포인터의 증가, 감소는 현재 위치를 기준으로 메모리 블록을 int형(4바이트), double형(8바이트)과 같은 자료형의 크기에 맞추어 이동한다.
#include <stdio.h>
int main(void)
{
int arr[3]={1, 2, 3};
int * ptr = &arrl;
printf("%d %d %d \n", *ptr, *(ptr+1), *(ptr+2));
printf("%d", *ptr); ptr++;
printf("%d", *ptr); ptr++;
printf("%d", *ptr); ptr--;
printf("%d", *ptr);
}
#출력 결과
1 2 3
1 2 3 2
포인터를 처음 배우는 과정에서 가장 혼란스러운 부분 중 하나는 메모리 주소와 그 주소에 저장된 변수의 값이 서로 다르다는 사실을 명확히 구분하는 것이다. 결론적으로 앞서 보인 예제를 보면 다음의 식을 도출할 수 있다. arr[i] ==*(arr+i)
arr+1 과 *(arr+1) 의 차이점
#include <stdio.h>
int main(void)
{
int arr[3]={1, 2, 3};
int * ptr = &arrl;
printf("%d %d %d \n", *ptr, *(ptr+1), *(ptr+2));
printf("%d", *ptr); ptr++;
printf("%d", *ptr); ptr++;
printf("%d", *ptr); ptr--;
printf("%d", *ptr);
}
결론적으로, arr+1 은 메모리의 주소 번지를 참조하고 *(arr+1) 은 포인터가 가리키는 실제 값을 참조한다.
위 예시에서, arr[0]의 메모리 주소가 0x0012FF50이라고 가정해 보자. 이 주소는 , arr 또는 ptr을 사용하면 참조할 수 있다.
이 주소에 저장된 값은 메모리 주소 0x0012FF50에 담겨 있으며, arr[0] 또는 *ptr을 통해 접근할 수 있다.
이를 통해 우리는 두 가지 중요한 사실을 이해할 수 있다.
- ptr 은 메모리의 번지 수를 나타내는 변수이다. 이 경우, 배열의 첫 번째 요소의 주소를 가리킨다.
- *ptr을 사용하면 ptr이 가리키는 위치에 저장된 실제 값을 얻을 수 있다.
상수 형태의 문자열을 가리키는 포인터
마지막에 널 문자가 삽입되는 문자열의 선언방식에는 두 가지가 있다.
하나는 배열이름, 다른 하나는 Char형 포인터 변수이다.
char str[] = "string";
char * str = "Ptr string";
이 둘의 차이점은 아래 그림과 같이 배열은 글자 하나씩 메모리 블록에 할당하지만 포인터는 글자를 블록 하나에 다 담아 블록 하나의 주소 값만 가리킨다는 것에서 아래와 같은 차이점과 특징이 나타난다.
1. 포인터 변수(char *str = "string")는 변수 형태의 포인터이자, 상수 형태의 문자열이다.
즉, 화살표 위치만 바꾸면 되기 때문에 가리키는 대상을 변경할 수 있다.
그러나 문자의 내용은 변경할 수 없다. 왜냐하면 한 메모리 블록 안에 모든 문자가 다 들어가 있기 때문이다.
2. 배열이름(char str[ ]="string")은 상수 형태의 포인터이자, 변수 형태의 문자열이다.
즉, 배열 이름은 언제나 첫 번째 메모리 블록의 값을 가지고 있어야 하기 때문에 가리키는 대상을 변경할 수 없다.
하지만 박스 안에 원하는 물건을 넣을 수 있는것처럼 문자 하나하나가 한 블록의 방을 가지고 있기 때문에 문자의 내용은 자유롭게 변경할 수 있다.
그렇다면 상수 형태의 문자열인 포인터 변수의 문장 처리 과정을 살펴보자
char * str ="string";
위의 문장이 실행되면 먼저 따옴표로 묶인 문자열이 OS가 할당한 어떤 메모리 한 블록에 저장된다.
그 다음 포인터 변수 str 에는 그 메모리의 주소 값이 저장된다.
char * str =0x123550;
참고로 printf도 같은 방식으로 문자열을 출력해야 할 때 문자열을 통째로 전달받지 않고 , 문자열의 "주소 값"을 전달받는다.
포인터 변수로 이루어진 배열 : 포인터 배열
포인터 변수로 이루어져 주소 값이 저장이 가능한 배열을 포인터 배열이라고 한다. 포인터 배열의 선언방식은 다음과 같다.
int * arr[10]; // 길이가 10인 int형 포인터 배열 arr
double * arr[5]; // 길이가 5인 double형 포인터 배열 arr
포인터 배열의 예제를 살펴보자
#include <stdio.h>
int main() {
// 세 개의 정수 배열 선언
int arr1[] = {1, 2, 3};
int arr2[] = {4, 5, 6};
int arr3[] = {7, 8, 9};
// 정수 배열의 포인터 배열 선언 및 초기화
int *arrPointers[3] = {arr1, arr2, arr3}; // 배열을 가리킬때는 배열의 이름(배열 1번째 값)으로 선언. & 사용 X
// 포인터 배열을 이용하여 배열의 요소 출력
for (int i = 0; i < 3; i++) { // 각 배열에 대하여
for (int j = 0; j < 3; j++) { // 각 배열의 요소에 대하여
printf("%d ", arrPointers[i][j]); // arrPointers[i]는 i번째 배열을 가리킴
}
printf("\n");
}
return 0;
}
- 배열 선언: 우선, int arr1[] = {1, 2, 3};, int arr2[] = {4, 5, 6};, int arr3[] = {7, 8, 9};를 통해 세 개의 정수 배열을 선언한다. 각 배열은 3개의 정수 요소를 갖는다.
- 포인터 배열 선언: int *arrPointers[3] = {arr1, arr2, arr3};에서 arrPointers는 정수 포인터의 배열을 선언한다. 이 배열은 세 개의 정수 배열 arr1, arr2, arr3 각각의 시작 주소를 저장한다. & 주소 값을 사용하면 각 배열 3개의 정수를 모두 가리키기 때문에 컴파일 오류가 발생한다.
- 배열 요소 접근: 이중 for 반복문을 사용하여 포인터 배열을 통해 각 배열의 요소에 접근하고 출력한다. arrPointers[i][j]는 i번째 배열의 j번째 요소를 의미한다. 예를 들어 arrPointers[0][0]은 arr1 의 첫 번째 요소, 즉 1을 출력하게 된다.
이와 같이 포인터 배열을 사용하면 1차원 배열을 이용하여 2차원 배열과 유사한 기능을 수행할 수 있다.
문자열을 저장하는 포인터 배열
문자열의 주소 값을 저장하는 배열을 포인터 배열이라고 한다.
char *strArr[3]; // 길이가 3인 char형 포인터 배열
아래 예제와 같은 큰 따옴표로 묶여서 표현되는 문자열은 그 형태에 상고나없이 메모리 공간에 저장된 후 그 주소 값이 반환된다.
#include <stdio.h>
int main(void)
{
char *strArr[3]={"ABC", "DEF", "GHI"};
printf("%s"\n, strArr[0]);
printf("%s"\n, strArr[1]);
printf("%s"\n, strArr[2]);
return 0;
}
// 출력 결과 :
ABC
DEF
GHI
즉, 문장이 실행되면 초기화 리스트에 선언된 문자열들이 메모리 공간에 저장되고, 그 위치에 저장된 문자열의 주소 값이 반환된다. 따라서 문자열이 저장된 이후에는 각 요소 첫 번째 문자의 주소 값이 저장된다.
char *strArr[3]={0xff1004, 0xff1005, 0xff1006};
이제 메모리 관점에서 살펴보자
- strArr 배열의 메모리 사용량:
- strArr는 char * 타입의 포인터 3개를 저장한다. 64비트 시스템에서 포인터의 크기는 보통 8바이트이다. 따라서, strArr 배열 전체는 8바이트 * 3 = 24바이트를 사용한다.
- 각 문자열 리터럴의 메모리 사용량:
- 문자열 리터럴 "ABC", "DEF", "GHI" 각각은 실제 문자 데이터와 문자열의 끝을 나타내는 널 문자('\0')를 포함하여 메모리에 저장된다.
- 각 문자열은 4개의 문자('A', 'B', 'C', '\0' 등)를 포함한다. char 타입은 대부분의 시스템에서 1바이트를 사용한다. 따라서, 각 문자열 리터럴은 4바이트를 사용한다.
여기서 주의할 점은 strArr 배열의 메모리 사용량과 문자열 리터럴의 메모리 사용량을 구분해서 이해해야 한다는 것이다. strArr 배열의 메모리 사용량은 포인터들이 차지하는 공간과 관련이 있고, 각 문자열 리터럴의 메모리 사용량은 별도로 계산되며, 이들은 서로 다른 메모리 영역에 저장된다.
-
- strArr 배열은 포인터들을 저장하는 데 24바이트(8바이트* 3)를 사용한다.
- 각 문자열 리터럴은 각각 별도의 4바이트를 사용한다.
'윤성우의 C Programming' 카테고리의 다른 글
Chapter 14. 포인터와 함수에 대한 이해 (0) | 2024.03.02 |
---|---|
Chapter 12. 포인터의 이해 (0) | 2024.02.28 |
두 개의 텍스트 파일 비교하기 (0) | 2024.02.13 |
A와 P로 시작하는 단어의 수를 세어서 출력하기 (1) | 2024.02.13 |
Chapter 27. 파일의 분할과 헤더파일의 디자인 (0) | 2024.02.12 |