[C++ 기초] 파일 경로, 포함 디렉터리, 순환 참조, extern 변수, 클래스 전방 선언, 상속

라멘커비·2023년 12월 27일
0

CPP 입문

목록 보기
11/25
post-thumbnail

🥔파일 경로

게임을 설치할 때 경로를 바꿀 때가 있다. 내 게임의 이미지를 절대경로로 설정할 수는 없다.

  • 절대경로 : D:\GM\Cpp
  • 상대경로 : ...로 표현하는 경로
    • . (.\) : 현재 위치 경로
    • .. (..\): 현재 위치에서 상위 디렉터리로 이동
    • ex) D:\GM\Cpp..\ == D:\GM\

이런 파일경로를 include문에서도 사용한다.

#include "ConsoleScreen.h"

위 include문에는 현재 위치 경로가 생략된 것이다.

#include ".\ConsoleScreen.h"

include문에서 상대경로 가능.

🥔include문에서 <>""의 차이

  • <> : 비주얼 스튜디오에 '포함 디렉토리'에 존재하는 파일을 사용하겠다.
  • "" : 파일이 존재하는 폴더의 경로를 사용하겠다.

포함 디렉터리

포함 디렉터리는 헤더의 위치이다.
<>로 include했을 때 비주얼 스튜디오는 포함 디렉터리 중에서 헤더 파일을 찾는다.

(비주얼 스튜디오에서는 특정 프로젝트마다 설정을 세팅할 수 있는데 다른 프로젝트에서는 적용되지 않는다.)
이상한 파일 경로가 있는데 이것은 비주얼 스튜디오에서 사용하는 매크로이다. 특정 문자나 기호를 어떤 경로로 생각하는 것이다. 하나의 매크로가 하나의 경로를 의미하지는 않는다. 여러 개 있을 수 있다.

;로 경로와 경로를 구분한다. 실수로 지우면 안된다. 의미있는 경로를 추가했으면 꼭 붙여야 한다.

$(VC_IncludePath)의 값 :

C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\include
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\atlmfc\include
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\VS\include

컴파일 전체 흐름

전처리기 > 컴파일러 > 어셈블러 > 링커

  • 전처리기 : 주석삭제, 코드치환(include).
  • 컴파일러 : (헤더는 컴파일 주체가 아님, cpp파일을 컴파일 하는 데 헤더 내용이 포함되게 됨)
    우리 눈에 보기에는 같은 이름인덴 같은 이름이 아닌 애들(오버로딩) 처리.
    중간 파일 .obj 파일 만들기 (=> 실행 메모리 크기 결정됨.)
  • 어셈블러 : 진짜 기계어 코드로 변경.
  • 링커 : 각 .obj파일에서 기계어로 번역된 것을 모두 하나로 모음. → 결과물 .exe파일

🥔헤더 중복 관련

헤더 내에서 불필요한 헤더 include는 피해야 한다. 순환참조나 변수 재정의의 원인이 될 수 있다.
또한 헤더쪽에 구현이 있는 건 문제가 생길 여지가 있기 때문에 최대한 분할해야 한다. 헤더에서 클래스를 사용하면 헤더와 구현의 완전한 분리를 할 수 없다.

순환 참조 주의

#include에서는 순환참조를 주의해야 한다.

A 클래스의 헤더에서 #include "B.h"
B 클래스의 헤더에서 #include "A.h"

→ A를 만들려면 B가 필요하고 B가 먼저 컴파일되면 A가 그걸 사용해야 한다. 그런데 B도 A가 필요하다.

컴파일러 :

A를 만들려면 B가 필요
B를 만들려면 A가 필요
A를 만들려면 B가 필요
B를 만들려면 A가 필요
...

많은 클래스끼리 순환참조가 생겨서 에러가 나면 찾기 어렵다.
근데 cpp파일에서 참조하는 건 됨!!

순환 참조 방지 방법

  • #pragma once : cpp파일별로 적용된다. 헤더 중복을 파일 단위로 막아준다.(프로젝트 전체에 이 헤더가 한 번 있게 만드는 게 아님)
    선언은 여러 번 가능하고 구현은 중복되면 안 된다. 따라서 헤더는 중복되어도 에러가 나지는 않는다.

  • #ifndef HEADERNAME_H / #define HEADERNAME_H : #pragma once와 동일한 역할을 한다. HEADERNAME_H가 define되지 않았다면 define하는 것.

