Trifles in C

 

    1. printf输出双引号,%f的问题

    win7 64位  VC++6.0和Dev-C++ 5.11下:

    双引号:printf("He said "I am a student."\n"); 会报错,输出双引号需要加斜杠 \" ,改为 printf("He said \"I am a student.\"\n");虽然按规定双引号用\"    \",单引号用\'    \', 但是在实测中,单引号直接用 ' 没关系,并不会报错。

    %f:printf("按格式%f%f给x,y赋值:\n"); 则系统会随机给x,y赋值,参考⑥:当“上台”人数不够时,系统填充随机数填充,以下是VC++6.0实测结果:

            

    要想输出像“按格式%f%f给x,y赋值:\n”,则需要printf("按格式%%f%%f给x,y赋值:\n"); 实际上,printf中输出百分号%的格式是%%,输出斜杠的格式是\\,双引号格式是\",单引号格式是\'。

    像在文件操作中

FILE *fp=;
fp=fopen("c:\\hzk16","r");         \\第一个\表示转义字符,第二个表示根目录

如果不按格式来printf会随机输出,而不是自动类型转换:

#include<stdio.h>

void main()
{
	int x = 1;
	float y = 12.34f;
	printf("%f  %d\n",x,y);
}

result:

-0.000000  1076407828
Press any key to continue


    2. 逗号表达式:<表达式1>,<表达式2>, · · ·,<表达式n>

      y=((x=a+b),(b+c));

      依次操作表达式1,表达式2,......,表达式n,但是将表达式n作为整个逗号表达式的值

      之所以不称之为语句而是表达式,是因为后面是逗号而不是分号


    3. 赋值表达式i=666与赋值语句i=666;

     if(i=666)

printf("i=%d \n",t);

     可以编译通过并运行,等效为 

      i=666; 

      if(i) 

            printf("i=%d \n",t);


      但是if(i=666;)

printf("i=%d \n",t); 

       就会报错,以为if括号内为一个赋值语句,而不是一个赋值表达式,并没有返回值供if判断真假。

       而C/C++中赋值语句有返回值,返回值即变量赋的值,如下:

  1. int main()  
  2. {  
  3.     int i;  
  4.     printf("%d\n",(i = 666));  
  5.     printf("%d\n",i);  
  6. }  

       两行打印结果均为666,也正因为赋值语句(或者说赋值符号=)有返回值,且返回值即赋的值大小,所以连续赋值更容易理解x=y=z=1;

       赋值=是“运算符”,所以a=1是个表达式,是有“值”的,为1,只是赋值运算符比较特殊,会改变操作数的值(一般的运算符并不会改变操作数的值,只会计算表达式的值而已),因为是“运算符”,所以像a+b+c一样,a=b=c,是可以复合赋值构成复合赋值表达式,结合性为右结合。


PS:

    1)

int main()
{
   int i;
   printf("%d %d",(i=666),i);
}

    会报错的,因为VC++6.0下,对printf()输出列表的表达式是从右往左(不同的编译器对此处理方式不同)计算的,故改为如下则编译通过:

int main()
{
   int i;
   printf("%d %d",i,(i=666));
}

Exercise:

#include<stdio.h>
void main()
{
  int i=8;
  printf("%d\n%d",++i,--i);
}

输出是:

8
7

(出自WHU教材,解题时注意从右往左外,格式控制符与变量的对应顺序并没有变)


    2)在Java中,if 括号内的表达式必须为boolean类型,譬如if(666) , if(t) (t为int,值为666)都是不合法的,编译报错:Type mismatch: cannot convert from int to boolean

        所以 if (t=66) 同样会报错,报错内容一样:Type mismatch: cannot convert from int to boolean

  3)char赋值给int,赋值给低8位;int赋值给char,把int低8位赋值给char。short,int,long同样道理,而精度高的赋值为精度低的(如果可以的话,如使用强制类型转换),会被截断称之为精度损失。

    4. main函数的结束

   int main(void)代表无参函数,返回值为int型,一般默认返回值为0时,程序运行正常,若没有return值回去一般也没事,但是最好return回去值,有些编译器会warning。

   而使用void main()就代表不需要返回值。

  另, main函数结尾用return 0和exit(0)完全等价。


    5. 不允许声明同时连续赋值多个变量

     int a,b,c=5; (错误)(的确可以运行得通,但是不是我们想要的结果)

     int a=5,b=5,c=5;(正确)

     int a,b,c; a=b=c=5;(正确)

     


   6. printf和scanf的“f”是指“format”之意,意为按格式输入输出。

    printf("%d %d %d",a,b);时,剩下的输出随机数,printf("%d %d %d",a,b,c,d);时,d不输出。


    scanf("%d %d %d",&a,&b); 若只输入两个int即Enter,则会一直换行,换行,换行......因为系统还在等scanf最后一个%d的输入;若输入3个int甚至更多,则系统直接崩溃(VC++6.0实测)。

    scanf("%d %d %d",&a,&b,&c); 若只输入两个int即Enter,则同上系统一直等最后一个%d的输入;若输入3个int,正常;若输入4个及以上,则只取前3个,正常运行,不会报错。

    scanf("%d %d",&a,&b,&c); 则输入2个及以上int,均只取前两个,正常运行,不报错。


    综上,scanf时:

    1)多少个%d,一定会等到多少个int进来,不然Enter也会一直等下去;

    2)可以留空房间(&a,&b...),不住人;但是不能人没有房间住(没有足够的地址&a,&b...存储scan进来的常量),此时系统崩溃。

    printf时:

    如果“上台”人数不够,找随机数充上;如果想“上台”人数太多,则后面人不上台。不会报错。


   

    7. scanf:如何控制停止scan
    数型变量:scanf("%d %d",&a,&b,&c); 实际输入时可以作为变量之间分隔符的有三个,又称之为空白字符:Space(一个或多个),Tab,Enter(回车键)。

    字符型变量:scanf("%c%c",&a,&b);则Space,Enter,Tab都会被认为是合法的普通字符型常量输入,此时终止scan只有一个方法:一直输入到超出(>=)变量个数,使用回车键。如果个数不够回车键也会被当做普通字符scan进去。

    另外,gets()则只有一个输入结束符,回车键\n,即输入的Tab和Space都会被当做合法字符输入,放在字符串中。

ps: puts()输出字符串时,将字符串末尾的\0转化为\n输出,即puts()自带换行效果,而printf显然没有。


    8. \n,\t,\r 的区别

    \n本意为换行,即垂直下移到下一行,但Windows下其效果等效为“Enter”,即回车+换行;

    \t为下一制表符,相当于Tab键,一般作用为四个空格键;

    \r为回车键,回到本行开头,但是不换行;

经在VC++6.0实测:

    \n相当于回车换行,即键盘上Enter/Return键,作用为:\r\n(此处的\n为本意:仅换行);

    \t为4个空格;

    \r为回到本行开头,且本行之前的所有打印内容清空。


    PS:

    1)Tab键在编译器(写代码时)中,作用为四个空格键,具体等效为几个空格键可以通过“工具—>选项—>制表符”内调整,但是经过VC++6.0的实测,只有编辑器中效果改变,printf中的\t作用(代表的空格数)并没有改变。

    2)\b的作用为退格,backspace,本行往后回退一格,且作用为清除前一格内容,并不是仅仅将光标往前移动。


   9. 运算符优先级

    不同语言的运算符有所不同,所以不用拘泥于所有运算符优先级细节,防止此处挖坑,Debug等注意即可,具体使用时,记住常用的几个,不放心的用括号。将精力用在算法和要实现的功能本身,不要在此处花费太多功夫,printf的格式控制同理。

1)    !    >    算术运算符    >    关系运算符    >    &&和||    >    赋值运算符    >    逗号运算符

        eg:    value=!a+b>=c/d&&d+e;

        等效为:value=(      (((!a)+b)  >=  c/d)      &&      (d+e)         );


2)    (类型)    >    自增++自减--    >    乘*除/取余%    >     加+减-n

        eg:    average=(float)sum/n;
sizeof int 出错

        实际上只对sum进行了类型转换。


PS:

sizeof是运算符,不是函数,一个例子:

int i;
int size=sizeof i;  //不会报错
引用函数是不能省略括号的,sizeof不使用括号也不会报错,说明不是函数
ps:sizeof计算变量可以直接加变量名,但是计算类型大小,必须加括号,不然无法识别:

sizeof(int) 不出错


C中最高优先级不是括号( ),而是数组下标运算符[ ]。

逗号运算符优先级最低,然后是赋值运算符:
a=(a=3,2*a);


    10. switch要注意的点

    如下程序:

switch(grade)
{
    case 'A':printf("85-100\n");
    case 'B':printf("70-84\n");
    case 'C':printf("60-69\n");
    case 'D':printf("不及格\n");
    default:printf("输入错误!\n");
}

    若grade为'B'输出为:

70-84
60-69
不及格
输入错误!

    switch的判断机制是:从第一个case开始判断,若值符合,则进入,若不符合则退出,检查下一个case...,直至检查到符合条件的case为止。之后,若遇见break则退出switch,若没有则往下一直执行,即使case值不符合也一样进入,即执行第一个符合case值的语句之后,一直执行到遇见break为止,中间不再检查case值或者default,均视为透明,只会顺序执行。若从头至尾没有找到case值,即没有进入任何case,则跳至default语句,之后同样将case值视为透明,一直执行到break或者switch结束。

1)在第一个符合的值之前的若case值不符合则跳过,但是在执行第一个符合case值的语句之后不再检查case或者default。

2)若将default位置变换,可以更好地理解switch的机制,如下:

switch(grade)
{
    case 'A':printf("85-100\n");
    default:printf("输入错误!\n");
    case 'B':printf("70-84\n");
    case 'C':printf("60-69\n");break;
    case 'D':printf("不及格\n");	
}

    

    如果输入为'B',则输出为:

