C语言学习笔记

本文详细介绍了C语言的学习笔记,涵盖C语言的发展历程、特点、可执行文件生成,以及基本概念、数据类型、运算符、表达式、流程控制、数组、指针、函数、构造类型、动态内存管理和静态库与动态库等内容,旨在帮助读者全面掌握C语言编程技能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 引言

1.1 C语言的发展史

  • 1960 原型A语言 -> ALGOL语言
  • 1963 CPL语言
  • 1967 BCPL语言
  • 1970 B语言 B语言+汇编编写了Unix
  • 1973 C语言 C语言重新编写了Unix

1.2 C语言的特点

  • 基础性语言
  • 语法简洁,紧凑,方便,灵活
  • 运算符丰富,数据结构丰富
  • 结构化,模块化编程
  • 移植性好,执行效率高
  • 允许直接对硬件操作

1.3 可执行文件的生成

​ C源文件 — 预处理器 — 编译器 — 汇编器 — 链接器 — 可执行文件

2 基本概念

写程序的注意事项

  • 头文件必须得正确包含
  • 以函数为单位来进行程序的编写
  • 程序包含声明部分+实现部分
  • return 0
  • 多用空格和空行
  • 适当添加注释

算法

算法即解决问题的方法(流程图, NS图, 有限状态机FSM)

程序

用某种语言实现算法

防止写越界,防止内存泄漏,谁打开谁关闭,谁申请谁释放

3 数据类型,运算符和表达式

3.1 数据类型(基本数据类型)

3.1.1 概述

  • 基本类型
    • 数值类型
      • 整数 short, int, long
      • 浮点型 float, double
    • 字符型 char
  • 构造类型
    • 数组
    • 结构体 struct
    • 共用体 union
    • 枚举类型 enum
  • 指针类型
  • 空类型 void

3.1.2 基本数据类型

以下大小是在64位机器上的大小

数值类型

  • 整数
    • 有符号数 short(2B), int(4B), long(8B)
    • 无符号数 unsigned short(2B), unsigned(4B), unsigned long(8B)
  • 浮点数
    • 单精度 float(4B)
    • 双精度double(8B)

字符型

  • 有符号 char(1B)
  • 无符号unsigned char(1B)

基本数据类型在底层是以二进制的形式进行存储的,其中有符号数是以补码的形式进行存储的

补码的最高位是符号位,1表示负数,0表示整数,最高位的权值为-2 ^ i,其他位的权值为2 ^ i,正数的补码就是其本身,负数的补码是其绝对值按位取反后末尾+1

3.1.3 数据类型的转换

  • 隐式类型转换,也成为自动类型转换,比如在做数据的运算时会向精度高的进行转换
  • 强制(显式)类型转换

3.1.4 特殊类型

  • 布尔型bool
  • float类型并不是一个精确的值
  • char型是否有符号

3.2 常量与变量

常量:在程序执行过程中值不会发生变化的量

分类:

  • 整型常量

  • 浮点型常量

  • 字符常量 : 由单引号引起来的单个字符或转义字符,如’a’, ‘\0’, ‘\015’标识一个8进制数,’\x7f’表示一个16进制数

  • 字符串常量 : 由双引号引起来的一个或多个字符组成的序列,如 “hello”, “”(空串只有一个尾0)

  • 标识常量 #define

    • 预处理的时候就会将所有的宏名替换为宏体,并不会去检查语法错误,同时注意宏体一定要加上扩号,比如下面ADD未加上括号导致输出并不出想要的5 * 5

      #include <stdio.h>
      #define ADD 2 + 3 
      int main() {
          int ans = ADD * ADD;
          printf("%d", ans);//输出11
          return 0;
      }
      
    • #define可以定义一些函数,宏相对于函数来讲,是比较危险的,但是能够节省运行时间,因为在编译阶段就已经完全替换掉了,比如内核中模块多用的是宏,应用层中要求稳定应用函数

      #define MAX(a, b) ((a) > (b) ? (a) : (b))
      
      int max(int a, int b) {
          return a > b ? a : b;    
      }
      
      int main() {
          int ans = MAX(1, 4);
          printf("%d", ans);//输出4
          return 0;
      }
      
    • #define可以定一个代码段

      #include <stdio.h>
      #define MAX(a, b) \
          ({int A = a, B = b; ((A) > (B) ? (A) : (B));})
      
      int main() {
          int ans = MAX(1, 4);
          printf("%d", ans);//输出4
          return 0;
      }
      

变量:用来保存一些特定的内容,在程序执行过程中值随时会发生变化的量

定义: 【存储类型】 数据类型 标识符 = 值

​ TYPE NAME = VALUE;

  • 标识符 就是一个表示变量的序列
  • 数据类型 基本数据类型+构造类型
  • 存储类型 auto static register extern(说明型关键字)
    • auto 默认,自动分配空间,自动释放空间
    • register 寄存器型,该型不能取址,且只能定义局部变量,同时是一个建议型关键字,至于该型是否真的放到寄存器中由编译器来决定
    • static 静态型,自动初始化为0值或空值,并且这种类型变量的值有继承性(该变量只被定义一次),另外常用来修饰变量或函数表示是当前文件私有的
    • extern 说明型关键字,意味着不能改变被说明的变量的值和类型

变量的生命周期和作用域

  • 全局变量和局部变量

    全局变量的作用域是从定义的位置开始到程序结束,局部变量的作用范围是从定义的位置开始,到期所在的块结束处,同时内部的会屏蔽外部的,即就近原则

    int i = 100;
    int main() {
        int i = 1;
        printf("%d", i); // 结果为1
        return 0;
    }
    
  • 局部变量和局部变量

    static与auto

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Htu03TxV-1672927149628)(../牛客项目/temp/image-20221215175406118.png)]

3.3 运算符和表达式

注意事项

  • 每个运算符所需要的参与运算的操作数的个数
  • 结合性
  • 优先级
  • 运算符的特殊使用
  • 位运算的重要意义

