CPP Module 06 (1)

“CPP Module 06에 대하여”

C++ casts

reinterpret cast를 다시 쓸 날이 올까?

ex00 Conversion of scalar types

** Scalar-Type이란 하나의 값만을 가지고 있는 value라고 볼 수 있다. 흔히 생각할 수 있는 int, char, float, double, long등이 scalar type이다. (vector, map, set과 같은 것들은 non-scalar type이라고 볼 수 있다)

Static 키워드 정리

  • 서브젝트에 아래와 같이 작성되어 있다.
Write a class ScalarConverter that will contain only one static methods "convert"
that will takes as parameter a string representation of a C++ literal in its most common
form and output its value in the following serie of scalar types :
  • 내가 과제할 때는 모호하게 써있었던 것 같은데, 서브젝트가 바뀐건지 명확하게 쓰여있다. convert라는 static 함수만을 포함하는 작성해야 한다. convert는 c++ 리터럴 문자열을 받아서 적절한 스칼라 값들의 시리즈로 변환해야 한다.

  • static 키워드는 클래스에 직접 사용할 수 없다. 그러니까 ‘static class’ 라는 표현은 존재하지 않는다. 다만 정적 멤버 변수나 정적 멤버 함수를 선언하여 사용할 수 있다.

  • static 키워드 참고, static 키워드 완벽 가이드

(1) hpp에서의 전역 변수 선언

hpp에서 extern int num으로 extern 전역 변수를 선언할 경우, 선언과 초기화를 동시에 할 수 없다.

#ifndef GLOBAL_HPP
# define GLOBAL_HPP

#include <iostream>
#include <string>

static int	s_val = 0; // ok
extern int	g_val = 0; // int g_val = 0; 
                           // this cause compile error

#endif


  • 42 seoul C과제 중에 전역 변수를 사용할 수 있었던 몇몇 과제에서, 전역 변수를 선언하면서 동시에 초기화하다가 이런 동일 변수 재정의 컴파일 에러를 많이 봤었던 기억이 난다.

(2) 전역 Static 변수와 전역 Extern 변수의 차이

전역 Static 변수는 프로그램이 시작되면서 생성되고 초기화되며 프로그램이 종료될 때까지 남는다. 전역 Extern 변수는 동일한 이름을 가진다면 프로그램 전체에서 동일한 변수를 가리키지만(외부 링크), 전역 Static 변수는 소스 파일 단위 접근 범위를 가지며(내부 링크), 파일이 다르다면 같은 이름을 가진 변수라고 하더라도 다른 변수로 취급된다.

  • 예를 들어 main.cpp, Global.cpp, Global.hpp, static1.cpp, static1.hpp, static2.cpp, static2.hpp로 구성된 프로그램이 있다고 생각해보자. Global.hpp에서 extern 전역 변수 g_val과 static 전역 변수 s_val을 선언하고 g_val을 1, s_val을 0으로 초기화한다. 물론 1번에서 봤다시피, g_val은 Global.cpp에서 따로 선언해야 하고(Global.hpp에서 초기화 불가), s_val은 Global.hpp에서 곧바로 0으로 초기화해줄수 있다. static1, static2 파일들에서는 s_val의 내부 링크를 보여주기 위한 함수들을 작성한다.

  • static1.cpp와 static2.cpp에서 작성한 함수는 다음과 같다.

// static1.cpp
#include "Global.hpp"
#include "static1.hpp"

namespace File1
{
    void	SetVariables(int i)
    {
        s_val = i;
        g_val = i;
        std::cout << BOLDBLUE << "G_val = " << g_val << ", S_val = " \
        << s_val << " in File1::SetVariable" << RESET << std::endl;
    }
    void IncrementValues()
    {
        s_val++;
        g_val++;
        std::cout << BOLDGREEN << "G_val = " << g_val << ", S_val= " \
        << s_val << " in File1::IncrementValues" << RESET << std::endl;
    }
}

// static2.cpp
#include "Global.hpp"
#include "static2.hpp"

