C语言详解(预编译)

news2024/11/25 6:46:37

Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~
💥💥个人主页:奋斗的小羊
💥💥所属专栏:C语言

🚀本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为展示我的学习过程及理解。文笔、排版拙劣,望见谅。


目录

  • 前言
    • 1、预定义符号
    • 2、#define定义常量和标识符
    • 3、#define定义宏
    • 4、带有副作用的宏参数
    • 5、宏替换的规则
    • 6、宏和函数的对比
    • 7、# 和
      • 7.1 #运算符
      • 7.2 ##运算符
    • 8、命名的约定
    • 9、#undef
    • 10、命令行定义
    • 11、条件编译
    • 12、头文件的包含
      • 12.1 头文件被包含的方式
        • 12.1.1 本地文件包含
        • 12.1.2 库文件包含
      • 12.2 嵌套文件的包含
  • 总结

前言

本篇文章将详细介绍编译过程中预编译的具体细节
在C语言的学习中部分人可能会忽视这一部分的学习,因为像VS这样相对强大的集成开发环境,我们在写好代码后只需要开始执行即可,所以部分人认为这一部分不值得我们花费时间去学习
其实不然,学习C语言预编译过程可以帮助我们更深入地了解C语言的编译过程和语法特性,提高代码编写的效率和质量,以及拓展编程技能


1、预定义符号

C语言设置了一些预定义符号,可以直接使用,预定义符号也是在预编译阶段处理的

  • __FILE__:正在编译的源文件的文件名
  • __LINE__:文件当前的行号
  • __DATE__:文件被编译的日期
  • __TIME__:文件被编译的时间
  • __STDC__:如果编译器遵循 ANSI C,其值为1,否则未定义

例如:

在这里插入图片描述


2、#define定义常量和标识符

#define定义的常量和标识符在预编译阶段完成替换

基本语法:

#define name stuff

特别的,为了区分普通常量这个name我们一般用大写形式
比如:

#define MAX 10000
#define REG register

#define后面的代码理论上讲只能写一行,但是如果后面的代码过长,我们可以使用'\'来实现换行,相当于转义转义字符'\'转义了转义字符'\n'

#define DEBUG_PRINT printf("file:%s\tline:%d\t\
							date:%s\ttime:%s\n,\
							__FILE__,__LINE__,\
							__DATE__,__TIME__)

值得注意的是,行末最好不要加;,在某些场景下是没什么问题,但是在大多数情况下是有语法错误的,所以我们要养成良好的编程习惯,行末不加;


3、#define定义宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)
基本语法:

#define name(parament_list) stuff

其中parament-list(参数列表)是一个由逗号隔开的符号表,它们可能出现在stuff

注意: 参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

举例:输入一个数,输出它的平方数

#include <stdio.h>
#define SQUARE(x) x*x

int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = SQUARE(n);
	printf("%d\n", ret);
	return 0;
}

在这里插入图片描述

上面的代码看似没有什么问题,但当我们想计算n+1的平方数时,就会出现问题:

#include <stdio.h>
#define SQUARE(x) x*x

int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = SQUARE(n + 1);
	printf("%d\n", ret);
	return 0;
}

在这里插入图片描述

这是为什么呢?

原因就是带参数的宏在替换的时候括号内的表达式是不做任何计算的

也就是说,上面替换后的形式是:5 + 1 * 5 + 1,为了解决这个问题,我们可以在定义宏的时候给x加上括号:

#define SQUARE(x) (x)*(x)

这样替换后的结果就变成了:(5 + 1)*(5 + 1),但是这样给单独的参数加括号的形式在某些场景下还是存在问题,比如:

#include <stdio.h>
#define SQUARE(x) (x)+(x)

int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = 5 * SQUARE(n + 1);
	printf("%d\n", ret);
	return 0;
}

在这里插入图片描述

那为了解决这个问题,我们可以(x)+(x)整体加上括号:((x) + (x))

#include <stdio.h>
#define SQUARE(x) ((x)+(x))

int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = 5 * SQUARE(n + 1);
	printf("%d\n", ret);
	return 0;
}

在这里插入图片描述

所以,在写宏的时候一定不要吝啬括号


4、带有副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果,副作用就是表达式求值的时候出现的永久性效果
例如:

  • x + 1; //不带副作用
  • x++; //带有副作用

上面两个表达式的值是相同的,但是第一个表达式x的本身没有发生改变,而第二个表达式x本身发现了改变,这就是副作用

例如:使用宏实现求两个数的较大值

