[TIL]20210701

박창현·2021년 7월 1일
0

TODAY I LEARNED

목록 보기
9/53

C

문자열 복사

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

int main(){
    char *s = get_string("s: ");
    char *t = s;

    t[0]=toupper(t[0]);

    printf("%s\n",s);
    printf("%s\n",t);
}

위의 코드를 실행하고 emma를 입력하면 각각의 printf에 Emma가 출력된다. 이는 char *t = s;가 실질적으로는 0x123과 같은 메모리의 주소를 가지고 있기에 t와 s는 결국 별명이 다를뿐 같은 주소 안에 이름을 가지고 있고, 이 이름을 수정당한 것으로 볼 수 있다. t와 s엔 문자열이 아닌 메모리의 주소가 저장되기 때문으로 생각하면 된다.

#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(n)은 n 자리만큼 메모리공간을 할당해준다는 것이다. #include <stdlib.h>가 필요함. 저기에서 +1을 하는 이유는 종단문자(\0)도 같이 넣기 위해서이다. 종단문자가 없다면 무한하게 뒤의 값을 불러올 가능성이 있음으로 꼭 더해줘야한다.

위의 코드를 이용하면 t의 새로운 메모리 공간을 만들어내고 그 공간에 s의 문자열을 for문을 통해 그대로 복사한다고 볼 수 있다.

    for (int i = 0, n = strlen(s); i < n + 1; i++)
    {
        t[i] = s[i];
    }

// for 대신에 strcpy를 사용할 수 있다.

    strcpy(t,s);

문자열 복사는 자주 사용해서 strcpy라는 함수가 이미 정의되어있다.

메모리 할당과 해제. 메모리 누수.

valgrind ./filename을 이용하면 메모리 관련 문제를 확인 할 수 있다.

void f(void)
{
    int *x = malloc(10 * sizeof(int));
    x[10] = 0;
}

누수에 대해 보기에 앞서 위의 코드를 보자.
sizeof(n)은 말그대로 int나 float, char등의 크기를 반환해준다. 즉, 위의 코드에서는 x에 40바이트를 할당한다. 여기서 40바이트를 받은거랑 배열이 40개 있는거랑 헷갈리면 안된다. 40바이트가 int형으로 선언되었으니까 총 배열은 10칸([0] ~ [9])이다. x[10]은 할당되지 않았다! 이렇게 할당되지 않은 메모리에 접근하는 것을 버퍼 오버플로우라고 한다.

malloc 함수를 이용하여 메모리를 할당한 후에는 free라는 함수를 이용하여 메모리를 해제해야한다.
그렇지 않은 경우 메모리에 저장한 값은 쓰레기 값으로 남게 되어 메모리 용량의 낭비가 발생하게 되기 때문.
이러한 현상이 ‘메모리 누수'이다.

여기서 궁금한게 malloc함수를 사용하지않고 함수를 선언하면 자동으로 free가 되서 메모리 누수가 생기지 않는건가? 이다. char *s = get_string("s: ");을 통해 메모리가 할당된 것에 대해서는 누수됬다는 안내가 없길래 의문이 든다.
--> 다음차시 강의를 들으면서 해결. get_string()도 malloc을 사용하지만 cs50 라이브러리에는 get_string()에 대한 쓰레기 수집(가비지 컬렉트)기능이 있어 프로그램이 종료될 때 해제되지 않은 메모리를 알아서 해제해준다.

메모리 교환, 스택, 힙

#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는 서로 다른 메모리 주소에 저장된다.

메모리 레이아웃

머신 코드 영역에는 우리 프로그램이 실행될 때 그 프로그램이 컴파일된 바이너리가 저장됩니다.
글로벌 영역에는 프로그램 안에서 저장된 전역 변수가 저장됩니다.
영역에는 malloc으로 할당된 메모리의 데이터가 저장됩니다.
스택에는 프로그램 내의 함수와 관련된 것들이 저장됩니다.

스택 내부에서도 함수별로 공간을 따로 배정받는다.

#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); 는 x와 y 의 '값'대신 '메모리 주소'를 보낸다는 것이고 이에 맞춰 void swap(int *a, int *b) 를 통해 포인터 a, b를 선언하여 값을 복제하는 대신 메모리 주소를 가리키는(?) 값을 복제해서 그림처럼 a, b가 새로운 값을 받는 대신 항상 x, y의 메모리 주소를 가르켜 swap이 발생하도록 만든다.

힙 영역에서는 malloc 에 의해 메모리가 더 할당될수록, 점점 사용하는 메모리의 범위가 아래로 늘어납니다.

마찬가지로 스택 영역에서도 함수가 더 많이 호출 될수록 사용하는 메모리의 범위가 점점 위로 늘어납니다.

이렇게 점점 늘어나다 보면 제한된 메모리 용량 하에서는 기존의 값을 침범하는 상황도 발생할 것입니다.

이를 힙 오버플로우 또는 스택 오버플로우라고 일컫습니다.