namespace File2
{
	void	SetVariables(int i)
	{
		s_val = i;
		g_val = i;
		std::cout << sBOLDBLUE << "G_val = " << g_val << ", S_val = " \
		<< s_val << " in File2::SetVariable" << RESET << std::endl;
	}
	void IncrementValues()
	{
		s_val++;
		g_val++;
		std::cout << BOLDGREEN << "G_val = " << g_val << ", S_val= " \
		<< s_val << " in File2::IncrementValues" << RESET << std::endl;
	}

}
  • static1.cpp의 SetVariables(int i)과 static2.cpp의 SetVariables(int i)는 거의 같은 일을 한다. 정수를 받아서 g_val과 s_val을 변경하고, g_val과 s_val을 출력한다. 다만 s_val은 내부 링크되어 해당 파일 안에서만 값이 변경되기 때문에 그것을 표시하기 위해 다른 출력값을 가진다. main문은 아래와 같이 작성했다.
#include "Global.hpp"
#include "static1.hpp"
#include "static2.hpp"
using namespace std;

// main.cpp Global.cpp 
// static1.cpp static2.cpp으로 구성된 프로그램, 
// Global.cpp, static1.cpp, static2.cpp에서
// g_val(extern)은 1, s_val(static)은 0으로 설정됨

int	main()
{
	cout<<BOLDYELLOW<<"G_val : "<<g_val<<", "<<"S_val : "\
	<<s_val<<' '<<"in Main function Line : "<<__LINE__ \
	<<RESET<<endl;
	// g_val은 1, s_val은 0 (line 8)
	
	File1::SetVariables(3); // defined in static1.cpp
	// static1.cpp에서 g_val과 s_val을 3으로 변경하고 변화를 출력
	// static1.cpp 내에서 g_val은 3, s_val은 3

	cout<<BOLDYELLOW<<"G_val : "<<g_val<<", "<<"S_val : "\
	<<s_val<<' '<<"in Main function Line : "<<__LINE__ \
	<<RESET<<endl;
	// main.cpp에서 g_val은 3, s_val은 0; (line 11)

	File2::SetVariables(5); // defined in static2.cpp
	// static2.cpp에서 g_val과 s_val을 5로 변경하고 변화를 출력
	// static2.cpp 내에서 g_val은 5, s_val은 5

	cout<<BOLDYELLOW<<"G_val : "<<g_val<<", "<<"S_val : "\
	<<s_val<<' '<<"in Main function Line : "<<__LINE__ \
	<<RESET<<endl;
	// main.cpp에서 g_val은 5, s_val은 0; (line 14)
}


  • 뭔가 복잡해보이기는 한데, 별건 없다. extern int g_val은 어디서든 바꾸는 족족 출력값이 변하는 반면, static int s_val은 각 파일마다 내부 링크되어 있기 때문에 static1.cpp이나 static2.cpp에서 백날 바꿔봐야 main.cpp에서는 절대 안바뀐다. static1.cpp에서 3으로 변경하고 main의 s_val을 출력하나, static2.cpp에서 5로 변경하고 main의 s_val을 출력하나 똑같이 main.cpp의 s_val은 0이다.

결론 : g_val은 프로그램 단위, s_val은 내부 파일 단위로 처리됨

기타

전역 변수의 초기화는 main 함수 호출 이전에 이루어지는데, 초기화를 위해 함수를 호출할 수 있어서 
이를 이용해 main 호출 이전에 다른 함수들을 호출할 수 있는 잔기술이 있다고 한다.

(3) 로컬 static 변수

함수 내부에 선언된 static 변수를 로컬 static 변수라고 하며 생성과 초기화 시점은 코드에서 변수 선언이 처음 호출되는 때이다. 생성된 로컬 static 변수는 프로그램이 종료될 때까지 유지된다. 특이한 점은 처음 호출에 단 한번만 초기화되며 나머지 호출에서 초기화 과정이 무시된다는 것이다.

// 함수 내부에서 static 변수 사용 예제
#include <iostream>
#include <string>

void demo()
{
    static int count = 0; // static 변수
    std::cout << count << " ";
    count++; // 함수가 호출 될 때 마다 값이 업데이트 된다
}

int main()
{
    for (int i=0; i<5; i++)	
    {
        demo();
    }
    return 0;
}
output:
    0 1 2 3 4

💡 로컬 static 변수는

(1) 함수가 호출 될 때 처음 한번만 초기화한다.

(2) 함수가 종료 되더라도 메모리에 값이 남아 있으며 프로그램이 종료 될 때 까지 값을 유지한다.

(3) 로컬 static 변수는 선언된 함수 내에서만 접근 할 수 있다.

(4) static 함수

static 키워드는 변수 뿐 아니라 함수에도 적용 가능하다. 일반 함수 선언 앞에 static 키워드를 추가하기만 하면 되며, static 전역 변수와 비슷한 Link 속성을 갖는다. 즉, 동일 소스 파일 내에서는 어디에서든 접근과 호출이 가능하지만 다른 소스 파일에서는 접근이 불가하며, 같은 이름을 가지고 있는 함수라 하더라도 다른 소스 파일에 있는 경우 다른 별도의 함수로 취급 된다.

// File1.h ---------------------------------------------------
void ExternalFunction();
static void StaticFunction();

// File1.cpp -------------------------------------------------
#include "File1.h"
#include <iostream>

void ExternalFunction()
{
    StaticFunction();
}

void StaticFunction()
{
    std::cout << "Static Function in File1 File" << std::endl;
}

// Program.cpp -----------------------------------------------
#include <iostream>
#include "File1.h"

void StaticFunction()
{
    std::cout << "Static Function in Program File" << std::endl;
}

int main()
{
    ExternalFunction();
    StaticFunction();

    return 0;
}
output :
    Static Function in File1 File
    Static Function in Program File
  • libft할 때 본 적이 있는 것 같은데, 안 쓰는걸 추천한다고 한다.

(5) 클래스에도 static 키워드를 적용할 수 있다.

static 멤버 변수

(1) 클래스 내에 static으로 선언된 멤버 변수는 특정 객체에 종속되지 않고 해당 클래스의 모든 객체들에 공유된다.

(2) static 멤버 변수는 생성자에서 초기화 될 수 없으며, 클래스 외부에서의 별도 초기화 과정이 필요하다. (클래스 내에서 초기화하면 컴파일 오류)

#include <iostream>

class Stat
    {
    public:
        static int i; // 클래스 내부에서 생성
        Stat() {};
};

int Stat::i = 0; // 클래스 외부에서 초기화

int main()
{
    Stat obj1;
    Stat obj2;

    obj1.i = 2;
    obj2.i = 3; // (Stat::i = 3 <- 권장 접근 방식)
                // 모든 객체가 i를 공유하므로 최종적인 i의 값은 3
    std::cout << obj1.i << " " << obj2.i << std::endl;
}
output : 
    3 3

(3) static 멤버 변수는 위 코드에서 처럼 클래스 객체에 ‘.’ 연산자를 이용해 접근할 수도 있고 클래스 이름을 이용해 다음과 같이 접근할 수도 있다. 위 예제는 이렇게도 사용가능 하다는 것을 보여주기 위한 예제일 뿐이고, 관례적으로 클래스 이름을 통한 접근 방식을 사용하길 권한다.

Stat::i = 2; // 클래스 이름을 이용해 static 멤버 변수에 접근. 권장 방식
obj2.i = 3; // 클래스 객체를 통해 static 멤버 변수에 접근.

이러한 정적 멤버 변수는 외부 연결(external linkage)을 가지므로, 여러 파일에서 접근할 수 있다.

static 멤버 함수

(1) static 멤버 변수와 마찬가지로 클래스 객체에 종속되지 않는다.

(2) 클래스 멤버 함수 역시 객체에 ‘.’ 연산자를 통해 접근 가능하지만 권장 되는 방법은 아니다. static 멤버 함수에 접근하기 위해서는 클래스 네임과 스코프 연산자(::)를 이용해 호출하는 것을 권장한다.

