入门学习计算机第十四天—结构体,C语言实用调试技巧
发布日期:2021-06-28 16:38:47 浏览次数:3 分类:技术文章

本文共 6595 字,大约阅读时间需要 21 分钟。

入门学习计算机第十四天—结构体,CC语言实用调试技巧

编译器:Microsoft Visual Studio 2010

前言 记录第十三天学习C语言的博客。

结构体

  • 结构体类型的声明
  • 结构体的初始化
  • 结构体成员访问
  • 结构体传参

结构体的声明

结构的基础知识

结构是一项值的集合,这些值称为成员变量。结构的每一个成员可以是不同类型的变量

struct tag//结构体标签{
member-list;//成员列表}variable-list;//变量列表

例如:

struct stu//struct-结构体关键字 stu-结构体标签{
char name[10]; short age; char tele[15]; //定义一个结构体类型,不占空间,相当于建房子的图纸 char sex[5];}s1,s2,s3;//是三个全局的结构体变量int main(){
struct stu s; //创建结构体变量,占有实际空间,相当于房子, s是局部变量 return 0;}

当在struct stu 前面加一个typedef,可以对整个类型,重新起名字,在之后的创建结构体变量时可以简化。比如:

typedef struct stu{
char name[10]; short age; char tele[15]; char sex[5];}Stu;//类型int main(){
Stu s ; return 0;}

结构成员的类型

结构的成员可以是标量,数组,指针,甚至是其他结构体。

结构体变量的定义和初始化

typedef struct stu{
char name[10]; short age; char tele[15]; char sex[5];}Stu;typedef struct resume{
Stu s; char homeadd[20]; char *pc;}Res;int main(){
char arr[]="白云校区"; Res s ={
{
"张三",20 , "18576854961", "保密"},"广师大",arr} ;//初始化 printf("%s\n",s.s.name); printf("%s\n",s.homeadd); printf("%s\n",s.pc); return 0;}

在这里插入图片描述

结构体传参

typedef struct stu{
char name[10]; short age; char tele[15]; char sex[7];}Stu;void Print(Stu tmp){
printf("name: %s\n",tmp.name); printf("age: %d\n",tmp.age); printf("tele: %s\n",tmp.tele); printf("sex: %s\n",tmp.sex);}void Print2(Stu* ps){
printf("name: %s\n",ps->name); printf("age: %d\n",ps->age); printf("tele: %s\n",ps->tele); printf("sex: %s\n",ps->sex); }int main{
Stu s = {
"张三",20 , "18576854961", "保密"}; Print(s); Print2(&s); return 0;}

在这里插入图片描述

Print与Print2函数对比:

Print函数在传参s时,需要再开辟一块相同的空间,拷贝s的值,空间浪费特别严重,拷贝也需要一定的时间。
而Print2,传的是地址,函数只需创建一个指针变量接收地址,一个指针变量只是4/8字节,不会开辟新的空间,也不要时间。
函数在传参的时候,参数是需要压栈的。如果传递是一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降。


C语言实用调试技巧

调试的基本步骤:

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

Debug和Release的介绍

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

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

在主页面输入以下函数,确认环境为Debug,运行代码:

在这里插入图片描述
运行代码后会产生一个Debug的可执行程序:

在这里插入图片描述

双击第一个文件,窗口会一闪而过。需要对代码进行修改。

int main(){
int i = 0; for(i=0; i<100; i++) {
printf("%d ",i); } system("puase"); return 0;}

再次双击

在这里插入图片描述

修改环境为Release,允许代码后,文件夹会产生Release可应用文件。

在这里插入图片描述
两者文件最直接的区别就是两个文件的大小不一样,Debug的版本稍大,因为里面有调试信息。

两者在功能上也有区别:

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; } system("pause"); return 0;}

在Debug环境下是死循环:

在这里插入图片描述
在Release环境下,输出的了13个hehe
在这里插入图片描述

快捷键

