본문 바로가기
C++/이것이 C++이다

[Chapter 02] C++ 함수와 네임스페이스

by HHack 2024. 3. 21.
반응형

1. 디폴트 매개변수

C에서는 함수를 호출하려면 반드시 매개변수의 실인수를 기술해야 합니다. 하지만 C++에서는 상황이 좀 달라 경우에 따라서는 생략할 수도 있습니다. C++에서는 함수 원형에 다음과 같이 초깃값을 기술할 수 있습니다. 아래는 디폴트 매개변수를 사용한 예제입니다.

#include "pch.h"
#include <iostream>

// nParm 매개변수의 디폴트 값은 10이다.
int TestFunc(int nParam = 10) {
    return nParam;
}

int main()
{
    // 호출자가 실인수를 기술하지 않았으므로 디폴트 값을 적용한다.
    std::cout << TestFunc() << std::endl;

    // 호출자가 실인수를 확정했으므로 디폴트 값을 무시한다.
    std::cout << TestFunc() << std::endl;

    return 0;
}
반환자료형 함수이름(매개변수 자료형 이름 = 디폴트값);

 

매개변수의 디폴트 값을 '선언'한 함수는 호출자 코드에서 실인수를 생략한 채 호출할 수 있습니다. 중요한 점은 매개변수의 디폴트 값은 반드시 함수 원형의 선언 부분에 기술해야 한다는 점입니다. 정의 부분에 기술할 경우 오류가 발생합니다. 다음코드는 함수 원형의 선언부에 디폴트 값을 기술한 예제입니다.

#include "pch.h"
#include <iostream>

int TestFunc(int = 10);

int TestFunc(int nParam) {
    return nParam;
}

int main() {
    // 호출자가 실인수를 기술하지 않았으므로 디폴트 값을 적용한다.
    std::cout << TestFunc() << std::endl;

    // 호출자가 실인수를 확정했으므로 디폴트 값을 무시한다.
    std::cout << TestFunc(20) << std::endl;

    return 0;
}

① int TestFunc(int = 10);

- 매개변수의 이름을 기술하지 않았습니다. 하지만 문법적으로는 전혀 문제가 없는 코드입니다. 왜냐하면 C/C++의 함수 원형 선언에서 매개변수의 이름을 생략할 수 있습니다.

 

여기서 중요한 사실은 "C++에서는 절대로 호출자의 코드만 보고 함수 원형을 확정하면 안 됩니다!"

(1) 매개변수가 2개 이상일 경우 디폴트 매개변수

이번에는 매개변수가 둘인 경우를 보겠습니다. 매개변수가 둘이므로 디폴트 값도 두 매개변수 모두 혹은 일부에 설정할 수 있습니다. 다만 신경 써야 할 부분이 있습니다. 예제로 확인해보겠습니다.

// 매개변수가 두 개일 때의 디폴트 값

#include "pch.h"
#include <iostream>

int TestFunc(int nParam1, int nParam2 = 2) {
	return nParam1 * nParam2;
}

int TestFunc1(int nParam1 = 5, int nParam2) {
	return nParam1 * nParam2;
}



int main() {
	// 호출자가 설정하는 실인수는 피호출자 함수 매개변수의 왼쪽부터 짝을 맞춘다.
	std::cout << TestFunc(10) << std::endl;
	std::cout << TestFunc(10, 5) << std::endl;

	int TestFunc(int nParam1 = 5, int nParam2, int nParam3 = 10); // 오류발생

	return 0;
}

 

특징 ① : 호출자가 설정하는 실인수는 피호출자 함수 매개변수의 왼쪽부터 짝을 맞춘다. 짝이 맞지 않으면 매개변수는 디폴트 값을 적용한다.

특징 ② : 매개변수의 디폴트 값은 반드시 오른쪽 매개변수부터 기술해야 한다.

특징 ③ : 왼쪽 첫 번째 매개변수의 디폴트 값을 기술하려면 나머지 오른쪽 '모든' 매개변수에 대한 디폴트 값을 기술해야 한다. 절대로 중간에 빼먹으면 안된다.

(2) 잘못 사용된 디폴트 매개변수

① 피호출자 함수 매개변수의 디폴트 값은 반드시 오른쪽 매개변수부터 기술해야 한다.

int TestFunc(int nParam1 = 5, int nParam2) {
	...
}

...

std::cout << TestFunc(10) << std::endl; // 오류 발생

TestFunc : 매개 개변수 2에 대한 기본 인수가 없습니다. 
TestFunc : TestFunc 함수는 1개의 매개변수를 사용하지 않습니다.

② 디폴트 값이 없는 매개변수에는 호출자 함수에 반드시 실인수를 기술해야 한다.

 int TestFunc(int nParam1 = 5, int nParam2) {
 	...
}
...
std::cout << TestFunc(10) << std::endl;

③ 중간에 위치한 매개변수에 디폴트 값을 생략할 수 없습니다.

int TestFunc(int nParam1 = 5, int nParam2, int nParam3 = 10); // 오류발생

④ 호출자의 코드만 보고서는 절대로 함수의 원형을 정확히 알아낼 수 없습니다.

std::cout << TeseFunc() << std::endl;

위 코드 한 줄만 보고서는 함수의 원형을 알아내기란 불가능합니다. 이처럼 디폴트 매개변수는 '모호성'을 만들 수 있습니다. 그리고 이 '모호성'은 코드를 이해하는데 매우 심각한 방해 요소로 작용합니다. 그러므로 가급적이면 디폴트 매개변수를 사용하지 않는것이 좋습니다.

 

2. 함수 다중 정의(오버로딩)

C++에서 '다중 정의'는 하나(함수 이름, 변수 이름 등)가 여러 의미를 동시에 갖는 것을 말합니다. 영어로 'Overloading' 이라고 합니다.

(1) 다중 정의 일반

아래 예제는 Add()라는 이름의 함수를 세 가지 형태로 다중 정의하고 사용하는 예입니다.

// 2.2.1 다중 정의 일반

#include "pch.h"
#include <iostream>

int Add(int a, int b, int c) {
    std::cout << "Add(int, int, int) : ";

    return a + b + c;
}

int Add(int a, int b) {
    std::cout << "Add(int, int) : ";

    return a + b;
}

double Add(double a, double b) {
    std::cout << "Add(double, double) : ";

    return a + b;
}

int main()
{
    // 호출되는 함수가 컴파일러에 의해 자동으로 결정된다.
    std::cout << Add(3, 4) << std::endl;
    std::cout << Add(3, 4, 5) << std::endl;
    std::cout << Add(3.3, 4.4) << std::endl;

    return 0;
}

 

위 예제를 실행해보면 '호출되는 함수가 컴파일러에 의해 자동으로 결정'된다는 사실을 알 수 있습니다.

 

함수는 크게 "반환 형식, 호출 규칙, 함수 이름, 매개변수 구성" 입니다. 이 중 다중 정의에 영향을 미치는 것은 매개변수 뿐입니다.

아래는 잘못된 다중정의 문법들의 예시입니다.

① 반환 형식만 다른 경우

int Add(int a, int b);
double Add(int a, int b);

② 호출 규칙만 다른 경우

int __cdecl Add(int a, int b);
int __stdcall Add(int a, int b);


※ Name Mangling

실제 저장되는 함수 이름이 따로 생성된다. 즉, 실제로는 중복되는 함수 이름을 갖지 않는다. exturn "C"를 사용하면 Name Mangling 기능을 사용하지 않을 수 있다.

(2) 다중 정의와 모호성

디폴트 매개변수와 다중 정의가 조합되면 매우 강력한 '모호성'이 발생할 수 있습니다. 그 중 문제가 될 만한 것을 예제로 만나보겠습니다.

// 2.2.2 다중 정의와 모호성

#include "pch.h"
#include <iostream>

