[C] C lang 문자열

문자열의 표현과 길이

  • C에는 자체적으로 문자열이 없다. -> 그래서 char 형 배열을 계속 사용.
    • 물론 이걸 대신 문자열처럼 표현하는 방법들이 있다. 이게 C스타일 문자열.
    • C에선 가장 기본적인 것들이고, 면접 볼떄 이거 몰라서 떨어지는 사람들도 많다.
  • 가장 기본적인 내용 문자열 길이는 어떻게 구하나.

  • 기본 자료형 배울 때, 어떤 걸 가장 많이 봤나?
    • 자료형의 크기, 범위
    • 언제나 고정

문자열 길이는 정해져 있지 않다.

  • 따라서 하드웨어보고 문자열 하나 읽으라 하면 어쩔 줄 모름.
  • 이게 문자열이 기본 자료형이 아닌 이유.

  • 문자열은 여러가지 글자 (문자)들이 섞인 것.
  • 심지어 공백조차 글자.

  • “여러 개”의 문자를 표기하는 데 뭐가 좋을까?
    • 배열

  • 따라서 char str[ 글자수 ] 로 표현 가능

배열의 길이도 저장해야 할 것 같은데.

  • 앞에서 배열에서 배열의 길이 구할 수 없다고 했는데?
    • 맞다. 그래서 배열의 길이가 배열과 같이 저장이 안 됨.
    • 따라서 프로그래머가 배열의 길이는 따로 알고 있어야 한다.
      • 그 말은 따로 변수 만들어서 이 배열의 길이는 몇이다 이런거 주면 좋다.
      • 함수 호출시 배열 길이 같이 매개변수로 넘겨준다거나 했던거는 이미 했었다.
    • char str[] = “c is fun”;

20221019_130842



문자열 관리 시 길이의 문제

그럼 길이를 저장하는 변수만 있으면 되겠네?

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

  1. 길이를 배열 첫 위치에 저장
  • 첫 메모리 위치에 문자열 길이를 저장하고, 실제 문자열이 따라오게 함.
    1. unsigned char로 길이를 저장하기엔 너무 짧음. (255자)
    2. 길이는 int로 저장하고(4 byte), 그 뒤에 char로 문자들을 저장.(그럼 2의 32승까지니까 엄청 큰 숫자가 된다.)

20221019_133312

20221019_134449

장점
  • 첫 주소만을 보는 것 만으로 총 글자수가 몇인지 알 수 있다.



  • 다른 언어에서 문자열 크기를 바로 알 수 있었던 이유
  • 이런 방식으로 문자열의 길이를 저장해 두기 때문
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];

20221019_135314



문자열 길이 문제 해결방법 2, C 스타일 문자열

문자열이 끝나는 위치를 표시

  • 그냥 char[] 만 쓰되 그 문자열이 끝나는 위치에 특수한 문자를 두자.

    • 배열에서 값을 찾을 수 없으면 존재할 수 없는 색인 -1을 반환하는 방식과 마찬가지.
size_t index_of(const char* str, char c)
{
    //str 안에 c가 있으면 해당 색인
    //없으면 -1 리턴

    return -1;
}



아스키 코드 중 화면에 출력되지 않는 특수한 문자들이 있음.
  • 제어문자라 불림.
  • 0~31, 127
  • 그 중 하나가 0
  • 널 문자(null character, 널 캐릭터)라고 불림.
  • 널 포인터하고 다른 아이니 헷갈리지 말자.

20221020_122849

char null_char ='\0';
  • 0은 숫자 영
  • \는 이스케이프 문자
  • 근데 아스키코드로 0이니 char null_char = 0으로 작성 가능
  • 그러나 읽기 쉽게 ‘\0’으로 써주자.

C스타일 문자열이라 하면 널 문자로 끝나는 char배열을 말함.



C스타일 문자열

  • char[] 로만 구성
  • 문자열이 끝나는 곳에 널 문자를 붙임.

20221020_123509



char str1[] = "abc"; //스택에 abc 저장
char *str2 = "abc"; //데이터 섹션에 abc저장
  • 이 두 코드는 저장위치 이외에는 동일

    • 문자열 뒤에 별도로 ‘\0’을 넣지 않아도, 컴파일러가 알아서 해줌.

20221020_124043


단, 이 경우에는 ‘\0’을 넣어주지 않음

  • char str[] ={‘a’,’b’,’c’};

20221020_234858

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;
}

위 코드는 어떤가 확실한가? - 뭔 질문인지 모르면 다시 공부해야됨.(포인터를) 정수 훑으면서 합 구할때 나왔었다.

