메모리 주소
16진수(Hexadecimal)
컴퓨터과학에서는 숫자를 10진수나, 2진수 대신 16진수로 표현하는 경우가 많다.
4bits 씩 16진수로 변환 후 0x를 붙혀 뒤에 오는 문자들이 16진수임을 알려준다.
컴퓨터는 8개의 비트가 모인 바이트 단위로 정보를 표현 한다.
그래서 2개의 16진수는 1byte의 2진수로 변환되기 때문에 정보를 표현하기 매우 유용하다.
C에서는 변수의 메모리상 주소를 받기 위해 &이라는 연산자를 사용할 수 있다.
#include <stdio.h>
int main(void)
{
int n = 50;
printf("%p\n", &n);
}
위 코드를 실행하면 ‘0x7ffe00b3adbc’와 같은 값을 얻을 수 있고, 이는 16진법으로 표현된 메모리의 주소이다
반대로 *를 사용하면 그 메모리 주소에 있는 실제 값을 얻을 수 있다.
#include <stdio.h>
int main(void)
{
int n = 50;
printf("%i\n", *&n);
}
포인터
#include <stdio.h>
int main(void)
{
int n = 50;
int *p = &n;
printf("%p\n", p);
printf("%i\n", *p);
}
위 코드에서는 정수형 변수 n에 50을 할당하고,
*p라는 포인터 변수에 &n이라는 값, 즉 변수 n의 주소를 저장한다.
첫번째 프린트 함수는 변수 n의 주소를 출력하고,
두번째 프린트 함수는 p가 가르키는 변수의 값 즉 변수 n의 값을 출력한다.
문자열
우리는 문자열을 저장하기 위해 CS50 라이브러리에 포함된 string 자료형을 사용했다.
string s = “EMMA”;
||
typedef char *string
여기서 typedef는 새로운 자료형을, char *은 문자에 대한 포인터를, string은 자료형의 이름을 의미한다.
#include <cs50.h>
#include <stdio.h>
int main(void)
{
string s = "EMMA";
printf("%s\n", s);
}
#include <stdio.h>
int main(void)
{
char *s = "EMMA";
printf("%s\n", s);
}
문자열 비교
#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]);
이렇게 하면 EMMA의 첫번째 부터 네번째 주소값 까지 출력할 수 있다.
printf("%c\n", *s);
printf("%c\n", *(s+1));
printf("%c\n", *(s+2));
printf("%c\n", *(s+3));
그래서 이 코드는 EMMA를 순서대로 출력할 것이다.
문자열 복사
#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라는 변수에는 emma라는 문자열이 아닌 그 문자열이 있는 메모리의 주소가 저장되기 때문이다.
따라서 t도 s와 동일한 주소를 가리키고 있고, t를 통한 수정은 s에도 그대로 반영이 되는 것이다.
#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);
}
malloc 이라는 함수를 이용해서 t를 정의해야 한다.
malloc 이라는 함수는 정해진 크기 만큼 메모리를 할당하는 함수이다.
새로운 공간을 만들고 그곳에 값을 채운다고 생각하면 된다.
단, s 문자열의 길이에 널 종단 문자에 해당하는 1을 더한 만큼 메모리를 할당한다.
이후 실행하면 s는 emma t는 EMMA 가 출력된다.
메모리 할당과 해제
malloc 함수를 이용하여 메모리를 할당한 후에는 free라는 함수를 이용해서 메모리를 해제해줘야 한다.
그러지 않으면, 메모리에 저장한 값은 쓰레기 값으로 남게 되어 메모리 용량의 낭비가 발생하기 때문이다.
이것을 메모리 누수 라고 한다.
valgrind 라는 프로그램을 쓰면 코드에 메모리 문제가 있는지 확인할 수 있다.
help50 valgrind ./filename
#include <stdlib.h>
void f(void)
{
int *x = malloc(10 * sizeof(int));
x[10] = 0;
}
int main(void)
{
f();
return 0;
}
그리고 위 코드는 40바이트의 메모리를 할당했는데,
10 index에 0을 할당하면서 버퍼 오버플로우가 발생하게 된다. (x[10] = 0 부분 )
따라서 0~9까지의 인덱스를 사용해야 한다.
또한 메모리 누수는 x라는 포인터를 통해 할당한 메모리를 해제하기 위해 free(x)라는 코드를 추가해줌으로써 해결할 수 있다.
메모리 교환, 스택, 힙
#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으로 할당된 메모리의 데이터가 저장된다.
그리고 스택에는 프로그램 내의 함수와 관련된 것이 저장된다.
따라서 a, b, x, y, tmp와 같은 것들은 모두 스택영역에 저장되지만,
a, x와 b,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에 의해 메모리가 더 할당될 수록, 점점 사용하는 메모리의 범위가 아래로 늘어간다.
스택 영역에서도 함수가 더 많이 호출 될수록 사용하는 메모리의 범위가 점점 위로 늘어난다.
이렇게 늘어나다보면, 제한된 메모리 용량 하에서는 기존의 값을 침범하는 상황이 발생한다.
이를 힙 오버플로우, 스택 오버플로우 라고 한다.
파일쓰기
[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");
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함수로 파일에 대한 작업을 종료해야 한다.
파일읽기
JPEG 형식의 파일을 정의할 때 만든 개발자의 마법의 단어인 0xFF, 0xD8, 0xFF로
해당 파일이 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);
}
'🖥️Computer Science > CS50' 카테고리의 다른 글
CS50_자료구조 (0) | 2023.06.13 |
---|---|
CS50_알고리즘 (0) | 2023.06.13 |
CS50_배열 (0) | 2023.06.13 |
CS50_C언어 (0) | 2023.06.06 |
CS50_컴퓨팅 사고 (0) | 2023.06.06 |