본문 바로가기
ZAION/C++

[C++]Lec03. Function

by 우기37 2024. 4. 25.

 

## 목차

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