常用快捷键 功能
F5 启动调试,经常用来直接调到下一个执行逻辑断点处,通常与F9配合使用
F9 创建断点和取消断点
F10 逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句
F11 逐语句,就是每次都执行一条语句,但是这个快捷键可以进行函数内部(最常用)
ctrl+F5 开始执行不测试,如果想让程序直接运行起来而不调试可以直接使用

F5,F9试用

F9设置断点,按下F5

代码如下:
在这里插入图片描述

在这里插入图片描述

F5可以快速跳到断点,并执行断点前的代码。再按下F5,会跳到下一个执行逻辑断点,而不是物理上的断点。
在这里插入图片描述

F10试用:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
按下多次F10,调试箭头并没有进入函数,而是直接到下一个语句,说明F10关注是结果。

F11:当按下F11时,箭头会进入函数内部

在这里插入图片描述

在这里插入图片描述

调试的时候查看程序当前信息

查看临时变量的值

1、自动窗口

在调试的状态下,调试——窗口——自动窗口
在这里插入图片描述
当程序调试过程中,自动窗口会自动将运行到的代码处的上下代码变量添加到窗口,不需手动添加,但缺点是,代码运行到其他地方时,某个变量会自动消失在窗口,所以无法一直观察某个变量的变化。

2、局部变量

在调试的状态下,调试——窗口——局部变量
在这里插入图片描述
在这里插入图片描述
一旦出了局部变量的范围,局部变量窗口就自动将变量移出。无法手动添加。

3,监视窗口

在调试的状态下,调试——窗口——监视
在这里插入图片描述需要手动添加观察的值,可以一直观察。

4,内存

在调试的状态下,调试——窗口——内存
在这里插入图片描述
也可以手动添加。

调用堆栈

当有这么一个代码:

void test2(){
printf("hehe\n");}void test1(){
test2();}void test(){
test1();}int main(){
test(); return 0;}

调出调用堆栈窗口

在这里插入图片描述
在这里插入图片描述
函数每次被调用的时候,都是在顶上加入信息。当返回上一级函数时,顶上的信息又是先被移出的。

调试实例:

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;}

输出的结果是死循环的hehe

为什么呢?
对代码进行调试:
F10进入调试状态。
在这里插入图片描述

当i = 10时,此时已经超出数组范围,非法访问:

在这里插入图片描述
当i=12时:
发现arr[12]与i的值相等,并且也同时为0了
在这里插入图片描述
在这里插入图片描述
发现i的地址与arr[12]的地址相等,所以i的值为0,重新进入循环,造成了死循环。
在这里插入图片描述

为什么i的地址和arr[12]的地址相等呢?

在这里插入图片描述

arr[i]非法访问其他空间时,遇到了i的地址,所以当两者相遇时,两者的值也就相等了。

在不同编译器下,i的地址的布局是不相同的。
vs2010 是<=12
vs6.0是<=10
gcc编译器是<=11
如果在vs2010下,将值改为i<=11,运行结果会报错。

在Release环境下,代码会实现优化

在这里插入图片描述

任何写出好(易于调试)的代码

优秀的代码:

1、代码运行正常

2、bug很少
3、效率高
4、可读性高
5、可维护性高
6、注释清晰
7、文档齐全

常见的coding技巧:

1、使用assert

2、尽量使用const
3、养好良好的编码风格
4、添加必要的注释
5、避免编码的陷阱

示范:

模拟实现库函数:strcpy

初步写:

viod my_strcpy(char* dest , char* src){
while(*src != '\0') {
*dest = *src; src++; dest++; } *dest = *src; //打印\0 }int main(){
//strcpy 字符串拷贝 char arr1[] = "#################"; char arr2[] = "bit"; my_strcpy(arr1,arr2); printf("%s\n",arr1); return 0;}

这种代码,面试官看到了只会给6\10分,还可以继续优化

第一次优化:

void my_strcpy(char* dest , char* src){
while( *dest++ = *src++)//简化了++,而且也考虑到了\0 {
; }}int main(){
char arr1[] = "###"; char arr2[] = "bit"; my_strcpy(arr1,arr2); printf("%s\n",arr1); return 0;}

