배움 저장소

홍정모의 따라하며 배우는 C언어 12. Storage Classes, Linkage and Memory Management 본문

Programming Language/C

홍정모의 따라하며 배우는 C언어 12. Storage Classes, Linkage and Memory Management

시옷지읏 2021. 11. 27. 12:38

12.1 메모리 레이아웃 훑어보기

  Read Only Memory(Text Segment)

프로그램이 시작되면 해당 프로그램을 실행하기 위한 코드가 Read Only Memory에 저장된다. 직접 작성한 코드가 저장되는 곳이다. 이 Memory는 프로그램이 끝날 때까지 변경되지 않는다. C++에서 가면 프로그램도 코드도 동적으로 변화시킬 수 있다. 프로그램을 실행하며 필요할 때마다 코드를 가져와 실행시킨다. 

 

  Global Variable 전역변수

Data 영역은 BSS와 Data Segment로 나뉜다

    BSS는 초기화되지 않은 global/static 변수가 저장된다. 모두 0으로 초기화 된다.

    Data Segment에는 초기화된 globa/static 변수가 저장된다.

 

  g_i와 g_j는 어디에서나 접근이 가능하다. 이 같은 변수를 전역변수라 한다. Data Segment 혹은 BSS Segment에 저장되어 프로그램이 끝날 때까지 남아있다. 전역변수는 항상 메모리를 차지하고 있기 때문에 비효율적일 수 있다.

 

Stack

ㄴ 함수 호출

  코드를 실행시키다 함수를 만나면 해당 함수를 실행시킨 뒤에 다시 돌아와서 나머지 코드를 실행시켜야 한다.

  이때 stack frame은 돌아가야하는 위치를 기억하고 있다.

ㄴ Local Variable(지역변수)

  l_arr[MAX]는 func( ) 함수 시작되어 메모리를 할당받은 후 func( ) 함수가 끝날 때 메모리를 돌려준다. 이같이 필요할 때만 효율적으로 사용하는 변수를 지역변수라 한다.

  지역변수는 Stack에서 관리된다. Stack은 필요한 메모리만큼 저장공간을 늘릴 수 있다. 지역변수 l_arr[MAX]는 컴파일 시점에 저장공간이 정해져있다. OS의 도움을 받지 않고도 메모리를 활용할 수 있기 때문에 메모리 관리를 빠르게 할 수 있다.

 

  Heap(동적으로 관리하는 메모리)

  메모리를 얼마나 사용할지 알 수 없을 때 Heap을 사용한다. (Stack은 컴파일 때 메모리를 얼마나 사용할지 알고있을 때 사용한다) OS에게 메모리를 요청하여 응답받은 후 사용가능하기 때문에 Stack보다 메모리 관리 속도가 느리다.

 

 stack에 저장된 포인터 공간에 동적으로 할당받은 공간의 첫 주소를 저장하여 사용한다.

 

Stack VS Heap

  Stack은 해당 프로그램만 사용할 수 있는 메모리 공간이다. 크기가 작다.

  Heap은 OS가 사용할 수 있는 메모리 공간을 가져올 수 있기 때문에 크기가 크다.

12.2 객체와 식별자, lvalue와 rvalue

  Object와 Identifier

    Object(객체) 메모리 공간으로 값을 저장할 수 있다.

                     메모리 공간은 할당받아 사용하고 반환한다(사용할 수 있는 기간이 정해져있다)

                     C++와 같은 OOP에서 Object는 발전된 개념으로 사용된다.

    Identifier(식별자) 변수의 이름, 함수의 이름, Macro 등을 Identifier라고 지칭한다. 

                           Identifier는 Scope(영역)을 가지고 있다.

int var_name = 3;
int* ptr = &var_name;

 위 코드는 Identifier(var_name, ptr)을 통해서 값 3, 주소 &var_name을 각 Object(메모리 공간)에 저장하고 있다.

int arr[100];
arr[0] = 7;

 배열의 이름 arr은 Identifier이다. arr은 Object가 아니다. arr자체는 저장공간을 가지고 있지 않고 Ampersand(&) 연산자를 사용해도 배열의 첫 값을 가리킨다.

 arr[0]는 Object이다.  arr[0]는 Identifier가 아니다. [0]는 expression으로 +0만큼 주소를 이동하였다. 자기 주소를 가지고 값을 저장하기에 Object이다.

 

L-value 와 R-value

식 왼쪽에서 값을 할당받으면 L-value. L-value는 expression(표현식)이다. Object를 다른 방식으로 가리킨다.(Referring)

