이번 포스팅에서 우리가 배워볼 것은 다음과 같다.
- 함수로 객체 전달하기(feat. 복사 생성자)
- 함수가 객체를 반환하기
- 객체끼리의 복사 방법
- 객체와 연산자 '='
- 객체 사이의 비교 연산자 사용 가능할까?
- 클래스에서의 정적 변수
- 클래스에서의 정적 멤버 함수
1. 함수로 객체 전달하기(feat. 복사 생성자)
- C++에서 함수로 인수를 전달하는 방법은 2가지가 있다.
- 값에 의한 호출(call by value)
- 참조에 의한 호출(call by reference)
- 함수로 객체를 전달하면 일반적인 생성자가 호출되는 것이 아닌 복사 생성자(copy constructor) 라는 특수한 생성자가 호출된다.
- 복사 생성자는 따로 만들지 않아도 컴파일러가 기본적인 버전은 만들어서 사용한다. 아래 예제를 보자.
예제1 : 값에 의한 호출(call by value)
#include <iostream>
using namespace std;
class Pizza {
int radius;
public:
Pizza(int r = 0) : radius{ r } {}
~Pizza() {}
void setRadius(int r) {
radius = r;
}
void print() { cout << "Pizza(" << radius << ")" << endl; }
};
void upgrade(Pizza p) { p.setRadius(20); }
int main() {
Pizza obj(10);
upgrade(obj);
obj.print();
return 0;
}
upgrade 함수의 매개변수가 객체를 값으로 전달(call by value)하고 있다. 객체가 복사되어 전달되기 때문에 실제로 전달되는 것은 원래 객체의 복사본이다. 따라서 이 함수 내에서 Pizza p가 새로운 복사본을 생성하고 setRadius를 호출하여 이 복사본의 반지름을 변경한다. 그러나 이 변경 사항은 main()에서 생성한 객체 obj에는 적용되지 않는다. 이를 통해 함수 내부에서 변경된 내용은 호출된 객체 자체에 영향을 미치지 않는다.
예제2 : 객체의 주소를 함수로 전달하기(call by reference)
다음 예제는 객체의 주소를 함수로 전달하는 코드이다.
#include <iostream>
using namespace std;
class Pizza {
int radius;
public:
Pizza(int r = 0) : radius{ r } {}
~Pizza() {}
void setRadius(int r) {
radius = r;
}
void print() { cout << "Pizza(" << radius << ")" << endl; }
};
void upgrade(Pizza *p) { p->setRadius(20); } // 참조에 의한 호출
int main() {
Pizza obj(10);
upgrade(&obj);
obj.print();
return 0;
}
- 객체의 주소를 &연산자로 전달하면 객체가 아니기 때문에 생성자나 소멸자가 호출되지 않는다.
- 이 연산자를 통해 함수 안에서 주소를 이용하여 원본 객체의 내용을 조작할 수 있다.
예제3 : 참조자 매개변수 사용하기(call by reference)
#include <iostream>
using namespace std;
class Pizza {
int radius;
public:
Pizza(int r = 0) : radius{ r } {}
~Pizza() {}
void setRadius(int r) {
radius = r;
}
void print() { cout << "Pizza(" << radius << ")" << endl; }
};
void upgrade(Pizza &p) { p.setRadius(20); }
int main() {
Pizza obj(10);
upgrade(obj);
obj.print();
return 0;
}
2. 함수가 객체를 반환하기
함수가 객체를 반환할 때는 함수 내의 return문에서 복사생성자가 호출한다. 그 복사생성자 호출에 의해 나온 '반환 객체'가 대입연산자 '='을 호출하여 반환될 변수에 대입된다.
예제1 : 함수가 객체를 반환하기 - 피자 반환하기
#include <iostream>
using namespace std;
class Pizza {
int radius;
public:
Pizza(int r = 0) : radius{ r } {}
~Pizza() {}
void setRadius(int r) {
radius = r;
}
void print() { cout << "Pizza(" << radius << ")" << endl; }
};
Pizza createPizza() {
Pizza p(10);
return p;
}
void upgrade(Pizza &p) { p.setRadius(20); }
int main() {
Pizza obj;
obj = createPizza();
obj.print();
return 0;
}
예제2 : 객체를 함수로 전달하기
- 복소수의 클래스로 정의하고 복소수 덧셈 연산을 구현해 보자.
- 복소수의 클래스는 다음과 같다.
-
class Complex { public: double real, imag; Complex(double r = 0.0, double i = 0.0) : real{ r }, imag{ i } { cout << "생성자 호출"; print(); } ~Complex() { cout << "소멸자 호출"; print(); } void print() { cout << real << "+" << imag << "i" << endl; } };
코드 정답 :
#include <iostream>
using namespace std;
class Complex {
public:
double real, imag;
Complex(double r = 0.0, double i = 0.0) : real{ r }, imag{ i } {
cout << "생성자 호출 " << this << " ";
print();
}
~Complex() {
cout << "소멸자 호출 " << this << " ";
print();
}
void print() {
cout << real << "+" << imag << "i" << endl;
}
};
Complex add(Complex c1, Complex c2) {
Complex temp;
temp.real = c1.real + c2.real;
temp.imag = c1.imag + c2.imag;
return temp;
}
int main() {
Complex c1(1, 2), c2(3, 4);
Complex sum;
sum = add(c1, c2);
sum.print();
return 0;
}
예제3 : 컬러풀한 원들 그리기(복사생성자 이용)
랜덤한 색상을 생성하고 이것을 원을 나타내는 Circle 객체로 전달하여 컬러풀한 원들이 그려지도록 하자.
#include <iostream>
#include <Windows.h>
using namespace std;
class Color {
public:
int red, green, blue;
Color() {
red = rand() % 256;
green = rand() % 256;
blue = rand() % 256;
}
};
class Circle {
int x, y, radius;
Color color;
public:
Circle(int x, int y, int r, Color c) : x(x), y(y), radius(r), color(c) {}
void draw();
};
// 원을 화면에 그리는 코드(이해하지 않아도 됨)
void Circle::draw() {
int r = radius / 2;
HDC hdc = GetWindowDC(GetForegroundWindow());
SelectObject(hdc, GetStockObject(DC_BRUSH));
SetDCBrushColor(hdc, RGB(color.red, color.green, color.blue));
Ellipse(hdc, x - r, y - r, x + r, y + r);
}
int main() {
for (int i = 0; i < 100; i++) {
Circle obj(rand() % 500, rand() % 500, rand() % 100, Color());
obj.draw();
}
return 0;
}
3. 객체끼리의 복사 방법
1. 직접 복사 생성자 방식
복사 생성자(copy constructor)는 동일한 클래스의 객체를 복사하여 객체를 생성할 때 사용하는 생성자이다.
아까 위에서 언급했다시피 굳이 사용자가 복사 생성자를 따로 만들지 않아도 컴파일러가 기본적인 버전은 만들어서 사용한다. 이번에는 사용자가 복사 생성자를 생성해서 쓰는 법을 배워보자.
// 복사 생성자의 선언(기본형)
MyClass(const MyClass& other) {
.... // other로 현재 객체를 초기화한다.
}
// =============================
// 무한루프 발생
MyClass(MyClass other);
// =============================
// 같은 종류의 객체로 초기화 하는 경우 - 타입명 생략 가능
MyClass obj(obj2);
// =============================
// 객체를 함수에 전달하는 경우
MyClass func (MyClass obj) {
....
}
// =============================
함수가 객체를 반환하는 경우
MyClass func (MyClass obj) {
MyClass tmp;
...
return tmp;
}
복사 생성자가 필요한 이유는 예제들을 통해 알아보자.
예제 1: 복사 생성자가 필요하지 않은 경우
#include <iostream>
using namespace std;
class Person {
public:
int age;
Person(int a) :age{ a } {}
};
int main() {
Person kim(21);
Person clone{ kim };
cout << "kim의 나이: " << kim.age << " clone의 나이: " << clone.age << endl;
kim.age = 23;
cout << "kim의 나이: " << kim.age << " clone의 나이: " << clone.age << endl;
return 0;
}
이 경우에는 Person clone{ kim }; 에서 위에서 얘기한 복사생성자(기본형)이 자동으로 생성되어 복사된다.
예제 2: 복사 생성자가 필요한 경우
#include <iostream>
using namespace std;
class MyArray {
public:
int size;
int* data;
MyArray(int size) {
this->size = size;
data = new int[size];
}
~MyArray() {
if (data != NULL) delete[] this->data;
}
};
int main() {
MyArray buffer(10);
buffer.data[0] = 1;
{
MyArray clone = buffer; // 기본 복사 생성자 호출한다.
}
buffer.data[0] = 2;
return 0;
}
위의 코드를 실행하면 다음과 같은 오류가 발생한다.
이유에 대해서 알아보자. 컴파일러는 디폴트로 buffer의 모든 멤버를 clone의 멤버로 복사하게 된다. 이 때 둘의 data들은 모두 동적 메모리의 하나를 가리키게 된다.
즉, MyArray 클래스는 기본 복사 생성자를 사용하고 있으며, 이는 얕은 복사(shallow copy)를 수행한다. 따라서 둘 다 객체의 data 멤버가 가리키는 메모리 주소를 복사하게 되는데, 이는 문제를 일으킬 수 있다. 여기서 발생할 수 있는 문제는 clone 객체가 buffer 객체와 같은 메모리를 가리키게 되므로, clone 객체가 블록이 종료되면서 파괴될 때 clone의 소멸자인 ~MyArray()가 호출되고 동적메로리가 해제되어 data가 가리키는 메모리 또한 파괴된다.
이는 buffer 객체에서 사용 중인 메모리를 clone이 참조하고 있을 때 발생할 수 있는 위험한 상황이다. 따라서 이 코드는 깊은 복사(deep copy)를 수행하거나 복사 생성자를 직접 정의하여 data 멤버가 가리키는 메모리를 새롭게 할당하여 이러한 문제를 방지해야 합니다.
잠깐, 여기서!) 깊은 복사와 얕은 복사란?
- 얕은 복사(shallow copy) : 지역 변수인 clone이 소멸되면서 이름을 저장한 메모리 공간을 반납하게 되고 동일한 공간을 다른 변수 buffer가 사용하려고 하는 문제
- 깊은 복사(deep copy) : 이 문제를 해결하기 위해 기본 복사 생성자를 사용하지 않고 직접 복사 생성자를 구현하는 것
직접 복사 생성자의 선언 방식은 다음과 같다.
class MyArray {
public:
// 기본 생성자
MyArray();
// 복사 생성자 (형식: ClassName(const ClassName &oldObject);)
MyArray(const MyArray &oldObject);
};
// 복사 클래스 정의
MyArray(const MyArray& other) {
// 복사 생성자 내에서 깊은 복사 수행
}
위 코드에서 MyClass는 복사 생성자를 갖는 클래스를 나타낸다. 복사 생성자는 클래스 이름 앞에 const 참조(&)로 선언되어야 합니다. 이때 const는 매개변수를 변경하지 않음을 의미하며, 참조로 전달됨으로써 복사본의 생성을 위한 효율적인 방법이다.
또한, 복사 생성자는 생성자이기 때문에 반환 형식을 명시하지 않는다. 일반적으로 복사 생성자는 객체를 복사하여 새로운 객체를 생성하는 데 사용된다.
따라서 위 코드를 깊은 복사 방식인 복사 생성자를 직접 만들어 이용한 코드로 바꾸면 다음과 같다.
#include <iostream>
using namespace std;
class MyArray {
public:
int size;
int* data;
MyArray(int size);
MyArray(const MyArray& other); // 복사생성자 정의
~MyArray();
};
MyArray::MyArray(int size) {
this->size = size;
data = new int[size];
}
// 직접 복사 생성자
MyArray::MyArray(const MyArray& other) {
this->size = other.size;
this->data = new int[other.size];
for (int i = 0; i < size; i++) {
this->data[i] = other.data[i];
}
}
MyArray::~MyArray() {
if (data != NULL) delete[] this->data;
data = nullptr;
}
int main() {
MyArray buffer(10);
buffer.data[0] = 1;
{
MyArray clone = buffer;
cout << "clone data[0] : " << clone.data[0] << endl;
}
buffer.data[0] = 2;
cout << "buffer data[0] : " << buffer.data[0] << endl;
return 0;
}
4. 객체와 연산자 '='
- 객체끼리는 대입연산자인 '='을 사용하여 복사할 수 있다.
- 같은 타입의 객체끼리는 대입 연산이 가능하다.
#include <iostream>
using namespace std;
class Person {
public:
int age;
Person(int a) : age(a) {}
};
int main() {
Person obj1(20);
Person obj2(5);
cout << "복사 전 obj2 나이 : " << obj2.age << endl;
obj2 = obj1; // obj의 멤버 변수가 obj2로 복사된다.
cout << "obj1 나이 : " << obj1.age << endl;
cout << "복사 후 obj2 나이 : " << obj2.age << endl;
return 0;
}
직접 복사 생성자와 복사 연산자 '='의 차이
두 방법 모두 객체 간의 복사를 수행하는 데 사용된다. 그러나 두 가지 방법의 동작 방식에는 차이가 있다.
직접 복사 생성자 이용하는 방법: 복사 생성자는 객체를 다른 객체로 복사할 때 호출되는 특별한 형태의 생성자이다. 이를 사용하여 복사를 수행하는 경우, 복사 생성자를 명시적으로 정의하여 복사 과정을 사용자가 직접 제어할 수 있다. 이를 통해 깊은 복사(deep copy) 등의 특정한 복사 방식을 구현할 수 있다.
= 이용하는 방법: 이 방법은 대입 연산자(=)를 사용하여 한 객체를 다른 객체에 복사하는 방법이다. 이 경우 대입 연산자 오버로딩을 통해 객체 복사 과정을 정의할 수 있습니다. 주로 이미 정의된 복사 생성자의 동작과 비슷하게 구현된다.
MyArray clone = buffer; // 복사 생성자가 호출되거나 대입 연산자 오버로딩이 수행됨
두 방법 모두 객체 간의 복사를 수행하지만, 직접 복사 생성자를 사용하는 경우 복사 생성자를 정의하고, = 연산자를 사용하는 경우 대입 연산자 오버로딩을 구현하여 객체의 복사 과정을 제어하게 된다.
잠깐? 오버로딩이란?
함수나 연산자의 기능을 확장하거나 변경하는 것을 의미한다. C++에서는 함수나 연산자에 대해 새로운 정의를 추가하거나 수정하여 사용할 수 있는데, 이를 함수 오버로딩 또는 연산자 오버로딩이라고 한다.
함수 오버로딩(Function Overloading): 동일한 이름의 함수를 여러 번 정의하는 것을 말한다. 이때 함수의 매개변수의 타입, 개수, 순서 등을 다르게 하여 여러 버전의 함수를 정의할 수 있다. 호출 시 전달되는 인자에 따라 적절한 함수가 선택되어 실행된다.
void print(int num) {
std::cout << "정수: " << num << std::endl;
}
void print(double num) {
std::cout << "실수: " << num << std::endl;
}
연산자 오버로딩(Operator Overloading): 기존의 연산자를 새로운 의미로 사용하거나 다른 종류의 피연산자들을 사용하도록 정의하는 것. 예를 들어, + 연산자를 문자열 덧셈에 사용하거나, 클래스에 사용자가 정의한 객체에 대해 연산자를 적용하는 등의 기능을 추가할 수 있다.
class Vector {
public:
int x, y;
Vector operator+(const Vector& other) {
Vector result;
result.x = this->x + other.x;
result.y = this->y + other.y;
return result;
}
};
이렇게 오버로딩을 통해 기존의 함수나 연산자의 동작을 바꾸거나 확장하여 사용할 수 있다. 이는 코드의 가독성을 높이고 작성하는 코드의 편의성을 높여준다.
5. 객체 사이의 비교 연산자 사용 가능할까?
과연 객체끼리의 비교를 비교연산자 "=="를 이용하여 비교할 수 있을까?
정답은 아니다. 아래 코드를 보자.
#include <iostream>
using namespace std;
class Person {
public:
int age;
Person(int a) : age(a) {}
};
int main() {
Person obj1(20);
Person obj2(5);
if (obj1 == obj2) {
cout << "같습니다." << endl;
}
else {
cout << "같지 않습니다." << endl;
}
return 0;
}
다음과 같이 일치하는 "==" 연산자가 없다는 오류가 나온다. 주어진 코드는 Person 클래스를 정의하고 두 객체를 생성한 후, 두 객체 간의 등호(==) 비교를 시도하고 있지만 현재의 Person 클래스는 등호 비교 연산자(==)를 오버로딩하지 않았기 때문에, 컴파일러는 두 객체를 비교하는 방법을 알 수 없다. 따라서 등호 비교 연산자는 사용자 정의 클래스에서 객체 간의 비교를 수행하기 위해 오버로딩을 해야 한다. 이를 통해 사용자가 정의한 조건에 따라 아래 코드처럼 두 객체를 비교할 수 있다.
#include <iostream>
using namespace std;
class Person {
public:
int age;
Person(int a) : age(a) {}
// 등호 비교 연산자 오버로딩
bool operator==(const Person& other) const {
return this->age == other.age;
}
};
int main() {
Person obj1(20);
Person obj2(5);
if (obj1 == obj2) {
cout << "같습니다." << endl;
}
else {
cout << "같지 않습니다." << endl;
}
return 0;
}
6. 클래스에서의 정적 변수
정적 변수는 static를 붙여서 선언하는 변수로서 클래스마다 하나만 생성된다. 즉, 클래스의 모든 객체가 공유하는 변수로, 객체 생성과 상관 없이 해당 클래스의 모든 객체에 대해 단 하나의 인스턴스를 공유합니다.
#include <iostream>
using namespace std;
class Circle {
int x, y;
int radius;
public:
static int count; // 정적 변수
Circle() :x{ 0 }, y{ 0 }, radius{ 0 } {
count++;
}
Circle(int x, int y, int r) : x{ x }, y{ y }, radius{ r } {
count++;
}
};
int Circle::count = 0; // 클래스의 정적 멤버는 클래스 외부에서 정의되고 초기화되어야 한다. 초기화는 딱 한 번만 이루어져야 한다.
int main() {
Circle c1;
cout << "지금까지 생성된 원의 개수 = " << c1.count << endl;
Circle c2(100, 100, 30);
cout << "지금까지 생성된 원의 개수 = " << c1.count << endl;
}
위와 같이 c1의 count 개수를 알아내는데 c2 추가 후 2개가 증가된 걸 확인할 수 있다.
7. 클래스에서의 정적 멤버 함수
- 정적 멤버 함수는 객체가 생성되지 않은 상태에서 호출되는 멤버 함수이다.
- 일반 멤버 변수들은 정적 멤버 함수 안에서 사용할 수 없고 정적 변수와 지역 변수만을 사용할 수 있다.
- 아래 정적 멤버 함수 getCount()를 사용한 예제를 보자.
#include <iostream>
using namespace std;
class Circle {
int x, y;
int radius;
public:
static int count; // 정적 변수
Circle() :x{ 0 }, y{ 0 }, radius{ 0 } {
count++;
}
Circle(int x, int y, int r) : x{ x }, y{ y }, radius{ r } {
count++;
}
// 정적 멤버 함수
static int getCount() {
return count;
}
};
int Circle::count = 0; // 클래스의 정적 멤버는 클래스 외부에서 정의되고 초기화되어야 한다. 초기화는 딱 한 번만 이루어져야 한다.
int main() {
Circle c1;
cout << "지금까지 생성된 원의 개수 = " << Circle::getCount() << endl;
Circle c2(100, 100, 30);
cout << "지금까지 생성된 원의 개수 = " << Circle::getCount() << endl;
}
'Develop > C, C++' 카테고리의 다른 글
[C/C++] 객체 지향 프로그래밍과 클래스 (0) | 2023.12.25 |
---|---|
[C/C++] 생성자(constructor) & 접근 제어(access control) (0) | 2023.12.23 |
[C++] 객체 배열과 벡터 (0) | 2023.12.22 |
[C/C++] 상속(Inheritance) (0) | 2023.12.19 |
[C/C++] 동적할당(Dynamic Memory Allocation) (0) | 2023.12.18 |