본문 바로가기

컴퓨터공학/C, C++

C,C++> 함수

함수별로 특정 기능을 담당하도록 프로그램 기능을 분할하면 코드 구조가 만들어지기 때문에 C언어를 구조적 프로그래밍 언어라고 하는 이유는 바로 여기에 있다. 

 

인수(Parameter)는 호출원에서 함수에게 넘겨주는 작업 대상이라고 할 수 있다. 

두 함수 사이 정보 교환에 사용되므로 매개 변수(Argument)라고 한다. 

인수는 형식인수와 실인수로 구분된다. 

함수의 인수 목록에 나타나는 인수를 형식 인수

함수 호출부에서 함수와 함께 전달되는 인수를 실인수 

표준 문서에서는 형식인수를 Parameter, 실인수를 Argument로 구분하고 있다. 

 

return 

가장 일반적 기능은 함수 결과값을 호출원으로 돌려주는 것이다. 

두 번째 기능은 함수를 강제 종료하는 기능을 한다. 

return 문이 있으면 뒷 부분이 남이있건 간에 무조건 함수를 종료하고 호출원으로 돌아가기 때문이다. 

return 문 함수강제 종료 기능은 인수 유효성 검사할 때 많이 사용한다. 

그래서 main 함수에서 return을 사용하는 건 곧 프로그램 종료를 의미한다.  

 

 

 

프로그래밍 언어는 해석 방식에 따라서 인터프리터 방식과 컴파일 방식으로 나뉘어 진다.

컴파일 방식이 성능이 더 좋다.  컴파일 방식은 소스를 읽어 기계어로 한꺼번에 번역하는 방식이다.  

번역을 몇 번에 나누어 하느냐에 따라서 1패스, 2패스 등으로 구분된다. 

한 번 읽어서 번역을 다하면 1패스 방식이다.  한 번 읽고 컴파일 준비한 다음 다시 읽어서 기계어 코드로 바꾸는 방식을 2패스 방식이다. 

2패스 방식이 대표적인 예가 어셈블러이다.

소스상 위치를 나타내는 레이블을 실제 번지로 바꾸기 위해 처음부터 끝까지 소스를 읽어 레이블 번지를 미리 파악하고 다시 처음부터 읽어서 기계어 코드로 번역한다. 어셈블러는 소스를 두 번 읽지 않으면 컴파일이 불가능하다. 

 

c언어 컴파일러들은 1패스 방식으로 작성되었기 때문에 c표준은 한 번에 소스를 읽을 수 있는 장치인 함수 원형을 마련할 필요가 있었다. 

원형(ProtoType)이란 함수에 관한 정확한 정보라는 뜻이다.

함수 원형은 리턴 타입, 함수 이름, 인수 리스트 등 정보들로 구성된다. 

구체적으로 이 방식이 왜 필요한 걸까? 

 

선언과 대입이 순서가 바뀌어 있으면 에러가 나므로 순서대로 해야 제대로 컴파일이 된다. 

먼저 정의하고 호출하면 문제가 없다. 그러나 이런 방법으로 복잡한 프로그램을 작성하기엔 한계가 있다.

왜냐하면 함수가 많아지고 함수끼리 복잡하게 호출하면 순서 정하기가 매우 어려워 지기 때문이다. 

따라서 함수를 사용하기 전에 미리 그 형태를 컴파일러가 알 수 있도록 해야 하는데 그 방법이 바로 원형을 선언하는 것이다.

함수 원형을 미리 선언하면 본체는 뒤에 있어도 함수 호출부에서 이 명칭이 함수이고 어떤 인수를 요구하는지 미리 알 수 있게 된다. 

이렇게 함수 정보를 미리 컴파일러에게 미리 알려주는 걸 "원형을 선언한다" 라고 한다. 

즉, 딱 한 번 읽어서 번역을 하는데 뒷 부분에 나올 함수 정보를 미리 제공하는 것이다.

 

원형 형식

원형에 적는 형식 인수 이름은 사실 아무거나 적어도 상관없다. 

원형 선언은 본체가 없어서  형식 인수를 사용하지 않기 때문이다. 

형식 인수 이름이 의미가 없어서 형식 인수를 생략한 간략한 원형 선언 방식이 있다. 

