본문 바로가기
공부/Programming Language

C++ 5일차

by Piva 2021. 5. 13.

  

(※ 개인 정리를 위한 것이므로, 이해한 내용 생략다수 + 불친절함 주의)

 

 


string 클래스

  기존 C에서 문자열을 다루는 것은 어려웠다. 두 문자열이 같은지를 확인하기 위해서도 별도의 함수를 사용해야 했다(strcmp). 그러나 C++에서 제공하는 표준 string 클래스를 사용하면 이러한 함수의 사용 없이 문자열을 편리하게 이용할 수 있다.

#include <iostream>
#include <string>

int main()
{
	std::string s = "cake";
    
    std::cout << s << std::endl;
    
    return 0;
}

  표준 클래스이기에 선언하여 사용할 때 std::string의 형태로 사용하는 것을 확인할 수 있다. 이 외에도 다양한 기능을 제공한다.

 

#include <iostream>
#include <string>

int main()
{
	std::string s = "cake";
    
    std::cout << s.length() << std::endl;
    
    std::string s2 = "make";
    
    if (s == s2) std::cout << "s and s2 are same." << std::endl;
    else std::cout << "s is different with s2." << std::endl;
    
    std::cout << s + s2 << std::endl;
    
    return 0;
}

  컴파일 해보면 두 문자열이 같지 않기 때문에 else문의 내용이 출력되며, s + s2의 결과물로 cakemake가 출력됨을 확인할 수 있다. 문자열 사이의 덧셈(즉, 두 문자열 붙이기) 뿐 아니라 비교 또한 ==로 이루어지며, 문자열의 길이 또한 length()를 통해 확인할 수 있음을 알 수 있다. 이 외에도 다양한 기능이 있다는 듯 싶다.

 

 

클래스의 상속(Inheritance)

  게임을 개발하는 상황을 상정해보자. 다양한 몬스터가 존재할 것이고, 몬스터마다 다양한 특징을 갖고 있을 것이다. 그러나 아무리 특징이 다양해도 공통적으로 체력, 이동속도 등의 공통점을 가지고 있을 것이다. 특징이 약간씩 다르다는 이유로 공통점이 훨씬 더 많은 몬스터들의 클래스를 각각 따로 구현하는 것은 너무 귀찮고, 반복적일 것이다. 이런 경우, 우리는 '상속'을 사용하여 이러한 클래스를 간단히 구현할 수 있다.

 

class Sample
{
	std::string name;
    
    public:
    	Sample() : name("기반") {	std::cout << "기반 클래스" << std::endl;	}
        
        void foo() {	std::cout << name << std::endl;		}
};

class DerivedSample : public Sample
{
	std::string name;
    
    public:
    	DerivedSample() : Sample(), name("파생")
        {
        	std::cout << "파생 클래스" << std::endl;
            foo();
        }
};

  Sample이라는, 기반이 될 클래스를 작성하고 이를 상속하는 DerivedSample 클래스를 작성하였다. C++에서 어떤 클래스를 상속하고자 할 때는, 상속할 클래스를 상속 받는 클래스의 정의 뒤에 :와 함께 적어주면 된다. 즉, 아래와 같은 형식을 갖는다. 접근 제한자의 역할에 대해서는 후술한다.

 

class [상속 받는 클래스] : [접근제한자] [기반이 되는 클래스]
{
	/* ... */
}

  DerivedSample클래스의 구성을 살펴보면, 생성자에서 기반 클래스인 Sample의 생성자를 호출하는 것을 알 수 있다. 여기에서는 기반 클래스의 생성자를 명시적으로 호출하였지만, 그러지 않았을 경우에는 기반 클래스의 디폴트 생성자가 호출된다고. 또한 생성자 내에 foo함수를 호출하는 부분이 있는데, 비록 DerivedSample 클래스는 foo함수를 정의하고 있지 않지만 그 기반 클래스인 Sample클래스가 foo함수를 갖고 있기 때문에 foo함수의 호출이 가능해진다.

 

  작성한 클래스를 바탕으로 객체를 생성해본다.

 

class Sample
{
	std::string name;
    
    public:
    	Sample() : name("기반") {	std::cout << "기반 클래스" << std::endl;	}
        
        void foo() {	std::cout << name << std::endl;		}
};

class DerivedSample : public Sample
{
	std::string name;
    
    public:
    	DerivedSample() : Sample(), name("파생")
        {
        	std::cout << "파생 클래스" << std::endl;
            foo();
        }
};

int main()
{
	Sample s;
    DerivedSample ds;
    
    return 0;
}

 

