거북이와 개구리의 끄적끄적

4일차_C언어(시프트연산자와 비트연산자, 지역변수와 전역변수, static과 const, 배열, 문자열) 본문

공부/팁스

4일차_C언어(시프트연산자와 비트연산자, 지역변수와 전역변수, static과 const, 배열, 문자열)

거북이가개굴개굴 2018. 7. 19. 00:21

4일차_C언어

이 글은 Tips 4일차 강의를 듣고 정리한 내용입니다.

잘못된 내용, 보충할 내용, 궁금한 내용 있으시다면 댓글 달아주시면 감사하겠습니다.


오늘의 목차는 아래와 같습니다.

-시프트연산자와 비트연산자

-지역변수와 전역변수

-static과 const키워드

-배열

-문자열



1.시프트연산자와 비트연산자

윈도우즈나 리눅스같은 OS는 메모리를 1바이트 단위로 관리 함.

C언어에서는 메모리 할당의 최소 단위는 1바이트(비트단위의 할당은 불가능)

그러나 바이트단위로 메모리를 할당하고 할당된 메모리를 비트단위로 관리 가능.

비트단위의 관리가 왜 필요한지 생각해보자.


첫번째, 메모리 효율이 좋다.

1600개의 좌석이 있는 도서관의 좌석 관리 시스템을 만든다고 해보자.

이 때, 바이트단위로 관리하는 경우와 비트단위로 관리하는 경우를 비교해보자.

좌석에 사람이 있는 경우를 1, 없는 경우를 0이라 하면

바이트 단위로 관리할 경우 1좌석에 1바이트이므로 1600좌석의 경우 1600바이트를 할당해야한다.

반면에 비트 단위로 관리하면 1좌석에 1비트이므로 8좌석에 1바이트, 1600좌석에 200바이트만 할당하면 된다.

메모리 관리에서 8배의 차이를 볼 수 있으ㅡㅁ로 비트단위의 관리가 왜 필요한지 충분한 이유라고 생각된다.


두번째, 속도가 빠르다.

시프트 연산자의 경우 시프트 연산을 담당하는 하드웨어 자체의 성능이 더 좋기때문이다.


그럼 비트단위의 관리는 가능하게 해주는 시프트연산자와 비트연산자에 대해 알아보자.

<시프트연산자>

시프트 연산자의 종류입니다.

연산자 기호

정의 

a << b

a에 저장된 값을 왼쪽으로 b비트만큼 shift 

a >> b 

a에 저장된 값을 오른쪽으로 b비트만큼 shift 

시프트 연산의 결과로 비어있는 부분은 채워지고 넘치는부분은 손실이 생깁니다.

비어있는 부분이 채워질 때 LSB가 채워지는 경우와 MSB가 채워지는 경우가 있습니다.

LSB일 경우 항상 0으로 채워지며, MSB일 경우 자료형에 부호가 없다면 0으로 부호가 있다면 기존의 MSB값과 동일하게 채워집니다.

넘치는 부분은 손실이 생기는데 MSB쪽이 자료형을 넘어 갈 경우 overflow, LSB쪽이 자료형을 넘어 갈 경우 underflow라고 합니다.


실제 예시를 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
 
int main(){
    unsigned char data;
 
    /* << 예제 */
    data = 0x55;// 0101 0101 (85);
    data <<= 1// 1010 1010 (170)  ---  (1)
    data <<= 2// 1010 1000 (168)  ---  (2) overflow 발생!
    data <<= 3// 0100 0000 (64)   ---  (3) overflow 발생!
 
    /* >> 예제 */
    data = 0xAA;// 1010 1010 (170);
    data >>= 1// 0101 0101 (85)   ---  (4)
    data >>= 2// 0001 0101 (21)   ---  (5) underflow 발생!
    data >>= 3// 0000 0010 (2)    ---  (6) underflow 발생!
 
    return 0;
}
cs


(1)의 경우 0101 0101을 왼쪽으로 1비트 이동 후 data에 저장하므로 그 값은 1010 1010이 됩니다.

(2)의 경우 1010 1010을 왼쪽으로 2비트 이동 후 data에 저장하므로 그 값은 1010 1000이 됩니다.

