본문으로 바로가기

파일의 IT 블로그

  1. Home
  2. 프로그래밍 강좌/C
  3. [C언어 강좌] #13-2 포인터(Pointer)

[C언어 강좌] #13-2 포인터(Pointer)

· 댓글개 · KRFile

안녕하세요 파일입니다.

저번 강의에 이어서 포인터 2번째 시간입니다.

 

바로 시작해보겠습니다!

 

포인터와 2차원 배열

&연산자와 *연산자로 2차원 배열을 공부하던것 기억나시나요? 

 

1차원 배열일때는 &array[0]을 하면 첫번째 주소의 값을 가르켰었습니다.

2차원 배열일때는 조금 달랐죠.

 

*(array + 0) == array[0] 이며 이것은 곧 0행의 대표주소 &array[0][0] 이였습니다.

1행의 대표주소는 *(array + 1) == array[1] 이며 이것은 곧 1행의 첫번째 요소의 주소 &array[1][0] 가 되었습니다.

 

포인터를 이용한 2차원 배열 탐색

#include <stdio.h>

int main(){
	int arr[3][4] = 
	{
		//3행 4열 
		{1,2,3,4},
		{5,6,7,8},
		{9,10,11,12} 
	};
	
	int * ptr = &arr[0][0];
	
	for (int i = 0; i < 12; i++){
		printf("%d ", *ptr);
		ptr = ptr + 1; //ptr++;
	}
	return 0;
}

1 2 3 4 5 6 7 8 9 10 11 12
--------------------------------
Process exited after 0.0344 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

2차원 배열의 경우에도 우리가 논리적으로 이해할땐 행, 열의 형태로 이해했지만

 

메모리란 것은 애초에 평면구조기 때문에,

 

물리적 구조로 배열을 이해할때는(실제 컴퓨터에 쌓이고 있는 메모리의 형태)는 2차원 배열이던, 3차원 배열이던, n차원 배열이던 간에 결국엔 모두 1차원 배열의 형태로 수직적으로 쌓인다는거 기억나시나요?

 

그렇기 때문에 포인터로 2차원 배열의 첫번째 요소에 대한 주소를 가리키고 1씩 더하는 포인터 연산을 수행하게 되면 4바이트씩 건너 뛰면서 1차원 배열처럼 요소 탐색이 가능해집니다.

 

#include <stdio.h>

int main(){
	int arr[3][4] = 
	{
		//3행 4열 
		{1,2,3,4},
		{5,6,7,8},
		{9,10,11,12} 
	};
	
	int * lptr = &arr[0][0]; //첫요소 가리킴 
	int * rptr = &arr[2][3]; //끝요소 가리킴 
	
	for (int i = 0; i < 6; i++){
		printf("%d ", *lptr);
		lptr++;
		
		printf("%d ", *rptr);
		rptr--;
	}
	return 0;
}

1 12 2 11 3 10 4 9 5 8 6 7
--------------------------------
Process exited after 0.06089 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

포인터를 조금 응용하면 다음과 같은것도 구현 가능합니다.

lptr로 배열 첫번째 요소를 가리키고 rptr로 끝요소를 가리킨 후 

lptr은 점점 오른쪽으로(++), rptr은 점점 왼쪽(--)으로 가면서

양 끝에서 시작해서 중앙까지 동시에 탐색할 수 있습니다.

 

즉 포인터로 원하는 곳의 배열요소의 주소를 가리키고

포인터 연산을 수행해서 배열의 아무 부분이나 무작위로 탐색이 가능해집니다.

 

 

int arr[12] 같은 배열이 있다면 포인터로 7번째 요소의 주소를 가리키고 1씩 더해가면서 8번째, 9번째 이런식으로 탐색이 가능하다는 말입니다.

 

물론 위의 예제는 꼭 포인터를 안쓰고 배열을 a[3], a[4] 처럼 써서 인덱스 접근으로도 구현이 가능합니다.

 

하지만 포인터로 하는 배열 탐색의 장점은, 인덱스 접근은 for문을 돌릴때 반복하는 i의 값을 잘 신경써서 조절해줘야 하는데 포인터는 원하는 곳을 가리키고 증감연산자로 더하거나 빼면 그만입니다.

때에 따라서는 훨씬 간편하게 탐색이 가능하단 말이죠.

 

 

