본문 바로가기

C++/윤성우의 열혈 프로그래밍 C++

윤성우의 열혈 C++ chpt9.

const 보충 내용

class SoSimple
{
private:
    int num;
public:
    SoSimple(int n) : num(n)
    {}
    SoSimple& AddNum(int n)
    {
        num += n;
        return *this;
    }
    void ShowData() const
    {
        cout << "num: " << num << endl;
    }
};

----------------------------------------------------------------------------

int main(void)
{
    const SoSimple obj(7);
    // obj.AddNum(20);	에러
    obj.ShowData();

    return 0;
}

// const 로 선언된 객체에서 비const 메소드를 호출하는 것은 불가능

 

const 와 함수 오버로딩

class SoSimple
{
private:
	int num;
public:
    SoSimple(int 3) : num(n)
    {}
    SoSimple& AddNum(int n)
    {
        num += n;
        return *this;
    }
    void SimpleFunc()
    {
        cout << "SimpleFunc: " <<< num << endl;
    }
    void SimpleFunc() const	// 동일한 함수를 const 만 추가
    {
        cout << "const SimpleFunc: " << num << endl;
    }
};

-----------------------------------------------------------------------------

void YourFunc(const SoSimple& obj)	// 비const 함수도 const 함수화
{
    obj.simpleFunc();
}

int main(void)
{
    SiSimle obj1(2);
    const SoSimple obj2(7);

    obj1.SimpleFunc();
    obj2.SimpleFunc();

    YourFunc(obj1);
    YourFunc(onj2);

    return 0;
}
---------------------------------------------------------------------
// 결과
SimpleFunc: 2
const SimpleFunc: 7
const SimpleFunc: 2
const SimpleFunc: 7

 

객체의 멤버 함수와 오버로딩된 const 함수가 있을 때,

 

인스턴스 비const 라면 일반 멤버 함수가 호출되고,

 

인스턴스const 함수라면 const 멤버 함수가 호출되도록 약속되어 있다.

 

YourFunc 에서도 파라미터가 const 로 되어있기 때문에, 비const 객체가 복사생성자를 통해 전달되어도, 

복사받은 객체는 const 이기 때문에 const 멤버 함수의 호출로인한 출력문이 출력된다.


클래스와 함수에 대한 friend 선언

friend 키워드는 클래스 외부의 함수나 다른 클래스가 해당 클래스의 private, protected 멤버에 접근 할 수 있도록 예외적으로 허용해주는 키워드이다.

 

C++ 에서 중요하게 여기는 정보 은닉 및 캡슐화 원칙에 위배되는 키워드로써 사용을 지양하지만, 불가피한 경우 신중하게 사용할 것을 권한다.

 

또한 friend 키워드는 단방향 관계으로써 해당 클래스만 접근을 허용할 뿐, 상대 클래스에서는 반대로 접근할 수는 없으며,

상속도 되지 않기 때문 자식 클래스가 부모 클래스의 friend 권한을 자동으로 갖지 않는다

class Boy
{
private:
    int height;
    friend class Girl;
public:
    Boy(int len) : heigh(len)
    {}
    ...
};

-----------------------------------------------------------------

class Girl
{
private:
    char phNum[20];
public:
    Girl(char *num)
    {
        strcpy(phNum, num);
    }
    void ShowYourFriendInfo(Boy& frn)
    {
        cout << "His height: " << frn.height << endl;
    }
};

Boy 클래스에서 Girl 클래스에 일방적 friend 키워드를 부여함으로써, 

Girl 클래스에서 참조로 받은 Boy 객체 인스턴스에 대해 멤버 변수 frn.height 직접 접근하여 출력하려고 하는 과정을 볼 수 있다.

 

 

class Point
{
private:
    int x;
    int y;
public:
    Point(const int &xpos, const int &ypos) : x(xpos), y(ypos)
    {}
    friend Point PointOP::PointAdd(const Point&, const Point&);
    friend Point PointOP::PointSub(const Point&, const Point&);
    friend void ShowPointPos(const Point&);
    
    // PointOP 클래스의 PointAdd 함수에 friend 부여
    // PointOP 클래스의 PointSub 함수에 friend 부여
    // ShowPointPos 함수에 friend 부여
};

--------------------------------------------------------------------
// PointOP.cpp

Point PointOP::PointAdd(const Point& pnt1, const Point& pnt2)
{
    opcnt++;
    return Point(pnt1.x + pnt2.x, pnt1.y + pnt2.y)
    // point 클래스의 멤버 변수 접근
}

Point PointOP::PointSub(const Point& pnt1, const Point& pnt2)
{
    opcnt++;
    return Point(pnt1.x - pnt2.x, pnt1.y - pnt2.y);
    // point 클래스의 멤버 변수 접근
}

------------------------------------------------------------------------
// 전역 함수

void ShowPointPos(const Point& pos)
{
    cout << "x: " << pos.x << ", ";
    cout << "y: " << pos.y << endl;
    // point 클래스의 멤버 변수 접근
}

 

PointOP 객체의 멤버 함수, ShowPointPos() 와 같이 전역 함수에도 friend 기능을 부여함으로써 부여해준 객체의 멤버 변수에 접근하는 것을 볼 수 있다.


C++ 에서 static

 

extern

  • 변수함수다른 파일에서 정의되어 있음을 나타내는 키워드(다른 소스파일의 전역에 설정된 변수나 함수를 가져와서 사용)
  • 현재 파일에서는 선언만 하고, 링크 타임에 실제 정의와 연결시키는 데 사용된다.
  • static 으로 선언된 변수나 함수에 대해선 접근할 수 없다.

static

  • 지역 변수에 사용하면 한 번 만 초기화되고, 해당 함수가 종료되어도 값을 유지하며, 
    메모리는 프로그램 전체 생명주기 동안 유지된다.
  • 전역 변수에 사용하면, 해당 파일 내에서만 접근 가능하도록 제한한다 (internal linkage)

static 을 전역 변수로 선언을 하고, 다른 파일에서 extern 으로 접근하려고 하면,

static 은 해당 파일 내에서만 유효하기 때문에, 접근하려는 쪽에선 해당 변수를 찾을 수 없어

링크 에러(undefined reference)가 발생한다.

 

 

객체에서 전역 변수 또는 함수를 사용할 경우, static 을 통해 사용해야 한다.

사용시에는 멤버 변수와 유사하게 접근이 가능하나, 멤버 변수라고 보긴 어렵다

'static 멤버 변수'라로도 불리지만, 스태틱 변수 또는 클래스 변수라고 부르는 게 더 적합하다.

또한 모든 객체가 공유하기 때문에 변수 값을 객체 별이 아닌 모두 동일한 static 변수의 값을 공유한다.

 

static 변수를 pubilc 에서 사용했을 때 한정으로,

[ 객체.변수 ] 방식으로 접근은 가능하지만 지양하는 방식이다. 이 변수가 멤버 변수인지 클래스 변수인지 알 수 없기 때문이고,

[ 클래스::변수 ] 방식을 통해 해당 변수가 클래스 변수이라는 것을 알 수 있게 사용하는 것이 좋다

 

void Counter()
{
    static int cnt;
    cnt++;
    cout << "Current cnt: " << cbt << endl;
}

int main(void)
{
    for (int i = 0; i < 10; i++)
        Counter();
        
    return 0;
}
---------------------------------------------------------------
// 결과

Current cnt: 1
Current cnt: 2
Current cnt: 3
Current cnt: 4
Current cnt: 5
Current cnt: 6
Current cnt: 7
Current cnt: 8
Current cnt: 9
Current cnt: 10

루프를 통해 Counter() 여러번 호출하고 닫히는 과정임에도 불구하고,

static 으로 인해 Counter 내의 cnt 값이 누적되는 것을 볼 수 있다.


static 멤버변수(클래스 변수)

class SoSimple
{
private:
    static int simObjCnt;	// static 멤버변수(클래스 변수)
public:
    SoSimple()
    {
        simObjCnt++;
        cout << simObjCnt << "번째 SoSimple 객체" << endl;
    }
};

int SoSimple::simObjCnt = 0;	// static 멤버변수의 초기화

-----------------------------------------------------------------

int main(void)
{
    SoSimple sim1;
    SoSimple sim2;
    SoSimple sim3;
    
    ...
}

------------------------------------------------------------------
// 결과

1번째 SoSimple 객체
2번째 SoSimple 객체
3번째 SoSimple 객체

 

static 클래스 변수는 클래스에 선언이 되나 한 객체에 국한되지 않는 성격을 가진다.

 

또한 객체 생성시 생성되는 것이 아닌

 

'프로그램 시작시' 메모리에 공간을 갖게 되며,

 

이로 인해 객체 외부에서 별도의 초기화 작업을 해주어야 변수 생성과 동시에 초기화가 된다.

(static 변수의 특성상 객체 내부에서 초기화 할 수 없으므로 인해 차용된 방식)

 

전역 변수 필요시 전역 설정보단 가급적이면 static 을 통해 사용할 것을 권고

 

 

static 멤버변수(클래스 변수)의 접근방법

class SoSimple
{
public:
    static int simObjCnt;
public:
    SoSimple()
    {
        simObjCnt++;	// 직접 접근가능
    }
};

int SoSimple::ObjCNt = 0;

---------------------------------------------------------------------

int main(void)
{
    cout << SoSimple::simObjCnt << "번째 SoSimple 객체" << endl;

    SoSimple sim1;
    SoSimple sim2;

    // 클래스명을 통한 접근 가능(단, static 변수가 public 에 선언될 것)
    cout << SoSImple::simObjCnt << "번째 SoSimple 객체" << endl;
    
    // 객체(오브젝트)를 통한 접근 가능(단, static 변수가 public 에 선언될 것)
    // 지양하는 접근 방식
    cout << sim1.simObjCnt << "번째 SoSimple 객체" << endl;
    cout << sim2.simObjCnt << "번째 SOSimple 객체" << endl;

    return 0;
}
---------------------------------------------------------------------
// 결과

0번째 SoSimple 객체
2번째 SoSimple 객체
2번째 SoSimple 객체
2번째 SoSimple 객체

 

 

static 멤버 함수

  • 선언된 클래스의 모든 객체가 공유
  • public 으로 선언이 되면, 클래스의 이름을 이용해서 호출 가능
  • 역시나 객체의 멤버로써 존재하지는 않음
xlass SoSimple
{
private:
    int num1;
    static int num2;
public:
    SoSimple(int n): num1(n)
    {}
    static void Adder(int n)
    {
        num1 += n;	// 컴파일 에러, 접근 불가
        num2 += n;
    }
};

int SoSimple::num2 = 0;

static 함수객체 내에 존재하는 함수가 아니기 때문에

 

멤버 변수나 멤버 함수에 접근이 불가능!!!

 

static 함수는 static 변수에만 접근이 가능하고, 같은 static 함수만 호출이 가능

 

 

 

static 함수가 멤버 접근이 허용될 경우, 만약 두 개 이상의 객체 생성 후

 

클래스명::Adder() 을 통해 객체 멤버 변수인 num1 을 증가 시키고자 할 때, 

 

어떤 객체의 num1 을 증가시켜야 되는지가 알 수 없는 상황이 생겨버림


Const static 멤버와 mutable

 

class CountryArea
{
public:
    const static int RUSSIA = 1707540;	// static 의 내부 초기화 선언을 가능케 해주는 const
    const static int CANADA = 998467;
    const static int CHINA = 957290;
    const static int SOUTH_KOREA = 9922;
};

------------------------------------------------------

int main(void)
{
    cout << "러시아 면적: " << CountryArea::RUSSIA << "km" << endl;
    cout << "캐나다 면적: " << CountryArea::CANADA << "km" << endl;
    cout << "중국 면적: " << CountryArea::CHINA << "km" << endl;
    cout << "한국 면적: " << CountryArea::SOUTH_KOREA << "km" << endl;

    return 0;
}

 

기존의 static 변수는 객체 내부에서 초기화 할 수 없다고 설명했었다.

 

하지먄 이 static 을 내부에서 초기화 가능케 해주는 키워드가 바로 const 이다.

 

const 는 초기화 값을 반드시 가져야되는 특성이 static 의 특성보다 우위로 판단되어 허용되겠끔 구조되어 있다.

 

 

 

단! const static 변수라도 모든 타입의 변수를 class 내부에서 초기화 할 수 있는 것은 아니다.

 

정수형과 열거형에 한해서만 유효하며, 그외에는 const static 이라 할지라도 외부에서 별도로 선언해줘야 된다.

 

ISO/ICE C++20 초안을 보면

더보기

If a non-volatile non-inline const static data member is of integral or enumeration type,

its declaration in the class definition can specify a brace-or-equal-initializer in which every initializer-clause that is an assignment-expression is a constant expression

더보기

const staic 멤버가 volatile 과 inline 이 아닌 정수 또는 열거 타입일 경우, 그

것의 선언은 정의 클래스 내에서 {} 또는 = 로 초기화를 명시할 수 있으며, 

모든 초기화절은 상수 표현식인 대입 표현식이어야 한다.

라고 적혀있어 int 형과 enum 아닌 경우에는 그대로 외부에서 선언이 되어야 한다.

 

 

그럼 왜 int 또는 enum 만 허용했을까?

 

그 이유는 C++ 창시자 Bjarne Stroustrup 의 개인 공식 홈페이지

Technical FAQ 의 How do I define an in-class constat? 란 에서 내용을 확인할 수 있다.

더보기

So why do these inconvenient restrictions exist? A class is typically declared in a header file and a header file is typically included into many translation units. However, to avoid complicated linker rules, C++ requires that every object has a unique definition. That rule would be broken if C++ allowed in‑class definition of entities that needed to be stored in memory as objects

더보기

그렇다면 왜 이런 불편한 제약들이 존재할까?
클래스는 보통 헤더 파일에서 선언되고, 헤더 파일은 일반적으로 여러 번역 단위(translation units, TU) 에 포함된다.
하지만 복잡한 링커 규칙을 피하기 위해, C++은 모든 객체가 고유한 정의(ODR)를 갖도록 요구한다.
그런데 만약 C++이 메모리에 객체로 저장되어야 하는 요소들을 클래스 내 정의를 허용한다면,
이 규칙은 깨지게 된다

