◆static 클래스 멤버
- static 클래스 멤버는 그 클래스의 모든 객체가 공유한다.
- static 기억 공간에 저장된다.
- 클래스 선언 바깥에서 별개의 구문을 사용하여 독립적으로 초기화해주어야 한다.
- 초기화 선언은 메서드 구현 파일(cpp)에 넣는다.
- ※ static 멤버가 정수형이나 열거형의 const이면 클래스 선언 자체에서 초기화할 수 있다.
- enum { INT = 10 } 같은 상수 표현을 대체한다.
◆생성자에서 new를 사용하여 메모리를 대입했을 때는, 대응하는 파괴자에서 delete를 사용하여 그 메모리를 해제해야 한다.
◆프로그램에서 하나의 객체를 다른 객체로 초기화하면, 컴파일러는 복사 생성자라고 부르는 생성자를 자동으로 발생시킨다.
◆◆특별 멤버 함수
- ▷C++는 다음과 같은 멤버 함수를 자동으로 제공한다.
- 디폴트 생성자 : 생생자를 전혀 정의하지 않았을 경우
- 디폴트 파괴자 : 디폴트 파괴자를 정의한 지 않았을 경우
- 복사 생성자 : 복사 생성자를 정의하지 않았을 경우
- 대입 연산자 : 대입 연산자를 정의하지 않았을 경우
- 주소 연산자 : 주소 연산자를 정의하지 않았을 경우
◆디폴트 생성자
- 사용자가 어떤 생성자를 정의했다면, C++는 디폴트 생성자를 제공하지 않는다.
- 매개변수를 사용하는 생성자들도, 모든 매개변수들에 디폴트 값을 제공한다면, 디폴트 생성자가 될 수 있다.
●매개변수도 사용하지 않고 아무 일도 하지 않는 생성자(디폴트된 디폴트 생성자)
Cat::Cat() {} //암시적 디폴트 생성자
※ 단, 디폴트 된 디폴트 생성자와 모든 매개변수들에 디폴트를 제공한 디폴트 생성자가 두 개 존재해선 안된다.
(디폴트 생성자는 하나만 가질 수 있다.)
◆복사 생성자
- 어떤 객체를 새로 생성되는 객체에 복사하는 데 사용된다.
- 값 전달에 의한 함수 매개변수 전달을 포함한 초기화 작업에 사용된다.
- ※ 암시적 복사 생성자는 값으로 얕은 복사를 하는 것으로 힙 메모리를 복사할 경우 포인터를 복사해 간다.
- ※ 클래스가 new에 의해 초기화되는 포인터들을 멤버로 가지고 있을 경우에, 포인터 자체를 복사하는 것이 아니라, 그 포인터가 지시하는 데이터를 복사하는 복사 생성자를 정의해야 한다. 이것을 깊은 복사(deep copy)라 한다. 복사의 다른 한 가지 형태(멤버별 복사 또는 얕은 복사)는 포인터 값만을 복사한다. 얕은 복사는, 포인터가 지시하는 데이터들을 복사하기 위해 깊게(deeply) 파고들지 않고, 포인터 정보만 얕게(shallowly) 살짝 들어 복사한다.
●일반적인 복사 생성자
Class_name(const Class_name &);
●복사 생성자를 사용하는 종류
Cat BabyBlack(Black); //Cat(const Cat Black &)을 호출한다.
Cat BabyBlack = Black; //Cat(const Cat Black &)을 호출한다.
Cat BabyBlack = Cat(Black); //Cat(const Cat Black &)을 호출한다.
Cat * BabyBlack = new Cat(Black); //Cat(const Cat Black &)을 호출한다.
- ※ 객체를 값으로 전달하면 복사 생성자가 호출되기 때문에, 참조로 전달하는 것이 더 좋다.
●복사 생성자가 주로 사용되는 경우
1. 객체가 함수에 인수로 전달될 때
2. 함수가 객체를 반환값으로 반환할 때
3. 새로운 객체를 같은 클래스 타입의 기존 객체와 똑같이 초기화할 때
◆대입 연산자
- 오버로딩 대입 연산자는 하나의 객체를 기존의 다른 객체에 대입할 때 사용된다.
- 대입 연산자는 객체를 초기화할 때 반드시 사용되는 것은 아니다. (복사 생성자를 쓰는 경우, 혹은 대입)
●일반적인 대입 연산자
Class_name & Class_name::operator=(const Class_name &);
●C++는 이 구문을 두 단계로 처리한다.
Cat BabyBlack, Black;
BabyBlack = Black;
1. 복사 생성자를 사용하여 임시 객체를 먼저 생성한 후
2. 대입을 사용하여 그 값들을 그 새로운 객체에 복사한다.
- 즉 초기화는 복사 생성자를 항상 호출한다. 또한 = 연산자를 사용하는 형식들은 대입 연산자를 호출할 수도 있다.
◆대입에서 발생하는 문제의 해결책
깊은 복사를 하는 대입 연산자 정의를 사용자가 직접 제공하는 것이다.
복사 생성자와 비슷하지만, 다음과 같은 점에서 몇 가지 차이가 있다.
타깃 객체가, 이전에 대입된 데이터를 참조하고 있을 수도 있으므로, 그 함수는 delete []를 사용하여 이전의 의무를 해제해 주어야 한다.
그 함수는 어떤 객체를 자기 자신에게 대입하지 못하게 막아야 한다. 그것을 막지 않으면, 앞에서 설명한 메모리의 해제가, 내용을 다시 대입하기도 전에 그 객체의 내용을 먼저 지울 수 있다.
그 함수는 호출한 객체에 대한 참조를 리턴한다.
ex)
S1 = S2 = S3;
S1.operator=(S2.operator=(S3));
//대입 연산자
StringBad & StringBad::operator=(const StringBad & st)
{
if(this == &st)
return *this;
delete [] str; //옛 문자열을 해제한다.
len = st.len;
str = new char[len +1]; //새로운 문자열을 위한 공간 확보
str::strcpy(str,st.str); //문자열을 복사한다.
return *this; //호출한 객체에 대한 참조를 리턴
}
- ※ 복사 생성자에서 생성된 값을 대입 연산자로 채우는 것으로, 대입 연산자는 새로운 객체를 만들지 않는다.
◆개선된 디폴트 생성자
String::String()
{
len =0;
str = new char[1];
str[0] = '\0';
}
●str = new char[1] 로 초기화해준 이유
▷파괴자의 경우 delete [] str인데 디폴트 생성자가 str = new char라면 []가 없어 호환이 어렵기 때문이다.
●C++ Null Pointer
▷nullptr 이라는 키워드를 제공하여 null pointer를 표시한다. 혹은 0을 사용할 수 있다.
◆[] 표기를 사용하여 개별 문자에 접근하기
●[] 기호는 하나의 연산자이다.
▷operator[]() 라는 메서드를 사용하여 이 연산자를 오버로딩할 수 있다.
▷첫 번째 피연산자[두 번째 피연산자]
String opera("The Magic Flute");
//진행 과정
opera[4]
opera.operator[](4) //String::operator[](int i)
char & String::operator[](int i)
{
return str[i];
}
●const String 객체의 경우 인덱싱 호출이 데이터를 변경하지 않는다는 약속을 하지 않기 때문에, operator[]()의 제2의 버전을 제공할 수 있다.
//const String 객체를 사용하기 위해
const char & String::operator[](int i) const
{
return str(i);
}
◆static 클래스 멤버 함수
▷멤버 함수를 static으로 선언하는 것이 가능하다.
※함수 정의와 함수 선언이 분리되어 있다면, 키워드 static은 함수 정의가 아니라 함수 선언에 나타나야 한다.
1. static 멤버 함수는 객체에 의해 호출될 필요가 없다.
2. static 멤버 함수는 어떤 특정 객체와도 결합하지 않기 때문에, 사용할 수 있는 데이터 멤버는 static 데이터 멤버밖에 없다.
▷static 멤버 함수를 사용하여, 클래스 인터페이스의 몇 가지 상황을 어떻게 처리할 것인지 제어하는, 클래스 사용 범위의 플래그를 설정할 수 있다.
get(char *, int의 오래된 버전들은 빈 줄을 읽었을 때 false로 평가하지 않는다. 그러한 버전들의 경우에, 빈 줄이 입력 되면 문자열의 첫 문자는 널 문자이다. 이 예제는 다음과 같은 코드를 사용하고 있다.
if(!cin || temp[0] =='\0') //입력이 실패했거나 입력이면
break;
◆◆생성자에 new를 사용할 때 주의할 사항
●생성자에서 new를 사용하여 포인터 멤버를 초기화한다면, 파괴자에 반드시 delete를 사용해야 한다.
●new와 delete의 사용은 서로 어울려야 한다. new는 delete와 짝을 이루고, new []는 delete []와 짝을 이루어야 한다.
●생성자가 여러 개일 경우에는, 모두 대괄호를 사용하든지 아니면 모두 대괄호 없이 사용하든지, 모든 생성자가 같은 방법으로 new를 사용해야 한다. 파괴자는 하나밖에 없으므로, 모든 생성자가 그 파괴자와 어울려야 한다. 그러나 하나의 생성자에서 new를 사용하여 포인터를 초기화하고, 다른 생성자에서 널 포인터로 초기화하는 것은 허용이 된다. 그 이유는 (대괄호고 있건 없건) delete 연산자를 널 포인터에 적용할 수 있기 때문이다.
●깊은 복사를 통해 하나의 객체를 다른 객체로 초기화하는, 복사 생성자를 정의해야 한다. 일반적으로 그러한 복사 생성자는 다음과 같아야 한다.
String::String(const String & st)
{
num_strings++;
len = st.len;
str = new char [len+1];
std::strcpy(str,st.str);
}
▷특히, 복사 생성자는 복사되는 데이터를 보관할 기억 공간을 대입해야 한다. 복사 생성자는 데이터의 주소가 아니라 데이터 그 자체를 복사해야 한다. 또한 그 과정에서 값이 영향을 받을 수 있는, 모든 static 클래스 멤버들을 갱신해야 한다.
●깊은 복사를 통해 하나의 객체를 다른 객체에 대입하는, 대입 연산자를 정의해야 한다. 일반적으로 그 클래스 메서드는 다음과 같아야 한다.
String & String::operator=(const String & st)
{
if(this == &st)
return this;
delete [] str;
len = st.len;
str = new char[len+1];
std::strcpy(str, st.str);
return *this;
}
▷특히, 그 메서드는 자기 자신에 대입하는지 검사해야 한다. 그것은 멤버 포인터가 이전에 지시하던 메모리를 해제해야 한다. 데이터의 주소가 아니라 데이터 자체를 복사해야 한다. 그리고 호출한 객체에 대한 참조를 리턴해야 한다.
◆사용하지 말아야 할 것과 사용해도 좋은 것
▷다음의 코드의 단편은, 사용하지 말아야 할 두 가지 예와, 한 가지 좋은 생성자의 예를 보여 준다.
●1.
String::String()
{
str = "디폴트 문자열";
len = std::strlen(str);
}
▷파괴자가 디폴트 객체에게 대해 호출되었을 때, str에 delete를 적용한다. new로 초기화되지 않은 포인터에 delete를 적용했을 때, 결과를 알 수 없지만 아마도 나쁠 것이다.
◎다음의 생성자들은 문제가 없을 것이다.
String::String()
{
len = 0;
str = new char [1];
str[0] = '\0';
}
String::String()
{
len = 0;
str = 0; //또는, C++11. str = nullptr
}
String::String()
{
static const char * s = "C++"; //한 번만 초기화된다.
len = std::strlen(s);
str = new char [len + 1];
std::strcpy(str,s);
}
●2.
String::String(const char * s)
{
len = std::strlen(s);
str = new char; //[]가 없다.
std::strcpy(str, s) //공간이 확보되지 않았다.
}
▷new를 적용하지만 정확한 크기의 메모리를 요청하지 못한다.
▷또한 대괄호 없이 new를 사용하는 것은, 올바른 다른 생성자들과 일관성이 없다.
●3.
String::String(const String &st)
{
len = st.len;
str = new char[len + 1];
std::strcpy(str,st.str);
}
▷문제가 없다.
●다음은 앞의 생성자들과 어울리지 못하는 파괴자의 예이다.
String::~String()
{
delete str; //안된다, delete [] str 이어야 한다.
}
▷생성자들이 문자들의 배열을 요청하고 있으므로, 파괴자는 배열을 삭제할 수 있어야 한다.
◆◆객체 리턴에 대한 관찰
●멤버 함수 또는 독립함수가 객체를 리턴할 때, 사용자는 몇 가지 중에서 선택할 수 있다.
▷객체에 대한 참조, 객체에 대한 const 참조, 객체, const 객체를 리턴할 수 있다.
◆const 객체에 대한 참조 리턴
●const 참조를 사용하는 주된 이유는 효율성이다.
▷객체를 리턴하는 것은 복사 생성자를 호출하지만, 참조를 리턴하는 것은 그렇지 않다.
▷참조는 호출하는 함수가 실행중일 때 존재하는 객체에 대한 참조여야 한다.
▷매개변수가 const 참조라면 리턴형도 const가 되어야 한다.
◆const가 아닌 객체에 대한 참조 리턴
●cout과 함께 사용하기 위한 대입 연산자의 오버로딩과 << 연산자의 오버로딩이, const가 아닌 객체를 리턴하는 두 일반적인 예이다.
●operator=()의 리턴값은 연쇄적인 대입에 사용된다.
String s1("Good stuff");
String s2, s3;
s3 = s2 = s1;
▷참조를 사용하면, 그 함수가 String 복사 생성자를 호출하여 새로운 String 객체를 만드는 일을 피할 수 있다.
●operator<<() 의 리턴값은 연쇄적인 출력에 사용된다.
String s1("Good stuff");
cout << s1 << "is coming!";
▷여기서, operator<<(cout,s1)의 리턴값은 문자열 "is coming!"을 출력하는 데 사용되는 객체가 된다.
▷여기서 리턴형은 ostrream이 아니라 ostream &이 되어야 한다.
▷ostream 리턴형을 사용하면, ostream 복사 생성자의 호출을 요구할 것이다.
▷ostream 클래스는 public 복사 생성자를 만들지 않는다.
◆객체 리턴
●리턴되는 객체가 피호출 함수에 지역적이면, 함수가 종료될 때 그 지역적인 객체가 파괴자를 호출하기 때문에, 참조로 리턴하면 안된다.
●이러한 경우에는 참조가 아니라 객체를 리턴해야 한다. 일반적으로, 오버로딩 산술 연산자들이 이 범주에 속한다.
◆const 객체 리턴
●앞에서 설명 한 객체 리턴에 대입연산자를 사용해 rvalue를 lvalue로 쓰려는 시도가 일어날 수 있다.
//operator+() 반환 값에 vector(100)을 대입
vector(1) + vector(2) = vector(100)
●하지만 객체 리턴은 사용 후 파괴되기 때문에 문제가 된다.
●이를 해결하기 위해 리턴형에 const 객체로 선언하는 것이다.
◆◆객체를 지시하는 포인터
●new에 의한 객체 초기화
일반적으로, Class_name이 클래스 이름이고 value가 Type_name형의 값이면, 다음과 같은 구문은
Class_name * pclass = new Class_name(value);
다음과 같은 생성자를 호출한다.
Class_name(Type_name);
다음과 같은 사소한 변환도 이루어질 수 있다.
Class_name(const Type_name &);
또한, int를 double로 변환하는 것과 같은, 원형 일치에 의해 호출되는 변환은 모호성이 없는 한 이루어질 것이다. 다음과 같은 형식의 초기화는 디폴트 생성자를 호출한다.
Class_name * ptr = new Class_name;
◆new와 delete
◆파괴자들이 호출될 때
●객체가 자동 변수이면 그 객체의 파괴자는, 프로그램이 그 객체가 정의된 블록을 벗어날 때 호출된다.
●객체가 static 변수이면, 그 객체의 파괴자는 프로그램이 종료될 때 호출된다.
●new에 의해 생성된 객체라면, 그 객체의 파괴자는 그 객체에 대해 명시적으로 delete를 사용할 때 호출된다.
◆◆테크닉의 복습
◆<<연산자의 오버로딩
- cout과 함께 사용하여 객체의 내용을 출력할 수 있도록 Much Less-Than(<<) 연산자 함수를 정의한다.
ostream & operator<<(ostream & os, const c_name & obj)
{
os<<...;
return os;
}
◆변환 함수들
- 어떤 하나의 값을 클래스형으로 변환하려면, 다음과 같은 원형을 가지는 클래스 생성자를 작성한다.
c_name(type_name value);
- 클래스형을 다른 데이터형으로 변환하려면 클래스 멤버 함수를 작성한다.
operator type_name();
▷암시적 변환이 이루어지지 않게 하려면, 생성자를 선언할 때 키워드 explicit를 사용해야 한다.
◆생성자가 new를 사용하는 클래스
클래스 멤버에 의해 지시되는 메모리를 대입하기 위해 new 연산자를 사용하는 클래스를 설계할 때, 몇 가지 점에서 주의할 필요가 있다.
- new에 의해 대입된 메모리를 지시하는 클래스 멤버는, 클래스 파괴자에서 delete 연산자를 그것에 적용해야 한다.
- 파괴자가 클래스 멤버인 어떤 포인터에 delete를 적용하여 메모리를 해제한다면, 그 클래스의 모든 생성자들은 new를 사용하거나, 그 포인터를 널 포인터로 설정함으로써, 그 포인터를 초기화해야 한다.
- 생성자들은 new [] 또는 new를 사용할 수 있으나 둘을 섞어서 사용할 수 없다. 생성자가 new[] 를 사용한다면, 파괴자도 delete []를 사용해야 한다. 생성자가 new를 사용한다면, 파괴자도 delete를 사용해야 한다.
- 기존의 메모리를 지시하는 포인터를 복사하지 않고, 새로운 하나의 객체를 다른 객체로 초기화할 수 있다. 복사 생성자는 다음과 같은 형식의 원형을 가져야 한다.
className(const className &);
- 다음과 같은 원형으로 함수 정의를 가지며, 대입 연산자를 오버로딩하는 클래스 멤버 함수를 정의해야 한다.(여기서 c_pointer는 c_name의 멤버이고 type_name을 지시하는 포인터형이다.) 다음의 예는 생성자가 new []를 사용하여 c_pointer 변수를 초기화한다고 가정한다.
c_name & c_name::operator=(const c_name & cn)
{
if(this == & cn)
return *this;
delete [] c_pointer;
//type_name형 단위로 size개만큼 복사하도록 설정
c_pointer = new type_name[size];
//그 다음에 cn.c_pointer가 지시하는 데이터를
//c_pointer가 지시하는 위치에 복사한다.
...
return *this;
}
◆멤버 초기자 리스트 문법
- ClassY가 클래스이고 mem1, mem2, mem3가 클래스 데이터 멤버라면, 클래스 생성자는 다음과 같은 문법을 사용하여 그 데이터 멤버들을 초기화시킬 수 있다.
ClassY::ClassY(int n, int m) : meml(n), mem2(0), mem3(n*m + 2)
{
...
}
개념적으로 이들 초기화는 중괄호 안의 코드가 실행되기 전, 객체가 생성될 때 이루어진다, 다음과 같은 사항에 주의해야 한다.
- 이 형식은 생성자에만 사용할 수 있다.
- static이 아닌 const 데이터 멤버를 초기화하려면 이 형식을 사용해야 한다.
- 참조 데이터 멤버를 초기화하려면 이 형식을 사용해야 한다.
데이터 멤버들은, 멤버 초기자 리스트에 나열된 순서가 아니라, 클래스 선언에 선언된 순서대로 초기화된다.
덧붙여, 멤버 초기자 리스트에 사용되는 괄호 형식은 일반적인 초기화에서도 사용할 수 있다.
'C++ > C++' 카테고리의 다른 글
[C++] Rule of three (0) | 2023.08.27 |
---|---|
Overloading (0) | 2023.07.28 |
11. 클래스의 활용 (0) | 2023.07.19 |
10. 객체와 클래스 (0) | 2023.07.17 |
9. 메모리 모델과 이름 공간 (0) | 2023.07.14 |