您的位置:首页 > 编程语言 > C语言/C++

《C语言深度剖析》学习笔记----C语言关键字

2013-08-24 20:02 337 查看

本节主要说的是C语言中的关键字,包括具体的定义和注意事项,以及部分小例程。

一、基本的数据类型

1.数据类型是固定内存大小的别名,变量是一块连续存储空间的别名。数据类型就是一块模子,而变量就是用这块模子弄出来的一块内存。我们可以通过变量的名字来使用内存空间。

例程:

#include <stdio.h>

int main()
{
char c = 0;
short s = 0;
int i = 0;

printf("%d, %d\n", sizeof(char), sizeof(c));
printf("%d, %d\n", sizeof(short), sizeof(s));
printf("%d, %d\n", sizeof(int), sizeof(i));

return 0;
}

通过上面的例程可以发现,我们的变量和定义这些变量的数据类型所占用的字节数是一致的。

2.用来修饰变量的关键字(auto,register,static)

C语言中局部变量也有自己的属性,我们可以通过关键字来描述这些属性。

(1)auto是所有局部变量默认的属性,是在栈上分配空间的。auto修饰的变量不要定义为全局变量,会报错。因为全部变量是位于静态存储区的,而auto修饰的变量是位于栈空间的。

错误程序:

#include <stdio.h>
#include <stdlib.h>

auto int a;

int main(int argc, char *argv[])
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf ("%d\n", f());
}

return 0;
}

(2)static修饰的变量是在程序的静态存储区分配空间的。

static可以修饰函数也可以修饰变量。

定义的函数和变量只能在本文件中使用。

同时:static修饰的变量在这个工程运行期间只能被初始化一次,以后无论怎么改变都不会回到最开始初始化的值。这点要额外注意。

例程:

int f()
{
int ret = 0;
static int a = 0;
a = a + 1;
ret = a;
return ret;
}
int main(int argc, char *argv[])
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf ("%d\n", f());
}

return 0;
}

该程序运行的结果是1,2,3,4,5,6,7,8,9,10.如果static修饰的 a每一次都会被初始化为0的话,那么我们将会打印出十个1,但是情况并不是10个1,所以可以从侧面证明static修饰的变量在程序运行之初只被初始化一次。

static同时也是作用域限定符,static修饰的全局变量作用域只在本文件中。static修饰的函数作用域只是声明的文件中。

(3)register修饰的变量是在寄存器中的变量,不允许使用&来获取变量的地址,因为寄存器的地址才是真的纯的物理地址,真的是不能再物理了。同样的register也不可以用来修饰全局变量,因为我们的全局变量是作用于程序的始终的,如果寄存器始终都是这么一个值,那么我们还要它做什么呢?为什么存在寄存器?因为我们要的是速度,要的是随时改变。

二、流程控制的关键字(if,switch,do,while,for)

1.if和else是一对好兄弟,总是成对出现的,不过我个人觉得有的时候为了逻辑上的简单可以单独出现if而没有else,不过绝对不可以只要else没有if,同时在if和else都有很多个在程序中出现的情况下,else总是和最近的if进行组合,C语言中有很多原则都是就近原则,暂时见一个写一个吧。

同时我们的判定条件也有几点需要注意的。

(1)安全编程

假如我们在程序中要对一个整型变量 a进行判定是否为1,如果a为1那么就执行if里面的内容,否则,执行else。

按照常规的思路来写这个判定如下:

if (a == 1)
{
/*******/
}
else
{
/*******/
}

但是谁都知道码农是最苦的,我们整天就看着这些程序总有个头晕或者笔误的时候,假如我们一不小心把if (a == 1)误写成了if (a = 1),那么我们的后果就可向而知了,估计整个程序都有可能毁掉。

所以我们最好的方法是把做这样的判定,if (1 == a),这样做我们一个变量是无论如何也不可以给一个常量赋值的,所以假如我们误写成了if (1 = a),我们的编译器会马上给我们报错。

例程如下:

if (1 == a)
//if (1 = a)这样会马上报错
{
/*******/
}
else
{
/*******/
}

(2)使用bool变量的时候,直接用if对这个变量进行判断就可以,不要将bool变量与其他值进行比较。

例程:

int main()
{
bool b = TRUE;
if (b)
{
printf ("b是真的\n");
}
else
{
printf ("b是假的\n");
}
}

切忌不要出现b是bool变量,同时还出现if (b == 1)类似的判定,这是错误的。

(3)当我们需要一个float变量和0值做比较的时候,这个时候我们需要定义精度,而不可以直接把float变量和0值做比较。

具体代码如下:

#define JD 0.00000001; //首先定义精度

float b = 0.0; //给变量赋初值

if ((-JD =< b) && (b <= JD))
{
....;
}
else
{
....;
}

这里的JD宏定义可以定义的再小一些,这个是随着我们要求的精度来实现的。

这里说到float,还要说一个的就是,如果两个变量都是float型的,那么我们不要将一个很大的float变量和一个很小的float变量进行相加,因为如果相加的话float的精度将达不到我们的要求,相加的结果自然也不是我们想要的结果。

具体代码如下:

#include <stdio.h>

int main ()
{
float c;
float a = 10000000000.00;
float b = 0.00000000001;
c = a + b;
printf("%f\n",c);
}

打印结果如图所示:



2.switch语句对应对应多个分支判定的情况

(1)每一个case语句都要有相应的break,否则会出现多个分支输出的情况,这个就上例程代码了,计算机二级里的常客。

(2)default语句必须出现在switch语句中,这个default语句可以用来处理默认情况也可以用来作为前面情况都没有而出现的最后一种情况,是具体情况而定。

(3)switch语句中case后面的表达式只能是整型或者字符型的常量或者整型或者字符型的表达式,不支持其他类型的判定。

例程:

#include <stdio.h>

int main ()
{
float a = 0.0;
switch (a)
case 0.0:
{
printf ("。。。",);
}

}

上面的程序将会报错,因为case后面只能是整型或者字符型的常量或者整型或者字符型的表达式。

(3)因为编程规范性问题,case后面要执行的语句最好加上{},这样可以使代码更加清晰。

3.循环语句(do,while,for)

(1)do的循环是while相互配合的,无论条件是否满足都要先执行一次循环,然后再进行判断,有的时候这种逻辑更能会符合自己的逻辑,但是有的时候会带来不必要的麻烦。

(2)while循环是先进行一次判定,然后再执行循环,for和while的机制是一样的,但是有的时候for比while更简单,有的时候while比for更简单,没有绝对的。

三种for循环的难点不再本身,而在于程序的逻辑,或者判定条件的选取,这个不是一下两下可以说清楚的,我觉得,敲打的多了,自然也就懂了。

具体代码如下:

#include <stdio.h>

int f1(int n)
{
int ret = 0;
int i = 0;

for(i=1; i<=n; i++)
{
ret += i;
}

return ret;
}

int f2(int n)
{
int ret = 0;

while( (n > 0) && (ret += n--) );

return ret;
}

int f3(int n)
{
int ret = 0;

if( n > 0 )
{
do
{
ret += n--;
}while( n );
}

return ret;
}

int main()
{
printf("%d\n", f1(10));
printf("%d\n", f2(10));
printf("%d\n", f3(10));
}

(3)说到了循环,就必要说到怎么跳出循环了,因为有的时候我们的确很需要强制性的跳出循环,这样我们就需要其他的关键字了。

continue表示的程序终止本次循环而进入下一次循环。

具体实例代码如下:

#include <stdio.h>

int main ()
{
int i = 0;
while (i != 3)
{
printf ("进入while循环\n");
if (i == 2)
{
continue;
}
printf ("退出循环\n");
i++;
}

}

这个程序将会进入无限的死循环,因为当i = 2的时候我们跳出了这个循环,所以不能够执行i++,也就是说i永远不会执行到i = 3,也就是循环永远无法跳出。

break表示终止本次循环,执行程序中循环以后的部分。

示例代码如下所示:

#include <stdio.h>

int main ()
{
int i = 0;
while (i != 3)
{
printf ("进入while循环\n");
if (i == 2)
{
break;
}
printf ("退出循环\n");
i++;
}
}

程序执行结果如下图所示:



(4)老唐在这里还留出来了一个问题:switch语句中可以使用contine么?最后经过测试,我的出来的结论是:switch就是和case搭配的,不可以使用continue,别为太多为什么,这就叫语法。

例程:

#include <stdio.h>

int main()
{
int a;
a = 3;
switch (a = 3)
{
case 1:
printf ("1\n");
break;
case 3:
printf ("3\n");
continue;
case 2:
printf("2\n");
break;
deault:
break;
}
}

编译期间就报错。

(5)我也有过这种疑问,while已经很强大了,为什么还要使用do while呢,因为有的时候它的逻辑真的很怪,但是后来老唐给了一个经典的例程。

例程如下:

#include <stdio.h>
#include <malloc.h>

int func(int n)
{
int i = 0;
int ret = 0;
int* p = (int*)malloc(sizeof(int) * n);

do
{
if( NULL == p ) break;

if( n < 0 ) break;

for(i=0; i<n; i++)
{
p[i] = i;
printf("%d\n", p[i]);
}

ret = 1;
}while(0);

free(p);

return ret;
}

int main()
{
if( func(10) )
{
printf("OK");
}
else
{
printf("ERROR");
}
}

这里do while 和free函数进行配合,我们完成了安全性的检测(两个安全性检测,两个if),也执行了for循环,也就是do while对我们要做的进行了一次封装。这也是do while 存在的一个必要性。

三、关键字goto,voide,sizeof,extern

1.goto无论是陈正冲老师还是唐老师都说最好不要使用,因为goto在跨内存使用的时候会出现无数个问题,程序的逻辑也会造成混乱。但是我粗略的看的内核代码中还是有很多goto,我觉得我同学理解很对:goto可以说C语言面试底层的一个标志性的东西,就像指针一样,因为那个时代很多C语言的使用者都是从汇编转变过来的,这样使用goto可以和他们汇编语言中的jump合拍。只是一家之谈,勿喷。

关于goto的害处老唐的例程很经典:

#include <stdio.h>

void func(int n)
{
int* p = NULL;

if(  n < 0 )
{
goto STATUS;
}

p = malloc(sizeof(int) * n);

STATUS:
p[0] = n;
}

int main()
{
f(1);
f(-1);

return 0;
}

在这个程序中,假如我们传进来的n判定小于0了,那么我们将直接跳转到STATUS位置,执行p[0] = n;但是这个时候的p我们并没有给p开辟空间,所以会造成内存错误。

这是一种情况,还有另外一种情况就是假如我们goto的位置没有什么危害性的操作,但是我们在goto之前就已经开辟空间,而在goto和跳转位置之间进行free,那么我们会造成内存泄露,出现野指针。

2.关于void

(1)void 这个关键字很常见,它主要用来修饰函数的返回值和参数,如果函数没有返回值,那么应该将函数的返回值类型声明为void,如果函数的参数为空,那么也应该置为void,void修饰函数的返回值和参数仅仅是为了表示为空。

(2)在C语言中绝对没有用void来修饰的变量。

(3)在C语言中只有两种相同类型的指针才可以相互赋值。

(4)当void *作为左值的时候可以接受任意的类型,当void *作为右值的时候必须要经过强制类型转换。

3.extern关键字

extern关键字有两种使用方法。

(1)extern "C",这种方法用于c++编译器或者非标准C编译器中指示编译器使用标准C的方式进行编译。

示例代码:

extern "C"
{
int add(int a, int b)
{
return a + b;
}
}

(2)extern的另一种用法是告知程序,使用extern修饰的变量是供其他程序使用的。同时extern也可以用来修饰函数。

// test.c

#include <stdio.h>

extern int g;
extern int get_min(int a, int b);

int main()
{
return 0;
}

// test2.c
int g = 100;

int get_min(int a, int b)
{
return (a < b) ? a : b;
}

(3)定义的时候我们要在本文件声明extern变量,我们同时也可以在本文件中使用,同时我们在其他的文件中调用这个变量的时候也要进行extern声明,函数也是这个使用方法。

4.sizeof不是函数,的确是关键字。

(1)sizeof所要求的值在函数的预编译期间就已经确定下来了,而函数要等到程序运行期间才可以返回值,所以这个可以证明siezeof是关键字而不是函数。

(2)sizeof用于计算相应实体所对应的内存大小。

(3)陈正冲老师的例子:

#include <stdio.h>

/*sizeof不是函数,只是关键字*/
/*int main ()
{
int a = 4;
printf ("%d\n",sizeof (int));
printf ("%d\n",sizeof (a));
//错误的。
//printf ("%d\n",sizeof int);
printf ("%d\n",sizeof a);
}

/*sizeof关键字和指针*/
int main ()
{
int *p = 4;
int a[100];
//这两个打印函数打印出来的都是4
printf ("%d\n",sizeof (p));
printf ("%d\n",sizeof (*p));
//这个函数打印出来的是400
printf ("%d\n",sizeof (a));
}

(4)sizeof可以修饰变量,也可以修饰变量名,但是修饰变量的时候只能使用sizeof (int)这种形式,而修饰变量的时候可以使用sizeof (a) 和sizeof a这两种形式。

示例代码如下:

#include <stdio.h>

int main()
{
int a;

printf("%d\n", sizeof(a));
printf("%d\n", sizeof a);
printf("%d\n", sizeof(int));

return 0;
}

四、一个难懂的const和一个不常见的volatile

1.const

(1)const修饰的变量是只读的,其实const修饰的还是变量,本质上const只对编译器起作用在运行的时候并没有实际作用。

(2)当用const用来修饰数组的时候,数组的空间并不可以被改变。

(3)const修饰指针时候的注意事项

 ①在.c文件中const修饰MAX会报错,因为const修饰的实际是一个变量。

 ②const 不可以修饰只读变量

 ③编译器通常不为普通的const只读变量分配空间。const与宏的主要区别是宏每一次的使用变量都要分配内存空间。

 ④const int a;

   int const b;这两个都正确。
 ⑤const int*p; //const修饰*p,p是指针,*p是指针指向的对象,不可变

 int const *p;//const修饰*p,p是指针,*p是指针指向的对象,不可变

 int*constp;//const修饰p,p不可变,p指向的对象可变

 const int*constp; //前一个const修饰*p,后一个const修饰p,指针p和p指向的对象都不可变

(4)当const用来修饰函数参数的时候,表示这个参数在传进函数内部的时候,并不希望这个函数来改变这个参数的值。

          当const用来修饰函数的返回值的时候,表示函数的返回值不可以改变,多用于表示返回指针的情形。

2.volatile

(1)volatile的作用是告诉编译器它所修饰的变量每次都要到内存中取值,这样做是为了防止硬件中断或者期间这个变量的值被修改而下次再调用的时候编译器根本不知道。



(2)程序中添加了volatile的时候程序就会告诉编译器每一次都要重新取值。

(3)老唐的问题:const和volatile可以同时修饰一个变量,同时const volatile int a = 100;也可以运行。

例程:

#include stdio.h
int main()
{
volatile int i;
int j = i;
int k = i;

const volatile int a = 100;
}

const volatile int a 这个可以执行,就是可以理解成每次从一个固定变量中取值。这个是正确的。

五、struct、union

1.struct

(1)一提到struct就必然想到结构体,老唐说:一个结构体的大小(所占的字节数)是这个结构体中每一个成员所占的字节数的总和。这里的结构体大小是所占用的字节数,其实里面还有知识,可以到我的令一片博客----内存对齐中去看。

(2)空结构体问题

例程截图:



空结构体的大小是1,这种问题不要想的太多,只是一个标准,只是一个灰色地带问题,人家的C语言是在开发中成长起来的,小的标准不正规也可以理解。

(3)struct和class

class 和struct可以通用,但是struct修饰的是public而class修饰的private,其实就是一个属性问题。

2.union

(1)struct中的每个域都在内存中独立的分配空间,然而union只分配最大的域空间,所有的成员都共享这个空间。

(2)union的使用受到系统是大端系统还是小端系统的影响。

系统大端小端的测试代码:

#include <stdio.h>
union
{
int i;
char a[2];
}*p,u;
int main()
{
p = &u;
p->a[0]=0x39;
p->a[1]=0x38;

printf ("%d\n",p->i);
}

如果打印出来的是3839那么系统是大端模式,如果打印出来的是3938则系统是小端模式,也可以说编译器是小端模式。

上述代码执行结果如图:



十进制是14393,那么十六进制是3839,也就是高字节存储在高地址中,低字节存储在低地址中,所以我们的编译器是小端的。在我们日常看见的编译器和处理器大多数都是小端模式的。
大端模式(Big_endian):字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中。

小端模式(Little_endian):字数据的高字节存储在高地址中,而字数据的低字节则存放在低地址中。



六、enum的枚举型常量是真正意义上的常量

(1)如果enum定义的变量没有进行赋初值,那么我们默认这个变量的值为自然数0.

例程:

#include <stdio.h>
int main()
{
enum color
{
red,
blue,
yellow,
};
enum color b = red;
printf ("%d", b);
}

打印结果如下:



(2)如果enum定义的枚举变量只给第一个成员赋了初值,那么其他的成员如果没有赋初值将会按照自然数进行自加。

#include <stdio.h>
int main()
{
enum color
{
red = 1,
blue,
yellow,
};
enum color b = blue;
printf ("b = %d", b);
}

打印结果如下:



(3)如果下一个成员没有定义,那么它会按照离自己最近的成员进行自然数加1.

#include <stdio.h>
int main()
{
enum color
{
red = 1,
blue = 3,
yellow,
};
enum color b = yellow;
printf ("b = %d", b);
}

打印结果如下:



(4)枚举类型和宏的差别

枚举常量是真正意义上的替换,而宏是在预编译期间就已经替换完毕了。

宏定义的常量没有办法被调试,而枚举常量是可以的。

宏定义的常量是没有类型信息的,而枚举常量是具有特定类型信息的。

七、typedef不是定义一种新类型!

1.定义:typedef用于给一个已经存在的数据类型重新命名。它并没有产生新的类型,经过typedef重命名的类型不能够再进行unsigned和signed扩展。不过如果是先使用unsigned或者signed对变量进行修饰,那么是可以再使用typedef进行重命名的。

例程:

#include <stdio.h>
int main()
{
typedef unsigned int INT;
INT a = 1;
printf ("a = %d", a);
}

上面的程序可以编译和正常执行。

2.常用方式

(1)常用的方式有给结构体起别名,我个人觉得那样会方便我们使用结构体类型,要不然每次使用结构体类ixngde时候都要写struct XXX,会显得比较繁琐。

#include <stdio.h>
int main()
{
typedef struct student
{

}Stu_st,*Stu_pst;
}

(2)typedef不支持使用已经被static修饰过的变量进行重命名。

例程:

#include <stdio.h>
int main()
{
typedef static int int32;
}

编译报错。

3.typedef和define的差别

(1)typedef是给已有的变量取别名。

(2)define是简单的字符串替换,没有其他的含义。

例程:

typedef char* PCHAR
PCHAR p1, p2;

#define char* PCHAR
PCHAR p1, p2;

这里的第一个程序,在程序执行的时候p1和p2是两个char*类型的指针。而第二个程序在预编译过后,p1是char*类型的指针,而p2只是一个char类型的字符。这是因为宏只是简单的替换。不过用宏也是有很强大的功能的,在后续章节会介绍给大家。

八、定义与声明

定义:定义是创建一个对象并为之分配内存。

声明:声明是告诉编译器有这么一个对象。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: