Git 사용법

  • add
    - 변경된 파일의 내용을 index(Stage)로 보냄

  • commit
    - index의 내용을 HEAD(local repository)로 보냄

  • push
    - local repository -> remote repository 로 보냄

  • pull
    - remote repository -> local repository 로 보냄

push, pull 이해를 위한 사용 예시

ex)

집 <-> 회사 동시 작업하는 경우

 

1. 집에서 작업한 후 git push로 원격 저장소로 보냄

 

2. 회사에서 작업할 때 작업하기 전 git pull로 원격저장소 내용을 가져와서 동기화한 후 
    작업을 시작한다. 작업을 완료한 후 git push로 원격저장소로 새로운 버전을 보냄

3. 집에 와서 작업하기 전 git pull로 원격저장소 내용을 가져와서 동기화한 후 작업 시작
    회사에서와 마찬가지로 작업을 완료한 후 git push로 원격저장소에 새로운 버전을 보냄

 

git branch 사용 방법

명령어

  • git branch
    - 현재 branch를 확인

  • git branch [ 브랜치명 ]
    - 새로운 branch [브랜치명]을 생성

  • git checkout [ 브랜치명 ]
    - 현재 속한 branch를 브랜치명 branch로 바꿈

  • git log --branches --graph --decorate --oneline
    - 현재 브랜치들의 흐름을 그래프로 간단히 보여줌

  • git branch -d [ 삭제할 브랜치명 ]
    - 브랜치 삭제

branch 사용 예시

예를 들어 현재 브랜치가 main, exp 두 개가 있다. 
두 개의 브랜치에서 한 일을 병합하고 싶을 때 어떻게 해야 할까?
만약 main에서 병합하고 싶다면 main 브랜치에서
$git merge exp 명령어를 입력한다.
    ->    exp 브랜치의 내용과 main 브랜치에서의 내용이 main 브랜치에서 병합된다.
    ->    < 주의!! > exp 브랜치에는 병합된 내용이 아닌 exp에서 수정한 내용만 들어있다.

 

위의 내용을 직접 수행해보자. ( 서로 다른 브랜치에서 같은 파일을 수정한 후 수정 내용을 병합하는 과정 )

( 기본 브랜치 : main )

$git branch exp       ->        exp branch 생성

$git checkout exp   ->        exp branch로 가서 거기서 소스코드 변경

$git add test.c

$git commit -m "exp에서 변경"

 

$git checkout main       ->        main branch로 가서 거기서 소스코드 변경

$git add test.c

$git commit -m "main에서 변경"

$git merge exp               ->        exp와 main이 main branch에서 병합됨 ( 겹치는 부분을 고친 게 아니라면 자동으로 병합해줌 )

$git push                         ->        병합한 최종본을 원격저장소로 push    

 

 

 

처음 local에서 Git 사용할 때 Git 세팅방법

1. git config -global user.name "git 아이디"

2. git config -global user.email "git 이메일"

3. git config -list  ->   아이디, 이메일이 잘 들어갔는지 정보 확인

 

 

내 로컬에 있는 프로젝트를 Git에 올리는 방법

( 1, 4번은 처음에만, 나중에 사용할 때는 2, 3, 5번 반복 )

  1.  Github에 저장소 작성 또는 복제 ( git init OR git clone )
    git init          :          현재 작업중인 디렉토리를 Git 저장소로 변환한다.

  2.  파일의 생성 / 변경 / 삭제를 index(stage)에 추가
    git add [ 파일명 ] 

  3.  변경 결과를 로컬 저장소에 커밋
    git commit -m "Comment" ( git status 명령어로 잘 올라갔는지 확인 가능 )

  4.  원격 저장소에 push로 반영하기 전 local repository에 remote repository를 연결
    git remote add origin [  https://github.com/username/repositoryName ]
    ( 연결 후 잘 연결됐는지 git remote -v 로 확인 가능 )


  5. 로컬 저장소를 push해서 원격 저장소에 반영 ( git push )
    git push        origin          [브랜치명]
                      [원격저장소]

 

 

자주 사용하는 Git 명령어

  • git status
    - 저장소의 상태를 확인하는 명령어 ( 현재 branch의 이름, 추가/변경된 파일 및 디렉토리 목록 표시 )

  • git add
    - 파일 or 디렉토리를 index(stage)에 추가하는데 사용하는 명령어

  • git checkout
    - 로컬 저장소의 현재 속한 branch를 바꿔줌

  • git clone
    - 기존 원격 저장소를 로컬에 다운로드하기 위해 사용
       ex) github에 공개된 저장소를 자신의 컴퓨터에 다운로드할 때 사용

  • git remote
    - 원격 저장소를 조작하는데 사용
      git remote -v                                                 :        원격 저장소에 대한 자세한 목록 표시
      git remote add origin [원격저장소 URL]       :        원격 저장소를 추가
      git remote remove [ name ]                         :        원격 저장소 제거

  • git reset
    - 로컬 저장소의 커밋을 취소하기 위해 사용 ( 잘못 커밋했거나 수정내용 누락이 있을 때 자주 사용 )

  • git pull
    - 원격 브랜치의 저장소를 가져옴
       ex) 로컬 저장소 master 브랜치에 원격 저장소 origin의 master 브랜치 가져오는 경우
              -> git checkout master git pull origin master

 

Git 사용시 자주 발생하는 에러 상황

1.  원격저장소에 local에 없는 새로운 파일이 생겼을 때 local에서 그걸 알지 못하고 그냥 push 하는 경우

해결 방법
- pull로 원격 저장소의 데이터들을 가져와 내 local 저장소와 원격 저장소를 동기화 시킨 후
   push로 수정사항을 원격 저장소에 보내면 해결된다.

 

 

 

파일읽기 ( ifstream )

  • 헤더 : <fstream>
  • input file stream
  • 파일 내용 읽어옴

사용함수

1. open()

- 파일 열 때 사용

 

void open( const char* fileName, ios_base::openmode mode = ios_base::in );

void open( const string& fileName, ios_base::openmode mode = ios_base::in );

