참조자(레퍼런스)란 C에서는 없던, C++에서 새로 생긴 개념.
포인터랑 의도하는 바는 같은데 포인터의 단점이 보완되어 출시된 것.
C++ 문서에서는 포인터보다 특정 경우가 아니라면 대부분 참조자를 사용하길 권장한다.
값으로 전달하는 방식의 한계
1. 큰 구조체나 클래스를 함수에 전달할 때 인수의 복사본을 매개변수로 만든다.
2. 함수의 호출자에 값을 전달하는 건 반환값을 사용하는 게 유일한 방법이나
함수에서 인수를 수정하는 게 확실하고 효율적이다.
→ 그래서 참조를 통해 문제를 해결한다.
변수를 참조로 전달하려면 매개변수를 참조로 선언한다.
함수가 호출되면 y는 인수에 대한 참조가 된다.
int x=5;
addOne(x);
//int &y=x; 이런 의미
void addOne(int& y)
{
y=y+1;
}
참조자의 수에는 제한이 없다.
참조자를 대상으로 참조자를 선언할 수 있다.
한번 선언된 참조자의 대상은 바꿀 수 없다. (포인터는 가능)
선언과 동시에 무조건 할당되어야 하며 포인터처럼 null은 불가하다.
&연산자의 2가지 사용 : 변수의 주소값 vs 참조자 선언
같은 &지만, 이미 선언된 변수 앞에 &연산자가 오면 변수의 주소값을 반환하고
새로 선언한 변수의 이름 앞에 &연산자가 오면 참조자의 선언을 의미한다.
또한 이 경우 동시에 null 외의 값을 할당한다.
int num1=2020;
int *ptr=&num1; // num1 변수의 주소값을 반환해서 포인터 ptr에 저장
int &num2=num1; // num1 변수에 대한 참조자 num2를 선언
num2=3047;
cout<<num1<<endl; // 3047
cout<<num2<<endl; // 3047
cout<<&num1<<endl; // 주소값이 같다
cout<<&num2<<endl;
참조자(레퍼런스)와 변수
포인터와 참조자 둘 다 특정 메모리공간을 접근하기 위한 이름이다.
C언어는 특정 공간에 하나의 이름만, C++ 은 특정 공간에 여러 이름을 부여할 수 있다.
그래서 한 변수를 참조할 참조자의 수에는 제한이 없다.
그러나 포인터처럼 선언된 참조자의 대상은 바꿀 수 없다.
지역적 참조자는 지역 변수처럼 함수를 빠져나가면 소멸된다.
int val=20;
int &ref=val;
//val == &ref
int val=20;
int &ref1=val;
int &ref2=ref1;
int &ref2=val;
// 2개는 같다
레퍼런스 값 변경으로 원본 값 변경이 가능하다. (원본 값의 별칭이므로)
참조 변수의 범위에는 배열 요소도 들어간다.
배열요소는 변수로 간주되어 참조자 선언이 가능하다.
#include <iostream>
using namespace std;
int main()
{
int arr[3]={1,3,5};
int &ref1=arr[0];
int &ref2=arr[1];
int &ref3=arr[2];
cout<<ref1<<endl;
cout<<ref2<<endl;
cout<<ref3<<endl;
return 0;
}
포인터 변수 참조자
#include <iostream>
using namespace std;
int main()
{
int num=12;
int *ptr=#
int **dptr=&ptr;
int &ref=num; //12
int *(&pref)=ptr; //ptr의 참조자
int **(&dpref)=dptr;
cout<<ref<<endl; //12
cout<<*pref<<endl; //12
cout<<**dpref<<endl; //12
return 0;
}
참조자의 장점
1. &, *, 등의 연산자를 안써서 깔끔한 코드가 완성된다.
2. 포인터의 경우 발생할 수 있는 엉뚱한 메모리 수정 사고를 예방할 수 있다.
3. (나중에 배워서 알고만 넘어갈) 벡터 관련
4. 복사생성자가 발생되지 않아 메모리공간이 절약된다.
참조자(레퍼런스) 한계점
- 변수와 차이점
1. 이름이 없는 대상(상수, NULL 등)을 레퍼런스 할 수 없다.
1)매개변수에 NULL 포인터를 넘겨주는 것이나 2)리턴값으로 NULL 포인터 반환이 안된다.
2. 참조의 대상 변경도 불가하다.
int &ref1; //초기화 안되서 에러
int &ref2=10; //상수가 올 수 없어서 에러
int &ref=NULL; //포인터변수 선언처럼 역시 안됨
참조자의 활용에는 함수가 큰 위치를 차지한다.
- call-by-value : 값을 인자로 전달하는 함수의 호출방식
- call-by-reference : 주소 값을 인자로 전달하는 함수의 호출방식
call-by-value기반의 함수는 아래와 같고, 2개의 정수를 인자로 요구한다.
함수 내에서는 함수 외부에 선언된 변수에 접근이 불가하며 두 변수에 저장된 값을 외부에서 바꿀 수 없다.
그래서 call-by-reference 기반의 함수가 필요하다.
int Adder(int num1, int num2) {
return num1+num2;
}
call-by-reference 기반의 함수는 변수의 주소값을 받아서 주소값이 참조하는 영역에 저장된 값을 직접 변경할 수 있다.
void SwapByRef(int* ptr1, int* ptr2)
{
int temp=*ptr1;
*ptr1=*ptr2;
*ptr2=temp;
}
// ... 이런식으로 호출
// SwapByRef(&val1, &val2);
return 부분의 코드가 없었다면 둘 다 가능하겠지만 다음처럼 정의되면 Call-by-value 방식이다.
함수의 연산 주체가 주소값일 뿐 value다.
주소값을 이용해서 함수 외부에 선언된 변수에 접근하는 call-by-reference 방식과는 거리가 멀다.
int * SimpleFunc(int * Ptr)
{
return ptr+1; //주소값을 증가시켜서 반환
}
아래는 주소 값을 이요해 함수 외부에 선언된 변수를 참조하니 call-by-reference 방식
다시 정리하면 call-by-reference는 "주소 값을 전달받아서 함수 외부에 선언된 변수에 접근하는 형태의 함수호출"
주소 값이 참조의 도구로 사용됐다는 것이 판단 기준
int * SimpleFunc(int * ptr)
{
if(ptr==NULL) {
return NULL;
}
*ptr=20;
return ptr;
}
call-by-reference 는 1)주소값을 이용한 call-by-reference , 2)참조자를 이용한 call-by-reference 두가지 방식이 있다.
레퍼런스를 이용한 call-by-reference
함수 외부에 선언된 변수의 접근 가능
포인터 연산을 할 필요가 없어 안정적
함수의 호출 형태 구분이 어렵다
메모리공간에 각각 대입. 레퍼런스로 받고 있다
val1이라는 이름이 붙은 메모리공간에 a라는 이름을 하나 더 붙여준 것이다.
#include <iostream>
using namespace std;
void SwapByRef2(int &ref1, int &ref2) {
int temp=ref1;
ref1=ref2;
ref2=temp;
}
int main()
{
int val1=10;
int val2=20;
SwapByRef2(val1, val2);
cout << "val1:" <<val1<< endl;
cout << "val2:" <<val2<< endl;
return 0;
}
포인터를 이용한 call-by-reference
함수 외부에 선언된 변수의 접근 가능
포인터 연산에 의해 가능하다
따라서 포인터 연산의 위험성 존재
// int v1, v2 대입한다고 가정하자
// 주소값을 인자로 전달할 것이다
// v1, v2의 값을 직접 바꾸는 예
void swap(int *a, int *b) {
int temp=*a;
*a=*b;
*b=temp;
}
포인터 연산의 위험성 예 : 예전이라면 운영체제 자체가 망가질 수도 있었다. 지금은 OS에 의해 막아짐.
void swap(int *a, int *b) {
int temp=*a;
a++; // 잘못된 코드 (4바이트만큼 증가) --지금은 프로그램 중단
*a=*b; // 줄줄이 잘못
*b=temp;
}
Call-by-value vs call-by-reference
#include <iostream>
using std::cout;
using std::endl;
using std::cin;
struct _Person {
int age;
char name[20];
char personalID[20];
}
typedef struct _Person Person;
// Person 구조체변수p를 인자로 받아서 출력해주는 기능.
void ShowData(Person p) {
// void ShowData(Person &p) {
// void ShowData(const Person &p) { //상수화로 안정적인 코드
cout<< "****** 개인정보 출력 ******"<<endl;
cout<< "이름 : "<<p.name<<endl;
cout<< "주민번호 : "<<p.personalID<<endl;
cout<< "나이 : "<<p.age<<endl;
// p.age = 20; //const를 붙이면컴파일 시 에러 발생
}
int main()
{
Person man;
cout<<"이름: ";
cin>>man.name;
cout<<"나이: ";
cin>>man.age;
cout<<"주민번호: ";
cin>>man.personalID;
ShowData(man); //call by value
// 많은 데이터 복사가 진행된다
// 레퍼런스 방식의 장점은
// &p 일때 레퍼런스 방식으로 변경, 데이터복사가 일어나지 않는다
// 이미 있는 메모리공간에 이름만 붙여준거라 속도가 빨라진다.
// 레퍼런스 방식의 단점은
// ShowData값을 바꾸면 원본 데이터가 변경될 수 있는데 (불안정적)
// 출력만 하니 딱히 문제가 없지만 상수화시키면 더 좋다
// 그래서 16줄에 const를 붙여준다. (상수화)
return 0;
}
참조자의 단점 : 함수의 호출문장만 보고도 함수의 특성을 판단할 수 있어야 하는데
함수의 원형을 확인하고, 확인결과 참조자가 매개변수의 선언에 와있다면
함수의 몸체까지 문장단위 확인이 필요하다.
이를 해결하려면 const 키워드를 사용한다.
반환형이 참조형인 경우
#include <iostream>
using namespace std;
// 매개변수가 참조자로 선언, 참조자를 반환
int& RefRetFuncOne(int &ref){
ref++;
return ref;
}
// 참조자를 반환해도 반환형은 참조형이 아님
// int RefRetFuncOne(int &ref){
// ref++;
// return ref;
// }
int main()
{
int num1=1;
//cout << "1:" <<num1<< endl;
int &num2=RefRetFuncOne(num1); // 참조자를 반환, 다시 참조자에 저장
// cout << "2:" <<num1<< endl;
// cout << "2:" <<num2<< endl;
num1++;
num2++;
cout << "num1:" <<num1<< endl;
cout << "num2:" <<num2<< endl;
cout << "주소:" << &num1 << &num2 << endl; //같은델 가리킨다
return 0;
}
레퍼런스를 리턴하는 함수의 정의 예제, 응용
#include <iostream>
int& increment(int &val)
// 2) int increment(int &val) //error
// 3) int increment(int &val) //error
{
// val은 지역변수라 값을 복사해서 리턴
val++;
return val;
//int형 vs int형 레퍼런스로 리턴?
//현재는 레퍼런스타입으로 리턴
}
int main()
{
int n = 10;
int &ref=increment(n);
// 레퍼런스로 받고 있다
// n, val, ref 이 같은 공간 가리키는 중)
// 2) increment에서 int 형을 리턴하면
// int &ref = 11; //상수가 리턴되게 된다
// 상수를 할당할 수 없으므로 컴파일에러
// 3) int ref=increment(n);
// int ref=11; //새로운 메모리공간
std::cout<<"n : "<<n<<std::endl;
std::cout<<"ref : "<<ref<<std::endl;
return 0;
}
작성하면 안되는 예시 (컴파일에러는 없다)
출력결과가 나오고 작동은 되나 나중에 문제의 소지가 있어서 이렇게 작성하지 않는다.
#include <iostream>
int& function(void)
{
int val = 10;
return val;
// 지역변수는 레퍼런스타입으로 리턴하면 안된다.
}
int main()
{
int &ref=function();
// val이 사라지면서 레퍼런스하던 대상이 사라진다
std::cout<<"ref : "<<ref<<std::endl;
return 0;
}
'프로그래밍 언어 > C++' 카테고리의 다른 글
[C] 정적변수, 지역변수, 전역변수 비교 (static, local, global) (0) | 2023.08.28 |
---|---|
[C++] 테스트용 map<int, 포인터배열> (0) | 2023.08.22 |
C Call-by-Value(값에 의한 호출) & Call-by-Reference(참조에 의한 호출)의 이해 (0) | 2023.08.21 |
[C++] 문자열 인코딩 (유니코드 멀티바이트 UTF-8 변환) (0) | 2023.08.10 |
[C++] *와 *& 연산자의 차이 (0) | 2023.08.10 |