void TestFunc(int a) {
    std::cout << "TestFunc(int)" << std::endl;
}

void TestFunc(int a, int b = 10) {
    std::cout << "TestFunc(int, int)" << std::endl;
}

int main() {
    TestFunc(5);

    return 0;
}

// 오류 C2668 : 'TestFunc': 오버로드된 함수에 대한 호출이 모호합니다.

컴파일러가 TestFunc(int a)를 호출해야 하는지 TestFunc(int a, int b = 10)을 호출해야 하는데 모호해서 오류가 발생합니다. 위와 같은 다중 정의 상태라면 사용자가 TestFunc(int)를 호출할 수 있는 방법은 없습니다. 

(3) 함수 템플릿

함수를 다중 정의 하는 이유는 사용자의 편의성과 확장성을 얻을 수 있기 때문입니다. 하지만 위와같이 여러 문제들이 생길 수 있기 때문에 C++에서는 가급적이면 함수 다중 정의보다는 '함수 템플릿'을 사용하기를 권장합니다.

template <typename T>
반환형식 함수이름(매개변수) {
	...
}

템플릿은 일종의 '틀'이며 판화에 틀을 만들어 여러 장의 판화를 인쇄하는 것과 같습니다. 다만 인쇄하는 것이 소스 코드라는 점만 기억하면 됩니다.

 

아래 예제를 보며 확인해보도록 하겠습니다.

// 2.2.3 함수 템플릿

#include "pch.h"
#include <iostream>

template <typename T>
T TestFunc(T a) {
    std::cout << "매개변수 a : " << std::endl;

    return a;
}

int main() {

    std::cout << "int\t" << TestFunc(3) << std::endl;
    std::cout << "double\t" << TestFunc(3.3) << std::endl;
    std::cout << "char\t" << TestFunc('A') << std::endl;
    std::cout << "char*\t" << TestFunc("TestString") << std::endl;
    
    return 0;
}

 

템플릿을 사용하면 호출자가 어떤 실인수로 TestFunc() 함수를 호출하는가에 따라 자동으로 다중 정의가 이루어집니다. 즉, 사용자 코드에 의해 컴파일러가 다중 정의 코드를 만들며 이 뜻은 메모리 어딘가에 함수를 생성한다는 의미입니다.

 

이번엔 함수 또한 템플릿을 사용하여 출력하는 함수 템플릿 예제를 보겠습니다.

// 함수 템플릿으로 만든 Add() 함수

#include "pch.h"
#include <iostream>

template <typename T>
T Add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << Add(3, 4) << std::endl;
    std::cout << Add(3.3, 4.4) << std::endl;

    return 0;
}

이렇게 하면 T를 int와 double로 해석해 각각의 코드를 생성합니다. 즉, int Add(int a, int b) 함수와 double Add(double a, double b) 함수 두 개를 메모리 어딘가에 생성합니다.

3. 인라인 함수

함수를 호출하면 스택 메모리 사용이 증가하고 매개변수 때문에 메모리 복사가 일어납니다. 이 같은 함수 호출로 인한 오버헤드 문제를 극복하고자 매크로를 사용합니다. 하지만 매크로는 함수가 아닙니다. 게다가 매개변수에 형식을 지정할 수 없다는 점도 큰 문제입니다. 이러한 문제점을 해결하기 위해 나온 것이 인라인 함수입니다. 즉, 매크로의 장점과 함수의 장점을 두루 갖춘 함수입니다. 인라인 함수는 내부적으로 매크로처럼 함수를 호출하지 않습니다.

// 2.3 인라인 함수

#include "pch.h"
#include <iostream>

// 매크로
#define ADD(a, b)((a) + (b))

// 일반적인 함수
int Add(int a, int b) {
    return a + b;
}

// 인라인 함수
inline int AddNew(int a, int b) {
    return a + b;
}

