C语言学习记录—调试技巧

本文围绕C语言展开,介绍了调试的概念、基本步骤,区分了Debug和Release版本。阐述调试环境准备、常用快捷键及查看临时变量、内存等信息的方法,通过实例展示调试作用。还讲解模拟实现strcpy库函数和const的作用,最后分析编译型、链接型和运行时三类编程常见错误及解决办法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第一节:调试是什么?

1. 调试

英语:Debugging / Debug,又称除错,是发现和减少计算机程序或电子仪器设备中程序
错误的一个过程。

2. 调试的基本步骤

  • 发现程序错误的存在
  • 以隔离、消除等方式对错误进行定位
  • 确定错误产生的原因
  • 提出纠正错误的解决办法
  • 对程序错误予以改正,重新测试

3. Debug和Release的介绍

Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。

Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。

第二节:调试介绍

1. 调试环境准备

在环境中选择 debug 选项,才能使代码正常调试。

2. 常用快捷键

F5
启动调试,经常用来直接跳到下一个断点处。
F9
创建断点和取消断点
断点的重要作用,可以在程序的任意位置设置断点。
这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。
F10
逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最
长用的)。
CTRL + F5
开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。
int main()
{
	int i = 0;
	int arr[10] = { 0 };
	//赋值
	//for (i = 0; i < 10; i++)
	//{
	//	arr[i] = i;
	//}
	//输入
	for (i = 0; i < 10; i++)
	{
		scanf("%d", &arr[i]);
	}
	//假设经过分析,上方for循环没有问题,那么要用F9设置断点直接调试下方for循环
	//断点:程序遇到断点就会停下来不会在往后执行,这样就可以一步一步调试
	//打印
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}

	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

3. 查看临时变量的值

4. 查看内存信息

5. 查看调用堆栈

6. 查看反汇编

F10打开调试有,在编辑窗口点击鼠标右键,选择反汇编

7. 查看寄存器

第三节:调试实例

实例一

通过上图可以看到,在j=3求3的阶乘时,第一次i * ret应该是1*1,但是第一次进入内层循环的时候ret就已经等于2了。这是因为上一次求完2的阶乘留下的。

通过调试可以清晰的看到循环中每个变量的变化,由此找到了问题。所以应该在内层循环开始前,在增加一条语句ret=1,这样每次重置ret的结果,防止上一次计算的阶乘结果被带到下一次阶乘的计算中。

实例二

从上图可以看到,i和arr[12]总是同时变化,到修改arr[12]时,i也被修改了,所以造成了死循环

第四节:如何写出好代码

1. 模拟实现库函数:strcpy

首先可以在MSDN中看到strcpy的用法char* strcpy(char* strDestination, const char* strSource);

