C++11模版元编程的应用

1.概述

  关于C++11模板元的基本用法和常用技巧,我在程序员2015年2月B《C++11模版元编程》一文(后称前文)中已经做了详细地介绍,那么C++11模版元编程用来解决什么实际问题呢,在实际工程中又该如何应用呢?本文将侧重介绍C++11模板的一些具体应用,向读者展示模版元编程的具体应用。

  我们将展示如何通过C++11模版元来实现function_traits、Vairant类型和泛型bind绑定器。function_traits侧重于如何萃取可调用对象的一些元信息,Variant则是一种能接受多种类型数据的“万能”类型,bind则是一个泛化的绑定器,下面来看看这些具体的例子。

2.function_traits

  function_traits用来获取函数语义的可调用对象的一些属性,比如函数类型、返回类型、函数指针类型和参数类型等。下面来看看如何实现function_traits。

template<typename T>
struct function_traits;//普通函数
template<typename Ret, typename... Args>
struct function_traits<Ret(Args...)>
{
public:enum { arity = sizeof...(Args) };typedef Ret function_type(Args...);typedef Ret return_type;using stl_function_type = std::function<function_type>;typedef Ret(*pointer)(Args...);template<size_t I>struct args{static_assert(I < arity, "index is out of range, index must less than sizeof Args");using type = typename std::tuple_element<I, std::tuple<Args...>>::type;};
};//函数指针
template<typename Ret, typename... Args>
struct function_traits<Ret(*)(Args...)> : function_traits<Ret(Args...)>{};//std::function
template <typename Ret, typename... Args>
struct function_traits<std::function<Ret(Args...)>> : function_traits<Ret(Args...)>{};//member function
#define FUNCTION_TRAITS(...) \template <typename ReturnType, typename ClassType, typename... Args>\struct function_traits<ReturnType(ClassType::*)(Args...) __VA_ARGS__> : function_traits<ReturnType(Args...)>{}; \FUNCTION_TRAITS()
FUNCTION_TRAITS(const)
FUNCTION_TRAITS(volatile)
FUNCTION_TRAITS(const volatile)//函数对象
template<typename Callable>
struct function_traits : function_traits<decltype(&Callable::operator())>{};

        由于可调用对象可能是普通的函数、函数指针、lambda、std::function和成员函数,所以我们需要针对这些类型分别做偏特化,然后萃取出可调用对象的元信息。其中,成员函数的偏特化稍微复杂一点,因为涉及到cv符的处理,这里通过定义一个宏来消除重复的模板类定义。参数类型的获取我们是借助于tuple,将参数转换为tuple类型,然后根据索引来获取对应类型。它的用法比较简单:

template<typename T>
void PrintType()
{cout << typeid(T).name() << endl;
}
int main()
{std::function<int(int)> f = [](int a){return a; };//打印函数类型PrintType<function_traits<std::function<int(int)>>::function_type>(); //将输出int __cdecl(int)//打印函数的第一个参数类型PrintType<function_traits<std::function<int(int)>>::args<0>::type>();//将输出int//打印函数的返回类型PrintType<function_traits<decltype(f)>::return_type>(); //将输出int//打印函数指针类型PrintType<function_traits<decltype(f)>::pointer>(); //将输出int (__cdecl*)(int)
}

  可以看到这个function_traits通过类型萃取,可以很方便地获取可调用对象(函数、函数指针、函数对象、std::function和lambda表达式)的一些元信息,功能非常强大,这个function_traits经常会用到是更高层模版元程序的基础。比如Variant类型的实现就要用到这个function_traits,下面来看看Variant的实现。

3.Variant

  借助上面的function_traits和前文实现的一些元函数,我们就能方便的实现一个“万能类型”—Variant,Variant实际上一个泛化的类型,这个Variant和boost.variant的用法类似,需要预定义一些类型作为可接受的类型。boost.variant的基本用法如下:

typedef variant<int,char, double> vt;
vt v = 1;
v = 'a';
v = 12.32;

  这个variant可以接受已经定义的那些类型,看起来有点类似于c#和java中的object类型,实际上variant是擦除了类型,要获取它的实际类型的时候就稍显麻烦,需要通过boost.visitor来访问:

struct VariantVisitor : public boost::static_visitor<void>
{void operator() (int a){cout << "int" << endl;}void operator() (short val){cout << "short" << endl;}void operator() (double val){cout << "double" << endl;}void operator() (std::string val){cout << "string" << endl;}
};boost::variant<int,short,double,std::string> v = 1;
boost::apply_visitor(visitor, v); //将输出int

  通过C++11模版元实现的Variant将改进值的获取,将获取实际值的方式改为内置的,即通过下面的方式来访问:

typedef Variant<int, double, string, int> cv;
cv v = 10;
v.Visit([&](double i){cout << i << endl; }, [](short i){cout << i << endl; }, [=](int i){cout << i << endl; },[](const string& i){cout << i << endl; });//结果将输出10

  这种方式更方便直观。Variant的实现需要借助前文中实现的一些元函数MaxInteger、MaxAlign、Contains和At等等。下面来看看Variant实现的关键代码,完整的代码请读者参考笔者在github上的代码https://github.com/qicosmos/cosmos/blob/master/Varaint.hpp。

template<typename... Types>
class Variant{enum{data_size = IntegerMax<sizeof(Types)...>::value,align_size = MaxAlign<Types...>::value};using data_t = typename std::aligned_storage<data_size, align_size>::type;
public:template<int index>using IndexType = typename At<index, Types...>::type;Variant(void) :m_typeIndex(typeid(void)){}~Variant(){ Destroy(m_typeIndex, &m_data); }Variant(Variant<Types...>&& old) : m_typeIndex(old.m_typeIndex){Move(old.m_typeIndex, &old.m_data, &m_data);}Variant(const Variant<Types...>& old) : m_typeIndex(old.m_typeIndex){Copy(old.m_typeIndex, &old.m_data, &m_data);}template <class T,class = typename std::enable_if<Contains<typename std::remove_reference<T>::type, Types...>::value>::type> Variant(T&& value) : m_typeIndex(typeid(void)){Destroy(m_typeIndex, &m_data);typedef typename std::remove_reference<T>::type U;new(&m_data) U(std::forward<T>(value));m_typeIndex = type_index(typeid(U));}template<typename T>bool Is() const{return (m_typeIndex == type_index(typeid(T)));}template<typename T>typename std::decay<T>::type& Get(){using U = typename std::decay<T>::type;if (!Is<U>()){cout << typeid(U).name() << " is not defined. " << "current type is " <<m_typeIndex.name() << endl;throw std::bad_cast();}return *(U*)(&m_data);}template<typename F>void Visit(F&& f){using T = typename Function_Traits<F>::template arg<0>::type;if (Is<T>())f(Get<T>());}template<typename F, typename... Rest>void Visit(F&& f, Rest&&... rest){using T = typename Function_Traits<F>::template arg<0>::type;if (Is<T>())Visit(std::forward<F>(f));elseVisit(std::forward<Rest>(rest)...);}
private:void Destroy(const type_index& index, void * buf){std::initializer_list<int>{(Destroy0<Types>(index, buf), 0)...};}template<typename T>void Destroy0(const type_index& id, void* data){if (id == type_index(typeid(T)))reinterpret_cast<T*>(data)->~T();}void Move(const type_index& old_t, void* old_v, void* new_v) {std::initializer_list<int>{(Move0<Types>(old_t, old_v, new_v), 0)...};}template<typename T>void Move0(const type_index& old_t, void* old_v, void* new_v){if (old_t == type_index(typeid(T)))new (new_v)T(std::move(*reinterpret_cast<T*>(old_v)));}void Copy(const type_index& old_t, void* old_v, void* new_v){std::initializer_list<int>{(Copy0<Types>(old_t, old_v, new_v), 0)...};}template<typename T>void Copy0(const type_index& old_t, void* old_v, void* new_v){if (old_t == type_index(typeid(T)))new (new_v)T(*reinterpret_cast<const T*>(old_v));}
private:data_t m_data;std::type_index m_typeIndex;//类型ID
};

  实现Variant首先需要定义一个足够大的缓冲区用来存放不同的类型的值,这个缓类型冲区实际上就是用来擦除类型,不同的类型都通过placement new在这个缓冲区上创建对象,因为类型长度不同,所以需要考虑内存对齐,C++11刚好提供了内存对齐的缓冲区aligned_storage:

template< std::size_t Len, std::size_t Align = /*default-alignment*/ >
struct aligned_storage;

  它的第一个参数是缓冲区的长度,第二个参数是缓冲区内存对齐的大小,由于Varaint可以接受多种类型,所以我们需要获取最大的类型长度,保证缓冲区足够大,然后还要获取最大的内存对齐大小,这里我们通过前面实现的MaxInteger和MaxAlign就可以了,Varaint中内存对齐的缓冲区定义如下:

enum
{data_size = IntegerMax<sizeof(Types)...>::value,align_size = MaxAlign<Types...>::value
};using data_t = typename std::aligned_storage<data_size, align_size>::type; //内存对齐的缓冲区类型

  其次,我们还要实现对缓冲区的构造、拷贝、析构和移动,因为Variant重新赋值的时候需要将缓冲区中原来的类型析构掉,拷贝构造和移动构造时则需要拷贝和移动。这里以析构为例,我们需要根据当前的type_index来遍历Variant的所有类型,找到对应的类型然后调用该类型的析构函数。

   void Destroy(const type_index& index, void * buf){std::initializer_list<int>{(Destroy0<Types>(index, buf), 0)...};}template<typename T>void Destroy0(const type_index& id, void* data){if (id == type_index(typeid(T)))reinterpret_cast<T*>(data)->~T();}

  这里,我们通过初始化列表和逗号表达式来展开可变模板参数,在展开的过程中查找对应的类型,如果找到了则析构。在Variant构造时还需要注意一个细节是,Variant不能接受没有预先定义的类型,所以在构造Variant时,需要限定类型必须在预定义的类型范围当中,这里通过type_traits的enable_if来限定模板参数的类型。

template <class T,class = typename std::enable_if<Contains<typename std::remove_reference<T>::type, Types...>::value>::type> Variant(T&& value) : m_typeIndex(typeid(void)){Destroy(m_typeIndex, &m_data);typedef typename std::remove_reference<T>::type U;new(&m_data) U(std::forward<T>(value));m_typeIndex = type_index(typeid(U));}

  这里enbale_if的条件就是前面实现的元函数Contains的值,当没有在预定义的类型中找到对应的类型时,即Contains返回false时,编译期会报一个编译错误。

最后还需要实现内置的Vistit功能,Visit的实现需要先通过定义一系列的访问函数,然后再遍历这些函数,遍历过程中,判断函数的第一个参数类型的type_index是否与当前的type_index相同,如果相同则获取当前类型的值。

template<typename F>void Visit(F&& f){using T = typename Function_Traits<F>::template arg<0>::type;if (Is<T>())f(Get<T>());}template<typename F, typename... Rest>void Visit(F&& f, Rest&&... rest){using T = typename Function_Traits<F>::template arg<0>::type;if (Is<T>())Visit(std::forward<F>(f));elseVisit(std::forward<Rest>(rest)...);}

  Visit功能的实现利用了可变模板参数和function_traits,通过可变模板参数来遍历一系列的访问函数,遍历过程中,通过function_traits来获取第一个参数的类型,和Variant当前的type_index相同时则取值。为什么要获取访问函数第一个参数的类型呢?因为Variant的值是唯一的,只有一个值,所以获取的访问函数的第一个参数的类型就是Variant中存储的对象的实际类型。

4.bind

  C++11中新增的std::bind是一个很灵活且功能强大的绑定器,std::bind用来将可调用对象与其参数进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。下面是它的基本用法:

void output(int x, int y)
{std::cout << x << " " << y << std::endl;
}int main(void)
{std::bind(output, 1, 2)();                          // 输出: 1 2std::bind(output, std::placeholders::_1, 2)(1);     // 输出: 1 2std::bind(output, 2, std::placeholders::_1)(1);     // 输出: 2 1
}

  std::placeholders::_1是一个占位符,代表这个位置将在函数调用时,被传入的第一个参数所替代。因为有了占位符的概念,std::bind的使用非常灵活,我们可以用它来替代任意位置的参数,延迟到后面再传入实际参数。下图是bind的一个原理图,更多的原理图读者可以参考:http://blog.think-async.com/2010/04/bind-illustrated.html。

  从上图中可以看到bind把参数和占位符保存起来了,然后在后面调用的时候再按照顺序去替换占位符,最终实现延迟执行。

我们可以通过模板元来实现一个简单的bind,实现bind需要解决两个问题:

1.将tuple展开为可变模板参数

  bind绑定可调用对象时,需要将可调用对象的形参(可能含占位符)保存起来,保存到tuple中了。到了调用阶段,我们就要反过来将tuple展开为可变参数,因为这个可变参数才是可调用对象的形参,否则就无法实现调用了。这里我们会借助于一个整形序列来将tuple变为可变参数,在展开tuple的过程中我们还需要根据占位符来选择合适实参,即占位符要替换为调用实参。这里要用到前文中实现的MakeIndexes。

