• 2.1.1 기본 자료형을 바라보는 관점
  • 2.1.2 Modern C++
  • 2.1.3 다양한 데이터 초기화 방법

2.1.1 기본 자료형을 바라보는 관점

 

C++에서 다루는 기본 자료형은 많이 있지만 모든 자료형을 완벽하게 외우려고 하는 것보다 사용하면서 체화하는게 프로그래밍 공부에서 덜 스트레스 받는 방법일 것이다. 나도 여전히 실무에서 기본적인 것에 대해 검색을 해보곤 한다. 기본 자료형의 종류를 암기하는 것보다 중요한 것은 자료형을 여러가지 종류로 두고 쓰는 이유가 바로 메모리 할당 크기에 있다는 점이다. 또한 자료형의 이름이 같을지라도 컴파일러마다 자료형의 메모리 크기는 다를 수 있으니 주의하자.

 

메모리 크기를 다룰 때 사용하는 것이 바로 바이트(byte)와 비트(bit)이다. 1 byte = 8 bit임을 기억해두자!

 

#include <iostream>
#include <bitset>

int main()
{
	using namespace std;

	int i = -1;


	char a = 'H';

	cout << (uintptr_t)static_cast<void*>(&i) << endl;
	cout << (uintptr_t)static_cast<void*>(&a) << endl;

	return 0;
}

Integer types 중에서 가장 대표적인 int 자료형은 4 bytes의 크기를 가지고 있다.

 

문자를 저장하는 char 자료형은 character type이기도 하고 Integer type이기도 하다. char 자료형에 'A'로 초기화하는 것과 65로 초기화하는 것은 'A'로 초기화하는 효과를 가져온다. 즉, 문자이면서 숫자가 된다. 이렇게 말하게 되는 이유가 네트워크 영역에서는 char 자료형을 통해 1 byte 단위의 숫자를 다루기 때문이다.

 

#include <iostream>
#include <bitset>

int main()
{
	using namespace std;

	signed int i1	= -1;
	unsigned int ui1 = 1;
	unsigned ui2 = 2;

	// Floating-point types
	float	f1 = 1.0f;	// 4 bytes
	double	d1 = 1.0;	// 8 bytes
	long double ld1 = 1.0;

	return 0;
}

자료형 앞에 signed 혹은 unsigned를 붙여서 쓸 수 있다. 부호도 취급하는 자료형인지 아닌지에 따라 각각 사용하게 되는데, 특수한 상황에서는 연산 속도마저 차이가 날 수 있다. 일반적인 자료형을 선언할 때는 signed가 붙어 있다고 생각을 하면 된다.

 

Float-point types(부동소수점 타입)는 우리가 익히 알고 있는 실수를 표현하기 위해 존재한다. float, double, long double이 있는데, 과학 계산을 사용하지 않는 경우에는 대부분 float 자료형으로 사용한다. 마찬가지로 메모리 크기의 차이가 있다는 점을 항상 생각해두자! 과학 계산에서 double 자료형을 쓰는 이유는 정확도를 위해서 쓰는 것으로 보면 된다. 사용하는 메모리의 크기가 클수록 더 낮은 소수점 계산까지 정확해진다. 하지만 과학 계산에서도 소수점 아래 열번째 자리를 넘어가는 수에 대해서는 truncation error 때문에 신뢰하지 않는 편이다.

 


2.1.2 Modern C++

 

조금 생소할 수도 있는 표현 중에 하나가 바로 자료형 위치에 auto를 두는 것이다.

auto

auto를 사용하면 컴파일 단계에서 컴파일러가 해당 변수를 알아서 알맞은 자료형으로 바꿔준다. 예시를 간단하게 두었지만 auto를 사용하여 r-value에 상수가 아닌 것을 넣어 변수를 만들어낼 수 있으니 참고하자!


2.1.3 다양한 데이터 초기화 방법

 

C++에서는 변수 초기화하는 방법이 3가지가 있다.

int a1 = 123;
int a2(314);
int a3{ 123 };

