이 책은 c와 c++의 차이점을 이야기 하는것부터 시작되며, c++가 어떻게 객체지향을 이루고 있는지를 객체지향의 도입, 전개, 완성의 목차로 다루고 있다. 나는 대학교에서 이미 c언어와 c++언어를 어느정도 배웠기 때문에 가볍게 읽을 수 있었고, 중간중간 새로 알게된 흥미로운 내용도 있었다. 느낌점보다는 요약으로 문법적 형식보다는 개념과 목적을 중심으로 작성하였다. 그리고 중간중간 내 생각으로 다음어 작성한 부분이 있다.
C++로의 전환
c++는 기본적으로 c의 내용을 포함하고 있다. c와 c++의 차이는 다음과 같다.
c | c++ | |
---|---|---|
입출력 | printf / scanf | cout / cin |
함수 오버로딩 | X | O |
매개변수 디폴트 값 | X | O |
인라인 함수 | X | O |
이름공간 | X | O |
bool 자료형 | X | O |
참조자 & | X | O |
동적 메모리 할당 | malloc / free | new / delete |
라이브러리 형식 | ex) stdio.h | ex) cstdio / iostream |
객체지향의 도입
객체지향 프로그래밍이란
객체지향 프로그래밍
이란 물체 또는 추상적 개념을 프로그램 상의 객체
라는 데이터로 모사해 문제를 해결하려는 프로그래밍이다. 이러한 객체는 클래스
를 통해 만들 수 있다. 클래스란 틀이다. 하나의 클래스는 여러개의 객체를 찍어낼 수 있다. 객체 안에는 멤버변수, 멤버함수가 있다.
클래스
c++의 클래스와 비교되는 것은 구조체이다. 구조체란 관련있는 데이터와 함수들의 묶음으로, 클래스는 여기에 더해 접근제어 지시자
(public, private, protect)가 포함되어 있다. 접근제어 지시자를 통해 정보은닉
을 할 수 있다. 정보은닉이란 외부에서 객체 안에 변수에 접근할 수 없게 만드는 것이다. 정보은닉의 목적은 프로그래머의 실수를 방지하는 것이다. 또한 정보은닉을 통해 캡슐화
를 할 수 있다. 캡슐화란 관련있는 함수와 변수를 하나의 클래스 안에 묶는 것이다. 캡슐화의 목적은 객체의 활용을 쉽게 만들고, 프로그램의 복잡도를 낮추는 것이다.
생성자와 소멸자
객체는 탄생과 죽음이 있고, 이는 클래스의 생성자(Constructor)
와 소멸자(Destructor)
로 표현된다. 생성자는 주로 멤버변수를 초기화하는 역할을 한다. 이때 멤버 이니셜라이저
를 사용하면 선언과 동시에 초기화하는 효과를 보이기 때문에 대입 연산자보다 효율적이며, const와 참조자 멤버변수도 초기화할 수 있다.
만약 매개변수가 같은 클래스의 객체이면 복사생성자가 호출되는데, 말그대로 객체의 모든 멤버변수가 복사된다. 이를 얕은 복사(Shallow Copy)
라 한다. 문제는 포인터 멤버변수가 복사되면 두 객체의 포인터 멤버변수가 하나의 데이터를 가리킨다는 것이다. 한쪽에서 데이터를 수정하면 반대쪽에도 영향을 주고, 만약 한쪽에 의해 데이터가 삭제라도 되면 큰 문제가 발생한다. 따라서 포인터 멤버변수를 만나면 새로운 데이터를 만들어 데이터 값을 복사해야한다. 이를 깊은 복사(Deep Copy)
라 한다. 복사생성자가 호출되는 시점은 3가지가 있다. 생성자로 직접 호출했을 때(대입연산자를 사용했을 때), 객체를 매개변수로 받아올 때, 함수의 반환형으로 객체가 반환될 때이다. 소멸자는 생성자에서 앞에 ‘~’가 붙은 형태이다. 소멸자는 메모리 누수를 막기위해 동적 할당된 메모리 공간을 소멸시켜야 한다.
생성자에서 주의할 점은 복사생성자의 매개변수는 참조형이 아니면 무한루프에 빠진다는 점, 객체를 생성할 때 ClassA a()의 형태로 작성하면 함수 선언과 동일한 형식이기 때문에 오류가 생긴다는 것이다.
각종 키워드
키워드 | 설명 |
---|---|
const | 변수를 수정할 수 없게 한다. 함수에 붙으면 그 함수는 변수를 수정하지 않는다 |
static | 전역변수에 붙으면 선언된 파일 안에서만 참조가능하다. 함수 내부의 변수에 붙으면 한번만 초기화되고, 함수를 빠져나가도 소멸되지 않는다. 클래스 멤버변수에 붙으면 클래스 당 하나만 생성된다. 클래스 멤버함수에 붙으면 static 멤버변수에만 접근이 가능하다. |
friend | 지정된 클래스는 private 영역에 접근할 수 있게한다. |
mutable | 이 키워드가 붙은 변수는 const 함수에서 값 변경이 가능하다. |
explicit | 대입연산자가 복사생성자로 대체되는 것을 방지한다. |
객체지향의 전개
상속
상속(inheritance)
이란 어떤 클래스의 멤버를 다른 클래스의 멤버로 포함시키는 것이다. 이를 통해 얻는 이점은 크게 두가지가 있다. 첫째, 코드 재사용이 가능하다. 둘째, 같은 클래스를 상속받은 클래스들은 모두 상속한 클래스의 자료형으로 다룰 수 있다. 두번째 이점에서 슈퍼클래스 타입으로 서브클래스 객체를 형 변환(casting)하는 것을 Up Casting
이라 한다. Down Casting
도 있는데 업캐스팅된 객체를 원래 타입으로 복귀하는 것이지 업캐스팅의 반대 개념이 아니다.
서브클래스는 슈퍼클래스의 멤버함수의 내용을 재정의할 수 있는데 이를 함수 오버라이딩
이라 한다. 하지만 업캐스팅된 객체는 오버라이딩되지 않고 캐스팅된 슈퍼클래스의 멤버함수를 가리킨다. 오버라이딩된 서브클래스 멤버함수를 가리키기 위해선 슈퍼클래스 멤버함수에 virtual 키워드를 붙여 가상함수
로 만들어 해결할 수 있다. 가상함수를 통해 같은 문장이지만 다른 결과를 보일 수 있는데, 이것이 상속의 대표적 특성 다형성(polymorphism)
이다. 하나 이상의 가상함수를 가지는 클래스를 Polymorphic class
라고 한다. 또한 업캐스팅되는 객체는 메모리 누수를 막기위해 소멸자를 가상 소멸자
로 만들어야 한다. 만약 가상함수의 구현부가 없으면 이를 순수 가상함수
라 하고 이를 가진 클래스를 추상 클래스(abstract class)
라 한다. 추상 클래스는 상속을 목적으로 만들어진 클래스로 혼자서는 객체가 생성될 수 없다.
클래스 멤버함수는 실제로 클래스 외부에 위치하고 각 객체가 함수를 공유한다. 클래스가 가상함수를 포함하면 컴파일러가 가상함수 테이블(V-table, virtual table)
을 만드는데, 멤버함수가 어디에 위치하는 지에 대한 정보가 있다. 함수가 가상함수라면 마지막으로 오버라이딩된 함수를 가리킨다. 또한 가상상속
이란 개념이 있다. 이것은 다중상속
에서 한 클래스가 중복으로 상속되는 문제를 해결할 수 있다.
객체지향의 완성
연산자 오버로딩
c++의 연산자는 함수이다. 따라서 함수처럼 오버로딩할 수 있다. 연산자를 오버로딩 하는데 주의사항은 본래의도를 유지해야 한다는 것, 연산자의 우선순위 결합성은 바꿀 수 없다는 것, 매개변수 디폴트 설정이 불가하다는 것, 연산자의 기본 기능을 바꿀 수 없다는 것이다. 연산자는 매우 많은 부분에서 쓰이고 있다.
사실 c++의 cout과 cin은 객체이고 시프트 연산자를 이용해 입출력을 하는것이다. 따라서 ‘»’, ‘«’ 연산자를 오버로딩하여 새로운 객체에 대한 입출력 기능을 만들 수 있다.
또한 클래스는 대입연산자가 정의되지 않으면 자동으로 디폴트 대입 연산자
를 갖는다. 디폴트 대입연산자는 얕은복사를 하기 때문에 깊은복사를 위해선 대입 연산자 함수를 정의해야한다.
new와 delete 또한 연산자이다. new 연산자의 기능은 1.메모리 공간의 할당, 2.생성자의 호출, 3.반환된 주소값을 알맞게 형변환이다. 여기서 1번만 오버로딩을 할 수 있다. 나머지는 컴파일러가 처리하기 때문이다. new 연산자 함수는 바이트 단위로 계산된 메모리 공간의 사이즈를 매개변수로 받고, 그 크기만큼의 메모리 공간의 주소값을 반환해야 한다.
포인터 연산자(‘*’, ‘->’)를 오버로딩하여 스마트 포인터
를 만들 수 있다. 스마트 포인터는 포인터처럼 동작하고, 객체를 자동으로 delete 연산을 해주는 등 추가적인 기능이 있는 객체이다.
함수 호출 연산자 ‘()’로 펑터(Functor)
다른 이름으로 함수 오브젝트(Function Object)
를 만든 수 있다. 펑터는 함수처럼 사용할 수 있는 객체이다.
String 클래스 디자인
c++의 String은 문자열 처리를 쉽게하는 것을 목적으로 정의된 클래스이다. String은 다음의 기능이 있다.
- 스트링 클래스는 문자열 리터럴로 생성이 되어야한다.
- 그리고 문자열 처리를 위해 ‘+’, ‘+=’, ‘=’, ‘==’ 연산들에 대해 오버로딩이 되어있어야 한다.
- 문자열이 변경될 때 마다 char 배열을 새로 동적할당한다.
- cout, cin으로 입출력을 하기위해 ‘»’, ‘«‘연산자를 오버로딩해야한다.
템플릿(Template)
템플릿(Template)
은 다양한 자료형의 함수 또는 클래스를 만들기 위한 문법이다. 템플릿으로 만들어진 함수나 클래스를 템플릿 함수
, 템플릿 클래스
라 하고, 이들을 만들기 위한 원형을 함수 템플릿
, 클래스 템플릿
이라 한다. 템플릿의 원리는 특정한 자료형의 템플릿 함수나 클래스가 호출되면 컴파일러가 그것을 생성하는 것이다. 템플릿 함수나 클래스를 자료형에 따라 다르게 처리해야 하는 경우 미리 정의할 수도 있는데, 이를 템플릿의 특수화
라 한다. 템플릿의 자료형은 두개 이상도 가능하며, 복수의 자료형 중 일부만 특수화하는 것도 가능하다. 이를 템플릿의 부분 특수화
라고 한다. 템플릿에서 정해지지 않은 자료형을 대신하는 문자를 템플릿 매개변수
라고 하는데, 디폴트 값을 지정할 수 있다. 또한 템플릿 매개변수에 변수도 올 수 있다.
예외처리(Exception Handling)
예외처리를 위해 try, catch, throw 문법이 있다. try 블록에서 예외가 발생하면, throw가 catch블록에 예외 데이터를 전달하고, catch는 이를 처리하는 것이다. try블록을 묶는 기준은 하나의 일과 관련된 모든 문장이다. try블록안에서 throw가 실행되면 그 이후 문장은 건너뛴다. throw는 꼭 try 블록안에 있지 않아도 된다. 함수안에서 throw가 실행되면 함수를 호출한 영역으로 예외 데이터(더불어 예외처리에 대한 책임까지)가 전달된다. 이를 스택 풀기(Stack Unwinding)
라 하고, 이러한 특성은 예외가 발생한 위치와 예외를 처리해야 하는 위치가 달라야할 경우 유용하다. 만약 스택이 전부 풀려 main 함수에 도달했는데 try-catch문이 없으면 terminate 함수가 호출되면서 프로그램이 종료된다. catch 블록은 여러 개일 수 있으며 전달받아야할 예외 데이터의 자료형에 따라 구분이 된다. throw가 어떤 자료형의 예외 데이터를 전달하는지 알려주기위해 함수에 명시할 수 있다.
예외 클래스
란 예외 데이터 전달을 목적으로 만들어진 클래스이다. 예를들어 new 연산에서 메모리 공간 할당에 실패하면 bad_alloc이라는 예외 클래스의 객체가 전달된다.
c++의 형변화 연산
c++는 형변환에서 프로그래머의 실수를 방지하기위해 추가적으로 4개의 용도에 따라 연산자를 제공한다. 참고로 이 연산자들은 오버로딩 불가하다.
연산자 | 설명 |
---|---|
dynamic_cast |
업캐스팅, (Polymorphic 클래스일 경우) 다운캐스팅 |
static_cast |
조금 제한이 있는 형 변환 |
reinterpret_cast |
제한없이 형 변환 |
const_cast |
const 성향 제거, volatile 성향 제거 |
…끝(2022. 4. 8)