C & C++/디딤돌 C언어

Part 20. 문자열

언휴 2024. 1. 16. 10:58

Part 20. 문자열

68. 문자열

문자열 - C언어

프로그램을 작성하다 보면 문자열 데이터를 사용할 때가 많아요.
대부분의 프로그래밍 언어에서는 문자열을 관리하는 별도의 형식을 제공하지만 C언어는 문자열 형식을 제공하지 않아요.

그렇다고 C언어에서 문자열 데이터를 표현할 수 없는 것은 아니예요.
C언어에서는 문자열을 char 형을 원소로 하는 배열이나 char 형을 원소로 하는 포인터 형식으로 문자열을 표현할 수 있어요.
그리고 문자열 데이터를 표현할 때 쉽게 표현할 수 있게 쌍 따옴표를 사용하여 문자열을 표현할 수 있어요.

#define MAX_NAME_LEN 50
char name[MAX_NAME_LEN + 1] = "hello";
const char *str = "yahoo";

char 형식 원소로 배열을 선언하면 문자열을 구성하는 문자 데이터를 기억하는 메모리를 할당하여 조회 및 변경이 가능하죠.
하지만 char 형을 원소로 하는 포인터는 데이터를 기억하는 저장소의 역할을 하는 것은 아니예요.

예를 들어 char 형을 원소로 하는 두 개의 배열의 초기값을 “hello”로 설정하면 각각의 배열을 위해 할당한 메모리에 독립적으로 문자열을 설정해요.
그리고 각각의 배열의 원소에 접근하여 변경하는 것이 가능하죠.

하지만 두 개의 포인터 변수에 초기값을 “yahoo”로 설정하면 읽기 전용 메모리에 문자열을 배치한답니다.
그리고 두 개의 포인터 변수는 해당 메모리 주소를 값으로 갖는 것이죠.
이 때 읽기 전용 메모리에 할당한 문자열을 문자열 리터럴 상수라 부르며 내용을 변경할 수 없어요.
◈ 배열과 포인터를 이용한 문자열 사용

#include <stdio.h>
int main()
{
    char name1[6]="hello";
    char name2[6]="hello";
    const char *str1 = "yahoo";
    const char *str2 = "yahoo";
 
    printf("name1: %p name2:%p\n",name1,name2);
    printf("str1: %p str2:%p\n",str1,str2);
 
    name1[0] = 'y';
    //str1[0] = 'k'; //값을 변경할 수 없음
    printf("name1: %s name2: %s\n",name1,name2);
    printf("str1: %s str2: %s\n",str1,str2);
    return 0;
}

◈ 실행 결과

name1: 0023FD90 name2: 0023FD80
str1: 00EA5848 str2: 00EA5848
name1: yello name2: hello
str1: yahoo str2: yahoo

문자열 선언과 메모리
문자열 선언과 메모리

따라서 포인터로 문자열 변수는 char 형 배열을 인자로 전달받거나 문자열 리터럴 상수로 초기화할 때 사용하세요.

C언어에서 문자열을 형식으로 제공하는 것이 아니라서 문자열을 복사하거나 비교하는 연산자는 제공하지 않아요.
이 때는 char 형식을 원소로 하는 포인터 형식과 더하기 연산, 간접 연산, 인덱스 연산 등을 이용하여 처리하세요.
그리고 문자열 관련 라이브러리 함수를 이용하면 이러한 작업을 쉽게 할 수 있어요.
이들을 사용하려면 string.h 파일을 포함하세요.
이번 장에서는 문자열을 선언하고 관련 함수들을 사용하는 방법을 다루기로 할게요.

◈ 배열과 포인터를 이용한 문자열 사용

#include <stdio.h>
#include <string.h>
int main()
{
    char name[10]="";
    strcpy(name,"hello");
    printf("이름:%s\n",name);
    printf("문자열 길이:%d\n", strlen(name));
    if(strcmp(name,"hello")==0){    printf("차이가 없다.\n");    }
    else{    printf("차이가 있다.\n");    }
    return 0;
}

◈ 실행 결과

이름:hello
문자열 길이:5
차이가 없다.

69. 문자열 사용 기초

C언어에서 문자열은 아스키 코드의 나열이예요.
특히 아스키 코드 값이 0인 문자를 널문자(‘\0’)라고 부르는데 문자열은 널문자를 만나기 전까지의 아스키 코드 나열이예요.
따라서 문자열을 char 형식 원소 배열을 선언할 때 널문자를 포함하여 원소 개수를 정하세요.
이 책에서는 문자열을 배열로 관리할 때 원소 개수를 나타내는 부분을 MAX_NAME_LEN+1 처럼 표현하고 있어요.

#define MAX_NAME_LEN 50
char name[MAX_NAME_LEN+1] = {'a','b','c'};