컴파일 결과

  코드를 컴파일할 경우, 위와 같은 결과가 출력된다. 먼저 선언된 Sample 타입의 객체가 생성되며 기반 클래스의 생성자가 호출되고, 후에 선언된 DerivedSample 타입의 객체가 생성되며 파생 클래스의 생성자가 호출된 것이다. 이때, 파생 클래스의 생성자는 기반 클래스의 생성자를 포함하고 있기에 기반 클래스의 생성자가 먼저 호출되고, 그 다음에 파생 클래스의 생성자가 호출됨을 알 수 있다.

 

  다만, foo 함수의 호출 부분을 살펴보면 분명히 파생 클래스인 DerivedSample 클래스에서 호출되었음에도 '기반'이라는 글자가 생성된 것을 알 수 있다. 이는 foo 함수가 Sample 클래스에 선언되어 있기에 일어나는 일이다. 이런 경우, 똑같은 역할을 하는 foo 함수를 DerivedSample 클래스에도 선언해주면 제대로 '파생'이라는 글씨가 출력될 것이다. 이런 것을 '오버라이딩(Overriding)'이라고 칭한다. 오버로딩과 헷갈리지 않게 주의하기.

 

 

상속에서의 접근제한자

  앞서 예시 코드에서 Sample 클래스를 상속받을 때 'public'을 사용하여 상속받았었다. 이 친구의 역할을 제대로 알아보기로 한다. 그러기 위해선 앞의 코드에서 확인할 것이 있다.

#include <iostream>
#include <string>

class Sample
{
	std::string sample_name;
    
    public:
    	Sample() : sample_name("기반") {	std::cout << "기반 클래스" << std::endl;	}
        
        void foo() {	std::cout << sample_name << std::endl;		}
};

class DerivedSample : public Sample
{
	std::string name;
    
    public:
    	DerivedSample() : Sample(), name("파생")
        {
        	std::cout << "파생 클래스" << std::endl;
            sample_name = "hello";
            foo();
        }
};

int main()
{
	Sample s;
    DerivedSample ds;
    
    return 0;
}

  코드를 살펴보면, DerivedSample 객체 안에서 그 기반 클래스인 Sample 타입 객체의 private 변수를 바꾸려는 시도를 하고 있음을 알 수 있다. 이 코드는 컴파일 시 에러를 내는데, 아무리 상속받은 클래스라도 private 변수에 접근하는 것을 불가능하기 때문이다. 그렇기 때문에 파생 클래스가 자신의 기반 클래스의 변수에 접근하고 싶다면 다른 접근 제한자를 사용하여야 한다. 이러한 처리는 매우 간단하다. Sample 클래스에서 private으로 선언된 부분을 'protected'로 바꾸어주기만 하면 된다.

 

#include <iostream>
#include <string>

class Sample
{
	protected:
		std::string sample_name;
    
    public:
    	Sample() : sample_name("기반") {	std::cout << "기반 클래스" << std::endl;	}
        
        void foo() {	std::cout << sample_name << std::endl;		}
};

class DerivedSample : public Sample
{
	std::string name;
    
    public:
    	DerivedSample() : Sample(), name("파생")
        {
        	std::cout << "파생 클래스" << std::endl;
            sample_name = "hello";
            foo();
        }
};

int main()
{
	Sample s;
    DerivedSample ds;
    
    return 0;
}

  이렇게 바꾸면 바로 컴파일이 되는 것을 확인할 수 있다.

  protected는 정확히 말하자면 private과 public 사이의 역할을 하는 접근 지시자로, 상속 받는 클래스에서는 접근이 가능하지만 그 외에서는 접근이 불가능하도록 만들어준다.

 

  그렇다면 클래스를 상속할 때 붙이는 접근 지시자는 어떠한 역할을 수행하는가. 이 때의 접근 지시자는 '기반 클래스에 선언된 멤버들이 기반 클래스에서 어떻게 작용하는가'를 결정한다.

접근 지시자 역할
public 별다른 변화 없이 기반 클래스의 접근 지시자들이 그대로 작용한다.
protected 기반 클래스에서 public으로 정의되어 있던 것들이 protected로 바뀐다.
private 기반 클래스의 모든 멤버들이 private으로 바뀐다.

 

 

특수화와 일반화

  앞서 클래스의 상속을 언급할 때, 몬스터에 대해 이야기한 적이 있다. 기본적인 몬스터 클래스가 있고, 그것을 상속받아 더 특수한 기믹을 가진 몬스터 클래스를 만들었다고 치자. 이렇게 만들어진 특수한 몬스터 클래스도 '어쨌거나' 몬스터이다. 다만 일반적인 몬스터와는 달리 좀더 이런저런 추가적인 특징을 갖고 있을 뿐이다. 이렇게 파생 클래스는 기반 클래스를 상속받는 행위는, 파생 클래스가 기반 클래스보다 조금 더 특수하게 된다는 이유로 '특수화(Specialize)' 라고 표현되기도 한다. 그 반대의 경우는 (특수해지지 않는다==평범하고 일반적인 것이 된다) '일반화(Generalize)' 라고 부른다.

'공부 > Programming Language' 카테고리의 다른 글

C++ STL - 연관 컨테이너  (0) 2021.06.08
C++ STL - 시퀀스 컨테이너  (0) 2021.06.04
C++ 4일차  (1) 2021.05.07
C++ 3일차  (1) 2021.05.03
C++ 2일차  (0) 2021.04.30