C语言技能数(知识点汇总)

C语言技能数(知识点汇总)

C 语言概述

特点

  1. C语言简洁、紧凑、灵活。C语言的核心内容很少,只有 32 个关键字,9 种控制语句;
  2. 表达方式简练、实用。C语言有一套强有力的运算符,达44种
  3. 具有丰富的数据类型。
  4. 具有低级语言的特点。具有与汇编语言相近的功能和描述方法,如地址运算、二进制数位运算等,对硬件端口等资源直接操作,可充分使用计算机资源。
  5. 是一种结构化语言,适合于大型程序的模块化设计。
  6. 预处理命令和预处理程序。
  7. 可移植性。
  8. 生成的目标代码质量高。
  9. C语言语法限制不严,程序设计自由度大。

不足之处

  1. C程序的错误更隐蔽
  2. C程序有时会难以理解
  3. C程序有时会难以修改

标准

  1. 1978年,丹尼斯·里奇(Dennis Ritchie)和布莱恩·科尔尼干(Brian Kernighan) 发布了 K&R C.
  2. 1989年,美国国家标准局为其指定标准,称为 ANSI C,也称 C89.
  3. 1990年,国际化标准组织为其指定标准,称为 ISO C,也称 C90.
  4. 1999年,发布的ISO/IEC 9899:1999标准,称为 C99.
  5. 2011年,美国国家标准局采纳了ISO/IEC 9899:2011标准,称为 C11.
  6. 2018年,发布的ISO/IEC 9899:2018标准,称为 C18.

编程机制

一个典型的C程序编译管道,包含预处理、编译、汇编、链接四个环节。

  1. 输入 .c.h 等源码文件
  2. 【预处理】宏展开(#include/#define/…)
  3. 【编译器】编译成汇编代码,得到 .s 文件
  4. 【汇编器】输出机器代码,得到 .o.obj 文件
  5. 【链接器】输出可执行文件,得到 .exe 文件

数据类型

变量

变量相当于内存中一个数据存储空间的表示,通过变量名可以访问到变量(值)。可以把变量看做是一个房间的门牌号,通过门牌号我们可以找到房间。

变量使用注意事项

  1. 变量表示内存中的一个存储区域(不同的数据类型,占用的空间大小不一样)
  2. 该区域有自己的名称和类型
  3. 变量必须先声明,后使用
  4. 该区域的数据可以在同一类型范围内不断变化
  5. 变量在同一个作用域内不能重名
  6. 变量三要素 (变量名+值+数据类型) 。

数据类型

每一种数据都定义了明确的数据类型,在内存中分配了不同大小的内存空间(使用字节多少表示)。

注意:

  1. 在c中没有字符串类型,使用字符数组 char[] 表示字符串
  2. 在不同系统上,部分数据类型字节长度不一样,举例:int可以占两个字节或四个字节

字符类型

字符类型可以表示单个字符,字符类型是charchar是1个字节(可以存字母或者数字),多个字符称为字符串, 在C语言中使用 char 数组 表示,数组不是基本数据类型,而是构造类型。

注意

  1. 字符常量是用单引号括起来的单个字符。 例如: char c1 ='a'; char c3 = '9';
  2. C中还允许使用转义字符‘\’来将其后的字符转变为特殊字符型常量。例如:char c3 = '\n';
  3. 在C中, char的本质是一个整数。字符型存储到计算机中,需要将字符对应的码值(整数)找出来。字符和码值的对应关系是通过字符编码表决定的。
  4. 可以直接给char赋一个整数,然后输出时,会按照对应的ASCII 字符输出。
  5. char类型是可以进行运算的,相当于一个整数,因为它都对应有Unicode码。

整数类型

对于整型和字符型,可以使用 signedunsigned 进行修饰。默认是 signed

类型 字节 取值下限 取值上限
signed char 1 -2^-7即 -128 2^7-1 即 127
unsigned char 1 0 2^8-1 即 255
signed short 2 -2^-15 即 -32768 2^15-1即 32767
unsigned short 2 0 2^16-1 即 65535
signed int 2或4 -2^-15 或 -2^-31 215-1或231-1
unsigned int 2或4 0 216-1或232-1
signed long 4 -2^-31 2^31-1
unsigned long 4 0 2^32-1
符号位
  • 存放signed类型的存储单元中,左边第一位表示符号位。
  • 如果该位为0,表示该整数是一个正数;如果该位为1,表示该整数是一个负数。
  • 一个32位的整型变量,除去左边第一位符号位,剩下表示值的只有31个比特位。

注意

  1. 各种类型的存储大小与操作系统、 系统位数和编译器有关 ,目前通用的以64位系统为主。

  2. C语言的整型类型, 分为有符号 (signed) 和无符号 (unsigned) 两种, 默认是有符号的。

  3. C程序中整型常声明为 int 型, 除非不足以表示大数, 才使用 long long

  4. 一个 B 等于 8 个 b

    • B 是 Byte 的缩写,意思是字节,是计算机存储容量的最基本的存储单元。
    • b 是 bit 的缩写,意思是比特位,是计算机中的存储数据的最小单位。指的是二进制中的一个位数。
  5. 一个字节有八位二进制组成,即 1 Byte = 8 bit

    • 通常用 Bit 来作数据传输的单位,因为物理层、数据链路层的传输对于用户是透明的。
    • 通常用 Byte 来作应用层的单位,比如表示文件的大小,在用户看来就是可见的数据大小。
二进制的原码、反码和补码

原码

将最高位作为符号位(0表示正,1表示负),其它数字位代表数值本身的绝对值的数字表示方式

反码

  • 如果是正数,则表示方法和原码一样;
  • 如果是负数,符号位不变,其余各位取反,则得到这个数字的反码表示形式。

补码

  • 如果是正数,则表示方法和原码一样;
  • 如果是负数,则将数字的反码加上1(相当于将原码数值位取反然后在最低位加1)。

浮点类型

类型 存储大小 值范围 精度
float 单精度 4 字节 1.2E-38 到 3.4E+38 6 位小数
double 双精度 8 字节 2.3E-308 到 1.7E+308 15 位小数

注意

  1. 关于浮点数在机器中存放形式的简单说明,浮点数=符号位+指数位+尾数位 , 浮点数是近视值
  2. 尾数部分可能丢失,造成精度损失。

浮点型使用细节

  1. 浮点型常量默认为 double 型 , 声明 float 型常量时, 须后加 fF
  2. 浮点型常量有两种表示形式
    1. 十进制数形式:如: 5.12 512.0f .512 (必须有小数点)
    2. 科学计数法形式:如: 5.12e2 5.12E-2
  3. 通常情况下,应该使用 double 型,因为它比float型更精确。
  4. 格式化输出时,%f 默认输出 6 位小数,%.2f 可指定输出 2 位小数。

布尔类型

布尔类型并非是基本类型, C语言标准(C89)没有定义布尔类型,所以C语言判断真假时以0为假,非0为真

C语言标准(C99)提供了_Bool 型, _Bool 仍是整数类型,但与一般整型不同的是,_Bool 变量只能赋值为0或1,非0的值都会被存储为1。

C99还提供了一个头文件 <stdbool.h> 定义了 bool 代表 _Booltrue 代表1, false代表0

只要导入 stdbool.h ,就能方便的操作布尔类型了 , 比如 bool flag = false;

常量

C编程中的常量是一些固定的值,它在整个程序运行过程中无法被改变。

字面常量

字面常量是直接写出的固定值,它包含C语言中可用的数据类型,可分为整型常量,字符常量等。

如:9.9“hello”等就属于这一类常量。

const 修饰的常变量

C语言标准提供了 const 关键字。在定义变量的同时,可在变量名之前加上 const 修饰。

const int n = 10;
int arr[n] = { 0 };
  • const 修饰的常变量,本质上是变量。
  • 但具有常属性,不能被修改。
  • 在C99标准之前,数组的大小只能是常量修饰,不支持变长数组。

#define定义的标识符常量

C语言提供了 #define 命令定义标识符常量,该标识符常量在程序中是个定值,通常用于代表数组容量或涉及数学的常量等。

#define PI 3.14159
#define SIZE 10
  • #define 又称宏定义,标识符为所定义的宏名,简称宏。
  • 对宏定义而言,预编译的时候会将程序中所有出现“标识符”的地方全部用这个“常量”替换,称为“宏替换”或“宏展开”。
  • 被定义的标识符不占内存,只是一个临时的符号,预编译后这个符号就会被宏替换。
  • 宏所表示的常量可以是数字、字符、字符串、表达式。其中最常用的是数字。

枚举常量

语言提供了一种 枚举(Enum)类型,能够列出所有可能会用到的取值,并给它们取一个名字

enum Gender { Male, Female, Secret, Unknown };

在使用枚举常量的时候,需要注意以下几点:

  • 不能对枚举常量赋值,只能将它们的值赋给其他的变量。
  • 不能再定义与枚举常量名字相同的变量。
  • 不能用 & 取得它们的地址。

sizeof

sizeof 是关键字,操作符,不是函数,用于获取操作数被分配的内存空间,以字节单位表示。

基本使用

  • sizeof(object);
  • sizeof object
  • sizeof(type_name);

例如:

int n = 10;
printf("sizeof(n)=%d\n", sizeof(n));
printf("sizeof n =%d\n", sizeof n );
printf("sizeof(int)=%d\n", sizeof(int));

sizeof(结构体)

  • 理论上讲结构体的各个成员在内存中是连续存放的,和数组非常类似,但是,结构体占用内存的总大小不一定等于全部成员变量占用内存大小之和。
  • 在编译器的具体实现中,为了提高内存寻址的效率,各个成员之间可能会存在缝隙。
  • sizeof 可以得到结构体占用内容在总大小。
  • sizeof(结构体名)sizeof(结构体变量名) 都可以。

不要对 void 类型使用 sizeof

void是无值型或空类型,不知道存储空间大小的类型,编译器也不能确定它的大小。

#include <stdio.h>

void func() {}

int main() {
	printf("sizeof(func)=%d\n", sizeof(func));
	// 输出结果:sizeof(func)=1
	return 0;
}

void不能声明变量,但是可以声明指针

void *pv;
printf("sizeof(pv)=%d\n",sizeof(pv));
printf("sizeof(void*)=%d\n",sizeof(void *));
// 输出结果:
// sizeof(pv)=4
// sizeof(void*)=4

不要在子函数中对指针类型使用 sizeof

如果把一个字符串的地址传给子函数,子函数用一个字符指针(如char *pstr)来存放传入的字符串的地址,如果在子函数中用 sizeof(pstr),得到的不是字符串占用内存的字节数,而是字符指针变量占用内存的字节数。

所以,不能在子函数中对传入的字符串进行初始化,除非字符串的长度也作为参数传入到了子函数中。

#include <stdio.h>

void func(char * pstr) {
	// sizeof(pstr)=4
	printf("sizeof(pstr)=%d\n", sizeof(pstr));
}

int main() {
	func("hello world!");
	return 0;
}

同理,也不要在子函数中对结构体指针用 sizeof,如果把一个结构体(如struct Human h)的地址传给子函数,子函数用一个结构体指针(如struct Human *h)来存放传入的结构体的地址,如果在子函数中用 sizeof(h),得到的不是结构体占用内存的字节数,而是结构体指针变量占用内存的字节数。

正确的用法是用 sizeof(struct Human)

#include <stdio.h>

struct Human {char * name; int age;};

void func(struct Human *h) {
	// 错误的输出:sizeof(h)=4
	printf("sizeof(h)=%d\n", sizeof(h));
	// 正确的输出:sizeof(*h)=8
	printf("sizeof(*h)=%d\n", sizeof(*h));
	// 正确的输出:sizeof(struct Human)=8
	printf("sizeof(struct Human)=%d\n", sizeof(struct Human));
}

int main() {
	struct Human human = {"tom", 23};
	func(&human);
	return 0;
}

sizeof 的结果取决于环境

在不同的 GCC 环境中,得到的结果是不同的。

通过 systeminfo 指令可以获取当前系统的信息,部分如下:

主机名:           THINKPADX1
OS 名称:          Microsoft Windows 11 专业版
OS 制造商:        Microsoft Corporation
OS 配置:          独立工作站
系统制造商:       LENOVO
系统类型:         x64-based PC

可以看出,设备是基于 x64 的芯片,也就是收机器字长是 64。通过下面这段源代码观察不同 gcc 下的结果

#include<stdio.h>

int main() {
	char c = 'A';
	short s = 10; int i = 10; long l = 10L;
	float f = 3.14F; double d = 3.14D;
	int *p = &i;
	printf("char c = %c\t", c);
	printf("short s = %d\t", s);
	printf("int s = %d\t", i);
	printf("long s = %d\n", l);
	printf("float s = %f\t", f);
	printf("double s = %f\n", d);
	printf("*p = %d\n", *p);
	printf(" p = %p\n", p);
	printf("&p = %p\n", &p);
	printf("sizeof(char) = %d\n", sizeof(char));
	printf("sizeof(short) = %d\n", sizeof(short));
	printf("sizeof(int) = %d\n", sizeof(int));
	printf("sizeof(long) = %d\n", sizeof(long));
	printf("sizeof(float) = %d\n", sizeof(float));
	printf("sizeof(double) = %d\n", sizeof(double));
	printf("sizeof(size_t) = %d\n", sizeof(size_t));
    // size_t是一种机器相关的无符号类型,它被设计的足够大以便能表示内存中任意对象的大小。
    return 0;
}

我已经在本机的 D:\Program\MinGWD:\Program\MinGW64 分别安装了 gcc 的32位版本和 64位版本。

在命令行中临时修改环境变量,用以切换 gcc 的寻址路径:

> SET PATH=C:\Windows\System32\;D:\Program\MinGW\bin
> WHERE gcc
D:\Program\MinGW\bin\gcc.exe

也可以通过 gcc -v 指令来查看当前 gcc 的版本,接下来使用 gcc 对上面的程序进行编译和执行,如下:

gcc app.c -o app.exe && app.exe

执行结果:

char c = A      short s = 10    int s = 10      long s = 10
float s = 3.140000      double s = 3.140000
*p = 10
 p = 0061FF04
&p = 0061FF00
sizeof(char) = 1
sizeof(short) = 2
sizeof(int) = 4
sizeof(long) = 4
sizeof(float) = 4
sizeof(double) = 8
sizeof(size_t) = 4

然后在命令行中通过再次修改环境变量,来临时切换下 gcc 的寻址位置,如下所示:

> SET PATH=C:\Windows\System32\;D:\Program\MinGW64\bin
> WHERE gcc
D:\Program\MinGW64\bin\gcc.exe

再次使用 gcc 命令对上面的程序进行编码和执行,执行结果如下:

char c = A      short s = 10    int s = 10      long s = 10
float s = 3.140000      double s = 3.140000
*p = 10
 p = 000000000061FE04
&p = 000000000061FDF8
sizeof(char) = 1
sizeof(short) = 2
sizeof(int) = 4
sizeof(long) = 4
sizeof(float) = 4
sizeof(double) = 8
sizeof(size_t) = 8

在基于 x64 的操作系统中安装两个不同版本的 gcc ,使用 sizeof 对基础类型测量的占用空间,得到结论如下:

type GCC x32 GCC x64
char 1 1
short 2 2
int 4 4
long 4 4
float 4 4
double 8 8
pointer 8 16
size_t 4 8

可以明显看到:

  • 基础数据类型占用的空间不会因为 gcc 的版本不同而不同的
  • 即便是基于 x64 的平台,在 gcc 32 位的环境下,指针的长度也为 8

运算符与表达式

  • 赋值运算符号
=+=-=*=/=%=
  • 算术运算符号
+-*/%
  • 自运算符号:++--

    • 前缀:先运算后赋值
    • 后缀:先赋值后运算
  • 关系运算符号

<<=>>===!=
  • 逻辑运算符号
(&&)、或(||)、非(!)
  • 逗号运算符号

一个句子就会像一个函数一样有返回值,如果用逗号隔开, 这个【返回值】就会变成最后那个表达式的值!

#include <stdio.h>

int main() {
	3, 4, 5;//这是一条语句
	int n = (3, 4, 5); // n 的值 5
	printf("n=%d\n", n);
	
	int a=3, b=4, c=5;
	int x=0;
	int y=((x=a+b),(b+c));
	// 等价于 y=b+c 致使 y 的值是 9
	printf("x=%d,y=%d\n", x, y);
	y=(x=a+b),(b+c);
	// 等价于 y=x=a+b 致使 y 的值是 7
	printf("x=%d,y=%d\n", x, y);
	return 0;
}
  • 条件运算符号
int num = 2024;
printf("num is %s\n", num % 2 == 0 ? "even" : "odd");

语句与控制流

  • 控制语句
// for 循环
for(int i = 0; i < 5; i++) {
	printf("%d\t", i);
}
printf("for end\n");

// do 循环
int i = 0;
do {
	printf("%d\t", i);
	i++;
} while(i < 5);
printf("do end\n");

// while 循环
i = 0;
while(i < 5) {
	printf("%d\t", i);
	i++;
}
printf("while end\n");

  • 函数返回(return)
int main() {
	return 0;
}
  • 转向语句
// for 循环
for(int i = 0; i < 5; i++) {
	printf("%d\t", i);
	if(i == 2) {
		goto end_flag;
	}
}
printf("for end\n");


end_flag:
printf("finish!\n");
  • 函数调用语句
printf(“Hello world.);
  • 表达式语句
i++;
  • 空语句

直接只有一个分号的一行语句。

  • 复合语句,也称为代码块或者语句块
// 代码块
{
    int i = 10;
    printf("i=%d\n", i);
}
// 复合语句
{
    int i = 20;
    printf("i=%d\n", i);
}

函数与程序结构

被调用的函数必须在调用行之前被定义

函数的声明和定义

在 C 语言中,一般地,一个函数由函数头和函数体两部分组成。一般形式如下:

返回值类型  函数名 (参数1, 参数2, ...) {
    // 代码
}

上述形式中,大括号部分就是函数体,除了大括号的那一部分就被称之为函数头。

如果只有函数的声明,而没有函数的定义,那么程序将会在链接时出错

void fun1() {} // 该行是对函数 fun1 进行的定义
void fun2(); // 该行只是函数声明,并没有对该函数进行定义

int main() {
	return 0;
}

// 如下代码是对 fun2 函数进行具体的定义,如果不定义 fun2, 会在链接环节出现错误。
void fun2() {
	// statements here...
}

函数的参数

在定义函数的时候,可以在小括号中定义一系列变量,这些变量被称之为函数的参数。

这一系列变量就构成了函数的参数列表,它定义了该函数可以接纳那些参数的输入。

参数列表和函数名一起构成了函数签名。

形式参数和实际参数

形式参数

  • 形参出现在被调函数当中,在整个函数体内都可以使用。

  • 形参在定义时编译系统并不分配存储空间,只有在调用该函数时才分配内存单元。

  • 调用结束内存单元被释放,故形参只有在函数调用时有效,调用结束时不能再使用。

实际参数

实参出现在主调函数当中,当函数调用时,主调函数把实参的值传送给被调函数的形参,从而实现函数间的数据传递。

  • 实参与形参必须个数相同
  • 对应的形参和实参的类型必须一致

实参的传递方式

在调用函数的时候,数据从实参传递给形参,可以使得函数在内部读取或使用函数外部的变量的数据。传递的方式主要有两种:传值和传址

传值(值传递)

值传递的特点是将实参的数据拷贝给形参,形参的修改并不会影响到实参本身,因为修改的是不同的地址。

传址(址传递)

址传递的特点是将实参的地址传递给形参,形参的修改会同步影响到实参本身,因为修改的是相同的地址。

数组作为实参进行传递

传递数组元素

数组元素(也称之为下标变量)作为函数的参数进行的数据传递是值传递方式。

传递数组名称

数组名(即数组首地址)、数组元素的地址(&arr[0])作为函数参数进行的数据传递是地址传递方式。

#include <stdio.h>

void func(int *p) {
	printf("p=%p\n", p);
}
int main() {
	int ns[] = {3,4,5};
	func(ns);
	func(&ns); // warning: expected 'int *' but argument is of type 'int (*)[3]'
	func(&ns[0]);
	return 0;
}

上述三种传递的方式,形参 p 接收到的是同一个地址,如果 p 修改了地址中的值,ns 中的值也会跟着变化。

函数的返回和嵌套

返回值类型:如果函数需要返回一个值,则需要为其指定具体的类型

函数的嵌套:定义函数时不能定义另一个函数,但是可以进行嵌套调用函数。

  • 如果需要从调用函数带回一个函数值(供主函数使用),被调函数中需包含return语句
  • 函数的返回值通过函数中的 return 语句传递到调用行。
  • 在定义函数时要指定函数值的类型,函数类型决定返回值的类型
int func(int a, int b) {
	return a + b;
}

函数的递归

函数的递归调用是指:一个函数在他的函数体内直接或间接地调用它自身。

分为:直接递归(函数直接调用自身)和间接递归(函数通过其他函数调用自身)。

可分为“回溯”和“递推”两个阶段。

#include<stdio.h>

// 阶乘:一个正整数的阶乘是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。
int factorial(int a) {
	if(a < 2) {
		return 1;
	}
	return a * factorial(a - 1);
}

int main() {	
	printf("5!=%d\n", factorial(5));
    return 0;
}

变量的作用域

局部变量

首先,它是一个变量,其次,这个变量只是在程序的局部范围内有效;局部变量定义可以定义在如下位置:

  1. 函数的开头;
  2. 函数内的复合语句内定义;
  3. 形式参数;
  4. 函数中间(非开头);

程序执行到某个函数时,这个函数内部的局部变量将会被分配内存空间;局部变量在函数执行结束后,变量所占内存将会被释放;

全局变量

首先,它是变量,其次,它可以在全局范围内是有意义的变量;

所谓全局也并不是真正的全局,而是在定义处以下的范围内才是有效的;全局变量定义的位置:

  1. 文件开头;
  2. 函数前;
  3. 函数后;
  4. 文件结尾;

注意事项

  • 为了区别全局变量和局部变量,往往大家在写程序的时候都喜欢将全局变量的首字母大写,而局部变量的首字母小写;

  • 全局变量的优点和缺点:

    **优点:**C语言的函数,每次最多只能返回一个值,但是如果定义了全局变量,那么在这个变量的有效范围内,很多函数都能改变这个变量的值,所以增加了函数之间的联系,通过函数的调用可以得到一个或一个以上的值;
    缺点:(大量使用全局变量的情况下)

    1. 占内存:全局变量所占的内存空间不会像局部变量一样会被释放;
    2. 降低程序清晰性:无法随时确定定义的全局变量的值的大小;
    3. 降低通用性:程序设计时要求函数的“内聚性”强,函数与函数之间“耦合性”弱;定义全局变是一定要注意在有效范围内变量不能重名,并且当全局变量被跨文件调用的函数调用时,不能出现全局变量与所跨文件中存在重名变量,否则有可能会出错;所以,为了提高程序的可靠性,可移植性和可读性等,全局变量尽量少用;

头文件

#include

  • #include 是C语言的预处理指令之一,在编译之前做的处理,预处理指令一般以 #开头
  • #include 指令后面会跟着一个文件名,预处理器发现 #include 指令后,就会根据文件名去查找文件,并把这个文件的内容包含到当前文件中。
  • 被包含文件中的文本将替换源文件中的 #include 指令,就像是把被包含文件中的全部内容拷贝到这个 #include 指令所在的位置一样。
  • 所以第一行的 #include <stdio.h> 指令的作用是将 stdio.h 文件里面的所有内容拷贝到第一行中。
  • 如果被包含的文件拓展名为 .h,我们称之为 头文件 (Header File)
  • 头文件可以用来声明函数,要想使用这些函数,就必须先用 #include 指令包含函数所在的头文件
  • #include 指令不仅仅限于 .h 文件,可以包含任何编译器能识别的 C/C++ 代码文件,如 .c.hpp.cpp 等,甚至 .txt.abc 等文本文件都可以
  • #include 使用的是相对路径,也可以使用绝对路径。比如#include "/Users/apple/Desktop/my.txt"

#include <> #include ""的区别

二者的区别在于:当被include的文件路径不是绝对路径的时候,有不同的搜索顺序。

  1. 对于使用双引号 ""include 文件,搜索的时候按以下顺序:
  • 先在这条 include 指令所在的文件的所在文件夹内搜索
  • 如果上一步找不到,则在该文件的父级目录内搜索;
  • 如果上一步找不到,则在编译器设置的 include 路径内搜索;
  • 如果上一步找不到,则在系统的 INCLUDE 环境变量内搜索
  1. 对于使用尖括号 <>include 文件,搜索的时候按以下顺序:
  • 在编译器设置的 include 路径内搜索;
  • 如果上一步找不到,则在系统的 INCLUDE 环境变量内搜索

头文件的引用

一般地,系统提供的头文件用 < > 引用, 自己写的用 " " 引用。

  • include 是可以包含 .c 源文件的,在某些工程里可以看到,但是这样的做法不常见也不推荐;
  • include 关键字包含 .c 源文件和 .h 头文件,理解都是一样的,在原地将引用的文件展开;

头文件的内容

头文件里一般包括宏定义, 全局变量, 函数原型声明。

头文件名的格式为 "_头文件名_" ,注意要大写

#ifndef 头文件名 	
#define 头文件名

头文件内容

#endif

示例

头文件:app.h

#ifndef _APP_H 	
#define _APP_H

void func(int n);

#endif

源文件:app.c

#include <stdio.h>
#include "app.h"

int main() {
	func(10);
	return 0;
}

void func(int n) {
	printf("n=%d\n", n);
}

可以使用 gcc app.c 即可对其进行编译,#include 指令会自动找到同目录下的 .h 文件

内部函数和外部函数

函数的调用,一般是对同一个源文件中的其他函数进行调用的,也可以对另外一个源文件中的函数进行调用
C语言中,根据函数能否被其他源文件调用,分为内部函数和外部函数
外部函数,可以被其他源文件调用的函数
内部函数,只在定义的文件中有效

外部函数 (extern)

定义外部函数的方式,在函数的返回值类型前面添加 extern 关键字

extern int add(int x,int y);

举例来说外部函数:

  • 第一个源文件 other.c
#include <stdio.h>

int add(int a, int b) {
	int c = a + b;
	printf("call add(%d,%d)=%d\n", a, b, c);
	return a + b;
}
  • 第二个源文件 app.c
// 该行的 extern 可省略
// 该行也可以直接删除,但不显式声明 add 函数的话,编译器会报警告
extern int add(int x,int y); 

int main() {
	add(3,4);
	return 0;
}

在调用方的源文件中,可以不用 #include 指令来引入定义方的源文件,使用 gcc 对这两个文件进行编译即可:

gcc other.c app.c

编译器通过 extern 关键字会明确地知道,add 函数是定义在其他文件中的外部函数。

在本例中,app.c 作为调用方,省略 extern 关键字也可以正常运行。但是必须在调用方的源文件中声明需要调用的函数的声明,否则会在编译时报警告,但依然能输出调用结果,如下所示:

app.c: In function 'main':
app.c:6:2: warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]
  add(3,4);
  ^~~

