벡터(Vector) 下

 

 

 

 

◎벡터의 크기

기하학적으로 한 벡터의 크기는 해당 방향이 있는 선분의 길이이다.

벡터의 크기는 이중 수직선으로 표기한다.

Ex ) 벡터 u의 크기는 ||u||이다. 

 

3차원 벡터의 크기는 피타고라스의 정리를 두 번 적용해 구할 수 있다.

 

어떻게 ||u||가 저 식이 될까?

더보기

우선 u의 크기를 빗변으로, y축을 높이 y로 두고 원점으로부터 y까지 직각이 되는 거리를 a라고 둔다.

그러면 ||u|| (빗변) 은 피타고라스의 정리를 통해 y제곱과 a제곱에 루트를 씌운 값이 된다. 

그리고 거기서 a를 구하기 위해 피타고라스의 정리를 한 번 더 사용한다.

a를 빗변으로 두고 x를 밑변, z축의 값을 높이 z로 두어 계산한다.

그러면 ||u||가 위와 같은 식이 된다. 

 

 

벡터를 순수하게 방향을 나타내는 용도로만 사용하면 벡터의 길이는 그다지 중요하지 않을 수 있다.

그런 방향 전용 벡터에서는 벡터의 길이를 정확히 단위 길이인 1로 맞추어 두는 것이 편리하다. 

따라서 벡터의 길이를 단위 길이가 되게 해서 단위 벡터로 만드는 것을 가리켜 벡터의 정규화(normalization)라고 부른다.

벡터의 각 성분을 벡터의 크기로 나누면 벡터가 정규화 된다. 

 

벡터의 정규화

왜 벡터의 크기로 각 성분을 나누는지 이해가 되지 않는다면 이렇게 생각하면 쉽다. 

 

 

 

 

벡터의 곱셈에는 두 가지 종류가 있다. 내적과 외적이다.

 내적

스칼라 값을 나타내는 벡터 곱셈의 일종으로 점곱(dot product)라고도 한다.

 

내적은 결과 값이 뒤에 나올 외적의 결과와 달리 스칼라 값으로 나온다.

 

u = (u1, u2, u3), v = (v1, v2, v3)라고 가정했을 때 내적은

u · v = u1v1 + u2v2 + u3v3

즉, 대응되는 성분들의 곱들의 합이다.

 

 

코사인 법칙을 이용하면 내적의 기하학적 의미를 알아낼 수 있다.

u · v = ||u|| ||v|| cosθ

여기서 θ는 벡터 u와 v 사이의 각도이다. 0 θ π 사이의 값.

따라서 위의 식은 두 벡터의 내적이 두 벡터 사이의 각도의 코사인을 벡터 크기들로 비례 시킨 것이다. 

특히 u와 v가 둘 다 단위 벡터이면 u, v의 내적은 두 벡터 사이의 각도의 코사인이다. 

u · v = 1 * 1 * cosθ = cosθ

 

1. u·v = 0 이면 u ⊥ v이다. ( = 두 벡터는 직교한다. )

2. u·v > 0 이면 두 벡터 사이의 각도 θ는 90˚보다 작다. ( = 두 벡터는 예각을 이룬다.)

3. u·v < 0 이면 두 벡터 사이의 각도 θ90˚보다 크다. ( = 두 벡터는 둔각을 이룬다. )

 

 

 

 

 외적

결과 값이 스칼라 값을 나타내는 내적과는 달리 외적의 결과는 또 다른 벡터가위곱(cross product)라고도 한다. 

 

외적은 오직 3차원 벡터에 대해서만 정의된다.

두 3차원 벡터 u와 v의 외적을 구하면 u와 v 모두에 직교(수직)인 또 다른 벡터 w가 나온다.

 

즉, w는 u와 직교이고 v와도 직교이다.

 

 

u = (u1, u2, u3), v = (v1, v2, v3)라고 가정했을 때 외적은 다음과 같이 구한다. 

w = u x v = (u2v3 - u3v2, u3v1 - u1v3, u1v2 - u2v1) 

w = u x v 

 

 

 

 

 

 

 

 

참고 출처

Direct11을 이용한 3D게임프로그래밍 (저; 프랭크 D. 루나, 역; 류광, 출판사; 한빛미디어)

 

 

 

매크로 함수와 인라인(Inline) 함수

in은 '내부', line은 '프로그램 코드라인'을 의미한다.

즉 프로그램 코드라인 안으로 들어가 버린 함수라는 뜻이다.

