• 4.6.1 C 스타일 문자열
  • 4.6.2 string library header
  • 4.6.3 string 자료형 사용 시 유의사항

4.6.1 C 스타일 문자열

 

이미 C 언어를 학습한 사람이라면 C 스타일의 문자열을 다룬 기억이 있을 것이다. C 스타일 문자열은 char 자료형을 배열로 지니는 형태이며, 문자열이 담길 경우 마지막에 널 문자('\0')가 들어간다.

#include <iostream>

int main()
{
	using namespace std;

	// 커서를 가져다 올리면 13 길이 배열이라고 표시됨.
	cout << "Hello, World" << endl;
	const char my_strs[] = "Hello, World";

	return 0;
}

문자열의 길이를 확인해보면 12가 아닌 13이라는 것을 알 수 있다.

보이는 길이보다 1 길다.


4.6.2 string library header

 

C++에서는 string 라이브러리를 활용하여 문자열을 다룬다. string은 프로그래머의 편의를 위해 만들어진 사용자 정의 자료형으로 볼 수 있다.

#include <iostream>
#include <string>

int main()
{
	using namespace std;

	const string my_hello = "Hello, World";
	//const string my_hello("Hello, World");
	//const string my_hello{ "Hello, World" };

	cout << my_hello << endl;

	return 0;
}

입력받는 방법은 위에 나열된 3가지 모두 가능하다.

 

숫자를 문자열로 취급하여 받을 수도 있는데, string은 정수를 암시적으로 형 변환을 해주진 않는다. 위에서 언급하였듯이 너무 친숙하여 기본 자료형처럼 보이지만 사용자 정의 자료형이기 때문에 암시적 형 변환이 불가능하다.

#include <string>

int main()
{
	using namespace std;

	// ""를 삭제하고 넣으면 에러가 뜬다.
	const string my_ID = "123";

	return 0;
}

 

또한 string 자료형은 + 연산자에 대한 오버로딩이 구현되어 있어서 문자열을 이어주는 것도 쉽게 수행할 수 있다. 이처럼 문자열을 붙여주는 것을 append라고 하며, 프로그래밍을 공부하다 보면 자주 만나게 될 용어이다. string으로 표현하는 문자열과 C 스타일 문자열의 가장 큰 차이점은 바로 문자열의 길이이다. string 문자열은 널 문자를 가지고 있지 않기 때문에 같은 문자열이라도 길이가 C 스타일 문자열보다 1 짧게 된다.

#include <iostream>
#include <string>

int main()
{
	using namespace std;

	string a("Hello, ");
	string b("World");

	string hw = a + b; // append

	// 12 길이(C 스타일과 길이가 1 차이남.)
	cout << hw.length() << endl;

	hw += " I'm good";
	cout << hw << endl;


	return 0;
}

4.6.3 string 자료형 사용 시 유의사항

 

string을 입력 받을 때는 생각했던 대로 작동하지 않는 경우가 종종 있다. 다음의 예시들을 보자. 아래 예시에서 name 부분에 입력할 문자열을 띄어쓰기가 포함된 문자열을 입력해보자.

#include <iostream>
#include <string>

int main()
{
	using namespace std;

	cout << "Your name ? : ";
	string name;
	cin >> name; // 빈칸이 있으면 이미 다받은 것으로 판단.

	cout << "Your age ? : ";
	string age;
	cin >> age;

	cout << name << " " << age << endl;

	return 0;
}

출력도 해주지 않고 끝나버린다.

문자열을 입력할 때 cin은 띄어쓰기를 만나는 순간 입력이 끝난 줄 알고 저장해준 뒤 다음으로 넘어간다. 이를 해결하는 방법은 여러 가지가 있는데, 그중 하나는 바로 getline을 활용하는 것이다.

 

getline은 enter를 치기 전까지 전부 입력을 받아준다. 이를 이용하면 문자열에 띄어쓰기가 있는 것까지 온전히 받아낼 수 있다.

#include <iostream>
#include <string>

int main()
{
	using namespace std;

	cout << "Your name ? : ";
	string name;
	getline(cin, name);

	cout << "Your age ? : ";
	string age;
	getline(cin, age);

	cout << name << " " << age << endl;

	return 0;
}

 

정수를 먼저 받으면 생기는 오작동도 있다. 이번에는 나이를 먼저 받게 구현을 하면 나이를 입력하자마자 바로 프로그램이 종료되는 것을 볼 수가 있다. int 자료형에 저장하는 경우엔 getline을 사용할 수 없게 되면서 cin으로 사용할 수밖에 없게 되고 숫자를 입력 후 enter를 치는 행위가 getline을 통과하는 조건으로 수행되면서 이러한 문제가 생긴다.

