[객체지향의 시작]

 

객체? -> 플레이어, 몬스터, GameRoom 등 오브젝트들

 

team by team 이지만 멤버변수를 명시적으로 표현할 때 접두어 m_, _, m 을 붙이는 방법들이 있다.

 

class는 일종의 설계도이다.

 

[생성자와 소멸자]

 

생성자는 오버로딩이 가능하지만 소멸자는 한개만 존재할 수 있다. 반환 타입이 없다.

 

기본 생성자 (인자 없음)

복사 생성자 (자기 자신의 클래스 참조 타입을 인자로 받음). 일반적으로 똑같은 데이터를 지닌 객체가 생성되길 기대함

기타 생성자. 인자를 1개만 받는 기타 생성자를 타입 변환 생성자 라고 부르기도 함

 

암시적 생성자(Implicit)

생성자를 명시적(Explicit)으로 만들지 않으면 기본 생성자가 컴파일러에 의해 자동으로 만들어짐.

생성자를 하나라도 명시적으로 정의해뒀다면 기본 생성자는 더 이상 자동으로 만들어주지 않는다.

 

[상속성]

 

Inheritance

 

상속받은 객체의 생성자와 소멸자는 부모 생성자 - 자식 생성자 - 자식 소멸자 - 부모 소멸자 순으로 작동하기는 하는데 디스어셈블리로 열어보면 우선 자식 생성자에 접근하기 바로 직전에 부모 생성자에 접근하는걸 확인할 수 있다.

 

[은닉성]

 

은닉성(Data Hiding) ≒ 캡슐화(Encapsulation)

 

캡슐화 : 연관된 데이터와 함수를 논리적으로 묶어놓은 것

 

몰라도 되는 데이터는 깔끔하게 숨기겠다.

데이터를 숨기는 이유는 1) 정말 위험하고 건드리면 안돼서, 2) 다른 경로로 접근하길 원해서 이다.

 

멤버 접근 지정자 : public, protected, private

 

대형 프로젝트의 경우 내가 설계한 모든 클래스들을 다른 사람들도 다 이해할 거라고 착각하면 안된다.

오히려 다른 사람들은 다 모른다고 가정해야 한다.

 

외부에서 호출하면 안되는 함수들을 public으로 만들다보면 로직이 꼬일 수 있으니 같이 동작해야 하는 로직은 하나로 묶어서 사용하는 것이 좋다.

 

상속 접근 지정자 : 부모 클래스의 접근 지정자의 최대치를 지정한다고 보면 된다. private 상속 시 자식 클래스들은 조부모 클래스에 아무것도 접근하지 못한다.

private, protected 상속은 웬만해서 써 볼 일이 없을 것이다.

 

[다형성]

 

Polymorphism : 겉은 똑같지만 기능이 다르게 동작함

 

오버로딩 : 함수 중복 정의 = 함수 이름의 재사용

오버라이딩 : 함수 재정의 = 부모 클래스의 함수를 자식 클래스에서 재정의

 

정적 바인딩 : 컴파일 시점에 결정. 일반 함수들은 정적 바인딩을 사용

동적 바인딩 : 실행 시점에 결정 (<- 면접 단골 질문)

 

동적 바인딩을 원한다면 가상 함수(Virtual function)을 사용해야 한다.

가상 함수는 재정의를 하더라도 가상 함수이다. 자식 클래스에서 virtual 키워드를 떼더라도 여전히 가상 함수이다.

 

객체가 어떤 타입인지를 알아서 호출해줄까? -> 가상 함수 테이블 (vftable, v-table)

가상 함수가 하나라도 포함된 객체의 경우 메모리에 가상 함수 테이블이 생성되는 것을 확인할 수 있다.

vftable을 채우는건 생성자의 선처리 과정에서 이루어지게 된다.

 

 

순수 가상 함수 : 구현은 없고 인터페이스만 전달하는 용도로 사용하고 싶은 경우

순수 가상 함수가 하나라도 존재하는 클래스는 추상 클래스로 인식하게 되어서 직접적으로 객체를 만들 수 없게된다.

 

[초기화 리스트]

 

멤버 변수를 초기화 하는 방법은 몇 가지가 있다.

생성자 내에서, 초기화 리스트, 클래스 내에서 선언과 동시에 초기화(C++11)

 

초기화 리스트는

상속 관계(is-a)에서 원하는 부모 생성자를 호출하거나,

포함 관계(has-a) 관계의 객체 생성자를 호출할 때,

정의와 동시에 초기화가 필요한 경우(참조 타입, const 타입)에 필요하다,

 

Is-A : 상속 관계

Has-A : 포함 관계. 선처리 과정에서 생성자가 호출되기 때문에 객체를 소유한 생성자에서 초기화를 하면 생성자가 두번 호출되는 일이 발생한다.

