이번 글에서는 Chapter 6의 후반부를 다루게 된다.

 

 

Chapter 6.11 메모리 동적 할당 new와 delete

 

 

이번 챕터에서는 메모리를 동적으로 할당하는 방법에 대해서 다룬다.

 

메모리가 할당되는 방식은 총 3가지이다.

 

1) Static Memory allocation, 전역 변수나 static 변수처럼 한번 만들면 프로그램이 끝날 때까지 메모리를 가지고 있는 경우이다.

 

2) 자동 메모리 할당, 변수를 선언하거나 정적 배열을 선언했을 때 블럭 밖으로 나가면 전부 사라지고 메모리를 os에 반납하는 경우

 

3) 동적 메모리 할당

 

이번 챕터에서는 마지막에 있는 동적 메모리 할당에 대해서 다루는 것이다.

 

 

지금까지는 정적으로 메모리를 할당해 왔는데, 정적으로 할당하는 메모리는 스택에 들어간다. 스택은 용량이 작기 때문에, 다루어야 하는 데이터가 큰 경우는 메모리가 부족하게 된다.

 

반면 동적으로 메모리를 할당하는 경우는 힙을 사용하게 되고, 힙은 훨씬 크다. 따라서 프로그램이 커질수록 당연히 정적 할당보다는 동적 할당을 사용하게 된다.

 

 

예제 코드 1

int main()
{
	int *ptr = new int; // 4 bytes로 받아온 다음에 메모리 주소르 우리에게 알려줍니다. 그래서 포인터로 받아야함.
	//int *ptr = new (std::nothrow) int{ 7 }; // 오류를 발생시키지 않고 밀어붙이는 방법.
	*ptr = 7;
    
	delete ptr; // 프로그램이 끝나기 전에 메모리를 제가 먼저 반납하겠습니다 라는 의미.
	ptr = nullptr; // 포인터가 가지고 있는 주소는 의미가 없는 값이다 라고 기록하는 것.

	cout << "after delete" << endl;

	// ptr의 주소가 의미가 있을 때만 dereference 하도록 만들어줌.
	if (ptr != nullptr)
	{
		cout << ptr << endl;
		cout << *ptr << endl; // 에러나옴. 
	}

 

 

new라는 키워드를 사용해서, 동적으로 할당을 받을 수 있다. 할당받은 ptr이라는 메모리 주소에, 7이라는 값을 *ptr = 7;을 통해 전달할 수 있다.

 

메모리를 할당받으려고 하는데, 다른 프로그램들이 메모리를 다 쓰고 있어서 못 받는 경우 프로그램이 죽어버리게 만들 수 있고 아니면 다른 프로그램이 다 쓸 때까지 기다렸다가 메모리를 할당받는 방법이 있다.

 

new로 동적 할당할 때 new (std::nothrow)를 걸어두면 오류를 발생하지 않고 다 쓸 때까지 기다리게 된다.

 

delete라는 키워드는 프로그램이 끝나기 전에 메모리를 먼저 반납하기 위한 방법으로, 이는 할당받은 메모리를 자발적으로 미리 os에게 돌려주는 방법이다.

 

데이터가 엄청 많아서 한 번에 데이터를 못 올리는 경우, 일부 데이터를 가지고 작업하고 다시 메모리를 반납했다가 다시 받고 이런 식으로 작업을 해야 할 수도 있고, 혹은 여러 프로그램들이 메모리를 많이 쓰는 경우에 메모리를 저 프로그램 줬다가 이 프로그램 줬다가 조절하면서 써야 할 수도 있는데 이런 경우 delete를 사용해서 적절하게 메모리 사용을 컨트롤할 수 있게 된다.

 

ptr의 메모리 주소를 이미 반납했기 때문에, 해당 포인터 변수는 의미가 없다는 것을 알려주기 위해 ptr = nullptr;로 선언해 준다.

 

ptr에 해당하는 메모리를 반납한 후에 ptr를 dereferencing 하면 당연히 에러가 발생하게 된다. 따라서 이러한 에러를 방지하기 위해서 if문으로 ptr이 nullptr가 아닌 경우에만 dereferencing 하도록 코드를 짜주는 것이 좋다.

 

 

 

예제 코드 2

int main()
{
	while (true)
	{
		// memory leak
		int *ptr = new int;
		cout << ptr << endl;
		delete ptr;
	}
}

 

while문을 이용해서 새로운 포인터를 계속 할당받게 만드는 케이스이다.

 

이때 delete로 ptr pointer를 os에 반납해주지 않으면 메모리가 끝없이 차오르게 된다.

 

이것이 C++에서 가장 난감한 문제 중 하나인 메모리 누수 현상이다.

 

따라서 적절하게 할당받은 포인터를 반납하는 것이 중요하다.

 

 

 

추가로, new와 delete를 사용하는 것은 조금 느리기 때문에, 이를 적게 사용하는 방식으로 프로그래밍하는 것이 중요하다고 한다.

 

 

 

예제 코드 3

int main()
{
	int *ptr = new (std::nothrow) int{ 7 };
	int *ptr2 = ptr;
    
	delete ptr;
	ptr = nullptr;
	//ptr2 = nullptr;
    
	cout << *ptr2 << endl;
}

 

ptr를 선언하고, 새로운 포인터인 ptr2를 선언했는데 이게 ptr랑 동일한 주소를 사용하는 경우이다.

 

그다음 줄에서 ptr를 os에 반납했기 때문에, ptr2도 사실상 nullptr가 된 상황이다.

 

그런데 만약 ptr2를 nullptr로 처리해주지 않은 상황에서 ptr2에 대해서 dereferencing을 한다면 당연히 에러가 날 수밖에 없다.

 

최근에는 스마트 포인터를 이용해서 이런 케이스를 프로그래머가 조금 덜 걱정할 수 있도록 코딩을 할 수 있도록 C++ 언어가 진화하고 있다고 한다.

 

다만, 우리가 수동으로 직접 nullptr로 처리해 주는 것보다는 속도가 조금씩 느리기는 하다고 한다. 

 

스마트 포인터 관련해서는 뒤에 다시 나온다고 하니, 추후에 나오는 내용을 봐야 할 것 같다.

 

 

 

Chapter 6.12 동적 할당 배열

 

이번 챕터에서는 기존에 정적으로 할당한 배열과 대비되는 방식인 동적 배열을 할당하는 방식에 대해서 알아본다.

 

 

예제 코드 1

int main()
{
	int length;

	cin >> length;

	//int *array = new int[length] {11, 22, 33, 44, 55, 66};
	int *array = new int[length]{};

	array[0] = 1;
	array[1] = 2;

	for (int i = 0; i < length; i++)
	{
		cout << array[i] << " " << (uintptr_t)&array[i] << endl;
	}

	delete [] array;

 

기존에 정적 배열의 경우 길이가 const int로 반드시 정해져 있어야 했지만, 동적 배열을 할당할 때는 cin으로 길이를 별도로 받아서도 선언할 수 있다.

 

이때 new 키워드를 활용해서 배열을 선언해 주면 된다.

 

동적 배열의 경우도 정적 배열처럼 indexing을 통해서 접근하면 된다. new를 사용했으니 마지막에 delete를 꼭 해주는 것을 잊지 말자. 그리고 저번 챕터에서처럼 일반 단일 변수의 경우는 delete만 적어주면 되었는데, 배열의 경우는 delete []를 해줘야 한다는 점을 알아두어야 한다.

 

 

int *array = new int [length] {11, 22, 33, 44, 55, 66}; 와 같은 방식으로 구체적인 값을 줄 수도 있으며, 만약 길이가 10인데 6번째까지만 명시적으로 값을 주었다면 나머지는 0으로 초기화된다. 

 

주의해야 할 점은 만약 위 케이스처럼 배열의 6번째까지 값을 주었는데, length를 4로 선언하게 되면 뒤에 2개는 주소를 할당받지 못해서 문제가 발생한다. 따라서 이 부분을 반드시 조심해야 한다.

 

 

new 키워드를 이용해서 배열을 만들어줄 때 length 부분(배열의 크기)은 반드시 지정해야 한다. 지정하지 않으면 컴파일이 되지 않는다.

 

그리고 동적 배열에서도 정적 배열처럼 동일하게 포인터 연산을 사용할 수 있다. *(array + 1)로 접근하면 두 번째 배열의 요소를 출력할 수 있다.

 

 

 

Chapter 6.13 포인터와 const

 

 

이번 챕터에서는 포인터에 const를 사용하는 경우에 대해서 알아본다.

 

 

예제 코드 1

int main()
{
	int value = 5;
	int *ptr = &value;
	*ptr = 6;
    
	int value = 5;
	const int *ptr = &value;
	//*ptr = 6; // const int는 포인터가 가리키는 대상을 변경할 수 없다.
	value = 6;

	cout << *ptr << endl;
}

 

윗 줄은 여태까지 써왔던 코드들과 유사하다.

 

value라는 int형 변수에 5의 값을 주고, int형 pointer 변수인 ptr에는 value의 주소를 대입해 준다.

 

그리고 역참조(dereferencing)를 이용해서 ptr의 주소가 가리키는 곳의 값을 6으로 바꿔주는 코드이다.

 

 

아래 코드는 윗 코드에 const를 적용한 경우이다.

 

int형 pointer 변수인 ptr에 const를 적용했다. const int *ptr인 경우, *ptr(dereference)를 이용해서 값을 바꿀 수 없게 된다.

 

근데 신기하게도 value = 6;으로 value라는 변수의 값을 6으로 바꿔주었을 땐 *ptr의 값이 5에서 6으로 변한다.

 

이는 const int가 dereference를 통해서 값을 바꿀 수 없다는 의미이지 ptr이 가리키는 주소의 값을 바꿀 수 없다는 것이 아니기 때문이다.

 

이게 사실 말로는 혼동이 올 수 있는데, 내가 이해할 때는 const int (*ptr)이라고 생각했다. 즉 *ptr을 하나의 변수처럼 생각하고, *ptr을 통해서 값을 바꿀 수 없다 로 이해하는 것이다.

 

 

 

예제 코드 2

int main()
{
	int value1 = 5;
	const int *ptr = &value1; // const는 가리키고 있는 주소에 있는 값을 안 바꾸겠다는 것이지 ptr 주소값을 안 바꾸겠다는 것은 아님.

	int value2 = 6;
	ptr = &value2; // ptr에 다른 주소를 주는건 가능
}

 

위에서 언급했듯이, const int *ptr의 경우는 *ptr을 이용한 값 변경만 불가능한 것이지 다른 작업은 모두 가능하다.

 

따라서 ptr가 가리키고 있는 메모리 주소는 변경할 수 있으며, 위 코드처럼 작성이 가능하다.

 

 

예제 코드 3

int main()
{
	int value = 5;
	int *const ptr = &value;
	*ptr = 10;

	int value2 = 8;
	//ptr = &value2; // pointer의 주소값을 못 바꾸는 경우
}

 

이번 코드 예제에서는 const int가 아니라, int *const ptr인 경우이다.

 

이 경우에는 pointer 변수인 ptr의 주소값을 못 바꾸는 경우이다.

 

따라서 ptr = &value2; 와 같이 ptr이 가리키는 메모리 주소를 변경할 수 없다.

 

 

예제 코드 4

int main()
{
	int value = 5;
	const int *const ptr = &value; // 바꿀 수 없으니 반드시 초기화 있어야 함.
	//*ptr = 10;

	int value2 = 7;
	//ptr = &value2;
}

 

마지막 예제이다.

 

이번에는 const int *const ptr인 케이스이다.

 

앞에서 나왔던 const int와 int *const가 모두 하나로 결합된 형태이다.

 

앞에서 언급했듯이 const int *ptr은 dereference를 통한 값 변경이 불가능했고, int *const는 ptr이 가리키고 있는 메모리 주소를 변경하는 것이 불가능했다.

 

따라서 const int *const는 두 기능이 모두 작동하여, *ptr = 10;와 같은 방식으로 dereference를 통한 값 변경이 불가능하고, ptr = &value2; 와 같은 방식으로 ptr이 가리키고 있는 메모리 주소를 변경하는 것 또한 불가능하다.

 

 

 

 

Chapter 6.14 참조 변수 reference variable

 

 

이번 강의에서는 포인터보다 조금 더 편하게 사용할 수 있는 참조 변수에 대해서 알아본다.

 

 

 

예제 코드 1

int main()
{
	int value = 5;

	int *ptr = nullptr;
	ptr = &value;

	int &ref = value; // 내부적으로 ref가 value와 같은 변수처럼 작동한다. 같은 메모리를 사용하는 것 처럼 작동함.

	cout << ref << endl;

	ref = 10; // *ptr = 10; 와 동일

	cout << value << " " << ref << endl;
}

 

참조 변수라고 하는 부분은 바로 int &ref = value; 부분에 해당한다.

 

이전에 우리가 포인터를 배웠을 때, ptr = &value;와 같이 포인터 변수에 어떤 특정 변수의 주소를 가리키도록 했었는데,

 

이와 유사하게 int &ref = value;는 ref라는 변수가 value 변수와 동일한 메모리 주소를 사용하게 된다.

 

int &ref = value;를 통해서, ref는 value라는 변수의 '별명'처럼 사용할 수 있게 된다.

 

그래서 int &ref = value;를 선언한 이후에 ref의 값을 변경하면 value의 값도 함께 변경이 된다.

 

포인터를 사용했을 때는 *ptr = 10;처럼 *을 달아서 역참조를 해야만 값을 바꿔줄 수 있었지만, 참조 변수를 사용하게 되면 역참조를 사용하지 않고도 값을 바꿔줄 수 있다는 장점이 있다.

 

 

예제 코드 2

int main()
{
	int value = 5;

	int *ptr = nullptr;
	ptr = &value;

	int &ref = value;
	
	cout << &value << endl;
	cout << &ref << endl;
	cout << ptr << endl;
	cout << &ptr << endl; // pointer 변수도 변수다. 
}

 

 

 

 

위 코드를 실행시켜 보면 다음과 같은 결과를 얻게 되는데,

 

value라는 변수의 주소는 00B5F768인 상황이고,

 

value의 참조 변수인 ref의 주소 &ref도 동일하게 00B5F768인 것을 확인할 수 있고,

 

value의 주소를 가리키고 있는 포인터 변수인 ptr의 값을 찍어보면 00B5F768인 것을 확인할 수 있다.

 

다만, 포인터 변수 또한 하나의 변수이기 때문에 포인터 변수의 주소는 별도이므로 다른 주소값이 찍히는 것을 확인할 수 있다.

 

 

 

예제 코드 3

int main()
{
	//int &ref; // 참조 변수는 반드시 초기화가 되어야 하므로 불가능
	//int &ref = 104; // 리터럴은 메모리 주소를 갖지를 못하기 때문에 사용 불가. 변수를 사용해야 함.
    
	int x = 5;
	int &ref = x;

	const int y = 8;
	//int &ref = y; // ref에서 y의 값을 바꿔버릴 수 있으므로 문법에서 허용하지 않는다. const를 사용하면 가능.
	const int &ref = y; // 가능
}

 

참조 변수의 특징으로는, 어떤 특정 변수와 동일한 메모리 주소를 가리키는 역할을 하기 때문에 반드시 초기화가 되어야 한다.

 

그리고 마찬가지로, 리터럴은 메모리 주소를 갖지 못하기 때문에 참조 변수에 리터럴을 사용할 수는 없다.

 

const는 이전 강의들에서 언급되었듯이 상수로 고정되는 기능을 가지고 있는데, const로 선언된 변수에 대해서 참조 변수를 사용할 수는 없다. 왜냐하면 같은 메모리 공간을 공유하기 때문에, 참조 변수를 이용해서 변수의 값을 변경할 수 있기 때문이다.

 

따라서 const로 선언된 변수에 대해서 참조 변수를 사용한다면, 참조 변수 또한 const를 사용하여 변수를 선언하여야 한다.

 

 

 

예제 코드 4

int main()
{
	int value1 = 5;
	int value2 = 10;

	int &ref1 = value1;

	cout << ref1 << endl; // 5

	ref1 = value2;

	cout << ref1 << endl; // 10
}

 

위 코드에서는, ref1에 value1이라는 변수를 지정해 주었고, 그 이후에 다시 value2라는 변수의 별명으로 사용한 상황이다.

 

첫 번째는 value1의 값인 5가 출력되고, 두 번째는 value2의 값인 10이 출력되는 것을 확인할 수 있다.

 

따라서 참조 변수는 참조하는 변수를 바꿀 수 있다.

 

 

 

예제 코드 5

void doSomething(int n)
{
	n = 10;
	cout << "In do something " << n << endl;
}

int main()
{
	int n = 5;

	cout << n << endl;

	doSomething(n);

	cout << n << endl;
}

 

 

 

처음에 n의 값은 5였는데, 이를 doSomething이라는 함수에 전달하는 경우이다.

 

이전 강의에서도 언급되었지만, 함수의 인자로 주는 경우 값의 주소를 전달하는 것이 아니라 값을 복사한다고 얘기를 했었다.

 

따라서 함수 내부에서 n = 10;으로 n의 값을 10으로 바꾸더라도, 함수 밖으로 나와 main 문에서 n을 찍으면 5가 유지되는 것을 확인할 수 있다.

 

 

예제 코드 6

void doSomething2(int &n)
{
	n = 10;
	cout << "In do something " << n << endl;
	cout << &n << endl;
}

int main()
{
	int n = 5;

	cout << n << endl;

	cout << &n << endl;

	doSomething2(n);

	cout << n << endl;
}

 

 

이번에는 함수의 인자로 전달하는데, 그 대신 함수에서 받을 때 참조를 사용하는 경우이다.

 

이런 경우에는 값을 복사해서 함수 내에 들고 가는 것이 아니라, 주소 자체를 전달해 줘서 main문에서의 n과 함수 내에서의 n은 같은 주소를 의미하게 된다.

 

그래서 main 문에서 &n을 찍었을 때와 함수 내부에서 &n을 찍었을 때 주소 값이 같은 것을 확인할 수 있다.

 

변수 자체를 넘겨주는 것이라고 이해하면 된다.

 

이렇게 참조를 사용하게 되면 값을 별도로 복사할 필요가 없기 때문에 훨씬 효율이 좋다고 한다.

 

만약 n의 값을 바꾸지 못하게 하고 싶다면, const int로 받으면 된다.

 

 

 

예제 코드 7

struct Something
{
	int v1;
	float v2;
};

struct Other
{
	Something st;
};

int main()
{
	Other ot;

	// 구조체가 여러개 얽혀있어서 복잡할 때 reference를 쓰면 쉽게 접근할 수 있다.
	int &v1 = ot.st.v1;
	//ot.st.v1 = 1;
	v1 = 1;
}

 

 

위 구조처럼, 구조체가 여러 개로 얽혀있는 경우, 변수를 호출하려고 할 때 여러 번 들어가야 하는 경우가 발생한다.

 

이런 경우에 참조 변수를 활용하게 되면 길게 사용하지 않고도 훨씬 쉽게 변수에 접근할 수 있다.

 

 

 

예제 코드 8

int main()
{
	int value = 5;
	int *const ptr = &value; // *const ptr는 주소값 변경 불가
	int &ref = value;
	
	// 두개가 동일.
	*ptr = 10;
	ref = 10;
}

 

ptr은 현재 value의 주소를 가리키고 있는 pointer 변수가 되고, ref는 value의 참조 변수인 상황이다.

 

pointer에서는 역참조를 활용해서 *ptr = 10;와 같은 방식으로 값을 바꿔줄 수 있는데, 이를 참조 변수를 활용하면 ref = 10;와 같이 훨씬 깔끔하게 할 수 있는 모습을 볼 수 있다.

 

참조 변수도 내부적으로는 pointer로 구현되어 있다고 한다.

 

 

Chapter 6.15 참조와 const

 

 

이번 챕터에서는 참조 변수에 const를 사용할 때 여러 가지 케이스에 대해서 다룬다.

 

 

예제 코드 1

void doSomething(const int &x)
{
	cout << x << endl;
}

int main()
{
	// ref_x를 통해서 x의 값을 바꿀 수 있어서 불가능한 코드.
	//const int x = 5;
	//int &ref_x = x; // ref_x가 바뀔 수 있음.
    
	// 둘다 const 사용하는 경우 가능
	const int x = 5;
	const int &ref_x = x;
    
	// 참조 변수는 초기화를 해야 하므로 리터럴은 불가능
	//int &ref_x = 3;
    
	const int &ref_x = 3; // const 사용 시 가능
    
	int a = 1;

	doSomething(a);
	doSomething(1);
	doSomething(a + 3);
}

 

 

첫 번째에 const int x = 5;로 선언하고 int &ref_x = x;는 이전 강의에서 언급했듯이, ref_x가 x와 동일한 메모리 공간을 가리키고 있는 상황이기 때문에 ref_x를 통해서 x를 바꿀 수 있어서 안된다는 얘기를 했었다.

 

두 번째로 x와 참조 변수 모두 const를 사용하는 경우는 문제없이 작동한다.

 

세 번째로 이 내용 또한 앞에서 언급된 내용인데, 참조 변수는 반드시 초기화를 해야 하기 때문에 리터럴을 사용하면 불가능하다.

 

그런데 네 번째에 const int &ref_x = 3;를 하는 경우는 가능하다.

 

강의에서는 이유를 설명해주질 않으셔서 제미나이를 활용해서 물어보니 이유가 다음과 같다고 한다.

 

 

일반적인 참조 변수의 경우, 이미 존재하는 변수의 별명이라고 하는 참조의 본질에 따라 메모리 공간을 사용하지 않는 리터럴은 불가능한데,

 

const 키워드를 사용하면 숫자 1을 저장할 수 있는 임시적인 메모리 공간을 생성하고, ref는 이 메모리 공간을 참조하게 된다. 숫자 1은 변경이 불가능한 값이고 ref도 const로 인해 변경 불가능하기 때문에 둘 다 불변성을 가지고 있어서 가능한 것이다.

 

마지막으로 함수의 파라미터로 const int &a를 사용하게 되면, 앞에서 설명했듯이 const의 성질에 의해서 일반 변수뿐 아니라 1과 같은 숫자를 직접적으로 사용할 수 있게 된다.

 

 

 

Chapter 6.16 포인터와 참조의 멤버 선택

 

 

이번 챕터에서는 참조를 사용할 때의 멤버 선택과, 포인터를 사용할 때의 멤버 선택에 대해서 다룬다.

 

 

예제 코드 1

#include <iostream>

struct Person
{
	int age;
	double weight;
};

using namespace std;

int main()
{
	Person person;

	person.age = 5; // . 은 멤버 selection operator. 멤버를 선택하게 해주는 연산자.
	person.weight = 30;

	Person &ref = person;

	ref.age = 15;
	ref.weight = 70;

	Person *ptr = &person;
	ptr->age = 8;
	ptr->weight = 45;

	(*ptr).age = 20; // *을 붙이는 것보다 .이 우선순위가 높아서 괄호를 쳐야함. 보통 이렇게 쓰지 않음.

	Person &ref2 = *ptr;
	ref2.age = 45;

	cout << &person << endl;
	cout << &ref2 << endl;
}

 

 

 

이번 코드에서는 레퍼런스 변수와 포인터를 이용해서 구조체의 내부 멤버 변수를 선택하는 내용들로 이루어져 있다.

 

Person 타입의 person이라는 변수의 내부 멤버인 age, weight에 접근하는 일반적인 방식은 person.age와 person.weight이다.

 

Person 타입인 참조 변수를 활용하는 경우도 동일한 방법으로. 연산자를 이용하면 된다.

 

그런데 person의 주소를 가리키는 포인터 변수를 사용할 땐. 연산자가 아니라 -> 연산자를 활용해야 한다.

 

혹은 역참조를 이용한 후 . 연산자를 사용하는 방식인 (*ptr). age로 접근하는 방법도 있다.

 

마지막으로 참조 변수 &ref2와 *ptr를 연결하면 ref2 = ptr = person으로 연결되어 person의 age에 접근하는 방법도 있다.

 

 

 

강의에서는 따로 다루진 않았어서, 제미나이한테 물어보니 다음과 같은 차이가 있다고 한다.

 

 

 

 

 

 

 

 

점 연산자로 접근하는 경우는 객체 자체로 멤버에 접근할 때 사용하는 것이고, 화살표 연산자로 접근하는 경우는 포인터를 통해 객체의 멤버에 접근할 때 사용한다고 한다.

 

그리고 ptr -> age로 하는 방식이 사실상 (*ptr). age라는 것을 알 수 있었고, *ptr는 결국 메모리 주소에 있는 실제 객체를 역참조 하는 방식이기 때문에, *ptr로 객체로 간 다음 .age를 통해서 멤버에 접근할 수 있는 것이다.

 

 

 

 

Chapter 6.17 C++ 11 For-each 반복문

 

 

이번 챕터에서는 modern C++ 기능 중 하나인 for-each 반복문을 사용하는 방법에 대해서 배운다.

 

 

 

예제 코드 1

#include <iostream>

int main()
{
	int fibonacci[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };

	for (const auto number : fibonacci)
	{
		std::cout << number << std::endl;
	}
		
	std::cout << std::endl;
	
}

 

 

 

int형 array인 finobancci를 정의하고, for (const auto number : fibonacci)로 작성하면 fibonacci라는 array에 정의된 각 요소들이 for문을 돌아가면서 하나씩 튀어나오게 된다.

 

따라서 위와 같은 코드를 작성해서 돌리면 array 내에 있는 요소들을 모두 출력해 볼 수 있다.

 

 

 

예제 코드 2

#include <iostream>

int main()
{
	int fibonacci[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
	
	// 함수에서 파라미터로 쓸 때와 동일, 참조 안 쓰면 실제 값이 변하지는 않음.
	for (auto &number : fibonacci)
	{
		number *= 10;
	}
	

	for (const auto number : fibonacci)
	{
		std::cout << number << std::endl;
	}
		
	std::cout << std::endl;
    
    
	int max_number = std::numeric_limits<int>::lowest();

	for (const auto n : fibonacci)
	{
		max_number = std::max(n, max_number);
	}

	std::cout << max_number << std::endl;
}

 

 

 

위에서 언급한 대로 for-each문을 활용하되, for문을 돌면서 number에 10씩 곱해주는 코드를 추가했다.

 

for-each문은 함수에서 파라미터로 쓸 때와 동일해서, &number가 아닌 number로만 받으면 실제 array 내 값이 변하지는 않는다. 무조건 참조 변수로 해서 받아야만 실제 array에서의 값이 바뀐다.

 

배열의 가장 큰 요소를 찾을 때 for-each문을 사용하면서 std::max를 이용하게 되면 손쉽게 찾을 수 있다.

 

 

 

추가로, for-each문은 배열을 동적 할당하는 경우 사용할 수 없다고 한다.

 

그래서 C++ 상에서 동적으로 크기가 조절되는 배열과 같은 기능을 사용하려면 std::vector를 이용한다고 한다. 

 

std::vector는 후속 강의들에서 다룬다고 한다.

 

 

이와 관련해서 제미나이에게 추가로 물어보니, 다음과 같은 답변을 들을 수 있었다.

 

 

 

 

int* dynamicArray = new int [5];와 같은 방식으로 int 5개 크기의 배열을 동적으로 할당하고, 배열에 값을 할당한 다음, for-each 방식으로 for (int value : dynamicArray)로 접근하게 되면 컴파일 에러가 발생하게 된다.

 

 

 

배열은 크기가 이미 정해져 있으니 괜찮은데, 동적 할당된 배열은 포인터일 뿐이니까 문제가 없다는 게 납득이 되었다.

 

 

 

Chapter 6.18 보이드 포인터

 

 

이번 챕터에서는 보이드 포인터에 대해서 알아본다.

 

 

예제 코드 1

#include <iostream>
using namespace std;

int main()
{
	int i = 5;
	float f = 3.0;
	char c = 'a';

	void *ptr = nullptr;

	ptr = &i;
	ptr = &f;

	cout << &f << " " << ptr << endl;
	cout << *static_cast<float*>(ptr) << endl;
	
	// int형 pointer를 사용하는 경우, 포인터 연산을 사용할 수 있음.
	int *ptr_i = &i;

	cout << ptr_i << endl;
	cout << ptr_i + 1 << endl;


}

 

 

보이드 포인터는 자료형을 void로 사용하는 포인터이다.

 

그렇다 보니, 기존에 특정 자료형으로 정해진 포인터와는 달리, 여러 가지 자료형 변수의 주소를 가리키는 포인터로 사용할 수 있다.

 

위 예제 코드에서 보듯이, int형 변수인 i에도 포인터를 사용할 수 있고, float형 변수인 f에도 포인터를 사용할 수 있다.

 

그런데, 보이드 포인터는 포인터 자체의 자료형이 없으므로 역참조를 할 때 어떤 자료형으로 표현해 주길 원하는지 알 수 없다.

 

따라서, void pointer를 사용할 때 역참조를 사용하려면 강제로 자료형을 명시해서 casting을 해줘야 하고, 복잡한 방식으로 역참조를 진행하게 된다.

 

그리고 또 하나의 차이점이 있다면, 특정 자료형의 pointer를 사용하는 경우 포인터 연산을 사용할 수 있지만 void 포인터는 포인터 연산을 했을 때 몇 byte씩 움직여야 되는지 알 수 없으므로 사용할 수 없다.

 

현실적으로 void pointer를 사용할 일이 많지는 않을 것이라고 하셨지만, '주소'라는 포인터의 특징을 이해하는 데는 도움이 되는 측면이 있다고 한다.

 

 

 

Chapter 6.19 다중 포인터와 동적 다차원 배열

 

 

이번 챕터에서는 다중 포인터와 동적으로 다차원 배열을 선언하는 방법에 대해서 다룬다.

 

 

예제 코드 1

#include <iostream>
using namespace std;

int main()
{
	int *ptr = nullptr;
	int **ptrptr = nullptr;

	int value = 5;
	ptr = &value;
	ptrptr = &ptr;

	// &value, ptr의 역참조, ptr(포인터의 주소)
	cout << ptr << " " << *ptr << " " << &ptr << endl;

	// ptr의 주소, &value, ptrptr의 주소
	cout << ptrptr << " " << *ptrptr << " " << &ptrptr << endl;

	// value의 값
	cout << **ptrptr << endl;	
}

 

 

지금까지는 포인터를 1개만 사용했지만, 이중 포인터를 사용하는 예제를 다뤄본다.

 

ptr이라는 포인터 변수는 현재 value라는 변수의 주소를 가리키도록 설정해 두었고, ptrptr이라는 이중 포인터 변수는 ptr의 주소를 가리키도록 했다.

 

줄줄이 사탕처럼 ptrptr -> ptr -> value의 구조라고 보면 될 것 같다.

 

그래서 ptr을 출력하면, value의 주소를 얻게 되고,

 

*ptr을 출력하면 value의 값인 5가 출력되고,

 

&ptr을 출력하면 ptr의 주소가 출력된다.

 

ptrptr은 ptr의 주소를 가리키고 있으니, &ptr와 같은 값을 얻는다.

 

*ptrptr는 ptr이라고 볼 수 있고, 이는 &value와 같아서 value의 주소를 얻는다.

 

&ptrptr는 ptrptr 포인터의 주소를 나타낸다.

 

**ptrptr는 역참조를 두 번 하는 것이라서, *(*ptrptr) -> *(ptr) -> value의 값을 얻게 되어 5가 된다.

 

 

 

예제 코드 2

 

#include <iostream>
using namespace std;

int main()
{

	const int row = 3;
	const int col = 5;

	const int s2da[row][col] =
	{
		{1, 2, 3, 4, 5},
	{6, 7, 8, 9, 10},
	{11, 12, 13, 14, 15}
	};


	
	int **matrix = new int*[row];

	for (int r = 0; r < row; ++r)
	{
		matrix[r] = new int[col];
	}

	for (int r = 0; r < row; ++r)
	{
		for (int c = 0; c < col; ++c)
		{
			matrix[r][c] = s2da[r][c];
		}
	}

	for (int r = 0; r < row; ++r)
	{
		for (int c = 0; c < col; ++c)
			cout << matrix[r][c] << " ";
		cout << endl;
	}

	// delete
	for (int r = 0; r < row; ++r)
	{
		delete[] matrix[r];
	}

	delete[] matrix;	
}

 

 

다음은 이중 포인터 + 동적 할당을 하는 예제 코드이다.

 

행과 열을 const int로 정의하고, 2차원 배열을 먼저 선언해 준다.

 

 

 

2차원 배열을 동적 할당할 수 있도록 이중 포인터 matrix를 정의하고, row 크기만큼만 new로 정의해 준다.

 

개념적으로 생각해 보면, 2차원 배열은 col 크기 짜리의 1차원 배열이 row 개수만큼 있는 것이기 때문에, 이중 포인터 matrix는 row 크기만큼만 정의해 주면 된다. 

 

그러고 나서 for문을 활용해서, matrix[r]에 col 크기의 int array를 new로 만들어준다.

 

 

2중 for문을 사용해 matrix의 [r][c] 위치에 s2da 배열의 [r][c]의 값을 전달해 주는 방식으로 matrix를 채워 넣는다.

 

matrix[r][c]을 이용해서 출력해 보고, delete[]를 해줄 때는 matrix의 row 단위로 처리해 주고 마지막에 matrix 전부를 delete 해준다.

 

 

 

예제 코드 3

#include <iostream>
using namespace std;

int main()
{
	const int row = 3;
	const int col = 5;

	const int s2da[row][col] =
	{
		{1, 2, 3, 4, 5},
	{6, 7, 8, 9, 10},
	{11, 12, 13, 14, 15}
	};

	// 1차원 array를 2차원인 것 처럼 사용해야 함
	
	int *matrix = new int[row*col];

	for (int r = 0; r < row; ++r)
	{
		for (int c = 0; c < col; ++c)
		{
			matrix[c + col * r] = s2da[r][c];
		}
	}

	for (int r = 0; r < row; ++r)
	{
		for (int c = 0; c < col; ++c)
			cout << matrix[c + col * r] << " ";
		cout << endl;
	}
	
}

 

이번 예제는 이중 포인터를 사용하지 않고, 1차원 array를 동적 할당으로 만들어준 다음 마치 2차원인 것처럼 처리하는 예제이다.

 

matrix라는 이름의 int type의 array를 동적 할당 해주고, 그 대신 크기는 row*col 만큼 해준다.

 

 

다음으로 2중 for문을 사용해서 matrix의 각 위치에 값을 전달해 주는데, 2차원 indexing이 불가능하기 때문에 1차원으로 바꾸어서 사용해야 한다.

 

현재 코드상에서는 c + col * r와 같은 방식으로 index를 잡아주고 있는데, 이는 다음과 같이 생각하면 된다.

 

지금 예제에서 보면 [1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]로 5개 크기의 array가 3개가 있는 셈이다.

 

이를 2차원이 아니라 일렬로 줄 세워서 추가해 준다고 보면 된다.

 

즉, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]로 추가해줘야 한다는 의미이다.

 

이렇게 처리하려면, 우선 0번째 row에서 0번째 col부터 마지막까지 쭉 추가를 해준다. 실제 예시에서는 [1, 2, 3, 4, 5]를 추가해 주는 것이다.

 

이 부분이 바로 c가 된다.

 

다음으로 1번째 row에서 0번째 col부터 마지막까지 쭉 추가를 해준다. 실제 예시에서는 [6, 7, 8, 9, 10]를 추가해 주는 것이다.

 

이 부분은 c + col * 1이 된다. 즉, col 크기만큼 한번 추가해 줬으니, c + col이 되는 것이다.

 

이후로 col이 계속 추가되니, col * r를 추가해 주는 개념이라고 보면 된다.

 

 

 

Chapter 6.20 std::array 소개

 

이번 챕터에서는 C style array의 단점을 보완한 std::array에 대해서 알아본다.

 

 

예제 코드 1

#include <iostream>
#include <array>

using namespace std;

void printLength(array<int, 5> my_arr)
{
	cout << my_arr.size() << endl;
}

int main()
{
	//int c_array[5] = { 1, 2, 3, 4, 5 };
	
	array<int, 5> my_arr = { 1, 2, 3, 4, 5 };
	my_arr = { 0, 1, 2, 3, 4 };
	my_arr = { 0, 1, 2, };

	//cout << my_arr[10] << endl; // index가 크기를 넘는지 아닌지 체크 안하고 그냥 작동함
	//cout << my_arr.at(10) << endl; // index가 크기를 넘는지 체크해보고 문제가 발생하면 예외처리 발생. 조금 느림.

	cout << my_arr.size() << endl;
	printLength(my_arr);

	for (auto &element : my_arr)
	{
		cout << element << " ";
	}
	cout << endl;

	my_arr = { 1, 21, 3, 40, 5 };

	//std::sort(my_arr.begin(), my_arr.end()); // 오름차순
	std::sort(my_arr.rbegin(), my_arr.rend()); // 내림차순

	for (auto &element : my_arr)
	{
		cout << element << " ";
	}
	cout << endl;
}

 

 

기존 c style array는 int c_array[5] = {1, 2, 3, 4, 5}; 와 같은 방식으로 정의했었다.

 

그러나 std::array를 사용하는 경우에는 std::array<int, 5> my_array = {1, 2, 3, 4, 5};와 같은 방식으로 정의한다.

 

기존 array와 std::array와의 차이를 보자면, 이전 강의에서도 다뤘듯이 array는 함수의 파라미터로 전달할 때 포인터로 처리된다.

 

만약 void doSomething(int arr[]) 이런 식으로 int형 array를 받게 되었을 때, arr는 포인터이기 때문에 sizeof를 사용하더라도 int형 포인터 변수의 size를 알려주는 것이지 배열의 사이즈를 알려주는 게 아니다.

 

이는 앞의 강의들에서 언급되었듯이 배열이 커졌을 때 이를 전부 복사하기에는 부담스럽기 때문이다.

 

하지만 std::array는 기본적으로 자료형과 크기가 픽스된 상태이기 때문에, 함수의 파라미터로 전달하더라도 이에 대한 정보를 가지고 있게 된다.

 

 

 

C style array에서는 없었던 .at 이라는 기능이 있는데, 이는 범위 검사 기능을 가지고 있어 out of index 에러가 발생했을 때 이를 미리 컴파일 단계에서 체크해서 에러를 발생시킨다.

 

그리고 std::array는 .size()를 사용할 수 있고, 이는 array의 사이즈를 출력해 준다.

 

void printLength 함수에서 my_arr.size()를 했을 때 결과가 나오는 것을 통해 std::array는 함수의 파라미터로 사용되었을 때도 size에 대한 정보를 가지고 있음을 확인할 수 있다.

 

 

최근 수업에서 다루었던 for-each 문도 std::array에 대해서 사용 가능하다는 점.

 

 

std::sort를 사용하면 정렬이 가능하고, begin()과 end()를 사용하면 오름차순, rbegin()과 rend()를 사용하면 내림차순이다.

 

 

 

Chapter 6.21 std::vector

 

이번 챕터에서는 array를 동적할당으로 쉽게 사용할 수 있도록 해주는 std::vector에 대해서 다룬다.

 

 

예제 코드 1

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	// 동적 할당
	std::vector<int> array;

	std::vector<int> array2 = { 1, 2, 3, 4, 5 };

	cout << array2.size() << endl;

	std::vector<int> array3 = { 1, 2, 3 };

	cout << array3.size() << endl;

	std::vector<int> array4 { 1, 2, 3 };

	cout << array4.size() << endl;
	
}

 

 

이전에 array를 동적 할당 하려면 int *arr = new int[5];와 같은 방식으로 만들어주었는데, std::vector를 사용하면 더욱 쉽게 동적 할당을 할 수 있다.

 

단순히 이름만 정의해도 되고, array2처럼 요소들을 같이 정의해도 된다.

 

std::vector는 항상 길이에 대한 정보를 가지고 있기 때문에, 함수에 파라미터로 전달했을 때 길이에 대한 정보를 잃게 되는 일이 없다.

 

std::vector는 .size()를 이용하면 배열의 사이즈를 바로 확인할 수 있다.

 

 

예제 코드 2

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	// 메모리 관리 유용하게.
	// 동적 메모리 할당의 장점을 충분히 활용할 수 있다.
	int *my_arr = new int[5];

	vector<int> arr = { 1 ,2 ,3 ,4 ,5 };

	arr.resize(10); // 나머지는 0으로 채우고 resize 진행

	for (auto &itr : arr)
	{
		cout << itr << " ";
	}
	cout << endl;

	cout << arr.size() << endl;
	cout << arr[1] << endl;
	cout << arr.at(1) << endl;

	delete[] my_arr;
}

 

 

