이름공간(namespace)


C언어에서 한 단계 더 발전한 언어, 바로 C++이 C언어와 차이점을 두고 있는 것은 무엇일까요?


우리는 그 차이점 중에서 한가지를 이야기해보려 합니다.

우선 바로 코드를 보면서 어떤 주제에 대해서 설명할 지 감을 잡아보겠습니다.



#include <iostream>

int main() {
        char input[50];
	std::cin >> input;
	std::cout << input << std::endl;
}



이 짧은 코드를 보며 C언어에서 보지 못한 것이 보이시나요?


몇가지 보입니다. 우선 iostream이라는 파일을 include하고, std::in과 std::cout이 보이네요. std::endl라는 것도 보입니다.




하나씩 보도록 하지요.


● #include <iostream>

원래는 iostream.h 헤더 파일 입니다만 c++에서는 .h확장자를 붙이지 않아도 됩니다. 그래서 파일명만 써주도록 하는 거죠. 우리는 이 헤더파일을 include 해주어야 콘솔 출력, 입력을 할 수 있습니다(물론 다른 헤더파일을 써도 할 수는 있습니다..). (아, iostream은 input, output stream이라는 뜻이라는 거~)



● std::cin

cin은 콘솔 입력을 담당하는 객체입니다. >>은 오퍼레이터로 연산자를 의미합니다. cin에서 >>은 콘솔로 입력할때 쓰이는 연산자입니다. 음 그냥 cin>>은 이 후에 데이터에 콘솔로 입력하여라라는 것입니다(in앞에 c는 console의 c입니다.).  C언어에서 scanf와 아주 비슷한 역할을 하는 녀석입니다.

그 앞의 std는 우리가 오늘 이야기할 네임스페이스라고 합니다.


● std::cout

cin을 알았으니 cout도 대충 눈치채셨겠죠? <<역시 연산자를 의미합니다. 

cout은 콘솔출력을 담당하는 객체입니다. 누군가가 cout 보고 카우트라고 하는데 그럼 cin은 킨이냐?


● std::endl

endl은  endline으로 줄을 바꾸어 줍니다. 


namespace

여기서 자주 등장하는 키워드는 무엇일까요? 바로 std입니다. 

std는 네임스페이스의 이름입니다. 네임스페이스는 이름 공간으로 그 변수를 구분해주는 역할을 합니다. 


왜 쓰일까요?

어떤 개발자 A, B 두명이 있습니다. 서로 협동하여 프로젝트를 진행하고 있지요. 

프로젝트를 진행하던 중 개발자A는 함수를 func(int a,int b)해서 함수를 선언했습니다. 개발자 B는 생각없이 func(int a,int b)로 다른 기능을 하지만 함수 이름은 A가 정한것과 같이 정의해버렸습니다.


나중에 프로젝트가 진행될때 같은 함수이름과 같은 매개변수 때문에 오류가 나게 되지요.


그렇게 등장한게 네임스페이스입니다. 개발자 A는 namespace의 이름을 namespaceA, 그리고 개발자 B는 namespaceB로 자신의 함수를 선언합니다. 이렇게 하면 충돌할 확률이 줄어들겠죠??


와 그런데도 namespace이름마저 같으면 답없다 너네 둘은


네임스페이스를 정의하는 것은 너무 쉽습니다. 그저 namespace 키워드를 써주고 namespace의 이름을 정해 중괄호({, })로 묶어주기만 하면 끝입니다.


namespace 네임스페이스 이름{

...

변수나 함수

...

}


어때요? 참 쉽지 않나요? 그래서 네임스페이스에 존재하는 함수와 이름을 사용하려면 네임스페이스이름::함수 또는 변수 로 써주면 됩니다. 


예를 한번 볼까요?





#include <iostream>

namespace myNamespace {
	int a, b;
	int sum(int a, int b) {
		return a + b;
	}
}
int main() {
	myNamespace::a = 30;
	myNamespace::b = 40;
	std::cout << "30+40="<< myNamespace::sum(myNamespace::a, myNamespace::b) << std::endl;
}


myNamespace라는 네임스페이스에서는 a,b라는 변수, sum이라는 함수가 있네요. 얘네들을 써먹어서 a+b를 sum함수로 값을 도출해냅니다. 그러니 myNamespace::가 앞에 붙어있죠.



원하는 결과가 나오죠?


네임스페이스안에 네임스페이스를 지정할 수도 있습니다. 그렇게 되면 충돌확률은 현저히 줄어들겠네요. 하지만 쓸 일이 거의 없습니다. 이름도 길어지고.


namespace 네임스페이스 이름{

namespace 네임스페이스 이름1{

...

변수나 함수

...

}

namespace 네임스페이스 이름2{

...

변수나 함수

...

}

namespace 네임스페이스 이름3{

...

변수나 함수

...

}

}



접근하는 방법도 역시 똑같습니다.

네임스페이스::네임스페이스이름n::함수나 변수명



좋은것 같은 데 귀찮아

근데요. 사실 우리는 공부하는데 일일이 std::붙어주는 게 여간 귀찮은게 아니죠.


그런 경우에는 using namespace 네임스페이스이름 을 써주기만 하면 namespace의 이름으로 구분할 것 없이 바로 변수, 함수 등의 데이터를 사용할 수 있습니다.


이렇게 말입니다.


#include <iostream>

using namespace std;
namespace myNamespace {
	int a, b;
	int sum(int a, int b) {
		return a + b;
	}
}

using namespace myNamespace;
int main() {
	a = 30;
	b = 40;
	cout << "30+40="<< sum(a, b) << endl;
}


바로 위의 코드를 네임스페이스를 사용하지 않고 구현했습니다. 코드가 확 줄고 편해졌지요?


이제까지 네임스페이스를 알아보았습니다.


다음에 봐요~



반응형
블로그 이미지

REAKWON

와나진짜

,

구조체와 포인터


지난 번에 구조체에 대한 이야기를 쬐~~~끔 했었죠? 이번에도 구조체를 가지고 놀아봐요.


구조체를 통해서 ,이야기했다 시피 여러가지 자료형을 통합적이고 효율적으로 작업을 할 수 있는 것이 장점입니다.


자료형이라...

우리는 이제까지 int, char, double, float 같은 자료형을 많이 봐왔지요. 하지만 구조체 자체도 자료형이 될 수 있습니다. 즉, 변수로 선언이 가능하다는 것이죠.




한번 되짚어 봅시다. int 자료형 변수 a를 선언하고 10이라는 값을 집어 넣어 보아라 한다면 우리는 식은 죽 먹기로 해낼 수 있습니다.


int a;

a = 10;


이렇게 자료형을 통한 변수는 값을 대입하는 것 외에도 


1. 매개변수로 쓰일 수 있다.

2. 포인터로 참조할 수 있다.

3. 배열로 쓰일 수 있다.

4. 구조체의 변수로 쓰일 수 있다.


뭐 이밖에도 여러분이 더 잘 알거에요.


그래서 무슨 말이 하고 싶은 거냐 넌?


구조체도 자료형이 될 수 있다고 했습니다. 그러니까 위와 같이 쓰일 수도 있다는 이야깁니다. 똑같습니다. 위의 순서대로 구조체를 갖고 놀아 봅시다.


일단 매개변수로 쓰이는 경우를 보도록 하지요.


#include <stdio.h>

typedef struct student{
	char *name;
	int math;
	int kor;
	int eng;
} student;

float avg(student person);
student getHonorStudent(student me, student you);

int main() {
	student reakwon = { "REAKWON",40,50,40 };
	student seonmi = { "선미",90,95,100 };

	student honorStudent = getHonorStudent(reakwon, seonmi);
	printf("우등생은 %s입니다.\n", honorStudent.name);
	return 0;
}

float avg(student person) {
	return (person.math + person.kor + person.eng) / 3.0;
}
student getHonorStudent(student me, student you) {
	if (avg(me) > avg(you))
		return me;
	if (avg(me) < avg(you))
		return you;
}

