▣ 대입 및 이동 대입 연산자
● 대입 연산자( = )란?
▷ 묵시적 대입 연산자: 오른쪽 피연산자 데이터 멤버를 왼쪽 피연산자에 그대로 복사
▷ 객체에 동적할당된 메모리를 가리키는 포인터가 포함되어 있으면
-> 얕은 복사로 인해 의도하지 않은 공유 상태의 문제가 발생할 수 있다.
-> 깊은 복사를 할 수 있게 대입 연산자를 다중정의 해야 한다.
● 대입 연산자 다중정의 예시
▷ VecF 클래스
VecF& VecF::operator=(const VecF& fv) { //VecF& 참조로 반환한다.
if (n != fv.n) { //벡터의 크기가 다르면
delete[] arr; //기존 메모리 반환
arr = new float[n = fv.n]; //새로 메모리 할당
}
memcpy(arr, fv.arr, sizeof(float)*n); //fv.arr을 arr에 데이터 복사
return *this;
}
● 이동 대입 연산자 다중정의
▷ 이동 대입 연산자
- 왼쪽 피연산자에 대입할 오른쪽 피연산자가 r-value일 때 사용된다.
- 대입 후에 오른쪽 피연산자의 내용이 더 이상 필요 없는 상황에 사용한다.
- 오른쪽 피연산자의 내용을 왼쪽 피연산자로 "이동"해서 불필요한 복사를 안하게 해서 효율성이 높아진다.
● 이동 대입 연산자 다중정의 예시
▷ VecF 클래스
VecF& VecF::operator=(VecF&& fv) { //r-value 참조 전달
delete[] arr; // 기존 메모리 반환
n = fv.n; //오른쪽 피연산자 내용을 이동시킨다
arr = fv.arr; //포인터만 복사 (얕은 복사)
fv.arr = nullptr; //fv.arr 은 아무것도 안가리키게 한다.
return *this;
}
● 대입 & 이동 대입 연산자 활용 예시
▷ VFMain3.cpp
int main()
{
float a[3] = {1,2,3};
float b[3] = {2,3,4};
VecF v1(3,a);
VecF v2(3,b);
VecF v3(3);
/* 대입 연산자 */
v3 = v1;
cout << v3 << endl;
/* 이동 대입 연산자 */
v3 = v1.add(v2);
cout << v1 << " + " << v2 << " = ";
cout << v3 << endl;
return 0;
}
- v1은 l-value이므로 "v3 = v1"에서 대입 연산자가 동작하고 v3에 v1 값만 복사된다.
- "v3 = v1.add(v2)"에서 v1.add(v2)는 v3에 값을 전달하고 나면 필요가 없어지는 객체이므로 r-value다. 따라서 v3에 값을 이동하는 이동 대입 연산자를 이용한다.
- 불필요한 복사를 피하면서 값을 이동만 시켜서 효율을 높일 수 있다.
- C++ Shell에서 실행 가능하게 하나의 코드로 합친 파일을 첨부하였다.
● std::move 함수 활용
▷ 두 VecF 객체를 교환하는 함수 구현 -1
void swapVecF(VecF& V1, VecF& v2) {
VecF tmp(v1); // 복사 생성자
v1 = v2; // 대입 연산자
v2 = tmp; // 대입 연산자
}
- "VecF tmp(v1);"에서 v1은 l-value이므로 복사 생성자가 동작하여 tmp에는 vec1의 데이터가 깊은 복사된다.
- v2도 l-value이기 때문에 "v1 = v2;"에서 대입 연산자가 동작하고 깊은 복사된다.
- tmp도 l-value이기 때문에 "v2=tmp;"에서 대입 연산자가 동작한다.
- 함수를 빠져나오면서 tmp는 쓸모 없어지기 때문에 delete에 의해 메모리가 소멸된다.
int main()
float a[3] = {1, 2, 3};
float b[3] = {2, 3, 4};
VecF vec1(3, a), vec2(3, b);
swapVecF(vec1, vec2);
...
}
▷ std::move 함수
- 인수로 전달되는 객체의 r-value 참조를 반환한다.
VecF tmp = std::move(v1);
- v1의 r-value 참조를 구해서 tmp를 초기화한다.
- 이동 생성자를 이용해서 tmp를 생성한다.
v1 = std::move(v2);
- v2의 r-value 참조를 구해서 v1에 대입한다.
- 이동 대입 연산자가 실행된다.
▷ std::move 함수 활용
void swapVecF(VecF& v1, VecF& V2) {
VecF tmp = move(v1);
v1 = move(v2);
v2 = move(tmp);
}
int main() {
float a[3] = {1, 2, 3};
float b[3] = {2, 3, 4};
VecF vec1(3, a), vec2(3, b);
swapVecF(vec1, vec2);
...
}
- VecF tmp = move(v1);
이동 생성자에 의해 tmp가 vec1의 arr을 가리킨다.
move 함수가 종료되면서 vec1의 arr은 아무것도 가리키지 않는다
- v1 = move(v2);
이동 대입 연산자에 의해 vec1은 vec2의 데이터를 가리킨다.
move 함수가 종료되면서 vec2의 arr은 아무것도 가리키지 않는다.
- v2 = move(tmp);
이동 대입 연산자에 의해 vec2는 tmp의 데이터를 가리킨다.
move 함수가 종료되면서 tmp의 arr은 아무것도 가리키지 않는다.
swapVecF 함수가 끝나면서 소멸자에 의해 tmp 객체가 소멸된다.
☞ 결국엔 vec1과 vec2가 가리키는 데이터가 교환된다. 교환 과정에서 arr 메모리의 내용을 복사하지 않고 포인터를 통해 객체의 내용만 교환했기 때문에 효율적이다. 이동 대입 연산자의 효과다.
▣ [ ] 연산자 다중정의
● 예시 - SafeIntArray 클래스
▷ 요구사항
- 배열처럼 지정된 개수의 int 값을 저장할 수 있다.
SafeIntArray a(10); --> int값 10개를 저장하는 객체 - 각각의 값들은 0번부터 시작하는 일련번호를 첨자로 지정하여 접근한다.
a[5] = 10; --> 6번째 위치에 10을 저장 - 첨자가 지정된 범위를 벗어나면 오류 메시지를 출력하고 프로그램 종료한다.
cout << a[11];
▷ [ ] 연산자
- 배열의 첨자를 지정하는 이항 연산자
- 피연산자: 배열과 첨자
▷ 데이터 저장을 위해 사용하는 [ ] 연산자
SafeIntArray a(10);
a[5] = 10;
int& SafeIntArray::operator[](int i){
.... *this
}
- int& : a[5]에 값 10을 넣기 위해 참조를 반환해야 한다.
- *this: a[ ]
- int i: a[5]에서 5에 해당된다.
▷ const 객체를 위한 [ ] 연산자
데이터를 읽기만 할 수 있도록 [ ] 연산자를 정의함
void f(const SafeIntArray& x) { //상수 참조 -> x의 값을 수정하면 안된다.
for (int i=0; i < x.size(); i++) //size: 배열의 크기
cout << x[i] << endl;
}
int& SafeIntArray::operator[](int i) {
...
}
객체 x는 상수참조 [ ] 연산자는 일반멤버로 정의되어 있다. [ ]는 x의 값을 수정할 수 있는 연산자라는 뜻이다. 그래서 오류가 발생한다.
int SafeIntArray::operator[](int i) const{
...
}
참조를 그대로 반환하면 상수라는 처리에 위반되도록 값이 바뀔 수 있으므로, 참조를 반환하지 말고 int형으로 반환한다. 하지만 데이터의 양이 많을 때는 상수참조형을 반환해도 된다.
▷ SafeIntArray.h
#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; }
//i번 원소 반환하는 멤버함수(int형 참조)
int& operator[](int i) { //첨자의 범위가 올바른지 확인
if (i < 0 || i >= limit) {
std::cout << "첨자가 범위를 벗어나서 프로그램이 종료됩니다.";
exit(EXIT_FAILURE); //EXIT_FAILURE은 return값을 1로 반환하며 프로그램이 종료되게 만든다.
}
return arr[i]; //i번 원소 반환
}
int operator[](int i) { //상수 반환형식
if (i < 0 || i >= limit) {
std::cout << "첨자가 범위를 벗어나서 프로그램이 종료됩니다.";
exit(EXIT_FAILURE);
}
return arr[i]; //i번 원소 반환
}
};
▷ SafeIntArray.h
#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형 배열
- 문자열 리터럴
문자열 리터럴이기 때문에 그냥 문자 포인터로는 가리킬 수 없고, 상수 문자 포인터로 가리키게 할 수 있다.
▷ C 스타일 문자열 처리 함수(헤더파일: #include <cstring>)
- 문자열 길이 구하기 (strlen 함수)
size_t strlen(const char* str);
n = strlen("abcde"); // n = 5
- MS Visual C++에서는 보안 개선 함수를 제공한다.
size_t strnlen_s(const char* str, size_t nE1);
//size_t nE1으로 문자열 버퍼 크기를 지정해서 이 크기를 넘어가면 에러가 발생하도록 한다.
char str[10] = "abcde";
n = strnlen_s(str, 10);
- 문자열 복사 (strcpy 함수)
char* strcpy(char* strDestination, const char* strSource);
char sample[20] = "HELLO";
strcpy(sample, "ICE");
문자열을 복사하면 복사한 문자열의 NULL(\0) 문자를 만나면 문자열의 끝이라고 인식하기 때문에 ICE만 출력되고 뒤에 O는 출력되지 않는다.
+) 대입 명령으로 배열에 문자열 리터럴을 넣는 것을 불가능하다.
sample = "ICE"; //불가능
sample은 배열의 이름이고 포인터를 저장하는 건데 포인터의 값을 직접적으로 바꾸는 것은 불가능하다.
- MS Visual C++의 보안 개선 함수
errno_t strcpy_s(char* strD, size_t nE1, const char* strS);
//목적지의 크기를 미리 size_t nE1로 지정해서 그 크기를 넘어가면 에러가 발생하게 한다.
- 문자열 연결하기 (strcat 함수)
char* strcat(char* strDestination, const char* strSource);
char sample[20] = "HELLO";
strcar(sample, "ICE");
문자열을 연결했을 때 전체 문자열 크기가 원래 할당된 크기를 넘어서지 않았는지 꼭 확인해야 한다.
- MS Visual C++의 보안 개선 함수
errno_t strcat_s(char* strD, size_t nE1, const char* strS);
C 스타일 문자열은 포인터를 이용해서 사용해야 하기 때문에 번거로울 수 있으므로 우리는 문자열 클래스를 만들어서 이용하려고 한다.
● MyString 클래스
▷ Requirements
- 문자열을 저장하되 다음의 다중정의된 연산자를 포함하며, 실행시 필요에 따라 저장공간을 늘릴 수 있다.
연산자 | 기능 |
= | 대입 연산자. C 스타일 문자열이나 MyString 객체를 복사한다. |
+ | 문자열 연결 연산자. 두 문자열을 연결한 문자열을 구한다. |
+= | 문자열을 뒤에 추가한다. |
==, >, < | 관계연산자. 두 문자열의 등호 및 순서를 비교한다. |
<< | 스트림 출력 연산자. 출력 스트림으로 문자열을 출력한다. |
[ ] | 문자열 내의 개별 문자 접근 |
- 생성자와 소멸자 등 멤버 함수 정의
메소드 | 설명 |
MyString( ) | default 생성자 (public 선언) |
MyString(const char* str); | 지정된 문자열을 가지고 만드는 생성자 (public 선언) |
MyString(const MyString* mstr); | 복사 생성자. 동일한 클래스의 객체를 상수 참조로 받음. (public 선언) |
MyString(MyString&& mstr); | 이동 생성자. r-value 참조로 받음. (public 선언) |
MyString(int s); | private - 내부용으로 사용 |
~MyString( ); | 소멸자 (public 선언) |
int length( ) const; | 문자열의 길이를 반환. 상수 멤버함수로 선언. |
- 데이터 멤버
데이터 멤버 | 용도 |
int len | 문자열의 길이를 저장한다. (len<=bufSize) |
int bufSize | 최대로 저장할 수 있는 문자열의 길이를 저장한다. |
char *buf | 문자열 저장 공간 |
▷ MyString.h
#include <iostream>
class MyString {
int len; //문자열 길이
int bufSize; // 저장 가능한 문자열의 길이
char *buf; // 문자열 저장할 곳
MyString(int s); //생성자(private). 문자열의 크기만 지정해줘서 메모리를 만드는 것까지만 함.
public:
MyString(); // default 생성자. 빈 문자열 생성.
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[](int i);
char operator[](int i) const;
//스트림 출력 연산자를 만들어야 함.
//cout 객체가 MyString 객체가 아니므로 클래스 외부에 별도로 선언을 해야 한다.
//외부에서 내부의 데이터에 접근할 수 있도록 friend 연산자 선언
friend std::ostream& operator<<(std::ostream &os, const MyString &mstr);
};
//스트림 출력 연산자
//좌측: ostream 객체 참조, 우측: 출력할 MyString 객체 참조
inline std::ostream& operator<<(std::ostream &os, const MyString &mstr)
{
os << mstr.buf;
return os;
}
▷ MyString.cpp
#include <iostream>
#include <cstring>
#include "MyString.h"
MyString::MyString(int s) : len(s), bufSize(s){ //클래스 내부에서만 사용됨(private)
buf = new char[s + 1]; // 길이가 s인 문자 + 널 문자 1개
buf[s] = '\0';
}
MyString::MyString() : len(0), bufSize(0) //default 생성자
{
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)
{ //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; //원래 mstr이 가리키던 포인터를 없애버림 나중에 아예 사라지게
return *this;
}
//문자열 연결 연산자
MyString MyString::operator+(const MyString &mstr) const
{
//문자열을 연결하면 길이가 길어질테니 tmstr이라는 임시 스트링객체를 만들고
//자기자신의 길이와 mstr 길이를 더한 길이만큼을 확보할 수 있는 생성자를 정의한다.(private)
MyString tmstr(len + mstr.len);
strcpy(tmstr.buf, buf);
strcpy(tmstr.buf + len, mstr.buf); //tmstr.buf + len이 null문자가 있는 위치에 이어붙임
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;
}
//(기존길이+복사할문자열)길이를 가지는 buf에서 len위치가 널문자 위치이므로
//거기에 mstr.buf를 복사해준다.
strcpy(buf + len, mstr.buf);
len += mstr.len;
return *this;
}
// 관계 연산자 ==
bool MyString::operator==(const MyString &mstr) const
{
return !strcmp(buf, mstr.buf);
}
// 관계 연산자 >
bool MyString::operator>(const MyString &mstr) const
{
return strcmp(buf, mstr.buf) > 0;
}
// 관계 연산자 <
bool MyString::operator<(const MyString &mstr) const
{
return strcmp(buf, mstr.buf) < 0;
}
char& MyString::operator[](int i)
{
return buf[i];
}
char MyString::operator[](int i) const
{
return buf[i];
}
대입 형태의 연산자들은 다 그 객체의 참조를 반환하게 선언되어 있다는 것을 유념하자.
"대입 연산자"를 정의했기 때문에 아래와 같은 코드 결과를 보일 수 있다.
char cstr1[10] = "C string";
char cstr2[10];
char *cstr3;
MyString mstr1("MyString 객체");
MyString mstr2;
cstr2 = cstr1; //에러!
cstr3 = cstr1; //포인터만 복사. 같은 문자열을 가리킴. shallow copy
mstr2 = mstr1; //메모리를 복사. 별개의 문자열이다. deep copy
▷ Main.cpp
#include <iostream>
#include "MyString.h"
using namespace std;
int main()
{
MyString str1("MyString class");
MyString str2("Object Oriented ");
MyString str3;
cout << str1 << endl;
str3 = "Programming"; //대입연산자 이용 -> 묵시적 형 변환 ->임시객체 생성-> r-value-> 이동 대입
cout << str3 << "'s length of the string is: ";
cout << str3.length() << endl;
str1 = str2; //l-value -> 대입 연산자 -> 문자열 복사
cout << str1 << endl;
str1 = str2 + str3; //문자열 연결 연산자 -> 더해진 객체 r-value -> 이동 대입 연산자
cout << str1 << endl;
MyString str4(str3);
cout << str4 << endl;
str2 += "Programming";
cout << str2 << endl;
str2[6] = '-';
cout << str2 << endl;
return 0;
}
▷ 출력 결과
● string 클래스
▷ C++ 표준 라이브러리에는 string이라는 문자열을 저장할 수 있는 클래스를 제공한다.
▷ basic_string<char>라는 표준 C++ 라이브러리의 클래스 템플릿으로 선언된 것이다.
- 헤더파일: #include <string>
- 연산자: [ ], +, =, +=, ==, !=, >, >=, <, <=, 스트림 입출력(<<, >>) 등
- 멤버함수: length, append, find, c_str 등
- 함수: stoi, stod, to_string, swap, getline 등
string to integer, string to double, ...
▣ 자료형 변환
● 묵시적 형 변환
▷ MyString 클래스의 묵시적 형 변환에 대해 더 알아보자.
str3 = "Programming";
위와 같이 대입을 하려면, 대입 연산자와 이동 대입 연산자를 사용할 것이다.
MyString& operator=(const MyString &mstr); //대입 연산자
MyString& operator=(MyString &&mstr); //이동 대입 연산자
두 대입 연산자를 사용하기 위해서는 MyString 참조를 받아야 하는데, str3 = "Programming"은 두 가지 모두에 해당되지 않는다. 그래서 두 연산자를 사용할 수 없다.
이 데이터를 가지고 MyString 객체로 형변환을 해줄 수 있는 생성자가 존재하는 지를 본다.
MyString(const char *str);
이런 생성자가 있기 때문에 "Programming"을 MyString 객체로 묵시적 형 변환을 해준다.
여기서, 묵시적 형 변환을 통해 임시로 만들어준 객체가 되기 때문에 r-value에 해당되므로 이동 대입 연산자를 통해서 대입 연산이 동작한다.
● 형 변환 연산자를 정의하는 위치
(1) 값을 제공하는 송신 측 클래스에서 정의하는 방법
- 값을 받는 수신 측 클래스의 이름으로 연산자를 정의한다.
- 예) MyString 클래스의 객체를 C 스타일 문자열로 변환하는 것
class MyString {
....
operator char*() const{
char *pt = new char[length()+1];
strcpy(pt, buf);
return pt;
}
};
(2) 값을 받는 수신 측 클래스에서 정의하는 방법
- 송신 측 클래스의 객체를 인수로 갖는 1 인수 생성자를 정의한다.
- 예) MyString(const char* str);
- 문제점: 송신 측 클래스의 private 멤버를 액세스해야 변환이 가능한 경우
--> 변환 대상 송신 측 클래스의 private 멤버를 접근할 수 있는 멤버함수가 송신 측 클래스에 정의되어 있어야 한다. 아니면, 송신 측 클래스에 수식 측 클래스의 멤버함수를 친구(friend) 클래스로 정의를 해준다.
▷ 생성자를 이용한 묵시적 형 변환 금지 상황에선 어떻게?
- explicit으로 선언된 생성자는 묵시적으로 사용할 수 없다.
class MyString {
...
explicit MyString(const char *str); //명시적으로만 사용해라!
...
};
// 실행
MyString str;
str = "Programming"; //에러 발생
이런 경우에는 아래와 같은 방법으로 객체를 만들어서 이동 대입이 될 수 있게 해야 한다.
// 해결방법 1
str = MyString("Programming"};
// 해결방법 2
str = static_cast<MyString>("Programming");
객체를 따로 만들어주거나, static_cast를 이용해서 강제 형 변환을 해줘야 한다.
'Programming > C++' 카테고리의 다른 글
C++ 언어 기초 (13) - 추상클래스, 다중상속 (0) | 2020.09.04 |
---|---|
C++ 언어 기초 (11) - 상속; 기초/파생클래스, 접근 제어, final, name binding (0) | 2020.09.03 |
C++ 언어 기초 (9) - 연산자 다중정의 (0) | 2020.08.31 |
[C++] 위임 생성자, 초기화 리스트 생성자 (0) | 2020.08.29 |
[C++] 복소수 Complex 연산 클래스 만드는 방법 (0) | 2020.08.28 |