매크로, 일반 함수, 인라인 함수 모두 결과는 동일하지만 결과를 위한 과정이나 성격은 상당히 다르다! 

 

 

 

 

◎ 매크로 함수

#define 선행 처리 지시문에 인수로 함수의 정의를 전달해서 함수처럼 동작하는 매크로를 만드는 것이다. 

물론 일반 함수와는 달리 단순 치환만을 해주므로, 일반 함수와 완전히 똑같은 방식으로 동작하지는 않는다. 

매크로 함수는 일반적인 함수에 비해 실행 속도에서 이점이 존재하지만 가독성도 떨어질 수 있고 디버깅도 어려우며 복잡한 함수를 매크로의 형태로 정의하는 것에 한계가 존재한다. 

빠른 이유?

더보기

일반 함수처럼 호출된 함수를 위한 스택 메모리의 할당을 할 필요도, 실행위치의 이동과 매개변수로의 인자를 전달할 필요도, return 문에 의한 값의 반환을 할 필요도 없기 때문이다. 

 

 

매크로 함수의 예시.

#define Add(x) ((x)+(x))

int main()
{
	int a = Add(3);
	cout << a << endl;
}

이렇게 실행하면 a의 출력값은 6이 나올 것이다. 

 

 

왜 '전처리 과정'을 거칠까?

더보기

#와 ## 연산자는 선행처리기 연산자이다. 

이 선행처리기 연산자는 전처리기-> 어셈블러로 넘어가기 전에 전처리기가 # 뒤에 붙은 코드들을 처리하기 때문이다. 그래서 전처리 과정에서 처리한다. 

 

 

그리고 이 과정은 

#define Add(x) ((x)+(x))

int main()
{
	int a = Add(3);
	cout << ((3)+(3)) << endl;
}

이런 과정을 거치게 되는데 위와 같이 함수의 몸체 부분이 함수 호출 문장을 완전히 대체했을 때 '함수가 인라인화 됐다'고 표현한다. 

 

 

 

 

◎ 인라인 함수

인라인 함수는 매크로 함수의 장점은 유지하고 단점은 제거했다고 볼 수 있다.

 

또한 인라인 함수는 inline 예약어를 빼면 기존 함수와 다를 바 없고 자료형 등이 지정돼 있어 문법적으로도 완벽한 함수이다. 

매크로를 이용한 함수의 인라인화는 전처리기에 의해 처리되지만 키워드 inline을 이용한 함수의 인라인화는 컴파일러에 의해 처리된다. 

 

inlineint Add(int x)
{
	return x+x;
 }

int main()
{
	int a = Add(3);
	cout << a << endl;
}

 

그러나 매크로 함수의 장점을 인라인 함수가 완벽하게 가져오지 못한 부분도 있다.

예를 들면 위의 매크로 함수의 예시에서 x는 자료형에 의존적이지 않다. 

즉 실수를 넣든, 정수를 넣든 데이터 손실이 발생하지 않는다.

3을 넣으면 6이 1.92를 넣으면 3.84가 나오게 된다. 

하지만 인라인 함수는 자료형 기반으로 정의된 함수이기 때문에 int 자료형 기반에서 1.92를 넣게 되면 0.84의 손실이 발생한다. 

 

물론 함수의 오버로딩을 통해 문제 해결이 가능하지만 간단하게 한 번만 정의하면 된다는 매크로 함수의 장점과는 멀어지게 된다.

그런데 C++에는 이에 대한 해결책이 또 하나 존재한다. 바로 템플릿을 이용하는 것이다. 템플릿을 이용하면 여러 번 정의하지 않아도 한 번에 해결된다!

 

 

 

 

 

◎ 추가

Visual Studio를 통해 컴파일할 경우, 인라인으로 선언하지 않았더라도 적합하면 인라인 함수로 확장하는 설정이 있다.

반대로 인라인으로 선언했다고 해도 적합하지 않다면 일반 함수로 컴파일한다. 

 

기본값은 '인라인 함수로 사용하는 데 적합한 함수는 모두 인라인 함수로 만든다'는 의미이다. 

설정값은 보통 '기본값'으로 지정돼 있다.

 

 

 

 

 

 

 

참고 출처;


http://tcpschool.com/c/c_prepro_macroFunc  (코딩의 시작, TCP School ; 매크로 함수)

 

코딩교육 티씨피스쿨

4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등

tcpschool.com


윤성우의 열혈 C 프로그래밍 ( 저; 윤성우, 출판사 ; 오레지 미디어)


이것이 C++이다 ( 저; 최호성, 출판사 ; 한빛미디어 )

 

 

 

 

 

 

