면접 대비 CS 질문 요약 (13~18)

나무에물주기·2022년 12월 18일
1
post-thumbnail
post-custom-banner

면접 대비 CS 질문 (13~18)

13. 함수 소멸자에서 virtual(가상함수)을 붙이는 이유

  • 먼저 상속을 받은 클래스의 생성과 소멸 과정을 보면, 생성자는 부모 클래스의 생성자에서 자식 클래스 생성자 순서로 불려지고, 소멸자는 자식 클래스의 소멸자가 먼저 불려지고 나서 부모 클래스의 소멸자가 불러진다.

  • 그런데 다형성을 이용하기 위해 부모 클래스의 포인터로 자식 클래스를 호출할 때, 가상 함수로 정의되어 있지 않은 자식 클래스의 오버라이된 함수를 호출하면 부모 클래스의 멤버 함수가 호출된다. 소멸자도 자식 클래에서 오버라이딩된 함수라고 볼 수 있기 때문에 만약 부모 포인터로 객체를 삭제하면 부모 클래스의 소멸자가 호출된다.

  • 따라서 소멸자를 가상 함수로 선언하지 않으면 이 경우 자식 클래스의 소멸자는 결코 호출되지 않는다. 가상 함수 키워드 virtual이 사용되었으면 이것은 자식 클래스에서 재정의 될 수 있음을 명시하기 때문에 포인터의 종류에 상관없이 항상 자식 클래의 메서드가 호출된다. 즉 자식 클래스의 소멸자가 호출 되고 나서 부모 클래스의 소멸자가 호출된다.

  • 따라서 상속 관계가 있고 소멸자에서 리소스를 해제해야 하는 경우 반드시 소멸자를 가상 함수로 선언해야 한다.

#include <iostream>
using namespace std;
class classA
{
public:
  classA();
  virtual ~classA();
};
class classB : public classA
{
public:
  classB();
  ~classB();
};
classA::classA()
{
  cout << "A" << endl;
}
classA::~classA()
{
  cout << "~A" << endl;
}
classB::classB()
{
  cout << "B"<< endl;
}
classB::~classB()
{
  cout << "~B" << endl;
}
int main()
{
  cout << "START" << endl;
  classB *B = new classB;
  classA *A = B;
  delete A;
  return 0;
}
결과 1 - classA 소멸자에 virtual을 쓰지 않았을 때
START
A
B
~A

결과 2 - classA 소멸자에 virtual을 사용했을 때
START
A
B
~B
~A

14. 멀티스레드 값이 맞지 않는 이유와 해결방법

  • 멀티스레드란 하나의 프로세스를 다수의 실행 단위로 구분하여 자원을 공유하고 자원의 생성과 관리의 중복성을 최소화하여 수행 능력을 향상시키는 것을 멀티 스레드라고 한다.

  • 멀티스레드는 자원을 공유하기 때문에 서로의 자원을 동시에 접근하려 할 때 서로 교착 상태(Deadlock)에 빠질 수 있다. 따라서 멀티스레드 환경에서는 Deadlock이 발생하지 않도록 동기화하는 작업이 필요하다.

  • A.V.에러(Access Violation error)
    가장 심각한 상황 중에 하나는 특정 스레드가 할당 받은 메모리 공간에서 작업하고 있는 동안, 다른 스레드가 메모리 할당을 해제하는 경우입니다.
    이렇게 여러 개의 스레드가 같은 자원을 공유하면서 발생하는 문제들은 임계영역을 사용하여 해결할 수 있습니다. 임계영역은 일종의 신호등이며 자물쇠의 역활을 합니다.
    그래서 임계영역을 락(lock)이라고도 부릅니다. 어떤 자원을 사용할 때, 사용중인 스레드가 락을 걸어서 다른 스레드는 접근할 수 없도록 하는 것입니다. 자원 사용을 마친 스레드가 락을 풀어주면 다른 스레드가 다시 락을 걸어서 사용하게 됩니다.

15. 상속 함수의 호출 순서

  1. 상속 관계에서 파생 클래스는 기반 클래스의 생성자를 먼저 호출한 뒤 그 다음에 파생 클래스의 생성자를 호출한다.

  2. 소멸자는 생성자의 역순으로 호출된다, 즉 파생클래스 소멸자 → 기반클래스 소멸 순으로 호출

  3. 파생클래스 : public기반 클래스 → 외부에서도 기반클래스의 public변수 및 함수 접근 가능

  4. 파생클래스 : private기반 클래스 → 외부에서 기반클래스의 모든 변수 및 함수 접근 불가능

  5. protected 접근 제어자는 파생클래스가 기반클래스의 변수 또는 함수에 접근할 수 있도록 해준다.

16. 포인터와 레퍼런스(참조자)

  • 포인터는 메모리의 주소를 가지고있는 변수이다. 주소 값을 통한 메모리 접근을 한다.

  • 레퍼런스는 자신이 참조하는 변수를 대신할 수 있는 또 다른 이름이다. 변수명을 통해서 메모리를 참조한다.

포인터와 레퍼런스의 차이

  1. 포인터는 NULL 초기화를 할 수 있지만, 레퍼런스는 NULL 초기화를 할 수 없다. 레퍼런스는 반드 선언과 동시에 초기화를 해야한다. 이러한 특성 때문에, 포인터는 가리킬 대상을 변경할 수 있지만, 레퍼런스는 불가능하다.

  2. 포인터는 주소 값을 저장하기 위해 별도의 메모리 공간을 소모한다. 반면, 레퍼런스는 같은 메모리 공간을 참조하므로 메모리 공간을 소모하지 않는다.

  3. call by pointer : 매개 변수로 함수 인자 전달시, 메모리 소모가 일어나고, 값 복사가 발생된다.

  4. call by reference : 메모리 소모가 없고, 값 복사 또한 발생하지 않는다.

17. Dynamic_Cast & Static_Cast

  • Dynamic_Cast : 안정적인 형 변환을 보장한다, 프로그램 실행 동안에 안정성을 검사하도록 컴파일러가 바이너리 코드를 생성한다, 속도가 느리다, 안정적이지 못한 형 변환을 시도 하는 경우 NULL을 반환한다.

  • Static_Cast : 안전성을 보장하지 않는(프로그래머가 책임), 컴파일러 무조건 형 변환이 되도록 바이너리 코드를 생성한다.

18. 스마트 포인터

Reference : http://www.tcpschool.com/cpp/cpp_template_smartPointer

  • C++ 프로그램에서 new 키워드를 사용하여 동적으로 할당받은 메모리는, 반드시 delete 키워드를 사용하여 해제해야 한다. C++에서는 메모리 누수(memory leak)로부터 프로그램의 안전성을 보장하기 위해 스마트 포인터를 제공하고 있다.

  • 스마트 포인터의 동작 : 보통 new 키워드를 사용해 기본 포인터(raw pointer)가 실제 메모리를 가리키도록 초기화한 후에, 기본 포인터를 스마트 포인터에 대입하여 사용한다. 이렇게 정의된 스마트 포인터의 수명이 다하면, 소멸자는 delete 키워드를 사용하여 할당된 메모리를 자동으로 해제한다. 따라서 new 키워드가 반환하는 주소값을 스마트 포인터에 대입하면, 따로 메모리를 해제할 필요가 없어진다.

  • 스마트 포인터의 종류 : unique_ptr, shared_ptr, weak_ptr

1. unique_ptr

unique_ptr은 하나의 스마트 포인터만이 특정 객체를 소유할 수 있도록, 객체에 소유권 개념을 도입한 스마트 포인터입니다.

이 스마트 포인터는 해당 객체의 소유권을 가지고 있을 때만, 소멸자가 해당 객체를 삭제할 수 있습니다.

unique_ptr 인스턴스는 move() 멤버 함수를 통해 소유권을 이전할 수는 있지만, 복사할 수는 없습니다.

소유권이 이전되면, 이전 unique_ptr 인스턴스는 더는 해당 객체를 소유하지 않게 재설정됩니다.

예제

unique_ptr<int> ptr01(new int(5)); // int형 unique_ptr인 ptr01을 선언하고 초기화함.

auto ptr02 = move(ptr01);          // ptr01에서 ptr02로 소유권을 이전함.

// unique_ptr<int> ptr03 = ptr01;  // 대입 연산자를 이용한 복사는 오류를 발생시킴. 

ptr02.reset();                     // ptr02가 가리키고 있는 메모리 영역을 삭제함.

ptr01.reset();                     // ptr01가 가리키고 있는 메모리 영역을 삭제함.

C++14 이후부터 제공되는 make_unique() 함수를 사용하면 unique_ptr 인스턴스를 안전하게 생성할 수 있습니다.

make_unique() 함수는 전달받은 인수를 사용해 지정된 타입의 객체를 생성하고, 생성된 객체를 가리키는 unique_ptr을 반환합니다.

이 함수를 사용하면, 예외 발생에 대해 안전하게 대처할 수 있습니다.

2. shared_ptr

shared_ptr은 하나의 특정 객체를 참조하는 스마트 포인터가 총 몇 개인지를 참조하는 스마트 포인터입니다.

이렇게 참조하고 있는 스마트 포인터의 개수를 참조 횟수(reference count)라고 합니다.

참조 횟수는 특정 객체에 새로운 shared_ptr이 추가될 때마다 1씩 증가하며, 수명이 다할 때마다 1씩 감소합니다.

따라서 마지막 shared_ptr의 수명이 다하여, 참조 횟수가 0이 되면 delete 키워드를 사용하여 메모리를 자동으로 해제합니다.

shared_ptr<int> ptr01(new int(5)); // int형 shared_ptr인 ptr01을 선언하고 초기화함.

cout << ptr01.use_count() << endl; // 1

auto ptr02(ptr01);                 // 복사 생성자를 이용한 초기화

cout << ptr01.use_count() << endl; // 2

auto ptr03 = ptr01;                // 대입을 통한 초기화

cout << ptr01.use_count() << endl; // 3

위의 예제에서 사용된 use_count() 멤버 함수는 shared_ptr 객체가 현재 가리키고 있는 리소스를 참조 중인 소유자의 수를 반환해 줍니다.

위와 같은 방법 이외에도 make_shared() 함수를 사용하면 shared_ptr 인스턴스를 안전하게 생성할 수 있습니다.

make_shared() 함수는 전달받은 인수를 사용해 지정된 타입의 객체를 생성하고, 생성된 객체를 가리키는 shared_ptr을 반환합니다.

이 함수를 사용하면, 예외 발생에 대해 안전하게 대처할 수 있습니다.

3. weak_ptr

weak_ptr은 하나 이상의 shared_ptr 인스턴스가 소유하는 객체에 대한 접근을 제공하지만, 소유자의 수에는 포함되지 않는 스마트 포인터입니다.

shared_ptr은 참조 횟수(reference count)를 기반으로 동작하는 스마트 포인터입니다.

만약 서로가 상대방을 가리키는 shared_ptr를 가지고 있다면, 참조 횟수는 절대 0이 되지 않으므로 메모리는 영원히 해제되지 않습니다.

이렇게 서로가 상대방을 참조하고 있는 상황을 순환 참조(circular reference)라고 합니다.

weak_ptr은 바로 이러한 shared_ptr 인스턴스 사이의 순환 참조를 제거하기 위해서 사용됩니다.

profile
개인 공부를 정리함니다
post-custom-banner

0개의 댓글