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

C语言笔记之关键字(一)

2016-03-10 16:01 232 查看
register

请求编译器尽可能的将变量存储在CPU内部寄存器,提高访问效率。补充:寄存器是一块一块小的存储空间,其存取速度比内存(大地址)要快得多,离CPU很近。register变量可能不存放在内存中,所以不能用取地址运算符

static:

修饰变量,但又分为局部变量和全局变量,但都存在内存的静态区,不过作用域不同。前者在函数体里面定义的,之鞥在这个函数里调用了,即使该函数的运行结束,其值仍不会被销毁,在下次使用时值也一样。而后者,作用域从定义之处开始,到文件结尾处结束,在定义之处的前面要想使用,也得加extern在其他文件里即使用extern也不能使用。

#include<stdio.h>
static int j;
void fun1(void)
{
static int i = 0;
i++;
printf("i = %4d\n",i);
}
void fun2(void)
{
j = 0;
j++;
printf("j = %4d\n",j);
}
int main()
{
for (int k = 0;k<10;k++)
{
fun1();
fun2();
}
}


该代码的运行结果为:i从1加到10.而j保持1不变。

修饰函数。值对函数的作用域仅局限于本文件。C++对static赋予新的含义,下次专门开专题来写啊啦啦啦。

sizeof:

首先需要注意的是sizeof是关键字而不是函数。

#include<stdio.h>
int main()
{
int i = 0;
int j;
j =sizeof int;
}


此处编译会报错。因为int 之前加关键字,系统会以为是类型扩展,而sizeof i是可以编译通过的。所以说,sizeof在计算变量所占空间大小时,括号可以省略,而计算类型大小时括号不能省略。

#include<stdio.h>
int b[100];
void fun(int b[100])
{
int o  = sizeof(b);
printf("%4d\n",o); //4字节
}
int main()
{
int *p = NULL;
int j = sizeof(p);     //4指针的大小为4个字节
int k = sizeof(*p);    //???*p没有具体指想,大小如何确定
int a[100];
int i =sizeof(a);      //400 ,此时的a代表整个数组空间
int l = sizeof(a[100]);//4   ???
int x = sizeof(&a);    //400  ???
int w = sizeof(&a[0]); //4  指针四个字节
fun(b);

}


signed,unsigned:

#include<stdio.h>
#include<string.h>
int main()
{
char a[1000];
int i;
for (i = 0;i<1000;i++)
{
a[i] = -1-i;
}
printf("%4d\n",strlen(a));

}


答案是255.让我们来一起分析分析,for循环内,当i的值为0时,a[0]的值为-1,-1在内存里面用补码存储,为0xff,-2的补码是0xfe…当i的值为127时,a[127]的值为-128,而-128是char类型数据能表示的最小的负数。当i继续增加时,a[128]的值发生溢出,丢弃最高位,0x7f,当i继续增加到255的时候,-256的补码的低八位为0。然后当i增加到256时,-257的补码的低八位全为1即OXff.经分析,a[0]到a[254]里面的值都不为0,而a[255]为0。strlen函数是计算字符串长度的,并不包含字符串最后的’\0’.

float变量:

不能直接与0值进行比较

case

#include<stdio.h>
#include<string.h>
int main()
{
int a=10;
switch(a)
{
case 1:
break;
case 'A':
break;
case 1+2:
break;
case "A"://expression not constant
break;
/*case 0.1:
break;//'const double' : illegal type for case expression
case -0.1:
break;//'const double' : illegal type for case expression
case 0.1+0.9:
break;
case 3/2:
break;//出现过了

*/
}
}


void

字面意思是空类型,void*则为空类型指针,可以只想任何类型的数据。void多用于对函数参数的限定和对函数返回的限定。

不能对 void*进行算法操作。如

void *pvoid;
pvoid++;
pvoid+=1;


