[C++] 객체 포인터와 객체 배열

HY K·2024년 8월 29일

명품 C++ 프로그래밍

목록 보기
20/24

이번부터는 C++의 객체 포인터, 객체 배열, 그리고 동적 할당에 대해서 알아보자.


1. 객체 포인터

C++에서는 객체를 다루기 위해서 객체에 대한 포인터 변수를 선언하고, 이 포인터 변수를 통해서 객체 멤버 변수를 읽고, 값을 쓰거나, 멤버 함수를 호출할 수 있다. 간단한 예시를 통해서 알아보자.

Circle donut;
double d = donut.getArea();

Circle *p; // 객체 포인터를 생성
p = &donut; // 포인터에 객체 주소를 저장하기
d = p->getArea(); // 객체 포인터를 통해서 멤버 함수를 호출하기

객체에 대한 포인터 변수 선언은 일반적인 포인터처럼 선언하면 된다.

Circle *p;

그리고 포인터 변수에 객체 주소를 할당하기 위해서는 & 연산자를 사용해야 한다.

p = &donut;

혹은 포인터 변수를 선언할 때 바로 초기화 할 수 있다.

Circle *p = &donut;

이렇게 선언한 포인터를 통해서 멤버 변수와 멤버 함수에 접근할 수 있다.
보통 객체 이름을 통해서 멤버 변수나 함수에 접근하고 싶다면 . 연산자를 사용하지만, 포인터를 통해서 접근하기 위해서는 -> 연산자를 사용한다.

d = donut.getArea(); // 일반적인 객체 접근
d = p->getArea(); // 포인터 변수를 통한 접근
d = (*p).getArea(); // 이렇게 코딩해도 된다.

참고로 매우 당연한 소리지만, 아무것도 가리키지 않는 포인터로 멤버 함수나 변수에 접근하려고 하면 null pointer 오류가 발생한다.

다음은 객체 포인터를 사용하는 예시 코드이다.

#include <iostream>
using namespace std;

class Circle{
	 int radius;
public:
	Circle(){radius = 1;}
    Circle(int r) {radius = r;}
    double getArea();
};

double Circle::getArea(){
	return 3.13*radius*radius;
}

int main(){
	Circle donut;
    Circle pizza(300);
    
    cout<<donut.getArea()<<endl; // 객체 이름을 통한 접근
    
    Circle *p;
    // 객체 포인터 접근
    p = &donut;
    cout << p->getArea() <<endl;
    cout << (*p).getArea() <<endl;
    // 객체 포인터를 통해서 멤버 함수를 접근함
    
    p = &pizza;
    cout << p->getArea() <<endl;
    cout << (*p).getArea() << endl;
    // 객체 포인터를 통해서 멤버 함수 접근
}

2. 객체 배열

C++에서는 객체 배열을 사용할 수 있다. 객체 배열(object array)은 원소(element)가 객체라는 점을 빼면, 기본 자료형을 사용해서 선언하고 사용하는 배열과 차이점이 없다.

예시 코드를 살펴보자.

#include<iostream>
using namespace std;

class Circle {
	int radius;
public:
	Circle() { radius = 1; }
	Circle(int r) { radius = r; }
	void setRadius(int r) { radius = r; }
	double getArea();
};

double Circle::getArea() {
	return 3.14 * radius * radius;
}

int main() {
	Circle circleArray[3];

	circleArray[0].setRadius(10); // 배열의 index를 활용해서 접근이 가능
	circleArray[1].setRadius(20);
	circleArray[2].setRadius(30);

	for (int i = 0; i < 3; i++) {
		cout << "Circle " << i << "의 면적은 " << circleArray[i].getArea() << endl;
	}

	Circle* p;
	p = circleArray; // 배열의 이름은 배열의 첫번째 주소를 가리키는 포인터
	for (int i = 0; i < 3; i++) {
		cout << "Circle " << i << "의 면적은 " << p->getArea() << endl; // 포인터를 통해서 접근
		p++; // 배열 원소 증가
	}

}

객체 배열을 선언할 때 주의할 점

