C, strcat, sprintf, strpbrk, wprintf, wcscpy, wcstombs, mbstowcs, atoi, atol, atof, time, localtime, ctime, srand, rand


이번에는 문자열 관련 함수와 유틸리티 관련 함수에 대해 알아보겠습니다.

모든 함수는 못살펴보겠지만, 중요하다고 생각되는(책에 나와있는)

함수를 중심으로 살펴보겠습니다.


문자열 함수


우리가 이미 살펴본

gets(), puts(), printf(), scanf()

함수들도 모두 문자열 처리 함수입니다.

이것들 이외의 문자열 처리 함수를 살펴보겠습니다.


strcat(), strncat()


두 함수 모두 두 문자열을 붙이는 함수입니다.

char *strcat(char *strDestination, const char *strSource);

  • 인자: 문자열을 이어서 저장할 메모리 주소(strDestination), strSource(추가할 문자열이 저장된 메모리 주소)
  • 반환값: strDestination 인자로 주어진 주소 반환

char *strncat(char *strDestination, const char *strSource, size_t count);

  • 인자: 문자열을 이어서 저장할 메모리 주소(strDestination), strSource(추가할 문자열이 저장된 메모리 주소), 추가할 문자열 길이(count)
  • 반환값: strDestination 인자로 주어진 주소 반환


참고로 strcat()는 메모리의 경계를 넘길 수 있고,

이로 인한 보안결함이 생길 수 있습니다.

예를 들어 strDestination의 길이가 10밖에 안되는데

strSource의 길이는 100인 경우,

strDestination에 길이 100의 문자열을 붙이려면

메모리 경계를 넘어서야만 하겠죠.

그래서 윈도우에서는 strcat_s()를,

Linux나 UNIX에서는 strncat()를 사용하는 것이 좋습니다.

다음은 간단한 예제입니다.

#include <stdio.h>

int main(void)
{
    char str1[100] = "Super ";
    char str2[100] = {0};
    
    printf("Super ???: ");
    gets(str2);
    putchar('\n');
    
    strcat(str1, str2);
    
    printf("str1: %s\n\n", str1);
    
    return 0;
}

/*
 Super ???: teemo
 
 str1: Super teemo
 */

그럼 잠시 strcat()가 어떤식으로 작동하는지 생각해보겠습니다.

strcat()는 str1에 이어서 str2를 붙이고 있습니다.

즉, 이어서 붙이려면 str1의 마지막 부분인 ‘\0’의 위치를 알아야합니다.

문자열의 마지막 위치를 알려면? 이전 챕터에서 살펴봤던 것처럼

while()문을 이용해 1byte씩 주소를 이동시키면서

해당 주소의 아스키코드가 ‘\0’인지 확인해야합니다.

결국 str1 문자열의 길이만큼 while()문이 돌아야한다는 의미입니다.


그렇다면 이 코드를 살펴보겠습니다.

// strcat() 여러번 쓰는경우 비효율!

#include <stdio.h>

int main(void)
{
    char str[100] = { "Teemo" };
    
    strcat(str, ", the ");
    strcat(str, "Swift ");
    strcat(str, "Scout");
    
    printf("str: %s\n\n", str);
    
    return 0;
}

/*
str: Teemo, the Swift Scout
*/

이번엔 str이란 문자열에 strcat()를 세번 써서

문자열을 이어붙이고 있습니다. 조심만 더 자세히 살펴보면

첫번째 strcat()에서는 str의 길이인 6번 while문을 돕니다.

두번째 strcat()에서는 12번을, 세번째 strcat()에서는 13번 돕니다.

뭔가 비효율적으로 느껴지는 것은 연속적으로 여러번 같은 문자열에 추가하는데

길이를 항상 처음부터 센다는 것입니다.

지금은 문자열의 길이가 짧으니 별 상관없지만,

strcat()을 매우 많이 하여 길이가 수십만이 되면

strcat()함수 한번 사용하는데 시간이 많이 걸릴 것입니다.


이점을 개선하여 CustomStrcat()함수를 만들어볼 수 있습니다.

// strcat() 함수 더 효율적으로 만들어보기

