read

세번째 열혈강의


computer

이전 포스팅에 이어서 OOP 단계별 프로젝트 2단계를 이어가겠다. C에 대한 간단한 리뷰를 했고, 앞으로도 C에 대한 얘기는 자주 나올듯 하니 제대로 알고 넘어가야 할 것이다.


bool


bool 형이 무엇인지는 다들 알 듯 하다. 원래 C언어에는 존재하지 않고, C++에만 있는 타입인데, 최근에 추가가 되었다고 한다. 그래도 상당수 C컴파일러가 인식을 못하니 조심해야 한다. bool형은 참과 거짓이라는 단어로 설명이 된다. C와 C++ 모두 정수 0은 거짓, 그리고 0이 아닌 모든 정수는 참을 의미한다. 따라서 C에서는 참과 거짓 표현을 위해 상수를 정의 해두는 것이 보통이다.

#define TRUE 1
#define FALSE 0

그런데 C++은 bool이라는 자료형으로 true,false를 정의하고 있기에 굳이 위 예제처럼 메크로 상수를 쓸 필요가 없다. 간단한 예제를 보도록 하자.

#include <iostream>

using namespace std;

int main() {

	int num = 10;
	int i = 0;

	cout << "true " << true << endl;
	cout << "false " << false << endl;

	while (true) {
		cout << i++ << ' ';
		if (i > num)
			break;
	}
	cout << endl;

	cout << "sizeof 1: " << sizeof(1) << endl;
	cout << "sizeof 0: " << sizeof(0) << endl;
	cout << "sizeof true: " << sizeof(true) << endl;
	cout << "sizeof false: " << sizeof(false) << endl;

	return 0;
}

위 예제로 흔히 착각하는 것들을 알 수 있다. 우리는 true와 false가 각각 1과 0을 의미한다고 느낀다. 그러나 위 예제를 실행해보면 “아니다”라는 것을 알 수 있다. true도 1이 아니며 false도 0이 아니고 사이즈 역시 1바이트에 해당한다.

즉, true false가 정의 되기 이전에 1과 0으로 사용을 해왔고 지금도 그렇게 사용하기에 정수의 형태로 형변환하거나 출력할 때는 1과 0으로 변환되도록 정의되어 있을 뿐이다.

int num1 = true;
int num2 = false;
int num3 = true + false;

이 예제의 경우도 변환되어 출력이 될 뿐이다. 즉, 우리는 true와 false를 굳이 숫자와 연결시켜 이해하지말고 참과 거짓을 나타내는 데이터로 알고 있는 것이 더 좋다. 이런 true와 false가 참과 거짓을 의미하는 데이터기 때문에 자료형도 bool로 따로 만들어져 있다. 따라서 다음과 같이 정의도 가능하다.

bool isTrueOne = true;
bool isTrueTwo = false;

그럼 어떻게 사용하는지 예시를 들어보자.

	#include <iostream>

using namespace std;

bool IsPostive(int num) {
	if (num < 0)
		return false;
	else
		return true;
}

int main() {

	bool isPos;
	int num;
	cout << "Input number: ";
	cin >> num;

	isPos = IsPostive(num);
	if (isPos) {
		cout << "Positive number" << endl;
	}
	else
		cout << "Negative number" << endl;

	return 0;
}

Reference


이전 포스팅에서 포인터를 쓸 때 자연스럽게 썼던 참조자(Reference)이다. 보통 이렇게 포인터를 쓰면서 자주 쓰다보니 연결고리가 많기는 하지만, 굳이 포인터를 모르더라도 이해할 수 있는 개념이기에 이해에 있어서 일부러 포인터를 끌어드릴꺼 까지는 없다.

참조자에 대해 이해해 보자.

먼저 변수가 무엇인지 말해보면 할당된 메모리 공간에 붙여진 이름이다. 그리고 그 이름을 통해서 해당 메모리 공간에 접근이 가능하다. 그럼 조금 다르게 생각해보도록 하자. 할당된 한 메모리 공간에 둘 이상의 이름을 부여할 수는 없을까?

이 책을 보면서 느끼는 건 접근이 참 쉽다. 나 역시 참조자를 이런식으로 배워 본적이 없기에 새로운 접근이 참 좋았다. (내가 잘못 배운게 크다.)

일반적으로 변수를 선언할 때,

int num1 = 100;

과 같이 선언을 한다. 그런데 여기서 num2라는 이름을 하나 더 붙여보자.

int num2 = num1;

당연히 오류가 나온다. 그런데 레퍼런스를 추가하면 그렇지 않다.

