LV01-11-C语言-关键字

本文主要是C语言——关键字总结相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

在 C 语言中,关键字一共有 32 个

一、存储类型声明

  • auto —— 声明自动变量,缺省时编译器一般默认为 auto
  • static —— 声明静态变量
  • register —— 声明寄存器变量,存取寻址访问以提高效率,注意是 尽可能,不是绝对。
  • extern —— 外部变量声明

1. static

static 本质就是延长变量或函数的生命周期,同时限制其作用域 。主要由以下作用

  • (1)在修饰变量的时候,static 修饰的静态局部变量只执行一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
  • (2)static修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是extern外部声明也不可以。
  • (3)static修饰一个函数,函数就被定义成为静态函数,这个函数的只能在本文件中调用,不能被其他文件调用。所以其他文件中可以定义相同名字的函数,不会发生冲突。
  • (4)static修饰的局部变量存放在全局数据区的静态变量区。初始化的时候自动初始化为0;

以下情况应该考虑使用 static:

(1)不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用static修饰

(2)考虑到数据安全性(当程想要使用全局变量的时候应该先考虑使用static)

static全局变量与普通的全局变量有什么区别?

存储方式上:static全局变量和普通全局变量都是静态存储方式;

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

static局部变量和普通局部变量有什么区别?

存储方式上:普通局部变量是动态存储方式,static局部变量是静态存储方式

作用域上:都是在块或者函数内部有效

static函数与普通函数有什么区别?

(1)用static修饰的函数,本限定在本源码文件中,不能被本源码文件以外的代码文件调用。而普通的函数,默认是extern的,也就是说,可以被其它代码文件调用该函数。

(2)static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝。

2. register

register 修饰表示相应的变量将被频繁使用,尽可能(注意是尽可能,不是绝对,因为一个CPU的寄存器就那么几个)的将这个变量保存在CPU内部寄存器中,而不是通过内存寻址来访问,这是为了提升程序的运行效率。

【使用限制】

(1)register变量必须是能被CPU所接受的类型,这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。但是,有些机器的寄存器也能存放浮点数。

(2)因为register变量可能不存放在内存中,所以不能用 “&” 来获取 register 变量的地址。

(3)只有局部自动变量和形式参数可以作为寄存器变量,其它(如全局变量)不行。在调用一个函数时占用一些寄存器以存放寄存器变量的值,函数调用结束后释放寄存器。此后,在调用另外一个函数时又可以利用这些寄存器来存放该函数的寄存器变量。

(4)局部静态变量不能定义为寄存器变量。不能写成:register static int a, b, c;

(5)由于寄存器的数量有限(不同的 CPU 寄存器数目不一),不能定义任意多个寄存器变量,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的 register 修饰符的数目和类型都依赖于运行程序的机器,而任何多余的 register 修饰符都将被编译程序所忽略。

3. extern

extern 用于函数或变量的外部声明。

对变量、函数而言,如果要在当前文件内引用外部变量或外部函数,就需要在使用前用 extern 声明该变量,或者在头文件中用extern声明该变量。

二、数据类型

  • 基本数据类型

void、char、short、int、long、float、double

  • 构造数据类型

struct、union、enum、typedef

1. void类型

  • 如果函数没有返回值,那么应声明为 void 类型,比如打印类型的函数。
  • void *,强制转换转来进行传参 如果函数的参数可以是任意类型指针,那么应声明其参数为 void *。
  • void 类型的数据默认占有一个字节空间(需要注意的是并没有这种说法,只是它在 C 语言中就是这样的)。

三、修饰类型

  • signed —— 声明有符号类型变量
  • unsigned —— 声明无符号类型变量
  • const —— 声明只读变量
  • volatile —— 说明变量在程序执行中可被隐含地改变

1. const

只要一个变量前用 const 来修饰,就意味着该变量里的数据只能被访问,而不能被修改,也就是意味着const“只读”(readonly)。它的规则就是:规则:const 离谁近,谁就不能被修改;

const修饰一个变量时,一定要给这个变量初始化,若不初始化,在后面也不能初始化。

