들어오는 인자를 대문자로 바꿔서 출력하는 프로그램을 만든다. 각 인자 사이는 공백으로 구분되며 아무 인자도 들어오지 않으면 특정한 메세지를 출력한다.
핵심 개념
- 네임스페이스(namespace)
c++과제의 첫 문제로 객체지향과 네임스페이스에 대한 감을 잡는 문제로 std::toupper을 사용하면 풀 수 있는 문제이다. 이때 std::와 toupper이 각각 무엇을 뜻하는지 알아야한다.
std는 c++ 표준 라이브러리(STL)의 모든 함수, 객체 등이 정의된 이름공간(namespace)이며, toupper은 이 이름공간에 속한 함수이다.
namespace란 어떤 정의된 객체에 대해 어디 소속인지 지정해주는 것을 의미한다. 같은 이름의 객체라도 namespace를 지정해 줌으로써 어디에 속한 함수인지 구분할 수 있게 된다.
이름공간::이름공간에 속한 객체
형식으로 쓰면된다.
*std는 c++ 표준 라이브러리의 네임스페이스 이름으로 미리 예약된 이름이다.
❓그렇다면 네임스페이스와 클래스의 차이는 무엇일까?
언뜻보면 비슷해보이지만 namespace는 단순히 고유한 이름 공간을 설정하는 것이고, class는 객체를 만들기 위한 것이다. 뚜렷한 특성을 가진 객체의 생성 유무가 큰 차이이다.
using namespace
로 이름공간을 쓰는 것을 생략할 수 있다. 하지만 이름공간에 겹치는 함수를 만들 수도 있기 때문에 이는 추천하는 사항은 아니다.
#include <iostream>
using namespace std;
int main() {
cout << "Hello, Wordl!!" << endl;
return 0;
}
//namespace 생략 예시
‘\n’도 여전히 사용할 수 있는데 단순히 객체지향적 프로그래밍을 위해 std::endl을 사용하는 걸까? 이 차이를 이해하려면 먼저 스트림과 버퍼에 대해 알아야 한다.
스트림이란 영어로 “줄줄 이어지다”라는 뜻으로 일련의 연속성을 갖는 데이터의 흐름을 의미한다. 예를 들어 영상 스트리밍이라고 하면 영상 파일을 잘게 쪼개 연속적으로 줄지어 컴퓨터로 보내는 것을 의미한다. c나 c++에서도 데이터를 스트림형태로 내보내거나 읽을 수 있는 기능을 제공한다.
버퍼는 데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리의 영역을 의미한다. 즉 임시 저장 공간이라고 할 수 있다. 버퍼를 채우는 동작을 버퍼링이라고 한다.
버퍼를 비우는 경우는 버퍼가 다 찼을 때 버퍼가 비워지는데 c++에서는 std::flush()라는 함수를 사용해서 버퍼를 수동으로 비울 수 있다.
std::endl과 ‘\n’의 가장 큰 차이는 std::endl은 개행을 찍고 버퍼를 비우는 방식으로 구현되어있다는 것이다. 버퍼를 비우면 바로 출력이 되는데 바로 출력하는 만큼 출력 동작이 한 번더 들어가있기 때문에 std::endl이 ‘\n’보다 느릴 수 있다. 일반적으로는 크게 상관이 없으나 성능이 중요한 문제에서는 고려해볼만한 사항이다.
처음에는 c언어에서 하듯 클래스를 만드는 연습도 할겸 클래스를 만들고 멤버함수로 대문자를 변환하는 함수를 만들어서 사용했다. 하지만 이 문제는 c++의 첫 문제이다보니 이렇게 복잡하게 하는 것보다 namespace를 사용해보는 것이 더 의미있을 것 같아서 std네임스페이스안에 있는 함수를 이용해서 구현했다.
Phonebook과 Contact 두 가지 클래스를 작성하고 연락처를 저장하는 ADD, 특정연락처를 보여주는 SEARCH, 프로그램을 종료하는 EXIT 동작을 만들어라
핵심개념
- 클래스, 객체, 객체지향
- 접근제한자
- 입출력 라이브러리
c++은 c와 다르게 객체지향적언어이다. 이번 문제부터는 class를 사용해 볼건데, 그전에 객체와 클래스에 대해 알아보도록 하자.
객체는 현실 세계의 개념을 프로그램 안에서 표현하기 위한 것으로, 데이터와 해당 데이터를 조작하는 함수들의 묶음이다. 변수들과 참고 자료들로 이루어진 소프트웨어 덩어리 라고도 표현할 수 있다.
여기서 ‘인스턴스’라는 개념도 함께 쓰이는데 인스턴스는 객체가 메모리에 실제로 생성되고 사용 가능한 상태로 존재하는 것을 의미한다. 즉 객체가 메모리에 올리에 할당되어 사용될 수 있는 상태로 된 것을 인스턴스 객체라고 하며 객체에 정의되어 있는 함수를 인스턴스 메소드라고 한다.
클래스는 쉽게 말하면 객체를 만들 수 있는 설계도역할을 한다. 클래스의 인스턴스라고하면 클래스를 이용해서 만들어진 객체를 의미한다. 클래스를 통해 멤버 변수와 멤버 함수를 선언하고 이를 메모리에 올리면 그 멤버 변수는 객체 또는 인스턴스라고 부를 수 있게 된다.
객체의 가장 큰 특징으로는 추상화와 캡슐화가 있다. 추상화는 현실세계의 개념을 프로그램안에서 나타내는 것을 의미한다. 캡슐화는 외부에서 직접 인스턴스 변수의 값을 바꿀 수 없고 항상 인스턴스 메소드를 통해서 간접적으로 조절하는 것을 의미한다. 흔히 getter, setter로 이름 짓는 함수들을 통해 인스턴스 변수의 값을 얻거나 셋팅할 수 있다. 캡슐화를 사용했을때는 내부적으로 어떻게 작동하는지 몰라도 알아서 동작이 처리된다는 장점이 있다.
객체에 대해서 알았으니 객체 지향 프로그래밍의 특성을 알아보도록 하자. 객체 지향 프로그래밍은 크게 추상화, 캡슐화, 상속, 다형성 네 가지의 특성을 가진다.
접근제한자는 접근의 범위를 제한하는 제한자이다. c++ 접근제한자에는 pirvate, protected, public 3가지가 있다.
priveate는 자신의 클래스 내부에서만 접근이 가능하고, protected는 부모 클래스 자신 내부와 부모 클래스를 상속받은 자식 클래스 내부에서 접근이 가능하다. public은 모든 곳에서 접근이 가능하다는 뜻이다. 특별한 명시가 없이 클래스내부에 선언된 변수는 private으로 선언된다.
class test
{
private:
int score;
protected:
int extra_score;
public:
int result;
}
//접근제한자 예시코드
c언어에서는 입출력을 할 때 scanf와 printf를 사용했지만 c++에서는 cout과 cin을 사용한다.
*c++입출력 라이브러리 클래스의 구성 <출처: https://modoocode.com/213>
c++의 모든 입출력 클래스는 스트림의 입출력 형식 관련 데이터를 처리하는 ios_base를 기반 클래스로 하게 된다. 그 다음으로 ios클래스에서는 스트림버퍼를 초기화하고, 현재 입출력 작업의 상태를 처리하는 역할을 한다.
istream클래스는 실제로 입력을 수행하는 클래스, ostream은 실제로 출력을 수항하는 클래스이다.
입출력에 관여하는 클래스의 구성과 역할에 대해 지금은 큰 틀로 알아두고, 문제를 풀면서 더 세부적으로 알아볼 기회가 생길 것이다.
class Phonebook
{
private:
Content content[MAX] : phonebook의 정보가 저장되는 content클래스의 배열
int 총 갯수: 현재 가지고 있는 연락처의 갯수(max값을 찍은 후에는 max값을 유지한다)
int 인덱스: 가장 최근에 저장한 연락처의 인덱스 번호
public:
모든 변수들에 대한 set함수
모든 변수들에 대한 get함수
ADD 함수(void) : 연락처를 add하는 함수
SEARCH 함수(void) : 연락처를 search하는 함수
}
class Content
{
std::string firstName, lastName, nickName, phoneNumber, darkestSecret
}
Phonebook::ADD함수
{
while (i = 0; i < 입력받을 정보의수, i++)
{
인덱스++;
인덱스가 MAX값보다 커지면 인덱스 = 0으로 지정함
firstName, lastName, nickName, phoneNumber 차례로 입력받음
입력받은 정보를 content[인덱스]에 저장함
중간에 eof가 들어올 경우 std::string형식이 들어올 때까지 무한정 다시 입력받음
}
if (총 갯수 < MAX) 총 갯수++;
}
Phonebook::SEARCH함수
{
while (i= 0; i < 총 갯수; i++)
{
번호(i + 1) | firstName | lastName | phoneNumber
조회할 번호를 입력받음
만약 정수가 아니거나 eof인 경우 올바른 수가 들어올 때 까지 입력받음
content[입력받은 번호]의 정보를 한줄씩 출력함
}
}
int main()
{
Phonebook phonebook
while (1)
{
ADD, SEARCH, EXIT중 입력받는 함수
if (입력받은 문자열 == ADD)
phonebook.ADD함수 실행
else if (입력받은 문자열 == SEARCH)
SEARCH함수 실행
else if (입력받은 문자열 == EXIT)
break 후 main함수 종료
else (그 외 다른 모든 문자열)
경고문구 출력
}
}
c언어에서는 문자열을 쓸 때 char 이라는 자료형을 사용했다. c++에서도 사용은 가능하지만, 헤더파일의 std::string
을 주로 사용한다. 헤더에서 헤더를 포함하고 있어서 헤더를 인클루드하고 있다면 헤더의 추가없이 사용가능하다.
std::string 객체는 동적으로 메모리를 할당하여 문자열을 저장하므로, 문자열의 길이에 따라 자동으로 크기가 조절될 수 있다. 이 점이 char 과의 가장 큰 차이점이다
Add함수에서 정보를 입력받을 때 “This is name”이렇게 공백문자를 넣어서 입력받으면 공백문자를 기준으로 구분되어 This만 입력되는 현상이 있다. 개행기준으로 입력받게 하기 위해 std::getline
이라는 함수를 사용했다.
std::getline(string)
istream& getline (istream& is, string& str, char delim);
istream& getline (istream& is, string& str);
입력스트림에서 문자들을 읽어서, 인자로 받은 문자열에 저장한다. 입력 스트림에서 문자를 읽다가 delim문자를 읽게되면 해당 문자를 버리고 읽어들이기를 종료한다. 만약 delim문자를 설정하지 않았다면, 디폴트로 개행문자(’\n’)를 설정하게 된다.
입력스트림에서 문자들을 읽어서, 인자로 받은 문자열에 저장한다. 입력 스트림에서 문자를 읽다가 delim문자를 읽게되면 해당 문자를 버리고 읽어들이기를 종료한다. 만약 delim문자를 설정하지 않았다면, 디폴트로 개행문자(’\n’)를 설정하게 된다.
std::cout << "Enter first name: " << std::endl;
std::getline(std::cin >> std::ws, input);
여기서
std::getline(std::cin, input)
이렇게 쓰면 될 줄 알았는데 왜 뒤에 std::ws를 붙여야할까? 이는 내가 이 프로그램에서 std::cin과 std::getline을 섞어서 썼기 때문이다.
일단 std::ws는 현재 입력 시퀀스에서 가능한 많은 공백문자를 출력하는 함수이다. 공백문자가 아닌 문자가 발견되는 즉시 추출을 중단하고, 이렇게 추출된 공백문자는 폐기된다.
std::cin >> 은 ‘\n’ 직전까지 읽어서 버퍼에 ‘\n’이 남아있다. 하지만 getline()은 ‘\n’까지 읽고 ‘\n’을 버린 문자열을 저장한다. 따라서 이전에 나는 std::cin을 쓴 상태라 버퍼에 ‘\n’이 남아있을 것이다. getline()을 사용하기 전에 std::cin.ignore
함수나 std::ws
를 이용해서 버퍼에 남은 개행문자를 비워줘야한다.
처음 프로그램이 실행되고 실행할 메뉴를 std::string형태로 입력받을 때 ctrl+D(eof)를 입력하면 무한루프가 돈다. 왜 이런 현상이 생길까?
std::cin으로 입력을 받을 때는 std::cin >> 입력받은 값을 저장할 변수
이런식으로 쓰면 되는데 여기서 쓰이는 연산자 >>
는 istream클래스에 정의되어 있는 연산자이다. 이 연산자는 들어온 값으로 스트림의 상태를 관리하는 ios클래스의 flag를 켠다. ios클래스에서는 현재 스트림이 어떤 상태인지 정보를 보관하는데 비트의 종류에 따라 goodbit, badbit, failbit, eofbit 4가지 flag를 가지고 있다.
이 때 eof가 들어오면 연산자 >>는 eofbit를 켜고 입력값을 받지 않고 리턴해버린다. 여기서 문제는 flag는 계속 켜져있기 때문에 루프가 돌면 또 eof를 받았다고 인식하고 루프를 돌고, 또 eofbit를 받았다고 인식하고… 이렇게 해서 무한루프를 돌게 된다.
이걸 방지하기 위해 std::cin.eof()
std::cin.clearerr()
std::cin.clear()
함수를 사용해 wheneof라는 함수를 만들었다.
void PhoneBook::whenEof()
{
std::clearerr(stdin);
std::cin.clear();
std::cout << std::endl;
}
if (std::cin.eof())
{
phonebook.eof();
}
std:ios:eof
bool eof()const;
std::clearerr
void clearerr(std::FILE* stream);
std::ios::clear
위와 비슷한 상황이지만 이번에는 std::cin.fail()을 이용해 볼 것이다. 이번 경우 search함수에서 정수타입 인덱스값에서 정수가 아닌 타입을 입력받았을 때 무한루프를 도는 현상이 생겼다.
이번에는 ios flag 중 failbit플래그가 켜진다. 무엇보다 문제는 버퍼에 잘못된 입력값이 그대로 남아있다는 것이다. 버퍼까지 비워주기위해 이번에는 std::cin.ignore
함수를 사용해보았다.
void PhoneBook::whenEof()
{
std::clearerr(stdin);
std::cin.clear();
std::cout << std::endl;
}
while (1)
{
std::cout << "Enter an index: ";
std::cin >> enteredIndex;
if (std:: cin.eof() || std::cin.fail() \
|| enteredIndex <= 0 || enteredIndex > totalSaved)
{
whenEof();
std::cin.ignore(256, '\n');
std::cout << "Wrong index. Try again" << std::endl;
}
else
break;
}
std::istream::ignore
istream& ignore (streamsize n = 1, int delim = EOF);
누락된 클래스를 복구하는 과제이다. test파일과 함께 출력했을 때 타임스템프 제외하고 모든 내용이 똑같이 나오게 출력되는 클래스를 만들어라(정확히는 복구하여라)
핵심개념
- 생성자
- 소멸자
- static 변수, static 함수
phonebook에서도 쓰긴 했지만 묶어서 공부하는게 좋을 것 같아 02번 문제에 넣었다.
생성자란 객체 생성시 자동으로 호출되는 함수이다. 객체를 초기화하는 역할을 하기 때문에 리턴값없이 써주면 된다. 객체를 생성할 때 생성자에서 정의한 인자에 맞게 써주면 생성자를 호출하면서 객체를 생성할 수 있다. 또 생성자의 오버로딩 또한 가능하다.
class Date{
int year_;
int month_;
int day_;
public:
Date(int year, int month, int day)
{
year_ = year;
month_ = month;
day_ = day;
}
};
//생성자 예시 코드
생성자 호출은 아래와 같이 하면 되는데 암시적방법과 명시적방법이 있다. 효율을 위해 암시적방식을 주로 사용한다.
Date day(2023, 11, 6) //암시적 방법
Date day = Date(2023, 11, 6) //명시적 방법
//생성자호출 예시 코드
인자를 하나도 가지지 않는 생성자는 디폴트 생성자(Default Constructor)라고 하며 클래스에서 사용자가 어떠한 생성자도 명시적으로 정의하지 않았을 경우 컴파일러가 자동으로 추가해주는 생성자이다.
//디폴트 생성자를 정의하는 방법
Date() {
year = 2023;
month = 11;
day = 6;
}
//디폴트 생성자를 호출하는 방법
Date day = Date();
Date day2;
//이는 생성자를 이용해 조기화 하는 것이 아니라 리턴값이 Date이고 인자가 없는 함수 day3을 정의한게 된다.
Date day3();
생성했던 객체가 소멸 될 때 자동으로 호출되는 함수를 소멸자라고 한다. 객체가 동적으로 할당받은 메모리를 해제하는 일을 한다. 소멸자는 아무 인자를 가지지 않으며 오버로딩 되지 않는다. ~(클래스이름)
형태로 써주면 된다.
class Date{
int year_;
int month_;
int day_;
public:
Date(int year, int month, int day)
{
year_ = year;
month_ = month;
day_ = day;
}
~Date()
{
std::cout << "Destructor called" << std::endl;
}
};
클래스의 모든 객체들이 공유하는 변수로써, 객체가 소멸될 때 소멸되는 것이 아니라 프로그램이 종료될 때 소멸되는 변수이다.
초기화는 전역 범위에서 자료형이름 클래스이름::static변수이름 = 초기화할 값
형식으로 초기화해줄 수 있다. 42과제처럼 hpp와 cpp파일을 분리할 때는 .cpp파일에서 초기화해주면 된다.
int Account::_nbAccounts
class Account
{
static int _nbAccounts;
...
}
//static변수의 선언과 초기화 예시
객체와 독립적인 함수이며 객체가 생성되지 않더라도 호출이 가능한 함수이다. 객체 이름으로도 접근이 가능하지만 클래스 이름으로 호출하는 것이 좋다. 또 static 멤버함수 내에서는 일반 변수나 함수는 사용 불가능하지만 미리 전역에 메모리가 할당되는 static멤버 변수나 함수는 사용가능하다. 그렇기 때문에 주로 static 멤버 변수에 접근할 때 사용된다.
class Account
{
private:
static int deposit;
public:
static int getDeposit()
{
return deposit;
}
}
int main()
{
std::cout << Account::getDeposit() << std::endl;
}
//static 멤버 함수 사용 예시
<ctime> 헤더의 std::strftime함수를 이용해서 타임스탬프 형식으로 출력할 수 있다.
#include <iostream>
#include <ctime>
void Account::_displayTimestamp(void)
{
std::time_t timestamp = std::time(nullptr);
char buffer[16];
std::strftime(buffer, 16, "%Y%m%d_%H%M%S", std::localtime(×tamp));
std::cout << "[" << buffer << "] ";
}
strftime
size_t strftime(char* ptr, size_t maxsize, const char* format, const struct tm* timeptr);
시간을 사용자가 원하는 형식에 맞추어 출력한다. format에 들어있는 형식에 맞추어서 timeptr 이 가리키는 tm 구조체의 값을 해석하여 현재 시간을 출력한다. 이 때, 출력되는 문자열의 최대 길이는 maxsize 로 한다.