new 와 delete 

new는 C언어의 malloc을, delete는 free를 대신하는 키워드이다. 

 

 

 

 

new와 delete연산자는 내부에서 malloc()과 free()함수를 호출한다. 

 

 

굳이 왜 malloc과 free를 쓰지 않고 new와 delete를 사용하는 것일까?

malloc을 이용하여 동적할당을 할 때 할당할 대상의 정보를 무조건 바이트 크기 단위로 전달해야 한다는 것과 반환형이 void형 포인터이기 때문에 적절한 형 변환을 거쳐야 한다는 불편사항이 존재했었다. 

그러나 new는 메모리 크기를 정하지 않는다. 이것이 바로 new를 사용하는 가장 도드라진 특징이다.

 

또한 new와 malloc의 다른 점을 하나 더 뽑으라면 '생성자 함수 호출'이라는 특징이 존재한다. 

new를 통해 객체를 동적으로 생성하면서 객체의 생성자를 호출하기 때문이다. 

 

new와 delete는 단순히 메모리 관리 이상의 일들을 수행한다. 또한 문법적으로 연산자이며 malloc과 free는 객체지향 프로그래밍을 방해하는 원인이 될 수 있다.

 

 

 

 

 

new

자료형* 이름 = new 자료형;

int *testInt = new int;

int *testArray = new int[10];

int **testArray2 = new int*[10]; // 이중배열 동적할당

float *testFloat = new float;

double *testDouble = new double;

 

위와 같은 형태로 선언하면 된다. 

 

 

 

 

delete

delete 이름 ;

위와 같은 형태로 사용하면 되며 배열은 이름의 앞에 []를 붙여 준다. 

delete[] 배열 이름;

delete testInt;

delete []testArray;

delete testFloat;

delete testDouble;

 

 

 

 

 

 

 

 

 

 

 

벡터(Vector) 上

 

 

 

 

 

 

벡터(Vector)는 크기와 방향을 모두 가지고 있다. 이 크기와 방향을 모두 가진 수량을 벡터 값 수량이라고 부른다.

 

벡터 값 수량의 예로는 (force; 힘은 특정 방향과 세기, 즉 크기로 가해진다.) 변위(displacement: 한 입자의 최종적 이동 방향 및 거리), 속도(빠르기와 방향)가 있다. 

 

따라서 벡터는 힘이나 변위, 속도를 나타내는 데 쓰인다.

 

 

또한 벡터는 게임에서 플레이어가 바라보는 방향이나 한 다각형이 향한 방향, 광선이 이동하는 방향, 한 표면에서 광선이 반사되는 방향 등 순수한 방향만 나타내는 경우에도 벡터를 사용한다. 

 

 

벡터를 기하학적으로 나타내 본다면 벡터는 시각적으로 방향이 있는 선분으로 표시한다.

 

 

2차원 평면에 그린 벡터

 

선분의 길이벡터의 크기를, 화살표가 향하는 방향벡터의 방향을 의미한다. 

벡터가 그려져 있는 위치는 중요하지 않다

왜?

위치를 바꿔도 벡터의 크기나 방향은 변하지 않기 때문이다. 벡터가 가진 두 가지 속성인 크기와 방향은 위치와 상관없다. 

따라서 두 벡터는 오직 길이가 같고 같은 방향을 가리킬 때에만 같다고 한다. 

 

w와 v벡터는 길이가 같고 가리키는 방향이 같으므로 같은 벡터이다.

 

 

우리는 이렇게 알 수 있지만 컴퓨터는 벡터들을 기하학적으로 다루지 못한다. 따라서 벡터들을 수치적으로 지정하는 방법이 필요하다.

이를 위해 공간에 하나의 3차원 좌표계를 도입하고 모든 벡터를 그 꼬리가 원점과 일치하도록 이동시켜준다. 그러면 하나의 벡터를 그 머리(화살표의 끝)의 좌표로 정할 수 있으며 벡터를 v=(x, y, z)로 표기할 수 있다. 

 

 

v의 꼬리를 좌표계의 원점으로 이동시켰으며 벡터의 꼬리가 원점과 일치하는 경우 그 벡터가 "표준 위치에 있다"고 말한다. 

 

 

 

 

◎기본적인 벡터 연산

u = (u1, u2, u3)이고 v = (v1, v2, v3) 라고 가정했을 때

 

◑ 벡터의 상등 

두 벡터는 오직 해당 좌표성분들이 모두 같을 때만 같다.

