嵌入式C语言基础知识查漏补缺-变量/数据类型/运算

目录

1.1 C语言的编译步骤:

1.2 system() 函数:

1.3 常量和变量

1.4 有符号数和无符号数:

1.4.1 数的范围

1.5 补码求原码:

1.6 打印输出格式:

1.7 字符型 char:

1.8 字符串常量

1.9  scanf() 和 getchar():

1.10 转义字符

1.11 限定符

1.12 printf附加格式:

1.13 运算符相关

1.14 类型转换

1.15 switch 和 if else的区别:

1.16 静态编译/动态编译&&静态链接库/动态链接库

1.17 VS中出现4996警告:

2. 嵌入式开发中的C语言

2.1 嵌入式C语言的编译过程

2.2 芯片内存和编程代码的映射关系:

2.3 变量类型的使用

2.4 嵌入式C语言中的位操作

STM32中的一些C语言知识点


1.1 C语言的编译步骤:

预处理:

gcc -E hello.c -o hello.i   //只进行预处理

宏定义展开、头文件展开、条件编译等,同时将代码中的注释去掉。这一步不检查语法错误。

生成预处理文件。

编译:

gcc -S hello.i -o hello.s     //只进行预处理和编译

将预处理后的文件( .i )生成汇编文件( .s)。

汇编:

gcc -c hello.s -o hello.o     //只进行预处理、编译和汇编

将汇编文件( .s)生成目标文件(二进制文件 .o)。

链接:

gcc hello.o -o hello   //指定输出的文件名为 hello

C语言写的程序需要依赖各种库,所以编译之后还需要把库链接到最终的可执行程序中去。

1.2 system() 函数:

作用:在程序中启动另一个程序

参数:待启动程序的路径名

如果这个程序系统可以找到,则不用加路径:

system("mspaint");   //启动画图板
system("notepad");   //启动记事本

如果环境变量找不到,需要加路径,windows路径以 / 或 \\ 表示:

system("C:/Users/SYH/Desktop/visual_studio/hello/hello.exe");   

1.3 常量和变量

常量:程序中不能改变的量。

  • 整型常量:1
  • 字符常量:'a'
  • 字符串常量:"hello"
  • 浮点型常量:1.23

变量:程序中运行可被改变的量,存在于内存中。

  • 定义:变量的定义即在内存中开辟空间;
  • 初始化:定义时赋值;
  • 声明:表示告诉编译器有这个变量。定义也是声明的一种。

单独的声明变量就是使用关键字extern,表明这个变量可能在其他的源文件中,但是这里不开辟空间。

extern int a;

变量的定义形式: 数据类型 + 变量名(变量名不能以数字开头)

int在内存中占4字节
short2字节
long在windows 4字节;在Linux,32位是占4字节,64位是占8字节
char1字节
float4字节,一般用来存小数
double8字节,一般用来存小数

1.4 有符号数和无符号数:

定义变量时没有写singned或unsigned时,默认是有符号的。

1.4.1 数的范围

有符号数和无符号数能够表示的数的个数相同,只是表示数的范围不一样。

char: 1Byte

signed char: 1 000 0000 ~ 1 111 1111 => -0 ~ -127 

                       0 000 0000 ~ 0 111 1111 => +0 ~ +127

C语言规定,将 -0 表示成 -128,所以signed char的范围是: -128 ~ +127;

unsigned char: 0000 0000 ~ 1111 1111 => 0 ~ 255 ( 0 ~ 2^8-1 )

short:2Byte

signed short:(-2^15 -> 2^15-1)

unsigned short:(0 -> 2^16-1)

int:4Byte

signed int:(-2^31 -> 2^31-1)

unsigned int:(0 -> 2^32-1)

1.5 补码求原码:

正数:原码、反码、补码一样;

负数的反码:符号位不变,其他位取反;

负数的补码:反码加 1 。

计算机中,数值一律用补码来表示和存储。

假设一个数-23,原码 = 1001 0111,反码 = 1110 1000,补码 = 1110 1001,怎么通过它的补码求原码呢?过程如下。

首先,求补码的反码:符号位不变,其他位取反,得出补码的反码 = 1001 0110;

然后,补码的反码求原码:反码+1,得出原码 = 1001 0111,即是 -23 的原码。

eg:

如果给一个变量赋的值超过上述范围,则会产生越界,溢出。如下:( char类型的范围是:-128 -> 127)

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

int main()
{
    char num = 129;
    printf("num = %d\n", num);

    system("pause");
    return 0;
}

运行结果是:

num = -127

解析:

num 是有符号数,它的二进制数最高位为1,计算机就会认为这是一个负数的补码。

所以先求一下 num 的原码:

① 1000 0001 的反码 = 1111 1110;

② 1111 1110 的原码 = 1111 1111;

1111 1111 在计算机中表示的是 -127,因此输出 num = -127。

注意,定义变量时,如果没有加 unsigned,默认变量的数据类型为 signed。

赋值时(输入),如果所赋的值是十进制,则给计算机的是原码。如果所赋的值是八进制或十六进制,则给计算机的直接是补码。

打印时(输出),十进制打印(%d)要的是原码;如果是八进制或十六进制,打印要的是补码。

针对上面方框中的红色字体进行如下例子的分析:

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

int main()
{
    int a = 056;
    
    printf("a = %o\n", a);
    printf("a = %x\n", a);
    printf("a = %d\n", a);

    system("pause");
    return 0;
}

输出为:

a = 56
a = 2e
a = 46

解析:

因为赋的值是 056,八进制,所以在计算机里存的就是 056 的补码(原码、反码、补码相等),为 0000 0000 0000 0000 0000 0000 0010 1110。

前两个输出要的是 %o和%x,要的是补码,所以值不变,还是056,只不过进制的形式变了;但是第三个输出要的是 %d,要的是原码,就要把补码转换成原码再进行输出,得出是46。

1.6 打印输出格式:

不管计算机里是怎么存储的,输出时看格式:

打印格式含义
%d输出一个有符号的10进制 int 类型,原码打印
%o输出8进制的 int 类型
%x输出16进制的 int 类型,字母以小写输出
%X输出16进制的 int 类型,字母以大写输出
%u输出一个无符号的10进制 int 类型
%hd输出一个有符号的 short 类型
%ld输出一个有符号的 long 类型

eg:

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

int main()
{
    unsigned int a = 0xffffffff;

    printf("有符号打印:a = %d\n", a);
    printf("无符号打印:a = %u\n", a);

    system("pause");
    return 0;
}

输出为:

有符号打印:a = -1
无符号打印:a = 4294967295

解析:

a 是无符号整型变量,以十六进制的方式赋值,所以给计算机的是补码形式。

所以,

a 的原码=反码=补码= ffff ffff;然而第一位是1,计算机误认为是符号位,所以,

a 的补码的反码:1000 0000 0000 0000 0000 0000 0000 0000; (符号位不变,其余位取反)

上面数字的原码:1000 0000 0000 0000 0000 0000 0000 0001;( 反码+1 )

所以在使用%d打印时,要的是原码、有符号数,即 -1。

但是在使用%u打印时,把其看成是一个无符号数,即 0xffff ffff。 

1.7 字符型 char:

  • 字符型变量用于存储一个单一字符,在C语言中用 char 表示。
  • 在给字符型变量赋值时,用 ' ' 把字符括起来:' a '。
  • 因为字符所对应的最大ASCII值是127,所以用char类型就可以存得所有字符。

字符变量实际上并不是把该字符本身放到变量的内存单元中,而是将该字符对应的ASCII编码放到变量的存储单元中。即,char 的本质就是 1 字节大小的整型。

求某个字符的ASCII值:

printf("%d\n", 'a');  //打印某个字符的ASCII值

打印某个ASCII值对应的字符:

char b = 97;
printf("%c\n", b);  //打印某个ASCII值对应的字符