标准认为,进行算法操作符的指针必须知道其指向数据类型大小的,即必须知道内存目的地址的确切值。void不能代表一个真实的变量,它的出现是一种抽象的需要。

return:

用来终止一个函数,并返回后表表达式的值。但return语句不可返回只想“栈内存”的“指针”(如局部变量),因为该内存在函数体结束时被自动销毁。

const:

const int Max[100];
int array[Max];


在vc里分别创建.c和.cpp文件,发现在.c文件中编译器会提示出错,而在.cpp文件中则顺利运行。证明在c语言中,const修饰的是只读属性的变量,而在C++里,扩展了属性。case语句后面不可以是const修饰的只读变量。编译器通常不为普通const只读变量分配存储空间,而是将他们保存在符号表红,使得它成为一个在编译期间的值,没有了存储与读内存的操作,使得效率高。const的定义从汇编的角度来看,只是给出了对应的 内存地址,而不是#define一样给出的是立即数。所以,const定义的只读变量在程序运行时只有一份拷贝(因为他是全局的额只读变量,存放在静态区),而#define定义的变量在内存中有若干份拷贝。#define宏是在预编译阶段进行替换,而const修饰的只读变量是在编译的时候确定其值,宏没有类型,而const修饰的只读变量有特定的类型。

#define M 3//宏常量
const int N = 5;//此时N并未被放入内存中
int i = N;//此时为N分配内存,以后不再分配
int I = M;//编译期间进行宏替换,分配内存
int j = N;//没有内存分配
int J=M;//再进行宏替换,又一次分配内存


volatile

1.编译器对访问该变量的的代码不再进行优化,从而可以提供对特殊地址的稳定访问。

比如要往某一地址送两条指令:

int   *ip   =...;   //设备地址

*ip   =   1;   //第一个指令

*ip   =   2;   //第二个指令


以上程序compiler可能做优化而成:

int *ip   =   ...;
*ip   =   2;
结果第一个指令丢失。如果用volatile, compiler就不允许做任何的优化,从而保证程序的原意。


volatile int *ip = …;

*ip = 1;

*ip = 2;

此时,即使你要compiler做优化,它也不会把两次付值语句间化为一。它只能做其它的优化。这对device driver程序员很有用。

2.表示用volatile定义的变量会在程序外被改变,每次都必须从内存中读取,而不能把他放在cache或寄存器中重复使用。volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改。

用volatile关键字声明的变量i每一次被访问时,执行部件都会从i相应的内存单元中取出i的值。没有用volatile关键字声明的变量i在被访问的时候可能直接从cpu的寄存器中取值(因为之前i被访问过,也就是说之前就从内存中取出i 的值保存到某个寄存器中),之所以直接从寄存器中取值,而不去内存中取值,是因为编译器优化代码的结果(访问cpu寄存器比访问ram快的多)。volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。

注意,在vc6中,一般调试模式没有进行代码优化,所以这个关键字的作用看不出来。下面通过插入汇编代码,测试有无volatile关键字,对程序最终代码的影响:

首先,用classwizard建一个win32 console工程,插入一voltest.cpp文件,输入下面的代码。

#include <stdio.h>

void main()

{

int i=10;

int a = i;

printf( "i= %d/n",a);

//下面汇编语句的作用就是改变内存中i的值,但是又不让编译器知道

__asm

{

mov dword ptr [ebp-4], 20h

}

int b = i;

printf("i= %d/n",b);

}


然后,在调试版本模式运行程序,输出结果如下:

i = 10

i = 32

然后,在release版本模式运行程序,输出结果如下:

i = 10

i = 10

输出的结果明显表明,release模式下,编译器对代码进行了优化,第二次没有输出正确的i值。

下面,我们把 i的声明加上volatile关键字,看看有什么变化:

“`

#include

}

volatile的本意是“易变的”

由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:

static int i=0;

int main(void)

{



while (1)

{

if (i) dosomething();

}

}

/* Interrupt service routine. */

void ISR_2(void)

{

i=1;

}

程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。

一般说来,volatile用在如下的几个地方:

1、中断服务程序中修改的供其它程序检测的变量需要加volatile;

2、多任务环境下各任务间共享的标志应该加volatile;

3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义;

另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过有关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。

含义

volatile总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优化,进一步可以使代码消除。但有时这些优化不是程序所需要的,这时可以用volatile关键字禁止做这些优化,volatile的字面含义是易变的,它有下面的作用:

1、不会在两个*作之间把volatile变量缓存在寄存器中。在多任务、中断、甚至setjmp环境下,变量可能被其他的程序改变,编译器自己无法知道,volatile就是告诉编译器这种情况。

2、不做常量合并、常量传播等优化,所以像下面的代码:

volatile int i = 1;

if (i > 0) …

if的条件不会当作无条件真。

3、对volatile变量的读写不会被优化掉。如果你对一个变量赋值但后面没用到,编译器常常可以省略那个赋值*作,然而对Memory Mapped IO的处理是不能这样优化的。

前面有人说volatile可以保证对内存操作的原子性,这种说法不大准确,其一,x86需要LOCK前缀才能在SMP下保证原子性,其二,RISC根本不能对内存直接运算,要保证原子性得用别的方法,如atomic_inc。

对于jiffies,它已经声明为volatile变量,我认为直接用jiffies++就可以了,没必要用那种复杂的形式,因为那样也不能保证原子性。

你可能不知道在Pentium及后续CPU中,下面两组指令:

inc jiffies

;;

mov jiffies, %eax

inc %eax

mov %eax, jiffies

作用相同,但一条指令反而不如三条指令快。

关键在于两个地方:

1、编译器的优化

在本线程内,当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致;当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致;当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致。

举一个不太准确的例子:

发薪资时,会计每次都把员工叫来登记他们的银行卡号,一次会计为了省事,没有即时登记,用了以前登记的银行卡号,刚好一个员工的银行卡丢了,已挂失该银行卡号,从而造成该员工领不到工资。

员工 --原始变量地址

银行卡号 --原始变量在寄存器的备份

2、在什么情况下会出现

1) 并行设备的硬件寄存器(如:状态寄存器)

2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

3) 多线程应用中被几个任务共享的变量

补充:volatile应该解释为“直接存取原始内存地址”比较合适,“易变的”这种解释简直有点误导人;

“易变”是因为外在因素引起的,象多线程,中断等,并不是因为用volatile修饰了的变量就是“易变”了,假如没有外因,即使用volatile定义,它也不会变化;

而用volatile定义之后,其实这个变量就不会因外因而变化了,可以放心使用了;大家看看前面那种解释(易变的)是不是在误导人。

volatile对应的变量可能在你的程序本身不知道的情况下发生改变,比如多线程的程序,共同访问的内存当中,多个程序都可以操纵这个变量,你自己的程序,是无法判定这个变量会发生变化;还比如,他和一个外部设备的某个状态对应,当外部设备发生操作的时候,通过驱动程序和中断事件,系统改变了这个变量的数值,而你的程序并不知道。

对于volatile类型的变量,系统每次用到他的时候都是直接从对应的内存当中提取,而不会利用cache当中的原有数值,以适应它的未知何时会发生的变化,系统对这种变量的处理不会做优化——显然也是因为它的数值随时都可能变化的情况。

典型的例子:

for ( int i=0; i<100000; i++);

这个语句用来测试空循环的速度的

但是编译器肯定要把它优化掉,根本就不执行

如果你写成:

for ( volatile int i=0; i<100000; i++);

它就会执行了

extern:

可以置于变量或者函数前,以标识变量或者函数的定义在别的文件中,编译器遇到时在其他模块中寻找其定义。

struct:

空结构体的大小是1.为什么呢?编译器认为任何一种数据类型都有其大小,用它来定义一个变量能够分配确定大小的空间。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: