아래와 같은 코드가 있다.
함수 swap은 정수 a와 b를 입력받아 그 값을 바꾸는 일을 수행한다.
main 함수에서는 x에 1, y에 2를 입력하고 swap 함수를 통해 그 두 값을 바꾸려고 한다.
과연 내 의도대로 잘 바뀌어 출력이 될까?
#include <stdio.h>
void swap(int a, int b);
int main(void)
{
int x = 1;
int y = 2;
printf("x is %i, y is %i\n", x, y);
swap(x, y);
printf("x is %i, y is %i\n", x, y);
}
void swap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
위 코드를 컴파일하고 출력해보면 의도와는 다르게 swap 함수를 거친 후에도 x와 y의 값이 바뀌지 않은채 그대로 출력됨을 알 수 있다.
사실 swap 함수는 교환 작업을 제대로 수행하고 있는데요,
문제는 교환하는 대상이 x, y 그 자체가 아닌 함수 내에서 새롭게 정의된 a, b라는 것이다.
a와 b는 각각 x와 y의 값을 복제하여 가지게 된다. 서로 다른 메모리 주소에 저장되는 것이다.
아래 그림에서와 같이 메모리 안에는 데이터 저장되는 구역이 나뉘어져 있다.
머신 코드 영역에는 우리 프로그램이 실행될 때 그 프로그램이 컴파일된 바이너리가 저장된다.
글로벌 영역에는 프로그램 안에서 저장된 전역 변수가 저장된다.
힙 영역에는 malloc으로 할당된 메모리의 데이터가 저장된다.
그리고 스택에는 프로그램 내의 함수와 관련된 것들이 저장된다.
이를 바탕으로 다시 생각해보면, 위의 코드에서 a, b, x, y, tmp 모두 스택 영역에 저장되지만
a와 x, b와 y는 그 안에서도 서로 다른 위치에 저장된 변수이다.
따라서 a와 b를 바꾸는 것은 x와 y를 바꾸는 것에 아무런 영향도 미치지 않는 것이다.
따라서 아래 그림 및 코드와 같이 a와 b를 각각 x와 y를 가리키는 포인터로 지정함으로써 이 문제를 쉽게 해결할 수 있다.
#include <stdio.h>
void swap(int *a, int *b);
int main(void)
{
int x = 1;
int y = 2;
printf("x is %i, y is %i\n", x, y);
swap(&x, &y);
printf("x is %i, y is %i\n", x, y);
}
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
메모리 구조 정리
머신 코드 영역에는 우리 프로그램이 실행될 때 그 프로그램이 컴파일된 바이너리가 저장된다.
글로벌 영역에는 프로그램 안에서 저장된 전역 변수가 저장된다.
힙 영역에는 malloc으로 할당된 메모리의 데이터가 저장된다.
그리고 스택에는 프로그램 내의 함수와 관련된 것들이 저장된다.
힙 영역에서는 malloc 에 의해 메모리가 더 할당될수록, 점점 사용하는 메모리의 범위가 아래로 늘어난다.
마찬가지로 스택 영역에서도 함수가 더 많이 호출 될수록 사용하는 메모리의 범위가 점점 위로 늘어난다.
이렇게 점점 늘어나다 보면 제한된 메모리 용량 하에서는 기존의 값을 침범하는 상황도 발생할 것이다.
이를 힙 오버플로우 또는 스택 오버플로우라고 일컫는다.
사용자에게 입력 받기
코드를 구현해보면...
[get_int 코드]
#include <stdio.h>
int main(void)
{
int x;
printf("x: ");
scanf("%i", &x);
printf("x: %i\n", x);
}
[get_string 코드]
#include <stdio.h>
int main(void)
{
char s[5];
printf("s: ");
scanf("%s", s);
printf("s: %s\n", s);
}
위 코드들에서 scanf라는 함수는 사용자로부터 형식 지정자에 해당되는 값을 입력받아 저장하는 함수이다.
scanf 함수의 변수가 실제로 스택 영역 안에 s가 저장된 주소로 찾아가서 사용자가 입력한 값을 저장하도록 하기 위함이다.
그 이유는 s를 크기가 5인 문자열, 즉 크기가 5인 char 자료형의 배열로 정의하였기 때문이다.
clang 컴파일러는 문자 배열의 이름을 포인터처럼 다룬다.
즉 scanf에 s라는 배열의 첫 바이트 주소를 넘겨주는 것.
파일 쓰기
이제 사용자로부터 입력을 받아 파일에 저장하는 프로그램도 작성할 수 있다.
#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);
fclose(file);
}
fopen이라는 함수를 이용하면 파일을 FILE이라는 자료형으로 불러올 수 있다.
fopen 함수의 첫번째 인자는 파일의 이름, 두번째 인자는 모드로 r은 읽기, w는 쓰기, a는 덧붙이기를 의미한다.
사용자에게 name과 number라는 문자열을 입력 받고,
이를 fprintf 함수를 이용하여 printf에서처럼 파일에 직접 내용을 출력할 수 있다.
작업이 끝난 후에는 fclose함수로 파일에 대한 작업을 종료해줘야 한다.
이제까지 파일에 쓰는 프로그램을 작성 했으니,
이번에는 파일의 내용을 읽어서 파일의 형식이 JPEG 이미지인지를 검사하는 프로그램을 작성해보자.
#include <stdio.h>
int main(int argc, char *argv[])
{
if (argc != 2)
{
return 1;
}
FILE *file = fopen(argv[1], "r");
if (file == NULL)
{
return 1;
}
unsigned char bytes[3];
fread(bytes, 3, 1, file);
if (bytes[0] == 0xff && bytes[1] == 0xd8 && bytes[2] == 0xff)
{
printf("Maybe\n");
}
else
{
printf("No\n");
}
fclose(file);
}
위 코드에서 main 함수를 보면 사용자로부터 입력을 받는 것을 알 수 있다.
여기서는 파일의 이름을 입력으로 받을 것이다.
만약 argc가 2가 아니라면,
파일명이 입력되지 않았거나 파일명 외의 다른 인자가 입력되었기 때문에 1(오류)을 리턴하고 프로그램을 종료한다.
만약 argc가 2라면 프로그램이 그대로 진행된다.
입력받은 파일명(argv[1])을 ‘읽기(r)’ 모드로 불러온다.
만약 파일이 제대로 열리지 않으면 fopen 함수는 NULL을 리턴하기 때문에 이를 검사해서 file을 제대로 쓸 수 있는지를 검사하고,
아니라면 역시 1(오류)를 리턴하고 프로그램을 종료한다.
만약 파일이 잘 열렸다면, 프로그램이 계속 진행된다.
그 후 크기가 3인 문자 배열을 만들고, fread 함수를 이용해서 파일에서 첫 3바이트를 읽어온다.
fread 함수의 각 인자는 (배열, 읽을 바이트 수, 읽을 횟수, 읽을 파일)을 의미한다.
그리고 마지막으로 읽어들인 각 바이트가 각각 0xFF, 0xD8, 0xFF 인지를 확인한다.
이는 JPEG 형식의 파일을 정의할 때 만든 약속으로, JPEG 파일의 시작점에 꼭 포함되어 있어야 한다.
따라서 이를 검사하면 JPEG 파일인지를 확인할 수 있다.
퀴즈
'공부 STUDY > CS' 카테고리의 다른 글
CS50 하버드 대학 컴퓨터 과학 Computer Science 과정 수료했다! (0) | 2022.06.29 |
---|---|
CS | 자료구조(1) - malloc 과 포인터 복습, 배열의 크기 조정, 연결 리스트 [도입, 코딩] (0) | 2022.06.29 |
CS50 | 메모리 - 매모리 주소, 포인터, 문자열, 문자열 비교, 복사, 할당 그리고 해제 (0) | 2022.06.28 |
CS50 | 알고리즘 - 버블 정렬, 선택 정렬, 정렬 알고리즘의 실행 시간 (0) | 2022.06.26 |
CS50 | 알고리즘 - 검색 알고리즘, 알고리즘 표기법, 선형 검색 (0) | 2022.06.25 |