본문으로 바로가기

Day-13 C++ 이야기3

category Programming/TIPS 17기 2017. 8. 14. 10:47

 

 

new 연산자와 delete 연산자

 

C언어에서는 동적으로 메모리를 할당하기 위해서 malloc 함수를 사용했다.

 

1
int *= (int *)malloc(sizeof(int));
cs

 

C++언어에서는 new라는 연산자가 추가 되어 동적으로 메모리 할당을 할 수 있다. C++언어에서도 malloc 함수를 사용할 수 있지만 new 연산자의 표현이 더 좋아서 malloc 함수보다 new 연산자를 더 많이 사용한다.

 

 

정리해보면 동적 메모리 할당을 위해서 C언어에서는 함수를 사용하고 C++언어에서는  연산자를 사용한다. 함수와 연산자의 차이를 알아보자.

함수는 C언어 문법이 아니지만 연산자는 C언어 문법이다. 연산자는 컴파일러가 자신의 기준에 따라서 기계어로 바꾸지만 함수는 컴파일러와 전혀 상관없다. C언어의 malloc 함수는 컴파일러와 문법적으로 약속된 부분이 없어서 매개변수, 형변환, sizeof 연산자를 사용해  표현해야 했지만 C++언어에서 new 연산자는 연산자이기 때문에 컴파일러와 문법적으로 약속되어 있다. 약속된 문법을 사용해서 정해진 방식으로 처리하기 때문에 표현이 malloc 함수에 비해서 단순하다.

 

함수는 컴파일러가 이해할 수 있는 상황이 아니기 때문에 주저리 주저리 적어야 한다.

연산자는 자료형만 적으면 컴파일러가 이해할 수 있기 때문에 간단히 사용할 수 있다.

 

연산자를 사용하게 되면 C++ 문법의 일부가 되기 때문에 좋다. 따라서 malloc 함수보다 new 연산자를, free 함수보다 delete 연산자를 사용하는 것이 좋다.

 

new 연산자를 사용할 때 그룹 지어진 형식의 메모리 할당도 가능하다.

 

 

C언어에서 malloc 함수로 동적 메모리할당을 하면 free 함수로 메모리 할당 해제를 했듯이 C++언어에서도 new 연산자로 동적 메모리 할당을 하면 메모리 할당을 해제하는 delete라는 연산자가 있다. new 연산자와 delete 연산자는 쌍으로 사용한다.

 

malloc 함수로 동적 메모리를 할당하고 delete 연산자로 할당된 메모리를 할당할 수 있고, new 연산자로 동적 메모리를 할당하고 free 연산자로 할당된 메모리를 해제할 수 있다. 오류가 발생하지 않지만 권장하지 않는 표현이다.

 

delete 연산자를 사용할 때 주의 사항이 있다. 단순하게 메모리가 할당 했는지 그룹 형식으로 메모리를 할당 했는지에 따라서 delete 연산자의 사용이 약간 다르다.

 

단순하게 메모리를 할당한 경우 - 단순히 delete 연산자를 사용해야 한다.

 

 

그룹 형식으로 할당한 경우 - delete[ ] 연산자를 사용해야 한다.

 

 

C++언어에서 new 연산자가 추가된 이유

 

클래스 문법을 사용해서 객체를 생성했다고 가정해보자. 객체에도 동적 메모리가 할당될 수 있다. 하지만 malloc 함수로 객체에 동적 메모리 할당을 하면 비정상적인 상황이다. 왜냐하면 객체는 선언이 되면 메모리만 할당하는 것이 아니고 객체 생성자가 자동으로 호출되어 객체 스스로 초기화 하는 작업을 한다. 하지만 객체를 malloc 함수를 사용하여 메모리 할당을 하면 단지 메모리 할당만 되고 객체 생성자는 호출되지 않는다. malloc 함수는 함수이기 때문에 C++의 class 문법의 특성을 반영할 수 없어 class 문법의 특성을 반영할 수 있는 new 연산작 추가 되었다.

 

Instance

 

C++언어에서 객체를 선언할 때 단순히 메모리만 할당되는 것이 아니라 객체 생성자도 호출된다. 따라서 객체를 선언할 때 "변수를 선언한다", "메모리를 할당한다"라고 말하는것은 올바르지 않다. 따라서 새로운 용어를 정의할 필요가 생겼는데 객체를 선언할 때 "객체를 선언한다.", 객체를 인스턴스(Instance)한다"라고 말한다.

 

link - bind - embed (용어 정리)

 

bind -- 인프라가 있고 인프라에 연결되는 것이다. ex) 콘센트가 있는 이유 : 전원에 꽂기 위해서 존재한다. 콘센트는 인프라, 꽂는 행위는 바인드이다.

link -- 물리적(정적)으로 연결되어 있는 것이다. ex) 타이어가 차에 연결 되어있다. 타이어가 차에 링크되어 있다고 한다.

 

embed -- 다른 프로그램내에 실시간으로 삽일할 수 있는 것이다. ex) 자동차에 설치되어 있는 카오디오. 차와 별개의 시스템으로 라디오가 차에 임베디드 되어 있다 라고 한다.

 

버그를 줄이기 위한 Reference 문법 (C++ 언어에서 잘 사용하지 않는다.)

 

Reference 문법은 포인터 사용시 실수를 막기 위한 문법이다.

 

포인터 문법의 const 키워드

 

포인터 문법에서 실수를 줄이기 위해 const 키워드를 사용한다. 하지만 아래와 같이 const 키워드를 사용하면 항상 *를 붙이고 다녀야 하기 때문에 실수로 *를 적지 않는 경우도 발생할 수 있다.

 

 

Reference 문법

 

주소를 변경할 수 없도록 하고 *를 붙이지 않아도 사용 가능하도록 불편함을 해소한 문법이다.

 

 

포인터 문법을 사용하든 참조 문법을 사용하든 기계어로 번역시 동일한 기계어 코드로 번역된다. 즉, 표기법만 바꾼것이지 성능의 문제는 아니다. 또한 기계어로 번역될 때 참조 문법을 포인터 문법으로 바꾸어서 기계어로 바꾼다. 실무에서 Refernce 문법은 거의 사용하지 않는다. 포인터의 다양한 표기법에 제한을 주고 다른 사람이 소스코드를 볼 때 오판의 여지가 있기 때문이다.

 

this 포인터

 

C++ 언어에는 변수가 인스턴스되면 변수와 함수가 메모리에 올라간다고 생각하는데 전혀 상관없다. 코드가 데이터 세그먼트에 올라갈 수 없다.

this 포인터를 제대로 알아보기 위해서 C언어 프로그램과 C++언어 프로그램을 비교해보자.

 

C언어로 구현한 이름과 나이를 출력하는 예제

 

 

C++언어로 구현한 이름과 나이를 출력하는 예제

 

 

C언어로 구현한 프로그램과 C++언어로 구현한 프로그램을 비교하면 포인터 문법이 사라진것처럼 보인다. C언어에서는 InputData ㅎ마수에 포인터를 사용했는데 C++언어에서는 포인터를 사용하지 않고 멤버 변수들을 바로 사용한다. 왜 이렇게 사용되는지 알아보기 위해서 this 포인터에 대해 알아볼 필요가 있다.

 

 

 

C++ 언어에서 명시적으로 자신이 사용할 데이터의 주소를 넘기지 않고 'data.'과 같은 표현으로 넘긴다. 따라서 명시적으로 주소를 넘기지 않았기 때문에 포인터 변수를 주소로 받는 함수에 매개변수가 없다. 즉, 소스 코드에는 직접 표현이 되지 않았지만 멤버 함수에서는 this 포인터가 선언된 것으로 처리되고 이 thist포인터는 'data.'표현에 의해 전달된 주소가 저장된다.

 

 

위와 같이 선언되어 있다. 하지만 프로그래머가 직접 this 포인터를 선언하면 오류가 난다. 왜냐하면 이미 내부적으로 선언되어있기 때문이다.

즉, 내부적으로 자동으로 this 포인터를 사용한다. 컴파일러가 this 포인터를 만들어서 사용한다. 따라서 주소를 넘기거나 매개변수를 받는 행위를 하지 않아도 된다. 그로인해 포인터가 없는것처럼 사용할 수 있다.

 

 

위와 같이 사용할 수 있는데 결과적으로 'this->'는 구별이 가능할 때 생략이 가능한 표현이라서 생략하고 사용하면 맨 위에서 구현한 소스와 같아진다.

 

'this->'는 구별이 가능할 때 생략이 가능하다고 했다. 그러면 구별을 할 수 없을때는 언제일까? 아래와 같이 멤버 변수와 지역 변수의 이름이 동일할 때에는 두 변수를 구별하지 못하기 때문에 'this->'는 생략할 수 없다. 'this->'를 정확하게 사용하여 사용할 대상이 멤버 변수임을 표시해 주어야 한다.

 

 

C++언어에서 포인터 표현을 적절하게 숨긴 형태이지만 포인터 문법이 사라진것은 아니다. 또한 멤버함수에서 멤버 함수를 호출한 객체의 주소를 다른 함수로 전달할 필요가 있을 때, 외부에서 객체의 주소를 전달받아서 사용할 필요 없이 this 포인터를 활용하여 사용할 수 있다.

 

상속

 

프로그램을 구현할 때 가장 큰 문제는 소스코드의 중복이 가장 큰 문제이다. C언어에서는 소스코드의 중복을 함수로 막는다. C++언어에서는 소스코드의 중복을 클래스로 막는다. 하지만 클래스를 찍어내는 문제 중 중간에 버그가 발견되었을때 모든 클래스를 일일히 고쳐야 하는 문제가 발생한다.

 

상속 문법을 사용하면 동일한 코디가 명시한 곳에 있으니 이 클래스는 해당 클래스와 기본적으로 동일하다는 뜻으로 컴파일러가 판단한다.

 

동일한 코드를 복제하면 나중에 변화 발생시 문제가 발생하기 때문에 상속을 사용하면 더 편하게 할 수 있다. 클래스 안에 있는 코드가 중복되어서 유지보수가 힘든 상황을 많이 완화할 수 있다.

 

클래스를 상속하지 않고 복사해서 사용하는 경우

 

일일히 클래스를 복사한후 클래스 명만 바꾸어 사용한다. 클래스에 변화가 생기면 복사한 모든 클래스에 수정을 해야한다.

 

class a                            class a2                            class a3

{                                      {                                        {

 

...                                     ...                                       ...

 

};                                     };                                       }

 

클래스 상속을 한 경우

 

class a                            class a2 : public a

{                                      {

        // class a와 "소스코드가 동일하다라"는 뜻이다.

...

 

};                                     };

 

class A 부모 클래스

 

class B : public A            //  클래스 B는 자식클래스이다.  public은 상속 방식으로 protected, private로 사용할 수 있다.

//  상속을 할 때 메모리는 다 상속되어 있는데 문법적으로 상속 방식에 따라 문법적으로 메모리를 쓰지 못하게 막아 놓는다.

 

상속을 사용하면 부모 클래스의 소스만 바꾸어 주면 자식 클래스에 반영이 되기 때문에 유지 보수에 편리하다.

 

private : 개인적인, 사적인이라는 뜻으로 아버지 입장에서 어머니 같은 존재로 비유된다. 접근 제한자가 2등급 상승한다. 실제로 거의 사용되지 않는다.

protected : 보호된 이라는 뜻으로 재산에 비유된다. 접근 제한자가 1등급 상승한다.

 

상속 등급은 거의 public을 주로 사용한다. 부모 클래스를 어떤 등급으로 상속 하느냐에 따라 자식 클래스에서 부모 클래스 멤버에 접근할 수 있는 등급이 달라진다.

 

오버라이딩(재적재, 올라탄다)

 

클래스간의 상속 관계에서 상위 클래스에 존재하는 함수를 하위 클래스에서 기능을 재정의하여 사용하는것을 오버라이딩이라고 한다.

 

오버라이딩은 오버로딩과 달리 함수의 이름, 인자의 개수, 인자의 데이터 타입이 반드시 일치해야한다. 이 중 인자가 늘어나거나 타입이 늘어나면 오버로딩이 발생한다. 오버라이딩은 반드시 상속 관계에서만 발생한다. 오버로딩보다 오버라이딩이 거의 대부분으로 사용하는 기술이다.

 

오버라이딩이 필요한 이유

 

상속을 할 때 보통의 경우에 부모 클래스를 그대로 두고 그 밑에 자기가 원하는 것을 추가한다. 이런 경우만 있는 것이 아니라 부모의 기능과 동일한데 부모 코드의 함수 중 일부가 마음에 들지 않아 수정하고 싶을때 상속 클래스에 c함수를 적는다. 그러면 a, b, d는 부모 클래스와 똑같이 사용하고 부모 클래스의 C만 배척하고 자기가 재정의한 c함수를 사용한다.

 

 

 

 

 

 

class b에는 오버라이딩 적용되어 test 함수를 호출하면 class b에 있는 test 파일이 호출된다. 하지만 상속으로 인해 class b에는 class a에 호출된 test 함수도 저장되어 있다. 따라서 class b에서 class a에 정의된 test함수도 호출할 수 있다.

 

 

 

 

다형성

 

소스 코드에서 데이터 타입이 바뀜에도 불구하고 내 코드에서는 데이터 타입이 바뀌지 않도록 하는 기술이다.

'캐스팅 작업을 하지 않고 자식 클래스를 부모 클래스의 포인터로 가리킬 수 있는 것'이 다형성의 의미이다.

 

 

위와 같이 p -> Test();를 하면 class A에서 호출된 Test 함수가 호출되 "Reversing"이라는 문자열이 출력된다. 그렇다면 자식 클래스의 Test 함수를 호출하려면 어떻게 해야 할까?

 

 

위와 같이 부모 클래스의 Test 함수에 virtual 키워드를 적으면 자식 클래스의 Test 함수가 호출되어 "pwnable" 문자열이 출력되는것을 볼 수 있다.

다형성을 사용하려면 virtual 키워드를 의도대로 사용할 수 있어야한다. 자식 클래스에서 사용할 가능성이 있는 함수를 부모 클래스에서 잘 만드는것이 중요하다.

 

다형성을 활용한 프로그램을 만들어보자.

 

 

위와 같이 프로그래밍 할 수 있다. 하지만 각 클래스의 내용을 보면 기능이 같은 함수들이 반복된다. 또한 클래스를 추가할 때마다 main 함수도 길어지고 변화가 많이 생긴다. 이러한것들에 착안해서 다시 다형성을 제대로 적용한 프로그램을 작성해보자.

 

 

위와 같은 코드가 다형성을 제일 잘 적용한 프로그램이라고 할 수 있다. class가 추가되면 main 함수에는 변화가 거의 생기지 않아 유지 보수가 쉬워진다.