#include <iostream>
#include <string>
#include <limits>

int main()
{
	using namespace std;

	cout << "Your age ? : ";
	int age;
	cin >> age;
	
	// 해결법
	//cin.ignore(32767, '\n'); // 2 byte 가장 큰 signed 정수 32767
	//cin.ignore(numeric_limits<streamsize>::max(), '\n');

	cout << "Your name ? : ";
	string name;
	getline(cin, name); // enter칠때까지 입력 받을 수 있음.

	cout << name << " " << age << endl;

	return 0;
}

이를 해결하기 위해 ignore라는 함수를 활용할 수 있다. ignore 함수의 첫 번째 인자가 입력 받는 길이가 되며, 두 번째 인자가 입력받으면 끝낼 문자가 되겠다. 입력받는 수가 얼마나 길지 모르기 때문에 2byte 가장 큰 signed 정수인 32767을 넣어주었다. 32767이라는 남들이 보기에 이해 못 할 수 있는 숫자를 넣는 것은 다른 사람들을 힘들게 하기 때문에 limit 라이브러리에 있는 streamsize max를 이용해도 되겠다.

  • 4.5.1 typeinfo library header
  • 4.5.2 암시적 형 변환(Implicit Type Conversion(coersion))
  • 4.5.3 명시적 형 변환(Explicit Type Conversion(casting))

4.5.1 typeinfo library header

 

형 변환에 대해서 다루기 전에 유용한 라이브러리 하나를 소개하고자 한다.

#include <iostream>
#include <typeinfo>

int main()
{
	using namespace std;

	cout << typeid(0.0).name() << endl;
	cout << typeid(0).name() << endl;

	bool variable = true;
	cout << typeid(variable).name() << endl;

	return 0;
}

typeinfo 라이브러리에 있는 함수 중 하나인 typeid().name()는 변수나 리터럴이 어떤 타입인지 알려주는 함수이다. 형 변환하면서 제대로 형 변환이 되었는지 체크하기 위해 쓰면 유용한 함수이다.


4.5.2 암시적 형 변환(Implicit Type Conversion(coersion))

 

암시적 형 변환은 컴파일러가 알아서 강제로 형 변환을 해주며, 프로그래머가 아무것도 하지 않아도 자동으로 수행하는 것이다. 아래의 예제에서는 프로그램이 실행되지만 다음과 같은 에러 메시지가 뜬다.

#include <iostream>
#include <typeinfo>

int main()
{
	using namespace std;

	int a = 123.0;
    
	cout << typeid(a).name() << endl;

	return 0;
}
warning C4244: 'initializing': conversion from 'double' to 'int', possible loss of data

C4244 에러는 데이터 손실이 있다고 언급을 하지만 프로그램이 실행되도록 둔다. 개발자가 실수로 값을 잘못 넣는 경우에 발생하니 발견하면 해당 변수에 대해 파악을 하고 수정해두는 것이 좋겠다.

 

빌드는 되지만 신경 쓰이는 에러 메시지가 뜬다.

 

암시적 형변환 중 numeric promotion이라 불리는 형 변환이 있다. numeric promotion은 작은 메모리를 가지는 데이터가 큰 메모리에 담기는 경우를 의미한다. 아래의 예시는 float 자료형이 double로 numeric promotion되었다. 우리말로 숫자 승격이라고 쓸 수 있겠다.

#include <iostream>
#include <typeinfo>

int main()
{
	using namespace std;

	float	f = 1.0f;
	double	d = f;

	cout << typeid(f).name() << endl;
	cout << typeid(d).name() << endl;

	return 0;
}

 

또 다른 암시적 형 변환으로 numeric conversion이 존재한다. 숫자 변환이란 의미를 가지는 이 암시적 형 변환은 서로 다른 자료형 간에 변환이나 큰 메모리를 가지는 데이터가 작은 메모리에 담기는 경우를 말한다. 큰 메모리에 담긴 데이터일지라도 작은 메모리가 감당 가능한 범위의 값이라면 컴파일러가 충분히 처리해준다.

#include <iostream>
#include <typeinfo>
#include <iomanip>

int main()
{
	using namespace std;

	float	d = 3;
	short	s = 2;
	
	cout << typeid(d).name() << endl;
	cout << typeid(s).name() << endl;

	int		i = 30000; // short 범위를 넘어가면 문제지만 안넘어가면 괜찮다.
	char		c = i;

	cout << static_cast<int>(c) << endl; // 48

	double	dd = 0.123456789;
	float		ff = dd;

	cout << setprecision(12) << dd << endl;
	cout << setprecision(12) << ff << endl;

	return 0;
}

4.5.3 명시적 형 변환(Explicit Type Conversion(casting))

 

명시적 형 변환은 프로그래머가 변환하겠다는 강력한 의사표현을 하는 것이다. 아래와 같이 C 스타일과 C++ 스타일, 그리고 최근 자주 쓰는 스타일이 있다.

int main()
{
	using namespace std;

	int i = int(4.0);			// c++ style
	i = (int)4.0;			// c style
	i = static_cast<int>(4.0);	// 최신
	
	return 0;
}

'Programming Language > C++' 카테고리의 다른 글

Section 4.7. 열거형 타입 enum  (0) 2021.11.28
Section 4.6. string 문자열 사용  (0) 2021.11.27
Section 4.4. using namespace  (0) 2021.11.13
Section 4.3. 외부 연결  (0) 2021.11.11
Section 4.2. 전역 변수와 정적 변수  (0) 2021.11.10
  • 4.4.1 using namespace
  • 4.4.2 사용 시 유의사항

4.4.1 using namespace

 

using namespace는 많은 C++ 개발자들이 사용하는 키워드이다. 해당 키워드를 사용해서 코드 타이핑할 때 네임스페이스를 쓰는 시간을 절약할 수 있다.

using namespace std;

대표적으로 std를 생략해서 코드를 작성할 수 있겠다.


4.4.2 사용 시 유의사항

 

편하게 하는만큼 사용할 때 주의해야 하는 부분들이 있다. 우선 using namespace를 선언한 순간 해당 선언 부분이 속한 블럭에서는 선언 이후 계속 적용된다는 점이다.

#include <iostream>

int main()
{
	using namespace std;

	cout << "Hello " << endl;

	return 0;
}

이처럼 main 함수 안에서는 std를 안쓰고 사용할 수 있지만 해당 함수 내부에서 선언한 것을 취소할 수 있는 기능은 주어지지 않는다. 따라서 적용시킬 부분만 적용되도록 최대한 사용하는 곳과 가까이 선언하자! 전역 위치에 선언하는건 좋지 않으니 위처럼 지역적으로 묶어서 사용하자. 또한 헤더 파일에 using namespace를 선언해버리면 해당 헤더 파일을 include하는 곳에 전부 영향을 미치니 헤더와 cpp로 분리하고 cpp에 선언해주는 습관을 가지자!

 

using namespace 대신 using만 사용하여 일부 함수들에 대해서만 생략을 적용시킬 수 있다.

#include <iostream>

int main()
{
	using std::cout;
	using std::endl;

	cout << "Hello " << endl;

	return 0;
}

 

아래처럼 namespace가 존재하고 using namespace를 사용하면 어떤 namespace의 my_var 변수를 사용하는지 모호해진다. 이때는 영역 지정 연산자(::)를 사용하여 소속을 확실히 해주자!

#include <iostream>

namespace a
{
	int my_var(10);
	int my_a(123);
}

namespace b
{
	int my_var(20);
	int my_b(456);
}

int main()
{
	using std::cout;
	using std::endl;

	using namespace a;
	using namespace b;

	cout << my_var << endl; // a와 b에 둘다 있으니 모호해짐
	cout << a::my_var << endl; // 영역지정 연산자를 통해 해결 가능

	return 0;
}

 

최대한 잘게 쪼개서 작은 영역에 using namespace가 영향력을 행사하도록 작성하는 것도 하나의 방법이다.

// using문과 모호성

#include <iostream>

namespace a
{
	int my_var(10);
	int my_a(123);
}

namespace b
{
	int my_var(20);
	int my_b(456);
}


int main()
{
	using std::cout;
	using std::endl;

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

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

	return 0;
}
  • 4.3.1 함수에 대한 외부 연결(External Linkage)
  • 4.3.2 변수에 대한 외부 연결
  • 4.3.3 전역 상수에 대한 외부 연결

4.3.1 함수에 대한 외부 연결(External Linkage)

 

일반적으로 다른 cpp 파일에 있는 것을 끌어다가 쓰려면 #include 전처리기를 통해 해당 cpp 파일을 끌어다가 쓸 수 있다. 하지만 cpp 파일을 직접 include하는 것은 권장하지 않는다고 한다. 마치 어릴 때 먹는 불량식품 느낌으로 먹어도 되는데 안 먹었으면 한다~ 정도의 느낌으로 받아들이면 되겠다.

 

