본문 바로가기

컴퓨터공학/C, C++

C,C++> 함수 고급

호출 규약

스택

스택은 시스템이 사용하는 메모리 공간이다. CPU가 임시 정보를 저장할 필요가 있을 때 이 영역을 사용한다. 

호출 규약(Calling Convention)이란 함수를 호출하는 방식에 관한 약속이다.

인수는 어떻게 전달하고 반환값은 어떻게 반환하고 인수 전달을 위해 사용한 메모리는 누가 정리할 것지 등을 규정한다. 

호출하는 쪽과 되는 쪽 약속이 맞아야 한다. 

컴파일러 내부에서 일어나는 일이라서 이해하기 쉽지 않다. 

컴파일러 내부 동작과 함수 호출 과정을 알면 재귀 호출이나 가변 인수 등 고급 기법들을 이해함으로써 자유롭게 코드를 작성할 수 있다. 또한 저수준 디버깅에도 활용할 수 있으며 다른 언어로 만든 함수를 호출하는 방법도 알게 된다. 

호출 규약을 알기 위해서 스택을 알아야 하며 스택은 기계어 수준에서 동작하기 때문에 어셈블리 언어 개념도 필요하다. 

 

간단 어셈블리어 개념 

eax, ebp 등의 레지스터는 CPU가 사용하는 변수라고 생각하면 되고,

mov a, b :  b 에서 a로 데이터 이동

add a b : a+b 결과를 a에 저장 

sub a b : a-b를 a에 저장 

대괄호는 주소가 가리키는 값을 의미한다. 포인터와 유사하다. 

 

코딩의 시작, TCP School

힙은 실행 중 동적으로 할당되는 메모리 영역이다. 

할당이 발생하면 뒤로 이동하면서 자유 영역을 사용한다.

스택은 값을 넣으면 위쪽으로 이동한다. 

힙과 스택 사이에는 자유 영역이 있어 두 영역 사이 완충 역할을 한다. 

만약 힙과 스택이 만나면 메모리 부족 상태가 된다. 

 

스택에 값을 저장하는 동작을 push, 저장한 값을 빼는 걸 pop이라고 한다. 

스택 현재 위치는 esp 레지스터에 기억된다. 

push하면 esp가 감소하면서 값이 스택으로 들어가고 pop하면 esp에 저장된 값을 빼고 esp가 증가한다. 

push A는 esp -= 4 와 mov [esp], A 이다.

pop A는 mov A, [esp] 와 esp += 4 이다.

여기서 4는 32비트 시스템 스택 한 칸 크기이다.  위가 낮은 주소이니 스택에 집어넣으면 주소값이 감소한다. 

스택은 LIFO(Last In First Out) 원칙이다.

CPU 범용 레지스터 개수는 많지 않아서 필요한 레지스터가 이미 값을 가지고 있으면 스택에 레지스터 값을 잠시 대피하고 사용한다. 

예를 들어 eax, ecx를 잠시 다른 용도로 사용하고 싶으면 CPU는 두 레지스터 값을 스택에 push하여 저장하고 레지스터를 다 사용하고 난 후 pop해서 복구한다. 

참고로 C언어는 스택을 직접 조작하지 않는다. 

 

스택 프레임

함수가 호출되면 스택에는 함수 전달 인수, 복귀 주소값, 지역변수 등 정보들이 저장된다. 

스택에 저장되는 함수 호출 정보를 스택 프레임(Stack Frame)이라고 한다. 

 

Guide to x86 Assembly (virginia.edu)

 

 

스택 프레임이 어떻게 작동하는지 하나씩 알아 보자.

다음은 분석할 예제이다.

int Add(int a, int b){
	int c,d,e;
    c=a+b;
    return c;
}

void main(){
	int result;
    result = Add(1,2);
    printf("result = %d\n", result);
}

다음은 main 함수에서 result = ADD(1,2); 를 호출하는 코드이다.

push 2

push 1

call Add

add esp, 8

result = eax

인수 2, 1을 스택에 푸시한다. 

뒤쪽 인수부터 순서대로 스택에 푸시한 후 call 명령으로 Add 함수를 호출한다. 

call 명령은 함수가 복귀할 번지를 스택에 푸시한다. 

일단 Add를 알아본다. 

 

다음은 Add 함수이다. 

push ebp 

mov ebp, esp

sub esp, 0ch

mov eax, [ebp+8]

add eax, [ebp+0ch]

mov [ebp-4], eax

mov eax, [ebp-4]

mov esp, ebp

pop ebp

ret

호출원(main 함수)에서 사용하던 ebp를 스택에 먼저 푸시한다. 

그리고 현재 위치를 ebp에 넣는다. 

ebp는 함수 실행 중 인수와 지역변수를 접근하기 위한 기준 번지(Base Pointer)로 사용된다. 

Add를 호출한 함수(main 함수)도 ebp를 기준 번지로 사용한다.

따라서 Add는 자신의 ebp를 설정하기 전 호출원 ebp를 저장해야 한다. 

그리고 나서 Add가 ebp를 사용하는 것이다. 

그 다음 esp를 0ch(십진수 12)만큼 감소시켜 지역변수가 들어갈 공간을 만든다. 

Add함수는 3개 정수형 변수를 사용하므로 지역변수 영역에 필요한 공간은 12바이트이다.

double이나 구조체를 사용하면 더 많은 메모리를 필요할 것이다. 

지역변수 영역은 함수에 선언된 지역변수 총 크기만큼 할당된다. 

별도 초기화하지 않는다. 

여기 까지 코드를 접두(prolog)라고 하며 함수 실행 준비하는 과정이다.

sub esp, 0ch  까지 실행할 때 모습은 다음과 같다. 

esp는 함수 실행 중 필요에 따라 오르내리락 하겠지만 ebp는 계속 기준 번지를 가리킨다. 

ebp 기준 아래는 인수, 위는 함수 지역 변수이다. 

인수와 지역변수는 ebp으로 구할 수 있다.

 

실행 준비가 되면 함수 본체 코드가 실행한다. 

함수 본체 c = a+b는  다음 세 줄로 컴파일된다. 

기계어는 메모리간 덧셈을 직접 지원하지 않아서 레지스터로 값을 읽은 후 레지스터와 연산한다. 

mov eax, [ebp+8]  // eax = a

add eax, [ebp+0ch]  // eax = eax + b

mov [ebp-4], eax  // c = eax

mov eax, [ebp-4]  // return c 

 

본체 실행을 마쳤고 이제 나머지 정리 작업을 수행한다. 

정리 작업 코드를 접미(epilog)라고 한다. 

mov esp, ebp

pop ebp

ret 

esp에 ebp를 대입하는데 지역변수를 위해 할당했던 스택 영역을 회수한다는 뜻이다. 

ebp를 pop하면 호출원에서 사용한 ebp가 복구된다. 

ebp는 현재 함수 기준 주소값을 갖고 있다가  pop을 하면  메인함수 기준값으로 옮긴다는 뜻이다.

이 과정을 자세하게 풀이하면, pop ebp는 mov ebp, [esp] 와 esp += 4 를 뜻한다.

esp는 현재 main ebp의 주소값을 갖고 있고 [esp]는 main ebp 값인데 main ebp는 main 함수 ebp값인 주소값이다. 

이 데이터를 ebp로 옮기는 거니까 ebp는 main 함수 ebp의 주소값을 갖게 된다.  

마지막으로 ret 명령으로 복귀하는데 스택에 저장된 복귀 번지를 꺼내 그 번지로 리턴한다(jmp [esp], esp+=4)

 

add esp, 8 

result = eax

esp에 8을 더하는 이유는 함수 호출 전 인수 전달을 위해 푸시한 값을 삭제하기 위해서다.

이 동작은 pop을 두 번 하는 것과 동일하다. 인수가 많으면 일일이 pop을 할 수 없으므로 esp를 직접 증가하여 여러 번 pop을 대신하고 있다.

 

esp, ebp 등 주요 레지스터들과 스택이 호출 전 상태로 북구한다. 

마지막으로 eax에 저장되어 있는 Add 함수 return 값을 result에 대입한다. 

이 값은 printf 호출로 화면에 출력될 것이고 프로그램은 종료된다. 

 

 

함수가 연속으로 두 번 호출될 때 보자. 

Dog 함수 안에 Add 함수가 있을 경우

main 스택 프레임 위 Dog 스택 프레임이 생성된다.

Dog는 자신을 호출한 main이 사용하던 ebp를 스택에 저장하고 새로 설정한 ebp를 기준으로 인수와 지역변수를 접근할 수 있다. 

이 상태에서 Add가 호출되면 Dog 스택 프레임 위 Add 스택 프레임이 생성된다. 

Dog는 Add 인수 2, 1 복귀 번지를 스택에 푸시.

Add는 Dog의 ebp를 스택에 저장한 후 자신의 ebp를 새로 대입하고 esp를 감소시켜 지역변수 영역을 만든다. 

Add도 자신의 ebp를 기준으로 인수와 지역변수를 접근.

 

Add는 또 다른 함수를 호출할 수 있다. 그렇다면 Add 스택 프레임 위 또 다른 함수 스택 프레임이 생성될 것이다. 

여기에 다른 함수를 더 호출할 수 있으며 그럴 때마다 위쪽으로 스택 프레임이 계속 쌓인다. 

몇 개가 쌓이든 스택 아래 쪽에는 Dog, Add, main 함수의 지역변수가 그대로 보관되어 있다. 

 

각 함수가 스택에 저장하는 ebp는 자신의 기준 주소값이 아니라 자신이 호출한 함수가 사용하는 ebp이다. 

함수 실행을 마치고 반환했을 때 호출원은 자신의 지역변수를 읽어야 하므로 호출원 ebp를 복구해야 한다. 

따라서 Add가 필요에 의해 ebp를 바꾸더라도 변경 전 ebp를 스택에 저장하고 복귀하기 전 pop하면 Dog는 Add가 끝난 후에 지역변수에 접근할 수 있다. 

 

결론.

1. 인수도 함수 호출 중에만 생명 유지되는 일종의 지역변수이다. 인수 초기화 시점은 함수가 호출될 때이며 함수 내부에서만 사용한다. 

2. 지역변수를 많이 선언하는 것과 함수 실행 속도는 직접적인 상관이 없다. 지역 변수 영역은 esp를 필요한 양만큼 감소시키켜서 생성하기 때문인데 10을 빼나 20을 빼나 연산 속도는 일정하다.

3. 지역변수를 많이 쓴다고 프로그램이 커지는 것은 아니다. 어차피 지역변수는 스택에 임시로 생겼다가 함수가 끝나면 없어진다. 프로그램 크기와는 무관하다. 

4. 지역변수를 위해 esp 위로 올려 공간만 만들 뿐 별도 초기식이 없으면 지역변수는 초기화되지 않는다. 이때 있는 값은 쓰레기값이다. 지역변수를 초기화하면 초기화하는 시간만큼 느려진다. 필요한 코드만큼 프로그램 크기도 늘어난다. 

5. 함수 호출할 때마다 스택 프레임이 생성되었다가 사라지는 과정을 거치므로 함수 호출에는 오버헤더가 있다. 

 

 

호출 규약 

인수는 뒤쪽부터 순서대로 전달하며,

인수 전달에 사용한 스택 영역은 호출원이 정리했는데,

이는 C/C++ 기본 호출 규약인 _cdecl의 스택 프레임 모양일 뿐이다. 

호출 규약이 바뀌면 스택 프레임 모양과 관리 방법도 달라질 수 있다. 

예를 들어 _stdcall 호출 규약은 인수 전달 방법은 같지만 함수가 인수 전달에 사용한 스택을 직접 정리한다. 

_cdecl은 함수가 리턴한 다음 인수 전달에 사용한 스택을 복구한다.

 

_cdecl과 차이점

둘 다 어차피 컴파일 결과 코드에 미치는 영향은 별로 없다. 

스택 정리 주체 상관없이 스택은 항상 호출 전 상태로 복구되며 프로그램 동작도 완전 동일하다. 

실행 속도도 거의 차이가 없으며 프로그램 크기는 무시할만한 수준이지만 _stdcall이 더 작다.

왜냐하면 함수를 여러 번 호출하더라도 스택을 정리하는 코드는 함수 끝 접미에 딱 한 번만 작성되기 때문이다.

반면 _cdecl은 호출원이 스택을 정리하므로 호출할 때마다 정리 코드가 반복되어 프로그램 크기가 조금 더 커진다. 

 

다른 차이점은 가변 인수 함수를 만들 수 있는가 아닌가 하는 점이다. 

_stdcall은 함수가 직접 스택을 정리하기 때문에 가변 인수 함수를 지원하지 않는다. 

함수 접미에  스택 정리 코드를 작성하려면  크기를 알아야 하는데 가변 인수 함수는 전달되는 인수가 가변이라서 크기가 고정이 아니다.  컴파일러가 접미 복귀 코드 ret n 명령에 관해서 n을 결정할 수 없기 때문이다.

그래서 접미에서 스택을 직접 정리할 수 없다.

 

반면에 _cdecl은 함수가 스택을 정리할 책임이 없으며 호출원이 함수를 부를 때마다 스택을 정리한다. 

함수 호출 하는 쪽은 인수 몇개를 전달했는지 알 수 있으니 실제 전달 인수 크기만큼 스택을 정리할 수 있다. 

그래서 printf 나 scanf 같은 가변 인수 지원 함수는 모두 _cdecl 호출 규약을 사용한다. 

윈도우즈 API 함수 기본 호출 규약은 _stdcall인데 wsprintf는 예외적으로 _cdecl로 작성되어 있다. 

 

 

_fastcall

mov edx, 2

mov ecx, 1

call Add

result = eax

인수 전달을 위해 edx, ecx 레지스터를 사용하는데 만약 인수가 두 개 이상이라면 세 번째 인수는 스택에 밀어 넣는다.

레지스터를 우선적으로 사용하므로 인수 전달 속도가 빠르다. 

레지스터를 통해 전달받은 인수는 순서대로 지역변수에 복사하고 사용한다. 

VC는 fastcall 호출 시 ecx, edx로 인수를 넘기지만 이걸 다시 스택 지역변수로 만드는데 이렇게 되면 fastcall 의미가 없다.

VC++은 fastcall을 형식적으로 지원하지만 장점을 취하지 않는데 컴파일러 구현상 ecx, edx 레지스터가 필요하기 때문이다.

호출속도는 빠르지만 이식성이 불리하다는 단점이 있다. 

ecx와 edx 레지스터를 사용하는데 모든 CPU에 존재하는 것이 아니기 때문이다. 

 

thiscall

클래스 멤버 함수에 적용되는데 ecx로 객체 포인터(this)가 전달된다. 

나머지는 _stdcall과 동일하다. 가변 인수를 사용하는 멤버 함수는 _cdecl로 작성된다. 

this는 스택 제일 마지막(첫 번째 인수)로 전달된다. 

 

_naked

컴파일러가 접두, 접미를 작성하지 않는 호출 규약이다. 

스택 프레임 상태 보존을 위해 컴파일러가 어떤 코드를 작성하지 않으므로 접두 접미는 사용자가 인라인 어셈블리로 사용해야 한다. 

이 호출을 사용하는 경우는 C/C++가 아니라 언어에서 호출하는 함수를 작성할 때이다. 

 

호출 규약 불일치

함수 호출 규약을 어떤 것으로 쓸 것인가는 고민할 문제가 아니다. 컴파일러가 지정한 호출 규약대로 함수를 컴파일하고 적합한 코드도 같이 만들기 때문이다.  그러나 호출원과 호출 규약이 달라질 수 있는데,

함수를 작성한 언어와 호출하는 언어가 다를 경우,

분리된 DLL에 있는 함수를 호출할 때,

원형이 이미 정해져 있는 콜백함수를 호출할 때 등이다.

함수를 C/C++로 작성했고 부르는 쪽도 C/C++이며 같은 모듈 내에서 호출하면 발생할 일이 없다. 

 

 

 

재귀호출 

큰 문제를 작게 나누어 호출하면서 점진적으로 문제를 해결하는 방법(Divide and Conquer : 분할 점령).

작게 만든 문제가 원래 문제와 같은 구조를 가지면 재귀 호출이 필요하다. 

재귀 호출이 연쇄적으로 발생하면서 문제가 점점 작아지는 모습을 볼 수 있다.

재귀 호출 구조를 가지는 함수가 일반 함수로 변환할 수 있으면 가급적 사용하지 않는게 좋다.

어떤 경우 재귀 호출 아니면 문제를 해결할 수 없는 경우가 있고, 구조나 속도상 훨씬 더 유리한 경우도 있어서 적재적소 사용하면 효율이 있다.

 

재귀 호출이 가능한 이유

함수 호출을 하면 함수 정보들이 스택에 생성되는데 스택 프레임이라고 한다.

같은 함수가 여러 번 호출되어도 함수 스택프레임은 개별적으로 생성된다. 

반환점이 없이 무한 루프가 된다면 스택 공간이 소진되는 사태(Stack Overflow)가 발생한다. 

 

디렉토리 검색

재귀 호출 대표 예시는 디렉토리 검색이다. 

특정 디렉토리 안에 조건에 맞는 모든 파일을 작업하고 싶다면 디렉토리 전체 순회할 수 있어야 하기 때문이다. 

 

계층 자료 표현

하나의 부모 아래 여러 개 자식이 포함될 수 있는 구조를 계층(Hierarchical)이라고 한다. 

계층 자료는 트리로 관리할 수 있지만 복잡하고 비효율적이고 데이터 베이스 테이블 구조와 안 맞다. 

데이터 베이스 테이블은 배열 형태로 되어 있기 때문이다.

그래서 계층 자료라도 배열이나 연결 리스트를 사용하는 방법을 많이 사용한다. 

각 레코드에 부모가 누구인지 표시하여 레코드간 계층을 구성한다. 

 

 

인라인 함수 

인라인 함수, 디폴트 인수, 오버로딩은 C++에서 새로 추가된 기능이다.

인라인 함수 

함수가 호출할 때마다 내부에서 어떤 일이 벌어지는 지 살펴보자. 

1. 인수를 전달하기 위해 인수값을 순서대로 스택에 밀어 넣는다.

2. 호출원은 바로 다음 번지를 스택에 기록하여 함수가 복귀할 번지를 저장한다. 

3. 함수가 정의되어 있는 번지로 점프하여 제어권을 함수에 넘긴다.

4. 함수는 스택에 지역변수를 위한 공간을 만든다. 

5. 함수 코드를 수행한다. 

6. 리턴값을 넘긴다. 

7. 복귀 번지로 리턴한다.

8. 인수 전달에 사용한 스택을 정리한다. 

 

함수 실행시간이 0.001초 걸리는데 호출 시간이 0.1초  걸리면 코드를 함수로 만들지 않고 호출부에 바로 삽입하는 것이 이득이다. 이런 개념이 바로 인라인 함수이다. 

함수를 호출하지만 함수가 있는 곳으로 점프하지 않고 호출부 자리에 본체 코드를 바로 삽입하는 방식의 함수이다. 

함수 정의부 앞에 inline 키워드만 붙이면 된다. 

컴파일러는 함수 본체 코드를 기억하고 있다가 인라인 함수가 호출되는 곳에 바로 삽입한다. 

복귀 번지를 기록하고 인수, 리턴값을 전달하는 시간만큼 절약할 수 있다. 

