-
[모두를 위한 컴퓨터 과학] Ch5. 메모리(Memory)Study/BoostCourse 2021. 11. 22. 22:47728x90
https://www.boostcourse.org/cs112/joinLectures/41307
💃 해당 강의를 듣고 스터디용으로 정리한 내용들입니다 💃
👉 이전 강의
https://ninano1109.tistory.com/192
1) 메모리 주소
16진수(Hexadecimal)
- CS에서는 숫자를 10진수나 2진수 대신 16진수로 표현함
- 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0A, 0B, 0C, 0D, 0E, 0F -> (16개까지 표현 가능)
- 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1A, 1B, 1C, 1D, 1E, 1F
- 16진수에서는 0x를 붙이기
- 16진수의 장점
- 2진수로 표현했을 때 보다 훨신 간단해짐
- 2개의 16진수 -> 1byte의 2진수로 변환(?)
RGB
00 00 00 => BLACK
FF 00 00 => RED
00 FF 00 => GREEN
00 00 FF => BLUE
FF FF FF => WHITE
메모리 주소
- int 타입 = 4byte
- 정수형 변수를 선언하면 컴퓨터 메모리 어딘가서 4바이트만큼 자리를 차지하여 저장됨.
- C에서는 변수의 메모리상 주소를 받기 위해 '&' 연산자 사용함.
#include <studio.h> int main(void) { int n = 50; printf(%p\n", &n) }
=> 출력값: ‘0x7ffe00b3adbc’ => 변수 n의 16진법으로 표현된 메모리 주소
- 메모리 주소에 있는 실제 값은 '*'를 사용함.
#include <studio.h> int main(void) { int n = 50; printf("%i\n", *&n); }
=> 먼저 변수 n의 주소를 받고, 그 주소에 해당하는 실제 값을 얻어 50을 출력함.
2) 포인터
- '*' 연산자를 이용해서 포인터 역할 하는 변수 선언
#include <stdio.h> int main(void) { int n = 50; int *p = &n; printf("%p\n", p); # 포인터 p의 값 = 변수 n의 주소 출력 printf("%i\n", *p); # 포인터 p가 가리키는 변수 값 = 변수 n의 값 출력 }
-
- *p 포인터 변수에 변수 n의 주소 값 저장.
- int는 해당 포인터가 int 타입의 변수를 가리킨다는 의미
- p가 n을 가리키고 있음
- 포인터의 크기 vs 메모리의 크기
- 크게 상관 없음.
- 포인터의 크기는 운영체제에 따라 64비트, 32비트 등으로 달라짐.
- 메모리의 크기는 랩 용량에 따라 증감
3) 문자열
- 문자열 = 문자의 배열
- 문자열 마지막의 \0은 0으로 이루어젠 바이트로, 문자열의 끝을 나타냄
- string s = ''EMMA"; 의 변수 s는 문자열을 가리키는 포인터의 역할을 함
- 더 자세히 말해서 문자열의 가장 첫번째 문자, s[0]를 가리킴
- string 자료형을 이용한 "EMMA" 출력
#include <cs50.h> #include <stdio.h> int main(void) { string s = "EMMA"; printf("%s\n", s); }
- char 포인터를 이용한 "EMMA" 출력
#include <stdio.h> int main(void) { char *s = "EMMA"; printf("%s\n", s); }
- s변수는 문자에 대한 포인터 역할이므로 "EMMA" 문자열의 가장 첫 번째 값을 저장함
4) 문자열 비교
#include <stdio.h> int main(void) { char *s = "EMMA"; printf("%p\n", s); }
s 포인터 값 = "EMMA"의 첫 번째 값인 'E'의 메모리 주소 출력
s = &s[0]
printf("%c\n", *s); # E printf("%c\n", *(s+1)); # M printf("%c\n", *(s+2)); # M printf("%c\n", *(s+3)); # A
- 주소 값을 하나씩 증가하여 문자 값을 출력
#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"); } }
- 문자열이 저장된 변수를 비교하면 변수가 저장되어 있는 주소가 다르기 때문에 다르다는 결과가 나옴
- 정확한 비교를 위해서 실제 문자열 저장되어 있는 곳으로 이동하여, 각 문자 하나씩 비교하기\
5) 문자열 복사
#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: %s\n", s); printf("t: %s\n", t); }
- s와 t는 값이 아닌 문자열이 있는 메모리의 주소가 저장되어 있기 때문에 동일하게 바뀌게됨.
- 이를 메모리상에서 복사하려면? => 메모리 할당 함수 사용
#include <cs50.h> #include <ctype.h> #include <stdio.h> #include <string.h> int main(void) { char *s = get_string("s: "); char *t = malloc(strlen(s) + 1); # s문자열 길이 + 널 종단 문자 for (int i = 0, n = strlen(s); i < n + 1; i++) { t[i] = s[i]; } t[0] = toupper(t[0]); printf("s: %s\n", s); printf("t: %s\n", t); }
- malloc(memory allocation) 함수를 사용하여 t를 정의하기
- malloc 함수는 정해진 크기 만큼 메모리 할당하는 함수
6) 메모리 할당과 해제
- malloc 함수 이용하여 메모리를 할당한 후에는 free 함수를 이용하여 메모리 해제가 필요함.
- Otherwise, 메모리에 저장된 값은 쓰레기 값으로 남게 되어 메모리 용량의 낭비가 발생하게 됨
- 이러한 현상을 ''메모리 누수'라고 부름
#include <stdlib.h> void f(void) { int *x = malloc(10 * sizeof(int)); x[10] = 0; # 1. 버퍼 오버플로우 } int main(void) { f(); return 0; }
- valgrind 프로그램을 사용하면 2가지 오류가 발생함
- 버퍼 오버플로우 : 10개의 int형 배열에서 11번째 인덱스 접근은 불가능함
- 메모리 누수 : 포인터 x를 통해 할당한 메모리르 해제하기 위해 free() 코드 추가로 해결 가능.
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 = b; b = tmp; }
- swap 함수가 의도대로 작동하지 않는 이유는 교환 대상이 x,y 자체가 아닌 함수 내에서 새롭게 정의 된 a,b 이기 때문에
- swap 함수 내에서 a와 b는 x와 y의 값을 복제하여 가지게 됨.
- 따라서 서로 다른 메모리 주소에 저장됨.
- stack 영역에 저장되어 있는 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) # 포인터 a,b { int tmp = *a; *a = *b; *b = tmp; }
- 메모리 안에는 데이터가 저장되는 구역이 나뉘어져 있음
- machine code: 프로그램 실행 시 프로그램이 compile된 binary가 저장됨
- globals: 프로그램 안에서 저장된 global variable 저장
- heap: malloc으로 할당된 메모리의 데이터가 저장됨
- stack: 프로그램 내의 함수와 관련된 것들 저장
- 메모리 영역을 다양하게 나누는 이유
- 메모리를 효율적으로, 빠르게 사용하기 위해서
- 데이터 유형에 따라 접근 시간, 메모리 공간 등의 측면에서 효율적 운영 가능
- 한정된 자원 내에서 프로그램을 효율적으로 실행하기 위해서
8) 파일 쓰기
- 힙 오버플로우: 메모리 구조에서 malloc에 의해 메모리가 더 할당될수록, heap 영역에서 사용하는 메모리의 범위가 아래로 늘어남
- 스택 오버플로우: 스택영역에서도 함수가 더 호출될수록 사용하는 메모리 범위가 위로 늘어남
- [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 함수: 사용자로부터 형식 지정자에 해당되는 값을 입력받아 저장하는 함수
- 사용자로부터 입력을 받아 파일에 작성, 저장하는 프로그램
#include <cs50.h> #include <stdio.h> #include <string.h> int main(void) { FILE *file = fopen("phonebook.csv", "a"); # 파일을 FILE이라는 자료형으로 불러오기 char *name = get_string("Name: "); char *number = get_string("Number: "); fprintf(file, "%s,%s\n", name, number) # 파일에 직접 내용 출력 fclose(file); # 파일에 대한 작업 종료 }
- fopen('filename', 'mode') => mode: r/w/a
함수 직접 구현하기
# 형식 지정자만 해당 자료형으로 변경해주기 get_long() → scanf("%li", &변수); get_float() → scanf("%f", &변수); get_char() → scanf("%c", &변수);
9) 파일 읽기
#include <stdio.h> int main(int argc, char *argv[]) { if(argc !=2) # 프로그램명, 파일명(인자2개) { return 1; } FILE *file = fopen(argv[1], "r"); # 읽기 모드로 파일명 불러오기 if (file == NULL) # 파일이 제대로 열리지 않을 때 { return 1; } unsigned char bytes[3]; # 크기가 3인 문자 배열 만들기 fread(bytes, 3, 1, file); # 파일에서 첫 3바이트 읽어오기 # (배열, 바이트 수, 횟수, 파일) if (bytes[0] == 0xff && bytes[1] == 0xd8 && bytes[2] == 0xff) { printf("Maybe\n"); } else { printf("No\n"); } fclose(file); }
- 파일마다 매직넘버라는 파일 시그니처가 존재하여, 파일 형태를 결정지음
- 이 파일 시그니처는 디지털 포렌식, 파일복구, 악성코드 분석 등에 활용됨
- JPEG 파일: “FF D8 FF E0″
- 디지털카메라로 캡쳐한 파일: “FF D8 FF E1”
- GIF 파일: " 47 49 46 38 37 61"
- MP4: "00 00 00 18 66 74 79 70"
추가조사
pointer
int a=1;
컴퓨터는 메모리에서 int형 자료가 저장될 공간 하나를 어딘가에 할당해주고, 그 변수의 이름을 a라고 칭한 다음 1이라는 정수를 저장함. 즉, 변수 a는 메모리 어딘가에 존재하는 것.
이 변수가 저장된 메모리상의 위치를 주소라고 표현함.
포인터는 변수의 주소를 뜻하고, 포인터 변수는 변수의 주소를 저장하는 변수를 말함.
포인터 변수도 변수이기 때문에, 메모리 어딘가에 할당이 된다.
여기서 중요한 점은, 변수의 주소를 저장한다는 건 주소를 안다는 것이고 이는 곧 해당 포인터변수가 아닌 다른 포인터 변수가 그 주소에 있는 변수에 접근 가능하다는 점이다.
그리고 주소를 통해 그 값에 접근하는 것이 바로 역참조(dereference) ⇒ *로 표현함
포인터 변수 선언 후 (*)를 붙여서 연산 수행함
포인터를 다룰 때 주의할 점은!
포인터를 사용하기 전 반드시 초기화를 해주어야 한다는 것.
여기서 초기화는 반드시 어떤 변수의 주소값, 혹은 NULL 값으로 초기화해주어야 한다.
# 변수의 주소값으로 초기화 int a = 1; int *pa; pa = &a; # NULL 값으로 초기화 int *p = NULL;
포인터는 메모리 주소를 직접 접근하기 때문에, 초기화를 정상적으로 안 해주면 접근해서는 안 될 곳까지 접근할 위험성이 있음.
초기화를 하지 않은 변수는 '쓰레기 값'이라는 이상한 값이 들어가 있을 수 있음.
만약 초기화를 제대로 하지 않은 상태에서 역참조 연산자로 값을 바꾸게된다면?
또 하필 포인터 변수가 가리킨 곳이 프로그램 상 매우 중요한 곳이었다면 문제가 생김.
ref.
https://m.blog.naver.com/nsj6646/221486028315
동적할당
포인터를 이용하면 원하는 만큰 메모리를 할당하여 배열처럼 사용할 수 있음 ⇒ 동적할당
C언어의 배열에는 대표적 단점이 있는데, 바로 배열의 크기를 선언할 때부터 정해줘야하고(정적할당) 그 정해진 크기는 불가변적(unchangeable) 이라는 점.
배열의 크기가 부족한 건 큰 문제가 되기 때문에, 메모리의 낭비가 있더라도 배열의 크기를 넉넉하게 할당하는 것이 일반적임.
하지만, 동적할당을 통해 필요한 공간만큼만 배열을 할당할 수 있음.
동적할당에 핵심이 되는 함수가 malloc() 함수
int n; int *arr; scanf("%d", &n); arr= (int*)malloc(sizeof(int) * n);
malloc(할당하고 싶은 메모리 크기) ⇒ malloc(자료형의 크기 * 원소의 개수)
해당 공간을 쓰려면 접근(참조)가 가능해야 하고, 메모리에 대한 접근은 포인터를 통해 할 수 있음.
따라서, 포인터 변수에 해당 메모리 시작주소를 저장해준다면 사용가능함.
동적할당해제(free())
포인터를 이용한 동적할당은 프로그램 종료 전 직접 할당된 메모리를 해제해주어야 할 것
배열과 같이 컴파일 당시 할당된 메모리에 대해서는 운영체제가 자동으로 메모리 해제함
free 함수의 인자로 포인터 변수를 인자로 넘겨주기
free(arr);
malloc()/free()
- malloc() 함수는 특정 바이트에 해당하는 사이즈를 할당하고, 할당 메모리의 첫번째 바이트를 가리키는 포인터를 반환함
- 메모리를 할당하는 것만이 목적이므로, 초기값을 줄 수 없음
- 만약, 메모리 할당에 실패하면, NULL 포인터를 반환함
- free() 함수
- 포인터의 값을 바꾸는 것이 아니므로, 여전히 동일한 메모리 위치를 가리키고 있음
- 는 malloc() 함수에서 할당한 메모리를 비할당하는 기능
ref.
next 👉
https://ninano1109.tistory.com/197
'Study > BoostCourse' 카테고리의 다른 글
[모두를 위한 컴퓨터 과학] Ch6. 데이터 구조(Data Structure) (0) 2021.11.28 [모두를 위한 컴퓨터 과학] Ch4. 알고리즘(Algorithm) (0) 2021.11.13 [모두를 위한 컴퓨터 과학] Ch3. 배열(Array) (0) 2021.11.05 [모두를 위한 컴퓨터 과학] Ch2. C언어 (0) 2021.10.29 [모두를 위한 컴퓨터 과학] Ch1. 컴퓨팅 사고(Computational Thinking) (0) 2021.10.11