형식 인수 이름은 빼 버리고 인수 타입만 적는 방식이다.

하지만 가급적이면 형식 인수 이름을 적는게 좋다. 왜냐하면 안 적으면 함수 본체를 보거나 레퍼런스 참조를 해야 하기 때문이다. 

가독성을 따져서 이름을 적어 놓는게 좋다. 

 

헤더파일 

표준 함수 원형을 미리 작성해 놓은 것을 헤더 파일이라고 한다. 

 

 

모듈

원형만 선형하면 함수 본체가 어디에 있던 상관없다. 컴파일러가 이미 함수 정보를 확보했으니 호출부보다 앞쪽에 있어도 되고 되에 있어도 상관 없다. 심지어 다른 소스 파일에 함수 본체를 둘 수 있다.

프로젝트는 하나의 실행 파일을 만들기 위한 모듈 집합이고 한 프로젝트 안에 여러 개의 모듈을 둘 수 있다.

따라서 함수를 그룹별로 모둘을 구성하고 각 모듈은 헤더 파일을 통해 제공하는 함수 목록을 제공하면 된다. 

이런 식으로 하나의 실행 파일을 만들기 위해 소스를 여러 개로 나누어 개발하는 방식을 모듈 분할(또는 다중 모듈 컴파일) 방식이라고 한다.

다음과 같은 장점이 있다.

1. 컴파일 속도가 빠르다. 왜냐하면 한 함수를 수정하고 컴파일하면 함수가 속한 모듈만 컴파일하면 된다. 

2. 분담 작업 가능하다. 

3. 프로젝트 관리가 쉽다. 

4. 재사용할 수 있다. 

 

함수 호출 방식

값호출(Call by value) 참조호출(Call by reference) 

형식 인수가 대입받는 대상이 실인수 값. 실인수의 임시 사본이고 실인수와 전혀 다른 새로운 변수. 같은 값을 가질 뿐이지 기억되는 메모리 공간 자체가 다르다. 사본값이 증가하든 감소하든 원본에 영향을 주지 못한다. 실인수 그 자체가 아니라 값이기 때문에 값호출이라고 한다. 

 

참조호출은 번지를 전달하는 방식이다. 메인 함수에서 주소값을 형식 인수에게 전달하고 함수에서 주고에 있는 값을 증가시킨다.

함수 내부에서 포인터를 통해 실인수값을 직접 조작하므로 결과값 리턴이 필요가 없다. 

값 호출이라는 말은 실인수로 값을 넘긴다는 뜻이고 참조 호출이라는 말은 번지값을 전달받아 실인수를 직접 참조할 수 있다는 뜻이다.

 

값 호출과 참조 호출의 또 다른 차이점은 실인수로 상수를 전달할 수 있는가 하는 점이다. 

값 호출 방식은 값을 전달하므로 상수를 실인수로 사용한다. 값을 가지기만 하면 형식인수가 이 값을 대입받는다. 

참조 호출은 주소값을 전달받기 때문에 주소값이 있는 변수만 사용할 수 있다. 상수는 사용할 수 없다. 왜냐하면 상수는 메모리 점유를 하지 않기 때문에 주소값이 없다. 

 

배열 이름은 배열 선두 번지를 나타내는 포인터 상수이다. 

 

전처리기기는 앞서 먼저 처리하는 명령이라는 뜻이다. 컴파일하기 전에 소스를 재작성하는 역할을 한다. 컴파일러가 소스를 읽기 전 전처리기기가 먼저 실행되어 컴파일하기 좋도록 소스 모양을 정리한다. 전처리기기는 코드 생성을 하지 않으며 어디까지 소스 재구성할 뿐이다. 

컴파일 전 단게에서 실행하므로 전처리문은 반드시 한 행을 모두 차지해야 하며 전처리문 뒤에 c 코드를 같이 쓸 수 없다. 

 

c에서 제공하는 표준 헤더파일을 포함할 때는 <> 괄호를 사용한다. 

사용자가 직접 작성한 헤더 파일은 ""괄호를 사용한다. 

헤더 파일만 아니라 cpp파일도 가능하고 txt파일도 가능하다.  

다른 디렉토리에 있으면 디렉토리 경로 사용 가능.

