## 목차
1. Function 정의
2. Prototype
3. parameter and pass-by-value
4. return 문
5. default argument
6. overloading
7. 함수 호출의 동작 방식
8. 포인터와 참조자
9. inline 함수
10. recursive(재귀함수)
#1 Function 정의
1 - 1) Functions
C++ 프로그램의 함수
- C++ 표준 라이브러리(함수와 클래스)
- 써드 파티 라이브러리(함수와 클래스) : 비공식 라이브러리이며, 외부에서 개발된 라이브러리
- 직접 구현한 함수와 클래스
추상적 흐름(함수 -> 모듈화 -> 재사용성)
- 코드를 독립적인 연산으로 분할
- 연산들을 재사용
가독성 비교
모듈화 전
int main() {
// read input
statement1;
statement2;
// process input
statement3;
statement4;
// write output
statement5;
statement6;
return 0;
}
모듈화
int main() {
readInput();
processInput();
writeOutput();
return 0;
}
함수를 사용할 때 알아야할 것으로 함수의 기능, 함수에서 필요로 하는 정보, 함수가 리턴하는 것, 어떤 오류가 발생하는지, 성능상의 제약에 대한 이해가 필요하다.
함수가 내부적으로 어떻게 동작하는지는 몰라도 된다.
수학적 연산 함수 예)
cout << sqrt(400.0) << endl; // 20
double result;
result = pow(2.0, 3.0); // 2^3
1 - 2) Function Definition
함수의 정의에 필요한 요소 :
1.이름 : 함수의 이름, 변수의 명명 규칙과 동일, 의미가 있는 함수명이어야 한다.
2. 매개변수 리스트 : 함수에 전달되는 값(인자)들, 타입이 명시되어야 한다.
3. 리턴 타입 : 연산 결과의 반환 타입
4. 본문 body : 함수가 호출되었을 때 실행되는 명령문, 대괄호 "{}" 내부
함수 정의
returnType FunctionName(parameters) {
statements;
return 0;
}
함수 호출
void PrintHello() {
cout << "Hello" << endl;
}
int main() {
for (int i = 0; i < 10; i++) {
PrintHello();
}
return 0;
}
컴파일러는 함수의 호출 이전에 함수의 정의를 알아야 한다.
그래서 아래와 같이 main() 아래에 함수를 정의하면 Error가 발생한다.
int main() {
SayHello();
return 0;
}
void SayHello() {
cout << "Hello" << endl;
}
#2 Prototype
2 - 1) Function Prototypes
함수의 호출 이전에 함수의 정의를 알 수 있어야 한다. 매개변수가 몇 개고, 어떤 타입의 데이터를 리턴하는지를 알려주어야 한다.
해결방법 1 : 항상 함수의 호출보다 윗 라인에 함수를 정의한다. 작은 프로그램에서는 문제가 없을 수 있지만, 일반적으로 효율적인 방법은 아니다.
해결방법 2 : 함수 프로토타입의 사용
- 함수의 전체 정의가 아닌 컴파일러가 알아야 할 부분만을 미리 알려주는 개념이다.
- 전방 선언(forward declaration)이라고도 명칭한다.
- 프로그램의 초기에 위치한다.
- 헤더 파일(.h)의 활용
int FunctionName(int, string); //prototype
int main() {
FunctionName(1, "JHW"); // Call(use)
}
int FunctionName(int a, string b) { // Definition
statements;
return 0;
}
2 - 2) 분산된 코드와 함수, Compile과 Linking
- 두 개의 cpp 파일에 모두 다 iostream이 필요한지
- 각 파일이 compile이 정상적으로 이루어지는 이유는
- 만일 Log.cpp파일에 Log 함수가 없다면 어떤 오류가 발생하는지
main.cpp
#include <iostream>
using namespace std;
void Log(const char* message);
int main() {
Log("Hello World!");
}
Log.cpp
#include <iostream>
using namespace std;
void Log(const char* message) {
cout << message << endl;
}
#3 Parameter and Pass-By-Value
3 - 1) Function Parameters
함수 매개변수
- 함수를 호출할 때, 데이터를 전달할 수 있다.
- 함수의 호출에 있어서 전달하는 값은 인수(argumnet)라 한다.
- 함수의 정의에 있어서 전달하는 값은 인자 또는 매개변수(parameter)라 한다.
- 인수와 매개변수는 개수, 순서와 타입이 일치해야 한다.
#include <iostream>
using namespace std;
int AddNumbers(int, int); // prototype
int main() {
int result = 0;
result = AddNumbers(100, 200); // call(use) - 인수(argument)
return 0;
}
int AddNumbers(int first, int second) { // definition - 인자(parameter)
return first + second;
}
3 - 2) Pass-By-Value
함수에 데이터를 전달할 때는 값으로 전달(pass-by-value)된다.
- 데이터의 값이 복사되어 전달된다.
- 함수 내에서는 원본에서 복사해서 만들어진 사본이 사용된다.
- 전달된 인수는 함수를 통해 변화되지 않는다.
- 사본을 바꾼다고 원본이 바뀌지 않는다.
- 실수로 값을 변화하는 것을 방지한다.
- 원본을 변화시키는 것이 필요하거나, 복사 비용이 높을 때를 위한 방법 = (포인터/참조자)
#include <iostream>
void changeValue(int x) {
x = 10;
}
int main() {
int num = 5;
std::cout << "Before function call: " << num << std::endl;
changeValue(num);
std::cout << "After function call: " << num << std::endl;
return 0;
}
#4 Return 문
반환(return)
- return 문을 통해서 함수의 결과값을 전달한다.
- void 형 반환인 경우 return문을 생략 가능하다.
- return 문은 함수 내 어느 곳에서나 정의가 가능하나 일반적으로 가독성을 위해 본문 젤 마지막에 사용한다.
- return 문을 통해 함수는 즉각적으로 종료한다.
#5 Default Argument
기본 인수
- 함수의 선언에서 정의한 모든 매개변수가 전달되어야 한다.
- 기본 인수를 사용하면 인수가 주어지지 않을 시 기본값을 사용하도록 정의 가능하다.
- 동일한 값을 자주 사용할 경우
- 기본값은 함수 프로토타입 또는 정의부에 선언한다.
- 프로토타입에 선언하는 것이 기본적이다.
- 둘 다 선언해서는 안된다.
- 여러 개의 기본값을 사용할 경우 오른쪽부터 선언해야 한다.
#include <iostream>
double CalcCost(double baseCost, double taxRate = 0.06, double shipping = 3.5);
double CalcCost(double baseCost, double taxRate, double shipping) {
return baseCost += (baseCost * taxRate) + shipping;
}
int main() {
double cost = 0;
cost = CalcCost(100.0, 0.08, 4.5);
cost = CalcCost(100.0, 0.08);
cost = CalcCost(200.0);
return 0;
}
#6 Overloading
함수 오버로딩
- 서로 다른 매개변수 리스트를 갖는 동일한 이름의 함수를 정의하는 것
- 추상화의 한 예이다.
- 다형성의 한 예이다. 유사한 개념의 함수를 다른 타입에 대해 정의한다.
- 객체지향 프로그램 구현을 위한 중요한 기법 중 하나이다.
- 컴파일러는 주어진 인수와 함수들의 파라미터 정의를 기반으로 개별적인 함수를 구분할 수 있어야 한다.
#include <iostream>
using namespace std;
int AddNumber(int, int);
double AddNumber(double, double);
int main() {
cout << AddNumber(10, 20) << endl;
cout << AddNumber(10.5, 20.5) << endl;
return 0;
}
int AddNumber(int first, int second) {
return first + second;
}
double AddNumber(double first, double second) {
return first + second;
}
- 반환 타입만 다른 오버로딩은 불가능하다
int GetValue();
double GetValue();
...
cout << GetValue() << endl; // int? double?
#7 함수 호출의 동작 방식
7 - 1) Local/Global Scope
지역 범위
- 블록 {} 내의 범위
- 함수의 매개변수까지 함수 범위 내의 지역 변수로 생각해야 한다.
- for 문에서 int i = 0;이 블록 내 범위인 것과 마찬가지이다.
- 따라서 함수의(복사된) 인자 및 지역 변수들은 함수의 실행 중에만 존재한다.
static 지역 변수
- static 한정어를 사용해 지역 내에 정의된 변수를 지역 밖에 정의한 것처럼 활용이 가능하다. 단, scope 밖에서 접근할 수는 없다.
- 초기화가 필요하다.
void StaticLocalIncrement() {
static int num = 1;
cout << "num : " << num << endl;
num++;
cout << "num : " << num << endl;
}
int main() {
StaticLocalIncrement(); // 1 2
StaticLocalIncrement(); // 2 3
StaticLocalIncrement(); // 3 4
}
전역 범위
- 함수 밖에 정의된 변수는 어디서나 접근이 가능하다.
- 전역 변수는 사용하지 않는 것이 좋다.(전역 상수는 가능)
7 - 2) Function Calls
메모리 레이아웃
함수 호출의 동작방식
- Function call stack
- LIFO(Last In First Out)
- Stack Frame(Activation Record)
- 함수의 호출이 발생할 때마다 일종의 구분선이 정의된다.
- 함수의 지역 변수와 매개변수는 그 구분선 영역 내에 생성된다.
- 함수의 호출이 끝나면 구분선 내의 메모리는 자동으로 해제된다.
- Stack은 유한하고 작아서, stack overflow(프로그램이 사용가능한 스택 메모리를 넘어선 상태에서 계속해서 데이터를 쌓아 발생하는 에러)가 발생할 수 있다.
#include <iostream>
using namespace std;
int Func2(int x, int y, int z) {
x += y + z;
return x;
}
int Func1(int a, int b) {
int result;
result = a + b;
result = Func2(result, a, b);
return result;
}
int main() {
int x = 10;
int y = 20;
int z;
z = Func1(x, y);
cout << z << endl;
return 0;
}
Check Function Calls in VS
#8 포인터와 참조자
범위 밖 메모리의 조작
- 위의 코드에서 Func2()를 실행 중일 때, main() 및 Func1()의 지역 변수들에 대한 접근은 불가능하지 않다. 하지만 이런 접근이 가능하려면 다른타입의 변수를 사용해야 한다.
- 1. 포인터 : 메모리 주소값을 갖는 변수
- 2. 참조자 : 변수에 또다른 이름(별명)을 부여
Pass-By-Address
포인터를 함수로 전달하는 예시
- 주소값을 명시하는 포인터를 활용하여 범위 밖 메모리에 접근할 수 있다.
*number는 main()의 지역변수이지만, ChangeValue() 함수를 통해 값이 바뀌었다. 즉, ChangeValue 함수는 자신의 범위 밖에 변수에 접근한 것이다.
#include <iostream>
using namespace std;
void ChangeValue(int* number);
int main() {
int number = 10;
cout << number << endl;
ChangeValue(&number);
cout << number << endl;
}
void ChangeValue(int* number) {
*number = 20;
}
Pass-By-Reference
- 함수 내에서 범위 밖 변수값을 바꾸고 싶은 경우 사용하는 또다른 방법
- 값의 변환을 위해서는 매개변수의 주소값(포인터)가 필요하다.
- 배열이 아닌 경우에도 C++에서는 참조자(reference)를 통해 가능하다.
- C언어를 사용한다면 포인터를 사용할 수밖에 없다.
- C++에서는 포인터 또는 참조자 두 가지 옵션이 존재한다.
- 형식 매개변수를 실제 매개변수의 별명처럼 사용하는 개념이다.
참조자
- & 기호 사용
- 기존 변수에 대한 별칭을 만들어내므로 변수에 직접적인 접근이 가능하다.
- 참조자는 반드시 선언과 동시에 초기화되어야 한다. 이후에 참조 대상을 변경 할 수 없다.
- 포인터와의 차이는 참조자는 한 번 참조된 후에는 다른 변수를 참조할 수 없지만, 포인터는 다른 변수를 참조할 수 있다.
#include <iostream>
using namespace std;
void ScaleNumber(int& num);
int main() {
int number = 1000;
ScaleNumber(number);
cout << number << endl;
return 0;
}
void ScaleNumber(int& num) {
if (num > 100) {
num = 100;
}
}
- swap 예제
#include <iostream>
using namespace std;
void Swap(int& a, int& b);
int main() {
int x = 10, y = 20;
cout << x << " " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
void Swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
- 참조자를 사용하지 않는 경우에는 print를 위해 메모리 두 배 사용한다.
- 값에 의한 전달을 사용할 경우 복사본을 사용하기에 메모리 사용량을 증가 시키기 때문이다.
- 참조자를 사용하되, 값의 변경이 필요 없을 시에는 const로 안정성 확보
#include <iostream>
#include <vector>
using namespace std;
void print(const vector<int>& v);
int main() {
vector<int> data{ 1, 2, 3, 4, 5 };
print(data);
return 0;
}
void print(const vector<int>& v) {
for (auto num : v) {
cout << num << endl;
}
}
#9 inline 함수
- 함수의 호출에는 어느정도 오버헤드가 존재한다.
- Activation stack 생성, 파라미터 처리, pop stack 리턴값 처리 등..
- 함수를 inline으로 정의하면 컴파일 단계에서 함수내의 명령문으로 함수 호출이 대체된다.
- 일반적인 함수 호출보다 빠르다. 단, 바이너리 파일의 용량이 커질 수 있다. 그리고 내가 명시하지 않아도 컴파일러에서 최적화를 위해 내부적으로 알아서 처리하기도 한다.
#include <iostream>
using namespace std;
inline int AddNumbers(int first, int second) {
return first + second;
}
int main() {
int result = 0;
result = AddNumbers(100, 200);
return 0;
}
#10 Recursive(재귀 함수)
- 스스로를 호출하는 함수
- Factorial
- 재귀 호출을 끝내는 base case가 반드시 실행되어야 한다.(stack overflow 주의!)
#include <iostream>
using namespace std;
unsigned long long Factorial(unsigned long long n) {
if (n == 0) {
return 1;
}
return n * Factorial(n - 1);
}
int main() {
cout << Factorial(5) << endl;
return 0;
}
※본 블로그는 학습을 하며 제가 이해한 내용을 바탕으로 작성되어 실제 정의와 다를 수 있습니다.
참고문헌
GitHub - diskhkme/cpp_lecture_material: C++ 프로그래밍 강의 자료
https://www.aladin.co.kr/shop/wproduct.aspx?ISBN=K842939734&start=pnaver_02
'ZAION > C++' 카테고리의 다른 글
[C++]Lec04-02. Pointer & Reference (2) | 2024.04.30 |
---|---|
[C++]Lec04-01. Pointer & Reference (0) | 2024.04.26 |
[C++]Lec02. Debugging (0) | 2024.04.24 |
[C++]Lec02. Basic Syntax (0) | 2024.04.23 |
[C++]Lec01. Variable and Constant (1) | 2024.04.23 |