1. 基本数据类型

1.1 算术类型

类型 解释 说明 注意事项 本地字节数
short 有符号短整数 完整形式signed short intsingedint可以省略 最左边一位表示符号,0为正数,1为负数 2
unsigned short 无符号短整数 完整形式unsigned short intint可以省略 全部位占满 2
int 有符号整数 完整形式signed intsinged可以省略 最左边一位表示符号,0为正数,1为负数 4
unsigned int 无符号整数   全部位占满 4
long 有符号长整数 完整形式为signed long intsingedint可以省略 最左边一位表示符号,0为正数,1为负数 8
unsigned long 无符号长整数 完整形式为unsigned short intint可以省略 全部位占满 8
long long 无符号长长整数 完整形式为signed long long intsingedint可以省略 C99特有 8
unsigned long long 有符号长长整数 完整形式为unsigned long long intint可以省略 C99特有 8
char 字符 实质是“小整数”(可能比短整数占用字节更少) 分为charsinged charunsigned char。使用单引号标记常量,比如'A'为65) 1
_Bool 布尔型整数 实质是无符号整数 只能赋值0或1,赋值_Bool类型变量为非零值会导致赋值为1 1
float 单精度浮点数     4
double 双精度浮点数     8
long double 扩展精度浮点数     16
  • 强制编辑器处理常量为长整数(十进制、八进制和十六进制),1135L;强制处理为无符号,1135U;混合使用,1135ULUL顺序和大小写不重要。 C99中增加了ll或者LL后缀,强制long long int型整数,可以与uU连用。避免无符号和有符号整数混用,特别是无符号和有符号整数比较,会产生意想不到的后果

  • 强制编辑器处理常量为单精度浮点数,11.3f11.3会被认为是double型;强制为双精度,11.3L或者11.3l(这里看出使用L更好,因为l会被误认为数字1)。

  • 强制类型转化表达式的一般形式为:(int) floatNumber。C语言把(type)视为一元运算符,所以其等级高于二元运算符。

  • 类型定义一般形式为:typedef int Newint;,注意结尾的。区别与使用宏定义类型,函数体内定义的typdef变量在函数体外无法使用,而宏可以作用于任何对应位置。

  • sizeof运算符一般形式为:sizeof(type),比如sizeof(long int)计算int类型占用多少个字节。sizeof表达式的类型是size_t(无符号整数),所以安全的方法是强制转换为unsigned long型,比如(unsigned long) sizeof(int)。括号不是强制需要,加上括号防止因为优先级不同而引起歧义。可以应用与常量、变量或者表达式。

1.2 数组

数组索引从0开始;数组按行存储。

1.2.1 一维数组

数组长度:

  • 数组长度可以是任何整数常量或整数常量表达式,比如testLen + 1testLen之前声明为整数常量(>-1)。

  • 数组长度之后可能会变,所以可以使用宏定义一维数组的长度,比如#define LEN 5

  • 确定数组长度,可以联合使用宏和sizeof,比如#define SIZE ((int) (sizeof(a) / sizeof(a[0])))

初始化:

  • 声明每一个元素的数值,比如int testArray[5] = {1, 2, 3, 4, 5};。这种情况可以忽略数组长度,比如int testArray[] = {1, 2, 3, 4, 5};

  • 声明部分元素,比如int testArray[5] = {1, 2, 3};,4号与5号元素默认为0。C99提供元素下标初始化,比如:int testArray[5] = {[0] = 1, [1] = 2, [2] = 3};;或者混用,比如int testArray[5] = {1, 2, [2] = 3};;甚至自动判断长度,比如int testArray[] = {[0] = 1, 2, [2] = 3, a[4] = 0};,编译器根据最大元素序号,制定数组长度为5。使用元素下标初始化,尽量按照下标序号从大到小初始化,否则可能引起后面元素覆盖前面元素。

  • 声明全部元素为0,比如int testArray[5] = {0};

1.2.2 多维数组

初始化:

  • 声明每一个元素或者部分元素,其余未声明元素为0。C99同样提供了下标初始化。比如:int testArray[2][2] = {[0][0] = 0, [1][1] = 1};

  • 声明全部元素为0,比如int testArray[5][10] = {0};

1.2.3 C99变长数组

C99允许声明变长数组。但是,变长数组的声明和初始化不能在一条语句中。正确的做法是:先声明变长数组,之后初始化。比如:

Initialization of variable-length array (VLA) C99
1
2
3
4
5
6
7
8
9
int size;
printf("Enter size of array: ");
scanf("%d", &size);
int square[size];

/* initial square */
for (int i = 0; i < size; i++) {
  square[i] = 0;
}

1.2.4 字符串

字符串长度:

  • 字符串的实现是使用数组储存字符,最后一个是空字符\0(对应ASCII码为0)。使用<string.h>头文件时,str()函数返回输入字符串中,第一个空字符之前的字符数。

初始化:

  • 第一种储存为字符数组。char a[6] = "hello";合法,最后一个是空字符;char a[6] = "hell";合法,最后两个是空字符;省略长度,比如char a[] = "hello";合法,最后一个是空字符,自动分配6个字符空间;char a[5] = "hello";对于字符串非法,对于字符数组合法。

  • 第二种储存为字符指针。char *b = "hello";合法,声明字符指针;声明字符指针,将其指向已有字符变量、字符串字面量或动态分配字符串,合法;已有字符指针重新指向其他字符串,合法;声明字符指针,却不初始化,非法;声明字符指针,却不初始化,而且修改或赋值指向的字符,非法。

  • 字符数组可以修改元素,比如a[0] = 'l';;字符指针指向的字符串不能修改,应该杜绝*b = ‘l’;之类的字符指针赋值,除非保证b是一个字符数组;对于操作字符串的函数,如果形参为字符指针,允许修改其指向的内容,比如*b = 'l';,因为默认(也只允许)传入参数是一个字符数组,而不是一个字符串字面量。

1.2.4 字符串数组

初始化:

  • 使用指针数组储存,比如char *a[5] = {"hello", "it", "is", "me", "!"};;省略长度,char *a[] = {"hello", "it", "is", "me", "!"};合法;char *a[5] = {"hello", "it", "is", "me"};合法,最后一个是空指针(NULL)。

1.3 结构

结构空间分配:

  • 成员按照声明的顺序在内存依次排列。

  • 第一个成员之前无间隙,因此方便指针指向结构。但成员之间或者最后一个成员之后,可能有间隙。

初始化:

  • 结构中可以嵌套结构、联合、枚举、字符串等多种类型。

  • 不需要初始化全部成员,剩下未初始化的成员使用0作为初始值,比如""作为空字符串。

  • C99使用符合字面量,比如(struct test1) {.b = "test that", .c = 1};

initialize struct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct test1 {
  int a;
  char b[10];
  int c;
};

struct {
  int a;
  char b[10];
} p3;

/* p1.c is 0 */
struct test1 p1 = {1, "hello"};

/* c99 */
/* p1.a is 0 and p1.c is 3 */
struct test1 p2 = {.b = "world", 3};

/* c99 */
(struct test1){.c = 10};
  • 结构、联合和枚举都可以使用typedef定义,总结如下:
initialize struct with typedef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct {
   int ele1;
   char ele2;
 } Test1;

typedef union {
  int ele1;
  double ele2;
} Test2;

typedef enum {CIRCLE, RECTANGLE} Test3;

int main(void)
{
  Test1 p1 = {.ele1 = 1};
  Test2 p2 = {.ele2 = 0.2};
  Test3 p3 = CIRCLE;

  return 0;
}

1.4 联合

联合空间分配:

  • 只为最大的成员分配空间。

初始化:

  • 初始化与结构类似。
initialize union
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
union test1 {
  int a;
  char b[10];
};

union {
  int a;
  char b[10];
} p3;

union test1 p1 = {0};

/* c99 */
union test1 p2 = {.b = "world"};

/* c99 */
(union test1){.a = 10};
  • 如果联合包括两个及两个以上结构成员,而且这些结构中最初一个或多个成员类型匹配,那么这些匹配成员会同步。
