void 형

CHOI·2021년 7월 17일
1

C 언어

목록 보기
25/28
post-thumbnail

리턴값이 없는 함수

때때로 우리가 만드는 함수는 리턴값이 없을 수 있다. 예를 들어서 int 변수에 1 을 더하는 함수를 생각해보자.

#include <stdio.h>

void add_one(int* p) {
  (*p) += 1;
}

int main() {
  int a = 1;
  printf("Before : %d \n", a);
  add_one(&a);
  printf("After : %d \n", a);
}

실행 결과

Before : 1
After : 2

add_one 함수는 인자로 전달 받은 포인터가 가리키는 값에 +1 을 수행하고 종료된다. 그리고 return 을 수행하는 문장이 없다. 왜냐하면

void add_one(int* p) {

위와 같이 void 로 선언되기 때문이다. void 의 사전적인 의미는 '진공','공허' 라는 뜻이다. 이를 C 언어 적용하자면 무(無)의 타입을 의미한다고 생각하면 된다. 만약 이 함수가 무언가 리턴하면 그 무언가에 맞는 타입이 있어야 하기 때문에 이 void 함수가 가능한 형태는 아무것도 리턴하지 않는 함수 가 된다.

따라서 다음과 같은 문장은 모두 틀린 셈이다.

void a();
int main() {
  int i;
  i = a();

  return 0;
}

void a() {}

함수 a 가 리턴하는 값이 없으므로 i 에 어떠한 값을 대입할 수 없다.

void 형 함수는 많은 곳에서 사용 된다. 주로, 리턴할 필요가 없는 함수 들의 경우가 대부분이다. 예를 들어서 두 변수의 값을 교환하는 함수를 보자.

int swap(int *a, int *b) {
  int temp;

  temp = *a;
  *a = *b;
  *b = temp;

  return 0;
}

우리는 여태까지 위와 같은 형태로 함수를 만들었다. 그러나 이 함수는 리턴할 필요가 없다. 왜냐하면 단순히 두 값을 바꾸면 끝인데 뭐하러 귀찮게 리턴을 하냐 이말이다. 불필요하게 swap 의 리턴 값이 있다면 오히려 swap 함수를 사용하는 사람의 입장에서 "이 함수의 리턴값은 무엇을 의미하는 것이지?" 를 생각해야 한다.

void swap(int *a, int *b) {
  int temp;

  temp = *a;
  *a = *b;
  *b = temp;
}

따라서 이와 같이 리턴할 필요가 없는 함수로 바꿔서 이러한 걱정을 날려버릴 수 있다.

void 형 변수


#include <stdio.h>
int main() {
  void a;

  a = 3;

  return 0;
}

컴파일 오류

error C2182: 'a' : 'void' 형식을 잘못 사용했습니다.

우리는 방금 전에 void 형 함수를 살펴보았다. 그러면 void 형 변수도 있을 것 같아서 위와 같이 했는데 오류가 발생하였다. 사실 이는 오류가 맞다. 그 이유는 컴파일러가

int a;

라는 문장을 본다면 컴파일러는 "아! int 형 변수를 선언하는 거구나! 메모리상에 4바이트 공간을 마련해야지" 라고 생각할 것이다. 그런데

void a;

이를 보게 되면 "이 변수는 무슨 타입이지?" 라고 생각하게 된다. 다시 말해서 메모리상에 얼마 만큼의 공간을 마련해야 하는지 알 수 없게 된다.(참고로 컴파일 때 모든 변수의 메모리상의 위치를 결정해야 한다.) 따라서 이와 같은 형식은 틀리게 된다.

그렇다면 다음은 괜찮을까?

int main() {
  void* a;

  return 0;
}

컴파일 해보면 문제가 없다는 것을 알 수 있다. 왜일까? 생각해보면 답은 쉽게 나온다. 앞서 우리가 void a; 를 하였을 때 문제는 메모리 상에 얼마만큼의 공간을 차지할지 결정할 수 없었기 때문인데 위의 예시는 void *a; 이다. 즉, '포인터'이기 때문에 메모리상에 8바이트(64비트 컴퓨터의 경우) 공간을 차지해야 한다. 즉, a 에는 어떠한 지점의 메모리의 주소값이 들어가게 된다.

그렇다면 void* a 포인터는 void 형의 변수의 메모리 주소 값을 가질까? 물론, 논리를 따지고 보면 맞지만 사실 void 형 변수라는 것이 존재 할 수 없기 때문에 void 형 포인터의 존재는 쓸모가 없어 보인다. 그러나 사실 void 는 타입이 없기 때문에 어떠한 변수의 포인터의 값도 넣을 수 있게 된다.

void *a;
double b = 123.3;

a = &b;

이와 같이 말이다. 다시 말해서 a 는 순전히 "주소값의 보관"역할만 하게 된다.

#include <stdio.h>
int main() {
  void *a;
  double b = 123.3;

  a = &b;

  printf("%lf", *a);
  return 0;
}

컴파일 오류

error C2100: 간접 참조가 잘못되었습니다.

이와 같이 주소값에 해당하는 값을 출력하려고 하면 오류가 발생한다. 그 이유에 대해서 예전에 포인터를 배울때 말한 적 있다. 컴파일러가 *a 를 해석할 때, a 가 가리키는 타입을 보고 메모리상에서 a 부터 얼마만큼 읽어야 할지 모르기 때문이다. 따라서 다음과 같이 수정해야 한다.

#include <stdio.h>
int main() {
  void *a;
  double b = 123.3;

  a = &b;

  printf("%lf", *(double *)a);
  return 0;
}

실행 결과

123.300000

우리는

printf("%lf", *(double *)a);

위 문장에서 형 변환을 이용하였다. 즉, 단순히 주소값만 가지고 있던 a 에게 (double *) 를 취함으로 써 이 포인터가 double 형 변수를 가리키고 있는 포인터라는 것을 알려주는 것이다. 따라서 (double *)a 부분을 통해서 컴파일러는 현재 a 가 가리키고 있는 곳의 주소값을 double 로 생각하여 8 바이트를 읽게 된다.

void 는 단순히 어떠한 타입의 포인터의 주소값을 편리하게 담을 수 있어서 다양하게 활용된다. 예를 들어서 다음과 같은 역할을 하는 함수를 생각해보자

어떠한 주소값으로 부터 1 바이트씩 값을 읽어오는 함수

그렇다면 이 함수에는 인자가 2개 전달 되어야 할 것이다. 특정한 주소값을 가리키는 포인터와 얼마나 읽을지에 대한 int 형 변수, 이렇게 두 가지를 받아야 한다. 그런데 인자로 전달될 포인터의 형은 제각각일 것이다. 예를 들어서 int * 일 수도 있고 double * 일 수도 있다.

따라서 순전히 주소값만 받기 위해서 void 형 포인터를 사용하는 것이 바람직하다. 물론 포인터간의 형 변환을 통해 처리할 수 있지만 어떠한 형태의 주소값도 가능하다라는 의미를 살려서 void 형 포인터를 이용하는 것이 바람직 하다.

#include <stdio.h>
int read_char(void *p, int byte);
int main() {
  int arr[1] = {0x12345678};

  printf("%x \n", arr[0]);
  read_char(arr, 4);
}
int read_char(void *p, int byte) {
  do {
    printf("%x \n", *(char *)p);
    byte--;

    p = (char *)p + 1;
  } while (p && byte);

  return 0;
}

실행 결과

12345678
78
56
34
12

찬찬히 살펴보자

do {
    printf("%x \n", *(char *)p);
    byte--;

    p = (char *)p + 1;
  } while (p && byte);

먼저 p = (char *)p + 1; 에 대해서 생각해보면 (char *)ppchar 형 포인터로 생각하라는 의미이다. 거기에 +1 을 하면 포인터의 덧셈으로 포인터 타입의 크기만큼 더해지니까 char 크기 만큼 더해져 1만큼 더해지게 된다.

아무튼 이러한 일들이 계속 반복되고 (char *)p 의 값이 0(NULL)이거나 byte 의 값이 0일 때 반복문이 종료된다.

printf("%x \n", *(char *)p);

p 가 가리키는 주소값에 위치한 데이터 1 바이트 씩 16진수로 출력하게 된다. 그리고 리틀 에디안이기 때문에 78 56 34 12 순으로 출력된다.

메인 함수의 인자

#include <stdio.h>
int main(int argc, char **argv) {
  printf("받은 인자의 개수 : %d \n", argc);
  printf("이 프로그램의 경로 : %s \n", argv[0]);

  return 0;
}

실행 결과

받은 인자의 개수 : 1 
이 프로그램의 경로 : /Users/****/Library/Developer/Xcode/DerivedData/HellowWorld-adklodlksjuwhyhcdfqgwpsspbda/Build/Products/Debug/HellowWorld
int main(int argc, char **argv)

보다시피 main 함수가 인자를 받고 있다. 이 인자에 누가 값을 넣어주고 있을까? 운영체제에서 알아서 인자를 넣어준다.

일단 argcmain 함수가 받은 인자의 수 이다. 그리고 argvmain 함수가 받은 각각의 인자들을 나타낸다. 프로그램을 실행하면 기본적으로 아무런 인자를 넣지 않더라도 위와 같은 정보는 들어가게 된다. 즉, main 함수는 자신의 실행 경로를 인자로 받게 된다. 그렇다면 다른 인자도 넣을 수 있을까?

/* 인자를 가지는 메인 함수 */
#include <stdio.h>
int main(int argc, char **argv) {
  int i;
  printf("받은 인자의 개수 : %d \n", argc);

  for (i = 0; i < argc; i++) {
    printf("이 프로그램이 받은 인자 : %s \n", argv[i]);
  }

  return 0;
}

이렇게 작성하고 터미널로 아까 위에서 실행 결과로 나왔던 프로그램의 경로로 들어가보자 /Users/****/Library/Developer/Xcode/DerivedData/HellowWorld-adklodlksjuwhyhcdfqgwpsspbda/Build/Products/Debug 까지 들어가서 ls 를 해보면

HellowWorld 라는 파일이 보인다. 이 파일을 실행함으로써 우리가 아까 만들어 놓은 소스 코드를 실행할 수 있는 것이다.

./HellowWolrd 라고 쳐서 실행해보면


잘 실행이 된다. 우리가 ./HellowWolrd 를 침으로써 실행하는 순간 프로그램의 첫번째 인자는 ./HellowWolrd 가 된다.

그렇다면 뒤에 다른 인자들을 넣어보자. 간단하게 ./HellowWolrd 뒤에 추가로 다른 것들을 더 써주면 된다.


./HellowWolrd 가 첫번째 인자, abc 가 두번째, def 가 세번째 인자가 된다. 그리고 받은 인자의 개수는 3이 된다.

그런데 여기까지 잘 따라온 사람은 main 함수에서 두번째 인자로 char ** 인데 이차원 배열을 전달하기 위해선 char (*avgv)[5] 와 같이 반드시 크기를 명시해줘야 하는거 아닌가라는 의문을 제기할 수 있다.

그 이유는 간단하다. char **(char *) 형 배열을 가리키는 포인터이기 때문이다. 즉, 포인터의 배열이다.(배열의 포인터가 아니다.) int arr[3] 라는 배열을 가리키는 포인터가 int * 형 인것 처럼 char *arr[5] 를 가리키는 포인터의 형은 char ** 가 된다.

argv 는 포인터들의 배열을 가리키고 있고 그 포인터 배열에서의 각각의 원소, 즉 포인터들은 인자로 전달된 문자열을 가리키고 있다.

따라서 우리는 argv[i] 를 통해 특정한 인자의 문자열에 저장된 주소값을 나타낼 수 있다.

profile
벨로그보단 티스토리를 사용합니다! https://flight-developer-stroy.tistory.com/

1개의 댓글

comment-user-thumbnail
2023년 9월 21일

공백형에 대해 알려주셔서 캄사합니다!

답글 달기