본문 바로가기

CS50

3. 배열

모두를 위한 컴퓨터 과학(CS50 2019) 강의를 듣고 요약한 내용입니다. 해당 글에는 퀴즈에 대한 정답 및 풀이도 있습니다. 아직 퀴즈를 풀지 못하신 분들은 퀴즈를 풀고 난 후에 해당 포스팅을 읽으시길 바랍니다.

 

목차

1) 컴파일링

2) 디버깅

3) 코드의 디자인

4) 배열(1)

5) 배열(2)

6) 문자열과 배열

7) 문자열의 활용

8) 명령행 인자

1. 컴파일링

학습목표: 우리가 작성한 C코드를 실행하기 위해서는 컴파일링을 해줘야합니다. 텍스트 형식의 파일은 컴파일링 시 4단계를 거쳐서 컴퓨터가 해석 가능한 파일로 변환됩니다. 컴파일링의 네 단계를 설명할 수 있습니다.

 

#include <stdio.h>

int main(void)
{
  printf("hello, world\n");
}

 

- printf 함수를 사용하기 위해서는 stiod.h 라이브러리가 필요합니다. stdio.h는 헤더 파일로 C언어로 작성되어 있으며 파일명이 .h로 끝나는 파일입니다. 이 파일에는 printf 함수의 프로토타입이 있어서 Clang 컴파일러가 프로그램을 컴파일 할 때 printf가 무엇인지 알려주는 역할을 합니다.

- 코드를 clang hello.c로 컴파일하고 ./a.out 명령으로 프로그램을 실행할 때 이 과정은 컴퓨터가 이해하는 0과 1로 가득찬 파일 a.out을 생성하여 실행 가능하게 합니다.

 

- make나 clang을 사용해서 프로그램을 실행할 때 아래 네 개의 단계를 거칩니다.

※ make 명령어 자체는 컴파일러가 아니고, clang 이라는 컴파일러를 호출해서 C 소스 코드를 오브젝트 코드로 컴파일 하도록 합니다.

 

전처리(Precompile)

- 전처리기에 의해 수행됩니다.

- # 으로 시작되는 C 소스 코드는 전처리기에 실질적인 컴파일이 이루어지기 전에 무언가를 실행하라고 알려줍니다.

- 예를 들어, #include 는 전처리기에게 다른 파일의 내용을 포함시키라고 알려줍니다.

- 프로그램의 소스 코드에 #include 와 같은 줄을 포함하면, 전처리기는 새로운 파일을 생성합니다.

- 새로운 파일은 여전히 C 소스 코드 형태이며 stdio.h 파일의 내용이 #include에 부분에 포함됩니다.

 

컴파일(Compile)

- 전처리기가 전처리한 소스 코드를 생성하고 나면 그 다음 단계는 컴파일입니다.

- 컴파일러라고 불리는 프로그램은 C 코드를 → 어셈블리어라는 저수준 프로그래밍 언어로 컴파일합니다. 

- C 코드를 어셈블리 코드로 변환시켜줌으로써 컴파일러는 컴퓨터가 이해할 수 있는 언어와 최대한 가까운 프로그램으로 만들어 줍니다.

- 컴파일이라는 용어는 소스 코드에서 오브젝트 코드로 변환하는 전체 과정을 통틀어 일컫기도 하지만, 구체적으로 전처리한 소스 코드를 어셈블리 코드로 변환시키는 단계를 말하기도 합니다.

 

어셈블(Assenble)

- 어셈블러라는 프로그램이 어셈블리 코드를 → 오브젝트 코드(기계어)로 변환시키는 작업을 뜻합니다.

- 컴퓨터의 중앙처리장치가 프로그램을 어떻게 수행해야 하는지 알 수 있는 명령어 형태인 연속된 0과 1들로 바꿔주는 작업입니다.

- 소스 코드에서 오브젝트 코드로 컴파일 되어야 할 파일이 딱 한 개라면, 컴파일 작업은 여기서 끝이 납니다. 그러나 그렇지 않은 경우에는 링크라 불리는 단계가 추가됩니다.

 

링크(Link)

- 만약 프로그램이 (math.h나 cs50.h와 같은 라이브러리를 포함해) 여러 개의 파일로 이러우져 있어 하나의 오브젝트 파일로 합쳐져야 합다면 링크라는 컴파일의 마지막 단계가 필요합니다. 

- 링커는 여러 개의 다른 오브젝트 코드 파일을 실행 가능한 하나의 오브젝트 코드 파일로 합쳐줍니다.

- 예를 들어, 컴파일을 하는 동안에 CS50 라이브러리를 링크하면 오브젝트 코드는 GetInt()나 GetString() 같은 함수를 어떻게 실행할 지 알 수 있게 됩니다.

 

