문자열이란

우리가 앞선 챕터에서도 배웠듯 문자열은 일종의 배열이다.
그렇다면, 포인터를 사용할 때에 배열의 이름은 포인터처럼 사용 가능했는데 문자열도 가능할까?
정답은 Yes이다.

c언어에서 string정의하기

원칙적으로 c언어에는 우리가 생각하는 string이 존재하지 않는다.
우리가 앞에서 문자를 담기 위해 char을 문자열의 길이만큼 선언했던 것도 바로 이 때문이다.
그래서 cs50라이브러리에서는 typedef라는 기능을 사용하여 문자를 편하게 사용할 수 있도록 string을 만들어주었다.

string만들기

typedef char* string;

참고로 대부분의 c언어 개발자들은 이 typedef를 include나 define 바로 하단 즉 main함수의 시작 이전에 둔다.
그 이유는 main이 아닌 다른 곳에서도 내가 선언한 자료형을 편하게 사용하기위해서이다.
typedef는 일종의 기존 자료형에서 내가 편한 부분을 떼와서 별칭을 정하여 사용하는 것으로 위 기능을 사용하여 c언어에서도 pair을 정의한다거나 하는 것도 가능해진다.
그러나, c언어에서는 원칙적으로 메모리 관리를 위하여 string을 저런 식으로 사용하는 것보다는 char배열을 사용하는 것을 권장한다.

관리하기

여기에서는 대충 99글자까지만 들어가도록 설계하였으나, 평소에는 들어오는 글자 크기+1만큼만 받을 수 있도록 하면된다.
또는 최대의 크기를 정한 뒤에 그 크기를 넘는지 아닌지에 대해서만 확인하면 된다.

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

typedef char* string;

int main() {
    string myString;

    // 사용자로부터 입력 받을 문자열의 최대 크기
    size_t maxSize = 100;

    // 동적으로 메모리 할당
    myString = (string)malloc(maxSize * sizeof(char));

    if (myString == NULL) {
        fprintf(stderr, "메모리 할당 오류\n");
        return 1;
    }

    // 문자열 입력 받기
    printf("문자열을 입력하세요: ");
    if (scanf_s("%s", myString, (unsigned)maxSize) != 1) {
        fprintf(stderr, "입력 오류\n");
        free(myString);
        return 1;
    }

    // 출력
    printf("입력한 문자열: %s\n", myString);

    // 메모리 해제
    free(myString);

    return 0;
}

cs50 라이브러리에 관리하기

여기에서는 이미 자료형을 만들어주었기 때문에 보다 편하게 자료의 출력이 가능하다.

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    string s = "EMMA";
    printf("%s\n", s);
}

문자열 비교하기

c언어에서 문자열을 비교하기 위한 가장 대표적인 방법은 for문을 통해 브루트포스로 문자열의 길이만큼 돌면서 비교를 하는 것이다.
여기에서 유의할 것은 만약 주소값을 사용하여 비교하는 방식을 쓴다면, 문자열의 주소값 자체를 비교하는 게 아니라 그 내부의 데이터를 참조하여 비교해야한다는 점이다.

코드1(라이브러리 사용)

string.h헤더에 있는 유용한 내장함수들 중 하나를 사용하는 방식이다.

#include<stdio.h>
#include<string.h>
const int MAX = 101;

int main() {
    char s1[MAX];
    char s2[MAX];

    scanf_s("%s %s", s1, (unsigned)(sizeof(s1) / sizeof(s1[0])), s2, (unsigned)(sizeof(s2) / sizeof(s2[0])));
    s1[sizeof(s1) / sizeof(s1[0]) - 1] = '\0';
    s2[sizeof(s2) / sizeof(s2[0]) - 1] = '\0';

    int result = strcmp(s1, s2);
    if (result == 0) {
        printf("두 문자열은 같습니다\n");
    }
    else {
        printf("두 문자열은 다릅니다\n");
    }

    return 0;
}

코드에 대해 간략하게 설명하자면, scanf_s를 통해 크기 제한을 주어, 혹시나 모르는 오버플로우를 방지하도록 해주었고(필수 아님) 이후 문자열이 101보다 짧을시 무조건 문자가 들어간 끝 부분을 널 문자로 바꾸어주는 작업을 진행하였다.

2번 방법- 직접 구현하기

위에서 작성한 것과 같이 for문을 통해 직접 구현하는 방식이다.
나의 경우 if문을 추가하여 길이가 다르다면 바로 문자열의 길이가 다름을 알려주도록 코드를 작성하였다.

#include<stdio.h>
#include<string.h>
const int MAX = 101;