这种代码,面试官看到了只会给7\10分,还可以继续优化

万一别人在使用代码的时候,将参数传错成了NULL

void my_strcpy(char* dest , char* src){
while( *dest++ = *src++) {
; }}int main(){
char arr1[] = "###"; char arr2[] = "bit"; my_strcpy(arr1,NULL);//在这个地方传成了空指针 printf("%s\n",arr1); return 0;}

在这里插入图片描述

程序什么也不会输出,说明在传参数的时候是有问题的。
想在运行代码时,可以直接把问题呈现出来,方便对参数进行修改。
所以需要在拷贝字符串之前,进行判断

所以需要用到assert( exp)函数——断言函数,表达式如果为真,代码会继续运行,如果为假,会直接报错。

在使用断言函数时,需要引头文件<assert.h>
第三次优化:

void my_strcpy(char* dest , char* src){
assert(dest != NULL); assert(src != NULL); while( *dest++ = *src++) {
; }}int main(){
char arr1[] = "###"; char arr2[] = "bit"; my_strcpy(arr1,NULL);//错误传参 printf("%s\n",arr1); return 0;}

当有空指针传参到函数时,输出的结果如下:

在这里插入图片描述可以很明显的看到 src 的值为空指针,方便对参数修改。
面试官会说:“小伙子不错,给你8分”

还有地方可以继续优化

结果发现还是传参的问题,如果有人在使用代码的时候,将目标地址和原地址位置弄反了。

//   将while( *dest++ = *src++)错误写成了while( *src++ = *dest++)

在这里插入图片描述

代码可以顺利运行,但就无法正确打印出来想要的效果,而且也无法直接将错误地方体现出来,可以需要调试进行排除错误,耗时耗力。所以有优化的地方。

第四次优化:

void my_strcpy(char* dest ,const char* src)//在这个地方写const,将* src规定为不可变的量{
assert(dest != NULL); assert(src != NULL); while( *src++ = *dest++) {
; }}int main(){
char arr1[] = "###"; char arr2[] = "bit"; my_strcpy(arr1,NULL);//错误传参 printf("%s\n",arr1); return 0;}

如果写错了,代码就无法直接运行了,问题也是一目了然

现在是9分了!!!!!!
在这里插入图片描述
对const 进行补充
const 修饰指针时有两个位置:

第一种:const char* p = &a,const 放在 * 左边时,修饰的是p, 也就说不能通过p来改变p(a)的值。

*第二种:char * const p = &a: , const放在 * 的右边,修饰是p本身,p不能被改变了

还可以再优化一次,在使用函数的时候,想直接能够看出这个函数是对字符串进行的操作。

将目的地的地址返回:

char* my_strcpy(char* dest ,const char* src){
char* ret = dest;//将原来首元素的地址保存起来 assert(dest != NULL); assert(src != NULL); while( *dest++ = *src++)//把src指向的字符串拷贝到dest指向的空间,包含'\0' {
; } return ret;//返回目的地首元素的地址}int main(){
char arr1[] = "###"; char arr2[] = "bit"; printf("%s\n",my_strcpy(arr1,arr2);); return 0;}

加上一些注释,这就是满分答案。

编程常见的错误

编译型错误(语法错误):

直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。
比如:少个{ },或者;
链接型错误:
看错误提示信息,主要是代码中找到了错误信息中的标识符,然后定位问题所在。
比如:未应用头文件,或者函数名写错
运行时错误:
借助调试,逐步定位问题,最困难。


计算机知识/代码知识(零碎)

设置断点后,右键断点,设置条件,可以快速停在符合条件的断点处:

转载地址:https://blog.csdn.net/xiaotangyu7dong/article/details/115869579 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:入门学习计算机第十五天——数据的存储
下一篇:入门学习计算机第十三天—初识指针

发表评论

最新留言

能坚持,总会有不一样的收获!
[***.219.124.196]2024年04月09日 12时50分45秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章