즉 u1=v1, u2=v2, u3=v3일 경우에만 u = v이다.

 

 

◑ 벡터의 덧셈

벡터 덧셈은 성분별로 이루어지며

    w = u+v일 때,  w = (u1+v1, u2+v2, u3+v3)이다. 

 

w = u+v

 

u와 v를 더하기 위해서는 v를 평행이동 해 u의 꼬리에 v의 머리를 일치시켜 준다. 

그리고 그 결과값인 w는 원점에서부터 v의 머리까지 향하는 벡터가 된다. 

 

 

 

◑ 스칼라 곱셈

벡터에는 상수, 실수를 곱할 수 있으며 그 결괏값도 벡터이다. 

보통 벡터에 곱하는 실수 값을 스칼라라고 부르는데 스칼라 값을 k라고 할 때 ku = (ku1, ku2, ku3)이다.

이를 스칼라 곱셈이라고 부른다. 

 

 

w에 2를 곱했을 때

 

 

 

◑ 벡터의 뺄셈

벡터의 뺄셈벡터의 덧셈과 스칼라 곱셈을 통해서 정의 된다.

   w = u - v일 경우 정확하게 말하면 u에 -1의 스칼라 값을 곱한 v를 더하는 것이다. 

   w = u + (-1 * v) 

 

 

w = u-v

 

뺄셈은 v의 머리에서부터 u의 머리까지의 벡터를 구하는 것이다. 

 

이 벡터의 뺄셈을 통해서 방향을 알 수 있다.

 

 

 

 

 

참고 출처

Direct11을 이용한 3D게임프로그래밍 (저; 프랭크 D. 루나, 역; 류광, 출판사; 한빛미디어)

 

 

 

 

렌더링 파이프라인(Rendering Pipeline)

 

GPU를 사용해 리소스를 2D 이미지로 렌더링 하는 과정이다.

파이프라인은 여러 개의 파이프라인 단계로 구성되어 있으며 프로그램 가능 단계와 고정 프로그램 단계로 구분된다. 

 

 

 

 

렌더링 파이프라인을 구성하는 단계들과 관련 GPU 메모리 자원들

 

 

고정 프로그램 단계?

Direct3D에서 모든 처리가 진행되며 응용 프로그램에서 변경할 수 없는 단계

- 입력 조립(IA) 단계, 테셀레이터 (TS) 단계, 스트림 출력(SO) 단계, 래스터라이저(RS) 단계, 출력 병합(OM) 단계

 

프로그램 가능 단계?

응용 프로그램에서 쉐이더 프로그램을 통해 제공해야 하는 단계

- 정점 쉐이더(VS) 단계, 헐 쉐이더(HS) 단계, 도메인 쉐이더(DS) 단계, 기하 쉐이더(GS) 단계, 픽셀 쉐이더(PS) 단계

 

 

위의 파이프라인에서 오른쪽은 자원 메모리 풀을 뜻한다. 자원 메모리 풀에서 파이프라인의 단계로 가는 화살표는  그 단계가 자원을 입력으로 사용할 수 있음을 뜻하고 파이프라인의 단계에서 자원으로 가는 화살표는 그 단계가 GPU 자원에 자료를 기록할 수 있음을 뜻한다. 

 

 

 

◎ 입력 조립기 (Input Assembler) 단계

고정 함수 단계이다. 응용 프로그램에서 제공받은 정점 버퍼의 정점 데이터(프리미티브 데이터 : 점, 선, 삼각형)를 다른 파이프라인 단계에서 사용할 프리미티브(선 리스트, 삼각형 리스트, 삼각형 스트립, 인접성을 가지는 프리미티브 등)로 조립한다. 

즉, 메모리에서 기하 자료(정점 데이터와 Index(색인))를 읽어 기하학적 기본도형(삼각형, 선분 등)을 조립한다. 

 

그럼 정점이란 무엇일까?

더보기

Direct3D에서의 정점은 공간적 위치, 즉 위치 값 이외의 정보를 담고 있으며 이를 통해 좀 더 복잡한 렌더링 효과를 구현할 수 있다. 예를 들면 조명 구현을 위해 정점에 법선 벡터를 추가하거나 텍스처를 적용하기 위해 텍스처 좌표를 추가하는 식으로 사용할 렌더링 효과에 따라 특정 정보를 추가할 수 있는 유연성을 갖고 있다. 

 

 

 

◎ 정점 쉐이더 (Vertext Shader) 단계

이 단계에서는 입력 조립 단계에서 출력되는 Primitive의 각 정점에 대한 연산을 수행한다. 

