값 타입 변환

의미를 유지하기 위해서, 원본 객체와 다른 비트열을 재구성

ex) int를 float형으로 변환시 비트의 구성이 다르기때문에 비슷한 값으로 비트열이 재구성 됨

 

참조 타입 변환

비트열을 재구성하지 않고, 관점만 바꾸는 것

ex) int를 float형의 레퍼런스로 변환시 비트는 그대로지만 비트의 구성이 다르기때문에 출력 값이 다르다.

실제로 사용하는 일은 거의 없을 것이다.

 

안전도 분류

 

안전한 변환: 의미가 항상 100% 일치하는 경우

ex) int를 long long으로 변환

 

불안전한 변환: 의미가 항상 100% 일치한다고 보장하지 못하는 경우

타입이 다르거나 더 작은 크기로 변환할 때

ex) int를 float로 변환

 

프로그래머의 의도에 따른 분류

 

암시적(implicit) 변환: 컴파일러가 자동으로 타입 변환

 

명시적(explicit) 변환: 사용자가 명시적으로 타입 지정

 

아무런 연관 관계가 없는 클래스 사이의 변환

일반적으로는 안되지만 타입 변환 생성자/연산자로 가능하게 만들어 줄 수 있다.

참고) 타입 변환 연산자는 반환값이 없다.

operator Knight()
{
	return (Knight)(*this);
}

 

연관 없는 클래스 사이의 참조 타입 변환

명시적으로 변환하는것은 가능하다. (=문법상으로는 문제 없다.)

 

상속 관계에 있는 클래스 사이의 변환

1) 상속 관계 클래스의 값 타입 변환 : 자식->부모(O) 부모->자식(X)

2) 상속 관계 클래스의 참조 타입 변환: 자식->부모(O) 부모->자식(암시적X, 명시적O)

 

 

크기가 큰 객체를 함수의 인자로 넘겨줄 때 값에 의한 복사로 넘겨주면 오버헤드가 심하게 발생하기 때문에 포인터로 넘겨주어야 한다.

 

명시적으로 타입을 변환할 때는 항상 조심해야만 한다.

객체를 동적 할당할 때 업캐스팅, 사용할 때 다운캐스팅을 하는 경우가 많기 때문에 타입 변환에 의한 실수가 일어날 가능성이 높다.

 

업캐스팅으로 동적 할당한 객체는 별다른 처리 없이 메모리 해제시에는 업캐스팅 대상 클래스의 소멸자까지만 호출이 된다.

따라서 상속 관계가 있는 클래스의 경우 부모 클래스(또는 최상위 클래스)의 소멸자를 가상 소멸자로 지정해주면 소멸자가 의도한 대로 잘 작동하게 된다.

 

가상 소멸자에 대한 내용은 면접에 거의 항상 나오는 단골 질문이다.

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

[동적 할당] 캐스팅 4총사  (0) 2022.08.29
[동적 할당] 얕은 복사 vs 깊은 복사  (0) 2022.08.29
[동적 할당] 동적 할당  (0) 2022.08.28
[객체지향 여행]  (0) 2022.08.28
[포인터] #2/2  (0) 2022.08.27

[동적 할당]

 

스택 영역은 다량의 메모리를 사용하기에 적합하지 않고, 데이터 영역은 사용자의 이용 여부와 관계없이 무조건 할당되어 사용되기 때문에 비효율적이다.

 

사용자가 원할 때 할당 및 해제해서 사용할 수 있는 영역을 힙 영역이라고 한다.

[ 코드영역  ] [     데이터 영역     ] [ 힙 영역  ] [ 스택 영역 ]

[   code   ] [.data][.bss][.rodata] [  heap   ] [   stack   ]

 

C++에서는 기본적으로 CRT(C Runtime Library)의 힙 관리자를 통해 힙 영역을 사용한다.

정말 원한다면 직접 API를 통해 힙을 생성하고 관리할 수도 있다. (ex: MMORPG 서버 풀링)

 

malloc - free

 

malloc

메모리 할당 후 시작 주소를 가리키는 void형 포인터를 반환해준다.

메모리가 부족하면 null을 반환한다.

 

free

메모리 해제시 크기를 지정하지 않아도 알아서 해제되는 이유는 할당할 때 헤더 영역을 추가로 만들어서 정보를 보관해두기 때문이다.

같은 메모리를 두 번 해제하는 것을 double free라고 하는데 보통 크래시만 발생한다. 첫 번째 free시 헤더 영역의 정보까지 모두 날아가므로 두 번째 free는 정보가 없어서 해제를 할 수가 없다.

 

use after free가 발생하는 것을 유의해야 한다.

 

new - delete

 

malloc-free는 함수이고 new-delete는 연산자이다.

사용 편의성은 new-delete가 좋지만 타입에 상관없이 특정한 크기의 메모리 영역을 할당 받으려면 malloc을 이용해야 한다.

 

malloc-free와 new-delete의 결정적인 차이

"new-delete는 생성 타입이 클래스인 경우 생성자와 소멸자를 호출해준다."

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

[동적 할당] 얕은 복사 vs 깊은 복사  (0) 2022.08.29
[동적 할당] 타입 변환  (0) 2022.08.29
[객체지향 여행]  (0) 2022.08.28
[포인터] #2/2  (0) 2022.08.27
[포인터] #1/2  (0) 2022.08.26

[객체지향의 시작]

 

객체? -> 플레이어, 몬스터, 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

+ Recent posts