클래스와 객체 선언 및 접근
클래스는 객체를 생성하기 위해 정의된 틀이다. 구조체와 유사하나 멤버 함수, 상속 등의 개념이 더해져있다. 클래스 내부에서는 멤버 변수와 멤버 함수 선언이 이루어진다. 멤버 변수 초기화값을 설정하여 멤버 변수의 초기값을 정해줄 수 있다.
객체는 클래스를 통해 생성된 것이다. 멤버 변수와 멤버 함수로 구성되며 메모리에 생성된다. 실체(instance)라고도 부른다. 하나의 클래스를 통해 여러 개의 객체 생성이 가능하며, 이렇게 생성된 객체들은 각각 별도의 메모리 공간에 할당된다.
클래스 선언부(declaration)는 class 키워드와 함께 앞서 언급한 바와 같이 멤버 변수와 멤버 함수의 선언으로 이루어진다. 클래스 구현부(implementation)에서는 클래스에 선언된 멤버 함수의 정의가 이루어진다. 클래스 선언은 보통은 전역(global)으로 이뤄지지만 지역(local)로도 가능하며, 클래스 구현, 즉 멤버 함수 정의는 클래스 선언부 내외부에서 모두 가능하다. 외부에서 정의해줄 경우 클래스 이름과 :: 을 앞에 붙여 클래스 멤버 함수라는 것을 나타낸다.
예를 들어 직사각형(rectangle)이라는 클래스를 만들어보자. 클래스의 멤버 변수는 밑변, 높이를, 멤버 함수로는 넓이를 구하는 함수, 정사각형인지 확인하는 함수를 넣는다 가정하면 아래와 같이 만들 수 있다.
class rectangle {
public:
int height;
int width;
int area() {
return height * width;
};
bool is_square() {
return height == width;
};
};
또는 아래와 같이 클래스 선언부 외부에 따로 클래스 구현부를 만들 수 있다. 즉 클래스 선언부 외부에서 멤버 함수를 정의할 수 있다.
class rectangle {
public:
int height;
int width;
int area();
bool is_square();
};
int rectangle::area() {
return height * width;
}
bool rectangle::is_square() {
return height == width;
}
이렇게 클래스를 정의했다면 클래스를 이용해서 객체를 선언하고 활용할 수 있다. 객체 선언은 아래와 같이 이루어진다.
rectangle A;
이 객체의 멤버 변수에 접근하는 것은 구조체의 멤버 변수에 접근하는 것과 동일하게 . 을 이용한다.
A.height = 1;
A.width = 2;
멤버 함수에 접근 하는 것도 같은 . 을 이용한다.
int A_area = A.area()
생성자 (Constructor) 및 소멸자 (Destructor)
- 생성자 (Constructor)
생성자는 객체가 생성되는 시점에서 자동으로 호출되는 멤버 함수이다. 따로 선언하지 않더라도 컴파일 과정에서 컴파일러에 의해 자동으로 생성된다. 따로 선언할 시에는 클래스 이름과 동일한 이름으로 선언해야 하며, return 타입을 따로 명시하지 않는다.
생성자를 통해 객체가 생성될 때 멤버 변수에 대한 초기화를 즉각 해줄 수 있으며, 특정 기능을 구현할 수도 있다. 이러한 생성자는 객체 선언시 단 한번 자동으로 호출되며 임의로 호출할 수 없다. 멤버 함수이므로 클래스 선언부 내외부에서 정의하여 사용하면 된다.
- 생성자 중복 (Constructor Overloading)
생성자는 하나의 클래스에 여러 개가 존재할 수 있는데 그 중 조건에 맞는 하나의 생성자만 실행된다. 예를 들어 위 예시로 나왔던 직사각형 클래스에 아래와 같이 명시적으로 생성자를 추가할 수 있다.
class rectangle {
public:
int height;
int width;
int area() {
return height * width;
};
bool is_square() {
return height == width;
};
rectangle() {
height = 1;
width = 2;
};
rectangle(int i, int j) {
height = i;
width = j;
};
};
이때 클래스 이름과 같은 생성자를 두 개 만들었는데, 둘 중 조건에 맞는 생성자가 객체 선언시 호출된다. 예를 들어 아래와 같이 객체를 선언한다고 가정하자.
rectangle A;
rectangle B(3, 5);
이때 A 는 rectangle() 이 호출되어 height = 1, width = 2 가 되었고, B 는 rectangle(int i, int j) 가 호출되어 height = 3, width = 5 가 되었다.
- 위임 생성자 (Delegating Constructor)
생성자를 여러 개 만들 때 각 생성자마다 중복되는 것을 줄이기 위해 위임 생성자를 활용할 수 있다. 위임 생성자는 생성자를 만들 때 다른 생성자를 호출할 수 있게 해준다. 위와 같은 생성자를 아래와 같이 만들 수도 있는 것이다.
rectangle::rectangle() : rectangle(1, 2) {}
rectangle::rectangle(int i, int j) {
height = i;
width = j;
}
- 멤버 초기화 리스트 (Member Initializer Lists)
생성자를 통해 멤버 변수를 초기화할 때 : (콜론)을 이용하여 보다 효율적이고 직관적으로 변수를 초기화해줄 수 있다.
rectangle::rectangle() : height(1), width(2) {}
rectangle::rectangle(int i, int j) : height(i), width(j) {}
위 생성자는 rectangle() 이 실행될 때는 height 를 1 로, width 를 2 로 초기화하고 rectangle(int i, int j) 가 실행될 때는 height 를 i 로, width 를 j 로 초기화한다.
- 소멸자 (Destructor)
객체가 소멸되는 시점에서 자동으로 호출되는 멤버 함수이다. 생성자와 마찬가지로 따로 선언하지 않더라도 컴파일 과정에서 컴파일러에 의해 자동으로 생성된다. 따로 선언할 시에는 클래스 이름과 동일한 이름 앞에 ~ (틸드)를 붙여 선언해야 하며, return 타입을 명시하지 않는다. 또한 생성자와 다르게 매개 변수(parameters)를 가질수도 없고, 중복 선언도 불가능하다.
예를 들어 위 예시로 나왔던 직사각형 클래스에 아래와 같은 소멸자를 추가해줄 수 있다.
rectangle::~rectangle() {
std::cout << "Destruction of a rectangle with height: " << height << " and width: " << width << std::endl;
}
- 생성자와 소멸자 실행 순서
객체는 선언된 위치에 따라 지역 객체와 전역 객체로 나눌 수 있다. 지역 객체는 함수가 호출된 순간에 선언된 순서대로 생성되어 함수가 종료될 때 역순으로 소멸한다. 전역 객체도 비슷하게 프로그램에 선언된 순서대로 생성되어 프로그램이 종료될 때 역순으로 소멸한다. 만약 new 를 이용하여 동적으로 생성된 객체가 있다면 new 를 실행하는 순간 객체가 생성되어 delete 를 실행할 때 소멸한다.
접근 지정자 (Access Modifier)
캡슐화는 중요한 멤버에 대해 다른 클래스나 외부에서 접근할 수 없도록 만들어 객체를 보호한다. 단 일부 멤버에 대해서는 외부 접근을 허용해야 하는 경우가 발생할 수 있다. 혹은 반대로 대부분 멤버에 접근 가능하지만, 특정 멤버에 대한 접근만 막아야 할 필요가 생길 수 있다. 이때 접근 지정자를 통해 멤버에 접근 가능한 권한을 부여하여 이를 조정할 수 있다. 접근 지정자의 종류는 아래와 같다.
- public: 모든 곳에 허용한다.
- protected: 클래스 자신과 상속받은 자식 클래스에만 허용한다.
- private: 해당 클래스의 멤버 함수에만 허용한다.
접근 지정자는 클래스 선언부에서 사용되는데, 멤버 앞에 선언된다. 예를 들어 위 직사각형 클래스 선언부를 확인하면 가장 위 public 접근 지정자가 사용된 것을 확인할 수 있다.
class rectangle {
public:
int height;
int width;
int area() {
return height * width;
};
bool is_square() {
return height == width;
};
};
이 클래스의 멤버 변수에 private 을 적용하면 아래와 같이 나타낼 수 있다.
class rectangle {
private:
int height;
int width;
public:
int area() {
return height * width;
};
bool is_square() {
return height == width;
};
};
접근 지정자는 중복 사용도 가능한데, 예를 들어 아래와 같이 사용할 수도 있는 것이다.
class rectangle {
private:
int height;
public:
int area() {
return height * width;
};
bool is_square() {
return height == width;
};
private:
int width;
};
또한 접근 지정자를 아예 사용하지 않은 경우 디폴트 접근 지정자는 private 이다. 즉 아래와 같이 코드를 구성한다면, 멤버 변수에 대해서는 private 접근 지정자가 적용된다.
class rectangle {
int height;
int width;
public:
int area() {
return height * width;
};
bool is_square() {
return height == width;
};
};
멤버 변수에 대해서는 private 지정이 바람직한데, 캡슐화(encapsulation)와 정보 은닉(information hiding)에 적합하기 때문이다. 멤버 변수에 직접 접근을 차단하기 때문에 멤버 함수를 통해서만 멤버 변수에 접근한다. 때문에 클래스가 객체의 상태 변화를 통제하기 쉽다. 예를 들어 특정 멤버 변수는 항상 양수여야 한다고 가정하자. 멤버 변수를 public 으로 지정하면 멤버 변수가 음수가 될 수 있지만, private 로 지정하고, 멤버 함수를 통해 멤버 변수를 다룬다면, 멤버 변수에 음수를 대입하려 할 때 멤버 함수가 이를 감지하고, 막을 수 있을 것이다.
인라인 함수
보통 함수를 호출하면 여러 과정을 거쳐 함수가 실행된다. 즉 따로 함수를 정의하여 사용하는 것보다 코드를 그대로 사용하는 것이 함수 호출에 따른 시간이 걸리지 않는다는 점에서 더 효과적이다. 그러나 가독성, 반복되는 사용 등으로 함수를 따로 정의하여 사용해야 될 때가 있는데, 이때 함수의 크기가 작아 함수 호출에 따른 시간 오버헤드가 커질 것으로 예상된다면 inline 키워드를 사용하여 이를 해결할 수 있다. #define 을 통해 사용되는 매크로와 유사한데, inline 으로 정의된 함수의 코드를 해당 함수가 사용된 곳에 컴파일러가 확장 삽입한다. 이를 통해 함수 호출이 생략되면서 함수 호출 때문에 생기는 시간 오버헤드를 없엘 수 있다.
단, inline 키워드를 사용하였다고 무조건 컴파일러가 적용시켜주는 것이 아니며, 컴파일러가 판단하여 적용한다. 또한 당연하게도 코드 전체 길이가 증가한다. 더욱이 컴파일러가 발전하면서 컴파일 과정에서 inline 이 없더라도 인라이닝하는게 이득이라 판단하면 컴파일러가 임의로 인라이닝 처리하여 최적화하기도 한다.
클래스 선언부에 구현된 멤버 함수는 모두 인라인 처리되며, 외부에서 멤버 함수를 구현하더라도 inline 을 사용하면 역시 선언부에 구현된 멤버 함수처럼 인라인 처리된다.
'Language > C & C++' 카테고리의 다른 글
[C++] 값에 의한 호출(call by value)과 주소에 의한 호출(call by address) 그리고 참조에 의한 호출(call by reference) (0) | 2024.10.13 |
---|---|
[C++] 동적 메모리 할당 및 반환 (0) | 2024.10.13 |
[C] 입출력 형식과 서식 지정자 (0) | 2024.08.30 |
[C] 연산자 우선순위 (0) | 2024.08.30 |
[C] 기본 자료형 크기와 범위 (0) | 2024.08.29 |