이 정점 쉐이더는 항상 모든 정점들에 대해 한 번씩 실행된다. 또한 하나의 정점에 대해 한 번만 호출되며 하나의 출력 정점을 생성될 뿐 아니라 파이프라인 단계에서 항상 수행이 되어야 하는 단계이다. 따라서 정점에 대한 변환이 필요하지 않아도 정점 쉐이더를 생성해 연결해야 한다. 

 

이 정점 셰이더 함수의 구체적인 내용은 프로그래머가 구현해서 GPU에 전달하게 된다. 그 함수는 각 정점에 대해 GPU에서 실행되기 때문에 속도가 빠르다. 

변환(Transformation), 스키닝(Skinning), 조명(Vertex Lighting) 등 수많은 특수 효과를 정점 쉐이더에서 수행할 수 있다. 정점 쉐이더에서 입력 정점 자료는 텍스쳐라든가 변환 행렬, 장면 광원 정보 등 GPU 메모리에 담긴 다른 자료에도 접근할 수 있다. 

 

더보기

이 단계에서 일어나는 작업 중 변환이 어느 물체의 로컬 좌표를 월드 좌표로, 월드 좌표를 뷰 스페이스 좌표로 바꿔주고 투영 변환등이 일어나는 변환 작업이다.

 

 

 

 

◎ 테셀레이션 단계

테셀레이션이란 한 메시의 삼각형들을 더 잘게 쪼개서 새로운 삼각형들을 만드는 과정을 말한다.

 

테셀레이션의 장점은

  1.  LOD 구현이 가능하다.
    더보기
    LOD란 Level-of-detail를 말한다.
    카메라에 가까운 삼각형들은 테셀레이션을 적용해 더욱 세밀한 구현을 적용하고 멀리 있는 삼각형들은 테셀레이션을 적용하지 않는 방식의 세부 수준 메커니즘을 구현해 눈에 띄는 부분에 대해서만 삼각형을 더 투여할 수 있게 된다. 
  2.  Low-poly 메시, 적은 수의 삼각형들로 이루어진 메시를 담아두고 즉석에서 삼각형들을 추가해 메모리를 적용할 수 있다.
  3.  애니메이션, 물리 처리 같은 연산들을 단순한 Low-Poly 메시에 대해 수행하고, 테셀레이션 된 High-Poly 메시는 렌더링에만 사용해 계산량을 줄일 수 있다. 

 

 

 

◎ 기하 셰이더 (Geometry Shader) 단계

이 단계는 정점 셰이더의 처리를 거친 정점 데이터를 입력받는다. 그리고 입력받은 정점에 정보를 추가하거나 삭제해 입력 기본도형을기본 도형을 다른 여러 기본 도형들로 확장할 수도, 특정 조건에 따라서는 입력된 기본 도형을 출력하지 않고 버릴 수도 있다. 

기하 셰이더의 흔한 용도는 하나의 점이나 선분을 하나의 사각형으로 확장하는 것이다. 

 

 

 

◎ 래스터라이저(Rasterizer) 단계

이 단계는 투영된 3차원 삼각형으로부터 픽셀 색상들을 계산해 내는 것이다. 

Rasterization을 하는 동안 각 프리미티브를 구성하는 정점은 픽셀로 변환되고 프리미티브의 내부에 해당하는 점들은 보간을 통해 픽셀로 변환된다.

 

과정

1. 원근 투영 나누기(z 나누기)

2. 카메라 절두체를 벗어나는 점(Pixel)들을 클리핑(Clipping)

3. Primitive를 2차원 Viewport로 매핑

4. Primitive의 모든 픽셀들에 대해 픽셀 쉐이더를 호출

 

 

 

 

◎ 픽셀 셰이더 (Pixel Shader) 단계

각 픽셀의 데이터(기본적으로 색상)를 생성한다. 프로그래머가 작성해서 GPU에서 실행하는 프로그램으로 각각의 필셀 단편(pixel fragment)마다 실행된다. 

상수 변수, 텍스처 데이터, 또는 픽셀 출력 데이터 등을 결합해 출력 색상을 결정한다.

이 단계에서 픽셀 조명 계산 또는 픽셀에 대한 후처리를 할 수 있다. 

 

하나의 프리미티브를 구성하는 각 픽셀에 대해 픽셀 쉐이더를 한 번씩 호출한다. 

다중 샘플링을 사용한다면 깊이/스텐실 테스트는 각 서브픽셀에 대해 한 번씩만 수행되며 다중 샘플링을 사용해도 이 픽셀 쉐이더는 하나의 픽셀에 대해 한 번만 호출된다.

 

 

 

 