전교생이 두 명인 학교에서 우열을 참 가리기 쉬운 학생 두명이 있습니다(그러니 평균이 같은 학생은 없다는 가정을 하겠습니다). 누가 우등생인가를 구하는 코드입니다.




위 코드에서 reakwon이라는 학생 성적은 제 고등학교 시절과 정확히 똑같군요.


우리는 avg나 getHonorStudent함수에서 매개변수로 쓰인 student 구조체를 주목해야합니다. 변수가 매개변수로 함수로 전달하는 것과 일치하죠. 그 매개변수 앞에는 student라는 자료형이 있는 것과 같은 겁니다.


결과를 보죠.



예상했다 싶이 선미라는 아이가 우등생이네요.

오~ 매개변수로 쓰일 수 있구나!


두번째, 포인터로 참조할 수도 있습니다.

우리는 포인터를 통해 그 주소에 접근할때 *를 이용해서 접근했었죠.

포인터도 역시 똑같습니다.


(*구조체 변수).변수이름


구조체는 이와 같은 접근 방법외에도 다른 방법으로도 포인터를 통해 참조할 수 있습니다. "->" 이와 같은 표시로 말이죠.


구조체 변수->변수이름


마치 화살표 같은게 포인터 티가 나죠?


이제 코드로 한번 확인해보도록 합시다.


#include <stdio.h>

typedef struct student{
	char *name;
	int math;
	int kor;
	int eng;
} student;


int main() {
	student reakwon = { "REAKWON",40,50,40 };
	student *me = &reakwon;

	printf("me의 크기:%d\n", sizeof(me));
	printf("reakwon의 주소:%p, me가 가리키는 주소:%p\n", &reakwon, me);
	printf("\n");
	printf("포인터를 통해서 값을 읽어오는 방법 1");
	printf("수학:%d, 국어:%d, 영어:%d\n", (*me).math,(*me).kor,(*me).eng);

	printf("\n");
	printf("포인터를 통해서 값을 읽어오는 방법 2");
	printf("수학:%d, 국어:%d, 영어:%d\n", me->math, me->kor, me->eng);
	
      return 0;
}



다음 결과 사진을 보고서 다시 이야기해 보도록 합시다.




포인터를 배울때와 같이 변수 me는 reakwon의 주소를 값으로 갖고 있습니다. 그리고 접근하는 방법 두가지 역시 같은 값을 나타내고 있습니다.


하지만 크기를 보세요. 구조체 포인터는 역시 포인터 크기(4바이트)와 같은 크기입니다. 주소만 갖고 있으면 되기 때문이죠.


그림으로 그려보면 이런 그림이겠군요.





우리는 한가지 생각해볼 점이 있습니다. 

구조체의 크기가 크고 함수 매개변수로 쓰일 경우 어떻게 넘겨주는 것이 더 효율적일까요?

값을 복사하는 normal한 매개변수로 쓴다면 구조체의 크기만큼 복사해야합니다.

그러나 포인터를 사용한다면 단지 주소값만 넘겨주면 되기 때문에 시간 면에서나 효율 면에서 유리할 수 있습니다.


물론 포인터는 매개변수의 변형을 일으킬 수도 있지만, 그런 원치않는 조작을 막기위해서 const라는 키워드가 존재하는 겁니다.



이제 구조체를 배열로 관리해보겠습니다. 역시 쉽습니다. 바로 코드로 봅시다.


#include <stdio.h>


typedef struct student{
	char name[30];
	int math;
	int kor;
	int eng;

} student;

float avg(student who) {
	return (who.math + who.kor + who.eng) / 3.0;
}

int main() {
	
	student students[3];

	for (int i = 0; i < 3; i++) {
		printf("이름:");
		scanf("%s", students[i].name);

		printf("수학 점수:");
		scanf("%d", &students[i].math);

		printf("국어 점수:");
		scanf("%d", &students[i].kor);

		printf("영어 점수:");
		scanf("%d", &students[i].eng);
		printf("\n");
		
	}


	for (int i = 0; i < 3; i++) {
		printf("%s의 점수\n",students[i].name);
		printf("수학 %d, 국어 %d, 영어 %d\n",
			students[i].math, students[i].kor, students[i].eng);
		printf("평균 %.1lf\n",avg(students[i]));
		printf("\n");
	}

	return 0;
}



세명의 학생의 이름과 점수를 입력받고 점수와 평균을 출력해주는 코드입니다. 


배열을 포인터 연산으로 나타낼 수 있듯이 구조체 배열 역시 포인터 연산으로 나타낼 수 있습니다. 위의 코드를 (*(students+i)).kor 과 같이 코드를 한 번 바꾸어 실행해보세요.


위 코드의 결과가 아래의 캡처화면입니다.



구조체는 이렇게 편리합니다. 




이제 마지막, 구조체는 구조체를 포함할 수 있습니다. 이것도 역시 변수와 같은 성격이죠. 구조체를 변수로 쓰는 방법은 아래와 같습니다.


typedef struct person {

char name[30];

int age;

char sex[10];

struct person friends[3];

} person;



구조체 안에 같은 구조체 타입의 변수가 배열로 들어가 있습니다. 구조체는 자료형이라고 했기 때문에 뭐 놀랍지도 않군요.


물론 다른 구조체 타입의 변수까지 멤버로 가질 수도 있습니다. 이런 엉뚱한 구조체는 어디서 쓰일까요?


나중에 자료구조에서나 알고리즘에서 트리의 노드와 같은 것으로 쓰일 수가 있습니다.



이제까지 구조체에 대해서 공부해봤습니다. 바이바이~

반응형
블로그 이미지

REAKWON

와나진짜

,

구초제(Structure)

 

우리는 같은 자료형을 여러개 쓸 때는 배열이라는 것을 썼었죠. 아주 유용합니다. 배열과 반복문을 통해서 조금 더 쉽게 프로그래밍을 할 수 있었습니다.

 

하지만 같은 자료형이 아닌, 다른 자료형을 하나로 통합해서 관리해야할때는 어떻게 할까요??

 

통합적이게 데이터를 묶고 쉽게 접근할 수 있는 기법이 바로 구조체라고 합니다.

 

만약 구조체가 없이 사람들의 데이터를 관리한다고 칩시다.

 

char name[MAX_N][30];

int age[MAX_N];

char phone[MAX_N][30];

 

자료형이 서로 다르니, 서로 같아도 쓰임새가 다르니, 여러개의 변수를 관리할 수 밖에 없습니다. 프로그래밍의 가독성과 효율성을 떨어뜨리게 되는 것이죠.

 

 

하지만 구조체는 우리의 작은 소원을 들어줍니다. 하나로 묶는 것이죠. 

구조체를 정의하는 방법은 상당히 간단합니다. 이렇게요.

 

struct User{

char name[30];

int age;

char phone[30];

}

 

User가 바로 구조체의 이름이며, 그 안의 변수들은 멤버변수라고 합니다.

 

이렇게 데이터를 묶어서 배열로 정의하게 되면 저 위의 코드를 조금 더 편하게 관리하고 접근할 수 있습니다.

 

struct User user[MAX_N];

 

대충 이런 느낌입니다. 쉽죠?

 

그럼 오늘 포스팅을 이걸로 마치겠습니다.

 

이제 본격적으로 구조체를 어떻게 사용하는 지 알아보도록 하겠습니다.

구조체 정의는 위에서 본것과 같이 함수밖에서(전역) 구조체 안의 멤버 변수들을 선언해줍니다. 

struct 구조체이름{

...

변수

...

};

 

그리고 함수안에서는 다음과 같이 사용할 수 있습니다.

 

struct 구조체이름 구조체변수명={ ..., 멤버 변수 값, ...};

 

이렇게 한꺼번에 구조체에 선언된 변수 순서대로 값을 초기화 할 수도 있고

또는

 

struct 구조체이름 구조체변수명;

구조체변수명.멤버변수명=변수값

...

 

하나하나 명시적으로 변수에 값을 할당할 수 있습니다.

 

변수에 접근하려면 어떻게 할까요??

그것도 역시 간단합니다. 구조체변수에 .(dot)을 찍어서 멤버변수명을 입력해서 접근할 수 있습니다.

 

구조체변수명.멤버변수명

 

 

이제까지 설명했던 내용을 코드로 간단하게 사용해보도록 합시다.

#include <stdio.h>
struct User {
        char name[30];
        int age;
        char phone[30];
};
int main() {
        struct User user = { "REAKWON",2018,"010-????-????" };
        printf("이름:%s\n", user.name);
        printf("나이:%d\n", user.age);
        printf("전화번호:%s\n", user.phone);
}

 

원하는 대로 결과가 나오죠?

 

그런데 우리는 구조체를 typedef로 조금 더 간단하게 선언할 수 있습니다. 

 

typedef은 자료형을 조금 더 편하게 관리하기 위해서 쓰입니다.

typedef 원래의 자료형 내가 정의한 자료형;

이렇게 하면 변수를 원하는 대로 정의해줄 수 있습니다.

아래와 같이요.

typedef unsigned int u_int;u_int val=30;

 

typedef struct 구조체명{

구조체 정의부

} 정의할 구조체명;

 

그러면 선언에서 struct를 빼고

 

정의한 구조체명 구조체변수

 

이렇게 조금 더 편하게 쓸 수도 있습니다.

 

아래의 코드는 바로 위의 코드를 typedef를 사용해서 바꾼 코드랍니다.

 

 

#include <stdio.h>
typedef struct User {
        char name[30];
        int age;
        char phone[30];
} _User;
int main() {
        _User user = { "REAKWON",2018,"010-????-????" };
        printf("이름:%s\n", user.name);
        printf("나이:%d\n", user.age);
        printf("전화번호:%s\n", user.phone);
}

 

구조체 크기

구조체의 크기를 한번 알아보도록 할까요?

아까와 같은 구조체를 한번 변경해서 보도록하겠습니다.

struct User { 
    char name[10]; 
    int age; 
    char phone[10]; 
}

char자료형은 1바이트이고 10개의 배열을 가졌으니, 10바이트

int자료형은 4바이트입니다. 

또 char배열 10개로 10바이트해서 구조체의 크기는 총 24바이트겠네요.

 

코드로 확인해보도록 하지요.

 

#include <stdio.h>

struct User {
    char name[10];
    int age;
    char phone[10];
};

int main() {
    printf("User 구조체 사이즈:%d\n",sizeof(struct User));
}

 

 

 

 

??????

제가 틀렸군요. 28바이트가 나오네요.

 

왜일까요??

구조체의 사이즈는 그 구조체 멤버중에서 가장 큰 자료형에 따라 결정이 됩니다. 가장 큰 자료형은 int로 4바이트가 되죠? 그래서 4의 배수로 크기가 결정됩니다.

이해를 돕기 위해 그림을 첨부합니다. 

 

 

사각형 한 칸의 크기는 1바이트를 나타냅니다. name과 phone은 10바이트를 차지할 것 같지만, char형은 1바이트이고, int형인 age는 4바이트니까 큰 자료형에 의해서 4의 배수로 메모리가 할당이 됩니다.

name과 같이 12바이트를 맞추기 위해 여분의 바이트를 붙여주는 것을 우리는 패딩(padding)이라고 합니다.

 

아~ 그래서 28바이트가 나오는 구나. 

그렇다면 메모리가 낭비될텐데 조금 더 아낄수 있는 방법은 없을까요?

 

우리는 변수명을 단지 바꾸어 주어 이런 메모리 낭비를 막을 수 있습니다. 이렇게요.

 

#include <stdio.h>

struct User {
    char name[10];
    char phone[10];
    int age;
};

int main() {
    printf("User 구조체 사이즈:%d\n",sizeof(struct User));
}

 

이때의 메모리를 보면 아래의 그림과 같습니다.

 

이렇게 4바이트를 아낄 수 있지요.

 

왜 이렇게 메모리를 큰 자료형에 맞출까요?? 네트워크 통신에서 관련이 있습니다. 우리는 아직 그 크기에 그렇게 신경써도 되지 않아도 됩니다.

 

이렇게 구조체가 무엇인지, 어떻게 정의하는지 알아보았는데요. 다음 시간에는 구조체에 대해서 조금 더 세세하게 알아보고 구조체 포인터에 대해서도 이야기해보도록 하겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

동적 메모리 할당


우리는 이제껏 메모리를 할당할때 정적으로 할당했습니다. 어떤 것이냐면


int arr[100];


이렇게 할당을 했었죠. 뭐 문제없습니다. 실행도 잘 되구요.


하지만 이런 상황은 조금 불편할 수 있겠죠.


● 처음 int배열 100개가 필요하다고 생각했는데 프로그램을 실행하다 보니 int배열이 500개만 필요한 경우


● int배열 500개가 모자라 배열 500개를 메모리에 더 할당해야 할 경우


우리는 메모리를 효율적으로 사용하기 위해서 너무 남거나, 너무 모자란 메모리 할당을 피해야할텐데요. 그 목적을 달성하기 위해서 이런 코딩은 어떨까요?




#include <stdio.h> int main() { int size, i; scanf("%d", &size); int arr[size]; for (i = 0; i < size; i++) scanf("%d", &arr[i]); for (i = 0; i < size; i++) printf("arr[%d] = %d\n",i,arr[i]); }


size를 입력으로 받고, 배열의 크기를 size만큼 할당하는 겁니다. 그리고 그 size만큼 for루프를 돌아 정수를 입력받고 있네요.

이 코드는 분명 얼핏 보기에는 문제가 없어보입니다. 그러나 메모리에 할당하는 시점을 고려한다면 이 코드는 실행조차 안되고 컴파일 에러가 발생하게 됩니다.


왜요?


stack에서 메모리는 컴파일 시점에서 결정됩니다. 우리는 실행중에 입력을 받고, 실행 중에 메모리를 할당해야하는데, 그 앞의 단계인 컴파일단계에서는 얼마만큼의 메모리를 할당할지 알 수 있는 방법이 없죠. 컴파일러는 모릅니다.


우선 우리는 메모리 구조에 대해서 조금 알아야 할 필요가 있습니다. 





스택영역

우리는 이제껏 스택영역에 메모리를 할당해 왔습니다. 컴파일 시점에 결정되는 영역입니다.


함수의 지역변수, 매개변수등이 이 메모리에 할당이 됩니다. 그리고 함수가 종료되었을때 할당된 메모리를 반환하게 됩니다.


그러니까 메인 함수안의 변수들은 모두 스택영역에 할당이 된거죠. 




힙 영역

이 영역의 메모리는 실행시점(Run Time)에 결정됩니다. 프로그래머에 의해서요. 이 영역을 힙영역이라고 합니다.


한가지 더 보충설명을 하자면 스택영역은 높은 주소에서 낮은 주소로 할당이 되고, 힙영역은 낮은 주소에서 높은 주소로 할당이 됩니다. 그래서 재귀함수를 통해 함수를 계속호출하게 되면 힙영역을 침범해 스택오버플로우가 발생하게 됩니다. 그 반대의 경우는 힙오버플로우가 발생합니다.


데이터 영역

이 영역의 메모리는 정적변수, 전역변수, 구조체 등 함수 외부에서 선언되는 변수들이 이 메모리에 할당됩니다.


코드 영역

코드영역에는 프로그램의 실행 명령어들이 존재합니다.


우리가 이번에 주목해야할 영역은 힙영역입니다. 위의 코드를 정상적이게 동작시키기 위해서는요.


그 목적을 달성하기 위해서 나온 함수가 바로 malloc함수입니다. malloc함수는 stdlib헤더에 선언되어 있으며 malloc함수를 사용하기 위해서는 stdlib.h를 include해야합니다.


void *malloc(size_t size)


이 함수는 size만큼의 메모리를 힙영역에 할당합니다. 어떤 자료형일지 모르니 반환형 데이터는 void포인터입니다. 


하지만 그냥 메모리만 할당하고 해제하지 않으면 메모리가 누출됩니다. 우리는 메모리를 이제 쓰지 않을 경우(거의 함수 반환 직전)에 free함수를 써서 메모리를 해제해야합니다.


