C++에서는 새로운 자료형(타입)을 만들 때 클래스(class)를 사용한다.
타입으로 클래스를 만들고, 이를 기반으로 인스턴스인 객체를 만든다. 클래스를 기반으로 인스턴스를 만드는 행위를 인스턴스화(instantiation)이라고 부른다.
객체 지향 프로그래밍에서는
속성과 행위를 선언하는 클래스 정의,
행위를 정의하는 멤버 함수 정의,
객체를 인스턴스화하고 사용하는 애플리케이션 구현이 필요하다.
클래스 작성하기
클래스 정의
#include <iostream>
using namespace std;
//클래스 정의
class Circle
{
private:
double radius;
public:
double getArea() const;
void setRadius(double value);
};
//멤버 함수 정의
double Circle::getArea() const
{
const double PI = 3.14;
return(PI * radius * radius);
}
void Circle::setRadius(double value) {
radius = value;
}
//애플리케이션
int main()
{
cout << "Circle1" << endl;
Circle circle1;
circle1.setRadius(10);
cout << "Area: " << circle1.getArea() << endl << endl;
cout << "Circle2" << endl;
Circle circle2;
circle2.setRadius(5);
cout << "Area: " << circle2.getArea() << endl;
return 0;
}
클래스를 이용해 원의 넓이를 출력하는 프로그램이다.
//클래스 정의
class Circle //헤더
{
private: //접근 제한자
double radius; //데이터 멤버 선언
public: //접근 제한자
double getArea() const; //멤버 함수 선언
void setRadius(double value); //멤버함수 선언
}; //세미콜론으로 정의를 마침
클래스를 정의하려면 클래스 이름 앞에 class라는 키워드를 붙이면 된다. 보통 클래스 이름은 대문자로 시작한다. 소문자로 시작하는 라이브러리 클래스와 구분하기 위함이다.
클래스 내부에서는 데이터 멤버와 멤버 함수를 선언한다.
접근 제한자(access modifier)는 클래스에 접근할 수 있는 권한을 나타낼 때 사용한다.
private, protected, public이 있는데 기본적으로 private이 붙는다.
같은 클래스에서 접근 | 서브 클래스에서 접근 | 모든 곳에서 접근 | |
private | O | X | X |
protected | O | O | X |
public | O | O | O |
일반적으로 데이터 멤버에는 private을 붙인다. 기본으로 붙어도 강조의 의미로 private을 붙여서 사용한다.
private이 적용된 멤버는 멤버 함수를 통해서만 접근할 수 있다.
멤버 함수는 애플리케이션에서 접근할 수 있어야 하므로 public을 붙인다. 클래스 내부에서만 활용하는 경우에는 private을 붙이기도 한다.
접근 제한자는 멤버와 함수마다 따로 지정하지 않고, 콜론을 사용해 그룹 단위로 붙인다.
멤버 함수를 선언하는 부분은 함수를 선언하는 부분과 마찬가지로 단순히 함수의 프로토타입을 적은 것이다.
함수 내부에서 값을 변경하지 못하도록 하려면 끝에 const 한정자를 붙인다.
getArea()
함수는 원의 넓이를 반환할 뿐이므로 값을 변경하지 못하게 const를 붙였다.
멤버 함수 정의
//멤버 함수 정의
double Circle::getArea() const
{
const double PI = 3.14;
return(PI * radius * radius);
}
void Circle::setRadius(double value) {
radius = value;
}
클래스 정의에서 선언한 함수를 구현하는 부분이다.
일반적인 함수 정의와 유사하지만 const 한정자가 있다는 점, 앞에 클래스 이름이 붙는다는 점이 다르다.
접근자 멤버 함수(accessor member function)는 호스트 객체(인스턴스 멤버 함수를 호출하는 객체)의 정보를 추출할 때 사용하는 함수이다. 겟터(getter)라고 부르며 객체의 상태를 변경하지는 않는 읽기 전용 함수이다. 따라서 헤더 끝에 const 한정자를 추가한다.
설정자 멤버 함수(mutator member function)는 호스트 객체의 상태를 변경하는 함수이다. 셋터(setter)라고 부르며 상태를 변경하므로 const를 붙이면 안된다.
멤버 함수 정의 부분은 클래스 블록 밖에서 작성하기 때문에 클래스 멤버라는 점을 알려주기 위해 스코프 기호(::)를 붙인다.
인스턴스 멤버 함수
멤버 함수는 객체의 데이터 멤버를 조작하기 위해 사용한다.
데이터 멤버는 선언한 객체가 개별적으로 갖지만, 멤버 함수는 메모리 위에 하나만 올라가며 모든 인스턴스가 공유한다.
아래 사진에서 데이터 멤버 radius
는 각 객체마다 할당되어 있고, 멤버 함수 getArea()
는 다른 영역에서 객체를 가리키고 있는 모습을 확인할 수 있다.
애플리케이션
int main()
{
cout << "Circle1" << endl;
Circle circle1; //객체1 인스턴스화
circle1.setRadius(10);
cout << "Area: " << circle1.getArea() << endl << endl;
cout << "Circle2" << endl; //객체2 인스턴스화
Circle circle2;
circle2.setRadius(5);
cout << "Area: " << circle2.getArea() << endl;
return 0;
}
애플리케이션에서는 작성한 클래스를 인스턴스화하여 사용한다.
인스턴스화 이후에 멤버 함수를 사용할 수 있다.
멤버 함수를 호출하려면 생성한 객체 이름에 점(.)을 찍고 호출한다.
멤버 함수 선택자
C++에서는 멤버 선택 연산자(emeber selector operator)로 점(.)과 화살표(->) 두 개가 있다.
멤버 함수가 하나씩만 메모리에 있다면, 어떻게 여러 객체가 멤버 함수를 공유할까? 어떻게 한 객체가 멤버 함수를 쓰고 있을 때 다른 객체가 쓰지 못하게 막는 것일까?
백그라운드에서는 함수를 잠그는 락킹(locking)과 잠금을 해제하는 언락킹(unlocking)이 일어난다.
이를 위해 멤버 함수에 포인터를 지정한다.
점(.) 연산자를 사용하면 컴파일러는 이를 포인터 멤버 선택 연산자로 바꾼다.
모든 멤버 함수에는 현재 사용하고 있는 객체를 나타내는 포인터인 this 포인터가 숨겨져 있다.
멤버 함수에는 하나의 this 포인터만 있으니 한 번에 한 객체만 맴버 함수를 호출할 수 있다.
멤버 함수 부분에서 호스트 객체 이야기를 했는데, this 포인터가 가리키는 객체가 호스트 객체이다. 따라서 어떤 멤버 함수가 실행되는 동안 호스트 객체는 하나이다.
컴파일러는 멤버 함수의 매개변수에 this 키워드를 자동으로 추가해 객체의 주소를 할당한다.
//사용자가 작성한 코드
double Circle::getArea() const
{
const double PI = 3.14;
return(PI * radius * radius);
}
//컴파일러가 바꾼 코드
double Circle::getArea(Circle *this) const
{
const double PI = 3.14;
return(PI * this->radius * this->radius);
}
멤버 함수를 호출하면 다음과 같이 변경한다.
//작성한 코드
circle1.getArea();
//컴파일러가 바꾼 코드
this = &circle1;
getArea(this);
this 포인터로 명시적으로 멤버를 가리킬 수도 있다.
이를 통해 데이터 멤버와 매개변수를 같은 이름으로 사용할 수 있다.
void Circle::setRadius(double radius)
{
this->radius = value;
}
생성자의 초기화 리스트 실행 중에는 객체가 생성되기 전이므로 this 포인터를 사용할 수 없다.
클래스 불변 속성
클래스 불변 속성(class invariant)는 데이터 멤버의 일부 또는 전체에 적용해야 하는 조건이다. 설계적으로 지켜야 하는 조건이라 보면 된다.
반지름은 음수일 수 없다. 하지만 double로 정의하는 것만으로는 음수가 들어가는 문제를 해결할 수 없다.
이를 해결하기 위해 생성자 또는 세터 함수에서 불변 속성을 적용한다. 만약 radius가 음수라면 프로그램을 종료하는 것이다.
void Circle::setRadius(double value)
{
if (value < 0.0) {
cout << "양수가 아닙니다. 프로그램을 종료합니다." << endl;
assert(false);
}
radius = value;
}
assert 함수는 매개변수가 false일 때 프로그램을 중단하는 함수이다. <cassert> 헤더 파일에 정의되어 있다.
정적 멤버
클래스 자료형에는 인스턴스 멤버와 정적 멤버가 있다. 인스턴스 멤버는 지금까지 살펴본 내용이다. 정적 멤버는 클래스 전체에 포함되는 멤버이다.
정적 데이터 멤버는 static 키워드를 붙여서 선언한다. 인스턴스에 속하지 않아 생성자에서 초기화할 수 없다. 따라서 전역 스코프에서 초기화한다.
정적 데이터 멤버에 접근하려면 보통 정적 멤버 함수를 사용한다. 정적 멤버 함수는 인스턴스와 연결되지 않아 호스트 객체가 없으므로 const를 사용할 수 없다.
class Circle
{
private:
...
static int count;
public:
...
static int getCount();
};
int Circle::count = 0;
int Circle::getCount()
{
return count;
}
정적 멤버 함수 호출
정적 멤버 함수는 인스턴스명과 클래스명으로 호출할 수 있다.
Circle1.getCount(); //인스턴스명
Circle::getCount(); //클래스명
정적 멤버 함수로는 인스턴스 데이터 멤버에 접근할 수 없지만, 인스턴스 멤버 함수에서는 정적 데이터 멤버에 접근할 수 있다. 그래도 타입을 분리해 사용하는 편이 좋다.
'프로그래밍 > C, C++' 카테고리의 다른 글
[C++]vector를 2차원으로 선언하는 방법 (0) | 2022.12.27 |
---|---|
[C++] 생성자와 소멸자 (0) | 2022.12.13 |
while 반복문을 종료하는 방법: 센티넬, EOF, 플래그 (0) | 2022.12.10 |
[C++] 조정자를 사용해 입출력 형식을 지정하는 방법 (0) | 2022.12.07 |
[C++] 변수를 초기화하는 세 가지 방법 (0) | 2022.11.18 |