한 장소에 여러 다른 자료형들을 저장할 필요가 있을 때 사용하며 필요에 따라 서로 다른 멤버의 자료형으로 참조할 수 있다.
구조체에는 메모리를 효율적으로 활용하기 위해 비트 필드 기능이 제공된다. 프로그램을 작성하다 보면 데이터의 값이 0이나 1, 또는 2, 3 비트로 표현할 수 있는 데이터를 저장할 경우가 있다. 이때 char형이나 int형으로 선언하면 2, 3 비트만으로도 표현할 수 있는 데이터를, 불 필요하게 메모리를 많이 사용하게 된다. 이 때 메모리 접근을 원하는 비트단위로 접근 허용하는 것이 구조체 비트필드 이다.
비트 연산은 응용 프로그램에서 섬세한 처리를 하고자 할 때 사용되는 중요한 연산자 중의 하나이다. 비트를 잘 조작하면 일반적인 산술 연산보다 더 빠른 속도로 연산을 할 수가 있다. 특히 시스템 소프트웨어에서는 아직도 비트 연산이 매우 중요하게 사용되며 활용 범위도 넓다. 비트 연산자는 원하는 값을 추출하거나 액세스 권한 같은 플래그를 다룰 때 사용할 수 있다.
define은 매크로 상수 뿐만 아니라 함수와 유사한 매크로 함수(function-like macro)를 정의할 때도 사용된다. 매크로 함수의 인수는 선행처리기를 통해 확장될 때 매크로로 전달될 수 있다.
매크로 함수는 자료형에 독립적이다.
매크로 함수는 전처리에 의한 단순 치환 방식으로 구현되므로, 전달 인자의 자료형을 명시할 필요가 없고, 어떠한 자료형 변수를 인자로 전달해도 잘 동작한다.
실행 속도가 일반 함수보다 빠르다.
매크로 함수는 전처리기를 통해 단순히 치환되는 방식이므로 프로그램 실행 시, 함수의 호출 때문에 발생하는 성능 저하가 일어나지 않는다.(매크로 함수는 함수처럼 보일 뿐이지 연속적인 코드들이 호출부에 대체된다)
원하는 결과를 얻는 정확한 매크로 함수의 구현은 어려우며, 따라서 디버깅 또한 매우 어렵다.
매크로 함수의 크기가 증가하면 증가할수록 사용되는 괄호 또한 매우 많아져서 가독성이 떨어진다!
매크로 함수는 매크로 대치이다. 따라서 인자의 계산식은 의도치 않는 결과를 가져올 수 있다. 증감연산은 매크로 함수 인자로 사용 하지 않을 것을 권장한다. => 인자를 괄호로 묶어준다., 증감연산자는 여러번 실행될 수 있다.
증감 연산자는 반복해서 사용하지 않는것이 좋다
ex)
int main()
{
int x=2;
printf("(++x) * (++x) * (++x) : %d \n",
(++x) * (++x) * (++x) );
printf(" x: %d \n", x);
return 0;
}
결과가 컴파일러에 따라 달라진다.
매크로 함수와 자주 사용되는 선행처리기 - 연산자 #, ##
# 연산자는 매크로 함수의 대체 리스트 안의 인수 앞에 사용하여, 토큰을 문자열로 변환시킨다. 해당 토큰은 실인수로 치환되면서 양쪽에 위치한 큰따옴표("")를 포함해 그대로 문자열 상수로 변환된다.
# 연산자를 사용하면 문자열 안에 매크로 함수로 전달된 인수를 포함시킬 수 있다.
## 연산자는 두 개의 토큰을 하나의 토큰으로 결합해 주는 선행처리기 연산자다.
이 연산자를 사용하면 변수나 함수의 이름을 프로그램의 런타임에 정의할 수 있다.
일반 함수로 구현하게 되면 2개의 정수 포인터를 받아 포인터를 참조하여 그 값을 바꾸어 주어야 한다. 이를 XOR(^) 연산을 사용하여 3번의 비트 조작으로 두 정수 값을 교환하였다. XOR 연산은 반전(Toggle) 동작이다. 이 특성을 사용하여 두 값을 빠르게 교환할 수 있다.
ex)
#define swap(x,y) { x^=y; y^=x; x^=y; }
int main()
{
int x=5, y=6;
printf("before x:%d, y:%d \n", x, y);
swap(x,y);
printf("after x:%d, y:%d \n", x, y);
return 0;
}
비트 연산과 관련하여 실무에서 자주 사용되는 매크로가 있다. 특정 비트 값을 반환하거나, 특정 비트를 0이나 1로 ON/OFF 를 하는 경우이다. 매크로 함수에 인자를 받아 구현해보자.
ex1)
#define GET_BIT(x, y) (((unsigned int) (x) >> (y)) & 0x01)
char ch='A', tmp;
printf("ch 이진수 : " );
for(i=7;i>=0;i--)
printf("%d ", ch>>i & 0x01);
printf("\n");
printf("%d 비트 : %d \n", 4, GET_BIT(ch, 4-1) );
printf("%d 비트 : %d \n", 1, GET_BIT(ch, 1-1) );
ex2)
#define SET_BIT_ON(x,y) ((unsigned int)(x) | (0x01 << (y)))
#define SET_BIT_OFF(x,y) ((unsigned int)(x) & ~(0x01 << (y)))
printf("4번째 비트 ON : ");
tmp=SET_BIT_ON(ch, 4-1);
printf("7번째 비트 OFF : ");
tmp=SET_BIT_OFF(ch, 7-1);
for(i=7;i>=0;i--)
printf("%d ", tmp>>i & 0x01);
printf("\n");
scanf() 함수는 문자열 입력 시 공백을 포함할 수 없다. 또한 입력버퍼의 크기를 넘는 것을 제한할 수 없다. 이를 제어하기 위해 매크로 함수를 활용해 보자.
ex)
#define CHAR_READ 9
int main()
{
char buf[CHAR_READ + 1 ];
printf("\ninput str ? ");
//scanf(" %s", buf);
scanf("%[^\n]s", buf); //’\n 일 때 문자열을 구분한다.
printf("%s \n", buf);
}
프로세스는 서로 독립된 메모리 공간을 사용하기 때문에 프로세스 간 통신을 위해서는 메시지 큐, 세마포어, 공유 메모리 등 시스템 자원을 사용해야만 한다. 이에 반해 스레드는 하나의 프로세스 내에 구성되고 동일한 메모리 공간을 사용하므로 간단한 라이브러리 함수나 전역 변수를 이용하여 통신할 수 있다.
스레드란 여러 가지 일을 하는 하나의 프로세스 내에 존재하는 별개의 작업 단위를 말한다. 하나의 프로세스를 스레드라는 단위로 내부적으로 분할하여 각각 동시에 실행될 수 있도록 하는 것을 의미한다.
스레드는 서로 독립적이지만, 한 스레드가 취한 행동은 프로세스에 있는 다른 스레드에 영향을 미친다.
스레드는 프로세스의 일부분이기 때문에 다른 스레드와 데이터를 공유하기가 쉽다.
한 프로세스가 exit() 시스템 콜을 통해 종료한다면, 해당 프로세스의 모든 스레드가 종료 된다.
단일 CPU 시스템에서의 성능 향상 : 스레드를 사용하지 않는 프로세스가 느린 속도의 보조 기억 장치에 있는 데이터를 사용하기 위해 read(), write() 와 같은 시스템 콜을 사용하게 되면, 시스템 콜의 작업이 종료될 때까지 프로세스 전체가 멈추어 있어야 한다. 그러나 스레드를 사용하게 되면, 시스템 콜을 요청한 스레드만 멈추어 있고 프로세스 안에 있는 다른 스레드들은 각자의 작업을 계속할 수 있다. 이 경우에 Context Switch 가 가볍게 일어나므로 CPU의 부하가 현저하게 줄어든다.
다중 CPU 시스템에서의 성능 향상 : 프로세스 단위로 스케줄링이 이루어질 때는 아무리 많은 CPU가 있어도 프로세스는 하나의 CPU에 할당될 수밖에 없다. 그러나 스레드를 사용 하게 되면 각 스레드가 각각의 CPU에 할당되어 독립적으로 실행될 수 있다.
임계영역 (Critical Section) : 임계영역이란 서로 다른 프로세스(쓰래드)가 하나의 데이터 영역을 공유하고 있을 때 이 영역을 임계영역이라고 한다. 임계영역으로 인하여 발생할 수 있는 문제는 둘 이상의 프로세스(쓰래드)들이 공유자원을 동시에 읽거나 쓸 때 발생한다.
상호배제 (Mutual Exclusion) : 상호배제란 위에서 설명한 임계영역을 보호하기 위한 개념이다. 즉 임계영역으로 인하여 발생할 수 있는 문제를 사전에 방지한다. 이는 둘 이상의 프로세스(쓰래드)들이 공유자원에 대해서 동시에 읽거나 쓰기를 못하게 하는 것이며 이것이 상호배제의 기본 개념이다.
상호배제란 한 프로세스가 공유 메모리 혹은 공유자원을 사용하고 있을 때 다른 프로세스들은 이 메모리 혹은 자원을 사용하지 못하게 배제시키는 제어 기법을 말한다. 상호배제는 동기화를 통해서 해결할 수 있다.
공유된 자원의 데이터를 여러 쓰레드가 접근하는 것을 막는 것이다.
임계영역(Critical Section)을 가진 스레드들의 Running time이 서로 겹치지 않게 단독적으로 실행되게 하는 것으로 다중 프로세스들의 공유 리소스에 대한 접근을 조율하기 위해 Locking과 Unlocking을 사용한다. 한 쓰레드가 임계영역에 들어가기 위해선 lock을 하고 나올 땐, unlock를 해준다.
뮤텍스는 상호 배제를 함으로써 임계구역에 하나의 쓰레드만 들어갈 수 있다.
뮤텍스는 Lock을 획득한 프로세스만 그 락을 해제할 수 있다.
ex)
4 #include <stdlib.h>
5 //Mutax : 쓰래드가 공유하는 영역을 공유사용하지 못하도록 데이터 영역을
6 // 보호한다. 즉 임계구역에 하나의 쓰래드만 사용된다.
7
8 //뮤텍스 선언
9 pthread_mutex_t mutex;
10
11 int g_count = 0; // 쓰레드 공유자원
12 //보호하려는 영역을 한번에 하나의 쓰레드만 실행가능하도록
13 //공유데이터를 보호해야 한다.
14
15
16 void *t_function(void *data)
17 {
18 int i;
19 char* thread_name = (char*)data;
20
21 //lock 요청, 해지시 까지 block 된다
22 pthread_mutex_lock(&mutex); //unlock 될 때까지 직렬실행
23 // critical section
24 g_count = 0; // 쓰레드마다 0부터 시작.
25 for (i = 0; i < 3; i++)
26 {
27 printf("%s COUNT %d\n", thread_name, g_count);
28 g_count++; // 쓰레드 공유자원
29 sleep(1);
30 }
31
32 pthread_mutex_unlock(&mutex); //block 해지
33 sleep(1);
34 printf("%s \n", thread_name);
35 }
36
37 int main()
38 {
39 pthread_t p_thread1, p_thread2;
40
41 // 뮤텍스 객체 초기화, 기본 특성으로 초기화 했음
42 // 두 번째 인자가 NULL이면 기본 뮤텍스 속성이 사용된다.
43 // 기본 뮤텍스 속성 개체의 주소를 전달하는 것과 같다
44 pthread_mutex_init(&mutex, NULL);
45
46 pthread_create(&p_thread1, NULL, t_function, (void *)"Thread1");
47 pthread_create(&p_thread2, NULL, t_function, (void *)"Thread2");
48
49 pthread_join(p_thread1, NULL);
50 pthread_join(p_thread2, NULL);
51
52 return 0;
53 }
뮤텍스가 임계 영역에 들어가는 스레드가 하나라면, 세마포어는 복수가 가능하다.
세마포어는 하나의 쓰레드(binary semaphore)만 들어가거나, 혹은 여러 개의 쓰레드(counting semaphore)가 들어가게 할 수도 있다. wait과 signal을 통해 구현한다. 현재 수행중인 프로세스가 아닌 다른 프로세스가 세마포어를 해제 할 수 있다.
세마포어는 뮤텍스가 될 수 있지만, 뮤텍스는 세마포어가 될 수 없다.
ex)
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <stdlib.h>
5 #include <semaphore.h>
6
7 //세마포어 : 두 개의 원자적 함수로 조작되는 정수 변수로서,
8 //멀티프로그래밍 환경에서 공유 자원에 대한 접근을 제한하는 방법으로 사용된다
9
10 //병렬로 동작되는 둘 이상의 프로세스(쓰래드)에서 공유 자원을 동시에
11 //사용할 수 없기 때문에, 한 프로세스가 사용하고 있는 동안에 세마포어를
12 //세워서 다른 프로세스를 대기시키고 사용이 끝나면 해제시키는 방법으로 사용한> 다.
13
14 //세마포어 객체 선언
15 sem_t sem;
16 int g_count = 0; // 쓰레드 공유자원(상호배제,즉 임계영역)
17
18 void *t_function(void *data)
19 {
20 int i;
21 char* thread_name = (char*)data;
22
23 for (i = 0; i < 3; i++)
24 {
25 //sem 값 -1
26 sem_wait(&sem);
27 //critical section
28 g_count++; // 쓰레드 공유자원
29 printf("%s COUNT %d\n", thread_name, g_count);
30 sem_post(&sem);
31 //sem 값 +1
32 sleep(1);
33 }
34
35 }
36
37 int main()
38 {
39 pthread_t p_thread1, p_thread2;
40 int state1;
41
42 state1=sem_init(&sem, 0, 1);
43
44 if(state1 !=0)
45 {
46 printf("세마포어 초기화 실패 \n");
47 exit(1);
48 }
49
50 pthread_create(&p_thread1, NULL, t_function, (void *)"Thread1");
51 pthread_create(&p_thread2, NULL, t_function, (void *)"Thread2");
52
53 pthread_join(p_thread1, NULL);
54 pthread_join(p_thread2, NULL);
55
56 return 0;
57 }
프로그램이 실행되는 동안을 프로세스라 하며 프로세스는 메모리의 영역을 할당받아 실행되다가 프로그램이 종료되면 그 영역은 운영체제에게 제어권이 넘어가게 된다.
다음은 프로세스 실행시 메모리의 서로 다른 영역에 데이터를 저장함을 보여준다.
- 변수의 범위(Scope)와 생존시간(Life Time)
범위(Scope)란 변수를 접근할 수 있는 영역, 즉 변수를 사용 가능한 영역을 말한다. 생존시간(Life Time)은 변수가 메모리에 얼마나 남아있는지를 뜻한다.
변수의 범위와 생존시간은 선언위치, 변수 선언 시 형 수정자에 따라 달라진다. 변수의 종류는 지역변수, 전역변수, 정적변수가 있다.
알테니 스킵
C 언어는 변수가 어디에 저장될 것인가에 영향을 주는 다음과 같은 형수정자를 정의하고 있다.
Stack과 Data 세그먼트인 고정 메모리 할당이 아닌, 프로그램 실행 시 필요할 때마다 메모리를 할당 받는 동적 메모리 할당의 개념이 필요하게 되었고, 대부분의 경우 이러한 기억 장소는 컴퓨터에서 모든 기억 장소를 충분히 이용하려는 응용 프로그램에서 요구된다.
프로그램 실행 도중 메모리를 할당 받아야 하는 동적 메모리 할당은 힙(Heap) 세그먼트를 사용한다.
동적 할당은 연결 리스트와 큐, 스택, 이진 트리의 구현에서 일반적으로 사용된다.
동적 메모리 할당은 힙(Heap) 세그먼트를 사용한다.
동적 메모리 함수는 <stdlib.h> 헤더 파일을 참조한다. 일반적으로 사용되는 메모리 할당 관련 함수의 원형은 다음과 같다.
malloc()
malloc( ) 함수의 인수로 크기(byte 단위)를 넘겨주면 운영체제에서는 비어 있는 영역을 할당해 그 시작 주소를 반환해 줌. malloc( ) 함수가 메모리를 할당 받을 수 없는 상태라면 NULL 값을 반환함.
calloc()
num_elelments * element_size 만큼의 크기를 할당하면서, 할당된 공간을 자동으로 0으로 초기화함.
이런 초기화가 편리하기도 하지만, 메모리를 사용하기 전에 먼저 할당 받은 메모리에 항상 값을 저장하는 프로그램이라면 이런 과정이 오히려 시간 낭비가 될 수 있음.
realloc()
이미 할당되어 있는 영역을 다시 size만큼 재할당하여 새로운 영역의 시작 주소를 반환함. 기존데이터를 보장해준다.
free()
free( ) 함수의 인수는 malloc( ), calloc( ), realloc( ) 호출에서 반환 받은 값, 즉 메모리주소여야 함. 이 함수는 할당 받은 영역을 해제하고, 이렇게 해제된 기억 장소는 다음의 동적 할당 요청에 다시 할당해 사용될 수 있음. 할당된 공간의 일부만을 해제할 수 없음.
참조
gets - 행단위 입력받음
=> 이전 scanf()에서 값을 입력받을때
%*c 로 \n를 없애줘야 정상작동한다.
malloc() 함수를 이용하여 동적으로 메모리를 할당 받아 데이터를 관리할 때 이중포인터를 사용할 수 있다. 이는 동적으로 할당 받은 주소공간에 또 다른 동적 할당의 주소가 저장됨을 의미한다.
예시
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int main() 5 { 6 int x,y,i,j; 7 int **ptr; 8 9 printf("\n행,열입력? "); 10 scanf("%d%d",&x, &y); 11 12 ptr=(int **)malloc(sizeof(int*)*x); 13 14 for(i=0;i<x;i++) 15 ptr[i]=(int*)malloc(sizeof(int)*y); 16 17 for(i=0;i<x;i++) 18 { 19 for(j=0;j<y;j++) 20 { 21 printf("%d행, %d열 값? ", i, j); 22 scanf("%d", &ptr[i][j]); 23 } 24 } 25 26 printf("\n입력된 행열의 값 \n"); 27 for(i=0;i<x;i++) 28 { 29 for(j=0;j<y;j++) 30 printf("%3d ,", *(*(ptr+i)+j)); 31 printf("\n"); 32 } 33 34 for(i=0;i<x;i++) 35 free(ptr[i]); 36 free(ptr); 37 38 ptr=NULL; 39 40 return 0; 41 }
메모리 누수(memory leak) 현상은 컴퓨터 프로그램이 필요하지 않은 메모리를 계속 점유하고 있는 현상으로. 할당된 메모리를 사용한 다음 반환하지 않는 것이다.
메모리 누수는 프로그램이 메모리를 할당 후, 해제하지 않음으로 시스템의 메모리를 고갈시키는 소프트웨어 오류로, 당장 프로그램이 비정상적으로 종료되지 않으나 메모리 누수가 누적되면 결국 메모리 부족으로 인한 프로그램의 비정상적인 종료를 유발한다.
동적메모리 할당에서 빈번히 일어날 수 있으며 개발자는 메모리 누수를 탐색하기 위해 여러 툴을 이용할 수 있다.
강의에서는 vld(visual leak detector) 사용법을 알려줬는데 다른 방법도 많다.
여기에 잘 나와 있다.
저번에 메모리 leak 검출한 적이 있는데, crtdbg를 이용했다.(따로 파일 추가해줄 필요가 없어 편했다.)
crtdbg예시 (내가 썼던것)
#include <crtdbg.h>
#ifdef _DEBUG
#define new new(_CLIENT_BLOCK, __FILE__, __LINE__,)
#define malloc(s) _malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__)
#endif
//메인문 안에
//_CrtSetBreakAlloc(n);
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
데이터 구조에 대한 개념을 살펴보자. C 언어의 자료형을 분류하면 다음과 같다.
위와 같은 C 언어의 자료형만으로는 대량의 데이터나 복잡한 형태의 데이터를 다루기가 어려우므로 좀더 구조적인 자료형을 필요로 할 때가 많다. 자료형을 체계적으로 조직화하기 위한 추상적인 자료형을 데이터 구조(Data Structure) 라고 한다.
대표적인 데이터 구조로는 다음과 같은 것들이 있다.
(대부분 자율주행스쿨 velog에 포스팅 되어있다.)
구조체의 멤버 중 자신의 구조체 자료형을 가리키는 포인터 멤버를 가질 수 있으며 이를 자기 참조 구조체라고 한다. 즉, 자기 참조 구조체란 자신의 구조체 자료형을 포인트 멤버로 포함하는 구조체를 의미하며, 이때 포인터 멤버를 이용하여 다음 구조체 노드(영역)를 참조하게 된다.
구조체를 선언할 때 다음과 같은 점을 유의해야 한다. 선언의 목적은 SELF_REF를 이 구조체의 타입 이름으로 선언하는 것이다. 그러나 SELF_REF이라는 타입 이름은 선언의 가장 뒤에 오므로 이 구조체 내부에서의 선언은 유효하지 않다. 따라서 이 구조체 선언문은 다음과 같이 선언되어야 한다.
위와 같이 선언된 자기참조 구조체는 포인터 멤버 next를 이용하여 자신과 같은 구조체의 시작주소를 저장하는 용도로 사용된다. 다음과 같은 리스트로 구성할 수 있다.