C运算符

  • 算数运算符 +, -, *, /, %, ++, –
  • 关系运算符 >, >=, <, <=, !=, ==
  • 逻辑运算符 !, &&, || (具有短路特性)
  • 位运算符 >>, <<, &, | ~, ^, >>>
  • 赋值运算符 =, +=, -=, *=, /=, %=等等
  • 条件(三目)运算符 ? :
  • 逗号运算符 ,
  • 指针运算符 *, &
  • 求字节数 sizeof
  • 强制类型转换 (类型)
  • 分量运算符 ., ->
  • 下标运算符 []
  • 其他 ()

几种典型的位运算

  • 将操作数中的第n位置1,其他位不变:num = num | (1 << n);
  • 将操作数中的第n位置0,其他位不变:num = num & (~(1 << n));
  • 测试第n位:num & (1 << n);
  • 从一个指定宽度的数中取出其中的某几位(比如取出第i到第j位):num & ((~(-1 << (j - i + 1))) << i)

4 出入、输出专题

4.1 格式化的输入输出scanf, printf

printf

函数定义

/**
 * extern int printf (const char *__restrict __format, ...);
 * __restrict__format : "%[修饰符]格式字符"
 * 函数的返回值是答应出来的字符串的长度,不包括尾0
 */

示例

#include <stdio.h>

int main() {

    printf("%%d的使用: %d\n", 123);  //输出 %d的使用: 123
    printf("%%x的使用: %x\n", 123);  //输出 %x的使用: 7b
    printf("%%o的使用: %o\n", 123);  //输出 %o的使用: 173
    printf("%%u的使用: %u\n", 123);  //输出 %u的使用: 123
    printf("%%c的使用: %c\n", 65);  //输出 %d的使用: %c的使用: A
    printf("%%s的使用: %s\n", "65");  //输出 %d的使用: %s的使用: 65
    printf("%%e的使用: %e\n", 123.456);  //输出 %d的使用: %e的使用: 1.234560e+02
    printf("%%f的使用: %f\n", 123.456);  //输出 %d的使用: %f的使用: 123.456000
    printf("%%g的使用: %g\n", 123.456);  //输出 %d的使用: %g的使用: 123.456

    return 0;
}

修饰符

在这里插入图片描述

注意事项,printf的缓冲区刷新,\n换行符能够强制进行输出缓冲区的刷新

加上换行符的效果

#include <stdio.h>
#include <unistd.h>

int main() {

    printf("[%s:%d]\n", __FUNCTION__ , __LINE__);
    sleep(5);
    printf("[%s:%d]\n", __FUNCTION__ , __LINE__);

    return 0;
}

在这里插入图片描述

不加换行符的效果

#include <stdio.h>
#include <unistd.h>

int main() {

    printf("[%s:%d]", __FUNCTION__ , __LINE__);
    sleep(5);
    printf("[%s:%d]", __FUNCTION__ , __LINE__);

    return 0;
}

在这里插入图片描述

可以看到不加换行符的时候是将缓冲区中的输出在程序退出的时候一起输出

scanf

函数定义

/**
 * extern int scanf (const char *__restrict __format, ...) __wur;
 * __restrict__format : "%[修饰符]格式字符", 后面的...是地址
 * 函数的返回值是成功接收到值的个数
 */

示例

#include <stdio.h>
int main() {

    int i;
    printf("Please enter: \n");
    scanf("%d", &i);
    printf("i = %d\n", i);

    return 0;
}

4.2 字符输入输出getchar, putchar

getchar

函数定义

int getchar (void)

putchar

函数定义

int putchar (int __c)

示例

#include <stdio.h>
int main() {

    int ch;
    ch = getchar();
    putchar(ch);

    return 0;
}

4.3 字符串输出输出gets, puts

gets

函数定义

gets并不会检查缓冲区是否溢出

char *gets (char *__s)

puts

函数定义

int puts (const char *__s)

示例

#include <stdio.h>
#define STRSIZE 32
int main() {

    char str[STRSIZE];
    gets(str);
    puts(str);

    return 0;
}

4.4 练习题

练习题一

一个水分子的质量大约为3.0e-23g, 一夸脱水大约有950g,编写一个程序,要去从终端输入水的夸脱数,然后显示这么多夸脱水中包含有大概多少水分子。

#include <stdio.h>
#define K 950
#define ATOM (3.0e-23)
int main() {
    
    double weight;
    double cnt;
    puts("请输入水的夸脱数: ");
    scanf("%lf", &weight);
    cnt = weight * K / ATOM;
    printf("这么多水中含有的水分子数为: %e\n", cnt);
    return 0;
    
}

练习题二

从终端输入三角形的三边长,求面积

s = 1 / 2 * (a + b + c);

area = sqrt(s * (s - a) * (s - b) * (s - c));

#include <stdio.h>
#include <math.h>

int main() {

    float a, b, c;
    puts("请输入三角形的三个边长: ");
    int ret = scanf("%f%f%f", &a, &b, &c);
    if (ret != 3) {
        puts("输入的边数必须是3");
        return 0;
    }
    if (a + b <= c || a + c <= b || c + b <= a) {
        puts("输入的三边不能构造成三角形!!!");
        return 0;
    }
    float s = (a + b + c) / 2;
    printf("%f", s);
    float area = sqrt(s * (s - a) * (s - b) * (s - c));
    printf("三角形的面积为 %f", area);
    return 0;

}

5 流程控制

5.1 概述

程序分为三种结构 顺序,选择,循环,所涉及到的关键字如下:

  • 顺序
  • 选择: if-else, switch-case
  • 循环: while, do-while, for, if-goto
  • 辅助控制: continue, break

5.2 选择结构

5.2.1 if-else

if (exp)
	cmd;
或者
if (exp)
	cmd;
else if (exp)
	cmd;
else
	cmd;
同一级别的if-else if-else只会匹配其中的一个

注意,else只与最近的if进行匹配(在没有代码块括号的情况下),比如下面代码,会输出a != b,因为else是和第二个if匹配上了(分支语句超过一句了就加上{})

int main() {

    int a = 1, b = 1, c = 2;
    if (a == b)
        if (a == c)
            printf("a == b == c\n");
    else
            printf("a != b\n");
    return 0;

}

将如上代码做以下修改,则什么也不会输出