상대 경로 지정법과 동일. 절대 경로도 줄 수 있지만 절대 경로는 다른 컴퓨터에서 컴파일할 때 디렉토리 구조를 똑같이 만들어야 하는 제약이 있어서 프로젝트 관리가 번거로워 진다. 

디렉토리 경로를 구분할 때 윈도우는 역슬레쉬를 사용하지만 슬레쉬를 사용한다. 왜냐하면 c언어가 유닉스에서 만들어졌기 때문에 유닉스 디렉토리 구분자인 슬레쉬를 사용하는 것이다. 문자열 상수 내에서는 \는 확장열이라서 \\로 써야하지만 헤더 파일 경로 표기할 때는 한 번만 써도 상관 없다. 헤더 파일명 지정 표현식은 전처리 단계에서 실행되어 컴파일 단계에서는 존재하지 않으므로 문자열 상수가 아니기 때문이다.

#include 다음 파일명은 대소문자 구분을 하지 않는다. c언어는 대소문자를 구분하지만 위도우즈 파일 시스템은 대소문자를 구분하지 않기 때문에 문제없다. 근데 유닉스나 리눅스 환경에서는 대소문자가 구분되므로 원래 파일명과 똑같이 쓴는 것이 좋다. 

 

#define 전처리문은 매크로 상수를 정의한다. 

매크로 상수를 정의하여 사용하면 가독성과 관리가 좋아진다.

가독성은 그렇다 치고 왜 관리가 좋아질까? 예를 들어 240 상수를 사용하는데 360으로 고쳐야 한다면 일일이 찾아서 240으로 바꿀 필요 없이 매크로 상수만 360으로 바꾸면 된다. #define 전처리가 소스 모든 상수를 일괄 치환하므로 수정하기 간편하고 실수로 빼먹을 위험이 없어진다. 이처럼 두 번 이상 사용할 가능성이 있는 상수라면 처음부터 매크로 상수를 정의하는 것이 좋다. 

#define문은 전처리문이지 코드 생성하는 명령이 아니다. 그래서 행 끝에 세미콜론을 붙이지 않는다. 

전처리문은 세미콜론을 붙이지 않으며 주석을 제외한 다른 문장이 뒤따라 나올 수 없다. 

매크로 이름은 공백이 들어갈 수 없는데 매크로 실제 값은 공백을 가질 수 있다. #define 전처리문은 매크로를 실제 값으로 단순 치환할 뿐 공백이 있건 한글을 사용하건 상관하지 않는다. 

문자열 상수 내 있는 매크로는 치환되지 않는다. 

매크로 중첩 가능.

값이 없는 빈 매크로도 정의 가능.

 

매크로 함수는 전처리기기를 이용하여 함수 흉내를 내는 것이다. 

함수 전체식을 괄호로 싸야 한다. 

매크로 인수도 개별적으로 괄호로 싸준다. 

매크로 함수는 인수 타입을 점검하지 않는다. 

매크로 함수에 여러 개 명령을 동시 포함할 수 있다. 

매크로 함수 호출문에는 증감 연산자나 복합 대입 연산자를 쓰지 않는 것이 좋다.  왜냐하면 결과 예측이 어렵고 부작용 발생이 생길 수 있기 때문이다. 

 

대부분 c 소스는 다음과 같은 구조를 가진다. 

#include <...>

#define <...>

함수 원형

전역변수

void main(){

코드

}

함수

함수

 

전처리문 순서에서 #include와 #define 순서는 고려해야 한다. #define이 #include보다 먼저 오는 건 바람직하지 않다. 왜냐하면 헤더 파일의 함수 원형 내용까지 매크로 치환이 되어버려 이상한 문자열이 들어가 버릴 수 있기 때문에 컴파일러가 함수 형태를 제대로 파악하지 못할 수 있기 때문이다. 

'컴퓨터공학 > C, C++' 카테고리의 다른 글

C,C++> 배열  (0) 2022.06.09
C,C++> 기억 부류  (0) 2022.06.08
C,C++> 조건문 제어문 연산자  (0) 2022.06.01
C,C++> 변수 이야기  (0) 2022.05.25
C, C++ 공부 프롤로그  (0) 2022.05.08