C, 메모리, 동적 할당


이번 챕터의 목표


입력하는 숫자의 갯수를 입력받고, 입력받은 숫자들을 오름차순 정렬하기


c_malloc_goal

단, 배열의 크기를 넉넉하게 미리 잡아놓지 않기!

배열의 크기를 입력받고 난 후, 그 크기만큼의 배열을 생성해야합니다.




메모리 할당


지금까지 프로그래밍을 하면서 변수를 선언하고 이용하면

그 변수는 메모리에 자동으로 저장되었습니다.

이를 다른 말로, 변수를 위해 메모리를 할당한다 라고 표현합니다.


지금까지 우리가 살펴봤던 변수들은 모두 지역변수(자동변수)입니다.

특정 스코프 내에서 선언되어, 스코프가 끝나면 자동으로 사라집니다.

하지만, 모든 변수가 그런것은 아닙니다.

어떤 변수는 프로그램이 끝날 때까지 살아있는 것도 있습니다.

이는 변수가 어느 메모리 영역에 저장되어 있는지에 따라 다릅니다.

즉, 변수의 특성에 따라 저장되는 위치가 다릅니다.

먼저 메모리의 종류에 대해서 간단하게 살펴보겠습니다.


분류 특징
PE
image
(실행파일)
Data
Section
Read/Write - 정적변수나 전역변수들이 사용하는 메모리 영역
- 별도의 초기화가 없을 경우 0으로 초기화
- 자동관리(할당 및 해제를 신경쓸 필요 없음)
Read only - 상수형태로 기술하는 문자열(예: "teemo")이 저장되는 영역
- 읽기만 가능
Text
section
- C 코드가 번역된 기계어가 저장된 메모리 영역
- 읽기만 가능
- 이 영역의 메모리 변조: 해킹
Heap - 동적 할당할 수 있는 자유 메모리 영역
- 개발자가 수동으로 관리
- 대량의 메모리가 필요하거나 필요 메모리를 모를 때 사용
Stack - 지역변수(자동변수)가 저장되는 메모리 영역
- 임시 메모리
- 크기 작고, 관리(할당 및 해제)가 자동으로 이루어짐

우리가 지금까지 프로그래밍 하면서 사용했던 변수들은

모두 스택 영역에서 관리되어왔습니다.

이번에 살펴볼 동적할당이란 표에서도 볼 수 있듯이 Heap영역에서 관리합니다.

우리는 지금까지 스택영역만 사용하면서 별 문제없이 프로그래밍 해왔습니다.

그런데 어떤 경우에 동적할당을 사용해야 할까요?


이 문제에 답을 알기위해선 동적 할당(Dynamic Allocation)이 뭔지 알아야합니다.

동적 할당이란 런타임(프로그램이 실행되는 시간)동안 메모리 공간을 할당하는 것을 말합니다.

지금까지 우리가 써온 지역변수는 컴파일과 동시에 메모리가 얼마나 할당되는지 정해져야 했습니다.

정확히는 Heap영역만 런타임에 메모리가 할당되고, 나머지 영역은 모두 컴파일 타임에 메모리가 할당됩니다.


예를 들어서 다음과 같은 int형 배열을 선언한다고 해보겠습니다.

지금까지 해왔던 방식으로 stack영역에 저장되는 변수들이고,

컴파일타임에 메모리의 크기가 정해져야 합니다.

int i = 0;
scanf("%d", &i);

int arr[i];

코드의 의도는 i값을 입력받아, 그 입력받은 크기만큼 배열을 만드는 것입니다.

하지만 이 코드는 컴파일을 하면 배열의 크기는 상수여야 한다면서 오류가 뜹니다.

왜냐하면 배열의 크기를 변수로 받게되면 stack영역에 메모리 할당을

얼마로 해야할지 정확히 정할 수 없기 때문입니다.

다음 코드도 안됩니다.

int i = 10;
int arr[i];

겉보기에는 값이 정해져 있는 것 같지만 컴파일 에러가 뜹니다.

결국 배열의 크기가 변수로 되어있기 때문입니다.


그럼 이런 예를 들어보겠습니다.

시험 점수를 입력 받아 평균을 출력하는 프로그램입니다.

그런데 몇명의 시험 점수를 받을지는 그때그때 다릅니다.

그럼 시험 점수는 int형 배열에 저장할텐데, 그 배열의 크기는 얼마로 선언해야할까요?

