c++ 테스트 정리 1

c++ 테스트 정리 1

* c++ 테스트 정리 (1)


  • 이 포스트는 Do it C++ 완전 정복을 베이스로 작성되었습니다.

  • Do it C++의 깃허브는 이쪽입니다. Do It! C++ 완전 정복

  • 단!돈! 32000원에 c++ 완전정복쌉가능 (바이럴맞음)

Chapter 3) 포인터와 메모리

3-1 포인터와 메모리

Q1) 포인터 변수에 대해 정의하시오.

A) 포인터 변수는 메모리 주소를 저장하는 변수이다.

Q2) 배열에 대해 정의하시오.

A) 배열은 여러 변수의 묶음으로, 같은 자료형의 변수들이 연속적으로 메모리에 저장된 형태이다.

Q3) new, delete를 이용해 정수 50개를 수용할 수 있는 배열을 생성한 후 해제하는 코드를 작성하시오.
int* arr = new int[50];
delete[] arr;

3-2 함수와 구조체

Q1) 함수에 대해 정의하시오.

A) 함수란 특정한 역할을 수행하는 코드의 집합이다.

Q2) 구조체에 대해 정의하시오.

A) 구조체란 하나 이상의 자료형을 묶어 만든 새로운 형태의 사용자 지정 자료형이다.

Q3) int 형식 포인터 변수를 매개변수로 가지는 함수 정의를 코드로 작성하시오.
void    func(int* pointer);

3-3 정적 변수와 상수 변수

Q1) 정적 변수의 수명 주기가 지역 변수와 다른 이유를 설명하시오.

A) 정적 변수와 지역 변수의 수명 주기가 다른 이유는, 두 변수가 저장되는 메모리의 위치가 다르기 때문이다. 메모리 구조는 코드 영역, 데이터 영역, 힙 영역, 스택 영역으로 구성되는데 지역 변수는 스택 영역에 저장된다. 스택 영역의 변수는 함수가 호출될 때 메모리 할당되며 종료될 때 메모리에서 해제된다. 하지만 static으로 선언된 정적 변수는 데이터 영역에 저장된다. 데이터 영역은 프로그램이 시작할 때 할당되며 종료할 때 해제된다.

Q2) const int *ptrint *const ptr의 차이점을 설명한 후, 아래 코드에서 컴파일 오류가 나는 지점을 체크하고, 그 이유를 설명하시오.
#include <iostream>

// 1번 main 문
int main()
{
    int a = 0;
    const int *ptr = &a;
    
    a = 1;
    *ptr = 2;
    return 0;
}

// 2번 main 문
int main()
{
    int a = 0;
    int b = 1;
    int *const ptr = &a;
    
    a = 1;
    ptr = &b;
    return 0;
}

1번 main 문의 const int *ptr은 ptr 포인터 변수가 가리키는 *ptr 값이 상수화된 것으로 변수 a 자체가 상수화된 것은 아니다. *ptr이 상수화되었으므로 *ptr = 2에서 컴파일 오류가 발생한다. 2번 main 문의 int *const ptr은 ptr 포인터 변수 자체를 const로 지정한 것으로, 다른 변수인 b의 주소로 변경할 수 없기 때문에 ptr = &b에서 컴파일 오류가 발생한다. 마찬가지로 a가 상수화된 것은 아니기 때문에 이 부분은 컴파일에 문제가 없다.

-> 결국 기억할 것은 const가 붙은 오른쪽 값이 상수화된다는 것이다.
const int *ptr => int *ptr이 상수화됨 -> (*ptr = 2) 처럼 변경 불가.
int *const ptr => ptr이 상수화됨 -> (ptr = &b) 처럼 변경 불가.

3-3 레퍼런스 변수

Q1) 레퍼런스 변수의 정의를 서술하시오.
레퍼런스 변수는 변수의 또다른 이름으로 별칭 변수이다.
Q2) 레퍼런스 변수를 사용할 때 지켜야 할 점 3가지를 서술하시오

A) 첫 번째, 레퍼런스 변수는 선언할 때 반드시 참조할 원본 변수를 지정해야 한다. 두 번째, 참조할 대상이 지정된 레퍼런스 변수는 다른 변수를 참조하도록 변경해서는 안된다. 세 번째, 레퍼런스 변수로 상수를 참조해서는 안된다.

Q3) 레퍼런스와 포인터의 차이를 서술하시오.

A) 레퍼런스로 할 수 있는 기능은 포인터로도 할 수 있다. 포인터는 강력하지만 능숙하게 다루기 어렵다. 포인터를 다루다가 문제가 발생할 수도 있다. 레퍼런스는 포인터를 안전하게 사용할 수 있도록 만든 도구이다. 포인터처럼 원본 값에 접근할 수 있지만 원본 자체나 공간의 크기, 메모리 주소 등은 변경할 수 없다.

Chapter 4로 넘어가기 전에…

Q1) 동적으로 할당한 메모리를 반드시 해제해야 하는 이유는 무엇인지, 또 일반 변수를 메모리 해제하지 않아도 되는 이유에 대해 서술하시오.

동적 메모리 할당은 런타임에 필요에 따라 메모리를 할당한다. 하지만 일반 변수는 프로그램 시작 시 컴파일 타임에 컴파일러에 의해 메모리가 할당된다. 사용이 끝난 동적 메모리가 프로그램 종료 후에도 해제되지 않으면 메모리 누수가 발생한다. 메모리가 누수되어 사용량이 증가하면 시스템 성능 저하 및 충돌을 야기한다. 동적 메모리 할당 및 해제는 프로그래머에게 메모리 사용에 대한 직접적인 제어 권한을 제공한다. 이를 통해 프로그래머는 필요에 따라 메모리를 효율적으로 할당하고 해제하여 메모리 사용량을 최소화하고 프로그램 성능을 향상시킬 수 있다.

일반 변수는 프로그램 종료 시 컴파일러에 의해 자동으로 메모리가 해제되어 프로그래머가 직접 해제할 필요가 없다. 또한 일반 변수는 함수 범위 내에서만 유효하며 함수 종료 시 자동으로 메모리가 해제된다. 따라서 메모리 누수가 발생할 가능성이 낮다. 반면 동적 메모리는 프로그램 종료 전까지 유지될 수 있으며, 명확하게 해제하지 않으면 메모리 누수가 발생할 수 있다.

Q2) 값에 의한 호출, 참조에 의한 호출, 주소에 의한 호출의 차이를 서술하시오.

값에 의한 호출은 함수 호출 시 인자의 값을 복사하여 함수 내에서 사용한다. 참조에 의한 호출은 함수 호출 시 인자의 주소를 복사하여 함수 내에서 사용한다. 주소에 의한 호출은 값에 의한 호출과 비슷하지만 인자 값을 복사하는 대신 포인터를 사용하여 직접 값을 변경한다.

Chapter 4) 실행 흐름 제어

Q1) 표현식과 구문의 정의에 대해 서술하고, 둘의 관계를 설명하시오.

표현식은 하나 이상의 변수, 연산자, 리터럴을 조합해 값을 평가하고 결과를 반환하는 코드를 의미한다. 구문은 하나 이상의 연산을 수행하거나 동작을 실행하는 명령문의 집합으로 값을 할당하거나 프로그램의 실행 흐름을 제어한다. 구문은 컴파일러가 이해하고 실행할 수 있는 최소의 독립된 코드 조각이다. 코드 한 줄부터 여러 줄까지 구문으로 볼 수도 있다. 따라서 구문은 한 개 이상의 표현식과 키워드를 포함할 수 있다. 따라서 구문이 표현식을 포함하는 관계로 볼 수 있다.

Q2) switch 분기를 활용하기 좋은 시점을 서술하시오.

조건부 분기가 많을 때, 분기 코드의 가독성을 높이기 위해 사용할 수 있다.

Q3) break 키워드의 기능을 서술하시오.

분기나 반복문에서 탈출하는 기능을 수행한다.

Q4) while문과 do-while문의 차이를 서술하시오.

두 반복문은 조건식을 평가하는 시점에 차이가 있다. while문은 반복을 실행하기 전에 조건을 평가하고, do-while문은 먼저 반복을 실행한 후 조건을 평가한다.

Q5) int result = a + b에는 표현식과 구문이 모두 포함되어 있다. 어느 부분이 표현식이고 구문인지 표시하시오.
표현식 : a + b
구문 : int result = a + b;

int result = a + b;  표현식 a + b 결과를 result 변수에 저장하는 구문이다.

Chapter 5) 예외 처리 하기

Q1) 예외 처리란 무엇인지 서술하시오.

A) 예외 처리는 프로그램 실행 흐름상 발생하는 오류에 대응하는 것이다.

Q2) assertion을 이용한 예외 처리에 대해 서술하시오.

assertion은 코드를 검증하여 예상치 못한 상황에서 프로그램 동작을 중단시키는 도구로, 코드의 안전성과 신뢰성을 높여준다. c++에서는 assert 매크로를 통해 예외를 간단하게 처리할 수 있다. assert를 이용해 프로그램 특정 지점에서 true일 것으로 예상되는 조건을 지정한다. 만약 지정한 조건이 true가 아니면 프로그램 실행이 중단되며, 어떤 오류가 발생했는지 출력된다.

Q3) noexcept를 이용한 예외 처리 생략에 대해 서술하시오.

함수가 예외를 던지지 않음을 나타낼 때 noexcept 키워드를 사용할 수 있다. 함수가 예외를 던지지 않음을 명시하면 컴파일러가 코드를 최적화하고 빠르게 실행하는데 도움이 된다. 그러나 함수에 noexcpet 키워드가 붙었다고 해서 예외를 던지지 못하는 것은 아니다. noexcept가 명시된 함수에서 예외가 발생하면 컴파일 경고 없이 런타임에 terminate()를 호출하며 프로그램이 종료된다.

Q4) set_terminate를 이용한 예외 처리 실패 대응에 대해 서술하시오.

런타임에 오류가 발생하면 내부적으로 terminate() 함수가 호출되며 프로그램이 종료되는데, 이때 종료 처리 함수를 설정하는 set_terminate로 프로그램이 강제 종료되기 전에 특정 동작을 수행하도록 구성할 수 있다. set_terminate(my_function)과 같이 사용할 수 있다.

2. 객체지향 프로그래밍

Chapter 6) 객체지향과 클래스

6-1 객체 지향 이전의 프로그래밍 패러다임

Q1) 절차적 프로그래밍과 그 효과를 기술하시오.

절차적 프로그래밍은 소스 코드를 여러 부분으로 나눠 활용하는 패러다임으로, 프로시저를 이용해 구조화하는 방식을 말한다. 프로시저는 일련의 코드 묶음으로, 보통 함수를 의미한다. 대표적으로 C언어, 포트란이 있다. 절차적 프로그래밍에선 코드의 논리 구조를 모듈화할 수 있다. 모듈화하면 같은 기능을 수행하는 코드를 다시 작성하지 않아도 재사용할 수 있으며 누군가가 만든 라이브러리 등을 이용하면 프로그램을 더 쉽게 개발할 수 있다. 또한 구조화된 코드는 다른 사람이 쉽게 읽을 수 있는 장점이 있다.

Q2) 절차적 프로그래밍의 한계를 기술하시오.

절차적 프로그래밍은 프로그래밍 대상을 논리 구조인 프로시저로 표현하는 것이 매우 복잡하다. 프로시저가 가진 논리적 다층 구조를 프로그래밍 내부에서 표현하는 데 한계가 있다. 불필요한 프로시저를 호출하거나 전역 변수를 수정할 수 있다. 이때 프로그램 동작에 치명적인 영향을 줄 수 있다.

6-2 객체지향 프로그래밍

Q1) 객체지향 프로그래밍을 정의하시오.

객체지향 프로그래밍이란 데이터와 함수를 포함하는 객체를 활용하는 프로그래밍 패러다임이다. 다양한 객체 간의 관계로 원하는 목적의 소프트웨어 프로그램을 완성한다.

Q2) has-a 관계와 is-a 관계에 대해 서술하시오.

객체 간의 상속이나 포함 관계에서 상위, 하위 객체가 교환되는 관계를 is-a 관계라 하고, 교환이 불가능한 관계를 has-a 관계라고 한다.

Q3) 객체지향 프로그래밍의 특징에 대해 기술하시오.

객체지향 프로그래밍의 특징에는 추상화, 캡슐화, 상속성, 다형성이 있다. 추상화는 현실 세계의 사물을 모델링하여 객체로 만들 때, 어떤 부류에서 불필요한 요소는 배제하고 공통된 특징만을 추출하는 것이다. 캡슐화는 복잡한 내부 기능을 묶어 외부에 불필요한 정보를 감추는 것이다. 상속은 파생 객체가 부모 객체의 특성을 이어받는 것이다. 다형성은 상속받은 객체가 자신만의 기능을 반영하여 다른 방식으로 동작하는 것이다.

6-3 클래스와 인스턴스

Q1) 클래스의 정의에 대해 서술하시오.

A) 클래스는 객체가 포함하는 데이터와 함수를 정의하는 문법적 요소이다.

Q2) 객체와 클래스의 차이와, 객체와 클래스의 관계에 대해서 서술하시오.

A) 객체와 클래스의 관계는 자료형과 변수의 관계와 비교할 수 있다. 객체는 클래스를 사용할 수 있도록 만든 변수이다. 객체와 클래스는 다대일 관계이다.

Chapter 7로 넘어가기 전에…

Q1) 비구조적 프로그래밍과 절차적 프로그래밍의 장단점을 비교하여 각 패러다임을 대표하는 언어가 무엇인지 기술하시오.

A) 비구조적 프로그래밍은 코드를 구조화하지 않고 작성한다. 순차적인 흐름 이해만으로 소스 코드를 이해할 수 있다. 하지만 복잡한 구조를 표현하기 어렵고, 스파게티 코드가 만들어질 가능성이 있다. 절차적 프로그래밍은 프로시저를 이용해 구조화하는 방식이다. 프로시저로 모듈화를 구현하여 복잡한 구조를 표현할 수 있고 개념화가 쉽다. 다만 접근 제어가 어렵고 전역 범위에 대한 접근이 불가피하다. 비구조적 프로그래밍 언어로는 Assembly와 초기 fortran이 있고, 절차적 프로그래밍 언어로는 C, COBOL, fortran 등이 있다.

Chapter 7 객체지향 프로그래밍 특징

7-1 추상화와 캡슐화

Q1) 객체지향 프로그래밍에서 캡슐화를 통해 얻을 수 있는 장점은 무엇인지 서술하시오.

A) 속성(멤버 변수)과 행위(멤버 함수)를 하나의 클래스로 묶어서 관리함으로써 복잡도를 낮추고 수정하기가 용이하다. 또한 클래스 외부에서 내부의 정보에 접근할 수 없도록 은닉(hiding)하기 때문에 보안성을 높이고 실수를 방지할 수 있다.

Q2) C++에서 추상화는 어떤 의미인지 서술하시오.

A) C++의 추상화는 현실의 사물들을 모델링하여 객체를 만들 때, 공통된 부분은 추려내고 불필요한 부분은 제거하여 코드를 간결하고 이해하기 쉽게 만드는 작업이다.

7-2 상속성과 다형성

Q1) 객체지향 프로그래밍에서 상속성이 무엇이고 왜 중요한지 서술하시오.

A) 상속성은 기본 클래스로부터 파생 클래스가 생성될 때, 그 속성과 기능을 이어받아 확장하는 것을 의미한다. 상속을 통해 논리적 포함 관계와 공통의 특질을 명확하게 모델링할 수 있다. 상속은 클래스를 설계하고 재사용하는 핵심적인 개념이기 때문에 중요하다.

Q2) 다형성 구현을 위해 C++에서 사용하는 문법이나 기법은 무엇인지 서술하시오.

A) 다형성은 하나의 클래스가 다양한 형태의 속성과 기능을 가질 수 있다는 것을 의미한다. 파생 클래스는 기본 클래스의 역할을 대신할 수 있으면서 고유한 특징을 가지고 동작한다. 다형성을 구현하기 위해 C++에서는 가상 함수를 이용한 함수의 오버라이딩을 사용한다.

Q3) 가상 함수를 사용하지 않고 오버라이딩하면 어떤 문제가 발생하는지 서술하시오.

A) 기본 클래스 포인터로 파생 클래스에 접근하면 자식 클래스의 오버라이딩된 함수가 아닌 부모 클래스의 함수가 호출된다.

I4) 오버라이딩과 오버로딩 추가 정보

부모의 멤버 함수와 같은 시그니처로 자식 클래스에서 재정의하는 것을 오버라이딩이라고 한다. 그리고 함수 이름만 같고 매개변수 구성이 다른 함수를 만드는 것을 오버로딩이라고 한다. 오버라이딩 함수는 네임스페이스로 원본함수를 선택해서 호출하지만, 오버로딩 함수는 호출 인자에 따라서 알맞은 함수가 호출된다.

7-3 생성자와 소멸자

Q1) 소멸자를 가상 함수로 정의해야 하는 때는 어떤 경우인지 서술하시오.

A) 다형성을 구현하기 위해 기본 클래스 포인터로 자식 객체를 처리하였을 때, 기본 클래스 소멸자를 가상 함수로 정의해야 한다.

Q2) 사용자가 복사 생성자를 직접 정의하지 않으면 객체를 대입할 때 어떤 복사가 이루어지는지 서술하시오.

A) 사용자가 복사 생성자를 직접 정의하지 않으면 객체를 대입할 때 얕은 복사가 이루어진다.

Q3) 정적 멤버 변수의 초기화 위치는 어디인지, 레퍼런스 변수는 무엇을 사용해서 초기화해야 하는지 서술하시오.

A) 정적 멤버 변수는 전역 범위에서 초기화되어야 하고, 레퍼런스 변수는 반드시 초기화자 리스트를 사용해야 한다. 생성자 본문에서는 이미 객체가 생성된 상태이고, 이는 객체가 생성되기 전에 모든 멤버가 유효한 상태로 존재해야 한다는 C++의 규칙을 위반하기 떄문이다.

7-4 자신을 가리키는 this 포인터

Q1) 객체 자신을 레퍼런스 또는 포인터로 반환하여 연속된 호출 형태를 만드는 방법을 무엇이라고 하는가?

A) 멤버 함수 체이닝

I2) 멤버 함수는 클래스 메모리 영역이 아닌 코드 메모리에 위치해서 같은 클래스로 갱성한 객체가 공유하기 때문에 this 포인터를 활용하여 멤버 변수를 구분해야 한다.

7-5 함수와 연산자 오버로딩

Q1) 오버로딩과 오버라이딩은 어떤 차이가 있는지 서술하시오.

A) 자식 클래스에서 부모 클래스의 멤버와 시그니처가 같은 함수를 재정의하는 것을 오버라이딩이라 하고, 함수 이름은 같지만 매개변수 구성이 다른 함수를 중복 정의하는 것을 오버로딩이라고 한다.

I2) 함수의 이름이 다르다면 그것은 새로운 함수이며, 매개변수 구성이 그대로인데 반환 형식만 바뀌었다면 컴파일 오류가 발생한다.

7-6 접근 지정자와 friend

I1) protected 접근 지정자는 자식 클래스에서 접근 가능하고 외부에서는 접근 불가능하다. private 접근 지정자는 자식 클래스에서도 접근 불가능하고 외부에서도 접근 불가능하다
I2) friend 키워드를 사용하면 접근 지정자가 무력화되어 외부에서도 private, protected 멤버에 접근할 수 있다. 따라서 private 멤버 함수를 특정 클래스에서 사용하려면 friend 키워드를 사용할 수 있다.

Chapter 8로 넘어가기 전에…

Q1) 객체지향 프로그래밍의 4가지 특징의 핵심을 요약하여 서술하시오.

A) 객체지향 프로그래밍의 특징으로는 추상화, 캡슐화, 상속성, 다형성이 있다. 추상화는 여러 개의 복잡한 자료, 모듈, 시스템 등으로부터 공통된 핵심 개념이나 기능으로 간추리는 것을 말한다. 불필요한 부분은 제거하고 공통된 특징은 추출하여 클래스를 간결하게 만들어 이해하기 쉽도록 한다. 캡슐화는 객체의 구체적인 정보와 동작을 외부로 노출하지 않도록 은닉하는 것이다. 멤버 함수와 멤버 변수를 클래스로 묶어 관리하여 프로그램의 복잡도는 낮아지고 재사용성은 높아진다. 상속성은 기반 클래스로의 속성과 기능을 파생 클래스가 이어받아 확장하는 것이다. 상속을 통해 논리적 포함 관계와 공통의 특징을 명확하게 모델링할 수 있다. 다형성은 하나의 클래스가 여러 속성을 지니고, 다양한 기능을 할 수 있음을 의미한다. 기반 클래스에서 정의한 함수를 자식 클래스에서 재정의한 후 기반 클래스로 사용할 수 있다. 이를 통해 기능이 확장, 변경 되어도 코드의 변경을 최소화할 수 있다.

Chapter 8 객체지향을 돕는 기능들

8-1 컴포지션과 어그리게이션

Q1) 다중 상속의 문제점 중 하나인 “클래스가 커지고 컴파일 시간이 늘어나는 것”에 대해 상술하시오.

A) 다중 상속이 많아질수록 클래스는 거대해진다. 거대 클래스는 속성과 기능이 많아 사용하기 어렵기 때문에 만드는 것을 지양해야한다. 그리고 기반 클래스 중 일부가 변경되면 상속받은 모든 클래스를 다시 컴파일해야 하는데, 파생 클래스가 많거나 다양한 라이브러리에서 사용되면 변경 사항이 여러 곳에 영향을 주므로 바람직하지 않다. 이렇게 상속이나 사용 관계로 의존도가 높아지면 결합도가 높다고 하는데, 소스 코드는 결합도가 낮을 수록 유지보수가 수월하다. 거대 클래스는 결합도를 높이는 주요한 원인이므로 피해야 한다. c++은 컴파일 언어이므로 소스 코드가 변경되면 다시 컴파일해서 실행 파일을 만들어야 한다. 상속 관계가 복잡하고 다중 상속이 복잡하게 얽혀 있으면 컴파일에 시간이 많이 소모되어 개발에 부담이 가게 된다.

Q2) 상속과 컴포지션의 차이에 대해 서술하시오.

A) 상속과 컴포지션은 메모리에 생성되고 소멸되는 관점에서 비슷하지만, 코드가 분리되어있다는 점이 다르다. 즉 분리된 클래스가 변경될 때 이 클래스의 객체를 멤버 변수로 포함하는 클래스는 변경하지 않아도 되며, 이에 따라 컴파일을 유도하지 않는다. 따라서 동적 바인딩이 가능하다.

