编程精粹—— Microsoft 编写优质无错 C 程序秘诀 06:危险的行业

news2025/1/10 23:36:40

这是一本老书,作者 Steve Maguire 在微软工作期间写了这本书,英文版于 1993 年发布。2013 年推出了 20 周年纪念第二版。我们看到的标题是中译版名字,英文版的名字是《Writing Clean Code ─── Microsoft’s Techniques for Developing》,这本书主要讨论如何编写健壮、高质量的代码。作者在书中分享了许多实际编程的技巧和经验,旨在帮助开发人员避免常见的编程错误,提高代码的可靠性和可维护性。


不记录,等于没读。本文记录书中第六章内容:危险的行业。

鉴于一个函数有多种实现可能性,不同实现的出错率有所不同也就不足为奇了。编写健壮函数的关键在于用已被证明同样高效但更安全的替代方案,来取代那些风险较高的算法和语言习惯。在极端情况下,这可能意味着使用明确的数据类型;在另一个极端情况下,这可能意味着抛弃整个设计,只因为它难以测试或无法测试。

当程序员有几种可能的实现方案时,他们却经常只考虑空间和速度,而完全忽视了风险性。假如你站在悬崖旁边,想要到达悬崖的底部,你会从悬崖直接跳下吗?毕竟这可是最快到达目的地的方法。

使用有严格定义的数据类型

ANSI在制定C语言标准时,有一些指导原则:

  1. 现存代码是非常重要的。不能定义过于严格的标准,这样会使大量现存代码无效。

    Existing code is important.

  2. 保持C语言的精神:即使不能保证可移植,也要使其快速。

    Keep the spirit of C: Make it fast,even if it is not guaranteed to be portable.

基于上面的准则,ANSI 标准没有定义固有数据类型(intrinsic data types,像 charintlong 这些数据类型),而是让编译器厂家定义详细的数据类型细节。

对于数据类型 char,有的编译器默认为无符号的,也有编译器默认为有符号的,这都符合ANSI标准。没有严格定义的数据类型会导致可移植问题。

ANSI虽然没有定义固有数据类型,但给出了数据类型必须遵循的最小要求。仔细分析 ANSI 标准,可以推导出可移植数据类型集的定义:

char0 ~ 127
signed char-127(并非-128) ~ 127
unsigned char0 ~255
short-32767(并非-32768) ~ 32768
signed short-32767 ~ 32767
unsigned short0 ~ 65535
int-32767(并非-32768) ~ 32768
signed int-32767 ~ 32767
unsigned int0 ~ 65535
long-2147483647(并非-2147483648) ~ 2147483647
signed long-2147483647 ~ 2147483647
unsigned long0 ~ 4294967295
int i : n0 ~ 2n-1 -1
signed int i : n-(2n-1 -1) ~ 2n-1 -1
unsigned int i : n0 ~ 2n -1

使用可移植的数据类型

int 类型不具备可移植性。它与硬件的位宽相同,16 位的机器上,int 变量占用 2 个字节,32 位的机器上占用 4 个字节。所以如果可以使用 long 类型,就不要使用 int 类型,即使将来的硬件设备使用 long 类型效率会低一些,也应该坚持使用可移植类型。

本书英文版发布于 1993 年,那时候还没有推出 C99 标准。现在,可移植的数据类型已经不再是问题,因为 C99 引入了新的标准头文件 stdint.h,它提供了明确的、固定宽度的整数类型。

总是问自己:这个变量或者表达式会溢出吗?

上溢:

unsigned char ch;

for(ch = 0; ch <= UCHAR_MAX; ch++)		//因为ch的上溢,ch <= UCHAR_MAX恒成立, 这里可能是死循环
{
    ...			
}

下溢:

size_t size;

while(--size >= 0)						//因为size的下溢,size >= 0恒成立,这里可能是死循环
{
    ...
}

为什么在上面的注释中写的是“可能是死循环”?

