C++은 할건데, C랑 다른 것만 합니다. 2편 new/delete, class

0

C++

목록 보기
2/10

new, delete, class

https://modoocode.com/169

본 글은 위 글을 정리한 내용입니다.

c++과 c언어의 가장 큰 차이가 무엇인가? 라고 한다면 바로 class의 유무이다.

c언어로 객체지향 프로그래밍을 하기에는 어렵다. 물론 할 수는 있지만 여러가지 제약이 많고 우리가 아는 모습도 아니다.

반면 c++은 객체지향 프로그래밍이 가능하기 때문에 더욱 유연하고 다양한 프로그래밍 기법이 가능하다. 물론 그렇다고해서 c++이 c보다 꼭 더 좋다고 말하는 것은 아니다.

fancy한 문법은 언제나 사용자에 따라 그 퀄리티와 복잡도가 달라지기 때문이다.

1. new/delete

c언어에서 힙(heap) 메모리 영역에 변수를 할당, 해제할 수 있는 함수는 malloc, free였다.

c++에서도 malloc, free를 사용할 수 있지만, 따로 new와 delete라는 것도 사용할 수 있다.

new와 malloc, delete와 free는 비슷하지만 사용 방법에 있어 new와 delete가 더 간편하다는 장점이 있다.

#include <iostream>
#define MAX_ARR_SIZE 10
using namespace std;

int main(){
    int *value = new int;
    int *arr = new int[MAX_ARR_SIZE];
    for(int i = 0; i < MAX_ARR_SIZE; i++){
        arr[i] = i;
    }
    *value = 20;
    cout << *value << endl;

    for(int i = 0; i < MAX_ARR_SIZE ; i++){
        cout << arr[i]  << ' ';
    }

    delete value;
    delete[] arr;
}
20
0 1 2 3 4 5 6 7 8 9 

이를 보면 new 다음에는 type이 나오는 것을 알 수 있다. 즉 type의 sizeof만큼 메모리를 힙에 할당하고, 해당 주소 영역을 포인터 변수에 넣어주는 것을 알 수 있다.

또한, 배열을 선언할 때는 [size]만큼 type의 공간을 잡아주는 것을 확인할 수 있다. 따라서 다음처럼 일반화 할 수 있다.

T *pointer = new T[ARR_SIZE];

다음에 처럼 일반화 할 수 있다. 타입이 안정해져있다는 것은 구조체에도 사용이 가능하다는 것이다.

delete는 힙에 할당된 메모리 공간을 해제할 수 있다. 지역 변수와 같이 스택에 있는 변수들은 삭제하지 못한다.

일반 변수를 지울 때는 delete, 이고 배열을 지울 때는 delete[] 이다.

2. 클래스

클래스는 객체를 표현하는 일종의 '틀'이다.

가장 많이 설명하는 예제로는 붕어빵 틀은 '클래스'고, 붕어빵 틀에서 나온 붕어빵은 '객체'라는 것이다.

즉, 클래스는 설계도이고 객체는 그 실제 결과물이다.

그래서 러프하게 클래스와 객체를 두루뭉실하게 비슷하게 설명할 때가 많으니 너무 딱딱 나누려고 하지말자.

객체들은 현실 세계의 사물이나 의식, 인식들 과 같은 유형, 무형의 것들을 프로그램으로 표현한 것이다.

현실 세계의 모든 것들은 행동상태를 갖고 있다고 정의하면, 객체들은 행동상태를 갖고 있게 된다.


출처 : https://www.google.com/search?q=class%20encapsulation%20java&tbm=isch&hl=en&tbs=rimg:CdykIi5E4Kv6YQcbw5w4oT4NsgIGCgIIABAA&rlz=1C1FKPE_koKR961KR961&sa=X&ved=0CBsQuIIBahcKEwj4haTqh_zyAhUAAAAAHQAAAAAQeA&biw=1903&bih=880#imgrc=X1vHQ3-zft-YdM

우리는 상태는 변수로, 행동은 메서드(객체 안의 함수)로 표현할 것이다.

