[C++] 05-1. 함수와 참조

SunowMin·2023년 9월 21일
0

C++

목록 보기
7/8

5.1 인자 전달 방식(argument passing)

① 값에 의한 호출(call by value)

  • 호출하는 코드에서 넘겨주는 실인자 값이 함수의 매개 변수에 복사되어 전달되는 방식
  • 특징 : 함수 내에서 실인자를 손상시킬 수 없음

호출 과정

	#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);
        cout << m << ' ' << n;

실행 결과
2 9

(1) 매개변수 a,b가 swap() 함수의 스택에 생성
(2) m,n 값이 a,b에 복사
(3) a와 b의 값이 교환
(4) swap() 함수 종료하면 swap() 스택과 함께 a,b도 사라짐
(5) 하지만 main() 스택에 m,n은 변함없이 2,9의 값 유지


② 주소에 의한 호출(call by address)

  • 주소를 직접 포인터 타입의 매개변수에 전달받는 방식
  • 함수 호출 시 배열이 전달되는 경우, 배열의 이름이 전달되므로 자연스럽게 '주소에 의한 호출'이 이루어짐
    (배열의 이름은 곧 배열의 주소이기 때문)
  • 특징 : 실인자의 주소를 넘겨주어 의도적으로 함수 내에서 실인자의 값을 변경하고자 할 때

호출 과정

#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);
        cout << m << ' ' << n;

실행 결과
9 2

(1) 포인터 매개변수 a,b가 swap()의 스택에 생성
(2) m,n의 주소가 a,b에 전달(포인터 a,b는 m,n을 각각 가리킴)
(3) 포인터 a,b가 가리키는 값이 교환되면
(4) 그 결과 m,n의 값이 교환
(5) swap() 함수가 종료하면 a,b가 사라지고 main() 스택의 m,n은 서로 교환된 채 남음





5.2 함수 호출시 객체 전달

C++에서는 함수에 객체를 전달할 수 있으며, 기본 타입의 값을 전달하는 것과는 약간 다름

① '값에 의한 호출'로 객체 전달

'값에 의한 호출' 과정

#include <iostream>
using namespace std;

class Circle {
private:
	int radius; 
public:
	Circle(); 
	Circle(int r);
	~Circle();
	double getArea()  { return 3.14*radius*radius; }
	int getRadius() { return radius; }
	void setRadius(int radius) { this->radius = radius; }
}; 

Circle::Circle() {
	radius = 1;
	cout << "생성자 실행 radius = " << radius << endl;
}

Circle::Circle(int radius) {
	this->radius = radius;
	cout << "생성자 실행 radius = "  << radius << endl;
}

Circle::~Circle() {
	cout << "소멸자 실행 radius = " << radius << endl;
}

void increase(Circle c) {
	int r = c.getRadius();
	c.setRadius(r+1);
}

int main() {
	Circle waffle(30);
	increase(waffle);
	cout << waffle.getRadius() << endl;
}

실행 결과
생성자 실행 radius = 30    // waffle 생성
소멸자 실행 radius = 31     // c 소멸 (c의 생성자 실행되지 않았음)
30
소멸자 실행 radius = 30    // waffle 소멸

  • Circle waffle(30);
    - 반지름이 30인 waffle 객체 생성
  • increase(waffle);
    - waffle 객체를 increase(Circle c) 함수에 전달(객체 이름만 적어서 전달)
    - increase()가 호출되면 waffle 객체가 현재 상태 그대로 매개 변수 c에 복사
    - increase()가 종료하면 매개 변수 객체 c가 소멸되고 main()문의 waffle 객체는 increase()를 호출하기 전과 동일하게 반지름이 30

📌 함수 안에서 매개 변수 객체에 어떤 변화가 일어나도 실인자(원본 객체)를 훼손시키지 않음

값에 의한 호출 시 객체 복사 시간
실인자 객체의 크기가 크면 객체를 복사하는 시간이 커지는 단점 존재


'값에 의한 호출'로 객체를 전달할 때 문제점

  • 매개 변수 객체의 생성자는 실행되지 않고 소멸자만 실행되도록 컴파일되는 비대칭 구조 실행

실행결과
생성자 실행 radius = 30     // waffle 생성
소멸자 실행 radius = 31    // c 소멸 (c의 생성자 실행되지 않았음)
30
소멸자 실행 radius = 30   // waffle 소멸


왜 매개 변수 객체의 생성자가 실행되지 않도록 컴파일 되는가?

  • 매개 변수 객체의 생성자를 실행한 뒤 멤버 변수의 값을 바꾸게 되면 전달 받은 원본의 상태를 잃어버리게 됨 = 이런 상황 방지하기 위해 생성자 실행하지 않음
  • 하지만, 매개 변수 객체의 생성자가 실행되지 않고 소멸자만 실행되는 비대칭 구조는, 함수 호출 시 원본 객체의 상태를 그대로 매개 변수 객체에 전달되도록 하기 위한 것이지만 중대한 문제를 동반함
    - 위 문제를 해결하기 위해 복사생성자 공부 필요(5.5절에서 공부할 예정)

복사생성자(copy constructor)

  • '값에 의한 호출'시 컴파일러는 매개 변수 객체의 생성자(constructor) 대신 복사생성자가 호출되도록 컴파일하기 때문에, 생성자가 실행되지 않는 것임



② '주소에 의한 호출'로 객체 전달

'주소에 의한 호출' 과정

  • 객체의 주소만 전달
  • 위 코드에서 increase() 함수와 함수 호출 코드만 수정
#include <iostream>
using namespace std;

class Circle {
private:
	int radius; 
public:
	Circle(); 
	Circle(int r);
	~Circle();
	double getArea()  { return 3.14*radius*radius; }
	int getRadius() { return radius; }
	void setRadius(int radius) { this->radius = radius; }
}; 

Circle::Circle() {
	radius = 1;
	cout << "생성자 실행 radius = " << radius << endl;
}

Circle::Circle(int radius) {
	this->radius = radius;
	cout << "생성자 실행 radius = "  << radius << endl;
}

Circle::~Circle() {
	cout << "소멸자 실행 radius = " << radius << endl;
}

void increase(Circle *p) {
	int r = p->getRadius();
	p->setRadius(r+1);
}

int main() {
	Circle waffle(30);
	increase(&waffle); // waffle 객체의 주소를 전달함
	cout << waffle.getRadius() << endl;
}

실행결과
생성자 실행 radius = 30      // waffle 생성
30
소멸자 실행 radius = 30      // waffle 소멸

  • Circle waffle(30);
    - 반지름이 30인 waffle 객체 생성
  • void increase(Circle *p)
    - '주소에 의한 호출'을 위해 *p로 선언
  • increase(&waffle);
    - main() 함수에서 increase() 호출하여 waffle 객체의 주소를 p에 전달
    - p는 객체가 아니므로 생성자/소멸자 상관 없음
  • p->setRadius(r+1);
    - waffle 객체의 반지름이 1증가



'주소에 의한 호출'의 특징

  • 원본 객체를 복사하는 시간 소모 없음
  • 매개 변수가 단순 포인터이므로 생성자 소멸자의 비대칭 문제도 없음
  • 단, 매개변수 포인터로 원본 객체를 훼손할 가능성





5.3 객체 치환 및 객체 리턴

객체 치환 (assignment)

  • 치환 시 객체의 모든 데이터가 비트 단위로 복사
Circle c1(5);
Circle c2(30);
c1 = c2;
  • c1과 c2는 내용물만 같은 뿐 하나의 객체가 되는 것은 아님
  • 객체 치환은 동일한 클래스 타입에 대해서만 적용



함수의 객체 리턴

#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; }
};

Circle getCircle() {
	Circle tmp(30);
	return tmp; // 객체 tmp을 리턴한다.
}

int main() {
	Circle c; // 객체가 생성된다. radius=1로 초기화된다.
	cout << c.getArea() << endl;

	c = getCircle();
	cout << c.getArea() << endl;
}

실행결과
3.14
2826

  • Circle 클래스의 객체를 리턴하는 getCircle() 함수
    (1) return 문이 실행되면 tmp의 복사본 생성
    (2) 이 복사본이 getCircle()을 호출한 곳으로 전달
    (3) tmp 소멸

  • c = getCircle();cout << c.getArea() << endl;
    - 객체 c가 생성될 때 반지름 값이 1이었지만, getCircle()이 리턴한 tmp 객체로 치환되면 객체 c의 반지름은 30이 됨
    - c는 tmp(30)과 내용물이 같아짐






5.4 참조와 함수

참조

  • 참조 : 가리킨다는 뜻으로, 참조변수(reference variable)는 이미 선언된 변수에 대한 별명(alias)이다.
  • 참조의 활용
    (1) 참조 변수
    (2) 참조에 의한 호출
    (3) 함수의 참조 리턴

(1) 참조 변수

① 참조 변수 선언

  • 참조 변수
    - 이미 선언된 변수(원본 변수)에 대한 별명으로서
    - 참조자(&)를 이용하여 선언하며
    - 선언 시 원본 변수로 초기화하여야 함

  • 아래 코드 : 참조 변수 refn과 refc를 선언
int n = 2;
int &refn = n;     // 참조 변수 refn 선언, refn은 n에 대한 별명, refn과 n은 동일한 변수
refn = 3;     // n의 값도 3으로 변경됨

Circle circle;
Circle &refc = circle;     // 참조 변수 refc 선언, refc는 circle에 대한 별명, refc와 circle은 동일한 변수
refc.setRadius(30);     //  refc가 참조하는 circle 객체의 radius 멤버 변수가 30으로 변경
// refc->setRadius(30);으로는 불가능(포인터가 아니라서)
  • 참조 변수가 선언되면, 참조 변수 이름만 생성되며 별도의 공간이 할당되지 않음
  • 참조 변수는 초기화로 지정된 원본 변수의 공간을 공유

② 참조 변수 사용

  • 사용 방법은 보통 변수와 동일
  • 아래 코드 : 참조 변수에 대한 사용은 바로 원본 변수의 사용
int n = 2;
int &refn = n; 

refn = 3;
n = 5;     // n = 5, refn = 5가 됨
refn++;     // n = 6, refn = 6이 됨
  • 아래 코드 : 참조 변수에 대한 포인터 생성
int n = 2;
int &refn = n; 

int *p = &refn;     // p는 refn의 주소를 가짐, p는 n의 주소
*p = 20;     // n = 20, refn = 20;

💡 참조 변수 선언 시 주의 사항

초기화가 없다면 컴파일 오류 발생

  • 선언 시 반드시 초기화!
int n = 2;
int &refn2; // refn2가 어떤 변수에 대한 참조인지 초기화되지 않았음

ⓑ 참조자 &의 위치는 무관

  • 아래 3개의 참조 변수 선언은 모두 동일
int &refn2;
int & refn2;
int& refn2;

ⓒ 참조 변수의 배열을 만들 수 없음

  • 이유 1.
    배열의 크기는 컴파일 시간에 결정되어야 함. 참조 변수는 이미 선언되어 있는 변수를 참조하므로 배열 크기를 미리 알 수 없으며, 런타임에 결정될 수도 있음.
  • 이유 2.
    참조 변수는 한 번 초기화되면 다른 변수를 참조할 수 없음. 배열의 원소를 바꾸려면 참조 변수를 다른 변수로 재할당해야하는데, 배열의 원소는 참조 변수로 바로 할당 불가능
  • 배열은 포인터를 사용하여 다루는 것이 일반적
char &n[10]; // 컴파일 오류

ⓓ 참조 변수에 대한 참조 선언이 가능

  • r과 refn 모두 n의 공간을 공유하며 구분 없이 사용 가능
int &r = refn;   // 참조 변수 refn에 대한 참조 변수 r 선언 가능

*(Asterisk) 연산자의 기능
1. 방금 배운 참조자 기능
2. 두 개의 변수, 혹은 상수 사이에 위치하게 되면 비트연산(AND) 기능
3. 변수의 메모리 주소를 나타내는 기능


객체에 대한 참조 예제

#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);
	cout << refc.getArea() << " " << circle.getArea(); // 두 호출은 동일 객체에 대한 호출
}

실행결과
314 314



