[CS] TIL 9. 문자열의 활용, 명령행 인자


  1. 문자열이 무엇인지 알아보고 특정분야의 문제야 사용할 수 있어야 한다.
  2. 문자열을 탐색하고 일부 문자를 수정하는 코드를 구현할 수 있다.
  3. 명령행 인자를 받는 프로그램을 C로 작성할 수 있다.


boostcourse모두를 위한 컴퓨터 과학 (CS50 2019) 강의를 듣고 정리한 필기입니다. 😀


1. 문자열의 활용

책의 독해 난이도를 판별하는 문제와 정보를 혼합해서 만드는 암호를 구현해야 하는 두 가지 문제가 있다. 이 두 문제 모두 문자열에 대한 이해가 필요하다. 이렇게 현실에서는 문자열을 활용해서 해결할 수 있는 문제들이 존재한다. 이것이 우리가 문자열을 활용하는 방법을 배워야 하는 이유이다.

1.1 문자열의 길이 및 탐색

사용자로부터 문자열을 입력받아 한 글자씩 출력하는 프로그램을 만들어보고자 한다.

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

int main(void)
{
    string s = get_string("Input: ");
    printf("Output:\n");
    for (int i = 0, ???; i < n; i++)
    {
    }
}

사용자에게 받은 문자열을 for 루프를 통해 문자열의 인덱스를 하나씩 증가시켜가면서 문자열을 출력할 것이다. 여기서 생각해야 할 것은 i를 어디까지 증가시켜야 하는가이다. 사용자에게 받는 문자열의 길이는 매번 다를 것이 아닌가.

첫 번째 방법은 특정 인덱스의 문자가 널 종단 문자(\n)와 일치하는지를 검사하는 것이다. 사용자에게 받은 문자열을 변수 s에 할당한다고 하자. 그러면, for루프는 for (int i = 0; s[i] != ‘\0’; i++) { ..}와 같은 형태가 될 것이다. 이것은 약간… 기계어에 가까운 방식이다. for루프의 가운데 있는 조건문을 블리언으로 표현하는 바람에 매번 물을 것이다. 그리고 함수를 호출하는 것을 시간이 걸린다. 컴퓨터는 매우 빠르기 때문에 아주 짧은 시간이 걸리겠지만, 굳이 같은 질문을 반복해야 할까?

두 번째 방법은 문자열의 길이를 사용하면 된다. 사용자에게 받은 문자열은 다시 받아오지 않는 이상 늘거나 줄지 않는다. strlen()는 문자열의 길이를 알려주는 함수로, string.h 라이브러리 안에 포함되어있다. 프로그래밍은 결국 추상화를 이용해서 복잡한 기계어를 간단하게 처리하는 것이기 때문에 strlen()을 사용해 컴퓨터에게 길이를 물어보면 된다. 일일이 널 종단 문자를 검사하는 것보다 훨씬 효율적일 것이다.

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

int main(void)
{
    string s = get_string("Input: ");
    printf("Output:\n");
    for (int i = 0, n = strlen(s); i < n; i++)
    {
        printf("%c", s[i]);
    }
    printf("\n");
}

for루프까지 완성하고 나면 위와 같은 코드가 된다. 특이한 점은 우리는 분명 string으로 변수를 선언했는데, 출력할 때는 %c를 사용한다. 이유는 간단하다, %c를 사용해서 한 번에 한 글자씩 출력하려는 것이다.

1.2 문자열 탐색 및 수정

이제 사용자에게 문자열을 입력받아 대문자로 바꿔주는 프로그램을 작성할 것이다.

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

int main(void)
{
    string s = get_string("Before: ");
    printf("After:  ");
    for (int i = 0, n = strlen(s); i < n; i++)
    {
        if (s[i] >= 'a' && s[i] <= 'z')
        {
            // convert to uppercase
        }
        else
        {
            printf("%c", s[i]);
        }
    }
    printf("\n");
}

역시나 사용자에게 문자열을 입력받고, 해당 문자열을 for루프로 도는 것까지는 똑같다. 이제 if() 조건문을 통해서 각 인덱스에 해당하는 문자가 ‘a’보다 크고 ‘z’보다 작은지 검사할 것이다. 즉, 소문자인지 대문자인지를 검사하는 것이다. 이것은 전에 배운 개념이다. 많이 사용하지는 않았지만 말이다. 문자열이 ‘a’보다 크고, ‘z’보다 작다면 해당 인덱스 글자는 대문자로 바꿀 것이다. 만약 그렇지 않다면, 아무것도 하지 않고, printf()로 그대로 출력하는 것이다.

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

int main(void)
{
    string s = get_string("Before: ");
    printf("After:  ");
    for (int i = 0, n = strlen(s); i < n; i++)
    {
        if (s[i] >= 'a' && s[i] <= 'z')
        {
            printf("%c", s[i] - 32);
        }
        else
        {
            printf("%c", s[i]);
        }
    }
    printf("\n");
}