int main() {
    int a, b;
    scanf_s("%d%d", &a, &b);

    printf("ADD() : %d\n", ADD(a, b));
    printf("Add() : %d\n", Add(a, b));
    printf("AddNew() : %d\n", AddNew(a, b));

    return 0;
}

inline 함수를 만들기 위해선 함수 원형 앞에 inline이라는 예약어만 쓰면 됩니다.

함수를 호출하게 되면 내부적으로 스택 메모리 사용이 증가하고 메모리 복사가 일어납니다. 제어 흐름도 이동해야 하며 여러 연산들이 일어나게 됩니다. 하지만 요즘 컴파일러는 인라인 함수로 사용하는 데 적합하다고 생각되는 함수는 모두 인라인 함수로 자동으로 만들어줍니다. Visual Studio를 이용해 컴파일할 경우 인라인으로 선언되지 않았다고 하더라도 적합하다면 인라인 함수로 확장하며, 인라인 함수가 선언되어 있더라고 적합하지 않다면 일반 함수로 컴파일 합니다.

 

※ 최적화란?
컴파일러가 고급어를 기계어로 번역하는 과정에서 CPU나 메모리 사용의 효율을 높이도록 코드를 변경하는 것.

4. 네임스페이스

네임스페이스는 C++이 지원하는 각종 요소들(변수, 함수, 클래스 등)을 한 범주로 묶어주기 위한 문법입니다. 의미상으로 '소속'이나 '구역'이라는 의미로 이해하면 적합합니다.

(1) 네임스페이스 선언

네임스페이스의 블록 내부에 선언하는 변수나 함수들은 모두 명시한 '이름'에 속하게 됩니다.

namespace 이름 {
	// 네임스페이스의 시작
    ...
    // 네임스페이스의 끝
}
// 네임스페이스 선언 및 정의

#include "pch.h"
#include <iostream>
#include <TCHAR.h>

namespace TEST {
    int g_nData = 100;

    void TestFunc(void) {
        std::cout << "TEST::TestFunc()" << std::endl;
    }
}

int _tmain(int garc, _TCHAR *argv[]) {
    TEST::TestFunc(); // ::은 범위지정 연산자이다.
    std::cout << TEST::g_nData << std::endl;

    return 0;
}

① TEST::TestFunc();

- 네임스페이스가 존재할 경우 식별자 앞에 범위 지정 연산자(::)를 이용해 네임스페이스를 기술할 수 있습니다.

② _tmain

- _tmain() 함수는 소속이 없습니다. 정확히는 '전역 네임스페이스'에 속합니다.

(2) using 선언

using 예약어를 이용하여 자주 사용해야 하는 네임스페이스를 생략할 수 있습니다.

// using 선언

#include "pch.h"
#include <iostream>

// std 네임스페이스를 using 예약어로 선언한다.
using namespace std;

namespace TEST {
    int g_nData = 100;

    void TestFunc(void) {
        // cout에 대해서 범위를 지정하지 않아도 상관없다.
        cout << "TEST::TestFunc()" << endl;
    }
}

using namespace TEST;

int _tmain(int garc, _TCHAR *argv[]) {
    // TestFunc()나 g_nData에도 범위를 지정할 필요가 없다.
    TestFunc();
    cout << g_nData << endl;

    return 0;
}

(3) 네임스페이스의 중첩

네임스페이스 안에 네임스페이스가 속하는 걸 네임스페이스의 중첩이라고 합니다. C++에서는 이름이 같은 변수나 함수라도 속해있는 네임스페이스가 다르다면 전혀 다른 개체로 인식합니다.

// 2.4.3 네임스페이스의 중첩

#include "pch.h"
#include <iostream>
using namespace std;

namespace TEST {
    int g_nData = 100;
    namespace DEV {
        int g_nData = 200;
        namespace WIN {
            int g_nData = 300;
        }
    }
}

int main() {
    cout << TEST::g_nData << endl;
    cout << TEST::DEV::g_nData << endl;
    cout << TEST::DEV::WIN::g_nData << endl;

    return 0;
}

위에 선언된 int g_nData 변수는 이름은 같아도 전혀 다른 세 개의 변수입니다. 그런데 만약 TEST::g_nData라고 하지 않고 cout << g_nData << endl;과 같이 컴파일하면 오류가 발생합니다. 왜냐하면 _tmain() 함수가 속한 전역 네임스페이스에 g_nData 변수는 존재하지 않기 때문입니다.

(4) 네임스페이스와 다중 정의

C++에서는 네임스페이스는 달라고 나머지(함수 이름과 매개변수 구성)가 같은 형태로 선언되어 있다면 다중 정의가 가능합니다.(전혀 다른 두 함수가 동명이인처럼 존재 하는것으로 인식하면 됩니다.)

// 2.4.4 네임스페이스를 포함한 다중 정의

#include "pch.h"
#include <iostream>
using namespace std;

// 전역(개념상 무소속)
void TestFunc(void) {
    cout << "::TestFunc()" << endl;
}

namespace TEST {
    // TEST 네임스페이스 소속
    void TestFunc(void) {
        cout << "TEST::TestFunc()" << endl;
    }
}

namespace MYDATA {
    // MYDATA 네임스페이스 소속
    void TestFunc(void) {
        cout << "DATA::TestFunc()" << endl;
    }
}

int main() {
    TestFunc(); // 묵시적 전역
    ::TestFunc(); // 명시적 전역
    TEST::TestFunc();
    MYDATA::TestFunc();

    return 0;
}

// C와 C++의 차이
// C++에서는 네임스페이스에 속하지 않은 식별자는 없습니다. 적어도 '전역' 네임스페이스에 속합니다.

① TestFunc()

 - 네임스페이스가 선언되어 있지 않습니다. 묵지적으로 전역 네임스페이스에 속하는 TestFunc()를 의미합니다.

② ::TestFunc()

- 똑같은 전역 네임스페이스에 속한 TestFunc()를 의미합니다. 다만 명시적으로 ::를 표시해줌으로써 명확하게 의미를 전달해줍니다.

 

만약 TEST, MYDATA 네임스페이스에 using을 사용하면 어떻게 될까요?

using namespace TEST;
using namespace MYDATA;
...

error C2668 : 'TestFunc' : 오버로드된 함수에 대한 호출이 모호합니다.

위처럼 모호성 관련 오류가 발생합니다.

5. 식별자 검색 순서

식별자가 선언된 위치를 검색하는 순서를 알아보도록 하겠습니다.

 

전역함수인 경우

① 현재 블록 범위

② 현재 블록 범위를 포함하고 있는 상위 블록 범위(최대 적용 범위는 함수 몸체까지)

③ 가장 최근에 선언된 전역 변수나 함수

④ using 선언된 네임스페이스 혹은 전역 네임스페이스. 단, 두 곳에 동일한 식별자가 존재할 경우 컴파일 오류 발생!

 

클래스 메서드인 경우

① 현재 블록 범위

② 현재 블록 범위를 포함하고 있는 상위 블록 범위(최대 적용 범위는 함수 몸체까지)

③ 클래스의 멤버

④ 부모 클래스의 멤버

⑤ 가장 최근에 선언된 전역 변수나 함수

⑥ 호출자 코드가 속한 네임스페이스의 상위 네임스페이스

⑦ using 선언된 네임스페이스 혹은 전역 네임스페이스. 단, 두 곳에 동일한 식별자가 존재할 경우 컴파일 오류 발생!

 

(1) 현재 블록 범위

가장 먼저 검색하는 범위는 식별자에 접근하는 코드가 속한 블록범위({ } 구간)입니다.

// 2.5.1 현재 블록 범위 - 식별자에 접근하는 코드가 속한 블록 범위

#include "pch.h"
#include <iostream>
using namespace std;

int nData(20);

int main(int argc, char *argv[]) {
    int nData(10);

    cout << nData << endl; // 현재 속한 지역변수인 10을 출력
    cout << argc << endl;

    return 0;
}

