入门学习计算机第十四天—结构体,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;
}
加上一些注释,这就是满分答案。
编程常见的错误
编译型错误(语法错误):
直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。
比如:少个{ },或者;
链接型错误:
看错误提示信息,主要是代码中找到了错误信息中的标识符,然后定位问题所在。
比如:未应用头文件,或者函数名写错
运行时错误:
借助调试,逐步定位问题,最困难。
计算机知识/代码知识(零碎)
设置断点后,右键断点,设置条件,可以快速停在符合条件的断点处: