「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战」。
四、一些调试的实例
0x00 实例一
💬 实现代码:求 1!+ 2! + 3! ··· + n!(不考虑溢出)
int main()
{
int n = 0;
scanf("%d", &n); // 3
// 1!+ 2!+ 3!
// 1 2 6 = 9
int i = 0;
int ret = 1;
int sum = 0;
int j = 0;
for (j = 1; j <= n; j++) {
for (i = 1; i <= j; i++) {
ret *= i;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
🚩 运行结果如下:
❓ 结果应该是9才对,但是输出结果为15,代码出错了;代码又没有语法错误,代码能够运行,属于运行时错误,而调试解决的就是运行时错误;
🐞 此时我们试着调试:
💡 此时我们发现了问题:每一次求阶乘时,应该从1开始乘,所以每一次进入时 ret 要置为1;
int main()
{
int n = 0;
scanf_s("%d", &n); // 3
// 1!+ 2!+ 3!
// 1 2 6 = 9
int i = 0;
int ret = 1;
int sum = 0;
int j = 0;
for (j = 1; j <= n; j++) {
ret = 1; // 每次进入,置为1,重新开始乘
for (i = 1; i <= j; i++) {
ret *= i;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
🚩 运行结果如下:
🔺 解决问题:
① 要知道程序应该是什么结果:预期
② 调试的时候发现不符合预期,就找到问题了;
0x01 实例二
💬 下列代码运行的结果是什么?
int main()
{
int i = 0;
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 👇 越界访问了
for (i = 0; i <= 12; i++) {
printf("hehe\n");
arr[i] = 0;
}
return 0;
}
🚩 运行结果如下:
❓ 研究导致死循环的原因:
💡 解析:
🔺 本题正确答案:死循环,因为 i 和 arr 是里昂个局部变量,先创建 i,再创建 arr,又因为局部变量是放在栈区上的,栈区的使用习惯是先使用高地址再使用低地址,所以内存的布局是这样子的(如图),又因为数组随着下标的增长地址是由低到高变化的,所以数组用下标访问时只要适当的越界,就有可能覆盖到 i,而 i 如果被覆盖的话,就会导致程序的死循环;
五、如何写出易于调试的代码(模拟实现strcpy)
0x00 优秀的代码
0x01 常见的coding技巧
0x02 strcpy函数介绍
/* strcpy: 字符串拷贝 */
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[20] = "xxxxxxxxxx";
char arr2[] = "hello";
strcpy(arr1, arr2); // 字符串拷贝(目标字符串,源字符串)
printf("%s\n", arr1); // hello
return 0;
}
0x03 模拟实现strcpy
💬 示例 - 模拟实现 strcpy
#include <stdio.h>
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 <stdio.h>
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
💡 解决方案:使用断言
断言是语言中常用的防御式编程方式,减少编程错误;
如果计算表达式expression值为假(0),那么向stderr打印一条错误信息,然后通过调用abort来终止程序运行;
断言被定义为宏的形式(assert(expression)),而不是函数;
#include <stdio.h>
#include <assert.h>
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;
}
🚩 运行结果如下:
0x07 关于const的使用
💬 将 num的值修改为20:
int main()
{
int num = 10;
int* p = #
*p = 20;
printf("%d\n", num);
return 0;
}
🚩 20
💬 此时在 int num 前放上 const :
const 修饰变量,这个变量就被称为常变量,不能被修改,但本质上还是变量;
但是!但是呢!!
int main()
{
const int num = 10;
int* p = #
*p = 20;
printf("%d\n", num);
return 0;
}
🚩 运行结果如下
❗ 我们希望 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;
}
🚩 运行结果如下:
🔑 解释: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)
💡 为了防止两个变量前后顺序写反,我们可以利用 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 <stdio.h>
#include <assert.h>
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 );
}
六、模拟实现strlen函数
0x00 计数器实现
#include <stdio.h>
#include <assert.h>
int my_strlen(const char* str)
{
assert(str);
int count = 0;
while (*str) {
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
0x01 指针减指针实现
#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char* str)
{
assert(str);
const char* eos = str;
while (*eos++);
return(eos - str - 1);
}
int main()
{
char arr[] = "abcdef";
printf("%d\n", my_strlen(arr));
return 0;
}
0x02 库函数写法
/***
*strlen.c - contains strlen() routine
*
* Copyright (c) Microsoft Corporation. All rights reserved.
*
*Purpose:
* strlen returns the length of a null-terminated string,
* not including the null byte itself.
*
*******************************************************************************/
#include <cruntime.h>
#include <string.h>
#pragma function(strlen)
/***
*strlen - return the length of a null-terminated string
*
*Purpose:
* Finds the length in bytes of the given string, not including
* the final null character.
*
*Entry:
* const char * str - string whose length is to be computed
*
*Exit:
* length of the string "str", exclusive of the final null byte
*
*Exceptions:
*
*******************************************************************************/
size_t __cdecl strlen (
const char * str
)
{
const char *eos = str;
while( *eos++ ) ;
return( eos - str - 1 );
}
size_t :无符号整型(unsigned int)
__cdecl :函数调用约定
七、编程常见的错误
0x00 编译型错误
📚 直接看错误提示信息(双击),解决问题;
或者凭借经验就可以搞定,相对来说简单;
0x01 链接型错误
📚 看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。
一般是 标识符名不存在 或者 拼写错误 ;
0x02 运行时错误
📚 代码明明跑起来了,但是结果是错的;
🔑 借助调试,逐步定位问题,利用本章说的实用调试技巧解决;
0x03 建议
📜 做一个有心人,每一次遇到错误都进行自我总结,积累错误经验!