-
[c++] C언어와 C++의 다른 부분(bool, 참조자, new/delete)a 2022. 3. 30. 09:10반응형
1. 키워드 const의 의미
- const int num=10;
정수형 num이 변하지 않는 값이다.
- const int* ptr1=&val1;
정수형 포인터 ptr1이 변하지 않는 값이다. ptr1으로 val1 값을 변경할 수 없다.
- int *const prt2 = &val2;
포인터 prt2가 상수가 되었다.
- const int * const ptr3 = &val3;
포인터 ptr3가 상수가 됐고 ptr3로 val3를 변경할 수 없다.
2. 실행중인 프로그램의 메모리 공간
실행중인 프로그램은 운영체제로부터 메모리 공간을 할당 받는데, 이는 데이터, 스택, 힙 영역으로 나뉜다. 각각의 영역에는 어떤 형태의 변수가 할당이 되는가?
- 데이터 : 전역변수가 저장되는 영역
- 스택 : 지역변수 맻 매개변수가 저장되는 영역
- 힙 : malloc 함수 호출에 의해 프로그램이 실행되는 과정에서 동적으로 할당이 이뤄지는 영역
- malloc & free : malloc 함수 호출에 의해 할당된 메모리 공간은 free함수 호출을 통해 소멸한다. free를 호출하지 않으면 해제되지 않는다.
3. call-by-value vs call-by-reference
함수의 호출은 '값에 의해서' 호출할 수 있고, '참조에 의해서' 호출할 수 있다. 둘의 차이는 무엇인가?
'값에 의한 호출은 함수 안에서 값을 복사해서 연산을 수행한다.
02-2 새로운 자료형 bool
bool형은 c언어에는 존재하지 않고, c++에만 존재한다.
1) true와 false
정수 0은 거짓을 의미하는 숫자이며, 0이 아닌 모든 정수는 '참'이 된다. 보통 참과 거짓을 구분하기 위해 다음과 같이 정의하는 게 보통이다.
#define TRUE 1 #define FALSE 0
그런데 C++에서는 참과 거짓을 나타내는 키워드 true와 false를 정의하고 있기 때문에 굳이 매크로 상수를 이용할 필요가 없다. 예제와 같이 참과 거짓을 나타낼 수 있다.
#include <iostream> using namespace std; int main(void){ int num = 10; int i =0; cout<<"true "<<true<<endl; cout<<"false "<<false<<endl; while(true){ cout<<i++<<' '; if(i > num)break; } cout<<endl; cout<<"sizeof 1"<<" "<<sizeof(1)<<endl; cout<<"sizeof 2"<<" "<<sizeof(0)<<endl; cout<<"sizeof true"<<" "<<sizeof(true)<<endl; return 0; }
true 1 false 0
0 1 2 3 4 5 6 7 8 9 10
sizeof 1 4
sizeof 2 4
sizeof true 1- C에서는 무한루프를 만들 때 while(1)이라고 했지만 c++에서는 1과 true를 쓸 수 있다.
- 실행결과에서 true를 출력하면 1이 나오고, false는 0이 나온다.
- true와 false의 데이터 크기는 1 바이트이다.
true와 false는 각각 1과 0이라는 숫자가 아닌 1바이트 크기의 데이터이다. 다만 이 둘을 출력하라고 했을 때 정수의 형태로 1과 0으로 변환되도록 정의돼 있을 뿐이다.
int num = true;
이 문장에서 true는 1로 변환이 돼 num에 저장된다.
2) 자료형 bool
true와 false는 그 자체로 참과 거짓을 의미하는 데이터이기 때문에, 이들 데이터의 저장을 위한 자료형이 별도로 정의되어 있다. true와 false를 가리켜 bool형 데이터라고 한다. bool은 int, float와 마찬가지로 자료형이기 때문에 변수로 선언할 수 있다.
bool isTrue = true;
bool형이 사용되는 예시는 다음과 같다.
#include <iostream> using namespace std; bool IsPositive( int num){ if(num <= 0) return false; else return true; } int main(){ bool isPos; int num; cout<<"INPUT A NUMBER : "; cin>>num; isPos = IsPositive(num); if(isPos) cout<<"It is positive"<<endl; else cout<<"It is negative"<<endl; return 0; }
- 함수의 반환형으로 bool을 넘겨주기로 정의했다. return값은 true 또는 false가 된다.
02-3 참조자(reference)의 이해
지금부터 설명하는 '참조자'라는 것은 성격 상 포인터와 비유되기 쉽다. 그러나 참조자는 포인터를 모르더라도 이해할 수 있는 개념이다.
1) 참조자의 이해
무엇을 가리켜 변수라 하는가? 다음은 익히 알고 있는 변수의 정의이다.
변수는 할당된 메모리 공간에 붙여진 이름이다. 그리고 그 이름을 통해서 해당 메모리 공간에 접근이 가능하다.
그렇다면, "할당된 하나의 메모리 공간에 여러 개의 이름이 붙여질 수 있을까" 라는 의문이 들 수 있다.
만약 이런 변수가 있다고 하자.
int num1 = 2022;
이 선언으로 num1이라는 이름을 가진 메모리 공간에 2022가 초기화 된다.
그런데 이 상황에서
int &num2 = num1;
num1이라는 이름이 붙은 메모리 공간에는 num2라는 이름이 하나 더 붙게 된다. & 연산자는 변수의 주소 값을 반환하는 연산자로 쓰이지만, 여기서는 참조자의 선언을 의미하게 된다.
이미 선언된 변수의 앞에 이 연산자가 붙으면 주소 값을 반환하고, 새로 선언된 변수 앞에서는 참조자임을 나타낸다.
int *ptr = &num1;
변수 num1의 주소 값을 반환해서 포인터 ptr에 저장하라
int &num2 = num1;
변수 num1에 대한 참조자 num2를 선언해라
따라서 num2는 num1의 참조자가 되며, num1이라는 이름이 붙은 메모리 공간에 num2라는 이름이 하나 더 붙은 꼴이 된다.
참조자를 사용하는 예시를 살펴보자.
#include<iostream> using namespace std; int main(void){ int num1 = 1020; int &num2 = num1; num2 = 3040; cout<<"value "<<num1<<endl; cout<<"reference "<<num2<<endl; cout<<"value "<<&num1<<endl; cout<<"reference "<<&num2<<endl; return 0; }
num1의 참조자 num2를 이용해 1020에서 3040으로 값을 변경했다. 그리고 num1과 num2가 가리키는 변수의 주소 값은 같다.
변수와 참조자의 사용은 거의 같다.
2) 참조자는 별칭입니다.
전통적으로 참조자를 다음과 같이 설명한다.
변수에 별명을 하나 붙여주는 것입니다.
즉, num1이 변수의 이름이라면, num2는 변수의 별명이다. ㅇ
3) 참조자의 수에는 제한이 없으며, 참조자를 대상으로도 참조자를 선언할 수 있다.
그러나 보통 필요하지 않다.
4) 참조자의 선언 가능 범위
참조자는 변수에 대해서만 선언이 가능하고, 선언됨과 동시에 누군가를 참조해야 한다.
틀린 예시를 살펴 보자.
- int &ref = 20;
상수를 상대로 참조할 수는 없다.
- int &ref;
선언과 동시에 누군가를 참조해야 한다.
- int &ref = NULL;
그리고 참조자로 배열의 요소를 참조하는 예시를 보자.
#include<iostream> using namespace std; int main(void){ int arr[3] = {1,3,5}; int &ref1 = arr[0]; cout<<ref1<<endl; return 0; }
배열 요소는 변수로 간주되어 참조자의 선언이 가능하다.
또,
참조자는 포인터 변수도 참조가 가능하다.
#include <iostream> using namespace std; int main(void){ int num=12; int *ptr = # int **dptr = &ptr; int &ref = num; int*(&pref) = ptr; int**(&dpref) = dptr; cout<<ref<<endl; cout<<*pref<<endl; cout<<**dpref<<endl; return 0; }
포인터 변수의 참조자 선언도 & 연산자를 하나 더 추가하는 형태로 선언할 수 있다.
pref는 포인터 변수 ptr의 참조자이므로 num에 저장된 값이 출력된다.
또, dpref는 포인터 변수 dptr의 참조자이므로 num에 저장된 값이 출력된다.
02-4 참조자와 함수
이번에는 참조자의 활용에 관해 이야기하고자 한다. 변수에 별명을 붙이는 개념의 참조자는 보통 함수에서 주로 활용된다.
1) Call-by-value & Call-by-reference
여러분이 c언어를 공부하면서 배운 함수의 두 가지 호출방식은 다음과 같다.
- call by value : 값을 인자로 전달하는 함수의 호출방식
- call by reference : 주소를 인자로 전달하는 함수의 호출방식
이 중에서, call-by-value 기반의 함수는 다음과 같이 정의된 함수를 의미한다.
void adder(int num1, int num2){ int temp = num1; num1 = num2; num2 = temp; }
-위 함수는 두 개의 정수를 인자로 요구하고 있다. 따라서 call-by-value 기반의 함수이다. 그런데 이런 형태의 함수의 내부에서는, 함수 외부에 선언된 변수에 접근이 불가능하다. 따라서 main 함수에서 adder 함수를 호출하면, 두 개의 값을 전달하지만 실제 main 함수 내부의 변수 값은 변하지 않는다.
main 함수의 변수에도 영향을 주기 위해, call-by-reference 형태의 함수가 필요하다.
int adder(int* ptr1. int* ptr2){ return *ptr1+*ptr2; }
이 함수는 두 개의 주소 값을 받아서, 그 주소 값이 참조하는 영역에 저장된 값을 직접 변경하고 있다. 함수에서 주소 값을 이용해 함수 외부에 선언된 변수를 참조했으니 분명 call-by-reference이다.
c에서 말하는 call-by-reference는 다음과 같다.
'' 주소 값을 전달 받아서, 함수 외부에 선언된 변수에 접근하는 형태의 함수 호출''
즉, 주소값이 외부 변수의 참조도구로 사용되는 함수의 호출을 뜻한다. 이렇듯 주소 값이 전달됐다는 게 중요한 게 아니라 주소 값이 참조의 도구로 사용됐다는 사실이 중요한 것이다.
c++에서는 함수 외부에 선언된 변수의 접근방법으로 두 가지가 존재하낟. 하나는 '주소 값'을 이용하는 방식이고, 다른 하나는 '참조자'를 이용하는 방식이다.
- 주소 값을 이용한 call-by-reference
- 참조자를 이용한 call-by-reference
2) 참조자를 이용한 call-by-reference
c++에서는 참조자를 기반으로도 call-by-reference의 함수 호출을 진행할 수 있다. 다음 함수를 보자.
void SwapByRef(int &ref1, int &ref2){ int temp = ref1; ref1 = ref2; ref2 = temp; }
int main(void){ int val1 = 10; int val2 = 20; SwapByRef(val1, val2); cout<<"val1 :"<<val1<<endl; cout<<"val2 :"<<val2<<endl; return 0; }
매개변수로 선언된 참조자 ref1과 ref2는 main함수에서 선언된 변수 val1과 val2의 또다른 이름이다. 그리고 SwapByRef 함수 내에서는 이 두 참조자를 통해서 값의 교환 과정을 거치므로 실제 val1과 val2에도 적용된다.
3) const 참조자가 필요한 이유
참조자를 사용해서 함수의 매개변수로 사용하는 경우, 함수의 원형을 확인해서 값의 변경이 일어나는지 확인해야 한다. 우리는 함수의 호출 문장만 보고도 값이 변경되는지 아닌지를 확인하고 싶다. 그래서 const로 이 함수 안에서 값의 변경이 일어나지 않음을 명시적으로 보여 줄 필요가 있다.
void Happy(const int &ref) {}
참조자 ref가 const로 선언이 됐다. 이는 함수 Happy 안에서는 값의 변경이 일어나지 않음을 의미한다.
4) 반환형이 참조형(reference type)인 경우
함수의 반환형에도 참조자 형이 선언될 수 ㅣㅇㅆ다.
#include <iostream> using namespace std; int& RefRetFunc(int &ref){ ref++; return ref; } int main(void){ int num = 1; int &num2 = RefRetFunc(num); num++; num2++; cout<<num<<endl; cout<<num2<<endl; return 0; }
- num2는 참조자이며 RefRetFunc 함수는 참조자를 반환한다.
4
4위와 같이 참조형으로 반환된 값을 참조자(num2)에 저장을 하게 되면, 참조의 관계가 하나 더 추가된다.
한 개의 정수 num을 num2, ref 참조자 별명이 붙은 것이다. 즉
int num =1;
int &ref = num; //매개변수로 선언된 ref가 정의되었을 때
int &num2 = ref; // 함수의 반환에서 일어나는 일그런데 RefRetFunc 함수의 매개변수인 ref는 지역변수와 같은 성질을 갖고 있어서 함수의 반환과 동시에 소멸한다. 즉, 함수가 return을 하면 ref는 없어지고 num을 가리키는 별명은 num2만 남는다.
그런데 num2를 변수로 선언하면 어떤 결과가 나올까?
#include <iostream> using namespace std; int& RefRetFunc(int &ref){ ref++; return ref; } int main(void){ int num=1; int num2 = RefRetFunc(num); // num2는 변수이다. cout<<num<<endl; cout<<num2<<endl; return 0; }
- 참조자가 반환이 되지만, num2라는 변수에 저장했다.
2
2num과 num2 모두 같은 값을 출력했지만,
num2+=100; num+=10;
위와 같은 연산을 해주면, 각각 12와 102를 출력한다.
즉, 함수의 인자로 전달된 것은 num이 맞고 이를 참조하는 ref가 생긴 것은 맞지만, num2라는 새로운 변수에 반환 값을 담음으로써 num과 num2라는 두 개의 변수가 있는 것이다.
int num = 1;
int &ref = num; //인자를 전달하면서 생기는 일
int num2 = ref; //함수의 반환 값을 저장이렇듯 반환형이 참조자인 경우, 반환 값을 무엇으로 저장하느냐에 따라서 그 결과에 차이가 있으므로 적절한 선택을 해야 한다.
이제 마지막으로 참조자르 반환하되, 반환형은 기본 자료형인 경우를 생각해보자.
#include <iostream> using namespace std; int RefRetFunc(int &ref){ ref++; return ref; } int main(void){ int num =1; int num2 = RefRetFunc(num); num++; num2+=100; cout<<num<<endl; cout<<num2<<endl; return 0; }
- 참조자를 반환하지만, 반환형이 int이므로 참조자가 참조하는 변수의 값이 반환된다.
3
102반환형이 int같이 기본자료형으로 선언이 되면, 그 함수의 반환 값은 반드시 변수에 저장해야 한다.
- int num2 = RefRetFunc(num) (0)
- int &num2 = RefRefFunc(num) (x)
기본 자료형이 반환되는 건 상수나 다름없기 때문이다.
5) 잘못된 참조의 반환
다음 함수의 문제점을 살펴보자
int& RetFunc(int n){ int num = 20; num += n; return num; }
지역변수 num에 저장된 값을 반환하지 않고, num을 참조형으로 반환하고 있다. 이처럼 지역변수를 참조형으로 반환하는 일은 없어야 한다. 소멸될 데이터를 참조해서 정상적인 결과를 기대할 수 없기 때문이다.
6) const 참조자의 또 다른 특징
const와 관련해서 보충할 내용이 있다.
const int num = 20;
int &ref = num;
ref+=10;
cout<<num<<endl;const를 통해서 변수 num을 상수로 선언했는데, 참조자 ref를 통해서 값을 변경하려고 하고 있다. 이걸 허용한다면, num을 const로 선언한 의미가 없다. c++에서는 이를 허용하지 않고 있다. 컴파일 에러로 처리된다.
]따라서 변수 num과 같이 상수로 선언된 변수에 대한 참조자 선언은 다음과 같이 해야 한다.
const int num =20; cont int &ref = num;
이렇게 선언이 되면 ref를 통한 값의 변경이 불가능하기 때문에 상수화에 대한 논리적인 문제점은 발생하지 않는다. 그리고 const 참조자는 상수도 참조할 수 있다.
const int &ref = 50;
진짜?
다음 예시를 살펴보자.
int num = 10+20;
여기서 10, 20 과 같은 숫자를 가리켜 리터럴 또는 리터럴 상수라고 한다. 이들의 특징은 다음과 같다.
" 임시적으로 존재하는 값이다. 다음 행으로 넘어가면 존재하지 않는 상수다."
덧셈 연산을 위해서는 10, 20 둘 다 메모리 공간에 저장돼야 한다. 하지만 저장되었다고 해서 재참조가 가능한 값은 아니다. 그런데, 이렇게 사라질 상수를 참조하는 것은 맞는 일일까?
const int &ref = 30;
이는 숫자 30이 메모리 공간에 계속 남아있을 때에만 가능한 문장이다. 그래서 c++에서는 위 문장이 성립할 수 있도록, cosnt 참조자를 이용해서 상수를 참조할 때, '임시변수'라는 것을 만든다. 그리고 이 장소에 상수 30을 저장하고 참조자가 이를 참조하게 한다.
02-5. malloc & free 를 대신하는 new&delete
c언어를 공부하면서 malloc과 free 함수는 힙의 메모리 할당 및 소멸에 필요한 함수이다.
1. new & delete
길이정보를 인자로 받아서, 해당 길이의 문자열을 저장하는 배열을 생성하고, 그 배열의 주소 값을 반환하는 함수를 정의해 보자.
#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 happy~"); cout<<str<<endl; free(str); return 0; }
위 예제는 c언어에서의 동적할당을 보이기 위한 것이다. 그런데 이 방법에는 다음의 두 가지 불편한 점이 생긴다.
- 할당할 대상의 정보를 무조건 바이트 크기 단위로 전달해야 한다.
- 반환형이 void형 포인터이기 때문에 적절한 형 변환을 거쳐야 한다.
그런데 c++에서 제공하는 키워드 new 와 delete를 사용하면 이러한 불편한 점이 사라진다,
new는 malloc함수를 대신하는 키워드이고, delete는 free함수를 대신하는 키워드이다. 먼저 new의 사용방법을 정리하면 다음과 같다.
- int 형 변수의 할당 : int * ptr1 = new int;
- double 형 변수의 할당 : double * ptr2 = new double;
- 길이가 3인 int형 배열의 할당 : int * arr1 = new int[3];
- 길이가 7인 double형 배열의 할당 : double arr2 = new double[7];
문장이 의미하는 바가 쉽게 이해될 것이다.
그 다음 delete를 사용하려면 다음과 같이 하면 된다.
- delete []arr1;
- delete ptr1;
할당된 영역이 배열이라면 []를 추가로 명시해 주면 된다. 그럼 앞의 예제를 new와 delete을 사용해서 나타내 보자.
#include<iosream> #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 happy~"); cout<<str<<endl; delete(str); return 0; }
이제 malloc 대신 new를 사용해서 메모리 할당을 하도록 하자
2. 객체의 생성에서는 반드시 new & delete
malloc과 free 함수의 호출이 어떻게 문제가 될 수 있는지 간단히 언급하게 ㅆ다. 아직은 클래스와 객체에 대한 설명이 진행되지 않았기 때문에 자세히 언급할 수는 없지만, 실행 결과를 통해서 문제점을 확인할 수 있다.
#include <iostream> #include <stdlib.h> using namespace std; class simple{ public: simple(){ cout<<"I'm simple constructor"<<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; }
case 1 : I'm simple constructor
case 2 :
end of mainnew로 생성한 simple이라는 자료형의 변수는 실행의 결과로 문자열을 출력하였다. 그러나 malloc으로 생성했을 때엔 그렇지 않았다. 즉, new와 malloc 함수의 동작 방식에는 차이가 있음을 알 수 있다.
3. 힙에 할당된 변수를 포인터 없이 접근할 수 있다.
참조자의 선언은 상수가 아닌 변수를 대상으로만 가능함을 알고 있을 것이다.(const 참조자가 아닌 경우). 그렇다면 new연산자를 이용해서 할당된 메모리 공간에도 참조자의 선언이 가능할까? 정의에 따르면, 변수의 자격을 갖추기 위해서는 메모리 공간이 할당되고, 그 공간을 의미하는 이름이 존재해야 하지만, c++에서는 new 연산자를 이용해서 할당된 메모리 공간도 변수로 간주하여 참조자의 선언이 가능하다. 따라서 다음과 같은 문장의 구성이 가능하다.
int *ptr = new int;
int &ref = *ptr;
ref = 20;
cout<<*ptr<<endl;참조자의 선언을 통해서, 포인터 연산없이 힘 영역에 접근했다는 사실에 주목하자.
02-6. c++에서 c언어의 표준함수 호출하기
1. c를 더하고 h를 빼라
c언어의 라이브러리에는 매우 다양한 유형의 함수들이 정의돼 있다. 그런데 이러한 함수들은 c++의 표준 라이브러리에도 포함돼 있다. 따라서 어렵지 않게 사용이 가능하다. c의 라이브러리를 c++에서 사용하려면 헤어파일에 .h를 생략하고 c를 붙이면 된다.
#include <stdio.h> -> #include <cstdio>
#include <stdlib.h> -> #include <cstdlib>
#include <math.h> -> #include <cmath>반응형'a' 카테고리의 다른 글
[알고리즘] AES 암호화 알고리즘 (0) 2022.04.01 [GPU] 후퍼(Hopper) 란 (0) 2022.03.30 [GPU] GPGPU-sssim ispass benchmark build (0) 2022.03.25 [JS] 날씨 API 호출하기 (0) 2022.03.25 [알고리즘] 그리디 알고리즘 (0) 2022.03.24