
this 포인터
멤버 함수가 호출될 때 C++은 어떤 객체에서 호출되었는지 어떻게 알 수 있을까?
class Knight
{
public:
Knight(int hp)
: hp(hp)
{
}
int GetHp() { return hp; }
void SetHp(int InHp) { hp = InHp; }
void PrintHp() const { std::cout << hp << std::endl; }
private:
int hp{};
};
int main()
{
Knight K1{ 100 };
K1.SetHp(200);
K1.PrintHp();
return 0;
}
결과는 당연히 200이 콘솔창에 출력된다
여기서 컴파일러는 SetHp(), PringHp()가 호출될 때 Knight 클래스 타입 객체 K1에 대해 동작해야 한다는것을 알아야 한다, 이때 this 포인터를 활용한다
this 포인터란 암시적 객체(implicit object)의 주소를 담고 있는 const 포인터이다, 여기서 암시적 객체는 멤버 함수를 호출한 객체를 의미한다

따라서 위의 PrintHP()를
void PringHp() const { std::cout << this->hp << std::endl; }
이렇게 바꿀 수 있다 (this는 K1이 됨)
프로그램을 컴파일 할 때 컴파일러는 암시적 객체를 참조하는 모든 멤버 앞에 this->를 암시적으로 붙히게 된다
따라서 명시적으로 멤버함수 내부에서 this->로 멤버에 접근할 필요가 없는것이다
함수 호출에서 인자도 this를 암시적으로 받는다
K1.SetHp(200);
위 함수는 단순히 인자 200을 받는것 처럼 보이지만 실제로는 2개를 받게 된다, 컴파일 시 컴파일러가 재작성한다
Knight::SetHp(&K1, 200);
Knight::SetHp()로 변경되고 인자로 객체의 주소가 들어가게 된다, 그렇다면 함수 정의는 어떤식으로 재작성될까?
void Knight SetHp(Knight* const this, int InHp) { this->hp = Inhp; }
이런식으로 매개변수도 마찬가지로 this포인터를 받도록 재작성된다, 이때 this는 타입 뒤에오는 const이기 때문에 가리키는 주소 변경은 불가능하지만 주소를 타고 들어간 값은 변경될 수 있다
결론적으로 모든 non-static 멤버 함수는 함수가 호출된 객체를 참조하는 this포인터를 가지고 있다
일반적으로 명시적으로 this를 사용하는 경우는 잘 없지만 필요한 경우가 있다
void SetHp(int hp) { this->hp = hp; }
std::cout << "Kelvin" << MyName;
여기서도 체이닝이 발생한다, std::cout << "Kelvin"은 std::cout 스트림 객체인 *this를 return하고 계속해서 이어서 << operator를 사용하게 된다
(std::cout << "Kelvin") << MyName;
(std::cout) << MyName; //이렇게 됨
멤버함수에서도 같은 함수 체이닝이 발생할 수 있다
class Knight
{
public:
Knight(int hp)
: hp(hp)
{
}
void AddHp(int InHp) { hp += InHp; }
void SubHp(int InHp) { hp -= InHp; }
void MulHp(int InHp) { hp *= InHp; }
private:
int hp{};
};
int main()
{
Knight K1{ 100 };
K1.AddHp(10);
K1.SubHp(5);
K1.MulHp(2);
return 0;
}
이렇게 따로따로 멤버 함수를 호출하는 부분을 함수 체이닝을 사용하여 다음과 같이 변경이 가능하다
class Knight
{
public:
Knight(int hp)
: hp(hp)
{
}
Knight& AddHp(int InHp) { hp += InHp; return *this; }
Knight& SubHp(int InHp) { hp -= InHp; return *this; }
Knight& MulHp(int InHp) { hp *= InHp; return *this; }
private:
int hp{};
};
int main()
{
Knight K1{ 100 };
K1.AddHp(10).SubHp(5).MulHp(2);
return 0;
}
(예시일 뿐 좋은 방식은 아니다)
this는 항상 암시적 객체를 가리키기 때문에 nullptr 체크를 할 필요가 없다
class Knight
{
public:
Knight& AddHp(int InHp) { hp += InHp; return *this; }
Knight& SubHp(int InHp) { hp -= InHp; return *this; }
Knight& MulHp(int InHp) { hp *= InHp; return *this; }
void reset() { *this = {}; }
private:
int hp{};
};
int main()
{
Knight K1{};
K1.AddHp(10).SubHp(5).MulHp(2);
K1.reset();
return 0;
}
여기서 { *this = {}; }는 새로운 객체의 값을 {}로 초기화하고 암시적 객체를 덮어쓴다는 의미가 된다
non-const, const 멤버함수에서의 this
non-const 멤버함수에서의 this는 ClassName* const this이다
즉 this가 가리키는 대상 (주소)는 변경할 수 없지만 주소를 타고 들어간 원본값은 변경이 가능하다, 따라서 멤버 데이터 변경이 가능하다
const 멤버함수에서의 this는 const ClassName* const this이다, 따라서 this가 가리키는 대상(주소)와 주소를 타고 들어간 원본값 변경이 불가능하여 멤버 데이터 변경이 불가능한 것이다
따라서 const객체에서 non-const 함수를 호출하려고 하면 다음과 같은 에러를 뱉는다