◎ 출력 병합기 (Output Merge) 단계

픽셀 셰이더가 생성한 픽셀 단편들은 이 단계로 입력된다. 최종적으로 픽셀의 색상을 생성해 렌더 타깃으로 출력하는 단계이다. 이 단계에서 일부 픽셀 단편들이 깊이 판정이나 스텐실 판정에 의해 버려지게 되며 버려지지 않은 픽셀 단편은 후면 버퍼에 기록된다.

흔히 말하는 블렌딩도 이 단계에서 일어난다.

더보기

블렌딩은 새 픽셀이 후면 버퍼의 기존 픽셀을 완전히 덮어 쓰는 것이 아니라 두 픽셀을 일정한 공식에 따라 혼합한 결과를 기록하는 것을 말한다. 

렌더 타겟의 픽셀 색상과 출력(픽셀 쉐이더) 색상을 결합한다. 

 

- 깊이-스텐실 검사(Depth-Stencil Testing)

픽셀이 그려져야 하는지를 결정하기 위해 깊이 값과 스텐실 값 데이터를 사용한다.

하나의 깊이/스텐실 버퍼만 활성화 되며 깊이-스텐실 버퍼에 스텐실 요소가 없으면 스텐실 검사는 항상 성공한다.

또한 깊이-스텐실 버퍼가 파이프라인에 연결되지 않으면 스텐실 검사는 항상 성공한다. 

 

깊이 검사?

더보기

z = min(Viewport.MaxDepth, max(Viewport.MinDepth, z))

출력 픽셀의 깊이 값을 깊이 버퍼의 같은 위치의 깊이 값과 비교한다. 

비교의 결과에 따라 출력 픽셀을 렌더 타깃에 출력하거나 출력하지 않는다.

 

 

 

 

 

 

 

 

참고 출처

Direct11을 이용한 3D게임프로그래밍 (저; 프랭크 D. 루나, 역; 류광, 출판사; 한빛미디어)

참조자와 함수

 

Call-by-value와 Call-by-reference는 많이 들어봤을 것이다.

Call-by-value란 '값'을 인자로 전달하는 함수의 호출 방식, Call-by-reference란 '주소 값'을 인자로 전달하는 함수의 호출방식이다. 

 

 

 

 

 

int main()
{
	int a = 1;
	int b = 2;
    
	CallByValue(a,b);
	CallByReference(&a, &b);
	ReferenceFunction(a, b);
}

위와 같이 구성된 main함수가 있다고 가정하자. 

 

 

 

⊙ Call-by-value!

void CallByValue(int num1, int num2)
{
	num1 = 50;
	num2 = 30; 
}

CallByValue함수는 위와 같이 함수의 내부에서 a와 b의 값을 바꿔도 main함수의 a, b의 값은 바뀌지 않는다.

 

왜?

CallByValue함수의 인자값인 int num1와 int num2는 CallByValue라는 함수의 지역변수이다. 따라서 int num1과 int num2의 값을 바꿔도 해당 num1과 num2는 CallByValue함수가 끝나면 함께 사라지게 되는 것이다.

 

그래서 main함수의 a,b. 즉, 함수 외부에 선언된 변수에 접근이 불가하다. 

 

그래서 필요한 것이 주소 값을 인자로 전달하는 Call-By-Reference이다. 

 

 

 

 Call-by-Reference

void CallByReference(int *num1, int *num2)
{
      *num1 = 30;
      *num2 = 50;
}

 이렇게 되면 int *num1과 int *num2가 주소 값을 넘겨받아서 a와 b의 주소 값에 저장된 값을 '직접' 변경하게 되므로 함수 외부에 선언된 변수의 값을 바꿀 수 있는 것이다. 

 

정리하자면 Call-by-Reference는 주소 값을 전달받아서, 함수 외부에 선언된 변수에 접근하는 형태의 함수 호출이다. 

 

주소 값이 전달되었다는 사실이 중요한 것이 아닌 주소 값이 참조의 도구로 사용되었다는 사실이 중요한 것이며 이것이 Call-by-Value와 Call-by-Reference를 구분하는 기준이 된다.

 

 

 

Call-by-Value와 Call-by-Reference를 구분하는 기준에 대한 추가 설명

더보기

예를 들어

int *Test(int *ptr)
{
	return ptr+1;
}

이 함수는 분명 인자값을 주소로 전달하고 있음에도 Call-By-Reference가 아닌 Call-By-Value이다. 

