浅谈代码规范&&基础调试&&几道面试题

 

废话篇:本文由CSUST的FINAL实验室的LX创作,用途是给予CSUST的小鲜肉们一些关于C语言代码规范的一些基本知识,若本文有什么错误或是表述不清之处,欢迎留言讨论指正。


代码规范:

在讲代码规范之前,我想给大家看一句感人肺腑的注释名言来告诫各位以后的优秀程序猿:

//When I wrote this, only God and I understood what I was doing.
//Now, God only knows.

在写第一行代码前,我希望大家思考一个问题,在第一行我们应该写什么?我相信大部分的初学者都会写:

#include 
///来一个有奖问答题
///这个头文件stdio的意义是什么?(提示:可以分成三个单词的简写)

有问题吗?答案是没有的。这样写的确是没有任何问题,但是。。。不够骚气。开个玩笑,不过我还是希望有人会喜欢在开始会写上注释,来表明这些代码的大概内容,作者,版权所有翻版必究(雾)

在C语言中基本上有两种注释方式如下

//单行注释
//每次换行都要在最前面加上//

/*多行注释
巴拉巴拉
以这个结尾->*/

可以看到一种是单行注释,即每行注释前都要在最前方加上//,换种说法就是如果在最前方看到了//,无论这行的内容有什么,都属于注释范畴不会被编译。

第二种是多行注释,又称作块注释,这样就可以愉快的在/*与*/间加入你想要写的任意注释,也就是在这块区域里的内容都回在编译时略过。

然而我想说,上面这种多行注释,事实上并不够骚(雾),事实上根据代码规范,应该像下面这样写:

/* 以"/* "开头
 * 中间的注释以" * "开头
 *///以" */"结尾(之所以这一段是 "*///"开头的原因是因为"*/"之后的就不算作注释内容了)

但是要注意,多行注释不能嵌套,例如下面这样:

/* 
    巴拉巴拉
    /*
        巴拉巴拉
    */
    巴拉巴拉
*/

很明显,编译器把第二个/*当成了注释来对待,当遇到了第一个*/时,便认为第一个*/对应的是第一个/*,而中间的/*是注释。这样第二个*/找不到一个匹配的/*,所以存在问题。

学了这些这时候我们可以试着写出一个比较像样子的代码注释了:

/* 
 * 标题:C代码规范
 * 作者:CSUST LX
 * 巴拉巴拉
 * 日期:2018/8/10
 * 本文意义:巴拉巴拉
 */

这样你就可以愉快的和你那没看过这种酷酷的注释的室友装逼了,是不是很棒呀?

当然注释的意义不是为了装X,在实际工程中,一个大型项目可能会由很多人接手,或者随着软件的更新换代,代码会交给其他的同事去使用、学习或者是改进,如果你写的乱七八糟又没有注释,或者这地方逻辑很绕却没有注释等,会让你的合作者感到十分困惑且痛苦。除此之外,对于自己而言,虽然说是自己写的代码,但是过了很久之后难免会忘记,因此写一个注释来概括代码的作用、解释某个函数的参数作用以及函数的意义等是十分有意义的。后还是会有很大的好处的。

举个例子,大家可以看一下下面这段代码:


int Fuc( char* a, char* b )

{

    int ret = 0 ;

    while( ( !(ret = *a - *b) && *b))

        ++a, ++b;

    if ( ret < 0 )

        ret = -1 ;

    else if ( ret > 0 )

        ret = 1 ;

    return( ret );

}

给大家看十秒钟,我相信,大家还是一头雾水吧。不过,我也是看的一头雾水呢!

不过如果我给你看的是下面这段代码呢?

///判断两个字符串是否相同

int Fuc( char* a, char* b )

{

    int ret = 0 ;

    while( ( !(ret = *a - *b) && *b))

        ++a, ++b;

    if ( ret < 0 )

        ret = -1 ;

    else if ( ret > 0 )

        ret = 1 ;

    return( ret );

}

是不是感觉瞬间知道了这个函数是干什么用的了呢?

但是,如果你没学过这个函数的话,你又怎么知道这个函数的返回值和它的参数具体是怎样的呢?

如果是像下面一样的话,是不是很快就能明白这个函数的具体用法了呢?