void free(void *ptr)


이제 malloc함수와 free함수를 사용해서 위의 코드를 오류없이 실행시켜보도록 하지요.



#include <stdio.h> #include <stdlib.h> int main() { int size, i; scanf("%d", &size); int *arr=(int*)malloc(sizeof(int)*size); for (i = 0; i < size; i++) scanf("%d", &arr[i]); for (i = 0; i < size; i++) printf("arr[%d]=%d\n", i, arr[i]); free(arr); }



무리없이 실행도 되고, 원하는 결과를 얻을 수 있습니다.


malloc함수 외에도 calloc, realloc함수가 있습니다.


void *calloc(size_t n, size_t size)


calloc은 malloc과 힙영역에 할당하는 것을 똑같습니다. 사용법과 초기값이 다른데요.

calloc은 할당된 메모리를 전부 0으로 초기화합니다. malloc은 0으로 전부 초기화 시키지 않죠. 

쓰임새는 아래의 코드와 같습니다.


#include <stdio.h> #include <stdlib.h> int main() { int n, i; scanf("%d", &n); int *arr=(int*)calloc(n,sizeof(int)); printf("calloc 0으로 초기화\n"); for (i = 0; i < n; i++) printf("arr[%d]=%d ", i, arr[i]); printf("\n"); for (i = 0; i < n; i++) scanf("%d", &arr[i]); for (i = 0; i < n; i++) printf("arr[%d]=%d\n", i, arr[i]); free(arr); }





realloc함수는 할당된 메모리를 다시 할당할때 쓰입니다. 기존에 할당했던 포인터와 다시 할당할 size를 매개변수로 전달합니다. 기존에 있던 값은 변함이 없습니다.


void *realloc(void *memblock, size_t size)


realloc은 아래처럼 쓰면 됩니다.

#include <stdio.h> #include <stdlib.h> int main() { int n,m,i; printf("처음 크기 입력\n"); scanf("%d", &n); int *arr=(int*)malloc(sizeof(int)*n); for (i = 0; i < n; i++) scanf("%d", &arr[i]); for (i = 0; i < n; i++) printf("arr[%d]=%d\n", i, arr[i]); printf("다시 할당될 크기 입력\n"); scanf("%d", &m); //realloc함수도 다시 할당 arr = (int*)realloc(arr, sizeof(int)*m); for (i = n; i < m;i++) scanf("%d", &arr[i]); for (i = 0; i < m; i++) printf("arr[%d]=%d\n", i, arr[i]); free(arr); }





2차원 배열은 어떻게 할당할까요?

우선 1차원 배열의 메모리를 힙에 할당하고, 1차원배열 각각 메모리를 할당하면 됩니다. 


메모리 해제할때도 일차원배열 메모리를 해제하고, 2차원배열 메모리를 하제하면 됩니다. 조금 복잡할 수도 있어요.



#include <stdio.h>
#include <stdlib.h>

int main() {
	int n, m, i;
	int **arr;
	scanf("%d %d", &n,&m);

	arr = (int**)malloc(sizeof(int *)*n);


	for (i = 0; i<n; i++)
		arr[i] = (int *)malloc(sizeof(int)*m);

	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
			scanf("%d", &arr[i][j]);
	printf("\n\n");
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < m; j++)
			printf("arr[%d][%d]=%d\n", i, j, arr[i][j]);
		printf("\n");
	}

	for (i = 0; i<n; i++)
		free(arr[i]);
	free(arr);

}
	




이제까지 메모리 할당 함수를 사용해서 동적메모리를 할당하는 방법을 알아보았습니다. 조금 어렵죠?

반응형
블로그 이미지

REAKWON

와나진짜

,

void 포인터


void 포인터??

이런 포인터는 처음 들어봤네요~ 분명 void라는 것은 함수 앞에서 반환형이 없을 때 쓰이는 키워드로 아는뎁;

void main은 많이 봤는데...


지금부터 이야기할 주제가 바로 void포인터라고 합니다. 우리는 이제껏 자료형이 정해져있는 포인터 예를 들면


int *a


라는 형태는 봐왔잖아요?


그리고 그 포인터에 주소를 할당하는 방법은 이런 것이죠.


int a=100;

int *b=&a; 


이걸 말로 풀어서 설명한다고 하면

b는 a의 주소를 가지고 있고, b를 통해 a를 참조할 수 있는데 그곳에는 int형 데이터가 있다!

라고 말이에요.


우리는 이 말속에서 힌트를 얻을 수 있습니다. 

그곳에는 int(정수형)형 데이터가 있다 라는 말을 집중해주세요.


위의 int *를 void *로만 바꾸어 써보고 읽어볼게요.


int a=100;

void *b=&a;


b는 a의 주소를 가지고 있고, b를 통해 a를 참조할 수 있는데 그곳에는 void형 데이터가 있다.




void 형 데이터가 있다....(?) void는 "빈공간"이라는 뜻을 내포하고 있는데요. 컴퓨터는 자료형을 모르기 때문에 빈공간처럼 보는 겁니다.

그러기 때문에 앞으로 우리는 이렇게 읽어야 할 겁니다.


b는 a의 주소를 가지고 있고, b를 통해 a를 참조할 수 있는데 그곳에는 알 수 없는 자료형 데이터가 있다.


이 형태 그대로 데이터를 참조하면 컴퓨터는 "아 몰랑!" 합니다.


printf("%d\n", *b);     //오류

 

얼마나 참조해야하는지 알 수 없기 때문입니다. void는 단순히 주소값만을 가지고 있습니다.


우리는 void가 가리키고 있는 데이터의 형태를 알고 있습니다. 우리는 똑똑하니까요(?). 그래서 우리는 *b가 무엇이냐 라고 질문할때 100이라고 대답할 수 있습니다. 우리는 똑똑하기 때문이죠.


하지만 단순히 주소값만!알고 있는 우리 void형은 그 형태가 int형이든 char 형이든 구조체든 문자열을 가리키고 있는 포인터이든 상관없이 단순히 주소값만이요. 그러니 void포인터는 자료형이 무엇이든 간에 주소값만 바라봅니다.


주소만 갖으면 되기 때문에 포인터의 크기(4바이트)만 갖고 있고, 포인터 연산조차 할 수 없습니다.


정말 읽을 수 없는 지 한번 코드로 살펴봅시다.

#include <stdio.h>

int main() {
	int a = 10;
	void *b = &a;
	printf("%d\n", *b);
}
	


실행시킬 수 조차없이 빨간줄로 


Error:Expression must be a pointer to a complete object type


라는 에러를 보게 됩니다. 

완전한 형태의 포인터로 바뀌어야한다 라고요.




우리는 컴퓨터에게 "너가 가리키고 있는 데이터 자료형은 int형이야" 라고 명확히 알려주어야합니다. "그러니까 넌 묻지도 따지지도 말고 4바이트만 읽으면 돼!" 라고요.

어떻게 알려줄까요??


우리는 형변환을 알고 있습니다. 그걸 사용하는 것이죠.


printf("%d\n", *(int*)b); 


이렇게 하면 void*는 int형을 읽을 수 있는 int*로 변환되게 됩니다.

정말 빨간 줄 없이 int형 데이터를 읽을 수 있는 지 코드로 볼까요?

#include <stdio.h>

int main() {
	int a = 10;
	void *b = &a;
	printf("%d\n", *(int*)b);
}


빨간 밑줄은 없어졌군요~ 





실행도 정상적으로 되는 것을 확인할 수 있고, 10을 정확히 읽는 것도 확인할 수 있습니다.


char형은 어떻게 변환할까요? 그것도 간단합니다. 바로 char*로만 바꾸어주면 됩니다.

#include <stdio.h>

int main() {
	char a= 'c';
	void *b = &a;
	printf("%c\n", *(char*)b);
}





네, 'c'라는 문자를 제대로 읽을 수 있군요.




문자열 역시 다르지 않습니다.

#include <stdio.h>

int main() {
	char *str= "문자열";
	void *b = str;
	printf("%s\n", (char*)b);
}

str자체가 문자열을 가리키고 있는 포인터이기 때문에 str변수 앞에 &(amp)를 붙여주지 않습니다. 이해하죠?




문자열도 잘 읽어오는군요.



이렇게 void포인터는 만능입니다. 어떤 자료형이건 바로 참조할 수있죠. 알맞은 자료형으로만 변경한다면 말이죠.


우리는 void포인터를 동적할당할때 유용하게 사용합니다. 동적할당에 사용하는 malloc과 같은 함수들이 void*로 반환하기 때문입니다.

malloc은 특정한 size의 크기로 메모리를 할당하고나서 우리들에게 알맞게 변환해서 써라 라는 의미로 void*를 내뱉게 됩니다.


어차피 나중에 배울 malloc함수의 원형 한번 볼까요?


void *malloc(size_t size)


앞서 말한대로 void*를 토하고 있습니다. 너네가 맘대로 바꾸라고 말이죠.


한번 보세요. 아~ 이런 변태같은 함수도 있구나~ 라고 기억하시기 바랍니다.



이것으로 void포인터에 대해 설명해보았습니다. 감사합니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

Fragment


프래그먼트(Fragment)는 안드로이드 3.0부터 지원되었다고 하는데요. 프래그먼트란 무엇일까요..?

간단하게 말하자면 여러가지 화면을 유연하게 지원하기 위한 것이라고 할 수 있겠습니다. 


어플리케이션의 단위는 Activity이고, 그 Activity도 여러 Fragment로 구성될 수가 있습니다. 


예를 들면 카카오톡의 친구목록, 채팅목록 같은 화면은 프래그먼트이고 이 두 프래그먼트는 같은 Activity에 있는 것과 같아요. 맞나? 제가 카톡 개발자가 아니어서


Activity에 Fragment가 포함되는 것이기 때문에 Fragment간의 직접적인 통신은 막고 있습니다. 반드시 Activity를 통해서 Fragment간의 통신을 해야합니다.






프래그먼트도 Activity와 같이 여러 단계의 주기를 거치게 됩니다. 그 중 눈여겨 볼 메소드는 onCreateonCreateView,그리고 onPause랍니다.




onCreate()

안드로이드 시스템은 프래그먼트가 실행될때 이 메소드를 호출하게 됩니다. 컴포넌트를 초기화하고 싶다면 이곳에서 하세요. 매개변수인 Bundle 객체를 통해서 Activity에서 데이터를 전달받을 수도 있습니다.


onCreateView()

이 메소드는 UI를 생성하는 메소드입니다. UI를 제공하지 않는다면 null을 반한 할 거구요. 대개 전달받은 LayoutInflater로 inflate메소드를 통하여 View를 반환합니다.


프래그먼트는 Activity에서 FragmentManager 객체를 통해서 관리되어 집니다.  MainActivity에서 getSupportFragmentManager메소드를 통해 객체를 생성할 수 있습니다.




프래그먼트를 사용한 어플리케이션 예제

이제 프래그먼트를 사용해서 간단한 어플을 만들어볼텐데요, 우선 Empty Activity로 프로젝트를 구성해주세요.


두 개의 프래그먼트를 구성 할 두 개의 레이아웃 파일을 만들어 주겠습니다.


fragment1.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffee11">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Fragment1"
android:textSize="30dp"/>
</RelativeLayout>

fragment2.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#22ff33">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Fragment2"
android:textSize="30dp"/>
</RelativeLayout>


두개의 레이아웃 파일은 별거 없습니다. 그냥 현재 어떤 프래그먼트인가를 표시하는 텍스트뷰를 하나 포함하고 있고, 더 구별하기 쉽도록 배경색을 다르게 지정했습니다.


우리는 이 레이아웃 파일을 프래그먼트와 연결해야합니다. 바로 UI를 구성해야하는 것이죠. 

그 메소드가 onCreateView라는 메소드라고 했습니다.


Fragment1.java


import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class Fragment1 extends Fragment {

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment1,container,false);
}
}


Fragment2.java

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class Fragment2 extends Fragment {

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment2,container,false);
}

}


Fragment1 클래스는 fragment1 레이아웃을 UI로 삼고 있고, Fragment2 클래스는 fragment2 레이아웃을 UI로 가지고 있습니다.


이것을 View 객체로 가져오기 위해서는 LayoutInflater로 inflate메소드를 통해 가져올 수 있습니다. 


이제 여기까지 프래그먼트와 레이아웃간의 연결을 끝났습니다. 이제 프래그먼트를 포함할 activity_main.xml을 구성해야합니다.


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="프래그먼트1"
android:textSize="25dp"
android:onClick="changeFragment"
android:id="@+id/fragment1"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="프래그먼트2"
android:textSize="25dp"
android:onClick="changeFragment"
android:id="@+id/fragment2"/>

</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/container"
android:orientation="vertical"/>
</LinearLayout>

id가 container인 LinearLayout이 보이시나요? 여기가 프래그먼트를 포함하는 container 역할을 하게 됩니다.


각 버튼은 changeFragment 메소드를 통해서 프래그먼트를 바꾸는 역할을 하게 됩니다.




MainActivity.java

import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends AppCompatActivity {

FragmentManager fm;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Fragment fragment=new Fragment1();
fm=getSupportFragmentManager();
fm.beginTransaction().add(R.id.container,fragment).commit();
}

public void changeFragment(View view){
int id=view.getId();
Fragment fragment=null;
switch(id){
case R.id.fragment1:
fragment=new Fragment1();
break;
case R.id.fragment2:
fragment=new Fragment2();
break;
}

fm.beginTransaction().replace(R.id.container,fragment).commit();
}
}

Activity는 FragmentManager를 통해서 Fragment를 관리하게 됩니다. FragmentManager를 통해서 FragmentTransaction객체를 가져옵니다. 


FragmentTransaction객체는 프래그먼트를 추가, 교환, 제거할 수 있습니다. replace메소드로 container의 프래그먼트를 교체하고 있는 걸 확인할 수 있습니다. 마지막에는 반드시 commit메소드를 호출해야 replace가 반영됩니다.




결과


프래그먼트1을 버튼을 누른 경우(또는 앱이 실행된 첫 화면)




프래그먼트2를 버튼을 누른 경우




프래그먼트를 사용해서 어플리케이션을 만들어보았습니다. 처음에만 조금 헷갈리지 나중에는 익숙해져... 요... 

반응형
블로그 이미지

REAKWON

와나진짜

,

함수포인터

우리는 이제까지 어떤 변수를 가리키는 포인터, 배열을 가리키는 포인터를 사용해보았어요. 물론 이정도를 알아도 상당히 훌륭하다고 생각합니다.

그렇지만 변수뿐만 아니라 함수도 포인터로 가리킬 수 있다는 것을 알게 된다면 조금 더 간지나는 프로그래밍을 시도해 볼 수 있겠습니다. 이제부터 설명할 것이 무엇이냐, 그 이름하여 함수포인터랍니다. 

함수포인터라... 그냥 포인터도 극혐인데 말이죠

함수포인터란 함수를 가리킬 수 있는 포인터를 의미합니다. 아니, 근데 그러면 주소를 알아야하는데, 함수에도 주소가 있나? 네, 있습니다. 함수가 주소를 가지고 있다구요? 제말이 구라가 아님을 보여드리겠습니다.

#include <stdio.h>
int sum(int a, int b) { 
        return a + b;
} 

int main() { 
        printf("함수 sum의 주소 : %p\n", &sum);
}


이 코드의 결과는 어떻게 될까요? 우리는 변수명 앞에 &(amp)를 붙여 변수의 주소를 알게 됩니다. 그렇다면 &함수명은 무엇을 의미하게 될까요? 아무리 눈치없어도 진짜 이정도면 눈치채야지~

