프로그래밍 언어/C언어

C언어 13. 문자열

닉네임못짓는사람 2020. 7. 16. 18:32
반응형

이번 글에선 문자열에 대해서 알아보겠습니다.

지금까지 저희는 printf를 사용해 문자열을 많이 출력해보았습니다.

하지만 반대로 문자열을 입력받고 저장한 적은 없습니다.

그렇다면 이 작업을 어떻게 해야 할까요?

문자열은 어떻게 저장하지?


그러려면 먼저 문자열에 대해 정확히 알고 넘어가야 할 것이 있습니다.

자료형에 대해 설명했을 때도 보셨겠지만, C언어에는 하나의 문자를 저장하는 char형은 있지만

문자열을 저장하는 자료형은 따로 존재하지 않습니다.

눈치채신 분들도 있을지 모르겠는데, 문자열은 char형의 배열에 저장하면 됩니다.

 

예를 들어 "seoul"이란 문자열은 크기가 6인 char형의 배열의 각 요소에 seoul을 넣은 것입니다.

그런데 왜 5글자인데 배열의 크기가 6이어야 하는 걸까요?

그건 바로 문자열의 끝에는 그 끝을 알려주는 NULL문자 '\0'가 입력되기 때문입니다.

따라서 배열의 크기는 최소 (글자의 개수+1)이 되어야 합니다.

코드로 한번 확인해보겠습니다.

 

#include<stdio.h>

int main() {
	char ch[6] = { 's','e','e','u','l' };
	printf("for문을 사용해 출력\n");
	for (int i = 0; i < 6; i++) {
		printf("%c", ch[i]);
		if (ch[i] == NULL) {
			printf("\ni : %d일 때 NULL값", i);
			break;
		}
	}
	printf("\n%s\n", ch);
	return 0;
}

먼저 printf를 통해 문자열을 출력하기 위해선 %s를 사용하며 값으론 char배열의 배열명이 사용됩니다.

이는 포인터를 통해 배열의 첫 번째 요소의 주소부터 NULL문자가 나올 때까지 모든 값을 읽어옵니다.

코드를 보면 ch배열의 크기를 6으로 선언했는데, 만약 5로 선언하면 %s로 출력 시

seoul뒤에 이상한 값이 같이 출력되는 것을 확인할 수 있습니다.

s는 문자열을 의미하는 string의 약자입니다.

이번엔 코드를 이렇게 바꿔보겠습니다.

#include<stdio.h>

int main() {
	char ch[6] = { 's','e',NULL,'u','l' };
	printf("for문을 사용해 출력\n");
	for (int i = 0; i < 6; i++) {
		printf("%c", ch[i]);
		if (ch[i] == NULL) {
			printf("\ni : %d일 때 NULL값", i);
			break;
		}
	}
	printf("\n%s\n", ch);
	return 0;
}

위와 같이 배열의 중간에 NULL을 입력할 경우 %s로 출력 시에

이 NULL을 문자열의 끝으로 인식하고 이전에 있는 se만 출력하게 됩니다.

 

C언어 내에서 문자열은 배열이기 때문에 문자열에 필요한 대부분의 작업은

배열 요소를 일일이 참조하는 방법을 사용합니다.

대표적인 작업으론 문자열 복사, 길이 계산, 문자열 비교, 두 문자열 합치기 등등이 있을 것입니다.

 

위와 같이 문자열이 문자의 배열이라면 포인터를 통해서 모든 값을 참조할 수 있을 것입니다.

그래서 컴파일러는 프로그램에서 사용된 모든 문자열을 첫 번째 문자를 가리키는 포인터로 변환하고,

이 포인터를 통해 문자열을 사용할 수 있도록 합니다.

따라서 문자열 상수는 컴파일 후에는 char형 기억공간을 가리키는 포인터 가됩니다.

코드를 통해서 이를 확인해보도록 합시다.

#include<stdio.h>

int main() {
	printf("문자열의 첫번째 주소값 : %u\n", "korea");
	printf("첫번째문자 : %c\n", *"korea");
	printf("세번째문자 : %c\n", "korea"[2]);
    	return 0;
}

이처럼 문자열 상수를 참조할 시 문자열의 첫 번째 문자가 출력됨을 확인함으로써,

문자열 상수가 그 문자열의 첫 번째 문자의 포인터라는 것을 알 수 있습니다.

또한 이 문자열 상수는 배열명과 동일하기 때문에 배열과 같은 형태로 값을 출력할 수도 있습니다.

 

이 값을 위와 같이 빈번하게 사용해야 한다면 포인터 변수에 저장해서 사용할 수도 있을 것입니다.

#include<stdio.h>

int main() {
	char* ch = "korea";
	printf("문자열의 첫번째 주소값 : %u\n", ch);
	printf("첫번째문자 : %c\n", *ch);
	printf("세번째문자 : %c\n", ch[2]);
    	return 0;
}

이처럼 포인터 변수에 문자열 상수를 저장해도 위와 완전히 동일한 결과를 얻을 수 있습니다.

주의해야 할 점은 문자열 상수는 어디까지나 상수이기 때문에 이후에 값을 절대 바꿀 수 없다는 점입니다.

문자열의 값은 바꿀 수 없지만 포인터 변수를 사용하면 문자열의 시작을 임의로 변경할 수 있기 때문에,

문자열을 부분적으로 사용하는 등의 작업은 가능합니다.

#include<stdio.h>

int main() {
	char* ch = "korea";
	ch = ch + 2;
	printf("문자열의 첫번째 주소값 : %u\n", ch);
	printf("첫번째문자 : %c\n", *ch);
	printf("세번째문자 : %c\n", ch[2]);
	printf("문자열 전체 : %s\n", ch);
    	return 0;
}

위와 같이 말이죠.

문자열 상수의 값은 변경할 수 없지만, 문자열을 char형 배열로 선언하면 값을 얼마든지 바꿀 수 있습니다.

#include<stdio.h>

int main() {
	char str[] = "seoul";
	printf("%s\n", str);
	str[3] = 'o';
	printf("%s\n", str);
	printf("문자열 길이 : %d", sizeof(str));
    	return 0;
}

또한 char형 배열에 값을 입력할 때는 위와 같이 ""로 묶어서 입력할 수 있으며,

크기를 정하지 않으면 알아서 문자열의 길이 + 1만큼 크기를 결정합니다.

크기를 정할 땐 꼭 문자열의 길이와 맞추지 않아도 상관없습니다.

 

근데 위에서 문자열 상수는 포인터라고 했었는데,

이러면 배열에 포인터를 입력하는 형태가 아닌가 할 수 있습니다.

하지만 실제로는 포인터를 입력하는 것이 아니라 컴파일러가 이 문자열 포인터의 위치로 가서,

문자들을 하나씩 읽어와 배열에 저장해주는 작업을 수행합니다.

확인해보면 문자열 상수와 배열명의 주소값이 서로 다른 것을 알 수 있습니다.

 

또한 정수형 배열을 초기화할 때와 마찬가지로 최초 선언 시에만 위와 같은 방법이 가능하고,

이후에는 위처럼 값을 변경할 수 없습니다.

#include<stdio.h>

int main() {
	char str[20];
	str = "seoul";		//에러발생
}

이는 상수인 배열명str에 상수인 문자열 상수를 대입하는 형태이기 때문입니다.

 

그래서 선언 이후에 ""를통해서 값을 변경하고 싶을 때에는 함수를 만들어서 사용해야 합니다.

#include<stdio.h>

void changeStr(char *p_Str, char *n_Str, int size);

int main() {
	char str[20] = "hello";
	int str_Size = sizeof(str);
	printf("이전 문자열 : %s\n", str);
	changeStr(str, "Hello World!", 20);
	printf("이후 문자열 : %s\n", str);
	return 0;
}

void changeStr(char* p_Str, char* n_Str, int size) {
	int i = 0;
	if (sizeof(n_Str) > 20) {
		printf("입력받은 문자가 배열의 크기보다 큽니다.");
		return 0;
	}
	while (*(n_Str + i) != NULL) {
		*(p_Str + i) = *(n_Str + i);
		i++;
	}
	*(p_Str + i) = NULL;
}

이처럼 함수를 새로 정의해서 최초 선언 이후에도 문자열 상수를 사용해 값을 변경할 수 있습니다.

문자열 관련 함수들


다음으로 자주 사용되는 문자열 관련 함수에 대해서 알아보도록 하겠습니다.

종류로는 문자열 복사, 문자열 길이 계산, 문자열 비교, 두 문자열 합치기 등등이 있습니다.

또한 문자열 관련 함수들을 사용하기 위해선 #include <string.h> 헤더를 선언해주셔야 합니다.

 

먼저 문자열 복사에 대해 알아보도록 하겠습니다.

문자열을 복사하는 함수는 strcpy(string copy)이며 함수의 원형은 다음과 같습니다.

