[C] C lang 문자열
문자열의 표현과 길이
- C에는 자체적으로 문자열이 없다. -> 그래서 char 형 배열을 계속 사용.
- 물론 이걸 대신 문자열처럼 표현하는 방법들이 있다. 이게 C스타일 문자열.
- C에선 가장 기본적인 것들이고, 면접 볼떄 이거 몰라서 떨어지는 사람들도 많다.
가장 기본적인 내용 문자열 길이는 어떻게 구하나.
- 기본 자료형 배울 때, 어떤 걸 가장 많이 봤나?
- 자료형의 크기, 범위
- 언제나 고정
문자열 길이는 정해져 있지 않다.
- 따라서 하드웨어보고 문자열 하나 읽으라 하면 어쩔 줄 모름.
이게 문자열이 기본 자료형이 아닌 이유.
- 문자열은 여러가지 글자 (문자)들이 섞인 것.
심지어 공백조차 글자.
- “여러 개”의 문자를 표기하는 데 뭐가 좋을까?
배열
- 따라서 char str[ 글자수 ] 로 표현 가능
배열의 길이도 저장해야 할 것 같은데.
- 앞에서 배열에서 배열의 길이 구할 수 없다고 했는데?
- 맞다. 그래서 배열의 길이가 배열과 같이 저장이 안 됨.
- 따라서 프로그래머가 배열의 길이는 따로 알고 있어야 한다.
- 그 말은 따로 변수 만들어서 이 배열의 길이는 몇이다 이런거 주면 좋다.
- 함수 호출시 배열 길이 같이 매개변수로 넘겨준다거나 했던거는 이미 했었다.
- char str[] = “c is fun”;
문자열 관리 시 길이의 문제
그럼 길이를 저장하는 변수만 있으면 되겠네?
void print_string(void)
{
char chars[] = "Pointer is the best";
const size_t NUMS = 20;
size_t =i;
for (i = 0; i<NUM_CHARS ; ++i)
{
printf("%c", chars[i]);
}
}
근데 여기서
void print_string(void)
{
char chars[] = "Pointer is the best C language";
const size_t NUMS = 20;
size_t =i;
for (i = 0; i<NUM_CHARS ; ++i)
{
printf("%c", chars[i]);
}
}
이런식으로 문자열 길이가 늘어나버리면? 배열의 크기도 바꿔야 함. 근데 깜빡하고 실제 배열의 크기를 저장하는 변수 NUMS의 값을 안 바꾸면? 배열의 크기는 자동으로 바꾸는 데 변수를 제대로 안 바꾼 것 -> 세보니까 30개 가량인듯 근데 세봤는데 잘 못 세거나 문자열이 어엄청 길면?
그래서 어떤 것에서 도출해 낼 수 있는 값들을 다른 변수를 써서 따로 저장해두고 기억해 두면 이런 실수를 할 일이 많다.
배열에서 길이를 곧바로 읽어오는 방법이 없기 때문에 이런 식으로 하는 경우가 많다.
문자열은 자주 쓰는데 이런 식으로 관리시 문제가 엄청 터지게 됨.
- 자주 쓰는 아이는 프로그래밍 언어 자체에서 관리해주면 좋다.
문자열 길이 문제 해결방법 1
- 길이를 배열 첫 위치에 저장
- 첫 메모리 위치에 문자열 길이를 저장하고, 실제 문자열이 따라오게 함.
- unsigned char로 길이를 저장하기엔 너무 짧음. (255자)
- 길이는 int로 저장하고(4 byte), 그 뒤에 char로 문자들을 저장.(그럼 2의 32승까지니까 엄청 큰 숫자가 된다.)
장점
- 첫 주소만을 보는 것 만으로 총 글자수가 몇인지 알 수 있다.
- 다른 언어에서 문자열 크기를 바로 알 수 있었던 이유
- 이런 방식으로 문자열의 길이를 저장해 두기 때문
string message = "HELLO";
Console.writeLine($"message len:{message.Length}");
단점
- 글자 하나가 1바이트인데 여기에 4바이트나 쓴다?
- 이건 클래스 또는 구조체에서나 쓰는 법
- 순수 C 코드로 이것을 어떻게 작성해야 할 지 애매함.
첫 데이터는 int 로 캐스팅해서 읽고, 그 다음 부터는 char로 읽어야 함.
물론, 디스크로부터 이진파일을 통째로 읽어 메모리에 읽어오면 이런 식으로 접근하기는 함. 근데 이걸 언어 기본으로 하는거 자체가..
char array[9]; //길이 5와 'hello'가 적혀있음.
int * len = (int*)&array[0];
char * str = &array[4];
ㄴ
문자열 길이 문제 해결방법 2, C 스타일 문자열
문자열이 끝나는 위치를 표시
그냥 char[] 만 쓰되 그 문자열이 끝나는 위치에 특수한 문자를 두자.
- 배열에서 값을 찾을 수 없으면 존재할 수 없는 색인 -1을 반환하는 방식과 마찬가지.
size_t index_of(const char* str, char c)
{
//str 안에 c가 있으면 해당 색인
//없으면 -1 리턴
return -1;
}
아스키 코드 중 화면에 출력되지 않는 특수한 문자들이 있음.
- 제어문자라 불림.
- 0~31, 127
- 그 중 하나가 0
- 널 문자(null character, 널 캐릭터)라고 불림.
- 널 포인터하고 다른 아이니 헷갈리지 말자.
char null_char ='\0';
- 0은 숫자 영
- \는 이스케이프 문자
- 근데 아스키코드로 0이니 char null_char = 0으로 작성 가능
- 그러나 읽기 쉽게 ‘\0’으로 써주자.
C스타일 문자열이라 하면 널 문자로 끝나는 char배열을 말함.
C스타일 문자열
- char[] 로만 구성
- 문자열이 끝나는 곳에 널 문자를 붙임.
char str1[] = "abc"; //스택에 abc 저장
char *str2 = "abc"; //데이터 섹션에 abc저장
이 두 코드는 저장위치 이외에는 동일
- 문자열 뒤에 별도로 ‘\0’을 넣지 않아도, 컴파일러가 알아서 해줌.
단, 이 경우에는 ‘\0’을 넣어주지 않음
- char str[] ={‘a’,’b’,’c’};
const char str[] = "hello"
printf("str length:%d", sizeof(str));
뭐가 출력 되나? (6)
C 스타일 문자열의 장단점, 문자열 길이 구하기
언제나 배열에 널 문자도 있다는 건 있지 말자.
- 문자열의 길이는 4
- 배열의 길이는 최소한 5
배열의 실제 크기를 잡아두고(충분히 크게 잡아두고) 거기 일부만 이런식으로 5개 채워놓고 널 집어넣어도 그건 유효한 문자열임.
어찌보면 C에서 이런식으로 문자열 구현해서 메모리 관리가 좀 더 효율적이고 빠르게 될 수 있었다.
C스타일 문자열의 장단점
- 장점
- 가장 최소한의 메모리
- 한 가지 데이터형으로 문자열과 길이를 다 표현
- 단점
- 어떤 문자열 길이를 알려면 배열을 끝까지 훑어야 함. O(N)
문자열 길이 구하기
- 이 정도는 스스로 작성 해야됨
- 사실 매우 흔한 기초 면접 문제
- 너무 기초라 못 맞추면 그 순간 탈락.
- 이거 못해서 떨어지는 사람들도 많다(경력직까지)
- fizzbuzz마저 못 푸는 시니어도 많다.
문자열 구하기 개념
1 . char 배열의 요소를 처음부터 차례로 읽는다. 2 . 널 문자를 만나면 멈춘다. 3 . 여태껏 몇개 char 방문한 지 그 카운터를 반환
size_t get_string_length(const char* str)
{
size_t i;
for(i =0; str[i]!= '\0'; ++i)
{
}
return i;
}
이런 문자열 길이 구하는 건 1~2분 안에 풀어야 한다. 메모리상에서 어떻게 문자열 구성해야 하는지 이건 기초. 이런 걸 못 풀면 기초조차 안 되어 있다고 봄.
size_t get_string_length(const char* str)
{
size_t i;
for(i = 0; str[i]!= '\0';++i)
{
}
return i;
}
위 코드는 어떤가 확실한가? - 뭔 질문인지 모르면 다시 공부해야됨.(포인터를) 정수 훑으면서 합 구할때 나왔었다.
왜 최선이 아닌가?
1번씩 돌 때 마다 시작주소 플러스 i 를 해야한다. 비효율 적일 수 있음.
앞에서 포인터 못 잡으면 문자열 해도 의미 없다.
그떄 개미만큼 더 효율적인 건 포인터 쓰는거라고 했다.
만약 코드에서 for문썼는데 효율적으로 될까요? 라고 물을때 이렇게 답하면 면접에서 만점인 것.
둘 다 작동하는 방법
1은 str을 p에 두고 p를 그냥 증가시키는 거 (널 캐릭터 나올떄 까지) 그거와 시작위치와 널포인터 위치 구하고 뺴면 위치 나옴. = 그 사이 몇개 요소 있는 지 알려줌.
2는 카운터 변수 하나 두고 메모리 위치 옮길 떄 마다 카운트 하나 증가.
앞으로는 문자열 설명시 배열은 다른 언어에서나 하는거지 C에서는 포인터 방식.(배열을 포인터 접근하는 거 잊지 말자.)
사실 문자열 길이 구하는 함수는 있다.
size_t strlen(const char * str)
- < stdio.h > 를 인클루드 하면 사용 가능
- 우리가 만든 것과 똑같음.
- 이 외에 다른 함수들도 존재.
- 하지만 이 모든 걸 혼자 작성할 수 있어야 훌륭한 기본기 가졌다가 인정받음.
즉, 안전하지 않을 수도 있음.
- 외부에서 들어오는 문자열 읽을 떄, 조심해서 읽어야 함.
- 입출력 할 때 더 자세히 봄.
- C11의 strlen_s() 가 이 문제 해결하기도 함.
- strlen(str)은 안전할 수 있나?
그냥 읽는 거니 안전할 수도 있다.
- 나중 가면 함수가 이런거 까지 해야하나 싶은 것들도 있음.
- 보안 상 더 안전한 것.
- 물론 아닐 수도 있음.
- 운 좋게 나중에라도 ‘\0’이 나오는 경우가 생기면?
- 하드웨어가 보호하는 메모리 읽을 경우, 아예 뻑이 남.
- 뭐든 소유하지 않은 메모리 위치에 접근 하는 건 아주 위험.
- 유효하지 않다고 임베디드 시스템 같은거 가는 경우, 어떤 메모리 공간은 이 프로그램에서 절대 못 읽는 공간들이 있다. 하드웨어에서 그 공간을 사용하려고 하는 순간 시스템이 아예 뻗거나, System fault 뭐 그런 인터럽터를 쏘거나, 그런식으로 운영체제 단에서 뭔가 제재가 들어가게 됨.
- C 쓸 땐 정확히 알고, 소유하지 않은 메모리는 언제나 위험이 내재하고 있다. 그리고 프로그램 실행 할 때마다 메모리 위치 바뀌는 경우가 있다.
- 나중 동적 메모리나 그런 경우는 더더욱 그럼. 한번 실행 할 때 아무 문제 없었음. 막 실행해도 운 좋게 여긴 널이 있었다거나,
근데 다시 실행하니까 다음 번 실행에 널이 없을 수도 있다. 그럼 그때는 문제가 생김. 그래서 정확히 구현이 안 되는 경우도 있음.
- 나중 동적 메모리나 그런 경우는 더더욱 그럼. 한번 실행 할 때 아무 문제 없었음. 막 실행해도 운 좋게 여긴 널이 있었다거나,
- 그래서 포인터가 위험하다는 이유?
이런 식으로 메모리 아무데나 접근할 수 있기 떄문에 어떤 경우는 뻑 나고 어떤 경우는 뻑 안나고 다시 재현해서 디버깅 하기도 너무 어려움. 그런 것 때문에 포인터 작성시 원칙 잘 잡고 어떻게 잘 사용해야 하는 지, 잘 이해하고 언제나 올바른 프로세스가 있어야 한다는 것.
배열 잡으면 언제나 +1 한다거나, 나는 배열 잡아 놓을 떄, 언제나 -1까지만 문자열 복사하는 그런거 만들기나, 그런 원칙이 중구난방으로 그러면 어떤 원칙에 맞춰야 할 지 모르니까 실수를 더 할 수 밖에 없게 됨.
그래서 C/C++ 하는 경우 규칙 잘 잡고, 규칙 따라하면서 하는 사람들이 많다.
자유로움이라는 게 먹히지 않는 언어기도 함. 운동선수가 미친듯 반복 훈련하는거 마냥 그래야 한다.
문자열 조작, 두 문자열의 비교
위 내용들은 간단한 문자열 길이 구하는 그런 정도였다.
근데 그 외 에도 문자열을 가지고 다양한 조작이 가능하다. 문자열 연산 할 수도 있고, 문자열을 여기저기 복사할 수 도 있고, 문자열을 분리하고 등. 앞의 기초가 되어야 조작이나 비교 등 문자열 컨트롤이 가능하다.
두 문자열의 비교
compare_string (const char* str0, const char* str1 ) //여기서 값을 보호하는 지 , 주소를 보호하는 지 이거 모름 다시 공부하자.(오른쪽에서 왼 쪽으로 읽음)
//포인터인데 뭘 가리키나? char 를 가리키네. 근데 캐릭터를 가리키는 데 바꿀 수 없는 애다. 이렇게 읽는 것. 왜 바꾸지 않나? 문자열 길이 구할 떄도 읽기만 하면 됨. 마찬가지로 비교하는 거도 읽기만 하면 됨.
//내가 문자열 바꾸는 게 아니니까 const가 붙는 거. C는 이런식으로 const가 잘 붙음. 그래서 함수 안에서 이상한 일 할 수 있는 여지가 적다. C가 위험한 언어지만 좀 더 안전성을 보장하는 방식도 더 많다.
//어쨌든 두 문자열 받아서 비교하니까 , 문자열을 받고 얘는 const이다.
- 두 문자열을 비교 할 거니까 매개변수는 두 문자열 포인터.
- 반환형은 아직 미정
- 단순히 같다/틀리다 를 비교하는 게 아님.
- 사전식 순서로 아스키코드로 크다/같다/작다를 비교
compare_string 의 반환 값
그러면 이 함수의 반환값은?
- 같으면 0
- 좌향이 작으면 < 0 ( 음수 )
- 좌향이 크면 > 0 ( 양수 )
- 음수도 반환하니(숫자도 반환하니) 반환형은 int
- 참/거짓 이런 의미가 아니라 문자열이 다른 것 보다 빨리 오냐 이거 표현하기 위함
int compare_string (const char * str0, const char * str1)
문자열 비교 알고리즘.
더 효율적인 문자열 비교 함수 작성하기
위 처럼 보면 if문 여럿 들어가고 복잡할 거 같다. 근데 생각보다 간단하게 작성 가능.
여기서 보아야 할 점은 비교할 떄, 첫번째 문자열이 끝까지 읽었냐? 널 문자냐?
방법 1
int compare_string(const char* str0, const char * str1)
{
while(* str0!= '\0' && * str0 == *str1)
{
++str0;
++str1;
}
return *str0 - *str1;
}
방법 2
int compare_string(const char* str0, const char * str1)
{
while(* str0!= '\0' && * str0 == *str1)
{
++str0;
++str1;
}
if(*str0 == *str1)
{
return 0;
}
}
더 효율적인 문자열 비교 함수 작성하기 2, strcmp()와 strncmp()
int compare_string(const char * str0, const char * str1)
{
size_t i;
size_t len0 = strlen(str0);
size_t len1 = strlen(str1);
if(len0 != len1)
{
for(int i = 0; i < min(len0, len1); ++i)
{
//달라지면 str[0] - str[1] 반환
}
//len0, len1에 따라 -1 또는 1 반환
}
for(int i = 0; i < len0; ++i)
{
//달라지면 str[0] - str[1] 반환
}
return 0;
}
이거 하는 순간 for문을 2번 더 돌린 것. 한번으로 가능한 알고리즘을 왜 굳이 저렇게? 데이터가 어떻게 저장되어 있는 지 알면 더 효율적으로 코드 작성 가능.
문자열 비교함수
int strcmp (const char * lhs, const char * rhs)
- 역시 이거 해주는 < string.h > 가 있음
#include <string.h>
const char * str1 = "AB";
const char * str2 = "AD";
int result = strcmp(str0, str1);
strncmp
int strncmp (const char * lhs, const char * rhs, size_t count)
- 최대 n문자 까지만 비교
- 종료 조건이 하나 더 추가될 뿐
- 이거를 거의 쓰는 일은 없긴 한데 만들라 하면 알아서 만들 수는 있어야 한다.
문자열 복사, strcpy(), strncpy()
문자열 복사
void copy_string(char * dest, const char * src)
{
while(*src!='\0') // 널문자가 아니면 계속 뒤로넣음. 최종 목적지 dest에 집어넣음.
{
*dest ++ = *src++; //dest의 위치, src의 위치를 계속 증가시키면서 dest에 넣는다.
//dest, src위치 변경하는 건 문제 없음.
}
*dest = '\0';
}
//다른 함수
const char* str1 = "Pope";
char str2[5];
copy_string(str2, str1);
문자열 복사 : strcpy(), strncpy()
char * strcpy(char * dest, const char* src)
- 역시 이걸 반환해주는 함수가 < string.h > 에 있음.
- 반환값 char * 는 dest를 반환
- 왜 그런지는 모르겠음.
- 실제 아무도 안 씀
C11에서 나온 strcpy_s() 는 error_t 반환
- error_t strcpy_s(char *restrict dest, rsize_t destsz, const char * restrict src); (c11)
- strcpy() 자체는 굉장히 많이 사용
그런데 dest 가 src보다 짧으면?
const char * str1 = "Pope"
char str2[3];
string_copy(str2, str1);
- 전에 스택에서 봤듯이 남의 메모리에 쓰는 건 문제.
- 위험한 함수라고 알려줌.
- 단, src와 dest의 크기를 확실하게 통제 가능하다면 안전
- C11에서 이보다 안전한 strcpy_s() 라는 함수가 나옴.
그렇다면 C89에서는 어떻게 해야 (그나마) 안전한가? => strncpy()
비교적 안전한 문자열 복사 : strncpy()
char * strncpy(char * dest, const char * src, size_t count);
- 최대 count만큼 복사
- 널 문자를 먼저 만나면 그 전에 끝냄
1 . src가 count 보다 짧으면 - 남은걸 다 0으로 채워줌.
2 . src가 count보다 길거나 같다면 - count 만큼 복사함 - 널 문자를 붙일 곳이 없음 - 따라서 안 붙여줌.
그래서 언제나 프로그래머가 이렇게 한 줄을 추가
strncpy(dest,src, DEST_SIZE);
dest[DEST_SIZE-1] = '\0'; //추가
마지막 요소, 복사 다 끝난 뒤, 마지막 요소를 무조건 널 캐릭터로 붙여주는 이런식의 코드가 많다. 이게 습관적으로 작성하는 코드 방식이다.
strcpy()보다는 strncpy()에서 더 중요한거.
- 왜 작동하지?
- 0이 앞에 붙으면 거기서 멈춤.
- 안 붙었으면 제일 마지막에 붙임.
정리 : strcpy() vs strncpy()
C11에서 이보다 안전한 strcpy_s(), strncpy()가 있음.
문자열 합치기, strcat(), strncat()
문자열 합치기 : strcat()
char * strcat(char * dest, const char* src);
- < string.h > 에 있음
- src의 문자열을 dest 뒤에 덧붙이는 함수
- dest의 널 문자가 들어있는 위치부터 src의 문자열 추가
- 바꿔 말하면 dest의 널 문자가 src[0]으로 교체
- 앞에거 strcpy()와 다른 점이 뭔가?
- dest의 길이가 충분해야 함.
- 길이를 넘어서 쓰는 경우 정의되지 않은 결과 발생
이 함수보다 좀 더 안전한 함수가 있다. -> strncpy()
문자열 합치기 : strncat()
char * strcat(char * dest, const char* src, size_t count);
- < string.h > 에 있음
- 최대 count개 만큼 src의 문자열을 dest 뒤에 덧붙이는 함수
- dest의 널 문자가 들어있는 위치부터 src의 문자열 추가
- 바꿔 말하면 dest의 널 문자가 src[0]으로 교체
실제로는 이런일이 일어나면 안됨(count+1만큼 덮어쓴다는 걸 보여주기 위한 예)
- dest의 길이보다 길게 쓰면 마찬가지로 정의되지 않은 결과 발생
- 그러나 count로 이러한 결과가 발생하지 않도록 프로그래머가 제어 가능
- 따라서 strcat보다는 조금 더 안전
#define DEST_COUNT(20)
const char* src= "hello"
char dest[DEST_COUNT] = "Hi";
strncat(dest,src, DEST_COUNT-strlen(dest) -1 );
정리 : strcat() vs strncat()
C11에서 이보다 안전한 strcat_s(), strncat_s() 함수가 있음.
문자열 찾기
존재하는 문자열을 찾을 경우
#include <stdio.h>
int main(void)
{
char msg[] = "I love string!, I love C, I love programming"
char * result = strstr(msg, "string");
printf("result : %d\n", result == NULL ? "(null)" : result );
return 0;
}
문자열 속에서 문자열 찾기
char * strstr(const char * str, const char * substr);
- < string.h > 에 있음
- 반환값 : char포인터
- substr이 str에 있다면: 해당 substr이 시작하는 주소
- substr이 str에 없다면: 널 포인터(NULL)
love 찾으면 거기 주소를 반환 102를 반환하고, 앞에서 원본 문자열에 const char 포인터를 쓰면 되게 애매하다고 했다.
고칠수 없는 데이터를 고칠수 없는 포인터(문자열)로 들어감. 근데 찾고 나올 때 이 포인터는 const가 아님. 그러면 이 매개변수를 const char로 하는 게 무슨 의미가 있는건가.(실제 함수는 char형인데)
그래서 const char*형을 매개변수로 받는게 조금은 이상하긴 하다.
문자열 찾기 함수가 메모리 주소를 반환하는 이유
근데 왜 C#의 string.IndexOf() 처럼 문자열의 색인을 반환하지도 않고, 그렇다고 찾은 위치부터 새로 만드는 것도 아닌 왜 찾은 위치의 주소를 알려주는 가?
- C이기 떄문
C는 새로운 문자 만들수 있는데 그걸 만드는 순간 메모리 관리 문제 있지, 실수할 문제 있지, 속도 저하 되지, 여러 문제들이 일어남.
왜 메모리 주소를 돌려주는가
- 새로운 문자열을 만들어서 반환할 경우, 메모리 관리 측면에서 효율적이지 못하고 실수할 수 있음.
어디에 그 새로운 문자열을 저장하는가
- 새 문자열을 반환하려면 메모리 “어딘가에” 그 문자열을 복사해야함
- 복사하는 위치가 스택이면
- 함수 끝나면 사라짐 -> 반환값이 더 이상 유효하지 않은 메모리 주소
char *strstr(const char* str, const char* substr)
{
//코드 생략
char result [] = str에서 찾은 substr부터 나머지; //string i love c
return result; //함수 끝나는 순간 이 주소는 유효하지 않음
}
char msg[] = "i love string! i love C"
char * result = strstr(msg, "string"); //유효하지 않은 주소받음
함수 내부에 배열 만들면 이 메모리가 함수 스택 프레임 안에 들어감 그래서 이걸 찾은 뒤 반환할 수 있음. 근데 반환하고 나가는 순간 이 주소는 유효하지 않게 됨. 더 이상 유효하지 않은 메모리 주소이기 때문에 쓴다는 것 자체가 위험.
복사하는 위치가 힙이면(동적 메모리 할당)
- 메모리 할당을 운영체제에 부탁해야 하므로 느리다.
- 그리고 더 이상 사용 안할 경우, 프로그래머가 직접 메모리 해제함수를 호출해야 하는데 깜빡 잊고 안할 수 있음(…) <- 다른 언어는 OS가 해줌
char * strstr(const char * str, const char* substr)
{
//코드생략
char * result = malloc(); //os에 메모리 요청 매우 단순화 시킨 예
//result 에 string i love C 복사
return result;
}
char msg[] = "I love string, I love C"
char * result = strstr(msg, "string") //유효한 정보를 받음
free(result);
주소 반환시 실수가 가장 적은 방법
- 그래서 그냥 원본에서 찾고자하는 문자열이 시작하는 주소를 반환하는 것으로 간단히 해결
- 추가적으로 메모리 쓰지도 않고, 사람이 저지를 수 있는 실수도 줄이기 가능
문자열 토큰화
- C에도 토큰화가 존재한다.
- 지금까지의 문자열 처럼 새로운 메모리를 할당하진 않음. 대신 우아하게 만들어 줌
토큰화 과정
- msg를 1번 토큰화 하면 첫 번째 토큰을 가져오라고 한다.
- 그 때 막 호출하는 함수가 strtok(), StringTokenizer.
input을 넣음. 그리고 구분문자 넣어줌. 그렇게 되면 처음 호출 시 token이 돌아오게 된다. 이 때 토큰의 위치가 문자열의 주소로 들어옴.
두 번쨰 토큰을 구하고 싶으면?
역시 strtok을 호출하는데, 여기는 NULL 을 넣어줌. 이게 무슨 의미?
- 이전에 니가 가진 문자열에서 거기서 다음 토큰을 내놔라
- 구분문자 대신에 널(\0)을 넣어줌. 그래서 there만 나오고
정리를 해보면..
char msg[] = "Hi, there, Hello" -> Hi\0 there\0 Hello\0 Bye\0
const char delims[] = ",. ";
char * token = strtok(msg, delims);
while(token!= null)
{
printf("%s\n", token);
token = strtok(NULL, delims)
}
- 토큰화 시작하려면 문자열 (msg)를 strtok에 넣음
- 그 msg의 다음 토큰을 구하려면 대신 NULL
- 더 이상의 토큰이 없다면, strtok()은 널을 반환
1 . 토큰화 하는 문자열은 const가 아니다. 원본이 바뀜.
2 . 함수 매개 변수로 NULL이 들어올 때, 그 전에 받은 msg를 사용하니 이건 어딘가에 저장이 되어 있어야 함.
- 함수 정적 변수가 제일 적합할 듯.
토큰화 어렵지 않다.
char* strtok(char * str , const char* delims)
- 이 함수도 혼자 작성할 수 있어야 함.
C11에 안전한 버전이 들어있음.
- strtok_s()
에 있는 문자열 함수들
- strlen()
- strcemp() / strncmp()
- strcpy() / strncpy()
- strstr()
- strcat() / strncat()
- strtok()
- 그 외 다수
C문자열 함수들의 특징
- 꽤 많은 함수들이 문자열을 절대 변경하지 않음.
- 그러면 매개변수에 뭘 붙여야 하나? -> const char*
- 문자열을 변경하더라도 원본은 변경 안하려 한다.
- 사본만 변경
- 예외: strtok()
- 다른 방법이 없음
- 원본 지키려면 호출 함수에서 사본 만든 뒤 strtok()을 호출해야 함.
- 절대 새로운 문자열, 즉, 연속된 메모리 char을 만들어 주지 않는다.
출력, 서식 지정(formatted) 출력, 서식 문자열(format string)
출력
- 프로그램에서 프로그램 외부로 데이터를 보내주는 행위
- 프로그램이 어떤 데이터를 출력할 지 아니까 괴상한 데이터들이 없다.
- 따라서 입력보다 쉽다.
서식 지정(form) 출력
- C에서 출력을 논할 때 가장 기본이 되는 함수
- 세 가지 종류
- printf(): 콘솔 창(stdout) 출력 <- 우리가 계속 써온 아이
- fprintf(): 스트림에 출력
- sprintf(): 문자열에 출력
- fprintf()와 sprintf()는 printf()하고 작동법 동일
- 첫 번쨰 매개변수로 ‘출력할 곳’을 넣어주는 게 유일한 차이점
const char * msg = "HELLO ";
fprintf(stdout, "%s\n", msg);
printf("%s\n", msg); //fprintf()와 동일한 결과
printf()의 첫 번째 매개변수는 문자열
- printf()는 그냥 int형 변수를 넣는다고 int를 출력해 주지 않음
- 함수 오버로딩 없음
- 따라서, printf() 무조건 첫번째 인자로 문자열을 받는다
서식 문자열(formating string)
printf("hello") //일반 문자열
printf("%d",score); //서식 문자열: 정수 출력
printf("%c", ch); //서식 문자열: 문자 출력
printf("%f", fi); //서식 문자열: 부동 소수점 출력
printf("%s", name); //서식 문자열: 문자열 출력
printf("Hello, %s\n Your score %d\n", name, score); //서식 문자열: 혼합 출력
- printf()는 일반 문자열 혹은 서식 문자열을 매개변수로 받음
- 서식 문자열
- %로 시작하는 문자열
- 소수점 이하 자리수, 자리수 정렬, 어떤 데이터(숫자, 문자를 출력할 지) 등을 알려주는 문자열
- 서식 문자열에는 하나 이상의 데이터가 들어갈 수 있음
- 이 때, 서식 지정자의 순서와 동일한 순서로 데이터들을 printf()의 추가 매개변수로 전달.
일반적인 서식 문자열 형식, 서식 지정자(format specifier)
서식 지정자(format specifier)
지정자 | 내용 | 출력 예 |
---|---|---|
% | ’%’를 출력 | printf(“%%\n”) |
c | 문자(char) 출력 | printf(“%c\n”,’D’) |
s | 문자열(char[]) 출력 | printf(“%s\n”, “LULU”) |
% | ’%’를 출력 | printf(“%%\n”) |
c | 문자(char) 출력 | printf(“%c\n”,’D’) |
s | 문자열(char[]) 출력 | printf(“%s\n”, “LULU”) |
- 서식 문자라고도 함.
- ’%’ 가 서식 문자열에서 사용중이므로, 출력하려면 어쩔수 없이 번복.
- 예전에 이스케이프 문자 때문에 \ 출력하려면 \넣은 것과 마찬가지
- “%s”쓸 바엔 그냥 문자열을 곧바로 printf() 해도 됨.
- 단 문자열 2개를 합치거나, 문자열 + 숫자 이런식으로 쓸 때 유용.
지정자 | 내용 | 출력 예 |
---|---|---|
d | 부호 있는 정수 출력 | printf(“%d\n”,’-10’) |
u | 부호 없는 정수 출력 | printf(“%u\n”,’10’) |
o | 부호 없는 정수를 8진수로 출력. 앞에 0x는 안 붙여 줌. | printf(“%o\n”,10) |
x | 부호 없는 정수를 16진수로 출력. 앞에 0x는 안 붙여 줌. | printf(“%x\n”,10) |
X | 부호 없는 정수를 16진수로 출력. 앞에 0x는 안 붙여 줌. | printf(“%X\n”,10) |
- %u에 부호 있는수를 넣을 경우, 해당 수의 비트패턴에 해당하는 부호 없는 수가 출력.
printf("%u\n", -10); //4294967286 출력
- %X는 있는데 %O는 없는 이유?
- 8진수는 숫자로만 이뤄져 있기 때문
지정자 | 내용 | 출력 예 |
---|---|---|
f | 부동 소수점 출력 | printf(“%f\n”,3.14) |
e/E | 부동 소수점을 지수 표기법으로 출력 | printf(“%e\n”,3.14) |
e/E | 부동 소수점을 지수 표기법으로 출력 | printf(“%E\n”,3.14) |
p | 포인터 값을 출력 | printf(“%p\n”,(void*)name) |
- “%p” 는 주소를 출력하는데 void* 만 받음.
- 모든 주소는 어차피 길이가 같다고 했으니 어떤 포인터를 void*로 캐스팅 해도 안전
- void*란?
너비
- 기본적으로 오른쪽 정렬이 됨.
- 그 밖의 너비 옵션은 레퍼런스 참조
플래그 1
지정자 | 내용 | 출력 예 |
---|---|---|
- | 왼쪽 정렬 | printf(“%-5d\n”,number) |
0 | 빈 공백을 0으로 채워줌 | printf(“%05d\n”,number) |
+ | 항상 부호(+,-)를 표시 | printf(“%+5d\n”,number) |
p | 양수인 경우에도 부호칸을 비워둠 | printf(“% d\n”,number) |
- ’-‘ : 기본은 오른쪽 정렬
- ’’: ‘+’가 있을 경우 무시됨
- ’+’: 기본은 음수 기호만 출력
- ‘0’: ‘-‘가 있을 경우 무시됨.
플래그 + 너비 예
플래그 2
’#’
- 어디에 붙이느냐에 따라 이미가 달라짐. 그나마 유용한 곳은 x나 X에 붙일 떄
- 레퍼런스 참조(https://en.cppreference.com/w/c/io/fprintf)
- “#x/X/o”를 쓸 때는 다른 플래그나 서식 지정자 d를 붙이지 않음.
- ’-‘는 붙일 수 있으나 의미 없음.
정밀도
- 서식 지정자 ‘f’와 함께 사용
- 최소 너비.소수점 아랫자리 수
- (소수점 포함) 원래 숫자의 너비보다 최소 너비가 크면 공백으로 채움.
- (소수점 포함하지 않음) 원래 숫자의 소수점 아랫자리 수 보다 소수점 아랫자리 수가 크면 0으로 채움
- 기본 소수점 아랫자리 수 : 6
정밀도2
- 서식 지정자 ‘s’와 함께 사용
- 최소 너비. 최대 너비
- 출력할 문자열의 길이가 최소 너비보다 작으면 왼쪽을 공백으로 채움.
- 출력할 문자열의 길이가 최대 너비보다 크면 자름
길이 수정자
길이 수정자 | 서식 수정자 | ||
---|---|---|---|
d | int | printf(“%d\n”,number) | |
l | d | long int | printf(“%ld\n”,number) |
f | double | printf(“%f\n”,number) | |
L | f | long double | printf(“%lf\n”,number) |
- 인자의 바이트 크기를 조정해준다.
- 몇 가지 있는데, ‘l’(소문자 L과) ‘L’만 그나마 유용할 듯
- 근데 최근 플랫폼은 별 의미 없음.
- int == long int, double == long double 인 경우가 보통
- 설사 위 경우 아니더라도 long double 잘 안씀
길이 수정자 예
int num = 100;
long int num2 = 100;
double num3 = 10.12345678
long double num4 = 10.12345678
printf("int : %d", size: %d\n", num1, sizeof(num1));
printf("long int : %ld", size: %d\n", num2, sizeof(num2));
printf("double : %f", size: %d\n", num3, sizeof(num3));
printf("long double : %Lf", size: %d\n", num4, sizeof(num4));
서식 문자열이 필요한 이유, fprintf(), stdout, 버퍼링, sprintf()
서식 문자열이 필요한 이유
- 일단 오버로딩 없음 -> printf(int), printf(char) 불가능
- 그리고 임시 문자열을 자동으로 생성 안해줌.
C#
console.writeLine("Hello, "+ name + "\nYour score is " +score);
C
printf("Hello," + name + "\nYour score is " +score); //컴파일 오류
- 물론 strcat()을 이용해서 프로그래머가 직접 임시 문자열을 관리할 수 있음.
- 하고 싶다면 그렇게 하면 됨.
즉, 서식 문자열은 추가 메모리 할당 없이, 있는 자료형을 출력 스트림에 문자들로 출력해줌.
- 그런데 여기서 C#의 문자열 포맷과 매우 비슷(문자열 보건 말고)
- 다 여기서 시작됨
- C는 C#의 조상님.
fprintf() 도 동잏하다.
- 단, 여기는 스트림을 사용
const char * name = "JAVA";
fprintf(stdout, "Hello %s\n" , name);
스트림이 뭔지는 배웠다.
스트림이란 실제의 입력이나 출력이 표현된 데이터의 이상화된 흐름을 의미한다.
즉, 스트림은 운영체제에 의해 생성되는 가상의 연결 고리를 의미하며, 중간 매개자 역할을 한다.보통 프로그래밍이 시작시 기본적으로 3개의 스트림을 줌.
- stdout(콘솔 출력)
- stdin(콘솔 입력)
- stderr(콘솔 출력. 하지만 오류 메시지를 출력하는 스트림)
이 3개의 스트림을 표준스트림(standard stream)이라고 함.
stdout
- 우리가 계속 봐온 프로그램.
- stdout은 보통 라인버퍼링 사용
- 버퍼링
- 출력할 내용이 어느정도 있어도, 곧바로 출력 하지 않고 쌓아둠.
- 어느정도 버퍼가 차면 그제서야 출력
- 라인 버퍼링
- 1 . 버퍼가 꽉 차거나
- 2 . 버퍼에 ‘\n’가 들어 있을 때
- 강제로 버퍼를 비우고 싶다면 fflush(stdout)을 호출하면 됨.
버퍼링의 종류
- 풀 버퍼링
- 버퍼가 가득 차면 비움. 라인 버퍼링과 마찬가지로, fflush()로 강제로 비울 수 있음.
- 라인 버퍼링
- 1 . 버퍼가 꽉 차거나
- 2 . 버퍼에 ‘\n’가 들어오면 버퍼를 비움
- 버퍼링 없음.
- 버퍼를 사용하지 않음.
- 표준에서 stdout, stderr, stdin의 버퍼링 종류를 지정하지 않음
- 구현에 따라 다를 수 있음.
fprintf()도 똑같다 2
- 우리는 출력에서 stdout, stderr을 쓰는 걸 봤다.
fprintf(stdout, "Hello");
fprintf(stderr, "error, pls enter number only");
- stdout/stderr이 같이 나오는데? 뭔 소용?
- 당연히 분리 하는 법 있음. -> 나중에 봄.
다른 스트림은 뭐가 있나?
- 모든 출력 스트림에 fprintf() 사용 가능.
- 첫 인자로 들어가는 스트림만 달라지고
- 나머지 매개변수는 그대로
C에서도 스트림은 여러개
- 파일 스트림
- C도 당연히 존재한다
- 문자열 스트림
- 놀랍게도 C엔 없다. 대신 sprintf()가 존재한다.
sprintf()도 있다.
int sprintf(char * buffer, const char* format, ...)
- 어디에 출력?
- 그냥 char 배열 밑에
- 이거 정말 많이 씀
- 심지어는 C++에서 String 클래스가 있음에도 이걸 대신 많이 씀
- 쓰는 이유? 속도 때문(가장 빨리 문자열을 조작하는 함수는 C언어)
- 다만 , 프로그래머가 충분히 큰 버퍼를 잡아주지 않으면 위험
char buffer[100];
int score= 100;
const char* name= "Rachael";
sprintf(buffer, "%s: %d", name, score);
printf("%s\n", buffer);
출력 함수의 안정성, 기타 출력 함수
char buffer[20];
const char* name = "Caterina Hassinger";
int score = 100;
sprintf(buffer, "%s's score : %d\n", name, score);
- 안전하지 않음
- strcpy(), strcat() 과 같은 이유.
그러면 ‘n’들어가는 안전한 함수가 있음.
C89에는 없음.
- C99에는 들어옴 sprintf();
- 대신 표준은 아니지만, 컴파일러마다 다르게 제공하는 함수가 있음.
- 예를 들어 vs의 _sprintf()
- 그러나 표준이 아니라서 작동하는 방법이 다름.(반드시 쓸 수 있는게 아니다.)
- snprintf()라는 함수도 존재한다.
기타 출력 함수
int puts(const char* str)
int fputs(const char* str, FILE* stream)
- puts()
- 문자열을 stdout에 출력
- 마지막에 줄도 바꿔줌: \n(마지막에 새 문자를 넣어준다는 의미)
put이 놓다의 의미니까 여기선 string을 출력한다의 의미.
- fputs(str, stdout)과 매우 비슷
- fputs()도 있는데?
- f가 보통 파일인데. 스트림 같은데..
- 이 puts는 fputs하고 string하고 stdout넣어주는 것과 결과적으로 행동은 같다.
int putchar(int ch)
int fputc(int ch , FILE * stream)
- putchar()
- 문자를 stdout에 출력
- 위에 행동 자체는 puts과 동일함.(char의 차이). 한 char만 출력해준다.
- fputc(ch, stdout)에 출력
- 얘들은 은근히 쓸일이 있다.
정리
- C 스타일 문자열
- 문자열의 표현 방법
- 다양한 문자열 함수들
- 안전한 문자열 처리
- 출력
- 서식 지정 출력
- printf(), fprintf(), sprintf()
- 서식 문자열 지정
- 기타 출력 함수