IO 스트림
C의 기본적인 입출력은 printf, scanf이다.
C++은 더 객체 지향적인 cin, cout 입출력 객체를 제공한다.
cout << 데이터 << 데이터 ... // 출력
cin >> 변수 // 입력
'<<' 연산자로 cout 객체에 데이터를 보내기만하면 된다. 데이터를 연속적으로 보낼수 있으며, 데이터 타입은 자동으로 판별하기때문에 %d, %s 같은 서식을 지정할 필요가 없으므로 서식이 잘못되서 프로그램이 다운되는 경우도 없어서 안전하다고 볼 수 있다.
C++에서 기본 데이터(?)를 출력해보자.
// 파일이름 : cout.cpp
#include <iostream>
using namespace std;
int main()
{
cout << "First C++ Program" << endl;
int i = 56;
char ch = 'S';
double d = 2.414;
cout << i << ch << d << endl;
}
cout를 사용하려면 C++ 기본 헤더파일인 iostream을 포함해야한다.
문자열을 cout 객체로 보내면 그대로 출력되고, endl은 개행하라는 뜻이며 C의 \n 과 같다.
cout는 여러개의 데이터를 연쇄적으로 보낼 수 있고 자동으로 타입을 판별한다.
데이터는 정수든 실수든 문자열이든 어떤 타입이든 '<<' 연산자로 보내기만하면 내부에서 자동으로 처리한다.
C++에서 데이터를 입력해보자.
// 파일이름 : cin.cpp
#include <iostream>
using namespace std;
int main()
{
int age;
cout << "나이를 입력하세요 : ";
cin >> age;
cout << "당신은 " << age << "살 입니다." << endl;
}
C++에서 입력할때는 cin객체를 사용하며 '>>' 연산자로 입력 내용을 변수로 보낸다.
cin >> age 문장은 정수값을 입력받아 age 변수로 보낸다는 뜻이다.
변수의 타입은 자동으로 구분하며, 데이터 보내는 형식도 직관적이라서 scanf에 비하면 편리하게 사용할 수 있다.
셀프테스트) IO 스트림으로 정수값 두 개를 입력받아 그 합을 출력하는 프로그램
#include <iostream>
using namespace std;
int main()
{
int a = 0;
int b = 0;
cout << "첫번째 정수 입력 : ";
cin >> a;
cout << "두번째 정수 입력 : ";
cin >> b;
cout << "두 정수의 합 : " << a + b << endl;
}
레퍼런스
레퍼런스는 변수에 대해 별명을 정의하여 이름을 하나 더 붙인다. 포인터와 비슷한데 차이점도 많다.
포인터는 *를 붙이는데, 레퍼런스는 &를 붙인다.
자료형 &변수 = 대상체;
레퍼런스는 처음 선언할때 대상체를 정해야하므로 초기값으로 반드시 대상을 지정해주어야한다.
(별명을 부르면 누군지 딱 아는데 그 사람이 없으면 별명을 불러도 누군지 모르는 거랑 비슷한 것임)
// 파일이름 : ref1.cpp
#include <stdio.h>
int main()
{
int i = 3;
int& ri = i;
printf("i = %d, ri = %d\n", i, ri);
ri++;
printf("i = %d, ri = %d\n", i, ri);
printf("i번지 = %x, ri번지 = %x\n\n", &i, &ri);
// pi로 데이터를 건드려보기
int* pi;
pi = &i;
printf("pi의 값 : %d, pi의 주소 : %p\n", pi, &pi);
printf("pi가 가리키는 값 : %d\n", *pi);
*pi = i + 1;
printf("pi의 값 : %d, pi의 주소 : %p\n", pi, &pi);
printf("pi가 가리키는 값 : %d\n", *pi);
}
해당 코드에서 정수형 변수 i를 선언하며 3으로 초기화했는데, 정수형 레퍼런스 ri를 i로 초기화하면 i에 대해 ri라는 별명이 하나 더 생기는 것이다.
이후 i와 ri는 완전히 동일한 대상을 가리켜 둘 중 하나를 바꾸면 나머지 하나도 같이 바뀐다.
ri를 1 증가시키면 ri뿐만아니라 i도 같이 증가하여 둘 다 4가 되며 반대로 i를 변경하면 ri도 같이 바뀐다.
두 변수가 저장되어있는 번지를 출력해보면 메모리 위치가 같게 나오는것도 이러한 원리 때문이다.
포인터를 사용했을때는 *pi가 i를 가리키고있어서 뭔가 연결(?) 되있는 느낌이있다면
레퍼런스는 i의 주소값을 다른 이름으로 부르는 느낌으로 생각하는 것이 쉽다.
레퍼런스가 저장되있는 주소가 같으니 두 변수는 이름만 다른 완전히 같은 변수이다.(이래서 별명과 같은 원리이다)
레퍼런스의 일반적인 특징 및 주의 사항
- 레퍼런스는 같은 타입에 대해 붙이는 것이기때문에 레퍼런스와 대상체는 타입이 완전히 일치해야한다.
int i; // 가능
int &ri = i; // 에러
double &rd = i; // 에러
short &rs = i; // 에러
unsigned &ru = i; // 에러
- 레퍼런스 대상체는 실제 메모리를 점유하는 좌변(L-value)값이어야 한다.
상수는 좌변값이 아니므로 레퍼런스로 만들 수 없고 비트 필드도 주소가 없어 레퍼런스의 대상체가 될 수 없다.
int &ri = 123;
만약 이 선언이 허용된다면 ri = 456; 대입문으로 상수값을 바꿀 수 있으므로 허용되지 않는다.
대신, const 지시자를 붙이면 상수를 가리킬 수 있다.
- 레퍼런스 생성시 대상체를 분명하게 지정해야한다.
포인터는 선언만 해놔도되는데, 나중에 가리킬 변수의 번지를 대입할 수 있다.
레퍼런스는 처음만들때부터 누구의 별명인지 명확히 지정해야하며 NULL 레퍼런스는 인정되지 않는다.
int i;
int *pi; // 가능
pi = &i;
int &ri; // 에러
ri = i;
초기값이 없는 int &ri; 선언문 자체가 에러가 떠서 컴파일 자체가 되지 않을 것이다.
대신 예외적으로 초기값없이 레퍼런스를 선언할 수 있는 경우가 있는데,
1. 함수의 형식인수로 사용되는 레퍼런스, 호출될 때 실인수의 별명으로 초기화
2. 클래스의 멤버로 선언된 레퍼런 => 생성자에서 초기화
3. 변수를 extern 선언할때는 외부에서 이미 대상체가 지정되어 있음.
말이 어려우니 차차 알아가도록하고 결론적으로 레퍼런스를 선언할때는 꼭 초기값인 대상체가 있어야한다는 점을 기억하자.
레퍼런스 인수
포인터가있는데 쓸데없이 레퍼런스라는 개념을 또 배우는게 쓸데없는 짓일수도 있다.
하지만 함수의 인수로 전달될때 레퍼런스의 위력을 실감할 수 있다.
레퍼런스를 이용해서 두 값을 바꾸는 Swab 함수 작성
#include <iostream>
using namespace std;
void ref_swab(int &a, int &b);
int main()
{
int a = 10;
int b = 20;
cout << "a : " << a << endl;
cout << "b : " << b << endl;
ref_swab(a, b);
cout << endl;
cout << "a : " << a << endl;
cout << "b : " << b << endl;
}
void ref_swab(int &x, int &y)
{
int temp;
temp = x;
x = y;
y = temp;
}
C에서는 함수의 인수를 포인터로줬는데,
C++에서는 함수의 인수를 실인수로 주고 매개변수로 받을때만 레퍼런스를 사용해서 인수를 전달해줄때 좀 더 편하게 사용할 수 있다.
만약 swap 함수의 매개변수를 &로 받지 않는다면 지역 변수 내에서만 값이 바뀌고 결국 main 함수에는 아무일도 일어나지 않는다.
call by value(값호출) : 실인수를 복사해서 매개변수에 적용
메인에서 swap(a, b);
void swap(int x, int y) { } // a, b값이 x, y값으로 복사된다.
포인터에 비해 레퍼런스를 받는 방법은 형식인수를 이름으로 참조할 수 있어서 깔끔하다.
포인터가 가리키는 실인수를 참조하기 위해 일일이 *를 붙이는 것은 무척 귀찮은 일이고 실수로 *를 빼먹으면 런타임 에러가 발생할 수 있기 때문에 이중포인터를 넘길때는 레퍼런스가 더 직관적이다.
// 파일이름 : refptr
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <malloc.h>
#include <string.h>
// 이중포인터로 매개변수 전달
//void InputName(char** Name)
//{
// *Name = (char*)malloc(32);
// strcpy(*Name, "Hong Kil Dong");
//}
//
//int main()
//{
// char* Name; // 포인터 선언
//
// InputName(&Name);
// printf("이름은 %s입니다.", Name);
// free(Name);
//}
// 레퍼런스로 매개변수 전달
void InputName(char*&Name)
{
Name = (char*)malloc(32);
strcpy(Name, "Hong Kil Dong");
}
int main()
{
char* Name; // 포인터 선언
InputName(Name);
printf("이름은 %s입니다.", Name);
free(Name);
}
char *&Name인수가 포인터의 레퍼런스이다. 포인터 자체를 받았으므로 Name이라는 이름으로 메모리를 할당하고 여기에 문자열을 복사해서 넣게된다.
포인터는 잠재적 배열이어서 주변값을 건드릴 수 있는데 레퍼런스는 대상체만 액세스 할 수 있어서 비교적 안전하다.
그러나 호출부의 형식이 값 호출과 같아서 함수의 원형을 봐야 참조 호출인지 값 호출인지 알 수 있는 번거로움 또한 존재한다.
그래서 레퍼런스를 받는 함수는 이름 뒤에 Ref나 ByRef 등의 접미를 붙여주는 것이 좋다.
레퍼런스에 대한 레퍼런스는 선언할 수 없다. 레퍼런스가 별명인데 별명에 대해 또 다른 별명을 붙이는 것은 가치가 없다. 포인터는 2중 포인터가 있지만 레퍼런스는 중첩을 허락하지 않는다.
포인터변수 선언한 다음에 레퍼런스 변수의 주소를 저장할수는 있다.
i의 주소를 가리킴.
레퍼런스 대상체
레퍼런스가 특수하고 신기해보일 수 있지만, 내부를 들여다보면 포인터의 변형이라고 볼 수 있다.
실용성은 없지만 배열이나 함수에 대한 레퍼런스도 가능은하다.
그러나 모든 타입에 대한 레퍼런스를 다 선언할 수 있는것은 아니며 일부 불가능한 형식도 있다.
- 레퍼런스의 중첩은 허용되지않는다.
int i;
int &ri;
int &rri = ri;
포인터는 이중포인터가 있지만 레퍼런스는 중첩이 되지않는다.
- 포인터에 대한 레퍼런스는 가능하지만 역으로 레퍼런스에 대한 포인터는 선언할 수 없다.
int i;
int &ri = i;
int &*pri = &ri; // 에러
레퍼런스에대한 포인터는 레퍼런스의 대상체에 대한 포인터형이다.
즉, 단순 포인터와 같으며 굳이 레퍼런스의 포인터형을 정의할 필요도 없다.
(굳이 보자면 int *pi = &ri; 선언은 가능은한데 pi가 ri, 즉 i의 주소를 가리킴 => 쓸데없이 복잡함)
- 레퍼런스의 배열도 선언할 수 없다.
int i, j;
int &ra[2] = {i, j}; // 에러
배열은 곧 포인터인데 레퍼런스에대한 포인터를 선언할 수 없으므로 배열도 선언할 수 없다.
i, j의 레퍼런스가 필요하면 각각 따로 레퍼런스를 선언해야한다.
레퍼런스 리턴
함수에 레퍼런스 타입을 반환할 수도 있다. 이렇게 되면 리턴되는 대상이 좌변값의 별명이다 보니 함수 호출문에 값을 대입하는 것이 가능해진다.
#include <stdio.h>
int ar[] = { 1,2,3,4,5 };
int& GetAr(int i) // 콜바이value에 의해 i에 3을 복사해서 저장
{
return ar[i]; // ar[3]을 리턴
//return 3; // 3이 상수이므로 int&와 맞지않으므로 에러뜸
}
int main()
{
// int& ra = 10; // 상수에는 별명을 줄 수 없음.(에러)
GetAr(3) = 6; // 실인수 전달후에 그 변수에 6을 대입
printf("ar[3] = %d\n", ar[3]);
}
C의 함수는 값을 리턴하기 때문에 이런 형식을 쓸 수 없지만 레퍼런스는 좌변값이어서 함수 호출문에 뭔가를 대입할 수 있다.
레퍼런스 내부
레퍼런스가 특수하고 신기해보일 수 있지만, 내부를 들여다보면 포인터의 변형에 불과하다.
컴파일러는 레퍼런스를 포인터로 바꿔 컴파일하며 내부 구현도 포인터로 되어있다.
int &ri = i; 선언문에 대해 컴파일러는 ri를 정수형 포인터로 생성하고 i의 주소값으로 초기화한다.
레퍼런스가 포인터이므로 대상체에 대해 암시적으로 & 연산자를 붙인다.
코드에서 ri를 참조하는 모든 문장에 대해 암시적으로 * 연산자를 적용하여 ri가 가리키는 대상체를 읽는다.
(*ri)가 곧 i와 같으니 ri는 i의 완전한 별명으로 동작한다.
이를 통해 레퍼런스는 컴파일러가 내부에서 절묘하게 조작하는 포인터라고 볼 수 있다.
하지만 주소를 읽고 내용을 액세스하는 &, * 연산자를 붙이는 과정이 자동화되어있어서 코드가 좀 더 간결해지는 이점이 있다. 포인터에대해 암시적 연산자를 붙여 일반 변수처럼 쓸 수 있게 해주는 편의적인 문법이다.
또, 클래스가 완전한 타입이 되기 위해서는 기능뿐만 아니라 형식도 기본타입과 일치시켜야하는 복사 생성자나 연산자 오버로딩을 쓸때 레퍼런스가 유용하다.
따라서 포인터를 사용하는 경우에 레퍼런스를 사용하면 좀 더 편하게 사용할 수 있고, 대상체의 별명이라는 점만 기억하고 넘어가도록 하자..
'개발자과정준비 > C++' 카테고리의 다른 글
[C++] 여러가지 생성자(디폴트, 복사, 초기화리스트, 변환) (0) | 2021.06.03 |
---|---|
[C++] 생성자, 소멸자(파괴자) (0) | 2021.06.02 |
[C++] 클래스 (0) | 2021.06.01 |
[C++] 디폴트 인수, 오버로딩, 인라인 함수 (0) | 2021.05.31 |
[C++] C언어의 확장 (1) | 2021.05.28 |