• 4.2.1 전역 변수(Global Variables)
  • 4.2.2 정적 변수(Static Variables)

4.2.1 전역 변수(Global Variables)

 

전역 변수란 함수 내에서 정의되지 않고 어디에도 종속되지 않은 변수를 뜻한다. 코드상에서 어느 중괄호 안에도 안 들어가면 전역 변수가 된다. 초기 할당이 진행되었다면 프로그램이 끝날 때까지 존재하며 extern 키워드를 사용하면 모든 파일에서 접근이 가능하다.

#include <iostream>

using namespace std;

// global variable
int value = 123;

int main()
{

	// 전역 변수가 출력
	cout << value << endl;

	// name hiding
	int value = 1;

	// 영역 연산자(전역 변수를 출력시킨다)
	cout << ::value << endl;

	// 지역 변수가 출력
	cout << value << endl;

	return 0;
}

전역 변수 value가 할당되고나면 접근이 가능해지는데, 지역 변수가 동일한 이름으로 작성되면 name hiding으로 인해 지역 변수를 먼저 접근하게 된다. 하지만 영역 연산자 ::를 사용하게 되면 지역 변수가 아닌 전역 변수로 접근해서 데이터를 사용할 수 있다.


4.2.2 정적 변수(Static Variables)

 

정적 변수도 할당이 되면 프로그램이 끝날 때까지 존재하기 때문에 전역 변수와 비슷하다고 생각될 수 있지만 조금 특성이 다르다. 정적 변수는 최초 초기화는 반드시 소스 파일에서 해야 하는 특징이 있다. 또한 정적 변수의 경우 선언된 파일 안에서만 사용이 가능하고 선언된 파일 이외에서는 사용이 불가능하다.

#include <iostream>

using namespace std;

// 해당 cpp 안에서만 전역으로 사용하는 변수 다른 곳에서는 접근 불가능
static int static_global_variable = 1;

void doSomething()
{
	// 정적 변수(메모리가 static이다, 메모리가 정적으로 선언된다)
	static int a = 1;
	++a;
	cout << a << endl;
}

int main()
{
	cout << "정적 변수 테스트" << endl;
    
	// 정적 변수 테스트
	doSomething();
	doSomething();
	doSomething();
	doSomething();

	return 0;
}

정적 변수는 특정 함수가 몇번 호출되는지 디버깅할 때 쓰면 유용하다. 전역 변수 같지만 선언된 함수 블럭 내에서만 같은 메모리를 사용하며, 함수 내부에 선언되어 있다면 외부에서는 접근할 수 없다. 물론 전역 변수의 위치에서 선언하면 전역 변수처럼 접근하겠지만 이마저도 초기화된 파일에서만 가능하지 외부 파일에서의 접근은 불가능하다. 이러한 특징 때문에 내부 연결(Internal Linkage)라고 한다.

  • 4.1.1 지역 변수(Local Variables)
  • 4.1.2 지역 변수의 범위와 지속기간(Scope and Duration)

4.1.1 지역 변수(Local Variables)

 

지역 변수란 함수 내에서 정의된 변수를 뜻한다. 우리가 C++를 다룰 때 가장 흔하게 쓰는 main 함수도 함수이므로 main 함수 내부에 정의한 변수 또한 지역 변수가 된다. 

#include <iostream>

int main()
{
	using namespace std;

	int apple = 5;

	cout << apple << endl;

	return 0;
}

코드에서 apple이 지역 변수가 되겠다.


4.1.2 지역 변수의 범위와 지속기간(Scope and Duration)

 

지역 변수는 사용할 수 있는 범위와 지속기간이 존재한다. 지역 변수의 범위란 변수에 접근할 수 있는 위치를 의미하고 지역 변수의 지속기간은 변수가 생성되고 소멸하는 위치를 말한다.

 

우선 지역 변수의 지속기간을 확인해보자!

#include <iostream>

int main()
{
	using namespace std;

	int apple = 5; // 지역 변수 생성

	cout << apple << endl;

	return 0; // 지역 변수 소멸
}

앞의 예제에서 지역 변수의 생성과 소멸에 대한 위치를 표시하였다. 지역 변수가 초기화되는 순간 생성되고 함수를 벗어나게 되는 순간에 지역 변수는 소멸하게 된다.

 

지역 변수의 범위에 대해서는 여러가지 케이스로 한번 확인해보자!

#include <iostream>

int main()
{
	using namespace std;

	apple = 1; //error

	int apple = 5;

	cout << apple << endl;

	return 0;
}