(3) static 멤버 함수는 클래스 객체에 종속된 것이 아니므로 함수 내부에서 클래스 객체에 종속 되는 일반 멤버 변수 또는 멤버 함수는 접근 불가능하다. 하지만 static 멤버 변수나 static 함수는 접근 가능하다. (일반 멤버 함수에는 정적 멤버 변수나 일반 멤버 변수 둘다 접근 가능)

→ static 멤버 함수가 객체 선언없이 클래스 네임스페이스만 가지고 호출이 가능한 이유는 static 멤버 함수를 호출할 때 this 포인터가 넘어가지 않기 때문이다. 즉 this 포인터를 사용하여 지목할 수 있는 일반 멤버 변수는 static 멤버 함수에서 사용할 수 없게 되는 것이다.

#include <iostream>

class Stat
{
    public:
        static int i; // 클래스 내부에서 생성
        Stat() {};
        Stat() {
                i++; std::cout<<i<<std::endl;
                //i를 하나 늘리며 생성
        }
        ~Stat() {
                i--; std::cout<<i<<std::endl;
                //i를 하나 줄이며 생성	
        }
        static int getStatic() {
                return (i);
        }
};

int Stat::i = 0; // 클래스 외부에서 초기화

int main()
{
    Stat obj1;
    Stat obj2;

    std::cout<< "i : " << Stat::getStatic() << std::endl;
}

output :
    2
class Test {
public:
    virtual static void fun1() {}
    // error: member ‘fun1’ cannot be declared 
    // both virtual and static 
    // virtual static void fun1() 
    
    static void fun2() const {}
    // error: static member function 
    // 'static void Test::fun2()’ cannot have cv-qualifier
    // static void fun2() const {}
};


→ 추가: static 멤버 함수의 정의에서 static 키워드는 클래스 정의부 안에서만 사용될 수 있고, cpp 파일에서 정의할때는 쓸 수 없다.

std::strtod

  • std::strtod를 사용하면 입력으로 들어온 리터럴 문자열을 편리하게 double로 파싱할 수 있다. 심지어 double이 아닌 경우에 대한 예외 처리 또한 훌륭하게 처리해낼 수 있다. 본인은 strtod를 사용해서 편리하게 과제를 풀어가려고 날먹하려고 했는데, 결론적으로는 strtod만 써서는 과제를 통과하기가 어려울 수도 있다. 그것은 서브젝트의 아래 구문 때문이다.
 You have to first **detect the type of the literal passed as parameter**, 
 **convert it from string to its actual type**, 
 **then convert it explicitly to the three other data types.**
 Lastly, display the results as shown below.
    : char, int, float, double
  • 그러니까 서브젝트 말대로면, 리터럴이 주어지면 먼저 적절한 파싱 단계를 거쳐서 그게 무슨 타입인지 알아낸 후 그 타입으로 바꾸고, 이후 나머지 세 개의 스칼라로 바꾸는 과정을 거쳐야한다는 것 같은데 여기서 그냥 strtod를 써버리면 리터럴로 뭐가 들어오든 간에 그냥 double로 바꿔버리고 나머지 남은 스칼라로 바꾸는 순서로 진행되어서 과제의 취지와 맞지 않다는 것이다.

  • 즉, 리터럴이 들어왔을 때 그것이 char인지 int인지, float인지 double인지 밝혀내기 위한 독자적인 로직을 만들어야 한다는 점인데…, 좀 까탈스러운 과제이기는 하다. ex00을 fail 받아도 통과하는데는 문제가 없기 때문에 통과만을 목적으로 한다면 굳이 노력해서 만들 필요는 없기는 하다만, 이 strtod 함수만은 cpp module 끝날 때까지 사용할 것이기 때문에 공부해두는 편이 좋을 것 같다. 프로토타입은 아래와 같다.

// defined in header <cstdlib>, std::strtof, std::strtod, std::strtold
float  strtof(const char *str, char **str_ptr)
double strtod(const char *str, char **str_ptr)
long double strtold(const char *str, char **str_ptr)
  • str이 가리키는 바이트 문자열에서 부동 소수점 값을 해석한다.

  • Parameters

(1) str : 해석 할 널 종료 바이트 문자열에 대한 포인터

(2) str_ptr : 문자를 가리키는 포인터

