| 문제


입력(표준 입력)

첫번째 줄에, 입력할 숫자들의 개수 N와 정수 k를 입력합니다.

다음줄 부터 N개의 줄만큼, 각각의 줄마다 하나의 입력 변수를 입력해야 합니다.


출력(표준 출력)

첫 번째 줄에는, 내림차순으로 가장 큰 k개의 원소들을 출력해 냅니다.

두 번째 줄에는, 가장 큰 k 원소들을 추출해낸 후에 힙을 출력해 냅니다.

 

ex) 

 

입력 

출력 

10 4

16

14

10

8

7

9

3

2

4

1

16 14 10 9

8 7 3 4 2 1 

 

 | 소스 코드 

#include <stdio.h>

void array_swap(int * arr, int a, int b)
{
	int temp;
	temp = arr[a];
	arr[a] = arr[b];
	arr[b] = temp;
}

void Heapify(int * arr, int parent_position, int heap_size)
{
	int left, right, largest;
	left=2*parent_position+1;
	right=2*parent_position+2;

	if((left<heap_size)&&(arr[left]>arr[parent_position]))
		largest = left;
	else
		largest = parent_position;

	if((right<heap_size)&&(arr[right]>arr[largest]))
		largest = right;

	if(largest != parent_position)
	{
		array_swap(arr, parent_position, largest);
		Heapify(arr, largest, heap_size);
	}
}

void Build_Heap(int * arr, int length)
{
	int parent_position;

	for(parent_position=length/2-1; parent_position>=0; parent_position--)
		Heapify(arr, parent_position, length);
}

void Heap_Sort(int * arr, int length, int k)
{
	Build_Heap(arr, length);
	int last_row;
	int count = 0;

	for(last_row = length-1; last_row>0; last_row--)
	{
		if(count == k)
			break;
		array_swap(arr, 0, last_row);
		length--;
		count ++;

		Heapify(arr, 0, length);
	}
}
int main(void)
{
	int arr[100000];
	int length = 10;
	int N, k, i;

	// input
	scanf("%d %d", &N, &k);
	for(i=0; i<N; i++)
	{
		scanf("%d", &arr[i]);
	}
	Heap_Sort(arr, N, k);

	// output
	for(i=N-1; i>(N-1)-k; i--)
		printf("%d ", arr[i]);

	printf("\n");
	for(i=0; i<=(N-1)-k; i++)
		printf("%d ", arr[i]);

	printf("\n");

	return 0;
}


| 문제


방향성 그래프 G에서 모든 간선들의 타입을 출력해내는 DFS(깊이 우선 탐색)을 구현하여라. DFS 과정을 진행하는 동안, 작은 숫자의 꼭짓점부터 방문하도록 설계하라. 


입력(표준 입력)

첫 번째 줄에, 꼭짓점의 개수 N을 입력한다.

두번 째 줄부터, 그래프 G의 인접 리스트가 x,y 두 정수로 표현이 된다. 이것은 x로 출발해서 y로 도착하는 간선이 있음을 의미한다.


출력(표준 출력)

G의 각각의 간선 타입들과 함께 간선들을 출력해내라. 간선들의 타입 표현은 다음과 같다.

1: 트리 엣지

2: 백 엣지

3: 포워드 엣지

4: 크로스 엣지

각각의 줄에는 세 개의 정수 x,y,z가 출력되어야 한다. 이것들은 x에서 출발하여 y로 도착하는 간선이 있으며 그 간선이 z의 타입을 가진다는 것을 의미한다.



입력 

출력 

6

1 2

1 4

2 5

3 5

3 6

4 2

5 4

6 6

1 2 1

1 4 3

2 5 1

3 5 4

3 6 1

4 2 2

5 4 1

6 6 2 


| 소스 코드

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

#define WHITE			0
#define GRAY			1
#define BLACK			2

#define TREE_EDGE		1
#define BACK_EDGE		2
#define FORWARD_EDGE	3
#define CROSS_EDGE	4

unsigned int time = 0;
unsigned int numV = 0;

typedef struct _vertex
{
	int color;
	int d;
	int f;
	int pred;
} Vertex;

// 구조체 멤버 중 자기 구조체를 타입으로 하는 포인터 next을 사용하는 이유는,
// 리스트 형태로 연결하고자 하기 위함.
typedef struct _edge
{
	int toV;
	int type;
	struct _edge * next;
} Edge;

// 각 Vertxt 마다 Adjacency list 구조체가 하나씩 배당되는 것으로 생각
Vertex SetV[1000];
Edge * adjlist[1000]={NULL,};

void addEdge(int fromV, int toV)
{
	Edge * newEdge;
	Edge * temp;
	temp = adjlist[fromV];

	newEdge = (Edge*)malloc(sizeof(Edge));
	newEdge->toV = toV;
	newEdge->next = NULL;

	if(adjlist[fromV] == NULL)
	{
		adjlist[fromV] = newEdge;
	}
	else
	{
		while(temp->next != NULL)
		{
			temp = temp->next;
		}
		temp->next = newEdge;
	}
}

void DFS_Visit(int fromV)
{
	Edge * edge_fromV;
	edge_fromV = adjlist[fromV];

	time ++;
	/* DISCOVERY */
	SetV[fromV].color = GRAY;
	SetV[fromV].d = time;

	while(edge_fromV != NULL)
	{
		int toV;
		toV = edge_fromV->toV;

		switch(SetV[toV].color)
		{
		case WHITE:
			edge_fromV->type = TREE_EDGE;		// 1
			SetV[toV].pred = fromV;
			DFS_Visit(toV);
			break;

		case GRAY:
			edge_fromV->type = BACK_EDGE;		// 2
			break;

		case BLACK:
			if(SetV[fromV].d < SetV[toV].d)
				edge_fromV->type = FORWARD_EDGE;// 3
			else if(SetV[fromV].d > SetV[toV].d)
				edge_fromV->type = CROSS_EDGE;	// 4
			break;

		default:
			break;
		}

		edge_fromV = edge_fromV->next;
	}

	/* FINISHED */
	SetV[fromV].color = BLACK;
	time ++;
	SetV[fromV].f = time;
}

void DFS(void)
{
	int i;
	for(i=1; i<=numV; i++)
	{
		SetV[i].color = WHITE;
		SetV[i].pred = NULL;
	}
	time = 0;
	for(i=1; i<=numV; i++)
	{
		if(SetV[i].color == WHITE)
		{
			DFS_Visit(i);
		}
	}
}

int main(void)
{
	int i=0;
	int fromV, toV;
	Edge * temp;

	scanf("%d", &numV);
	while(scanf("%d %d", &fromV, &toV) != EOF)
	{
		addEdge(fromV, toV);
	}

	DFS();

	for(i=1; i<=numV; i++)
	{
		temp = adjlist[i];
		while(temp != NULL)
		{
			printf("%d %d %d \n", i, temp->toV, temp->type);
			temp = temp->next;
		}
	}
	return 0;
}


이전에 동영상으로 소개를 드린 비밀번호 시스템 만들기 소스 코드를 공개합니다.

EEPROM을 사용하여 비밀번호를 저장하고,

비밀번호를 확인하는 간단한 MCU 시스템을 만들게 한 c 소스 코드입니다.


#include <avr/io.h> #include <avr/interrupt.h> #include <avr/eeprom.h> // for eeprom #include "EEPROM.h" int main(void) { unsigned char password[4]; unsigned char input[4]; unsigned char i=0; int check = 0; int step = 0; int count = 0; DDRB = 0xFF; DDRG = 0xFF; DDRF = 0x00; DDRE = 0xFF; PORTB = 0x00; PORTG = 0xFF; Init_Timer(); sei(); // 제일 처음에는 FND에 아무것도 표시되지 않는다. digit3=digit2=digit1=digit0=16; while(1) { if(KeyPressed) { KeyPressed = FALSE; switch(key) { case 0x0A: // * 키 // 비밀번호 입력 부분. step = 2; count = 0; digit3=digit2=digit1=digit0=3; // 3333 표시. break; case 0x0C: // # 키 // 비밀번호 설정 부분. step = 1; // 비밀번호 설정 시작! if(check == 0) { digit3=digit2=digit1=digit0=0; // 0000 표시 check = 1; // check가 1이어야 입력 받기 가능. } // 비밀번호 설정이 종료. else { digit3=digit2=digit1=digit0=2; // 2222 표시. /* Write password to eeprom */ write_eeprom(0, password[0]); write_eeprom(1, password[1]); write_eeprom(2, password[2]); write_eeprom(3, password[3]); check = 0; } break; default: // *,# 키 말고 일반 숫자키를 누르는 경우 if(step == 1 && check==1) // 비밀번호 설정시 입력 { password[3] = password[2]; password[2] = password[1]; password[1] = password[0]; password[0] = key; digit3 = digit2; digit2 = digit1; digit1 = digit0; digit0 = 1; } else if(step == 2) // 비밀번호 체크시 입력 { count ++; input[3] = input[2]; input[2] = input[1]; input[1] = input[0]; input[0] = key; if(count == 4) // 4번 눌렀을 때, 비밀번호 체크. { count = 0; // EEPROM에 저장된 비밀번호를 불러옴. for(i=0; i<4; i++) password[i] = read_eeprom(i); /* 이 strcmp는 제가 직접 만든 함수입니다. * 같으면 TRUE를 반환 합니다. */ if(strcmp(input, password)) // 문을 여는 데 성공. digit3=digit2=digit1=digit0=7; // 7777 표시. else // 문을 여는 데 실패. digit3=digit2=digit1=digit0=4; // 4444 표시. } } break; } // switch(key) end } // if(KeyPressed) end } // Infinite loop end. }