왜냐하면 이 함수의 연산의 주체는 값이기 때문이다. 다만 그 값이 주소 값일 뿐인 것이다. 주소 값을 이용해 외부에 선언된 변수에 접근하는 Call-By-Reference와 거리가 멀다.

 

int *Test2(int *ptr)
{
	*ptr = 20;
	return ptr;
}

그러나 이런 식으로 사용되었다면 이 함수는 Call-By-Reference이다. 주소 값을 이용해서 함수 외부에 선언된 변수를 참조했기 때문이다. 

 

 

 

C++에서는 함수 외부에 선언된 변수의 접근 방법으로 두 가지가 존재한다. 

하나는 '주소 값'을, 다른 하나는 '참조자'를 이용하는 방식이다.

 

 

 

 참조자를 이용한 Call-By-Reference? 

Call-By-Reference의 가장 큰 핵심은 함수 내에서 함수 외부에 선언된 변수에 접근할 수 있다는 것이었다. 

void ReferenceFunction(int &num1, int &num2)
{
	num1 = 10;
	num2 = 20;
 }

매개변수로 참조자가 들어와 있다. 참조자는 선언과 동시에 변수로 초기화가 되어야 하지 않나?라고 생각할 수 있다.

하지만 매개변수는 함수가 호출되어야 초기화가 진행되는 변수들이다. 즉, 초기화가 이뤄지지 않은 것이 아니라 함수 호출 시 전달되는 인자로 초기화를 하겠다는 의미이다.

 

 

이와 같은 코드는 포인터로 주소값을 넘겨받는 Call-By-Reference 보다 함수의 특성이 한눈에 파악되지 않는다는 단점이 존재한다. 참조자로 넘겨줄 때 함수는 ReferenceFunction(a, b)로 Call-By-Value함수와 동일해 보인다.

 

그러나 실제 함수 외부의 변수 값에 영향을 끼칠 수 없는 CallByValue함수와 달리 이 함수는 함수 외부의 값을 변경할 수 있으므로 이 함수 내부에서 어떤 일이 일어날 지 직관적으로 파악이 불가능하기 때문에 단점이라고 하는 것이다.

 

 

이 단점은 const를 사용함으로써 극복이 가능하다. 함수의 매개 변수 선언 시 const를 붙여줌으로써 함수 내에서 참조자를 이용한 값의 변경을 하지 않겠다고 선언하는 것이다.

 

이렇게 하면 함수 내부에서 참조자에 값을 저장하는 경우 컴파일 에러가 발생하며 함수의 원형만 봐도 값의 변경이 이루어지지 않음을 확신할수 있다. 

 

 

 

 

 

 

 

 

 

 

참조자(Reference)

 

 

변수를 선언하면 해당 변수의 이름은 할당된 메모리 공간을 가리키는, 구분 짓는 이름이 된다. 

그래서 변수를 선언할 때 int a; 를 선언했다면?

이미 a라는 이름이 가리키고 있는 메모리 공간이 있으므로 해당 범위(scope, { })에서 동일한 이름의 변수를 선언할 수가 없다. 

 

 

 

그런데 이 상황에서 number가 가리키는 공간에 또 하나의 이름을 부여하고 싶다면

int &b = a;

위와 같이 b앞에 &를 붙여 선언하면 된다.

이 의미는 b라는 이름도 a라는 메모리 공간을 가리키는 또 하나의 이름이 된 것이다. 

 

 

왜 그럴까? 

보통 & 연산자는 변수의 주소값을 반환하는 연산자이다. 그러나 변수를 선언할 때 &를 붙이게 되면 이는 참조자의 선언을 의미하게 된다. 

 

int *c = &a;  → 변수 a의 주소 값을 포인터 c에 저장
int &b = a;  →  변수 a에 대한 참조자 b를 선언.

 

또한 참조자를 대상으로 참조자를 선언하는 것도 가능하지만 바람직하지는 않으며 딱히 크게 사용할 일이 없다.

int a;

int &b = a;

int &c = b;

 

 

참조자의 중요한 성질 중 하나는 변수에 대해서만 선언이 가능하다는 것과 선언됨과 동시에 참조할 대상이 있어야 한다는 것이다.

또한 NULL로 초기화하는 것도 불가능하다. 

 

int &b;  (X)  → 선언과 동시에 참조할 대상이 있어야 한다.
int &b = NULL;  (X)  → NULL로 초기화 하는 것은 불가능하다.
int &b = 100;  (X)  → 변수에 대해서만 선언이 가능하므로 상수를 넣을 수 없다. 

 

