IT공부/IT서적

[윤성우 열혈 C++프로그래밍] Part2. 객체지향의 도입

shine94 2024. 8. 15. 16:56

** 구조체 등장 이유?

 : 연관 있는 데이터를 하나로 묶으면, 프로그램의 구현 및 관리가 용이

 

1) C++은 구조체 안에 함수와 enum 상수의 사용이 가능함

#include <iostream>
using namespace std;

struct Car
{
    // 구조체 안에 enum 상수의 선언
    enum
    {
        ID_LEN = 20,
        MAX_SPD = 200,
        FUEL_STEP = 2,
        ACC_STEP = 10,
        BRK_STEP = 10
    };

    // 구조체 변수의 선언
    char gamerID[ID_LEN];	// 소유자 ID
    int fuelGauge;		// 연료량
    int curSpeed;		// 현재속도

    // 구조체 안에 함수 삽입
    void ShowCarState()
    {
        cout << "소유자ID: " << gamerID << endl;
        cout << "연료량: " << fuelGauge << "%" << endl;
        cout << "현재속도: " << curSpeed << "km/s" << endl << endl;
    }

    void Accel()
    {
        if (fuelGauge <= 0)
            return;
        else
            fuelGauge -= FUEL_STEP;

        if (curSpeed + ACC_STEP >= MAX_SPD)
        {
            curSpeed = MAX_SPD;
            return;
        }

        curSpeed += ACC_STEP;
    }

    void Break()
    {
        if (curSpeed < BRK_STEP)
        {
            curSpeed = 0;
            return;
        }

        curSpeed -= BRK_STEP;
    }
};

int main()
{
    Car run99 = { "run99", 100, 0 };
    run99.Accel();
    run99.Accel();
    run99.ShowCarState();
    run99.Break();
    run99.ShowCarState();

    cout << endl;

    Car run77 = { "run77", 100, 0 };
    run77.Accel();
    run77.Break();
    run77.ShowCarState();
    return 0;
}

 

2) C++의 함수는 외부로 뺄 수 있음

#include <iostream>
using namespace std;

struct Car
{
    enum
    {
        ID_LEN = 20,
        MAX_SPD = 200,
        FUEL_STEP = 2,
        ACC_STEP = 10,
        BRK_STEP = 10
    };

    char gamerID[ID_LEN];
    int fuelGauge;
    int curSpeed;

    // 함수는 외부로 뺄 수 있다
    void ShowCarState();
    void Accel();
    void Break();
};

void Car::ShowCarState()
{
    cout << "소유자ID: " << gamerID << endl;
    cout << "연료량: " << fuelGauge << "%" << endl;
    cout << "현재속도: " << curSpeed << "km/s" << endl << endl;
}

void Car::Accel()
{
    if (fuelGauge <= 0)
        return;
    else
        fuelGauge -= FUEL_STEP;

    if (curSpeed + ACC_STEP >= MAX_SPD)
    {
        curSpeed = MAX_SPD;
        return;
    }

    curSpeed += ACC_STEP;
}

void Car::Break()
{
    if (curSpeed < BRK_STEP)
    {
        curSpeed = 0;
        return;
    }

    curSpeed -= BRK_STEP;
}

int main()
{
    Car run99 = { "run99", 100, 0 };
    run99.Accel();
    run99.ShowCarState();
    run99.Break();
    run99.ShowCarState();
    return 0;
}

 

** 구조체 안에 함수가 정의되어 있으면, 함수를 인라인으로 처리

   ㄴ 함수를 외부로 빼면 이러한 의미가 사라지므로, 키워드 inline을 이용하여 인라인 처리를 명시적으로 지시

 

** C++의 구조체는 클래스 일종

[차이점]

1) 키워드를 struct를 대신해서 class를 사용하면, 구조체가 아닌 클래스가 됨

2) 접근제어 지시자를 선언하지 않았을 때, 클래스는 private으로 구조체는 public으로 선언

 

