노력과 삽질 퇴적물

혼잣말처럼 넘기는 C++ (파트2) 본문

프로그래밍note/언어. C&C++ 계열

혼잣말처럼 넘기는 C++ (파트2)

MTG 2024. 1. 3. 15:46

 
목차
 시작하기
 기초
 > 키워드와 식별자
 > 자료형, 변수
 > 연산자
 > 자료형 캐스팅
 > 배열
 제어문
 함수
 포인터와 참조
 클래스와 상속
 > 기본 요소
 > 상속: 액세스 지시어, 가시성의 상속, 가상함수
 > 라이프 사이클로 본 생성자&소멸자
 > friend키워드
 > 다중상속
 생성자 종류
 > 복사 생성자
 > 이동 생성자
 > 위임 생성자
 연산자 다중정의(오버로딩)
 업 캐스팅&다운 캐스팅
 파트3.
 템플릿
 예외처리


* 해당 포스팅은 JAVA혹은 파이썬처럼 C기반과 직결되지는 않는 언어에 익숙한 상태로 진행되는터라 과감히 생략하고 넘기는 부분이 존재합니다.
* 일반적인 강의형 포스팅이라기보다는 개인적인 노트정리여서 보조 설명이나 자체적인 언어(?)로 해석/재구성한 부분은 밑줄을 포함한 이탤릭체 혹은 기울임꼴로 표기됩니다.

* 참조 자료상 auto, long long int등이 있는터라 최소사양을 C++11(2011년 승인)에 두고 있습니다.






1. 클래스와 상속
 
/*이제는 '클래스가 클래스인거지 다른게 클래스인가'하는 상태가 되버려서 해당 구간을 굳이 기입해야 하나 싶다가도 C++ 스타일이 또 있어서 선택지가 X
그러고 보니 C++에서도 this가 가능
 프로그래밍 언어에서 상속을 처음 배울때 예시들이 대체로 생물이였다보니 상속을 유전같은걸로 보는 경향이 있어 포스팅도 그 방향입니다. */

1) 기본 요소
-> private/protected/public에 대한 상세한 설명은 1-2) 상속에서 마저.
 MainHeader.h
#ifndef HEADER_FOR_INCLUDE
#define HEADER_FOR_INCLUDE


using namespace std;

class BaseCar
{
//START. 멤버 변수 및 함수
private://상속시 유전X. 클래스 멤버 가시성 지시어 기본값이 private라 생략가능.
... ... ...
... ... ...
protected://상속시 유전O.
const string modelName;//const로 지정? 생성자 초기화 리스트에서 초기화
long price; int maxSpeed;
... ... ...
public://상속시 유전O. 외부용 인터페이스. 특히 생성자/소멸자

BaseCar(string arg1, long arg2, int arg3=90): modelName{argModelName}
{//생성자. 클래스명과 동일 / 반환형X
             //modelName = argModelName;//const로 선언된 멤버변수여서 에러 발생.
             this->maxSpeed = arg3;//C++에도 this 가능!
}
int getXxxXxx() const
{//read-only인 멤버함수. 함수선언시 const로 못박힘. 함수내 변수값 수정 금지.
             return 멤버변수;
}
... ... ...
... ... ...
~BaseCar()
{//소멸자. 클래스명 앞에 ~가 붙음.
... ... ...
//반환타입 및 return X. 매개변수X
}
//END. 멤버 변수 및 함수
};  //<-클래스 끝단 세미콜론 잊지 말것.
#endif//HEADER_FOR_INCLUDE
main.cpp
-> 클래스 호출
#include "MainHeader.h"

using namespace std;

int main()
{
BaseCar baseCar("심플카", 500L);//매개변수가 있는 객체선언.
const BaseCar constCar("const카", 500L);//const로 선언된 객체는 이후에 내부값 변경 함수 사용시 에러.
return 0;
}
-> (생성자의) 초기화 리스트의 형태는 2가지다.
 
생성자(매개변수) : 변수명(초깃값) //형태1
생성자(매개변수) : 변수명{초깃값} //형태2
 
-> 만약 클래스 멤버 변수/함수에 static으로 선언해두면 객체 선언없이도 클래스 외부에서 접근이 가능하지만, static 멤버 함수에서는 static으로 선언된것만 사용이 가능.
-> "클래스를 선언할 떄 static으로 선언한 데이터 멤버는 각각의 객체마다 개별적으로 존재하는 것이 아니라 [그림 4-9]와 같이 전체 클래스에 대해 하나만 존재하며, 해당 클래스의 객체들이 그 데이터 멤버를 공유한다. 일반 데이터 멤버는 객체를 정의하면 자동으로 생성되지만, static 데이터 멤버는 생성되지 않는다. 따라서 static 데이터 멤버는 클래스의 외부에서 별도로 정의해야 한다." (p.150, C++프로그래밍, 2019)
-> "static 멤버변수는 어디서든 접근이 가능한 변수이다. ...(중략)... static 멤버가 private으로 선언되면, 해당 클래스의 객체들만 접근이 가능하지만, public으로 선언되면, 클래스의 이름 또는 객체의 이름을 통해서 어디서든 접근이 가능한다."
(p.261, 윤성우의 열혈 C++ 프로그래밍, 2010)


2) 상속
 상속을
한 클래스
 부모 클래스/상위 클래스/기초(base) 클래스/슈퍼(super) 클래스
상속을
받은 클래스
 자식 클래스/하위 클래스/파생or유도(derived) 클래스/서브(sub) 클래스
-> '열혈 C++ 프로그래밍'에서는 기초&유도 클래스, 방송대 교재는 기초&파생 클래스로 용어를 쓰고 있는데다가 제 코딩 습관상 (JAVA)부모 클래스 접두어를 Base로 쓰지만, 상속에 대한 개념 설명상 해당 페이지에서는 부모&자식 클래스로 용어를 통일하겠습니다.
-> 앞서 말했지만 상속을 처음 배울때 예시들이 대체로 생물이니 상속을 유전같은걸로 보는 경향이 있어 그 방향으로.
 닭(부모 클래스)에게는 머리가 하나씩 있죠? 암수를 교배시키면, 병아리(자식 클래스)의 머리는 몇 개 일까요? 자연계에서는 1개이나, 이게 C++에서 병아리 머리가 1개일수도 2개일수도 있게 되는... 이 말은 상속 할 때 멤버변수/함수에 지정에서 실수를 하면 위험하겠죠? 의학뉴스에서 유전자 조작이 그리 쉽지 않은것만 봐도
 2개 이상의 부모 클래스에게 상속받는 다중 상속은 실무에서 자주 보던 JAVA나 C#같은 언어에는 없는것는것도 그렇고.
액세스 지시어
public: 상속후 자식 클래스가 사용O.     //유전O
          해당 클래스 외부에서 사용O.
protected: 상속후 자식 클래스가 사용O.//유전O
              해당 클래스 외부에서 사용X.
private: 상속후 자식 클래스가 사용X.    //유전X
          해당 클래스 외부에서 사용X.

/* 뭣하러 용도별로 나누고, 이럴바에는 public만 쓰고 무슨 함수를 호출해서 쓸지 공유하면 되는거 아닐까도 싶지만 공지와 매뉴얼을 죽어도 안 읽고 그냥 해버리는 사람들이... 에휴. 현실적으로는 프로젝트 규모가 클수록 용도에 맞게 액세스 지시어를 지정하는게 이득인걸로.
 캡슐화 모형도 있고.*/
가시성의 상속
class ChildClass : ParentClass          //가장 기본적인 상속
class ChildClass : public ParentClass
class ChildClass : protected ParentClass
class ChildClass : private ParentClass
-> "가시성 상속 지시어는 기초 클래스로부터 상속된 멤버가 파생 클래스의 멤버로서 가지게 되는 가시성의 상한을 나타내는 것이다." (p.243, C++프로그래밍, 2019)
-> "public이 허용하는 접근의 범위가 가장 넓고, private이 허용하는 접근의 범위가 가장 좁다. 반면, protected는 그 중간 범위의 접근을 허용한다." (p.297, 윤성우의 열혈 C++ 프로그래밍, 2010)
예.
부모 클래스를 public로 상속=부모의 멤버들 그대로, 기본적인 상속과 동일한 동작.
부모 클래스를 protected로 상속=부모의 public멤버는 protected로.
부모 클래스를 private로 상속=부모의 public/protected멤버는 private로