var_name = 3; // var_name is modifiable l-value

 변수 이름 var_name이 가리키고 있는(Referring) 저장공간에 값 3을 복사하여 붙여넣는다.

int temp = var_name; // var_name is r-value

var_name은 값을 임시 공간에 복사하여 temp에 붙여주고 있다. 1+2는 r-value이다. 저장공간을 따로 가지고 있지 않으므로 L-value가 될 수 없다.

temp = 1 + 2;

 

 아래 *ptr은 expression으로 Object를 가리키고 있다. *ptr은 ptr을 indirect해준 expression이므로 Identifier가 아니다. L-value로 사용하고 있는 expression이다.

*ptr = 1;
int *ptr = arr + 2 * var_name; // address rvalue
//         addresses + 6    = arr[6]
*(arr+2 * var_name) = 456;     // lvalue expression

const char* str = "Constant string"; // str is a modifiable lvalue.
str = "Second string"; 
str[0] = 'A';                          // Error!
// "Constant string" = "Second String" // Error! str literal isn't r-value

char char* str2[] = "String in an array";
str2[0] = 'A';                         // OK

 

12.3 변수의 영역(Scope)과 연결 상태(Linkage), 객체의 지속 기간(Duration)

  Scope 개념

int g_j;// global variable uninitialized is set to 0

void funcArray(int n, int arr[n]); // VLA must use identifier

int main()
{
	printf("%d \n", g_j); // global variable uninitialized is set to 0
	
	if (g_j == 0)
		goto jump;  // label could be used before it is marked
jump:
	return 0;
}

 

Linkage

  extern(external linkage)

    파일은 컴파일의 최소 단위다. 같은 말로 파일은 Translation unit(기계어를 번역하는 단위)이다.

    따라서 각 파일에 정의되어 있는 전역 변수는 다른 파일에서 알 수 없다.

    다른 파일의 전역변수(File Scope에서 생존)를 사용하려면 'extern' keyword와 해당 변수의 이름을 적어준다.

// Link.c
int extern_i;      // file scope with external linkage

// Second.c
extern int extern_i;

 이 때 두 파일에서 extern_i 변수는 object(저장 공간)을 공유하고 있다.

 

static(internal linkage)

  static을 사용하면 해당 변수의 scope를 파일 내로 제한한다. 다른 파일에서 'extern'을 사용해도 사용불가능하다.

// Link.c
static int intern_i; // file scope with internal linkage

// Second.c
extern int intern_i; 

...
    printf("%d \n", intern_i); // Link Error!

 

Strage duration(메모리의 지속기간)

- static storage duration       : (being allocated untill program is executed)

- automatic storage duration : (Local variable, Stack)

- allocated storage duration  : (dynamic allocation)

- thread storage duration     : ( relates to multi threading )

 

Static storage duration

  위에서 설명한 static keyword와 다르게 사용된다.

void static_func()
{
	static int static_count = 0;
	printf("%d \n", static_count++);
}

int main()
{
	static_func();
	static_func();
	return 0;
}

Output

0
1

  static_func( ) 함수 내에 count는 함수가 종료되어도 메모리 공간을 사용하고 있다. 이후 다시 initialzing이 될 거 같지만 기존의 메모리 공간을 그대로 사용함을 알 수 있다. 프로그램에 시작될 때 저장공간을 할당받아 프로그램이 끝날 때까지 반환하지 않는다.

 

12.4 저장 공간의 다섯 가지 분류

지역변수는 모두 자동으로 분류된다.

레지스터는 CPU내부 처리장치이다. 레지스터에 저장되면 빠르게 처리할 수 있다.

고정적 Static은 프로그램 시작부터 끝까지 메모리를 할당받아 사용한다.

할당메모리는 OS에 메모리 공간을 요청하면 Pointer를 받아 사용한다.

                  할당메모리는 저장 공간의 분류에 포함되지 않는다.

 

12.5 자동 변수 ( Automatic Variables )

 지역변수라고도 한다. 필요할 때마다 Stack의 데이터 공간을 늘려 사용하고 Scope를 벗어나면 반환한다. 모든 변수를 메모리에 저장하지 않기 때문에 결과적으로 메모리를 아낄 수 있다.

 Linkage 연결은 불가능하다. 항상 존재하는 메모리 공간이 아니다. 다른 파일에서 해당 주소에 접근하면 오류를 만들어낼 것이다.

 

 C++과 함께 사용할 때 헷갈린다. 사용하지 말자

auto int i;   // Compiler didn't initializing it automatically
auto char c;  // Global variable initailized 0 automatically
auto double d;

지역변수임을 명시하기위하여 keyword 'auto'를 사용할 수 있다. c++에서 사용하는 auto와 다른 의미로 쓴다.

 