内部函数 (static)

只在定义的文件中有效,这类函数称为内部函数。

在定义内部函数时,需要在函数的返回值类型前面添加 static 关键字,也称静态函数。

举个例子来说明内部函数:

  • 第一个源文件 other.c
#include <stdio.h>

void func() {
	printf("func in other.c\n");
}
  • 第二个源文件 app.c
#include <stdio.h>

static void func() {
	printf("func in app.c\n");
}

int main() {
	func();
	return 0;
}

在调用方的源文件中,可以不用 #include 指令来引入定义方的源文件,使用 gcc 对这两个文件进行编译即可:

gcc other.c app.c

编译器通过 static 关键字会明确地知道,func 函数是仅供内部使用的。故而在 main 方法中调用 func 函数的时候,输出的结果自然是 func in app.c

如果将第二个文件中的 static 去掉,会在编译过程中抛出 multiple definition 的错误信息。致使无法得到最后的目标程序。

如果在 app.c 中直接删除 func 函数的定义,输出的结果就会是 func in other.c 。但同时也会出现警告,因为 main 中的 func 是作为外部函数来进行调用的,而 app.c 中又不存在该函数的声明。

思考:如果在 other.c 中定义一个函数,默认是可以在 app.c 中作为外部函数进行调用的,那使用 static 修饰后,在 app.c 中又显示注明需要调用外部函数,会出现什么结果呢?

  • 第一个源文件 other.c
#include <stdio.h>

static void func() {
	printf("func in other.c\n");
}
  • 第二个源文件 app.c
extern void func();

int main() {
	func();
	return 0;
}

执行编译后,出现 undefined reference to func' 的异常,编译不通过,删除 other.c 中的 static 关键字删除后,就可以正常编译通过了。编译器通过 extern 关键字知道调用方源文件中需要使用外部函数 func ,但是能够在 other.c 中被找到的 func 函数使用了 static 关键字修饰,也就不能允许外部源文件调用。故而将该函数视作是未定义进而抛出编译时异常。

数组

数组是一组相同类型元素的集合。

若将有限个类型相同的变量的集合命名,那么这个名称为数组名。

组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。

用于区分数组的各个元素的数字编号称为下标,下标从零开始。

数组的创建

一维数组创建示例:

int ns[5];
// 数组创建,[]中要给一个常量才可以,不要使用变量。
const int N = 10;
int ms[N];

数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定。

char cs1[] = "abc";
char cs2[3] = {'a','b','c'};

数组的使用

使用下标引用操作符 [] 能对数组中的元素进行索引(下标)访问。

使用 sizeof 可以量出数组的长度和某个元素的长度,以此来计算数组的长度。

int ns[] = {2, 5, 4};
int len = sizeof(ns) / sizeof(ns[0]);
printf("sizeof(ns)=%d\n", sizeof(ns));
printf("sizeof(ns[0])=%d\n", sizeof(ns[0]));
printf("len=%d\n", len);
  1. 数组的大小可以通过计算得到。
  2. 数组在内存中是连续存放的。

二维数组

二维数组可以视作是一个一维数组嵌套一个一维数组。表现形式上可以理解为是一个矩阵。

int ns1[3][4];
// ns1 可视作 3x4 的矩阵,如下所示:
// □ □ □ □
// □ □ □ □
// □ □ □ □
int ns2[2][3] = {3,5,7};
// ns2 可视作 2x3 的矩阵,如下所示:
// 3 5 7
// □ □ □
int ns3[3][3] = {{2,4},{6}};
// ns3 可视作 3x3 的矩阵,如下所示:
// 2 4 □
// 6 □ □
// □ □ □

// 双层循环以遍历二维数组
for(int i=0;i<3;i++) {
    for(int j=0;j<3;j++) {
        printf("(%d,%d)=%d\t", i, j, ns3[i][j]);
    }
    printf("\n");
}

数组越界

  • 数组的下标是有范围限制的。
  • 数组的下规定是从0开始的,如果输入有n个元素,最后一个元素的下标就是n-1。
  • 所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。
  • C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就是正确的,所以程序员写代码时,最好自己做越界的检查。
  • 二维数组的行和列也可能存在越界。

变长数组

数组变量本身表达的就是地址。在 C 中,数组变量就是一个特殊常量指针,也称之为数组指针。

可以利用 malloc 进行动态创建指定长度的数组,如下所示:

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

int main(void) {
    int size = 12, i=0;
    int* const p = (int*)malloc(size*(sizeof(int)));
	
	// 为所有下标位置赋值
    for (i=0;i<size; i++) {
        p[i] = i;
    }
	// 输出所有下标位置的值
    for (i = 0; i <size; i++) {
        printf("%d,", p[i]);
    }
	
    free(p);
    return 0;
}

指针

在计算机科学中,指针(Pointer)是编程语言中的一个对象。

它的值直接指向(points to)存在电脑存储器中另一个地方的值。

由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。

因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。

指针的声明

指针是一个变量,其值是另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:type* var-name; 如下例所示:

#include<stdio.h>

int main() {
	// 声明一个 int 类型的指针
	int* ip;
	// 声明一个 long 类型的指针
	long *lp;
	// 声明一个 double 类型的指针
	double* dp;
	// 声明一个 float 类型的指针
	float* fp;

	printf("size of int pointer is %d.\n", sizeof(ip));
	printf("size of long pointer is %d.\n", sizeof(lp));
	printf("size of double pointer is %d.\n", sizeof(dp));
	printf("size of float pointer is %d.\n", sizeof(fp));
	
	return 0;
}

执行结果:

size of int pointer is 4.
size of long pointer is 4.
size of double pointer is 4.
size of float pointer is 4.

在声明的时候,* 无论是偏向数据类型的关键字还是偏向变量名称,都是被认为是合法的。也就是说:int *ip;int* ip; 都是正确的声明方式。但是当多个变量在同一个声明语句时候,需要使用 *ip 的形式。例如:

int *a, *b;		// 声明两个 int 类型的指针变量,分别是 a 和 b
int a, *b, c;	// 声明一个 int 类型的指针变量 b,变量 a 和 c 都是普通的 int 变量。

因为指针变量存储的值是一个地址值,所以,无论什么类型的指针,都不会影响其本身需要占用内存的空间。由于指针变量接受的是地址值,所以,在给指针变量赋值的时候需使用到取址符 &,如下例所示:

#include<stdio.h>

int main() {
	int number = 10;
	int* ip = &number;
	
	printf("number is %d\n", number);
	printf("&number is %d\n", &number);
	
	printf("ip is %d\n", ip);
	printf("&ip is %d\n", &ip);
	printf("*ip is %d\n", *ip);
	
	return 0;
}

执行结果:

number is 10
&number is 2293564
ip is 2293564
&ip is 2293560
*ip is 10

既然指针也是变量,那根据上面得到的结果对照到表格中来看:

变量名称 变量的类型 变量的值 变量的地址 为变量赋值
int number = 10; number int 10 2293564 number = 10
int* ip = &number; ip int* 2293564 2293560 ip = &number

指针是一种特殊的变量,其存储的数据不是一个可以直接被人识别的数字,或者文本,而是某个其他变量的内存地址值。

指针的简单应用

借助指针,可以对该内存地址的数据进行读写。如如下例:

#include<stdio.h>

int main() {
	int a = 10;
	int* ip = &a;
	
	printf("a  is %d,\t &a  is %d\n", a, &a);
	printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);
	
	// 修改指针所指向的内存地址(2293564)中的值
	*ip = 100;
	
	printf("a  is %d,\t &a  is %d\n", a, &a);
	printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);
		
	return 0;
}

执行结果:

a  is 10,        &a  is 2293564
ip is 2293564    &ip is 2293560          *ip is 10
a  is 100,       &a  is 2293564
ip is 2293564    &ip is 2293560          *ip is 100

这样不需要变量 a 就能实现修改变量 a 所存储的值。

在执行过程中,也可以修改指针的存储的地址值为其他的变量。如下所示:

#include<stdio.h>

int main() {
	int a = 10, b = 20;
	int* ip = &a;
	
	printf("a  is %d,\t &a  is %d\n", a, &a);
	printf("b  is %d,\t &b  is %d\n", b, &b);
	printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);
	*ip *= 2;
	printf("a  is %d,\t &a  is %d\n", a, &a);
	printf("b  is %d,\t &b  is %d\n", b, &b);
	printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);
	ip = &b;
	*ip *= 3;
	printf("a  is %d,\t &a  is %d\n", a, &a);
	printf("b  is %d,\t &b  is %d\n", b, &b);
	printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);
	
	return 0;
}

执行结果:

a  is 10,        &a  is 2293564
b  is 20,        &b  is 2293560
ip is 2293564    &ip is 2293556          *ip is 10
a  is 20,        &a  is 2293564
b  is 20,        &b  is 2293560
ip is 2293564    &ip is 2293556          *ip is 20
a  is 20,        &a  is 2293564
b  is 60,        &b  is 2293560
ip is 2293560    &ip is 2293556          *ip is 60

这里的指针 ip 先指向的是变量 a 的地址,在对该地址的数据进行累乘操作后,指向了变量 b 的地址,又对变量 b 的地址中的值进行了累乘操作。最终 a 的值被乘以 2,b 的值被乘以3。

NULL 指针

如果在声明一个指针的时候,没有为其赋予确切的地址值,那指针可能会指向一个未知的地址。如下例所示:

#include<stdio.h>

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

执行结果:

a=2293540, *a=4200720, b=2147344384, *b=0

在没有为指针变量 a 和 b 赋予地址值的时候,既然还能有值,并且能读取到该地址里的值。因为一旦指针存储了地址值后,可以对该地址进行修改操作,可能会出现一些不该出现的现象,比如某个地方的变量的值因为这个指针的原因被修改了之类的。

如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值作为初始值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。

因为在 C 中,把任何非零和非空的值假定为 true,把零或 null 假定为 false,所以,当一个指针是空指针的时候,是可以被判断的。这也有利于后面更加顺利地操作指针。代码如下:

#include<stdio.h>

int main() {
	int a = 10;
	int* ip = NULL;
	printf("a  is %d,\t &a  is %d\n", a, &a);
	
	if(ip) {
		printf("ip is not null pointer.\n");
	} else {
		printf("ip is null pointer.\n");
		ip = &a;
	}
	*ip *= 2;
	printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);
	
	return 0;
}

执行结果:

a  is 10,        &a  is 2293564
ip is null pointer.
ip is 2293564    &ip is 2293560          *ip is 20

当然,在该程序中 ip 自然是为空指针,但当 ip 作为一个函数参数的时候,对指针判空的处理就很重要了,因为不知道外部传递的指针到底是否是空的。

指针的算术运算

指针是一种存储其他变量地址值的特殊变量,同时也能进行简单的算数运算和逻辑运算。运用自运算遍历数组一种常见的指针应用。

#include<stdio.h>

int main() {
	int i, size = 7;
	int nums[7] = {101, 202, 303, 404, 505, 606, 707};
	int *p = NULL;
	p = nums;
	for(i = 0; i < size; i++) {
		printf("nums[%d] = %d, p==%d, *p=%d\n", i, nums[i], p, *p);
		p++;
	}
	return 0;
}

执行结果:

nums[0] = 101, p==2293528, *p=101
nums[1] = 202, p==2293532, *p=202
nums[2] = 303, p==2293536, *p=303
nums[3] = 404, p==2293540, *p=404
nums[4] = 505, p==2293544, *p=505
nums[5] = 606, p==2293548, *p=606
nums[6] = 707, p==2293552, *p=707

因为数组变量在内存中是以连续的内存空间来存储数据的,故而,p = nums; 等价于 p = &nums[0]; 也就是说,将数组 nums 的地址值赋予给指针,等价于将数组中第一个元素的地址值赋值给指针。

从输出的结果上来看也会发现,连续的每个元素的地址相差 4 位,这个值正是在开篇使用 sizeof 量出来的指针的大小是一致的。

那既然 p = nums; 等价于 p = &nums[0]; 那将数组中最后一个元素的地址值赋予给指针做自减运算输出会怎么样。代码如下:

#include<stdio.h>

int main() {
	int i, size = 7;
	int nums[7] = {101, 202, 303, 404, 505, 606, 707};
	int *p = NULL;
	p = &nums[size - 1];
	for(i = size - 1; i > -1; i--, p--) {
		printf("nums[%d] = %d, p==%d, *p=%d\n", i, nums[i], p, *p);
	}
	return 0;
}

执行结果:

nums[6] = 707, p==2293552, *p=707
nums[5] = 606, p==2293548, *p=606
nums[4] = 505, p==2293544, *p=505
nums[3] = 404, p==2293540, *p=404
nums[2] = 303, p==2293536, *p=303
nums[1] = 202, p==2293532, *p=202
nums[0] = 101, p==2293528, *p=101

因为数组中每个元素的地址是连续的,当指针指向第一个元素的时候,只要指针指向的地址没有超过最后一位,就能进行循环取值。这里就需要对指针中存储的地址值进行比较,示例代码如下:

#include<stdio.h>

int main() {
	int size = 7;
	int nums[7] = {101, 202, 303, 404, 505, 606, 707};
	int *p = nums; // 等价于 &nums[0]
	while(p <= &nums[size - 1]) {
		printf("%d ", *p);
		p++;
	}
	return 0;
}

执行结果:

101 202 303 404 505 606 707

指针数组

指针数组往往是用于存储一系列相同类型的指针的集合。其声明的一般形式为:type* var-name[size];,例如:

int* ps[3];

在理解指针的时候,可以类比变量,同样的,理解指针数组可以类比数组。

数组名称 数组中每个元素的类型 获取下标为0的元素的值 获取下标为0的元素的地址值 为下标为0的元素赋值
int nums[3]; nums int nums[0] &nums[0] nums[0] = 10;
int* ps[3]; ps int* ps[0] &ps[0] ps[0] = &nums[0]

这样一来,很容易理解指针数组了,它就是一系列指针的集合。他们不一定是连续的,就像数组中的数据不一定是连续的数字的道理一样。这里使用一个案例来对指针数组和普通数组进行对比:

#include<stdio.h>

int main() {
	int i, a = 101, b = 202, c = 303, size = 3;
	
	int nums[3];
	nums[0] = a;
	nums[1] = b;
	nums[2] = c;
	
	int* ps[3];
	ps[0] = &a;
	ps[1] = &b;
	ps[2] = &c;
	
	// 输出各个变量的值
	for(i = 0; i < size; i++) {
		printf("nums[%d]=%d, &nums[%d]=%d\n", i, nums[i], i, &nums[i]);
	}	
	printf("\na=%d, &a=%d, b=%d, &b=%d, c=%d, &c=%d\n\n", a, &a, b, &b, c, &c);
	for(i = 0; i < size; i++) {
		printf("ps[%d]=%d, *ps[%d]=%d, &ps[%d]=%d\n", i, ps[i], i, *ps[i], i, &ps[i]);
	}
	
	// 修改指针指向的地址中的值
	for(i = 0; i < size; i++) {
		*ps[i] *= 10;
	}
	
	// 再次输出各个变量的值
	printf("\n------------------\n");
	for(i = 0; i < size; i++) {
		printf("nums[%d]=%d, &nums[%d]=%d\n", i, nums[i], i, &nums[i]);
	}	
	printf("\na=%d, &a=%d, b=%d, &b=%d, c=%d, &c=%d\n\n", a, &a, b, &b, c, &c);
	for(i = 0; i < size; i++) {
		printf("ps[%d]=%d, *ps[%d]=%d, &ps[%d]=%d\n", i, ps[i], i, *ps[i], i, &ps[i]);
	}
	
	return 0;
}

执行结果:

nums[0]=101, &nums[0]=2293536
nums[1]=202, &nums[1]=2293540
nums[2]=303, &nums[2]=2293544

a=101, &a=2293556, b=202, &b=2293552, c=303, &c=2293548

ps[0]=2293556, *ps[0]=101, &ps[0]=2293524
ps[1]=2293552, *ps[1]=202, &ps[1]=2293528
ps[2]=2293548, *ps[2]=303, &ps[2]=2293532

------------------
nums[0]=101, &nums[0]=2293536
nums[1]=202, &nums[1]=2293540
nums[2]=303, &nums[2]=2293544

a=1010, &a=2293556, b=2020, &b=2293552, c=3030, &c=2293548

ps[0]=2293556, *ps[0]=1010, &ps[0]=2293524
ps[1]=2293552, *ps[1]=2020, &ps[1]=2293528
ps[2]=2293548, *ps[2]=3030, &ps[2]=2293532

从执行结果来看,指针数组中存储的三个指针,修改这三个指针对应地址的值,会影响变量 a, b, c,但是不影响数组 nums。因为在给数组赋值的时候,是将变量 a, b, c 的值赋予了数组 nums,也就是 传值。给指针数组赋值的时候,是将变量 a, b, c 的地址值赋予了 ps,也就是 传址。这样就更加好理解指针数组了,数组是一系列相同数据类型的数据的集合,而指针数组是一系列相同数据类型的指针的集合。

数组指针

在 C 中,数组到底是什么?

现象一:

在上文中的 指针的算术运算 段落中的内容可以知道:p = nums; 等价于 p = &nums[0];

分析:

赋值运算符 = 两边的数据类型在一致的时候才能将右值赋予左边的变量,如果不是因为数据类型发生了隐式转换,那就是符号两边的数据类型本就是一致的。这是不是能说明数组变量 nums 就是一个指针?如果是这样,那这个指针存储的值难道是数组中第一个元素的地址?如果是这样,那就能解释为什么在给指针 p 赋值的时候,nums 不需要 & 作为前缀了。

现象二:

回顾在之前的变量学习中,可以知道,变量的赋值可以是这样的:

int a = 10; int b = 20; b = a;

最终变量 b 的值变成了 20,那同样的代码,数组能这样使用吗?比如:

int a[] = {1, 2}; int b[] = {3, 4}; b = a;

程序在编译的时候会抛出一个错误信息:

error: assignment to expression with array type
  b = a;
    ^

分析:

为什么这里会提示数组类型的变量在声明之后不允许使用赋值表达式?这个现象和常量很相似,常量在定义完成之后,也是不允许为其赋值。这是不是能说明数组是一个常量?

解释:

在 C 中,数组变量就是一个特殊常量指针,也称之为数组指针。它有如下特点:

  1. 数组变量本身表达的就是地址。所以 nums == &nums[0]
  2. 运算符 [] 可以对数组做运算,也可以对指针做运算。所以 p[0] 等价于 nums[0]
  3. 运算符 * 可以对指针做运算,也可以对数组做运算。所以 *nums 是被允许的
  4. 数组变量是 const 的指针,所以不允许被赋值。也就是说 int nums[] 等价于 int * const nums

实例:

#include<stdio.h>

#define SIZE 6
int main() {

	int nums[SIZE] = {101, 202, 303, 404, 505, 606};
	int *p = nums;
	
	// nums 和 p 都能使用运算符 *
	printf("*p=%d, *nums=%d\n", *p, *nums);
	printf(" p=%p, nums=%p\n", p, nums);
	printf("&p=%p, &nums=%p\n", &p, &nums);
	
	// nums 和 p 都能使用逻辑运算符
	printf("p <= &nums[1] is %s\n", p <= &nums[1] ? "true" : "false");
	printf("nums < &nums[1] is %s\n", nums < &nums[1] ? "true" : "false");
	
	// nums 和 p 都能使用下标来操作元素
	for(int i = 0; i < SIZE; i++) {
		printf("nums[%d]=%d, &nums[%d]=%p, p[%d]=%d, &p[%d]=%p\n", i, nums[i], i, &nums[i], i, p[i], i, &p[i]);
	}
	
	// nums 是常量指针,故而不能做自运算
	while(p < &nums[SIZE - 1]) {
		p++;
		// error: lvalue required as increment operand
		// nums++;
		printf("p=%p, *p=%d, nums=%p, *nums=%d\n", p, *p, nums, *nums);
	}
	
	return 0;
}

执行结果:

*p=101, *nums=101
 p=0022FF24, nums=0022FF24
&p=0022FF20, &nums=0022FF24
p <= &nums[1] is true
nums < &nums[1] is true
nums[0]=101, &nums[0]=0022FF24, p[0]=101, &p[0]=0022FF24
nums[1]=202, &nums[1]=0022FF28, p[1]=202, &p[1]=0022FF28
nums[2]=303, &nums[2]=0022FF2C, p[2]=303, &p[2]=0022FF2C
nums[3]=404, &nums[3]=0022FF30, p[3]=404, &p[3]=0022FF30
nums[4]=505, &nums[4]=0022FF34, p[4]=505, &p[4]=0022FF34
nums[5]=606, &nums[5]=0022FF38, p[5]=606, &p[5]=0022FF38
p=0022FF28, *p=202, nums=0022FF24, *nums=101
p=0022FF2C, *p=303, nums=0022FF24, *nums=101
p=0022FF30, *p=404, nums=0022FF24, *nums=101
p=0022FF34, *p=505, nums=0022FF24, *nums=101
p=0022FF38, *p=606, nums=0022FF24, *nums=101

指向指针的指针

我们知道指针存储的是其他变量的的地址值,而指针本身也是一个变量,那一个指针指向的变量正好也是一个指针变量呢?这种情况被称之为指向指针的指针。

指向指针的指针在声明的时候必须比被指向的指针变量多一个星号,如下所示:

#include<stdio.h>

int main() {
	int num = 10;
	int *p = &num;
	int **ip = &p;
	int ***ipp = &ip;
	printf("num=%d, &num=%d\n", num, &num);
	printf("p=%d, *p=%d, &p=%d\n", p, *p, &p);
	printf("ip=%d, *ip=%d, &ip=%d\n", ip, *ip, &ip);
	printf("ipp=%d, *ipp=%d, &ipp=%d\n", ipp, *ipp, &ipp);
	return 0;
}

执行结果:

num=10, &num=2293564
p=2293564, *p=10, &p=2293560
ip=2293560, *ip=2293564, &ip=2293556
ipp=2293556, *ipp=2293560, &ipp=2293552

如果上例中的指针 ipp 指向指针 p 会在编译的时候抛出一个警告:

warning: initialization of 'int ***' from incompatible pointer type
 'int **' [-Wincompatible-pointer-types]
  int ***ipp = &p;
               ^

但是还是会编译通过,执行结果是:

num=10, &num=2293564
p=2293564, *p=10, &p=2293560
ip=2293560, *ip=2293564, &ip=2293556
ipp=2293560, *ipp=2293564, &ipp=2293552

指针 ipp 最终还是成功指向了指针 p,虽然这样做是可行的,但不建议这样去写。