얼마로 해야할지 몰라 그냥 넉넉잡아 10000 으로 선언했다고 가정하겠습니다.

int score[10000] = {0};

그럼 10명의 점수 평균을 낸다면? 9990*4바이트 만큼의 메모리는 낭비됩니다.

100명의 점수 평균을 낸다면? 9900*4바이트 만큼의 메모리가 낭비됩니다.

그렇다면, 수도권에 사는 학생들의 점수를 받아 평균을 내는 상황이라면?

아마 학생수가 만명은 넘을 것이기 때문에(과연?), 제대로 된 값을 얻지 못할 것입니다.


이런 상황에 동적 할당을 유용하게 사용할 수 있습니다.

일단 아래 그림을 살펴보고 넘어가겠습니다.

c_memory_map_wiki

[출처: 위키백과]


조금 복잡하긴 하지만, 어떤 변수가 어느 영역에 저장되는지 잘 보여주고 있습니다.

Heap영역을 가리키는 코드부분을 살펴보면 malloc()함수가 보입니다.

이 malloc() 함수가 동적 할당할 때 쓰이는 함수입니다.


참고로 Heap영역은 화살표가 위에서 아래를 향하고있고,

Stack영역은 화살표가 아래에서 위로 향하고 있습니다.

이 방향대로 메모리가 할당된다고 보면 됩니다.

이를 조금 더 단순화 시키면 아래와 같이 표현할 수 있습니다.

c_memory_map_custom




메모리의 동적 할당 및 관리


malloc(), free()


malloc()함수는 메모리를 할당하는 함수입니다.

memory allocation의 약자로 보면 됩니다.

void *malloc(size_t size);

  • 매개변수: 할당받을 메모리의 바이트 단위 크기(size)
  • 반환값: Heap영역에 할당된 메모리 덩어리 중 첫번째 바이트 메모리 주소. 에러가 발생하면 NULL 반환
  • 설명: 기본적으로 쓰레기 값이 들어 있다.


이렇게 사용자가 임의로 메모리를 할당 했을시,

해당 메모리를 다 사용하고 나서 꼭 메모리를 반환해야 합니다.

그 메모리를 반환하는 함수는 free()입니다.

void free(void *memblock);

  • 매개변수: 반환할 메모리의 주소(memblock)
  • 반환값: 없음
  • 설명: 동적으로 할당받은 메모리를 운영체제에 반환하는 함수

간단한 예제를 살펴보겠습니다.

#include <stdio.h>
// malloc() 함수 사용 위해 헤더 추가
#include <stdlib.h>

int main(void)
{
    int *pArr = NULL;
    
    // 12바이트(sizeof(int)*3)의 메모리를 동적할당
    // 반환값은 시작주소인데, void* 형으로 반환되므로 int* 형으로 형변환
    pArr = (int*)malloc(sizeof(int)*3);
    
    // 동적할당한 메모리를 배열 연산자로 접근
    pArr[0] = 10;
    pArr[1] = 20;
    pArr[2] = 30;
    
    for(int i=0; i<3; i++){
        printf("%d ", *(pArr+i));
    }
    
    // 동적할당한 메모리를 해제
    free(pArr);
    
    printf("\n\n");
    return 0;
}

/*
10 20 30
*/

코드에 대한 설명은 주석에 적어져있습니다.

이 코드에서는 free(pArr)을 안해줘도 아마 에러가 안날 것입니다.

하지만 동적할당한 메모리를 free()함수로 반환하는 것은 매우 중요합니다.

할당은 했지만 반환하지 않은 메모리는 프로그램이 끝날 때까지 사용할 수 없습니다.

이런 현상을 메모리 누수(memory leak)라 부릅니다.

또한 코드가 복잡해지면 메모리 누수로 인해 어떤 에러가 발생할지 모릅니다.


memset(), calloc()


변수를 선언하면 즉시 0으로 초기화해주는 것이 일반적입니다.

친절하게 초기값을 0으로 설정해주는 운영체제도 있지만,

쓰레기값을 그대로 두는 운영체제도 있습니다.

아까 malloc()함수의 설명을 보면 기본값으로 쓰레기값이 들어간다고 나와있습니다.

이때 이 동적할당된 메모리의 값들을 0으로 초기화해주는 함수가 memset()함수입니다.

void *memset(void *dest, int c, size_t count);

  • 매개변수: 초기화할 대상 메모리 주소(dest), 초깃값(c, 이 값이 0이면 메모리를 0으로 초기화), 초기화 대상 메모리의 바이트 단위 크기(count)
  • 반환값: 대상 메모리 주소


또한 malloc()과 다르게, 아예 할당할 때부터

메모리를 0으로 초기화하고 할당하는 calloc()이란 함수도 있습니다.

void *calloc(size_t num, size_t size);

  • 매개변수: 요소의 개수(num), 각 요소의 바이트 단위 크기(size)
  • 반환값: Heap영역에 할당된 메모리 덩어리 중 첫번째 바이트 메모리의 주소, 에러가 발생하면 NULL 반환
  • 설명: 이 함수도 동적할당하는 함수이므로, 반드시 free()함수로 반환해야한다.


그럼 동적할당된 메모리를 0으로 초기화하는 두 방법을 사용한 코드를 보겠습니다.

#include <stdio.h>
// malloc(), calloc() 사용을 위한 헤더
#include <stdlib.h>
// memset() 사용을 위한 헤더
#include <string.h>

void printArr(int *arr, int size)
{
    for(int i=0; i<size; i++){
        printf("%d ", arr[i]);
    }
}

int main(void)
{
    int *pArr1 = NULL, *pArr2 = NULL;
    
    // malloc()으로 할당
    pArr1 = (int*)malloc(sizeof(int)*3);
    
    printf("malloc() 함수 직후 pArr1 출력\n");
    // pArr1 출력
    printArr(pArr1, 3);
    printf("\n\n");
    
    // memset()으로 pArr1메모리값을 0으로 초기화
    memset(pArr1, 0, sizeof(int)*3);
    printf("memset() 함수 직후 pArr1 출력\n");
    // pArr1 출력
    printArr(pArr1, 3);
    
    printf("\n\n");
    
    // calloc()로 pArr2에 동적할당
    pArr2 = (int*)calloc(3, sizeof(int));
    printf("calloc() 함수로 정의한 pArr2 출력\n");
    // pArr2 출력
    printArr(pArr2, 3);
    
    free(pArr1);
    free(pArr2);
    printf("\n\n");
    
    return 0;
}

/*
 malloc() 함수 직후 pArr1 출력
 0 0 271188084
 
 memset() 함수 직후 pArr1 출력
 0 0 0
 
 calloc() 함수로 정의한 pArr2 출력
 0 0 0
 */


malloc()을 이용하여 문자열 다루기


위 코드에서 동적할당한 메모리를 배열처럼 사용한 것을 볼 수 있었습니다.

당연한게, 결국 반환값이 할당된 메모리의 시작 주소이고,

이게 포인터 변수에 들어가기 때문입니다.

이전 챕터에서 포인터 변수는 결국 배열과 같다고 했었죠.

그럼 동적할당을 통한 문자열 다루기를 살펴보겠습니다.

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char str1[] = {"teemo"};
    char *str2 = "jinx";
    
    char *str3 = NULL;
    str3 = (char*)malloc(sizeof(char)*5);
    str3[0] = 'g';
    str3[1] = 'n';
    *(str3+2) = 'a';
    *(str3+3) = 'r';
    str3[4] = '\0';
    
    printf("str1: %s\n", str1);
    printf("str2: %s\n", str2);
    printf("str3: %s\n\n", str3);
    
    free(str3);
    return 0;
}

/*
 str1: teemo
 str2: jinx
 str3: gnar
 */


동적할당된 메모리 크기 확인


메모리를 동적할당 할때는 malloc()을 쓰든, calloc()를 쓰던

매개변수를 통해서 할당할 메모리의 크기를 명시하기 때문에

할당받는 입장에서는 그 크기를 모를 수 없습니다.

하지만 간혹 어떤 외부 함수에서 매개변수로 포인터를 받았는데

크기를 모르는 경우도 있습니다.

그럴 경우 사용할 수 있는 함수가 _msize() 또는 malloc_usable_size() 함수 입니다.

_msize() 함수는 윈도우에서만 가능하고,

malloc_usable_size()함수는 리눅스나 유닉스 환경에서 사용가능합니다.

(맥os에서는 이런 함수가 없는 것 같습니다… 제가 못찾는 것일수도…)

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int *str = NULL;
	str = (char*)malloc(sizeof(char) * 7);
	printf("_msize(str): %d\n\n", _msize(str));

	free(str);
	return 0;
}


sprintf()


sprintf()함수는 특정 주소의 메모리에 문자열을 저장하는(덮어쓰는) 함수입니다.

int sprintf(char *buffer, const char *format [, argument]…)

  • 매개변수: 출력문자열이 저장될 메모리 주소(buffer), 형식문자열이 저장된 메모리 주소(format), 형식문자열에 대응하는 가변인자들([, argument])
  • 반환값: 출력된 문자열의 개수

sprintf()는 보안 결함이 있기 때문에,

윈도우에서는 sprintf_s()함수를,

Linux/UNIX 에서는 snprintf()함수를 사용하는 것이 좋습니다.

그럼 간단한 예제를 살펴보겠습니다.

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char *str = NULL;
    str = (char*)malloc(sizeof(char)*20);

    sprintf(str, "%s", "show me the money");
    printf("original str: %s\n\n", str);

    sprintf(str, "%s", "give");
    printf("changed str: %s\n\n", str);
    
    free(str);
    
    return 0;
}

/*
 original str: show me the money
 
 changed str: give
 */


realloc()


realloc()함수는 함수명에서도 알 수 있듯이,

이미 할당된 메모리를 재할당하는 함수입니다.

void *realloc(void *memblock, size_t size);

  • 매개변수: 기존에 동적 할당한 메모리 주소(memblock, 만일 이 주소가 NULL이면 malloc()함수와 동일하게 동작), 다시 할당받을 메모리의 바이트 단위 크기(size)
  • 반환값: 다시 할당된 메모리 덩어리 중 첫번째 바이트의 메모리 주소, 만일 실패하면 NULL반환(이때 첫번째 매개변수로 전달된 메모리를 수동 반환해야함)

만일 이미 할당된 메모리 영역에서 크기를 조정할 수 있다면,

반환될 주소는 첫번째 매개변수로 전달된 주소와 같습니다.

하지만 불가능할 경우에는 기존의 메모리를 해제하고

새로운 영역에 다시 할당한 후, 새로 할당된 메모리의 주소를 반환합니다.

예제를 살펴보겠습니다.

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char *str1 = NULL, *str2 = NULL;
    
    str1 = (char*)malloc(sizeof(char)*5);
    sprintf(str1, "%s", "show");
    printf("address of str1: %p\n", str1);
    printf("str1: %s\n", str1);
    printf("address of str2: %p\n", str2);
    printf("str2: %s\n\n", str2);
    
    // 5바이트의 메모리를 30바이트로 확장 시도
    str2 = (char*)realloc(str1, 30);
    printf("after realloc...\n");
    printf("address of str1: %p\n", str1);
    printf("str1: %s\n", str1);
    printf("address of str2: %p\n", str2);
    printf("str2: %s\n\n", str2);
    // 만약 확장에 실패했다면 str2는 NULL값을 반환받음
    if(str2 == NULL){
        printf("realloc() fail\n");
        free(str1);
    }
    sprintf(str2, "%s", "show me the money");
    printf("after sprintf str2...\n");
    printf("address of str1: %p\n", str1);
    printf("str1: %s\n", str1);
    printf("address of str2: %p\n", str2);
    printf("str2: %s\n\n", str2);
    
    
    free(str2);
    
    return 0;
}

/*
 address of str1: 0x102316c30
 str1: show
 address of str2: 0x0
 str2: (null)
 
 after realloc...
 address of str1: 0x102316c30
 str1: show
 address of str2: 0x102353ec0
 str2: show
 
 after sprintf str2...
 address of str1: 0x102316c30
 str1: show
 address of str2: 0x102353ec0
 str2: show me the money
*/

c_dynamic_allocation_1

코드안에 주석으로 써논 건 맥의 Xcode에서 돌렸을 때 결과이고,

위 결과창은 윈도우에서 Visual Studio로 돌렸을 때의 결과입니다.

맥에서는 str1 자체의 주소에서는 확장이 불가능하여

따로 새로운 곳에 30바이트의 주소를 잡아 str2에 시작 주소를 반환합니다.

하지만 윈도우에서는 str1 자체에서 확장하여 주소값이 같게 나오는군요.




메모리 다루기


memcpy()


memcpy()함수는 매개변수로 전달된 두 주소의 메모리에 담긴 정보를 복사하는 함수입니다.

void *memcpy(void *dest, const void *src, size_t count);

  • 매개변수: 대상 메모리 주소(dest), 원본 메모리 주소(src), 복사할 바이트 크기(count)
  • 반환값: 대상 메모리 주소

간단한 예제를 살펴보겠습니다.

#include <stdio.h>
// memcpy()함수를 사용하기 위한 헤더 추가
#include <string.h>

int main(void)
{
    char str1[40] = {"Teemo, The Swift Scout"};
    char str2[40] = {0};
    
    // str1에서 5바이트만 str2로 복사
    memcpy(str2, str1, 5);
    puts(str2);
    
    // str1에서 10바이트만 str2로 복사
    memcpy(str2, str1, 10);
    puts(str2);
    
    // str1에서 1바이트만 str2로 복사
    memcpy(str2, str1, 1);
    puts(str2);
    
    // str1 전체를 str2로 복사
    memcpy(str2, str1, sizeof(str1));
    puts(str2);
    
    return 0;
}

/*
 Teemo
 Teemo, The
 Teemo, The
 Teemo, The Swift Scout
*/

주의할 점은 10바이트 복사했다가

오히려 더 짧게 1바이트 복사를 하면

말그대로 1바이트만 같은 ‘T’로 덮어쓰기 되고

뒤의 “eemo, The”는 그대로 있기 때문에

출력하면 “Teemo, The”가 나오게 됩니다.


memcmp()


메모리를 비교하는 함수입니다.

void memcmp(const void *buf1, const void *buf2, size_t count);

  • 매개변수: 비교 메모리주소1(buf1), 비교 메모리주소2(buf2), 비교할 메모리의 바이트 단위 크기(count)
  • 반환값: 0(같을 때), 양수(buf1이 buf2보다 클때), 음수(buf1이 buf2보다 작을때)

이 함수는 (첫번째 매개변수에 저장된 값 - 두번째 매개변수에 저장된 값)을 계산하여

반환값을 결정하는 함수입니다.

간단한 예제를 살펴보겠습니다.

#include <stdio.h>
#include <string.h>

int main(void)
{
    char str1[20] = "teemo";
    char *str2 = "Teemo";
    char *str3 = "teEmo";
    
    printf("t의 아스키 코드: %d\n", 't');
    printf("T의 아스키 코드: %d\n", 'T');
    printf("e의 아스키 코드: %d\n", 'e');
    printf("E의 아스키 코드: %d\n\n", 'E');
    
    // str1과 "teemo"는 완전 동일하므로 0이 나옴
    printf("memcmp(str1, \"teemo\", 6): %d\n", memcmp(str1, "teemo", 6));

    // str1과 str2를 첫번째 문자부터 비교했을 때,
    // str1의 첫문자인 't'의 아스키 코드가 str2의 첫문자인 'T'의 아스키 코드보다 크므로 양수가 나옴
    printf("memcmp(str1, str2, 6): %d\n", memcmp(str1, str2, 6));

    // str1과 str3를 2바이트 까지만 비교하니 둘다 "te"로 같음. 즉 0이 나옴
    printf("memcmp(str1, str3, 2): %d\n", memcmp(str1, str3, 2));

    // str1과 str3를 3바이트까지 비교해보니 앞의 2바이트는 "te"로 똑같은데
    // 3바이트째가 str1은 'e'고 str3는 'E'이다.
    // 그런데 아스키 코드는 'e'가 더 크므로 결국 str1이 더 큰것이 되어 양수 출력
    printf("memcmp(str1, str3, 3): %d\n\n", memcmp(str1, str3, 3));
    
    return 0;
}

/*
 t의 아스키 코드: 116
 T의 아스키 코드: 84
 e의 아스키 코드: 101
 E의 아스키 코드: 69
 
 memcmp(str1, "teemo", 6): 0
 memcmp(str1, str2, 6): 32
 memcmp(str1, str3, 2): 0
 memcmp(str1, str3, 3): 32
*/

설명은 코드에 나와있습니다.

참고로 주석의 결과는 맥의 Xcode에서 실행한 결과이며

아스키코드의 차이만큼의 숫자가 나오는 것을 볼 수 있습니다.

하지만 윈도우의 Visual Studio에서 실행했을 땐, 32대신 1이 나왔습니다.

여러분의 PC에서는 또 다르게 나올수도 있습니다.