우리가 흔히 사용하는 초기화 방법인 첫번째 방법을 copy initialization이라고 부른다. 위의 예시에서 만약 r-value에 int가 아닌 부동소수점 변수를 둘 경우 자동으로 타입 캐스팅을 한다.

 

두번째 예시의 초기화는 direct initialization이며 객체 지향 프로그래밍을 할 때 많이 사용한다. 앞선 예시와 마찬가지로 컴파일러가 자동으로 타입 캐스팅 해준다.


마지막 초기화는 uniform initialization이며 이 또한 객체 지향 프로그래밍 때 많이 사용한다. 최근 권장하는 초기화 방법이라고 하며 앞선 예시들보다 조금 더 엄격하다. 만약 잘못된 r-value를 넣어주면 컴파일 단계에서 에러를 출력한다.

 

C2397

C2397 conversion from 'double' to 'int' requires a narrowing conversion이라는 말과 함께 컴파일러가 에러를 보여준다.

 

프로그램을 만들 때 언제나 휴먼 에러를 생각해야하기 때문에 오해의 소지가 될 법한 방식의 코딩은 피해야 한다.

초기화

위처럼 작성하면 마치 k와 l도 123으로 초기화될 것 같아 보이지만 실제로는 그렇게 동작하지 않는다.

 

예전 코드 작성 방법 중에 하나가 사용할 모든 변수를 프로그램 가장 앞에 모두 선언하는 방식인데, 요즘은 사용하는 곳에서 선언하고 쓰는 방식을 채택하니 참고해두는 것이 좋다. 쓰는 곳에서 선언하는 방식으로 구현해야 리팩토링할 때 편하고 다루기 쉬워진다.

  • 1.10.1 전처리기 include & define
  • 1.10.2 전처리기 ifdef & endif

1.10.1 전처리기 include & define


전처리기 중에 가장 익숙한 전처리기가 아닐까 싶다.

#include 전처리기를 사용하면 프로그램에 필요한 라이브러리를 끌어다가 쓸 수 있다. 가장 처음 쓰게 되는 라이브러리는 아마도 #include <iostream>인 것 같다.

#include <iostream> 
using namespace std; 
#define MY_NUMBER 333 
#define MAX(a, b) (((a)>(b)) ? (a) : (b)) 
#include <algorithm> 
#define LIKE_APPLE


#define을 통해 코드를 좀더 깔끔하게 작성할 수 있다. 매크로(marco)라는 명칭으로도 부르는 이 전처리기는 코드 내에 해당 매크로가 존재하는 경우 정의한 데이터로 컴파일 단계에서 교체를 해준다. 이를 통해 과거에는 많은 사람들이 max 함수를 만들어서 쓰곤 했는데, 지나친 괄호를 작성해야 올바르게 동작하기 떄문에 요즘에는 쓰질 않는다. 처음에는 공부하는 차원에서 해볼 수 있지만 max 함수 자체는 algorithm 라이브러리에 존재하니 끌어다가 쓰면 되겠다.

#define을 통해 교체하는 작업을 하지 않는 경우가 있는데, 바로 데이터를 작성하지 않고서 #define NAME만으로 작성된 매크로이다. 이전에도 사용하긴 했지만 #ifdef를 활용하여 코드를 작성할 때 사용하게 된다. 또한 해당 매크로는 작성된 파일 내에서만 영향력을 가지며, 그 영역을 벗어나서 동작하는 함수가 있다면 매크로가 없는 것으로 간주되니 주의하자. 이 부분은 뒤에 예시로 한번 보도록 하겠다!

참고로 매크로는 항상 대문자로만 구성해야하는 프로그래머의 관습이 존재한다.


1.10.2 전처리기 ifdef & endif

// 다른 코드에 정의되어 있는 함수 
void doSomething();


int main()
{
	cout << MY_NUMBER << endl; cout << std::max(1, 2) << endl;
#ifdef LIKE_APPLE 
	cout << "Apple " << endl; 
#endif 

#ifndef LIKE_APPLE 
	cout << "Orange " << endl; 
#endif 

#ifdef LIKE_APPLE 
	cout << "Apple " << endl; 
#else 
	cout << "Orange " << endl; 
#endif 
	
	doSomething(); 

	return 0;
}


