CE : 하랑 2023. 10. 30. 21:47

 

1. const

(1) 의미적인 제약 -> const  키워드가 붙은 객체는 외부 변경을 불가능하게 한다.

(2) 소스 코드 수준에서 붙인다는 점과 컴파일러가 이 제약을 단단히 지켜준다는 점

(3) 클래스 바깥에서는 전역 혹은 네임스페이스 유효범위의 상수를 선언(정의)하는 데 쓸 수 있다.

(4) 파일, 함수, 블록 유효범위에서 static으로 선언한 객체에도 const를 붙일 수 있다.

 

(5) 클래스 내부 -> 정적 멤버 및 비정적 데이터 멤버 모두를 상수로 선언할 수 있다.

(6) 포인터인 경우 -> 기본적으로 포인터 자체를 상수로, 혹은 포인터가 가리키는 데이터를 상수로 지정할 수 있는데, 둘 다 지정할 수도 있고 아무것도 지정하지 않을 수도 있다.

 

- const 키워드가 *표의 왼쪽에 있으면 포인터가 가리키는 대상이 상수

- const가 *표의 오른쪽에 있는 경우엔 포인터 자체가 상수

- const가 *표의 양쪽에 다 있으면 포인터가 가리키는 대상 및 포인터가 다 상수

 

(7) 

 

void f1(const widget *pw);

 

void f2(widget const *pw);

 

-> 매개변수 타입은 모두 똑같다.

 


STL 반복자(iterator)는 포인터를 본뜬 것이기 때문에, 기본적인 동작 원리가 T* 포인터와 흡사

 

- 어떤 반복자를 const로 선언하는 일은 포인터를 상수로 선언하는 것(T* const 포인터)과 같다.

 

- 반복자는 자신이 가리키는 대상이 아닌 것을 가리키는 경우가 허용되지 않지만, 반복자가 가리키는 대상 자체는 변경 가능

 

- 만약 변경 불가능한 객체를 가리키는 반복자(const T* 포인터의 STL 대응물)가 필요하다면 const_iterator 쓰면 된다.

 

 


 

반복자(iterator)란 STL 컨테이너에 저장된 요소를 반복적으로 순회하여, 각각의 요소에 대한 접근을 제공하는 객체



 

가장 강력한 const 용도 -> 함수 선언에 쓸 경우

 

1. 함수 선언문에 있어서 const는 함수 변환 값, 각각의 매개변수, 멤버 함수 앞에 붙을 수 있고, 함수 전체에 대해 const의 성질을 붙일 수 있다.

2. 함수 반환 값을 상수로 정해 주면, 안전성이나 효율을 포기하지 않고도 사용자측의 에러 돌발 상황을 줄이는 효과를 자주 볼 수 있다.

3. 매개 변수 혹은 지역 객체를 수정할 수 없게 하는 것이 목적

 


 

매개변수(媒介變數), 파라미터(parameter), 모수(母數)는 수학과 통계학에서 어떠한 시스템이나 함수의 특정한 성질을 나타내는 변수를 말한다.



 

상수 멤버 함수

 

1. 멤버 함수에 붙은 const 키워드의 역할은 해당 멤버 함수가 상수 객체에 대해 호출될 함수이다라는 사실을 알려주는 것

 

이런 함수가 중요한 이유?

 

(1). 클래스의 인터페이스를 이해하기 좋게 하기 위해서

- 그 클래스로 만들어진 객체를 변경할 수 있는 함수는 무엇이고, 또 변경할 수 없는 함수는 무엇인가를 사용자 쪽에서 알고 있어야 하는 것입니다.

 

(2). 이 키워드를 통해 상수 객체를 사용할 수 있게 하는 것 

- 코드의 효율을 위해 아주 중요한 부분

- C++ 프로그램의 실행 성능을 높이는 핵심 기법 중 하나가 객체 전달을 상수 객체에 대한 참조자로 진행하는 것이기 때문 -> 상수 멤버 함수가 준비되어 있어야 한다.

 

- const 키워드가 있고 없고의 차이만 있는 멤버 함수들은 오버로딩이 가능

 


 

참조자는 할당된 하나의 메모리 공간에 다른 이름을 붙이는 것을 말한다.


 

2. 실제 프로그램에서 상수 객체가 생기는 경우

(1) 상수 객체에 대한 포인터

(2) 상수 객체에 대한 참조자로 객체가 전달될 때

 

 

 

 

3. 어떤 멤버 함수가 상수 멤버(const)라는 것이 대체 어떤 의미?

(1) 비트수준 상수성 -> 물리적 상수성

- 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 멤버 함수가 const 임을 인정하는 개념 -> 그 객체를 구성하는 비트들 중 어떤 것도 바꾸면 안된다.

- 상수성 위반을 발견하는 데 힘이 많이 들지 않는다. -> 컴파일러는 데이터 멤버에 대해 대입 연산이 수행되었는지만 보면 된다.

- C++에서 정의하고 있는 상수성

- 상수 멤버 함수는 그 함수가 호출된 객체의 어떤 비정적 멤버도 수정할 수 없게 된다.

 

(2) 논리적 상수성

- 어떤 값으로 초기화된 상수 객체를 하나 만들어 놓고 이것에다 상수 멤버 함수를 호출했더니 값이 변하는 경우

-> 이러한 상황을 보완하는 대체 개념으로 나온 논리적 상수성

 

- 상수 멤버 함수라고 해서 객체의 한 비트도 수정할 수 없는 것이 아니라 일부 몇 비트 정도는 바꿀 수 있되, 그것을 사용자측에서 알아채지 못하게만 하면 상수 멤버 자격이 있다는 것

 

class test{

    ...........................................  

 

private:

  char *pText;

  std::size_t textlength;

  bool lengthIsValid;

};

 

std::size_t CTextBlock::length() const

{

  if(!lengthIsValid){

     textLength = std::strlen(pText); // 에러!

     lengthIsValid = true;  // 상수 멤버 함수 안에서는 textLength 및 lengthIsValid에 대입할 수 없다.

  }

 

  return textLength;

}

 

-> 해결책 mutable

mutable을 사용하는 것

-> mutable은 비정적 데이터 멤버를 비트수준 상수성의 족쇄에서 풀어 준다.

 

private:

  char *pText;

  mutable std::size_t textlength; // 상수 멤버 함수 안에서도 수정 할 수 있다.

  mutable  bool lengthIsValid;

};

 

std::size_t CTextBlock::length() const // 상수 멤버 함수

{

  if(!lengthIsValid){

     textLength = std::strlen(pText); 

     lengthIsValid = true;  

  }

 

  return textLength;

}

 


 

상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법

 

- mutable로 const에 관련된 골칫거리 전부를 말끔히 해결하진 못한다.

 

(1) const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는데 도움을 준다.

(2) const는 어떤 유효범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수 및 반환 타입에도 붙을 수 있으며, 멤버 함수에도 붙을 수 있다.

(3) 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 개념적인(논리적인) 상수성을 사용해서 프로그래밍해야 한다.

(4) 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하도록 만든다.

 


객체를 사용하기 전에 반드시 그 객체를 초기화하자

 

class point{

  int x,y;

}

 

. . .

 

point p;

 

p의 데이터 멤버 역시 어떤 상황에서는 초기화가 보장되지만 어쩔 때는 또 안된다.

 

- 초기화되지 않은 값을 읽도록 내버려 두면 정의되지 않은 동작이 그대로 흘러오게 된다.

- 어떤 플랫폼의 경우에는 미초기화 객체를 읽기만 해도 프로그램이 서 버리기도 한다.

- 대체적인 경우에는 적당히 무작위 비트의 값을 일고 객체의 내부가 이상한 값을 갖게 된다.

 

 

가장 좋은 방법

- 모든 객체를 사용하기 전에 항상 초기화하는 것

 