如果按照正常的方式去编写,这种指向指针的指针能最多能写多少个星号呢?目前貌似没有找到相关资料来解释这个问题。就下面的案例来看,能写到至少 6 颗星。

#include<stdio.h>

int main() {
	int num = 10;
	int *p = &num;
	int **ip = &p;
	int ***ipp = &ip;
	int ****ippp = &ipp;
	int *****ipppp = &ippp;
	int ******ippppp = &ipppp;
	printf("num=%d, &num=%d\n", num, &num);
	printf("p=%d, *p=%d, &p=%d\n", p, *p, &p);
	printf("ip=%d, *ip=%d, &ip=%d\n", ip, *ip, &ip);
	printf("ipp=%d, *ipp=%d, &ipp=%d\n", ipp, *ipp, &ipp);
	printf("ippp=%d, *ippp=%d, &ippp=%d\n", ippp, *ippp, &ippp);
	printf("ipppp=%d, *ipppp=%d, &ipppp=%d\n", ipppp, *ipppp, &ipppp);
	printf("ippppp=%d, *ippppp=%d, &ippppp=%d\n", ippppp, *ippppp, &ippppp);
	return 0;
}

执行结果:

num=10, &num=2293564
p=2293564, *p=10, &p=2293560
ip=2293560, *ip=2293564, &ip=2293556
ipp=2293556, *ipp=2293560, &ipp=2293552
ippp=2293552, *ippp=2293556, &ippp=2293548
ipppp=2293548, *ipppp=2293552, &ipppp=2293544
ippppp=2293544, *ippppp=2293548, &ippppp=2293540

字符串

在 C 语言中,字符串实际上是使用 null 字符 ‘\0’ 终止的一维字符数组。

因此,一个以 null 结尾的字符串,包含了组成字符串的字符。

字符

关键字 char 可以用来声明一个字符变量。char 变量既是一种整数(最小的整数类型),也是一种特殊的字符类型。如下所示:

#include<stdio.h>

int main() {
	char c = 'A';
	printf("c=%d, c=%c", c, c);
	return 0;
}

执行结果:

c=65, c=A

可以发现,字符 A 在输出的时候,既可以使用数字格式输出,也可以使用字符格式输出,原因是它的数据类型是 char 类型,是一种既是整型又是字符的数据类型。既然是整型,那就能进行算数运算。如下例:

#include<stdio.h>

int main() {
	char c = 'A' + 1;
	printf("c=%d, c=%c", c, c);
	return 0;
}

执行结果:

c=66, c=B

实际上,char 能将字符 A 进行算数运算的原因是它的每个字符都有对应的无符号整型的数据与之对应。从这里的例子中可以知道,字符 A 对应的整数是 65,字符 B 对应的整数就是 66,如果进行 'Z' - 'A' 的运算,能得到与整数 26对应的字符。

在计算机早期,所有的数据在存储和运算时都要使用二进制数表示(因为计算机用高电平和低电平分别表示1和0),例如,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示。

而具体用哪些二进制数字表示哪个符号的问题,美国国家标准学会(American National Standard Institute , ANSI)制定了一套标准的单字节字符编码方案,用于基于文本的数据。

它最初是美国国家标准,供不同计算机在相互通信时用作共同遵守的西文字符编码标准,后来它被国际标准化组织(International Organization for Standardization, ISO)定为国际标准,称为ISO 646标准。适用于所有拉丁文字字母。

—— 摘自百度百科

这种字符和整数对应关系的标准被称之为 ASCII (American Standard Code for Information Interchange),即美国信息交换标准代码,ASCII 码表的具体内容如下:

二进制 八进制 十进制 十六进制 缩写/字符 解释
0000 0000 0 0 0x00 NUL(null) 空字符
0000 0001 1 1 0x01 SOH(start of headline) 标题开始
0000 0010 2 2 0x02 STX (start of text) 正文开始
0000 0011 3 3 0x03 ETX (end of text) 正文结束
0000 0100 4 4 0x04 EOT (end of transmission) 传输结束
0000 0101 5 5 0x05 ENQ (enquiry) 请求
0000 0110 6 6 0x06 ACK (acknowledge) 收到通知
0000 0111 7 7 0x07 BEL (bell) 响铃
0000 1000 10 8 0x08 BS (backspace) 退格
0000 1001 11 9 0x09 HT (horizontal tab) 水平制表符
0000 1010 12 10 0x0A LF (NL line feed, new line) 换行键
0000 1011 13 11 0x0B VT (vertical tab) 垂直制表符
0000 1100 14 12 0x0C FF (NP form feed, new page) 换页键
0000 1101 15 13 0x0D CR (carriage return) 回车键
0000 1110 16 14 0x0E SO (shift out) 不用切换
0000 1111 17 15 0x0F SI (shift in) 启用切换
0001 0000 20 16 0x10 DLE (data link escape) 数据链路转义
0001 0001 21 17 0x11 DC1 (device control 1) 设备控制1
0001 0010 22 18 0x12 DC2 (device control 2) 设备控制2
0001 0011 23 19 0x13 DC3 (device control 3) 设备控制3
0001 0100 24 20 0x14 DC4 (device control 4) 设备控制4
0001 0101 25 21 0x15 NAK (negative acknowledge) 拒绝接收
0001 0110 26 22 0x16 SYN (synchronous idle) 同步空闲
0001 0111 27 23 0x17 ETB (end of trans. block) 结束传输块
0001 1000 30 24 0x18 CAN (cancel) 取消
0001 1001 31 25 0x19 EM (end of medium) 媒介结束
0001 1010 32 26 0x1A SUB (substitute) 代替
0001 1011 33 27 0x1B ESC (escape) 换码(溢出)
0001 1100 34 28 0x1C FS (file separator) 文件分隔符
0001 1101 35 29 0x1D GS (group separator) 分组符
0001 1110 36 30 0x1E RS (record separator) 记录分隔符
0001 1111 37 31 0x1F US (unit separator) 单元分隔符
0010 0000 40 32 0x20 (space) 空格
0010 0001 41 33 0x21 ! 叹号
0010 0010 42 34 0x22 " 双引号
0010 0011 43 35 0x23 # 井号
0010 0100 44 36 0x24 $ 美元符
0010 0101 45 37 0x25 % 百分号
0010 0110 46 38 0x26 & 和号
0010 0111 47 39 0x27 闭单引号
0010 1000 50 40 0x28 ( 开括号
0010 1001 51 41 0x29 ) 闭括号
0010 1010 52 42 0x2A * 星号
0010 1011 53 43 0x2B + 加号
0010 1100 54 44 0x2C , 逗号
0010 1101 55 45 0x2D - 减号/破折号
0010 1110 56 46 0x2E . 句号
0010 1111 57 47 0x2F / 斜杠
0011 0000 60 48 0x30 0 字符0
0011 0001 61 49 0x31 1 字符1
0011 0010 62 50 0x32 2 字符2
0011 0011 63 51 0x33 3 字符3
0011 0100 64 52 0x34 4 字符4
0011 0101 65 53 0x35 5 字符5
0011 0110 66 54 0x36 6 字符6
0011 0111 67 55 0x37 7 字符7
0011 1000 70 56 0x38 8 字符8
0011 1001 71 57 0x39 9 字符9
0011 1010 72 58 0x3A : 冒号
0011 1011 73 59 0x3B ; 分号
0011 1100 74 60 0x3C < 小于
0011 1101 75 61 0x3D = 等号
0011 1110 76 62 0x3E > 大于
0011 1111 77 63 0x3F ? 问号
0100 0000 100 64 0x40 @ 电子邮件符号
0100 0001 101 65 0x41 A 大写字母A
0100 0010 102 66 0x42 B 大写字母B
0100 0011 103 67 0x43 C 大写字母C
0100 0100 104 68 0x44 D 大写字母D
0100 0101 105 69 0x45 E 大写字母E
0100 0110 106 70 0x46 F 大写字母F
0100 0111 107 71 0x47 G 大写字母G
0100 1000 110 72 0x48 H 大写字母H
0100 1001 111 73 0x49 I 大写字母I
1001010 112 74 0x4A J 大写字母J
0100 1011 113 75 0x4B K 大写字母K
0100 1100 114 76 0x4C L 大写字母L
0100 1101 115 77 0x4D M 大写字母M
0100 1110 116 78 0x4E N 大写字母N
0100 1111 117 79 0x4F O 大写字母O
0101 0000 120 80 0x50 P 大写字母P
0101 0001 121 81 0x51 Q 大写字母Q
0101 0010 122 82 0x52 R 大写字母R
0101 0011 123 83 0x53 S 大写字母S
0101 0100 124 84 0x54 T 大写字母T
0101 0101 125 85 0x55 U 大写字母U
0101 0110 126 86 0x56 V 大写字母V
0101 0111 127 87 0x57 W 大写字母W
0101 1000 130 88 0x58 X 大写字母X
0101 1001 131 89 0x59 Y 大写字母Y
0101 1010 132 90 0x5A Z 大写字母Z
0101 1011 133 91 0x5B [ 开方括号
0101 1100 134 92 0x5C \ 反斜杠
0101 1101 135 93 0x5D ] 闭方括号
0101 1110 136 94 0x5E ^ 脱字符
0101 1111 137 95 0x5F _ 下划线
0110 0000 140 96 0x60 ` 开单引号
0110 0001 141 97 0x61 a 小写字母a
0110 0010 142 98 0x62 b 小写字母b
0110 0011 143 99 0x63 c 小写字母c
0110 0100 144 100 0x64 d 小写字母d
0110 0101 145 101 0x65 e 小写字母e
0110 0110 146 102 0x66 f 小写字母f
0110 0111 147 103 0x67 g 小写字母g
0110 1000 150 104 0x68 h 小写字母h
0110 1001 151 105 0x69 i 小写字母i
0110 1010 152 106 0x6A j 小写字母j
0110 1011 153 107 0x6B k 小写字母k
0110 1100 154 108 0x6C l 小写字母l
0110 1101 155 109 0x6D m 小写字母m
0110 1110 156 110 0x6E n 小写字母n
0110 1111 157 111 0x6F o 小写字母o
0111 0000 160 112 0x70 p 小写字母p
0111 0001 161 113 0x71 q 小写字母q
0111 0010 162 114 0x72 r 小写字母r
0111 0011 163 115 0x73 s 小写字母s
0111 0100 164 116 0x74 t 小写字母t
0111 0101 165 117 0x75 u 小写字母u
0111 0110 166 118 0x76 v 小写字母v
0111 0111 167 119 0x77 w 小写字母w
0111 1000 170 120 0x78 x 小写字母x
0111 1001 171 121 0x79 y 小写字母y
0111 1010 172 122 0x7A z 小写字母z
0111 1011 173 123 0x7B { 开花括号
0111 1100 174 124 0x7C | 垂线
0111 1101 175 125 0x7D } 闭花括号
0111 1110 176 126 0x7E ~ 波浪号
0111 1111 177 127 0x7F DEL (delete) 删除

所以,char 类型的数据对应的整型数据的取值范围应该在 [0, 127] 之间。使用循环可以将其全部输出并查看:

#include<stdio.h>

int main() {
	for(char c = 0; c < 128; c++) {
		printf("c=%d, c=%c\n", c, c);
		if(c < 0) {
			break;
		}
	}
	return 0;
}

因为 char 的最大值是 127, 如果当变量 c 是 127 的时候,再执行自增操作,则 c 的最高位会被进位为 1,就被解释为负数了,故而,需要在循环体中做判断 c < 0 就跳出循环的操作,否则该循环将成为无限循环。

C 的字符串

很多资料在描述 C 的字符串的时候,说在 C 中,字符数组就是字符串,其实不完全正确。根据定义,字符串实际上是使用 null 字符 ‘\0’ 终止的一维字符数组。换句话说,如果一个字符数组中的最后一个元素不是 NULL 字符,那这个字符数组不能称之为字符串。示例如下:

#include<stdio.h>

int main() {
	char a[] = {'H', 'e', 'l', 'l', 'o'};
	char b[] = {'H', 'e', 'l', 'l', 'o', '\0'};
	char c[] = "Hello";
	printf("sizeof(a) is %d, &a=%p, a=%s\n", sizeof(a), &a, a);
	printf("sizeof(b) is %d, &b=%p, b=%s\n", sizeof(b), &b, b);
	printf("sizeof(c) is %d, &c=%p, c=%s\n", sizeof(c), &c, c);
	return 0;
}

执行结果:

sizeof(a) is 5, &a=0022FF3B, a=Hello
sizeof(b) is 6, &b=0022FF35, b=Hello
sizeof(c) is 6, &c=0022FF2F, c=Hello

这里的变量 a, b, c 都是字符数组,但变量 a 是不能被称之为是 C 的字符串的,因为根据定义,它的最后一个元素的不是 NULL 字符。

那为什么变量 c 能被称之为是字符串呢?原因是在编写源程序的时候,不需要把 null 字符放在字符串常量的末尾。编译器会在初始化数组时,自动把 '\0' 放在字符串的末尾。也就是说,C 在编译的时候,将字符串字面量 "Hello" 分解成了一个以 NULL 字符结尾的字符数组。这也就是为什么使用 sizeof 运算变量 c 的时候,得到的值是 6 的原因,因为在字符 o 的后面还有一个 NULL 字符。可以简单的理解为 "Hello" 等价于 {'H', 'e', 'l', 'l', 'o', '\0'}

在之前以 指针 为主题的文章中说:在 C 中,数组变量就是一个特殊常量指针,也称之为数组指针。那也就是说在上面的例子中,变量 a, b, c 也都是指针。换句话说,字符串变量也是一个指针变量

同样的,把之前数组指针的案例修改下,更改数据类型为 char,然后增加输出格式 %c。得到如下代码:

#include<stdio.h>

#define SIZE 6

int main() {

	char cs[SIZE] = {'H', 'e', 'l', 'l', 'o', '\0'};
	char *p = cs;
	
	// cs 和 p 都能使用运算符 *
	printf("*p=%d, *cs=%d, *cs=%c\n", *p, *cs, *cs);
	printf(" p=%p, cs=%p\n", p, cs);
	printf("&p=%p, &cs=%p\n", &p, &cs);
	
	// cs 和 p 都能使用逻辑运算符
	printf("p <= &cs[1] is %s\n", p <= &cs[1] ? "true" : "false");
	printf("cs < &cs[1] is %s\n", cs < &cs[1] ? "true" : "false");
	
	// cs 和 p 都能使用下标来操作元素
	for(int i = 0; i < SIZE; i++) {
		printf("cs[%d]=%d, cs[%d]=%c, &cs[%d]=%p, p[%d]=%d, &p[%d]=%p\n", i, cs[i], i, cs[i], i, &cs[i], i, p[i], i, &p[i]);
	}
	
	// cs 是常量指针,故而不能做自运算
	while(p < &cs[SIZE - 1]) {
		p++;
		// error: lvalue required as increment operand
		// cs++;
		printf("p=%p, *p=%d, cs=%p, *cs=%d, *cs=%c\n", p, *p, cs, *cs, *cs);
	}
	
	return 0;
}

执行结果:

*p=72, *cs=72, *cs=H
 p=0022FF26, cs=0022FF26
&p=0022FF20, &cs=0022FF26
p <= &cs[1] is true
cs < &cs[1] is true
cs[0]=72, cs[0]=H, &cs[0]=0022FF26, p[0]=72, &p[0]=0022FF26
cs[1]=101, cs[1]=e, &cs[1]=0022FF27, p[1]=101, &p[1]=0022FF27
cs[2]=108, cs[2]=l, &cs[2]=0022FF28, p[2]=108, &p[2]=0022FF28
cs[3]=108, cs[3]=l, &cs[3]=0022FF29, p[3]=108, &p[3]=0022FF29
cs[4]=111, cs[4]=o, &cs[4]=0022FF2A, p[4]=111, &p[4]=0022FF2A
cs[5]=0, cs[5]= , &cs[5]=0022FF2B, p[5]=0, &p[5]=0022FF2B
p=0022FF27, *p=101, cs=0022FF26, *cs=72, *cs=H
p=0022FF28, *p=108, cs=0022FF26, *cs=72, *cs=H
p=0022FF29, *p=108, cs=0022FF26, *cs=72, *cs=H
p=0022FF2A, *p=111, cs=0022FF26, *cs=72, *cs=H
p=0022FF2B, *p=0, cs=0022FF26, *cs=72, *cs=H

从这个简单的案例中证明了,字符串变量也是一个指针变量。原因是因为数组是一个常量指针。那也就是说可以使用声明指针的形式来声明一个字符串。示例如下:

#include<stdio.h>

int main() {
	char *a = "Hello";
	printf("sizeof(a) is %d, &a=%p, a=%s\n", sizeof(a), &a, a);
	return 0;
}

执行结果:

sizeof(a) is 4, &a=0022FF3C, a=Hello

那如果声明两个相同的字符串指针变量,他们的值相同吗?代码如下:

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

int main() {
	char *a = "Hello";
	char *b = "Hello";
	if(a == b) {
		printf("a == b.\n");
	} else {
		printf("a != b.\n");
	}
	printf("a=%s, a=%p, &a[0]=%p, *a=%c, &a=%p\n", a, a, &a[0], *a, &a);
	printf("b=%s, b=%p, &b[0]=%p, *b=%c, &b=%p\n", b, b, &b[0], *b, &b);
	return 0;
}

执行结果:

a == b.
a=Hello, a=00405044, &a[0]=00405044, *a=H, &a=0022FF3C
b=Hello, b=00405044, &b[0]=00405044, *b=H, &b=0022FF38

从这个例子中可以知道的是,两个字符指针指向的地址是同一个地址,也就是说源程序中的两个字面量 Hello 在内存中使用同一个字符数组来进行存储的。换句话讲,这个例子中的变量 a 和变量 b 不是两个长得一样,而是它们就是同一个字符串。

综上可知,字符串的特点有:

  1. 字符串变量是使用 NULL 字符作为最后一个元素的一维字符数组
  2. 字符串变量也是一个指针变量
  3. 字符串指针指向的是字符数组的第一个元素的地址
  4. 两个相同的字符串字面量在内存中使用同一个字符数组来进行存储

字符串输入与输出

如果想把一个字符串读取到程序中,必须首先预留存储字符串的空间,然后使用输入函数来获取这个字符串,C 在 stdio.h 中提供了三个读取字符串的函数:scanfgetsfgets

scanf 函数

scanf 函数可以读取输入的信息到指定的地址值中,配合变量的取值符可以更有效的读入数据:

int age = 1;
scanf("%d", &age);
printf("age = %d\n", age);

如果是指针变量,可以很容易的将地址传递给 scanf 函数,如下:

int *p;
scanf("%d", p);
printf("*p = %d\n", *p);

但是读入数据到指针指定位置的时候,会覆盖所指向的数据,并可能导致程序异常终止。

char *name = "hello";
scanf("%s", name);
printf("name = %s\n", name);

这个是 因为 scanf 把信息复制到由name指定的地址中,而在这种情况下,参数是个未被初始化的指针,name可能指向任何地方。

gets 函数

gets 函数对于交互式程序非常方便,它从系统的标准输入设备(通常是键盘)获得一个字符串。

因为字符串没有预定的长度,所以 gets 函数通过判断遇到的第一个换行符 \n 结束输入,按回车键可以产生这个字符。它读取换行符之前(不包括换行符)的所有字符,并在这些字符后添加一个空字符(\0)。

char n[12];
char *p;
p = gets(n);
printf("n=%s\n", n);
printf("p=%p\n", p);
printf("n=%p\n", n);

如果在 gets 函数在读取字符串时出错或者遇到文件结尾,它就返回一个空(或0)地址,这个空地址被称为空指针,并且 stdio.h 里面定义的常量 NULL 来表示,可以用下面的代码来进行一些错误检测。

char name[1024];
while(get(name) != NULL) {
    // ... do something here ....
}

也可以通过 getchar 函数来完成上面的错误检测。

char ch;
while((ch = getchar()) != EOF) {
    // ... do something ...
}

fgets 函数

fgets 函数也可以读入字符数据到变量中,它和 gets 函数的区别主要有:

  • fgets 需要第二个参数来说明最大读入字符数。如果这个参数值为 nfgets 就会读取最多 n-1 个字符或者读完一个换行符为止(因为会自动添加一个空字符(\n)),由这两者中最先满足的那个结束输入。
  • 如果 fgets 读取到换行符,就会把它存到字符串里,而不是像 gets 那样丢弃。
  • fgets 还需要第三个参数来说明读哪一个文件,从键盘上读取数据时,可以使用stdin(代表standard input)作为参数,这个标识符在 stdio.h 中被定义。
#include<stdio.h>
#define MAX 1024

int main() {
    char n[MAX];
    char *p;
	
	printf("input:");
	p = gets(n);
	printf("n=%s\n", n);
	printf("p=%p\n", p);
	printf("n=%p\n", n);

	printf("input:");
    p = fgets(n, MAX, stdin);	
	printf("n=%s\n", n);
	printf("p=%p\n", p);
	printf("n=%p\n", n);

    return 0;
}

执行结果:

input:hello
n=hello
p=0061FB1C
n=0061FB1C
input:hello
n=hello

p=0061FB1C
n=0061FB1C

明显看到第二次输入的 hello 后面会带有一个回车符号。

scanf 和 gets 的区别

scanf 函数和 gets 函数的主要区别在于如何决定字符串何时结束。scanf 函数可以使用 %s 来读入一个单词,而 gets 则是读入一个长字符串,以回车键结束。如下所示代码:

char n[1024];
char *p;

printf("input:");
p = gets(n);
printf("n=%s\n", n);

printf("input:");
scanf("%s", n);
printf("n=%s\n", n);

如果输入均为 hello world,则输出结果如下:

input:hello world
n=hello world
input:hello world
n=hello

这两种方法都是以遇到的第一个非空白字符开始的,针对 scanf 函数的结束,可以归纳为:

  1. 如果使用 %s 格式,字符串读取到(但不包括)下一个空白字符(比如空格、制表符或换行符)结束
  2. 如果指定了字段宽度,比如 %10sscanf 函数就会读取10个字符或者直到遇到第一个空白字符,由二者最先满足的那一个终止输入

printf 函数

printf 函数接受一个格式控制字符串,格式控制字符串是用双引号括起来的字符串,包括三类信息:

格式字符:格式字符由 % 引导,如 %d%f等。它的作用是控制输出字符的格式。
转义字符:格式控制字符串里的转义字符按照转义后的含义输出,如换行符\n,即输出回车。
普通字符:普通字符即需要在输出时原样输出的字符,如汉字或者其他普通单词。

格式字符 说明
d 输出带符号的十进制整数,正数的符号省略
u 以无符号的十进制整数形式输出
o 以无符号的八进制整数形式输出,不输出前导符0
x 以无符号十六进制整数形式(小写)输出,不输出前导符0x
X 以无符号十六进制整数形式(大写)输出,不输出前导符0X
f 以小数形式输出单、双精度数,隐含输出6位小数
e 以指数形式(小写e表示指数部分)输出实数
E 以指数形式(大写E表示指数部分)输出实数
g 自动选取f或e中输出宽度较小的一种使用,且不输出无意义的0
c 输出一个字符
s 输出字符串

puts 函数

puts 函数接受一个字符串参数的地址,它遇到空字符(\0)就会结束输出(所以必须要有空字符)。puts 函数在显示字符串的时候,会自动在其后添加一个换行符(\n)。

#include<stdio.h>
#define STR "learn C++ together!"

int main() {
	char cs[] = "defined by array!";
	char *cp = "defined by pointer!";
	puts("puts string!");
	puts(cs);
	puts(cp);
	puts(STR);
	puts(&cs[3]);
	puts(cs+4);
	puts(&cp[3]);
	puts(cp+4);
    return 0;
}

fputs 函数

fputs 函数 puts 函数面向文件版本,两者主要的区别是:

  • fputs 函数需要第二个参数来说明要写的文件,可以使用 stdout (standard output) 作为参数来进行输出显示。
  • puts 函数不同,fputs 函数并不为输出自动添加换行符。

读取一行并把它回显在下一行,用下面的两种循环都可以办到

// get & puts
{
    char line[81];
    while(gets(line))
        puts(line);
}
// fgets & fputs
{
    char line[81];
    while(fgets(line,81,stdin))
        fputs(line,stdout);
}

字符串函数

在 C 的标准库中,提供了关于字符串的函数,在使用之前需要引入头文件 string.h。关于这个头文件中包含的函数说明请参考:http://www.cplusplus.com/reference/cstring/

strcpy

函数签名char * strcpy ( char * destination, const char * source );
函数描述:将 source 所指的 C 字符串复制到 destination 所指的数组中,包括终止的空字符(并在该点停止)。
名称来源: Copy string
使用示例

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

int main() {
	char a[] = "Hello";
	char b[] = "World";
	
	// strcpy(a, b) 等价于 a = b
	strcpy(a, b);
	
	printf("a=%s\n", a);
	printf("b=%s\n", b);
	
	return 0;
}

执行结果:

a=World
b=World

因为数组是一个常量指针,所以它不能像普通变量那样直接将变量 b 的值赋予给变量 a。这里使用到 C 标准库中提供的 strcpy 函数将 b 的值赋予给 a。当然,如果使用指针变量声明的字符串完成就可以不需要 strcpy 函数了。示例如下:

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

int main() {
	char *a = "Hello";
	char *b = "World";
	a = b;
	printf("a=%s\n", a);
	printf("b=%s\n", b);
	return 0;
}

执行结果:

a=World
b=World

结果虽然一样,但是使用指针的做法实际上就是将 b 的指针指向的地址值赋予了 a,使得 a 的指向发生变化而已。

strcat

函数签名char * strcat ( char * destination, const char * source );
函数描述: 将 source 字符串的副本追加到 destination 字符串。destination 中的终止空字符被 source 的第一个字符覆盖,并且在由 destination 中的两个字符串联而成的新字符串的末尾包含一个空字符。
名称来源: Concatenate strings
使用示例

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

int main() {
	char a[] = "Hello";
	char b[] = "World";
	
	strcat(strcat(a, " "), b);
	
	printf("a=%s\n", a);
	printf("b=%s\n", b);
	return 0;
}

执行结果:

a=Hello World
b=World

strlen

函数签名size_t strlen ( const char * str );
函数描述: 返回 C 字符串 str 的长度。
名称来源: Get string length
使用示例

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

int main() {
	char a[] = "Hello";
	char b[] = "World";
				
	printf("a=%s, a's length is %d\n", a, strlen(a));
	printf("b=%s, b's length is %d\n", b, strlen(b));
	return 0;
}

执行结果:

a=Hello, a's length is 5
b=World, b's length is 5

strcmp

函数签名int strcmp ( const char * str1, const char * str2 );
函数描述: 如果每个字符都一致,则返回零,否则比较第一个不同的字符的 ASCII 值。
名称来源: Compare two strings
使用示例

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

int main() {

	char a[] = "Hello";
	char b[] = "World";
	int c = strcmp(a, b);
	printf("strcmp(\"%s\", \"%s\") is %d\n", a, b, c);
	return 0;
}

执行结果:

strcmp("Hello", "World") is -1

函数 strcmp 比较 HelloWorld 的结果是 -1,也就是 a < b。那是因为 World 的第一个字符的 ASCII 值比 Hello 的大。如果将 World 更改为 Happy,那就是比较第二个字符的 ASCII 值,结果就是 a > b 了。关于它的更多介绍,可以参考 http://www.cplusplus.com/reference/cstring/strcmp/

strchr

函数签名char * strchr ( const char * str, int character);
函数描述: 返回指向 C 字符串 str 中 character 的第一个匹配项的指针。
名称来源: Locate first occurrence of character in string
使用示例

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

int main() {

	char a[] = "Hello World!";
	char *c = strchr(a, 'l');
	printf("c=%p, c=%c, c=%d, c=%s", c, *c, *c, c);
	
	return 0;
}

执行结果:

c=0022FF31, c=l, c=108, c=llo World!

返回的指针 c 指向的是原字符串的一个第一个 l 的字符,可以使用指针运算,统计出该字符串中某个字符的总数。如下例所示:

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

int main() {
	char a[] = "Hello World!";
	char *c = a, ch = 'l';
	int count = 0;
	while(c) {
		c = strchr(c, ch);
		if(!c) {
			// 如果找不到对应的字符,则返回空指针
			break;
		}
		// printf("c=%p, c=%c, c=%d, c=%s\n", c, *c, *c, c);
		c++;
		count++;
	}
	printf("count of '%c' is %d.\n", ch, count);
	return 0;
}

执行结果:

count of 'l' is 3.

strstr

函数签名char * strstr ( char * str1, const char * str2 );
函数描述: 返回指向 str1 中第一个 str2 的指针,如果 str2 不是 str1 的一部分,则返回空指针。
使用示例

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

int main() {
	char a[] = "The matching process does not include the terminating null-characters, but it stops there.";
	char *c = strstr(a, "null");
	if(c) {
		// c is not null pointer
		printf("c=%p, c=%c, c=%d, c=%s\n", c, *c, *c, c);
	}
	return 0;
}

执行结果:

c=0022FF07, c=n, c=110, c=null-characters, but it stops there.

C++ 的字符串

在 C++ 中,字符串的声明和使用都比 C 中来得简单。示例代码如下:

#include<iostream>
#include<string>
using namespace std;

int main() {
	string str = "Hello World";
	cout << "str's length is " << str.size() << endl;
	str += "!";
	cout << "str's length is " << str.size() << endl;
	cout << str << endl;
	// 在字符串从左开始查找第一次出现指定字符串的位置,并返回下标
	int index = str.find("l");
	cout << "index is " << index << endl;
	// 在字符串从右开始查找第一次出现指定字符串的位置,并返回下标
	index = str.rfind(" ");
	cout << "index is " << index << endl;
	// 从指定下标开始截取字符串,截取指定的长度
	string substring = str.substr(index + 1, 2);
	cout << "substring is " << substring << endl;
	// 从指定下标开始截取字符串,截取到末尾
	substring = str.substr(index + 1);
	cout << "substring is " << substring << endl;
	substring = str.replace(index + 1, 5, "C++");
	cout << "substring is " << substring << endl;
	return 0;
}

执行结果:

str's length is 11
str's length is 12
Hello World!
index is 2
index is 5
substring is Wo
substring is World!
substring is Hello C++!

枚举类型

枚举是 C 语言中的一种基本数据类型,它可以让数据更简洁,更易读。

使用 enum 关键字定义一系列枚举值,表示星期,例如:

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

接下来就能在程序中使用这些枚举值表示星期了

#include<stdio.h>

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

int main() {
	enum WEEK w = SAT;
	
	switch(w) {
		case MON:
		case TUE:
		case WED:
		case THU:
		case FRI:
			printf("today is week. ");
			break;
		case SAT:
		case SUN:
			printf("today is weekend. ");
			break;
	}	
	return 0;
}

执行结果:

today is weekend.

非常简单,如果尝试将其以数字的形式输出呢?代码如下:

#include<stdio.h>

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

int main() {
	enum WEEK w = SAT;
	printf("today is %d. ", w);	
	return 0;
}

执行结果:

today is 5.

会发现,输出的 5 正好与 SAT 在声明枚举的语句中所在的下标是一致的,于是,全部将其输出,查看结果:

printf("MON is %d.\n", MON);
printf("TUE is %d.\n", TUE);
printf("WED is %d.\n", WED);
printf("THU is %d.\n", THU);
printf("FRI is %d.\n", FRI);
printf("SAT is %d.\n", SAT);
printf("SUN is %d.\n", SUN);

其结果是:

MON is 0.
TUE is 1.
WED is 2.
THU is 3.
FRI is 4.
SAT is 5.
SUN is 6.

因为枚举值实际上就是一系列连续的数字,默认从零开始。当然,可以在定义的时候手动更改他们的值。例如:

enum WEEK { MON, TUE, WED=8, THU, FRI, SAT, SUN };

输出对应的数字值分别是:

MON is 0.
TUE is 1.
WED is 8.
THU is 9.
FRI is 10.
SAT is 11.
SUN is 12.

当然,故意将其设置为相同的值是被允许的,但不建议这样做:

反例1:

enum WEEK { MON, TUE, WED=8, THU=8, FRI, SAT, SUN };

反例2:

enum WEEK { MON=5, TUE, WED, THU=3, FRI, SAT, SUN };

这样写都会导致枚举值出现重复的数值,也不利于程序的维护。

结构体

在C语言中,结构体(struct)指的是一种数据结构,是C语言中聚合数据类型(aggregate data type)的一类。

结构体可以被声明为变量、指针或数组等,用以实现较复杂的数据结构。

结构体同时也是一些元素的集合,这些元素称为结构体的成员(member),且这些成员可以为不同的类型,成员一般用名字访问。

—— 摘自百度百科

声明与定义

在 C 语言提供的基本数据类型中,只能表示单一的类型的数据类型,如果需要将多个数据类型组合成为一个整体的作为新的类型的话,就需要使用到结构体。

在表达式 int a = 10;中,变量 a 被定义为了 int 类型,无论如何,变量 a 都表示的是一个数。

而实际运用中,需要依赖变量某个变量来表示更多的内容。

例如,需要表示某个人的信息,包括姓名,年龄,体重等。那每个信息可以使用一个基本数据类型来表示。如下代码所示:

#include<stdio.h>

int main() {
	char name[50] = "tom";
	unsigned int age = 27;
	double weight = 57.9;
	printf("name is %s, age is %d, weight is %f\n", name, age, weight);
	return 0;
}

执行结果:

name is tom, age is 27, weight is 57.900000

但是作为某个人的信息,它们应该是一个整体。这时候可以使用结构体定义一个新的类型,这个类型将包含需要表示的某个人的信息的基本类型。如下代码所示:

/**
 * 人
 */
struct Person {
	/**
	 * 姓名
	 */
	char name[50];
	/**
	 * 年龄
	 */
	unsigned int age;
	/**
	 * 体重
	 */
	double weight;
};

这里定义了一个 Person 类型,该类型包含了 C 提供的三个基本类型。而作为整体,该类型将表示某个人的数据。

结构体的定义语法:

struct 结构体名称 {
	member-list
}

定义好的结构体与基本数据类型的使用方式是一样的。如下所示:

char name[50];
unsigned int age;
double weight;
struct Person man;

或者可以在声明的时候为其赋予初始值:

char name[50] = "Tom";
unsigned int age = 27;
double weight = 57.9;
struct Person man = {"Mark", 28, 60.2};

也可以在声明之后,使用运算符 . 来为结构体变量赋值,完整代码如下所示:

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

/**
 * 人
 */
struct Person {
	/**
	 * 姓名
	 */
	char name[50];
	/**
	 * 年龄
	 */
	unsigned int age;
	/**
	 * 体重
	 */
	double weight;
};

int main() {
	// 声明普通的变量
	char name[50];
	unsigned int age;
	double weight;
	// 为普通的变量赋值
	strcpy(name, "Tom");
	age = 27;
	weight = 57.9;
	// 输出变量的值
	printf("name is %s, age is %d, weight is %f\n", name, age, weight);
	
	// 声明结构体变量
	struct Person man;
	// 为结构体变量的内部成员赋值
	strcpy(man.name, "Mark");
	man.age = 28;
	man.weight = 60.2;
	// 输出结构体变量的内部成员的值
	printf("name is %s, age is %d, weight is %f\n", man.name, man.age, man.weight);
	return 0;
}

执行结果:

name is Tom, age is 27, weight is 57.900000
name is Mark, age is 28, weight is 60.200000

可以将结构体看作是将多个基础数据类型的变量进行打包的容器,使得这些只能表示单一信息的单元组合成一个整体,无论是取值还是储值都需要通过变量 man 使用运算符 . 来操作。在阅读代码的时候,可以将运算符 . 理解为汉字 ,比如 man.age = 28 可以理解为 man 的 age 是 28 <==> man's age is 28。也加强了代码的可读性。

当然,结构体的内部成员除了基础数据类型之外,还可以是指针变量,枚举变量和其他结构体,例如:

/**
 * 性别
 */
enum Gender {Girl, Boy};

/**
 * 成绩
 */
struct Score {
	/**
	 * 语文成绩
	 */
	int Chinese;
	/**
	 * 数学成绩
	 */
	int Math;
	/**
	 * 英语成绩
	 */
	int English;
};
/**
 * 人
 */
struct Person {
	/**
	 * 姓名
	 */
	char name[50];
	/**
	 * 年龄
	 */
	unsigned int age;
	/**
	 * 性别
	 */
	enum Gender gender;
	/**
	 * 体重
	 */
	double weight;
	/**
	 * 成绩
	 */
	struct Score score;
};

结构体作为参数

因为结构体类型中包含多个数据类型的成员,以结构体作为函数参数能减少函数的参数列表的长度,使得代码更加简洁。示例如下:

#include<stdio.h>

enum Gender {Girl=0, Boy=1};

/**
 * 人
 */
struct Person {
	/**
	 * 姓名
	 */
	char name[50];
	/**
	 * 年龄
	 */
	unsigned int age;
	/**
	 * 性别
	 */
	enum Gender gender;
	/**
	 * 体重
	 */
	double weight;
};

/**
 * 打印 Person 结构体的成员变量
 */ 
void print(struct Person person) {
	char *gender = person.gender ? "Boy" : "Girl";
	printf("Person{name:%s, age:%d, gender:%s, weight:%f}\n", person.name, person.age, gender, person.weight);
	
}

int main() {
	struct Person man = {"Mary", 16, Girl, 48.2};
	print(man);
	return 0;
}

执行结果:

Person{name:Mary, age:16, gender:Boy, weight:48.200000}

如果结构体中的成员变量较多,使用这种方式传递数据相比较把所有的成员变量列举到形参列表中而言显得更加方便简洁。

指向结构体的指针

同样的,声明一个指向结构体的指针,也能作为函数参数进行数据的传递。但是作为指针变量,需要访问结构体中的成员变量时,需要使用 -> 运算符。如下所示:

#include<stdio.h>

enum Gender {Girl=0, Boy=1};

/**
 * 人
 */
struct Person {
	/**
	 * 姓名
	 */
	char name[50];
	/**
	 * 年龄
	 */
	unsigned int age;
	/**
	 * 性别
	 */
	enum Gender gender;
	/**
	 * 体重
	 */
	double weight;
};

/**
 * 打印 Person 结构体的成员变量
 */ 
void print(struct Person *p) {
	char *gender = p->gender ? "Boy" : "Girl";
	printf("Person{name:%s, age:%d, gender:%s, weight:%f}\n", p->name, p->age, gender, p->weight);
}

int main() {
	struct Person man = {"Mary", 16, Girl, 48.2};
	struct Person *p = &man;
	printf("this person's name is %s\n", p->name);
	print(p);
	return 0;
}

执行结果:

this person's name is Mary
Person{name:Mary, age:16, gender:Girl, weight:48.200000}

结构体占用的内存空间大小

使用 sizeof 能够量出某个变量或者类型的占用空间的字节数,那结构体占用多大的内存空间呢?首先来看一段代码:

#include<stdio.h>

int main() {
	printf("sizeof(char) is %d\n", sizeof(char));
	printf("sizeof(int) is %d\n", sizeof(int));
	printf("sizeof(double) is %d\n", sizeof(double));
	printf("========================\n");
	
	struct A{int a; int b;};
	printf("sizeof(A) is %d\n", sizeof(struct A));
	
	struct B{int a; double b;};
	printf("sizeof(B) is %d\n", sizeof(struct B));
	
	struct C{double a; double b;};
	printf("sizeof(C) is %d\n", sizeof(struct C));
	
	struct D{double a; double b; int c;};
	printf("sizeof(D) is %d\n", sizeof(struct D));
	
	struct E{double a; double b; int c; int d;};
	printf("sizeof(E) is %d\n", sizeof(struct E));
	
	struct F{double a; double b; int c; int d; char e;};
	printf("sizeof(F) is %d\n", sizeof(struct F));
	
	return 0;
}

执行结果:

sizeof(char) is 1
sizeof(int) is 4
sizeof(double) is 8
========================
sizeof(A) is 8
sizeof(B) is 16
sizeof(C) is 16
sizeof(D) is 24
sizeof(E) is 24
sizeof(F) is 32

结构体 A, C, E 的大小正好是所有的内部成员的 size 之和。而其他的结构体的 size 都比内部成员的 size 的和还要大些。由此可知:结构体占用的内存空间大小不是简单的几个类型的 size 的和

从结果上看,结构体的 size 貌似是以 8 为单位扩张的。例如当成员中已经有两个 double 成员的时候,再增加一个 int 成员,就将自身扩张 8 个字节,于是 size 为 24。

实际上,结构体的 size 在扩张的时候是取决于 C 中的 #pragma pack(n) 指令。缺省情况下,n 的值为8。这也就是该例中扩张 8 个字节的原因。因为 n 的合法的数值分别是1、2、4、8、16。若将该例中增加该指令并设置 n 的值为 1,结构体就会以 1 位单位进行扩张。执行结果如下:

sizeof(char) is 1
sizeof(int) is 4
sizeof(double) is 8
========================
sizeof(A) is 8
sizeof(B) is 12
sizeof(C) is 16
sizeof(D) is 20
sizeof(E) is 24
sizeof(F) is 25

这样就能看到结构体的 size 就正好是各个成员的 size 之和了。

编译器中提供了#pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:

  1. 如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式
  2. 如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。

结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;否则必须为n的倍数。

—— 摘自百度百科

位域

除了使用指令能约束结构体的 size 之外,使用位域也能约束某个成员变量占用的内存大小,进而调整 结构体的 size。在结构内声明位域的形式如下:

struct 结构体标识符
{
  type [member_name] : width ;
};

在使用位域的时候,需要注意以下几个问题:

  1. 成员类型只能为 int(整型),unsigned int(无符号整型),signed int(有符号整型) 三种类型,决定了如何解释位域的值。
  2. 位域中位的数量。宽度必须小于或等于指定类型的位宽度。

示例代码如下:

#include<stdio.h>

int main() {
	
	struct A { int a:1; int b:1; };	
	printf("sizeof(A) is %d\n", sizeof(struct A));
	
	return 0;
}

执行结果:

sizeof(A) is 4

因为结构体 A 内部成员使用的 int 型,其 size 为 4,也就是 32 个 bit 位。所以位域的宽度不能超过 int 的宽度,也就是不能超过 32。在本例中,两个 int 类型的位域宽度值都是 1 ,在实际存储中,它只占用 1 个 bit 位,所以结构体的 size 就是一个 int 类型的 size。换句话将,结构体中所有 int 类型的成员的位域值之和不超过 32 的话,结构体 A 的 size 就为 4。如下例所示:

#include<stdio.h>

int main() {
	
	struct A {
		int a0:1; int a1:1; int a2:1; int a3:1; int a4:1;
		int a5:1; int a6:1; int a7:1; int a8:1; int a9:1;
		
		int b0:1; int b1:1; int b2:1; int b3:1; int b4:1;
		int b5:1; int b6:1; int b7:1; int b8:1; int b9:1;
		
		int c0:1; int c1:1; int c2:1; int c3:1; int c4:1;
		int c5:1; int c6:1; int c7:1; int c8:1; int c9:1;
		
		int d0:1; int d1:1;
	};
	
	printf("sizeof(A) is %d\n", sizeof(struct A));
	
	return 0;
}

执行结果:

sizeof(A) is 4

该例中结构体 A 的成员变量有 32 个,但是结构体 A 的 size 仍然是 4,因为没有超出一个 int 的位宽。如果在增加一个相同的成员,则超出了一个 int 的位宽,其 size 就会扩张为 8。

结构体小案例

结构体能够表示很多场景的实体,比如订单,商品等。在程序中,如果需要使用某个数据类型来表示一个订单信息,或者一个商品信息。使用结构体是一个不错的选择。

下面是一个模拟了一个简单的订单实体的数据小案例。本例中,声明了三个结构体,一个枚举类型;运用随机数并结合之前的文章的内容。具体代码如下所示:

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

// 定义存货量的上限值
#define INVENTORY_QUANTITY 9

/**
 * 支付方式枚举
 */
enum PayWay {Alipay, WeChatPay, ApplePay, UnionPay, CreditCard};

/**
 * 商品详情
 */
struct Goods {
	/**
	 * 商品名称
	 */
	char name[50];
	/**
	 * 商品价格
	 */
	double price;
	/**
	 * 商品折扣
	 */
	unsigned short int discount;
};

/**
 * 订单详情
 */
struct OrderDetail {
	/**
	 * 该商品购买的数量
	 */
	int quantity;
	/**
	 * 小计
	 */
	double subtotalAmount;
	/**
	 * 该商品的详细信息
	 */
	struct Goods goods;
};

/**
 * 订单
 */
struct Order {
	/**
	 * 总计
	 */
	double totalAmount;
	/**
	 * 订单支付方式
	 */
	enum PayWay payway;
	/**
	 * 订单详情
	 */
	struct OrderDetail details[100];
};

int main() {
	
	// 定义多个商品集合用于后面模拟存货
	struct Goods stocks[INVENTORY_QUANTITY] = {
		{"键盘", 199.0, 9},
		{"鼠标", 129.5, 8},
		{"电源", 109.0, 10},
		{"音响", 699.0, 9},
		{"耳机", 169.0, 10},
		{"插板", 19.0, 10},
		{"电脑", 7899.0, 10},
		{"手机", 4999.0, 10},
		{"平板", 299.0, 10}
	};		
	// 使用随机数模拟购物需要的数据
	int count = 0;
	int indexs[INVENTORY_QUANTITY];
	struct Order order = {0.0, Alipay};
	{
		// 初始化已购清单的索引
		for(int k = 0; k < INVENTORY_QUANTITY; k++) {
			indexs[k] = -1;
		}
		// 使用当前时间作来初始化随机函数种子
		srand((unsigned)time(NULL));
		// 随机一个数字,这个数字不能超过 stocks 的长度
		count = rand() % INVENTORY_QUANTITY;
		for(int i = 0; i < count; i++) {
			// 随机一个商品
			int index = -1, exist = 0;
			while (index < 0) {
				index = rand() % INVENTORY_QUANTITY;
				for(int j = 0; j < INVENTORY_QUANTITY; j++) {
					if(indexs[j] == index) {
						exist = 1;
						break;
					}
				}
				if(exist == 1) {
					// 已经在买过该商品, 重新选择新的下标
					exist = 0;
					index = -1;
				}
			}
			for(int k = 0; k < INVENTORY_QUANTITY; k++) {
				if(indexs[k] < 0) {
					indexs[k] = index;
					break;
				}
			}
			struct Goods goods = stocks[index];
			// 随机一个数量,表示购买该商品的数量
			int quantity = rand() % 50;
			// 计算小计
			double subtotalAmount = quantity * goods.price * goods.discount / 10.0;
			order.totalAmount += subtotalAmount;
			// 生成一个订单详情信息
			struct OrderDetail detail = {quantity, subtotalAmount, goods};
			order.details[i] = detail;
		}
	}
	// 打印订单信息
	if(count > 0) {
		printf("\n==========================================================\n");
		{
			char way[11] = "";
			switch(order.payway) {
				case Alipay: strcpy(way, "支付宝"); break;
				case WeChatPay: strcpy(way, "微信支付"); break;
				case ApplePay: strcpy(way, "苹果支付"); break;
				case UnionPay: strcpy(way, "银联支付"); break;
				case CreditCard: strcpy(way, "信用卡支付"); break;
			}
			printf("订单总额:%f, 支付方式:%s", order.totalAmount, way);
		}
		printf("\n----------------------------------------------------------\n");
		printf("序号 \t 商品 \t 数量 \t 单价 \t\t 小计.\n");
		for(int i = 0; i < count; i++) {
			struct OrderDetail detail = order.details[i];
			char *name = detail.goods.name;
			int quantity = detail.quantity;
			double price = detail.goods.price;
			int discount = detail.goods.discount;
			double subtotalAmount = detail.subtotalAmount;
			printf(" %d. \t %s \t %d \t %.2f \t %.2f\n", (i + 1), name, quantity, price, subtotalAmount);
			if(discount > 0 && discount < 10) {
				double subPrice = price * discount / 10.0;
				printf(" \t \t \t 折后价 -> %.2f\n", subPrice);
			}
		}
		printf("\n==========================================================\n");
	} else {
		printf("没有查询到有效的订单信息");
	}
	
	return 0;
}

执行结果:


==========================================================
订单总额:53539.800000, 支付方式:支付宝
----------------------------------------------------------
序号     商品    数量    单价            小计.
 1.      鼠标    44      129.50          4558.40
                         折后价 -> 103.60
 2.      音响    25      699.00          15727.50
                         折后价 -> 629.10
 3.      键盘    19      199.00          3402.90
                         折后价 -> 179.10
 4.      平板    15      299.00          4485.00
 5.      插板    13      19.00   247.00
 6.      电源    47      109.00          5123.00
 7.      手机    4       4999.00         19996.00

==========================================================

联合体

在进行某些算法的C语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中。

也就是使用覆盖技术让几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型结构,简称共用体,也叫联合体。

—— 摘自百度百科

定义联合体与使用

联合体是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型。程序中可以定义带有多个成员的联合体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。用到的关键字是 union,其格式如下:

union 联合体名称 {
	member-list
}

实例代码如下:

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

union Person {
	char name[50];
	unsigned int age;
	double weight;
};

int main() {

	union Person person;
	strcpy(person.name, "tom");
	person.age = 27;
	person.weight = 46.9;
	printf("person.name = %s, person.age = %d, person.weight = %f\n", person.name, person.age, person.weight);
	
	return 0;
}

执行结果:

person.name = 33333sG@諏鱱AQ, person.age = 858993459, person.weight = 46.900000

可以看到,只有成员 weight 的值是被正确的输出,原因是这三个成员变量在储值的时候占用的是同一个内存空间,相当于这三个成员指向的是同一块内存空间,当 weight 被赋值之后,其他的成员的值就被损坏。使用 & 可以查看各个成员指向的地址值:

union Person up;
printf("&up=%p, &up.name=%p, &up.age=%p, &up.weight=%p\n", &up, &up.name, &up.age, &up.weight);

执行结果如下:

&up=0022FF08, &up.name=0022FF08, &up.age=0022FF08, &up.weight=0022FF08

如果需要在联合体变量被声明的时候赋予初始值的话,因为它们共用同一个内存空间,故而,不需要将每个值都赋值,而且编译器只会接收第一个值作为有效值填入合适的成员中。例如:

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

union Person {
	char name[50];
	unsigned int age;
	double weight;
};

int main() {

	union Person up1 = {"Tom", 12, 4.6};
	union Person up2 = {12};
	union Person up3 = {4.6};
	union Person up4 = {"Tom"};
	printf("name=%s, age=%d, weight=%f\n", up1.name, up1.age, up1.weight);
	printf("name=%s, age=%d, weight=%f\n", up2.name, up2.age, up2.weight);
	printf("name=%s, age=%d, weight=%f\n", up3.name, up3.age, up3.weight);
	printf("name=%s, age=%d, weight=%f\n", up4.name, up4.age, up4.weight);
	
	return 0;
}

该源程序在编译的时候给出警告和提示:

warning: excess elements in union initializer
  union Person up1 = {"Tom", 12, 4.6};
                             ^~
note: (near initialization for 'up1')
warning: excess elements in union initializer
  union Person up1 = {"Tom", 12, 4.6};
                                 ^~~
note: (near initialization for 'up1')

执行结果变成了:

name=Tom, age=7171924, weight=0.000000
name=, age=12, weight=0.000000
name=, age=4, weight=0.000000
name=Tom, age=7171924, weight=0.000000

从本例中可以看到,联合体 up1 和 up4 接受的值是字符串 Tom,联合体 up2 接受的值是 12,联合体 up3 将传递的小数直接转换为了整型赋值给了 age 成员,接受的值是 4。故而,在给联合体变量声明的时候不需要为每个成员变量赋值,只需要赋予一个合适类型的值即可。

联合体作为参数

将联合体作为参数传递到函数中的操作方式与结构体一样。但是因为共享内存的原因,在函数内,仅接受一个有效值。代码如下:

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

union Person {
	char name[50];
	unsigned int age;
	double weight;
};

void printPerson(union Person p) {
	printf("Person{name:%s, age:%d, weight:%f}", p.name, p.age, p.weight);
}

int main() {
	union Person up = {12};
	printPerson(up);
	return 0;
}

执行结果:

Person{name:, age:12, weight:0.000000}

指向联合体的指针

指向联合体的指针在使用上与结构体一致,主要使用的运算符是 ->。具体代码如下:

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

union Person {
	char name[50];
	unsigned int age;
	double weight;
};

void printPerson(union Person *p) {
	printf("Person{name:%s, age:%d, weight:%f}", p->name, p->age, p->weight);
}

int main() {
	union Person up = {12};
	union Person *p = &up;
	printPerson(p);	
	return 0;
}

执行结果:

Person{name:, age:12, weight:0.000000}

位运算

位和字节

1字节(byte) = 8 比特(bit)

字长指的是 CPU 一次性能够运算的数据的位数,不同的计算机可能不一样。

一个英文字符和英文标点占用一个字节,一个中文字符和中文标点占用两个字节。

计算机中的位

  • 二进制数系统中,每个0或1就是一个位(bit),位是数据存储的最小单位。
  • 其中8 bit就称为一个字节(Byte)。
  • 计算机中的CPU位数指的是CPU一次能处理的最大位数,
  • 例如32位计算机的CPU一次最多能处理32位数据,计算机中的CPU位数也成为机器字长和数据总线(CPU与内部存储器之间连接的用于传输数据的线的根数)的概念是统一的。

比特

  • 计算机专业术语,是信息量单位,是由英文 bit 音译而来。
  • 二进制数的一位所包含的信息就是 1bit,如二进制数 0101 就是 4bit。
  • 二进制数字中的位,信息量的度量单位,为信息量的最小单位。
  • 数字化音响中用电脉冲表达音频信号,1 代表有脉冲,0 代表脉冲间隔。
  • 如果波形上每个点的信息用四位一组的代码表示,则称4比特,比特数越高,表达模拟信号就越精确,对音频信号信号还原能力越强。

进制数

在实际开发中,我们可以用0和1的字符串来表达信息,例如某设备有八个传感器,每个传感器的状态用1表示正常,用0表示故障,用一个二进制的字符串表示它们如01111011,用一个字符或整数表示它就是123。

十进制转二进制

  1. 把十进数除以2,记下余数(余数保存在字符串中),现用商除以2,再记下余数,如此循环,直到商为0。
  2. 把保存余数的字符串反过来,就是结果。

例如123转化成二进制:

123 ÷ 2 = 61 ... 1
61  ÷ 2 = 30 ... 1
30  ÷ 2 = 15 ... 0
15  ÷ 2 = 7  ... 1
7   ÷ 2 = 3  ... 1
3   ÷ 2 = 1  ... 1
1   ÷ 1 = 0  ... 1

于是,十进制的 123 的用二进制表示就是:1111011,代码实现如下:

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

// 十进制转换为二进制:辗转相除法
void dec2bin(int decimal, char * bstr) {
	int quotient = decimal;
	char cs[32];
	int i = -1;
    // 初始化 cs 全部使用 \0 填充
	for(int j = 0; j < 32;j++) {
		cs[j] = '\0';
	}
	do {
        // div 函数来自 stdlib.h, 可以通过该函数得到商(quotient)和余数(remainder)
		div_t r = div(quotient, 2);
		quotient = r.quot;
		cs[++i] = (char)(48 + r.rem);
	} while(quotient > 0);
	// 反转 cs 中的字符,得到二进制的字符串表达形式
	for(int j = 0; j < i;j++) {
		char c = cs[i-j];
		bstr[j]=c;
	}
}

int main() {
	int n = 123;
	char bstr[32];
	dec2bin(n, bstr);
	printf("%d --> %s\n", n, bstr);
    return 0;
}

二进制转十进制

把二进制数从右往左依次与2的次幂数相乘,最右边的为0次幂,往左次幂依次升高,将每位的乘积相加即可:

例如1111011转化成十进制:

1*2^6 + 1*2^5 + 1*2^4 + 1*2^3 + 0*2^2 + 1*2^1 + 1*2^0

结果是123。代码实现如下:

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

// 二进制转换为十进制
int bin2dec(char * bstr) {
	int len = strlen(bstr);
	int sum = 0;
	for(int i = 0; i < len; i++) {
		char c = bstr[len-i-1];
		int b = (int)c - 48;
		sum += b * exp2(i);
	}
	return sum;
}

int main() {
	char bstr[] = "1111011";
	int n = bin2dec(bstr);
	printf("%s --> %d\n", bstr, n);
    return 0;
}

八进制

一种以8为基数的计数法,采用0,1,2,3,4,5,6,7八个数字,逢八进1。

一些编程语言中常常以数字0开始表明该数字是八进制。

八进制的数和二进制数可以按位对应(八进制一位对应二进制三位),因此常应用在计算机语言中。

十六进制

在数学中是一种逢16进1的进位制。一般用数字0到9和字母A到F(或af)表示,其中:AF表示10~15。

位逻辑运算符

位运算的运算分量只能是整型或字符型数据。

位运算把运算对象看作是由二进位组成的位串信息,按位完成指定的运算,得到位串信息的结果。

位运算符有: & (按位与)、|(按位或)、^(按位异或)、~ (按位取反)。

优先级从高到低依次为:~&^|

可以将位运算中的 1 和 0 理解为是逻辑中的真和假,或者为电路中的闭路和开路。

与运算:&

同真为真,存假则假

printf("(1 & 1) = %d\n", (1 & 1)); // 1
printf("(1 & 0) = %d\n", (1 & 0)); // 0
printf("(0 & 1) = %d\n", (0 & 1)); // 0
printf("(0 & 0) = %d\n", (0 & 0)); // 0

或运算:|

同假为假,存真为真

printf("(1 | 1) = %d\n", (1 | 1)); // 1
printf("(1 | 0) = %d\n", (1 | 0)); // 1
printf("(0 | 1) = %d\n", (0 | 1)); // 1
printf("(0 | 0) = %d\n", (0 | 0)); // 0

非运算:~

真即是假,假即是真


异或运算:^

相同为假,相异为真

printf("(1 ^ 1) = %d\n", (1 ^ 1)); // 0
printf("(1 ^ 0) = %d\n", (1 ^ 0)); // 1
printf("(0 ^ 1) = %d\n", (0 ^ 1)); // 1
printf("(0 ^ 0) = %d\n", (0 ^ 0)); // 0

位移运算:<< 或 >>

左移运算符

左移运算符(<<):是用来将一个数的各二进制位左移若干位,移动的位数由右操作数指定(右操作数必须是非负值),其右边空出的位用0填补,高位左移溢出则舍弃该高位。

// 128 << 1 ⇔ 256 × 2^1
printf("(128 << 1) = %d\n", (128 << 1)); // 256
// 64 << 2 ⇔ 64 × 2^2
printf("(64 << 2) = %d\n", (64 << 2)); // 256
// 32 << 3 ⇔ 32 × 2^3
printf("(32 << 3) = %d\n", (32 << 3)); // 256

右移运算符

右移运算符(>>):是用来将一个数的各二进制位右移若干位,移动的位数由右操作数指定(右操作数必须是非负值),移到右端的低位被舍弃,对于无符号数,高位补0。对于有符号数,某些机器将对左边空出的部分用符号位填补(即“算术移位”),而另一些机器则对左边空出的部分用0填补(即“逻辑移位”)。

注意:

  • 对于无符号数,右移时左边高位移入0

  • 对于有符号的值

    • 如果原来符号位为 0(即正数),则左边也是移入0
    • 如果符号位原来为 1(即负数),则左边移入0还是1,要取决于所用的计算机系统
      • 移入0的称为“逻辑移位”,即简单移位;
      • 移入1的称为“算术移位”。
// 256 >> 1 ⇔ 256 ÷ 2^1
printf("(256 >> 1) = %d\n", (256 >> 1)); // 128
// 256 >> 2 ⇔ 256 ÷ 2^2
printf("(256 >> 2) = %d\n", (256 >> 2)); // 64
// 256 >> 3 ⇔ 256 ÷ 2^3
printf("(256 >> 3) = %d\n", (256 >> 3)); // 32

位运算赋值运算符

&=, |=, >>=, <<=, ∧=

int a = 256;
printf("a = %d\n", a);
a >>= 1;
printf("a = %d\n", a);
a >>= 1;
printf("a = %d\n", a);
a >>= 1;
printf("a = %d\n", a);
a <<= 1;
printf("a = %d\n", a);
a <<= 1;
printf("a = %d\n", a);
a <<= 1;
printf("a = %d\n", a);

预处理器

预处理器是在真正的编译开始之前由编译器调用的独立程序。预处理器可以删除注释、包含其他文件以及执行宏(宏macro是一段重复文字的简短描写)替代。

#include

包含一个源代码文件,例如:

#include <stdio.h>

#define

定义宏,例如:

#define IDC_STATIC -1

#ifndef & #ifdef & #endif

#ifndef 与 #endif 联合使用,表示如果某个宏未定义,则执行相应的逻辑。例如:

#ifndef IDC_STATIC
	#define IDC_STATIC -1
#endif

#ifdef 与 #endif 联合使用,表示如果某个宏已定义,则执行相应的逻辑。例如:

#ifdef IDC_STATIC
	// do something here.
#endif

#if & #elif & #else

预处理器中的条件结构,示例代码:

#if IDC_STATIC > 0
	#define IDC_STATIC -1
#elif !defined(MSG)
	#define MSG "MSG is undefined!"
#elif defined(MSG)
	#define MSG "MSG is defined!"
#else
	#define MSG "invalid code is here"
#endif

#undef

取消已定义的宏,例如:

#ifdef IDC_STATIC
	#undef IDC_STATIC
#endif

文件

C 的标输出库 stdio 中提供了对文件进行读写的函数。

打开文件
函数签名:FILE * fopen ( const char * filename, const char * mode );
返回说明:如果成功打开该文件,则返回指向该文件的 FILE 对象的指针
参数说明:

  1. filename:需要打开的文件名
  2. mode:打开文件的模式

关于参数 mode 的取值:

说明
"r" read
"w" write
"a" append
"r+" read/update
"w+" write/update
"a+" append/update

详细说明请参考:http://www.cplusplus.com/reference/cstdio/fopen/

关闭文件
函数签名:int fclose ( FILE * stream );
返回说明:如果成功关闭该文件,则返回 0
参数说明:

  1. stream:指向该文件的 FILE 对象的指针

详细说明请参考:http://www.cplusplus.com/reference/cstdio/fclose/

写入文件
函数签名:int fputs ( const char * str, FILE * stream );
返回说明:成功时返回非负值
参数说明:

  1. str:需要写入的字符串常量
  2. stream:指向该文件的 FILE 对象的指针

详细说明请参考:http://www.cplusplus.com/reference/cstdio/fputs/

读取文件
函数签名:char * fgets ( char * str, int num, FILE * stream );
返回说明:成功时传入的参数 str 的值
参数说明:

  1. str:指向在其中复制字符串读取的字符数组的指针
  2. num:要复制到str中的最大字符数(包括终止的空字符)
  3. stream:指向该文件的 FILE 对象的指针

详细说明请参考:http://www.cplusplus.com/reference/cstdio/fgets/

简单示例

#include<stdio.h>

int main() {
	const char *filename = "/io.txt";
	// 写
	{
		FILE *f = fopen("/io.txt", "w+");
		int r = fputs("Hello World.\n —— marvelousness", f);
		if(r > -1) {
			r = fclose(f);
			if(r == 0) {
				// printf("写入完成.\n");
			}
		}
	}
	// 读
	{
		FILE *f = fopen("/io.txt", "r");
		char buff[1024];
		while(fgets(buff, 1024, f) != NULL) {
			printf("%s", buff);
		}
		int r = fclose(f);
	}
	
	return 0;
}

执行结果:

Hello World.
 —— marvelousness

内存管理

了解变量的存储类别以及动态内存分配的知识

存储类别

C语言中的四种存储类别: 自动变量 (auto)、静态变量(static)、寄存器(register)、外部变量 (extern)。

自动变量 (auto)

通常在自定义函数内或代码段中(用 {} 括起来的)定义的变量,都是自动变量,除了加了static关键字修饰的变量,也称为局部变量。

自动变量都是动态地分配存储空间的,数据存储在动态存储区中。

函数中的形参和在函数中定义的变量(包括在复合语句中定义的变量)都属于这个分类,在调用该函数时系统会给它们分配存储空间,在函数调用结束时就自动释放这些存储空间。

自动变量用关键字 auto 进行存储类别的声明,例如声明一个自动变量:

void func() {
	auto int a = 10;
}

外部变量

外部变量(即全局变量)是在函数的外部定义的,它的作用域为从变量定义处开始,到本程序文件的末尾。

int a = 10;

int main() {
	return 0;
}

或者通过 extern 指定某个全局变量来自外部文件,例如在 other.c 中定义的全局变量在 app.c 中使用:

第一个源文件 other.c

静态变量(static)

有时希望函数中的局部变量的值在函数调用结束后不消失而保留原值,这时就应该指定局部变量为静态局部变量,用关键字 static 进行声明。

#include<stdio.h>

void func(int i) {
	static int a=0;
	a += i;
	printf("a=%d\n", a);
}

int main() {
	func(1);
	func(1);
	func(1);
	return 0;
}

执行结果:

a=1
a=2
a=3

如果将程序中的 static 去掉,则输出的结果将会恒为 a=1

寄存器变量(register)

为提高效率,C 语言允许将局部变量的值存放在 CPU 的寄存器中,这种变量叫做寄存器变量。

用关键字 register 声明。

使用寄存器变量需要注意以下几点:

  1. 只有局部自动变量和形式参数可以作为寄存器变量。
  2. 一个计算机系统中的寄存器数目有限,不能定义任意多个寄存器变量。
  3. 不能使用取地址运算符 & 求寄存器变量的地址。
void func(register int i) {
	register int a = 10;
	printf("a=%d,i=%d\n", a, i);
}

相关概念

了解变量的作用域、链接、存出期

作用域(scope)

作用域描述了程序中合法访问一个标识符的区域。

一个C变量的作用域可以是:

  • 代码块作用域(block scope)
  • 函数原型作用域(function prototype scope)
  • 文件作用域(file scope)
链接(linkage)

跟作用域类似,变量的链接是一个空间概念,描述了程序中合法访问一个标识符的区域。

一个C变量的链接类型可以是:

  • 外部链接(external linkage)
  • 内部链接(internal linkage)
  • 空链接(no linkage)
存储期(storage duration)

变量的声明周期,描述了一个C变量在程序执行期间的存在时间。

一个C变量的存储期可以是:

  • 静态存储期(static storage duration)
  • 自动存储期(automatic storage duration)
  • 动态存储期(dynamic storage duration)

存储类别小结

存储类 时期 作用域 链接 声明方式
自动 自动 代码块 代码块内
寄存器 自动 代码块 代码块内 Register
具有外部链接的静态 静态 文件 外部 所有函数之外
具有内部链接的静态 静态 文件 内部 所有函数之外 static
空链接的静态 静态 代码块 代码块之内 static

内存动态管理

C 语言为内存的分配和管理提供了几个函数。这些函数可以在 <stdlib.h>头文件中找到。

栈上开辟空间

默认情况下,C 语言会根据基础类型为变量在栈上开辟空间。

int num = 20;//在栈空间上开辟四个字节
char ns[4] = {1, 2, 3, 4};//在栈空间上开辟10个字节的连续空间

特点如下:

  • 空间开辟大小是固定的。
  • 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

动态内存函数

  1. malloc 用来开辟动态内存
void * malloc(size_t size);

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
  • 返回值的类型是 void* ,所以 malloc 函数并不知道开辟空间的类型,在使用的时候使用者需要手动转换
  • 如果参数 size 为 0,malloc 的行为是标准是未定义的,取决于编译器。
int n = 26;
// 开辟一块内存,占 n 个 char 类型的大小
char *p = malloc(n * sizeof(char));
for(int i = 0; i < n; i++) {
    p[i] = (char)(i + 97);
}
printf("p=%s\n", p);
free(p);
  1. free 函数用来释放动态开辟的内存
void free (void* ptr);
  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptr 是NULL指针,则函数什么事都不做。
  • free前后指向的地址不发生任何变化,改变的只是指针和对应的内存的管理关系。
int num = 20;
int* p = &num;
printf("p=%p\n", p);
free(p);
if(p == NULL) {
	printf("p is NULL");
} else {
	printf("p=%p\n", p);
}
p = NULL;// 一般再添加一句代码令指针为NULL。
  1. calloc 函数也用来动态内存分配
void* calloc (size_t num, size_t size);
  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
int n = 26;
// 开辟 n 块内存,每块内存占 char 类型的大小
char *p = calloc(n, sizeof(char));
for(int i = 0; i < n; i++) {
    p[i] = (char)(i + 65);
}
printf("p=%s\n", p);
free(p); // 释放指针
  1. realloc 函数的出现让动态内存管理更加灵活。
void * realloc(void * ptr, size_t size);
  • ptr 是要调整的内存地址
  • size 调整之后新大小
  • 返回值为调整之后的内存起始位置
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新的空间
  • realloc 在调整内存空间的是存在两种情况:
    • 情况1:原有空间之后有足够大的空间。这种情况下,要扩展内存就在原有内存之后直接追加空间,原来空间的数据不发生变化
    • 情况2 :原有空间之后没有足够多的空间。这种情况下,在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址
#include<stdio.h>
#include<stdlib.h>
#define SIZE_A 50
#define SIZE_B 100

int main() {
	int *ptr = malloc(SIZE_A);
	if(ptr == NULL) {
		printf("动态分配内存失败\n");
		exit(0);
	}
	printf("ptr=%p\n", ptr);
	int *p = realloc(ptr, SIZE_B);
	if(p == NULL) {
		printf("重新分配内存失败\n");
		exit(0);
	}
	printf(" p =%p\n", p);
	ptr = p;
	free(ptr);
	free(p);
	return 0;
}

在上述源代码中,当 SIZE_B 为 100,在重新分配内存的时候,原有空间之后没有足够的空间,符合情况2,会返回一个新的地址,修改 SIZE_B 为4,在重新分配内存的时候,原有空间有足够的空间,符合情况1,会返回原指针的地址。

常见动态内存错误

列举几个常见的错误

  1. 对NULL指针的解引用操作
*p = 20; //如果p的值是NULL,就会有问题
  1. 对动态开辟空间的越界访问
int *p = (int *)malloc(5*sizeof(int));
if(p != NULL) {
    for(int i=0; i<=5; i++) {
        *(p+i) = i;//当i是5的时候越界访问
    }
}
  1. 对非动态开辟内存使用free释放
int a = 10;
int *p = &a;
free(p);// p 的地址来自变量 a, 但 a 的内存并非动态开辟, 这种用法是错误的
  1. 使用free释放一块动态开辟内存的一部分
int *p = (int *)malloc(100);
p++;
free(p);// p不再指向动态内存的起始位置
  1. 对同一块动态内存多次释放
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
  1. 动态开辟内存忘记释放(内存泄漏)
#include<stdio.h>
#include<stdlib.h>

int main() {
	int *p = (int *)malloc(100);
    if(NULL != p) {
        *p = 20;
        // free(p); // 在使用完指针后,不要记得将其 free
    }
	return 0;
}

标准函数库

数学库

math.h 中提供了关于数学相关的函数

三角函数(Trigonometric functions)

数学中的自然常数 π 的值作为常量使用 M_PI 表示

printf("sin(30°)=%.1f\n", sin(M_PI/6));
printf("cos(60°)=%.1f\n", cos(M_PI/3));
printf("tan(45°)=%.1f\n", tan(M_PI/4));

printf("asin(0.5)=%.0f°\n", asin(0.5) * (180/M_PI));
printf("acos(0.5)=%.0f°\n", acos(0.5) * (180/M_PI));
printf("atan(1)=%.0f°\n", atan(1) * (180/M_PI));

指数函数(Exponential functions)

exp 用来求以自然常数 e 的常数的指数结果。exp2 用来求以 2 的常数的指数结果。

printf("e^(0)=%.3f\n", exp(0));
printf("e^(1)=%.3f\n", exp(1));
printf("e^(2)=%.3f\n", exp(2));

printf("exp2(2)=%f\n", exp2(2));
printf("exp2(3 )=%f\n", exp2(3));

对数函数(Logarithmic functions)

数学中的自然常数 e 的值在 C 语言中使用 M_E 表示

// 求以 e 为底数的对数
printf("ln(e)=%f\n", log(M_E));
printf("ln(1)=%f\n", log(1));

// 求以 10 为底数的对数
printf("log10(10)=%f\n", log10(10));
printf("log10(1)=%f\n", log10(1));

// 求以 2 为底数的对数
printf("log2(2)=%f\n", log2(2));
printf("log2(1)=%f\n", log2(1));

幂函数(Power functions)

printf("7^2 = %f\n", pow(7.0, 2.0));

平方根(square root)

printf(" 4 的平方根是 %f\n", sqrt(4.0));

立方根(cube root)

printf(" 8 的立方根是 %f\n", cbrt(8.0));

求斜边(hypotenuse)

已知直角三角形的两个直角边长度,求斜边长度

printf("直角边为 3.0 和 4.0, 斜边为 %.1f\n", hypot(3.0, 4.0));

取整函数

// 向上取整
printf( "ceil(2.3)=%.1f\n", ceil(2.3));
printf( "ceil(2.6)=%.1f\n", ceil(2.6));
// 向下取整
printf( "floor(2.3)=%.1f\n", floor(2.3));
printf( "floor(2.6)=%.1f\n", floor(2.6));
// 四舍五入
printf( "round(2.3)=%.1f\n", round(2.3));
printf( "round(2.6)=%.1f\n", round(2.6));
// 取余
printf( "fmod(5.0, 3.0) = %.1f\n", fmod(5.0,3.0));
printf( "5 mod 3 = %.0f\n", 5%3);

绝对值和最值

printf( "|3.14| = %.2f\n", fabs(3.14));
printf( "|-3.14| = %.2f\n", fabs(-3.14));
printf( "fmax(3.14, 2.72) = %.2f\n", fmax(3.14, 2.72));
printf( "fmin(3.14, 2.72) = %.2f\n", fmin(3.14, 2.72));

通用工具库

stdlib.h 中,提供了大量的工具库,可以在开发过程中去使用。

  1. double atof(const char *str)

把参数 str 所指向的字符串转换为一个浮点数(类型为 double 型)。

double r = atof("3.14"); // 结果是 3.14
r = atof("3.14ok"); // 结果是 3.14
r = atof("3.14 ok"); // 结果是 3.14
  1. int atoi(const char *str)

把参数 str 所指向的字符串转换为一个整数(类型为 int 型)。

int r = atoi("2024"); // 结果是 2024
r = atoi("2024ok"); // 结果是 2024
r = atoi("2024 ok"); // 结果是 2024
  1. long int atol(const char *str)

把参数 str 所指向的字符串转换为一个长整数(类型为 long int 型)

long int r = atol("2024"); // 结果是 2024
r = atol("2024ok"); // 结果是 2024
r = atol("2024 ok"); // 结果是 2024
  1. double strtod(const char *str, char *endptr))

把参数 str 所指向的字符串转换为一个浮点数(类型为 double 型)

  • str:要转换为双精度浮点数的字符串。
  • endptr:对类型为 char* 的对象的引用,其值由函数设置为 str 中数值后的下一个字符。

这个当第二个参数为空时和 double atof(const char *str) 是相同,但是当而第二形参引用后,第二个指针会指向存储字符串位置的地址。

char str[] = "23.67km";
char *pstr;
double ret = strtod(str, &pstr);
printf("ret=%f, pstr=%s", ret, pstr);

ret 的值是 23.67, pstr 的值是 km

  1. long int strtol(const char *str, char *endptr, int base)

把参数 str 所指向的字符串转换为一个有符号长整数(类型为 long int 型)。

  • str:要转换为长整数的字符串。
  • endptr:对类型为 char* 的对象的引用,其值由函数设置为 str 中数值后的下一个字符。
  • base:基数,必须介于 2 和 36(包含)之间,或者是特殊值 0。这个基数就表示这个多少数字就多少进制
char str[] = "2024year";
char *pstr;
long int ret = strtol(str, &pstr, 8);
printf("ret=%d, pstr=%s\n", ret, pstr);// ret 的值是 1044, pstr 的值是 year
ret = strtol(str, &pstr, 10);
printf("ret=%d, pstr=%s\n", ret, pstr);// ret 的值是 2024, pstr 的值是 year
  1. unsigned long int strtoul(const char *str, char **endptr, int base)

把参数 str 所指向的字符串转换为一个无符号长整数(类型为 unsigned long int 型)。

char str[] = "2024year";
char *pstr;
unsigned long int ret = strtoul(str, &pstr, 8);
printf("ret=%d, pstr=%s\n", ret, pstr);
ret = strtol(str, &pstr, 10);
printf("ret=%d, pstr=%s\n", ret, pstr);
  1. void *calloc(size_t nitems, size_t size)

分配所需的内存空间,并返回一个指向它的指针。

malloccalloc 之间的不同点是,malloc 不会设置内存为零。

// 参考前文【内存管理】章节的代码
  1. void free(void *ptr)

释放指针所指内存空间的数据,该函数不返回任何值

// 参考前文【内存管理】章节的代码
  1. void *malloc(size_t size)

分配所需的内存空间,并返回一个指向它的指针。

// 参考前文【内存管理】章节的代码
  1. void *realloc(void *ptr, size_t size)

尝试重新调整之前调用 malloccalloc 所分配的 ptr 所指向的内存块的大小。

该函数返回一个指针 ,指向重新分配大小的内存。如果请求失败,则返回 NULL。

// 参考前文内存管理章节的代码
  1. void abort(void)

使一个异常程序终止。

for(int i =0; i < 100; i++) {
    if(i == 30) {
        abort();终止程序
    }
    printf("i=%d\n", i);
}
  1. int atexit(void (*func)(void))

当程序正常终止时,调用指定的函数 func。

void func() {
	printf("The program exited!");
}
int main() {
	atexit(func);
    return 0;
}
  1. void exit(int status)

使程序正常终止。

for(int i =0; i < 100; i++) {
    if(i == 10) {
        exit(0);
    }
    printf("i=%d\n", i);
}
  1. char *getenv(const char *name)

搜索 name 所指向的环境字符串,并返回相关的值给字符串。

printf("JAVA_HOME : %s\n", getenv("JAVA_HOME"));
  1. int system(const char *string)

由 string 指定的命令传给要被命令处理器执行的主机环境。

system("dir");
  1. void *bsearch(const void *key, const void *base, size_t nitems,size_t size, int (*compar)(const void *, const void *))

执行二分查找

  • key:指向要查找的元素的指针,类型转换为 void* 。
  • base:指向进行查找的数组的第一个对象的指针,类型转换为 void* 。
  • nitems:base 所指向的数组中元素的个数。
  • size:数组中每个元素的大小,以字节为单位。
  • compar:用来比较两个元素的函数。
#include<stdio.h>
#include<stdlib.h>
#define SIZE 5

int compar(const void * a, const void *b) {
	return *(int *)a - *(int *)b;
}

int main() {
	int ns[SIZE] = {5, 8, 3, 6, 4};
	int key = 6;
	int *result = (int *)bsearch(&key, ns, SIZE, sizeof(int), compar);
	
	if(result != NULL) {
		printf("*result=%d, result=%p\n", *result, result);
	} else {
		printf("can`t find %d\n", key);
	}
	
	for(int i = 0; i < SIZE; i++) {
		printf("ns[%d]=%d, &ns[%d]=%p\n", i, ns[i], i, &ns[i]);
	}
    return 0;
}
  1. void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))

数组排序

  • base:指向要排序的数组的第一个元素的指针。
  • nitems:由 base 指向的数组中元素的个数。
  • size:数组中每个元素的大小,以字节为单位。
  • compar:用来比较两个元素的函数。
#include<stdio.h>
#include<stdlib.h>
#define SIZE 5

int compar(const void * a, const void *b) {
	return *(int *)a - *(int *)b;
}

int main() {
	int ns[SIZE] = {5, 8, 3, 6, 4};
    
	qsort(ns, SIZE, sizeof(int), compar);
	
	for(int i = 0; i < SIZE; i++) {
		printf("ns[%d]=%d, &ns[%d]=%p\n", i, ns[i], i, &ns[i]);
	}
    return 0;
}
  1. int abs(int x)

返回 x 的绝对值。

int a = abs(-4);
printf("a=%d\n", a);
  1. div_t div(int numer, int denom)

分子除以分母。

div_t output = div(27, 4);
printf("(27/4) quotient  = %d\n", output.quot);
printf("(27/4) remainder = %d\n", output.rem);
  1. int rand(void)void srand(unsigned int seed)

返回一个范围在 0 到 RAND_MAX 之间的伪随机数。

// 初始化随机数发生器 以CPU的时间作为随机种子所以是伪随机数
srand((unsigned)time(NULL));
// 输出 0 到 49 之间的 5 个随机数
for (int i = 0; i < 5; i++) {
	printf("%d\n", rand() % 50);
}

