C++,CString

一、为什么在使用String之前需要使用usingnamespacestd这样一句?EssentialC++告诉我标准程序库所提供的任何事物,都被封装在命名空间std内。这样子可以避免产生命名冲突。你看到这句话一定想要挖开里面的东东,看看Microsoft是如何做的很奇怪的我没有看到我所想要找的namespacestd{...}但是我注意到有_STD_BEGIN和_STD_END在几乎每个标准库头文件中。如是我想Microsoft的有宏定义嗜好的程序员一定又在用#define这个法宝。我只需查找_STD_BEGIN的定义就好了。再一次感谢Microsoft在VC中提供的FindinFiles...功能,听到硬盘一阵狂响后我看到了在Yvals.h中定义的STD家族的几行。(当然你也可以用GREP这个程序来查找,只是没有VC这个方便吧了!)#ifdefined(__cplusplus)#define_STDstd::#define_STD_BEGINnamespacestd{#define_STD_END};#define_STD_USING#else#define_STD::#define_STD_BEGIN#define_STD_END#endif/*__cplusplus*/

很明显,Microsoft已经将标准库封在命名空间std中了,所以下次我们要使用时一定不要忘记了加上usingnamespacestd;呵呵

二、关于list::list的定义说明几点(MicrsoftVC6.0版本)对于list的使用大家一定不会陌生,可是一定会对如何实现这些是一些疑惑的,我也是如此。我一直想知道list是如何实现其定义的。好的,首先我们看到MSDN中给出list::list的实现list::listexplicitlist(constA&al=A());explicitlist(size_typen,constT&v=T(),constA&al=A());list(constlist&x);list(const_iteratorfirst,const_iteratorlast,constA&al=A());说明list有四种初使化方式,但我对于这四个定义有几个问题1.explicit是作什么用的?explicit的中文意义是"外在的",其反意词是implicit那么它有什么特点,让我们来看一个例子(MSDN中有)classX{public:explicitX(int);file://正确explicitX(double){file://正确//...}};

explicitX::X(int){}file://不正确...说明:explicit仅能用于类定义的内部还有一个例子(定义为以上正确定义的类)

voidf(X){}voidg(intI){f(i);//错误}voidh(){Xx1(1);//正确}如果没有explicit定义,f(i)是可以通过的,因为编译器可以实现一个隐式转换int->(implicit)X用i来构造一个匿名X类,作为f()的参数。而由于有了explicit定义,故而此一步无法实现。MSDN的原文是:Thefunctioncallf(i)failsbecausethereisnoavailableimplicitconversionfrominttoX.

CString

这里我们简单的看看MicrosoftMFC中的CString是如何实现的。当然,要看原理,直接把它的代码拿过来分析是最好的。MFC里的关于CString的类的实现大部分在strcore.cpp中。

CString就是对一个用来存放字符串的缓冲区和对施加于这个字符串的操作封装。也就是说,CString里需要有一个用来存放字符串的缓冲区,并且有一个指针指向该缓冲区,该指针就是LPTSTRm_pchData。但是有些字符串操作会增建或减少字符串的长度,因此为了减少频繁的申请内存或者释放内存,CString会先申请一个大的内存块用来存放字符串。这样,以后当字符串长度增长时,如果增加的总长度不超过预先申请的内存块的长度,就不用再申请内存。当增加后的字符串长度超过预先申请的内存时,CString先释放原先的内存,然后再重新申请一个更大的内存块。同样的,当字符串长度减少时,也不释放多出来的内存空间。而是等到积累到一定程度时,才一次性将多余的内存释放。

还有,当使用一个CString对象a来初始化另一个CString对象b时,为了节省空间,新对象b并不分配空间,它所要做的只是将自己的指针指向对象a的那块内存空间,只有当需要修改对象a或者b中的字符串时,才会为新对象b申请内存空间,这叫做写入复制技术(CopyBeforeWrite)。

这样,仅仅通过一个指针就不能完整的描述这块内存的具体情况,需要更多的信息来描述。

首先,需要有一个变量来描述当前内存块的总的大小。其次,需要一个变量来描述当前内存块已经使用的情况。也就是当前字符串的长度另外,还需要一个变量来描述该内存块被其他CString引用的情况。有一个对象引用该内存块,就将该数值加一。

CString中专门定义了一个结构体来描述这些信息:structCStringData{longnRefs;//referencecountintnDataLength;//lengthofdata(includingterminator)intnAllocLength;//lengthofallocation//TCHARdata[nAllocLength]

TCHAR*data()//TCHAR*tomanageddata{return(TCHAR*)(this+1);}};

实际使用时,该结构体的所占用的内存块大小是不固定的,在CString内部的内存块头部,放置的是该结构体。从该内存块头部开始的sizeof(CStringData)个BYTE后才是真正的用于存放字符串的内存空间。这种结构的数据结构的申请方法是这样实现的:pData=(CStringData*)newBYTE[sizeof(CStringData)+(nLen+1)*sizeof(TCHAR)];pData->nAllocLength=nLen;其中nLen是用于说明需要一次性申请的内存空间的大小的。

从代码中可以很容易的看出,如果想申请一个256个TCHAR的内存块用于存放字符串,实际申请的大小是:sizeof(CStringData)个BYTE+(nLen+1)个TCHAR

CString中所有的operations的都是针对这个缓冲区的。比如LPTSTRCString::GetBuffer(intnMinBufLength),它的实现方法是:首先通过CString::GetData()取得CStringData对象的指针。该指针是通过存放字符串的指针m_pchData先后偏移sizeof(CStringData),从而得到了CStringData的地址。然后根据参数nMinBufLength给定的值重新实例化一个CStringData对象,使得新的对象里的字符串缓冲长度能够满足nMinBufLength。然后在重新设置一下新的CStringData中的一些描述值。最后将新CStringData对象里的字符串缓冲直接返回给调用者。

这些过程用C++代码描述就是:if(GetData()->nRefs>1||nMinBufLength>GetData()->nAllocLength){//wehavetogrowthebufferCStringData*pOldData=GetData();intnOldLen=GetData()->nDataLength;//AllocBufferwilltrompitif(nMinBufLengthdata(),(nOldLen+1)*sizeof(TCHAR));GetData()->nDataLength=nOldLen;CString::Release(pOldData);}ASSERT(GetData()->nRefs<=1);

//returnapointertothecharacterstorageforthisstringASSERT(m_pchData!=NULL);returnm_pchData;

很多时候,我们经常的对大批量的字符串进行互相拷贝修改等,CString使用了CopyBeforeWrite技术。使用这种方法,当利用一个CString对象a实例化另一个对象b的时候,其实两个对象的数值是完全相同的,但是如果简单的给两个对象都申请内存的话,对于只有几个、几十个字节的字符串还没有什么,如果是一个几K甚至几M的数据量来说,是一个很大的浪费。因此CString在这个时候只是简单的将新对象b的字符串地址m_pchData直接指向另一个对象a的字符串地址m_pchData。所做的额外工作是将对象a的内存应用CStringData::nRefs加一。CString::CString(constCString&stringSrc){m_pchData=stringSrc.m_pchData;InterlockedIncrement(&GetData()->nRefs);}

这样当修改对象a或对象b的字符串内容时,首先检查CStringData::nRefs的值,如果大于一(等于一,说明只有自己一个应用该内存空间),说明该对象引用了别的对象内存或者自己的内存被别人应用,该对象首先将该应用值减一,然后将该内存交给其他的对象管理,自己重新申请一块内存,并将原来内存的内容拷贝过来。

其实现的简单代码是:voidCString::CopyBeforeWrite(){if(GetData()->nRefs>1){CStringData*pData=GetData();Release();AllocBuffer(pData->nDataLength);memcpy(m_pchData,pData->data(),(pData->nDataLength+1)*sizeof(TCHAR));}}其中Release就是用来判断该内存的被引用情况的。voidCString::Release(){if(GetData()!=_afxDataNil){if(InterlockedDecrement(&GetData()->nRefs)<=0)FreeData(GetData());}}

当多个对象共享同一块内存时,这块内存就属于多个对象,而不在属于原来的申请这块内存的那个对象了。但是,每个对象在其生命结束时,都首先将这块内存的引用减一,然后再判断这个引用值,如果小于等于零时,就将其释放,否则,将之交给另外的正在引用这块内存的对象控制。

通过上面的分析,我们已经对CString的内部机制已经有了一个大致的了解了。总的说来MFC中的CString是比较成功的。但是,由于数据结构比较复杂(使用CStringData),所以在使用的时候就出现了很多的问题,最典型的一个就是用来描述内存块属性的属性值和实际的值不一致。出现这个问题的原因就是CString为了方便某些应用,提供了一些operations,这些operation可以直接返回内存块中的字符串的地址值,用户可以通过对这个地址值指向的地址进行修改,但是,修改后又没有调用相应的operations1使CStringData中的值来保持一致。比如,用户可以首先通过operations得到字符串地址,然后将一些新的字符增加到这个字符串中,使得字符串的长度增加,但是,由于是直接通过指针修改的,所以描述该字符串长度的CStringData中的nDataLength却还是原来的长度,因此当通过GetLength获取字符串长度时,返回的必然是不正确的。

存在这些问题的operations下面一一介绍。

1.GetBuffer

很多错误用法中最典型的一个就是CString::GetBuffer()了.查了MSDN,里面对这个operation的描述是:ReturnsapointertotheinternalcharacterbufferfortheCStringobject.ThereturnedLPTSTRisnotconstandthusallowsdirectmodificationofCStringcontents。这段很清楚的说明,对于这个operation返回的字符串指针,我们可以直接修改其中的值:CStringstr1("Thisisthestring1");――――――――――――――――1intnOldLen=str1.GetLength();―――――――――――――――――2char*pstr1=str1.GetBuffer(nOldLen);――――――――――――――3strcpy(pstr1,"modified");――――――――――――――――――――4intnNewLen=str1.GetLength();―――――――――――――――――5

很显然,str1工作不正常是在对通过GetBuffer返回的指针进行一个字符串拷贝之后的。

再看MSDN上的关于这个operation的说明,可以看到里面有这么一段话:IfyouusethepointerreturnedbyGetBuffertochangethestringcontents,youmustcallReleaseBufferbeforeusinganyotherCStringmemberfunctions.

原来在对GetBuffer返回的指针使用之后需要调用ReleaseBuffer,这样才能使用其他CString的operations。上面的代码中,我们在4-5处增建一行代码:str2.ReleaseBuffer(),然后再观察nNewLen,发现这个时候已经是我们想要的值8了。

从CString的机理上也可以看出:GetBuffer返回的是CStringData对象里的字符串缓冲的首地址。根据这个地址,我们对这个地址里的值进行的修改,改变的只是CStringData里的字符串缓冲中的值,CStringData中的其他用来描述字符串缓冲的属性的值已经不是正确的了。比如此时CStringData::nDataLength很显然还是原来的值20,但是现在实际上字符串的长度已经是8了。也就是说我们还需要对CStringData中的其他值进行修改。这也就是需要调用ReleaseBuffer()的原因了。

正如我们所预料的,ReleaseBuffer源代码中显示的正是我们所猜想的:CopyBeforeWrite();//justincaseGetBufferwasnotcalled

if(nNewLength==-1)nNewLength=lstrlen(m_pchData);//zeroterminated

ASSERT(nNewLength<=GetData()->nAllocLength);GetData()->nDataLength=nNewLength;m_pchData[nNewLength]=‘\0‘;其中CopyBeforeWrite是实现写拷贝技术的,这里不管它。

下面的代码就是重新设置CStringData对象中描述字符串长度的那个属性值的。首先取得当前字符串的长度,然后通过GetData()取得CStringData的对象指针,并修改里面的nDataLength成员值。

但是,现在的问题是,我们虽然知道了错误的原因,知道了当修改了GetBuffer返回的指针所指向的值之后需要调用ReleaseBuffer才能使用CString的其他operations时,我们就能避免不在犯这个错误了。答案是否定的。这就像虽然每一个懂一点编程知识的人都知道通过new申请的内存在使用完以后需要通过delete来释放一样,道理虽然很简单,但是,最后实际的结果还是有由于忘记调用delete而出现了内存泄漏。实际工作中,常常是对GetBuffer返回的值进行了修改,但是最后却忘记调用ReleaseBuffer来释放。而且,由于这个错误不象new和delete人人都知道的并重视的,因此也没有一个检查机制来专门检查,所以最终程序中由于忘记调用ReleaseBuffer而引起的错误被带到了发行版本中。

要避免这个错误,方法很多。但是最简单也是最有效的就是避免这种用法。很多时候,我们并不需要这种用法,我们完全可以通过其他的安全方法来实现。比如上面的代码,我们完全可以这样写:CStringstr1("Thisisthestring1");intnOldLen=str1.GetLength();str1="modified";intnNewLen=str1.GetLength();

但是有时候确实需要,比如:我们需要将一个CString对象中的字符串进行一些转换,这个转换是通过调用一个dll里的函数Translate来完成的,但是要命的是,不知道什么原因,这个函数的参数使用的是char*型的:DWORDTranslate(char*pSrc,char*pDest,intnSrcLen,intnDestLen);这个时候我们可能就需要这个方法了:CStringstrDest;IntnDestLen=100;DWORDdwRet=Translate(_strSrc.GetBuffer(_strSrc.GetLength()),strDest.GetBuffer(nDestLen),_strSrc.GetLength(),nDestlen);_strSrc.ReleaseBuffer();strDest.ReleaseBuffer();if(SUCCESSCALL(dwRet)){}if(FAILEDCALL(dwRet)){}

的确,这种情况是存在的,但是,我还是建议尽量避免这种用法,如果确实需要使用,请不要使用一个专门的指针来保存GetBuffer返回的值,因为这样常常会让我们忘记调用ReleaseBuffer。就像上面的代码,我们可以在调用GetBuffer之后马上就调用ReleaseBuffer来调整CString对象。

2.LPCTSTR

关于LPCTSTR的错误常常发生在初学者身上。例如在调用函数DWORDTranslate(char*pSrc,char*pDest,intnSrcLen,intnDestLen);时,初学者常常使用的方法就是:intnLen=_strSrc.GetLength();DWORDdwRet=Translate((char*)(LPCTSTR)_strSrc),(char*)(LPCTSTR)_strSrc),nLen,nLen);if(SUCCESSCALL(dwRet)){}if(FAILEDCALL(dwRet)){}

他原本的初衷是将转换后的字符串仍然放在_strSrc中,但是,当调用完Translate以后之后再使用_strSrc时,却发现_strSrc已经工作不正常了。检查代码却又找不到问题到底出在哪里。

其实这个问题和第一个问题是一样的。CString类已经将LPCTST重载了。在CString中LPCTST实际上已经是一个operation了。对LPCTST的调用实际上和GetBuffer是类似的,直接返回CStringData对象中的字符串缓冲的首地址。其C++代码实现是:_AFX_INLINECString::operatorLPCTSTR()const{returnm_pchData;}

因此在使用完以后同样需要调用ReleaseBuffer()。但是,这个谁又能看出来呢

其实这个问题的本质原因出在类型转换上。LPCTSTR返回的是一个constchar*类型,因此使用这个指针来调用Translate编译是不能通过的。对于一个初学者,或者一个有很长编程经验的人都会再通过强行类型转换将constchar*转换为char*。最终造成了CString工作不正常,并且这样也很容易造成缓冲溢出。

第一章:关于对象(ObjectLessons)

读完这一章使我想到了一个很久以前看到的一个笑话,编写一个HELLOWORLD的程序,随着水平和职务的不一样,程序代码也随着变化。当初看时完全当作笑话来看,现在看来写此笑话的人水平不一般。如果要使你的代码能够最大限度的适应不同的运行环境,和最大限度的复用,则在设计和编写的过程中需要考虑的问题很多,因此代码已变的不在具有C语言的简洁,高效。而牺牲了这些优势换来的是更好的封装。当然如果你只是要打印HelloWorld则不必这样做了。

本章是对对象模型的一个大略浏览。既然我们选择了C++而不是C作为开发工具,那我们的编程思想也应该转为C++的,而不能再延续C的Procedural方式。我们必须学会C++的思考方式。采用抽象数据类型或用一个多层的class体系对数据以及数据处理函数进行封装,只有摆脱C程序的使用全局数据的惯性,才能充分发挥出C++对象模型的强大威力。

我们设计的每一个类几乎都要有一个或多个构造函数、析构函数和一个Assignment运算符。他们的作用是构造函数产生一个新的对象并确定它被初始化。析构函数销毁一个对象并确定它已经被适当的清理(避免出现内存泄露的问题),Assignment运算符给对象一个新值。

THE END
1.c++写的电子宠物系统讯易软件语言: C/C++ 标签: pet 高速下载 资源简介 学了一个学期,自己写的一个电子宠物小程序,初次发表,代码简单,适合初学c++的同志参考。 代码片段和文件信息 #include#include#include using namespace std;class Pet{public: void change_with_time(int w); //随时间变化函数 void display(); void eat(); ...http://www.nz998.com/c/274386.html
2.数据库管理系统(原书第3版)(GeraldV.Post).pdfSally认识到要实现这两个目标需要收集和管理大量的数据。在参加了MBA课程中的信息 系统课程之后,她认识到需要数据库来帮助自己收集数据和管理宠物商店的经营。 当前Sally只有一个商店,但她渴望能扩展到其他城市。她想将雇佣的员工培训为“动物 的朋友”,而不是销售人员。这些员工能帮助顾客选择合适的动物,能回答有关...https://max.book118.com/html/2024/0111/8067065035006025.shtm
3.C++桌面宠物金鱼MFC环境下的桌面宠物,金鱼。。。 使用方法: //0,添加left.png,right.png资源文件 //1, 头文件定义变量 GoldFish pet; //2, 在CPP文件创建 // Create a goldfish if (!pet.Create(NULL, NULL, WS_CHILD | WS_VISIBLE, CRect(0,0,0,0), /* CWnd:: */ GetDesktopWindow(), 0x556)) { TRACE...https://www.iteye.com/resource/hemmingway-6823935
4.江苏省人力资源和社会保障厅热点新闻江苏省2019年高校毕业生...车行天下商务管理发展集团有限公司 运营经理、文案编辑 江苏科宁舒适家居系统集成有限责任公司 暖通外场设计师、见习客户经理、暖通内场设计师 江苏建发建设项目咨询有限公司 监理员、实习生、项目管理员 点米网络科技股份有限公司 苏宁易购客服专员、大客户销售代表 ...http://jshrss.jiangsu.gov.cn/art/2019/2/22/art_57444_8180462.html
1.C++实现宠物商店信息管理系统C语言本文实例为大家分享了C++实现宠物商店信息管理系统的具体代码,供大家参考,具体内容如下 一、问题描述 设计一个程序实现对小动物商店的简单管理,主要功能:宠物基本信息(编号,名称,体重, 年龄,类别,价格,性格等)的输入、显示、查询等功能;宠物的交易、状态及顾客(宠物主人)的记录查询和修改。 二、基本要求 (1)使用面...https://www.jb51.net/article/241320.htm
2.TurboC++3.2宠物照管系统开发实践简介:该课程项目旨在利用经典的 Turbo C++ 3.2 编译器开发一个宠物照管服务系统。学生将通过实践活动学习C++基础、面向对象编程、文件操作、图形用户界面设计、数据库集成、错误处理和内存管理等技能。项目将涵盖从需求分析到系统设计再到测试的整个软件开发流程,最终实现一个能够帮助用户管理宠物照管服务的完整系统。 https://blog.csdn.net/weixin_32925455/article/details/142250490
3.回复即可体验ChatGPT大圣前端进阶指南1、Java:Java是目前最受欢迎的编程语言,它非常强大,可以构建从小型应用程序到大型企业级系统。 2、Python:Python是另一种流行的编程语言,它的语法简洁易懂,可以轻松地开发网络服务器、游戏引擎、科学应用等。 3、C++:C++是另一种用于开发应用程序的编程语言,它提供了更多的灵活性,可以与机器硬件进行更深入的交互。http://shengxinjing.cn/blog/chatgpt.html
4.基于django+python+mysql的进销存管理系统的设计与实现管理用户添加功能:可以在系统中输入管理用户的员工编号、用户名、密码来添加新 的管理用户信息。 管理用户删除功能:可以在管理用户列表中删除任意不需要的管理用户。 管理用户修改功能:可以在管理用户列表中选择任意需要修改的管理用户进行修改, 可以任意修改管理用户的用户名、密码。 https://cloud.tencent.com/developer/article/1951701
5.医院影像图片库管理系统码农集市专业分享IT编程学习资源医院影像图片库管理系统点赞(0) 踩踩(0) 反馈 所需:3 积分 电信网络下载 qq_39630492 2018-01-03 19:54:31 评论 阻止下载,我也很绝望joy6262 2012-12-20 08:59:47 评论 界面美观,实用springboot网上宠物管理系统演示录像2023-db4o2.mp4 2024-10-24 10:16:19 积分:1 基于python+django+vue的...https://www.coder100.com/index/index/content/id/110630
6.月薪最高2万!松江这些优质岗位等你来~澎湃号·政务澎湃新闻上海丰柏物业管理有限公司 公司地址 松江区九亭镇沪亭北路338弄110号贝尚湾物业 联系方式 戴女士:15026599571 一、工程维修 2人 ...惊喜宠物用品(上海)有限公司 公司地址 松江区九亭镇伴亭路228号美林创意园A3幢105-106室 联系方式 赵女士:13061636663 一、兽药质检 1人 ...https://www.thepaper.cn/newsDetail_forward_7324475
7.(通用)高职教师培训总结课程设计内容:总体能力目标——能按照软件工程规范,以团队方式使用C++Builder进行编码工作,开发出(远程)数据库应用系统;选择教学内容——成绩管理系统软件团队开发工程实战、软件团队开发总结课、自主软件开发作业课;能力项目设计——分析成绩管理系统概况和概要设计、系统总控模块详细设计与编码、查询打印模块详细设计与编码...https://www.jy135.com/peixunzongjie/2106198.html
8.软件研发工程师岗位职责17篇(全文)应用软件系统项目评审;应用软件项目疑难问题处理;应用软件疑难故障分析处理;软件人力资源组织/考评;应用软件开发团队组织;应用软件工程师集训学习;应用软件体系框架设计与定制;应用软件技术积累与探索;应用软件开发技术规范编制;应用软件的技术资料管理;应用软件知识产权等相关文档编制;应用软件的鉴定、认证; 应用软件的质量...https://www.99xueshu.com/w/fileya61xaaf.html
9.杭州非适利宠物食品有限公司人脉圈怎么样发现1+脉友C端运营生命周期管理电商运营商品运营RFM模型活动运营 暂无内容 展开更多 杜先生 中国系统· 数据库研发专家 影响力655 访客3928北京 个人简介:数据库内核开发。 个人简介 数据库内核开发。 职业标签 工作经历 数据库研发专家 中国系统 2023.02 - 2024.09(1年7个月) C/C++CmakeDockerGolangKubernetesLinuxMakefileMyS...https://maimai.cn/mai/HPwvg1KbuGzAEv2MHsJtNnmDgiTuyKeNTu_mXtiHYLE
10.人才培养兴安职业技术学院本专业培养德、智、体、美、劳全面发展,具有良好的职业生涯发展基础,具有计算机软硬件基础、数据库与程序设计、现代通信与网络原理等方面的知识,具有计算机及网络设备的售前与售后技术支持、网络工程的设计与施工、网络及安全管理与维护、网络应用开发等等方面的能力,从事网络组建、网络安全与管理、网络服务应用开发等岗位...https://www.nmxzy.cn/contents/266/2246.html
11.专业课教学内容设置(精选十篇)重点培养学生掌握计算机科学与技术基本理论、方法与技能,掌握网络系统设计与工程的基本理论、方法和技能,使学生能主动适应计算机学科的发展,了解与其它学科(如通讯、自动控制)的交叉应用,掌握现代通信知识,具有计算机网络建设、网络管理和网络维护的能力,具备设计和开发网络应用软件的能力和计算机信息管理、数据处理、实时控制...https://www.360wenmi.com/f/cnkeyb16f85f.html
12.宠物美容师及助理和c++开发工程师有什么区别说明:宠物美容师及助理和c++开发工程师哪个工资高?宠物美容师及助理低于c++开发工程师。宠物美容师及助理平均工资¥/月,2024年工资¥5.2K,2024年工资高于2023年,c++开发工程师平均工资¥23.0K/月,2024年工资¥23.2K,2024年工资低于2023年,统计依赖于各大平台发布的公开数据,系统稳定性会影响客观性,仅供参考。 就业...https://www.jobui.com/gangwei/pk/chongwumeirongshijizhuli-c%2B%2Bkaifagongchengshi/
13.2024重庆农业学校开设的专业一览表2024重庆农业学校开设的专业有:学前教育、汽车制造与检修、数控技术应用、机电技术应用、电子与信息技术、计算机网络技术、会计电算化、物流服务管理、旅游服务与管理、文秘与现代办公、现代农艺技术、农产品营销与储运、设施农业技术、观光农业经营、农村经济综合管理、农村机械使用与维护、农村电气技术、畜牧兽医、宠物养护...https://m.bangboer.net/news/show397957.html