int main() {

    int a = 1, b = 1, c = 2;
    if (a == b) {
        if (a == c)
            printf("a == b == c\n");
    } else
            printf("a != b\n");
    return 0;

}

练习 输出分数对应的等级

#include <stdio.h>

int main() {
    double score;
    char class;
    puts("请输入你的考试成绩: ");
    scanf("%lf", &score);
    if (score < 0 || score > 100) {
        puts("成绩应该为不小于0且不大于100的数");
        return 0;
    }

    if (score >= 90 && score <= 100) class = 'A';
    else if (score >= 80) class = 'B';
    else if (score >= 70) class = 'C';
    else if (score >= 60) class = 'D';
    else class = 'E';
    printf("您成绩的级别为: %c", class);
    
    return 0;
    
}

5.2.2 switch-case

switch (exp) {
    case 常量表达式:
        cmd;
        break; //当没有break时会一直向下判断直到遇到break
    case 常量表达式:
        cmd;
        break;
    ......
    default:
        cmd;
}

利用switch-case重新写成绩顶级的逻辑:

#include <stdio.h>

int main() {
    int score;
    char class;
    puts("请输入你的考试成绩: ");
    scanf("%d", &score);
    if (score < 0 || score > 100) {
        puts("成绩应该为不小于0且不大于100的数");
        return 0;
    }
    switch (score / 10) {
        case 10:
        case 9:
            puts("A");
            break;
        case 8:
            puts("B");
            break;
        case 7:
            puts("C");
            break;
        case 6:
            puts("D");
            break;
        default:
            puts("E");
    }
    
    return 0;

}

5.3 循环结构

5.3.1 while

while (exp) { //最少执行0次
    loop;
}

练习 求1-100和

#include <stdio.h>

int main() {

    int sum = 0;
    int i = 0;
    while (i <= 100) sum += i++;
    printf("sum = %d", sum);
    return 0;

}

5.3.2 do-while

do { //最少执行1次
    loop;
} while (exp);

练习 求1-100和

#include <stdio.h>

int main() {

    int sum = 0;
    int i = 0;
    do {
        sum += i++;
    } while (i <= 100);
    printf("sum = %d", sum);
    return 0;

}

5.3.3 for

for (exp1; exp2; exp3) {// 最少循环次数为0次
    loop;
}

练习 求1-100和

#include <stdio.h>

int main() {

    int sum = 0;
    int i = 1;
    for (; i <= 100; i++) {
        sum += i;
    }
    printf("sum = %d", sum);
    return 0;

}

5.3.4 if-goto

该结构慎用,因为goto表示无条件跳转,且不能跨函数跳转

#include <stdio.h>

int main() {

    int sum = 0;
    int i = 1;
    loop:
    sum += i;
    i++;
    if (i <= 100)
        goto loop;
    printf("sum = %d", sum);
    return 0;

}

5.3.5 辅助控制

break 表示跳出最近一层的循环

continue: 表示后面的语句不在执行,即跳出本次循环

5.4 练习题

练习题一

A以每年10%的单利息投资了100美元,B以每年5%的复合利息投资了100美元。编写一个程序,计算需要多少年B的投资总额才会超过A,并且显示出到了哪个时刻两个人各自的资产总额。

#include <stdio.h>

int main() {

    double base = 100;
    double sumA = base, sumB = base;
    int y = 0;
    while (sumB <= sumA) {
        sumA += base * 0.1;
        sumB = sumB * (1 + 0.05);
        y++;
    }
    printf("在第%d年,B的资产大于A,B的资产为%lf, A的资产为%lf", y, sumB, sumA);
    return 0;

}

练习题二

从终端读入数据,直到输入0为止,计算出其中的偶数的个数及平均值和奇数的个数及平均值

int main() {

    int cntS = 0, cntD = 0;
    int sumS = 0, sumD = 0;
    int num;
    for ( ; ; ) {
        puts("请输出一个数: ");
        scanf("%d", &num);
        if (num == 0) {
            break;
        }
        if (num % 2 == 0) {
            cntD++;
            sumD += num;
        } else {
            cntS++;
            sumS += num;
        }
    }
    printf("共有奇数%d个,奇数的平均数为%lf; 共有偶数%d个,偶数的平均数为%lf", cntS,
           sumS * 1.0 / cntS, cntD, sumD * 1.0 / cntD);

}

练习题三

从终端上输入若干字符,对其中的元音字母进行统计

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

#define STRLEN 32

int main() {

    char str[STRLEN];
    int cnt = 0;
    printf("请输入一个长度小于%d的字符串: \n", STRLEN - 1);
    scanf("%s", str);
    int len = strlen(str);
    for (int i = 0; i < len; i++) {
        char c = str[i];
        if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
            cnt++;
        }
    }
    printf("输入的字符串中有%d个元音字符。", cnt);
    return 0;

}

练习题四

写出fibonacci数列的前40项(1, 1, 2, 3…)

int main() {

    int n1 = 1; int n2 = 1;
    printf("%d %d ", n1, n2);
    for (int i = 3; i <= 40; i++) {
        int tmp = n1 + n2;
        n1 = n2;
        n2 = tmp;
        printf("%d ", n2);
    }
    return 0;

}

练习题五

输出九九乘法表

int main() {

    for (int i = 1; i <= 9; i++) {
        for (int j = 1; j <= i; j++) {
            printf("%d * %d = %d ", j, i, i * j);
        }
        printf("\n");
    }
    return 0;

}

练习题六

百钱买百鸡:鸡翁一,值钱五;鸡母一,值钱三;三鸡雏,值钱一;百钱买百鸡,问鸡翁,鸡母,鸡雏各几何

int main() {

    /**
     * 7x + 4y = 100;
     * z = 100 - 5 * x - 3 * y
     * 0 <= x <= 14; 0 <= y <= 25; 0 <= z <= 100;
     */
     int cnt = 1;
     int x = 0, y = 0, z = 0;
     for (x = 0; x <= 14; x++) {
         int tmp = 100 - 7 * x;
         if (tmp % 4 != 0) continue;
         y = tmp / 4;
         z = (100 - 5 * x - 3 * y) * 3;
         printf("第%d种买法: 鸡翁%d个, 鸡母%d个, 鸡雏%d个\n", cnt, x, y, z);
         cnt++;
     }
    return 0;

}