인라인 함수가 호출되는 곳마다 함수 본체가 삽입되어서 실행 파일 크기가 커진다는 단점이 있다.

속도는 빠르지만 크기가 커지는 특징이다.

 

본체코드가 작을 때 유리하다. 장문 코드가 매번 반복되면 실행 파일 크기가 커져버리기 때문이다.

 

여러 모듈에서 공유하는 함수라면 헤더 파일에 작성한다. 

인라인 함수 본체는 정의가 아니라 선언이라서 메모리 소모를 하지 않으며 중복 선언해도 상관 없다. 

보통 헤더 파일에 작성한다. 단 같은 모듈에서 두 번 선언하는 것은 안된다. 자세한 내용은 헤더 파일 중복 오류 참조.

선언과 정의의 차이점은 메모리를 메모리를 할당하는가 이다.

선언은 메모리에 올리지 않기 때문에 중복돼도 상관없다. 

반면 정의는 이름이 중복되면 안된다. 

 

함수가 인라인이 될 것인지 아닌지는 컴파일러가 결정한다. 

함수 앞에 inline을 붙여도 컴파일러는 이걸 무시하고 일반 함수로 만들 수 있다. 

예를 들어 재귀호출은 인라인이 될 수 없다. 왜냐하면 재귀 호출은 스택기반으로 동작하기 때문이다.

프로그램의 다른 곳에서 이 함수 주소를 참조하는 경우가 있다면 인라인 함수가 될 수 없다.

인라인 함수는 번지를 가질 수 없다.

함수 길이가 너무 길면 득보다 실이 많아서 컴파일러 는 이 함수를 강제로 일반 함수로 만든다. 

inline 키워드를 쓰지 않아도 자동으로 인라인 함수가 될 수 있는데, 클래스 선언에 코드가 작성되어 있는 멤버함수는 자동 인라인 속성을 가진다. 

함수가 인라인이 될 것인가 아닌가는 속도와 크기차이가 있을 뿐 동작이 달라지는 건 아니라서 컴파일러가 inline을 무시하거나 강제 지정해도 별 상관 없다. 

 

매크로 함수와 다른 점

매크로 함수는 호출될 때마다 매번 전개되어 호출부에 삽입된다.

본체 코드가 반복 삽입되므로 실행 파일 크기가 증가된다. 

인라인 함수와 비슷해서 매크로 함수가 대신 사용될 수 있으며 반대도 된다. 

하지만 인라인 함수는 매크로 함수보다 좋은 점이 몇 가지 있다.

1. 인라인 함수는 타입을 인식한다.

매크로 함수는 단순 치환하는 것과 비교된다. 그래서 잘못된 타입으로 생기는 에러처리에 유리하다. 

2. 인라인 함수는 함수 형태라서 지역 변수를 사용할 수 있다. 매크로도 블록 변수 형태로 지역변수를 사용할 수 있지만 일반적이지 않다. 그래서 인라인 함수가 더 복잡한 동작을 정의할 수 있다. 

3. 매크로는 컴파일 이전 전처리 단계에서 인수를 치환하기 때문에 괄호를 싸지 않으면 예상치 못한 부작용이 생길 수 있다. 인라인 함수는 값에 의한 인수 전달, 연산자 우선 순위와 결합 법칙 적용을 받아서 괜찮다.

 

 

디폴트 인수

디폴트 인수 작성법

디폴트 인수는 함수 원형에만 지정할 수 있으며 정의부에서는 중복 지정할 수 없다. 

원형 선언없이 정의부만 있다면 정의부에 디폴트 인수를 지정할 수 있지만 원형과 정의부가 동시 존재하는 경우 원형 선언에만 디폴트 인수가 있어야 한다. 

디폴트 인수는 오른쪽부터 순서대로 지정할 수 있으며 가운데 인수들은 기본값을 지정할 수 없다. 

함수 호출할 때도 기본 인수들은 오른쪽부터 생략할 수 있으며 중간 한 인수만 생략할 수 없다.

