C语言 | 进阶修炼

目录

前言 - 文章概述

我是Luckyhorse5。

  • 本文是我在C语言进阶系列中的学习记录,也是我的第一篇博客。
  • 文章内容分为七大章节:深入剖析数据在内存中的存储、指针进阶、字符函数和字符串函数、自定义类型、动态内存管理、文件操作、程序环境和预处理。
  • 本文用时29天完成。在Markdown编辑器下共56780字。
  • 如有需要完善的地方或建议,请批评指正。

深入剖析数据在内存中的存储

数据的类型

整型家族

  • char
  • short
  • int
  • long

浮点型家族

  • float
  • double

构造类型 - 自定义类型

  • 数组类型
  • 结构体类型struct
  • 枚举类型enum
  • 联合类型union

指针类型

  • char*
  • int*

空类型

  • 函数返回类型void test()
  • 函数参数void test(void) - 无参
  • 指针 void* p

我们主要探讨整型浮点型在内存中的存储

整型在内存中的存储

整数二进制的三种表示形式及其在内存中的存储形式

  • 原码
  • 反码
  • 补码

整数在内存中存储的是补码

补码和原码的相互转换

正整数原反补相同

负整数补码和原码相互转换的步骤

  1. 除了符号位 其余位取反
  2. 末位加1

运算1-1

CPU上是没有减法器的,只有加法器。
所以运算1-1实质上是运算1+(-1)

用原码运算
00000000 00000000 00000000 00000001
10000000 00000000 00000000 00000001
相加之后是 -2,很明显是答案是错误的
用补码运算
01111111 11111111 11111111 11111111
11111111 11111111 11111111 11111111
10000000 00000000 00000000 00000000 0(前面多一位的1丢掉)
00000000 00000000 00000000 00000000
结果是0,答案正确

大小端的引入及介绍

我们先来看一段代码:

#include <stdio.h>
int main()
{
	int a = 10;
	int b = -10;
	return 0;
}

a的补码是:
00000000 00000000 00000000 00001010
转化为十六进制是:
00 00 00 0a
b的补码是:
11111111 11111111 11111111 11110110
转化为十六进制是:
ff ff ff f6

下面我们来看一下a,b在内存中的存储情况
a,b在内存中的存储情况
我们发现,内存中是“倒着”存储的,这是为什么呢?

这是因为分为大小端两种储存模式(大端字节序和小端字节序)

大端模式
是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;

小端模式
是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中;

如何写一段代码来判断是大端模式还是小端模式呢?

#include <stdio.h>
int main()
{
	//写代码判断当前及其的字节序
	int a = 1;//创建整型变量a 值为1
	char* p = (char*)&a;
	//将a的地址取出并强制转化为char*字符指针类型并赋给p
	//如果是01 00 00 00则为小端,反之为大端
	if (*p == 1)
		printf("小端");
	else
		printf("大端");
	return 0;
}

char类型是signed char还是 unsigned char?

C语言标准没有规定,取决于编译器(大部分是signed char)
int 是 signed int
short 是 signed short

char类型变量的取值范围

有符号的char的取值范围是 -128 – 127
00000001 - 1
00000010 - 2

01111111 - 127
10000000 - (因为是负数,且无法减1再取反求原码)直接解析成 -128
10000001 - -127 (负数 先减1 然后取反 得到原码)

11111110 - -2
11111111 - -1

10000000解析成 -128的原因
原10000000 00000000 00000000 10000000
反11111111 11111111 11111111 01111111
补11111111 11111111 11111111 10000000 取后面的10000000

例题1

#include <stdio.h>
int main()
{
	char a = -1;
	//补码 11111111 11111111 11111111 11111111
	//a里存的是 11111111
	signed char b = -1;
	//11111111
	unsigned char c = -1;
	//11111111

	printf("a=%d,b=%d,c=%d", a, b, c);//-1 -1 255
	//整型提升
	//a b - 负数 - 提升的时候补1
	//c - 无符号 - 提升的时候补0
	//00000000 00000000 00000000 11111111 - 正数原反补相同
	//就是255
	return 0;
}

例题2

#include <stdio.h>
int main()
{
	char a = -128;
	//11111111 11111111 11111111 10000000 - 补码
	//a中存的是10000000
	//负数整型提升补1
	//11111111 11111111 11111111 10000000
	printf("%u\n", a);//无符号型直接打印 答案是4294967168
	printf("%d\n", a);//需要取反加1算原码 答案是-128
	return 0;
}

例题3

#include <stdio.h>
int main()
{
	char a = 128;
	//00000000 00000000 00000000 10000000 - 原反补
	//10000000 由于被截断后符号位是1,代表负数,所以整型提升补1
	//负数整型提升补1
	//11111111 11111111 11111111 10000000
	printf("%u\n", a);//答案是4294967168
	return 0;
}

例题4

#include <stdio.h>
int main()
{
	int i = -20;
	unsigned int j = 10;
	//11111111 11111111 11111111 11101100 -20的补码
	//00000000 00000000 00000000 00001010 10的补码
	//11111111 11111111 11111111 11110110(补码相加后)
	printf("%d\n", i + j);//10000000 00000000 00000000 00001010(转换为原码为-10)
	//结果是10
	printf("%u\n", i + j);//11111111 11111111 11111111 11110110(无符号直接打印)
	//结果是4294967286
	return 0;
}

例题5

#include <stdio.h>
int main()
{
	unsigned int i;
	for (i = 9; i >= 0; i--)
	{
		printf("%u\n", i);//i是无符号类型,始终>=0
		//程序死循环
	}
	return 0;
}

例题6

#include <string.h>
#include <stdio.h>
int main()
{
	char a[1000];
	int i = 0;
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	// -1 -2 -3 ... -127 -128 127 126 125 ... 3 2 1 0 -1 -2 ...
	printf("%d", strlen(a));//找到'\0' ASCII码是0 就是找到0
	//128 + 127 = 255
	//答案是255
	return 0;
}

例题7

#include <stdio.h>
unsigned char i = 0;//全局变量
int main()
{
	for (i = 0; i <= 255; i++)
	{
		printf("hello world\n");
	}
	//unsigned char 取值范围 0 - 255
	//程序死循环
	return 0;
}

浮点型在内存中的存储

二进制浮点数表示形式

根据国际标准IEEE(电气和电子工程协会)754,任何一个二进制浮点数V可以表示成下面的形式:

  • (-1)*S * M * 2^E
  • (-1)*S表示符号位,当S=0,V为正数;当S=1,V为负数
  • M表示有效数字,大于等于1,小于2
  • 2^E表示指数位

举例来说:
十进制的5.0,写成二进制时101.0,相当于1.01 * 2 ^ 2
S=0,M=1.01,E=2
十进制的-5.0,写成二进制时101.0,相当于1.01 * 2 ^ 2
S=1,M=1.01,E=2

IEEE 754规定:
对于32位的浮点数,最高的1位是符号位S,接着的8位是指数E,剩下的23位是有效数字M
单精度浮点数存储模型
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位是有效数字M
双精度浮点数存储模型

IEEE 754对有效数字M和指数E的特别规定

有效数字M

IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的部分。
比如1.01保存的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去之后,可以保存24位有效数字。

指数E

首先,E作为一个无符号整数(unsigned int),意味着如果E为8位,它的取值范围是0 - 255 ;如果E为11位,它的取值范围是0 - 2047。但是科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时的E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

比如0.5的二进制表示为0.1 表示为(-1)*0 * 1 * 2^-1
即S = 0;M = 1;E = -1
储存的时候S为0;E为 -1+127 = 126;M除去前面的1后值为0
则其二进制的表示形式为:
0 01111110 00000000000000000000000

然后指数E从内存中取出分为三种情况:

  • E不全为0或不全为1

这时浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M加上第一位的1。

  • E全为0

这时浮点数的指数E等于1-127(或1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原成0.xxxxxx的小数,这样做是为了表示±0,以及接近于0很小的数字。

  • E全为1
    这时如果有效数字M全为0,表示±无穷大(正负取决于符号位S)

例题

#include <stdio.h>
int main()
{
	int n = 9;//4 byte (32bit)
	//00000000 00000000 00000000 00001001
	float* pFloat = (float*)&n;//取出n的地址强制转化为 float*类型
	//0 00000000 00000000000000000001001
	//s  E          M
	printf("n的值为:%d\n", n);//打印的是9
	printf("*pFloat的值为:%f\n", *pFloat);//打印的是0.000000                                                                                                                                                                                                                                                                                                                                         

	*pFloat = 9.0;
	//1001.0 -- 1.001 * 2^3 (E = 3 +127 = 130)
	//s=0 E = 10000010  M=00100000000000000000000
	//0 10000010 00100000000000000000000
	// --> 01000001000100000000000000000000
	printf("num的值为:%d\n", n);//打印的是1091567616
	printf("*pFloat的值为:%f\n", *pFloat);//打印的是9.000000
	return 0;
}

指针进阶

先来回顾一下初阶学习中指针的相关知识:

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间
  2. 指针的大小是固定的4/8个字节(32位平台/64位平台)
  3. 指针有类型,指针的类型决定了指针的+ -整数的步长,指针解引用操作的时候的权限
  4. 指针的运算

字符指针

字符指针可以指向一个字符

#include <stdio.h>
int main()
{
	char ch = 'q';
	char* pc = &ch;
	return 0;
}

字符指针也可以指向一个字符串

#include <stdio.h>
int main()
{
	const char* ps = "Luckyhorse5";
	//这里的字符串是常量字符串,不能更改
	printf("%c\n", *ps);//打印结果是 L
}

我们发现打印的结果是L
本质上是把"hello world"这个字符串的首字符的地址存储在了ps中
同时此字符串是常量字符串,不能通过*ps = ‘x’来修改

我们要区分char*指向字符串和char arr[]数组存放字符串

#include <stdio.h>
int main()
{
	char arr[] = "Luckyhorse5";
	printf("%s\n", arr);//打印结果是 Luckyhorse5
	return 0;
}

打印结果是Luckyhorse5
本质上是把整个字符串放到arr数组里

例题

#include <stdio.h>
int main()
{
	char str1[] = "hello";
	char str2[] = "hello";
	const char* str3 = "hello";
	const char* str4 = "hello";
	if (str1 == str2)
		printf("str1 and str2 are same\n");
	else
		printf("str1 and str2 are not same\n");
	if (str3 == str4)
		printf("str3 and str4 are same\n");
	else
		printf("str3 and str4 are not same\n");
	return 0;
}

运行结果是:
str1 and str2 are not same
str3 and str4 are same

str1和str2中存放的是字符串变量,str1开辟一块空间,str2开辟一块空间,地址是不同的。而数组名代表首元素的地址,所以str1和str2指向的地址也是不同的。

str3和str4所指向的是常量字符串,str3和str4指向同一块地址,都指向hello的首元素h。

指针数组

指针数组的概念

指针数组是一个存放指针的数组
也就是说:数组中存放的是指针(地址)

先来回顾一下下面指针数组是什么意思:

  • int* arr1[10]; //整型指针的数组
  • char* arr2[3]; //一级字符指针的数组
  • char** arr3[5]; //二级字符指针的数组

一段简单粗糙的代码

#include <stdio.h>
int main()
{
	int* arr1[10];//存放整型指针的数组
	int a = 10, b = 20, c = 30;
	int* arr[3] = { &a,&b,&c };//存放a,b,c三个变量的地址
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", *arr[i]);//解引用遍历打印
	}
	return 0;
}