这是因为循环体可能有提前结束循环的条件,然后 returnbreak 离开循环体。这种情况会使得错误更难发现。

避免无关紧要的 if 语句

DOS时代也有图形化界面的。用户看到的图形界面是一系列窗口的集合。这些窗口是有层次的,它们有一个相同的根窗口。根窗口下面有一系列子窗口,其中每个子窗口下面可能还有一些列子窗口。这样在移动、删除、最小化一个窗口的时候,会将这个窗口以及它相关的窗口一起变动。

为了表示窗口层次结构,使用了二叉树结构。二叉树的一个分支指向子窗口,称为为 子节点(children),另一个分支指向具有相同父窗口的窗口,称为同级节点

typedef struct WINDOW 
{ 
 	struct WINDOW *pwndChild;  		/* 如果没有子节点,则为 NULL */ 
 	strcut WINDOW *pwindSibling;  	/* 如果没有同级节点,则为 NULL */ 
	char *strWndTitle;} window; 						   /* 命名:wnd, *pwnd */

向二叉树中插入子窗口,有以下三种代码,比较它们的不同。

第一种代码:

/* pwndRootChildren 是一个指针,指向顶层窗口的列表,例如象菜单框和主文件窗口 */ 
static window *pwndRootChildren = NULL; 

void AddChild( window *pwndParent, window *pwndNewBorn ) 
{ 
	/* 新窗口不会有同级窗口 */ 
	ASSERT( pwndNewBorn->pwndSibling == NULL ); 
    
	if( pwndParent == NULL ) 
	{ 
		/* 将窗口加入到顶层根列表 */ 
		pwndNewBorn->pwndSibling = pwndRootChildren; 
		pwndRootChildren = pwndNewBorn; 
	} 
	else 
	{ 
		/* 如果是父节点的第一个子节点,则在子节点字段存储新窗口,
		   开启新的同级节点链表, 否则加到现存同级节点链的末尾处 */ 
		if( pwndParent -> pwndChild == NULL ) 
		{
			pwndParent -> pwndChild = pwndNewBorn; 
		}
		else 
		{ 
			window *pwnd = pwndParent -> pwndChild; 
			while( pwnd -> pwndSibling != NULL) 
				pwnd = pwnd -> pwndSibling; 
			pwnd -> pwndSibling = pwndNewBorn; 
		} 
	} 
}

这个函数的作用是插入新的窗口,但程序做了它所需工作的 3 倍:判断是否根窗口、判断是否是父节点的第一个子节点和插入新的窗口。引起这个情况的主要原因是设计上的不合理。

由于根窗口没有同级窗口也不会移动、最小化、删除等操作(以现在的眼光看,那时的DOS图形应用程序就是这样的弱,不支持多应用同开),在 window 结构中只有 pwndChild 字段才有意义。因此设计人员为了节省一点内存,没有什么完整的 window 类型对象,而是用指向顶层窗口的指针 pwndRootChildren 来代替。

这样做会给代码实现带来巨大的麻烦,很多地方不得不处理两种数据结构,虽然窗口结构设计为二叉树,但并不是按照二叉树结构实现的。

改进代码的第一步非常容易,砍掉内存“优化”,使用 pwndDisplay 代替 pwndRootChildren 指针。pwndDisplay 是一个指针,指向表示显示的窗口结构。在根窗口节点下面插入窗口时,再也不用传递特殊意义的 NULL 了,因为改进后只需要传递 pwndDisplay,这样可以省掉一种处理根窗口的专用代码判断。

第二种代码:

/* pwndDisplay 指向根窗口,根窗口在程序初始化过程中分配内存 */ 
window *pwndDisplay = NULL; 