char* strcpy(char*, char*);

매개변수는 포인터 변수 두 개를 사용하며, 반환 타입이 char형 포인터 변수입니다.

반환값에 대해선 지금은 필요가 없으니 다음에 자세히 설명하도록 하겠습니다.

실제 코드를 통해 사용법을 알아보겠습니다.

#include<stdio.h>
#include<string.h>

int main() {
	char str1[20] = "C programming";
	char str2[20] = "Hello World!";
	char tmp[20];
	printf("str1 : %s\n", str1);
	printf("str2 : %s\n", str2);

	//strcpy를 사용해 두 문자열의 값을 교환
	strcpy(tmp, str1);				//str1에 있는 문자열을 tmp에 복사
	strcpy(str1, str2);				
	strcpy(str2, tmp);

	printf("\nstr1 : %s\n", str1);
	printf("str2 : %s\n", str2);
	printf("tmp : %s\n", tmp);
    	return 0;
}

이처럼 뒤에 있는 인자의 문자열을 앞에 있는 인자의 문자열에 복사하는 함수입니다.

이를 사용해 두 문자 배열의 값을 변경하도록 코드를 작성해보았습니다.

 

다음은 문자열 길이를 계산하는 함수에 대해서 알아보도록 합시다.

이 함수명은 strlen(string length)이며 함수 원형은 다음과 같습니다.

unsigned int strlen(char*);

위의 함수 같은 경우엔 특별히 설명할 필요 없이, 인자로 보낸 문자열의 길이를 반환해주는 함수입니다.

주의할 점은 반환하는 문자열 길이는 문자열 끝의 NULL의 제외한 길이라는 점입니다.

또한 배열의 크기와 상관없이 배열 안의 문자들의 개수만을 계산해서 반환합니다.

 

다음은 문자열을 비교하는 함수입니다.

이 함수는 strcmp(string compare)이며 두 문자열을 비교하는데,

주의할 점은 문자열의 길이를 비교하는 것이 아니라 사전적 순서를 비교하는 것입니다.

함수 원형은 다음과 같습니다.

int strcmp(char* str1, char* str2);

반환 타입은 int인데 반환값에 따라 순서를 알려줍니다.

반환값이 1이면 사전 순서상으로 str2가 먼저고, -1이면 str1이 먼저, 0이면 두 문자열이 같음을 의미합니다.

 

코드를 통해 활용해보도록 합시다.

#include<stdio.h>
#include<string.h>

int main() {
	int i = 0, j = 0;
	char str[5][20] = { "banana", "melon", "apple", "cherry", "lemon" };
	char tmp[20];
	int str_Size = sizeof(str) / sizeof(str[0]);

	for (i = 0; i < str_Size; i++) {
		printf("%s\n", str[i]);
	}
	
	for (i = 0; i < str_Size-1; i++) {
		for (j = i + 1; j < str_Size; j++) {
			if (strcmp(str[i], str[j]) == 1) {
				//strcpy를 사용해 두 문자열의 값을 교환
				strcpy(tmp, str[i]);
				strcpy(str[i], str[j]);
				strcpy(str[j], tmp);
			}
		}
	}

	printf("\n\n", str[i]);
	
	for (i = 0; i < str_Size; i++) {
		printf("%s\n", str[i]);
	}
    	return 0;
}

위의 코드는 char형 2차원 배열에 입력한 5개의 문자열을

사전 순서에 맞춰서 재 정렬하는 코드입니다.

char형 2차원 배열에선 하나의 문자열(1차원 배열)이 2차원 배열의 요소가 됩니다.

 

마지막으로 두 문자열을 합치는 함수에 대해서 알아보도록 하겠습니다.

두 개의 문자열을 합칠 때에는 "str1" + "str2"같은 형식은 불가능하고 이런 함수를 사용해야 합니다.

이 함수는 strcat(string concatenation)이며 함수 원형은 다음과 같습니다.

char* strcat(char* str1, char* str2);

str1문자열의 뒤에 str2문자열을 붙여서 더 긴 문자열을 만드는 것이 이 함수의 기능입니다.

합친 문자열은 str1에 저장되기 때문에, 사용 시엔 str1의 크기를 충분히 크게 초기화해주셔야 합니다.

 

예를 들어 str1이 "hello"이고 str2가 "world"일 때 strcat(str1, str2)를 실행하면,

str1 = "helloworld"가 되는 것입니다.

코드를 통해서 직접 확인해보도록 합시다.

#include<stdio.h>
#include<string.h>

int main() {
	char str1[40] = "C programming";
	char str2[20] = "Hello World!";
	printf("%s\n", str1);
	printf("%s\n", str2);
	strcat(str1, str2);
	printf("strcat함수 실행\n");
	printf("%s\n", str1);
	printf("%s\n", str2);

	return 0;
}

이처럼 str1이 끝나는 부분부터 str2의 내용을 입력하는 함수입니다.

이 함수들은 배열과 포인터를 사용해 직접 만들수도 있으니 관심이 있으면 한 번 만들어보시길 권합니다.

문자열을 입출력해보자


다음은 문자열을 입출력하는 함수에 대해서 알아보도록 하겠습니다.

지금까지 저희는 scanf와 printf를 주로 사용했었는데, 이 함수들은 데이터 입출력에 관해선

아주 다양한 기능을 가지고 있습니다. 하지만 그만큼 몸집이 크고 전문성은 떨어집니다.

 

예를 들어 scanf의 같은 경우에는 문자열을 입력받을 시 중간에 공백문자가 있으면,

그 이후의 문자는 입력을 못 받는다는 단점을 가지고 있습니다.

#include<stdio.h>

int main() {
	char str1[20];
	printf("문자를 입력하세요. : ");
	scanf("%s", str1);
	printf("%s", str1);

	return 0;
}

(scanf로 문자열을 입력받을 때에도 %s를 사용합니다.)

그렇기 때문에 C언어에선 문자열에 대해 좀 더 전문적으로 입출력을 담당하는 함수가 따로 존재합니다.

이 또한 사용하려면 string.h헤더를 선언해주셔야 합니다.

 

먼저 문자열 입력을 받는 함수로 gets함수가 있습니다.

이는 키보드에서 엔터를 누르기 전까지 입력되는 모든 문자열을 저장할 수 있는 함수입니다.

함수 원형은 다음과 같습니다.

char* gets(char*);

이를 사용해서 위의 코드를 바꿔보도록 합시다.

#include<stdio.h>
#include<string.h>

int main() {
	char str1[20];
	printf("문자를 입력하세요. : ");
	gets(str1);
	printf("%s", str1);

	return 0;
}

이 함수는 문자열을 입력받기 때문에 자료형을 따로 지정해줄 필요가 없습니다.

전달 인자는 입력받은 문자열을 저장할 문자 배열의 포인터, 즉 배열명입니다.

 

다음은 문자열 전용 출력 함수인 puts가 있으며 함수 원형은 다음과 같습니다.

int puts(char*);

전달 인자는 gets와 마찬가지로 포인터 변수이며, 출력할 문자열이 저장된 배열의 배열명입니다.

문자열 출력은 printf로도 충분히 수행 가능하지만 문자열에 관해서 사용이 더 편리하다는 점과,

문자열을 출력한다는 의미를 포함하기 때문에

가독성 좋은 코드를 작성할 수 있다는 의미로 도움이 될 것입니다.

 

printf와 다른 점은 puts의 경우 문자열을 하나 출력한 뒤에 자동으로 줄 바꿈을 해준다는 점입니다.

또한 리턴값을 통해서 출력이 정상적으로 실행됐는지 확인할 수 있습니다.

리턴값이 0일 경우 정상출력이며, 실패한 경우에는 -1을 리턴합니다.

#include<stdio.h>
#include<string.h>

int main() {
	char str1[20];
	printf("문자를 입력하세요. : ");
	gets(str1);
	printf("정상출력시 0, 아닐경우 -1 : %d", puts(str1));

	return 0;
}

 

gets를 사용해 문자열을 입력받고 puts를 통해 출력해보았습니다.

또한 return값을 통해 문자열이 정상적으로 출력된 것 또한 확인해보았습니다.

 

문자열에 관련된 두 가지 함수에 대해서 알아보았는데, 책에선 이 함수를 특별히 권장하진 않고 있습니다.

그 이유는 gets함수에 경우엔 입력받은 문자열이 배열의 크기를 넘을시엔 허용되지 않은 기억공간을

침범할 우려가 있는데, 이는 프로그램에 치명적인 오류를 초래할 수 있기 때문입니다.

따라서 gets함수를 사용해 문자열을 입력받을 땐 사용자에게 정확한 매뉴얼과 경고가 필요할 것입니다.

 

이번에는 문자열이 아닌 하나의 문자를 입출력하는 함수를 알아보도록 합시다.

 

먼저 알아볼 것은 하나의 문자를 입력받는 getchar인데 함수 원형은 다음과 같습니다.