打印结果是:
10 20 30

我们看到,int* 类型中可以储存整型指针(地址)。

下面研究更高级一点的代码

#include <stdio.h>
int main()
{
	int a[5] = { 1,2,3,4,5 };
	int b[] = { 2,3,4,5,6 };
	int c[] = { 3,4,5,6,7 };
	int* arr[3] = { a,b,c };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			printf("%d ", *(arr[i]+j));
			//假设a,b,c定义为1,2,3行,i就代表了行
			//后面的+j是指向在第i行第j+1个元素
			//达到循环遍历的效果
		}
		printf("\n");//打印完一行换行
	}
	return 0;
}

打印结果是:
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7

看起来是不是与二维数组很像?

实际上,用二维数组的形式来打印,也能实现最终结果

//printf("%d ", *(arr[i]+j));
printf("%d ", arr[i][j]);
//打印结果同上

但这是模拟出来的二维数组,实际并不是二维数组。
二维数组的存放是独立的,一行存放完紧接着存放下一行。
而此代码中的三行是完全独立的,是被三个指针联系起来了。

数组指针

数组指针的定义

数组指针是指针呢?还是数组呢?
答案是:指针

我们已经熟悉:

  • 整型指针int* pint; 能够指向整型数据的指针
  • 浮点型指针float* pfloat; 能够指向浮点型数据的指针

那数组指针应该是:能够指向数组的指针。

来看一段代码

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5 };
	int (*parr)[10] = &arr;//取出的是数组的地址
	return 0;
}

代码中的parr就是一个数组指针,它指向数组的地址。

类似的代码

#include<stdio.h>
int main()
{
	double* d[5];
	double* (*pd)[5] = &d;
	return 0;
}

代码中的pd也是一个数组指针。

区分:&数组名 和 数组名

回顾一下初阶学习中关于数组名的知识:

数组名是首元素的地址

但是有两个例外

  1. sizeof(数组名) - 数组名表示整个数组,计算的是整个数组大小,单位是字节。
  2. &数组名 - 数组名表示整个数组,取出的是整个数组的地址

我们用一段代码加以区分

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p1 = arr;
	int (*p2)[10] = &arr;
	printf("%p\n", p1);
	printf("%p\n", p1 + 1);//地址相差4
	
	printf("%p\n", p2);
	printf("%p\n", p2 + 1);//地址相差40
	return 0;
}

虽然&arr和arr的值是一样的,但意义是不同的。

&arr表示的是数组的地址,而不是数组首元素的地址。
数组的地址+1,跳过整个数组的大小,所以&arr+1相对于&arr的差值是40。

数组指针的使用

先来看一段代码

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int(*pa)[10] = &arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *((*pa) + i));
	}
	return 0;

打印结果是:
1 2 3 4 5 6 7 8 9 10
pa指向数组arr的地址, * pa指向数组arr的首元素, * pa + i 指向数组arr的第 i + 1个元素, *( ( *pa ) + i )就是数组arr的第 i + 1个元素。

但我们一般很少这样写代码

例题: 打印二维数组中的元素

二维数组数组名表示首元素的地址
二维数组的首元素是第一行!

学习数组指针前,我们可以这样写代码:

#include<stdio.h>
void print1(int arr[3][5], int r, int c)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6 },{3,4,5,6,7} };
	print1(arr, 3, 5);
	return 0;
}

学习数组指针后,我们可以这样写代码:

#include<stdio.h>
void print2(int(*pa)[5], int r, int c)//pa是一个数组指针
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ", *(*(pa + i) + j));
			//pa是数组指针 指向一行,*pa是指向一行的首元素
			//*(pa + i)是指向第i + 1行的首元素
			//*(pa + i) + j 是指向第i + 1行的第j + 1个元素
			//再解引用 得到 第i + 1行的第j + 1个元素
		}
		printf("\n");//打印一行之后换行
	}
}
int main()
{
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
	print2(arr, 3, 5);
	return 0;
}

回顾下面代码的意义

int arr[5];//整型数组

int* parr1[10];//整型指针的数组

int (*parr2)[10];//数组指针,该指针能指向一个数组,数组10个元素,每个元素的类型是 int

int (*parr3[10])[5];//parr3是一个存储数组指针的数组,该数组能存放10个数组指针,每个数组指针能指向一个数组,数组5个元素,每个元素是int类型

数组传参和指针传参

写代码的时候难免要把【数组】或【指针】传给参数,那函数的参数如何设计呢?

一维数组传参

我们有下面一段代码:

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };//存放int*的数组
	test(arr);
	test2(arr2);
	return 0;
}

我们有下面几种方式来传参:

  • test() 方式1
void test(int arr[])
{}

这种传参方式是可行的。
形参用一个数组接收天然可行,因为实参是arr,即数组首元素的地址,所以接收的时候并非整个数组传过去,因此可以省略大小。这里的arr[]只是便于大家理解,写成了数组的形式,其实本质上是一个指针。

  • test() 方式2
void test(int arr[10])
{}

这种传参方式是可行的。
如上所讲,arr[10]中的10可以省略,写成10,100,1000都是可行的,原因是本质上test函数接收的并非整个数组,而是数组首元素的地址,所以不会根据数组大小去创建真正的数组。

  • test() 方式3
void test(int* arr)
{}

这种传参方式是可行的。
形参传的是数组首元素的地址,既然是一个地址,所以用指针接收是可行的。

test2() 方式1

void test2(int* arr[])
{}

这种传参方式是可行的。
int * arr2[10] = { 0 }; 这条语句代表创建了一个指针数组arr2,数组有10个元素,每个元素的类型为int*,并全部初始化为0。所以形参接收的时候也用一个指针数组类型接收,当然可行。

test2() 方式2

void test2(int** arr)
{}

这种传参方式是可行的。
arr2是一个指针数组类型,里面放的是指针。arr2代表这个指针数组的首元素地址。arr2这个地址存放的是一个int*类型的指针。存放指针的地址,就是一个二级指针。因此实参本质上是一个二级指针,所以形参用二级指针接收是可行的。

二维数组传参

二维数组首元素的地址为第一行的地址,即整个一维数组的地址

我们有下面一段代码:

#include<stdio.h>
int main()
{
	int arr[3][5] = { 0 };
	test(arr);
	return 0;
}

我们有下面几种方式来传参:

  • test() 方式1
void test(int arr[3][5])
{}

这种传参方式是可行的。
二维数组传参,用一个二维数组接收,自然可行。

  • test() 方式2
void test(int arr[][5])
{}

这种传参方式是可行的。
二维数组的行能省略,列不能省略。且5要和形参的数组列相同。这是因为形参arr代表二维数组的首地址,即整个一维数组的地址,所以我必须知道地址结束的地方,因此5不能省略。

  • test() 方式3
void test(int*p)[5])
{}

这种传参方式是可行的。
arr代表了二维数组中第一行的地址,也就是实参是一个一维数组的地址,所以我用一个数组指针类型去接收是可行的。

一级指针传参

我们有这样一段代码:

#include<stdio.h>
void print(int* ptr,int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(ptr + i));
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;//p是一级指针
	int sz = sizeof(arr) / sizeof(arr[0]);
	print(p,sz);
	return 0;
}

p作为一级指针,形参在接收的时候用int*类型来接收是非常合理的。

那假如我们形参接收的时候用char*类型,实参我们可以传什么过去呢?

#include<stdio.h>
void test(char* p)
{}
int main()
{
	char ch = 'w';
	char* p1 = &ch;
	test(&ch);
	test(p1);
	return 0;
}

我们看到当形参用char* 类型来接收的时候,我们可以:

  • 将ch的地址传过去
  • 可以将ch的地址放到char*类型的p1中,再将p1传过去。

二级指针传参

#include<stdio.h>
void test(int** p2)
{
	**p2 = 20;
}
int main()
{
	int a = 10;
	int* pa = &a;//pa是一级指针
	int** ppa = &pa;//ppa是二级指针
	//把二级指针进行传参
	test(ppa);//传二级指针变量
	test(&pa);//传一级指针变量的地址
	int* arr[10] = { 0 };
	test(arr);//传存放一级指针的数组
	printf("%d\n", a);//打印结果是20
	return 0;
}

我们看到,当形参我们用int**类型接收的时候,我们可以:

  • 传二级指针变量
  • 传一级指针变量的地址
  • 传存放一级指针的数组

函数指针

什么是函数指针

函数指针是存放函数地址的指针

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	//&函数名 - 取到的就是函数的地址
	printf("%p\n", &Add);
	//pf就是一个函数指针变量
	int (*pf)(int,int) = &Add;
	return 0;
}
#include <stdio.h>
void test(char* str)
{}
int main()
{
	void (*pt)(char*) = &test;//pt是一个函数指针变量
	return 0;
}

&函数名和函数名的区别

我们之前学习过&数组名和数组名的区别,二者意义是不同的
但是&函数名和函数名有没有区别呢?

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	printf("%p\n", &Add);
	printf("%p\n", Add);
	int (*pf)(int,int) = &Add;
	return 0;
}

我们发现,两次打印的结果是一样的,二者实际上是等价的。

等价的书写方式

我们知道&函数名和函数名是等价的,那是不是可以在调用的时候书写更加简洁呢?

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pf)(int, int) = Add;//Add === pf
	int ret1 = (*pf)(3, 5);//1
	int ret2 = pf(3, 5);//2
	int ret3 = Add(3, 5);//3
	//三种方式等价
	printf("%d %d %d\n", ret1,ret2,ret3);
	return 0;
}

实际上在第一种方式中,pf前面的*是没有实际意义的。

两段有趣的代码

代码1

int main()
{
	(*(void (*)())0)();
	return 0;
}

代码解读:
调用0地址处的函数,该函数无参,返回类型是void

  • void ( * ) ( ) - 函数指针类型
  • (void ( * ) ( ) ) 0 - 对0进行强制类型转换,被解释为一个函数地址
  • *(void ( * ) ( ) ) 0 - 对0地址进行了解引用操作
  • (*(void ( * ) ( ) ) 0 ) ( ) - 调用0地址处的函数

代码2

int main()
{
	void (*signal(int, void(*)(int)))(int);
}
  • signal和()先结合,说明signal是函数名
  • signal函数的第一个参数的类型是int,第二个参数的类型是函数指针
    该函数指针,指向一个参数为int,返回类型是void的函数
  • signal函数的返回类型也是一个函数指针
    该函数指针,指向一个参数为int,返回类型是void的函数 signal是一个函数的声明

typedef - 对类型进行重定义

#include <stdio.h>
int main()
{
	typedef void(*pfun_t)(int);
	//对void(*)(int)的函数指针类型重命名为pfun_t
	typedef unsigned int uint;
	//把unsigned int重命名为uint
}

假设我们对void(*)(int)的函数指针类型重命名为pfun_t
那上面的代码2可以写的更加简洁:

int main()
{
	void (*signal(int, void(*)(int)))(int);
	pfun_t signal(int, pfun_t);
}

函数指针数组

什么是函数指针数组?

函数指针数组是存放函数指针的数组

用代码来表示函数指针数组

int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int main()
{
	int (*pf1)(int, int) = Add;//pf1是一个函数指针
	int (*pf2)(int, int) = Sub;//pf2是一个函数指针
	int (*pfArr[2])(int, int) = { Add,Sub };//pfArr就是函数指针数组
	return 0;
}

代码中的 pfArr 就是函数指针数组

函数指针数组的运用

我们来用代码写一个计算器吧O.o

void menu()
{
	printf("*************************\n");
	printf("****  1.add   2.sub  ****\n");
	printf("****  3.mul   4.div  ****\n");
	printf("****     0. exit     ****\n");
	printf("*************************\n");
}
int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
int main()
{
	//计算器 - 计算整型变量的加减乘除
	int input = 0;
	do
	{
		menu();
		//pfArr就是函数指针数组
		int (*pfArr[5])(int, int) = { NULL,Add,Sub,Mul,Div };
		int x = 0;
		int y = 0;
		int ret = 0;
		printf("请输入你的选择:>");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入2个操作数:>");
			scanf("%d %d", &x, &y);
			ret = (pfArr[input])(x, y);
			printf("ret = %d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出游戏\n");
		}
		else
		{
			printf("输入错误,请重新输入0.o\n");
		}
	} while (input);
	return 0;
}

指向函数指针数组的指针

用代码来表示指向函数指针数组的指针

int main()
{
	int(*p1)(int, int);//函数指针
	int(*p2[4])(int, int);//函数指针数组
	int(*(*p3)[4])(int, int) = &p2;//取出的是函数指针数组的地址
	//p3就是一个指向【函数指针数组】p2的指针
	return 0;
}

代码中p3就是一个 指向【函数指针数组】p2 的指针

数组元素类型 和 数组类型 的区分

int main()
{
	int arr[10];
	//数组元素类型 - int
	//arr数组的类型 - int [10]
	return 0;
}

回调函数

回调函数的概念

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于该事件或条件进行响应。

tip:函数递归是自己调用自己,跟回调函数没有关系,不要搞混

运用回调函数机制再来写一个计算器

void menu()
{
	printf("*************************\n");
	printf("****  1.add   2.sub  ****\n");
	printf("****  3.mul   4.div  ****\n");
	printf("****     0. exit     ****\n");
	printf("*************************\n");
}
int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
int Calc(int (*pf)(int, int))
{
	int x = 0;
	int y = 0;
	printf("请输入2个操作数:>");
	scanf("%d %d", &x, &y);
	return pf(x, y);
}
int main()
{
	//计算器 - 计算整型变量的加减乘除
	int input = 0;
	do
	{
		menu();
		int ret = 0;
		printf("请输入你的选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			ret = Calc(Add);
			printf("ret = %d\n", ret);
			break;
		case 2:
			ret = Calc(Sub);
			printf("ret = %d\n", ret);
			break;
		case 3:
			ret = Calc(Mul);
			printf("ret = %d\n", ret);
			break;
		case 4:
			ret = Calc(Div);
			printf("ret = %d\n", ret);
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("输入错误,重新选择吧…\n");
			break;
		}
	} while (input);
	return 0;
}

qsort函数

什么是qsort函数?

qsort函数是一个C语言编译器函数库自带的排序函数
qsort - quick sort

冒泡排序回顾

在学习qsort函数前,我们先来回顾一下之前学过的冒泡排序吧O.o

void bubble_sort(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		//一趟冒泡排序
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				//交换
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}
void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
	int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print_arr(arr, sz);//排序前打印
	bubble_sort(arr, sz);
	print_arr(arr, sz);//排序后打印
	return 0;
}

qsort函数的使用方法

void qsort(
	void* base, //base中存放的是待排序数据中第一个对象的地址
	size_t num, //排序数据元素的个数
	size_t width, //排序数据中一个元素的大小,单位是字节
	int(__cdecl* compare)(const void* elem1, const void* elem2)
		);//(函数指针)用来比较待排序数据中的2个元素的函数

使用qsort函数排序整型数据

#include <stdlib.h> //qsort函数需要引用的头文件
#include <stdio.h>
int cmp_int(const void* e1, const void* e2)
{
	//e1和e2是void*类型,无法确定解引用权限空间的大小
	//将e1和e2强制类型转换为int*类型,可以确定解引用权限有4个字节
	return *(int*)e1 - *(int*)e2;//升序
	//如果需要降序就把e1和e2交换一下位置
}
void print_arr(int arr[], int sz) //打印数组的函数
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
	//整型数据的排序
	
	int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);//排序
	print_arr(arr, sz);//打印
	return 0;
}

使用qsort函数排序结构体数据

#include <stdlib.h> //qsort函数需要引用的头文件
#include <string.h> //strcmp函数需要引用的头文件
#include <stdio.h>
struct Stu //结构体的创建
{
	char name[20];
	int age;
};
int sort_by_age(const void* e1, const void* e2)
{
	//将e1和e2强制类型转换为结构体指针类型
	//转化为结构体指针类型后用 -> 访问 age
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int sort_by_name(const void* e1, const void* e2)
{
	//用strcmp函数来比较字符串大小
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void test2()
{
	//使用qsort函数排序结构体数据
	
	struct Stu s[] = { {"zhangsan",30},{"lisi",34} ,{"wangwu",20} };
	//结构体初始化
	
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s,sz, sizeof(s[0]),sort_by_age);//按照年龄来排序
	qsort(s, sz, sizeof(s[0]), sort_by_name);//按照名字来排序
}

回顾strcmp函数 - 比较字符串大小

比较字符串大小需要<string.h>中的库函数strcmp

那strcmp怎么比较abcq和amc呢?
答案是:每位分别对应比较
abcq
amc
因为m的ASCII码值大于b的,所以字符串amc大于字符串abcq

模仿qsort实现一个冒泡排序的通用算法

我们写一个bubble_sort函数来实现qsort的功能吧O.o

void Swap(char* buf1, char* buf2, int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}
void bubble_sort(void* base,
				int sz,
				int width,
				int (*cmp)(const void* e1, const void* e2)
				)
{
	int i = 0;
	//sz个元素就有sz-1趟
	for (i = 0; i < sz - 1; i++)
	{
		//一趟的排序
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			//两个元素比较
			if (cmp((char*)base+j*width, (char*)base + (j+1)* width) > 0)
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}

为什么要将base的类型转化为char * 呢?
因为char * 访问一个字节,可以作为一个单位长度。

使用bubble_sort函数排序整型数据

#include <stdio.h>
void Swap(char* buf1, char* buf2, int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}
void bubble_sort(void* base,
				int sz,
				int width,
				int (*cmp)(const void* e1, const void* e2)
				)
{
	int i = 0;
	//sz个元素就有sz-1趟
	for (i = 0; i < sz - 1; i++)
	{
		//一趟的排序
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			//两个元素比较
			if (cmp((char*)base+j*width, (char*)base + (j+1)* width) > 0)
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}
void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}
int main()
{
	//整型数据的排序
	int arr[10] = { 8,9,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);//排序
	print_arr(arr, sz);
	return 0;
}

使用bubble_sort函数排序结构体数据

#include <stdio.h>
struct Stu
{
	char name[20];
	int age;
};
int sort_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int sort_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void Swap(char* buf1, char* buf2, int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}
void bubble_sort(void* base,
	int sz,
	int width,
	int (*cmp)(const void* e1, const void* e2)
)
{
	int i = 0;
	//sz个元素就有sz-1趟
	for (i = 0; i < sz - 1; i++)
	{
		//一趟的排序
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			//两个元素比较
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}
int main()
{
	//使用qsort函数排序结构体数据
	struct Stu s[] = { {"zhangsan",30},{"lisi",34} ,{"wangwu",20} };
	int sz = sizeof(s) / sizeof(s[0]);
	//按照年龄来排序
	bubble_sort(s,sz, sizeof(s[0]),sort_by_age);
	//按照名字来排序
	bubble_sort(s, sz, sizeof(s[0]), sort_by_name);
	return 0;
}

指针和数组练习解析

第一组

#include <stdio.h>
int main()
{
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));//16
	//sizeof(a)是整个数组的大小
	printf("%d\n", sizeof(a + 0));//4/8
	//a + 0 是第一个元素的地址 sizeof(a + 0)计算的是地址的大小
	printf("%d\n", sizeof(*a));//4
	//*a 是数组的第一个元素 sizeof(*a)计算的是第一个元素的大小
	printf("%d\n", sizeof(a + 1));//4/8
	//a + 1 是第二个元素的地址 sizeof(a + 1)计算的是地址的大小
	printf("%d\n", sizeof(a[1]));//4
	//计算的是第二个元素的大小

	printf("%d\n", sizeof(&a));//4/8
	//&a虽然是数组的地址,但也是地址,sizeof(&a)计算的是一个地址的大小
	printf("%d\n", sizeof(*&a));//16
	//&a是整个数组的地址,解引用后是整个数组,sizeof(*&a)是整个数组的大小
	//*和&可以抵消 相当于sizeof(a)
	printf("%d\n", sizeof(&a + 1));//4/8
	//&a + 1 是跳过整个数组后下一块空间的地址
	printf("%d\n", sizeof(&a[0]));//4/8
	//&a[0] 是第一个元素的地址
	printf("%d\n", sizeof(&a[0] + 1));//4/8
	//(&a[0] + 1) 是第二个元素的地址
	return 0;
}

第二组

#include <stdio.h>
int main()
{
	//字符数组
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", sizeof(arr));//6
	//整个数组的大小
	printf("%d\n", sizeof(arr + 0));//4/8
	//数组首元素地址的大小
	printf("%d\n", sizeof(*arr));//1
	//数组首元素的大小
	printf("%d\n", sizeof(arr[1]));//1
	//数组第二个元素的大小
	printf("%d\n", sizeof(&arr));//4/8
	//整个数组地址的大小
	printf("%d\n", sizeof(&arr + 1));//4/8
	//跳过整个数组后 后面的地址的大小
	printf("%d\n", sizeof(&arr[0] + 1));//4/8
	//数组第二个元素地址的大小
	return 0;
}

第三组

#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", strlen(arr));
	//strlen在内存中找到 '\0'才会停止 - 随机值a
	printf("%d\n", strlen(arr + 0));
	//随机值a(同上)
	printf("%d\n", strlen(*arr));
	//*arr是字符a,会把97当成地址,会出错
	printf("%d\n", strlen(arr[1]));
	//arr[1]是字符b,ASCII码是98,会出错
	printf("%d\n", strlen(&arr));
	//取出的是数组的地址,类型会从char(*)[6]数组指针类型强制转换为char*类型
	//随机值a
	printf("%d\n", strlen(&arr + 1));
	//跳过数组后,继续找'\0' - 随机值 
	//如果说上面的随机值是a,那这个随机值其实是a-6
	printf("%d\n", strlen(&arr[0] + 1));
	//从b的地址往后找'\0' - 随机值
	//随机值是a-1

	//影响随机值的因素就是内存中'\0'的位置
	return 0;
}