20221021_014205

왜 최선이 아닌가?

1번씩 돌 때 마다 시작주소 플러스 i 를 해야한다. 비효율 적일 수 있음.

앞에서 포인터 못 잡으면 문자열 해도 의미 없다.

그떄 개미만큼 더 효율적인 건 포인터 쓰는거라고 했다.

만약 코드에서 for문썼는데 효율적으로 될까요? 라고 물을때 이렇게 답하면 면접에서 만점인 것.

20221021_014933

둘 다 작동하는 방법

1은 str을 p에 두고 p를 그냥 증가시키는 거 (널 캐릭터 나올떄 까지) 그거와 시작위치와 널포인터 위치 구하고 뺴면 위치 나옴. = 그 사이 몇개 요소 있는 지 알려줌.

2는 카운터 변수 하나 두고 메모리 위치 옮길 떄 마다 카운트 하나 증가.

앞으로는 문자열 설명시 배열은 다른 언어에서나 하는거지 C에서는 포인터 방식.(배열을 포인터 접근하는 거 잊지 말자.)



사실 문자열 길이 구하는 함수는 있다.

  • size_t strlen(const char * str)

  • < stdio.h > 를 인클루드 하면 사용 가능
  • 우리가 만든 것과 똑같음.
  • 이 외에 다른 함수들도 존재.
  • 하지만 이 모든 걸 혼자 작성할 수 있어야 훌륭한 기본기 가졌다가 인정받음.




20221021_100414

즉, 안전하지 않을 수도 있음.

  • 외부에서 들어오는 문자열 읽을 떄, 조심해서 읽어야 함.
  • 입출력 할 때 더 자세히 봄.
  • 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)



문자열 비교 알고리즘.

20221021_111234



더 효율적인 문자열 비교 함수 작성하기

위 처럼 보면 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);

20221021_151042



문자열 복사 : 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);

20221021_154445


  • 전에 스택에서 봤듯이 남의 메모리에 쓰는 건 문제.
  • 위험한 함수라고 알려줌.
  • 단, 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 만큼 복사함 - 널 문자를 붙일 곳이 없음 - 따라서 안 붙여줌.

20221021_160001 20221021_160415


그래서 언제나 프로그래머가 이렇게 한 줄을 추가

strncpy(dest,src, DEST_SIZE);
dest[DEST_SIZE-1] = '\0'; //추가

마지막 요소, 복사 다 끝난 뒤, 마지막 요소를 무조건 널 캐릭터로 붙여주는 이런식의 코드가 많다. 이게 습관적으로 작성하는 코드 방식이다.

strcpy()보다는 strncpy()에서 더 중요한거.

  • 왜 작동하지?
    • 0이 앞에 붙으면 거기서 멈춤.
    • 안 붙었으면 제일 마지막에 붙임.

20221021_160918 20221021_160938



정리 : strcpy() vs strncpy()

20221021_161027

C11에서 이보다 안전한 strcpy_s(), strncpy()가 있음.



문자열 합치기, strcat(), strncat()

문자열 합치기 : strcat()

char * strcat(char * dest, const char* src);
  • < string.h > 에 있음
  • src의 문자열을 dest 뒤에 덧붙이는 함수
    • dest의 널 문자가 들어있는 위치부터 src의 문자열 추가
    • 바꿔 말하면 dest의 널 문자가 src[0]으로 교체
  • 앞에거 strcpy()와 다른 점이 뭔가?

20221021_161502

  • dest의 길이가 충분해야 함.
    • 길이를 넘어서 쓰는 경우 정의되지 않은 결과 발생

20221021_162234


이 함수보다 좀 더 안전한 함수가 있다. -> strncpy()


문자열 합치기 : strncat()

char * strcat(char * dest, const char* src, size_t count);
  • < string.h > 에 있음
  • 최대 count개 만큼 src의 문자열을 dest 뒤에 덧붙이는 함수
    • dest의 널 문자가 들어있는 위치부터 src의 문자열 추가
    • 바꿔 말하면 dest의 널 문자가 src[0]으로 교체

20221021_162822

실제로는 이런일이 일어나면 안됨(count+1만큼 덮어쓴다는 걸 보여주기 위한 예)

20221021_162908


  • 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()

20221021_163422

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;
}

20221021_171742



문자열 속에서 문자열 찾기

char * strstr(const char * str, const char * substr);
  • < string.h > 에 있음
  • 반환값 : char포인터
    • substr이 str에 있다면: 해당 substr이 시작하는 주소
    • substr이 str에 없다면: 널 포인터(NULL)

