明解C语言入门篇笔记

203 阅读46分钟
  • strerror 和 perror
char *strerror(int __errnum)
返回错误码对应的错误信息的字符串的起始地址

void     perror(const char * str)
将 errono 中对应的错误信息打印出来, 先打印 str 指向的字符串信息,再打印错误码对应的错误信息
perror = printf + strerror

  • const char * strstr ( const char * str1, const char * str2 )

从 str1 中查找 str2 第一次出现的位置,如果找到了,则返回第一次出现的起始地址;如果找不到就返回 NULL

char str1[] = "abcdefabcdef";
char *str2 =  "efab";
char *ret = strstr(str1,str2);
if (ret != NULL) {
    //输出: ret的值为efabcdef
    printf("found %s at position:%ld, result:%s\n",str2, (ret - str1),ret);
}else {
    printf("%s not found in %s\n",str2,str1);
}
  • strtok
char *strtok(char *str, const char *delim);
`strtok` 是 C 标准库中的一个函数,用于将字符串分割成多个子字符串(称为“标记”或“token”)。它通常用于解析由特定分隔符分隔的字符串,例如逗号分隔的 CSV 文件或空格分隔的命令行输入。

char str[] = "hello,你好呀,this is a test@sina.com";
char buffer[256] = {0};
char *sep = ",@";
//因为会修改源字符串,所以临时拷贝一份
strcpy(buffer,str);

char *token = strtok(buffer,sep);
while (token != NULL) {
    printf("%s\n",token);
    token = strtok(NULL,sep);
}
//输出 hello
printf("buffer:%s",buffer);


image.png

  • printf
//默认情况下,如果没有指定 02,时,会输出 51 日,但是加上后,如果数字的位数不足 2 位,会在数字前面补0, 如果数字本身已经是 2 位了,则不会补 0
printf("%02d 月 %02d 日",5,1);


  • 读写二进制文件, fwrite 和读取: fread 实数
void save_read_binary_data_by_fwrite_fread() {
        double pi = 3.1415926;
        FILE *fp;
        char *filename = "PI.bin";
        if ((fp = fopen(filename, "wb")) == NULL) {
            printf("文件打开失败\n");
            return;
        }
        fwrite(&pi, sizeof(double), 1, fp);
        fclose(fp);
        if ((fp = fopen(filename, "r")) == NULL) {
            printf("文件打开失败\n");
            return;
        }
        fread(&pi, sizeof(double), 1, fp);
        printf("从文件中读取到的pi 为:%lf\n", pi);
        fclose(fp);
    }
  • 保存 : fprintf 和读取: fscanf 实数
void save_read_real_num() {
    FILE  *fp;
    double pi = 3.1415926535897;
    char *filename = "PI.txt";
    if ((fp = fopen(filename, "w")) == NULL) {
        printf("写入文件打开失败\n");
        return;
    }
    //将 pi 的值写入 PI.txt
    fprintf(fp, "%lf", pi);
    fclose(fp);
    if ((fp = fopen(filename, "r")) == NULL) {
        printf("读取文件打开失败\n");
        return;
    }
    double result = 0.0;
    //从 fp 中读取值,存放到 result 中
    fscanf(fp,"%lf", &result);
    printf("从文件读取到的值为:%.10lf",result);
    fclose(fp);
}
  • 拷贝文件
int ch;
FILE *sfp;
FILE *dfp;
char sname[] = FILE_NAME;
char dname[] = "copy_dat";

if ((sfp = fopen(sname, "r")) == NULL) {
    printf(" 源文件打开失败\n");
    return;
}
if ((dfp = fopen(dname, "w")) == NULL) {
    printf("目标文件打开失败\n");
    return;
}
while ((ch = fgetc(sfp)) != EOF) {
    fputc(ch,dfp);
}
fclose(dfp);
  • 读取文件内容
int ch;
FILE *fp;
//打开文件
if ((fp = fopen(FILE_NAME, "r")) == NULL) {
    printf("文件打开失败\n");
}else {
    printf("读取的文件内容:");
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }
    fclose(fp);   //关闭文件
}
  • 获取当前年月日时分秒
time_t current = time(NULL);
struct tm *timer = localtime(&current);
    
printf("%d %d %d %d %d %d\n",
        timer->tm_year + 1900,
        timer->tm_mon + 1,
        timer->tm_mday,
        timer->tm_hour,
        timer->tm_min,
        timer->tm_sec
);
  • 将格式化内容写入到流
    void write_to_file() {
        FILE *fp;
        time_t current = time(NULL);
        struct tm *timer = localtime(&current);
        if ((fp = fopen("dt_dat", "w")) == NULL) {
            printf("文件打开失败\n");
        } else {
            printf("写出当前日期和时间\n");
            fprintf(fp, "%d %d %d %d %d %d\n",
                    timer->tm_year + 1900,
                    timer->tm_mon + 1,
                    timer->tm_mday,
                    timer->tm_hour,
                    timer->tm_min,
                    timer->tm_sec
            );
            fclose(fp);
        }
    }

  • fscanf 用法

/**
 * 从文件读取数据
 */
void read_from_file() {
    FILE *fp;
    const char *filename = "abc.txt";
    fp = fopen(filename,"r");
    char name[100];
    double height,weight;

    double hsum = 0.0;
    double wsum = 0.0;
    int count = 0;
    if (fp == NULL) {
        printf("无法打开文件:%s\n",filename);
        return;
    }
    printf("成功打开文件:%s\n",filename);
    //fscanf: 成功时,返回成功读取的数据项的数量;如果到达文件末尾或发生错误,返回 EOF。fscanf 相比进增加了第一个参数:输入流
    while (fscanf(fp, "%s%lf%lf",name,&height,&weight) == 3) {
        printf("name:%s,height:%.1f,weight:%.1f\n",name,height,weight);
        hsum += height;
        wsum += weight;
        count++;
    }
    printf("height 平均值为:%5.1f,weight 平均值为 %5.1f\n", hsum / count, wsum / count);
    fclose(fp);
}
  • 在 .c 文件同级目录下放置 abc.txt, 但是执行 fopen 时提示打开文件失败

char cwd[1024];
if (getcwd(cwd, sizeof(cwd)) != NULL) {
    printf("当前工作目录: %s\n", cwd);
} else {
    perror("getcwd() 错误");
}
FILE * fp;
const char *filename = "abc.txt";
fp = fopen(filename,"r");
if (fp == NULL) {
    printf("无法打开文件:%s\n",filename);
}else {
    printf("成功打开文件:%s\n",filename);
    fclose(fp);
}

问题原因是:程序运行时,当前工作目录可能不是你认为的目录。fopen 使用的是相对路径,依赖于程序的当前工作目录。

问题定位办法:使用 getcwd 打印当前工作路径,发现果然不是当前目录 解决办法:将文件放在工作目录或者是使用绝对路径

  • 标准流
1stdin - 标准输入流
用于读取普通输入的流,在大多数环境中为从键盘输入, scanf 与 getchar 等函数汇总这个流中读取字符

2stdout - 标准输出流
用于写入普通输出的流,在大多数环境中为输出至显示器页面, printfputsputchar 等函数会向着流写入字符

3stderr  -标准错误流

用于写出错误的流,在大多数环境中为输入至显示器界面


  • 结构体成员在内存空间上的排列顺序和成员声明的顺序一样, 函数不能返回数组,但是能返回结构体

  • 对结构体数组进行排序


typedef struct person {
    char name[20];
    int height;
    float weight;
} Person;

void sort_by_height(Person *p, int n) {
    qsort(p,n,sizeof(Person),compare_by_height);
}


Person arr[] = {
    {"zhangsan",178,61.2},
    {"lisi",183,61.2},
    {"wangwu",167,61.2}
};
int size = sizeof(arr) / sizeof(arr[0]);
// 排序前
printf("排序前:\n");
for (int i = 0; i < size; i++) {
    printf("姓名: %s, 身高: %d, 体重: %.1f\n", arr[i].name, arr[i].height, arr[i].weight);
}
sort_by_height2(arr,size);
printf("排序后:\n");
for (int i = 0; i < size;i++) {
    Person person = arr[i];
    printf("姓名:%s,身高:%d,体重:%.1fd\n", person.name, person.height, person.weight);
}

  • 结构体作为返回值
struct student struct_as_return_value(const char *name,int height, float weight,int schols) {
    struct  student stu;
    //error:stu.name 是一个字符数组,不能直接赋值给指针。需要使用 strcpy 或 strncpy 将字符串复制到 stu.name 中。
    // stu.name = name;
    // 复制姓名到 stu->name
    strncpy(stu.name,name,sizeof(stu.name)-1);
    //手动添加字符串结束符
    stu.name[sizeof(stu.name) -1] = '\0';
    stu.height = height;
    stu.weight = weight;
    stu.schols = schols;
}
  • 数组和结构体的区别