ex)

int x=0; // int의 직접 초기화

 

const char* text="TEXT" // 포인터의 직접 초기화

 

double d;

std :: cin >> d; // 입력 스트림에서 읽음으로써 초기화 수행

 

ex) 부분을 제외하고 나면, C++ 초기화의 나머지 부분은 생성자로 귀결

 


 

생성자 (Constructor)는 해당 클래스의 객체가 인스턴스화될 때 자동으로 호출되는 특수한 종류의 멤버 함수다. 생성자는 일반적으로 클래스의 멤버 변수를 적절한 기본값 또는 사용자 제공 값으로 초기화하거나 클래스를 사용하는 데 필요한 설정(ex. 파일 열기 등)이 필요한 경우 사용된다.

 

 

인스턴스는 일반적으로 실행 중인 임의의 프로세스, 클래스의 현재 생성된 오브젝트를 가리킨다.


 

 

 

생성자에서 지킬 규칙

- 그 객체의 모든 것을 초기화

- 대입을 초기화와 헷갈리지 않는 것이 가장 중요

 

C++ 규칙에 의하면 어떤 객체이든 그 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에 초기화되어야 한다.

 

 

ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)

{

   thename=name;

   theAddress = address;

   thePhone = phones;

   numtimesConsulted=0;

}  // 대입

 

- 대입만 사용한 버전의 경우 thename, theAddress 및 thePhones에 대해 기본 생성자를 호출해서 초기화를 미리 해 놓은 후에 생성자에서 곧바로 새로운 값을 대입

 

먼저 호출된 기본 생성자에서 해 놓은 초기화는 헛짓 -> 멤버 초기화 리스트 사용

- 초기화 리스트에 들어가는 인자는 바로 데이터 멤버에 대한 생성자의 인자로 쓰이기 때문

 

ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)

 : thename(name),

   theAddress(address),

   thePhone(phones),

   numtimesConsulted(0)

{}

// 초기화

 

- thename은 name으로부터 복사 생성자의 의해 초기화

- theAddress는 address로부터 역시 복사 생성자에 의해 초기화

- thePhone도 동일한 과정을 거쳐 phones로부터 초기화

 


 

(1) 대부분의 데이터 타입에 대해서는, 기본 생성자 호출 후 복사 대입 연산자를 연달아 호출하는 이전의 방법보다 복사 생성자를 한 번 호출하는 쪽이 더 효율적이다.

 

(2) 기본제공 타입( numtimesConsulted)의 객체는 초기화의 대입에 걸리는 비용의 차이가 없지만, 역시 멤버 초기화 리스트에 모두 넣어 주는 쪽이 가장 좋다.

 

(3) 데이터 멤버를 기본 생성자로 초기화하고 싶을 때도 멤버 초기화 리스트를 사용하는 습관

 

(4) 상수이거나 참조자로 되어 있는 데이터 멤버의 경우엔 반드시 초기화되어야 한다.

- 상수와 참조자는 대입 자체가 불가능

 

 

(5) 각 생성자마다 멤버 초기화 리스트가 붙어 있는 경우

- 같은 멤버 겹칠 수 있음

- 대입으로도 초기화가 가능한 데이터 멤버들을 초기화 리스트에서 빼내어 별도의 함수로 옮겨 사용 -> 데이터 멤버의 진짜 초기값을 파일에서 읽어온다든지 데이터베이스에서 찾아오는 경우에 유용하게 사용

 

 

(6) C++에서의 변덕스럽지 않은 부분 -> 객체를 구성하는 데이터의 초기화 순서

- 어떤 컴파일러를 막론하고 항상 똑같다.

- 기본 클래스는 파생 클래스보다 먼저 초기화

- 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화

 


 

정적 객체(static object)

(1) 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체를 일컫는다.

(2) 스택 객체 및 힙 기반 객체는 애초부터 정적 객체가 될 수 없다.

(3) 정적 객체의 범주에 들어가는 것들