포인터 변수(EX, char *x)는 그 자체가 주소로 정의됨으로 &(엠퍼센드, 변수의 주소를 가져오는 연산자)

scanf에 s가 아닌 &s로 그 주소를 입력해주는 부분을 유의하기 바랍니다.
scanf 함수의 변수가 실제로 스택 영역 안에 s가 저장된 주소로 찾아가서 사용자가 입력한 값을 저장하도록 하기 위함입니다.
반면 문자열을 받는 코드에서 scanf에 그대로 s를 입력합니다.
그 이유는 s를 크기가 n인 문자열, 즉 크기가 n인 char 자료형의 배열로 정의하였기 때문입니다.
clang 컴파일러는 문자 배열의 이름을 포인터처럼 다룹니다. 즉 scanf에 s라는 배열의 첫 바이트 주소를 넘겨주는 것이죠.

파일 쓰기

fopen이라는 함수를 이용하면 파일을 FILE이라는 자료형으로 불러올 수 있습니다.

fopen 함수의 첫번째 인자는 파일의 이름, 두번째 인자는 모드로 r은 읽기, w는 쓰기, a는 덧붙이기를 의미합니다.

사용자에게 name과 number라는 문자열을 입력 받고, 이를 fprintf 함수를 이용하여 printf에서처럼 파일에 직접 내용을 출력할 수 있습니다.

작업이 끝난 후에는 fclose함수로 파일에 대한 작업을 종료해줘야 합니다.

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

포인터, 포인티 요약
int *x; --> 정수 포인터 할당, 이 상태에서는 아무것도 가리키고 있지 않다.
포인터가 가리키고 있는 것을 포인티 라고 부른다.
malloc(sizeof(int));으로 정수형 포인티를 선언하고
x=malloc(sizeof(int)); <-- 이 코드를 통해 포인티를 가리키게 한다.

*x=42; 를통해 포인터x가 가리키고 있는(화살표를 따라) 포인티에 접근해서 42를 저장.
포인터 변수에는 메모리 주소가 저장됨. 이때 메모리 주소가 있는 곳으로 이동해서 값을 가져오고 싶다면 역참조(dereference) 연산자 *를 사용한다.

만약 int *y; 이후 바로
*y=13; 할 수 없다. 포인터y를 할당했지만. 포인티를 만드는 것은 별개의 과정인데 이를 수행한 적이 없기때문에, 포인터가 포인티를 향해 화살표를 만들어 놓지 않았기 때문이다.

x=y; 를 통해 두 포인터가 같은 포인티를 가리키도록 만들었다.(x와 y 모두 포인터이기에 정수 값을 주는 것이 아닌 주소 값을 주기에 가능하다.)

궁금증. 메모리 주소값을 가질때 char은 1바이트를 차지하고 int는 4바이트를 차지한다. char[0]=0x123 char[1]=0x124 인데 int의 경우 int[0]=0x123 이면 int[1]=0x124 인가 int[1]=0x127 인가??
혹시 주소값이니까 4바이트씩을 한덩어리로 묶어서 한 주소로 취급하는가 싶었다.
적어놓고 생각해보니까 바로 학인해 볼 수 있어서 해봤다.

  int *list = malloc(3*sizeof(int));
  printf("%p\n",&list[0]);
  printf("%p\n",&list[1]);
  printf("%p\n",&list[2]);
  

결과는
0xb8e260
0xb8e264
0xb8e268

이처럼 4바이트씩 묶여서 처리됨을 알 수 있다.

기존에 쓰던 배열에서 값을 더 넣기위해 배열의 크기를 키우려면 새로운 공간에 큰 크기의 메모리를 다시 할당하고 기존 배열의 값들을 하나씩 옮겨줘야한다.
따라서 이런 작업은 O(n), 즉 배열의 크기 n만큼의 실행 시간이 소요될 것.
기존 메모리 위치 바로 뒤에 메모리를 더 덧붙일 수도 있지만, 이 방법은 뒤에 메모리에 어떤 중요한 또는 중요하지 않은 값들이 저장되어있는지 모르기에 안전성을 위해 사용하지 않는다.

//int 자료형 4개 크기의 tmp 라는 포인터를 선언하고 메모리 할당
    int *tmp = malloc(4 * sizeof(int));

    if (tmp == NULL)
    {
        return 1;
    }

    // list의 값을 tmp로 복사
    for (int i = 0; i < 3; i++)
    {
        tmp[i] = list[i];
    }

    // tmp배열의 네 번째 값도 저장
    tmp[3] = 4;

위코드는 이미 list에 있는 값들을 tmp에 옮기는 과정이다.

    // tmp 포인터에 메모리를 할당하고 list의 값 복사
    int *tmp = realloc(list, 4 * sizeof(int));
    
    if (tmp == NULL)
    {
        return 1;
    }
    tmp[3] = 4;

for문이 해야할 일들을 realloc이 대신했다.

profile
개강했기에 가끔씩 업로드.

0개의 댓글