记住, %c 就是打印一个字符。

也可以用 putchar() 打印一个字符:

putchar(a);  //putchar只有一个参数,就是要输出的char

大写转小写:

char c = 'A';  // 'A' = 65,'a' = 97
c += 32;
printf("%c\n", c); 

也可以是
printf("%c\n", 'A'+' ');  //空字符的ASCII值是32

字符 '8' 转成数字 8:

char d = '8';
int e = d - '0';
printf("%d\n", e);
结果输出是 8 

 另:

char f = 'a';
printf("%d\n", sizeof(f));
printf("%d\n", sizeof('a')); 

输出为:

1
4   // 'a'是一个常量,可以看做'a' = 97

1.8 字符串常量

  • 字符串常量是由双引号括起来的字符序列。
  • 字符串是内存中一段连续的char空间,以 '\0' 结尾。
  • 每个字符串的结尾,编译器会自动添加一个结束标志位 '\0' 。
  • 打印字符串用 %s:printf("%s", "abc"); 。

另外有一点需注意:

数字0内存中存的是0
'0'内存中存的是48
'\0'内存中存的是0

所以可以说,'\0'与0是等价的。

1.9  scanf() 和 getchar():

 读取多个字符的问题:

int main()
{
    char a=0, b=0;
    scanf("%c",&a);
    printf("a=%c\n",a);
    scanf("%c",&b);
    printf("b=%c\n",b);
}

如果按照上面的这样写,第二个字符不会输出来(其实是输出了一个 '\n'),需要改成下面这样:

int main()
{
    char a=0, b=0;
    scanf("%c",&a);
    printf("a=%c\n",a);
    scanf("%c",&a);    //这一行相当于把 '\n' 给吃掉了
    scanf("%c",&b);
    printf("b=%c\n",b);
}

scanf和getchar:都是从键盘读取一个字符。

char ch = 0;

scanf("%c", &ch);    

ch = getchar();       //调用getchar函数读取一个字符赋值给 ch。

int main()
{
    char a=0;
    a = getchar();
    printf("a=%c\n",a);
    a = getchar();
    printf("a=%c\n",a);

    system("pause");
    return 0;
}

输出为:

a         //输入 a
a = a
a = 
请按任意键继续. . .

上述程序中,第一个 getchar() 获得了我们输入的 a,并且输出了 a;第二个 getchar() 获得了一个 “\n”,所以第二个 printf() 直接打印了一个“回车”,即 “\n” 字符;若想打印两个,做如下修改:

int main()
{
    char a=0;
    a = getchar();
    printf("a=%c\n",a);
    getchar();
    a = getchar();
    printf("a=%c\n",a);

    system("pause");
    return 0;
}

输出为:

1        //第一次输入1
a = 1
2        //第二次输入2
a = 2 
请按任意键继续. . .

解析:

其实我们输入的是 “a+\n”,增加了一个getchar() 把 “\n” 给吃掉了。和scanf读取多个字符是一样的。

1.10 转义字符

\n换行
\t跳到下一个tab位置
\r回车,将当前位置移到本行开头

1.11 限定符

extern

声明变量,告诉编译器有这个东西,但是不开辟空间。

可以是别的 .c 文件下定义的变量,拿到这里来使用。

eg:extern char abc_def

const修饰的内容不可改变
volatile防止编译器优化
Register建议将变量定义在寄存器中

1.12 printf附加格式:

字符含义
字母 l附加在d,u,o等前面,表示长整数
-左对齐
数字 0将输出的前面补上0直到占满指定列宽为止,不可与 - 搭配使用
m.n

m代表数据最小宽度,即对应的输出项在输出设备上所占的字节数;

n指精度,用于说明输出的实型数的小数位数。

int main()
{
    int a = 10;
    printf("a = %6d\n",a);
    printf("a = %-6d\n",a);
    printf("a = %06d\n",a);
    printf("a = %-06d\n",a);            // - 和 0 不能同时使用

    double b = 12.3;
    printf("b = \'%-10.3lf\'\n", b);    // 反斜杠加单引号,输出一个单引号

    system("pause");
    return 0;
}