#include <stdio.h>
#define MAX(x, y) ((x)>(y)?(x):(y))

int main()
{
	int a = 10;
	int b = 20;
	int ret = MAX(a, b);
	printf("%d\n", ret);
	return 0;
}

在这里插入图片描述

上面代码中宏参数在宏定义中出现了两次,我们使用MAX(a, b);时没什么问题,但当我们使用MAX(a++, b++);时问题就会出现:

#include <stdio.h>
#define MAX(x, y) ((x)>(y)?(x):(y))

int main()
{
	int a = 10;
	int b = 20;
	int ret = MAX(a++, b++);
	printf("%d\n", ret);
	printf("a = %d, b = %d\n", a, b);
	return 0;
}

在这里插入图片描述

可以发现a和b的值会发生改变,就是表达式求值的时候出现了永久性效果。

与函数对比:

#include <stdio.h>

int MAX(int x, int y)
{
	printf("a = %d, b = %d\n", x, y);
	return (x > y ? x : y);
}

int main()
{
	int a = 10;
	int b = 20;
	int ret = MAX(a++, b++);
	printf("%d\n", ret);
	return 0;
}

请添加图片描述

从上面的代码中可以看出来,带参数的宏替换和函数传参是非常相似的,但是它们的传参是有本质区别的。
带参数的宏替换是直接将参数做整体替换,替换过后的表达式是:((a++)>(b++)?(a++):(b++));而函数参过后的表达式是:(a > b ? a : b)


5、宏替换的规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  • 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果有,它们首先被替换
  • 替换文本随后被插入到程序中原来文本的位置,对于宏,参数名被他们的值所替换
  • 最后,再次对结果文件进行扫描,看看是否包含任何由#define定义的符号,如果有,重复上述步骤

例如:

#include <stdio.h>
#define M 10
#define N M + 2
#define MAX(x, y) ((x)>(y)?(x):(y))

int main()
{
	int ret = MAX(M, N);
	
	return 0;
}

MAX(M, N)首先被替换成:((10)>(M + 2)?(10):(M + 2))
然后((10)>(M + 2)?(10):(M + 2))再被替换成:((10)>(10 + 2)?(10):(10 + 2))

注意:

  • 宏参数和#define定义中可以出现其他#define定义的符号,但宏不能实现递归

比如:#define N M + 2这个是可以的,但#define N N + 2是不行的。

  • 当预处理器搜索#define定义的符号时,字符串常量的内容并不被搜索

比如:

#include <stdio.h>
#define M 10
#define N M + 2
#define MAX(x, y) ((x)>(y)?(x):(y))

int main()
{
	printf("MAX(M, N)");
	return 0;
}

请添加图片描述

可以看到宏MAX(M, N)并没有展开。


6、宏和函数的对比

宏通常被应用于执行简单的运算。

比如在两个数中找较大数,用宏实现更有优势:

#define MAX(x, y) ((x)>(y)?(x):(y))

那为什么不用函数呢?原因有二:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作需要的时间更多,函数调用还需要一些入栈出栈的过程,所以宏比函数在程序的规模和速度方面更胜一筹
  2. 更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用。但宏可以使用于整型、长整型、浮点型等可以用于>来比较的类型,宏参数是无关类型的

但是和函数相比宏还是有劣势的:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度
  2. 宏是不能调试的
  3. 宏由于无关类型,也就不够严禁,所以宏定义是不够安全的
  4. 宏可能会带来运算符优先级的问题,导致程序容易出错

宏有时候能做到函数做不到的事,比如:宏的参数可以出现类型,但是函数不行

#include <stdio.h>
#define MALLOC(n, type) (type*)malloc(n * sizeof(type))

int main()
{
	//int* p = (int*)malloc(10 * sizeof(int));
	int* p = MALLOC(10, int);
	//int *p = (int*)malloc(10 * sizeof(int));

	return 0;
}

宏和函数的对比:

属性#define定义宏函数
代码长度每次使用时,宏代码都会被插入到程序中,除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方,每次使用这个函数时,都调用那个地方的用一份代码
执行速度更快存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的结果,所以建议宏在书写的时候多写括号函数参数只在函数调用的时候求值一次,它的结果值传递给函数,表达式的求值结果更容易预测
带有副作用的参数参数可能被替换到宏体中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预测的结果函数参数只在传参的时候求值一次,结果更容易控制
参数类型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用任何参数类型函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的
调试宏是不方便调试的函数是可以逐语句调试的
递归宏是不能递归的函数是可以递归的

7、# 和

7.1 #运算符