단, 다음 게시글에 나올 const 참조자는 상수도 참조가 가능하다.

https://www.acmicpc.net/problem/7576

 

7576번: 토마토

첫 줄에는 상자의 크기를 나타내는 두 정수 M,N이 주어진다. M은 상자의 가로 칸의 수, N은 상자의 세로 칸의 수를 나타낸다. 단, 2 ≤ M,N ≤ 1,000 이다. 둘째 줄부터는 하나의 상자에 저장된 토마토들의 정보가 주어진다. 즉, 둘째 줄부터 N개의 줄에는 상자에 담긴 토마토의 정보가 주어진다. 하나의 줄에는 상자 가로줄에 들어있는 토마토의 상태가 M개의 정수로 주어진다. 정수 1은 익은 토마토, 정수 0은 익지 않은 토마토, 정수 -1은 토마

www.acmicpc.net

 

 

BFS로 풀면 되는 문제이다. 

우선 토마토에 대한 정보를 입력받고 

	for (int i = 0; i < h; i++)
	{
		for (int j = 0; j < w; j++)
		{
			if (arr[i][j] == 1) {
				check[i][j] = true;
				q.push(make_pair(i, j));
			}
		}
	}

처음에 전체 토마토 상자를 보면서 안에 익은 토마토가 어느 위치에 있는지 찾고 queue에 넣어주었다.

 

그후 while문으로 들어가서 현재 queue의 size만큼 for문을 돌려주면서 해당 위치의 동, 서, 남, 북의 칸을 비교해서

익지 않은 토마토(arr[y][x] == 0)이며 방문하지 않았으면(check[y][x] == false) queue에 넣고 방문했다는 표시를 한다.

 

for (int i = 0; i < h; i++)
	{
		for (int j = 0; j < w; j++)
		{
			if (arr[i][j] == 0 && check[i][j] == false)
			{
				return -1;
			}
		}
	}

이 부분은 처음에 런타임 에러도 발생하고 채점 결과가 틀렸다고 계속 나와서

Visual studio에서 예제를 돌렸을 때는 문제가 없어서 뭐가 문제인지 한참 고민하다가 

예를 들면 3x3 에서

0 -1 -1
-1 1 1
1 1 1

이렇게 예상치 못한 상황에서 익지 못하게 되는 토마토를 확인하기 위해서 추가하게 되었다. 

 

#include <iostream>
#include <queue>

using namespace std;
const int MAX_BOX = 1000;
int dirX[4] = { 1, -1, 0, 0 };
int dirY[4] = { 0,0,-1,1 };
bool check[MAX_BOX + 1][MAX_BOX + 1];

int tomato(int **arr, int w, int h)
{
	int day = 0;
	queue<pair<int, int>> q;

	// 처음에 넘겨받은 토마토 상자 안에 익은 토마토가 몇 개나 있는지 
	for (int i = 0; i < h; i++)
	{
		for (int j = 0; j < w; j++)
		{
			if (arr[i][j] == 1) {
				check[i][j] = true;
				q.push(make_pair(i, j));
			}
		}
	}

	while (!q.empty())
	{
		int q_size = q.size();
		for (int j = 0; j < q_size; j++) {

			int y = q.front().first;
			int x = q.front().second;

			q.pop();

			for (int i = 0; i < 4; i++)
			{
				int tempy = y + dirY[i];
				int tempx = x + dirX[i];

				if ((tempy < h && tempy >= 0) && (tempx >= 0 && tempx < w)) {
					if (arr[tempy][tempx] == 0 && check[tempy][tempx] == false)
					{
						check[tempy][tempx] = true;
						q.push(make_pair(tempy, tempx));
					}
				}
			}
		}
		day += 1;
	}

	// 다 돌아봤을 때 익지 않은 토마토가 존재하면서 그곳에 방문하지 않은 경우가 있는지 확인
	for (int i = 0; i < h; i++)
	{
		for (int j = 0; j < w; j++)
		{
			if (arr[i][j] == 0 && check[i][j] == false)
			{
				return -1;
			}
		}
	}
	return day - 1;
}

int main()
{
	int w, h;
	cin >> w >> h;

	// 배열
	int **box = new int*[h];
	for (int i = 0; i < h; i++)
	{
		box[i] = new int[w];
	}

	// 입력받기
	for (int i = 0; i < h; i++)
	{
		for (int j = 0; j < w; j++)
		{
			cin >> box[i][j];
		}
	}
	cout << tomato(box, w, h);
	return 0;
}

 

 

+ Recent posts