(3)의 경우 1010 1000을 왼쪽으로 3비트 이동 후 data에 저장하므로 그 값은 0100 0000이 됩니다.

(4)의 경우 1010 1010을 오른쪽으로 1비트 이동 후 data에 저장하므로 그 값은 0101 0101이 됩니다.

(5)의 경우 0101 0101을 오른쪽으로 2비트 이동 후 data에 저장하므로 그 값은 0001 0101이 됩니다.

(6)의 경우 0001 0101을 오른쪽으로 3비트 이동 후 data에 저장하므로 그 값은 0000 0010이 됩니다.


(2),(3)에서 overflow가 발생하는 과정입니다.

(5),(6)에서 underflow가 발생하는 과정입니다.

시프트 연산자를 사용하는 방법은 2가지가 있습니다.


첫번째, 비트적 의미

특정 비트 수 만큼 왼쪽 혹은 오른쪽으로 밀 때 사용합니다.

이를 응용하여 특정 비트만 보고싶은 경우 시프트연산자를 이용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
 
int main(){
    unsigned char data;
 
    /* 비트적 의미 예제 */
    /* data에서 3번 비트만 볼 때 */
    data = 0x55;// 0101 0101 (85);
    data <<= 4; // 0101 0000
    data >>= 7; // 0000 0000
    printf("3번 비트 : %d\n", data); // 0 출력
 
    /* 비트적 의미 예제 */
    /* data에서 6번 비트만 볼 때 */
    data = 0x55;// 0101 0101 (85);
    data <<= 1; // 1010 0000
    data >>= 7; // 0000 0001
    printf("6번 비트 : %d\n", data); // 1 출력
 
    return 0;
}
cs


두번째, 산술적 의미

data의 특정 값을 곱셈 혹은 나누셈을 하고 싶은 경우 시프트연산자를 이용할 수 있습니다.


1.곱연산 대신 시프트연산 사용하기 - 2의 n승의 곱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

#include <stdio.h>

// bottom : 밑
// idx : 지수
// IntegerPow(3,5)는 3의 5승
int IntegerPow(int bottom, int idx){
    while(idx) {
        bottom *= 2;
        idx--;
    }
    return bottom;
}
int main(){
    unsigned char data = 0x09// 0000 1001 (9)
    /* 산술적 의미 */
    /* 2의 n승의 형태의 곱 */
    /* 'a * 2^n' 은 'a << n'과 같다. */
    for(int i=1; i<=3; i++){
        if( (data<<i) == IntegerPow(data,i) )
            printf("결과는 %d로 같다\n", data<<i); // 18,36,72 순으로 출력
    }
}
 
cs


1.곱연산 대신 시프트연산 사용하기 - 임의의 정수의 곱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
 
int main(){
    unsigned char data = 0x09// 0000 1001 (9)
    /* 산술적 의미 */
    /* 임의의 정수의 곱 */
    /* 임의의 정수를 2의 n승의 합의 형태로 표현하면 됨. */
    int result1, result2;
    result1 = data * 10// 10 = 2^3 + 2^1
    result2 = (data<<3+ (data<<1); // 2^3은 <<3, 2^1은 <<1
    if( result1 == result2 )
        printf("결과는 %d로 같다\n", result1); // 90 출력
    return 0;
}
 
cs


2.나눗셈연산 대신 시프트 연산 사용하기 - 부호가 없는 자료형

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
 
int main(){
    int result1, result2;
    unsigned char data = 0x99// 1001 1001 (153)
 
    /* 산술적 의미 */
    /* 2의 n승의 형태의 나눗셈 */
    /* 'a / 2^n' 은 'a >> n'과 같다. */
 
    result1 = data / 4;
    result2 = data >> 2;
    if(result1 == result2)
        printf("결과는 %d로 같다.\n",result1); // 38 출력
 
    result1 = data / 16;
    result2 = data >> 4;
    if(result1 == result2)
        printf("결과는 %d로 같다.\n",result1); // 9 출력
 
    return 0;
}
 
cs

2.나눗셈 연산 대신 시프트 연산 사용하기 - 부호가 있는 자료형

부호가 있는 자료형의 경우 MSB의 비트에 따라 예외처리가 필요하게 됩니다.

따라서 나눗셈연산자에 비해 시프트연산자가 얻는 이득은 거의 없으므로 그냥 나눗셈 연산자를 사용하는것을 권장합니다.


그러나 시프트 연산자가 어려운 이유에는 3가지 주의사항이 있습니다.


첫번째, 오버플로우 혹은 언더플로우

위에서 언급했듯이 자료형의 크기를 넘어가게 될 경우 오버플로우 혹은 언더플로우가  발생할 수 있습니다.


두번째, 자료형의 부호 여부

자료형에 부호 여부에 따라 MSB에 채워질 수 있는 비트가 달라지므로 주의해야 합니다.


세번째, 낮은 우선순위

연산자 우선순위가 낮기때문에 아래와 같은 상황이 발생할 수 있습니다.

1
2
3
result2 = data<<3 + data<<1 // 버그, (data << (3+data)) << 1
result2 = (data<<3+ (data<<1// 정상작동
 
cs

<비트연산자>

비트연산자는 같은 바이트의 크기를 가진 데이터끼리 비트단위로 특정 연산을 하는것입니다.

비트연산자의 종류는 4가지가 있습니다.

연산자 기호

정의 

a & b 

a,b의 각 비트끼리 AND연산을 한다. 

a | b

a,b의 각 비트끼리 OR연산을 한다. 

a ^ b 

a,b의 각 비트끼리 XOR연산을 한다. 

~a 

a의 각 비트에 NOT연산을 한다. 


비트연산자별 특성을 이용하면 비트단위로 접근하여 값 변경이 가능합니다.

1.&연산은 둘 중 하나라도 0이면 결과가 0이 되는 특성이 있습니다.
이 특성을 이용하면 특정 비트를 0으로 변경할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main(){
    unsigned char data = 0xFF// 1111 1111
    unsigned char mask = 0x01// 0000 0001
 
    data & mask; // 0번 비트를 제외하고 전부 0으로 변경
                 // 결과는 0000 0001 
 
    mask = 0x35// 0011 0101
    data & mask; // 0,2,4,5번 비트를 제외하고 전부 0으로 변경
                 // 결과는 0011 0101
 
    return 0;
}
 
cs

2.|연산은 둘 중 하나라도 1이면 결과가 1이 되는 특성이 있습니다.
이 특성을 이용하면 특정 비트를 1로 변경할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main(){
    unsigned char data = 0x00// 0000 0000
    unsigned char mask = 0x01// 0000 0001
 
    data & mask; // 0번 비트만 1으로 변경
                 // 결과는 0000 0001 
 
    mask = 0x35// 0011 0101
    data & mask; // 0,2,4,5번 비트만 1으로 변경
                 // 결과는 0011 0101
 
    return 0;
}
cs

3.^연산은 두개가 같으면 0, 다르면 1인 특성이 있습니다.
이 특성을 이용하면 초기화, not연산 구현, 암호화 및 복호화가 가능합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 간단한 암호화 및 복호화 예제 */
#include <stdio.h>
#define DATA_SIZE 15
int main(){
    char data[DATA_SIZE] = "Hello Tips!!!!";
    char key[DATA_SIZE] = "I love guys :)";
 
    printf("before encryption : %s\n", data);
 
    for(int i=0;i<15;i++)
        data[i] ^= key[i];
    printf("encryption : %s\n", data);
 
    for(int i=0;i<15;i++)
        data[i] ^= key[i];
    printf("decryption : %s\n", data);
    
}
cs



또한 콘솔화면 뿐만 아니라 배열과 파일입출력을 응용하여 파일을 읽어 암호화 및 복호화도 가능합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 1.generate key */
// 난수를 이용해 8바이트의 key.txt파일을 생성한다.
#include <stdio.h> // fopen(),fwrite()
#include <stdlib.h> // srand(),rand()
#include <time.h> // time seed
 
#define KEY_SIZE (8// 8 byte
 
int main() {
    FILE* key_fp = fopen("key.txt""w+"); // key.txt라는 파일을 만들고 그 스트림을 key_fp에 저장
    char key_buf[KEY_SIZE];
    srand((unsigned int)time(NULL));
 
    for (int i = 0; i < KEY_SIZE; i++)
        *(key_buf + i) = (rand() % 128); // 0~127사이의 임의의 난수 생성 후 배열에 저장
    fwrite(key_buf, 1, KEY_SIZE, key_fp); // 배열에 저장된 값들을 파일에 write
 
    fclose(key_fp); // key_fp에 저장된 파일스트림을 닫음.
 
    return 0;
}
cs


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* 2.encryption */
// key.txt와 source.txt encryption.txt를 open한다(파일스트림 생성)
// source.txt를 BLK_SIZE만큼 READ하고 key.txt의 값과 XOR연산한다.(암호화)
// 암호화 된 내용을 enryption.txt에 write한다.
#include <stdio.h>
 
#define KEY_SIZE 8 // 8 byte
#define BLK_SIZE 20 // 20 byte
 
// buf : srouce.txt를 BLK_SIZE만큼 읽어들인 후 저장된 버퍼.
// key_buf : key.txt의 KEY_SIZE만큼 읽어들인 후 저장된 버퍼.
// len : src_buf의 길이
void Code(char* src_buf, char* key_buf, int len) {
    int i = 0, j = 0;
    while (i < len) {
        src_buf[i] ^= key_buf[j];
        i++;
        j = (++j) % KEY_SIZE;
    }
}
int main() {
     FILE* key_fp = fopen("key.txt""r+");
     FILE* src_fp = fopen("source.txt""r+");
     FILE* dst_fp = fopen("encryption.txt""a+");
     char src_buf[BLK_SIZE], key_buf[KEY_SIZE];
     int len;
 
    while (len = fread(src_buf, 1, BLK_SIZE, src_fp)) {
        fread(key_buf, 1, KEY_SIZE, key_fp);
        Code(src_buf, key_buf, len);
        fwrite(src_buf, 1, len, dst_fp);
    }
 
     return 0;
}
cs


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* 3.decryption */
// key.txt와 encryption.txt decryption.txt를 open한다(파일스트림 생성)
// encryption.txt를 BLK_SIZE만큼 READ하고 key.txt의 값과 XOR연산한다.(복호화)
// 복호화 된 내용을 decryption.txt에 write한다.
#include <stdio.h>
 
#define KEY_SIZE 8 // 8 byte
#define BLK_SIZE 20 // 20 byte
 
// buf : srouce.txt를 BLK_SIZE만큼 읽어들인 후 저장된 버퍼.
// key_buf : key.txt의 KEY_SIZE만큼 읽어들인 후 저장된 버퍼.
// len : src_buf의 길이
void Code(char* src_buf, char* key_buf, int len) {
    int i = 0, j = 0;
    while (i < len) {
        src_buf[i] ^= key_buf[j];
        i++;
        j = (++j) % KEY_SIZE;
    }
}
int main() {
    FILE* key_fp = fopen("key.txt""r+");
    FILE* src_fp = fopen("encryption.txt""a+");
    FILE* dst_fp = fopen("decryption.txt""a+");
    char src_buf[BLK_SIZE], key_buf[KEY_SIZE];
    int len;
 
    while (len = fread(src_buf, 1, BLK_SIZE, src_fp)) {
        fread(key_buf, 1, KEY_SIZE, key_fp);
        Code(src_buf, key_buf, len);
        fwrite(src_buf, 1, len, dst_fp);
    }
 
    return 0;
}
cs

실행 결과입니다.


4.~연산은 모든 비트가 반전되는 특성이 있습니다.
이 특성을 이용하면 보수를 구할 수 있고, 보수를 이용해 덧셈으로 뺄셈을 구현할 수 있습니다.
먼저 unsigned char의 범위는 0(0000 0000) ~ 255(1111 1111)입니다.
255에서 1을 더하면 1 0000 0000이지만 이는 오버플로우가 생기므로 결국 0이됩니다.
즉, 255 + 1은 0이 되며 이는 255 - 255와 같은 결과입니다.
255 + 2는 1이며 이는 255 - 254와 같은 결과입니다.
255 + 3은 3이며 이는 255 - 253과 같은 결과입니다.
위의 예시들을 통해 덧셈과 뺄셈이 같은 결과가 나옵니다.
이 말은 우리가 뺄셈을 하고싶다면 특정 수를 더해도 된다는 뜻이며, 이 특정 수를 보수라고 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
 
int main(){
    char A = 0x86// 1000 0110
    char B = 0x1d// 0001 1101
    
    /* 1의 보수 */
    ~A; // 0111 1001
    ~B; // 1110 0010
 
    /* 2의 보수 */
    ~A + 1// 0111 1010
    ~B + 1// 1110 0011
 
    /* 2의 보수를 이용한 A-B */
    printf("%d\n",A-B); // -151 출력
    printf("%d\n",A+(~B+1)); // -151 출력
 
    return 0;
}
cs


2.지역변수와 전역변

<지역변수와 전역변수>

 이름

 정의

참조 가능 범위 

생명 주기

특징 

지역변수

함수 안에 선언 된 변수 

선언 된 함수 내에서만 참조 가능

(다름 함수의 변수는 참조 불가능) 

함수 호출시 생성

함수 종료시 소멸 

  1.전역변수와 이름기 같아도 상관 없음

  2.이름이 같다면 지역변수 우선참조.

  3.즉, 전역변수는 참조 불가.(c++은 가능)

 전역변수

함수 밖에 선언 된 변수

어디서든 참조 가능 

프로그램 실행시 생성

프로그램 종료시 소멸 

  1.c언어의 특징인 구조화를 다 깨트림.

  2.안쓰는걸 권장함.

  3.특히 라이브러리에 전역변수 선언시 굉장히 좋지않음.

주로 지역변수를 사용하며 전역변수는 사용하지 않는 것을 권장한다.



3.static과 const 키워드

<extern>

전역변수에 붙는 키워드.
거대한 소스파일을 여러개의 파일로 나눌 때 사용.
같은 전역변수를 여러번 선언할 수 없으니 한번 선언하고 다른 파일에서 참조할 때 사용.
1
2
3
4
5
6
7
8
9
10
11
12
13
/* 전역변수가 선언된 state.c 파일 */
char state[99= "NULL";
 
void ChangeGood(){
    state = "Greate";
}
void ChangeNormal(){
    state = "Normal";
}
void ChangeBad(){
    state = "Bad";
}
 
cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 전역변수가 선언안된 main.c 파일 */
#include <stdio.h>
 
/* 함수 원형 선언 */
void ChangeBad();
void ChangeNormal();
void ChangeGood();
extern char* state;
int main(){
    printf("%s\n", state);
 
    ChangeGood();
    printf("%s\n", state);
 
    ChangeBad();
    printf("%s\n", state);
 
    ChangeNormal();
    printf("%s\n", state);
 
    return 0;
}
cs
그러나 extern 키워드 역시 전역변수에서 사용되는 키워드이므로 자주 볼 일은 없을것같다.

<static>

전역변수에 접근 가능 범위를 static키워드가 붙은 변수가 선언된 곳으로 제한하는 것.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 전역 변수에 static 키워드가 붙은 경우 */
#include <stdio.h>
static int s = 0;
 
void Function(){
    printf("%d\n", s);
    s++;
}
 
int main(){
    Function(); // 1 출력
    s = 5
    Function(); // 6 출력
}
cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 함수 안의 변수에 static 키워드가 붙은 경우 */
#include <stdio.h>
 
void Function(){
    static int s = 0// 지역변수처럼 보이지만 실제론 전역변수.
                      // 함수가 종료되도 소멸되지 않음.
    printf("%d\n"++s);
}
 
int main(){
    Function(); // 0 출력
    // s = 1; // 컴파일 에러
           // s는 메모리에 남아 있다.
           // 하지만 선언된 Fucntion함수 내에서만 접근가능
    Function(); // 1 출력
}
 
cs

<const>

const키워드가 붙은 변수는 변경이 불가능하다.
실수로 변경하는 경우 그 실수를 방지할 수 있게 컴파일러에서 제공하는 기능.
또한 이건 절대 변경하면 안돼! 라는 의미의 주석보다 더 강한 주석.
캐스팅으로 형변환 후 변경가능. 하지만 이 경우 책임은 변경한 프로그래머에게 전가 됨.
1
2
3
4
5
6
7
8
9
10
11
12
13
/* const 예제 */
#include <stdio.h>
 
// data : 변경하면 안되는 변수입니다.
void Function(const int data){
    data = 2l; // 컴파일 에러
}
int main(){
    Function(1);
 
    return 0;
}
 
cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* const : 주석보다 강한 주석 예제 */
#include <stdio.h>
 
// data : 변경하면 안되는 변수입니다.
void Function(const int data){
    .
    .
    .
}
int main(){
    Function(1);
 
    return 0;
}
cs



4.배열

<배열이란>

배열이란 같은 자료형을 기준으로 데이터를 묶어서 관리하는 것.

ex1) 5명의 학생들의 학점(unsigned char)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* 1차원 배열 예제 */
#include <stdio.h>
 
int main() {
    /*-----------------------------------------*/
    // 1)선언 
    // 자료형 변수명[갯수); 
 
    /*-----------------------------------------*/
    // 1.선언과 함께 초기화 
    // 학생 5명의 학점
    // 자료형 : unsigned char
    // 변수명 : grade
    // 배열의 인덱스의 갯수 : 5
    // 초기화 값 : 'A','B','C','D','F'
    unsigned char grade[5= { 'A','B','C','D','F' };
 
    // 인덱스 0번부터 4번까지 출력
    for(int i=0; i<5; i++){
        printf("%c\n", grade[i]); // A,B,C,D,F 
    }
 
    // 2,4번 인덱스의 값 변경
    grade[2= grade[4= 'A';
 
    // 인덱스 0번부터 4번까지 출력
    for(int i=0; i<5; i++){
        printf("%c\n", grade[i]); // A,B,A,D,A 출력
    }
 
    return 0;
}
cs


ex2) 5명의 학생들의 성적(unsigned char, 100점 만점)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* 1차원 배열 선언 및 초기화 및 배열 요소 접근 예제 */
#include <stdio.h>
 
int main() {
    /*-----------------------------------------*/
    // 1)선언 
    // 자료형 변수명[갯수]; 
 
    /*-----------------------------------------*/
    // 2.선언과 함께 일부만 초기화 값 지정. 나머지는 0으로
    // 학생 5명의 성적
    // 자료형 : unsigned char
    // 변수명 : score
    // 배열의 인덱스의 갯수 : 5
    // 초기화 값 : 0,78,99,0,0
    unsigned char score[5= { 0,78,99, }; // 99 이후의 2개의 요소는 0으로 초기화
 
    // 인덱스 0번부터 4번까지 출력
    for(int i=0; i<5; i++){
        printf("%d\n", score[i]); // 0,78,99,0,0 출력
    }
 
    // 2,4번 인덱스의 값 변경
    score[2= 80; score[4=100;
 
    // 인덱스 0번부터 4번까지 출력
    for(int i=0; i<5; i++){
        printf("%dn", score[i]); // 0,78,80,0,100 출력
    }
    
    return 0;
}
cs


ex3) 5명의 학생들의 연애상태(unsigned char, 1이면 커플, 0이면 솔로)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* 1차원 배열 선언 및 초기화 및 배열 요소 접근 예제 */
#include <stdio.h>
 
int main() {
    /*-----------------------------------------*/
    // 1)선언 
    // 자료형 변수명[갯수]; 
 
    /*-----------------------------------------*/    // 3.배열의 크기 생략하고 선언 및 초기화
    // 학생 5명의 연애상태
    // 자료형 : unsigned char
    // 변수명 : dating
    // 배열의 인덱스의 갯수 : 초기화 값의 갯수
    // 초기화 값 : 1,0,1,0,1
    // 이 방법은 선언할때만 가능 
    unsigned char dating[] = { 1,0,1,0,1 };
 
    // 인덱스 0번부터 4번까지 출력
    for(int i=0; i<5; i++){
        printf("%d\n", dating[i]); // 1,0,1,0,1 출력
    }
 
    // 0,2,4번 인덱스 값 변경
    dating[0= dating[2= dating[4=0;
 
    // 인덱스 0번부터 4번까지 출력
    for(int i=0; i<5; i++){
        printf("%d\n", dating[i]); // 0,0,0,0,0 출력
    }
    
    return 0;
}
cs


이번에는 2차원 배열에 대해 보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 2차원 배열 선언 및 초기화 및 배열 요소 접근 예제 */
#include <stdio.h>
 
int main() {
    // 1차원 배열과 다른건 요소 개수.
    // 1차원 : []   ->   2차원 : [][]
    // 요소 개수가 다르기 때문에 초기화도 달라짐
    // 1차원 : {}   ->   2차원 : { {},{},{} }
    unsigned char pix[3][3= {{  29198107 },
                                      { 140,  39205 },
                                       {  39,  41,  33 }};
    // 2차원 배열을 선언해도 메모리는 연속된 주소에 할당 됨.
    // 그러므로 위의 pixel[3][3]은 메모리에 할당된 형태가 pixel[9]와 같다.
    // 이런 과정을 해주는게 컴파일러.
    
    for(int i=0; i<3; i++){
        pix[i][1= 50// 1열의 값들을 50으로 변경
    }                     // {{  29, 198, 107 },
                          //  { 140,  39, 205 },
                          //  {  39,   41,  33 }};
 
    return 0;
}
cs

그렇다면 2차원 배열과 1차원 배열의 표현사이에는 어떤 수식을 통해 계산되는걸까?

1
2
3
4
5
6
7
/* 2차원 배열의 1차원 배열의 표현 */
 
int data[ m ][ n ]; // 2차원 배열 선언
 
data[ a ][ b ] == data[ n * a + b ]
 
data[ c ] == data[ c / n ][ c % n ]
cs
위와 같은 수식을 통해 계산된다.
우리가 다차원 배열을 사용할 때 저런 수식을 일일히 적지않지만 컴파일러가 대신해주는것이다.
또한 c99부터는 배열 선언시 배열의 개수에 변수를 사용할 수 있다.
하지만 우리가 사용하는 VC에서는 불가능하다.
또한 배열을 초기화 할 때 보다 효율적인 방법은 memset 함수를 이용하는 것.
하지만 memset함수는 초기화값이 한개라는 한계가 있다.

<배열과 포인터>

배열연산은 실제로 포인터연산과 동일하다.
배열은 초보용, 포인터는 고수용
배열이 포인터보다 쉬운이유
첫번째, 연산자우선순위가 높음 -> 수식이 깔끔
두번째, 선택사항이 적음
포인터를 사용하게되면 여러 선택사항이 있는데 배열은 이런부분을 전부 컴파일러가 해줌.
배열과 포인터 사이의 관계는 포인터 강의에서 자세하게 소개하겠습니다.


5.문자열

<문자열이란?>

문자열이 처음 만들어진건 메모리의 용량이 굉장히 작았을 때
메모리 효율이 굉장히 중요하므로 문자열 뒤에 널문자를 추가하는 방식으로 구현.
문자열 관련 많은 함수에서 매 요소마다 널문자를 확인하는 조건이 들어가므로 속도 저하의 주 원인.
문자열은 배열을 이용해 표현.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 문자열 선언 및 초기화 예제 */
#include <stdio.h>
 
int main(){
    /*-----------------------------------------*/
    // 1)선언 
    // 자료형 변수명[문자열길이 + 1 이상];
 
    // Tips! 라는 문자열의 길이는 5이므로 배열은 6이상 선언해야 함
    // 선언방법1.배열처럼 각 요소값 직접 대입.
    char str1[8= { 'T','i','p','s','!',0 };
    // 선언방법2.문자열상수 이용.
    char str2[8= "Tips!";
 
    return 0;
}
cs

<문자열 길이 구하기>

문자열 길이 구하는 함수를 구현해보자.
문자열의 끝에는 0이라는 널문자가 있다는 특징을 이용하면 됨
1
2
3
4
5
6
7
8
9
10
11
12
/* 문자열 길이 구하는 함수 구현 예제 */
#include <stdio.h>
 
int GetStringLength(char str[]) {
    int count = 0// 문자 갯수를 세기위한 변수
    while (str[count]) count++// 문자열의 끝 검사 및 카운팅
    return count;
}
int main(){
    char str[10= "Tips";
    printf("%d",GetStringLength(str)); // 4 출력
}
cs


읽어주셔서 감사하고 강의정리가 너무 늦어져서 죄송합니다.