boostcource
모두를 위한 컴퓨터 과학 (CS50 2019) : David J. Malan
www.boostcourse.org/cs11
실습환경 : CS50 Sandbox & CS50 IDE
C로 작성한 변수들이 실제로 컴퓨터 메모리에 저장되는 모습에 대해서 알아보자.
메모리 주소를 나타내는 방법과 그 주소를 알아내는 방법, 그 주소에 찾아가는 방법을 차례로 보자.
문자열
복습해보자면 문자열은 문자의 배열이다.
string s = "EMMA";
위 처럼 선언된 변수 s은 사실은 ["E", "M", "M", "A", \0] 라는 문자열을 가리키는 포인터가 된다.
더 상세히는 문자열의 가장 첫번째 문자인 s[0]을 가리키는 것, 주소 0x123에 있는 것을 가리키는 것이다.
실제 CS50 라이브러리 속 string 자료형은 아래와 같이 작성되어 있는데
typedef char *string
문자에 대한 포인터(char *)를 string이라는 이름의 새로운 자료형으로 선언(typedef)한다는 의미이다.
때문에 아래와 같이
string 자료형을 이용하거나, char 포인터를 이용하거나 같은 결과를 가질 수 있음을 알 수 있다.
#include <cs50.h>
#include <stdio.h>
int main(void){
string s1 = "EMMA";
printf("%s\n", s1); // ? EMMA
char *s2 = "EMMA";
printf("%s\n", s2); // ? EMMA
}
형식 지정자 %p 를 이용해서 위에서 정리한 내용인
변수 s가 문자열의 첫번째 문자, 즉 주소 0x42ad5a에 있는 s[0]을 가리킨다는 것도 확인할 수 있다.
#include <cs50.h>
#include <stdio.h>
int main(void){
string s = "EMMA";
printf("%s\n", s); // ? EMMA
printf("%p\n", &s); // ? 0x7fff9dab57d8 // s의 주소
printf("%p\n", s); // ? 0x42ad5a // s의 첫 번째 문자의 주소
printf("%p\n", &s[0]); // ? 0x42ad5a
}
문자열 비교
문자열이 저장되어 있는 방식에 근거해서 문자열 자료형을 비교해보자.
형식 지정자 %p 를 이용해서 s를 출력하면 s[0]에 해당하는 메모리 주소가 출력됨을 위에서 확인했다.
int main(void){
string s = "EMMA";
printf("%s\n", s); // ? EMMA
printf("%p\n", s); // ? 0x42ad5a // "E"에 해당하는 메모리 주소
}
그리고 & 연산자를 앞에 붙여 s라는 문자열의 n 번째 문자에 해당하는 주소값을 차례로 출력할 수 있다.
printf("%p\n", &s[0]); // ? 0x42ad5a
printf("%p\n", &s[1]); // ? 0x42ad5b
printf("%p\n", &s[2]); // ? 0x42ad5c
printf("%p\n", &s[3]); // ? 0x42ad5d
이 주소값은 인덱스가 연속되는 것과 같이 1씩 증가함을 알 수 있다.
이를 이용해서 역으로 주소값을 하나씩 증가시켜 문자의 값을 출력할 수도 있다.
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");
}
}
// ? s: hello
// ? t: hello
// ? Different
정확한 비교를 위해서는 실제 문자열이 저장되어 있는 곳으로 이동해서 각 문자를 하나하나씩 비교해야 한다.
아래는 임의로 작성해 본 문자열 비교 프로그램이다.
#include <cs50.h>
#include <stdio.h>
#include <string.h>
int main(void){
string s = get_string("s: ");
string t = get_string("t: ");
string answer; // 답을 저장할 문자열 answer
// s와 t의 길이가 동일한지 우선 비교
int s_len = strlen(s);
int t_len = strlen(t);
if(s_len != t_len){
answer = "Different\n";
}
// 주소값을 하나씩 증가시키면서 *s == *t 비교
int count = 0;
for(int i = 0; i < s_len; i++){
if(*(s+i) == *(t+i)){
count++;
}
}
if(count == s_len){
answer = "Same\n";
} else {
answer = "Different\n";
}
printf("%s", answer);
}
s와 t의 길이가 같은지 string 라이브러리의 strlen 함수를 이용해 비교를 우선 진행한 뒤
* 를 이용해 s와 t의 실제 문자열이 저장되어 있는 곳으로 이동해
변수 i를 통해 주소값을 하나씩 증가시키면서 비교하는 형식으로 프로그램을 작성한 예시이다.
또는 아래와 같이 string 라이브러리의 strcmp 함수를 이용해서 바로 비교를 진행할 수도 있다.
#include <cs50.h>
#include <stdio.h>
#include <string.h>
int main(void){
string s = get_string("s: ");
string t = get_string("t: ");
int answer = strcmp(s, t); // s와 t가 동일하면 0 반환
if(answer == 0){
printf("Same\n");
} else {
printf("Different\n");
}
}
문자열 복사
문자열이 메모리에 저장되어 있는 방식에 대해 배웠으니
이제는 이미 저장되어 있는 문자열을 다른 곳으로 복사하기 위한 방법을 알아보자.
#include <cs50.h>
#include <ctype.h>
#include <stdio.h>
int main(void){
string s = get_string("s: "); // > Hello
s[0] = tolower(s[0]);
printf("s: %s\n", s); // ? hello
}
위는 문자열 s를 사용자로부터 입력받은 뒤, s의 첫 번째 문자를 소문자로 변경하는 프로그램이다.
(소문자로 변환하는 것은 ctype 라이브러리의 tolower 함수를 사용한다. 대문자로의 변환은 toupper 이다.)
여기서 문자열 t를 선언하면서 s를 대입해주면 복사가 되지 않을까? 확인해보자.
#include <cs50.h>
#include <ctype.h>
#include <stdio.h>
int main(void){
string s = get_string("s: "); // > Hello
s[0] = tolower(s[0]);
printf("s: %s\n", s); // ? hello
string t = s;
t[0] = toupper(t[0]);
printf("s: %s\n", s); // ? Hello
printf("t: %s\n", t); // ? Hello
}
string t = s; 를 이용해 대입한 뒤, t의 첫 번째 문자를 대문자로 변경해보았다.
그 다음 문자열 s와 t를 각각 출력해 비교해보니 s의 첫 번째 문자도 똑같이 대문자로 변경된 것을 확인할 수 있다.
이유는
변수 t에는 문자열 s가 아닌 문자열 s가 있는 메모리의 주소가 저장된 것이기 때문이다.
string s 가 char *s 와 동일한 의미였던 것을 떠올려보자.
따라서 string t = s; 를 하면 t도 s와 동일한 주소를 가리키는 것이기에
t를 통해 수정한 것이 s에도 똑같이 반영되는 것이다.
그렇다면 실제 메모리상에서 복사하려면 어떻게 해야할까?
C에서는 메모리 할당 함수 malloc 를 사용하면 된다.
malloc 함수는 stdlib 라이브러리에 존재한다.
#include <cs50.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void){
string s = get_string("s: "); // > Hello
s[0] = tolower(s[0]);
printf("s: %s\n", s); // ? hello
string 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); // ? hello
printf("t: %s\n", t); // ? Hello
}
malloc 함수는 정해진 크기 만큼 메모리를 할당하는 함수로
string t = malloc(strlen(s)+1) 을 해석하자면
문자열 s의 길이 + 1(널 종단 문자) 크기 만큼의 메모리를 새롭게 할당한 것이다.
그다음에 for문을 사용해서 문자열 s 배열에 있는 문자 하나하나를 t 배열에 복사하는 것이다.
이렇게 일반 문자 지정하듯이 바로 대입하는 것이 아닌 하나하나씩 복사해주어야 함을 기억해야 한다.
그렇지 않으면 위와 같이 서로 의존하고 있기에 원치 않은 영향을 주는 상황이 일어날 수 있다.
메모리 문제 파악하기
메모리 할당 함수 malloc 가 존재한다면 메모리 해제 함수 free 도 존재한다.
malloc 를 이용해서 메모리를 할당 했다면 free 를 이용해 메모리 해제를 해줘야 하는데
그렇지 않으면 메모리에 저장한 값이 쓰레기 값으로 남게 되어 메모리 용량의 낭비가 발생하기 때문이다.
이 현상을 메모리 누수라고 말한다.
이로 인해 다른 응용 프로그램의 작동 메모리가 부족하거나 하는 문제가 발생할 수 있다.
우리가 작성한 프로그램에 메모리와 관련된 문제가 있는지를 확인하기 위해서는
help50의 valgrind라는 프로그램을 사용해 검사해 볼 수 있다.
$ help50 valgrind ./파일명
valgrind를 아래의 코드에서 사용해보자.
#include <stdlib.h>
void f(void){
int *x = malloc(10 * sizeof(int));
x[10] = 0;
}
int main(void){
f();
return 0;
}
위 코드에서 f 함수는
포인터 x는 int 자료형의 사이즈(4byte)의 10배에 해당하는 40byte 크기의 메모리를 할당한다.
(10개의 int 자료형 배열을 만들었다 라고도 해석 가능하다.)
그리고 x의 10번째 값에 0을 할당한다.
이 프로그램을 valgrind 로 검사해보면 아래와 같이 결과를 출력해준다.
확인 되는 오류는 버퍼 오버플로우와 메모리 누수 두 가지이다.
버퍼 오버플로우
==2069== Invalid write of size 4
==2069== at 0x4229CD: f (index.c:5)
==2069== by 0x422A03: main (index.c:10)
Looks like you're trying to modify 4 bytes of memory that isn't yours? Did you try to store something
beyond the bounds of an array? Take a closer look at line 5 of index.c.
- 10개의 int형 배열을 만들었으므로 x에는 11번째 인덱스(x[10])이 존재하지 않기 때문에 발생
- 0 ~ 9 사이 인덱스 (x0] ~ x[9]) 만 사용하면 해결 가능
메모리 누수
==2011== 40 bytes in 1 blocks are definitely lost in loss record 2 of 2
==2011== at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2011== by 0x422901: f (index.c:4)
==2011== by 0x4229B3: main (index.c:9)
Looks like your program leaked 40 bytes of memory. Did you forget to free memory that you
allocated via malloc? Take a closer look at line 4 of index.c.
- x라는 포인터를 통해 할당한 메모리를 해제하지 않기 때문에 발생
- free(x) 를 추가해줌으로써 해결 가능
주어진 오류를 해결하면 아래와 같은 코드가 완성된다.
#include <stdlib.h>
void f(void){
int *x = malloc(10 * sizeof(int));
x[9] = 0;
free(x);
}
int main(void){
f();
return 0;
}
Sorry, help50 does not yet know how to help with this!
메모리 교환
변수 두 개의 값을 서로 교환하는 프로그램을 아래와 같이 작성하려고 한다.
#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); // ? x is 1, y is 2
swap(x, y);
printf("x is %i, y is %i\n", x, y); // ? x is 1, y is 2
}
void swap(int a, int b){
int tmp = a;
a = b;
b = tmp;
}
하지만 의도와 다르게 swap(x, y) 를 진행하고도 값이 변하지 않았음이 확인됐다.
swap 함수는 교환 작업을 제대로 하고 있으나
교환하는 대상이 x와 y 그 자체가 아닌 함수 내에서 새로 정의된 a와 b 이기 때문에
x와 y 값을 복제한 것과 같아 서로 다른 메모리 주소에 저장된 것이다.
더 상세히 알아보기 위해서 메모리 구조를 확인해보자.
메모리 구조
메모리 안에는 다음과 같이 데이터를 저장하는 구역이 나뉘어져 있다.
- machine code : 프로그램이 실행될 때 그 프로그램이 컴파일된 바이너리가 저장된다.
- globals : 프로그램 안에서 저장된 전역 변수가 저장된다.
- heap (힙) : malloc로 할당된 메모리의 데이터가 저장된다.
- stack (스택) : 프로그램 내의 함수와 관련된 것들이 저장된다.
위 구역 분배를 예시에 적용해 확인해보자면 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); // ? x is 1, y is 2
swap(&x, &y);
printf("x is %i, y is %i\n", x, y); // ? x is 2, y is 1
}
void swap(int *a, int *b){
int tmp = *a;
*a = *b;
*b = tmp;
}
복습 퀴즈
Q1. 아래와 같이 변수 s를 생성했을 때, 문자 'W'를 출력하는 코드는? (단, 포인터를 활용한다)
char *s = "EDWITH";
답: printf("%c \n", *(s+2));
포인터를 활용하지 않으면 printf("%c \n", s[2]); 로 표현 가능하다.
Q2. 아래와 같이 변수 s를 생성한 뒤, 새로운 변수 t에 "EDWITH" 문자를 복사하려고 한다. malloc 함수를 이용해 변수 t를 생성할 때 총 몇 바이트의 메모리를 할당해야 할까?
char *s = "EDWITH";
답: 7 바이트
문자열의 길이 6 + 널 종단 문자 1
Q3. 할당된 메모리를 해제 하기 위해 사용하는 함수는?
① malloc()
② unmemory()
③ mfree()
④ free()
답: ④ free()
메모리를 할당하기 위해 malloc()를 사용하고, 해제하기 위해 free()를 사용한다.
Q4. malloc() 함수를 통해 할당받은 메모리가 위치하는 곳은?
① 머신 코드(machine code)
② 글로벌(globals)
③ 힙(heap)
④ 스택(stack)
답: ③ 힙(heap)
글로벌(globals)에는 전역 변수가, 스택(stack)에는 프로그램 내의 함수와 관련된 것들이 저장된다.
Q5. 아래 코드와 같이 swap 함수를 통해 메모리에 저장된 x와 y의 값을 교환하려고 한다. 즉, swap 함수가 호출된 이후 x는 5, y는 3의 값을 가져야 한다. main 함수에서 호출되는 swap 함수의 괄호에 포함되어야 할 코드로 적절한 것은?
#include <stdio.h>
void swap(int *a, int *b);
int main(void){
int x = 3;
int y = 5;
swap( /* 답 */ );
}
void swap(int *a, int *b){
int tmp = *a;
*a = *b;
*b = tmp;
}
① x, y
② *x, *y
③ &a, &b
④ &x, &y
답: ④ &x, &y
x와 y의 주소를 swap 함수로 보내어 교환한다. swap(x, y)를 할 경우는 값을 복제한 것과 같아 a, b 와는 별개가 된다.
'CS > CS50' 카테고리의 다른 글
[CS][CS50] 자료구조 - malloc과 포인터 복습 / 배열의 크기 조정하기 (0) | 2023.06.19 |
---|---|
[CS][CS50] 메모리 - 파일 쓰기 / 파일 읽기 (1) | 2023.06.19 |
[CS][CS50] 메모리 - 메모리 주소 / 포인터 (1) | 2023.06.18 |
[CS][CS50] 알고리즘 - 재귀 / 병합 정렬 / 정렬 알고리즘의 실행시간 (0) | 2023.06.18 |
[CS][CS50] 알고리즘 - 버블 정렬 / 선택 정렬 (0) | 2023.06.17 |