다만 문자열을 비교할 때 이렇게 비교하지 않도록 주의해야합니다.

char str1[6] = "teemo";
char *str2 = "teemo";

printf("%d\n", str1 == str2);

위 코드는 문자열이 같은지 체크하는 코드가 아닙니다.

str1은 배열의 시작 주소, str2는 포인터 변수로

str1 == str2 는 각각의 주소가 같은지 비교하는 구문이 됩니다.


strcmp()


strcmp()함수도 memcmp()처럼 각 메모리 주소에 저장된 값을 비교하여

0, 양수, 음수를 출력합니다.

하지만 strcmp()는 비교 대상이 되는 정보를 문자열로 가정해서 처리하여

메모리의 길이를 매개변수로 받지 않습니다.

int strcmp(const char *string1, const char *string2);

  • 매개변수: 비교할 문자열1이 저장된 주소(string1), 비교할 문자열2가 저장된 주소(string2)
  • 반환값: 0(두 문자열이 동일), 양수(string1이 string2보다 사전상 나중), 음수(string2가 string1보다 사전상 나중)
  • 설명: 대소문자를 식별하여 두 문자열이 같은지 비교하는 함수

간단한 예제코드를 살펴보겠습니다.

#include <stdio.h>
#include <string.h>

int main(void)
{
    char str1[40] = {"Teemo, The Swift Scout"};
    char *str2 = "Teemo, The Swift Scout";
    char *str3 = "Teemo";
    
    // strcmp()는 비교 문자열의 주소에 상관없이
    // 문자열 자체가 같은지 비교
    // 길이가 다른 문자열은 다른 문자열
    
    printf("strcmp(str1, str2): %d\n", strcmp(str1, str2));
    printf("strcmp(str1, \"Teemo, The Swift, Scout\"): %d\n", strcmp(str1, "Teemo, The Swift Scout"));
    
    // 사전상으로 str1이 str3보다 뒤에 나온다.
    // 즉, str1이 더 크므로 양수가 나온다.
    printf("strcmp(str1, str3): %d\n\n", strcmp(str1, str3));
    
    return 0;
}

/*
 strcmp(str1, str2): 0
 strcmp(str1, "Teemo, The Swift, Scout"): 0
 strcmp(str1, str3): 44
*/


strstr()


strstr()함수는 문자열을 검색하는 함수입니다.

char *strstr(const char *string, const char *strCharSet);

  • 매개변수: 검색 대상이 될 문자열의 메모리 주소(string), 검색할 문자열의 메모리 주소(strCharSet)
  • 반환값: 문자열을 찾으면 해당 문자열이 저장된 메모리 주소 반환, 찾지못하면 NULL반환

간단한 예제코드를 살펴보겠습니다.

#include <stdio.h>
#include <string.h>

int main(void)
{
    char asd[30] = {"Teemo, The Swift Scout"};
    
    printf("asd: %p\n\n", asd);
    
    printf("strstr(asd, \"The\"): %p\n", strstr(asd, "The"));
    
    // asd에서 "S"란 문자열은 두군데 존재(Swift, Scout)
    // 이 중 먼저 나온 Swift의 S의 주소를 반환
    printf("strstr(asd, \"S\"): %p\n", strstr(asd, "S"));
    
    // asd에 "Jinx"란 문자열은 없으므로 NULL을 반환
    printf("strstr(asd, \"Jinx\"): %p\n\n", strstr(asd, "Jinx"));
    
    printf("asd에서 \"The\"가 제일 처음 나온 위치는 %d번째 입니다.\n", strstr(asd, "The") - asd);
    printf("asddptj \"S\"가 제일 처음 나온 위치는 %d번째 입니다.\n", strstr(asd, "S") - asd);
    
    return 0;
}

/*
 asd: 0x7ffeefbff5a0
 
 strstr(asd, "The"): 0x7ffeefbff5a7
 strstr(asd, "S"): 0x7ffeefbff5ab
 strstr(asd, "Jinx"): 0x0
 
 asd에서 "The"가 제일 처음 나온 위치는 7번째 입니다.
 asddptj "S"가 제일 처음 나온 위치는 11번째 입니다.
*/




변수와 메모리


아까 위에서도 말했지만, 변수는 성격에 따라

저장되는 메모리의 위치가 다릅니다.

다시한번 메모리 구조 그림을 살펴보겠습니다.

c_memory_structure