이전 강의에서 얘기했듯이, 동적 할당은 반드시 delete 명령어를 통해 메모리에서 해제해 주는 작업을 해줘야 한다.

 

근데 변수가 적으면 괜찮지만, 변수가 많을 때 이를 일일이 다 delete로 관리하는 일은 쉽지 않을 것이다.

 

std::vector는 delete로 처리해주지 않아도 알아서 되기 때문에 훨씬 편하다.

 

for-each 문도 사용이 가능하고, resize라는 기능도 있어서 배열의 사이즈를 조절할 수 있다.

 

 

 

여기까지 Chapter 6을 모두 다루었다.

 

워낙 챕터가 길다 보니까, 정리하는데 긴 시간이 걸렸다.

 

확실히 이번 챕터부터 내용이 조금 어렵고 중요하다는 생각이 든다.

 

Chapter 7로 찾아오겠습니다.

이번 글에서는 Chatper 6을 정리해보려고 합니다.

 

 

 

 

다른 Chapter 대비 분량이 많아서, 6.1부터 6.10까지 한번 끊고 6.11부터 6.21까지 다른 글로 정리해보겠습니다.

 

 

Chapter 6.1 배열 기초 [1 of 2]

 

이번 Chapter 에서는 배열의 기초에 대해서 다루게 된다. 배열 내용이 많다보니, 2개 강의에 걸쳐 설명하신다고 한다.

 

 

 

예제 코드 1

using namespace std;

struct Rectangle
{
	int length;
	int width;

};

int one_student_score; // 1 variable
int student_scores[5]; // 5 int

student_scores[0] = 100;
student_scores[1] = 80;
student_scores[2] = 90;
student_scores[3] = 50;
student_scores[4] = 0;
//student_scores[5] = 20;

for (int i = 0; i < 5; i++)
{
	cout << student_scores[i] << endl;
}

Rectangle rect_arr[10];

rect_arr[0].length = 1;
rect_arr[0].width = 2;

 

배열을 사용하는 방법은 단순하다. 자료형 변수명[크기] 순으로 사용하면 된다.

 

위 코드에서는 int student_scores[5]로 사용하였다. 이전에는 변수 1개를 선언했다면, 배열은 같은 자료형의 여러 개의 변수가 담길 수 있는 그릇을 선언하는 셈이다.

 

배열의 값에 접근할 때는 []를 사용하며, index를 이용해서 값을 주거나 출력을 할 수 있다.

 

이전에 배웠던 여러 변수들을 함께 정의하는 방법인 구조체를 사용해서도 동일한 방식으로 배열을 선언할 수 있다.

 

rect_arr에서 0번째를 접근할 때는 [0]로 접근하고, 구조체 내 변수들의 값을 핸들링할 때는 . 을 사용하여 접근할 수 있다.

 

 

 

예제 코드 2

enum StudentName
{
	JACKJACK, // 0
	DASH,     // 1
	VIOLET,   // 2
	NUM_STUDENTS	 // 3
};

//int my_array[5] = { 1, 2, 3, 4, 5 };
//int my_array[5] = { 1, 2, };
//int my_array[] = { 1, 2, 3, 4, 5 };
int my_array[]{ 1, 2, 3, 4, 5 };

for (int cnt = 0; cnt < size(my_array); cnt++)
{
    cout << my_array[cnt] << endl;
}

int student_scores[NUM_STUDENTS];

student_scores[JACKJACK] = 0;
student_scores[DASH] = 1;
student_scores[VIOLET] = 2;


const int num_students = 20;
//cin >> num_students;

// compile time에 length가 반드시 결정되어 있어야 함
int student_scores[num_students];

 

배열을 선언할 때 직접적으로 값을 주면서 선언하는 방법은 여러가지가 있다.

 

int my_array[5] = { 1, 2, 3, 4, 5 }; 처럼 배열의 크기와 배열 내 요소들의 값을 명시적으로 주는 방법도 가능하고,

 

int my_array[5] = { 1, 2, }; 처럼 만드는 경우 3번째부터 5번째까지의 element는 값이 모두 0이다.

 

int my_array[] = { 1, 2, 3, 4, 5 }; 처럼 만들면 값을 명시적으로 준 경우라 배열의 크기를 유추할 수 있으니 5개짜리 배열이 만들어진다.

 

int my_array[]{ 1, 2, 3, 4, 5 }; 처럼 만드는 것도 가능하다.

 

 

이전에 배웠던 enum을 활용하는 방법도 가능하다.

 

enum 안에 NUM_STUDENTS 변수를 만들어두면, 보기 좋게 배열의 크기를 선언할 수 있다는 점이 눈에 띈다.

 

 

마지막으로 배열의 크기에는 반드시 상수가 들어가야만 한다. 

 

그래서 단순히 int형 변수를 선언한 후 배열 선언할 때 배열의 크기로 사용하면 에러가 나며, 변수가 상수가 될 수 있도록 const를 사용해주면 배열을 선언할 때 배열의 크기에 변수를 사용할 수 있다.

 

 

Chatper 6.2 배열 기초 [2 of 2] array

 

챕터 6.1에 이어서 배열에 대해서 알아본다. 챕터 6.1에서는 기본적인 배열이 무엇인지에 대해서 알아보았다면, 6.2에서는 포인터와 메모리 주소 관점에서 다룬다.

 

 

예제 코드 1

int main()
{
	const int num_students = 20;

	// 배열의 이름 = 배열의 식별자, 내부적으로 주소로 사용됨.
	int students_scores[num_students] = { 1, 2, 3, 4, 5, };

	// array는 첫번째 주소를 가지고 있음.
	cout << (int)&students_scores << endl;

	// 배열은 주소 연산자를 붙이지 않아도 주소를 얻을 수 있다.
	// 배열은 주소로 데이터를 주고 받는 것이 원소를 전부 복사하는 것 보다 효율적이기 때문에 문법이 이렇게 되어 있음.
	cout << (int)students_scores << endl;
    
    	cout << (int)&students_scores[0] << endl;
}

 

 

예제 코드 1번에서 다루는 내용은, 우선 첫 번째로 배열의 이름은 배열의 식별자로 내부적으로 주소로 사용된다는 점이다.

 

 

C++에서 어떤 변수의 주소를 확인하려면 &를 붙여서 확인해야 하는데, 배열의 경우 변수명만 출력해도 해당 배열의 주소를 확인할 수 있다.

 

그리고 특이한 점은 array가 첫 번째 주소를 가지고 있다는 것이다. 

 

따라서 stduent_scores로 출력하나, &students_scores로 출력하나, &students_scores[0]로 출력하나 모두 다 같은 주소를 가리키게 된다.

 

 

예제 코드 2

int main()
{
	const int num_students = 20;

	int students_scores[num_students] = { 1, 2, 3, 4, 5, };

	cout << (int)&students_scores[0] << endl;
	cout << (int)&students_scores[1] << endl;
	cout << (int)&students_scores[2] << endl;
	cout << (int)&students_scores[3] << endl;
}

 

 

 

20개의 int 변수로 이루어진 배열에서, 첫 번째 요소의 주소, 두 번째 요소의 주소, 세 번째 요소의 주소, 네 번째 요소의 주소를 각각 찍어보면 4씩 이동하는 것을 알 수 있다.

 

따라서 배열은 일종의 20개의 집이 붙어있는 주택 단지 같은 개념이고, 한 집씩 이동할 때 마다 주소가 4씩 이동한다고 이해하면 된다.

 

 

예제 코드 3

void doSomething(int student_scores[])
{
	cout << (int)&student_scores << endl;
	cout << (int)&student_scores[0] << endl; // 첫 번째 주소값을 출력하면 밖에서 정의한 주소랑 같음.
	cout << student_scores[0] << endl;
	cout << student_scores[1] << endl;
	cout << student_scores[2] << endl;
	cout << "Size? : " << sizeof(student_scores) << endl; // 넘어올 때 포인터로 넘어왔음. 포인터 사이즈가 4라는 의미임.
}

int main()
{
	const int num_students = 20;

	// 배열의 이름 = 배열의 식별자, 내부적으로 주소로 사용됨.
	int students_scores[num_students] = { 1, 2, 3, 4, 5, };

	// array는 첫번째 주소를 가지고 있음.
	cout << "address in main " << (int)&students_scores << endl;

	// 함수의 parameter로 넣어줄 수 있다.
	doSomething(students_scores);
}

 

 

예제 코드 3번에서 다루고 있는 내용은, 함수의 parameter로 배열을 전달할 수 있다는 점이다.

 

다만, 굉장히 독특한 부분이 있는데, 함수의 parameter로 전달된 것은 배열이 아니라 포인터이다.

 

즉, 우리가 생각하기에 함수 doSomething의 parameter인 student_scores[]는 당연히 배열이라고 생각하지만, 컴파일러는 내부적으로 이를 포인터로 처리하게 된다.

 

 

추가로, 또 독특한 부분 중 하나가 바로 함수 내부에서 &student_scores를 할 때와 밖에서 &student_scores를 할 때가 주소가 다르다는 점인데, 이는 함수 내부에서의 student_scores는 '배열의 주소 값을 저장하는 다른 변수'이기 때문이다.

 

그래서 함수 내부에서의 &student_scores를 찍어보면 주소 값을 저장하는 다른 변수의 주소가 출력이 된다.

 

그런데 첫 번째 주소값을 출력하면 밖에서 정의한 주소와 같다. (&student_scores[0])

 

 

마지막으로 함수 내에서 sizeof(student_scores)를 하면 4로 출력되는데, 이는 student_scores가 포인터이기 때문이며 포인터의 사이즈가 4라는 의미이다.

 

 

Chapter 6.3 배열과 반복문

 

 

이번 챕터에서는 배열에 반복문을 사용하는 케이스에 대해서 알아본다.

 

 

예제 코드 1

#include <iostream>

using namespace std;

int main()
{
	const int num_students = 5;

	int scores[num_students] = { 84, 92, 76, 81, 56 };

	//const int num_students = sizeof(scores) / sizeof(int); // 파라미터로 넘어간 array인 경우 포인터이므로 이와 같은 방식으로는 불가능.

	int total_score = 0;
	int max_score = 0;
	int min_score = numeric_limits<int>::max();

	// < 인지 <= 인지 확인.
	for (int i = 0; i < num_students; i++)
	{
		total_score += scores[i];
		max_score = (max_score < scores[i]) ? scores[i] : max_score;
		min_score = (min_score > scores[i]) ? scores[i] : min_score;
	}

	double avg_score = static_cast<double>(total_score) / num_students;

	cout << max_score << endl;
	cout << min_score << endl;
	
}

 

 

이번 챕터에서는 배열에 반복문을 사용해본다.

 

이전 챕터에서도 얘기했지만, 배열의 크기는 반드시 상수를 사용해야 하므로, const int로 선언된 변수를 가지고 배열의 크기를 선언해야하고, 배열 내 값들은 { }를 활용하면 된다.

 

만약 위와 같은 케이스일 때는 배열 scores의 sizeof를 변수 타입인 int의 sizeof로 나누면 student의 수를 구할 수 있다. 다만, 이전 예제에서 나온 것 처럼 함수의 파라미터로 배열을 사용하는 경우, 파라미터는 배열 그 자체가 아니라 배열의 주소를 저장하는 포인터이므로 이와 같은 방식으로 사용하면 이상한 값을 구하게 된다.

 

 

다음으로는 for문과 배열을 활용해서 배열의 합, 최댓값, 최솟값을 구하는 방법이다.

 

배열의 값은 인덱스를 이용해서 접근할 수 있으며, 합은 단순하게 for문을 돌리면서 int 변수에 더해주면 된다.

 

최댓값의 경우, 0 값을 가지는 변수로부터 시작해서, for문을 돌면서 배열의 각 index의 값과 비교해서 큰 경우 해당 변수에 값을 저장하도록 하는 원리를 이용해서 구할 수 있다.

 

위 예제 코드상에서는 삼항 연산자로 작성했으나, if문 활용해서 작성도 가능하다.

 

최솟값은 최댓값과 반대로 작성하면 구할 수 있다.

 

 

Chapter 6.4 배열과 선택 정렬 selection sort

 

 

이번 챕터에서는 배열을 이용해서 선택 정렬을 배워본다.

 

 

예제 코드에 들어가기 전에, 선택 정렬(selection sort)가 어떻게 이루어지는지부터 설명하고, 이를 코드로 옮겼을 때 어떻게 구현이 가능한지 살펴본다.

 

예를 들어서 배열이 array = {3, 5, 2, 1, 4}로 구성되어 있다고 가정하자.

 

선택 정렬은 첫 번째 값과 그 이후에 있는 값들을 반복적으로 비교하면서 가장 작은 값을 찾고, 이를 서로 교체해준다.

 

그 다음으로는 두 번째 값과 그 이후에 있는 값들을 반복적으로 비교하면서 가장 작은 값을 찾고, 이를 서로 교체해준다.

 

이렇게 쭉 반복해서 마지막에서 두번째 값과 그 이후에 있는 값을 비교한 다음 작으면 교체해준다.

 

 

첫 번째 step에서는, 3과 그 이후의 5, 2, 1, 4와 비교를 하게 된다. 

 

5는 3보다 크니 넘어가고, 2는 3보다 크니 교체 후보가 된다.

 

1은 2보다 크니 교체 후보가 되며, 4는 1보다 작으니 교체될 수 없다. 그래서 첫 번째 step에서는 3과 1을 교체한다.

 

그럼 두 번째 step에서는 array가 {1, 5, 2, 3, 4}로 바뀐 후 작업을 진행한다.

 

동일한 방식으로, 이번엔 5와 나머지 2, 3, 4과 비교를 하게 된다.

 

2가 5보다 크니 후보가 되고, 그 후에 3, 4는 2보다 크니 최종적으로는 2가 교체 후보가 된다.

 

다음 세 번째 step에서는 array가 {1, 2, 5, 3, 4}가 된다.

 

같은 방식으로 다음 단계에서는 {1, 2, 3, 5, 4}가 되고, 마지막으로는 {1, 2, 3, 4, 5}가 된다.

 

 

이를 코드로 구현하게 되면 다음과 같다.

 

 

const int length = 5;

int array[length] = { 3, 5, 2, 1, 4 };

printArray(array, length);


for (int i = 0; i < length - 1; i++)
{
    int idx = i;

    for (int j = i + 1; j < length; j++)
    {
        if (array[idx] > array[j])
        {
            idx = j; // 맨 처음 index보다 작을 때 마다 계속 갱신, 최종적으로 가장 작은 값의 index가 됨.
        }
    }

    int tmp = array[i];
    array[i] = array[idx];
    array[idx] = tmp;

}

 

for문을 총 두개를 써야한다.

 

가장 메인으로 비교할 대상은 첫 번째, 두 번째, 세 번째 ,네 번째 있는 숫자로 이동하기 때문에 먼저 한 번의 for문이 필요하다.

 

그리고 메인으로 비교할 대상과 그 이후에 있는 숫자들을 비교해야 하니, 이 때 for문이 한번 더 필요하게 된다.

 

첫 번째 for문은 i = 0부터 length - 1 까지 진행되는데, 이는 배열의 마지막 요소는 비교할 대상이 없기 때문이다.

 

두 번째 for문은 j = i+1부터 length까지 진행된다.

 

메인으로 비교할 대상인 array[idx]와, 그 이후에 있는 array 요소들인 array[j]와 비교해서 만약 작으면, idx의 값을 바꿔준다.

 

이 작업을 계속 진행하면, 나머지 숫자들 중 가장 작은 숫자의 index를 얻을 수 있게 된다.

 

이렇게 해서 가장 작은 숫자의 index를 얻었다면, 기존 array[i]의 값을 tmp라는 변수에 저장한 다음, i번째 array 요소를 idx번째 array요소로 바꿔주고, idx번째 array 요소를 tmp 값으로 바꿔준다.

 

 

Chapter 6.5 정적 다차원 배열

 

이번 챕터에서는 기존 1차원 배열에서 확장하여, 다차원 배열에 대해서 다룬다. 

 

 

#include <iostream>

using namespace std;

int main()
{
	const int num_rows = 3;
	const int num_columns = 5;

	// row-major <-> column-major
	// 두 번째 꺼는 꼭 값을 추가해줘야함.
	// 첫 번째 꺼는 뺄 수 있음.

	// int array[num_rows][num_columns] = {0}; // 이런식으로 전부 0으로 만들 수 있음.
	
	int array[num_rows][num_columns] =
	{
		{1, 2, 3, 4, 5},	 // row 0
	    {6, 7, 8, 9, 10},	 // row 1
	    {11, 12, 13, 14, 15} // row 2
	};
	


	for (int row = 0; row < num_rows; row++)
	{
		for (int col = 0; col < num_columns; col++)
		{
			cout << array[row][col] << '\t';
			//cout << (int)&array[row][col] << '\t'; // 실제로는 결국 1차원으로 이루어진 것을 접어서 표현한 것이다.
		}

		cout << endl;
	}
	

}

 

 

1차원 배열을 선언했던 것과 비슷하게, 2차원 배열을 선언할 때는 array[3][5]와 같이 각각의 크기를 입력해주면 된다. 단, 2차원일 때는 왼쪽이 row이고 오른쪽이 column의 개수가 된다.

 

특이하게도 row는 빼도 되지만, column의 개수는 빼면 오류가 난다.

 

1차원 배열때와 비슷하게, 각각의 값은 { }를 이용해서 입력해주면 배열의 값을 채워줄 수 있다.

 

 

2차원 배열의 경우, 행과 열이 있는 표와 같은 형식이기 때문에 모든 요소를 출력하려면 for문을 row 쪽으로 한번, column 쪽으로 한번해서 총 두 번을 써야한다.

 

그리고 array 내 값들의 주소를 확인해보면, 배열이 실제로는 2차원이더라도 결국 본질적으로는 1차원이 이어져있는 형태임을 확인할 수 있었다.

 

 

 

첫 번째 row의 마지막 column이 9435320인데, 두 번째 row의 첫 번째 column은 9435324로, 4만큼 차이가 난다.

 

이 얘기는 결국 [0][0] -> [0][1] -> [0][2] -> [0][3] -> [0][4] -> [1][0] -> [1][1] -> [1][2] -> [1][3] -> [1][4] -> [2][0]... 이런식으로 연결되어 있음을 의미한다.

 

그저 1차원 배열을 잘 접어서 2차원처럼 보이도록만 만들어준 것이지 컴퓨터 내부적으로는 쭉 이어져있는 형태인 것이다.

 

 

Chapter 6.6 C언어 스타일의 배열 문자열

 

 

이번 챕터에서는 C언어 스타일로 문자열을 표현하는 방식을 배운다. 추후 문자열을 쓸 때는 std::string을 사용하지만, C언어 스타일의 문자열 사용방법도 중요하다고 한다.

 

 

 

예제 코드 1

char myString[] = "string";


// 마지막에 0이 나옴 -> Null character. 겹따옴표로 되어 있는 문자열은 \0이 하나 들어있음.
for (int i = 0; i < 7; i++)
{
    cout << (int)myString[i] << endl;
}


cout << sizeof(myString) / sizeof(char) << endl;

 

 

char type으로 문자열을 선언한 다음, 해당 변수에 마우스를 올려보면 "string"인데도 크기가 7로 찍힌다.

 

string은 문자가 6개인데, 왜 7이 찍히는걸까?

 

이를 확인해보기 위해, for문을 이용해서 해당 char 변수의 각 자리에 어떤 문자열이 있는지 확인해본다.

 

 

 

확인해보면 마지막에 빈 문자열이 있는 것을 알 수 있는데, 이를 int로 casting해서 보면 다음과 같다.

 

 

 

이를 확인해보면, char 타입의 문자열은 가장 마지막에 int 기준으로 0인 Null character가 있는 것을 확인할 수 있다.

 

이처럼 겹따옴표로 되어 있는 문자열은 "\0" 이라고 하는 빈칸이 존재한다.

 

 

 

예제 코드 2

char myString[255];

cin >> myString;

myString[0] = 'A'; // 배열하고 똑같은 방식으로 처리할 수 있다.

cout << myString << endl;

int ix = 0;
while (1)
{
    if (myString[ix] == '\0')
    {
        break;
    }

    cout << myString[ix] << " " << (int)myString[ix] << endl;
    ++ix;
}

 

이번에는 char type의 변수에 cin을 활용해서 문자열을 받아본다.

 

myString[0] = 'A';와 같은 방식을 통해서 cin을 통해 받은 문자열의 일부를 수정할 수 있다.

 

 

 

apple을 입력했을 때, Apple로 변하는 것을 확인할 수 있다.

 

 

예제 코드 3

char myString[255];

cin >> myString;

myString[4] = '\0'; // null character가 나오기 전까지만 출력하기 때문에, 이렇게 되면 그 이후 문자 끊김. 

cout << myString << endl;

int ix = 0;
while (1)
{
    if (myString[ix] == '\0')
    {
        break;
    }

    cout << myString[ix] << " " << (int)myString[ix] << endl;
    ++ix;
}

 

만약 문자열의 중간 위치쯤에 문자열을 \0로 바꾸면 어떻게 될까?

 

 

 

banana라는 문자열을 cin으로 받았을 때, myString[4] = '\0'로 바꾸면 4번째 문자열까지만 출력이 된다.

 

 

 

예제 코드 4

char myString[255];

std::cin.getline(myString, 50000);

cout << myString << endl;

int ix = 0;
while (1)
{
    if (myString[ix] == '\0')
    {
        break;
    }

    cout << myString[ix] << " " << (int)myString[ix] << endl;
    ++ix;
}

 

만약 cin을 받을 때 띄어쓰기가 발생하더라도 문자열을 받아야 하는 상황이라면 어떻게 해야할까?

 

단순히 cin >> 으로 받을게 아니라, cin.getline을 이용해서 받는 방법이 있다.

 

 

 

apple banana를 입력했을 때, 처음부터 끝까지 다 들어오는 것을 확인할 수 있으며 중간에 빈칸은 int로 casting 했을 때 32인 것을 보면 null character와 다른 것을 알 수 있다.

 

 

 

 

예제 코드 5

#include <cstring>

char source[] = "Copy this!";
char dest[50];
strcpy_s(dest, 50, source); // 메모리 침범이 해킹이 될 수 있기 때문에 막아주기 위해서 최대 복사할 수 있는 메모리 사이즈를 강제로 적어주도록 함.

cout << source << endl;
cout << dest << endl;

 

C언어에서 많이 사용하게 되는 기능이라고 하는데, C++에서도 동일한 기능을 지원한다.

 

strcpy라는 함수는 source에 있는 문자열을 dest로 복사해준다.

 

단, 지금은 strcpy_s를 사용하여야 하고, destination의 사이즈를 명시해줘야한다.

 

이는 메모리 침범이 해킹이 될 수 있기 때문에 이를 막기 위해 최대로 복사할 수 있는 메모리 사이즈를 강제로 적어주도록 하는 것이라고 한다.

 

 

 

위 코드를 이용하면, 사이즈만 정의해준 dest라는 변수에 source와 동일한 문자열이 복사된 것을 확인할 수 있다.

 

 

 

예제 코드 6

#include <cstring>

char source[] = "Copy this!";
char dest[50];
strcpy_s(dest, 50, source); // 메모리 침범이 해킹이 될 수 있기 때문에 막아주기 위해서 최대 복사할 수 있는 메모리 사이즈를 강제로 적어주도록 함.

strcat_s(dest, source);
cout << source << endl;
cout << dest << endl;

cout << strcmp(source, dest) << endl;

 

strcpy와 비슷한 느낌으로, 문자열을 합쳐주는 기능을 가진 함수인 strcat도 사용해본다.

 

 

 

strcat_s를 사용하면 다음과 같이 source에 있는 문자를 dest 뒤에다가 합쳐준다.

 

 

strcmp는 두 문자열을 비교해주는 함수인데, 같으면 0을 리턴해주고, 동일하지 않으면 -1을 리턴해준다. 

 

 

위 케이스에서는 dest와 source가 서로 다르므로, -1 값을 출력하게 된다.

 

 

 

Chapter 6.7 포인터의 기본적인 사용법

 

 

이번 챕터에서는 포인터에 대해서 다루게 된다.

 

 

 

예제 코드 1

int x = 5;

cout << x << endl;
cout << &x << endl; // & : address-of operator
cout << (int)&x << endl;

cout << *(&x) << endl;

 

변수를 선언한다는 것은, Operating System으로부터 변수를 선언할 공간을 빌려오는 것이다.

 

위 코드에서처럼, int x = 5; 라고 선언했다면 이것은 OS로부터 어떤 특정 메모리 공간을 빌려온 다음, 그 공간에 5라는 값을 복사해서 사용하는 것이다. 따라서 모든 변수는 내부적으로 메모리 주소를 가지고 있다.

 

&는 앰퍼샌드로, 변수의 주소를 알 수 있는 operator이다.

 

 

 

위 예시 코드 1번을 돌리면 위 사진처럼 나오게 되는데, 0096FBAC가 바로 x라는 변수의 메모리 주소라고 볼 수 있다.

 

*는 de-reference operator로, 포인터가 "저쪽 주소에 가면 이 데이터가 있어요"라고 가리키는 것에 직접적으로 접근해서 어떤 값이 있는지를 들여다보는 기능을 한다.

 

&x가 변수 x의 주소를 나타내기 때문에, *(&x) 는 x의 메모리 주소에 접근해서 어떤 값이 있는지를 확인하는 것이며, x가 5 이므로 5가 나오는 것을 확인할 수 있다.

 

 

포인터란? 

 