int main()
{
	char arr1[20] = "XXXXXXXXXXXX";//验证是否拷贝了\0,如果拷贝了\0,那么倒数第三个X会被覆盖
	char arr2[] = "hello bit";
    strcpy(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}

通过上图可以看到strcpy库函数在拷贝时,会拷贝\0。

版本一
void my_strcpy(char* dest, char* src)
{
	while (*src != '\0')
	{
		*dest = *src;
		dest++;
		src++;
	}
	//上方代码没有将\0拷贝,但*src已经指向了\0
	*dest = *src;//最后一步将\0也拷贝
}

优化版本一
//优化版本1
void my_strcpy(char* dest, char* src)
{
	while (*src != '\0')
	{
		*dest++ = *src++;//优化1:后置++。因为先解引用才++
	}
	*dest = *src;
}

优化版本二
//优化版本2
#include <assert.h>
//为了防止(下方代码)有人把源字符串和destination写反,在源字符串指针前const,把源字符串的指针修改为常对象,让它不能修改
void my_strcpy(char* dest, const char* src)
{
	//防止传了空指针
	//断言
	assert(src != NULL);//这个表达式判断为假(即src为空指针)时,assert就报错
	assert(dest != NULL);
	
	//这样,如果像下面写反,编译器会报错
	//while (*src++ = *dest++)

    //1. 首先*src指向字符串的h,并放到dest第一个X位置上
	//2. 这是一个赋值表达式,h赋值过去的是时候,这个表达是的结果就是h的ASCII码值
	//3. 这个ASCII码值不为0,不是假,所以进入循环
	//4. 然后因为后置++,src和dest往后移动一位,继续重复上述123步骤
	//因为拷贝的每一个字符的ASCII码值都不为0,所以都进入循环,
	//遇到\0,*src依然赋值给了*dest。此时表达式结果0,循环停止
	while (*dest++ = *src++)
	{
		;
	}
	//*dest = *src;//上面也拷贝,这里也拷贝可以优化
}

优化版本三

为什么库函数strcpy返回char*,是为了实现链式调用,返回类型写成char*,那么这个函数的返回值就可以作为其他函数的参数。

char* my_strcpy(char* dest, const char* src)
{
	char* ret = dest;//一开始保存目标地址,方便最后返回
	assert(src != NULL);
	assert(dest != NULL);

	while (*dest++ = *src++)
	{
		;
	}
	//return dest;//err,原因strcpy返回的是目标空间的起始地址,但是目标地址在循环过程中一直在++
	return ret;
}
int main()
{
	char arr1[20] = "XXXXXXXXXXXX";
	char arr2[] = "hello bit";

	//模拟实现strcpy
	my_strcpy(arr1, arr2);

	//因为my_strcpy返回的是目标地址,所以可以直接打印
	//printf("%s\n", my_strcpy(arr1, arr2));
	printf("%s\n", arr1);//类似上方,arr1也是字符串的起始地址
	return 0;
}

2. const的作用

1. const修饰变量

如果const修饰一个整形变量,那么无法重新赋值修改它。但是获取该变量的地址,通过指针可以修改该变量。

int main()
{
	const int num = 10;
	num = 20;//不能改
	int* p = &num;//可以改
	*p = 20;//可以修改

	return 0;
}

2. const修饰指针变量
int main()
{
	//为了防止修改,在指针变量前加const
	//const修饰指针变量
	//1. const放在*的左边(下方两种写法都一样)
	//const int* p;//一般使用这种,const修饰的是*p
	//int const* p;
	//意思是:p指向的对象不能通过p来改变,但是p变量本身的值可以改变(也就是指针p里面存的地址可以改变,即可以让它指向另外一个对象)
    const int num = 10;
	const int* p = &num;
	int n = 100;

	*p = 20;//err
	p = &n;//ok
	


	//2. const放在*的右边
	//意思是:p指向的对象可以通过p来改变,但是不能修改p变量本身的值(即指针变量p里面存放的地址不能改变,即不能让它指向其他对象)
	const int num = 10;
    int* const p = &num;
	*p = 0;//ok
	int n = 100;
	p = &n;//err


	//3. 如果*两边都加const,那么p指向的对象不能修改,p本身也不能修改
	
	printf("%d\n", num);
	return 0;
}

第五节:编程常见错误

1.  编译型错误

直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。

2. 链接型错误

看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。

3. 运行时错误

借助调试,逐步定位问题。

作业

 1. 调整奇数偶数顺序

调整数组使奇数全部都位于偶数前面。
输入一个整数数组,实现一个函数,来调整该数组中数字的顺序使得数组中所有的奇数位于数组的前半部分,所有偶数位于数组的后半部分。

思路:设置left和right指针。left从左开始找偶数(即遇到奇数就跳过);right从又开始找奇数(即遇到偶数就跳过)。将left找到的偶数和right找到的奇数交换。

void move_odd_even(int arr[], int sz)
{
	int left = 0;
	int right = sz - 1;
	//left找偶数,right找奇数

	while (left < right)
	{
		//从左往右找一个偶数,停下来。(遇到奇数就++)
		//等效于left%2,因为模2等于1就为真,真就进入
		while ((left < right) && arr[left] % 2 == 1)//假设全是奇数,left找不到偶数,一直++,那么left可能越界。所以要限制范围
		{
			left++;
		}
		//从右往左找一个奇数,停下来(遇到偶数就--)
		while ((left < right) && arr[right] % 2 == 0)
		{
			right--;
		}
		//交换奇数和偶数
		if (left < right)
		{
			int tmp = arr[left];
			arr[left] = arr[right];
			arr[right] = tmp;
			//交换完以后,让指针继续移动,回到上面判断下一个元素是奇数还是偶数。
			left++;
			right--;
		}
	}
}
int main()
{
	int arr[10] = { 0 };
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//输入
	for (i = 0; i < sz; i++)
	{
		//scanf("%d", &arr[i]);//两种方式都可以
		scanf("%d", arr + i);//arr是数组名,加i就是下标为i的元素的地址
	}

	//调整
	//方法一思路:第一次遍历找奇数,第二次遍历找偶数,第三次遍历拷贝进原数组。

	//方法二思路:
	//设置两个指针,left指针从最左开始找,right指针从最右边开始找。
	//当left指针找到偶数,并且right指针找到奇数时交换,
	//left要小于right指针,否则当right移动到left左边时,可能把位置正确的奇数和偶数交换
	move_odd_even(arr, sz);

	//输出
	for (i = 0; i < sz; i++)//调整完之后还是打印这个数组
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

2. 输入两个升序排列的序列,将两个序列合并为一个有序序列并输出。

输入描述:
输入包含三行,
第一行包含两个正整数n, m,用空格分隔。n表示第二行第一个升序序列中数字的个数,m表示第三行第二个升序序列中数字的个数。
第二行包含n个整数,用空格分隔。
第三行包含m个整数,用空格分隔。
输出描述:输出为一行,输出长度为n + m的升序序列,即长度为n的升序序列和长度为m的升序序列中的元素重新进行升序序列排列合并。
 

int main() {
	int n = 0;
	int m = 0;
	scanf("%d %d", &n, &m);
	int arr1[n];
	int arr2[m];
	int i = 0;
	for (i = 0; i < n; i++) {
		scanf("%d", &arr1[i]);
	}
	for (i = 0; i < m; i++) {
		scanf("%d", &arr2[i]);
	}

	// 思路:
    // 假设第一个数组的下标为j,第二个数组的下标为k。
    // 用arr1[j]和arr2[k]比较,谁小打印谁,打印以后该数组下标+1,依次类推


	int j = 0;//arr1
	int k = 0;//arr2

	while (j < n && k < m) {
		if (arr1[j] < arr2[k]) {
			printf("%d ", arr1[j]);
			j++;
		}
		else {
			printf("%d ", arr2[k]);
			k++;
		}
	}
	// 上方循环不满足,意味着有一个数组已经遍历完了
	// 所以需要判断还有哪个数组有剩余元素,然后打印
	if (j < n) //这里说明arr1数组还有没遍历完
	{
		for (; j < n; j++) {
			printf("%d ", arr1[j]);
		}
	}
	else {
		for (; k < m; k++) {
			printf("%d ", arr2[k]);
		}
	}
	return 0;
}

//合并版本
int main() {
    int n = 0;
    int m = 0;
    scanf("%d %d", &n, &m);
    int arr1[n];
    int arr2[m];
    int arr3[m + n];
    int i = 0;
    for (i = 0; i < n; i++) {
        scanf("%d", &arr1[i]);
    }
    for (i = 0; i < m; i++) {
        scanf("%d", &arr2[i]);
    }

    // 思路:
    // 假设第一个数组的下标为j,第二个数组的下标为k。创建第三个数组放置前两个数组的元素
    // 用arr1[j]和arr2[k]比较,谁小先放进第三个数组,之后该数组下标+1,依次类推

    int j = 0;//arr1
    int k = 0;//arr2
    int r = 0;//arr3
    while (j < n && k < m) {
        if (arr1[j] < arr2[k]) {
            arr3[r++] = arr1[j];
            j++;
        }
        else {
            arr3[r++] = arr2[k];
            k++;
        }
    }
    // 上方循环不满足,意味着有一个数组已经遍历完了
    // 所以需要判断还有哪个数组有剩余元素,然后打印
    if (j < n) { //这里说明arr1数组还有没遍历完
        for (; j < n; j++) {
            arr3[r++] = arr1[j];
        }
    }
    else {
        for (; k < m; k++) {
            arr3[r++] = arr2[k];
        }
    }
    // 打印
    for (i = 0; i < m + n; i++) {
        printf("%d ", arr3[i]);
    }
    return 0;
}

3. 把一个数的每一位的奇数变成1,偶数变成0

示例1:        输入:222222        输出:0
示例2:        输入:123              输出:101

#include <math.h>
//思路:
//通过%10 得到最后一位
//通过/10 去掉最后一位
//再用每一位%2,确定结果是0或1
//每一位的0或1需要权重,假设结果是101,那么最高位的1就是1*10^2,依次类推,把所有结果加起来
int main() {
    int input = 0;
    scanf("%d", &input);

    int sum = 0;
    int i = 0;
    while (input) {

        int bit = input % 10; //获取最后一位

        // if (bit % 2 == 1) {
        //     sum += 1 * pow(10, i);
        //     i++;
        // } else {
        //     sum += 0 * pow(10, i);
        //     i++;
        // }

        // 简化
        if (bit % 2 == 1) {
            bit = 1;
        }
        else {
            bit = 0;
        }
        sum += bit * pow(10, i++);

        input /= 10; //剥离最后一位
    }
    printf("%d\n", sum);
    return 0;
}

4. 打印直角三角形

KiKi学习了循环,BoBo老师给他出了一系列打印图案的练习,该任务是打印用“* ”组成的带空格直角三角形图案。
输入描述:多组输入,一个整数(2~20),表示直角三角形直角边的长度,即“ * ”的数量,也表示输出行数。
输出描述:针对每行输入,输出用“ * ”组成的对应长度的直角三角形,每个“ * ”后面有一个空格。
输入:5
输出:
        * 
      * * 
    * * * 
  * * * * 
* * * * * 

//思路:
// 0 1 2 3 4
//0        *
//1      * *
//2    * * *
//3  * * * *
//4* * * * *
//行和列加起来小于4打印空格,大于等于4打印*空格
int main() {
    int n = 0;
    while (scanf("%d", &n) == 1) {
        int i = 0;
        int j = 0;

        for (i = 0; i < n; i++) {
            for (j = 0; j < n; j++) {
                if (i + j < n - 1) {
                    printf("  ");
                }
                else {
                    printf("* ");
                }
            }
            printf("\n");
        }
    }
    return 0;
}

5. 计算打折后的价格

KiKi非常喜欢网购,在一家店铺他看中了一件衣服,他了解到,如果今天是“双11”(11月11日)则这件衣服打7折,“双12” (12月12日)则这件衣服打8折,如果有优惠券可以额外减50元(优惠券只能在双11或双12使用),求KiKi最终所花的钱数。
输入描述:一行,四个数字,第一个数表示小明看中的衣服价格,第二和第三个整数分别表示当天的月份、当天的日期、第四个整数表示是否有优惠券(有优惠券用1表示,无优惠券用0表示)。
注:输入日期保证只有“双11”和“双12”。
输出描述:一行,小明实际花的钱数(保留两位小数)。(提示:不要指望商家倒找你钱)

int main() {
	double price = 0.0;
	int month = 0;
	int day = 0;
	int discount = 0;

	scanf("%lf %d %d %d", &price, &month, &day, &discount);
	//计算
	if (month == 11)
	{
		price = price * 0.7 - discount * 50;
	}
	else if (month == 12)
	{
		price = price * 0.8 - discount * 50;
	}
	
	//输出
	if (price < 0)
	{
		printf("%.2lf", 0);
	}
	else 
	{
		printf("%.2lf", price);
	}

	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值