1、元素类型:数组用于高效地操作 "相同类型"数据的集合,而结构体这种数据结构通常用于高效地操作 “不同类型”数据的集合

2、可否赋值:即使两个数组的元素个数相同,也不能相互赋值。但是,相同类型的结构体可以相互赋值

struct student stu = {"lisi", 175,83.4f};

struct student  stu2 = stu;

3、是否能作为返回值: 由于数组不可进行赋值,所以不可用作函数的返回值类型;结构体可以作为返回值

  • 结构体指针作为参数
void struct_as_param(struct student *stu) {
    //访问结构体成员变量时,可以使用 a.b 访问属性
    if ((*stu).height < 180) {
        (*stu).height = 180;
    }
    //通过 结构体指针的 a -> b 访问属性
    if (stu->weight > 70) {
       stu->weight = 70;
    }
}
  • 结构体初始化
struct student stu1;
strcpy(stu1.name, "zhangsan");
stu1.height = 173;
stu1.weight = 73.5f;
stu1.schols = 100;
printf("stu1 姓名:%s,身高:%d,奖学金:%ld\n",stu1.name,stu1.height,stu1.schols);

struct student stu2 = {"lisi", 181,67.2f};
//schols 未被初始化,默认值为 0
printf("stu2 姓名:%s,身高:%d,体重:%f,奖学金:%ld\n",stu2.name,stu2.height,stu2.weight,stu2.schols);
  • 将字符串转换为大写并复制
/**
 * 将字符串转化为大写并复制
 * @param d
 * @param s
 * @return
 */
char *str_cpy_toupper(char *d, const char *s) {
    //存储目标字符串的起始地址
    char *tmp = d;
    while (*s) {
      *d++ =(char) toupper(*s++);
    }
    return tmp;
}
  • 将字符串括起来显示
/**
 * 将字符串 str 括起来显示
 * @param str
 */
void put_str(const char *str) {
    putchar('"');
    while (*str) {
        putchar(*str++);
    }
    putchar('"');
}
  • 因为无法保证 "字符串字面量" 被保存在能够改写的空间中,所以不要对该空间和其前后的空间进入写入操作

  • 计算字符串数组大小

//使用这种方式定义的数组,所有字符(二维数组的构成元素)都被保存在连续的内存空间中

char lan_arr[][10] = {"Java", "Koltin", "C++"};
// 计算行数和列数
size_t rows = sizeof(lan_arr) / sizeof(lan_arr[0]);  // 行数
size_t cols = sizeof(lan_arr[0]);                   // 每行的大小

printf("行数: %zu\n", rows);  // 输出: 3
printf("每行大小: %zu\n", cols);  // 输出: 10
printf("总大小: %zu\n", sizeof(lan_arr));  // 输出: 30


//无法保证个字符串被保存在连续的空间中
char *arr2[] = {"Java", "Koltin", "C++"};
//sizeof(arr2) 计算的是指针数组本身的大小,而不是字符串常量的总大小。在 64 位机器上占用的内存空间为 3 × 8 = 24;在 32 位机器上占用的内存空间为 3 × 4 = 12
printf("arr2 总大小: %zu\n", sizeof(arr2)); // 输出: 30

  • 字符串定义方式
1、用数组实现:  `a` 是一个字符数组,编译器会为它分配足够的内存来存储字符串 `"CIA"` 和结尾的 `\0`。-   内存大小为 `4` 字节(`'C'``'I'``'A'``'\0'`)。`a` 是可修改的,可以通过下标或指针修改数组中的字符。

char a[] = "CIA"

2、用指针实现,末尾会自动添加 "\0"
char *p = "FBI"
因为字符串字面量会被解释为指向第一个字符的指针,所以指针 p 会被初始化为指向字符串字面量 "FBI" 的第一个字符 'F',尝试修改字符串常量会导致未定义行为(通常是程序崩溃)。字符串字面量以及指向它的指针,二者都占用内存空间,

两者的区别:

image.png

  • 如何突破 char *a 指向的字符串不可修改性:使用动态内存分配方式
char *a = malloc(4 * sizeof(char));
strcpy(a, "FBI");
a[0] = 'G';  // 合法
free(a);     // 释放内存
  • 表示字符串数组的一个方法是使用 "用数组实现的字符串"的数组
char str[][5] = {"Java", "Koltin", "C++"};
  • 删除字符串中的所有数字,例如,参数为 AB1C9, 返回 ABC
/**
 * 不使用索引方式删除字符串中的数字
 * @param str
 */
void del_digit2(char *str) {
    if (str == NULL) {
        return;
    }
    //记录起始地址
    char *dest = str;
    while (*str) {
        if (!isdigit(*str)) {
            *dest++ = *str;
        }
        //移动到下一个字符
        str++;
    }
    //添加字符串结束符
    *dest = '\0';
}





/**
 * 删除字符串中的数字,并返回一个新的字符串
 * 注意:调用者需要负责释放返回的字符串
 * @param str
 */
char * del_digit(const char *str) {
    if (str == NULL) {
        return str;
    }
    size_t len = strlen(str);
    size_t new_len = 0;
    for (size_t i = 0;i < len;i++) {
        if (!isdigit(str[i])) {
            new_len++;
        }
    }
    //+1 用于 \0
    char *ret = calloc(new_len + 1, sizeof(char));
    if (ret == NULL) {
        printf("Failed to allowcate memory\n");
        return NULL;
    }
    //复制非数字字符到结果字符串
    size_t j = 0;
    for (size_t i = 0; i < len;i++) {
        if (!isdigit(str[i])) {
            ret[j++] = str[i];
        }
    }
    //返回结果字符串
    return ret;
}




char * str2 = "AB1C9";
char* del_digit_ret = del_digit(str2);
if (del_digit_ret != NULL) {
    printf("原始字符串:%s, 删除后的字符串:%s\n",str2,del_digit_ret);
    free(del_digit_ret);
}else {
    printf("内存分配失败\n");
}

char str3[] = "AB1C9";

//不能传 str2 这种字符串字面量方式,因为字符串字面量不能修改
//del_digit2(str2);
del_digit2(str3);
printf(" 删除数字后的字符串:%s\n",str3);


  • 字符串转换
atoi、atol、atof 分别是把字符串转换为 intlongfloat 等类型,但是不推荐使用,原因
1、缺乏错误处理: 
如果字符串包含非数字字符(如 "123abc"),atoi 会忽略这些字符并返回部分转换结果(如 123)。

如果字符串完全无法转换(如 "abc"),atoi 会返回 0,但无法区分是转换失败还是字符串本身就是 "0"2、不处理溢出:

如果字符串表示的整数超出 int 的范围(如 "2147483648",大于 INT_MAX),atoi 的行为是未定义的。

3、无法检测无效输入:

atoi 不会告诉你输入是否有效,因此在实际开发中容易隐藏潜在的错误。

更推荐用 strtol 的方式来替代 atol:


char str[128];
printf("请输入字符串:");
scanf("%s", str);
int num = 0;

char *endptr;

num = strtol(str, &endptr,10);
if (errno == ERANGE) {
    printf("转换失败,超出范围\n");
}else if (endptr == str) {
    printf("转换失败,无法输入\n");
}else if (endptr != '\0') {
    printf("转换部分成功,无效字符从 %s 开始\n",endptr);
}else {
    printf("转换成功:%ld\n",num);
}


  • 字符串比较

/**
 * 比较两个字符串大小关系
 * @param s1
 * @param s2
 * @return
 */
int my_strcmp(const char* s1, const char* s2) {
    if (s1 == NULL || s2 == NULL) {  // 检查空指针
        return -1;  // 如果任一字符串为空指针,返回 -1
    }

    // 逐个字符比较
    while (*s1 == *s2) {
        if (*s1 == '\0') {  // 如果到达字符串末尾
            return 0;       // 字符串相等
        }
        s1++;
        s2++;
    }
    // 返回字符差值
    return (unsigned char)*s1 - (unsigned char)*s2;
}





int my_strncmp(const char* s1, const char* s2, int n) {
    if (s1 == NULL || s2 == NULL || n < 0) {  // 检查空指针和长度
        return -1;  // 如果任一字符串为空指针或 n 为负数,返回 -1
    }

    if (n == 0) {  // 如果 n 为 0,直接返回 0
        return 0;
    }

    // 逐个字符比较
    while (n--) {
        if (*s1 != *s2) {  // 如果字符不匹配
            return (unsigned char)*s1 - (unsigned char)*s2;
        }
        if (*s1 == '\0') {  // 如果到达字符串末尾
            return 0;
        }
        s1++;
        s2++;
    }

    return 0;  // 前 n 个字符相等
}

  • 字符串拼接