#运算符将宏的一个参数转换为字符串字面量,它仅允许出现在带参数的宏的替换列表中
#运算符所执行的操作可以理解为“字符串化
比如:当我们有一个变量int a = 10;的时候,我们想打印出:the value of a is 10.
下面是常规写法:

#include <stdio.h>

int main()
{
	int a = 10;
	printf("the value of a is %d\n", a);
	return 0;
}

如果我们想把打印的这条代码通过宏替换来实现,该怎么做呢?

#include <stdio.h>
#define PRINT(format, n) printf("the value of n is "format"\n", n)

int main()
{
	int a = 10;
	PRINT("%d", a);
	//printf("the value of n is ""%d""\n", a);
	return 0;
}

如果写成上面这种代码很明显并没有解决问题,因为如果我们将n写成%d时并不能打印出a,而只能打印出a的值,那为了能打印出a本身的字面量,我们就可以使用#操作符
如下:

#include <stdio.h>
#define PRINT(format, n) printf("the value of "#n" is "format"\n", n)

int main()
{
	int a = 10;
	PRINT("%d", a);
	//printf("the value of "a" is ""%d""\n", a);
	double b = 3.14;
	PRINT("%lf", b);
	//printf("the value of "b" is ""%lf""\n", b);
	return 0;
}

请添加图片描述

所以我们说:#运算符所执行的操作可以理解为“字符串化”,上面的代码中是将a和b字符串化了。

当n = a的时候,#n 就相当于“a”


7.2 ##运算符

##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。##被称为记号粘合
这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。

比如现在有这么一个问题:当我们写一个函数来求两个数的较大值的时候,不同的类型我们就需要写不同的函数,这样写太繁琐了,我们可以使用宏来简化这件事:

#include <stdio.h>
#define GENERIC(type) \
type type##_max(type x, type y)\
{\
	return ((x) > (y) ? (x) : (y));\
}

GENERIC(int)
//int int_max(int x, int y)
//{
//	return ((x) > (y) ? (x) : (y));
//}

GENERIC(double)
//double double_max(double x, double y)
//{
//	return ((x) > (y) ? (x) : (y));
//}

int main()
{
	printf("%d\n", int_max(10, 20));
	printf("%lf\n", double_max(3.14, 6.28));
	return 0;
}

请添加图片描述

上面的代码中我们利用宏替换来实现创建不同类型的函数,type##_max中的##操作符将type_max连接成了一个新的标识符


8、命名的约定

一般来讲函数和宏的使用语法很相似,所以语言本身没法帮我们区分二者,那我们平时的习惯是:

  • 把宏名全部大写
  • 函数名不要全部大写或不大写

9、#undef

#undef这条指令用于移除一个宏定义

#include <stdio.h>
#define M 10

int main()
{
	printf("%d\n", M);
#undef M
	printf("%d\n", M);
	return 0;
}

请添加图片描述

如果现存的一个宏名需要被重新定义,那么它的旧名字首先需要被移除


10、命令行定义

许多C编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程。
例如:当我们根据同一个源文件想要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个一定长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个较大的数组)

编译指令:

//linux  环境演示
gcc -D ARRAY_SIZE=10 programe.c

11、条件编译

满足条件,就参与编译;不满足条件,就不参与编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的,因为我们有条件编译指令:

1.
#if   常量表达式   //常量表达式由预处理器求值
//...
#endif

如:
#define _DEBUG_ 1
int main()
{
#if _DEBUG_
	printf("a");
#endif
	return 0;
}
2.多个分支的条件编译
#if   常量表达式
//...
#elif   常量表达式
//...
#else
//...
#endif

如:
#define M 1
int main()
{
#if M == 1
	printf("a");
#elif M == 2
	printf("b"):
#else
	printf("C");
#endif
	return 0;
}
3.判断是否被定义
//如果定义了
#if defined(symbol)
#ifdef symbol

如:
#define M 2
int main()
{
#ifdef M
	printf("a");
#endif
	return 0;
}

//如果没定义
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#ifdef OS_UNIX
	#ifdef OPTION1
		unix_version_option1();
	#endif
	#ifdef OPTION2
		unix_version_option2();
	#endif
#elif defined(OS_MSDOS)
	#ifdef OPTION2
		msdos_version_option2():
	#endif
#endif
	

条件编译通常用于跨平台性代码的编译


12、头文件的包含

12.1 头文件被包含的方式

12.1.1 本地文件包含

一般指自己创建的头文件

#include "filename.h"

查找策略:
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误。


12.1.2 库文件包含

一般指标准库中头文件的包含

