[C] 13. Storage Classes, Linkage and Memory Management

Taeil Nam·2022년 6월 20일
0

C

목록 보기
13/18
post-thumbnail

1. Memory 레이아웃

  • C 언어로 프로그램을 실행할 때, memory를 어떻게 사용하는지 나타낸 것.

스택(Stack)

  • 특정 함수 및 영역({})에 속한 변수처럼, 프로그램의 일부에서만 사용되는 변수가 저장되는 memory 공간.
  • 특정 함수 및 영역이 실행될 때만 memory를 사용.
  • 사용하지 않을 때는, 다른 곳에 사용할 수 있도록 운영체제에게 권한을 줌.
  • 필요할 때는 memory 크기가 늘어나고, 필요하지 않을 때는 memory 크기가 줄어듬.
  • memory 공간을 효율적으로 사용 가능.
  • main() 함수의 경우 프로그램 시작부터 끝까지 사용되는 함수이므로, main() 함수에 속하는 변수는 memory를 계속 사용하게 됨.


자유 공간(Free)

  • 스택 또는 힙에서 memory가 추가로 필요할 경우 사용되는 memory 공간.
  • 운영체제에서 가상 주소 공간을 사용하여, 스택과 힙의 memory 충돌이 나지 않도록 관리.

힙(Heap)

  • 크기를 알 수 없는 변수가 저장되는 memory 공간. (동적 할당)
  • memory 할당, 반납 과정이 운영체제를 통해 이루어져야 하기 때문에 속도가 느림.
  • memory를 할당 받고 사용하지 않을 수도 있으며, 사용 후 memory 반납을 하지 않을 수도 있음.
  • memory를 사용했으면 항상 memory 반납을 해줘야 함.


초기화 되지 않은 전역/정적 변수들(BSS Segment)

  • 초기화 되지 않은 전역/정적 변수들이 저장되는 memory 공간.
  • BSS = Block Started by Symbol.
  • 전역/정적 변수는 프로그램이 종료될 때까지 계속 쓰이므로, 프로그램이 종료될 때까지 memory에 남아 있음.


초기화 된 전역/정적 변수들(DATA Segment)

  • 초기화 된 전역/정적 변수들이 저장되는 memory 공간.
  • 전역/정적 변수는 프로그램이 종료될 때까지 계속 쓰이므로, 프로그램이 종료될 때까지 memory에 남아 있음.


프로그램 코드(TEXT Segment)

  • 프로그램 실행을 위해, 프로그램의 코드가 저장되는 memory 공간.
  • 코드의 내용은 변경되면 안되기 때문에, Read-Only 형태로 저장됨.
  • 코드에 사용된 문자열도 TEXT Segment에 저장됨.
  • TEXT Segment에 저장된 값을 변경하려고 시도하면, 운영체제가 런타임에러를 발생 시킴.
  • 프로그램이 종료될 때까지 memory에 남아 있음.

2. 객체와 식별자, L-value와 R-value

객체(Object)와 식별자(Identifier)

  1. 객체(Object)
    • 값을 저장할 수 있는 memory 공간(block of memory).
    • 객체지향 언어(C++)에서 말하는 object의 뜻은 비슷하나 다를 수 있음.
  2. 식별자(Identifier)
    • 변수 이름, 함수 이름, 매크로 등을 말함.

L-value, R-value

  1. L-value
    • object를 참조하는 표현식.
    • 대입식에서 왼쪽.
  2. R-value
    • 값, 변수, 상수, 표현식.
    • 대입식에서 오른쪽.

코드

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
    // 1. Object, Identifier.
    int var_name = 314;
    //변수의 memory 공간 = object, 변수 이름 var_name = identifier.

    int* pt = &var_name;
    //포인터의 memory 공간 = object, 변수 이름 pt = identifier. 

    *pt = 1;
    //*pt는 object를 나타내며, identifier가 아님.

    int arr[100];
    //배열 이름 arr = identifier.
    //arr = arr자체의 memory 주소가 아닌, arr[0]의 memory 주소를 가리키므로 object가 아님.

    arr[0];
    //arr[0] = object이며, identifier가 아니고 expression임.



    // 2. L-value, R-value.
    var_name = 314;
    //var_name = lvalue.

    int temp = var_name;
    //var_name = rvalue.

    pt = &var_name;
    //pt = lvalue, &var_name = rvalue.

    int* ptr = arr;
    //ptr = lvalue, arr = rvalue.

    *pt = 7;
    //*pt = lvalue, 7 = rvalue.

    int* ptr2 = arr + 2 * var_name;
    //ptr2 = lvalue, arr + 2 * var_name = rvalue.

    *(arr + 2 * var_name) = 456;
    //*(arr + 2 * var_name) = lvalue, 456 = rvalue.

    const char* str = "Constant string";
    //str = lvalue, "Constant string" = rvalue.

    str = "Second string";
    //str = lvalue, "Second string" = rvalue.

    return 0;
}

3. Variable Scope, Linkage

Variable Scope

  • 변수의 영역을 뜻함.
  • 특정 영역에서 선언된 변수는 다른 영역에서 사용할 수 없음. (Visibility)
  • C 언어에서는 4개의 Scope가 있음.
  1. block
    • {} 영역.
  2. function
    • 함수 영역.
  3. function prototype
    • 함수 프로토타입 영역.
  4. file
    • 소스 파일 영역.
    • 모든 함수에 포함되지 않는 영역.
    • file 영역의 변수는 모든 곳에서 사용 가능.

코드

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>


int g_i = 123;  //전역 변수(global variable), file scope.
int g_j;        //전역 변수(global variable), file scope.

void f1(int hello, double world);   // function prototype scope.

void func1()
{
    g_i++;      //전역 변수 g_i 사용.
}

void func2()
{
    g_i += 2;   //전역 변수 g_i 사용.
}

int main()
{
    int local = 314;    // function scope. (main())
    {
        local = 999;    // block scope.
    }
    func1();
    func2();

    printf("%d\n", g_i);    //전역 변수 g_i 사용.
    printf("%d\n", g_j);
    //초기화 되지 않은 전역 변수 g_j = BSS Segment memory에 저장되며, BSS Segment에 저장된 변수는 전부 0 으로 초기화 됨.
    printf("%d\n", local);

    return 0;
}

결과



Linkage

  • 다른 소스 파일의 file scope 변수를 사용할 수 있도록 연결해주는 과정.
  • extern을 사용하면 다른 소스 파일의 file scope 변수 사용 가능.(External Linkage)
  • static을 사용한 file scope 변수는 Linkage 불가능.(Internal Linkage)
  • 컴파일러는 소스 파일마다(translation unit) 컴파일 수행 후 .obj 파일 생성.
  • 링커가 .obj 파일을 연결.

코드(1번 파일)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int el;			//external linkage.
static int il;	//internal linkage.

void testLinkage();	//testLinkage() 함수 프로토타입.

int main()
{
    el = 1024;	// 전역 변수 el에 1024 저장.

    testLinkage();	//testLinkage() 함수 실행.

    printf("%d\n", el);	// el 값 출력.

    return 0;
}

코드(2번 파일)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

extern int el;	// 다른 소스 파일에서 선언된 변수 el을 가져옴. 

void testLinkage()	// testLinkage() 함수 정의.
{
	printf("DoSomething called\n");
	printf("%d\n", el); // el 값 출력.

	el++;	// el 값에 1 더함.
}

결과




4. 변수 저장 공간의 다섯 가지 분류

  • 일반적으로 저장 지속 기간(Duration)이 자동인지, 고정인지 두 가지로 분류함.
  • 세부적으로는 변수 유형에 따라 5개로 분류됨.

자동(Automatic)

  • 지속 기간(Duration)이 자동으로 결정 됨.
  • 지역 변수가 사용하는 저장 공간.
  • Stack memory 사용.
  • 사용되지 않으면 memory를 반납.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