지역 변수가 정의되기 이전에 사용하는 것은 불가능하다. 아직 apple이라는 변수가 생성되기 이전이기 때문에 접근할 수 없는 범위가 된다.

 

#include <iostream>

int main()
{
	using namespace std;

	int apple = 5;

	cout << apple << endl;

	return 0;
}

apple = 3; // error

함수 밖에서는 사용할 수가 없다. main 함수를 거치면서 이미 생성이 되었지만 소멸도 수행되면서 apple이라는 변수가 삭제되었으므로 접근할 수 없는 범위이다.

 

#include <iostream>

int main()
{
	using namespace std;

	int apple = 5;

	cout << apple << endl;

	{
		apple = 1;
		int apple = 3; // 내부 apple 생성
		cout << apple << endl;
	} // 내부 apple 소멸

	//apple에 커서 올리면 같은 apple이 표시된다.
	cout << apple << endl;

	return 0;
}

이번에는 내부 블럭을 하나 더 생성한 뒤에 apple이라는 지역 변수를 하나 더 생성해주었다. 이 경우 apple이 두 개가 존재하는데, 가장 가까운 범위에서 정의된 apple을 사용하게 된다. 이러한 말이 어려울 수 있는데, 요즘 visual studio는 이 부분에 대해 서포트를 해준다.

변수 클릭을 하면 같은 변수에 대해 표시해준다.

위처럼 변수를 클릭하면 같은 메모리를 가리키는 변수에 대해 친절하게 표시해준다.

  • 3.5.1 비트 플래그(Bit Flags)
  • 3.5.2 비트 마스크(Bit Masks)

3.5.1 비트 플래그(Bit Flags)

 

이전 포스팅에서 비트 연산에 대하여 다루었다. 이를 활용하는 방법 중 하나가 바로 비트 플래그가 되겠다. 기본 자료형의 경우 바이트 단위로 끊어지는데 가장 작은 1 byte가 8 bit이지만 하나의 데이터만 담을 수 있게 된다. 하지만 비트 플래그를 사용하게 되면 1 byte를 8 bit로 다룰 수 있게 되고 최대 8개의 정보를 담을 수 있게 된다.

#include <iostream>

using namespace std;

int main()
{
	bool item1_flag = false;
	bool item2_flag = false;
	bool item3_flag = false;
	bool item4_flag = false;

	// event!
	item1_flag = true;

	// die! item2 los
	item2_flag = false;

	if (item3_flag == true)
	{
		// event
	}

	if (item3_flag == true && item4_flag == false)
	{
		item3_flag == false;
		item4_flag == true;
	}

	return 0;
}

우선 비트 플래그가 아닌 일반적인 상황 중 게임에서 아이템 소지 여부에 대해 생각을 해보자. 아이템이 총 4가지가 있고 최초의 상태는 모든 아이템을 가지고 있지는 않게 된다. 그것을 나타내기 위해 boolean 자료형 4개를 사용하였다. 이벤트가 발생하고 1번 아이템을 습득하면 true로 소지하고 있음을 표현한다. 반대로 잃는 경우는 false로 하면 되겠다.

 

이렇게 아이템 4개의 소지 유무를 4개의 boolean 자료형으로 표현하다 보니 4 byte를 사용하게 되었다. 용량을 줄이기 위하여 비트 플래그를 통해 하나의 아이템 소지 유무를 1 bit로 표현해보자!

#include <iostream>
#include <bitset>

using namespace std;

int main()
{
	const unsigned char opt0 = 1 << 0;
	const unsigned char opt1 = 1 << 1;
	const unsigned char opt2 = 1 << 2;
	const unsigned char opt3 = 1 << 3;

	cout << bitset<8>(opt0) << endl;
	cout << bitset<8>(opt1) << endl;
	cout << bitset<8>(opt2) << endl;
	cout << bitset<8>(opt3) << endl;

	unsigned char items_flag = 0;

	cout << "No item " << bitset<8>(items_flag) << endl;

	// item0 on
	items_flag |= opt0;
	cout << "Item0 obtained " << bitset<8>(items_flag) << endl;

	// item3 on
	items_flag |= opt3;
	cout << "Item3 obtained " << bitset<8>(items_flag) << endl;

	// item3 lost
	items_flag &= ~opt3;
	cout << "Item3 lost " << bitset<8>(items_flag) << endl;

	// has item1 ?
	if (items_flag & opt1) { cout << "Has Item1 " << endl; }
	else { cout << "Not have Item1 " << endl; }

	// has item0 ?
	if (items_flag & opt0) { cout << "Has Item0 " << endl; }
	else { cout << "Not have Item0 " << endl; }

	// obtain item 2, 3
	items_flag |= (opt2 | opt3);

	cout << bitset<8>(opt2 | opt3) << endl;
	cout << "Item2, 3 obtained " << bitset<8>(items_flag) << endl;

	if ((items_flag & opt2) && !(items_flag & opt1))
	{
		// 가지고 있는걸 잃게 하고 없는걸 가지게 한다.
		items_flag ^= opt2;
		items_flag ^= opt1;
	}

	cout << bitset<8>(items_flag) << endl;

	return 0;
}

