C语言常见关键字及使用

static

static作用有三个:

1.修饰全局变量

static 全局变量

  • 当一个进程的全局变量被声明为static之后,它的中文名叫静态全局变量。
  • 静态全局变量和其他的全局变量的存储地点并没有区别,都是在.data段(已初始化)或者.bss段(未初始化)内,但是它只在定义它的源文件内有效,其他源文件无法访问它。

结果展示如下:
1.c

#include <stdio.h>
  static int num = 10;

2.c

#include <stdio.h>
  extern int num;
  int main(int argc, char *argv[])
  {
     printf ("num:%d\n",num);
     return 0;
   }

在这里插入图片描述
把static删除,结果如下:
在这里插入图片描述

2.修饰局部变量

static 局部变量

普通的局部变量在栈空间上分配,这个局部变量所在的函数被多次调用时,每次调用这个局部变量在栈上的位置都不一定相同。

static局部变量中文名叫静态局部变量。与普通局部变量的区别:
1)位置:静态局部变量被编译器放在全局存储区.data(注意:不在.bss段内,原因见3)),所以它虽然是局部的,但是在程序的整个生命周期中存在。
2)访问权限:静态局部变量只能被其作用域内的变量或函数访问。也就是说虽然它会在程序的整个生命周期中存在,由于它是static的,它不能被其他的函数和源文件访问。
3)值:静态局部变量如果没有被用户初始化,则会被编译器自动赋值为0,以后每次调用静态局部变量的时候都用上次调用后的值。这个比较好理解,每次函数调用静态局部变量的时候都修改它然后离开,下次读的时候从全局存储区读出的静态局部变量就是上次修改后的值。

这两者的区别在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。
而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效,
在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。

#include <stdio.h>

void PrintTest (void);

int main(int argc, char *argv[])
{
	int tmp;
   
	for (tmp = 0; tmp < 5; tmp++) {
		
		int tmp1 = 10;

		printf ("\n");

    PrintTest();
	}
  
  printf ("static  num : %d     static num address : %p \n",StaticNum,&StaticNum);

	return 0;
}


void PrintTest (void)
{
	static int StaticNum = 0;
	int Num = 0;
 
	printf ("static  num : %d     static num address : %p \n",StaticNum,&StaticNum);
  printf ("        num : %d\n",Num);

  StaticNum++;
	Num++;
}

在这里插入图片描述

3.修饰函数

static 函数

static函数与普通函数作用域不同,仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static修饰的函数),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件.

相信大家还记得C++面向对象编程中的private函数,私有函数只有该类的成员变量或成员函数可以访问。在C语言中,也有“private函数”,它就是接下来要说的static函数,完成面向对象编程中private函数的功能。

简单测试一下,在main函数里面调用另一个文件里面用static修饰的函数
main.c

#include "test.h"

int main(int argc, char *argv[])
{
  MyPrint ();
  return 0;
}

test.h

#ifndef  TEST_
#define  TEST_

#include <stdio.h>

static void MyPrint (void);

#endif

function.c

#include "test.h"

static void MyPrint (void)
{
  printf ("print from function !\n");
}

编译就出现了错误,结果如下:
在这里插入图片描述

extern

extern 变量

1.修饰变量

只能用于扩展没有被static关键字修饰的全局变量。
在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。
默认情况下全局变量只能在定义它的文件中使用(从定义该全局变量开始到所在文件的文件尾),但如果在另一个文件中将这个变量声明为外部变量,那么这个变量的作用域将被扩展到另外一个文件中。也可以在定义全局变量之前声明该变量,从而在文件中可以在定义该全局变量前使用该全局变量。

main.c

#include "func.h"
int num = 20;

int main (void)
{
  Func();
  return 0;
}

func.h

#ifndef  FUNC_
#define  FUNC_

#include <stdio.h>

void Func (void);

#endif

func.c

#include "func.h"

void Func (void)
{
  extern int num;
  int i;
  printf ("num : %d\n",num);
}

结果:
在这里插入图片描述

2.修饰函数

extern 函数

在定义函数时如果在函数首部的最左端冠以关键字extern,则表示此函数是外部函数,可供其他文件调用。C语言规定,如果在定义函数时省略extern,则隐含为外部函数。
extern修饰函数声明。从本质上来讲,变量和函数没有区别。函数名是指向函数二进制块开头处的指针。使用extern修饰函数,这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。在大型C程序编译过程中,这种差异是非常明显的。