void func(int k);	//함수 프로토타입에서 지역 변수 k 선언.

int main()
{
    auto int a; //auto : 지역 변수 임을 강조하는 키워드.
    a = 1024;

    int i = 1;  //지역 변수 i 초기화.
    int j = 2;  //지역 변수 j 초기화.
     
    printf("i %lld\n", (long long)&i);  //main() 지역변수 i의 메모리 주소 출력.

    {
        int i = 3;  //내부 영역(scope)에서 지역 변수 i 초기화.
        printf("i %lld\n", (long long)&i);  //내부 영역 지역변수 i의 메모리 주소 출력.

        printf("j = %d\n", j);  //j의 값 출력. (내부 영역에서 상위 영역의 변수 사용 가능)
    }


    return 0;
}

레지스터(Register)

  • 레지스터 = CPU 내부의 임시 저장 공간.
  • CPU의 내부에 있기 때문에, 속도가 가장 빠르다. (memory 보다 더)
  • 변수를 memory가 아닌 레지스터에 저장하고 싶을 때 사용.
  • 레지스터 변수로 선언된 경우, 컴파일러가 상황에 따라 레지스터에 저장할 수도 있고 자동 변수로 저장할 수도 있음.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

void temp(register int r);  //함수 프로토타입에서 레지스터 변수 r 선언.

int main()
{
    register int r; //레지스터 변수 r 선언.
    r = 123;

    return 0;
}

정적 변수의 내부 연결(Internal linkage)

  • 같은 파일에서만 사용 가능한 전역 변수가 사용하는 공간.
  • static을 사용하여 전역 변수를 선언하면, 같은 파일에서만 사용 가능.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

static int el;			//internal linkage가 가능한 전역 변수 el 선언.

void testLinkage();		//testLinkage() 함수 프로토타입.

int main()
{
    el = 1024;	// internal linkage 전역 변수 el에 1024 저장.
    // extern int g_int; -> extern 사용은 옵션임.

    testLinkage();	//testLinkage() 함수 실행.

    printf("%d\n", el);	// el 값 출력.

    return 0;
}

void testLinkage()	// testLinkage() 함수 정의.
{
    printf("DoSomething called\n");
    printf("%d\n", el); // el 값 출력.

    el++;	// el 값에 1 더함.
}

정적 변수의 외부 연결(External linkage)

  • file scope의 변수(전역 변수)가 사용하는 저장 공간.
  • 다른 외부 소스 파일간 변수 연결하여 공유할 수 있음.
  • 링커가 .exe(실행) 파일을 만들기 전, .obj 파일들을 연결하는 링크 과정을 통해 External linkage 수행.

코드(1번 파일)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int el;			//external linkage가 가능한 전역 변수 el 선언.

void testLinkage();	//testLinkage() 함수 프로토타입.

int main()
{
    el = 1024;	// 전역 변수 el에 1024 저장.

    testLinkage();	//testLinkage() 함수 실행.

    printf("%d\n", el);	// el 값 출력.

    return 0;
}

코드(2번 파일)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

extern int el;	// 다른 소스 파일에서 선언된 external linkage가 가능한 전역 변수 el을 가져옴. (extern)

void testLinkage()	// testLinkage() 함수 정의.
{
	printf("DoSomething called\n");
	printf("%d\n", el); // el 값 출력.

	el++;	// el 값에 1 더함.
}

블록 영역의 정적 변수

  • 블록({}) 내부에서만 사용되는 변수가 사용하는 저장 공간.
  • static을 사용하여 변수 선언.
  • stack memory가 아닌 변수를 계속 유지할 수 있는 memory 공간에 저장됨.
  • 사용되지 않아도 memory를 반납하지 않음.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

void count()
{
    int ct = 0; // count() 함수의 지역 변수 ct 선언.
    printf("count = %d %lld\n", ct, (long long)&ct);    //ct 값과 ct memory 주소 출력.
    ct++;   // ct에 1 더함.
}