예를 들어 A객체가 B객체를 소유하고 A생성자 내에서 B객체의 생성자를 호출 시 [A생성자-B생성자-B소멸자-B생성자] 라는 흐름이 발생한다.

 

[연산자 오버로딩]

 

연산자 함수를 정의해야 한다.

 

멤버 연산자 함수: 클래스 내에서 정의된다. 연산은 A op B 형태에서 left to right로 연산한다. A는 기준 피연산자 라고 한다.

A가 클래스가 아니면 사용이 불가능 하다.

class Position
{
public:
	Position operator+(const Position& arg)
	{
		// do something
	}
}

 

전역 연산자 함수: A op B 형태라면 A, B 모두 연산자 함수의 피연산자로 만들어줄 수 있다.

Position operator+(const int a, const Position& b)
{
	// do something
}

 

상황에 따라 맞는걸 골라서 사용하게 된다.

대입 연산자 같은 경우는 전역 연산자 함수로 만들 수 없다.

참고) 객체의 선언과 동시에 초기화 하는것과 대입 연산자는 다르다.

 

복사 대입 연산자: 대입 연산자 중, 자기 자신을 참조 타입으로 받는 것. 동적할당에서 다룰 예정

 

모든 연산자를 다 오버로딩 할 수 있는 것은 아니다. (:: . * 이런건 안됨)

++, -- 같은 단항 연산자의 경우 전위형은 operator++(), 후위형은 operator++(int) 와 같은 형태로 구분할 수 있다.

다만 전위형은 this 레퍼런스를 반환해 주어야 한다.

..
// 복사 대입 연산자. ref타입을 인자로 받음
Position& operator=(const Position& arg);
// 후위형. 복사된 값 반환
Position operator+(int);
..

Position pos1;
Position pos2;
pos2 = pos1++; //ref 요구 = 복사값 반환 -> 타입 불일치

 

타입이 일치하지 않지만 문법상 정상 실행이 되는 이유는 const가 붙었기 때문이다.

디스어셈블리로 추적해보면 operator++로 값이 반환되고 해당 값이 스택 영역에 임시로 저장된 후 해당 영역을 레퍼런스로 operator=에 넘겨주는 형태가 된다.

Position pos1;
Position pos2;
Position temp = pos1++;
pos2 = temp;

 

즉, 이런 형태로 작동하는 셈이다.

 

[객체지향 마무리]

 

class와 struct의 미묘한 차이

struct의 기본 접근 지정자는 public, class는 private이다.

 

static 변수

개별 객체가 아닌 클래스 자체와 연관이 생긴다.

객체와는 다른 공간에 할당된다.

전역변수와 마찬가지로 .data(.bss) 영역에 할당된다.

 

static 함수

개별 객체가 아닌 클래스 자체와 연관이 생긴다.

static 함수의 경우 static 변수를 조작하는것은 문제 없지만 일반 멤버 변수는 조작할 수 없다.

'C++ > Rookiss C++' 카테고리의 다른 글

[동적 할당] 타입 변환  (0) 2022.08.29
[동적 할당] 동적 할당  (0) 2022.08.28
[포인터] #2/2  (0) 2022.08.27
[포인터] #1/2  (0) 2022.08.26
[함수]  (0) 2022.08.26

[배열 기초]

 

VC 기준으로 배열의 크기는 상수로 지정해야 한다. gcc는 아님

배열의 이름은 배열의 시작주소. 시작 주소를 가리키는 TYPE* 포인터이다.

 

[포인터 vs 배열]

 

 

둘은 서로 다르다.

test1은 Hello World가 .data 영역(아마도 .rodata)에 존재하는 "Hello World"의 주소를 대입하는건데 read only이기 때문에 값 변경이 불가능하다.

test2는 .data 영역에 존재하는 "Hello World"를 4(8)바이트씩 끊어서 복사한다. 복사해온 값이기 때문에 값 수정이 가능하다.

 

배열을 함수의 인자로 넘기게 되면 배열의 시작 주소만 넘기게 된다.

컴파일러가 알아서 포인터로 치환하는 것이다.

 

[로또 번호 생성기]

 

별거 없음

 

[다중 포인터]

 

이중 포인터 사용시 스택프레임

main                                                              SetMessage

[매개변수][RET][지역변수(msg(Hello주소))] [매개변수(a(&msg))][RET][지역변수]

*a = "Bye"; 를 실행 시 의도한 결과가 나온다. 일차원 포인터로는 char로 이루어진 문자열을 건드리는 것은 call by value나 다름없다.

 

다중 포인터는 그냥 양파까기라고 생각하면 된다.