ODR(One Definition Rule): 하나의 정의 규칙

  • 어떤 객체나 함수에 대해 프로그램 전체에 단 하나의 정의(메모리)만 있어야 한다.
  • 같은 객체나 함수가 여러 번 정의되면 컴파일러/링터가 어떤 정의를 써야 할지 모르게 되어 링커 오류나 미정의 동작이 발생할 수 있다.

즉, 창사지의 말을 풀어보자면  static 멤버 변수는 모든 객체가 공유하는 변수로,

만약 클래스 정의 안에서 static 을 초기화하게 된다면

  1. 해당 클래스가 정의된 헤더 파일 여러 소스파일(.cpp)에서 include 하게 되고,
  2. 그 결과, 여러 개의 번역 단위(TU)에 걸쳐 해당 static 멤버가 중복 정의될 수 있다.(한 변수에 여러 메모리를 할당)
  3. 이로 인해 링커동일한 이름을 가진 객체가 여러 번 정의되었다는 에러를 발생 시킴
  4. 이 상황이 ODR 에 위반되는 상황이 발생된 것이라 할 수 있다.

이러한 이유로 클래스의 static 변수는 정수형 또는 열거형만 클래스 내에서 초기화를 가능하게 제한을 둔 것이다.

 

정수형이나 열거형상수 표현식(constant expression) 으로 초기화할 수 있기 때문이다.

  • 컴파일러정수형이나 열거형실제 메모리에 객체로 저장하지 않고
  • 그냥 값 자체로 치환해서 사용
  • 그러므로 메모리 상에 객체가 존재하지 않고
  • 실제로 정의된 것도 아니기 때문에
  • 여러 TU에서 동일하게 포함되어도 ODR을 위반하지 않음

 

이게 도대체 무슨 말인지 처음엔 이해를 하지 못했다

 

이 부분은 컴파일 단계에서 어셈블리어 수준까지 내려가야 조금 이해되는 부분이다!

더보기

우리가 일반적으로 사용하는 보통의 변수는 어셈블리어에서 해당 변수의 주소(메모리 공간)를 통해서 연산에 사용된다.

 

예를 들어 다음과 같은 코드가 있을 때,

 

int a = 2 + 5;

a 는 데이터 영역 또는 스택에 위치한 메모리 공간의 주소를 통해 다뤄진다.

그런데 2 와 5 는 어떻게 처리가 되는가?

2 와 5 는 리터럴 즉, 컴파일 타임에 결정되는 상수값으로, 어셈블리 단계에선 메모리 주소가 아닌 2와 5라는 값 그 자체를 활용해서 연산을 한다.

 

간단하게


mov eax, [a]  <- 이게 보편적인 변수를 다루는 방식이라면

 

mov eax, 2  <- 이게 리터럴을 다루는 방식이다

 

즉, 주소를 활용하지 않고 값 자체를 어셈블리어에서 사용하는 거고 기계어로 내려가면 2 || 5 의 2진수를 활용하는 것이다.

 

이와 같이 C++ 의 클래스 변수 const static int 의 경우

 

const 로 인해 불변 속성 얻었고, static 으로 인해 전역적인 공간에서 단일 메모리를 가지고 있기 때문에

 

객체와 무관해져 인스턴스마다 복사할 필요가 없다.

 

따라서 해당 값은 바뀌지 않고 공유됨으로 인해 위의 2 나 5 와 동일한 리터럴 개체로 볼 수 있다.

 

그렇기 때문에 const static int 를 상수값으로써만 사용한다면

 

컴파일러가 메모리 형성하지 않고 리터럴로 치환해줘서 값 그 자체로써 존재하게 된다.

 

이는 C의 enum 상수와 유사한 개념이다.

 

enum 값도 컴파일 타임정수 리터럴로 결정되어 별도의 객체나 메모리 공간 없이 치환되기 때문이다.

 

 

 

실제로 const static int 변수참조(&) 를 통해 주소를 얻으려고 한다면

 

const static int 는 이미 리터럴로 치환 된 값 그 자체이기 때문에 주소가 없어 에러를 발생시킨다.

 

 

mutable 선언

class SoSimple
{
private:
    int num1;
    mutable int num2;
public:
    SoSimple(int n1, int n2)
        : num1(n1), num2(n2)
    {}
    void CopyToNum2() const
    {
        num2=num1;
    }
};

 

mutable 로 선언된 멤버 변수const 함수 내에선 접근할 수 없었던 멤버 변수의 접근 및 값의 변경을 가능하게 해준다.

 

역시 사용하는 것을 지양하는 바이다.