int &num2 = num1;

물론 다소 혼란스런 선언문이다. 보통 &연산자는 변수의 주소값을 반환하는 연산자이기 때문이다. 그러나 위 예시처럼 전혀 다른 의미로도 사용한다. 이미 선언된 변수의 앞에 이 연산자가 오면 주소 값의 반환을 명령하는 뜻이 되지만, 새로 선언되는 변수 앞에 등장하면 참조자의 선언이 된다.

int *ptr = &num1; int &num2 = num1;

즉 이렇게 되면 num2와 num1이 같은 메모리 공간의 이름이 된다.

그럼 이런 참조자도 변수인가? 에 대한 물음이 나온다. 사실상은 크게 무리는 없다. 실제로 변수와 똑같은 기능과 연산이 가능하다. 그러나 C++에서는 참조자와 변수를 구분하여 이야기 한다. 특히 선언 방식에 있어서 큰 차이가 있는데 참조자는 변수를 대상으로만 선언이 가능하기 때문이다. 그러나 일단 선언이 되면 큰 차이는 없다. (local변수처럼 local참조자가 되는 것도 동일하다.)

굳이, 우리가 느끼기 쉽게 하려면 별명과 같다라고 많이들 얘기 하니 이해하기 쉽게 알아두면 좋다.

재밌는 것은 참조자의 수는 제한이 없고, 참조자의 참조자 역시 가능하다.

선언에 있어서 안되는 것들이 있는데 정리해 보면,

int &ref = 20;
int &ref;
int &ref = NULL;

위 3가지는 불가능한 선언이다. 참조자는 변수에 대해서만 가능하고, 선엄됨가 동시에 누군가를 참조해야 한다. 그리고 참조자를 선언했다가 후에 참조 역시 안되며, 참조의 대상을 바꾸는 것도 불가능 하다. 물론 NULL 초기화 역시 불가능하다.

참조자는 배열의 원소에 대해서는 가능한데(배열이 아니라 배열의 원소) int &num2 = arr[0]; 과 같이 선언하는 것은 가능하다. 또한, 포인터 변수에 대해서도 똑같이 선언 가능하다.

#include <iostream>

using namespace std;

int main() {

	int num = 12;
	int *ptr = &num;
	int **dptr = &ptr;

	int &ref = num;
	int* (&pref) = ptr;
	int ** (&dpref) = dptr;

	cout << ref << endl;
	cout << *pref << endl;
	cout << **dpref << endl;

	return 0;
}

더블포인터라고 무서워 하지 말고 찬찬히 보면 이해가 될 것이다.


참조자와 함수


이제 참조자에 대해 조금 더 깊은 얘기로 들어간다. 혹시, 참조자를 변수에 대해 써본적이 몇번이나 있는지 묻고 싶다.

int num1 = 10; int &num2 = num1;

위와 같은 코드를 나는 실제 코딩하면서 써본 적이 없다. (물론 내 실력탓도 있다.) 여기 책에서도 학습을 위한 코드라고 한다. 참조자의 활용은 함수가 큰 위치를 차지한다.

Call-by-value 와 Call-by-reference는 지난 포스팅에서도 언급을 했었다. C에서도 위 방식을 얘기하는데, Call-by-value는 값을 인자로 전달하는 함수의 호출방식 이고, Call-by-reference는 주소값을 인자로 전달하는 함수의 호출방식이다. 이는 지난번 포스팅에 swap으로 설명을 하였다.

여기서 우리는 Call-by-reference에 대해 좀더 깊이 들어가야 한다. 일부 책에서는 주소 값을 전달하는 Call-by-reference 형태의 함수호출이 Call-by-address라 불리는 경우가 있다. 이것은 앞으로 설명할 C++ 참조자(reference) 기반의 함수호출과 구분을 위함인데, 이 책에서는 그냥 Call-by-reference라고 표현 할 것이라고 한다.

다음 예시를 보자.

int* SimpleFunc(int* ptr){
....
}

자, 위 예시는 Call-by-reference인가? 아니면 Call-by-value인가? 대부분은 Call-by-reference라고 하겠지만 사실 둘다 될 수 있다. 밑의 예제를 보자.

int* SimpleFunc(int* ptr){

return ptr+1;

}

이렇게 한다면 단순히 포인터에 int형 하나의 크기만큼 증가시킨 (배열로 치면 index를 1증가시킨) 값이 출력된다. 이것은 우리가 흔히 생각하는 주소 값을 이용해서 함수 외부에 선언된 변수에 접근하는 Call-by-reference와는 거리가 멀다. 그럼 다음 예제를 보자.