第四组

#include <stdio.h>
int main()
{
	char arr[] = "abcdef";
	//[a b c d e f \0]
	printf("%d\n", sizeof(arr));//7
	//整个数组的大小
	printf("%d\n", sizeof(arr + 0));//4/8
	//首元素地址的大小
	printf("%d\n", sizeof(*arr));//1
	//首元素的大小
	printf("%d\n", sizeof(arr[1]));//1
	//第二个元素的大小
	printf("%d\n", sizeof(&arr));//4/8
	//整个数组地址的大小 取出arr地址的类型是char(*)[7]
	printf("%d\n", sizeof(&arr + 1));//4/8
	//跳过整个数组后,后面的地址大小
	printf("%d\n", sizeof(&arr[0] + 1));//4/8
	//第二个元素地址的大小
	return 0;
}

第五组

#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = "abcdef";
	// [a b c d e f \0]
	//strlen在内存中找到 '\0'才会停止
	printf("%d\n", strlen(arr));//6
	printf("%d\n", strlen(arr + 0));//6
	printf("%d\n", strlen(*arr));//err
	printf("%d\n", strlen(arr[1]));//err
	printf("%d\n", strlen(&arr));//6
	printf("%d\n", strlen(&arr + 1));//随机值
	printf("%d\n", strlen(&arr[0] + 1));//5
	return 0;
}

第六组

#include <stdio.h>
int main()
{
	char* p = "abcdef"; //const char* p = "abcdef";
	printf("%d\n", sizeof(p));//4/8
	//指针变量的大小(a的地址的大小)
	printf("%d\n", sizeof(p + 1));//4/8
	//b的地址的大小
	printf("%d\n", sizeof(*p));//1
	//元素a的大小
	printf("%d\n", sizeof(p[0]));//1
	//p[0] 等价于 *(p+0) - 元素a的大小
	printf("%d\n", sizeof(&p));//4/8
	//p是指针变量 取p的地址 但还是地址的大小
	printf("%d\n", sizeof(&p + 1));//4/8 
	//跳过p 指向p后面的地址 还是地址的大小
	printf("%d\n", sizeof(&p[0] + 1));//4/8
	//&p[0]是a的地址,&p[0] + 1就是b的地址 b的地址的大小
	return 0;
}

第七组

#include <stdio.h>
#include <string.h>
int main()
{
	char* p = "abcdef"; //const char* p = "abcdef";
	printf("%d\n", strlen(p));//6
	//从a的地址往后找'\0'
	printf("%d\n", strlen(p + 1));//5
	//从b的地址往后找'\0'
	printf("%d\n", strlen(*p));//err
	//*p就是字符a 传的是ASCII码97
	printf("%d\n", strlen(p[0]));//err
	//p[0] 等价于 *(p+0) - 元素a 传的是ASCII码97
	printf("%d\n", strlen(&p));//随机值a
	//p里面存的东西未知 - 而且可能有'\0'也可能没有
	//&p是二级指针
	printf("%d\n", strlen(&p + 1));//随机值b
	//跳过p 后面存的内容也不清楚 
	//因为不确定前面p里面有没有'\0' 所以跟前面随机值无关
	//假如p里面没有'\0' 那b = a - 4 (a - 8)
	printf("%d\n", strlen(&p[0] + 1));//5
	//取出a的地址+1 - 从b的地址开始找'\0'
	return 0;
}

第八组

#include <stdio.h>
int main()
{
	//二维数组
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));//48
	//sizeof(数组名)表示整个数组
	printf("%d\n", sizeof(a[0][0]));//4
	//a[0][0] - 第一行第一个元素的大小
	printf("%d\n", sizeof(a[0]));//16
	//a[0]可以理解为第一行一维数组的数组名 算整个数组大小
	printf("%d\n", sizeof(a[0] + 1));//4/8
	//a[0]作为数组名没有单独在sizeof内部,所以代表第一行首元素的地址
	//a[0] + 1代表第一行第二个元素的地址
	printf("%d\n", sizeof(*(a[0] + 1)));//4
	//*(a[0] + 1)) 是第一行第二个元素
	printf("%d\n", sizeof(a + 1));//4/8
	//a代表的是二维数组首元素(第一行)的地址
	//a + 1代表的是第二行的地址 但还是地址
	printf("%d\n", sizeof(*(a + 1)));//16
	//*(a + 1) 等价于 a[1] 是第二行 里面有四个元素
	printf("%d\n", sizeof(&a[0] + 1));//4/8
	//取出整个第一行的地址后+1 表示第二行的地址
	printf("%d\n", sizeof(*(&a[0] + 1)));//16
	//解引用第二行的地址 - 第二行(四个元素)
	printf("%d\n", sizeof(*a));//16
	//a代表二维数组首元素(第一行)的地址 解引用得到第一行
	//实践上也是*(a + 0) 等价于 a[0]
	printf("%d\n", sizeof(a[3]));//16
	//a[3]其实是第四行的数组名(如果有的话)
	//所以即使不存在,也能通过类型计算大小的,不会去访问a[3]
	//没有第四行,但是我们知道a[3]的类型是int[4]
	
	//在C语言和Pascal语言中,对sizeof的处理都是在编译阶段进行,
	//所以只关心参数的类型,不关心参数最后的值是多少
	
	//3+5 表达式
	//1.值属性 - 8
	//2.类型属性 - int
	return 0;
}

总结

数组名的意义:

  • sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
  • &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
  • 除此之外所以的数组名都表示首元素的地址

字符函数和字符串函数

重点介绍处理字符和字符串的库函数的使用和注意事项

求字符串长度

strlen函数

strlen库函数的参数及返回类型

size_t strlen( const char* str );

strlen函数的使用方法及注意事项

#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = "abc";
	// [a,b,c,\0]
	int len = strlen(arr);
	printf("%d\n", len);
	return 0;
}
  • 字符串以 ‘\0’ 作为结束标志,strlen函数返回的是在字符串中 ‘\0’ 前面出现的字符个数(不包含 ‘\0’ )。
  • 参数指向的字符串必须要以 ‘\0’ 结束。
  • 注意函数的返回值是 size_t ,是无符号的。(易错)

模拟实现strlen函数

#include <stdio.h>
#include <assert.h>
int my_strlen(const char* str)
{
	int len = 0;//计数器
	assert(str != NULL);
	while (*str != '\0')
	{
		str++;
		len++;
	}
	return len;
}
int main()
{
	char arr[] = "abc";
	// [a,b,c,\0]
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}

这里模拟的时候,函数返回类型用的是有符号整型int。因为如果用my_strlen(a1)减去my_strlen(a2),有可能是负数。而库函数中是用无符号类型写。二者各有优势。

长度不受限制的字符串函数

strcpy函数

strcpy库函数的参数及返回类型

char* strcpy(char* destination,const char* source);

strcpy函数的使用方法及注意事项

#include <stdio.h>
#include <string.h>
int main()
{
	char arr[20] = { 0 };
	strcpy(arr, "hello");
	printf("%s\n", arr);
	return 0;
}
  • Copies the C strings pointed by source into the array pointed by destination, including the terminating null character (and stopping at that point).
  • 源字符串必须以 ‘\0’ 结束。
  • 会将源字符串中的 ‘\0’ 拷贝到目标空间。
  • 目标空间必须足够大,以确保能存放源字符串。
  • 目标空间必须可变。(例如 (const) char* p = “abc” 这种常量字符串就不可修改)

模拟实现strcpy函数

#include <stdio.h>
#include <assert.h>
char* my_strcpy(char* dest,const char* source)
{
	char* ret = dest;
	assert(dest && source);
	while (*source != '\0')
	{
		*dest = *source;
		dest++;
		source++;
	}
	*dest = *source;
	return ret;
}
int main()
{
	char arr[20] = { 0 };
	printf("%s\n", my_strcpy(arr, "hello"));
	return 0;
}

strcat函数

strcat库函数的参数及返回类型

char* strcat(char* destination, const char* source);

strcat函数的使用方法及注意事项

#include <stdio.h>
#include <string.h>
int main()
{
	char arr[20] = "hello-";
	strcat(arr, "world");//字符串追加
	printf("%s\n", arr);
	return 0;
}
  • 源字符串必须以 ‘\0’ 结束。
  • 目标空间必须足够大,能容下源字符串的内容。
  • 目标空间必须可修改。
  • 字符串不可以自己给自己追加,因为会一直覆盖 ‘\0’ 无限循环。

模拟实现strcat函数

#include <stdio.h>
#include <assert.h>
char* my_strcat(char* dest, const char* source)
{
	char* ret = dest;
	assert(dest && source);
	while (*dest != '\0')//找目标字符串中的'\0'
	{
		dest++;
	}
	while (*source != '\0')//追加源字符串,包含'\0'
	{
		*dest = *source;
		dest++;
		source++;
	}
	return ret;//返回目标空间的起始地址
}
int main()
{
	char arr1[20] = "hello-";
	char arr2[] = "world";
	printf("%s\n", my_strcat(arr1, arr2));
	return 0;
}

