Featured image of post C语言学习记录

C语言学习记录

C语言学习笔记总结喵!不保证内容100%无误哦!

基础语法

C语言是一门通用计算机编程语言,广泛应用于底层开发。C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。

二进制语言(101011110) -> 汇编语言(mov eax 10) -> 高级语言(如B语言, C语言等等)

输出 hello world!

1
2
3
4
5
6
7
#inculde <stdio.h> //引入头文件

int main() //main函数
{
    printf("hello world!"); //printf 基本输出函数
    return 0; //返回值 0
}

头文件:C 语言中包含函数声明和宏定义的文件,可被多个源文件中引用共享

main 函数:C 语言中最主要的函数,程序的入口,代码从这里依次往下执行

变量

1
2
int a = 114514;
char str = "hello";

可以被改变的量,必须先声明后才可以使用

在函数内申明的变量叫局部变量,局部变量的作用域是局部变量所在的局部范围。出范围就会销毁

在函数体外声明的变量叫全局变量,全局变量的作用域是程序的生命周期

若有全局变量和局部变量一样的情况,局部变量的优先级更高

变量的声明

1
int i;

数据类型 变量名;

如果需要在一个源文件中引用另一个源文件中的变量,只需要在要引用的文件中加上 extern 关键字即可

1
extren int i;

变量的初始化

1
2
3
int i; //不完全初始化
i = 3; //给变量赋值
int i = 3; //初始化变量

常用基础数据类型

1
2
3
4
5
6
7
char ch = 'a';
short a = 10;
int b = 25565;
long c = 114514145;
long long d = 1145145208910;
float e = 3.14;
double f = 3.1415926535;
  • char 字符数据类型 1 字节 范围:(有符号:-128 ~ 127/ 无符号 0 ~ 255)
  • short 短整型 2 字节 范围:(-32768 ~ 32767)
  • int 整型 4 字节 范围:(-2147483648 ~ 2147483647)
  • long 长整型 4 字节 范围:(-2147483648 ~ 2147483647)
  • long long 更长的整型 8 字节
  • float 单精度浮点数 4 字节 范围:(1.2E-38 ~ 3.4E+38)
  • double 双精度浮点数 8 字节 范围:(2.3E-308 ~ 1.7E+308)

char 类型也可以通过ACSII码表等价于 int 来使用

常量

不能被改变的量被称为常量,分为如下 4 种

  • 字面常量

  • const 修饰的常变量

  • #define 定义的标识符常量

  • 枚举常量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#define A 10 //define定义的常量

enum Color //枚举常量
{
    red;
    green;
    blue;
};

int main()
{
    1; //字面常量
    const int m = 100; //const修饰的常变量

    return 0;

}

字符串

一串字符就是字符串,用双引号括起来

1
char str[] = "hello world"; 

\0 转义字符代表了该字符串的结束(包括strlen()库函数的返回值),所有定义的字符串最后面都有 \0,只是被隐藏了

若一个字符数组内没有加入\0,则strlen()的返回值为随机值

基本输出函数 printf()

1
2
3
4
5
int a = 2;
char ch = 'a';
double b = 3.14;

printf("%c=%d,圆周率是%lf", ch, a, b); //输出:a=2,圆周率是3.14

格式: printf("占位符与内容", 替换);

其中占位符会替换后面的变量,常用的占位符有

  • %c 读入一个字符

  • %d 读入十进制数

  • %lld 输入一个长整数

  • %o 读入八进制整数

  • %x 读入十六进制整数

  • %s 读入一个字符串,遇到空格、制表符、或换行符结束

  • %lf 读入一个浮点数

  • %p 按十六进制读入一个指针 (内存地址)

  • %u 读入一个无符号整型

其中,如果需要保留小数后的位数,可以这样写

1
printf("%2lf", 3.14159265357); //保留小数点后两位

基本输入函数 scanf()

1
2
int a =0;
scanf("%d", &a); //将用户输入的值赋值给变量a

格式: scanf("占位符", 变量的地址);

其中占位符同 printf () 函数


判断 / 选择语句

布尔值、真与假

在 C 语言中,非数字 0 代表条件为真(true);数字 0 或空(NULL)代表条件为假(false)

if

1
2
3
4
5
6
int a = 1;

if (a) //if语句
{
    printf("%d", a);
}

if,即如果,如果 if 后面的 () 内的表达式为真则执行 if 后面的代码

其格式为:

1
2
3
4
if (/*表达式*/)
{
    //表达式为真执行的代码
}

else

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int a = 0;

if (a)
{
    printf("true");
}
else
{
    printf("flase");
}

else,与 if 搭配使用。即当 if 中的表达式不满足条件时执行的代码

其格式为:

1
2
3
4
5
6
7
8
if (/*表达式*/)
{
    //表达式为真时执行的代码
}
else
{
    //不满足表达式的条件时执行的代码
}

悬空 else:else 会与最近的 if 相匹配

else if

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int a = 2;

if (a == 1)
{
    printf("a=1");
}
else if (a == 2)
{
    printf("a=2");
}
else
{
    printf("a=other");
}

else if,如果不满足 if 中表达式的条件则尝试匹配 else if 后括号内表达式的内容。在一段判断语句中可有多个 else if

其格式为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
if (/*表达式1*/)
{
    //表达式1为真时执行的代码
}
else if (/*表达式2*/)
{
    //表达式2为真时执行的代码
}
else
{
    //不满足以上所有表达式的条件时执行的代码
}

在代码书写上建议添加大括号,即使执行的代码只有一句。

switch

switch 语句是一种有限制的控制流语句,它用于根据表达式的值执行不同的代码块

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
char ch; //定义变量ch,但未初始化变量
scanf("%c", ch); //将用户输入的值存储在变量ch中

switch (ch) //switch根据ch的值进行判断
{
    case 'a'://若ch的值为字符a
        //条件成立执行的代码
        printf("input a");
        break; //结束判断
    case 'b'://若ch的值为字符b
        //条件成立执行的代码
        printf("input b");
        break; //结束判断
    case 'c'://若ch的值为字符c
        //条件成立执行的代码
        printf("input c");
        break; //结束判断
    default://若以上条件都不满足
        //若以上条件都不满足执行的代码
        printf("input other");
        break; //结束判断
}
  • case: 选项标签,若 case 标签后的值符合则执行下面的代码

  • break (可选): 退出判断,若在单个 case 中没有写 break,将自动往后执行后面的 case 内的语句而不会退出判断

  • default (可选): 若以上所有 case 条件都不满足执行,相当于默认执行语句

注意:

  • case 标签后的值必须是一个常量,不能被改变

  • case 标签的顺序并不重要,可以按照任意顺序编写,编译器会从上往下依次匹配

  • break 不是必须的,只要符合你的运行逻辑就可以了

switch() 内的判断变量必须是整型,不能是浮点数


循环语句

while 循环

while 循环是一种基础的循环,当表达式为 true 则进入循环

1
2
3
4
5
6
7
8
int a = 0; //初始化变量

while (a <= 5) //当a<=5时进入循环
{
    //循环体
    printf("%d", a);
    a++;
}

其格式为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
while (/*表达式*/)
{
    //循环体
    if (/*表达式*/)
    {
        break; //用于中止循环
    }
    else
    {
        continue; //跳过下面的代码,重新开始循环
    }
    //循环体
}
  • break (可选): 结束循环,无论后面还是否有代码,条件是否还满足

  • continue (可选): 跳过后面的代码,重新开始循环

for 循环

在 for 循环中,将初始化,判断,调整部分作为参数值包含在括号内

1
2
3
4
for (int i = 0; i <= 10; i++)
{
    printf("%d", i);
}

其格式为:

1
2
3
4
for (/*初始化*/; /*判断表达式*/; /*调整部分*/)
{
    //循环体
}
  • break (可选): 结束循环,无论后面还是否有代码,条件是否还满足

  • continue (可选): 跳过后面的代码,重新开始循环

循环内部不要重复写循环变量,容易使 for 循环失去控制

在循环体内改变循环变量的值不可取

通常使用左闭右开区间的写法

初始化,判断部分,调整部分均可以省略,这样如果在循环体内没有写退出的判断语句的话,回导致死循环

且一个 for 循环可以同时初始化 2 个变量

do…while 循环

do…while 循环与 while 循环最大的区别是 do…while 循环会先执行依次循环体内的代码,然后再进行判断

1
2
3
4
5
6
7
int a = 0;
int count = 0;

do
    count++;
    printf("%d", count);
while (a);

其格式为:

1
2
3
do
    //循环语句
while (/*判断表达式*/);
  • break (可选): 结束循环,无论后面还是否有代码,条件是否还满足

  • continue (可选): 跳过后面的代码,重新开始循环

goto 语句

前往一个语句,允许把控制无条件转移到同意函数内的被指定的语句

不建议使用!容易出现混乱

最常用的就是跳出多层嵌套循环

且 goto 语句只能在一个函数范围内跳转,不能跨函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
for(...)
{
        for(...)
        {
            for(...)
            {
                if(disaster)
                {
                    goto error;
                }
            }
        }
}
...

error:
    if(disaster)
    {
        //处理错误情况
    }

操作符

运算符是一种告诉编译器执行特定的数学或逻辑操作的符号

算数操作符

  • + 加法操作符 对操作符左右两边进行加法运算

  • - 减法操作符 对操作符左右两边进行减法运算

  • * 乘法操作符 对操作符左右左边进行乘法运算

  • / 除法操作符 将操作符左边的数除以右边 (仅返回整数)

  • % 取余运算符 将操作符左边的数除以右边并返回余数 (两端的操作数必须都为整数)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main()
{
    int a = 12;
    int b = 5;

    printf("%d", a + b); //17
    printf("%d", a - b); //7
    printf("%d", a * b); //60
    printf("%d", a / b); //2
    printf("%d", a % b); //2

    printf("%d", ++a); //13
    printf("%d", a--); //13
    printf("%d", a); //12

    return 0;
}

如何让 / 运算符输出小数

一般情况下,/ 运算符得到的商不会是小数,即使是两个 float 类型的数相除也是整数,因此

1
2
3
4
float a = 6.28;

printf("%2lf", a / 2); //3
printf("%2lf", a / 2.0); //3.14

要让除数 / 被除数是小数

关系运算符

  • == 检查两个操作数是否相等,如果相等条件为真

  • != 检查两个操作数是否相等,如果不相等条件为真

  • > 检查操作符左边是否大于右边,如果大于条件为真

  • < 检查操作符左边是否小于右边,如果小于条件为真

  • >= 检查操作符左边是否大于等于右边,如果大于或等于条件为真

  • <= 检查操作符左边是否小于等于右边,如果小于或等于条件为真

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if (a == b)
{
    printf("a=b");
}
else if (a > b)
{
    printf("a>b");
}
else
{
    printf("a<b");
}

//执行结果为 a>b

逻辑运算符

  • && 逻辑与运算符,如果左右两个表达式都为真,则表达式为真

  • || 逻辑或运算符,如果左右两个表达式有一个为真,则表达式为真

  • ! 取反 (逻辑非)运算符,逆转后面表达式的逻辑状态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int a = 10;
int b = 0;

if (a && b)
{
    printf("a&&b为true\n");
}
if (a || b)
{
    printf("a||b有一个为true\n");
}
if ( !b )
{
    printf("b为flase\n");
}

//执行结果为
//a||b有一个为true
//b为flase

对于 && 操作符来说,如果第一个表达式为假,后面的表达式不会进行计算

对于||操作符来说,如果第一个表达式为真,后面的表达式都不会进行计算

赋值操作符

  • = 赋值操作符,将操作符右边的数赋值给左边

  • += 加等运算符,将操作符右边的数累加给左边

  • -= 减等运算符,将操作符左边的数减去右边并赋值给操作符左边的变量

  • *= 乘等运算符,将操作符右边的数乘给左边

  • /= 除等运算符,将操作符左边的数除以右边并将结果赋值给左边

  • %= 模等运算符,将操作符左边的数除以右边得到余数后赋值给左边

  • ++ 自增运算符 将整数值增加 1

  • –- 自减运算符 将整数值减少 1

1
2
3
4
5
6
7
8
int a = 10;
int b = 20;

a += b;
printf("%d", a); //30

a -= b;
printf("%d", a); //10

自增 / 自减中前置与后置的区别

前置 ++:先自增后使用

1
2
3
int a = 10;

printf("%d", ++a) //11

后置 ++:先使用再自增

1
2
3
int a = 10;

printf("%d", a++); //10

请不要过分追求 ++-- 等运算符的结果 因为没有意义且不同编译器结果可能不同

1
2
3
4
5
6
7
8
9
int main()
{
    int a = 1;
    int b = (++a) + (++a) + (++a);
    
    printf("%d\n", b);
    
    return 0
}

上面这段代码在MSVC编辑器运行的结果为12,而在Linux系统gcc编译器运行的结果为10

  • «= 左移并赋值运算符

  • >>= 右移并赋值运算符

  • &= 按位与并赋值运算符

  • ^= 按位异或并赋值运算符

  • |= 按位或并赋值运算符

位运算符

  • & 按位与运算符:按二进制位,如果相同为 1,不同为 0

  • | 按位或运算符:按二进制位,如果有 1 则为 1,如果没有 1 则为 0

  • ^ 按位异或运算符:按对应的二进制位进行异或,相同位 0,相异为 1

  • << 左移运算符,将左边丢弃,右边补 0

  • >> 右移运算符,①算术右移:右边丢弃,左边补原符号位;②逻辑右移:右边丢弃,左边补 0

位操作符所运算的数字必须是整数

所有位操作符所操作的对象都为二进制位

  • 原码 直接写出来的二进制数
  • 反码 原码的所有位按位取反
  • 补码 反码 + 1
  • 移码 补码的基础上将符号位按位取反(仅能表示整数)

单目操作符

只有一个操作数的情况下叫做单目操作符

  • + 正号

  • - 负号

  • sizeof 计算和统计操作符的类型变量等所占用内存空间的大小,结果为 unsigned int 类型 (不是函数! 是操作数符!)

  • ~ 对一个数的二进制位进行按位取反,原来的 1 变成 0,原来的 0 变成 1。包括符号位

  • & 取地址操作符,取出该变量的地址

  • * 解引用操作符 / 指针变量类型 / 间接访问操作符

  • (类型) 强制类型转换操作符

1
2
3
4
int a = 10;
int* pa = &a;

pirntf("%d", *pa); //10

sizof 括号中存入的表达式是不参与运算的,并且会返回第一个读取到的变量值