(2) 참조에 의한 호출, call by reference

  • 참조는 '참조에 의한 호출'에 많이 사용됨

참조에 의한 호출

  • 참조에 의한 호출(call by reference) : 함수의 매개 변수를 참조 타입으로 선언하여, 매개 변수가 함수를 호출하는 쪽의 실인자를 참조(reference)하여 실인자와 공간을 공유하도록 하는 인자 전달 방식
  • 참조 매개 변수(reference parameter) : 참조 타입으로 선언된 매개 변수
  • 아래 코드 : 참조 매개 변수를 가진 함수의 사례
    - swap() 함수는 참조 매개 변수 a, b를 가지며, swap() 함수에 대한 호출은 '참조에 대한 호출'이 됨
    - swap(m, n);은 '값에 의한 호출'과 모양이 동일하지만, 함수의 원형(void swap(int &a, int &b))을 보고 구분할 수 있음
    - 참조 매개 변수 a, b는 swap()의 스택에 공간을 할당받지 않음 -> a,b의 값이 교환되면, 참조하고 있던 m,n의 값도 교환 -> swap() 함수가 종료되면 a,b의 이름 사라짐
#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);     // 참조에 의한 호출
	cout << m << ' ' << n;
}

실행 결과
9 2


참조 매개 변수가 필요한 사례

  • 아래 코드 : 평균을 구하여 리턴하는 함수
int average(int a[], int size) {
	if(size <= 0) return 0; // size는 음수가 될 수 없음
	int sum = 0;
	for(int i=0; i<size; i++) sum += a[i];
	return sum/size;
}

위 함수를 아래와 같이 호출하면

int x[] = {1,2,3,4};
int avg = average(x, -1); //avg에 0이 리턴됨
  • average()의 리턴값이 0인 경우, 계산된 평균이 0인지, 잘못된 매개 변수를 알리기 위한 0인지 알 수 없음

  • 따라서 아래와 같이 average() 함수 수정

#include <iostream>
using namespace std;

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 << endl;
	else cout << "매개 변수 오류" << endl;

	if(average(x, -2, avg)) cout << "평균은 " << avg << endl;
	else cout << "매개 변수 오류 " << endl;
}
  • 리턴 타입을 bool로 하고 평균값을 전달하기 위해 참조 매개 변수를 추가함

참조에 의한 호출의 장점

  • 참조 매개 변수를 사용하면 간단히 변수를 넘겨주기만 하면 됨
  • 함수 내에서도 참조 매개 변수를 보통 변수처럼 사용하기 때문에 보기 좋은 코드

참조에 의한 호출로 객체 전달

  • 참조 매개 변수로 이루어진 모든 연산은 원본 객체에 대한 연산이 됨
  • 참조 매개 변수는 이름만 생성되므로, 생성자와 소멸자는 아예 실행되지 않음




(3) 참조 리턴

  • C 언어에서 함수가 리턴할 수 있는 건 오직 값 뿐(기본 타입, 포인터)
  • C++에서는 함수가 참조를 리턴할 수 있음

참조 리턴

  • 참조 리턴 : 변수 등과 같이 현존하는 공간에 대한 참조의 리턴
  • 아래 코드 : char 타입의 참조를 리턴하는 find() 함수
char c = 'a';

char& fine() {   // char 타입의 참조 리턴
	return c;   // 변수 c에 대한 참조 리턴
}

char a = find();   // a = 'a'

char& ref = find();   // ref는 c에 대한 참조
ref = 'M';   // c = 'M'

find() = 'b';   // c = 'b'

참조 리턴에 대한 치환문

  • find()가 치환문(=)의 오른쪽에 온다면 변수 c의 값 'a'가 변수 a에 치환
char a = find();
  • 참조 변수로 참조를 리턴받을 수도 있음
char& ref = find();
  • ref는 find()가 리턴한 변수 c의 참조가 됨
  • 따라서 ref에 대한 연산은 모두 변수 c에 대해 이루어지는 연산이 됨
ref = 'M';  // c = 'M'





해당 포스팅은 '황기태, 『명품 C++ Programming』, 생능출판사'를 참고하여 작성하였습니다.

0개의 댓글