Post

[ STUDY/Effective C++ ] 2022. 6. 23. 23:41

항목5를 보면 알 수 있다시피 컴파일러는 클래스에 대해 기본생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 만들 수 있다.

 

그러면 복사가 되지않게 복사 생성자 자체를 만들지 않을 수 없는걸까?

 

1. 해당 멤버함수를 private으로 선언

 

class HomeForSale {
public:
	...

private:
	...
	HomeForSale(const HomeForSale&);
	HomeForSale& operator=(const HomeForSale&);
};

결론은 컴파일러에서 자동으로 생성하는것을 방지하고자하는 멤버함수를 따로 private으로 선언만하고 구현을 하지않으면 되는일이다.

 

2. Uncopyable 상속

class Uncopyable {
protected: 
	Uncopyable() {}//생성과 소멸 허용
	~Uncopyable() {}

private:
	Uncopyable(const Uncopyable&) //복사 방지
	Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale: private Uncopyable {
...
};

아니면 기본 클래스인 Uncopyable 을 상속받으면 되는일이다. 좀 더 깔끔하긴하다.

이 Uncopyable이라는 클래스를 보면  생성과 소멸을 허용하는건 protected내에서 선언을 해준다. 

그리고 앞의 방법과 똑같이 방지가 되었으면 하는 멤버함수의 경우에는 private으로 선언을 한다. 

 

 

3. noncopyable 상속

Uncopyable과 같은 기능을 하나, 부스트 라이브러리안에 있는 클래스이다.

Post

[ STUDY/Effective C++ ] 2022. 6. 23. 21:51

컴파일러는 우리를 대신해서 기본생성자, 복사생성장, 복사 대입 연산자, 소멸자 등 기본형 함수를 만들어낸다.

 

하지만 의 경우에는

std::string newDog("Persephone");
std::string oldDog("Satch");

NameObject<int> p(newDog, 2);
NameObject<int> s(oldDog, 36);

p=s;

 

참조자 p는 원래 자신이 참조하고 있는것과 다른 객체를 참조할 수 없다. 그렇다고 값 자체가 바뀌는것은 대입연산에 직접관여하지 않는 객체(string에 대한 포인터나 참조자를 품는 다른 객체들)까지 영향을 받게된다.

 

그렇게 된다면 컴파일 거부가 뜰 수 있다.

이럴 경우를 대비해서 복사 대입 연산자의 경우에는 직접 정의를 해야한다.

Post

[ STUDY/Effective C++ ] 2022. 6. 23. 20:41

대입(assignment)과 초기화(initialization)를 헷갈리지않기

class People {...};

class Person {
public:
	Person(const std::string& name, const std::int& age);
    
private:
	std::string theName;
	std::string theAge;
};

Person::Person(const std::string& name, const std::int& age)
{
	theName = name;
	theAge = age;
}

위 코드는 초기화가 아니라 대입이다. 

Person::Person(const std::string& name, const std::int& age)
	: theName(name),
	  theAge(age)
{}

위 코드는 대입이 아닌 초기화이다.

앞의 코드는 초기화를 따로하고, 대입을 따로 하기때문에 처음 초기화한 값은 헛것이 되어버린다.

하지만 멤버 초기화 리스트를 이용하면, 복사 생성자를 통해 들어가는 인자가 바로 데이터 멤버에 대한 생성자의 인자로 쓰인다. 이런 면에서 앞의 코드보다 좀 더 효율적이다.

 

Person::Person(const std::string& name, const std::int& age)
	: theName(), //theName의 기본ctor(생성자)호출
	  theAge()
{}

이렇게 매개변수가 있든없든간에 멤버 초기화 리스트에 모두 넣어주는 쪽이 좋다. 

 

비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.

정적 객체 : 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체 (스택, 힙기반 객체는 애초에 될 수 없음)

                  - 전역 객체

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

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

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

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

 

함수 안에 있는 개체 -> 지역 정적 객체 (함수라는 지역성을 가짐)

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

 

별개의 번역 단위에서 정의된 비지역 정적 객체들의 초기화 순서는 정해져있지않다.

이러한 사실로 인해 초기화되지도 않은 객체를 사용할 일이 생길수도 있다는것이다.

 

이는 비지역 정적 객체를 지역 정적 개체로 바꾸면 해결되는 일이다.< 단일체 패턴 >

비지역 정적 객체를 하나씩 맡는 함수를 준비 - 안에 객체를 넣음 - 함수에서는 그 객체에 대한 참조자를 반환하게함.

=> 사용자가 직접 비지역 정적 객체를 참조x , 함수호출로 대신함! 

Post

[ STUDY/Effective C++ ] 2022. 6. 18. 18:24

const의 장점

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

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

- 클래스 내부에서 정적 멤버 및 미정적 데이터 멤버 모두를 상수로 선언할 수 있다.

- 포인터 자체를 상수로, 포인터가 가리키는 데이터를 상수로 지정할 수 있다.

 

 

상수 포인터

char greeting[] = "Hello";
char *p = greeting; //비상수 포인터, 비상수 데이터

const char *p = greeting;// 비상수 포인터, 상수 데이터

char * const p = greeting; //상수 포인터, 비상수 데이터

const char* const p = greeting; //상수 포인터, 상수 데이터

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

const가 *의 오른쪽에 있으면 포인터 자체가 상수

 

void f1(const Widget *pw);

void f2(Widget const *pw)

두 가지 형태 모두 함수가 받아들이는 매개변수 타입(상수 Widget 객체에 대한 포인터)이 똑같다.

 

 

반복자

const std::vector<int>::iterator iter = vec.begin();
*iter = 10;
++iter; //에러!

////////////////////////////////////////////////////
const std::vector<int>::const_iterator iter = vec.begin();
*iter = 10; //에러!
++iter;

반복자를 const로 선언하는것은 포인터를 상수로 선언하는것과 같다. 

반복자는 자신이 가리키는 대상이 아닌것을 가리키는 경우가 허용되지 않지만, 반복자가 가리키는 대상 자체는 변경이 가능하다. 만약 불가능한 객체를 가리키는 반복자가 필요하다면 const_iterator를 쓰면 된다.

 

그냥 iterator은 가리키는 대상의 변경이 가능하지만, const_iterator은 변경이 불가능하다.