70-84
60-69

    即switch是先顺序检查case值,即使default位置放在case之前也不理会;之后一直执行到break或switch结束。

    

    如果输入为'F',则输出为:

输入错误!
70-84
60-69

     若没有找到case值,则回到default开始执行,并且往下继续顺序执行直至遇到break或者switch程序体结束。

    如果输入为'A',则输出为:

85-100
输入错误!
70-84
60-69

     故找到case值之后,只有break或者switch结束才能终止程序的顺序执行,case值和default都视为透明。


3)case后的多语句不需要用花括号括住,程序流程会自动按顺序执行case后的所有语句,一直遇到break,或者下个case,或者default。(其实考虑到上面的机制,程序进入case后,之后所有语句(至switch结束)只有break才能让程序停下)

4)switch 可以没有default分支,这样找不到case时,不执行任何操作。


PS:

    break本身为跳出循环体(三种循环体for,while,switch,if不是循环体)所用,不是专门为switch专用,这样理解起switch的机制更容易。另外,break为直接跳出本循环体,而continue为跳出本循环。如下:


break:

for(i=1;i<10;i++)
{
  if(i==5)
     continue;   //则输出1,2,3,4
  printf("%d\n",i);                
}

continue:

for(i=1;i<10;i++)
{
  if(i==5)
     continue;   //则输出1,2,3,4,6,7,8,9
  printf("%d\n",i);                
}

    continue只跳过一次循环,即执行到continue,停止执行continue下面的语句,回到循环体条件语句,进入下一循环。冒泡程序用return,即后面没有发生一次交换--后面全部有序,则直接结束。

Exercise:

用1,2,3,4可以组成多少种不同的三位数?  C语言编程统计并输出。

#include<stdio.h>

void main()
{
	int i,j,k,sum=0;
	for(i=1;i<5;i++)
	{
		for(j=1;j<5;j++)
		{
			if(j==i)
				continue;
			for(k=1;k<5;k++)
			{
				if(k==i||k==j)
					continue;
				printf("%d\n",i*100+j*10+k);
				sum++;
			}
		}
	}
	printf("一种有%d种\n",sum);
}

运行结果:

123
124
132
134
142
143
213
214
231
234
241
243
312
314
321
324
341
342
412
413
421
423
431
432
一种有24种



    11. do-while
    用法:

                 do

                 {

                   循环体

                 }

                 while(条件);//注意while后面的分号


    举例:
do
{
  t--;
  printf("HelloWorld!");
}
while(t>0);           //注意while后面有分号

    12. goto

    goto使程序无条件转移至语句标号处,由于使用灵活,将原来顺序执行的程序打乱,程序流程无规律,可读性差,不不符合结构化程序设计的原则,故不提倡使用,实际程序设计中也用的很少,但是

1)要熟悉基本用法

goto loop;      //其实挺像汇编语言中的CALL LOOP;

...

...

loop: {程序体}          //语句标号的命名规则同C变量命名规则一样

2)以下两个场合用goto挺合适

① 与if构成循环体:

loop:i++;   //loop只管一行,要多句则用 {...}
if(i<10)
  goto loop;

    功能类似于fo-while或者while(具体像哪个看goto与语句标号e.g.loop的前后位置)


②从内循环跳到循环体外

    类似于循环体内break的作用,只是是跳至指定位置。


    13. C中定义数组及初始化数组大小

    C中定义数组时,必须用正整型常量或表达式指定数组大小,不允许使用变量。如果不对数组进行初始化,则系统随机指定值。

1)一维数组

    要么指定长度,要么空着让后面初始化大小来决定

    ① 

#define MaxSize 100       //#define不是语句,后面没有分号

int x[MaxSize*2];        //合法


    ②

int x[]={8,5,4,3};  //可以不指定大小,则此时大小由赋值的个数决定

    ③

int x[4];
x={8,5,4,3};//不可以,数组的整体赋值只可以在定义的同时进行
           //这一点类似于结构体

    ④

int x[10]={8,5,4,3};//则剩下的赋值为0,结构体类似

    由此启发,可以使用一种快捷方式给数组初始化为0

int x[MaxSize]={0};
    ⑤
int x[]={0};   //由②,④可以推出,能够编译通过,定义大小为1的int数组并初始化为0


2)二维数组

    要么二维长度均指定,要么只指定列数,行数由初始化赋值个数决定

int x[3][5]={{1}.{6,7},{4}};  //每一行没有初始化系统自动赋值为0
int x[][5]={1,2,3,4,5,6,7,8,9,10};//自动初始化为两行
int x[][5]={1,2,3,4,5,6,7};//不是列数(5)的整数倍,则向上取整,不会报错
   
    14. 自定义函数的声明与定义

    1)函数定义在main()函数之前,则不需要提前声明,eg:

#include<stdio.h>

int Max(int m,int n)               // Max()函数定义在main()之前,则不需额外声明
{
  if(m>n)
    return m;
  else
    return n;
}

