[C언어] #4 문자열 함수, 출력

Ilhoon·2022년 2월 17일
2
post-thumbnail

다른 언어들처럼 C에는 문자열 자료형이 없다.

그러나 C언어 문자열을 공부하면 다른 언어에서 사용하는 문자열 자료형의 내부 구현 알 수 있다.

C문자열 함수들의 특징

  • 메모리를 추가로 할당하지 않는다. (프로그래머가 직접 할당, 해제하는 것이 원칙)

문자열 표현

문자열은 char형 문자의 배열로 표현할 수 있다.

문자열 길이

문자열 길이를 C언어 내부적으로 저장해 놓지 않기 때문에 문자열 길이를 따로 저장해줘야한다.:question:

방법 1. 문자열의 첫 4바이트에 문자열의 길이를 저장해놓는다.

  • 다른 언어에서 사용하는 방식
  • C언어로 작성하기 애매하다 (첫 4바이트는 int형으로 읽고 다음부터는 char형으로 읽어야함)

방법 2. 문자열이 끝나는 위치에 특수한 문자를 둬서 구분하자.

  • NULL 문자 (아스키코드 0) = \0
  • C스타일 문자열 = NULL문자로 끝나는 char배열
  • ""로 선언하면 자동으로 배열의 맨 뒤에 \0을 같이 넣어준다.
  • 가장 최소한의 메모리를 사용하지만, 문자열의 길이를 알려면 문자열 끝까지 탐색해야한다.
char str1[] = "abc";			// 스택에 "abc" 저장
char* str2 = "abc";				// 데이터 섹션에 "abc" 저장, const 반드시 붙여야한다.
char str[] = {'a', 'b', 'c'}; 	// 문자열로 보지않는다. 그냥 char 배열

문자열 포인터로 선언된 문자열은 데이터 섹션에 저장된다. 데이터 섹션은 읽기전용 메모리이기 때문에 값을 변경할 수 없다.

C스타일 문자열 길이 구하는 방법

문자열의 길이 N, 배열의 길이 N+1(널문자 포함)

char str[4] = "1234";		// X
char str[4+1] = "1234";		// O (헷갈리지 않도록 +1을 코딩표준으로 사용하는 것도 좋다)

배열의 요소를 접근하여 NULL문자 전까지 하나씩 카운트 증가

char str[] = "qwer";

int i;
for (i = 0; str[i] != '\0'; i++){
}
printf("%d\n", i);

반복문의 회차가 마다 시작주소를 가리키고 있다가 +i를 해줘서 이동해준다.

반복문의 내부 동작을 살펴보면,

&str[0] + 0, &str[0] + 1, &str[0] + 2 ...

더 효율적으로 작성하려면 다음 회차 주소값 자체를 가지고 있다가 바로 그 주소로 접근하는 것이 쬐끔 더 나은 방식이다.

좀 더 효율적인 코드

  • 흔히 사용하는<string.h>헤더파일 strlen 함수 내부를 보면 거의 유사하게 구현되어있다
char str[] = "qwer";
char* ptr = str;
int cnt = 0;

while (*ptr != '\0'){
    ptr++;					// 다음 회차에 접근할 주소를 ptr이 가지고 있다.
}

printf("%ld", ptr - str);	// 배열의 마지막주소 - 배열의 첫번째 주소 = 그 사이 요소의 개수

문자열 비교

문자열 각 요소들을 아스키 코드값의 대소비교를 통해 비교할 수 있다.

A, B 문자열의 대소비교를 위해서 각각의 길이를 구하고, 여러번의 조건문을 작성하지 않고도 간결하게 아래와 같이 구할 수 있다.

<string.h> 헤더 파일 내부의 strcmp()함수를 보면 다음과 유사한 방식으로 구현되어있다..

int compare_string(const char* str0, const char* str1){
    
    // 널문자가 나오는 순간 비교 종료, *str1이 널문자라도 두번째 조건에 의해 반복문 종료
    while (*str0 != '\0' && *str0 == *str1){
		// 비교대상 문자열 하나씩 증가
        str0++;
        str1++;
    }
    
    if (*str0 == *str1){
        return 0;
    }
    // str0이 더 크면 1, 더 작으면 -1 
    return *str0 > *str1 ? 1 : -1;
}

int main(void){
    const char* msg1[] = "AB";
    const char* msg2[] = "AC";
    compare_string(msg1, msg2);
}

문자열 복사

역시 유사하게 구현된 <string.h> 내부에 strcpy()함수가 있음

주의해야할 점은 dest의 길이가 src보다 작다면? 유효하지 않은 메모리 주소에 값을 변경해버린다.

