중요한 Part이다.★★★★★
Stram Insertion and Extraction Overloading
◆ 스트림 삽입 연산자 오버로딩, 구현
● ostream의 참조자를 반환하여 chain insertion 이 가능하도록 구현해야 한다.
☞ 참조자를 반환하지 않으면 cout << p1 << p2와 같은 연산 불가
☞ 또한 기본적으로 cout 객체는 복사 불가
friend ostream& operator<<(ostream& os, const Point& rhs)
{
os << "[" << rhs.xPos << "," << rhs.yPos << "]";
return os;
}
● 전역 함수로 선언 필요하다.
☞ 멤버 함수로 선언하면 아래와 같이 사용해야 한다.
p1 << cout; // p1.operator<<(cout);
📌 Stream Insertion 연산자 오버로딩 정리
스트림 삽입 연산자 <<를 오버로딩할 때는 ostream 객체를 참조자로 받아서 참조자로 반환해야 한다.
이렇게 해야 cout << p1 << p2 << endl;처럼 여러 객체를 연속적으로 출력하는 체인 인서션(chain insertion) 이 가능하다.
만약 ostream을 값으로 반환하면 cout << p1 << p2 형태에서 p2에 대한 출력 연산을 적용할 수 없다.
이유는 cout << p1의 결과가 ostream 객체의 복사본이 되며, ostream은 복사가 금지된 객체이기 때문이다.
friend ostream& operator<<(ostream& os, const Point& rhs)
{
os << "[" << rhs.xpos << "," << rhs.ypos << "]";
return os;
}
위와 같이 ostream&을 반환하면 cout << p1 결과가 여전히 cout 객체의 참조이므로 그 뒤에 또 다른 객체를 출력할 수 있다.
예: cout << p1 << p2 << endl;
ostream은 내부적으로 복사가 금지된 클래스이기 때문에 함수 인자로 전달할 때도 참조로 받아야 한다.
예를 들어 ostream os와 같이 값으로 받게 되면 cout 객체를 복사하려 하므로 컴파일 오류가 발생한다.
또한 ostream을 반환할 때도 참조로 반환해야 한다.
void로 반환할 경우 cout << p1 연산의 결과가 없어 다음 연산 << p2가 불가능해지기 때문이다.
void << p2와 같은 형태는 문법적으로도 성립하지 않는다.
operator<<는 일반적으로 전역 함수로 선언한다.
멤버 함수로 선언할 경우 p1 << cout;처럼 객체가 왼쪽에 오게 되므로 직관성이 떨어지고 사용이 불편하다.
멤버 함수로 선언하면 다음과 같이 작성하게 된다:
void operator<<(std::ostream& os) {
os << xPos << "," << yPos;
}
이때 호출 방식은 p1.operator<<(cout); 또는 p1 << cout;이 되며, 출력 대상이 p1이 아니라 cout이라는 점에서 직관성이 떨어진다.
표준 스트림 연산의 방향성과 다르기 때문에 일반적으로는 전역 함수로 구현한다.
❓ chain insertion이란?
chain insertion은 cout << p1 << p2 << endl;처럼 연속된 << 연산이 가능한 구조를 의미한다.
이 구조가 가능하려면 << 연산의 결과로 ostream&이 반환되어야 한다.
즉, 각 연산의 결과가 다시 cout 객체로 이어져야 다음 출력도 가능하다.
반환형이 void이거나 값이면 다음 연산을 연결할 수 없다.
❓ 전역함수로 operator<< 선언 시 왜 반환형이 void면 안 되는가?
<< 연산자의 반환형이 void일 경우, 그 다음 << 연산을 연결할 수 없기 때문이다.
예: cout << p1 << p2;에서 cout << p1이 void를 반환한다면,
다음은 void << p2;가 되어 문법 오류가 발생한다.
또한 operator<<는 연속된 출력 작업을 가능하게 하기 위해 반드시 ostream&을 반환해야 한다.
이는 C++ 표준 라이브러리에서 정의된 방식과도 일치한다.
❓ int Fun1() 일 경우 int 인자가 반환된다고 하는 게 맞는가?
int Fun1()은 int 값을 반환한다.
정확히 말하면, 반환되는 int 값은 함수 바깥에서 사용할 수 있는 리턴값이다.
예시:
int Fun1() {
return 42;
}
int main() {
int x = Fun1(); // x에는 42가 들어간다.
}
이처럼 Fun1() 함수의 반환형이 int이면, 함수 호출 결과는 int 타입의 값이다.
만약 반환형이 int&라면 참조를 반환하는 것이고, 원본 변수에 직접 접근 가능하다.
❓cout 이 복사가 불가능 하기 때문에 인자로 받을 때 또 반환해 줄 때에도 참조자로 반환해줘야 한다. 더 깊은 심화 내용이 궁금하다.
- std::ostream은 내부적으로 복사 생성자와 복사 대입 연산자가 delete 되어 있음. 이는 복사 방지 목적으로 설계된 것.
- operator<< 함수는 std::ostream 외에도 std::stringstream, std::ofstream 등 다양한 출력 스트림에서 사용될 수 있다.
- operator<<는 보통 friend로 선언되지만, Point 클래스 내부 데이터에 접근할 필요가 없다면 일반 전역 함수로도 충분하다.
- operator<< 함수는 오버로딩될 수 있는 수많은 연산자 중에서 가장 표현력 있는 연산자로, 디버깅용 로그 출력에도 자주 쓰인다.
❓ 왜 전역 함수로 선언해야 하는가?
operator<<를 전역 함수로 선언하는 이유는 다음과 같다:
- 왼쪽 피연산자가 사용자 정의 클래스가 아니기 때문
- cout << p1;에서 cout은 ostream이고, 이는 표준 라이브러리 타입이다.
- 멤버 함수로 만들려면 왼쪽 피연산자인 ostream 클래스 안에 정의해야 하는데, 이는 우리가 수정할 수 없다.
- 표준적인 연산 방향 유지
- cout << p1은 익숙하고 직관적인 방식이다.
- 반면 p1 << cout은 사용자가 이해하기 어렵고 일반적이지 않다.
- 호환성과 유연성
- 전역 함수로 정의하면 여러 스트림(stringstream, ofstream 등)에 동일한 방식으로 적용 가능하다.
ChatGPT 로 만든 예
✅ 예제: Point_stream_out_in_depth.cpp
목적
- operator<<가 왜 참조자를 반환해야 하는지
- 왜 cout은 복사할 수 없는 객체인지 확인
#include <iostream>
using namespace std;
class Point {
private:
int x, y;
public:
Point(int xpos, int ypos) : x(xpos), y(ypos) {}
// 출력 연산자 오버로딩 (전역 함수, 참조자 반환)
friend ostream& operator<<(ostream& os, const Point& pt) {
os << "[" << pt.x << ", " << pt.y << "]";
return os; // 반드시 참조자 반환
}
};
int main() {
Point p1(1, 2);
Point p2(3, 4);
// 체인 인서션 확인
cout << p1 << " and " << p2 << endl;
return 0;
}
🔍 개념 정리
❓ 왜 참조자를 반환해야 하는가?
cout << p1 << p2;는 아래와 같이 해석된다:
((cout << p1) << p2)
- cout << p1의 결과는 ostream 참조자여야 한다.
- 그래야 다시 << p2가 연속해서 연결(chain) 될 수 있다.
- 만약 void를 반환하면 (void << p2)처럼 되므로 컴파일 오류 발생.
❓ 왜 cout은 복사할 수 없는가?
- std::cout은 std::ostream 타입의 전역 객체이며, 복사 생성자와 복사 대입 연산자가 삭제(deleted) 되어 있다.
- 예시로, 다음과 같이 작성하면 컴파일 오류가 발생한다:
ostream temp = cout; // ❌ 복사 생성자 없음
- 따라서 cout을 함수 인자로 전달할 때는 참조자(ostream&) 로 받아야 한다.
🔒 실험: 복사 시도 코드 (컴파일 오류 발생)
ostream copy = cout; // ❌ error: use of deleted function
컴파일러 오류 메시지:
error: use of deleted function ‘std::basic_ostream<char>::basic_ostream(const std::basic_ostream<char>&)’
이는 ostream이 복사 불가능하다는 명확한 증거이다.
◆ 스트림 추출 연산자 오버로딩, 구현
● 우측 매개변수(rhs)는 const가 아님에 유의할 것
friend istream& operator>>(istream& is, Point& rhs) {
int x = 0; int y = 0;
is >> x >> y;
rhs = Point{ x,y };
return is;
}
● 전역 함수로 선언
☞ 삽입 연산자와 마찬가지
📌 Stream Extraction 연산자 오버로딩 정리 (operator>>)
C++에서 >> 연산자는 표준 입력 스트림 객체 std::cin과 함께 사용된다.
cin은 istream 클래스의 객체이며, 사용자 정의 타입에 대해서도 입력 연산을 가능하게 하려면 operator>>를 오버로딩해야 한다.
🔸 예제 코드
class Point {
private:
int x, y;
public:
Point(int xpos = 0, int ypos = 0) : x(xpos), y(ypos) {}
friend istream& operator>>(istream& is, Point& rhs) {
int x = 0, y = 0;
is >> x >> y;
rhs = Point{ x, y };
return is;
}
friend ostream& operator<<(ostream& os, const Point& pt) {
os << "[" << pt.x << ", " << pt.y << "]";
return os;
}
};
이렇게 정의하면 다음과 같은 입력 코드가 가능해진다:
Point p1;
cin >> p1; // 사용자가 입력한 값으로 p1이 초기화된다
이 연산은 내부적으로 다음과 같이 해석된다:
operator>>(cin, p1);
🔍 함수 정의 설명
friend istream& operator>>(istream& is, Point& rhs)
- is: std::cin과 같은 입력 스트림 객체
- rhs: 입력을 받아 값을 채울 대상인 Point 객체 (p1)의 비-상수 참조
함수 내부 동작:
- 지역 변수 x, y를 선언하고 초기화한다.
- cin >> x >> y를 통해 사용자 입력을 받는다.
- 받은 값을 기반으로 새로운 Point 객체를 생성하여 rhs에 대입한다.
- is를 참조로 반환하여 chain extraction이 가능하게 한다 (cin >> p1 >> p2 등).
✅ 핵심 개념 요약
- rhs는 참조자이기 때문에 함수 내부에서 원래 객체(p1)의 값을 직접 수정할 수 있다.
- const가 붙으면 값을 바꿀 수 없으므로, 입력을 받아 대입할 수 없다.
- 반환형이 istream&인 이유는 >> 연산을 체이닝 하기 위해서이다.
❓ 스코프란 무엇인가?
스코프(scope)란 변수나 식별자가 유효한 범위를 의미한다.
예를 들어, 함수 내부에서 선언한 변수는 해당 함수 내에서만 유효하다:
void func() {
int x = 5; // x의 스코프는 func 함수 내부
}
하지만 참조자로 받은 rhs는 함수 내에서 수정하더라도 원본 객체인 p1이 영향을 받는다.
이것이 "스코프 밖에서 값을 바꿀 수 있다"는 말의 의미다.
스코프는 함수 범위의 개념이고, 참조는 그 바깥 객체를 가리키므로 값이 바뀌는 것이다.
❓ 참조자는 값 복사는 하지 않으면서 값을 바꿀 수 있는가?
그렇다. 참조자는 원본 객체에 대한 별칭(alias) 이다.
int a = 10;
int& ref = a; // ref는 a의 참조자
ref = 20; // a도 20으로 바뀐다
- ref는 a의 복사본이 아니라, a 자신을 가리킨다.
- 따라서 참조자는 복사 비용 없이 값을 직접 바꿀 수 있다.
- 함수 인자로 참조자를 쓰면, 값 전달이 아니라 직접 접근이므로 성능에도 좋고 수정도 가능하다.
❓ 참조자가 아닌 경우에는 어떤 문제가 발생하는가?
예: friend istream& operator>>(istream& is, Point rhs)
- rhs는 값 복사로 전달된다.
- 즉, p1의 복사본이 생성되어 함수 내에서 사용된다.
- 함수 내에서 rhs = Point{x, y};를 해도, 원본 p1은 바뀌지 않는다.
추가 문제점:
- 불필요한 복사 비용이 발생한다.
- 입력 연산의 목적은 값을 바꾸는 것이므로, 복사본을 수정하는 건 의미가 없다.
ChatGPT 로 만든 예
📄 예제: Point_stream_in.cpp
🎯 목적
- >> 연산자를 오버로딩하여 사용자 정의 타입 Point에 대해 std::cin >> p1; 사용 가능하도록 구현
- 참조자(Point&)를 통해 입력 값을 객체 내부에 반영
- istream& 반환을 통해 체인 입력 가능 (cin >> p1 >> p2;)
🔸 전체 코드
#include <iostream>
using namespace std;
class Point {
private:
int x, y;
public:
Point(int xpos = 0, int ypos = 0) : x(xpos), y(ypos) {}
// 출력 연산자 오버로딩 (디버깅용)
friend ostream& operator<<(ostream& os, const Point& pt) {
os << "[" << pt.x << ", " << pt.y << "]";
return os;
}
// 입력 연산자 오버로딩
friend istream& operator>>(istream& is, Point& pt) {
int x = 0, y = 0;
is >> x >> y; // 입력 받기
pt = Point{x, y}; // 객체 값 수정
return is; // 체인 입력을 위해 istream 참조 반환
}
};
int main() {
Point p1, p2;
cout << "두 개의 점 좌표를 입력하세요 (예: 3 4 5 6): ";
cin >> p1 >> p2;
cout << "입력된 좌표: ";
cout << p1 << " and " << p2 << endl;
return 0;
}
🔍 코드 설명
- cin >> p1;이 호출되면 operator>>(cin, p1) 형태로 컴파일러가 해석한다.
- p1은 참조자로 전달되므로 함수 내부에서 값을 바꾸면 원본 p1 객체에 직접 반영된다.
- x, y를 사용자로부터 입력받고, Point{x, y}를 만들어 p1에 대입한다.
- istream&을 반환하므로 cin >> p1 >> p2;와 같이 체인 입력이 가능하다.
- operator<<도 같이 구현해 두면 디버깅이나 확인용 출력에 매우 편리하다.
Operator Overloading
◆ 연산자 오버로딩 : 클래스에 대한 연산자의 적용 방식을 사용자가 직접 오버로딩하여 구현할 수 있다.
◆ 멤버 한수인 연산자 오버로딩 : 클래스의 멤버함수로 operatorX()라는 이름을 갖는 함수를 구현하여 연산자를 오버로딩 할 수 있다. 이때 이항 연산자의 경우 우측 피연산자는 인자로 넘어온다.
◆ 전역 함수인 연산자 오버로딩 : 멤버 함수로 구현 시 교환 법칙 문제가 발생할 수 있고, 이러한 경우 전역 함수로 오버로딩하며, 이때 friend 키워드를 사용하면 편리함
◆ 스트림 삽입 및 추출 연산자 오버로딩 : << , >> 도 연산자이며, cout / cin 객체에 대해 오버라이딩 하면 된다. Chain insertion, extraction을 위해 참조자 반환 필요
◆ 대입 연산자 오버로딩
◆ 첨자 연상자 오버로딩
📘 스트림 삽입 및 추출 연산자 정리
C++에서 <<, >>는 연산자 오버로딩이 가능한 이항 연산자이며,
스트림 삽입 연산자 (<<) 와 스트림 추출 연산자 (>>) 로 사용된다.
✅ cout과 cin의 정체
- cout은 ostream 클래스의 객체
- cin은 istream 클래스의 객체
즉, cout << "Hello"는 다음처럼 해석된다:
operator<<(ostream&, const char*);
cin >> x는 다음처럼 해석된다:
operator>>(istream&, int&);
✅ 스트림 삽입 연산자 (<<)
- 목적: 출력 대상 객체를 출력 스트림에 삽입
- 정의 예시:
friend ostream& operator<<(ostream& os, const Point& pt);
- 두 번째 인자는 const 참조자로 받는다.
- 이유: 출력을 위한 읽기 전용 접근만 필요하며, 값을 수정하지 않기 때문
- 반환형은 ostream& 이어야 체인 인설션(cout << a << b;)이 가능하다.
✅ 스트림 추출 연산자 (>>)
- 목적: 입력 스트림으로부터 값을 읽어 객체에 저장
- 정의 예시:
friend istream& operator>>(istream& is, Point& pt);
- 두 번째 인자는 비-const 참조자로 받는다.
- 이유: 입력을 받아 객체의 내부 상태를 수정해야 하기 때문
- 반환형은 istream& 이어야 체인 입력(cin >> a >> b;)이 가능하다.
✅ 핵심 요약
스트림 삽입 연산자는 객체 내용을 출력하는 용도이며,
추출 연산자는 입력값을 객체에 대입하는 용도이다.
이 둘 모두 연산자 오버로딩으로 구현 가능하고,
반환형은 참조자로 해야 체이닝이 가능하다는 점을 반드시 기억해야 한다.
연산자 | 첫 번쨰 인자 | 두 번째 인자 | 반환형 | 목적 |
<< | ostream& | const T& | ostream& | 객체를 출력 |
>> | istream& | T& | istream& | 입력 받아 저장 |
https://youtu.be/wWmgMQlyp58?si=ImTy6CnaHIEZJRY8
'
'C++ > C++ : Study' 카테고리의 다른 글
8. 연산자 오버로딩 (8) - 대입 연산자 , 깊은 복사 (3) | 2025.06.21 |
---|---|
8. 연산자 오버로딩 (7) - 대입 연산자 , 얕은 복사 (0) | 2025.06.20 |
8. 연산자 오버로딩 (5) - Ostream 객체 (0) | 2025.06.16 |
8. 연산자 오버로딩 (4) - 전역 함수로의 구현 (3) | 2025.06.15 |
8. 연산자 오버로딩 (3) - 단항 연산자 오버로딩 멤버 함수로의 구현 (0) | 2025.06.10 |