그리고 현실 세계의 것을 컴퓨터 상에 표현하는 것을 추상화라고 한다. 현실의 것을 추상적으로 표현했다고 생각하면 된다.

  • cpp에서의 클래스

    c++에서 현실 세계의 것을 추상화하기 위해서는 class 키워드를 사용하여 클래스를 정의하고 new를 사용해서 객체를 만들어내면 된다.

#include <iostream>

using namespace std;

class Carp {
    private:
        int weight;
        int height;
        int width;
    public:
        void setWeight(int _weight){
            weight = _weight;
        }
        void setHeight(int _height){
            height = _height;
        }
        void setWidth(int _width){
            width = _width;
        }
        int getWeight(){
            return weight;
        }
        int getHeight(){
            return height;
        }
        int getWidth(){
            return width;
        }
        void print_info(){
            cout << "carp weight : " << weight << " width : " << width << " height : " << height << endl;
        }
};

int main(){
    Carp carp;
    carp.setHeight(10);
    carp.setWeight(20);
    carp.setWidth(30);
    carp.print_info();
}
carp weight : 20 width : 30 height : 10

다음과 같이 Carp클래스를 만들고, carp 객체를 만들어 낼 수 있다.

Carp클래스에서 weight, height, width는 하나의 상태를 나타내는 변수이다.

setWidth, setHeight, setWeight,getWidth, getHeight, getWeight, print_info은 Carp의 행동을 나타내는 메서드이다.

여기서 변수는 private , 메서드는 public으로 설정하였다. 이들은 클래스 안에 있으므로 맴버 변수와 맴버 함수라고도 부른다.

나중에 배우겠지만 private는 클래스 내부에서만 접근 가능하다는 것이고, public은 클래스 내부가 아닌, 외부에서도 접근이 가능한 것들이다.

따라서, 메서드는 객체에서 . operator를 이용해서 접근할 수 있었지만, 변수들은 . operator로 접근할 수 없다.

carp.weight = 10; // error!!

왜 변수들은 private에 넣고, 메서드는 public에 넣을까??

내부적인 상태는 클래스에 있어 굉장히 중요한 요소이다. 상태에 따라 같은 메서드도 다른 결과를 낼 수 있으며, 상태에 따라 해당 클래스의 의미도 달라지기 때문이다.

그렇기 때문에 상태는 외부 인자로 접근하지 못하도록하는 것이다. 가령, 휴대폰을 사용하는데 있어서 우리는 버튼을 누리고, 설정하는 행동(메서드)를 할 뿐이지, 내부의 상태(변수)를 직접 바꿔서 휴대폰을 설정하진 않는다.

즉, 휴대폰을 통해 전화를 할 때, 우리는 단지 번호를 누르고 통화 버튼을 누를 뿐, 그 안에 내부적으로 어떻게 돌아가는 지는 몰라도 되고, 노출되어서도 안된다.

만약 우리가 직접 상태(변수)까지 설정해주어야 한다면 전화 한 번거는데 엄청난 어려움이 있을 것이다.

이렇게, 내부동작은 모르지만 메서드가 내부 상태(변수)를 관리해주고, 동작해주는 것을 보고 캡슐화(encapsulation)했다고 한다.

또, 내부 동작에 있어서 보안적으로 노출되어서는 안되는 부분을 가리는 것을 정보 은닉(information hiding)이라고 한다.

즉, 캡슐화를 통해 정보 은닉을 하여 보안적인 측면을 강화할 수 있고, 일련의 과정을 단순하게 보여주어 복잡도를 낮추게 할 수 있다.

추상화캡슐화, 정보 은닉은 이후에 나올 상속과 다형성과 더불어 객체 지향프로그래밍의 중요한 관점이므로 잘 알아두자.

3. 오버로딩과 생성자

3.1 함수 오버로딩

오버로딩은 같은 이름의 함수이지만, 파라미터 타입, 파라미터 수이 달라 다른 함수처럼 여겨지는 것을 말한다. 단, 반환형은 무조건 같아야 한다. 만약 반환형 타입만 다른 경우, 어떤 함수를 써야하는 지 컴파일러가 모르기 때문이다.

