linux alsa-lib snd_pcm_open函数源码分析(三)

欢迎直接到博客

linux alsa-lib snd_pcm_open函数源码分析(三)

系列文章其他部分:

linux alsa-lib snd_pcm_open函数源码分析(一)

linux alsa-lib snd_pcm_open函数源码分析(二)

linux alsa-lib snd_pcm_open函数源码分析(四)

linux alsa-lib snd_pcm_open函数源码分析(五)

linux alsa-lib snd_pcm_open函数源码分析(六)

解析配置的最后一个超复杂子函数

1. snd_config_hooks

核心函数之一,配置hooks功能,其中使用hooks功能加载并解析了配置文件中引用的其他配置文件。
所谓的其他配置文件通常为"/etc/asound.conf"及"~/.asoundrc",也就是用户经常自定义修改的配置文件。

static int snd_config_hooks_call(snd_config_t *root, snd_config_t *config, snd_config_t *private_data)
{void *h = NULL;snd_config_t *c, *func_conf = NULL;char *buf = NULL;const char *lib = NULL, *func_name = NULL;const char *str;int (*func)(snd_config_t *root, snd_config_t *config, snd_config_t **dst, snd_config_t *private_data) = NULL;int err;//在配置树中通过id寻找某个节点//实际通常这里func后会跟一个load,比如alsa.conf中的func load//目的是加载其他配置//见下文详细分析err = snd_config_search(config, "func", &c);if (err < 0) {SNDERR("Field func is missing");return err;}//功能及实现都很简单//返回string类型节点的string值//本质上就是直接把节点的值返回一下//见下文分析err = snd_config_get_string(c, &str);if (err < 0) {SNDERR("Invalid type for field func");return err;}assert(str);//配置树中查找"hook_func"节点//详细分析见下文err = snd_config_search_definition(root, "hook_func", str, &func_conf);if (err >= 0) {snd_config_iterator_t i, next;if (snd_config_get_type(func_conf) != SND_CONFIG_TYPE_COMPOUND) {SNDERR("Invalid type for func %s definition", str);err = -EINVAL;goto _err;}snd_config_for_each(i, next, func_conf) {snd_config_t *n = snd_config_iterator_entry(i);const char *id = n->id;if (strcmp(id, "comment") == 0)continue;if (strcmp(id, "lib") == 0) {err = snd_config_get_string(n, &lib);if (err < 0) {SNDERR("Invalid type for %s", id);goto _err;}continue;}if (strcmp(id, "func") == 0) {err = snd_config_get_string(n, &func_name);if (err < 0) {SNDERR("Invalid type for %s", id);goto _err;}continue;}SNDERR("Unknown field %s", id);}}if (!func_name) {int len = 16 + strlen(str) + 1;buf = malloc(len);if (! buf) {err = -ENOMEM;goto _err;}//这里即可拼接出函数的名字snprintf(buf, len, "snd_config_hook_%s", str);buf[len-1] = '\0';func_name = buf;}//对dlopen的包装,打开库,如果指定的库不存在,则打开默认的库h = snd_dlopen(lib, RTLD_NOW);//对dlsym的包装,从动态库中解析函数符号,即通过字符串查找到函数func = h ? snd_dlsym(h, func_name, SND_DLSYM_VERSION(SND_CONFIG_DLSYM_VERSION_HOOK)) : NULL;err = 0;if (!h) {SNDERR("Cannot open shared library %s", lib);err = -ENOENT;} else if (!func) {SNDERR("symbol %s is not defined inside %s", func_name, lib);snd_dlclose(h);err = -ENXIO;}_err:if (func_conf)snd_config_delete(func_conf);if (err >= 0) {snd_config_t *nroot;//这里执行了根据字符串查找出来的函数err = func(root, config, &nroot, private_data);if (err < 0)SNDERR("function %s returned error: %s", func_name, snd_strerror(err));snd_dlclose(h);if (err >= 0 && nroot)err = snd_config_substitute(root, nroot);}free(buf);if (err < 0)return err;return 0;
}

1.1 snd_config_search

在配置树中通过id查找一个子节点。id可以是一个或多个,中间以圆点(.)分隔。
如果是多个id的情况,则每个id需要依次指定前一级的复合节点。
比如在下面的配置树中,假设config是复合节点的句柄,
每个节点后的注释为找到这个节点需要用到的key

  config {a 42               # "a"b {                # "b"c "cee"        # "b.c"d {            # "b.d"e 2.71828  # "b.d.e"}}}

函数原型如下:

int snd_config_search(snd_config_t *config, const char *key, snd_config_t **result)
{SND_CONFIG_SEARCH(config, key, result, );
}

实际主要是使用了SND_CONFIG_SEARCH宏,这类宏有多个,后面会对所有类型的宏都做详细的分析。

1.1.1 SND_CONFIG_SEARCH

此宏是相对来说最简单的宏,实现的目的比较单一,即通过Key查找节点。
其中主要调用了_snd_config_search函数,此函数在上一篇中已经分析过。

#define SND_CONFIG_SEARCH(config, key, result, extra_code) \
{ \snd_config_t *n; \int err; \const char *p; \assert(config && key); \while (1) { \//如果不是复合节点直接报错。//这是因为传入的config本身即为父节点,如果不是复合节点则本身就不应该在一个单身节点下面查找子节点if (config->type != SND_CONFIG_TYPE_COMPOUND) \return -ENOENT; \//这里执行extra_code,这也是为什么宏中可以添加代码的原因。{ extra_code ; } \//此函数返回key中第一次出现字符'.'的位置,如果每找到,则返回nullp = strchr(key, '.'); \if (p) { \//如果找到,则返回(.)的位置,注意此处的p-key//由于返回的是.的位置,即.的地址,key为字符串最开头的地址//这样p-key即为字符串开始到.的字符的个数//于是搜索key字符串的前p-key个字符,则变成了搜索key字符串第一个点前的字符串//比如搜索a.b.c的话,此时相当于搜索aerr = _snd_config_search(config, key, p - key, &n); \if (err < 0) \return err; \//注意这里,如果搜索到了返回n,则把n赋值为config,下个循环则从n开始搜索config = n; \//p+1的目的是跳过(.),这里的1其实就是这个(.)//这样一来,在下个循环的时候,相当于从刚刚搜索到的节点开始,搜索(.)后面的内容key = p + 1; \} else \//如果没有(.)则传下来的key就是要搜索的内容return _snd_config_search(config, key, -1, result); \} \
}

1.2 snd_config_get_string

非常简单,返回string类型节点的string值。

int snd_config_get_string(const snd_config_t *config, const char **ptr)
{assert(config && ptr);if (config->type != SND_CONFIG_TYPE_STRING)return -EINVAL;*ptr = config->u.string;return 0;
}

1.3 snd_config_search_definition

在配置树中查找节点,函数允许传入的参数为别名(alias),同时如果传入的参数为别名,函数会还把别名展开。
如果传入的名字中包含冒号(😃,则冒号(:)后则为snd_config_expand展开所用的参数。

int snd_config_search_definition(snd_config_t *config,const char *base, const char *name,snd_config_t **result)
{snd_config_t *conf;char *key;//查找":"//如果返回参数作为char*,则实际是找到的:的地址const char *args = strchr(name, ':');int err;if (args) {//如果找到了:,则自增1,目的是跳过:这个字符args++;//args - name则等于:前的字符长度+1,多出的1个字符刚好作为\0key = alloca(args - name);//注意这里需要把多出来的\0字符的位置减掉//实际刚好为:前的内容memcpy(key, name, args - name - 1);//最后的位置为\0key[args - name - 1] = '\0';} else {//找不到则直接就是keykey = (char *) name;}/**  if key contains dot (.), the implicit base is ignored*  and the key starts from root given by the 'config' parameter*/snd_config_lock();//如果key中有(.),则传入的base会被忽略//否则如果没有找到(.),说明没有指明base,则采用传入的base//函数查找子结点,如果传入的是别名(alias)则可以展开别名//详细见下文分析err = snd_config_search_alias_hooks(config, strchr(key, '.') ? NULL : base, key, &conf);if (err < 0) {snd_config_unlock();return err;}//展开节点,并执行对应的函数//见下文分析err = snd_config_expand(conf, config, args, NULL, result);snd_config_unlock();return err;
}

1.3.1 snd_config_search_alias_hooks

在配置树下使用别名及hooks查找节点。主要用来实现snd_config_search_definition的功能。
对别两个函数,snd_config_search_definition的第三个参数name可以包含’:‘,
如果有’:‘则’:‘后的内容为snd_config_expand的参数,而snd_config_search_alias_hooks的第三个参数更纯粹,
就是key,无法包含’:'用于snd_config_expand

int snd_config_search_alias_hooks(snd_config_t *config,const char *base, const char *key,snd_config_t **result)
{SND_CONFIG_SEARCH_ALIAS(config, base, key, result,snd_config_searcha_hooks,snd_config_searchva_hooks);
}

这里出现了SND_CONFIG_SEARCH_ALIAS宏,与此类似的宏由多个,这些宏分别实现类似的功能,但是彼此之间有差异。
这些宏分析起来及其复杂,因为大部分都涉及到递归处理,以及相互嵌套。
这些函数中最基础的有两个,在此做介绍,其他类似的函数的函数只做功能说明.

1.3.2 snd_config_searcha

通过key在配置树中查找节点,展开别名。注意与1.1 snd_config_search的区别。

  config {a {b bb}}root {bb {c cc}cc cccccc {d {x "icks"}}}

在上面的配置树中,使用snd_config_searcha(root, config, "a.b.c.d", &result);则最终返回d节点。

int snd_config_searcha(snd_config_t *root, snd_config_t *config, const char *key, snd_config_t **result)
{SND_CONFIG_SEARCHA(root, config, key, result, snd_config_searcha, );
}

1.3.2.1 SND_CONFIG_SEARCHA

此宏主要用来实现snd_config_searcha,使用key查找一个配置节点,同时在root下查找是否有别名并展开。
注意传入的fcn为调用者本身,意味着此处会有递归处理。

#define SND_CONFIG_SEARCHA(root, config, key, result, fcn, extra_code) \
{ \snd_config_t *n; \int err; \const char *p; \assert(config && key); \while (1) { \//如果不是复合类型,通常意味着搜索结束或者错误if (config->type != SND_CONFIG_TYPE_COMPOUND) { \//获取string类型的字符串值if (snd_config_get_string(config, &p) < 0) \return -ENOENT; \//递归处理err = fcn(root, root, p, &config); \if (err < 0) \return err; \} \{ extra_code ; } \//此处是查看key中是否有(.)p = strchr(key, '.'); \if (p) { \err = _snd_config_search(config, key, p - key, &n); \if (err < 0) \return err; \//把找到的n赋值给config,相当于从root逐步往下查找config = n; \key = p + 1; \} else \return _snd_config_search(config, key, -1, result); \} \
}

此类函数的功能,所使用的宏,以及主要作用总结在下表:

函数名功能说明使用到的宏
snd_config_search在配置树中根据key查找节点SND_CONFIG_SEARCH
snd_config_searcha在配置树中根据key查找节点,展开别名。别名从root下查找SND_CONFIG_SEARCHA
snd_config_searchv在配置树中根据key查找节点;key可以是一系列的多个keySND_CONFIG_SEARCHV
snd_config_searchva在配置树中根据key查找节点,展开别名;key可以是连续多个keySND_CONFIG_SEARCHVA
snd_config_search_alias在配置树中根据key查找节点,展开别名.与snd_config_searcha类似,但是只能在config下查找。如果config下找不到id,则函数会尝试寻找base.idSND_CONFIG_SEARCH_ALIAS
snd_config_search_hooks在配置树中根据key查找节点,并且展开hooks。与snd_config_search类似,但是搜索的任何包含hooks的节点都会被各自的hooks函数修改SND_CONFIG_SEARCH
snd_config_searcha_hooks在配置树中根据key查找节点,并且展开alias与hooksSND_CONFIG_SEARCHA
snd_config_searchva_hooks在配置树中根据key查找节点,并且展开alias与hooks。与snd_config_searcha_hooks类似但是key可以是一系列的keySND_CONFIG_SEARCHVA
snd_config_search_alias_hooks在配置树中根据key查找节点,并且展开alias与hooks。与snd_config_search_alias相似,并且展开hooks与snd_config_search_hooks相似SND_CONFIG_SEARCH_ALIAS

1.3.2 snd_config_expand

使用参数及函数展开一个配置节点。如果传入的这个节点中有参数(通过一个id为@args的子节点定义),
则这个函数会用各自的参数值,或默认的参数值或者空来取代任何以$开头的string节点。
而且任何函数都会被评估(参考snd_config_evaluate),结果的副本将会在result中返回。
这里评估(evaluated)的意思比较模糊,从代码分析上看应该是所有的函数都被执行了。

int snd_config_expand(snd_config_t *config, snd_config_t *root, const char *args,snd_config_t *private_data, snd_config_t **result)
{int err;snd_config_t *defs, *subs = NULL, *res;//寻找参数//前面已分析过err = snd_config_search(config, "@args", &defs);if (err < 0) {if (args != NULL) {SNDERR("Unknown parameters %s", args);return -EINVAL;}//创建config的副本到res,注意是深层拷贝//也就是说如果config是复合节点//它的子节点也会被拷贝//详细见下文分析err = snd_config_copy(&res, config);if (err < 0)return err;} else {//如果找到参数,则直接创建一个top节点//前文已分析err = snd_config_top(&subs);if (err < 0)return err;//把defs里面的"default“节点添加到空的subs里面//详细见下文分析err = load_defaults(subs, defs);if (err < 0) {SNDERR("Load defaults error: %s", snd_strerror(err));goto _end;}//解析参数args//太太太复杂err = parse_args(subs, args, defs);if (err < 0) {SNDERR("Parse arguments error: %s", snd_strerror(err));goto _end;}//在运行时评估一个配置节点err = snd_config_evaluate(subs, root, private_data, NULL);if (err < 0) {SNDERR("Args evaluate error: %s", snd_strerror(err));goto _end;}             }err = snd_config_walk(config, root, &res, _snd_config_expand, subs);if (err < 0) {SNDERR("Expand error (walk): %s", snd_strerror(err));goto _end;}}err = snd_config_evaluate(res, root, private_data, NULL);if (err < 0) {SNDERR("Evaluate error: %s", snd_strerror(err));snd_config_delete(res);goto _end;}*result = res;err = 1;_end:if (subs)snd_config_delete(subs);return err;
}

1.3.2.1 snd_config_evaluate

在运行时评估一个函数,此函数会评估配置树中的任何一个函数(@func),
并用各自函数的结果替换这些节点。这里的评估应该时计算的意思。

int snd_config_evaluate(snd_config_t *config, snd_config_t *root,snd_config_t *private_data, snd_config_t **result)
{/* FIXME: Only in place evaluation is currently implemented */assert(result == NULL);return snd_config_walk(config, root, result, _snd_config_evaluate, private_data);
}

1.3.2.2 snd_config_walk

这里面传入的回调函数为_snd_config_evaluate,函数本身又会有递归,
大概目的就是一步一步查找func,找到并执行,并且由于创建了新的配置树,
会把执行函数后的节点信息替换掉原来的节点。

static int snd_config_walk(snd_config_t *src,snd_config_t *root,snd_config_t **dst,snd_config_walk_callback_t callback,snd_config_t *private_data)
{int err;snd_config_iterator_t i, next;switch (snd_config_get_type(src)) {case SND_CONFIG_TYPE_COMPOUND:err = callback(src, root, dst, SND_CONFIG_WALK_PASS_PRE, private_data);if (err <= 0)return err;snd_config_for_each(i, next, src) {snd_config_t *s = snd_config_iterator_entry(i);snd_config_t *d = NULL;err = snd_config_walk(s, root, (dst && *dst) ? &d : NULL,callback, private_data);if (err < 0)goto _error;if (err && d) {err = snd_config_add(*dst, d);if (err < 0)goto _error;}}err = callback(src, root, dst, SND_CONFIG_WALK_PASS_POST, private_data);if (err <= 0) {_error:if (dst && *dst)snd_config_delete(*dst);}break;default:err = callback(src, root, dst, SND_CONFIG_WALK_PASS_LEAF, private_data);break;}return err;
}

1.3.2.2 _snd_config_evaluate

具体执行函数,太复杂,即有循环,又有递归,注意里面的snd_dlopen
snd_dlsym,会从动态库中根据func符号找到函数,并且执行。
所以func其实是执行了,evaluate可以理解为计算的意思。

static int _snd_config_evaluate(snd_config_t *src,snd_config_t *root,snd_config_t **dst ATTRIBUTE_UNUSED,snd_config_walk_pass_t pass,snd_config_t *private_data)
{int err;if (pass == SND_CONFIG_WALK_PASS_PRE) {char *buf = NULL;const char *lib = NULL, *func_name = NULL;const char *str;int (*func)(snd_config_t **dst, snd_config_t *root,snd_config_t *src, snd_config_t *private_data) = NULL;void *h = NULL;snd_config_t *c, *func_conf = NULL;err = snd_config_search(src, "@func", &c);if (err < 0)return 1;err = snd_config_get_string(c, &str);if (err < 0) {SNDERR("Invalid type for @func");return err;}assert(str);err = snd_config_search_definition(root, "func", str, &func_conf);if (err >= 0) {snd_config_iterator_t i, next;if (snd_config_get_type(func_conf) != SND_CONFIG_TYPE_COMPOUND) {SNDERR("Invalid type for func %s definition", str);err = -EINVAL;goto _err;}snd_config_for_each(i, next, func_conf) {snd_config_t *n = snd_config_iterator_entry(i);const char *id = n->id;if (strcmp(id, "comment") == 0)continue;if (strcmp(id, "lib") == 0) {err = snd_config_get_string(n, &lib);if (err < 0) {SNDERR("Invalid type for %s", id);goto _err;}continue;}if (strcmp(id, "func") == 0) {err = snd_config_get_string(n, &func_name);if (err < 0) {SNDERR("Invalid type for %s", id);goto _err;}continue;}SNDERR("Unknown field %s", id);}}if (!func_name) {int len = 9 + strlen(str) + 1;buf = malloc(len);if (! buf) {err = -ENOMEM;goto _err;}snprintf(buf, len, "snd_func_%s", str);buf[len-1] = '\0';func_name = buf;}h = snd_dlopen(lib, RTLD_NOW);if (h)func = snd_dlsym(h, func_name, SND_DLSYM_VERSION(SND_CONFIG_DLSYM_VERSION_EVALUATE));err = 0;if (!h) {SNDERR("Cannot open shared library %s", lib);err = -ENOENT;goto _errbuf;} else if (!func) {SNDERR("symbol %s is not defined inside %s", func_name, lib);snd_dlclose(h);err = -ENXIO;goto _errbuf;}_err:if (func_conf)snd_config_delete(func_conf);if (err >= 0) {snd_config_t *eval;err = func(&eval, root, src, private_data);if (err < 0)SNDERR("function %s returned error: %s", func_name, snd_strerror(err));snd_dlclose(h);if (err >= 0 && eval) {/* substitute merges compound members *//* we don't want merging at all */err = snd_config_delete_compound_members(src);if (err >= 0)//替换节点err = snd_config_substitute(src, eval);}}_errbuf:free(buf);if (err < 0)return err;return 0;}return 1;
}

至此,snd_config_expand的功能为展开节点,从节点中搜索函数,逐个执行,并最终用执行结果替换掉原来的节点。
snd_config_search_definition则查找某个定义,查找到后使用snd_config_expand去展开执行。
根据读取alsa的默认配置文件alsa.conf,通常会构建一个func load函数,再用构造出的函数去load配置文件。
里面又是一大堆递归。总之alsa配置的目的就是通过在配置文件中修改配置,即可控制运行时的函数执行。
为了实现这个目标,alsa丧心病狂的实现了众多嵌套,递归,为了实现代码复用,采用了众多的宏,整体让alsa变得及其复杂。

比如alsa.conf中的配置:

@hooks [{func loadfiles ["/etc/alsa/conf.d""/etc/asound.conf""~/.asoundrc"]errors false}
]

通过这个hooks,实际上构造了snd_config_hooks_load函数,运行时会解析符号并从动态库中找到此函数,
用来加载接下来的三个配额文件,"/etc/alsa/conf.d""/etc/asound.conf","~/.asoundrc"

至此snd_config_update_r的大致功能已经比较清晰了,从配置文件中读取配置文件,解析为配置树,
执行所有的hooks,其中可能又会包含读取配置文件,解析为配置树。执行所有的hooks函数,重新更新配置树。
返回最后的配置树。

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

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

相关文章

瑞格智慧心理服务平台 NPreenSMSList.asmx SQL注入漏洞复现

0x01 产品简介 瑞格智慧心理服务平台是一家致力于提供个性化心理健康支持的平台。通过先进的AI技术和专业心理学家团队,为用户提供定制化的心理评估和个性化的心理咨询服务。平台注重隐私保护和数据安全,用户可以安全、便捷地接受在线咨询和心理指导,帮助他们理解和应对各种…

MATLAB口罩检测

在当今疫情严峻的背景下&#xff0c;口罩成为了人们必备的防护用品。然而&#xff0c;市面上却出现了大量假冒伪劣口罩。为了帮助大家准确辨别真假口罩&#xff0c;小编将为大家介绍一种基于MATLAB的口罩检测方法。 1.口罩检测原理 通过图像处理技术&#xff0c;借助MATLAB强大…

一款实用的Word文档图片转换与水印保护工具

目录 前言软件功能简介软件实现方法及关键代码 1. Word 文档转图片的实现2. 图片水印添加功能3. 生成数字指纹&#xff08;哈希值&#xff09;4. 保存图片信息到 JSON 文件 软件的实际使用场景软件操作指南 1. 下载和安装2. 操作流程 总结 1&#xff0c;前言 在日常办公和内…

【刷题13】链表专题

目录 一、两数相加二、两两交换链表的节点三、重排链表四、合并k个升序链表五、k个一组翻转链表 一、两数相加 题目&#xff1a; 思路&#xff1a; 注意整数是逆序存储的&#xff0c;结果要按照题目的要求用链表连接起来遍历l1的cur1&#xff0c;遍历l2的cur2&#xff0c;和…

消息队列面试——打破沙锅问到底

消息队列的面试连环炮 前言 你用过消息队列么&#xff1f;说说你们项目里是怎么用消息队列的&#xff1f; 我们有一个订单系统&#xff0c;订单系统会每次下一个新订单的时候&#xff0c;就会发送一条消息到ActiveMQ里面去&#xff0c;后台有一个库存系统&#xff0c;负责获取…

Linux 下执行定时任务之 Systemd Timers

不知道 ECS 因为什么缘故&#xff0c;上面安装的 MySQL 服务老是不定期挂掉&#xff0c;本来想通过 Linux 得 Cron 配置个半小时的定时检测任务&#xff0c;结果一直没有执行&#xff0c;因此又尝试使用了 Systemd Timers 进行了重新配置&#xff0c;简要做个记录。 Systemd Ti…

yocto中如何来安装systemd服务

在 Yocto 项目中安装 systemd 服务可以按照以下步骤进行&#xff1a; 1. 创建服务单元文件&#xff08;.service 文件&#xff09; 在 Recipe 中处理&#xff1a;在 Yocto 中&#xff0c;为了将服务单元文件安装到目标系统&#xff0c;首先需要在软件包的 Recipe&#xff08;…

UE5相机系统初探(一)

UE5相机系统初探&#xff08;一&#xff09; 和Unity类似&#xff0c;UE的相机也是由名为Camera的component控制的。那么&#xff0c;在UE中要如何实现一个跟随玩家的第三人称相机呢&#xff1f;假设我们已经有了一个表示玩家的类ACF_Character&#xff0c;首先第一步就是要先在…

【python】OpenCV—Tracking(10.4)—Centroid

文章目录 1、任务描述2、人脸检测模型3、完整代码4、结果展示5、涉及到的库函数6、参考 1、任务描述 基于质心实现多目标&#xff08;以人脸为例&#xff09;跟踪 人脸检测采用深度学习的方法 核心步骤&#xff1a; 步骤#1&#xff1a;接受边界框坐标并计算质心 步骤#2&…

萤火虫算法优化BILSTM神经网络多输入回归分析

目录 LSTM的基本定义 LSTM实现的步骤 BILSTM神经网络 代码 结果分析 展望 完整代码下载:的MATALB代码(代码完整,数据齐全)资源-CSDN文库 https://download.csdn.net/download/abc991835105/88755564 背影 bp神经网络是一种成熟的神经网络,应用非常广,本文用萤火虫算法…

通过嵌套循环输出二维矩阵

输出以下4*5的矩阵。 1 2 3 4 52 4 6 8 103 6 9 12 154 8 12 16 20 输入格式: 无。 输出格式: 二维矩阵 代码如下&#xff1a; #include <stdio.h> int main() {for (int i 1; i < 4; i) {//行for (int j 1; j < 5; …

【maven】idea执行了maven的install命令给本地安装项目依赖包 安装后删除

目录 事件起因环境和工具操作过程解决办法1、找到对应的目录下的文件&#xff0c;手动去删除&#xff0c;比如我的依赖库的路径是D:\qc_code\apache-maven-3.8.2\repository 我只需要找到这个目录下对应的依赖包进行手动删除即可&#xff08;不推荐&#xff0c;强行删除文件夹文…

PostgreSQL 到 PostgreSQL 数据迁移同步

简述 PostgreSQL 是一个历史悠久且广泛使用的数据库&#xff0c;不仅具备标准的关系型数据库能力&#xff0c;还具有相当不错的复杂 SQL 执行能力。用户常常会将 PostgreSQL 应用于在线事务型业务&#xff0c;以及部分数据分析工作&#xff0c;所以 PostgreSQL 到 PostgreSQL …

Java项目实战II基于Java+Spring Boot+MySQL的智能推荐的卫生健康系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 基于Java、…

免费插件集-illustrator插件-Ai插件-闭合开放路径

文章目录 1.介绍2.安装3.通过窗口>扩展>知了插件4.功能解释5.总结 1.介绍 本文介绍一款免费插件&#xff0c;加强illustrator使用人员工作效率&#xff0c;实现图形编辑中闭合开放路径。首先从下载网址下载这款插件https://download.csdn.net/download/m0_67316550/8789…

WPF 实现冒泡排序可视化

WPF 实现冒泡排序可视化 实现冒泡排序代码就不过多讲解,主要是实现动画效果思路,本demo使用MVVM模式编写,读者可自行参考部分核心代码,即可实现如视频所示效果。 对于新手了解算法相关知识应该有些许帮助,至于其它类型排序,也可按该思路自行修改实现。 直接上代码,页面布…

老电脑不能装纯净版windows

手上有一台2016年的老笔记本电脑&#xff0c;用了8年&#xff0c;基本上能换的都换了一遍&#xff0c;散热风扇换了&#xff0c;键盘换了&#xff0c;屏幕换了&#xff0c;扬声器也换了&#xff0c;内存也换大了&#xff0c;甚至都换过固态硬盘&#xff0c;但是CPU没法换&#…

angular登录按钮输入框监听

说明&#xff1a;angular实现简单的登录页面&#xff0c;监听输入框的值&#xff0c;打印出来&#xff0c;按钮监听&#xff0c;打印数据 效果图: step1:E:\projectgood\ajnine\untitled4\src\app\app.config.ts import { ApplicationConfig, provideZoneChangeDetection } …

Java基于SpringBoot+Vue框架的房屋租赁管理系统(附源码,文档)

博主介绍&#xff1a;✌Java徐师兄、7年大厂程序员经历。全网粉丝13w、csdn博客专家、掘金/华为云等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb; 不…

【前端】CSS知识梳理

基础&#xff1a;标签选择器、类选择器、id选择器和通配符选择器 font:font-style(normal) font-weight(400) font-size(16px) /line-height(0) font-family(宋体&#xff09; 复合&#xff1a; 后代选择器&#xff08; &#xff09;、子选择器&#xff08;>)、并集选择器(…