non-const 멤버함수의 this는 Knight* const이다, 하지만 넘겨받은 암시적 객체는 const Knight이기 때문에 에러가 발생하는 것이다
그렇다면 왜 this는 &가 아니고 포인터일까?
(this가 추가될 때 그냥 & 문법이 없었음, 있었다면 참조이지 않았을까..?, 이미 Java나 C#은 this가 참조임)
class, header file
정의가 굉장히 간단한 멤버 함수들은 클래스 선언부에서 구현이 가능하다, 하지만 클래스가 더 복잡해지고 길어지게 되면 모든 멤버 함수의 정의를 클래스 선언부에서 구현하는건 좋지 않다
클래스를 사용할 때는 내부에서 어떻게 동작하는지는 중요하지 않고 인터페이스용으로 사용되어야 하기 때문에 클래스 선언부에 멤버 함수 구현을 하는건 좋은 방식이 아니다 (공개 인터페이스를 어지럽힌다)
따라서 멤버함수의 선언과 정의를 구분해야한다
class Knight
{
public:
void Attack(); //멤버 함수 선언 (멤버 함수 프로토타입)
private:
int hp{};
};
void Knight::Attack() //멤버 함수 정의
{
std::cout << "Knight attacks!" << std::endl;
}
int main()
{
Knight K1{};
K1.Attack();
return 0;
}
타입 클래스명::함수() { }로 함수를 정의한다, 생성자는 반환형이 없기 때문에 클래스명::생성자명() { }로 정의한다
멤버함수의 정의는 클래스 선언부 외부에서 가능하기 때문에 컴파일러에게 비멤버 함수가 아닌 특정 클래스 타입의 멤버함수를 정의하고 있다고 알려주기 위해 ::을 사용해야 한다
(Knight::Attack()으로 Knight클래스의 Attack() 멤버함수를 나타내야함)
Get/Set함수는 굉장히 간단하기 때문에 보통 클래스 선언 내부에서 관리한다
보통 함수의 선언은 .h, 정의는 .cpp에서 구현하며 원하는 헤더파일을 #include하여 해당 cpp에서 사용한다
(클래스 타입을 사용하기 위해서는 컴파일러는 전방선언만 보는게 아니라 전체 클래스를 봐야한다)
컴파일러가 멤버들이 올바르게 사용되었는지 확인하기 위해 멤버의 선언을 알아야 하고 타입의 객체를 인스턴스화 할 때 크기를 알아야 하기 때문이다

#include "Knight.h"
int main()
{
Knight K1{};
K1.Attack();
return 0;
}
원하는 클래스를 사용하려면 반드시 헤더파일 #include를 해주어야 하고 헤더파일과 연결되는 cpp파일도 컴파일이 되어야 linker가 멤버 함수 호출을 해당 정의와 연결이 가능하다
class는 타입이기 때문에 정의가 동일하다면 여러 파일에서 include되어도 ODR을 위배하지 않는다, 따라서 여러곳에서 #include해도 전혀 상관이 없다
하지만 단일 소스코드에서 클래스 정의를 두번 이상 하는건 ODR위반이다, 따라서 헤더가드나 #pragma once로 방지해야 한다
//헤더가드
#ifndef Knight_H
#define Knight_H
//...헤더구현
#endif
or
#pragma once
왜 헤더파일에 모든 함수의 정의를 넣지 않을까?
위에서 설명했듯 클래스 선언 내부에서 멤버 함수를 정의하면 클래스를 많이 어지럽힌다, 또한 헤더의 코드를 변경하게 되면 헤당 헤더를 #include하는 모든 소스파일을 전부 recompile해야 한다
(사소한 변경으로 인해 아주 많은 recompile이 실행될 수 있다)
(.cpp파일만 수정하게 되면 해당 .cpp만 컴파일하면 된다)
물론 반드시 헤더파일을 만들고 거기에 함수의 선언, cpp에는 정의를 하지 않아도 효과적일때가 있다
1. 아주 작은 클래스를 만들고 이 파일이 단 하나의 .cpp에서만 사용될 것 같을때는 그냥 .cpp에서 전부 처리한다
2. 헤더 전용 라이브러리를 만들때
3. 템플릿 클래스, 템플릿 함수를 만들 때
또한 함수의 기본 인자는 반드시 .h의 함수 선언부에만 작성한다 (정의부에 작성하지 않는다)
//Knight.h
void Foo(int a, int b = 100);
//Knight.cpp
void Knight::Foo(int a, int b);
만약 cpp 함수 정의부분에만 함수의 기본 인자를 작성한다면 다른 cpp파일에서 헤더 include를 보고 기본값을 모르기 때문에 에러가 발생한다
헤더 파일은 컴파일러가 작성 중인 프로그램이 구문적으로 올바른지 확인하는 데 필요한 선언을 제공한다
iostream과 같이 OS와 통신하는 라이브러리의 내부 구현은 미리 컴파일된 라이브러리 파일에 들어있어서 링크 단계에서 자동으로 링크된다(따라서 std::cout과 같은 경우 iostream헤더에 선언만 있다)
이때 헤더에는 진짜 코드 구현은 들어가있지 않다 (.h는 사용법만 알려주고 실제 동작 코드는 미리 컴파일된 라이브러리 파일 형태로 존재하며 링커가 알아서 링킹함)