/* 즉, 허용하는 접근 범위를 따지면 public>protected>private라서 부모 클래스를 어떤 액세스 지시어로 놓고 상속 시키는가에 따라 자식 클래스에게 유전되는 멤버의 공개 범위 상한선에 변형이 있을 수도 있고 없을 수도 있다. */
 virtual 함수
(가상함수)
virtual 반환타입 함수명(매개변수);      //기본가상함수
virtual 반환타입 함수명(매개변수) = 0;//순수가상함수
-> 부모 클래스에서 함수 내부 블록이 없음.
-> 생성자에는 컴파일러 에러등 불허지만, 소멸자는 virtual 키워드 적용이 가능하며 이런 소멸자는 '가상 소멸자'라 한다.
-> 클래스 멤버에 '순수가상함수'가 있으면 추상클래스(abstract class)
-> 자식 클래스에서 가상함수 동작을 작성하는것이 재정의(오버라이딩, Overriding)
-> 해당 추상클래스로 객체선언 불가! 자식클래스만 객체 선언가능.
 
 
3) 라이프 사이클로 본 생성자&가상 소멸자
 
자식 클래스로 객체 생성: (부모)      생성자 -> (자식)     생성자
자식 클래스의 객체 소멸: (자식)가상소멸자 -> (부모)가상소멸자
 
-> "상속을 통해 파생 클래스를 선언할 필요가 있는 클래스라면 소멸자를 가상함수로 만드는 것이 바람직하다."  (p.129, C++프로그래밍, 2019)
-> "기초 클래스의 소멸자만 virtual로 선언하면, 이를 상속하는 유도 클래스의 소멸자들도 모두 '가상 소멸자'로 선언이 된다(별도로 virtual 선언을 추가하지 않아도). 가상 소멸자가 호출되면, 상속의 계층구조상 맨 아래에 존재하는 유도 클래스의 소멸자가 대신 호출되면서, 기초 클래스의 소멸자가 순차적으로 호출된다."  (p.359, 윤성우의 열혈 C++ 프로그래밍, 2010)
-> /* 안드로이드앱이나 유니티 엔진에서도 객체 생성시 부모가 먼저, 객체 소멸시 자식이 먼저 흐름이 되니 C++에 한정된 방식은 아니다로? */
 
 
4) friend키워드
class StudentA
{
private:
   friend class StudentB;
   int mPrivateSecret;
protected:
   int mProtectedSecret;
public:
   ... ... ...
};
class StudentB
{//클래스내 friend 선언X.
private:
   ... ... ...
protected:
   ... ... ...
public:
   ... ... ...
   void doChat(StudentA& arg)
   {
   	arg.mPrivateSecret;
   	arg.mProtectedSecret;
   	... ... ...
   }
};
class StudentC
{//클래스내 friend 선언X.
private:
   ... ... ...
protected:
   ... ... ...
public:
   ... ... ...
   ... ... ...
};
코드 및 동작 설명.
-> StudentA는 StudentB가 친구라 선언했으니 StudentB내에서 private인 멤버에 접근이 됩니다. //보안이 뚫렸다.
-> 아무리 친구라도 개인적인 비밀들을 전부 내보일수는 없다?
friend class StudentB;대신 friend void StudentB::doChat(StudentA&);를 통해
친구야, 아무리 그래도 이럴때만 써야한다로 못을 박는겁니다.
-> 친구 클래스의 특정 함수에만 허용이 되면 전역함수에서도 혹시?
예, 맞습니다. 전역함수doChat이 있다면, friend void doChat(StudentA&); 입니다.
-> StudentC는 StudentA한테는 친구로 선언되지 않았으니 StudentA의 속내 전혀 모릅니다.
-> "friend 선언이 좋은 약으로 사용되는 상황은, 이후에 연산자 오버로딩을 공부하면서 보게 될것이다. ...(중략)... 전역함수를 대상으로도, 클래스의 멤버함수를 대상으로도 friend 선언이 가능하다. 물론 friend로 선언된 함수는 자신이 선언된 클래스의 private 영역에 접근이 가능하다."  (pp.251-252, C++프로그래밍, 2019)
-> /* 친구끼리 비밀이랄게 있겠어? 그러지 말고 나한테도 말해봐를 (C++)프로그래밍적으로 가능한 그런 키워드. 왜 이런식으로 정보은닉에서 예외인 키워드가 있는걸까 싶어지고. */
-> /* 방송대 교재는 어떤 예외처리에서 사용이 가능타로 언급된 정도지만 이건 직장인 대상으로 하다보니 (전업학생에 비해)적은 시간내에서 전공에서의 기초와 개념을 짚고 갈 적정선 문제로 분량이 적은거니 보충 설명이 필요하면 다른 자료 찾아보면 되겠죠. 적어도 용어는 알고 있으니 검색이 가능할테니. */
 
 
5) 다중상속
-> /* 이 병아리 클래스의 머리는 1개 일까, 아니면 N가 되는가의 작업(?) 어지간한 언어에 다중상속이 존재치 않는건 아무래도... */
 
정석적인 방법 (1)가상 상속
-> virtual로 인해 (아마도 이론상?)실행속도가 감소하나, 하드웨어 자체의 발전으로 극복이 되는 단점류여서 해당 감속은 무의미에 가까운걸로.
class Parent
{
... ... ...
public:
   virtual void doPrint() = 0;//순수가상함수
};
class Child: public Parent
{
... ... ...
public:
    void doPrint();//재정의 해야함.
};

 

정석적인 방법 (2)모호성없는 접근
-> /* 부모에 공통되는 로직을 심어두고, 자식이 코드 재활용을 하는 부분에서는 좋기야 해도. JAVA+안드로이드에서 자식클래스.xxx()에 호출하는 super.xxx()같네. */
class Parent1
{
... ... ...
public:
   void doPrint() {    ... ... ...    }
};

class Parent2
{
... ... ...
public:
   void doPrint() {    ... ... ...    }
};
class Child: public Parent1, public Parent2
{
... ... ...
public:
    void doPrint()
    {
        Parent1::doPrint();
        Parent2::doPrint();
    }
};

 

 
 
 
 
2. 생성자 종류
-> "C++11부터는 복사 할당 및 이동 할당이라는 두 가지 종류의 할당이 언어로 지원됩니다."
(MSDN, 복사 생성자 및 복사 할당 연산자(C++)) [#링크]
1) 디폴트 생성자(or 기본 생성자)
-> 생성자에 매개변수가 아예 없는 생성자. (디폴트 인수등으로 객체 선언시 입력하는것이 없는것도 디폴트 생성자)
 
 
2) 복사 생성자(Copy constructors)
class SampleClass
{
   SampleClass(const SampleClass& arg)
   {//MEMO. 클래스를 깊은 복사(deep copy)하는 생성자
      //복사1. memcopy같은 복사함수 사용.
      //복사2. this->mVar = arg.mVar로 특정 멤버변수만 복사.
   }
   ... ... ...
}
-> /* 방송대 교재와 열혈C++에는 복사생성자 앞에 explicit지정하는걸 짧막하게 다루는데,
MS, 위키독스, IBM 자료를 보니 위키독스에만 나옴... [#MS] [#위키독스] [#IBM]
 묵시적 형변환을 막아 코드의 모호성을 없애는 용도인걸 보면... 말하지 않아도 알아서 쓰시오?? */
 

3) 이동 생성자(Move Constructors)
class SampleClass
{
   SampleClass(SampleClass&& arg)//&&이 r-value 참조라는 소리.
   {//MEMO. 이동 생성자내에서 새 객체에 값을 복사.
      //복사1. memcopy같은 복사함수 사용.
      //복사2. this->mVar = arg.mVar로 특정 멤버변수만 복사.
      arg.arr = nullptr;//매개변수로 들어온 객체의 멤버를 기본 값으로(1)
      arg.score = 0;     //매개변수로 들어온 객체의 멤버를 기본 값으로(2)
   }
   ... ... ...
};
-> "반환된 객체는 소멸될 객체이므로 굳이 깊은 복사를 하는것은 비효율적이다. 특히 크기가 큰 벡터일수록 이러한 비효율에 의한 손실이 크다. 이동 생성자(move constructors)를 이용하면 이러한 상황의 효율을 개선할 수 있다.
 이동 생성자는 r-value참조를 사용한다. 값을 저장할 수 있는 실체가 있는 l-value와는 달리 r-value는 값을 제공하기 위한 것이다."  (p.145, C++프로그래밍, 2019)
-> /* 포인터에도 나온 용어긴 하지만... l-value, r-value는 프로그래밍 언어론에서 본격적으로 다루다보니... */


4) 위임 생성자(Delegating constructors)
class SampleClass
{
   SampleClass(int arg1)
   {//생성자A
      ... ... ...
   }
   SampleClass(int arg1, int arg2) : SampleClass(arg1)
   {//생성자B. 초기화 리스트에서 생성자A에게 매개변수 일부 분담.
      ... ... ...
   }
   SampleClass(int arg1, int arg2, int arg3) : SampleClass(arg1, int arg2)
   {//생성자C. 초기화 리스트에서 생성자B에게 매개변수 일부 분담.
      ... ... ...
   }
   ... ... ...
};
-> 방송대 교재와 열혈C++에는 별도 항목이 없어도 인강에서 아주 잠깐 언급? MS자료는 별도 페이지O. [#MS]
-> /* 여러 생성자에 반복되는 매개변수가 있다면? (생성자)초기화 리스트로 다른 생성자를 써서 중복 코드 줄이는 기법쯤?
 자바는 함수에 매개변수에 디폴트 인수가 지원되지 않아서 함수속에 일부 기능이 겹치는 동명의 함수를 호출케 하는거랑 엇비슷? */

 
 
 
 
3. 연산자 다중정의(오버로딩)
 
1) 개념
-> "함수가 오버로딩 되면, 오버로딩 된 수만큼 다양한 기능을 제공하게 된다. 즉, 이름은 하나지만 기능은(기능을 제공하는 함수는) 여러 가지가 되는 세밍다. 마찬가지로 연산자의 오버로딩을 통해, 기존에 존재하던 연산자의 기본 기능 이외에 다른 기능을 추가할 수 있다."
(p.359, 윤성우의 열혈 C++ 프로그래밍, 2010)
-> /* 사칙연산 혹은 대입연산을 클래스에도 적용케 하는것으로 C++이 만능에 가까운 맥가이버 나이프라 불리는 요인인가 싶기도? 이후 표준은 모르겠지만 이 방식으로는 매번 수동으로 해줘야 해서 조금 번거로운감은 있어도. */
-> 주의사항
주의사항
 기존 연산자가 갖는 고유한 특성과 의미에 벗어나지 않게.(코드분석 및 유지보수) // 연산자의 기본 기능, 우선순위, 결합성 등등.
 클래스 내부의 멤버로 정의하고, private으로 지정 하면 절대 안 됨.
다중정의가
금지된 연산자
 .     멤버 선택 연산자
 .*    멤버(에 대한) 포인터 연산자
 ::     (유효)범위 지정 연산자
 ?:    삼항 연산자 혹은 조건 연산자
-> 방송대 교재는 여기 까지만 언급.

 sizeof                바이트 크기 계산
 typeid               RTTI 관련 연산자
 static_cast          형변환 연산자(1)
 dynamic_cast      형변환 연산자(2)
 const_cast          형변환 연산자(3)
 reinterprest_cast  형변환 연산자(4)
 
 
 
2) 단항 연산자 다중정의
-> 여기서 잠시 짚고 갈것 하나.
 파트1 증감연산자를 보면 증감연산자는 전위에 둬야 즉시 반영되고, 후위에 두면 해당 세미콜론 이후에 값이 적용되는 흐름이다보니 연산자를 다중정의 할때도 주의.
class SampleOperatorType1
{
private:
   int mScore;

public:
... ... ...
   SampleOperatorType1 operator++()
   {//CASE. 객체에 대한 전위표기 증가연산자 다중정의.[++obj]
      ++mScore;
      return *this;
   }
   SampleOperatorType1 operator++(int arg)
   {//CASE. 객체에 대한 후위표기 증가연산자 다중정의.[obj++]
      SampleOperatorType1 beforeObject(*this);
      mScore+=1;
      return beforeObject;//원래 특성에 벗어나지 않게 멤버값 변경전 복사해둔 객체를 반환.
   }//'이항 연산자 다중정의' 예시에도 있지만, 연산자 우측에 있는 쪽이 함수 매개변수로
... ... ...
};


3) 이항 연산자 다중정의
 
className& operator연산자기호(const className& arg)
{
   ... ... ...
};
 
-> 산술 연산자를 다중정의할 경우, 연산자 기준으로 좌측은 객체 자신이고 우측의 피연산자가 다중정의되는 함수에 입력.
class SampleOperatorType2
{
private:
    int *mArr;
    int mScore;
... ... ...
public:
... ... ...
   SampleOperatorType2& operator+(const SampleOperatorType2& arg)
   {//CASE. 덧셈연산자 다중정의. objA+objB인 상황이면, objB가 arg로 입력.
      ... ... ...
      return *this;
   }
   SampleOperatorType2& operator-(const SampleOperatorType2& arg)
   {//CASE. 뺄셈연산자 다중정의. objA-objB인 상황이면, objB가 arg로 입력.
      ... ... ...
      return *this;
   }
   SampleOperatorType2& operator*(const SampleOperatorType2& arg)
   {//CASE. 곱셈연산자 다중정의. objA*objB인 상황이면, objB가 arg로 입력.
      ... ... ...
      return *this;
   }
   SampleOperatorType2& operator/(const SampleOperatorType2& arg)
   {//CASE. 나눗셈연산자 다중정의. objA/objB인 상황이면, objB가 arg로 입력.
      ... ... ...
      return *this;
   }
   
   SampleOperatorType2& operator=(const SampleOperatorType2& arg)
   {//CASE. 대입연산자를 다중정의.
      mScore = arg.mScore;
      return *this;
   }
   SampleOperatorType2& operator+=(const SampleOperatorType2& arg)
   {
      mScore += arg.mScore;
      return *this;
   }

   int& operator[](int argIdx){	return mArr[argIdx];	}//첨자 연산자
   
   bool operator ==(const SampleOperatorType2& arg) const{	/* 중략 */	}
   bool operator >(const SampleOperatorType2& arg) const {	/* 중략 */	}
   bool operator <(const SampleOperatorType2& arg) const {	/* 중략 */	}
... ... ...
};
-> 주의사항! 헷갈리기 딱 좋은 경우가 대입 연산자와 이동 연산자 다중정의시 앞부분만 보면 버그난다.
대입연산자
다중정의
 className& operator=(const className&   arg)
이동연산자
다중정의
 className& operator=(        className&& arg)
 
 




4. 업 캐스팅&다운 캐스팅

1) 업 캐스팅(upcasting)
Parent parentObj = new Child();
-> (상속 구조상)자식 클래스를 부모 클래스로 올리는 변환. 묵시적으로 처리.
 
 
2) 다운 캐스팅(downcasting)
 
Parent *parentObj;
Child   *childObj;
childObj = dynamic_cast<Child*>(parentObj);
 
-> (상속 구조상)부모 클래스를 자식 클래스로 내리는 변환. 명시적으로 처리.
-> dynamic_cast의 결과는 정상적으로 형변환이 된 객체이거나 nullPtr(변환 실패 결과)이다.





기타. 참조자료

1) 서적
윤성우 저. 윤성우의 열혈 C++ 프로그래밍, 오렌지미디어, 2010년.
전중남, 이병래 저. C++프로그래밍, 한국방송통신대학교출판문화원, 2019년.


2) 웹페이지
위키독스
05-01. 복사생성자(copy constructor) (접속: 2024-01-01)
코딩의 시작, TCP School
> 50) 복사 생성자 (접속: 2024-01-01)
 
IBM Documentation (IBM i 7.5)
Learn / Microsoft C++, C 및 어셈블러
friend (C++) | Microsoft Learn (접속: 2024-01-02)
> private (C++) | Microsoft Learn (접속: 2023-11-11)
> protected (C++) | Microsoft Learn (접속: 2023-11-11)
> public (C++) | Microsoft Learn (접속: 2023-11-11)
this 포인터 | Microsoft Learn (접속: 2023-11-11)
복사 생성자 및 복사 할당 연산자(C++) | Microsoft Learn (접속: 2023-11-11)
방법: 이동 생성자 정의 및 할당 연산자 이동(C++) | Microsoft Learn (접속: 2024-01-01)
생성자 위임(C++) | Microsoft Learn (접속: 2024-01-01)


3) 블로그
C++] 가상 함수를 생성자에 사용하면?? (접속: 2023-12-31)





기타. 변경이력
일자
 변경이력
 2024-01-03  초안 공개. [#블로거] [#티스토리]