728x90
728x90
C에 없던 참조자

참조자(레퍼런스)란 C에서는 없던, C++에서 새로 생긴 개념.
포인터랑 의도하는 바는 같은데 포인터의 단점이 보완되어 출시된 것.
C++ 문서에서는 포인터보다 특정 경우가 아니라면 대부분 참조자를 사용하길 권장한다.

 

값으로 전달하는 방식의 한계

1. 큰 구조체나 클래스를 함수에 전달할 때 인수의 복사본을 매개변수로 만든다.

2. 함수의 호출자에 값을 전달하는 건 반환값을 사용하는 게 유일한 방법이나
함수에서 인수를 수정하는 게 확실하고 효율적이다.

→ 그래서 참조를 통해 문제를 해결한다.

 

변수를 참조로 전달하려면 매개변수를 참조로 선언한다.
함수가 호출되면 y는 인수에 대한 참조가 된다.

int x=5;
addOne(x);

//int &y=x; 이런 의미

void addOne(int& y)
{
	y=y+1;
}

 

 

참조자는 변수에 n개로 별명을 붙여준 것

참조자의 수에는 제한이 없다.
참조자를 대상으로 참조자를 선언할 수 있다.
한번 선언된 참조자의 대상은 바꿀 수 없다. (포인터는 가능)
선언과 동시에 무조건 할당되어야 하며 포인터처럼 null은 불가하다.

 

https://wonjayk.tistory.com/253 :: 포인터와 레퍼런스 비교

 

 

&연산자의 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-reference2)참조자를 이용한 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 키워드를 사용한다. 


int num=24;
Ha....

 

 

반환형이 참조형인 경우

#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.5. malloc&free를 대신하는 new&delete


추가적인 메모리공간을 실시간으로 확보해야할 예 : 채팅 대화방에 2명이 있는데 3명이 됐다던가, 프로그램에 자료를 추가 입력해 저장하는 등의 예가 있다.

 

C의 malloc / free : 힙의 메모리 할당 및 소멸에 필요한 함수

힙 영역에 동적으로 메모리를 할당하는 함수로 함수 호출 시 할당하고자 하는 메모리의 크기를 바이트 단위로 전달하면 그 크기만큼 메모리 할당 후 할당한 메모리의 주소 즉 첫 번째 바이트의 주소를 리턴한다. 메모리 할당에 실패하면 NULL 리턴. 반환값이 주소값이어서 포인터로 받아야 한다.

C의 동적할당 예시로, 길이정보를 인자로 받아 해당 길이의 문자열 배열을 생성하고, 그 배열의 주소값을 반환한다.

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string.h>
#include <stdlib.h>

using namespace std;

char* MakeStrAdr(int len) {
    char* str = (char*)malloc(sizeof(char)*len); 
    //문자열 저장을 위한 배열을 힙에 할당중
 
    return str;
}

int main(void)
{
    char* str = MakeStrAdr(20);
    strcpy(str, "I am so");
    cout << str << endl;
    free(str);//힙에 할당한 메모리공간 소멸
    
    return 0;
}

 

단점 : 1) 할당할 대상의 정보를 바이트 단위로 전달해야 하며 2) 반환형이 void형 포인트라 적절한 형변환을 거쳐야 한다 → 보완된 게 C++ new, delete 이다.

 

C calloc

C malloc : 할당 메모리를 초기화하지 않아 쓰레기값이 들어있음 
C calloc : 할당된 메모리공간을 모두 0으로 초기화시킴

void* calloc(size_t n, size_t size); // 메모리의 단위 갯수, 하나당 크기 (예 : 4개 * 4바이트)
char* p1 = (char*)malloc(sizeof(char)*10));
char* p2 = (char*)calloc(10, sizeof(char));



void포인터 (void*)

어떤 변수의 주소값을 담을 것인지 지정된 타입이 없는 것
추후 casting을 통해 타입을 바꾸어 줄 수 있다.
메모리의 주소만 있고 데이터형이 없는 것

 

 

C realloc()

malloc이나 calloc으로 메모리를 할당한 뒤 이 공간의 크기를 조절하기 위해 사용한다.

void* realloc(void*p, size_t size);
char* p = (char*)malloc(sizeof(char)*10); 
p = (char*)realloc(p, 12); // 메모리 공간 확장

 

 

new와 delete 연산자

malloc, free함수에 비해 사이즈 계산, 포인터형 변환의 필요가 줄었다.

new 연산자 : 데이터형 저장하기 위한 메모리공간을 힙에 할당
필요 메모리공간만큼 자동계산해서 힙에 할당 후 알아서 맞는 데이터형 포인터로 주소값 리턴한다.

* 주의 : C에서 malloc 해지는 단순히 free()를 썼지만 C++에서는 할당한 메모리공간 해지로 delete 를 쓸 때 변수의 경우는 같지만, 배열인 경우 명시적으로 인덱스 연산자를 붙인다.

 

new 사용방법

할당할 대상의 정보를 직접 명시한다. 

int * ptr1=new int;
int * arr1=new int[3];

 

delete 사용방법

new 연산 시 반환된 주소 값을 대상으로 delete 연산을 진행한다.

delete ptr1;
delete []arr1;

 

C++ 동적할당 예시
#define _CRT_SECURE_NO_WARNINGS 
#include <iostream>
#include <string.h>
using namespace std;

char* MakeStrAdr(int len) {
    char* str = new char[len];
    return str;
}

int main(void)
{
    char* str = MakeStrAdr(20);
    strcpy(str, "I am so");
    cout << str << endl;
    delete[]str;
    return 0;
}

 

 

C++에서는 malloc 과 free 함수의 호출이 문제가 될 수 있다

new와 malloc 함수의 동작방식에는 차이가 있다...만 기억하고
나머지는 클래스, 객체, 생성자에 대해 알고나면 정확히 이해할 수 있다

//#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <stdlib.h>
using namespace std;

class Simple 
{
    public : 
        Simple()
        {
            cout << "am simple consturctor" << endl;
        }
};

int main(void)
{
    cout << "case 1";
    Simple* sp1 = new Simple;  //힙 영역에 변수 할당

    cout << "case 2";
    Simple* sp2 = (Simple*)malloc(sizeof(Simple) * 1); //힙 영역에 변수 할당
    
    cout << endl << "end of main" << endl;
    
    delete sp1; //소멸
    free(sp2); //소멸

    return 0;
}
//#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <stdlib.h>
using namespace std;

class Simple 
{
    private:
        string m_strmyname;
        int m_myage;
        int* m_p;

    public : 
        Simple()        // 생성자.
            : m_strmyname("햄버거")
            , m_myage(12)
            , m_p(nullptr)
        {
            m_strmyname = "롯데리아";
            m_p = new int();
            cout << "am simple consturctor" << endl;
        }

        ~Simple()        // 소멸자
        {
            if (m_p)
            {
                delete m_p;
                m_p = nullptr;
            }
            cout << "am simple disturctor" << endl;
        }

        void myfunc()
        {

        }
};

int main(void)
{
    cout << "case 1";
    Simple* sp1 = new Simple;  //힙 영역에 변수 할당

    cout << "case 2";
    Simple* sp2 = (Simple*)malloc(sizeof(Simple) * 1); //힙  영역에 변수 할당
    
    cout << endl << "end of main" << endl;
    
    delete sp1; //소멸
    free(sp2); //소멸

    return 0;
}

 

 

malloc을 잘못 사용하게 되면 오류나 메모리 낭비를 하게 될 가능성이 높아진다. (출처)

 

더보기 (malloc/free, new/delete)

 

[c++] malloc/free, new/delete

malloc / free - malloc 은 "stdlib.h" 헤더파일에 포함되어 있다. - malloc 함수는 입력받은 바이트 크기만큼 메모리 공간을 Heap Memory에 할당해준다. 그리고 void *를 리턴하므로 sizeof 연산자와 캐스트 연산..

armful-log.tistory.com

 

 

 

힙에 할당된 변수 포인터 없이 접근해서 값 변경하기

참조자의 선언은 상수아닌 변수를 대상으로만 가능하다. (const참조자 제외)
new 연산자를 이용해 할당된 메모리 공간에도 참조자의 선언이 가능하다. 변수와 같은 특징을 지녔으므로
(메모리 공간할당, 이름 존재)

int *ptr=new int;
int &ref=*ptr; //힙 영역에 할당된 변수에 대한 참조자 선언
ref=20;
cout<<*ptr<<endl; //20

 

 

null포인터 리턴하는 new연산자

원하는 메모리 공간을 할당하지 못하는 경우(메모리 부족) NULL 포인터를 리턴한다.
새로운 표준에 관한 내용은 예외처리를 통해 다시 언급한다

// OS가 좋아져서 이 방식으로는 잘 안한다
int* arr=new int[size]; //배열의 동적할당
if(arr=NULL) { //동적할당검사
    cout<<"메모리할당실패"<<endl;
    // 0이면 메모리가 꽉 참
    return -1; //프로그램 종료
}
#include <iostream>

// 이걸로 조절
#define DEBUG 1;
// #define DEBUG 0;

//조건부컴파일
int main()
{
    int size;
    std::cout<<"할당하고자 하는 배열의 크기: ";
    std::cin>>size;
    
    int* arr=new int[size]; //배열의 동적할당
        
#if DEBUG==1
    cout<<"디버그 모드 입니다"<<endl;
    if(arr=NULL) 
    {
        cout<<"메모리할당 실패"<<endl;
        return  -1; //프로그램 종료
    }
    
#endif
    for(int i=0; i<size; i++)
        arr[i]=i+10;
        
        ....
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
 
void main()
{
    int* pPoint;
 
    int nCount = 0;
 
    printf("malloc size ?? ");
    scanf("%d", &nCount);
 
    pPoint = (int*)malloc(sizeof(int) * nCount);
 
    int i = 0;
    for (i = 0; i < nCount; i++)
    {
        printf("input pPoint[%d] : ", i);
        scanf("%d", &pPoint[i]);
    }
 
    for (i = 0; i < nCount; i++)
        printf("Output pPoint[%d] : %d\n", i, pPoint[i]);
 
    free(pPoint);
}

 

 

 

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,

v