C++ 기초플러스 (08. 함수의 활용 - 10. 객체와 클래스)
참조 변수
미리 정의된 어떤 변수의 실제 이름 대신 쓸 수 있는 대용 이름이다.
예를 들어, twain을 clemens 변수의 참조로 만들면, twain과 clemens는 같은 변수를 나타내는 것으로 사용할 수 있다.
참조의 주된 용도는 함수의 형식 매개변수에 사용하는 것이다. 참조를 매개변수로 사용하면, 그 함수는 복사본 대신 원본 데이터를 가지고 작업한다.
- 덩치 큰 구조체를 처리해야 하는 함수에서 포인터 대신에 참조를 사용할 수 있다.
- 클래스를 설계할 때 필수적으로 사용된다.
참조 변수의 생성
int rats;
int & rodents = rats; // rodents를 rats의 대용 이름으로 만든다
** 여기에서 "&(앰퍼샌드)" 는 주소 연산자가 아니라, 데이터형 식별자의 일부로 사용된 것이다.
int &(앰퍼샌드) 는 int에 대한 참조를 의미한다. 참조 선언은 rats와 rodents를 서로 바꾸어 사용할 수 있게 해 준다.
** 이들 둘은 모두 같은 값과 같은 메모리 위치를 참조한다.
// firstref.cpp -- 참조 변수의 정의와 사용
#include <iostream>
int main()
{
using namespace std;
int rats = 101;
int & rodents = rats; // rodents는 참조변수 이다
cout << "rats = " << rats;
cout << ", rodents = " << rodents << endl;
rodents++;
cout << "rats = " << rats;
cout << ", rodents = " << rodents << endl;
cout << "rats address = " << &rats;
cout << ", rodents address = " << &rodents << endl;
// cin.get();
return 0;
}
=> rodents를 int &(앰퍼샌드)형, 즉 int형 변수에 대한 참조로 선언한다.
cout << ", rodents의 주소 = " << &rodents << endl;
&rodents는 주소연산자이며, rodents가 참조하는 변수의 주소를 나타낸다.
=> 참조는 포인터와 비슷하다고 생각하면된다.
rats를 참조하기 위해 참조와 포인터를 둘 다 만들 수 있다.
int rats = 101 ;
int & rodents = rats;
int * prats = &rats;
참조 변수와 포인터의 차이점
참조 변수: 참조 변수는 기존 변수의 또 다른 이름, 즉 별명이라고 생각하면 쉬워요. 마치 친구를 부를 때 별명을 사용하는 것과 비슷하다. 참조 변수를 사용하면 기존 변수에 직접 접근하는 것처럼 값을 변경하거나 읽어올 수 있습니다.
포인터: 포인터는 변수의 메모리 주소를 저장하는 특별한 변수이다. 마치 집 주소를 알면 그 집을 찾아갈 수 있는 것처럼, 포인터를 사용하면 변수의 메모리 주소를 통해 해당 변수에 접근할 수 있습니다.
**참조 변수(&)는 선언 시 반드시 초기화해야 하고, 한 번 초기화되면 다른 변수를 가리킬 수 없습니다. 반면 **포인터(*)는 널(null) 값을 가질 수 있고, 언제든지 다른 변수의 주소를 저장할 수 있습니다.
참조 변수: 함수 매개변수 전달 시 값을 변경하거나 큰 객체를 복사하지 않고 효율적으로 전달하고 싶을 때 유용합니다.
포인터: 동적 메모리 할당, 연결 리스트와 같은 자료 구조 구현, 널 값을 이용한 특정 상태 표현 등에 유용합니다.
1. 널(null) 값 허용 여부
- 포인터: 어떤 변수도 가리키지 않는 상태, 즉 '아무것도 가리키지 않음'을 나타내는 특별한 값인 널(null)을 가질 수 있습니다.
- 참조 변수: 항상 실제 변수를 참조해야 하므로 널 값을 가질 수 없습니다.
2. 재할당 가능 여부
- 포인터: 다른 변수의 주소를 자유롭게 저장할 수 있어서, 프로그램 실행 중에 가리키는 변수를 변경할 수 있습니다. 마치 이사를 가듯이요!
- 참조 변수: 초기화 시 연결된 변수만을 계속 참조하며, 다른 변수로 변경할 수 없습니다. 한 번 맺은 인연을 끝까지 지키는 것과 같죠!
3. 메모리 사용량
- 포인터: 포인터 자체가 메모리 공간을 차지하며, 일반적으로 변수의 크기와 같은 크기의 메모리를 사용합니다.
- 참조 변수: 별도의 메모리 공간을 사용하지 않고, 기존 변수의 메모리 공간을 공유합니다.
4. 사용 편의성
- 포인터: 포인터 연산(->, *)을 사용하여 가리키는 변수에 접근해야 하므로, 코드가 다소 복잡해 보일 수 있습니다.
- 참조 변수: 일반 변수처럼 사용할 수 있어서 코드가 간결하고 직관적입니다.
특징 | 포인터 | 참조 변수 |
널 값 | 허용 | 허용하지 않음 |
재할당 | 가능 | 불가능 |
메모리 | 추가 메모리 필요 | 추가 메모리 불필요 |
사용 편의성 | 포인터 연산 필요, 다소 복잡 | 일반 변수처럼 사용, 간결하고 직관적 |
장점 | 유연성, 동적 메모리 할당 등에 유용 | 코드 가독성 향상, 안전성 확보에 유용 |
단점 | 널 포인터 역참조 오류 가능성, 코드 복잡성 증가 | 초기화된 변수만 참조 가능, 유연성 부족 |
함수 매개변수로서의 참조
참조는 주로 함수의 매개변수로 사용된다. 그것은 어떤 함수에서 사용하는 변수의 이름을 그 함수를 호출한 프로그램(호출 함수)에 어떤 변수의 대용 이름 (별명)으로 만든다.
참조로 전달하면 피호출 함수가 호출 함수의 변수를 사용할 수 있다.
// swaps.cpp -- 참조를 이용한 교환과 포인터를 이용한 교환
#include <iostream>
void swapr(int & a, int & b); // a, b 는 int형 변수의 대용이름
void swapp(int * p, int * q); // p, q 는 int형을 지시하는 주소
void swapv(int a, int b); // a, b 는 새로운 변수
int main()
{
using namespace std;
int wallet1 = 300;
int wallet2 = 350;
cout << "지갑 = " << wallet1 << "원";
cout << " 지갑2 = " << wallet2 << "원\n";
cout << "참조를 이용하여 내용들을 교환:\n";
swapr(wallet1, wallet2); // 변수를 전달
cout << "지갑 = " << wallet1 << "원";
cout << " 지갑2 = " << wallet2 << "원\n";
cout << "Using pointers to swap contents again:\n";
swapp(&wallet1, &wallet2); // 변수의 주소를 전달
cout << "지갑 = " << wallet1 << "원";
cout << " 지갑2 = " << wallet2 << "원\n";
cout << "Trying to use passing by value:\n";
swapv(wallet1, wallet2); // 변수의 값을 전달
cout << "지갑 = " << wallet1 << "원";
cout << " 지갑2 = " << wallet2 << "원\n";
// cin.get();
return 0;
}
void swapr(int & a, int & b) // 참조를 이용하여 교환
{
int temp;
temp = a; // 변수의 값으로 a,b를 사용
a = b;
b = temp;
}
void swapp(int * p, int * q) // 포인터를 이용하여 교환
{
int temp;
temp = *p; // 변수의 값으로 *p, *q를 사용
*p = *q;
*q = temp;
}
void swapv(int a, int b) // 값으로 전달하여 교환 시도
{
int temp;
temp = a; // 변수의 값으로 a,b를 사용
a = b;
b = temp;
}
=> 참조변수는 정의할 때 함께 초기화해야 한다. 참조로 전달하는 함수의 매개변수는 함수호출의 넘겨주는 매개변수로 초기화된다고 생각할 수 있다. 즉, 다음과 같은 함수 호출은
swapr(wallet1, wallet2);
형식 매개변수 a를 wallet1로 초기화하고, 형식 매개변수 b를 wallet2로 초기화 한다.
임시변수 , 참조 매개변수, const
C++는 실제 매개변수와 참조 매개변수가 일치하지 않을 때 임시 변수를 생성할 수 있다.
최근의 C++는 매개변수가 const 참조일 경우에만 이것을 허용한다.
[임시변수가 생성될 때]
- 실제 매개변수가 올바른 데이터형이지만 lvalue가 아닐때
- 실제 매개변수가 잘못된 데이터형이지만 올바른 데이터형으로 변환할 수 있을 때
** lvalue란?
lvalue는 메모리 상에 고유한 주소를 가지고 있는 값
lvalue 매개변수는 참조가 가능한 데이터 객체이다. 예를들어 변수, 배열의 원소, 구조체의 멤버, 참조 또는 역참조 포인터는 lvalue이다. 대입연산자 왼쪽에 나타낼 수 있는 독립체들을 의미한다.
여러 번 읽거나 쓸 수 있습니다. 마치 집 주소처럼, lvalue를 알면 언제든 그 값을 찾아가서 확인하거나 변경가능하다.
** rvalue란?
rvalue는 임시적인 값 또는 식의 결과
예를 들어, 리터럴 값(숫자, 문자열 등), 함수의 반환 값, 산술 연산의 결과 등이 rvalue에 해당됩니다. rvalue는 대입 연산자의 오른쪽에만 올 수 있고, 일반적으로 한 번만 사용됩니다.
rvalue는 한 번 사용되고 나면 사라지는 임시적인 값
[상수 참조 매개변수]
double refcube (const double &ra)
{
return ra * ra * ra;
}
double side = 3.0;
double * pd = &side;
double & rd = side;
long edge = 5L;
double lens[4] = {2.0, 5.0, 10.0, 12.0};
double c1 = refcube(side) ; // ra는 side
double c2 = refcube(lens[2]) ; // ra는 lens[2]
double c3 = refcube(rd) ; // ra는 rd이며 side
double c4 = refcube(*pd) ; // ra는 *pd이며 side
double c5 = refcube(edge) ; // ra는 임시변수
double c6 = refcube(7.0) ; // ra는 임시변수
double c7 = refcube(side + 10.0) ; // ra는 임시변수
=> 매개변수 side, lens[2], rd, *pd는 이름을 가지고 있는 double형 데이터 객체이다 . 따라서 이들에 대한 참조 매개변수를 생성할 수 있으며, 임시 변수는 필요없다. (배열의 원소는 그 원소와 데이터형이 같은 일반 변수처럼 동작한다.)
=> edge는 변수이기는 하지만 데이터형이 일치하지 않는다. 즉, double형에 대한 참조로는 long형을 참조할 수 없다.
=> 매개변수 0.7과 side + 10.0은 데이터형은 일치하지만, 이름을 가지고 있는 데이터 객체는 아니다. 이러한 경우 컴파일러는 익명의 임시 변수를 만들고, ra로 하여금 그것을 참조하게 한다.
(임시 변수는 함수가 호출되어 있는 동안 유지되지만, 그 후에는 컴파일러는 그것을 마음대로 없앨 수 있다.)
*** 참조 매개변수를 가진 함수의 목적이 매개변수로 전달되는 변수를 변경하는 것이라면, 임시 변수의 생성은 그 목적을 방해한다.
참조 매개변수를 상수 데이터에 대한 참조로 선언하는 이유는 다음과 같은 세 가지 이점이 있기 때문이다.
1. const를 사용하면, 실수로 데이터 변경을 일으키는 프로그래밍 에러를 막을 수 있다.
2. 원형에 const를 사용하면, 함수가 const와 const가 아닌 실제 매개변수를 모두 처리할 수 있지만, 원형에 const를 생략한 함수는 const가 아닌 데이터만 처리 할 수 있다.
3. const참조를 사용하면, 함수가 자신의 필요에 따라 임시 변수를 생성하여 사용할 수 있다.
** 따라서 가능하면 참조 형식 매개변수를 const로 선언하는 것이 좋다.
함수 오버로딩
** 다형성 이란?
다형성은 마치 카멜레온처럼, 하나의 이름으로 다양한 형태를 가질 수 있는 능력이라고 생각하면 돼요. 예를 들어, "동물"이라는 개념은 강아지, 고양이, 새 등 다양한 형태로 나타날 수 있죠. 마찬가지로, C++에서 다형성은 하나의 함수 이름이나 연산자가 여러 가지 방식으로 동작할 수 있도록 해주는 기능입니다.
다형성은 크게 두 가지 종류로 나눌 수 있습니다.
컴파일 시간 다형성: 컴파일 시점에 어떤 함수를 호출할지 결정되는 다형성입니다. 함수 오버로딩과 템플릿이 대표적인 예시입니다.
런타임 다형성: 프로그램 실행 중에 어떤 함수를 호출할지 결정되는 다형성입니다. 가상 함수와 상속을 통해 구현됩니다.
다형성을 사용하면 코드의 유연성과 재사용성을 높일 수 있습니다. 예를 들어, "동물" 클래스를 상속받아 "강아지", "고양이", "새" 클래스를 만들고, 각 클래스에 "소리내기"라는 함수를 정의할 수 있습니다. 이때, "동물" 클래스의 포인터를 사용하여 "강아지", "고양이", "새" 객체를 가리키고 "소리내기" 함수를 호출하면, 각 객체는 자신의 종류에 맞는 소리를 낼 수 있습니다. 이처럼 다형성을 통해 하나의 인터페이스("소리내기")로 다양한 객체("강아지", "고양이", "새")를 동일한 방식으로 다룰 수 있습니다.
다형성은 객체지향 프로그래밍의 핵심 개념 중 하나이며, 복잡한 프로그램을 더욱 효율적이고 유연하게 설계하고 구현하는 데 중요한 역할을 합니다.
// leftover.cpp -- left() 함수의 오버로딩
#include <iostream>
unsigned long left(unsigned long num, unsigned ct);
char * left(const char * str, int n = 1);
int main()
{
using namespace std;
char * trip = "Hawaii!!"; // 테스트 값
unsigned long n = 12345678; // 테스트 값
int i;
char * temp;
for (i = 1; i < 10; i++)
{
cout << left(n, i) << endl;
temp = left(trip,i);
cout << temp << endl;
delete [] temp; // 재사용을 위해 임시 기억 공간을 해제한다.
}
// cin.get();
return 0;
}
// 이 함수는 정수 num의 앞에서부터 ct개의 숫자를 리턴한다.
unsigned long left(unsigned long num, unsigned ct)
{
unsigned digits = 1;
unsigned long n = num;
if (ct == 0 || num == 0)
return 0; // 숫자가 없으면 0을 리턴한다
while (n /= 10)
digits++;
if (digits > ct)
{
ct = digits - ct;
while (ct--)
num /= 10;
return num; // 남아있는 ct개의 숫자를 리턴한다
}
else // 'ct >= 전체 숫자 개수' 이면
return num; // 그 정수 자체를 리턴한다
}
// 이 함수는 str 문자열의 앞에서부터 n개의 문자를 취하여
// 새로운 문자열을 구성하고, 그것을 지시하는 포인터를 리턴한다.
char * left(const char * str, int n)
{
if(n < 0)
n = 0;
char * p = new char[n+1];
int i;
for (i = 0; i < n && str[i]; i++)
p[i] = str[i]; // 문자열을 복사한다
while (i <= n)
p[i++] = '\0'; // 문자열의 나머지를 '\0' 으로 설정한다
return p;
}
=> 함수 오버로딩은 서로 다른 데이터형을 대상으로 하지만 기본적으로는 같은 작업을 수행하는 함수들에만 사용하는것이 바람직하다. 또한 독자는 디폴트 매개변수를 사용하여 같은 목적을 수행할 수 있는지 확인하는 것이 좋다.
** 그러나 서로 다른 데이터형의 매개변수를 요구하고, 디폴트 매개변수가 소용이 없을 때에는 함수 오버로딩을 사용해야 한다.
char * left(const char * str, unsigned n); // 두 개의 매개변수
char * left(const char * str); // 한 개의 매개변수
09. 메모리 모델과 이름 공간
이름공간 (namespace)
새로운 종류의 선언 영역을 정의함으로써 이름이 명명된 이름 공간을 만들 수 있는 기능이다.
이름을 선언하는 영역을 따로 제공하는것이다. 하나의 이름 공간에 속한 이름은, 동일한 이름으로 다른 이름공간에 선언된 이름과 충돌하지 않는다.
- 이름 공간은 전역 위치에 또는 다른 이름 공간 안에도 놓을 수 있다. 그러나 블록 안에는 놓을 수 없다. 그러므로 하나의 이름 공간에 선언된 이름은, (그것이 상수를 참조하지 않는다면) 기본적으로 외부 링크를 가진다.
- 사용자가 정의하는 이름 공간 외에, 전역 이름 공간(global namespace)이라는 또 하나의 이름 공간이 있다.
- 이름 공간 선언을 중첩시킬 수 있다.
[using 선언과 using 지시자]
1. using 선언
- 하나의 특별한 식별자를 사용할 수 있게 만든다. 제한된 이름 앞에 키워드 using을 붙이는 것이다.
- 그것이 나타나는 선언 영역에 하나의 특별한 이름을 추가한다.
- 예를 들어, main()에 있는 Jill : : fetch의 using 선언은 main()에 의해 정의되는 선언 영역에 fetch를 추가한다.
이선언을 한 이후에는 Jill : : fetch 대신에 fetch라는 이름을 사용할 수 있다.
using Jill : : fetch ; // using 선언
2. using 지시자
- 그 이름 공간 전체에 접근할 수 있게 만든다.
- using 지시자를 어떤 특별한 함수 안에 넣으면, 그 이름들을 그 함수 안에서만 사용할 수 있다.
int main()
{
using namespace jack ; // 이름들을 main() 안에서만 사용할 수 있다
}
- using namespace라는 키워드를 붙이는 것이다. 이렇게 사용하면 사용 범위 결정 연산자를 사용하지 않고도 그 이름 공간에 속한 모든 이름을 사용할 수 있게 된다.
# include <iostream> // 이름들을 std라는 이름공간에 넣는다
using namespace std; // 이름들을 전역적으로 사용할 수 있게 만든다
*** using 지시자 보다는 using 선언을 사용하는 것이 더 안전하다.
=> 이름 공간의 이름이 지역 이름과 충돌하면, 컴파일러가 그 사실을 알려준다. using 지시자는 필요하지 않은 이름까지 포함하여 모든 이름을 추가시킨다.
int x;
std :: cin >> x;
std :: cout << x << std :: endl;
=> 이렇게 사용되어야 한다!!!!
using std :: cin;
using std :: cout;
using std :: endl;
int x;
cin >> x;
cout << x << endl;
=> 자주 사용하는 using 선언들이 들어있는 이름 공간을 따로 만들기 위해 저렇게 사용되어진다.
// namesp.h
#include <string>
// pers와 debts 이름 공간을 만든다.
namespace pers
{
struct Person
{
std::string fname;
std::string lname;
};
void getPerson(Person &);
void showPerson(const Person &);
}
namespace debts
{
using namespace pers;
struct Debt
{
Person name;
double amount;
};
void getDebt(Debt &);
void showDebt(const Debt &);
double sumDebts(const Debt ar[], int n);
}
// namesp.cpp -- 이름 공간들
#include <iostream>
#include "namesp.h"
namespace pers
{
using std::cout;
using std::cin;
void getPerson(Person & rp)
{
cout << "이름을 입력하세요: ";
cin >> rp.fname;
cout << "성씨를 입력하세요: ";
cin >> rp.lname;
}
void showPerson(const Person & rp)
{
std::cout << rp.lname << ", " << rp.fname;
}
}
namespace debts
{
void getDebt(Debt & rd)
{
getPerson(rd.name);
std::cout << "부채를 입력하세요: ";
std::cin >> rd.amount;
}
void showDebt(const Debt & rd)
{
showPerson(rd.name);
std::cout <<": $" << rd.amount << std::endl;
}
double sumDebts(const Debt ar[], int n)
{
double total = 0;
for (int i = 0; i < n; i++)
total += ar[i].amount;
return total;
}
}
// usenmsp.cpp -- 이름 공간의 사용
#include <iostream>
#include "namesp.h"
void other(void);
void another(void);
int main(void)
{
using debts::Debt;
using debts::showDebt;
Debt golf = { {"Benny", "Goatsniff"}, 120.0 };
showDebt(golf);
other();
another();
// std::cin.get();
// std::cin.get();
return 0;
}
void other(void)
{
using std::cout;
using std::endl;
using namespace debts;
Person dg = {"Doodles", "Glister"};
showPerson(dg);
cout << endl;
Debt zippy[3];
int i;
for (i = 0; i < 3; i++)
getDebt(zippy[i]);
for (i = 0; i < 3; i++)
showDebt(zippy[i]);
cout << "부채총액: $" << sumDebts(zippy, 3) << endl;
return;
}
void another(void)
{
using pers::Person;;
Person collector = { "Milo", "Rightshift" };
pers::showPerson(collector);
std::cout << std::endl;
}
g++ 컴파일
출력
10. 객체와 클래스
객체지향 프로그래밍 (OOP)의 기능
- 추상화 (abstraction)
- 캡슐화 (encapsulation)와 데이터 은닉 (data hiding)
- 다형 (polymorphism)
- 상속 (inheritance)
- 코드의 재활용(reusability of code)
추상화 와 클래스
추상화 란?
정보를 사용자 인터페이스로 표현하는 것이다. 즉, 어떤 문제에 필수적인 조작적 기능들을 추상화하고, 그것으로 해결책을 표현하는 것이다.
(인터페이스는 사용자가 어떻게 데이터를 초기화하고, 갱신하고 출력하는지를 서술한다. 즉, 사용자와 컴퓨터 사이의 매개체)
** 클래스는 추상화 인터페이스를 구현하는 사용자 정의 데이터형이다.
클래스
- 클래스 선언 (Class Declaration) : 데이터 멤버와 public 인터페이스(메서드라고 부르는) 멤버 함수를 이용하여 데이터 표현을 서술한다.
- 클래스 메서드 정의 (Class Method Definitions) : 클래스 멤버 함수가 어떻게 구현되는지를 서술한다.
class ClassName {
// 멤버 변수
// 멤버 함수
};
ClassName은 우리가 정의하는 클래스의 이름입니다. 클래스의 본문은 중괄호 {}로 둘러싸여 있으며, 세미콜론(;)으로 끝납니다. 이 안에 들어가는 멤버 변수와 멤버 함수를 우리가 원하는 대로 정의하게 됩니다.
클래스의 구조
클래스는 멤버 변수와 멤버 함수로 구성됩니다. 멤버 변수는 클래스가 가지는 데이터를 정의하며, 멤버 함수는 그 데이터를 다루는 방법을 정의합니다.
멤버 변수와 함수는 보통 '접근 지시자'에 의해 제어됩니다. 'public', 'private', 'protected'와 같은 접근 지시자를 사용하여 멤버들의 접근 권한을 설정할 수 있습니다.
- 'public'은 어떤 곳에서든 접근이 가능하게 합니다.
- 'private'은 클래스 내부에서만 접근이 가능하게 합니다.
- 'protected'는 클래스 내부와 해당 클래스를 상속받은 자식 클래스에서만 접근이 가능하게 합니다.
C++에서는 보통 멤버 변수는 private로, 멤버 함수는 public으로 설정하는 것이 일반적입니다. 이렇게 하면 멤버 변수에 직접 접근하는 것을 방지하고, 멤버 함수를 통해 멤버 변수를 안전하게 조작하는 것을 보장할 수 있습니다.
접근 생성자를 통해 외부에서 객체를 생성할 때 필요한 값들을 전달받고, 객체의 내부 상태를 초기화할 수 있습니다.
public 인터페이스는 설계의 추상화를 나타낸다.
세부적인 구현들을 따로 결합하여 추상화와 분리하는것을 캡슐화라고 한다.
데이터 은닉(데이터를 클래스 private 부분에 넣는 것)은 캡슐화의 한 예이다.
** 데이터 은닉(data hiding) : 프로그램이 데이터에 직접 접근하지 못하게 차단하는 것
*** 클래스는 접근제어를 선언해주지 않으면 디폴트로 private로 선언되어진다.
객체란 무엇인가
클래스를 통해 우리는 데이터와 함수를 한 곳에 묶어서 관리할 수 있습니다. 이제 클래스를 이용해 실제로 우리가 다룰 수 있는 '객체'에 대해 알아보겠습니다.
객체는 클래스를 기반으로 만들어진 실체입니다. 클래스는 일종의 틀이며, 그 틀을 이용해 만든 것이 바로 객체입니다. 다르게 표현하자면, 클래스는 설계도이고 객체는 그 설계도를 바탕으로 만든 제품입니다. 이 객체를 우리가 코드에서 직접 다루게 됩니다.
객체는 클래스에서 정의한 멤버 변수와 멤버 함수를 모두 가지게 됩니다. 객체마다 멤버 변수는 독립적으로 메모리에 할당되며, 그 값을 개별적으로 유지합니다. 멤버 함수는 모든 객체가 공유하게 됩니다.
객체를 생성하려면 다음과 같이 '클래스 이름' 다음에 '객체 이름'을 적고, 세미콜론(;)으로 끝내면 됩니다.
[예제]
ClassName objectName;
이제 'Dog' 클래스를 기반으로 'myDog'라는 객체를 만들어 봅시다.
[예제]
Dog myDog;
이 코드는 'Dog' 클래스의 인스턴스인 'myDog' 객체를 생성합니다. 이제 'myDog' 객체는 'name'과 'age'라는 멤버 변수를 가지고, 'bark'라는 멤버 함수를 사용할 수 있습니다.
멤버 변수에 접근하려면 다음과 같이 객체 이름 다음에 점(.)을 찍고, 변수 이름을 적으면 됩니다.
[예제]
myDog.name = "Puppy";
myDog.age = 1;
멤버 함수를 호출하려면, 변수에 접근하는 방법과 동일하게 객체 이름 다음에 점(.)을 찍고, 함수 이름을 적으면 됩니다.
[예제]
myDog.bark(); // "Bark!"를 출력합니다.
이처럼 클래스와 객체를 이해하고 사용하는 것은 C++ 프로그래밍의 기본이며, 효율적이고 체계적인 코드 작성의 핵심입니다.
클래스와 객체의 관계
클래스: 클래스는 마치 설계도와 같다. 우리가 집을 지을 때 설계도를 보고 짓듯이, 프로그래밍에서도 객체를 만들 때 클래스라는 설계도를 사용합니다. 클래스는 객체의 속성(변수)과 행동(함수)을 정의하는 틀이라고 할 수 있습니다.
객체: 객체는 클래스라는 설계도를 바탕으로 실제로 만들어진 **'제품'**이에요. 설계도를 보고 여러 채의 집을 지을 수 있듯이, 하나의 클래스로부터 여러 개의 객체를 생성할 수 있습니다. 각 객체는 클래스에 정의된 속성과 행동을 가지지만, 속성 값은 서로 다를 수 있습니다.
비유
- 클래스: 자동차 설계도
- 객체: 설계도를 바탕으로 만들어진 실제 자동차들 (색깔, 모델 등은 다를 수 있음)
객체 생성 과정:
객체를 생성하는 과정은 다음과 같습니다.
- 클래스 선언: 먼저 객체를 만들기 위한 설계도인 클래스를 선언합니다. 클래스에는 객체의 속성(멤버 변수)과 행동(멤버 함수)을 정의합니다.
- 객체 선언: 클래스를 이용하여 실제 객체를 선언합니다. 이때 객체의 이름을 지정하고, 필요한 경우 생성자를 호출하여 객체를 초기화합니다.
- 객체 사용: 생성된 객체의 멤버 변수에 접근하거나 멤버 함수를 호출하여 객체의 상태를 변경하거나 작업을 수행합니다.
객체 멤버 접근 방법:
객체의 멤버 변수와 멤버 함수에 접근하려면 점(.) 연산자를 사용합니다. 예를 들어, myCar.color는 myCar 객체의 color 멤버 변수에 접근하고, myCar.startEngine()은 myCar 객체의 startEngine 멤버 함수를 호출합니다.
생성자와 소멸자:
- 생성자: 객체가 생성될 때 자동으로 호출되는 특별한 멤버 함수입니다. 객체의 멤버 변수를 초기화하거나 필요한 자원을 할당하는 등의 작업을 수행합니다.
- 소멸자: 객체가 소멸될 때 자동으로 호출되는 특별한 멤버 함수입니다. 객체가 사용하던 자원을 해제하거나 정리하는 등의 작업을 수행합니다.
디폴트 생성자:
- 매개변수가 없으며, 명시적으로 초기화하지 않고 객체를 생성할 때 사용한다.
- 사용자가 어떠한 생성자도 제공하지 않으면, 사용자를 대신하여 컴파일러가 디폴트 생성자를 정의한다.
- 그렇지 않을 경우에는 사용자 자신의 디폴트 생성자를 제공해야 한다. 사용자가 제공하는 생성자는 어떠한 매개변수도 사용하지 않거나, 모든 매개변수에 디폴트 값을 사용해야 한다.
this 포인터
this 포인터: 객체 자신의 '명찰'
C++에서 this 포인터는 객체 지향 프로그래밍의 핵심 개념 중 하나입니다. 마치 각 객체가 자신의 '명찰'을 가지고 있는 것처럼, this 포인터는 멤버 함수 내에서 객체 자신을 가리키는 특별한 포인터입니다.
- 숨겨진 매개변수: 멤버 함수가 호출될 때, 컴파일러는 자동으로 this 포인터를 숨겨진 매개변수로 전달합니다.
- 객체 자기 참조: 멤버 함수 내에서 this 포인터를 사용하면 해당 함수가 호출된 객체의 멤버 변수나 다른 멤버 함수에 접근할 수 있습니다.
**this 포인터는 멤버 함수 내에서만 사용할 수 있습니다.
class Person {
public:
std::string name;
int age;
void introduce() {
std::cout << "안녕하세요, 저는 " << this->name << "이고 "
<< this->age << "살입니다." << std::endl;
}
};
int main() {
Person person1;
person1.name = "홍길동";
person1.age = 20;
Person person2;
person2.name = "김철수";
person2.age = 30;
person1.introduce(); // "안녕하세요, 저는 홍길동이고 20살입니다." 출력
person2.introduce(); // "안녕하세요, 저는 김철수이고 30살입니다." 출력
return 0;
}
=> introduce() 함수 내에서 this->name과 this->age는 각각 person1 객체와 person2 객체의 name과 age 멤버 변수를 가리킵니다. 즉, this 포인터를 통해 어떤 객체의 멤버 변수에 접근해야 하는지 명확하게 구분할 수 있습니다.
[this 포인터를 사용하는 이유]
1. 멤버 변수와 매개변수 이름 충돌 해결
멤버 함수의 매개변수 이름과 멤버 변수 이름이 같을 때, 컴파일러는 매개변수를 우선적으로 인식합니다. 이러한 모호성을 해결하기 위해 this 포인터를 사용하여 멤버 변수를 명시적으로 참조할 수 있습니다.
2. 객체 자신 반환
멤버 함수에서 객체 자신을 반환하고 싶을 때, this 포인터를 사용하여 객체의 주소를 반환할 수 있습니다. 이는 연쇄 호출(chaining)을 가능하게 하여 코드를 더욱 간결하고 읽기 쉽게 만들어 줍니다.
3. 연결된 멤버 함수 호출
this 포인터를 사용하여 현재 객체의 다른 멤버 함수를 호출할 수도 있습니다.
객체 배열
1. 객체 배열이란?
객체 배열은 같은 종류의 객체들을 여러 개 담을 수 있는 컨테이너라고 생각하면된다.
일반 배열은 int, double과 같은 기본 데이터 타입을 저장하는 반면, 객체 배열은 사용자 정의 클래스 타입의 객체들을 저장합니다. 즉, 객체 배열의 각 요소는 하나의 완전한 객체이며, 각 객체는 클래스에 정의된 멤버 변수와 멤버 함수를 모두 가지고 있습니다.
예를 들어,
강아지들을 관리하는 프로그램을 만든다고 가정해 봅시다. 각 강아지는 이름, 나이, 품종 등의 정보를 가지고 있고, 짖거나 꼬리를 흔드는 등의 행동을 할 수 있습니다. 이때 각 강아지를 나타내는 객체들을 효율적으로 관리하기 위해 객체 배열을 사용할 수 있습니다.
- 여러 개의 객체를 효율적으로 관리: 객체 배열을 사용하면 같은 종류의 객체를 여러 개 만들고 관리하는 것이 훨씬 쉬워진다. 마치 여러 개의 물건을 상자에 담아 정리하는 것처럼, 객체 배열을 사용하면 많은 객체들을 깔끔하게 관리할 수 있다.
- 객체들의 정보를 쉽게 접근하고 변경: 객체 배열을 사용하면 각 객체의 정보에 쉽게 접근하고 변경할 수 있다. 예를 들어, 학생들의 정보를 담은 객체 배열을 만들면, 각 학생의 이름, 나이, 성적 등을 쉽게 확인하고 수정할 수 있다.
객체 배열과 구조체의 차이점
- 데이터 저장 방식: 객체 배열은 객체 자체를 저장하는 반면, 구조체는 데이터 멤버만 저장합니다.
- 멤버 함수: 객체 배열의 각 요소는 멤버 함수를 가질 수 있지만, 구조체는 멤버 함수를 가질 수 없어요.
- 메모리 할당: 객체 배열은 선언 시에 메모리가 할당되지만, 구조체는 변수를 선언할 때 메모리가 할당됩니다.
객체 열거 enum class
- 의미 있는 이름으로 값을 표현: 숫자나 문자열 대신 의미 있는 이름을 사용하여 값을 표현할 수 있습니다. 예를 들어, 요일을 나타낼 때 0, 1, 2... 대신 MONDAY, TUESDAY, WEDNESDAY...와 같은 이름을 사용하면 코드를 이해하기 훨씬 쉬워집니다.
- 강력한 타입 안정성: enum class는 기존의 enum보다 더 강력한 타입 안정성을 제공합니다. 즉, 다른 enum class 또는 정수형과 암묵적으로 변환되지 않으므로 예기치 않은 오류를 방지할 수 있습니다.
- 범위 지정: enum class는 값들의 범위를 명확하게 지정할 수 있습니다. 이를 통해 코드의 안정성을 높이고, 실수로 잘못된 값을 사용하는 것을 방지할 수 있습니다.
enum class (enum class명) { 값1, 값2, 값3, 값4};
변수에 대입할 때
enum클래스명 (클래스_변수명지정) = (enum class명) :: 값3 ;
enum 과 enum class의 차이점
- 1. 타입 안정성
- enum: 암묵적으로 정수형으로 변환될 수 있어요. 이는 의도치 않은 오류를 발생시킬 수 있습니다. 예를 들어, 서로 다른 enum 타입의 값들을 비교하거나 연산할 때 문제가 발생할 수 있죠.
- enum class: 더 강력한 타입 안정성을 제공합니다. 다른 enum class나 정수형으로 암묵적으로 변환되지 않기 때문에, 실수로 잘못된 값을 사용하는 것을 방지할 수 있습니다. 마치 서로 다른 종류의 블록을 섞어 쌓을 수 없는 것처럼, enum class는 타입 안정성을 보장해 줍니다.
- enum: 열거형 이름 없이 열거자(enumerator)를 직접 사용할 수 있습니다. 이는 이름 충돌의 가능성을 높입니다. 예를 들어, 두 개의 enum에 같은 이름의 열거자가 있을 경우, 컴파일러는 어떤 enum의 열거자인지 구분할 수 없어 오류가 발생할 수 있습니다.
- enum class: 열거형 이름과 범위 지정 연산자(::)를 함께 사용하여 열거자에 접근해야 합니다. 이는 이름 충돌을 방지하고 코드의 명확성을 높여줍니다. 마치 서랍장에서 물건을 찾을 때 서랍 이름을 먼저 확인하는 것처럼, enum class는 값의 범위를 명확하게 해 줍니다.
- enum: 기본 자료형이 int로 고정되어 있습니다.
- enum class: 기본 자료형을 명시적으로 지정할 수 있습니다 (int, char, unsigned int 등). 이를 통해 메모리 사용량을 최적화할 수 있습니다.
특징 | enum | enum class |
타입 안정성 | 약함 | 강함 |
범위 | 전역 범위 | 열거형 범위 |
기본 자료형 | int | 지정 가능 |