네 단계를 거치면 최종적으로 실행 가능한 파일이 완성됩니다.

2. 디버깅

학습 목표: 디버깅 하는 여러 방법을 설명할 수 있습니다.

 

버그와 디버깅

- 버그(bug)는 코드에 들어있는 오류입니다. 버그로 인해 프로그램의 실행에 실패하거나 프로그래머가 원하는대로 동작하지 않게 됩니다.

- 디버깅(debuggin)은 코드에 있는 버그를 식별하고 고치는 과정입니다.

 

디버깅의 기본

- 프로그래머는 디버거(debugger)라고 불리는 프로그램을 사용하여 디버깅을 합니다.

- 디버거는 프로그램을 특정 행(row)에서 멈출수 있게 해주어 버그를 찾는데 도움을 줍니다.

- 프로그램이 멈추는 특정 지점을 중지점이라고 합니다.

 

help50

 

int main(void)
{
  printf("hello, world\n");
}

 

- 위 코드를 make 프로그램을 이용하여 컴파일 해보면 "implicitly declaring library function 'printf'" 이라는 메세지가 나타납니다.

- 터미널에다가 help50 make 파일이름 코드를 실행하면 다시 컴파일 시 생기는 오류를 해석해줍니다.

 

3. 코드의 디자인

학습목표: 코드의 정확성과 디자인을 관리하는 방법을 설명할 수 있습니다.

 

4. 배열(1)

학습 목표: 배열을 정의하고 사용하는 방법을 설명할 수 있습니다.

 

메모리

- C에는 아래와 같은 여러 자료형이 있고, 각각의 자료형은 서로 다른 크기의 메모리를 차지합니다.

- bool: 불리언, 1바이트

- char: 문자, 1바이트

- int: 정수, 4바이트

- float: 실수, 4바이트

- long: (더 큰) 정수, 8바이트

- double: (더 큰) 실수, 8바이트

- string: 문자열, ?바이트

 

배열

- 배열은 같은 자료형의 데이터를 메모리상에 연이어 저장하고 이를 하나의 변수로 관리하기 위해 사용됩니다.

 

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

int main(void)
{
  // Scores
  int scores[3];
  scores[0] = 72;
  scores[1] = 73;
  scores[2] = 33;

  // Print average
  printf("Average: %i\n", (scores[0] + scores[1] + scores[2]) / 3);
}

 

- int scores[3]; 이라는 코드는 int 자료형을 가지는 크기 3의 배열을 scores 라는 이름으로 생성하겠다는 의미입니다. 

 

5. 배열(2)

학습 목표: 메모리상에서 여러 값을 연이어 저장하고 사용하는 방법과 그 이 점에 대해서 알아볼 수 있습니다. (배열을 정의하고 사용하는 방법을 설명할 수 있습니다.) 

 

전역 변수

- 아래 코드에서 scores 배열의 크기를 정해주는 N이라는 변수를 새로 선언하였습니다.

- 만약 N이 고정된 값(상수)이라면 그 값을 선언할 때 const를 앞에 붙여서 전역 변수, 즉 코드 전반에 거쳐 바뀌지 않는 값임을 지정할 수 있습니다.

- 관례적으로 이런 전역 변수의 이름은 대문자로 표기합니다.

 

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

const int N = 3;

int main(void)
{
  // 점수 배열 선언 및 값 저장
  int scores[N];
  scores[0] = 72;
  scores[1] = 73;
  scores[2] = 33;
  
  // 평균 점수 출력
  printf("Averge %i\n", (scores[0] + scores[1] + scores[2]) / N);
}

 

배열의 동적 선언 및 저장

- 아래 코드는 배열의 크기를 사용자에게 직접 입력 받고, 배열의 크기만큼 루프를 돌면서 각 인덱스에 해당하는 값을 역시 사용자에게 동적으로 입력 받아 저장합니다.

 

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

float average(int length, int array[]);

int main(void)
{
  // 사용자로부터 점수의 갯수 입력
  int n = get_int("Scroes: ");
  
  // 점수 배열 선언 및 사용자로부터 값 입력
  int socres[n];
  for (int i = 0; i < n; i++)
  {
    scores[i] = get_int("Score %i:", i + 1);
  }
  
  // 평균 출력
  printf("Average: %.1f\n", average(n, scores));
}

// 평균을 계산하는 함수
float average(int length, int array[])
{
  int sum = 0;
  for (int i = 0; i < length; i++)
  {
    sum += array[i]
  }
  
  // 괄호 안에다가 타입을 쓰면 **형변환** 의 의미
  return (float) sum / (float) length;
}

 

6. 문자열과 배열

학습 목표: 문자열이 C에서 정의되는 방식과 메모리에 저장되는 방식을 설명할 수 있습니다.

 