이제 의사 코드로 남겨놓은 부분을 채우기 위해서는 ASCII 값을 생각해야 한다. 문자로 입력했다고 한들, 컴퓨터는 10진수로 바꾸고, 그것을 또 2진법으로 바꿔서 저장할 테니 말이다. 즉, 그 문자가 정의되는 ASCII 코드상에서의 숫자 값으로 비교할 수 있다는 말이다.

title

또한, 알파벳의 ASCII 값을 잘 살펴보면 각 알파벳의 소문자와 대문자는 32씩 차이가 나는 것을 확인할 수 있다. 따라서 각 문자가 소문자인 경우 그 값에서 32를 뺀 후 ‘문자’ 형태로 출력하면 대문자가 된다.

이는 잘 출력되지만, 기계어에 아주 가까운 형태로 구현한 것이다. 만약 우리가 구글 닥스나 워드에서 어떤 글을 대문자로 변환하면 컴퓨터는 모든 글자에 대해 뺄셈을 반복하면서 글을 수정하는 것이다. 하지만 이것은 여전히 개선의 여지가 보인다.

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

int main(void)
{
    string s = get_string("Before: ");
    printf("After:  ");
    for (int i = 0, n = strlen(s); i < n; i++)
    {
        printf("%c", toupper(s[i]));
    }
    printf("\n");
}

소문자를 대문자로 바꾸는 작업을 하는 toupper()이라는 함수가 ctype.h 라이브러리에 정의 되어있다. 이것을 활용하면 보다 간단하게 프로그램을 작성할 수 있다.



2. 명령행 인자

또 어떤 것을 할 수 있을까? 우리는 지금까지 몇 가지의 새로운 기능들을 배웠는데, 그 중 하나가 명령행 인자(command-line argument)를 사용해 새로운 프로그램을 구현해보고자 한다.

$ clang -o string string.c

명령행 인자의 대표적인 예로는 -o가 있다. clang이 기본적으로 a.out이라는 파일을 출력했는데, -o 원하는 파일명과 같은 명령어를 추가하면 원하는 파일 이름으로 출력할 수 있었다. 이것이 명령행 인자의 예시이다. 말 그대로 명령어 뒤에 쓰고, 프로그램의 입력과 같이 넣어주는 인자이다. 그래서 명령행 인자는 보통 실행하고자 하는 프로그램 뒤에 적는다.

#include <stdio.h>

int main(void)
{
    // ...
}

이게 무슨 관련이 있을까? stdio.h를 추가하는 것에 관해서 이야기한 적이 있다. stdio.h에는 printf()와 같은 함수의 프로토타입이 내장되어있고, 그 코드들이 파일의 전처리 과정에서 복사되어 우리가 작성한 프로그램에 옮겨지는 식이었다. 하지만 아직, int main(void)에 대해서는 이야기하지 않았다. 이제 우리가 여태껏 많이 사용해온 main(void)를 보다 자세히 들여다볼 때가 왔다. 사실 C에서는 main()의 괄호 안에 void만 적어야 하는 것은 아니다.

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

int main(int argc, string argv[])
{
    if (argc == 2)
    {
        printf("hello, %s\n", argv[1]);
    }
    else
    {
        printf("hello, world\n");
    }
}

놀랍게도 위 프로그램처럼 int argc, string argv[]를 적을 수 있다. main()도 그 형태를 보면 하나의 함수임을 알 수 있다. 하여 그동안 수업을 들으면서 작성했던 함수들처럼 매개변수를 가질 수있다는 말이다. 즉, main() 안에 기계적으로 void 라고 입력하지 않아도 된다.

여기서 첫번째 매개변수 argcint 변수로 main()이 받게 될 입력의 갯수이다. 그리고 두번째 변수인 string argv[]string이 아니라, string의 배열이다. argv는 인자 벡터를 말하는 관습적인 표현이다. 이게 배열이라는 것은 []를 보면 알 수 있다.

그렇다면 int argc, string argv[]void의 차이는 무엇일까? 전자를 사용하게 되면 우리가 사용자에게 입력을 받을 때, string 대신 실제 명령 프롬프트로 받을 수 있다. clang과 같은 기능을 구현할 수 있다는 말이다.

매개변수로 받는 argv의 0번째 인덱스는 기본적으로 프로그램의 이름으로 저장된다. 만약 하나의 입력이 더 주한다면 argv[1]에 저장될 것이다.

$ clang -o arg arg.c
$ ./argc

예를 들어 위 프로그램을 ‘arg.c’라는 이름으로 저장하고 컴파일 한 후 “./argc”로 실행해보면 “hello, world”라는 값이 출력된다. 명령행 인자에 주어진 값이 프로그램 이름 하나밖에 없기 때문이다.

$ clang -o arg arg.c
$ ./argc David

하지만 “./argc David”로 실행해보면 “hello, David”라는 값이 출력된다.명령행 인자에 David라는 값이 추가로 입력되었고, 따라서 argc 는 2, argv[1] 은 “David”가 되기 때문이다.




© 2020. by RIVER

Powered by RIVER