Ch 09. 함수
함수의 기본
반환타입 함수이름(인자)
{
함수몸체
return 반환값;
}
메인 함수
int main(int argc, char* argv[])
{ return 0; }
//argc는 아규먼트의 개수, argv는 문자열 배열인 아규먼트를 의미한다.
//위 변수들이 실행 시 넘겨주는 인자를 받게 된다.
return 은 반환뿐만 아니라 함수의 종료도 의미한다.
컴파일러는 위에서 아래로 읽기 때문에 함수를 사용하려면 사용한 시점보다 위에 함수가 정의되어 있어야 한다. 이를 해결하기 위해 함수의 선언 (prototype)을 앞쪽에 선언한다. (전방선언)
int csun(int a, int b); // 프로토 타입 전방선언
int cabs(int); // 프로토 타입의 변수명은 생략이 가능하다.
int csumabs(int a, int b)
{
return cabs(a) + cabs(b);
}
선언은 함수의 헤드부분만 있는 것이고, 정의는 함수의 몸체 부분까지 있는 것이다. 물론 정의가 사용한 시점 위에 있다면 전방선언은 하지 않아도 된다.
재귀 함수
자신 스스로를 호출하는 함수를 재귀함수라고 한다.
void count(int n)
{
if (n < 0)
return;
cout << n << endl;
count(n - 1); //자기 자신을 호출
}
int main() {
count(10);
return 0;
}
점화식을 나타내는데 유리하다. 팩토리얼 예시
int fac(int n)
{
if(n == 1)
return 1;
return n * fac(n - 1);
}
트리 순회 예시
struct Node {
int value;
Node* left;
Node* right;
};
void visit(Node* node) {
if (node == nullptr) {
return;
}
cout << node->value << endl;
visit(node->left);
visit(node->right);
}
재귀함수의 단점 : 함수 호출 비용이 커지면 스택 오버플로우가 날 수 있다. 트레이드오프, 꼬리 재귀 최적화, 등을 생각하여 최적화할 수 있다.
값으로 전달
//Pass by Value
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10;
int y = 20;
cout << "Before Swapping: " << x << " " << y << endl;
swap(x, y);
cout << "After Swapping: " << x << " " << y << endl;
return 0;
}
함수에 값으로 전달하게 되면 복사본을 가지고 연산을 하기 때문에 실제 변수에 변화가 일어나지 않는다. 그 예시를 코드로 표현하면 이렇다.
int main() {
int x = 10;
int y = 20;
cout << "Before Swapping: " << x << " " << y << endl;
//swap(x, y);
int a = x;
int b = y;
int temp = a;
a = b;
b = temp; //x, y의 값은 변하지 않음
cout << "After Swapping: " << x << " " << y << endl;
return 0;
}
값을 리턴하여 할당하는 방식으로 일부 해결할 수 있다.
문제점 : 굉장히 큰 자료형의 경우 복사가 일어나서 큰 오버헤드가 있을 수 있다.
(오버헤드 : 어떤 처리를 위한 비용)
중요한 점 : 값으로 전달했을 경우에는 “전달한 인자에 변화가 없을것” 이라고 알 수 있다.
주소로 전달
포인터를 이용해서 전달하면 함수내에서 외부의 변숫값을 변경할 수 있다.
//pass by adress
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int a = 5;
int b = 10;
swap(&a, &b);
cout << a << " " << b << endl;
return 0;
}
주소로 전달의 예시는 scanf가 있다.
scanf("%d", &num); //num의 주소를 전달하고 있다.
pass by adress 도 일종의 pass by value라고 볼 수 있다.
주소값이 복사되어 전달되는 것이기 때문이다.
직접 값으로 전달하게 되면 인자의 크기가 클 경우 큰 비용이 발생하는데 이를 포인터로 대체해 줄 수 있다. 하지만 포인터로 대체할 경우 값의 변경 우려가 있기 때문에 const를 붙여주게 된다.
void print(const int* num) {
cout << *num << endl;
} // 포인터로 전달 받지만 값이 변경 될 수 없다.
배열을 인자로 전달하게되면 배열의 size정보를 잃어버리고(decay) 포인터로 전달되게 된다.
2차원 배열을 전달할 경우 char [i][j]의 경우 i의 정보만 decay 된다.
void print(char(*strs)[5], int n) { //크기가 5인 char배열을 가르키는 포인터
for (int i = 0; i < n; i++) {
cout << strs[i] << endl;
}
}
int main() {
char strs[][5] = { "abcd", "efgh", "ijkl", "mnop" };
print(strs, 4);
return 0;
}
참조로 전달
//pass by reference
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int a = 10, b = 20;
cout << "Before swap: " << a << " " << b << endl;
swap(a, b);
cout << "After swap: " << a << " " << b << endl;
return 0;
}
참조로 전달을 이용하게 되면 코드가 직관적이고 간결해진다. 또한 사이즈가 큰 자료형을 넘겨줄 때 복사가 일어나지 않는다. 하지만 이대로 사용하면 값이 변경이 되었는지 알 수 없기 때문에 변경되지 않는 경우 이를 명확히 하기 위해 const를 붙인다.
void print(const int& x) {
cout << x << endl;
}
또한 참조로 전달은 값으로 전달되었을 때의 호환성을 위해서 리터럴 상수도 인자로 받을 수 있다.
int num = 10;
float num2 = 10.1f;
print(num);
print(num2);
print(20);
참조로 반환을 하는 경우 반환값을 이용해서 값을 변경할 수 있다.
int& func(int& num) {
return num;
}
int main() {
int a = 10;
func(a) = 20;
cout << a << endl;
return 0;
}
디폴트 매개변수
매개변수에 디폴트 값을 설정해서 값을 받지 않았을 때 어떤 값을 사용할지 정의할 수 있다.
//defalut parameter
int pow(int num, int exp = 2) { //exp 가 주어지지 않으면 제곱
int result = 1;
for (int i = 0; i < exp; i++) {
result *= num;
}
return result;
}
int main() {
cout << pow(2, 3) << endl;
cout << pow(2) << endl;
return 0;
}
또한 디폴트 값을 정의가 아니라 선언부에 넣어 줄 수 도 있다.
//defalut parameter
int pow(int, int = 2); //선언부에서 디폴트값을 정의하고 있다.
int main() {
cout << pow(2, 3) << endl;
cout << pow(2) << endl;
return 0;
}
int pow(int num, int exp) {
int result = 1;
for (int i = 0; i < exp; i++) {
result *= num;
}
return result;
}
널 포인터를 디폴트값으로 주어 예외처리하기
void print(Person* p = nullptr){
if(p)
cout << p -> height << endl;
else
cout << "invalid" << endl;
}
함수 오버로딩
//function overloading
int countDigit(int n) {
if (!n) return 1;
int count = 0;
while (n)
{
n /= 10;
count++;
}
return count;
}
int countDigit(string s) { //오버로딩
return countDigit(stoi(s));
}
int main() {
cout << countDigit(12345) << endl;
cout << countDigit("12345") << endl;
return 0;
}
이름이 같은데 파라미터의 타입이나 개수가 다르다면 동시에 정의할 수 있다. 이것이 함수 오버로딩이다. 주의할 점은 리턴타입만으로는 호출 시 함수를 구분 지을 수 없어서 오버로딩이 되지 않는다. 또한 디폴트 파라미터의 유무로는 오버로딩이 되지 않는다. 함수 오버로딩은 정적 다형성이다.
함수 포인터
함수를 가리키는 포인터이다. 반환형 (*포인터이름)(시그니쳐)로 선언한다.
int sum(int a, int b) {
return a + b;
}
int main() {
int (*p)(int, int) = sum; //int (*p)(int, int) = ∑ 도 가능
cout << p(1, 2) << endl; //cout << (*p)(1, 2) << endl; 도 가능
return 0;
}
함수 레퍼런스는 *을 &로 바꾸면 된다. 레퍼런스기 때문에 선언과 동시에 초기화해야 한다.
int sum(int a, int b) {
return a + b;
}
int main() {
int (&p)(int, int) = sum; //int (&p)(int, int) = ∑ 는 불가능
cout << p(1, 2) << endl; //cout << (&p)(1, 2) << endl; 도 가능
//cout << (*p)(1, 2) << endl; 도 가능
return 0;
}
원래의 함수도 역참조하여 사용할 수 있다.
cout << (*sum)(1, 2) << endl; // 함수의 이름은 필요하다면 주소값을 리턴한다.
콜백, 함수포인터 배열 예시
enum struct RequestType
{
Login, Register, Update, Delete
};
bool onLogin(string id, string password)
{
cout << "onLogin" << endl;
cout << id << endl;
cout << password << endl;
return true;
}
bool onRegister(string id, string password)
{
cout << "onRegister" << endl;
cout << id << endl;
cout << password << endl;
return true;
}
bool onUpdate(string id, string password)
{
cout << "onUpdate" << endl;
cout << id << endl;
cout << password << endl;
return true;
}
bool onDelete(string id, string password)
{
cout << "onDelete" << endl;
cout << id << endl;
cout << password << endl;
return true;
}
void callback()
{
bool (*callbacks[])(string, string) {
onLogin, onRegister, onUpdate, onDelete
};
callbacks[(int)RequestType::Login]("david", "1234");
callbacks[(int)RequestType::Register]("david", "1234");
callbacks[(int)RequestType::Update]("david", "1234");
callbacks[(int)RequestType::Delete]("david", "1234");
}
게임의 예시
struct Character
{
int health;
void (*dieCallback)();
};
void damaged(Character& character)
{
character.health -= 100;
if (character.health <= 0)
{
cout << "died" << endl;
if (character.dieCallback)
character.dieCallback();
}
}
void gameOver()
{
cout << "gameOver" << endl;
}
void main()
{
Character character0{ 200, nullptr }; //몬스터
Character character1{ 200, gameOver }; //플레이어
damaged(character0); //죽어도 게임오버되지 않음
damaged(character0);
damaged(character1); // 죽으면 게임오버
damaged(character1);
}
auto를 사용하면 복잡합 선언을 간단하게 할 수 있다.
void (*p)() = gameOver;
auto p = gameOver; //둘은 같은 의미이다.
auto p = &gameOver; //이것도 가능
auto& r = gameOver; //레퍼런스 형태
auto는 타입을 컴파일 타임에 알아서 맞춰준다.
std::fuction을 이용한 시그니쳐 표현
#include <functional>
int sum(int a, int b)
{
return a + b;
}
void main()
{
std::function<int(int, int)> f = sum; //시그니쳐
}
typedef를 이용할 수 도 있다.
typedef 타입 별칭;
typedef float real32;
복잡한 함수 포인터 타입명
typedef 반환형 (*별칭)(시그니쳐);
typedef void (*FuncType)(int); //사용 : FuncType f = 함수이름;
변형
typedef void (FuncType)(int); //사용 : FuncType *f = 함수이름;
using을 이용한 타입 정의
using real32 = float;
using real64 = double;
using FuncType = void(*)(int); //함수 포인터 타입
호출 규약
cdec 호출규약이란 C Declaration 호출규약의 약어이다. 함수 호출 시 스택을 관리하고 인수를 전달하고, 반환값을 처리하는 방식을 정의한다.
ESP (Stack Pointer) - 스택 포인터
ESP는 현재 스택의 위치를 가리키는 레지스터로, 함수 호출 시 인수와 지역 변수를 저장하는 데 사용되는 스택의 상단을 가리킵니다.
역할:
- 스택의 최상단 위치를 가리킵니다.
- 함수 호출 시 인수와 지역 변수를 관리합니다.
- 스택 프레임 사이의 이동을 관리합니다.
작동 원리:
- 스택은 메모리의 높은 주소에서 낮은 주소로 확장됩니다. 즉, 새 데이터를 푸시(push)하면 ESP는 감소하고, 데이터를 팝(pop)하면 ESP는 증가합니다.
- 함수 호출 시, 반환 주소와 함수의 인수가 스택에 저장되고, ESP가 조정되어 새 스택 프레임이 생성됩니다.
EBP (Base Pointer) - 베이스 포인터
EBP는 스택 프레임의 시작을 가리키는 레지스터로, 주로 함수 호출 시 스택 프레임 내에서 지역 변수와 인수를 참조하는 데 사용됩니다.
역할:
- 함수의 스택 프레임의 기준점으로 사용됩니다.
- 지역 변수와 함수 인수에 접근하는 기준이 됩니다.
작동 원리:
- 함수가 호출될 때, 현재 EBP 값이 스택에 저장되고 새로운 스택 프레임이 만들어집니다.
- EBP는 스택 프레임의 시작을 가리키므로, 상대적인 오프셋을 사용하여 함수 인수와 지역 변수에 접근할 수 있습니다.
EIP (Instruction Pointer) - 명령어 포인터
EIP는 현재 실행 중인 명령어의 메모리 주소를 가리키는 레지스터로, 프로그램의 제어 흐름을 관리합니다.
역할:
- 현재 실행할 명령어의 주소를 저장합니다.
- 다음에 실행할 명령어로 자동으로 증가하여 프로그램의 순차 실행을 유지합니다.
작동 원리:
- 명령어가 실행될 때마다 EIP는 자동으로 다음 명령어의 주소로 업데이트됩니다.
- 분기나 함수 호출 같은 제어 흐름 변경이 발생하면, EIP는 새로운 명령어 주소로 갱신됩니다.
inline
함수 호출에 대한 비용을 줄이기 위해 사용한다. 함수가 호출되지 않고 함수의 몸체 부분이 호출 부분을 대체하게 된다.
// 함수 호출 비용을 줄여줌
// 실행 파일 사이즈가 커질 수 있음
// 항상 컴파일러가 인라인화 해주지는 않음
// 함수 프로토타입에 붙여서 사용할 수 도 있다.
inline int square(int x) //함수 앞에 inline 키워드를 붙여서 사용한다
{
return x * x;
}
// 전처리를 이용한 인라인.
// 오류가 발생할 수 있으니 C++의 inline 문법을 사용
#define SQUARE(X) X*X
int main()
{
int x = square(10);
cout << x << endl;
int y = 10;
cout << SQUARE(++y) << endl; // 의도치 않은 결과 발생
}
Ch 10. 범위, 공간
빌드
소스코드 → 전처리 → .i파일 생성 → 파일별로 컴파일 → .obj파일 생성 → 링크 → .obj파일들 묶기 → 실행파일 생성
위 과정들을 빌드라고 한다.
developer command prompt를 이용하여 직접 단계별로 실행해 볼 수 있다.
범위
블록스코프
int main()
{
//block scope
int x = 10;
{
int x = 5;
cout << x << endl;
}
cout << x << endl;
return 0;
}
외부에서는 블록 내부에 접근이 되지 않는다. static선언을 하여서 메모리 해제가 되지 않는다고 하더라도 외부에서는 접근할 수 없기 때문에 블록 스코프는 유지된다.
int main()
{
//block scope
int x = 10;
{
static int x = 5; //스태틱
cout << x << endl;
}
cout << x << endl;
return 0;
}
다른 블록스코프로는 if문, while문 등이 있다.
{// 가상의 스코프 형태
if(조건문)
{
몸체
}
}
함수 밖에 선언한 변수는 전역스코프를 가지게 된다. 전역스코프에 같은 이름의 변수가 겹치면 안 된다. 그래서 다른 소스코드에 있는 변수를 사용할 때는 extern을 사용한다.
// 메인.cpp
{
int x = 10;
}
// 소스코드1.cpp
{
extern int x;
}
한 파일 내에서만 전역적인 변수는 static을 사용하여 파일스코프로 선언한다.
// 메인.cpp
{
static int x = 10; //파일 스코프
}
// 소스코드1.cpp
{
int x;
}
열거형 스코프
enum struct Color{
Red, Blue, Green
}
cout << Color::Blue << endl; //접근할 때 범위지정자를 사용해야 함
네임스페이스
namespace CompanyA
{
int num = 10;
}
int main()
{
cout << CompanyA::num;
}
자주 쓰는 std 또한 네임스페이스 스코프에 해당한다.
using의 제한적인 사용
using std::cout;
cout << 10;
std::cin >> num; //cin은 std를 붙여야함
공간 기억 부류(자동, 정적, 동적)
오토매틱 스토리지, 자동 공간, 스태틱 같은 키워드가 붙지 않은 일반적인 변수들이 해당된다. 스코프를 벗어나면 자동으로 메모리가 해제된다. 스택에 할당된다.
지역변수에 스태틱을 붙이게 되면 정적변수가 된다. 데이터 영역에 저장된다. 전역 변수 또한 정적변수에 해당된다. 정적 공간에 할당되는 변수들은 한 번만 초기화되고 프로그램이 종료될 때 해제 된다.
동적 변수는 이름이 없기 때문에 포인터를 사용해야 한다. new와 delete를 사용하여 명시적으로 할당, 해제해주어야 한다. 힙이라는 영역에 할당된다.
자동 변수 정적 변수 동적 변수
스택 영역 | 데이터 영역 | 힙 영역 |
지역 | static, 전역 | new, delete |
'C & C++ > C++' 카테고리의 다른 글
[C++] 연산자 오버로딩 (4) | 2024.07.16 |
---|---|
[C++] 클래스 (0) | 2024.07.13 |
[C++] 포인터, 참조 (1) | 2024.07.01 |
[C++] 흐름 제어, 복합 데이터 (0) | 2024.06.26 |
[C++] 데이터, 입출력, 연산자 (0) | 2024.06.25 |