- 문자열(string) 자료형의 데이터는 사실 문자(char) 자료형의 데이터들의 배열입니다.

- string s = "Hi!"; 과 같이 문자열 s가 정의되어 있을 때, s는 문자의 배열이기 때문에 메모리상에 아래 그림과 같이 저장되고, 인덱스로 각 문자에 접근할 수 있다.

- 가장 끝의 '\0'은 문자열의 끝을 나타내는 널 종단 문자입니다. (널 종단 문자가 필요한 이유는 문자열의 끝을 구분하기 위해서 입니다)

7. 문자열의 활용

학습 목표: 문자열을 탐색하고 일부 문자를 수정하는 코드를 구현할 수 있습니다.

 

문자열의 길이 및 탐색

- 문자열의 끝을 알 수 있는 방법은 해당하는 인덱스의 문자가 널 종단 문자, 즉 '\n'와 일치하는 지 검사하는 것입니다.

 

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

int mian(void)
{
  string s = get_string("input: ");
  printf("Output: \n");
  
  for (int i = 0; s[i] != '\n'; i++)
  {
    printf("%c\n", s[i]);
  }
}

 

- 또 다른 방법으로는 stlent() 이라는 함수를 사용할 수도 있습니다.

 

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

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

 

- strlen 은 문자열의 길이를 알려주는 함수로, string.h 라이브러리 안에 포함되어 있습니다.

- 위 코드에서 n 이라는 변수에 s의 길이를 저장하고, 해당 길이 만큼만 for 루프를 순환합니다. 따라서 일일이 널 종단 문자를 검사하는 것보다 훨씬 효율적입니다.

 

문자열 탐색 및 수정

- 아래 코드는 사용자로부터 문자열을 입력받아 대문자로 바꿔주는 프로그램이다.

 

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

int mian(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\n", s[i] - 32);
    }
    else 
    {
      printf("%c", s[i]);
    }
  }
  printf(""\n);
}

 

- 문자의 대소비교가 가능한 이유는 ASCII 값, 즉 그 문자가 정의하는 ASCII 코드 상에서의 숫자값으로 비교할 수 있기 때문입니다.

- 또한 알파벳의 ASCII 값을 잘 살펴보면 각 알파벳의 소문자와 대문자는 32씩 차이가 남는 다는 것을 확인할 수 있습니다.

- 따라서 각 문자가 소문자인 경우 그 값에서 32를 뺀 후에 '문자'형태로 출력하면 대문자가 출력이 됩니다.

 

- 이와 동일한 작업을 수행하는 함수가 ctype 라이브러리의 toupper() 이라는 함수로 정의되어 있습니다.

 

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

int mian(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);
}

8. 명령행 인자

학습 목표: 명령행 인자를 받는 프로그램을 C로 작성할 수 있습니다.

 

- make나 clang과 같은 프로그램을 실행할 때 컴파일하고자 하는 코드 외에도 컴파일 후 저장 또는 실행 하고자 하는 파일명과 같이 추가적인 정보를 함께 줄 수도 있습니다. 이런 정보들을 명령행 인자라고 부릅니다.

 

#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");
  }
}

 

- 첫번째 변수 argc는 main 함수가 받게 될 입력의 개수입니다.

- argv[]는 그 입력이 포함되어 있는 배열입니다. 프고그램을 명령행에서 실행하므로, 입력은 문자열로 주어집니다. 따라서 argv[]는 string 배열이 됩니다. (argv 는 argument vectors의 약자로 관습적으로 쓰이고 있습니다)

- argv[0]는 기본적으로 프로그램의 이름으로 저장됩니다. 만약 하나의 입력이 더 주어진다면 argv[1]에 저장될 것입니다.

 

- 왜 main 함수는 리턴값이 있을까요? 그 리턴값은 int 일까요? C의 main 함수는 기본적으로 반환값을 가집니다. main 함수는 0를 반환합니다. 컴퓨터에서 0은 보통 문제 없음을 의미합니다. 0이 아닌 다른 값을 반환받았다면, 사람들이 뭔가 문제가 생기면 main 이 반환하도록 정한 임의의 숫자일 뿐입니다.

 

예를 들어,

- 위 프로그램을 "arg.c"라는 이름으로 저장하고 컴파일 한 후 "./argc" 로 실행해보면 "hello, world"라는 값이 출력됩니다. 명령행 인자에 주어진 값이 프로그램 이름 하나밖에 없기 때문입니다.

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

 

- 명령행 인자에 따라 하나의 프로그램에서 여러가지의 기능을 수행할 수 있다.(확장성 좋음)

 

'CS50' 카테고리의 다른 글

5. 메모리  (0) 2023.08.24
4. 알고리즘  (0) 2023.08.22
2. C 언어  (0) 2022.12.14
1. 컴퓨팅 사고  (0) 2022.12.13