输出为:

a =     10
a = 10
a = 000010
a = 10
b = '12.300    '

1.13 运算符相关

① 逗号运算符:

是二元运算符。

逗号运算符确保操作数被顺序地处理:先计算左边的操作数,再计算右边的操作数。右操作数的类型和值作为整个表达式的结果。eg:

int main()
{
    int a = 1, b = 2;
    int c = (a++, b++, a+b, 10, 200, a+b);
}

输出为:c = 5

② 比较运算符:

C语言中,0为假,1为真。eg:

int main()
{
    printf("%d\n", 3 == 4);
    printf("%d\n", 3 != 4);
    printf("%d\n", 3 > 4);
    printf("%d\n", 3 < 4);
    printf("%d\n", 3 <= 4);
}

输出为:

0
1
0
1
1

③ 逻辑运算符:

逻辑与:A && B:必须A和B都为真才为真;如果A为假,则表达式B不执行。

逻辑非:A || B:A或B有一个为真即可;如果A为真,则表达式B不执行。

1.14 类型转换

隐式转换:

int a = 3;
double b = a;   //把a转换成double型

强制转换:  (需要转的类型)原来的数据类型

int a = 3;
printf("%d\n", (int)3.14);
printf("%lf\n", (double)a);

输出为:

3
3.000000

强制转换的原则:占用内存字节数少(值域小)的类型,向占用内存字节数多(值域大)的类型转换,以保证精度不降低。eg:

int a;
char b;
可以使用 a = (int)b;
而不是 b = (char)a;

又eg:

    int a = 0;
    float b = 0;
    b = 3.6f;

    a = b;
    a = (int)b;     //b为实型,a为整型,直接赋值会有警告,值不对
    printf("a = %d, b = %f\n", a,b);   //使用强制类型转换

当不使用强制类型转换直接输出时,出现:warning C4244: “=”: 从“float”转换到“int”,可能丢失数据。所以采用第二种方式,即 a = (int)b。

输出为:

a = 3, b = 3.600000

1.15 switch 和 if else的区别:

switch() 语句中的判断条件只能是整数;而 if() 语句中的判断条件可以是任意的。

1.16 静态编译/动态编译&&静态链接库/动态链接库

https://www.cnblogs.com/lisuyun/p/3953589.html

静态编译:

  1. 编译器在编译可执行文件时,把需要用到的对应动态链接库(.so 或 .lib)中的部分提取出来,链接到可执行文件中,使可执行文件在运行时不需要依赖于动态链接库。
  2. 静态 lib 将导出声明和实现都放在lib中。编译后所有代码都嵌入到宿主程序。
  3. 静态编译可以理解为加载静态链接库。
  4. 静态链接库中不能包含其他库。

动态编译:

  1. 动态编译的可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库中的命令。所以它缩小了执行文件本身的体积,加快了编译速度。缺点是哪怕是很简单的程序,只用到了链接库中的一两条命令,也需要附带一个庞大的链接库。
  2. 动态 lib 相当于一个h文件,是对实现部分(.dll文件)的导出部分的声明。编译后只是将导出声明部分编译到宿主程序中,运行时需要相应的dll文件支持。
  3. 动态编译可以理解为加载动态链接库。
  4. 动态链接库能包含其他库。

静态链接库:

  • 创建一个静态链接库,会生成 x.lib 文件;
  • 想要调用静态链接库里面的内容需要 x.lib 文件和 x.h 文件;
  • 库中内容会直接编译到 x.exe 文件中;
  • 可执行程序使用静态库编译成 x.exe 后,x.exe 的运行就不再需要静态链接库了,可以独立运行。

动态链接库:

创建一个静态链接库,会生成 x.dll,x.lib 文件;

动态链接库有两种加载方式:

① 静态加载:在编译的时候就载入动态链接库。

  • 可执行程序静态加载动态链接库需要 x.dll,x.lib,x.h 三个文件;
  • 可执行程序的头文件加入“ #include "x.h"   #pragma comment(lib,"x.lib") ”,编译时还要附加库目录,防止程序编译时无法找到 x.dll。

② 动态加载:

  • 只需要 x.dll 文件;在程序执行需要该动态链接库的地方加载 x.dll,然后获取需要的 x.dll 库里面的函数或数据。
  • 可执行程序调用了动态链接库,运行时不能缺少动态链接库。

1.17 VS中出现4996警告:

只需要在文件的最前边加上一句话:

#define _CRT_SECURE_NO_WARNINGS  //这个宏定义最好放到.c文件的第一行
#pragma warning(disable:4996)    //或者用这个

2. 嵌入式开发中的C语言

2.1 嵌入式C语言的编译过程

整体过程介绍如下:

 首先,我们写的.c文件经过编译器Compiler的编译,绝大多数编译器会选择以汇编作为中间步骤,生成汇编代码。

然后与我们自己写的汇编代码合在一起交给汇编器Assembler,并根据它和机器码指令的映射关系,将汇编代码编程为0和1的指令序列 ,即指令段,也称作程序段。

这些程序段构成了目标文件,然后调用外边的库文件。这些程序段和孤立的库函数通过.ld文件的链接形成一个可以互相调用的整体。

.id文件是用链接脚本语言写成的链接文件(告诉我们哪个地址段是flash可以放程序、哪个地址段是RAM可以放变量、哪个地址段是RAM的底部可以是堆栈),然后通过链接器linker,把代码按照装配图的指示放到对应的空间里组合起来,最终形成一个具有相互调用关系的可执行文件。

执行文件:在windows平台上,就是.exe,.com文件;在Linux平台下,它可能是个脚本,可能是个.elf的二进制文件;在嵌入式行业里,它可能是一个binary文件,或者是一个ascrecord(摩托罗拉时代传递下来的ASCII码脚本文件),或者是Inter体系架构里的hex文件。这些文件本质上是一样的,都是记录了详细地址与函数代码段之间地址相互关系的一个完整的映射关系。

扩展:

在最终的链接环节生成可执行文件的同时,一般会生成一个.map文件,也就是一个映射文件。它记录了我们在链接的时候声明的每一个变量、每一个函数体所使用的空间,所以可以把它称为是个装配总图。

2.2 芯片内存和编程代码的映射关系:

在进行开发时会涉及到一系列的文件,最终所有的编程结果都会烧写到芯片里。他们之间是怎么形成一种逻辑上的联系呢?eg:

① C语言里的头文件,用来声明寄存器;

② flash空间里,编程的函数代码会放到flash保证CPU上电执行;

③ RAM里,编程过程中声明的变量、数据结构、函数的调用等,这些会去到RAM里,从顶部使用堆,从底部使用栈。

这么一个映射关系,使我们在开发环境中遇到的头文件、宏定义、代码段、数据段等,全部放在了芯片里。这就是.ld文件的作用。看下面例子:

它分为了几个段:

  1. 堆栈指针寄存器stack point的地址放在了内存的最底部,这个地址值赋给了一个下划线起头的变量名,这个值会放在中断向量表的第一项上,也就是堆栈指针寄存器的初始值那里。这样就使得堆栈指针在上电复位的时候指向了内存的底端,堆栈就位。
  2. 定义了堆栈的大小。注意:它只能在编译的时候进行堆栈的检查,而在运行的时候,如果堆栈溢出,这两句话起不到保护作用。
  3. 指定一个地址段,并起了一个名字。在芯片手册的内存映射表里,已经把这些地址段定义好了,.ld文件只是将这些地址段用脚本的形式写出来。
  4. 最后一段,是编译器的规定动作。比如:.text段是编译出来的可执行代码段,代码放在flash里保证掉电不丢失;堆栈放在RAM里。