첫번째 인자  :  open할 파일명
두번째 인자  :  오픈할 때 모드 설정

         ios::in         :     읽기 위한 파일 열기

         ios::out      :    쓰기 위한 파일 열기

         ios::ate      :    파일의 끝에 위치

         ios::app     :    모든 출력은 파일의 끝에 추가된다.

         ios::trunc   :    만약 파일이 존재하면 지운다.

         ios::binary  :    이진 모드 

 

<두번째 인자 디폴트값>
파일을 열 때 첫번째 인자만 주면 두번째 인자는 클래스별로 디폴트값이 정해진다.

  • ofstream               :                 ios::out | ios::trunc
  • ifstream                :                 ios::in
  • fstream                 :                 ios::in | ios::out

 

사용 예시)

만약 데이터 추가 모드인 이진 모드로 "example.bin"파일을 열길 원한다면?

➡︎  ofstream file;

     file.open("example.bin", ios::out | ios::app | ios::binary);

 

 

2. is_open()

- 열렸는지 확인

 

bool is_open() const;

 

3. close()

- 파일과의 연결을 닫는 함수

 

void close();

 

4. get()

- 읽은 파일에서 문자 단위(char)로 읽어오는 함수

 

ifstream& get(char& c);

 

사용법

char c;
while(readFile.get(c))
{
  cout << c;  // 읽은 문자가 c에 들어있다.
}

 

5. getline()

- 한 줄씩 읽어오는 함수 ( 한 줄씩 문자열을 읽어 str에 저장 )

- 한 줄의 기준은 '\n'이 올 때까지  or 파일의 끝을 알리는 EOF를 만날 때까지

 

ifstream& getline(char* str, streamsize len);

 

<주의사항>

문자열을 받아오는 형태가 char* 타입이기 때문에 string타입으로 바로 받을 수 없다.
str.c_str()로 string을 문자열로 바꿔도 불가능하다.
-> c_str()의 반환값은 const char*이기 때문에 첫번째 매개변수의 타입인 char*와 맞지 않다.

 

6. eof()

- 파일을 읽을 때 커서가 움직이게 되는데 그 커서가 getline(), get()함수를 돌게 되면 쭉쭉 뒤로 가게 된다.

- 파일의 끝이 나오면 true, 아니면 false 반환

 

 bool eof() const;

 

7. read()

- stream에서 문자를 읽어옴

- count개의 문자를 읽어들이거나 EOF가 발생할 때까지 읽어들임

- 읽어들인 문자수는 gcount()를 사용해 알 수 있다

 

basic_istream& read(char_type* s, std::streamsize count);

    - 첫번째 인자  :  읽어들인 문자들을 저장할 문자배열

    - 두번째 인자  :  문자 몇개를 읽을지 설정

 

 

파일읽기 간단 예제

std::ifstream readFile;
readFile.open("test.txt");

if(readFile.is_open())
{
  while(!readFile.eof())
  {
    char arr[256];
    readFile.getline(arr, 256);
  }
}
readFile.close();

 

파일읽기 방법 총정리

1. 한 문자씩 읽기

#include<fstream>
#include<iostream>
using namespace std;

int main()
{
  ifstream file("file.txt");
  
  while(!file.eof())
    cout << file.get();
  
}

 

2. 한 줄씩 읽기

#include<fstream>
#include<iostream>
#include<string>
using namespace std;

int main()
{
  string str;
  ifstream file("file.txt");
  
  while(getline(file, str))
    cout << str << '\n';
}

 

3. 버퍼 블록 단위 읽기

#include<fstream>
#include<iostream>
using namespace std;

int main()
{
  char buf[5];
  ifstream file("file.txt");
  
  while(file)
  {
    file.read(buf, 4);
    buf[file.gcount()] = '\0'; // gcount() = 읽은 문자 수
    cout << buf;
  }
}

 

4.  한번에 파일 전체 읽기 ( stringstream 사용 )

#include<fstream>
#include<sstream>
using namespace std;

int main()
{
  ifstream file("file.txt");
  stringstream ss;
  
  ss << file.rdbuf(); // file과 연관된 스트림버퍼 전체를 리턴한다.
  cout << ss.str();
}

 

 

 

 파일에 쓰기 ( ofstream )

  • 헤더 : <fstream>
  • output file steram

 

사용함수

1. write()

- 파일에 쓰는 함수

- 첫번째 매개변수로 받은 str을 n길이만큼 파일에 write한다.

 

ostream& write(const char* str, streamsize n);

 

 

파일에 쓰기 간단 예제

std::ofstream writeFile;
writeFile.open("test.txt");

char arr[11] = "kkddyy";

if(writeFile.is_open())
{
  writeFile.write(arr, 10);  // char배열로 된 문자열은 끝에 '\0'이 들어가 있기 때문에 
                             // 배열의 "총길이 - 1"만큼만 write해야 한다.
                             // c++ string타입의 문자열로 사용한다면 신경쓰지 않아도 됨!!
}
writeFile.close();

 

싱글턴 패턴, Singleton Pattern

오직 한 개의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역적인 접근점을 제공한다.

싱글턴 패턴은 의도와는 달리 득보다는 실이 많다. 
싱글턴 패턴을 남용하지 말라고 하지만, 개발자들 중에는 귀담아 듣는 사람이 많지 않다.
워낙 남용되는 패턴이다 보니 싱글턴을 피할 방법을 주로 다루겠지만, 그래도 우선은 싱글턴 패턴에 대해 살펴보자.


싱글턴 패턴

1. 오직 한 개의 인스턴스(객체)만을 갖도록 보장

인스턴스가 여러 개면 제대로 작동하지 않는 상황이 종종 있다. 외부 시스템과 상호작용하면서 전역 상태를
관리하는 클래스 같은 게 그렇다. ex) 파일 시스템 API

 

파일 시스템 클래스로 들어온 호출이 이전 작업 전체에 대해 접근할 수 있어야 한다. 아무데서나 파일 시스템 클래스 인스턴스를
만들 수 있다면 다른 인스턴스에서 어떤 작업을 진행 중인지를 알 수 없다. 이를 싱글턴으로 만들면 클래스가 인스턴스를
하나만 가지도록 컴파일 단계에서 강제할 수 있다.

 

2. 전역적인 접근점을 제공

로깅, 콘텐츠 로딩, 게임 저장 등 여러 내부 시스템에서 파일 시스템 참조 클래스를 사용할 것이다. 
이들 시스템에서 파일 시스템 클래스 인스턴스를 따로 생성할 수 없으므로 싱글턴 패턴은 하나의 인스턴스만 
생성하는 것 뿐만 아니라 그 인스턴스를 전역에서 접근할 수 있는 메서드를 제공한다.

 

class FileSystem{
private:
  FileSystem(){}
  static FileSystem* instance_;

public:
  static FileSystem& instance(){
    // 게으른 초기화
    if(instance_ NULL){
      instance_ = new FileSystem();
    }
    
    return *instance_;
  }
};

instance_ 정적 멤버 변수는 클래스 인스턴스를 저장한다. 
생성자가 private이기 때문에 밖에서는 생성할 수 없다.
instance() 정적 메서드는 코드 어디에서나 싱글턴 인스턴스에 접근할 수 있게 하고,
싱글턴을 실제로 필요로 할 때까지 인스턴스 초기화를 미루는 역할(게으른 초기화)도 한다.

 

 

요즘은 아래처럼도 만든다.

class FileSystem{
public:
  static FileSystem& instance(){
    static FileSystem* instance = new FileSystem();
    return *instance;
  }
  
private:
  FileSystem(){}
};

c++ 11에서는 정적 지역 변수 초기화 코드가 멀티스레드 환경에서도 딱 한번 실행되어야 한다. 
즉, 최신 c++ 컴파일로 컴파일하면 이 코드는 이전 예제와는 달리 스레드에서 안전하다.


싱글턴을 사용하는 이유는?

1. 한 번도 사용하지 않는다면 아예 인스턴스를 생성하지 않는다.

 

2. 런타임에 초기화된다.

  보통 싱글턴 대안으로 정적 멤버 변수를 많이 사용한다. 정적 멤버 변수는 자동 초기화되는 문제가 있다.
  즉, 컴파일러는 main함수를 호출하기 전에 정적 변수를 초기화하기 때문에 프로그램이 실행된 다음에야
  알 수 있는 정보들은 활용할 수 없다.
  또한 정적 변수 초기화 순서도 보장되지 않기 대문에 한 정적 변수가 다른 정적 변수에 안전하게 의존할 수도 없다.

 

3. 싱글턴을 상속할 수 있다.

파일 시스템 래퍼가 크로스 플랫폼을 지원해야 한다면 추상 인터페이스를 만든 뒤, 플랫폼마다 구체 클래스를 만들면 된다.

 

1. 먼저 아래와 같이 상위 클래스를 만든다.

class FileSystem{
public:
  virtual ~FileSystem(){}
  virtual char* readFile(char* path) = 0;
  virtual void writeFile(char* path, char* contents) = 0;
};

 

2. 이제 플랫폼 별로 하위 클래스를 정의한다.

class PS3FileSystem : public FileSystem{
public:
  virtual char* readFile(char* path){
    // SONY의 파일 IO API를 사용한다
  }
  virtual void writeFile(char* path, char* contents){
    // 소니의 파일 IO API를 사용한다
  }
};

class WIIFileSystem : public FileSystem{
public:
  virtual char* readFile(char* path){
    // 닌텐도의 파일 IO API를 사용한다
  }
  virtual void writeFile(char* path, char* contents){
    // 닌텐도의 파일 IO API를 사용한다
  }
};

 

3. 이제 FileSystem 클래스를 싱글턴으로 만든다.

class FileSystem{
public:
  static FileSystem& instance();
  
  virtual ~FileSystem(){}
  virtual char* readFile(char* path) = 0;
  virtual void writeFile(char* path, char* contents) = 0;

protected:
  FileSystem(){}
};

 

4. 핵심!!
-> 인스턴스를 생성하는 부분!!

FileSystem& FileSystem::instance(){
#if PLATFORM == PLAYSTATION3
  static FileSystem* instance = new PS3FileSystem();
#elif PLATFORM == WII
  static FileSystem* instance = new WiiFileSystem();
#endif
  return *instance;
}

FileSystem::instance() 를 통해서 파일 시스템에 접근하기 때문에, 플랫폼 전용 코드는 FileSystem 클래스 내부에
숨겨놓을 수 있다.

 


싱글턴이 왜 문자인가?

1. 알고 보니 전역 변수이다.

  전역 변수는 코드를 이해하기 어렵게 한다. 예를 들어 함수에 SomeClass::getSomeGlobalData() 같은 코드가 있다면
  전체 코드에서 SomeGlobalData에 접근하는 곳을 다 살펴봐야 상황을 파악할 수 있다.
  남이 만든 함수에서 버그를 찾아야 할 때, 함수가 전역 상태를 건드리지 않는다면
  함수 코드와 매개변수만 보면 된다.

 

2. 전역 변수는 커플링을 조장한다.

  신입에게 '돌멩이가 땅에 떨어질 때 소리가 나게 하는' 작업을 첫 일감으로 줬다고 하자.
  기존 작업자들은 물리 코드와 사운드 코드 사이에 커플링이 생기는 걸 피하겠지만
  안타깝게도 경험이 부족한 신입은 AudioPlayer 인스턴스를 전역적으로 접근할 수 있다 보니,
  #include한 줄만 추가해도 신중하게 만들어놓은 아키텍처를 더럽힐 수 있다. 
  인스턴스에 대한 접근을 통제함으로써 커플링을 통제할 수 있다.

 

3. 전역 변수는 멀티스레딩 같은 동시성 프로그래밍에 알맞지 않다.

  다른 스레드가 전역 데이터에 무슨 작업을 하는지 모를 때 교착상태, 경쟁상태 등 정말 찾기 어려운 스레드
  동기화 버그가 생기기 쉽다.

 