initialize union
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
union test {
  int a;
  struct test1 {
    int b1;
    char b2[10];
  } b;
  struct test2 {
    int c1;
    int c2;
  } c;
} t1;

t1.b.b1 = 10;
/* t.c.c1 is 10 */
printf("%d \n", t1.c.c1);

1.5 枚举

初始化:

  • 枚举元素如不指定,从0开始,依次增加1;可以指定具体数值,指定数值可以相同,允许尾逗号,例如enum {THIS = 2, THAT = 10, THESE = 2,} test1;
initialize enum
1
2
3
enum {FIRST, SECOND} p1;
enum test1 {FIRST, SECOND};
enum test1 = SECOND;

1.6 指针

参考C语言指针记录

1.7 函数

1.7.1 函数定义

  • 函数不能返回数组,也不能返回两个数。

  • 函数返回类型可以是void,形参可以是voidvoid类型函数没有返回值,可以写一条表达式为空的return语句,比如return;

  • 每个形参必须单独声明类型,使用,连接;

  • 使用void丢弃函数返回值,比如printf()函数返回显示的字符数目,强制丢弃返回数值写为:(void) printf("Drop the return value.");

  • main函数返回值:return语句;exit()函数(位于<stdlib.h>库)。任何函数调用exit()函数,都会导致程序终止;只有main函数调用return语句,程序才终止。C99规定:非void类型函数必须指定返回类型,不能缺省。

  • 函数声明的形式为int thisFun(double para1, int para2);C99规定:函数在调用前,必须事先声明或者定义。调用前声明有很多好处,比如避免实际参数的默认转换等。

1.7.2 数组型参数

  • 定义形参为一维数组的函数,普遍形式为:
Definition of function using array as the parameter
1
2
3
4
5
6
7
8
9
10
11
12
/* 一维数组长度不是必须 */
int oneArrayFun(int a[], int len) {
...
}

/* 函数声明 */
int oneArrayFun(int a[], int len);
/* 省略形参声明 */
int oneArrayFun(int [], int);

/* 实际调用 */
oneArrayFun(testArry, testLen);
  • 对于一维数组类型的参数,“长度”的实际参数可以比形式参数小,但不能大。

  • 函数可以改变一维数组形参的值,并在实参中体现。

  • C89定义形参为多维数组的函数,只能省略第一维长度,其余维度必须声明(即常量),普遍形式为:

Definition of function using array as the parameter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 比如二维数组,行不是必须,但必须制定列 */
#define COL 10

int twoArrayFun(int a[][COL], int row) {
...
}

/* 函数声明 */
int twoArrayFun(int a[][COL], int row);
/* 省略形参声明 */
int twoArrayFun(int [][COL], int);

/* 实际调用 */
twoArrayFun(testArry, testRow);
  • C99可以声明任意变长数组作为函数参数,普遍形式为:
Definition of function using VLA as the parameter in C99
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
/* 一维VLA */
int oneArrayFun(int len, int a[len]) {
...
}

/* 一维VLA声明的各种形式 */
int oneArrayFun(int len, int a[len]);
int oneArrayFun(int len, int a[*]);
int oneArrayFun(int, int [*]);
int oneArrayFun(int len, int a[]);
int oneArrayFun(int, int []);

/* 实际调用 */
oneArrayFun(testArry, testLen);

/* 二维VLA */
int twoArrayFun(int row, int col, int a[row][col]) {
...
}

/* 二维VLA声明的各种形式 */
int twoArrayFun(int row, int col, int a[row][col]);
int twoArrayFun(int row, int col, int a[*][*]);
int twoArrayFun(int row, int col, int a[][col]);
int twoArrayFun(int row, int col, int a[][*]);
  • C99允许在数组参数中使用关键字static,但只能用于第一维。好处是编译器更快访问数组,比如:
Use static in C99
1
2
3
4
/* 一维数组长度至少为5 */
int oneArrayFun(int a[static 5], int len) {
...
}
  • C99允许使用复合字面量。复合字面量是“左值”,可以包括常量或者常量表达式,形式与数组初始化类似;也可以使用const修饰。
