객체지향프로그래밍의 시작
- 객체란, 변수들과 참고 자료들로 이루어진 소프트웨어 덩어리 이다.
Encapsulation
- 외부에서 직접 인스턴스 변수의 값을 바꿀 수 없고 항상 인스턴스 메소드를 통해서 간접적으로 조절하는 것
- “객체가 내부적으 로 어떻게 작동하는지 몰라도 사용할 줄 알게 된다”
클래스를 이용해 만들어진 객체(= Instance)의 구성요소
- Instance variable (인스턴스변수)
- Instance method (인스턴스메소드)
클래스?
- 객체의 설계도
- 클래스의 구성요소
- Member variable (멤버변수)
- Member function (멤버함수)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Animal {
private:
int food;
int weight;
public:
void set_animal(int _food, int _weight){
food = _food;
weight = _weight;
}
...
}; //세미콜론 꼭 붙이기!!
int main(){
Animal animal;
animal.set_animal(100,50);
...
return 0;
}
클래스의 접근 지시자
- Encapsulation 구현하는데 사용
-
private
: 클래스 외부에서 접근이 불가능한 멤버 함수와 변수를 정의- 키워드를 명시하지 않았으면 자동으로
private
으로 설정
- 키워드를 명시하지 않았으면 자동으로
-
public
: 클래스 외부에서도 접근이 가능한 멤버 함수와 변수를 정의 -
protected
:private
과 유사하지만, 파생 클래스에서는 접근이 가능
생성자와 함수의 오버로딩
함수의 오버로딩
- 함수 이름이 같더라도, 인자가 다르면 다른 함수로 판단!
- 따라서 인자를 달리 하는 여러 함수를 하나의 이름으로 호출 할 수 있음
- 각각의 (인자가 다르고 함수 이름은 같은) 함수를 각각 선언해야함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void print(int x) {
std::cout << "int : " << x << std::endl;
}
void print(double x) {
std::cout << "double : " << x << std::endl;
}
int main() {
int a = 1;
char b = 'c';
double c = 3.2f;
print(a); // void print(int x) 호출됨
print(b); // char 타입이 int로 casting 되서
// void print(int x) 호출됨
print(c); // void print(double x) 호출됨
return 0;
컴파일러가 함수를 오버로딩하는 과정
- (1단계) 타입이 정확히 일치하는 함수 찾기
- (2단계) 타입이 딱 안맞으면, casting (타입 변환)해서 함수 일치되는지 보기
- (3단계) 또 안되면, 좀 더 포괄적인 타입 변환을 시도 (float $\rightarrow$ int, 포인터 $\rightarrow$ void 포인터)
- (4단계) 유저 정의된 타입변환으로 일치하는 것 찾기 (?)
- 그래도 안되면 “ambiguous call” 오류!
클래스의 멤버함수 안쪽에/바깥쪽에 정의하는 법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Date {
private: // 안 적어도 private 됨.
int year_; // private 변수
public:
// 안에서 함수 정의할거면 세미콜론 하지마
void AddDay(int ...){
...
}
// 밖에서 함수 정의할거면 선언만 하고 세미콜론 박아
void SetDate(int ...);
}; // 클래스 끝에는 꼭 세미콜론 붙이기
// 클래스 안에서 선언만 된 함수는 밖에서
// {클래스이름}::{함수이름}(인자){ ... } 로 함수 정의해야 함
void Date::Setdata(int ...){
...
}
생성자 (Constructor) 만들기
- 생성자는 기본적으로 “객체 생성시 자동으로 호출되는 함수”
- 객체를 초기화만 하면 되니깐 리턴값 없다!!
- 리턴값이 없으니깐, 함수 이름 앞에 붙이는 타입도 적지 마!!
- 클래스 안에 정의할 때:
{클래스이름}({인자}){ 초기화 내용 }
- 명시적 디폴트 생성자 정의 (안해도 됨)
{클래스이름}() = default;
- 클래스 밖에 정의 할 때:
- 안에서 선언:
{클래스이름}({인자});
- 밖에서 정의:
{클래스이름}::{클래스이름}({인자}){ 초기화 내용 }
- 안에서 선언:
생성자 써서 객체 만들기
- 객체 생성하는 방법
-
Date day(2011, 3, 1);
- 객체 이름 뒤의 인자들을, Date 클래스 생성자에 implicitly 전달
- 암시적 방법 (implicit)
- 편해서 더 자주 씀
- 하지만, 암시적 표현 문법이 함수 선언이랑 헷갈릴 수 있으니 조심!
-
Date day = Date(2011, 3, 1);
- 명시적인 객체 생성 문법
- “객체 day는 Date 클래스 생성자에 2011, 3, 1 인자들을 전달해서 초기화한 것”
- 명시적 방법 (explicit)
-
new
를 써서 heap에 저장하고 싶으면 명시적 생성 문법 이용Date day = new Date(2011, 3, 1);
- 꼭 delete 해줘야 함
디폴트 생성자 (Default constructor)
- 인자가 없는 생성자
- eg.
Date::Data(){ ... }
- 생성자를 전혀 정의하지 않으면 아무것도 하지 않는 디폴트 생성자 호출됨.
- 컴파일러가 마음대로 (아무것도 안하는 방식으로) 초기화해버림
- Implicitly 디폴트 생성자 호출:
Date day;
-
Date day()
는 리턴값이 Date이고 인자가 없는 함수 day를 정의한 것이 되니깐 조심!
-
- Explicitly 디폴트 생성자 호출:
Date day = Date();
- 명시적 디폴트 생성자 정의 (안해도 됨)
{클래스이름}() = default;
생성자 오버로딩
- 생성자 역시 함수 이기 때문에 마찬가지로 함수의 오버로딩 가능!
1
2
3
4
5
6
7
8
class Date{
private:
...
public:
Date(){ ... }
Date(int year, int month, int day){ ... }
...
};
복사 생성자와 소멸자
같은 클래스의 객체 많이 만들어서 관리 해야 할 때?
- 스타크래프트에서 마린이 정말 많아질 경우, 객체의 배열을 만들자
- 포인터로 이뤄진 배열을 stack에 만들고, 각 포인터 원소가 heap 에 저장한 객체를 가리키자
Marine* marines[100]; marine[0] = new Marine(2,3);
- 각 객체별로 heap에서 지워줘야 해
delete marine[0];
1
2
3
4
5
6
7
8
9
10
// Marine 클래스의 생성자는
// Marine::Marrine(int x, int y){ ... } 형태
// Marine 객체를 담는 배열 선언하고 원소를 채워넣자
// 여기선 포인터 배열은 스택에, 원소가 가리키는 객체는 힙에 저장했음
Marine* marines[100];
marine[0] = new Marine(2,3); // heap에 객체들 하나씩 생성
marine[1] = new Marine(3,5); // new 다음에 명시적 생성자 호출
delete marine[0]; // heap에 있는 객체들 하나씩 소멸
delete marine[1];
소멸자 (Destructor)
- 멤버 변수가 heap에 있는 데이터를 가리키는 포인터 일 때?
- 멤버 변수 :
char* name;
- 생성자에서 초기화할 때 heap에 문자열 저장 :
name = new char[strlen(marine_name)+1];
- 멤버 변수 :
- 소멸자에서 delete 해줘야 함!! 안그러면 memory leak 발생!
- name이 배열을 가리키는 포인터니깐,
delete[]
해줘야 컴파일러가 배열인줄 알고 잘 지워줌 ~Marine(){ delete[] name; }
- name이 배열을 가리키는 포인터니깐,
- 디폴트 소멸자가 있긴한데, 아무일도 안 함.
- 소멸자 역할 #1 : 객체가 동적으로 할당받은 메모리를 해제
- 소멸자 역할 #2 : 쓰레드 사이에서 lock 된 것을 푸는 역할
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Marine {
private:
...
public:
...
~Marine();
};
Marine::Marine(int x, int y, const char* marine_name){
name = new char[strlen(marine_name)+1];
strcpy(name, marine_name);
...
}
Marine::~Marine(){
if (name != NULL) {
delete[] name;
}
}
복사 생성자 (Copy constructor)
- 스타크래프트에서 포토캐논 겹치기가 하고 싶어!!
- 즉, 모든 인스턴스 변수가 같은 객체를 많이 만들고 싶을 때!
- 복사하려는 객체를 상수 레퍼런스로 받아야 해.
- 클래스 안에서
Photon_Cannon::Photon_Cannon(const Photon_Cannon& pc);
로 선언
- 클래스 안에서
- 사용할 때는 ..
-
Photon_Cannon pc1(3,3);
으로 기본 객체 만들고, -
Photon_Cannon pc2(pc1);
으로 객체를 인자로 받아서 복사! -
Photon_Cannon pc3 = pc2
$\rightarrow$ 컴파일러가operator=
오버라이드 해줘서 복사 생성사 호출!-
pc3 = pc2
는 그냥 대입하는거니깐 복사생성자 안 부름
-
-
- 디폴트 복사 생성자?
- 사실, 복사 생성자 따로 안 만들어도 (!)
- 인스턴스 변수를 복사해주는 기능을 자동으로 해줌.
- 한계:
- 그대로 복사해버리니깐,
- Heap에 있는 데이터를 가르키는 포인터인 멤버변수도 그대로 복사해버려서 = 얕은 복사 = Shallow copy
- 두 개의 다른 객체가 heap에 있는 하나의 데이터를 가르킴
- 그러다가 한 객체가 소멸되면, 그 데이터가 delete되면서
- Dangling pointer 문제 발생 !!
- 그래서 함수 scope을 벗어날 때 (eg. main 함수) 둘 중 하나가 먼저 소멸 되면서, 남은 객체를 소멸할 때, 소멸 대상이 되는 데이터가 없어서 run time 오류 발생 함.
- So, 복사 생성자 만들 때는 꼭 멤버변수 포인터가 가리키는 heap의 데이터 복사해주는 깊은 복사 (Deep copy)를 해주자!
- 복사 생성자 안에서,
name = new char[strlen(pc.name)+1]; strcpy(name, pc.name);
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
class Photon_Cannon {
int coord_x, coord_y;
char* name;
...
public:
Photon_Cannon(int x, int y, const char* input_name);
Photon_Cannon(const Photon_Cannon& pc);
...
};
// 기본 생성자
Photon_Cannon::Photon_Cannon(int x, int y, const char* input_name){
name = new char[strlen(input_name)+1];
strcpy(name, input_name);
...
}
// 복사 생성자
Photon_Cannon::Photon_Cannon(const Photon_Cannon& pc){
//일반 변수는 그냥 복사
{멤버변수} = pc.{멤버변수};
//포인터 변수는 deep copy
name = new char[strlen(pc.name)+1];
strcpy(name, pc.name);
...
}
Const, const, const!
생성자의 초기화 리스트 (Initializer list)
- 생성자 정의할 때,
{클래스이름}::{클래스이름}(인자) : {인자1 이름}({인자1 값}), {인자2 이름}({인자2 값}) {생성자 본문}
- 즉… 원래 생성자의 이름부분과 함수내용 부분 사이에
- 콜론 + {인자1 이름}({인자1 값}) + 콤마 + {인자2 이름}({인자2 값}) 형태의
- Initializer list를 삽입하면,
- 생성자의 멤버변수를 조금 더 편하게 초기화 할 수 있음.
1
2
Marine::Marine() : hp(50), coord_x(0), coord_y(0), damage(5), is_dead(false) {}
Marine::Marine(int x, int y) : coord_x(x), coord_y(y), hp(50), damage(5), is_dead(false) {}
굳이 초기화 리스트 써야하는 이유?
- 생성과 초기화를 동시에 함!!
int a = 0;
- 멤버변수가 클래스라면, 복사생성자를 호출!
- 보통 생성자는 생성을 하고, 초기화를 함!!
int a; a = 0;
- 멤버변수가 클래스라면, 디폴트 생성자를 호출한 다음에 대입!
- 따라서, 생성을 하면서 초기화해야하는 경우에 Initializer를 써야 함!!
- 상수 선언
-
const int a = 10;
처럼 처음에 상수값 정해야 함 - 따라서, 예외적으로 클래스의 멤버변수로서의 상수는 상수값없이 선언할 수 있음
-
private: const int a;
가능. - Q: 원래 에러나는거 아님?
- A: 객체만들 때, 당연히 그리고 무조건 Initializer list로 상수값을 정하면서 상수가 만들어질테니깐!!
-
-
- 레퍼런스 선언
-
int& ref = a;
처럼 처음에 참조하려는 변수 정해야 함
-
- 복사 생성자로 객체를 만들고 싶을 때
- 상수 선언
Static 멤버 변수
- 특정 클래스를 찍어낸 객체의 갯수를 count 하고 싶을 때
- 사실 std::vector 쓰면 되긴 하지만… ㅋ
- 하나의 객체에 속한 것이 아닌,
- 같은 클래스로 만든 모든 객체가 공유하는 딱 하나의 변수인 static 변수 (eg. counter)를 써서 기록하자
- eg.
static int total_marine_number;
1
2
3
4
5
6
7
8
9
10
11
class Marine {
static int total_marine_number;
private:
int hp;
public:
Marine();
~Marine();
};
Marine::Marine() : hp(50) {total_marine_number++;} //생성할 때 마린숫자 더하기
Marine::~Marine() {total_marine_number--;} // 생성할 때 마린숫자 줄이기
Static 멤버 함수
- 같은 클래스로 만든 모든 객체가 공유하는 딱 하나의 함수인 static 함수
- 객체가 하나도 생성되지 않아도 쓸 수 있다!
- 함수 선언/정의 할 때
static {함수이름}({인자})
처럼 static을 붙여주면 됨 - 사용할 때,
- {객체}.{멤버함수} 형태가 아니고,
- {클래스}::{static멤버함수} 형태!! 콜론 두개 !!
1
2
3
4
5
6
7
8
9
10
11
12
13
class Marine {
static int total_marine_number;
...
static void show_total_marine(){
std::cout << "전체 마린 수 : " << total_marine_num << std::endl;
}
};
int main(){
Marine marine1(2, 3, 5); // 마린 객체 하나 만듦
Marine::show_total_marine(); // 마린 객체에 의존하지 않고, static 함수를 호출할 수 있다!!!
...
}
this
- 객체 안에서 자기 자신을 가리키는 포인터
- 포인터의 값, 즉 객체 자신은
*this
로 표현 - 객체 자신의 멤버 변수/함수를 가리킬 경우,
this->{멤버변수/함수이름}
- 클래스 내에서 그냥
{멤버변수/함수이름}
을 쓰는것 과 같은 효과
- Static 함수는 this로 접근 $\times$
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
// Marine 클래스 안의 be_attacked 멤버함수
// 객체 자신을 상태변화 시킨다음에 레퍼런스로 리턴!
Marine& Marine::be_attacked(int damage_earn) {
hp -= damage_earn;
if (hp <= 0) is_dead = true;
return *this;
}
Marine marine1(2, 3, 5);
Marine marine2(3, 10, 10);
// marine2가 marine1한테 두 번 연속 공격받았을 때,
// ".be_attacked(marine1.attack())"으로
// 한 번 be_attacked 멤버함수를 호출한 결과가 객체 자신의 레퍼런스기 때문에,
// ".be_attacked(marine1.attack())"을
// 한 번 더 적용해서 두번의 연속 공격을 표현할 수 있다.
marine2.be_attacked(marine1.attack()).be_attacked(marin1.attack());
// Qustion:
// Marine::be_attacked 함수가 레퍼런스 리턴이 아니라,
// 그냥 객체 값 리턴하는 함수라면,
// 두 번 공격하려고 적은 위 문장은 어떤 결과를 갖는가?
// Answer:
// marine2는 한 번 공격당한 상태로만 변하고,
// marine2.be_attacked(...)에 의해 생성된
// 임시객체(R-value)인 마린객체를 한 번 더 공격하는 셈이 되어서,
// 두번쨰 공격이 무의미해진다.
레퍼런스를 리턴하는 함수?
- 함수 안에 존재하는 변수/객체의 레퍼런스를 반환
- 따라서 레퍼런스 반환하는 변수/객체가 소멸되지 않는 한,
- 주소가 존재하는 L-value
- 따라서, 외부에서 정의한 레퍼런스에 담을 수 있다.
- 반대로, 값을 리턴하는 함수의 값은
- 값을 복사해서 리턴하기 때문에, 함수 내에서 리턴한 변수/객체에 접근할 수 없고,
- 함수를 쓰는 문장이 끝나면 사라지는 임시객체 (R-value) 이므로, 외부에서 정의한 레퍼런스에 담을 수도 없다
- C++ 의 참조자 참고요~
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
// 레퍼런스를 리턴하는 함수
#include <iostream>
class A {
int x;
public:
A(int c) : x(c) {}
// 레퍼런스를 리턴하는 함수? -> 객체 내부의 변수 x의 레퍼런스를 반환
// 이 때, 함수의 값은 a.x의 레퍼런스는 주소가 존재하는 L-value
int& access_x() { return x; }
// 값을 리턴? -> 객체 내부의 변수인 x의 값을 복사해서 반환
// 이 때, 함수의 값은 임시객체인 R-value
// 임시 객체는 문장이 끝나면 소멸 됨
int get_x() { return x; }
void show_x() { std::cout << x << std::endl; }
};
int main(){
A a(5); //A클래스 객체a를 암시적으로 생성,
a.show_x() // Print "5"
int& c = a.access_x();
// a.x의 레퍼런스를 레퍼런스 c에 전달
// 따라서 c는 a.x의 레퍼런스
c = 4;
a.show_x() // Print "4"
int d = a.access_x();
// a.x의 레퍼런스를 보통 변수 d에 복사 전달
// 따라서 d는 a.x 값이 복사된 별개의 변수
d = 3;
a.show_x() // Print "4"
int& e = a.get_x();
// a.x의 값을 레퍼런스 타입 변수 c에 전달??
// 임시객체인 R-value를 레퍼런스하는건 불가능! 컴파일 오류남!
int f = a.get_x();
// a.x의 값을 보통 변수 d에 복사 전달
// 따라서 f는 a.x 값이 복사된 별개의 변수
// Note: 값이 두번 복사됨!!
f = 1;
a.show_x() // Print "4"
}
Const 함수
- 변수들의 값을 바꾸지 않고 읽기만 하는 함수
- 함수 선언 뒤에
const
넣으면 됨 - 클래스 내부에서 선언
int attack() const;
- 클래스 밖에서 정의
int Marine::attack() const { return x; }
내가 만들어보는 문자열 클래스
-
MyString
문자열 클래스 만들기! - 지원할 내용
- 문자(char) 로 부터의 문자열 생성, C 문자열 (char *) 로 부터의 생성
- 문자열 길이를 구하는 함수
- 문자열 뒤에 다른 문자열 붙이기
- 문자열 내에 포함되어 있는 문자열 구하기
- 문자열이 같은지 비교
- 문자열 크기 비교 (사전 순)
- C++ 에서는 정말 왠만하면 char 배열을 사용하는 것보다 string 을 사용해서 문자열을 다루는 것을 권장!!
MyString 멤버변수
- 문자열 데이터의 저장공간을 가리키는 포인터
char* string_content;
- 데이터 자체를 멤버변수로 만들지 않는 이유?
- 문자열 데이터의 크기가 바뀔 때,
- 데이터가 할당된 메모리를 해제했다가 새로 할당해야하는데,
- 런타임 중간에 멤버변수를 지웠다가 새로 쓰고 할 수가 없음!
- 그니깐 포인터를 멤버변수로 하고, 동적할당되는 데이터를 가리키게 하자.
- 포인터가 가리킬 공간 할당
string_conent = new char[string_length];
- 데이터 자체를 멤버변수로 만들지 않는 이유?
- 문자열 데이터의 길이
int string_length;
- 문자열 데이터 공간의 크기
int memory_capacity;
MyString 생성자/소멸자
- 문자 하나로 생성 : 인자 =
char c
- 문자열로부터 생성 : 인자 =
const char* str
- 복사생성자 : 인자 =
const MyString& str
-
MyString
클래스에는 문자열 끝에NULL
안 넣고, 필요한 정보만 저장하기로. 문자열 끝은string_length
로 판별. - 소멸자?
- Heap 에 할당된 문자열 데이터 지우자
delete[] string_content;
-
각종 함수
- 길이 구하는
length
함수- 읽기만 하는 함수이므로 const 함수로 만들자.
int MyString::length() const { return string_length; }
- 출력하는
print
함수- 리턴할 필요가 없는 함수니깐,
return
생략! - 함수형은
void
- 출력을 마치는 순가은
string_length
만큼for-loop
돌려서 판단 void MyString::print(){ ... }
- 상수함수!
- 리턴할 필요가 없는 함수니깐,
- 줄바꿔 출력해주는
println
함수-
std::cout << std::endl;
넣고 안 넣고 차이 - 상수함수!
-
-
assign
함수-
str.assign("abc")
해서 기존 문자열 지우고, 새 문자열을 대입시키는 함수 - 인자는 char배열 형태일 수도 있고, MyString 객체일 수도 있음
- *this의 레퍼런스를 리턴하면,
str.assign("abc")
자체가 새로운str
이 되서, 계속 dot-operator (“.”) 로 멤버변수 호출 가능 - 기존 문자열의 memory_capacity와 새 문자열 길이 (혹은 memory capacity) 비교해서, 필요하면 공간 지우고 새로 할당.
-
1
2
MyString& assign(const MyString& str);
MyString& assign(const char* str);
-
reserve
함수- 문자열 크기를 미리 예약해 놓는 함수
-
capacity
함수- 문자열의 크기를 알려주는 함수
- 상수함수!
-
at
함수- Index 써서 random access!
- 범위 컨디션 체크
- 상수함수!
-
insert
함수- 인자 = 삽입할 위치, 문자열 (MyString 객체, or char배열, or 문자하나)
- 따라서, MyString 객체, char배열, 문자하나 별로 함수 하나씩 만들어서 오버로드!!
- MyString 객체 버전
insert
함수 하나 만든 다음에 재활용해서 오버로드된insert
함수들 만들 수 있음
- 삽입된 객체를 레퍼런스로 리턴하면, dot-operator 연달아 쓸 수 있음
- 함수 내용?
- 삽입 위치의 범위 맞는지 확인
- 메모리 공간이 확보되어있는지 확인.
- 부족하면 새로 생성
- 기존 공간
delete
- 잦은 insert 에 대비해서, 메모리를 미리 할당할 경우 현재 메모리 크기의 두 배 정도를 할당하자
- 삽입할 위치를 기준으로 문장을 잘라서, 문장 위치를 미뤄버리거나/복사하기
-
*this
레퍼런스 리턴!
- 인자 = 삽입할 위치, 문자열 (MyString 객체, or char배열, or 문자하나)
1
2
3
MyString& MyString::insert(int loc, const MyString& str);
MyString& MyString::insert(int loc, const char* str);
MyString& MyString::insert(int loc, char c);
-
erase
함수-
insert
함수보다 쉬운데, 메모리 크기가 늘어날 일이 없기 때문!
-
-
find
함수- 인자 = 찾기 시작할 위치, 찾는 문자열 (MyString 객체, or char배열, or 문자하나)
- 상수함수!
- 찾았으면 문장의 위치를 리턴, 못 찾았으면 -1 리턴
-
compare
함수- 문자열간의 크기 비교하는 함수 (
*this
vsstr
)- i.e. 사전식으로 배열해서 어떤 문자열이 더 뒤에 오는지 판단
- “abb” > “acb”
- “abc” > “abcd”
-
(*this) - (str)
을 수행해서 그 1, 0, -1 로 그 결과를 리턴한다- 1 은 (*this) 가 사전식으로 더 뒤에 온다는 의미
- 0 은 두 문자열이 같다는 의미
- -1 은 (*this) 가 사전식으로 더 앞에 온다는 의미이다.
- 상수함수!
- 각 문자를
>
로 비교하면 됨
- 문자열간의 크기 비교하는 함수 (
클래스의 explicit 과 mutable 키워드
explicit
1
2
3
4
5
6
7
void DoSomethingWithString(MyString s) {
// Do something...
}
DoSomethingWithString(MyString("abc"))
DoSomethingWithString("abc")
- 위 첫번째 함수 호출은, MyString 객체를 명시적으로 생성해서 인자로 전달
- 두번째 함수 호출은, 원래는 안되지만
- 컴파일러가 “abc” 를 써서 MyString 객체를 만들어주는 생성자를 찾아서 객체를 생성하고 인자로 전달해 줌
- 즉,
MyString(const char* str);
이거를 갖다 씀 - 이렇게 컴파일러가 알아서 생성자 체크하고 골라주는걸 암시적 변환 (implicit conversion) 이라고 함.
- 암시적 변환이 좋은 것 같지만,
- 프로그래머가 실수로 잘못된 타입을 입력해도
- 컴파일러가 제멋대로 생성자 골라서 객체 생성하기 때문에
- 이상한 일이 벌어질 수 있음!!
- explicit 키워드
- C++가 암시적 변환을 하지 못하도록 해주는 키워드!!
- 생성자 선언할 때, 앞에 붙여주면 됨
1
explicit MyString(int capacity);
mutable
- const 함수 내부에서 멤버 변수를 바꾸는거 안됨
- 근데 멤버변수를 mutable로 선언 했으면, const 함수에서도 값을 바꿀 수 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
mutable int data_;
public:
A(int data) : data_(data) {}
void DoSomething(int x) const { // 상수함수
data_ = x; // 상수함수지만, data_는 mutable 키워드 달았으니깐 바꿀 수 있음
}
}
int main(){
A a(10);
a.DoSomething(3); // 실행 잘 됨
}
- mutable 필요한 이유?
- 멤버함수에 const를 선언하는건 “이 함수는 객체의 내부 상태를 안 바꿉니다”는 표현
- 근데, “읽기” 같이 객체 내부를 안바꿔야하는 함수도, 조금은 바꿀 필요가 있다.
- 예시: 읽기 함수에서 소프트웨어 캐시에 데이터를 저장/삭제 해야할 경우
- 서버에서 데이터를 읽는 const 함수가 있는데,
- 데이터베이스에서 데이터 요청해서 가져오는게 너무 느리니깐,
- 소프트웨어 캐시를 사용할 때가 있음
- 그럼, cache miss 가 날 때, cache에 데이터를 저장하는 기능이 함수에 들어가야 함
- 따라서, cache buffer 역할을 하는 변수나 객체는 mutable 선언을 해야 함!!