9. C++ 함수(C++ Function)
1. 키워드
- 참조자(Reference)
- 디폴트 인수(Default Argument)
- 함수 오버로딩(Function Overloading)과 함수 시그니처(Function Signature)
- 인라인 함수(Inline Function)와
inline
키워드 - 매크로 함수(Macro Function)
2. 참조자
- C++에서는 특정 변수의 실제 이름 대신 사용할 수 있는 참조자라는 새로운 기능이 추가되었다.
- 이러한 참조자는 크기가 큰 구조체와 같은 데이터를 함수의 인수로 전달해야 할 경우에 사용할 수 있다.
- 또한, C++의 클래스를 설계할 때에도 자주 사용된다.
1) 참조자의 선언
- C++에서 참조자는 다음과 같은 문법으로 선언한다.
- 이때
&
는 주소 연산자가 아닌 타입을 식별하기 위해 사용하는 식별자로 사용된 것이다. - 즉,
int &
는int
타입 변수에 대한 참조를 의미한다. - 이렇게 선언된 참조자는 대상 변수와 같은 메모리 위치를 참조하게 된다.
- 참조자를 선언할 때에는 다음과 같은 사항에 주의해야 한다.
1] 참조자의 타입은 대상이 되는 변수의 타입과 일치해야 한다.
2] 참조자는 선언과 동시에 초기화되어야 한다.
3] 참조자는 한 번 초기화되면, 참조하는 대상을 변경할 수 없다.
- 다음의 코드에서 참조자를 이용해 증가 연산을 수행하면, 참조 변수뿐만 아니라 대상 변수도 같이 변경됨을 알 수 있다.
#include <iostream>
using namespace std;
int main(void) {
int x = 10; // 변수의 선언
int &y = x; // 참조자 선언
cout << "x: " << x << ", y: " << y << endl;
y++; // 참조자를 이용한 증가 연산
cout << "x: " << x << ", y: " << y << endl;
cout << "x의 주소값: " << &x << ", y의 주소값: " << &y << endl;
return 0;
}
// x: 10, y: 10
// x: 11, y: 11
// x의 주소값: 0x16b433478, y의 주소값: 0x16b433478
2) 함수의 인수로서 전달
- C++에서 참조자는 주로 함수에 인수를 전달할 때 사용된다.
- 함수가 참조자를 인수로 전달받으면, 참조자가 참조하고 있는 실제 변수의 값을 함수 내에서 조작할 수 있다.
- 다음의 코드는 참조자를 사용하여 두 변수의 값을 서로 맞바꾸는 것이다.
#include <iostream>
using namespace std;
void Swap(int &, int &);
int main(void) {
int num1 = 3, num2 = 7;
cout << "변경 전 num1의 값은 " << num1
<< "이며, num2의 값은 " << num2 << "입니다." << endl;
Swap(num1, num2);
cout << "변경 후 num1의 값은 " << num1
<< "이며, num2의 값은 " << num2 << "입니다." << endl;
return 0;
}
void Swap(int &x, int &y) {
int temp;
temp = x;
x = y;
y = temp;
}
// 변경 전 num1의 값은 3이며, num2의 값은 7입니다.
// 변경 후 num1의 값은 7이며, num2의 값은 3입니다.
- 위와 같은 참조에 의한 전달은 참조자뿐만 아니라 포인터를 사용해도 똑같은 결과를 얻을 수 있다.
- 포인터를 사용하는 방법과 참조자를 사용하는 방법 모두 결과는 같으며, 구문 형태 상의 차이점만이 존재한다.
- 다음의 코드는 포인터를 사용하여 두 변수의 값을 서로 맞바꾸는 것이다.
#include <iostream>
using namespace std;
void Swap(int *, int *);
int main(void) {
int num1 = 3, num2 = 7;
int *ptr_num1 = &num1, *ptr_num2 = &num2;
cout << "변경 전 num1의 값은 " << num1
<< "이며, num2의 값은 " << num2 << "입니다." << endl;
Swap(ptr_num1, ptr_num2);
cout << "변경 후 num1의 값은 " << num1
<< "이며, num2의 값은 " << num2 << "입니다." << endl;
return 0;
}
void Swap(int *ptr_x, int *ptr_y) {
int temp;
temp = *ptr_x;
*ptr_x = *ptr_y;
*ptr_y = temp;
}
// 변경 전 num1의 값은 3이며, num2의 값은 7입니다.
// 변경 후 num1의 값은 7이며, num2의 값은 3입니다.
- C++에서 함수의 인수로 참조자를 사용하는 방법의 특징은 다음과 같다.
1] 함수 내에서 *
(참조 연산자)를 사용하지 않으므로, 함수 내부의 코드가 깔끔하고 직관적이다.
2] 함수의 호출이 값에 의한 전달 방법과 같은 형태가 되어, 코드를 읽기가 쉽지 않다.
- 따라서 간단한 함수에서는 굳이 참조에 의한 전달을 하지 말고 값에 의한 전달을 사용하는 것이 좋을 수 있다.
- 또한, 참조 호출이 꼭 필요할 때에는 참조자보다는 포인터를 사용하는 것이 더욱 직관적일 수 있다.
- 이런 이유로 C++에서 참조자는 크기가 큰 구조체나 클래스를 다룰 때에만 사용하는 것이 좋다.
3) 구조체의 참조
- C++에서 참조자는 주로 구조체와 같은 사용자 정의 타입을 다룰 때 유용하게 사용된다.
- 구조체를 참조하는 방법은 변수를 참조하는 방법과 같다.
#include <iostream>
#include <string>
using namespace std;
struct Book {
string title;
string author;
int price;
};
void Display(const Book &);
int main(void) {
Book web_book = {.title = "HTML과 CSS", .author = "홍길동", .price = 28000};
Display(web_book);
return 0;
}
void Display(const Book &bk) {
cout << "책의 제목은 " << bk.title << "이고, "
<< "저자는 " << bk.author << "이며, "
<< "가격은 " << bk.price << "원입니다." << endl;
}
// 책의 제목은 HTML과 CSS이고, 저자는 홍길동이며, 가격은 28000원입니다.
- 위의 코드처럼 함수 내부에서 구조체를 직접 변경할 필요가 없을 때는
const
키워드를 사용하여 원본 구조체에 대한 변경을 허용하지 않는 것이 좋다.
- 다음의 코드는 포인터를 사용하여 구조체를 참조하는 것이다.
#include <iostream>
#include <string>
using namespace std;
struct Book {
string title;
string author;
int price;
};
void Display(const Book *);
int main(void) {
Book web_book = {.title = "HTML과 CSS", .author = "홍길동", .price = 28000};
Book *ptr_web_book = &web_book;
Display(ptr_web_book);
return 0;
}
void Display(const Book *ptr_bk) {
cout << "책의 제목은 " << ptr_bk->title << "이고, "
<< "저자는 " << ptr_bk->author << "이며, "
<< "가격은 " << ptr_bk->price << "원입니다." << endl;
}
// 책의 제목은 HTML과 CSS이고, 저자는 홍길동이며, 가격은 28000원입니다.
- 함수 내에서 참조자를 사용하면 변수나 구조체 등을 직접적으로 다루는 것과 같은 느낌이고, 포인터를 사용하면 주소를 통해 간접적으로 다루는 것과 같은 느낌이다.
3. 디폴트 인수
- C++에서 새롭게 정의된 디폴트 인수는 기본값이 미리 정의되어 있는 인수를 의미한다.
- 함수를 호출할 때 인수를 전달하지 않으면, 함수는 자동으로 미리 정의되어 있는 디폴트 인수값을 사용하게 된다.
- 물론 인수를 전달하여 함수를 호출하면, 디폴트 인수값이 아닌 전달된 인수를 가지고 함수를 호출하게 된다.
1) 디폴트 인수의 설정
- C++에서 디폴트 인수를 설정할 때에는 다음과 같은 사항에 주의해야 한다.
1] 디폴트 인수는 함수의 원형에만 지정할 수 있다.
2] 디폴트 인수는 가장 오른쪽부터 시작하여 순서대로만 지정할 수 있다.
3] 가운데 인수들만 별도로 디폴트 인수를 지정할 수는 없다.
1. void Display(int x, int y, char ch, int z = 4); // 가능함
2. void Display(int x, int y, char ch = 'a', int z = 4); // 가능함
3. void Display(int x, int y = 2, char ch, int z = 4); // 오류
4. void Display(int x = 1, int y = 2, char ch, int z); // 오류
2) 디폴트 인수가 설정된 함수의 호출
- 함수로 전달된 인수는 왼쪽부터 순서대로 매개변수 목록에 대입된다.
- 따라서 디폴트 인수가 설정된 함수를 호출할 때에는 인수의 전달을 건너뛸 수 없다.
void Display(int x, int y, char ch = 'a', int z = 4); // 함수의 원형
1. Display(1, 2); // 가능함 -> display(1, 2, 'a', 4)와 같음
2. Display(3, 4, 'b'); // 가능함 -> display(3, 4, 'b', 4)와 같음
3. Display(5, 6, 'c', 9); // 가능함 -> display(5, 6, 'c', 8)와 같음
4. Display(7, 8, , 9); // 오류: 인수 전달은 건너뛸 수 없음
#include <iostream>
using namespace std;
double Multi(double, double n = 2);
int main(void) {
cout << Multi(3) << endl; // Multi(3, 2)와 같음 : 3 * 3
cout << Multi(3, 3) << endl; // 3 * 3 * 3
cout << Multi(3, 4) << endl; // 3 * 3 * 3 * 3
return 0;
}
double Multi(double x, double n) {
double result = x;
for (int i = 1; i < n; i++) {
result *= x;
}
return result;
}
// 9
// 27
// 81
4. 함수 오버로딩
- 디폴트 인수가 인수의 개수를 달리하여 같은 함수를 호출하는 것이라면, 함수 오버로딩은 같은 이름의 함수를 중복하여 정의하는 것을 의미한다.
- C++에서 새롭게 추가된 함수 오버로딩은 여러 함수를 하나의 이름으로 연결해 준다.
- 즉, 함수 오버로딩이란 같은 일을 처리하는 함수를 매개변수의 형식을 조금씩 달리하여, 하나의 이름으로 작성할 수 있게 해주는 것이다.
- 이와 같은 함수 오버로딩은 객체 지향 프로그래밍의 특징 중 바로 다형성의 구현이다.
1) 함수 시그니처
- 함수 오버로딩의 핵심은 바로 함수 시그니처에 있다.
- 함수 시그니처란 함수의 원형에 명시되는 매개변수 리스트를 가리킨다.
- 만약 두 함수의 매개변수의 개수와 그 타입이 모두 같다면, 이 두 함수의 시그니처는 같다고 할 수 있다.
- 즉, 함수의 오버로딩은 서로 다른 시그니처를 가지는 여러 함수를 같은 이름으로 정의하는 것이라고 할 수 있다.
2) 함수 오버로딩의 예제
- C++ 컴파일러는 사용자가 오버로딩된 함수를 호출하면, 그것과 같은 시그니처를 가지는 함수의 함수의 원형을 찾아 호출해 준다.
- 다음의 코드는 함수의 오버로딩을 이용하여 정의한
Display()
함수의 원형이다.
1. void Display(const char* str, int n); // 문자열 str을 n번 출력함
2. void Display(const char* str1, const char* str2); // 문자열 str1과 str2를 연달아 출력함
3. void Display(int x, int y); // x * y를 출력함
4. void Display(double x, double y); // x / y를 출력함
- 이제 사용자가
Display()
함수를 호출하면, C++ 컴파일러는 자동으로 같은 시그니처를 가지는 함수의 원형을 찾아 호출한다.
1. Display("C++", 3); // 1번 Display() 함수 호출 -> "C++C++C++"
2. Display("C++", " Programming"); // 2번 Display() 함수 호출 -> "C++ Programming"
3. Display(3, 4); // 3번 Display() 함수 호출 -> 12
4. Display(4.2, 2.1); // 4번 Display() 함수 호출 -> 2
5. Display(4.2, 3); // 3번과 4번 모두 호출 가능 -> 컴파일 오류가 발생함
- 문제는
Display(4.2, 3);
과 같은 함수의 호출로 첫 번째 인수로는double
타입의 인수를, 두 번째 인수로는int
타입의 인수를 전달한다. - 이와 같은 함수 호출은
void Display(int x, int y);
과void Display(double x, double y);
과 같은 시그니처를 가지는Display()
함수를 모두 호출할 수 있으므로, 모호한 호출이 된다. - C++에서는 오버로딩한 함수의 이러한 모호한 호출을 허용하지 않으며, 이럴 때에는 오류를 발생시킨다.
- 다음의 코드는 함수 오버로딩을 이용한 것이다.
#include <iostream>
using namespace std;
void Shift(int, int);
void Shift(int, int, int);
void Shift(int, int, int, int);
int main(void) {
Shift(1, 2);
Shift(1, 2, 3);
Shift(1, 2, 3, 4);
return 0;
}
void Shift(int w, int x) {
int temp;
temp = w;
w = x;
x = temp;
cout << w << ", " << x << endl;
}
void Shift(int w, int x, int y) {
int temp;
temp = w;
w = x;
x = y;
y = temp;
cout << w << ", " << x << ", " << y << endl;
}
void Shift(int w, int x, int y, int z) {
int temp;
temp = w;
w = x;
x = y;
y = z;
z = temp;
cout << w << ", " << x << ", " << y << ", " << z << endl;
}
// 2, 1
// 2, 3, 1
// 2, 3, 4, 1
- 위의 코드처럼 함수의 오버로딩은 매개변수의 타입뿐만 아니라 매개변수의 개수를 달리해도 작성할 수 있다.
5. 인라인 함수
1) C++에서의 함수 호출 과정
- 함수가 호출되면 우선 스택에, 함수로 전달할 매개변수와 함께 호출이 끝난 뒤 돌아갈 반환 주소값을 저장하게 된다.
- 그리고서 프로그램의 제어가 함수의 위치로 넘어와 함수 내에 선언된 지역 변수도 스택에 저장한다.
- 그때부터 함수의 모든 코드를 실행하게 되고, 실행이 전부 끝나면 반환값을 넘겨 준다.
- 그 후 프로그램의 제어는 스택에 저장된 돌아갈 반환 주소값으로 이동하여, 스택에 저장된 함수 호출 정보를 제거한다.
- 이와 같은 일련의 함수 호출 과정이 함수마다 일어나게 된다.
2) 인라인 함수
- 위와 같이 C++에서 함수의 호출은 꽤 복잡한 과정을 거치므로, 약간의 시간이 걸리게 된다.
- 이때 함수를 실행하는 시간이 오래 걸린다면, 함수를 호출하는 데 걸리는 시간은 전혀 문제가 되지 않는다.
- 하지만 함수의 실행 시간이 매우 짧다면, 함수 호출에 걸리는 시간도 부담이 될 수 있다.
- C++에서는 이러한 경우에 사용할 수 있는 인라인 함수라는 것을 제공한다.
- 인라인 함수는 호출될 때 일반적인 함수의 호출 과정을 거치지 않고, 함수의 모든 코드를 호출된 자리에 바로 삽입하는 방식의 함수이다.
- 이 방식은 함수를 호출하는 데 걸리는 시간은 절약되나, 함수 호출 과정으로 생기는 여러 이점을 포기하게 된다.
- 따라서 코드가 매우 적은 함수만을 인라인 함수로 선언하는 것이 좋다.
3) 인라인 함수의 선언
- C++에서 인라인 함수는 다음과 같이 선언한다.
inline
키워드는 함수의 원형이나 함수의 정의 어느 한 쪽에만 표기해도 되며, 양쪽 다 표기해도 상관없다.
#include <iostream>
using namespace std;
inline int Sub(int, int);
void Print(int);
int main(void) {
int num1 = 5, num2 = 3;
int result;
result = Sub(num1, num2);
Print(result);
return 0;
}
int Sub(int x, int y) { return x - y; }
inline void Print(int x) {
cout << "계산 결과는 " << x << "입니다." << endl;
}
// 계산 결과는 2입니다.
- 위의 코드에서
Sub()
함수와Print()
함수는 인라인 함수로 정의되어 호출된다. - 보통 인라인 함수는 크기가 작으므로, 함수의 원형이 나오는 자리에 함수의 본체까지 함께 정의하는 경우가 많다.
#include <iostream>
using namespace std;
inline int Sub(int x, int y) { return x - y; }
inline void Print(int x) {
cout << "계산 결과는 " << x << "입니다." << endl;
}
int main(void) {
int num1 = 5, num2 = 3;
int result;
result = Sub(num1, num2);
Print(result);
return 0;
}
// 계산 결과는 2입니다.
- 위의 코드는 실제로는 다음과 같이 인라인 코드로 삽입되어 실행된다.
#include <iostream>
using namespace std;
int main(void) {
int num1 = 5, num2 = 3;
int result;
{
int x = num1, y = num2;
result = x - y;
}
{
int x = result;
cout << "계산 결과는 " << x << "입니다." << endl;
}
return 0;
}
// 계산 결과는 2입니다.
- 참고로 인라인 함수에서는 재귀 호출이 허용되지 않는다.
4) 매크로 함수와 인라인 함수
- C에서는 C++의 인라인 함수와 비슷한 기능의 매크로 함수를 사용한다.
#define
선행처리 지시문에 인수로 함수의 정의를 전달함으로써, 함수처럼 동작하는 매크로를 만들 수 있다.- 이러한 매크로를 함수 같은 매크로(Function-like Macro) 또는 매크로 함수라고 한다.
- 하지만 매크로 함수는 일반 함수와는 달리 단순 치환만을 해주므로, 일반 함수와 똑같은 방식으로 동작하지 않는다.
- 이러한 매크로 함수를 일반 함수처럼 사용하기 위해서는 모든 인수를
()
로 감싸야만 한다.
#include <iostream>
using namespace std;
#define SQR(X) X * X
int main(void) {
int x = 5;
cout << SQR(10) << endl;
cout << SQR(x) << endl;
cout << SQR(x + 3) << endl;
return 0;
}
// 100
// 25
// 23
- 위의 코드에서 매크로 함수
SQR(x + 3)
의 결과는 예상과 많이 다르다. - 그 이유는 매크로 함수의 단순 치환 과정에 있으며, 제대로 된 결과를 얻기 위해서는 매크로 함수의 정의 부분을 다음과 같이 수정해야 한다.
#include <iostream>
using namespace std;
#define SQR(X) ((X) * (X))
int main(void) {
int x = 5;
cout << SQR(10) << endl;
cout << SQR(x) << endl;
cout << SQR(x + 3) << endl;
return 0;
}
// 100
// 25
// 64
- 하지만 C++의 인라인 함수는 단순 치환이 아닌 함수의 모든 코드를 호출된 자리에 인라인 코드로 삽입해 주는 것이다.
- 따라서 일반 함수처럼 값이나 수식을 인수로 전달할 수 있으며, 매개변수 타입에 맞춘 자동 타입 변환도 지원한다.
- 다음의 코드는 앞선 매크로 함수의 코드를 인라인 함수로 작성한 것이다.
#include <iostream>
using namespace std;
inline int Sqr(int x) { return x * x; }
int main(void) {
int x = 5;
cout << "계산 결과는 " << Sqr(10) << "입니다." << endl;
cout << "계산 결과는 " << Sqr(x) << "입니다." << endl;
cout << "계산 결과는 " << Sqr(x + 3) << "입니다." << endl;
return 0;
}
// 계산 결과는 100입니다.
// 계산 결과는 25입니다.
// 계산 결과는 64입니다.