함수 원형 선언에서 형식 인수 이름은 별다른 의미가 없어서 인수 이름을 생략한 채 기본값을 지정할 수 있다.

 

 

오버로딩

함수 중복

같은 이름으로 함수를 중복 정의하는 것이다.

컴파일러는 호출부 인수를 보고 어떤 함수를 호출할 건지 정할 수 있다.

 

모든 프로그래밍 언어는 목적 파일(obj)을 만들고 링크로 목적 파일을 연결하여 최종 실행 파일을 만든다. 

목적 파일로 자신이 정의한 함수 명칭과 주소를 공개해야 한다.

이 명칭 공개 방식이 언어에 상관없이 표준 포맷으로 규정되어 있다.

그래서 각각 다른 언어로 컴파일된 오브젝트 파일들이 링크될 수 있다.

초기 C++도 범용 링커를 사용하도록 디자인됐다. 범용 링커는 함수를 이름으로만 찾기 때문에 오버로딩 함수들을 제대로 구분하지 못 한다. 그래서 함수 이름을 외부 공개할 때 인수 개수와 타입 정보까지 함수명에 포함한다. 이런 명칭을 작성하는 걸 이름 장식(name mangling)이라고 한다. 

근데 모든 언어가 오버로딩을 지원하는게 아니라서 다른 언어와 링크되어야 하는 모듈은 함수 명칭을 이름만으로 외부에 공개할 필요가 있다. 이 때 사용하는 지시자가 extern "C"이다.  함수 앞에 이 지시자를 사용하면 오버로딩 기능은 사용할 수 없지만 다른 언어와 링크될 수 있는 범용 함수를 만들 수 있다. 

 

중복이 안 되는 경우 

1. 리턴 타입만 다른 경우

2. 레퍼런스와 일반 변수.

인수 목록에 레퍼런스가 전달되는 경우와 일반 변수가 전달되는 경우도 중복 정의할 수 없다.

3. const 지정자가 있는 경우와 없는 경우.

포인터가 가리키는 문자열이 상수인지 아닌지는 컴파일러가 쉽게 판단할 수 있어서 전달되는 인수 타입으로 호출할 함수를 결정할 수 있다. 그러나 포인터 자체가 상수인 경우와 그렇지 않은 경우는 구분되지 않는다. 

4. 인수의 논리적 의미만 다른 경우.

인수의 논리적 의미가 달라도 물리적 타입이 동일하면 중복 정의할 수 없다. 컴파일러는 인수 타입만 볼 뿐 인수 의미까지 판단하지 않는다. 굳이 구별하고 싶다면 인수 순서를 바꾸거나 더미 인수를 하나 더 집어넣는 식으로 구분할 수 있다.

5. 디폴트 인수로 같아질 수 있는 경우

int Add(int, int, int = 0);

Add(1,2) 호출문이 Add(int, int)인지 Add(int, int, 0) 인지 모호하다.

6. 달라 보이지만 실제로 같은 타입인 경우

컴파일러는 타입 이름이 아니라 실제 타입을 검사하므로 typedef나 매크로 등으로 정의된 사용자 정의 타입이 결국 같은 타입이 되는 경우는 중복 정의할 수 없다. 

 

 


 

책은 수집하는데 돈이 들고 읽는데 시간이 든다. 하지만 당장 책 몇 푼 아낄 생각하지 말고 책 하나로 자신을 발전하는데 열중해라. 책은 소비가 아니라 투자이다. 한 페이지라도 읽을 것이 있으면 사서 읽고 소장하라. 책 도움을 받아 시간 절약을 할 수 있다면 본전을 찾은 것이며 앞으로도 계속 도움을 줄 것이다. 

 

 

 

 

'컴퓨터공학 > C, C++' 카테고리의 다른 글

C,C++> 기타 내용  (0) 2022.08.03
C,C++> 파일 입출력  (0) 2022.07.28
C,C++> 포인터 고급  (0) 2022.07.25
C,C++> 구조체  (0) 2022.07.21
C,C++> 배열과 포인터  (0) 2022.07.19