[전공]

[명품 C++] 05 함수와 참조, 복사 생성자

danhan 2022. 6. 5. 02:35

출처 :

  • 명품 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 순으로 힙에 반환                   
}

묵시적 복사 생성에 의해 복사 생성자가 자동 호출되는 경우