void static_count()
{
    static int ct = 0;  //static_count() 함수의 static 지역 변수 ct 선언.
    printf("count = %d %lld\n", ct, (long long)&ct);    //ct 값과 ct memory 주소 출력.
    ct++;   // ct에 1 더함.
}

int main()
{
    count();    // count() 함수 호출.
    count();    // count() 함수 호출. 함수를 호출할 때마다 변수가 계속 초기화 됨.
    static_count(); // static_count() 호출.
    static_count(); // static_count() 호출. 함수를 호출할 때마다 기존 변수가 계속 사용됨.

    return 0;
}

5. Memory 동적 할당(Dynamic Storage Allocation)

  • 필요한 memory 크기를 미리 알 수 없을 때 사용.
  • 런타임 단계에서 필요한 memory 크기를 알 수 있기 때문에, 컴파일 단계가 아닌 런타임 단계에서 memory가 할당 됨.
  • Heap memory 사용.
  • 필요할 때 운영체제에게 memory 할당을 요청하고, 필요하지 않으면 memory 반납.
  • memory 동적 할당은 malloc() 함수를 사용하며, void 포인터 값(memory 주소 자체)을 반환.
  • memory 할당이 불가능 할 경우 NULL 값 반환.
  • malloc() 함수의 반환 값을 캐스팅하여 특정 자료형의 배열처럼 사용 가능.
  • 대부분 배열처럼 사용.
  • 다 사용한 memory는 free() 함수를 사용하여 무조건 반납을 해줘야 함.
  • 동적 할당에 사용된 포인터 변수의 값은, 다 사용한 뒤 NULL 값을 넣어줌.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h> 	// malloc(), free() 사용.

int main()
{
    int n = 5;			// 변수 n 초기화.
    double* ptr = NULL;	// 포인터 변수 ptr 초기화.

    ptr = (double*)malloc(n * sizeof(double));	// 포인터 변수 ptr에 동적 할당된 memory 주소 저장.

    if (ptr == NULL)	// ptr이 NULL일 경우 = memory 동적 할당이 안된 경우
    {
        puts("Memory allocation failed.");	// Memory 할당 실패 출력.
        exit(EXIT_FAILURE);	// 프로그램 강제 종료. (return 1)
    }
    else {	// memory 동적 할당이 된 경우.
        for (int i = 0; i < n; ++i)	// n 만큼 반복 실행.
        {
            *(ptr + i) = i;		// ptr의 첫번 째 memory 주소 값부터 차례대로 i 대입.
            printf("%f ", ptr[i]);	// ptr의 첫번 째 memory 주소 값부터 차례대로 출력.
        }
    }
    free(ptr);	// 사용된 동적 할당 memory 반납.
    ptr = NULL;	// 사용된 동적 할당 memory 용 포인터 변수 값 NULL 초기화.

    return 0;
}

6. Memory 누수(Leak)와 free()의 중요성

  • Memory 누수 = 동적 할당에 사용된 memory 반납을 하지 않는 것.
  • 더 이상 사용되지 않는 동적 할당 memory를 반납하지 않으면, 계속 Heap memory에 저장된 채로 남아 있게되어 불필요한 memory가 사용 됨.
  • 불필요한 memory가 계속 쌓이면 memory 부족으로 인해 프로그램 강제 종료.

Memory 누수 방지 (free() 사용)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h> // malloc(), free() 사용.

int main()
{
    for (int k = 0; k < 1000000; ++k)
    {
        int n = 100000000;
        int* ptr = (int*)malloc(n * sizeof(int));	// 100000000 * 4 bytes = 약 300 MB.

        if (!ptr) // = if (ptr == NULL)
        {
            printf("Malloc() failed\n");	// 동적 할당 실패 출력.
            exit(EXIT_FAILURE);				// 프로그램 강제 종료. (return 1)
        }

        for (int i = 0; i < n; ++i)		// n 만큼 반복 실행.
        {
            ptr[i] = i + 1;				// ptr 첫번 째 memory 주소부터 차례대로 i+1 저장.
        }
        free(ptr);		// 사용한 동적 할당 memory 반납. (300 MB)
        ptr = NULL;		// 사용한 동적 할당 memory 용 포인터 변수 NULL 초기화.
    }

    return 0;
}

