본문 바로가기
프로그래밍 언어/C, C++

[Effective C++] EC++ 정리하기 - Prechapter: 독자 여러분 반갑습니다

by 니키티스 2025. 5. 18.

C++을 공부하면서, 실제로 깊이까지 공부해 본 적은 없다는 생각이 들어 책 'Effective C++'을 읽고 배운 점을 정리하고자 하였습니다. 잘못된 점이나 오류가 있다면 지적 바랍니다.

해당 글은 Effective C++을 정리한 내용이지만, 제가 새로 배우거나 느낀 점 위주로 정리하였습니다. 책 자체가 워낙에 잘 쓰여서, 필요하다면 직접 해당 책을 읽어보시길 권장드립니다.


이번 글에서는 도입부에서 알아놓고 가면 좋을 것들을 정리했다.

 

1. 암시적 타입 변환을 막기 위해 생성자는 explicit을 사용하자

생성자를 만들 때, 암시적 타입 변환으로 객체를 생성해서 넣기를 바라지 않는 경우가 있다.

이 경우, explicit으로 생성자를 만들자.

class B {
public:
  explicit B(int x = 0, bool b = true); // 기본 생성자
};

void doSomething(B bObject); // B 타입 객체를 하나 받는 함수

B bObj1;
doSomething(bObj1); // 문제 없이 성공
B bObj2(28); // 28로부터 성공적으로 B 객체 생성 성공.
doSomething(28); // Error! doSomething은 B를 취해야 하는데, explicit으로 인해 int에서 B로 암시적 변환을 할 수 없다.
doSomething(B(28)); // B 클래스 생성자로 명시적으로 int에서 B로 변환했으므로, 성공

explicit으로 생성자를 만들면 doSomething(28)과 같이 int에서 암시적으로 B를 만들어서 매개변수로 넘기는 경우를 막을 수 있어 좋다고 한다.

2. 복사 생성자

복사 생성자(copy constructor): 어떤 객체의 초기화를 위해 그와 같은 타입의 객체로부터 초기화할 때 호출되는 함수

복사 대입 연산자(copy assignment operator): 같은 타입의 다른 객체에 어떤 객체의 값을 복사하는 용도의 함수

class Widget {
public:
  Widget(); // 기본 생성자
  Widget(const Widget& rhs); // 복사 생성자
  Widget& operator=(const Widget& rhs); // 복사 대입 연산자
  ...
};

Widget w1; // 기본 생성자 호출
Widget w2(w1); // 복사 생성자 호출
w1 = w2; // 복사 대입 연산자 호출

문제는 대입문처럼 보이는 것이 대입이 아닐 수도 있다는 점.

‘=’ 문법은 복사 생성자를 호출하는 데도 쓰일 수 있다.

Widget w3 = w2; // 복사 생성자가 호출된다!

복사 생성과 복사 대입을 구분하는 기준은 나름 간단하다.

  • 어떤 객체가 새로 생성될 때(ex: 위 문장의 w3)는 생성자가 불려져야 한다. 절대 대입이 될 수 없다.
  • 새로운 객체가 정의되지 않는 상황에서는(”w1=w2” 문장처럼) 생성자가 호출될 수 없으므로, 대입이 호출된다.

복사 생성자는 ‘값에 의한 객체 전달’을 정의하는 함수이므로, 중요도가 높다.

bool hasAcceptableQuality(Widget w);
...
Widget aWidget;
if (hasAcceptableQuality(aWidget)) ...⁠⁠

이 코드에서 매개변수 w는 함수에 값으로 넘겨지도록 되어 있다. 그래서 실제 호출 시 aWidget은 w로 복사되어, 복사 생성자가 쓰이게 된다.

‘값에 의한 전달(pass-by-value)’는 ‘복사 생성자 호출’이라 이해하면 된다.

(다만, 사용자 정의 타입을 값으로 넘기는 발상은 일반적으로 좋지 않다고 알려져 있다. ‘상수 객체에 대한 참조로 넘기기’가 더 좋다고 한다.)

3. 미정의 동작(undefined behavior)

Java, C#하다가 C++ 하려고 하면 당황하는 부분이니 주의하는 게 좋다.

C++에서 사용하는 구문요소 중 일부는 동작 자체가 글자 그대로 ‘정의되어 있지 않다’. 쉽게 말해, 실행 시간에 어떤 현상이 터질지 확실히 예측할 수 없다.

예를 들어,

int *p = 0; // nullptr
std::cout << *p; // 널포인터를 역참조하면, 미정의 동작이 발생한다.

char name[] "Darla"; // name은 크기가 6(끝에 붙은 널 문자 포함!)인 배열
char c = name[10]; // 유효하지 않은 배열 원소지정번호(index)로 참조하면 미정의 동작 발생

위 같이 작성하면 미정의 동작이 발생한다.

미정의 동작의 결과는 예측이 불가능하고 동작 후의 뒷맛도 썩 좋지 않다. 그래서 경험치가 높은 고수 프로그래머는 “프로그램에서 미정의 동작이 발생하면 하드 드라이브까지 날려버릴 수 있다”라고 한다. 이는 비유적인 표현으로 대부분은 그렇게까지 되는 경우는 없다. 그렇지만 분명 실행 결과를 알 수 없는 코드를 만들어 낼 가능성이 있으므로, 좋은 C++ 프로그래머는 미정의 동작과 멀리 떨어져 가는 코드를 만들기 위해 최선을 다한다고 한다. 그러니 앞으로 개발할 땐 이런 미정의 동작을 만들지 않도록 최선을 다하자.

댓글