练习题七

求出1000以内的水仙花数,水仙花数:个十百位的立方相加为当前数字

int main() {

    for (int i = 100; i < 1000; i++) {
        int tmp = i;
        int n = 0;
        while (tmp > 0) {
            int t = tmp % 10;
            n += t * t * t;
            tmp /= 10;
        }
        if (n == i) {
            printf("%d是水仙花数\n", i);
        }
    }
    return 0;

}

练习题八

求出1000以内的质数:2, 3, 5, 7…

int main() {

    for (int i = 2; i <= 1000; i++) {
        bool flag = true;
        for (int j = 2; j < i / 2; j++) {
            if (i % j == 0) {
                flag = false;
                break;
            }
        }
        if (flag) {
            printf("%d ", i);
        }
    }
    return 0;

}

练习题九

在中断上实现如下输出

ABCDEF

BCDEF

CDEF

DEF

EF

F

int main() {

    char c = 'A';

    for (int i = 5; i >= 0; i--) {
        for (int j = 0; j <= i; j++) {
            printf("%c", c + j);
        }
        c++;
        printf("\n");
    }
    return 0;

}

练习题十

输出钻石形

int main() {

    int r = 7;
    int max = (r + 1) / 2;
    for (int i = 1; i <= r; i++) {
        int blank = i > max ? i - max : max - i;
        int cnt = max - blank;
        for (int j = 0; j < blank; j++) {
            printf(" ");
        }
        for (int j = 0; j < cnt; j++) {
            printf("* ");
        }
        printf("\n");
    }
    return 0;

}

练习题十一

从终端输入N个数(以字母Q/q作为终止),求和

int main() {

    int sum = 0;
    int num = 0;
    puts("请输入一个数(输入q/Q终止): ");
    while (scanf("%d", &num) == 1) {
        if (num == 'q' || num == 'Q') break;
        sum += num;
        puts("请输入下一个值(输入q/Q终止)");
    }
    printf("您输入的数据的总和为%d", sum);
    return 0;

}

练习题十二

从半径为1开始,输出圆的面积,直到面积大于100为止

#define PI 3.14

int main() {

    int r = 1;
    double area = PI * r * r;
    while (area <= 100) {
        printf("%lf ", area);
        r++;
        area = PI * r * r;
    }

    return 0;

}

6 数组

数组是构造类型之一,且在内存中连续存储数据

6.1 一维数组

6.1.1基本概念

定义

【存储类型】 数据类型 标识符【下标(整型的常量或者常量表达式/C99以后支持变长数组)】

初始化

  • 未初始化的时候数组内未随机值
  • 全部初始化
  • 部分初始化 未初始化到的元素赋值为0
  • 若存储类型为static,则即使未被初始化也会全部初始化0值

元素的引用

数组名【下标】

数组名

数组名是表示地址的常量,也是整个数组的起始位置

数组越界

访问的下标超过了数组的长度

6.1.2 练习题

斐波那契数列