// src는 변하지 않는 원본이기 때문에 const, dest는 값을 변경해줘야되기 때문에 const없음
void copy_string(char* dest, const char* src){
	while (*src != '\0'){
        *dest = *src;
        src++;
        dest++;
    }
    *dest = '\0';
}

int main(void){
    char str1[] = "asdf";
    char str2[5];				/* 길이에 주의 */
    copy_string(str2, str1);
}

strncpy()

최대 복사할 문자열의 길이를 지정하여 비교적 안전한 문자열 복사 방법이다. (최종 목적지의 길이만큼 설정해주면 된다)

원본 길이가 지정한 길이보다 작다면 남은 크기를 모두 \0으로 채워준다.

원본 길이가 지정한 길이보다 길다면 지정한 길이만큼만 복사하고 끝. (마지막에 \0문자를 안 붙여준다.)

strncpy(dest, src, DEST_SIZE);
dest[DEST_SIZE - 1] = '\0';		// 마지막 요소에 널문자 추가해주는 부분

문자열 합치기

dest의 널문자 위치부터 src[0]을 추가해주는 방식이다.

역시 dest의 길이가 충분히 크지 않다면 유효하지 않은 메모리에 값을 덮어쓸 위험이 있다.

#include <stdio.h>
#include <string.h>
void concat_string(char* dest, const char* src){
  dest = dest + strlen(dest);		// dest의 마지막 위치로 포인터 이동
  
  while (*src != '\0'){
    *dest = *src;
    dest++;
    src++;
  }
  *dest = '\0';
}

int main(void){
    char str1[] = "qbcd";
    char str2[] = "efg";
    concat_string(str2, str1);
}

<string.h>strcat()함수가 위와 같은 방식으로 구현되어있음

// src의 문자열을 dest에 덧붙이는 함수
char* strcat(char* dest, const char* src);

strncat()

count 개수만큼 복사한 뒤 dest 널문자 위치에 붙여주고 마지막에 \0도 추가해준다.

count의 개수를 잘못 정의해서 dest의 길이보다 길게 쓰면 마찬가지 문제 발생할 순 있다.

char* strcat(char* dest, const char* src, size_t count);

문자열 찾기

<string.h>속에 strstr()함수

char* strstr(const char* str, const char* substr);

반환값은 char 포인터

  • 값을 찾았다면 해당 값이 시작하는 주소값
  • 값을 못찾았다면 NULL 포인터

왜 주소값을 반환하지?

  • 문자열을 리턴하려면 ?
    • 함수 내부에 문자열 char []선언해서 리턴할 시 함수 종료되는 순간 유효하지 않은 주소가 되버림.
    • 함수 내부에 동적메모리 할당해서 리턴할 시
      • 속도저하
      • 함수 사용자 입장에서 내부적으로 동적메모리 할당되는지 알 수 없다. 메모리 해제를 안할 가능성이 높고 메모리 누수 문제가 생긴다.
  • 따라서 원본 주소값을 반환하는 것이 가장 실수가 적은방법

문자열 토큰화

메모리를 추가로 할당하지 않으면서 토큰화 하는 방법

  • 토큰 문자를 \0으로 바꾸는 것만으로 가능하다.

<string.h> 내부에 strtok()함수

char* strtok(char* str, const char* delims);
char msg[] = "Hi, there. Hello. Bye"; // const 쓸수 없다, 포인터 배열 안된다. 토큰 문자를 \0으로 변경할 거다.
const char delims[] = ",. ";

char* token = strtok(msg, delims);
while(token != NULL){
    printf("%s\n", token);
    token = strtok(NULL, delims); // msg의 다음 토큰을 구하려면 NULL을 입력
}

함수 매개변수로 NULL이 들어올 때 그 전에 받았던 msg를 사용하니 어딘가 저장하고 있어야 하지 않을까?

  • 함수 내부의 정적변수 (static : 함수가 사라져도 날아가지 않는다. 데이터섹션에서 주소를 저장하고 있다가 토큰끝나면 주소 변경)

서식문자열

서식 문자열이 필요한 이유?

  • 함수 오버로딩이 없다. printf(int), printf(char) 불가능하다.
  • string + string 같은 연산을 하려면 내부적으로 임시 문자열이 생성되어야 하는데 C는 그런거 안됨.

sprintf()

char 배열에 문자열을 출력해주는 함수

C++ 등에서도 속도 때문에 많이 쓴다.

int sprintf(char* buffer, const char* format, ...)

버퍼의 사이즈를 충분히 넉넉히 잡아줘야 한다.

char buffer[100]; //buffer사이즈가 작으면.. 메모리가 잘못 덮어씌워진다.
int score = 100;
const char* name = "rachel";

sprintf(buffer, "%s: %d", name, score);

printf("%s\n", buffer); // -> rachel: 100
profile
꾸준하게!

0개의 댓글