포인터는 변수다. 메모리의 주소를 담는 변수다.

 

포인터는 왜 필요한가?

 

1) array에 데이터가 엄청 많을 때, 이 array를 함수 파라미터로 넣어주면 전부 모든 값을 다시 복사를 해야 함.

 

여기에 for문을 이용해서 여러 번 복사하면 엄청 느려지게 됨. 따라서 포인터로 첫 번째 주소하고 데이터의 개수만 알려주는 방식으로 효율적으로 사용할 수 있게 해줌.

 

2) 변수를 여기저기서 사용을 해야 될 경우가 있는데, 그때 매번 변수를 직접 보내면 복사를 해야 하니 부담이 된다. 

 

 

 

예제 코드 2

int x = 5;
double d = 1.0;

int *ptr_x = &x; // 초기화 하지 않으면 de-reference 시도 시 에러 발생함.
double *ptr_d = &d;

cout << ptr_x << endl;
cout << *ptr_x << endl;

cout << ptr_d << endl;
cout << *ptr_d << endl;

cout << typeid(ptr_x).name() << endl;

cout << sizeof(x) << endl;
cout << sizeof(d) << endl; 
cout << sizeof(&x) << " " << sizeof(ptr_x) << endl; 
cout << sizeof(&d) << " " << sizeof(ptr_d) << endl;

 

포인터는 int *ptr_x = &x; 의 형식으로 사용한다.

 

포인터도 자료형을 가지고 있는데, 이는 추후 de-reference 할 때 어떤 자료형으로 값을 출력해줘야 하는지 알아야 하기 때문이다.

 

 

 

ptr_x를 출력하면, 00DAF7EC와 같은 메모리 주소 값이 나오는 것을 알 수 있다. 따라서 ptr_x라는 포인터 변수는 메모리 주소를 가지고 있는 변수이다.

 

다음으로 *ptr_x는 포인터 변수(메모리 주소)를 de-reference 한 것이므로 변수 x의 값인 5를 출력하게 된다.

 

ptr_d와 *ptr_d는 동일한 원리로 이루어진다.

 

 

그 다음으로 확인해볼 것은 sizeof 값이 어떻게 나오는지이다.

 

x는 int 변수이고, d는 double 변수 이기 때문에, sizeof를 사용하게 되면 각각 4와 8이 나오는 것을 확인할 수 있다.

 

그런데 신기하게도 &x와 &d, ptr_x, ptr_d는 모두 크기가 4이다.

 

주소는 말그대로 메모리 주소이기 때문에, 주소 자체를 저장하는 변수의 크기는 고정이기 때문이다.

 

32비트 운영체제에서는 포인터의 크기가 4이며, 64비트 운영체제에서는 포인터의 크기가 8이 된다.

 

 

Chapter 6.7a 널 포인터 Null Point

 

 

이번 챕터에서는 포인터 중 특수한 케이스인 널 포인터에 대해서 다룬다.

 

 

예제 코드 1

void doSomething(double *ptr)
{
    if (ptr != nullptr)
    {
        // do something useful.
        std::cout << *ptr << std::endl;
    }
    else
    {
        // 포인터 주소가 제대로 들어온게 아니구나. 아무것도 하지 말아야지.
        // do nothing with ptr
        std::cout << "Null ptr, do nothing" << std::endl;
    }
}

int main()
{
    //double *ptr = 0; // c-style로는 0을 넣어줌.
    //double *ptr = NULL;
    double *ptr = nullptr; // modern c++

    doSomething(ptr);
    doSomething(nullptr);

    double d = 123.4;

    doSomething(&d);

    ptr = &d;

    doSomething(ptr);
}

 

null pointer라는 것은, 포인터에 null이 들어가있는 경우를 의미한다.

 

포인터는 메모리 주소를 저장하는 변수이기 때문에, 포인터가 null이라는 것은 유효하지 않은, 아무것도 가리키지 않는 주소를 포인터가 담고 있는 경우라고 생각하면 된다.

 

double *ptr = 0; 으로 작성하거나, double *ptr = NULL;로 작성하거나, double *ptr = nullptr;로 작성할 수 있다. 가장 마지막인 nullptr가 가장 널리 사용되는 경우라고 한다.

 

포인터 변수가 유효한지를 확인하기 위해 if (ptr != nullptr)라는 조건문을 활용해서 점검할 수 있다.

 

 

 

결과를 보면 알 수 있듯이, nullptr를 사용하면 (ptr != nullptr) 조건에 걸리지 않는 것을 볼 수 있고, double 변수인 d의 주소인 &d를 사용하거나, 이 주소를 저장하고 있는 변수인 ptr를 사용하게 되면 정상적으로 해당 조건에 걸리는 것을 확인할 수 있다.

 

 

예제 코드 2

void doSomething(double *ptr)
{
	// 파라미터로 넘어오는 변수는 여기서 다시 선언이 되고
	// argument로 들어온 변수에 들어있는 값이 복사가 되는것이다.

	std::cout << "Address of pointer variable in doSomething() " << &ptr << std::endl;
    std::cout << "ptr in func ? :" << ptr << std::endl;
    
	if (ptr != nullptr)
	{
		// do something useful.
		std::cout << *ptr << std::endl;
	}
	else
	{
		// 포인터 주소가 제대로 들어온게 아니구나. 아무것도 하지 말아야지.
		// do nothing with ptr
		std::cout << "Null ptr, do nothing" << std::endl;
	}
}

int main()
{
	double *ptr = nullptr; // modern c++
	double d = 123.4;

	ptr = &d;

	doSomething(ptr);

	std::cout << "Address of pointer variable in main() " << &ptr << std::endl;
	std::cout << "ptr in main() ? :" << ptr << std::endl;
}

 

 

 

 

main() 안에서 확인해보면 ptr의 주소가 008FF7EC인 것을 알 수 있는데, 이를 doSomething 함수에 파라미터로 넣었을 때 doSomething 함수문 안에서 주소를 찍어보면 008FF7E8로 다른 주소가 찍히는 것을 확인할 수 있다.

 

이를 통해 알 수 있는 것은, 함수의 파라미터로 넘어오는 변수는 함수 내부에서 다시 선언이 되고, 파라미터로 들어온 변수에 들어있는 값이 복사가 되는 개념이라는 것이다.

 

main() 안에서 ptr의 값을 찍으면 008FF7F0인데, 동일한 방식으로 function 안에서 ptr의 값을 찍으면 동일하게 008FF7F0이 나오는 것을 확인할 수 있다.

 

즉, ptr이 가지고 있던 메모리 주소의 값은 함수의 파라미터로 넘어가면서 잘 전달이 된 것을 확인하였으나, ptr의 주소는 달라진 것이다. 

 

이를 비유를 통해 표현하자면, ptr은 그릇이고 ptr이 가지고 있는 &d(메모리 주소)는 음식이다.

 

함수를 호출할 때, ptr를 전달하게 되면, 그릇이 다시 바뀌지만 안에 있는 음식은 그대로 유지해서 전달되는 상황인 것이다.

 

 

 

 

Chatper 6.8 포인터와 정적 배열

 

 

이번 챕터에서는 포인터와 정적 배열은 어떤 관계에 있는지에 대해서 알아본다.

 

사실상 포인터와 정적 배열은 같다.

 

예제 코드 1

int array[5] = { 9, 7, 5, 3, 1 };

for (int i = 0; i < size(array); i++)
{
    cout << array[i] << endl;
}

// Array는 배열이 아니라 포인터이다.
// 포인터는 주소를 담고 있는데, 배열은 첫 번째 바이트의 주소를 담는다.
cout << array << endl;
cout << &(array[0]) << endl;

// de-reference 하면 첫 번째 배열의 값이 나옴.
cout << *array << endl;

int *ptr = array; // 
cout << "ptr? : " << ptr << endl;
cout << "de-reference ptr? : " << *ptr << endl;

char name[] = "jackjack";
cout << *name << endl;

 

 

 

첫 번째 예제 코드에서는 포인터와 정적 배열의 관계에 대한 기본적인 내용들을 다룬다.

 

사실 이전 강의에서도 어느정도 다룬 내용들도 함께 포함되어 있다.

 

우선 정적 배열(array)의 요소들을 출력하려면 index를 통해서 접근하면 된다는 사실은 이미 이전에 나온 바가 있고, 이를 for문으로 구현해서 모든 배열의 요소를 출력해본 것이다.

 

 

 

array를 cout하면 배열의 주소가 나오는 것을 알 수 있고, 배열은 첫 번째 바이트의 주소를 담고 있기 때문에 첫 번째 요소의 주소를 출력하면 동일한 값을 얻을 수 있다. 이를 표현한게 &(array[0]) 라고 보면 된다.

 

이를 역으로 생각하면, 정적 배열의 주소를 dereference하게 되면 첫 번째 배열의 값을 구할 수 있다. 그래서 *array 를 cout하면 9라는 첫 번째 배열 값을 얻을 수 있다.

 

 

int형 pointer인 ptr를 선언하고, 여기에 array 값을 준다. 이러면 ptr은 array의 주소를 가지고 있게 된다. 

 

따라서 ptr를 그대로 출력하면 array를 출력한 결과, &(array[0])를 출력한 결과와 동일하게 메모리 주소를 출력하게 된다.

 

그리고 ptr은 결국 배열 array의 주소를 가지고 있으니, ptr를 dereference해서 *ptr로 출력하면 *array한 것과 동일한 결과인 첫 번째 배열의 값인 9가 나오게 되는 것이다.

 

 

마지막으로는 간단하게 char type의 name이라는 배열을 만들어주고, 여기에 문자열을 주게 되면, 아까 숫자로 이루어진 배열과 동일하게 작동하는 것을 알 수 있다. 동일하게 name은 당연히 배열의 주소를 가지고 있고, 이를 dereference하게 되면 배열의 첫 번째 값을 얻을 수 있으니, 출력 결과는 문자열의 첫 번째 값인 j를 얻게 된다.

 

 

 

예제 코드 2

//void printArray(int array[]) // int array[]로 받아도 동일함
void printArray(int *array)
{
	// array처럼 보이더라도 내부적으로는 포인터임.
	cout << "size of array: " << sizeof(array) << endl;
	cout << "De-referenceing: " << *array << endl;
	*array = 100;
}

int main()
{
    int array[5] = { 9, 7, 5, 3, 1 };

    cout << sizeof(array) << endl; // int 4 byte x 5개 = 20

    int *ptr = array;

    cout << "size of ptr: " << sizeof(ptr) << endl; // 포인터 변수 자체의 사이즈가 4 byte

    printArray(array);

    // 함수 안에서 바뀐건데 함수 밖에서도 값이 변함.
    for (int i = 0; i < size(array); i++)
    {
        cout << array[i] << endl;
    }

    // 포인터 연산(pointer arithmetic)
    cout << *ptr << " " << *(ptr + 1) << endl;
}

 

 

 

먼저 sizeof(array)를 하게 되면, 배열의 size를 출력하는 것이기 때문에 int형 변수가 1개당 4 byte로, 5개가 있으니 20 byte라는 것을 알 수 있다.

 

int *ptr = array;로 해주면 ptr이라는 int형 pointer 변수에 array의 주소를 전달하게 되고, sizeof(ptr)은 포인터 변수의 사이즈를 묻는 것이니 4가 나온다. 이는 int형 pointer라서 그런게 아니라, 32비트 체계에서는 메모리 주소의 사이즈가 항상 4이기 떄문이다. 64비트로 바꾸면 8이 나오게 된다.

 

 

printArray 함수는 배열인 array의 주소를 array라는 변수 이름으로 전달 받는다.

 

printArray 함수 내 array라는 변수는 결국 포인터이니, 함수 내에서 sizeof(array)를 하면 포인터의 size를 묻는 코드라서 4가 나온다.

 

그리고 *array를 하게 되면 array 주소에 대해 dereference 하면 예제 코드 1번과 동일한 원리로 첫 번재 배열 값인 9를 얻을 수 있다.

 

printArray 함수 내에서 *array = 100;라는 코드가 있는데, 이는 array를 dereference한 결과에 100을 복사한 것이다. 

 

array를 dereference하면 첫 번째 배열 값을 얻는 것이니, 결국 첫 번째 배열의 값을 100으로 바꿔주는 역할을 하는 것이다.

 

함수 내에서 *array의 값을 바꿔주더라도 main 문 안에서 array의 값을 뽑아보면 첫 번째 배열의 값이 100으로 변한 것을 확인할 수 있다.

 

마지막으로 포인터 연산이라는 부분이 있는데, *(ptr + 1) 으로 출력하면 두 번째 배열의 값을 얻을 수 있다.

 

 

 

예제 코드 3

struct MyStruct
{
	int array[5] = { 9, 7, 5, 3, 1 };
};

// array가 struct나 class 안에 들어가있는 경우에는 
// 포인터로 강제 변환이 되지 않는다. array 자체가 간다.

void doSomething(MyStruct ms)
{
	cout << "doSomething size of ms array: " << sizeof(ms.array) << endl;
}

int main()
{
    int array[5] = { 9, 7, 5, 3, 1 };
    
    MyStruct ms;
	cout << ms.array[0] << endl;
	cout << "main () size of ms array: " << sizeof(ms.array) << endl;

	doSomething(ms);
}

 

 

 

이번에는 array를 그냥 사용하는 경우가 아니라, 구조체 내부에 정의하는 경우이다.

 

MyStruct라는 구조체 타입으로 ms 변수를 정의하고, ms 내부에 있는 array에 접근할 때는 . 연산자를 이용하면 된다.

 

따라서 ms.array[0]로 접근하면 ms 내부에 있는 array의 첫 번째 요소를 얻을 수 있다. 그래서 결과가 9가 나오는 것이다.

 

 

 

sizeof(ms.array)를 출력하면 array의 크기를 알 수 있고, 20인 것을 확인할 수 있다.

 

정적 배열은 함수에 전달될 때 포인터로 전달되기 때문에 주소를 이용해서 전달이 되었는데, struct나 class의 경우는 포인터로 강제 변환이 되지 않는다. 따라서 ms.array로 출력이 바로 가능하다.

 

만약 ms 변수가 구조체로 전달되는 것이 아니라, 정적 배열처럼 전달이 되는 거였다면 결국 ms는 포인터이니, 포인터에 array가 없으므로 당연히 오류가 날 것이다. 

 

따라서 doSomething 함수 내에서 ms에서 array를 직접적으로 바로 접근이 가능하다는 것은, ms가 포인터가 아니라는 것을 의미한다.

 

 

 

doSomething(&ms); 로  MyStruct 변수의 주소를 전달해주면,

 

doSomething의 파라미터로 MyStruct *ms로 바꿔주어 포인터 변수로 받아야하고,

 

이렇게 되는 경우 ms는 포인터가 되니, ms를 dereference 해서 구조체로 만들어준 다음 array를 접근해야하기 때문에 (*ms).array로 접근해야만 구조체 내부에 array에 접근할 수 있다.

 

 

 

 

Chapter 6.9 포인터 연산과 배열 인덱싱

 

 

이번 챕터에서는 포인터 연산에 대해서 알아본다. 이는 배열 인덱싱을 하던 것과 굉장히 유사한 방식으로 이루어진다.

 

 

예제 코드 1

int main()
{
	double value = 7;
	double *ptr = &value;

	// ptr int형이면 4개씩, double형이면 8개씩 움직인다. short면 2개씩 움직임.
	// pointer의 데이터 타입이 필요한 이유.
	// 1) dereference 할 때 어떤 자료형으로 만들어줘야할지 알아야 하기 때문.
	// 2) 포인터 연산을 할 때 1을 더한다고 하면 실제로 몇 바이트인지 알아야 하니까.
	cout << uintptr_t(ptr) << endl;
	cout << uintptr_t(ptr+1) << endl;
}

 

 

 

double형 변수인 value를 선언하고, value의 주소를 가지고 있는 변수인 ptr를 선언해준다.

 

uintptr_t는 메모리 주소를 int형 변수로 보여주기 위한 것으로, ptr의 메모리 주소를 보면 17823544이나 (ptr+1)의 메모리 주소를 보면 17823552인 것을 볼 수 있다. 

 

이처럼 포인터 변수에 +1을 해주면 포인터의 자료형의 크기 만큼 이동하게 된다.

 

현재 ptr은 double형 변수이므로, 8 byte씩 이동하게 된다.

 

만약 ptr이 int형이였다면, 4 byte씩 이동하게 될 것이다.

 

이처럼 메모리의 주소를 담고 있는 포인터 변수에 연산을 할 수 있다.

 

 

예제 코드 2

int main()
{
    int array[] = { 9, 7, 5, 3, 1 };
    int *ptr = array;
    for (int i = 0; i < 5; i++)
    {
        //cout << array[i] << " " << (uintptr_t)&array[i] << endl;
        cout << *(ptr + i) << " " << (uintptr_t)(ptr + i) << endl;
    }
}

 

 

 

int형 배열인 array를 선언해주고, array의 주소를 가지고 있을 int형 pointer 변수인 ptr를 선언해준다.

 

ptr + i를 dereference 하면 배열 내에서 for문을 돌면서 배열 내 요소들을 출력할 수 있고, 예제 코드 1번에서 포인터 연산을 보여주었듯이, ptr은 배열 array의 첫 번째 주소를 가지고 있으므로 ptr + i를 해주면 int형 변수이니 4 byte씩 이동하는 것을 확인할 수 있다.

 

 

 

예제 코드 3

int main()
{
	char name[] = "Jack jack";

	const int n_name = sizeof(name) / sizeof(name[0]);

	char *ptr = name;

	for (int i = 0; i < n_name; i++)
	{
		cout << *(name + i);
	}

	cout << " " << endl;

	while (1)
	{

		if (*ptr == '\0')
		{
			break;
		}

		cout << *ptr;
		ptr++;
	}
}

 

 

 

 

이번에는 char type의 배열 name을 선언해주고, 배열 name의 메모리 주소를 가지고 있는 char type pointer 변수인 ptr를 선언해준다.

 

위 예제 코드하고 유사하게, name 변수는 배열 name의 첫 번째 메모리 주소를 가지고 있으니, (name + i)은 주소를 한 칸씩 이동하는 것이고, 이를 *를 통해서 dereferencing하게 되면 배열 name에 있는 문자열들을 for문을 이용해서 순차적으로 출력할 수 있다.

 

아래에 while문은 연습문제로 내주신 것인데, char 타입인 name은 맨 마지막에 우리 눈에는 보이지 않지만 null character를 가지고 있다는 특성이 있다.

 

따라서 이를 이용해서, while 문으로 무한 반복하게 만든 다음, null character가 발생했을 때 break를 하도록 만들면 반복해나가면서 문자열을 출력할 수 있다.

 

char type pointer인 ptr은 name의 첫 번째 메모리 주소를 가지고 있으니, *ptr를 하면 name의 첫 번째 요소인 J를 우선적으로 출력한다. 그 다음 ptr++를 해주면 ptr + i를 해주는 효과로, 메모리 주소상으로 한 칸씩 이동을 하게된다.

 

단 while의 경우 break를 반드시 해줘야하는데, break를 해주는 조건이 *ptr가 "\0"가 되는 경우이다. 따라서 이 조건을 추가해주게 되면 name에 있는 문자열을 모두 출력할 수 있다.

 

 

마지막에 while 문을 작성하는 연습문제를 해주면서, 사실 조금 헤매고 있었던 부분이 바로 *ptr == '\0'인지, 아니면 *ptr == "\0"인지 였다. 처음에 "\0"인줄 알고 작성했는데 계속 자료형이 맞지 않다고 해서 한참 고민했었는데, C++에서 작은따옴표와 큰따옴표가 다르다는 것을 제대로 알고 있지 못했다. 내가 주력으로 쓰는 python의 경우 두 케이스 모두 사실상 같은 것으로 인정되기 때문에 큰 차이 없이 사용할 수 있다.

 

따라서 이번 기회에 작은 따옴표와 큰 따옴표를 조금 정리하고 넘어가려고 한다. 아래 자료는 gemini를 사용해서 만든 자료이다.

 

 

 

작은따옴표의 경우 단일 char 타입이며, 문자 1개를 나타내는 것이다. 따라서 위 예제 코드에서 *ptr이 null character와 같은지 확인하기 위해서는 char type인 '\0'를 사용했어야 하는 것이다.

 

반면, 큰 따옴표의 경우 단일 문자가 아닌 문자열이며, 자료형은 const char*이다. 문자열은 마지막에 null character를 포함하고 있기 때문에 메모리 상에서 문자열 길이 + 1 바이트를 가지고 있게 된다.

 

분명 문자열을 사용하는 케이스가 많을태니, 두 따옴표 간 차이를 분명하게 알고 있어야겠다.

 

 

 

Chapter 6.10 C언어 스타일의 문자열 심볼릭 상수

 

 

이번 챕터에서는 문자열의 기호적 상수에 대해서 다루게 된다.

 

 

예제 코드 1

const char* getName()
{
	return "Jack jack";
}

int main()
{
	//char *name = "Jack Jack"; // 포인터는 메모리의 주소를 가리키기만 할 수 있기 때문에 불가능.
    
	const char *name = "Jack Jack"; // 기호적인 상수처럼 사용할 수 있음. const 이용할 시
	const char *name2 = "Jack Jack";
    
	// const char*를 return하는 getName()을 이용해서도 동일하게 구현 가능.
	//const char *name = getName();
	//const char *name2 = getName();
    
    
	cout << (uintptr_t)name << endl;
	cout << (uintptr_t)name2 << endl;
}

 

 

앞에서 다루었듯이 포인터는 메모리 주소를 가지고 있는 변수이다. 따라서 pointer 변수에 문자열 값을 줄 수는 없다.

 

그런데 pointer 변수에 문자열 값을 줄 수 있는 방법이 있는데, 바로 const를 이용하는 것이다.

 

신기하게도 const char type으로 포인터를 선언하면 문자열을 줄 수 있다.

 

그리고 똑같은 문자열을 다른 이름의 포인터에 각각 주면, 두 변수가 모두 같은 위치의 메모리 주소를 가지고 있게 된다.

 

컴파일러가 name과 name2가 같으니, 메모리를 같이 쓰라고 하는 것이다. 

 

만약 name2의 문자열을 name하고 다르게 만들어주면, 다른 메모리 주소를 가지고 있는 것을 확인할 수 있다.

 

추가로, const char type의 pointer를 return 해주는 함수인 getName()을 이용해서도 구현이 가능하다.

 

 

 

예제 코드 2

int main()
{
	int int_arr[5] = { 1, 2, 3, 4, 5 };
	char char_arr[] = "Hello, World!";
	const char *name = "Jack Jack";

	cout << int_arr  << endl;
	cout << char_arr << endl;
	cout << name << endl;
}

 

 

 

 

앞 강의들에서 계속 언급했듯이, 배열은 포인터이다. 그래서 int_arr를 출력하게 되면 당연히 int_arr의 메모리 주소를 출력하는 것이 맞다.

 

그런데 char type인 char_arr는 cout으로 출력을 하면 char_arr의 메모리 주소를 출력하는 것이 아니라, 이 array가 가지고 있는 문자열을 그대로 출력해준다. 

 

마찬가지로 const char 타입의 pointer인 name도 pointer임에도 불구하고 메모리 주소를 출력하는 것이 아니라 문자열을 그대로 출력해준다.

 

이는 cout에서 문자열은 특별히 처리를 하기 때문이다. 문자의 포인터가 들어왔을 때 이건 문자열이 아닐까? 하고서 메모리 주소를 출력하는 것이 아니라 array 자체를 쭉 출력해주게 되는 것이다.

 

 

 

예제 코드 3

int main()
{
	char c = 'Q';
	cout << &c << endl;
}

 

다음은 char type 변수의 주소를 cout으로 출력하면 어떻게 될까?

 

 

 

Q는 분명 잘 나왔는데, 그 이후에 이상한 문자가 섞여 나오는 것을 확인할 수 있다.

 

cout이 보기에는 문자열인가보다 하고 생각한 것이다. c가 문자열이라고 가정하고 null character가 나올 때까지 출력한 것인데, c는 실제론 문자열이 아니라 캐릭터 타입이다보니 그 뒤에 null character가 보장되지 않는 상황이다. 따라서 c 변수 이후의 메모리 영역에 접근하게 되고, 이 영역의 내용은 예측할 수 없다. 그래서 쓰레기 값이나 이상한 문자열이 출력될 수 있는 것이다.

 

 

 

 

Chapter 6이 워낙 길다보니, 6.1부터 6.10까지 전반부로 정리해보았다.

 

Chapter 6부터는 배열이나 포인터 등 중요한 내용들이 많이 나오고 있다보니 굉장히 흥미로운 것 같다.

 

챕터의 후반부는 다음 글에서 이어서 작성할 예정이다.

 

 

이번 글에서는 Chapter 5 내용을 정리해보려고 합니다.

 

 

 

 

 

 

Chapter 5.1 제어 흐름 개요 (Control Flow)

 

 

코드 흐름을 제어하는 방법에 대해서 다룬다는 intro 강의이다.

 

 

중단, 점프, 조건 분기, 반복 까지는 이번 챕터에서 다루고, 예외 처리는 뒤에서 다룬다고 한다.

 

 

std::cout << "I love you " << std::endl;

exit(0); // 프로그램을 강제 종료

std::cout << "I love you " << std::endl;

 

exit(0);를 사용하면, 강제로 해당 프로그램을 원하는 위치에서 강제종료 할 수 있다.

 

 

배포되는 코드에 사용되기보다는, 프로그래밍을 분석하거나 디버깅하는 용도로 사용한다고 보면 된다.

 

 

 

 

Chapter 5.2 조건문 if

 

 

이번 강의에서는 조건문 if에 대해서 다룬다. 다른 언어를 다뤄보신 분이라면 C++도 큰 그림에서는 그다지 차이가 없기 때문에 손쉽게 익힐 수 있다.

 

 

 

예시 코드 1

int x;
cin >> x;

if (x > 10)
{
    cout << x << " is greater than 10" << endl;
}
else
{
    cout << x << " is not greater than 10" << endl;
}


if (x > 10)
{
    cout << "x is greater than 10" << endl;
}
else if (x < 10)
{
    cout << "x is less than 10" << endl;
}
else
{
    cout << "x is 10" << endl;
}

 

 

기본적인 사용법인데, if 안에 조건을 걸어주고 해당 조건을 만족하면 그 아래 코드가 실행되는 식이다.

 

{ }를 사용하지 않아도 if 문을 사용할 수 있지만, 그럴 경우 1줄만 실행되며 여러 줄 실행이 불가능하다. 

 

필자는 이미 { }를 사용하는 것에 너무 익숙해져서 기본적으로는 다 { }로 묶어주고 있다. C++는 특히 scope가 중요해서 { }로 묶어서 표현하는 게 인지하기가 편하다고 생각하고 있다.

 

기본적으로는 if / else 로 분기할 수 있고, 조금 더 세부적으로 사용하는 경우 if /else if / else로 분기할 수 있다.

 

 

 

예시 코드 2

int x;
cin >> x;

if (x > 10)
{
    cout << "A" << endl;
}
else if (x == -1)
{
    exit(0);
}
else if (x < 0)
{
    cout << "B " << endl;
}

cout << "Hello " << endl;


if (x > 10)
    ; // null statement. 아무것도 하지 않음

if (x = 0) // x = 0; -> if (x) 판단. 주의해야함.
    cout << x << endl;

 

 

if 문을 활용해서 어떤 조건을 만족시킬 때 프로그램이 강제 종료되도록 만들 수 있다. 이전 강의에서 언급된 exit(0)를 활용하는 방식이다.

 

그리고 if 문에 아무것도 하지 않게끔 코드를 작성할 수도 있다고 한다. 단, 이렇게 코드에 아무것도 없는 경우는 다른 사람들이 봤을 때 그 이유를 알 수 있도록 주석을 작성해 주는 게 바람직해 보인다.

 

만약 if (x = 0)으로 작성해버리면, 우선 x에 0을 할당하고 if (x)를 판단하게 된다. 따라서 실제로는 해당 if 문 안에 작성된 코드는 실행이 되지 않는다. 

 

일반적으로는 if (x == 0)를 작성하다가 실수로 =를 빼먹는 경우이기는 하지만, if (x = 0)인 경우 어떻게 코드가 돌아가는지는 알아두는 것이 좋다고 한다.

 

 

Chapter 5.3 switch-case

 

 

이번 강의에서는 if문과 비슷한 듯 하지만 특정 케이스에서 더 깔끔하게 코드를 만들어주는 방식인 switch-case문에 대해서 알아본다.

 

 

 

예제 코드 1

#include <iostream>

using namespace std;

enum class Colors
{
	BLACK,
	WHITE,
	RED,
	GREEN,
	BLUE
};

void printColorName(Colors color)
{
	switch (static_cast<int>(color))
	{
	case 0:
		cout << "Black" << endl;
		break;
	case 1:
		cout << "White" << endl;
		break;
	case 2:
		cout << "Red" << endl;
		break;
	case 3:
		cout << "Green" << endl;
		break;
	case 4:
		cout << "Blue" << endl;
		break;
	}
}



int main()
{
	
	printColorName(Colors::BLACK);
    
}

 

색깔을 나타내는 enum class가 있는 상황에서, 색깔을 input으로 받아서 색을 출력해 주는 함수인 printColorName 함수를 실행하려고 한다.

 

이때, 들어온 input에 따라서 각각에 대응하는 출력을 해줄 수 있도록, switch 문에 color를 static cast로 int로 변환해 준 뒤, case 옆에 각각 0, 1, 2, 3, 4에 따라 어떤 내용을 출력해 줄지를 작성해 준다.

 

주의해야 할 점은 break;를 적지 않으면 그다음 case문도 순차적으로 실행된다.

 

예를 들어서, switch의 값이 2였을 때, case 2에 있는 내용이 실행되고, break가 없는 경우 그 뒤에 case 3과 case 4도 작동하게 된다. 따라서 이를 감안해서 case 2만 작동해야 한다면 반드시 break;를 걸어줘야 한다.

 

 

예시 코드 2

int x;
cin >> x;

switch (x)
	{
		int a; // 선언은 가능하지만
		// int b = 5; // 초기화는 불가능. (메모리 할당 불가, case문 다음에서만 할 수 있음)

	case 0:
	{
		int y = 5;
		y = y + x;
		cout << y << endl;
		break;
	}
	case 1:
	{
		int y = 10;
		y = y + x;
		cout << "y: " << y << endl;
		break;
	}
	default: // case에서 정의되지 않는 모든 경우
		cout << "Undefined input: " << x << endl;
		// break;  // default는 아래에 다른 코드가 없으므로 break가 필요 없음.
	}

 

switch 문에서 특이한 부분이 있다. 우선 swtich문 안에서 변수에 대한 선언은 가능하지만, 초기화는 불가능하다는 점이다. case 문이 오기 전에 int b = 5;처럼 변수를 초기화해주면 메모리 할당이 불가능하여 빌드가 안된다.

 

그리고 case문 안에서 지역 변수 따로 선언해서 사용할 수 있으며, case 문에 해당되지 않는 케이스(2, 3, 4...)에 대응하는 방안으로는 default:를 사용하는 방법이 있다. 

 

 

 

 

Chapter 5.4 Goto

 

 

원하는 소스 코드의 위치로 이동할 수 있는 방법인 goto에 대해서 알아본다.

 

 

예시 코드 1

#include <iostream>
#include <cmath>

using namespace std;

int main()
{
	double x;

tryAgain : // label

	cout << "Enter a non-negative number" << endl;

	cin >> x;

	if (x < 0.0)
	{
		goto tryAgain;
	}

	cout << sqrt(x) << endl;
}

 

goto는 원하는 코드 위치로 이동할 수 있는 구문이다.

 

특이하게 goto를 사용할 때 선언하는 변수는 일반 코드와 indentation이 조금 다르다.

 

goto문을 사용하면, 특정 조건일 때 원하는 코드 위치로 이동할 수 있으며 이는 for문이나 while문과 같이 반복문 형태와 비슷하게 만들 수 있다.

 

 

Chapter 5.5 반복문 while

 

 

이번 강의에서는 while을 사용해서 반복하는 방법에 대해서 알아본다.

 

 

 

예제 코드 1

while (1)
{
    static int count = 0; // static 사용하면 계속 올라감.
    cout << count << endl;
    ++count;

    if (count == 10) break;
}

 

while은 괄호 안에 조건을 만족시키는 경우만 반복하도록 하는 문법이다. 위 코드처럼 while (1)로 정의하면 무한 루프가 된다.

 

앞에서 배운 내용이지만 static을 선언하면 변수를 정적 메모리 영역에 가지고 있게 되므로 지역 변수이지만 계속 값이 더해지는 것을 확인할 수 있다.

 

 

 

예제 코드 2

unsigned int count = 10;

while (count >= 0)
{
    if (count == 0)
    {
        cout << "Zero";
    }
    else
    {
        cout << count << " ";
    }

    count--;

}

 

 

count 변수가 unsigned int이기 때문에, 계속해서 값을 빼게 되면 0 이후로는 이상한 값이 되어버린다. 즉 overflow 현상이 발생하게 된다. 따라서 변수를 반복적으로 감소하거나 증가시키는 경우에는 이 부분을 주의해야 한다.

 

 

 

예제 코드 3

// 연습문제 1
// 1
// 1 2
// 1 2 3
// 1 2 3 4
// 1 2 3 4 5 만들기

int i = 1;

while (i <= 5)
{
    int j = 1;

    while (j <= i)
    {
        cout << j << " ";
        j++;
    }

    i++;
    cout << endl;

}

 

while을 두 개 반복해서 사용해 출력해 보는 연습문제이다.

 

위에서 주석으로 표시했듯이,

 

1

1 2

1 2 3

1 2 3 4

1 2 3 4 5 를 출력해 보는 문제다.

 

전체적으로 보면 5번을 반복해야 하니 바깥쪽에 while문이 하나 있어야 하고, row를 기준으로 보면 그 안에서도 반복문을 돌면서 출력해야 하므로 이중 while문이 요구되는 것을 알 수 있다.

 

첫 번째 줄에서는 row 기준으로는 한 번만 출력하고, 두 번째 줄에서는 두 번만 출력하니 i 번째 줄에서 i 번만 출력하도록 만들어야 한다는 것이 중요 포인트라고 볼 수 있다.

 

 

 

예제 코드 4

// 연습문제 2
// 5
// 5 4
// 5 4 3
// 5 4 3 2
// 5 4 3 2 1 만들기

int a = 5;

while (a > 0)
{
    int b = 5;

    while (b >= a)
    {
        cout << b << " ";
        b--;
    }

    a--;
    cout << endl;
}

 

 

예제 코드 3번과 반대의 경우이다.

 

이번에는 반대로 값이 줄어드는 형태이므로, --를 사용해야 한다는 점을 알고 있으면 된다.

 

 

 

예제 코드 5번

// 연습문제 3
// 1 x x x x
// 1 2 x x x
// 1 2 3 x x
// 1 2 3 4 x
// 1 2 3 4 5 만들기

int a_ = 1;

while (a_ <= 5)
{
    int b_ = 1;

    while (b_ <= 5)
    {
        if (b_ <= a_)
        {
            cout << b_ << " ";
        }
        else
        {
            cout << "x" << " ";
        }

        b_++;
    }

    a_++;
    cout << endl;
}

 

이번에는 예제 코드 3번 하고 비슷하지만 빈 공간만큼을 x로 채우는 코드이다.

 

전체적으로 예제 코드 3번과 로직은 비슷하지만, 안쪽 while 문에서 if로 분기해서 i 번째 줄에서 i 번째 까지는 숫자를 출력하고, 그 외에는 x를 출력하도록 만들기만 하면 된다.

 

 

Chapter 5.6 반복문 do-while

 

 

반복문이지만 반드시 한 번은 실행해야 하는 경우에 사용하는 do while문에 대한 내용이다.

 

 

예제 코드 1

#include <iostream>

using namespace std;

int main()
{

	int selection;
	do
	{
		cout << "1. add" << endl;
		cout << "2. sub" << endl;
		cout << "3. mult" << endl;
		cout << "4. div" << endl;
		cin >> selection;
	} while (selection <= 0 || selection >= 5);

	cout << "You selected " << selection << endl;


}

 

do while문은 while 하고 사용 방식이 조금 달라서, 잘 확인해야 할 것 같다.

 

특이한 게 while이 뒤에 오고 기존 while 하고 동일하게 조건이 오며, 마지막에 ; 를 꼭 찍어줘야 한다.

 

 

Chapter 5.7 반복문 for

 

 

이번 강의에서는 while과 더불어서 가장 흔하게 쓰이는 while문에 대해서 알아본다.

 

 

 

예제 코드 1

for (int cnt = 0; cnt < 10; cnt++)
{
    cout << cnt << endl;
}

for (int count = 9; count >= 0; count -= 2)
{
    cout << count << endl;
}

for (int i = 0, j = 0; (i+j) < 10; ++i, j+=2)
{
    cout << i << " " << j << endl;
}

 

 

우선 기본적인 for 문의 사용은 총 세 개 부분으로 나뉘는데, 카운터로 선언할 변수를 선언하는 부분, 반복 조건, 카운터의 변화를 정의하는 부분으로 나눠져 있다. 

 

맨 처음 코드인 for (int cnt = 0; cnt < 10; cnt ++)이 가장 basic 한 쓰임이라고 볼 수 있다.

 

두 번째 코드는 카운터의 변화를 단순히 ++나 --만 사용하는 것이 아닌 다양하게 사용할 수 있음을 보여준다.

 

세 번째 코드는 여러 개의 변수를 카운터로 사용할 수 있음을 보여준다.

 

 

예제 코드 2

int cnt = 0;
for (; cnt < 10; cnt++)
//for(;;++cnt) // 무한루프
{
    cout << cnt << endl;
}

for (int j = 2; j < 10; ++j)
{
    for (int i = 1; i < 10; ++i)
    {
        cout << j << "*" << i << "=" << j * i << endl;
    }
}

 

다음은 카운터에 해당하는 변수를 for문의 조건 쪽에서 선언하는 것이 아니라 밖에서 따로 선언하는 경우를 보여준다.

 

 

그리고 while에서 while (true)를 해주듯이 for문으로도 무한루프를 만들 수 있는데, for (;;++cnt) 이런 식으로 만들어주면 반복 조건이 true가 되면서 제약이 사라지게 되고 무한 루프에 빠지게 된다.

 

아래쪽 코드는 이중 for문을 활용해서 구구단을 만드는 예시이다. for문을 활용해서 해보는 예제 문제 중에 가장 대표적인 예제문제라고 볼 수 있다.

 

 

Chapter 5.8 break, continue

 

 

이번 챕터에서는 반복문에서 탈출할 수 있도록 만들어주는 break와, 특정 조건에서 반복문의 처음 지점으로 다시 돌아가게 해주는 기능을 가지고 있는 continue에 대해서 알아본다.

 

 

 

예제 코드 1

// break 예제 //

int count = 0;
while (true)
{
    cout << count << endl;

    if (count == 10)
    {
        break;
    }

    count++;
}


// continue 예제 //
for (int i = 0; i < 10; i++)
{
    if (i % 2 == 0)
    {
        continue;
    }

    cout << i << endl;
}

 

break는 특정 조건을 만족할 때 반복문을 탈출할 수 있는 기능을 가지고 있다.

 

break 예제 코드에서, 현재 while (true)로 인해 무한 loop를 돌도록 만들어져 있는데, count가 10일 때 break를 선언하여 반복문에서 빠져나오게 된다.

 

continue는 특정 조건을 만족할 때 반복문 내 나머지 실행문을 생략하고, 다시 반복문의 초입으로 돌아가는 기능을 가지고 있다.

 

continue 예제 코드에서, i를 2로 나눴을 때 0인 경우는 짝수인 경우인데, 짝수인 경우는 continue를 걸어두어서 i가 홀수인 경우만 cout을 하도록 되어 있다.

 

 

예제 코드 2

// do while 문과 continue의 결합
int count = 0;
do
{
    if (count == 5)
    {
        continue;
    }

    cout << count << endl;
} while (++count < 10);

// while 문에서 break를 사용하지 않고 break와 같은 효과를 내는 경우
bool escape_flag = false;
while (!escape_flag)
{
    char ch;
    cin >> ch;

    cout << ch << " " << count++ << endl;

    if (ch == 'x')
    {
        escape_flag = true;
    }

}

 

do while은 앞에서 다룬 구문인데, while 문을 실행하지만 반드시 한 번은 실행해야 하는 경우에 사용한다.

 

while 문과는 다르게, while의 조건이 do의 끝쪽에 붙는 특징을 가지고 있다.

 

do 문에서 count가 5인 경우 continue 하도록 되어 있고, 그 외에는 count 변수를 cout 하도록 되어 있다.

 

while 에는 단순 조건뿐만 아니라 ++count를 넣어두어 count가 계속 증가하도록 만들어두었다.

 

따라서 해당 코드에서는 0부터 9까지는 cout을 하되, count가 5인 경우는 continue를 해서 cout을 하지 않는다.

 

 

아래쪽 코드는 break문을 사용하지 않고 break와 같은 효과를 내도록 만든 코드이다.

 

별도의 bool 자료형의 flag를 만든 다음, 만약 char 타입의 변수 ch가 x인 경우 escape_flag를 true로 만들어서 while 문이 더 이상 작동하지 않도록 만들어진 코드이다.

 

break문을 사용하지 않으려고 하다 보니, 별도의 bool 자료형의 변수가 하나 더 필요하고 이를 관리해야 된다는 점에서 비효율이 발생한다고 볼 수 있다.

 

flag를 사용하지 않고, while은 그냥 while (true)로 사용하면서 ch가 x일 때 break;를 하도록 코드를 작성하면 훨씬 편하게 동일한 기능을 구현할 수 있다.

 

 

Chapter 5.9 난수 만들기

 

 

이번 챕터에서는 C++에서 난수를 만드는 방법에 대해서 다룬다.

 

 

예제 코드 1

// 난수를 만드는 원리를 설명
unsigned int PRNG()
{
	static unsigned int seed = 5523;

	seed = 8253729 * seed + 2396403;

	return seed % 32768;
}

for (int cnt = 1; cnt <= 100; ++cnt)
{
	cout << PRNG() << "\t";
}

 

컴퓨터는 실제로 난수를 만들 수 있는 능력이 없다고 한다. 따라서 엄밀하게 따지면 난수를 만드는 것이 아니라, 난수처럼 보이는 숫자들을 연속적으로 계산해 내는 것이라고 한다.

 

PRNG 함수를 보면, 처음 정해진 seed 값에 임의의 값들을 더해주고 곱해준 다음, 이를 임의의 숫자로 나눈 값을 return 해주고 있다. seed가 static 변수이므로, seed 변수는 PRNG 함수가 호출될 때마다 계속 값이 변한다고 볼 수 있다.

 

신기하게도 100번을 돌리면 들쭉날쭉하게 숫자들이 생성되는데, 정말 난수처럼 보인다.

 

 

예제 코드 2

#include <cstdlib> // std::rand(), std::srand()
int getRandomNumber(int min, int max)
{
	static const double fraction = 1.0 / (RAND_MAX + 1.0);

	return min + static_cast<int>((max - min + 1) * (std::rand() * fraction));
}

for (int cnt = 1; cnt <= 100; ++cnt)
{

    cout << getRandomNumber(5, 8); << "\t";

    if (cnt % 5 == 0) cout << endl;
}

 

특정한 범위 안에서 난수를 만들 수 있을까? 예제 코드 2번은 이에 대해서 설명해 준다.

 

이 함수의 return은 크게 두 부분으로 나눠져 있다.

 

우선 min이 기본 값이 된다. 왜냐하면 최소한 min 값은 보장해야 하기 때문이다. 뒤 항이 0이 되는 경우를 생각하면 된다.

 

두 번째 항을 보면, 앞에 static_cast <int>가 보인다. 즉 뒤에 나오는 값을 int로 casting 할 예정이다.

 

내부를 보면 두 가지 term으로 나눠지는데, (max - min + 1) 과 (std::rand() * fraction)의 곱이다. 

 

