728x90

다트 언어에서 비동기 처리를 지원합니다. 비동기 처리는 어떤 호출이 끝나기를 마냥 기다리는 것이 아닌 일단 호출 후 다른 작업 후 추후에 처리를 해주게 됩니다. 이 비동기 처리는 파일 IO, 데이터베이스 처리, 네트워크와 같이 언제 끝날 지 모르는 작업에 유용하게 사용됩니다. 

동기/비동기에 대한 윈도우 시스템이 궁금하시다면 이 링크를 클릭해주세요.

 

다트에서 비동기 처리를 위해 사용되는 예약어는 async, await, Future을 통해 진행됩니다. 설명에 앞서 간단한 예제를 보여드리겠습니다.

void main() {
  print('Hello World!');
  PrintValue(); 
  print('Main End');
}

Future PrintValue() async {
  print('PrintValue Start');
  var value = await Getvalue();
  print(value);
  print('PrintValue End');
}

int Getvalue() {
  return 1;
}

비동기 처리에 대한 개념 없이 봤을 때 예상되는 호출 결과는

Hello World!

PrintValue Start

1

PrintValue End

Main End

가 될 것입니다. 하지만 실제 실행 결과를 보시면 아래와 같습니다.

PrintValue 함수에서 await가 붙은 함수 다음 동작을 기다리지 않고 Main End부터 실행합니다. 그 이후 Getvalue의 결과를 출력하고 마지막으로 PrintValue End를 출력합니다. 

이 결과는 future async await로 인해 비동기로 처리가 되는데 PrintValue 함수 뒤에 붙은 async로 PrintValue 함수를 비동기 함수로 만들겠으며 await가 붙은 GetValue 함수를 비동기 처리를 하겠다는 의미입니다. 그렇기 때문에 PrintValue 함수 안의 GetValue 다음 행동은 GetValue가 마무리 될 때 까지 기다렸다 실행히 되고 PrintValue 이후는 그대로 진행이 되는 것입니다.

 

위 구성에서 await를 사용하기 위해서 Future는 생략해도 가능하지만 async를 생략하면 에러가 발생합니다.

await의 반환 값을 사용하는 방법에는 위와 같이 사용하는 방법과 then을 사용하는 방법이 있습니다. 그러기 위해선 호출하는 함수의 반환 타입이 Future 클래스에 타입을 명시해줘야 합니다.

void main() {
  print('Hello World!');
  PrintValue(); 
  print('Main End');
}

Future PrintValue() async {
  print('PrintValue Start');
  await Getvalue().then((value) => {
    print(value)
  });
  
  print('PrintValue End');
}

Future<int> Getvalue() async {
  return 1;
}

728x90

'Programming > Flutter' 카테고리의 다른 글

[Flutter] Dart 문법  (0) 2022.11.23
[Flutter] 레이아웃과 위젯  (0) 2022.11.20
[Flutter] 프로젝트 내 폰으로 실행  (0) 2022.11.20
[Flutter] 기본 프로젝트 설명  (0) 2022.11.18
[Flutter] 개발 환경 세팅  (0) 2022.11.17
728x90

플러터 개발 환경 세팅에 앞서 먼저 현재 모바일 시장은 안드로이드와 IOS로 나뉠 수 있고 각 환경의 어플 개발을 위해서 안드로이드는 자바, 코틀린 언어를 통해 개발을 하며 IOS의 경우 Object-C, Swift를 통해 개발을 하고 있습니다. 이렇게 각 운영체제에 맞는 언어로 개발한 것은 네이티브 앱이라고 합니다. 반대로 두 운영체제를 구분하지 않고 구별할 수 있는 방법으로 웹앱, 프로그레시브 웹앱, 하이브리드 앱 등이 등장했습니다.

그러더니 자바스크립트를 사용하는 리액트 네이티브와 다트라는 언어를 사용하는 플러터라는 크로스 플랫폼 프레임워크가 등장하였습니다. 이 중 우리는 플러터라는 프레임워크를 공부해볼 예정입니다.

 

환경 세팅에 앞서 저의 PC 환경은 Windows 10, Dell Notebook 16GB Ram입니다. 윈도우 환경에 맞춰 개발 및 포스팅이 진행되니 이점 참고 부탁드립니다.

 

먼저 안드로이드 스튜디오를 설치하겠습니다. 

 

Download Android Studio & App Tools - Android Developers

Android Studio provides app builders with an integrated development environment (IDE) optimized for Android apps. Download Android Studio today.

developer.android.com

다운받은 설치 파일을 실행합니다.

선택 후 Next를 통해 설치를 마무리합니다.

 

 

여기까지 진행했으면 안드로이드 스튜디오 설치는 마쳤습니다. 다음으로 플러터 SDK를 설치하겠습니다.

 

Install

Install Flutter and get started. Downloads available for Windows, macOS, Linux, and Chrome OS operating systems.

docs.flutter.dev

다운로드한 zip 파일의 압축을 해제합니다.

압축이 성공적으로 해제됐다면 [Win] + [R]을 통해 CMD 창으로 들어가 아래처럼 flutter 설치 경로의 bin폴더에 들어갑니다. flutter doctor을 실행하여 개발 환경을 체크합니다.  

다음으로 다시 android studio로 돌아가 flutter plugin 설치해야 합니다. 중간에 나오는 Dart 설치 알림으로 같이 설치합니다.

Restart IDE를 통해 android studio 재 실행 시 New Flutter Project가 생겼고 클릭해줍니다.

아래 창이 나오면 Flutter 선택 후 아까 설치한 Flutter SDK 경로를 선택합니다.

Finish 버튼을 누르면 기본 프로젝트가 생성된 것을 확인할 수 있습니다. 

예제 프로젝트를 실행해보기 위해 우측 상단에 핸드폰 아이콘 옆 안드로이드 아이콘이 있는 Device Manager를 선택합니다. 

재생(시작)아이콘을 눌러 AVD를 실행합니다. 

마지막으로 오른쪽 상단에 있는 초록색 재생(실행) 버튼을 누르거나 [Ctrl] + [F10]으로 실행하면 우측 Emulator에 실행 화면을 볼 수 있고 드래그하면 큰 화면으로도 볼 수 있습니다.

728x90

'Programming > Flutter' 카테고리의 다른 글

[Flutter] Dart 문법  (0) 2022.11.23
[Flutter] 레이아웃과 위젯  (0) 2022.11.20
[Flutter] 프로젝트 내 폰으로 실행  (0) 2022.11.20
[Flutter] 기본 프로젝트 설명  (0) 2022.11.18
[Flutter] 비동기 처리  (0) 2022.11.18
728x90

18. 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자

제목과 같이 제대로 쓰기엔 쉽고, 엉터리로 사용하기 어렵게 하려면 사용자가 저지를 실수에 미리 예상해야 합니다. 그리고 그 실수에는 컴파일되지 않게 해야 합니다.