(2) 상위 블록 범위

접근자 코드가 속해 있는 범위에서 식별자 선언을 찾지 못한다면 중첩된 블록 범위의 상위(혹은 바깥쪽)로 검색 범위를 확장합니다. 대표적인 예시로 제어문이 있습니다.

// 2.5.2 상위 블록 범위 - 범위 검색의 확장

#include "pch.h"
#include <iostream>
using namespace std;

int main(int argc, char *argv[]) {
    int nInput = 0;

    cout << "11 이상의 정수를 입력하세요 : " << endl;
    cin >> nInput;

    if(nInput > 10) {
        cout << nInput << endl; // 8번째 행의 nInput
    }

    else {
        cout << "Error" << endl;
    }

    return 0;
}

 

(3) 가장 최근에 선언된 전역 변수

컴파일러는 1번 행부터 마지막 행까지 순차적으로 해석하지 않습니다. 즉, 전역변수가 네임스페이스보다 선언 위치가 더 우선 하지만 검색 순서는 상위 블록범위보다 후순위입니다.

// 2.5.3 가장 최근에 선언된 전역 변수 - 네임스페이스와 저역 변수의 검색 우선권

#include "pch.h"
#include <iostream>
using namespace std;

int nData = 200;

namespace TEST {
    int nData = 100;
    void TestFunc(void) {
        cout << nData << endl;
    }
}

int main() {
    TEST::TestFunc(); // 10번 행의 nData를 출력
    
    return 0;
}

 

그런데 만약 코드를 아래와 같이 수정하면 결과가 어떻게 될까요?

#include "pch.h"
#include <iostream>
using namespace std;

int nData = 200;

namespace TEST {
    void TestFunc(void) {
        cout << nData << endl;
    }
    int nData = 100;
}

int main() {
    TEST::TestFunc(); // 10번 행의 nData를 출력
    
    return 0;
}

결과는 200이 나옵니다. TestFunc()가 호출 되었을 때 nData는 5번행 전역변수인 nData밖에 없기 때문에 200이 출력됩니다.

 

마지막으로 아래와 같이 코드를 수정한다면 어떻게 될까요?

#include "pch.h"
#include <iostream>
using namespace std;

namespace TEST {
    void TestFunc(void) {
        cout << nData << endl; // 실행 당시 선언되어있는 전역변수 200이 출력
    }
    int nData = 100;
}

int nData = 200;

int main() {
    TEST::TestFunc();

    return 0;
}

 " 'nData': 선언되지 않은 식별자입니다. " 라는 컴파일 에러가 발생합니다. 즉, 전역 변수는 네임스페이스를 생각하지 말고 선언 순서를 생각해야 합니다.

(4) using 선언과 전역 변수

// 2.5.4 using 선언과 전역 변수 - using namespace 선언을 적용하기 전

#include "pch.h"
#include <iostream>
using namespace std;

int nData = 100;

namespace TEST {
    int nData = 200;
}

int main() {
    cout << nData << endl;

    return 0;
}

위 코드는 전역변수인 nData의 값인 100이 출력됩니다. 다른 예제를 하나 더 보겠습니다.

// 2.5.4 using 선언과 전역 변수 - TEST 네임스페이스에 using 선언 추가

#include "pch.h"
#include <iostream>
using namespace std;

int nData = 100;

namespace TEST {
    int nData = 200;
}

using namespace TEST;

int main() {
    cout << nData << endl;

    return 0;
}

// 오류 : nData가 모호합니다.

위 코드는 컴파일 오류가 발생합니다. main 함수에서 변수 nData에 접근하는 코드는 전역 네임스페이스에 속한 nData와 TEST 네임스페이스에 속해있는 nData 모두가 해당됩니다. 오류를 해결하려면 ::nData라고 범위지정 식별자를 기술하거나 TEST::nData 라고 정확히 네임스페이스를 기술해야 합니다.

반응형