char 자료형 또한 1 byte 크기를 지니고 있으며 8개의 bit 자리를 가지고 있다. 그중 오른쪽 끝 bit부터 순서대로 총 4개의 bit를 opt로 표현하여 상수를 정의한다. opt가 각각 아이템을 의미하며 비트 연산을 통해 각 아이템을 지니게 되고 잃게 됨을 표현할 수 있게 된다.

$$ \begin{align*}&0000\ 0001& &0번\ 아이템\ 소유&\\ &0000\ 0010& &1번\ 아이템\ 소유&\\ &0000\ 1010& &4번\ 아이템과\ 1번\ 아이템\ 소유& \end{align*}$$


3.5.1 비트 마스크(Bit Masks)

 

비트 플래그는 비트 단위의 0과 1을 조작하는 개념이라면 비트 마스크는 조작된 비트 값에 대해 어떤 값이 존재하는지 체크해주는 것이라고 생각하면 되겠다. 

 

아래의 코드는 비트 마스크를 통해 rgb 값을 red/green/blue에 대한 수치를 뽑아내는 것을 보여주고 있다. rgb color에 대한 table이 궁금하다면 링크를 통해 확인해보자! rgb 각각 1byte를 차지하고 알파를 넣어서 총 4byte로 색을 표현하는 경우도 있다.

#include <iostream>
#include <bitset>

using namespace std;

int main()
{
	const unsigned int red_mask = 0xFF0000;
	const unsigned int green_mask = 0x00FF00;
	const unsigned int blue_mask = 0x0000FF;
    
	cout << bitset<32>(red_mask) << endl;
	cout << bitset<32>(green_mask) << endl;
	cout << bitset<32>(blue_mask) << endl;

	unsigned int pixel_color = 0xDAA520;//0x00DAA520
	cout << bitset<32>(pixel_color) << endl;

	unsigned char red, green, blue;

	blue = pixel_color & blue_mask;
	cout << "blue " << bitset<8>(blue) << " " << int(blue) << endl;
	green = (pixel_color & green_mask) >> 8;
	cout << "green " << bitset<8>(green) << " " << int(green) << endl;

	return 0;
}

16진수 표현을 통해 각 색에 대한 비트 마스크 값을 상수로 정의한다. pixel color가 있으면 각 비트 마스크 상수 값을 통해 해당하는 색의 수치만 뽑아내서 표현할 수가 있겠다. C++ 14부터는 비트 마스크를 표현하는 방식이 추가되었는데 링크를 추가해두도록 하겠다!

  • 3.4.1 이진수(Binary numbers)
  • 3.4.2 1의 보수와 2의 보수(Complement)
  • 3.4.3 bitset 표준 헤더(bitset standard header)
  • 3.4.4 비트단위 연산자(Bitwise Operators)

3.4.1 이진수(Binary numbers)

 

개발 관련 지식을 쌓다 보면 반드시 만나는 개념이 바로 이진수이다. 우리가 평소 사용하는 수 체계는 십진수로 이루어져 있지만 컴퓨터는 이진수로 모든 것을 해결한다. 컴퓨터 화면에 보이는 것들은 모두 이진수를 조합해서 보여주는 것이다. 이진수는 모든 자리가 0과 1로만 이루어져 있으며, 모든 자리수는 2의 배수로 나타낼 수 있다.

$$ 337 = 300 + 30 + 7 = 3 \times 10^2 + 3 \times 10^1 + 7 \times 10^0 $$

$$ 0101 1110_{(2)} =  0 \times 2^7 + 1 \times 2^6 + 0 \times 2^5 + 1 \times 2^4 + 1 \times 2^3 + 1 \times 2^2 + 1 \times 2^1 + 0 \times 2^0 $$

 

혹시나 이진수에 대해 다룬적이 없다면 우리 친구 위키피디아를 참고하자. 


3.4.2 1의 보수와 2의 보수(Complement)

 

컴퓨터가 -5를 구하려면 어떻게 구할까? 여기서는 두 가지 방법에 대해 소개할 것이며, 추가적인 내용이 필요한 사람이 있다면 마지막에 참고할 위키피디아를 링크로 남겨두겠다.

 

