학습 개요
- 복사와 이동을 용도에 맞게 활용할 수 있는 대입 및 이동 대입 연산자의 다중 정의에 대하여 학습함
- 두 가지 클래스의 예를 통하여 연산자 다중 정의를 적용해봄
- 먼저 배열을 표현하는 클래스를 통해 첨자를 통해 객체의 데이터를 액세스하기 위한 연산자를 구현하여 본 후, 문자열을 표현하기 위한 클래스를 구현해 봄
- C++의 표준 라이브러리에는 문자열을 보다 편리하게 사용할 수 있게 하는 string이라는 클래스를 제공하고 있는데, string 클래스와 유사한 처리를 할 수 있는 클래스를 직접 만들어 보면서, 그 과정에서 다양한 연산자를 다중 정의해 볼 것임
- 이 과정에서 자료형의 변환 연산자를 정의하는 방법도 함께 살펴볼 것임
학습 목표
- 대입 및 이동 대입 연산자를 필요에 맞게 다중 정의할 수 있음
- 배열 첨자 연산자를 다중 정의할 수 있음
- 문자열을 처리하기 위한 여러 가지 연산자를 다중 정의할 수 있음
- 자료형을 변환하는 연산자를 다중 정의할 수 있음
주요 용어
- 이동 대입 연산자
- 좌측 피 연산자에 대입할 우측 피 연산자가 rvalue일 때 우측 피 연산자의 내용을 좌측 피 연산자로 이동하는 대입 연산자
강의록
대입 및 이동 대입 연산자
대입 연산자의 다중 정의
- 대입 연산자 (
=
)- 묵시적인 대입 연산자
- 우측 피 연산자 데이터 멤버를 좌측 피 연산자에 그대로 복사함
- 객체에 동적 할당 된 메모리를 가리키는 포인터가 포함되어 있을 경우 얕은 복사로 인해 의도하지 않은 공유 상태의 문제가 발생할 수 있음
- 깊은 복사를 할 수 있는 대입 연산자를 다중 정의할 필요가 있음
- 묵시적인 대입 연산자
VecF 클래스의 대입 연산자 다중 정의
1 2 3 4 5 6 7 8
VecF& VecF::operator=(const VecF& fv) { if (n != fv.n) { // 벡터의 크기가 다르다면 delete[] arr; // 기존 메모리를 반환하고 arr = new float[n = fv.n]; // 새로 메모리를 할당함 } memcpy(arr, fv.arr, sizeof(float)*n); // 데이터 복사 return *this; // 자기 자신을 참조로 돌려줌 }
이동 대입 연산자의 다중 정의
- 이동 대입 연산자 (
=
)- 좌측 피 연산자에 대입 할 우측 피 연산자가 rvalue일 때 사용됨
- 대입 후 우측 피 연산자의 내용이 더 이상 필요 없는 상황
- 우측 피 연산자의 내용을 좌측 피 연산자로 이동하여 불필요한 복사를 피함으로써 효율성을 높일 수 있음
- 좌측 피 연산자에 대입 할 우측 피 연산자가 rvalue일 때 사용됨
VecF 클래스의 이동 대입 연산자 다중 정의
1 2 3 4 5 6 7
VecF& VecF::operator=(VecF&& fv) { // 임시로 만들어진 VecF 객체와 자원을 이동 delete[] arr; // 기존 메모리를 반환하고 n = fv.n; // 우측 피 연산자의 내용을 이동함 arr = fv.arr; fv.arr = nullptr; // nullptr로 바꿔 fv가 배열을 가리키지 않도록 함 return *this; }
대입 및 이동 대입 연산자의 활용
VFMain3.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#include <iostream> using namespace std; int main() { float a[3] = { 1, 2, 3 }; float b[3] = { 2, 4, 6 }; VecF v1(3, a); VecF v2(3, b); VecF v3(3); // 대입 연산자 (l-value를 사용한 깊은 복사) v3 = v1; // 복사 대입 연산자 호출 cout << v3 << endl; // 이동 대입 연산자 (r-value를 사용한 이동) v3 = v1.add(v2); // v1과 v2를 더한 결과를 새로 만들어 임시 객체로 반환 cout << v1 << " + " << v2 << " = " << v3 << endl; return 0; } // [ 1 2 3 ] // [ 1 2 3 ] + [ 2 4 6 ] = [ 3 6 9 ]
복사 대입
이동 대입
- 이동 대입에서는
v1.add(v2)
로 생성된 임시 객체의 메모리 소유권이v3
로 효율적으로 이동 됨
- 이동 대입에서는
std::move
함수의 활용
- 두 VecF 객체를 교환하는 함수의 구현
아래
swapVecF
함수는 대입 연산자를 사용하므로 비효율적인 복사가 두 번 발생함1 2 3 4 5
void swapVecF(VecF& v1, VecF& v2) { VecF tmp(v1); // 복사 생성 v1 = v2; // 대입 연산자 사용 v2 = tmp; // 대입 연산자 사용 }
1 2 3 4 5 6 7 8
int main() { float a[3] = { 1, 2, 3 }; float b[3] = { 2, 4, 6 }; VecF vec1(3, a); VecF vec2(3, b); swapVecF(vec1, vec2); }
std::move
함수- 인수로 전달되는 객체의 rvalue 참조를 반환
- lvalue를 rvalue처럼 사용할 수 있게 함
- 이동 생성자나 이동 대입 연산자를 강제로 호출할 수 있음
VecF tmp = std::move(v1);
- v1의 rvalue 참조를 구하여 tmp의 초기화에 사용
- 이동 생성자를 이용하여 tmp 생성
v1 = std::move(v2);
- v2의 rvalue 참조를 구하여 v1에 대입
- 이동 대입 연산자 실행
- 인수로 전달되는 객체의 rvalue 참조를 반환
- 두 VecF 객체를 교환하는 함수의 구현 - std::move 활용
std::move
를 사용하면 불필요한 데이터 복사 없이 포인터의 교환만으로 함수를 구현할 수 있어 효율적임
1 2 3 4 5
void swapVecF(VecF& v1, VecF& v2) { VecF tmp = move(v1); // 이동 생성자 v1 = move(v2); // 이동 대입 연산자 사용 v2 = move(tmp); // 이동 대입 연산자 사용 }
1 2 3 4 5 6 7 8
int main() { float a[3] = { 1, 2, 3 }; float b[3] = { 2, 4, 6 }; VecF vec1(3, a); VecF vec2(3, b); swapVecF(vec1, vec2); }
- 실행 과정
VecF tmp = move(v1);
tmp
가v1
의 데이터 소유권을 이전 받음v1
의 포인터는nullptr
가 됨
v1 = move(v2);
v1
이v2
의 데이터 소유권을 이전 받음v2
의 포인터는nullptr
가 됨
v2 = move(tmp);
v2
가tmp
의 데이터 소유권을 이전 받음tmp
의 포인터는nullptr
가 됨
- 결과적으로
v1
과v2
의 내용물이 효율적으로 교환 됨
[]
연산자의 다중 정의
SafeIntArray
클래스
- 요구 사항
배열처럼 지정 된 개수의 int 값을 저장할 수 있음
1
SafeIntArray a(10); // 10개의 int 값을 저장하는 객체
각각의 값들은 0번부터 시작하는 일련 번호를 첨자로 지정하여 액세스함
1
a[5] = 10; // 6번째 위치에 10을 저장함
첨자가 지정 된 범위를 벗어날 경우 오류 메시지를 출력한 후 프로그램을 종료함
1
cout << a[11]; // 범위를 벗어나므로 오류 발생
[]
연산자의 다중 정의
[]
연산자- 배열의 첨자를 지정하는 이항 연산자
- 피 연산자
- 배열과 첨자
데이터를 저장하기 위해 사용할 [] 연산자
1 2
SafeIntArray a(10); a[5] = 10; // a -> *this, 5 -> i
1 2 3 4
// SafeIntArray 클래스 내부 int& SafeIntArray::operator[](/assets/img/knou/cpp/2025-06-23-knou-cpp-9/int i) { // 정수 변수의 참조를 반환하기 때문에 a(n)의 값을 직접 바꾸거나 읽을 수 있음 return arr[i]; // *this 객체의 멤버인 배열의 원소를 참조로 반환 }
[]
연산자의 다중 정의
const
객체를 위한[]
연산자- 데이터를 읽기만 할 수 있도록
[]
연산자를 정의const
로 선언된 객체는 내부 데이터를 변경할 수 없으므로, 데이터를 읽기만 하는const
멤버 함수만 호출할 수 있음const
객체에 대해[]
연산자를 사용하려면const
버전의operator[]
를 별도로 정의(다중 정의)해야 함
1 2 3 4
void f(const SafeIntArray& x) { // x가 const이기 때문에 값을 바꿀 수 없음 for (int i=0; i < x.size(); i++) cout << x[i] << endl; // []는 x의 값을 수정할 수 있는 연산자임 -> 에러 발생 }
1 2 3
// int& SafeIntArray::operator[](/assets/img/knou/cpp/2025-06-23-knou-cpp-9/int i) { // const가 아닌 operator[]는 멤버를 수정할 수 있는 int&를 반환하므로 const 객체에 대해 호출될 수 없음 // }
1 2 3
int SafeIntArray::operator[](/assets/img/knou/cpp/2025-06-23-knou-cpp-9/int i) const { // const 한정자를 붙이고 값을 반환(l-value가 아닌)하도록 operator[]를 다중 정의 }
- 데이터를 읽기만 할 수 있도록
SafeIntArray.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
class SafeIntArray {
int limit; // 원소의 개수
int *arr; // 데이터 저장 공간
public:
// 생성자
SafeIntArray(int n) : limit(n) {
arr = new int[n]; // 공간 할당
}
// 소멸자
~SafeIntArray() {
delete[] arr; // 공간 반환
}
// 배열의 크기를 반환
int size() const { return limit; }
// l-value로 사용될 수 있는 operator[] (non-const 객체용)
int& operator[](/assets/img/knou/cpp/2025-06-23-knou-cpp-9/int i) { // i번 원소를 반환하는 멤버 함수
if(i < 0 || i >= limit) {
std::cout << "첨자가 범위를 벗어나 프로그램을 종료합니다.";
exit(EXIT_FAILURE);
}
return arr[i]; // i번 원소 반환
}
// r-value로 사용될 수 있는 operator[] (const 객체용)
int operator[](/assets/img/knou/cpp/2025-06-23-knou-cpp-9/int i) const {
if(i < 0 || i >= limit) {
std::cout << "첨자가 범위를 벗어나 프로그램을 종료합니다.";
exit(EXIT_FAILURE);
}
return arr[i]; // i번 원소 반환
}
};
SafeArr.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include "SafeIntArray.h"
using namespace std;
int main()
{
SafeIntArray a(10); // 10개의 원소를 갖는 객체 생성
for (int i = 0; i < 10; i++)
a[i] = i;
cout << a[5] << endl; // 올바른 범위의 원소 액세스
cout << a[12] << endl; // 범위를 벗어난 액세스
return 0;
}
문자열 클래스
C 스타일 문자열
- 문자열의 저장
- 문자열의 끝을 알리기 위해 널(null) 문자(
'\0'
)를 사용함- Null-terminated string
문자열을 저장하기 위한
char
형 배열1
char str1[40] = "C style string";
1
char str2[] = "Object-Oriented ";
문자열 리터럴
1
const char* str3 = "Programming";
- 문자열의 끝을 알리기 위해 널(null) 문자(
- C 스타일 문자열 처리 함수 (헤더 파일:
#include <cstring>
)문자열의 길이 구하기
1
size_t strlen(const char* str);
n = strlen("abcde");
→n = 5
- 널 문자는 길이에 포함되지 않음
문자열 복사하기
1
char* strcpy(char* strDestination, const char* strSource);
1 2
char str1[10] = "KNOU"; strcpy(str1, "CS");
문자열 연결하기
1
char* strcat(char* strDestination, const char* strSource);
1 2
char str2[10] = "KNOU"; strcat(str2, "CS");
MyString
클래스
- 요구 사항
문자열을 저장하되 다음의 다중 정의된 연산자를 포함하며, 실행 시 필요에 따라 저장 공간을 늘릴 수 있음
연산자 기능 =
대입 연산자, C 스타일 문자열이나 MyString 객체를 복사함 +
문자열 연결 연산자, 두 문자열을 연결한 문자열을 구힘 +=
문자열을 뒤에 추가함 ==
,>
,<
관계 연산자, 두 문자열의 등호 및 순서를 비교함 <<
스트림 출력 연산자, 출력 스트림으로 문자열을 출력함 []
문자열 내의 개별 문자 접근 생성자 및 소멸자
1
MyString();
1
MyString(const char* str);
1
MyString(const MyString& mstr);
1
MyString(MyString&& mstr);
1
MyString(int s); // private - 내부용으로만 사용함
1
~MyString();
기타 메소드
1
int length() const; // 문자열 길이 반환
데이터 멤버
데이터 멤버 용도 int len
문자열의 길이를 저장함 int bufSize
최대로 저장할 수 있는 문자열의 길이를 저장함 char* buf
문자열 저장 공간
MyString.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
class MyString {
int len; // 문자열의 길이
int bufSize; // 저장 가능한 문자열의 길이
char* buf;
MyString(int s); // 생성자(private)
public:
MyString(); // 디폴트 생성자
MyString(const char* str); // 생성자
MyString(const MyString& mstr); // 복사 생성자
MyString(MyString&& mstr); // 이동 생성자
~MyString(); // 소멸자
int length() const; // 문자열 길이 반환 메소드
// --- 연산자 다중 정의 ---
MyString& operator=(const MyString& mstr); // 대입 연산자
MyString& operator=(MyString&& mstr); // 이동 대입 연산자
MyString operator+(const MyString& mstr) const; // 문자열 연결 연산자
MyString& operator+=(const MyString& mstr); // 문자열 추가 연산자
bool operator==(const MyString& mstr) const; // == 연산자
bool operator>(const MyString& mstr) const; // > 연산자
bool operator<(const MyString& mstr) const; // < 연산자
char& operator[](/assets/img/knou/cpp/2025-06-23-knou-cpp-9/int i); // [] 연산자
char operator[](/assets/img/knou/cpp/2025-06-23-knou-cpp-9/int i) const; // const [] 연산자
// 스트림 출력 연산자 (friend 함수로 선언)
friend std::ostream& operator<<(std::ostream& os, const MyString& mstr); // 외부 함수나 클래스가 클래스 내부에 접근 가능
};
// 스트림 출력 연산자 구현 (inline)
inline std::ostream& operator<<(std::ostream& os, const MyString& mstr)
{
os << mstr.buf;
return os;
}
MyString.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <iostream>
#include <cstring>
#include "MyString.h"
// private 생성자
MyString::MyString(int s) : len(s), bufSize(s + 1) { // 멤버 변수 값 선언 시 초기화
buf = new char[s + 1];
buf[s] = '\0';
}
// 디폴트 생성자
MyString::MyString() : len(0), bufSize(1) {
buf = new char[1];
buf[0] = '\0';
}
// 생성자
MyString::MyString(const char* str) {
len = bufSize = strlen(str); // 문자열 길이
buf = new char[len + 1]; // 문자열 저장 공간 할당
strcpy(buf, str); // 문자열 복사
}
// 복사 생성자
MyString::MyString(const MyString& mstr) : len(mstr.len), bufSize(mstr.len) {
buf = new char[len + 1];
strcpy(buf, mstr.buf);
}
// 이동 생성자
MyString::MyString(MyString&& mstr) : len(mstr.len), bufSize(mstr.bufSize), buf(mstr.buf) {
mstr.buf = nullptr;
}
// 소멸자
MyString::~MyString() {
delete[] buf;
}
// 문자열 길이 반환 메소드
int MyString::length() const {
return len;
}
// 대입 연산자
MyString& MyString::operator=(const MyString& mstr) {
if (bufSize < mstr.len) { // 문자열 공간이 필요량보다 작으면
delete[] buf; // 기존 공간 반환
len = bufSize = mstr.len; // 새로운 문자열의 길이
buf = new char[len + 1]; // 새로운 공간 할당
} else { // 그렇지 않으면
len = mstr.len; // 문자열의 길이만 수정
}
strcpy(buf, mstr.buf);
return *this;
}
// 이동 연산자
MyString& MyString::operator=(MyString&& mstr) {
delete[] buf;
len = mstr.len;
bufSize = mstr.bufSize;
buf = mstr.buf;
mstr.buf = nullptr;
return *this;
}
// 문자열 연결 연산자
MyString MyString::operator+(const MyString& mstr) const {
MyString tmstr(len + mstr.len); // private으로 정의한 생성자
strcpy(tmstr.buf, buf);
strcpy(tmstr.buf + len, mstr.buf);
return tmstr;
}
MyString& MyString::operator+=(const MyString& mstr) {
if (bufSize < len + mstr.len) {
char* tbuf = new char[bufSize = len + mstr.len + 1];
strcpy(tbuf, buf);
delete[] buf;
buf = tbuf;
}
strcpy(buf + len, mstr.buf);
len += mstr.len;
return *this;
}
1
2
3
4
5
6
7
8
char cstr1[10] = "C string";
char cstr2[10];
char* cstr3;
MyString mstr1("MyString 객체");
MyString mstr2;
// cstr2 = cstr1; // 에러
cstr3 = cstr1; // 포인터 복사
mstr2 = mstr1; // deep copy
MSMain.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
MyString str1("MyString class");
MyString str2("Object Oriented ");
MyString str3;
cout << str1 << endl; // 문자열 출력
str3 = "Programming"; // 묵시적 형 변환 및 이동 대입
cout << str3 << "의 문자열 길이는 ";
cout << str3.length() << endl; // 문자열 길이를 구하는 메소드
str1 = str2; // 대입 연산자로 문자열 복사
cout << str1 << endl;
str1 = str1 + str3; // 문자열 연결 연산자, 이동 대입 연산자
}
string
string
이란?문자열을 저장하기 위한 표준 C++ 라이브러리의 클래스
1
basic_string<char>
헤더 파일
1
#include <string>
- 연산자
[]
,+
,+=
,=
,==
,!=
,>
,>=
,<
,<=
, 스트림 입출력(<<
,>>
) 등
- 멤버 함수
length
,append
,find
,c_str
등
- 함수
stoi
,stod
,to_string
,swap
,getline
등
자료형의 변환
묵시적 형 변환
MyString 클래스의 묵시적 형 변환
1 2
str3 = "Programming"; // 묵시적 변환 (자동) str3 = MyString("Programming"); // 명시적 객체 생성
1 2
MyString& operator=(const MyString& mstr); // 복사 대입 연산자 MyString& operator=(MyString&& mstr); // 이동 대입 연산자
MyString("Programming")
은 임시 객체 → rvalueMyString&&
이동 대입 연산자가 호출 됨
형 변환 연산자를 정의하는 위치
- 값을 제공하는 송신 측 클래스에서 정의하는 방법
- 값을 받는 수신 측 클래스의 이름으로 연산자를 정의함
MyString 클래스의 객체를 C 스타일 문자열로 변환하기
1 2 3 4 5 6 7 8
class MyString { operator char*() const { char* pt = new char[length()+1]; strcpy(pt, buf); return pt; } };
- 값을 받는 수신 측 클래스의 이름으로 연산자를 정의함
- 값을 제공 받는 수신 측 클래스에서 정의하는 방법
송신 측 클래스의 객체를 인수로 받는 1인수 생성자를 정의함
1
MyString(const char* str);
- 문제점
- 송신 측 클래스의
private
멤버를 액세스해야 변환이 가능한 경우
- 송신 측 클래스의
- 변환 대상 송신 측 클래스의
private
멤버를 액세스할 수 있는 멤버 함수가 송신 측 클래스에 정의되어 있어야 함 Meter 클래스와 Feet 클래스
1 2 3 4 5 6 7 8 9 10 11
class Meter { int m; int cm; public: int getM() const { return m; } int getCM() const { seturn cm; } };
1 2 3 4 5 6 7 8 9 10 11 12
class Feet { int ft; int in; public: Feet(const Meter& m) { int cmeter = m.getM()*100 + m.getCM(); in = static_cast<int>(cmeter/2.54+0.5); ft = in / 12; in %= 12; } };
생성자를 이용한 묵시적 형 변환의 금지
explicit
으로 선언된 생성자1 2 3
class MyString { explicit MyString(const char* str); // 생성자 };
- 묵시적 변환 금지
에러 발생
1 2
MyString str; str = "Programming"; // 자동 변환 안됨
명시적 변환 시켜 사용
1
str = MyString{ "Programming" };
1
str = static_cast<MyString>("Programming");
연습 문제
int형 값을 저장하는 배열을 표현하는 클래스에서 배열 멤버의 원소를 액세스하기 위한
[]
연산자의 원형을 올바르게 작성한 것은?a.
int& operator [ ] (int i);
- 값을 읽거나 값을 저장할 수 있어야 하므로 참조형을 사용할 필요가 있음
- 원소를 참조하기 위한 첨자는 int형 값으로 해야 하며, 2항 연산자로서 배열 객체와 첨자를 각각 좌측과 우측 피 연산자로 간주하여 연산자의 매개변수로 int형 첨자를 받을 수 있게 선언함
위 지문은 두 객체의 내용을 서로 교환하기 위한 함수를 작성한 것이다. ㈀에 넣을 적절한 내용은?
1 2 3 4 5
void swap(C1& a, C2& b) { C1 tmp = // ___㈀___(a); // C1의 이동 생성자 a = // ___㈀___(b); // C1의 이동 대입 연산자 사용 b = // ___㈀___(tmp); // C1의 이동 대입 연산자 사용 }
a.
std::move
std::move
함수는 인수로 전달되는 객체의 rvalue 참조를 반환함으로써 이 함수에서 이동 생성자 및 이동 대입 연산자가 사용될 수 있게 함
int형 값을 저장하는 배열을 표현하는 클래스에서 배열 멤버의 원소를 액세스하기 위한
[]
연산자의 원형을 올바르게 작성한 것은?1 2 3 4 5 6 7 8
class MyString { int len, bufSize; char* buf; public: bool operator >= (const MyString& mstr) const { return // ___㈀___; } };
a.
strcmp(buf, mstr.buf) >= 0
strcmp(str1, str2)
는 str1과 str2가 같으면 0, str1이 str2보다 사전 순서로 앞서면 음, 그렇지 않으면 양의 값을 반환함
위 지문의 클래스에서 Pencils 객체를 int형인 연필 자루 수로 형 변환하는 연산자를 선언하기 위해 ㈀에 넣을 내용은?
1 2 3 4 5 6 7 8
class Pencils { int dozen; // 타 int np; // 낱개 public: // ___㈀___ { return dozen * 12 + np; } };
a.
operator int( ) const
- 변환하고자 하는 자료형을 연산자로 선언하면 형 변환 연산자가 만들어짐
정리 하기
- 대입 연산자는 우측 피 연산자의 내용을 좌측 피 연산자에 복사하고, 이동 대입 연산자는 rvalue인 우측 피 연산자를 이동하는 연산자임
std::move
함수는 인수로 전달되는 객체의 rvalue 참조를 반환함- 첨자 연산자(
[]
)는 첨자에 의해 지정되는 객체에 값을 저장하는 용도와 읽기 전용의 용도를 모두 고려하여 정의함 - string은 문자열을 저장하기 위한 표준 C++ 라이브러리의 클래스로서, 문자열을 저장하고 처리하기 위한 유용한 기능을 제공함
- 형 변환 연산자는 값을 제공하는 클래스에서 값을 받는 수신 측 클래스의 이름으로 연산자를 제공하거나, 값을 제공 받는 클래스에서 값을 제공하는 클래스의 객체를 인수로 받는 1인수 생성자를 정의하여 만듬