일반 자료형처럼 구조체도 메모리에 할당되기 때문에 포인터로 그 구조체의 값을 읽고 쓸 수 있다.
struct A
{
int x;
};
int main()
{
A st1 = { 3 };
A* st2 = &st1;
}
이렇게 구조체도 별다르지 않게 구조체 자료형 뒤로 *를 붙여 포인터로 만들 수 있다. 하지만 어떻게 멤버에 접근해야하는가?
우리는 기존의 멤버 엑세스 연산자로 . 을 배웠다. 이를 사용하면 될 것 같긴하지만 포인터랑 같이 사용할 때는 주의해야한다.
(*구조체포인터).멤버이름
구조체포인터->멤버이름
우선 첫 번째 줄을 보자. 연산자의 우선순위에서 . 의 연산이 *(포인터)의 연산보다 하나 위의 우선순위를 갖기 때문에 ( ) 가 없으면 먼저 . 가 실행된다. 즉, 괄호가 없다면 1. 구조체포인터를 구조체라고 간주하고 멤버 연산자를 수행하며 2. 그 멤버변수를 주소처럼 생각하고 * 로 참조한다라는 이상한 문법이 되어버리며, 컴파일 오류가 난다.
우리는 1. 구조체포인터에 있는 주소를 찾아가서 2. 그 안에 있는 멤버 변수에 접근하고 싶기 때문에 현재 문법에서 연산자 우선순위에 역순으로 실행해야하므로 *구조체포인터를 괄호로 감싸서 먼저 실행되게 한다. 그리고 멤버 연산자로 멤버를 참조하여 읽고 쓸 수 있게된다.
첫 번째 방식도 올바르긴 하지만 ( ) 와 *, .를 항상 써야하므로 아래와 같은 두 번째 방식이 있다. 위의 문법과 아래 문법은 똑같이 수행된다.
struct A
{
int x;
};
int main()
{
A st1 = { 3 };
A* st2 = &st1;
cout << (*st2).x << endl;
cout << st2->x << endl;
(*st2).x = 7;
cout << st1.x << endl;
st2->x = 10;
cout << st1.x << endl;
}
// 출력 결과
// 3
// 3
// 7
// 10
함수가 호출될 때 매개변수들은 함수의 새로운 지역변수로 생성되면서 값이 복사되고 원본이 아닌 그 지역변수가 읽고 써지면서 값이 수정되기도 한다. 그리고 함수가 종료되면 지역변수가 사라지며, 원본은 값이 변경되어 있지 않고 그대로 보존되어 있다. 그렇다면 포인터의 주소값을 복사하여서 함수 내에서 그 주소값을 찾아가서 값을 수정하는 방식이 되지 않을까 라는 의문이 든다. 이는 실제로도 이렇게 동작하고 다음과 같은 코드를 보자.
struct Player
{
int hp;
int damage;
};
Player CreatePlayer(); // 값 전달 방식
int main()
{
Player player1;
player1.hp = 100;
player1.damage = 10;
player1 = CreatePlayer();
}
Player CreatePlayer() // 값 전달 방식
{
Player ret;
ret.hp = 120;
ret.damage = 20;
return ret;
}
Player 구조체를 만들었고 이 구조체의 멤버로 정수 변수 두 개가 선언되어 있다. 또한 CreatePlayer라는 함수를 선언하고, 이 함수는 Player 구조체를 반환한다.
CreatePlayer 함수 내부에서 Player 구조체를 하나 생성하고, hp, damage 멤버에 각각의 값을 대입한 후 이 구조체를 반환한다. 이 동작을 좀 더 자세히 보면, 반환값 자체도 지역변수처럼 생성되는듯 싶다
강의 중의 내용이고 StatInfo 구조체를 Player 구조체라고 생각하고 보면 될 것 같다. 결국에는 구조체 자체를 보기 위해서 하는거라 뭐 큰 차이는 없다. 주석에 써있는 내용 중 main 함수에는 temp라는 지역변수가 하나 만들어지고 CreatePlayer 함수에서는 지역변수 ret도 지역변수로 만들어진다. 그리고 함수 내부에서는 ret에 값을 하나 하나 넣어준 후 이 ret 의 값은 main 함수의 temp라는 방금 생긴 지역변수에 값이 복사된다. 그리고 나서 temp에서 또 player 라는 구조체로 값이 또 복사된다.
요약하자면 CreatePlayer 함수 내의 ret 구조체 변수 → main 함수에 임시로 생긴 temp → player 로 두 번 값이 복사된다. 구조체의 크기가 클수록(멤버 변수가 많을수록) 각 멤버로의 복사하는 비용이 증가한다는 것으로 비효율적인 방식이라고도 볼 수도 있겠다.
struct Player
{
int hp;
int damage;
};
void CreatePlayer(Player* player); // 주소 전달 방식
int main()
{
Player player2;
player2.hp = 130;
player2.damage = 30;
CreatePlayer(&player2);
}
void CreatePlayer(Player* player) // 주소 전달 방식
{
player->hp = 140;
player->damage = 40;
}
다음은 주소 전달 방식이다. 매개변수로 구조체의 포인터만 있으므로 구조체의 크기(int, int = 8byte)가 아닌 주소(* = 8byte)만 필요하다. 물론 현재로서는 구조체의 크기가 주소 체계의 크기와 일치하여 차이가 아예 없지만 만약 구조체가 int 멤버 변수 100개가 존재한다할 때 400Byte 이므로 392Byte의 차이가 날 것이다.
함수가 호출될 때 포인터 자료형 매개변수 하나만 생성되고, 이 변수 안에 들어잇는 주소를 찾아가 원본의 데이터를 변경하고 함수가 종료된다. 원본을 건들이기에 값 복사에 대한 연산이 이루어지지 않아 효율적이라고 바라볼 수 있지만, 원본을 건드는 것이기 때문에 위험요소가 있다.
void TestArgumentAddr(int* ptr)
{
cout << &ptr << endl; // 포인터 자체의 주소 : int*의 주소
cout << ptr << endl; // 포인터가 갖고 있는 주소값 : int*
cout << *ptr << endl; // 포인터가 갖고 있는 주소값을 찾아갔을 때의 값 : int
}
int main()
{
int num = 5;
int* ptr = #
cout << &ptr << endl; // 포인터 자체의 주소 : int*의 주소
cout << ptr << endl; // 포인터가 갖고 있는 주소값 : int*
cout << *ptr << "\n\n"; // 포인터가 갖고 있는 주소값을 찾아갔을 때의 값 : int
TestArgumentAddr(ptr);
}
// 출력 결과
// 0x16bb6f250
// 0x16bb6f25c
// 5
// 0x16bb6f228
// 0x16bb6f25c
// 5
// 두 포인터 변수가 갖고 있는 값은 같지만,
// main 함수 내의 지역변수 포인터의 주소와 함수 내의 매개변수 포인터의 주소는 서로 다른 것을 볼 수 있음
주소 전달 방식도 사실 지역 변수가 생성되어 그 지역 변수에 원본이 갖고 있는 주소를 전달해주는 것(이 말 자체가 주소 전달 방식이지만 좀 더 의미를 파악해보고자 쓰는 것) 뿐이다.
결국 지역 변수에 원본 값을 복사하여 사용하는 것이지만 복사된 값이 주소라는 값이기 때문에 원본에 접근할 수 있게 되는 것
포인터를 다룰 때, 주소를 잘못 건드릴 수도 있기 때문에 사용할 수 있는 방식이다. 매개변수의 자료형 옆에 &를 붙여서 사용하면 된다. 이는 원본의 값을 사용하는데 함수 내에서는 지역 변수처럼 문법을 사용한다.
참조 전달 방식은 어셈블리 레벨까지 들어가면 주소 전달 방식과 동일하게 작동한다.
struct Player
{
int hp;
int damage;
};
void CreatePlayer(Player& player); // 참조 전달 방식
int main()
{
Player player3;
player3.hp = 150;
player3.damage = 50;
CreatePlayer(player3);
}
void CreatePlayer(Player& player) // 참조 전달 방식
{
player.hp = 160;
player.damage = 60;
}
이처럼 일반 지역변수처럼 사용하는 느낌을 줄 수 있어서 편리하지만 지역변수라고 생각할 수 있는 착각 때문에 원본을 변경했다는 것을 깜빡할 수 있기 때문에 조심해야한다.
두 방식 모두 원본을 사용하는 것이기 때문에 무엇을 선택하든 상관없지만 여러 예시를 보고 따라 쓰자.
// 값 변경 불가
void CreatePlayer(const Player* player)
{
Player temp;
player = &temp; // 가능
cout << player->hp; // 읽는 것은 가능
// (*player).hp = 140; // 쓰는 것은 불가
// player->damage = 40; // 쓰는 것은 불가
}
// 주소 변경 불가
void CreatePlayer(Player* const player)
{
Player temp;
// player = &temp; // 불가
(*player).hp = 140; // 읽고 쓰기 가능
player->damage = 40; // 읽고 쓰기 가능
}
// 값 변경 불가
// 주소 변경 불가
void CreatePlayer(const Player* const player)
{
Player temp;
// player = &temp; // 불가
cout << player->hp; // 읽는 것은 가능
// player->hp = 140; // 불가
// player->damage = 40; // 불가
}
포인터를 활용하기에 원본의 값에 접근하지만 제약을 걸 수 있는 const와의 활용 예시이다.