I3) 상속은 부모 클래스를 자식 클래스가 대체할 수 있어야 할 때 사용하며, 컴포지션은 클래스의 공통 기능이나 단위 기능을 클래스로 분리할 때 사용한다.
I4) 컴포지션과 어그리게이션의 중요한 구별점은 생명 주기이다. 이에 따라 분리된 클래스와 포함하는 클래스의 관계를 컴포지션은 part-of이라고 하며, 어그리게이션은 has-a라고 한다.

8-2 가상함수와 동적 바인딩

Q1) 가상 함수에 대해 정의하고 어떤 상황에서 사용할 수 있는 서술하시오.

A) 가상 함수는 다형성을 구현하기 위해 함수를 오버라이딩하는 특별한 방법이다. 객체가 업캐스팅된 후에 호출해도 자식 클래스의 오버라이딩 함수를 호출해야 할 때 사용한다.

I2) 가상 소멸자를 사용하지 않으면 업캐스팅된 클래스의 객체를 생성했을 떄 부모 클래스의 소멸자만 호출되어 자식 클래스의 객체에 필요한 후처리를 할 수 없게 된다.
I3) 가상 함수는 가상 함수의 위치를 저장할 수 있는 가상 함수 테이블이 있기 때문에 동작한다.

8-3 추상 클래스와 정적 멤버

I1) 추상 클래스는 순수 가상 함수가 포함된 클래스를 말한다. 추상 클래스로 객체를 선언할 수 없다. 그 이유는 순수 가상 함수의 정의가 없어서 메모리에 인스턴스를 생성할 수 없기 때문이다.
I2) 정적 클래스 멤버는 객체 생성없이 함수와 변수를 사용할 수 있다. 전역 변수/함수와 다른 것은 접근 지정자를 통해 클래스 외부에서 사용할 수 없도록 강제할 수 있다는 점이다.

Chapter 9 객체지향 설계 원칙

Q1) 5가지 객체지향 설계 원칙인 SOLID를 영어와 한글로 풀어서 설명하시오.

A) 단일 책임 원칙(single responsibility principle), 개방 폐쇄 원칙(open-close principle), 리스코프 치환 법칙(Liskov substitution principle), 인터페이스 분리 원칙(interface segregation principle), 의존성 역전 법칙(dependency inversion principle)

I2) 개방 폐쇄 원칙은 특히 소프트웨어 개발과 유지 보수에 있어 중요한 원칙이다.

I3) 의존성 역전 원칙은 구체적인 것 보다는 추상적인 것에 의존해야 한다는 원칙이다. C++에서는 추상 클래스와 인터페이스를 사용해 의존성 역전 원칙을 준수한다.

Chapter 10으로 넘어가기 전에…

Q1) SOLID의 각 알파벳이 의미하는 원칙을 설명하고, 객체지향의 4가지 특징 가운데 어떤 특징이 적용되는지 기술하시오.

A) 단일 책임 원칙(single responsibility principle), 개방 폐쇄 원칙(open-close principle), 리스코프 치환 법칙(Liskov substitution principle), 인터페이스 분리 원칙(interface segregation principle), 의존성 역전 법칙(dependency inversion principle). 객체지향 특성 4가지와 연결되는 SOLID 원칙은 다음과 같다.

  • 추상화 - LSP / 캡슐화 - ISP / 상속성 - ISP, DIP / 다형성 - LSP, OCP

이렇게 딱 잘라서 말하기가 어려워보이긴하는데…


(1) SRP : 클래스는 한 가지의 기능만을 해야 하며, 한 가지의 이유만으로 수정되어야 한다. -> SRP는 상속 관계보다 컴포지션, 어그리게이션을 적극 활용하여 수정 범위를 한 클래스에 갇히게 하는 것으로 지킬 수 있다.

(2) OCP : 확장에 열려 있고, 수정에 닫혀 있어야 한다. -> OCP는 추상 클래스(인터페이스)를 통해 구현할 수 있다. 주요 기능은 추상 클래스를 통해 작성하고, 이를 상속받아 구현하는 클래스에 따라 세부적인 동작을 결정한다(템플릿 메서드 패턴).