const char*& a 같은 문법도 사용이 가능하다.

 

[다차원 배열]

 

메모리 할당된것을 보면 성능, 구조적으로 1차원 배열과 똑같이 할당되어있다.

사용시 약간의 계산 순서 차이는 있지만 1차원 배열과 2차원 배열은 다를 게 없다.

3차원 배열 이상 사용하는 경우는 거의 없다.

 

2차원 배열은 대표적으로 2D 로그라이크 맵 생성에 이용한다.

 

[포인터 마무리]

 

TYPE형 1차원 배열과 TYPE*형 포인터는 완전히 호환이 된다.

2차원 배열과 2차원 포인터는 전혀 호환이 안된다.

 

주소값을 다룰 때는 그 주소가 끝까지 유효한지 항상 유의해야 한다.

스택 영역 데이터의 주소값을 밖으로 넘겨줄때는 조심해야한다.

 

[파일 분할 관리]

 

헤더파일의 내용은 최대한 간결하게 유지하는 것이 좋다.

클래스 전방선언때 한번 더 언급할 예정

'C++ > Rookiss C++' 카테고리의 다른 글

[동적 할당] 동적 할당  (0) 2022.08.28
[객체지향 여행]  (0) 2022.08.28
[포인터] #1/2  (0) 2022.08.26
[함수]  (0) 2022.08.26
[코드의 흐름 제어]  (0) 2022.08.26

[포인터 기초 #1]

 

포인터 변수는 타입에 상관없이 4바이트(32비트 환경) or 8바이트(64비트 환경) 고정 크기이다.

디스어셈블리로 확인해보면 포인터 값을 참조하는 경우 mov를 두번 타고 넘어가서 값을 가져오게 된다. 

 

변수 : 값을 담는 바구니

포인터 변수 : 주소를 담는 바구니

 

[포인터 기초 #2]

 

포인터 변수는 타입에 상관없이 크기 고정이면 왜 타입을 지정할까?

-> 포인터의 타입보다는 가리키고 있는 주소의 타입이 어떤건지 알려주는 준다고 보면 된다.

 

가리키는 주소의 범위를 넘겨서 데이터를 잘못 조작하면 치명적인 문제가 발생할 수 있다. (타입의 불일치)

예) int형 변수를 __int64로 캐스팅한 주소를 받아서 8바이트 데이터를 대입하는 경우.

 

[포인터 연산]

 

주소 연산자 (&) : 해당 변수의 주소를 알아냄

산술 연산자 (+, -) : 타입의 크기만큼 주소 증감

간접 연산자 (*) : 주소가 가리키는 값에 접근

간접 멤버 연산자 (->) : 간접 연산자와 . 연산을 한큐에 함. (*ptr).a = 1과 ptr->a = 1은 어셈블리상 완벽하게 동일하다.

 

[포인터 실습]

 

구조체(클래스)를 반환하여 값을 초기화 하는 방식은 임시 공간을 만들어서 함수를 빠져나올 때 임시 공간에 있는 값을을 전부 복사해서 넘겨준다는 것을 레지스터를 통해 확인할 수 있다. (오버헤드 발생)

 

포인터를 넘겨줘서 간접 참조로 초기화 하면 임시 공간을 사용하지 않기 때문에 훨씬 효율적이다.

 

구조체(클래스)끼리 복사하는 경우 각 인자를 복사하는 것과 다름없다.

 

[참조 기초]

 

1) 값에 의한 전달 (Call by value) : 구조체/클래스의 크기에 따라 오버헤드가 크게 발생

2) 주소에 의한 전달 (Call by address) : 언제나 4-8바이트만 사용

3) 참조에 의한 전달 (Call by reference) : 로우레벨(어셈블리) 관점에서 실제 작동 방식은 주소에 의한 전달과 완벽하게 똑같음

 

참조에 의한 전달은 값에 의한 전달과 동일하게 사용하지만 작동 구조는 주소의 의한 전달로 이뤄져서 양쪽의 장점만을 빼온듯한 모양새이다.

 

[포인터 vs 참조]

 

성능적인 면에서는 완벽하게 똑같다.

편의성은 참조가 더 좋다고도 볼 수 있다.

 

편의성 관련

포인터는 주소를 명시적으로 넘기기 때문에 코드를 확인할 때 바로 확인할 수 있지만 참조는 값에 의한 전달인지, 참조에 의한 전달인지 겉으로 보기에는 티가 안난다. (휴먼에러 발생 가능성)

 

레퍼런스와 const는 세트로 등장하는 경우가 매우 많다.

 

const를 포인터에 붙일 때 * 앞에 붙는 경우 주소를 바꿀 수 없고 뒤에 붙는 경우 참조값을 바꿀 수 없다.

StatInfo globalInfo;

info = &globalInfo; // 매개변수가 const StatInfo* info 라면 사용 불가
info->hp = 10000; // 매개변수가 StatInfo* const info 라면 사용 불가

 

초기화 관련

포인터는 선언 시 초기화 하지 않아도 되지만 참조는 반드시 초기화 되어야 한다.

 

없는 값을 표현하는 경우 포인터는 nullptr이 있지만 참조는 그런게 없다.

웬만해서는 코드 실행전에 nullptr인지 아닌지 체크하는게 좋다.

실제로 프로그램이 뻗는 대부분은 null크래시이다.

 

결론

team by team이라서 정해진 답은 없다.

ex) 구글에서 만든 오픈소스를 보면 거의 무조건 포인터를 사용한다.

ex) 언리얼 엔진에서는 reference도 애용한다.

 

값이 없는 경우도 고려한다면 포인터, readonly만 한다면 const ref&.

다만 reference를 넘겨서 값이 변한다면 매개변수에 OUT이라는 명시를 해주는 방법이 있다.(언리얼에서 자주 사용)

가독성 차원에서는 바로 알아볼 수 있지만 기능면에서는 아무것도 없다.

#define OUT // 아무것도 정의하지 않음

void ChangeInfo(OUT StatInfo& info)
{
	// do something
}

// 함수 사용시
ChangeInfo(OUT info);

 

다만 딱히 정해진 규칙은 아니고 팀의 스타일에 따라가면 된다.

 

 

'C++ > Rookiss C++' 카테고리의 다른 글

[객체지향 여행]  (0) 2022.08.28
[포인터] #2/2  (0) 2022.08.27
[함수]  (0) 2022.08.26
[코드의 흐름 제어]  (0) 2022.08.26
[데이터 갖고 놀기]  (0) 2022.08.25

[함수 기초]

 

별거 없음

 

[스택 프레임]

 

함수 호출시 내부적으로는 스택 프레임을 사용한다.

프로그램 실행 시 스택 프레임의 크기는 정해진다.

함수끼리 돌려 쓰는 메모장 같은 느낌이다.

 

레지스터를 추적해보면 매개변수-반환 주소값-지역변수 순으로 쌓이게 된다.

 

[지역 변수와 값 전달]

 

별거 없음

 

[호출 스택]

 

호출 스택을 보는 연습을 계속 해야한다.

프로그래밍 하면서 가장 많이 보게되는 것중 하나이다.

 

[함수 마무리]

 

오버로딩 : 중복 적재/정의. 함수 이름의 재사용

매개변수 갯수가 다르거나 타입이 다른경우에만 가능

 

스택 오버플로우: 스택 메모리가 꽉 차서 터지는 경우

 

[Text RPG #1, #2]

 

여기도 뭔가 딱히 없음

 

모든 함수 다 접는 단축키는 Ctrl+M+O

'C++ > Rookiss C++' 카테고리의 다른 글

[포인터] #2/2  (0) 2022.08.27
[포인터] #1/2  (0) 2022.08.26
[코드의 흐름 제어]  (0) 2022.08.26
[데이터 갖고 놀기]  (0) 2022.08.25
[어셈블리 언어 입문]  (0) 2022.08.25

[조건문]

 

별거 없음

 

switch-case는 정수 계열만 넣을 수 있다.

 

[반복문]

 

별거 없음

 

do-while은 쓰는걸 거의 본 적이 없음

for, while은 어셈블리 cmp, jmp로 이뤄져있다.

 

[열거형]

 

const를 이용한 상수화는 전역변수 선언시 VS 기준으로는 메모리에 잡히지 않고 상수로 대체가 되는 반면 주소값을 얻어오거나 스택영역에 선언될 경우 메모리에 잡히게 된다.

하지만 열거형은 완벽하게 상수로 대체되기 때문에 어디에 선언하던간에 메모리에 잡히지 않는다.

 

define보다는 enum을 사용하는게 낫다.

디버그 단계에서 define 내용이 날아가서 알아보기 어렵다.

 

정리하자면,

const 상수화는 컴파일러에 따라 메모리에 잡힐수도, 안잡힐수도 있다.

define 상수는 빌드시 전처리 단계에 하드코딩으로 박히기 때문에 디버그때 값의 추적이 어렵다.

enum은 메모리에 잡히지 않고 디버그에서 값의 추적도 잘 된다.

 

'C++ > Rookiss C++' 카테고리의 다른 글

[포인터] #2/2  (0) 2022.08.27
[포인터] #1/2  (0) 2022.08.26
[함수]  (0) 2022.08.26
[데이터 갖고 놀기]  (0) 2022.08.25
[어셈블리 언어 입문]  (0) 2022.08.25

+ Recent posts