operator[] 함수의 두 번째 인자는 어떤 타입이든 가능하다. → 따라서 vector, 연관 배열 같은 것들도 정의할 수 있다.
operator[] () 는 비 static 멤버 함수여야 한다.
struct Assoc {
vector<pair<string, int>> vec; // {name, value} 쌍의 배열
const int& operator[] (const string&) const;
int& operator[] (const string&);
}
// ...
// s 를 찾는다. 그 값이 발견되면 참조자를 반환한다.
// 그렇지 않을 경우 새로운 쌍 {s, 0} 을 만들고, 그 값에 대한 참조자를 반환한다.
int& Assoc::operator[](const string& s)
{
for(auto : vec)
if(s==x.first) return x.second;
vec.push_back({s,0});
return vec.back().second;
}
int main()
{
Assoc values;
string buf;
while(cin>>buf) ++values[buf];
for(auto x : values.vec)
cout << '{' << x.first << ',' << x.second << "}\\n";
}
19.2.2 함수 호출 ()
함수 호출 표기 () 는 다른 연산자와 동일하게 오버로딩 될 수 있고, 아래와 같이 사용될 수 있다.
() 연산자의 가장 중요한 용도는 어떤 측면에서 함수처럼 동작하는 개체를 위한 통상적인 함수 호출 문법을 제공하는 것이다.
struct Action {
int operator() (int);
pair<int, int> operator()(int, int);
double operator() (double);
// ...
};
void f(Action act)
{
int x = act(2);
int x = act(3,4);
int x = act(2.3);
};
이러한 개체를 이용하면 매개변수를 받아들이는 연산을 수행할 수 있다.
많은 경우 함수 개체가 연산을 수행하는 데 필요한 데이터를 갖고 있을 수 있느냐가 중요하다.
예를 들어 아래와 같이 저장된 값을 인자로 추가해주는 opearotr() () 를 가진 함수를 정의할 수 있다.
class Add {
complex val;
public :
Add(complex c) : val{c} {} // 값을 저장한다.
Add(double r, double i) : val{{r,i}} {}
void operator() (complex& c) const {c += val;} // 값을 인자에 추가한다.
};
아래 코드는 complex{2,3} 을 vec의 모든 원소에 추가하고 , complex{z} 를 lst 의 모든 원소에 추가 할 것이다.
for_each 함수의 3번째 인자는 opearotr() () 를 오버로딩 한 개체여야만 한다.
void g(Ptr p)
{
X* q1 = p -> // 문법 오류
X* q2 = p.operator->(); // ok
}
→ 오버로딩은 주로 ‘스마트 포인터’ 를 만드는 데 유용하다.
이후 내용은 강의 후 ..
19.2.4 증가와 감소 ++--
‘스마트 포인터’ 가 등장하자 사람들은 증가 연산자 ++ 과 감소 연산자 -- 를 자주 사용하게 됐다. → 이 연산자들은 기본 제공 타입에 대해 쓰이는 용도를 흉내 내기 위해서다. → 이는 통상적인 포인터 타입을 ‘스마트 포인터’ 로 대체하는 것이 목적인 경우에 필요하다.
스마트 포인터는 런타임 오류 체크가 추가된다는 점만 제외하면 통상적인 포인터와 동일한 의미 구조를 갖는다.
다음 코드는 p 가 범위를 벗어난다는 문제가 있다.
void f1(X a)
{
X v[200];
X* p = &v[0];
p--;
*p = a; // 문제 발생 : p 가 범위를 벗어난다!
++p;
*p = a; // ok
}
개선된 아래 코드를 살펴보자
여기서는 *X 를 Ptr<X> 클래스의 개체로 대체하려고 하는데, 이 개체는 실제로 X 를 가리키는 경우에만 역참조 될 수 있다.
또한 p 가 배열 내에서 가리키고 증가와 감소 연산의 결과로 해당 배열에 포함된 개체가 나올 떄만 p 가 증가되고 감소될 수 있게 보장하려고 한다.
void f2(Ptr<X> a)
{
X v[200];
Ptr<X> p(&v[0], v);
p--;
*p = a; // runtime error : p 는 범위를 벗어남
++p;
*p = a // ok
}
증가, 감소 연산자는 전위형 및 후위형 연산자로 모두 사용 가능하다는 점에서 C++ 연산자 중에서도 특이하다.
따라서 Ptr<T> 에 대해서 전위형 및 후위형의 증가, 감소 연산자를 정의해야 한다.
후위 연산자에 int 인자는 해당 함수가 ++ 의 후위형 적용에 대해 호출된다는 점을 나타내기 위해 사용된다. → 실제 int 는 사용되지 않는다.
template<typename T>
class Ptr {
T* ptr;
T* array;
int sz;
public :
template<int N>
Ptr(T* p, T(&a)[N]; // 배열 a 에 연결된다. sz==N, 초기 값 p
Ptr(T* p, T* a, int s) // s 크기를 갖는 배열 a 와 연결된다. 초기 값 p
Ptr(T* p) // 단일 개체와 연결된다. sz==0, 초기 값 p
Ptr& operator++(); // 전위형
Ptr& operator--(); // 전위형
Ptr operator++(int); // 후위형
Ptr operator--(int); // 후위형
T& operator*(); // 전위형
};
19.2.5 할당 및 할당 해제 newdelete
일반적으로 newdelete 연산자를 오버로딩 하는 것은 권장되지 않는다.. → 복잡도가 높아진다..
19.2.6 사용자 정의 리터럴
C++ 는 다양한 기본 제공 타입에 대한 리터럴을 제공한다.
123 // int
1.2 // double
1.2F // float
'a' // char
1ULL // unsigned long long
0xD0 // 16진수 unsigned
"aa" // C 스타일 문자열
추가적으로 우리는 사용자 정의 타입에 대한 리터럴과 기본 제공 타입에 대한 리터럴의 새로운 형식을 정의할 수 있다.
"Hi!"s // 문자열, "0 으로 종료되는 char 의 배열" 이 아니다.
1.2i // 허수
101010111000101b // 이진수
123s // 초
123.56km // 마일이 아니다(단위)!
1234567891234567890x // 확장된 정밀도
정의하는 방법은 다음과 같다. → 리터럴 연산자의 이름은 operaotr”” 에 접미사가 뒤따르는 것이다.
문자에 대한 접근 연산자의 설계는 어려운 주제이다. → 이상적으로 접근은 관용적 표기법 []을 이용해야 하고, 최대한 효율적이고, 범위를 체크해야 하기 때문이다. → 하지만 이런 속성을 전부 수용할 수 있는 방법은 존재하지 않는다…
여기서는 표준 라이브러리를 따라서 관용적인 [] 연산자와 범위 체크 at() 연산을 제공하고자 한다.
class String {
public :
// ...
char& operator[](int n) {return ptr[n];} // 체크되지 않은 원소 접근
char operator[](int n) const {return ptr[n];}
char& at(int n) {check(n); return ptr[n];} // 범위 체크 후 원소 접근
char at(int n) const {check(n); return ptr[n];}
String& operator+=(char c); // 끝에 c 를 추가한다.
char* c_str() {return ptr}; // C 스타일 문자열 접근
const char* c_str() const {return ptr;}
int size() const {return sz;} // 원소의 개수
int capacity() const // 원소 더하기에 이용 가능한 공간
{ return (sz<=short_max) ? short_max : sz+space;}
// ...
};
[] 를 통상적인 용도로 사용하자는 것이 기본 구상이다.
여기서는 at() 의 사용이 불필요한데, s 에 대해 0에서 s.size()-1 까지만 접근하기 때문이다.
int hash(const String& s)
{
if(s.size() == 0) return 0;
int h{s[0]}; // s 에 대한 체크되지 않은 접근
for(int i{1}; i<s.size(); ++i)
h ^= s[i] >> 1 // s 에 대한 체크되지 않은 접근
return h;
}
실수의 가능성이 보이는 곳에서는 at() 을 사용할 수 있다.
실수 가능성이 있는 곳에서도 [] 을 사용할 수 있기 때문에, std::string 표준 라이브러리의 일부 [] 구현에서는 범위를 체크한다.
하지만, 범위 체크에는 오버헤드가 발생한다는 점을 생각하고 상황에 따라서 구현이 필요하다.
C 스타일 문자열을 String 으로 변환하기 쉬고, C 스타일 문자열로서 String 문자에 쉽게 접근할 수 있게
힙 공간의 사용을 최소화하게
String 끝에 문자를 효율적으로 추가할 수 있게
결과는 복잡도가 올라갔지만, 효율적으로 동작한다.
아래 코드는 두 개의 문자열 표현을 통해서 **짧은 문자열 최적화 기법(short string optimization)**을 구현한다.
sz≤short_max 라면 문자들이 String 개체 내 ch 란 이름의 배열로 저장된다.
!(sz≤short_max) 라면 문자들이 힙 영역에 저장되고 확장을 위한 추가 공간을 할당할 수 있다.
두 경우 모두 ptr 은 원소를 가리킨다.
접근 함수는 어떤 표현이 쓰이고 있는지 검사할 필요가 없다.
그저 ptr 만 사용되면 되는 것이다.
sz≤short_max 인 경우에만 배열 ch 를 사용하고, !(sz≤short_max) 일 때는 정수 space 를 사용하기 때문에 ch 와 space 두 개 모두에 대한 공간을 할당하는 것은 낭비가 될 것이다. → 이런 낭비를 피하기 위해서 union 을 사용한다.
class String {
public:
// ...
private:
static const int short_max = 15;
int sz; // 문자의 개수
char∗ ptr;
union {
int space; // 사용되지 않는 할당된 공간
char ch[shor t_max+1]; // 종료에 쓰이는 0을 위해 공간을 남겨둔다
};
void check(int n) const // 범위 체크
{
if (n<0 || sz<=n)
throw std::out_of_rang e("String::at()");
}
// 부속 멤버 함수
void copy_from(const String& x);
void move_from(String& x);
};
19.3.4 멤버 함수
기본 생성자는 String 이 비어 있게 정의한다.
String::String()
: sz{0}, ptr{ch}
{
ch[0] = 0;
}
copy_from(), move_from() 이 있으면 생성자, 이동, 대입의 구현이 상당히 간단해진다.
아래 코드에서는 공간 체크, 새로운 공간 할당, 등 .. 상당히 많은 일들이 벌어지고 있다.
이런 코드의 복잡성을 생각하고 싶지 않다면 std::string 을 사용하자. → 라이브러리를 사용하는 이유가 바로 이것이다!
String::String(const char* p)
: sz{strlen(p)},
ptr{(sz<=short_max) ? ch : new char[sz+1]}, // 길이가 길다면 힙 영역에 할당한다.
space{0}
{
strcpy(ptr, p);
}
String::String(const String& x)
{
copy_from(x);
}
String::String(String&& x)
{
move_from(x);
}
String& String::operator=(const String& x)
{
if(this==&x) retrun *this;
char* p = (short_max<sz) ? ptr : 0;
copy_from(x);
delete[] p;
return *this;
}
String& String::operator(String&& x)
{
if(this==&x) return *this;
if(short_max<sz) delete[] ptr;
move_from(x);
return *this;
}
String& String::operator+=(char c)
{
if(sz==short_max) {
int n = sz+sz+2;
ptr = expand(ptr, n);
space = n-sz-2;
}
else if(short_max<sz) {
if(space==0)
int n = sz+sz+2;
char* p = expand(ptr, n);
delete[] ptr;
ptr = p;
space = n-sz-2;
else
--space;
}
ptr[sz] = c;
ptr[++sz] = 0;
return *this;
}
19.3.5 보조 함수
그 외 입출력, 연산자 오버로딩 등.. 수 많은 함수 ..
19.4 프렌드
통상적인 멤버 함수 선언은 논리적으로 구분되는 3가지 속성을 지정한다.
함수는 클래스 선언의 비공개 부분을 접근할 수 있어야 한다.
함수는 클래스의 유효 범위 내에 있어야 한다.
함수는 개체에 대해 호출될 수 있어야 한다. (this 포인터를 가진다)
즉 friend 로 선언된 함수는 멤버 함수와 똑같이 클래스의 구현에 접근이 혀용되지만, 그렇지 않을 경우에 해당 클래스와 무관하다.
friend 선언은 클래스 선언의 private 부분이나 public 부분 중 어느 쪽에도 놓일 수 있다. → 어느 곳에 선언되는 것은 중요하지 않다.
다음과 같이 함수에 friend 선언을 할수도 있고, 클래스에 friend 선언을 할수도 있다.
class List_iterator {
// ...
int* next();
};
class List {
friend int* List_iterator::next(); // 함수에 friend 선언
// ...
};
class List {
friend class List_interator; // 클래스에 friend 선언
// ...
};