int getchar();

getchar는 전달 인자가 없으며 함수호출시 키보드로부터 하나의 문자를 입력받아 리턴합니다.

 

문자 하나를 입력하는 함수는 putchar이며 함수 원형은 다음과 같습니다.

int putchar(int);

putchar는 출력할 문자를 인자로 넘겨주여야 사용할 수 있습니다.

 

코드를 통해 문자 하나를 입력받고 이를 출력해보도록 합시다.

#include<stdio.h>

int main() {
	char ch;
	printf("문자를 입력하세요. : ");
	ch = getchar();
	printf("입력된 문자 : ");
	putchar(ch);
	return 0;
}

또한 이 두 함수를 사용할 땐 string.h헤더 파일을 선언하지 않으셔도 됩니다.

특이한 점은 이 두 함수의 반환 타입과 인자가 모두 int형이라는 것인데요,

그 이유에 대해서도 추후에 설명하도록 하겠습니다.

 

그전에 getchar를 사용해 문자열을 입력하는 방법에 대해 알아보도록 합시다.

방법은 아주 간단한데, 반복문을 통해서 문자를 하나씩 입력받고 이를 배열에 저장하면 됩니다.

#include<stdio.h>

int main() {
	char str[20];
	int i;
	for (i = 0; i < 5; i++) {
		str[i] = getchar();
	}
	str[i] = NULL;

	printf("입력된 문자열 : %s", str);
    	return 0;
}

그리고 반복문이 끝나면 배열의 끝에 문자열의 끝을 의미하는 NULL값을 입력해줍니다.

그런데 한번 생각해봅시다. 위와 같이 getchar를 사용할 때 문자를 연속적으로 입력해서 사용해야 할까요?

아니면 문자 하나를 입력하고 엔터를 입력하는 과정을 반복해야 할까요?

 

이것은 이전에도 설명했던 이야기인데, 프로그램은 사용자로부터 값을 입력받으면

이를 입력 버퍼에 저장했다가 가져옵니다.

따라서 후자처럼 문자 하나를 입력하고 엔터를 입력하면 오히려 문자열이 이상하게 입력될 것입니다.

위와 같이 줄 바꿈 값이 같이 입력되기 때문에 입력이 정상적으로 이루어지지 않습니다.

따라서 문자를 연속적으로 입력해주셔야 정상적으로 사용하실 수 있습니다.

 

그리고 또 한 가지 생각해보아야 할 문제가 있습니다.

그것은 위와 같이 사용하면 입력받을 문자열의 길이를 꼭 지정해주어야 한다는 것입니다.

그렇기 때문에 이런 문제점을 해결하기 위해서 다음과 같이 사용합니다.

#include<stdio.h>

int main() {
	char str[100];
	int i = 0;
	while (1) {
		str[i] = getchar();
		if (str[i] == -1) {
			i++;
			break;
		}
		i++;
	}
	str[i] = NULL;

	printf("입력된 문자열 : %s", str);
    	return 0;
}

여기서 getchar의 반환 타입이 int였던 이유가 나오게 됩니다.

getchar의 경우 실행창에서 ctrl+z를 누르게 될 경우 -1을 리턴하도록 설계되어있습니다.

따라서 무한 반복문을 통해 문자를 계속해서 입력받고, 그 값이 -1이면 break문을 통해 종료하도록 한 뒤

입력을 그만하고 싶을 때 ctrl+z를 입력해서 무한반복문을 종료시키면 되는 것입니다.

주의해야 할 점은 한 줄의 끝에 입력하는 것이 아니라 새로운 줄을 시작할 때 ctrl+z를 눌러야 합니다.

 

또한 putchar의 경우엔 출력을 실패하면 -1을 리턴해야 하기 때문에 반환 타입이 int인 것입니다.

 

이렇게 문자열, 문자열과 관련된 함수, 문자와 관련된 함수, 이를 사용하는 법에 대해서 알아보았습니다.

다음 글에선 포인터 배열과 다중 포인터에 대해 알아보도록 하겠습니다.

감사합니다.

반응형

'프로그래밍 언어 > C언어' 카테고리의 다른 글

C언어 15. 메모리 동적 할당  (0) 2020.07.20
C언어 14. 포인터 배열, 다중 포인터  (0) 2020.07.17
C언어 12. 배열과 포인터  (0) 2020.07.15
C언어 11. 포인터  (0) 2020.07.14
C언어 10. 배열  (0) 2020.07.13