- 널 - 종료 문자열 (Null-Terminated String)
- 문자열 리터럴(String Literal)
- 문자열 입력 받기
- 문자열의 활용
- Q&A
- 마치며
문자열은 영어로 String입니다.
실
이란 의미인데요.
실제로 컴퓨터는 문자열을,
문자들의 배열
즉, char
배열에 저장합니다.
만약 우리가 문자열 s
에 Psi
라는 문자열을 저장했다고 합시다.
우리의 시각으로 볼 때,
문자열 s
를 이용한다고 하면,
당연히 Psi
를 이용한다고 생각합니다.
하지만 컴퓨터는 그렇지 않습니다.
길이를 지정해주지 않으면 어디까지 이용할지를 모르는거죠.
따라서 C언어 개발자들은 문자열의 끝에,
'Null'값을 넣어,
'여기까지가 문자열이야!'라고 표시를 해주었습니다.
- 널(Null)
: 문자열의 종료를 알려주는 종료 문자
: 아스키값은0
,'\0'
로 지칭하기도 함.//다음 3개는 모두 Null값을 지칭 char null1 = (char)NULL; char null2 = 0; char null3 = '\0'; //다음은 Null이 아님 char not_null = '0';
(
'0'
은 아스키 코드값이 48이므로, Null이 아님)
이 Null값 때문에,
우리가 Psi
라는 문자열을 저장할 때는
크기가 3
이 아닌, 4
인 배열이 필요합니다.
아래의 예시를 볼까요?
#include <stdio.h>
int main() {
char s1[4] = { 'P', 's', 'i', '\0' };
char s2[4] = { 'P', 's', 'i', 0 };
char s3[4] = { 'P', 's', 'i', (char)NULL };
char s4[4] = {"Psi"};
printf("s1 : %s\n", s1);
printf("s2 : %s\n", s2);
printf("s3 : %s\n", s3);
printf("s4 : %s\n", s4);
return 0;
}
[Result]
sentence_1 : Psi
sentence_2 : Psi
sentence_3 : Psi
sentence_4 : Psi
s1, s2, s3
는 모두 완벽한 널-종료 문자열입니다.
그런데 s4
는 조금 형태가 다르죠?
사실 각 문자들을 작은 따옴표로 구분해 표시하는 것은 매우 번거롭습니다.
그래서 C언어에서는 큰 따옴표로 문자열을 묶어주면 알아서 각각의 문자로 넣어주고,
자동으로 Null값을 추가해줍니다.
저같은 초보자가 흔히 하는 실수로,
Psi
의 크기를 3
으로 지정하는 것입니다.
이렇게 하면 Null값이 들어가지 않아,
허용되지 않는 메모리 범위를 읽게 됩니다.
따라서, 반드시 Null 문자를 위한 공간을 추가하는 것을 기억합시다!!!!!
그리고 출력을 할 때는,
형식 지정자로 %s
를 사용합니다.
%s
는 문자열의 시작부터 ~ Null 값까지 문자를 계속 출력합니다.
(%c
는 문자 1개만 출력합니다!)
- " "(큰 따옴표)
: 문자열(한 개 이상의 문자)을 지정할 때 사용됩니다.
ex) "abc"
, "aasdfasdf"
, "a"
등등
- ' '(작은 따옴표)
: 한 개의 문자를 지정할 때 사용됩니다.
ex) 'a'
, 'b'
, '\0
등등
틀린 ex) 'abc'
, 'ab'
등등
#include <stdio.h>
int main() {
char word[30] = {"long sentence"};
char *str = word;
printf("%s \n", str);
return 0;
}
[Result]
long sentence
위의 예제는,
char을 가리키는 포인터 str
이 배열 word
를 가리키고 있습니다.
그리고 Null값이 나올 때까지 출력합니다.
그리고 char word[30] = {"long sentence"};
에서
문자열의 크기를 지정하지 않아도 됩니다.
즉, char word[] = {"long sentence"};
로 선언해도,
컴파일러가 알아서 원소의 수를 세어 빈칸에 채워 넣습니다.
다음 예제를 봅시다.
#include <stdio.h>
int main() {
char str[] = "sentence";
char *pstr = "sentence";
printf("str : %s \n", str);
printf("pstr : %s \n", pstr);
return 0;
}
[Result]
str : sentence
pstr : sentence
뭔가 이상합니다....
분명 str
은 평범한 문자열이네요.
그런데 pstr
은?
pstr
은 char형을 가리키는 포인터이므로,
char형 변수의 주소값이 들어가야합니다.
그런데 "sentence"
는 문자열입니다.
어떤 변수의 주소값이 아닙니다.
그런데 사실 "sentence"
는 사실 주소값이 맞습니다😊
"sentence"
라는 문자열이 저장된 시작 주소값입니다.
#include <stdio.h>
int main() {
char str[] = "hello";
char *pstr = "goodbye";
str[1] = 'a';
pstr[1] = 'a';
return 0;
}
위 코드를 실행해보면, 런-타임 에러가 발생합니다.
바로 ptsr[1] = 'a';
때문입니다.
실제로 해당 코드를 주석처리해보면, 정상적으로 실행됩니다.
그 말은?
ptsr[1] = 'a';
을 통해 pstr
의 값을 변경할 수 없다는 것입니다.
- 리터럴(Literal)
: 소스 코드 상에서 고정된 값을 가지는 것
: 특히, C언어에서 큰 따옴표(" "
)로 묶인 것들은 '문자열 리터럴(String Literal)'이라고 함.
char *pstr = "goodbye";
printf("why so serious?");
scanf("%c", str[0]);
에서 리터럴은
"goodbye", "why so serious", "%c"
모두 리터럴 입니다.
컴퓨터는 이러한 리터럴 들을 따로 모아서 보관합니다.
즉, 프로그램을 실행하면 해당 리터럴들을 따로 보관하는 공간이 생깁니다.
(참고로, text segment라고 합니다.)
char *pstr = "goodbye";
은,
"goodbye
의 시작 주소값을 가져와서 pstr
에 대입해!!!"라는 말입니다.
위에서 본 예시에 의하면,
이러한 리터럴들은 어떤 경우라도 값이 변경되면 안됩니다!!!!
따라서 리터럴이 보관되는 곳은 오직 읽기만 가능한 곳입니다.
즉, 위의 예시에서 본 ptsr[1] = 'a';
는
"리터럴 세상에 저장된 리터럴 "goodbye"
의 값을 변경하려고 한 것"입니다.
엥?? 그런데
char str[] = "hello";
에서 "hello"
도 리터럴 아닌가요?
그런데 왜 str[1] = 'a';
는 실행되죠?
사실, 위 "hello"
는 리터럴이라고 보기 애매합니다.
해당 문장은 컴파일러에 의해 다음과 같이 해석됩니다.
char str[] = {'h', 'e', 'l', 'l', 'o', '\n'};
아하..
즉, str
이라는 배열에 hello
라는 문자열을 복사하는 것 뿐입니다.
그리고 text segment가 아닌, stack에 저장되어 메모리 수정이 가능합니다.
(참고로, VS2017 이상에서는 리터럴을 const char*
가 가리켜야 한다고 합니다...)
#include <stdio.h>
int main() {
char str[30];
scanf("%s", str);
return 0;
}
위의 문자열 str
은 29글자를 저장할 수 있습니다.
(마지막은 Null값이 들어가야 합니다)
그리고 scanf
함수를 통해 문자열을 입력받습니다.
이 때 형식 지정자는 %s
입니다.
단, 이 때는 다른 Type과는 달리 &
을 붙이지 않습니다.
그 이유는, 배열의 이름 자체가 배열을 가리키고 있는 포인터이기 떄문에,
시작 주소값을 잘 전달할 수 있습니다.
scanf
함수는 enter
가 나올 때까지 입력을 받습니다.
그런데 이 때, 입력으로 what is your name
이라고 주면,
결과는 what
까지만 나옵니다.
무슨 일이죠..🤔
한 번 살펴봅시다.
자, 다음 예제를 봅시다.
#include <stdio.h>
int main() {
int num;
char c;
printf("숫자를 입력하세요 : ");
scanf("%d", &num);
printf("문자를 입력하세요 : ");
scanf("%c", &c);
return 0;
}
[Result]
숫자를 입력하세요 : 1
문자를 입력하세요 :
위의 코드를 실행해서,
먼저 입력으로 1
을 주게되면 프로그램이 종료됩니다.
오잉??
c
도 입력을 받아야 할텐데 말이죠....😢
이 이유에 대해서 알기 위해서 scanf
가 어떻게 작동하는지 알 필요가 있습니다.
컴퓨터는 우리의 입력을 어떻게 처리할까요?
만약, abced
를 입력한다고 할 때,
이 2가지 방법이 있습니다.
이는 다음과 같은 예시를 들 수 있는데요,
만약 우리가 물을 약수터에서 3L 퍼온다고 할 때,
뭐가 좋을까요?
당연히 2번이겠죠.
컴퓨터도 마찬가지입니다.
이 양동이
에 해당하는 부분이 바로,
'버퍼(Buffer)'입니다.
그리고, 수 많은 버퍼 중에서도,
키보드 입력을 처리하는 버퍼는,
'입력 버퍼' 혹은 'stdin(입력 스트림)'이라고 부릅니다.
- 버퍼(Buffer)
: 데이터를 한 곳에서 다른 곳으로 전달하는 동안, 일시적으로 그 데이터를 보관하는 메모리 영역
즉, 우리가 키보드로 입력하는 모든 정보는 일시적으로 'stdin'에 저장되었다가,
입력이 종료되면 한꺼번에 처리합니다.
그 종료를 알 수 있는 것이 바로, enter
입니다.
다시 말해, 컴퓨터는 개행 문자(\n
)를
"입력이 종료되었으니 버퍼에 있는 내용을 처리해!"
라고 받아 들입니다.
scanf함수는 stdin으로부터,
' '(공백), '\n', '\t'
을 만날 때까지 데이터를 얻습니다.
그런데 컴퓨터는 공백문자(' '(공백), '\n', '\t'
)까지 버퍼에 저장합니다.
예를 들어 입력으로 1
을 주고 enter
를 누르면,
stdin에는 1\n
이 저장됩니다.
위의 예시에서 scanf("%d", &num);
을 통해,
컴퓨터는 scanf
함수를 이용해서 stdin으로부터 숫자를 얻습니다.
그런데 문제는 숫자 데이터만 가져옵니다.
(참고로, %d
계열은, 수가 아닌 데이터가 와도 입력을 종료합니다.
그리고 처음부터 공백문자가 나타나면 수가 입력될 때까지 입력을 받습니다.
(그래서, 처음에 엔터를 무한 번 눌러도 넘어가지 않습니다.))
정리하면, scanf
함수는
공백 문자(' '(공백), '\n', '\t'
)를 만날 때까지 stdin에서 데이터를 가져간 후 버퍼에서 삭제해버립니다.
그러면 버퍼에는 1\n
에서 \n
이 됩니다.
그리고 컴퓨터는 다음 명령인 scanf("%c", &c);
을 실행합니다.
%c
는 이유 불문하고, stdin에서 딱 한 개의 문자만을 가져갑니다.
만일, stdin에 아무것도 없다면 입력을 기다리겠지만,
지금 stdin에는 \n
이 있으므로 그걸 들고옵니다..😂
그래서, c
에는 사용자의 입력을 받지 않고 \n
이 저장됩니다......
(겁나 멍청하네...)
실제로 c
를 출력하면,
출력으로 \n(한 칸 개행)
이 출력됩니다.
그렇다면 위의 예시와 달리,
%c
대신 %s
로 scanf를 활용해볼까요?
#include <stdio.h>
int main() {
char str[30];
int i;
scanf("%d", &i);
scanf("%s", str);
printf("str : %s", str);
return 0;
}
[Input]
1
abcde
[Result]
str : abcde
오!
정상적으로 작동합니다!!
scanf("%d", &i);
을 하면,
stdin에는 \n
이 남게 됩니다.
scanf("%s", str);
을 실행하면, %d
와 마찬가지로,
실질적인 데이터(공백 문자가 아닌)가 나오기 전까지 버퍼에 남아 있는 공백 문자들을 무시하고,
실질적인 문자가 입력이 되면,
그 다음에 등장하는 공백 문자에서 입력을 종료합니다.
즉, 기존에 1
을 입력하고 받은 \n
은 무시하고,
abcde
를 입력하고 받은 \n
을 인식하는 것입니다.
정리하면,
%d, %s나 수 데이터를 입력 받는 형식
은 버퍼에 남아있는 공백 문자를 신경쓰지 않고 사용할 수 있습니다.
하지만 %c
는 버퍼에 무엇이 남아있는지 잘 생각해야 합니다.
그렇다면 처음으로 돌아와서,
#include <stdio.h>
int main() {
char str[30];
scanf("%s", str);
return 0;
}
[Input]
what is your name
[Result]
what
사실 위에서 배운 내용만으로도 이유를 알 수 있겠죠?
입력으로 준 what is your name
에서,
what
다음에 ' '(공백)
이 있기 때문이죠.
scanf함수는 stdin으로부터 실질적인 데이터(공백 문자가 아닌)를 제외한 문자가 나올 때까지 모든 공백 문자들을 무시합니다.
처음 stdin에는 아무것도 없었고,
위의 입력에서는 ' '(공백)
이 공백 문자이기 때문에 what
까지만 입력이 됩니다.
그렇다면 stdin에 남은 모습은,
_is_your_name\n
이 되겠죠.
하..............
(개짜증나네요...................🤬)
도대체 어떻게 해야할까요ㅠㅠ
다음을 보시죠...
#include <stdio.h>
int main() {
int num;
char c;
printf("숫자를 입력하세요 : ");
scanf("%d", &num);
getchar();
printf("문자를 입력하세요 : ");
scanf("%c", &c);
return 0;
}
[Result]
숫자를 입력하세요 : 1
문자를 입력하세요 : s
scanf("%d", &num);
을 실행하면,
stdin에는 \n
이 남습니다.
이후에 getchar()
가 등장합니다.
이는 "stdin에서 한 문자를 읽어와서 그 값을 Return한다"라는 의미입니다.
물론, 읽어온 그 문자는 사라집니다.
따라서, stdin에 1
을 입력하고 남은 \n
을 읽어와서 지워버립니다.
그러면 scanf("%c", &c);
을 실행할 때,
stdin에는 아무것도 없으므로,
새로운 입력을 받을 때까지 기다리게 됩니다.
그런데.............이것도 완벽한 방법은 아닙니다.........
만약 입력으로 123abc
를 주면요?
[Result]
숫자를 입력하세요 : 123abc
문자를 입력하세요 : b
....😐
scanf("%d", &num);
로 숫자 데이터 123
까지만 읽고,
getchar()
로 a
를 읽어옵니다.
그러면 stdin에는 bc\n
이 남습니다.
이 때, scanf("%c", &c);
을 실행하면
당연히 b
만 읽어오겠죠.
여기서 알 수 있는 사실!!!!
scanf에서는 되도록이면 %c
를 사용하지 말자!!!!!!!!!!!!!
그러면... 한 개 문자만을 받을때는요??
그 떄는 %s
를 사용해, 문자열의 제일 앞 문자만 가져오도록 하는 것이 좋습니다.
정 하고 싶으면, 문자열 %s
로 입력받자!!!!!!!!!!!!!!
사실 C언어에서 문자열을 다루는 것은 꽤나 복잡합니다.
문자열에서는 +
연산자도 사용할 수 없습니다.
char str1[] = {"abc"};
char str2[] = {"def"};
str1 = str1 + str2;
이는, 각 배열의 주소값을 더하는 것인데,
배열의 이름은 포인터 상수이기 때문에,
대입 연산을 할 수 없습니다.
또한, 비교하는 것도 불가능합니다.
if (str1 == str2)
이도 마찬가지로, str1
과 str2
의 시작 주소를 비교해라는 문장이므로,
우리가 원하는 진정한 '비교'가 아닌 것이죠...
if (str1 == "abc")
도 안되겠죠...
str1
이 저장된 시작 주소값과 "abc"
라는 문자열 리터럴이 보관된 메모리 주소값을,
비교하는 것이기 때문이죠....
가장 답답한 것은 "복사", 다시 말해 "대입"도 못합니다.
str1 = str2;
str1
은 포인터 상수로서, 값을 바꿀 수 없기 때문이죠..
하지만 '함수'를 이용해서 그나마 편리하게 다룰 수는 있습니다.
문자열 내의 문자의 수를 세는 함수
문자열을 복사하는 함수
문자열을 합치는 함수(즉, 더하는)
문자열을 비교하는 함수
문자열을 사용하다보면, 특정 문자열의 개수를 세는 일이 있습니다.
int str_length(char* str) {
int i = 0;
while (str[i]) {
i++;
}
return i;
}
함수를 만들기 전에 고려해야 하는 것이 있습니다.
1. 이 함수는 무슨 작업을 하는가?
(자세할 수록 좋다고 합니다.)
2. 함수의 리턴형은 무엇이면 좋은가?
3. 함수의 인자로는 무엇을 받아야 하는가?
1번 같은 경우 매우 중요합니다!!
무턱대고 만들면 코드가 난잡해지고 어려워지죠.
우리는 "문자열을 복사하는 함수"
즉, a
라는 문자열의 모든 내용을 문자열 b
로 복사하는 것입니다.
2번 같은 경우는, 원본 작성자분께서는
복사에 성공하면 1을 리턴하도록 하셨습니다.
즉, int형이죠.
3번 같은 경우는, 두 개의 문자열을 받아야 하므로,
char*
을 인자로 2개를 받습니다.
int copy_str(char *src, char *dest) {
while (*src) {
*dest = *src;
src++; // 그 다음 문자를 가리킨다.
dest++;
}
*dest = '\0';
return 1;
}
이 함수는 src
에 Null값, 즉 0이 될 때까지
src
의 값을 dest
에 복사합니다.
하지만, 이 함수는 상당히 위험한 편입니다.
만약, dest
가 src
의 크기가 같지 않다면
메모리의 공간을 침범하기 때문이죠...
그렇다면 다음은 어떨까요?
char str[100];
str = "abcdefg";
이 역시 컴파일 오류를 일으킵니다.
"str
에 문자열 리터럴 "abcdefg"
가 저장된 주소값을 넣어라"는 것인데,
배열 이름은 상수이므로, 배열의 주소값을 바꿀 수 없습니다.
상수에는 값을 대입할 수 없기 때문에 오류가 나죠.
char str[100] = "abcdefg";
근데 왜 이거는 돼죠?
이것은 C언어에서 사용자의 편의를 위해 제공하는 방법이라서요...
오직 배열을 정의할 때만 사용할 수 있는 방법입니다.
따라서, 복사를 해줄때는,
해당 문자열들의 크기가 같은지 살펴보고,
다르다면 맞춰준 후 사용합시다!!
해당 함수는 다음 역할을 수행합니다.
char str1[] = "Wonder Land!";
char str2[100] = "Hi! ";
stradd(str1, str2);
// str2 은 "Hi! Wonder Land!" 가 된다.
그럼 stradd
함수를 보시죠!!
int stradd(char* src, char* dest) {
// dest의 끝 부분을 찾습니다.
while (*dest) {
dest++;
}
/*
현재 dest는 문자열의 끝 부분 NULL을 가리키고 있습니다.
이제 src의 문자열을, dest의 NULL부터 복사합니다.
*/
while (*src) {
*dest = *src;
src++;
dest++;
}
// 마지막으로 dest에 NULL 추가
*dest = '\0';
return 1;
}
dest
의 NULL값이 있는 곳으로 가서,
해당 부분부터 src
의 값을 복사해줍니다.
마지막으로 새로운 NULL값을 넣어주면 완성!
활용은 다음과 같습니다.
#include <stdio.h>
int stradd(char* src, char* dest); //Prototype
int main() {
char str1[] = "Wonder Land!";
char str2[100] = "Hi! ";
stradd(str1, str2);
printf("%s", str2);
return 0;
}
int stradd(char* src, char* dest) {
// dest의 끝 부분을 찾습니다.
while (*dest) {
dest++;
}
/*
현재 dest는 문자열의 끝 부분 NULL을 가리키고 있습니다.
이제 src의 문자열을, dest의 NULL부터 복사합니다.
*/
while (*src) {
*dest = *src;
src++;
dest++;
}
// 마지막으로 dest에 새로운 NULL을 추가합니다.
*dest = '\0';
return 1;
}
[Result]
Hi! Wonder Land!
해당 함수는 다음 역할을 수행합니다.
if (compare(str1, str2)) {
/*
만일 str1 과 str2 가,
같다면 이 부분이 실행되고,
아니면 지나갑니다.
*/
}
그럼 compare
함수를 보시죠!!
int compare(char* str1, char* str2) {
while (*str1) {
if (*str1 != *str2) {
return 0;
}
str1++;
str2++;
}
if (*str2 == '\0') { return 1; }
return 0;
}
먼저, while문을 통해, 각 글자들을 하나씩 비교합니다.
만약 다르다면 0
을 리턴합니다.
그런데 만약, str1
과 str2
가 str1
부분만 일치했다면?
예를 들어, str1
은 abc
이고, str2
가 abcd
라면?
그래서 다음 if문으로 str2
의 끝에 도달했는지를 확인합니다.
도달했다면, 두 문자열은 같은 것이고,
아니라면 str2
는 str1
을 포함한 다른 문자열입니다.
활용은 다음과 같습니다.
#include <stdio.h>
int compare(char* str1, char* str2); //Prototype
int main() {
char str1[20] = "Wonder Land!";
char str2[20] = "wonder Land!";
char str3[20] = "Wonder Land!";
if (compare(str1, str2)) {
printf("str1과 str2는 같다\n");
}
else {
printf("str1과 str2는 다르다\n");
}
if (compare(str1, str3)) {
printf("str1과 str3는 같다\n");
}
else {
printf("str1과 str3는 다르다\n");
}
return 0;
}
int compare(char* str1, char* str2) {
while (*str1) {
if (*str1 != *str2) {
return 0;
}
str1++;
str2++;
}
if (*str2 == '\0') { return 1; }
return 0;
}
[Result]
str1은 str2와 다르다
str1은 str3와 같다
-
C의 문자열...
정말 복잡해서 학교 다니면서 고생했던 기억이 나네요...
어마무시한 포인터 다음에 배우는게 문자열이라니..
하지만 두 개념을 떼고 볼 수 없어서 어쩔 수 없지만...
너무 힘든 건 어쩔 수 없네요...😢
그래도 그 때 고생해서 지금은 문자열 활용 방법을 보니까 꽤나 쉽게 받아들일 수 있네요.
시험 때 문자열 문제 1개를 못 풀었었는데,
그 쉬운 걸 왜 그 때는.......😢
지금 주면 바로 풀 수 있겠어요...
이제 다음은 구조체(struct)입니다.
빨리 C를 정리해야 '보안' 공부를 시작할텐데...
목표는 7월 말이나, 8월 초에 C를 다 복습하는건데...
가능하겠죠???
열심히 하겠습니다 😊
[Reference] : 위 글은 다음 내용을 참고, 인용하여 만들어졌습니다.