strcmp函数

strcmp库函数的参数及返回类型

int strcmp(const char* str1, const char* str2);

strcmp函数的使用方法及注意事项

#include <stdio.h>
#include <string.h>
int main()
{
	int ret1 = strcmp("abbb", "abbq");// < 0
	int ret2 = strcmp("abbb", "abbb");// = 0
	int ret3 = strcmp("abbb", "abba");// > 0
	printf("%d\n%d\n%d\n", ret1, ret2, ret3);
	return 0;
}
  • This function starts comparing the first character of each string. If they are equal to each other, it continues with the following pairs until the characters differ or until a terminating null-character is reached.
  • 标准规定:
    第一个字符串大于第二个字符串,则返回大于0的数字
    第一个字符串等于第二个字符串,则返回0
    第一个字符串小于第二个字符串,则返回小于0的数字

模拟实现strcmp函数

#include <stdio.h>
#include <assert.h>
int my_strcmp(const char* s1, const char* s2)
{
	assert(s1 && s2);
	while (*s1 == *s2)
	{
		if (*s1 == '\0')
		{
			return 0;
		}
		s1++;
		s2++;
	}
	return *s1 - *s2;
}
int main()
{
	char* p = "abcdef";
	char* q = "abbb";
	int ret = my_strcmp(p, q);
	if (ret > 0)
		printf("p > q\n");
	else if (ret < 0)
		printf("p < q\n");
	else
		printf("p == q\n");
	return 0;
}

长度受限制的字符串函数

strncpy函数

strncpy库函数的参数及返回类型

char* strncpy(char* destination, const char* source, size_t num);

strncpy函数的使用方法及注意事项

#include <stdio.h>
#include <string.h>
int main()
{
	char arr1[20] = "abcdefgh";
	char arr2[] = "qwer";
	strncpy(arr1, arr2, 2);
	printf("%s\n", arr1); //qwcdefgh
	char arr3[20] = "abcdefgh";
	char arr4[] = "qwer";
	strncpy(arr3, arr4, 6);//源字符串不够6个后面补\0
	//arr3 q w e r \0 \0 g h ...
	printf("%s\n", arr3);//qwer
	return 0;
}
  • 拷贝num个字符从源字符串到目标空间。
  • 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后面追加0,直到num个。

strncat函数

strncat库函数的参数及返回类型

char* strncat(char* destination, const char* source, size_t num);

strncat函数的使用方法及注意事项

#include <stdio.h>
#include <string.h>
int main()
{
	char arr1[20] = "hello-";
	char arr2[] = "worldwwww";
	strncat(arr1, arr2, 5);//追加5个后继续追加一个\0
	printf("%s\n", arr1);//hello-world
	char arr3[20] = "hello-";
	char arr4[] = "world";
	strncat(arr3, arr4, 9);//实际上追加了6个(包括\0)
	printf("%s\n", arr3);//hello-world
 	return 0;
}
  • Appends the first num characters of source to destination, plus a terminating null-character.
  • If the length of the C string in source is less than num, only the content up to the terminating null-character is copied.

strncmp函数

strncmp库函数的参数及返回类型

int strncmp(const char* str1, const char* str2, size_t num);

strncmp函数的使用方法及注意事项

#include <stdio.h>
#include <string.h>
int main()
{
	char* p = "abcdef";
	char* q = "abcqwert";
	int ret = strncmp(p, q, 4);//ret = -1
	printf("%d\n", ret);
	return 0;
}
  • 比较到出现一个字符不一样或者一个字符串结束或者num个字符全部比较完。

字符串查找

strstr函数

strstr库函数的参数及返回类型

char* strstr(const char*, const char*);

strsrt函数的使用方法及注意事项

#include <stdio.h>
#include <string.h>
int main()
{
	char arr1[] = "abcdefabcedf";
	char arr2[] = "bcd";
	//在arr1中查找是否包含arr2数组
	char* ret = strstr(arr1, arr2);
	if (ret == NULL)
		printf("没找到\n");
	else
	{
		printf("找到啦:%s\n", ret);
	}
	return 0;
}

模拟实现strstr函数

#include <stdio.h>
#include <assert.h>
char* my_strstr(const char* str1, const char* str2)
{
	assert(str1 && str2);
	const char* s1 = str1;
	const char* s2 = str2;
	const char* cp = str1;

	if (*str2 == '\0')
	{
		return (char*)str1;
	}
	while (*cp)
	{
		s1 = cp;
		s2 = str2;
		while (*s1 && *s2 && (*s1 == *s2))
		{
			s1++;
			s2++;
		}
		if (*s2 == '\0')
		{
			return (char*)cp;
		}
		cp++;
	}
	return NULL;
}
int main()
{
	char arr1[] = "abbbcsdwa";
	char arr2[] = "bqc";
	char* ret = my_strstr(arr1, arr2);
	if (ret == NULL)
		printf("没找到\n");
	else
	{
		printf("找到啦:%s\n", ret);
	}
	return 0;
}

KMP - 字符串查找算法

strtok函数

strtok库函数的参数及返回类型

char* strtok(char* str, const char* sep);

strtok函数的使用方法及注意事项

#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = "zzu_luckyhorse.best";
	char* p = "_.";
	char tmp[20] = { 0 };
	strcpy(tmp, arr);

	char* ret = NULL;
	for (ret = strtok(tmp, p); ret != NULL; ret = strtok(NULL, p))
	{
		printf("%s\n", ret);
	}
	return 0;
}
  • sep参数是个字符串,定义了用作分隔符的字符集合
  • 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符的标记
  • strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
  • strtok函数的第一个参数不为NULL,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
  • strtok函数的第一个参数为NULL,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
  • 如果字符串中不存在更多的标记,则返回NULL指针。

错误信息报告

strerror函数

strerror库函数的参数及返回类型

char* strerror(int errnum);

strerror函数的使用方法及注意事项

调用库函数失败的时候,都会设置错误码。
函数会返回错误码所对应的错误信息。

我们可以写一段代码来看一下不同数字对应的错误信息

#include <stdio.h>
#include <string.h>
int main()
{
	printf("%s\n", strerror(0));
	printf("%s\n", strerror(1));
	printf("%s\n", strerror(2));
	printf("%s\n", strerror(3));
	printf("%s\n", strerror(4));
	printf("%s\n", strerror(5));
	return 0;
}

使用strerror函数的情景:打开文件
全局的错误码errno(需要引用头文件 errno.h )

#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
	FILE* pf = fopen("test.txt", "r");
	//打开文件失败的时候会返回NULL
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));//找不到文件会报错
		return 1;
	}
	//读文件
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}
  • strerror函数把错误码转换成错误信息
  • 打印取决于自己

perror函数

perror库函数的参数及返回类型

void perror(const char* str);

perror函数的使用方法及注意事项

#include <stdio.h>//头文件也包含了perror函数
int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");//自定义的信息fopen
		//打印结果是:
		//fopen:No such file or directory
		return 1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
  1. 函数首先把错误码转化为错误信息
  2. 打印错误信息(包含了自定义的信息)

字符分类函数

函数如果它的参数符合下列条件就返回真
iscntrl任何控制字符
isspace空白字符:空格’ ',换页 ‘\f’ ,换行 ‘\n’ ,回车 ‘\r’ ,制表符 ‘\t’ ,或者垂直制表符 ‘\v’
isdigit十进制数字0-9
isxdigit十六进制数字,包括所有十进制数字,小写字母a-f,大写字母A-F
islower小写字母a-z
isupper大写字母A-Z
isalpha字母a-z或A-Z
isalnum字母或者数字,a-z,A-Z,0-9
ispunct标点符号,任何不属于数字或者字母的图形字符(可打印)
isgraph任何图形字符
isprint任何可打印字符,包括图形字符和空白字符

字符转换函数

tolower 函数和 toupper 函数

int tolower (int c);
int toupper (int c);

使用举例

#include <stdio.h>
int main()
{
	char arr[20] = { 0 };
	scanf("%s", arr);
	int i = 0;
	while (arr[i] != '\0')
	{
		if (isupper(arr[i]))
			arr[i] = tolower(arr[i]);
		printf("%c ", arr[i]);
		i++;
	}
	return 0;
}

内存操作函数

memcpy函数

memcpy库函数的参数及返回类型

void memcpy(void* destination, const void* source, size_t num);

memcpy函数的使用

memcpy - 内存拷贝

拷贝不重叠的内存

#include <stdio.h>
#include <string.h>
int main()
{
	int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//01 00 00 00 02 00 00 00...
	int arr2[20] = { 0 };
	memcpy(arr2, arr1, 20);//4个字节*5
	for (int i = 0; i < 5; i++)
	{
		printf("%d\n", arr2[i]);
	}
	return 0;
}

模拟实现memcpy函数

#include <stdio.h>
#include <assert.h>
void my_memcpy(void* dest, const void* src, size_t num)
{
	assert(src && dest);
	void* ret = dest;
	while (num--)
	{
		*(char*)dest = *(char*)src;
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}
	return ret;
}
int main()
{
	int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//01 00 00 00 02 00 00 00...
	int arr2[20] = { 0 };
	my_memcpy(arr2, arr1, 20);//4个字节*5
	for (int i = 0; i < 5; i++)
	{
		printf("%d\n", arr2[i]);
	}
	return 0;
}

memmove函数

memmove函数可以处理内存重叠的情况

memmove库函数的参数及返回类型

void memmove(void* destination, const void* source, size_t num);

memmove函数的使用

#include <stdio.h>
#include <string.h>
int main()
{
	int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//把12345移动到34567
	//               1 2 1 2 3 4 5 8 9 10
	memmove(arr1+2, arr1, 20);//4个字节*5
	return 0;
}

模拟实现memmove函数

#include <stdio.h>
#include <assert.h>
void my_memmove(void* dest, const void* source, size_t num)
{
	//可以把源数据从前向后拷贝 也可以从后向前拷贝
	void* ret = dest;
	assert(dest && source);
	if (dest < source)
	{
		while (num--)
		{
			*(char*)dest = *(char*)source;
			dest = (char*)dest + 1;
			source = (char*)source + 1;
		}
	}
	else
	{
		while (num--)
		{
			*((char*)dest + num) = *((char*)source + num);
		}
	}
	return ret;
}
int main()
{
	int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//把12345移动到34567
	//               1 2 1 2 3 4 5 8 9 10
	my_memmove(arr1 + 2, arr1, 20);//4个字节*5
	return 0;
}