포인터 1편에서 1차원 배열 탐색을 제대로 이해하셨고, 몇차원 배열이던 간에 결국 주소 자체는 순차적으로 쌓인다는걸 이해하셨으면 이 부분도 어렵지 않게 따라오실 수 있을겁니다. 잘 이해가 되지않으시면 1편을 다시 복습해보세요.


앞서 포인터 1편에서 배열이 포인터로 구현되어 있다는걸 알았고,

배열 이름이 포인터란것도 배웠습니다.

 

그래서 배열 이름이 배열의 시작주소를 가리키고 a[0], a[1] 처럼 인덱스로 접근하듯

 

포인터로 배열의 시작주소를 가리키게 하면 p[0], p[1] 와 같이 똑같이 인덱스로 접근해서 가리킨 그 배열처럼 똑같이 쓸 수 있다는것도 배웠습니다.

 

그런데 이것을 적용시킨건 1차원 배열뿐만이였죠?

 

그러면 2차원 배열도 포인터로 시작주소만 가리키고 p[0][0], p[1][0] 처럼 접근이 가능할까요?

한번 해보겠습니다.

 

#include <stdio.h>

int main(){
	int array[2][3] = {
	{10,20,30},
	{40,50,60}
	};
	
	int * ptr = array; //포인터가 2차원배열의 시작주소를 가리키게 한다. 
	
	printf("%d", array[1][2]);
	
	printf("%d", ptr[1][2]);
	
	return 0;
}

다음과 같이 코드를 작성하고 한번 실행해봅니다.

잘 될까요?

 

아쉽게도 원하는 결과로 컴파일 되지 않습니다. 

오류 메세지를 보시면 incompatible pointer type -> 호환성이 없는 포인터 타입

즉, 호환되지 않는다는 오류 메세지입니다.

 

2차원 배열의 경우에는 1차원 배열처럼 그냥 가리키기만 해서 포인터와 연결할 수 없습니다.

조금 특별한 방법이 필요한데요.

 

이 문제를 해결 하기 위해선 배열 포인터 변수를 사용하면 됩니다.

배열 포인터 변수란 배열을 가리키는 포인터 변수 입니다.

 

배열 포인터 변수를 이용한 2차원 배열 접근

 

int (*p)[열];

 

 

배열 포인터 변수는 위와 같이 선언합니다. 

 

배열 포인터 변수는 배열을 가리키는 포인터 변수라고 하였는데 1차원 배열에선 의미가 없고 2차원 이상의 배열에서만 의미를 가지게 됩니다.

 

배열 포인터 변수를 만든 목적이 애초에 2차원 이상의 배열을 가리켜서 사용하기 위해 만들어진 것이기 때문입니다.

 

int는 자료형, 포인터이므로 가리킬 곳의 자료형이 되겠고 *p는 포인터 변수 이름입니다.

뒤에 넣는 값이 중요한데 열의 값을 넣어주셔야 합니다.

 

왜 행이 아니라 열의 값을 넣냐고 하면 컴퓨터가 읽을때 행을 기준으로 들어가서 열을 각각 읽어내기 때문입니다.

이 부분은 크게 중요한 부분은 아니니 사용 용례 자체를 익히시고 넘어가는게 더 중요합니다.

 

심층적으로 이해하고 싶으신분들은 다음 링크를 참고해주세요

http://tcpschool.com/c/c_pointerArray_arrayPointer

 

코딩교육 티씨피스쿨

4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등

tcpschool.com

 

이것을 활용하여 아까 문제를 해결해보겠습니다.

 

#include <stdio.h>

int main(){
	int array[2][3] = {
	{10,20,30},
	{40,50,60}
	};
	
	int (*ptr)[3] = array; //포인터가 2차원배열의 시작주소를 가리키게 한다. 
	
	printf("%d\n", array[1][2]);
	
	printf("%d", ptr[1][2]);
	
	return 0;
}

60
60
--------------------------------
Process exited after 0.02347 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

짜잔. 오류없이 출력이 되었습니다.

이렇게 2차원 배열과 포인터를 연결하는건 함수의 매개변수로 2차원 배열을 받을때 유용하게 사용되니 꼭 기억해두세요.

 

포인터 배열

포인터 배열이란 주소들을 저장할 수 있는 배열을 의미합니다.

앞에서 배운 포인터 변수를 배열 형태로 만든것입니다.

 

#include <stdio.h>