int main() {

    int dp[40] = {1, 1};
    for (int i = 2; i < 40; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    for (int i = 0; i < 40; i++) {
        printf("%d ", dp[i]);
    }

    return 0;

}

冒泡排序

void bubble_sort(int * arr, int len) {
    for (int i = 0; i < len; i++) {
        for (int j = 0; j < len - i -1; j++) {
            if (arr[j] > arr[j + 1]) {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

int main() {
    int arr[6] = {13, 5, 4, 3, 2, 1};
    bubble_sort(arr, 6);
    for (int i = 0; i < 6; i++) {
        printf("%d ", arr[i]);
    }

    return 0;
}

选择排序

void select_sort(int * arr, int len) {
    for (int i = 0; i < len; i++) {
        int t = i;
        for (int j = t; j < len; j++) {
            if (arr[j] < arr[t]) {
                t = j;
            }
        }
        int tmp = arr[t];
        arr[t] = arr[i];
        arr[i] = tmp;
    }
}

int main() {
    int arr[6] = {13, 5, 4, 3, 2, 1};
    select_sort(arr, 6);
    for (int i = 0; i < 6; i++) {
        printf("%d ", arr[i]);
    }

    return 0;
}

进制转换

int main() {

    int num, cnt;
    printf("请输入待转换的数值: \n");
    scanf("%d", &num);
    printf("请输入你要转换的进制(2~16): \n");
    scanf("%d", &cnt);
    char map[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'A', 'B', 'C', 'D', 'E', 'F'};
    int idx = 0;
    char ans[64];
    while (num != 0) {
        int t = num % cnt;
        ans[idx++] = map[t];
        num /= cnt;
    }

    for (int i = idx - 1; i >= 0; i--) {
        printf("%c", ans[i]);
    }
    return 0;
}

删除法求质数

int main() {

    int isPrime[1001] = {0};
    for (int i = 2; i < 1001; i++) {
        for (int j = i * i; j < 1001; j += i) {
            isPrime[j] = 1;
        }
    }
    for (int i = 2; i < 1001; i++) {
        if (isPrime[i] == 0) {
            printf("%d ", i);
        }
    }
    return 0;
}

6.2 二维数组

6.2.1 基本概念

定义与初始化

​ 【存储类型】 数据类型 标识符【行下标】【列下标】 (行下标是可以省略的)

  • 初始化规则和一维数组一样

元素引用

数组名【行标】【列标】

存储形式

在内存中顺序存储,且按行存储

6.2.2 练习题

练习题一

二维数组行列互换

#include <stdio.h>
#define M 2
#define N 3

int main() {

    int arr[M][N] = {{1, 2, 3}, {4, 5, 6}};
    int ans[N][M];
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < M; j++) {
            ans[i][j] = arr[j][i];
            printf("%d ", ans[i][j]);
        }
        printf("\n");
    }

    return 0;
}

练习题二

二维数组求最大值及其下标

#include <stdio.h>
#define M 2
#define N 3

int main() {

    int arr[M][N] = {{1, 7, 3}, {4, 5, 6}};
    int x = 0, y = 0;
    for (int i = 0; i < M; i++) {
        for (int j = 0; j < N; j++) {
            if (arr[i][j] > arr[x][y]) {
                x = i; y = j;
            }
        }
    }
    printf("数组的最大值为arr[%d][%d]=%d", x, y, arr[x][y]);

    return 0;
}

练习题三

求二维数组当中各行和各列的和

int main() {

    int arr[M][N] = {{1, 7, 3}, {4, 5, 6}};
    int x = 0, y = 0;
    for (int i = 0; i < M; i++) {
        int sum = 0;
        for (int j = 0; j < N; j++) {
            sum += arr[i][j];
        }
        printf("第%d行的和为%d\n", i, sum);
    }
    for (int j = 0; j < N; j++) {
        int sum = 0;
        for (int i = 0; i < M; i++) {
            sum += arr[i][j];
        }
        printf("第%d列的和为%d\n", j, sum);
    }
    return 0;
}

练习题四

求矩阵的乘积

#include <stdio.h>
#define M1 2
#define N1 3
#define M2 3
#define N2 3
int main() {

    int arr1[M1][N1] = {{1, 2, 3},
                        {4, 5, 6}};
    int arr2[M2][N2] = {{1, 2, 1},
                        {3, 4, 3},
                        {5, 6, 5}};
    int ans[M1][N2] = {0};
    for (int i = 0; i < M1; i++) {
        for (int j = 0; j < N2; j++) {
            for (int k = 0; k < N1; k++) {
                ans[i][j] += arr1[i][k] * arr2[k][j];
            }
            printf("%d ", ans[i][j]);
        }
        printf("\n");
    }
    return 0;
}

6.3 字符数组

6.3.1 基本概念

定义,初始化

【存储类型】 char 标识符【下标】

  • 可以单个字符初始化
  • 可以使用字符串常量来初始化

存储特点

对于字符串来说,就是一个字符数组,且字符数组的最后一位是’\0’

6.3.2 常用函数

strlen 和 sizeof

strlen返回当前字符串的长度,sizeof返回当前元素在内存所占的字节数

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

int main() {

    char str[] = "hello";
    printf("%d\n", strlen(str));//5
    printf("%d\n", sizeof (str));//6
    return 0;
}

strcpy和strncpy

函数定义 复制串

/* Copy SRC to DEST.  */ 包括尾0
extern char *strcpy (char *__restrict __dest, const char *__restrict __src)
     __THROW __nonnull ((1, 2));
/* Copy no more than N characters of SRC to DEST.  */
extern char *strncpy (char *__restrict __dest,
            const char *__restrict __src, size_t __n)
     __THROW __nonnull ((1, 2));

strncpy中n常用dest的大小来保证复制过去的串不会超过目标字符数组的大小

strcat和strncat

函数定义 追加串

/* Append SRC onto DEST.  */
extern char *strcat (char *__restrict __dest, const char *__restrict __src)
     __THROW __nonnull ((1, 2));
/* Append no more than N characters from SRC onto DEST.  */ 如果SRC不够n则追加到'\0'为止
extern char *strncat (char *__restrict __dest, const char *__restrict __src,
            size_t __n) __THROW __nonnull ((1, 2));

n参数的目的同样是为了保证dest存储区域足以容纳src中追加的内容,防止越界

#include <stdio.h>
#include <string.h>
#define STRSIZE 32
int main() {

    char str[STRSIZE] = "hello";
    strncat(str, " ", STRSIZE - strlen(str) - 1);
    strncat(str, "world!", STRSIZE - strlen(str) - 1);
    puts(str);
    return 0;
}

strcmp和strncmp

函数定义 字符串的比较

/* Compare S1 and S2.  */
extern int strcmp (const char *__s1, const char *__s2)
     __THROW __attribute_pure__ __nonnull ((1, 2));
/* Compare N characters of S1 and S2.  */
extern int strncmp (const char *__s1, const char *__s2, size_t __n)
     __THROW __attribute_pure__ __nonnull ((1, 2));

字符串比较比较的是ASCII码,返回负值表示str1的ascii比str2小,正值表示str1的ascii比str2大,0表示两个字符串相等

n参数就是指定比较的s2的大小

6.3.3 练习题

单词计数

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

int main() {

    char str[128];
    int cnt = 0;
    puts("请输入一个字符串: ");
    gets(str);
    int len = strlen(str);
    for (int i = 0; i < len; ) {
        if (str[i] == ' ') {
            i++;
        } else {
            cnt++;
            while ((str[i] >= 'a' && str[i] <= 'z') || (str[i] >= 'A' && str[i] <= 'Z')) i++;
        }
    }
    printf("共有%d个单词\n", cnt);
    return 0;
}

6.4 多维数组

譬如:

int a[2][3][4];

解释:

  • a是一个数组,有两个元素a[0]和a[1]
  • a[0]也是一个数组,有三个元素a [0] [0-2]
  • 上述元素每个元素又是一个一维数组,每个数组中含有4个整数

7 指针

7.1 变量和指针

要理解指针首先得理解内存,我们的内存可以认为就是一个非常大的一个字节数组,然后按照字节进行编址,这样每一个字节都有一个唯一的地址,而指针其实就是一个地址,再来看下面这段代码:

#include <stdio.h>

int main() {
    /**
     * &表示取值,*表示解引用
     * 获取i值的两种方式,直接用i为直接访问,*p和**q为间接访问
     */
    int i = 1;
    int *p = &i;
    int **q = &p;
    printf("i=%d, p=%p, *p=%d, q=%p, *q=%p, **q=%d", i, p, *p, q, *q, **q);
    return 0;
}

这段代码用一个图来表示就是这样的

在这里插入图片描述

7.2 空指针,野指针和空类型

空指针和野指针

int main() {
    int *p; //p这种定义了但没有指向的指针就是野指针
    int *q = NULL; //NULL就是空指针
    return 0;
}

空类型

void *,当不确定要使用什么样的类型指针的时候就可以使用void *,在使用的时候再将其转换为其他类型的指针

7.3 指针运算

关系运算

<, >, ==, <=, >=就是用来比较两个指针的大小的,即两个地址的大小

如下代码所示:

#include <stdio.h>

int main() {
    int i = 2,  j = 1;
    int *p = &i;
    int *q = &j;
    printf("p=%p, q=%p, p>q?%d", p, q, p < q);
    return 0;
}

输出为

p=0x7ffed07b8940, q=0x7ffed07b8944, p>q?1

加减运算

加减运算主要用在数组中,比如:

arr[1]是访问数组的第二个元素;
(arr + 1)就是指向数组第二个元素的指针,*(arr + 1)就是数组第二个元素的值了

7.4 指针与数组

7.4.1 指针和一维数组

定义数组时的哪个变量其实就是一个指针,区别在于a是一个数组名,是一个指向这个数组第一个元素的常量,而p是一个变量

int main() {
    int a[3];
    int *p;
    //a[i] : a[i] = *(a + i) = *(p+i) = p[i]
    //&a[i] : &a[i] = (a + i) = (p + i) = &p[i]
    return 0;
}

需要注意的地方:

  • sizeof(a)返回的是整个数组的大小,sizeof§返回的是指针的大小(64位,8B)
  • p能够执行p++和p–的运算,但是a不能用来做++和–的运算,因为a是一个常量

7.4.2 指针和二维数组

二维数组的数组名可以理解成一个二级指针,如下代码(a + i)是一个指针,指向第i行的元素(该元素是一个三列的一维数组), *(a + i)就是指向第i行的数组,*(a + i) + j是指向第i行第j列元素(一个整数)的一个指针,*(*(a + i) + j)就是第i行第j列的元素

int main() {
    int a[2][3] = {1, 2, 3, 4, 5, 6};
    //a[i][j] = *(*(a + i) + j)
    printf("%p %p\n", &a[1][2], (*(a + 1) + 2));
    printf("%d %d\n", a[1][2], *(*(a + 1) + 2));
    return 0;
}

输出:
0x7ffc8d002744 0x7ffc8d002744
6 6

7.4.3 指针与字符数组

相关操作实际上就是一个一维数组,但是需要注意字符串的特殊性如下代码:

#include <stdio.h>

int main() {
    char s1[] = "hello";//开辟了一块内存区域用作字符数组
    char *s2 = "hello";//s2和s3都指向字符串常量"hello",这个字符串常量是存储在一块特定的区域
    char *s3 = "hello";
    printf("s1=%p, s2=%p, s3=%p", s1, s2, s3);
    return 0;
}

输出:
s1=0x7ffcf6acfc82, s2=0x561b057ef7a4, s3=0x561b057ef7a4

7.5 const与指针

const修饰变量表示这个变量是不能变化的

int main() {
    //const修饰变量表示这个变量是不能改变的
    const int i = 1;
    i = 2;//报错
    return 0;
}

const修饰指针

  • 常量指针 const在前,表示这个指针指向的空间不能被修改

    int main() {
    
        int i = 1, j = 2;
        const int *p = &i;
        *p = 2;//报错,表示这个指针指向的空间并不能被修改
        p = &j;//成功,表示可修改这个指针的指向
        return 0;
    }
    
  • 指针常量 const在后,表示这个指针指向的内容可以变化,但不能改变其指向

    int main() {
    
        int i = 1, j = 2;
        int * const p = &i;
        *p = 2;//成功,表示这个指针指向的空间能被修改
        p = &j;//报错,表示不能修改这个指针的指向
        return 0;
    }
    
  • const前后都有表示指针的指向和指向的内容都不能变

    int main() {
    
        int i = 1, j = 2;
        const int * const p = &i;
        *p = 2;//报错,表示这个指针指向的空间不能被修改
        p = &j;//报错,表示不能修改这个指针的指向
        return 0;
    }
    

7.6 指针数组与数组指针

7.6.1 数组指针

​ 【存储类型】 数据类型 (* 指针名)【下标】 = 值;

int (*p)[3]; 
如上表示p是一个指针,指向一个含有三个元素的一维数组

示例 所以p和a在如下代码中是可以互换的了,最根本的区别只有p是变量,a是常量

#include <stdio.h>

int main() {
    int a[2][3] = {1, 2, 3, 4, 5, 6};
    int (*p)[3] = a;

    puts("===============用数组名表示================");
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%p -> %d\n", &a[i][j], a[i][j]);
        }
    }

    puts("===============用数组指针表示================");
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%p -> %d\n", *(p + i) + j, *(*(p + i) + j));
        }
    }
    return 0;
}

输出:
===============用数组名表示================
0x7ffdcaf28890 -> 1
0x7ffdcaf28894 -> 2
0x7ffdcaf28898 -> 3
0x7ffdcaf2889c -> 4
0x7ffdcaf288a0 -> 5
0x7ffdcaf288a4 -> 6
===============用数组指针表示================
0x7ffdcaf28890 -> 1
0x7ffdcaf28894 -> 2
0x7ffdcaf28898 -> 3
0x7ffdcaf2889c -> 4
0x7ffdcaf288a0 -> 5
0x7ffdcaf288a4 -> 6

7.6.2 指针数组

数组中的每个元素都是一个指针,如下:

int * arr[3];
arr[i]是一个指针,指向一个整数

示例

#include <stdio.h>

int main() {

    char * arr[5] = {"hello", "world", "tony", "hello", "text"};
    for (int i = 0; i < 5; i++) {
        printf("%p -> %s\n", arr[i], arr[i]);
    }
    return 0;
}

输出:
0x555a941ad7d4 -> hello
0x555a941ad7da -> world
0x555a941ad7e0 -> tony
0x555a941ad7d4 -> hello
0x555a941ad7e5 -> text

7.7 多级指针

多级指针实际就是指向指针的指针

8 函数

8.1 函数的定义

​ 数据类型 函数名(【形式参数说明表】)

8.2 函数的传参

  • 值传递 顾名思义就是传递参数的值,这时两个函数中传递的变量是互相不干扰的

  • 地址传递 传递参数的地址,在被调函数中操作这些地址的时候是会对原函数中的参数造成影响的

如下代码中,当swap是值传递的时候并不能交换main函数的i,j的值,当采用地址传递的时候就能够交换了

  • 值传递

    void swap(int i, int j) {
        int tmp = i;
        i = j;
        j = tmp;
    }
    
    int main() {
        int i = 3, j = 5;
        swap(i, j);
        printf("i=%d, j=%d\n", i, j);
    
        return 0;
    }
    
  • 地址传递

    void swap(int *p, int *q) {
        int tmp = *p;
        *p = *q;
        *q = tmp;
    }
    
    int main() {
        int i = 3, j = 5;
        swap(&i, &j);
        printf("i=%d, j=%d\n", i, j);
    
        return 0;
    }
    

8.3 函数的调用

8.3.1 函数的嵌套调用

嵌套调用就是函数A调用函数B,函数B调用函数C…

示例:求三个数中最大值和最小值之差

#include <stdio.h>

int max(int a, int b, int c) {
    if (a > b && a > c) return a;
    else if (b > a && b > c) return b;
    return c;
}

int min(int a, int b, int c) {
    if (a < b && a < c) return a;
    else if (b < a && b < c) return b;
    return c;
}

int dist(int a, int b, int c) {
    return max(a, b, c) - min(a, b, c);
}

int main() {
    int a = 3, b = 5, c = 10;
    printf("%d", dist(a, b, c));

    return 0;
}

8.3.2 函数的递归调用

递归就是一个函数直接或间接的调用自身

递归求阶乘

int test(int n) {
    if (n == 1 || n == 0) return 1;
    return n * test(n - 1);
}
int main() {

    printf("%d", test(4));

    return 0;
}

递归求解斐波那契数列

#include <stdio.h>

int fib(int n) {
    if (n == 1 || n  == 2) return 1;
    return fib(n - 1) + fib(n - 2);
}
int main() {

    printf("%d", fib(7));

    return 0;
}

汉诺塔

#include <stdio.h>

void hanoi(int n, char a, char b, char c) {
    if (n == 1) {
        printf("no%d: %c -> %c\n", n, a, c);
        return;
    }
    hanoi(n - 1, a, c, b);
    printf("no%d: %c -> %c\n", n, a, c);
    hanoi(n - 1, b, a, c);
}

int main() {

    hanoi(3, 'A', 'B', 'C');

    return 0;
}

8.4 函数与数组

8.4.1 函数与一维数组

当使用一维数组作为参数的是要需要指定一维数组的大小

void print_arr(int *p, int len) {
    for (int i = 0; i < len; i++) {
        printf("%d ", p[i]);
    }
    printf("\n");
}

int main() {

    int arr[] = {3, 4, 1, 2, 5};
    print_arr(arr, sizeof (arr) / sizeof (*arr));
    return 0;
}

8.4.2 函数与二维数组

可以通过数组指针和数组的形式来进行传递,注意传递时需要指明列数,同时传递数组的大小

#include <stdio.h>

void print_arr(int (*p)[4], int m, int n) {
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            printf("%d ", *(*(p + i) + j));
        }
        printf("\n");
    }
}

void print_arr1(int p[][4], int m, int n) {
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            printf("%d ", *(*(p + i) + j));
        }
        printf("\n");
    }
}

int main() {

    int arr[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

    print_arr1(arr, 3, 4);
    return 0;
}

8.4.3 函数与字符数组

注意传参时不变的字符数组可以加上const来修饰

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

char * my_strcpy(char * dest, const char * src) {
    if (dest == NULL || src == NULL) return NULL;
    char *ret = dest;
    while ((*dest++ = *src++) != '\0');
    return ret;
}

int main() {

    char *str1 = "helloworld";
    char str2[128];
    my_strcpy(str2, str1);
    printf(str2);
    return 0;
}

8.5 函数与指针

指针函数

返回值是一个指针的函数:

  • 定义

    类型 * 函数名(形参列表)

  • 例如

    int * func(int)

函数指针

一个指向函数的指针,有时候需要给一个函数传入一个函数指针,来告诉这个函数要使用哪个函数,比如排序算法中的比较方法。

  • 定义

    返回类型 (* 指针名) (形参列表)

  • 例如

    int (*p)(int);

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {

    int a = 3, b = 5;
    int ret;
    int (*p)(int, int) = add;
    ret = p(a, b);
    printf("%p %d", p, ret);
    return 0;
}

9 构造类型

9.1 结构体

9.1.1 概述

产生及意义

数组只能够连续存放相同类型的数据,结构体可以用来连续存放不同类型的数据,这些类型的数据可以看做是一个结构体的特征。

类型描述

struct 结构体名 {
    数据类型 成员1;
    数据类型 成员2;
    ......
};

9.1.2 嵌套定义

#define NAMESIZE 32

struct birthday {
    int year;
    int month;
    int day;
};

struct student {
    int id;
    char name[NAMESIZE];
    struct birthday birth;
    int math;
    int chinese;
};

9.1.3 结构体的使用

直接使用

通过结构体名.成员名来引用结构体中的成员

#include <stdio.h>

struct simp {
    int a;
    float b;
    char c;
};

int main() {

    //结构体定义及初始化
    /**
     * 部分成员初始化
     * struct simp sp = {.a = 1, .c = 'A};
     */
    struct simp sp = {123, 1.2, 'A'};
    //结构体的成员引用
    sp.a = 1;
    printf("%d, %f, %c", sp.a, sp.b, sp.c);

    return 0;
}

简洁使用——指针

通过==指针名->成员名【(* 指针名).成员名】==来访问成员

#include <stdio.h>

struct simp {
    int a;
    float b;
    char c;
};

int main() {

    //结构体定义及初始化
    struct simp sp = {123, 1.2, 'A'};
    struct simp * p = &sp;
    printf("%d, %f, %c", p->a, p->b, p->c);

    return 0;
}

9.1.4 结构体所占内存空间

结构体中的成员会自动对齐

#include <stdio.h>

struct simp {
    int a;
    float b;
    char c;
};

int main() {

    //结构体定义及初始化
    struct simp sp = {123, 1.2, 'A'};
    struct simp * p = &sp;
    printf("%d", sizeof sp);//输出12可以看到并不是9字节,这是因为系统要求需要对齐

    return 0;
}

若不想要自动对齐则可以加上_attribute_((packed))

#include <stdio.h>

struct simp {
    int a;
    float b;
    char c;
} __attribute__((packed));

int main() {

    //结构体定义及初始化
    struct simp sp = {123, 1.2, 'A'};
    struct simp * p = &sp;
    printf("%d", sizeof sp);//输出9

    return 0;
}

9.2 共用体

9.2.1 概述

产生及意义

当需要用多种条件来描述一种特种,同时某一时刻又只有一种特征存在的时候使用共用体

类型描述

union 共用体名 {
    数据类型 成员名1;
    数据类型 成员名2;
    ......
};
其中这些数据类型公用通过一块空间,且该块空间的大小为其中最大的数据类型

9.2.2 共用体的使用

成员的引用:

  • 共用体名.成员名
  • 指针名->成员名
union test {
    int i;
    float f;
    double d;
    char ch;
};

int main() {
    //成员定义
    union test t;
    //指针定义
    union test *p = &t;
    //成员初始化
    t.f = 3.14;
    printf("%f %f", t.f, p->f);

    return 0;
}

9.2.3 嵌套定义

结构体和共用体互相嵌套:

比如实现求一个数的高16位和低16位的和

#include <stdio.h>
#include <stdint.h>
union num {
    struct {
        uint16_t i;
        uint16_t j;
    } x;
    uint32_t y;
};

int main() {

    union num a;
    a.y = 0x11223344;
    printf("%x", a.x.i + a.x.j);

    return 0;
}

9.2.4 位域

union num {
    struct {
        char a : 1;//存储1位char
        char b : 2;//存储2位char
        char c : 1;//存储3位char
    } x;
    char y;
};

int main() {

    union num n;
    n.y = 13;
    /**
     * 打印结果为-1,-2,-1
     * 13:1101
     * c:1;b:10;a:1 打印的时候是%d,说明是以十进制补码打印
     */
    printf("a=%d b=%d c=%d", n.x.a, n.x.b, n.x.c);

    return 0;
}

9.3 枚举

定义

enum 标识符 {
    成员1;
    成员2;
    .....
}

使用

可以认为枚举类型就是整数,enum可以给我们定义一系列整数,或者胡搜enum帮助我们定义了一系列的宏

#include <stdio.h>

enum day {
    MON,
    TUS,
    THR,
    WES,
    FRI,
    SAT,
    SUN
};

int main() {

    enum day a = FRI;
    /**
     * 打印出来4,当我们在定义enum时没有定义的时候就会从0~6以此给MON~SUN赋值
     */
    printf("%d\n", a);
    //可以将enum中的定义看做宏直接使用
    printf("%d", MON);//打印0

    return 0;
}

10 动态内存管理

10.1 相关函数

  • malloc() 申请size字节的空间
  • calloc() 申请nmemb*size字节的空间
  • realloc() 重新给ptr申请size字节的空间
  • free()释放给ptr申请的空间

注意内存申请的函数遵守谁申请谁释放的使用原则

/* Allocate SIZE bytes of memory.  */
extern void *malloc (size_t __size) __THROW __attribute_malloc__ __wur;

/* Allocate NMEMB elements of SIZE bytes each, all initialized to 0.  */
extern void *calloc (size_t __nmemb, size_t __size)
__THROW __attribute_malloc__ __wur;

/* Re-allocate the previously allocated block in __ptr, making the new
   block SIZE bytes long.  */
/* __attribute_malloc__ is not used, because if realloc returns
   the same pointer that was passed to it, aliasing needs to be allowed
   between objects pointed by the old and new pointers.  */
extern void *realloc (void *__ptr, size_t __size)
__THROW __attribute_warn_unused_result__;

/* Free a block allocated by `malloc', `realloc' or `calloc'.  */
extern void free (void *__ptr) __THROW;

10.2 函数的使用

10.2.1 基本使用

#include <stdio.h>
#include <malloc.h>

int main() {

    int len = 5;
    int *p = NULL;
    p = malloc(sizeof (int) * len);
    if (p == NULL) {
        printf("malloc() error");
        return 0;
    }
    for (int i = 0; i < len; i++) {
        scanf("%d", &p[i]);
    }
    for (int i = 0; i < len; i++) {
        printf("%d ", p[i]);
    }
    printf("\n");
    return 0;
}

10.2.2 注意事项

在其他函数中申请空间,在主调函数中释放空间的情况

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

void func(int *p, int num) {
    p = malloc(num);
    if (p == NULL) {
        printf("malloc() error");
        exit(1);
    }
}

int main() {

    int len = 4;
    int *p = NULL;
    /**
     * 注意这时已经出现内存泄漏了,因为在func函数中p指向了新申请的空间,但是main函数中的
     * p仍然是NULL,若想这样在函数中申请空间,则需要再func函数中将申请的首地址返回
     * 即 p = func(p, len)
     */
    func(p, len);
    free(p);
    return 0;
}

释放的指针的使用

比如在如下代码中,可以看到free后p仍然是指向之前申请的空间,表示这个指针还是可以用的,就类似在使用野指针,所以在free指针之后,我们应该最好再加上p=NULL

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

int main() {

    int len = 4;
    int *p = NULL;
    p = malloc(len);
    *p = 6;
    printf("%p--->%d\n", p, *p);//0x558049b83260--->6
    free(p);
    printf("%p--->%d", p, *p);//0x558049b83260--->0
    return 0;
}

10.3 typedef

用法

为已有的数据类型改名

typedef 已有的数据类型 新名字;

11 静态库和动态库

11.1 静态库

静态库:
libxx.a	xx指代库名

ar -cr libxx.a yyy.o

发布到
/usr/local/include (.h)
/use/local/lib	(.a)

使用静态库:
gcc -L/usr/local/lib -o main main.o -lxx
-l参数必须在最后,有依赖

11.2 动态库

动态库:
libxx.so

gcc -share -fpic -o libxx.so yyy.c

发布到:
/usr/local/include	(.h)
/usr/local/lib	(.so)

在	/etc/ld.so.conf中添加路径
/sbin/ldconfig	重读/etc/ld.so.conf

使用动态库:
gcc -I/usr/local/include -L/usr/local/lib -o ... -lxx
ldd - print shared library dependencies
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值