이렇게 다른 ‘메모리의 종류’를 ‘storage-class’ 또는 ‘기억부류’라 합니다.

그리고 원래 변수를 선언할 때는 변수 앞에 예약어를 붙어야하는데

이 예약어를 ‘기억부류 지정자(storage-class specifier)’라고 부릅니다.

C에서 기억부류 지정자는 extern(외부), auto(자동), static(정적), register(래지스터) 등이 있습니다.


먼저 auto에 대해 알아보겠습니다.

사실 우리가 지금까지 별다른 예약어 없이 선언한 변수는

모두 auto가 생략된 것입니다.

즉, 아래 두개의 선언은 동일한 선언입니다.

int a = 7;
auto int a = 7;

이 변수의 이름이 auto인 이유는 자동으로 메모리가 관리되기 때문입니다.

이 자동변수는 선언되면 stack영역에 저장됐다가, 선언된 스코프가 끝나면

알아서 stack영역에서 소멸됩니다.


다음으로 static에 대해 알아보겠습니다.

이러한 정적변수나 전역변수는 데이터영역에 저장되고,

프로그램이 시작될 때 확보되어 종료될 때까지 그대로 유지됩니다.

선언된 스코프가 끝나도 소멸되지 않습니다.

간단한 예제 코드를 살펴보겠습니다.

#include <stdio.h>

int staticVarIncrease(void)
{
    // 외부 함수 내에서 선언되었지만,
    // 함수가 끝나도 변수 a는 사라지지 않음
    // 다만 함수가 여러번 호출되도 선언부는 한번만 적용
    static int a = 7;
    a++;
    
    return a;
}

int main(void)
{
    printf("%d\n", staticVarIncrease());
    printf("%d\n", staticVarIncrease());
    printf("%d\n\n", staticVarIncrease());
    
    return 0;
}

/*
 8
 9
 10

*/


다음으로 register에 대해 알아보겠습니다.

레지스터는 일반 메모리가 아니라 CPU가 가진 메모리 입니다.

입/출력 속도가 CPU와 같기 때문에 컴퓨터에서 가장 빠른 메모리입니다.

이는 과거(약 20년전) 주기억장치의 속도가 느리던 시절에 사용됐습니다.

하지만 요즘에는 하드웨어는 물론 컴파일러도 매우 빨라졌기 때문에

지금은 레지스터 변수는 다루는 일이 PC에서는 큰 의미가 없습니다.

임베디드나 펌웨어 개발일이 아니면 쓸 일이 거의 없다는데, 저도 안해봐서 잘 모릅니다…

간단한 예제를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
    register int a = 7;
    printf("a: %d\n", a);
    
    //printf("%p", &a); <-- ERROR!
    
    return 0;
}

/*
 a: 7
*/

다만 주의할 점이 하나 있습니다.

레지스터 변수는 CPU의 일부이므로 별도의 주소가 없습니다.

CPU의 일부인 레지스터는 일반 메모리와 달리

주소가 아니라 고유명사로 식별됩니다.

즉, 주소가 없으므로 주소연산은 불가능합니다.




Back to the Goal


입력하는 숫자의 갯수를 입력받고, 입력받은 숫자들을 오름차순 정렬하기


c_malloc_goal

#include <stdio.h>
#include <stdlib.h>

void BubbleSort(int *arr, int size)
{
	int temp = 0;
	for (int i = 0; i < size; i++) {
		for (int j = 0; j < size - i - 1; j++) {
			if (arr[j] > arr[j + 1]) {
				temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
}

int main(void)
{
	int n = 0, *arr = NULL;
	printf("입력받을 숫자의 갯수를 입력하세요(2~10): ");
	scanf("%d", &n);
	putchar('\n');

	arr = (int*)malloc(sizeof(int)*n);

	// 숫자 입력받기
	for (int i = 0; i < n; i++)
	{
		printf("%d번째 수: ", i + 1);
		scanf("%d", arr + i);
	}

	putchar('\n');

	// 입력받은 수 출력
	printf("입력받은 수: ");
	for (int i = 0; i < n; i++) printf("%d ", *(arr + i));
	printf("\n\n");

	// 오름차순 정렬
	BubbleSort(arr, n);

	// 정렬한 숫자 출력
	printf("정렬된 수: ");
	for (int i = 0; i < n; i++) printf("%d ", *(arr + i));

	printf("\n\n");

	free(arr);
	return 0;
}