#include <stdio.h>

char* CustomStrcat(char* dest, char* src)
{
    while (*dest != '\0') dest++;
    while (*src != '\0') *dest++ = *src++;
    *dest = '\0';
    
    return dest;
}

int main(void)
{
    char str1[100] = { "Teemo" };
    
    char *str2 = NULL;
    
    str2 = CustomStrcat(str1, ", the ");
    str2 = CustomStrcat(str2, "Swift ");
    str2 = CustomStrcat(str2, "Scout");
    
    printf("str1: %s\n\n", str1);
    
    return 0;
}

/*
 str1: Teemo, the Swift Scout
 */

직접 만든 CustomStrcat() 함수에서는

리턴값이 문자열의 시작주소가 아니라 끝주소입니다.

그래서 이 함수를 쓰면, 쓸때마다 처음부터 문자열의 끝을 검색하지 않습니다.

그래서 첫번째 사용할 때는 6번, 두번째는 6번, 세번째는 7번 while문을 돕니다.

다만 주의할 점은 이렇게 만들어놓고, 두번째/세번째 사용할 때

첫번째 인자로 str1을 입력하면 그냥 기존의 strcpy()함수와 효율이 똑같아져버립니다.


sprintf()


sprintf()함수는 동적할당 챕터에서도 살펴본 함수입니다.

문자열을 콘솔 화면이 아니라 ‘메모리’에 출력하는 함수입니다.

// sprintf()

#include <stdio.h>

int main(void)
{
    char str1[100] = { 0 };
    char str2[100] = { 0 };
    
    printf("가장 강려크한 어벤져스 멤버: ");
    scanf("%s", str2);
    
    sprintf(str1, "The Strongest Avenger: %s", str2);
    putchar('\n');
    printf("str1: %s\n\n", str1);
    
    return 0;
}

/*
 가장 강려크한 어벤져스 멤버: Thor
 
 str1: The Strongest Avenger: Thor
*/

주의해야할 점은 보안결함이 있기 때문에

sprint_s()나 snprintf()함수를 사용하는 것이 좋습니다.


strpbrk()


이전에 살펴봤던 strstr()함수는 대상 문자열에서

특정 문자열을 검색하는 함수였습니다.

이 strpbrk()함수는 대상 문자열에서 특정 문자가 있는지 검색합니다.

char *strpbrk(const char *string, const char *strCharSet);

  • 매개변수: 검색 대상 문자열이 저장된 메모리 주소(string), 검색할 문자집합(strCharSet)
  • 반환값: 찾으면 해당 문자가 저장된 메모리 주소 변환, 못찾으면 NULL반환

예제를 살펴보겠습니다.

// strpbrk()

#include <stdio.h>

void main(void)
{
    // 검색 대상 문자열
    char src[100] = { 0 };
    // 찾을 문자
    char target[100] = { 0 };
    // 문자열 임시 저장 주소
    char *temp = src;
    
    printf("문자열을 입력하세요: ");
    //scanf("%s", src);
    // 그냥 scanf에서 %s로 쓰면 공백에서 끊김
    // 공백 포함해서 문자열 받으려면 아래와 같이
    // 아래의 뜻은 엔터(\n)을 받을때까지 계속 입력받겠다는 뜻
    scanf("%[^\n]", src);
    putchar('\n');
    printf("찾을 문자를 붙여서 입력하세요: ");
    scanf("%s", target);
    putchar('\n');
    
    while ((temp = strpbrk(temp, target)) != NULL)
    {
        printf("temp: %p\n", temp);
        printf("index: %d\n", temp - src);
        printf("찾은 문자: %c\n\n", *temp);
        
        temp++;
    }
}

/*
 문자열을 입력하세요: Super teemo
 
 찾을 문자를 붙여서 입력하세요: mp
 
 temp: 0x7ffeefbff562
 index: 2
 찾은 문자: p
 
 temp: 0x7ffeefbff569
 index: 9
 찾은 문자: m
*/

while문에서 strpbrk()가 조건식으로 사용되고 있습니다.

더이상 못찾을 때까지(NULL이 반환될때까지) 계속 검색합니다.

찾을 문자를 “mp”로 입력했더니, m이 있는곳과 p가 있는 곳의 주소를 반환합니다.


구문 분석용으로 strtok()함수라고,

문자열을 토큰화(여러 부분으로 잘라냄)하는 함수가 있는데

이 함수는 대상 메모리에 NULL을 삽입하고,

내부적으로 정적변수를 사용하기 때문에 멀티쓰레드 환경에서

문제가 발생할 수 있다 합니다.

저자님이 되도록 strtok()대신 strpbrk()를 사용하라고 권장합니다.




유니코드 문자열


C언어에서 문자열은 크게 두가지로 나눠집니다.

  1. MBCS(Multi-bytes Character Sets)
  2. 유니코드(UNICODE)

지금까지 우리가 써왔었던 문자열 방식이 MBCS입니다.

워드프로세서 자격증을 공부해봤다면 이런 말을 들어봤을 것입니다.

영어는 1byte, 한글은 2byte


유니코드 문자는 이런 문제점을 극복하는 방식입니다.

유니코드에서는 영문, 한글에 상관없이 필요한 메모리의 크기가

(문자열의 길이 + 1)*sizeof(wchar_t)

만큼 필요합니다.

다음 에제를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
    char str1[] = "Strongest";
    char str2[] = "어벤져";
    
    printf("str1: %s, sizeof(str1): %d\n", str1, sizeof(str1));
    printf("str2: %s, sizeof(str2): %d\n", str2, sizeof(str2));
    printf("sizeof(\"어벤져스\"): %d\n\n", sizeof("어벤져스"));
    
    wchar_t str3[] = L"Strongest";
    wchar_t str4[] = L"어벤져";
    
    printf("str3: %s, sizeof(str3): %d\n", str3, sizeof(str3));
    printf("str4: %s, sizeof(str4): %d\n\n", str4, sizeof(str4));
    
    
    return 0;
}

/*
 str1: Strongest, sizeof(str1): 10
 str2: 어벤져, sizeof(str2): 10
 sizeof("어벤져스"): 13
 
 str3: S, sizeof(str3): 40
 str4: \264\305, sizeof(str4): 16
*/

c_unicode

주석은 저의 맥 Xcode의 실행결과이고,

사진은 윈도우 Visual Studio 2017에서의 실행결과입니다.

일단 윈도우부터 보면 실제론 제 컴퓨터의 환경은 64비트이지만,

비쥬얼 스튜디오에서는 32비트로 실행이 기본 설정입니다.

그래서 MBCS 형식인 str1과 str2는

영어는 1바이트, 한글은 2바이트로 계산됩니다.

결과값은 문자열 마지막에 ‘\0’까지 고려한 크기입니다.

wchar_t는 유니코드 문자열 자료형입니다.

유니코드 자료형을 정의할 때는 문자열 앞에 L을 붙여주면 됩니다.

윈도우에서 유니코드 문자는 영어 한글 모두 한글자당 2바이트인걸 볼 수 있습니다.


반면 64비트인 맥 OS를 보면 MBCS형식일때

영어는 1바이트, 한글은 3바이트로 계산합니다.

하지만 유니코드 자료형에서는 한글, 영어 모두 한글자당 4바이트입니다.


그리고 한가지 더 확인해야할 게 str3의 출력입니다.

str3을 출력했는데 맨앞에 ‘S’만 출력이 됩니다.

이렇게 출력이 되는 이유는 유니코드 문자열의 저장 방식때문입니다.

c_mbcs_unicode_diff

맨앞에 S를 출력하자마자 ‘\0’을 만나기 때문에 문자열의 끝으로 인식합니다.


참고로 윈도우는 유니코드를 사용하는 운영체제입니다.

예를 들어서 MBCS문자를 출력하는 프로그램이 있다면

윈도우는 MBCS 문자열을 유니코드 문자열로 변환하여 처리합니다.

하지만 프로그램이 처음부터 유니코드 문자열을 사용하고 있다면

변환없이 바로 처리할 수 있습니다.

즉, MBCS 문자열을 유니코드 운영체제에서 사용하는 건

일을 두번하는 것과 마찬가지입니다.

하지만 현재 MBCS 문자열을 사용하는 프로그램을

한번에 모두 유니코드로 변경하는 건 불가능하기 때문에,

우리는 MBCS와 유니코드 문자열을 모두 다룰 줄 알아야합니다.


wprintf(), wcscpy()


유니코드 문자열은 유니코드 문자열 전용 함수를 사용해야 합니다.

유니코드용 printf()는 wprintf()이고,

유니코드용 strcpy()는 wcscpy()입니다.

간단한 예제를 살펴보겠습니다.

// wprintf(), wcscpy()

#include <stdio.h>

void main(void)
{
	wchar_t *str1 = L"Teemo";
	wchar_t str2[100];
	char *str3 = "Teemo";

	wcscpy(str2, str1);
	wprintf(L"str1: %s\n", str1);
	wprintf(L"str2: %s\n\n", str2);

}

wcscpy() 부분에 중단점을 찍고 디버깅을 통해

MBCS(str3)과 UNICODE(str1, str2)가

메모리에 저장되는 방식의 차이를 확인해볼 수 있습니다.

참고로 메모리1이 str1, 메모리2가 str2, 메모리3이 str3입니다.

c_mbcs_unicode_1

c_mbcs_unicode_2


wcstombs(), mbstowcs()


먼저 wcs와 mbs의 뜻에 대해 알아보겠습니다.

wcs는 Wide Character String의 약자로 유니코드 문자열을 의미합니다.

아까 영어 문자 하나에 mbcs에서는 1바이트였던게

유니코드에서는 2바이트로 좀 더 Wide 해졌죠?

mbs는 Multi Byte String으로 아까 위에서 설명한 MBCS라 생각하시면 됩니다.


즉, wcstombs()는 wcs를 mbs로 바꿔주는 함수이고

mbstowcs()는 mbs를 wcs로 바꿔주는 함수입니다.

size_t wcstombs(char *mbstr, const wchar_t *wcstr, size_t count);

  • 매개변수: MBCS 문자열 주소(mbstr), 유니코드 문자열 주소(wcstr), MBCS로 변환할 문자열의 최대 크기(count)
  • 반환값: MBCS로 변환된 문자열의 길이. 만약 mbstr이 NULL이면 변환을 위해 필요한 메모리 길이 반환

size_t mbstowcs(wchar_t *wcstr, const char *mbstr, size_t count);

  • 매개변수: 유니코드 문자열 주소(wcstr), MBCS 문자열 주소(mbstr), 유니코드로 변환할 문자열의 최대 크기(count)
  • 매개변수: 유니코드로 변환된 문자열으 길이. 만약 wcstr이 NULL이면 반환을 위해 필요한 메모리 길이 반환


다음은 wcstombs() 함수의 간단한 예 입니다.

// wcstombs(), mbstowcs()

#include <stdio.h>

void main(void)
{
	wchar_t *str1 = L"Teemo";
	char str2[100] = { 0 };
	size_t size = 0;

	// 변환된 문자의 길이 알아내기
	size = wcstombs(NULL, str1, 100);
	printf("size: %d\n\n", size);

	// 알아낸 문자열의 길이를 이용해 wcstombs()함수 사용하기
	size = wcstombs(str2, str1, size);

	printf("str2: %s\n", str2);
	printf("size: %d\n\n", size);

}




유틸리티 함수


유틸리티 함수는 프로그램을 개발하는 과정에서

자주 사용되는 다양한 기능을 함수로 구현해 놓은 것을 말합니다.

책에 나온 몇가지만 살펴보겠습니다.


atoi(), atol(), atof()


함수에서 a는 ASCII 코드, i는 int, l은 long, f는 double을 의미합니다.

즉, atoi()는 ASCII 코드 문자여로 된 숫자 문자열을 int로 바꿔주고,

atol()은 long으로 atof()는 double로 바꿔줍니다.

int atoi(const char *string);

  • 매개변수: 변환할 문자열 주소(string)
  • 반환값: 변환된 int값. 변환에 실패한 경우 0

