1. 1. c
    1. 1.1. 环境配置
    2. 1.2. code runner的配置
    3. 1.3. hello world
    4. 1.4. 注释
    5. 1.5. 主函数
    6. 1.6. 语句
    7. 1.7. 头文件
    8. 1.8. 内建函数
    9. 1.9. 数据类型
      1. 1.9.1. 整型
      2. 1.9.2. 为什么有符号int范围是这个
      3. 1.9.3. 查看数据类型大小
      4. 1.9.4. 一个linux上的链接问题
      5. 1.9.5. 连续定义
      6. 1.9.6. 无符号整型
    10. 1.10. 变量取内存地址
    11. 1.11. 表达式和语句
    12. 1.12. 用户输入scanf
    13. 1.13. 浮点数
      1. 1.13.1. 浮点型后缀
    14. 1.14. 自加自减误区
    15. 1.15. 流程结构
      1. 1.15.1. while
      2. 1.15.2. 关系运算符
      3. 1.15.3. 逻辑运算符
      4. 1.15.4. for
      5. 1.15.5. do while
      6. 1.15.6. break continue
    16. 1.16. 条件结构
      1. 1.16.1. if
      2. 1.16.2. switch case
      3. 1.16.3. goto
    17. 1.17. 数组
      1. 1.17.1. 一维数组
      2. 1.17.2. 二维数组/多维数组
    18. 1.18. 指针
      1. 1.18.1. 指针的数组
      2. 1.18.2. 数组的指针
    19. 1.19. 堆区空间的使用
      1. 1.19.1. malloc
        1. 1.19.1.1. 一维数组指针
      2. 1.19.2. calloc
      3. 1.19.3. realloc
    20. 1.20. 函数
      1. 1.20.1. 函数调用:直接写函数名()
      2. 1.20.2. 函数声明
      3. 1.20.3. 返回多个值
      4. 1.20.4. 参数
      5. 1.20.5. 在函数内部修改函数外部的变量
      6. 1.20.6. 二级指针
      7. 1.20.7. 数组作为参数
      8. 1.20.8. 函数类型
      9. 1.20.9. 递归函数
      10. 1.20.10. 动态指定参数个数
    21. 1.21. 字符
      1. 1.21.1. 字符变量
      2. 1.21.2. 字符数组
        1. 1.21.2.1. 使用库函数
      3. 1.21.3. 字符串
        1. 1.21.3.1. 读取和输出
        2. 1.21.3.2. 长度
        3. 1.21.3.3. 比较是否相等
        4. 1.21.3.4. 拼接
        5. 1.21.3.5. 字符串转数字
        6. 1.21.3.6. 将不同类型数据转为字符串
      4. 1.21.4. 字符串数组
    22. 1.22. 结构体
      1. 1.22.1. 结构体变量
      2. 1.22.2. 初始化
      3. 1.22.3. 访问成员变量
        1. 1.22.3.1. 实例变量
        2. 1.22.3.2. 指针变量
        3. 1.22.3.3. 赋值
      4. 1.22.4. 指针成员
      5. 1.22.5. 函数成员
      6. 1.22.6. 结构体嵌套
      7. 1.22.7. 结构体数组
      8. 1.22.8. 内存对齐
      9. 1.22.9. 结构体大小
    23. 1.23. 联合
    24. 1.24. 枚举
    25. 1.25. 内存管理
      1. 1.25.1. 隐式类型转换
      2. 1.25.2. 显式类型转换
      3. 1.25.3. 小端存储
        1. 1.25.3.1. 验证小端存储:强制类型转换
        2. 1.25.3.2. 验证小端存储:联合
      4. 1.25.4. 大端存储
      5. 1.25.5. 类型重命名
        1. 1.25.5.1. 结构体重命名
        2. 1.25.5.2. 函数指针重命名
    26. 1.26.
      1. 1.26.1. 参数宏
      2. 1.26.2. 字符串指示符
    27. 1.27. 项目管理
    28. 1.28. 静态存储区
      1. 1.28.1. 全局变量
      2. 1.28.2. 静态全局变量
      3. 1.28.3. 静态局部变量
    29. 1.29. 寄存器变量
    30. 1.30. const
      1. 1.30.1. 修饰常量
      2. 1.30.2. 修饰指针
    31. 1.31. volatile
    32. 1.32. restrict
    33. 1.33. 内存分区
    34. 1.34. 命令行参数
    35. 1.35. 位运算

c

c

环境配置

  1. 使用vscode来编辑,由于是在linux下,不需要下载gcc(编译)默认已经安装了
  2. 下载c/c++插件,可以自动补全
  3. 下载code runner,可以实现一键运行,不需要手动编译

code runner的配置

  1. 在左下角的设置/或者快捷键ctrl+k ctrl+s进入快捷键设置界面,设置ctrl+enter运行代码
  2. 在setting中输入code runner terminal找到whether to run code in integated terminal选中

hello world

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <stdlib.h>

int main(void){
printf("hello world\n");
system("sleep 2"); //调用系统命令(linux)
return 0;
}

注释

单行注释使用//,多行注释使用/* */,vscode中只需要选中行,ctrl+/就可以注释了

主函数

一个项目只能有一个主函数,main函数是程序的入口,int定义函数返回值,return 0,0表示正常结束

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

// 第一种写法,void无参数
int main(void){
return 0;
}

// 第二种写法,有参数
int main(int argc, char* argv[]){
return 0;
}

语句

语句以分号分隔

头文件

使用#include来导入预装的c文件,路径为/usr/include
printf()函数在stdio.h文件中,system()函数在stdlib.h文件中

内建函数

像printf这种常用的函数,如果不导入stdio.h文件的时候,程序也不会报错,仍然可以运行。因为printf为内建函数,即为gcc内置的函数,是为了提高编译的效率,而不需要去库文件中去查找复制对应的函数代码。