/// 本函数用于比较字符串a与字符串b是否相同
/// 参数a与参数b:要比较的字符串的首地址
/// 返回值:当ab时,返回正数。

另外,如果这段函数的名字我改动一下的话,是否也能够很快知道这个函数大概是干什么用的呢?

int strcmp( char* str1, char* str2 )

{

    int ret = 0 ;

    while( ( !(ret = *str1 - *str2) && *str2))

        ++str1, ++str2;

    if ( ret < 0 )

        ret = -1 ;

    else if ( ret > 0 )

        ret = 1 ;

    return( ret );

}

你们应该发现了吧!这个其实是头文件里的一个函数strcmp,大家应该多多少少接触到过,这个函数名字是string(字符串)和compare(比较)的缩写结合,有这样一个函数名的话,是不是感觉看起来就很容易理解呢?

下面给大家几个函数名大家来猜猜是干什么的:

int StrToInt(char strData[]);

Girlfriend CreatAGirlfriend();

int find_max_value(int data[]);

从上面的命名里,我们看到了三种常见的命名规范:

1. 驼峰命名法:驼峰命名法就是当变量名或函数名是由一个或多个单词连结在一起时,第一个单词以小写字母开始;从第二个单词开始以后的每个单词的首字母都采用大写字母。(例如:myName,hisBoyfriend)

2. 帕斯卡命名法:帕斯卡命名法就是当变量名或函数名是由一个或多个单词连结在一起时,每个单词的首字母都采用大写字母。(例如:MyName,HisBoyfriend)

3.下划线命名法:所有字母均小写,每个单词间以下划线分割。(例如:my_name,his_name)

在使用上,这样的命名规范都十分清晰明了。具体而言,我们一般给函数名、全局变量和结构体名等通用性强的采用帕斯卡命名法,局部变量、函数形参采用驼峰命名法或下划线命名法。

总之,取这些名字的意义是:让自己的代码更便于理解阅读。

所以请以后写代码的时候,尽量给自己的变量和函数等起一个合理的名字,让你的代码易于理解。然后如果代码有逻辑不好理解或者使用方法与参数需要说明的话,请记得加上注释!

下面我们开始讲点代码规范。

#include  //代码规范:在include与<间留一个空格
/* 
 * stdio的意思 std是standard的缩写,i是input的缩写,o是output的缩写
 * h是head的缩写
 * 所以stdio是标准输入输出头文件的意思
 */
int main()
{
    return 0; //在返回值为int的情况下记得加return 0;养成好习惯(有些题目要求了main函数返回值必须为0,否则算错
}

下面来一段写的看不下去的代码,大家来找找茬。

#include
 int main()
{double a,b;
scanf("%lf%lf",&a,&b);
if(a>b)
printf("%lf",a);
else printf("%lf",b);
return 0;}

这段代码真的是让人无法忍受的,这样的代码层次特别不清晰。如果你问一个学长学姐的时候,你的代码是这样的话,他/她可能鸟都不想鸟你。

正常点的话,代码应该是下面这样的:

#include          ///空格最好加上
int main()                 ///nt前面不要加空格
{                          ///每出现一个'{'后代码要向后缩进直到遇到'}',语句不要直接放在'{'后面
    double a,b;
    scanf("%lf%lf",&a,&b);
    if(a>b)
    {
        printf("%lf",a);
    }
    else                   ///像if,else这类后面要接语句的关键词,后面最好加复合语句,以方便以后修改
    {
        printf("%lf",b);
    }
    return 0;
}

但是如果你想只想大体上可以让别人看的顺眼的话,下面用Codeblocks编译器举个栗子:

浅谈代码规范&&基础调试&&几道面试题_第1张图片

这就是刚刚那段难以置信的代码。这时候请你赶紧对着文本框里按右键别瞎了眼睛。

这时候你会发现一个让你难以置信的无敌法宝——Format use AStyle!!!

浅谈代码规范&&基础调试&&几道面试题_第2张图片

他可以一下就让你的代码变这样:

浅谈代码规范&&基础调试&&几道面试题_第3张图片

这样一来,就很方便的解决了大部分的代码规范问题,这时候你才有脸去找学长学姐问问题。

但是千万不要滥用这个,代码规范要自己写的时候注意好,不要写了一堆乱七八糟的代码后再去点那个,为什么要辣自己的眼睛呢?眼睛做错了什么?

但是如果,你要是用的不是Codeblocks呢?

这样你是不是要等着我像告诉一个小宝宝一样告诉你呢?

当然是选择!!!!

浅谈代码规范&&基础调试&&几道面试题_第4张图片

浅谈代码规范&&基础调试&&几道面试题_第5张图片

这两个可爱的网站难道你们不会使用吗?你们难道不是经历了九年义务教育的同学么,有问题么?当然选择去查啦!!!


调试篇:

调试是一个程序猿的基础能力,学了这个之后,希望各位发现问题了之后别第一时间想到去问别人,想想办法自己去DEBUG吧~

后面的代码有包含函数的内容,如果有小同学还没有学到这里的话我就稍微提一下:

例如下面的代码:

#include
int main()
{
    printf("倒计时:\n");
    printf("3\n");
    printf("2\n");
    printf("1\n");
    printf("...\n");
    printf("何畅大佬女装登场!\n");
    printf("倒计时:\n");
    printf("3\n");
    printf("2\n");
    printf("1\n");
    printf("...\n");
    printf("何畅大佬和你们挥手说再见!\n");
    printf("倒计时:\n");
    printf("3\n");
    printf("2\n");
    printf("1\n");
    printf("...\n");
    printf("再见!何畅女装大佬!\n");
    return 0;
}

我们发现,这段代码中有很多重复的代码。总结下来的话,主要是倒计时那一段:

    printf("倒计时:\n");
    printf("3\n");
    printf("2\n");
    printf("1\n");
    printf("...\n");

我们为了简化代码,增加代码的复用性,采用了一个叫做CountDown(倒着数)的函数。这个CountDown就是我们给这个函数自定义起的函数名。

#include
void CountDown()
{
    printf("倒计时:\n");
    printf("3\n");
    printf("2\n");
    printf("1\n");
    printf("...\n");
}

int main()
{
    CountDown();
    printf("何畅大佬女装登场!\n");
    CountDown();
    printf("何畅大佬和你们挥手说再见!\n");
    CountDown();
    printf("再见!何畅女装大佬!\n");
    return 0;
}

接着考虑下,如果我们现在要从4开始倒数,我们要怎么办?再写一个函数?还是说我们可以找到一个办法可以更好的解决这个问题呢?

答案是:用函数形参和循环。

void CountDown(int n)
{
    printf("倒计时:\n");
    for(int i=n;i>=1;i--)
    {
        printf("%d\n",i);
    }
    printf("...\n");
}

这里我们可以看到括号里那个n,其实就是我们所称的形参,形参的话是没有实际值的,只有在调用这个函数的时候你给它指定值,那个值不管是变量还是常量都称之为实参。

然后让我们调用这个函数:

#include
void CountDown(int n)
{
    printf("倒计时:\n");
    for(int i=n;i>=1;i--)
    {
        printf("%d\n",i);
    }
    printf("...\n");
}

int main()
{
    int a;
    scanf("%d",&a);
    CountDown(a);
    printf("何畅大佬女装登场!\n");
    CountDown(a);
    printf("何畅大佬和你们挥手说再见!\n");
    CountDown(a);
    printf("再见!何畅女装大佬!\n");
    return 0;
}

我们发现,这个程序里,CountDown函数的形参是n,而调用这个函数的时候实参是a,但是仍然可以成功运行,说明函数的形参和传进来的实参并没有关系。

为了说明这点,我先来说一下变量的作用域:

//作用域 :从定义的地方,开始,到他所在的大括号截止。
//同一个作用域 内不允许重名。
//小作用域内,出现在了与大作用域内重名的时候,屏蔽。
 
//局部变量,大括号以内的变量叫作,局部变量。包含了形参列表中的形参。
//局部变量未初始化状态的值是随机的,
 
//全局变量,大括号以外的变量叫作,全局变量.
//全局变量的作用域:从定义的地方开始,到本文件结束,而非main结束.
//全局变量的值是,若未初始化,系统默认初始化为零

--------------------- 
作者:寂寂寂寂寂蝶丶 
来源:CSDN 
原文:https://blog.csdn.net/SwordArcher/article/details/78514996 
版权声明:本文为博主原创文章,转载请附上博文链接!

