OC开发基础-C语言基础

2025/3/17

# C语言编译

  • gcc main.c -o my_program:GNU Compiler Collection
  • gcc main.c func.c -o my_program:多文件开发

# Xcode配置

  • 代码练习:新建项目 - MacOS - Command Line Tool
  • 关闭代码提示:Preferences -> Text Editing -> Code Completion -> Disable
  • 为project添加多个target:
    • 新建Project -> Targets -> +
    • 切换:编译链接旁边切换目标target
  • 打开僵尸对象调试:Edit Schemes -> Run -> Diagnostics -> Zombie Objects -> Enable

# Xcode快捷键

  • 同时编辑一个变量control+command+e

# Xcode文档安装

  • Window -> Developer Documentation

# C语言基础

  • 程序只会从main函数开始执行
  • 函数:void funcName(int a, int b)
  • int范围在-21亿~21亿左右
  • float只能存7位有效位数,定义加f,如f1 = 1.1f
  • double可以存16位有效位数,直接写一个小数就是double类型的
  • char只能存一个字符:char a = 'a',必须用单引号
  • int赋值小数会被截断,如int a = 1.9,结果为1
  • 布尔值:c语言用int表示,1为真,0为假
  • 只读变量:const int a = 1;
  • 只读指针变量:const int * p = &a;无法通过指针修改变量的值
    int a = 1;
    int b = 2;
    const int * p = &a;
    *p = 5; // 报错
    
    1
    2
    3
    4
  • 只读变量指针:int * const p = &a;无法通过指针修改变量的值
    int a = 1;
    int b = 2;
    int * const  p = &a;
    p = &b; // 报错
    
    1
    2
    3
    4
  • 完全只读指针:int const * const p = &a;

# printf格式化

  • int: %d,float: %f,char: %c,string: %s,double: %lf
  • 格式化位数:
    • %.2f,如%.2f表示保留两位小数
    • %5d,如%5d表示输出的数字前面补空格,共5位
    • printf("%05d\n", 3);:,用0补齐五位,输出00003
    • 八进制:以0开头,07777777,打印占位符%o
    • 十六进制:以0x开头,0x12acde,打印占位符%x
  • 格式化地址位:%p,例::printf("%p\n", &a);
  • 对齐输出:%-5d,如%-5d表示输出的数字前面补空格,共5位,输出1
  • 对齐输出变量控制:printf("编号:%-*d姓名:%-*s性别:%-*s成绩:%-*.0lf\n", 4, stu.id, 15, stu.name, 5, gender, 10, stu.score);