c언어에서는 오버로딩이 불가능하다. make_file이라는 함수가 하나라도 선언되어있다면 파라미터 수를 달리하든, 타입을 달리하든 상관없이 먼저 선언한 make_file만 호출된다.

하지만 c++에서는 함수 오버로딩이 가능하다.

#include <iostream>

using namespace std;

void print_info(int id){
    cout << "info : " << id << endl;
}

void print_info(char* name){
    cout << "info : " << name << endl;
}

void print_info(int id, int addr){
    cout << "info : " << id << " " << addr << endl;
}

int main(){
    print_info(10);
    print_info("hello");
    print_info(10,20);
}
info : 10
info : hello
info : 10 20

다음과 같이 오버로딩이 되는 것을 알 수 있다.

오버로딩은 함수 이름은 같지만 매개변수 타입, 개수에 따라 다른 함수처럼 사용할 수 있다는 것을 알아두자.

이런 오버로딩을 클래스에서도 사용할 수 있다.

#include <iostream>

using namespace std;

class Log{
    private:
        int status;
    public:
        void setStatus(int _status);
        void print_info(int id);
        void print_info(char* name);
        void print_info(int id, int addr);
};

void Log::setStatus(int _status){
    status = _status;
}

void Log::print_info(int id){
    cout << "info "<<status << ": " << id << endl;
}

void Log::print_info(char* name){
    cout << "info "<<status << ": "<< name << endl;
}

void Log::print_info(int id, int addr){
    cout << "info "<<status << ": "<< id << " " << addr << endl;
}

int main(){
    Log log;
    log.setStatus(1);
    log.print_info(10);
    log.print_info("hello");
    log.print_info(10,20);
}
info 1: 10
info 1: hello
info 1: 10 20

위의 코드를 보면 같은 클래스 내의 print_info 함수가 3개가 있지만 매개 변수의 타입과 수가 달라 오버로딩되어있다.

참고로 class안에 함수가 너무 많아지면 복잡해지므로 class에는 함수의 선언만을 적어넣고, 함수의 정의는 따로 떼어놓을 수 있다.

이때 함수를 클래스 밖에서 정의할 때는 선언과 동일하게 적되, 앞에 해당 클래스의 메서드이다. 라는 것을 알려주기 위해 class::method라고 써준다.

위의 예제에서는 void Log::print_info(int id)라고 써줬다.

3.2 생성자(constructor)

클래스에서 상태는 맴버 변수로 표현된다고 했다. 만약 맴버 변수의 초기값을 설정해주려면 어떻게 해야할까??

그것이 바로 , 생성자이다. 생성자 함수는 클래스를 통해 객체가 생성될 때 호출되는 함수로 초기 상태을 설정해준다.

class Carp {
    private:
        int weight;
        int height;
        int width;
    public:
        void setWeight(int _weight){
            weight = _weight;
        }
        void setHeight(int _height){
            height = _height;
        }
        void setWidth(int _width){
            width = _width;
        }
        int getWeight(){
            return weight;
        }
        int getHeight(){
            return height;
        }
        int getWidth(){
            return width;
        }
        void print_info(){
            cout << "carp weight : " << weight << " width : " << width << " height : " << height << endl;
        }
};

해당 클래스의 weight와 width, height를 설정해주려면 get,set함수들을 이용하여 값들을 채워주어야 한다.

그런데 기본적으로 Carp객체를 생성할 때 weight, width, height를 넣어주고 싶다.

매번 값들을 setting하기 어렵기도 하고, 복잡하다. 그래서 생성자 함수를 통해 기본 값들을 설정할 수 있도록 하자

#include <iostream>

using namespace std;

class Carp {
    private:
        int weight;
        int height;
        int width;
    public:
        Carp(int _weight, int _height, int _width){
            weight = _weight;
            height = _height;
            width = _width;
        }
        void setWeight(int _weight);
        void setHeight(int _height);
        void setWidth(int _width);
        int getWeight();
        int getHeight();
        int getWidth();
        void print_info();
};