name hiding

int i = 1;
printf("i=%i, %p \n", i, &i);    // >> i=1, 00EFFD2C
if (i == 1)
{
  int i = 2;
  printf("i=%i, %p \n", i, &i);  // >> i=2, 00EFFD20
}
printf("i=%i, %p \n", i, &i);    // >> i=1, 00EFFD2C

specifier가 동일할 때는 stack 가장 위에 있는 specifier, scope 내에 있는 specifier를 우선으로 불러온다.

 

stack frame

void func(int k)
{
	int i = k * 2;
}

int main()
{
	int i = 1;
	func(5);
	return 0;
}

 func( )함수가 호출되면 Stack에 Stack Frame을 쌓는다. Stack Frame은 데이터를 내부에 저장한다. Stack Frame이 Stack에서 빠져나오면 내부에 저장된 데이터도 모두 날라간다.

 결과적으로 main( ) 함수에서 할당받은 i의 object(저장공간)은 func( ) 함수 내부에서 사용할 수 없다. function call이 시작되면 Scope는 새로운 Stack Frame으로 한정된다.

 

 

12.6 레지스터 변수

 레지스터는 CPU가 가지고 있는 임시 저장 공간이다. 메모리는 CPU와 분리되어 있기 때문에 Bus를 통하여 데이터를 주고 받는다. 레지스터에 등록된 데이터를 가져올 때 보다 느리다. 

 프로그래머가 register keyword를 사용하여 컴파일러에게 register에 메모리를 할당하여 저장해달라고 요청할 수 있다. 컴파일러는 register에 메모리를 할당할 수도 있고 무시하고 Stack을 사용할 수도 있다.

void temp(register int r)
{
  // do sth
}

int main()
{
  register int r;
  int* ptr = &r; // Error!
}

- Register 주소값은 가져올 수 없다.

- 임베디드 프로그램이나 특수한 목적의 컴파일러를 사용할 때 register keyword를 사용한다.

 

12.7 블록 영역의 정적 변수

 life time은 블록 안이지만 메모리는 계속 할당되어 사용할 수 있다. 블록 밖에서 해당 메모리에 접근하여 값을 가져올 수는 있지만 그 경우에 함수 밖에서 전역변수 static을 사용함이 바람직하다. 블록 밖에서는 invisibe 상태이다.

 함수를 계속하여 사용하되 변수의 값을 새로 초기화하지 않고 그대로 사용하고 싶을 때 사용한다.

void static_count()
{
	static int count = 0;
	printf("static count = %d %p \n", count, &count); // Initialized only ONCE!!
	count++;
}
int main()
{
	static_count();
	static_count();
	static_count();
}

Output

static count = 0 009FC140
static count = 1 009FC140
static count = 2 009FC140

 

  Static variable with no linkage는 단 한번 초기화 된다. 위 코드의 정의와 선언을 다음과 같이하면 항상 0이 출력된다.

static int count;
count = 0;

 

참고)

 static int 변수와 일반 int 변수를 비교하기 위하여 내부에서 일반 int 변수를 사용하는 int count ( ) 함수를 만들어

 실험했다. 이 때 여러 번 함수를 호출하여도 내부에 있는 int count 의 주소가 동일하게 출력되는 경우가 있다. 

 이는 Stack에서 뺐다 넣어 동일한 위치에 있게 되었기 때문이다. 

 

Static variable with no linkage의 잘못된 사용

int func(static int i) // Warning ( Error in GCC )
{
}

  함수의 Parameter는 호출되는 시점에 메모리를 할당받아 초기화된다.

  이는 Stack에서 메모리를 할당받음을 의미한다. 정적 메모리를 사용함은 적절하지 않다. 전역변수를 사용하자.

 

 

Static variable with no linkage을 반환하기

int* count()
{
  int count = 0;
  return &count; // Bad! deallocated memory addresses
}

int* static_count()
{
  static int count = 0;
  return &count; // OK
}

  count( ) 함수에서 반환되는 주소는 사용가능할지 몰라도 이미 반환된 메모리이다. 예기치 않은 동작을 실행할 수 있다.

  stat_count( ) 함수에서 반환되는 주소는 ptr로 사용해도 된다. 이 경우는 전역변수를 사용함이 더 바람직하다. 

 

12.8 정적 변수의 외부 연결 external linkage

Static Variable with external linkage

 번역 단위란 Translation Unit으로 C file을 지칭한다. C file을 컴파일하여 obj를 만든다.  obj파일끼리 linkage 작업을 하여 이어준다. 이 때 external varaible, 함수를 연결한다.

 