上文的说明参考了上面的博主写的东西,感兴趣的小同学可以去看看。

#include 

int tmp;

int main()
{
    int a,i;
    printf("tmp=%d\n",tmp);
    printf("未赋值前a=%d\n",a);
    a=5;
    printf("赋值后a=%d\n",a);
    for(i=0;i<2;i++)
    {
        int a=i;
        printf("%d\n",a);
    }
    printf("%d\n",a);
    return 0;
}

我们把代码拖去运行会发现。。。

运行不了!

因为a没有进行初始化啊!!!

所以把相关代码去掉后接着来看:

#include 

int tmp;

int main()
{
    int a,i;
    printf("tmp=%d\n",tmp);
    a=5;
    printf("赋值后a=%d\n",a);
    for(i=0;i<2;i++)
    {
        int a=i;
        printf("%d\n",a);
    }
    printf("%d\n",a);
    return 0;
}

 我们发现,a在被赋值了之后,在循环中,被赋值成了0和1,退出循环后又变为了之前的5。这个现象说明了什么呢?

就是循环中的那个a和之前所定义的那个a所处的内存位置不一样。

为了验证我们的想法,我们用下面这段代码:

#include 

int tmp;

int main()
{
    int a,i;
    printf("tmp=%d\n",tmp);
    a=5;
    printf("main函数里的a的地址为:%d\n",&a);
    for(i=0;i<2;i++)
    {
        int a=i;
        printf("for循环里的a的地址为%d\n",&a);
    }
    printf("%d\n",a);
    return 0;
}

你会很神奇的发现,他们的地址还真的不一样。

这说明了一点:当一个嵌套结构中出现两个不同作用域的变量时,变量的名称可以相同,在使用时以其小作用域为准。

所以这些相同变量名的不同变量的来源就是作用域不相同这个原因。

于是我们看下面这个栗子:

#include 

void Change(int a ,int b)
{
    int c=a;
    a=b;
    b=c;
}

int main()
{
    int a=3,b=5;
    Change(a,b);
    printf("%d %d",a,b);
    return 0;
}

我们会发现,a和b的值,调用了Change函数之后并没有发生改变,聪明的你一定想到了是定义域不同的原因,所以函数里的a不是main函数里的a。 

但是,如果我执意要交换a和b的值呢?

那当然是选择放弃啊。。。

当然不是,这时候就有一个叫指针的好东西来了。

指针是什么呢?它事实上是一个数字,代表着一个地址。

指针是怎么玩的呢?首先我们在学习用scanf函数的时候,我们学到了一个叫做&取地址符的符号,用这个单目运算符可以得到变量的地址,于是我们来试一下。

#include 
int main()
{
    int a=3;
    int* p_a=&a;
    printf("%d %d\n",&a,p_a);
    printf("%d %d %d %d\n",sizeof(int),sizeof(int*),sizeof(char),sizeof(char*));
    return 0;
}

 运行结果大概是这样的,不过p_a的值可能你会和我的不一样。

我们惊奇的发现,p_a里面存储的就是&a的数值,而且对于char和int,他们所占的内存空间大小不同,但是他们的指针的大小却是一模一样,这是因为在相同的环境(编译器和系统都一样)下,不同类型的指针的大小都是一样的。

这个地址呢,就好像是一个房间号。当我们使用普通传参调用函数的时候,电脑会把实参的值赋值给形参,相当于就是何畅学长在213号房间坐着,他男朋友去找他,但是他男朋友的做法是:在旁边的214号房把213号房间复制了一份,里面的花花瓶瓶都是一模一样的,然后他就坐在里面。这样很明显是错误的,因为他们两个不在一个房间里,当然就不能很愉快的玩耍了。

然鹅,如果我们使用指针呢?我们想起来了,原来指针是表示地址的,那么,现在何畅学长在213号房的话,他男朋友就可以开心的直接去找他了不是?

于是我们用指针来改写上面的代码:

#include 

void Change(int* a ,int* b)
{
    int c=(*a);
    (*a)=(*b);
    (*b)=c;
}

int main()
{
    int a=3,b=5;
    Change(&a,&b);
    printf("%d %d",a,b);
    return 0;
}

这样我们的运行结果就是下面这样了:

我在网上居然找到一个关于codeblocks的断点调试的博客,这个时候!