4. 게으른 초기화는 제어할 수가 없다.

  게으른 초기화는 괜찮은 기법이다. 그러나 게임은 다르다.
  시스템을 초기화할 때 메모리 할당, 리소스 로딩 등 할 일이 많다 보니 시간이 꽤 걸릴 수 있다.
  오디오 시스템 초기화에 몇백 밀리세컨드 이상 걸린다면 초기화 시점을 제어해야 한다.
  처음 소리를 재생할 때 게으른 초기화를 하게 만들면 전투 도중에 초기화가 시작되는 바람에
  화면 프레임이 떨어지고 버벅댈 수 있다.

 


대안

클래스가 꼭 필요한가?

게임 코드의 싱글턴 클래스 중에는 애매하게 다른 객체 관리용도로만 존재하는 관리자가 많다.
Monster, MonsterManager, Sound, SoundManager 등 모든 클래스에 관리자 클래스가 있는게 아닐까
싶을 정도이다. 

 

아래의 두 클래스를 보자.

class Bullet{
public:
  int getX() const { return x_; }
  int getY() const { return y_; }
  void setX(int x) { x_ = x; }
  void setY(int y) { y_ = y; }

private:
  int x_;
  int y_;
};

class BulletManager{
public:
  Bullet* create(Bullet& bullet){
    Bullet* bullet = new Bullet();
    bullet->setX(x);
    bullet->setY(y);
    
    return bullet;
  }
  
  bool isOnScreen(Bullet& bullet){
    return bullet.getX() >= 0 &&
           bullet.getY() >= 0 &&
           bullet.getX() < SCREEN_WIDTH &&
           bullet.getY() < SCREEN_HEIGHT;
  }
  
  void move(Bullet& bullet){
    bullet.setX(bullet.getX() + 5);
  }
};

언뜻 보면 BulletManager를 싱글턴으로 만들어야겠다는 생각이 들 수 있다.
Bullet을 쓰려면 BulletManager도 필요할 테니 말이다. 그렇다면 관리자 클래스 인스턴스는 몇 개 필요할까?
정답은 0개다. 아래와 같이 만들면 관리자 클래스에 대한 '싱글턴' 문제를 해결할 수 있다.

 

class Bullet{
private:
  Bullet(int x, int y) : x_(x), y_(y) {}
  bool isOnScreen(){
    return x_ >= 0 && x_ < SCREEN_WIDTH &&
           y_ >= 0 && y_ < SCREEN_HEIGHT;
  }
  
  void move() { x_ += 5; }
private:
  int x_;
  int y_;
};

관리자 클래스를 없애고 나니 문제도 없어졌다. 서툴게 만든 싱글턴은 다른 클래스에 기능을 더해주는
도우미인 경우가 많다. 가능하다면 도우미 클래스에 있던 작동 코드를 모두 원래 클래스로 옮기자.

public 상속

#include<iostream>
using namespace std;
class Asset{
   int money;
public:
   Asset() { money = 0; }
   void inc_money(int m) { money += m; }
   void show_asset() { 
      cout << "money : " << money << endl;
   }
};

class Richman : public Asset
{
   int gold;
public:
   Richman() { gold = 0; }
   void inc_gold(int g) { gold += g; }
   void gold_to_money() { 
       inc_money(gold);
       gold = 0;
   }
};

int main()
{
  Richman R1;

  R1.inc_money(100);
  R1.show_asset();

  R1.inc_gold(200);
  R1.gold_to_money();

  R1.show_asset();
  return 0;
}

결과는

money : 100

money : 300

 

public Asset을 상속받은 Richman 클래스는 부모인 Asset의 public 멤버에는 바로 접근할 수 있다. 하지만
Asset클래스의 private 멤버변수로 지정된 money에 접근하려면 Asset의 public 멤버를 활용해서 접근해야 한다.


➡️  public으로 상속받았을 때는 부모의 public에는 R1.inc_money()처럼 직접 접근 가능

      하지만 부모의 private은 직접 접근은 안되고 부모의 public 멤버함수를 활용해 접근해야 한다.

 

 

private 상속

#include<iostream>
using namespace std;
class Asset{
   int money;
public:
   Asset() { money = 0; }
   void inc_money(int m) { money += m; }
   void show_asset() { 
      cout << "money : " << money << endl;
   }
};

class Richman : private Asset
{
   int gold;
public:
   Richman() { gold = 0; }
   void inc_gold(int g) { gold += g; }
   void gold_to_money() { 
       inc_money(gold);
       gold = 0;
   }
   void show() {
     show_asset();
   }
};

int main()
{
  Richman R1;

  //R1.inc_money(100); 직접 접근 불가능!!
  //R1.show_asset(); 직접 접근 불가능!!

  R1.inc_gold(200);
  R1.gold_to_money(); // 내부에서 부모의 public멤버를 호출하여 접근

  R1.show();
  return 0;
}

결과는

money : 200

 

private은 은닉성 때문에  private으로 상속받았을 때는 부모의 public 멤버, private 멤버 모두 직접 접근할 수 없다.

➡️   private으로 상속 받았을 때는 부모의 public 멤버여도 R1.inc_money()처럼 직접 접근 불가능

      부모에 접근하려면 직접 접근이 아닌 R1.gold_to_money()처럼 부모의 public 멤버함수를 내부에서
      호출하는 형태로
접근해야 함

 

 

protected 멤버 상속

private과 public 상속은 위에서와 같이 좀 불편한 경우들이 생긴다.
하지만 protected 상속을 이용하면 외부에서는 접근 불가능하지만 상속받은 자식에서는 부모에 접근이 가능하게 된다.

#include<iostream>
using namespace std;
class Asset{
protected:   // protected 멤버
   int money;
public:
   Asset() { money = 0; }
   void show_asset() { 
      cout << "money : " << money << endl;
   }
   void inc_money(int m) { money += m; }
};

class Richman : public Asset
{
   int gold;
public:
   Richman() { gold = 0; }
   void inc_gold(int g) { gold += g; }
   void gold_to_money() { 
       money += gold;     // money가 protected 멤버라 자식에서 이렇게 직접 접근 가능
       gold = 0;
   }
   void show() {
     cout << "money : " << money << endl;
   }
};

int main()
{
  Richman R1;

  R1.inc_gold(200);
  R1.gold_to_money();

  R1.show();
  return 0;
}

