c
环境配置
- 使用vscode来编辑,由于是在linux下,不需要下载gcc(编译)默认已经安装了
- 下载c/c++插件,可以自动补全
- 下载code runner,可以实现一键运行,不需要手动编译
code runner的配置
- 在左下角的设置/或者快捷键ctrl+k ctrl+s进入快捷键设置界面,设置ctrl+enter运行代码
- 在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"); return 0; }
|
注释
单行注释使用//
,多行注释使用/* */
,vscode中只需要选中行,ctrl+/
就可以注释了
主函数
一个项目只能有一个主函数,main函数是程序的入口,int定义函数返回值,return 0,0表示正常结束
1 2 3 4 5 6 7 8 9 10 11
| #include <stdio.h>
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
- 字符型
构造类型
指针
整型
- 声明与定义区别:声明一个变量未赋值,定义一个变量同时赋值
- 变量与常量区别:常量不可改变,变量可变
- 有符号和无符号:有负数的为有符号(会将最高位作为符号位,0代表正数,1代表负数),没有负值的为无符号
- int默认为有符号
- int范围:-2^31~2^31-1,4个bitex8bit
- 无符号范围: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; }
|
表达式和语句
表达式和语句的区别在有分号,以分号结尾的为语句,否则为表达式,表达式有值,由运算符组合而成
用户输入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)
|
浮点数
- 浮点型的零写0.0
- 有效数位:从左边开始非0的位数
- 设置显示小数位数
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 a = 12313.12312f; printf("%f\n", a); return 0; }
|
自加自减误区
- 在一个语句中,不能超过两个的自加自减运算符
- 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; }
|
流程结构
- 顺序
- 循环
- 分支和跳转
- 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
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
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
- 注意每个case后都需要加break等跳出switch,否则会执行后面的语句,不管是否符合条件
- switch只能用整型
- 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
| int queue[4] = {1,2,3}; int queue[4] = {1,[3]=4};
queue[3] = 4;
int queue[4] = {0};
|
地址
+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 = #
|
1 2 3 4 5 6 7 8
| #include <stdio.h>
int main(void){ int num = 10; int *p = # printf("%p %p", p, &num); return 0; }
|
我有个疑问:变量明明可以操作数据,为什么还要用指针操作呢
通过指针操作具体数据:
1 2 3 4 5 6
| int a = 10; int *p = &a; printf("%d", *p); *p = 20; printf("%d", 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])
|
数组的指针
昨天写了一个用数组变量名地址来作为指针地址的,结果发现老是报错:
1 2 3
| int num[3] = {2,3,4}; int *p = #
|
开始我还以为只能用元素地址,今天学了这个才发现原来有这个概念,只是我写法不对
1 2 3 4 5 6 7 8
| #include <stdio.h>
int main(void){ int num[3] = {2,3,4}; int (*p)[3] = # 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] = # printf("%d\n", (*p)[0][1]); }
|
虽然指针学了这么多,但是我还是不知道它到底有什么用。而且不是都说指针挺难吗,可是我不知道这到底哪里难了,难道我没学到它真正难的知识点吗
指针在程序中的设置:如果程序设置了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); 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; }
|
另一种方式,按字节对申请空间进行赋值
使用free()
函数释放空间
内存泄漏:当使用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
| 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 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* 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]); 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);
|
二级指针
数组作为参数
一维数组
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]); } } }
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"); }
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'); }
|
字符变量
scanf不是直接从输入中读取,输入的数据会存储到一个缓冲区,scanf会自动从缓冲区读取数据,读取一个,移除一个。
### 清空缓冲区的两种方式
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(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'};
|
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); printf("%s\n", &list[3]); }
|
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 3
| struct struct_name{ };
|
1 2 3 4 5
| struct Student{ char name[10]; unsigned int age; double high; };
|
结构体变量
其实我有一点搞不懂,声明结构体的时候不是已经有了一个结构体的名字了吗,为什么还要有结构体变量?
现在明白了,结构体数据类型包含了struct struct_name
。这两个才是一个数据类型,类似于int
也是一种数据类型
初始化
1
| struct Student stu1 = {'昊天', 28, 1.82};
|
1 2
| 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>
#include <string.h>
#include <stdlib.h>
struct Student{ char name[20]; unsigned int age; double high; };
void main(void){ 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};
enum Shell{bash=20, fish, zsh};
enum Shell{bash, fish=11, zsh};
|
内存管理
隐式类型转换
对数值类型的数据会自动进行类型转换
显式类型转换
即强制类型转换,对类型进行手动类型转换
小端存储
平常使用的都是小端存储
验证小端存储:强制类型转换
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的特性,后面的值会覆盖前面的数据。
大端存储
一般用在通行方面
类型重命名
typedef
可以对类型进行重命名,简化书写
1 2
| 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的头文件
- 函数声明
- 结构体类型声明
- 宏
- typedef
- .c的源文件
- 包含头文件(自己的用双引号,在项目目录下面找。而<>则是在系统库中去找)。也可写相对路径/绝对路径。
- 函数
静态存储区
- 所有变量自动初始化为0
- 生命周期为整个程序运行期间
- 初始化的时候只能初始化为常量,不能进行计算
- 可以多次声明,但只能定义一次。所以定义只能写在源文件中,如果写在头文件中那么会多次被引用定义
- 包含
全局变量
一般都会加extern
来标识,虽然不加也可以
在源文件中申明的函数也会加extern
,表明函数定义在别的文件中
如果是在局部变量中要声明全局变量(比如在函数中),必须要extern
,且不能初始化
1 2 3
| void fun(void){ extern a; }
|
静态全局变量
static
关键字
只在当前文件中有效
定义在主函数外
静态局部变量
- 定义在函数中,在作用域内有效,生命周期为程序运行期间
- 与普通变量不同,普通变量在函数执行完后销毁
寄存器变量
register
- 不能用在全局变量,只能局部变量
- 不能取地址,在cpu中
- 但是基本上不会存成功,因为由cpu控制
const
修饰常量
常量修饰符,被修饰的变量不可(通过变量)改变,所以必须要初始化
但是可以通过指针去修改
1 2 3 4 5 6 7 8 9
| #include <stdio.h>
void main(void){ const int a = 1; 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
| #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
| #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
| #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
|
位运算