3.在c++中使用

extern修饰符可用于指示C或者C++函数的调用规范。

比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。

const

const其实就是constant的简写
修饰变量,限定这个变量为只读,意思是这个变量不能更改

1.修饰普通变量

const 普通变量

#include <stdio.h>

int main (void)
{
  const int num = 5;

  num = 9;

  return 0;
}
zl@zl-VirtualBox:~/workspace/4$ gcc 3.c
3.c: In function ‘main’:
3.c:7:3: error: assignment of read-only variable ‘num’
   num = 9; 
   ^

如上,在用const修饰普通变量后,后面再修改这个变量 编译器就会报错

其实const就是告诉编译器这个变量被const修饰,我们可以看下有无const修饰的汇编代码的差异。发现汇编没有差别,这里就不贴代码了。

我们可以绕过编译器,用指针去修改这个变量,如下:

#include <stdio.h>

int main (void)
{
  const int num = 5;

  int *p = &num;
  *p = 9;

  printf ("num : %d\n",num);

  return 0;
}
zl@zl-VirtualBox:~/workspace/4$ gcc 3.c 
3.c: In function ‘main’:
3.c:7:12: warning: initialization discards ‘const’ qualifier from pointer target type [enabled by default]
   int *p = &num;
            ^
zl@zl-VirtualBox:~/workspace/4$ ./a.out 
num : 9

这里使用指针修改,虽然编译器报告了warning,但是我们仍然可以看到被const修饰的num的值已经改变。

2.修饰数组

const 数组

数组元素不允许修改

const int arry[] = {1,2,3,4,5};

3.修饰指针

主要有三种情况:

3.1 常量指针

const放在 * 左侧位置,叫做 常量指针,表示该指针指向的对象是只读的

int a = 10;
int b = 20;
const int *p = &a;
p = &b;
int a = 10;
int b = 20;
int const *p = &a;
p = &b;

3.2 指针常量

const放在*的右侧,叫做 指针常量,限定了指针本身是只读的,即不可变的。

int a = 10;
int b = 20;
int * const p = &a;
*p = 50;
p = &b; //编译错误,这个时候指针本身是只读的

3.3常量指针常量

指针不可变,指针指向的内容也不可变

int a = 10;
int b = 20;
const int * const p = &a;
*p = 50;  //编译错误
p = &b;   //编译错误

4. 修饰函数形参

我们在ubuntu下man strcpy这个函数

char *strcpy(char *dest, const char *src);

上面用了const,这里的const表示传入的指针变量仅用于访问数据,这里理解为字符串。

5. 修饰全局变量

对全局变量增加const修饰,就可以避免全局变量被更改。

const int num  = 20;
extern const int num;

volatile

1.编译器优化

内存访问速度远不及CPU处理速度,为提高机器整体性能。

  1. 硬件方面:入硬件高速缓存Cache,加速对内存的访问。另外在现代CPU中指令的执行并不一定严格按照顺序执行,没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度。
  2. 软件方面:一种是在编写代码时由程序员优化,另一种是由编译器进行优化。
    编译器优化常用的方法有:
  3. 将内存变量缓存到寄存器。由于访问寄存器要比访问内存单元快的多,编译器在存取变量时,为提高存取速度,编译器优化有时会先把变量读取到一个寄存器中;以后再取变量值时就直接从寄存器中取值。但在很多情况下会读取到脏数据,严重影响程序的运行效果。
  4. 调整指令顺序充分利用CPU指令流水线,常见的是重新排序读写指令。

2.volatile详解

volatile字面意思是“易变的”,在C里面,我们可以理解为“直接存取原始内存地址”
提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)

2.1用处

  1. 防止编译器优化普通变量
#include <stdio.h>

int main (void)
{
  int i;
 // volatile int num;
  int num;

  for (i = 0; i < 1000000; i++) {
    num = i;
  }

  return 0;
}


zl@zl-VirtualBox:~/workspace/4$ time ./a.out 
real	0m0.005s
user	0m0.005s
sys	0m0.000s
#include <stdio.h>

int main (void)
{
  int i;
  volatile int num;
  //int num;

  for (i = 0; i < 1000000; i++) {
    num = i;
  }

  return 0;
}


zl@zl-VirtualBox:~/workspace/4$ time ./a.out 
real	0m0.007s
user	0m0.007s
sys	0m0.000s

  1. 中断服务程序操作其他程序的变量。
    变量在触发某中断程序中修改,而编译器判断主函数里面没有修改该变量,因此可能只执行一次从内存到某寄存器的读操作,而后每次只会从该寄存器中读取变量副本,使得中断程序的操作被短路。
  2. 多任务环境下各任务间共享的标志。
    在本次线程内, 当读取一个变量时,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;当内存变量或寄存器变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致 。
  3. 存储器映射的硬件寄存器
    操作某些特定的寄存器时,可能要对同一个寄存器进行反复操作,如果不用volatile,就有可能存在只有最后一次操作有效,前面几次操作就被忽略。这样就会出现错误,参考访问EC space。

restrict

restrict是c99标准引入的,它只可以用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式.即它告诉编译器,所有修改该指针所指向内存中内容的操作都必须通过该指针来修改,而不能通过其它途径(其它变量或指针)来修改;这样做的好处是,能帮助编译器进行更好的优化代码,生成更有效率的汇编代码.如 int *restrict ptr, ptr 指向的内存单元只能被 ptr问到,任何同样指向这个内存单元的其他指针都是未定义的,直白点就是无效指针。restrict 的出现是因为 C 语言本身固有的缺陷,C 程序员应当主动地规避这个缺陷,而编译器也会很配合地优化你的代码.

register修饰的变量必须是CPU所能接受的类型,就是该变量必须是单个值且小于等于整型长度(想想汇编就明白了)
由于该变量不存在内存,所以不能用&取地址

关键字restrict通过允许编译器优化某几种代码增强了计算支持。它只可用于指针,并表明指针是访问一个数据对象的惟一且初始的方式。为了清楚这样做为何有用,我们需要看一些例子。考虑下面的代码:

int ar[10];
int * restrict restar = (int *) malloc(10 * sizeof(int));
int * par = ar;

这里,指针restar是访问由malloc()分配的内存的惟一且初始的方式。因此,它可以由关键字restrict限定。然而,par指针既不是初始的,也不是访问数组ar中数据的惟一方式,因此不可以把它限定为restrict。
现在考虑下面这个更复杂的例子,其中n是一个int:

for(n=0; n < 10; n++)
{
par[n] += 5;
restart[n] += 5;
ar[n] *= 2;
par[n] += 3;
restar[n] += 3;
}

知道了restar是访问它所指向数据块的惟一初始方式,编译器就可以用具有同样效果的一条语句来代替包含restar的两个语句:

restar[n] += 8;

然而,将两个包含par的语句精简为一个语句将导致计算错误:

par[n] += 8;

出现错误结果的原因是循环在par两次访问同一个数据之间,使用ar改变了该数据的值。
没有关键字restrict,编译器将不得不设想比较糟的那种情形,也就是两次使用指针之间,其他标识符可能改变了数据的值。使用restrict关键字之后,编译器可以放心地寻找计算的捷径。
可以将关键字restrict作为指针型函数参量的限定词使用。这意味着编译器可以假定在函数体内没有其他标识符修改指针指向的数据,因而可以试着优化代码,反之则不然。例如,C库中有两个函数可以从一个位置把数据复制到另一个位置。在C99标准下,他们的原型如下:

void * memcpy(void * restrict dst, const void * restrict src, size_t n);
void * memmove(void * dst, const void * src, size_t n);

两个函数都从位置src把n个字节复制到dst中。函数memcpy()要求两个位置之间不重叠,但memmove()没有这个要求。把dst和src声明为restrict意味着每个指针都是相应数据的惟一访问方式,因此他们不能访问同一数据块。这满足了不能有重叠的要求。函数memmove()允许重叠,它不得不在复制数据时更加小心,以防在使用数据前就覆盖了数据。
关键字restrict有两个读者。一个是编译器,它告诉编译器可以自由地做一些有关优化的假定。另一个读者是用户,它告诉用户仅使用满足restrict要求的参数。一般,编译器无法检查您是否遵循了这一限制,如果您蔑视它也就是在让自己冒险。

以上参考《C Primer Plus》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值