(3) LSP : 하위 클래스는 상위 클래스를 대체할 수 있어야 한다. -> 파생 클래스가 기반 클래스를 완전히 대체할 수 있는 관계를 ‘is-a’ 관계라고 하는데, is-a 관계로 정의된 클래스는 리스코프 치환 원칙에 따르는 클래스이며, 이는 파생 클래스가 부모 클래스로 업캐스팅이 가능하다는 점과, 파생 클래스에서 기반 클래스의 멤버 함수를 상속받아 오버라이드하거나 유지해야 한다는 것을 의미한다. (다형성의 동작 원리)

(4) ISP : 인터페이스는 작고 섬세해야 하며, 클래스는 필요한 인터페이스만 구현해야 한다. -> ISP는 단일 책임 원칙을 인터페이스에 적용한 것으로 생각할 수 있다. 인터페이스가 단일 책임 원칙을 준수하려면 작고 섬세해야 하며, 클래스는 역할에 특화된 최소한의 인터페이스를 구현해야만 한다.

(5) DIP : 상위 수준 모듈은 하위 수준의 모듈에 의존해 는 안 되며 상위/하위 수준 모두 추상 레이어(인터페이스)에 의존해야 한다. -> DIP는 개방 폐쇄 원칙을 적용하기 위해 사용된다. 인터페이스를 사용하지 않고 상위 수준 모듈이 하위 수준의 모듈을 의존하도록 설계하면, 하위 클래스의 추가를 통해 기능을 추가할 때 더하여 상위 클래스까지 수정해야 한다. 상위/하위 클래스가 모두 인터페이스에 의존하도록 만들면 상위 클래스를 변경하지 않고도 하위 클래스를 추가함으로써 기능을 확장할 수 있기 때문에 개방 폐쇄 원칙도 함께 적용된다.


Chapter 10 템플릿

I1) 함수 템플릿이나 클래스 템플릿에서 사용한 템플릿 매개변수는 소스 코드를 작성할 때가 아니라 컴파일할 때 추론된 형식으로 인스턴스화된다.

I2) 템플릿 매개변수로 정의한 함수나 클래스의 멤버 함수는 전체 매개변수의 데이터 형식을 지정하는 명시적 특수화와 일부 매개변수의 데이터 형식을 지정하는 부분 특수화가 있다. 부분 특수화는 클래스 템플릿에서만 가능하고 명시적 특수화는 함수 템플릿과 클래스 템플릿에서 모두 사용할 수 있다.

추가 14-4 스마트 포인터

Q1) 메모리와 리소스 관리를 효율적으로 할 수 있는 RAII 패턴에 대해 설명하시오.

A) RAII는 Resource Acquisition Is Initialization의 앞 글자를 따서 만든 단어로, 리소스 할당은 초기화다라고 직역할 수 있다. RAII 패턴의 핵심은 리소스가 필요할 때 이미 할당되어 있고, 리소스가 필요 없어질 때 객체와 함께 해제되어 객체 내의 변숫값이 객체와 함께 일정하게 유지되는 클래스 불변성이다. RAII 패턴의 주요 특징은 동적으로 할당된 메모리가 생성된 범위를 벗어나면 자동으로 해제되는 것이다.

모던 C++에서 RAII 패턴을 구현할 수 있는 클래스는 무엇인지 서술하시오.

A) C++11부터 제공되는 unique_ptr는 포인터 객체에 RAII 디자인 패턴을 적용한 범용 스마트 포인터 클래스로, RAII 패턴이 적용되어 있기 때문에 메모리의 사용이 끝나 스코프를 벗어나게 되면 자동으로 해제된다. 따라서 개발자가 객체의 생명 주기를 직접 관리하면서도 메모리 관리에 많은 신경을 쓰지 않아도 된다.