첫번째로 1의 보수를 사용하는 방법이다.

0000 0101  <- 5를 이진수로 표현
1111 1010  <- 0은 1로 1은 0으로 바꾼다.(1의 보수) 
1111 1011  <- 1을 더한 결과

여기서 1을 더하는 이유는 바로 0과 -0을 구분하지 않기 위함이다. 1의 보수만을 사용한다면 0의 경우 두 개가 존재하게 되며 1을 더했을 때 다시 원래의 수로 돌아오는 효과를 받을 수 있다.

0000 0000  <- 0를 이진수로 표현
1111 1111  <- 0은 1로 1은 0으로 바꾼다.(1의 보수) 
0000 0000  <- 1을 더한 결과(최대 자리를 넘어간 수는 버린다.)

 

두 번째로 2의 보수를 사용하는 방법이다.

1 0000 0000  <- 2의 보수를 사용하기 위한 9자리 이진수(맨 앞 숫자만 1)
0 0000 0101  <- 5에 대한 이진수(위의 숫자에서 이 수를 뺀다.) 
0 1111 1011  <- 뺄셈에 대한 결과

 

1의 보수를 구한 뒤 1을 더하는 연산과 2의 보수를 사용하는 것은 동일한 결과를 얻을 수 있게 된다.

 

추가적으로 더 공부하고 싶다면 2의 보수 위키피디아를 참고하자


3.4.3 bitset 표준 헤더(bitset standard header)

 

이진수를 정수 자료형으로도 다룰 수 있겠지만 메모리에 대한 부담이 크다면 bitset 표준 헤더를 쓰는 것도 하나의 방법이 되겠다. 코딩 문제를 풀거나 이진수 구현에서 어느 정도 서포트를 받고 싶을 때 쓰자! 참고로 컴퓨터는 비트를 다루다 보니 비트단위 연산이 자료형 단위 연산보다 빠르게 계산된다.

#include <iostream>
#include <bitset>

int main()
{
	using namespace std;

	unsigned int a = 1024;
	cout << std::bitset<16>(a) << " " << a << endl;

	unsigned int b = 0b1100;
	unsigned int c = 0b0110;
	cout << b << " " << c << endl;

	return 0;
}

a는 bitset 헤더의 도움을 받아서 작성하였다. 총 16자리로 구성된 이진법이다. 나머지 b와 c는 int 자료형에 이진법으로 담았다. 출력을 한 번 해보자!

 

bitset에 대한 추가적인 정보가 필요하다면 네이버 블로그 링크에 잘 설명되어 있으니 참고해보자!


3.4.4 비트단위 연산자(Bitwise Operators)

 

아래 코드에 주석으로 표현한 것들이 비트단위 연산자의 전부이다.

#include <iostream>
#include <bitset>

int main()
{
	using namespace std;

	// <<	left	shift
	// >>	right	shift
	// ~	not
	// &	and
	// |	or
	// ^	xor

	unsigned int a = 1024;
	cout << std::bitset<16>(a) << " " << a << endl;
	cout << std::bitset<16>(a >> 1) << " " << (a >> 1) << endl;
	cout << std::bitset<16>(a >> 2) << " " << (a >> 2) << endl;
	cout << std::bitset<16>(a >> 3) << " " << (a >> 3) << endl;
	cout << std::bitset<16>(a >> 4) << " " << (a >> 4) << endl;
	cout << std::bitset<16>(~a) << " " << (~a) << endl;
    
	unsigned int b = a << 3;
	cout << std::bitset<16>(b) << " " << b << endl;

	cout << std::bitset<4>(a & b) << endl; // bitwise AND
	cout << std::bitset<4>(a | b) << endl; // bitwise OR
	cout << std::bitset<4>(a ^ b) << endl; // bitwise XOR

	return 0;
}

shift 연산은 방향에 따라 모든 비트를 주어진 숫자만큼 이동시킨다. 최대 자리를 초과한 비트 값은 버리게 된다.

not 연산은 모든 자리를 반전시킨다. 0은 1로 만들고 1은 0으로 만든다. 이전에 다룬 1의 보수와 같은 연산이다.

and 연산은 이항 연산자이며 양 옆의 이진수에 대해서 둘다 1인 비트 단위만 1로 남기고 나머지는 0으로 채운다.

or 연산은 동일한 비트 자리에서 하나라도 1이 있으면 1로 쓰고 둘 다 0이면 0으로 쓴다.

xor 연산은 동일한 비트 자리의 수가 일치하면 0 다르면 1을 주는 연산이다.

+ Recent posts