void AddChild( window *pwndParent, window *pwndNewBorn ) 
{ 
	/* 新窗口不会有同级窗口 */ 
  	ASSERT( pwndNewBorn -> pwndSibling == NULL ); 
    
	/* 如果是父节点的第一个子节点,则在子节点字段存储新窗口,
	   开启新的同级节点链表, 否则加到现存同级节点链的末尾处 */
  	if( pwndParent -> pwndChild == NULL) 
  	{
      	pwndParent -> pwndChild = pwndNewBorn; 
  	}	
  	else 
	{ 
   		window *pwnd = pwndParent -> pwndChild; 
		while( pwnd -> pwndSibling != NULL ) 
			pwnd = pwnd -> pwndSibling; 
		pwnd -> pwndSibling = pwndNewBorn; 
	} 
} 

第二个版本要比第一个版本好些,但仍做了它所需工作的 2 倍。在你的头脑中应该有一个警戒线:一旦看到 if 语句就要引发报警,核查你是否在执行两次相同的工作,尽管执行方式不同。有些场合需要合法使用if执行一些条件操作,但大多数情况下,这是草率设计、粗心实现的结果:因为写出设计良好、实现也良好的代码非常困难,而且人们也往往喜欢走容易走的道路。

第三种代码:

void AddChild(window *pwndParent, window *pwndNewBorn ) 
{ 
	window **ppwindNext; 
    
	/* 新窗口不会有同级窗口 */  
	ASSERT( pwndNewBorn -> pwndSibling == NULL ); 
    
	/* 使用以指针为中心的算法 */
	ppwndNext = &pwndParent->pwndChild; 
	while( *ppwndNext != NULL ) 
		ppwndNext = &( *ppwndNext )->pwndSibling; 
	*ppwndNext = pwndNewBorn; 
} 

第三个版本消除了if语句。避免无关紧要的 if 语句

有意思的是,作者虽然一直强调避免无关紧要的 if 语句,但是在附录 B 内存日志例程给出的源码中,也存在无关紧要的 if 语句。

内存日志的作用在为子系统设防一节中详细讲解过。它用一个链表保存内存信息,包括内存地址、大小、是否引用等。当删除一个内存块时,也要把内存日志中保存的信息块删除掉,这个内容在函数 FreeBlockInfo 中实现。作者给出的实现方式要使用一个变量 pbiPrev 来保存前一个节点位置,并且要处理删除的是第一个节点 A 这种边界条件,这和他批评的代码实现方式很像:

void FreeBlockInfo(byte *pbToFree)
{
	blockinfo *pbi, *pbiPrev;

 pbiPrev = NULL;
	for(pbi = pbiHead; pbi != NULL; pbi = pbi->pbiNext)
	{
		if(fPtrEqual(pbi->pb, pbToFree)
		{
			if(pbiPrev == NULL)
				pbiHead = pbi->pbiHead;
			else
				pbiPrev->pbiNext = pbi->pbiNext;
			break;
		}
		pbiPrev = pbi;
	}

	/*如果pbi是NULL, 说明参数pbToFree非法*/
	ASSERT(pbi != NULL);

	/*在释放前破坏掉要释放内存中的内容*/
	memset(pbi, bGarbage, sizeof(blockinfo));
	free(pbi);
}

有一种惯用的用法可以巧妙的省掉变量pbiPrev以及边界判断,那就是使用二级指针:

void FreeBlockInfo(byte *pbToFree)
{
 blockinfo **ppbi, *pbiFind;

 pbiFind = NULL;
 for(ppbi = &pbiHead; *ppbi != NULL; ppbi = &(*ppbi)->pbiNext)
 {
     if(fPtrEqual((*ppbi)->pb, pbToFree)
	{
         pbiFind = *ppbi;
         *ppbi = (*ppbi)->pbiNext;
         break;
     }
 }

 /*如果pbiFind是NULL, 说明参数pbToFree非法*/
	ASSERT(pbiFind != NULL);

	/*在释放前破坏掉要释放内存中的内容*/
	memset(pbiFind, bGarbage, sizeof(blockinfo));
	free(pbiFind);
}

作者没有用最佳的实现方法编写 FreeBlockInfo 函数,恰恰证明了编写优质代码是多么的困难!

避免使用嵌套的 ?:

函数 uCycleCheckBox 用于返回对话框的下一个状态值,参数是当前状态值。状态值有两种循环,可能在 0->1->0->… 内循环变化,也可能是在 2->3->4->2->3->… 内循环变化。

第一种代码:

unsigned uCycleCheckBox(unsigned uCur) 
{ 
 	return( (uCur<=1)? (uCur? 0:1): (uCur==4)? 2:(uCur+1) ); 
} 

嵌套使用?:的代码不具有可读性.

第二种代码:

usigned uCycleCheckBox(unsigned uCur) 
{ 
	unsigned uRet; 
	if(uCur <= 1) 
	{ 
		if(uCur != 0) /* 处理 0,1,0 …… 循环 */ 
			uRet = 0; 
		else 
			uRet = 1; 
	} 
	else 
	{ 
		if(uCur == 4) /* 处理 2,3,4,2 …… 循环 */ 
			uRet = 2; 
		else 
			uRet = uCur + 1; 
	} 
	return(uRet) 
} 

?: 改为 if 语句.

第三种代码:

unsigned uCycleCheckBox( unsigned uCur ) 
{ 
	unsigned uRet; 
	if( uCur <= 1 ) 
	{ 
		uRet = 0;   /* 处理 0,1,0 …… 循环 */ 
		if( uCur == 0 ) 
			uRet = 1; 
	} 
	else 
	{ 
		cuRet = 2; /* 处理 2,3,4,2 …… 循环 */ 
		if( uCur != 4 ) 
			uRet = uCur + 1; 
	} 
	return( uRet ); 
} 

认真看下这三种版本的代码,他们都不具备可读性:一眼看过去,代码意图不够明显。虽然这些函数都能正确的维护两个循环,但实现方式就像用废机油清理发动机一样自欺欺人(没有实质上的改进)。它们的本质都是相同的,只不过是 3 种稍微不同的实现方式。完全可以写出更好的实现代码,只需要真正的思考,而不是只停留在表面。

第四种代码:

unsigned uCycleCheckBox( unsigned uCur ) 
{ 
 	ASSERT( uCur >= 0 && uCur <= 4 ); 
    
  	if( uCur == 1 )   		/* 重新开始第一个循环?*/ 
		return( 0 ); 
 	if( uCur == 4 )    		/* 重新开始第二个循环?*/ 
		return( 2 ); 
 	return( uCur + 1 );   	/* 这时没有任何特殊处理 */ 
} 

第五种代码:

unsigned uCycleCheckBox( unsigned uCur ) 
{ 
  	static const unsigned uNextState[] ={1,0,3,4,2 }; 
    
  	ASSERT( uCur >= 0 && uCur <= 4 ); 
    
	return ( uNextState[uCur] ); 
} 

虽然第五种代码没有第四种那么容易理解,但它是最简洁、执行速度最快的代码。第五种代码需要添加注释,告诉别人为什么要在数组中存储那些数字。

每种特殊情况只能处理一次

也就是编程界中最重要的基本原则之一:尽一切可能消除重复

不要过高的估计代价

Macintosh 操作系统进行版本升级后,Excel 不能正常工作了。Apple 请求 Microsoft 删除过时的工作区以保持与最新的操作系统一致。

但是,删除 Excel 的工作区就意味着要重写关键的手工优化的汇编函数。重写后的代码会增加 12 个指令周期。因为函数很关键,关于是否重写函数的争论持续了很久。一部分人认为要与 Apple 保持一致,另一部分人则要保持速度。

最后,一个程序员在这个函数中放入一个临时计数器,然后运行 Excel,进行了三个小时的高强度测试,考察这个函数被调用了多少次。这个数字很大,有 76000 次。虽然这个数字很大,但是重写函数并执行 12 个额外指令周期 76000 次,也只不过增加 0.1 秒,这还是在最慢的 Macintosh 电脑上得出的结果。有了这些发现,代码很快进行了更改。

这个例子说明了:关心局部效率是不值得的。如果你很注重效率的话,请集中于全局和算法的效率上,这样你才会看到努力的效果。这个例子还说明了一个问题,在《代码整洁之道-程序员的职业素养》一书中对此进行了描述:

凡是不能在 5 分钟内解决的争论,都不能靠辩论解决

争论之所以要花这么多时间,是因为各方都拿不出足够有力的证据。如果观点无法在短时间里达成一致,就永远无法达成一致。唯一的出路是,用数据说话。

消除不一致

很多场合需要用到字符流,比如两个设备间通讯。向对方传送一个 short 型数据,需要将这个数据拆分成两个字节,然后根据协议决定先发送低字节还是高字节。在接收方,需要将两个字节数据合并成一个 short 型数据,这里给出 3 种代码。

第一种代码:

word = high << 8 + low ; 

这个代码是错的。因为忽略了运算符 + 优先级大于 << 。即便写成 word = (high << 8) + low ;也不是理想代码,因为混用了位操作符和算术操作符。如果只使用位操作符或者算术操作符,出错的概率就要小一些,因为凭直觉,同一类操作符的优先级容易掌握。

第二种代码:

word = high * 256 + low;  	/* 算术解法 */ 

第三种代码:

word = high << 8 | low;  	/* 移位解法 */ 

小结

  • 在选择数据类型的时候要谨慎。虽然 ANSI 标准要求所有的执行程序都要支持 char、int、long 等类型,但是它并没有具体定义这类型。为了避免程序出错,应该只按照 ANSI 的标准选择数据类型。
  • 存在这种可能,你的算法正确,但是因为运行在指标不理想的硬件上,也会产生 BUG。所以要经常检查计算结果和测试结果,避免你的数据类型范围上溢或下溢。
  • 代码如实的反映你的设计。引入细微错误的最简单方法就是代码与设计不符。
  • 一个函数只做一件事。避免不必要的分支,用一条路径完成这件事。不管什么输入都执行相同的代码,就会降低出现遗留 BUG的概率。
  • if 语句是个特别好的警告信号,表明你可能正在做不必要的工作。问自己:为了去除这个特殊条件,我如何更改设计?然后努力消除每一个不必要的if语句。有时需要修改你的数据结构,有时又要改变自己看问题的方式。透镜是凸起的还是凹下的,取决于你在那一面观察。
  • 不要忘记,if 语句有时会隐藏在 whilefor 循环的控制表达式中。?: 操作符是 if 的另一种表达形式。
  • 警惕有风险的编程惯用语,比如用移位代替除法等。关注那些类似但更安全的惯用语。要特别注意那些可能会给你带来更好性能的代码微调。
  • 一个表达式中尽可能只用相同类型的运算符。如果必须混用运算符,使用小括号把它们分开。
  • 错误处理是特殊情况中的特殊情况。只要有可能,就不要调用会返回失败情况的函数。如果必须要调用会返回错误值的函数,尝试把错误处理本地化——这样能增加发现错误处理代码中的 BUG 的机会。
  • 有时候,通过确保你想做的事情不会失败,来消除一般的错误处理,这是有可能的。这可能需要在初始化期间处理一次错误,或者从根本上改变你的设计。






每一份打赏,都是对创作者劳动的肯定与回报。
千金难买知识,但可以买好多奶粉

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

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

相关文章

Mac安装多个jdk环境(jdk8+jdk17)保姆级

Mac安装多个jdk环境&#xff08;jdk8jdk17&#xff09;保姆级 背景&#xff1a;新机安装开发环境发现需要找很多文章&#xff0c;&#xff0c;&#xff0c;&#xff0c;这里一篇文章安装所有环境 文章目录 Mac安装多个jdk环境&#xff08;jdk8jdk17&#xff09;保姆级&#x1f…

基于springboot实现火车票订票系统项目【项目源码+论文说明】

基于springboot实现火车票订票系统演示 摘要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装火车票订票系统软件来…

【SpringCloud】Eureka的简单使用

本文使用的是jdk17&#xff0c;mysql8。 以下用两个服务做演示&#xff1a; 订单服务&#xff1a;提供订单ID&#xff0c;获取订单详细信息。 商品服务&#xff1a;提供商品ID&#xff0c;获取商品详细信息。 对于上篇http://t.csdnimg.cn/vcWpo 订单服务调用商品服务的时候&a…

一文读懂 HTTP 和 RPC 的区别

随着互联网技术的发展&#xff0c;网络通信在各种应用中扮演着至关重要的角色。无论是构建 Web 应用还是进行服务之间的交互&#xff0c;选择合适的通讯协议成为开发者们需要深入思考的问题。在众多协议中&#xff0c;HTTP&#xff08;HyperText Transfer Protocol&#xff09;…

JavaSE 面向对象程序设计进阶 抽象类和接口 2024年详解

目录 抽象类 抽象方法 抽象类和抽象方法的注意事项 ​编辑 接口 如何定义接口 注意 代码实现 ​编辑 接口中的成员特点 接口和类之间的关系 1.类与类的关系 2.类与接口的关系 3.接口与接口的关系 ​编辑 拓展 接口中的默认方法 接口中的静态方法 ​编辑 接口…

全新升级微信分销商城小程序源码系统 前后端分离 带完整的安装代码包以及搭建部署教程

系统概述 微信分销商城小程序源码系统是基于先进的技术和理念开发而成的。它旨在为企业和商家打造一个功能齐全、用户体验良好的分销平台&#xff0c;帮助他们更好地管理商品、销售渠道和用户关系&#xff0c;实现业务的快速增长和持续发展。 代码示例 系统特色功能一览 1.多…

TikTok API接口——获取TikTok用户QRcode二维码

一、引言 在数字化时代&#xff0c;QRcode二维码已经成为连接线上线下的重要桥梁。在社交媒体领域&#xff0c;TikTok作为短视频领域的佼佼者&#xff0c;用户量庞大且活跃度高。为了满足用户之间更便捷的互动需求&#xff0c;我们特别开发了一款针对TikTok平台的接口&#xf…

C++并发之协程实例(二)(计算斐波那契序列)

目录 1 协程2 实例-计算斐波那契序列2.1 斐波那契序列2.2 代码 3 运行 1 协程 协程(Coroutines)是一个可以挂起执行以便稍后恢复的函数。协程是无堆栈的&#xff1a;它们通过返回到调用方来暂停执行&#xff0c;并且恢复执行所需的数据与堆栈分开存储。这允许异步执行的顺序代码…

[图解]企业应用架构模式2024新译本讲解15-行数据入口

1 00:00:01,060 --> 00:00:02,770 数据算完了 2 00:00:03,070 --> 00:00:07,720 接下来就是我们这一节的主要内容了 3 00:00:08,500 --> 00:00:13,630 应用服务调用第三方的&#xff0c;Email 4 00:00:13,640 --> 00:00:18,280 包括集成应用的接口来发Email 5 …

【C++】————类和对象(上)

作者主页&#xff1a; 作者主页 本篇博客专栏&#xff1a;C 创作时间 &#xff1a;2024年6月21日 一、类与对象的初步认识 1、类其实就是对对象的抽象&#xff0c;而对象就是对类的具体实例 类不占用内存&#xff0c;而对象占用内存。 2、面向对象与面向过程 C语言是面…

技术探索:如何利用合合信息智能文档处理提升审查效率

官.网地址&#xff1a;合合TextIn - 合合信息旗下OCR云服务产品 智能文档处理技术是一系列技术的集合&#xff0c;旨在自动化地捕获、理解、处理和分析文档内容&#xff0c;以支持企业的数字化转型和提升文档处理效率。 智能文档处理技术的核心包括光学字符识别&#xff08;O…

【码银送书第二十一期】《大数据智能风控:模型、平台与业务实践》

人行印发的《金融科技&#xff08;FinTech&#xff09;发展规划&#xff08;2022一2025年&#xff09;》明确指出金融科技成为防范化解金融风险的利器&#xff0c;运用大数据、人工智能等技术建立金融风控模型&#xff0c;有效甄别高风险交易&#xff0c;智能感知异常交易&…

光电数鸡算法《java》

一&#xff1a;需求 题目&#xff1a;一条流水线有工位D1,D2,D3…D20,总共20个工位。 每个工位都装有一个光电计数器&#xff0c;每个工位都为本工位的计数减去前一个工位&#xff08;第一个有数值的工位除外&#xff0c;不计算。&#xff09; 计算规则&#xff1a;比如D1,D2都…

Android CTS环境搭建

CTS即Compatibility Test Suite意为兼容性测试&#xff0c;是Google推出的Android平台兼容性测试机制。其目的是尽早发现不兼容性&#xff0c;并确保软件在整个开发过程中保持兼容性。只有通过CTS认证的设备才能合法的安装并使用Google market等Google应用。 搭建CTS测试环境需…

计算机网络:应用层 - 域名系统 DNS

计算机网络&#xff1a;应用层 - 域名系统 DNS 域名结构域名服务器域名解析迭代查询递归查询 互联网中的每台设备都有一个唯一的IP地址&#xff0c;但这些地址通常是复杂的数字组合&#xff0c;例如 172.217.160.142&#xff0c;难以记忆和识别。域名系统将这些复杂的IP地址与易…

通过腾讯云TDSQL TCPTCE(MySQL版)认证考试秘籍宝典

腾讯云TDSQL(MySQL版)交付运维高级工程师TCCP证书展示 腾讯云TDSQL(MySQL版)交付运维专家TCCE考试成绩、证书展示 认证类型与级别 TCCA:入门级(初级) TCCP:高级(中级) TCCE:专家级(高级) 考试形式 考试是在线考试&#xff0c;考生需要在腾讯云大学官网上完成。 腾讯云TDSQ…

【会议征稿】2024年应用计算智能、信息学与大数据国际会议(ACIIBD 2024,7月26-28)

2024年应用计算智能、信息学与大数据国际学术会议&#xff08;ACIIBD 2024&#xff09;将于2024年7月26-28日在中国广州举办。会议将聚焦于计算智能及其应用、信息、大数据等相关的研究领域&#xff0c; 广泛邀请国内外知名专家学者&#xff0c;共同探讨相关学科领域的最新发展…

深入理解和实现Windows进程间通信(信号量)

常见的进程间通信方法 常见的进程间通信方法有&#xff1a; 管道&#xff08;Pipe&#xff09;消息队列共享内存信号量套接字 下面&#xff0c;我们将详细介绍信号量的原理以及具体实现。 什么是信号量&#xff1f; 信号量&#xff08;Semaphore&#xff09;是一个非常重要…

【数据库】数据库脚本编写规范(Word原件)

编写本文档的目的是保证在开发过程中产出高效、格式统一、易阅读、易维护的SQL代码。 1 编写目的 2 SQL书写规范 3 SQL编写原则 软件全套资料获取进主页或者本文末个人名片直接获取。

OPPO布局自动驾驶?基于语义地图的自动驾驶汽车单目定位

论文标题&#xff1a; Monocular Localization with Semantics Map for Autonomous Vehicles 论文作者&#xff1a; Jixiang Wan, Xudong Zhang, Shuzhou Dong, Yuwei Zhang, Yuchen Yang, Ruoxi Wu, Ye Jiang, Jijunnan Li, Jinquan Lin, Ming Yang 作者单位&#xff1a;O…