- 우선 전역 객체가 있다.

- 네임스페이스 유효범위에서 정의된 객체

- 클래스 안에서 static으로 선언된 객체

- 함수 안에서 static으로 선언된 객체

- 파일 유효범위에서 static으로 정의된 객체

 

-> 함수 안에 있는 정적 객체 : 지역 정적 객체

-> 나머지는 : 비지역 정적 객체

 

(4) 정적 객체는 프로그램이 끝날 때 자동으로 소멸 ->  main() 함수의 실행이 끝날 때 정적 객체의 소멸자가 호출된다.

 

 


 

 

번역 단위(translation unit)

 

(1) 컴파일을 통해 하나의 목적 파일을 만드는 바탕이 되는 소스 코드

(2) 번역 : 소스의 언어를 기계어로 옮긴다는 의미

(3) 기본적으로는 소스 파일 하나가 되는데, 그 파일이 #include하는 파일까지 합쳐서 하나의 번역 단위가 됩니다.

 

(4) 별도로 컴파일된 소스 파일이 두 개 이상 있으며 각 소스 파일에 비지역 정적 객체가 한 개 이상 들어 있는 경우

- 한쪽 번역 단위에 있는 비정적 객체의 초기화가 진행되면서 다른 쪽 번역 단위에 있는 비지역 정적 객체가 사용되는데, 불행히도 이 객체가 초기화되어 있지 않을지도 모른다는 점

 

 

이유?

- 별개의 번역 단위에서 정의된 비지역 정적 객체들의 초기화 순서는 정해져 있지 않다 라는 사실 때문

 

 

해결 방법

- 비지역 정적 객체를 하나씩 맡는 함수를 준비하고 이 안에 각 객체를 넣는 것 -> 함수 속에서도 이들은 정적 객체로 선언하고, 그 함수에서는 이들에 대한 참조자를 반환하게 만든다.

- 사용자 쪽에서는 비지역 정적 객체를 직접 참조하는 과거의 폐단을 버리고 이제는 함수 호출로 대신 한다.

 

정리

- 비지역 정적 객체가 지역 정적 객체로 바뀐 것이다.

 

 

(5) 지역 정적 객체는 함수 호출 중에 그 객체의 정의에 최초로 닿았을 때 초기화되도록 만들어져 있다.

-> 따라서 비지역 정적 객체를 직접 접근하지 않고 지역 정적 객체에 대한 참조자를 반환하는 쪽으로 바꾸었다면, 얻어낸 참조자는 반드시 초기화된 객체를 참조하도록 맞추어 주어야 한다.

 

- 비지역 정적 객체 대신에 준비한 지역 객체 참조자 반환 함수를 호출할 일이 없다면 해당 객체의 생성/소멸 비용도 생기지 않게 막아야 한다.

 

 

(6) 어떤 객체가 초기화되기 전에 그 객체를 사용하는 일이 생기지 않도록 하려면 ?

- 멤버가 아닌 기본제공 타입 객체는 직접 초기화 -> 경우에 따라 되기도 하고 안되기 때문

- 객체의 모든 부분에 대한 초기화에는 멤버 초기화 리스트를 사용

-> 생성자에서는, 데이터 멤버에 대한 대입문을 생성자 본문 내부에 넣는 방법으로 멤버를 초기화하지 말고 멤버 초기화 리스트를 사용한다. 그리고 초기화 리스트에 데이터 멤버를 나열할 때는 클래스에 각 데이터 멤버가 선언된 순서와 똑같이 나열

 

- 별개의 번역 단위에 정의된 비지역 정적 객체에 영향을 끼치는 불확실한 초기화 순서를 염두에 두고 이러한 불확실성을 피해서 프로그램을 설계

-> 비지역 정적 객체를 지역 정적 객체로 바꾸면 된다.


 

 

 

 

 

 

출처 : 스콧 마이어스, [Effective C++ 제 3판], 곽용재, 프로텍미디어(2015), p 59-79p