본문 바로가기

컴퓨터공학/C, C++

C,C++> 배열과 포인터

첨자 연산

배열 특징 복습. 배열은 첨자가 0부터 시작되며 초기화되지 않고 끝 점검을 하지 않는다. 

배열은 포인터와 만날 때 진정한 가치를 발휘한다. 

C언어 배열은 다음과 같은 두 가지 특징을 가진다. 

1. C는 내부적으로 1차원 배열만 지원한다. 2차원 이상 다차원 배열은 1차원 배열의 확장에 불과하다. 

2. 배열을 구성하는 배열 요소 타입에는 전혀 제한이 없다. 배열 그 자체가 배열의 요소가 될 수 있다. 

 

C의 모든 배열은 내부적으로 1차원이다.

배열 요소로 또 다른 배열을 사용할 수 있으므로 배열끼리 중첩이 가능하다.

외부적으로는 다차원 배열을 지원하는 셈이다. 

배열 중첩끼리 중첩되어 있을 때 다른 배열에 포함된 배열을 부분 배열(SubArray)라고 한다. 

부분 배열을 배열 요소로 가지는 배열을 전체 배열(모배열)이라고 한다. 

2차원 배열은 배열의 배열이고 3차원 배열은 배열의 배열의 배열이다. 

그렇다면 왜 2차원 배열과 배열의 배열을 구분하려고 하는가?

배열이 내부적으로 어떻게 돌아가든 외부에서 사용할 때 두 개의 첨자를 사용하면 되므로 2차원 배열이다. 

배열을 단순히 사용하기만 한다면 2차원 배열이든 배열의 배열로 보든 차이점이 없다.

그러나 내부적으로 어떻게 처리되는가에 따라 차이점이 생긴다. 

1. 첨자 연산 방법

2. 부분 배열 자격 문제

다차원 배열에서는 부분 배열만 단독으로 사용할 수 없지만 배열의 배열에서는 부분 배열단독으로 배열로 인정된다. 

 

다차원 배열과는 다르게 부분 배열이 배열명으로 인정되어 부분 배열 혼자만 떼어내어 사용하는 것이 가능하다.

예를 들어 3차원 배열이라면 ari[0][0][0] 이런 식으로 하지 않고 ari[0][0] 이렇게 해도 사용할 수 있다. 

전체 배열에 속해 있지만 독립성을 갖고 있다. 

 

복습하면 배열 이름은 시작 위치를 가리키는 포인터 상수이다. 

문자형 배열이 있으면 문자형 배열명은 시작 위치를 가리킨다. 

주소값은 printf의 %s 서식과 대응한다. 

 

[ ] 연산자 

ptr이 임의의 배열을 가리키는 포인터이고 n이 정수일 때 

ptr[n] = *(ptr+n)

 

배열명 그 자체는 배열 시작 번지를 가리키는 포인터 상수이며 주소값에서 상수값을 더하면 상수만큼 주소값을 이동한다. 

배열 요소를 읽을 때 사용하는 [ ] 연산자는 내부적으로 포인터 연산을 수행하는 포인터 연산자로 해석되며 *(ptr + n)과 완전히 동일한 식이다. 약속이자 정의되어 있는 것이다. 

 

다음은 모두 같은 의미를 갖는다. 

ar[2], *(ar+2), 2[ar] 

2[ar]은 *(2+ar)이 되고 덧셈 교환 법칙으로 *(ar + 2)이 되므로 ar[2]이 된다. 따라서 2[ar] 은 ar[2]이다. 

 

그렇다면 2차원 배열에서 [ ] 연산자가 어떻게 동작하는지 알아보자. 

ar[2][1]은 정의에 의해서 

*(*(ar+2)+1)이 되며 컴파일러는 이 포인터 연산식을 실행할 것이다. 

연산 순위에 따라서 괄호 제일 안쪽부터 연산된다. 

 

ar이 2차원 배열일 경우

ar+2 

전체 배열명 ar은 배열 선두 번지(&ar[0][0])를 가리키는 포인터 상수이다.

여기에 2를 더하면 ar 배열 요소 중 2번째 요소를 가리킨다. 따라서 ar+2는 ar[2] 선두 번지값이 된다.  

sizeof(ar+2)는 4가 되지만 포인터 덧셈 연산 결과는 32바이트 증가한다.

ar요소가 정수형이 아니라 크기 4의 정수형 부분 배열(크기는 16바이트)인 것을 알 수 있다.  컴파일러가 2차 정수 배열로 인식하기보다 정수형 부분 배열의 1차 배열로 인식하는 것을 알 수 있다. 

 

*(ar+2)

ar+2가 부분 배열이므로 읽혀지는 값은 정수형이 아니라 크기 4의 정수 배열형이 될 것이다. 

sizeof(*(ar+2))를 계산하면 16이 되는데 배열이 sizeof 피연산자가 되면 배열 그 자체로 평가되기 때문이다. 

*(ar+2)는 ar 부분 배열 ar[2]이고 이 값은 ar[2] 부분 배열 선두번지 (&ar[2][0])으로 평가된다. 

 

*(ar+2)+1

부분 배열 ar[2]에서 1번째 배열 요소를 가리키는 포인터가 된다. 

*(ar+2)가 부분 배열명이므로 포인터 상수이고 포인터 상수에 상수를 더했으므로 결과는 포인터이다. 

ar[2]+1 와 같고 &ar[2][1]과도 동일하다. 

 

*(*(ar+2)+1)

최종 연산 결과는 ar[2][1]이다. 컴파일러는 ar[2][1] 같은 연산문을 만나면 동일한 포인터 연산식으로 바꾼 후 배열 요소를 읽는다. 부분 배열의 선두 번지를 구하다가 최하위 배열 요소를 찾아내 그 값을 읽는 단계이다. 

 

[ ] 연산자 동작을 설명하기 위해서 포인터 지식 세 가지가 필요하다. 

1. 배열명은 포인터 상수이다.

2. 포인터와 정수끼리 덧셈하면 sizeof(T)만큼 이동하고 그 결과 포인터이다. 

3. * 연산자는 포인터가 가리키는 번지 내용을 읽는다. 

 

내부 처리 과정을 몰라도 배열을 사용하는데 문제 없다.

정의란 외워서 사용하는 것이며 한 번 이해하고 나면 다음에는 잊어 버려도 상관없다. 

피타고라스 정리나 근의 공식은 이해하고 사용하는 대상이지 증명 자체를 외울 필요가 없다. 

반드시 이해해야 할 요점은 [ ] 연산자는 포인터 연산을 한다는 내용이다. 

 

 

포인터 배열 

정의

포인터 배열은 요소가 포인터형인 배열이다. 

정수형 포인터 배열을 선언하고 싶으면 다음과 같이 선언한다. 

int *arpi[5];

arpi 라는 이름 아래 포인터 변수가 다섯 개 모여 있다는 것 외에 특별한 점은 없다.

포인터 배열이 사용되는 예시는 문자형 포인터 배열이다. 가장 실용적인 예시는 구조체 포인터 배열이다. 

문자형 포인터가 하나의 문자열을 표현할 수 있으므로 문자형 포인터 배열은 곧 문자열 배열이다. 

char *arps[ ] = {"dsdfd", "dsfdfsd", "aaas", "ewrwe"};

 

각 문자열은 메모리 상에 흩어져서 존재한다.

이 문자열 주소값을 포인터 배열로 갖고 있으면 위치에 상관없이 문자열에 관한 반복 처리가 가능해진다. 

이런 문자열 배열을 Ragged 배열이라고 한다. 개별 문자열 길이가 달라도 낭비되는 메모리가 없다. 

2차원 배열이 있을 때 각 행 길이가 모두 똑같으면(Rectangular)

배열 길이가 각각 다르면(Ragged)

 

포인터와 배열

int ar[n];
int *pi;

ar 이란 배열 이름은 배열 시작 번지를 가리키는 주소값이다.

pi는 정수형 변수 하나를 가리킬 수 있는 포인터이다. 포인터 변수이므로 pi를 위해 할당하는 메모리는 항상 4바이트이다. 

정수형 변수 주소값을 대입받지만 정수형 배열을 가리킬 수 있다.

pi = (int *)malloc(n*sizeof(int));

pi = (int *)malloc(n*sizeof(int));

이렇게 되면 pi는 ar 과 같은 자격을 가진다. 정수형 배열처럼 행세할 수 있다. ar[2]처럼 pi[2]로도 읽을 수 있다. 

공통점 

동일 타입 변수 집합을 다룰 수 있다는 점에서 기능적으로 같고 범위 점검을 할 수 없다는 제약도 동일하다. 

배열은 선언할 때 지정한 크기만큼, 포인터는 할당할 때 지정한 크기만큼 실제 크기를 가지고 있으니 이 크기를 넘지 않아야 한다. 

차이점

1. 포인터는 변수이고 배열은 상수이다. pi는 고유 메모리를 차지하고 있어서 언제든지 다른 대상을 가리킬 수 있다. 하지만 ar은 고정되어 있어서 바꿀 수 없다. ar은 오직 배열 선두 주소값을 읽을 수 있다. 

2. pi 배열 크기는 동적이고 ar 배열 크기는 정적이다. 포인터로 할당한 배열은 실행 중에도 realloc으로 재할당할 수 있다.

3. 배열은 크기가 커서 함수 인수로 전달할 수 없다. 포인터는 대상체가 무엇이든 크기가 4바이트라서 함수로 전달할 수 있다. 배열을 함수로 전달할 때 반드시 포인터를 사용해야 한다. 

4. 배열로 요소를 읽는 것과 포인터로 대상체를 읽는 동작은 속도 차이가 있다. 배열은 매번 선두에서 시작하지만 포인터는 대상체로 직접 이동해서 읽으므로 접근 속도가 빠르다. *pi는 pi가 가리키는 곳을 바로 읽지만 ar[n]은 *(ar+n)이라서 번지를 더한 후 읽어서 느리기 때문이다. 대략 포인터가 배열보다 2배 정도 빠르다.

 

배열 포인터

포인터 배열(Array of pointer) : 원소가 포인터인 배열 

배열 포인터(Pointer to array) : 배열 번지를 담는 포인터 변수

배열 포인터는 사용 빈도가 높지 않고 실용성이 떨어진다. 

배열 포인터는 2차원 이상 배열에서 의미가 있다. 1차원 배열은 부분 배열 개념이 없어서 배열 포인터를 선언할 수 없으면 선언할 필요가 없다. 최소 2차원 이상이어야 전체 배열 요소가 부분 배열이 되며 부분 배열을 가리키는 배열 포인터를 선언할 수 있다. 

일차원 배열 int ar[5]을 가리키는 포인터가 필요하면 int *pa라고 선언하고 pa = ar로 초기화하면 된다. 

이때 pa 대상은 ar 배열 요소이지 ar 배열 그 자체가 아니기 대문에 다순한 정수형 포인터 변수에 불과하다. 

따라서 배열 포인터가 아니다.  

 

2차원 배열 포인터를 선언하는 방법은 다음과 같다. 

요소형 (*포인터명)[2차 첨자 크기] 

고라호가 없으면 배열 포인터가 아니라 포인터 배열이다. 배열 포인터 선언할 때는 2차 첨자 크기를 밝혀야 하는데 이 크기를 알아야 대상체 배열  전체 크기를 구할 수 있기 때문이다. 

포인터 복습. 포인터는 대상 크기를 알아야 * 연산자로 읽을 수 있고, ++와 -- 연산자로 앞뒤 이동할 수 있다. 그래서 요소 타입과 2차 첨자 크기 정보가 필요하다. 쉽게 말해서 어떤 녀석이 얼마나 모여 있는 지 알아야 한다. 

1차 첨자 크기는 알지 않아도 되는데 포인터는 자신이 가리키는 번지 앞뒤 동일 타입 데이터가 임의 개수만큼 있다고 가정하고 이동하기 때문이다. 

3차는 2차 이후 첨자만 적고 앞에 (*변수명)을 붙이면 된다. 

char arps[5][9] = {"고양이", "강아지", "송아지", "망아지"};
char (*ps)[9];
ps = arps;

char *ps[9]는 문자형을 가리키는 포인터 9개를 요소로 가지는 포인터 배열이다.

이 선언으로 ps 배열 포인터는 크기 9 문자형 배열이라는 것을 알 수 있다. 

*ps로 읽으면 문자열이 읽혀지고 ps++, ps--는 대상체 크기 9바이트 만큼 앞뒤로 이동한다. 

 

배열 인수

배열 포인터는 타입이 다른 배열은 변수에 대입할 수 없다. 

대입하고 싶으면 캐스팅해야 한다.

int (*)[7]은 크기 7 배열 포인터라는 뜻이다. 

int의 타입은 int이고 int *pi 타입은 int * 이다. 변언 선언문에서 변수를 빼면 타입이다. 그러므로 int (*pa)[7] 선언문에서 변수명 pa를 빼면 int (*)[7]만 남는다. 이게 배열 포인터의 타입이다. 

 

배열 인수 표기법 

C는 함수 인수로 배열을 전달하는 방법은 제공하지 않는다. 오로지 포인터로만 전달한다. 다음은 모두 같은 내용이다.

void OutArray(int ar[])
void OutArray(int *ar)
void OutArray(int ar[5])

int ar[5]는 정수형 포인터 ar을 의미하는 것이지 정수형 배열을 의미하는 건 아니다.

다만 정수형 포인터 인수를 배열처럼 표기하는 것을 허용할 뿐이다. 모두 int *ar과 동일하다. 

괄호 안 배열 크기는 생략할 수 있고 상수값은 적을 수 있지만 컴파일러는 상수값을 무시한다. 

실제로는 포인터로 넘어가지만 배열 형태 그대로 적을 수 있다. 

 

같은 원리로 다음은 두 가지 방법으로 표기할 수 있다.

int GetTotalForWeek(int (*pa)[7])
int GetTotalForWeek(int pa[][7])

int pa[3][7]처럼 첫 번째 첨자 자리에 크기를 밝힐 수 있는데 무시된다. 

 

포인터 인수를 배열 형태로 표기하는 예는 main 함수 인수에서도 볼 수 있다.

void main(int argv, char *argv[]);
void main(int argv, char **argv);

함수 인수 목록에서 포인터형 인수에 대해서 int *ar 외에 int ar[]이라는 표기도 허용되며 두 표기법은 동일하게 해석한다.

어디까지 함수 인수 목록에서만 그럴 뿐이며 일반 선언문에서는 int ar[]이라는 표기로 포인터를 선언할 수 없다. 

두 표기법의 차이점은 다음과 같다.

int *ar : 이 인수가 포인터라는 것을 강조한다. 호출원에서 &i나 pi 등을 넘길 때 이런 표기법이 좋다. 함수를 쓰는 사람은 이 표기를 보고 이 인수가 정수형 변수 주소값을 전달한다고 생각할 것이다. 

int ar[] : 인수가 배열로부터 온 포인터라는 것을 강조한다. 이렇게 표시된 인수는 배열이라는 것을 쉽게 알 수 있다.

문법적으로 동일하게 작동하지만 함수를 읽는 사람에게 인수 의미를 제공할 수 있다. 

 

이차 배열 인수 

일차 배열을 함수 인수로 전달할 때 단순 포인터와 개수를 넘긴다.

이차 이상 배열을 함수 인수로 넘길 땐 배열 포인터를 사용해야 한다.

이차원 이상 배열도 그 요소가 1차원 배열로 취급되어 시작 번지와 개수를 넘겨야 한다.  

만약 임의 모양을 가지는 이차원 배열을 함수로 전달하려면 시작 번지를 전달하고 별도 인수로 폭과 높이를 따로 알려줘야 한다. 

 

2차원 배열을 인수로 전달하는 방식은 실용성도 없고 실제로 사용할 일이 없다. 왜냐하면 이처원 배열이면 핵심 자료 구조로 사용될 확률이 높고 이런 배열은 전역 선언되기 때문이다. 따라서 모든 함수가 이 배열을 자유롭게 읽고 쓸 수 있어서 함수 인수로 전달할 필요가 없다. 설령 쓸 지라도 이차원 배열 단독으로 전달되는 경우보다 구조체 멤버로 포함되어 전달하는 경우가 많아서 이차원 배열 인수는 큰 의미가 없다. 

 

이차 배열 할당

2차원 배열을 동적 할당할 수 있을까?

정적 선언 문장은 char ar[3][4]가 되는데 이 선언문의 상수 3과 4를 실행 중에 결정하고 싶다. 

char *p = (char *)malloc(3*4*sizeof(char));
free(p);

그러나 이렇게 하면 길이 12 문자형 일차 배열을 할당한 것이지 2차원 배열 할당이 아니다. 

원래 char[3][4]는 길이 4의 문자열 3개를 의도한 내용이다. 근데 p는 길이 12 문자열 하나에 불과하다. 따라서 2차원 배열로 사용할 수 없다. 할당 길이만 같을 뿐이다. 동적 할당하기 위해서는 배열 포인터로 받아야 한다. 

char (*p)[4] = (char (*)[4])malloc(3*4*sizeof(char));
strcpy(p[0], "dog");
strcpy(p[1], "cow");
strcpy(p[2], "cat");
for(int i=0; i<3; i++) puts(p[i]);
free(p);

첨자 길이가 4이지만 null문자를 고려하여 실제 3자까지밖에 저장하지 못한다. 사실 이 예제는 동적할당이 아니다. 왜냐하면 실행 중 크기를 결정하고 싶은데 이 예제는 컴파일 중에 결정된 것이기 때문이다. 따라서 다음과 같은 것이 진정한 동적 할당이다. 

int n = 5;
int *pi = (int *)malloc(n*sizeof(int));
free(pi)

 

n은 상수가 아니라 변수이다. 입력값이나 함수 인수로 전달된 값으로 대체할 수 있다. 

int n = 3;
int (*p)[4] = (char (*)[4])malloc(n*4*sizeof(char));
free(p);

 

잘 컴파일되고 일차 첨자 크기인 4도 바꿔서 할당해보면

int n=3; m=4;
char (*p)[m] = (char (*)[m])malloc(n*m*sizeof(char));

 

위 코드는 컴파일이 안된다. 왜냐하면 배열 포인터 대상체 배열 크기값은 상수로만 줄 수 있기 때문이다. 

p가 어떤 배열을 가리킬 것인가는 실행중에 결정할 수 없고 컴파일할 때 이미 결정되어 있어야 한다. 

따라서 위 코드를 쓰고 싶다면 const int m=4;라고 해야 한다.

의도한 대로 두 첨자를 실행 중에 바꾸고 싶다면 이중 포인터를 사용해야 한다. 

 

int n=3, m=4;
int i;

char **p;
p =(char**)malloc(n*sizeof(char *));
for (i=0;i<n;i++){
	p[i]=(char *)malloc(m*sizeof(char));
}
strcpy(p[0],"dog");
strcpy(p[1],"cow");
strcpy(p[2],"cat");
for(i=0;i<n;i++) puts(p[i]);
for(i=0;i<n;i++) {
	free(p[i]);
}
free(p);

문자형 이중 포인터 p를 선언하고 p를 먼저 동적 할당. p 자체 크기를 변수로 지정할 수 있기 때문이다. 

그리고 각 p요소를 다시 동적 할당한다. 

메모리 여기저기 흩어져서 할당받지만 실행 중에 할당된 이차원 배열이라고 할 수 있다. 

2단계로 할당했으므로 해제할 때도 2단계 거쳐야 한다. 

 

정리하자면 2차원 배열을 한 번에 할당하는 건 불가능하다. 두 첨자 크기를 실행 중에 모두 결정할 수 없기 때문이다. 

다만 이중 포인터로 동일한 형태 배열을 만들 수 있으므로 가능하다고 볼 수 있다.

 

&ar

int ar[5] 선언문은 크기 5 정수형 배열을 선언하는 문장이다. ar은 배열 시작 번지를 가리키는 포인터 상수이다. 

ar에 &연산자를 붙인 &ar은 무슨 의미일까? ar은 포인터 상수이므로 주소값을 가지지 않아서 &연산자를 쓸 수 없다.

그러나 ANSI C 이후 배열이 & 피연산자가 될 때 포인터 상수가 아니라 배열 그 자체를 가리키는 것으로 변경되어서 사용 가능하다. 실제로 어떻게 다른지 연구해보자.

int ar[5] = {1,2,3,4,5};
printf("%p\n", ar);
printf("%p\n", &ar);

실행하면 똑같은 주소값을 가리킨다. 

int ar[5] = {1,2,3,4,5};
int *pi;
pi = ar; //가능
pi = &ar; //에러

 

정수형 포인터 pi는 정수형 포인터 상수인 ar을 대입받을 수 있지만 &ar을 받을 수 없다.

대입이 안된다는 이야기는 좌우변 타입이 다르다는 뜻이다. 

ar은 정수형 배열 시작 번지를 가리키는 포인터 상수이므로 정확한 타입은 int * const이며 대상체는 int이다. 

그러나 &ar은 대상체가 크기 5 정수형 배열이고 타입은 int (*)[5] const이다. 즉 배열 포인터 상수이다.

 

int ar[5] = {1,2,3,4,5};
int *p1;
int (*p2)[5];

p1=ar;
p2=&ar;
printf("before=%p\n",p1);
printf("before=%p\n",p2);
p1++;
p2++;
printf("after=%p\n",p1);
printf("after=%p\n",p2);

위 예제에서 p1, p2는 최초 같은 주소값을 가리키고 있으나 1 증가할 때 p1은 4바이트 이동하지만 p2는 20바이트 이동한다. 이것으로 두 포인터 타입과 대상체가 다르다는 것을 알 수 있다.  p1은 정수형 포인터이고 p2는 정수형 배열 포인터이다.

 

scanf로 문자열을 입력받는데 이 함수는 참조 호출하므로 변수 앞에 &이 붙어야 하지만 배열은 그 자체가 포인터이므로 &을 붙이지 않아도 된다. 그래서 문자열을 입력받을 땐 다음과 같이 한다.

char name[20];
scanf("%s", name);

하지만 scanf("%s", &name);으로 써도 잘 동작한다. name과 &name 타입은 다르지만 가리키는 주소는 같다. 

둘 다 포인터형이므로 4바이트이고 %s 서식과 대응할 수 있으며 타입은 다르지만 같은 주소값을 가리키고 있어서 잘 동작한다. 하지만 정상 문법은 아니라서 &이 붙어선 안된다. &name[0]이라고 쓰는 것이 정상이다.

 

배열과 문자열

문자열 상수 

정수 상수가 필요하면 아라비아 숫자를 적는다.

문자 상수가 필요하면 홑따옴표 안에 문자 하나를 넣는다. 

문자열 상수가 필요하면 겹따옴표를 사용한다. 

 

문자열 상수는 정적 데이터 영역에 기록된다. 정적 데이터 영역은 전역변수, 정적 변수가 저장되는 곳이다. 

정수나 문자형 상수는 기본 타입이고 크기가 작아서 그 값을 코드에 곧바로 기록할 수 있다.

특정 메모리 위치에 상수를 저장하는 명령은 기계어 코드로 곧바로 표현할 수 있다. 

하지만 문자열 상수는 그 자체가 배열이며 길이가 길 수 있어서 코드로 곧바로 표현하지 못한다. 

따라서 문자열을 어딘가에 기록하고 포인터를 사용해 메모리에 복사해야하는 것이다. 

저장 장소가 바로 프록램 뒤쪽인 정적 데이터 영역이다. 

어떤 실행파일이든 뒷부분을 확인하면 무슨 문자열 상수들을 사용하는지 확인할 수 있다. 

컴파일러는 문자열 상수를 정적 데이터 영역에 기록할 때 항상 null 종료 문자도 같이 포함한다. 

"Hey" 문자열 상수를 사용하면 정적 데이터 영역에는 "Hey\0"을 기록된다. 

그래야 문자열 상수를  사용하는 곳에서 문자열 길이를 알 수 있기 때문이다. 

코드에서 문자열 상수는 문자열 시작 주소값을 가리키는 문자형 포인터 상수로 평가된다. 

 

컴파일러는 문자열 상수를 특별하게 취급한다.

첫 번째는 같은 문자열 상수를 두 번 이상 사용하면 이 문자열은 한 번만 기록된다. 

두 번째는 컴파일러는 중간에 콤마나 영문자 등이 없는 연속된 문자열 상수를 하나로 합쳐서 기록한다.

그래서 따옴표를 닫고 개행한 후 다음 줄을 써도 상관없고  행 계속 문자인 \을 사용할 수 있다.

 

문자 배열 초기화

char str[] = "sdfsdfsd sdfjksdfjkl  kdjfs";

큰 따옴표로 싸여진 문자열 상수를 적으면 문자열을 구상하는 문자들을 str 배열 요소에 순서대로 복사한다. 

배열 제일 끝에 nul 종료 문자도 자동으로 붙이고 배열 크기를 생략하면 문자열 길이 +1도 알아서 길이 계산한다.

컴파일러가 이런 초기화 방법을 허용하는 건 다른 타입에 비해 특별한 예외 처리이다. 

이게 가능한 이유는 각 자리 크기가 일정해서 문자끼리 구분되며 바이트 단위 복사할 수 있기 때문이다. 

정수형이나 실수형은 각 요소 크기가 가변적이라서 초기값 형태만으로 어디까지가 몇 번째인지 구분이 안된다. 

예를 들어 int ar[] = {1231412}; 라고 한다고 구분되는게 아니라 콤마로 구분해야 한다. 

 

문자형 포인터

문자열은 문자의 집합이고 시작 주소값을 문자열이라고 할 수 있다.

따라서 문자열 시작 주소값을 저장할 수 있는 문자형 포인터도 문자열을 다룰 수 있다.

문자형 포인터는 얼마든지 다른 문자열을 가리킬 수 있다. 

반면 문자 배열은 선언할 때 문자열로 초기화할 수 있을 뿐 선언된 후 다른 문자열로 바꿔서 대입할 수 없다. 

배열명은 포인터 상수라서 한 번 정해지면 다른 번지를 가리킬 수 없기 때문이다. 

 

정리

char str[] = "Korea";
char *ptr = "Korea";

puts(str);
puts(ptr);

ptr = "Japan";
str = "Japan";

ptr[0] = 'J';
str[0] = 'J';

"Korea", "Japan" 문자열 상수를 사용했는데 이 문자열은 정적 데이터 영역에 종료 문자를 포함하여 기록된다. 

Korea는 두 번 사용되었는데 컴파일러는 이 문자열을 정적 데이터 영역에 한 번 기록한다. 

