文章目录
- 前言
- 基础知识
- main函数
- 防BUG
- 注释(重要)
- 关键字
- 标识符命名(驼峰命名)
- 常量类型
- 变量
- printf
- 1.输出不同类型数据
- 2.输出不同宽度数据
- 3.不同类型数据长度归类
- scanf函数
- 运算符
- sizeof(运算符,优先级2)
- 逗号运算符
- 关系运算符
- 逻辑运算符
- 三目运算符
- 强制类型转换
- 流程控制
- if语句
- switch case
- 循环结构
- for循环
- 跳转语句
- 嵌套循环
- 函数
- main函数(了解)
- 递归函数(了解)
- 进制
- exit函数
- 位运算符
- char类型转义字符
- 数组
- 数组名作为参数
- 二维数组
- 字符串
- 指针
- 二级指针
- 数组指针
- 指针字符串
- 函数指针
- 结构体
- 结构体成员的访问
- 结构体成员的初始化
- 结构体数组
- 结构体指针
- 结构体内存分配
- 结构体嵌套
- 共用体
- 枚举
- 其他杂乱知识点补充
- 预处理指令
- 宏定义
- 普通宏定义
- 带参宏定义
- 条件编译
- typedef关键字
- const关键字
- 内存管理
- malloc
- free
- calloc
- realloc
- 链表
- 文件操作(记录的比较潦草)
- STM32 常用数据类型
前言
在嵌入式开发的时候,发现由于没有系统学习过C语言时,导致编程时只会一些简单语法,既导致开发效率低,又导致程序结构很差,于是打算重新学习一下C语言。
摘抄自:c语言入门这一篇就够了-学习笔记(一万字)
基础知识
main函数
主函数(main)会由系统自动调用,其他的函数不会,一个程序只能有一个main函数。
main函数前面的int可以换成void或者不写(不会报错),甚至return 0;都可以省略。(因为产生多种C语言标准)
(当然还是要写最标准的)
嵌入式C语言:main.c文件中
int main(void)
{//初始化等操作while(1){//裸机程序循环执行}
}
防BUG
我喜欢的一个防BUG神器
━━━━━━神兽出没━━━━━━┏┓ ┏┓┏┛┻━━━━━━┛┻┓┃ ┃┃ ━ ┃┃ ┳┛ ┗┳ ┃┃ ┃┃ ┻ ┃┃ ┃┗━┓ ┏━┛Code is far away from bug with the animal protecting┃ ┃ 神兽保佑,代码无bug┃ ┃┃ ┗━━━┓┃ ┣┓┃ ┏━━┛┛┗┓┓┏━┳┓┏┛┃┫┫ ┃┫┫┗┻┛ ┗┻┛━━━━━━感觉萌萌哒━━━━━━
注释(重要)
简单列出我觉得有用的两种类型
//第一种:注释函数
/*** @brief printMap* @param map 需要打印的二维数组* @param row 二维数组的行数* @param col 二维数组的列数*/
void printMap(char map[6][7] , int row, int col)
{........//第二种:思路分析
/*R代表一个人#代表一堵墙
// 0123456####### // 0# # // 1#R ## # // 2# # # // 3## # // 4####### // 5分析:>1.保存地图(二维数组)>2.输出地图>3.操作R前进(控制小人行走)3.1.接收用户输入(scanf/getchar)w(向上走) s(向下走) a(向左走) d(向右走)3.2.判断用户的输入,控制小人行走3.2.1.替换二维数组中保存的数据(1.判断是否可以修改(如果不是#就可以修改)2.修改现有位置为空白3.修改下一步为R)3.3.输出修改后的二维数组4.判断用户是否走出出口
*/
关键字
图片来自:c语言入门这一篇就够了-学习笔记(一万字)
标识符命名(驼峰命名)
当变量名或函数名是由多个单词连接在一起,构成标识符时,第一个单词以小写字母开始,之后的单词的首字母大写。
例如: myFirstName
常量类型
科学计数法:例123000:1.23e5或1.23E5(E前后必须有数字,不能有空格,后面的必须是整数,前面的可以是小数,也可以是类似90这样的可以约的数字)
字符常量:单引号括起来的‘a’,单引号里面只能有一个字符,转义字符(特殊情况):’\n’(换行)、’\t’(跳格)
字符串常量:双引号括起来的“aba”、“a”,末尾会自动加一个字符‘\0’为结束标志
只有小数位:.3
单精度小数:0.5f或0.5F(6位小数)
双精度小数(默认):3.14(15位小数)
八进制:0123(0开头)
十六进制:0x开头
二进制:0b开头
变量
变量初始化:(不推荐的)
int a, b = 10; //部分初始化
int c, d, e;
c = d = e =0;
变量占用储存空间:
变量存储的过程:
根据定义变量时声明的类型和当前编译环境确定需要开辟多大存储空间
在内存中开辟一块存储空间,开辟时从内存地址大的开始开辟(内存寻址从大到小)
将数据保存到已经开辟好的对应内存空间中
printf
1.输出不同类型数据
在嵌入式中,经常将串口数据发送重定向为printf,确实方便很多
printf("a = %类型", a);
int b = -10;
printf("b = %u\n", b); // 429496786
//无符号int(2^32),所以输出的2^32-10
printf("b = %o\n", b); // 37777777766
printf("b = %x\n", b); // fffffff6float c = 6.6f;
double d = 3.1415926;
// 单、双精度浮点数(默认保留6位小数)
printf("c = %f\n", c); // 6.600000,f是单精度
printf("d = %lf\n", d); // 3.141593,lf是双精度,但默认都是6位,所以6位下用f也行,也可以%.15lf,测试%.15f也行
//一般情况下 printf是兼容f和lf的,所以一般用f就行,可能某些编译器(没遇到过)不兼容
//当然在scanf函数输入的时候还是要规范,但嵌入式里面没有double e = 10.10;
// 以指数形式输出单、双精度浮点数
printf("e = %e\n", e); // 1.010000e+001
printf("e = %E\n", e); // 1.010000E+001
2.输出不同宽度数据
定义输出宽度,在十进制下,实际位数多于指定宽度,则按照实际位数输出, 如果实际位数少于指定宽度则以空格补位。
对于小数,按照".宽度"的格式,用于定义输出的小数位数,多出来的用0补齐。(会四舍五入)
printf("a = %[宽度]类型", a);
printf("a = %.[位数]f", a);printf("d = %.0f\n", c); //四舍五入为整数
动态指定保留小数:
double a = 3.1415926;
printf("a = %.*f", 2, a); // 3.14
printf("a = %.4f", a); // 3.1415
对于小数来说:(注意整数部分也是算在有效数字里面的)
对于单精度数,使用%f格式符输出时,仅前6~7位是有效数字
对于双精度数,使用%lf格式符输出时,前15~16位是有效数字
float c = 5546.62222222222f;
printf("c = %.15f\n", c); // c = 5546.622070312500000
整数不同宽度:
int a = 1;printf("a =|%5d|\n", a); // | 1|int b = 1234567;printf("b =|%3d|\n", b); // |1234567|
不同标志:
int a = 1;
int b = -1;
// -号标志
printf("a =|%5d|\n", a); // | 1|
printf("a =|%-5d|\n", a);// |1 |
// +号标志
printf("a =|%+d|\n", a);// |+1|
printf("b =|%+d|\n", b);// |-1|
// 0标志
printf("a =|%5d|\n", a); // | 1|
printf("a =|%05d|\n", a); // |00001|
// 空格标志
printf("a =|% d|\n", a); // | 1|
printf("b =|% d|\n", b); // |-1|
// #号
int c = 10;
printf("c = %o\n", c); // 12
printf("c = %#o\n", c); // 012
printf("c = %x\n", c); // a
printf("c = %#x\n", c); // 0xa
3.不同类型数据长度归类
printf("a = %[长度]类型", a);
char a = 'a';
short int b = 123;
int c = 123;
long int d = 123;
long long int e = 123;
printf("a = %hhd\n", a); // 97
printf("b = %hd\n", b); // 123
printf("c = %d\n", c); // 123
printf("d = %ld\n", d); // 123
printf("e = %lld\n", e); // 123
数据类型如果不匹配,会警告,数据进行转换,如果没超范围没什么,超出范围数据就会错误
float:
1bit(符号位) 8bits(指数位) 23bits(尾数位)
float的指数范围为 -127 ~ +129
float的范围为-2^128 ~ +2^128,也即-3.40E+38 ~ +3.40E+38;
double:
1bit(符号位) 11bits(指数位) 52bits(尾数位)
double的指数范围为 -1023 ~ +1024
double的范围为-2^1024 ~ +2^1024,也即-1.79E+308 ~ +1.79E+308。
scanf函数
由于嵌入式里面没有这个,所以就简单记录一下:
scanf接收键盘数据,用法和printf类似,接收非字符和字符串类型时, 空格、Tab和回车会被忽略
int number;
int value;
// 可以输入 数字 空格 数字, 或者 数字 回车 数字
scanf("%d%d", &number, &value);
\n是scanf函数的结束符号,所以格式化字符串中不能出现\n
// 输入完毕之后按下回车无法结束输入
scanf("%d\n", &number);
系统输入时放在缓冲区内部的,输入缓冲区不为空的时候,scanf会一直从缓冲区中获取,例如:
#include <stdio.h>
int main(){int num1;int num2;char ch1;scanf("%d%c%d", &num1, &ch1, &num2);printf("num1 = %d, ch1 = %c, num2 = %d\n", num1, ch1, num2);char ch2;int num3;scanf("%c%d",&ch2, &num3);printf("ch2 = %c, num3 = %d\n", ch2, num3);
}
系统输入时放在缓冲区内部的,输入一次就把两次的函数scanf调用解决了。
可以利用下面函数清空缓冲区(所有平台有效)
setbuf(stdin, NULL);
向屏幕输出一个字符的putchar和从键盘获取一个字符的getchar
char ch = 'a';
putchar(ch); // 输出achar ch;
ch = getchar();// 获取一个字符
运算符
在加、减、乘、除这些双目运算符中,如果参与运算的两个操作数其中一个是浮点数,那么结果一定是浮点数。
求余运算符中,两个数必须都是整数,运算结果正负取决于被除数,与除数无关。
result = 10 % -3; //1
result = -10 % 3;//-1
还有一些复合运算符/=、*=、%=、+=、-=,具有右结合性
自增和自减都是只能用于单个变量,不能用于常量和表达式,且在企业开发中,尽量单独出现,不和其他运算符混用。因为不同编译器下是不一样的自增或自减后如何运算。
sizeof(运算符,优先级2)
sizeof用来计算一个变量或常量、数据类型所占的内存字节数
有几种写法:
sizeof( 变量\常量 );
sizeof 变量\常量;
sizeof( 数据类型);
sizeof(10);
char c = 'a'; sizeof(c);
char c = 'a'; sizeof c;
sizeof(float); //数据类型不能省略括号
sizeof是运算符
int a = 10;
double b = 3.14;
double res = sizeof a+b; //先计算sizeof a:4,再加b,最后为7.14
逗号运算符
把多个表达式连接起来组成一个表达式
逗号运算符会从左至右依次取出每个表达式的值, 最后整个逗号表达式的值等于最后一个表达式的值
int a = 10, b = 20, c;//这不是c = (a + 1, b + 1); //21
关系运算符
C语言中,任何非零值都为“真”
避免浮点数的==判断,因为float和double有精度问题
float a = 0.1;
float b = a * 10 + 0.00000000001; //超精度了
double c = 1.0 + + 0.00000000001; //b和c是不相等的double a = 0.1;
double c = 0.1; //a和c是相等的float a = 0.1;
double c = 0.1; //a和c是不相等的
逻辑运算符
A && B:A为假,B不执行
A || B:A为真,B不执行
三目运算符
格式: 表达式1?表达式2(结果A):表达式3(结果B)
int res = a>b?a:c>d?c:d; //先计算a>b?a:(c>d?c:d); 具有右结合性
强制类型转换
// 将double转换为int
int a = (int)10.5;// 当前表达式用1.0占用8个字节, 2占用4个字节
// 系统会自动对占用内存较少的类型做一个“自动类型提升”的操作, 先将其转换为当前算数表达式中占用内存高的类型, 然后再参与运算
double b = 1.0 / 2; //2转换为double// 赋值时左边是什么类型,就会自动将右边转换为什么类型再保存
int a = 10.6;// 结果为0, 因为参与运算的都是整型
double a = (double)(1 / 2);
// 结果为0.5, 因为1被强制转换为了double类型, 2也会被自动提升为double类型
double b = (double)1 / 2;//类型转换不影响与原变量的值
流程控制
if语句
当我们省略大括号的时候,后面就不能定义变量了。
一般if里面的判断语句中,常量放在前面,if(10==a),这样漏写=的时候会报错(好习惯)
switch case
switch(表达式)
{case 常量表达式1:....break;case 常量表达式2:....break;case 常量表达式3:....break;default:.....break;
}
switch的条件表达式必须是整型,或者是可以被提升为整型的值(char类型、short类型的值)。
case的值也只能是常量,并且还必须是整型, 或者可以被提升为整型的值(char、short),并且case后面不能一样,同样case后面要定义变量也要加大括号。警惕无break导致的穿透问题。
switch中default可以省略,switch中default放到哪都会等到所有case都不匹配才会执行(穿透问题除外)
switch(1.1)// 报错case 'a'://可以,相当case 97:case num: // 报错case 4.0: // 报错case 1: // 报错,两个case后面不能一样.....break;
case 1: // 报错.....break;
循环结构
while和do while,如果while省略了大括号, 那么后面不能定义变量
while(循环条件)
{
}//不管while中的条件是否成立, 都会执行一次"循环体"
do{}while(循环条件)
for循环
while能做的for都能做, 所以企业开发中能用for就用for, 因为for更为灵活,for更节约内存空间
for(初始化表达式;循环条件表达式;循环后的操作表达式) {循环体中的语句;
}//最简单的死循环
for(;;);//for循环里面初始化可以放在里面也可以放在外面
for (int count = 0; count < 10; count++) {int count = 0;
for (; count < 10; count++) {
跳转语句
- break
跳出switch或者各种循环
while(...)
{............... if(...){......break;//立刻跳出while}
}
//break离开应用范围,存在是没有意义的
if(1) {break; // 会报错
}
在多层循环中,一个break语句只向外跳一层:
while(1) {while(2) {break;// 只对while2有效, 不会影响while1}
}
- return
结束当前函数,将结果返回给调用者 - continue
结束本轮循环进入下一轮:
while(1)
{.....if(...){.....continue; //跳过本轮while循环进入下一轮}
}//不能离开应用范围
if(1) {continue; // 会报错
}
- goto
破坏程序结构,不利于维护阅读(但是该用还是要用)
goto 语句,仅能在本函数内实现跳转,不能实现跨函数跳转(短跳转)。但是他在跳出多重循环的时候效率还是蛮高的
//例1:
// loop:是定义的标记
loop:if(num < 10){printf("num = %d\n", num);num++;// goto loop代表跳转到标记的位置goto loop;}
//例2:while (1) {while(2){goto lnj;}}lnj:printf("跳过了所有循环");
嵌套循环
应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数
(例如多个for循环嵌套)
函数
返回值类型 函数名(参数类型 形式参数1,参数类型 形式参数2,…) {函数体;返回值;
}
函数名后面小括号()中定义的变量称为形式参数,简称形参,形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。形参只有在函数内部有效,函数调用结束返回主调函数后则不能再使用该形参变量。
在调用函数时, 传入的值称为实际参数,简称实参形参实参类型不一致, 会自动转换为形参类型,实参和形参之间只是值传递,修改形参的值并不影响到实参函数可以没有形参。
如果没有写返回值类型,默认是int:
max(int number1, int number2) {// 形式参数return number1 > number2 ? number1 : number2;
}
一个函数内部可以多次使用return语句,但是return语句后面的代码就不再被执行。
函数声明很熟悉了,有几个注意点:
函数的实现不能重复, 而函数的声明可以重复。
函数声明可以写在函数外面,也可以写在函数里面, 只要在调用之前被声明即可(比如在main函数里面声明后再调用。不过感觉有些乱这样)
如果被调函数的返回值是整型时,可以不对被调函数作说明,而直接调用(真的,但是会有警告,还是不知道为好,啥用没有)
main函数(了解)
关于main函数(了解):
int main(int argc,const char * argv[])
{}
int argc :
系统在启动程序时调用main函数时传递给argv的值的个数
const char * argv[] :
系统在启动程序时传入的的值, 默认情况下系统只会传入一个值, 这个值就是main函数执行文件的路径
也可以通过命令行或项目设置传入其它参数
递归函数(了解)
自己嵌套自己,能用循环实现的功能,用递归都可以实现,但代码理解难度大,内存消耗大(易导致栈溢出), 所以考虑到代码理解难度和内存消耗问题, 在企业开发中一般能用循环都不会使用递归。
进制
二进制:0b…
八进制:0…
十六进制:0x…
十进制:正常
exit函数
exit(0) 表示程序正常退出
exit⑴/exit(-1)表示程序异常退出。
只要一调用,整个程序都会立马结束
位运算符
位运算只用于所有的整型!!(char,short,int,long int,long long,unsigned char,unsigned short…),浮点值均不适用!!!
可以对多位数操作:
& 按位与
| 按位或
^ 按位异或
~ 按位取反
其中~有些特殊,相当单目运算,例如:(怪怪的)
~9 -> -10
0000 0000 0000 0000 0000 1001 // 取反前 9的补码
1111 1111 1111 1111 1111 0110 // 取反后// 根据负数补码得出结果
1111 1111 1111 1111 1111 0110 // 补码=反码加1(计算机内部运算是补码形式)
1111 1111 1111 1111 1111 0101 // 反码=负数的源码除了第一位符号位,剩下的全部取反
1000 0000 0000 0000 0000 1010 // 源码 == -10//但是可以应用在其他方面
char a=0x89; //即a=1000 1001
char ch=~a; //则ch=0111 0110,但a的值仍为1000 1001
char类型转义字符
数组
数组初始化:(没有初始化,数值是随机的,不一定是0)
int ages[3] = {4, 6, 9}; //普通初始化int nums[5] = {[4] = 3,[1] = 2}; //部分初始化int nums[3]; //先定义后初始化
nums[0] = 1;
nums[1] = 2;
nums[2] = 3;
数组长度计算:
int ages[4] = {19, 22, 33, 13};
int length = sizeof(ages)/sizeof(int); //4
数组名指向的是整个数据存储空间最小的地址,即[0]的地址。
定义数组的时候, []里面可以写整型常量或者常量表达式
int ages4['A'] = {19, 22, 33}; //奇奇怪怪,会有这样写的场景吗
printf("ages4[0] = %d\n", ages4[0]);int ages5[5 + 5] = {19, 22, 33};
printf("ages5[0] = %d\n", ages5[0]);int ages5['A' + 5] = {19, 22, 33};
printf("ages5[0] = %d\n", ages5[0]);//错误的写法
// 没有指定元素个数,错误
int a[];// []中不能放变量
int number = 10;
int ages[number]; // 老版本的C语言规范不支持int number = 10;
int ages2[number] = {19, 22, 33} // 直接报错// 只能在定义数组的时候进行一次性(全部赋值)的初始化
int ages3[5];
ages10 = {19, 22, 33};//不能这样赋值,只能单个赋值
数组元素作为实参,还是值传递
void change(int val)// int val = number
{val = 55;
}
change(ages[0]);
数组名作为参数
数组名作为参数,是地址传递:(数组名代表了该数组在内存中的起始地址,实参数组名将该数组的起始地址传递给形参数组,两个数组共享一段内存单元, 系统不再为形参数组分配存储单元,两个数组共享一段内存单元, 所以形参数组修改时,实参数组也同时被修改了)
void change2(int array[3])// int array = 0ffd1
{array[0] = 88;
}int ages[3] = {1, 5, 8};change(ages);printf("ages[0] = %d", ages[0]);// 88
在函数形参表中,允许不给出形参数组的长度:
void change(int array[])
{array[0] = 88;
}
形参数组和实参数组的类型必须一致,否则将引起错误:
当数组名作为函数参数时, 因为自动转换为了指针类型,所以在函数中无法动态计算数组的元素个数:
void printArray(int array[])
{printf("printArray size = %lu\n", sizeof(array)); // 8int length = sizeof(array)/ sizeof(int); // 2printf("length = %d", length);
}
二维数组
//分段赋值
int a[2][3]={ {80,75,92}, {61,65,71}};
//按行连续赋值
int a[2][3]={ 80,75,92,61,65,71};//省略第一维长度
int a[][3]={{1,2,3},{4,5,6}};
int a[][3]={1,2,3,4,5,6};int a[2][] = {1, 2, 3, 4, 5, 6}; // 错误写法
//这样二维数组的列数不确定,可以是任意个//指定元素初始化
int a[2][3]={[1][2]=10};
int a[2][3]={[1]={1,2,3}}
值传递和地址传递:(看的是函数形参的类型,如果是数组就是地址传递)
void change(char ch){ch = 'n';
}change(cs[0][0]);//值传递,不改变原数组的值void change(char ch[]){ch[0] = 'n';
}change(cs[0]);//地址传递,改变cs[0][0]为’n‘void change(char ch[][3]){ch[0][0] = 'n';
}
char cs[2][3] = {{'a', 'b', 'c'},{'d', 'e', 'f'}};
change(cs); //地址传递,改变cs[0][0]为’n‘void test(char cs[2][]) // 错误写法
{}
二维数组作为函数参数,在被调函数中不能获得其有多少行,可以计算出二维数组有多少列
void test(char cs[2][3])
{size_t col = sizeof(cs[0]); // 输出3printf("col = %zd\n", col);
}
字符串
char name[9] = "jhj"; //在内存中以“\0”结束, \0ASCII码值是0
char name[] = "c\0ool"; //"中间不能包含\0", 因为\0是字符串的结束标志,这个字符串就是“c”
char name3[9] = {'j','n','j'}; //其他元素默认是0
字符串输出:
char chs[] = "jhj";
printf("%s\n", chs); //根据传入的地址逐个取出输出,直到遇到\0
当定义一个char xxx[10]的时候,最多存放9个字符,留一个给\0。
用scanf函数输入字符串时,字符串中不能含有空格。
C语言常有函数:puts、gets(输出和输入)
sizeof判断字符串长度,结束符\0也是算的
strlen判断字符串长度,结束符\0不算
字符串连接函数:strcat(string catenate),格式: strcat(字符数组名1,字符数组名2)
将两个字符串连接在一起,并且删除字符串1的结束符\0
字符串拷贝函数:strcpy,格式: strcpy(字符数组名1,字符数组名2),把字符数组2中的字符串拷贝到字符数组1中。串结束标志“\0”也一同拷贝
字符串比较函数:strcmp,格式: strcmp(字符数组名1,字符数组名2)
按照ASCII码顺序比较两个数组中的字符串,并由函数返回值返回比较结果。(顺序比较,一旦遇到不同就停下比较了)
字符串1=字符串2,返回值=0;
字符串1>字符串2,返回值1;
字符串1<字符串2,返回值-1。
char oldStr[100] = "asd";
char newStr[50] = "asaaa";
printf("%d", strcmp(oldStr, newStr)); //输出结果:-1
char oldStr[100] = "1";
char newStr[50] = "1";
printf("%d", strcmp(oldStr, newStr)); //输出结果:0
char oldStr[100] = "asd";
char newStr[50] = "asd";
printf("%d", strcmp(oldStr, newStr)); //输出结果:0
字符串数组:
char names[2][10] = { {'j','h','j','\0'}, {'w','j','\0'} };
char names2[2][10] = { {"wj"}, {"jhj"} };
char names3[2][10] = { "wj", "jhj" };
指针
有内存地址和存储,地址是编号,存储空间是放数据的,每个内存都有对应的地址。
指针变量就是存放其他变量的地址。格式:指针指向数据的类型 * 指针变量名
*表示这是一个指针变量
char ch = 'a';
char *p; // 一个用于指向字符型变量的指针
p = &ch;
int num = 666;
int *q; // 一个用于指向整型变量的指针
q = # int *p=NULL; // 定义指针变量int *p;
*p=&a; //错误写法 ,前面不能加*
p = 250; // 错误写法
多个指针可以指向同一地址,指针的指向可以改变,指针没有初始化里面就是一个垃圾值,称为野指针,可能会导致程序崩溃。
取地址:&变量名 (取得是其他变量的地址)
*只是用来说明这是一个指针变量,在不是定义的好时候,是一个操作符,表示访问指针的指向的存储
int a = 5;
int *p = &a;
printf("a = %d", *p); // 访问指针变量p的指向的空间的数据
关于占用空间的问题,一个int占4个字节,一个char占1个字节,一个double占8个字节
所以对于指针变量来说,会自动根据指针变量前面的类型名,判断要访问多少个字节的存储空间
二级指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。也称为“二级指针”
char c = 'a';
char *cp;
cp = &c;
char **cp2;
cp2 = &cp;
printf("c = %c", **cp2);
数组指针
指针变量保存数组元素的地址
int a={1,2,3,4,5,6}int *p;
p=a;//a是数据首元素的地址int *p=a; //等价int *p=&a[0]; //等价
数组名a不代表整个数组,只代表数组首元素的地址。
当指针变量指向数组的第一个数据的时候,允许进行的运算:
+整数、+=整数:p+1
-整数、-=整数:p-1
++:p++
–:p- -
加法指向数据后面的元素的地址
减法向前指向
访问数据的时候,可以a[1]下标访问或者*(p+1)这样指针变量形式访问。
数组名固定指向数组的第一个数据的地址,它是不能运算操作的,例如:a++;(错误的)
指针字符串
char string[]=”I love wj!”; //字符串名指向的也是第一个元素的地址,即string[0]的地址char *str = "abc"// 数组名保存的是数组第0个元素的地址, 指针也可以保存第0个元素的地址
char *str = "wj"; //str是一个指针,和上面的string一样,字符串指针,名字指向的是第一个元素的地址,
for(int i = 0; i < strlen(str);i++) //就像strlen(string)
{printf("%c-", *(str+i)); // 输出结果:w-j
}
不能修改字符串内容:(测试一下)
//用字符数组来保存的字符串是保存栈里的,保存栈里面东西是可读可写,所有可以修改字符串中的的字符
//使用字符指针来保存字符串,它保存的是字符串常量地址,常量区是只读的,所以我们不可以修改字符串中的字符//但是我实际测试两种都能改
char *str = "wj";
*(str+2) = 'y'; // 可以char string[]= "wj";
string[1] = 'y'; // 可以printf("%s",string)// 错误的原因是:str是一个野指针,他并没有指向某一块内存空间
// 所以不允许这样写如果给str分配内存空间是可以这样用
char *str;
scanf("%s", str);
函数指针
函数在内存中也占据部分存储空间,也是有起始地址的,因此可以用指针指向一个函数
格式:返回值类型 (*指针变量名)(形参1, 形参2, …);
int sum(int a,int b)
{return a+b;
}
int main()
{int (*p)(int,int);p=sum;printf("%d",(*p)(1,2)); //测试p(1,2)也是可以的
}
有一些格式注意点,函数名称使用小括号括起来并在前面加上*
应用在一些调用函数或者将函数作为参数在函数间传递,这里的*当作是一种表示符号
结构体
和数组一样是构造类型,结构体是相当用来保存一组不同数据类型的数组
定义结构体类型,制定好要存储的类型
struct 结构体名{类型名1 成员名1;类型名2 成员名2;……类型名n 成员名n;};
定义结构体变量
格式: struct 结构体名 结构体变量名;
//先定义结构体类型,再定义变量
struct Student {char *name;int age;};
struct Student stu;//定义结构体类型的同时定义结构体变量
struct Student {char *name;int age;
} stu;//匿名结构体定义,定义变量,只能定义这一次,无法复用
struct {char *name;int age;
} stu;
结构体成员的访问
格式:结构体变量名.成员名
struct Student {char *name;int age;};struct Student stu;// 访问stu的age成员stu.age = 27;printf("age = %d", stu.age);
结构体成员的初始化
//定义按顺序初始化
struct Student {char *name;int age;};
struct Student stu = {“wj", 27};//定义的同时,调出内部成员初始化
struct Student stu = {.age = 35, .name = “wj"};//定义后再逐个初始化
stu.name = "wj";
stu.age = 35;//定义后,一次性初始化
stu2 = (struct Student){"wj", 35};
结构体数组
数组的元素全是结构体
格式:struct 结构体类型名称 数组名称[元素个数]
struct Student {char *name;int age;
};
struct Student stu[2];
初始化:
//定义的同时初始化
struct Student {char *name;int age;
};
struct Student stu[2] = {{"jhj", 35},{"wj", 18}}; //先定义之后再初始化
struct Student {char *name;int age;
};
struct Student stu[2];
stu[0] = {"wj", 35};
stu[1] = {"jhj", 18};
结构体指针
格式: struct 结构名 *结构指针变量名
// 定义一个结构体类型struct Student {char *name;int age;};// 定义一个结构体变量struct Student stu = {"wj", 18};// 定义一个指向结构体的指针变量struct Student *p;
// 指向结构体变量stup = &stu;// 可以用3种方式访问结构体的成员// 方式1:结构体变量名.成员名printf("name=%s, age = %d \n", stu.name, stu.age);// 方式2:(*指针变量名).成员名printf("name=%s, age = %d \n", (*p).name, (*p).age);// 方式3:指针变量名->成员名printf("name=%s, age = %d \n", p->name, p->age);
(*结构指针变量).成员名 (括号不能省略,成员符“.”的优先级高于“ * ”)
结构指针变量->成员名(常用)
结构体内存分配
给结构体变量和普通的开辟空间一样,会从内存地址大的位置开始开辟
结构体成员则从占用内存地址小的开始
内存对齐:是占用内存最大成员的整数倍
按照最大成员进行申请,然后按顺序分配内存
struct Person{int age; // 4char ch; // 1double score; // 8};struct Person p;printf("sizeof = %i\n", sizeof(p)); // 16
//按照最大的double类型的8字节分配,先第一个8字节,给int4,给char1,之后还剩3不够了,再申请8字节给doublestruct Person{int age; // 4double score; // 8char ch; // 1};struct Person p;printf("sizeof = %i\n", sizeof(p)); // 24
//同理,申请8字节给 int 4,之后还剩4不够给double,再申请8给它,之后再申请8给char 1个字节。
结构体嵌套
struct Date{int month;int day;int year;
}
struct stu{int num;char *name;char sex;struct Date birthday;Float score;
}
注意不能嵌套自己类型的变量,但可以嵌套指向自己这种类型的指针
struct office{int chair;int computer;struct office *of1;
} ; //感觉就像变成了链表//结构体嵌套:访问stu.birthday.year = 1986; //结构体变量名.结构体变量名.结构体成员
结构体变量之间的操作,作为函数参数等都是值传递,当然不是指作为全局变量直接操作
int main()
{struct Person p1 = {"lnj", 35};printf("p1.name = %s\n", p1.name); // lnjtest(p1);printf("p1.name = %s\n", p1.name); // lnjreturn 0;
}
void test(struct Person per){per.name = "zs";
}
共用体
共用体每一个成员都共用一块存储空间,共用体在使用之前必须先定义共用体类型, 再定义共用体变量
定义类型:
union 共用体名{数据类型 属性名称;数据类型 属性名称;... ....
};
定义变量:
union 共用体名 共用体变量名称;
由于所有属性共享同一块内存空间, 所以只要其中一个属性发生了改变, 其它的属性都会受到影响
所以当里面的成员ch=‘a’的时候,age的值就是97(ASIIC)
union Test{int age;char ch;};union Test t;printf("sizeof(p) = %i\n", sizeof(t)); //按int来,是4t.age = 33;printf("t.age = %i\n", t.age); // 33t.ch = 'a';printf("t.ch = %c\n", t.ch); // aprintf("t.age = %i\n", t.age); // 97
(1)通信中的数据包会用到共用体,因为不知道对方会发送什么样的数据包过来,用共用体的话就简单了,定义几种格式的包,收到包之后就可以根据包的格式取出数据。
(2)节约内存。如果有2个很长的数据结构,但不会同时使用,比如一个表示老师,一个表示学生,要统计老师和学生的情况,用结构体就比较浪费内存,这时就可以考虑用共用体来设计。
(3)某些应用需要大量的临时变量,这些变量类型不同,而且会随时更换。而你的堆栈空间有限,不能同时分配那么多临时变量。这时可以使用共用体让这些变量共享同一个内存空间,这些临时变量不用长期保存,用完即丢,和寄存器差不多,不用维护。
枚举
有些变量的取值只能再一个范围内,在“枚举”类型的定义中列举出所有可能的取值, 被说明为该“枚举”类型的变量取值不能超过定义的范围。
enum 枚举名 {枚举元素1,枚举元素2,……
};// 表示一年四季
enum Season {Spring,Summer,Autumn,Winter
};
定义方式和结构体类似:
//先定义类型再定义变量
enum Season {Spring,Summer,Autumn,Winter
};
enum Season s;//定义类型的同时定义变量
enum Season {Spring,Summer,Autumn,Winter
} s;//定义类型省略类型名直接定义变量
enum {Spring,Summer,Autumn,Winter
} s;
使用方式:C语言编译器会将枚举元素(spring、summer等)作为整型常量处理,称为枚举常量。
枚举元素的值取决于定义时各枚举元素排列的先后顺序。默认情况下,第一个枚举元素的值为0,第二个为1,依次顺序加1。
也可以在定义枚举类型时改变枚举元素的值
enum Season {Spring, //0Summer, //1Autumn, //2Winter //3
} s;
s = Spring; // 等价于 s = 0;
s = 3; // 等价于 s = winter;
printf("%d", s); //3enum Season { //当然也可以自己把全部都重新赋值,如果没有手动赋值,就在前面一个赋值的量自加Spring = 9,Summer = 5,Autumn, //6Winter //7
};
// 也就是说spring的值为9,summer的值为10,autumn的值为11,winter的值为12
其他杂乱知识点补充
局部变量的存储位置再内存的堆栈中
全局变量存储再静态存储区中
auto和register都用来修饰局部变量
auto int num; // 等价于 int num;(默认都是auto,用完销毁,随用随开)
register int num; //内存中变量提升到CPU寄存器中存储, 这样访问速度会更快(实际可能编译器自动优化为auto)
static除了常用静态变量,还可以用来修饰全局变量
static int num; // 这样num将不能在其他文件中共享(嵌入式C有时常跨到main.c文件访问其他文件的全局变量)
//其他文件中也可以定义num了,但两个是不一样的,不共享的
相对的extern常用的就是告诉编译器,这个变量在其他文件中定义的
extern int num;
static修饰函数也是类似的作用(在定义和声明的时候都要加),只能在本文件中使用
static int sum(int num1,int num2); //声明static int sum(int num1,int num2) //定义
{return num1 + num2;
}
extern修饰函数就是可以在外部文件使用,但是一般默认都是这样的,省略了extern
预处理指令
在对源程序进行编译之前,会先对一些特殊的预处理指令作解释,#include这个东西
结尾不需要加分号
可以出现在程序的任何位置,作用范围是出现的位置到文件尾,通常功能是宏定义、文件包含、条件编译
宏定义
普通宏定义
格式:#define 标识符 字符串
以“#”开头的为预处理命令,“define”为宏定义命令,“标识符”为所定义的宏名,“字符串”可以是常数、表达式、格式串等。
宏名一般用大写字母,以便与变量名区别开来,但用小写也没有语法错误。
对程序中用双引号扩起来的字符串内的字符,不会进行宏的替换操作
#define R 10
char *s = "Radio"; // 在第1行定义了一个叫R的宏,但是第4行中"Radio"里面的'R'并不会被替换成10
可以用#undef终止宏定义的作用域
#define PI 3.14
..............
#undef PI
定义一个宏时可以引用已经定义的宏名
#define R 3.0
#define PI 3.14
#define L 2*PI*R //但是要注意,你这里最好要加括号,否则后续程序中替换后可能出现运算符优先级的问题
用宏定义表示数据类型,使书写方便
#define String char *
String str = "This is a string!";
带参宏定义
有点像函数的用法:
格式:#define 宏名(形参表) 字符串
// 第1行中定义了一个带有2个参数的宏average,
#define average(a, b) (a+b)/2
// 会被替换成:int a = (10 + 4)/2;,
int a = average(10, 4);
宏名和参数列表之间不能有空格,否则空格后面的所有字符串都作为替换的字符串.
#define average (a, b) (a+b)/2
int a = average(10, 4);
//注意第1行的宏定义,宏名average跟(a, b)之间是有空格的,于是:
//int a = (a, b) (a+b)/2(10, 4);
//这个肯定是编译不通过的
一般宏定义,要用括号括住,不管是整体(计算结果)还是参数部分,否则:
// 下面定义一个宏D(a),作用是返回a的2倍数值:
#define D(a) 2*a
// 如果定义宏的时候不用小括号括住参数// 将被替换成int b = 2*3+4;,输出结果10,如果定义宏的时候用小括号括住参数,把上面的第3行改成:#define D(a) 2*(a),注意右边的a是有括号的,第7行将被替换成int b = 2*(3+4);,输出结果14
int b = D(3+4);//最好写成这样
#define D(a) (2*(a))//否则 D(1)/D(2) 就变成2*1/2*2,而不是(2*1)/(2*2)
条件编译
程序的其中一部分代码只有在满足一定条件时才进行编译,否则不参与编译(只有参与编译的代码最终才能被执行),这就是条件编译。(嵌入式常把调试用的printf啥的设置个条件编译,当然也有运行不同方案时采用这个方法)。
使用if else这样也能实现,但是结构不清晰,而且全部都会编译,也不利于阅读维护。
#if 常量表达式..code1...
#else..code2...
#endif//例子
#define SCORE 67
#if SCORE > 90printf("优秀\n");
#elseprintf("不及格\n");
#endif#define SCORE 67
#if SCORE > 90printf("优秀\n");
#elif SCORE > 60printf("良好\n");
#elseprintf("不及格\n");
#endif#define SCORE 1
#if SCORE printf("优秀\n");
#elseprintf("不及格\n");
#endif
typedef关键字
用户自己定义类型说明符
格式: typedef 原类型名 新类型名;
typedef int INTEGER
INTEGER a; // 等价于 int a;//在别名的基础上再起别名
typedef int Integer;
typedef Integer MyInteger;
用typedef定义数组、指针、结构等类型将带来很大的方便
typedef char NAME[20]; // 表示NAME是字符数组类型,数组长度为20。然后可用NAME 说明变量,
NAME a; // 等价于 char a[20];
用来应用在结构体上也能让代码更简洁,可读性增强
//第一种
struct Person{int age;char *name;
};
typedef struct Person PersonType;//第二种
typedef struct Person{int age;char *name;
} PersonType;//第三种
typedef struct {int age;char *name;
} PersonType;
应用在枚举:
//第一种
enum Sex{SexMan,SexWoman,SexOther
};
typedef enum Sex SexType;//第二种
typedef enum Sex{SexMan,SexWoman,SexOther
} SexType;//第三种
typedef enum{SexMan,SexWoman,SexOther
} SexType;
应用在指针:
指向结构体的指针
// 定义一个结构体并起别名
typedef struct {float x;float y;
} Point;
// 起别名
typedef Point *PP;
指向函数的指针:
// 定义一个sum函数,计算a跟b的和int sum(int a, int b) {...........................}typedef int (*MySum)(int, int);
// 定义一个指向sum函数的指针变量pMySum p = sum;
宏定义在编译预处理阶段进行,比函数具有更高的执行效率。
typedef是在编译时处理的,相比宏定义不是作简单的代换,而是对类型说明符重新命名
typedef char *String1; // 给char *起了个别名String1
#define String2 char * // 定义了宏String2
// 由于String1就是char *,所以上面的两行代码等于:char *str1;char *str2;
String1 str1, str2;
// 宏定义只是简单替换, 所以相当于char *str3, str4;*号只对最近的一个有效, 所以相当于 char *str3; char str4;
String2 str3, str4;
const关键字
使用const修饰变量则可以让变量的值不能改变
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表 中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
const int Max=100;
int const Max=100;void f(const int i) { i=10;//error! }// const对于基本数据类型, 无论写在左边还是右边, 变量中的值不能改变
const int a = 5;
// a = 666; // 直接修改会报错
// 偷梁换柱, 利用指针指向变量
int *p;
p = &a;
// 利用指针间接修改变量中的值,所以要注意这些指针的问题
*p = 10;//修饰指针
// 先看“*”的位置
// 如果const 在 *的左侧 表示值不能修改,但是指向可以改。
// 如果const 在 *的右侧 表示指向不能改,但是值可以改
// 如果在“*”的两侧都有const 标识指向和值都不能改。
const int *A; //const修饰指针,A指向的地址可以变,*A不能变
int const *A; //A指向的地址可以变,*A不能变
int *const A; //const修饰指针A, A指向的地址不可以变,*A能变
const int *const A;//A指向的地址不可以变,*A不能变
内存管理
栈内存存放的是任意类型的变量,随用随开,用完即销,栈的最大尺寸固定,超出则引起栈溢出
int ages[10240*10240]; // 程序会崩溃, 栈溢出
堆内存可以存放任意类型的数据,但需要自己申请与释放,堆大小,想像中的无穷大,但实际使用中,受限于实际内存的大小和内存是否连续性。
int *p = (int *)malloc(10240 * 1024); // 不一定会崩溃 ,10240*1024是申请多大的字节
int *p1 = malloc(4); //申请4字节的大小
malloc
#include <stdlib.h>//malloc,第一个参数: 需要申请多少个字节空间,返回值类型: void *
int *p = (int *)malloc(sizeof(int));
printf("p = %i\n", *p); // 保存垃圾数据
//第一个参数: 需要初始化的内存地址,第二个参数: 需要初始化的值,第三个参数: 需要初始化对少个字节
memset(p, 0, sizeof(int)); // 对申请的内存空间进行初始化,或者*p=0;
printf("p = %i\n", *p); // 初始化为0
free
malloc申请的存储空间一定要用free释放,他们成对出现
// 1.申请4个字节存储空间
int *p = (int *)malloc(sizeof(int));
// 2.初始化4个字节存储空间为0
memset(p, 0, sizeof(int));
// 3.释放申请的存储空间
free(p);
calloc
/*
// 1.申请3块4个字节存储空间
int *p = (int *)malloc(sizeof(int) * 3);
// 2.使用申请好的3块存储空间
p[0] = 1;
p[1] = 3;
p[2] = 5;
printf("p[0] = %i\n", p[0]);
printf("p[1] = %i\n", p[1]);
printf("p[2] = %i\n", p[2]);
// 3.释放空间
free(p);
*/// 1.申请3块4个字节存储空间
int *p = calloc(3, sizeof(int));
// 2.使用申请好的3块存储空间
p[0] = 1;
p[1] = 3;
p[2] = 5;
printf("p[0] = %i\n", p[0]); //对这个[x]不太理解,这不是数组的格式吗????
printf("p[1] = %i\n", p[1]);
printf("p[2] = %i\n", p[2]);
// 3.释放空间
free(p);
realloc
若参数ptr==NULL,则该函数等同于 malloc
返回的指针,可能与 ptr 的值相同,也有可能不同。若相同,则说明在原空间后面申请,否则,则可能后续空间不足,重新申请的新的连续空间,原数据拷贝到新空间, 原有空间自动释放
// 1.申请4个字节存储空间
int *p = malloc(sizeof(int));
printf("p = %p\n", p);
// 如果能在传入存储空间地址后面扩容, 返回传入存储空间地址
// 如果不能在传入存储空间地址后面扩容, 返回一个新的存储空间地址
p = realloc(p, sizeof(int) * 2);
*p = 666;
// 3.释放空间
free(p);
链表
可以创建静态链表,就是所有节点提前初始化,但是这样没有什么意义
// 1.定义链表节点
typedef struct node{int data;struct node *next; //结构体种有指向自己的指针
}Node;
动态链表,就需要对新创建的节点动态插入
目前我还没有如何在嵌入式里面应用链表,所以之后如果学会怎么用再补充记录。
文件操作(记录的比较潦草)
文件操作虽然在嵌入式里面用不到,但是在QT上位机中常常用到,虽然函数名不太一样,可以类比一下
以 ASCII 码格式存放,一个字节存放一个字符。文本文件的每一个字节存放一个 ASCII 码,代表一个字符。这便于对字符的逐个处理,但占用存储空间较多,而且要花费时间转换。(.c)
以补码格式存放。二进制文件是把数据以二进制数的格式存放在文件中的,其占用存储空间较少。数据按其内存中的存储形式原样存放(.exe)
//以文本形式存储,会将每个字符先转换为对应的ASCII,然后再将ASCII码的二进制存储到计算机中
int num = 666;
FILE *fa = fopen("ascii.txt", "w");
fprintf(fa, "%d", num);
fclose(fa);//以二进制形式存储,会将666的二进制直接存储到文件中
FILE *fb = fopen("bin.txt", "w");
fwrite(&num, 4, 1, fb);
fclose(fb);
通常文本打开的时候,默认会按照ASCII码逐个直接解码文件,ASIIC码格式会正常显示,二进制文件会乱码,需要利用一些软件打开转换
文件打开关闭的FILE结构体:
struct _iobuf {char *_ptr; //文件输入的下一个位置int _cnt; //当前缓冲区的相对位置char *_base; //文件的起始位置)int _flag; //文件标志int _file; //文件的有效性验证int _charbuf; //检查缓冲区状况,如果无缓冲区则不读取int _bufsiz; // 缓冲区大小char *_tmpfname; //临时文件名};typedef struct _iobuf FILE;
常用函数:
#include "stdlib.h"//以 mode 的方式,打开一个 filename 命名的文件,返回一个指向该文件缓冲的 FILE 结构体指针。
//char*filaname :要打开,或是创建文件的路径。char*mode :打开文件的方式。
FILE * fopen ( const char * filename, const char * mode );
Windows如果读写的是二进制文件,则还要加 b,比如 rb, r+b 等。 unix/linux 不区分文本和二进制文件
//fclose()用来关闭先前 fopen()打开的文件.
//int 成功返回 0 ,失败返回 EOF(-1)。
int fclose ( FILE * stream );//将 ch 字符,写入文件。FILE* stream :指向文件缓冲的指针。
int fputc (int ch, FILE * stream );//从文件流中读取一个字符并返回。FILE* stream :指向文件缓冲的指针。
int fgetc ( FILE * stream );//判断文件是否读到文件结尾,FILE* stream :指向文件缓冲的指针。
int feof( FILE * stream );
windows 平台在写入’\n’是会体现为’\r\n’,linux 平台在写入’\n’时会体现为’\n’。windows 平台在读入’\r\n’时,体现为一个字符’\n’,linux 平台在读入’\n’时,体现为一个字符’\n’
linux 读 windows 中的换行,则会多读一个字符,windows 读 linux 中的换行,则没有问题
//写入一行
//把 str 指向的字符串写入 fp 指向的文件中。char * str : 表示指向的字符串的指针。char * str : 表示指向的字符串的指针。
int fputs(char *str,FILE *fp)//读取一行
//从 fp 所指向的文件中,至多读 length-1 个字符,送入字符数组 str 中, 如果在读入 length-1 个字符结束前遇\n 或 EOF,读入即结束,字符串读入后在最后加一个‘\0’字符。
//char * str :指向需要读入数据的缓冲区。int length :每一次读数字符的字数。FILE* fp :文件流指针。
char *fgets(char *str,int length,FILE *fp)
遇到\n自动结束,读取到EOF自动结束
//写入一块数据
//把buffer 指向的数据写入fp 指向的文件中
//char * buffer : 指向要写入数据存储区的首地址的指针,char * buffer : 指向要写入数据存储区的首地址的指针,int count : 要写的字段的个数,int count : 要写的字段的个数
int fwrite(void *buffer, int num_bytes, int count, FILE *fp)//读取一块数据
//把fp 指向的文件中的数据读到 buffer 中。
//char * buffer : 指向要读入数据存储区的首地址的指针,int num_bytes: 每个要读的字段的字节数count,int num_bytes: 每个要读的字段的字节数count,int num_bytes: 每个要读的字段的字节数count
int fread(void *buffer, int num_bytes, int count, FILE *fp)
还有读写结构体啥的,如果用到这部分再把参考文章里面的东西详细学一下。