결과

money : 200

 

부모를 상속받은 자식 클래스에서 money에 직접 접근이 가능하여 소스코드가 훨씬 간결해졌다.

 

 

상속 관계에서 생성자, 소멸자 호출 순서

#include<iostream>
using namespace std;
class parentClass {
public:
  parentClass() { 
     cout << "parentClass 생성자 " << endl;
  }
  ~parentClass() {
     cout << "parentClass 소멸자 " << endl;
  }
};
class childClass : public parentClass {
public:
  childClass() {
     cout << "childClass 생성자 " << endl;
  }
  ~childClass() {
     cout << "childClass 소멸자 " << endl;
  }
};

int main()
{
  childClass C;

  return 0;
}

결과

parentClass 생성자

childClass 생성자

childClass 소멸자

parentClass 소멸자

 

부모가 먼저 생성되고, 이후 상속받은 자식이 생성되고
자식이 제거된 후, 부모가 제거된다.

접근 제어자

객체지향 프로그래밍의 특징 중 하나인 정보 은닉을 위한 키워드

 

 

 

클래스와 구조체에서의 접근 제어자

c++ 클래스의 기본 접근 제어 권한                 :  private

c++ 구조체, 응용체의 기본 접근 제어 권한 :  public

 

<Tip>

c++ 클래스에서 private 접근 제어자는 생략 가능하다. 단, 나머지 접근 제어자는 생략 불가능

class Book {
private:                //   생략 가능함.
    int current_page_; 
    void set_percent(); 
public:
    string title_;
    int total_page_;
    double percent_;
    void Move(int page);
    void Open();
    void Read();
};

 

클래스 선언 방법

class Book
{
private:
  int current_page;         // 멤버 변수
public:
  void set_percent();       // 멤버 함수
  int total_page;
  . . .
};

Book my_book;     //    클래스 Book 객체 my_book 선언

 

 

외부클래스 정의 방법

클래스 선언 밖에서 멤버함수를 정의할 때 주의사항

  • 외부에서 멤버함수를 정의하려면 클래스 안에 정의할 멤버함수가 이미 있어야 한다. ( 오버라이딩? 같은 느낌 )
  • 외부에서 정의하려면 함수의 몸체, 즉 { } 가 클래스 내에 있으면 안 된다.

 

사용법

class Member{
public:
	Member();
	void print();
};

Member::Member(){
	printf("생성자 외부클래스로 정의~");
}

void Member::print(){
	printf("멤버함수 외부클래스로 정의~");
}

 

 

this 포인터

  • c++에서는 모든 멤버 함수가 자신만의 this포인터를 가지고 있다.
  • this 포인터는 해당 멤버 함수를 호출한 객체를 가리킨다.
  • this는 현재 객체의 주소를 저장한 포인터 변수이므로 반환할 때 참조 연산자(*)를 사용해 객체를 반환한다.
    ( *를 사용하지 않으면 this는 포인터이므로 주소값만 반환한다. )
  • 정적 멤버함수는 this 포인터가 없다.

 

 

생성자

생성자는 3가지 타입이 있다.            ➡️            기본 생성자, 디폴트 생성자, 복사 생성자

 

1. 기본 생성자

#include<iostream>
using namespace std;

class Test
{
private:
  int num1;
  double num2;
public:
  Test(int x, double y){
    num1 = x;
    num2 = y;
  }
  
  void printNum() const {
    cout << num1 << " " << num2 << endl;
  }
};

int main(){
  Test test(10, 10.1);            //          암시적 호출로 객체 생성
  test.printNum();
  
  Test test = Test(10, 10.1);     //          명시적 호출로 객체 생성
  test.printNum();          
  
  Test test;                      //          기본 생성자로 들어감
  test.printNum();
  
  Test(10, 10.1);        // 임시객체 생성 ( 실행도중 잠깐만 사용되는 객체 ) ➡️ 다음줄로 넘어가면 사라진다.
  return 0;
}

 

 

2. 디폴트 생성자

  • 객체를 생성할 때 아무런 인자를 넘겨주지 않을 때 멤버 변수를 초기화하는 생성자
  • 생성자를 만들지 않으면 컴파일러는 자동으로 텅 빈 생성자를 만든다.

예시1)

class Test{
private:
  int num1;
  double num2;
public:
  Test(){}   
  void printNum() const{}
};

int main(){
  Test test;
}

 

예시2)

class Test{
private:
  int num1;
  double num2;
public:
  Test(int a=10, double b = 10.1){
	cout << "디폴트 호출" << endl;
	num1 = a;
	num2 = b;
  }
  
  void printNum() const{}
};

int main(){
  Test test; // a = 10. b = 10.1 
  Test test2(15,15.1) // a = 15, b = 15.1 ➡️ 여기도 디폴트 생성자로 들어가는데 a와 b값이 지정해준 값들로 들어감
}

 

 

3. 복사 생성자

 

c++은 아래와 같이 묵시적 변환을 해준다.

int num1 = 5;              ➡️        int num1(5); 와 동일

int num2 = num1;      ➡️        int num2(num1); 와 동일

 

객체 선언도 마찬가지로 묵시적 변환을 해준다.

A a;

A b = a;                      ➡️        A b(a); 와 동일하다. 즉, 복사이다!!

 

 

복사는 얕은 복사와 깊은 복사가 있다.

 

얕은 복사

  • 객체를 복사할 때 기존 객체의 메모리 주소만 복사
  • 디폴트 복사 생성자이다. 즉, 연산자 오버로딩으로 재정의하지 않아도 이미 존재하는 대입연산자이다.
  • 대충 복사하는 느낌

얕은 복사의 문제점

#include<iostream>
#include<string>
using namespace std;

class A
{
pvivate:
  char* name;
  int age;
public:
  A(const char* myname, int myage) : age(myage)
  {
    name = new char[strlen(myname) + 1];
    strcpy(name, myname);
  }
  
  void ShowInfo() const {
    cout << "이름 : " << name << endl;
    cout << "나이 : " << age << endl;
  }
  
  ~A(){
    delete []name;
    cout << "소멸자 호출" << endl;
  }
};

int main(){
  A a("이름", 20);
  A b = a;   //   shallow copy로 주소값만 복사해온다. ( shallow copy가 디폴트 복사생성자임 )
  a.ShowInfo();
  b.ShowInfo();   //  객체 a의 name을 가리키는 주소값을 b도 할당받아 두 객체가 같은 문자열을 delete해서 에러 발생

  return 0;
}

얕은 복사가 이루어지면서 a와 b가 둘 다 같은 주소를 delete해서 중복 소멸하는 문제가 발생한다.
그리고 b의 원래 주소는 다시는 접근할 방법이 없게 되면서 delete할 수가 없게 되어 메모리 누수가 발생한다.

 

깊은 복사

  • 객체가 가진 멤버의 값과 형식 자체를 복사하여 "객체 자체"가 복사되는 것
  • 복사 생성자를 직접 정의(연산자 오버로딩을 통해 재정의)함으로써 객체간의 같은 메모리 공간 참조를 막아준다.
#include<stdio.h>

class Sample
{
private:
  int num1;
  int num2;
public:
  Sample(const Sample& Sp);
};


// 참조전달 방법으로 객체 자체를 가져와서 복사한다. ( 대입연산자를 직접 재정의한 것 )
Sample::Sample(const Sample& Sp){
  printf("복사 생성자\n");
  num1 = Sp.num1;
  num2 = Sp.num2;
}

int main(){
  Sample a;
  Sample b(a);  
  //  Sample b = a;와 같은 문장이다.
  //  c++에서 묵시적 변환한 것 ( Sample b = a;를 Sample b(a)로 알아서 변환해줌 ) 
  //  묵시적 변환을 막아주고 싶으면 재정의한 부분 앞에 explicit을 써주면 된다.
  //  ➡️ 그러면 Sample b = a;는 에러가 난다.
  //  묵시적 변환을 최소로 만들어주는게 좋은 코드이다. 
  //  묵시적 변환이 많이 발생할수록 결과를 예측하기 어렵다.
}

// 재정의한 복사(대입연산자)로 인해 깊은 복사가 이루어졌다.

'C,C++' 카테고리의 다른 글

C++ 상속  (0) 2022.04.13
C++ 접근 제어자  (0) 2022.04.10
C++ enum ( 열거형 자료형 )  (0) 2022.04.10
C++ 스마트 포인터 ( shared_ptr, make_shared )  (0) 2022.04.10
C++ 범위 지정 연산자(::)와 namespace  (0) 2022.04.10

enum

  • C++에는 많은 자료형이 내장되어 있지만 이 자료형들이 원하는 걸 표현하기에 충분하지 않다.
    ➡️ 그래서 프로그래머들은 사용자 정의 자료형으로 자신만의 자료형을 만든다.

  • 가장 간단한 사용자 정의 자료형이 바로 열거형 자료형이다.
  • 열거형을 정의해도 메모리에는 할당되지 않는다.
    ➡️ 열거된 유형의 변수를 선언한 경우에 해당 변수에 대한 메모리가 할당된다!!

  • 각 열거자는 열거 목록의 위치에 따라 정수 값이 자동으로 할당된다.
  • 기본적으로 첫 번째 열거자에는 정수 값 0이 할당되며 그 이후 열거자에는 이전 열거자보다 1 더 큰 값이 할당된다.
  • 열거형은 고유한 자료형으로 간주한다. ( 열거형에 다른 열거형을 할당하려고 하면 컴파일 에러 발생 )
  • enum 식별자는 대문자로 시작하는 경우가 많으며, 열거자는 보통 모두 대문자로 이름이 지어진다.
  • 정수를 열거형 값으로 암시적으로 변환하지 않는다. (ex   Animal animal = 5;    ➡️   컴파일 에러 발생 )
    대신 static_cast를 통해 강제로 변환할 수 있다. ( ex   Color color = static_cast<Color>(5);  )

 

사용법

예시1)

#include<iostream>

enum Color
{
  COLOR_BLACK,   // 0
  COLOR_RED,     // 1
  COLOR_BLUE,    // 2
  COLOR_GREEN,   // 3
  COLOR_WHITE,   // 4
  COLOR_CYAN,    // 5
  COLOR_YELLOW,  // 6
  COLOR_MAGENTA  // 7
};

int main(){
  Color paint = COLOR_WHITE;
  Color house(COLOR_BLUE);
  Color apple {COLOR_RED};
  
  std::cout << paint  // 4 
}

 

예시2) 열거자의 값을 명시적으로 정의 가능

enum Animal
{
  ANIMAL_CAT = -3,
  ANIMAL_DOG,          // -2
  ANIMAL_PIG,          // -1
  ANIMAL_HORSE = 5,
  ANIMAL_GIRAFFE = 5,
  ANIMAL_CHICKEN       // 6
}

 

예시3) 열거자들은 같은 네임스페이스에 배치되므로, 열거자 이름을 같은 이름으로 사용하면 에러 발생

enum Color
{
	RED,
	BLUE, // BLUE is put into the global namespace
	GREEN
};

enum Feeling
{
	HAPPY,
	TIRED,
	BLUE // error, BLUE was already used in enum Color in the global namespace
};

 

 

열거형은 언제 유용할까?

특정한 상태 집합을 나타내야 할 때 코드 문서화 및 가독성 목적으로 매우 유용하다.

 

int readFileContents() 
{ 
	if (!openFile()) 
		return -1; 
	if (!readFile()) 
		return -2; 
	if (!parseFile()) 
		return -3; 

	return 0; // success 
}

위와 같이 코드를 작성하면 가독성이 좋지 않다. 이렇게 쓰는 대신 열거형을 사용해보자.

 

enum ParseResult 
{ 
	SUCCESS = 0, 
	ERROR_OPENING_FILE = -1, 
	ERROR_READING_FILE = -2, 
	ERROR_PARSING_FILE = -3 
};

int readFileContents() 
{ 
	if (!openFile()) 
		return ERROR_OPENING_FILE; 
	if (!readFile()) 
		return ERROR_READING_FILE; 
	if (!parseFile()) 
		return ERROR_PARSING_FILE; 

	return 0; // success 
}

이전 코드보다 훨씬 가독성이 좋아졌다.

스마트 포인터

  • #include < memory>
  • c++11부터 지원
  • c++에서는 "메모리 누수"로부터 프로그램의 안정성을 보장하기 위해 스마트 포인터를 제공한다.
  • 스마트 포인터는 생성하면 힙 메모리에 올라간다. (동적할당한다고 보면 됨)
  • 스마트 포인터는 사용이 끝난 메모리를 자동으로 해제해 준다.

 

스마트 포인터의 동작

보통 new를 사용해 기본 포인터가 실제 메모리를 가리키도록 초기화한 후에, 기본 포인터를 스마트 포인터에 
대입하여 사용한다.
➡️ new가 반환하는 주소값을 스마트 포인터에 대입하면 따로 메모리를 해제할 필요가 없다!!

 

 

스마트 포인터의 종류 3가지

  • unique_ptr
  • shared_ptr
  • weak_ptr

 

shared_ptr 

  •  하나의 특정 객체를 참조하는 스마트 포인터가 총 몇개인지를 참조하는 스마트 포인터
  • 원시 포인터 하나를 여러 소유자에게 할당하려고 할 때 사용
  • 참조하고 있는 스마트 포인터의 갯수 = 참조 횟수 ( reference count )
  • 참조 횟수는 특정 객체에 새로운 shared_ptr이 추가될 때마다 +1 증가 / 수명이 다할 때마다 -1 감소
  • 마지막 shared_ptr의 수명이 다하여 참조 횟수가 0이 되면 delete키워드를 사용해 메모리를 자동으로 해제
    -> 원시 포인터는 모든 shared_ptr소유자가 해제될 때까지 메모리에서 사라지지 않는다.

 

shared_ptr 사용 예시

#include<iostream>
#include<memory>

using namespace std;

int main(){
  shared_ptr<int> ptr01(new int(5));  // int형 shared_ptr인 ptr01을 선언하고 초기화함
  cout << ptr01.use_count() << endl;  // 1

  auto ptr02(ptr01);                  // 복사 생성자를 이용한 초기화
  cout << ptr01.use_count() << endl;  // 2

  auto ptr03 = ptr01;                 // 대입을 통한 초기화
  cout << ptr01.use_count() << endl;  // 3

  cout << *ptr01 << endl;             // shared_ptr에 저장되어 있는 값인 5 출력됨
}

 

 

 

make_shared ( shared_ptr 스마트포인터를 안전하게 생성하는 방법 )

shared_ptr<Book> b3 = new Book("CC");
shared_ptr<Book> b4 = b3;

위와 같이 복사생성자로 b4 객체를 만들면 주소값만 복사하는 얕은 복사가 이루어지므로 메모리 해제 시
같은 곳을 해제하게 되어 오류가 발생할 것이다.
이럴 경우 어떻게 동기화시킬 수 있을까? 이때 사용해야 할 것이 make_shared이다.

 

make_shared

  • make_shared()를 이용하면 shared_ptr 인스턴스를 안전하게 생성할 수 있다.
  • make shared는 객체와 참조 카운터를 위한 메모리를 할당한다.
  • shared_ptr를 생성할 때는 make_shared를 사용해서 선언하도록 하자!!
  • 여러 개의 스마트포인터에서 동일한 메모리를 가리킬 때 사용해야 한다.
  • 지정된 타입의 객체를 생성하고, 생성된 객체를 가리키는 shared_ptr을 반환한다.
  • 위와 같은 문제에 대해 안전하게 대처 가능하다.

 

make_shared 사용 예시

 

예시1)

shared_ptr<Person> hong = make_shared<Person>("길동", 29);
cout << hong.use_count() << endl; // 1

auto han = hong;
cout << hong.use_count() << endl; // 2

han.reset();
cout << hong.use_count() << endl; // 1

class Person 객체를 가리키는 hong이라는 shared_ptr 변수를 make_shared()를 통해 생성한 것이다.

 

 

예시2)

#include<iostream>
#include<memory>

using namespace std;

int main(){
  shared_ptr<int> s1 = make_shared<ibnt>(10);
  cout << s1.use_count() << endl; // 1
  
  {
    shared_ptr<int> s2 = s1;
    cout << s1.use_count() << " " << s2.use_count() << endl; // 2 2
  }
  
  cout << s1.use_count() << endl; // 1
  
  return 0;
}

중간에  {}로 임시로 만든 범위가 끝나면 shared_ptr s2는 지역변수라서 자동으로 사라진다.
이때 스마트 포인터이기 때문에 사용자가 delete를 해주지 않아도 자동으로 delete로 소멸자를 호출해준다.
또한 make_shared로 shared_ptr 인스턴스를 생성했기 때문에 객체를 복사해서 만든 새로운 객체에서
delete로 메모리를 해제해줘도 얕은 복사로 인한 문제가 발생하지 않는다.
그래서 블록을 빠져나온 후 s1.use_count()에서 카운트가 준 것이다.

 

 

예시3)

#include<iostream>
#include<memory>
#include<string>

using namespace std;

class Monster
{
private:
  string name_;
  float hp_;
  float damage_;

public:
  Monster(const string& name, float hp, float damage); // 생성자 선언

  ~Monster(){
    cout << "메모리 해제" << endl;
  }

  void PrintMonsterInfo();
};

Monster::Monster(const string& name, float hp, float damage)
{
  name_ = name;
  hp_ = hp;
  damage_ = damage;

  cout << "생성자 호출" << endl;
}

void Monster::PrintMonsterInfo()
{
  cout << "몬스터 이름 : " << name_ << endl;
  cout << "체력 : " << hp_ << endl;
  cout << "공격력 : " << damage_ << endl;
}

int main()
{
  shared_ptr<Monster> dragon = make_shared<Monster>("드래곤", 5000.f, 500.f);

  cout << "현재 소유자 수 : " << dragon.use_count() << endl; // 1

  auto dragon2 = dragon; // 2개의 스마트포인터가 동일한 메모리를 가리키게 됨
  cout << "현재 소유자 수 : " << dragon.use_count() << endl; // 2

  dragon2->PrintMonsterInfo();

  cout << "메모리 해제" << endl;
  dragon2.reset(); // dragon2 해제

  cout << "현재 소유자 수 : " << dragon.use_count() << endl; // 1
}

make_shared를 이용해 스마트포인터를 생성했고, make_shared를 이용해 선언한 스마트포인터를
dragon2가 복사했기 때문에 dragon2 객체를 해제해도 dragon객체의 메모리를 해제하는데 문제되지 않는다.
만약 make_shared를 사용하지 않고 그냥 shared_ptr<Monster> dragon("", , )이렇게 스마트 포인터를 선언해서
이 스마트포인터를 dragon2가 복사했다면 dragon2와 dragon이 같은 주소의 메모리를 해제하기 때문에 
dragon2의 소멸자를 호출해서 메모리를 해제하면 나중에  dragon객체의 소멸자를 호출할 때
dragon은 이미 사라진 메모리에 소멸자로 접근하는 형태가 되기 때문에 에러가 발생하게 될 것이다.

범위 지정 연산자 ( :: )

함수나 변수명 등을 namespace에 따라 구분할 때 사용

 

namespace

영역이라는 말 그대로 변수나 함수들이 선언된 범위, 묶음 이라고 생각하면 된다.

 

사용법

#include<iostream>
#include<string>
using namespace std;

namespace A{
  int test = 2;
  void hello(){
    cout << "I am A" << endl;
  }
}

namespace B{
  void hello(){
    cout << "I am B" << endl;
  }
}

int main(){
  A::hello();
  B::hello();
  cout << A::test << endl;
}

 

클래스 멤버변수 초기화 리스트 사용 방법

  1.  생성자 괄호() 뒤에 콜론(:)으로 표기한다.
  2. 초기화 할 멤버 변수들을 쉼표로 구변하여 표기한다.
  3. 소괄호()를 이용해서 멤버 변수를 초기화한다.

 

사용법

 

예시1)

#include<iostream>
#include<string>
using namespace std;

class Car
{
private:
  string name;
  int number;
  bool inSuv;
public:
  Car() : name("kdy"), number(1212), inSuv(false)
  {
    cout << "생성자 호출" << endl;
  }
  
  ~Book(){
    cout << "소멸자 호출" << endl;
  }
};

Car의 멤버변수 name, number, inSuv를 호출할 때 바로 "kdy", 1212, false로 초기화 시켜준다.

 

 

예시2)

#include<iostream>
#include<string>
using namespace std;

class Person
{
private:
  int age_;
  string str_;
public:
  Person() : age_(0), str_("kdy")
  {
    cout << "디폴트 생성자 호출" << endl;
  }
  
  Person(int age, string str) : age_(age), str_(str)
  {
    cout << "age, str 생성자 호출" << endl;
  }
  
  Person& operator = (const Person& rsh)
  {
    this->age_ = rsh.age_;
    this->str_ = rsh.str_;
    cout << "대입연산자 호출" << endl;
    return *this;
  }
  
  void printAll(){
    cout << "age : " << age_ << endl;
    cout << "str : " << str_ << endl;
  }
};

int main(){
  cout << "\n=====\n";
  Person p1(10, "aa");
  p1.printAll();
  
  cout << " /n=====/n";
  Person p2 = {20, "bb"};
  p2.printAll();
  
  cout << "\n=====\n";
  Person p3 = Person(30, "cc");
  p3.printAll();
  
  cout << "\n=====\n";
  Person p4;
  p4 = Person(40, "dd");
  p4.printAll();
  
  return 0;
}

 

위의 코드를 실행해보면 결과값이 아래처럼 나온다.

 

멤버 초기화 리스트를 사용한 것과 사용하지 않은 것의 차이점

"초기화를 바로 하는 것 vs 나중에 대입으로 값을 초기화 해주는 것"

      "생성 시 초기화      vs         생성 후 초기화"

 

 

생성 후 초기화

class Test{
  int a, b;
public:
  Test(int x, int y) { a = x; b = y; }
};

생성자 내부에서 클래스의 멤버변수들을 초기화했다. 
일반적인 생성자를 이용하여 초기화하는 방법이다.

 

 

생성 시 초기화

class Test{
  int a, b;
public:
  Test(int x, int y) : a(x), b(y) { }
};

멤버 초기화 리스트(member initialization list)를 이용한 초기화 방법이다.

 

 

C++ 멤버 초기화 리스트를 꼭 사용해야 하는 경우

선언과 동시에 초기화 해야하는 변수들이 있을 때 사용

  • 상수(const) 멤버 변수 초기화
  • reference 멤버 변수 초기화
  • 멤버 객체 초기화
  • 상속 멤버 변수 초기화

 

상수 멤버 변수 초기화

틀린 방법

class Test{
  const int test;
  Test(int x) {
    test = x;
  }
};

const value는 위와 같이 초기화하면 에러가 난다. 그래서 선언과 동시에 초기화를 해야 하므로 멤버 초기화 리스트를 사용한다.

 

옳은 방법

class Test{
  const int test;
  Test(int x) : test(x){ }
};

 

 

reference 멤버 변수 초기화

class Test{
  int& test;
  Test(int x) : test(x) { }
};

 

 

멤버 객체 초기화

클래스 내에 객체 멤버가 있는 경우 해당 객체의 생성자가 호출되어야 한다.
이 경우에도 초기화리스트의 사용이 강제된다.

class Test1
{
  int x;
  int y;
public:
  Test1(int a, int b){
    x = a;
    y = b;
  }
};

class Test2
{
  int test;
public:
  Test1 t;
  Test2(int x, int a, int b) : t(a, b){
    test = x;
  }
};

 

 

상속 멤버 변수 초기화

class Parent
{
public:
  int x,y;
  Parent(int i, int j){
    x = i;
    y = j;
  }
};

class Test : public Parent
{
public:
  int test;
  Test(int i, int j, int k) : Parent(i, j)
  {
    test = k;
  }
};

위의 멤버 객체 초기화와 거의 동일하다.

+ Recent posts