본문 바로가기
ZAION/C++

[C++]Lec05. OOP / Class

by 우기37 2024. 4. 18.

 

## 목차

1. OOP 개요

2. 클래스와 객체

3. 생성자와 소멸자

4. 생성자 오버로딩

5. 복사 생성자

6. this, const, static, friend

 

 

 

#1 OOP 개요

1 - 1) 절차적 프로그래밍이란(Procedure Programming)

프로그래밍이 수행하는 일련의 작업을 기준으로 하는 프로그래밍 패러다임이며, 명령형 프로그래밍으로도 불린다.

작업의 구현을 '함수'라고 하며, 함수의 집합을 '프로그램'이라고 한다.

중요한 점은, 데이터와 작업이 분리되어 있는 개념인 점이다. 그래서 함수 호출을 통해서 추상화와 재사용성을 얻어내는 것이 본질이다.

데이터는 작업의 실행을 위해 매개변수로 전달될 뿐이고, 비교적 이해가 쉬운 방식이다.

 

 

1 - 2) 절차적 프로그래밍의 단점

함수가 데이터의 구조를 정확히 알아야만 한다는 단점이 있다. 그렇기에 데이터가 변하면 함수의 수정이 필요하여 밀접한 관계를 갖고있다.

 

또한 프로그램의 규모가 커지면 데이터의 구조를 이해하기가 어렵고, 유지/보수하기가 어렵다. 유지/보수하기가 어렵기에 확장성이 낮고 코드를 재사용하기에도 어렵다. 그리고 코드가 길고 복잡하고 데이터의 구조를 변경할 때 코드의 많은 부분을 수정해야 하며 한 단계에서 발생한 오류가 다음 단계에서 영향을 미칠 수 있기 때문에 디버깅하기에도 어려움이 있다. 

 

 

1 - 3) 객체지향 프로그래밍의 개념과 장점

객체지향 프로그래밍은 Procedure Programming의 단점을 극복하기 위해 제안된 프로그래밍 패러다임 중의 하나이다.

프로그램을 수많은 '객체'(object = 메소드 + 변수)라는 기본 단위로 나누고 이들의 상호작용으로 서술하는 방식이다.\

 

C++, C#, Java 등에서는 이러한 방식을 손쉽게 구현할 수 있는 언어의 문법을 제공하기에 객체지향 프로그래밍을 사용할 수 있습니다. 그렇다고 C++, C#, Java 언어에서 절차적 프로그래밍을 구현하지 못하는 것은 아니다.

 

추가적으로 함수형 프로그래밍 등 새로운 패러다임을 적용할 수 있도록 언어는 계속 확장될 수 있다.

 

클래스와 객체를 기반으로 하며, 이를 데이터와 작업을 하나로 묶어서 표현한다.

 

특징으로는 아래와 같다.

1. 캡슐화(Encapsulation) : 캡슐화는 변수와 함수를 하나의 단위로 묶는 것을 의미한다. 이는 클래스 안에서 이루어지는데 클래스는 (데이터) + (데이터를 기반으로 기능=함수)을 모두 포함한다.

 

2. 정보은닉(Information Hiding) : 잘못된 사용과 수정을 방지하기 위해 사용자는 내부 구현에 대해 알 필요도 없고, 알아서도 안된다. 사용자는 외부로 노출된 인터페이스만 활용이 가능해야한다. 테스트, 디버깅, 유지보수, 확장이 용이하다.

 

3. 상속(Inheritance) : 부모 클래스의 특징을 자식 클래스가 물려받는 것을 의미한다. 자식클래스에서 상속받은 기능을 수정해서 재정의할 수 있는데 이를, '오버라이딩(overriding)'이라고 한다. 이를 통해 캡슐화를 유지하며, 클래스의 재사용성이 용이해진다.

 

4. 다형성(Polymorphism) : 동일한 코드를 사용하여 여러 형태의 객체를 다룰 수 있는 것을 말한다. 즉, 같은 이름의 메서드를 사용하여 다른 객체들에 대해 서로 다른 동작을 수행할 수 있는 기능을 말한다.

 

 

1 - 4) 객체지향 프로그래밍의 단점

객체지향 프로그래밍은 절차적 프로그래밍의 상위 호환이 아닙니다.

