C++로 마스터하는 객체지향 언어와 STL
Ch 01. 클래스
객체지향 개괄
객체지향도 절차지향적으로 코드를 작성하지만 객체를 중심으로 기술한다. 어떤 방식으로 사고를 하며 코드를 작성하는 것인지가 중요하다.
{ //C++ 스타일
string s0 = "hello";
string s1 = "world";
s0.append(s1); //s0 += s1;
cout << s0 << endl;
}
{ // C 스타일
char s2[100] = "hello";
char s3[] = "world";
strcat(s2, s3);
cout << s2 << endl;
}
C style : 내부 구현이 드러나 있고 이를 고려해서 작성해야한다.
C++ Style : 내부 구현이 드러나 있지 않은 추상화 된 상태로 작성한다.
클래스와 객체
class Player //클래스
{
int health;
int damage;
};
int main()
{
Player player0; //객체
return 0;
}
struct와 유사하지만 단 하나의 차이점이 있다. 클래스는 기본 접근 지정자가 private이고 구조체는 public 이다.
클래스의 필드(데이터)를 private으로 제한하는 이유는 아무곳에서나 접근할 수 있으면 오류가 발생할 확률이 높아지기 때문이다.
class Player
{
private:
int _health;
int _power;
string _name;
public:
Player(int health, int power, string name) //생성자
{
_health = health;
_power = power;
_name = name;
}
void attack(Player4& target)
{
cout << "Attack " << _name << "->" << target._name << endl;
target.damaged(_power);
}
void damaged(int power)
{
_health -= power;
if (_health <= 0)
cout << "Died" << _name << endl;
}
};
int main()
{
{
Player david{ 200, 100, "David" };
Player daniel{ 200, 100, "Daniel" };
//daniel._health -= 300; // 불가능하다
david.attack(daniel);
david.attack(daniel);
}
}
생성자
생성자란 객체가 생성될 때 호출되는 함수로, 반환형이 없고 클래스의 이름과 함수의 이름이 같다. 생성자는 private 영역의 데이터를 초기화 해주는 역할을 한다.
class Player
{
private:
int _health;
int _power;
string _name;
public:
Player(int health, int power, string name) //생성자
{
_health = health;
_power = power;
_name = name;
}
};
생성자를 따로 정의하면 디폴트 생성자는 삽입되지 않기 때문에 직접 정의해주어야 한다.
class Player0
{
private:
int _health;
int _power;
public:
Player0()
{
// 디폴트 생성자(파라메터가 한 개도 없는 생성자)
// 한 개의 생성자도 주어지지 않는다면 컴파일러가 암시적으로 만들어준다
_health = 100;
_power = 50;
cout << "Player " << _health << ", " << _power << endl;
}
Player0(int health, int power)
{
_health = health;
_power = power;
cout << "Player " << _health << ", " << _power << endl;
}
};
void player0()
{
Player0 player0(100, 50);
Player0 player1 = Player0(300, 150);
Player0* player2 = new Player0(400, 250);
Player0 player3;
Player0 player4 = Player0();
Player0* player5 = new Player0();
}
위임 생성자는 생성자 내에서 다른 생성자를 호출하는 방법이다.
class Player3
{
private:
int _health;
int _power;
public:
Player3() : Player3(100, 50) // 위임 생성자
{
}
Player3(int health, int power)
{
_health = health;
_power = power;
cout << "Player " << _health << ", " << _power << endl;
}
void print()
{
cout << _health << endl;
cout << _power << endl;
}
};
void player3()
{
Player3 player;
player.print();
}
이니셜라이저로 const 멤버를 초기화 하는 방법
Player(int health, int power, string name)
: _health(health), _power(power), _name(name) //이니셜 라이저
// 멤버 초기화 리스트, 순서가 바뀌어도 초기화 순서는 클래스에 선언된 순서이다
{
cout << "Player " << _health << ", " << _power << endl;
}
참조자 멤버 변수를 초기화 할때도 위와 같이 초기화 한다. 성능에서 약간의 이점이 있다.
default, delete 키워드를 사용하여 생성자 관리
class Player
{
private:
// 인라인 초기화, 명시적으로 초기화 하지 않으면 아래값으로 초기화 된다.
int _health = 100;
int _power = 50;
const string _name = "noname";
public:
Player5() = default; // 컴파일러가 만든 생성자를 사용하겠다는 것을 명시
Player5() = delete; // 컴파일러가 생성자를 암시적으로 만들지 말 것을 명시
};
파괴자
파괴자는 클래스가 소멸하는 시점에 데이터를 정리하는 역할을 맡는다. 만약 동적할당으로 생성된 객체가 있다면 delete해주어야 파괴자가 호출된다.
class String
{
private:
char* _str;
public:
String(const char* str)
{
int len = strlen(str);
_str = new char[len + 1]; // '\\0;
strcpy(_str, str);
}
~String()
{
delete[] _str; // 파괴자에서 해제를 해줘야한다
}
void print()
{
cout << _str << endl;
}
};
선언과 정의의 분리 & 전방 선언
선언의 두가지 종류
- 정의가 포함된 선언 : 함수의 정의
- 정의가 포함되지 않은 선언 : 함수의 프로토타입
C++ 에서는 원칙적으로 중복정의가 허용되지 않는다. 하지만 예외적인 경우가 몇가지 있다.
- 클래스
- 구조체
- 열거형
- 인라인 함수
한 파일 내에서는 한번만 정의되야 하지만 다른 소스파일에서 사용하려면 같은 내용으로 해당 소스파일내에서 한번 더 정의해주어야 한다. 하지만 매번 다른 소스파일에서 재정의 해주는것은 매우 번거롭기 때문에 헤더파일을 이용한다.
파일을 분리할 때는 크게 세가지 분류로 분리할 수 있다.
- main.cpp : 메인 함수를 포함하는 소스파일
- util.cpp : 클래스의 멤버함수를 포함한 다른 함수들의 정의가 포함된 소스파일
- util.h : 클래스와 함수의 선언이 포함된 헤더파일
헤더파일에 함수의 정의까지 넣게 되면 중복 정의가 되기 때문에 오류가 발생한다. 그래서 cpp파일로 분리하는 것이다. 또한 클래스내부에서 외부로 함수의 정의를 분리하게 되면 캡슐화를 할 수 있지만 자동 인라인화가 되지 않는다. (클래스 내부 정의는 자동 인라인화 됨)
클래스가 직접 사용되지 않고 단순히 포인터나 참조로 사용된다면 전방선언으로 해결할 수 있다.
#include <iostream>
#include "function.h"
#include "Villiage.h"
#include "person.h"
using namespace std;
void foo1(); // util1.cpp
int main()
{
func();
foo1();
Villiage v;
v.add(new Person(10, 10, "david"));
}
#pragma once
// #pragma once -> 헤더파일은 한 번만 include 하도록 함, 비표준
// #ifndef #define #endif -> 헤더파일을 한 번만 include 하도록 함, 표준
#include <iostream>
#include <string>
// Person 클래스의 선언 분리
class Person
{
private:
float _weight;
float _height;
const std::string _name;
public:
Person(float weight, float height, const std::string& name);
void print();
};
// 함수의 선언
void foo();
#include "person.h"
// Person 클래스 함수들의 정의 분리
Person::Person(float weight, float height, const std::string& name)
: _weight(_weight), _height(height), _name(name)
{
}
void Person::print()
{
using namespace std;
cout << _name << endl;
}
// 함수의 정의 분리
void foo()
{
Person p(60.f, 160.f, "davoid");
p.print();
}
#pragma once
#include <vector>
//#include "person.h" // 헤더 파일 include는 최소화 하자
class Person; // 전방 선언
class Villiage
{
private:
// 전방 선언을 위해서는 포인터나 레퍼런스 형이어야 함.
// 포인터나 레퍼런스는 사이즈가 알려져있기 떄문
std::vector<Person*> persons;
public:
void add(Person* person);
};
#include "Villiage.h"
#include "person.h"
void Villiage::add(Person* person)
{
//함수의 정의
}
this 포인터
this 포인터는 자기 자신을 가르키는 포인터이다. this 포인터를 사용하면 변수 충돌을 방지할 수 있고, 자기 자신을 반환 할 때 유용하게 사용한다.
class Person
{
private:
float weight;
float height;
string name;
public:
Person(float weight, float height, const string& NAME)
: weight(weight), height(height), name(NAME)
{
// 이름이 같은 경우 this로 구분 가능
weight; // 파라메터 weight
this->weight; // 멤버 변수 weight
// 이름이 같지 않기 때문에 this 생략 가능
name; // 멤버 변수 name
this->name; // 멤버 변수 name
}
void loseWeight(float weight)
{
// 아래의 둘은 같다
this->weight;
weight;
this->weight -= weight;
if (this->weight < 0)
this->weight = 0;
}
float getBMI()
{
return weight / (height * 100 * height * 100);
}
Person& complete(Person& person)
{
if (this->getBMI() < person.getBMI())
return *this; // this의 반환
else
return person;
}
void doCeremony()
{
cout << name << " win!!" << endl;
}
};
void personFunc()
{
Person david(67.3f, 172.3f, "david");
Person daniel(58.3f, 167.3f, "daniel");
daniel.complete(david).doCeremony(); //승리한 객체가 세레모니를 호출함
}
this 포인터를 이용한 연쇄 호출
struct Transaction
{
const int txID;
const int fromID;
const int toID;
const int value;
class Builder // nested class
{
private:
int _fromID;
int _toID;
int _value;
public:
Transaction build()
{
int txID = _fromID ^ _toID ^ _value;
return Transaction{txID, _fromID, _toID, _value};
}
Builder& setFromID(int fromID)
{
_fromID = fromID;
return *this;
}
Builder& setToID(int toID)
{
_toID = toID;
return *this;
}
Builder& setValue(int value)
{
_value = value;
return *this;
}
};
};
void buildTransaction()
{
Transaction::Builder builder;
builder.setFromID(1).setToID(2).setValue(100); //연쇄 호출
Transaction tx = builder.build();
}
const
일반 포인터에 컨스트 포인터를 대입할 수 없다. 왜냐하면 일반 포인터를 통해 간접적으로 값을 바꿀 수 있기 때문이다. 또한 컨스트 객체는 컨스트 함수만 호출할 수 있다. 그래서 값을 변경하지 않는 함수에는 const 키워드를 붙여 const 객체에서 호출할 수 있게 한다.
class Person
{
private:
const string _name = "david"; // const 멤버 변수는 인라인으로 초기화 가능
float _weight;
float _height;
public:
Person(const string& name, float weight, float height)
: _name(name), _weight(weight), _height(height)
// const 멤버 변수는 멤버 초기화 리스트에서 초기화 가능
{}
float getWeight(/* const Person* this */) const
{
// const가 붙은 멤버 함수에서의 this 포인터의 타입
// const Person*
return _weight;
}
float getHeight(/* Person* this */)
{
// this 포인터의 타입
// Person*
return _height;
}
};
int main()
{
const Person person("David", 75.f, 181.f);
cout << person.getWeight() << endl; // getWeight에 const Person*가 넘어간다.
//cout << person.getHeight() << endl; // getHeight에 const Person*을 넘길 수 없다.
Person *person0 = new Person("Daniel", 57.f, 175.f);
const Person* person1 = person0; // 변환 가능
//Person* person2 = person1; // 변환 불가능
}
정적 멤버
자동 지역변수 : 지역을 벗어나면 메모리가 해제됨
정적 지역변수 : 실행 중 한번만 초기화되고 프로그램이 끝날때 해제됨
클래스 정적 멤버 변수도 똑같이 시행된다. 접근 제한이 클래스 이름인 하나의 전역변수로 볼 수 있다. 클래스 멤버 변수 또한 객체가 여러개 생성되어도 한번만 생성된다.
- 정적 멤버 함수 : static 멤버함수 또는 static 멤버변수에만 접근이 가능하다.
- 일반 멤버 함수 : 모든 멤버에 접근이 가능하다.
class Person
{
private:
static int num0;
public:
static int num1; //inline static int num1 = 0; 처럼 인라인 초기화 가능
static const int num2; //인라인 초기화 가능
Person();
void print();
static void staticPrint();
};
void personFunc();
헤더파일에서 멤버변수로 다른 클래스를 포함하고 있다면 전방선언으로 해결할 수 있다. 하지만 인라인 초기화까지 한다면 전방선언으로 되지 않는다.
// 클래스 멤버 변수 초기화
int Person::num0 = 0;
int Person::num1 = 0;
const int Person::num2 = 0; // const 초기화
Person::Person()
{
num0++;
}
void Person::print()
{
cout << num0 << endl;
}
void Person::staticPrint()
{
cout << num0 << endl;
//print(); // 비 정적 멤버 접근 불가, 즉 this가 없다
}
void personFunc()
{
Person p0;
Person p1;
p0.print();
p1.print();
// public static 멤버 접근
cout << Person::num1 << endl;
Person::staticPrint();
p0.staticPrint();
}
멤버 함수 포인터
class Person
{
public:
void print(int i) //일반 멤버 함수
{
cout << i << endl;
}
static void staticPrint(int i) //정적 멤버 함수
{
cout << i << endl;
}
};
int main()
{
void (Person:: * fn)(int) = &Person::print; // &을 꼭 붙여야한다.
Person person;
(person.*fn)(1); // this를 넘겨줘야 함
typedef void (Person::* Func0)(int); // typedef 를 사용하여 간략히 표기
Func0 fn0 = &Person::print;
(person.*fn0)(2);
using Func1 = void (Person::*)(int); //using 사용
Func1 fn1 = &Person::print;
(person.*fn1)(3);
function<void(Person*, int)> fn2 = &Person::print; //function 사용
fn2(&person, 4);
// static은 일반 함수와 동일
void (*fn3)(int) = &Person::staticPrint; // &없이도 가능
fn3(5);
}
'C & C++ > C++' 카테고리의 다른 글
[C++] 상속 (0) | 2024.08.05 |
---|---|
[C++] 연산자 오버로딩 (4) | 2024.07.16 |
[C++] 함수, 범위, 공간 (0) | 2024.07.08 |
[C++] 포인터, 참조 (1) | 2024.07.01 |
[C++] 흐름 제어, 복합 데이터 (0) | 2024.06.26 |