객체 배열 선언문은 오직 매개 변수 없는 기본 생성자(default constructor)를 사용한다. 따라서 매개변수 있는 생성자가 존재하지만, 기본 생성자가 없는 클래스를 작성한 후 객체 배열을 사용하면 컴파일 오류가 발생하게 된다.

또한 매개변수 있는 생성자를 사용하기 위해서

class_name array_name[index](param);

이런 식으로 선언하는 것 역시 컴파일 오류를 불러일으킨다.

그리고 객체배열을 통해서는 "." 연산자를 통해서 멤버 변수와 함수에 접근함도 유의해야 한다. 포인터와는 다르게 접근한다.

포인터를 통한 객체 배열 접근

일반적인 배열과 마찬가지로 객체 배열 또한 포인터를 이용해서 다룰 수 있다. 그리고 포인터가 배열의 처음 원소의 주소를 가리킬 경우, 포인터 역시 인덱스를 통해 마치 배열 변수처럼 다룰 수 있다는 점 역시 동일하다. 가령 예를 들어보면 다음과 같다.

p[0].getArea();
(*p).getArea();
p->getArea();

이 3가지 활용법은 모두 동일한 의미를 가지고 있다.
(마지막 p가 배열의 첫번째 원소를 가리킬 경우에만)

배얼 소멸과 소멸자

객체 배열이 소멸할 경우, 각 객체 원소마다 소멸자가 호출되게 된다. 이 경우에도 당연히 생성된 순서의 역순으로 소멸자가 호출되어 소멸한다.

객체 배열은 기본 생성자가 아닌 생성자를 사용 못하나?

결론부터 얘기하면 그렇지 않다. 아까 위에서는 생성자를 사용하지 못한다고 했지만, 사실 그것은 선언할 때 얘기이다.

만약 선언과 동시에 객체 원소들을 초기화 한다면, 그 때는 기본 생성자가 아닌 생성자도 얼마든지 사용할 수 있다.

예를 들어보자.

Circle circleArray[3] = {Circle(10), Circle(20), Circle()};

이 경우에는 마지막 원소에만 기본 생성자가 사용되며, 나머지 원소들의 경우 다른 생성자를 통해서 생성된다.

객체 배열의 초기화 예시 코드를 살펴보자.

#include<iostream>
using namespace std;

class Circle {
	int radius;
public:
	Circle() { radius = 1; }
	Circle(int r) { radius = r; }
	void setRadius(int r) { radius = r; }
	double getArea();
};

double Circle::getArea() {
	return 3.14 * radius * radius;
}

int main() {
	Circle circleArray[3] = { Circle(10), Circle(20), Circle() };
	for (int i = 0; i < 3; i++)
		cout << "Circle " << i << "의 면적은 " << circleArray[i].getArea() << endl;

}

다차원 객체 배열

다른 프로그래밍 언어와 마찬가지로 C++ 역시 1차원을 넘어선 배열을 만들 수 있다. 객체 배열 역시 마찬가지로 다차원으로 선언 및 생성이 가능하다.

Circle circles[2][3];

단순히 선언만 하면 1차원 객체 배열과 마찬가지로 기본 생성자를 통해서 초기화가 이루어지며, 구현부에서 생성자를 지정할 경우 기본 생성자를 사용하지 않고도 초기화가 가능하다는 것까지 완벽하게 동일하다.

예시 코드를 살펴보자.

#include<iostream>
using namespace std;

class Circle {
	int radius;
public:
	Circle() { radius = 1; }
	Circle(int r) { radius = r; }
	void setRadius(int r) { radius = r; }
	double getArea();
};

double Circle::getArea() {
	return 3.14 * radius * radius;
}

int main() {
	Circle circles[2][3];

	circles[0][0].setRadius(1);
	circles[0][1].setRadius(2);
	circles[0][2].setRadius(3);
	circles[1][0].setRadius(4);
	circles[1][1].setRadius(5);
	circles[1][2].setRadius(6);

	for (int i = 0; i < 2; i++) {
		for (int j = 0; j < 3; j++) {
			cout << "Circle [" << i << ", " << j << "]의 면적은 ";
			cout << circles[i][j].getArea() << endl;
		}
	}
}
profile
로봇, 드론, SLAM, 제어 공학 초보

0개의 댓글