【const的作用】

  • (1)可以用来定义常量修饰函数参数修饰函数返回值 ,且被const修饰的东西,都受到强制保护,可以预防其它代码无意识的进行修改,从而提高了程序的健壮性(是指系统对于规范要求以外的输入能够判断这个输入不符合规范要求,并能有合理的处理方式。
  • (2)使编译器保护那些不希望被修改的参数,防止无意代码的修改,减少bug;
  • (3)给读程序的人传递有用的信息,声明一个参数,只是为了告诉用户这个参数的应用目的;

【const的优点】

  • (1)编译器可以对const进行类型安全检查(所谓的类型安全检查,能将程序集间彼此隔离开来,这种隔离能确保程序集彼此间不会产生负面影响,提高程序的可读性);
  • (2)有些集成化的调试工具可以对const常量进行调试,使编译器对处理内容有了更多的了解,消除了一些隐患。

例如

1
2
3
4
void func(const int i)
{

}

编译器就会知道 i 是一个不允许被修改的常量。

  • (3)可以节省空间,避免不必要的内存分配,因为编译器通常不为 const 常量分配内存空间,而是将它保存在符号表中,这样就没有了存储于读内存的操作,使效率也得以提高;
  • (4)可以很方便的进行参数的修改和调整,同时避免意义模糊的数字出现。

【修饰指针】

  • 第一种
1
const int * a;  /* 意味着 a 是一个指向常整型数的指针(也就是,指针指向的整型数是不可修改的,但指针可以被修改)*/
  • 第二种
1
int * const a;  /* 意味着 a 是一个指向一个整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)*/
  • 第三种
1
const int const * a; /* 意味着 a 是一个指向常整型数的常指针(也就说说,指针指向的整型数是不可以修改的,同时指针也是不可修改的) */

记法:左数右指

当const出现在 * 号左边时,指针指向的数据不可以变,但指针可以。

当const出现在 * 号右边时,指针本身不可以改变,但指针指向的数据可以。

2. volatile

2.1使用说明

volatile是易变的,不稳定的; 这是一个编译器警告提示字,防止编译器优化用该修饰符修饰的变量。意思就是一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。

如果一个变量 a ,使用了 volatile 修饰,那么可以保证每次取 a 的值都不是从缓存中取,而是从 a 所真正对应的内存地址中取。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。这是因为访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部 RAM 的优化。

【注意】

(1)一个参数既可以是 const,也可以是 volatile,它可以同时由这两者进行修饰,例如只读的状态寄存器,它是 volatile 表示它可能被意想不到地改变,它还是 const 表示程序不应该试图去修改它。

(2)一个指针可以是 volatile ,当一个中服务子程序修改一个指向一个 buffer 的指针时,这个指针就可以是一个 volatile。

  • volatile用在如下的几个地方

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

(2)多任务共享资源,多任务环境下各任务间共享的标志应该加 volatile ;

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

2.2使用实例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define MACRO 0

void *thread_func(void *arg)
{
sleep(2);
*(int *)arg = 0;
}

int main(int argc, const char *argv[])
{
pthread_t tid;
#if MACRO == 1
volatile int a = 1;
#else
int a = 1;
#endif
pthread_create(&tid, NULL, thread_func, &a);

while (a);
printf("----------------------\n");
return 0;
}

我们使用下边的命令编译:

1
2
gcc main.c -lpthread       # 不进行优化
gcc main.c -lpthread -O1 # 进行优化,或者用 -O3

不进行优化的时候,MACRO 若等于 1 ,那么整个程序编译会有警告,但是可以正常运行完毕,若 MACRO 若等于 0,程序也可以正常运行完毕。

进行优化的时候,MACRO 若等于 1 ,那么整个程序编译会有警告,但是可以正常运行完毕,若 MACRO 若等于 0,程序就不会。结束了。不会打印出最后的一句话。

2.3使用实例2

如下程序,想要打印出传入参数的平方:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int func(volatile int *p)
{
return *p * *p;
}

int main(int argc, const char *argv[])
{
int a = 10;
printf("a^2 = %d\n", func(&a));
return 0;
}

但是这样其实是可能会发生问题的,因为 *p 指向一个 volatile 类型参数,它的值可能会被意想不到地改变,最后返回的用于相乘的两个数可能是不同的,正确的写法应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int func(volatile int *p)
{
int temp = *p
return temp * temp;
}

int main(int argc, const char *argv[])
{
int a = 10;
printf("a^2 = %d\n", func(&a));
return 0;
}

四、控制语句

  • if … else

  • switch … case、default

  • do、while、for

  • break、continue、goto

  • return

1. break

break语句的作用是跳出当前循环。如有嵌套的循环,则跳出最近的循环体。

2. continue

continue语句的作用是跳出一次循环,即忽略 continue 后面的其它语句,紧接着执行下一次循环。

3. goto

尽量少用 goto,但是在内核中除外,在 linux 中是内存页式管理,goto 跳转区域大于页表的大小,就可能会跳转到下一页,返回的时候就可能无法返回了。

4. return

return 用来终止一个函数并返回其后面跟着的值。它不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁。例如,

1
2
3
4
5
6
char * Func(void)
{
char str[30];
// ... ...
return str;
}

五、其它关键字

  • sizeof
  • inline

1. sizeof

这是一个函数,但是也被称之为关键字,可以用于获取数据类型、字符串、数组等的大小(在64位平台下,它的返回值是long unsigned int 类型,打印的时候 print 格式字符应该为 %ld )。在获取字符串的大小的时候它会计算字符串结束符 ‘\0’ 在内,例如

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(int argc, char *argv[])
{
char a[] = "hello";
int *p = NULL;
printf("sizeof(a)=%ld\n", sizeof(a));
printf("sizeof(int)=%ld\n", sizeof(int));
printf("sizeof(p)=%ld\n", sizeof(p));

return 0;
}

我们将会得到以下输出:

1
2
3
sizeof(a)=6
sizeof(int)=4
sizeof(p)=8

2. inline

这个其实挺奇怪的,都说 C语言关键字有32个,但是在内核中有些函数前边会有这个修饰符,它将函数指定为内联函数,这样可以解决一些频繁调用的函数大量消耗栈空间(栈内存)的问题。