이 글은 퍼온 글입니다.
원본은 아래의 주소에 있습니다.
http://yatoyato.tistory.com/1537

C++는 참조변수라는 새로운 복합형을 언어에 추가했다. 참조(reference)는 미리 정의된 어떤 변수의 실제 이름 대신 쓸 수 있는 대용이름이다. 예를 들어, twain을 clemens변수의 참조로 만들면, twain과 clemens는 같은 변수를 나타내는 것으로 사용할 수 있다. 참조의 주된 용도는 함수의 형식 매개변수에 사용하는 것이다. 참조를 전달인자로 사용하면, 그 함수는 복사본 대신 원본데이터를 가지고 작업한다. 덩치 큰 구조체를 처리해야 하는 함수에서 포인터 대신에 참조를 사용할 수 있다. 참조는 클래스를 설계할 때 필수적으로 사용된다.

참조변수의 생성

C와 C++는 변수의 주소를 나타내기 위해 &기호를 사용한다. C++는 &기호에 또하나의 의미를 추가하여 참조선언을 나타내게 하였다. 예를 들어, rodents를 변수 rats의 대용이름으로 만들려면 다음과 같이 할 수 있다.

int rats; 
int &rodents = rats;               // rodents를 rats의 대용이름으로 만든다.

여기에서 &가 주소연산자가 아니라, 데이터형 식별자의 일부로 사용된 것이다. 변수선언에서 char *가 문자를 지시하는 포인터를 의미하는 것처럼, int &는 int에 대한 참조를 의미한다. 이 두 변수는 모두 같은 값과 같은 메모리 위치를 참조한다.

rodents++;                         // rats가 증가한다.

참조는 포인터를 생각나게 한다. 예를 들면 rats를 참조하기 위해 참조와 포인터를 둘다 만들 수 있다.

int *prats = &rats;

그러나 포인터와 참조사이에는 표기방식 외에도 차이가 존재한다. 참조를 선언할 때 그 참조를 초기화해야 한다. 포인터를 선언할 때처럼, 참조를 먼저 선언하고 나중에 값을 지정할 수는 없다. 즉 최초 선언시 가리키는 변수를 변경할 수 없다.

int &rodent; 
rodent = rats;                     // 불가능하다.

참조가 다른 변수를 가리키면 어떻게 되는가?

int rats = 101; 
int bunnies = 50; 
int &rodents = rats; 
rodents = bunnies;                 // 후에 다른 변수를 가리키고자 할 때

이 때 rodents는 bunnies를 가리키는 것이 아니라 rats에 bunnies변수의 값 50을 대입한다. 이런 경우는 어떻게 될까?

int rats = 101; 
int *pt = &rats; 
int &rodents = *pt; 
int bunnies = 50; 
pt = &bunnies;

rodents를 *pt로 초기화하면 rodents가 rats를 참조한다. 이어서 pt가 bunnies를 지시하도록 변경해도 rodents가 rats를 참조한다는 사실은 변하지 않는다.

함수 매개변수로서의 참조

참조는 주로 함수의 매개변수로 사용된다. 그것은 어떤 함수에서 사용하는 변수의 이름을 그 함수를 호출한 프로그램(호출함수)에 있는 어떤 변수의 대용이름으로 만든다. 이러한 방식으로 매개변수를 전달하는 것을 참조로 전달이라 한다. 참조로 전달하면 피호출함수가 호출함수의 변수를 사용할 수 있다. C++에 새로 추가된 이 기능은 오로지 값으로만 전달하는 C로부터 탈피하는 것이다. 물론 C는 포인터를 사용하여 값으로 전달하는 것에 따른 제약을 극복한다.

swapr(x, y);                       // 함수호출 
void swapr(int &a, int &b);        // 함수원형

간단한 함수를 작성할 때에는, 쓸데없이 참조로 전달하려 하지말고, 값으로 전달해야 한다. 곧 알게 되겠지만, 참조 매개변수는 구조체나 클래스와 같이 덩치 큰 데이터를 다룰 때에나 유익하다. 당연히 참조변수는 상수가 포함된 전달인자(예를 들어, x+3.0)를 받아들일 수 없다. 그러나 예외적인 경우도 있는데, C++의 초창기에는 참조변수에 표현식도 전달할 수 있었다. 지금도 그런 경우가 있기는 하다. 그런 경우에는 다음과 같은 일이 일어난다. x+3.0은 double형 변수가 아니므로, 프로그램은 이름없는 임시변수를 만들어 그것을 표현식 x+3.0의 값으로 초기화한다. 그렇게 되었을 때 형식 매개변수는 그 임시변수에 대한 참조가 된다.

C++는 실제 전달인자와 참조 전달인자가 일치하지 않을 때 임시변수를 생성할 수 있다. 최근의 C++는 전달인자가 const참조일 경우에만 이것을 허용한다. 이것은 새로운 제약이다. 참조 매개변수가 const일 경우, 컴파일러는 다음과 같은 두가지 상황에서 임시변수를 생성한다.

  • 실제 전달인자가 올바른 데이터형이지만 lvalue가 아닐 때
  • 실제 전달인자가 잘못된 데이터형이지만 올바른 데이터형으로 변환할 수 있을 때

lvalue전달인자는 참조가 가능한 데이터 객체이다. 예를 들어 변수, 배열의 원소, 구조체의 멤버, 참조, 내용참조 포인터는 lvalue이다. 일반상수와 여러개의 항으로 이루어진 표현식은 lvalue가 아니다. const double형의 참조 형식매개변수가 존재할 때, 데이터형이 일치하지 않거나, 상수, 상수표현식은 임시변수를 생성하고 형식 매개변수가 이를 참조하게 만든다. 임시변수는 함수가 호출되어 있는 동안 유지되지만, 그 후에 컴파일러는 그것을 마음대로 없앨 수 있다.

이러한 행동이 상수참조일 경우에는 옳지만 다른 경우에는 옳지 않다. 예를 들어 두 변수의 값을 교환하는 스왑함수의 경우, 함수 내에서 임시변수의 내용의 교환이 실제 전달된 변수의 교환으로 이루어지지는 않는다. 간단히 말해서, 참조 전달인자를 가진 함수의 목적이 전달인자로 전달되는 변수를 변경하는 것이라면, 임시변수의 생성은 그 목적을 방해한다. 이것을 해결하는 방법은 이러한 상황에서는 임시변수의 생성을 허용하지 않는 것이다. 최근의 C++표준은 그렇게 하고 있다. 그러나 일부 컴파일러들은 여전히 에러메시지 대신 경고메시지를 디폴트로 내보낸다. 따라서 임시변수와 관련된 경고가 나왔을 때, 절대 그것을 무시하면 안된다.

참조 전달인자를 상수데이터에 대한 참조로 선언하는 이유는 다음과 같은 세가지 이점이 있기 때문이다.

  • const를 사용하면, 실수로 데이터변경을 일으키는 프로그래밍 에러를 막을 수 있다.
  • 원형에 const를 사용하면, 함수가 const와 const가 아닌 실제 전달인자를 모두 처리할 수 있지만, 원형에 const를 생략한 함수는 const가 아닌 데이터만 처리할 수 있다.
  • const참조를 사용하면, 함수가 자신의 필요에 따라 임시변수를 생성하여 사용할 수 있다.

따라서 가능하면 참조형식 매개변수를 const로 선언하는 것이 좋다.

구조체에 대한 참조

참조는 C++의 사용자정의 데이터형인 구조체나 클래스를 다루는데 아주 유용하게 사용된다. 참조는 기본 데이터형보다 주로 그러한 사용자정의 데이터형에 사용하기 위해 도입된 것이다.

struct sysop { ... } 
sysop looper { ... , int used; }; 
sysop copycat; 

use(looper); 
copycat = use(looper); 
use(looper).used

const sysop &use(sysop &sysopref) { ... , return sysopref; }

구조체를 참조로 전달하여 함수를 수행하므로 구조체 멤버가 변경된다. 일반적으로 함수의 리턴값은 임시저장영역에 복사되고, 호출프로그램이 그것을 사용한다. 그러나 참조를 리턴하면 호출프로그램은 복사본이 아니라 리턴값을 직접 사용한다. 리턴되는 참조는 처음에 함수에 전달했던 바로 그 참조를 말하므로, 호출프로그램은 실제로는 자기 자신의 변수를 직접 사용하게 된다. 예를 들면, sysopref는 looper에 대한 참조이므로, 리턴값은 main()에 있는 looper변수 자체가 된다.

참조를 리턴할 때 기억해야 할 가장 중요한 것은, 함수가 종료할 때 수명이 함께 끝나는 메모리 위치에 대한 참조를 리턴하지 않도록 조심하는 것이다. 다음과 같은 코드를 사용하지 말아야 한다.

const sysop& clone2(sysop &sysopref) { 
    sysop newguy;                  // 큰 에러를 일으키는 첫걸음 
    newguy = sysopref;             // 정보를 복사한다. 
    return newguy;                 // 복사본에 대한 참조를 리턴한다. 
}

이 코드는 함수가 종료할 때 함께 사라질 운명인 임시변수에 대한 참조를 리턴하는 불행한 실수를 저지르고 있다. 마찬가지로, 그러한 임시변수를 지시하는 포인터를 리턴하는 것도 피해야 한다. 이 문제를 피하는 가장 간단한 방법은, 함수에 전달인자로 전달된 참조를 리턴하는 것이다. 참조 매개변수는 호출함수가 사용하는 데이터를 참조한다. 그러므로 리턴되는 참조도 동일한 그 데이터를 참조한다.

두번째 방법은 new를 사용하여 새로운 기억공간을 만드는 것이다.

const sysop& clone(sysop &sysopref) { 
    sysop *psysop = new sysop; 
    &psysop = sysopref; 
    return *psysop; 
}

이 코드는 언뜻 보면 구조체를 리턴하는 것처럼 보이지만, 함수선언을 살펴보면 실제로는 구조체에 대한 참조를 리턴하는 것이다. 그러면 이제 이 함수를 다음과 같은 방법으로 사용할 수 있다.

sysop &jolly = clone(looper);

여기서 한가지 주의할 점은 할당된 메모리를 delete로 반드시 삭제해 주어야 한다는 점이다.

함수의 리턴형을 const로 지정하라. 이것은 sysop구조체 자체가 const라는 것을 의미하지는 않는다. 이것은 단지 그 구조체를 변경하는데 리턴값을 직접 사용할 수 없다는 것을 의미한다. 예를 들어, const를 생략하면 다음과 같은 코드를 사용할 수 있다.

use(looper).used = 10;

sysop newgal = { ... }; 
use(looper) = newgal;              // looper = newgal;

요약하면, const를 생략하면 짧지만 더 모호해 보이는 코드를 작성할 수 있다.

일반적으로 모호한 코드는 모호한 에러를 일으킬 기회를 높이기 때문에 모호한 코드를 추가하지 않도록 하는 것이 좋다. 리턴형을 const참조로 만드는 것은 프로그래머를 혼미의 유혹으로부터 보호한다. 때로는 const를 생략하는 것이 옳을 때도 있다. <<오버로딩 연산자가 그 예다.

클래스 객체와 참조

일반적으로 C++는, 클래스 객체를 함수에 전달할 때 참조를 사용한다. 예를 들면, string, ostream, istream, ofstream, ifstream클래스의 객체를 전달인자로 취하는 함수들에 참조 매개변수를 사용할 수 있다.

string version1(const string &s1, const string &s2) { 
    string temp; 
    temp = s2 + s1 + s2; 
    return temp; 

const string& version2(string &s1, const string &s2) {      // 참조를 리턴한다. 
    s1 = s2 + s1 + s2; 
    return s1; 
}

const string& version3(string &s1, const string &s2) {      // 나쁜 설계 
    string temp; 
    temp = s2 + s1 + s2; 
    return temp;                                            // 사용되지 않을 메모리를 리턴 
}

const string형 매개변수에 const char *형을 전달인자로 하는 것을 프로그램은 허용한다. 이것에는 두가지 이유가 숨어있다. 첫째, string클래스가 char *형을 string으로 변환하는 것을 정의하고 있다. 이것이 string객체를 C스타일 문자열로 초기화하는 것을 가능하게 한다. 둘째, 앞에서 설명한 const 참조형식 매개변수의 특성때문이다. 살전달인자의 데이터형이 참조 매개변수의 데이터형과 일치하지 않지만, 참조 데이터형으로 변환될 수 있다고 가정하자. 그러면 프로그램은 올바른 데이터형의 임시변수를 만들고, 변환된 값으로 그것을 초기화한고, 임시변수에 참조를 전달한다.

결과적으로 형식 매개변수가 const string &형이면, string객체 또는 큰따옴표로 묶은 문자열 리터럴, 널 문자로 종결된 char형의 배열, char형을 지시하는 포인터변수와 같은 C스타일 문자열이 함수 호출에 사용되는 실전달인자가 될 수 있다. 그러므로 다음과 같은 행은 바르게 동작한다.

result = version1(input, "***");                            // input은 string형 객체

객체, 상속, 참조

ostream과 ofstream클래스는 참조의 흥미있는 특성을 전면에 부각시킨다. ofstream형 객체들은 ostream메서드를 사용할 수 있다. 그들은 콘솔입력 / 출력과 동일한 형식을 파일입력 / 출력에 사용하는 것을 허용한다. 한 클래스에서 다른 클래스로 기능을 전달하는 것을 가능하게 하는 C++언어의 기능을 상속(inheritance)이라 부른다. 간단히 말해서 ostream을 기초클래스(base class)라 하고 (ofstream클래스가 그것에 기초하고 있기 때문에), ofstream을 파생클래스(derived class)라 한다(ostream으로부터 파생되었기 때문에). 파생클래스는 기초클래스의 메서드들을 상속한다. 이것은 ofstream객체가 precision(), setf()등의 포맷팅 메서드와 같은 기초클래스의 기능들을 사용할 수 있다는 것을 의미한다.

상속의 또 한가지 측면은, 데이터형 변환없이 기초클래스 참조가 파생클래스 객체를 참조할 수 있다는 것이다. 이것의 실제적인 결과는, 기초클래스 참조 매개변수를 가지는 함수를 정의하고, 그 함수를 기초클래스에도 사용하고 파생객체에도 사용할 수 있다는 것이다. 예를 들면, ostream &형 매개변수를 가지는 함수는, cout과 같은 ostream객체와 사용자가 선언하는 ofstream객체를 동등하게 받아들일 수 있다.

ofstream fout; 
const char *fn = "ep-data.txt"; 
fout.open(fn);

file_it(fout, objective, eps, LIMIT); 
file_it(cout, objective, eps, LIMIT);

void file_it(ostream &os, double fo, const double fe[], int n) { 
    os << ... 
}

동일한 내용이 파일에, 그리고 모니터에 디스플레이된다. 이 예제의 중요한 점은 ostream &형인 os매개변수가 cout과 같은 ostream객체와 fout과 같은 ofstream객체를 참조할 수 있다는 것이다.

참조 전달인자를 사용하는 주된 이유는 다음 두가지이다.

  • 호출함수에 있는 데이터 객체의 변경을 허용하기 위해
  • 전체 데이터 객체 대신에 참조를 전달하여 프로그램의 속도를 높이기 위해

두번째 이유는 구조체나 클래스 객체와 같이 덩치 큰 데이터 객체를 다룰 때 가장 중요하다. 위의 두가지 이유는 포인터를 전달인자로 사용하는 것과 같은 이유이다. 사실상 참조 전달인자는 포인터를 사용하도록 짜여진 코드의 또 다른 인터페이스이기 때문에, 이것은 이치에 맞는다. 참조와 포인터는 어떤 때 구별하여 사용할 수 있을까? 다음은 이를 위한 몇가지 지침이다.

함수가 전달된 데이터를 변경하지 않고 사용만 하는 경우,

  • 데이터 객체가 기본 데이터형이나 작은 구조체라면 값으로 전달한다.
  • 데이터 객체가 배열이라면 포인터가 유일한 선택이므로 포인터를 사용한다. 포인터를 const를 지시하는 포인터로 만든다.
  • 데이터 객체가 덩치 큰 구조체라면 const포인터나 const참조를 사용하여 프로그램의 속도를 높인다. 이것은 구조체나 클래스 설계를 복사하는데 드는 시간과 공간을 절약한다. 포인터나 참조를 const로 만든다.
  • 데이터 객체가 클래스 객체라면 const참조를 사용한다. 클래스 설계 자체가 흔히 참조를 사용할 것을 요구한다. 이것이 C++에 const기능을 추가한 주된 이유이기도 하다. 클래스 객체 전달인자의 전달은 참조로 전달하는 것이 표준이다.

함수가 호출함수의 데이터를 변경하는 경우

  • 데이터 객체가 기본 데이터형이면 포인터를 사용한다.
  • 데이터 객체가 배열이면 유일한 선택은 포인터를 사용하는 것이다.
  • 데이터 객체가 구조체이면 참조 또는 포인터를 사용한다.
  • 데이터 객체가 클래스 객체이면 참조를 사용한다.

이것은 단지 지침일 뿐이다. 다른 방식으로 선택한다면 그 나름대로의 이유가 있을 것이다. 예를 들어, cin은 cin >> &n 대신에 cin >> n을 사용할 수 있도록 기본형에 대한 참조를 사용한다.

, .