잘 설계된 절차적 프로그램이 잘못 설계된 객체지향 프로그램보다 여러 방면에서 좋을 수도 있다. 객체지향 프로그래밍은 모든 문제에 어울리는 설계 방안이 아니며, 모든 대상이 클래스로 치환되는 것이 아니다. 즉, 클래스로 표현되지 않는 개념이나 데이터도 있을 수 있고, 오로지 객체의 특징과 동작이 클래스로 표현된다.

 

객체지향 프로그램은 객체라는 추상적인 개념을 만들어야 하기에 직관적이지 않아 오히려 어려울 수도 있다.

 

추상적인 구조에 대해 문제를 잘 분석하여 좋은 설계를 만들어야 하는데 어려움이 있으며, 데이터 메모리를 직접적으로 관리하지 않기에 프로그램이 무거워지고 성능에서 손해를 보거나 지나치게 복잡한 코드가 작성될 수도 있다.

 

이미지 출처 : https://coderzero.tistory.com/entry/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EA%B8%B0%ED%83%80-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8DObject-Oriented-Programming-OOP

 

 

#2 클래스와 객체

2 - 1) 클래스란?

객체(object)가 생성되기 위한 틀, 객체가 가져야 할 데이터와 기능을 정의

 

"사용자 정의 자료형(User-Defined Data-Type)" :

멤버 변수를 가짐(데이터) - 속성(property(html DOM 안에서 동적인 attribute를 표현), attribute(html의 정적인 속성, 요소)), 필드(field), 클래스 변수(class variable) == 멤버 변수와 비슷한 용어

 

멤버 함수를 가짐(함수, 동작) - Method == 멤버 함수의 또다른 용어

 

데이터와 함수를 은닉 가능하며 인터페이스를 공개 가능하다.

 

 

2 - 2) 객체란?

클래스로부터 생성된 실체, 메모리에 올라간 객체는 인스턴스로 구분하여 명명하는 경우도 있다.

객체는 개별적으로 관리되며, 원하는 만큼 생성이 가능하다.

객체를 통해 클래스에 정의된 멤버함수 호출, 멤버변수 접근이 가능하다.

 

 

2 - 3) 클래스의 선언과 객체의 생성

클래스의 이름은 자료형처럼 사용된다.

클래스를 정의하는 것은 새로운 데이터 타입을 만드는 것이다. 우리가 사용할 실체는 '객체'로 만들어 메모리에 올려야 한다.

class ClassName
{

};

 

 

객체의 생성은 변수와 동일하게 스택 또는 힙 메모리에 선택적으로 생성이 가능하다.

int main()
{
// 객체의 생성(스택)
ClassName tistory;
ClassName naverBlog;

// 객체의 생성(힙)
ClassName *google = new ClassName();
delete google;	// 객체 해제(소멸)
}

 

스택 영역은 함수의 매개변수와 지역변수가 할당되는 영역이다. 이러한 변수들은 함수 호출 시 스택에 할당되며 함수 내에서만 사용 가능하고 함수 종료 시 자동으로 소멸한다.

 

힙 영역은 개발자가 직접 관리하는 메모리 영역으로 new 키워드로 메모리를 할당하고 해제한다. 동적으로 할당된 메모리는 delete 하기 전가지 힙에 생존해 있다. 따라서 자동으로 소멸자가 호출되지 않는다.

 

google이라는 포인터 변수는 힙의 주소를 가리키고, 힙에 객체를 할당하고, 할당된 메모리 주소를 변수에 받는 과정이다.

 

클래스 멤버의 접근

멤버는 멤버 변수 + 멤버 함수를 말한다. C++에서는 변수와 함수 앞에 '멤버'를 붙여서 통칭한다.

멤버 변수/함수에 접근하기 위해서는 객체가 필요하다.(static 멤버의 경우는 예외) 어떠한 멤버 변수/함수는 "클래스 외부"에서 접근이 불가능하게 만들 수 있다.(정보 은닉)

 

ClassName Class1;

class1.name;	// class1이 가진 name 멤버 변수에 접근
class1.Move(2, 3);	// class1이 가진 Move() 멤버 함수에 접근

 

 

화살표 연산자 (member of pointer 연산자) 사용

역참조 후 점 연산자 사용하는 객체의 포인터인 경우와 동일한 의미, 축약 표현