因为 sizeof 是在编译阶段进行处理的,而表达式是在运行阶段才执行的

其他操作符

  • exp1?exp2:exp3 三目操作符:exp1 是否满足?满足执行 exp2,不满足执行 exp3

  • , 逗号表达式,重做向右依次执行,结果位最后一个表达式的结果

  • [] 下标引用操作符

  • () 函数调用操作符

  • . 结构体成员访问操作符 (变量)

  • -> 结构体成员访问操作符 (指针)

1
2
3
4
5
6
7
int a = 0;
int b = 3;
int c = 5;

int d = (a = b + 2, c= a - 4, b = c + 2);
//        a=5         c=1       b=3        d=b=3
printf("%d", d);

[] 操作符的操作数有两个,一个是数组名,一个是下标准

操作符优先级

类别 运算符 结合性
后缀 () [] -> . ++ – 从左到右
一元 + - ! ~ ++ – (tpye)* & sizeof 从右到左
乘除 * / % 从左到右
加减 + - 从左到右
移位 « » 从左到右
关系 < <= > >= 从左到右
相等 == != 从左到右
位与 and & 从左到右
位异或 ^ 从左到右
位或 | 从左到右
逻辑与 and && 从左到右
逻辑或 OR || 从左到右
条件 ?: 从右到左
赋值 = += - += *= /= %= »= «= &= ^= |= 从右到左
逗号 , 从左到右

关键字

函数 / 变量名不能是关键字

以下为部分常见关键字:

  • auto 每个局部变量都是由 auto 修饰的 比如 auto int a = 10 只不过默认省略

  • const 常变量 将一个变量修饰为常变量(严格来说不算常量)

  • extern 用于声明外部符号(如变量、函数等)

  • register 寄存器关键字 用于建议编译器将此变量存储在寄存器中

  • signed 有符号的

  • unsigned 无符号的

  • static 静态的 改变生命周期(本质上是改变了变量的存储类型) 只能在自己所在的源文件中使用

  • typedef 类型重命名 可以将一个类型重命名另一个关键字 如 typedef unsigned int u_int

  • volatile ?

请注意 defineinclude 不是关键字,而是预处理指令


函数

函数分为库函数和自定义函数两种,库函数为 C 编译器编译的函数如:

  • IO函数 printf() scnaf() getchar() putchar()

  • 字符串操作函数 strcmp() strlen()

  • 字符操作函数 toupper()

  • 内存操作函数 memcpy() memcmp() memset()

  • 时间日期函数 time()

  • 数学函数 sqrt() pow()

  • 其他…

学习库函数两个网站:cplusplus(英文)cppreference (中文)

定义函数

自定义函数即自己定义的函数,是一组一起执行一个任务的语句块。而每个 C 程序都必须要有一根函数,即主函数 main ()

一个函数内不可定义另一个函数,但函数内部可以相互调用,也可以自己调用自己 (函数递归)

函数的自定义格式如下

1
2
3
4
ret_type fun_name (para1, *)
{
    statement;
}
  • ret_type 函数的返回值类型

  • fun_name 函数名

  • para1, * 函数形参

  • statement 函数体

例:一个加法函数

1
2
3
4
int Add (int x, int y)
{
    return x+y;
}
  • 函数的返回类型 一个函数可以返回一个值,ret_type 是函数返回值的数据类型。有些函数可能并不需要返回值,在这种情况下,可以写关键词 void

  • 函数名 函数的名称,你可以给函数起一个让人一看就可以看懂这个函数的作用的名字

  • 函数形参 函数的形式参数,当函数被调用时,你可以将一个实参传入该函数的形参,然后在该函数内使用该参数。函数的形参是可选的,也就是说,创建一个函数可以没有形参

  • 函数体 函数主体内的一系列要执行任务的语句

若一个函数不写返回类型的话,默认返回int
函数的参数不宜过多
函数的设计应该追求高内聚低耦合
设计函数时,应做到谁申请的资源由谁释放

调用函数

调用函数使用函数调用操作符()

1
fun(para1, );
  • fun 函数名

  • () 函数调用操作符

  • para1 传给函数的实参

函数参数

实参与形参

  • 实际参数 又称实参 真实传给函数的参数。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参

  • 形式参数 又称形参 形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化 (分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

传值调用

  • 实参 即为实际的参数,有值。通常用于将值传递给形参

  • 形参 形式参数,在 C 中无值,用于接收实参传递过来的值

  • 例如,在这个例子中,实参 a 将值传递给形 x,实参 b 将值传递给 y。函数将 x 和 y 的值相加后后返回

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int Add (int x, int y)
{
    return x+y;
}

int main ()
{
    int a = 5;
    int b = 8;
    printf("%d", Add(a, b));

    return 0;
}

在这个案例中,函数的形参和实参分别占有不同的内存,对形参的修改不会影响实参,也就是说,形参只是形参的一份临时拷贝。实参中的值并未发生改变

传址调用

那如果我想要改变形参中的值呢?这个时候就要将形参的内存地址传递给函数形参 (详见 “指针”),这种方式可以使函数和函数外边的变量建立起真正的练习,也就是说可以通过指针操作函数外部的变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int Add (int* x, int* y)
{
    *x = 50;
    *y = 80;
    return *x+*y;
}

int main ()
{
    int a = 5;
    int b = 8;
    printf("%d\n", Add(&a, &b)); //130
    printf("%d %d", a, b); //50 80

    return 0;
}

如上案例,将实参 a 和 b 的地址分别传入形参 x 和 y 中,并修改 x 和 y 的值,由于 x 和 y 所存储的是变量 a 和 b 的地址,所以在函数内部对 x 和 y 做更改实际上会更改 a 和 b 的值

数组传参

数组传参,实际传递的并不是数组本身,而是将数组首元素的地址传了过去

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void fun (int arr[], int sz)
{
    for (int i = 0; i <= sz; i++)
    {
        printf("%d ", arr[i]);
    }
}

int main ()
{
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int sz = sizeof(arr) / 4 - 1;
    fun(arr, sz);

    return 0;
}

务必在函数体外计算数组元素个数,在函数内部是无法得知数组元素个数的

数组传参可以使用 int*int arr[] 来接收。本质都是指针

详细解释见 “数组” 一章

函数声明

由于编译器是一行一行顺序编译代码,如果你的函数体在函数调用之后,那么编译器是找不到要执行的代码的,因此会报错

我们可以在调用这个函数前先对函数进行声明

函数需要满足先声明后使用

1
int Add (int, int);

声明需要函数名,函数参数,返回类型。但是该函数具体存不存在无所谓

通常会将函数的声明放在头文件中,然后再在.c 文件中引用

模块化函数

可以将函数分开来写,然后在通过头文件引入

通常需要一个.h 文件和一个.c 文件。将函数声明放在.h 文件中,函数体放在.c 文件中,然后在要使用该函数的.c 文件中引用.h 头文件

例如,main.c 要使用由 add.h 声明的 add.c 文件中的 add () 函数,那可以这样写

1
int Add (int x, int y);
1
2
3
4
int Add (int x, int y)
{
    return x+y;
}
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include "add.h"

int main ()
{
    printf("%d", Add(5, 8));

    return 0;
}

要将这些文件放在可以调用到的路径下


递归

一个过程或函数在其定义或说明中直接或间接调用自己的方式就叫做递归。可以简单理解为自己调用自己 (我搞懂了,但我依然不会写)

递归只需要少量的代码即可完成之前所需要的多次重复计算

在函数调用自己的时候,后面的代码不会执行。只有等函数执行完之后才会继续执行下面的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void Print(unsigned int n)
{
    if (n / 10)
    {
        Print(n / 10);
    }
    printf("%d ", n % 10);
}

int main()
{
    int a = 12345;
    Print(a);

    return 0;

}

如上代码是一个将一个整数按每一位的顺序输出的代码,其输出结果为 1 2 3 4 5

以下是原理讲解

将 a 传入 Print 函数的形参 n,n 的值现在为 12345

  1. 调用函数,第一次判断 n/10>0,条件为真,进入 将 n/10,即 1234 传给 Print 函数的形参 n

  2. 调用函数,第二次判断 n/10>0,条件为真,进入 将 n/10,即 123 传给 Print 函数的形参 n

  3. 调用函数,第三次判断 n/10>0,条件为真,进入 将 n/10,即 12 传给 Print 函数的形参 n

  4. 调用函数,第四次判断 n/10>0,条件为真,进入 将 n/10,即 1 传给 Print 函数的形参 n

  5. 调用函数,第五次判断 n/10>0,条件为假,继续执行下面的代码

此时 n%10 为 1,printf 语句即输出 1

到此,Print 函数一共被调用了 5 次,但是目前只有第五次完完全全执行完了函数体,所以,现在开始返回执行

  1. 返回,执行第 4 次调用函数后的 printf 语句,输出 2

  2. 返回,执行第 3 次调用函数后的 printf 语句,输出 3

  3. 返回,执行第 2 次调用函数后的 printf 语句,输出 4

  4. 返回,执行第 1 次调用函数后的 printf 语句,输出 5

  5. 此时,函数运行结束

因此递归有两条重要条件

  1. 要存在限制条件

  2. 每次递归必须越来越接近这个限制条件

这俩是必要条件,如果没有,一定会导致死循环

在 C 中,每一个函数调用都会占用栈区的空间,每调用一次就会多占用一次。调用太多次函数就会导致栈溢出的问题。因此递归调用层次不能太深


数组

数组就是一种相同类型元素的集合

一维数组

声明数组

1
type_t arr_name[const_n];
  • type_t 元素的数据类型

  • arr_name 数组的名称

  • const_n(可为空) 数组的大小,为常量表达式。若为空编译器会根据后面存储内容的个数来确定数组的大小

1
int arr[10];

数组的初始化

1
2
int arr[10] = {1, 2}; //不完全初始化
int arr[2] = {1, 2,}; //完全初始化

数组的元素通常使用 {存储数字型}“存储字符串”{‘存储字符’} 等符号来存储元素

1
2
3
int arr[10] = {1, 2, 3, 4, 5}; //存储数字型 
char ch[] = {'a', 'b', 'c'}; //存储字符
char str[] = "hello world!"; //存储字符串,结尾有隐藏的'\0'

数组的使用与更改

数组使用 [] 下标引用操作符来获取数组中的元素

请注意!数组中的元素从 0 开始排列,也就是说,数组的第一个元素下标为 0,依次类推

可以使用数组名 [下标] 的方式为数组中的元素重新赋值

1
2
3
4
int arr[10] = {1, 2, ,3, 4, 5, 6, 7, 8, 9};
printf("%d", arr[0]); //1
arr[0] = 0;
printf("%d", arr[0]); //0

数组与内存

数组名是数组首元素的地址,每个元素都是挨着存储的,随着数组下标的增长,地址是由低到高变化的

二维数组

声明二维数组

1
type_t arr_name [const_n1] [const_n2];
  • type_t 元素的数据类型

  • arr_name 数组的名称

  • const_n1 数组的行数 (可以省略)

  • const_n2 数组的列数 (不可省略)

1
int arr[2][3]; //数组为2行3列,总共可容纳6个元素

二维数组的行号可以省略不写,但是列必须写!

二维数组的初始化

1
2
int arr[][3] = {1}; //不完全初始化
int arr[2][3] = {{1, 2},{3, 4},{5, 6}}; //完全初始化

二维数组的使用与更改

二维数组同一位数组一样,都是使用 [] 下标引用操作符来操作数组中的元素,二维数组中的元素行和列都是从 0 开始排列的

1
2
3
int arr[2][3] = {{1, 2},{3, 4},{5, 6}};
printf("%d", arr[0][0]); //1
printf("%d", (arr[1])[1]); //4

二维数组的每行都可以看作一维数组数组名即为数组名[行号]

二维数组在内存中的存储

每个元素的位置是连续的,在一行内部连续,换行也是连续的。二维数组的首元素是第一行

二维数组与一维数组

二维数组每一行可以单独拆分称为一个一维数组

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

在这里,p 是第一行数组的地址,p+1 就是第二行数组的地址

数组作为函数参数

数组传参本质上传过去的是数组首元素的地址,是一个指针。即数组名就是数组首元素的地址

详细解释见 “指针” 一章

1
2
type_t fun_name (type_t arr_name[]);
type_t fun_name (type_t* arr_name);
  • type_t 数据类型

  • fun_name 函数名

  • arr_name[] 数组形参,接收数组首元素地址

  • arr_name 数组名,接收数组首元素地址 (即数组名)

但是有两个例外

  1. sizeof (数组名) 这里的数组名代表的是整个数组,计算整个数组的大小,单位是字节
  2. &数组名 数组名表示整个数组,取出的是整个数组的地址,而数组首元素的地址相当于数组的起始地址

假设都要 + 1,&arr 会跳过这个数组,而 arr 只会跳过这个数组中的第一个元素

传参传过去的数组在函数体内部是无法计算其元素个数的


指针

每一个变量都有其内存位置,每个内存位置都定义了一个可以使用 & 运算符访问的地址。它表示了其变量在内存中的地址 (为第一个字节的地址)

指针其实也就是内存地址,指针变量就是用来存放内存地址的变量

32位计算机表示32根地址线,即32个bit位

64位计算机表示64根地址线,即64个bit位

指针变量

1
2
int a = 10;
int* pa = &a; //取出a的地址,放入指针变量pa中
  • * 表示是指针变量

  • int 说明指向的变量类型是 int

在这里,pa 是指针变量。*pa 等于对指针变量 pa 进行解引用,这样就可以获取到值 (也就是那个 10)

解引用操作

通过指针变量的地址改变原本存在的变量

1
2
3
4
5
int a = 10;
int* pa = &a;
*pa = 20;

printf("%d", a); //20

在这里,指针变量 pa 存入 int 型变量 a 的地址,也就是说 pa 指向的是 a 的地址

那么 *pa 就是对 pa 进行解引用操作,可以获取到 a 这块内存地址中的值,故可以改变 a 变量的值

指针变量的大小

在 32 位计算机上是 4 个字节
在 64 位计算机上是 8 个字节

指针变量类型的两个意义:

  1. 指针解引用的权限有多大
  2. 指针的步长 (如 int 为 4,char 为 1,double 为 8)

void* 是一种无具体类型的指针,所以任何类型的地址都可以丢进去 注: void*类型的指针无法解引用!

这里的权限与步长指的是该指针变量可以操作内存的bit位数

野指针

野指针就是指针指向的位置是不可知的指针

指针未初始化

1
2
3
4
5
6
7
int main()
{
    int* p; //指针变量p是一个未初始化的局部变量,不初始化的话默认是随机值
    *p = 20; //非法访问内存

    return 0;
}

越界访问

1
2
3
4
5
6
7
8
int arr[10] = {0};
int* p = arr;

for(int i = 0; i <= 10; i++)
{
    *p = i;
    p++;
}

在上面这个例子中,i 会等于 10。然后在循环中 i 会从 0 循环到 10。但是数组的元素个数是 10 个,下标却最高到 9。解析到 10 的时候就会导致指针越界访问

指针指向的空间被释放

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int* test()
{
    int a = 10;
    return &a;
}

int main()
{
    int* p = test();
    *p = 20;

    return 0;
}

在上面的这个例子中,函数执行结束后该函数所占用的内存会被释放 (归还给操作系统)。因此 *p 实际上指向的内存地址其实是越界访问。然后解引用将 20 赋值给指针变量 p,自然会出问题

如何避免野指针?

  1. 指针初始化:当不知道指针应该初始化为什么的时候,就初始化为 NULL 吧!准没错

  2. 小心指针访问越界

  3. 指针指向空间释放后及时置空 (指向 NULL)

  4. 指针使用之前检查其有效性 (不要访问空指针,空指针无法访问)

当知道指针指向哪里的时候,指向一个地址
当不知道指针指向哪里的时候,置为空指针
当指针空间被释放的时候,也可以置为空指针
每次使用指针变量的时候,可以先 if 判断一下指针变量是否为空

指针运算

  1. 指针++指针-- (移动指针)

  2. 指针 - 指针 得到两个指针之间的元素个数

  3. 指针的关系运算 (如比较指针所在的位置)

1
2
3
*p++;
*p[9] - *p[0]
*p < &arr[0]

指针和指针相减的前提是两个指针指向同一块空间

标准规定:允许指向数组元素与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较

指针+指针是没有意义的。类比如日期加日期

指针与数组

数组名是数组首元素的地址

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

//则有:arr[2] == *(arr+2) == *(p+2) == *(2+p) == *(2+arr) == 2[arr] == *(2+arr)

上文中注释的原理:因为代码为 arr[2] 时,编译器会处理为 *(arr+2) 也就等价于 *(2+arr) 等价于 2[arr]

二级指针

1
2
3
4
5
6
7
8
9
int main ()
{
    int a = 10;
    int* pa = &a; //pa是一级指针变量

    int** ppa = &pa; //pa也是给变量,取出pa的内存地址放入二级指针变量ppa中

    return 0;
}

*ppa == pa; *pa == a;
**ppa == a;

二级指针在实际开发中不常用,当然也可以有三级指针,四级指针

指针数组

接下来要开始乱了,做好心理准备

  • 存放整型的数组就是整型数组

  • 存放字符的数组就是字符数组

  • 存放指针的数组就是指针数组

1
2
3
4
5
int a[] = {1, 2, 3, 4, 5};
int b[] = {2, 3, 4, 5, 6};
int c[] = {3, 4, 5, 6, 7};

int* arr[3] = {a, b ,c}; //指针数组

字符指针

1
2
3
4
char* s = 'a';
char * 本质上是将字符串的首字符地址存储起来了

char * 是常量字符串,通常会写成 const chart* s = 'a';

指针是可以指向一个字符串的,指向的字符串的首元素所在的地址

数组指针

数组指针就是一种指向数组的指针

1
int arr[] = {0};
  • arr 是数组首元素的地址

  • &arr 才是数组的地址

1
int (*parr)[10] = &arr;

* 要和 parr 结合,表示是数组指针。类型是 int

[] 内的元素个数一定要写,并且只能写原数组的元素个数

1
2
3
4
int arr[5]; //整型数组
int* parr1[10]; //整型指针的数组
int (*parr2)[10]; //数组指针,指向一个数组,数组10个元素每个元素类型是int
int(*parr3[10])[5]; //存放数组指针的数组,存放10个数组指针,每个数组指针指向一个数组,数组5个元素。类型是int

函数指针

指向函数的指针,存放函数地址的指针

&函数名 可以得到函数的地址

函数名也就是函数的地址

因此 函数名 == & 函数名

函数指针变量

1
2
3
4
5
6
7
8
int Add(int x, int y)
{
    return x + y;
}

int(*pf)(int, int) = Add;

(*pf)(3, 5); //使用函数指针传参

在这里 (*pf) == pf == Add == &Add 因为函数名就是函数的地址

来两道练习题吧 (

请解释下面代码是什么意思

1
(*(void(*) ())0) ();

请解释下面代码是什么意思

1
void (* singnal (int, void(*) (int)) ) (int);

函数指针数组

指存放函数指针的数组

1
int (*pfArr[5]) (int, int) = {NULL, Add, Sub, Mul, Div};

指向函数指针数组的指针

取出函数指针数组的地址

1
2
3
int (*p)(int, int); //函数指针
int (*p2[4])(int, int); //函数指针的数组
int (*(*p3)[4])(int, int) = &p2; //取出的是函数指针数组的地址

在这里,p3 就是一个指向函数指针数组的指针

回调函数

通过函数指针调用的函数,如果你把函数的指针作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就是回调函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int Add(int x, int y)
{
    return x + y;
}

int Calc(int (*pf)(int, int))
{
    int x = 0;
    int y = 0
    scanf("%d %d", &x, &y);
    return pf(x, y);
}

int ret = Calc(Add);
printf("ret = %d\n", ret);

结构体

结构体是一些值的集合,但是值的类型可以不同

1
2
3
4
5
struct Person
{
    char name[]; //我也忘记是不是这样写的了 C++谁用char[]类型
    int age;
}p1;
  • Person 结构体标签,也就是这个结构体的名称

  • {}; 大括号内为结构体成员,请务必注意要在右括号后面加;

  • 结构体成员 可以是变量,数组,指针,甚至是其他结构体

  • p1 结构体全局变量

结构体的初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Book
{
    char name[20];
    int price;
    char id[12];
}b4,b5,b6;

struct Book b1;
struct Book b2;
struct Book b3;

在这里 b1,b2,b3 为局部变量,而 b4,b5,b6 为全局变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct S
{
    char c;
    int i;
};

int main()
{
    struct S s3 = {'A', 20};
    return 0;
}

struct 结构体标签 对象 = {值};

匿名结构体类型

1
2
3
4
5
6
7
struct
{
    char c;
    int i;
    char ch;
    double d;
};

匿名结构体类型,只能使用一次,有局限性

结构体的自引用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct A
{
    int i;
    char c;
};

struct B
{
    char c;
    struct A sa;
    double d;
};

一个结构体内可以包含另一个结构体的成员,但是不可以包含结构体自己的成员,会导致死递归

但是可以存自己的结构体指针

1
2
3
4
5
struct Node
{
    int data;
    struct Node* next;
};

表示这个节点可以找到同类型的下一个节点就叫做结构体自引用 对的 就是链表

结构体成员的访问

. 用于变量
-> 用于地址

1
2
3
4
5
tag.name
tag.tage2.id

struct tag* ps = &a;
ps->name;

结构体传参

1
2
function_name(struct tag s); //传结构体
function_name(struct tag* pa); //传地址

传址调用更好,效率高。能够改变变量的数值。同时,函数传参时,参数是需要压栈的,结构体如果过大会导致系统开销比较大,这将会导致性能的下降

结构体内存对齐

首先,掌握结构体的对齐规则

  1. 第一个成员在与结构体变量偏移量为 0 的地址处

  2. 其他成员变量要对齐到某个数字 (对齐数)的整数倍的地址处

  3. 结构体总大小为最大对齐数 (每个成员变量都有一个对齐数)的整数倍

  4. 如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数 (含嵌套结构体的对齐数)的整数倍

  5. 对齐数 编译器默认的一个对齐数与该成员大小的较小值 (VS 中的默认对齐数为 8)

为什么会存在内存对齐?

  1. 平台原因 不是所有的硬件平台都能访问任意地址上的任意数据的 某些硬件平台只能
在某些地址处取某些特定类型的数据,否则抛出硬件异常
  2. 性能原因 数据结构(尤其是栈)应该尽可能地在自然边界上对齐 为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

总体来说,结构体内存对齐是拿空间换时间的做法

可以使用 #pragma pack(8) 来修改默认对齐数 比如这里设定为8

* 柔性数组

在 C99 中,结构体的最后一个元素允许是未知大小的数组,这就是柔性数组成员

柔性数组的声明

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct S
{
    int n;
    int arr[0]; //大小是未知
};

struct S
{
    int n;
    int arr[];
};

两种写法都可以,具体取决于编译器

  • 结构体中的柔性数组成员前面必须只有有一个其他成员

  • sizeof 返回的这种结构的大小不包括柔性数组的内存大小

  • 包含柔性数组成员的结构使用 malloc () 函数进行内存的动态分配,并且分配的内存应当但与结构体的大小,以适应柔性数组的预期大小

柔性数组的使用方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct S
{
    int n;
    int arr[0];
};

int main()
{
    //期望arr的大小是10个int
    struct S *ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));
}

柔性数组的优点

方便内存释放 如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用 free 可以释放结构体,但是用户并不知道这个结构体内的成员也需要 free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次 free 就可以把所有的内存也给释放掉。 这样有利于访问速度 连续的内存有益于提高访问速度,也有益于减少内存碎片。~~ (其实,我个人觉得也没多高,反正你跑不了要用做偏移量的加法来寻址~~


位段

位段的声明

位段的声明和结构体是类似的,只是有两个不同

  1. 位段的成员必须是 int. unsigned int 或 signed int

  2. 位段的成员名后面有一个冒号和一个数字

  3. 位段的成员也可以 char 类型,因为 char 属于整型家族

1
2
3
4
5
6
7
struct A
{
    int _a :2; //_a成员占用2个bit位
    int _b :5; //_b成员占用5个bit位
    int _c :10; //_c成员占用10个bit位
    int _d :30; //_d成员占用30个bit位
};
  • 位段的空间是按照需要以 4 个字节 (int) 或者是 1 个字节 (char) 的方式来开辟的

  • 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应当避免使用位段

位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的

  2. 位段中的最大位的数目不能确定 (16 位机器最大 16,32 位机器最大 32。例如写成 27,在 16 位机器上就会出现问题)

  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义

  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位还是利用,这也是不确定的

总结:跟结构体相比,位段可以达到同样的效果,并且可以更好的节省空间。不过会有跨平台的问题存在


枚举

枚举的意思就是一一列举,把可能的类型一一列举

枚举的声明

1
2
3
4
5
6
enum Color
{
    RED,
    GREEN,
    BLUE
};
  • enum 声明枚举的关键字

  • Color 枚举名

  • {}; 枚举的类型,值为常量

枚举类型的值默认从 0 开始,每往后自增 1

为什么使用枚举?

虽然我们可以使用 #define 定义常量,为什么非要使用枚举?枚举的优点:

  1. 增加代码的可读性和可维护性

  2. 和 #define 定义的标识符比较,枚举有类型检查,更加严谨

  3. 防止了命名污染 (封装)

  4. 便于调试

  5. 使用方便,一次性即可定义多个常量


联合(共用)体

联合体也叫共用体,其中的成员共用一块内存空间。这个联合体的大小,至少是最大成员的大小

联合体的声明

1
2
3
4
5
6
union Un
{
    char c;
    int i;
    float f;
};

联合体的初始化

1
union Un u = {10};

联合体在同一时间只能使用一个

联合体的大小

  • 联合体也是存在内存对齐的

  • 联合体的大小至少是最大成员的成员的大小

  • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍


* 数据存储

该内容为计算机组成原理,了解即可

栈区使用习惯

先使用高地址,再使用低地址

char 并没有规定有无符号,因此其判断取决于编译器

整型数据的存储

  • 正整数 原码,反码,补码相同
  • 负整数 原码,反码,补码需要进行计算
  1. 原码 根据数字的值直接写出的值就是原码 (比如 1 的原码 00000000 00000000 00000000 00000001)

  2. 反码 原码的符号位不变,其他位按位取反 (比如 1 的反码 01111111 11111111 11111111 11111110)

  3. 补码 在反码的基础上 + 1 即为补码 (比如 1 的补码 01111111 11111111 11111111 11111111)

  4. 整数在内存中存储的都是补码

大小端字节序

大端字节序 把数据的低位字节序内容存储在高地址处,高位字节序的内容存储在低地址处 小端字节序 把数据的低位字节序内容存储在低地址处,高位字节序的内容存储在高地址处

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int main()
{
    //通过代码来判断当前机器的字节序
    int a = 1;
    char* p = (char*)&a;
    if(*p == 1)
    {
        printf("小端\n");
    }
    else
    {
        printf("大端\n");
    }

    return 0;
}

浮点型数据的存储

浮点数的存储由 IEEE 754 规定

(-1) ^S * M *2^E

  • (-1)^S 表示符号位 当 S=0,V 为正数,当 S=1,V 为负数

  • M 表示有效数字 大于等于 1,小于 2

  • 2^E 表示指数位


字符串相关库函数

C++ string 容器启动!

首先需要包含头文件 <string.h>

strlen () 字符串计数函数

1
size_t strlen(const char* str);

字符串使用\0作为结束标志,而 strlen 函数返回的就是在字符串中遇到’\0’之前所出现的字符的个数(不包括 \0)

也因此,传入 strlen() 的函数参数必须以\0结束

同时,strlen() 的返回值为 size_t,是无符号的

由于 strlen 返回的是无符号整数,因此该函数的返回值相间会进行整型提升

strcpy () 字符串复制函数

1
char* strcpy(char* destination, const char* source);
  • destination 要复制到的字符串(目标字符串)

  • source 被复制的字符串(原字符串)

    1. 源字符串必须以’\0‘结束
    2. 会将源字符串中的’’\0’拷贝到目标空间
    3. 目标空间必须足够大,以确保能存放源字符串
    4. 目标空间必须可变

strcat () 字符串追加函数

1
char* strcpy(char* destination, const char* source);
  • destination 要复制到的字符串

  • source 被追加的字符串

    1. 源字符串必须以’\0’结束
    2. 目标空间必须有足够大,能容纳下源字符串的内容
    3. 目标空间必须可修改
    4. 字符串不能自己给自己追加
    5. 会把目标空间的’\0’覆盖掉
    6. 返回的是目标空间首字符的地址

strcmp () 字符串比较函数

1
int strcmp(const char* str1, const char* str2);

通过ACSII码表的值进行比较

  • 返回值 <0 str1 比 str2 小

  • 返回值为 0 str1 和 str2 相等

  • 返回值 >0 str1 比 str2 大

strncpy () 字符串拷贝函数 (长度受限)

1
char* strncpy(char* destination, const char* sourse, size_t num);
  • destination 要拷贝到的的字符串

  • sourse 被拷贝的字符串

  • num 要拷贝 sourse 字符串的多少个字符

若拷贝的字符数量大于要拷贝的字符串的长度,会全部替换为\0,且原字符串后面的字符不变

strncat () 字符串追加函数 (长度受限)

1
char* strncat(char* destination, const char* sourse, size_t num);
  • destination 要追加到的的字符串

  • sourse 被追加的字符串

  • num 追加 sourse 字符串的多少个字符

strncmp () 字符串比较函数 (长度受限)

1
int strncmp(char* str1, counst char* str2, size_t num);
  • str1 str2 要被比较的两个字符串

  • num 比较多少个字符

会比较到出现另一个不一样的字符或者字符串结束或者num个字符全部比较完

strstr () 字符串查找函数

1
char* strstr(const char* str1,const char* str2);

在 str1 中查找 str2 是否存在 找到了返回第一个字符在被查找的字符串中的位置,找不到返回 NULL

strtok () 字符串切割函数

1
char* strtok(char* str, const char* sep);
  • str 要被切割的字符串

  • sep 定义了用作分隔符的字符的集合

  • 第一个参数指定一个字符串,它包含了 0 个或者多个由 sep 字符串中一个或者多个分隔符分割的标记
  • strtok 函数找到 str 中的下一个标记,并将其用’\0’结尾,返回一个指向这个标记的指针 (注:strtok 函数会改
  • 变被操作的字符串,所以在使用 strtok 函数切分的字符串一般都是临时拷贝的内容并且可修改)
  • strtok 函数的第一个参数不为 NULL,函数将找到 str 中第一个标记,strtok 函数将保存它在字符串中的位置
  • strtok 函数的第一个参数为 NULL,函数将在同一个字符串中被保存的位置开始,查找下一个标记;如果字符串中不存在更多的标记,则返回 NULL 指针。

strerror 错误码解析函数

1
char* strerror(int errnum);
  • errnum返回的错误码
错误码 strerror 函数处理后内容
0 No error
1 Operation not permitted
2 No such file or directory
3 No such process
4 Interrupted function call
5 Input/output error

字符相关库函数

首先需要包含头文件 “ctype.h”

C++ 就变成 cctype 抽象死了

函数 如果其参数符合下列条件就返回真
iscntrl 任何控制字符
isspace 空白字符:空格’ ’ 换页’\f’ 换行’\n’ 回车’\r’ 制表符’\t’ 垂直制表符’\v’
isdigit 十进制数字 0-9
isxdigit 十六进制数字,包括所有十进制数字,小写字母 a-f,大写字母 A-F
islower 小写字母 a-z
isupper 大写字母 A-Z
isalpha 字母 a-z 或 A-Z
isalnum 字母或者数字,a-z,A-Z,0-9
ispunct 标点符号,任何不属于数字或者字母的图形字符 (可打印)
isgraph 任何图形字符
isprint 任何可打印字符,包括图形字符和空白字符
tolower 将字符转换为小写
toupper 将字符转换为大写

内存相关库函数

memcpy() 内存拷贝函数

该函数应当拷贝不重叠的内存,如果内存重叠就需要使用 memmove() (在 VS 编译器中虽然可以拷贝过去,但是并非标准所规定,建议优先使用 memmove())

1
void* memcpy (void* destination, const void* source, size_t num);
  • destination 目标内存

  • source 被拷贝的内存

  • num 拷贝多少 单位:字节

memmove() 内存拷贝函数

1
void* memmove (void* destination, const void* source, size_t num);
  • destination 目标内存

  • source 被拷贝的内存

  • num 拷贝多少 单位:字节

memcmp() 内存比较函数

1
void* memcmp (void* ptr1, const void* ptr2, size_t num);
  • 返回 0 ptr1 和 ptr2 相同

  • 返回 > 0 ptr1 大于 ptr2

  • 返回 < 0 ptr1 小于 ptr2

memset() 内存设定函数

1
void* memset(void* ptr int value, size_t num);

把 ptr 所指向的那块空间的前 num 个字节的内容设定为 value 的值

malloc() 内存开辟函数

1
void* malloc(size_t size);
  • size 单位为字节,不要写 0,这是标准未定义行为

如果找到空间了,返回这块内存空间的指针

如果没有找到空间,返回 NULL

因此,使用由 malloc() 开辟的空间前建议先判断空间是否开辟成功

free() 返还内存空间函数

1
free(void* ptr);

将 ptr 所指向的空间返回给操作系统

请注意:该函数只能释放在堆区上的内存空间

如果 ptr 为 NULL,该函数将不会执行

在使用该函数后要将 ptr 置为 NULL

calloc() 内存开辟并初始化函数

1
void* calloc(size_t num, size_t size);
  • num 元素的个数

  • size 每个元素的长度

同时会将每个元素的值初始化为 0

realloc() 内存调整函数

该函数可以对动态开辟的内存大小进行调整

1
void* realloc (void*ptr, size_t size);
  • ptr 之前所开辟的内存空间的起始地址

  • size 所要调整的新的大小

返回值是全新开辟的新的内存地址

如果需要增加空间,但是原地址后面的空间不够,就会将该内存空间移动至新的位置,同时地址也会被改变。当然,原空间会被释放掉

有时候看你找不到合适的空间来调整大小,这时就会返回 NULL。因此不建议用调整之前的指针变量来接受 realloc 返回的地址。建议使用临时的指针变量然后进行判断后赋值


文件操作

在 C 中,操作系统中的文件有一个文件缓冲系统。在其中最重要的即为文件类型指针,简称文件指针

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息 (如文件的名字,文件状态及文件当前的位置等) 这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名为 FILE

每当打开一个文件的时候,系统会根据文件的情况自动创建一个 FILE 结构的变量,并填充其中的信息 通常,都是通过一个 FILE 的指针来维护这个 FILE 结构的变量,这样使用起来更加方便

1
FILE* pf; //文件指针变量

定义 pf 是一个指向 FILE 类型数据的指针变量。可以使 pf 指向某个文件的文件信息区 (是一个结构体变量) 通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件

fopen() 文件打开函数

1
FILE* fopen(const char* filename, const char* mode);
  • filename 文件名

  • mode 打开模式,见下表

文件打开模式 含义 如果指定的文件不存在
“r” 只读 为了输入数据,打开一个已经存在的文本文件 ERROR
“w” 只写 为了输出数据,打开一个文本文件 建立一个新的文件
“a” 追加 向文本尾部追加数据 建立一个新的文件
“rb” 只读 为了输入数据,打开一个二进制文件 ERROR
“wb” 只写 为了输出数据,打开一个二进制文件 建立一个新的文件
“ab” 追加 向一个二进制文件尾部追加数据 建立一个新的文件
“r+” 读写 为了读和写,打开一个文本文件 ERROR
“w+” 读写 为了读和写,建立一个新的文件 建立一个新的文件
“a+” 读写 打开一个文件,在文件尾部进行读写 建立一个新的文件
“rb+” 读写 为了读和写,打开一个二进制文件 ERROR
“wb+” 读写 为了读和写,建立一个新的二进制文件 建立一个新的文件
“ab+” 读写 打开一个二进制文件,在文件尾部进行读写 建立一个新的文件

在使用 w 模式打开一个文件时,会将源文件中已有的数据清空!!!

fclose() 文件关闭函数

1
int fclose(FILE* stream);
  • stream 文件指针

  • 在写完文件后需要使用 fclose() 关闭文件

fputc() 字符输出函数

1
void fputc(char ch, FILE* stream);
  • ch 要写入的数据

  • stream 文件指针

fgetc() 字符输入函数

1
int fgetc(FILE* stream);

从标准输入流中读取信息

如果该函数读取正常,会返回这个字符的 ACSII 码值,如果读取错误或者文件结束会返回 EOF

该函数每读一次都会将指针 + 1

fputs() 文本行输出函数

1
void fputs(const char str, FILE* stream);
  • str 要写入的字符串

  • stream 文件指针

fgets() 文本行输入函数

1
char* fgets(char* string, int n, FILE*stream);
  • string 字符指针

  • n 读取的字符 (写 100 其实只会读取 99,因为最后一个要填充’\0’)

  • stream 文件指针

fgets 函数在读取结束的时候,会返回 NULL

正常读取的时候,返回存放字符串的空间起始地址

fprintf() 格式化输出函数

1
int fprintf(FILE* stream, const char* format [, argument]...);
  • stream 文件指针

  • format 格式化转义字符

  • argument (可选) 值

fscanf() 格式化输入函数

1
int fscanf(FILE* stream, const char* format [, argument]...);
  • stream 文件指针

  • format 格式化转义字符

  • argument (可选) 值

fwrite() 二进制输出函数

1
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
  • buffer 指针指向要被写的数据

  • size 元素的大小 (可以写多个 取决于需求)

  • count 最多写多少个元素

  • stream 文件指针

fread() 二进制输入函数

1
size_t fread(void* buffer, size_t size, size_t count, FILE* stream);
  • buffer 指针指向要被读的数据

  • size 元素的大小 (可以写多个 取决于需求)

  • count 最多读多少个元素

  • stream 文件指针

fread 函数在读取的时候,返回的是实际读取到的完整元素的个数

如果发现读取到的完整的元素的个数小于指定的元素个数,这就是最后一次读取了

sscanf() 字符串格式化读取函数

1
int sscanf(const char* buffer, const char* format [, argument]...);
  • buffer 要存储的变量

  • format 格式化转义字符

  • argument (可选) 值

从一个字符串中读取一个格式化的数据

sprintf() 格式化数据转换函数

1
int sprintf(const char* buffer, const char* format [, argument]...);
  • buffer 要存储的变量

  • format 格式化转义字符

  • argument (可选) 值

把一个格式化的数据转换为字符串

fseek() 文件指针定位函数

1
int fseek(FILE* stream, long offfset, int origin);
  • stream 文件指针

  • offset 偏移量 (负数是往前倒着走)

  • origin 位置 (SEEK_CUR 当前文件指针的位置;SEEK_END 文件结尾;SEEK_SET 文件起始位置)

ftell() 文件指针位置返回函数

1
long int ftell(FILE* stream);
  • stream 文件指针

返回文件指针相较于起始位置的偏移量

rewind() 文件指针返回函数

1
void rewind(FILE* stream);
  • stream 文件指针

让文件指针回到起始位置

文件缓冲区

ANSIC 标准采用 “缓冲文件系统 “处理的数据文件的,所谓缓冲文件系统是指系统自动地在内中为程序中每一个正在使用的文件开辟一块” 文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区 (充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区 (程序变量等)。缓冲区的大小根据 C 编译系统决定的


程序环境和预处理

一个 C 程序会通过编译器经过预编译、编译、汇编、链接后才会在运行环境中运行

预处理

在预处理阶段,编译器会进行如下步骤

  1. 完成头文件的包含 #include

  2. #define 定义的符号或宏替换

  3. 删除注释

编译

把 C 语言代码转化为汇编代码

  1. 语法分析

  2. 词法分析

  3. 语义分析

  4. 符号汇总

汇编

见汇编代码转换为机器指令 (即二进制命令)

会生成符号表

链接

把多个目标文件和链接库进行链接

  1. 合并段表

  2. 符号表的合并和重定位

运行环境

程序执行的过程:

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成

  2. 程序的执行便开始。接着便调用 main 函数

  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈 (stack),存储函数的局部变量和返回地址。程序同时也可以使用静态 (static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值

  4. 终止程序。正常终止 main 函数;也有可能是意外终止


宏和函数

宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个

1
#define MAX(a, b) ((a)>(b) ? (a) :(b))

那为什么不用函数来完成这个任务?

原因有二:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。

  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于 > 来比较的类型。宏是类型无关的。

当然和宏相比函数也有劣势的地方:

  • 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

  • 宏是没法调试的。

  • 宏由于类型无关,也就不够严谨。

  • 宏可能会带来运算符优先级的问题,导致程容易出现错。宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型但是函数做不到

  • 宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到

宏和函数的一个对比

属性 #define 函数
代码长度 每次使用时,宏代码都会被插入到程序中。出了非常小的宏之外,程序的长度会大幅度增长 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度 更快 存在函数的调用和返回的额外开销,所以相对会慢一点
操作符优先级 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则临近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号 函数参数只在函数调用的时候求值一次,它的结果传递给函数。表达式的求值结果更容易预测
带有副作用的参数 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果 函数参数只在传参的时候求值一次,结果更容易控制
参数类型 宏的参数于类型无关,只要对参数的操作是合法的,它就可以适用于任何参数类型 函数的参数是与类型有关的,如果参数的类型不通,就需要不同的函数,即使他们执行的任务是不同的 C++:函数模板了解一下不?
调试 宏是不方便调试的,因为在预处理阶段就会被替换掉 函数是可以逐语句调试的
递归 宏是不能递归的

#define 所定义的宏会在预编译的时候被替换掉

命名约定

把宏的名称全部大写,函数名的首字母大写

#undef 取消定义

1
#undef M

取消定义一个宏

条件编译

我早就忘了

文件包含

头文件的包含方式

  • #include <stdio.h> 库文件包含,C 语言库中提供的函数头文件使用 <>

  • #include "test.h" 本地文件包含,自定义函数的头文件使用 ""

本质区别是查找策略不同

  • "" 会在源文件目录下查找,如果未找到就去库文件中查找

  • <> 只会在库文件中查找

所以你也可以使用 "" 来包含 C 语言库中的头文件 (不推荐

嵌套文件包含

1
#pragma once

在第一行添加如上代码,可以避免一个头文件被多次包含

类型转换

隐式类型转换

C的整型算术运算总是至少以缺省整型类型的精度来进行的。

为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。

算术操作符转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。

优先级 从上到下

  1. long double
  2. double
  3. float
  4. unsigned long int
  5. long int
  6. unsigned int
  7. int

整型提升

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。

因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。

通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。


常用 C 算法

请移步至 C语言相关算法

不向焦虑与抑郁投降 这个世界终会有我们存在的地方
使用 Hugo 构建
主题 StackJimmy 设计
本博客已稳定运行
发表了9篇文章 · 总计52.05k字