본문 바로가기

프로그래밍/C, C++

[Tips 18기] C언어 일곱번째 강좌

1. void 포인터


우리가 C 언어에서 포인터를 사용할 때는 보통 이런 형식을 따라 선언한다

int *p = &data;

다음의 코드는 "int 형으로 선언한 포인터 변수에 data의 주소를 저장해라" 라는 의미를 갖는다. 

그렇다면 void 형으로 포인터를 선언하면 어떻게 될까?


일단 코드는 다음과 같을 것이다

void *p = &data;

보통 포인터는 자기가 가르키는 변수의 자료형에 맞춘 포인터 타입을 사용한다. 


하지만 void 포인터는 특수하다.

void 포인터는  타입이 지정되어 있지  void 포인터는 모든 자료형의 주소를 대입할 수 있다. 


모든 변수의 주소를 대입할수 있다는 점에서 void 포인터는 만능 포인터라고 불리기도 한다.


1.2 . void 포인터의 사용법


앞서 말했듯이 void 포인터는 타입이 지정되지 않은 포인터다. 이 말은 곧 바로 역참조를 할 수 없다는 뜻이다.

따라서 void 포인터는 사용할 때마다 매번 타입 캐스팅을 사용해야 한다.  

int data = 42;

void *p = &data;

*p = 43; //오류 발생, 타입 캐스팅 필요

*(int *)p = 43; //오류 발생 X, int * 형으로 캐스팅 되어서 정상적으로 역참조 가능

기본적인 void 포인터는 다음과 같이 사용될 수 있다.


1.3 . void 포인터의 응용

대표적인 응용 방법은 함수의 인자로 void 포인터를 사용하는 것이다.

 scanf  함수를 떠올려 보자, 보통 다음과 같이 사용할 것이다

scanf("%d", &data)

void 포인터를 이해 했다면 이러한 함수 원형의 뜻이 void 포인터를 두번째 인자로 입력받아 서식 문자에 따라 캐스팅을 한다는 것이라는 것을 알 수 있을 것이다. 


간추리자면, void 포인터를 인자로 사용하면 만능 포인터답게 다양한 타입의 포인터를 사용할 수 있다는 것이다.


2. 표준 입력 함수

표준 입력이란 운영체제에서 제공하는 가장 기본적인 입력이다. 표준 입력은 표준 입력 버퍼라는 것을 사용하는데 표준 입력 버퍼는 입력과 CPU 사이의 속도 차이를 해결하기 위해 만들어졌으며 특정키를 누를때까지 사용자가 입력한 것을 임시로 저장하는 버퍼이다. 그리고 표준 입력 함수는 이 표준 입력 버퍼에서 데이터를 읽어 오는 함수이다. 

대표적인 표준 입력 함수는 getc, gets, scanf 3가지가 있다.

1. getc(stdin)

getc는 다음과 같이 사용합니다.

char txt;


txt = getc(stdin)

stdin에서 한 글자를 읽어온다는 뜻입니다. (사실 getc는 일반 파일 포인터에도 사용 가능합니다)

여기서 유의할 점이 있습니다. 만약 사용자가 여러 글자를 치고 엔터 키를 누른다면 읽어 온 한 글자 이외에는 표준 입력 버퍼에 남아 다음에 표준 입력 함수를 사용하면 버퍼에 남은 데이터를 읽어오게 됩니다. 이러한 현상을 막으려면 버퍼를 비워주는 rewind 함수를 사용하면 됩니다

char txt;

char txt2;


txt = getc(stdin)

rewind(stdin) // 표준 입력버퍼 비움

txt2 = getc(stdin) // 정상적으로 새 문자를 받기 위해 대기

2. gets(arr)

gets 함수는 stdin에서 한 줄을(개행 문자가 입력되기 전까지 읽어온다) 읽어 반환하는 함수이다. 

char txt[10];


gets(txt)

gets는 버퍼 오버플로우 문제가 발생할 소지가 있어 최신 C/C++ 컴파일러에서는 제외되었다.

