5. 메모리

eunheelog·2023년 6월 14일
0

boostcourse

목록 보기
9/13

boostcourse - 메모리

기초 지식


  • 16진수(Hexadecimal)
    - 10진수를 16진수로 바꾸기

    - 먼저 255 216 255를 2진수로 나타내보면 11111111, 11011000, 11111111이 된다.
    - 0000부터 1111까지 표현 가능
    - 4bit씩 16진수로 변환 후 0x를 붙여 뒤에 오는 문자가 16진수임을 알려준다.

1. 메모리 주소

  • 정수형 변수 n에 50이라는 값을 저장하고 출력하는 경우
    → n이라는 값은 int 타입이므로 4바이트 만큼의 자리를 차지함.

    SourceCode

    #include <stdio.h>
    
     int main(void)
     {
        int n = 50;
        printf("%p\n", &n);
     }

    → 메모리상 주소를 받기 위해 '&'이라는 연산자 사용

    SourceCode

    #include <stdio.h>
    
    int main(void)
    {
        int n = 50;
        printf("%i\n", *&n);
    }

    → 반대로 '*'를 사용하면 메모리 주소에 있는 실제 값을 얻을 수 있다 !

생각해보기-1


'CS50'을 16진수로 표현해볼까요?
① 2진수

  • C : 01000011
  • S : 01010011
  • 5 : 00110101
  • 0 : 00110000
    ② 16진수 (4bit씩 끊고 바꾸기 !)
  • C : 0100 → 4 / 0011 → 3 ∴ 0x43
  • S : 0101 → 5 / 0011 → 3 ∴ 0x53
  • 5 : 0011 → 3 / 0101 → 5 ∴ 0x35
  • 0 : 0011 → 3 / 0000 → 0 ∴ 0x30
    ∴ 0x43 0x53 0x35 0x30 이다.

ASCII 코드표

2. 포인터

SourceCode

#include <stdio.h>

int main(void)
{
   int n = 50;
   int *p = &n;
   printf("%p\n", p); // n의 주소값
   printf("%i\n", *p); // n의 값(p가 가리키는 변수 값)
}

- p라는 포인터 변수에 &n이라는 값(=n의 주소)을 저장
- int
p에서 p앞의 *은 이 변수가 포인터라는 의미
- int는 int 타입의 변수를 가리킨다는 의미

- 변수 p가 메모리가 위와 같이 저장된다.
(이때 아래 그림처럼 p가 n을 가리키고 있다고 생각해도 됨)

생각해보기-2


포인터의 크기는 메모리의 크기와 어떤 관계가 있을까요?
→ 포인터 변수는 자료형과 상관없이 메모리 크기가 항상 같다.

3. 문자열

  • 문자열은 결국 문자의 배열이고 문자열의 끝을 표시하는 '\0'을 붙인다.
  • 변수 s는 문자열을 가리키는 포인터가 된다.
    (문자열의 가장 첫번째 문자, 즉 주소 0x123에 있는 s[0]을 가리킴)
  • typedef는 새로운 자료형을, char *은 문자에 대한 포인터를, string은 자료형의 이름을 의미 !

SourceCode
1. string 자료형으로 "EMMA" 출력

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

int main(void)
{
    string s = "EMMA";
    printf("%s\n", s);
}
  1. char 포인터로 "EMMA" 출력
#include <stdio.h>

int main(void)
{
    char *s = "EMMA";
    printf("%s\n", s);
}
  • 2번 코드의 char *s에서 s라는 변수는 문자에 대한 포인터가 되고, "EMMA"라는 문자열의 가장 첫 번째 값을 저장하기 때문

생각해보기-3


string 자료형을 정의해서 사용하면 어떤 장점이 있을까요?
→ 문자열을 쉽게 출력할 수 있다.

4. 문자열 비교

SourceCode

#include <stdio.h>

int main(void)
{
    char *s = "EMMA";
    printf("%p\n", s);
}

→ s라는 포인터의 값, 즉 "EMMA"라는 문자열의 가장 첫 글자인 "E"에 해당하는 메모리 주소를 출력

printf("%p\n", &s[0]);
printf("%p\n", &s[1]);
printf("%p\n", &s[2]);
printf("%p\n", &s[3]);

→ 해당 위치의 문자에 대한 주소값 출력
(&s[0] : “E”의 주소값, &s[1]은 “M”의 주소값, &s[2]은 “M”의 주소값, &s[3]은 “A”의 주소값)

printf("%c\n", *s);
printf("%c\n", *(s+1));
printf("%c\n", *(s+2));
printf("%c\n", *(s+3));

→ 이렇게 주소값을 하나씩 증가시켜 출력할 수도 있다 !

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

int main(void)
{
    // 사용자로부터 s와 t 두 개의 문자열 입력받아 저장
    string s = get_string("s: ");
    string t = get_string("t: ");

    // 두 문자열을 비교 (각 문자들을 비교)
    if (s == t)
    {
        printf("Same\n");
    }
    else
    {
        printf("Different\n");
    }
}

→ 문자열이 저장된 변수로 비교하면 저장된 주소가 다르기 때문에 다르다.
∴ 문자열 하나씩 비교해야한다 !

생각해보기-4


문자열을 비교하는 코드는 어떻게 작성해야 할까요?
① 문자열의 길이가 다르면 다르다고 판단한다.
② 문자열의 길이가 같다면 문자열의 길이만큼 돌면서 해당 위치의 문자가 같은지 판단한다.

5. 문자열 복사

SourceCode

#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 모두 "Emma"라고 출력됨
(s라는 변수에는 문자열이 있는 메모리의 주소가 저장되기 때문)
→ string s는 char *s와 동일한 의미
SourceCode
메모리 할당 함수로 복사하기

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

    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이라는 함수로 t를 정의한다.
→ s 문자열의 길이에 1을 더한 만큼 메모리 할당 !

생각해보기-5


배운 바와 같이 메모리 할당을 통해 문자열을 복사하지 않고, 단순히 문자열의 주소만 복사했을 때는 어떤 문제가 생길까요?
→ 값을 바꾸게 되면 전체가 바뀌게 된다. (의도하지 않은 값 변경 발생)

6. 메모리 할당과 해제

  • malloc 함수를 이용해 메모리를 할당한 후에는 free라는 함수로 메모리를 해제해줘야한다 !
    (해제하지 않으면 메모리에 저장한 값은 쓰레기 값으로 남아 용량의 낭비가 발생함 → 메모리 누수)

    SourceCode

    #include <stdlib.h>
    
    void f(void)
    {
        int *x = malloc(10 * sizeof(int));
        x[10] = 0;
    }
    
    int main(void)
    {
        f();
        return 0;
    }

→ x에 int형 크기(4바이트) * 10만큼의 크기를 할당(40바이트)
→ x의 10번째 값으로 0을 할당
→ main에서 함수 f 호출, valgrind로 검사할 시 버퍼 오버플로우와 메모리 누수 확인 가능
→ x[10] = 0; 으로 인해 버퍼 오버플로우 발생
→ 메모리 누수는 x라는 포인터로 할당한 메모리를 해제하기 위해 free(x)라는 코드 추가하여 해결 !

생각해보기-6


제한된 메모리를 가지고 프로그래밍 할 때 메모리를 해제하지 않으면 어떤 문제가 발생할 수 있을까요?
→ 메모리에 저장된 값은 쓰레기 값으로 남게되고 결국 메모리를 낭비하게 됩니다.

7. 메모리 교환, 스택, 힙

SourceCode

#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 함수를 거친 후에도 값이 바뀌지 않음
(a와 b는 x와 y의 값을 복제한 것이기 때문)
(= 서로 다른 메모리 주소에 저장됨)

  • 데이터 저장되는 구역
    - 머신 코드 영역
    : 프로그램이 컴파일된 바이너리 저장
    - 글로벌 영역
    : 프로그램 안에서 저장된 전역 변수 저장
    - 힙 영역
    : malloc으로 할당된 메모리의 데이터 저장
    - 스택 영역
    : 프로그램 내의 함수와 관련된 것 저장

    SourceCode

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

    → 위의 코드에서 a와 b를 x와 y를 가리키는 포인터로 지정하면 된다 !

생각해보기-7


메모리 영역을 다양하게 나누는 이유는 무엇일까요?
→ 메모리를 효율적으로 접근, 사용, 관리하기 위해서

8. 파일 쓰기

  • 사용자에게 입력 받기

    SourceCode
    [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로 입력해줘야한다 !)

    [파일 쓰기]

    #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는 덧붙이기)
    → fprintf로 파일에 직접 내용을 출력할 수 있음.
    → 작업이 끝난 후 fclose로 종료해줘야 함.

생각해보기-8


get_long, get_float, get_char도 비슷한 방식으로 직접 구현할 수 있을까요?
→ get_int에서 %i를 %ld, %f, %c로 바꾸면 된다 !

9. 파일 읽기

SourceCode
파일 내용을 읽고 파일의 형식이 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);
}

→ 파일의 이름을 입력으로 받음.
→ if(argc != 2)인 경우 1을 return하고 종료
→ if(argc == 2)인 경우 그대로 진행
→ 입력받은 파일명(argv[1])을 읽기(r) 모드로 불러옴
→ 파일이 제대로 열리지 않으면 NULL을 return, 제대로 쓸 수 없다면 1 return
→ 파일이 잘 열렸다면 계속 진행
→ 크기가 3인 문자 배열을 만들어 fread 함수로 파일에서 첫 3바이트를 읽어옴
*fread(배열, 읽을 바이트 수, 읽을 횟수, 읽을 파일)
→ 마지막으로 읽은 바이트가 각각 0xFF, 0xD8, 0xFF인지 확인

생각해보기-9


JPEG 외에 다른 파일 형식도 그 형식임을 알려주는 약속이 있을까요?
→ 있을 것 같다.

profile
⛧1일 1알고리즘⛧

0개의 댓글