-> 함수는 str_ptr 가 가리키는 포인터가 해석된 마지막 문자 이후의 문자를 가리키도록 설정한다. str_ptr 가 널 포인터면 무시된다.

예시 1) str_ptr이 null이 아닌 경우

#include <iostream>
#include <cstdlib>

using namesspace std;

int main()
{
    char numberString[] = "12.44b 0xy";
    char *end;
    double number;

    number = strtod(numberString, &end);
    cout << "Number in String = " << numberString << endl;
    cout << "Number in Double = " << number << endl;
    cout << "End String = " << end << endl;

    return 0;
}
output: 
    Number in End String = 12.44b 0xy
    Number in Double = 12.44
    String = b 0xy

예시 2) str_ptr이 null이 아닌 경우

#include <iostream>
#include <cstdlib>

using namespace std;

int main()
{
    char numberString[] = "12.44";
    char *end;
    double number;

    number = strtod(numberString,&end);
    cout << "Number in String = " << numberString << endl;
    cout << "Number in Double = " << number << endl;

    // If end is not Null
    if (*end) {
        cout << end;
    }
    // If end is Null
    else {
        cout << "Null pointer";
    }
    return 0;
}
output: 
    Number in End String = 12.44b 0xy
    Number in Double = 12.44
    Null pointer
  • Return Value : 성공시 str 의 내용에 해당하는 부동 소수점 값(혹은 double, long double 값). 적당한 conversion이 이루어질 수 없었다면 0.0을 반환.

  • 추가 : .으로 끝나는 string의 경우, strtod가 자동으로 .0으로 변환하여 double로 바꿔준다. (f는 해당 사항 없다.)

Convert to ***

  • strtod를 통해 double 값을 받았으면, 그것들을 char, int, float로 변환해야 한다. 이때 유용하게 쓸 수 있는 함수들이 std::isinfstd::isnan이다
int isnan(double a) 
int isinf(double a)
int isnanf(float a)
int isinff(float a)

💡 정보 (1) isinf(double x)는 매개 변수 x가 Infinity라면 1을 반환하고, 그렇지 않다면 0을 반환한다. (2) isnan(double x)는 매개 변수 x가 NaN이라면 1을 반환하고, 그렇지 않다면 0을 반환한다

→ isinff(float x)와 isnanf(float x)는 각각 isinf(double x)와 isnan(double x) 단정도(single-precision) 버전이다.

  • isnan()과 isinf()의 구현도가 꽤나 높다는 점은 여러가지 입력값을 받아봄으로써 알아볼 수가 있다. 이는 굉장히 재미있는 부분이다.
#include <iostream>
#include <cmath>

int main()
{
    double b;
	while (1)
	{
		std::cout<<"Input any value that related with inf and nan : "<<RESET;
		std::cin>>b;
		if (std::isnan(b))
		{
			std::cout << "NANI!!!\n";
		}
		else if (std::isinf(b))
		{
			std::cout << "INFINTIY!!!\n";
		}
		else if (static_cast<float>(b) && static_cast<double>(b))
		{
			std::cout << BOLDRED << static_cast<float>(b)<<std::endl;
			std::cout << BOLDRED << static_cast<double>(b)<<std::endl;
		}
		else
			break ;
	}
	return 0;
}


  • 소문자든, 대문자든 nan과 inf이기만 하면 적절한 처리를 해준다. 편리한 부분이 아닐 수가 없다.

  • 그 외에는 변환하려는 스칼라에 따라 적절한 제약 사항을 넣어 분기 처리를 하면 되겠다. 예를 들어 convert to char라면 static_cast<char>(value) 아스키 넘버를 확인한다든지, convert to int라면 static_cast<long>(value)가 INT_MAX보다 큰지, INT_MIN보다 작은지, convert to float/double이라면 static_cast<int64_t>(value)가 value와 동일하다면 소수점 처리를 따로 해준다든지 하는 조건을 걸면 될 것 같다.

  • 반쪽짜리 구현으로 100점 통과를 못하긴 했는데, 솔직히 리트할 것 같진 않다. 굳이