概览
此笔记仅供大家免费学习参考所用
视频地址
https://www.bilibili.com/video/BV18p4y167Md/
c的历史
- 1960 原型A语言->ALGOL语言
- 1963 CPL语言
- 1967 BCPL
- 1970 B语言
- 1973 C语言
C语言特点
- 基础性语言
- 语法简洁 紧凑 方便 灵活(得益于指针)
- 运算符 数据结构丰富
- 结构化 模块化编程
- 移植性好 执行效率高
- 允许直接对硬件操作
学习建议
- 概念的正确性
- 动手能力
- 主动阅读优秀的程序段
- 大量练习,编程是技术不是理论
学习思路
- 基本概念
- 数据类型 运算符 表达式
- 输入输出
- 流程控制
- 数组
- 指针
- 函数
- 构造类型
- 动态内存管理
- 常用库函数
- 调试工具和调试技巧
环境搭建与"Hello world"
环境
- 当前测试环境是安装了基于
archlinux
的manjarolinux
发行版的物理机,大家自己搭建linux环境的话推荐试用或租用云服务器或者尝试WSL
gcc
版本是 10.2.0- 编辑器使用
vim
(推荐vim配置vimplus) - vim配置脚本以及常用快捷方式
“Hello world”
#inlcude <stdio.h>
#include <stdlib.h>
int main(void){
printf("hello world\n");
exit(0);
}
gcc 编译c的源文件过程
gcc -v
C源文件->预处理->编译->汇编->链接->可执行文件
完整过程
- 预处理 重定向 > 转存
gcc -E hello.c > hello.i
- 编译
gcc -S hello.i
- 汇编
gcc -c hello.s
- 链接->可执行文件
hello.o//目标文件
gcc hello.o -o hello
或者
gcc hello.c
gcc hello.c -o hello
又或者
make hello
执行
./hello
hello world
基本概念
怎么写代码
头文件的重要性
以函数为单位来进行程序编写
声明部分+实现部分;return 0; 多用空格空行;添加注释;
防止写越界,防止内存泄露,谁打开水关闭,谁申请谁释放;
程序员的约定:在c中,如果没有出现函数原型,就默认函数的返回值是int
#include <stdio.h>
int main()
{
<<<<<<< HEAD
int *num = malloc(sizeof(int));
*num = 100;
printf("%d\n",*num);
=======
int *num = (int *)malloc(sizeof(int));
>>>>>>> d8201497ccb6ce60411ef4f1d6347921a54e7c22
return 0;
}
hello.c: 在函数‘main’中:
hello.c:5:23: 警告:隐式声明函数‘malloc’ [-Wimplicit-function-declaration]
5 | int *num = (int *)malloc(sizeof(int));
| ^~~~~~
hello.c:5:23: 警告:隐式声明与内建函数‘malloc’不兼容
- 正确写法
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *num = (int *)malloc(sizeof(int));
return 0;
}
数据类型 运算符 表达式
- 基本类型
- 数值类型
- short
- int
- long
- float
- double
- 字符类型(char)
- 数值类型
- 构造类型
- 数组
- 结构体 struct
- 共用体 union
- 枚举类型 enum
- 指针类型
- 空类型 void
254 -> unsigned int -> 32bit
(254)10 = (1111 1110)2 = (376)8 = (FE)16
(以上括号外面表示进制)
254
B11111110(c不认识这个表示)
0376
0xFE
类型转换
int i;
float f;
doubel d;
char ch;
(隐式和显式(强制类型转换))
ch + i -> i
f -d -> d
(ch + i) - (dloat -double) -> double
布尔型bool
#incldue <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
int main() {
bool a = false;
printf("a = %d\n", a);
exit(0);
}
浮点型的失真问题
float类型的数本身精度不精确
int func(float f) {
if (f < 0) {
return -1;
} else if (fabs(f-0) <= 1e-6) {
return 0;
} else {
return 1;
}
}
浮点数
比喻1e1(科学计数法)
e后面跟的是10的指数(也就是1的10次方,e表示10次方),f表示浮点数
1e1表示1×10¹,其实就是10
再例如5e2f,表示5×10²,也就是500
========================================================================================================================
-1.56E+12 的常量表示法怎么计算?
理解为1.56的12次方的负数?也就是:-1560000000000?
-1.56*10^12=-1560000000000
理解为-1.56*10的12次方-1560000000000
========================================================================================================================
0.1101101111
+ 0.0000000001
---------------
0.110110000
因为在任何区间内(比如1.0和2.0之间)都存在无穷多个实数,所以计算机浮点数不能表示区域内所有的值。浮点数往往只是实际值的近似。例如7.0可能以浮点数值6.99999存储。
解释
十进制转化为二进制的方法是 依次与2^(-n)作比较(n从1开始)
若大于该值则为1,且减去此值,否则为0;然后继续下一轮比较
举例说明:将0.842356转换成二进制,你会发现比较将会是无穷无尽的。
如果你截取到某位,必须做一些取舍。取舍的标准是:其后一位若为1则进1;后一位为0则不进。
若要截取9位,因为第10位为0,故不进位,则最终的结果为:0.110101111;
若要截取到8位,因为第9位为1,故要进位,则最终的结果为:0.110110000(即0.1101101111 + 0.0000000001)。
从这个例子可以看出十进制小数的转换成二进制时只是一个近似值。其实大部分浮点数保存在计算机中都只是一个近似值。至于是稍微大于原值还是稍微小于原值,要看截取时有无进位。
0.842356
0.110101111 0 1001001010010010001111100101101110000101011 截取第9位 第10位为0,所以不进位=0.110101111
0.11010111 1 01001001010010010001111100101101110000101011 截取第8位 第9位为1,所以进位 =0.110110000
char
在iso c中 char
有无符号是未定义行为
0
0(整型) '0'(字符常量) "0"(字符串常量) '\0'(字符常量)
输入输出
数据类型要和后续代码中所使用的输入输出要相匹配(小心自相矛盾)
精度超出部分会从输出的类型范围的最小值开始累积计算
#include <stdlib.h>
#include <stdio.h>
int main() {
unsigned int a;
a = 1 << 31;
printf("%d", a);
}
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
int main(){
unsigned a=4146464646;
printf("a=%d \n",a);
exit(0);
}
输出:a=-148502650
4294967295 - 4146464646 = 148502650
正确
#include <stdlib.h>
#include <stdio.h>
int main() {
unsigned int a;
a = 1 << 31;
printf("%ud", a);
}
常量与变量
常量
在程序执行过程中值不会发生变化的量
-
整形常量 1 890
-
实型常量 1.2 3.14
-
字符常量:由单引号引起来的单个的字符或转义字符,如 :
‘\t’ ‘\n’ ‘\0’ ‘\015’(8进制) ‘\x7f’ (16进制)‘\018’(错误的表示!!)
-
字符串常量:由双引号引起来的一个或多个字符组成的序列,如:
“” “a” “abXYZ” “abc\n\021\010”(a b c \n \021 \0 1 8)
-
标识常量 :#define,处理在程序的预处理阶段,占编译时间;特点一改全改;缺点:不检查语法,只是单纯的宏体与宏名之间的替换。
宏的用法
#include <stdlib.h>
#include <stdio.h>
#define PI 3.1415926
#define ADD 2+3
// 正确写法
//#define ADD (2+3)
int main() {
printf("%f\n", PI);
printf("%d\n", ADD * ADD);
}
#ifndef Tool1_H_
#define Tool1_H_
//如果没有Tool1_H_,那就define一个Tool1_H_
# 6 "pi.c"
int main() {
printf("%f\n", 3.1415926);
printf("%d\n", 2+3 * 2+3);
}
#include <stdlib.h>
#include <stdio.h>
#define MAX(a,b) ({int A=a,B=b; ((A) > (B) ? (A) : (B));})
//define MAX(a,b) ((A) > (B) ? (A) : (B))
int main() {
int a = 3, b = 5;
printf("%d\n",MAX(a++, b++));//较大的值会自增两次
printf("%d\n",MAX(a++, b++));
}
#include <stdlib.h>
#include <stdio.h>
//typeof函数获取a的类型
#define MAX(a,b) ({typeof(a) A=a,B=b; ((A) > (B) ? (A) : (B));})
int main() {
int a = 3, b = 5;
printf("%d\n",MAX(a++, b++));
printf("%d\n",MAX(a++, b++));
}
在程序的预处理阶段,占编译时间,不占运行时间(没有函数调用的消耗),但不检查语法(比较危险)
变量
用来保存一些特定内容,并且在程序执行过程中值随时会发生变化的量。
[存储类型] 数据类型 标识符 = 值
TYPE NAME = VALUE
标识符:由字母,数字,下划线组成且不能以数字开头的一个标识序列。写标识符尽量做到见名生义。
数据类型:基本数据类型+构造类型
值:注意匹配
存储类型:
-
auto 默认 自动分配空间,自动回收空间。
-
register (建议型) 寄存器类型 只能定义局部变量,不能定义全局变量,大小有限制,只能定义32位大小的数据类型,比如double就不可以。因为寄存器没有地址,所以一个register类型的变量无法打印出地址查看或使用。
-
static (静态型) 自动初始化为0值或空值 并且static变量的值有继承性。另外常用来修饰一个变量或者函数(防止当前函数对外扩展)
#include <stdlib.h>
#include <stdio.h>
void func() {
static int x = 1;
x++;
printf("%d\n", x);
}
int main() {
func();
func();
func();
}
2
3
4
- extern (说明型) 意味着不能改变被说明的量的值或类型 可以用来扩展外部变量的作用域(extern就相当于我虽然不知道在哪里有,但是肯定存在。就用extern)
变量的生命周期和作用范围
1.全局变量和局部变量 2.局部变量和局部变量 3.图片
#ifndef EXTERN_H__
#define EXTERN_H__
void func();
#endif
#include "extern.h"
extern int i; // 不定义 而是引用了其他地方的i
int func() {
printf("[%s]%d\n", __FUNCTION__, i);/*gcc自带的宏函数,当前所在的函数名*/
}
#include "stdlib.h"
#include "stdio.h"
#include "extern.h"
int i = 10;
int main() {
printf("[%s]%d\n", __FUNCTION__, i);
}
linux命令
vim * -p 打开当前目录所有文件
gt 或 gT 左右切换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2TPlmFha-1693146368195)(E:\Git_study\LinuxC\C\image-20230729175127822.png)]
表达式
表达式与语句的区别
运算符部分:1.每个运算所需要的参与运算的操作数个数
2.结合性
3.优先级
4.运算符的特殊用法; 如:% 、==和=、 逻辑运算符(&&,||)的短路特性;
5.位运算的重要意义:将操作数中的第n位置1 其他位不变: num = num | 1 << n;// num | (1 << n)
将操作数中的第n位置0 其他位不变: num = num & ~(1<<n);
测试第n位: if(num & (1<<n))
逻辑运算符的短路性
#include <stdio.h>
#include <stdlib.h>
int main() {
int a = 1, b = 2, c = 3, d = 4;
int m = 1, n = 1;
(m = a > b) && (n = c > d);//后面表达式未计算,各变量仍是原值
printf("m = %d\n n = %d\n", m, n); // m : 0 n : 1
}
a -= a *= a += 3;//最终a的值为0;
sizeof
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("%lu, %lu, %lu, %lu, %lu, %lu, %lu\n",
sizeof(int),sizeof(short), sizeof(long),
sizeof(double), sizeof(float), sizeof(char), sizeof(void*));
}
4, 2, 8, 8, 4, 1, 8
位运算
- | 按位或
- & 按位与
- ^ 按位异或
- ~ 按位取反
应用 :小重点
将操作数中的第n位置1 其他位不变: num = num | 1 << n;// num | (1 << n)
将操作数中的第n位置0 其他位不变: num = num & ~(1<<n);
测试第n位: if(num & (1<<n))
从一个指定宽度的数中取出其中的某几位:
比如对于1101, 你要第二位(1)
1101 & 0100 = 0100
0100 >> 2 = 01 = 1
假设 i = B1100 =12
~i --> B0011;
i >> 1 --> 110 =6;
110 << 1 --> 1100 = 12;
int i = B1100;
int j = B1001;
i | j
1100
| 1001
---------
1101
i & j
1100
& 1001
---------
1000
i ^ j
1100
^ 1001
---------
0101
if i=B1000
int i=i|1<<2
1000
| 100
---------
1100
从一个指定宽度的数中取出其中的某几位:
比如对于1101, 你要中间两位
1101 & 0110 = 0100
0100 >> 1 = 010 = 10
IO
input & output -> I/O(标准IO,文件IO)
-
格式化输入输出函数:scanf,printf
int printf(const char *format, …);
format:“%[修饰符] 格式字符”
参照图片标准输出修饰符与输入输出格式字符。
int scanf(const char *format, …);//省略的是地址表
format:抑制符*
scanf 在使用
%s
的时候要特别小心,已经越界却不报错,放在循环结构中要注意能否接收到正常有效的内容。 -
字符输入输出函数:getchar,putchar
-
字符串输入输出函数:gets(!),puts
gets非常危险,不要用,用fgets()或者getline()来代替
变长参数
int main() {
int i = 123;
printf("%4d\n", i);
int d=65;
printf("%c\n",d);\\A
float f = 1.23455;
printf("%.3f\n", f);
char* s= "helloworld";
printf("%10.5s\n", s);
}
//定参 变参
刷新缓冲区
int main() {
printf("[%s:%d] before while().", __FUNCTION__, __LINE__);
while(1);
printf("[%s:%d] after while().", __FUNCTION__, __LINE__);
}
/n还有刷新缓冲区的意思,
__LINE__打印该语句行号
exit()会刷新io流
正确写法
#include <stdlib.h>
#include <stdio.h>
int main() {
printf("[%s:%d] before while().]\n", __FUNCTION__, __LINE__);
// 或者
//printf("[%s:%d] before while().", __FUNCTION__, __LINE__);
//fflush(stdout);
//sleep(5);//睡眠5秒
while(1);
printf("[%s:%d] after while().", __FUNCTION__, __LINE__);
}
- scanf
int main() {
int i;
scanf("%d", &i);
printf("%d\n", i);
}
//空格 回车 tab键
scanf 在使用
%s
的时候要特别小心,已经越界却不报错
#include <stdio.h>
#include <stdlib.h>
int main() {
char S[3];
scanf("%s", S); // 如果输入 abcdef
printf("%s", S); //可能会出现段错误,比如中间有空格
}
scanf 在循环中使用的时候要特别小心
int main() {
int ret = 0;
int d = 0;
while(1) {
ret = scanf("%d", d);//加scanf返回值的校验
if (ret == -1) {
perror("Error enter");
break;
}
printf("%d\n", d);
}
exit(0);
}
处理换行
int main() {
int i = 0;
char c = 0;
scanf("%d", &i);
scanf("%*c%c", &c);//%*c,抑制符用法
// 或者
//getchar();
//scanf("%c", &c);
printf("i = %d, c = %c", i, c);
}
10是换行符号,32是空格的ASCII码
流程控制
//逗号表达式的值,去最后一个表达式的值。
1.goto语句(慎用)
goto语句是一种无条件转移语句,goto 语句的使用格式为:
goto 语句标号;
其中语句标号是一个有效的标识符,这个标识符加上一个 ":" 一起出现在函数内某处,执行goto语句后,程序将跳转到该标号处并执行其后的语句;
另外语句标号必须与goto语句同处于一个函数中,但可以不在一个循环层中;通常goto语句与if条件语句连用,当满足某一条件时,程序跳到标号处运行;如下例子:
#include <stdio.h>
int main(void){
int i,sum=0;
i=1;
loop: if(i<=100){
sum=sum+i;
i++;
goto loop;
}
printf("%d\n",sum);
return 0;
}
2.if语句
if使用格式为:
if(condition1)
{}
else if(condition2)
{}
else if(condition3)
...
else
{}
除此之外,if语句之间可以嵌套,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g6eXxCz3-1693146368196)(E:\Git_study\LinuxC\C\watermark,type_ZmFuZ3poZ34.jpg)]
3.switch语句
switch的语法格式为:
switch (表达式)
{
case 常量1:语句1
case 常量2:语句2
...
default: 语句n+1
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WK54bbMQ-1693146368196)(E:\Git_study\LinuxC\C\watermark,type_ZmFuZ34.jpg)]
注意:
1)switch后面括号内的表达式,其值类型应该为整数类型,包括字符型;
2)可以没有default语句,此时流程转到switch语句的下一条语句执行;
3)每一个分支语句后一般都要加一条break语句,用来跳出switch语句块,最后一个分支可以不加;如将上面的例子改为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bLNSby8N-1693146368196)(E:\Git_study\LinuxC\C\20210816235525146.png)]
此时的执行结果为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aE4geTmN-1693146368197)(E:\Git_study\LinuxC\C\20210816235600523.png)]
这是因为分支后的常量表达式只会匹配一次,匹配成功后就会执行该分支后的所有语句,如果不加break,则会将后面所有分支的语句执行完!!
4)多个 case 分支可以共用一组执行语句,如下:
case 'A':
case 'B':
{
printf....
}
4.while循环
语法格式:
while (表达式)
{
语句
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ltbZY2r2-1693146368197)(E:\Git_study\LinuxC\C\20210817215700216.png)]
5.do...while循环
语法格式:
do
{
语句
}while(表达式);
与while不同的是,do..while的循环体至少会被执行一次
6.for循环
语法格式:
for ([表达式1];[表达式2];[表达式3])
语句
break和continue
break用于终止当前层次的循环,而continue用于结束本次循环,转入下一次循环
数组
构造类型之一,连续存放
一维数组
[存储类型] 数据类型 标识符[下标]
初始化
不初始化,全部初始化,部分初始化,static
- static
static int a[10];//会初始化为全零元素
- {}
int a[3] = {1, 2, 3};
元素引用
- arr[i]
- arr+i
数组名
数组名是表示地址的常量,也是数组的起始位置。
#include <stdio.h>
#include <stdlib.h>
int main() {
int arr[3] = {1, 2, 3};
printf("%ld\n", sizeof(arr));
// 下面这句是错的
arr = {4, 5, 6};
for (int i = 0;i < sizeof(arr)/sizeof(int);i++) {
printf("%d", *(arr+i));
}
}
//打印地址符,%p
//strlen是用来求一个字符串(string)的长度
1.定义数组时,要先明确数组的类型(int还是char或者其他类型)。
2.sizeof不能用来计算已经定义好长度的数组的数组长度(如int arr[10] = {0},不能有中括号里的10)。
3.strlen不能用来计算int类型的数组长度。
4.int型计算数组长度,最好用 sizeof(arr) / sizeof(arr[0]) 来计算数组长度。
5.char型数组如果要计算数组长度,一定要用双引号赋值。
6.char型数组如果要用大括号赋值,一定要注意数组最后是否需要结束标志 \0
数组越界
c对数组不进行越界检查,需要格外小心
练习
#include <stdio.h>
#include <stdlib.h>
int main() {
int fib[10] = {1, 1};
for (int i = 2;i < 10;i++) {
fib[i] = fib[i-1]+ fib[i-2];
}
for (int i = 0;i < 10;i++) {
printf("%d ", fib[i]);
}
}
#include <stdio.h>
#include <stdlib.h>
int main() {
int arr[] = {2, 3, 5, 4, 6, 7, 1, 9};
for (int i = 0;i < sizeof(arr)/sizeof(int);i++) {
for (int j = 0;j < sizeof(arr)/sizeof(int)-1-i;j++) {
if(arr[j] > arr[j+1]) {
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
for (int i = 0;i < sizeof(arr)/sizeof(int);i++) {
printf("%d ", arr[i]);
}
}
#include <stdio.h>
#include <stdlib.h>
int main() {
int arr[] = {3, 2, 5, 4, 9, 7, 1, 6};
for (int i = 0;i < sizeof(arr)/sizeof(int);i++) {
int m = i;
for (int j = i+1;j < sizeof(arr)/sizeof(int);j++) {
if(arr[j] < arr[m]) {
m = j;
}
}
if (m != i) {
int tmp = arr[i];
arr[i] = arr[m];
arr[m] = tmp;
}
}
for (int i = 0; i < sizeof(arr) / sizeof(int); i++) {
printf("%d ", arr[i]);
}
}
二维数组
-
定义,初始化:[存储类型] 数据类型 标识符 【行下标】【列下标】
2.元素引用:数组名【行标】【列标】
3.存储形式:顺序存储,按行存储
int main() {
int a[M][N] = {1, 2, 3, 4, 5};//合法
int b[][N] = {1, 2, 3, 4, 5};//合法
int c[M][] = {1, 2, 3, 4, 5}; // 错误
for (int i = 0;i < M;i++) {
for (int j = 0;j < N;j++) {
//printf("%d ", *(a+i+j*));
printf("%p-->%d",&a[i][j],a[i]a[j]);
}
}
}
//数组没初始化,就是没往里面写值,原来每位的电平是啥就是啥
//a+1 看作: 归属为a的一维数组的基本元素的1个单位长度
//行指针
深入理解二维数组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WOMyIF0s-1693146368197)(E:\Git_study\LinuxC\C\image-20230805104538945.png)]
a[2][3] = b[3] + c[3]
a[0] = b[0]
a[1] = c[0]
字符数组
定义,初始化,存储特点
[存储类型] 数据类型 标识符 [下标]…
单个字符初始化;用字符串常量初始化
注意部分初始化的时候,尾部会自动初始化为’\0’;这个出错的情况是不确定的,例如如果有一个变量的地址刚好在str[N]的后面,你的输出也没问题,但是那个变量的内容就被改了,之后再访问那个变量就是乱码,又如你的输入特别长,遇到一块不可写的内存,会段错误
IO
输入输出:scanf 无法获取带有分隔符的字符串(\t
, \n
,
)
常用函数
- strlen & sizeof
- strcpy & strncpy 复制
- strcat & strncat 连接
- strcmp & strncmp 比较
单词统计
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define STRSIZE 1024
int main() {
char str[STRSIZE] = {};
fgets(str, STRSIZE, stdin);
int count= 0, flag = 0;
for (int i = 0;str[i] != '\0';i++){
if (str[i] == ' ') {
flag = 0;
} else if(!flag) {
count++;
flag = 1;
}
}
printf("%d\n", count);
}
//strlen以尾零为结束标记 sizeof计算所占内存全部大小
//char *strcpy(char *dest, const char *src);
//char *strncpy(char *dest, const char *src, size_t n);
//char *strcat(char *dest, const char *src);
//char *strncat(char *dest, const char *src, size_t n);
//int strcmp(const char *s1, const char *s2);
//int strncmp(const char *s1, const char *s2, size_t n);
多维数组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vx8JTCL3-1693146368198)(E:\Git_study\LinuxC\C\image-20230806194815821.png)]
指针
64位环境 指针类型占用8个字节
32位环境 指针类型占用4个字节
变量与地址
变量是对某块内存的抽象表示
指针 == 地址 变量名 == 抽象出来的某块空间的别名
指针与指针变量
int i = 1;
int *p = &i;
int ** q = &p;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kqD75Uzq-1693146368198)(E:\Git_study\LinuxC\C\image-20230806201226144.png)]
直接访问与间接访问
i = 1;
&i = 0x2000;
p = 0x2000;
&p = 0x3000;
*p = 1;
q = 0x3000;
&q = 0x4000;
*q = 0x2000;
**q = 1;
//要翻译回来的,类型不一致了就翻译不回来咯
//指针类型用来使用数据时正确的分割内存单元
//不管什么类型几级的指针,它在某一平台所占的内存大小是确定的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6G4zkOLt-1693146368198)(E:\Git_study\LinuxC\C\image-20230812102659888.png)]
空指针与野指针
void func(){
int *p=NULL;
printf("%d\n",*p);
}
空类型
char *s = "hello";
void *i = s;//不确定什么类型就用void空类型
定义与初始化的写法
指针运算
& * 关系运算 ++ –
指针与数组
指针与一维数组
#include <stdlib.h>
#include <stdio.h>
int main () {
int a[3] = {1, 2, 3};
// a是常量 p是变量
int *p = a;
for (int i = 0;i < sizeof(a)/sizeof(*a);i++) {
printf("%p -> %d\n", p+i, *(p+i));
}
}
//a[i]: a[i]=*(a+i)=*(p+i)=p[i]
//&a[i]: &a[i]=a+i=p+i=&p[i]
//p++!= p+1
//因为p++ == p = p+1,已经发生了赋值操作
p++
!= p+1
#include <stdlib.h>
#include <stdio.h>
int main () {
int a[3];
int *p = a;
for (int i = 0;i < sizeof(a)/sizeof(*a);i++) {
scanf("%d", p++);
}
for (int i = 0;i < sizeof(a)/sizeof(*a);i++) {
printf("%d\n", *(p++));
}
}
//printf是从右边开始取内容,所以刚刚先取了*p++导致p又自增了1,所以从4开始而不是0
//*和++同级运算啊,并列按自右向左的顺序运算。
//int * p=(int [3]){1,2,3};匿名数组
指针与二维数组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gADMCmqT-1693146368198)(E:\Git_study\LinuxC\C\image-20230814213143790.png)]
#include <stdlib.h>
#include <stdio.h>
int main() {
int a[2][3] = {{1, 2, 3},{ 4, 5, 6}};
int (*p)[3] = a;
//p=&a[i][j];*(a+0);*a;
for (int i = 0;i < sizeof(a)/sizeof(*a);i++) {
for (int j = 0;j < sizeof(*a)/sizeof(**a);j++) {
printf("%d ", *(*(p+i)+j));
}
}
}
//因为二维数组*完是一维数组
//行指针和列指针,帮助理解的方言
//因为a实际存储的是每一列的首地址,a是个二级指针
//取*为啥是降级变成列指针
#include <stdlib.h>
#include <stdio.h>
int main() {
int a[2][3] = {{1, 2, 3},{ 4, 5, 6}};
int *p = &a[0][0];
for (int i = 0;i < sizeof(a)/sizeof(*a);i++) {
for (int j = 0;j < sizeof(*a)/sizeof(**a);j++) {
printf("%d ",*(p+(i * sizeof(*a)/sizeof(**a))+j));
}
}
}
指针与字符指针
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main() {
char str[]="Hello";//字符数组
//str="World" 典型错误
char *str1="hell0";//字符指针
printf("%d %d",sizeof(str1),strlen(str1));
//4 5 前面是输出一个字符指针的内存长度
//strcpy(str1,"world"); 想覆盖字符串常量??no
str1="world";
puts(str);
}
const与指针
首先,来看看const的基本含义。在 C/C++ 语言中,const关键字是一种修饰符。所谓“修饰符”,就是在编译器进行编译的过程中,给编译器一些“要求”或“提示”,但修饰符本身,并不产生任何实际代码。就 const 修饰符而言,它用来告诉编译器,被修饰的这些东西,具有“只读”的特点。在编译的过程中,一旦我们的代码试图去改变这些东西,编译器就应该给出错误提示。
所以,const修饰符的作用主要是利用编译器帮助我们检查自己代码的正确性。我们使用const在源码中标示出“不应该改变”的地方,然后利用编译器,帮助我们检查这些地方是否真的没有被改变过。如果我们不小心去修改了这些地方,编译器就会报错,从而帮助我们纠正错误。使用const和不使用const,对于最终编译产生的代码并没有影响。
虽然const对于最终代码没有影响,但是尽可能使用const,将帮助我们避免很多错误,提高程序正确率。
const float pi = 3.14; // 常量化变量
先看到指针就是指针 先看到常量就是常量
如何区分指针常量和常量指针,看const在后还是前,*在前指针常量,*在后常量指针
- 常量指针 指向的内存不能通过这个指针修改
const int* p;//常量指针
int const *p;
char *strcpy(char *restrict dest, const char *src); // src是源字符串 不应该被修改
- 指针常量 指向的位置不能变 可以通过这个指针修改内存的值
int *const p;
const int *const p;
//const int *const p;常量指针常量
指针数组与数组指针
指针数组
//我的理解,无论是指针数组(数组),数组指针(指针)强调的都是后者
// 【存储类型】 数据类型 * 数组名 【长度】
//int *arr[3]--》type name;-->int *[3] arr;
int *arr[3]
指针数组排序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *name[5] = {"golang", "java", "c", "dart", "erlang"};
int k;
char *tmp;
for (int i = 0;i < (sizeof(name)/sizeof(*name))-1;i++) {
k = i;
for (int j = i+1;j < (sizeof(name)/sizeof(*name));j++) {
if (strcmp(name[k], name[j]) > 0) {
k = j;
}
}
if (k != i) {
tmp = name[i];
name[i] = name[k];
name[k] = tmp;
}
}
for (int i = 0;i < (sizeof(name)/sizeof(*name));i++) {
printf("%s\n", *(name+i));
}
}
数组指针
// 【存储类型】 数据类型 (*指针名)【下标】 = 值
//int (*p)[3];--》type name;-->int[3] *p;
//这样指针移动一次的距离就是一个数组的大小;
int a[2][3] = {{1, 2, 3},{ 4, 5, 6}};
int (*p)[3] = a;
#include <stdlib.h>
#include <stdio.h>
int main() {
int a[2][3] = {{1, 2, 3},{ 4, 5, 6}};
int (*p)[3] = a;
for (int i = 0;i < sizeof(a)/sizeof(*a);i++) {
for (int j = 0;j < sizeof(*a)/sizeof(**a);j++) {
printf("%d ", *(*(p+i)+j));
}
}
}
多级指针
没啥好说的
函数
函数的定义
数据类型 函数名 (【形式参数说明表】)
【形式参数说明表】:数据类型 形参名,数据类型 形参名,…
#include <stdlib.h>
#include <stdio.h>
int main(int argc,char *argv[]) {
exit(printf("Hello!\n"));//7
}
//echo $? 返回上一个命令的状态,0表示没有错误,其它任何值表明有错误
//agrc是计数器,来计算终端传了多少个参数过来;
//argv这个指针数组有多少元素,取决于argc是多少
//c语言中最好主调函数在被调函数下面,不然会有警告
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TXzWpZNh-1693146368199)(E:\Git_study\LinuxC\C\image-20230816205029075.png)]
函数的传参
- 值传递
- 地址传递
- 全局变量
函数的调用
- 嵌套
#include <stdlib.h>
#include <stdio.h>
int max(int a, int b, int c);//函数声明
int max(int a, int b, int c) {
int tmp = a > b ? a : b;
return tmp > c ? tmp : c;
}
int min(int a, int b, int c) {
int tmp = a < b ? a : b;
return tmp < c ? tmp : c;
}
int dist(int a, int b, int c) {
return max(a, b, c) - min(a, b, c);
}
int main() {
printf("%d\n", dist(8, 5, 10));
}
- 递归:一个函数直接或间接的嵌套自身
#include <stdio.h>
#include <stdlib.h>
int func(int n) {
if (n <= 0) {
return -1;
}
if (n == 1 || n == 2) {
return 1;
}
return func(n-1) + func(n-2);
}
int main() {
int n;
scanf("%d", &n);
printf("fib %d = %d", n, func(n));
exit(0);
}
#include <stdio.h>
#include <stdlib.h>
int func(int n) {
if (n < 0) {
return -1;
}
if (n == 0 || n == -1) {
return 1;
}
return n * func(n - 1);
}
int main() {
int n;
scanf("%d", &n);
printf("%d! = %d", n, func(n));
exit(0);
}
函数与数组
#include <stdlib.h>
#include <stdio.h>
// 注意这里的int *arr 的大小是8个字节 是一个普通的指针不是数组 所以一定要传大小
//形参时int p[] 等同与int *p都是指针
//在形参形式定义时,其实一个[] 就等价于*;二维也是这样
void printarr(int *arr, int size) {
for (int i = 0;i < size;i++) {
printf("%d ", *(arr+i));
}
printf("\n");
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
printarr(arr, sizeof(arr)/sizeof(*arr));
}
#include <stdlib.h>
#include <stdio.h>
void printarr(int *arr, int size) {
for (int i = 0;i < size;i++) {
printf("%d ", *(arr+i));
}
printf("\n");
}
void printarr2(int (*p)[3], int m , int n) {
for (int i = 0;i < m;i++) {
for (int j =0;j < n;j++) {
printf("%4d ", *(*(p+i)+j));
}
printf("\n");
}
}
int main() {
int arr[][3] = {1, 2, 3, 4, 5, 6};
printarr2(arr, 2, 3);
}
//今天遇到了一段代码对赋值运算符之后的表达式进行判断,由于之前没接触过类似的代码,特此记录。
if ((pTmpBuf = pBuf) != NULL)
结论: C/C++中 (A = B) 返回得到是赋值号(=)的左面的值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H3pHCy7Y-1693146368199)(E:\Git_study\LinuxC\C\image-20230819145110344.png)]
函数的指针
- 指针函数
// 类型 * 指针名 (形参);
//如: int * fun(int);
#include <stdio.h>
#include <stdlib.h>
#define M 2
#define N 3
int *findnum(int (*p)[N], int num) {
if (num > M - 1) {
return NULL;
}
return *(p + num);
}
int main() {
int arr[M][N] = {{1, 2, 3},{ 4, 5, 6}};
int *res = findnum(arr, 1);
for (int i = 0; i < N; i++) {
printf("%d ", *(res + i));
}
}
/*函数名是地址吗
我们经常把函数名当地址用,可实际上他不是地址,但是你可以把他理解成地址,心里清楚他不是就行。
对于test和&test你应该这样理解,test是函数的首地址,它的类型是void (),&test表示一个指向函数test这个对象的地址,它的类型是void (*)(),因此test和&test所代表的地址值是一样的,但类型不一样。test是一个函数,&test表达式的值是一个指针!*/
-
函数指针
类型 (*指针名)(形参)
如:int ( *p )(int);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oR439BUl-1693146368199)(E:\Git_study\LinuxC\C\image-20230819182222168.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iVDeSlGb-1693146368199)(E:\Git_study\LinuxC\C\image-20230819182303198.png)]
-
函数指针数组
类型 (*数组名[下标]) (形参)
如:int (* arr[N]) (int);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-satfjstC-1693146368200)(E:\Git_study\LinuxC\C\image-20230819184022607.png)]
指向指针函数的函数指针数组
int *(*funcp[N])(int);
实际例子
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);
构造类型
结构体
1 产生及意义
描述复杂的数据类型
2 类型描述
struct node_st{
type1 name1;
type2 name2;
...
};
3 嵌套定义
struct day {
int H;
int M;
int S;
};
struct student_st{
char *name;
struct day day;
};
4 定义变量(变量,数组,指针) 初始化以及成员引用
- 结构体 .
- 结构体指针 ->
//成员引用:变量名.成员名; 指针->成员名
// (*指针).成员名
struct A {
int i;
char c;
float f;
};
int main() {
// TYPE NAME = VALUE;
struct A a = {123, 'A', 2.22}; // 初始化
struct A a_ = { .c = 'A', .f = 2.22}; // 部分初始化
struct A *ap = &a_; // 部分初始化
//还可以定义结构体数组
printf("%d %c %.2f\n",a.i, a.c, a.f); // 成员引用
// 123 A 2.22
printf("%d %c %.2f\n",a_.i, a_.c, a_.f); // 成员引用
// 0 A 2.22
printf("%d %c %.2f\n",,(*ap).i, ap->c, ap->f); // 成员引用
// 0 A 2.22
}
占用内存空间大小
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jh6zStHz-1693146368200)(E:\Git_study\LinuxC\C\image-20230822212958596.png)]
addr % sizeof(type)
不能整除的话就要继续往下偏移
//结构体里的元素会对齐,不能整除的话就要继续往下偏移
#include <stdio.h>
#include <stdlib.h>
struct A {
int i;
char c;
float f;
};
// 可以使用下面的方法取消对齐 常用于网络通信
struct B {
int i;
char c;
float f;
}__attribute__((packed));
int main() {
struct A a;
struct B b;
printf("A = %ld\n", sizeof(a));
printf("B = %ld\n", sizeof(b));
}
//一般结构体传参给函数时,尽量采用地址传参,节省开销
//无名结构体或者匿名结构体
共用体
产生及意义
N选一 多个成员共用一块空间 取最大的成员的类型大小作为共用体的类型大小 ; 别名:联合体;
类型描述
union test_un{
int i;
float f;
double d;
char ch;
};
嵌套定义
同结构体 可以互相嵌套
定义变量 初始化以及成员引用
成员引用:
- u.成员名
- up->成员名
32位的无符号数的高16位和低16位相加
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
int main() {
uint32_t i = 0x11223344;
printf("%x\n", (i>>16)+(i&0xFFFF));//4466
}
//结构体变量和共用体变量都是正常变量,如果要用指针变量取地址得用取地址符&
//共用体和结构体很多地方都是大同小异
另一种写法
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
union {
struct {
uint16_t i;
uint16_t j;
}x;
uint32_t y;
}u;
int main() {
uint32_t i = 0x11223344;
printf("%x\n", (i>>16)+(i&0xFFFF));
u.y = 0x11223344;
printf("%x\n", u.x.i + u.x.j);
}
占用内存空间大小
函数传参(值,地址)
位域
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DlLwqg4a-1693146368200)(E:\Git_study\LinuxC\C\image-20230826131938301.png)]
union {
struct {
char a:1;
char b:2;//2位
char c:1;
}x;
char y;
}w;//该共用体一个char型一个字节大小
int main() {
w.y=6;
printf("%d\n", w.x.b);//-1
exit(0);
}
枚举
enum 标识符{
成员1,
...
};
enum dar {
MON = 1,//取了第一个值,后面的值会顺次增加
TUS, //2
WEB,
THR,
FRI =1,
SAT, //2
SUN,
};
int main() {
enum day a = FRI;
printf("%d\n", a);
}
enum status {
STATE_RUNNING = 1,
STATE_CANCELED,
STATE_OVER,
};
struct job {
int id;
int state;
time_t start, end;
};
int main() {
struct job_st job1;
switch(jobs.state) {
case STATE_RUNNING:
// TODO
break;
case STATE_CANCELED:
// TODO
break;
case STATE_OVER:
// TODO
break;
default:
abort();
}
}
动态内存管理
-
malloc
-
calloc
-
realloc
-
free
原则:谁申请谁释放,在同一模块成对申请释放,就像IO的使用一样
// void *malloc(size_t size);
// void free(void *ptr);
// void *calloc(size_t nmemb, size_t size);
// void *realloc(void *ptr, size_t size);
#include <stdlib.h>
#include <stdio.h>
int main() {
int *ip = malloc(sizeof(int));
if(ip==NULL){
printf("malloc() error!\n");
}
*ip = 1;
printf("%d\n", *ip);
free(ip);
}
动态数组
#include <stdlib.h>
#include <stdio.h>
int main() {
int *p;
int num = 5;
p = malloc(num * sizeof(int));
for (int i = 0;i < num;i++) {
scanf("%d", p+i);
}
for (int i = 0;i < num; i++) {
printf("%d ", *(p+i));
}
printf("\n");
exit(0);
}
内存申请与函数传值
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
void func1(void *p, size_t size) {
p = malloc(size);
if(p == NULL) {
return;
}
}
void func2(int **p, size_t size) {
if(*p == NULL) {
return;
}
*p = malloc(size);
}
void *func3(void *p, size_t size) {
if(p == NULL) {
return NULL;
}
p = malloc(size);
return p;
}
int main() {
int num = 100;
//p在指向NULL时,不能对NULL这块空间进行写操作
int *p = NULL;
func1(p, num); // 内存会泄露
func2(&p, num); // 传递二级指针
p = func3(p, num); // 将申请的内存返回
free(p);
exit(0);
}
free的理解
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
void func2(int **p, size_t size) {
if(*p == NULL) {
return;
}
*p = malloc(size);
}
int main() {
int num = 100;
int *p = NULL;
func2(&p, num); // 传递二级指针
printf("%p-->%d\n", *p);
free(p);
// p = NULL;//如果没重新分配空间,会报段错误(core dumped)
*p = 123;
printf("%p-->%d\n", *p); // 这个指针已经是野指针了
exit(0);
}
- free代表着变量p不再拥有原来指向内存空间的引用权限
- free后最好马上将指针置NULL
typedef
typedef type typename
typedef(type define):为已有的数据类型改名
//typedef 已有的数据类型 新名字;
typedef int INT
int main() {
INT i = 9;
}
typedef 和 define 的区别,一个别名一个是替换
#define IP int *
type int *IP;
int main() {
// #define
IP p, q;
int *p, q; // 一个int * 一个int
// typedef
IP p, q;
int *p, *q; // 两个int *
}
数组
typedef int ARR[6]; // int [6] 改名为 ARR
ARR a; // int a[6];
结构体
typedef struct {
int i;
float f;
}NODE, *NODEP;
//typedef int FUNC(int);--> int(int) FUNC;
//FUNC f; --> int f(int);
函数
typedef int *FUNC(int);
FUNC p; -->int *p(int);
指向指针函数
typedef int* (*FUNCP)(int);
//FUNCP p; --> int *(*p)(int);
Makefile
工程管理 依赖管理
- makefile(用户自定义 更高优先级)
- Makefile(默认)
mytool:main.o tool1.o tool2.o
gcc main.o tool1.o tool2.o -o mytool
main.o:main.c
gcc main.c -c -Wall -g -o main.o
tool1.o:tool1.c
gcc tool1.c -c -Wall -g -o tool1.o
tool2.o:tool2.c
gcc tool2.c -c -Wall -g -o tool2.o
clean:
rm *.o mytool -rf
OBJS=main.o tool1.o tool2.o
CC=gcc //默认就是gcc
mytool:$(OBJS) //替换语法
$(CC) $(OBJS) -o mytool
main.o:main.c
$(CC) main.c -c -Wall -g -o main.o
tool1.o:tool1.c
$(CC) tool1.c -c -Wall -g -o tool1.o
tool2.o:tool2.c
$(CC) tool2.c -c -Wall -g -o tool2.o
clean:
$(RM) $(OBJS) mytool -r
//rm -f
$^ 表示在上一句依赖关系中被依赖的所有文件
$@ 表示在上一句依赖关系中依赖项的目标文件
CFLAGS+=-Wall -g -c //GCC编译选项CFLAGS参数
OBJS=main.o tool1.o tool2.o
CC=gcc
mytool:$(OBJS)
$(CC) $^ -o $@
main.o:main.c
$(CC) $^ $(CFLAGS) -o $@
tool1.o:tool1.c
$(CC) $^ $(CFLAGS) -o $@
tool2.o:tool2.c
$(CC) $^ $(CFLAGS) -o $@
clean:
$(RM) $(OBJS) mytool -r
% 表示同一个名字
CFLAGS=-Wall -g -c
OBJS=main.o tool1.o tool2.o
CC=gcc
mytool:$(OBJS)
$(CC) $^ -o $@
//通配符
%.o:%.c
$(CC) $^ $(CFLAGS) -o $@
clean:
$(RM) $(OBJS) mytool -r
- **free代表着变量p不再拥有原来指向内存空间的引用权限**
- **free后最好马上将指针置NULL**
# typedef
`typedef type typename`
typedef(type define):为已有的数据类型改名
```c++
//typedef 已有的数据类型 新名字;
typedef int INT
int main() {
INT i = 9;
}
typedef 和 define 的区别,一个别名一个是替换
#define IP int *
type int *IP;
int main() {
// #define
IP p, q;
int *p, q; // 一个int * 一个int
// typedef
IP p, q;
int *p, *q; // 两个int *
}
数组
typedef int ARR[6]; // int [6] 改名为 ARR
ARR a; // int a[6];
结构体
typedef struct {
int i;
float f;
}NODE, *NODEP;
//typedef int FUNC(int);--> int(int) FUNC;
//FUNC f; --> int f(int);
函数
typedef int *FUNC(int);
FUNC p; -->int *p(int);
指向指针函数
typedef int* (*FUNCP)(int);
//FUNCP p; --> int *(*p)(int);
Makefile
工程管理 依赖管理
- makefile(用户自定义 更高优先级)
- Makefile(默认)
mytool:main.o tool1.o tool2.o
gcc main.o tool1.o tool2.o -o mytool
main.o:main.c
gcc main.c -c -Wall -g -o main.o
tool1.o:tool1.c
gcc tool1.c -c -Wall -g -o tool1.o
tool2.o:tool2.c
gcc tool2.c -c -Wall -g -o tool2.o
clean:
rm *.o mytool -rf
OBJS=main.o tool1.o tool2.o
CC=gcc //默认就是gcc
mytool:$(OBJS) //替换语法
$(CC) $(OBJS) -o mytool
main.o:main.c
$(CC) main.c -c -Wall -g -o main.o
tool1.o:tool1.c
$(CC) tool1.c -c -Wall -g -o tool1.o
tool2.o:tool2.c
$(CC) tool2.c -c -Wall -g -o tool2.o
clean:
$(RM) $(OBJS) mytool -r
//rm -f
$^ 表示在上一句依赖关系中被依赖的所有文件
$@ 表示在上一句依赖关系中依赖项的目标文件
CFLAGS+=-Wall -g -c //GCC编译选项CFLAGS参数
OBJS=main.o tool1.o tool2.o
CC=gcc
mytool:$(OBJS)
$(CC) $^ -o $@
main.o:main.c
$(CC) $^ $(CFLAGS) -o $@
tool1.o:tool1.c
$(CC) $^ $(CFLAGS) -o $@
tool2.o:tool2.c
$(CC) $^ $(CFLAGS) -o $@
clean:
$(RM) $(OBJS) mytool -r
% 表示同一个名字
CFLAGS=-Wall -g -c
OBJS=main.o tool1.o tool2.o
CC=gcc
mytool:$(OBJS)
$(CC) $^ -o $@
//通配符
%.o:%.c
$(CC) $^ $(CFLAGS) -o $@
clean:
$(RM) $(OBJS) mytool -r