신기하네요. 16진수로 어떤 수가 나오네요. 눈치채셨겠지만 함수의 주소를 의미하게 됩니다. 바늘가는데 실간다는데 주소가 있으면 포인터가 있겠죠. 그렇다면 함수포인터를 선언하는 방법을 알아보도록 하겠습니다. 간단합니다.

①int ②(*ptrSum)③(int a,int b)

 

일반 포인터와 마찬가지로 주소를 가리킬때는 *을 사용해서 포인터라고 알려줍니다.

①은 함수의 반환형을 의미합니다.

②는 함수포인터의 이름을 의미합니다. (변수명과 같이 임의로 정해줍니다.)

③은 매개변수를 의미합니다. 매개변수가 없을 때는 빈 괄호나 void를 사용합니다.

 

네, 위 세가지만 지켜주면 됩니다.

그러니까 ptrSum이라는 함수포인터는 반환형이 int형이고 매개변수 2개를 갖는데, 둘 다 int형 매개변수인 함수포인터가 되겠습니다.

그렇다면 다음과 같은 함수포인터는 무엇을 의미할까요?

void (*ptrFunc)()

함수명이 ptrFunc인데, 반환값이 없고(void), 매개변수도 없는 함수포인터를 의미합니다. 이제 응용가능하시겠죠? 함수포인터에서 반환형과 매개변수가 일치하는 함수만 함수포인터에 할당이 가능합니다. 기억하세요.

함수포인터를 선언하는 방법은 알게 되었으니, 다음 코드를 통해서 함수포인터의 값이 바로 그 함수의 주소인지 확인도 해보고, 사용도 해봅시다.

#include <stdio.h>
int sum(int a, int b) { 
        return a + b;
}

int main() {
        int(*ptrSum)(int a,int b); 
        ptrSum = sum; 
        printf("sum의 주소: %p\n", &sum); //&sum은 sum과 같음 
        printf("ptrSum의 값: %p\n", ptrSum);
        printf("ptrSum의 주소: %p\n", &ptrSum);
        printf("ptrSum(%d, %d) = %d\n", 3, 4, ptrSum(3, 4));
}

 

우리는 sum이라는 함수의 주소가 0x00081023번지라는 것과 ptrSum이 0x00081023을 가리키는 것을 확인할 수 있습니다(실제 sum도 함수의 주소, &sum도 함수의 주소입니다).

그리고 그 함수포인터의 주소는 0x0026FB04번지네요.

함수포인터를 통해서 호출(ptrSum(3,4)) 확인해볼 수 있군요. 

(코드에서 나오지는 않았지만 함수포인터의 크기는 역시 4바이트입니다. 포인터이기 때문에요.)

조금 더 쉽게 그림을 통해서 보도록할게요.

 

포인터를 설명할때 아주 많이 사용하는 그림이죠? 지겹죠?

0x0026FB04번지에 있는 ptrSum이라는 함수포인터는 0x00081023번지에 있는 함수 sum의 시작주소를 가리키게 됩니다.

sum은 함수이기 때문에 시작주소를 갖고 있고, 그 주소를 기점으로 매개변수를 할당, 그리고 함수 내부의 변수들을 스택에 따라 쌓아올립니다. 그러니까 함수의 시작주소를 통해 변수의 주소를 알게 됩니다. 함수의 시작주소는 중요하단 거죠. (더 자세하게 함수호출과정을 아는 것도 도움이 됩니다. 구글형님께 물어보세요.)

그런 sum의 시작주소를 ptrSum은 알고 있기 때문에 sum함수를 호출할 수 있는 겁니다.

아니, 그렇다면 sum함수만 호출하면 되지, 왜 굳이 포인터를 써서 호출하는 거야?

저도 무척이나 이런 생각을 하고 하고 또 하면서 그냥 함수포인터를 흘려 넘겼었죠.

근데 쓰임새는 꽤나 많습니다. 예를 들면 함수 자체를 매개변수로 받고 싶을때가 있을 겁니다.

프로젝트를 진행할때 한 사람만이 진행한다면 물론 상관이 없지만, 여러 사람과 협업을 해야하거나, 라이브러리를 제공할때, 함수에 함수자체를 매개변수로 받아야할 때가 있습니다. 누가 어떤 함수를 필요로 할지 모르니, 어떤 형식으로 함수를 정의해서 매개변수로 전달하게 되면 그 함수를 내부에서는 호출하게 되는 식으로 말이죠.

void func(void (*ptr)()) {
...      ptr();       ... 
}

그리고 또 구조체에서 멤버함수를 커스터마이징할때도 사용할 수가 있습니다. 

struct animal{     
...       void (*walk)();       ... 
}

 

위에서 walk라는 멤버함수를 우리가 원하는 대로 기능을 정의해줄 수 있습니다. 물론, 함수포인터의 형식과 맞는다면 말이죠. 이런 방식은 자바에서 interface라고 하여 메소드를 구현하는 방식과 비슷해보입니다. 함수포인터를 더 연습해보고 개념을 확실히 이해하세요.

이것으로 함수포인터에 대해 알아보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

1912번 연속합



문제 해석


수열에서 연속된 수들의 합이 가장 큰 것을 구하는 문제입니다.


10, -4, 3, 1, 5, 6, -35, 12, 21, -1 라는 수열에서 가장 큰 연속은 얼마일까요?

12, 21 부분이 가장 큰 연속합입니다.


연속합의 수들은 한개 이상이라는 점입니다.


제약 조건
100,000개의 수들이 주어지고 각각의 수는 -1000~1000의 크기를 갖습니다.




풀이

쉬워보입니다.


처음 시작점을 고정하고 끝점을 하나씩 늘려가면서 시작점과 끝점을 전부 더하는 거죠. 전부 현재의 최대값과 비교해서 크면 그게 최대값이 되는 식으로요.


#include <cstdio>
#include <algorithm>

using namespace std;

int main() {
	int n;
	int nums[100001];

	scanf("%d", &n);
	for (int i = 0; i < n; i++)
		scanf("%d", &nums[i]);

	int ans = -1001;
	for (int start = 0; start < n; start++) {
		for (int end = start; end < n; end++) {
			int sum = 0;
			for (int i = start; i <= end; i++)
				sum += nums[i];
			ans = max(sum, ans);
		}
	}
	printf("%d\n", ans);
}
	

네, 이렇게 제출하면 시간초과랍니다. 하하

입력이 자그마치 10만개가 되거든요.


단순히 이렇게 for루프만 돈다면 문제를 해결할 수가 없습니다.


여기서 간단한 이론(?)이 있는데요.

부분합이라는 개념이 여기 사용됩니다.


우리는 전에 계산한 합을 이용할 수 있다는 거죠.



[0] 

[1] 

[2] 

[3] 

[4]

[5] 

 [6]

[7] 

[8] 

[9] 

10

-4 

3 

1 

5 

6 

-35 

12 

21 

-1 

10

6 

9 

10 

15 

21 

-14 

-2 

19 

18 



[0]~[5]까지의 합은 [0]~[4]까지의 합 + 현재의 수를 더하면 된다는 것을 알 수 있습니다.


코드로 본다면 이런 형태이죠.

for (int i = 1; i <= n; i++)
		partialSum[i] = partialSum[i - 1] + numbers[i];

i가 1부터 시작한 이유는 조금 더 보기 편하게 하기 위함입니다. 그러니까 n까지 for루프를 돌아야 됩니다.


그렇다면 [7]~[8]까지의 합만을 구하려면 어떻게 할까요?

partialSum[8]-partialSum[6]을 하게 되면 [7]과 [8]의 부분합만을 구할 수 있습니다.




그럼 1부터 3까지의 합은 partialSum[3]-partialSum[0]이 되겠죠? 인덱스를 왜 1부터 시작하는 지 이유를 알 것 같습니다. 그렇지 않으면 if문을 사용해야합니다.


이제 우리는 위의 코드를 2개의 for문으로 줄일 수 있습니다.



#include <cstdio>
#include <algorithm>

using namespace std;

