C++ Friend 에 대해서
객체지향 프로그래밍의 핵심 원칙중 하나는 캡슐화, 그리고 데이터 은닉이다.
하지만 때로는 예외를 둘필요가 있는데, C++에서는 friend 키워드를 사용하여
은닉성의 예외를 두기도 한다.
Friend 키워드는 C++에서 제공하는 특별한 접근 권한 지정자로.
다른 클래스나 함수에게 자신의 private 또는 protected 멤버에 접근할 수 있는 권한을 부여한다.
해당 기능으로 코드의 효율성이나 유연성을 높이기 위해서 제공이 된다.
1. 전체 클래스 권한 부여.
class PlayLevel
{
// 다른 멤버들...
friend class DesignedLevel; // DesignedLevel 클래스에 접근 권한 부여
private:
int mComponentNum;
protected:
int calcBlockNum();
};
위에 예시에서 DesignedLevel 클래스는 PlayLevel의 모든 private 와 protected 멤버에 접근이 가능하다.
즉, 위에처럼 선언시 DesignedLevel이 PlayLevel의 보호된 모든 변수, 함수 멤버들에 접근을 허용하겠다는 일종의 선언이다.
2. 특정 멤버 함수에만 권한 부여.
class PlayLevel
{
// 다른 멤버들...
private:
friend void DesignedLevel::printsBlockInfoInPlayLevel(const PlayLevel& lv);
int mBlockNum;
protected:
int calcBlockNum();
};
위의 방법은 DesignedLevel 클래스의 printsBlockInfoInPlyLevel 함수만
PlayLevel의 private 및 protected 멤버에 접근 권한을 준 것이다.
3. 전역함수에게 권한 부여
class PlayLevel
{
// 다른 멤버들...
private:
friend void printsBlockInfoInPlayLevel(const PlayLevel& lv);
int mBlockNum;
protected:
int calcBlockNum();
};
2번과 달리 printsBlockInfoInPlyLevel가 전역 함수로 선언이 되었을때,
printsBlockInfoInPlyLevel 함수만 PlayLevel의 private 및 protected 멤버에 접근 권한을 준 것이다.
이쯤에서 알아보는 friend 특징
일단 중요한 특징들이 몇 가지 있다.
- 일방향성이다. friend 지정은 한 방향으로만 이루어진다.
즉, A가 B를 friend로 지정했다고 해서 B도 A를 friend로 간주하지 않는다는 뜻. - 명시적 지정이다. 명시적으로 지정한 대상들만 friend 관계가 형성된다.
- 이전이 불가능하다. friend 관계는 다른 클래스로 이전이 안된다.
- 상속이 안된다. 부모클래스의 friend 관계는 자식 클래스에 절대 상속되지 않는다.
- 개별 정의가 필요하다. friend 선언은 하나식 개별적으로 정의를 해야한다.
위에 5가지 주의점을 알고서 사용해야 컴파일 에러를 에방할 수 있다.
friend의 장단점
일단 장점을 알아보자.
기본적으로 코드의 활용성이 극대화 된다.
멤버 함수를 통하지 않고서 직접 멤버 변수에 접근가능해서 성능이 향상 될 수 있기 때문.
또한, 특정 기능을 구현할 때 코드를 좀 더 간결하게 사용하여 작성이 가능하다.
하지만 장점은 이게 끝.
단점을 나열하자면 ...
객체지향 프로그래밍의 은닉성 원칙을 훼손해버린다. 때문에 코드의 결합도가 높아져 유지보수가 어려워질 수 있으며,
과도한 friend 키워드 사용은 프로그램의 구조를 복잡하게 만들어 버릴 수 있다.
때문에 꼭 필요한 경우에만 제한적으로 사용하는 것이 좋다.
friend 실전 예제
#include <iostream>
#include <string>
using namespace std;
class Player; // 전방선언.
class OtherLevel
{
public:
void showPlayerName(Player & Player);
// 플레이어의 printName을 호출한다.
};
class Player
{
private :
friend class OtherLevel;
// OtherLevel 클래스에 전체 접근 권한을 부여한다.
string m_name;
void printName();
public:
Player(string name) : m_name(name) {}
//OtherLevel의 특정 함수(showPlayerName)에게만 접근 권한을 부여한다.
friend void OtherLevel :: showPlayerName(Player & Player);
};
void Player::printName()
{
cout << "Player Name: " << m_name << endl;
}
void OtherLevel::showPlayerName(Player& player)
{
player.printName(); // private 멤버 함수 호출 가능
}
int main()
{
Player player("박용석");
OtherLevel level;
level.showPlayerName(player); // "Player Name: 박용석" 출력
return 0;
}
Player 클래스는 OtherLevel의 클래스 전체와 OtherLevel::showPlayerName 함수를 friend로 지정했다.
즉, 위에 두 클래스와 클래스 내부 멤버함수는 friend로 선언 되었으므로
Player 클래스의 private 멤버함수인 printName()을 호출 할 수 있다.
이제 그러면 동적 메모리 관리에 대해서 알아보자.
동적 메모리 관리
C++에서 효과적인 메모리 관리는 성능 좋은 프로그램을 작성하는데 매우 필수적이다.
C#이나 여타 언어에 있는 가비지 컬렉터 기능이 없으므로 사용이 끝난 포인터 객체가
자동으로 소멸되지도 않아 일일이 다 없애줘야 하는게 C++이다.
어찌보면 좀 불편하다 느낄 수도 있다.(사실 그냥 개불편하다.)
하지만 이러한 점은 그만큼 성능 최적화 방면에서는 커스텀으로 개발하여
성능을 올릴 수 있다는 이야기이다.
특히 실행 시간에 필요한 메모리 크기를 결정해야 하는 경우에는 동적 메모리 할당이 매우 중요하다.
동적 메모리 할당의 필요성이 무엇일까?
아래 3가지의 경우가 있다.
- 프로그램 실행 전에 필요한 메모리 양을 알 수 없을 때.
- 사용자 입력이나 파일에서 읽은 데이터에 따라 메모리 요구량이 변할 때.
- 대량의 데이터를 효율적으로 관리해야 할 때.
이런경우 동적 메모리 할당이 매우 필연적으로 사용이 된다.
동적 메모리 할당을 소멸자로 해제 하는 법.
위에서 말했듯이 C++에서 동적으로 할당한 메모리는 반드시 메모리 해제를 해야
메모리 누수를 방지 할 수가 있다. 이때, C++에서는 소멸자를 이용해서 이를 처리한다.
class Inventory
{
public:
Inventory(int itemNum);
~Inventory(); // 소멸자
private:
Item* mItems = nullptr;
};
// 생성자
Inventory::Inventory(int itemNum)
{
mItems = new Item[itemNum];
}
// 소멸자
Inventory::~Inventory()
{
delete[] mItems; // 배열 메모리 해제
}
위에 방법처럼 소멸자 함수(~클래스 이름())의 안에서 delete 키워드를 사용하여 메모리를 해제한다.
소멸자의 특징은?
소멸자의 특징은 일단 클래스 앞에 틸드(~)기호가 붙는다.
또한 매개변수를 받으면 안되며, 객체가 소멸될때 자동으로 호출된다.
주로 메모리 해제나 리소스 반환 코드를 포함한 점이 소멸자의 특징이다.
깊은 복사와 얕은 복사의 관계, 그리고 문제점
동적으로 할당한 메모리를 가진 객체를 복사 할 때 특히 주의를 해야할 점이 있다.
컴파일러가 자동으로 생성하는 복사 생성자와 대입연산자는 얕은 복사(shallow copy)를 수행하기 때문.
일단 얕은 복사의 문제점에 대해서 살펴보자.
1. 얕은 복사의 문제점은?
int main()
{
Inventory playerInven1(10);
{
Inventory playerInven2 = playerInven1; // 얕은 복사 발생
} // playerInven2가 소멸되고 mItems 메모리 해제
} // playerInven1 소멸 시 이미 해제된 메모리를 다시 해제하려고 시도 -> 오류 발생
해당 코드는 얕은 복사의 예시이다.
일단 얕은 복사는 값만 복사하기 때문에 두 객체가 같은 메모리를 가리키는 문제가 발생한다.
이로 인해 한 객체가 소멸될 때 메모리를 해제한다면,
다른 객체는 이미 해제 되어버린 메모리를 참조하게 되는 댕글링 포인터(dangling pointer) 문제가 발생한다.
int main()
{
Inventory playerInven(10);
Inventory npcInven(5);
playerInven = npcInven; // 얕은 복사로 인한 메모리 누수 발생
}
또 다른 문제점이라면 위에 코드의 경우 playerInven이 가리키던 메모리는 해제가 되지 않고
접근이 불가능 하게 되어 메모리 누수가 발생한다.
2. 얕은 복사의 해결책 : 복사 생성자와 대입 연산자 오버로딩
위와 같은 문제를 해결하기 위해서 깊은 복사를 수행하는 복사 생성자와 대입 연산자를 직접 구현을 해야한다.
#include <iostream>
using namespace std;
class Item
{
//구현이 되어있다고 치고...
};
class Inventory
{
public:
//생성자
Inventory(int itemNum);
//복사 생성자
Inventory(const Inventory& other);
//소멸자
~Inventory();
//복사 할당 연산자
Inventory & operator=(const Inventory& other);
private:
Item* mItems;
int mSize; // 현재 아이템 개수 저장.
};
//생성자 정의
Inventory::Inventory(int itemNum) : mSize(itemNum)
{
mItems = new Item[itemNum];
}
//복사 생성자
Inventory::Inventory(const Inventory &other) : mSize(other.mSize)
{
mItems = new Item[mSize]; //새로운 메모리를 할당하고
for (int i = 0; i < mSize; i++)
{
mItems[i] = other.mItems[i]; // 개별 아이템들을 복사한다.
}
}
Inventory & Inventory::operator=(const Inventory &other)
{
if (this == &other) return *this; // 자기 자신과의 대입 방지
//기존 메모리 해제
delete[] mItems;
//새 메모리 할당 및 복사
mSize = other.mSize;
mItems = new Item[mSize];
for (int i = 0; i < mSize; i++)
{
mItems[i] = other.mItems[i];
}
return *this; //자기 자신 반환(연쇄 대입 지원함)
}
Inventory::~Inventory()
{
delete[] mItems;
}
int main()
{
Inventory playerInven(10);
Inventory npcInven(5);
playerInven = npcInven; // 이제 안전한 깊은 복사 수행
}
위와 같은 코드로 복사 생성자와 대입연산자를 구현한다.
중요한 포인트를 지정한다면.
복사 생성자의 경우 새로운 메모리를 할당하고, 원본 객체의 내용을 새 메모리에 복사한다.
대입 연산자의 경우 자기 자신과의 대입을 확인하고 기존 메모리를 해제한다.
이후 새 메모리를 할당하고 내용을 복사한다.
마지막으로 자기 자신을 반환시킨다.
결론
C++의 friend 키워드와 동적 메모리 관리는 모두 꽤나 좋은 기능들을 가지고 잇지만,
매우 신중하게 사용하며 추후관리까지 완벽히 해야한다.
정리를 하자면 friend는 은닉성을 꺠는 대신 유연성을 가지게 한다. (줄건 줘야 한다)
동적메모리를 사용하는 경우 소멸자, 복사 생성자, 대입 연산자를 올바르게 구현해야한다.
'Game DevTip > C++' 카테고리의 다른 글
8. C++로 GameObjectPool을 구현 해보자. (0) | 2025.05.21 |
---|---|
6. C++ string 타입에 대해서 (0) | 2024.12.05 |
5. C++ auto & decltype에 대해서. (0) | 2024.12.05 |
4. C++ Typedef(타입 별칭)에 대해서 (0) | 2024.12.05 |
3. C++ constexpr에 대해서 (1) | 2024.12.05 |
댓글