cpp 파일을 include할 수 있지만 클래스를 구성하게 되면 헤더에 선언을 전부 올려놓고 cpp에 정의를 하기 때문에 그냥 헤더 파일을 include하면 될 것이다. 정말 특수한 경우라서 cpp 파일 내부의 함수가 필요하다면 전방 선언을 통해 해당 함수를 끌어다가 쓸 수 있겠다.

// Extern c++ file
#include <iostream>

void doSomething()
{
	using namespace std;

	cout << "Hello" << endl;
}
// main 파일

// extern이 생략되어 있다.
void doSomething();

int main()
{
	doSomething();

	return 0;
}

전방 선언을 해서 외부 cpp 파일의 함수를 끌어다가 쓰는 예시가 위의 코드로 볼 수 있다. 전방 선언을 통해 main 함수에서 외부 파일의 함수가 호출되면 연결되어 사용할 수 있게 된다. 전방 선언 앞에는 extern 키워드가 숨어 있으며, 이 키워드를 통해 외부 파일의 함수를 끌어 쓸 수 있다.


4.3.2 변수에 대한 외부 연결

 

extern 키워드를 활용하면 외부에 있는 변수도 끌어 쓸 수 있다. 위와 비슷하게 한 번 설정하고 사용해보자!

// Extern c++ file

extern int exA;
// main file
#include <iostream>

using namespace std;


extern int exA;

int main()
{
	cout << exA << endl;

	return 0;
}

위의 코드를 실행하면 다음과 같은 에러가 뜬다.

LNK2001 and LNK1120

링커 에러라고 불리는 LNK2001과 LNK1120이다.

error LNK2001: unresolved external symbol "int exA"
fatal error LNK1120: 1 unresolved externals

해당 에러가 발생한 이유는 선언만 되어 있고 exA가 메모리에 할당이 안되어 있기 때문이다. 사용하기 위해서 외부 cpp 파일에 해당 변수를 초기화하고 사용하자! 아래처럼 수정하고 사용하면 된다.

// Extern c++ file

extern int exA = 123;

 

만약 양쪽 파일에 전부 exA를 초기화시켜주면 어떻게 될까? 이번에도 두개의 에러 메시지를 만날 수 있다. 바로 LNK2005와 LNK1169이다.

error LNK2005: "int exA" (?exA@@3HA) already defined in Source.obj
fatal error LNK1169: one or more multiply defined symbols found

위 에러를 해결하기 위해서는 중복된 초기화를 삭제해주면 되겠다!


4.3.3 전역 상수에 대한 외부 연결

 

전역 상수에 대한 외부 연결을 다뤄보자. 우선은 하나의 헤더 파일에 프로그램에서 쓸 상수를 모아둔다.

// My_const.h
#pragma once

namespace Constants
{
	const double pi(3.141592);
	const double gravity(9.8);
}

여기서 만들어진 상수를 test.cpp에서 사용하는 함수로 구현하였다. 상수에 대한 주소도 같이 보기 위하여 주소도 출력한다.

// test.cpp
#include <iostream>
#include "My_const.h"

void doSomething()
{
	using namespace std;

	cout << "Hello" << endl;

	cout << "In 4_2_1.cpp file" << Constants::pi << " " << &Constants::pi << endl;
}

이제 위의 상수가 담긴 헤더 파일과 test.cpp 파일을 include하여 사용하는 파일을 만들고 이 파일에서도 상수를 끌어와서 사용도 하고 test.cpp의 함수를 사용해서 써보기도 한다.

// main.cpp
#include <iostream>
#include "My_const.h"

using namespace std;

void doSomething();

int main()
{
	doSomething();

	// pi가 중복되게 존재한다. 메모리 낭비
	cout << "In main.cpp file" << Constants::pi << " " << &Constants::pi << endl;
    
	return 0;
}

총 3개의 파일을 만들고서 주소를 출력하면 pi의 주소가 다름을 확인할 수 있다. 이게 의미하는 것은 만약 해당 상수를 수십만번 사용할 경우 수십만번의 메모리 할당이 이루어지고 낭비가 생긴다는 것이다.

 

상수를 초기화해준 파일을 헤더와 cpp 파일로 분할하고 상수 앞에 extern 키워드를 붙이면 메모리 할당을 계속하지 않고 단 한번 수행하며, 해당 메모리에 접근하여 상수를 가져다가 쓰게 된다.

namespace Constants
{
	extern const double pi(3.141592);
	extern const double gravity(9.8);
}

+ Recent posts