#include <filename.h>

查找策略:
直接去标准路径下去查找,如果找不到就提示编译错误。
那这样是不是就说明,对库文件也可以使用" "的形式包含呢?
答案是可以的。但是这样查找的效率比较低,也不容易区分是库文件还是本地文件


12.2 嵌套文件的包含

我们已经知道,#include指令可以使另外一个文件被编译,就像它实际出现于#include指令的地方一样。
这种替换的方式很简单:预编译器先删除这条指令,并用包含文件的内容替换
一个头文件被包含几次,就会被实际编译几次,如果重复包含,对编译的压力就比较大

#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"

int main()
{
	return 0;
}

如果像上面这样写,test.h文件的内容就会被拷贝5份
如果test.h文件比较大,这样预处理后代码量会剧增,如果工程比较大,有公共使用的文件,被大家都能用,又不做任何的处理,那么后果会不堪设想。
为了解决头文件被重复引入的问题,就要用到条件编译
我们在每个头文件的开头这样写:

#ifndef __FILENAME_H__
#define __FILENAME_H__

//...

#endif

或者

#pragma once

就可以避免头文件的重复引入。


总结

  • 增强对C语言编译过程的整体理解:预编译是C语言编译过程的第一阶段,在预编译阶段可以对源代码进行预处理,如宏定义、头文件包含等。通过学习预编译过程,可以更全面地理解C语言代码的编译过程。
  • 优化代码结构:预编译指令能够简化代码结构、提高代码的重用性和可维护性。学习预编译过程可以帮助程序员更好地利用预编译指令优化代码结构,提高代码的质量。
  • 理解条件编译和跨平台编译:条件编译是预编译指令中的重要功能,可以根据不同条件编译不同的代码。通过学习预编译过程,可以了解如何使用条件编译来实现跨平台编译,提高代码的可移植性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1817235.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【vue-8】记事本案例

小知识点&#xff1a; 列表末尾插入数据&#xff1a; list.push("lihua") 列表删除数据&#xff1a; # index要删除数据的索引值&#xff0c;1为删除数据长度 list.splice(index,1) 完整示例代码&#xff1a; <!DOCTYPE html> <html lang"en&quo…

Leetcode刷题笔记10

14. 最长公共前缀 14. 最长公共前缀 - 力扣&#xff08;LeetCode&#xff09; 首先&#xff0c;检查边界条件 如果输入的字符串数组为空&#xff0c;直接返回空字符串。 然后使用minmax_element函数找到数组中字典序最小和最大的字符串。 因为公共前缀一定会出现在字典序最…

Python:基础爬虫

Python爬虫学习&#xff08;网络爬虫&#xff08;又称为网页蜘蛛&#xff0c;网络机器人&#xff0c;在FOAF社区中间&#xff0c;更经常的称为网页追逐者&#xff09;&#xff0c;是一种按照一定的规则&#xff0c;自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字…

用 KV 缓存量化解锁长文本生成

很高兴和大家分享 Hugging Face 的一项新功能: KV 缓存量化 &#xff0c;它能够把你的语言模型的速度提升到一个新水平。 太长不看版: KV 缓存量化可在最小化对生成质量的影响的条件下&#xff0c;减少 LLM 在长文本生成场景下的内存使用量&#xff0c;从而在内存效率和生成速度…

.net 调用海康SDK以及常见的坑解释

📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!📢本文作者:由webmote 原创📢作者格言:新的征程,我们面对的不仅仅是技术还有人心,人心不可测,海水不可量,唯有技术,才是深沉黑夜中的一座闪烁的灯塔 !序言 在工控领域,很多时候需要…

【博士每天一篇文献-算法】Progressive Neural Networks

阅读时间&#xff1a;2023-12-12 1 介绍 年份&#xff1a;2016 作者&#xff1a;Andrei A. Rusu,Neil Rabinowitz,Guillaume Desjardins,DeepMind 研究科学家,也都是EWC(Overcoming catastrophic forgetting in neural networks)算法的共同作者。 期刊&#xff1a; 未录用&am…

.NET MAUI Sqlite数据库操作(一)

一、安装 NuGet 包 安装 sqlite-net-pcl 安装 SQLitePCLRawEx.bundle_green 二、配置数据库&#xff08;数据库文件名和路径&#xff09; namespace TodoSQLite; public static class Constants {public const string DatabaseFilename "TodoSQLite.db3";//数据库…

ModuleNotFoundError: No module named ‘MySQLdb‘