memcmp函数

memcmp - 内存比较

memcmp库函数的参数及返回类型

int memcmp(const void* ptr1, const void* ptr2, size_t num);

memcmp函数的使用

#include <stdio.h>
#include <string.h>
int main()
{
	float arr1[] = { 1.0,2.0,3.0,4.0 };
	float arr2[] = { 1.0,3.0 };
	int ret = memcmp(arr1, arr2, 8);
	printf("%d\n", ret);
	return 0;
}
  • memcmp 和 strcmp 返回值的思路一样
  • 相等 - 返回0
  • 小于 - 返回小于0的数字
  • 大于 - 返回大于0的数字

memset函数

memset - 内存设置

memset库函数的参数及返回类型

void* memset( void* dest, int c, size_t count );

memset函数的使用

int main()
{
	int arr[10] = { 0 };
	//前20个字节全部设置成 1
	memset(arr, 1, 20);
	return 0;
}
  • 函数以字节为单位设置内存

自定义类型

结构体

结构体的声明

结构的基础知识

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

结构是一些值的集合,这些值称为成员变量。
结构的每个成员可以是不同类型的变量。

结构的声明

struct tag 
//struct是关键字 tag是结构体的标签名
//struct tag是结构体类型
{
	member-list;//成员列表
}variable-list;//变量列表

创建结构体变量

struct Book
{
	char name[20];
	int price;
	char id[12];
}b4,b5,b6;//b4,b5,b6 是全局的
int main()
{
	//b1,b2,b3 是局部的
	struct Book b1;
	struct Book b2;
	struct Book b3;
	return 0;
}

匿名结构体类型

struct
{
	char c;
	int i;
	char ch;
} s;
struct
{
	char c;
	int i;
	char ch;
}a[20],* ps;
int main()
{
	ps = &s;//err
	return 0;
}

警告:编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。

结构的自引用

正确的自引用方式

struct Node
{
	int data;
	struct Node* next;
};
typedef struct Node
{
	int data;
	struct Node* next;
}Node;

结构体变量的定义和初始化

struct Point
{
	int x;
	int y;
}p1;//声明类型的同时定义变量p1
struct Point p2;//定义结构体变量p2

//初始化:定义变量的同时赋初值。
struct Point p3 = { 1,2 };

struct Stu //类型声明
{
	char name[15];//名字
	int age;//年龄
};
struct Stu s = { "zhangsan",20 };//初始化

struct Node
{
	int data;
	struct Point p;
	struct Node* next;
}n1 = { 10,{4,5},NULL };//结构体嵌套初始化

struct Node n2 = { 20,{5,6},NULL };//结构体嵌套初始化

结构体内存对齐

掌握了结构体的基本使用,我们来深入探讨一个问题:计算结构体的大小。
这也是一个特别热门的考点:结构体内存对齐。

结构体的对齐规则

  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐处)的整数倍的地址处。

对齐数 = 编译器默认的一个对齐数与该成员大小的较小值

  • VS中默认的值为8
  • Linux没有默认对齐数的概念
  1. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  2. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
#include <stdio.h>
struct S
{
	char c1;//1
	int i;//4
	char c2;//1
};
struct S2
{
	char c1;//1
	int i;//4
	double d;//8
};
struct S3
{
	double d;//8
	char c;//1
	int i;//4
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{
	struct S s = { 0 };
	printf("%d\n", sizeof(s));//12
	struct S2 s2 = { 0 };
	printf("%d\n", sizeof(s2));//16
	struct S3 s3 = { 0 };
	printf("%d\n", sizeof(s3));//16
	struct S4 s4 = { 0 };
	printf("%d\n", sizeof(s4));//32
	return 0;
}

为什么存在内存对齐?

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

小结

结构体的内存对齐是拿空间来换取时间的做法

设计结构体的时候,我们既要满足对齐,又要节省空间,怎么做到呢?

让占用空间小的成员尽量集中在一起。

struct S1
{
	char c1;
	int i;
	char c2;
};//12
struct S2
{
	char c1;
	char c2;
	int i;
};//8

S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有区别。

修改默认对齐数

之前我们见过 #pragma 这个预处理指令,这里我们再次使用可以改变我们的默认对齐数。

#pragma pack(2)//设置默认对齐数为2
struct S1
{
	char c1;
	int i;
	char c2;
};
#pragma (pack)//取消设置的默认对齐数,还原为默认
int main()
{
	struct S1 s1 = { 0 };
	printf("%d\n", sizeof(s1));//8
	return 0;
}

结论

结构在对齐方式不合适的时候,我们可以自己更改默认对齐数

百度面试题

写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明。
考察:offsetof 宏的实现

#include <stddef.h>// offsetof
#include <stdio.h>
struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", offsetof(struct S1, c1));//0
	printf("%d\n", offsetof(struct S1, i));//4
	printf("%d\n", offsetof(struct S1, c2));//8
	return 0;
}

注:可以学习完宏之后再实现

结构体传参

结构体传参

struct S
{
	int data[1000];
	int num;
};
struct S s = { {1,2,3,4},1000 };
//结构体传参
void print1(struct S s)
{
	printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps )
{
	printf("%d\n",ps->num);
}
int main()
{
	print1(s);//传结构体
	print2(&s);//传地址
	return 0;
}

直接传结构体需要传递4004个字节,浪费时间和空间。
而传递地址只需要4或8个字节,更加节约,而且能通过地址回找结构体。
所以 print2 比 print1 更合适。

结论

结构体传参,要传结构体地址!

位段

学习完结构体,我们再来学习结构体实现位段的能力。

什么是位段?

位段的声明和结构体是类似的,有两个不同:

  1. 位段的成员必须是 int、unsigned int、signed int、char。
  2. 位段的成员名后面有一个冒号和一个数字。

例如:

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 20;
};

A就是一个位段类型。

位段的内存分配

  1. 位段的成员可以是int、unsigned int、signed int、char(整型家族)类型。
  2. 位段上的空间是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

还是上个例子:

struct A
{
	//开辟 4个字节
	int _a : 2;//_a成员占2个bit位
	int _b : 5;//_b成员占5个bit位
	int _c : 10;//_c成员占10个bit位
	//剩下 15个 bit位
	//开辟 4个字节
	int _d : 30;//_d成员占30个bit位
};
int main()
{
	printf("%d\n", sizeof(struct A));//8
	return 0;
}

位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27在16位机器会出问题)
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

总结

跟结构相比,位段可以达到同样的效果,可以很好的节省空间,但是有跨平台问题的存在。

枚举

枚举顾名思义就是一一列举
比如:
一周从星期一到星期日是7天,可以一一列举
性别有男、女、保密,也可以一一列举
月份有12个月,也可以一一列举

枚举类型的定义

enum Day //星期
{
	Mon,
	Tues,
	Wed,
	Thu,
	Fri,
	Sat,
	Sun
};
enum Sex //性别
{
	Male,
	Female,
	Secret
};
enum Color //颜色
{
	Red,
	Green,
	Blue
};

以上定义的enum Day,enum Sex,enum Color都是枚举类型。{}中的内容是枚举类型的可能取值,也叫枚举常量

这些可能取值都是有值的,默认从0开始,一次递增1,当然也可以在定义的时候赋初值。例如:

enum Sex
{
	Male,//0
	Female,//1
	Secret//2
};
enum Color
{
	Red = 5,//5
	Green = 8,//8
	Blue //9
};

枚举的优点

为什么使用枚举

我们可以用#define定义常量,为什么非要使用枚举?枚举的优点:

  1. 增加代码的可读性和可维护性
  2. 与#define定义的标识符比较枚举有类型检查,更加严谨
  3. 防止了命名污染(封装)
  4. 便于调试
  5. 使用方便,一次可以定义多个常量

枚举的使用

enum Color //颜色
{
	Red,
	Green,
	Blue
};
enum Color c = Blue;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异
int main()
{
	printf("%d\n", c);//2
	return 0;
}

联合(共用体)

联合类型的定义

联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。

//联合类型的声明
union Un
{
	char c;
	int i;
};
int main()
{
	//联合变量的定义
	union Un un;
	//计算联合变量的大小
	printf("%d\n", sizeof(un));//4
	return 0;
}

联合的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

union Un
{
	char c;
	int i;
};
int main()
{
	union Un un;
	printf("%p\n", &(un.c));
	printf("%p\n", &(un.i));
	//地址相同
	un.i = 0x11223344;
	un.c = 0x55;
	printf("%x\n", un.i);//11223355
	//同一时间只能使用它的一个成员
	//改c的时候也把i改变了
	return 0;
}

判断当前计算机的大小端存储

int check_sys()
{
	union U
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
	//返回1就是小端
	//返回0就是大端
}
int main()
{
	int ret = check_sys();
	if (ret == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

联合大小的计算

  • 联合的大小至少是最大成员的大小。
  • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un1
{
	char c[5];//最大对齐数是1 占5个字节
	int i;//最大对齐数是4 占4个字节
};
union Un2
{
	short c[7];//最大对齐数是2 占14个字节
	int i;//最大对齐数是4 占4个字节
};
int main()
{
	printf("%d\n", sizeof(union Un1));//8
	printf("%d\n", sizeof(union Un2));//16
	return 0;
}

动态内存管理

为什么存在动态内存分配

我们已经掌握的内存开辟方式有:

int val = 20;//在栈空间上开辟四个字节
char arr[10] = { 0 };//在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个特点:

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

但是对于空间的需求,不仅仅是上述的情况。
有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。这时候就只能试试动态存开辟了。

内存分为栈区、堆区、静态区(数据段)三个区域:

栈区堆区静态区(数据段)
局部变量、函数形参动态内存开辟(malloc、free、realloc、calloc)全局变量、静态变量

动态内存函数

malloc函数和free函数

malloc函数

C语言提供了一个动态内存开辟的函数:

void* malloc (size_t size);

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

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数 size 为 0,malloc的行为是标准是未定义的,取决于编译器。

free函数

C语言提供了另外一个函数free,专门用来做动态内存的释放和回收的,函数原型如下:

void free (void* ptr);

free函数用来释放动态开辟的内存。

  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptr 是NULL指针,则函数什么事都不做。

使用举例

malloc和free都声明在 stdlib.h 头文件中。

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int* ptr = NULL;
    ptr = (int*)malloc(10 * sizeof(int));
    if (NULL != ptr)//判断ptr指针是否为空
    {
        int i = 0;
        for (i = 0; i < 10; i++)
        {
            *(ptr + i) = i;
        }
        for (i = 0; i < 10; i++)
        {
            printf("%d ", ptr[i]);
            //ptr[i] == *(p+i)
        }
    }
    free(ptr);//释放ptr所指向的动态内存
    ptr = NULL;//自己动手把ptr置空指针
    return 0;
}

calloc函数

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:

void* calloc (size_t num, size_t size);
  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

使用举例

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (NULL != p)
	{
		//使用空间
		for (int i = 0; i < 10; i++)
		{
			printf("%d\n", p[i]);//全为 0
		}
	}
	free(p);
	p = NULL;
	return 0;
}