 iterator은 상수이기때문에 대상의 값을 변경하는게 불가능하지만, const_iterator은 변경이 가능하다.

 

 

함수선언

함수 반환 값, 매개변수, 멤버 함수 앞, 함수 전체 에 대해서 const의 성질을 붙일 수 있다.

함수 반환값을 상수로 정해 주면, 안전성과 효율을 가지고가며 에러 돌발상황을 줄일 수 있다.

 

 

상수 멤버 함수

멤버 함수에 붙는 const키워드의 역할 : 해당 멤버 함수가 상수 객체에 대해 호출될 함수란것을 알려줌

 

이 역할의 중요성

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

그 클래스로 만들어진 객체를 변경할 수 있는 함수는 무엇이고, 또 변경할 수 없는 함수는 무엇인가를 사용자가 알아야함

 

2. 이 키워드를 통해 상수 객체를 사용할 수 있게 함

상수 상태로 전돨된 객체를 조작할 수 있는 const 멤버 함수(상수 멤버 함수) 가 준비되어야 c++ 프로그램의 실행 성능을 높이는 객체 전달을 상수객체에 대한 참조자로 진행할 수 있다.

 

 

오버로딩

const 키워드의 유무로 멤버함수들의 오버로딩이 가능하다.

 

 

비트수준 상수성(물리적 상수성)과 논리적 상수성

비트수준 상수성은 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 멤버함수가 'const'임을 인정하는 개념이다.

C++에서 정의하고 있는 상수성이 비트수준 상수성이다.그래서 상수 멤버 함수는 그 함수가 호출된 객체의 어떤 비정적 멤버도 수정할 수 없게되어있다.

class CTextBlock{
public:
	...
    char& operatorr[] (std::size_t position) const
    { return pText[position];}
    
private:
	char *pText;
};



const CTextBlock cctb("Hello");
char *pc = &cctb[0];
*pc = 'J'; //cctb는 Jello가 된다.

코드를 보면 알다시피 operator함수가 비트수준 상수성이 있음으로 인해 상수 멤버 함수로 선언되어있다.

(원래라면 틀린거지만, 코드 내에서 pText를 건드리지 않기때문에 허용)

 

하지만, 어떤 값으로 초기화된 상수 객체를 하나 만들어 놓고 여기다 상수 멤버 함수를 호출하였더니 값이 변했다!

(pText를 건들지않아서 컴파일러가 넘어가줬는데!)

 

이를 보완하는것이 바로 논리적 상수성이다.

상수 멤버 함수라고해서 객체의 한 비트도 수정이 불가능한게 아니라, 몇 비트정도는 바꿀 수 있게하는것이다. 

class CTextBlock {
public:
	...
    std::size_t length() const;
    
private:
	char *pText;
    std::size_t textlength;
    bool lenthIsValid;
};

std::size_t CTextBlock::length() const
{
	if( !lengthIsValid) {
    	textLength = std::strlen(pText);
        lengthIsvalid = true;
}

 return textLength;
 }

위 코드에서 상수 멤버 함수 안에서는 textLength, lengthIsValid에 대입을 나타내는 코드인 만큼 비트수준 상수성과는 멀리 떨어져있다. 이 경우 컴파일러에서 오류를 띄우게된다.

 

    mutable std::size_t textlength;
    mutable bool lenthIsValid;

이때, mutable을 쓰게된다면 비정적 데이터 멤버를 비트수준 상수성에서 벗어나게해준다.

 

 

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

똑같은 역할을 하는 함수가 const의 유무에따라서 한번 더 쓰여 코드 중복이 된다면 컴파일시간, 유지보수, 코드 크기 등의 신경써야할것들이 늘어나게된다. 물론 보기에도 안좋고!

 

class TextBlock {
public:
	...
    const char& operator[] (std::size_t position) const
    {
    ...
    ...
    ...
    return text[position];
    }
    
    char& operator[] (std::size_t position)
    {
     return
     	const_cast<char&>( //캐스팅 적용, const떼어냄
        
         static_cast<const TextBlock&> //*this의 타입에 const붙임,op[]상수 버전 호출
          (*this) [position] 
       );
    }
    ...
};

해결법으로 캐스팅을 쓰며 안성정도 유지하며 코드중복을 피하는 방법은 비상수 operator[]가 상수 버전을 호출하도록 구현하는것이다. 총 두번의 캐스팅을 썻는데, 첫번째 캐스팅은 *this에 const를 붙이기 위함(static_cast)이고 두번째 캐스팅은 상수operator[]의 반환 값에서 const를 떼어내는 캐스팅(const_cast)이다. 

 

 

 

 

 

 

 

 

▲ top