본격적으로 각 항목에 들어가기 전 C++에서 기본적으로 사용되는 문법과 그 용어들에 대해서 정리합니다.
이후에 나올 내용들의 기본이 되는 내용이므로 확실히 짚어두고 넘어갑니다.
선언, 시그니처, 정의, 초기화
선언(declaration)
코드에 사용되는 어떤 대상의 이름과 타입을 컴파일러에게 알려 주는 것
extern int x; // 객체 선언
std::size_t numDigits(int number); // 함수 선언
class Widget; // 클래스 선언
template<typename T> // 템플릿 선언
class GraphNode; // "typename"의 용법에 대한 정보는 42번 항목에서 다룹니다.
정수 x를 기본제공(built-in) 타입임에도 객체라고 부르고 있습니다.
프로그래머중 "객체" 라는 용어의 정의를 사용자 정의 타입의 변수로 제한하는 부류가 있지만,
해당 책에선 그렇지 않습니다.
size_t에 대한 여담
size_t는 부호없는 정수 타입을 typedef로 정의한 것.
C++에선 개수를 셀 때 이 타입을 사용중
예 : char* 기반 문자열의 크기, STL 컨테이너의 원소 개수 등
또한 vector, deque, string의 operator[] 함수가 취하는 타입이기도 함. (항목 3 참조)
시그니처(signature)
모든 함수의 선언문에는 시그니처, 함수의 매개변수 리스트와 반환 타입이 나와 있습니다.
함수에겐 시그니처가 곧 타입입니다.
위의 코드에선 numDigits 함수의 시그니처는 std::size_t(int) 입니다.
공식적인 C++ 정의에선 함수의 반환 타입을 제외하고 있지만, 해당 책에선 반환 타입도 시그니처의 일부로 간주합니다.
정의(definition)
정의는 선언에서 빠진 구체적인 세부사항을 컴파일러에게 제공하는 것입니다.
객체의 경우에 있어서 정의는 컴파일러가 그 객체에 대한 메모리를 마련해 놓는 부분이 됩니다.
함수나 함수 템플릿에 대한 정의는 그들에 대한 코드 본문(body)를 제공하는 것입니다.
클래스 혹은 클래스 템플릿에 경우엔 클래스, 혹은 템플릿의 멤버를 넣어 준 결과가 정의 입니다.
int x; // 객체 정의
std::size_t numDigits(int number) // 함수 정의 (매개변수에 들어있는 숫자의 값을 반환)
{
std::size_t digitsSoFar = 1;
while((number /= 10) != 0) ++digitsSoFar;
return digitsSoFar;
}
class Widget { // 클래스 정의
public:
Widget();
~Widget();
};
template<typename T> // 템플릿 정의
class GraphNode{
public:
GraphNode();
~GraphNode();
};
초기화(initialization)
어떤 객체의 최초의 값을 부여하는 과정입니다.
사용자 정의 타입으로 생성한 객체의 경우 초기화는 생성자에 의해 이루어집니다.
기본 생성자(default constructor)
어떤 인자도 주어지지 않은 채로 호출될 수 있는 생성자입니다.
원래부터 매개변수가 없거나, 모든 매개변수가 기본 값을 가지고 있으면 기본 생성자가 될 수 있습니다.
class A {
public:
// 기본 생성자
A();
};
class B {
public:
// 기본 생성자
// explicit의 의미는 좀 더 아래에서 확인 가능합니다.
explict B(int x = 0, bool b = true);
};
class C {
public:
// 기본 생성자 아님.
explict C(int x);
};
클래스 B와 C의 생성자가 explicit으로 선언되어있는 것을 확인할 수 있는데, 이런 생성자들은 암시적인 타입 변환을 수행하느데 쓰이지 않게 됩니다. (명시적으로 되는 곳에는 물론 사용 가능합니다.)
// B 타입의 객체를 하나 받는 함수
void doSomething(B bObject);
// B 타입의 객체
B bObj1;
// 문제 없는 코드. B 객체를 doSomething에 넘김.
doSomething(bObj1);
// B 타입의 객체. int 인자가 28임. bool은 기본값인 true로 설정됨.
B bObj2(28);
// 에러!
// doSomething은 B를 취해야함.
// int를 doSomething으로 변환하는 암시적 변환이 존재하지 않음.
doSomething(28);
// 좋음.
// B 클래스의 생성자를 사용해 int에서 B로 명시적 변환, 즉 캐스팅함.
// (캐스팅에 대한 자세한 이야기는 항목 27 참조)
doSomething(B(28));
explicit으로 선언된 생성자는 explicit이 아닌 것들과 비교시 쓸모가 많습니다.
프로그래머가 예상하지도 못했던 타입 변환을 막아주기 때문입니다.
책의 필자는 암시적 타입 변환에 생성자가 사용될 여지를 남겨둘 뚜렷한 이유가 없는 한 생성자는 explicit 선언을 우선적으로 한다고 합니다.
여러분들도 주저 마시고 이런 식으로 해보시길 적극 추천드립니다.
복사 생성자(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;
복사 생성과 복사 대입을 구분하기가 어려운 것은 아닙니다. 객체가 새로 정의될 때(w3 처럼)는 생성자가 불려야 합니다. 대입될 수 없습니다.
새로운 객체가 정의되지 않는 상황에서는 (w1 = w2 처럼) 생성자가 호출될 리 없기 때문에 대입이 될 수 밖에 없습니다.
복사 생성자는 그 중요도에 있어서 꽤나 각별한 함수입니다.
" 값에 의한 객체 전달 (Call by Value) "을 정의해주는 함수가 바로 복사 생성자이기 때문입니다.
bool hasAcceptableQuality(Widget w);
...
Widget aWidget;
if (hasAcceptableQuality(aWidget)) {
...
}
이 코드에서 매개변수 w는 함수에 값으로 넘겨지도록 되어있습니다. 그렇기에 aWidget은 w로 복사됩니다.
이때 수행되는 복사에 Widget의 복사 생성자가 쓰이는 것입니다.
결론적으로 "값에 의한 객체 전달 (pass-by-value)" 는 곧 "복사 생성자 호출" 이라고 이해하면 됩니다.
사용자 정의 타입을 값으로 넘기는 발상은 일반적으로 좋지 않다고 알려져 있습니다.
그보다는 상수 객체에 대한 참조로 넘기기가 더 좋습니다.
자세한 이야기는 항목 20에서 확인 가능합니다.
STL (Standard Template Library)
표준 템플릿 라이브러리의 준말입니다.
표준 라이브러리에 속해있는 라이브러리이고, 컨테이너, 반복자, 알고리즘 및 이와 관련된 기능들이 집결한 결정체입니다.
여기서 컨테이너, 반복자, 알고리즘에 관련된 기능들의 상당 부분을 이른바 함수 객체 (function object), 즉 함수처럼 동작하는 C++ 객체가 차지하고 있습니다. 함수 객체는 함수 호출 연산자인 operator()을 오버로드한 클래스로 만듭니다. 코드에 안 넣으려야 안 넣을 수 없을 정도로 유용한 라이브러리이기 때문에 살짝만 써보시면 동감할 것입니다.
미정의 동작(undefined behavior)
C++에서 사용되는 구문요소들 중 몇개는 이런저런 이유로 동작 자체가 `글자 그대로 정의되어 있지 않습니다.`
쉽게 말해 실행 시간에 어떤 현상이 터질지 예측할 수 없다는 의미입니다.
아래는 그 예시 두가지입니다.
// 널 포인터
int *p = 0;
// 널 포인터를 역참조하면 미정의 동작이 발생됨.
std::cout << *p;
// 크기가 6인 (끝의 널문자 포함) 배열.
char name[] = "Darla";
// 유효하지 않은 배열 원소지정번호(index)로 참조하려면
// 미정의 동작이 발생됨.
char c = name[10];
미정의 동작의 결과는 예측이 불가하고, 동작 후의 뒷맛도 썩 좋지 않습니다.
C++ 프로그램을 잘 만드는 사람들은 미정의 동작과 멀리 떨어져 가는 코드를 만드는데 최선을 다합니다.
해당 책은 미정의 동작을 잘 잡아내고 싶을 때 어디를 잘 보아야 할 지, 어떻게 해야 할지에 대해 정확히 찍어주는 맥점에 대해 아실 수 있을 것입니다.
인터페이스(interface)
자바 및 닷넷 계얼의 언어의 경우에는 Interface가 언어 차원에서 주어져 있지만, C++에는 그런게 없습니다.
C++로도 인터페이스와 흡사하게 할 수 있고, 항목 31에서 이것에 대해 이야기 하고 있지만 말입니다.
이 책에서 나오는 이야기 중 인터페이스 라는 용어는 함수의 시그니처, 혹은 클래스의 접근 가능 요소(public 인터페이스, protected 인터페이스, private 인터페이스가 해당됩니다.) 라던지, 템플릿의 타입 매개변수 (항목 41 참조) 로서 유효해야 하는 표현식 등을 가리킵니다. 정리하면 이 책에서의 인터페이스는 지극히 평범하고 일반적인 설계 아이디어로서의 인터페이스입니다.
사용자(client)
이 책에서 사용자는 여러분이 만든 코드(인터페이스)를 사용하는 아무개 혹은 아무것 입니다.
예를 들어 함수의 사용자는 함수를 사용하는 모든 대상이 해당됩니다.
그 함수를 호출하는, 혹은 그 주소를 취하는 코드, 그런 코드를 작성하거나 유지보수하는 사람들 모두가 사용자입니다.
클래스 혹은 템플릿의 상요자는 그 클래스/템플릿을 사용하는 소프트웨어의 코드, 그 코드를 작성하고 유지보수하는 모든 프로그래머도 됩니다.
책에서 주로 언급하고 싶어하는 사용자는 프로그래머입니다. 잘못 만든 인터페이스 때문에 헷갈려 하고, 골치 썩는 쪽은 프로그래머이니까요.
사용자를 염두에 두는 일이 익숙하지 못한 독자분들도 계시겠지만 저희도 다른 사람이 개발한 코드의 사용자이고, 저희 역시 다른 사람들에게 사용될 코드의 작성자이다 보니, 인터페이스를 개발할때 "손님은 왕이다" 라는 마인드를 품고 가시길 바랍니다.
이 책에서는 함수와 함수 템플릿, 클래스와 클래스 템플릿의 구분이 그리 확실히 되어있지 않은 부분을 자주 만나게 됩니다. 한쪽이 그러하면 다른 쪽도 그러한 경우가 아주 많기에 그런데, 그렇지 않은 경우는 클래스, 함수, 이들을 찍어낸 템플릿을 명확히 구분해 두었다고 합니다.
이름짓기에 대한 규약
이 책에서 주로 자주 나오는 변수명 중 lhs와 rhs가 있습니다.
좌변 (left-hand side) 와 우변(right-hand side)의 약자인데, 이항 연산자들의 매개변수 이름으로 곧잘 사용됩니다.
// Rational은 유리수 라고 생각하시면 됩니다. (블로그 작성자 주석)
const Rational operator*(const Rational& lhs, const Rational& rhs);
연산자 함수가 멤버 함수로 만들어진 경우는 좌변 인자가 this로 표현되기 때문에 rhs만 사용되는 예시도 종종 보입니다.
class Widget {
public:
// 기본 생성자
Widget();
// 복사 생성자
Widget(const Widget& rhs);
// 복사 대입 연산자
Widget& operator=(const Widget& rhs);
...
};
이전에 나왔던 위의 예시에도 사용되었죠.
포인터의 이름을 지을 때는 타입 T의 객체에 대한 포인터는 pt라고 부릅니다.
아레는 그 예시입니다.
Widget *pw;
class Airplane;
Airplane *pa;
class GameCharacter;
GameCharacter *pgc;
참조자에 대해서도 비슷한 규칙으로 r을 붙입니다.
멤버 함수에 대해 이야기할때 때때로 mf 라는 이름을 쓰니 기억해두시면 좋을때도 있다고 합니다.
스레딩에 대한 고려사항
C++에선 언어 차원에서 스레드의 대한 개념 자체가 없습니다. 어떤 종류의 병행성도 고려하지 않은 언어입니다.
실질적으로 그렇지 않은게 일상이기도 합니다. 이 책은 표준 C++ 을 따르며 이식성을 고려한 C++ 프로그래밍 만을 염두에 두고 있지만 스레드 안전성에 대한 이야기를 덮고 지나가진 않습니다.
책의 필자는 고민 끝에 C++ 구문요소 중 스레드 환경에서 문제를 일으킬만한 것들을 지적해서 알려 주는 쪽으로 정했다고 합니다. 이 책을 다중스레드 프로그래밍 책으로 둔갑시킬 내용은 아닙니다.
TR1 그리고 부스트
TR1(Technical Report 1)
C++ 표준 라이브러리에 새로 추가되는 기능들에 대한 명세입니다.
새로 추가된 기능들은 클래스 및 함수 템플릿이 주류인데, 해시 테이블(hash table), 참조 카운팅 방식 스마트 포인터(referece-counting smart pointer), 정규 표현식(regular expression) 등입니다.
TRI의 모든 구성요소는 tr1 네임스페이스에 들어있고, tr1 네임스페이스는 std 네임스페이스 안에 중첩되어 있습니다.
Boost
비슷한 고수들 사이 교차 검증을 거쳤고 플랫폼 간 이식성도 가진 오픈소스 C++ 라이브러리를 제공하는 단체입니다. 이 단체의 웹사이트를 가르키기도 합니다. TR1의 구성요소들 중 대부분은 이곳에서 만들어졌습니다. 컴파일러 공급업체들이 그들의 C++ 라이브러리 출시본에 TR1을 포함하기 전엔느 개발잗르이 TR1 라이브러리를 찾아 떠돌다가 들르는 첫 번째 장소이기도 합니다.
'개발 > [시리즈] Effective C++' 카테고리의 다른 글
Effective C++ : 0장. 번역 용어 정리 (0) | 2024.06.12 |
---|