如何写出高质量的函数?快来学习这些coding技巧

相关系列文章

为什么C++17要引入std::string_view?

C++高质量编程

C++技术要点总结, 面试必备, 收藏起来慢慢看

目录

1.前言

2.函数的编码规范

3.函数设计技巧


1.前言

作为一个coder,设计出一个好的架构和写出一手高质量的代码,都是不可缺少的技能;在我理解,高质量的代码意味着代码具有比较强的扩展性、维护性,高内聚和低耦合和尽可能少的bug;函数是我们编码过程中使用频率比较高的不可缺少的步骤,如何写出高质量的函数?不仅要遵循编写函数的代码规范,而且还要遵循函数的一些设计技巧。

2.函数的编码规范

1.可重入函数使用局部变量;可重入函数使用全部变量需要保护。

2.防止将函数的参数作为工作变量。

3.函数的规模尽量限制在200行以内,不包括注释和空格行。

4.如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该指针在函数体内被意外修改。

5.函数功能尽可能的单一,不要把没有关联的语句放到一个函数中。

6.如果输入参数是以值传递的方式传递对象,则宜改用“ const &”方式来 传递,这样可以省去临时对象的构造和析构过程,从而提高效率。

7.避免设计多参数函数,参数个数尽量控制在 5 个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。

8.尽量不要使用类型和数目不确定的参数。

9.善于使用断言,检查参数非法性和避免错误情况,提高程序可测性。

说明:断言是对某种假设条件进行检查(可理解为若条件成立则无动作,否则应报告),它可以快速发现并定位软件问题,同时对系统错误进行自动报警。断言可以对在系统中隐藏很深,用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的可测性。

10.循环体内工作量最小化,多重循环中,应将最忙的循环放在最内层。

3.函数设计技巧

1.在用C语言编写一个函数封装成dll,设置一个回调,dll有事件就调用回调,通知上层,函数设计为:

typedef  void (*eventCallBack)(int x, int y, void* pParam);
void  setEventCallBack(eventCallBack, void* pParam);

这个参数void* pParam是关键,它可以把设置回调前的状态通过dll流入回调后,省去了程序自身保留状态的过程,简化了流程。举个例子,在C++中需要在类中调用这个函数,那就可以把这个函数和类的this关联起来,函数调用就比较顺手了,示例如下:

class MyTest
{
public:
    explicit MyTest(){
        setEventCallBack(&MyTest::_eventCallBack, this);
    } 
    void event(int x, int y){
        //...
    }

private:
    static void  _eventCallBack(int x, int y, void* pParam);
};

void  MyTest::_eventCallBack(int x, int y, void* pParam)
{
    MyTest* pTest = static_cast<MyTest*>(pParam);
    if (pTest ){
        pTest->event(x, y);
    }
}

在构造函数把this指针通过函数setEventCallBack把回调函数_eventCallBack设置进去,事件通知的时候,又把void*转换为this指针,直接调用event函数。如果没有void*参数,需要事先保存MyTest*,有了viod*就省去这个步骤了。

2.设计函数通过两次调用获取返回值

在有些场景下,比如通过文章的id获取文章的内容,设计函数为:

int  getBookContent(char* pContent, int& len);

用户调用的时候,他不知道pContent需要开辟多大的空间,只能尽可能的把pContent弄大一些,尽管如此,但还是不一定能满足要求,怎么办呢?此时,就可以把函数设计分为两步:第一步获取内容的大小,第二步获取内容数据,当然也可以通过函数返回值来判断传入的pContent缓冲区是否满足要求,调用过程可以这样:

int  getBookContent(char* pContent, int& len);

void test()
{
    int len  = 0;
    int result = 0;
    
    //[1] 第一步
    result = getBookContent(null, len);

    //[2] 第二步
    std::unique_ptr<char[]> pContent(new char[len]);
    result = getBookContent(pContent.get(), len);
}

实际案例:windows系统函数GetAdaptersInfo,获取网卡配置信息

DWORD GetAdaptersInfo(
  PIP_ADAPTER_INFO pAdapterInfo,  //指向一个缓冲区,用来取得IP_ADAPTER_INFO结构列表
  PULONG pOutBufLen   //指定上面缓冲区大小,如果大小不够,此参数返回所需大小
)

IP_ADAPTER_INFO结构包含了本地计算机网络适配器的信息

#include <iostream>
#include <windows.h>
#include <Iphlpapi.h>
#pragma comment(lib, "Iphlpapi.lib")

using namespace std;

BOOL GetNetworkAdapterInfo()
{
    PIP_ADAPTER_INFO pIPAdapterInfo = nullptr;
    ULONG size = sizeof(IP_ADAPTER_INFO);
    //填充pIPadapterInfo变量,其中size既是一个输入量,也是一个输出量
    int nRet = GetAdaptersInfo(null, &size);
    //记录网卡数量
    int netCarNum = 0;

    if (ERROR_BUFFER_OVERFLOW == nRet)
    {
        //如果返回此参数,说明GetAdaptersInfo参数传递的内存空间大小不够,同时传出size表示需要的内存空间大小
        //释放原来的内存空间
        pIPAdapterInfo = (PIP_ADAPTER_INFO)new byte[size];
        //再次调用GetAdaptersInfo填充结构体
        nRet = GetAdaptersInfo(pIPAdapterInfo, &size);
    }

    if (ERROR_SUCCESS == nRet)
    {
        //...
    }
  //释放分配的内存
  if (pIPAdapterInfo)
    delete pIPAdapterInfo;    
    return true;
}

调用函数GetAdaptersInfo第一次传入null,获取到了电脑所有网络适配器数据占空间的总大小,第二次动态申请内存,获取所有的网络适配器真实数据。

3.实现链式表达式

就是为了后来函数调用者方便而设计的,这种方便的实现方法,看起来就是链子链在一起的,所以称为链式表达式;strcpy函数就是这样的典型:

char *strcpy(char *strDest, const char *strSrc);
{
    assert((strDest!=NULL) && (strSrc !=NULL)); 
    char *address = strDest; 
    while( (*strDest++ = * strSrc++) != ‘\0’ ) {;}
    return address ;
}

为什么要返回char*,就是为了实现链式表达式,实现如下面这样的调用:

int length = strlen( strcpy( strDest, “hello world”) );

在我的另外一篇博客中提到的序列化类CDataStream就是链式表达式最好的示例,可自行阅读:

手撕代码: C++实现数据的序列化和反序列化-CSDN博客

实际案例:C++标准库中的std::cout 

std::cout类继承自ostream类,ostream(即basic_ostream)类继承自ios类,ios类继承自ios_base类,详细的继承关系如下图:

在basic_ostream类中详细实现了operator<<操作符

class basic_ostream : virtual public basic_ios<_Elem, _Traits> { // control insertions into a stream buffer
    //...

    //...
    basic_ostream& __CLR_OR_THIS_CALL operator<<(
        basic_ostream&(__cdecl* _Pfn)(basic_ostream&) );
    basic_ostream& __CLR_OR_THIS_CALL operator<<(_Myios&(__cdecl* _Pfn)(_Myios&) );
    basic_ostream& __CLR_OR_THIS_CALL operator<<(ios_base&(__cdecl* _Pfn)(ios_base&) );
    basic_ostream& __CLR_OR_THIS_CALL operator<<(bool _Val);
    basic_ostream& __CLR_OR_THIS_CALL operator<<(short _Val);
    basic_ostream& __CLR_OR_THIS_CALL operator<<(unsigned short _Val);
    basic_ostream& __CLR_OR_THIS_CALL operator<<(int _Val);
    basic_ostream& __CLR_OR_THIS_CALL operator<<(unsigned int _Val);
    basic_ostream& __CLR_OR_THIS_CALL operator<<(long _Val);
    basic_ostream& __CLR_OR_THIS_CALL operator<<(unsigned long _Val);
    basic_ostream& __CLR_OR_THIS_CALL operator<<(long long _Val);
    basic_ostream& __CLR_OR_THIS_CALL operator<<(unsigned long long _Val);
    basic_ostream& __CLR_OR_THIS_CALL operator<<(float _Val);
    basic_ostream& __CLR_OR_THIS_CALL operator<<(double_Val);
    basic_ostream& __CLR_OR_THIS_CALL operator<<(long double_Val);
    basic_ostream& operator<<(nullptr_t);
    basic_ostream& __CLR_OR_THIS_CALL operator<<(_Mysb* _Strbuf);
    //...
};

重载操作符<<,函数都是返回的basic_ostream&,就是为了实现链式表达式,完成如下的调用:

bool a = false;
int  b = 100;
double c = 15.666
std::cout << a << b << c << "hello world" << std::endl;

4.函数参数类型选择

C++17增加了std::string_view它在很多情况下会优于使用std::string 。

尤其是用做函数形参的时候,使用std::string_view基本一定优于老式的const std::string&这种写法。

C++17之前的写法:

void func(const std::string&s){
    std::cout << s << '\n';
}

C++17之后的写法:

void func(std::string_view s){
    std::cout << s << '\n';
}

std::string_view 只是一个视图,用来指代原字符串的,保有一个size和一个指针即可。比起std::string有一个构造的过程,要高效一些,更多详细的介绍可以参考 为什么C++17要引入std::string_view?-CSDN博客。

5.重载类型强制转换运算符

先看一下代码:

//【1】
namespace xyLinkCorePrivate {
	template <typename T, typename Tag = void>
	struct TCheck {
		using type = T;
	};
	template <typename T>
	struct TCheck < T, typename std::enable_if_t<!std::is_arithmetic_v<T> && !std::is_pointer_v<T>>> {
		//除数值类型和指针除外
		using type = const T&;
	};
}

//【2】
template <typename T>
class ICloneable
{
public:
	virtual T* clone() const = 0;
};


//【3】
class IMarshalData
{
public:
	virtual CByteArray toByteArray() const = 0;
	virtual bool parseData(const char* pData, PUInt64 len) = 0;
};

class IPersistData
{
public:
	virtual std::string getPersistValue() const = 0;
	virtual bool setPersistValue(const std::string& data) = 0;
};

class IMemoryData
{
public:
	virtual bool setValue(const std::any& data) = 0;
	virtual std::any value() const = 0;
};

class IParamField : public IMarshalData
	, public IPersistData
	, public IMemoryData
	, public ICloneable<IParamField>
{
public:
	virtual ~IParamField() {}
	virtual IParamField* clone() const = 0;
};

template<typename T>
class CBasicParamFieldMemoryData : public IMemoryData
{
public:
	explicit CBasicParamFieldMemoryData(typename xyLinkCorePrivate::TCheck<T>::type value) : m_value(value) {}
	CBasicParamFieldMemoryData& operator=(typename xyLinkCorePrivate::TCheck<T>::type value) {
		m_value = value;
		return *this;
	}

	bool setValue(const std::any& data) override {
		try
		{
			if (data.has_value()) {
				m_value = std::any_cast<T>(data);
				return true;
			}
			assert(false);
			return false;
		}
		catch (std::bad_any_cast& e)
		{
			assert(false);
			return false;
		}
	}
	std::any value() const override {
		return m_value;
	}
private:
	T  m_value;
};

//T一定是基本类型参数
template<typename T, typename Y = void>
class CBasicParamField : public IParamField
{
public:
	explicit CBasicParamField(const T value = 0) : m_value(value) {}
	CByteArray toByteArray() const override {
		CByteArray data;
		CDataStream dataStream(&data);
        const T temp = std::any_cast<const T>(m_value);
		dataStream << temp;
		return data;
	}
	bool parseData(const char* pData, PUInt64 len) override {
		assert(len == sizeof(T));
		CByteArray data(pData, len);
		CDataStream dataStream(&data);
	    T temp;
		dataStream >> temp;
		m_value = temp;
		return true;
	}
	IParamField* clone() const override {
		//...
	}
	std::string getPersistValue() const override {
		//...
	}
	bool setPersistValue(const std::string& data) override {
		return true;
	}
	std::any value() const override {
		return m_value.value();
	}
	bool setValue(const std::any& data) override {
		return m_value.setValue(data);
	}
private:
	CBasicParamFieldMemoryData<T>  m_value;
};

CByteArray 和 CDataStream 的用法可参考:

手撕代码: C++实现数据的序列化和反序列化-CSDN博客

        在类 CBasicParamField 中 toByteArray() 函数使用了std::any_cast转化为 const T,如果使用了struct或class,必然有个拷贝构造的过程;parseData函数使用了 m_value = temp 同样也有赋值构造的过程,如果struct或class的构造函数比较复杂,那么这样代码编写的效率就比较低了,那怎么做才能避免这个拷贝的过程呢?

        于是我们想到了重载 T& 这个强制转换运算符,在toByteArray() 和 parseData() 函数都使用这个引用,直接赋值,没有拷贝的过程,效率就提高了。

        在类 CBasicParamFieldMemoryData 中增加两个重载引号转换运算符,代码如下:

	//[1]
    operator T& () {
		return m_value;
	}
    //[2]
	operator const T& () const {
		return m_value;
	}

        需要修改值的,调用2函数;不需要修改值的,调用1函数。

        在使用的地方必须使用static_cast转换使用,如:

   const T& temp = static_cast<const T&>(m_value);

   T& temp = static_cast<T&>(m_value);

  上述代码经过优化后的如下:

template<typename T>
class CBasicParamFieldMemoryData : public IMemoryData
{
public:
	explicit CBasicParamFieldMemoryData(typename xyLinkCorePrivate::TCheck<T>::type value) : m_value(value) {}
	CBasicParamFieldMemoryData& operator=(typename xyLinkCorePrivate::TCheck<T>::type value) {
		m_value = value;
		return *this;
	}

	bool setValue(const std::any& data) override {
		try
		{
			if (data.has_value()) {
				m_value = std::any_cast<T>(data);
				return true;
			}
			assert(false);
			return false;
		}
		catch (std::bad_any_cast& e)
		{
			assert(false);
			return false;
		}
	}
	std::any value() const override {
		return m_value;
	}
    operator T& () {
		return m_value;
	}
	operator const T& () const {
		return m_value;
	}
private:
	T  m_value;
};

//T一定是基本类型参数
template<typename T, typename Y = void>
class CBasicParamField : public IParamField
{
public:
	explicit CBasicParamField(const T value = 0) : m_value(value) {}
	CByteArray toByteArray() const override {
		CByteArray data;
		CDataStream dataStream(&data);
		const T& temp = static_cast<const T&>(m_value);
		dataStream << temp;
		return data;
	}
	bool parseData(const char* pData, PUInt64 len) override {
		assert(len == sizeof(T));
		CByteArray data(pData, len);
		CDataStream dataStream(&data);
		T& temp = static_cast<T&>(m_value);
		dataStream >> temp;
		return true;
	}
	IParamField* clone() const override {
		//...
	}
	std::string getPersistValue() const override {
		//...
	}
	bool setPersistValue(const std::string& data) override {
		return true;
	}
	std::any value() const override {
		return m_value.value();
	}
	bool setValue(const std::any& data) override {
		return m_value.setValue(data);
	}
private:
	CBasicParamFieldMemoryData<T>  m_value;
};

C++中的explicit关键字详解-CSDN博客

6.未完待续。。。

你们知道还有哪些函数编程规范和实现技巧,欢迎在评论区留言讨论。

参考:

为什么C++17要引入std::string_view?-CSDN博客

  • 56
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
### 回答1: channel coding是一种用于提高通信系统可靠性的技术,其主要目的是在数据传输过程中引入冗余信息,以便在信道中出现噪声和误码时进行纠正。MATLAB是一种常用的科学计算和仿真软件,其中包括许多用于通信系统设计的功能和工具。 在MATLAB中,可以通过使用通信系统工具箱来实现channel coding函数。该工具箱提供了一系列用于通信系统设计和仿真的函数和算法。以下是一些常用的channel coding函数: 1. convenc:这个函数用于实现卷积编码,将输入比特序列编码为输出比特序列。具体来说,该函数使用给定的生成多项式和约束长度对输入序列进行编码。 2. vitdec:这个函数用于实现维特比译码,将接收到的编码序列译码为原始输入序列。维特比译码是一种基于有限状态机的最大概率译码算法,通过在信道传播路径中进行前向和后向传播来估计最佳的编码序列。 3. ldpcenc:这个函数用于实现低密度奇偶校验编码(LDPC编码),将输入比特序列编码为输出比特序列。LDPC编码是一种具有低复杂性的前向纠错编码,通过使用稀疏校验矩阵和迭代解码算法来提高系统性能。 4. ldpcdec:这个函数用于实现LDPC译码,将接收到的编码序列译码为原始输入序列。该函数使用和 ldpcenc 相同的稀疏校验矩阵和迭代解码算法进行译码。 这些函数提供了一个简便的方式来实现常用的channel coding技术,并且可以根据具体的系统需求来选择合适的编码方案。通过MATLAB中的这些函数,设计师可以进行各种通信系统的建模和仿真,以评估系统性能并进行性能优化。 ### 回答2: 在MATLAB中,有几种方法可以实现信道编码函数。其中最常用的一种是使用调制函数,例如convenc。该函数用于将输入二进制序列编码为经过FEC(前向纠错)编码的信号。它可以应用于各种调制方案,例如卷积码、哈达码、RS码等。此函数的语法如下: codedBits = convenc(inputBits, trellis) 其中,inputBits是输入的二进制序列,trellis是描述编码方案的Trellis结构。输出codedBits是经过FEC编码的信号。 另一种常用的方法是使用编码器对象。编码器对象可以使用comm.CodedBitEncoder函数来创建,并通过encode方法来实现信道编码。该方法的语法如下: encoder = comm.CodedBitEncoder(coder, 'TerminationMethod', termination) encodedBits = encode(encoder, inputBits) 其中,coder是编码方案的类型,例如'conv', 'hamm'等,termination是终止方法,例如'Truncated', 'Terminated'等。encodedBits是经过FEC编码的信号。 除了以上两种方法,还可以使用自定义的编码函数来实现信道编码。可以使用for循环和逻辑运算符实现卷积码、海明码等编码方案。例如,对于卷积码,可以使用MATLAB的bitxor 和 bitand函数对输入位进行逻辑操作,并将结果存储在输出序列中。 综上所述,MATLAB提供了多种方法来实现信道编码函数,其中调制函数和编码器对象是最常用的方法。对于特定的编码方案,也可以自定义编码函数来实现信道编码。无论使用哪种方法,都可以根据实际需求选择合适的函数来实现信道编码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值