相关推荐

  1. C++】知识汇总(下)

    2024-03-19 23:20:03       27 阅读
  2. C++】知识汇总(上)

    2024-03-19 23:20:03       32 阅读
  3. C语言】小知识(函数,浮,作用域)

    2024-03-19 23:20:03       15 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-19 23:20:03       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-19 23:20:03       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-19 23:20:03       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-19 23:20:03       18 阅读

热门阅读

  1. 全屏时框架的message alert 下拉框失效问题

    2024-03-19 23:20:03       17 阅读
  2. Linux 常用运维使用指令

    2024-03-19 23:20:03       20 阅读
  3. pytorch升级打怪(五)

    2024-03-19 23:20:03       18 阅读
  4. C++学习之旅(一)- 序言

    2024-03-19 23:20:03       18 阅读
  5. android 网络检测简单方法

    2024-03-19 23:20:03       19 阅读
  6. 【C语言】数组基础

    2024-03-19 23:20:03       19 阅读
  7. Linux作业

    2024-03-19 23:20:03       16 阅读
  8. 网页的制作

    2024-03-19 23:20:03       16 阅读
  9. 关于我的经历

    2024-03-19 23:20:03       21 阅读
  10. 【笔记】Linux常用命令

    2024-03-19 23:20:03       17 阅读
  11. PHP使用AES进行加解密

    2024-03-19 23:20:03       18 阅读