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

入门学习计算机第十四天—结构体,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;
}

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

编程常见的错误

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


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

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值