1. Const와 포인터, 그리고 멤버 함수
const의 위치에 따라 의미가 달라지는 것은 C++ 입문자가 가장 헷갈려 하는 부분이다. 핵심은 const 바로 왼쪽(없으면 오른쪽)이 상수화된다는 것이다.
#include <iostream>
class SoSimple {
private:
int num;
public:
SoSimple(int n) : num(n) {}
// 체이닝(Chaining)을 위한 참조 반환
SoSimple& AddNum(int n) {
num += n;
return *this;
}
// 일반 멤버 함수
void SimpleFunc() {
std::cout << "SimpleFunc: " << num << std::endl;
}
// const 멤버 함수: 이 함수 내에서는 멤버 변수(num)를 변경할 수 없다.
// const 객체는 const 함수만 호출할 수 있다.
void SimpleFunc() const {
std::cout << "const SimpleFunc: " << num << std::endl;
}
};
void YourFunc(const SoSimple &obj) {
// obj가 const 참조로 전달되었으므로, const 함수만 호출 가능하다.
obj.SimpleFunc();
}
int main() {
int val1 = 10, val2 = 20, val3 = 30;
// 1. 포인터가 가리키는 대상이 상수 (값 변경 불가, 주소 변경 가능)
const int * ptr = &val1;
// 2. 포인터 자체가 상수 (값 변경 가능, 주소 변경 불가)
int * const ptr2 = &val2;
// 3. 둘 다 상수 (값, 주소 모두 변경 불가)
const int * const ptr3 = &val3;
SoSimple obj1(2);
const SoSimple obj2(7); // const 객체 생성
obj1.SimpleFunc(); // 일반 함수 호출
obj2.SimpleFunc(); // const 함수 호출 (오버로딩)
YourFunc(obj1);
YourFunc(obj2);
return 0;
}
2. 참조자(Reference)와 배열
“배열의 레퍼런스”와 “레퍼런스의 배열”은 다르다. C++ 표준 문법상 레퍼런스의 배열은 만들 수 없지만, 배열을 가리키는 레퍼런스는 선언할 수 있다.
#include <iostream>
int main() {
int arr[3] = {1, 2, 3};
// 배열 포인터와 유사한 문법.
// 크기가 3인 int 배열에 대한 참조자 선언
int (&ref)[3] = arr;
ref[0] = 10;
ref[1] = 20;
ref[2] = 30;
// ref를 통해 원본 arr이 수정됨
std::cout << arr[0] << ", " << arr[1] << ", " << arr[2] << std::endl;
return 0;
}
3. 동적 할당 (new & delete)
new는 메모리 공간 할당(malloc)과 동시에 생성자 호출(초기화)을 수행한다. 질문 답변: delete list가 아니라 delete[] list를 써야 하는 이유는 컴파일러에게 **”이 포인터가 가리키는 것이 단순 변수 하나가 아니라 배열 덩어리다”**라고 알려줘야 하기 때문이다. 그래야 배열의 모든 요소(N개)에 대해 소멸자를 호출하여 메모리 누수를 막을 수 있다.
#include <iostream>
int main() {
// 1. 단일 변수 동적 할당
int* p = new int;
*p = 10;
std::cout << "Single allocation: " << *p << std::endl;
delete p; // 단일 해제
// 2. 배열 동적 할당
int arr_size;
std::cout << "Array size: ";
std::cin >> arr_size;
// 런타임에 크기가 결정되는 배열 할당
int *list = new int[arr_size];
for (int i = 0; i < arr_size; i++) {
list[i] = i * 10;
}
for (int i = 0; i < arr_size; i++) {
std::cout << i << "th element: " << list[i] << std::endl;
}
// 배열 해제 시 반드시 []를 붙여야 함.
// 그렇지 않으면 첫 번째 요소만 소멸되거나 정의되지 않은 동작 발생.
delete[] list;
return 0;
}
4. 함수 객체 (Functor)와 operator()
operator()를 오버로딩하면 객체를 함수처럼 사용할 수 있다. 이를 **Functor(함수 객체)**라고 한다. 언제 쓰는가? 일반 함수와 달리 객체는 ‘상태(Member Variable)’를 가질 수 있다. 즉, 함수가 호출될 때마다 데이터를 누적하거나, 특정 설정값을 유지한 채로 동작해야 할 때 매우 유용하다. (STL의 정렬 기준이나 알고리즘 등에서 많이 쓰임).
#include <iostream>
using namespace std;
class WowCat {
public:
// 객체를 함수처럼 호출할 때 실행됨 (인자 2개)
int operator()(int arg1, int arg2) {
return arg1 + arg2;
}
// 오버로딩 가능 (인자 3개)
int operator()(int arg1, int arg2, int arg3);
};
int WowCat::operator()(int arg1, int arg2, int arg3) {
return arg1 + arg2 + arg3;
}
int main() {
WowCat obj;
// 객체를 함수 이름처럼 사용
cout << "Functor(2 args): " << obj(1, 2) << endl;
cout << "Functor(3 args): " << obj(1, 2, 3) << endl;
// 명시적 호출 (잘 안 씀)
cout << "Explicit call: " << obj.operator()(2, 3) << endl;
// 멤버 함수 포인터 사용법 (복잡하지만 콜백 구현 시 사용됨)
int (WowCat::*fp)(int, int) = &WowCat::operator();
cout << "Function Pointer: " << (obj.*fp)(10, 100) << endl;
return 0;
}
5. 클래스, 생성자, 그리고 깊은 복사(Deep Copy)
C++에서 클래스 내부에 포인터 변수가 있다면, 복사 생성자를 직접 정의해야 한다. 기본 복사 생성자는 포인터의 주소값만 복사하는 얕은 복사(Shallow Copy)를 수행하기 때문에, 소멸자에서 delete를 두 번 하려다 에러(Double Free)가 발생한다. 이를 해결하는 것이 깊은 복사(Deep Copy)다.
#include <iostream>
#include <cstring> // for strlen, strcpy
class PhotonCannon {
int hp, shield;
int coord_x, coord_y;
int damage;
char *name; // 동적 할당할 멤버 변수
public:
PhotonCannon(int x, int y, const char *cannon_name);
PhotonCannon(const PhotonCannon &pc); // 복사 생성자
~PhotonCannon(); // 소멸자
void show_status();
};
// 일반 생성자
PhotonCannon::PhotonCannon(int x, int y, const char *cannon_name) {
hp = shield = 100;
coord_x = x;
coord_y = y;
damage = 20;
// 깊은 복사를 위한 메모리 할당
if (cannon_name) {
name = new char[strlen(cannon_name) + 1];
strcpy(name, cannon_name);
} else {
name = NULL;
}
}
// 복사 생성자 (Deep Copy 구현)
PhotonCannon::PhotonCannon(const PhotonCannon &pc) {
std::cout << "복사 생성자 호출! (Deep Copy)" << std::endl;
hp = pc.hp;
shield = pc.shield;
coord_x = pc.coord_x;
coord_y = pc.coord_y;
damage = pc.damage;
// 포인터 변수는 새로 메모리를 할당해서 내용물만 복사해야 한다.
if (pc.name) {
name = new char[strlen(pc.name) + 1];
strcpy(name, pc.name);
} else {
name = NULL;
}
}
// 소멸자
PhotonCannon::~PhotonCannon() {
if (name)
delete[] name; // 동적 할당 해제
std::cout << "소멸자 호출" << std::endl;
}
void PhotonCannon::show_status() {
std::cout << "Photon Cannon :: " << (name ? name : "No Name") << std::endl;
std::cout << " HP : " << hp << std::endl;
}
int main() {
PhotonCannon pc1(3, 3, "Cannon");
// 복사 생성자 호출됨. pc1과 pc2는 서로 다른 name 메모리 공간을 가짐.
PhotonCannon pc2 = pc1;
pc1.show_status();
pc2.show_status();
return 0;
}