void main()
{
  M=Max(100,99);
}


    2)函数定义在main()函数之前,则不需要提前声明,eg:

#include<stdio.h>

int Max(int m,int n);       // Max()函数定义在main()之后,则需在main()函数前面额外声明
                            //且注意有个分号

void main()
{
  M=Max(100,99);
}

int Max(int m,int n)      //仍然有int,int,int,不是Max(m,n)
{
  if(m>n)
    return m;
  else
    return n;
}

    相对于之前在main()函数之前声明定义,纯粹地多了一条声明语句:int Max(int m,int n);



    15. 函数不能嵌套定义,即不能在函数内部再定义函数
    1)即使是main()函数内部也不能定义其他函数,main() 函数内部只能调用在main()外部定义的函数。

    2)C语言中,函数不可以嵌套定义,但是可以嵌套调用,eg:

int MaxInFour(int a,int b,int c,int d)
{
  int value1=Max(a,b);     // 在函数MaxInFour()内部调用了函数Max()
  int valule2=Max(c,d);
  return Max(value1,value2);
}

    3)当一个函数嵌套调用自身时,就构成了递归函数。注意,这并没有违反“C语言不支持函数嵌套定义的原则”,因为在递归函数内部是“调用”自身,不是“定义”自身,eg:

int Factorial(int n)
{
  if(n==1)
    return 1;
  else
    return n*Factorial(n-1);  //注意,此处并不是重新定义int Factorial(...),而是直接调用的
}

    4)结构体支持嵌套定义的(结构体不是函数),但是不支持递归定义(如下面的strut person不能是struct student)

struct student 
{
  char No[10];
  struct person 
  {
    char name[15];
    char sex[5];
    int age;
  };
};

        PS:递归函数胜在代码简练,败在效率太低,占用计算机大量时间空间,尽量少用。

        因为程序运行实际是编译—链接—执行的过程,代码少不代表编译过程简单,占用内存空间少及运行时间少,递归的幕后工作交给编译器去做了而已,实际上生成的机器码可能比自己写的非递归代码要多得多,而编辑器或者工程的源代码是20行还是150行,其实无关紧要吧。

16. 函数的参数传递

    1)函数定义时,形参并没有给定值,仅具有接收实际参数的意义。处理(应该是指编译)时,也不会立即为其分配存储单元。只有函数被调用,启动后,才临时为其分配存储空间,并接受主调函数传送来的数据。实参的值通过堆栈向形参传递,二者不共享存储空间(不是指针,各用各的,拷贝过去的),名为“值传递”,在函数调用结束后,形参占用的存储空间也立即被释放。


    2)C语言只有值传递和地址传递,C++提供了引用,具体区别可以参见“C语言地址传递之经典swap()”一文,需要注意的是当参数为数组名时,实际上是传地址,即函数体对数组的所有操作会影响数组本身(因为本来就是对其本尊的操作)。

void merge(char A[],char B[])  {...}  //一维数组可以不指定大小,因为形参实参都是数组首地址,但是二者数据类型必须一致
void merge(char A[][5],char B[][5])  {...}   //二维数组可以只指定列数

    

    3)因为C语言支持传值的方式,所以实际调用时,实参可以是值的类型符合形参的变量或者表达式,当为表达式时,就涉及到形参的求值顺序问题,从左至右或者从右至左,不同编译系统的处理不同(类似于printf),大多数采用从右至左,但是为保险起见尽量避免在实参中使用相互关联的变量表达式,要么在调用前先行计算各表达式的值放在其他变量中,进而将后者作为实参传递;要么修改程序使实参各表达式之间变量不相关联。


17. 函数的返回值

    1)当没有指定函数的返回值时(连void也没有),系统返回值类型为整型,且函数体内return回来的是其他类型数据,系统也会通过强制类型转换将其转换为整型。


    2)当指定了函数的返回值类型,但是函数体中没有return,或者有return但是没有执行(未进入判断条件内),此时系统仍然会自动返回一个值作为函数的返回值,但是这样是非常危险的,故须留意。

注:VC++6.0 实测:

#include<stdio.h>

int Max(int m,int n)
{
	if(m>n) 
	{
		return 100;
		printf("执行了?");    //不放心,检测到底有没有进入循环体
	}
}

void main()
{
	int M=Max(1,100);
	printf("最大值为%d\n",M);
}

    编译未报错,运行结果:

最大值为1

    可以确认没有进入if(m>n),故Max(1,100)的返回值1是系统自动指定的。


    3)正是由于上述原因:没有写明返回值类型被强制转换为int损失精确度,或者没有return值被系统随机指定值返回,所以在不需要函数返回值时,要写上void。


    18. 全局变量与局部变量
#include<stdio.h>

int CallTimes=0;           //定义全局变量CallTimes并初始化为0

int max(int m,int n)
{
	CallTimes++;       //所有函数均可以对CallTimes进行操作
	return (m>n? m:n);
}
 
int min(int m,int n)    // m,n也是局部变量
{	CallTimes++;
	return (m<n? m:n);
}

void main()
{
	int a=10,b=8;       //局部变量
	printf("两数较大数为%d,较小数为%d\n",max(a,b),min(a,b));
	printf("调用了%d次\n",CallTimes);
}
    CallTimes为全局变量,形参m,n和main()函数内部变量a,b均为局部变量,即在所有函数体外定义的为全局变量,形参和函数体内的为局部变量。


1)注意可以声明而不初始化,如果要初始化一定要同时。

     像:    

int CallTimes;
CallTimes=0;

    是不允许的,因为在主函数main()和自定义函数外部只能定义(或同时初始化)变量,其他语句(如赋值)均

不能通过编译,上述这种方式不能称之为初始化,而是赋值。


2)C语言中,而局部变量可以用用类型相符的任意表达式来初始化,全局变量只能用常量表达式初始化,不能用

一个数学函数如sin(3.1415)或者其他的需要在运行时才能计算出结果的表达式进行初始化。

       究其原因,程序开始运行时要用适当的值来初始化全局变量,所以初始值必须保存在编译生成的可执行文件

中,因此初始值在编译时就要计算出来,然而有些数学函数例如sin(3.14)的值必须在程序运行时调用sin函数

才能得到,所以不能用来初始化全局变量。

3)如果定义全局变量而没有初始化,系统会自动初始化为0,局部变量为随机值。

4)C语言支持局部函数和全局变量同名,同名时在二者作用域重叠部分系统将自动忽略全局变量,视为对局部变

量的操作。但是实际程序设计中应该尽量避免这种情况。


使用全局变量的利弊:

利:    

      1) 可以传输多个数据,如自定义函数通常只能返回一个特定的值(指定类型的),要是想传输多个数据,

只能通过数组或者指针作为形参的形式,如快排中 void QuickSort(int A[],int high,int low)。像上述例子中统

计每个函数被调用的次数,使用全局变量则方便的多。

       2)全局变量相当于在各个函数之间打通了一个“公用通道”,减少了各个函数的参数传递及由此带来的内

存损耗和数据传送时间。

弊:

      1)全局变量在整个程序运行期间一直占用着存储空间,局部变量只在函数被调用分配内存,调用结束回收。

所以占用时间长,不能用于他用。

      2)过多使用全局变量降低了函数的通用性,特别是不同源程序之间的函数调用。全局变量使函数之间相互影

响,破坏了函数的封闭性,独立性,使。移植性大大降低,也违背了模块化的结构程序设计原则。



    19. 变量的存储类别—auto,static,register,extern

    变量的另一个属性: 存储类别,它涉及变量存在的时间长短,作用范围的大小,主硬件中存放它们的地点、区

域等。c 语言中,变量的存储类别大致分为四种: 自动类auto,静态类static,寄存器类register和外部类

extern。加上变量的存储类别,变量定义的一般形式应为:
   
         存储类别 数据类型 变量名
    
    之所以要讨论变量的存储类别,是由于在程序执行过程中,程序和数据,尤其是数据在内存中存放的区域是有

一定规定的。这种规定是为了更好地利用存储空间,提高程序执行效率的。供用户使用的存储空间大致分为三个

不同的部分:  程序区、静态存储区 以及运行栈区(也称动态存储区)。其中:

程序区: 存放程序的可执行代码模块。

静态存储区: 存放所有的全局变量以及标明为静态类的局部变量部分。

运行栈区: 存放的数据又分为以下几种:

(1) 函数调用时,顺序动态存放主调函数执行过程中的现场(各类寄存器、程序计数器、状态寄存器等的内容以及返回地址等),此类数据存放也称现场保护。

(2) 所有未标明为静态类的局部变量。

(3) 函数的形式参数。

    存放在运行栈区的数据均采用 动态存储分配方法,即在函数调用时临时指定存储空间,函数结束时又

逐一加以释放,腾出所占内存空间,以供后面函数调用时再分配使用。

    1)auto

    所有局部变量(形参是,所以也包含在内)的默认存储类别都是auto。

    2)static

    所有全局变量的默认存储类别都是static,如果在函数体内部(即局部变量区)定义静态变量,一定要加关键字static。

    static变量存储在静态储存区,固定地占有分配给他们的存储空间,在整个程序运行期间有效。全局变量(即全局的static)只能为当前源文件使用,不能为其他源文件使用,局部static只能为当前函数使用,不能为其他函数使用。

    3)register

    CPU使用寄存器中的数据速度要远远快于使用内存中的数据速度。因此,应用好CPU 内的寄存器将可以大大提

高程序的运行速度、运行效率,C语言允许某些变量的存储类别为寄存器类,就是为了充分利用CPU 内的通用寄

存器,提高程序运行的效率。由于CPU中通用寄存器的数量有限,所以,通常是把使用频繁的变量定义为寄存

器变量。定义为寄存器变量的变量将在可能的情况下,在程序执行时,分配存放于CPU 的通用寄存器中。

        各种计算机硬件上的差异会比较大,通用寄存器的数目、使用方式也各不相同。所以,对C语言中寄存器变

量的处理方式也不尽相同。下面给出使用寄存器变量必须注意的一般性注意事项:

    ①通用寄存器的长度一般与机器的字长相同,因为数据类型为float,long 以及double的变量一般占大于机器

字长,所以通常不能定义为寄存器类别,而int、short 和char类型的变量准许定义为寄存器变量类别。

    ②寄存器变量的作用城和生命周期与自动变量是一样的。故只有自动类局部变量可以作为寄存器变量。寄存器

变量的分配方式也是动态分配的

    ③任何计算机内通用寄存器的数目都是有限的。故不可能不受限制地定义寄存器变量。超过可用寄存器数目的

寄存器变量,一般是按自动变量进行处理。另外,有些计算机系统对C语言定义的寄存器变量,处理时并不真正

分配给其寄存器,而是当做一般的自动变量来对待,在运行栈区为其分配存储单元; 也有些能进行优化的编译系

统,能自动识别使用频繁的变量,在有可使用寄存器的情况下,自动为它们分配寄存器,而不需程序员来指定,

此时有register定义变量已不太重要: 加上现在计算机发展较快,一般程序使用寄存器变量节省时间有限,故用不

用register 定义变量已无明显作用。

     4)extern

    extern只可以修饰全局变量(函数体外),修饰的变量不仅可以在一个源文件使用,也可以在一个工程内使用,即不同的源文件使用[1]。

Exercise & Question:

#include<stdio.h>
#include<time.h>       //计算一段程序运行时间使用的变量clock_k和函数clock()均在"time.h"

void main()
{
    clock_t start,finish;
    double  duration;

    register int i;
    start=clock();
    for(i=0;i<100000000;i++);     //1亿次
    finish=clock();
    duration=(double)(finish-start)/CLOCKS_PER_SEC;
    printf("register用时: %f seconds\n", duration);

	auto int t;
	start=clock();
	for(i=0;t<10000;t++);
    finish=clock();
    duration=(double)(finish-start)/CLOCKS_PER_SEC;  //1万次
    printf("auto用时: %f seconds\n", duration);

}

结果:

register用时: 0.210000 seconds   //1亿次
auto用时: 1.775000 seconds       //1万次

    有趣的是,当将计时功能写成一个函数(register,auto作为实参分别传入),分别计时时,二者的用时竟然基本相同,都相当于register用时。

#include<stdio.h>
#include<time.h>

void Interval(int number);

void main()
{
	register int i;
	Interval(i);            //register int i 作为实参计时

	auto int j;             //auto int i 作为实参计时
	Interval(j);
}


void Interval(int number)
{
    clock_t start,finish;
    double  duration;

    start=clock();
    for(number=0;number<100000000;number++);
    finish=clock();
    duration=(double)(finish-start)/CLOCKS_PER_SEC;
    printf("用时: %f seconds\n", duration);
}

结果:

用时: 0.209000 seconds        //上个单独register运行结果基本相同
用时: 0.211000 seconds     

推测的可能原因:

    1)计时函数void Interval(int number) 在实际调用时,忽略了register int i 和 auto int j 和区别,因为关于

auto C语言有一个说明:当未指定变量的存储类别是,默认为auto,所以编译器在编译

void Interval(int number) 时,自动地视为auto int number处理(虽然形参并不分配存储空间,只有等到实参

传入才会真正在动态存储区分配),而且Interval()为值传递,所以真正调用时,register int i 和 auto int j 均只

拷贝了一份32位的int“值”过去,没有考虑到存储类别的区别,只是对i,j分配了存储空间,对于Interval()传入的实参均使用auto int number类型存储,二者的运行是一样的,都是按照auto int的运行时间。

   但是问题来了,从运行结果来看,二者都是按照register int存储类型来计算的

    ???


    2)void Interval(int number)的参数传递的确是值传递,但是不仅传递参数的数据类型,值大小,也传递其

存储类别,因为定义变量的完整形式为:

    存储类别    数据类型    变量名;

    参数传递的应该是完整的参数,三要素都包含,故编译时会考虑register和auto的区别,而不是统一处理。故

编译时,因为int number是形参,先不确定其存储位置(当然,知道其肯定不在静态存储区,auto和register都

是动态分配的方法),当实参到来时,会根据实参的存储类别来决定其存储位置,若为register则放在register

中,若为auto则放在动态存储区。

    至于为什么最后二者时间会一样,而且都是register的运行时间,可能的原因应该是编译器优化。

    为了充分利用计算资源,如果有空闲的寄存器,不管int的存储类别(除static,如auto),默认将int储存位置

预定在register,这样做到了既参数传递传递了完整的三要素,实际的运行中又优化全部作为register处理,结果

