数据类型

变量

值可以改变的量就叫变量。一个变量包括如下几部分:

  1. 名字
  2. 类型
  3. 地址(存储空间)

名字用于在程序中使用变量,类型用于解释变量,地址用于存储变量。

要创建变量,我们通过 类型 变量名; 的形式来 定义 一个变量。比如:

char a;
int b;
float c;
double d; 

Note:声明 Declaration定义 Definition 是编程中常混淆的两个概率。声明仅仅是说这个名字被使用了,但不一定有分配内存空间(比如形参);而定义则是创建对象,取名,并分配内存空间(比如上面的定义变量)。

C 语言规定变量名必须以字母或下划线开头,后面跟字母、数字、下划线。已有的关键字不能作为变量名,包括下面这些。

auto  break  case  char  const  continue  default  do  double
else  enum  extern  float  for  goto  if  inline  int  long
register  restrict  return  short  signed  sizeof  static  struct  switch  typedef
union  unsigned  void  volatile  while  _Bool  _Complex  _Imaginary

命名时最好避免以下划线开头,并最好用蛇形命名法或驼峰命名法。

局部变量与全局变量

变量有一定的作用域。简单来说,一个变量只有在 { } 内是有效的,在括号外无效,这种变量称为 局部变量。局部变量一般在用完后,存储空间会被释放。

特殊的,如果括号内部与括号外部有同名变量,那么内部的变量会“屏蔽”外部的变量。

如果在所有的 { } 外定义变量,那么其作用域是整个源程序,这种变量称为 全局变量

#include<stdio.h>

int a=1;

int func1()
{
  int b=2;
  printf("%d\n", a);
  printf("%d\n", b);
  return 0;
}

int func2()
{
  int c=3;
  printf("%d\n", a);
  printf("%d\n", c);
  return 0;
}

int main()
{
  printf("%d\n",a);
  {
    int a=10;
    printf("%d\n", a);
  }
  func1();
  func2();
  return 0
}
//输出
1
10
1
2
1
3

static与extern

在 C 程序的世界里,不同代码“国度”以.c文件为国界分隔开。 每个国家里有不同军阀(函数)割据 每个 C 程序里有一个君主(main) 君主通过下达圣旨(参数)来调用军阀(函数) 但某些军阀不想单纯听从于 main,树立了自己的政权旗帜 static static 不用听从 main 的调度,私藏金库(空间) main 对此很无奈,因为相对于 static,extern 更让它皇权不保。 不同国家之间通过 extern 私通消息。 ——《高质量嵌入式Linux C编程》

一般的局部变量存放在栈区,在函数开始时入栈,在函数结束时出栈。而使用 static 修饰变量后,该变量便存储在静态数据区,只在初次运行时进行初始化,并且不会在函数结束时销毁,只不过其作用域依然局限于该语句块。

#include<stdio.h>

viud func()
{
  static int a=1;
  a++;
  int b=0;
  a++;
  printf("%d\n",a);
  printf("%d\n",b);
}

int main(void)
{
  func();
  func();
  return 0
}
//输出
2
0
3
0

可以看出,加了 static 后,定义变量a 的语句只执行了一次。并且在第二次调用 func 时,a 的值依然是上次调用后的值。

如果对一个全局变量用 static 修饰,那么 static 会将其作用域由原来的整个工程可见变为仅本源文件可见。(以后在讲“链接”时还会提)。

extern 可以用于修饰函数或变量,用于表示当前函数或变量是在外部定义的。这里的外部即可以指文件外,也可以语句段外。但要注意的是,extern 所对应的变量必须定义为全局变量,而不是局部变量。看如下例子:

//file1.c
int a=1;
void func(int x)
{
  printf("%d/n",x);
}
//file2.c
#include<stdio.h>

int b=2;

int main()
{
  extern int a;
  extern void func(int);
  func(0);
  printf("%x\n",a);
  int b=3;
  {
    extern int b;
    printf("%d\n",b);
  }
  return 0;
}
#bash
gcc file1.c file2.c -o main
//输出
0
1
2

函数 func 与变量 a、b 分别展示了 extern 指另一个文件的函数、全局变量,也可以指本文件的全局变量。同时,extern 也会忽视同名的局部变量的影响。

注意,extern 后面的类型要和原类型一致。比如下面这个在 gcc 中是无法编译通过的。但据说某些编译器也可能会通过。总之,类型还是要保持一致。

#include<stdio.h>

char c[]="hello";

int main()
{
  extern int b;
  printf("%s\n",b);
  return 0;
}

const

const 是 constant 的缩写,表示恒定不变。用 const 修饰的变量一旦定义就永远不变。比如:

const int a=0;

int main()
{
  a++;
  return 0;
}

编译会提示错误信息:

main.c: In function main:
main.c:5:4: error: increment of read-only variable a
   a++;
    ^~

然而,有时候 const 会和其他东西组合。比如:

const int a = 10;
int const a = 10;
const int a[3] = {1,2,3};
const int *p;
int* const p; 

一个很好的判断方法就是:将类型去掉,看 const 修饰的是什么。比如:

  1. const a:a 内的值不变;
  2. const a:a 内的值不变;
  3. const a[3]:a[] 数组内的值不变;
  4. const *p:p 所指向的空间里的值不变;
  5. * const p:p 所指的地址不变,但地址内的值可变。

register

一般的变量存储在内存中,而 register 修饰的变量存储在寄存器。寄存器中的变量在运算上会快很多。

在使用寄存器变量时,要注意:

  1. 数量不能超过 CPU 内的寄存器的总数,尽量只在大量频繁操作时使用寄存器变量
  2. 长度要小于寄存器长度
  3. 不能对寄存器使用取地址符 “&”,因为寄存器没有内存地址
#include<stdio.h>
#include<time.h>   //用到clock()函数
int main() {
  int begintime,endtime;

  //用内存
	begintime=clock();	//计时开始
  for(int i = 0;i<10000;i++){};
	endtime = clock();	//计时结束
	printf("Running Time:%dms\n", endtime-begintime);

  //用寄存器
  begintime=clock();	//计时开始
  for(register int i = 0;i<10000;i++){};
	endtime = clock();	//计时结束
	printf("Running Time:%dms\n", endtime-begintime);

	return 0;
}
//树莓派4输出
Running Time140ms
Running Time39ms
//阿里云服务器输出
Running Time27ms
Running Time5ms

内存时间是寄存器时间的 3~6 倍。差距有点大啊……

volatile

volatile 在英语中的意思是:易变的、易挥发的。

为了解释 volatile 的作用,引入下面的例子:

int a=10;
int b=a;
int c=a;

第一行定义了 a 的值为 10,第二行则将 a 的值赋给 b,第三行将 a 的值赋给 c。

然而在编译器中不一定如此。有时候,编译器会优化为:

int a=10;
int b=a;
int c=10;

乍一看,这样貌似没问题。但是,如果在多线程中,a的值可能会改变。为了防止编译器“省事”,可以将 a 声明为 volatile,这样每次都会从内存中读取数据。

#define 与 typedef

有时候我们希望给类型换个名字,有两种方法:

#define int integer
//or
typedef integer int;

#define 仅仅将代码中的 integer 替换为 int,而 typedef 则是定义了一个新的类型 integer 来指代 int. 这种差别导致这两种方法并不等价。

typedef char * p_str1;
#define p_str2 char *
p_str1 s1, s2;
p_str2 s3, s4;

上面的变量定义中,s1,s2,s3 都定义为 char *,而 s4 则定义为 char

另一个例子是 consttypedef

typedef char * p_str;
char string[4] = "abc";
const char *p1 = string;
const p_str p2 = string;

int main()
{
  p1++;
  p2++;
  return 0;
}
//输出
main.c: In function main:
main.c:9:5: error: increment of read-only variable p2
   p2++;
     ^~

显然,const p_str p2 并不等同于 const char *p1,而是类似于 const int x,即对变量本身进行只读限制。

常量

常量是不可改变的量。按类型分,有:

  • 字符(Character)常量:’a’,”abc”
  • 整型(Integer)常量:int,short int,long int,unsigned short int,unsigned long等
  • 浮点型(Floating Point)常量:float,double,long double
  • 枚举类型
  • 构造类型:数组、结构、共用
  • 指针类型
  • void 类型

按用法分,有:

  • 直接常量(字面型常量):0,1.3,’a’,”abc”
  • 符号常量:#define Pi 3.14

整型

类型 范围 字节数
int $-2^{31}\sim(2^{31}-1)$ 4
unsigned int $0\sim(2^{32}-1)$ 4
short int $-2^{15}\sim (2^{15}-1)$ 2
unsigned short int $0\sim (2^{16}-1)$ 2
long int $-2^{31}\sim(2^{31}-1)$ 4
unsigned long $0\sim(2^{32}-1)$ 4
char $-2^7\sim(2^7-1)$ 1
unsigned char $0\sim (2^8-1)$ 1

对于变量,在定义时已经规定了类型。而对于常量,则通过加后缀来表明类型。

后缀十进制常量八进制或十六进制常量

int
long int
long long int

int
unsigned int
long int
unsigned long int
long long int
unsigned long long int

u或U

unsigned int
unsigned long int
unsigned long long int

unsigned int
unsigned long int
unsigned long long int

l或L

