2 분 소요

❄️ David Malan 교수의 모두를 위한 컴퓨터 과학(CS50 2019)을 바탕으로 정리한 내용입니다.


지난 강의에서 아래 그림과 같은 메모리 구조를 간략하게 배웠습니다.
스크린샷 2023-06-18 오전 1 44 27

다시 복습하면,

  • 코드 영역에는 (프로그램 실행 시) 프로그램이 컴파일된 바이너리가 저장됩니다.
  • 데이터 영역에는 프로그램 안에서 저장된 전역 변수가 저장됩니다.
  • 힙 영역에는 malloc 을 이용해 동적으로 할당된 메모리의 데이터가 저장됩니다.
  • 스택 영역에는 프로그램 내의 함수, 지역 변수와 관련된 것들이 저장됩니다.

힙 영역에서는 malloc 에 의해 메모리가 더 할당될수록, 점점 사용하는 메모리의 범위가 아래로 늘어납니다.
마찬가지로 스택 영역에서도 함수가 더 많이 호출 될수록 사용하는 메모리의 범위가 점점 위로 늘어납니다.

이렇게 점점 늘어나다 보면 제한된 메모리 용량 하에서는 기존의 값을 침범하는 상황도 발생할 것입니다.
이를 힙 오버플로우 또는 스택 오버플로우라고 일컫습니다.

사용자에게 입력 받기

scanf 를 이용해 사용자에게 입력받은 정수를 출력하는 코드를 작성해 봅시다.

#include <stdio.h>

int main(void)
{
  int x;
  printf("x: ");
  scanf("%i", &x); // ✅ x의 "주소"에 입력받은 값을 저장한다
  printf("x: %i\n", x);
}

scanf 라는 함수는 사용자로부터 형식 지정자에 해당되는 값을 입력받아 저장하는 함수입니다.
위 코드에서 int x 를 정의한 후에 scanf 에 x가 아닌 &x로 그 “주소”를 입력해주는 부분을 유의하기 바랍니다.
scanf 함수의 변수가 실제로 스택 영역 안에 x가 저장된 주소로 찾아가서 사용자가 입력한 값을 저장하도록 하기 위함입니다.

그럼 이제 사용자에게 입력받은 문자열을 출력하는 코드를 작성해 봅시다.

#include <stdio.h>

int main(void)
{
  char *s = NULL; // ✅ 주소를 미리 알 수 없으니 빈 공간으로 초기화한다
  printf("s: ");
  scanf("%s", s); // Emma 입력
  printf("s: %s\n", s);
}

위 코드를 실행해보면 아래와 같이 출력됩니다.

s: (null)

왜 우리의 의도대로 동작하지 않을까요?
char *s의 의미를 기억하나요?
메모리 영역의 주소를 저장할 수 있는 변수를 말합니다.

그리고 (null)은 메모리 공간이 할당되지 않았다는 뜻입니다.
즉, EMMA의 이름이 저장될 공간을 할당하지 않은 것입니다.

따라서 코드를 아래와 같이 바꾸어야 합니다.

#include <stdio.h>

int main(void)
{
  char s[5];
  printf("s: ");
  // ✅ char * 는 주소이기 때문에 & 가 필요 없다 (포인터 변수는 그 자체가 주소로 정의되기 때문)
  scanf("%s", s); // ✅ 컴파일러는 문자 배열의 이름을 포인터처럼 다룬다 -> 따라서 이 코드에서 배열 첫 바이트의 추소를 넘겨주는 것이다
  printf("s: %s\n", s);
}

그런데 왜 정수를 입력받을 때에는 scanf 에 &x를 입력하고, 문자열을 입력받을 때에는 scanf 에 s를 그대로 입력하는 걸까요?
그 이유는 s를 크기가 5인 문자열, 즉 크기가 5인 char 자료형의 “배열”로 정의하였기 때문입니다.
clang 컴파일러는 문자 배열의 이름을 포인터처럼 다룹니다.
즉 scanf에 s라는 배열의 첫 바이트 주소를 넘겨주는 것이죠.

배열과 포인터는 사실 연관되어 있습니다.

배열은 메모리가 연속적으로 할당된 공간입니다.
문자열은 문자가 연속적으로 있는 겁니다.
그리고 문자열은 사실 그 메모리 공간의 첫 번째 주소를 의미합니다.

따라서 최소한 이 문맥에서는 포인터는 배열과 같다고 볼 수 있습니다.

하지만 위 코드를 실행했을 때에도 문제가 있습니다.
“Emma” 를 입력했을 때에는 정상적으로 작동하지만, “Emma Humphrey” 를 입력했을 때에도 s: Emma를 출력합니다.

이는 5바이트의 메모리 공간만을 할당했기 때문입니다.

파일 쓰기

이제 사용자로부터 입력을 받아 파일에 저장하는 프로그램도 작성할 수 있습니다.

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

int main(void)
{
  // 파일을 연다
  FILE *file = fopen("phonebook.csv", "a");

  // 사용자로부터 문자열을 입력받는다
  char *name = get_string("Name: ");
  char *number = get_string("Number: ");

  // 파일에 출력한다
  fprintf(file, "%s, %s\n", name, number); // 파일용 printf 로 파일에 출력할 수 있다

  // 파일을 닫는다
  fclose(file);
}

fopen이라는 함수를 이용하면 파일을 FILE이라는 자료형으로 불러올 수 있습니다.
fopen 함수의 첫번째 인자는 파일의 이름, 두번째 인자는 모드로 r(=read)은 읽기, w(=write)는 쓰기, a(=append)는 덧붙이기를 의미합니다.
사용자에게 name과 number라는 문자열을 입력 받고, 이를 fprintf 함수를 이용하여 printf에서처럼 파일에 직접 내용을 출력할 수 있습니다.
작업이 끝난 후에는 fclose 함수로 파일에 대한 작업을 종료해줘야 합니다.



💛 개인 공부 기록용 블로그입니다. 👻

맨 위로 이동하기

태그:

카테고리:

업데이트: