최초작성일: 2023-02-19
마지막 수정일: 2023-02-25
부스트코스 <모두를 위한 컴퓨터 과학 (CS50 2019)> 강의 수강
5. 메모리
1. 메모리 주소
- 16진법을 읽고 쓸 수 있다.
- 메모리 주소에 접근하고 값을 받아오는 코드를 C로 작성할 수 있다.
- 16진법, 메모리 주소
- 진법 - 컴퓨터나 휴대폰 속 메모리의 위치를 표현하는 데 매우 유용.
- 각 바이트에 고유한 숫자를 부여하여 메모리 속 내용물에 대해 이야기할 수 있다.
16진법(Hexadecimal)
- 컴퓨터가 숫자를 16진법으로 표현하는 경우가 많다.
- 16진수를 사용하면 10진수보다 2진수를 간단하게 나타낼 수 있다.
- 0 ~ 9, A ~ F: F는 15
- 8bit 이진법으로 나타낼 수 있는 최대 수는 255(11111111): 16진법으로는 FF
- 4bits씩 두 덩어리로 나누어 16진수로 표현 가능
- 다른 숫자 표현과 헷갈리지 않도록 숫자 앞에
0x
를 붙임- 0x8, 0x1F, ...
16진수의 유용성
- 2진수로 표현했을 때보다 간단해짐.
- 컴퓨터는 8개의 비트가 모인 바이트 단위로 정보를 표현하는데, 2개의 16진수는 1바이트의 2진수로 변환되기 때문에 정보를 표현하기 매우 유용
RGB
- 컴퓨터는 빨간색, 초록색, 파란색으로 색을 표현
- 16진법으로 각 색의 양을 나타내도록 정한 것
000000
: 검은색FF0000
: 빨간색FFFFFF
: 흰색
메모리 주소
#include <stdio.h>
int main(void)
{
int n = 50;
printf("%i\n", n);
}
- 메모리 어딘가에 변수 n이 저장. int는 4bytes
#include <stdio.h>
int main(void)
{
int n = 50;
printf("%p\n", &n); // &: ~의 주소를 의미하는 연산자. %p가 주소를 출력해줌
}
// 0x7ffdee62027c
- n이 저장되어있는 위치(메모리상 주소)를 알 수 있음.
포인터
: 컴퓨터 메모리의 주소를 가리키는 것.%p
printf
로 포인터를 출력해달라고 하면 16진수로 출력해 줌.&n
: 'n의 주소를 달라'*
: 그 주소로 가 달라
#include <stdio.h>
int main(void)
{
int n = 50;
printf("%i\n", *&n); // *: 그 주소로 가달라
}
// 50
- 변수의 자료형을 모른다면 어떤 형식 지정자를 사용해야 할까?
- 직접 결정해야.
- 추정하거나 뭐든 알려줘야.
- c에서 자료형을 알려주는 기능은 없음.
2. 포인터
- 포인터 변수를 정의하고 사용할 수 있다.
포인터 변수 정의
#include <stdio.h>
int main(void)
{
int n = 50;
int *p = &n; //포인터는 앞에 *을 붙여 표시. int는 포인터가 가리키는 값의 자료형
printf("%p\n", p);
printf("%i\n", *p);
}
// 0x7ffc3aa393ac
// 50
- n의 주소를 p에 저장. p는 int를 가리키는 포인터.
- 주소는 반드시 포인터에 저장해야. 그냥
int p = &n;
해버리면 컴파일러가 경고.- int형에 주소를 저장할 수 없다.
- 최신 컴퓨터는 64bits(8bytes) 포인터를 사용
- 포인터는 추상화를 위해 사용됨
- 우리는 주소 자체가 궁금한 게 아니라 그 위치에 접근하고 싶은 것.
- 포인터를 이용해 p가 50을 가리킨다는 개념을 표현.
- 정수 50은 정수형 변수 안에 존재. 주소는 포인터 변수 p 안에 존재.
- 개념적으로는 p라는 변수가 다른 변수를 가리키는 것.
- 이를 이용해 정교한 자료형을 만들 수 있다.
3. 문자열
- 문자열 형태의 새로운 자료형인 string이 어떻게 정의되었는지 설명할 수 있다.
- 포인터, 문자열
c에는 문자열 자료형이 없다
- 각 바이트는 고유의 주소를 갖고 있다.
string s = "EMMA"
- s는 포인터로 메모리에 있는 EMMA의 첫 글자를 가리킨다.
- 컴퓨터는 문자열의 첫 번째 글자를 가리키면 널 종단 문자를 만날 때까지 루프를 돌면서 끝을 알아냄
- c에는 '문자열(string)'이라는 건 없음
- 문자열: 포인터. 첫번째 문자의 주소를 가리키면 그게 문자열
string s = "EMMA";
char *s = "EMMA"; // 문자를 가리키는 주소 저장
- cs50 라이브러리에서 새로운 자료형을 정의했던 것
- 문자열: 문자 배열의 첫 번째 바이트 주소
- 실제 cs50 라이브러리에서 문자열을 정의한 방식
typedef char *string; // typedef 새로운 자료형을 정의한다 // 'char *' 이 값의 형태가 문자의 주소가 될 것이다 // 데이터타입의 이름은 string으로
#include <stdio.h>
int main(void)
{
char *s = "EMMA";
printf("%s\n", s);
printf("%p\n", s);
printf("%p\n", &s[0]);
printf("%p\n", &s[1]);
printf("%p\n", &s[2]);
printf("%p\n", &s[3]);
}
// EMMA
// 0x402004
// 0x402004
// 0x402005
// 0x402006
// 0x402007
- 문자열은 여러 문자의 묶음을 추상화한 것
- 실제 변수 s는 그냥 주소
4. 문자열 비교
- 문자열이 저장되어 있는 방식에 근거해서 문자열을 비교하는 방법에 대해 설명할 수 있다.
- 문자열
#include <stdio.h>
int main(void)
{
char *s = "EMMA";
printf("%c\n", *s); // *s: s라는 주소로 가달라
printf("%c\n", *(s+1));
printf("%c\n", *(s+2));
printf("%c\n", *(s+3));
}
// E
// M
// M
// A
Syntactic sugar
: 프로그래머에게 유용한 기능- s[0], s[1] 등의 대괄호 표현식으로 적으면 컴퓨터 내부에서 clang 컴파일러가 대괄호 표현식을 위의 형태로 바꿔준다.
- s를 출력하면 전체 문자열이 출력되는 이유?
%s
형식 지정자에 의해 그 주소로 가서 첫 글자만이 아니라 널 종단 문자를 만날 때까지 다음 문자를 계속 출력해주는 것
포인터 연산(Pointer Arithmetic)
주소를 가져와서 1, 2, 3을 더하는 것처럼 계산하는 것
비교
정수 비교
#include <cs50.h>
#include <stdio.h>
int main(void)
{
int i = get_int("i: ");
int j = get_int("j: ");
if (i == j)
{
printf("Same\n");
}
else
{
printf("Different\n");
}
}
// i: 5
// j: 5
// Same
문자열 비교
#include <cs50.h>
#include <stdio.h>
int main(void)
{
string s = get_string("s: ");
string t = get_string("t: ");
if (s == t)
{
printf("Same\n");
}
else
{
printf("Different\n");
}
}
// s: Emma
// t: Emma
// Different // 다른 곳에 저장되어 있어서
- s와 t가 다른 곳에 저장되어 있기 때문
- 내용이 같을 수는 있지만 같은 곳에 있다는 의미는 아님
- 문자열을 비교하면
변수의 주소
를 비교하게 되는 것
- CS50 라이브러리의
get_string
함수는 첫 글자의 주소를 반환하는 함수 - 메모리 공간에서 입력한 문자 크기만큼의 공간을 찾아 입력한 글자를 넣어두고 메모리 공간의 첫 바이트 주소(포인터) 반환
#include <cs50.h>
#include <stdio.h>
int main(void)
{
char *s = get_string("s: ");
char *t = get_string("t: ");
printf("%p\n", s);
printf("%p\n", t);
}
// s: EMMA
// t: EMMA
// 0x20846b0
// 0x20846f0
- 주소 출력해보면 서로 다르단 걸 알 수 있다.
5. 문자열 복사
- 문자열을 복사할 수 있다.
- malloc
일반적인 방법으로 복사해보면
#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\n", s);
printf("%s\n", t);
}
// s: emma
// Emma // t를 바꿨는데 s도 바뀜
// Emma
- t라는 변수에 s를 복사할 때 값이 아니라
주소를 복사
한 것이기 때문 - 같은 곳을 가리키고 있기 때문에 하나의 값을 바꾸면 다른 변수를 통해 읽은 값도 바뀌는 것
- 메모리를 추가로 사용해서 동일한 크기의 변수를 만들고 s 안에 있는 글자를 하나씩 t로 복사하는 과정이 필요
malloc 함수를 이용해 문자열 복사
#include <cs50.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h> // strlen
#include <stdlib.h> // malloc
int main(void)
{
char *s = get_string("s: ");
char *t = malloc(strlen(s) + 1); // 메모리 할당 함수. 할당받을 메모리 크기를 인자로 받음.
for (int i = 0, n= strlen(s); i < n + 1; i++) // 널 종단 문자까지 복사해야.
{
t[i] = s[i];
}
t[0] = toupper(t[0]);
printf("%s\n", s);
printf("%s\n", t);
}
// s: emma
// emma
// Emma
malloc(bytes)
: 메모리 크기를 인자로 받아 메모리를 할당하는 함수for문
부분을 간단하게 할 수 있음strcpy(t, s);
: t에 s를 복사한다. 자주 하는 작업이라 함수를 만들어 놓음
- 널 종단 문자를 복사하지 않으면 어떻게 될까?
- 모름
- s와 t를 출력하려고 할 때 그 전에 뭔가 써둔 게 있다면 널 종단 문자가 나올 때까지 출력하려고 할 것.
- 변수의 값을 초기화하지 않으면
garbage value(쓰레기값)
라고 함.
6. 메모리 할당과 해제
- 메모리를 할당하고 해제할 수 있다.
- free, valgrind
malloc 함수
- 메모리 할당 함수
- 메모리 일부분을 가져와서 그곳을 가리키는 포인터를 주는 것
- 할당한 메모리의 첫 바이트 주소를 리턴
free 함수
- 메모리 해제 함수
- 할당되었던 메모리를 다시 반환
malloc
으로 메모리를 할당한 후 해제하지 않으면 메모리가 부족해질 수 있음- 쓰레기 값으로 남게 되어 메모리 용량의 낭비 발생. '메모리 누수'
- 사용하지 않는 메모리는 해제하는 것이 좋다.
- 그걸 어떻게 찾을까? ->
valgrind
이용
valgrind
- 디버깅 도구
$ valgrind ./filename
- 메모리 누수를 알려줌
- 파일의 어떤 줄의 어떤 블록 안에서 얼마만큼의 메모리가 누수되는지 알려줌
int main(void)
{
char *s = get_string("s: ");
char *t = malloc(strlen(s) + 1);
strcpy(t, s); // 자주 하는 작업이라 함수를 만들어놓음음
t[0] = toupper(t[0]);
printf("%s\n", s);
printf("%s\n", t);
free(t); // 메모리 할당 해제
}
버퍼 오버플로우(buffer overflow)
메모리를 이야기할 때 혹은 메모리 배열을 다룰 때 할당된 공간을 넘어 접근하는 것
#include <stdlib.h>
void f(void)
{
int *x = malloc(10 * sizeof(int)); // 정수형 10개만큼의 메모리를 할당
x[10] = 0; // x[10]의 공간은 없음. 0 ~ 9까지만 있음. 버퍼 오버플로우
free(x);
}
int main(void)
{
f();
return 0;
}
- 여기서 버퍼는 배열
- valgrind를 통해 디버깅하면 "Invalid write of size 4"라고 나옴.
- 할당하지 않은 메모리 영역에 접근하려 함.
7. 메모리 교환, 스택, 힙
- 메모리에 저장된 두 값을 교환하는 코드를 작성할 수 있다.
- 스택, 힙, 포인터
메모리에 저장된 두 값의 교환
#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를 넣고
a = b; // a에 b를 넣고
b = tmp; // b에 임시 변수를 넣으면?
}
// x is 1, y is 2
// x is 1, y is 2
// 교환이 되지 않음
- 함수에 인자를 전달할 때 그 값을 복사해서 전달함.
- 함수는 x와 y가 아니라 x와 y를 복사한 값을 서로 교환한 것.
메모리의 영역
- 자료형, 변수의 종류에 따라 메모리에 저장되는 위치가 다르다.
- 메모리 가장 위쪽부터 머신 코드 영역, 글로벌 영역(전역 변수 저장), 힙 영역, 스택 영역
힙(heap)
: 메모리를 할당받을 수 있는 커다란 영역.malloc
이 메모리를 할당하는 곳- 힙 영역은 아래로 자람. 메모리를 사용할수록 점점 아래로
스택(stack)
: 함수가 호출될 때 지역 변수가 쌓이는 공간- 함수를 호출할 때마다 함수의 지역변수는 스택이라는 메모리 제일 아래 영역에 놓인다.
메모리의 영역을 생각하며 변수 값 교환 코드 다시 보기
main
함수에 변수 x, y가 있다.main
함수는 프로그램이 실행되면 스택의 가장 아랫부분에스택 프레임
이라는 공간이 주어짐. 여기에 변수 저장.swap
함수가 실행되면 스택의main
함수 위 영역에스택 프레임
생성. a, b, tmp의 세 변수가 여기에 존재.- a, b는 x, y를 복사해온 값.
swap
함수로 a, b의 교환이 일어난 후 함수 실행이 종료되면swap
함수 스택 프레임은 사라짐. - x, y에는 아무런 영향이 없기 때문에 x, y를 출력하면 원래 상태 그대로 나온다.
해결책
- 함수에 x, y의 복사본을 전달하는 대신
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); // x와 y의 주소를 전달
printf("x is %i, y is %i\n", x, y);
}
void swap(int *a, int *b) // 정수의 주소를 받아 a, b라 부른다
{
int tmp = *a; // a가 가리키는 곳으로 따라가라 -> x의 값을 tmp에 저장
*a = *b; // b가 가리키는 곳으로 가서 y의 값을 a가 가리키는 곳에 저장
*b = tmp; // tmp의 값을 b가 가리키는 곳에 저장
}
// x is 1, y is 2
// x is 2, y is 1
- a와 b를 각각 x와 y를 가리키는 포인터로 지정
swap
함수가 실행되면 a, b가 가리키는 주소로 가서 값을 바꾸기 때문에main
함수의 x, y 변수가 바뀐다.
8. 파일 쓰기
- 사용자로부터 값을 입력받아 파일에 출력하는 프로그램을 작성할 수 있다.
- scanf, fopen, fprintf, fclose
메모리 구조의 문제점
- 힙은 아래로, 스택은 위로 커짐.
- 둘 다 커지다보면 두 메모리 영역이 충돌할 수 있음
heap overflow
:malloc
함수를 계속 호출해 너무 많은 메모리를 할당해서 메모리 속 다른 내용을 덮어씀stack overflow
: 재귀호출 등으로 인해 스택이 넘침- 제한된 크기의 메모리 영역에선 어쩔 수 없는 현상
- 컴퓨터가 너무 많은 메모리를 사용해서 파일이나 사진이 열리지 않거나 화면이 정지하거나 동작하지 않는 현상 발생
get_int 구현
#include <stdio.h>
int main(void)
{
int x;
printf("x: ");
scanf("%i", &x); // scanf 형식지정자를 쓰면 그 형식대로 입력받음. 사용자의 입력을 저장하고 싶은 변수의 주소를 적음.
printf("x: %i\n", x);
}
// x: 5 5 입력
// x: 5 출력
scanf
함수- 형식지정자를 쓰면 그 형식대로 입력 받음.
- 사용자의 입력을 저장하고 싶은 변수의 주소를 적으면 그 주소에 가서 입력값을 저장해 줌.
- 에러 확인 기능이 없어서 사용자가 지정된 자료형이 아닌 다른 자료형을 입력한다면 프로그램이 죽거나 예상치 못하게 동작할 것임.
get_string 구현
#include <stdio.h>
int main(void)
{
char *s = NULL; // s를 주소로 초기화해야. 주소를 미리 알 수 없기에 NULL(빈 공간. 가리키는 곳이 없다는 의미)로 둠.
printf("s: ");
scanf("%s", s); // char *로 이미 주소로 정의된 포인터 변수이기 때문에 &는 필요 없음
printf("s: %s\n", s);
}
// s: Emma is the head ca for cs50 - 입력
// s: (null) -> 안 됨
char *s
: 메모리 영역의 주소를 저장할 수 있는 변수NULL
: 메모리 공간이 할당되지 않았다.
#include <stdio.h>
int main(void)
{
char s[5]; // 크기가 5인 문자 배열 선언
printf("s: ");
scanf("%s", s);
printf("s: %s\n", s);
}
// s: EMMA 입력
// s: EMMA 출력
// s: EMMA HUMPHREY 입력
// s: EMMA 출력 - 충분한 공간을 할당하지 않아서.
- clang 컴파일러는 문자 배열의 이름을 포인터처럼 다룸
- scanf에 s라는 배열의 첫 바이트 주소를 넘겨 줌.
- 충분한 공간을 할당해서 저장해야.
- 주어진 공간보다 더 많은 내용을 입력할 경우 제대로 되지 않음
- 위 예시는 프로그램이 멈추진 않았지만 더 많은 내용을 입력할 경우 프로그램이 멈추거나 세그멘테이션 오류가 발생할 수 있음
파일 쓰기
- 사용자로부터 이름과 번호를 입력받아 텍스트파일에 덧붙이는 프로그램
#include <cs50.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
// Open file
FILE *file = fopen("phonebook.csv", "a");
// Get strings from user
char *name = get_string("Name: ");
char *number = get_string("Number: ");
// Print (write) strings to file
fprintf(file, "%s,%s\n", name, number); // 파일용 printf
// Close file
fclose(file);
}
fopen
: 파일을 FILE 이라는 자료형으로 불러올 수 있음- `fopen("파일명", "모드")
- 모드:
"r" - 읽기
,"w" - 쓰기
,"a" - 덧붙이기
- 모드:
fprintf
: 파일에 직접 내용 출력fclose
: 파일에 대한 작업 종료
- 프로그램을 실행시켜 이름과 전화번호를 입력하면
phonebook.csv
라는 파일이 생성되어 입력한 내용이 저장된다.
9. 파일 읽기
- 파일을 읽고 JPEG 파일인지를 검사하는 프로그램을 작성할 수 있다.
- JPEG, fread
#include <stdio.h>
int main(int argc, char *argv[])
{
// Ensure user ran program with two words at prompt
if (argc != 2) // 제대로 된 입력이 아니므로
{
return 1; // 1(오류) 리턴
}
// Open file
FILE *file = fopen(argv[1], "r"); // argv[1]: 입력받은 파일명. 읽기 모드로
if (file == NULL) // 파일이 제대로 열리지 않으면 fopen 함수가 NULL 리턴
{
return 1; // 1(오류) 리턴
}
// Read 3 bytes from file
unsigned char bytes[3]; // unsigned -128부터 127이 아닌 0부터 255 범위의 값을 의미
fread(bytes, 3, 1, file); // 파일에서 첫 3바이트를 읽어옴
// Check if bytes are 0xff 0xd8 0xff
if (bytes[0] == 0xff && bytes[1] == 0xd8 && bytes[2] == 0xff)
{
printf("Maybe\n");
}
else
{
printf("No\n");
}
}
- Ch3 - 8. 명령행 인자(command-line arguments) 참고.
argc
는 함수가 받을 입력의 개수argv[]
는 그 입력이 포함된 배열
fread(배열, 읽을 바이트 수, 읽을 횟수, 읽을 파일)
: 파일 읽는 함수- jpeg 파일은 첫 세 바이트가 각각 0xff, 0xd8, 0xff임. jpeg 형식의 파일을 정의할 때 만든 약속.
- 이를 검사해서 jpeg 파일인지 확인
'NOTES > CS50' 카테고리의 다른 글
[CS50] Chapter 6. 자료구조 (0) | 2023.03.04 |
---|---|
[CS50] Chapter 4. 알고리즘 (0) | 2023.02.13 |
[CS50] Chapter 3. 배열 (2) | 2023.02.04 |
[CS50] Chapter 2. C언어 (1) | 2023.01.30 |
[CS50] Chapter 1. 컴퓨팅 사고 (0) | 2023.01.30 |