C언어

C언어 문법 & 개념 정리

jamie-lee 2022. 11. 27. 23:57

1. C언어 기초

1. C 기초

  • 핵심 단어와 개념
    • stdio.h
    • clang
    • 컴파일러
  1. C언어 특징 및 기초적인 주의사항
    • C언어는 동작 끝에 ; 꼭 필요함!
    • 파이썬과 달리 출력시 줄바꿈을 일일이 해줘야 하는 면이 있음
    • 컴파일을 수동으로 해주어야 함
    • 변수에 담을 자료형을 먼저 지정해야 함
    • C언어의 문법이 자바스크립트 문법이랑 비슷하다! → C언어의 기본 구문에 바탕을 두고 개발되었다고 함
    • C에서 자료형을 알려주는 기능은 없다고 함! wow
  2. C언어 메인함수
    #include <stdio.h>
    
    int main(void)
    {
    	int n;      // 변수 선언부 
    	printf("hello, world\n"); // 데이터 처리부 
    
    	return 0;
    }
    
    • 파이썬에서 import 하듯이 #include <모듈파일.h>
      ※ 함수 호출 실행 순서 Pasted image 20221125212537.png
    • printf() : print + formatting 의 줄임말로, 출력 함수 (stdio.h 파일에 포함되어 있음)
    • int n; : 변수 값을 따로 넣지 않고 미리 선언할 수 있음 → 이때 변수 n에는 쓰레기 값garbage value 라고 하는 것이 들어가 있음
    • return 0; : 메인 함수를 끝내는 역할

2. 컴파일러

|500

  1. 용어
    1. 소스코드: 우리가 작성한 코드
    2. 머신코드: 2진수로 작성된 기계가 이해하는 코드
    3. 컴파일러: 소스코드를 머신코드로 바꿔주는 프로그램
  2. C언어의 컴파일러
    1. clang, gcc 등을 주로 사용
    2. 프롬프트에서 $ clang(혹은 gcc) 파일명.c 라고 치면 컴파일이 됨
    3. $ clang -o <실행파일제목이될부분> <C소스코드파일> -l<헤더에include한파일이름> → 자세히 컴파일하기`
    4. $ make 파일명 → 컴파일 + 해당 파일명을 가진 프로그램 생성 + 링킹 작업까지 해주는 명령어
  3. 컴파일링
    • 핵심 단어
      • 컴파일링
      • 어셈블링
      • 링킹
    1. 컴파일링의 4단계 ^yawgn3
      1. 전처리(precomplie, preprocess)
        • 실질적인 컴파일을 하기 전에 처리하는 작업
        • 추가된 라이브러리 파일 확인 → 실제로 그 파일에 들어가 해당하는 소스코드를 복사해옴(ex: #include)
        • 프로그래밍 편의를 위해 작성된 매크로 변환(ex: #define)
        • 컴파일할 영역 명시 (ex: #if, #ifdef 등)
        • test.ctest.i로 확장자가 바뀜
      2. 컴파일링(compile)
        • C코드를 어셈블리어라는 저수준 프로그래밍 언어로 변환
        • 컴퓨터가 이해할 수 있는 언어와 최대한 가까운 프로그램으로 만듦
        • 어셈블리어는 C보다 연산의 종류가 훨씬 적지만, 여러 연산들이 함께 사용되면 C에서 할 수 있는 모든 것들을 수행 가능
        • 컴파일이라는 용어는 이 단계를 얘기하기도, 전체 컴파일링 4단계를 통칭하여 부르기도 함
        • test.itest.s로 확장자가 바뀜
      3. 어셈블링(assemble)
        • 어셈블리어를 기계어로 변환
        • 어셈블리 코드를 목적 코드(오브젝트 코드)로 변환 → binary 형태의 목적 파일이 됨
        • CPU가 프로그램을 어떻게 수행해야 하는지 알 수 있는 명령어 형태인 연속된 0과 1들로 바꿔주는 작업.
        • 이 변환작업은 어셈블러라는 프로그램이 수행.
        • 소스 코드에서 오브젝트 코드로 컴파일 되어야 할 파일이 딱 한 개라면, 컴파일 작업은 여기서 끝나지만, 그렇지 않은 경우에는 링크라 불리는 단계가 추가.
        • test.stest.o로 확장자가 바뀜
      4. 링킹(link)
        • 만약 프로그램이 (math.h나 cs50.h와 같은 라이브러리를 포함해) 여러 개의 파일로 이루어져 있고, 하나의 실행 파일로 합쳐져야 하는 경우 필요한 단계.
        • 링커는 여러 개의 다른 목적 파일을 실행 가능한 하나의 목적 파일로 합침.
        • 예를 들어, 컴파일을 하는 동안에 CS50 라이브러리를 링크하면 오브젝트 코드는 GetInt()GetString() 같은 함수를 어떻게 실행할 지 알 수 있게 됨.
        • 목적 파일은 링킹을 거친 이후에 실행 파일이 됨
        • test.otest.exe로 확장자가 바뀜

3. 버그와 디버깅

  1. 버그와 디버깅이란
    1. 버그: 코드에 들어있는 오류
    2. 디버깅: 코드에 있는 버그를 식별해 고치는 과정 → 디버거라는 프로그램 사용
  2. 디버깅의 기본
    1. 중지점을 설정해 한 단계씩 수행하게 함
    2. printf 함수 사용

4. 코드의 디자인

  1. 코드의 정확성 체크하기
  2. 코드의 스타일가이드 참조하기
  3. 코드의 설계를 생각하기
    • 이게 중요함
    • 어떻게 더 효율적이고 문제를 적절하게 잘 해결하는 코드를 만들 것인가

5. 변수와 자료형

  1. 상수, 변수, 대입문
    int age;     // 변수 선언
    age = 15;    // 대입문: 변수에 상수 데이터를 저장
    
    • 여행 가기 전에 펜션을 예약하듯이, 데이터를 담을 변수를 예약하는 것 → 메모리 공간이 할당됨
    • 상수: 데이터 값 자체
    • 변수: 저장한 상수값에 따라 변수 값도 변함
    • 대입문: 대입 연산자(=)를 사용하여 오른 쪽의 식을 변수에 저장하는 것
  2. C의 자료형 Pasted image 20221125143031.png Pasted image 20221125150935.png
    • 그림에서 괄호는 생략 가능
    • 큰 자료형 값을 작은 자료형 변수에 넣으려면 작은 자료형 변수 사이즈에 맞춰지므로 조심(코끼리를 냉장고에 꾸겨 넣는 것)
    • sign과 unsign의 차이 Pasted image 20221125152441.png
      • sign은 음수와 양수의 표시를 의미하며, 비트의 맨 왼쪽 칸을 할애하여 사용함
      • 따라서 같은 3비트를 사용하여도 unsign 자료형과 sign 자료형이 표현할 수 있는 숫자의 범위는 다름! → 목적에 따라 음수 양수를 둘 다 표현할건지, 음수 버리고 양수만 크게 쓸 것인지 선택
      • 음수, 양수 부호가 붙어있다는 뜻으로, unsigned는 양수만 쓰겠다는 것.
      • sizeof(자료형) : 자료형의 크기를 확인하는 함수
    • 아래의 볼드체는 기본적으로 많이 사용하는 자료형이라는 뜻!
    • 정수
      • (signed) int: 특정 크기 또는 특정 비트까지의 정수 (ex: 5, 28, -3, 0 (일부 거대 기업이 아닌 이상 일반 사용자들은 대부분 int 사용))
      • long: 더 큰 크기의 정수
    • 실수
      • double: 부동소수점을 포함한 더 큰 실수 (float보다 메모리 두 배를 차지함)
      • float: 부동소수점을 갖는 실수 (ex: 3.14, 0.0, .2, 4., -28.56)
      • long double: 시스템에 따라 16 바이트 일수도 있다고 함.
    • 문자
      • char: 문자 하나 (ex: 'a', 'Z', '?')
    • bool: 불리언 표현 (ex: True, False, 1, 0, yes, no). 1 byte.
  3. 왜 이렇게 많은 자료형?
    1. 값에 따라 가장 적절한 자료형을 선택하도록 하게끔(ex. 나이를 저장하는데 굳이 double형 변수에 저장할 필요 X)
    2. 실수형은 2진수 변환 시 오차가 발생함 (ex. 0.1을 10개 더하면 1이 아닌 0.999…가 되지만 출력할 때는 1이라고 출력된다고 함)
  4. C언어 자료 종류 Pasted image 20221125150802.png
    • 16진수는 로봇이나 제품에 들어가는 프로그램을 작성할 때 많이 쓴다고 함
    • string: 문자열, ""로 묶은 1개 이상의 문자 (ex: "apple", "한" → 한글 1자는 최소 2바이트가 필요하므로 문자 상수가 못 됨)
  5. 식별자, 예약어
    1. 식별자identifier: 프로그램에서 이름으로 사용하는 것의 총칭(ex: 변수명, 배열명, 함수명, 구조체명)
    2. 예약어reserved word, keyword: 자료형 이름, for, while 등 (※ printf 같은 C언어에서 제공하는 라이브러리 함수명은 예약어가 아니며, 식별자로 사용할 수 있지만 함수 원래의 기능은 사라진다고 함 )

6. 출력 & 입력

  1. printf 함수의 형식 지정자(변환 명세) 더 자세한 형식 지정자 확인하기
    • %d, %i : int형
      • 출력 시 둘은 차이가 없으나, 입력시 %i는 10진수 외에 8진수, 16진수를 입력 받을 수 있음
      • %3d : 필드 폭 설정. 3칸을 차지하면서 int형 출력
      • %-d : 왼쪽 맞춤
      • %+d : 부호 표시 → 음수, 양수 표현이 가능해짐
    • %lf, %f : float, double
      • 무조건 소수 아래 6자리 출력
      • %.2f : 소수점 2번째 자리까지만 표현하기
    • %li : long
    • %c : char (문자 한 개 )
    • %s : string
      • printf("%s\n", 배열명) : 배열의 인덱스를 따로 지정하지 않고 배열명만 포함(배열명 자체가 문자열 배열의 시작 위치를 가리키는 포인터이므로, &을 붙여도 되고 안 붙여도 됨)
    • %p : 포인터 변수
  2. 출력값과 형식 지정자가 틀리면?
    • 에러는 발생하지 않으면서 이상한 값 출력 → 결과가 이상할 때 먼저 확인하기
  3. putchar 함수
    • putchar('A')
      • 문자 1개를 출력
      • 형식 지정자 필요 없음
  4. 입력함수 scanf
    • scanf("%d", &height):
      • 사용자로부터 형식 지정자에 해당되는 값을 입력받아 변수에 저장
      • 형식 지정자는 printf와 동일
      • &기호는 주소를 나타내는 연산자로, printf에서는 필요하지 않지만 scanf에서 필요함
      • scanf("%d %d", &age, &height) : 두 개의 변수에 각각의 값을 저장. 이때 두 개의 변수 입력을 구분할 때 공백 문자(빈칸, 탭, 엔터키)를 이용 → 따라서 아래 주의점 1번과 연결됨
    • 주의할 점
      1. scanf("%d\t", %height), scanf("%d\n", %height) 쌍따옴표를 닫기 전에 빈칸, 줄바꿈을 넣으면 안 됨! → 데이터를 한개 더 입력해야 입력이 완료되고, 이 입력값은 다음 scanf문에서 사용된 변수 안에 저장 됨
      2. %2d처럼 필드폭을 지정하면 최대 n자리까지 입력을 제한하는 효과
      3. scanf("키는?%d", &height) 처럼 처럼 출력할 내용을 scanf에 담지 말고 printf와 scanf를 따로 사용함
  5. getchar 함수
    • 변수 = getchar();
    • 키보드에서 문자 1개를 입력
    • 형식 지정자 필요 X

7. 연산자

  1. 기본 연산자 및 주석
    • +:  더하기
    • -: 빼기
    • *: 곱하기
      • 제곱 연산자는 없다…!!! → 라이브러리 함수를 이용
    • /: 나누기
      • 정수끼리 연산하면 값도 정수가 나오므로 주의
      • 컴파일링이 자동 형 변환 해줌
    • %: 나머지
    • &&: 그리고
    • ||: 또는
    • <, >, <=, >= : 비교 연산자. 10 <= x < 20 처럼 사용하면 이상한 값이 나올 거니까 조심.
    • //: 주석
    • /* 긴 주석 */
  2. 삼항 조건 연산자
    • 조건문 ? 참값 : 거짓값;
      • int data = num1 > num2 ? num1 : num2; : 숫자1이 숫자2보다 크다면 숫자1, 아니면 숫자2를 data에 넣는다
      • if ~ else 문을 일부 대체할 수 있으므로 알아둘 것!
  3. 특별한 연산자
    • 복합 대입 연산자 Pasted image 20221125164359.png
    • 증감 연산자 (위치가 중요하다!)
      • 변수++, 변수-- : 현재 변수의 값 그대로 식에서 사용 → 1씩 증가(감소) (ex: a가 2일때, ++a * 10은 30)
      • ++변수, --변수 : 일단 1씩 증가(감소) → 그 값을 식에서 사용 (ex: a가 2일때, a++ * 10은 21)
    • 형 변환 연산자(type cast operator)
      • (바꾸려는자료형) 변수[값][(수식)]
        • 변수 = (double)sum / (double)n : 괄호 안에 변환하고자 하는 자료형을 입력해주면, 저 식에서만 자료형이 바뀌어서 계산 됨.
        • (int) (pi+1.9) : 수식을 사용할 땐 괄호
        • 내가 할당하려는 왼쪽 변수에는 형 변환을 할 수 없음(==냉장고의 크기를 마음대로 바꿀 수 없듯이 ==)
    • 비트 연산자 bitwise operators 출처: https://www.examtray.com/tutorials/last-minute-c-programming-bitwise-operators-tutorial
      • 비트끼리 연산함
      • & , |, ~ : 각각 기본 논리연산자의 AND, OR, NOT (1획이면 충분하다는 공통점)
      • <<, >> : 특정 수의 비트를 왼쪽, 오른쪽으로 이동하여 수를 계산함. 전자는 곱하기 대신, 후자는 나누기 대신으로 사용할 수 있음. (ex: 5 << 1의 값은 5 * 2 = 10이다 → 5를 이진수로 나타내면 0101인데 이를 왼쪽으로 한 칸 옮기면 1010이고 이것을 십진수로 나타내면 10)

8. 조건문과 루프

  • 자바스크립트랑 문법이 똑같음! (※ ; 꼭 붙여줘야 함)
  1. 조건문 - if문
if (조건)
{
	수행할 구문;
}
else if (조건)
{
	수행할 구문;
}
else (조건)
{
	수행할 구문;
}
  1. 조건문 - switch문
// 변수와 case의 값이 동일할 때, 해당하는 case 실행문이 수행된다 
// 만약 동일한 값이 없다면 default의 실행문을 실행함 (default는 생략 가능)
switch(변수)
{
    case 값1 : 
        실행문; 
        break;
    case 값2 : 
        실행문; 
        break;  
    default :
        실행문;    
}
  1. 반복문
while (조건)
{
	수행할 구문;
}

// 횟수를 지정하고 반복 할 때
for (변수 초기화; 변수 조건; 변수 증가)
{
	수행할 구문;
}

// 일반적인 for문 예시
for (int i = 0; i < 반복횟수; i++) 
{
	수행할 구문;
}

// do-while문
do
{
	n = get_int("Positive Integer: ");
}
while (n < 1);
  • do-while문은 while 단독 사용과 달리, do의 구문을 무조건 한 번은 수행하게 하는 차이가 있음

9. 사용자 정의 함수

  1. 사용자 함수 사용하기
#include <stdio.h>

// 사용할 함수의 프로토타입을 미리 선언문
void cough(void);

// 메인 프로그램 
int main(void)
{
	cough();
}

// 내가 정의한 함수 
void cough(void)
{
	printf("cough\n");
}
  • 메인 함수와 분리하기
    • 메인 함수 내에서 정의하지 않음.
    • 메인 함수와 내가 정의한 함수는 완전히 별개. (메인 함수는 우리 집, 내가 정의한 함수는 친구 집 )
  • 함수 원형prototype 선언
    • 프로토타입을 미리 선언하지 않으면 오류 → C는 오래된 언어라 뒤에 뭐가 있을 거라 생각을 안함
  1. 사용자 함수 정의하기
아웃풋자료형 함수이름(인풋자료형1 인수명1, 인풋자료형2 인수명2)
{
	수행할 구문
}
  • 입출력
    • 입출력이 없으면 void
    • 출력이 있다는 건 return 값이 있다는 것
  • 인풋 자료형 선언
    • 메인 함수에서 이미 자료형을 선언한 값이라 할지라도, 인수로 넘겨주면 별개로 저장되는 값이 됨 → 인수명에서 따로 또 선언을 해주어야 함!

10. 하드웨어의 한계

  • 핵심 단어
    • 메모리
    • 오버플로우
  1. 메모리
    1. 모든 프로그램은 실행 중에 RAMrandom access memory이라는 물리적 저장장치에 저장 됨
    2. RAM은 유한하다 → 때때로 부정확한 결과를 냄
  2. 오버플로우
    1. 부동 소수점 부정확성
      1. float 타입이 표현 가능한 범위 이상의 소수점 자리수를 지정한 뒤 출력하면 부정확한 숫자가 나옴
    2. 정수 오버플로우
      1. 숫자의 크기가 커져 int 타입(40억까지 표현 가능) 이상의 수를 넘기면 (232 이상의 숫자) 이상한 숫자가 나옴
    3. 실생활 사례
      1. 1999년에 연도가 마지막 두 자리수로 저장되어 있어 99에서 00으로 정수 오버플로우 발생 → 새해가 2000년이 아닌 1900년으로 인식되는 문제 → 돈을 때려 부어 메모리를 추가해 해결함
      2. 보잉 787은 248일이 지나면 모든 전력이 초기화 → 248일은 대략 232임 → 오버플로우 발생 → 주기적으로 변수를 0으로 세팅함

2. 배열

1. 배열

  1. RAM과 바이트
    • 노란색 사각형 → 메모리
    • 작은 사각형 하나 → 1바이트(8비트) 라고 생각!
  2. 배열이란?
    1. 자료형이 같은 많은 자료하나의 이름으로 저장하는 연속된 기억 공간 (ex. 5단 옷 서랍장, 12층 아파트, … )
    2. 배열 인덱스는 왜 0부터 시작할까?: 배열 첫 원소로부터 몇 개 뒤의 원소인가를 나타냄
  3. 배열 선언 및 초기화
const int N = 3; // 배열에 저장할 값 갯수를 *상수* 전역변수로 설정

int main(void)
{
// 방법 1
int 배열이름[N];  // 배열선언: N개 만큼의 값을 저장할 공간 생김 
배열이름[i] = 값1;
...
배열이름[i] = 값n; 

// 방법 2
int 배열이름[5] = {1, 2, 3, 4, 5}; // 중괄호를 이용해 배열 선언 및 초기화

// 다차원 배열 선언 및 초기화
int 배열이름[2][4] = {1, 2, 3, 4, 5, 6, 7, 8};
}
  • 자료형 배열명[원소수];
    • 다른 자료형을 섞어서 같은 배열에 저장할 수 없음
    • 배열을 선언할 때 원소수는 상수여야 함 (const)
    • 원소 개수보다 큰 인덱스에 값을 할당하는 경우 인덱스 에러 뜨지 않음! ⇒ 다음 메모리 공간에 값을 할당하기 때문에 결과를 예측할 수 없게 됨.
  • 자료형 배열명[원소수] = {값1, 값2, 값3 …};
    • 중괄호를 이용한 값 저장은 배열을 선언함과 동시에만 가능. 선언문이 아닌 곳에서는 불가능.

2. 문자열과 배열

  1. 문자열
    1. 문자열 데이터는 “char 자료형의 배열
    2. 노란색 한 칸은 1바이트(char 데이터 메모리)
    3. 문자열 데이터는 끝에 \0이라고 하는 널 종단 문자(null terminating character)가 포함 됨! (모든 비트가 0인 1바이트) → 따라서 작성하고자 하는 문자열 개수 +1 을 해서 원소수를 결정!
    4. 우리가 정한 임의의 배열명 자체가 배열 시작 주소이며 즉, 배열 시작 위치를 가리키는 포인터
  2. 문자열 선언 및 초기화
    • char 변수명[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
    • char 변수명[6] = "Hello";
    • char *변수명 = "Hello"; : 원소수 지정하지 않고 초기화하는 방법(아래 “포인터와 문자열” 항목에서 더 자세히)
  3. 관련 함수
    1. strlen() : 스트링 + 렝쓰의 합성어로 문자열의 길이를 알려줌 string.h 파일에 포함 됨
    2. toupper() : 첫 글자를 대문자로 바꿔주는 함수 ctype.h 파일에 포함
    3. strcmp(문자열1, 문자열2) : 두 문자열이 같으면 결과값이 0인 함수. string.h 라이브러리에 포함
    4. strstr(): 문자열 안에서 특정 문자열을 검색할 때 사용함. 찾으려는 문자열이 여러개 존재하는 경우, 반환 값을 받아서 다시 검색하는 코드를 사용하면 순차적으로 substr에 접근하거나 몇 개의 substr이 있는지 체크할 수 있음
      • str에서 substr을 찾은 경우: str 기준으로 첫 번째 substr의 위치를 포인터로 반환
      • str에서 substr을 찾지 못한 경우: NULL 반환
#include <string.h>

// 검색을 진행할 문자열, str 문자열에서 검색하려는 sub string
// - 검색을 진행할 문자열 : 종단 문자가 나올 때까지 `str`의 내용을 검색함
// - `substr`: 반드시 종단 문자로 종료되어야 함
char* strstr(const char* str, const char* substr);
  1. 매크로 상수
    • #define 변수명(대문자로사용) 값
      1. #define N 5 와 같이 많이 사용
      2. 코드 내의 변수명을 뒤의 값으로 정의함
      3. 전처리기가 소스 파일에서 해당 변수명을 만나면 정의한 내용으로 바꿔줌

3. 명령행 인자

  • 핵심 단어
    • make 파일명.c처럼 파일을 실행할 때, 뒤에 파일명을 써넣는 프로그램을 만드려면?
    • 명령행 인자
    • argv
    • argc
  1. 명령행 인자 설정하기
    int main(int argc, string argv[])
    {
    	if (argc == 2) // 명령행 인자가 입력되면 다음을 실행하라
    	{
    		printf("hello, %s\n", argv[1]);
    	}
    	else
    	{
    		printf("hello, world\n");
    	}
    }
    
    • argc
      • main 함수가 받게 될 입력의 개수
    • argv[]
      • 우리가 입력하는 인자가 포함된 배열(즉, 파일명 처럼 우리가 입력하는 내용)
      • argv[0]는 기본적으로 프로그램의 이름
      • 하나의 입력이 더 주어진다면 argv[1]에 저장

3. 메모리

1. 메모리 주소

  1. 16진수(Hexadecimal) |500
    1. 컴퓨터 과학에서는 10진수, 2진수 대신 16진수를 사용하는 경우가 많음 → 2진수를 간단하게 나타낼 수 있음 (4비트가 하나의 16진수로 표현 → 1바이트는 두 자리의 16진수로 표현)
    2. 0~9 + a~f 로 나타냄!
    3. 0x는 뒤에 붙은 문자가 16진수임을 나타내는 표기

2. 포인터

|500 |500

  1. 포인터란?
    1. 메모리 주소를 담고 있는 변수
    2. 포인터가 담고 있는 주소가 뭔지가 중요한 게 아니라, 그 포인터가 가리키고 있는 것이 무슨 변수인지가 더 중요. ("접근"의 개념) 보물지도 같은 것. P의 상자 안에는 '화살표’가 있다고 생각해라!
    3. 포인터는 보통 64비트! (=8바이트)
    4. 초기에 별표를 붙이는 것은 포인터를 선언하겠다는 뜻이고, 선언된 이후에는 지정된 메모리 주소에 해당하는 메모리 값을 뜻하게 됨
  2. &*
    1. &변수명 : 해당 변수의 메모리 시작 주소(즉, 지도)
    2. *포인터변수명 : 해당 포인터 변수의 주소가 가리키는 곳이 담고 있는 값
  3. 사용 예시
int n = 50;
int *p = &n;
printf("%p\n", &n);   // 주소 = 포인터 
printf("%i\n", *&n);  // 주소로 간 곳 = 정수, float, 문자 등의 값
printf("%p\n", p);  // p란 변수는 포인터 변수
printf("%i\n", *p); // p에 담긴 주소로 가라는 것
  1. 다중 포인터
    1. 포인터를 가리키는 포인터
    2. **포인터변수명, ***포인터변수명 처럼 사용하고, 이때 *의 개수가 화살표의 개수라고 생각해보면 됨
    3. 사용 예시
int n = 50;
int *p1 = &n;
int **p2 = &p1;
printf("%p\n", p1); // p1 변수가 가리키는 화살표 -> n으로 가는 주소 값
printf("%p\n", p2); // p2 변수가 가리키는 화살표 -> p1으로 가는 주소 값
printf("%i\n", *p1); // p1 변수가 가리키는 곳에 담겨 있는 것 -> n 값
printf("%i\n", **p2); // p2 변수가 가리키는 곳에 담겨 있는 것 -> n 값
printf("%p\n", &p1); // p1의 주소 = p2
printf("%p\n", &n); // n의 주소 = p1

3. 포인터와 문자열

|500

  1. 문자열은 결국 문자의 배열
    1. char *s = "EMMA"에서 변수 s는 결국 이러한 문자열을 가리키는 포인터
    2. 변수는 가장 첫번째 문자, 즉 변수[0]을 가리키는 것이나 마찬가지
  2. 문자열 비교
    1. 두 문자열이 같은 내용이라는 것은 두 문자열의 주소가 같다는 것!
    printf("%c\n", *s);     // s의 첫 글자
    printf("%c\n", *(s+1)); // s의 두 번째 글자(s의 첫주소에 +1 한 것)
    printf("%c\n", *(s+2));  
    printf("%c\n", *(s+3));
    
  3. 문자열 복사
    1. malloc() 함수
      2. 우리가 문자열을 정의할 때, 주소를 전달하는 것이기에 문자열을 다른 변수에 할당한다고 해서 사본이 만들어지지 않음 → 동일한 주소를 전달하기에 결국 똑같은 데이터를 가리키게 됨
      3. malloc을 이용해 또 다른 메모리 주소를 할당하고 여기에 문자열을 복붙해서 넣어야 함
      4. 말록을 통해 메모리 주소를 할당할 때, 문자열이 경우 널 종단 문자까지 포함해야 하므로 +1 바이트 추가하여 할당

4. 메모리 할당과 해제

  1. malloc
    • 동적 메모리 할당을 위해 사용
    • 말록 함수는 정해진 크기 만큼 메모리를 할당하고, free시킬 수 있는 포인터를 반환
    • malloc(memory allocation)은 비워진 메모리의 첫 주소를 줌! ⇒ 포인터 변수에 담을 수 있다!
    • 초기화 된 포인터 변수에 malloc으로 메모리를 할당하지 않은 채, 데이터를 넣으면 안 됨 → 그 변수에 어떤 쓰레기 값이 담겨져 있을지 모름! 오류 발생!
    • 예를 들어, int 형 변수를 선언하기 위해 메모리를 할당하려면 다음과 같이 malloc 함수를 사용
// int 형 변수를 선언하기 위해 메모리를 할당
int *ptr; 
ptr = (int*) malloc(sizeof(int)); 
  1. calloc
    • 항상 malloc 함수와 비슷하게 사용
    • 이 함수는 메모리를 할당하고 할당한 메모리를 0으로 초기화
    • (void *) calloc(size_t num, size_t size)
    • 예를 들어, int 형 변수를 선언하기 위해 메모리를 할당하려면 다음과 같이 calloc 함수를 사용
// int 형 변수를 선언하기 위해 메모리를 할당
int *ptr; 
ptr = (int*) calloc(1, sizeof(int)); 
  1. realloc
    • 이 함수는 메모리의 크기를 변경하고, 기존 메모리를 복사하여 유지(ex: 배열의 크기를 늘려야 하는 경우)
    • 식구가 더 늘어날 예정이라 기존 식구들 데리고 더 큰 집으로 이사하는 상황
    • 먼저 메모리의 크기를 늘려야 하는 배열의 포인터를 인자로 받음 → 새로운 크기로 재할당한 메모리를 반환
    • (void *) realloc(void *p, size_t size)
    • 메모리 크기를 줄일 때도 realloc을 사용할까? → Yes
      • 주어진 크기보다 작은 값을 인자로 줄 경우, 해당 메모리 영역의 크기가 줄어들고 남은 부분은 반환
      • 언제 그렇게 사용할까?
        • realloc을 사용해 메모리 크기를 줄이는 것은 크기가 큰 메모리 공간을 줄여야 할 때 유용
        • 특히 동적 메모리 할당을 사용하는 경우, 가비지 컬렉션을 사용하지 않는 C 프로그램이나 시스템 프로그래밍에서도 자주 사용
    • 예를 들어, int 형 변수를 선언하기 위해 메모리를 늘려야한다면 다음과 같이 realloc 함수를 사용
// int 형 변수를 선언하기 위해 메모리 크기 늘리기
int *ptr; 
ptr = (int*) realloc(ptr, 2 * sizeof(int)); 

// A라는 리스트에 원소를 더 추가하기 위해 메모리 크기 늘리기 
int *A = (int*) realloc(A, sizeof(int)*(n+1)); 
A[n] = x; // x를 배열 A의 마지막 인덱스에 추가합니다. 
n++;      // 원소 개수를 하나 늘립니다.
  1. mallocrealloc의 사용 예시
// malloc으로 메모리를 할당 받아서 0부터 2까지 값 삽입
int *ptr; 
ptr = (int*) malloc(3 * sizeof(int)); 
for(int i = 0; i < 3; i++) 
  ptr[i] = i+1; 

// 3, 4를 추가하여 5개의 원소를 가지기 위해 메모리 크기를 늘려야한다면 
ptr = (int*) realloc(ptr, 5 * sizeof(int)); // int 타입 5개가 들어갈 메모리 할당 받음
for(int i = 3; i < 5; i++) // 3, 4를 삽입
  ptr[i] = i+1;
  1. free
    • 말록을 이용해 메모리를 할당한 후 free(포인터변수)을 통해 메모리 해제해야 함
    • 그렇지 않으면 메모리에 저장한 값은 쓰레기 값으로 남아, 메모리 누수가 발생
  2. memcpy()
    • 소스 영역에서 목적지 영역으로 특정 바이트만큼 복사한 후, 목적지 영역의 포인터를 반환!
    • 말록 랩의 realloc의 메커니즘에 포함 되었음
#include <string.h>

// src 영역에서, dest 영역으로, n 바이트만큼 복사 → dset의 포인터 반환 
void *memcpy(void *dest, const void *src, size_t n);
  1. valgrind
    4. $ valgrind ./파일명 → 메모리 누수가 얼마나 일어나는지 검사해줌
  2. 버퍼 오버플로우
    1. 말록으로 n개의 메모리를 할당했는데, n+1 번 인덱스에 접근하려고 할 때 생기는 현상
    2. 할당하지 않은 메모리에 접근하려고 할 때!

5. 메모리 교환, 스택, 힙

  1. x의 값을 y에, y의 값을 x로 옮기는 것
  2. x와 y 교환을 위해 swap(x, y)와 같은 함수를 이용한다고 가정 → 메모리 주소를 넘겨주지 않으므로 이는 작동하지 않음! (=call by value)
  3. malloc으로 메모리를 할당한 데이터는 힙(heap) 영역에 존재
  4. main 함수를 포함하여 우리가 할당한 변수 x, y, 함수의 매개변수 a, b 등 함수와 관련된 것들은 스택(stack) 영역에 존재
  5. 함수에 변수를 넘겨주면 동일한 스택 구역이지만 데이터 값을 "복제"하여 전달 (=call by value)
  6. 따라서 swap(&x, &y)처럼 x, y의 주소를 함수에 넘겨주어야 함! (아래 그림 처럼 되게) + swap 함수의 매개변수는 포인터 변수여야 함! (주소를 건네줄거니까) (=call by address) |200
void swap(int *a, int *b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
  1. *a : 역참조 연산자. 의미는 'a라는 변수에 화살표(지도)가 있고 그 화살표가 가리키는 곳’을 의미함! (지도 자체가 아니라 지도가 가리키는 곳! &a가 ‘a로 가는 지도’ 자체임.)
  2. 포인티(pointee) : 포인터가 가리키고 있는 것. malloc() 함수를 통해 별도로 우리가 설정해줘야 한다.
  3. 힙 오버플로우: malloc을 계속 호출하다 보면 점점 사용하는 메모리 영역이 아래로 늘어나고, 스택 영역을 침범하게 되어 힙 오버플로우 현상 발생.
  4. 스택 오버플로우: 재귀함수처럼 자기 자신을 계속 호출하는 함수를 쓰다 보면, 점점 메모리 영역이 위로 늘어나며 힙 영역을 침범하는 스택 오버플로우 현상 발생.

4. 구조체

  • 구조체
    typedef struct
    {
    	string name;
    	int year;
    	float gpa;
    }
    student;
    
    student s1 = {'Zamyla', 2014, 4.0};
    s1.gpa = 3.5;
    
    • 배열 말고 데이터를 묶어주는 또 다른 방법
    • 서로 다른 자료형의 변수를 하나로 묶어 새로운 자료형을 만들 수 있음(int가 자료형이듯, 예시의 student는 우리가 정의한 자료형)
    • 구조체명.멤버명으로 접근할 수 있음(ex: student.name)
  • 구조체와 배열의 비교
    • 배열은 같은 자료형만 묶을 수 있으나, 구조체는 다른 자료형도 묶을 수 있음
    • 학생이 몇 명 있는지 미리 선언할 필요 없음
    • 배열은 인덱스를 이용해 순환할 수 있지만, 구조체는 멤버를 순환할 수 없음

5. 파일 쓰기

  • 핵심 단어
    • scanf
    • fopen
    • fprintf
    • fclose
  1. FILE *file = fopen("phonebook.csv", "a"); : 파일 열기, a는 append, r은 read, w은 write 모드
    • file이 NULL이면 잘못되었으므로 1을 반환하는 함수를 fopen뒤에 넣어 줌
    • return시, 뭔가 정상적이면 0을 반환. 뭔가 잘못되면 1을 반환.
  2. fprintf(file, "%s,%s\n", name, number); : fprintf는 file printf. 사용자에게 name, number라는 문자열을 입력 받고, 파일에 출력 해줌
  3. fclose(file); : 파일 닫기
  4. 파일 읽기
    1. fread(bytes, 3, 1, file); : fread(배열, 읽을 바이트 수, 읽을 횟수, 읽을 파일)

6. C 라이브러리

  1. #include <math.h>
    1. pow(숫자, 지수) : 제곱을 구할 때
    2. sqrt(숫자) : 루트값
    3. abs(숫자) : 절대값
    4. exp(x) : 자연상수 ex
    5. log10(x)
    6. log(x)
  2. #include <stdlib.h>
    1. rand() : 정수 0~32767 중의 난수 한 개를 반환
    2. srand(time(NULL)) : 현재 시간(time(NULL))을 난수 발생기의 씨드로 설정
  3. #include <stdint.h> ; 자세한 용례는 여기서 ☞ C언어 코딩도장
    1. 크기별로 정수 자료형이 정의된 헤더 파일