当然是选择偷懒啦,你们自己点进去看看吧:当然是选择偷懒鸭不过里面的样例是个C++程序,你们现在可能还看不懂,那么也可以参考下我后面举的栗子,其他的操作细节的话你们可以参考上面我给你们的博客。

是不是很简单都觉得很无聊了?咱们接着讲调试栗子。

新建什么的咱们就不管了。直接上代码:

#include 
#include 

///复读str字符串n次
void Repeater(char str[],int n)
{
    int i;
    for(i=0;i

没错就是这段代码啦~

有没有想过写一个复读机呢?虽然上面的代码写的也不是一个复读机,我们就假装它是好了,毕竟里面有个函数叫做Repeater呢。

我们复制到Codeblocks里新建的工程后,大概是长这个样子:

浅谈代码规范&&基础调试&&几道面试题_第6张图片

然后我们就可以很愉快的点击行数和黄线中间的白色区域给指定的行数添加断点,或者是其他什么方法都行,这个方面有什么问题看刚刚那个博客,强调!

愉快的瞎点了几行之后,你会发现居然变这样了:

浅谈代码规范&&基础调试&&几道面试题_第7张图片

很明显,那些红点点不是什么好家伙,一看就是刚刚你点的断点,但是这个到底有什么用呢?

浅谈代码规范&&基础调试&&几道面试题_第8张图片

我们找到上面的Debug->Start/Continue,点击这个。

你会发现出现了一个黑框框:

浅谈代码规范&&基础调试&&几道面试题_第9张图片

欸?你说你没看到这个框框吗?有奖问答,这时候要怎么办?

接下来我们输入3,也就是让n=3。

等下!这里好像发生了一件不得了的事情,为什么程序没有结束运行?而且这儿一个红点点上怎么出现了一个黄色三角形?

浅谈代码规范&&基础调试&&几道面试题_第10张图片

聪明的小伙伴肯定就会想到了,哦,原来是运行到这里就停止了鸭,这就是断点的作用!

浅谈代码规范&&基础调试&&几道面试题_第11张图片

这时候我们点击Debug->Debugging windows->Watches打开变量观察框(Watches)。

浅谈代码规范&&基础调试&&几道面试题_第12张图片

这时候我们发现n=3了,而str却很奇怪。

 

浅谈代码规范&&基础调试&&几道面试题_第13张图片

我们点击下Debug->Next line

浅谈代码规范&&基础调试&&几道面试题_第14张图片

你就会发现Watch窗口变成这样了:

浅谈代码规范&&基础调试&&几道面试题_第15张图片

你发现了,str变成了“ouyanghaoxuezhangzhenniubi”。但是,i却变得很是奇怪。

浅谈代码规范&&基础调试&&几道面试题_第16张图片

来吧,让我们看一下代码~

让我们来一起想想想~

结论是:一条打了断点的语句,会在运行到这句语句之前停止。直到你手动去让程序运行。

接着运行:

浅谈代码规范&&基础调试&&几道面试题_第17张图片

我们发现我们停留在printf语句里,黑框框里没有输出,n变成了3。和你想的一样吗?

来,猜一下运行下一条语句会是怎样的。

结果是:

浅谈代码规范&&基础调试&&几道面试题_第18张图片

由于这是个循环语句,所以我们又返回到了for语句那里,发现i==0,所以i++还没有运行。

而黑框框里:

发现有输出,说明循环进行了一次了。

接下来的验证操作就交给大家吧,然后我们准备关闭调试。

浅谈代码规范&&基础调试&&几道面试题_第19张图片

点一下,你就完成了我们的Debug操作。

正所谓是:点一下,De一年,Bug不花一分钱。

什么?到现在你还不知道Debug有什么用?我们可以通过断点让程序在我们想观察的点停下来,然后我们可以通过Watch窗口里变量的值和我们心里对该程序运行时该变量的理论上的值比对,从而推断出我们的程序是哪儿出错了。

搞了半天,还是要用脑子的啊!


某面试题的讲解

首先,我是不会给你们讲解太难的那种题的,碰到了的同学,只能怪你自己了,谁叫你这么强呢?

1.判断一个正整数是否能被2整除:

关键代码:n%2==0

然鹅,这样一点也不骚啊。

让我们来想想2进制下的世界:

1 : 0001
2 : 0010
3 : 0011
4 : 0100
5 : 0101
6 : 0110
7 : 0111
...

发现了什么吗?好像只要是2的整数倍的数字,最后一位一定是0。

接下来我们介绍下位运算。相信各位接受了九年制义务教育的优秀大学生肯定对逻辑运算是有所了解的啦。相必&&符号大家都知道,这个符号是当左右两边都不为0的情况下返回1,否则返回0的一个运算符。在位运算里,有一个&符号,这个是对于数的二进制的每一位来进行运算的,举一个栗子:

01 & 01 = 01
00 & 01 = 00
10 & 00 = 00
10 & 11 = 10
11 & 11 = 11

这样我们发现,这个&运算相当于就是逻辑运算与的二进制版。

& 按位与运算    两个位都为1时,结果才为1
| 按位或运算    两个位都为0时,结果才为0
^ 按位异或运算  两个位相同为0,相异为1
~ 按位取反运算  0变1,1变0

知道了这些的话就很简单了。

我们发现对于每个可以被二整除的数字来说,最后一位都是0,所以我们只要考虑最后一位。

所以我们对一个数与1进行或运算,举例如下:

在4bit的情况下,1的二进制是0001。
若n = 4为偶数的话,它的二进制是0100。
0100 & 0001 = 0000
若n = 3为奇数的话,它的二进制是0011。
0011 & 0001 = 0001
这时候我们发现,与1做按位与运算后,奇数变成了1,偶数变成了0。

于是我们写个稍微骚点的代码:

#include 
int main()
{
    int n;
    scanf("%d",&n);
    if((n&1)==1)
    {
        printf("%d是奇数",n);
    }
    else {
        printf("%d是偶数",n);
    }
    return 0;
}

接着如果你答上来了的话,我一般会问:

2.判断一个正整数是不是2的n次方。

我们来观察一下2的n次方的二进制的规律。

2^0    0001    1
2^1    0010    2
2^2    0100    4
2^3    1000    8

我们发现了,如果这个数是2的n次方的话,这个数的二进制里肯定只有一个1。

所以我们可以通过将这个数字转换成2进制然后统计它里面有多少个1来判断这个数是不是2的n次方。

但是这样还是不够骚啊!那要怎么办?

我们让这些数减1看看。

2^0-1    0000    0
2^1-1    0001    1
2^2-1    0011    3
2^3-1    0111    7

我们惊奇的发现,除了原来的1变成了0以外,1后面的所有0都变成了1。

浅谈代码规范&&基础调试&&几道面试题_第20张图片

我们来进行一下&运算看看:

0001 & 0000 = 0000
0010 & 0001 = 0000
0100 & 0011 = 0000
1000 & 0111 = 0000

居然全都是0!

那么不是2的n次方的数字与它减一后的数进行&运算后,会怎样呢?

让我们来想一下,2^n的条件就是,二进制里只有一个1,反之,出现了1个以上1的数字就不是2^n次方。

那么,也就意味着,在最前面的1的后面,至少也存在一个1,减了1之后,它的最前面那位肯定不会变成0的,也就是说,最前面的那位1是一定不会变化的,于是进行了与运算后一定不为0。

代码如下:

#include 
int main()
{
    int n;
    scanf("%d",&n);
    if(n&(n-1)==0)
    {
        printf("%d是2的n次方\n",n);
    }
    else {
        printf("%d不是2的n次方\n",n);
    }
    return 0;
}

让我们运行一下,输入一个2,大家想想会输出什么?

怎么样?

浅谈代码规范&&基础调试&&几道面试题_第21张图片

因为按位运算符的运算优先度不高,所以首先会运算(n-1)==0,1!=0,所以返回的是0。

2&0=0,所以会去执行else里面的语句。

因此事实上应该这么写:

#include 
int main()
{
    int n;
    scanf("%d",&n);
    if((n&(n-1))==0)
    {
        printf("%d是2的n次方\n",n);
    }
    else {
        printf("%d不是2的n次方\n",n);
    }
    return 0;
}

希望大家能够从这节课里多多少少学到点什么,不说了,待会何畅大佬要提刀来见我了,请各位给点面子保护下我。

你可能感兴趣的:(浅谈代码规范&&基础调试&&几道面试题)