span-span-经常出现避坑指南-danger-C-class=text (span是行内元素吗)
空指针调用成员函数会crash??
当调用一个空指针所指向的类的成员函数时,大少数人的反响都是程序会crash。空指针并不指向任何有效的内存地址,所以在调用成员函数时会尝试访问一个不存在的内存地址,从而造成程序解体。
理想上有点出人预料,先来看段代码:
classMyClass{public:staticvoidTest_Func1(){cout<<"HandleTest_Func1!"<<endl;}voidTest_Func2(){cout<<"HandleTest_Func2!"<<endl;}voidTest_Func3(){cout<<"HandleTest_Func3!value:"<<value<<endl;}virtualvoidTest_Func4(){cout<<"HandleTest_Func4!"<<endl;}intvalue=0;};intmn(){MyClass*ptr=nullptr;ptr->Test_Func1();//ok,printHandleTest_Func1!ptr->Test_Func2();//ok,printHandleTest_Func2!ptr->Test_Func3();//crashptr->Test_Func4();//crashreturn0;}
上方例子中,空指针对Test_Func1和Test_Func2的调用反常,对Test_Func3和Test_Func4的调用会crash。或许很多人反响都会crash,实践上并没有,这是为啥?
类的成员函数并不与详细对象绑定,一切的对象共用同一份成员函数体,当程序被编译后,成员函数的地址即已确定,这份共有的成员函数体之所以能够把不同对象的数据区离开来,靠的是隐式传递给成员函数的this指针,成员函数中对成员变量的访问都是转化成"this->数据成员"的方式。因此,从这一角度说,成员函数与普通函数一样,只是多了this指针。而类的静态成员函数只能访问静态成员变量,不能访问非静态成员变量,所以静态成员函数不须要this指针作为隐式参数。
因此,Test_Func1是静态成员函数,不须要this指针,所以即使ptr是空指针,也不影响对Test_Fun1的反常调用。Test_Fun2虽然须要传递隐式指针,但是函数体中并没有经常使用到这个隐式指针,所以ptr为空也不影响对Test_Fun2的反常调用。Test_Fun3就不一样了,由于函数中经常使用到了非静态的成员变量,对num的调用被转化成this->num,也就是ptr->num,而ptr是空指针,因此会crash。Test_Fun4是虚函数,有虚函数的类会有一个成员变量,即虚表指针,当调用虚函数时,会经常使用虚表指针,对虚表指针的经常使用也是经过隐式指针经常使用的,因此Test_Fun4的调用也会crash。
同理,以下std::shared_ptr的调用也是如此,日常开发须要留意,记得加上判空。
std::shared_ptr<UrlHandler>url_handler;...if(url_handler->IsUrlNeedHandle(data)){url_handler->HandleUrl(param);}
字符串关系
字符串查找
对字符串启动处置是一个很经常出现的业务场景,其中字符串查找也是十分经常出现的,但是用的不好也是会存在各种坑。经常出现的字符串查找方法有:std::string::find、std::string::find_first_of、std::string::find_first_not_of、std::string::find_last_of,各位C++Engineer都能熟练经常使用了吗?先来段代码瞧瞧:
boolIsBlacklistDllFromSrv(conststd::string&dll_name){try{std::stringtarget_str=dll_name;std::transform(target_str.begin(),target_str.end(),target_str.begin(),::tolower);if(dll_blacklist_from_srv.find(target_str)!=std::string::npos){returntrue;}}catch(...){}returnfalse;}
上方这段代码,看上去没啥疑问的样子。但是细心看上去,就会发现字符串比对这里逻辑不够谨严,存在很大的破绽。std::string::find只是用来在字符串中查找指定的子字符串,只需蕴含该子串就合乎,假设dll_blacklist_from_srv="abcd.dll;hhhh.dll;test.dll"是这样的字符串,传入d.dll、hh.dll、dll;test.dll也会命中逻辑,显著是不太合乎预期的。
这里顺带回忆下C++std::string经常出现的字符串查找的方法:
std::string::find 用于在字符串中查找指定的子字符串。假设找到了子串,则前往子串的起始位置,否则前往std::string::npos。用于各种字符串操作,例如判别子字符串能否存在、失掉子字符串的位置等。经过联合其余成员函数和算法,可以成功更复杂的字符串处置逻辑。
std::string::find_first_of 用于查找字符串中第一个与指定字符汇合中的恣意字符婚配的字符,并前往其位置。可用来审核字符串中能否蕴含指定的某些字符或许查找字符串中第一个出现的特定字符
std::string::find_first_not_of 用于查找字符串中第一个不与指定字符汇合中的任何字符婚配的字符,并前往其位置。
std::string::find_last_of 用于查找字符串中最后一个与指定字符汇合中的恣意字符婚配的字符,并前往其位置。可以用来审核字符串中能否蕴含指定的某些字符,或许查找字符串中最后一个出现的特定字符
std::string::find_last_not_of 用于查找字符串中最后一个不与指定字符汇合中的任何字符婚配的字符,并前往其位置。
除了以上几个方法外,还有查找满足指定条件的元素std::find_if,
std::find_if是C++规范库中的一个算法函数,用于在指定范围外调找第一个满足指定条件的元素,并前往其迭代器。须要留意的是,经常使用std::find_if函数时须要提供一个可调用对象(例如lambda表白式或函数对象),用于指定查找条件。
std::vector<int>vec={1,2,3,4,5};autoit=std::find_if(vec.begin(),vec.end(),[](intx){returnx%2==0;});if(it!=vec.end()){std::cout<<"Foundevennumber:"<<*it<<std::endl;}
此外,在业务开发有时刻也会遇到须要C++boost库支持的starts_with、ends_with。假设用C++规范库来成功,惯例编写方法可如下:
boolstarts_with(conststd::string&str,conststd::string&prefix){returnstr.compare(0,prefix.length(),prefix)==0;}boolends_with(conststd::string&str,conststd::string&suffix){if(str.length()<suffix.length()){returnfalse;}else{returnstr.compare(str.length()-suffix.length(),suffix.length(),suffix)==0;}}
以上代码中,starts_with函数和ends_with函数区分用于审核字符串的前缀和后缀。两个函数外部都经常使用了std::string::compare方法来比拟字符串的子串和指定的前缀或后缀能否相等。假设相等,则说明字符串满足条件,前往true;否则前往false。
std::string与std::wstring转换
对字符串启动处置是一个很经常出现的业务场景,尤其是C++客户端开发,咱们经常须要在窄字符串std::string与宽字符串std::wstring之间启动转换,有时刻一不小心就会出现各种中文乱码。还有就是一提到窄字符串与宽字符串互转以及时不时出现的中文乱码,很多人就犯晕。
在C++中,std::string和std::wstring之间的转换触及到字符编码的转换。假设在转换环节中出现乱码,或许是由于字符编码不婚配造成的。要正确地启动std::string和std::wstring之间的转换,须要确保源字符串的字符编码和指标字符串的字符编码分歧,防止C++中的字符串处置乱码,可以经常使用Unicode编码(如UTF-8、UTF-16或UTF-32)来存储和处置字符串。
咱们想要处置或解析一些Unicode数据,例如从REG文件读取,经常使用std::wstring变量更能繁难的处置它们。例如:std::wstringws=L"中国a"(6个八位字节内存:0x4E2D0x56FD0x0061),咱们可以经常使用ws[0]失掉字符中,经常使用ws[1]失掉字符国,经常使用ws[2]失掉字符国失掉字符'a'等,这个时刻假设经常使用std::string,ws[0]拿进去的就是乱码。
此外还受代码页编码的影响(比如VS可以经过文件->初级保留选项->编码来更改以后代码页的编码)。
上方是一些示例代码,展示了如何启动正确的转换,针对Windows平台,官网提供了相应的系统Api(MultiByteToWideChar):
std::wstringUtf8ToUnicode(conststd::string&str){intlen=str.length();if(0==len)returnL"";intnLength=MultiByteToWideChar(CP_UTF8,0,str.c_str(),len,0,0);std::wstringbuf(nLength+1,L' ');MultiByteToWideChar(CP_UTF8,0,str.c_str(),len,&buf[0],nLength);buf.resize(wcslen(buf.c_str()));returnbuf;}std::stringUnicodeToUtf8(conststd::wstring&wstr){if(wstr.empty()){returnstd::string();}intsize_needed=WideCharToMultiByte(CP_UTF8,0,&wstr[0],static_cast<int>(wstr.size()),nullptr,0,nullptr,nullptr);std::stringstr_to(size_needed,0);WideCharToMultiByte(CP_UTF8,0,&wstr[0],static_cast<int>(wstr.size()),&str_to[0],size_needed,nullptr,nullptr);returnstr_to;}
假设经常使用C++规范库来成功,惯例写法可以参考上方:
#include<tream>#include<string>#include<locale>#include<codecvt>//从窄字符串到宽字符串的转换std::wstringnarrowToWide(conststd::string&narrowStr){try{std::wstring_convert<std::codecvt_utf8<wchar_t>>converter;returnconverter.from_bytes(narrowStr);}catch(...){//假设传出去的字符串不是utf8编码的,这里会抛出std::range_error异常return{};}}//从宽字符串到窄字符串的转换std::stringwideToNarrow(conststd::wstring&wideStr){try{std::wstring_convert<std::codecvt_utf8<wchar_t>>converter;returnconverter.to_bytes(wideStr);}catch(...){return{};}}//utf8字符串转成stringstd::stringutf8ToString(constchar8_t*str){std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t>convert;std::u16stringu16str=convert.from_bytes(reinterpret_cast<constchar*>(str),reinterpret_cast<constchar*>(str+std::char_traits<char8_t>::length(str)));returnstd::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t>{}.to_bytes(u16str);}intmain(){{std::wstringwideStr=L"Hello,你好!";std::stringnarrowStr=wideToNarrow(wideStr);std::wstringconvertedWideStr=narrowToWide(narrowStr);}{//std::stringnarrowStr="Hello,你好!";(1)std::stringnarrowStr=utf8ToString(u8"Hello,你好!");//(2)std::wstringwideStr=narrowToWide(narrowStr);std::stringconvertedNarrowStr=wideToNarrow(wideStr);}return0;}
(1)首先std::string不了解编码,在CPP官网手册外面也解释了,std::string处置字节的方式与所经常使用的编码有关,假设用于处置多字节或可变长度字符的序列(例如UTF-8),则此类的一切成员以及它的迭代器依然以字节(而不是实践的编码字符)为单位启动操作,假设用来处置蕴含中文的字符串就或许出现乱码。这里间接将蕴含中文的字符串赋值给std::string,无法保障是UTF8编码,启动转换时会提醒std::range_error异常;此外,std::wstring是会了解编码的,其中的字符串通经常常使用UTF-16或UTF-32编码,这取决于操作系统和编译器的成功。
(2)这里由于经常使用u8""结构了UTF8编码字符串,但是不能间接用来结构std::string,所以启动转了下utf8ToString;
全局静态对象
大家有没有在工程代码中发现有上方这种写法,将常量字符串申明为静态全局的。
staticconststd::string;
staticconststd::wstring;
好处:
可读性好:经常使用有意义的变量名,可以明晰地表白变量的含意和用途,提高了代码的可读性。
安保性高:由于经常使用了const关键字,这个字符串变量是无法修正的,可以防止异常的修正和安保疑问。
生命周期长:静态变量的生命周期从程序启动到完结,不受函数的调用和前往影响。
缺陷:
结构开支:静态变量的初始化出当初程序启动时也就是口头main()之前,会参与程序启动的时期和资源消耗。少量的这种静态全局对象,会拖慢程序启动速度
静态变量共享:静态变量在整个程序中只要一份实例,或许会造成全局形态共享和难以调试的疑问。
此外,静态变量的初始化顺序或许会遭到编译单元(源文件)中其余静态变量初始化顺序的影响,因此在跨编译单元的状况下,静态变量的初始化顺序或许是不确定的。
在实践编程中,还是不太倡导经常使用全局静态对象,倡导的写法:
要申明全局的常量字符串,可以经常使用const关键字和extern关键字的组合:
//constants.hexternconstchar*GLOBAL_STRING;//constants.cpp#include"constants.h"constchar*GLOBAL_STRING="Hello,world!";constexprchar*kVal="hahhahah";经常使用constexpr关键字来申明全局的常量字符串://constants.hconstexprconstchar*GLOBAL_STRING="Hello,world!";
迭代器删除
在处置缓存时,容器元素的增删查改是很经常出现的,经过迭代器去删除容器(vector/map/set/unordered_map/list)元素也是常有的,但这其中经常使用不当也会存在很多坑。
std::vector<int>numbers={88,101,56,203,72,135};autoit=std::find_if(numbers.begin(),numbers.end(),[](intnum){returnnum>100&&num%2!=0;});vec.erase(it);
上方代码,查找std::vector中大于100并且为奇数的整数并将其删除。std::find_if将冷静器的扫尾开局查找,直到找到满足条件的元素或许遍历完整个容器,并前往迭代器it,而后去删除该元素。但是这里没有判别it为空的状况,间接就erase了,假设erase一个空的迭代器会引发crash。很多新手程序员会犯这样的失误,随时判空是个不错的习气。
删除元素不得不讲下std::remove和std::remove_if,用于冷静器中移除指定的元素,函数会将合乎条件的元素移动到容器的末尾,并前往指向新的末尾位置之后的迭代器,最后经常使用容器的erase来擦除重新的末尾位置开局的元素。
std::vector<std::string>vecs={"A","","B","","C","hhhhh","D"};vecs.erase(std::remove(vecs.begin(),vecs.end(),""),vecs.end());//移除一切偶数元素vec.erase(std::remove_if(vec.begin(),vec.end(),[](intx){returnx%2==0;}),vec.end());
这里的erase不用判空,其外部成功曾经有判空处置。
_CONSTEXPR20iteratorerase(const_iterator_First,const_iterator_Last)noexcept(is_nothrow_move_assignable_v<value_type>)/*strengthened*/{constpointer_Firstptr=_First._Ptr;constpointer_Lastptr=_Last._Ptr;auto&_My_data=_Mypair._Myval2;pointer&_Mylast=_My_data._Mylast;//....if(_Firstptr!=_Lastptr){//somethingtodo,invalidateiterators_Orphan_range(_Firstptr,_Mylast);constpointer_Newlast=_Move_unchecked(_Lastptr,_Mylast,_Firstptr);_Destroy_range(_Newlast,_Mylast,_Getal());_Mylast=_Newlast;}returniterator(_Firstptr,_STDaddressof(_My_data));}
此外,STL容器的删除也要小心迭代器失效,先来看个vector、list、map删除的例子:
//vector、list、map遍历并删除偶数元素std::vector<int>elements={1,2,3,4,5};for(autoit=elements.begin();it!=elements.end();){if(*it%2==0){elements.erase(it++);}else{it++;}}//Errorstd::list<int>cont{88,101,56,203,72,135};for(autoit=cont.begin();it!=cont.end();){if(*it%2==0){cont.erase(it++);}else{it++;}}//Okstd::map<int,std::string>myMap={{1,"one"},{2,"two"},{3,"three"},{4,"four"},{5,"five"}};//遍历并删除键值对,删除键为偶数的元素for(autoit=myMap.begin();it!=myMap.end();){if(it->first%2==0){myMap.erase(it++);}else{it++;}}//Ok
迭代器的失效疑问:对容器的操作影响了元素的寄存位置,称为迭代器失效。迭代器失效的状况:
●假设容器扩容,在其余中央重新又开拓了一块内存,原来容器底层的内存上所保留的迭代器全都失效。
迭代器失效有三种状况,由于底层的存储数据结构,分三种状况:
序列式迭代器失效 ,序列式容器(std::vector和std::deque),其对应的数据结构调配在延续的内存中,对其中的迭代器启动insert和erase操作都会使得删除点和拔出点之后的元素挪位置,进而造成拔出点和删除掉之后的迭代器所有失效。可以应用erase迭代器接口前往的是下一个有效的迭代器。
链表式迭代器失效 ,链表式容器(std::list)经常使用链表启动数据存储,拔出或许删除只会对以后的节点形成影响,不会影响其余的迭代器。可以应用erase迭代器接口前往的是下一个有效的迭代器,或许将以后的迭代器指向下一个erase(iter++)。
关联式迭代器失效 ,关联式容器,如map,set,multimap,multiset等,经常使用红黑树启动数据存储,删除以后的迭代器,仅会使以后的迭代器失效。erase迭代器的前往值为void(C++11之前),可以驳回erase(iter++)的方式启动删除。值得一提的是,在最新的C++11规范中,曾经新增了一个map::erase函数口头后会前往下一个元素的iterator,因此可以经常使用erase的前往值失掉下一个有效的迭代器。
在成功上有两种模板,其一是经过erase取得下一个有效的iterator,经常使用于序列式迭代器和链表式迭代器(C++11开局关联式迭代器也可以经常使用)
for(autoit=elements.begin();it!=elements.end();){if(ShouldDelete(*it)){it=elements.erase(it);//erase删除元素,前往下一个迭代器}else{it++;}}
其二是,递增以后迭代器,适用于链表式迭代器和关联式迭代器。
for(autoit=elements.begin();it!=elements.end();){if(ShouldDelete(*it)){elements.erase(it++);}else{it++;}}
对象拷贝
在泛滥编程言语中C++的好处之一便是其高性能,可是开发者代码写得不好(比如:很多不用要的对象拷贝),间接会影响到代码性能,接上去就讲几个经常出现的会惹起有意义拷贝的场景。
for循环:
std::vector<std::string>vec;for(std::strings:vec){}//orfor(autos:vec){}
这里每个string都会被拷贝一次性,为防止有意义拷贝可以将其改成:
for(constauto&s:vec)或许for(conststd::string&s:vec)
lambda捕捉
//失掉对应信息类型的内容std::stringGetRichTextMessageXxxContent(conststd::shared_ptr<model::Message>&message,conststd::map<model::MessageId,std::map<model::UserId,std::string>>&related_user_names,constmodel::UserId&login_userid,boolfor_message_index){//...//解析RichText内容returnDecodeRichTextMessage(message,[=](uint32_titem_type,conststd::string&data){std::stringoutput_text;//...returnoutput_text;});}
上述代码用于解析失掉文本信息内容,触及到富文本信息的解析和一些逻辑的计算,高频调用,他在解析RichText内容的callback中间接繁难粗犷的按值捕捉了一切变量,将一切变量都拷贝了一份,这里形成不用要的性能损耗,尤其上方那个std::map。这里可以改成按援用来捕捉,规避不用要的拷贝。
lambda函数在捕捉时会将被捕捉对象拷贝,假设捕捉的对象很多或许很占内存,将会影响全体的性能,可以依据需求经常使用援用捕捉或许按需捕捉:
autofunc=&a{};
autofunc=a=std::move(a){};(限C++14以后)
隐式类型转换
std::map<int,std::string>myMap={{1,"One"},{2,"Two"},{3,"Three"}};for(conststd::pair<int,std::string>&pair:myMap){//...}
这里在遍历关联容器时,看着是const援用的,心想着不会出现拷贝,但是由于类型错了还是会出现拷贝,std::map中的键值对是以std::pair<constKey,T>的方式存储的,其中key是常量。因此,在每次迭代时,会将以后键值对拷贝到暂时变量中。在处置大型容器或频繁遍历时,这种拷贝操作或许会发生一些性能开支,所以在遍历时介绍经常使用constauto&,也可以经常使用结构化绑定:for(constauto&[key,value]:map){}(限C++17后)
函数前往值优化
RVO是ReturnValueOptimization的缩写,即前往值优化,NRVO就是具名的前往值优化,为RVO的一个变种,此个性从C++11开局支持。为了更明晰的了解编译器的行为,这里成功了结构/析构及拷贝结构、赋值操作函数,如下:
classWidget{public:Widget(){std::cout<<"Widget:Constructor"<<std::endl;}Widget(constWidget&other){name=other.name;std::cout<<"Widget:Copyconstruct"<<std::endl;}Widget&operator=(constWidget&other){std::cout<<"Widget:Assignmentconstruct"<<std::endl;name=other.name;return*this;}~Widget(){std::cout<<"Widget:Destructor"<<std::endl;}public:std::stringname;};WidgetGetMyWidget(intv){Widgetw;if(v%2==0){w.name=1;returnw;}else{returnw;}}intmain(){constWidget&w=GetMyWidget(2);//(1)Widgetw=GetMyWidget(2);//(2)GetMyWidget(2);//(3)return0;}
运转上方代码,跑出的结果:
未优化:(msvc2022,C++14)Widget:ConstructorWidget:CopyconstructWidget:DestructorWidget:Destructor优化后:Widget:ConstructorWidget:Destructor
针对上方(1)(2)(3)的调用,我之前也是有点蛊惑,以为要缩小拷贝必定得用常援用来接,但是发现编译器启动前往值优化后(1)(2)(3)运转结果都是一样的,也就是日常开发中,针对函数中前往的暂时对象,可以用对象的常援用或许新的一个对象来接,最后的影响其实可以疏忽不计的。不过团体还是偏差于对象的常援用来接,一是出于没有优化时(编译器不支持或许不满足RVO条件)可以缩小一次性拷贝,二是假设前往的是对象的援用时可以防止拷贝。但是也要留意不要前往暂时对象的援用。
//pb协定接口成功inlineconst::PB::XXXConfig&XXConfigRsp::config()const{//...}voidXXSettingView::SetSettingInfo(constPB::XXConfigRsp&rsp){constautoconfig=rsp.config();//外部前往的是对象的援用,这里没有援用来接造成不用要的拷贝}
当遇到上方这种前往对象的援用时,外部最好也是用对象的援用来接,缩小不用要的拷贝。
此外,假设Widget的拷贝赋值操作比拟耗时,通常在经常使用函数前往这个类的一个对象时也是会有必定的考究的。
//style1Widgetfunc(Argsparam);//style2boolfunc(Widget*ptr,Argsparam);
上方的两种方式都能到达雷同的目的,但直观上的经常使用体验的差异也是十分显著的:
//style1Widgetobj=func(params);//style2Widgetobj;func(&obj,params);
但是,能到达雷同的目的,消耗的老本却未必是一样的,这取决于多个起因,比如编译器支持的个性、C++言语规范的规范强迫性等等。
看起来style2虽然须要写两行代码,但函数外部的老本却是确定的,只会取决于你以后的编译器,外部即使驳回不同的编译器启动函数调用,也并不会有多余的时期开支和稳固性疑问。经常使用style1时,较复杂的函数成功或许并不会如你希冀的经常使用RVO优化,假设编译器启动RVO优化,经常使用style1无疑是比拟好的选用。应用好编译器RVO个性,也是能为程序带来必定的性能优化。
函数传参经常使用对象的援用
effectiveC++中也提到了:以pass-by-reference-to-const交流pass-by-value
指在函数参数传递时,将原本经常使用"pass-by-value"(按值传递)的方式改为经常使用"pass-by-reference-to-const"(按常量援用传递)的方式。
在"pass-by-value"中,函数参数会创立一个正本,而在"pass-by-reference-to-const"中,函数参数会成为原始对象的一个援用,且为了防止修正原始对象,经常使用了常量援用。
经过经常使用"pass-by-reference-to-const",可以防止在函数调用时启动对象的拷贝操作,从而提高程序的性能和效率;还可以防止对象被切割疑问:当一个派生类对象以传值的方式传入一个函数,但是该函数的形参是基类,则只会调用基类的结构函数结构基类局部,派生类的新个性将会被切割。此外,经常使用常量援用还可以确保函数外部不会异常地修正原始对象的值。
std::shared_ptr线程安保
对shared_ptr置信大家都很相熟,但是一提到能否线程安保,或许很多人心里就没底了,借助本节,对shared_ptr线程安保方面的疑问启动剖析和解释。shared_ptr的线程安保疑问关键有两种:1.援用计数的加减操作能否线程安保;2.shared_ptr修正指向时能否线程安保。
援用计数
shared_ptr中有两个指针,一个指向所治理数据的地址,另一个指向口头控制块的地址。
口头控制块包括对关联资源的援用计数以及弱援用计数等。在前面咱们提到shared_ptr支持跨线程操作,援用计数变量是存储在堆上的,那么在多线程的状况下,指向同一数据的多个shared_ptr在启动计数的++或--时能否线程安保呢?
援用计数在STL中的定义如下:
_Atomic__M_use_count;//#shared_Atomic_word_M_weak_count;//#weak+(#shared!=0)
当对shared_ptr启动拷贝时,引入计数参与,成功如下:
template<>inlinebool_Sp_counted_base<_S_atomic>::_M_add_ref_lock_nothrow()noexcept{//Performlock-freeadd-if-not-zerooperation._Atomic_word__count=_M_get_use_count();do{if(__count==0)returnfalse;//Replacethecurrentcountervaluewiththeoldvalue+1,as//longasit'snotchangedmeanwhile.}while(!__atomic_compare_exchange_n(&_M_use_count,&__count,__count+1,true,__ATOMIC_ACQ_REL,__ATOMIC_RELAXED));returntrue;}template<>inlinevoid_Sp_counted_base<_S_single>::_M_add_ref_copy(){++_M_use_count;}
对援用计数的参与关键有以下2种方法:
_M_add_ref_copy
函数,对
_M_use_count+1
,是原子操作。
_M_add_ref_lock
函数,是调用
__atomic_compare_exchange_n``成功的``,
关键逻辑依然是
_M_use_count+1,而该函数是线程安保的,
和
_M_add_ref_copy
的区别是对不同
_Lock_policy
有不同的成功,蕴含间接加、原子操作加、加锁。
因此咱们可以得出论断:在多线程环境下,治理同一个数据的shared_ptr在启动计数的参与或缩小的时刻是线程安保的,这是一波原子操作。
修正指向
修正指向分为操作同一个shared_ptr对象和操作不同的shared_ptr对象两种。
多线程代码操作的是同一个shared_ptr的对象
比如std::thread的回调函数,是一个lambda表白式,其中援用捕捉了一个shared_ptr对象
shared_ptr<A>sp1=make_shared<A>();std::threadtd([&sp1](){....});
又或许经过回调函数的参数传入的shared_ptr对象,参数类型是指针或援用:
`
指针类型:voidfn(shared_ptr<A>*sp){...}
std::threadtd(fn,&sp1);援用类型:
voidfn(shared_ptr<A>&sp){...}
std::threadtd(fn,std::ref(sp1));`
当你在多线程回调中修正shared_ptr指向的时刻,这时刻确实不是线程安保的。
voidfn(shared_ptr<A>&sp){if(..){sp=other_sp;}elseif(...){sp=other_sp2;}}
shared_ptr内数据指针要修正指向,sp原先指向的援用计数的值要减去1,other_sp指向的援用计数值要加1。但是这几步操作加起来并不是一个原子操作,假设多个线程都在修正sp的指向的时刻,那么有或许会出疑问。比如在造成计数在操作-1的时刻,其外部的指向曾经被其余线程修正过了,援用计数的异常会造成某个治理的对象被提早析构,后续在经常使用到该数据的时刻触发coredump。当然假设你没有修正指向的时刻,是没有疑问的。也就是:
同一个shared_ptr对象被多个线程同时读是安保的
同一个shared_ptr对象被多个线程同时读写是不安保的
多线程代码操作的不是同一个shared_ptr的对象。
这里指的是治理的数据是同一份,而shared_ptr不是同一个对象,比如多线程回调的lambda是按值捕捉的对象。
std::threadtd([sp1](){....});
或许参数传递的shared_ptr是值传递,而非援用:
voidfn(shared_ptr<A>sp){...}std::threadtd(fn,sp1);
这时刻每个线程内看到的sp,他们所治理的是同一份数据,用的是同一个援用计数。但是各自是不同的对象,当出现多线程中修正sp指向的操作的时刻,是不会出现非预期的异常行为的。也就是说,如下操作是安保的:
voidfn(shared_ptr<A>sp){if(..){sp=other_sp;}elseif(...){sp=other_sp2;}}
虽然前面咱们提到了假设是按值捕捉(或传参)的shared_ptr对象,那么该对象是线程安保的,但是话虽如此,但却或许让人悬崖勒马。由于咱们经常使用shared_ptr更多的是操作其中的数据,对齐治理的数据启动读写,虽然在按值捕捉的时刻shared_ptr是线程安保的,咱们不须要对此施加额外的同步操作(比如加解锁),但是这并不象征着shared_ptr所治理的对象是线程安保的!请留意这是两回事。
最后再来看下std官网手册是怎样讲的:
这段话的意思是,shared_ptr的一切成员函数(包括复制结构函数和复制赋值运算符)都可以由多个线程在不同的shared_ptr实例上调用,即使这些实例是正本并且共享同一个对象的一切权。假设多个口头线程在没有同步的状况下访问同一个shared_ptr实例,并且这些访问中的任何一个经常使用了shared_ptr的非const成员函数,则会出现数据竞争;可以经常使用shared_ptr的原子函数重载来防止数据竞争。
咱们可以失掉上方的论断:
1.多线程环境中,关于持有相反裸指针的std::shared_ptr实例,一切成员函数的调用都是线程安保的。
a.当然,关于不同的裸指针的std::shared_ptr实例,更是线程安保的
b.这里的成员函数指的是std::shared_ptr的成员函数,比如get()、reset()、operrator->()等
2.多线程环境中,关于同一个std::shared_ptr实例,只要访问const的成员函数,才是线程安保的,关于非const成员函数,是非线程安保的,须要加锁访问。
首先来看一下std::shared_ptr的一切成员函数,只要前3个是non-const的,残余的全是const的:
成员函数 | 能否const |
---|---|
operator、operator-> | |
operatorbool |
讲了这么多,来个栗子通常下:
ant::Promise<JsAPIResultCode,CefRefPtr<CefDictionaryValue>>XXXHandler::OnOpenSelectContactH5(constJsAPIContext&context,std::shared_ptr<RequestType>arguments){ant::Promise<JsAPIResultCode,CefRefPtr<CefDictionaryValue>>promise;base::GetUIThread()->PostTask(weak_lambda(this,[this,promise,context,arguments](){autob_executed_flag=std::make_shared<std::atomic_bool>(false);autoext_param=xx::OpenWebViewWindow::OpenURLExtParam();//...//SelectCorpGroupContactjsapi的回调ext_param.select_group_contact_callback=[promise,b_executed_flag](JsAPIResultCoderesCode,CefRefPtr<CefDictionaryValue>res)mutable{*b_executed_flag=true;base::GetUIThread()->PostTask([promise,resCode,res](){promise.resolve(resCode,res);});};//窗口封锁回调ext_param.dismiss_callback=[promise,b_executed_flag](){if(*b_executed_flag){return;}promise.resolve(JSAPI_RESULT_CANCEL,CefDictionaryValue::Create());};//...xx::OpenWebViewWindow::OpenURL(nullptr,url,false,ext_param);}));returnpromise;}
该段代码场景是一个Jsapi接口,在接口中关上另一个webview的选人窗口,选人窗口操作后或许封锁时都须要回调下,将结果前往jsapi。选人终了确认后会回调select_group_contact_callback,同时封锁webview窗口还会回调dismiss_callback,这俩回调外面都会回包,这里还触及多线程调用。这俩回调只能调用一个,为了能繁难到达这种成果,作者用std::shared_ptrstd::atomic_boolb_executed_flag来处置多线程同步,假设一个回调已口头就标志下,shared_ptr自身对援用计数的操作是线程安保的,经过原子变量std::atomic_bool来保障其治理的对象的线程安保。
//定义数据缓存类classDataCache{private:std::map<std::string,std::string>cache;public:voidaddData(conststd::string&key,conststd::string&value){cache[key]=value;}std::stringgetData(conststd::string&key){returncache[key];}};
在上述示例中,繁难定义了个数据缓存类,经常使用std::map作为数据缓存,而后提供addData参与数据到缓存,getData从map缓存中失掉数据。一切看起来毫无违和感,代码跑起来也没什么疑问,但是假设经常使用没有缓存的key去getData,发现会往缓存外面新拔出一条value为自动值的记载。
须要留意的是,假设咱们经常使用[]运算符访问一个不存在的键,并且在拔出新键值对时没有指定自动值,那么新键值对的值将是未定义的。因此,在经常使用[]运算符访问std::map中的元素时,应该一直确保该键曾经存在或许在拔出新键值对时指定了自动值。
voidaddData(conststd::string&key,conststd::string&value){if(key.empty())return;cache[key]=value;}std::stringgetData(conststd::string&key){constautoiter=cache.find(key);returniter!=cache.end()?iter->second:"";}
sizeof&strlen
置信大家都有过这样的阅历,在名目中经常使用系统API或许与某些公共库编写逻辑时,须要C++与C字符串混写甚至转换,在处置字符串结构体的时刻就免不了经常使用sizeof和strlen,这俩看着都有计算size的才干,有时刻很容易搞混杂或许出错。
是个操作符,可用于任何类型或变量,包括数组、结构体、指针等,前往的是一个类型或变量所占用的字节数;在编译时求值,不会对表白式启动求值。
是个函数,只能用于以null字符开头的字符串,前往的是一个以null字符(' ')开头的字符串的长度(不包括null字符自身),且在运转时才会计算字符串的长度。
须要留意的是,经常使用sizeof操作符计算数组长度时须要留意数组元素类型的大小。例如,关于一个int类型的数组,经常使用sizeof操作符计算其长度应该为sizeof(array)/sizeof(int)。而关于一个字符数组,经常使用strlen函数计算其长度应该为strlen(array)。
charstr[]="hello";char*p=str;
此时,用sizeof(str)失掉的是6,由于hello是5个字符,系统贮存的时刻会在hello的末尾加上完结标识 ,一共为6个字符;
而sizeof(p)失掉的却是4,它求得的是指针变量p的长度,在32位机器上,一个地址都是32位,即4个字节。
用sizeof( p)失掉的是1,由于 p定义为char,相当于一个字符,所以只占一个字节。
用strlen(str),失掉的会是5,由于strlen求得的长度不包括最后的 。
用strlen(p),失掉的是5,与strlen(str)等价。
上方的是sizeof和strlen的区别,也是指针字符串和数组字符串的区别。
constchar*src="helloworld";char*dest=NULL;intlen=strlen(src);//这里很容易出错,写成sizeof(src)就是求指针的长度,即4dest=(char*)malloc(len+1);//这里很容易出错,写成lenchar*d=dest;constchar*s=&src[len-1];//这里很容易出错,写成lenwhile(len--!=0){*d++=*s--;}*d=' ';//这句很容易漏写printf("%sIn",dest);free(dest);
std::async真的异步吗?
std::async是C++11开局支持多线程时参与的同步多线程结构函数,其补偿了std::thread没有前往值的疑问,并参与了更多的个性,使得多线程愈加灵敏。
望文生义,std::async是一个函数模板,它将函数或函数对象作为参数(称为回调)并异步运转它们,最终前往一个std::future,它存储std::async()口头的函数对象前往的值,为了从中失掉值,程序员须要调用其成员future::get.
那std::async必定是异步口头吗?先来看段代码:
intcalculate_sum(conststd::vector<int>&numbers){std::cout<<"StartCalculate..."<<std::endl;//(4)intsum=0;for(intnum:numbers){sum+=num;}returnsum;}intmain(){std::vector<int>numbers={88,101,56,203,72,135};std::future<int>future_sum=std::async(calculate_sum,numbers);std::cout<<"Otheroperationsareinprogress..."<<std::endl;//(1)intcounter=1;while(counter<=1000000000){counter++;}std::cout<<"Otheroperationsarecompleted."<<std::endl;//(2)//期待异步义务成功并失掉结果intsum=future_sum.get();std::cout<<"Thecalculationresultis:"<<sum<<std::endl;//(3)return0;}
间接运转上方的代码,输入结果如下:
Otheroperationsareinprogress...StartCalculate...Otheroperationsarecompleted.Thecalculationresultis:655口头完(1)就去口头(4),而后再(2)(3),说明这里是异步口头的。那可以以为async必定是异步的吗?假设改成std::async(std::launch::deferred,calculate_sum,numbers);运转结果如下:Otheroperationsareinprogress...Otheroperationsarecompleted.StartCalculate...Thecalculationresultis:655
口头完(1)(2),而后再(4)(3),说明是真正调用std::future<>::get()才去口头的,假设没有调用get,那么就不时不会口头。
std::async能否异步受参数控制的,其第一个参数是启动战略,它控制std::async的异步行为。可以经常使用3种不同的启动战略创立std::async,即:
假设咱们不指定启动战略,其行为相似于 std::launch::async|std::launch::deferred. 也就是不必定是异步的。
EffectiveModernC++外面也提到了,假设异步口头是必定的,则指定 std::launch::async 战略。
内存走漏?
关于这样的一个函数:
voidprocesswidget(std::shared_ptrpw,int);
假设经常使用以下方式调用,会有什么疑问吗?
processwidget(std::shared_ptr(newWidget),priority());
一眼看上去感觉没啥疑问,甚至或许新手C++开发者也会这么写,其实上方调用或许会存在内存走漏。
编译器在生成对processWidget函数的调用之前,必定先解析其中的参数。processWidget函数接纳两个参数,区分是智能指针的结构函数和整型的函数priority()。在调用智能指针结构函数之前,编译器必定先解析其中的newWidget语句。因此,解析该函数的参数分为三步:
(1)调用priority();
(2)口头newWidget.
(3)调用std:shared_ptr结构函数
C++编译器以什么样的固定顺序去成功上方的这些事件是未知的,不同的编译器是有差异的。在C++中可以确定(2)必定先于(3)口头,由于newWidoet还要被传递作为std::shared_ptr结构函数的一个实参。但是,关于priority()的调用可以在第(1)、(2)、(3)步口头,假定编译器选用以(2)口头它,最终的操作秩序如下:(1)口头newWidget;(2)调用priority():(3)调用std::shared_ptr结构函数。但是,假设priority()函数抛出了异常,经由newWidget前往的指针尚未被智能指针治理,将会遗失造成内存走漏。
处置方法:经常使用一个独自的语句来创立智能指针对象。
std::sharedptr<Widget>pw(newwidget);//放在独自的语句中processwidget(pw,priority())://orprocesswidget(std::make_shared<Widget>(),priority());
编译器是逐语句编译的,经过经常使用一个独自的语句来结构智能指针对象,编译器就不会轻易改变解析顺序,保障了生成的机器代码顺序是异常安保的。
总结:尤其是在跨平台开发的时刻愈加要留意这类费解的异常疑问,EffectiveC++中也提到了,要以独立语句将new对象存储于智能指针内。假设不这样做,一旦异常被抛出,有或许造成难以发觉的内存走漏。
const/constexpr
假设C++11中引入的新词要评一个"最令人困惑"奖,那么constexprhen很有或许获此殊荣。当它运行于对象时,其实就是一个增强版的const,但运行于函数时,却有着相当不同的意义。在经常使用C++const和consterpx的时刻,或许都会犯晕乎,那constexpr和const都有什么区别,这节繁难梳理下。
const普通的用法就是润色变量、援用、指针,润色之后它们就变成了常量,须要留意的是const并未区分出编译期常量和运转期常量,并且const只保障了运转时不间接被修正。
普通的状况,const也就繁难这么用一下,const放在左边,示意常量:
constintx=100;//常量
constint&rx=x;//常量援用
constint*px=&x;//常量指针
给变量加上const之后就成了常量,只能读、不能修正,编译器会审核出一切对它的修正操作,收回正告,在编译阶段防止有意或许有意的修正。这样一来,const常量用起来就相对安保一点。在设计函数的时刻,将参数用const润色的话,可以保障效率和安保。
除此之外,const还能申明在成员函数上,const被放在了函数的前面,示意这个函数是一个常量,函数的口头环节是const的,不会修正成员变量。
此外,const还有上方这种与指针联合的比拟绕的用法:
inta=1;constintb=2;constint*p=&a;intconst*p1=&a;//*p=2;//errorC3892:p:不能给常量赋值p=&b;//*p1=3;//errorC3892:p1:不能给常量赋值p1=&b;int*constp2=&a;//p2=&b;//errorC2440:=:无法从constint*转换为int*const*p2=5;constint*constp3=&a;
与并无很大区别,都示意:指向常量的指针,可以修正指针自身,但不能经过指针修正所指向的值。
而关于,则是示意:一个常量指针,可以修正所指向的值,但不能修正指针自身。
constint*const 示意一个无法修正的指针,既不能修正指针自身,也不能经过指针修正所指向的值。
总之,const自动与其左边联合,当左边没有任何物品则与左边联合。
外表上看,constexpr不只是const,而且在编译时期就已知,这种说法并不片面,当它运行在函数上时,就跟它名字有点不一样了。经常使用constexpr关键字可以将对象或函数定义为在编译时期可求值的常量,这样可以在编译时期启动计算,防止了运转时的开支。
constexpr对象 必定在编译时就能确定其值,并且通罕用于基本数据类型。例如:
constexprintMAX_SIZE=100;//定义一个编译时整型常量
constexprdoublePI=3.14159;//定义一个编译时双精度浮点型常量
const和constexpr变量之间的关键区别在于变量的初始化,const可以推早退运转时,constexpr变量必定在编译时初始化。const并未区分出编译期常量和运转期常量,并且const只保障了运转时不间接被修正,而constexpr是限定在了编译期常量。简而言之,一切constexpr对象都是const对象,而并非一切的const对象都是constexpr对象。
●当变量具备字面型别(literaltype)(这样的型别能够持有编译期可以决议的值)并已初始化时,可以经常使用constexpr来申明该变量。假设初始化由结构函数口头,则必定将结构函数申明为constexpr.
●当满足这两个条件时,可以申明援用constexpr:援用的对象由常量表白式初始化,并且在初始化时期调用的任何隐式转换也是常量表白式。
●constexpr变量或函数的一切申明都必定具备constexpr说明符。
constexprfloatx=42.0;constexprfloaty{108};constexprfloatz=exp(5,3);constexprinti;//Error!Notinitializedintj=0;constexprintk=j+1;//Error!jnotaconstantexpression
constexpr函数 是指能够在编译时期计算结果的函数。它们的参数和前往值类型必定是字面值类型,并且函数体必定由单个前往语句组成。例如:
constexprintsquare(intx){returnx*x;}
constexprintresult=square(5);//在编译时期计算结果,result的值为25
经常使用constexpr可以提高程序的性能和效率,由于它准许在编译时期启动计算,防止了运转时的计算开支。同时,constexpr还可以用于指定数组的大小、模板参数等场景,提供更灵敏的编程方式。
对constexpr函数的了解:
1.constexpr函数可以用在要求编译器常量的语境中。在这样的语境中,假设你传给constexpr函数的实参值是在编译期已知的,则结果也会在编译时期计算进去。假设任何一个实参值在编译时期未知,则代码将无法经过编译。
2.在调用constexpr函数时,若传入的值有一个或多个在编译时期未知,则它的运作方式和普通函数无异,也就是它也是在运转期口头结果的计算。也就是说,假设一个函数口头的是雷同的操作,仅仅运行语境一个是要求编译期常量,一个是用于一切其余值的话,那就不用写两个函数。constexpr函数就可以同时满足需求。
constexprfloatexp(floatx,intn){returnn==0?1:n%2==0?exp(x*x,n/2):exp(x*x,(n-1)/2)*x;}constexprautox=5;constexprauton=3;constexprintresult=exp(x,n);//ok,前面加上constexpr,启动编译时期求值,单步伐试基本进不去intxx=4;intnn=3;//constexprintresult2=exp(xx,nn);//errorC2131:表白式的计算结果不是常数intresult3=exp(xx,nn);//ok,这里作为普通函数来经常使用
比起非constexpr对象或constexpr函数而言,constexpr对象或是constexpr函数可以用在一个作用域更广的语境中。只需有或许经常使用constexpr,就经常使用它吧。
最后
欢迎C++大佬们一同交流阅历,站在凡人的肩膀上,写的有疑问的中央欢迎拍砖补充。
万字避坑指南!C++的缺陷与思索(下)-知乎
智能指针-经常使用、避坑和成功
c++-What'sthedifferencebetweenconstexprandconst?-StackOverflow
<input type="text" value="" data-format="yyyy大神帮我分析一下代码?
这是bootstrap自带的时间控件 这个是关键如果你想搞明白那你就得去看源码是怎么实现的了 type=text 这是文本输入框value= 默认值这就是一个属性就是用于选择这个没什么好说的 (data-format 格式化时间)你要把时间显示成什么样的格式就在这里写什么格式就可以了(yyyy-MM-dd-hh-mm-ss)这是年月日时分秒的格式希望对你有帮助。
测试html代码
<!DOCTYPE html> <html lang=ch> <head> <meta charset=utf-8> <meta http-equiv=X-UA-Compatible content=IE=edge> <meta content=width=device-width, initial-scale=1.0, maximum-scale=1.0> <title>whatsns专业级开源内容付费php问答系统</title> <meta content=whatsns是一款全面开放源码采用php+mysql技术研发的问答系统,产品端包括免费版,个人版,基础版,高级版,微信版,小程序版,APP版,个人站长和企业可以选择合适的产品端。 /> <meta content=whatsns,问答系统,php问答系统,语音问答系统,内容付费问答系统,采集问答系统 /> <meta content=whatsns Team /> <meta content= /> <link href=./dist/css/> <link href=./dist/font-awesome/css/> <script src=./dist/js/ type=text/javascript></script> <script src=./dist/js/ type=text/javascript></script> <script src=./dist/js/ type=text/javascript></script></head> <body> <nav> <div> <div> <button type=button aria-expanded=false> <span>折叠</span> <span></span> <span></span> <span></span> </button> <a href=./> <img src=dist/images/ alt=> </a> </div> <div> <ul> <li><a href=./>首页</a></li> <li><a href=./>产品购买</a></li> <li><a href=./>定制开发</a></li> <li><a href=./>代理招商</a></li> <li><a target=_blank href=开发配置视频教程</a></li> <li><a target=_blank href=开发文档</a></li> <li><a target=_blank href=配置文档</a></li> </ul> <ul> <li><a target=_blank href=社区交流</a></li> <li><a track-event=Clicked Get Started target=_blank href=购买咨询</a></li> </ul> </div> </div> </nav> <main> <div> <div> <h1>WHATSNS</h1> <h2>极速安装,配置简单,功能强大,二开方便</h2> <h2>集问答,文章,课程,采集,SEO优化,运营功能于一体的开源系统</h2> <h2>适合网站类型:个人流量网站,企业部门交流网站,企业知识库网站,邀请入住型网站,内容付费型网站</h2> <p> <a href=target=_blank role=button >开源版V4</a> <a href=./ target=_blank role=button >商业授权</a> </p> <p> <a target=_blank href=///wpa/qunwpa?idkey=ba66b440d8042da9fe78d78a4e7fdc79ddf7cdbb527d8a25c8cdb2><img border=0 src=///wpa/images/ alt=ask2问答系统(whatsns)></a> <a target=_blank href=///wpa/qunwpa?idkey=f8afbaaa1e209c5892b4fb84a8bbe1ecc218af0c90b8b5ba2e7a2cf6a><img border=0 src=///wpa/images/ alt=ask2问答系统官方群二></a> </p> <p> <span>官方QQ群一</span><span>官方QQ群二</span> </p> </div> </div> <section> <div> <div> <div> <h2>为什么选择whatsns</h2> <p>1、可以满足在废品虚拟主机运行流畅</p> <p>2、傻瓜式安装,快速采集,快速SEO优化内容</p> <p>3、轻松配置对接熊掌号,火车头采集,xunsearch,OSS</p> <p>4、PC和wap模板分离自由组合终端展示效果</p> <p>5、网站从注册,登录,防灌水,内容优化,内容采集挖掘,网站运营等方方面面为站长深度定制细节</p> <p>6、超强大的功能配置,自由开关前端功能</p> <p>7、深度挖掘问答,文章,课程内容付费,让站长没有难赚的钱</p> <p>8、解决内容付费终端支付问题,您可以在PC,wap和微信端,APP中动态调用不同底层支付方案解决内容付费问题<label>独家</label></p> <p>9、支持微信语音回答,自动转码,任何终端可以收听语音回答<label>独家</label></p> <p>10、性价比超值,货比三家,市面上没有任何一家问答+课程的开源系统能和我们比,有的一个终端(要么只有pc模板,要么只有wap模板)多人民币,有的只有问答模块,一套就是多人民币只包含卖系统钱,技术支持单算,选择我们是花一份钱买多个终端可用的产品,而且功能都碾压其他家。 </p> </div> </div> </section> <section> <div> <div> <div> <h2>使用whatsns如何实现盈利</h2> <p>可以通过广告获得收入,用户充值现金和财富值,付费看答案,付费看文章内容,优质回答和文章被打赏,付费咨询专家,现金悬赏和财富值悬赏问题,用户可以付费购买课程,办理VIP打折购买课程,拥有偷看卡免费看答案。 平台可以设置分成比例,提现可收取手续费,付费看回答可分成,付费咨询可分成,悬赏问题采纳可分成。 </p> </div> </div> </div> </section> <section> <div> <div> <div> <h3>成为商业客户,享受私人服务,一对一安装程序,程序问题及时解决。 </h3> <a target=_blank href=点击购买咨询</a> <p> 如果不能一键唤起咨询可加 </p> </div> </div> </div> </section> <section> <div> <div> <div> <h1>合作伙伴</h1> <p>历经多年的技术研究,知名国企,上市公司,国内五百强企业,以及其它优秀的企业和个人站长在使用我们的系统</p> </div> </div> </div> </section> <section> <div> <img src=./dist/images/> </div> </section> <footer> <div> <div> <div> <h3>网站相关</h3> <ul> <li> <a href=./>关于我们</a> </li> <li> <a href=>定制开发</a> </li> <li> <a href=>授权说明</a> </li> <li> <a target=_blank href=问题建议</a> </li> <li> <a href=./>联系我们</a> </li> <li> <a target=_blank href=码云地址</a> </li> </ul> </div> <div> <h3>友情链接</h3> <ul> <li> <a target=_blank href=产品社区</a> </li> <li> <a target=_blank href=源码</a> </li> <li> <a target=_blank href=源市场</a> </li> <li> <a target=_blank href=云帆互联</a> </li> <li> <a target=_blank href=产品展示</a> </li> <li> <a target=_blank href=宝塔面板</a> </li> </ul> </div> <div> <h3>帮助</h3> <ul> <li> <a target=_blank href=伪静态配置</a> </li> <li> <a target=_blank href=水印设置</a> </li> <li> <a target=_blank href=邮箱配置</a> </li> <li> <a target=_blank href=标签管理</a> </li> <li> <a target=_blank href=使用教程</a> </li> </ul> </div> <div> <ul> <li> <a href=target=_blank> <i aria-hidden=true></i> </a> </li> <li> <a href=./> <i aria-hidden=true></i> </a> </li> </ul> <p> <span>Powered by WHATSNS V4</span>⚬ <span>京ICP备号-3</span> </p> </div> </div> </div> </footer> </main> <a href=javascript:;></a> <script src=./dist/js/ type=text/javascript></script> <script src=./dist/js/ type=text/javascript></script> <script src=./dist/js/ type=text/javascript></script> <script> (function(){ var bp = (script); var curProtocol = (:)[0]; if (curProtocol ===https) { =} else { =} var s = (script)[0]; (bp, s); })(); </script> </body> </html>
免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。