ClassName *class1 = new ClassName();

class1->name;
class1->Move(1,1);

 

ClassName class1 = new ClassName();

(*class1).name;	// 포인터가 가리키는 곳에 있는 객체의 name에 접근
(*class1).Move(1,1);	// 포인터가 가리키는 곳에 있는 객체의 Move()에 접근

 

 

2 - 4) 접근 제한자

클래스 멤버 접근 제한자

public : 클래스의 외부에서 객체에 접근하여 사용 가능

protected : 다른 클래스에는 노출되지 않지만, 상속받은 자식 클래스에게는 노출되어 사용 가능

private : 클래스의 외부로 노출되지 않고 내부에서만 사용 가능(클래스의 멤버들만 접근 가능)

 

private 멤버에 접근하기 위해서는 public 멤버 함수가 필요하다. 그렇다면 다 public으로 해서 자유롭게 접근가능도록 하면 되는거 아닌가? 라는 의문이 드실겁니다. 그렇게 멤버에 직접 접근하는 것이 실수나 오류를 초래할 수 있다.

 

그렇기에 적재적소에 접근 제한자를 사용하는 것이 테스트 및 디버깅이 쉬워지고, 오류의 가능성이 줄어들며, 방어적 프로그래밍을 하기에 좋습니다.

 

class ClassName
{
public:
	void Study(std::string subject);
    bool IsDead();
private:
	std::string name;
    int age;
    int studentId;
};

 

 

2 - 5) 멤버 변수와 멤버 함수

멤버 함수의 구현은 기존 일반 함수의 구현과 유사합니다. 멤버 변수에 접근이 가능하기 때문에 인자로 전달할 데이터가 적어집니다.

클래스 선언 내에 구현이 가능합니다. 즉 함수의 내용을 호출을 통해서 실행시키는 것이 아니라, 호출하는 코드 자체가 함수 내용의 코드가 되는 Inline 함수 구현이 가능합니다.

 

클래스 선언 외부에서도 구현이 가능합니다.(ex. ClassName::MethodName)

명세(Specification)와 구현의 분리가 됩니다. 클래스의 선언은 .h(헤더파일) 파일에 작성하고, 클래스의 구현은 .cpp(소스코드파일)파일에 작성합니다.

 

멤버 변수는 클래스 내에서 모든 멤버함수가 사용할 수 있기 때문에 매개변수로 전달할 필요가 없지만, 주의할 점은 어떤 멤버함수가 멤버변수의 값을 변경하고 사용하는지 구분을 잘해서 사용해야 한다.

 

 

2 - 6) 명세와 구현의 파일 분리

헤더파일(.h)

Include guard를 통해 전처리기에서 중복적인 헤더 파일의 포함을 방지한다.

#indef ~ #endif, #pragma one = Include guard

 

#ifndef _ClassName_H
#define _ClassName_H

class ClassName
{
public :
	void SetStudentId(double sid);
    double GetStudentId();
private :
	double StudentId;
};

#endif

 

#pragma once

class ClassName
{
public :
	void SetStudentId(double sid);
    double GetStudentId();
private :
	double StudentId;
};

 

일반적으로 마주치는 대부분의 라이브러리는 클래스 명세(정의)와 구현이 별도의 파일로 분리 되어 있다.

 

구현파일(ClassName.cpp)

#include "ClassName.h"

void ClassName::SetStudentId(double id)
 {
 	StudentId = id;
 }
 double ClassName::GetStudentId()
 {
 	return StudentId;
 }

 

메인파일(main.cpp)

#include <iostream>
#include "ClassName.h"

int main()
{
	ClassName kim;
    kim.SetStudentId(10.0);
    double id = kim.GetStudentId();
    
    std::cout << id << std::endl;
    return 0;
}

 

 

2 - 7) 구조체 vs 클래스

C++에서는 구조체와 클래스 모두 사용이 가능하다. 문법적으로는 기본 접근 권한의 차이 외에는 차이점이 존재하지 않는다.

 

클래스 : 명시되어 있지 않으면 private이 기본값

멤버 함수를 통해서 멤버 변수에 접근하도록 get/set 구현

private 멤버변수와 멤버함수를 사용 

 

구조체 : 명시되어 있지 않으면 public 이 기본값

멤버 함수를 구조체 안에 설정하지 않는 것을 권고

public 접근이 필요한 데이터로 사용

 

// 클래스
class Person
{
	std::string name;
    std::string GetName();
};
Person p;
p.name = "kim";	// ERROR!
cout << p.GetName();	// ERROR!

// 구조체
struct Person
{
	std::string name;
    std::string GetName();
};
Person p;
p.name = "kim";	// OK!
cout << p.GetName();	// OK!

 

 

 

#3 생성자와 소멸자

3 - 1) 생성자(Constructor)

객체가 생성될 때 자동으로 호출되는 측수한 종류의 멤버 함수이다. 그리고 초기화 목적으로 유용하게 사용된다.

클래스와 동일한 이름을 가져야 하고, 반환형은 존재하지 않으며 오버로딩(Overloading)이 가능하다.

 

class Player
{
public :
	Player();
    Player(std::string name);
    Player(std::string name, int health, int xp);
private :
	std::string name;
    int health;
    int xp;
};

 

 

3 - 2) 소멸자(Destructor)

객체가 소멸할 때 자동으로 호출되는 특수한 멤버 함수이다.

메모리 및 기타 리소스(파일 close 등)해제 목적으로 유용하게 사용된다. 생성자는 초기화를 목적으로 사용되는 반면에, 소멸자는 청소를 돕는 목적이다. 그리고 클래스와 동일한 이름 앞에 "~"을 갖는 멤버 함수이다. 마찬가지로 반환형은 존재하지 않고 파라미터도 존재하지 않는다. 그렇기에 오버로딩도 불가능하다.

 

이러한 규칙때문에 소멸자는 클래스당 하나밖에 존재할 수 없고 소멸자를 명시적으로 호출하는 경우도 없다.

 

class Player
{
public :
	Player();
    Player(std::string name);
    Player(std::string name, int health, int xp);
    ~Player();
private :
	std::string name;
    int health;
    int xp;
};

 

아래와 같은 경우 객체 또는 객체의 포인터가 소멸되는 시점에 소멸자를 자동으로 호출한다.

이로인해 메모리 누수나 자원 누수를 방지하고 정리 작업을 할 수 있다.

#include <iostream>

class MyClass
{
public :
	MyClass() {
    std::cout << "Constructor called" << std::endl;
    }
    ~MyClass() {
    std::cout << "Destructor called" << std::endl;
    }
};

int main() {
	MyClass* ptr = new MyClass();	// 객체 생성
    delete ptr;	// 객체 소멸
    return 0;
}

 

 

3 - 3) 기본 생성자

인자가 없는 생성자를 말한다.

클래스의 생성자를 직접 구현하지 않으면, 컴파일러가 기본적으로 만들어 준다. 초반부에, 생성자가 없는 상태에서도 객체를 만들 수 있었다. 컴파일러가 기본 생성자를 알아서 만들어 사용했기 때문이다.

 

그러나 인자가 있는 생성자만 정의되어 있는 경우 기본 생성자가 자동으로 생성되지 않는다.

 

아래와 같이 클래스를 만들면, 

class Player
{

};

 

 

컴파일시에 아래와 같이 코드로 보이지는 않지만, 내부에 자동으로 생성자가 하나 생긴다.

class Player
{
	Player() {
    }
};

 

따라서 아래와 같이 명령문을 생성자 만들지 않고도 사용이 가능하다.

Player kim;
Player *enemy = new Player;

 

 

인자가 없는 클래스 생성자도 구현 해주는 것이 좋다. 왜냐하면 쓰레기 값은 항상 방지하는 것이 안전하기 때문이다.

class Account
{
public :
	Account() {
    name = "None";
    balance = 0.0;
    }
    bool Withdraw(double amount);
    bool Deposit(double amount);
private :
	std::string name;
    double balance;
};

 

 

 

 

#4 생성자 오버로딩

4 - 1) 생성자 오버로딩 개요

생성자도 함수이므로 오버로딩이 가능하다.

각각의 생성자는 고유해야 한다. 즉 매개변수가 달라야 한다.

class Player
{
public :
	Player();
    Player(std::string nameVal);
    Player(std::string nameVal, int healthVal, int xpVal);
private :
	std::string naem;
    int health;
    int xp;
};

 

생성자 오버로딩

Player::Player()
{
	name = "None";
    health = 0;
    xp = 0;
}

Player::Player(std::string nameVal)
{
	name = nameVal;
    health = 0;
    xp = 0;
}

Player::Player(std::string nameVal, int healthVal, int xpVal)
{
	name = nameVal;
    health - healthVal;
    xp = xpVal;
}

 

오버로딩된 생성자의 활용

Player empty;	// None, 0, 0

Player hero {"Hero");	// Hero, 0, 0

Player kim {"Kim", 100, 5};	// Kim, 100, 5

Player *player1 = new Player;	// None, 0, 0
delete player1;

Player *player2 = new Player {"Enemy2"}	// Enemy2, 0, 0
delete player2;

Player *player3 = new Player {"Enemy3, 1000, 0};	// Enemy3, 1000, 0
delete player3;

 

 

4 - 2) 생성자 초기화 리스트

이전의 생성자는 생성자 본체(body) 내에서 멤버 변수에 값을 대입했지만, 생성자 초기화 리스트를 사용할 경우 생성과 동시에 값이 지정된다.

 

이전의 생성자 초기화

Player::Player()
{
	name = "None";
    health = 0;
    xp = 0;
}

 

 

생성자 멤버 초기화 리스트(권장)

Player::Player()
	: name{"None"}, health{0}, xp{0}
    {
    }

 

 

 

4 - 3) 생성자 위임

다양한 생성자의 오버로딩에 유사한 코드가 반복적으로 사용된다. 그렇게 되면 오류의 가능성이 높아질 수 있다. 그래서 생성자 위임을 통해 오류 가능성과 코드 반복을 줄일 수 있다.

 

다른 생성자를 멤버 초기화 리스트 위치에서 호출하고 생성자 멤버 초기화 리스트를 사용해서만 가능하다.

 

생성자 위임을 사용하지 않은 기존 코드

Player::Player(std::string nameVal, int healthVal, int xpVal)
	: name{nameVal}, health{healthVal}, xp{xpVal}
    {
    }
Player::Player()
	: name{"None"}, health{0}, xp{0}
    {
    }
 Player::Player(std::string nameVal)
 	: name{nameVal}, health{0}, xp{0}
    {
    }

 

생성자 위임을 사용한 코드

Player::Player(std::string name_val, int health_val, int xp_val)
	: name{name_val}, health{health_val}, xp{xp_val}
    {
    }
Player::Player()
	: Player{"None", 0, 0}
    {
    }
Player::Player(std::string name_val)
	: Player{name_val, 0, 0}
    {
    }

 

 

 

4 - 4) 생성자 기본 매개변수

생성자 또한 함수이므로, 기본 매개변수를 사용 가능하다.

class Player
{
public :
	Player(std::string nameVal="None", int healthVal = 0, int xpVal = 0);
private :
	std::string name;
    int health;
    int xp;
};

Player::Player(std::string nameVal, int healthVal, int xpVal)
	: name{nameVal}, health{healthVal}, xp{xpVal}
    {
    }

 

Player empty;
Player kim{"Kim"};	// Kim, 0, 0
Player hero{"Hero", 100};	// Hero, 100, 0
Player enemy{"Enemy", 1000, 0}	// Enemy, 1000, 0

 

 

 

#5 복사 생성자

5 - 1) 복사 생성자 개요

객체가 복사될 때 자동으로 호출되는 함수이다.

객체가 복사되는 경우는 첫 번째로 객체를 pass by value(함수 호출 시에 매개변수의 값을 복사하여 전달한다) 방식으로 함수의 매개변수로 전달할 때이다. 두 번째로 함수에서 value의 형태로 결과를 반환할 때이다. 세 번째로 기존 객체를 기반으로 새로운 객체를 생성할 때이다.

 

*pass by reference : 함수에게 매개변수를 복사하여 넘기는 것이 아니라, 매개변수의 레퍼런스를 넘겨준다. 따라서 함수 내부에서 매개변수의 값을 수정할 수 있다.

 

이와 같은 상황이 발생할 때, 객체가 '어떻게' 복사될 지 정의해 주어야 하는데, 왜? 내가 만든 클래스이기 때문에 잘못 복사했다가는 어떤일이 발생할지 모른다. 하지만 일단 복사가 아예 안되지는 않도록 컴파일러에서 자동으로 복사 생성자를 만들어 준다.

 

그렇기에 포인터가 존재하는 경우 복사 과정에서 문자가 발생할 수 있다. 그리고 클래스는 많은 데이터를 포함할 수 있어서 복사 비용에 대한 고려도 해야 하기에 중요하다. 

 

1. pass by value로 객체 전달 예시

void DisplayPlayer(Player p)
{
	p.Print();	// p는 main()의 hero 지역 객체의 사본
}	// p 소멸자 호출 시점

int main()
{
	Player hero {0, 0, 1};
    DisplayPlayer(hero);
}

 

2. value의 형태로 결과값 반환

Player CreateSuperPlayer()
{
	Player superPlayer {1, 1, 1};
    return super Player;	// superPlayer 객체가 복사되어 main()의 player 지역 객체로 전달
}	// superPlayer 소멸자 호출 시점

int main()
{
	Player player;
    player = CreateSuperPlayer();
}

 

3. 기존 객체를 기반으로 새로운 객체를 생성

Player hero {1, 1, 1};
Player anotherHero = hero;	// hero를 복사해서 anotherHero를 만듬
	// 또는 Player anotherHero {hero};

 

이와 같이 복사가 일어날 때, 정확히 어떻게 복사본을 만들어야 하는지를 클래스를 정의한 누군가가 알려주어야 할 필요가 있다.

 

예제)

#include <iostream>

class MyClass {
private:
    int x;

public:
    // 기본 생성자
    MyClass() {
        std::cout << "기본 생성자 호출" << std::endl;
        x = 0;
    }

    // 복사 생성자
    MyClass(const MyClass& other) {
        std::cout << "복사 생성자 호출" << std::endl;
        x = other.x;
    }

    // x 값을 설정하는 멤버 함수
    void setX(int val) {
        x = val;
    }

    // x 값을 출력하는 멤버 함수
    void printX() {
        std::cout << "x의 값: " << x << std::endl;
    }
};

int main() {
    // 객체 생성
    MyClass obj1;

    // obj1의 멤버 변수 설정
    obj1.setX(5);

    // obj1의 값 출력
    std::cout << "obj1 값:" << std::endl;
    obj1.printX();

    // 복사 생성자 호출하여 obj1을 복사하여 obj2 생성
    MyClass obj2 = obj1;

    // obj2의 값 출력
    std::cout << "obj2 값:" << std::endl;
    obj2.printX();

    return 0;
}

 

결과1-복사생성자 정의 o)

 

결과2-복사생성자 정의 x)

 

 

5 - 2) 기본 복사 생성자

자동 생성되는 복사 생성자의 특징으로 사용자가 복사 생성자를 구현하지 않으면, 기본 복사 생성자가 자동으로 만들어져 사용된다. 멤버 변수들의 값을 복사하여 대입하는 방식이다.

 

포인터 타입의 멤버 변수가 존재할 때는 주의야 한다. 기본 복사 생성자는 포인터 타입의 변수 또한 복사하여 대입된다. 즉, 포인터가 가리키는 데이터의 복사가 아닌 포인터 주소값의 복사이다.

 

동일한 타입의 const 참조자가 인자인 생성자

Type::Type(const Type &source);

Player::Player(const Player &source);	// Player 클래스의 복사 생성자
Account::Account(const Account &source);	// Account 클래스의 복사 생성자

 

 

5 - 3) 얕은 복사 vs 깊은 복사

Shallow Copy(얕은 복사)

자동 생성되는 복사 생성자는 "얕은 복사" 수행

값을 복사하는 것이 아닌, 값을 가리키는 포인터(주소값)를 복사하는 것이다.

동적으로 할당된 멤버 변수는 메모리를 할당하지 않고 대입한 객체의 멤버 변수를 포인터로 참조한다.

 

생성자 초기화 리스트를 사용한 방법

Player(const Player &other)
	:x{other.x}, y{other.y}, speed{other.speed}
    {
    }

 

대입을 사용한 방법

Player(const Player &other)
{
	x = other.x;
    y = other.y;
    speed = other.speed;
}

 

얕은 복사의 문제점

만약 복사할 때 포인터 변수가 존재한다면 포인터가 가리키는 데이터가 아닌 포인터 주소의 값을 복사하는데 그렇게 되면 메모리 주소가 기존변수와 같이 메모리 공간을 공유한다. 여기서 문제는 소멸자가 실행될 때 발생한다.

 

생성자늬 실행 순서와 반대로 소멸자가 실행되므로 복사된 변수의 주소에 할당된 메모리가 먼저 할당됩니다. 다음으로 기존의 주소에 메모리를 해제하는데 이 때 이미 해제된 상태이므로 메모리 접근 에러가 발생한다.

 

class Person
{
	int age;
    char *name;
    
public : 
	Person(int age, char *name) {	// 생성자
    	this->age = age;
        this->name = new char [10];	// 동적 메모리 할당
        strcpy(this->name, name);
        }
        ~Person() {	// 소멸자
        	delete[] name;	// 동적 할당된 메모리 해제
            }
};

int main()
{
	Person p1(20, "Mike");
    Person p2(p1);
}

 

얕은 복사,

이미지 출처 :&nbsp;https://brightwon.tistory.com/9

 

소멸자 호출,

이미지 출처 :&nbsp;https://brightwon.tistory.com/9

 

Deep Copy(깊은 복사)

주소값을 복사하는 것이 아니라. 데이터를 복사하여 복사 생성하는 방식

즉, 복사 생성자가 새로운 힙 공간을 할당한 뒤 동일한 데이터를 복사한다.

얕은 복사의 문제점을 해결하고 동적으로 메모리를 할당해야 하는 포인터 변수까지 고려해 복사한다.

 

Person(Person& p)
{	// 깊은 복사 생성자 명시적 작성
	this->age = age;
    this->name = new char [10];	// 새로운 메모리를 동적으로 할당하고
    strcpy(this->name, p.name);	// p.name의 값을 복사
}

 

데이터도 복사하므로 이중 해제의 문제를 해결한다. 즉 새로운 힙 공간을 할당한다.

 

이미지 출처 :&nbsp;https://brightwon.tistory.com/9

 

복사 생성자를 잘 사용하는 방법으로는 포인터 타입의 멤버 변수가 존재할 때는 깊은 복사 생성자를 직접 구현한다. 새로운 힙 공간을 할당하여 값을 복사해 두어야 한다는 것을 명심하고 사용해야 한다.

 

 

 

#6 this, static, const, freind

6 - 1) this

멤버 함수를 호출한 객체의 주소값

this 포인터는 멤버 함수가 호출된 객체의 주소값 가리키는 숨겨진 포인터이다.

 

아래와 같이 변수에 대한 정의가 명확하지 않습니다.

void Player::SetPosition(int x, int y)
{
	x = x;
    y = y;
}

 

위와 같은 정의이지만 좌변은 멤버 변수, 우변은 인자로 명확하게 구분이 됩니다.

void Player::SetPosition(int x, int y)
{
	this->x = x;
    this->y = y;
}

 

멤버 함수를 호출하는 코드에서는 객체의 주소를 첫 번째 인자로 전달하고, 이에 맞춰 멤버 함수에서는 this 포인터 주소를 전달받아서 멤버 변수에 접근할 때 사용합니다. 이렇게 하면 멤버 함수가 스택에서 다른 위치에 할당되더라고 해당 객체의 멤버 변수에 접근할 수 있습니다.

 

클래스에서 생성된 인스턴스는 독립된 메모리에 저장된 자신만의 멤버 변수를 갖지만, 멤버 함수는 모든 인스턴스가 공유한다.

this는 객체 자신을 가리키는 포인터이며, this를 통해 어떤 객체를 비교하는 것인지 명시할 수 있습니다.

 

 

6 - 2) const

const 객체의 멤버 변수의 값은 변경이 불가능하다. 즉, 그 대상을 변경하지 않는 "상수"를 의미한다.

 

첫 번째, const 위치가 맨 앞에 있으면서 포인터 변수가 가리키는 값에 대하여 상수화를 시키는 경우

int num = 1;
const int* ptr = &num;	// *ptr을 상수화

*ptr = 2;	// Compile Error
num = 2;	// Pass

*ptr = 2;는 ptr이 const 변수이기 때문에 컴파일 에러가 발생하지만, num = 2;는 num이 const 변수가 아니기 때문에 정상 동작한다. 즉, 포인터 변수가 가리키는 num 자체가 상수화가 되는 것이 아니다.

 

두 번째, const 위치가 type과 변수 이름 사이에 있으면서 포인터 변수 자체를 상수화 시키는 경우

int num1 = 1;
int num2 = 2;
int* const ptr = &num1;	// ptr을 상수화

ptr = &num2;	// Compile Error

위의 ptr은 자기 자신을 상수화 시키는 것이기 때문에 num2의 주소값으로 변경하려고 하면 컴파일 에러가 발생한다.

 

const 멤버 함수는 이름 그대로 class의 멤버 함수만 const로 상수화를 시킬 수 있고 멤버 함수가 아닌 함수는 const 함수로 선언이 불가능하다. 그래서 const로 선언된 객체에서 값이 바뀌지 않는 것이 보장된 함수를 선별하여 호출이 가능하다. 값이 바뀌지 않는 것을 보장하는 법은 멤버 함수 선언 뒤에 const를 붙인다.

 

int GetString(void) const;	// Compile Error

class Foo
{
	int num = 1;
    
    int GetNum(void)  const
    {
    int a = 1;
    a++;	// 지연 변수는 가능
    
    num++;	// Compile Error
    return num;
    }
};

 

해당 멤버 함수 내에서는 모든 멤버 변수를 상수화 시킨다는 의미이며, 따라서 위와 같이 멤버 변수인 num은 경변할 수 없고 지연 변수인 a는 변경 할 수 있다.

 

 

6 - 3) static

static 클래스 멤버 변수

객체가 아닌 클래스에 속하는 변수, 개별적인 객체의 데이터가 아닌 클래스에 공통 데이터 구현이 필요할 때 사용한다.

멤버 변수가 정적(static)으로 선언되면, 해당 클래스의 모든 객체에 대해 하나의 데이터만이 유지 관리된다.

 

정적 멤버 변수는 클래스 영역에서 선언되지만, 정의는 파일 영역에서 수행된다.

함수 내에 선언된 static 멤버 변수는 한번만 초기화되고, 지역변수와 달리 함수를 빠져나가도 소멸되지 않는다. 

 

static 클래스 멤버 함수

객체가 아닌 클래스에 속하는 함수, 클래스 이름 하에서 바로 호출이 가능하다.

static 클래스 멤버 함수는 static 클래스 멤버 변수에서만 접근이 가능하다.

 

#include <iostream>

using namespace std;

void func() {
  int a = 10;
  static int b = 10;

  a++;
  b++;

  cout << "a : " << a << " , b : " << b << endl;
}

int main() {
  func();
  func();
  func();
  func();
  func();

  return 0;
}

 

 

a는 지역 변수이므로 func 함수가 호출될 때 매번 새로 생성되고, func 함수가 종료되면 사라진다. 반면에 b는 static 키워드 때문에 정적 변수로 선언되어 func 함수가 호출된 만큼 값이 누적된다.

 

 

 

6 - 4) friend

private, protected 멤버에 대해 접근을 허용할 특정 함수나 클래스를 선언할 때 사용

 

비대칭 : A가 B의 friend라고 B가 A의 friend는 아니다

전이되지 않는다 : A가 B의 friend이고 B가 C의 friend라고, A가 C의 friend는 아니다

 

예제1)

class gs_engine : public ic_engine
{
public :
    gs_engine();
    ~gs_engine();
private : 
	void acceleration_output();
    friend class automobile;	// 프렌드 클래스 선언
};

 

예제2)

class Player
{
	friend void DisplayPlayer(const Player& p);
public : 
	Player(int x, int y, int speed)
    	: x {x}, y {y}, speed {speed}
        {
        	cout << this << endl;
        }
        void SetPosition(int x, int y)
        {
        	this->x = x;
            this->y=  y;
        }
private : 
	int x, y;
    int speed;
};

void DisplayPlayer(const Player& p)	//주의 : Player::DisplayPlayer()가 아님
{
	cout << p.x << "," << p.y << endl;
}

 

 

 

※본 블로그는 학습을 하며 제가 이해한 내용을 바탕으로 작성되어 실제 정의와 다를 수 있습니다.

 

참고문헌

GitHub - diskhkme/cpp_lecture_material: C++ 프로그래밍 강의 자료

https://www.aladin.co.kr/shop/wproduct.aspx?ISBN=K842939734&start=pnaver_02