《软件开发的科学与艺术》节选-写好代码的十个秘诀

    本文选自电子工业出版社2002年即将出版的由微软公司华人专家编著的《软件开发的科学与艺术》一书。全书透彻解析了微软软件开发的思想与过程。

 双手互搏, 无坚不摧

     作为一个软件开发人员,必须测试自己的程序,使得代码做得更好,更加稳定。就我个人的经验来说,如果没有测试过代码,程序就不可能正确运行。

 

     另外,在同一组的开发人员之间做得很多的一件事就是:别人来对你的代码进行检查,反过来你对别人的代码进行检查,这个过程不仅是希望检查的人来发现你的代码中的问题,或是你去发现别人代码中的问题,更重要的是在向别人讲解你的代码的过程中,可以发现自己遗漏的地方和问题,理顺自己思路。

 

     下面这段程序是我在开发Exchange Server时写的一段代码,当时写完以后我没有测试它。因为这段代码实在是太简单了,只有几行代码:取文件的长度,如果出错就返回。于是我仅仅是编译通过后就将其提交(checkin)到实际产品中了。

 

     结果第二天早上当我到办公室的时候,发现我的三位上司都已经铁青着脸在那里等我了。原来,整个Exchange Server都运行不起来了!因为我的这段代码被加在了Exchange Server启动代码序列中,当Server启动时,由于我这段代码的错误,一启动就失败,导致了DOAdead on arrival)。

 

.

815

 

 

//

// Get file size first

//

DWORD   dwFileSize = GetFileSize hFile, NULL ;

if dwFileSize = -1 {

  // what can we do ? keep silent

  ErrorTrace0, "GetFileSize failed with %d", GetLastError());

  return;

}

 

 

     注:GetFileSize调用失败时将返回–1

 

     这段代码的错误在于:if的判断条件写成了赋值,所以无论怎样都会出错,然后返回。

 

     其实改进的方法很简单:就是将-1移到前面。这样,如果你遗漏了一个“=”,编译时编译器就会发现错误。所以在if语句中,要把常量放在前面。

 

//

// Get file size first

//

DWORD   dwFileSize = GetFileSize hFile, NULL ;

if -1 == dwFileSize {

  // what can we do ? keep silent

  ErrorTrace0, "GetFileSize failed with %d", GetLastError());

  return;

}

 

     这件事情给我的教训是很深刻的。

 

见招拆招, 滴水不漏

错误情况(Error Case)是指那些不易重现的错误。一定要对错误情况进行处理,免得程序崩溃。其中最常出现的情况如下。

×    内存耗尽。不要认为100 B内存很小,不会出错。因为在系统运行时,尽管我们自己的程序申请的内存很少,但是不能保证别人的程序申请的内存也很少。在开发过程中就经常出现这样的情况:别人的程序申请的内存太多,导致我的程序由于内存申请不到而崩溃了。

×    异常。在C++中经常会出现异常,要做好处理它的准备。用过MFC的用户都知道,如果出现异常,就必须处理它,否则程序随时会崩溃。

×    网络中断。不要以为发送一个socket都会成功。尤其是用异步socket做一些网络软件,客户端将数据包发送到服务器端时很容易出错。由于是异步,所以尽管发送方是正确的,但过一段时间接收方就可能出错,而且这种异步错误是很难检测的。有些错误可以在调试过程中借助一些手段产生出来,有些错误则是不能重现的。在错误不能重现的情况下,我们就要做一些工具,迫使错误情况出现。

    

     另外,在编程的过程中还应该注意:

×    C++的对象的构造函数中不要做一些可能会失败的操作,如内存的操作,因为在构造函数中出错后是没有方法知道的。

×    处理错误情况时,要释放分配到的资源;接口中应该清楚地定义程序的行为,如返回状态,异常的处理等,要让调用者清楚地知道接口的定义。

×    千万不要忽略错误,造成程序崩溃或退出。

 

8.10

 

 


 

CWInfFile::CWInfFile() {

  m_plLines     = new TPtrList(); // ...

  m_plSections  = new TPtrList(); // ...

  m_ReadContext.posLine = m_plLines->end();

    . . .

}

 

     这段程序中最大的问题是MFC的new操作,如果失败了会产生异常。如果前一个new操作分配正常,而后面的new分配出错,则前一个变量的分配会导致内存的泄漏。解决的办法是:用一个try…catch语句来处理,如果出现异常,则删除变量,释放内存。

 

CWInfFile::CWInfFile() {

try {

  m_plLines     = new TPtrList(); // ...

  m_plSections  = new TPtrList(); // ...

  m_ReadContext.posLine = m_plLines->end();

    . . .

} catch ( . . . ) {

   if (m_plLines) delete m_plLines;

   if (m_plSections) delete m_plSections;

 }

}

 

     但是,这里面还有一个问题:由于构造函数是最先执行的,如果new分配出错,就会执行delete语句,而此时变量m_plLines和m_plSections还没有初始化,若对它们进行delete操作就会出错。所以我们应该在构造函数中将其初始化。

 