2.3 变量类型的使用

        当我们做嵌入式开发的时候,语言只是一个工具,你必须清楚你写的每一句话在时序上是怎么样的,在电路上是如何工作的。

  • ARM Cortex M系列的CPU是32bit的,所以大多数时候它的int类型对应的是32位的四字节存储空间。在很多时候,我们使用的变量只是用来给if else做一个判断,我们所需要用到的值只是1或0这样一个1bit的信息。如果使用习惯性的int类型,就会导致因为1bit的信息用掉32bit四个字节的空间,是很不划算的。所以,实际上在进行嵌入式系统的C语言编程时,应该尽量使用满足功能要求的最小的变量类型
  • 关于浮点运算,不带有浮点运算功能的CPU(比如ARM Cortex M0、M0+、M3),与带有浮点运算功能的CPU(M4)是不一样的。浮点运算功能需要一个庞大的C语言的标准函数库来支撑(比如说你要运行一个几点几乘以几点几这句话,会需要程序include进来一个非常大的浮点运算的支持函数库,使整个代码编译出来的量比原来大很多,占掉更多的存储空间。)
  • 数据类型不仅取决于CPU,也取决于我们所使用的编译器(比如说Codewarrior、gcc,可以通过编译器的一些参数进行设置,来指定int型对应一个字节或者两个字节)。

例子:一些开放源代码的写法。

  • 避免直接使用基本类型来声明变量(char、int、short、long)。在程序的最开头或者头文件里,使用typedef将一种变量类型声明为一种替代类型。

eg:

        int型的变量在8bit的MCU或CPU上,一般编译器会把它处理成16bit占2个字节,而在32bit的系统上默认它就是32bit。反过来,short型变量在32bit的系统上才是16个bit。这样一种不一样的对应关系,怎么在代码上做到统一呢?

答:

        先把char、int等基本类型的变量typedef成u32_t或者是int32_t这样的变量类型。用一种替代变量类型直接说明这个变量是8bit还是16bit。日后声明变量时只需使用这个替代类型来声明变量(比如说用uint32_t声明变量,就知道该变量是32bit4个字节的)。

2.4 嵌入式C语言中的位操作

下图中,第一句表示按位取反;第二句表示判断GPIOD_PDOR这个寄存器的值“与”上0x80是否等于0,即判断第八个引脚(也就是最高的那个引脚)输出是否为1。 

eg:

将一个变量的bit7也就是第8个bit置为1。

uchar_var = 0x34;          //34是十进制形式,先转化为二进制,再做位运算。
// set bit7
uchar_var |= 0b10000000;

上述0b10000000称作mask(掩码)。0b是二进制的C语言写法。0与任何数进行或运算,不改变它的值。

如果把上述掩码拎出来,define成一个宏,如下所示。这样代码就变得更加可读,如例子所示就会很清楚地知道是哪个位被置位了。

反过来说,如果要把寄存器中的某一个位清零怎么办?答:把上边的代码按位取反,再与变量自身“与”

置位用“或”,清零用“与”。

左移、右移:

0x34往右移4位,4没有了,只剩下0x03;往左移4位,3被移出去,4移到高位,后边补0,变成0x40。

左移和右移有什么用呢?

比如说,要把某个寄存器当中某几个比特的值取出来,赋给一个新的变量。如下,最简单的取低四位和高四位。取高四位时,让这个变量与高四位的掩码1111 0000来或,这样它的4就没了,但这个时候剩下的是0x30,需要再右移四位,就把3作为一个独立的变量取到了一个新的变量里。

另外还有清0、左移、右移、把一个变量里的某几个bit拿出来做判断等位操作,省略。

C语言如何实现对硬件的控制?

C语言的语句是怎么访问寄存器的呢?

我们来看一下寄存器的本质:

寄存器在嵌入式系统中,是一个映射在一个统一地址映射上,不同地址的电路实现,看起来像一个存储单元,按地址可以访问它,每个寄存器8bit或32bit。实际上,在C语言视角,它访问的是存储,存储的0或1背后连接的是电路,用电压值的高或低来发挥电路控制作用。