사용방법

int global_i;
int global_arr[10];
size_t size = sizeof(int);

int x = 5;          // OK, Constant Expression
int y = x;          // Invalid

- global_i처럼 전역변수는 compiler가 자동으로 초기화해준다. 전역변수는 한 번 초기화 해주면 계속 사용할 수 있다.

  지역변수를 초기화 할 경우 overhead가 생기고 상황에 따라 지역변수의 초기화가 반복될 수 있다.

  이 때 낭비되는 overhead를 아끼기 위하여 초기화 하지 않는 선택지를 남겨두었다. 

- 전역변수는 변수가 들어간 표현으로 초기화할 수 없다. 항상 Constant를 사용해야 한다.

 

Hides global variable

int global_i;

int main()
{
	printf("%i %p \n", global_i, &global_i);   // >> 0 00CEC140
	int global_i = 5;  // hides global variable "global_i"
	printf("%i %p \n", global_i, &global_i);   // >> 5 0135F7D4
}

같은 이름을 내부에서 선언하여 사용하고 싶다면 extern keyword를 활용하자.

int global_i;            // defining declaration

int main()
{
	printf("%i %p \n", global_i, &global_i); // >> 0 0094C140
	extern int global_i; // referencing declaration
	printf("%i %p \n", global_i, &global_i); // >> 0 0094C140
}

extern keyword를 사용하여 블럭 외부 File Scope에 있는 전역함수의 메모리에 접근한다.

 

서로 다른 파일에서 Extern 활용하기

// main.c
int global_i;
void func_linking();

void func_main()
{
	printf("v:%i &%p in main \n", global_i, &global_i);
}

int main()
{
	func_linking();
	func_main();
}


// 1. linking.c
extern int global_i;
void func_linking()
{
	printf("v:%i &%p in linking \n", global_i, &global_i);

	global_i++;
}

// 2. linking.c
void func_linking()
{
	extern int global_i;
	printf("v:%i &%p in linking \n", global_i, &global_i);

	global_i++;
}

이 때 extern으로 설정된 identifier는 블록 scope일 때 초기화 할 수 없다. stack 저장공간을 사용하기 때문이다.

파일 scope일 때는 초기화 가능하다.

// main.c
int global_i;  // defining declaration

// OK
extern int global_i = -10; // OK ,referencing declaration
void func_linking()
{
	printf("v:%i &%p in linking \n", global_i, &global_i);

	global_i++;
}

// Error!
void func_linking()
{
	extern int global_i = -10 // Error!
	printf("v:%i &%p in linking \n", global_i, &global_i);

	global_i++;
}

주의

 전역변수는 한 번만 초기화할 수 있다. 만약 main.c에서 초기화를 해주었다면 다른 파일에서 초기화할 수 없다. 초기화

 는 어디에서든 할 수 있지만 extern keyword가 없는 곳(원본), 같은 말로 defining declaration에서 초기화를 해주자

12.9 정적 변수의 내부 연결 internal linkage

  static keyword를 사용하여 다른 파일에서 사용할 수 없게 만들 수 있다.

// main.c
static int i = 1;  // defining declaration

// second.c
int i = 2;         // defining declaration

// third.c
extern int i;      // referencing declaration

  main.c에서 정의된 i는 다른 파일에서 사용할 수 없다. third.c 파일에서 i는 second.c파일에 있는 i를 불러온다.

 

12.11 함수의 저장 공간 분류

  함수는 기본적으로 external, 외부 파일에서 읽을 수 있다. static으로 설정하기도 가능하다.

// main.c
extern void first();
extern void second();

int main()
{
	first();
	second();
}
void first()
{
	printf("function first in main.c \n");
}


// linking.c
static void utility() // Only validates on this file
{
  // ...
}
void second()
{
	utility();
	printf("function second in linking.c \n");
}

Output

function first in main.c
function second in linking.c

  위 예제에서 보듯, 함수의 prototype은 기본적으로 extern, referencing declaration이다. extern keyword는 대부분 생략

 하여 사용한다. 이 때 second( ) 함수에 static keyword를 붙이면 main.c에서 사용할 수 없기 때문에 Error가 생긴다.

  외부에서 사용할 일이 없는 utility 함수를 static으로 설정하여 사용해보자. 

 함수의 prototype에서 static을 넣었다면 함수의 body에서는 생략가능하다. 반대도 가능하고 둘 다 넣을 수도 있다.

 

12.12 난수 생성기 모듈 만들기 예제

  STD library

#include <stdio.h>
#include <time.h>   // -> time()
#include <stdlib.h> // -> srand()