먼저 작성된 코드와 함께 붙여서 쓰면 되는 코드이다. #ifdef LIKE_APPLE는 LIKE_APPLE이 정의되어 있다면 #endif까지 동작하게 해주는 전처리기이다. #ifndef는 반대로 정의되어 있지 않은 경우에 동작하게 된다. 위처럼 ifdef와 ifndef로 분할해서 만들수도 있지만, ifdef와 else를 활용하여 작성할 수도 있다. 둘 다 똑같이 작동하게 될 것이다.

앞에서 LIKE_APPLE을 정의했으니 Apple이 출력되도록 코드가 작동할 것이다. 실제 화면을 보면 Orange 파트의 코드는 희미하게 표기가 된다.

LIKE_APPLE이 정의된 경우

doSomething 함수는 다음과 같다.

doSomething

다른 파일에 작성된 함수이고 전방 선언을 통해 가져오도록 한다. 여기서는 Orange가 출력 될 것으로 보이는데, 앞의 코드들을 합치면 이미 LIKE_APPLE이 정의되어 있으니 doSomething도 Apple을 출력해야 할 것 같다는 생각이 든다.

result

실제 결과를 보면 doSomething에 의해 Orange가 출력되었다. doSomething 함수를 호출하는 곳에는 LIKE_APPLE이 정의되어 있어서 Apple을 출력시켰지만 doSomething 함수를 실행하기 위해 건너가는 파일에는 정의가 되어 있지 않아서 Orange가 출력된다. 이처럼 매크로의 정의는 해당 파일에만 한정되어 있으니 주의해야하며, 만약 Apple을 출력시키고 싶다면 해당 매크로를 또 작성하거나 매크로가 작성된 파일을 include로 끌어오면 되겠다.

  • 1.9.1 네임스페이스(namespace)
  • 1.9.2 using namespace

1.9.1 네임스페이스(namespace)

 

우리말로는 명칭 공간이라고도 하는 개념이 C++에 있다. 프로그램을 만들다 보면 다른 동작을 하면서 같은 이름의 함수를 많이 쓰게 되는데, 이때 컴파일러가 어떤 함수인지 명확히 선택할 수 있도록 구분해줘야 한다. 이를 위해 사용하는 것이 바로 네임스페이스이다.

 

#include <iostream>

namespace MySpace1
{
	// 네임스페이스 안에 네임스페이스 가능
	namespace InnerSpace
	{
		int myFunction()
		{
			return 0;
		}
	}
	int doSomething(int a, int b)
	{
		return a + b;
	}
}

namespace MySpace2
{

	int doSomething(int a, int b)
	{
		return a * b;
	}
}


using namespace std;

int main()
{
	
	// 3*4가 실행된다.
	cout << MySpace2::doSomething(3, 4) << endl;

	//앞으로 MySpace1이라는 namespace를 사용할 것이다.
	using namespace MySpace1;

	// 3+4가 실행된다.
	cout << doSomething(3, 4) << endl;
	InnerSpace::myFunction();

	return 0;
}

 

코드를 보면 같은 이름의 함수가 존재하지만 빌드가 잘되는 것을 볼 수가 있다. 바로 네임스페이스 개념을 사용하였기 때문이다. 네임스페이스 안에 함수를 정의하면 해당 함수를 사용할 때는 네임스페이스를 앞에 작성해야 하고 이를 통해 컴파일러가 어떤 함수인지 혼동하지 않고 바로 찾을 수가 있다.

 

이러한 방식이 왜 필요한지 독학을 하는 경우엔 이해가 되지 않을 수 있는데, 실무에 가면 협업하는 사람들이나 선배님들이 작성한 코드에서 함수의 이름을 잘 나타내기 위해 최대한 보편적인 언어로 작성을 한다. 그러한 방식을 사용하다 보니 서로가 같은 이름의 함수를 작성하는 경우가 생기기도 한다. 실제로 현 직장에서 다루는 프로그램의 코드를 보면 configure 함수가 여러 개 존재하는 것을 볼 수가 있다.


1.9.2 using namespace

 

아마 많은 C++ 개발자들이 기초 교육을 받을 때 많이 써본적이 있을 것이다. using namespace std;라는 명령문은 정말 친숙하게 사용했을 것이라 생각한다. using namespace를 통해 해당 영역을 벗어나지 않는다면 계속 언급한 네임스페이스를 사용할테니 네임스페이스를 제외하고 함수를 호출해도 알아서 찾아달라는 의미이다.

 

앞의 예시에서는 std를 그런식으로 쓰고 MySpace1도 사용하였다. 이처럼 네임스페이스를 구현하고 사용하는 방법이 다채로운데, 처음 접한다면 시간을 투자해서 이것저것 실험을 해보는 것이 좋겠다.

  • 1.8.1 링킹 에러
  • 1.8.2 중복 정의
  • 1.8.3 헤더 가드(Header Guards)

1.8.1 링킹 에러

 

Linking Error

프로그램을 만들다 보면 LNK2019, LNK1120이라는 링킹 에러를 만나는 경우가 종종 생긴다. 위와 같은 에러가 생기는 이유는 특정 함수에 대하여 선언은 존재하지만 정의가 존재하지 않는 경우이다. 

 

add function header

위처럼 add 함수에 대한 선언을 헤더 파일로 작성하고서 include를 통해 main 함수가 작성된 코드에서 add 함수를 사용했다면 전방 선언이 된 효과를 가지게 된다. 선언이 되어 있으므로 컴파일러는 통과를 시키고 빌드가 이루어지는데, 결국 해당 함수의 정의가 담긴 cpp 파일이 없다면 함수 호출 뒤에 어떤 연산을 해야 할지 모르는 상황이 발생하면서 링킹 에러가 생긴다. 따라서 링킹 에러가 생긴다면 혹시나 정의를 빼먹은 함수가 없는지 확인해보자!


1.8.2 중복 정의

 

위와는 다르게 과도한 정의로 인한 에러가 생기는 경우가 있다.

function already has a body

이처럼 C2084 에러가 잡히면 Description 의미 그대로 이미 중복 선언이 된 경우이다. 일반적으로 많은 함수를 선언과 정의를 해서 헤더 파일로 가지고 있다면 여기저기서 include를 통해 함수를 재사용하고 있을 것이다. 다음의 예시를 보자.

 

add header
dosomething header

add 함수가 있는 헤더 파일을 만들고 add 함수를 끌어다 쓰는 doSomething 함수를 만들어 다른 헤더 파일에 만들었다.

 

main 함수

그리고 main 함수가 담긴 cpp 파일이 위처럼 작성되어 있다. 여기서 #include 전처리기가 작동하는 방식을 이해하면 왜 중복 정의인지 알 수가 있는데, #include 전처리기는 해당 파일을 복사해서 가지고 오는 것처럼 작동한다. 따라서 위의 코드는 아래와 같은 상황이 벌어진다.

 

add 중복


1.8.3 헤더 가드(Header Guards)

 

아마 헤더 파일을 만드는 순간 #pragma once라는 전처리기를 보았을 것이다. 해당 전처리기는 이미 한번 정의된 함수라면 다시 정의하지 않도록 해주는 전처리기로 프로그램이 커질수록 해당 전처리기의 도움을 많이 받는다. 같은 방식으로 동작하는 것이 아래의 코드가 되겠다.

 

#ifndef  MY_ADD
#define  MY_ADD

int add(int a, int b)
{
	return a + b;
}

#endif // ! MY_ADD

 

다음과 같이 코드가 작성 되어 있다면 가장 앞에 #pragma once 전처리기를 둔 것과 같은 효과를 지닌다. #ifndef MY_ADD를 통해 만약 MY_ADD가 정의되어 있지 않다면 #endif 바로 위까지 코드를 읽어드리는 것이다. 이런 경우 #define MY_ADD가 최상단에서 정의가 되고 add 함수를 정의하기 때문에 추후 해당 헤더 파일을 다시 include하더라도 MY_ADD가 정의되어 있기 때문에 다시 add 함수를 정의하는 불상사는 발생하지 않는다.

+ Recent posts