C Language [핵심정리] - 36
1. 함수 사용하기 - 2
"Hello World!"를 출력하는 함수를 만들어보겠습니다.
여기서 main 함수는 프로그램이 시작되었을 때 가장 먼저 실행되는 함수입니다. 그래서 main 함수를 가장 앞에 위치시켜야 한다고 생각할 수 있습니다만 안타깝게도 잘못된 생각입니다. 왜냐하면 C언어가 절차지향 언어라 위에서 아래로 실행되기 때문이에요. 만약 생각하신 것처럼 main 함수가 가장 위에 위치한다면 PRINT_HELLO 함수를 호출할 때 PRINT_HELLO 함수는 아직 정의 되지 않았기에 컴파일러는 알지 못합니다.
따라서 이 경우에는 함수 원형을 선언함으로써 우리가 이런 함수를 사용할 거라고 명시해주면 되요.
어,, 이렇게 보니? 우리가 그전에 구조체 배울 때 사용한 전방선언(Forward Declaration)과 비슷하죠?
여기서 사용한 함수 PRINT_HELLO()는 void 라는 반환값을 가지고 있습니다. void는 아무것도 없다라는 뜻이에요. 그러니까 함수의 반환값이 없다는 뜻이 되겠네요. 그래서 저 함수를 해석하면 PRINT_HELLO 함수는 전달값과 반환값이 존재하지 않는 단순 반복을 수행할 때 사용되는 함수입니다. 정도로 해석되겠네요.
main 함수에서 PRINT_HELLO 함수를 사용할 때는 함수의 이름과 괄호를 사용해 전달값을 적어주고(없다면 아무것도 적지 않습니다.) 세미콜론을 붙이면 함수를 사용할 수 있습니다. 이처럼 함수를 사용하는 방법을 함수를 호출(call)한다고 합니다.
+ 지역변수
함수 안에 선언된 변수를 지역 변수라고 부릅니다. 이 지역변수의 특징은 함수가 끝나면 사라진다는 점입니다. 즉, HELLO 함수 안에 선언된 지역변수는 HELLO 함수 안에서만 사용할 수 있습니다.
함수를 디버깅 하는 방법은 함수의 시작부분에 중단점을 설정하면 됩니다.
프로그램이 시작될 때 main 함수가 호출되고 main 함수에서 hello 함수를 호출했으므로, main -> hello 순으로 중단점이 작동하시는 거 보이나요? 이처럼 중단점은 여러 개 설정할 수 있으며, 함수마다 중단점을 설정하면 함수의 실행 순서를 파악할 수 있습니다.
hello 함수와는 다르게 반환값(return value)를 통해 함수에서 값을 꺼내오는 함수를 만들어보겠습니다. 이 때 반환값은 함수를 호출해준 외부에 결과값을 전달하기 위해 사용합니다. 반환값은 함수안에서 return 키워드로 값을 반환하면 됩니다.
여기선 반환값과 반환값의 자료형이 일치해야 한다는 건 당연하겠죠?
그러니까 반환값이 무엇이든 반환값과 반환값의 자료형을 일치해준다는 사실만 기억해주시면 됩니다.
만약 반환값의 자료형과 반환하는 값의 자료형이 다르다면 형변환의 규칙을 따릅니다. 만약 큰 자료형에서 작은 자료형으로 형변환 되어 값의 손실이 일어날 수 있다는 경고가 발생한다면 자료형을 변환하며 반환하면 됩니다.
◇ return (자료형)변수; ◆ return (자료형)값;
위에 반환값의 자료형과 반환하려는 값의 자료형이 다른 경우와는 달리 함수 반환값의 자료형과 반환값을 저장할 변수의 자료형이 다를 때도 있는데 이때도 컴파일 경고가 발생한다면 다음과 같이 함수 자체를 형변환 시킨 상태로 호출해서 반환값의 자료형을 강제로 변환합니다.
이제 함수에서 일반적인 값이 아닌 포인터(메모리 주소)를 가져와보겠습니다. 포인터를 반환하려면 반환 값 자료형과 함수명 사이에 *(Asterisk)를 사용하면 됩니다.
지역변수 또는 임시 변수의 주소를 반환한다고 경고가 발생했네요. 즉, 함수 안에서만 사용할 수 있는 변수 num(지역변수)은 함수가 끝나면 사라지기 때문에 return # 과 같이 지역변수의 주소를 반환하는 건 잘못된 방법입니다. 그럼에도 불구하고 값이 정상적으로 나온것은 해당 변수가 덮어쓰여지지 않았기 때문입니다.
따라서 포인터를 반환하려면 malloc 함수로 메모리를 할당한 뒤 반환해야 합니다.
우리는 외부 함수에서 malloc 을 통해 포인터에 공간을 동적으로 할당했지만, 이 할당한 공간을 해제하기 전까진 외부 함수 뿐만 아니라 main 함수 내부에서도 사용할 수 있어요. 다만 이처럼 동적 메모리는 함수를 벗어나도 유지되기 때문에 main 함수에서 사용하고 나면 해제를 해줘야 해요. (할당을 main 함수에서 하지않았더라도)
보통 포인터를 반환하는 함수를 정의할 떄 *(Asterisk)의 위치가 애매한 경우가 있습니다. 왜냐하면 자료형 바로 앞, 중간, 뒤를 모두 C언어에서 허용하기 때문인데요. 사실 위치는 기능과 별 상관없고 취향차이라고 보시면 됩니다. 더군다나 저와 같이 visual studio를 쓰시고 계실 경우 컴파일러가 알아서 위치를 조정해주기 때문에 위치때문에 고민하시는 일은 없었으면 합니다.
방금 전까진 자료형을 정해서 값을 반환했습니다. 이제는 자료형에 상관없이 값을 꺼내기 위해 void 포인터를 반환해보겠습니다.
void 포인터는 모든 형태로 변할 수 있기에 함수를 호출할 때 강제 형변환을 하지 않았습니다.
코드의 길이를 줄이고 싶다면 포인터 변수를 선언하지 말고 바로 malloc을 호출하며 반환하면 됩니다.
어차피 우리는 메모리를 해제할 때 해당 공간(동적할당 된)을 반환 받은 변수명으로 해제하지 함수에서 할당한 변수 명으로 해제하지 않기 때문에 이처럼 굳이 변수를 선언해줄 필요는 없습니다.
그리고 C언어에서는 값을 하나만 반환할 수 있습니다. 물론
이처럼 조건에 따라 값을 다르게 반환할 수는 있으나 이 방법 또한 하나의 경우에 다른 값을 반환하는 것 뿐이지 반환하는 값이 1개에서 2개로 늘어나는 건 아닙니다. 그런데 만약 인적정보와 같이 여러 개의 값을 반환 받아야 할 상황이 생긴다면 어떻게 해야할까요? 그 때도 인적 정보를 하나씩 항목마다 받아올까요? 그건 너무 비효율적입니다.
따라서 이 경우 우리는 반환 값으로 구조체를 사용하여 조금 더 간결하게 만들 수 있습니다.
여기서 중요한 점 다른 함수들과는 다르게 반환값으로 구조체를 가지는 함수는 정의할 때는 struct 로 끝나지 않고 struct □□처럼 사용할 구조체 명을 함께 명시해야 합니다.
우리는 위 코드의 결과를 통해 구조체 변수를 반환한 뒤 다른 변수에 저장하면 반환된 구조체의 내용을 모두 복사하게 된다는 점을 알아낼 수 있었습니다. 하지만 구조체의 크기가 커지면 복사할 크기 또한 그만큼 커지기 때문에 비효율적이죠.
+ 함수가 끝나면 구조체 변수도 사라집니다. 따라서 &(주소 연산자)로 구조체 변수의 메모리 주소를 반환하면 안됩니다.
그래서 우리는 비효율적인 구조체 복사가 일어나지 않도록 malloc 함수로 동적 메모리 할당을 한뒤 구조체 포인터를 반환하는 게 더 좋습니다.
위 코드는 그전과는 달리 모든 내용을 복사한게 아니라 메모리를 할당한 뒤 메모리 주소가 들어있는 포인터만을 반환하여 사용하므로 구조체 내용을 모두 복사하지 않아 더 효율적입니다.