그리고 C언어에서는 문자열 리터럴 상수를 초기화 구문에서도 사용할 수 있어요.

#define MAX_ADDR_LEN  100
char addr[MAX_ADDR_LEN+1] = "제주도 제주시 애월읍 고내리";

◈ char 형식 배열에 문자열 초기화

#include <stdio.h>
#define MAX_NAME_LEN      50
#define MAX_ADDR_LEN      100
int main()
{
    char name[MAX_NAME_LEN+1] = {'a','b','c'};
    char addr[MAX_ADDR_LEN+1] = "제주도 제주시 애월읍 고내리";
    printf("이름:%s\n",name);
    printf("주소:%s\n",addr);
    return 0;
}

◈ 실행 결과

이름:abc
주소:제주도 제주시 애월읍 고내리

문자열 리터럴 상수를 포인터 변수로 사용할 수도 있어요.
주의할 점은 포인터는 단순히 문자열이 있는 주소를 기억할 뿐이예요.
문자열을 변경하는 작업은 할 수 없어요.
따라서 문자열 데이터를 상수로 사용할 때는 const char *로 변수를 선언하세요.

const char *str = "안녕하세요.";

선언문에 const 키워드를 나타내어 상수화하는 것을 알고 있죠.
포인터 변수를 선언할 때 const 키워드는 여러 위치에 표현할 수 있으며 위치에 따라 의미가 달라져요.

만약 선언한 이름 앞에 const가 붙으면 포인터 상수를 선언한 것이 되며 값을 변경하지 못해요.

char * const str1 = name;
//str1++; str1의 값을 변경할 수 없음
str1[0] = 'k';

포인터 선언문의 지시 연산자 * 앞에 const가 붙으면 가리키는 곳의 값을 변경하지 못한다는 의미예요.

const char *str2 = name;
str2++;
//str2[0]='k';str2로 간접 연산이나 인덱스 연산으로 가리키는 원소의 값을 변경할 수 없음

참고로 const char *str2;와 char const *str2; 는 같은 표현이예요.

실제 프로그래밍을 하다보면 *앞에 const가 붙는 const char *str2;나 char const *str2; 와 같은 표현은 자주 만나요.
특히 함수의 입력 매개 변수로 사용할 때가 많은데 호출하는 곳에서 전달받은 문자열을 변경하지 않고 사용만 하겠다는 의미죠.

void View(const char *str)
{
    printf("%s\n",str);
}

70. 문자열 길이와 strlen함수

 

문자열 길이 ,C언어 strlen 함수

C 컴파일러에서는 문자열 데이터에 관한 여러가지 함수를 제공하고 있어요.
이러한 함수들을 사용하려면 string.h 파일을 포함하세요.

#include <string.h>

strlen 함수는 문자열 길이를 구하는 함수예요.

size_t strlen(const char *str);

입력 인자로 문자열을 const char * 형식 변수로 받고 문자열 길이를 size_t 형식으로 반환해요.
const char * 형식으로 받는 이유는 전달받은 문자열의 내용을 바꾸지 않겠다는 의미예요.
그리고 반환 형식인 size_t 형식은 부호없는 정수형으로 unsigned int 와 같은 표현이예요.
실제 헤더 파일에는 다음처럼 size_t를 typedef 문으로 정의하고 있어요.
typedef unsigned int size_t;
◈ strlen 함수 사용 예

#include <stdio.h>
#include <string.h>
#define MAX_NAME_LEN 50
int main()
{
    char name[MAX_NAME_LEN+1] = "hello";
    printf("%s 길이: %d\n",name, strlen(name));
    return 0;
}

◈ 실행 결과

hello 길이: 5

주의할 점은 한글은 ASCII 코드가 아니어서 한글 한 글자가 ASCII 코드 두 개의 글자로 취급해요.
C99 표준 이후에는 유니 코드를 취급할 수 있는 wchar_t 형식을 제공하고 있어요.
wchar_t 형식을 사용하면 개발자의 지역에서 사용하는 언어의 문자를 하나의 문자로 취급하죠.

wchar_t 형식을 표현할 때는 L’홍’처럼 대문자 L을 앞에 붙여서 사용하세요.
wchar_t 형식 문자열을 표현할 때는 L”홍길동”처럼 사용하죠.

그리고 locale.h 파일에서 제공하는 setlocale함수를 사용하면 printf 함수로 출력할 수 있어요.
setlocale(LC_ALL,”Korean”); 으로 지정하고 printf 함수에 %lc 와 %ls 포멧을 사용해서 출력하세요.

◈ 유니코드와 ASCII 코드

