프로그래머가 새로 만든 클래스는 또 하나의 형(type)이다. 해당 클래스끼리의 연산이 가능해진다면, 직관적인 코드 작성과 가독성이 향상될 것이다.
class Point
{
private:
int x;
int y;
public:
Point(int a=0, int b=0) {x=a; y=b;}
void setX(int a) {x=a;}
void setY(int b) {y=b;}
void show() {cout << "x : " << x << ", y : " << y << '\n';}
};
이렇게 x,y 좌표 위의 점을 나타내는 Point 클래스를 만들었을 때,
int main()
{
Point p1(1, 3);
Point p2(5, 2);
// Point p3 = p1 + p2;
}
와 같은 '+' 연산이 가능하다면 더욱 편리할 것이다.
객체를 다루는 연산자를 새로 정의하는 방법이 연산자 오버로드 이다.
class Point
{
...
public:
...
Point operator+(Point p);
}
...
Point Point::operator+(Point p)
{
Point tmp;
tmp.x = x + p.x;
tmp.y = y + p.y;
return tmp;
}
Point 클래스와 정수값을 + 연산자로 더해야 하는 상황이라면, 이전에 오버로딩한 + 연산자를 호출할 수 없다. 그 이유는 + 연산자 왼쪽의 피연산자는 반드시 Point 클래스 객체여야 하기 때문이다.
연산자의 왼쪽 피연산자가 그 클래스의 객체가 아닌 경우에는 연산자를 다른 방식으로 오버로딩해야 한다.
이때, 리턴 값의 형 앞에 friend 를 붙이고 프렌드 함수 라고 한다.
class Point
{
private:
int x;
int y;
public:
...
friend Point operator+(int a, Point p);
};
Point operator+(int a,Point p)
{
Point tmp;
tmp.x = a + p.x;
tmp.y = a + p.y;
return tmp;
}
프렌드 함수는 해당 클래스의 멤버 함수가 아닌, 특별히 해당 클래스에 접근할 수 있는 함수가 된다고 한다.
따라서 함수 본체를 정의할 때, Point:: 를 붙이지 않는다.
// 1. 멤버 함수로 만들 경우
class Point
{
...
Point operator+(Point p);
};
Point Point::operator+(Point p)
{
Point tmp;
tmp.x = x + p.x;
tmp.y = y + p.y;
return tmp;
}
// 2. 프렌드 함수로 만들 경우
class Point
{
...
friend Point operator+(int a, Point p);
}
Point operator+(Point p1, Point p2)
{
Point tmp;
tmp.x = p1.x + p2.x;
tmp.y = p1.y + p2.y;
return tmp;
}
멤버 함수와 프렌드 함수는 받는 인수의 개수가 다르다.
예외적으로 대입 연산자(=) 는 오버로드하지 않아도 사용할 수 있다.
대입 연산자에는 객체의 멤버 값을 다른 개체의 멤버로 복사하는 기능이 있기 때문이다.
기본적으로 제공되는 대입 연산자의 기능을 바꿔 새로 정의한다면, 대입 연산자가 오버로드 되어 새로 정의한 함수가 실행된다.
// 1. 전위 증가 연산자 오버로드
Point Point::operator++()
{
x++;
y++;
return *this; // 값을 더한 뒤, 객체 스스로를 리턴
}
// 2. 후위 증가 연산자 오버로드
Point Point::opertator++(int d) // 전위 증가 연산자와 구분하기 위해 인수 받음
{
Point p = *this; // 증가시키기 이전의 객체를 저장
x++;
y++;
return p; // 증가시키기 이전의 객체를 리턴
}
클래스 또한 새로운 형으로 인식되므로 형 변환이 가능하다.
해당 클래스의 객체를 다른 형의 객체로 변환시키는 역할을 담당한다.
class Number
{
private:
int num;
public:
Number() {num = 0;}
Number(int n) {num = n;} // 변환 생성자
operator int() {return num;} // 변환 함수 (Number형 -> int형)
Number operator++();
Number operator++(int d);
Number operator--();
Number operator--(int d);
};
...
int main()
{
Number n;
int i = (int) n; // Number형 객체 n 을 int형으로 형변환
...
}
해당 클래스의 객체를 다른 형 객체로 변환시키는 것은 변환 함수의 기능이다.
반대로 인수를 하나 받는 생성자를 정의하면 다른 클래스의 객체를 해당 클래스의 객체로 변환시킬 수 있다.
int main()
{
int i = 10;
Number n(i);
// Number n = i;
...
}
형 변환 함수는 해당 클래스의 객체를 다른 객체로 변환시키고,
변환 생성자는 다른 클래스의 객체를 해당 클래스의 객체로 변환시킨다.
클래스 안에서 동적으로 메모리를 확보한 뒤, 반드시 클래스 어딘가에서 delete 연산자를 사용하여 메모리를 해제해야 한다.
소멸자는 객체가 소멸될 때 자동으로 호출된다. 또한, 소멸자 안에서 확보한 메모리를 해제할 수 있다.
#include <iostream>
#include <string>
using namespace std;
class Car
{
private:
int num;
double gas;
char* pName;
public:
Car(char* pN, int n, double g);
~Car();
void show();
};
Car::Car(char* pN, int n, double g)
{
cout << pN << " 를 생성합니다. \n";
pName = new char[strlen(pN) + 1]; // 생성자 함수 안에서 동적으로 메모리 확보
strcpy(pName, pN);
num = n;
gas = g;
}
Car::~Car() // 소멸자 정의
{
cout << pName << "를 소멸시킵니다.\n";
delete[] pName; // 소멸자 함수 안에서 메모리를 해제
}
...
int main()
{
Car car1("mycar", 1234, 25.5);
car1.show();
return 0;
}
출력 결과
mycar를 생성했습니다.
차량 번호는 1234 입니다.
연료량은 25.5 입니다.
이름은 mycar 입니다.
mycar를 소멸시킵니다.
파생 클래스의 객체가 소멸될 때, 기본 클래스의 소멸자만 호출되는 상황이 발생한다. 파생 클래스에서 동적으로 메모리를 확보한다면 파생 클래스의 소멸자를 통해 메모리를 해제해야 하는데, 기본 클래스의 소멸자만 호출된다.
파생 클래스의 소멸자가 호출되게 하려면, 기본 클래스의 소멸자에 virtual 을 붙여 가상함수로 정의하자.
class Car
{
protected:
...
public:
...
virtual ~Car(); // 가상 함수로 정의
};
class RacingCar : public Car
{
private:
...
public:
...
~RacingCar();
};
int main
{
Car* pCars[2];
Car car1;
RacingCar rccar1;
pCars[0] = &car1;
pCars[1] = &rccar1;
pCars[0]->show();
pCars[1]->show();
return 0;
}
다른 객체를 대입해서 초기화 시킬 때 단순히 복사하면 각 개체가 똑같은 주소를 가리키게 되어 메모리를 공유하게 된다.
Car car1 = mycar; // 객체 초기화
Car car2;
car2 = mycar; // 객체에 다른 객체를 대입
이 때, mycar 의 멤버 값이 아닌 주소값이 복사되기 때문에 같은 메모리를 공유하게 된다.
다른 객체를 사용하여 해당 클래스의 객체를 초기화하거나 대입할 때, 멤버를 복사하는 것이 아닌 멤버의 값을 저장할 수 있는 메모리를 확보하는 작업이 필요하다.
클래스명 : 클래스명 (const 레버런스형 인수) { ... }
대입 연산자는 반드시 멤버 함수로 오버로드해야 한다.
이미 존재하는 객체를 고려해야 하기 때문에, 복사되는 객체가 자기 자신인지의 여부를 this 포인터를 통해 조사하고, 해당 경우를 제거한다.
이후, 이미 확보했었던 메모리를 삭제하고, 새롭게 메모리를 확보한다.
#include <iostream>
#include <cstring>
using namespace std;
class Car
{
private:
int num;
double gas;
char* pName;
public:
Car(char* pN, int n, double g);
~Car();
Car(const Car& c);
Car& operator=(const Car& c);
};
Car::Car(char* pN, int n, double g)
{
pName = new char[strlen(pN)+1];
strcpy(pName,pN);
num = n;
gas = g;
cout << pName << "를 생성했습니다.\n";
}
Car::~Car()
{
cout << pName << "를 소멸시킵니다.\n";
delete[] pName;
}
Car::Car(const Car& c)
{
cout << c.pName << "로 초기화합니다.\n";
// 복사되는 객체를 위해 메모리를 확보
pName = new char[strlen(c.pName) + strlen("의 복사본 1") + 1];
strcpy(pName,c.pName);
strcat(pName,"의 복사본 1");
num = c.num;
gas = c.gas;
}
Car& Car::operator=(const Car& c)
{
cout << pName << "에 " << c.pName << "를 대입합니다.\n";
if(this != &c) // 복사되는 객체 스스로가 대입하는 것을 방지
{
delete[] pName; // 기존에 확보한 메모리를 해제
// 복사되는 객체를 위해 메모리를 확보
pName = new char[strlen(c.pName) + strlen("의 복사본 2") + 1];
strcpy(pName,c.pName);
strcat(pName,"의 복사본 2");
num = c.num;
gas = c.gas;
}
return *this;
}
int main()
{
Car mycar("mycar", 1234, 20.5");
Car car1 = mycar; // 초기화
Car car2("car2", 0, 0);
car2 = mycar; // 대입
return 0;
}
출력 결과
mycar를 생성했습니다.
mycar로 초기화합니다. (복사 생성자가 출력)
car2를 생성했습니다.
car2에 mycar를 대입합니다. (대입 연산자가 출력)
mycar의 복사본 2를 소멸시킵니다.
mycar의 복사본 1를 소멸시킵니다.
mycar를 소멸시킵니다.
Car mycar;
func1(mycar);
...
void func1(Car c) // 실인수로 가인수가 초기화
{
...
}
Car mycar;
mycar = func2(); // 리턴 값으로 대입
...
Car func2()
{
Car tmp;
...
return tmp;
}
다양한 형을 다루는 함수 템플릿처럼 클래스의 '틀'을 바탕으로 다양한 형을 다루는 클래스를 찍어내듯 생성하는 기능을 클래스 템플릿 이라 부른다.
// 클래스 템플릿
template <class T>
class Array
{
private:
T data[5];
public:
void setData(int num, T d);
T getData(int num);
};
// 클래스 템플리 멤버 함수 정의
template <class T>
void Array<T>::setData(int num, T d)
{
if(num < 0 || num > 4)
cout << "배열 길이를 넘어섰습니다.\n";
else
data[num] = d;
}
template <class T>
T Array<T>::getData(int num)
{
if(num < 0 || num >4 ) {
cout << "배열 길이를 넘어섰습니다.\n";
return data[0];
}
else
return data[num];
}
int main()
{
cout << "int형 배열을 생성합니다.\n";
Array<int> i_array; // int형을 다루는 클래스와 그 객체 선언
i_array.setData(0,80);
i_array.setData(1,60);
i_array.setData(2,58);
i_array.setData(3,77);
i_array.setData(4,57);
for(int i=0;i<5;++i)
cout << i_array.getData(i) << '\n';
cout << "double형 배열을 생성합니다.\n";
Array<double> d_array; // double형을 다루는 클래스와 그 객체 선언
d_array.setData(0,30.5);
d_array.setData(1,40.6);
d_array.setData(2,50.4);
d_array.setData(3,60.7);
d_array.setData(4,80.9);
for(int j=0;j<5;++j)
cout << d_array.getData(i) << '\n';
return 0;
}
C++ 표준 라이브러리에는 다수의 클래스 템플릿과 함수 템플릿이 마련되어 있다. 이들을 따로 떼어서, 표준 템플릿 라이브러리 (Standard Template Library : STL) 라고 부른다.
많은 PS 문제를 접하면서 잘 활용한 vector, queue, unordered_map 등이 떠올랐다.
프로그램 실행 중 일어나는 여러 가지 에러를 처리하는 기능이다.
컴퓨터학부의 필수 전공이었던 JAVA 프로그래밍에서 상당히 애를 먹었던 부분이다.
#include <iostream>
using namespace std;
int main()
{
int num;
cout << "1~9까지의 숫자를 입력하십시오.\n";
cin >> num;
try {
if(num <=0)
throw "0 이하의 수가 입력되었습니다.";
if(num >= 10)
throw "10 이상의 수가 입력되었습니다.";
cout << num << "입니다.\n";
}
catch(char* err) {
cout << "에러 : " << err << '\n';
return 1;
}
return 0;
}
클래스의 고급 기능을 정리해보았다. 처음 보는 내용이 많아 자주 사용해보며 익숙해져야겠다.
또한 삼성전자 S/W 알고리즘 특강을 들으며 코치님의 풀이 중 깊은 복사와 얕은 복사에 대한 설명이 짧게나마 나온 적이 있다.
복사 생성자와 관련이 있는 것 같아 복사 생성자에 대한 개념을 완전히 익힌 뒤, 그 부분도 공부해봐야겠다.