int main() {
    char s1[MAX];
    char s2[MAX];

    scanf_s("%s %s", s1, (unsigned)(sizeof(s1) / sizeof(s1[0])), s2, (unsigned)(sizeof(s2) / sizeof(s2[0])));
    s1[sizeof(s1) / sizeof(s1[0]) - 1] = '\0';
    s2[sizeof(s2) / sizeof(s2[0]) - 1] = '\0';
    int len1 = strlen(s1);
    int len2 = strlen(s2);
    if (len1 != len2) {
        printf("두 문자열은 길이부터 다릅니다");
    }
    else {
        for (int i = 0; i < len1; i++) {
            if (s1[i] != s2[i]) {
                printf("두 문자열은 다릅니다");
                return 0;
            }
        }
        printf("두 문자열은 일치합니다");
    }
    return 0;
}

문자열 복사하기

문자열을 복사할 때에는 몇가지 유의점이 있다.
우선 c언어에는 깊은 복사와 얕은 복사의 개념이 존재하는데, 문자열을 등호로 가져온 뒤 하드코딩하여 복사하면 얕은 복사가 이루어진다.
그래서 원본 문자열을 해치는 사태가 일어나기 때문에, 원본을 보존해야하는 경우 라이브러리를 쓰거나 malloc을 사용해야한다.

라이브러리 사용하기

나는 c언어든 c++든 있는 기능은 모조리 외워서라도 최대한 기계에게 맡기자 주의이기 때문에 강의에는 나와있지 않은 라이브러리들을 가져다 쓰는 경우도 많다.

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
const int MAX = 101;

int main() {
    char s1[MAX];

    scanf_s("%s", s1, (unsigned)(sizeof(s1) / sizeof(s1[0])));
    s1[sizeof(s1) / sizeof(s1[0]) - 1] = '\0';
    char* s2 = (char*)malloc(strlen(s1) + 1);
    if (s2 == NULL) {
        printf("메모리 할당 이상");
        return 0;
    }
    strcpy_s(s2,strlen(s1)+1,s1);
    printf("%s", s2);
    free(s2);
    return 0;
}

왜 그런지 모르겠으나, 비주얼 스튜디오에서는 strcpy_s대신 scrcpy를 쓰면 에러가 나서(scanf와 scanf_s와 비슷한 이슈로 추정)_s를 사용하고 저렇게 길이를 지정해주었다.
주의할점은 길이가 아니라 sizeof로 할당을 하면 문제가 생길 수 있는데 이는 sizoef의 경우 크기를 반환하다보니 널문자까지 포함을 하기 때문이다.
저 라이브러리를 사용하면 알아서 기계가 깊은 복사를 진행해주기 때문에 얕은 복사로 인한 오류는 사라지게 된다.

강의에 나오는 방식

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<ctype.h>
const int MAX = 101;

int main() {
    char s1[MAX];

    scanf_s("%s", s1, (unsigned)(sizeof(s1) / sizeof(s1[0])));
    s1[sizeof(s1) / sizeof(s1[0]) - 1] = '\0';
    char* s2 = (char*)malloc(strlen(s1) + 1);
    if (s2 == NULL) {
        printf("메모리 할당 이상");
        return 0;
    }
    memset(s2, 0, strlen(s1) + 1);
    for (int i = 0; i < strlen(s1); i++) {
        s2[i] = toupper(s1[i]);
    }
    s2[strlen(s1)] = '\0';//강제로 null주입
    printf("%s", s2);
    free(s2);
    return 0;


}

그런데 지금 이 코드가 조금 이상한 것은, 분명 malloc으로 일부로 크기도 널 문자를 받을 수 있도록 해주었고, memset을 통해 초기화까지 하였는데 왜 버퍼 오버런에 대한 경고가 뜨는지를 모르겠다.
그래서 혹시나 하는 마음에 s2마지막 끝점에 무조건 null을 주입시켰으나 오류가 사리지지를 않았다.
차후 이 부분에 대해서는 알게 되면 정정하는 글을 작성하도록 하고, 일단 malloc을 쓴 경우 메모리를 절약하기 위해 그 변수를 쓸 일이 사라지면 free를 해주어야 하기에 나는 prtinf이후 free를 해주었다.

깊은 복사와 얕은 복사

#include <cs50.h>
#include <ctype.h>
#include <stdio.h>

int main(void)
{
    string s = get_string("s: ");
    string t = s;

    t[0] = toupper(t[0]);

    printf("s: %s\n", s);
    printf("t: %s\n", t);
}

위 코드는 얕은 복사를 진행하기 때문에 원본 문자열을 해치게 되는 것이다.
s를 t가 참조하는 과정에서 s의 주소값을 불러들였고 그 때문에 원본 주소값 안에 있는 데이터가 잘못된 값으로 덮어씌워진 것이러고 이해하면 쉽다.

댓글남기기