내가 아는 익숙한 for문과는 for문을 보았기에 이를 기록하려 한다...!


일반적인 for문

 

남들에게는 일반적인 게 아닐 수 있지만 내가 프로그래밍을 하면서 보았던 코드들에서 대부분 for문은 다음과 같은 형태로 구현되어 있었다.

#include <iostream>

using namespace std;

int main()
{
	int arrayTest[10] = { 1,2,3,4,5,6,7,8,9,10 };

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

	return 0;
}

정말 교과서에 나오는 정석적인 for문이다...! 이렇게 생긴 코드를 대부분의 C/C++ 개발자들이 제일 처음 본 반복문이라고 나는 생각한다.


auto 키워드를 활용한 for문

 

주말에 프로그래밍 공부를 조금씩 하다가 발견한 for문이다. 정말 너무 간결하고 nice해서 충격을 먹고 포스팅을 한다.

#include <iostream>

using namespace std;

int main()
{
	int arrayTest[10] = { 1,2,3,4,5,6,7,8,9,10 };

	for (auto element : arrayTest)
	{
		cout << element << endl;
	}

	return 0;
}

for문 첫 줄이 엄청나게 간결해졌다...!! 기존의 for문과 한번 비교 분석을 해보자!

 

우선 첫 번째로 당연히 코드가 간결해지고 코드 작성 시간이 줄어들게 되었다. 이전 코드는 은근히 코드를 작성하면서 기호를 추가하는 경우 끊김이 생기곤 했는데 위처럼 작성하는 경우엔 콜론 하나만 있으니 금방 타이핑을 할 수가 있었다. 두 번째로는 주어진 배열의 길이를 자동으로 for문이 가지게 된다는 점이다. for문을 작성할 때 항상 고민이었던 점은 배열을 사용하는 경우 for문에 해당 배열의 끝 지점이 어디인지 반드시 알고 있어야 했고 이를 잘못 지키는 경우 코드가 제대로 동작하지 않았다. 그래서 항상 어떻게 배열의 길이를 전달해줄까 고민하곤 했는데 이제 이런 고민을 할 필요는 없어졌다.

 

당연히 장점만 있는 것은 아닐 것이다. 바로 직전의 코드처럼 작성한다면 element에 매번 배열의 값이 복사된다. 이는 성능 하락의 주범이 될 수도 있겠다. 언제나 복사는 덜할수록 좋은 것 같다. 그리고 아직 검증은 안 해봤지만 저러한 작성 방식도 openMP가 적용되는지 의문이다. 이건 내일 회사 가서 한번 적용해서 돌려봐야겠다.

 

참고로 복사를 막는 방법으로 구현한 것은 아래와 같다. 참조 연산자를 사용하면 매번 복사하지 않고 수행시킬 수 있게 된다!

#include <iostream>

using namespace std;

int main()
{
	int arrayTest[10] = { 1,2,3,4,5,6,7,8,9,10 };

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

	return 0;
}

오랜만에 최신 문물을 접한 느낌으로 충격을 받았다. 기존과 성능 차이가 꽤 있을지는 모르겠지만 적어도 코드가 간결해질 수 있어서 너무 좋은 것 같다. 혹시나 추가 설명이 필요하다면 아래 글을 참고하자!

 

 

Range-based for loop (since C++11) - cppreference.com