Memory 누수 발생 (free() 미사용)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h> // malloc(), free() 사용.

int main()
{
    for (int k = 0; k < 1000000; ++k)	// for 문 실행마다 memory 300 MB 씩 누수.
    {
        int n = 100000000;
        int* ptr = (int*)malloc(n * sizeof(int));	// 100000000 * 4 bytes = 약 300 MB.

        if (!ptr) // = if (ptr == NULL)
        {
            printf("Malloc() failed\n");	// 동적 할당 실패 출력.
            exit(EXIT_FAILURE);				// 프로그램 강제 종료. (return 1)
        }

        for (int i = 0; i < n; ++i)		// n 만큼 반복 실행.
        {
            ptr[i] = i + 1;				// ptr 첫번 째 memory 주소부터 차례대로 i+1 저장.
        }
        //free(ptr);		// 사용한 동적 할당 memory 반납 안함 = memory 누수 발생.
        //ptr = NULL;
    }

    return 0;
}

💡 디버깅 하다가 3번 정도 memory 누수 되니까 프로그램 갑자기 종료 됨.

7. calloc(), realloc()

  1. calloc()
    • count 인자를 사용하여 memory 공간의 개수 지정 가능.
    • 동적 할당된 memory 값을 0 으로 자동으로 초기화.
  2. realloc()
    • 기존에 동적 할당 받은 memory 크기를 더 크거나 작은 값으로 수정할 때 사용.
    • memory 크기를 높일 경우, 기존 memory에 저장되어 있던 값들을 그대로 유지한 채로 높임.
    • 기존 memory는 자동으로 반납 됨.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h> // malloc(), calloc(), realloc(), free() 사용.

int main()
{
    int n = 10;

    int* ptr = NULL;

    ptr = (int*)calloc(n, sizeof(int));	// calloc() 함수로 4 bytes 크기의 memory 공간을 n개 만큼 할당 받음.
    if (!ptr)
        exit(1);
        
    for (int i = 0; i < n; ++i)
    {
        printf("%d ", ptr[i]);	// ptr[i] 값 출력.
        printf("\n");
        printf("%p ", &ptr[i]);	// ptr[i] 값의 memory 주소 출력.
        printf("\n");
    }
    printf("\n");

    for (int i = 0; i < n; ++i)
    {
        ptr[i] = i + 1;	// ptr[0] ~ ptr[9] 까지 1 ~ 9를 차례대로 대입.
    }

    n = 20;	// n에 20 대입.

    int* ptr2 = NULL;	// ptr2 초기화.
    ptr2 = (int*)realloc(ptr, n * sizeof(int));
    // realloc() 함수를 사용하여 ptr의 memory 크기를 수정하여 만들어진 memory의 첫번째 주소를 ptr2에 저장하고 ptr의 memory 반납.

    printf("%p %p\n", ptr, ptr2);	// ptr, ptr2 의 memory 주소 출력.
    printf("%d\n", ptr[0]);			// ptr[0]의 값 출력.

    if (!ptr2)		// realloc() 함수로 동적 할당을 못 받은 경우.
        exit(1);	// 프로그램 강제 종료.
    else
        ptr = NULL;	// realloc() 함수 정상 실행 후 기존의 ptr은 사용되지 않으므로 NULL 초기화.

    for (int i = 0; i < n; ++i)	// n 만큼 반복 실행.
        printf("%d ", ptr2[i]);	// ptr2[0] ~ ptr2[19] 의 값 차례대로 출력.
    printf("\n");

    free(ptr2);	// 더 이상 사용하지 않는 ptr2 의 memory 반납.

    return 0;
}

🚩 출처 및 참고자료 : 홍정모의 따라하며 배우는 C 언어 (따배씨)

0개의 댓글