생일을 나타내는 클래스가 있습니다. 이 클래스는 생성할 때 년, 월, 일을 전달받습니다.

#include <iostream>
using namespace std;

class Birth {
public:
  Birth(int year, int month, int day);
};

int main()
{
  Birth myBirth(1996,9,21);
}

하지만 사용자의 실수 혹은 여러 요인에 따라 순서를 다르게 넣을 수 있습니다. 아래의 경우 컴파일은 정상적으로 되지만 실제 동작은 설계자가 의도하지 않은 대로 흘러갈 것입니다.

#include <iostream>
using namespace std;

class Birth {
public:
  Birth(int year, int month, int day);
};

int main()
{
  Birth myBirth(1996,9,21);
  Birth wrongBth(9,21,1996);
}

이런 실수를 방지하기 위해 할 수 있는 방법은 넘겨주는 타입을 클래스 랩퍼로 명시해 컴파일 단에서 오류를 내 실수를 방지하는 것입니다.

다른 방법으로는 const 붙이기 방법이 있습니다. 예전 예제에서 봤던 operator* 예제가 이에 해당합니다.

class Test { };

const Test operator*(const Test& lhs, const Test& rhs);

int main()
{
    Test a,b,c;
    
    if ( a * b = c ) {		// 원래 비교 연산자를 사용하려 했지만 대입이 되어 버린 상황
    }
}

인터페이스를 일관성 있게 제공해야 한다는 점을 가장 잘 이해하기 위해서는 STL을 생각하면 편합니다. STL의 경우 size 함수 하나로 원소의 개수를 알 수 있습니다.

인터페이스를 사용자가 호출 후 어떤 동작을 해주지 않으면 문제가 될 수 있는 설계는 좋지 않습니다. 예전 예제에서 했던 팩토리 함수가 그 예입니다. create함수를 통해 동적 할당한 포인터 객체를 넘기게 되는데 이는 나중에 사용자가 해제해줘야 하는 번거로움이 있었습니다. 아니면 이 생성된 객체를 스마트 포인터에 넘기곤 했습니다. 이런 번거로움 없이 바로 스마트 포인터를 넘긴다면 사용자는 더 편하지 않을까요?

19. 클래스 설계는 타입 설계와 똑같이 취급하자

c++에서 클래스를 설계하는 것은 하나의 타입을 설계하는 것과 다르지 않습니다. 그리고 타입 설계자에 맞게 함수, 연산자를 오버로드 해야하며 메모리 관리 그리고 초기화 및 정상 종료 등을 처리해야 합니다. 효과적인 클래스를 설계하기 위해서는 아래와 같은 고려사항들이 존재합니다.

  • 새로 정의한 타입에 대한 객체 생성 및 소멸에 대하여
  • 객체 초기화 및 대입에 대하여
  • 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에는 어떤 의미를 줄지에 대하여
  • 새로운 타입이 가질 수 있는 적절한 값의 범위와 제약에 대하여
  • 기존 클래스 상속에 대하여
  • 타입 변환에 대하여
  • 연산자 오버로드 및 멤버 함수에 대하여
  • 컴파일러가 기본으로 생성하는 함수의 접근성에 대하여
  • 접근 권한에 대하여
  • 선언되지 않은 인터페이스에 대하여
  • 일반적이며 꼭 필요한지에 대하여

20. 값에 의한 전달보다는 상수 객체 참조자에 의한 전달 방식을 택하는 편이 대개 낫다

값에 의한 전달은 인자로 전달된 객체의 사본을 통해 초기화되고 호출한 쪽은 반환한 값의 사본을 전달 받습니다. 결국 클래스의 멤버 변수 및 상속 관계에 따라 여러 번의 생성자와 소멸자들이 호출되게 됩니다.

#include <iostream>

class Human {
private:
  std::string sName;
  std::string sAddress;
};
class Student : public Human {};

bool checkStudent(Student s);

위 연산 과정을 줄이기 위해서는 참조자를 통해 전달 그리고 const를 통해 전달된 객체의 불변성을 보장하게 합니다.

#include <iostream>

class Human {
private:
  std::string sName;
  std::string sAddress;
};
class Student : public Human {};

bool checkStudent(const Student& s);

그리고 값에 대한 전달에 가장 큰 문제 중 하나는 함수 내에서 객체를 생성하여 처리하기 때문에 상속 관계에 있는 클래스의 기본 클래스를 인자로 전달 받는 경우 복사 손실 문제가 일어날 수 있습니다.

#include <iostream>

class Human {
private:
  std::string sName;
  std::string sAddress;
};
class Student : public Human {};

bool checkStudent(Human s);

int main()
{
  Student s;
  checkStudent(s);
}

위와 같이 인자로 기본 클래스를 전달받고 호출부분에 파생 클래스를 넘겨주더라도 함수 안에서 기본 클래스를 통해 생성되며 그에 해당하는 멤버 변수들과 함수가 호출되게 됩니다. 하지만 상수 참조자를 통해 전달하면 새로 생성하는 과정이 없기 때문에 위와 같은 문제를 방지할 수 있습니다.

하지만 무조건적인 참조자를 통한 전달을 선택하는 것이 아닙니다. STL 반복자, 함수 객체 타입, 기본 제공 타입의 경우에는 값에 의한 전달을 하는 것이 더욱 효율적입니다. 이처럼 상황에 맞는 전달 방식을 사용하는 것이 중요합니다.

728x90
728x90

13. 자원 관리에는 객체가 그만

어떤 클래스가 있고 그 클래스를 동적으로 생성 후 반환해 주는 함수(팩토리 함수)가 있습니다.

class Base { };

Base* CreateBase() {
  Base* b = new Base;
  return b;
}

int main() {
  Base* p = CreateBase();

  ...;

  delete p;
}

당연하게도 new를 통해 동적 할당해주었기 때문에 delete로 삭제시켜줘야 합니다. 하지만 CreateBase()와 delete 사이에 어떤 일이 일어날지 모릅니다. 이런 결과는 결국은 메모리 누수로 이어지고 계속 누적된다면 어떤 문제가 일어날지 모릅니다.

이 문제의 대안 중 하나는 스마트 포인터 입니다. 가리키는 대상에 소멸자가 자동으로 delete를 시켜주는데 이 스마트 포인터로는 auto_ptr, shared_ptr 등이 있습니다. 위 예제에서 auto_ptr을 적용해보겠습니다. 스마트 포인터를 사용하기 위해서는 #include <memory>를 해주셔야 합니다.

#include <iostream>
#include <memory>

class Base { };

Base* CreateBase() {
  Base* b = new Base;
  return b;
}

int main() {
  std::auto_ptr<Base> pBase(CreateBase());
  // auto_ptr의 소멸자를 통해 동적 할당된 객체는 자동으로 해제시켜줍니다.
}

https://cplusplus.com/reference/memory/auto_ptr/

 

https://cplusplus.com/reference/memory/auto_ptr/

class template <memory> std::auto_ptr template class auto_ptr; Automatic Pointer [deprecated] Note: This class template is deprecated as of C++11. unique_ptr is a new facility with a similar functionality, but with improved security (no fake copy assignmen

cplusplus.com

이 스마트 포인터 같은 자원 관리 객체는 자원을 획득 후 자원 관리 객체에 넘기고 자원 관리 객체는 자신의 소멸자를 통해 얻는 자원을 확실히 해제되도록 합니다. 이 특징 때문에 자원 획득 즉 초기화 (Resource Acquisition Is Initalization: RAII)라고도 합니다. 이 auto_ptr은 소멸할 때 관리하는 객체를 소멸시키기 때문에 이 객체는 가리키는 대상이 둘 이상일 땐 먼저 가리킨 객체는 null이 됩니다.

#include <iostream>
#include <memory>
using namespace std;

class Base { };

Base* CreateBase() {
  Base* b = new Base;
  return b;
}

int main() {
  std::auto_ptr<Base> pBase1(CreateBase());
  std::auto_ptr<Base> pBase2(pBase1);
  // pBase1 == NULL
  pBase1 = pBase2;
  // pBase2 == NULL
}

위와 같은 auto_ptr의 문제를 해결하기 위해 shared_ptr이 있습니다. 이 shared_ptr은 참조 카운팅 방식 스마트 포인터라고 해서 객체를 참조하고 있는 수를 체크해서 참조 중인 관리 객체가 0이 됐을 경우에 소멸 시키는 방식입니다.

아래처럼 사용했을 경우 CreateBase를 통해 생성된 객체를 관리하는 자원 객체는 1개 입니다. (pBase1)

#include <iostream>
#include <memory>
using namespace std;

class Base { };

Base* CreateBase() {
  Base* b = new Base;
  return b;
}

int main() {
  std::shared_ptr<Base> pBase1(CreateBase());
}

이 pBase1이 소멸되면서 CreateBase를 통해 생성된 객체를 관리하는 자원 객체는 0개가 되고 참조 카운트가 0이 됐다는 것을 의미합니다. 그렇기 때문에 소멸시킵니다. 아까 위에서 봤던 복사되는 예제를 살펴봅시다.

#include <iostream>
#include <memory>
using namespace std;

class Base { };

Base* CreateBase() {
  Base* b = new Base;
  return b;
}

int main() {
  std::shared_ptr<Base> pBase1(CreateBase());
  // 참조 카운트 1
  std::shared_ptr<Base> pBase2(pBase1);
  // 참조 카운트 2
  pBase1 = pBase2;
  // 참조 카운트 2
}

shared_ptr을 통해 생성된 객체로 객체를 넘기면 참조 카운트가 올라가면서 자원 관리 객체가 소멸될 때마다 참조 카운트가 감소하게 되고 결국에 0이 됐을 때 넘겨받은 객체를 소멸시켜 줍니다.

하지만 이 스마트 포인터들이 호출하는 delete는 delete []가 아니기 때문에 동적으로 할당한 배열을 넘겨주면 안됩니다.

14. 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자

RAII를 통해 동적 생성, 즉 힙 자원에 관리에 대해 다뤄보았습니다. 하지만 힙 자원이 아닌 경우에는 스마트 포인터로 해결할 수 없습니다.

스레드들의 임계 구역을 정해주는 mutex가 있습니다. 이 mutex를 좀 더 손쉽게 사용하기 위해 RAII 모양새로 클래스를 하나 설계하려고 합니다. 생성 시에 전달받은 mutex의 lock을 걸고 소멸 시에 unlock을 하는 것입니다.

#include <iostream>
#include <mutex>
#include <memory>
using namespace std;

void lock(mutex* p) { p->lock(); }
void unlock(mutex* p) { p->unlock(); }

class MutexLocker { 
public:
  explicit MutexLocker(mutex* pm) :
  mutexPtr(pm) {
    lock(mutexPtr);
  }
  ~MutexLocker() {
    unlock(mutexPtr);
  }
private:
  mutex* mutexPtr;
};

mutex m;

{
  ...;
  MutexLocker locker(&m);
  ...;
}

그런데 만약 우리가 만든 자원 관리 객체를 복사하려고 한다면 여러 방향으로 처리할 수 있습니다.
먼저 복사 자체를 금지하는 경우입니다. 예전에 했던 복사 생성자, 복사 대입 연산자를 막기 위한 여러 방법을 시도합니다.

다음으론 클래스 내부에서 mutex를 관리할 shared_ptr을 사용하는 것입니다. 이 shared_ptr은 삭제 자라는 것을 넘겨줄 수 있는데 소멸될 시 호출할 함수 포인터를 넘겨 삭제 시 호출할 함수를 지정합니다.

#include <iostream>
#include <mutex>
#include <memory>
using namespace std;

void lock(mutex* p) { p->lock(); }
void unlock(mutex* p) { p->unlock(); }

class MutexLocker { 
public:
  explicit MutexLocker(mutex* pm) :
  mutexPtr(pm, unlock) {
    lock(mutexPtr.get());
  }
private:
  shared_ptr<mutex> mutexPtr;
};

마지막 방법은 auto_ptr처럼 하나의 객체에서만 관리할 수 있게 완전히 넘겨준 후 자신은 NULL으로 만드는 것입니다.

15. 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자

위에서 좋은 자원 관리 객체들을 많이 봤습니다. 하지만 관리하고 있는 자원을 직접 접근해야 하는 경우는 언제든 생길 수 있습니다.

#include <iostream>
#include <memory>

class Base { };

Base* CreateBase() {
  Base* b = new Base;
  return b;
}

int main() {
  std::auto_ptr<Base> pBase(CreateBase());
  // auto_ptr의 소멸자를 통해 동적 할당된 객체는 자동으로 해제시켜줍니다.
}

13장에서 아래의 예제처럼 팩토리 함수를 통해 객체를 생성해서 넘겨주는 예제가 있었습니다. 만약 스마트 포인터가 관리하는 자원을 매개변수로 넘기는 함수가 있다면 관리하고 있는 자원의 포인터를 넘겨줄 방법이 필요합니다.

auto_ptr, shared_ptr에서는 get 함수를 통해 실제 포인터를 넘길 수 있습니다.

#include <iostream>
#include <memory>

class Base { };

Base* CreateBase() {
  Base* b = new Base;
  return b;
}

int GetBaseInfo(Base* b) {
  ...;
}

int main() {
  std::auto_ptr<Base> pBase(CreateBase());
  GetBaseInfo(pBase.get());
}

혹은 클래스가 갖고 있는 멤버 변수를 오퍼레이터를 통해 접근 가능합니다.

#include <iostream>
#include <memory>

class Base { 
public:
  void print() { std::cout << "Base\n"; }
};

Base* CreateBase() {
  Base* b = new Base;
  return b;
}

int main() {
  std::auto_ptr<Base> pBase(CreateBase());
  pBase->print();
  (*pBase).print();
}

마지막으로 암시적 변환 함수를 통해 사용하는 방법입니다. 폰트를 관리하는 Font 클래스와 실제 자원을 나타내는 FontHandle 클래스를 갖고 있습니다.

#include <iostream>
using namespace std;

void releaseFont(FontHandle fh);

class Font {
public:
  explicit Font(FontHandle fh) : f(fh) {}
  ~Font() { releaseFont(f); }
private:
  FontHandle f;
};

사용자가 FontHandle 자원을 접근하기 위해서는 클래스에서 Get 함수를 구현해줘야 하고 사용자는 그 함수를 통해서 접근해야 합니다.

class Font {
public:
  FontHandle get() const { return f; }
private:
  FontHandle f;
};

하지만 접근시 마다 get을 호출하는 것은 사용자에 따라 귀찮고 번거로울 수 있습니다. 이럴 경우 암시적 변환 함수를 제공하여 접근성을 더 높일 수 있습니다.

#include <iostream>
using namespace std;

class FontHandle {};
class Font {
public:
  // FontHandle get() const { return f; }
  operator FontHandle() const { return f; }
private:
  FontHandle f;
};

void releaseFont(FontHandle fh);

int main() 
{
  Font f;
  FontHandle f2;
  f2 = f;
  releaseFont(f);
}

16. new 및 delete를 사용할 때는 형태를 반드시 맞추자

이 항목은 아래 예제로 모든 것이 설명이 가능합니다. []로 new 시에는 무조건 []로 delete를 해야 합니다.

#include <iostream>
using namespace std;

int main() 
{
  int* p = new int;
  int* pA = new int[100];
  delete p;
  delete[] pA;
}

new는 결국 메모리 할당, delete는 메모리 해제하는 것입니다. 단일로 객체를 동적 생성하는 것과는 다르게 배열로 동적 생성하게 되면 할당됐을 때 앞에 배열의 크기(개수) 정보를 통해 생성된 객체들을 소멸시키게 됩니다.

17. new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자

어떤 함수를 통해 Base 객체를 처리하는 작업을 하고 싶은데 동적 할당된 Base 객체를 스마트 포인터로 관리하기 위해 아래와 같이 작성했습니다.

#include <iostream>
#include <memory>
using namespace std;

class Base{ };

int RandomNum() { return 1; }
void ProcessBase(std::shared_ptr<Base> pB, int nNum);

int main() 
{
  ProcessBase(new Base, RandomNum());
}

하지만 shared_ptr의 생성자는 explicit으로 선언되어 있어 new Base를 통해 만들어진 포인터가 shared_ptr로 바꾸는 암시적 변환이 불가능합니다.

그렇기 때문에 아래처럼 전달 자체를 shared_ptr로 생성 후 넘겨줘야 합니다.

#include <iostream>
#include <memory>
using namespace std;

class Base{ };

int RandomNum() { return 1; }
void ProcessBase(std::shared_ptr<Base> pB, int nNum);

int main() 
{
  ProcessBase(std::shared_ptr<Base>(new Base), RandomNum());
}

이 ProecessBase 함수에서는 총 3가지 동작을 수행합니다.
1. new Base
2. shared_ptr 생성자 호출
3. RandomNum() 호출

위 순서는 컴파일러마다 다르게 호출하게 되는데 만약 아래의 순서에서 문제가 발생했을 때 메모리가 누수될 가능성이 있습니다.
1. new Base
2. RandomNum() 호출
3. shared_ptr 생성자 호출
RandomNum() 호출에서 예외가 발생하는 경우 동적 생성된 Base는 제대로 관리할 수 없습니다. 이러한 문제를 최대한 방지하기 위해 new로 생성한 객체를 담는 스마트 포인터를 생성하는 코드는 따로 사용합니다.

#include <iostream>
#include <memory>
using namespace std;

class Base{ };

int RandomNum() { return 1; }
void ProcessBase(std::shared_ptr<Base> pB, int nNum);

int main() 
{
  std::shared_ptr<Base> pSB(new Base);
  ProcessBase(pSB, RandomNum());
}

 

728x90
728x90

5. C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자

c++에서 생성자는 새로운 객체를 메모리에 만드는데 필요한 과정을 제어하고 객체의 초기화를 맡으며 소멸자는 객체를 없애면서 그 객체가 메모리에서 적절하게 사라질 수 있도록 하는 함수입니다. 그리고 대입 연산자는 기존의 객체에 값이나 정보를 다른 객체에게 줄 때 사용합니다.

우리가 클래스를 그냥 공백으로 생성하여도 컴파일러는 복사 생성자, 복사 대입 연산자, 소멸자는 자동으로 만들어 줍니다. 아래처럼 아무것도 선언하지 않는 클래스의 객체를 생성해서 연산을 해도 문제가 없습니다.

#include <iostream>

using namespace std;

class Empty {};

int main()
{
  Empty e1, e2;
  e2 = e1;
  Empty e3(e1);
}

하지만 기본 생성자의 경우 클래스 내부에 아무런 생성자가 선언되어 있지 않을 때만 자동으로 생성해줍니다.

기본으로 만들어주는 복사 생성자와 복사 대입 연산자의 경우에는 단순히 비정적 데이터를 복사해주게 됩니다. 그런데 만약 클래스의 멤버 변수가 아래와 같다면 문제가 생깁니다. 참조자로 선언된 변수는 자신이 참조하고 있지 않는 것을 참조할 수 없습니다. 그렇기에 컴파일 단계에서 에러를 떨어트리게 됩니다.

6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금지해 버리자

위에 설명한 것처럼 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자는 선언한 게 없다면 자동으로 만들어 줍니다. 이제 여기서 문제가 될 수 있는 상황은 복사 생성자나 복사 대입 연산자입니다. 만약 개체를 복사하면 안 되는 경우 복사 생성자와 복사 대입 연산자를 생성하지 않고 객체를 선언한다면 어디에서 의도치 않게 복사를 시도했을 때 제대로 막을 수 없습니다.

이러한 경우 복사를 막기 위해서는 private으로 선언 후 아무런 정의를 해주지 않는 것입니다.

class Empty {
public:
  Empty() {}
private:
  Empty(const Empty&);
  Empty& operator=(const Empty&);
};

int main()
{
  Empty e1, e2;
  e2 = e1;
  Empty e3(e1);
}

또 다른 방법은 복사 방지를 할 클래스를 선언 후 이 클래스를 상속받는 방법입니다.

#include <iostream>

using namespace std;

class PreventCopy {
protected:
  PreventCopy() {}
  ~PreventCopy() {}
private:
  PreventCopy(const PreventCopy&);
  PreventCopy& operator=(const PreventCopy&);
};

class Empty : private PreventCopy {
};

int main()
{
  Empty e1, e2;
  e2 = e1;
  Empty e3(e1);
}

7. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자

어떤 기본 클래스를 상속받는 클래스를 만들고 팩토리 함수처럼 상속된 함수 객체를 만들어서 기본 클래스 형태로 접근해서 사용하기도 합니다. 이렇게 생성한 객체는 결국 다시 소멸시키는데 만약 이처럼 기본 클래스 형태로 소멸시키게 됐을 때 소멸자가 비가상 소멸자일 경우 파생 클래스까지 제대로 소멸이 이루어지지 않습니다.

class Base {
public:
  Base() { cout << "Base Create\n"; }
  ~Base() { cout << "Base Delete\n"; }
  virtual void f() { cout << "Base\n"; }
};

class Test : public Base {
public:
  Test() { cout << "Test Create\n"; }
  ~Test() { cout << "Test Delete\n"; }
  virtual void f() { cout << "Test\n";}
};

Base* getBase() {
  Test* p = new Test;
  return p;
}

int main() 
{
  Base* b = getBase();
  b->f();
  delete b;
}

위처럼 파생 클래스의 생성자는 제대로 호출되었지만 소멸자는 제대로 호출되지 않습니다. 이럴 경우 파생 클래스에서 할당한 메모리가 제대로 해제가 되지 않을 수 있어 문제를 일으킬 수 있습니다.

이러한 문제를 방지하기 위해서는 기본 클래스의 소멸자를 가상 소멸자로 선언하는 것입니다. 위 코드에서 기본 클래스의 소멸자에 virtual만 붙여줬을 뿐인데 파생 클래스도 모두 제대로 소멸자가 호출되었습니다.

하지만 기본 클래스로 설계되지 않았거나 다형성을 갖도록 설계되지 않은 클래스의 경우에는 소멸자에 virtual을 해주게 되면 오히려 독이 될 수 있으므로 상황에 맞게 사용해야 합니다.

8. 예외가 소멸자를 떠나지 못하도록 붙들어 놓자

우리가 클래스를 정의하고 그 클래스를 아래처럼 사용하는 경우에 만약 소멸자에서 예외가 발생한다고 봅시다.

class Widget {
public:
  Widget() {}
  ~Widget() { ...; }
};

void func() {
  std::vector<Widget> vec;
  ...;    // action
}

func 함수의 어떤 행위 이후 vec은 소멸하면서 자신이 가지고 있는 Widget 객체들을 소멸해줍니다. 첫 번째 객체에서 예외가 떨어지고 두 번째 예외에서도 예외가 떨어진다면 어떤 동작이 벌어질지 예측할 수 없게 됩니다.

위처럼 만약 소멸자에서 어떤 행위를 해야 하고 예외 처리를 해야 한다면 2가지 방법이 있습니다. 예외 처리 이후 바로 프로그램을 종료시키거나 예외를 삼킨다고 하는 방식을 사용합니다.

class Widget {
public:
  Widget() {}
  ~Widget() { 
    try { 
      close();
    } catch ( ... ) {
      std::abort();
    }
  }
  void close() { ...; }
};

void func() {
  std::vector<Widget> vec;
  ...;    // action
}
class Widget {
public:
  Widget() {}
  ~Widget() { 
    try { 
      close();
    } catch ( ... ) {
      // 예외 삼키기 따로 처리 x
    }
  }
  void close() { ...; }
};

void func() {
  std::vector<Widget> vec;
  ...;    // action
}

하지만 두 방법 모두 올바른 방법이라 보기엔 애매합니다. 결국 어떤 행위를 했을 때 에러 처리를 회피하는 뉘앙스를 보이는데 이런 방식보다 예외가 일어날 소지가 있는 부분은 따로 어딘가에서 처리하게 하는 것이 맞다고 생각합니다.

9. 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자

제목처럼 하면 안 되는 이유는 원하는 동작이 제대로 동작하지 않아서입니다.
아래와 같이 기본 클래스와 여러 파생 클래스가 있다고 가정해 보겠습니다.

class Skill {
public:
  Skill() {
    cout << "Skill\n";
    SetDamage();
  };
  virtual void SetDamage() = 0;
};

class fireball : public Skill {
public:
  fireball() {
    cout << "fireball\n";
  };
  virtual void SetDamage() { }
};

class PowerStrike : public Skill {
public:
  PowerStrike() {
    cout << "PowerStrike\n";
  };
  virtual void SetDamage() { }
};

int main()
{
  PowerStrike sk;
}

여기서 PowerStrike 객체를 생성하면서 Skill의 생성자를 먼저 호출합니다. 이 Skill의 생성자에서 가상 함수인 SetDamage를 호출하는데 아직 기본 클래스에서 함수를 호출하기 때문에 Skill::SetDamage()가 호출됩니다. 만약 위 예제처럼 순수 가상 함수로 선언했다면 실행 자체가 되지 않게 됩니다.

위 문제를 해결하는 방법은 일단 해당 함수를 비가상 함수로 만들고 파생 클래스의 생성자에서 필요한 정보를 기본 클래스의 생성자로 넘기도록 합니다.

class Skill {
public:
  Skill(int nDamage) {
    SetDamage(nDamage);
  };
  void SetDamage(int& nDamage) {
    nPower = nDamage;
  }
  int GetDamage() { return nPower; }
protected:
  int nPower;
};

class PowerStrike : public Skill {
public:
  PowerStrike() : Skill(Calc()) {
  };
  static int Calc() { return 1; }
};

int main()
{
  PowerStrike sk;
  cout << sk.GetDamage();
}

이렇게 되면 이전 방식에서 필요로 했던 기본 클래스가 파생 클래스로부터 특정 정보를 가져온 후 세팅하는 게 문제 없어집니다.

10. 대입 연산자는 *this의 참조자를 반환하게 하자

c++에서 대입 연산은 아래처럼 어떤 연산의 결과는 그다음 연산에 영향을 미치게 됩니다. 이러한 결과는 대입 시 참조자를 반환하게 되었기 때문인데 클래스를 구현했을 때도 이러한 방식을 따르는 게 기존의 대입과 동일하게 구현하는 것이라고 생각합니다.

이 규칙은 대입에 관련된 모든 대입 연산자에서 지켜져야 합니다.

class Test {
public:
  Test& operator=(const Test& rhs) {
    return *this;
  }
  Test& operator+=(const Test& rhs) {
    return *this;
  }
  Test& operator=(int rhs) {
    return *this;
  }
  ...;
};

11. operator=에서는 자기 대입에 대한 처리가 빠지지 않도록 하자

자기 대입이란 아래처럼 대입하는 작업입니다.

int main()
{
    Widget w;
    w = w;
}

물론 위 코드만 봐서는 왜 저렇게 하나 싶지만 눈에 보이지 않게 자기 대입이 일어날 수 있습니다. 배열 인덱스를 통한 복사 혹은 포인터를 통한 복사 등으로 일어날 수 있습니다.

아래처럼 어떤 자원을 관리하는 클래스가 있다고 봅시다. 이 객체들을 복사할 때 조심해야 하는 것이 자기 대입 관련되어 대입 연산자를 올바르게 구현하는 것입니다.
만약 아래처럼 Widget이 대입이 이루어질 때 기존의 자원은 해제 후 복사 대상의 자원 정보를 갖고 오게 됩니다.

class Resource {};
class Widget {
  Widget& operator=(const Widget& rhs) {
    delete pR;
    pR = new Resource(*rhs.pR);
    return *this;
  }
private:
  Resource* pR;
};


그런데 위 상황에서 복사 대상이 자신이라면 자신의 자원 정보는 delete 된 후 새로 할당하려 할 때 문제가 일어나게 됩니다. 이러한 문제를 방지하기 위해 이 복사 대상이 자기 자신인지 체크하는 로직이 필요합니다.

class Resource {};
class Widget {
  Widget& operator=(const Widget& rhs) {
    if ( this == &rhs ) { return *this; }
    // 복사 대상이 자기 자신이라면 반환
    delete pR;
    pR = new Resource(*rhs.pR);
    return *this;
  }
private:
  Resource* pR;
};

만약 어떤 문제가 있어 new 자체에서 예외가 발생한다면 삭제가 돼버린 자원 정보만을 갖게 됩니다. 이러한 문제를 방지하기 위해선 선 삭제 후 생성이 아닌 선 생성 후 삭제를 하면 됩니다. 먼저 생성을 해보고 문제가 없다 싶으면 삭제를 하는 것입니다.

class Resource {};
class Widget {
  Widget& operator=(const Widget& rhs) {
    if ( this == &rhs ) { return *this; }
    // 복사 대상이 자기 자신이라면 반환
    Resource* pOrigin = pR;
    pR = new Resource(*rhs.pR);
    delete pOrigin;

    return *this;
  }
private:
  Resource* pR;
};

12. 객체의 모든 부분을 빠짐없이 복사하자

이전에 설명했듯이 복사 생성자, 복사 대입 연산자는 사용자가 선언하지 않으면 컴파일러가 알아서 구현해줍니다. 만약 사용자가 어떤 이유에서 위 함수들을 선언했다면 모든 데이터들을 빠짐없이 복사해줘야 합니다.

이 복사 관련해서 실수하기 좋은 부분이 파생 클래스의 복사 생성, 대입 연산일 때입니다.

class Base {
public:
  Base(const Base& rhs) : nBase(rhs.nBase) {}
  Base& operator=(const Base& rhs) {
    nBase = rhs.nBase;
    return *this;
  }
private:
  int nBase;
};

class Test : public Base {
public:
  Test(const Test& rhs) : nTest(rhs.nTest) {}
  Test& operator=(const Test& rhs) {
    nTest = rhs.nTest;
    return *this;
  }
private:
  int nTest;
};

위와 같이 정의했을 때 얼핏 보기에는 복사가 잘 되는 것 같지만 기본 클래스에 담겨 있는 데이터는 제대로 복사가 되지 않게 됩니다. 복사를 제대로 해주기 위해선 파생 클래스의 복사 생성자에서는 기본 클래스의 복사 생성자까지 호출하고 파생 클래스의 복사 대입 연산자에서는 동일하게 기본 클래스의 복사 대입 연산자까지 호출해줘야 합니다.

class Base {
public:
  Base(const Base& rhs) : nBase(rhs.nBase) {}
  Base& operator=(const Base& rhs) {
    nBase = rhs.nBase;
    return *this;
  }
private:
  int nBase;
};

class Test : public Base {
public:
  Test(const Test& rhs) :
    Base(rhs), 
    nTest(rhs.nTest) {}
  Test& operator=(const Test& rhs) {
    Base::operator=(rhs);
    nTest = rhs.nTest;
    return *this;
  }
private:
  int nTest;
};




728x90
728x90

1. C++을 언어들의 연합체로 바라보는 안목은 필수

오늘날의 C++은 다중 패러다임 프로그래밍 언어라 불리고 있습니다. 절차적, 객체 지향, 함수식, 일반화, 메타프로그래밍 개념까지 지원하는 이 언어를 이해하려면 단일 언어라는 개념을 넘어 상관 관계가 있는 여러 언어들의 연합체로 보는 것입니다. 

 

여러개의 하위 언어를 지원하는데 하위 언어 4가지는 아래와 같습니다.

  • C :  C++ 기본적으로 C를 베이스로 하고 있으며 포인터를 비롯한 많은 개념들은 C에서 따왔습니다. 물론 C++에는 많은 유용하면서 월등한 기능을 제공하고 있지만 C의 기능만으로도 구현, 구성이 가능합니다. 
  • 객제 지향 개념의 C++ : 클래스라는 개념을 기반으로 캡슐화, 상속, 다형성, 가상 함수 등을 얘기합니다.
  • 템플릿 C++ : 템플릿 메타 프로그래밍(TMP) 이라는 개념이 등장합니다.
  • STL : 템플릿 라이브러리는 컨테이너, 반복자, 알고리즘 및 함수 객체와 같은 개념들이 어떤 규약으로 얽혀져 있습니다. STL을 사용하려면 그 규약을 이용하면 됩니다.

추가로 효과적인 프로그램 개발을 위해 C++이 C보다 더 나은 점이 있다면 언제든지 활용해야 합니다. 예를 들어 값 전달, 참조 전달과 같은 개념이 C++에서는 참조자에 의한 전달(pass-by-reference-to-const) 방식이 더 효율이 좋게 됩니다. 

 

2. #define을 쓰려거든 const, enum, inline을 떠올리자

우리는 예전부터 어떤 상수를 사용할 때 아래와 같이 사용을 많이 했습니다.

#define PI 3.14

위와 같은 방법은 우리는 PI가 3.14라는 것을 인지할 수 있지만 컴파일러에겐 그냥 3.14로 처리가 됩니다. 컴파일러가 쓰는 symbol table에는 PI가 아닌 3.14가 들어가게 됩니다. symbol table에 관한 내용은 아래 링크를 참고 부탁드립니다.

 

심볼 테이블 - 위키백과, 우리 모두의 백과사전

심볼 테이블(symbol table)은 컴파일러 또는 인터프리터 같은 언어 변환기(프로그램의 소스 코드의 각 식별자가 자신의 선언 또는 소스에서의 외형과 관련된 정보와 연관되는)에서 사용되는 데이터

ko.m.wikipedia.org

이러한 현상을 미연에 방지하기 위해선 매크로 대신에 상수를 사용하는 방법이 있습니다.

const double PI = 3.14;

위와 같이 #define을 const로 교체하게 되면 컴파일러에게도 PI라는 것을 명시할 수 있게 됩니다. #define을 const로 바꿀 때 주의해야 할 점은 상수 포인터를 정의하는 경우 입니다. 포인터를 const로 해줄 뿐 아니라 포인터가 가르키는 대상까지 const로 선언해야 합니다.

const char* const sName = "Seyong";

클래스 멤버로 상수로 정의하는 경우 상수의 값이 객체가 아닌 클래스에 고정되게 하려면 정적 멤버로 만들어야 합니다. 

class Player{
private:
    static const int nNumTurn = 5;
    int scores[nNumTurn];
};

nNumTurn은 선언되어 있지만 정적 멤버로 정수 타입의 멤버 상수들은 예외입니다. 선언만해도 문제가 없습니다. 간혹 컴파일러에 따라 지원이 되지 않는다면 정의부분만 구현 파일에 두면 됩니다.

class Player {
private:
    static const int nNumTurn;
};

const int Player::nNumTurn = 5;	// 구현 부분에 정의

만약 위 처럼 상수를 컴파일하는 도중에 필요한 경우 (위 처럼 상수가 배열의 개수가 되는 경우)에는 enum을 사용하여 처리할 수 있습니다.

class Player{
private:
    enum { nNumTurn = 5 };
    int scores[nNumTurn];
};

enum을 사용하는 것은 const보단 #define에 가까운 동작을 보이게 됩니다. 이 값 혹은 변수는 주소 값을 가지고 처리도 불가능 하며 메모리 할당도 하지 않게 됩니다. 그리고 이 방식을 나열자 둔갑술(enum hack)이라고 불리며 템플릿 메타 프로그래밍의 핵심 기법입니다.

 

이 #define이 또 많이 쓰이는 부분이 매크로 함수인데 매크로 함수는 인자의 괄호 처리가 매우 중요합니다. 그리고 증감 연산자를 사용하는 경우 원하는 처리가 이루어지지 않을 수 있습니다.

#define SQR(X) X*X

위와 같은 매크로 함수의 경우 선언에 따른 처리가 완전히 달라질 수 있습니다. 아래와 같이 매크로 함수 선언 시 괄호를 제대로 넘기지 않고 처리 시 원하는 결과가 나오지 않기도 합니다.

위와 같이 매크로 함수의 단점을 커버하고 효율성은 그대로 가져오는 방법은 인라인 템플릿 함수 사용입니다.

template<typename T>
inline int SQT(const T& a)
{
    return a*a;
}

3. 낌새만 보이면 const를 들이대 보자

const는 변경이 불가능하게 해주는 예약어입니다. const로 상수를 선언하여 사용하거나 정적, 비정적 멤버를 상수로 선언할 수 있습니다. 그리고 포인터에 사용한다면 아래와 같은 용도로도 사용할 수 있습니다.

char str[] = "Hello";
char* p = str;				// 비상수 포인터, 비상수 데이터
const char* p = str;		// 비상수 포인터, 상수 데이터
char* const p = str;		// 상수 포인터, 비상수 데이터
const char* const p = str;	// 상수 포인터, 상수 데이터

그리고 매개 변수에 대해 const가 붙은 경우에 아래 두 경우 매개변수 타입은 모두 같습니다. 즉 const 순서를 어떻게 붙이느냐는 아래 두 경우에는 중요하지 않습니다. 하지만 저는 개인적으론 위의 방식으로 선언하는 것을 선호합니다.

void f1(const Test* pw);
void f2(Test const* pw);

const는 STL 반복자의 경우에도 중요하게 다가옵니다. 아래 두가지의 iterator는 const의 위치에 따라 다른 동작을 하게 됩니다.

d아래의 경우에는 포인터를 상수로 선언한 것이기 때문에 위치를 변경하는 것에 에러가 발생했습니다.

std::vector<int> vec;

const std::vector<int>::iterator iter = vec.begin();
*iter = 10;
++iter;		// error

하지만 아래의 경우에는 const_iterator를 사용하여 가르키는 대상 자체의 값을 변경하는 것을 막았기 때문에 값 변경에 에러가 발생하게 됩니다.

std::vector<int> vec;

std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10;
cIter++;

이 const를 가장 잘 사용할 수 있는 방법 중 하나는 함수 반환에 const를 붙이는 것입니다. 이렇게 사용한다면 반환된 값이 변경되는 것을 막아야할 경우 유용합니다.

class Test { ...; };

const Test operator*(const Test& lhs, const Test& rhs);

int main()
{
	...;
    Test a,b,c;
    
    if ( a * b = c ) {		// 원래 비교 연산자를 사용하려 했지만 대입이 되어 버린 상황
    	...;
    }
    
    ...;
}

멤버 함수에 const가 붙게 된다면 이 멤버 함수는 상수 객체에 대해서 호출될거야 라고 알려주게 되고 이렇게 된다면 클래스의 인터페이스를 보고 객체를 변경할 수 있는 함수는 무엇인지 변경할 수 없는 함수는 무엇인지 확인이 가능하며, const의 사용을 통해 상수 객체를 사용하여 코드의 효율을 높일 수 있다는 점입니다.

효율을 높인다는 것은 상수 객체에 대한 참조전달을 통해 객체 전달흘 한다는 것인데 이렇게 사용하기 위해서는 상수 멤버 함수가 준비되어 있어야 합니다. 

 

아래는 상수 멤버 함수를 이해하기 위한 예제 코드입니다. 상수 객체로 선언된 객체는 상수 멤버가 호출됩니다.

#include <iostream>
#include <vector>
using namespace std;

class Test {
public:
  Test(int n) : nNum(n) {}
  int& GetData() {
    cout << "Func" << endl;
    return nNum;
  }
  const int& GetData() const {
    cout << "const Func" << endl;
    return nNum;
  }
  
private:
  int nNum;
};

int main()
{
  Test t1(10);
  const Test t2(20);

  cout << t1.GetData() << endl;
  cout << t2.GetData() << endl;
}

상수 객체를 생성하는 경우는 상수 객체에 대한 포인터 혹은 상수 객체에 대한 참조자가 객체로 전달될 때 입니다.

#include <iostream>
#include <vector>
using namespace std;

class Test {
public:
  Test(int n) : nNum(n) {}
  int& GetData() {
    cout << "Func" << endl;
    return nNum;
  }
  const int& GetData() const {
    cout << "const Func" << endl;
    return nNum;
  }
  
private:
  int nNum;
};

void print(const Test& p)
{
  cout << p.GetData() << endl;
}

int main()
{
  Test t1(10);
  print(t1);
}

상수 멤버 함수의 경우에는 두 가지 상수성이 존재합니다. 하나는 비트 수준의 상속성 또는 물리적 상속성이며, 다른 하나는 논리적 상수성 입니다. 

물리적 상수성은 멤버 함수가 어떠한 데이터 멤버도 건드리지 않는다는 개념입니다. 우리가 멤버 함수 가장 뒤에 const를 붙이는 것이 이 상수성을 따르는 것입니다. 논리적 상수성은 완전히 바꾸지 않는 것이 아니라 바뀐지 모르게 하자는 것입니다. 그런 방식을 사용하기 위해서는 mutable이라는 예약어를 사용합니다. 이 예약어를 사용하게 되면 상수로 선언된 객체, 멤버 함수에서 값을 변경할 수 있습니다.

#include <iostream>
#include <string.h>
using namespace std;

class Test {
public:
  Test(char* pData) : p(pData) {} 
  int GetLength() const {
    m_nLen = strlen(p);
    return m_nLen;
  }
  
private:
  char* p;
  mutable int m_nLen;
};

int main()
{
  const Test t1("Test");
  cout << t1.GetLength();
}

4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자

변수를 제대로 초기화 하지 않으면 어떤 값이 들어가 있을 지 모릅니다. 초기화 할 때 '='를 통해서 초기화 하거나 기타 여러 방법으로 초기화 하게 됩니다. 그리고 우리가 객체를 사용할 때 초기화는 생성자를 통해서 이루어지게 됩니다. 우리는 생성자를 통해 값을 세팅하는 방법을 두가지 알고 있습니다.

class Test {
public:
  Test(int n) : nNum(n) {}
private:
  int nNum;
};
class Test {
public:
  Test(int n) {
    nNum = n;
  }
private:
  int nNum;
};

두 가지 모두 같다고 생각하지만 첫번째 방법은 초기화, 두번째 방법은 대입이라고 생각하셔야 합니다. 우리는 첫번째 방법인 초기화를 통해 객체의 멤버 값을 초기화 해주어야 하며 이 방법을 사용해야 생성자의 본문이 시작되기 전에 세팅이 되기 때문에 진정한 초기화라고 할 수 있습니다.

 

만약 멤버 변수가 어떤 객체이고 그 객체를 초기화 하면서 기본 생성자로 초기화 해주고 싶은 경우에도 멤버 리스트 초기화를 사용하는 것을 습관으로 들이는게 좋습니다.

class Temp { ... };

class Test {
public:
  Test() : t() {}
private:
  Temp t;
};

위 방식에 습관을 들이는 것이 좋을 뿐 아니라 특정 상황에서는 의무로 사용해야 합니다. 만약 상수 혹은 참조자 형태의 멤버 변수의 경우 바로 초기화를 해주어야 하기 때문에 멤버 리스트 초기화를 사용해야 합니다.

 

마지막으로 여러 번역 단위 비지역 정적 객체들의 초기화 순서 문제는 피해야 합니다. 이 문장을 하나씩 해석해 보겠습니다.

번역 단위는 컴파일을 통해 object 파일을 만드는 바탕이 되는 소스코드를 말합니다. 여러 번역 단위라는 것은 별도 컴파일 된 소스 파일이라는 것입니다. 

정적 객체는 생성된 시점부터 프로그램이 끝날 때까지 즉 main 함수의 실행이 끝날때 까지 살아 있는 객체를 말하며 정적 객체는 지역 정적 객체와 비지역 정적 객체로 나뉩니다. 지역 정적 객체는 함수 안에 있는 정적 객체를 말하며 비지역 정적 객체는 전역 객체, 네임스페이스 안의 정의된 객체, 클래스 안에서 static으로 선언된 객체, 파일 유효 범위 내에서 static으로 선언된 객체가 있습니다.

 

초기화 순서 문제라는 것은 만약 각 소스 파일에 비 지역 정적 객체가 있을 경우 사용할 경우 초기화가 되어 있다는 보장을 할 수 없게 됩니다. Directory 생성자에서 FileSystem의 numDisk를 호출하게 되면 FileSystem의 어떤 값을 가져오게 되는데 이 값이 제대로 초기화 됐다는 보장이 없습니다.

class FileSystem {
    int numDisk() const;
};

extern FileSystem fts;

///////////////////////////////////////////

class Directory {
	Directory() {
    	int nDisk = fts.numDisk();
    }
};

위 문제를 해결하기 위해서는 비지역 정적 객체를 지역 정적 객체로 바꿔주면 됩니다.

 

728x90

'Programming > C++' 카테고리의 다른 글

[Effective C++] 3. 자원 관리  (0) 2022.11.07
[Effective C++] 2. 생성자, 소멸자 및 대입 연산자  (4) 2022.10.31
C++ 방문자 패턴  (0) 2021.06.20
C++ 템플릿 메소드 패턴  (0) 2021.06.20
c++ 전략 패턴  (0) 2021.06.20
728x90
#!/bin/sh

USER="계정이름"
PW="패스워드"
IP="FTP 클라이언트 IP"
FILE "FILE 경로"

cd $1

ftp -n $IP << FTP_SESSION
user $USER $PW
bin
prompt
put $FILE
bye

FTP_SESSION
728x90
728x90

리눅스에서 kill 명령어를 사용하려면 기본적으로 pid를 넘겨서 종료시킵니다. 이럴때마다 내가 종료시킬 프로세스의 PID를 ps 명령어를 통해 확인하고 넘겨주는게 귀찮다면 아래 명령으로 프로세스 이름만으로 종료시킬 수 있습니다.

kill `ps -C ProcessName | grep ProcessName | gawk '{print$1}'`

명령어를 앞에서부터 짤라서 보면 ps -C ProcessName은 ProcessName 프로세스의 정보를 출력해줍니다.

 

grep을 통해 해당 문자에 대한 정보를 파싱하고 gawk (awk) 를 통해 원하는 필드(데이터)만을 추출합니다.

gawk (awk)에 대한 자세한 내용은 https://recipes4dev.tistory.com/171 를 참고하시면 도움이 되실거라고 생각합니다.

 

리눅스 awk 명령어 사용법. (Linux awk command) - 리눅스 파일 텍스트 데이터 검사, 조작, 출력.

1. awk 명령어. 대부분의 리눅스 명령들이, 그 명령의 이름만으로 대략적인 기능이 예상되는 것과 다르게, awk 명령은 이름에 그 기능을 의미하는 단어나 약어가 포함되어 있지 않습니다. awk는 최

recipes4dev.tistory.com

 

728x90

+ Recent posts