void Carp::setWeight(int _weight){
    weight = _weight;
}
void Carp::setHeight(int _height){
    height = _height;
}
void Carp::setWidth(int _width){
    width = _width;
}
int Carp::getWeight(){
    return weight;
}
int Carp::getHeight(){
    return height;
}
int Carp::getWidth(){
    return width;
}
void Carp::print_info(){
    cout << "carp weight : " << weight << " width : " << width << " height : " << height << endl;
}

int main(){
    Carp carp1(10,10,10);
    carp1.print_info();
}
carp weight : 10 width : 10 height : 10

이와 같이 생성자 함수를 사용하여 객체가 생성될 때 초기 상태값을 넣을 수 있다.

Carp(int _weight, int _height, int _width){
    weight = _weight;
    height = _height;
    width = _width;
}

이 처럼 생성자는 반환형이 없고, 클래스와 이름이 같아야 한다.

단, 생성자 함수는 오버로딩이 가능하기 때문에 매개변수의 타입과 수를 통해서 구분할 수 있다.

Carp carp1(10,10,10);
carp1.print_info();

Carp carp1(10,10,10);으로 생성자 함수를 호출 할 수 있다.

그런데 만약, 생성자 함수가 없는 경우는 어떨까?? 사실 생성자 함수는 모든 클래스에 다 있다. 이것이 바로 default constructor, 기본 생성자 함수이다.

class Carp {
    private:
        int weight;
        int height;
        int width;
    public:
};

만약 다음과 같이 생성자 함수가 없는 클래스를 선언했다고 하자.

이때, 객체를 생성하기 위해서는 반드시 클래스의 생성자 함수를 호출해야 한다. 따라서 컴파일러는 생성자 함수가 없는 Carp에 아무것도 안해주는 default constructor를 넣어준다.

class Carp {
    private:
        int weight;
        int height;
        int width;
    public:
        Carp(){};
};

Carp carp1;
carp1.print_info();

Carp(){};는 정말 아무것도 안하는 생성자 함수이다. 따라서 상태값들은 기본적으로 알 수 없는 값들이 채워진다.

또한, 디폴트 생성자는 모든 경우에 넣어주는 것은 아니다. 생성자 함수가 하나라도 있다면 디폴트 생성자를 컴파일러가 자동으로 넣어주지 않는다.

따라서 위의 클래스

class Carp {
    private:
        int weight;
        int height;
        int width;
    public:
        Carp(int _weight, int _height, int _width){
            weight = _weight;
            height = _height;
            width = _width;
        }
        void setWeight(int _weight);
        void setHeight(int _height);
        void setWidth(int _width);
        int getWeight();
        int getHeight();
        int getWidth();
        void print_info();
};

에서, Carp(){} 라는 기본 생성자가 없기 때문에

Carp carp;

는 에러가 발생한다.

c++11에 와서는 컴파일러 지시자를 통해 컴파일러에게 디폴트 생성자를 추가하라고 지시할 수 있다.

Carp() = default;

사실상 내가 만드는 것과 별반 수고스러움의 차이는 없는 듯하다.

마지막으로 생성자 오버로딩이다.

위에서 생성자를 오버로딩할 수 있다고 했다. 따라서 우리는 기본 생성자와 매개변수가 있는 생성자를 오버로딩하여 각기 다른 생성자를 호출해 객체를 생성할 수 있다.

class Carp {
    private:
        int weight;
        int height;
        int width;
    public:
        Carp(){

        }
        Carp(int _weight, int _height, int _width){
            weight = _weight;
            height = _height;
            width = _width;
        }
        void print_info();
};

Carp carp1(10,10,10);
carp1.print_info();

Carp carp2;
carp2.print_info();

다음과 같이 오버로딩된 두 개의 Carp 생성자를 만들 수 있고, 생성자를 호출하여 값을 셋팅할 수 있다.

0개의 댓글