Use static in C99
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
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>

int oneArray(int a[], int len);

int main(void)
{
  printf("Sum is %d. \n", oneArray((int []){1, 2, 3, 4, 5}, 5));

  int i = 1, j = 2, k = 3;
  printf("Sum is %d. \n", oneArray((int [5]){i, j, k}, 5));

  printf("Sum is %d. \n", oneArray((int [5]){1, 3, 5, 7}, 4));

  printf("Sum is %d. \n",
         constOneArray((const int [5]){1, 1, 1, 1}, 3));

  return 0;
}

int oneArray(int a[], int len) {

  int sum = 0;

  for (int i = 0; i < len; i ++) {
    sum += a[i];
  }

  return sum;
}

int constOneArray(const int a[], int len) {

  int sum = 0;

  for (int i = 0; i < len; i ++) {
    sum += a[i];
  }

  return sum;
}

2. 操作符类型和优先级

C operators

图片取自参考资料2

  • 操作符有两个性质:结合方向优先级结合方向决定操作符的执行对象,比如多个同等操作符;而优先级决定操作符的结合方式,通俗来讲即谁和谁结合在一起。但是,C没有规定表达式运算的先后顺序。比如对于二元操作符+a = i + i++;,由编译器决定是i还是i++先执行。

  • 操作符/%用于负整数操作,结果由编译器决定。C99/%操作负数,返回最靠近0的结果。

  • 运算符&&||(从左向右结合),两侧的两个表达式有运算顺序,先左后右。有可能右侧表达式没有计算,因此不要在右侧放入有副作用的表达式

3. 表达式

  • 条件表达式i > 0 ? i : f,如果if是整数型和浮点型,即使条件判定为真,表达式的值为浮点型。

  • switch语句最后一个分支,添加break;语句。原因是防止之后修改程序,需要再添加判断条件时,遗漏break;语句。C语言的switch语句不能判断范围,但适合替代多个OR连接的判断语句。

  • 逗号表达式中,表达式1, 表达式2表达式1先计算之后丢弃其值,之后计算表达式2。因此,表达式1必须有副作用

4. 语法注意事项

  • #define LOWER 0定义常量的语句之后,没有分号;

  • break;只能跳出一层循环。

  • continue;。有意思的应用场景,结合if语句和continue;语句,在循环中条件性忽略一些语句。

  • EOF是文档结束的标志(End of File),在<stdio.h>定义为一个整数-1,代码如下:

Definition of EOF in C
1
2
3
#ifndef EOF
# define EOF (-1)
#endif
  • 用单引号''标记的字符,对应的是ASCII的数字值。

  • for循环声明需要有主体;如无,在for语句后添加;作“无效声明(null statement)”。为了避免误解,;单独占一行,或者使用{}代替单独一行的;

  • 顺序点(sequence point)

    • &&||和comma operators,左边和右边表达式之间。

    • 三元条件操作符?:,在条件判断表达式与第二(第三)表达式之间。

    • 完整表达式结束,包括赋值、return语句、if/switch/while/do-while条件表达式判断结束和for三个表示式。

    • 函数的所有参数赋值和函数第一条语句执行之前(见后举例)。

    • 变量初始化语句结束,比如int a = 1;。如果多个变量初始化(,分割),则在每一个,结束处,比如 int a = 1, b = 2;

  • i++++i

    • 大多数情况用于整数操作。

    • ++i马上自增,i++自增则在两个相邻顺序点之间进行。表达式++i值为i+1,表达式i++值为i

    • 表达式i++ == 0;中,i使用原始数值,在表达式结束后自增;相反,表达式++i == 0;中,i马上自增后与0比较。

    • 对于表达式f(i++),传入的参数值为i,但是在函数内部开始执行前,i完成自增。这是因为在函数的所有参数赋值和函数第一条语句执行之前有一个顺序点

    • for语句中,使用for(i = 0; i < 10; i++)for(i = 0; i < 10; ++i)效果一样。

    • 对于现代编译器,i++++i的执行效率没有区别。所以写代码时,按照自认为最清楚的方式写。

  • 未定义行为(undefined behavior)

    • 一个表达式中既访问又修改同一个变量。

    • 数组访问超过上下限。造成原因:很有可能是忘记数组从0开始索引

    • C89中,非void类型函数,执行到末尾,没有执行return语句。程序调用此类函数,会出现未定义行为。C99中,不合法。

  • 副作用(side effect)

    • 所有赋值运算、i++++i都有副作用,即改变原始变量的值。

    • 赋值运算的值是赋值操作后右侧的值,并且将其强制转换为左侧值类型;赋值运算左侧必须为“左值(lvalue)”。

    • 在同一个表达式,即访问某个变量,同时又修改这个变量,会造成“未定义行为”。有副作用的操作,会带来隐晦未定义行为。未定义行为会随着不同的编译器,而产生不同的结果。其危险性不仅在于阻碍跨平台使用,而且也会有程序运行失败或者得到意想不到结果。建议:不在一个表达式中即访问又修改同一个变量。一些典型的未定义行为的例子:

Undefined behavior in C
1
2
3
4
5
6
7
# can not decide whether "++", "=", or "+" is the first
a = i + i++;
i = i++;
a[i] = i++;

# "," is not comma operator
printf("%d %d\n", ++i, i);

5. 标准库

5.1 #include <stdio.h>

1
printf("%3.0f %6.1f\n", fahr, celsius);

%m.pX:格式串模板,m表示要显示的最小字段宽度,p表示精度,X表示类型。

对于 printf()函数:

  • %d:十进制整数。

    • %1d一个十进制整数,配合scanf()实现单个整数输入操作。

    • %6d:十进制整数,至少6位宽,右对齐;如果数字位数超过6位,则全部显示。

    • %-6d:同上,区别左对齐。

    • %-6.3d:同上,如果数字位数少于3位,左侧加0。比如,printf(".2d%", 5)的输出为05

  • %u:无符号十进制数。

    • %o:无符号八进制数。

    • %x:无符号十六进制数。

    • 没有标准方法打印负八进制和负十六进制数。

  • %h:短整数前缀。

    • %l:长整数前缀。

    • %ll:长长整数前缀,C99特有。

    • %h%l%llduox连用,比如ho表示无符号短八进制数。

  • %f:浮点数,默认小数位为6位。

    • %6f:浮点数,至少6位宽。

    • %6.0f:浮点树,至少6位宽,无小数点而且无小数位。

    • %.2f:浮点数,小数点后两位。

    • %6.2f:浮点数,至少6位宽,小数点后两位。

    • %e:科学计数。

    • %g:科学计数或者浮点数。

  • %ldouble型前缀。

    • %l只在scanf()中使用,在printf()使用%f%e(或者%g)表示float型和double型。

    • C99中允许在printf()使用%lf%le%lg,但是l不起作用。

    • %Llong double型前缀。

    • %l%Lefg连用。

  • %c:字符。

  • %s:字符串。

  • %%%自身。

对于scanf()函数:

  • 格式串中出现空白字符(空格、水平或者垂直制表符、换页符、换行符),数量无关紧要,因为可以匹配任意数量(包括0个)空白字符。

  • 不要输把空白字符(比如"%d ")放入格式串的结尾。原因:scanf()会挂起,直到出现不能匹配的空白字符。

6. C99注意事项

  • for语句中声明的计数变量,只能在for循环中使用。比如 for(int i = 0; i < 10; i++){...},变量i只能在循环内起作用。

  • 新增加表示布尔数值的头文件<stdbool.h>,使用方法:

bool in C99
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdbool.h>

int main(void)
{
  bool trueVal = true, falseVal = false;

  bool a1[10] = {false};

  bool a2[10][10] = {false};

  return 0;
}
  • 允许使用typedef定义占用特定位数(是“比特数”,不是“字节数”)的整数,比如:
typedef 32 bit integer
1
2
3
4
5
6
7
8
#include <stdint.h>

int main(void)
{
  typedef int32_t Int32;

  return 0;
}

参考资料

更新记录

2015年4月30日

Comments