str은 "Korea"로 초기화했다. 문자열 상수 길이가 6이므로 str 배열 크기는 자동으로 6이 된다. 

컴파일러는 str을 정적 데이터 영역에 있는 Korea 문자열로 이 배열을 초기화한다.

이 초기화는 str 배열 주소값이 정적 데이터 영역에 있는 Korea로 바뀌는 것이 아니라 정적 데이터 영역의 Korea 문자열이 str 배열로 복사되는 것이다. 즉 str 배열은 사본 문자열이다. 

문자형 포인터 ptr도 Korea로 초기화되는데 ptr이 정적 데이터 영역에 있는 문자열 상수 Korea를 가리킨다. 

이상 태에서 puts 함수로 str와 ptr을 입력하면 동일한 문자열을 출력하지만 출력 대상은 다르다. 

str은 사본값이고 ptr은 정적 데이터 영역에 있는 Korea 그 자체를 출력하는 것이다. 

str 배열은 문자열 사본을 가지고 있어서 내용을 바꿀 수 있다. str[0], str[1]은 문자형 변수이기 때문에 언제든지 바꿀 수 있다. 또는 strcpy 함수를 사용하면 str 내용을 통째로 다른 문자열로 바꿀 수 있다. 

반면 ptr이 가리키는 내용은 변경할 수 없다.

왜냐하면 정적 데이터 영역은 실행 파일 내부이기 때문에 읽을 수 있지만 쓸수 없다. 

 

문자열 배열

문자의 배열은 문자열이다. 문자열의 배열은 어떻게 할까? 2차원 문자 배열이 필요하다. 

char arCon[][32] = {"Korea","America","Russia","Japan"};
char *pCon[] = {"Korea","America","Russia","Japan"};

전자는 배열이 직사각경 형태를 이루므로 직사각형(Rectangular) 배열이라고 하고

후자는 배열 오른쪽 끝이 들쭉날쭉 하므로 들쭉날쭉(Ragged) 배열이라고 한다. 

전자는 문자형 2차원 배열이다. 모든 배열 폭이 32바이트로 동일하다.글자 수를 채우지 않으면 메모리 낭비이다.  초기화 과정에서 정적 데이터 영역에 있는 문자열들의 사본이 이 배열로 복사되어서 실행 중에 문자열 내용을 자유롭게 변경할 수 있다. 

후자는 문자형 포인터 1차 배열이다. 초기화 과정에서 정적 데이터 영역에 저장된 문자열 상수 주소값이 각 배열에 대입된다. 따라서 문자열 길이가 아무리 길어도 주소값은 항상 4바이트라서 메모리 낭비가 거의 없다. 그러나 실행 중 문자열 내용 수정이 안된다. 

 

 

포인터 핵심 내용

1. &, * 연산자 기능과 포인터로 변수를 간접적으로 참조하는 방법

2. 함수가 참조 호출로 실인수 값을 변경하는 방법과 원리

3. 포인터가 티입을 가지는 이유와 포인터 연산 동작 

4. 포인터를 이용한 동적 메모리 할당 

 

 

 


어떤 공부에나 고비는 있고 누구나 고비를 피해갈 수 없다. 고비를 만날 때마다 남들도 어려워하는 하는 부분이라고 생각하고 나도 남들과 다르지 않다는 걸 인정해야 한다. 조급하게 마음 먹으면 스스로에게 실망만 커져 금방 포기한다.  잠시 쉬었다가 하는 한이 있더라도 고비를 잘 넘기자. 시간을 투자하고 정성을 쏟으면 다 넘을 수 있다. 

 

'컴퓨터공학 > C, C++' 카테고리의 다른 글

C,C++> 포인터 고급  (0) 2022.07.25
C,C++> 구조체  (0) 2022.07.21
C,C++> 포인터  (0) 2022.07.05
C,C++> 배열  (0) 2022.06.09
C,C++> 기억 부류  (0) 2022.06.08