10天学会嵌入式技术之c语言-Day-8

11.4.4 回调函数

函数指针可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数简单的讲,回调函数是由别的函数执行时调用你传入的函数。
示例代码:
使用回调函数的方式,给一个整型数组 int arr[10] 赋 10 个随机数。

#include<stdio.h>
#include<stdlib.h>

//1.f就是函数指针
//2.f再initArray中被调用,是回调函数
void initArray(int* array, int arraySize, int(*f)()) 
{
  //循环
	for (int i = 0; i < arraySize; i++)
	{
		array[i] = f();//通过函数指针调用了getNextRandomValue 函数
	}
}

int main()
{
	int myarray[10];

	//rand是系统函数,会返回一个随机整数,位于<stdio.h>标准库中
	//传入rand作为参数
	initArray(myarray, 10, rand);

	//输出赋值后的数组
	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", myarray[i]);
	}
	return 0;
}

41
18467
6334
26500
19169
15724
11478
29358
26962
24464

11.5 指向指针的指针(多级指针)

(1)基本介绍
指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包
含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地
址,第二个指针指向包含实际值的位置。

(2)多级指针的定义与使用
        1)声明多级指针时,需要使用多个星号来表示指针的级别。

int *ptr;//一级指针
int**ptr;//二级指针
int***ptr;//三级指针

        2)初始化多级指针时,你需要逐级给指针赋值,确保每个级别的指针指向正确的目
标。

int var;
int *ptr = &var; // 一级指针指向 var
int **pptr = &ptr; // 二级指针指向 ptr
int ***ppptr = &pptr; // 三级指针指向 pptr

     3)解引用多级指针时,需要根据指针的级别使用适当数量的星号解引用操作。

printf("Value of var: %d\n", var);
printf("Value of ptr: %d\n", *ptr); // 解引用一次
printf("Value of pptr: %d\n", **pptr); // 解引用两次
printf("Value of ppptr: %d\n", ***ppptr); // 解引用三次

(3)多级指针案例演示

#include<Stdio.h>

int main()
{

    int var;
    int *ptr;     //一级指针
    int **pptr;    //二级指针
    int ***ppptr;   //三级指针

    
    var=3000;
    ptr=&var;        //var变量的地址赋给ptr
    pptr=&ptr;       //表示将ptr存放的地址赋给pptr
    ppptr=&pptr;     //表示将pptr存放的地址赋给ppptr


    printf("var's address is %p\nav's valua is %d \n\n",&avr,arv);
    printf("ptr`s adddress is %p \nptr`s value is %p \n*ptr=%d \n\n",
 &ptr, ptr, *ptr);
    printf("pptr`s address is %p \npptr`s value is %p \n*pptr=%p
\n**pptr=%d \n\n", &pptr, pptr, *pptr, **pptr);
    printf("ppptr`s address is %p \nppptr`s value is %p \n*ppptr=%p
\n**ppptr=%p \n***ppptr=%d \n\n", &ppptr, ppptr, *ppptr, **ppptr,
***ppptr);

return 0;
}

var`s address is 0000000cffbffdec
var`s value is 3000
ptr`s adddress is 0000000cffbffde0
ptr`s value is 0000000cffbffdec
*ptr=3000
pptr`s address is 0000000cffbffdd8
pptr`s value is 0000000cffbffde0
*pptr=0000000cffbffdec
**pptr=3000
ppptr`s address is 0000000cffbffdd0
ppptr`s value is 0000000cffbffdd8
*ppptr=0000000cffbffde0
**ppptr=0000000cffbffdec
***ppptr=3000

11.6 空指针

赋为 NULL 值的指针被称为空指针,NULL 是一个定义在标准库 <stdio.h>中的值为零
的宏常量。
声明指针变量的时候,如果没有确切的地址赋值,为指针变量赋一个 NULL 值是好的
编程习惯

#include<Stdio.h>

int main()
{

 int *p=NULL;
 int num=34;
 p=&num;

 printf("p=%p\n",p);
 printf("*p=%p\n".*p);

 return 0;

}

p=000000000061FE14
*p=34
 

11.7 野指针

野指针就是指针指向的位置是不可知(随机性,不正确,没有明确限制的)。

11.7.1

(1)指针使用前未初始化
指针变量在定义时如果未初始化,其值是随机的,此时操作指针就是去访问一个不确
定的地址,所以结果是不可知的。此时 p 就为野指针。

int *p;
printf("%d\n");

在没有给指针变量显式初始化的情况下,一系列的操作(包括修改指向内存的数据的值)
也是错误的。

(2)指针越界访问

int arr[4]={10,20,30,40}
int *p=arr;
p+=4;
printf("%d",p);  //此时*p即为越界访问

当 p += 4 之后,此时 *p 访问的内存空间不在数组有效范围内,此时 *p 就属于非法
访问内存空间,p 为野指针。

(3)指针指向已释放的空间

#include<Stdio.h>

int *test()
{
    int a=10;
    return &a;
}

  int main()
{
  int *p=test();
 printf("%d",*p);
 return 0;
 
}     

调用 test()函数将返回值赋给 p,test 函数的返回值是局部变量 a 的地址,函数调用结束
局部变量会被销毁,其所占用的内存空间会被释放,p 指向的是已经释放的内存空间,所以
p 是野指针。

11.7.3 如何避免野指针

(1)指针初始化如果没有确切的地址赋值,为指针变量赋一个 NULL 值是好的编程
习惯。
(2)小心指针越界。
(3)避免返回指向局部变量的指针。
(4)指针使用之前检查指针是否为 NULL。

11.8 指针使用一览

第十二章  预处理器

12.1 基本介绍

12.1.1 预处理器

C 语言编译器在编译程序之前,会先使用预处理器(preprocessor)处理代码,代码经过
预处理之后再送入编译器进行编译。
预处理器的主要任务包括宏替换、文件包含、条件编译等。
 

12.1.2 预处理指令

预处理过程中会执行预处理指令,预处理指令以 # 号开头,用于指导预处理器执行不
同的任务。
预处理指令具有如下特点:
(1)预处理指令应该放在代码的开头部分。
(2)预处理指令都以 # 开头,指令前面可以有空白字符(比如空格或制表符),# 和
指令的其余部分之间也可以有空格,但是为了兼容老的编译器,一般不留空格。

//推荐写法(顶格写)
#include<stdio.h>

//不推荐写法(有空格)
  #include<stdio.h>

(3)预处理指令都是一行的,除非在行尾使用反斜杠,将其折行。

#include<std\
io.h>

(4)预处理指令不需要分号作为结束符,指令结束是通过换行符来识别的。

#include<stdio.h>;//有分号会报错

#define PI 3.14;  //宏定义不用分号  ,会成为PI的值的一部分

12.2 宏定义

12.2.1 基本结束

宏定义,就是用一个标识符(宏名称)来表示一个替换文本,如果在后面的代码中出现
了宏名称,预处理器会将它替换为对应的文本,称为宏替换或宏展开。
宏定义的基本语法形式如下:

#define 宏名称 替换文本

宏名称:宏的名称,是一个标识符,通常使用大写字母表示,以便与变量名区分开来。
替换文本:宏名称在代码中的每次出现都会被替换为这段文本。
前面的案例中,我们曾经使用宏定义来定义常量和布尔类型。

12.2.2 使用宏定义常量

#include<stdio.h>

//定义常量

#define PI 3.14

int main()
{
  //定义变量保存半径,值通过用户输入获取
 double radius;
printf("请输入半径:");
scanf("%lf",&radius);

//计算面积并输出
printf("圆的面积:%lf",PI*PI*radius);

return 0;

12.2.3 使用宏定义数据类型

#include<stdio.h>

//宏定义
#define BOOL int
#define TRUE 1
#define FALSE 0

int main()
{

    //使用整型表示真假两种状态
    //int isPass = 0;
    //int isOk = 1;

    //借助于宏定义
BOOL isPass = FALSE;
BOOL isOk = TRUE;

if(isPass)
{
    printf("Pass");
}
if("Ok");
{
printf("Ok");
}

return0;
}

上面代码中使用宏定义声明了 BOOL、TURE、FALSE,后面代码中出现 BOOL 会替换
成 int,出现 TRUE 会替换成 1,出现 FALSE 替换成 0。

12.2.4 宏定义的替换文本

替换文本可以含任何字符,它可以是字面量、表达式、if 语句、函数等,预处理程序
对它不作任何检查,直接进行文本替换,如有错误,只能在编译已被宏展开后的源程序时发
现。

#include<stdio.h>
#define M (n*n+3*n)
#define PRINT_SUN printf("sum=%d",sum)

int main()
{
    int n=3;
    int sum = 3*M+4*M+5*M;
    3*(n*n+3*n)+4*(n*n+3*n)+5*(n*n+3*n);
    PRINT_SUM;

return 0;
}

sum=216

12.2.5 宏定义嵌套

宏定义允许嵌套,在宏定义的替换文本中可以使用已经定义的宏名,在宏展开时由预处
理程序层层替换。
代码示例:
 

#include<stdio.h>
#define PI 3.1415926
#define S*PI*y*y

int main()
{

    int y=2;
    printf("%f",s);

    Rreturn 0;
}

12.566370

12.2.6 取消宏定义

如要取消宏定义使用#undef 命令

#include <stdio.h>
#define PI 3.14159
int main()
{
    printf("PI=%f", PI);
    return 0;
}

#undef PI // 取消宏定义
    void func()
{
    // printf("PI=%f", PI); //错误,这里不能使用到 PI 了
}

12.3 带参数的宏定义

12.3.1 基本介绍

(1)C 语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参
数称为“实际参数”,这点和函数有些类似。
(2)对带参数的宏,在展开过程中不仅要进行文本替换,还要用实参去替换形参。
(3)带参宏定义的一般形式为#define 宏名(形参列表)替换文本,在替换文本中可以含有各个形参。
(4)带参宏调用的一般形式为:宏名(实参列表)。
代码示例:

#include<stdio.h>

//说明
//1.MAX就是带参数的宏
//2.(a,b)就是形参
//3.(a>b)?a:b 是带参数的宏对应字符串,该字符串中可以使用形参

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

int main()
{

    int x,y,max;
    printf("input tow numbers:");
    scanf("%d%d",&x,&y);

// 说明
// 1. MAX(x, y); 调用带参数宏定义
// 2. 在宏替换时(预处理,由预处理器), 会进行字符串的替换,同时会使用实
       //参, 去替换形参
// 3. 即 MAX(x, y) 宏替换后 (x>y) ? x : y

max=MAX(x,y);
printf("max=%d\n",max);

return 0;
}

12.3.2 注意事项和细节

1)带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出
现。
(2)在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型,而在宏调
用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。
(3)在宏定义中,替换文本内的形参通常要用括号括起来以避免出错。
代码示例:

#include <stdio.h>

// 带参宏定义,字符串内的形参通常要用括号括起来以避免出错
#define SQ(y) (y) * (y)

int main()
{
     int a, sq;
     printf("input a number: ");
     scanf("%d", &a);

// 宏替换为 (a+1) * (a+1)
     sq = SQ(a + 1);
     printf("sq=%d\n", sq);
     return 0;
}

12.3.3 带参数定义和函数的区别

(1)宏展开仅仅是文本的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,
它没有机会参与编译,也不会占用内存。
(2)函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,
就是执行这块内存中的代码。
案例:分别使用函数和带参数的宏计算平方值:
函数实现:

#include<stdio.h>

int SQ(int y)
{
	return y * y;
}

int main()
{
	int i = 1;
	while (i <= 5)
	{

		printf("%d\n",SQ(i++));
	}

	return 0;
}

带参数的宏实现:

#include<stdio.h>
#define SQ(y)(y)*(y)

int main()
{
	int i = 1;

	while (i <= 5)
	{
		//SQ(i++)会被替换成(i++)+(i++),i(++)会被执行两次,最终无法得到我们想要的结果
		//printf("%d\n",SQ(i++));

		printf("%d\n",SQ(i));
		i++;
	}

	return 0;

}

12.4 文件包含

#include 指令用于引入标准库头文件、自定义头文件或其他外部源代码文件,以便在当
前源文件中使用其中定义的函数、变量、宏等内容。

一个源文件可以导入多个头文件,一个头文件也可以被多个源文件导入。
标准库头文件、自定义头文件的扩展名都是 .h。

12.4.1 包含标准库文件

标准库头文件是系统提供的头文件,直接引入即可,像我们前面用过的 stdio.h、stdbool.h、string.h、time.h 等。引入标准库头文件需要使用尖括号将文件名包裹起来,格式
如下:

#include<头文件名.h>

12.4.2 包含自定义头文件

自定义头文件的文件名写在双引号里面, 格式如下:

#include“文件名.h”

建议把所有的常量、宏、系统全局变量和函数原型写在自定义的头文件中,在需要的时
候随时引用这些头文件。

(1)使用相对路劲

如果自定义的头文件在源文件的同级目录或源文件所在目录的下级目录,使用 ./ 开头
的路径,./ 可以省略。
如果自定义的头文件在源文件所在目录的上级或者更上级,使用 ../ 开头的路径。
参照以下目录结构:

├─ myheader05.h
└─ project
     ├─ inc
     │     └─ myheader04.h
    ├─ myheader03.h
    └─ src
         ├─ includes
         │    └─ myheader02.h
         ├─ main.c
         └─ myheader01.h

如果要在源文件 main.c 中引入以上自定义的头文件,需按照如下写法:

#include "myheader01.h" // 等价于 #include "./myheader01.h"
#include "includes/myheader02.h" // 等价于 #include "./myheader01.h"
#include "../myheader03.h"
#include "../inc/myheader04.h"
#include "../../myheader05.h"

需要注意的是,建议将头文件放置在源文件所在目录或子目录中,以保持项目的组织结
构清晰

(2)使用绝对路径

绝对路径是文件在文件系统中的完整路径,它从文件系统的盘符(Windows 系统)或
根目录(Linux 系统、MacOS 系统)开始,沿着文件系统的目录结构一直到达目标文件。

Windows 系统中使用绝对路径引入自定义头文件,示例如下:

#include "C:\Preparation\Embedded\01CLang\code\project\foo.h"

Linux 系统或 MacOS 系统中使用绝对路径引入自定义头文件,示例如下:

#include "/usr/local/lib/foo.h"

需要注意的是,通常我们建议使用相对路径引入自定义头文件,因为相对路径更加灵活
和可移植。

12.5 条件编译

12.5.1 #if

(1)  #if...#endif

#if...#endif 指令用于预处理器的条件判断,满足条件时,内部的行会被编译,否则就被
编译器忽略。

#if 0
const double pi = 3.1415; // 不会执行
#endif

上面示例中,#if 后面的 0,表示判断条件不成立。所以,内部的变量定义语句会被编
译器忽略。#if 0 这种写法常用来当作注释使用,不需要的代码就放在#if 0 里面。
#if 后面的判断条件,通常是一个表达式。如果表达式的值不等于 0,就表示判断条件
为真,编译内部的语句;如果表达式的值等于 0,表示判断条件为伪,则忽略内部的语句。

(2)#if … #else … #endif

#if...#endif 之间还可以加入#else 指令,用于指定判断条件不成立时,需要编译的语句。

#define FOO 1

#if FOO
    printf("define\n");
#else
    printf("not defined\n");
#endif

上面示例中,宏 FOO 如果定义过,会被替换成 1,从而输出 defined,否则输出 not
defined。

(3)#if … #elif … #else … #endif

如果有多个判断条件,还可以加入#elif 命令。

#if HAPPY_FACTOR == 0
    printf("I'm not happy!\n");
#elif HAPPY_FACTOR == 1
    printf("I'm just regular\n");
#else
    printf("I'm extra happy!\n");
#endif

上面示例中,通过#elif 指定了第二重判断。注意,#elif 的位置必须在#else 之前。如果
多个判断条件皆不满足,则执行#else 的部分。
没有定义过的宏,等同于 0。因此如果 UNDEFINED 是一个没有定义过的宏,那么#if
UNDEFINED 为伪,而#if !UNDEFINED 为真。

12.5.2 #ifdef 

#ifdef...#endif 指令用于判断某个宏是否定义过。
有时源码文件可能会重复加载某个库,为了避免这种情况,可以在库文件里使用#define
定义一个空的宏。通过这个宏,判断库文件是否被加载了。

#ifdef EXTRA_HAPPY
    printf("I'm extra happy!\n");
#endif
#ifndef EXTRA_HAPPY
    printf("I'm just regular\n");
#endif

上面示例中,针对宏 EXTRA_HAPPY 是否被定义过,#ifdef 和#ifndef 分别指定了两种
情况各自需要编译的代码。
#ifndef 常用于防止重复加载。举例来说,为了防止头文件 myheader.h 被重复加载,可
以把它放在#ifndef...#endif 里面加载。

#ifndef MYHEADER_H
#define MYHEADER_H
#include "myheader.h"
#endif

上面示例中,宏 MYHEADER_H 对应文件名 myheader.h 的大写。只要#ifndef 发现这个
宏没有被定义过,就说明该头文件没有加载过,从而加载内部的代码,并会定义宏MYHEADER_H,防止被再次加载。

12.5.5 案例分析

(1)具体要求
开发一个 C 语言程序,让它暂停 5 秒以后再输出内容"helllo, 尚硅谷!~",并且要求跨
平台,在 Windows 和 Linux 下都能运行,如何处理。
(2)提示
        1)Windows 平台下的暂停函数的原型是 void Sleep(DWORD dwMilliseconds),参数的
单位是“毫秒”,位于<windows.h>头文件。
        2)Linux 平台下暂停函数的原型是 unsigned int sleep (unsigned int seconds),参数的单
位是“秒”,位于 <unistd.h> 头文件。

#include <stdio.h>
#if _WIN32 //如果是 windows 平台, 就引入 <windows.h>
#include <windows.h>
#define SLEEP(t) Sleep(t * 1000)
#elif __linux__ // 如果是 linux 平台, 就引入<unistd.h>
#include <unistd.h>
#define SLEEP sleep
#endif
int main()
{
SLEEP(5);
    printf("hello, 尚硅谷~");
return 0;
}

13.6 预处理命令总结

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值