C++ Pointers and Memory – Free Coding Tutorials


포인터의 사용법의 구문과 의미는 C 표준 문서(http://bit.ly/173cDxJ)에 매우 자세히 설명되어 있다. 그러나 표준 문서가 포인터의 동작을 명확히 정의하지 못하는 경우가 있다. 이러 떄 표준 문서는 포인터의 동작을 다음과 같이 정의한다.


- 구현 방법에 따라 정의된 행동(Implementation-defined behavior)

 동작에 대한 문서화된 구현을 제공한다. 구현 방법에 따라 정의된 행동의 예로, 정수에 대한 오른쪽 시피트 연산에서 상위 비트의 확장 방법이 있다.


- 명시되지 않은 행동(Unspecified behavior)

 동작에 대한 구현을 제공하지만 문서화하지 않는다. malloc함수에 인자로 0을 주고, 실핼할 때 메모리가 얼마나 할당되는가 하는 것이 명시되지 않은 행동의 예가 될 수 있다. 명시되지 않은 행동의 목록을 CERT Secure Coding Appendix DD에서 볼 수 있다.


- 정의되지 않은 행동(Undefined behavior)

 포인터의 동작에 대해 어떠한 것도 강요하지 않으므로 어떠한 동작도 발생할 수 있다. 이 경우의 예로, free 함수에 의해 해제된 포인터의 값이 있다. (C 표준은 해제된 포인터에 어떤 값이 들어 있는지 정의하지 않는다.) 정의되지 않은 행동의 목록은 CERT Secure Coding Appendix CC(http://bit.ly/16msOVK)에서 찾을 수 있다.

What is a dangling pointer? - Stack Overflow

 

포인터가 여전히 해제된 메모리 영역을 가리키고 있다면, 이러한 포인터를 댕글링 포인터(Dangling Pointer)라고 한다. 댕글링 포인터가 가리키는 메모리는 더는 유효하지 않다. 댕글링 포인터는 premature free(조숙한 해제, 너무 급한 해제)라고 부르기도 한다.

 

 

 

댕글링 포인터의 사용은 아래 목록에 나열된 문제를 포함한 다양한 문제를 야기한다.

 

 

 

- 메모리 접근시 예측 불가능한 동작

 

- 메모리 접근 불가 시 Segmentation fault

 

- 잠재적인 보안 위험

 

 

 

이러한 유형의 문제는 다음과 같은 동작의 결과로 발생한다.

 

 

 

- 메모리 해제 후, 해제된 메모리에 접근

 

- 함수 호출에서 자동 변수를 가리키는 포인터의 반환

 

 

 

 

< 댕글링 포인터 예제 >

 

int * pi = (int * )malloc(sizeof(int)); *pi = 5;  printf("*pi: %d\n", *pi); free(pi);

 

 

free 함수로 메모리를 해제한 후에도 변수 pi는 여전히 메모리의 주소를 가리키고 있다. 그러나 이 메모리는 힙 관리자에 의해 재사용되거나 기존의 정수가 아닌 다른 타입으로도 사용될 수 있다. free 함수를 호출하면 원래 pi 포인터가 가리키고 있던 주소에 위치한 메모리는 해제되며 다시는 사용할 수 없다. 그러나 대부분의 런타임 시스템에서 해제 뒤에 발생하는 메모리의 접근이나 변경을 막지 않는다. 아래 코드에서 보듯이 여전히 해당 메모리에 접근하여 쓰기를 시도할 수 있으며, 이러한 시도의 결과는 예측할 수 없다.

 

 

free(pi); *pi = 10; 

 

 

하나 이상의 포인터가 같은 메모리 영역을 가리키고 그 중 하나가 해제된 경우에는 좀 더 복잡하다. 아래 코드처럼 변수 p1과 p2는 둘 다 같은 메모리 영역을 가리키고 있으며, 이러한 상황을 포인터 에일리어싱(Aliasing)이라고 한다. 그런데, p1이 해제되었다.

 

 

int * p1 = (int *)malloc(sizeof(int)); *pi = 5; ... int * p2; p2 = p1; ... free(p1); ... *p2 = 10;          // p2는 댕글링 포인터이다. 

 

 

또 다른 예제가 있다. 아래 코드와 같이 블록 구문을 사용할 때도 다른 미묘한 문제가 발생한다. 변수 pi에는 tmp의 주소가 할당되며, 변수 pi는 전역 변수이거나 로컬 변수이다. 그러나 변수 tmp는 블록 안에서 선언되고 블록 구문이 닫힐 때 스택에서 제거되며, tmp의 주소는 더는 유효하지 않다.

 

 

int *pi; ... {     int tmp = 5;     pi = &tmp; } // 이 위치에서 pi는 댕글링 포인터가 된다. foo(); 

 

 

 

 

대부분 컴파일러는 블록 구문을 스택 프레임으로 다룬다. 변수 tmp는 블록 안의 스택 프레임에 할당되며, 이어서 블록 구문이 종료되면서 스택 프레임이 제거된다. 블록의 스택 프레임으로 사용된 메모리 영역은 나중에 다른 방식으로 재사용(예제에는 foo함수가 호출되므로 foo함수에 의해 재사용)되며, 변수 pi는 여전히 그 위치를 가리키고 있게 된다.

 

 

 

 

< 댕글링 포인터 다루기 >

 

 

 

포인터가 원인인 문제들의 디버깅은 떄로 해결하기 어려울 때가 있다. 댕글링 포인터 문제를 처리하기 위한 몇 가지 접근 방법을 아래에 나열하였다.

 

 

 

- 메모리 해제 후 포인터를 NULL로 설정하여라.

 

NULL로 설정한 포인터를 그 이후에 사용하면 애플리케이션이 종료할 것이다. 그러나 해당 포인터에 대한 다수의 복사본이 존재할 경우 문제는 여전히 발생한다. 포인터에 NULL을 설정하는 일은 많은 포인터 복사본 중에 단 하나의 포인터에만 영향을 미치기 때문이다. 이와 비슷한 문제를 앞 절 "이중 해제"에서 언급한 적이 있다.

 

- free 함수를 대체할 새로운 함수를 작성하여라.

 

- 몇몇 런타임 시스템이나 디버깅 시스템은 해제된 메모리를 특별한 값으로 덮어쓴다.

 

(예를 들어, 0xDEADBEEF - Visual Studio는 해제된 메모리의 종류에 따라 0xCC, 0xCD, 0xDD 값을 사용하여 덮어쓴다). 예외가 발생하지 않은 상황이라도 프로그래머는 예상치 못한 곳에 이러한 값이 포함된 것을 보고 프로그램이 해제된 메모리에 접근한 것을 알 수 있다.

 

- 댕글링 포인터와 다른 문제들을 발견하기 위해 서드파티 도구들을 사용하라.

 

 

 

 

< 메모리 누수 탐지 기능 >

 

마이크로소프트는 동적으로 할당된 메모리를 덮어쓰는 문제와 메모리 누수 문제를 해결하기 위한 기술을 제공하며, 이 접근 방식은 프로그램의 디버그 버전에서 아래에 나열된 특별한 메모리 관리 기술을 사용한다.

 

 

 

- 힙의 무결성 검사

 

- 메모리 누수 검사

 

- 힙 메모리가 부족한 상황 재현

 

 

 

마이크로소프트는 메모리 할당을 관리하기 위한 특별한 데이터 구조체를 사용한다. 그리고 이 구조체의 사용으로 위와 같은 메모리 관리 기술을 제공한다. 이 구조체는 malloc 함수가 호출된 파일명과 줄 번호와 같은 디버그 정보를 관리한다. 게다가, 메모리를 덮어쓴느 문제를 찾기 위해 메모리 할당 전후로 버퍼가 할당된다. 이 기술에 대한 추가적인 정보는 Microsoft Developer Network(http://bit.ly/12SftWV)에서 찾을 수 있다.

 

 

 

Mudflap 라이브러리(http://bit.ly/YilPI1) 사용하면 GCC에 비슷한 기능을 사용할 수 있다. Mudflap의 런타임 라이브러리는 수많은 기능을 제공하며 그중에서도 특히 메모리 누수 탐지 기능이 제공된다. 이 메모리 누수 탐지 기능은 포인터 역참조 연산들을 계산하고 측정하는 방식으로 수행된다.

 

 

 

 

 

/// 이 글의 출처는 "Understanding and Using C Pointers(리처드 리스 지음)"입니다.

 

This post is some part from the book called "Understanding and Using C Pointers".

+ Recent posts