数据类型

  • 基本数据类型

    • 数值类型
      • 整型(有符号/无符号)
        • short:短整型2字节 %hd
        • int:整型4字节 %d/%u
        • long:长整型,大于等于int长度,根据平台而不同 %ld
        • long long:超长整型8字节 %lld
      • 浮点型(有符号/无符号),都可使用%e使用科学计数法
        • float:整型4字节,%f
        • double:整型8字节,%lf
        • long double:大于等于double长度,%lf
    • 字符型
      • 字符数组, %c
      • 字符串, %s
  • 构造类型

    • 数组
    • 结构体
    • 联合
    • 枚举
  • 指针

整型

  1. 声明与定义区别:声明一个变量未赋值,定义一个变量同时赋值
  2. 变量与常量区别:常量不可改变,变量可变
  3. 有符号和无符号:有负数的为有符号(会将最高位作为符号位,0代表正数,1代表负数),没有负值的为无符号
  4. int默认为有符号
  5. int范围:-2^31~2^31-1,4个bitex8bit
  6. 无符号范围:0~2^32-1

为什么有符号int范围是这个

这篇文章讲到了反码以及补码的问题,在计算机中负数是以补码的形式存在的,目的是为了让计算机做加法而不是减法减轻计算难度,但是为什么负数会比正数多1呢,因为-0的存在与+0冲突了,所以人为规定了-0的补码表示最小的那个数,这个补码不会进行原码计算,例如在一个字节中10000000就是-0的补码,表示-2^7

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void){
int a = 1;
int b = 2;
int c = a + b;
printf("%d+%d=%d\n",a,b,c);
return 0;
}

查看数据类型大小

1
printf("%d\n", sizeof(int))

一个linux上的链接问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <math.h>

int main(){
int x = 2,y = 3;
double result = pow(x,y);
printf("%lf\n", result);
return 0;
}

//报错信息
/usr/bin/ld: /tmp/ccX8ix3h.o: in function `main':
start.c:(.text+0x21): undefined reference to `pow'
collect2: error: ld returned 1 exit status

google了之后找到了原因:因为历史原因,没有将math库包括到标准libc.so/libc.a中,而是放到了libm.so/libm.a中。但是gcc编译时没有默认导入libm.so/libm.a,所以会提示找不到pow函数,这都是ld链接的问题。参考链接:1,2

解决方法:编译时指定导入math库:gcc start.c -o start -lm

连续定义

1
2
3
4
5
6
7
8
9
int a, b, c, d;
//也可以这么定义,这么写的好处是可以在每个变量后加注释
int a,
b,
c,
d;
short e;
long f;
long long g;

无符号整型

有符号的整型默认不写,无符号的整型使用unsigned关键字定义,printf中使用%u

1
2
3
4
5
6
7
#include <stdio.h>

int main(void){
unsigned int a = 123;
printf("%u\n", a);
return 0;
}

变量取内存地址

使用符号&,格式化输出使用%p

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void){
long a = -123;
printf("%p\n", &a);
return 0;
}

//0x7ffdbeeec7205

表达式和语句

表达式和语句的区别在有分号,以分号结尾的为语句,否则为表达式,表达式有值,由运算符组合而成

用户输入scanf

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(void){
// 声明变量
int a;
printf("输入一个数字: ");
// 将数据写入指定内存地址
scanf("%d", &a);
printf("%d\n", a);
return 0;
}

输入多个数据时,数据之间的分隔默认为空格,如果格式化中使用其它字符,那么输入的分隔符也使用对应的

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void){
int a,b;
printf("输入两个数字: ");
scanf("%d,%d", &a, &b);
printf("%d,%d\n", a, b);
return 0;
}

由于编译器的不同,scanf可能会有警告,消除警告的方法:找到对应的警告码,添加到头文件

1
2
#include <stdio.h>
#pragma warning(disable:4996)

浮点数

  1. 浮点型的零写0.0
  2. 有效数位:从左边开始非0的位数
  3. 设置显示小数位数printf("%.numf\n", variable)

浮点型后缀

有小数点的数据类型默认为doube,在小数后添加f才变成float类型。long double需要添加l后缀。
不过我还是不明白,使用float这些关键字不是在声明变量时,取指定大小的内存吗,如果这些不能限定,为什么还需要这些关键字呢?而且用sizeof查看时,使用float声明的变量和double声明的长度不同啊,不懂?
过了一天,我好像明白了一点,小数默认为double类型,那么其精度就高一些,但是将其赋值给float后,精度自然会损失一些,可能会有精度损失的报错。如果在后面加f,那么就是指定了其为float,精度减少了,但是不会报错。

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void){
// 即使是表明float类型,其实也是以double来存储的
// float a = 12313.12312;
float a = 12313.12312f;
printf("%f\n", a);
return 0;
}

自加自减误区

  1. 在一个语句中,不能超过两个的自加自减运算符
  2. int a = i++,并不是先赋值后计算,而是在内存中分配出来一个地方给i存储值,然后自增,不过赋值时会将原来的值赋值给变量
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(void){
int a = 12;
int b = a++ + ++a + a++;
printf("%d\n", b);
return 0;
}

//例如超过两个后其值为 40

流程结构

  1. 顺序
  2. 循环
    • while
    • for
    • do while
  3. 分支和跳转
  4. goto

while

1
2
3
while(condition){
code block
}

如果不加{},那么只有接下来的一行会被当作循环体。用循环计算1到1亿的和,用c秒算出来,而用效率比shell高的awk需要4秒,c是真的快。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(void){
int a = 1;
while(a<=5){
printf("%d\n", a);
a++;
}
return 0;
}

非零为真,零为假

关系运算符

< <= > >= == !=

逻辑运算符

and的优先级高于or

  • and:&&
  • or:||
  • not:!

for

1
2
3
for(语句;条件;语句){
code block;
}
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void){
for(int i=1; i<5; i++){
printf("%d\n",i);
}
return 0;
}

do while

1
2
3
do{

}while()

break continue

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

int main(void){
for(int i=1;i<5;i++){
printf("%d\n",i);
if(i==1){
continue;
}
if(i==2){
break;
}
}
return 0;
}

条件结构

if

1
2
3
4
5
6
7
8
9
if(){

}
else if(){

}
else{

}

switch case

  1. 注意每个case后都需要加break等跳出switch,否则会执行后面的语句,不管是否符合条件
  2. switch只能用整型
  3. case为起始点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main(void){
int num;
while(1){
scanf("%d", &num);
switch(num){
case 1:
printf("%d\n", num);
break;
case 2:
printf("%d\n", num);
break;
default:
printf("num\n");
}
}
return 0;
}

goto

不建议使用,不过goto跳出多重循环时比break简单,只需要写一次

1
2
3
4
5
6
7
8
9
#include <stdio.h>

//死循环
int main(void){
one:
printf("one");
goto one;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main(void){

for(int i=1;i<10;i++){
for(int i=1;i<10;i++){
for(int i=1;i<10;i++){
goto outer;
}
}
}
outer:
printf("outer");
return 0;
}

数组

数组名即为一个指针,存储的为第一个元素的地址。

一维数组

类型相同的数据

在内存中申请指定个数的连续空间

  • 申请

    默认存储的为内存地址

    也可以不写元素个数,但是前提是必须有初始化数据

1
2
类型 变量名[元素个数]
int queue[4];
1
int queue[] = {1,2,3,4,5,6,7,8};
  • 初始化

    可以初始化全部数据,或者部分数据

1
2
3
4
5
6
7
// 初始化部分数据,其余数据默认为0
int queue[4] = {1,2,3};
int queue[4] = {1,[3]=4};
// 赋值
queue[3] = 4;
// 全部初始化为0
int queue[4] = {0};
  • 访问
1
printf("%d",queue[1]);
  • 地址

    +1表示加一个数据类型的内存大小,即下一个元素

1
2
3
4
5
6
7
#include <stdio.h>

int main(void){
int queue[4] = {1,2,3,4};
printf("%p %p %p",&queue, &queue[1], &queue[0]+1);
return 0;
}

二维数组/多维数组

即数组的嵌套

1
2
3
4
5
6
7
#include <stdio.h>

int main(void){
int queue[4][2] = {{1,2},{3,4},{5,6},{7,8}};
printf("%d", queue[3][1]);
return 0;
}

指针

是一种数据类型,用来存储变量地址

类型:也是一些基本的数据类型,比如char,short,float等

声明:类型+*+变量名,两种写法:int *p / int * p

赋值:

1
2
int num = 10;
int *p = &num;
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void){
int num = 10;
int *p = &num;
printf("%p %p", p, &num);
return 0;
}

我有个疑问:变量明明可以操作数据,为什么还要用指针操作呢

通过指针操作具体数据:

1
2
3
4
5
6
int a = 10;
int *p = &a;
printf("%d", *p); //获得和变量a一样的值
*p = 20;
printf("%d", a); //a变量的值也会被改变
// 其实*p 和变量a相同

数组指针:当指针为某一个元素的地址,那么指针+1也表示后一个元素,与数组中的操作类似,这时的1表示的是一个类型长度。

数组指针的下标运算:这个与数组有点不一样

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

int main(void){
int list[3] = {1,2,3};
printf("%p %p\n", &list, &list[0]);

int *p = &list[0];
for(int i=0;i<3;i++){
printf("%d\n", p[i]);
}
}

0x7ffe9e04bd2c 0x7ffe9e04bd2c
1
2
3

可以看到数组list的地址其实是第一个元素的地址。那么就好理解了,使用指针下标来循环所有元素的时候是使用的第一个元素的地址,而不是数组变量的地址。

1
2
printf("%d, %d\n", p[0], 0[p]);
printf("%p, %p\n", &p[0], &0[p]);

通过上面这个例子发现通过指针的下标取地址的另外一种写法,而这种写法居然是可以的。可以这么简单的理解:第一个是取p的第一个值,第二个是取第一个p的值

指针的数组

一个数组全部装的是指针,数组可以嵌套,那么指针数组也有嵌套

1
2
int a = 1 ,b = 2;
int *list[2] = {&a, &b};
1
2
3
int com[5] = {1,2,3,4,5};
printf("%p %p\n", com, &com);
0x7ffcbccdbb20 0x7ffcbccdbb20

通过上面这个例子可以看出数组的名字和取地址表示的是同一个,所以在写嵌套的指针数组时,可以直接写数组名字,而不用写取地址

1
2
3
4
int a[2] = {1,2};
int b[2] = {3,4};
int *c[2] = {a, b};
printf("%d", c[1][0]) //3

数组的指针

昨天写了一个用数组变量名地址来作为指针地址的,结果发现老是报错:

1
2
3
int num[3] = {2,3,4};
int *p = &num;
// 我想用变量名既然可以取所有元素,那么用变量名的地址来写也应该可以,但是一直报错

开始我还以为只能用元素地址,今天学了这个才发现原来有这个概念,只是我写法不对

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void){
int num[3] = {2,3,4};
int (*p)[3] = &num;
printf("%d\n", (*p)[0]);
}
// 这么写就和数组一样,也可以取元素了

而这个就叫做数组指针,在写的时候要注意加小括号,否则就不对了

二维数组指针

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void){
int num[2][2] = {{1,2}, {3,4}};
int (*p)[2][2] = &num;
printf("%d\n", (*p)[0][1]);
}
// 2

虽然指针学了这么多,但是我还是不知道它到底有什么用。而且不是都说指针挺难吗,可是我不知道这到底哪里难了,难道我没学到它真正难的知识点吗

指针在程序中的设置:如果程序设置了32位,那么只能运行在32位系统,如果设置了64位,那么可以运行在32和64位系统上。32位和64位程序数据类型大小不一致。所以本质上还是编译器编译所选择的位数决定了程序的位数。

没错,在学到下面这一节的时候,我就有点分不清了。数组指针和指针数组不好区分,因为在中文的名字上太容易混了,所以得重新理解,一种是将所有元素都声明为指针int *p[3],一种是将数组名声明为指针int (*p)[3],如此就好区分了。

另外对于*星号的理解是在声明时表明这是一个指针变量,在使用时作为解引用/间接引用

堆区空间的使用

即手动进行内存的申请和释放

内存分区:

  • 栈区:申请和释放都由操作系统决定。约为4Gb,系统需要检测何时释放会占用cpu资源
  • 堆区:程序决定何时申请和释放
  • 全局区
  • 字符常量区
  • 代码区

malloc

堆区空间的申请使用malloc()函数,会申请一段连续空间并返回空间的首地址,类似指针数组(连续空间也就是说每次操作都会操作一个申请的数据类型大小,申请的空间和数组差不多,是分段的)

malloc(size),size单位为byte字节

1
2
3
4
5
6
7
#include <stdio.h>
#include <stdlib.h>

int main(void){
int *p = (int*)malloc(4); //后面的int*表示类型为int。前面的int表示一次操作4字节
printf("%p", p);
}

可以直接写字节大小,也可以用sizeof来写

1
int *p = (int*)malloc(sizeof(int));

判断申请空间是否成功

1
2
3
if(p == NULL){
printf("申请失败")
}

可以使用循环来给申请的空间初始化,没有特殊的申请数组的方式,但是可以通过下标对这一连续空间进行初始化0

1
2
3
4
5
int *p = (int*)malloc(40);
for(int i=0;i<10;i++){
p[i] = 0;
}
// 这样就达到了数组的效果

另一种方式,按字节对申请空间进行赋值

1
memset(p, 0, 40);

使用free()函数释放空间

1
free(p);

内存泄漏:当使用malloc申请了一块内存空间后,改变了指针地址,原先申请的内存空间无法找到操作也无法释放,这种现象被称为内存泄漏。

当空间使用free释放后,空间数据被初始化,但是指针变量依旧没变,再下次使用此指针时才会改变

一维数组指针

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

int main(void){
// 将所有元素声明为指针
int a = 1, b = 2;
int * p[2] = {&a, &b};
printf("%d\n", *p[0]);

// 将变量名声明为指针,使用小括号是因为*的元算级别低于[]
int c[2] = {3,4};
int (*p1)[2] = &c;
printf("%d\n", (*p1)[1]);

// 一维数组的指针变量的手动声明
int (*p2)[2] = (int(*)[2])malloc(sizeof(int)*2);
for(int i=0;i<2;i++){
(*p2)[i] = 0;
}
printf("%d\n", (*p2)[0]);

return 0;
}

calloc

与malloc函数功能一样,但是在声明变量的时候,会将所有元素初始化为0

1
2
3
// 两个元素,每个大小为4字节
int *p3 = (int*)calloc(2,sizeof(int));
printf("%d\n", p3[1]);

realloc

重新定义元素空间大小,也会返回之前的首地址。如果申请的空间在内存碎片上,不够申请的空间,那么会在其它地方申请,那么返回的地址会与原来的地址不同。

1
int *p4 = (int*)realloc(p3, sizeof(int)*2);

函数

1
2
3
4
5
6
7
8
9
返回值类型 函数名(参数列表){

}
// 第一个void表示无返回值,第二个void表示无参数
void fun(void){

}
// 调用
fun();

函数调用:直接写函数名()

函数名:即为函数的地址,函数名为一个变量,其存储了函数代码段的首地址。不过函数比较特殊,函数名与函数名取地址都相同fun == (&fun)。所以可以直接写函数名来调用函数

如果没有参数,必须要写void

主函数与自定义函数区别:main主函数有系统自动调用,自定义函数需要手动调用

自定义函数要写在主函数外面,系统只会执行主函数里面的代码,所以必须在主函数中对自定义函数进行了调用,才会被执行

函数声明

本来函数需要写在main函数前面,而且各个函数先后顺序必须要符合逻辑调用的顺序。但是函数的声明解决了这个问题,只需要在主函数前面对函数进行了声明,然后函数就可以写在main函数后面了,而且不用考虑之间的先后顺序。函数声明只不过是没有写函数的内容

1
2
3
4
5
6
7
8
9
10
11
12
// 函数声明
void fun(void);

int main(void){
// 主函数中的函数调用
fun();
}

//函数定义
void fun(void){

}

无参数有返回值的函数,需要return来返回值

1
2
3
int fun(void){
return number;
}

在无返回值的函数中可以使用return来结束

1
2
3
void fun(void){
return;
}

返回多个值

return只能返回一个值,所以在需要返回多个值的时候需要用指针去返回一段空间

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

// 注意这里的返回值类型为int*
int* fun(void){
int* p = (int*)malloc(sizeof(int)*2);
p[0] = 1;
p[1] = 2;
return p;
}

void main(void){
int* a = fun();
printf("%d, %d\n", a[0], a[1]);
// 手动申请的空间需要释放,注意这里是释放的a,a与p的地址一样
free(a);
}

虽然用数组也可以返回多个值,但是程序可能会有异常,因为使用了栈区空间就有可能出问题。而使用指针则是堆区空间,不会出问题。

参数

使用逗号隔开,参数不能初始化(默认值)

1
2
3
void fun(int a, double b){

}