int main()
{
    srand( (unsigned int)time(0) ); // random seed
    for (int i = 0; i < 10; ++i)
        printf("%d\n", rand());
}

  모듈 만들어보기

// main.c
#include "my_rand.h"
int main()
{
    my_srand( (unsigned int)time(0) );
    for (int i = 0; i < 10; ++i)
        printf("%d \n", my_rand());
}


// my_rand.h
int my_rand();
void my_srand(unsigned int s);


// my_rand.c
static unsigned int seed = 1;
int my_rand()
{
	seed = seed * 123456789 + 12345;
	seed = (unsigned int)(seed / 7) % 99999;

	return (int)seed;
}

void my_srand(unsigned int s)
{
	seed = s;
}

 

12.13 메모리 동적 할당

  컴파일 타임이 아닌 런타임에 저장공간이 결정되기 때문에 동적(Dynamic)이라는 말을 사용한다.

- 동적할당을 이용한 메모리는 Identifier가 없다.

- 운영체제에게 저장 공간을 요청하면 운영체제가 포인터를 반환하는 식으로 작동한다.

- 한 번 저장공간을 할당받으면 Scope와 관계없이 계속해서 저장공간을 사용한다.

printf("Before free %p \n", ptr); // Before free 00FC0E60
free(ptr);  // no action ouccurs when ptr is NULL
printf("After free %p \n", ptr);  // After free 00FC0E60
ptr = NULL; // Optional

 

- n값을 외부에서 불러오는 경우에도(constant를 사용하지 않아도) 문제없이 동적할당이 가능하다. (런타임에 결정됨)

- malloc( )함수는 size_t를 argument로 받고 void pointer(주소값)를 반환한다.

- malloc( )함수가 반환한 포인터를 캐스팅하여 배열처럼 사용한다.

- void pointer를 generic pointer라고도 부른다.

- void pointer는 크기가 0이기 때문에 pointer 연산이 불가능하다.

    int n = 10;
    int* ptr = NULL;
    ptr = (int*)malloc(n * sizeof(int));
    if (ptr == NULL)
    {
        puts("Memory allocation failed!");
        //exit(EXIT_FAILURE) is simillar to return 1 in main()
        //exit(EXIT_SUCCESS) is simillar to return 0 in main()
        exit(EXIT_FAILURE);
    }
    printf("Memory allocation success! \n");

    printf("Before free %p \n", ptr);   // Before free 00FC0E60
    free(ptr); // no action ouccurs when ptr is NULL
    printf("After free %p \n", ptr);    // After free 00FC0E60
    // ptr = NULL; // Optional

- pointer가 scope 끝을 만나 사라지면 Heap 메모리는 계속하여 사용하고 있지만 값을 불러올 수가 없다.

- free( )함수를 사용하여 Heap에서 사용하는 메모리를 반환해도 ptr 값은 변함없다. NULL값을 주면 안전하다.

 

동적할당 메모리 사용하기

    if (ptr != NULL)
    {
        for (int i = 0; i < n; ++i)
            printf("%i ", ptr[i]);
        printf("\n");

        for (int i = 0; i < n; ++i)
            *(ptr + i) = (int)i;
            
        for (int i = 0; i < n; ++i)
            printf("%i ", ptr[i]);
        printf("\n");
    }

Output

-572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307
0 1 2 3 4 5 6 7 8 9

동적메모리 할당 직후 메모리에 쓰레기 값이 들어있다.

 

VLA vs Dynamic Memory Allocation

  Dynamic Memory Allocation VLA
메모리 공간 가변성 메모리를 재 할당받아 사용한다 할당 받은 메모리 공간의 크기를 바꿀 수 없다
최대 저장공간 Heap의 한계까지 저장공간을 사용
(상대적으로 크다)
Stack의 한계까지 저장공간을 사용
(상대적으로 작은 공간)
Life Time Heap에 저장되어 Scope와 관계없이 사용한다 Stack에 저장되어 Scope 내에서 사용가능하다

    큰 저장공간이 필요하다면 64bit 환경에서 동적메모리를 할당받자. 32bit과 큰 차이가 있다.

 

12.14 메모리 누수와 free()의 중요성

Visual Studio Diagnostic Tools사용하기

  원활한 테스트를 위하여 x64환경에서 실행하자

    printf("Dummy Ouput\n");
    {
        int n = 100'000'000;
        int* ptr = (int*)malloc(n * sizeof(int));
        if (!ptr)
        {
            printf("Malloc( ) Failed\n");
            exit(EXIT_FAILURE);
        }
        for (int i = 0; i < n; ++i)
            ptr[i] = i + 1;
    }

    printf("Dummy Ouput\n");

 위 코드를 디버깅 모드에서 실행해보자.

