함수
함수라는 이름은 여러 언어에서 다른 이름으로 불린다. 어셈블리에서는 프로시저, C#은 메소드, 루틴 등등으로 다양한 이름으로 불린다.
함수는 특정 코드의 묶음이다.
우리가 알고 있는 수학에서의 함수랑 동일하다고 볼 수 있다. 하지만 다른 점은 입력값이 없을 수도 있고 출력값이 없을 수도 있다.
함수를 만들 때는 input으로 무엇을 받고, ouput으로 무엇을 뱉을지 정해줘야한다
반환 타입 함수이름([인자타입 매개변수])
{
함수 내용
return ~~~;
}
와 같은 형식을 가진다.
만약 hello world를 출력하는 함수를 만든다고 해보자.
input : 없음 / output : 없음
타입 : void -> 반환값없음
#include <iostream>
using namespace std;
void PirintHelloWorld()
{
cout << "Hello, World" << endl;
}
int main()
{
PirintHelloWorld();
return 0;
}
이런식으로 작성할 수 있을 것이다. 이렇게 코드를 재사용할 수도 있고 편하게 작업할 수 있다. return은 어셈블리에서 call 을 한 후 ret을 만났을 때 돌아가는 것과 동일하다.
이것이외의 다양한 타입을 반환하거나 매개변수를 받는 경우가 있는데 그 부분은 자세히 다루지 않겠다.
스택 프레임
어셈블리 포스팅을 하면서 잠깐 다룬 내용이지만 정말 중요한 개념이자 내용이다.
함수를 호출 했을 때 어떤 일이 일어날까?
이런 식으로 높은 주소에서 낮은 주소로 스택 메모리를 사용하게 되는데, 함수의 매개변수와 돌아갈 곳, 그리고 함수 내부에서 사용되는 지역변수가 쌓기에 된다.
과연 그렇게 되는지 확인해보자.
브레이킹 포인트를 잡고 디버깅을 해보자.
이것이 스택에 쌓인 모습이다. push 2, push 1 을 해서 00D7FB6C 까지 저장되어 있다. (본인 stack 시작 지점은 esp 값이 00D7FB74이다.) 현재까지 매개변수가 들어간 모습이다. 그 후 함수를 call 을 하면 다시 돌아와서 실행해야하는 주소 001D18BE가 그 다음 stack 주소에 push가 된다. 그리고 함수가 실행되고 거기서 사용되는 지역변수 들이 저장 될 것이다. 함수가 종료되면 stack 에 있는 값들을 하나하나 pop을 통해 정리 한후 현재 esp가 가르키는 지점(함수가 돌아갈 곳)으로 ret을 통해 돌아가게된다.
지역 변수와 값 전달
함수 내부에서 선언해서 사용하는 변수가 바로 지역 변수 이다. 전역 변수는 함수 내부가 아닌 코드 안에서 전부 사용 가능한 변수를 말한다. 따라서 전역 변수는 어떤 함수에서건 전부 접근이 가능해 사용이 가능하다. 다만 메모리에 올라가는 영역이 다르다. 즉 전역 변수는 메모리에서 데이터 영역에 할당이 된다. 반면 지역 변수는 스택 영역에 할당이 된다. 그렇게 된다면 전역 변수만 사용해서 코딩하면 되는것이 아닐까?? 생각이든다.
하지만 코드가 몇 십만줄을 쓴다고 하면 자유도가 오히려 높기 때문에 서로 수정하고 난리 부르스를 하다보면 원하는 결과를 도출 할 수 없다.
지역 변수를 사용할 때, 스택 프레임을 제대로 이해하지 않으면 많은 실수가 일어나곤 한다.
대표적인 예이다.
#include <iostream>
using namespace std;
void IncreaseHp(int hp)
{
hp++;
}
int main()
{
int hp = 1;
cout << "함수 호출 전 :" << hp << endl;
IncreaseHp(hp);
cout << "함수 호출 후 :" << hp << endl;
return 0;
}
이렇게 하면 hp가 마치 증가된거 같지만 현실은 둘다 1로 출력 될것이다. 왜인가?
그 이유는 지역변수로 사용한 hp를 전달하게 되면 매개변수 라는 통 안에 hp를 복사해 저장한 후 그것을 사용하기 때문에 increasehp 함수 내부에서 hp를 하나 더 만든 거나 다름이 없는것이다.(실제로는 아님) 즉 hp가 1 증가하는건 매개변수가 1 증가하는 것이지 지역 변수 hp가 증가하는 것은 아니다. 그렇게 된다면 지역 변수 hp를 건드리려면 어떻게 할것인가?
다음에 다룰 포인터에서 다뤄보겠다.
호출 스택
함수를 호출 한 후 그 함수에서 다른 함수를 호출 하면 어떤 방식으로 동작이 될까?
#include <iostream>
using namespace std;
void Func1()
{
cout << "Func1" << endl;
Func2(1, 2);
}
void Func2(int a, int b)
{
cout << " Func2" << endl;
Func3(10);
}
void Func3(float a)
{
cout << "Func3" << endl;
}
int main()
{
cout << "main" << endl;
Func1();
return 0;
}
이렇게 하면 빌드가 오류가 날것이다. c++ 컴파일러는 조금 무식해서 위에서 부터 분석하기 시작한다. 그래서 func1에서 아직 func2를 모르기 때문이다. 그것을 해결하기 위해 함수를 먼저 선언하는 것이다.
#include <iostream>
using namespace std;
// 함수 선언
void Func1();
void Func2(int a, int b);
void Func3(float a);
void Func1()
{
cout << "Func1" << endl;
Func2(1, 2);
}
void Func2(int a, int b)
{
cout << " Func2" << endl;
Func3(10);
}
void Func3(float a)
{
cout << "Func3" << endl;
}
int main()
{
cout << "main" << endl;
Func1();
return 0;
}
이러면 컴파일러가 아 이런 함수가 있구나를 알아서 이제 빌드에 오류가 없을 것이다.
만약 func3가 어디서 호출 됐는지 알 수 있을까?? 만약 서로 얽히고 설키면 정말 찾기 어려울 것이다. 하지만 스택 프레임이 따라 코드의 흐름을 찾을 수 있을 것이다. 하지만 매번 어셈블리어를 읽거나 메모리, 레지스터를 확인할 수 는 없을 것이다. 그럴 경우를 대비해 IDE에서는 호출 스택이라는 것을 제공한다.
'C++ > 기초' 카테고리의 다른 글
[C++] 포인터 - 2 (참조) (1) | 2023.11.25 |
---|---|
[C++] 포인터 - 1 (0) | 2023.11.24 |
[C++] 코드의 흐름 제어 - 2 (가위-바위-보, 열거형) (1) | 2023.11.23 |
[C++] 코드의 흐름 제어 - 1 (분기문, 반복문) (1) | 2023.11.23 |
[C++] 데이터 가지고 놀기 - 2 (데이터 연산, const) (0) | 2023.11.23 |