프로그래밍 공부
작성일
2024. 2. 6. 15:56
작성자
WDmil
728x90

8.3 메서드의 종류

데이터 멤버에 여러 종류가 있듯이 메서드 또한 여러 종류가 있다.


8.3.1 static 메서드

데이터 멤버의 경우와 마찬가지로 메서드도 특정 클래스의 모든 객체에 공통적으로 적용되어야 할 때가 있다. static 메서드의 선언 방법은 데이터 멤버와 같다. 예를 들어 SpreadSheetCell 클래스의 편의 메서드 stringToDouble() 과 doubleToString()을 생각해보자, 이 두 메서드는 객체에 종속되는 부분이 없다. 따라서 static으로 선언해도 된다. 다음은 이 두 메서드를 static으로 선언한 SpreadSheetCell 클래스 정의이다.

	static std::string doubleToString(double val);
	static double stringToDouble(const std::string& str);

이 두 메서드는 객체 데이터에 변화를 주지 않기 떄문에 const로 선언했었다. 하지만, static메서드를 선언하려면 const를 뺴야 한다. static 메서드는 객체에 묵이지 않기 때문에 

'객체에 변경을 가하지 않는다' 라는 시맨틱이 더 이상 적용될 수 없기 때문이다. static으로 선언하더라도 구현 부분은 전혀 바뀌지 않는다.

 

그리고 구현 부분에서는 static을 안붙여도 된다. static 메서드는 연결된 객체가 없음으로 코드 구현부에서 this 포인터를 이용할 수 없다. 단, static 데이터 멤버는 이용할 수 있다.

 

사실 static 메서드는 일반 함수와 같다. 클래스의 private와 protected멤버에 접근할 수 있는 권한이 있다는 점이 다를 뿐이다. this포인터는 없지만 같은 클래스 타입의 객체를 파라미터나 전역 변수 등으로 접근할 떄 그 접근 권한이 클래스 메서드에 준하여 적용된다.

 

같은 클래스 의 메서드라면 static 메서드를 보통의 메서드처럼 호출할 수 있다. 이 때문에 위 두개의 메서드가 static으로 바뀌어도 이 메서드를 이용하는 다른 메서드의 구현부는 바뀔 필요가 없다. 같은 클래스 내 메서드가 아닌 바깥에서 이 메서드를 호출해야 한다면 스코프로 지정 연산자::를 이용해야 한다. 이때 제한자(private, protected, public)에 따른 접근 제한이 그대로 적용된다.

 

만약, staingToDouble()과 doubleToString() 메서드를 public으로 선언하면 어디에서나 호출할 수 있다.


8.3.2 const메서드

값이 절대 바뀔 수 없는 객체를 const 객체라고 한다. 객체에 대한 참조나 포인터 변수를 const로 선언하고 객체의 메서드를 호출하면 해당 메서드가 객체의 데이터 값을 바꾸지 않는다는 보증이 있어야 컴파일러가 정상적으로 컴파일을 진행한다.

 

이때 메서드가 객체의 데이터 값을 바꾸지 않는다는 선언이 바로 const다, 다음은 SpreadSheetCell의 메서드 중 객체에 변경을 하지 않는 메서드에 대해 const를 선언한 예 이다.

	double getValue() const;
	const std::string& getString() const;

 

double SpreadSheetCell::getValue() const
{
    return mValue;
}


const std::string& SpreadSheetCell::getString() const
{
    return mString;
}

const 제한자는 메서드 프로토타입 선언에 포함되기 때문에 구현부에서도 똑같이 적용해야 한다.

 

메서드를 const로 선언하는 그 메서드의 사용자에게 메서드 호출 때문에 객체의 데이터값이 바뀌지 않는 다는 것을 보증해주는 계약과도 같다. 만약 const로 선언한 메서드 안에서 객체의 데이터 멤버를 변경하려 하면 컴파일러가 오류 메시지를 출력할 것이다.

 

그리고 이전 절의 doubleToString()이나 stringToDouble()과 같은 static메서드는 클래스 공통으로 연계되는 객체가 없으므로 cosnt 선언이 무의미하다. 이 때문에 static메서드에는 const제한자를 적용할 수 없다. const메서드의 작동 방식은 메서드 내에서 접근하는 모든 데이터 멤버를 const로 취급하는 방법으로 구현된다. 그래서 멤버를 변경하려 할 때 컴파일러가 에러를 발생시킨다.

 

cosnt가 아닌 객체에 대해서는 const여부와 관계없이 모든 메서드를 호출할 수 있다. 하지만 const객체에 대해서는 const 메서드만 호출할 수 있다.

SpreadSheetCell myCell(5);
cout << myCell.getValue() << endl; // 정상
myCell.setString("6") // 정상
const SpreadSheetCell& anotherCell = myCell;
cout << anotherCell.getValue() << endl; // 정상
anotherCell.setString("); // non-const 메서드 호출, 컴파일 에러 발생!

가능하다면 객체를 변경하지 않는 모든 베서드에 const 제한자를 적용하여 const 객체에서도 호출할 수 있게 하는것이 바람직하다.

 

비록 const 객체라 하더라도 객체의 소멸은 가능하다. 어떤 경우라도 소멸자에 const제한자를 적용하는 것은 좋은 생각이 아니다.


8.3.2.1 mutable 데이터 멤버

메서드가 객체의 특정 데이터 멤버를 변경하기는 하지만 논리적으로는 const인 경우가 있다. 즉, 특정 데이터의 변경이 메서드의 사용자 쪽에서 볼 때는 전혀 영향이 없지만, 여하튼 코드 상으로는 변경이기 떄문에 컴파일러가 해당 메서드를 cosnt로 선언하지 못하게 할 수 있다.

 

예를 들어 스프레드시트 프로그램의 사용자 이용 성향을 프로파일링 해보기 위해 SpreadSheetCell 클래스의 getValue()나 getString()메서드에 접근 카운터를 구현했다고 해보자.

 

이러한 카운터는 의미상 객체의 데이터에 변화를 주지 않지만, 컴파일러 측면에서 볼 때는 객체의 데이터 멤버를 변경하는 행위와 구별이 안 된다. 이 때의 해결책이 mutable 속성이다. mutable 속성을 접근 카운터 멤버 변수에 적용하면 컴파일러는 해당 변수의 변경이 메서드의 const에 영향을 미치지 않는 것으로 간주한다.

	mutable int mNumAccesses = 0;
double SpreadSheetCell::getValue() const
{
    mNumAccesses++;
    return mValue;
}


const std::string& SpreadSheetCell::getString() const
{
    mNumAccesses++;
    return mString;
}

8.3.3 메서드 오버로딩

앞서 살펴보았듯이 클래스 생성자는 이름이 같더라도 파라미터의 타입이나 개수가 다르다면 여러개 정의할 수 있다. 그런데 이러한 정의 방식은 일반 메서드나 함수에도 그대로 적용된다. 파라미터만 다르면 이름이 같은 메서드 또는 함수를 얼마든지 만들 수 있다. 이렇게 이름이 같은 메서드 또는 함수를 파라미터만 달리하여 정의하는 것을 오버로딩(overloading)이라고 한다.

 

예를 들어 SpreadSheetCell 클래스의 setString()과 setValue()메서드를 다음처럼 모두 set()메서드로 이름을 바꿀 수 있다.

	void set(double inValue);
	void set(const std::string& inString);

메서드의 이름이 바뀌었지만 메서드 구현부는 전혀 바뀐것 이 없다. 단, 기존에 setString()과 setValue()메서드를 사용하던 코드에서 메서드 이름을 set()으로 변경해야 한다.

 

컴파일러가 set()메서드의 호출을 만나면, 인자의 데이터 타입과 개수를 보고 적합한 메서드로 메핑해준다. 즉, 인자가 string타입이면 string타입 파라미터를 갖는 set()함수를, 인자가 double이면 double타입을 갖는 set() 함수를 호출하게 한다. 이러한 메커니즘을 오버로드 지정(overload resolution)이라고 한다.

 

메서드 오버로딩을 getValue()와 getString()에도 적용하여 get()으로 통일하고 싶을 수 있다. 하지만 그렇게 바꾸면 컴파일이 안된다.

 

C++에서는 파라미터가 같고 리턴 타입만 다른 메서드나 함수에 오버로딩을 허용하지 않는다. 왜냐하면 리턴 타입의 차이만으로는 컴파일러가 오버로드 지정을 할 수 없는 상황이 많기 때문이다. 예를 들어 메서드나 함수를 호출하면서 리턴값을 받아서 사용하지 않고 그냥 버리는 경우 컴파일러 입장에서는 어떤 메서드 또는 함수를 지정해야 할지, 프로그래머의 의도를 알 방법이 없다.

 

const 제한자에 기반한 오버로딩도 가능하다. 예를 들어 두 개의 이름도 같고 파라미터도 같은 메서드 중 어느 한쪽만 const라면, 그 메서드를 호출한 객체의 타입이 const냐 아니냐에 맞춰서 컴파일러가 호출할 메서드를 지정할 수 있다.

 

특정 파라미터 타입의 오버로드 메서드가 사용되지 않도록 명시적으로 삭제할 수도 있다.

class MyClass
{
	public:
    	void foo(int i);
};

foo() 메서드는 다음과 같이 호출될 수 있다.

MyClass c;
c.foo(123);
c.foo(1.23);

두 번째 호출에서 컴파일러는 double 타입 값 1.23 을 정수 1로 타입 캐스팅하여 foo(int i)를 호출한다. 이떄 컴파일러는 타입 캐스팅이 일어남을 경고하겠지만, 프로그래머 의도에 반하는 결과물일 수도 있다. 이러한 상황을 방지하기 위해 double타입 파라미터의 foo() 메서드에 대해 다음과 같이 명시적으로 삭제 선언을 할 수 있다.

class MyClass
{
	public:
    	void foo(int i);
        void foo(double d) = delete;
};

이렇게 선언하면 double 타입 인자로 foo()메서드가 호출될 때 컴파일러 int로 타입 캐스팅 하는 대신 에러 메시지를 출력한다.


8.3.4 디폴트 파라미터

디폴트 파라미터는 메서드 오버로딩과 비슷한 기능이다. 함수나 메서드의 프로토타입을 선언할 떄 각 파라미터에 디폴트 값을 지정할 수 있다. 만약 사용자가 해당 인자를 직접 제공하면 디폴트 값은 무시된다. 만약 해당 인자를 공란으로 하여 호출하면 디폴트값이 자동으로 적용된다.

 

디폴트 파라미터는 가장 마지막(가장 오른쪽의) 파라미터 부터 시작해서 파라미터 건너뜀 없이 연속으로 적용할 수 있다. 그렇게 하지 않으면 메서드가 호출될 떄 어느 파라미터에 디폴트 값을 적용할 지 컴파일러가 판단할 수 없다.

#pragma once
#include "SpreadSheetApplication.h"
#include "SpreadSheetCell.h"

class SpreadSheet
{
public:
	SpreadSheet(int inWidth, int inHeight,
		const SpreadSheetApplication& theApp);
	SpreadSheet(const SpreadSheetApplication& theApp,
		int inWidth = kMaxWidth, int inHeight = kMaxHeight);

파라미터에 디폴트 값을 지정해도 생성자의 구현부는 바뀔 것이 전혀 없다. 디폴트값은 메서드나 함수의 프로토타입을 선언할 떄만 지정할 수 있고 구현부 정의에서는 지정할 수 없다.

 

생성자가 디폴트 파라미터를 가졌기 떄문에 단 하나의 생성자 만으로도 다음과 같이 인자가 하나인 것, 둘인 것, 셋인 것을 모두 이용할 수 있다.

SpreadSheetApplication theApp;
SpreadSheet s1(theApp);
SpreadSheet s2(theApp, 5);
SpreadSheet s3(theApp, 5, 6);

만약 생성자의 모든 파라미터에 대해 디폴트 값이 지정되었다면 아무런 인자 없이 객체의 생성자를 호출할 수 있게 되어 디폴트 생성자 역할이 가능하다.

 

즉, 디폴트 생성자를 따로 만들지 않아도 디폴트 파라미터를 통해 디폴트 생성자 역할까지 모두 맡길 수 있다. 만약, 이 상태에서 디폴트 생성자를 별도로 정의하면 인자가 없는 객체 생성 시 컴파일러가 어느 메서드를 이용해야 할지 알 수 없으므로 에러 메세지가 발생한다.

 

디폴트 파라미터로 할 수 있는 일은 메서드 오버로딩으로도 할 수 있다. 디폴트 파라미터 대신 서로 다른 파라미터를 가지는 세 개의 생성자를 만들면 된다. 하지만 디폴트 파라미터를 이용하면 생성자를 하나만 만들어도 된다. 둘 중 더 선호하는 방법을 이용하면 된다.


 8.3.5 inline 메서드

C++에서는 메서드나 함수를 별도의 분리된 코드 블록으로 호출하는 대신, 호출 지점에 따로 메서드나 함수의 바디를 옮겨놓아 호출 오버헤드를 줄이는 방법을 제공한다. 이러한 방법을 인라이닝(inlining)이라고 하며, 인라이닝되는 메서드나 함수를 inline 메서드 또는 linline함수라고 한다. 인라이닝은 #define 매크로를 이용해서 코드를 삽입하는 방법보다 훨씬 안전하다.

 

inline 메서드나 함수는 메서드나 함수의 프로토타입을 선언할 때 inline 키워드를 붙여서 지정할 수 있다. 예를 들어 SpreadSheetCell에 있는 get과 set 메서드를 inline화 하고 싶을 때 는 다음과 같이 메서드를 정의한다.

inline void SpreadSheetCell::set(double inValue)
{
    mValue = inValue;
    mString = doubleToString(mValue);
}

inline void SpreadSheetCell::set(const std::string& inString)
{
    mString = inString;
    mValue = stringToDouble(mString);
}

이제 get과 set 메서드 호출을 만날 때 마다 컴파일러가 메서드 호출 코드 대신 메서드 바디를 해당 위치에 삽입한다.

 inline키워드는 컴파일러에 힌트를 주는 정도의 역할을 한다. 컴파일러가 인라이닝이 오히려 성능에 해가 된다고 판단하면 inline 키워드를 무시할 수도 있다.

 

한가지 제약 사항은 inline이 제대로 동작하기 위해서는 해당 메서드와 함수의 바디가 inline할 소스 파일에서 보여야 한다는 것 이다. 상식적으로 생각할 떄 컴파일러가 함수 호출 대신 그 바디를 삽입할 수 있으려면 당연히 그 바디의 내용을 해당 코드의 컴파일 타임에 가지고 있어야 한다.

 

그러므로 inline함수는 헤더 파일에 해당 프로토타입과 함께 정의부가 들어간다. 클래스 메서드라면 클래스 정의가 있는 .h파일에 정의되어 있어야 한다.

 

진보된 C++컴파일러는 inline 메서드를 헤더 파일에 정의하지 않더라도 인라이닝이 가능하다. 마이크로소프트 비주얼C++는 링크 타임 코드 생성 기능을 통해 헤더파일에 정의되어 있지 않은 inline함수는 물론 심지어 inline으로 선언되지 않은 경우에도 작은 크기의 함수 바디를 자동으로 인라이닝 해준다. 이러한 컴파일러를 사용할 여건이 된다면 굳이 inline 을 위해 헤더파일에 메서드 정의를 넣지 말고 cpp파일을 이용하는 것이 좋다. 이렇게 하면 인터페이스가 훨씬 간결해지고, 구현 내용을 노출하지 않기 떄문에 디자인적으로 더 안전하다.

 

C++는 inline 키워드를 사용하지 않고도 메서드를 인라이닝 할 수 있는 방법을 제공한다. 메서드의 구현부를 클래스 정의부에 따로 만드는 대신, 클래스 정의 안에서 바로 구현부를 정의하면 인라이닝이 적용된다.

	double getValue() const {     mNumAccesses++; return mValue;	}
	const std::string& getString() const {     mNumAccesses++; return mString;	}

디버거를 이용해서 라인 단위로 디버깅을 할 때 만약 함수가 인라이닝되어 있다면 인라이닝된 함수로 점프한다. 이것은 실제로 함수 호출이 일어난 것이 아니라 프로그래머를 돕기 위해 디버거가 지능적으로 동작한 것이다. 실제고 코드가 인라이닝되었는지는 디버거의 어셈블리 뷰를 통해 확인할 수 있다.

 

만약 다소 오래된 버전의 디버거를 이용한다면 인라이닝된 코드에 대해 라인 단위 디버깅을 제대로 지원하지 않을 수 있다.

 

많은 C++프로그래머가 inline 메서드의 적용 메커니즘을 이해하지 못한채 사용하고 있다. inline으로 선언했더라도 실제로 인라이닝될지 안될지는 상황에 따라 다르다. 컴파일러는 작은 크기의 메서드와 함수만 인라이닝 한다. 만약 적절치 않은 메서드나 함수를 인라이닝하려 하면 컴파일러가 무시해버린다.

 

최신 컴파일러들은 코드의 크기같은 여러 가지 요인을 감안하여 메서드를 인라이닝할지 판단한다. 만약 비용대비 효과가 별로 없다면 인라이닝 하지 않는다.

728x90