ShovelingLife
A Game Programmer
ShovelingLife
전체 방문자
오늘
어제
  • 분류 전체보기 (1066) N
    • 그래픽스 (57)
      • 공통 (19)
      • 수학 물리 (22)
      • OpenGL & Vulkan (1)
      • DirectX (14)
    • 게임엔진 (179) N
      • Unreal (69)
      • Unity (99) N
      • Cocos2D-X (3)
      • 개인 플젝 (8)
    • 코딩테스트 (221)
      • 공통 (7)
      • 프로그래머스 (22)
      • 백준 (162)
      • LeetCode (19)
      • HackerRank (2)
      • 코딩테스트 알고리즘 (8)
    • CS (235)
      • 공통 (21)
      • 네트워크 (44)
      • OS & 하드웨어 (55)
      • 자료구조 & 알고리즘 (98)
      • 디자인패턴 (6)
      • UML (4)
      • 데이터베이스 (7)
    • 프로그래밍 언어 (346)
      • C++ (167)
      • C# (88)
      • Java (9)
      • Python (33)
      • SQL (30)
      • JavaScript (8)
      • React (7)
    • 그 외 (9)
      • Math (5)
      • 일상 (5)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

  • Source Code 좌측 상단에 복사 버튼 추가 완료
  • 언리얼 엔진 C++ 빌드시간 단축 꿀팁
  • 게임 업계 코딩테스트 관련
  • 1인칭 시점으로 써내려가는 글들

인기 글

태그

  • string
  • 포인터
  • 유니티
  • 클래스
  • 알고리즘
  • 배열
  • 문자열
  • 프로그래머스
  • c#
  • 티스토리챌린지
  • 언리얼
  • Unity
  • C++
  • 백준
  • 파이썬
  • 함수
  • SQL
  • C
  • 그래픽스
  • 오블완

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
ShovelingLife

A Game Programmer

C++ 참조자(Reference)의 이해
프로그래밍 언어/C++

C++ 참조자(Reference)의 이해

2023. 8. 21. 19:08

참조자(레퍼런스)란 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=&num;
    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;
}

 

코딩맛집 :: 02-3. 참조자(Reference)의 이해 (tistory.com)

저작자표시 (새창열림)

'프로그래밍 언어 > 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
    '프로그래밍 언어/C++' 카테고리의 다른 글
    • [C] 정적변수, 지역변수, 전역변수 비교 (static, local, global)
    • [C++] 테스트용 map<int, 포인터배열>
    • C Call-by-Value(값에 의한 호출) & Call-by-Reference(참조에 의한 호출)의 이해
    • [C++] 문자열 인코딩 (유니코드 멀티바이트 UTF-8 변환)
    ShovelingLife
    ShovelingLife
    Main skill stack => Unity C# / Unreal C++ Studying Front / BackEnd, Java Python

    티스토리툴바