char *my_strcat(char *s1, const char *s2) {
    if (s1 == NULL || s2 == NULL) {  // 检查空指针和长度
        return s1;
    }
    //记录起始地址
    char *tmp = s1;
    while (*s1) {
        s1++;
    }
    while (*s1++ = *s2++) {
       ;
    }
    return tmp;
}



/**
 * 限定字符个数的字符串拼接
 * @param s1
 * @param s2
 * @param n
 * @return
 */
char* my_strncat(char* s1, const char* s2, int n) {
    if (s1 == NULL || s2 == NULL || n <= 0) {  // 检查空指针和长度
        return s1;
    }

    char* tmp = s1;  // 保存 s1 的起始地址

    // 找到 s1 的末尾
    while (*s1) {
        s1++;
    }

    // 将 s2 的前 n 个字符复制到 s1 的末尾
    while (n-- && (*s1++ = *s2++)) {
        ;
    }

    *s1 = '\0';  // 确保字符串以 \0 结尾

    return tmp;  // 返回 s1 的起始地址
}

  • 限定复制字符串的函数

/**
 * 限定复制字符串函数
 * @param dest  
 * @param src 
 * @param n 
 * @return 
 */
char *my_strncpy(char *dest, const char *src, size_t n) {
    if (dest == NULL || src == NULL) {
        return NULL;
    }
    // 保存 s1 的起始地址
    char *tmp = dest;
    while (n >0) {
        //遇到 \0 就结束循环
        if (!(*dest++ = *src++)) {
            break;
        }
        n--;
    }
    //用 \0 补充剩余的部分
    while (n--) {
        *dest++ = '\0';
    }
    return tmp;
}
  • 不使用下标运算符,编写如下函数,若字符串 str 中含有字符 c(若含有多个,以先出现的为准),则返回指向该字符的指针;否则返回空指针

/**
 * 若字符串 str 中含有字符 c(若含有多个,以先出现的为准),则返回指向该字符的指针;否则返回空指针
 * @param str
 * @param c
 * @return
 */
char *str_chr(const char *str, int c) {
    while (*str) {
        if (*str == c) {
            return (char*)str;
        }
        str++;
    }
    return NULL;
}


char* result = str_chr(str, target);
if (result != NULL) {
    printf("字符 '%c' 在字符串 "%s" 中的位置是: %ld,结果是:%s\n", target, str, result - str,result);
} else {
    printf("字符 '%c' 不在字符串 "%s" 中。\n", target, str);
}

  • 计算某个字符在字符串中出现的次数
int  get_char_count(const char *str, int c) {
    int count = 0;
    while (*str) {
        if (*str == c) {
            count++;
        }
        str++;
    }
    return count;
}


char target = 'l';
int count = get_char_count(str, target);
printf("字符:%c 在 %s 中出现的次数为:count:%d",target,str,count);

  • 显示字符串
void put_string(const char *s) {
    while (*s) {
        // printf("%c",*s);
        // 以上 printf 和 putchar 都可以输出字符
         putchar(*s);
        s++;
    }
    printf("\n");
}
  • 错误的字符串复制
/**
 * 错误的字符串复制
 */
void wrong_copy_string() {
    char *ptr = "1234";
    char tmp[128];
    printf("ptr:%s\n",ptr);
    //tmp 未初始化,输出的是未定义的内容。
    printf("复制的是:%s", tmp);
    //scanf("%s", tmp); 不会检查输入的长度,如果用户输入的字符串长度超过 127 个字符(不包括 \0),会导致缓冲区溢出。
    scanf("%s",tmp);
    //字符串常量存储在只读内存段,尝试修改它会导致未定义行为(通常是程序崩溃)。
    str_copy(ptr,tmp);
    puts("复制了。");
    printf("ptr:%s\n",ptr);
}
存在的问题:
//tmp 未初始化,输出的是未定义的内容。
1printf("复制的是:%s", tmp);
2scanf("%s", tmp); 不会检查输入的长度,如果用户输入的字符串长度超过 127 个字符(不包括 \0),会导致缓冲区溢出
3str_copy(ptr,tmp);  修改了 ptr 指向的字符串字面量的内容

修正后的代码


/**
 * 正确的字符串复制
 */
void right_copy_string() {
    char tmp[128] = {0};
    char dest[128] = {0};

    printf("请输入一个字符串:");
    //使用 fgets 读取带空格的字符串
    fgets(tmp,sizeof(tmp),stdin);
    //去掉 fgets 读取的换行符
    //size_t strcspn(const char *str, const char *reject);用于计算字符串中不包含指定字符集的初始段的长度
    tmp[strcspn(tmp, "\n")] = '\0';

    printf("你输入的是:%s\n",tmp);
    str_copy(dest,tmp);

    puts("复制完成。");
    printf("dest:%s\n",dest);
}

  • 字符串相关 api 模拟
/**
 * 字符串拷贝
 * @param dest
 * @param src
 * @return
 */
char * str_copy(char *dest,const char *src) {
    assert(dest);
    assert(src);
    //记录 dest 的起始地址
    char * t = dest;
    while (*dest++ = *src++)
        ;
    return t;
}



char * str_copy2(char *dest, const char *src) {
    assert(dest);
    assert(src);
    char * t = dest;
    int i = 0;
    //dest[i] 等价于 *(dest+i), src[i] 等价于 *(src+i)
    while (dest[i] = src[i]) {
        i++;
    }
    return t;
}


char dest [20];
char * copyResult = str_copy(dest,"ABC");
printf("copyResult:%s\n",copyResult);

copyResult = str_copy2(dest,"Hello");
printf("copyResult2:%s\n",copyResult);

  • 对于指针 p 而言, p++ 即 p = p +1, p-- 即 p = p -1

  • 分析以下代码输出结果

char * ptr = "ABC";
//指针指向字符串首地址 4 所在的位置, 指针 +1 指向字符 '5', 所以输出 56 
ptr = "456" + 1;
//输出:56
printf("ptr:%s\n",ptr);
  • 数组实现的字符串和指针实现的字符串区别

image.png

 // 方式 1: 字符数组
    char str1[] = "ABC";
    str1[0] = 'X';  // 合法
    printf("str1: %s\n", str1);  // 输出: XBC

    // 方式 2: 字符指针
    char *str2 = "ABC";
    // str2[0] = 'X';  // 非法,可能导致程序崩溃
    printf("str2: %s\n", str2);  // 输出: ABC
    
    
    
    str[0] = 'H';           //ok: 可以修改数组中的字符
    printf("str:%s\n",str);

    //非法,可能导致程序崩溃
    ptr[2] = "H";
    printf("ptr2:%s\n",ptr);

    
    
    ### 注意事项

1.  **字符串常量的不可修改性**:

    -   使用 `char *str = "ABC"` 时,务必注意不要尝试修改字符串内容,否则会导致未定义行为。

1.  **数组与指针的区别**:

    -   `char str[] = "ABC"` 是数组,`sizeof(str)` 返回数组的大小(包括 `\0`)。
    -   `char *str = "ABC"` 是指针,`sizeof(str)` 返回指针的大小(通常是 48 字节,取决于系统)。

1.  **灵活性**:

    -   如果需要修改字符串内容,必须使用 `char str[] = "ABC"`。
    -   如果字符串内容固定且不需要修改,可以使用 `char *str = "ABC"`。
    
1.  **占用内存**:
     -  `char str[] = "ABC"` 的内存占用更低,因为它只存储字符串内容:4 个字节
     -   `char *str = "ABC"` 的内存占用更高,因为除了存储字符串字面量以外,它需要额外的内存来存储指针变量(4或者8个字节)
-   如果需要节省内存,且字符串内容不需要修改,优先使用 `char str[] = "ABC"`。
  • 用数组和指针实现的字符串
char str[] = "ABC"; //用数组实现的字符串
char * ptr = "123"; //用指针实现的字符串

char *ptr2 = {'1','2','3'}; //error: 数组用的 {} 形式的初始值,不可用于单一的变量

printf("str:%s\n",str);
printf("ptr:%s\n",ptr);
  • 指针 p 指向数组中的元素 e 时, 指向元素 e 后第 i 个元素的 *(p + i),可以写为 p[i], 指向元素 e 前第 i 个元素的 *(p - i),可以写为 p[-i],

  • 数组名一般情况下会被解释为指向该数组起始元素的指针,以下两种情况例外:

1、作为 sizeof 运算符的操作数出现时
2、作为取址运算符 & 的操作数出现时,这种情况下,数组名不是指向起始元素的指针,而是指向数组整体的指针
  • 作为函数参数的指针
void pointer_as_param(int *height) {
    if (*height < 180) {
        *height = 180;
    }
}

int zhangsan = 185;
int lisi = 175;
//zhangsan:185 lisi:175

printf("before zhangsan:%d,lisi:%d\n",zhangsan,lisi);
pointer_as_param(&zhangsan);
pointer_as_param(&lisi);
//zhangsan:185 lisi:180
printf("after zhangsan:%d,lisi:%d",zhangsan,lisi);




void sum_diff(int x,int y, int *sum, int *diff) {
    *sum = x + y;
    *diff =  x- y;
}


void swap(int *pa, int *pb) {
    int temp = *pa;
    *pa = *pb;
    *pb = temp;
}


void sort(int *pa, int *pb) {
    if (*pa < *pb) {
        swap(pa,pb);
    }
}

/**
 * 将 pa、pb、pc 指向的 3 个 int 型整数按升序排列
 * @param pa
 * @param pb
 * @param pc
 */
void sort2(int *pa, int *pb,int *pc) {
    if (*pa > *pb) {
        swap(pa,pb);
    }
    if (*pa > *pc) {
        swap(pa,pc);
    }
    if (*pb > *pc) {
        swap(pb,pc);
    }


}


  • 指针
int a = 10;
printf("before a:%d\n", a);

//指针 pa 的值为 a 的地址,一般说 pa 指向 a, 可以把 *pa 理解为 a 的别名
int *pa = &a;

*pa = 20;
printf("after a:%d\n", a);
  • 字符串总结

1、字符串字面量的末尾是 null 字符,因此,字符串字面量 "ABC"实际占用了 4个字符的内存空间,一个字符也没有的字符串字面量 ""占用 1 个字节. 2、字符串字面量的长度,和包括末尾的 null 字符在内的字符数的值,可以通过 sizeof 求得 3、字符串字面量具有静态存储器,因此它 "活在"从程序开始到结束的整个生命周期内

  • 逐个显示字符串中的字符
void display_str(const char s[][6], int n) {
    for (int i = 0; i < n;i++) {
        printf("s[%d]:",i);
        int j = 0;
        while (s[i][j]) {
            putchar(s[i][j++]);
        }
        puts(""");
    }
}

char cs[][6] = {"zhangsan","lisi","wangwu"};
display_str(cs,3);
  • 非字符串的字符数组
char str[4] = {'A','B','C','D'};
这种声明不会被当做字符串,我们把它当做 4 个字符的集合,也就是普通的数组来使用
  • 以下代码是否有错误
/**
 * 
 * 当函数返回时,result 数组的内存会被释放,因此返回的指针将指向无效的内存地址。
 * @param str 
 * @return 
 */
char* str_toupper(const char *str) {
    int i = 0;
    char result[128] = {0};
    while (str[i]) {
        result[i] = toupper(str[i]);
        i++;
    }
    return result;
}

result 是一个局部数组,其生命周期仅限于 str_toupper 函数内部。

  • 不修改源字符串的条件下返回转换后的字符串

/**
 * 不修改源字符串的条件下返回转换后的字符串
 * @param str 
 * @return 
 */
char* str_toupper3(const char *str) {
    assert(str);
    size_t len = strlen(str);
    char *result = malloc(len + 1);
    if (result == NULL) {
        printf("内存分配失败\n");
        return NULL;
    }
    for (size_t i =0;i < len;i++) {
        result[i] =  toupper((unsigned char)str[i]);
    }
    result[len] = '\0';
    return result;
}


const char *greeting = "Hello World";
char *upper_str = str_toupper3(greeting);
if (upper_str != NULL) {
    printf("Original:%s\n",greeting);
    printf("Uppercase:%s\n",upper_str);
    free(upper_str);
}else {
    printf("内存分配失败或输入为空\n");
}

  • 大小写字符转换
void str_toupper(char *str) {
    int i = 0;
    while (str[i]) {
        str[i] = toupper(str[i]);
        i++;
    }
}


void str_lower(char *str) {
    int i = 0;
    while (str[i]) {
        str[i] = tolower(str[i]);
        i++;
    }
}


char str[] = "hello World";
str_toupper(str);
printf("after to upper:%s\n",str);

str_lower(str);
printf("after to lower:%s\n",str);

  • 统计字符串中字符显示次数
/**
 * 统计字符串中字符串出现的次数
 * @param str   字符串
 * @param count 统计字符分布数组
 */
void str_dcount2(const char *str, int *count) {
    int i = 0;
    while (str[i]) {
        if (str[i] >= '0' && str[i] <= '9') {
            count[str[i] - '0']++;
        }
        i++;
    }
}
  • 使字符串显示 n 次
/**
 * 将字符串 s 显示 n 次
 * @param s 
 * @param n 
 */
void put_string(const char *s, int n) {
    for (int i = 0; i < n;i++) {
        printf("%s",s);
    }
}
  • 模拟获取字符串长度

/**
* 采用计数器
* @param str 
* @return 
*/
size_t my_str_len(const char *str) {
 size_t len = 0;
 while (str[len]) {
     len++;
 }
 return len;
}

/**
* 指针相减的方式
* @param str
* @return
*/
size_t my_str_len2( char *str) {
 char *start = str;
 while (*str) {
    str++;
 }
 return str - start;
}

/**
* 采用递归计算
* @param str 
* @return 
*/
size_t my_str_len3(const char *str) {
 if (str == NULL || *str == '\0') {  // 检查空指针或空字符串
     return 0;
 }
 return 1 + my_str_len3(str + 1);  // 递归调用
}
  • 读取字符串数组中的字符串
//没有初始值,元素个数 3 不可省略
char str[3][128];
int size = 3;

for (int i = 0; i < size; i++) {
    printf("s[%d] :", i);
    //由于 `str[i]` 本身就是一个 `char*` 类型的指针(指向 `char[128]` 数组的首元素, 因此这里 str[i] 不需要加取地址符 &
    scanf("%s", str[i]);
}
for (int i = 0; i < size; i++) {
    printf("str[%d]:%s", i, str[i]);
}
  • 字符串数组

字符串可以用数组来表示,所以字符串的集合也可以用数组的数组来表示

//cs 是 3 行 6 列的二维数组(元素类型为 char[6]类型, 元素个数为 3),3个元素cs[0]、cs[1]、cs[2]分别初始化为字符串
//"zhangsan"、"lisi"、"wangwu"
char cs[][6] = {"zhangsan", "lisi", "wangwu"};
int size = sizeof(cs) / sizeof(cs[0]);
for (int i = 0; i < size;i++) {
    printf("cs[%d]:%s\n",i, cs[i]);
}
  • 转换说明符结构
%8.2s

8:输出最小宽度,表示至少要输出指定的位数,如果省略本项或实际输出的字符串位数超过指定值,则按实际位数输出。
如果设置了 - 标记,表示左对齐,否则表示右对齐(空白部分填补空格)

2:精度,指示显示位数的上限(即不可能显示超过指定位数的字符,超过则截去)

s:转换说明符, s 表示输出字符串,即输出数组的字符,直到 null 字符的前一个字符为止,如果没有指定精度或精度大于数组长度,则数组中必须含有 null 字符
  • 格式化显示字符串
void format_str() {
    char str[] = "12345";
    printf("%s\n",str);   //原样输出                : 12345
    printf("%3s\n",str);  //至少显示3位             : 12345
    printf("%.3s\n",str); //最多显示3位             : 123
    printf("%8s\n",str); //至少显示8位,右对齐       :    12345
    printf("%-8s\n",str); //至少显示8位,左对齐      : 12345
}
  • 字符串字面量 像 "ABC" 那样带双引号的一系列字符称为 "字符串字面量",字符串自变量的末尾会加上一个叫做 null 字符就是 '\0'
printf("%zu\n",sizeof("ABC")); //输出值是 4, 因为在末尾会自动添加 "\0"
  • 字符数组的初始化
char str[] = {'A','B','C','\0'}; //方式一  ok
char str[] = {"ABC"};           //方式二   ok



char str3[4];
str3 = {'A','B','C'}; //error 不能赋初始值
str3 = "ABC"; //error 不能赋初始值

  • 枚举名不是类型名, "enum 枚举名"才是类型名

  • 统计字符出现次数

void char_count() {
    printf("请输入一个字符串(包含数字字符):\n");
    int cn;
    int cnt[10] = {0};  // 初始化数字字符计数器

    // 读取输入字符并统计数字字符
    while ((cn = getchar()) != '\n' && cn != EOF) {
        if (cn >= '0' && cn <= '9') {
            cnt[cn - '0']++;  // 将字符转换为索引并计数
        }
    }

    // 输出统计结果
    puts("数字字符出现的次数:");
    for (int i = 0; i < 10; i++) {
        printf("%d: 出现的次数是: %d\n", i, cnt[i]);
    }
}
  • 从输入流读取数据
int ch;
while ((ch = getchar()) != EOF ) {
    putchar(ch);
}

上面代码,用户在控制台输入什么,就会输出什么

  •  枚举常量
enum day{
    RED,
    BLUE = 5,
    BLACK = 10,
    WHITE,
};

枚举的值可以根据需要设置,只要在枚举变量的名称后面写上赋值运算法 "="和值就行了,没有给定值的枚举常量,其值为前一个枚举常量加1

  • 函数式宏和逗号运算符
#define puts_alert(str) (putchar('\a'), puts(str))

int n;
printf("请输入一个整数:");
scanf("%d",&n);
if (n) {
    puts_alert("这个数不是0");
}else {
    puts_alert("这个数是0");
}
  • 使用宏 交换元素的值
    #define swap(type,x,y)  do {\
        type temp = x;\
        x = y;\
        y = temp;\
    } while (0)

int a = 5, b = 10;
    printf("Before swap: a = %d, b = %d\n", a, b);

    swap(int, a, b); // Swap two integers

    printf("After swap: a = %d, b = %d\n", a, b);

    double x = 3.14, y = 2.71;
    printf("Before swap: x = %f, y = %f\n", x, y);

    swap(double, x, y); // Swap two doubles

    printf("After swap: x = %f, y = %f\n", x, y);

  • 函数式宏
//带参数的宏
#define sqr(x) ((x) * (x))

//不带参数的宏
#define alert()  (putchar('\a')) //响铃的宏


int sqr_int(int x) {
    return x * x;
}

double sqr_double(double x) {
    return x * x;
}

printf("sqr_int(3) = %d\n",sqr_int(3));
printf("sqr_int(3.0) = %f\n",sqr_double(3.0));

printf("sqr 3 = %d\n",sqr(3));
printf("sqr 3.0 = %f\n",sqr(3.0));

以上,使用函数式宏 sqr 可以替代 sqr_int、sqr_double

注意: 1、定义函数式宏时,注意不要将空格写入宏名称和 "("之间 2、在宏定义时将每个参数以及整个表达式都用()

  • 数据类型

image.png

  • 数组内容拷贝
void copy_arr_test() {
    int src[] = {1,2,3,4,5};
    int size = sizeof src / sizeof (src[0]);
    //error:变长数组(VLA,即数组大小由变量决定)不能使用初始化列表(如 {0})进行初始化
    // int dest[size] = {0};
    //改为:
    int dest[size];
    for (int i = 0; i < size;i++) {
        dest[i] = 0;
    }
    copy_arr(dest,src,size);
    print_array(dest,size);
}
  • 精度丢失

double average(int a, int b) {
    return  (a + b) / 2;
}

表达式 `(a + b) / 2` 中,`a` 和 `b` 都是整数,因此 `(a + b)` 的结果也是整数。
如果 `(a + b)` 是奇数,整数除法会丢弃小数部分,导致结果不准确。例如,`average(3, 4)` 的结果是 `3` 而不是 `3.5`。

double average(int a, int b) {
    return  (a + b) / 2.0;
}
修正:浮点数除法:
   将除数 `2` 改为 `2.0`,这样 `(a + b)` 会被提升为浮点数,执行浮点数除法,结果会保留小数部分。
  • 函数内或者函数外使用 static 定义的对象,如果没有显示进行初始化,默认值是 0, 函数外未使用 static 修饰的对象,如果没有显示进行初始化,默认值是 0; 函数内未使用 static 修饰的变量,如果没有显示进行初始化,该对象会被初始化为 “随机值”

  • 函数参数传递

函数参数的传递是通过值的传递进行的,实参的值会被赋值给形参,因此及时修改所接收的形参的值,也不会影响到实参。反之他,通过灵活应用值传递的有点,可以让函数更加间接紧凑

  • 函数参数如果只是用于接收读取值而不改写的话,在声明形参时就应该加上 const

  • 从数组中查找元素

方式一:
/**
 * 从元素个数为 n 的数组 arr 中查找和 key 一致的元素
 * @param arr
 * @param key
 * @param n
 * @return
 */
int search(const int arr[], int key, int n) {
    int i = 0;
    while (1) {
        if (i == n) {
            return FAILED;
        }
        if (arr[i] == key) {
            return i;
        }
        i++;
    }
}

方式二:哨兵模式

int search2( int arr[], int key, int n) {
    int i = 0;
    int last_element = arr[n - 1];  // 保存数组最后一个元素的值
    arr[n - 1] = key;               // 将最后一个元素替换为哨兵

    // 搜索
    while (arr[i] != key) {
        i++;
    }

    // 恢复数组的最后一个元素
    arr[n - 1] = last_element;

    // 返回结果
    if (i < n - 1 || arr[n - 1] == key) {
        return i;  // 找到 key
    } else {
        return FAILED;  // 未找到 key
    }
}

int arr[] = {31, 45, 24, 16, 28};
int key = 26;
int n = sizeof(arr) / sizeof(arr[0]);
int ret = search(arr, key, n);
if (ret == FAILED) {
    printf("\a查找失败");
} else {
    printf("%d 是数组中索引为第 %d 号元素\n", key, ret);
}

ret = search2(arr, key, n);
if (ret == FAILED) {
    printf("\a方式2 查找失败");
} else {
    printf("方式2 %d 是数组中索引为第 %d 号元素\n", key, ret);
}

  • 创建一个函数 search idx,将和有 n个元素的数组 v中的 key 相等的所有元素的下标存储在数组 idx 中,返回和 key相等的元素的个数。 int search idx(const int v[], int idx[],int key, int n);例如,如果v中所接收的数组的元素是{1,7,5,7,2,4,7},key为7的话,(1,3,6} 就会被存储在 idx 中,并返回 3。
int search_idx(const int arr[], int key, int idx[], int n) {
    int count = 0;
    for (int i = 0; i < n; i++) {
        if (arr[i] == key) {
            idx[count] = i;
            count++;
        }
    }
    return count;
}


int search_idx2(const int *v, int *idx, int key, int n) {
    int count = 0; // 用于记录匹配的元素个数

    // 遍历数组 v
    for (int i = 0; i < n; i++) {
        if (v[i] == key) { // 如果当前元素等于 key
            idx[count] = i; // 将下标存储到 idx 中
            count++;        // 增加匹配元素的计数
        }
    }

    return count; // 返回匹配的元素个数
}


int idx[n]; //用于存储匹配元素的下标
int count = search_idx(arr,key,idx,n);
printf("Number of matched count:%d\n",count);
printf("Indices of matches:");
for (int i = 0; i < count;i++) {
    printf("%d",idx[i]);
}

count = search_idx2(arr,idx,key,n);
printf("222 Number of matched count:%d\n",count);
printf("222 Indices of matches:");
for (int i = 0; i < count;i++) {
    printf("%d",idx[i]);
}
printf("\n");

  • 转义字符

\a :显示响铃

8puts 函数

void putsTest()
{
    int n1,n2;
    //在需要换行且不需要格式化输出的时候,可以使用 puts 函数来代替 printf 函数
    puts("输入连个整数。");
    printf("整数1:");scanf("%d", &n1);
    printf("整数2:");scanf("%d", &n2);
    printf("它们的和是:%d\n",n1 + n2);
}

第五章 数组

  • 禁止在函数内修改数组方法
//给 arr 加上 const 关键字
void set_zero(const int arr[], int n) {
    int i;
    for (i = 0; i < n;i++) {
       //error: arr 加上 const 关键字后,执行 arr[i]  会报错 
        arr[i] = 0;
    }
}
  • 数组按倒序排列
void invert_order(int arr[], int size) {
    int times = size / 2;
    printf("invert_order times:%d\n",times);
    for (int i = 0; i < times; i++) {
        int temp = arr[i];
        arr[i] = arr[size - 1- i];
        arr[size - 1- i] = temp;
    }
}
  • 变量初始化

变量在生成的时候会被放入不确定的值,因此在声明变量时,除了特别要求之外,一定要为其赋初始值 变量 vx 和 vy 的值变成了奇怪的值, 打印出 3535 和 938(编译平台不一样,输出的值也不一样) ,因为在生成变量的时候,变量会被放入一个不确定的值,即垃圾值,因此,如果从没有设定值得变量中取出数据,结果是不可预测的。


 int vx,vy;
    //
    //vx的值是:3535,vy 的值是:938
    printf("vx的值是:%d,vy 的值是:%d\n",vx,vy);
  • puts 函数

puts 函数可以按顺序输出作为实参的字符串,并在结尾换行,因此, puts("....") 等价于 printf("...\n"),但是 puts 只支持输出一个字符串吗,且不支持格式和输出。在需要换行且不需要格式和输出的时候,可以使用 puts 替代 printf

第二章 运算和数据类型

  • 把 float 型数据赋值给 int 型,小数部分会被舍去, %f 默认显示小数点后 6 位数字
 int n;
    double x;

    n = 9.99;
    x = 9.99;

    //输出 9 
    printf("int 型变量的值是:%d\n", n);
    //输出 4 
    printf("n / 2:%d\n", n / 2);
    //输出 9.990000
    printf("double 型变量的值是:%f\n", x);
     //输出4.995000
    printf("             x / 2:%f\n", x / 2.0);
  • 转换说明

double 类型的变量通过 scanf 函数赋值的时候需要使用格式字符串 %lf (l 是小写字母 l)

使用 printf 函数显示:
int 类型:
printf("%d", no);

double 类型:
printf("%f", no);


使用 scanf 函数显示:
int 类型:
printf("%d", &no);

double 类型:
printf("%lf", &no);

  • 若要将某个表达式的值转换为别的数据类型所对应的值,需要使用

  • 运算对象,即操作数的类型不同时,较小的数据类型的操作数会转换成较大的数据类型(范围更大),然后再进行运算(所谓 较大的数据类型,并不是说 double 类型实际上比 int 类型更大,而是它可以保存小数点后面的部分)

  • 在C语言中,当进行除法运算时,结果的数据类型取决于操作数的数据类型。如果两个操作数都是整数,结果也将是整数。如果至少有一个操作数是浮点数(float 或 double),结果将是浮点数。

  double x = 3.14;
  //double 类型的变量通过 scanf 函数赋值的时候需要使用格式字符串 %lf
  scanf("%lf",&x);
  
  
  
  
  // 整数 /整数 运算,商的小数部分会被舍弃,但是浮点数之间的运算,就不会进行舍弃处理

    int n1, n2, n3, n4;
    double d1, d2, d3, d4;

    n1 = 5 / 2;
    n2 = 5.0 / 2.0;
    n3 = 5.0 / 2;
    n4 = 5 / 2.0;

    d1 = 5 / 2;
    d2 = 5.0 / 2.0;
    d3 = 5.0 / 2;
    d4 = 5 / 2.0;

    // 2
    printf("n1=%d\n", n1);
    // 2: 2.5 赋值给 int 型变量会舍弃小数部分,变成 2
    printf("n2=%d\n", n2);
    // 2: 2.5 赋值给 int 型变量会舍弃小数部分,变成 2
    printf("n3=%d\n", n3);
    // 2: 2.5 赋值给 int 型变量会舍弃小数部分,变成 2
    printf("n4=%d\n", n4);

    printf("d1=%f\n", d1);
    printf("d2=%f\n", d2);
    printf("d3=%f\n", d3);
    printf("d4=%f\n", d4);
  • 转换说明

转换说明的形式如下: %09.9f,类似于 %AB.Cf 的形式 ,包括 % 和 . 在内,总共由 6 部分组成

A:0标志位,如果数值的前面有空格,则使用 0 补齐位数; 如果省略了 0标志,则使用空白补齐位数 B:最小字段宽度(最少要显示出的字符位数) C:精度,指定显示的最小位数,如果不指定,则整数的时候默认是 1,浮点数的时候默认是 6 D:转换说明符

如果设置定了 "-",数据会左对齐显示(默认是右对齐)

int a = 10;
double pi = 3.14;
// %5d 显示至少5位的 10进制数
printf("%5d\n", a);
//%5.1f 显示至少 5 位的浮点数,但是小数点后只显示 1printf("testFormatOutput %5.1f\n", pi);

当用 printf 函数来显示 double 类型的值时,转换说明是 %f; 当用 scanf 函数来读取时,转换说明是 %lf。

  • 浮点型常量

和整型常量有猴嘴 U 和 L 一样,浮点型常量末尾也可以加上指定类型的浮点型后缀, 后缀 f 或 F 表示 float 型,后缀 l 或 L 表示 long double 型,例如

57.3    // double 型
57.3f   // float 型
57.3L   //long double 型(因为小写的 l 和 数字 1容易混淆,因此推荐用大写的 L)

也可以使用指数表示为科学计数法

1.23 E4   //1.23* 10的 4 次方
1.23 E-5  //1.23* 10的 -5 次方

  • printf 打印
printf 函数技能输出八进制数,也能输出 16 进制数,输出八进制用 %o, 输出 16进制用 %x 或 %X (%x 用小写字母 a-f 表示, %X 用大写字母 A-F 表示)
  • 整型常量的数据类型
u 和 U 表示该整型常量为无符号类型
l 和 L 表示该整型常量为 long 型
例如 3517Uunsigned型 , 12567long
  • 枚举 枚举常量的数据类型是 int 型,因此在返回值类型为 enum animal 型的 select 函数中,可以返回 int 型变量 tmp 的值。 为了明确起见,也可以将返回值进行强制类型转换为 enum animal
enum animal anim_select()
{
    int temp;
    do
    {
        printf("0...狗  1...猫  2...猴  3...结束:");
        scanf("%d", &temp);
    } while (temp < Dog || temp > Invalid);
    // return temp;
    return (enum animal)temp;
}
  • 输入输出
void inputOutput()
{
    int ch;
    //EOF: windows 按下 ctrl + Z,  mac: 按下 ctrl + D
    while ((ch = getchar()) != EOF)
    {
        putchar(ch);
    }
}
  • 字符串字面量 在字符串字面量的末尾会被加上一个叫做 null 字符的值为 0 的字符, null 字符用八进制转义字符是 "\0", 用整数来表示就是 0
 // 4
    printf("sizeof(\"123\"):%zu\n", sizeof("123"));

    // 5
    printf("sizeof(\"AB\tC\"):%zu\n", sizeof("AB\tC"));
    // 8
    printf("sizeof(\"abc\\0def\"):%zu\n", sizeof("abc\0def"));
  • 字符数组的初始化 因为初始值的个数决定了数组元素的个数,所以元素个数可以省略。此外, b 的初始值也可以用 {} 括起来,如 {"ABC"}
a:  char str[] = {'A', 'B','C','\0'}
b:  char str[] = "ABC"


但是除了初始化赋值的时候,我们不能将数组的初始值或字符串直接赋予数组变量

char s[4];
char s[4];
//error
s = {'A','B'};
//error
s = "ABC";
  • 格式化字符串输出
/**
 * 格式化字符串输出
 */
void formatStrShow(){
    char str[] = "12345";
    printf("%s\n",str);  //原样输出             12345
    printf("%3s\n",str); //至少显示 3 位        12345
    printf("%.3s\n",str); //最多显示 3 位       123
    printf("%8s\n",str);  //右对齐                 12345
    printf("%-8s\n",str); //左对齐              12345
 }
  • 字符串数组

字符串可以用数组表示,所以字符串的集合也可以用数组来表示

    /**
     * 字符串数组
     * 运行结果
     * cs[0] = "apple"
     * cs[1] = "banana"
     * cs[2] = "pear"
     */
    void strArray()
    {
        char cs[][6] = {"apple", "banana", "pear"};
        int sz = sizeof(cs) / sizeof(cs[0]);
        for (size_t i = 0; i < sz; i++)
        {
            printf("cs[%zu] = %s\n", i, cs[i]);
        }
    }


  • 显示字符串
void put_string(const char s[]){
    int i = 0;
    while (s[i])
    {
        putchar(s[i++]);
    }
}
  • 显示字符串数组

/**
 * 显示字符串数组(逐个显示字符)
 */
void putStrArray(const char s[][6], int n)
{
    for (size_t i = 0; i < n; i++)
    {
        int j = 0;
        printf("s[%d=\"", i);
        while (s[i][j])
        {
            putchar(s[i][j++]);
        }
        puts("\"");
    }
}

/**
 * 显示字符串数组
 */
void showStrArray()
{
    char s[][6] = {"apple", "banana", "pear"};
    putStrArray(s,3);
}
  • 字符串总结
1null 是值为 0 的字符,用八进制转移文字符表示为'\0', 用整数常量表示就是 0
2、字符串字面量的末尾是 null 字符。因此,字符串字面量 "ABC" 实际上占用了 4 个字符的内存空间,一个字符也没有的字符串字面量 ""占用 1个字节(这里说的空间是通过 sizeof 获取),通过 strlen 获取的字符串长度是不包括 null 字符的
3、字符串字面量具有静态存储期,因此它”活在“从程序开始到结束的整个生命周期内。当具有多个拼写相同的字符串字面量时,如果将其作为一个存储,就能减少所需的内存空间,反之也可以分别进行储存。至于采用哪种方式,要由编译器而定
4、函数锁接受的字符串,就是调用方赋予的数组本身,因为字符串的末尾有 null 字符,所以无需将元素个数作为别的参数进行传递
5、字符串数组可以用数组的数组,即二维数组来表示。例如 5 个最多能存储 12 个字符(包括 null 字符在内)的字符串(即 char[12] 型数组)集中在一起形成的数组,可以定义成 char ss[5][12] //元素类型为 char[12], 元素个数为 5 的

指针

  • %p, 是转换说明符,p 是 pointer 的首字母
 int a[3];
    //%p 显示地址
    printf("a[0] 的地址:%p\n", &a[0]);
  • 指针作为参数
void sum_diff(int n1, int n2, int *sum, int *diff)
{
    //*sum 是传过来的 wa 的别名, *diff 是 sa 的别名,在函数体中,
    //将求得和的值赋值给 *sum, 将差赋值给 *diff 就相当于是对 wa 和 sa 进行赋值,
    //因此,sum_diff 函数返回到 main 函数之后,和与差分别存在 wa 和 sa
    *sum = n1 + n2;
    *diff = (n1 - n2) ? n1 - n2 : n2 - n1;
}

void sum_diff_test()
{
    int na, nb;
    int sum = 0, diff = 0;
    printf("请输入两个整数。");
    printf("整数A:"); scanf("%d",&na);
    printf("整数B:"); scanf("%d",&nb);

    sum_diff(na,nb,&sum,&diff);
    printf("两数之和是:%d,之差是:%d\n",sum,diff);
}
  • 对于使用 register 关键字声明的寄存器对象,不能加上取址运算符 &, 因此,下述程序在编译时会报错
register int x;
print("%p\n", &x);
  • 指针和数组

int arr[5] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
// p 指向 arr[0]
int *p = arr;
数组名原则上被解释为指向数组起始元素的指针,也就是说,如果 a 是数组,那么表达式 a 的值就与 a[0]的值一致。如果数组 a 的元素类型为 Type 型,那么不管元素个数是多少,表达式 a 的类型就是 Type*

指针 p 指向数组中的元素 e 时:
p + i 为指向元素 e 后第 i 个元素的指针, 可以写成 p[i]
p - i 为指向元素 e 前第 i 个元素的指针, 可以写成 p[-i]

* 因为 p +2 指向 a[2], 所以 *(p+2) 是 a[2]的别名
* 因为 *(p +2) 可以写成 p[2], 所以 p[2]也是 a[2] 的别名
* 数组名 a 是指向起始元素 a[0]的指针,所以 a +2 就是指向第 3 个元素 a[2]的指针
* 因为指针 a +2 指向元素 a[2],所以在前写上指针运算符后得到 *(a+2)就是 a[2]的别名





  • 数组名在什么情况下不被视为指向起始元素的指针
1、作为 sizeof 运算符的操作数出现时
2、作为取址运算符 & 的操作数出现时
&数组名不是指向起始元素的指针,而是指向数组整体的指针

  • 指针访问数组元素
以下 4个表达式都是访问各元素的表达式(从开头数第 i 个元素)

arr[i]    *(arr + i)   p[i]     *(p +i)  

以下 4个表达式都是指向各元素的指针(指向从开头数第 i 个元素的指针)

&arr[i]    arr + i   &p[i]     p + i

  • 字符串和指针
//用数组实现的字符串
char str[] = "ABC";
//用指针实现的字符串, 由于字符串字面量会被解释为指向第一个字符的指针,所以指针p被初始化为指向字符串字面量  "FBI"的第一个字符 "F"
char *ptr = "ABC"


用指针数显的字符串比数组实现的字符串需要更多的空间,原因是除了子目录 ”ABC“占用空间以外, ptr也会占用空间:sizeof(ptr)

指针 ptr 不可进行如下声明

char *ptr = {'A','B','C','\0'};
  • 字符串数组
char *p[] = {"apple", "banna", "pear"};

/**
 *  字符串数组
 */
void strArrayByPoint()
{
    char *p[] = {"apple", "banna", "pear"};
    int size = sizeof(p) / sizeof(p[0]);
    for (size_t i = 0; i < size; i++)
    {
        printf("p[%zu]=%s\n", i,p[i]);
    }
}

指针 p 是 元素类型为 char *, 元素个数为 3 的数组,数组各元素 p[0]、p[1]、p[2] 的初始值分别指向个字符串字面量的首字符 "a"、”b“、”c“的指针,因此,除了数组 p 占用的 3个 sizeof(char *)长度的空间外,还占用 3个字符的字面量空间

  • 字符串转换
int atoi(const char \*nptr ) // 将 nptr 指向的字符串转换为 int 型表示
long atoi(const char \*nptr ) // 将 nptr 指向的字符串转换为 long 型表示
float atoi(const char \*nptr ) // 将 nptr 指向的字符串转换为 float 型表示

 char *str = "123";
    int n1 = atoi(str);
    long n2 = atol(str);
    float n3 = atol(str);
    printf("%s 转换后 Int 型整数为 %d\n", str, n1);
    printf("%s 转换后 Long 型整数为 %d\n", str, n2);
    printf("%s 转换后 Float 型整数为 %f\n", str, n3);
  • 字符串数组 表示字符串数组的一个方法是使用 ”用数组实现的字符串“的数组
char a2[][5] = {"apple}, "banana", "pear"}

另一种表示字符串数组的方法是使用 ”用指针实现的字符串“的数组

char *p2 = {"apple}, "banana", "pear"}

这种方式无法保证各字符串都被保存在连续的内存空间中, 数组的带下是 sizeof(p2), 即 (sizeof(char *) * 元素个数)。除数组本身之外,各字符串字面量也占用内存空间

  • 因为无法保证字符串字面量被保存在能够改写的空间中,所以不要对该空间及其前后的空间进行写入操作

结构体

  • 结构体声明
//student 是结构名, 由两个单词构成的 struct student 是类型名,如同枚举类型中
// ”enum 枚举名“, 是类型名一样, zhangsan 是实体对象(变量)
struct student zhangsan 
  • 结构体作为参数

void structAsParam(struct student *stu)
{
    if (stu->height < 180)
    {
        stu->height = 180;
    }
    if (stu->weight > 80)
    {
        stu->weight = 80;
    }
}

struct student wangwu = {"wangwu", 170, 90.5, 200};
structAsParam(&wangwu);
    
  • 结构体可以赋值,因此可以作为函数的返回值类型;由于数组不可以进行赋值,因此数组不可用作函数的返回值,如果想要突破这个限制,可在函数内通过 动态分配内存方式,但是调用者需要注意释放内存

  • 从键盘输入创建结构体对象

struct student generateStudent()
{
    struct student stu;
    printf("请输入姓名:");
    //name  是字符数组,本来就是指向首元素底子,会因此不用加取址运算符
    scanf("%19s", stu.name); // 限制输入长度以避免溢出
    printf("请输入身高:");
    scanf("%d", &stu.height);
    printf("请输入体重:");
    scanf("%f", &stu.weight);
    printf("请输入奖学金:");
    scanf("%ld", &stu.schols);
    return stu;
}

struct student stu = generateStudent();
printf("stu 个人信息 姓名:%s,身高:%d,体重:%.1f,奖学金:%ld\n", stu.name, stu.height, stu.weight, stu.schols);
  • 当传递数组名给像 scanf 这样的函数时,实际上是在传递一个指向数组第一个元素的指针。因此,不需要使用 & 操作符来获取地址
   struct student stu;
    printf("请输入姓名:");
    //name  是字符数组,本来就是指向首元素底子,会因此不用加取址运算符
    scanf("%19s", stu.name); // 限制输入长度以避免溢出
  • 具有结构体成员的结构体
typedef struct
{
    double x;
    double y;
} Point;

typedef struct
{
    Point pt;    //当前位置
    double fuel; //剩余燃料
} Car;
  • 行驶汽车游戏

#define SQR(n) ((n) * (n))

typedef struct
{
    double x;
    double y;
} Point;

typedef struct
{
    Point pt;    // 当前位置
    double fuel; // 剩余燃料
} Car;


double distance_of(Point pa, Point pb)
{
    return sqrt(SQR(pa.x - pb.x) + SQR(pa.y - pb.y));
}

void showCarInfo(Car car)
{
    printf("当前位置:( %.2f,%.2f)\n", car.pt.x, car.pt.y);
    printf("剩余燃料:%.2f\n", car.fuel);
}

int move(Car *car, Point dest)
{
    double distance = distance_of(car->pt, dest);
    // 行驶距离超过了燃料,无法行驶
    if (distance > car->fuel)
    {
        return 0;
    }
     //更新当前位置和燃料
    car->pt = dest;
    car->fuel -= distance;
}


void driveCarTest()
{
    Car myCar = {{0,0}, 300};
    while (1)
    {
        int select;
        showCarInfo(myCar);
        printf("开动汽车吗[Yes --- 1/No --- 0]");
        scanf("%d", &select);
        if (select != 1)
        {
            break;
        }

        Point dest;
        printf("输入目的地 X 和 Y坐标: "); scanf("%lf %lf", &dest.x,&dest.y);

        if (!move(&myCar,dest))
        {
            perror("\a燃料不足,无法行驶\n");
        }
    }
    
}
  • 构成结构体的元素称为成员,结构体的成员也可以是结构体,而不能继续分解的成员,称为结构成员

  • 结构体成员在内存空间上的排列顺序和成员声明的顺序一样

  • 函数不能返回数组(因为数组不能赋值),但能返回结构体

文件处理

  • 读取文件列表,对结果数据排序
#define FILE_NAME "haw.bat"

typedef struct
{
    char name[100];
    double height;
    double weight;
} Person;

int compareHeight(const void *a, const void *b)
{
    Person *personA = (Person *)a;
    Person *personB = (Person *)b;
    if (personA->height > personB->height)
        return 1;
    if (personA->height < personB->height)
        return -1;
    return 0;
}

void fileTest2()
{
    FILE *fp = NULL;
    fp = fopen(FILE_NAME, "r");
    if (fp == NULL)
    {
        printf("打开文件 %s 失败\n", FILE_NAME);
        fprintf(stderr, "错误打开文件: %s\n", FILE_NAME); // 使用 fprintf 来包含文件名
        return;
    }
    printf("成功打开文件 %s\n", FILE_NAME);

    Person list[100]; // 假设最多 100 个人
    int count = 0;
    while (fscanf(fp, "%99s %lf %lf", list[count].name, &list[count].height, &list[count].weight) == 3)
    {
        // 这里取 Person 值时注意不能通过list[count] 来取
        printf("111 %s %5.1f %5.1f\n", list[count].name, list[count].height, list[count].weight);
        count++;
    }
    fclose(fp);

    qsort(list, count, sizeof(Person), compareHeight);

    // 打印排序后的数组
    printf("-------按身高排序后的数组:------\n");
    for (int i = 0; i < count; i++)
    {
        printf("%s %5.1f %5.1f\n", list[i].name, list[i].height, list[i].weight);
    }
    printf("-------------\n");

    double hsum = 0;
    double wsum = 0;
    for (int i = 0; i < count; i++)
    {
        hsum += list[i].height;
        wsum += list[i].weight;
    }

    printf("平均  %5.1f %5.1f\n", hsum / count, wsum / count);
}

  • 将当前日期数据写入文件/从文件中读取日期
void put_data_to_file(void)
{
    FILE *fp;
    // 使用 time 函数获取当前时间
    time_t current = time(NULL);
    //*使用 localtime 函数将 current 变量中的时间转换为本地时间,并将其转换为 struct tm 类型的指针 timer。struct tm 是一个结构体,它包含了关于日期和时间的详细信息。
    struct tm *timer = localtime(&current);
    if ((fp = fopen(FILE_DATE, "w")) == NULL)
    {
        printf("打开文件 %s 失败\n", FILE_DATE);
        return;
    }
    fprintf(fp, "%d %d %d %d %d %d\n",
            // tm_year 存储的是自1900年以来的年数。所以加上 1900 为当前的年数
            timer->tm_year + 1900,
            timer->tm_mon + 1,
            timer->tm_mday,
            timer->tm_hour,
            timer->tm_min,
            timer->tm_sec);
    fclose(fp);
}

void get_data_from_file(void)
{
    FILE *fp;
    fp = fopen(FILE_DATE, "r");
    if (fp == NULL)
    {
        printf("本程序第一次运行\n");
        return;
    }
    int year, month, day, hour, minute, second;
    fscanf(fp, "%d%d%d%d%d%d", &year, &month, &day, &hour, &minute, &second);
    printf("上一次运行时间是在%d %d %d %d %d %d\n", year, month, day, hour, minute, second);
}
  • 日期时间类
 time_t current = time(NULL);
    //*timer 是一个结构体指针,指向 tm
    struct tm *timer = localtime(&current);
    
     fprintf(fp, "%d %d %d %d %d %d\n",
            timer->tm_year + 1900,
            timer->tm_mon + 1,
            timer->tm_mday,
            timer->tm_hour,
            timer->tm_min,
            timer->tm_sec);
  • 获取当前日期时间字符串
char *getCurrentTime2()
{
    time_t current = time(NULL);
    //*使用 localtime 函数将 current 变量中的时间转换为本地时间,并将其转换为 struct tm 类型的指针 timer。struct tm 是一个结构体,它包含了关于日期和时间的详细信息。
    struct tm *timer = localtime(&current);
    if (timer == NULL)
    {
        perror("Failed to get local time");
        return NULL;
    }
    char *buffer = malloc(100 * sizeof(char));
    if (buffer == NULL)
    {
        perror("Failed to allocate buffer memory");
        return NULL;
    }
    if (strftime(buffer, sizeof(buff), "%Y-%m-%d %H:%M:%S", timer) == 0)
    {
        perror("Failed to format time");
        free(buffer);
        return NULL;
    }
    return buffer;
}

 char *dataTimeStr = getCurrentTime2();
    if (dataTimeStr != NULL)
    {
        printf("当前日期和时间:%s\n",dataTimeStr);
        free(dataTimeStr);
    }
  • 标准输入流 stdin 和 标准输出流 stdout 都是指向 FILE 的指针类型,因此这些变量会直接传递给 fscanf 函数好 fprintf 函数的第一个参数, 因此下面两条语句的功能相同
scanf("%d", &x);
等同于
fscanf(stdin, "%d", &x);

printf("%d",x);
等同于
fprintf(stdout, "%d", x);
  • 读取文件内容
/**
 * 读取文件内容
 */
char *readFileContent(const char *fileName)
{
    FILE *fp = fopen(fileName, "r");
    if (fp == NULL)
    {
        perror("文件打开失败");
        return NULL;
    }

    fseek(fp, 0, SEEK_END); // 移动到文件末尾,获取文件大小
     // fseek(fp, 10, SEEK_SET); // 从文件开头移动10字节
    // 获取当前位置,即文件大小
    long fileSize = ftell(fp);

    //将文件指针重置到文件开头
    rewind(fp);

    // 分配内存
    char *buffer = (char *)malloc(fileSize + 1); // +1 for the null-terminator
    if (buffer == NULL)
    {
        perror("内存分配失败");
        fclose(fp);
        return NULL;
    }

    // 读取 fileSize 个字符到 buffer
    size_t bytesRead = fread(buffer, sizeof(char), fileSize, fp);
    if (bytesRead < fileSize)
    {
        free(buffer);
        fclose(fp);
        perror("读取文件失败");
        return NULL;
    }

    // 添加字符串结束符
    buffer[bytesRead] = '\0';

    fclose(fp);
    return buffer;
}
  • 拷贝文件内容(一次拷贝多个字符)

/**
 * 拷贝文件内容(一次拷贝 1024个字符)
 */
int copyFile2(const char *sFileName, const char *dFileName)
{
    FILE *sourceFile;
    FILE *destFile;
    char buff[1024];
    size_t bytesRead;

    if ((sourceFile = fopen(FILE_DATE, "r")) == NULL)
    {
        printf(" 打开文件: %s 失败 \n", sFileName);
        return 1;
    }
    if ((destFile = fopen(dFileName, "w")) == NULL)
    {
        printf(" 打开文件: %s 失败 \n", dFileName);
        return 1;
    }
    while ((bytesRead = fread(buff, sizeof(char), sizeof(buff), sourceFile)) > 0)
    {
        if (fwrite(buff, sizeof(char), bytesRead, destFile) != bytesRead)
        {
            printf("写入文件 %s 时出错\n", dFileName);
            fclose(sourceFile);
            fclose(destFile);
            return 1;
        }
    }
    if (ferror(sourceFile))
    {
        perror("读取源文件报错");
        fclose(sourceFile);
        fclose(destFile);

        return 1;
    }
    fclose(sourceFile);
    fclose(destFile);
    return 0;
}