int* SimpleFunc(int* ptr){
	if(ptr==NULL){
		return NULL;
    }
    
    *ptr = 20;
    return ptr;
}

이 함수는 외부 선언된 변수를 참조(reference)했으니 분면 Call-by-reference다. C에서의 Call-by-reference의 의미를 보자.

주소 값을 전달받아서, 함수 외부에 선언된 변수에 접근하는 형태의 함수호출 여기서 중요한 것은 주소 값이 전달되었다가 아니라 주소 값이 참조의 도구로 사용되었다는게 중요하고 이것이 Call-by-value와 Call-by-reference의 차이가 된다.

C++에서는 함수 외부에 선언된 변수의 접근 방법으로 2가지가 존재한다. 하나는 주소 값을 이용한 방식이고, 다른 하나는 참조자를 이용한 방식이다. 그렇다면 참조자를 이용한 방식에 대해 알아보자.

void SwapByRef2(int &ref1, int &ref2){
	int temp = ref1;
    ref1 = ref2;
    ref2 = temp;
}//Call-by-reference

이것을 보고 다음과 같은 질문을 한다면 대단한 것이다. 참조자는 선언과 동시에 변수로 초기화 되어야 하는 것이 아닌가?

정확하다! 그렇지만 파라미터는 함수가 호출되어야 초기화가 진행되는 변수들이다. 따라서 위 예시는 초기화가 이루어지지 않은 것이 아니라 함수 호출시 전달된 인자로 초기화 하겠다는 의미다.

int main(void){
	int val1 = 10;
    int val2 = 20;
    SwapByRef2(val1,val2);
    cout << "val1: " << endl;
    cout << "val2: " << endl;
    return 0;
}

다음과 같이 main을 작성하면 main에서의 val1,val2의 주소와 SwapByRef2에서 ref1,ref2의 주소는 같아진다.

참고로 swapByRef(23,45); 를 main에서 호출하면? 당연히 컴파일 에러가 나온다.


참조자의 문제와 const 참조자


computer

보면 포인터를 잘못 사용할 확률도 낮고, 상대적으로 쉬워서 참조자를 많이 쓸 것 같지만 좋은 점이 있으면 안좋은 점도 있는 법이다.

int num = 25;
HappyFunc(num);
cout << num << endl;

이럴 때, C언어의 관점에서는 무조건 25가 출력이다. 그러나 C++에서는 알수가 없다.

void HappyFunc(int prm){...} void HappyFunc(int $ref){...}

와 같이 참조자로 num을 받아버린다면 값이 달라질 수도 있는 것이다.

즉 이게 참조자의 단점이다. 보기에는 단순해서 크게 단점이라고 느끼지 못할 수 있지만 예를 들어 보겠다.

우리가 누군가의 코드를 받아서 해석하는 단계에 있다고 하자. 그럴 때 함수의 호출문장만 보고도 함수의 특성을 알 수 있는 게 제일 좋다. 그런데 참조자를 사용하면 함수의 원형을 확인해야 하고, 만약 참조자가 파라미터에 있다면 어떤식으로 변형되는지 내부를 살펴야 한다.

그럼 이 단점의 해결은 어떻게 해야 할까? 사실 완벽한 해결은 힘들다고 한다. C는 함수의 호출문장만 보고도 값이 변경되지 않음을 알지만, C++에서는 그게 되지 않는다. 최소한 함수의 원형을 확인해야 하는데, 만일 완벽한 해결을 원하면 참조자 기반의 함수정의를 쓰지 않는 것이 좋다. 그러나 const 키워드를 사용하면 조금은 극복이 가능하다.

void HappyFunc(const int &ref){....}

이 문장의 의미는 무엇일까 참조자 ref의 값이 고정되었다. 즉, 함수 HappyFunc 내에서 참조자 ref를 이용한 값의 변경은 하지 않겠다라는 의미가 된다.

위와 같은 선언을 하면 함수 내에서 변경되지 않음을 함수 선언문으로 알 수가 있다. 따라서 다음 원칙을 되도록 지켜주도록 하자.

함수 내에서, 참조자를 통한 값의 벼경을 진행하지 않을 경우, 참조자를 const로 선언해서, 함수의 원형만 봐도 값의 변경이 이뤄지지 않음을 알 수 있게 한다.

C++ 프로그래머중에 참조자를 사용하지 않는 사람도 많다. 따라서 포인터의 활용도 잘 알아두어야 한다.

다음 포스팅에서 이어 하겠다.

Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

구찌의 개발 블로그 입니다.

Back to Overview