[C++] OOP (2) 함수 오버로딩 / 함수 호출
C++의 메모리 모델은 C와 동일하다.
간단하게 관련 문법을 살펴보자.
RHS : Right Hand Side expression
LHS : Left Hand Side expression
&x : RHS에서 x의 주소를 반환한다. (x값이 저장된 메모리 주소를 반환한다)
&x : LHS에서는 Reference Type을 의미한다.
*x : RHS에서 x가 가리키는 주소의 값을 반환한다. (x값으로 메모리 주소를 가진다. x값이 가리키는 주소에 할당된 값을 반환한다)
*x : LHS에서 RHS의 값을 x가 가리키는 주소에 값으로 할당한다. (x값으로 메모리 주소를 가진다. x값이 가리키는 주소에 RHS 값을 할당한다)
Const
기본 개념은 이쪽을 참고하자. (https://13months.tistory.com/228)
const int x = 16;
int *y = &x;
const int x = 16; // x의 주소가 100번지라고 하자.
100번지에는 16이 저장돼있다.
const 키워드로 인해 100번지의 값은 수정되지 않는다.
const int x는 readonly 속성을 가진다.
int *y = &x; 는 x를 수정할 수 있는 가능성이 있어 컴파일 시점에서 에러를 뱉는다.
생각해보면 당연하다.
int *y = &x;로 선언하면 *y는 const가 아니라서 마음대로 수정할 수 있고, 이에 따라 const int x 의 값도 수정될 수 있으니 컴파일 에러를 뱉어야 한다.
const int x = 16;
const int* y = &x;
*y = 10;
const int* y 는 y에 주소값이 저장돼있고, y에 저장된 주소값을 찾아간 값을 변경할 수 없음을 의미한다.
const int* y = &x; 까지는 오류를 뱉지 않는다. 어차피 수정할 수 없으니까.
*y = 10; 에서 y의 값을 수정하려 할 때는 당연히 오류를 뱉는다.
int x = 16;
const int* y = &x;
x = 10;
*y = 10;
x = 10 까지는 오류를 뱉지 않는다.
*y = 10에서 컴파일 오류가 발생한다.
y에 저장된 주소를 찾아간 값을 변경할 수 없어야 하는데, 변경을 시도했기 때문이다.
int x = 1;
int y = 2;
int* const z = &x;
z = &y;
int* const z 는 z에는 주소값이 저장돼있고, 주소값 자체를 변경할 수 없음을 의미한다.
z가 const 속성을 가져 y = &z에서 컴파일 오류를 뱉는다.
int x = 1;
int* const z = &x;
*z = 10;
z는 const지만 *z는 const가 아니다. 오류를 뱉지 않는다.
클래스 단에서 const를 살펴보자.
class Car{
public:
Car(std::string name) : name_(name){}
const std::string* name() {return &name_;}
void set_name(const std::string name) {name = name + "a";} // const라서 컴파일 오류
void Something() const {name_ = name_ + "d";} // const는 모든 멤버변수에 적용된다.
private:
std::string name_;
}
1. 리턴 타입으로 const
리턴 값이 immutable하다. 여기서는 const pointer를 반환하기 때문에 string* asfd = car1.name() 처럼 값을 할당받을 수 없다. (수정될 가능성이 있기 때문)
2. 파라미터 타입으로 const
파라미터를 immutable로 설정한다.
인자로 들어오는 값이 immutable 하게 설정된다.
3. 함수 시그니처 이후 const
모든 멤버변수들이 const로 설정된다.
이미 멤버변수를 const로 설정해도 충돌이 발생하지 않고, 실수를 방지하기 위해 사용한다.
바뀌지 않는 요소에는 const를 붙여주자. 실수를 방지할 수 있고 여러 사람들과 협업 시 const키워드가 매우 중요해진다.
클래스 자체에 public과 같은 접근제어자와 const 키워드를 붙이지는 않는다. (자바와 다름)
Shallow Copy (얕은 복사)
값 까지 복사하지 않고 주소만 복사하는 경우를 말한다.
객체 하나의 값을 변경했는데 다른 객체까지 영향을 주는 경우가 생긴다.
cpp 컴파일러가 기본으로 지원하는 copy constructor가 shallow copy를 제공한다.
Deep Copy (깊은 복사)
값 까지 복사해 독립적으로 동작하도록 한다.
deep copy는 기본 constructor로 제공해주지 않으니 필요 시 직접 만들어야 한다.
shallow와 deep은 상대적인 개념이지만..
몇 다리를 건너서 복사하든 실제 데이터를 복사한다면 deep copy로 생각할 수 있다.
실제 데이터가 복사되는지, 아니면 데이터의 주소까지만 복사하는지를 판단 기준으로 정하자.
Car(const Car& car) : name_(car.name_) {} // Shallow Copy
Car(const Car& car){
name_ = new char[sizeof car.name_];
strcpy(name_, car.name_);
} // Deep Copy
인자를 메서드로 넘기는 방법에는 세 가지가 있다.
1. Call by values
2. Call by pointers
3. Call by references
1. Call by values
void Add(int y){
y += 3;
}
int main() {
int x = 1;
Add(x);
}
인자의 값을 복사해서 메모리에 새로 할당한다.
여기서 할당하는 메모리는 Stack 메모리이기 때문에 함수가 끝나면 사라진다. (위의 그림에서 y가 사라진다)
코드를 차근차근 따라가보자.
Car car1("mycar") 을 실행해 메모리에 car1을 할당했다.
ChangeCarName(car1, "yourcar") 메서드를 실행했다.
Car car2 = car1 이 실행되고, Copy Constructor가 호출된다.
car2가 새롭게 메모리에 할당되고, car2의 name_은 car1의 값을 복사한 값이 됐다.
car2.set_name(name)으로 car2의 이름이 "yourcar"로 바뀌었다. (car1의 이름은 그대로이다.)
ChangeCarName 메서드가 종료된 후 스택에서 제거되기 때문에 car2 객체 또한 사라진다.
2. Call by pointers
void Add(int* y){
if(y != NULL) *y += 3;
}
int main() {
int x = 1;
Add(&x);
}
인자로 변수의 주소를 넘겨준다.
메서드에서는 넘어온 인자가 NULL인지 아닌지를 확인하는 부분이 있다.
주소를 넘겨 연산하기 때문에 함수가 끝나면 함수가 스택에서 제거돼 변수 y가 사라지지만, 연산 결과는 유지된다.
3. Call by references
void Add(int& y){
y += 3;
}
int main() {
int x = 1;
Add(x);
}
인자로 레퍼런스를 받는다.
여기서 레퍼런스는 같은 주소를 가지는 변수를 말한다. (그림 참고)
연산 결과가 유지된다는 점에서 Call by pointers와 비슷하지만, NULL 체크를 할 필요가 없는 점에서 다르다.
포인터를 넘길 때는 값이 NULL일 수도 있지만, 레퍼런스는 해당 변수를 그대로 사용하기 때문에 NULL일 수 없다.
객체가 넘어갈 때 NULL일 가능성이 있으면 Call by references를 사용하고, 구글은 Call by references를 사용할 때는 읽기 전용으로 사용함을 권장한다.
함수를 통해 지역 변수를 반환할 때 주소나 레퍼런스를 반환 시 Dangling Pointer를 사용하게 될 수 있으니 주의하자!!
Call by values를 사용하면 값을 모두 복사해야 해서 비용이 많이 들 때가 있다.
이런 경우 Call by pointers 혹은 Call by references를 사용하는 편이 합리적이다.
new 연산자 (C에서의 malloc) 를 사용해 객체를 생성할 경우 heap 메모리에 저장되고, 진행 중인 메서드가 끝나더라도 값이 사라지지 않는다.
자바에서는 거의 모든 객체를 new 연산자를 사용해서 생성하지만, C++에서는 객체를 생성하는 방법이 여러 가지가 있으니 적절하게 사용하도록 하자.
C++에서 함수 오버로딩의 가능 여부는 함수의 signature에 따라서 결정된다.
Signature : 함수 이름 / 파라미터 타입
Descriptor : 함수 이름 / 파라미터 타입 / 리턴 타입
오버로딩된 여러 함수를 사용할 때 어떤 함수가 사용될지는 컴파일 시점에서 결정된다. (static dispatch)
그럼 컴파일러의 선택 기준에 대해 알아보자.
위와 같이 작성된 코드에서, 각각의 foo 메서드는 어떤 메서드를 오버라이딩해서 사용할까?
1. Collect viable functions
일단 이름이 같고 파라미터 수가 같은 메서드들을 후보로 설정한다. (파라미터의 타입은 고려하지 않고, 후보가 하나도 없으면 컴파일 에러가 발생한다.)
2. Exact match
후보 중 타입이 정확히 일치하는 메서드를 선택한다. (2 = int / 3.2 = double / 2.3f = float.. )
3. Promotion
정확히 일치하는 타입이 없으면 값 손실을 방지하기 위해 타입이 더 큰 요소를 따라간다. (int 4byte -> double 8byte)
4. Standard Type Conversion
값 손실이 발생할 가능성이 있는 메서드라도 사용한다. (int 4byte -> char 1byte)
5. User-define
사용자가 정의한 타입 변환을 시도하는데.. 잘 안 쓴다.
마지막 과정까지 왔는데도 후보가 하나도 없으면 컴파일 에러를 뱉는다.
후보가 여러 개일 경우 비용이 저렴한 쪽으로 선택한다.
이 때 타입 캐스팅의 비용은 공식 문서를 참고하는게 정확하지만, 기본적으로 변환하는 타입과의 차이가 클 수록 비용이 크다고 할 수 있다.
파라미터가 여러 개일 경우도 각각 비용을 따져서 계산한 후 판단하자.
'Programming Language > C++' 카테고리의 다른 글
[C++] OOP (4) Type Casting (0) | 2022.10.19 |
---|---|
[C++] OOP (3) Inheritance / 연산자 오버로딩 (1) | 2022.10.12 |
[C++] C++과 Java (1) | 2022.10.05 |
[C++] Memory Allocation / static (0) | 2022.10.05 |
[C++] OOP (1) 개념 / namespace (0) | 2022.09.14 |
댓글
이 글 공유하기
다른 글
-
[C++] OOP (3) Inheritance / 연산자 오버로딩
[C++] OOP (3) Inheritance / 연산자 오버로딩
2022.10.12 -
[C++] C++과 Java
[C++] C++과 Java
2022.10.05 -
[C++] Memory Allocation / static
[C++] Memory Allocation / static
2022.10.05 -
[C++] OOP (1) 개념 / namespace
[C++] OOP (1) 개념 / namespace
2022.09.14