프로그램에 진입하면 Heap Profiling - Take Snapshot을 클릭하여 Heap 사용량을 확인해보자.

 

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

위 코드에 진입하고나서 Snap shot을 다시 클릭하자.

 두 번째 Dummy Output이 출력되었을 때 Take SnapShot을 사용하자. ptr은 소멸하지만 Heap 메모리공간 변화는 없다.

 

 따라서 동적 메모리를 활용할 때는 free( ) 함수를 이용해주어 더 이상 사용하지 않은 Heap 저장공간을 반환하자.

덧붙여 ptr = NULL을 해줌이 좋다.

    printf("Dummy Ouput\n");
    {
        int n = 100'000'000;
        int* ptr = (int*)malloc(n * sizeof(int));
        if (!ptr)
        {
            printf("Malloc( ) Failed\n");
            exit(EXIT_FAILURE);
        }
        for (int i = 0; i < n; ++i)
            ptr[i] = i + 1;
        free(ptr);           // << Preventing Memory Leak
        ptr = NULL;          // << For Safety
    }
    printf("Dummy Ouput\n");

 

free( ) 함수는 해제해야 하는 저장공간이 얼마인지 알고 있을까?

malloc으로 메모리를 동적으로 할당받으면 malloc은 그 힙 메모리의 주소를 반환한다. 운영체제는 이 때 해당 주소에 할당한 힙 메모리의 크기를 내부적으로 저장하고 있다가 free( )함수를 사용할 때 해당 메모리를 반환하는데 쓴다.

 

12.15 동적 할당 메모리를 배열처럼 사용하기

단일변수, 1차원 배열, 2차원배열

/* One Variable */
    int* ptr = NULL;
    ptr = (int*)malloc(sizeof(int) * 1);
    if (!ptr) exit(EXIT_FAILURE);
    *ptr = -111;
    printf("%i \n", *ptr);
    free(ptr);
    ptr = NULL;


    /* 1D Array */
    int n = 3;
    ptr = (int*)malloc(sizeof(int) * n);
    if (!ptr) exit(EXIT_FAILURE);
    ptr[0] = 100;
    *(ptr + 1) = 200;
    *(ptr + 2) = 300;
    for (int i = 0; i < 3; ++i)
        printf("%d %p\n", *(ptr + i), ptr + i);
    printf("\n");
    free(ptr);
    ptr = NULL;

    /* 2D Array */ 
    int row = 3, col = 2;
      int(*ptr2d)[2] =   (int(*)[2]  )malloc(sizeof(int) * row * col); // pointer to Array has 2 elem
    //int(*ptr2d)[col] = (int(*)[col])malloc(sizeof(int) * row * col); // VLA is blocked on vs
    if (!ptr2d) exit(1);


    for (int r = 0; r < row; r++)
        for (int c = 0; c < col; c++)
            ptr2d[r][c] = c + col * r;

    for (int r = 0; r < row; r++)
    {
        for (int c = 0; c < col; c++)
            printf("%d ", ptr2d[r][c]);
        printf("\n");
    }

 

1차원배열로 2차원 배열 표현하기

arr[0][0] = 1 arr[0][1] = 2 arr[0][2] = 3
arr[1][0] = 4 arr[1][1] = 5 arr[1][2] = 6
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
arr[0][0]=1 arr[0][1]=2 arr[0][2]=3 arr[1][0]=4 arr[1][1]=5 arr[1][2]=6
    row = 3, col = 2;
    int* ptr_cv = (int*)malloc(row * col * sizeof(int));
    if (!ptr_cv) exit(1);

    for (int r = 0; r < row; r++)
    {
        for (int c = 0; c < col; c++)
        {
            ptr_cv[r * col + c] = c + col * r;
            // *(ptr_cv + r * col + c) = c + col * r;
        }
    }
    for (int r = 0; r < row; r++)
    {
        for (int c = 0; c < col; c++)
        {
            printf("%d ", ptr_cv[r * col + c]);
            //printf("%d ", *(ptr_cv + r * col + c));
        }
        printf("\n");
    }

 

1차원 배열로 3차원 배열 표현하기

    /* Using 1D Arrays as 3D Arrays*/ printf("Using 1D Array as 3D Array\n");
    int depth = 2;
    row = 3, col = 2;
    int* ptr3d = (int*)malloc( row * col * depth * sizeof(int));
    if (!ptr3d) exit(0);

    for (int d = 0; d < depth; d++)
        for (int r = 0; r < row; r++)
            for (int c = 0; c < col; c++)
            {
                ptr3d[d * row * col + r * col + c] = d * row * col + r * col + c;
                //*(ptr3d + d * row * col + r * col + c) = d * row * col + r * col + c;
            }

    for (int d = 0; d < depth; d++)
    {
        for (int r = 0; r < row; r++)
        {
            for (int c = 0; c < col; c++)
            {
                printf("%d ", ptr3d[d * row * col + r * col + c] );
                //printf("%d ", * (ptr3d + d * row * col + r * col + c) );
            }
            printf("\n");
        }
        printf("==========================\n");
    }

 

1차원으로 2차원, 3차원 배열을 만들 때 Index가 길어지면 보조 함수를 사용할 수 있다

int idx2(int c, int r)
{
    return c + col * r;
}
int idx3(int c, int r, int d)
{
    static const int cr = col * row; // Save your calculating time
    return c + col * r + cr * d;
}

- static 변수를 사용하면 초기화 된 이후로 연산되지 않으므로 리소스를 아낀다.

- C에서 static 변수 값에 항상 constant상수을 넣어주어야 한다. 따라서 위의 col, row는 define macro를 이용하자.

  C++은 static 변수 값에 다른 변수를 넣어줄 수 있다.

 

참고

  포인터 +1연산은 다음과 같다. 포인터가 가리키는 주소에서 해당 포인터의 기본 타입의 byte만큼 주소를 이동한다.

 

12.16 calloc(), realloc()

calloc( )함수

    int n = 10;
    int* ptr = NULL;

    ptr = (int*)calloc(n, sizeof(int)); // Contiguous allocation
    //ptr = (int*)malloc( n * sizeof(int) );
    if (!ptr) exit(1);

    for (int i = 0; i < n; ++i)
        printf("%d ", ptr[i]); // >> 0 0 0 0 0 0 0 0 0 0 
    printf("\n");

- calloc( ) 함수는 값을 초기화해준다. 효율적으로 프로그램을 사용하기 위해 초기화를 하지 않고 싶다면 malloc을 쓰자

 

realloc( )함수

    n = 15;
    int* ptr2 = NULL;
    ptr2 = (int*)realloc(ptr, n * sizeof(int));
    //ptr2 = (int*)realloc(ptr, n * sizeof(int));

    if (!ptr2)
        exit(1);
    else
        ptr = NULL;

    printf("ptr:%p ptr2:%p \n", ptr, ptr2);
    for (int i = 0; i < n; ++i)
        printf("%d ", *(ptr2+i));
    printf("\n");

    free(ptr2);
    ptr2 = NULL;

void* realloc(void* _Block, size_t _Size)

- 변경 후 할당받은 메모리가 더 크면 기존의 값을 복사하여준다. 나머지 값은 초기화되지 않는다.

- 변경 전 포인터가 NULL일 경우 malloc( )함수처럼 동작한다

- Size가 0이면 free( ) 함수처럼 동작한다

- 실패할 경우 NULL을 반환한다.

 

12.17 동적 할당 메모리와 저장 공간 분류

Global variable(int g)는 Data Segment에 저장되어 있다. 주소값을 보면 Text Segment에 있는 변수들 보다 약간 높은 위치에 자리잡고있다. Allocated memory 주소는 Heap의 위치처럼 중간즘에 위치하고 있다.

 

12.18 자료형 한정자들 const, volatile, restrict (Type Qualifier)

Type Qualified Types : const, volatile, restrict, _Atomic

 

Const

    /* idempotent : possible to add duplicates */
    const const const int n = 11;
    typedef const int zip;
    const zip q = 8;       // == const const zip 
                           // Usage of this needs ideopotent

    // const int i; // Error! Not initialized!
    // i = 111;     // const variable must be initialzied

    int i1 = 1, i2 = 2;
    const int* ptr_i1 = &i1;  // const int* == int const*
    ptr_i1 = &i2;    // Allowed!
    // *ptr_i1 = -1; // Error!

    int* const ptr_i2 = &i1;
    //ptr_i2 = &i2;  // Error!
    *ptr_i2 = -1;    // Allowed!

    int const* const ptr_i3 = &i1;
    //ptr_i3 = &i2;  // Error! 
    //*ptr_i3 = -1;  // Error!

 

Global Constant used in many file

// constants.h
static const double PI = 3.141592;
static const double GRAVITY = 9.8;

// main.c
#include "constants.h"

double area = PI * 2.0 * 2.0;

여러 파일에서 사용하는 전역변수는 main 파일에 넣기보다 별도 헤더파일을 만들어서 관리해주자. 매 번 extern keyword를 사용하기 번거롭다.

 

volatile(날라갈 수 있다)

 프로그램에서 의도한 바가 없어도 값이 바뀔 수 있다. 예를 들면 OS에서 시간 값을 해당 변수로 가져올 경우이다. 컴파일러에게 값이 의도하지 않게 바뀔 수 있으니 최적화 대상이 아니라고 알려준다.

volatile int v1 = 1;
volatile int* pv1 = &v1;

int i1 = v1;

// ...

int i2 = v1;

컴파일러가 v1값을 에 복사하여 i1에 넣어주었다. 그 이후 i2에도 v1값을 복사하여 넣어주어야 한다. 최적화가 가능하다면 v1값을 복사하지 않고 Cache에 저장된 값을 그대로 사용할 수 있다. volatile은 해당 작업을 막아준다. imbedding 프로그램에 쓰인다.

 

restrict

 포인터를 사용하면 같은 주소값을 다양한 방법으로 접근할 수 있다. 여러 방법으로 같은 주소값에 접근하여 값을 변경하면 비효율적이다. 해당 저장공간(object)에 접근하는 방법을 하나로 제한할 때 사용한다. 이 때 컴파일러에게 특정 동작을 유도하지는 않는다. 프로그래머가 해당 변수로 접근하는 포인터를 만들지 않겠다고 알리는 기능이다.

int arr[5] = { 0,1,2,3,4 };
int* ptr = arr;

arr[0] += 10;     // compiler treat these differently
ptr[0] += 100;    // 
// arr[0] += 110; // is much efficient!

/* use __restrict for optimization */
int* __restrict rest_arr = (int*)calloc(5 * sizeof(int));
if (!rest_arr) exit(1);
rest_arr[0] += 10;
rest_arr[0] += 100;  
// rest_arr[0] += 110; // no diff

 

12.19 멀티 쓰레딩

_Atomic

전역 변수를 서로 다른 Thread가 사용할 때 문제가 생길 수 있다. CPU 내부 Register에 값을 등록해두었다가 값을 사용하거나 계산하여 메모리에 저장한다. CPU 작업중 다른 Thread가 전역변수 값을 바꾸어버리면, 의도한대로 업데이트 되지 않은 값을 사용할 수 있다. 이를 방지하는 type qualifier는 _Atomic이다. 단점은 느리다

 c언어적으로 multithread를 지원하지 않는다. os에서 지원하는 library를 사용해야 한다.

 

GCC

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>  // sleep()
#include <pthread.h> // multithreading
#include <stdatomic.h>

_Atomic int acnt = 0; // atomic type qualifier (C11)

/* A normal C Function is executed as thread
   when name of it is specified in pthread_create() */
void *myThread(void *vargp)
{
    int n = 1; // thread storage duration
    for(int i=0; i<3; ++i)
    {
        sleep(1);
        acnt += n;
        printf("Printing from Thread %d %p \n", acnt, &n);
    }
    return NULL;
}

int main()
{
    pthread_t thread_id1, thread_id2;
    
    printf("Before Thread\n");
    
    pthread_create(&thread_id1, NULL, myThread, NULL);
    pthread_create(&thread_id2, NULL, myThread, NULL);
    
    // If you didn't wait the other, the program will be end during multithreading
    pthread_join(thread_id1, NULL); // waiting until the other 
    pthread_join(thread_id2, NULL); // thread's process ends
    
    printf("After Thread\n");
    printf("Atomic %d   \n", acnt);
    
    return 0;
}
/* 
Compile cmd
  $gcc <file-name.c> -o <output-file-name> -lpthread
Run cmd
  $./<output-file-name>
*/

- pthread_create( ) 함수가 실행되면 첫 argument에 해당 thread 주소를 삽입한다.

- 각 Thread 내에 있는 변수는 automatic duration이다.

 

Windows

  windows.h에서 _atomic type qualifier를 지원하지 않는다.

#include <windows.h>

//_Atomic int acnt = 0; // NA

DWORD WINAPI* myThread(void* vargp)
{
    int n = 1; // thread storage duration
    for (int i = 0; i < 3; ++i)
    {
        sleep(1000);
        //acnt += n; // NA
        printf("Printing from Thread \n");
    }
    return 0;
}

int main()
{
    HANDLE thread = CreateThread(NULL, 0, myThread, NULL, 0, NULL);
    // Executing multithread version of myThread( )

    if (thread)
        WaitForSingleObject(thread, INFINITE);
    // Waiting till end
    return 0;
}

multithreading 중 동시에 한 변수를 접근하지 못하게 lock할 수 있다. 여기서 다루지 않는다.

Comments