20221021_172652

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에도 토큰화가 존재한다.
  • 지금까지의 문자열 처럼 새로운 메모리를 할당하진 않음. 대신 우아하게 만들어 줌



토큰화 과정

20221021_180027

  • msg를 1번 토큰화 하면 첫 번째 토큰을 가져오라고 한다.
  • 그 때 막 호출하는 함수가 strtok(), StringTokenizer.

input을 넣음. 그리고 구분문자 넣어줌. 그렇게 되면 처음 호출 시 token이 돌아오게 된다. 이 때 토큰의 위치가 문자열의 주소로 들어옴.

두 번쨰 토큰을 구하고 싶으면?

20221021_180335

역시 strtok을 호출하는데, 여기는 NULL 을 넣어줌. 이게 무슨 의미?

  • 이전에 니가 가진 문자열에서 거기서 다음 토큰을 내놔라
  • 구분문자 대신에 널(\0)을 넣어줌. 그래서 there만 나오고

20221021_180426



정리를 해보면..

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)

출력

  • 프로그램에서 프로그램 외부로 데이터를 보내주는 행위
  • 프로그램이 어떤 데이터를 출력할 지 아니까 괴상한 데이터들이 없다.
  • 따라서 입력보다 쉽다.

20221023_205808



서식 지정(form) 출력

  • C에서 출력을 논할 때 가장 기본이 되는 함수
  • 세 가지 종류
    • printf(): 콘솔 창(stdout) 출력 <- 우리가 계속 써온 아이
    • fprintf(): 스트림에 출력
    • sprintf(): 문자열에 출력
  • fprintf()와 sprintf()는 printf()하고 작동법 동일
    • 첫 번쨰 매개변수로 ‘출력할 곳’을 넣어주는 게 유일한 차이점
const char * msg = "HELLO ";

fprintf(stdout, "%s\n", msg);
printf("%s\n", msg); //fprintf()와 동일한 결과



printf()의 첫 번째 매개변수는 문자열

20221023_214025

  • 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*란?



너비

20221024_094039

  • 기본적으로 오른쪽 정렬이 됨.
  • 그 밖의 너비 옵션은 레퍼런스 참조



플래그 1

지정자내용출력 예
-왼쪽 정렬printf(“%-5d\n”,number)
0빈 공백을 0으로 채워줌printf(“%05d\n”,number)
+항상 부호(+,-)를 표시printf(“%+5d\n”,number)
p양수인 경우에도 부호칸을 비워둠printf(“% d\n”,number)
  • ’-‘ : 기본은 오른쪽 정렬
  • ’’: ‘+’가 있을 경우 무시됨
  • ’+’: 기본은 음수 기호만 출력
  • ‘0’: ‘-‘가 있을 경우 무시됨.



플래그 + 너비 예

20221024_100812



플래그 2

  • ’#’

    • 어디에 붙이느냐에 따라 이미가 달라짐. 그나마 유용한 곳은 x나 X에 붙일 떄
    • 레퍼런스 참조(https://en.cppreference.com/w/c/io/fprintf)
    • “#x/X/o”를 쓸 때는 다른 플래그나 서식 지정자 d를 붙이지 않음.
      • ’-‘는 붙일 수 있으나 의미 없음.

20221024_100943

20221024_101454



정밀도

  • 서식 지정자 ‘f’와 함께 사용
    • 최소 너비.소수점 아랫자리 수
    • (소수점 포함) 원래 숫자의 너비보다 최소 너비가 크면 공백으로 채움.
    • (소수점 포함하지 않음) 원래 숫자의 소수점 아랫자리 수 보다 소수점 아랫자리 수가 크면 0으로 채움
    • 기본 소수점 아랫자리 수 : 6

20221024_101527

20221024_101741



정밀도2

  • 서식 지정자 ‘s’와 함께 사용
    • 최소 너비. 최대 너비
    • 출력할 문자열의 길이가 최소 너비보다 작으면 왼쪽을 공백으로 채움.
    • 출력할 문자열의 길이가 최대 너비보다 크면 자름

20221024_102132



길이 수정자

길이 수정자서식 수정자  
 dintprintf(“%d\n”,number)
ldlong intprintf(“%ld\n”,number)
 fdoubleprintf(“%f\n”,number)
Lflong doubleprintf(“%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");

20221024_133142

  • 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);

20221024_144122



출력 함수의 안정성, 기타 출력 함수

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()
    • 서식 문자열 지정
    • 기타 출력 함수








© 2021.03. by yacho

Powered by github