** 접근제어 지시자(접근제어 레이블)

1) public : 어디서든 접근허용

2) protected : 상속관계에 놓여있을 때, 유도(=자식) 클래스에서의 접근 허용

3) private : 클래스 내에서만 접근 허용

 

** 레이블

 : 접근제어 지시자의 뒤에는 세미콜론이 아닌 콜론이 붙는데,

   이는 접근제어 지시자가 특정 위치 정보를 알리는 '레이블(라벨)'이기 때문

   우리가 알고 있는 switch문에 사용되는 case도 레이블이기 때문에 콜론이 붙음

 

** 클래스를 구성하는 변수와 함수를 '멤버변수'와 '멤버함수'라고 부름

 

▼ Car.h : 헤더파일, 클래스의 선언을 담는다, 클래스의 선언(declaration)

#ifndef __CAR_H__
#define __CAR_H__

namespace CAR_CONST
{
	enum 
	{
		ID_LEN = 20,
		MAX_SPD = 200,
		FUEL_STEP = 2,
		ACC_STEP = 10,
		BRK_STEP = 10
	};
}

class Car
{
private:
	char gameID[CAR_CONST::ID_LEN];
	int fuelGauge;
	int curSpeed;
public:
	void InitMembers(const char* ID, int fuel);
	void ShowCarState();
	void Accel();
	void Break();
};

#endif // !__CAR_H__

 

▼ Car.cpp : 소스파일, 클래스의 정의(멤버함수의 정의)를 담는다, 클래스의 정의(definition)

#include <iostream>
#include <cstring>
#include "Car.h"

using namespace std;

void Car::InitMembers(const char* ID, int fuel)
{
	strcpy_s(gameID, ID);
	fuelGauge = fuel;
	curSpeed = 0;
}

void Car::ShowCarState()
{
	cout << "소유자ID: " << gameID << endl;
	cout << "연료량: " << fuelGauge << "%" << endl;
	cout << "현재속도: " << curSpeed << "km/s"  << endl << endl;
}

void Car::Accel()
{
	if (fuelGauge <= 0)
		return;
	else
		fuelGauge -= CAR_CONST::FUEL_STEP;

	if ((curSpeed + CAR_CONST::ACC_STEP) >= CAR_CONST::MAX_SPD)
	{
		curSpeed = CAR_CONST::MAX_SPD;
		return;
	}
	curSpeed += CAR_CONST::ACC_STEP;
}

void Car::Break()
{
	if (curSpeed < CAR_CONST::BRK_STEP)
	{
		curSpeed = 0;
		return;
	}
	curSpeed -= CAR_CONST::BRK_STEP;
}

 

▼ Main.cpp

#include "Car.h"

int main(void)
{
	Car run99;
	run99.InitMembers("run99", 100);
	run99.Accel();
	run99.Accel();
	run99.Accel();
	run99.ShowCarState();
	run99.Break();
	run99.ShowCarState();
	return 0;
}

 

** 인라인 함수는 헤더파일에 함께 넣어야 함

   ㄴ 컴파일 과정에서 함수의 호출 문이 있는 곳에 함수의 몸체 부분이 삽입되어야 하므로

 

** 객체지향 프로그래밍

 : 현실에 존재하는 사물과 대상, 그래도 그에 따른 행동을 있는 그대로 실체화시키는 형태의 프로그래밍

 

** 객체는 하나 이상의 상태 정보(데이터)와 하나 이상의 행동(기능)으로 구성

 

** 클래스 기반의 두 가지 객체 생성 방법

1) 일반적인 변수의 선언방식

ClassName objName;

 

2) 동적 할당방식(힙 할당방식)

ClassName * ptrObj = new ClassName

 

** 객체 간의 대화 방법 - Message Passing, 메시지 전달

 : 하나의 객체가 다른 하나의 객체에게 메시지를 전달하는 방법은 함수 호출을 기반

 

** 정보은닉(Information Hiding)

 : 멤버변수를 private으로 선언하고,

   해당 변수에 접근하는 함수를 별도로 정의하여, 안전한 형태로 멤버 변수의 접근을 유도

   ㄴ 엑세스 함수(access function) : GetXXX, SetXXX

 

** const 함수

 : 이 함수 내에서는 멤버변수에 저장된 값을 변경하지 않겠다

 

** 캡슐화(Encapsulation)

 : 관련 있는 함수와 변수를 하나의 클래스 안에 묶는 것

 

** 생성자(Constructor)

 : 객체 생성시 딱 한번 호출

   ㄴ 특징

        ① 클래스의 이름과 함수의 이름이 동일

        ② 반환형 선언되어 있지 않으며, 실제로는 반환하지 않음

   ㄴ 디폴트 생성자(Default Constructor) : 객체가 되기 위해서는 반드시 하나의 생성자가 호출되어야 함

 

+ 멤버 이니셜라이저(Member Initializer)를 이용한 멤버 초기화

 : 이니셜라이저를 이용하면 선언과 동시에 초기화가 이뤄지는 형태로 바이너리 코드가 생성

   const 멤버변수도 이니셜라이저를 이용하면 초기화가 가능

#include <iostream>
using namespace std;

class FruitSeller
{
private:
	const int APPLE_PRICE;
	int numOfApples;
	int myMoney;

public:
	FruitSeller(int price, int num, int meney) 
		: APPLE_PRICE(price), numOfApples(num), myMoney(meney)
	{}

	int SaleApple(int money)
	{
		int num = money / APPLE_PRICE;
		numOfApples -= num;
		myMoney += money;
		return num;
	}

	void ShowSalesResult() const
	{
		cout << "남은 사과" << numOfApples << endl;
		cout << "판매 수익" << myMoney << endl << endl;
	}
};

class FruitBuyer
{
private:
	int myMoney;
	int numOfApples;

public:
	FruitBuyer(int meney) : myMoney(meney), numOfApples(0)
	{}

	void BuyApples(FruitSeller& seller, int money)
	{
		numOfApples += seller.SaleApple(money);
		myMoney -= money;
	}

	void ShowBuyResult() const
	{
		cout << "현재 잔액" << myMoney << endl;
		cout << "사과 개수" << numOfApples << endl << endl;
	}
};

int main(void)
{
	FruitSeller seller(1000, 20, 0);
	FruitBuyer buyer(5000);
	buyer.BuyApples(seller, 2000);

	cout << "과일 판매자의 현황" << endl;
	seller.ShowSalesResult();
	cout << "과일 구매자의 현황" << endl;
	buyer.ShowBuyResult();
	return 0;
}

 

** 소멸자(Destructor)

 : 객체 소멸시 반드시 호출

   ㄴ 특징

      ① 클래스의 이름 앞에 '~'가 붙은 형태의 이름을 갖음

      ② 반환형이 선언되지 있지 않으며, 실제로 반환하지 않음

      ③ 매개변수는 void형으로 선언되어야 하기 때문에 오버로딩도, 디폴트 값 설정도 불가능

 

** this 포인터

 : 멤버함수 내에서는 this라는 이름의 포인터를 사용할 수 있는데,

   이는 객체 자기 자신을 가리키는 용도로 사용되는 포인터

public:
    void ThisFunc(int num)
    {
    	this->num = 207;
        num = 105;		// 매개변수의 값을 105로 변경
    }

 

** 복사 생성자(copy constructor)

 : 한 객체의 내용을 다른 객체로 복사하여 생성된 생성자

   ㄴ 복사 생성자를 정의하지 않으면,

       멤버 대 멤버의 복사를 진행하는 디폴트 복사 생성자가 자동으로 삽입됨

#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num1;
	int num2;