int main() {
	int n;
	int partialSum[100001];

	scanf("%d", &n);
	for (int i = 1; i <= n; i++) {
		int number;
		scanf("%d", &number);
		partialSum[i] = partialSum[i - 1] + number;
	}
	int ans = -1001;
	for (int start = 0; start <= n-1; start++) {
		for (int end = start + 1; end <= n; end++) {
			ans = max(ans, partialSum[end] - partialSum[start]);
		}
	}
	printf("%d\n", ans);
}
	



부분합을 이용해서 이중 for문으로 문제에 접근할 수도 있지만, 이 역시 시간초과가 됩니다. 아까 말한것 처럼 입력이 10만개이기 때문에 이중 for문 역시 시간초과죠.


여기서 한 번 더 줄일 수 없을까요?

부분합을 이용해서 말이죠. 


그전까지 계산한 부분합 + 지금 숫자가 지금 숫자보다 작다면 부분합은 다시 계산 되어야 한다는 것을 직관적으로 느낄 수 있을까요??


그러니까...


현재의 수를 numbers[i]라고 하고 그 전까지 계산했던 부분합이 partialSum[i-1]라고 할때 , partialSum[i]=max(partialSum[i-1]+numbers[i], numbers[i]) 라고 하는 것 말이에요.


다시말해,


partialSum[i]=max(partialSum[i-1]+numers[i], numbers[i])

                =max(지금까지의 부분합, 현재 수)

라고 하는 것이 이해가 가시나요?


지금까지 구한 부분합이 현재의 수보다 작다면 현재의 수부터 다시 부분합을 계산하는 것이 더 클 거니까요.


이것만 이해가 간다면, 다음의 코드는 이 과정을 배열없이 구현했다라는 것을 알 것입니다.




#include <cstdio> #include <algorithm> using namespace std; int main() { int n, partialSum = 0, maxPartialSum = -1001; scanf("%d", &n); for (int i = 1; i <= n; i++) { int number; scanf("%d", &number ); partialSum = max(number, number + partialSum); maxPartialSum = max(partialSum, maxPartialSum); } printf("%d\n", maxPartialSum); }

코드가 무척이나 짧아졌습니다.

문제의 답은 maxPartialSum입니다. 


코드에서 이 부분을 보세요.


partialSum = max(number, number + partialSum);


number는 현재의 수를 말하는 것이고, partialSum은 이전까지 계산했던 부분합을 의미합니다.

현재의 수와 이전까지 계산했던 부분합 + 현재의 수가 현재의 수보다 작다면 부분합은 다시 현재의 수부터 시작입니다.


이후 이 부분합(partialSum)과 지금까지 부분합의 최대값(maxPartialSum)을 비교해서 큰 값이 다시

maxPartialSum이 되는 것입니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

2178번 미로탐색



문제 해석



추석인데 할게 없으니, 알고리즘 하나 풀어보자구요!

NxM행렬이 있습니다. N은 행의 수, M은 열의 수인데, 1은 통과 가능한 구역, 0은 통과하지 못하는 공간이라고 문제 설명에서 말합니다.

(1,1)부터 시작해 (N,M)까지 오는데, 가장 적게 1을 통과하는 수를 구하는 게 문제의 답입니다.


아래처럼 빨간 색으로 된 1을 통과하는 게 답이 되겠고, 15가 답이 됩니다.

1 

0

 1

 1

 1

 1

1

0

 1

 0

 1

 0

1

0

 1

 0

 1

 1

1

1

 1

 0

 1

 1



물론 답은 여러가지가 있을 수도 있겠죠?

위에 빨간점대로 이동하지 않고도 녹색점을 거쳐서 오는 경우도 가능한데, 어차피 답은 똑같다라는 점입니다.

한칸 띄어서 정수형으로 주지 귀찮게




제약 조건
N과 M 둘 다 2이상 100이하가 되겠습니다. 


풀이
음...
최소라... 

일단 어떤 지도같은 것을 주고, "최단거리를 구하라" 라는 문제는 BFS로 풀어볼만 합니다. BFS가 최단 거리를 구하는데 안성맞춤이거든요. 어떤 정점이 있을때 그 주위에 있는 정점들만 우선적으로 돌기 때문이지요.

그리고 N과 M, 모든 점을 계산했을 때 10000개 이하가 되니까 Queue에 담기도 충분합니다.


우선 어떤 점 (y,x)가 있을때, 사방에 1인 녀석들을 일단 찾습니다. 그놈들이 그래프의 정점 중 다음에 방문해야할 후보들에 속하거든요.
그렇다면 상하좌우이니까 (y, x-1), (y, x+1), (y+1, x), (y-1, x)로 주변 정점들을 찾고 이미 방문된 정점이라면 그냥 skip하면 되겠네요.




#include <cstdio>
#include <queue>
#include <string>
#include <string.h>

using namespace std;

int n, m;
bool visited[101][101];
char map[101][101];

struct Point {
	int x, y;
	Point(int _y, int _x) {
		x = _x; y = _y;
	}
};

queue<Point> q;

void pushQ(int y, int x) {

	if (y >= n || x >= m || y<0 || x<0 || visited[y][x] || map[y][x] == '0') return;
	q.push(Point(y, x));

	visited[y][x] = true;
}

int bfs() {

	pushQ(0, 0);
	int count = 1;
	while (!q.empty()) {

		int size = q.size();

		for (int i = 0; i<size; i++) {
			Point here = q.front();
			q.pop();

			if (here.y == n - 1 && here.x == m - 1) return count;

			pushQ(here.y + 1, here.x);
			pushQ(here.y - 1, here.x);
			pushQ(here.y, here.x + 1);
			pushQ(here.y, here.x - 1);
		}
		count++;
	}
	return count;
}

int main() {
	scanf("%d %d", &n, &m);

	memset(visited, false, sizeof(visited));
	for (int i = 0; i < n; i++)
		scanf("%s", map[i]);

	printf("%d\n", bfs());
	return 0;
}


위 코드는 단순하게 (0,0) (문제에서는 1,1라고 했지만, 배열은 0,0부터 첫번째 요소이기 때문에)부터 시작해서 (n-1, m-1)( 역시 배열의 가장 마지막 요소는 n-1, m-1)까지 문자열로 입력을 받고,  bfs를 돕니다.


bfs에서는 우선 시작 정점(0,0)을 큐에 집어 넣습니다. 그 후에 bfs의 size를 호출해 포문을 돌고 그 상하좌우의 정점을 집어 넣게 되는 겁니다. 주의해야할 점은 for루프 조건문에서 직접적으로 q.size()를 호출하면 안된다는 점입니다. for루프 안에서 q에 push되므로 for문을 한번 돌때마다 q.size()가 유동적으로 바뀌게 됩니다.

                int size = q.size();

		for (int i = 0; i<size; i++) {
			Point here = q.front();
			q.pop();

			if (here.y == n - 1 && here.x == m - 1) return count;

			pushQ(here.y + 1, here.x);
			pushQ(here.y - 1, here.x);
			pushQ(here.y, here.x + 1);
			pushQ(here.y, here.x - 1);
		}
		count++;

그렇게 for문을 한 번 돌면 그 주위에 있는 정점 한번을 탐색한 것이니까 count를 하나 증가시켜 주고요. 만약 n-1, m-1 정점까지 도달했다면 바로 이 count를 return하게 되면 몇 번만에 (n-1, m-1)까지 도달했는지 알 수 있습니다.




pushQ함수는 조건에 맞지 않는 정점은 그냥 지나쳐 버립니다.

조건에 맞지 않는 정점이라는 것은 범위밖의 정점( 0보다 작거나 n또는 m보다 큰 정점), 이미 방문된 정점, 또는 0인 정점을 말하는 것이죠.

방문해야 할 정점이라면 큐에 push하고 방문되었다는 표시만 해주는 아주 단순한 함수입니다. visited[y][x] = true가 바로 방문되었다는 표시를 해주는 작업이랍니다.


단순하긴 하지만 이 함수가 없다면 for루프 안에 일일히 if문으로 검사를 해주어야하는 뻘짓을 하게 될겁니다.

void pushQ(int y, int x) {

	if (y >= n || x >= m || y<0 || x<0 || visited[y][x] || map[y][x] == '0') return;
	q.push(Point(y, x));

	visited[y][x] = true;
}