2.根据占位符来选择合适的实参

  这个地方比较关键,因为tuple中可能含有占位符,我们展开tuple时,如果发现某个元素类型为占位符,则从调用的实参生成的tuple中取出一个实参,用来作为变参的一个参数;当某个类型不为占位符时,则直接从绑定时生成的形参tuple中取出参数,用来作为变参的一个参数。最终tuple被展开为一个变参列表,这时,这个列表中没有占位符了,全是实参,就可以实现调用了。这里还有一个细节要注意,替换占位符的时候,如何从tuple中选择合适的参数呢,因为替换的时候要根据顺序来选择。这里是通过占位符的模板参数I来选择,因为占位符place_holder<I>的实例_1实际上place_holder<1>, 占位符实例_2实际上是palce_holder<2>,我们是可以根据占位符的模板参数来获取其顺序的。

  下面来看看bind实现的关键代码,完整的代码读者可以参考我github上的代码:https://github.com/qicosmos/cosmos/blob/master/Bind.hpp。

template <int I>
struct Placeholder{};Placeholder<1> _1; Placeholder<2> _2; Placeholder<3> _3; Placeholder<4> _4; Placeholder<5>_5; Placeholder<6> _6; Placeholder<7> _7;Placeholder<8> _8; Placeholder<9> _9; Placeholder<10> _10;template <typename T, class Tuple>
inline auto select(T&& val, Tuple&)->T&&{return std::forward<T>(val);
}template <int I, class Tuple>
inline auto select(Placeholder<I>&, Tuple& tp) -> decltype(std::get<I - 1>(tp)){return std::get<I - 1>(tp);
}template <typename R, typename F, typename... P>
inline typename std::enable_if<is_pointer_noref<F>::value, R>::type invoke(F&& f, P&&... par){return (*std::forward<F>(f))(std::forward<P>(par)...);
}template<typename Fun, typename... Args>
struct Bind_t{typedef typename decay<Fun>::type FunType;typedef std::tuple<typename decay<Args>::type...> ArgType;typedef typename function_traits<FunType>::return_type     result_type;
public:template<class F, class... BArgs>Bind_t(F& f, BArgs&... args) : m_func(f), m_args(args...){}template<typename F, typename... BArgs>Bind_t(F&& f, BArgs&&... par) : m_func(std::move(f)), m_args(std::move(par)...){}template <typename... CArgs>result_type operator()(CArgs&&... args){return do_call(MakeIndexes<std::tuple_size<ArgType>::value>::type(),std::forward_as_tuple(std::forward<CArgs>(args)...));}template<typename ArgTuple, int... Indexes >result_type do_call(IndexTuple< Indexes... >& in, ArgTuple& argtp){return invoke<result_type>(m_func, select(std::get<Indexes>(m_args),argtp)...);}private:FunType m_func;ArgType m_args;
};template <typename F, typename... P>
inline Bind_t<F, P...> Bind(F&& f, P&&... par){return Bind_t<F, P...>(std::forward<F>(f), std::forward<P>(par)...);
}template <typename F, typename... P>
inline Bind_t<F, P...> Bind(F& f, P&... par){return Bind_t<F, P...>(f, par...);
}
测试代码:
void TestFun1(int a, int b, int c)
{
}void TestBind1()
{Bind(&TestFun1, _1, _2, _3)(1, 2, 3);Bind(&TestFun1, 4, 5, _1)(6);Bind(&TestFun1, _1, 4, 5)(3);Bind(&TestFun1, 3, _1, 5)(4);
}

  由于只是展示bind实现的关键技术,很多的实现细节并没有处理,比如参数是否是引用、右值、cv符、绑定非静态的成员变量都还没处理,仅仅用来展示如何综合运用一些模版元技巧和元函数,并非是重复发明轮子,只是展示bind是如何实现, 实际项目中还是使用c++11的std::bind为好。

5总结

  可以看到C++11模板元编程能解决很复杂的问题,能实现一些几乎不可能完成的功能,比如实现了“万能”类型Variant,和泛化的绑定器bind,这些东西的实现如果不通过模版元的话,几乎是无法想象的。虽然这些应用看起来比较复杂,但是它将复杂性彻底的隐藏起来了,通过眼花缭乱的模版元技巧来解决复杂的问题,并最终给用户提供简单、强大和灵活的接口,这也是模版元编程最有魅力也最令人着迷的地方。模版元编程的应用很广,本文只是展示了一小部分的应用,如果读者还希望了解更多的应用,读还可以参考boost的mpl库和C++11的源码。

 

备注:本文是我发表在《程序员》2015.3月A,转载请注明出处。

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

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

相关文章

《零基础看得懂的C语言入门教程 》——(二)C语言没那么难简单开发带你了解流程

一、学习目标 了解DevC集成开发环境了解集成开发环境了解HelloWorld程序了解HelloWorld程序的编写方法 目录 C语言真的很难吗&#xff1f;那是你没看这张图&#xff0c;化整为零轻松学习C语言。 第一篇&#xff1a;&#xff08;一&#xff09;脱离学习误区 第二篇&#xff1…

11选5下期算法_本周六周日【高二直播】辅导网课预告:通用技术电控二三极管、多用电表测量、数字逻辑电路、解析枚举递归算法,2022浙江选考技术...

01第19-21讲 2020年11月28日29日开课目录鲸学名师考点精讲系统提高高二共3阶段精品课夯实基础冲刺技术选考97-100分&#xff01;11月28日【高二|提高|直播】高二精品直播课讲授&#xff1a;浙江选考技术科目第19讲 高二综合提高鲸学名师讲授高中通用技术&#xff1a;第19讲 电控…

十分钟完成Bash 脚本进阶!列举Bash经典用法及其案例

前言&#xff1a;在linux中&#xff0c;Bash脚本是很基础的知识&#xff0c;大家可能一听脚本感觉很高大上&#xff0c;像小编当初刚开始学一样&#xff0c;感觉会写脚本的都是大神。虽然复杂的脚本是很烧脑&#xff0c;但是&#xff0c;当我们熟练的掌握了其中的用法与技巧&am…

【经典回放】多种语言系列数据结构算法:基数排序

目录 一、算法思路 二、C#语言实现 三、C语言实现 一、算法思路 1. 思想基础 基数排序的思想就是先找出待排序中的最大者&#xff0c;然后按最大者申请一个足够大的内存空间&#xff0c;并将其初始化为零&#xff0c;然后将所有待排序的数装入其中&#xff0c;标记装入的数…

探索链路追踪在.NET6工业物联网项目中的应用

如果觉得有用&#xff0c;请留言学到了。已经会了的老哥&#xff0c;请留言就这&#xff1f;可能遇到的问题工业物联网系统自上而下一般分为ERP、Mes、SCADA、WCS、边缘网关、设备等一个生产订单从SAP发送到设备要经过上述多个系统&#xff0c;当某个环节出现问题&#xff0c;可…

《零基础看得懂的C语言入门教程 》——(三)轻轻松松理解第一个C语言程序

一、学习目标 了解C语言代码的一般结构了解函数的概念了解printf函数的使用方法了解头文件的概念了解system函数的使用方法 目录 C语言真的很难吗&#xff1f;那是你没看这张图&#xff0c;化整为零轻松学习C语言。 第一篇&#xff1a;&#xff08;一&#xff09;脱离学习误…

hdu_1728_逃离迷宫(bfs)

题目连接&#xff1a;http://acm.hdu.edu.cn/showproblem.php?pid1728 题意&#xff1a;走迷宫&#xff0c;找最小的拐角 题解&#xff1a;对BFS有了新的理解&#xff0c;DFS剪枝应该也能过&#xff0c;用BFS就要以拐角作为增量来搜&#xff0c;即以当前点为坐标&#xff0c;4…

把文件放在SD卡

2019独角兽企业重金招聘Python工程师标准>>> 在程序中访问SDCard&#xff0c;你需要申请访问SDCard的权限。 在AndroidManifest.xml中加入访问SDCard的权限如下: <!-- 在SDCard中创建与删除文件权限--> <uses-permissionandroid:name"android.permiss…

如何用 windbg 导出 C# 中的 string 内容?

咨询区 driis我在用 windbg 调试一个生产上的 程序卡死 故障 &#xff0c;在线程栈上有一个 string 类型的参数相当大&#xff0c;我用 !dumpobj 命令不能正常显示内容&#xff0c;参考如下&#xff1a;0:036> !do 00000001b30d8668 Name: System.String MethodTable: 00000…

《零基础看得懂的C语言入门教程 》——(四)C语言的基本数据类型及变量

一、学习目标 了解C语言的基本数据类型了解变量的基本概念了解变量的使用方法了解了变量的命名方法了解格式占位符了解变量的输出 目录 C语言真的很难吗&#xff1f;那是你没看这张图&#xff0c;化整为零轻松学习C语言。 第一篇&#xff1a;&#xff08;一&#xff09;脱离…

转HTML+CSS总结/深入理解CSS盒子模型

原文地址&#xff1a;http://www.chinaz.com/design/2010/1229/151993.shtml 前言&#xff1a;前阵子在做一个项目时&#xff0c;在页面布局方面遇到了一点小问题&#xff0c;于是上stackoverflow上求助。ifaou在帮助我解决我问题的同时&#xff0c;还推荐我阅读一篇有关CSS盒子…

主成分分析步骤_多元分析(1)--主成分分析

主成分分析主成分分析&#xff08;PCA&#xff09;是数据降维的一种常见方法&#xff0c;其它常见的方法还有因子分析&#xff08;FA&#xff09;,独立成分分析&#xff0c;在进行大数据处理时&#xff0c;因为数据有很多特征&#xff0c;维数过高&#xff0c;不容易进行处理且…

ArcGIS实验教程——实验十九:网络分析(最短路径实现)

ArcGIS实验视频教程合集:《ArcGIS实验教程从入门到精通》(附配套实验数据) 一、实验描述 网络分析模块用于实现基于网络数据集的网络分析功能,包括路径分析、服务区分析、最近设施点分析、OD成本矩阵分析、多路径配送分析、位置分配分析和高级网络的管理与创建等。 网络…

设计模式之策略模式和状态模式

1 策略模式 我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法&#xff0c; 我们可以简单理解为更加不同的策略对象&#xff0c;执行不同策略方法。 2 类图 3 代码实现 1&#xff09;接口&#xff1a;Strat…

AspNetCore7.0源码解读之UseMiddleware

前言本文编写时源码参考github仓库主分支。aspnetcore提供了Use方法供开发者自定义中间件&#xff0c;该方法接收一个委托对象&#xff0c;该委托接收一个RequestDelegate对象&#xff0c;并返回一个RequestDelegate对象&#xff0c;方法定义如下&#xff1a;IApplicationBuild…

《零基础看得懂的C语言入门教程 》——(五)C语言的变量、常量及运算

一、学习目标 了解C语言变量的其它创建方式了解C语言常量了解C语言的运算符 目录 C语言真的很难吗&#xff1f;那是你没看这张图&#xff0c;化整为零轻松学习C语言。 第一篇&#xff1a;&#xff08;一&#xff09;脱离学习误区 第二篇&#xff1a;&#xff08;二&#xff…

实战使用Axure设计App,使用WebStorm开发(4) – 实现页面UI

系列文章 实战使用Axure设计App,使用WebStorm开发(1) – 用Axure描述需求 实战使用Axure设计App,使用WebStorm开发(2) – 创建 Ionic 项目 实战使用Axure设计App,使用WebStorm开发(3) – 构建页面架构 实战使用Axure设计App,使用WebStorm开发(4) – 实现页面UI 实战使用Axu…

英文词频统计预备,组合数据类型练习

实例: 下载一首英文的歌词或文章&#xff0c;将所有,.&#xff1f;&#xff01;等替换为空格&#xff0c;将所有大写转换为小写&#xff0c;统计某几个单词出现的次数&#xff0c;分隔出一个一个的单词。2.列表实例&#xff1a;由字符串创建一个作业评分列表&#xff0c;做增删…

《零基础看得懂的C语言入门教程 》——(六)轻轻松松了解C语言的逻辑运算

一、学习目标 了解逻辑判断的概念了解if语句的使用方法了解switch语句的使用方法了解逻辑运算符的使用方法 目录 C语言真的很难吗&#xff1f;那是你没看这张图&#xff0c;化整为零轻松学习C语言。 第一篇&#xff1a;&#xff08;一&#xff09;脱离学习误区 第二篇&#…

TCP之滑动窗口

一、滑动窗口的基本知识 TCP滑动窗口包含了发送窗口和接收窗口 1)、TCP滑动窗口的最大值 TCP数据包头部里面有个窗口值,默认窗口是一个16bit位字段,表示窗口的字节容量,所以TCP滑动窗口的最大值是2^16-1=65535个字节,TCP里面也有窗口扩大因子可把原来16bit的窗口,扩大为…