例如:一个叫GPIOD_PDOR的寄存器:它映射在一个地址上,占了4个字节共32bit,每一个bit对应一个芯片的物理引脚。这个bit为1,在设为输出的时候,输出的值就是高电平3.3V;这个bit为0,引脚对应的电压就是0V。(根据芯片手册的不同,0、1代表的含义有所不同。)

所以我们编程的本质,就是给这个寄存器赋值,就是让这些比特为0或1。那么这件事情在编程中怎么做呢?

C语言是如何知道我们访问的变量名(如GDIOD_PDOR)是一个寄存器的?

这里使用到了一个C语言的关键字:Volatile,它的基本含义是“易失的、可变的”。在C语言的语法里,这个词的定义是:maybe change outside the normal program flow(在正常的程序流程之外,可能自己发生改变的一种变量)。

STM32中的一些C语言知识点

#和##运算符

1. #

我们平时使用带参宏时,字符串中的宏参数是没有被替换的。如下:

#include <stdio.h>

#define ADD(A,B) printf("A + B = %d\n", ((A)+(B)));

int main(void)
{
    ADD(5,20);
    return 0;
}

输出为:

A + B = 25

并没有得到我们期望中的结果(5+20=25),该怎么办呢?如下:

C语言允许在字符串中包含宏参数,在类函数宏(带参宏)中,#号作为一个预处理运算符,可以把记号转换成字符串。

#include <stdio.h>

#define ADD(A,B) printf(#A " + " #B " = %d\n", ((A)+(B)));

int main(void)
{
    ADD(5,20);

    return 0;
}

输出为:

5 + 20 = 25

2. ##运算符

可以把两个记号组合成一个记号。如下,

#include <stdio.h>

#define XNAME(n) x ## n
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n);

int main(void)
{
    int XNAME(1) = 14;     //即int x1 = 14;
    int x2 = 30;     
    PRINT_XN(1);           //即printf("x3 = %d\n", x3);
    PRINT_XN(2);

    return 0;
}

输出为,

x1 = 14
x2 = 03

看着没啥用,但有时候可以提高封装性及程序的可读性。

3. #pragma message

用于在预处理过程中输出一些有用的提示信息。

#define TEST

#ifdef TEST
#pragma message("hello world!!!")
#endif

输出为,

在程序运行前它会输出一条:
#pragma message("hello world!!!")

4. #if, #elif, #else, #endif, #ifdef, #ifndef

(1) #if, #elif, #else, #endif

如下。如果整型常量表达式1为真,则执行程序段1,以此类推。#endif是#if结束的标志。

#if 整型常量表达式1
    程序段1
#elif 整型常量表达式2
    程序段2
#else
    程序段3
#endif

(2)#ifdef, #ifndef

#ifdef的作用是判断某个宏是否定义。如果已经被定义,则对程序段1进行编译,否则编译程序段2,#endif是#if的结束标志。如下,

#ifdef 宏名
    程序段1
#else
    程序段2
#endif

#ifndef的作用是判断某个宏是否没被定义。

5. static和extern

(1)static

在函数内用于修饰变量、用于修饰函数、用于修饰本.c文件中的全局变量。

后两个容易理解,用于修饰函数与全局变量表明变量和函数在本模块内使用。

用于修饰变量的例子如下,

#include <stdio.h>

void test(void)
{
    int normal_var = 0;
    static int static_var = 0;
    printf("normal_var:%d  static_var:%d\n", normal_var, static_var);
    normal_var++;
    static_var++;
}

int main(void)
{
    int i;     
    for (i = 0; i < 3; i++){
    test();
    }

    return 0;
}

输出为,

normal_var:0  static_var:0
normal_var:0  static_var:1
normal_var:0  static_var:2

可以看出,函数每次被调用,普通局部变量都是重新分配,而static修饰的变量保持上次调用的值不变,即只被初始化一次。

(2)extern

用于声明多个模块共享的全局变量、声明外部函数。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值