#ifndef HEADERNAME_H
#define HEADERNAME_H

// 헤더 파일 내용

#endif

헤더에 전역 변수가 있을 때 헤더가 중복되면 같은 변수가 재정의 되었다는 에러가 난다. 어제 게임 코드에서는 전역변수가 const로 선언되었기 때문에 에러가 안 난 것이다.
어제 코드에서 Math.h(int2 클래스 있는 곳)와 같은 근본적인 헤더는 여기저기에서 사용하기 때문에 컴파일 과정에 헤더가 중복될 수 있다. 따라서 헤더에 전역변수가 있었다면 같은 이름의 변수가 여러 개 있다고 인식되는 것이다.

(cpp파일을 include하는 것은 금기..)

extern 변수

헤더에 전역 변수 쓰는 방법.
extern : "이런 변수가 있어, 절대 초기화하면 안돼", 다른 파일에서 extern 변수 사용 가능.

// in Global.h
extern int Value; // int Value가 있을 거야.

헤더에서 extern 키워드로 선언한 전역변수는 다른 파일에서 그 전역변수를 정의해서 쓸 수 있다.
(헤더에 extern 키워드로 전역 변수를 선언해두면 나중에 cpp파일에서 정의해서 사용할 수 있다.)

// in Main.cpp
int Value = 10;

근데 꼭 헤더와 cpp파일 사이에 가능한 게 아니고 .cpp 사이에서도 가능한듯.

  • 헷갈려서 돌려본 예시
    외부의 전역 변수를 가져온다고 봐야하나?

Test.cpp

int TestVar = 10;
class Test {

};

Main.cpp

#include <iostream>
extern int TestVar;
int main() {
	printf("%d", TestVar);
	return 0;
}

출력 결과

클래스 전방 선언 (헤더 추가없이 선언하기)

클래스의 헤더에서 다른 클래스의 헤더를 include해야 하는 경우는 2가지가 있다.
1. (내일 배울)상속.
2. 값형으로 쓸 때.

  • 아래 경우가 값형으로 써야하는 경우.
#include "Monster.h"
class Player{
    Monster NewMonter; // Monster의 크기를 알아야 Player의 크기도 정의가 되기 때문.
};

Monster의 크기를 알아야하고 크기를 알려면 멤버변수들을 알아야 한다.

전방 선언

참조형(포인터나 레퍼런스)으로 사용하면 헤더를 include하지 않아도 쓸 수 있다.
모든 포인터와 레퍼런스는 8바이트이고, "이런 클래스가 있을 것이다" 라는 의미만 있기 때문에 선언 가능하다.
class 클래스이름* 으로 전방 선언이 가능하다.
class 클래스이름;으로 전방 선언하고 선언은 참조형으로 해도 된다.

//#include "Monster.h"
class Player{
    class Monster* NewMonter; // 이러이러한 클래스가 있을거다 라는 뜻, 포인터형이니까 크기는 8바이트 고정
};

🥔코드 중복 해결

Player, Monster는 모두 Att, Hp가 필요하다. 불합리적.
문법의 발전은 프로그래밍 코드의 재활용성을 증가시키기 위해서 발전해왔다.
상속은 코드 재활용성을 위해 필요하다.

상속

" xxx분야의 클래스들이 공통적으로 가져야할 기능을 만든다. "
그 기능을 물려받기를 원하는 클래스이름 : 접근제한자 부모클래스이름 으로 사용 가능하다.
상속은 무조건 헤더를 추가해야 한다.
이유? 자식 클래스의 크기를 알아야 함 -> 부모를 상속받았으니 부모 크기를 알아야 함.

#include <iostream>

// 싸우는 애들이 공통적으로 가져야할 기능을 가진다.
class FightUnit {
public:
	int Att = 10;
	int Hp = 100;
	void Damage(int _Att) {
		Hp -= _Att;
	}
};

class Player : public FightUnit {

};

class Monster : public FightUnit {

};
int main() {
	Player NewPlayer = Player();
	NewPlayer.Att = 20;
	NewPlayer.Damage(20);

	Monster NewMonster = Monster();
	NewMonster.Att = 10;
	NewMonster.Damage(20);

	return 0;
}

상속의 접근 제한자

(근디 선생님은 상속에서 접근 제한자에 public말고는 쓰실 일이 없었다고 함.)

상속 접근 제한자와 기존 접근 제한자를 비교해서 더 좁은 접근 제한자로 만든다.
부모에서 public나 protected이었던걸 자식에 protected나 private(상속 접근제한자)로 가져오는 것이 가능하다는 듯?

  • public
외부자식멤버
OOO
  • protected
외부자식멤버
XOO
  • private
외부자식멤버
XXO

그냥 상속 접근 제한자로 바뀐다고 생각하면 된다. 한 번 더 상속을 하면 의미가 생길 수 있지만 보통 public 상속만 사용한다.

🥔과제

과제 1. 부모클래스, 자식클래스 만들어 상속 후 자식클래스 크기 확인해보기.

  • (private 변수도 사용할 수는 없지만 상속되기 때문에 크기에 포함된다)

과제 2. ConsoleObject라는 클래스 만들어서 Player, Bullet 클래스에 반복되는 내용을 넣고 상속시킨다.

  • Player 와 Bullet 클래스에서 겹치는 기능 : 함수 GetPos(), GetRenderChar(), 변수 Pos, RenderChar, 헤더 "Math.h"
  • Player와 Bullet의 위치를 비교적 자유롭게 바꿀 수 있는 SetPos() 추가
ConsoleObject.h
#pragma once
#include "Math.h"

// 자식들이 공통적으로 가져야 할 기능을 만들어야 한다.
class ConsoleObject
{
public:
	int2 GetPos();
	void SetPos(const int2& _Pos);
	char GetRenderChar();
protected:
	int2 Pos = { 0, 0 };
	char RenderChar = '@';
};
ConsoleObject.cpp
#include "ConsoleObject.h"
int2 ConsoleObject::GetPos()
{
	return Pos;
}
char ConsoleObject::GetRenderChar()
{
	return RenderChar;
}
void ConsoleObject::SetPos(const int2& _Pos) {
	Pos = _Pos;
}
(ConsoleObejct로 옮긴 기능들은 Player, Bullet 클래스에서는 삭제하고 ConsoleObject.h를 include 해야 함)

main함수에서 SetPos를 이용해서 총알 위치를 플레이어 머리 위로 바꿔 봤음 (근데 머리위에 따라다님)

#include <iostream>
#include "ConsoleScreen.h"
#include "Galaga.h"
#include "Player.h"
#include "Bullet.h"

int main()
{
	ConsoleScreen NewScreen = ConsoleScreen('*');
	Galaga NewGalaga = Galaga();

	Bullet NewBullet = Bullet({ 0,0 }, '^');
	Player NewPlayer = Player({ ScreenXHalf, ScreenYHalf }, '@');

	bool& Ref = NewBullet.GetIsFireRef();
	NewPlayer.SetBulletFire(&Ref);

	while (true)
	{
		NewScreen.ClearScreen();
		NewGalaga.GalagaWallDraw(NewScreen);

		int2 Index = NewPlayer.GetPos();
		char Ch = NewPlayer.GetRenderChar();

		NewScreen.SetPixel(Index, Ch); //플레이어 위치에 플레이어 char 찍기

		if (true == NewBullet.GetIsFireRef())
		{
			NewBullet.SetPos({ NewPlayer.GetPos().X, NewPlayer.GetPos().Y - 1 });
			NewScreen.SetPixel(NewBullet.GetPos(), NewBullet.GetRenderChar()); //총알 위치에 총알 char 찍기
		}

		NewScreen.PrintScreen();
		NewPlayer.Update();
	}
}
profile
일단 시작해보자

2개의 댓글

comment-user-thumbnail
2024년 11월 22일

@block blast How do circular references between classes in C++ impact compilation, and what strategies can be used to resolve them, such as using forward declarations?

1개의 답글

관련 채용 정보