long int
long long int

long int
unsigned long int
long long int
unsigned long long int

既有u或U,又有l或L

unsigned long int
unsigned long long int

unsigned long int
unsigned long long int

ll或LL

long long int

long long int
unsigned long long int

既有u或U,又有ll或LL

unsigned long long int

unsigned long long int

考虑一下如下程序:

#include<stdio.h>
#include<string.h>

int main()
{
  char a[1000];
  int i;
  for(i=0; i<1000; i++)
  {
    a[i]=-1-i;
  }
  printf("%d\n",strlen(a));
  return 0;
}

尽管 for 循环内,i 从 0 自增到 1000,但由于 char 类型只有 1 个字节,故只能存储 i 的低 8 位。换句话说,char 只能存储 -128~127,一旦超过了 -128,就会导致溢出,变为 127。所以,a[0~127]=-1~-128,a[128~255]=127~1,a[256]=0,……。

而 string 类型以 “\0”(0x00) 作为结束标志,而 strlen[a] 只计算 “\0” 前面的字符数,所以最终输出 255.

字符型

字符常量中有一类特殊的字符叫转义字符(Escape Sequence),比如:

转义字符 作用
\’ 单引号’(Single Quote或Apostrophe)
\” 双引号”
\? 问号?(Question Mark)
|反斜线\(Backslash)  
\a 响铃(Alert或Bell)
\b 退格(Backspace)
\n 换行(Line Feed)
\t 水平制表符(Horizontal Tab)

enum

枚举的声明方式如下:

enum enum_type_name
{
  ENUM_CONST_1,
  ENUM_CONST_2,
  ……,
  ENUM_CONST_n
};
enum enum_type_name enum_variable_name;

这里我们声明了一个新的枚举类型 enum_type_name,并定义了一个新的枚举变量 enum_variable_name,这个枚举变量的取值只能是枚举类型里面的值。我们也可以将声明和定义结合起来:

enum enum_type_name
{
  ENUM_CONST_1,
  ENUM_CONST_2,
  ……,
  ENUM_CONST_n
} enum_variable_name;

下面是一个例子:

#include<stdio.h>

enum DAY
{
  MON, TUE, WED, THU, FRI, SAT, SUN
};

int main()
{
  enum DAY day = WED;
  printf("%d\n", day); //2
}

枚举里面的常量符号的值从 0 开始递增。也可以手动幅值:

enum Color
{
  GREEN = 1,   // --> 1
  RED,         // --> 2
  BLUE,        // --> 3
  ORANGE = 10, // --> 10
  PURPLE       // --> 11
}

更多例子可以看 C enum (枚举) | 菜鸟教程

联合体

联合体也叫共用体,这种类型可以用于在同一内存位置存储不同的数据类型。联合体可以有多个成员,但任何时候只有一个成员带有值。

定义联合体:

union Student
{
  int class;
  char name[10];
};

使用联合体:

union Student A;

A.class=1;
printf("class:%d\n",A.class);

strcpy(A.name,"Todd");
printf("name:%s\n",A.name);

printf("class:%d\n",A.class);
//输出
class:1
name:Todd
class:1684303700

可以看出,当给 name 幅值后,class 的值就改变了。

下面举几个例子说明联合体的妙用。

联合体用于分离高低字节

单片机中经常会遇见分离高低字节的操作,比如进行计时中断复位操作时往往会进行 (65535-200)/256,(65535-200)%256这样的操作,而一个除法消耗四个机器周期,取余也需要进行一些列复杂的运算,如果在短时间内需要进行很多次这样的运算无疑会给程序带来巨大的负担。其实进行这些操作的时候我们需要的仅仅是高低字节的数据分离而已,这样利用联合体我们很容易降低这部分开销。 代码:

!that's good,仅仅用了一条减法指令就达到了除法、取余的操作,在进行高频率定时时尤为有用。

联合体用于判断 CPU 模式

CPU 存放数据有两种模式:Little endian 和 Big endian,分别表示存储方式是从低地址到高地址 和 从高地址到低地址。

我们可以通过联合体来判断,因为联合体的存放顺序是从低地址开始存放,比如下面函数中,char 占最低字节,int 占低4个字节(但顺序不知道),因此如果是 Little endian,那么 c.b 内存储的是 1:

跨字节的类型转换

这个用处是之前做电设时遇到的,当时有两个 Arduino 需要通过 i2c 传输一个两个字节浮点数,然而 i2c 每次只能传送一个字节,于是就利用联合体,先通过 char 传输,传完后再用 float 提取数据。
当时有想过利用类型转换,但后来发现类型转换只能转换一个 char,而不能两个 char 合在一起转换。而联合体则可以很好的完成这个任务。