Subscript([ ]) operator overloading
◆ 첨자 연산자 오버로딩에서 고려해야 할 점
● 첨자 연산자 오버로딩에서 배열에 대한 복사 생성 허용 여부의 결정이 필요
☞ 허용하지 않고 싶을 경우 아래와 같이 해상 생성자를 private 선언 또는 delete로 접근 불가능하게 한다.
private:
Point* arr;
int arr_len;
public:
PointArr(const PointArr &arr) {} // private 복사 생성자
PointArr& operator=(const PointArr &arr) {} // private 대입 연산자
혹은
private:
Point* arr;
int arr_len;
public:
PointArr(const PointArr &arr) = delete; // delete 는 해당 함수를 구현하지 않는다는 의미
PointArr& operator=(const PointArr &arr) = delete;
📘 핵심 요약: 객체 복사 허용 여부를 제어하는 방법
✅ 상황
Array a1{5,10};
Array a2{3,5};
a2 = a1;
Array a3 = a1;
- 위 코드는 대입 연산자와 복사 생성자가 자동 생성되어 있기 때문에 컴파일이 됨
- 하지만 실무에서 배열을 깊게 복사하거나 복사 자체를 허용하지 않는 경우도 많음
- 그 이유는 복사 시 이중 할당/해제, 성능 저하, 의도치 않은 값 공유 등의 문제가 발생할 수 있기 때문
✅ 복사 금지 방법 1: private 으로 숨김
class Array {
private:
Array(const Array& rhs) {}
Array& operator=(const Array& rhs) {}
};
- 외부에서 호출이 불가능해짐
- 복사 생성이나 대입을 차단
✅ 복사 금지 방법 2: = delete (C++11 이후)
class Array {
public:
Array(const Array& rhs) = delete;
Array& operator=(const Array& rhs) = delete;
};
- 컴파일러가 이 함수 자체를 생성하지 말라고 명시
- 오히려 private보다 더 명확하고 안전한 방법
✅ 정말 실무에서 배열 복사를 제한할까?
대부분 제한하는 편이다. 이유는:
이유 | 설명 |
성능 이슈 | 큰 배열을 복사하면 시간/메모리 낭비 |
얕은 복사 문제 | 공유된 포인터가 이중 delete 유발 |
의도하지 않은 데이터 공유 방지 | 버그 유발 가능성 있음 |
그래서 C++ STL의 대부분 컨테이너(std::unique_ptr, std::ofstream 등)는
복사 불가능하게 만들고 이동만 허용한다.
ChatGPT 로 만든 예제
📄 예제: Point_asMember_subscript.cpp
🎯 설명
첨자 연산자 전체 코드
// Point_asMember_subscript.cpp
// 첨자 연산자 오버로딩 (배열 멤버 접근) 전체 예제
#include <iostream>
using namespace std;
class Point {
private:
int xpos;
int ypos;
public:
Point(int x = 0, int y = 0) : xpos(x), ypos(y) {}
void ShowPosition() const {
cout << "[" << xpos << ", " << ypos << "]" << endl;
}
};
class PointArr {
private:
Point* arr;
int arr_len;
public:
PointArr(int len) : arr_len(len) {
arr = new Point[len];
}
~PointArr() {
delete[] arr;
}
int get_arr_len() const {
return arr_len;
}
// 쓰기 가능한 operator[] 오버로딩
Point& operator[](int idx) {
if (idx < 0 || idx >= arr_len) {
cout << "Array out of bound!" << endl;
exit(1);
}
return arr[idx];
}
// 읽기 전용 operator[] 오버로딩 (const 객체용)
Point operator[](int idx) const {
if (idx < 0 || idx >= arr_len) {
cout << "Array out of bound!" << endl;
exit(1);
}
return arr[idx]; // 복사본 리턴
}
};
int main() {
PointArr parr(3);
parr[0] = Point(3, 4);
parr[1] = Point(5, 6);
parr[2] = Point(7, 8);
for (int i = 0; i < parr.get_arr_len(); i++) {
parr[i].ShowPosition();
}
const PointArr cparr = parr;
cparr[0].ShowPosition(); // 읽기 전용 접근 가능
return 0;
}
- Point 클래스를 배열처럼 관리하는 PointArr 클래스가 있고,
- 첨자 연산자 operator[]를 읽기/쓰기용 두 가지 버전으로 오버로딩했다.
🔐 경계 검사도 들어가 있어서 안전하고,
📦 const PointArr로 선언한 객체도 읽기 접근 가능하도록 설계했음.
Operator Overloading
◆ 연산자 오버로딩 : 클래스에 대한 연산자의 적용 방식을 사용자가 직접 오버로딩하여 구현할 수 있다.
◆ 멤버 한수인 연산자 오버로딩 : 클래스의 멤버함수로 operatorX()라는 이름을 갖는 함수를 구현하여 연산자를 오버로딩 할 수 있다. 이때 이항 연산자의 경우 우측 피연산자는 인자로 넘어온다.
◆ 전역 함수인 연산자 오버로딩 : 멤버 함수로 구현 시 교환 법칙 문제가 발생할 수 있고, 이러한 경우 전역 함수로 오버로딩하며, 이때 friend 키워드를 사용하면 편리함
◆ 스트림 삽입 및 추출 연산자 오버로딩 : << , >> 도 연산자이며, cout / cin 객체에 대해 오버라이딩 하면 된다. Chain insertion, extraction을 위해 참조자 반환 필요
◆ 대입 연산자 오버로딩 : 기본 대입 연산자는 얕은 복사를 수행하기 때문에, 깊은 보가사가 필요한 경우 대입 연산자 직접 오버로딩이 반드시 필요
◆ 첨자 연상자 오버로딩 : 일반적으로 const 멤버와 참조 반환 함수 두 개를 오버로딩하여 구현
📘 연산자 오버로딩 총정리
1. ✅ 연산자 오버로딩이란?
- 클래스 객체는 기본적으로 연산자 사용이 불가능하다.
- 사용자가 직접 operatorX() 형식의 함수를 정의해 연산자 동작을 설정할 수 있다.
a1 + a2 → a1.operator+(a2)
p1 * 3 → p1.operator*(3)
3 * p1 → operator*(3, p1) // 반드시 전역 함수!
2. ✅ 오버로딩 방식
구분 | 설명 |
멤버 함수 | a.operator+(b) 형식. 왼쪽 피연산자가 클래스 객체일 때 유리 |
전역 함수 | operator+(a, b) 형식. friend 필요. 좌우 대칭 또는 기본형 연산에 필요 |
3. ✅ 주요 연산자별 오버로딩 정리
💬 스트림 삽입/추출 연산자 (<<, >>)
- operator<<(ostream&, const T&)
- operator>>(istream&, T&)
- 반드시 참조형 반환해야 chain insertion/extraction 가능
💬 대입 연산자 (=)
- 기본 대입 연산자는 얕은 복사를 수행
- 멤버 변수에 포인터가 있다면 깊은 복사 필요
- 패턴:
Array& operator=(const Array& rhs) {
if (this == &rhs) return *this; // ① 자기 자신 확인
delete[] ptr; // ② 기존 메모리 해제
ptr = new int[rhs.size]; // ③ 새 메모리 할당
size = rhs.size;
for (...) ptr[i] = rhs.ptr[i]; // ④ 값 복사
return *this; // ⑤ 자기 자신 참조 반환
}
💬 첨자 연산자 ([ ])
- a[i] 형태를 가능하게 만드는 연산자
- 반드시 멤버 함수로만 구현해야 한다.
- 두 가지 버전으로 오버로딩해야 완전하다:
int& operator[](int idx); // 쓰기 가능 (a[i] = 10;)
int operator[](int idx) const; // 읽기 전용 (const 객체에서 호출 가능)
- 범위 검사도 함께 해주는 것이 안전하다.
4. ✅ 복사 금지 설정
이유 | 설명 |
성능 문제 방지 | 큰 배열 복사는 느리고 위험 |
포인터 복사 | 이중 해제 오류 유발 가능 |
class Array {
Array(const Array& rhs) = delete;
Array& operator=(const Array& rhs) = delete;
};
5. ✅ 연산자 오버로딩을 배우면서 반드시 알아야 하는 전제 지식
- 클래스 생성자/소멸자
- 참조자, 포인터, this 포인터
- 복사 생성자/대입 연산자
- const 멤버 함수, const 객체 사용 규칙
- friend 키워드
- 힙 메모리와 동적 할당
💡 마무리 멘트
연산자 오버로딩은 문법이 아니라 개념이다.
의미에 맞는 동작을 사용자가 책임지고 설계해야 하며,
클래스와 메모리 구조에 대한 충분한 이해가 전제되어야 한다.
실무에서도 자주 쓰이며, 코딩 테스트나 인터뷰에서도 아주 좋은 질문 포인트가 된다.
🧠 연산자 오버로딩 복습 퀴즈 (5문제)
✅ Q1. 다음 코드의 출력 결과는?
class Test {
public:
int value;
Test(int v) : value(v) {}
Test operator+(const Test& rhs) {
return Test(value + rhs.value);
}
};
int main() {
Test t1(5), t2(7);
Test t3 = t1 + t2;
std::cout << t3.value << std::endl;
}
📌 보기:
A) 컴파일 에러
B) 12
C) 0
D) 쓰레기 값 출력
정답: B
✅ Q2. 다음 중 전역 함수로만 오버로딩이 가능한 연산자는?
📌 보기:
A) [ ]
B) <<
C) =
D) ->
정답: B
(해설)
- []: 반드시 멤버 함수로만 가능 ❌
- <<: 보통 전역 함수로 구현 (ostream은 왼쪽이 기본형이라 멤버로 못 씀) ✅
- =: 반드시 멤버 함수만 가능 ❌
- ->: 반드시 멤버 함수만 가능 ❌
✅ Q3. 아래 코드에서 오류가 발생하는 이유는?
class Sample {
public:
int operator[](int idx) const {
return arr[idx];
}
private:
int arr[5] = {1, 2, 3, 4, 5};
};
int main() {
Sample s;
s[0] = 10;
}
📌 보기:
A) const 함수에서 값을 수정하려 했기 때문
B) 참조자를 반환하지 않았기 때문
C) 배열이 선언되어 있지 않기 때문
D) 오버로딩 자체가 잘못되었기 때문
정답: B
(해설)
int operator[](int idx) const {
return arr[idx]; // 값 복사
}
// s[0] = 10; 은 불가능
- 반환형이 int이므로 값 복사 → 수정 불가능 → lvalue 아님
🔹 정답: B) 참조자를 반환하지 않았기 때문 ✅
✅ Q4. 다음 중 operator= 오버로딩에서 빠뜨리면 이중 해제 오류가 발생할 수 있는 중요한 단계는?
📌 보기:
A) 자기 자신인지 확인
B) delete[] 기존 메모리
C) size 값 복사
D) return *this;
정답: B
(해설)
Array& operator=(const Array& rhs) {
if (this == &rhs) return *this;
delete[] ptr; // 💥 중요!
...
}
- 이 단계가 없으면 기존 포인터 메모리를 누수 or 이중 해제할 위험
🔹 정답: B) delete[] 기존 메모리 ✅
✅ Q5. 다음 중 아래 코드에서 출력 결과로 올바른 것은?
class My {
public:
int data;
My(int d) : data(d) {}
friend std::ostream& operator<<(std::ostream& os, const My& m) {
os << m.data;
return os;
}
};
int main() {
My m1(42), m2(7);
std::cout << m1 << m2 << std::endl;
}
📌 보기:
A) 컴파일 에러
B) 427
C) 42 7
D) "My Object"
정답: B
💻 실전 코테형 문제: Matrix 클래스 구현
📘 문제 설명
정수 행렬을 표현하는 Matrix 클래스를 구현하시오.
다음 조건을 만족해야 한다.
✅ 요구사항
- 생성자
- Matrix(int rows, int cols) 형태로 생성
- 모든 원소는 0으로 초기화
- 첨자 연산자 오버로딩
- matrix[i][j] 형태로 접근 가능해야 함
- 유효하지 않은 인덱스 접근 시 throw std::out_of_range 예외 발생
- 대입 연산자 오버로딩
- 깊은 복사로 동작할 것
- 기존 메모리는 삭제하고, 새롭게 할당하여 복사
- 출력 연산자 오버로딩
- std::cout << matrix; 호출 시 아래와 같이 출력
1 2 3
4 5 6
🔎 함수 시그니처 예시
class Matrix {
public:
Matrix(int rows, int cols);
Matrix(const Matrix& other);
Matrix& operator=(const Matrix& other);
~Matrix();
int* operator[](int row);
const int* operator[](int row) const;
friend std::ostream& operator<<(std::ostream& os, const Matrix& m);
};
🧪 채점용 예시 코드
int main() {
Matrix m1(2, 3);
m1[0][0] = 1; m1[0][1] = 2; m1[0][2] = 3;
m1[1][0] = 4; m1[1][1] = 5; m1[1][2] = 6;
std::cout << m1;
Matrix m2 = m1;
m2[0][0] = 10;
std::cout << m2;
std::cout << m1; // 원본 m1은 여전히 1로 출력되어야 함
try {
m1[3][0]; // 예외 발생
} catch (const std::out_of_range& e) {
std::cout << "Index error caught!" << std::endl;
}
}
📝 구현 포인트 요약
구현 포인트 | 설명 |
첨자 연산자 오버로딩 | int* operator[](int)로 2차원처럼 접근 |
예외 처리 | 범위 벗어나면 throw std::out_of_range |
깊은 복사 구현 | operator=에서 기존 메모리 해제 후 복사 |
출력 오버로딩 | friend std::ostream& operator<< |
💡 구현 코드 전문
#include <iostream>
class Matrix {
private:
int rows;
int cols;
int** ptr;
public:
Matrix(int rows, int cols)
: rows(rows), cols(cols)
{
// 선언 : 포인터의 포인터 (행 갯수만큼 포인터 배열 만들고, 각 포인터는 열 배열을 가르킴)
ptr = new int* [rows];
for (int i = 0; i < rows; ++i)
{
ptr[i] = new int[cols] {}; // 각 행마다 열 수만큼 배열 할당 {} 으로 값은 0으로 초기화
}
}
int* operator[](int value)
{
if (value < 0 || value >= rows)
throw std::out_of_range("Row out of range");
return ptr[value];
}
const int* operator[](int value) const { // 읽기 전용
if (value < 0 || value >= rows)
throw std::out_of_range("Row out of range");
return ptr[value];
}
Matrix& operator=(const Matrix& rhs)
{
if (this == &rhs) return *this;
// 기존 메모리 해제
for (int i = 0; i < rows; ++i) delete[] ptr[i];
delete[] ptr;
// 새 메모리 할당
rows = rhs.rows;
cols = rhs.cols;
ptr = new int* [rows];
for (int i = 0; i < rows; ++i) {
ptr[i] = new int[cols];
for (int j = 0; j < cols; ++j) {
ptr[i][j] = rhs.ptr[i][j];
}
}
return *this;
}
friend std::ostream& operator<<(std::ostream& os, const Matrix& m)
{
for (int i = 0; i < m.rows; ++i)
{
for (int j = 0; j < m.cols; ++j)
{
os << m.ptr[i][j] << ' ';
}
os << '\n';
}
return os;
}
~Matrix()
{
for (int i = 0; i < rows; ++i)
{
delete[] ptr[i];
}
delete[] ptr;
// 2차 배열을 만들었다면 2중으로 delete도 필요하다.
}
};
int main()
{
Matrix m1(2, 3);
m1[0][0] = 1; m1[0][1] = 2; m1[0][2] = 3;
m1[1][0] = 4; m1[1][1] = 5; m1[1][2] = 6;
// m1[0][0] => m1[0](int) 호출 int* 반환
std::cout << m1;
Matrix m2 = m1;
m2[0][0] = 10;
std::cout << m2;
std::cout << m1; // 원본 m1 은 여전히 1로 출력되어야 함
try
{
m1[3][0]; // 예외 발생
}
catch (const std::out_of_range& e)
{
std::cout << "Index error caught!" << std::endl;
}
}
https://youtu.be/k5GxkrfsFNw?si=CkAZ1dJ24uKNavXa
6월 8일부터 시작한 연산자 오버로딩 6월 22일에 끝났다. 다음 강의로 넘어가자.
'C++ > C++ : Study' 카테고리의 다른 글
9. Generic Programming과 템플릿 (2) - 매크로 사용 (1) | 2025.06.25 |
---|---|
9. Generic Programming과 템플릿 (1) - 개요 (0) | 2025.06.24 |
8. 연산자 오버로딩 (9) - 첨자 연산자 오버로딩 (2) | 2025.06.22 |
8. 연산자 오버로딩 (8) - 대입 연산자 , 깊은 복사 (3) | 2025.06.21 |
8. 연산자 오버로딩 (7) - 대입 연산자 , 얕은 복사 (0) | 2025.06.20 |