数据表现与运算
常量
八进制整数:以0开头,如 0123
就是一个八进制整数
十六进制整数:以 0x
或 0X
开头,如 0x12
或 0xFF
。
科学计数法:以 e 或者 E 表示以 10 为底的指数,e 前需有数字,e 后必须为整数。如 1e10
或 1.2E-12
普通字符:由单引号包裹。字符常量存储的是其 ASCII 码,本质是一个数字,所以可以参与运算。例如 'A' + 1
的结果是 B
转义字符:通常是控制字符,用来控制终端的光标。常用的如:
\n
:换行,将光标移动到下一行的开头\t
:水平制表符,将光标移动到下一个制表符的位置\\
:因为\
表示转义,所以需要输出反斜杠的时候需要再次转义。
字符串:由双引号包裹。
符号常量:由 #define
定义的常量。如 #define PI 3.14
,在编译时,程序中所有的 PI 符号都会被直接替换为 3.14。
变量
先定义,后使用。C89 标准中,一个代码块(大括号包裹的块)中所有的变量需要在代码块的开头定义好。
标识符
对变量或函数的命名即为标识符。标识符只可以由字母、数字和下划线组成,且不可以由数字开头。
数据类型
这里只考虑 C89 中的类型。并且数据占用长度按照 64 位 GNU 编译器的实现。
基本类型:
- int,整型,4字节
- short,短整型,2字节
- long,长整型,8字节
- char,字符型,1字节
- float,单精度浮点型,4字节
- double,双精度浮点型,8字节
派生类型:
- 指针类型,表示一个内存地址,32位系统中为4字节,64位系统中为8字节
- 数组类型,表示一个连续的内存空间
- 结构体
- 联合体
sizeof
运算符可以用于测量各个类型所占用的字节数,例如 sizeof(int)
返回 int 类型的数据占用的字节数。
在各种整型前加上 unsigned
关键字即可得到其无符号类型,这种数据只能是正数。
char 字符型的本质是占用一个字节的整型,范围是 0~127。所有整数可以参与的运算它也可以参与:
// 小写字母转大写字母
char func(char c)
{
return c+'A'-'a';
}
运算符和表达式
%:取余运算
++、–:自增和自减运算
+±-可以放在变量前和变量后,单独存在时行为一致,结果参与运算时不一致。
int i = 3;
int j = i++; // i=4,j=3
int i = 3;
int j = ++i; // i=4,j=4
以i++和++i为例,如果i在前,那么先使用i的值,再做自增。++在前的话,i先自增,再使用i的值。即:谁在前,就先执行谁。
赋值传递:a=b=c,从右至左,先将c的值赋给b,再把b的值赋给a,最后a和b的值都等于赋值前的c。
不同类型的数据间计算:会先将低精度的类型转成高精度类型,随后参与运算,返回的结果一定是高精度的。如果高精度的类型是float类型,那么会被自动转为double(系统的所有浮点计算都按照double进行)。
布尔运算
C布尔运算符,即关系运算符。<、>、<=、>=、==、!=
C语言中没有布尔类型数据,所有的关系运算返回的都是整型数据。关系运算为真则返回1,为假则返回0。
对于使用关系运算的结构,如 if、while之类的,不为0则都是真,为0则是假。
例如:
// 死循环,100永远为真
while(100){}
逻辑运算,&&、||、!,与或非。
其中 && 和 || 一定是两个符号,单个 & 和 | 都是位运算,按位与 按位或。
不为0则都是真。例如,!100
的结果是0,!0
的结果是1。
格式化输入输出
scanf,printf
参数都是 (const char *, args…)
第一个字符串是格式化字符串。其中包括占位符:
- %d,整型
- %ld,长整型
- %f,单精度浮点型
- %lf,双精度浮点型
- %c,字符型
- %s,字符串类型
有几个占位符,就需要有几个 args。scanf 由于需要给变量赋值,args 必须都是指针(注意,数组的本质也是指针,所以如果需要输入字符串,则无需加&取地址)。例如
int i;
scanf("%d", &i);
char str[30];
scanf("%s", str);
结构
switch 结构
switch(i)
{
case 1:
case 2:
case 3:
default:
}
会将 i 与 case 后的变量或常量逐个比较,如果相等就开始执行 case 后的语句,否则比较下一个 case。default 则默认直接执行。
如果一个 case 后没有 break 语句,那么就会接着执行下一个 case 或 default 的内容。
break 用于跳出 switch 结构。
循环结构
分为 while 循环、for 循环、do-while 循环。
while 循环:
首先判断 1 是否为真,如果符合就执行 2。执行 2 结束后会再次判断 1 是否为真。
while(1)
{
2
}
for 循环:
首先执行1(1一共只会执行1次),接着判断2,如果2为真,执行4,再执行3,再判断2是否为真。
for(1;2;3)
{
4
}
其中1、2、3都可以为空。但是分号不可省略。
do-while 循环:
首先执行1,再判断2是否为真,如果2为真,再次执行1。
do
{
1
} while(2);
多个循环结构之间可以嵌套。
改变循环状态:break、continue。
break 直接跳出当前循环,continue 结束本次循环,进行下一次循环。
对于 for 循环来说:
for(1;2;3)
{
4
}
如果 4 中执行到了 continue,则停止执行 4,开始执行 3,并进行下一次循环。
对于嵌套的循环,break 和 continue 都只能作用于当前最小范围的循环(最内层)。
break 同时也可以跳出 switch 语句,所以无论是 循环嵌套还是switch嵌套还是互相嵌套,break 跳出的永远是最内层的结构。
数组
普通数组
数组在定义时就必须指定数组的长度(所有维度)。C89 不允许使用变量作为数组的长度。
数组元素下标从 0 开始,每个元素都是同一个类型,占用一片连续的内存空间。
int a[10];
声明的数组,其中符号 a 还可直接当作 int *
使用,表示一个指向 a[0]
的指针。这也是二维数组和二级指针的基础。
字符数组与字符串
C语言中,字符串是一种特殊的字符数组。字符串以 \0
字符作为自身的结尾。C 通过检测 \0
字符来判断字符串是否结束,而非是否已经到了字符串的结尾。
char str[30] = "Hello";
如上,虽然 str 字符数组的长度是30,但是 str 作为字符串的长度只有 5,占用了 str 数组中的 6 个字节(\0
也需要占用一个字节)。
使用 printf 的 %s
输出字符串时,也是输出到 \0
元素为止的。
C 提供了一些特殊的函数专门处理字符串,这些函数都在 string.h 中:
- puts(字符数组),等价于 printf %s
- gets,直接输入一个字符串,等价于 scanf %s
- strcat(str1, str2),将 str2 接在 str1 的尾部,str1 必须足够大,可以容纳 str2,否则就会溢出。
- strcpy(字符数组1, 字符串),将字符串复制到字符数组中。
- strcmp(str1, str2),按照 ASCII 码序比较两个字符串,如果相等则返回0,如果 str1 的码序大于 str2 的码序则返回正数,否则返回负数。
- strlen(str),返回字符串的长度(不是字符数组的!)
函数
参数的传递是值传递。基本类型传递的是它的值,函数的修改不会改变原变量。如果传递的是地址,那么所有的改变都会反映到原变量(除了修改地址值)
由于数组的本质是指针,将数组作为函数参数传递时,所有函数中对数组的修改都是对原数组的修改。
指针
指针操作
指针的本质,是一个整型数据,表示一个内存地址。
在定义指针变量时必须指定基类型,用来表示这个指针变量指向的数据类型。
声明指针类型:int *a
,* 表示这是一个指针变量
相关操作:
&:取地址符,对一个普通变量取地址,返回指向这个变量的指针。
int i = 0;
int *p = &i;
*:解引用符(注意区别于声明中的那个*,那个只可以在类型中使用),对一个指针变量,获取它指向的数据。
int *p; // 假设已经指向了某个值
int i = *p;
当指针变量作为参数传递时,对其修改就会反映到原始变量。
int main()
{
int i = 0;
func(&i); // 执行后i变成2
}
void func(int *p)
{
*p = 2; // 接引用并修改
}
指针与数组
对于一个数组 int a[10]
,a 同时可以作为一个指针。
int a[10];
a == &a[0] // 真
在引用数组元素时,指针可以进行运算。
int a[10];
int *p = &a[0];
p ++; // 此时p指向a[1]
所以,获得一个数组a的第i个元素,有以下两种方式:
- a[i]
- *(a+i)
指针与二维数组
二维数组的本质是一维数组,这个一维数组中的每一个元素也是一个一维数组。
例如 int a[5][10]
,就是一个 5 个元素的一维数组,每个元素又都是一个 10 个元素的二维数组。
这样的话,如果将 a 当作指针的话,那就是一个二级指针。对 a 进行一次解引用之后,获取到的就是一个一级指针(一维数组),指向内层数组。
例如,我要通过指针的方式找到 a[3][5]
这个元素。
int a[5][10];
int *inner = *(a+3);
int res = *(inner+5);
这个例子中,inner 就指向了 a[3] 表示的数组,于是再做一次解引用就能拿到具体的值了。合在一起结果就是 *(*(a+3)+5)
函数指针不写了,但是函数是一等公民!
动态内存分配
直接在函数中声明的变量,是存放在内存的栈区的,函数的栈随着函数的结束而被回收。如果一个指针指向了在栈中分配的一个变量,那么在其所在的函数结束之后,再使用这个指针是不安全的。
所以可以通过动态内存分配的方式,在堆区上分配一块内存存储变量。这个变量就不会随着函数的结束而被回收。
动态内存分配通过 malloc、calloc、free、realloc 四个库函数实现,都在 stdlib.h 中。
malloc 函数原型为 void *malloc(unsigned int size)
,size 为要分配的字节数,返回的是指向这片区域的指针,这个指针无类型,在使用时需要强转成对应类型。如:
int *p = (int *)malloc(sizeof(int));
calloc 的功能类似于 malloc,原型为 void *calloc(unsigned n, unsigned size)
,这样就会分配 n*size 大小的内存空间(当然也可以给 malloc 直接传 n*size 也是一样的效果)。通常用在分配数组:
int *array = calloc(10, sizeof(int));
申请了 10 个 int 大小的空间,array 等效于一个 int array[10]
。
已经通过上面两种方式获取的内存空间,可以通过 realloc 改变大小:
int *array = calloc(10, sizeof(int));
// 扩充到 15 个 int 大小
p = realloc(p, 15*(sizeof(int)));
通过上面的方法分配的空间不会自动回收,如果大量申请可能会导致内存用尽。所以需要 free 函数手动回收,防止内存泄漏。
int *p = (int *)malloc(sizeof(int));
free(p); // 这之后p就无法再使用
结构体
结构体可以将多种不同类型的数据聚合成一个整体,作为一种新的类型。
struct Student
{
int id;
char name[30];
int age;
};
这样 struct Student
就成了一种数据类型(注意不能漏掉 struct)
// 声明一个学生变量 stu1
struct Student stu1;
可以通过 typedef 关键字给结构体类型一个简短的别名:
typedef struct Student
{
int id;
char name[30];
int age;
} student;
这样,student 和 struct Student
就是两个等价的类型。
对于一个结构体变量,可以通过 .
运算符获取其中的变量。例如声明的 stu1 变量,就可以通过 stu1.id
获取其 id。
声明的结构体类型,同样可以作为数组的元素类型使用。如 student stus[10];
对于一个结构体指针,如果想访问结构体中的变量,有两种方式,一种是直接接引用,然后用 . 访问:
student stu;
student *p = &stu;
*p.id // 解引用再访问
或者,可以通过 ->
运算符直接访问:
student stu;
student *p = &stu;
p->id
结构体变量作为参数传递的时候,同样是传值的,内部的修改无法反映到外部。也可以通过传递结构体指针的方式传递,这样可以同步修改。