그리고 또 한가지는 pushQ를 총 4번 호출하죠?

for문으로 돌릴 수 있으나, 거기까지 생각하면서 귀찮게 코딩 안하잖아요 우리는...

그냥 짧은거는 복붙합시다... 티도 안나요.


점(point)을 조금 편하게 관리하게 위해서 Point라는 구조체를 사용했습니다.

뭐... 사용하지 않고 그냥 배열로만 해도 상관없구요.


십쥬?

반응형
블로그 이미지

REAKWON

와나진짜

,

문자열과 char 포인터

오늘은 심심한데 문자열에 대해서 이야기 해볼까 해요~ 문자열과 포인터는 C언어에서 너무나 귀찮은 놈들인데,,, 그래도 꼭 쓰이니까요. char 자료형은 문자를 변수로 갖는 건 모두 아는 사실이죠?

근데~ 우리는 문자열을 쉽게 할당하고 싶단 말이에요. 우리는 배열이라는 아주 편리한 변수 선언 법을 알고 있답니다.

배열로 문자열을 표현하는 방법을 알아보겠습니다.

char hello[6] = { 'h','e','l','l','o','\0' };

이 표현은 char이 자료형을 배열로 문자열을 표현한 방법이랍니다. '\0' 라는 문자는 NULL문자라는 뜻입니다. 문자열의 끝을 알려줍니다.

그래서 hello가 다섯글자임에도 불구하고 배열 크기를 6으로 잡은 겁니다.

또 이런 선언법도 가능합니다.

char hello[6] = "hello";

그렇다면 우리가 문자열의 길이를 알고 싶다면 널문자가 나타나기 전까지만 세어주면 문자열의 길이를 알 수 있겠네요.

#include <stdio.h>

int main() {
        char *ptr = "ABCDEF";
        int len = -1;
       
        while (*(ptr+(++len)));
        printf("문자열 길이: %d\n", len);
}

그 결과는 이렇겠네요.

 

len=-1인 이유는 null문자 이전까지만 세어주기 위함입니다. while의 조건절은 null이면 멈추어 버립니다. 여기까지는 쉽네요. 포인터로는 어떻게 표현할까요?

사실 문자열("~~~~")은 그 문자열이 시작되는 주소를 가리키게 됩니다. 주소를 가리킨다!?  그러면 포인터가 생각나지 않나요?

왜냐면 주소를 포인터로 가리키면 문자열을 찾을 수 있으니까요.

그러면 이렇게 선언할 수 있을까요?

char *ptr = "hello";

포인터 ptr은 "hello"라는 문자열을 가리키는 포인터입니다. 

그림에서 보는 것과 같이 ptr은 문자열 "hello"의 주소를 가리키고 있고, 그렇기 때문에 참조가 가능한 상태가 됩니다. 그렇다면 어떤 포인터 역시 hello를 가리킨다면 그 주소는 같을까요?

코드와 결과로 확인해보도록 합시다.

 

#include <stdio.h>  int main() {   	char *ptr1 = "hello"; 	char *ptr2 = "hello"; 	printf("%s, %s\n", ptr1, ptr2); 	printf("%p, %p\n", ptr1, ptr2); } #include <stdio.h> 

int main() {   
        char *ptr1 = "hello"; 
        char *ptr2 = "hello"; 

        printf("%s, %s\n", ptr1, ptr2);
        printf("%p, %p\n", ptr1, ptr2);
}

 

같다는 것을 알 수 있습니다. 우리는 이런 그림을 그려볼 수 있겠네요.

ptr1과 ptr2는 서로 같은 문자열을 가리킵니다.  배열과 포인터에 대해서 선언방법은 그렇게 차이가 없어보이죠?

그렇다면 문자열 배열과 포인터는 서로 같은 성질을 갖고 있을까요?

만약 아래와 같은 코드를 입력한다 arr에 ptr가 가리키는 문자열을 넣으라는 거겠죠??

char arr[10] = "world";

char *ptr = "hello";

arr = ptr;

 "hello"라는 문자열의 길이는 배열크기보다 작기 때문에 들어갈 것입니다. 이렇게 생각하셨다면 다시 생각해봅시다. 오류나니까요.

문자열 "hello" 그 주소 자체를 반환합니다. 그러니까 "hello"의 시작주소가 되는 것이죠. 그것이 ptr이 갖고 있는 값입니다.

arr자체는 arr[0]의 주소, 즉 배열의 시작 위치를 말합니다. 이러한 시작 주소를 마음대로 ptr이 가리키고 있는 주소로 바꿀 수 없습니다.

이 의미는 더 쉽게 풀어서 이야기하면

int a = 0;

int b = 30;

&a = b;

랑 유사한 짓거리를 하는 것이라는 거죠. 마치 a의 주소를 b의 값으로 변경하라는 것과 유사하게 되어버립니다.

하지만 그 반대는 가능합니다. 이렇게요.

char arr[10] = "world" ;

char *ptr = "hello";

ptr = arr;

ptr은 주소를 갖을 수 있는 포인터, arr은 arr[0]의 주소! 말이 되죠 이건??

그러니 ptr은 arr과 동일한 곳을 가리키게 되는 겁니다.

만약

arr = ptr;

이걸 죽어도 써야겠다. 난 arr에다가 ptr의 문자열을 진짜 안쓰면 디질거 같다.  하시는 분들은 ptr의 문자열을 복사해서 쓰는 방법밖에 없습니다.

strcpy(arr, ptr)

이렇게 하시면 arr에 ptr이 가리키는 문자열을 그대로 복사해서 arr에 쑤셔 넣습니다. 주의 할 사항은 arr의 크기는 ptr이 가리키고 있는 문자열의 길이 이상으로 커야한다는 겁니다.

그렇지 않으면 런타임 오류납니다. 컴파일에서 문자열의 길이를 검사하지 않습니다!

이 오류가 바로 그 유명한 버퍼오버플로우(buffer overflow)가 됩니다. 취약점인거죠. 버퍼오버플로우를 통해 해커는 함수의 return 주소를 변경하여 자신의 실행코드를 실행합니다. 별짓을 다할 수가 있게 됩니다그래서 그 대안으로 strncpy, strncat 이런것이 나오게 된겁니다.

그리고 또!

포인터와 배열에는 다른 차이점이 있습니다.

문자열을 초기화 할때 배열은 배열 원소를 변경할 수 있지만, 포인터는 배열의 원소를 바꿀 수 없습니다. 즉, 포인터로 초기화 한다면 상수적인 성격을 띈다라는 것입니다.

가령, 아래 코드가 있다면 ptr[0] 변경시 오류가 발생합니다.

char hello[10] = "hello";

char *ptr = "hello";

ptr[0] = 'H';  //오류

hello[0] = 'H';

하지만

char hello[10] = "hello";

char *ptr = hello; 

ptr[0] = 'H'; 

hello[0] = 'H';

이건 오류가 나지 않습니다. 왜냐면 hello는 배열이거든요. 배열은 그 원소의 값이 변경가능합니다. ptr은 배열의 시작주소를 참조하고 있는 포인터이기 때문입니다.

 

그래서 이러한 strcat를 써먹을때도

#include <stdio.h>
#include <string.h>  

int main() {
        char *hello = "hello, ";  
        strcat(hello, "world");
        printf("%s\n", hello);
}

가 아닌

#include <stdio.h>
#include <string.h>

int main() { 
        char hello[20] = "hello, ";  
        strcat(hello, "world");
        printf("%s\n", hello);
}

 

이런 형태나

#include <stdio.h>
#include <string.h>  

int main() {  
        char hello[20] = "hello, ";
        char *ptr = hello;  
        strcat(ptr, "world");  
        printf("%s\n", ptr);
}

이런식으로 쓰여야 한다는 겁니다. 이렇게 간단하게 문자열과 포인터에 대해서 알아보았습니다. 부족한 점은 나중에 또 보충 설명해보도록 할게요 ㅎㅎ

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,