이름공간이란, c++의 표준 라이브러리의 모든 함수, 객체등이 정의된 공간
다시 말해, 어떤 정의된 객체에 대한 소속을 지정해주는 것이다.
윤호도 송천 윤호가 있고, 평화 윤호가 있듯이, 이름이 같으면, 어떤 공간에 소속되어져 있는지 명시해주는 것이다.
std::cout 에서 만약, std를 정해주지 않았다면, 그냥 cout만으로는 어떤 공간에 소속되어져 있는지 알 수 없다.
namespace header1 {
int foo();
void bar();
}
namespace header2 {
int foo();
void bar();
}
이 두 공간안에 foo, bar의 구분은 어떤식으로 지을것인가?
또한 자기 자신이 포함된 곳 안에서는 따로 명시하지 않아도 괜찮다.
#include "header1.h
namespace header1 {
int func() {
foo(); // 이건 header1의 foo
}
}
#include "header1.h
namespace header1 {
int func() {
foo(); // 이건 header1의 foo
header2::foo(); // 이건 header2의 foo
}
}
이 경우를 우리는 방지해야 한다. 그렇기 때문에, 이름 공간을 꼭 지정해줘야 하는데, 이 작업이 반복되는 경우도 존재한다.
#include "header1.h"
#include "header2.h"
int func() {
header1::foo(); // header1 이란 이름 공간에 있는 foo 를 호출
}
만약, 사용자의 기본 foo() 호출을 정한다면 다음과 같이 해주면 된다.
#include "header1.h"
#include "header2.h"
using namespace header1;
int main() {
header2::foo(); // header2 에 있는 함수를 호출
foo(); // header1 에 있는 함수를 호출
}
#include <iostream>
int main() {
std::cout << "Hello, World!!!" << std::endl;
return 0;
}
다시 우리가 작성했던 코드를 보면, iostream 헤더파일의 std라는 이름 공간에 정의된 cout, endl을 불러온 것이다. 그렇기에 다음과 같이 코드를 변경할 수 있다.
#include <iostream>
using namespace std;
int main() {
cout << "Hello, World!!!" << endl;
return 0;
}
하지만, 잘 쓰지 말자. 이름 충돌이 발생하고 헷갈림을 더할 뿐이기 때문이다. 그냥 백준 풀때만 쓰는게 좋다
추가로 만약, iostream을 파보면 다음과 같을 것이다.
namespace iostream {
void cout;
void endl;
// 함수이거나 변수일텐데, 아마 동작을 하니 함수일 것
}
이 예제를 보자.
#include <iostream>
namespace {
int OnlyInThisFile() {} // 이 함수는 이 파일 안에서만 사용 가능함
int only_in_this_file = 0; // static int x와 동일
/*
* static int OnlyInThisFile()
* ststic int only_in_this_file = 0 과 동일
*/
}
int main() {
OnlyInThisFile();
only_in_this_file() = 3;
}
이 경우에는, 이 소스코드 cpp파일을 제외하고, 다른 파일에선 접근할 수 없다. 익명의 Namespaces는 헤더파일을 통해서 받았더라 하더라도, 불가능하다.
C++에서는 C와 다르게, 입력받는 scanf, cin의 차이가 존재한다.
scanf는 &를 붙여서 사용했지만, C++의 Cin은 char인지, int로 받을지 따로 구분하지 않아도 된다.
std::cin >> user_input;
c에서 지역변수를 바꾸는 방법 (함수를 불러옴으로써)
void change_val(int *p, int value){
*p = value;
}
int main(){
int number = 5;
std::cout << number << std::endl;
change_val(&number, 3);
std::cout << number << std::endl;
return 0;
}
다음과 같이 포인터 변수를 받아와야 한다.
하지만 c++에서는 다음과 같다.
int main() {
int a = 3;
int& another_a = a;
another_a = 5;
std::cout << "a : " << a << std::endl;
std::cout << "another_a : " << another_a << std::endl;
return 0;
}
레퍼런스라는 이름으로, a의 변수를 그대로 사용할 수 있다.
선언 방법은 새로 선언한 변수 앞에 &를 붙여주면 된다.
하지만, 레퍼런스와 포인터의 차이가 있다.
레퍼런스는 반드시 정의 전에 누구의 별명인지 명시해야 한다.
int& another_a; // 불가능하다.
하지만, 포인터는 그냥 선언해도 문제 없다.
int* p;
래퍼런스는 한 번 멸명이 되면 다른이의 별명이 될 수 없다.
int a = 10;
int& another_a = a;
std::cout << "another_a = " <<another_a << std::endl;
int b = 3;
another_a = b;
std::cout << "another_a = " <<another_a << std::endl;
std::cout << "a = " << a << std::endl;
nho/soyeong/C++/"reference
another_a = 10
another_a = 3
a = 3
결과를 보니, 그냥 a = b 와 같은 문장이 된다.
또한 선언을 만약 이런식으로 했다면?
&another_a = b;
이것도 말이 안된다. 결국 &a = b 가 되는건데, 주소값를 값에 넣는 행위인 것이다. 안된다.
그러면 포인터를 다시 살펴보면
int a = 5;
int* p = &a;
int b = 10;
p = &b;
가리키는 주소를 이제 마음대로 변경할 수 있게 된 것이다. 포인터의 장점은 오히려 더 접근성이 좋다는 것이다.
레퍼런스는 메모리상에 존재하지 않을 수 있다.
int a = 10;
int* p = &a; // p는 메모리상에서 8바이트를 차지하고 있다.
p는 주소를 저장하느 int* 으로, 8바이트 크기의 공간을 차지한다.
반면에, 레퍼런스는
int a = 10;
int &another_a = a; // another_a 가 자리를 차지할 필요가 있을까?
새로 할당할 필요가 없다. 왜냐하면, a와 anotehr_a는 같은 취급을 하기 때문에, 바꿔치기만 하면 된다.
void change_val(int &p, int value) {
p = value;
}
이 코드는 &p처럼 참조자를 받게 만들어졌습니다. 이런 경우라면, 사실 매개별수의 이름을 단순히 재지정해서 다시 프로시져 호출하면, 스택프레임이 생성될때, p로 생성되게 됩니다.
int main() {
int x;
int& y = x;
int& z = y;
x = 1;
std::cout << "x : " << x << " y : " << y << " z : " << z << std::endl;
x = 2;
std::cout << "x : " << x << " y : " << y << " z : " << z << std::endl;
x = 3;
std::cout << "x : " << x << " y : " << y << " z : " << z << std::endl;
}
x : 1 y : 1 z : 1
x : 2 y : 2 z : 2
x : 3 y : 3 z : 3
또한, 우리가 c언어에서 scanf를 사용하면 다음과 같다.
scanf(”%d”, &user_input);
근데 c++을 보면 cin >> user_input 으로 값을 입력한다.
cin안에 user_input을 참조자로 가지고 있기 때문이다.
결국 이런 형식인거다
cin(int& user_input) {
...
}
추가로, 레퍼런스의 배열은 불가능하다.
int a, b;
int& arr[2] = {a, b};
근데 왜 안될지 한번 생각해보는게 좋다.
위의 코드를 보면, arr[0]은 a를 의미하고, arr[1]은 b를 의미한다고 생각하면 될 것 같지만, 사실은 조금 어렵다.
왜냐하면, 위에서 얘기했듯이, 참조자는 사실 메모리에 올라가있는게 아니다. 다시 말해, a와 b가 존재할 뿐이지, arr로 대체할 수 없다.
그리고 추가적인 이유로 a와 b가 연속적인 메모리를 차지하지 않고있다면, 우리는 포인터 연산으로 직접 생각해보면 조금 편하다.
*(arr) = a이고, *(arr+1) = b가 될거라고 보장할 수 있나? 결론은 없다.
조금 더 자세히 얘기하자면, b의 주소값이 a의 정확히 4바이트 뒤에 존재할 수 있다면의 얘기인 것이다.
하지만, 이런 경우라면 가능하다.
int main() {
int Arr[3] = {0, 1, 2};
int(&RefArr)[3] = arr;
RefArr[0] = 2;
RefArr[1] = 0;
RefArr[2] = 1;
std::cout << arr[0] << arr[1] << arr[2] << std::endl;
return 0;
}
결과는 201이다.
이게 가능했던 이유는 Arr 의 값을 RefArr에 저장하고, (Arr+1), *(Arr+2)의 값이 1, 2라는 것을 보장할 수 있기 때문에 가능한 것이다.
int function() {
int a = 2;
return a;
}
int main() {
int a = function();
std::cout << "a = " << a << std::endl;
return 0;
}
이 결과를 대충 예상해보면, 2가 나올것이라고 생각할 것이다. 그리고 그게 정답이다. 근데 왜?? 나는 자바를 하면서 이런건 본적이 없다.
이부분만 집중적으로 보면 될 것 같다.
int a = function();
function 안에 정의된 a라는 값이 b에 복사되었다. 우리는 복사되었다는것에 중점을 두면 된다. 왜냐하면, a라는 값은 결국 함수를 실행하고 나면, return을 만나서 사라지기 때문이다. 우리 컴퓨터시스템에서 배웠던 것이 함수를 새로 호출하게 되면 프로시져 호출에 의해 스택 프레임이 생성되는것이 기억 나는가? 그렇기 때문에, 스택프레임에서 a라는 변수는 사라지고, return하면서 나오는 결과만을 남기고 사라진다.
int& function() {
int a = 2;
return a;
}
int main() {
int b = function();
b = 3;
return 0;
}
이 코드를 보면, linux환경에서 실행한다면 어떻게 될까?
segment fault가 발생하게 된다. 이유를 조금 더 살펴보자면, 우리는 위에서 복사에 대한 얘기를 했다. 일단 int&를 return 한다는 것은 결국 function()에 대한 결과는 참조자라는 것이다.
조금 더 풀어쓰자면, b값의 정의되는 방식은 다음과 같다.
a = 2;
int& ref = a;
int b = ref;
중간에 만약에 a가 사라진다면? 바로 segment fault가 발생한다.
이런 상황을 Dangling Reference라고 한다.
지금 맥os라서 테스트 하기 어렵지만, linux에서 실행한다면 오류가 발생하니, 조금 풀어써서 오류 상황을 만들어보자.
#include <iostream>
int main() {
int& ref = []() -> int& {
int a = 2;
return a;
}();
int b = ref;
std::cout << "b = " << b << std::endl;
return 0;
}
왜 안되니
아무튼 해결 방법은 다음과 같다. 지금 값이 사라지고 있으니, 그 값을 유지시키면 된다.
#include <iostream>
int& function(int& a) {
a = 5;
return a;
}
int main() {
int a = 10;
int b = function(a);
std::cout << "b = " << b << std::endl;
return 0;
}
이렇게 되면 메인함수 안에서는 a값을 정의했고, function에 정의된 a의 주소, 다시 말해 참조자로 가지고 있으니까, 값이 변경되어도 그대로 적용되고 segment fault가 발생하는 일도 없어지게 된다.
근데 잠깐만, 이걸 왜 쓸까?
구조체 변수를 리턴하는 경우의 장점을 생각해보면 된다. 만약, C에서 큰 구조체가 있을때, 전체를 다 복사해서 쓰면 비용이 많이 들게 된다 하지만, 전체를 복사하는게 아닌 특정 변수만 가져다 쓸 수 있기 때문에, 비용적인 측면에서 훨씬 이점이 크다.
int function() {
int a = 5;
return a;
}
int main() {
int& c = function();
return 0;
}
이번엔 이 코드를 봐보자. 지금 보면, c의 참조자의 값을 function으로 복사하려는 행위이다. 이제 이게 컴파일 오류가 발생하는 이유를 바로 알 수 있다. function()은 그냥 먼지처럼 사라진다. 그러면, c가 가리키고 있던 값은 댕글링 레퍼런스가 되어버린다. 그런데 또, C++의 아주 좋은 기능인 ㅋㅋㅋ 웃기네 생각해보니까 어떻게든 보완하기 위해서 노력한 사람들이라는게
int function() {
int a = 5;
return a;
}
int main() {
const int& c = function();
std::cout << "c = " << c << std::endl;
return 0;
}
이건 또 된다 ㅋㅋㅋㅋㅋㅋㅋ뭐징
const 참조자를 받으니 갑자기 컴파일과 출력도 정상적으로 된다.
하지만, 상수 래퍼런스로 리턴값을 받으니 리턴값의 생명이 연장되었다. 그러면, const는 function()의 복사를 위한 변수가 계속 살아남아있다고 생각할 수 있다.
❔레퍼런스가메모리상에반드시존재해야하는경우는어떤경우가있을까요? 그리고메모리상에 존재할필요가없는경우는또어떤경우가있을까요?
반드시 메모리에 존재해야 하는 경우: 복사한 후, 참조자의 값이 사라지는 경우에는 메모리상에 반드시 존재해야 한다. 그래서 const가 있는거고 그러면 왜 필요하냐면, 댕글링 래퍼런스가 발생하지 않기 위해서이다.만약, 스택 프레임이 없어져서 값을 더이상 참조하지 못하면 segment fault가 발생하게 되기 때문이다. 다른 방법으론 매개변수로 값을 넘겨주는 행위가 있다.
메모리상에 필요하지 않은 이유: 단순히 값 변경을 위해서 종속적인 필요가 없을때일 것 같다. 임시적으로 값을 알고만 있고, 그 값의 역할이 끝나면 사라지는 경우 예를 들면, function안에서 임시로 값을 쓰고 없어져도 되는 경우를 예로 들 수 있다.
그러면 왜 참조자를 쓰나? 구조체의 사이즈가 큰 경우라면, 특정 필드만 가져올 수 있어서 장점으로 작용한다.
C은 malloc과 free를 통해서 힙 영역에 메모리를 할당받아서 사용했습니다. C++에서또한 마찬가지로 malloc과 free를 사용할 수 있습니다. 하지만 C와 다르게 new와 int 예약어를 통해서도 힙을 할당받고 해제할 수 있습니다.
int main(){
int *p = new int;
*p = 10;
std::cout << "*p = " << *p << std::endl;
delete p;
return 0;
}
이걸 보면, p라는 값은 10으로 결과가 출력되었다.
그러면 문법 형식은 다음과 같다.
T* pointer = new T;
T는 타입, pointer는 변수, new라는 키워드와 타입
해제에는 다음과 같다.
delete p;
int main(){
int a = 5;
delete &a;
return 0;
}
이 코드를 봐보자. 조금 이상하다. 지역변수 자체를 해제하려는 행위를 하면, 다음과 같은 버그가 발생한다.
younho@gim-yunhoui-MacBookAir C++ % cd "/Users/younho/soyeong/C++/" && g++
heap.cpp -o heap && "/Users/younho/soyeong/C++/"heap
heap(47458,0x20cb10c80) malloc: *** error for object 0x16f9d6f28: pointer being freed was not allocated
heap(47458,0x20cb10c80) malloc: *** set a breakpoint in malloc_error_break to debug
zsh: abort "/Users/younho/soyeong/C++/"heap
Heap 공간이 아닌곳에 free를 하는 행위에 대한 에러 메세지이다.
int main(){
int arr_size;
std::cout << "array size";
std::cin >> arr_size;
int *list = new int[arr_size];
for(int i = 0; i<arr_size; i++) {
std::cin >> list[i];
}
for(int i = 0; i<arr_size; i++) {
std::cout << i << "th element of list : " << list[i] << std::endl;
}
delete[] list;
return 0;
}
이 내용을 봐보자.
input받은 값에 따라 배열의 크기를 선언해주고, 배열을 선언할때 포인터와 배열의 크기를 다음과 같이 정의했다.
int *list = new int[arr_size];
배열을 생성할때, []안에 크기를 넣어주면 되는데, 이 코드에서는 초기화가 동적으로 이루어진다.
int main() {
int a = 4;
{
std::cout << "외부의 변수 " << a << std::endl;
int a = 5;
std::cout << "내부의 변수 " << a << std::endl;
}
std::cout << "외부의 변수 " << a << std::endl;
}
이 코드를 보면 a를 중복선언한 것처럼 보이지만, 새로운 scope를 나타내는 { 에 의해서 a의 값은 int a = 5 를 참조하게 되는 것이다.
우리는 언어에서의 특징을 하나 알 수 있다. 가장 가까운 범위에서 찾는다. 라는 규칙이 존재한다는 것을 알 수 있다.
하지만, 이런짓은 하지 말자. 우리는 컴파일러처럼 체계적인 지식을 가지고 있지 않다. 변수 이름은 확실하고 명확하게 바꾸자.
#include <iostream>
typedef struct Animal {
char name[30];
int age;
int health;
int food;
int clean;
} Animal;
void createAnimal(Animal *animal) {
std::cout << "동물의 이름? ";
std::cin >> animal->name;
std::cout << "동물의 나이? ";
std::cin >> animal->age;
animal->health = 100;
animal->food = 100;
animal->clean = 100;
}
void play(Animal *animal) {
animal->health += 10;
animal->food -= 20;
animal->clean -= 30;
}
void one_day_pass(Animal *animal) {
animal->health -= 10;
animal->food -= 30;
animal->clean -= 20;
}
void show_stat(Animal *animal) {
std::cout << animal->name << "의 상태" << std::endl;
std::cout << "체력 :" << animal->health << std::endl;
std::cout << "배부름 :" << animal->food << std::endl;
std::cout << "청결 :" << animal->clean << std::endl;
}
int main() {
Animal *list[10];
int animal_num = 0;
for(;;) {
std::cout << "1. 동물 추가하기" << std::endl;
std::cout << "2. 놀기" << std::endl;
std::cout << "3. 상태 보기" << std::endl;
int input;
std::cin >> input;
switch (input)
{
int play_with;
case 1:
list[animal_num] = new Animal;
createAnimal(list[animal_num]);
animal_num++;
break;
case 2:
std::cout << "누구랑 놀게? : ";
std::cin >> play_with;
if(play_with < animal_num) play(list[play_with]);
break;
case 3:
std::cout << "누구꺼 보게? : ";
std::cin >> play_with;
if(play_with < animal_num) show_stat(list[play_with]);
break;
}
for (int i = 0; i != animal_num; i++) {
one_day_pass(list[i]);
}
}
for(int i = 0; i != animal_num; i++) {
delete list[i];
}
}
팻을 키우는 프로그램이다. 근데 이거 보면, Fish 라는 친구를 새로 만들어본다고 생각하면 정말 답이 없다.
typedef로 새로운 클래스를 만들어야 하고, 정신 나가기 시작하는 것이다. 이런 역겨운 행동을 하기 싫기 때문에 우리는 C++의 객체지향 프로그래밍을 이용해보자.
위의 내용을 보자. 예를 들어서 createAnimal 함수를 보면 실제 사용은 다음과 같다. createAnimal(list[animal_num]);
이걸 보면, createAnimal이 list[animal_num] 을 한다. 이상하다.
동물을 만드는 것이 animal을 한다 라고 직독직해 할 수 있다.
근데 이런 표현보다는 우리는 animal을 create 한다는 표현이 조금 더 옳을 것이다.
그러면 animal 이후 동작을 얘기하는게 조금 더 좋아보일텐데, 우리는 이런식으로 표현해야 한다.
animal.create();
animal.play()
이런 표현이 조금 더 올바르지 않겠나?
우리는 play함수에 animal인자를 넘겨주는게 아니라, 주체인 animal이 play를 하게 되는것이다. 어차피 모든 animal은 play를 알 고 있다.
animal은 자신의 상태를 알려주는 변수와 행동들을 수행하는 함수로 이루어져 있다고 생각하면 된다.
그렇기 때문에, 모든 객체의 공통점을 추출해내는 추상화 과정이 필요하다.
play라는 메서드를 호출했을때, 모든 animal이 그에 맞는 행동을 수행할 수 있어야 한다.
우리는 이런 함수 또는 변수를 인스턴스 변수, 인스턴스 메서드락 부르게 될 것이다.
앞으로 우리의 코드 지향은 다음과 같아야 한다.
Animal animla;
animal.food += 100; /// 이런 방식 x
animal.increase_food(100); /// 이런 방식
왜 이렇게 해야 하나. 하나의 인스턴스의 변수는 보호되어야 하기 때문에, 직접 접근하는 행위를 막아야 한다.
외부에서 직접 인스턴스의 값을 바꿀 수 없고, 인스턴스 메서드를 통해서 조절하는 것을 우리는 캡슐화(Encapsulation) 이라고 부른다.
이 캡슐화의 장점은, 변수의 값을 직접 조절하는게 아니라, 변수 값을 조절하는 행위에 대해 발생하는 부가적인 행동들 또한 정의할 수 있다는 것이다. 예를 들어보자.
void increase_food(int value) {
this->food += value;
this->weight += value/10;
}
와 같은 식으로, 포만감이 증가하니 몸무게도 증가하는 식의 작성이 가능해진다.
그러면 사용자는 단지 음식을 먹이는 행위를 통해서 발생하는 인스턴스의 변화에 대해서는 신경쓰지 않아도 된다. 내부적으로 어떤 동작이 이루어 지지 않아도 된다는 의미가 된다.
이런 캡슐화를 적용해내기 위해서 우리는 어떤 특징을 이용해야 하나?
클래스를 이용해서 객체의 설계를 진행할 것이다. 청사진을 찍어내는 행위를 하는 것이다.
위의 typedef struct Animal 코드를 class형식으로 변경해보자.
#include <iostream>
class Animal {
private:
int food;
int weight;
public:
void set_animal(int _food, int _weight) {
food = _food;
weight = _weight;
}
void increase_food(int inc) {
food += inc;
weight += (inc/3);
}
void view_stat() {
std::cout << "이 동물의 food :" << food << std::endl;
std::cout << "이 동물의 wieght :" << weight << std::endl;
}
};
int main() {
Animal animal;
animal.set_animal(100, 50);
animal.increase_food(30);
animal.view_stat();
return 0;
}
이제 우리는 animal이라는 객체 자체에 그냥 함수만 전달해서 알아서 처리해주는 코드를 구현한 것이다. 더이상은 몰라도 괜찮다.
우리는 private라는 키워드를 이용해서 food와 weight에 대한 직접 접근을 막을 수 있게 되었다. 하지만 public키워드의 경우에는 외부에서도 마음껏 이용할 수 있게 된다.
객체지향 프로그래밍과 조금 더 친숙해지기 위한 기법들을 살펴보자.
오버로딩(overloading)의 한국말 번역은 다음과 같다.
그러면 오버리딩이라는 말은 컴퓨터 프로그래밍에서의 뜻으로는 하나의 함수에 다른 기능을 쌓아 올리는 행위라고 생각하면 된다.
#include <iostream>
void print(char c){
std::cout << "char : " << c << std::endl;
};
void print(int i){
std::cout << "int : " << i << std::endl;
};
void print(double x){
std::cout << "double : " << x << std::endl;
};
int main(){
int a = 10;
char c = 'c';
double d = 3.14;
print(a);
print(c);
print(d);
}
int : 10
char : c
double : 3.14
C언어라면, 위의 함수는 오류가 발생했을 것이다. 하지만, C++의 오버로딩을 이용해서 print함수를 매개변수 타입에 맞춰서 변경할 수 있다.
추가로 이것도 알아야 한다.
#include <iostream>
void print(int i){
std::cout << "int : " << i << std::endl;
};
void print(double x){
std::cout << "double : " << x << std::endl;
};
int main(){
int a = 10;
char c = 'c';
double d = 3.14;
print(a);
print(c);
print(d);
}
int : 10
int : 99
double : 3.14
만약에, char 타입의 매개변수 입력에 따른 처리가 없으면, 매개변수 처리에 가장 적합한 처리를 진행하게 된다.
오버로딩의 과정을 살펴보자.
#include <iostream>
void print(char c){
std::cout << "char : " << c << std::endl;
};
void print(int i){
std::cout << "int : " << i << std::endl;
};
int main(){
int a = 10;
char c = 'c';
double d = 3.14f;
print(a);
print(c);
print(d);
}
이것을 보면, d의 3.14f의 경우는 1단계에 없으니 2단계로 가고, double에 관련한 캐스팅 내용이 없으니 3단계로 넘어오게 되는데, char, int로 변환할 수 있기 때문에 2개 이상의 존재가 발생하여 오류가 발생한다.
class Date {
int year_;
int month_;
int day_;
int date[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
public:
void SetDate(int year, int month, int date);
...
};
void Date::SetDate(int year, int month, int date){
year_ = year;
month_ = month;
day_ = date;
MakeDate();
};
이제 다른방식으로 우리는 코드를 구현할 것이다. 위와 같이 클래스 내부에 구현까지 전부 하는것이 아닌, 클래스를 명시해주고 외부에서 작성할 것이다.