# scanf

  • 接收用户输入,赋值给指定变量地址scanf("%d", &a);
  • 使用&获取变量的地址
    int main(int argc, const char * argv[]) {
        int num = 0;
        int pwd = 0;
        printf("请输入账号:");
        scanf("%d", &num);
        if (num == 1) {
            printf("请输入密码:");
            scanf("%d", &pwd);
            if (pwd == 123456) {
                printf("密码正确\n");
            } else {
                printf("密码错误\n");
            }
        } else {
            printf("请输入数字\n");
        }
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
  • 输入多个字符用空格隔开
    scanf("%d%d", &num, &pwd);
    
    1
  • 可以自定义分隔符
    scanf("%d-%d", &num, &pwd);
    
    1
  • scan是从缓冲区获取数据,如果缓冲区有数据,则不会等用户输入
    int num = 0;
    int pwd = 0;
    scanf("%d", &num);
    rewind(stdin); // 清空缓冲区
    printf("num = %d\n", num);
    
    // 输入 10 20
    scanf("%d", &pwd);
    printf("pwd = %d\n", pwd);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • 清空scanf的缓冲区:rewind(stdin);
  • 清空缓冲区函数
    void clearInputBuffer(void) {
        int c;
        while ((c = getchar()) != '\n' && c != EOF);
    }
    
    1
    2
    3
    4

# 运算

  • 如果算数运算的表达式类型是一致的,那么结果也会是这个类型
int num1 = 10;
int num2 = 4;
double num3 = num1 / num2;
printf("num3 = %lf\n", num3); // num3 = 2.000000
1
2
3
4

# 内置方法

  • 做随机数
#include <stdlib.h>

// 0-9 的随机数
printf("请输入分数:%d\n", arc4random_uniform(10));

// 10-20的随机数
printf("请输入分数:%d\n", arc4random_uniform(11) + 10);
1
2
3
4
5
6
7
  • 最大值INT32_MAX
  • 最小值INT32_MIN

# switch-case

  • 注意:break,不然会继续执行下面的case
  • switch表达式的结果不可以是浮点数,否则会报错
  • 如果要在case里面声明变量,就需要用大括号括起来这个case的语句
    int score = 6;
    
    switch(score * 10) {
        case 60: {
            printf("刚好及格\n");
            break;
        }
        case 70:
            printf("及格\n");
            break;
        default:
            printf("不懂\n");
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

# 函数声明

  • 函数声明可以在函数调用之前声明
  • 函数声明和函数定义可以放在同一个文件,也可以放在不同的文件
  • 函数声明:void funcName(int a, int b);
  • 函数定义:int funcName(int a, int b) { return a + b; }
  • 只读形参void printArray(const int * arr, int len);

# 预处理指令

  • 宏定义没有分号;
  • 编译之前把程序中所有使用宏名的地方换成宏值
  • #define LEN 10
  • #undef LEN: 删除宏定义,后续代码宏不可再使用
  • 条件编译指令:#if#ifdef#ifndef#else#elif#endif
  • 分类
    • 文件包含指令:#include,把头文件内容复制到当前位置
    • 宏指令:#define
    • 条件编译指令:#if#ifdef#ifndef#else#elif#endif
    • 错误处理指令:#error
    • 警告处理指令:#warning
    • 运算指令:#pragma
  • 特点
    • #开头
    • 没有分号;
    • 预处理指令是编译器在编译之前处理的,所以不能在函数里面使用
  • 带参数宏,参数不需要加类型说明符
    #define ADD(a, b) a + b
    int c = ADD(10, 20);
    printf("%d\n", c); // 30
    
    1
    2
    3
  • 条件编译指令#if:只编译部分代码,条件只能是宏,因为是在编译阶段,还没有变量
    #define DEBUG 1
    #define DEV 0
    #define DEPLOY 0
    
    #if DEBUG
    printf("DEBUG 模式!\n");
    #elif DEV
    printf("DEV 模式!\n");
    #elif DEPLOY
    printf("DEPLOY 模式!\n");
    #endif
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • 条件编译指令#ifdef:判断宏是否存在,存在就编译,不存在就不编译
    #ifdef DEBUG
        printf("DEBUG模式!!");
    #endif
    
    1
    2
    3
  • 条件编译指令#ifndef:判断宏不存在,不存在就编译,存在就不编译
  • 技巧:debug打log,只有在debug模式才会编译的log代码
    #define DEBUG 1
    #define DEV 0
    #define DEPLOY 0
    
    #if DEBUG
        #define LOG(str, ...) printf(str, ##__VA_ARGS__)
    #else
        #define LOG(str, ...)
    #endif
    
    LOG("DEBUG 模式!\n");
    LOG("%d\n", a);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

# 进制

  • 十进制:10,打印占位符%d
  • 二进制:以0b开头,ob101010111
  • 八进制:以0开头,07777777,打印占位符%o
  • 十六进制:以0x开头,0x12acde,打印占位符%x

# 修饰符

  • int
    • signed:有符号,默认
    • unsigned:无符号
    • short:短整型,占2个字节,short int可以省略int
    • long:长整型,32位操作系统占4个字节,64位系统占8个字节,long int可以省略int
    • long long:长长整型,任何系统都占8个字节,long long int可以省略int

# 数组

  • 声明数组:int arr[10];
  • 声明直接赋值:int arr[3] = {100, 2,10};
  • 自动计算初始化长度:int arr[] = {100, 2,10};
  • 自动计算初始化值为0:int arr[10] = {};
  • 指定位置初始化值:int arr[10] = {[1] = 3, [9] = 7};
  • 使用直接赋值的方式,就不能使用变量做长度,可以使用作为长度。
    • 错误示范:int arr[len] = {100, 2,10};
    • 正确示范:int arr[LEN] = {100, 2,10};
  • 长度计算:sizeof(arr) / sizeof(arr[0]);
  • 当数组作为函数的参数的时候,会丢失数组的长度信息,所以需要使用指针来传递数组。同属传入数组长度。
    • 例子:void func(int *arr, int len);/void printArr(int arr[], int len);
    • 原因:函数参数是数组,那么该形参就是指针地址。
  • 数组名是一个地址常量,赋之后无法修改

# 二维数组

  • 声明:int arr[3][4];

  • 直接赋值示例:

    int arr[3][4] = {
        {1, 2, 3, 4},
        {1, 2, 3, 4},
        {1, 2, 3, 4},
    };
    printf("第二行第一列是:%d\n", arr[2][1]); // 244
    
    1
    2
    3
    4
    5
    6
  • 指定行初始化

    int arr[3][4] = {
        [1] = {1, 2, 3, 4},
        [2] = {1, 233, 3, 4},
    };
    printf("第二行第一列是:%d\n", arr[2][1]);
    
    1
    2
    3
    4
    5
  • 全部初始化为0

    int arr[3][4] = {0};
    printf("第二行第一列是:%d\n", arr[2][1]); // 0
    
    1
    2
  • 求行数列数

    int arr[3][4] = {0};
    int rows = sizeof(arr) / sizeof(arr[0]);
    int columns = sizeof(arr[0]) / sizeof(arr[0][0]);
    int cells = sizeof(arr) / sizeof(arr[0][0]);
    
    printf("行数是:%d\n列数是:%d\n总元素个数是:%d\n", rows, columns, cells);
    
    1
    2
    3
    4
    5
    6
  • 函数入参指定任意行列的数组:void func(int rows, int cols, int arr[][cols]);

# 字符串

  • 字符串的声明通常用指针声明,可以避免再赋值空间不够程序崩溃
  • 字符串的声明:char str[10];/char *str = "hello world";
  • 字符串的初始化:char str[] = "hello world";
  • 打印字符串:printf("%s", str);
  • 分割原理:字符串的最后一个多一位\0,用来表示字符数组结束
  • 面试题:
char str[] = "jack";
char str2[4] = "rose";

printf("%s\n", str2); // rosejack
// 原因:rose只被分配了4个字节,没位置存\0,jack被读取了
1
2
3
4
5
  • 以字符数组的形式赋值:char str[] = { 'j', 'a', 'c', 'k', '\0' };
  • 获取字符串长度:
char str[10] = "jack";
unsigned long len = strlen(str);
printf("欢迎你:%s\n名字长度:%lu\n", str, len);
1
2
3
  • 输入字符串:scanf("%s", str);
    • 输入字符串时,如果输入的字符串长度大于数组长度,则会出现越界,程序会崩溃
    • 输入空格会被认为结束字符串,不会被存储到数组中
  • 常用函数
    • <stdio.h>
      • puts(str);:打印字符串
      • gets(str): 从键盘输入字符串,并返回字符串长度,也是超出长度会崩溃
    • <string.h>
      • strlen(str):获取字符串长度
      • strcpy(str1, str2):将字符串str2复制到str1中,修改str1长度不够运行会崩溃
      • strcat(str1, str2):将字符串str2连接到str1的末尾,修改str1长度不够运行会崩溃
      • strstr(str1, str2),在一个字符串中查找另一个字符串首次出现的位置
      • strcmp(str1, str2):比较字符串str1str2
        • 返回值:完全相等返回0
      • fputs()/fopen/fclose: 将字符串str写入文件/控制台
        • 输出到标准输出流
          char* str = "hello world";
          // 输出到标准输出流
          fputs(str, stdout);
          
          1
          2
          3
        • 输出到文件流
          /**
           * 文件打开,用一个file指针指向文件
           * fopen第二个参数
           * w: 写入
           * r: 读取
           * a: 追加
           */
          FILE *fp = fopen(
              "/Users/dreamarts/Documents/coderhdy/c-study/00_base/12_指针/test.txt",
              "w");
          /** 输入 */
          fputs(str, fp);
          /** 关闭读取文件 */
          fclose(fp);
          
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
      • fgets():从文件流中读取字符串,返回读取的字符串长度。安全,推荐用法
        • 读取标准输入流数据
          char str[5];
          printf("请输入:\n");
          fgets(str, sizeof(str), stdin);
          
          // 输入的最后一位可能是\n,在字符串长度不够的时候(5 - 1 = 4)
          unsigned long len = strlen(str);
          if (str[len] == '\n') {
              str[len] = '\0';
          }
          
          printf("%s\n", str);
          
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
        • 读取文件流数据
          FILE* fp = fopen("/Users/dreamarts/Documents/coderhdy/c-study/00_base/12_指针/test.txt", "r");
          char str[100];
          fgets(str, sizeof(str), fp);
          printf("%s\n", str);
          fclose(fp);
          
          1
          2
          3
          4
          5

# 指针

  • 指针变量声明:int *p;/int* p;/int * p;
  • 指针变量赋值:p = &a;
  • 指针变量取值:*p = 100;
  • 指针存NULL值,被访问时会报错
  • 形参指针修改:
    void add(int *pNum1, int *pNum2) {
        *pNum1 += *pNum2;
    }
    
    int main(int argc, const char *argv[]) {
        int num1 = 1;
        int num2 = 2;
        
        add(&num1, &num2);
        
        printf("num1 = %d\n", num1); // 3
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 声明二级指针:int **p = &pNum;二级指针只能存储一级指针的地址
  • 二级指针使用:*(*p)*p是二级指针指向的一级指针,*(*p)是二级指针指向的一级指针指向的值
  • 指针的加减法,单位是指针自身单位变量:p++
    • 如果指针指向的是数组,则p++会移动到下一个数组元素
      int arr[] = {1, 2, 3, 4, 5};
      int *p = &arr[0];
      for (int i = 0; i < 5; i++) {
          printf("第%d个元素是:%d\n", i, *(p + i)); // 遍历数组
      }
      
      1
      2
      3
      4
      5
      // 数组名也是指针
      int arr[] = {1, 2, 3, 4, 5};
      for (int i = 0; i < 5; i++) {
          printf("第%d个元素是:%d\n", i, *(arr + i)); // 遍历数组
      }
      
      1
      2
      3
      4
      5
    • 如果指针指向的是结构体,则p++会移动到下一个结构体成员。
  • 指针数组:是一个数组,数组中每个元素都是指针。例:int* p[3] = {&a, &b, &c};
  • 指针之间的减法:两个指针之间相差多少个元素。
    int arr[6]  = {1, 2, 3, 4, 5, 6};
    int* p1 = &arr[2];
    int* p2 = &arr[4];
    long diff = p2 - p1;
    printf("%ld\n", diff); // 2
    
    1
    2
    3
    4
    5

# 指针和字符变量

  • 全局变量的指针字符变量:都存储在内存的Data区中。
  • 局部变量的指针字符变量:字符串存储在内存的Data区中,字符串的结束符是\0
    // 局部变量:
    // 存储在栈中的字符串
    char str1[] = "jack";
    // 存储在Data区中的字符串
    char* str2 = "hello";
    
    1
    2
    3
    4
    5
  • 以指针方式存储的字符串无法通过下标修改
    char* str = "hello";
    str[0] = 'H'; // 报错
    
    char* str3 = "hello";
    str3 = "rose"; // 可以
    
    1
    2
    3
    4
    5
  • 声明指针字符串的时候,会从Date区寻找是否有相同的字符串,如果有则返回该字符串的指针,如果没有则创建一个字符串,并返回该字符串的指针。
    char* str1 = "hello";
    char* str2 = "hello";
    char* str3 = "hello";
    
    printf("str1 = %p \n", str1); // 相同地址
    printf("str1 = %p \n", str2); // 相同地址
    printf("str1 = %p \n", str3); // 相同地址
    
    printf("str1 = %s \n", str1); // hello
    printf("str1 = %s \n", str2); // hello
    printf("str1 = %s \n", str3); // hello
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • 等价:str[i] == *(str + i)
    char* str = "asdfghasdfgasdfgh";
    int count = 0;
    
    int i = 0;
    while (*(str + i) != '\0') {
        if (*(str + i) == 'a') {
            count++;
        }
        i++;
    }
    
    // 等价:str[i] ==  *(str + i)
     while (str[i] != '\0') {
        if (str[i] == 'a') {
            count++;
        }
        i++;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
  • 存储多个字符串,建议使用指针数组,可以存储不固定长度的字符串
char* str[] = {"hello", "world", "rose"};
for (int i = 0; i < 3; i++) {
    printf("%s\n", str[i]);
}
1
2
3
4

# 指针和函数

  • 指针指向函数,可以调用
char* getWeekDay(int weekDayNum) {
    switch(weekDayNum) {
        case 1:
            return "Monday";
        case 2:
            return "Tuesday";
        case 3:
            return "Wednesday";
        case 4:
            return "Thursday";
        case 5:
            return "Friday";
        case 6:
            return "Saturday";
        case 7:
            return "Sunday";
        default:
            return "错误值";
    }
}

int main(int argc, const char * argv[]) {
    // 指向外部函数的指针
    // 函数返回值 (*指针函数名)(参数)
    char* (*getWeekDayFn)(int weekDayNum) = getWeekDay;
    // 通过指针调用函数
    char* weekDay = getWeekDayFn(4);
    printf("%s\n", weekDay);
    return 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

# 内存分区

  • 栈:存储局部变量,函数调用参数,函数返回地址,函数返回值。
  • 堆:存储动态分配的内存。允许程序员动态分配内存。
  • Bss区:存储未初始化的全局变量,静态变量,常量。
  • Data区:存储已经初始化的全局变量,静态变量,常量,字符串
  • Text区:存储代码。

  • 字符串存储在Data区中,字符串的结束符是\0。所以函数可以返回字符串
  • 但是不能修以字符指针存储在Data区中的改字符串。
char* getWeekDay(int weekDayNum) {
    switch(weekDayNum) {
        case 1:
            return "Monday";
        case 2:
            return "Tuesday";
        case 3:
            return "Wednesday";
        case 4:
            return "Thursday";
        case 5:
            return "Friday";
        case 6:
            return "Saturday";
        case 7:
            return "Sunday";
        default:
            return "错误值";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 内存使用

  • 申请内存:malloc()/calloc()/realloc()/free()
  • malloc():申请内存,返回内存地址,失败返回NULL,没申请到会有NULL,使用完后需要释放。
    #include <stdlib.h>
    
    int *p = malloc(sizeof(int) * 10);
    if (p == NULL) {
        printf("内存申请失败");
        return 1;
    }
    for (int i = 0; i < 10; i++) {
        p[i] = i + 1;
    }
    for (int i = 0; i < 10; i++) {
        printf("%d ", p[i]);
    }
    //释放
    free(p);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • calloc()常用,申请内存,返回内存地址,失败返回NULL并且初始化内存为0
    #include <stdlib.h>
    
    // calloc使用
    int *p = calloc(10, sizeof(int));
    if (p == NULL) {
        printf("内存申请失败");
        return 1;
    }
    for (int i = 0; i < 10; i++) {
        p[i] = i + 1;
    }
    for (int i = 0; i < 10; i++) {
        printf("%d ", p[i]);
    }
    //释放
    free(p);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  • realloc():(扩容)重新分配内存,返回内存地址,失败返回NULL
    #include <stdlib.h>
    
    int *p = calloc(10, sizeof(int));
    int *p1 = realloc(p, 11);
    if (p1 == NULL) {
        printf("内存申请失败");
        return 1;
    }
    for (int i = 0; i < 11; i++) {
        p1[i] = i + 1;
    }
    for (int i = 0; i < 11; i++) {
        printf("%d\n", p1[i]);
    }
    //释放
    free(p1);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  • free():释放内存,释放内存地址,释放内存后,内存地址变为NULL

# static和extern

  • 如果要声明全局变量,必须使用static/extern修饰。
  • 声明全局变量要声明在.h文件中,实现在.c文件中。
  • 区别:
    • static只有当前模块能访问的变量。
    • extern是外部变量,是全局变量全局变量所有源文件都能访问的变量
extern int num = 10; // 外部能直接操作
static int age = 20; // 外部只能通过本文件的函数访问操作

int getAge() {
    return age;
}
void addAge() {
    age++;
}
1
2
3
4
5
6
7
8
9

# static与malloc

  • static:静态变量,静态函数,静态代码块,静态方法,静态成员变量,静态成员函数。
  • static修饰全局变量:让全局变量变成只有源文件能访问的变量
  • static修饰局部变量:让局部变量变成只有函数能访问的变量,第一次访问初始化,后续访问复用。

static和malloc的区别

  • calloc可以初始化内存为0
  • 生命周期:
    • static:全局变量,函数,代码块,成员变量,成员函数,生命周期是整个程序。
    • malloc:动态分配内存,生命周期是可以由程序员调用free释放掉

# 结构体

  • 结构体:一种特殊的自定义的数据类型,可以存储多个变量,变量类型可以不同。
  • 结构体变量赋值形式是拷贝成员变量的值进行赋值
  • 初始化语法
    struct Student {
        char* name;
        int gender;
        float height;
    };
    
    int main(int argc, const char * argv[]) {
        struct Student ming;
        ming.name = "小明";
        ming.gender = 1;
        ming.height = 1.75;
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 创建结构体时初始化
    struct Student {
        char* name;
        int gender;
        float height;
    } ming,hong,fang;
    
    1
    2
    3
    4
    5
  • 简易初始化
    struct Student ming = {"小明", 1, 1.75};
    
    1
    struct Student ming = {
        .name = "小明",
        .gender = 1,
        .height = 1.75
    };
    
    1
    2
    3
    4
    5
  • 创建结构体指针


     

     


    struct Student fang = {"小芳", 0, 1.65};
    struct Student* pStu = &fang;
    // 指针访问有简写
    (*pStu).name = "小方";
    // 简写
    pStu -> height = 1.70;
    
    1
    2
    3
    4
    5
    6

# 枚举

  • 枚举值的命名规范:以枚举类型名称开头,单词首字母大写
enum WeekDay {
    WeekDayMonday = 1,
    WeekDayTuesday,
    WeekDayWednesday,
    WeekDayThursday,
    WeekDayFriday,
    WeekDaySaturday,
    WeekDaySunday
}

enum Gender {
    Male = 1,
    Female = 2
}

Gender gender = Male;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# typedef

  • 作用:定义类型别名,方便使用,减少代码量,提高代码可读性。
  • 常用简写,为匿名结构体/枚举重命名
// 结构体
typedef struct {
    int age;
    char* name;
} Student;
Student xiaoming = {19, "小明"};

// 枚举
typedef enum {
    DirectionEast,
    DirectionWest
} Direction;
Direction d1 = DirectionEast;
1
2
3
4
5
6
7
8
9
10
11
12
13
上次更新: 4/24/2025