long atol(const char *string);

  • 매개변수: 변환할 문자열 주소(string)
  • 반환값: 변환된 long값. 변환에 실패한 경우 0

double atoi(const char *string);

  • 매개변수: 변환할 문자열 주소(string)
  • 반환값: 변환된 double값. 변환에 실패한 경우 0.0

간단한 에제 코드를 살펴보겠습니다.

// atoi(), atol(), atof()

#include <stdio.h>
// atof()함수가 선언되어 있는 헤더 포함
#include <stdlib.h>

void main(void)
{
    char *str1 = "77";
    char *str2 = "7.77";
    char *str3 = "2147483648";
    
    int num1 = 0;
    long num2 = 0;
    double num3 = 0.0;
    int num4 = 0;
    
    num1 = atoi(str1);
    num2 = atol(str1);
    num3 = atof(str2);
    
    printf("num1: %d\n", num1);
    printf("num2: %d\n", num2);
    printf("num3: %f\n\n", num3);
    
    // int형 범위 넘어서는 수일때
    num4 = atoi(str3);
    printf("num4: %d\n\n", num4);
}

/*
 num1: 77
 num2: 77
 num3: 7.770000
 
 num4: -2147483648

*/

참고로 num4 의 경우엔

int의 최대값보다 더 큰 값을 매개변수로 넣은 경우입니다.

맥의 경우 2147483647(int의 최대값) + 1 의 값이 나왔지만

윈도우에서는 그냥 최대값인 2147483647이 나옵니다.


time(), localtime(), ctime()


위의 함수는 시간 관련 함수입니다.

참고로 컴퓨터에서는 UTC기준 시간을 사용합니다.

time_t time(time_t *timer);

  • 매개변수: 결과를 저장할 time_t 변수의 주소
  • 반환값: 1970년 1월 1일 자정부터 현재까지 흐른 시간을 초 단위로 반환

struct tm *localtime(const time_t *timer);

  • 매개변수: time()함수로 알아낸 시간 값이 저장된 변수의 주소
  • 반환값: tm 구조체 주소

char *ctime(const time_t *timer);

  • 매개변수: time()함수로 알아낸 시간 값이 저장된 변수의 주소
  • 반환값: 시간을 형식에 맞는 문자열로 변환하여 그 주소를 반환

아직 구조체에 대해선 살펴보지 않았지만

일단 간단한 코드를 보고 넘어가면 되겠습니다.

// time(), localtime(), ctime()

#include <stdio.h>
#include <time.h>

void main(void)
{
    struct tm *ptime = { 0 };
    time_t t = 0;
    
    t = time(NULL);
    ptime = localtime(&t);
    
    printf("t: %d\n", t);
    printf("ctime(&t): %s\n\n", ctime(&t));
    
    printf("현재 시간: %04d-%02d-%02d\n\n", ptime->tm_year+1900, ptime->tm_mon+1, ptime->tm_mday);
}

/*
 t: 1533910419
 ctime(&t): Fri Aug 10 23:13:39 2018
 
 
 현재 시간: 2018-08-10
 */

마지막 현재시간을 출력하는 구문을 조금 보면

ptime은 tm이라는 time.h에 선언되어 있는 구조체 변수입니다.

c_time_struct_tm

year을 보면 since 1900이라 되어 있습니다.

이게 출력문에서 연에 1900을 더해준 이유입니다.

mon을 보면 [0-11] 이라고 써져있습니다.

이게 출력문에서 달에 1을 더해준 이유입니다.


srand(), rand()


rand()는 난수(random number)를 발생시키는 함수입니다.

임의의 숫자 범위는 0~0x7FFF입니다.

참고로 0x7FFF는 32767입니다.

그리고 srand()는 rand()함수의 초깃값을 설정해주는 함수입니다.

rand() 함수도 어떤 정해진 기준에 따라 수를 반환하는데

그 기준에 매번 똑같다면 계속 똑같은 숫자만 반환할 것입니다.

그럼 랜덤한 수를 반환한다는 의미가 없어지겠죠.

그래서 rand()함수를 사용하기전에는 반드시 srand()함수로

rand()함수의 조깃값을 정해주어야 합니다.

// srand(), rand()

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void main()
{
    srand((unsigned)time(NULL));
    printf("RAND_MAX: %d\n\n", RAND_MAX);
    
    for (int i = 0; i < 10; i++)
    {
        printf("rand(): %6d\n", rand());
    }
    
    putchar('\n');
    
    for (int i = 0; i < 10; i++)
    {
        printf("rand()%%10: %6d\n", rand() % 10);
    }
    
    putchar('\n');
}

/*
 RAND_MAX: 2147483647
 
 rand(): 10307493
 rand(): 1439343091
 rand(): 1783530629
 rand(): 1222536777
 rand(): 52076543
 rand(): 1224613872
 rand(): 602073856
 rand(): 112353128
 rand(): 680896583
 rand(): 2035999265
 
 rand()%10:      7
 rand()%10:      2
 rand()%10:      5
 rand()%10:      3
 rand()%10:      9
 rand()%10:      6
 rand()%10:      2
 rand()%10:      2
 rand()%10:      4
 rand()%10:      2
*/

c_rand_result

주석의 결과값은 맥의 Xcode에서 실행한 결과이고,

그림의 결과값은 윈도우의 VS에서 실행한 결과입니다.

눈여겨 볼점은 RAND_MAX의 값이 다르다는 것입니다.

시스템 환경에 따라 다른것 같습니다.


srand()를 보면 매개변수로 time()함수가 들어가있습니다.

time()함수를 쓰면 실행을 시키는 그 당시의 시간이 들어가기 때문에

매번 실행할 때마다 다른 값이 들어가서

rand()함수가 사용하는 씨드값이 매번 달라질 것입니다.

기준이 매번 달라지니 값도 매번 달라져서 진정한 랜덤함수가 됩니다.


두번째 for문을 살펴보면 rand() % 10 을 사용하고 있습니다.

%는 나머지를 반환하는 연산자이기 때문에 항상 10미만의 수가 나올 것입니다.

즉, 0~9 사이의 랜덤한 숫자가 나오겠죠.

참고로 윈도우 기준으로 rand()가 반환하는 값의 범위가 0~32767 이기때문에

사실 엄밀하게 따지면, rand()%10 의 값은

0~7 사이의 숫자가 나올 확률이 8~9 사이의 숫자가 나올 확률보다 조금 더 높긴합니다.

하지만 뭐 그정도 미세한 차이정도야…


system()


system() 함수는 명령 프롬프트를 통해

명령을 내리는 것과 같은 기능을 제공합니다.

int system(const char *command);

  • 매개변수: 명령 콘솔에서 실행할 문자열이 저장된 메모리 주소(command)
  • 반환값: 성공하면 0이 아닌 값 반환. 에러가 발생하면 -1 반환
// system()

#include <stdio.h>
#include <stdlib.h>

void main()
{
	printf("윈도우에서 윈도우키+R 누른 후 실행할 프로그램 입력\n");
	printf("ex) mspaint(그림판), notepad(메모장)\n\n");

	char str[100] = { 0 };
	printf("프로그램명(ex: notepad): ");
	gets(str);

	system(str);
}

c_system


exit()


exit() 함수는 프로그램을 즉시 종료하는 함수입니다.

원래 C 언어에서 프로그램 종료는 main()함수의 반환을 의미합니다.

하지만 exit() 함수를 이용하면 이와 상관없이 즉시 종료됩니다.

void exit(int status);

  • 매개변수: 응용 프로그램의 종료 상태 값(status)
  • 반환값: 없음
// exit()

#include <stdio.h>

int main(void)
{
    char ch = 0;
    printf("바로 끝내거나 혹은 문자열 하나 출력하고 끝내거나...\n\n");
    
    printf("바로끝[Y] / 문자열 하나 출력[N] : ");
    ch = getchar();
    
    if (ch == 'Y' || ch == 'y') exit(1);
    
    printf("Teemo, the Swift Scout\n\n");
    return 0;
}

/*
 바로 끝내거나 혹은 문자열 하나 출력하고 끝내거나...
 
 바로끝[Y] / 문자열 하나 출력[N] : n
 Teemo, the Swift Scout
*/