相同。

    但是,如果考虑到编译器优化,则参数传递到底有没有传递存储类别就无法得知,因为无论是分别传入了auto int和register int还是全部当做auto int来传入,编译器最终都是作为register运行,参数传递的区别被屏蔽了,Coder无法得知。

        另外注意的一点,个人猜测,由于编译时编译器并不知道实际运行时CPU的寄存器使用情况,所以应该不会强行指定所有int存储在register中,而是在运行时视寄存器的使用情况来来决定是否优化。


    指针可以进行的运算:赋值,加减整数,两个之间相减,两个之间比较

    1)赋值

    取地址赋值和用其他指针变量(同类型)赋值:

    char *ptr=&A[0][0];

    char *ptr1=ptr2;     //ptr2已经定义且使用过


    2加减整数

 double A[N][N];

 double *ptr=A;

 for(;i<N*N;i++)

     printf("%f ",*(ptr++));    //ptr++就可以达到遍历数组元素的目的

    要注意的是,指针变量加减整数n代表指针实际的值(所指变量的地址)加减这个整数n,应该是

    指针实际的值(所指变量的地址)    +/-    (整数n*所指变量类型所占字节数)

    (PS:假设前提:内存按字节编址)

    就比如上例中,ptr++,即ptr加上1,实际上ptr的值移动了8个字节,于是ptr指向了下个数组元素。即指针++,代表指向了下个同型变量。


/*  待定

    3)两个之间指针相减

    由2)其实已经知道,指针“外显”值为所指变量地址值/sizeof(变量类型),在加减运算中可以“认为”是一个整数。其实,C语言规定,两个同型指针之差的返回值为int型。

Exercise

#include<stdio.h>

void main()
{
	double a[2];
	printf("a的值为%p,a+1的值为%p\n",a,a+1);

	char *p1=(char *)a,*p2=(char *)(a+1);
	printf("p1的值为%p,p2的值为%p\n",p1,p2);

	int length=p2-p1;
	printf("所占字节数为:%d\n",length);
}

    结果:

a的值为0018FF38,a+1的值为0018FF40         //可以明显看出相差 2*16 bit=32 bit =4 Byte
p1的值为0018FF38,p2的值为0018FF40         //转换为char后,地址值并没有改变,只是“基本单位”变为1B
所占字节数为:8 

    上面除了验证指针加整数后指针本身的值其实加 n*sizeof(类型) 外,也验证了两个指针之差返回值为int,另外,本例也提供了除用sizeof()运算符之外,利用指针特性自行计算各变量类型所占字节的新思路。当然,前提是知道char类型占1字节。


PS:C语言不支持两个指针相加的运算,会报错:error C2110: cannot add two pointers

*/

4)两个之间指针比较

    用以判断两指针所指变量的前后关系(当然,是通过比较指针值(即地址)的大小来实现的),通常用于判断一个数组内的元素前后关系。


    C支持的换行:

① 宏定义一般都是单独成行,如果想换行则用拼接符\,预处理时会自动忽略,连接下一行

② 普通代码中,直接换行或者加空格,只是不能破坏关键字和标识符

③ 对于字符串常量,可以用连续的“ ”连接起来,位置任意

// C 支持的语句换行,测试环境:VC++6.0

#include<stdio.h>
#include<string.h>

#define MA\
X 100                  //宏定义中必须要用拼接符\

void main()                  
{
	double 
		a[2]={1,MAX};       //支持如此直接换行,不要拆分关键字和标识符即可

	printf("即使换行,值仍然为%f,%f\n",

	a[0],a[1]);                          //成功赋值并输出了

	char *p="This"  " is "

	"my "  "world" 

	"!";                  //对于字符串可以用连续的双引号来解决

	puts(p);
}

    结果:

即使换行,值仍然为1.000000,100.000000
This is my world!
    
    20. 结构体struct注意点:定义,内存分配,成员引用

1)结构体允许嵌套定义,即大的包含小的,这点与函数不同。但是结构体不支持递归定义,即大的可以包含小的,但是不可以包含本身(小的不可以是本身),注意,函数同样不支持递归定义(因为连嵌套都不允许),递归函数的定义只是嵌套调用而已。


2)结构体分配内存遵循两个原则:

① 分配给当前变量的空间首地址(相对于结构体首地址的偏移)必须为当前变量类型所占字节数的整数倍;

② 结构体最终所占空间大小必须为结构体中占用空间最大的数据类型大小的整数倍。

eg:

struct example1
{
  double a;    // 8B
  char c;     // 1B
  int i;       // 4B
};
struct example1 ex1;        //注意,对类型并不分配空间,只有声明了结构体变量如ex1,才分配空间

    ① 起始地址为0(其实是指偏移量),double a占8B:0B~7B;对于char c来到了8B,刚好是char类型大小1B的整数倍,不用填充;int i来到了9B,但是9B不是int大小4B的整数倍,于是填充9B~11B (内容任意),来到12B,放入int i。

    ② 最终结构体占0B~15B=16B,刚好为最大类型double 8B的整数倍,末尾不用填充。


struct example2
{
  char c;       // 1B
  double a;     // 8B
  int i;       // 4B
};
struct example2 ex2;        //注意,对类型并不分配空间,只有声明了结构体变量如ex2,才分配空间

    ① 起始地址为0(其实是指偏移量),char c占1B:0B;对于double a来到了1B,不是double类型大小8B的整数倍,填充至1B~7B,于是double a:8B~15B;int i来到了16B,刚好为int大小4B的整数倍,于是填充16B~19B放入int i。

    ② 最终结构体占0B~19B=20B,不是最大类型double 8B的整数倍,末尾填充4B:0B~23B=24B,完成。


PS:

① 对于没有初始化的结构体变量或者成员,如果是数值型数据,系统自动赋值为0;如果是字符型数据,数据自动赋值为\0。

② 共用体union分配其最大的数据类型的大小;

    关于枚举类型enum,首先,对于枚举元素系统是当作常量来处理的,称之为枚举常量,从0开始的整数:0,1,2,3...,如可以用printf的%d格式控制符来直接输出枚举变量的值,eg:

#include<stdio.h>

enum Color
{
	Red,Green,Blue  //注意枚举类型是用逗号,而且末尾没有符号(更像是数组,在switch中使用更是这样,不要当作字符串常量)
};                           

void main()
{
	enum Color color;
	color=Blue;
	switch(color)
	{
	case Red:printf("红色Red,系统中其值为%d\n",color);break;
	case Green:printf("绿色Green,系统中其值为%d\n",color);break;
	case Blue:printf("蓝色Blue,系统中其值为%d\n",color);break; //没有双引号"",直接就是其值,更像是集合{}中取值比较

	default:printf("Error!\n");
	}
}

运行结果:

蓝色Blue,系统中其值为2

    其次,C++标准文档中是这样说明的:“枚举类型的尺寸是以能够容纳最大枚举子的值的整数的尺寸”,同时标准中也说名了:“枚举类型中的枚举子的值必须要能够用一个int类型表述”[2],即如果枚举类型大小固定,为int大小,4B。


    3)结构体成员引用

① 对于结构体变量,使用 . 操作符,如:

struct example1
{
  double a;    // 8B
  char c;     // 1B
  int i;       // 4B
};
struct example1 ex1;   
ex1.a=666;       // 对于结构体变量,使用点操作符 “.”


② 对于指向结构体变量的指针变量,使用->操作符,如:

struct example1
{
  double a;    // 8B
  char c;     // 1B
  int i;       // 4B
};
struct example1 ex1,*p;
p=&ex1;   
p->a=666;       // 对于结构体指针,使用操作符 “->”
    特别地,因为对指针变量使用了“*”相当于对其值(所指变量的内存内存地址)进行了寻址,如上例中“*p”其实就相当于ex1本身,故 (*p).a=666同样合法。


③ 结构体成员为数组时,可以直接引用,但是为数值型数组只能一个一个引用,eg:

struct student
{
  char name[20];
  float score[4];
}stu;

scanf("%s",stu.name);            //直接引用字符串数组
for(i=0;i<4;i++)scanf("%f",&stu,score[i]);  //只能一个一个引用数值型数组



    21. 三种包含指令:#include

    #include <stdio.h>

    #include "stdio.h"

    #inlude  "c:\tc\include\stdio.h"


    ① #include <stdio.h>

    只在专门存放头文件的目录(通常为include)下查找该文件。


    ② #include "stdio.h"

    先在当前源文件的目录下查找该头文件(一般都不放在这里),再去专门存放头文件的目录(通常为include)下查找该文件。

    所以,一般情况下<stdio.h>要比"stdio.h"快。


    ③ #inlude  "c:\tc\include\stdio.h"

    指明路径,应该是最快的,但是太耗费时间,应用实例:目录“c:\tc\f.c”下“f.c”文件为以下内容:

#include<stdio.h>
#define PI 3.1415

    则以下源程序可以编译运行无误:

#include"c:\tc\f.c"

void main()
{
  int R=2;
  printf("圆面积为%f\n",(double)PI*R*R);
}

   

后记: 

     在一些文献中指出,C语言的语句用来向计算机系统发出操作指令,而变量的数据类型声明和函i数返回值的类型声明不产生机器操作,因此,将函数声明部分的内容不称为语句,且也能够知道C对变量分配存储空间,对类型(如int,strut,union)不分配存储空间。

     宏定义很多细节,拼接\,#,##啥的不用太纠结。

    

初始化是编译时静态赋值,占用编译时间;普通赋值操作是运行时动态赋值,占用运行时间。

可以在多个.c文件中定义同名全局变量,但是前面要加static。且只有一个.c可以赋初值,否则连接错误。





参考资料:

[1]  杨建霑,汪同庆《C语言程序设计》第1版,武汉大学出版社

[2]  https://blog.csdn.net/zz460833359/article/details/48878513


声明:除已注明的引用外,文章系作者原创,引用转载请注明出处,如有必要请联系作者。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值