Executes a for loop over a range. Used as a more readable equivalent to the traditional for loop operating over a range of values, such as all elements in a container. [edit] Syntax attr(optional) for ( init-statement(optional)range-declaration : range-exp

en.cppreference.com

 

  • 2.7.1 Literal Constants
  • 2.7.2 Symbolic Constants
  • 2.7.3 매크로와 상수의 차이점
  • 2.7.4 상수를 사용하여 퇴근을 빨리 하는 방법
  • 2.7.5 const 키워드가 매개변수에 있는 경우

2.7.1 Literal Constants

 

상수는 변하지 않는 수를 의미하며 그중 하나가 리터럴 상수이다. 리터럴 상수에 대한 개념을 처음에는 받아들이기 힘들 수 있는데, 변수처럼 메모리에 이름이 지어져서 할당되는 것이 아니며, 메모리 공간을 가리키는 이름을 가지지 않은 수라 생각하면 좋을 것 같다.

 

일반적으로 우리가 변수에 값을 대입할 때 사용하는 숫자 및 문자가 리터럴 상수가 되겠다. C++ 14에서 Binary Literals도 추가가 되었다.

#include <iostream>

int main()
{
	using namespace std;

	// 숫자나 글자로 표기되는 r-value = literals
	float pi = 3.14f;

	unsigned int n = 5u;
	long n2 = 5L;

	// Decimal	: 0 1 2 3 4 5 6 7 8 9 10
	// Octal	: 0 1 2 3 4 5 6 7 10
	// Hexa		: 0 1 2 3 4 5 6 7 8 9 A B C D E F 10

	int x = 0xF;
	cout << x << endl;
	x = 012;
	cout << x << endl;
	x = 0b1010;
	cout << x << endl;
	x = 0b1011'1111'1010; // 컴파일러는 '를 무시한다. 10진수에도 사용가능하다.
	cout << x << endl;

	return 0;
}

위의 코드에서는 대입 연산자(=) 오른쪽에 있는 것들이 모두 리터럴 상수이다. 바이너리 리터럴 상수를 보면 숫자들 사이에 문자가 들어간 것을 볼 수가 있는데 바이너리 리티럴 상수를 4bit 단위로 끊어서 볼 수 있도록 생겨난 표기법이다. 컴파일러는 '를 무시하므로 써도되고 안 써도 된다. 십진수에서도 사용할 수 있다.

 

숫자 앞에 여러 표현을 활용하면 프로그래밍에서 자주 쓰이는 진수 표현을 쓸 수 있다.

0x : 16진수
0  :  8진수
0b : 2진수

2.7.2 Symbolic Constants

 

심볼릭 상수란 const 키워드와 constexpr 키워드를 통해서 명시적으로 데이터를 상수화시키는 것이다. 심볼릭 상수를 만들면 추후에 해당 상수의 값을 변경하려 할 때 컴파일러가 막아준다. 즉, 우리가 실수할 수도 있는 부분을 컴파일러가 서포트해주는 것이다. constexpr은 C++ 11에서 생긴 것으로 완벽하게 컴파일타임에서 상수로 결정 난다는 것을 키워드로 표현한다.

#include <iostream>

using namespace std;

int main()
{
	const double gravity = 9.8;
	//double const gravity = 9.8; // 이처럼 선언해도 가능하다.

	int number;
	cin >> number;
	const int special_number(number); // 사용자가 입력하는 심볼릭 상수

	constexpr int price_per_item = 123;
    
    return 0;
}

컴파일타임에서 상수임이 결정 나는 것을 컴파일타임 상수라고 칭하며, 런타임에서 결정 나는 경우 런타임 상수라고 말한다. 런타임 상수의 경우 사용자가 입력하는 심볼릭 상수가 된다. 심볼릭 상수는 상수이기 때문에 선언과 동시에 초기화를 해야 한다.


2.7.3 매크로와 상수의 차이점

 

C언어를 먼저 배우는 사람의 경우 매크로를 상수처럼 취급하여 쓰는 경우가 많다. 매크로를 활용하면 컴파일 단계에서 매크로로 만든 리터럴 상수를 치환하여 입력해주기 때문에 편하다. 하지만 매크로를 C++에서는 잘 안 쓰게 되는데 그 이유는 바로 적용 범위가 너무 넓기 때문이다. 매크로가 존재하는 파일을 include하는 경우 그 파일에도 해당 매크로가 적용되기 때문에 객체 지향 언어로 넘어간 C++에서 보는 매크로가 전역 변수처럼 작동하는 게 좋지 않다고 보는 것이다.

#include <iostream>
#define PRICE_PER_ITEM 30 // c에서 많이 쓰지 c++에서는 잘 안쓴다. why? 적용범위가 너무 넓다.

using namespace std;

int main()
{
	constexpr int price_per_item = 123; // 매크로보다 const가 더 좋다.

	return 0;
}

2.7.4 상수를 사용하여 퇴근을 빨리 하는 방법

 

과학 계산을 수행하는 곳에서 업무를 수행하다 보면 많은 상수를 사용하게 되는데, 이를 한 곳에서 모아서 심볼릭 상수로 잡아두면 관리가 편해진다.

 

상수를 모아둔 헤더 파일

 

위처럼 상수를 한 곳에 모아서 두고 상수를 써야 하는 곳에서 include를 해준다.

#include <iostream>
#include "MY_CONSTANTS.h"

using namespace std;

int main()
{
	double radius;
	cin >> radius;
	double circumference = 2.0 * radius * constants::pi;

	return 0;
}

헤더에서 사용한 namespace와 함께 파이를 사용한 코드이다.


2.7.5 const 키워드가 매개변수에 있는 경우

 

함수가 선언된 코드들을 보면 간혹 매개변수에 const 키워드가 붙어 있는 경우를 볼 수가 있다. 매개변수에 const 키워드가 붙어 있는 이유는 해당 함수에서 인자로 들어온 매개변수를 함수 내에서 변경하면 안 된다는 제약 조건을 컴파일 수준에서 정하기 위함이다.

#include <iostream>

using namespace std;

void printNumber(const int my_number)
{
	cout << my_number << endl;
}

int main()
{
	printNumber(123);

	return 0;
}

이처럼 코드를 작성하면 printNumber 함수 안에서 my_number를 수정하게 될 때 컴파일 에러가 뜨게 된다.

  • 2.6.1 Char type
  • 2.6.2 limits 헤더를 이용한 char 한계치 확인
  • 2.6.3 입력이 문자열이고 받는 메모리가 문자인 경우 버퍼의 작동 방식
  • 2.6.4 강제 형 변환

2.6.1 Char type

 

Char type은 문자를 다루는 자료형이다. 문자의 경우 ASCII CODE로 대응시켜서 다루게 되며, 이 때문에 char 자료형에 숫자를 저장해서 출력하는 경우 숫자가 아닌 문자가 나오게 된다. 출력 시 문자가 나오지만 숫자로 다룰 수 있는 특징이 있다. 문자를 대입하는 경우 홑따옴표(' ')를 사용하게 된다.

#include <iostream>

int main()
{
	using namespace std;

	char c1(65);
	char c2 = 'A';
    
	cout << c1 << " " << c2 << " " << int(c1) << " " << int(c2) << endl;

	return 0;
}

위의 코드를 실행하게 되면 둘 다 문자로는 A가 출력되고 숫자로는 65가 출력되는 것을 확인할 수 있다.


2.6.2 limits 헤더를 이용한 char 한계치 확인

 

아래의 코드를 통해 char 자료형의 한계치를 확인할 수 있다.

#include <iostream>
#include <limits>

int main()
{
	using namespace std;

	cout << sizeof(char) << endl;
	cout << (int)numeric_limits<char>::max() << endl;
	cout << (int)numeric_limits<char>::lowest() << endl;

	return 0;
}

char 자료형은 1byte의 크기를 가지며 양수는 127, 음수로는 -128까지 표현이 가능하다. 이러한 특징 덕분에 네트워크 통신 분야에서는 이를 작은 크기의 정수로 다루어서 사용하기도 한다.


2.6.3 입력이 문자열이고 받는 메모리가 문자인 경우 버퍼의 작동 방식

 

앞에서도 언급하였듯이 문자로 할당된 메모리는 하나의 값만 받는다. 하지만 사용자가 입력을 문자가 아닌 문자열로 입력하는 실수를 저지르면 어떻게 작동이 될까?

#include <iostream>

int main()
{
	using namespace std;

	char c1(65);

	cin >> c1;
	cout << c1 << " " << static_cast<int>(c1) << endl;

	return 0;
}

c1의 초기화는 65로 A가 저장되어 있다. 여기에 cin을 통한 사용자 입력을 받기로 한다. 하지만 사용자가 실수로 qwe를 입력하였다. 결과는 아래와 같다.

 

q만 받는다.

 

qwe가 입력되었지만 q만 c1에 저장해서 출력하는 것으로 프로그램이 끝나게 된다. 그럼 we는 자동으로 버려지도록 동작할까? 이번에는 위의 코드에서 입력을 두 번 더 받을 수 있도록 작성해서 코드를 작성하고 똑같이 qwe를 입력한다.

#include <iostream>

int main()
{
	using namespace std;

	char c1(65);

	cin >> c1;
	cout << c1 << " " << static_cast<int>(c1) << endl;

	cin >> c1;
	cout << c1 << " " << static_cast<int>(c1) << endl;

	cin >> c1;
	cout << c1 << " " << static_cast<int>(c1) << endl;

	return 0;
}

실행 결과는 아래와 같다.

 

qwe를 하나씩 출력한다.

 

이처럼 stream을 사용하게 되었을 때 문자열을 길게 나열해서 넣으면 받는 메모리 길이에 따라 자동으로 받고 남는 부분을 버퍼에 남기도록 동작하게 된다.


2.6.4 강제 형 변환

 

형 변환으로 여러 가지가 존재한다. 프로그래밍하면서 자주 본건 c-style casting이었다. cpp-style casting을 직접 써보진 않았지만 지금 보면서 생각해보면 아래처럼 제대로 감싸서 형 변환시키는 것이 안전할 거란 생각이 든다. static_cast도 존재하는데 이는 나중에 추가적으로 설명할 예정이다.

#include <iostream>
#include <limits>

int main()
{
	using namespace std;

	// c-style casting(강제 변환)
	cout << (char)65 << endl;
	cout << (int)'A' << endl;

	//cpp-style casting(강제 변환)
	cout << char(65) << endl;
	cout << int('A') << endl;

	// 추후 설명(기본형에 대한 변환을 컴파일러에게 미리 알림)
	cout << static_cast<char>(65) << endl;
	cout << static_cast<int>('A') << endl;

	// 위의 3개는 같은 결과를 보인다.


	//static_cast를 사용한다고해서 기존에 있는 것이 변하는 것은 아님
	char ch(97);
	cout << ch << endl;
	cout << static_cast<int>(ch) << endl;
	cout << ch << endl;

	return 0;
}

+ Recent posts