【维生素C语言】第八章 - 实用调试技巧(三)

简介: 一名优秀的程序员是一名出色的侦探,每一次调试都是尝试破案的过程……本章将详细带你学习实用调试技巧!正式开启DEBUG

五、如何写出易于调试的代码(模拟实现strcpy)


0x00 优秀的代码

7dcc472d2b3a85e4a43dbf0b6c3e0647_20210608143601705.png


0x01 常见的coding技巧

8264b7bff81f43415a166461561b146b_20210608143910758.png


0x02 strcpy函数介绍

c5004b99ee5f8801e3a3534cb12e3716_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl81MDUwMjg2Mg==,size_16,color_FFFFFF,t_70.png


/* strcpy: 字符串拷贝 */
#include 
#include 
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char arr2[] = "hello";
  strcpy(arr1, arr2);   // 字符串拷贝(目标字符串,源字符串)
  printf("%s\n", arr1); // hello
  return 0;
}

0x03 模拟实现strcpy

💬 示例 - 模拟实现 strcpy

e8b0a19c3ea6c7e9b93da99d0f43febf_20210608150107854.png



#include 
char* my_strcpy (
  char* dest, // 目标字符串
  char* src   // 源字符串
  )
{
  while (*src != '\0') {
  *dest = *src;
  dest++;
  src++;
  }
  *dest = *src; // 拷贝'\0'
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char arr2[] = "hello";
  my_strcpy(arr1, arr2);
  printf("%s\n", arr1); // hello
  return 0;
}

0x04 优化 - 提高代码简洁性

💬 函数部分的代码,++ 部分其实可以整合到一起:


#include 
char* my_strcpy (char* dest, char* src)
{
  while (*src != '\0') {
  *dest++ = *src++;
  }
  *dest = *src;
}

0x05 优化 - 修改while

2. 甚至可以把这些代码都放到 while 内:


( 利用 while 最后的判断 ,*dest++ = *src++ 正好拷走斜杠0 )
char* my_strcpy (char* dest, char* src)
{
  while (*dest++ = *src++) // 既拷贝了斜杠0,又使得循环停止
  ;
}

0x06 优化 - 防止传入空指针

❓ 如果传入空指针NULL,会产生BUG

331059f0c8feae3b0801da9e6bcedead_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl81MDUwMjg2Mg==,size_16,color_FFFFFF,t_70.png

💡 解决方案:使用断言


断言是语言中常用的防御式编程方式,减少编程错误;

如果计算表达式expression值为假(0),那么向stderr打印一条错误信息,然后通过调用abort来终止程序运行;

断言被定义为宏的形式(assert(expression)),而不是函数;

9df5a00202ecc09d1bff6d356246278d_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl81MDUwMjg2Mg==,size_16,color_FFFFFF,t_70.png

#include 
#include 
char* my_strcpy(char* dest, char* src)
{
  assert(dest != NULL); // 断言   "dest不能等于NULL"
  assert(src != NULL);  // 断言   "src 不能等于NULL"
  while (*dest++ = *src++)
  ;
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char arr2[] = "hello";
  my_strcpy(arr1, NULL); // 👈 实验:传入一个NULL
  printf("%s\n", arr1);
  return 0;
}


🚩 运行结果如下:

7263de6753be84e32003a55d8611cb1a_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl81MDUwMjg2Mg==,size_16,color_FFFFFF,t_70.png


0x07 关于const的使用

💬 将 num的值修改为20:


int main()
{
  int num = 10;
  int* p = #
  *p = 20;
  printf("%d\n", num);
  return 0;
}

🚩  20


💬 此时在 int num 前放上 const :


const 修饰变量,这个变量就被称为常变量,不能被修改,但本质上还是变量;

6f451afff04c448c8bc64d5cb84a76e3_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl81MDUwMjg2Mg==,size_16,color_FFFFFF,t_70.png

但是!但是呢!!


int main()
{
    const int num = 10;
    int* p = #
    *p = 20;
    printf("%d\n", num);
    return 0;
}

🚩 运行结果如下

f5b4054fc4dd485101491882622d045c_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl81MDUwMjg2Mg==,size_16,color_FFFFFF,t_70.png

❗ 我们希望 num 不被修改,结果还是改了,这不出乱子了吗?!


num 居然把自己的地址交给了 p,然后 *p = 20,通过 p 来修改 num 的值,不讲武德!


💡 解决方案:只需要在 int* p 前面加上一个 const,此时 *p 就没用了


int main()
{
    const int num = 10;
    const int* p = # 
// 如果放在 * 左边,修饰的是 *p,表示指针指向的内容,是不能通过指针来改变的
    *p = 20; // ❌ 不可修改
    printf("%d\n", num);
    return 0;
}

🚩 运行结果如下:

111428aa064696d0cff5ffafdfac55cf_20210609103724499.png


🔑 解释:const 修饰指针变量的时候,const 如果放在 * 左边,修饰的是 *p,表示指针指向的内容是不能通过指针来改变的;


❓ 如果我们再加一个变量 n = 100, 我们不&num,我们&n,可不可以?


int main()
{
    const int num = 10;
    int n = 100;
    const int* p = #
    *p = 20 // ❌ 不能修改
    p = &n; // ✅ 但是指针变量的本身是可以修改的
    printf("%d\n", num);
    return 0;
}

🔑 可以,p 虽然不能改变 num,但是 p 可以改变指向,修改 p 变量的值


❓ 那把 const 放在 * 右边呢?


int main()
{
    const int num = 10;
    int n = 100;
    int* const  p = #
// 如果放在 * 右边,修饰的是指针变量p,表示的指针变量不能被改变
// 但是指针指向的内容,可以被改变
    p = 20; // ✅ 可以修改
    p = &n; // ❌ 不能修改
    printf("%d\n", num);
    return 0;
}

🔑 此时指针指向的内容可以修改,但是指针变量


❓ 如果两边都放 const :


int main()
{
    const int num = 10;
    const int* const p = #
    int n = 100;
    *p = 20; // ❌ 不能修改
    p = &n;  // ❌ 不能修改
    printf("%d\n", num);
    return 0;
}

0x08 优化 - 提高代码健壮性(加入const)


e9c902f4e41373ac601d265baa7a057e_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl81MDUwMjg2Mg==,size_16,color_FFFFFF,t_70.png

💡 为了防止两个变量前后顺序写反,我们可以利用 const 常量,给自己“设定规矩”,这样一来,当我们写反的时候, 因为是常量的原因,不可以被解引用修改,从而报错,容易发现问题之所在!


char* my_strcpy (char* dest, const char* src)
{
  assert(dest != NULL);
  assert(src != NULL);
  // while(*src++ = *dest) 👈 防止写反,加一个const
  while (*dest++ = *src++)
  ;
}

可以无形的防止你写出 while(*src++ = *dest) ,即使你写错了,编译器也会报错(语法错误);


📌 注意事项:按照逻辑加 const,不要随便加const(比如在dest前也加个const);


0x09 最终优化 - 使其支持链式访问

💬 实现返回目标空间的起始位置


#include 
#include 
char* my_strcpy (char* dest,const char* src)
{
  char* ret = dest; // 在刚开始的时候记录一下dest
  assert(dest != NULL);
  assert(src != NULL);
  while (*dest++ = *src++)
  ;
  return ret; // 最后返回dest
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char arr2[] = "hello";
  printf("%s\n", my_strcpy(arr1, arr2)); // 链式访问
  return 0;
}


0x0A 库函数写法

/***
*char *strcpy(dst, src) - copy one string over another
*
*Purpose:
*       Copies the string src into the spot specified by
*       dest; assumes enough room.
*
*Entry:
*       char * dst - string over which "src" is to be copied
*       const char * src - string to be copied over "dst"
*
*Exit:
*       The address of "dst"
*
*Exceptions:
*******************************************************************************/
char * strcpy(char * dst, const char * src)
{
        char * cp = dst;
        assert(dst && src);
        while( *cp++ = *src++ )
               ;     /* Copy src over dst */
        return( dst );
}
相关文章
|
1月前
|
存储 编译器 C语言
【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存2
【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存
|
1月前
|
程序员 C语言 C++
【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存1
【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存
|
1月前
|
C语言
C语言使用宏定义实现等级调试输出PRINT_LEVEL
C语言使用宏定义实现等级调试输出PRINT_LEVEL
|
5天前
|
NoSQL 编译器 C语言
【C 言专栏】C 语言中的调试技巧与工具
【5月更文挑战第6天】在C语言编程中,调试是必不可少的技能,涉及基本技巧如打印输出、断点调试和单步执行,以及使用GCC、GDB、Visual Studio和Eclipse CDT等工具。高级技巧包括内存检查和性能分析。通过分析问题、设置断点、逐步调试和检查逻辑来解决错误。调试时需保持耐心,合理选用工具,记录过程,并避免过度调试。熟练掌握这些技能将提升代码质量和开发效率。
【C 言专栏】C 语言中的调试技巧与工具
|
4月前
|
NoSQL Linux Redis
redis源码调试---vscode使用技巧-----C语言跳转到函数定义
redis源码调试---vscode使用技巧-----C语言跳转到函数定义
74 0
|
2月前
|
程序员 编译器 C语言
编程最重要的技术之一 — 调试(以C语言代码为例)
编程最重要的技术之一 — 调试(以C语言代码为例)
42 0
|
2月前
|
存储 程序员 编译器
C语言第十三弹---VS使用调试技巧
C语言第十三弹---VS使用调试技巧
|
6月前
|
程序员 C语言
【C语言】如何写出好(易于调试)的代码——assert和const的使用
【C语言】如何写出好(易于调试)的代码——assert和const的使用
22 0
|
2月前
|
存储 程序员 编译器
c语言从入门到实战——VS2022实用调试技巧
Visual Studio是集成开发环境,其内置了多种调试工具和技巧帮助开发人员在开发过程中解决问题。以下是一些VS实用调试技巧的简介
53 0
|
2月前
|
存储 IDE 开发工具
C语言实用调试详解
C语言实用调试详解
37 0