(max - min + 1)을 getRandomNumber(5, 8) 케이스에서 생각해 보면 (8 - 5 + 1)가 되며 값은 4가 된다.

 

std::rand()는 0부터 RAND_MAX까지의 값 중 하나가 나오게 된다.

 

그래서 std::rand() * fraction의 값은 0보다 크거나 같고, std::rand()가 RAND_MAX가 되는 경우, RAND_MAX * (1 / RAND_MAX + 1)이 되는 것이니 1보다 아주 약간 작은 값이 된다. 

 

즉 뒤쪽 항인 std::rand() * fraction의 범위를 수식으로 표현하자면 0 <= std::rand() * fraction < 1가 된다.

 

여기에 (max - min + 1)을 곱해주면, 0 <= (max - min + 1) * (std::rand() * fraction) < 4가 된다.

 

이를 static_cast<int> 해주면, 내림으로 처리해야 하니 int 값 기준 0, 1, 2, 3만 가질 수 있다. 여기에 min만 더해주면 return 될 수 있는 정수는 5, 6, 7, 8이 된다.

 

 

 

예제 코드 3

#include <cstdlib> // std::rand(), std::srand()
#include <ctime> // std::time()
#include <random>

int main()
{
	std::srand(5323); // seed를 설정해주는 역할
    //std::srand(static_cast<unsigned int>(std::time(0))); // seed가 계속 변경되는 케이스
    
    for (int cnt = 1; cnt <= 100; ++cnt)
    {
    	cout << std::rand() << "\t"; // 난수 생성 케이스
        
        cout << rand() % 4 + 5 << "\t"; // 5부터 8 사이의 난수 생성 케이스
    }

}

 

std::srand는 난수 생성 시 seed를 고정해 주는 역할을 한다. 따라서 srand가 같은 값이면 계속 같은 값들이 난수로 생성되게 된다.

 

디버깅하는 경우는 시드가 계속 바뀌면 오히려 디버깅이 안되니, 고정해야 한다.

 

만약 시드를 고정하지 않고 계속 변경해서 난수가 생성되도록 하고 싶다면, seed를 현재 시간을 기준으로 정하게 하면 코드를 작동할 때마다 계속 다른 seed가 적용되도록 만들 수 있다.

 

std::rand()를 사용하면 난수를 생성할 수 있으며, 만약 특정 난수 생성 범위를 정해주고 싶다면 rand() % 4 + 5와 같은 방식을 통해 생성 범위를 만들 수 있다. 단, 나눠주는 숫자(현 케이스에서는 4)가 작으면 괜찮으나 큰 경우 난수가 생성될 때 특정 범위에 숫자가 몰리는 케이스가 생길 수 있다고 한다.

 

 

 

예제 코드 4

std::random_device rd;
std::mt19937 mersenne(rd()); // create a mersenne twister,
std::uniform_int_distribution<> dice(1, 6); // 1포함 6 이하

for (int count = 1; count <= 20; ++count)
{
    cout << dice(mersenne) << endl;
}

 

해당 코드는 1) 랜덤 디바이스를 생성하고, 2) 랜덤 디바이스를 이용해서 생성기를 만들고, 3) 생성기가 어떤 분포를 따르는지 결정해서 원하는 분포를 따르는 숫자를 만드는 예시이다.

 

std::mt19937 mersenne는 매우 질이 좋은 난수를 빠르게 생성할 수 있도록 만들어진 유사난수 생성기라고 한다.

 

 

 

Chapter 5.10 std::cin 더 잘 쓰기

 

이번 챕터에서는 std::cin을 사용할 때, 다양한 안전장치를 통해서 사용자가 다양하게 입력을 줄 때 거기에 대응할 수 있는 코드를 사용하는 방법을 다룬다.

 

 

 

예제 코드 1

#include <iostream>

using namespace std;

int getInt()
{
	while (1)
	{
		cout << "Enter a integer number : ";
		int x;
		cin >> x;

		// 입력 스트림에 오류가 발생했을 때 true
		if (std::cin.fail())
		{
			std::cin.clear(); // std::cin.fail()이 true가 되는 경우, 오류 플래그가 설정되어 있어 cin 사용 불가. clear()를 하면 오류 플래그 초기화.
			std::cin.ignore(32767, '\n'); // cin은 공백을 기준으로 입력을 구분하고, 나머지 입력 버퍼에 남은 내용을 건너뛰게 해줌.
			cout << "Invalid number, please try again" << endl;
		}
		else
		{
			std::cin.ignore(32767, '\n');
			return x;
		}
	}
	
}

char getOperator()
{
	while (1)
	{
		cout << "Enter an operator (+, -, *) :";
		char op;
		cin >> op;
		std::cin.ignore(32767, '\n');

		if (op == '+' || op == '-' || op == '*') return op;
		else cout << "Invalid operator, please try again" << endl;
	}
}

void printResult(int x, char op, int y)
{
	switch (op)
	{
		case '+':
			cout << x + y << endl;
			break;
		case '-':
			cout << x - y << endl;
			break;
		case '*':
			cout << x * y << endl;
			break;
		default:
			cout << "Invalid operator" << endl;
	}
}

int main()
{
	int x = getInt();
	char op = getOperator();
	int y = getInt();

	printResult(x, op, y);

}

 

입력을 받는 함수의 경우, while문을 활용해서 유효한 값이 입력될 때까지 계속 반복이 되도록 해주는 것이 중요하다.

 

그리고 std::cin.fail()을 이용해서 입력 스트림에 오류가 발생했는지 여부를 파악하고, 만약 오류가 발생했다면 std::cin.clear()를 통해 오류 플래그를 초기화시켜 다시 std::cin 기능이 정상적으로 작동할 수 있도록 설정한다.

 

해당 코드에서는 정수를 1개 받아야 하는데, 사용자가 "1 2" 와 같은 방식으로 입력하면, std::cin의 경우 공백을 기준으로 입력을 구분하기 때문에 1은 받되 공백 뒤에 있는 2는 입력 버퍼에 남겨두게 된다. 이러면 그다음 입력에 영향을 끼친다. 따라서 이러한 문제를 방지하고자 std::cin.ignore를 사용하여, 문장의 끝을 나타내는 '\n'이 나오는 경우 나머지 입력 버퍼에 남은 내용을 건너뛰게 만들어 문제 발생을 방지할 수 있다.

 

 

 

 

 

여기까지 해서 Chapter 5를 정리하였다.

 

 

Chapter 4를 정리한 게 벌써 2달 전인데, 여러 가지 일로 계속 미뤄지다 보니 글 작성이 많이 지연되었다.

 

 

워낙 강의가 꼼꼼하고 양이 많다 보니 완강하려면 열심히 봐야겠다..!

 

 

 

 

다음으로 Chapter 4를 정리해 봅니다.

 

 

 

 

 

Chapter 4.1 지역 변수, 범위, 지속시간

 

 

이번 챕터는 변수의 범위에 대한 내용들입니다.

 

namespace work1
{
	int a = 1;
	void doSomething()
	{
		a += 3;
	}
}

namespace work2
{
	int a = 1;
	// 이름은 같은데 하는 일이 다른 경우(파라미터까지 다 같음)
	// 이름이 같은데 파라미터가 다른 경우는 다른 함수로 인식해서 다른 함수로 인식.
	void doSomething(int b)
	{
		a += 5;
	}
}



int main()
{
	using namespace std;

	int apple = 5;

	cout << apple << endl;
	// apple을 선언한 이후로는 apple을 볼 수 있다. scope를 알 수 있음.
	// main문 밖에서는 사용할 수 없다.
	// 중괄호가 끝나면 메모리가 이미 반납되어서 apple이라는 변수는 없다.
	// apple = 1;

	// nested block
	// 밖에서 정의된 변수는 block 안쪽에서 볼 수도 있고 사용할 수도 있다.

	{
		// 만약 int apple = 1;로 정의하면 밖에 있는 apple하고 다름.
		// 이름이 같더라도 더 적은 영역에 있으면 기존 apple이 사라짐 (name hiding), 이름은 같지만 완전히 다름.
		// 따라서 가급적 변수 이름은 범위가 다르더라도 다르게 짓는 것이 좋음.
		apple = 1;
		cout << apple << endl;
	}

	// 현대적 프로그래밍에서는 변수가 살아남는 범위를 줄이려고 함.
	// 따라서 변수가 사용되는 곳 근처에서만 살아남도록 최소한의 범위를 갖도록 블럭으로 만들어줌.
	// 범위를 쪼개고 쪼개는게 객체지향 프로그래밍의 기본적인 철학.

	cout << apple << endl;


	// :: 연산자는 영역/범위 결정 연산자. Scope resolution operator
	// 이름이 충돌할 때 이를 해결하기 위해서 이름 공간을 쪼갠다.
	// C++17 에서는 nested namespace 지원함.
	work1::a;
	work1::doSomething();

	work2::a;
	work2::doSomething(3);
    
}

 

주석으로 내용을 정리해 두었는데, 글로 한 번만 더 정리해 보자면,

 

- 중괄호가 끝나면 메모리가 반납되며 변수는 제거됨. (변수의 범위와 지속시간)

- block 밖에서 정의된 변수는 block 안쪽에서 볼 수도 있고 사용도 가능.

- 만약 block 안에서 같은 자료형과 같은 변수명으로 다시 한번 선언하는 경우, block 밖에 있던 변수와는 별개.

- 범위가 다르더라도 혼동을 줄이기 위해 변수명은 다르게 지어야 한다.

- 변수 이름으로 충돌하는 경우를 해결하기 위해 namespace 기능을 사용함. 

 

 

 

 

Chapter 4.2 전역 변수, 정적 변수, 내부 연결, 외부 연결

 

 

- 지역 변수(Local variable): Linkage가 없으며, Linking 시 고려하지 않음.

- 내부 연결(Internal linkage): 한 파일 안에서는 어디서든 사용할 수 있음.

- 외부 연결(External linkage): cpp 파일이 여러 개 있을 때, 한 cpp 파일에서 선언한 변수를 다른 cpp에서도 사용할 수 있음.

 

 

예시 코드 1

int value = 123;

int main()
{

	std::cout << value << std::endl;
	
	int value = 1;
	
	std::cout << value << std::endl;

	// Global scope operator
	std::cout << ::value << std::endl; // 영역 연산자, 다른 영역에 정의된 변수를 사용하는 것. 
}

 

 

 

 

main문 안에서 value가 선언되기 전에 cout으로 호출하게 되면 밖에서 이미 선언된 변수의 값을 가져온다.

 

main문 안에서 value가 선언된 후에는 cout으로 호출했을 때 내부에 선언된 변수 값을 가져온다.

 

만약 밖에서 선언된 변수의 값을 출력하고 싶다면, ::를 활용한다. 

 

 

 

예시 코드 2

void doSomething()
{
	// static? c를 만든 사람 입장에서 봐야한다... 
	// 변수 a가 os로부터 받은 메모리가 static이다. 
	// 이 영역 안에 변수가 선언될 때 같은 메모리를 사용한다. 두번, 세번 실행되더라도 같은 메모리 사용. 초기화를 한번만 함.
	// static 변수는 반드시 한 번은 초기화를 해줘야함. (메모리에 얹어줘야하기 때문에)
	// 함수를 몇 번 호출되는지 확인해볼 때 사용. 디버깅에서 쓴다.
	static int static_a = 1;
	int a = 1; // 계속 메모리를 새로 할당받음. 

	a++;
	static_a++;

	std::cout << "a: " << a << std::endl;

	std::cout << "static_a: " << static_a << std::endl;
}

int main()
{
	doSomething();
	doSomething();
	doSomething();
	doSomething();
}

 

 

 

다음은 static 변수에 대한 설명이다.

 

static이라는 것은 os로부터 받은 메모리가 static이라는 얘기이다. 즉, 이 영역 안에서 변수가 선언될 때 같은 메모리를 사용한다. 2번, 3번 실행되더라도 같은 메모리를 사용하게 되며, 초기화는 한 번만 한다.

 

따라서 static 변수는 반드시 한 번은 초기화를 해줘야한다는 특징이 있다.

 

반면 static int가 아닌, 일반 int를 사용하면 계속 초기화를 해주기 때문에 값이 같은 것을 확인할 수 있다.

 

 

 

 

 

 

여러 cpp 파일에서 사용할 수 있는 방법은 바로 extern을 붙이는 것이다.

 

test_cpp.cpp라는 파일에서 선언된 int e_a는 Chapter 4_2.cpp라는 파일에서도 사용할 수 있는데, 이는 extern int e_a로 선언했기 때문이다.

 

함수 또한 다른 cpp 파일에서 정의된 것을 사용할 수 있는데, 이는 전방선언을 활용하면 된다. 단 함수의 경우 extern이 필수는 아니다.

 

 

 

만약 내가 여러 cpp 파일에서 사용할 상수가 있다고 가정하자.

 

그럼 어떻게 해야 효율적으로 상수를 정의해서 사용할 수 있을까?

 

 

// my_constant.h
namespace Constants
{
	extern const double pi = 3.14;
	extern const double gravity = 9.8;
}

// test_1.cpp
#include "my_constant.h"

int main() {
    std::cout << &Constants::pi << std::endl;
    return 0;
}

// test_2.cpp
#include "my_constant.h"

int main() {
    std::cout << &Constants::pi << std::endl;
    return 0;
}

 

위 코드처럼, 만약 헤더파일에 extern const 변수를 선언하게 되면, 각각의 cpp에서 해당 변수를 사용할 때 메모리가 각각 사용된다.

 

안타깝게도 강의에서는 자세히 언급을 해주시지 않아서, 제미나이를 활용해 보았더니 다음과 같은 대답을 얻을 수 있었다.

 

 

 

 

헤더 파일에 extern const로 변수를 선언하는 경우, 컴파일러에 의해서 번역될 때 각 cpp 파일에 별도의 변수 인스턴스를 만들도록 지시한다고 한다.

 

따라서 이를 방지하려면, 헤더 파일에서는 변수 선언만 하고, 값을 선언하는건 별도의 cpp 파일을 만들어서 해줘야 한다.

 

 

// Constants.h
namespace Constants
{
    extern const double pi;
    extern const double gravity;
}

// Constants.cpp
#include "Constants.h"
namespace Constants
{
    const double pi = 3.14;
    const double gravity = 9.8;
}

// test_1.cpp
#include "Constants.h"
#include <iostream>

int main() {
    std::cout << &Constants::pi << std::endl;
    return 0;
}

// test_2.cpp
#include "Constants.h"
#include <iostream>

int main() {
    std::cout << &Constants::pi << std::endl;
    return 0;
}

 

다음과 같이, 별도의 cpp 파일에서 값을 제공해주는 방식으로 하면 다른 cpp 파일에서 해당 변수 사용 시 주소가 같다.

 

 

 

 

 

 

Chatper 4.3 Using문과 모호성

 

 

 

예시 코드 1

namespace a
{
	int my_var(10);
}

namespace b
{
	int my_var(20);
}

int main()
{

	using namespace a;
	using namespace b;

	std::cout << my_var << std::endl; // 모호하다는 에러 발생
    
    std::cout << a::my_var << std::endl;
    std::cout << b::my_var << std::endl;
}

 

 

namespace a와 namespace b 모두에 my_var라는 int 변수가 있는 경우, using namespace a, b 모두를 사용하면 my_var이 a에 있는 걸 써야 할지, b에 있는 걸 써야 할지 몰라서 모호하다는 에러가 발생한다.

 

가장 명확하게 해결하는 방법은, :: 연산자를 활용해 어떤 namespace에 있는 변수인지를 명시적으로 알려주는 것이다.

 

 

 

예시 코드 2

namespace a
{
	int my_var(10);
}

namespace b
{
	int my_var(20);
}


int main()
{

	using namespace b;
    
	{
		using namespace a;
		std::cout << my_var << std::endl; // a와 b 영향을 다 받는다, 모호함 해결 X
	}

	std::cout << my_var << std::endl; // namespace b로 사용됨.
}

 

블록으로 따로 영역을 잡아주고, using namespace a를 적용해주는 경우, 블록 안에 있는 my_var는 여전히 using이 두 개 적용 되는 상황이므로 모호성이 해결되지 않는다.

 

단, 블록 바깥쪽에 있는 my_var는 b의 my_var로 적용된다.

 

 

예시 코드 3

namespace a
{
	int my_var(10);
}

namespace b
{
	int my_var(20);
}


int main()
{

	
    
	{
		using namespace a;
		std::cout << my_var << std::endl; 
	}

	{
		using namespace b;
		std::cout << my_var << std::endl; 
	}
}

 

블록으로 영역을 따로 따로 잡아주면, 모호성이 없어진다.

 

 

using namespace를 특정 헤더에서 전역 범위에 넣어버리면, 헤더를 include 하는 모든 cpp 파일에 영향을 주게 된다.

 

따라서 가급적이면 cpp 파일에 넣는 방향이 좋다. 

 

 

using namespace std처럼 큰 범위에 적용하는 using을 사용하는 경우, 다른 namespace에서 std에 들어간 함수명과 겹치는 이름의 변수를 사용하려고 할 때 선택을 해야 한다. using namespace std를 포기할지, 혹은 using namespace a를 포기할지. 강의에서는 count라는 변수를 통해서 예시를 보여주셨는데, std::count라는 함수가 있어서 namespace에 count라는 이름으로 변수를 사용하게 되면 겹치게 되어 문제가 발생하는 것을 확인할 수 있었다.

 

 

 

 

Chapter 4.4 auto 키워드와 자료형 추론

 

 

 

예시 코드 1

// Parameter에는 auto 사용 불가.
auto add(int x, int y) -> int
{
	return x + y;
}



int main()
{
	using namespace std;

	// 자료형을 상황에 따라서 결정하게 만드는 것을 형 추론이라고 한다. auto 키워드 사용.
	// 값을 주지 않으면 사용할 수 없음.
	// 계산 결과가 어떻게 나오는지 인지하고 있어야 함.

	auto a(123); // int
	auto d(123.0); // double
	auto c(1 + 2.0); // double

	// 함수의 return 값에도 auto 사용 가능.
	auto result = add(1, 2);
}

 

 

자료형을 상황에 따라 결정하게 만드는 것을 형 추론이라고 하는데, 이는 auto 키워드를 사용한다.

 

예를 들어서, auto a(123); 라고 하면 값이 int 이므로 a는 int로 결정이 된다.

 

auto d(123.0); 라고 하면 값이 소수점이므로 d는 double로 결정이 된다.

 

함수의 return type에도 auto를 사용할 수 있고, 함수의 return을 받는 변수도 auto를 사용할 수 있다.

 

함수의 return type을 auto로 하더라도, 명시적으로 자료형을 표기하고 싶다면 ->를 사용해서 표현할 수 있다.

 

auto func() -> int 로 만드나, int func()로 만드나 같지만, 여러 함수가 있을 때 각 함수의 return type을 한눈에 보기에는 전자를 사용하는 것이 더 좋다.

 

 

 

Chapter 4.5 형변환 (Type conversion)

 

 

 

예시 코드 1

#include <iostream>
#include <typeinfo>


cout << typeid(4).name() << endl;
cout << typeid(a).name() << endl;

 

typeinfo를 include하면, typeid라는 함수를 사용할 수 있는데, 이는 자료형을 알려주는 함수이다.

 

auto를 사용하는 경우라던가, 변수의 자료형을 알 수 없는 경우 유용하게 사용할 수 있다.

 

 

 

예시 코드 2

// 메모리를 작게 사용하는 데이터 타입을 메모리를 크게 사용하는 데이터 타입으로, 
// numeric promotion, 암시적 형변환 사용
float a = 1.0f;
double d = a;

 

다음으로는 numeric promotion에 대한 설명이다.

2

메모리를 작게 사용하는 데이터 타입(float)를 메모리를 크게 사용하는 데이터 타입(double)으로 변경하는 경우를 말한다. 이런 경우는 명시적인 형변환이 아닌, 암시적인 형변환을 사용한다는 특징이 있다.

 

적은 메모리에서 큰 메모리로 형변환이 되기 때문에, 값의 변경 없이 type만 변경된다.

 

 

 

예시 코드 3

// numeric conversion
double dd = 3; // int를 double로, 타입이 바뀌는 경우
short s = 2; // int를 short로, 큰 자료형을 작은 자료형으로 보내는 경우

// numeric conversion issue
int i = 30000;
char c = i;

// issue 2
double d_ = 0.123456789;
float f_ = d_;

// unsigned라서 제대로 저장될 수 없는 경우
// 자료형의 우선순위가 int가 가장 낮고, unsigned int,
// long, unsigned long, long long, unsigned long long, float, double, long double 순이다.
// unsigned가 우선순위가 더 높으니 int로 바꾸지 않는 경우임. (주의!)
cout << 5u - 10u;

 

다음은 numeric conversion에 대한 설명이다.

 

int를 double로 바꾸는 것 처럼, data type 자체가 변하는 경우가 있으며

 

int를 short로, 즉 큰 자료형을 작은 자료형으로 보내는 경우가 있다.

 

 

 

이런 경우에 문제가 발생할 수 있는데, 예를 들어 int i = 30000;로 정의했는데 char 타입으로 받게 되면 char 타입의 자료형 크기를 초과하므로 c의 값은 정상적으로 i 값을 받아올 수 없다.

 

마찬가지로 double 자료형의 변수를 float로 받는 경우, 정밀도의 차이가 있어 double 변수의 값을 온전히 float 변수가 받을 수 없다.

 

그리고 자료형의 우선순위 문제로, 5u(숫자 5이나, unsigned int를 의미함) - 10u는 실제로 -5 값으로 나와야 하나, 우선순위 상 unsigned int가 더 높기 때문에 이를 signed int 형태인 -5로 표현하지 않고 이상한 값으로 표현하게 된다. 이런 경우 우리가 기대하는 값과 전혀 다른 값이 들어갈 수 있기 때문에 주의해야 한다. 

 

 

 

 

Chatper 4.6 문자열 std string 소개

 

 

해당 강의에서는 문자열을 표현하는 자료형인 std::string에 대해서 소개한다.

 

 

예시 코드 1

#include <string>