public:
	SoSimple(int n1, int n2) : num1(n1), num2(n2)
	{}

	// 객체로 받는 생성자
	// 이니셜라이저를 이용해 멤버 대 멤버의 복사를 진행
	// 아래의 생성자를 가리켜 복사 생성자(copy constructor)라고 부른다
	SoSimple(SoSimple& copy) : num1(copy.num1), num2(copy.num2)
	{
		cout << "Called SoSimple(SoSimple &copy)" << endl;
	}

	void ShowSimpleData()
	{
		cout << num1 << endl;
		cout << num2 << endl;
	}
};

int main(void)
{
	SoSimple sim1(15, 30);
	cout << "생성 및 초기화 직전" << endl;
	SoSimple sim2 = sim1;	// SoSimple sim2(sim1); 으로 변환
                                // 즉, 묵시적 형변환
                                // 이를 방지하기 위해 키워드 explicit를 이용하면 
                                // 더이상 대입연산자로 사용 불가능
	cout << "생성 및 초기화 직후" << endl;
	sim2.ShowSimpleData();
	return 0;
}

 

https://velog.io/@sjongyuuu/C-%EB%B3%B5%EC%82%AC-%EC%83%9D%EC%84%B1%EC%9E%90Copy-Constructor

 

C++ 복사 생성자(Copy Constructor)

지난 생성자와 초기화 리스트편 포스팅에 이어 이번 포스팅에서는 복사 생성자(Copy Constructor)에 대해 다루어 보려고 한다.

velog.io

 

** 위의 예시와 디폴트 복사 생성자는 얕은 복사(shallow copy)

 : 멤버 대 멤버를 단순히 복사함

   ㄴ 멤버 변수가 힙의 메모리 공간을 참조하는 경우에 문제됨

       [어떤 문제?] 하나의 문자열을 두 개의 객체가 공유하는데

                            만약에, 둘 중 하나의 객체를 소멸해야 하는데

                                         그 과정에서 문자열이 소멸되는 경우가 만약에 생기게 된다면..?

                            혹은, 둘 중 하나의 객체의 문자열에만 내용을 바꿔야 한다면...?

#include <iostream>
using namespace std;

class Human
{	
public:
	int age;
	char* name;
	Human(int n1, const char* s) : age(n1)
	{
		name = new char[strlen(s) + 1];
		strcpy_s(name, strlen(s) + 1, s);
	}

	Human(Human& copy) : age(copy.age), name(copy.name)
	{ }

	void ShowSimpleData()
	{
		cout << "나이 : " << age << ", 나이 주소: " << &age << " // 이름: " << name << ", 이름 주소: " << &name << endl;
	}
};

int main(void)
{
	Human sim1(15, "Shine");
	Human sim2 = sim1;

	////////////////////////////////////////////////////
	sim1.ShowSimpleData();
	sim2.ShowSimpleData();

	cout << "=================" << endl;

	////////////////////////////////////////////////////
	const char* changeName = "Shine Muscat";
	strcpy_s(sim1.name, strlen(changeName) + 1, changeName);
	sim1.age = 100;

	////////////////////////////////////////////////////
	sim1.ShowSimpleData();
	sim2.ShowSimpleData();

	////////////////////////////////////////////////////
	cout << endl << endl;
	cout << "sim1 주소 : " << &sim1 << " // sim2 주소 : " << &sim2 << endl;
	return 0;
}

 

 

** 깊은 복사(deep copy)

 : 멤버변수가 참조하는 문자열까지 복사를 해서 각각의 객체가 완전히 별개의 문자열을 소유

Human(const Human& copy) : age(copy.age)
{
    name = new char[strlen(copy.name) + 1];
    strcpy(name, copy.name)
}

 

** 메모리 공간 할당과 초기화가 동시에 일어나는 상황

1) num1이라는 이름의 메모리 공간 할당과 동시에 num2에 저장된 값으로 초기화 하는 문장

int num1 = num2;

 

2) SimpleFunc 함수가 호출되는 순간에 매개변수 n이 할당과 동시에 변수 num에 저장되어 있는 값으로 초기화

int SimpleFunc(int n)
{
    ...
}