python项目运行遇到报错&#xff1a;ModuleNotFoundError: No module named ‘MySQLdb’ 解决办法 1、安装依赖 pip install pymysql2、新增配置 在项目的__init__.py文件中添加以下代码即可解决。 import pymysql pymysql.install_as_MySQLdb()

tim定时器 输入捕获模式下 TIM–ICStructinit(TIM–ICStructinit) 这个值 解析

主要需要看着图来理解 1.这是stm中文手册的图 2.这是解析 我觉得写的不错 注&#xff1a;有个很坑的地方 我觉得是stm32中文手册的问题 他写的解释只写了tim输入2 3 4和ic1 2 3 4&#xff0c;少写了一个输入1 第一次看见很不好理解

vue2动态路由实现

实现一个简单的动态路由&#xff1a; 1、先定义菜单页面组件的结构&#xff0c;使用的是elementUI的NavMenu 导航菜单 <template><div><el-menu default-active"1" router><el-submenu :index"item.path" v-for"item in menu_…

Android14音频进阶之CarAudioManager::getOutputDeviceForUsage流程分析(七十七)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒体系统工程师系列【原创干货持续更新中……】🚀 优质视频课程:AAOS车载系统+AOSP…

C数据结构:排序

目录 冒泡排序 选择排序 堆排序 插入排序 希尔排序 快速排序 hoare版本 挖坑法 前后指针法 快速排序优化 三数取中法 小区间优化 快速排序非递归 栈版本 队列版本 归并排序 归并排序非递归 ​编辑 计数排序 各排序时间、空间、稳定汇总 冒泡排序 void Bub…

嵌入式linux系统中设备树的经典使用方法

第一:设备树简介 大家好,今天主要给大家分享一下,如何使用linux系统里面的设备树,详细分析如下。 可以参考的官方文档有: 官方文档(可以下载到 devicetree-specification-v0.2.pdf): https://www.devicetree.org/specifications/ 内核文档: …

MNIST手写字符分类

MNIST手写字符分类 文章目录 MNIST手写字符分类1 数据集2 模型构建3 训练4 模型保存5 推理6 模型导出7 导出模型测试 1 数据集 MNIST手写字符集包括60000张用于训练的训练集图片和10000张用于测试的测试集图片&#xff0c;所有图片均归一化为28*28的灰度图像。其中字符区域为白…

LabVIEW水箱液位控制系统

介绍了如何使用LabVIEW软件和硬件工具开发水箱液位控制系统。系统集成了数据采集、实时控制和模拟仿真技术&#xff0c;展示了高精度和高可靠性的特点&#xff0c;适用于需要精细水位调节的工业应用。 项目背景 在制造和化工行业&#xff0c;液位控制是保证生产安全与效率的关…

第3章 Unity 3D着色器系统

3.1 从一个外观着色器程序谈起 新建名为basic_diffuse.shader的文件&#xff0c;被一个名为basic_diffuse.mat的材质文件所引用&#xff0c;而basic_diffuse.mat文件则被场景中名为Sphere的game object的MeshRenderer组件所使用。 basic_diffuse.shader代码文件的内容如下所示…

51.Python-web框架-Django开始第一个应用的增删改查

目录 1.概述 2.创建应用 创建app01 在settings.py里引用app01 3.定义模型 在app01\models.py里创建模型 数据库迁移 4.创建视图 引用头 部门列表视图 部门添加视图 部门编辑视图 部门删除视图 5.创建Template 在app01下创建目录templates 部门列表模板depart.ht…

java+vue3+el-tree实现树形结构操作

基于springboot vue3 elementPlus实现树形结构数据的添加、删除和页面展示 效果如下 代码如下&#xff0c;业务部分可以自行修改 java后台代码 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.daztk.mes.common.annotation.LogOperation…

高通Android 12 右边导航栏改成底部显示

最近同事说需要修改右边导航栏到底部&#xff0c;问怎么搞&#xff1f;然后看下源码尝试下。 1、Android 12修改代码路径 frameworks/base/services/core/java/com/android/server/wm/DisplayPolicy.java a/frameworks/base/services/core/java/com/android/server/wm/Display…

树莓派4B_OpenCv学习笔记6:OpenCv识别已知颜色_运用掩膜

今日继续学习树莓派4B 4G&#xff1a;&#xff08;Raspberry Pi&#xff0c;简称RPi或RasPi&#xff09; 本人所用树莓派4B 装载的系统与版本如下: 版本可用命令 (lsb_release -a) 查询: Opencv 版本是4.5.1&#xff1a; 学了这些OpenCv的理论性知识&#xff0c;不进行实践实在…