int main()
{

	// char하고 string하고 색이 다르다.
	// C++에서 기본적으로 제공해주는건 한 글자다.
	// 한 글자를 여러개 나열하는 방식으로 문자열을 사용한다.
	// char가 기본적으로 사용하는 방식
	const char my_strs [] = "Hello, World";

	// 프로그래머들이 편하라고 제공하는 방식.
	// 프로그래머들이 문자를 다룰 때 많이 사용하는 것들을 string 안에 미리 구현해뒀음.
	// 일종의 사용자 정의 데이터 타입이라고 보면 됨. 기본 자료형처럼 쓸 수 있는데 추가적인 기능이 들어가있는 것.
	const string my_hello = "Hello, World";

	string my_ID = "123";

	cout << my_strs << endl;

 

 

C++에서 제공하는 기본적인 자료형은 char이나, 문자를 조금 더 편하게 다룰 수 있도록 만들어진 것이 std::string 자료형이라고 이해하면 된다고 한다. 

 

그렇다 보니, const char는 글자색이 같은데, string은 색이 다르다. string은 기본 자료형이 아니며, 일종의 사용자 정의 데이터 타입이라고 볼 수 있다. 

 

 

예시 코드 2

cout << "Your name ? : ";
string name;

//cin >> name;
std::getline(std::cin, name);

cout << "Your age ? : ";
string age;

std::getline(std::cin, age);

cout << "My name is: " << name << " My age is : " << age << endl;

 

일반적으로 데이터를 입력받는다고 하면 cin을 사용하면 된다. 

 

그러나, cin을 사용할 때 한 칸을 띄어버리게 되면 두 개의 cin으로 나눠서 들어가게 된다.

 

예를 들어서, 

 

 

 

cin을 사용할 때는 한 칸을 띄우게 되면 happy는 앞 cin에, cat은 그다음 cin으로 들어가게 되어서 두 번째 cin에는 입력을 할 수 없으며 자동적으로 할당을 하게 된다.

 

따라서 이를 방지하려면 std::getline을 사용하여 cin을 받아야한다. 

 

 

 

예시 코드 3

cout << "Your age ? : ";
int age;

cin >> age;

// \n이 만날 때 까지 글자를 무시하라
//std::cin.ignore(std::numeric_limits<std::streamsize>::max() , '\n');

cout << "Your name ? : ";
string name;

std::getline(std::cin, name);

cout << "My name is: " << name << " My age is : " << age << endl;

 

이번에는 이전과 달리, int를 입력받고 string을 입력 받으려고 하는 경우이다.

 

int를 입력 받으려면 cin을 사용해야 하므로, cin으로 age를 받고 다음은 문자열을 받기 위해 getline을 사용하려고 하였다.

 

하지만 cin을 사용해서 입력한 후 엔터 키를 누르면 입력 버퍼에 \n가 남아있어서 이를 getline에서 읽고 빈 문자열을 name에 할당한 후 종료하게 되는 문제가 있다.

 

따라서 이를 해결하려면 cin.ignore를 적용하여 글자를 무시하도록 적용해줘야 한다.

 

 

예시 코드 4

string a = "Hello, "; // 마우스를 대보면 [8]로 나오는데, 이는 문자열이 8개라는 의미. 하지만 실제로는 7개, 맨 마지막에 널 캐릭터가 숨어 있음.
string b = "World";
string hw = a + b;

hw += " Good";

//cout << hw << endl;
cout << hw.length() << endl; // string 길이 확인.

 

다음은 string을 + 연산자를 활용해서 붙이는 경우를 보여준다.

string a 하고 string b를 + 연산자로 붙이면, 그대로 문자열이 붙게 된다.

 

그리고 문자열의 길이를 확인할 때는 .length()를 활용하면 된다.

 

조금 특이한 점은, string a의 경우 "Hello, "이니까 실제로는 7글자인데, 마우스 커서를 대보면 const char [8]로 나온다. 이는 맨 마지막에 널 캐릭터가 숨어져 있기 때문이며, C 스타일 문자열은 항상 널 캐릭터로 끝나야 하는 규칙이 있다. 이를 통해 문자열의 끝을 확인할 수 있는 것이다.

 

 

 

Chatper 4.7 열거형 (enumerated types)

 

 

이번 강의에서는 enum 이라고 하는 자료형인 열거형에 대해서 다룬다.

 

// 사용자 정의 자료형 (user-defined data types)
enum Color
{
	COLOR_BLACK, // -3으로 지정나면 나머지들은 +1씩 값으로 지정됨.
	COLOR_RED,
	COLOR_BLUE,
	COLOR_GREEN,
};  // ; 없으면 빌드 에러남.

enum Feeling
{
	HAPPY,
	JOY,
	TIRED,
	BLUE
};

Color my_color = COLOR_RED;
int color_id = COLOR_RED;

 

일단 enum의 특징은, 처음 값을 0으로 시작해서 1씩 증가하도록 설계되어 있다. COLOR_BLACK이 0이고, COLOR_RED가 1이고 이런 식이다.

 

기본적으로는 그렇지만, 만약 수동으로 값을 지정해 주면, 값이 달라지나 규칙은 유지된다.

 

예를 들어 맨 처음 값을 -3으로 주면 그다음 값은 -2, -1, 0이 되는 것이다.

 

enum 사용 시 맨 마지막에 괄호 닫고 ;를 꼭 써줘야만 빌드에 문제가 없다.

 

그리고 다른 enum이더라도 만약 안에 사용하는 요소의 이름이 같으면 문제가 생긴다. 따라서 이름은 항상 다르게 지어주어야 한다.

 

 

COLOR_RED는 Color라는 이름의 자료형인 my_color에 사용할 수도 있고, int형 변수에도 사용할 수 있다. cout으로 출력하면 정상적으로 1로 출력이 된다.

 

그리고 여러 cpp 파일에서 enum을 공동으로 사용하는 경우가 많기 때문에, 헤더파일에 enum을 정의해 두고 각. cpp에서 #include 형식으로 헤더 파일을 읽어오게 만들어주면 유용하게 사용할 수 있다고 한다.

 

 

Chapter 4.8 영역 제한 열거형 (열거형 클래스)

 

 

해당 강의에서는 이전 강의에서 배웠던 열거형 자료형에서 변화한 형태인 열거형 클래스에 대해서 다루게 된다.

 

 

예시 코드 1

enum Color
{
    RED,
    BLUE
};

enum Fruit
{
    BANANA,
    APPLE
};

Color color = RED;
Fruit fruit = BANANA;

if (color == fruit)
{
    cout << "Color == fruit";
}

 

우선 저번 강의 때 배웠던 열거형 type을 그대로 사용해 보자.

 

열거형의 특성상 안에 있는 값들을 int로 사용한다.

 

따라서 Color 자료형의 RED와 Fruit 자료형의 BANANA는 동일하게 int로는 0이다.

 

그래서 if 문에서도 같다고 판단을 하게 된다.

 

이런 경우 int 값으로는 같으나 실제 데이터는 다른 상황이니, 실수할 가능성을 만들 수 있다.

 

이를 방지하고자 사용하는 것이 enum class이다.

 

 

 

예시 코드 2

using namespace std;

enum class Color
{
    RED,
    BLUE
};

enum class Fruit
{
    BANANA,
    APPLE
};

Color c_1 = Color::RED;
Color c_2 = Color::BLUE;

if (c_1 == c_2)
{
    cout << "same color! " << endl;
}

 

enum과 다른 점은, enum class에서 사용된 멤버는 enum 때처럼 그냥 사용할 수 없고, 마치 namepsace처럼 ::를 사용해야 한다.

 

그래서 기존하고 다르게 Color::RED 이런 식으로 사용해야 한다.

 

그리고 enum 때와 다르게 만약 Fruit 자료형 변수를 사용할 경우 == 연산자 자체를 사용할 수가 없다.

 

 

강의를 듣고 나서 약간 헷갈리는 부분들이 있어서, Gemini를 활용해 정리를 요청했더니 조금 더 깔끔하게 정리된 것 같다.

 

해당 내용을 첨부하니, 정리용으로 보면 좋을 듯하다.

 

 

 

enum을 수업할 때 언급된 내용도 있고 아닌 내용도 있는데 한번 정리해 보자면...

 

우선 Scope가 다른데, 이전 enum 수업에서도 언급이 나왔다시피 enum 안에 멤버로 선언된 변수는 다른 범위에서도 접근이 가능하다. 그래서 서로 다른 enum 끼리 같은 멤버를 쓰면 안 된다는 내용이 수업시간에 나왔었다. 마치 전역 변수처럼 사용되기 때문이다. 

 

그런데 enum class의 경우 namespace처럼 범위가 한정되기 때문에 이런 문제가 발생하지 않는다.

 

다음으로는 암시적 변환에 대한 부분인데... enum에 사용된 멤버는 암시적으로 형 변환이 된다. 특히 enum의 멤버는 int 값으로 저장이 되고 있기 때문에... 이를 int로 형변환 할 수 있다.

 

하지만 enum class의 경우 static_cast <int>를 통해서 명시적 형변환을 해줘야만 int로 변환이 가능하다. 이런 관점에서는 enum class가 훨씬 안전하다고 볼 수 있겠다.

 

그리고 자료형에 대해서는, enum의 경우 보통 int로 사용되나 enum class는 자료형을 명시적으로 지정할 수 있다고 한다. 이 내용은 수업시간에 나오진 않았던 내용인데 이런 기능을 보니 확실히 enum보다는 enum class가 더 많이 쓰이는 이유를 알 수 있을 듯하다.

 

namespace의 경우 이미 잘 아는 내용이고... 비교에 대해서도 앞의 예제에서 언급했듯이 다른 enum class 끼리는 비교 연산자가 작동하지 않는다. (에러가 발생함)

 

 

 

Chapter 4.9 자료형에게 가명 붙여주기

 

 

해당 강의에서는 자료형에 새로운 이름을 붙여서 사용하는 방식인 typedef에 대해서 다룬다.

 

 

#include <iostream>
#include <cstdint>
#include <vector>

int main()
{
	typedef double distance_t; // 코드상에서 둘 다 사용 가능함. 보통 뒤에는 _t는 타입 이름이다 라는 의미.

	double		my_distance;
	distance_t	home2work;
	distance_t  home2school;

	// aliases를 사용하는 이유에 가까움.
	//typedef std::vector<std::pair<std::string, int>> pairlist_t;
	using pairlist_t = std::vector<std::pair<std::string, int>>;

	pairlist_t pairlist1;
	pairlist_t pairlist2;




}

 

 

typedef의 경우 typedef 사용하고자 하는 자료형 별명 순서로 사용하게 된다

 

위 코드에서 distance_t라는 자료형은 실제로 double과 같은 자료형이나 이름만 붙여준 것이라고 보면 된다.

 

그리고 여러 자료형을 혼합해서 사용해 자료형의 이름이 길어질 때, 이를 축약해서 사용할 때도 유용하게 사용할 수 있다.

 

 

Chapter 4.10 구조체 struct

 

 

이번 강의에서는 클래스로 넘어가는 다리이자, 다양한 자료형을 하나의 바구니에 담을 수 있는 방법인 구조체에 대해서 다룬다.

 

 

예시 코드 1

#include <string>

struct Person
{
	double  height = 3.0;
	float	weight = 200.0;
	int		age = 100;
	string  name = "Hello";

	// 여러 변수들을 가지고 기능을 하려고 하는 함수다.
	// 데이터와 기능을 묶은 것.
	void print()
	{
		cout << height << " " << weight << " " << age << " " << name << " ";
		cout << endl;
	}
};

int main()
{
	Person me;
    cout << me.name << endl;
    
    Person me{ 2.0, 100.0, 20, "Jack Jack"};
    me.print();
}

 

구조체는 여러 자료형을 한 번에 담을 수 있는 특징을 가지고 있다.

 

따라서 struct 안에 double float int string 등 다양한 자료형이 존재할 수 있으며, 위 코드와 같이 초기값을 주기도 한다.

 

그리고 신기하게 struct 안에 함수를 쓸 수도 있다. 

 

구조체의 멤버 변수에 접근할 때는 . 을 사용하면 된다.

 

uniform initialization을 활용하면 조금 더 손쉽게 구조체에 값을 줄 수 있다.

 

 

 

예시 코드 2

struct Employee			// 14 bytes
{
	short	id;			// 2 bytes
	int		age;		// 4 bytes
	double	wage;		// 8 bytes
};

// 자료를 배치할 때 컴퓨터 잘 처리할 수 있는 형태로 하다보면 추가로 더 들어감. (패딩 이라고 부름)
Employee emp1;
cout << sizeof(emp1) << endl; // 16

 

구조체 type에도 sizeof를 사용할 수 있다.

 

다만, 자료를 배치할 때 컴퓨터가 잘 처리할 수 있는 형태로 하다 보면 실제 값하고는 차이가 있을 수 있다고 하니 주의해야 한다고 한다. 이를 패딩이라고 한다고 한다.

 

Gemini에게 물어보니, 조금 더 구체적인 답변을 들을 수 있었다.

 

 

 

수업 시간에도 2바이트를 처리하기가 좀 어렵다? 이런 얘기를 하셨는데, Gemini의 답변을 듣고 나니 CPU가 메모리에 접근할 때 4 byte나 8 byte와 같은 경계 기준이 있는 것으로 보인다.

 

 

 

 

 

여기까지 Chatper 4를 모두 정리해보았다.

 

 

중간에 회사 일이 너무 바빠 퇴근을 할 수 없어, 작성이 계속 지연되었다.

 

 

다시 열심히..! 해보려고 한다

 

 

 

 

순서상으로는 Chapter 0부터 해야 되지만, 지금 Chapter 3을 듣고 있어서... 우선 3부터 정리합니다.

 

 

현재 정리하고 있는 강의는 다음 강의입니다.

https://www.inflearn.com/course/following-c-plus

 

홍정모의 따라하며 배우는 C++ 강의 | 홍정모 - 인프런

홍정모 | , 6,000+명의 수강생이 증명합니다 👍모던 C++ 바이블, 따배씨++를 만나보세요! <2024 프로그래밍 공부 순서> [임베딩 영상]   뛰어난 프로그래밍 실력을 갖추고 싶다면. [사진] 모던(Modern) C+

www.inflearn.com

 

 

 

 

 

Chapter 3 목차

 

 

 

 

 

Chapter 3.1 연산자 우선순위와 결합 법칙

 

 

 

C++에서는 연산자 간의 우선순위가 있다. 여러 연산자가 나열되어 있는 경우, 우선순위가 높은 연산자를 먼저 실행하게 된다.

 

https://en.cppreference.com/w/cpp/language/operator_precedence

 

C++ Operator Precedence - cppreference.com

The following table lists the precedence and associativity of C++ operators. Operators are listed top to bottom, in descending precedence. a, b and c are operands. ↑ The operand of sizeof cannot be a C-style type cast: the expression sizeof (int) * p is

en.cppreference.com

 

사실 이 우선순위를 다 외우고 있기는 어려울 것 같고, 원하는 결과가 나오지 않는다면 표를 참고해서 문제를 해결해야 한다.

 

그리고 원하지 않는 결과가 나오는 것을 방지하려면, 괄호를 통해서 연산의 범위를 잘 정해주는 것이 위험을 줄이는 방법이다. (다른 개발자들도 우선순위를 모두 외우지는 못한다!)

 

연산자 우선순위가 적용되는 방향이 Left-to-right가 있고, Right-to-left가 있으니 이 또한 주의해야 한다.

 

우리가 수학에서 많이 쓰는 우선순위 (*, /가 +, -보다 더 우선순위가 높다)는 헷갈리지 않지만, 그렇지 않은 경우가 어려운 것 같다.

 

 

수업 코드 1

int a = 1;
int b = 2;
int c = 3;

a = b = c; // c -> b -> a로 assignment 됨.

std::cout << "a: " << a << " b: " << b << " c: " << c << std::endl; // a, b, c 모두 3

 

-> a = b = c로 해주는 경우 c를 b로 assignment 하고, b를 다시 a에 assignment 해서 결국 a, b, c가 같은 값이 되어 버린다.

 

 

수업 코드 2

int w = 3;
double t = 20;
t /= --w + 5; // --w + 5부터 진행하고, /=를 적용함.

std::cout << w << std::endl;

std::cout << "t: " << t << std::endl;

 

-> /= 연산자의 경우, right-to-left라서 우측부터 진행하고 좌측 진행.

 

따라서 w는 2가 되고(--w 영향), t는 2.85714 값이 나온다. 이는 t가 double이기 때문에, 결과가 소수점으로 출력된다.

 

 

 

 

Chapter 3.2 산술 연산자 (Arithmetic Operators)

 

 

몫 연산자(/)와 나머지 연산자(%)에 대해서 정리하는 챕터이다.

 

 

수업코드 1

int x = 7;
int y = 4;

// x / y ==> 몫, x % y ==> 나머지
// 나누기 시 한쪽이 실수면 결과가 실수로 나온다.
cout << "x / y : " << x / y << endl;
cout << "x % y : " << x % y << endl;
cout << "only x float case: " << float(x) / y << endl;
cout << "only y float case: " << x / (float)y << endl;
cout << "x and y float case: " << float(x) / float(y) << endl;

// 음의 정수의 경우, 소수점을 모두 절삭함. 
cout << "-5 / 2 ? " << -5 / 2 << endl;

// 왼쪽에 있는 숫자가 음수면 나머지의 결과도 음수다.
// 왼쪽에 있는 숫자가 양수면 나머지의 결과도 양수다.
cout << "-5 % 2 ? " << -5 % 2 << endl;
cout << "5 % 2 ? " << 5 % 2 << endl;

 

 

 / 연산자는 몫을 나타내고, %는 나머지를 나타낸다. 

 

피연산자들이 둘 다 int면 몫과 나머지 모두 int로 출력한다. 단, 피연산자 중 한 개라도 float이면 결과를 float로 출력한다.

 

음의 정수의 경우 몫 계산 시 소수점을 절삭하며, 나머지 계산 시 왼쪽에 있는 숫자의 부호에 맞춰서 결과가 나온다.

 

 

 

 

Chapter 3.3 증감 연산자 (Increment decrement operators)

 

 

증감 연산자의 위치에 따른 차이를 알려주는 챕터이다.

 

 

수업코드 1

int add(int a, int b)
{
	return a + b;
}

int x = 6, y = 6;

cout << x << " " << y << endl; // 6 6

// 앞에 +가 붙는 경우 x에 1을 더해주고 출력
// 뒤에 +가 붙는 경우 x를 stream에 보낸 다음에 1을 더해지는 것.
cout << ++x << " " << --y << endl; // 7 5

cout << x << " " << y << endl; // 7 5

cout << x++ << " " << y-- << endl; // 7 5

cout << x << " " << y << endl; // 8 4

int a = 1, b = 2;
int v = add(a, ++b);

cout << v << endl;

 

 

 

 

증감 연산자가 앞에 붙는 경우, 바로 해당 변수에 적용해 준다.

 

따라서 ++x를 출력할 때 기존 x 대비 +1이 된 것을 알 수 있다.

 

하지만 증감 연산자가 뒤에 붙는 경우, 해당 출력 코드가 종료된 이후에 적용된다.

 

따라서 x++를 출력할 때는 여전히 x의 값이 7이지만, 그다음 줄에서 x를 cout 했을 때 8이 되는 것을 알 수 있다.

 

 

그리고 add(a, ++b)를 실행시켰을 때는 b가 기존 대비 1만큼 올라간 값을 기준으로 add 함수를 타게 된다. (함수의 인자값으로 들어갈 때도 동일하다는 것을 보여주는 것)

 

 

 

 

 

Chapter 3.4 sizeof, 쉼표 연산자, 조건부 연산자

 

 

sizeof와 쉼표 연산자, 조건부 연산자에 대해서 설명하는 챕터이다.

 

우선 sizeof에 대해서 정리해 보자.

 

 

수업코드 1

using namespace std;

float a;

// sizeof는 데이터 타입을 넣거나, 변수를 넣을 수 있다.
// struct나 class 처럼 사용자가 만든 자료형에도 사용할 수 있음.
// sizeof는 연산자(Operator)임.
// 변수명에 사용하는 경우는 괄호를 사용하지 않더라도 가능.
cout << sizeof(float) << endl; // 4 바이트, 32비트 
cout << sizeof(a) << endl; // 4바이트, 32비트
cout << sizeof a << endl;

 

 

 

sizeof는 변수나 데이터 타입의 size를 출력하는 연산자이다. 출력 기준은 byte이다. 

 

자료형을 direct로 인자값으로 넣을 수도 있고, 혹은 변수명을 인자값으로 넣을 수도 있다. 

 

 

 

수업 코드 2

// comma operator
int x = 3;
int y = 10;
int z = (++x, ++y);
// ++x;
// ++y;
// int z = y;
cout << "x: " << x << " " << " y: " << y << " " << " z: " << z << endl;

int a1 = 1, b1 = 10;
int z1, z2;

z1 = a1, b1; // =가 , 보다 우선순위가 더 높다, 따라서 결과가 z1 = a1를 하는 것과 동일한 상황임.
z2 = (++a1, a1 + b1);

cout << z1 << endl;
cout << z2 << endl;

 

 

 

 

다음은 쉼표 연산자에 대한 설명이다.

 

z = (++x, ++y)로 작성하는 경우, 먼저 왼쪽을 실행하고(++x),  다음으로 오른쪽을 실행하고(++y), z에 y 값을 할당한다.

 

따라서 아래 출력 값을 확인해 보면, x는 4가 되어 있고, y는 11이 되어 있으며, z에는 y 값인 11이 할당되어 있는 것을 확인할 수 있다. 

 

코드상으로는 3줄을 써야 하는 것을 1줄로 묶은 느낌인데, for문에서 많이 쓴다고 한다.

 

 

그리고 z1를 보면, z1 = a1, b1; 로 코드를 작성한 것을 알 수 있는데, = 연산자가 , 보다 우선순위가 높기 때문에 b1에는 아무 일도 일어나지 않고 a1 값이 z1에 할당되는 것을 확인할 수 있다. 

 

 

수업 코드 3

int getPrice(bool onSale)
{
	if (onSale) return 10;
	else return 100;
}

// conditional operator (arithmetric if)
bool onSale = true;

// const로 사용하는 경우, 처음 변수 정의할 때 값을 줘야하니 conditional operator를 사용하기에 적합.
// 조건이 복잡하거나, 값이 복잡한 경우에는 if문으로 쪼개는 것이 좋다. 간단한 경우 사용하는 것이 적합.
const int price = (onSale == true) ? 10 : 100;
//const int price = getPrice(onSale);


cout << "Price: " << price << endl;

 

 

 

 

다음은 조건 연산자에 대한 설명이다.

 

const 변수를 사용할 때, 조건에 따라 해당 변수에 값을 줘야 하는 경우 사용한다고 한다.

 

현재 코드 상으로는 onSale 변수가 true면 price가 10이고, false면 100이 되는 것을 알 수 있다.

 

삼항 연산자를 활용하는 것이기 때문에, 조건이 가급적 간단할 때만 사용하는 것을 추천한다고 한다.

 

삼항 연산자를 사용하지 않게 되면, getPrice 함수를 따로 짜서 삼항 연산자를 if문 형식으로 풀어서 구현하는 방법도 있다.

 

 

수업 코드 4

int sample_x = 5;

// ? : 보다 <<가 우선순위가 높아서 벌어지는 현상.
// cout << (x % 2 == 0) ? "even" : "odd" << endl;
cout << ((x % 2 == 0) ? "even" : "odd") << endl;

 

마지막으로 연산자 우선순위 때문에 벌어지는 문제를 보여주는 코드이다.

 

위 코드는 아예 컴파일 자체가 안되고, 다음과 같은 오류가 나온다.

 

 

 

이는? 와 :보다 <<가 우선순위가 높기 때문에, cout << (x % 2 == 0)에서 << 연산이 끊기고, 그 뒤에 ? "even" : "odd"만 딸랑 남게 되기 때문에 문제가 발생한다고 볼 수 있다.

 

따라서, 여러 가지 연산이 복합적으로 사용될 때는 괄호를 잘 활용해서 묶어줘야만 의도된 대로 코드가 작동할 수 있다.

 

 

 

 

Chapter 3.5 관계 연산자 (Relational Operators)

 

 

해당 챕터에서는 관계 연산자에 대해서 알아본다.

 

 

수업 코드 1

if (x == y)
{
    cout << "Equal ! " << endl;
}

if (x != y)
{
    cout << "Not Equal ! " << endl;
}

if (x > y)
{
    cout << "x is bigger than y" << endl;
}

if (x < y)
{
    cout << "x is smaller than y" << endl;
}

if (x >= y)
{
    cout << "x is bigger than y or equal to y" << endl;
}

if (x <= y)
{
    cout << "x is smaller than y or equal to y" << endl;
}

 

 

관계 연산자는 크게 ==, !=, >, <, >=, <=가 있다. 부등호는 수학에서 사용하는 것과 동일해서 크게 어려울 게 없고, 코딩을 처음 해보는 분이라면 같다는 표기가 ==라는 것만 주의하면 될 것 같다. 

 

int 변수에서는 크게 문제가 없지만, float나 double처럼 실수형 변수에서가 주의할 내용들이 조금 있다.

 

 

 

수업 코드 2

double d1(100 - 99.99); // 0.01
double d2(10 - 9.99);	// 0.01

const double epsilon = 1e-10;

if (abs(d1 - d2) < epsilon)
{
    cout << "Approximatedly equal" << endl;
}
else
{
    cout << "Not Equal" << endl;
}

if (d1 == d2)
{
    cout << "Equal " << endl;
}
else
{
    cout << "Not Equal" << endl;

    cout << std::setprecision(20);

    cout << "d1: " << d1 << " d2: " << d2 << endl;

    cout << abs(d1 - d2) << endl;

    if (d1 > d2) cout << "d1 > d2" << endl;
    else cout << "d1 < d2" << endl;

}

 

 

 

double 변수 d1, d2를 정의하고, 각각 두 값이 0.01이 되도록 연산해 줬다.

 

이전에 강의에서도 언급되었듯이, 실제로 우리가 인식하는 값과 컴퓨터가 계산한 값이 다를 수 있다. 

 

따라서 d1 == d2를 확인해 보면, Not Equal이 나오는 것을 확인할 수 있다.

 

그래서 실제로 왜 다른지를 확인해 보면(setprecision을 높여서 확인), 아주 엄밀하게 따지면 아주 작은 값의 차이가 있는 것을 알 수 있다.

 

그래서 보통 소수점 변수에 대해서 계산하는 경우, 두 변수간 차이(abs(d1 - d2))를 계산해서 이 값이 특정 값보다 작은 경우는 두 변수의 값이 같다고 인정하는 경우가 많다고 한다. 어느 정도의 차이까지 인정할지는 domain에 따라 다르므로, domain knowledge를 기반으로 정한다고 한다.

 

 

 

 

Chapter 3.6 논리 연산자 (Logical operators)

 

 

해당 챕터에서는 논리 연산자(!(not), &&(and), ||(or))에 대해서 알아본다.

 

 

 

수업 코드 1

int x = 2;
int y = 2;

// short circuit evaluation
// &&로 묶여 있어서 왼쪽에 있는 식을 먼저 계산하고 오른쪽 식을 진행하게 됨.
// 이때, And 연산자는 왼쪽을 판단하고 false인 경우 오른쪽 계산을 진행하지 않고 false를 return함.
if (x == 1 && y++ == 2)
{
    // do something
}

 

 

short circuit evaluation에 대한 설명이다.

 

if 조건문에서 만약 좌측 조건이 false가 되는 경우, 우측 코드가 작동하지 않는다는 설명이다.

 

좌측 조건이 true인 경우, 우측 코드가 작동하면서 y는 3으로 값이 올라가게 된다.

 

 

 

수업 코드 2

bool a = false;
bool b = false;

// De Morgan's Law
// !(a && b); <==> !a || !b
// !(a || b); <==> !a && !b

// XOR
// false false -> false
// false true -> true
// true false -> true
// true true -> false

// XOR는 이렇게 표현한다.
cout << (a != b) << endl;

 

수학 시간에 배웠던 드 모르간 법칙에 대해서도 설명이 있었다.

 

사실 수학시간에 배운 걸 그대로 적용하면 되는 거라 어렵지는 않다. 명제 파트에서 배운 내용과 비슷하다.

 

a && b(a and b)의 !(not)을 적용하게 되면 !a || !b(not a or not b)가 된다는 내용이다.

 

그리고 XOR에 대해서도 설명이 있었는데, 이는 a != b 연산으로 구현이 가능하다고 한다.

 

연산 결과를 보면, a와 b의 값이 다르면 true이고 같으면 false이므로, a와 b가 다르다는 연산으로 구현이 가능하다.

 

 

 

수업 코드 3

bool v1 = true;
bool v2 = false;
bool v3 = false;

// Logical AND가 logical OR보다 우선순위가 높다!
// 괄호를 통해서 잘 나눠주는 것이 적합하다. 
// 다른 사람들도 실수할 수 있는 부분을 배려해서 코딩하자. 
bool r1 = v1 || v2 && v3;
bool r2 = (v1 || v2) && v3;
bool r3 = v1 || (v2 && v3);

cout << "r1: " << r1 << " " << " r2: " << r2 << "  " << endl;
cout << "r3: " << r3 << endl;

 

 

 

 

다음은 And가 Or보다 우선순위가 높다는 것을 설명하는 내용이다.

 

v1 || v2 && v3로 연산하는 경우, v1 || (v2 && v3) 연산하는 것과 결과가 같다.

 

물론 And가 Or보다 우선순위가 높다는 사실을 기억하는 것도 좋지만, 코딩을 할 때 조금 더 분명하게 괄호로 잘 나눠주는 것이 더 바람직하다고 한다. 

 

 

 

 

 

Chapter 3.7 이진수 (Binary Numbers)

 

 

 

이진수에 대해서 설명하는 챕터이다. 코드로 구현하는 챕터는 아니고, 이론적인 내용으로 진행된다.

 

 

우선 10진법을 정리해보자면, 다음과 같다.

 

11이라는 숫자는 $1$x$10^1$ + $1$x$10^0$으로 이루어진다. 

 

같은 원리로, 이진법일 때 11이라는 숫자는 $1$x$2^1$ + $1$x$2^0$로 표현된다.

 

 

 

다음으로는 2진수 덧셈이 있다.

 

우리가 익숙한 10진수 덧셈과 동일하지만, 2진수는 0과 1로만 이루어져 있어서 자릿수가 2일 때 넘어간다는 점을 기억하고 있으면 된다.

 

 

    0 1 1 0

+  0 1 1 1

----------------

              1

 

       1 

    0 1 1 0

+  0 1 1 1

----------------

          0 1

 

 

    0 1 1 0

+  0 1 1 1

----------------

    1 1 0 1

 

 

로 계산이 된다.

 

더해서 2가 되면, 다음 자릿수로 숫자가 넘어간다는 점만 기억하고 있으면 된다.

 

 

 

다음으로는 음의 정수를 표현하는 방법을 알아보자. 

 

-5를 8비트, 1byte 2진수로 표현해본다고 하면, 우선 부호를 제외하고 5를 먼저 표현해 본다.

 

0 0 0 0  0 1 0 1로 표현된다.

 

여기서 보수(Complement)를 취한다. 이는 0을 1로, 1을 0으로 바꿔주면 된다.

 

1 1 1 1  1 0 1 0로 바뀐다.

 

여기서 + 1을 해준다.

 

1 1 1 1  1 0 1 1이 된다.

 

이것이 최종적으로 -5를 표현한 것으로 이해하면 된다. 

 

2진수에서 부호(-, +)를 감안해서 표현하는 경우, 맨 앞자리가 부호를 나타낸다고 보면 된다.

 

따라서 아까 5를 나타낼 때 맨 앞자리가 0이었고, -5를 나타낼 때는 맨 앞자리가 1이다.

 

마지막에 +1을 해주는 이유는, 숫자 0 때문이다.

 

0을 4bit로 표현한다면 0 0 0 0이 되는데, 이를 보수로 표현하면 1 1 1 1이다.

 

이 상태로 사용한다면 0과 -0이 존재하는 셈인 것이다.

 

따라서 여기에 +1을 해서 0 0 0 0으로 만들어준다면, 0과 -0이 모두 0 0 0 0으로 표현되는 셈이다.

 

 

 

다음으로는 10진수를 2진수로 바꾸는 법에 대해서 알아보자.

 

148을 2진수로 바꾼다고 한다면,

 

148 / 2 = 74, 나머지 0

74 / 2 = 37, 나머지 0

37 / 2 = 18, 나머지 1

18 / 2 = 9, 나머지 0

9 / 2 = 4, 나머지 1

4 / 2 = 2, 나머지 0

2 / 2 = 1, 나머지 0

1 / 2 = 0, 나머지 1

 

여기서 나머지 부분을 아래부터 위로 쪽 이어서 적으면, 1 0 0 1 0 1 0 0이 된다.

 

 

 

하나의 2진수 정수에 대해서 이를 signed int로 볼 때와 unsigned int로 볼 때를 나눠서 생각해 보자.

 

1 0 0 1   1 1 1 0이라는 2진수 정수가 있다.

 

우선 이를 signed int로 보자면, 부호가 없으므로 단순히 변환만 해주면 된다.

 

128 + 16 + 8 + 4 + 2 이므로 158이 된다.

 

unsigned int라고 하면, 맨 앞자리가 부호가 된다. 맨 앞자리가 1이므로, 이는 음수라는 의미이며, 앞에서 언급했듯이 음수 2진수 값을 표현하기 위해서는 우선 양수로 바꾼 후, 처리해줘야 한다.

 

1 0 0 1    1 1 1 0에서 보수를 취해주면,

 

0 1 1 0    0 0 0 1이 되고, 여기서 +1을 취해주면,

 

0 1 1 0    0 0 1 0이 된다.

 

이를 계산하면 64 + 32 + 2 = 98이며, 기존에 음수였으므로 -98이다.

 

 

 

Chapter 3.8 비트단위 연산자 (Bitwise Operators)

 

 

bool 자료형을 생각해 보자. 이는 0과 1만 표현하면 되니, 사실상 1 bit만 있어도 정보를 모두 표현할 수 있다.

 

그러나 메모리 할당의 가장 기본 크기는 1 byte로, 8 bit를 사용한다. 그럼 메모리의 나머지 7 bit는 노는 것이다.

 

따라서 메모리를 아끼고, 전부 계산에 사용해서 의미 있게 사용하기 위해서 비트단위 연산자를 사용하게 된다.

 

비트단위 연산자를 사용하면 계산 속도가 빠르다.

 

 

비트단위 연산자는 총 6개로, << (left shift), >> (right shift), ~(not), &(and), |(or), ^(XOR)가 있다.

 

<<는 cout에서, >>는 cin에서 볼 수 있는데, 생긴 거만 같지 사실 cout과 cin에서 사용하는 것과 의미는 다르다.

 

cout과 cin은 라이브러리에서 강제로 연산자를 오버로딩해서 사용하고 있는 케이스라고 보면 된다.

 

 

 

#include <bitset>

unsigned int a = 0b01100; 
unsigned int b = a << 1;


cout << std::bitset<5>(a) << endl; 

cout << std::bitset<5>(b) << " " << b << endl;

 

 

 

 

a는 01100으로, 원래는 값이 12이다.

 

이를 b = a << 1로 해서, left shift를 적용해 주게 되면, b는 1 숫자를 왼쪽으로 당긴 결과인 11000으로 변하게 되고, 그 값은 24가 되면서 2배가 된다.

 

이처럼, left shift 해주는 경우는 이진수 기준으로 1을 왼쪽으로 이동시켜 주고, 숫자를 2배로 올려준다.

 

만약 left shift를 4번 해주면, 2의 4 제곱만큼 커지는 개념이다. 

 

 

right shift는 반대의 개념이다. 이진수 기준으로 오른쪽으로 이동시키게 되고, 자릿수가 반대로 이동되니 오히려 숫자를 /2로 만들어준다. 

 

 

다음으로는 bitwise and, or, xor, not 정리해 본다.

 

unsigned int a = 0b01100; 
unsigned int b = 0b10110;

cout << "bitwise and " << std::bitset<5>(a & b) << endl;

cout << "bitwise or " << std::bitset<5>(a | b) << endl;

cout << "bitwise XOR " << std::bitset<5>(a ^ b) << endl;

cout << "Not " << std::bitset<5>(~a) << endl;

 

 

 

&는 이진수 기준으로 and를 나타낸다.

 

a가 01100이고 b가 10110이니, 각 자리마다 생각해 보면, 첫자리는 a가 0이고 b가 1이니 0으로,

 

두 번째 자리는 a가 1이고 b가 0이니 0, 세 번째 짜리는 a가 1이고 b가 1이니 and로 1,

 

네 번째 자리는 a가 0이고 b가 1이니 0, 마지막 자리는 a가 0이고 b가 0이니 0이다. 따라서 a & b의 결과는 00100이다.

 

bitwise or인 |도 똑같은 원리로 생각하면 되고, bitwise XOR는 이전 강의에서도 얘기했듯이 != 관점으로 생각하면 된다.

 

즉, 두 값이 달라야 1이고, 같으면 0이 되는 것이다.

 

그리고 bitwise not인 ~a는 보수를 생각하면 된다.

 

 

 

마지막으로 수업시간에 얘기해 주신 Quiz가 있다. 이를 풀어보자.

 

1. 0110 >> 2를 10진수로 결과를 표현하면?

2. 5 | 12를 10진수로 결과를 내면?

3. 5 & 12를 10진수로 결과를 내면?

4. 5 ^ 12를 10진수로 결과를 내면?

 

답은 접은 글로 적어둡니다.

 

더보기

A1: 0001 (두 번 밀리면 자릿수를 벗어난 1은 그냥 사라짐.), 10진수로는 1

A2: 13 (5를 0101로, 12를 1100로 바꾼 뒤 계산하면 됨.)

A3: 4

A4: 9

 

 

Chapter 3.9 비트 플래그, 비트 마스크 (Bit flags, Bit masks)

 

 

이 챕터에서는 비트 플래그와 비트 마스크에 대해서 다룬다.

 

 

수업 코드 1

const unsigned char opt0 = 1 << 0;
const unsigned char opt1 = 1 << 1;
const unsigned char opt2 = 1 << 2;
const unsigned char opt3 = 1 << 3;

unsigned char items_flag = 0;

// 00000000이니까, 8개에 대해서 true/false를 만들 수 있다.
cout << "No item " << bitset<8>(items_flag) << endl;

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

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

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



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

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

// obtain item 2, 3
items_flag |= (opt2 | opt3);
cout << "Obtain item 2 and item 3 " << bitset<8>(items_flag) << endl;

// item 2를 가지고 있고 item 1을 가지고 있지 않은 경우
if ((items_flag & opt2) && !(items_flag & opt1))
{
    // 상태를 바꿔줄 땐 XOR 사용
    //items_flag ^= opt2;
    //items_flag ^= opt1;
    items_flag ^= (opt2 | opt1);
}

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

 

 

게임에서 8개의 아이템이 있다고 할 때, 각 아이템을 가지고 있는지 아닌지를 표현한다면 bool 변수 8개가 필요하다. 

 

 

하지만 아이템의 개수가 100개 1000개로 늘어난다면, 개별 변수로 선언해가지고는 감당하기 어렵다.

 

 

따라서 이를 비트 플래그라는 방식으로 대응하는 것이다. 4 byte (32bit)짜리 변수 1개면 32 종류의 아이템의 소지 여부를 bool 변수로 나타낼 수 있는 것이다.

 

 

 

 

우선 아무것도 가지고 있지 않을 때, bitset으로 출력해보면 00000000인 것을 알 수 있다.

 

다음으로, item0 번을 가지면 첫 번째 값이 1로 바뀌어야 하며, 이는 |=를 통해서 구현할 수 있다.

 

만약 item3 번을 가지고 있다가 이를 잃는다면, opt3을 not으로 바꾼 뒤 이를 and로 연산하면 된다.

 

이를 조금 더 설명해보자면,

 

기존에 0000 1001 이였을 때, ~opt3은 1111 0111이다. 여기서 & 연산자로 계산해 보면 0000 0001이 된다.

 

&가 and이니, 4번째 자리 숫자를 제외하고 나머지를 그대로 유지하게 된다. 4번째 자리는 반대로 변경된다.

 

해당 아이템을 가지고 있는지 여부는 &를 통해서 확인할 수 있는데, 이는 확인하고자 하는 자릿수만 1이고 나머지가 0인 변수와 and 연산자이기 때문에 여부를 확인할 수 있다.

 

그리고 item 2과 item 3을 한 번에 얻는다고 하면, item 2인 char와 item 3인 char를 or로 해서 합쳐주고 다시 or 연산자를 해준다.

 

마지막으로, item 2를 가지고 있고, item 1을 가지고 있지 않다면, 전자는 &로, 후자는 &를 한 뒤 not(!)을 붙여서 조건을 완성하면 된다.

 

상태를 바꿔줄 땐 XOR를 사용하는데, 이 부분도 예시를 통해 생각해 보자.

 

item flag가 0000 1001이고, 4번째 아이템의 여부를 바꾸려고 한다면, item3이 0000 1000일 태니

 

XOR의 성질은 같으면 0, 다르면 1이다.

 

그래서 0000 0001이 된다. 

 

 

수업 코드 2

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;

cout << bitset<32>(pixel_color) << endl;

unsigned char green = (pixel_color & green_mask) >> 8;
unsigned char blue = pixel_color & blue_mask;
unsigned char red = (pixel_color & red_mask) >> 16;

cout << "blue: " << bitset<8>(blue) << " " << int(blue) << endl;
cout << "green: " << bitset<8>(green) << " " << int(green) << endl;
cout << "red: " << bitset<8>(red) << " " << int(red) << endl;

 

다음은 비트 마스크이다.

 

수업시간에 왜 마스크인지는 얘기를 안 해주셨지만, int에서 일부분만 추출한다는 측면에서 나머지 값들에 마스크를 씌운다는 느낌이 아닐까 하고 생각해 본다.

 

 

 

 

pixel_color는 16진수로 구성되어 있고, 각각 red값 2개 + green값 2개 + blue값 2개로 구성된다.

 

따라서 DAA520 중 DA는 red, A5는 green, 20는 blue에 해당하는 값이다. 이를 적절하게 추출하고자 하는 것이 목표이다.

 

red_mask는 0 xFF0000로 되어 있는데, 이는 FF가 255이고, 1 byte(8 bit)를 전부 1로 채운 값이 된다. 

 

그래서 출력값의 첫 번째가 0 8개 + 1 8개 + 0 16개로 구성이 되어 있는 것이다. 

 

개념적으로 보자면 앞에서 다루었던 비트 플래그와 유사하다.

 

 

blue의 경우, 맨 오른쪽 자리를 차지하기 때문에 단순히 &를 활용하면 값을 추출할 수 있다.

 

그러나 green이나 red의 경우 오른쪽에 각각 8자리, 16자리의 수가 있기 때문에 실제 값을 제대로 추출할 수 없다.

 

따라서 이전에 배웠던 비트 연산자를 이용해서, 각각 8자리, 16자리씩 오른쪽으로 이동(>> 연산자 사용)을 해야만 실제 값을 제대로 얻을 수 있다.

 

 

 

 

 

블로그에 정리해 보니, 확실히 내용을 제대로 복습할 수 있는 것 같다.

 

 

다음 글에서는 Chapter 4를 정리해보려고 한다.

 

안녕하세요?

 

 

거의 2년만에 블로그에 다시 글을 쓰네요.

 

 

요즘 C++을 기초부터 공부중인데, 블로그에 정리해두면 추후 복습할 때도 좋을 것 같아 원래 쓰던 이 블로그에 정리하려고 합니다.

 

 

현재 수강하고 있는 강의는 https://www.inflearn.com/course/following-c-plus 이고, 열심히 들어서 상반기 안에 완강하는게 목표입니다.

 

 

다음 게시물에서 뵙겠습니다.

 

 

 

+ Recent posts