함수
하나의 특별한 목적의 작업을 수행하기 위해 독립적으로 설계된 프로그램 코드의 집합
함수의 생성
함수의 정의
- 반환 값이 없는 함수(void 형)와 반환 값이 있는 함수(void를 제외한 데이터형)로 나눌 수 있다.
- 함수의 이름은 되도록 함수의 기능을 나타내도록 설정하는 것이 좋다.
- 아래와 같이 함수를 구현하는 것을 "함수를 정의한다."라고 한다.
// 반환값이 없는 함수
void func(){
/*실행구문*/;
}
// 반환값이 있는 함수
int func(){
/*실행구문*/;
return 0; // return을 이용하여 값을 반환
}
함수 호출
- 함수를 생성하면, 함수의 이름을 적는 것으로 함수를 호출할 수 있다.
- 함수를 호출하면, 해당 함수의 기능을 수행하고 다시 원래 있던 코드로 복귀한다.
- 다음은 간단한 출력 함수를 생성하여 호출한 예제이다.
#include <iostream>
void print_hello() {
std::cout << "Hello World!!" << std::endl;
}
int main() {
print_hello();
return 0;
}
Hello World!!
return의 기능
- return은 함수를 종료하는 기능을 포함한다. 어디에서든 return을 만나게 되면 함수가 종료된다.
- 반환값이 없는 함수를 정의하는 void도 반환값을 따로 적지 않는다면 return을 사용할 수 있다.
#include <iostream>
void print_hello() {
std::cout << "Hello World!!" << std::endl;
return;
std::cout << "Hello World!!" << std::endl;
}
int print_zero() {
std::cout << "0" << std::endl;
return 0;
std::cout << "0" << std::endl;
}
int main() {
print_hello();
print_zero();
return 0;
}
Hello World!!
0
함수 매개변수(인자)
- 프로그램 코드에서 {} 중괄호 내부에서 생성된 변수들은 해당 중괄호에서만 사용할 수 있다. ▶ 지역변수
- 다음의 {} 중괄호 내부의 변수를 밖에서 사용하려는 경우의 예제이다.
Ex) a 출력하기
#include <iostream>
using namespace std;
void print_a() {
cout << a << endl;
}
int main() {
int a = 10;
print_a();
return 0;
}
- main에서 생성한 변수 a를 출력하는 함수를 생성하였으나, 호출하니 오류가 발생하였다.
- 이는 함수는 정확히 a가 무엇인지 알지 못하기 때문에 발생한 문제이다.
- 이 문제를 해결하는 2가지 방법이 있다.
해결법 1) 전역변수화
#include <iostream>
int a = 10; // a를 전역변수로 둔다.
void print_a() {
std::cout << a << std::endl;
}
int main() {
print_a();
return 0;
}
10
- a를 전역변수로 두는 경우 이 문제를 해결할 수 있다.
해결법 2) 매개변수
#include <iostream>
void print_a(int number) {
std::cout << number << std::endl;
}
int main() {
int a = 10;
print_a(a);
return 0;
}
10
- 함수에 number라는 매개변수를 생성하고, main에서 number라는 매개변수에 a의 값을 전달한다.
- 이렇게 하면 함수는 a의 값을 정확히 알기 때문에 함수 호출 시 정상적으로 동작이 이루어진다.
매개변수 = 복사의 개념
- 매개변수로 값을 전달하는 것은 복사의 개념으로, 매개변수로 전달된 값과 함수에서 사용된 값은 서로 다른 메모리 공간을 점유한다. 즉, 두 값은 서로 다른 데이터이다.
- 다음의 예시를 통해 이를 이해해보자.
Ex) 10을 더하는 함수
#include <iostream>
int plus_ten(int number) {
number += 10;
return 0;
}
int main() {
int a = 10;
std::cout << "a(호출 전): " << a << std::endl;
plus_ten(a);
std::cout << "a(호출 후): " << a << std::endl;
return 0;
}
a(호출 전): 10
a(호출 후): 10
- 10을 더하는 함수를 호출하여 실행시켰음에도 a의 값이 전혀 바뀌지 않음을 알 수 있다.
- 함수에서 매개변수를 전달한다는 것은 실제 데이터 자체를 전달하는 것이 아니라, 실제 데이터에 해당하는 값을 전달하여 새로운 메모리 공간에 할당하는것이기 때문이다.
- 그럼 함수를 통해서는 값을 영영 바꿀 수 없는 것인가? ▶ 이때 사용되는 것이 포인터(Pointer)이다. (후술)
Call-by-value / Call-by-reference
- 함수에서 매개변수를 전달하는 방식은 2가지이다.
- 1. Call-by-value (값에 의한 호출) ▶ 값을 복사하여 처리
- 2. Call-by-reference (참조에 의한 호출) ▶ 값의 주소를 참조하여 처리
Call-by-value
- Call-by-value는 매개변수로 받은 값을 복사(새로운 메모리 공간을 할당하여)하여 처리하는 방식이다.
- 바로 위에서 함수를 호출했음에도 값이 변경되지 않은 예제가 이와 같은 처리 방식이다.
Ex) 10을 더하는 함수 (Call-by-value)
#include <iostream>
int plus_ten(int number) {
number += 10;
std::cout << "함수 내에서 a의 값: " << number << std::endl;
std::cout << "함수 내 a의 주소: " << &number << std::endl << std::endl;
return 0;
}
int main() {
int a = 10;
std::cout << "a(호출 전): " << a << std::endl;
std::cout << "main에서 a의 주소: " << &a << std::endl << std::endl;
plus_ten(a);
std::cout << "a(호출 후): " << a << std::endl;
return 0;
}
a(호출 전): 10
main에서 a의 주소: 00B4F74C
함수 내에서 a의 값: 20
함수 내 a의 주소: 00B4F678
a(호출 후): 10
- main에서 선언된 변수 a의 메모리 주소와 함수에서 사용된 변수의 메모리 주소가 상이함을 알 수 있다.
- 이처럼 단순히 값을 인자로 넘기는 경우에는 Call-by-value로 동작하기 때문에 값을 변경할 수 없다.
Call-by-reference
- Call-by-reference는 매개변수로 사용되는 데이터의 주소를 전달하여 처리하는 방식이다.
- Call-by-reference를 사용하기 위해서 데이터의 주소를 가리키는 변수인 포인터를 이용한다. (C와 달리 C++에서는 레퍼런스 변수를 이용하는 방법도 있지만 이는 다음에 따로 작성하겠다.)
Ex) 10을 더하는 함수 (Call-by-reference)
#include <iostream>
int plus_ten(int* number) {
*number += 10;
std::cout << "함수 내에서 a의 값: " << *number << std::endl;
std::cout << "함수 내 a의 주소: " << number << std::endl << std::endl;
return 0;
}
int main() {
int a = 10;
std::cout << "a(호출 전): " << a << std::endl;
std::cout << "main에서 a의 주소: " << &a << std::endl << std::endl;
plus_ten(&a); // 주소를 매개변수로 전달
std::cout << "a(호출 후): " << a << std::endl;
return 0;
}
a(호출 전): 10
main에서 a의 주소: 00EBFD04
함수 내에서 a의 값: 20
함수 내 a의 주소: 00EBFD04
a(호출 후): 20
- 놀랍게도 이번에는 함수 호출 이후 a의 값이 변경되었다.
- 함수의 매개변수를 포인터 변수 int*로 설정하였고, main에서는 a가 아닌 a의 메모리 주소인 &a를 인자로 전달한다.
- 즉, 함수가 직접적으로 변수 a의 메모리 주소를 참조하여 처리하기 때문에 값을 변경할 수 있는 것이다.
Swap 함수
- 변수의 값을 교환하는 함수인 Swap을 Call-by-value, Call-by-reference 2가지 방식으로 구현한다.
Ex) Swap (Call-by-value)
#include <iostream>
int swap(int a, int b) {
int temp = a;
a = b;
b = temp;
return 0;
}
int main() {
int i = 5, j = 7;
std::cout << "swap 호출 전 >> i = " << i << ", j = " << j << std::endl;
// swap 호출
swap(i, j);
std::cout << "swap 호출 후 >> i = " << i << ", j = " << j << std::endl;
return 0;
}
swap 호출 전 >> i = 5, j = 7
swap 호출 후 >> i = 5, j = 7
- 역시나 Call-by-value 방식으로는 변수의 값을 교환할 수 없었다.
Ex) Swap (Call-by-reference)
#include <iostream>
int swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
return 0;
}
int main() {
int i = 5, j = 7;
std::cout << "swap 호출 전 >> i = " << i << ", j = " << j << std::endl;
// swap 호출
swap(&i, &j);
std::cout << "swap 호출 후 >> i = " << i << ", j = " << j << std::endl;
return 0;
}
swap 호출 전 >> i = 5, j = 7
swap 호출 후 >> i = 7, j = 5
- Call-by-reference 방식으로 swap을 구현하였을 때는 정상적으로 값이 교환되었음을 알 수 있다.
프로토타입(prototype)
- 프로토타입 == 함수의 원형
- 프로토타입이란 함수의 정의(함수의 구현)를 하기 전에 함수에 대한 원형을 main 함수 앞에 선언하는 것을 말하며 다음과 같이 구현한다.
// main 함수 앞에 프로토타입(함수 원형) 선언
int func(int a); // 꼭 ;(세미콜론)을 적어야 함. 변수명은 꼭 적지 않아도 됨.(int만 적어도됨)
// main 함수
int main(){
}
// 함수의 정의
int func(int a){
}
- 99% 이상의 프로그래머는 main 앞에 함수의 원형을 선언하고, main 뒤에 함수를 정의하는 방식을 이용한다. ▶ 프로그램의 코드를 작성할 때 수많은 함수를 구현할 것이기 때문에, 함수를 계속 main 앞에서 정의하게 되면 main 함수가 계속해서 뒤로 밀리게 될 것이다. 그렇기 때문에 main 앞에 함수의 원형을 선언하고, 뒤에서 함수를 구현함으로써 깔끔하게 코드를 작성할 수 있다.
- C에서는 main 함수 뒤에 함수를 정의하더라도 프로그램 실행이 가능했다. 그러나 C++에서는 main 뒤에 함수를 정의하고, 함수의 원형을 선언하지 않으면 오류가 발생하여 실행이 불가능하다.
Ex) main 뒤에 함수를 정의하는 경우 (함수 원형 선언 X)
#include <iostream>
int main() {
int i = 5;
print_i(i);
return 0;
}
int print_i(int num) {
std::cout << "i의 값: " << num;
return 0;
}
- 오류가 발생하여 프로그램이 실행되지 않는다.
- 이러한 문제가 발생하지 않도록 main 앞에 함수의 원형을 선언하는 부분, 즉 프로토타입을 적어주어야 한다. ▶ 컴파일러가 이를 통해 함수가 있음을 확인하며, 동시에 매개변수가 제대로 설정되어 있는지 등을 확인하여 오류를 검토한다.
Ex) main 뒤에 함수를 정의하는 경우 (함수 원형 선언 O)
#include <iostream>
int print_i(int num);
int main() {
int i = 5;
print_i(i);
return 0;
}
int print_i(int num) {
std::cout << "i의 값: " << num;
return 0;
}
i의 값: 5
- 프로토타입을 선언함으로써 오류없이 프로그램이 동작하였다.
배열을 매개변수로 전달하기
- 배열 또한 함수의 인자(매개변수)로 받아서 사용할 수 있다.
- 배열을 매개변수로 넘기는 것은 기본 동작이 Call-by-reference이다. ▶ 기본적으로 배열은 그 자체로 포인터이기 때문에 배열을 매개변수로 넘기는 것은 포인터, 즉 데이터의 주소를 넘기는 것과 같음.
- Call-by-reference로 배열을 넘기기 싫다면, vector을 이용하여 배열을 생성하고 매개변수로 전달하면 된다.
배열 매개변수를 Call-by-reference로 넘기기
- 배열을 매개변수로 넘기는 표현 방식은 2개가 있다.
1) [ ] 연산자 이용하기
2) * 연산자 이용하기
Ex 1) [ ] 연산자 이용하여 매개변수 전달
#include <iostream>
int print_array(int arr[]);
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };
std::cout << "함수 호출 전 arr 출력" << std::endl;
for (int i = 0; i < 5; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl << std::endl;
print_array(arr);
std::cout << "함수 호출 후 arr 출력" << std::endl;
for (int i = 0; i < 5; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
return 0;
}
int print_array(int arr[]) {
for (int i = 0; i < 5; i++) {
arr[i] += 10;
}
std::cout << "함수 내에서 arr 출력" << std::endl;
for (int i = 0; i < 5; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl << std::endl;
return 0;
}
함수 호출 전 arr 출력
1 2 3 4 5
함수 내에서 arr 출력
11 12 13 14 15
함수 호출 후 arr 출력
11 12 13 14 15
- Call-by-reference에 의해 arr의 값이 변경되었음을 알 수 있다.
Ex 2) * 연산자 이용하여 매개변수 전달
#include <iostream>
int print_array(int* parr);
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };
std::cout << "함수 호출 전 arr 출력" << std::endl;
for (int i = 0; i < 5; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl << std::endl;
print_array(arr); // 혹은 &arr[0]
std::cout << "함수 호출 후 arr 출력" << std::endl;
for (int i = 0; i < 5; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
return 0;
}
int print_array(int* parr) {
for (int i = 0; i < 5; i++) {
*(parr + i) += 10;
}
std::cout << "함수 내에서 arr 출력" << std::endl;
for (int i = 0; i < 5; i++) {
std::cout << *(parr + i) << " ";
}
std::cout << std::endl << std::endl;
return 0;
}
함수 호출 전 arr 출력
1 2 3 4 5
함수 내에서 arr 출력
11 12 13 14 15
함수 호출 후 arr 출력
11 12 13 14 15
- Ex 1과 동일한 동작이 이루어졌다.
배열 포인터를 이용하여 2차원 배열을 매개변수로 넘기기
- 2차원 배열 또한 당연하게도 매개변수로 넘길 수 있다. 이 또한 2가지 방식으로 이루어진다.
1) [ ] 연산자 이용하기
2) 배열 포인터 이용하기
Ex 1) [ ] 연산자 이용하여 매개변수 전달
#include <iostream>
int print_array(int arr[][3]);
int main() {
int arr[2][3] = { {1, 2, 3}, {4, 5, 6} };
std::cout << "함수 호출 전 arr 출력" << std::endl;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
std::cout << arr[i][j] << " ";
}
std::cout << std::endl;
}
std::cout << std::endl << std::endl;
print_array(arr);
std::cout << "함수 호출 후 arr 출력" << std::endl;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
std::cout << arr[i][j] << " ";
}
std::cout << std::endl;
}
std::cout << std::endl;
return 0;
}
int print_array(int arr[][3]) { // 2차원 배열에서 열의 개수는 정확히 명시해야 함.
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
arr[i][j] += 10;
}
}
std::cout << "함수 내에서 arr 출력" << std::endl;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
std::cout << arr[i][j] << " ";
}
std::cout << std::endl;
}
std::cout << std::endl << std::endl;
return 0;
}
함수 호출 전 arr 출력
1 2 3
4 5 6
함수 내에서 arr 출력
11 12 13
14 15 16
함수 호출 후 arr 출력
11 12 13
14 15 16
- Call-by-reference에 의해 2차원 배열 arr의 값이 변경되었다.
Ex 2) 배열 포인터 이용하여 매개변수 전달
#include <iostream>
int print_array(int(*parr)[3]); // 배열 포인터
int main() {
int arr[2][3] = { {1, 2, 3}, {4, 5, 6} };
std::cout << "함수 호출 전 arr 출력" << std::endl;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
std::cout << arr[i][j] << " ";
}
std::cout << std::endl;
}
std::cout << std::endl << std::endl;
print_array(arr);
std::cout << "함수 호출 후 arr 출력" << std::endl;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
std::cout << arr[i][j] << " ";
}
std::cout << std::endl;
}
std::cout << std::endl;
return 0;
}
int print_array(int(*parr)[3]) { // 배열 포인터 역시 열의 개수를 명시해야 한다.
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
parr[i][j] += 10;
}
}
std::cout << "함수 내에서 arr 출력" << std::endl;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
std::cout << parr[i][j] << " ";
}
std::cout << std::endl;
}
std::cout << std::endl << std::endl;
return 0;
}
함수 호출 전 arr 출력
1 2 3
4 5 6
함수 내에서 arr 출력
11 12 13
14 15 16
함수 호출 후 arr 출력
11 12 13
14 15 16
- 배열 포인터를 이용하여 2차원 배열을 매개변수로 전달할 수 있다.
상수 매개변수
- 인자로 넘기는 변수를 상수처리하여 전달할 수 있다.
- 당연하게도 const 키워드를 사용한 매개변수를 변경하려고 하면 오류가 발생한다.
#include <iostream>
int plus_a(const int* num);
int main() {
int a = 10;
std::cout << "a(호출 전): " << a << std::endl;
plus_a(&a);
std::cout << "a(호출 후): " << a << std::endl;
return 0;
}
int plus_a(const int* num) {
*num += 10; // 값을 변경하려고 하면 오류가 발생
return 0;
}
함수 포인터
- 함수가 저장되어 있는 메모리의 시작주소를 가리키는 포인터
- 함수 또한 코드 자체가 메모리 상에 저장되어 있다.
- 배열과 마찬가지로, 함수의 이름은 함수의 시작 주소값을 나타낸다.
- 함수 포인터는 다음과 같이 정의한다.
// (함수 리턴형) (*포인터명)(인자 타입1, 인자 타입2, ...);
// 인자가 없는 함수라면 int (*pfunc)(); 처럼 공백으로 두면 됨
int (*pfunc)(int);
- 함수의 return 값은 int이므로, int 형 포인터를 선언하고 매개변수에 맞게 데이터형을 가리킨다.
Ex) 단순한 함수 포인터
#include <iostream>
int plus_a(int* num);
int main() {
int a = 10;
int (*pfunc)(int*); // 함수 포인터
pfunc = plus_a; // 함수의 이름은 함수의 시작주소를 나타냄
std::cout << "a(호출 전): " << a << std::endl;
pfunc(&a);
std::cout << "a(호출 후): " << a << std::endl;
return 0;
}
int plus_a(int* num) {
*num += 10;
return 0;
}
a(호출 전): 10
a(호출 후): 20
Ex) 다차원 배열을 인자로 갖는 함수에 대한 함수 포인터
- 다차원 배열을 인자로 갖는 함수의 함수 포인터는 다음과 같이 인자의 이름만 빼버리면 된다.
#include <iostream>
int plus_array(int(*parr)[3]);
int main() {
int arr[2][3] = { {1, 2, 3}, {4, 5, 6} };
int(*pfunc)(int(*)[3]); // 함수 포인터 정의
pfunc = plus_array;
std::cout << "함수 호출 전 arr 출력" << std::endl;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
std::cout << arr[i][j] << " ";
}
std::cout << std::endl;
}
std::cout << std::endl << std::endl;
pfunc(arr);
std::cout << "함수 호출 후 arr 출력" << std::endl;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
std::cout << arr[i][j] << " ";
}
std::cout << std::endl;
}
std::cout << std::endl;
return 0;
}
int plus_array(int(*parr)[3]) {
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
parr[i][j] += 10;
}
}
return 0;
}
함수 호출 전 arr 출력
1 2 3
4 5 6
함수 호출 후 arr 출력
11 12 13
14 15 16
main 함수
- 당연하게도, main 또한 함수이다.
- main은 프로그램의 시작점이며, 컴퓨터는 프로그램을 실행할 때 가장 먼저 main을 확인하게 된다.
- main은 프로그램 내에서 단 1개 존재하며, 무조건 1개는 존재하여야 한다.
- main 함수의 반환값은 컴퓨터의 운영체제가 받으며, 정상 종료시 0을, 비정상 종료시 1을 반환하는 것으로 규정되어 있다.