int main(void)
{
    int num = 10;
    SimpleFunc(num);	// 호출되는 순간 매개변수 n이 할당과 동시에 초기화
    ...
}

 

3) 함수가 값을 반환하면, 별도의 메모리 공간에 할당되고, 이 공간에 반환 값이 저장(반환 값으로 초기화)

int SimpleFunc(int n)
{
    ...
    return n;	// 반환하는 순간 메모리 공간에 할당되면서 동시에 초기화
}

int main(void)
{
    int num = 10;
    cout << SimpleFunc(num) << endl;
    ...
}

 

** 복사 생성자가 호출되는 시점은?

1) 기존에 생성된 객체를 이용해서 새로운 객체를 초기화하는 경우

2) Call-by-value 방식의 함수호출 과정에서 객체를 인자로 전달하는 경우

3) 객체를 반환하되, 참조형으로 반환하지 않는 경우

 

** 임시객체가 사라지는 타이밍

1) 다음 행으로 넘어가면 바로 소멸

Temporary(100);

 

2) 참조자에 참조되는 임시객체는 바로 소멸하지 않음

const Temporary &ref = Temporary(100);

 

** const 객체

 : 이 객체의 데이터 변경을 허용하지 않음

   ㄴ const의 선언 유무도 함수 오버로딩의 조건에 해당

       (예) void SimpleFunc() { ... }

              void SimpleFunc() const { ... }

#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num;

public:
	SoSimple(int n) : num(n) 
	{}
	
	SoSimple& AddNum(int n)
	{
		num += n;
		return *this;
	}

	void SimpleFunc()
	{
		cout << "SimpleFunc " << num << endl;
	}

	void SimpleFunc() const
	{
		cout << "const SimpleFunc " << num << endl;
	}
};

void YourFunc(const SoSimple& obj)
{
	obj.SimpleFunc();
}

int main(void)
{
	SoSimple obj1(2);
	const SoSimple obj2(7);

	obj1.SimpleFunc();
	obj2.SimpleFunc();

	YourFunc(obj1);
	YourFunc(obj2);
}

 

 

** friend

 : A 클래스가 B 클래스를 대상으로 friend 선언을 하면, B 클래스는 A 클래스의 private 맴버에 직접 적용 가능

   ㄴ 정보은닉의 규칙을 깨기 때문에 지나치면 아주 위험할 수 있으니 꼭 필요할 때 소극적으로 사용해야 함

   ㄴ 전역함수를 대상으로 사용 가능

 

** static

1) 전역변수에 선언 - C 에서 배움

 : 선언된 파일 내에서만 참조를 허용

 

2) 함수 내에서 선언 - C 에서 배움

 : 한번만 초기화되고, 지역변수와 달리 함수를 빠져나가도 소멸하지 않음

 

3) static 멤버변수(= 클래스 변수)

: 일반적인 멤버변수와 달리 클래스당 하나씩만 생성

 

4) static 멤버함수

 : 선언된 클래스의 모든객체가 공유

   public 으로 선언이 되면, 클래스의 이름을 이용해서 호출 가능

   객체의 맴버로 존재하는 것이 아님

 

5) const static 멤버

 : 클래스 내에 선언된 const 멤버변수(상수)의 초기화는 이니셜라이저를 통해서만 가능

 

** 키워드 mutable

 : const 함수 내에서의 값의 변경을 예외적으로 허용

 

 

** 해당 글은 윤성우의 열혈 C++ 프로그래밍 도서를 읽고 정리한 글입니다.

   https://product.kyobobook.co.kr/detail/S000001589147

 

윤성우의 열혈 C++ 프로그래밍 | 윤성우 - 교보문고

윤성우의 열혈 C++ 프로그래밍 | 『C++ 프로그래밍』은 C언어를 이해하고 있다는 가정하에서 집필된 C++ 기본서로서, 초보자에게 적절한 설명과 예제를 통해서 C++ 프로그래밍에 대해 설명한다. 더

product.kyobobook.co.kr