C에 없던 참조자
참조자(레퍼런스)란 C에서는 없던, C++에서 새로 생긴 개념.
포인터랑 의도하는 바는 같은데 포인터의 단점이 보완되어 출시된 것.
C++ 문서에서는 포인터보다 특정 경우가 아니라면 대부분 참조자를 사용하길 권장한다.
값으로 전달하는 방식의 한계
1. 큰 구조체나 클래스를 함수에 전달할 때 인수의 복사본을 매개변수로 만든다.
2. 함수의 호출자에 값을 전달하는 건 반환값을 사용하는 게 유일한 방법이나
함수에서 인수를 수정하는 게 확실하고 효율적이다.
→ 그래서 참조를 통해 문제를 해결한다.
변수를 참조로 전달하려면 매개변수를 참조로 선언한다.
함수가 호출되면 y는 인수에 대한 참조가 된다.
int x=5;
addOne(x);
//int &y=x; 이런 의미
void addOne(int& y)
{
y=y+1;
}
참조자는 변수에 n개로 별명을 붙여준 것
참조자의 수에는 제한이 없다.
참조자를 대상으로 참조자를 선언할 수 있다.
한번 선언된 참조자의 대상은 바꿀 수 없다. (포인터는 가능)
선언과 동시에 무조건 할당되어야 하며 포인터처럼 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 키워드를 사용한다.
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)
힙에 할당된 변수 포인터 없이 접근해서 값 변경하기
참조자의 선언은 상수아닌 변수를 대상으로만 가능하다. (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);
}
'C, C++ > 열혈 C++ 프로그래밍' 카테고리의 다른 글
3. 클래스의 기본 (0) | 2021.09.15 |
---|---|
02. C언어 기반의 C++ 연습문제 (0) | 2021.09.14 |
[윤성우 열혈 c++] OOP 단계별 프로젝트 01단계 (0) | 2021.08.19 |
02-1 챕터 02의 시작에 앞서 (0) | 2021.08.19 |
01-5 이름공간 (0) | 2021.08.19 |