int main(){
	int a = 1, b = 2, c = 3;
	int* ptr[3] = {NULL, NULL, NULL};
	ptr[0] = &a;
	ptr[1] = &b;
	ptr[2] = &c;
	
	printf("%d %d %d\n",a,b,c);
	
	*ptr[0] = *ptr[0] + 1;
	*ptr[1] = *ptr[1] + 1;
	*ptr[2] = *ptr[2] + 1;
	
	printf("%d %d %d",a,b,c);
	
	return 0;
}

1 2 3
2 3 4
--------------------------------
Process exited after 0.02271 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

컴퓨터가 int* ptr[3] 을 읽을때 연산자 우선순위에 따라 int* ptr 을 먼저 읽고 ptr은 int형 포인터 변수가 되고, 그 후 뒤에 [3] 을 읽어서 포인터 배열이 되게 됩니다.

 

그렇기 때문에 각 배열 항목이 int형 포인터 변수가 되게 되므로 주소를 가리킬 수 있게 됩니다.

 

 

 

기존의 경우 포인터 변수로는 주소값 하나만을 가리킬 수 있었지만 포인터 배열로 주소를 여러개 담음으로써 여러개를 포인팅하고 한꺼번에 그 값을 참조해서 바꿀 수 있게 되었습니다.

 

다음 예제는 a,b,c에 포인터로 각각 접근해서 값에 1을 더해서 저장하는 예시인데요.

 

*ptr[0] = *ptr[0] + 1; 이 부분만 설명을 드리자면 우선 대입연산자의 경우 오른쪽에서 왼쪽으로 연산이 시행됩니다.

*ptr[0]을 하면 ptr[0]이 가리키는 주소의 값이 되는데 ptr[0]이 a의 주소를 가리키고 있으므로 *ptr[0]은 a의 값, 즉 1이 되게 됩니다 그러므로 1+1을 *ptr[0]에 대입하는 셈이 되는데 

 

*ptr[0]은 방금전에 말했듯이 a의 값이라고 했으니깐 1인데 거기에 2를 덮어씌운다는 의미가 됩니다.

그러므로 정리하면 a의 값 1에 2를 덮어씌우므로 a의 값은 2가 됩니다.

 

b,c의 경우엔 a와 마찬가지이므로 설명은 생략하겠습니다.

 

어렵죠? 많이 하면 익숙해질겁니다.

 


배열 포인터 변수와 포인터 배열은 엄연히 다릅니다.

이름은 비슷하지만 제대로 읽으셨다면 애초에 용례 자체가 완전히 다른 녀석들입니다.

 

배열 포인터 변수는 2차원 배열의 주소를 포인팅 할때 쓰고, 포인터 배열은 주소를 여러개 저장할 수 있는 배열입니다.

 

 

문자열 다루기

문자, 문자배열, 문자열의 차이에 대해 알아보겠습니다.

 

우선 문자는 문자 딱 한 글자를 의미합니다

ex) A, B, C, D

보통 C언어에서는 문자를 나타내기 위한 자료형인 char로 선언을 많이 합니다

char c = 'A';

 

처럼요. C언어에서 문자 한글자(=문자)를 표현할땐 반드시 작은따옴표를 씁니다.

 

문자 배열은 문자 여러개로 이루어진 배열입니다.

 

char이 한글자였으므로 char[]과 같은 배열 형태로 만들면 그것이 문자 배열이 됩니다.

보통 다음과 같이 선언합니다

char a[2] = {'a','b'};

 

 

문자열은 문자 여러개로 이루어진 배열입니다.

이렇게 설명만 들어선 문자 배열과 똑같아 보이지만 결정적인 차이가 있는데 끝에 널문자가 있습니다.

(문자배열은 끝에 널문자가 없습니다.)

 

보통 큰따옴표를 써서 다음과 같이 선언합니다

char a[] = "ab";

 

오늘 핵심은 문자, 문자 배열도 아닌 문자열을 아는것입니다.

문자열에 주목하시길 바랍니다.

 

문자열과 널문자 / 문자열 vs 문자비교

지금까지 char자료형 자체가 ASCII 코드를 활용하여 문자 표현을 위해 만들어진 자료형이란 것은 배웠지만

문자열을 다루는것에 대해선 자세히 다루지 않았습니다.

 

문자열은 위에서 말했듯이 문자 여러개로 이루어진 배열인데 끝에 널문자가 있다는것이라고 했습니다.

 

널문자는 무엇일까요?

 

만약에 "Hello World!" 라는 문자열을 컴퓨터가 출력한다고 칩시다.

우리가 보기엔 그냥 "Hello World!", 느낌표 에서 끝나지만 실제로 느낌표 뒤에 널문자가 하나 더 붙어있습니다.

 

물론 출력할때나 선언할때 우리눈엔 보이지 않습니다.

문자열 선언만 제대로 했다면 널문자가 알아서 붙어서 실행됩니다.

 

널문자의 역할은 문자열의 끝을 알리는 겁니다.

 

컴퓨터가 "Hello World!" 라는 문자를 읽을때 "Hello World!(널문자)" 라는 형태로 이해를 하고

널문자를 끝으로 생각하고 널문자까지만 문자열을 읽습니다.

 

이것이 문자열을 다룰때 핵심적인 내용입니다.

 

#include <stdio.h>

int main(){
	char string[] = "A";
	char c = 'A';
	return 0;
}

 

우선 위에도 짧막하게 설명했듯이 문자열을 선언할때는 char의 배열형(char[])로 선언해주면 됩니다. 그리고 넣을값을 쌍따옴표에 넣습니다.

==> char string[] = "A";

 

그럼 두 선언의 차이는 무엇일까요? "A"'A'를 비교해봅시다.

 

우선 C언어에서는 문자열은 큰따옴표로, 문자는 작은따옴표로 표현한다고 했으니 따옴표의 차이가 있겠네요.

그거 이외에는 A문자를 둘다 출력하는거라 차이가 없어보이지만 "A" 로 선언을 하면 맨 끝에 자동으로 널문자가 들어가서  사실 "A" 는 "A(널문자)" 의 형태로 저장이 되어있습니다.

 

한번 이것을 확인해볼까요??

 

#include <stdio.h>

int main(){
	char string[] = "A";
	
	printf("%d %d", string[0], string[1]); 
	return 0;
}

65 0
--------------------------------
Process exited after 0.05522 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

char 각 요소의 ASCII 코드 확인을 위해 %d 서식문자로 출력을 해보았습니다.

 

string[0] 번째에서는 A가 저장되어있고 string[1] 번째엔 널문자가 저장되어있다고 추정하고 ASCII 코드 값을 보니

대문자 A의 ASCII 코드값인 65가 출력되었고 그 뒤엔 0이 출력되었습니다.

 

ASCII 코드 0번째 = 바로 널문자입니다.

소문자 'a' 의 경우 ascii 코드 값이 97이고, 'a'를 출력해보면 실제 정수 97과 똑같듯이, 널문자는 결국 정수 0과 똑같습니다.

 

그래서 "A"는 보기엔 한가지 문자로 보이지만 끝에 널문자까지 해서 문자 2개가 들어있습니다.

 

 

https://dojang.io/mod/page/view.php?id=740

ASCII 코드값 표를 보시면 더 이해가 쉽습니다.

 

C언어에서 프로그래밍을 할때 널문자를 직접 표현하고 싶으시면 '\0' 으로 표현을 합니다. (아까도 말했듯이 '\0' == 0 이지만, 명시적으로 표현하기 위해선 '\0' 라는 문자를 사용합니다. 사실 char 배열을 0으로 전부 초기화 하는행위가 널문자로 가득 채우는 행위와 같습니다.)

문자 이므로 문자기때문에 반드시 작은따옴표로 표시하셔야 하구요. \, 0 이라 문자가 2개라 문자열이 아닌가 싶겠지만 일종의 이스케이핑 문자 (\n과 같이) 처럼 한 세트로 생각하시면 되겠습니다.

 

작은따옴표에서 실수가 많이 나오니 "\0" 으로 쓰지 않게 주의하시길 바랍니다.

작성자의 경우에도 끝에 널문자를 검사하려고 큰따옴표로 쓰는 실수를 했었는데 프로그램 작동을 하지 않아 몇십분째 고민한 기억이 있습니다.

 

 

#include <stdio.h>

int main(){
	char string[] = "A";
	char c = 'A';
	
	printf("%c\n", c);
	printf("%s", string); 
	return 0;
}

A
A
--------------------------------
Process exited after 0.05455 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

문자열을 출력할땐 %s 서식문자를 이용합니다. %s로 문자열을 출력을 시도하면 컴퓨터가 알아서 널문자까지 문자열을 읽어서 출력합니다.

 

보면 위의 char로 A한글자와 문자열 A는 출력결과는 같지만 내부적으로 널문자가 들어간다는것을 꼭 이해하셔야 합니다.

 

문자열 vs 문자배열 비교

#include <stdio.h>

int main(){
	char string[] = "abcd";
	char chars[4] = {'a','b', 'c', 'd'};

	printf("%s\n", string); 
	
	for (int i = 0; i < 4; i++){
		printf("%c", chars[i]);
	}
	
	
	return 0;
}

abcd
abcd
--------------------------------
Process exited after 0.05492 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

이번엔 문자열과 문자배열을 비교해봅시다.

 

string은 문자열이고 chars는 문자배열입니다.

이것도 출력결과는 일단 같습니다.

 

그런데 문자배열엔 보시면 알겠지만 끝에 널문자가 없습니다.

널문자의 역할은 문자열의 끝을 알리는건데 만약에 chars를 문자열 출력 형식인 %s로 출력해버리면 어떻게 될까요?

 

 

#include <stdio.h>

int main() {
	char chars[5] = { 'H','e', 'l', 'l', 'o' };
	printf("%s\n", chars);


	return 0;
}

Hello儆儆儆?낋戾?

(프로세스 30272개)이(가) 종료되었습니다(코드: 0개).
디버깅이 중지될 때 콘솔을 자동으로 닫으려면 [도구] -> [옵션] -> [디버깅] > [디버깅이 중지되면 자동으로 콘솔 닫기]를 사용하도록 설정합니다.
이 창을 닫으려면 아무 키나 누르세요...

보시면 출력값이 Hello 까진 제대로 출력이 되나 뒤부턴 깨져버립니다.

 

(Dev C++ 같은걸로 출력을 하면 가끔 뒤에 깨진 문자가 공백같은걸로 나와서 제대로 출력하는것처럼 보이는 경우가 있는데 이때는 코드블럭이나 Visual Studio 같은걸로 컴파일을 해보고 실행하면 확실하게 알 수 있습니다.)

 

chars는 문자열이 아닌 문자배열이기 때문에 끝에 널문자가 없어서 끝을 모르니 뒤가 다 깨져버립니다.

그러면 chars 라는 문자배열 끝에 널문자를 넣어주면 문자열이 될까요?

 

한번 해봅시다.

 

#include <stdio.h>

int main(){
	char chars[6] = {'H','e', 'l', 'l', 'o', '\0'};
	printf("%s\n", chars); 
	
	
	return 0;
}

Hello

--------------------------------
Process exited after 0.0998 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

우와. 깨지지 않고 실행이 되네요! 

chars 라는 문자배열 끝에 널문자가 삽입되었으니

이제 chars 는 문자열이 되었습니다.

 

갑자기 이게 왜 문자열이 되냐고 갸우뚱 하시는 분이 있을건데 어떻게 됬던 

끝에 널문자만 제대로 삽입되면 그건 문자열입니다.

 

애초에 문자열과 문자배열을 구분짓던 핵심이 끝의 널문자였으니깐요..

 

#include <stdio.h>

int main(){
	char chars[6] = {'H','e', 'l', 'l', 'o', '\0'};
	char string[] = "Hello";
	
	
	return 0;
}

그래서 결국 위처럼 선언하면 두개는 똑같은 형태입니다.

 

근데 저렇게 한글자씩 하는거보다 그냥 쌍따옴표로 "Hello" 라고 적는게 훨씬편하고 널문자도 알아서 붙여주니 

char string[] = "Hello"; 의 형태로 문자열 선언하는것을 익혀두시길 바랍니다.

 

char string[] = "Hello"; 로 선언을 하면 내부적으론 {'H','e', 'l', 'l', 'o', '\0'}; 가 알아서 들어가게 됩니다.

 

그리고 지금까지 문자열을 배열에 담을때 따로 배열요소의 크기를 정해주지 않았습니다.

이렇게 선언하는것이 편한 이유는 "Hello" 라고 5글자로 담아도 배열 끝에 널문자가 하나 추가되야되서 배열 요소 개수는 결론적으로 6개 입니다.

 

그런데 아래와 같은 실수를 했다면?

 

#include <stdio.h>

int main(){
	char string[5] = "Hello";
	printf("%s", string);
	
	return 0;
}

Hello儆儆儆謙R뜽?

프로그래머가 실수를 범하여 끝의 널문자를 고려하지 않고 Hello 가 단순히 5글자인거만 생각해서 배열의 크기를 5개로 잡았습니다.

 

컴파일 결과 끝에 널문자 들어갈 자리가 없어서 문자의 끝을 읽지 못하고 글자가 깨져버린 경우입니다.

 

그래서 저런 실수를 안하려면 애초에 배열의 크기를 비워두는게 낫습니다.

1차원 배열의 경우 배열 요소만 입력해주면 그 크기에 맞춰서 알아서 크기가 할당을 해주잖아요?

 

문자열의 경우에도 "Hello" 라고 적어주면 컴퓨터가 알아서 널문자까지 고려를 해서, 널문자 삽입후 배열크기가 6개로 자동으로 할당됩니다.

 

그러므로 배열 크기에 제한이 있지 않는 이상 그냥 배열 크기를 비워두는게 정신건강에 이롭습니다. 편하기도 하구요.

 

문자열 포인터

지금까진 char[] 형태로 문자열을 표현하는것을 배웠지만 사실 문자열을 저장하는 방식엔 한가지가 더있는데요

char * 형태로 표현하는 문자열 포인터입니다!

 

그래서 결론적으로 문자열을 표현하는 방법은

1. 배열을 이용한 방법

2. 포인터를 이용한 방법

두가지가 있습니다.

 

포인터를 이용하는 방법을 알아보겠습니다.

 

#include <stdio.h>

int main(){
	char * ptr_string = "Hello!";
	printf("%s", ptr_string);
	return 0;
}

선언을 할때는 간단한데 char * 포인터 이름 = "저장할 문자열"; 로 선언해주시면 됩니다.

%s로 출력도 잘되고 끝에 널문자도 알아서 붙습니다.

 

#include <stdio.h>

int main(){
	char * ptr_string = "Hello!";
	char array_string[] = "Hello!";
	
	printf("%s\n", ptr_string);
	printf("%s", array_string)
	return 0;
}

그러면 다시 돌아와서 아까 배열로 선언한 문자열과, 포인터로 선언한 문자열의 차이는 무엇일까요?

 

결정적인 차이는 배열로 선언한 문자열의 경우 배열 내부의 한문자, 한문자를 각각 바꿔낼 수 있지만

포인터로 선언한 문자열의 경우에는 한번 선언하면 수정이 불가합니다.

 

둘의 원리 차이를 보자면

배열로 선언한 문자열의 경우에는 배열 요소에 문자 하나하나씩을 저장하는 형태지만

 

포인터로 선언한 문자열은 "Hello!" 라는 문자열이 메모리 내부에 자동 할당이 되고 포인터가 이 문자열이 저장된 주소를 받아와서 가리키는 형식입니다.

 

그래서 array_string[]의 경우에는 배열 내부에 문자들이 각각 저장되어있겠지만

ptr_string의 경우에 출력해보면 "Hello!"라는 문자열이 할당된 곳의 주소 값을 저장하고 있습니다.

 

#include <stdio.h>

int main(){
	char * ptr_string = "Hello!";
	char array_string[] = "Hello!";
	
	array_string[5] = '?';
	ptr_strings[5] = '?'; //--- 오류 발생 

	printf("%s\n", ptr_string);
	printf("%s", array_string);
	return 0;
}

이 코드를 보시면 배열로 선언된 문자열의 경우 요소 하나에 접근해서 문자를 바꿔낼 수 있지만 포인터로 선언된 문자열의 경우 바뀌지 않고 오류가 발생하는걸 알 수 있습니다.

 

그러면 문자열 포인터를 한번 만들면 아예 값 자체 바꾸는게 안되냐 하면 그건 아닙니다.

대신에 통째로 바꿔내야 합니다.

 

#include <stdio.h>

int main(){
	char * ptr_string = "Hello!";
	char array_string[] = "Hello!";
	
	ptr_string = "Hello?";

	printf("%s\n", ptr_string);
	printf("%s", array_string);
	return 0;
}

보시면 이렇게 쌍따옴표로 값을 통째로 초기화 시켜줘야 합니다.

저렇게 하면 처음에는 "Hello!"라는 문자열이 메모리 공간에 생성되고 그 주소값을 가져와서 가리키고 있다가

 

ptr_string = "Hello?"; 이 부분이 실행되면 다시 "Hello?" 에 대한 공간을 할당하고 그 주소를 가리키는 셈입니다.

 

 

#include <stdio.h>

int main(){
	char * ptr_string = "Hello!";
	char array_string[] = "Hello!";
	
	array_string = "Hello?"; //오류 발생

	printf("%s\n", ptr_string);
	printf("%s", array_string);
	return 0;
}

배열로 선언한 문자열의 경우 이렇게 다시 문자열 전체를 통째로 갈아내는것이 허용되지 않습니다.

선언을 할때 이미 배열의 크기가 이미 고정되어 버렸기 때문입니다.

 

#include <stdio.h>

int main(){
	char * string = "Hello";
	scanf("%s", string); //실행 에러 
	printf("%s", string);
	
}

그리고 포인터로 선언한 문자열을 사용할때 주의해야 할점은 가리키는 "Hello" 라는 메모리 공간은 읽기만 할 수 있고 쓰기를 할 수 없습니다. 즉 읽기 전용입니다.

그래서 scanf() 함수 같은것으로 입력받아 값을 쓸 수 없습니다.

 

즉 문자열 포인터로는 쓰기에 관련된 함수를 사용할 수 없습니다.

위에서 문자열을 통째로 갈아내는걸 보고 쓰기를 했다고 착각할 수 있겠지만 저건 쓰기를 한게 아니라 그냥 메모리 공간을 새로 만들고 그것을 포인터로 가리켜서 읽는겁니다.

 

새로만들어진 공간 역시 읽기 전용이구요.

 

그래서 정리하자면

 

포인터로 선언한 문자열 vs 배열로 선언한 문자열

<공통점>

둘다 문자열 선언의 방식, %s로 출력 가능함.

<차이점>

포인터로 선언한 문자열 : 배열 처럼 글자 하나하나를 바꿔내는건 안됨, 통째로 갈아내는건 가능.

배열로 선언한 문자열 : 글자 하나 하나를 배열요소에 들어가서 바꿔낼 수 있음, 통째로 갈아내는건 불가능

 

로 정리해볼 수 있습니다.

 

어떨땐 포인터로 선언한 문자열을 쓰고 배열로 선언한 문자열을 쓰냐? 하면 위의 차이점에 주목해서 선언할때 잘 생각해주셔야 합니다.

 

애초에 문자열 하나에 대한 선언 방식이 두가지인것이 제 개인적인 의견으론 별로 마음에 들지 않습니다.

Modern 하지 않달까요.. 

 

문자열 배열

"Hello", "I am", "File", "nice to meet you"

이라는 4가지 문자열을 배열에 넣어보려고 합니다.

 

어떤 방법이 좋을까요?

 

우선 문자열 표현 방식중에 하나인 배열로 선언한 문자열로 나타낸다고 하면

문자열이 총 4개, char형 1차원 배열 4개로 표현이 될 것인데 1차원 배열 4개를 묶어서 정리해야 하니 차원이 하나 더 필요하게 됩니다.

 

즉 2차원 배열을 만들어야 문자열 배열을 만들 수 있습니다.

예제로 한번 보겠습니다.

 

#include <stdio.h>

int main(){
	char strings[4][20] =
	{
		{"Hello"},{"I am"}, {"File"}, {"nice to meet you"}
	};
	
	for (int i = 0; i < 4; i++){
		printf("%s ", strings[i]);
	}
	return 0;
}

 

위 예제의 2차원 배열 strings의 상태를 그림으로 나타내면 아래와 같게 됩니다.

 

 

2차원 배열 strings의 경우 4행 20열로 선언을 하였습니다.

각 0행, 1행, 2행 , 3행에 차례대로 문자열이 들어가는 형태입니다.

 

문자열이므로 끝에 당연히 널문자가 붙습니다.

 

그리고 출력을 할때

	for (int i = 0; i < 4; i++){
		printf("%s ", strings[i]);
	}

위와 같은 형태로 출력하였는데요, %s로 출력할때 strings[0] 로 출력하게 되면

strings[0]이 Hello의 위치에 있는 행이므로 열을 따로 제공해주지 않으면

여기에 있는 Hello 라는 문자열로 찾아가 알아서 출력해줍니다.

 

그럼 위 예제를 보면서 한가지 의문이 생길건데요. 저기 파란색 부분은 비어있는데 그럼 저 부분은 메모리 공간이 그냥 낭비되냐? 라고 물을 수 있습니다.

 

네 맞습니다 2차원 배열로 선언을 하게 되면 저렇게 문자열 배열 요소 중에서 가장 큰 문자열의 크기 이상으로 배열

크기를 잡아줘야 하고 (다른 문자나 널문자가 짤릴 수 있기 때문에) 또 너무 크게 잡아주면 메모리 공간이 낭비되게 됩니다.

 

이것을 해결할 순 없을까요? 이 문제는 아까전에 포인터를 이용한 문자열의 표현으로 해결 해볼 수 있습니다.

아래의 예제를 봐주세요.

 

#include <stdio.h>

int main(){
	char * strings[4] = {"Hello", "I am", "File", "nice to meet you"};
	
	for (int i = 0; i < 4; i++){
		printf("%s ", strings[i]);
	}
	
	return 0;
}

Hello I am File nice to meet you
--------------------------------
Process exited after 0.0245 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

 

문자열 4개, 1차원 배열 4개로 훨씬 직관적이게 변했고 코드도 훨씬 간결해졌죠?

 

그림으로 나타내면 아래와 같습니다.

 

char * 형태의 배열을 선언하였습니다. strings는 포인터 배열이고 각 요소가 포인터 변수입니다.

포인터 변수는 주소를 가리키는 변수라고 했습니다.

 

char * strings[4] = {"Hello", "I am", "File", "nice to meet you"};

이 코드가 실행되면 Hello, I am, File, nice to meet you 라는 문자열이 각각 메모리 공간에 생성되고

그 주소값을 포인터 배열의 각 요소가 가리키게 됩니다.

 

그리고 출력할때는 저장된 주소값을 따라가서 거기에 있는 값을 읽어서 출력합니다. (%s)

이것은 기본적으로 포인터에 의한 문자열의 구현이므로 아까전에 포인터를 이용한 문자열의 특성

그대로 각 요소가 수정이 안됩니다.

 

각 배열 요소가 그 문자열 값을 직접 저장하는게 아니라 가리키는 주소를 저장하고 있기 때문에

아까전의 2차원 배열을 이용한 구현때처럼 메모리가 낭비될 일이 없습니다.

 

 

#include <stdio.h>

int main(){
	char * strings[4] = {"Hello", "I am", "File", "nice to meet you"};
	
	for (int i = 0; i < 4; i++){
		printf("%p\n", strings[i]);
	}
	
	return 0;
}

0000000000404000
0000000000404006
000000000040400B
0000000000404010

--------------------------------
Process exited after 0.03186 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

%p로 각 배열 요소의 값을 출력해보면 어느 주소를 가리키고 있는지 알 수 있습니다.

 

0000000000404000 -> Hello
0000000000404006 -> I am
000000000040400B -> File
0000000000404010 -> nice to meet you

 

포인터 변수의 상수화

포인터 변수는 주소를 저장하는 변수로 여러가지 주소를 저장하고, 그 주소에 간접적으로 접근해서 값을 변경하는 행동을 수행할 수 있습니다.

 

예전에 const 라는 상수 선언에 대해 배운적이 있습니다. 변수 선언을 하고 맨앞에 const를 띡 하고 붙여주면 그 변수는 수정이 안되는 상수 상태가 됩니다. (수학적으로 값이 안변하는 PI값 같은 것 선언에 유용했습니다.)

 

그런데 포인터 변수에 const 키워드를 붙여주면 포인터 변수를 상수화 할 수 있습니다.

 

포인터 변수가 상수화 되면 아래와 같은 의미를 가지게 됩니다.

 

1. 포인터 변수에 다른 메모리 공간의 주소를 저장하지 못하게 한다.

2. 포인터 변수를 통해 메모리 주소에 해당하는 값을 변경하지 못하게 한다.

3. 1,2 둘다 못하게 한다.

 

위 3가지 case는 const 키워드를 포인터 변수 어디에 붙여주냐에 있습니다.

 

그 중에서 저희는 2번만을 보겠습니다.

#include <stdio.h>

int main(){
	char a = 'A';
	char b = 'B';
	
	const char* p = &a;
	*p = 'C'; //오류 발생
		
	return 0;
}

포인터 변수에 const 기호를 맨 앞에 써주면 그 포인터 변수를 이용해서 값을 변경하는 것을 막을 수 있습니다.

위 예제의 경우 p라는 포인터 변수로 a의 주소를 가리켰고  *p = 'C'로 a의 값을 변경하려고 시도했습니다.

 

하지만 상수화를 하였기 때문에 포인터 변수로 값을 변경하는건 허용되지 않습니다.

 


오늘은 내용이 많았습니다. 조금 정신없이 작성해서 조금 오류가 있을 수 있습니다만 큰 오류가 있을시 댓글로 정정 부탁드리겠습니다! 

 

다음장에서 계속 됩니다..

 

 

 

 

SNS 공유하기
💬 댓글 개
이모티콘창 닫기
울음
안녕
감사해요
당황
피폐

이모티콘을 클릭하면 댓글창에 입력됩니다.