在声明的时候可以不写具体的变量名,也可以写

1
2
3
void fun(int, double);
void fun(int a, double b);
// 这两种都可以

在函数内部修改函数外部的变量

在python中可以通过global来修改,理解为不同的作用域。不过在c语言中没有这个,从本质上来说,就是通过指针去修改,因为指针是不变的,所以通过参数将指针传递进来便可

1
2
3
4
5
6
7
8
9
10
int fun(int* p){
*p = 1;
return *p;
}

void main(void){
int a = 0;
fun(&a);
printf("%d\n", a);
}

如果通过变量名是无法做到的,比如

1
2
3
4
5
6
7
8
int fun(int b){
b = 1;
return b;
}

int a = 0;
fun(a);
// 在函数的参数中,其实是声明了一个变量b,然后将a传递进去,实际上是给变量b赋值为a。在内存中存在两个变量a,b,其内存地址不同,所以修改b是无法做到的

二级指针

1
2
int **p;
// 二级指针装的是指针的地址

数组作为参数

一维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

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

// 用数组,其实质也是数组的指针,数组中不需要写元素个数
void fun1(int n[], int len)
{
for(int i=0;i<len;i++)
{
printf("%d\n", n[i]);
}
}

void main(void)
{
int num[3] = {1,2,3};
// 可以看到数组名存储的就是数组的地址, 所以可以直接将数组名作为参数传递
printf("%p, %p\n", &num, num);
fun(num, 3);
fun1(num, 3);
}

二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>

// 需要注意的就是嵌套的数组长度必须写上
void fun(int (*p)[3], int len1, int len2)
{
for(int i=0;i<len1;i++)
{
for(int j=0;j<len2;j++)
{
printf("%d\n", p[i][j]);
}
}
}

// 这里可以写 n[2][3] 或者 n[][3]
void fun1(int n[][3], int len1, int len2)
{
for(int i=0;i<len1;i++)
{
for(int j=0;j<len2;j++)
{
printf("%d\n", n[i][j]);
}
}
}

void main(void)
{
int num[2][3] = {{1,2,3},{4,5,6}};
fun(num, 2, 3);
fun1(num, 2, 3);
}

函数类型

函数名与函数名指针相等,所以函数名也是一个指针。其类型为将函数名换成(*p)指针

1
2
3
4
5
6
7
int fun(int a, int b)
{
printf("hello");
}
// 用指针p代替函数名
int (*p)(int a, int b) = fun;
p(1,2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int fun(int a, int b)
{
printf("%d, %d\n", a, b);
}

void main(void)
{
int (*p)(int a, int b) = fun;
p(2,3);
// 与函数名调用作用相同
fun(2,3);
}

递归函数

动态指定参数个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdarg.h> //包含的头文件

void fun(int n, ...)
{
va_list list; // 声明可变长度变量
va_start(list, n); // 赋值,参数个数
va_arg(list, int); // 取值,指定类型依次取值
va_arg(list, double);
}

void main(void)
{
fun(2, 1, 2.2);
}

字符

使用单引号标识,两种输出方式:printf和putchar

一个字符占用一个字节byte(8bit)

1
2
3
4
5
6
7
8
#include <stdio.h>

void main(void)
{
printf("%c\n", 'a');
putchar('A');
putchar('\n');
}

字符变量

1
char c = 'a';

scanf不是直接从输入中读取,输入的数据会存储到一个缓冲区,scanf会自动从缓冲区读取数据,读取一个,移除一个。

### 清空缓冲区的两种方式

  • setbuf()
  • while()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

void main(void)
{
char a,b,c;
scanf("%c", &a);
// 使用函数一次全部清空缓存,没有while通用
// setbuf(stdin, NULL);

// 使用while循环,一个读取一个缓存,读取一个移除一个
while(c = getchar() != '\n' && c != EOF);

scanf("%c", &b);
printf("%c, %c\n", a, b);
}

要达到输入一个数读取一个数,并不需要按回车,在win上有conio.h文件。在linux中没有conio.h文件,不能使用_getch()函数。不过可以使用curses.h文件达到相同效果

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


void main(void)
{
// 初始化
initscr();
char a;
// 循环读取值
while(a != '\n')
{
a = getch();
printf("%c\n", a);
}
// 结束
endwin();
}

字符数组

1
char list[3] = {'a','b','c'};
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

void main(void)
{
char list[5] = {'a','b','c','d','e'};
for(int i=0;i<5;i++)
{
printf("%c\n",list[i]);
}
}

与数组类似,重新赋值也需要用循环一个一个的赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

void main(void)
{
char list[5] = "abcde";
char m[] = "123";
for(int i=0;i<5;i++)
{
list[i] = m[i];
printf("%c %c\n",list[i],m[i]);
}
printf("%s\n", list);
}

使用库函数

因为操作这么复杂,所以c提供了操作它的库:string.h

重新赋值变得很简单

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <string.h>

int main(void){
char str[] = "hello world!";
char *p = "linux";
strcpy(str, p);
printf("%s\n", str);
}

指定覆盖前多少个字符

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <string.h>

int main(void){
char str[] = "hello world!";
char *p = "linux is simple";
strncpy(str, p, 3);
printf("%s\n", str);
}

字符串

\0(数字0)结尾的字符数组为字符串,用%s

读取和输出

%s会为自动在最后面加\0,同时scanf遇到空格会结束(无法读取空格),默认空格为分隔符。

1
2
3
4
5
6
7
#include <stdio.h>

void main(void){
char str[30];
scanf("%s", str);
printf("%s\n", str);
}

fgets可以读取空格,使用的更多

但是中文输入用fgets会乱码

1
2
3
4
5
6
7
#include <stdio.h>

void main(void){
char str[30];
fgets(str, 30, stdin);
printf("%s\n", str);
}

使用fputs代替printf

1
2
3
4
5
6
7
#include <stdio.h>

void main(void){
char str[30];
fgets(str, 30, stdin);
fputs(str, stdout);
}

声明:

  • 指定元素个数
1
2
3
char list[3] = {'a','b','\0'};
char list[3] = {'a','b',0};
char list[3] = {'a','b'}; //这里有三个元素,只写了两个,默认为0。所以为字符串
  • 不指定元素个数,必须加上\0或者0
1
char list[] = {'a','b','c',0};

在没有遇到0或者\0时一直输出,遇到0后停止,所以不需要写循环。

可以从指定位置开始,默认为第一个字符

%s需要的为指针

1
2
3
4
5
6
7
8
#include <stdio.h>

void main(void)
{
char list[6] = {'a','b',0,'c','d'};
printf("%s\n",list); // 遇到0停止输出,所以结果为ab
printf("%s\n", &list[3]); // 从index为3的位置开始输出,所以结果为cd
}

puts也可以达到相同效果

1
2
puts(list);
puts(&list[3]);

常量字符串:不可修改的字符串,使用双引号,双引号的作用为返回指针

1
2
char *p = "hello world";
printf("%s\n", p);

长度

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <string.h>

void main(void){
char *str = "12345 67";
unsigned int len = strlen(str);
printf("%d\n", len);
}

比较是否相等

strcmp相等返回值为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <string.h>

void main(void){
char *str = "abc123";
char sget[10];
scanf("%s", sget);
if(strcmp(str, sget) == 0){
printf("%s\n","equal");
}
else{
printf("%s\n", "not equal");
}
}

strncmp(str, sget, 3)比较前3个是否相等

拼接

strcat,将后面的字符拼接给前面,所以前面的必须为一个字符数组,且空间要够拼接后面的,不然会越界。

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

void main(void){
char *str = "abc123";
char sget[10];
scanf("%s", sget);
if(strncmp(str, sget, 2) == 0){
printf("%s\n","equal");
}
else{
printf("%s\n", "not equal");
}
printf("%s\n", strcat(sget, str));
}

strncat类似的取第二个字符串的前n个拼接

字符串转数字

不过,他会从第一个开始查找,如果遇到字符就退出,所以如果开头就是字符的,那么不会进行转换。

1
2
3
4
5
6
7
#include <stdio.h>
#include <stdlib.h>

void main(void){
int num = atoi("123ab4c");
printf("%d\n", num);
}

将不同类型数据转为字符串

sprintf没有输出,仅仅是类型转换

1
2
3
4
5
6
7
8
#include <stdio.h>

void main(void){
char buffer[20];
int n = 1234;
sprintf(buffer,"%d%s%f",123, "abc", 12,23);
printf("%s\n", buffer);
}

字符串数组

1
char str[2] = {"abc", "dfs"};

结构体

  • 一种数据类型
  • 包含了多种基本数据类型
  • 构造类型:自由组合基本数据类型
  • 是一种由基本数据类型自由组合的数据类型,类似与对象,是一种复合数据类型。
  1. 结构体一般放在主函数外面,作为一个全局变量,可以被引用
  2. 结构体最后需要分号
1
2
3
struct struct_name{

};
1
2
3
4
5
struct Student{
char name[10];
unsigned int age;
double high;
};

结构体变量

其实我有一点搞不懂,声明结构体的时候不是已经有了一个结构体的名字了吗,为什么还要有结构体变量?

  • 声明结构体变量可以直接写在结构体后

    1
    2
    3
    4
    5
    struct Student{
    char name[10];
    unsigned int age;
    double high;
    } stu1, stu2;

    结构体可以没有名字,就比如说这种声明结构体的同时声明变量

    1
    2
    3
    4
    5
    struct {
    char name[10];
    unsigned int age;
    double high;
    } stu1, stu2;
  • 声明结构体变量的另一种方式

    1
    2
    3
    4
    5
    6
    7
    struct Student{
    char name[10];
    unsigned int age;
    double high;
    };

    struct student stu1;

    这种方式更加灵活,因为随时可以添加别的结构体变量,而不是声明的时候已经指定好了。

现在明白了,结构体数据类型包含了struct struct_name。这两个才是一个数据类型,类似于int也是一种数据类型

初始化

1
struct Student stu1 = {'昊天', 28, 1.82};
1
2
// 初始化部分元素,其它元素默认为0
struct Student stu1 = {.name="昊天"};

访问成员变量

实例变量

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

struct Student{
char name[20];
unsigned int age;
double high;
};

void main(void){
struct Student stu1 = {"昊天", 28, 1.82};
printf("%s %d %f\n", stu1.name, stu1.age, stu1.high);
}

我发现char不能使用单引号,必须使用双引号!

指针变量

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

struct Student{
char name[20];
unsigned int age;
double high;
};

void main(void){
struct Student stu1 = {"昊天", 28, 1.82};
struct Student *p = &stu1;
printf("%s %d %f\n", p->name, p->age, p->high);
}

赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
// strcpy使用模块
#include <string.h>
// malloc使用模块
#include <stdlib.h>

struct Student{
char name[20];
unsigned int age;
double high;
};

void main(void){
// 强制类型转换,malloc手动申请空间
struct Student *p = (struct Student *)malloc(sizeof(struct Student));
// 字符数组的赋值必须使用拷贝
strcpy(p->name , "昊天");
// 指针变量赋值
p->age = 28;
// 解引用等价于实例变量
(*p).high = 1.82;
printf("%s %d %f\n", p->name, p->age, p->high);
// 释放空间
free(p);
}

整体赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

struct Student{
char name[20];
unsigned int age;
double high;
};

void main(void){
struct Student stu1 = {"昊天", 28, 1.82};
stu1 = (struct Student){"斗罗", 21, 1.76};
printf("%s %d %f\n", stu1.name, stu1.age, stu1.high);
}

指针成员

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

struct Student{
int *p;
};

void main(void){
int num[3] = {1,2,3};
struct Student stu1 = {num};
for(int i=0;i<3;i++){
printf("%d\n", num[i]);
}
}

函数成员

虽然结构体内不能直接写函数,但是可以通过指针间接使用

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

void fun(void){
printf("hello wolrd!\n");
}
struct Student{
void (*p)(void);
};

void main(void){
struct Student stu1 = {fun};
stu1.p();
}

结构体嵌套

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

struct City{
char cit_name[20];
unsigned int cit_age;
};
struct Region{
char reg_name[20];
struct City cty;
};

void main(void){
struct Region e1 = {"China", {"Beijing", 2000}};
struct Region w1 = {"American", {"Yework", 500}};
printf("%s %s %d\n", e1.reg_name, e1.cty.cit_name, e1.cty.cit_age);
}

结构体数组

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

struct Region{
char reg_name[20];
unsigned int reg_age;
};

void main(void){
struct Region e1[2] = {{"China", 2000}, {"American", 500}};
printf("%s %s\n", e1[0].reg_name, e1[1].reg_name);
}

内存对齐

32位系统,4字节为最小cpu处理单位。64位系统,8字节为最小cpu处理单位。所以内存对齐,虽然浪费了一些内存,但是cpu处理速度提高。

结构体大小

以最大类型为字节对齐宽度

联合

union,使用与结构体相同,只不过数据存储时,共享内存,即后面的会覆盖前面的,所以改变一个,其它的都会变化

1
2
3
4
union Region{
char reg_name[20];
unsigned int reg_age;
};

其实我搞不懂这有啥作用啊?有不能控制其它变量,而且这种变化并没有一个对应关系。

枚举

给整数常量起一个名称,其实和变量赋值的作用一样。只不过这个占用空间小

1
2
3
4
5
6
7
8
#include <stdio.h>

enum Shell{bash, fish, zsh};
void main(void){
printf("%d %d %d\n", bash, fish, zsh);
}

0 1 2

作用为给整形常数取一个有意义的名字,enum的常数为0开始的1递增的有序整形常数。

我觉得这个作用还挺大的,因为他表达了一种数据与变量的对应关系,而且是一次性指定多个,比变量赋值简单一些。而且值指定以后不可改变,默认为0开始,也可以自己指定,但是递增为1不变。

1
2
3
4
5
6
enum Shell{bash=20, fish=11, zsh=34};
// 20 11 34
enum Shell{bash=20, fish, zsh};
// 20 21 22
enum Shell{bash, fish=11, zsh};
// 0 11 12

内存管理

隐式类型转换

对数值类型的数据会自动进行类型转换

显式类型转换

即强制类型转换,对类型进行手动类型转换

  • 数据类型

    1
    double a = 1;
  • 指针类型

    1
    2
    3
    double a = 1.2;
    int *p = (int *)&a;
    *p = 2;

    可以由空间大的数据类型转换为数据空间小的,这样会造成精度丢失

    但是不能有数据空间小的转换为大的,因为这样会造成内存越界

小端存储

平常使用的都是小端存储

image-20200609101909648

验证小端存储:强制类型转换

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


void main(void){
int a = 134480385;
char *p = (char *)&a;
printf(" %p -> %d\n %p -> %d\n %p -> %d\n %p -> %d\n", &p[0], p[0], &p[1], p[1], &p[2],p[2], &p[3],p[3]);
}


0x7ffc3900433c -> 1
0x7ffc3900433d -> 2
0x7ffc3900433e -> 4
0x7ffc3900433f -> 8

地址由高到低,数据由低到高

1
2
3
4
内存低位对应数据低位 
0x7ffc3900433c 0x7ffc3900433d 0x7ffc3900433e 0x7ffc3900433f
1 2 4 8
00000001 00000010 00000100 00001000
1
2
实际数据应该是
00001000 00000100 00000010 00000001

验证小端存储:联合

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


union Num{
int a;
char b[4];
};

void main(void){
union Num u = {134480385};
printf(" %p -> %d\n %p -> %d\n %p -> %d\n %p -> %d\n",
&u.b[0], u.b[0], &u.b[1], u.b[1], &u.b[2],u.b[2], &u.b[3],u.b[3]);
}

0x7ffdf48ee024 -> 1
0x7ffdf48ee025 -> 2
0x7ffdf48ee026 -> 4
0x7ffdf48ee027 -> 8

使用union的特性,后面的值会覆盖前面的数据。

大端存储

一般用在通行方面

image-20200609102943704

类型重命名

typedef可以对类型进行重命名,简化书写

1
2
// 将unsigned int 重命名为unint
typedef unsigned int unint;

结构体重命名

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

typedef struct Region{
char reg_name[20];
}_Region;

void main(void){
_Region e1 = {"China"};
printf("%s\n", e1.reg_name);
}

函数指针重命名

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

float cal(int a, float b){
return a+b;
}

void main(void){
// 没有进行重命名的时候写起来比较麻烦
float (*c)(int, float) = cal;
float m = c(2, 3.1);
printf("%f\n", m);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>


float cal(int a, float b){
return a+b;
}
// 对函数指针进行重命名
typedef float (*ncal)(int, float) ;

void main(void){
ncal c = cal;
float m = c(2, 3.1);
printf("%f\n", m);
}

#define可以给一切重命名,本质是替换(会将后面的全部替换为前面的,所以不能加分号,否则分号也会当作被替换的),不会进行任何计算。

1
2
3
#define NUM 1

printf("%d\n", NUM);

参数宏

宏也可以带参数,有点像lambda表达式

1
2
3
#define PRINT(x) printf("%d\n", x)

PRINT(2);
1
2
3
4
5
6
7
8
9
#include <stdio.h>

#define PRINT(x,y) printf("%d\n", x+y)

void main(void){
PRINT(2,3);
}

5

如果传递的参数是一个表达式,那么在定义宏的时候需要加括号,否则容易因为结合性导致错误

1
#define PRINT(x,y) printf("%d\n", (x)+(y))

如果一行写不下,那么可以使用\拼接

字符串指示符

在最后面添加#参数表示将传递过来的作为字符串

1
2
3
#define PRINT(x) #x

printf("%s\n", PRINT(2));
1
2
3
4
5
#define PRINT(x, y) #x #y

printf("%s\n", 123, abc);

123abc

项目管理

一般包含了两个部分:

  • .h的头文件
    1. 函数声明
    2. 结构体类型声明
    3. typedef
  • .c的源文件
    1. 包含头文件(自己的用双引号,在项目目录下面找。而<>则是在系统库中去找)。也可写相对路径/绝对路径。
    2. 函数

静态存储区

  • 所有变量自动初始化为0
  • 生命周期为整个程序运行期间
  • 初始化的时候只能初始化为常量,不能进行计算
  • 可以多次声明,但只能定义一次。所以定义只能写在源文件中,如果写在头文件中那么会多次被引用定义
  • 包含
    • 全局变量
    • 静态全局变量
    • 静态局部变量
    • 静态函数

全局变量

一般都会加extern来标识,虽然不加也可以

1
extern a = 0;

在源文件中申明的函数也会加extern,表明函数定义在别的文件中

1
extern void fun(void);

如果是在局部变量中要声明全局变量(比如在函数中),必须要extern,且不能初始化

1
2
3
void fun(void){
extern a;
}

静态全局变量

  • static关键字

  • 只在当前文件中有效

  • 定义在主函数外

静态局部变量

  • 定义在函数中,在作用域内有效,生命周期为程序运行期间
  • 与普通变量不同,普通变量在函数执行完后销毁

寄存器变量

  • register
  • 不能用在全局变量,只能局部变量
  • 不能取地址,在cpu中
  • 但是基本上不会存成功,因为由cpu控制

const

修饰常量

常量修饰符,被修饰的变量不可(通过变量)改变,所以必须要初始化

1
const int a = 1;

但是可以通过指针去修改

1
2
3
4
5
6
7
8
9
#include <stdio.h>

void main(void){
const int a = 1;
// a被const修饰,类型为const int,要进行类型转换为int
int *p = (int *)&a;
*p = 2;
printf("%d\n", a);
}

修饰指针

只是被const修饰的变量不能直接修改(*p =2),但是其它途径都是可以修改的

1
2
3
4
5
6
7
8
9
// 通过二级指针修改
#include <stdio.h>

void main(void){
int a = 1;
const int *p = &a;
int **fp = (int **)&p;
**fp = 2;
printf("%d\n", a);
1
2
3
4
5
6
7
8
9
// 通过a修改
#include <stdio.h>

void main(void){
int a = 1;
const int *p = &a;
a = 2;
printf("%d\n", a);
}
1
2
3
4
5
6
7
8
9
10
// 还可以将p指向其它空间
#include <stdio.h>

void main(void){
int a = 1;
int b = 2;
const int *p = &a;
p = &b;
printf("%d\n", *p);
}

本质上const只是对紧接着后面的变量进行修饰,例如对p进行修饰后*p又可以赋值了

1
2
3
4
5
6
7
8
9
// 对p进行修饰
#include <stdio.h>

void main(void){
int a = 1;
int * const p = &a;
*p = 2;
printf("%d\n", *p);
}

可对p和*p同时修饰,这时对p和*p都不可直接修改了

1
2
3
4
5
6
7
#include <stdio.h>

void main(void){
int a = 1;
const int * const p = &a;
printf("%d\n", *p);
}

但是还是可以用二级指针修改

1
2
3
4
5
6
7
8
#include <stdio.h>

void main(void){
int a = 1;
const int * const p = &a;
int **m = (int **)&p;
**m = 2;
printf("%d\n", *p);

总之:const就是对变量进行一个修饰,表示不可修改,但是并不是说真正的修改不了。在c++中const修饰后就真正的无法修改了

volatile

被修饰的变量告诉系统,该变量不需要被优化,不需要放入寄存器或者高速缓存

restrict

只能用来修饰指针,当指针存在连续的算术运算时,会在编译的时候进行一些优化

内存分区

内存分区:

  • 栈区:申请和释放都由操作系统决定。约为4Gb(与操作系统有关),系统需要检测何时释放会占用cpu资源

  • 堆区:程序决定何时申请和释放malloc/free。空间大小没有限制

  • 静态全局区:全局变量/static变量,自动初始化为0,生命周期为程序运行期间

  • 字符常量区:只读。数值(12),字符常量(’a’被系统识别为ascii码),字符串常量(”abc”)。字符串常量生命周期为程序运行期间。数值常量不占用空间,立即数存储,随用随丢。

    全局变量被const修饰后存储到字符常量区,局部变量被const修饰后存储在栈区

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>

    const int a = 1;

    void main(void){
    int *p = (int *)&a;
    *p = 2;
    printf("%d\n", a);
    }

    [1] 78458 segmentation fault (core dumped) "/home/fsl/Data/code/c/"learn
    // 不可修改
  • 代码区:只读

命令行参数

1
main(int argc, char *argv[])

argc:命令行参数个数

argv:命令行参数数组,argv[0]为程序路径

1
2
3
4
5
6
7
8
9
#include <stdio.h>

void main(int argc, char *argv[]){
printf("%d %s %s %s\n", argc, argv[0], argv[1], argv[2]);
}


》》》 ./learn ab cd
3 ./learn ab cd

位运算