realloc函数

realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。函数原型如下:

void* realloc (void* ptr, size_t size);
  • ptr 是要调整的内存地址。
  • size 是调整之后新大小。
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
  • realloc在调整内存空间的是存在两种情况:
    • 情况1:原有空间之后有足够大的空间
      要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
    • 情况2:原有空间之后没有足够大的空间
      扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。

使用举例

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("main");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = 5;
	}
	//这里需要p指向的空间更大,需要20个int的空间
	//realloc调整空间
	int*ptr = (int*)realloc(p, 20*sizeof(int));
	if (ptr != NULL)
	{
		p = ptr;
	}
	free(p);
	p = NULL;
	return 0;
}

realloc单独使用举例

这里功能类似于malloc,就是直接在堆区开辟40个字节

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)realloc(NULL, 40);
	return 0;
}

常见的动态内存错误

对NULL指针的解引用操作

void test()
{
	int *p = (int *)malloc(INT_MAX/4);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
}

对动态开辟空间的越界访问

void test()
{
	int i = 0;
	int *p = (int *)malloc(10*sizeof(int));
	if(NULL == p)
	{
		exit(EXIT_FAILURE);
	}
	for(i=0; i<=10; i++)
	{
		*(p+i) = i;//当i是10的时候越界访问
	}
	free(p);
}

对非动态开辟内存使用free释放

void test()
{
	int a = 10;
	int *p = &a;
	free(p);
}

使用free释放一块动态开辟内存的一部分

void test()
{
	int *p = (int *)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
}

对同一块动态内存多次释放

void test()
{
	int *p = (int *)malloc(100);
	free(p);
	free(p);//重复释放
}

动态开辟内存忘记释放(内存泄漏 - 比较严重的问题)

void test()
{
	int *p = (int *)malloc(100);
	if(NULL != p)
	{
		*p = 20;
	}
}
int main()
{
	test();
	while(1);
}

经典笔试题

题目1

void GetMemory(char *p)
{
	p = (char *)malloc(100);
}

void Test(void)
{
	char *str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

问:运行Test函数会有什么样的结果?
解答:
str传递给GetMemory函数的时候是值传递,所以GetMemory函数的形参p是str的一份临时拷贝。在GetMemory函数内部动态申请空间的地址,存放在p中,不会影响外面str,所以当GetMemory函数返回之后,str依然是NULL,所以strcpy会失败。
当GetMemory函数返回之后,形参p销毁,使得动态开辟的100个字节存在内存泄漏,无法释放。

题目2

char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

问:运行Test函数会有什么样的结果?
解答:
GetMemory函数内部创建的数组实在栈区上创建的。出了函数,p数组的空间就还给了操作系统。返回的地址是没有实际的意义,如果通过返回的地址,去访问内存就是非法访问内存的。

题目3

void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}

问:运行Test函数会有什么样的结果?
解答:
没有free(),且没有将str置空指针,会造成内存泄漏。

题目4

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

问:运行Test函数会有什么样的结果?
解答:
str已经还给操作系统了,后面属于非法访问:即使str未置成空指针,能找到之前的地址,但是已经不能访问了。

C/C++程序的内存开辟

C/C++程序内存开辟
C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。

  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。

  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

有了这幅图,我们可以更好的理解static关键字修饰局部变量的例子。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁所以生命周期变长。

柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。
比如:

typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员

}type_a;
typedef struct st_type

{
	int i;
	int a[];//柔性数组成员

}type_a;

柔性数组的特点

  • 结构中的柔性数组成员前面必须至少一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

例如:

typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;
int main()
{
	printf("%d\n", sizeof(type_a));//输出的是4
	return 0;
}

柔性数组的使用

typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;
int main()
{
	int i = 0;
	type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
	//这样柔性数组成员a,相当于获得了100个整型元素的连续空间。
	p->i = 100;
	for(i=0; i<100; i++)
	{
		p->a[i] = i;
	}
	type_a * ptr = (type_a*)realloc(p,sizeof(type_a)+100*sizeof(int));
	if(ptr != NULL)
	{
		p = ptr;
	}
	//使用
	
	//释放
	free(p);
	return 0;
}

柔性数组的优势

//代码 1
typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;
int main()
{
	int i = 0;
	type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
	//业务处理
	p->i = 100;
	for (i = 0; i < 100; i++)
	{
		p->a[i] = i;
	}
	free(p);
	return 0;
}

上述的 type_a 结构也可以设计为:

//代码 2
typedef struct st_type
{
	int i;
	int* p_a;
}type_a;
int main()
{
	type_a* p = (type_a*)malloc(sizeof(type_a));
	p->i = 100;
	p->p_a = (int*)malloc(p->i * sizeof(int));
	//业务处理
	for (int i = 0; i < 100; i++)
	{
		p->p_a[i] = i;
	}
	//释放空间
	free(p->p_a);
	p->p_a = NULL;
	free(p);
	p = NULL;
	return 0;
}

上述 代码1 和 代码2 可以完成同样的功能,但是 代码1 的实现有两个好处:

第一个好处是:方便内存释放

如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。

第二个好处是:这样有利于访问速度.

连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实也没多高了,反正跑不了要用做偏移量的加法来寻址)

文件操作

为什么使用文件

我们前面学习结构体时,写了通讯录的程序,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受。
我们在想既然是通讯录就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。
使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。

什么是文件

磁盘上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。

程序文件

包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。

数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

本章节讨论的是数据文件。

在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。

文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀。
例如: c:\code\test.txt

为了方便起见,文件标识常被称为文件名。

文件的打开和关闭

文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE。

例如,VS2019编译环境提供的 stdio.h 头文件中有以下的文件类型申明:

struct _iobuf {
        char *_ptr;
        int   _cnt;
        char *_base;
        int   _flag;
        int   _file;
        int   _charbuf;
        int   _bufsiz;
        char *_tmpfname;
       };
typedef struct _iobuf FILE;

不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。

下面我们可以创建一个FILE*的指针变量:

FILE* pf;//文件指针变量

定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。

pf指向文件信息区

文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。

ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。

//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );

打开方式如下:

文件使用方式含义如果指定文件不存在
“r” (只读)为了输入数据,打开一个已经存在的文本文件出错
“w” (只写)为了输出数据,打开一个文本文件建立一个新的文件
“a” (追加向文本文件尾添加数据建立一个新的文件
“rb”(只读)为了输入数据,打开一个二进制文件出错
“wb”(只写)为了输出数据,打开一个二进制文件建立一个新的文件
“ab”(追加)向一个二进制文件尾添加数据出错
“r+”(读写)为了读和写,打开一个文本文件出错
“w+”(读写)为了读和写,建议一个新的文件建立一个新的文件
“a+”(读写)打开一个文件,在文件尾进行读写建立一个新的文件
“rb+”(读写)为了读和写打开一个二进制文件出错
“wb+”(读写)为了读和写,新建一个新的二进制文件建立一个新的文件
“ab+”(读写)打开一个二进制文件,在文件尾进行读和写建立一个新的文件

实例代码:

#include <stdio.h>
int main ()
{
	FILE * pFile;
	//打开文件
	pFile = fopen ("myfile.txt","w");
	//文件操作
	if (pFile!=NULL)
	{
		fputs ("fopen example",pFile);
		//关闭文件
		fclose (pFile);
	}
	return 0;
}

文件的顺序读写

功能函数名适用于
字符输入函数fgetc所有输入流
字符输出函数fputc所有输出流
文本行输入函数fgets所有输入流
文本行输出函数fputs所有输出流
格式化输入函数fscanf所有输入流
格式化输出函数fprintf所有输出流
二进制输入fread文件
二进制输出fwrite文件

文件的随机读写

fseek函数

根据文件指针的位置和偏移量来定位文件指针。

int fseek ( FILE * stream, long int offset, int origin );

比如:

#include <stdio.h>
int main ()
{
	FILE * pFile;
	pFile = fopen ( "example.txt" , "wb" );
	fputs ( "This is an apple." , pFile );
	fseek ( pFile , 9 , SEEK_SET );
	fputs ( " sam" , pFile );
	fclose ( pFile );
	pFile = NULL;
	return 0;
}

ftell函数

返回文件指针相对于起始位置的偏移量。

long int ftell ( FILE * stream );

比如:

#include <stdio.h>
int main()
{
	FILE* pFile;
	long size;
	pFile = fopen("myfile.txt", "rb");
	if (pFile == NULL)
		perror("Error opening file");
	else
	{
		fseek(pFile, 0, SEEK_END); //non-portable
		size = ftell(pFile);
		fclose(pFile);
		printf("Size of myfile.txt: %ld bytes.\n", size);
	}
	return 0;
}

rewind函数

让文件指针的位置回到文件的起始位置。

void rewind ( FILE * stream );

比如:

#include <stdio.h>
int main()
{
	int n;
	FILE* pFile;
	char buffer[27];
	pFile = fopen("myfile.txt", "w+");
	for (n = 'A'; n <= 'Z'; n++)
		fputc(n, pFile);
	rewind(pFile);
	fread(buffer, 1, 26, pFile);
	fclose(pFile);
	buffer[26] = '\0';
	puts(buffer);
	return 0;
}

文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件或者二进制文件
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件

如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件

一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2019测试)。

10000的存储
测试代码:

#include <stdio.h>
int main()
{
	int a = 10000;
	FILE* pf = fopen("test.txt", "wb");
	fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
	fclose(pf);
	pf = NULL;
	return 0;
}

文件读取结束的判定

被错误使用的feof函数

在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。

  1. 文本文件读取是否结束,判断返回值是否为 EOF(fgetc),或者 NULL(fgets)
    例如:

    • fgetc 判断是否为 EOF .
    • fgets 判断返回值是否为 NULL .
  2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
    例如:

    • fread判断返回值是否小于实际要读的个数。

正确的使用

文本文件的例子:

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
	int c; // 注意:int,非char,要求处理EOF
	FILE* fp = fopen("test.txt", "r");
	if (!fp)
	{
		perror("File opening failed");
		return EXIT_FAILURE;
	}
	//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
	while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
	{
		putchar(c);
	}
	//判断是什么原因结束的
	if (ferror(fp))
		puts("I/O error when reading");
	else if (feof(fp))
		puts("End of file reached successfully");
	fclose(fp);
}