#include <locale.h>
#include <stdio.h>
#include <string.h>
int main()
{
    char c ='a';
    wchar_t wc = L'홍';
    char name[10]="홍길동";
    wchar_t wname[10]=L"홍길동";    
    setlocale(LC_ALL,"Korean"); //로케일 설정(지역 설정)
    printf("c:%c wc:%lc\n",c,wc);
    printf("name:%s wname:%ls\n",name,wname);    
    printf("name 길이:%d wname 길이:%d\n",strlen(name), wcslen(wname));
    return 0;
}

◈ 실행 결과

c:a wc:홍
name:홍길동 wname:홍길동
name 길이:6 wname 길이:3

71. 문자열 비교와 strcmp함수

문자열 비교, C언어 strcmp 함수

C언어에서 기본 형식은 비교 연산으로 값의 크기를 비교할 수 있죠.
그런데 배열은 메모리 주소를 값으로 갖기 때문에 비교 연산을 하면 메모리 주소를 비교해요.
개발자는 문자열의 내용을 비교하길 원하는데 메모리 주소를 비교하기 때문에 원하지 않는 결과가 나오지 않아요.
이는 프로그램 버그를 만드는 것이라 주의하세요.

◈ 비교 연산으로 문자열을 비교했을 때의 버그

#include <stdio.h>
#include <string.h>
#define MAX_NAME_LEN      50
int main()
{
    char name1[MAX_NAME_LEN+1] = "hello";
    char name2[MAX_NAME_LEN+1] = "hello";
    if(name1 == name2)
    {
        printf("%s와 %s는 같다.\n",name1,name2);
    }
    else
    {
        printf("%s와 %s는 다르다.\n",name1,name2);
    }
    return 0;
}

◈ 실행 결과

hello와 hello는 다르다.

이러한 문제점을 갖고 있어서 문자열을 비교는 표준 라이브러리 함수를 제공하고 있어요.

int strcmp(const char *str1, const char *str2);
int strncmp(const char *str1, const char *str2, size_t n);

두 가지 함수는 사전식으로 문자열을 비교하죠.
사전식 비교는 맨 앞의 문자부터 비교하는 것으로 차이가 생기면 더 이상 비교하지 않아요.
그리고 비교 결과는 차이가 없을 때 0, 앞쪽이 크면 양수, 뒤쪽이 크면 음수를 반환하죠.

◈ 문자열 비교

#include <stdio.h>
#include <string.h>
#define MAX_NAME_LEN      50
int main()
{
    char name1[MAX_NAME_LEN+1] = "hello";
    char name2[MAX_NAME_LEN+1] = "hello";
    if(strcmp(name1,name2) == 0)
    {
        printf("%s 와 %s는 같다.\n",name1,name2);
    }
    else
    {
        printf("%s 와 %s는 다르다.\n",name1,name2);
    }
    return 0;
}

◈ 실행 결과

hello와 hello는 같다.

◈ 부분 문자열 비교

#include <stdio.h>
#include <string.h>
#define MAX_NAME_LEN      50
int main()
{
    char name1[MAX_NAME_LEN+1] = "hello";
    char name2[MAX_NAME_LEN+1] = "hello world";
    if(strcmp(name1,name2) == 0)
    {
        printf("%s 와 %s는 같다.\n",name1,name2);
    }
    else
    {
        printf("%s 와 %s는 다르다.\n",name1,name2);
    }
    if(strncmp(name1,name2,5) == 0)
    {
        printf("%s 와 %s의 %d개의 문자는 같다.\n",name1,name2,5);
    }
    else
    {
        printf("%s 와 %s의 %d개의 문자는 다르다.\n",name1,name2,5);
    }
    return 0;
}

◈ 실행 결과

hello와 hello world는 다르다.
hello와 hello world의 5개의 문자는 같다.

72. 문자열 복사

문자열 복사, C언어 strcpy 함수

C언어에서 문자열을 char 형식을 원소로 하는 배열이나 포인터를 사용하죠.
특히 문자열을 변경하는 작업을 하려면 배열을 사용한다고 했어요.
그런데 C언어에서 배열 이름은 포인터 상수로 취급하여 대입 연산 좌항에 올 수 없어요.

배열이름은 포인터 상수로 l-value로 사용할 수 없습니다.
배열이름은 포인터 상수로 l-value로 사용할 수 없습니다.

C 언어에서 문자열 데이터를 복사할 때는 다음 함수를 사용할 수 있어요.

char *strcpy(char *dest, const char *src);
char *strcpy_s(char *dest, size_t size, const char *src);
char *strncpy(char *dest, const char *src, size_t n);
char *strncpy_s(char *dest, size_t size, const char *src, size_t n);

입력 인자는 문자열을 복사할 주소와 원본 문자열을 받아요.
문자열을 복사하기 위해서 첫 번째 인자는 const 키워드가 없어요.
반환 형식은 char *로 첫 번째 인자 그대로 반환한답니다.
printf(“%s\n”, strcpy(name,”hello”));처럼 연쇄 작업을 할 수 있게 반환는 것으로 다른 특별한 의미는 없어요.

그리고 strcpy 함수와 strncpy 함수는 src가 가리키는 문자열의 길이나 복사할 길이 n이 dest 버퍼 크기보다 크면 버퍼 오버플로우 버그가 발생할 수 있어요.
strcpy_s와 strncpy_s는 dest 버퍼의 크기를 두 번째 입력 인자로 받아요.
dest 버퍼 크기를 받아 내부에서 버퍼 오버플로우 버그가 발생하지 않게 개선한 함수예요.

◈ 문자열 복사

#include <stdio.h>
#include <string.h>
#define MAX_NAME_LEN      50
int main()
{
    char name1[MAX_NAME_LEN+1] = "hello";
    char name2[MAX_NAME_LEN+1] = "";
    char name3[MAX_NAME_LEN+1] = "";
 
    strcpy_s(name2,sizeof(name2),name1);
    printf("%s\n",name2);
    strncpy_s(name3,sizeof(name3),name1,3);
    printf("%s\n",name3);
    return 0;
}

◈ 실행 결과

hello
hel

strncpy 함수를 사용하면 복사한 문자 뒤에 널문자를 대입해 주지 않아요.
이러한 이유로 초기화하지 않고 사용하면 쓰레기 값이 남아있을 수 있으므로 주의하세요.

이 외에도 문자열을 사용하기 위한 다양한 함수들을 제공하고 있어요.

C언어 표준 라이브러리 함수를 참고하세요.

[C 언어 표준 라이브러리 함수 가이드]는 C언어 표준 라이브러리 함수에 관한 사용법을 다루고 있습니다.

73. 정리하기

1. 문자열 길이를 구하는 함수를 만드시오.
(strlen 함수와 같은 기능을 하는 함수 만들기)

더보기
//문자열 길이를 구하는 함수를 만드시오.
//(strlen 함수와 같은 기능을 하는 함수 만들기)
 
#include <string.h>
#include <stdio.h>
int mystrlen(const char *str);
int main(void)
{
    char arr[100];
    printf("문장 입력:");
    gets_s(arr, sizeof(arr));
    printf("<mystrlen 함수 이용>%s string length:%d\n", arr, mystrlen(arr));
    printf("<strlen 함수 이용>%s string length:%d\n", arr, strlen(arr));
    return 0;
}
int mystrlen(const char *str)
{
    const char *tstr = str;
    while (*str++);//참인 문자면 반복
    return str - tstr -1; //종료 문자가 있는 주소에서 원래 주소의 차 -1이 문자열 길이
}

2. 문자열 비교하는 함수를 만드시오.
(strcmp 함수와 같은 기능을 하는 함수 만들기)

더보기
//문자열 비교하는 함수를 만드시오.
//(strcmp 함수와 같은 기능을 하는 함수 만들기)
 
#include <stdio.h>
#include <string.h>
 
int mystrcmp(const char *str1, const char *str2);
void TestCase(const char *str1, const char *str2);
int main(void)
{
    char arr1[10] = "hello";
    char arr2[10] = "yahoo";
    char arr3[10] = "hello";
    
    TestCase(arr1, arr2);
    TestCase(arr1, arr3);
    TestCase(arr2, arr3);
    return 0;
}
void TestCase(const char *str1, const char *str2)
{
    int gap;
    gap = mystrcmp(str1, str2);
    if (gap == 0)
    {
        printf("%s와 %s는 서로 같음\n",str1,str2);
    }
    else
    {
        if (gap > 0)
        {
            printf("%s가 %s보다 사전식 비교에서 큽니다.\n", str1, str2);
        }
        else
        {
            printf("%s가 %s보다 사전식 비교에서 작습니다.\n", str1, str2);
        }
    }
}
int mystrcmp(const char *str1, const char *str2)
{
    while (*str1 && (*str1++ == *str2++));
    return *str1 - *str2;
}

3. 문자열 복사하는 함수를 만드시오.
(strcpy_s 함수와 같은 기능을 하는 함수 만들기)

더보기
//문자열 복사하는 함수를 만드시오.
//(strcpy_s 함수와 같은 기능을 하는 함수 만들기)
 
 
#include <stdio.h>
#include <string.h>
#include <assert.h>
 
char *mystrcpy_s(char *dest, size_t size, const char *src);
int main(void)
{
    char name[10] = "hello";
    char name2[10];
 
    mystrcpy_s(name2, sizeof(name2), name);
    printf("%s\n", name2);
    return 0;
}
 
char *mystrcpy_s(char *dest, size_t size, const char *src)
{
    char *temp = dest;
    if (strlen(src) >= size)//src문자열 길이가 size보다 크거나 같으면
    {
        assert(0);//예외 창 띄우기
    }
    while (*dest++ = *src++);
    return temp;
}