CWInfFile::CWInfFile() : m_plLines(NULL), m_plSections(NULL) {

try {

  m_plLines     = new TPtrList(); // ...

  m_plSections  = new TPtrList(); // ...

  m_ReadContext.posLine = m_plLines->end();

    . . .

} catch ( . . . ) {

   if (m_plLines) delete m_plLines;

   if (m_plSections) delete m_plSections;

 }

}

 

     请注意,这里如果用的不是MFC,则new操作出错时就会返回NULL,而不会抛出异常,那么第三个变量的初始化就会出错。

 

CWInfFile::CWInfFile() : m_plLines(NULL), m_plSections(NULL) {

try {

  m_plLines     = new TPtrList(); // ...

  m_plSections  = new TPtrList(); // ...

  m_ReadContext.posLine = m_plLines->end();

    . . .

} catch ( . . . ) {

   if (m_plLines) delete m_plLines;

   if (m_plSections) delete m_plSections;

 }

}

 

     下面讲的是在构造函数中不要使用一些会失败的操作。


 

 

8.11

 

 

 


 

Class foo {

private:

    CHAR* m_pszName;

    DWORD m_cbName;

 

public:

    foo(CHAR* pszName);

    CHAR* GetName()

    {return m_pszName;}

};

 

foo::foo(CHAR* pszName)

{

    m_pszName = (BYTE*) malloc(NAME_LEN);

    if (m_pszName == NULL) {

        return;

    }

    strcpy(m_pszName, pszName);

    m_cbName = strlen(pszName);

}

……

 

foo* pfoo = new foo(MyName;

if (pfoo) {

    CHAR c = *(pfoo->GetName());

}

 

     由于在构造函数中无法返回错误码,当我们使用new操作符创建一个foo对象时,会调用其构造函数,如果malloc函数出错,构造函数就直接返回,则pfoo就没有分配值,这样在后面的程序中对pfoo的访问就会出错。因此,在构造函数中不要用一些会失败的操作。

 

     在下面这个改动后的版本中,构造函数中不再包含失败的成分,而是增加了一个init函数来完成内存的分配操作。在使用中先调用new函数再调用init函数,检验返回值,判断是否执行正确,有错就将其返回。

 

Class foo {

private:

  CHAR* m_pszName;

  DWORD m_cbName;

 

public:

  foo();

  HRESULT

  Init(CHAR* pszName);

};

 

foo::foo()

{

  m_cbName = 0;

  m_pszName = NULL;

}

 

HRESULT foo::Init(CHAR* pszName)

{

  HRESULT hr = S_OK;

  if (pszName) {

    m_cbName = lstrlen(pszName);

    m_pszName = (CHAR*)malloc(m_cbName+1);

    if (m_pszName == NULL) {

      hr = E_OUTOFMEMORY;

      return hr;

    }

    strcpy(m_pszName, pszName);

  } else {

    hr = E_INVALIDARG;

  }

  return hr;

}

 

 

阅读终点,创作起航,您可以撰写心得或摘录文章要点写篇博文。去创作
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: 好的,下面是基于STM32的ADS1118数据采集的配置代码(使用STM32CubeIDE): ```c #include "stm32f1xx_hal.h" #include "ads1118.h" /* 定义ADS1118相关寄存器 */ #define ADS1118_CONVERSION_REG 0x00 #define ADS1118_CONFIG_REG 0x01 #define ADS1118_LO_THRESH_REG 0x02 #define ADS1118_HI_THRESH_REG 0x03 /* 定义ADS1118转换配置参数 */ #define ADS1118_CONFIG_MUX_0 0x4000 /* AINP = AIN0, AINN = GND */ #define ADS1118_CONFIG_PGA_6_144 0x0200 /* FSR = ±6.144V */ #define ADS1118_CONFIG_MODE_CONT 0x0000 /* 连续模式 */ #define ADS1118_CONFIG_DR_860 0x0080 /* 数据率为860 SPS */ #define ADS1118_CONFIG_TS_MODE 0x0001 /* 温度传感器模式 */ /* 定义ADS1118读取指令 */ #define ADS1118_CMD_RDATA 0x0000 #define ADS1118_CMD_RCONFIG 0x8583 #define ADS1118_CMD_WCONFIG 0x4383 /* 定义ADS1118 SPI传输延时 */ #define ADS1118_SPI_DELAY_MS 10 /* 定义ADS1118 GPIO引脚 */ #define ADS1118_CS_GPIO_Port GPIOB #define ADS1118_CS_Pin GPIO_PIN_12 /* 定义ADS1118 SPI句柄 */ extern SPI_HandleTypeDef hspi2; /** * @brief 通过SPI总线读取ADS1118的指定寄存器值 * @param addr 要读取的寄存器地址 * @return 读取到的寄存器值 */ static uint16_t ADS1118_ReadRegister(uint8_t addr) { uint16_t value = 0; /* 选择ADS1118芯片 */ HAL_GPIO_WritePin(ADS1118_CS_GPIO_Port, ADS1118_CS_Pin, GPIO_PIN_RESET); /* 发送读取指令 */ uint16_t cmd = ADS1118_CONVERSION_REG | (addr << 8); HAL_SPI_Transmit(&hspi2, (uint8_t *)&cmd, 1, ADS1118_SPI_DELAY_MS); /* 读取寄存器值 */ HAL_SPI_Receive(&hspi2, (uint8_t *)&value, 1, ADS1118_SPI_DELAY_MS); HAL_SPI_Receive(&hspi2, (uint8_t *)&value + 1, 1, ADS1118_SPI_DELAY_MS); /* 取消ADS1118芯片选择 */ HAL_GPIO_WritePin(ADS1118_CS_GPIO_Port, ADS1118_CS_Pin, GPIO_PIN_SET); return value; } /** * @brief 通过SPI总线向ADS1118的指定寄存器入指定值 * @param addr 要入的寄存器地址 * @param value 要入的寄存器值 */ static void ADS1118_WriteRegister(uint8_t addr, uint16_t value) { /* 选择ADS1118芯片 */ HAL_GPIO_WritePin(ADS1118_CS_GPIO_Port, ADS ### 回答2: 基于STM32一个配置ADS1118数据采集的代码需要分为几个步骤,包括配置STM32的GPIO和SPI通信以及ADS1118的寄存器设置。 首先,需要在STM32中配置SPI通信。配置SPI的时钟,数据节选,传输模式等参数。然后,设置GPIO口的功能模式为SPI模式,并设置CS引脚为输出模式。 接下来,需要配置ADS1118的寄存器。首先入控制寄存器来设置ADS1118的采样率和增益。然后,入配置寄存器来设置输入通道、比较器模式和参考电压等。 在代码中,可以使用STM32提供的SPI发送函数来发送配置寄存器的数据,并通过SPI接收函数读取ADS1118的返回数据。可以使用delay函数来添加适当的延时等待ADS1118的采集结束。 之后,可以使用SPI发送函数发送指令字节,以读取ADS1118的采样数据。通过SPI接收函数读取ADS1118返回的数据,并根据相应的数据处理算法来获取和解析实际的采样值。 在代码的最后,可以将采集到的数据发送到外部设备(如PC)或进行相应的处理和存储等。 需要注意的是,在编代码时,需要根据对应的STM32型号和ADS1118的具体规格来进行适当的配置和寄存器设置。另外,还需要根据实际的电路连接方式来配置相关的GPIO引脚。 总之,基于STM32编配置ADS1118数据采集的代码需要配置SPI通信和GPIO口,并通过相应的寄存器设置来配置ADS1118的参数。然后,可以通过SPI发送和接收函数来控制ADS1118,获取和解析采样数据。 ### 回答3: 基于STM32的ADS1118数据采集可以通过以下步骤进行配置: 步骤一:引入ADS1118的头文件和STM32的库文件。 首先需要引入ADS1118的头文件,包括ADS1118寄存器的定义和相关函数的声明。同时还需要引入STM32的库文件,包括GPIO、SPI等相关库文件。 步骤二:初始化SPI通信。 配置STM32的SPI通信参数,包括SPI的工作模式、传输速度、数据位、校验等。 步骤三:配置ADS1118工作模式。 使用SPI发送配置命令给ADS1118,包括测量通道、参考电压、增益等参数。可以根据具体的采集需求设置相关参数。 步骤四:进行数据采集。 通过SPI向ADS1118发送采集命令,并读取采集结果。可以根据ADS1118的工作模式选择单次转换模式或连续转换模式。 步骤五:处理采集数据。 根据ADS1118的工作模式和采集参数,将采集到的原始数据转换为具体的物理量,如电压、温度等,并进行相应的处理。 步骤六:输出结果。 将处理后的采集结果输出,可以通过串口、LCD显示屏等方式展示。 最后需要对ADS1118进行复位或休眠操作,释放资源并结束采集过程。 总结: 以上是基于STM32编配置ADS1118数据采集的主要步骤,具体的实现过程需要根据ADS1118的具体配置和STM32的硬件资源进行调整。同时,还可以根据需求添加额外的功能,如数据存储、异常处理等,以满足实际应用的要求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jiangtao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值