二进制文件的例子:

enum { SIZE = 5 };
int main(void)
{
	double a[SIZE] = { 1.,2.,3.,4.,5. };
	FILE* fp = fopen("test.bin", "wb"); // 必须用二进制模式
	fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组
	fclose(fp);
	double b[SIZE];
	fp = fopen("test.bin", "rb");
	size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 读 double 的数组
	if (ret_code == SIZE) 
	{
		puts("Array read successfully, contents: ");
		for (int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
		putchar('\n');
	}
	else
	 { // error handling
		if (feof(fp))
			printf("Error reading test.bin: unexpected end of file\n");
		else if (ferror(fp)) 
		{
			perror("Error reading test.bin");
		}
	}
	fclose(fp);
}

文件缓冲区

ANSI C 标准采用 “ 缓冲文件系统” 处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
文件缓冲区

#include <stdio.h>
#include <windows.h>
//VS2019 WIN10环境测试
int main()
{
	FILE*pf = fopen("test.txt", "w");
	fputs("abcdef", pf);//先将代码放在输出缓冲区
	printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
	Sleep(10000);
	printf("刷新缓冲区\n");
	fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
	//注:fflush 在高版本的VS上不能使用了
	printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
	Sleep(10000);
	fclose(pf);
	//注:fclose在关闭文件的时候,也会刷新缓冲区
	pf = NULL;
	return 0;
}

这里可以得出一个结论
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。

程序环境与预处理

程序的翻译环境和执行环境

在ANSIC的任何一种实现中,存在两个不同的环境。

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。

第2种是执行环境,它用于实际执行代码。

详解编译+链接

翻译环境

翻译环境

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

编译本身也分为几个阶段:

编译

如何查看编译期间的每一步发生了什么呢?

  1. 预处理 选项 gcc -E test.c -o test.i
    预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。

  2. 编译 选项 gcc -S test.c
    编译完成之后就停下来,结果保存在test.s中。

  3. 汇编 gcc -c test.c
    汇编完成之后就停下来,结果保存在test.o中。

运行环境

程序执行的过程:

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序
    的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

  2. 程序的执行便开始。接着便调用main函数。

  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(栈)(stack),存储函数的局部变量和返回
    地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程
    一直保留他们的值。

  4. 终止程序。正常终止main函数;也有可能是意外终止。

预处理详解

预定义符号

__FILE__      //进行编译的源文件
__LINE__     //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义

这些预定义符号都是语言内置的。
举个栗子:

printf("file:%s line:%d\n", __FILE__, __LINE__);

#define

#define定义标识符

语法:

 #define name stuff

举个栗子:

#define MAX 1000
#define reg register          //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)     //用更形象的符号来替换一种实现
#define CASE break;case        //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:% s\ttime : % s\n" ,\
__FILE__, __LINE__, \
__DATE__, __TIME__)

在define定义标识符的时候,要不要在最后加上 ; ?
比如:

#define MAX 1000;
#define MAX 1000

建议不要加上 ; ,这样容易导致问题。
比如下面的场景:

if(condition)
	max = MAX;
else
	max = 0;

这里会出现语法错误。

#define定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义
宏(define macro)。
下面是宏的申明方式:

#define name( parament-list ) stuff

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

//#define定义宏,括号很重要
#define SQUARE(X) (X)*(X)
#define DOUBLE(X) ((X)+(X))
int main()
{
	printf("%d\n", SQUARE(3+1));
	//printf("%d\n", 3 + 1 * 3 + 1);
	printf("%d\n", 10 * DOUBLE(4));
	//printf("%d\n", 10 * ((4) + (4)));
	return 0;
}
#define 替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
    被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
    述处理过程。

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#和##

#的作用:

如何把参数插入到字符串中?
首先我们看看这样的代码:

char* p = "hello ""Luckyhorse5\n";
printf("hello"," Luckyhorse5\n");
printf("%s", p);

输出的是hello Luckyhorse5
我们发现字符串是有自动连接的特点的。

  • 那我们是不是可以写这样的代码:
#define PRINT(FORMAT, VALUE)\
	printf("the value is "FORMAT"\n", VALUE);
...
PRINT("%d", 10);

这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。

  • 另外一个技巧是:
    使用 # ,把一个宏参数变成对应的字符串。
    比如:
int i = 10;
#define PRINT(FORMAT, VALUE)\
	printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
...
PRINT("%d", i+3);//产生了什么效果?

代码中的 #VALUE 会预处理器处理为:
“VALUE” .
最终的输出的结果应该是:

the value of i+3 is 13

##的作用:
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。

#define ADD_TO_SUM(num, value) \
	sum##num += value;
...
ADD_TO_SUM(5, 10);//作用是:给sum5增加10.

注:
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能
出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:

x+1;//不带副作用
x++;//带有副作用

MAX宏可以证明具有副作用的参数所引起的问题。

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?

这里我们得知道预处理器处理之后的结果是什么:

z = ( (x++) > (y++) ? (x++) : (y++));

所以输出的结果是:

x=6 y=10 z=9
宏和函数对比

宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。

#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不用函数来完成这个任务?
原因有二:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比
    函数在程序的规模和速度方面更胜一筹。

  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之
    这个宏则可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的

当然和宏相比函数也有劣势的地方:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序
    的长度。

  2. 宏是没法调试的。

  3. 宏由于类型无关,也就不够严谨。

  4. 宏可能会带来运算符优先级的问题,导致程序容易出现错误。

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

#define MALLOC(num, type)\
	(type *)malloc(num * sizeof(type))
...
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 * sizeof(int));
属性#define定义宏函数
代码长度每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度更快存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。
带有副作用的参数参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一次,结果更容易控制。
参数类型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。
调试宏是不方便调试的函数是可以逐语句调试的
递归宏是不能递归的函数是可以递归的

命名约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:

  • 把宏名全部大写
  • 函数名不要全部大写

#undef

这条指令用于移除一个宏定义。

#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

命令行定义

许多C的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假
定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一
个机器内存大写,我们需要一个数组能够大写。)

#include <stdio.h>
int main()
{
	int array[ARRAY_SIZE];
	int i = 0;
	for (i = 0; i < ARRAY_SIZE; i++)
	{
		array[i] = i;
	}
	for (i = 0; i < ARRAY_SIZE; i++)
	{
		printf("%d ", array[i]);
	}
	printf("\n");
	return 0;
}

编译指令:

gcc -D ARRAY_SIZE=10 programe.c

条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件
编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

#include <stdio.h>
#define __DEBUG__
int main()
{
	int i = 0;
	int arr[10] = {0};
	for(i=0; i<10; i++)
	{
		arr[i] = i;
		#ifdef __DEBUG__
		printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 
		#endif //__DEBUG__
	}
	return 0;
}

常见的条件编译指令:

//1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif

//2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

//3.判断是否被定义
#if defined(symbol)
#ifdef symbol

#if !defined(symbol)
#ifndef symbol

//4.嵌套指令
#if defined(OS_UNIX)
	#ifdef OPTION1
		unix_version_option1();
	#endif
	#ifdef OPTION2
		unix_version_option2();
	#endif
#elif defined(OS_MSDOS)
	#ifdef OPTION2
		msdos_version_option2();
	#endif
#endif

文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方
一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。

头文件被包含的方式:

  • 本地文件包含
#include "filename"

查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标
准位置查找头文件。
如果找不到就提示编译错误。

Linux环境的标准头文件的路径:

/usr/include

VS环境的标准头文件的路径:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

注意按照自己的安装路径去找。

  • 库文件包含
#include <filename.h>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?

可以,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

嵌套文件包含

嵌套文件包含示意图
comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。

如何解决这个问题?

答案:条件编译。
每个头文件的开头写:

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif   //__TEST_H__

或者

#pragma once

就可以避免头文件的重复引入。

其他预处理指令

#error//编译程序时只要遇到#error就会生成一个编译错误提示消息,并停止编译。

#pragma//为实现时定义的命令,它允许向编译程序传送各种指令。
//例如:编译程序可能有一种选择,它支持对程序执行的跟踪。可用#pragma语句指定一个跟踪选择。

#line //改变当前行数和文件名称,它们是在编译程序中预先定义的标识符。
//命令的基本形式: #line number["filename"]
...
//还有很多预处理指令可以参考《C语言深度解剖》继续学习

结语 - 只是刚刚开始

当我坐在电脑屏幕前,缓缓打出这行字的时候,距离我自学C语言已经过去了4个月。
万事开头难
小破站上有许多的C语言课程,选择一个适合自己的课程很重要,我试听了四五个课程,最终选择了比特科技张鹏伟老师的课程。首先是内容很全,讲的也很细致,不管考到考不到的知识点都会提及,中间会穿插很经典的面试题、校招题,毕竟比特科技主打的就是帮助IT学子就业,在这里感谢鹏哥和比特科技,很开心能白嫖到优质的课程。
坚持从来都不是一件容易的事情
我在C语言初阶部分学习了90天左右,在初阶的学习过程中,我并没有写博客来记录我的编程生活,一个原因是懒,另一个原因是我觉得基础的C语言部分,比如函数、分支、循环等每天都会用到,不容易忘记。但是不写博客的缺点也很明显,就是缺少了“仪式感”。记得刚开始的时候我给自己计划每天学2个小时,到后面的1个小时,再到后面加上学校社团活动的缘故停滞了一段时间。很明显能感受到自己学下去的动力在慢慢减弱。变数发生在课程中讲三子棋,重新让我有了学下去的动力。当花了一下午时间,敲下来近300行代码的时候,我的成就感爆棚,把全宿舍的人来我电脑面前体验我写的三子棋游戏,这也是我第一次感觉到自己像一个程序员了。后面的扫雷游戏亦是如此。完成C语言初阶的内容后,便开始学习进阶的知识。我明显感觉到进阶的内容更加复杂、更容易忘,因此我尝试着自己去写博客,便有了这一篇博客。每天学完一节之后,我会及时更新博客内容,重温知识。有时我会把博客分享给朋友们,他们对我肯定也是我坚持下去的动力。在此感谢:dhc、zyj、yty。
编程之路才刚刚开始
从一开始写出hello world的喜悦,到现在轻松写出百行代码,再到后面的数据结构……长路漫漫,我希望这篇博客可以在我低谷的时候带来动力,可以在我枯燥无味的生活中增添一份惊喜。

谨以此篇五万余字的博客
献给向编程迈出第一步的我和你

【完】

Luckyhorse5

2023.2.1

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Luckyhorse5

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值