3. scanf("%~", &~)

scanf는 가장 많이 사용하는 표준 입력 함수입니다. 

int data;


scanf("%d", &data)

scanf 함수는 서식 문자를 지정해 다양한 타입의 데이터를 받아올 수 있다. 
이를 구현하기 위해 scanf 함수는 void 포인터를 이용해 저장할 변수의 주솟값을 받아오며 서식 문자로 캐스팅을 내부적으로 처리한다. 여기서 주의할 점이 있는데 scnaf 함수는 입력의 끝은 엔터 뿐만 아니라 공백으로도 처리된다. 따라서 공백이 있는 문자열을 처리하기 위해서는 gets 함수를 구현하여 사용하게 된다.

마지막으로 표준 입력에 대해 더 알아보자면 C 언어에서 표준 입력 스트림은 stdin이다. 그런데 이 stdin은 파일 포인터기 때문에 파일을 읽는것처럼 사용할 수 있다.

3. 포인터와 배열

포인터는 메모리 주소를 담고 있는 변수이다, 또한 배열은 연속된 인덱스와 그 인덱스에 매칭되는 데이터들의 모음이다. 

배열과 포인터는 사실상 같은 것이다. 잘 생각해 보자. 배열의 이름은 배열의 시작 주소와 같다. 여기서 짐작할 수 있듯이 배열은 데이터를 관리하는 포인터를 쉽게 사용하기 위한 언어의 트릭이다. 컴파일러를 거치게 되면 포인터와 배열의 기계어 문법은 같다고 보면 될 정도이다. 

배열은 포인터를 쉽게 사용하는 문법이라고 했는데 왜일까? 다음의 코드를 살펴 보자.

char txt[10];


printf("%c", *txt);


char *ptr = &txt[0];

char *ptr2 = txt; // 위의 코드와 같은 기능 수행

char *ptr3 = txt + 1; txt[1]의 메모리 주소와 같다

배열에 부분적으로 포인터 문법을 사용할 수 있는 것을 알 수 있다. 

포인터와 배열을 섞어서 사용하는 방법이 대표적으로 배열의 한 요소를 포인터에 저장해 두고 사용하는 것이다. 여기서 주의할 점이 있다.

char *ptr = &txt[0];

이 코드는 아주 나쁜 코드다. 
잘 생각해보자. 배열 표기를 포인터 문법으로 풀어 적으면 다음과 같다.

char *ptr = &*txt

역참조 연산자와 참조 연산자는 서로 상쇄되는데 굳이 이렇게 쓸 필요 없이 배열의 이름으로 포인터를 사용할 수 있는 것이다. 

또한 배열의 인덱스를 지정해서 저장할 때도 다음과 같이 사용하면 메모리 주소를 데이터 타입에 맞춰 증가시킨 값을 포인터에 담을 수 있다.

char *ptr = txt + 2; // &txt[2]와 같다


다음의 설명을 모두 이해 했다면 포인터와 배열의 상관관계를 잘 알수 있을 것이다. 

그렇다면 왜 배열 문법이 있는 것일까? 그것은 바로 프로그래머의 실수를 적게 하기 위해서다. 포인터는 햇갈릴 수 있는 여지가 있기 때문에 포인터 연산을 내부적으로 처리하는 배열 문법이 사용되는 것이다. 대신에 배열 문법을 사용하면 표현의 범위에 제약이 걸리기 때문에 그럴 때는 포인터 문법을 사용하게 된다. 

결국 배열 문법이 포인터를 wrap 하는 문법이라는 것을 염두에 두고 사용하여 더 좋은 코드를 작성할 수 있는 것이다.

+1월 29일 강좌 들으러 갈 때는 이제 메모리 구조 심화에 대해 공부할 것 같습니다 파이팅...
++ highlight.JS 적용해야 하는데 귀찮아서 코드가 회색..
+++C 언어는 매번 새로운것같다. ㅇㅁㅇ