출처 :
- 명품 C++ Programming (저자 황기태)
- 객체지향프로그래밍
- 노션 공유를 원하시는 분은 댓글에 이메일 남겨주세요
함수의 인자 전달 방식
- 값에 의한 호출 call by value
- 주소에 의한 호출 call by address
- 참조에 의한 호출 call by reference
값에 의한 호출 call by value
- 함수가 호출되면 매개 변수가 stack에 생성됨
- 호출하는 코드에서 값을 넘겨줌
- 호출하는 코드에서 넘어온 값이 매개 변수에 복사됨
#include <iostream>
using namespace std;
void swap(int a, int b) {
int tmp;
tmp = a;
a = b;
b = tmp;
}
int main() {
int m = 2, n = 9;
swap(m, n); // 값이 복사될 뿐 m,n 은 변화 없음
}
값에 의한 호출로 객체 전달
- 함수를 호출하는 쪽에서 객체 전달 - 객체 이름만 사용
- 함수의 매개 변수 객체 생성
- 매개 변수 객체의 공간이 stack에 할당
- 호출하는 쪽의 객체가 매개 변수 객체에 그대로 복사됨
- 매개 변수 객체의 생성자는 호출되지 않음 → 호출되는 순간의 실인자 객체 상태를 매개 변수 객체에 그대~로 복사하여 전달하기 위해서
int main() {
// main() stack에 "radius가 30인 waffle 객체" 생성됨
Circle waffle(30);
// call by value하면 increase() 스택에 "radius가 30인 객체"가 그대로 복사됨
increase(waffle);
- 함수 종료
- 매개 변수 객체의 소멸자 호출
- 매개 변수 객체의 생성자, 소멸자의 비대칭 실행 구조
void increase(Circle c) {
int r = c.getRadius();
c.setRadius(r + 1);
}
int main() {
// waffle 생성
Circle waffle(30);
// waffle의 내용 그대로 c에 복사 -> c 생성자 실행 X -> c 소멸
increase(waffle);
cout << waffle.getRadius() << endl;
// main 함수 종료와 함께 waffle 소멸
}
값에 의한 호출의 비대칭 구조 정리!
- increase ()의 매개 변수 c 가 생성될 때 생성자 실행 없이 c의 객체 공간에 waffle 객체가 그대로 복사된다.
- 하지만 increase()가 종료될 때 객체 c의 소멸자는 실행된다.
- 왜? 만일 c의 생성자 Circle()이 실행된다면, 객체 c의 반지름이 1로 초기화되어, 전달 받은 원본의 상태를 잃어버린다.
주소에 의한 호출 call by address
- 함수가 호출되면 “포인터 타입”의 매개 변수가 stack에 생성됨
- 호출하는 코드에서는 명시적으로 주소를 넘겨줌
- 변수나 객체의 경우 주소 전달, 배열은 배열의 이름
- 호출하는 코드에서 넘어온 주소 값이 매개 변수에 저장됨
#include <iostream>
using namespace std;
void swap(int * a, int * b) {
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
int main() {
int m = 2, n = 9;
// a, b에 m, n의 주소가 전달되고 m, n이 변경된다.
swap(&m, &n);
}
주소에 의한 호출로 객체 전달
- 함수 호출시 객체 주소만 전달
- 함수의 매개 변수는 객체에 대한 포인터 변수로 선언
- 함수 호출시 생성자, 소멸자가 실행되지 않는 구조
- 왜? 포인터는 객체가 아니므로 생성자나 소멸자와 상관없다.
void increase(Circle *p) { // 매개 변수 포인터 p 생성
int r = p->getRadius();
p->setRadius(r + 1);
} // 함수 종료하면서 포인터 p 소명
int main() {
Circle waffle(30); // waffle 생성
increase(&waffle); // waffle의 주소가 p에 전달
cout << waffle.getRadius();
}
객체 치환 및 객체 리턴
객체 치환
- 동일한 클래스 타입의 객체까지 치환 가능
- 객체의 모든 데이터가 비트 단위로 복사
- 치환된 두 객체는 복사 당시 내용물만 같을 뿐 독립적인 공간 유지
Circle c1(5);
Circle c2(30);
c1 = c2; // c2 객체를 c1 객체에 비트 단위 복사 - c1의 반지름 30으로 바뀜
객체 리턴
Circle getCircle() {
Circle tmp(30);
return tmp;
}
Circle c;
c = getCircle(); // tmp 객체의 복사본이 c에 치환 - c의 반지름 30으로
참조
- reference 가리킨다는 뜻으로 이미 존재하는 객체나 변수에 대한 별명
- 참조 변수, 참조에 의한 호출, 참조 리턴
참조 변수
참조 변수 선언
- 참조자 & 기호 사용
- 이미 존재하는 변수에 대한 다른 이름(별명)을 선언
- 참조 변수는 이름만 생기고 새로운 공간을 “할당하지 않는다.”
- 초기화로 지정된 기본 변수를 공유한다.
참조 변수 선언 시 주의!
- 반드시 원본 변수로 초기화해야 한다.
- 초기화가 없다면 컴파일 오류가 발생한다. int &refn;
- 참조 변수의 배열은 만들 수 없다. char &n[10];
- 참조 변수에 대한 참조 선언은 가능하다.
Circle circle;
Circle &refc = circle; // 참조 변수 refc 선언, circle에 대한 별명
refc.setRadius(30); // retc->setRadius(30);으로 하면 안됨
예제 - 기본 타입 변수에 대한 참조
#include <iostream>
using namespace std;
int main() {
int i = 1;
int n = 2;
int &refn = n; // 참조 변수 refn 선언. refn은 n에 대한 별명
n = 4;
refn++; // refn은 5, n도 5
refn = i; // refn이 i를 가리키게 된게 아니라, refn이 가리키느 참조공간 n에 i의 값을 넣음
refn++; // refn과 n 모두 2
int * p = &refn; // p는 n의 주소를 가짐
*p = 20; // refn과 n 모두 20
}
예제 - 객체에 대한 참조
#include <iostream>
using namespace std;
class Circle {
int radius;
public:
Circle() { radius = 1; }
Circle(int radius) { this->radius = radius; }
void setRadius(int radius) { this->radius = radius; }
double getArea() {return 3.14 * radius * radius; }
};
int main() {
Circle circle;
Circle &refc = circle;
refc.setRadius(10);
// refc.getArea()랑 circle.getArea() 모두 314
}
참조에 의한 호출 call by reference
call by address와 다르다!!!
- 참조를 가장 많이 활용하는 사례
- 함수의 매개 변수를 참조 타입으로 선언
- 참조 매개 변수 reference parameter
- 참조 매개 변수의 이름만 생기고 공간이 생기지 않음
- 참조 매개 변수는 실인자 변수 공간 공유
- 참조 매개 변수에 대한 조작은 실인자 변수 조작 효과
*참조는 그냥 별명, 이름
*포인터와 달리 swap() 스택에 공간을 할당받지 않는게 키포인트!
*함수 내에서 사용할 때는 일반 변수처럼 사용한다.
void swap(int &a, int &b) { // 참조 매개 변수 a, b
int tmp; // a와 b는 m, n의 별명일 뿐 변수 공간은 생기지 않음
tmp = a; // 참조 매개 변수는 보통 변수처럼 사용
a = b;
b = tmp;
}
주의!
- 참조 매개 변수로 이루어진 모~든 연산은 원본 객체에 대한 연산이 된다.
- 이름만 생성되므로, 생성자와 소멸자는 아예 실행되지 않는다.
참조 매개변수가 필요한 사례
참조 매개 변수로 평균 리턴하기 (중요 문제!!)
bool average(int a[], int size, int& avg) {
if (size <= 0)
return false;
int sum = 0;
for (int i = 0; i < size; i++)
sum += a[i];
avg = sum / size;
return true;
}
int main() {
int x[] = {0,1,2,3,4,5};
int avg;
if (average(x, 6, avg))
cout << avg << "\\n";
else
cout << "매개 변수 오류";
참조에 의한 호출로 Circle 객체에 참조 전달
void increaseCircle(Cirel &c) {
int r = c.getRadius();
c.setRadius(r + 1);
}
increaseCircle(waffle);
참조 리턴
- C++의 함수 리턴
- 함수는 값 외에 참조 리턴 가능
- 변수 등과 같이 현존하는 공간에 대한 참조 리턴
- 변수의 값을 리턴하는 것이 아님
- 참조는 공간이 없음
// 값을 리턴하는 함수
char c = 'a';
char get() {
return c; // 변수 c에 문자 'a' 리턴
}
char a = get(); // a = 'a'
get() = 'b' // 컴파일 오류
// 참조를 리턴하는 함수
char c = 'a';
char& find() {
return c; // 변수 c에 대한 참조 리턴
}
char &ref = find();
ref = 'M'; // c = 'M'
find() = 'b'; // c = 'b'로 컴파일 오류 안남. find()는 c에 대한 참조를 반환
char& find(char s[], int index) {
return s[index]; // 공간에 대한 참조 리턴
}
find(name, 0) = 'S'; // name[0] = 'S'로 변경
// 함수가 = 왼쪽에 나왔다는 건 참조를 반환한다는 뜻
char& ref = find(name, 2); // ref는 name[2]를 참조
ref = 't';
교재 문제 245p - 3
int ar[] = {0,1,3,5,7};
int& f(int n) {
return ar[n];
}
(1) f(0) = 100; // {100,1,3,5,7}
(2) f(0) = f(1) + f(2) + f(3) + f(4); // {16,1,3,5,7}
(3) int& v = f(2); v++ // {16,1,4,5,7}
얕은 복사와 깊은 복사
얕은 복사 shallow copy
- 객체 복사 시, 객체의 멤버를 1:1로 복사
- 객체의 멤버 변수에 동적 메모리가 할당된 경우
- 사본은 원본 객체가 할당 받은 메모리를 공유하는 문제 발생
깊은 복사 deep copy
- 객체 복사 시, 객체의 멤버를 1:1로 복사
- 객체의 멤버 변수에 동적 메모리가 할당된 경우
- 사본은 원본이 가진 메모리 크기만큼 별도로 동적 할당
- 원본의 동적 메모리에 있는 내용을 사본에 복사
- 완전한 형태의 복사
- 사본과 원본은 메모리를 공유하는 문제 없음
복사 생성자 copy constructor (4/8 강의 참고)
- 객체의 복사 생성시 호출되는 특별한 생성자
- 한 클래스에 오직 한 개만 선언 가능
- 보통 생성자와 클래스 내에 중복 선언 가능
- 클래스에 대한 참조 매개 변수를 가지는 독특한 생성자
class Circle {
Circle(const Circle& c); // 자기 클래스에 대한 참조 매개 변수
} // const 키워드 주의
Circle::Circle(const Circle& c) {
this->radius = c.radius;
}
디폴트 복사 생성자
- 복사 생성자가 선언되어 있지 않는 클래스
- 컴파일러는 자동으로 디폴트 복사 생성자 삽입
- 동적으로 할당된 메모리 주소만 복사된다. → 얕은 복사 실행
// 복사 생성자가 없는 Book 클래스
class Book {
double price;
char *title;
public:
Book(double pr, char* t);
~Book();
};
// 컴파일러가 삽입하는 디폴트 복사 생성자
Book(const Book& book) {
this->price = book.price;
this->title = book.title; // 주소값이 복사됨
}
얕은 복사 생성자를 사용하여 프로그램이 비정상 종료되는 경우
class Person {
char* name;
int id;
public:
Person(int id, const char* name); // 생성자
~Person(); // 소멸자
// 컴파일러에 의해 디폴트 복사 생성자 삽입
// Person::Person(const Person& p) {
// this->id = p.id;
// this->name = p.name;
// }
void changeName(const char *name);
};
Person::Person(int id, const char* name) {
this->id = id;
int len = strlen(name);
this->name = new char [len+1];
strcpy(this->name, name); // name에 문자열 복사
}
Person::~Person() {
if (name) // 동적 할당된 배열이 있으면
delete [] name; // 동적 할당 메모리 소멸
}
void Person::changeName(const char* name) {
if (strlen(name) > strlen(this->name))
return;
strcpy(this->name, name);
}
int main() {
Person father(1, "Kitae"); // father 객체 생성
Person daughter(father); // dauther 객체 복사 생성, 디폴트 복사 생성자 호출
dauther.changeName("Grace"); // dauther와 father의 이름 "Grace 로 변경
return 0; // dauther, father 순으로 소멸
} // 이미 소멸했는데 또 소멸시키려니까 비정상 종료됨
깊은 복사 생성자를 가진 정상적인 Person 클래스
class Person {
char* name;
int id;
public:
Person(int id, const char* name); // 생성자
~Person(); // 소멸자
Person(const Person& person) // 깊은 복사 생성자 호출
void changeName(const char *name);
};
Person::Person(const Person& person) {// 깊은 복사 생성자 별도로 정의
this->id = person.id;
int len = strlen(person.name);
this->name = new char [len+1];
strcpy(this->name, person.name);
}
int main() {
Person father(1, "Kitae");
Person daugther(father);
daughter.changeName("Grace");
return 0; // dauther, father 순으로 힙에 반환
}
묵시적 복사 생성에 의해 복사 생성자가 자동 호출되는 경우
'[전공]' 카테고리의 다른 글
[명품 C++] 07 프렌드와 연산자 중복 (0) | 2022.06.05 |
---|---|
[명품 C++] 6장 함수 중복과 static 멤버 (3) | 2022.06.05 |
[명품 C++] 04 객체 포인터와 객체 배열, 객체의 동적 생성 (0) | 2022.06.02 |
[명품 C++] 03 - 2 생성자 소멸자 구조체 (0) | 2022.06.02 |
[명품 C++] 03 클래스와 객체 (0) | 2022.06.02 |