C语言学习笔记,学懂C语言,看这篇就够了!(下)

笔记汇总(将上中下汇总):
链接:https

目录

第15章、数据的存储

15.1 数据类型介绍

  • 内置类型
  • 自定义类型(构造类型)

前面我们已经学习了基本的内置类型:

char        //字符数据类型
short       //短整型
int         //整形
long        //长整型
long long   //更长的整形
float       //单精度浮点数
double      //双精度浮点数
//C语言有没有字符串类型?

以及他们所占存储空间的大小。

类型的意义:

  1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
  2. 如何看待内存空间的视角。
15.1.1 类型的基本归类

整形家族:

char
 unsigned char
 signed char
short
 unsigned short [int]//有无int均可
 signed short [int]
int
 unsigned int
 signed int
long
 unsigned long [int]
 signed long [int]

浮点数家族:

float
double

构造类型:

> 数组类型
    //int arr[10]---数组的类型:将数组名去掉,剩下的就是数组的类型:int[10]
    //int[5]跟int[10]、char[5]是不同的类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union

指针类型

int *pi;
char *pc;
float* pf;
void* pv;//无具体类型的指针,不是空指针

空类型:

void 表示空类型(无类型)

通常应用于函数的返回类型、函数的参数、指针类型。

//函数的参数
#include <stdio.h>
void test()//C语言中,函数括号里卖弄什么都不写的时候,你传参也可以,不传参也可以
    		//你如果明确test函数不需要传参,你在括号里面写一个void
{
    printf("hehe\n");
}
int mian
{
    test(100);
    return 0;
}

15.2 整型在内存中的存储

我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。

那接下来我们谈谈数据在所开辟内存中到底是如何存储的?

比如:

int a = 20;//4个字节-32bit
//00000000 00000000 00000000 00010100 - 原码 - 转变成十六进制 - 0x00 00 00 14
//00000000 00000000 00000000 00010100 - 饭码 - 转变成十六进制 - 0x00 00 00 14
//00000000 00000000 00000000 00010100 - 补码 - 转变成十六进制 - 0x00 00 00 14
int b = -10;
//10000000 00000000 00000000 00001010 - 原码 - 转变成十六进制 - 0x80 00 00 0A
//11111111 11111111 11111111 11110101 - 反码 - 转变成十六进制 - 0xFF FF FF F5
//11111111 11111111 11111111 11110110 - 补码 - 转变成十六进制 - 0xFF FF FF F6

我们知道为 a 分配四个字节的空间。

那如何存储?

下来了解下面的概念:

15.2.1 原码、反码、补码

计算机中的整数有三种表示方法,即原码、反码和补码。

三种表示方法均有符号位数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位负整数的三种表示方法各不相同。

整数

  1. 有符号数;

    1. 正数:原码、反码、补码相同
    2. 负数:原码、反码、补码不同,要进行计算
  2. 无符号数:

    原码、反码、补码相同

原码

直接将二进制按照正负数的形式翻译成二进制就可以。

反码

将原码的符号位不变,其他位依次按位取反就可以得到了。

补码

反码+1就得到补码。

正数的原、反、补码都相同。

对于整型来说:数据存放内存中其实存放的是二进制数的补码。

显示的是二进制的十六进制数的补码

为什么呢?

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;

同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

//1-1在计算机中的运算原理
#include <stdio.h>
int main()
{
	1 - 1;
    //1 + (-1)
    //若使用原码
    //00000000 00000000 00000000 00000001 - 1
    //10000000 00000000 00000000 00000001 - -1
    //10000000 00000000 00000000 00000010
    //结果是-2
    //所以发明了反码和补码:
    
    //1的原码、反码、补码
    //00000000 00000000 00000000 00000001
    
    //-1的原码、反码、补码
    //10000000 00000000 00000000 00000001
    //11111111 11111111 11111111 11111110
    //11111111 11111111 11111111 11111111
    
    //用补码进行计算
    //00000000 00000000 00000000 00000001
    //11111111 11111111 11111111 11111111
    //10000000 00000000 00000000 000000000//多一位,溢出的舍去,只能存32位
	return 0;
}

我们看看在内存中的存储:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们可以看到对于a和b分别存储的是补码,但是我们发现顺序有点不对劲

这又是为什么?

15.2.2 大小端介绍

内存空间都是有编号的,编号有大有小,编号小的叫低地址,编号大的叫高地址

什么是大端小端:

大端(存储)模式,也叫大端字节序,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;

小端(存储)模式,也叫小端字节序,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。

低地址 ---------------> 高地址
低地址  - (高位)0x11 22 33 44(低位)  -  高地址 - 大端(存储)模式
低地址  - (高位)0x44 33 22 11(低位)  -  高地址 - 小端(存储)模式

为什么有大端和小端:

为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。

例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式,

刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以

由硬件来选择是大端模式还是小端模式。

百度2015年系统工程师笔试题:

请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。

//代码1
#include <stdio.h>
int check_sys()
{
 	int i = 1;
 	return (*(char *)&i);
}
//指针类型的意义:
//1. 指针类型决定了解引用操作符能访问几个字节。char*p,*p访问了1个字节,int*p,*p访问4个字节
//2. 指针类型决定了指针+1, -1 ,加的或者减的是几个字节;char*p,p+1跳过一个字符,int*p,p+1跳过一个整型-4个字节
int main()
{
 	int ret = check_sys();
 	if(ret == 1)
 	{
 		printf("小端\n");
 	}
 	else
	{
		 printf("大端\n");
 	}
 	return 0;
}
//代码2
int check_sys()
{
 union
 {
 	int i;
 	char c;
 }un;
 un.i = 1;
 return un.c;
}
15.2.3 整型提升

整型提升是C程序设计语言中的一项规定:在表达式计算时,各种整形首先要提升为int类型。表达式的整型运算要在CPU的相应运算器件内执行,CPU内的整型运算器的操作数的字节长度一般是int 类型的字节长度,即四个字节。

因此即使是两个char( char类型为一个字节)类型的数相加,在CPU相应的运算器件中也很难直接进行相加,这时候就要进行整型提升到四个字节才能进行相加计算。

整型提升的规则:
整型提升分为有符号和无符号两种,有符号的:整型提升时是按照变量的补码被截断时的最高位是什么进行补位的,如果截断后最高位即最左面的一位数为 1 则在最高位前补 1 ,如果最高位是 0 则在前面补 0 ,补够32位即int类型即可无符号的: 直接在被截断的前面补 0 即可。

比如:

#include<stdio.h>
int main()
{
	char a = 3;
	char b = 127;
	char c = a + b;
	printf("%d\n", c);
	return 0;
}

如果按照正常的加减运算,是不是计算的结果为130,但实际上结果是-126.

造成这种差异的原因就是发生了整型提升与截断

上题解析:

先截断后提升
数字3的补码为:00000000000000000000000000000011 , 将数字3交给a时,因为a的类型为char类型即只有一个字节,所以a中只能储存一个字节即8个比特位,所以需要进行截断只保留最后的8个比特位,所以此时a中储存的比特位为:00000011

数字127的补码为:00000000000000000000000001111111 同理也因为为char类型发生截断,截断后b中储存的比特位为: 01111111

在执行 a+b 时先对8比特位的a,b进行整型提升,因为都为char 类型所以为有符号位,提升时,补最高位的数字,a,b的最高位都为 0 ,所以在最高位前面补0即可,补够32位。
提升后两者的补码为:
00000000 00000000 00000000 00000011
00000000 00000000 00000000 01111111
将a和b的补码进行相加后得到的补码为:
00000000 00000000 00000000 10000010

又因为c 也为char类型,所以也只能存放8个比特位,所以也需要截断,截断后c 中储存的比特位为:10000010

在进行打印时是以 %d(打印十进制的有符号数字) / %u(打印十进制的无符号位数字) 的形式即整数 int 的类型,此时需要32位比特位,这时就要对 c 进行整型提升了

注:打印%u的时候,整型提升后的补码与原码、反码相同

因为c 的最高位是 1 所以在最高位前面补 1 即可,补够32位,提升后补码为:11111111 11111111 11111111 10000010
将补码转化为原码的形式打印出来,转化后的原码为 : 10000000 0 0000000 00000000 01111110 原码首位是 0 时为正数,为 1 时为负数,此原码对应的整数就为 -126

又比如:

#include<stdio.h>
int main()
{
	char n = -129;
	printf("%d\n", n);
	return 0;
}

结果为:127

此题也是同样的道理:
整数 -129 的补码为: 11111111 11111111 11111111 01111111
被截断后为 01111111,
打印%d的形式时再进行整型提升,截断后最高位为 0 ,所以在最高位前面补 0 提升后的补码为: 00000000 00000000 00000000 01111111
因为补码的最前面的一位数为0 ,所以是正数,正数的原码 反码 补码相同,即将补码转化的原码为: 00000000 00000000 00000000 01111111 此原码对应的值就是 127

15.2.4 练习
1.
//输出什么?
#include <stdio.h>
int main()
{
    char a = -1;
    //-1在内存存的是补码
    //10000000 00000000 00000000 00000001
    //11111111 11111111 11111111 11111110
    //11111111 11111111 11111111 11111111(-1的补码为全1)
    //截断
    //11111111 
    //整型提升
    //11111111 11111111 11111111 11111111(补码)
    //10000000 00000000 00000000 00000001(原码) - -1
    signed char b = -1;
    //与上面相同
    //11111111(截断)
    //-1
    unsigned char c = -1;
    //无符号位-整型提升时补0
    //11111111(截断)
    //00000000 00000000 00000000 11111111(正数)
    //补码即原码,二进制转换成十进制:255
    printf("a=%d,b=%d,c=%d",a,b,c);//-1 -1 255
    //%d - 打印十进制的有符号数字 - 打印补码的原码
    return 0;
}

下面程序输出什么?

2.
#include <stdio.h>
int main()
{
    char a = -128;
    //10000000 00000000 00000000 10000000
    //11111111 11111111 11111111 01111111
    //11111111 11111111 11111111 10000000 - 补码
    //截断
    //10000000
    //整型提升(因为打印的是整数)
    //11111111 11111111 11111111 10000000 - 补码
    printf("%u\n",a);//4294967168
    //%u - 打印十进制的无符号数字
    //%u认为为内存放的是无符号数,没有符号位了,当成是补码数字位的一部分
    //因此,原码,反码,补码是相同的
    return 0;
}

总结:

有符号的char的范围是:-128~~127 --------相当于一个圆,0到127,然后-128到-1

无符号的char的范围是:0~~255

3.
#include <stdio.h>
int main()
{
    char a = 128;//char类型放不下
    //127 + 1 
    //相当于在一个圆圈,127,下一个就是-128,
    printf("%u\n",a);//与上一题结果一样
    return 0;
}
4.
int i = -20;
unsigned  int  j = 10;
//%d,最终的运算结果要编程原码打印
printf("%d\n", i+j);//-10 
//按照补码的形式进行运算,最后格式化成为有符号整数
5.
unsigned int i;//i永远大于0
for(i = 9; i >= 0; i--)//当 i = -1时,因为是无符号的数字,所以-1补码当原码,-1的补码全为1,是一个超大的数字 
{
    printf("%u\n",i);//死循环
}
6.
int main()
{
    char a[1000];//char:-128~~127,超过这个范围,会在这个圆圈继续绕
    int i;
    for(i = 0; i < 1000; i++)//-1~~-128,然后到127~~3,2,1,0
   {
        a[i] = -1 - i;
   }
    printf("%d", strlen(a));//strlen():计算\0之前的长度,\0的值是0
    //结果是255
    return 0;
}
7.
#include <stdio.h>
unsigned char i = 0;//0~255
int main()
{
    for (i = 0; i <= 255; i++)//条件恒成立, 255+1 = 0
   {
        printf("hello world\n");//死循环
   }
    return 0;
}

15.3 浮点型在内存中的存储

常见的浮点数:

3.14159

1E10 // 1.0 X 10^10

浮点数家族包括: float、double、long double 类型。

浮点数表示的范围:float.h中定义

15.3.1 举一个例子

浮点数存储的例子:

int main()
{
     int n = 9;
     float *pFloat = (float *)&n;//&n的类型是int*
    //强制类型转换如果没有产生精度的丢失,是不会产生值的变化
     printf("n的值为:%d\n",n);//9
     printf("*pFloat的值为:%f\n",*pFloat);//0.000000
     *pFloat = 9.0;
     printf("num的值为:%d\n",n);//1091567616
     printf("*pFloat的值为:%f\n",*pFloat);9.0000
     return 0;
}

输出的结果是什么呢?

15.3.2 浮点数存储规则
#incldue <stdio.h>
int main()
{
    float f = 5.5;
    //5.5
    //101.1
    //(-1)^0 * 1.011*2^2
    //S=0,M=1.011,E=2
    //01000000 10110000 00000000 00000000
    //0x40 b0 00 00
    return 0;
}

num 和 *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?

要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。

详细解读:

根据国际标准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)^0 * 1.01*2^2 。

那么,按照上面V的格式,可以得出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位的浮点数,即float类型,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。

对于64位的浮点数,即double类型,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。

IEEE 754对有效数字M和指数E,还有一些特别规定。

前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。

IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的

xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位

浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。

至于指数E,情况就比较复杂。

首先,E为一个无符号整数(unsigned int)

这意味着,如果E为8位,它的取值范围为0255;如果E为11位,它的取值范围为02047。但是,我们

知道,科学计数法中的E是可以出

现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数

是127;对于11位的E,这个中间

数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即

10001001。

然后,指数E从内存中取出还可以再分成三种情况:

E不全为0或不全为1

这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将

有效数字M前加上第一位的1。

比如:

0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为

1.0*2^(-1),其阶码为-1+127=126,表示为

01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,

则其二进制表示形式为:

0 01111110 00000000000000000000000

E全为0

这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,

有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于

0的很小的数字。

E全为1

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

好了,关于浮点数的表示规则,就说到这里。

解释前面的题目:

下面,让我们回到一开始的问题:为什么 0x00000009 还原成浮点数,就成了 0.000000 ?

首先,将 0x00000009 拆分,得到第一位符号位s=0,后面8位的指数 E=00000000 ,最后23位的有效数

字M=000 0000 0000 0000 0000

1001。

9 -> 0000 0000 0000 0000 0000 0000 0000 1001

由于指数E全为0,所以符合上一节的第二种情况。因此,浮点数V就写成:

V=(-1)^0 × 0.00000000000000000001001×2(-126)=1.001×2(-146)

显然,V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000。

再看例题的第二部分。

请问浮点数9.0,如何用二进制表示?还原成十进制又是多少?

首先,浮点数9.0等于二进制的1001.0,即1.001×2^3。

9.0 -> 1001.0 ->(-1)^01.0012^3 -> s=0, M=1.001,E=3+127=130

那么,第一位的符号位s=0,有效数字M等于001后面再加20个0,凑满23位,指数E等于3+127=130,

即10000010。

所以,写成二进制形式,应该是s+E+M,即

0 10000010 001 0000 0000 0000 0000 0000

这个32位的二进制数,还原成十进制,正是 1091567616 。

第16章、指针进阶

指针的主题,我们在初级阶段的《指针》章节已经接触过了,我们知道了指针的概念:

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。

  2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。

  3. 指针是有类型,指针的类型决定了指针的 ± 整数的步长,指针解引用操作的时候的权限。

  4. 指针的运算。

这个章节,我们继续探讨指针的高级主题。

16.1 字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ;

一般使用:

int main()
{
    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';
    return 0;
}

还有一种使用方式如下:

int main()
{
    //这里是把一个字符串放到pstr指针变量里了吗?不是
    char* pstr = "abcdef";//"abcef"是一个常量字符串,里面的内容不能被修改--段错误
    //最正确的写法:
    const char* pstr = "abcdef";
    printf("%c\n", *pstr);//a
    printf("%s\n", pstr);//abcdef
    return 0;
}

代码 const char* pstr = “hello bit.”;

特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr 里了,但是/本质是把字符串 hello bit. 首字符的地址放到了pstr中。

上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。

那就有可这样的面试题:

#include <stdio.h>
int main()
{
    char str1[] = "abcdef";
    char str2[] = "abcdef";
    const char *str3 = "abcdef";//常量字符串
    const char *str4 = "abcdef";
    if(str1 == str2)
 		printf("hehe\n");
    else
 		printf("haha\n");//haha,创建的空间不同
       
    if(str3 ==str4)
		printf("hehe\n");//hehe,str3和str4存的地址相同
    else
 		printf("haha\n");
       
    return 0;
}

这里最终输出的是:

haha
hehe

这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针,指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。

16.2 指针数组

在《指针》章节我们也学了指针数组,指针数组是一个存放指针的数组。

这里我们再复习一下,下面指针数组是什么意思?

int* arr1[10]; //存放整形指针的数组 --- 指针数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组

指针数组的应用:

#include <stdio.h>
int main()
{
    int arr1[] = {1, 2, 3, 4, 5};
    int arr2[] = {2, 3, 4, 5, 6};
    int arr3[] = {3, 4, 5, 6, 7};
    int* parr[] = {arr1, arr2, arr3};
    int i;
    for (i = 0; i < 3; i++)
    {
        int j;
        for (j = 0; j < 5; j++)
        {
            printf("%d ", *(parr[i] + j));
        }
    }
    return 0;
}

16.3 数组指针

16.3.1 数组指针的定义

数组指针是指针?还是数组?

答案是:指针。

我们已经熟悉:

整型指针: int * pint; 能够指向整形数据的指针。

浮点型指针: float * pf; 能够指向浮点型数据的指针。

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

#include <stdio.h>
int main()
{
    int *p = NULL;//p是整型指针,指向整型的指针,可以存放整型的地址
    char *pc = NULL;//pc是字符指针,指向字符的指针,可以存放字符的地址
    //同理,数组指针,指向数组的指针,存放数组的地址
    int arr[10] = {0};
    //arr - 首元素地址
    //&arr[0] - 首元素地址
    //&arr - 数组的地址
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int (*p)[10] = &arr;//数组的地址要存起来
    //上面的p就是数组指针
    return 0;
}

下面代码哪个是数组指针?

int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
//p1是存放指针的数组
//p2是存放数组的指针

解释:

int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指针指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
#include <stdio.h>
int main()
{
    char* arr[5];
    char* (*pa)[5] = &arr;
    //(*pa)中的pa-指针变量的名字,*说明pa是指针
    //[5]-pa指向的数组是5个元素的
    //char*-pa指向的数组的元素类型是char*
    int arr2[10] = {0};
    int (*pa2)[10] = &arr2;//pa2-指针变量的名字-存的是数组的地址
	return 0;
}
16.3.2 &数组名VS数组名

对于下面的数组:

int arr[10];

arr 和 &arr 分别是啥?

我们知道arr是数组名,数组名表示数组首元素的地址。

那&arr数组名到底是啥?

我们看一段代码:

#include <stdio.h>
int main()
{
    int arr[10] = {0};
    printf("%p\n", arr);
    printf("%p\n", &arr);
    return 0;
}

运行结果如下:

arr = 00EFF920
&arr= 00EFF920

可见数组名和&数组名打印的地址是一样的。

难道两个是一样的吗?

我们再看一段代码:

#include <stdio.h>
int main()
{
 int arr[10] = { 0 };
 printf("arr = %p\n", arr);//arr = 0133FBB0 
 printf("&arr= %p\n", &arr);//&arr= 0133FBB0
 printf("arr+1 = %p\n", arr+1);//arr + 1 = 1033FBB4
 printf("&arr+1= %p\n", &arr+1);//&arr + 1= 0133FBD8
 return 0;
}

根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。

实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)

本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型

数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40.

16.3.3 数组指针的使用

那数组指针是怎么使用的呢?

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

看代码:

#include <stdio.h>
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
    //但是我们一般很少这样写代码
    return 0;
}

一个数组指针的使用:

#include <stdio.h>
//参数是数组的形式
void print_arr1(int arr[3][5], int row, int col)
{
    int i = 0;
    for(i = 0; i < row; i++)
   {
        for(j = 0; j < col; j++)
       {
            printf("%d ", arr[i][j]);
       }
         printf("\n");
   }
}
//参数是指针的形式
void print_arr2(int (*p)[5], int row, int col)
{
    int i = 0;
    for(i = 0; i < row; i++)
   {
        for(j = 0; j < col; j++)
       {
            printf("%d ", p[i][j]);
            printf("%d ", *(p[i] + j));
            printf("%d ", *(*(p + i) + j));
            //arr-指向第一行,p+i-跳过i行,*(p + i)-拿到了这一行,现在要拿到这一行的某个元素
            printf("%d ", (*(p + 1))[j]);
       }
        printf("\n");
   }
}
int main()
{
    int arr[3][5] = { {1,2,3,4,5}{2,3,4,5,6}{3,4,5,6,7} };
    print_arr1(arr, 3, 5);
    //数组名arr,表示首元素的地址
    //但是二维数组的首元素是二维数组的第一行
    //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
    //可以数组指针来接收
    print_arr2(arr, 3, 5);
    
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int* p = arr;
    int i;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", *(p + i));
        //等价
        printf("%d ", *(arr + i));
        printf("%d ", arr[i]);
        printf("%d ", p[i]);//arr[i] == *(arr + i) == *(p + i) == p[i]
    }
    return 0;
}

学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:

int arr[5];//arr是一个5个元素整型数组
int *parr1[10];//parr1是一个数组,数组有10个元素,每个元素的类型是int*, parr1是指针数组
int (*parr2)[10];//parr2是一个指针,该指针指向了一个数组,数组有10个元素,每个元素的类型是int-parr2是数组指针
int (*parr3[10])[5];
//parr3是一个数组,该数组有10个元素,每个元素是1个int(*)[5]的数组指针,该数组指针指向的数组的5个元素,每个元素是int
//类型:一般去掉变量名就是这个变量的类型

16.4 数组参数、指针参数

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

16.4.1 一维数组传参
#include <stdio.h>
void test(int arr[])//ok
{}
void test(int arr[10])//ok
{}
void test(int *arr)//ok
{}
void test2(int *arr[20])//ok - 20可以省略
{}
void test2(int **arr)//ok - 一级指针的地址传过来正好放在一个二级指针里面去
{}
int main()
{
     int arr[10] = {0};
     int *arr2[20] = {0};
     test(arr);
     test2(arr2);//首元素地址 - 类型是int* - 一级指针 - 传过去的是一级指针的地址
}
16.4.2 二维数组传参
void test(int arr[3][5])//ok
{}
void test(int arr[][])//不ok
{}
void test(int arr[3][])//不ok
{}
void test(int arr[][5])//ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。即行可以省略,列不可以省略
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//不ok,arr是二维数组,数组名表示首元素(第一行)的地址,int *arr-整型指针是用来存放整型的地址
{}
void test(int* arr[5])//不ok
{}
void test(int (*arr)[5])//ok
{}
void test(int **arr)//不ok,传过来的是第一行的地址,即一级指针的地址,而你接收的却是二级指针的地址
{}
int main()
{
     int arr[3][5] = {0};
     test(arr);
}
16.4.3 一级指针传参
#include <stdio.h>
void print(int *p, int sz)
{
     int i = 0;
     for(i=0; i<sz; i++)
     {
        printf("%d\n", *(p+i));
     }
}
int main()
{
     int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
     int *p = arr;
     int sz = sizeof(arr) / sizeof(arr[0]);
     //一级指针p,传给函数
    //指针传参,指针接收即可
     print(p, sz);
     return 0;
}c

思考:

当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

比如:

#include <stdio.h>
void test1(int *p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数?
int main()
{
    int a = 10;
    int* p = &a;
    test1(&a);//ok
    test1(p1);//ok
    char ch = 'w';
    char* pc = &ch;
    test2(&ch);//ok
    test2(pc);//ok
    return 0;
}
16.4.4 二级指针传参
#include <stdio.h>
void test(int** ptr)//参数直接设置成二级指针
{
 	printf("num = %d\n", **ptr); 
}
int main()
{
     int n = 10;
     int*p = &n;
     int **pp = &p;
     test(pp);
     test(&p);
     return 0;
}

思考:

当函数的参数为二级指针的时候,可以接收什么参数?

void test(char **p)//一级指针的地址恰好可以在二级指针接收
{
 
}
int main()
{
     char c = 'b';
     char*pc = &c;
     char** ppc = &pc;
     char* arr[10];
     test(&pc);
     test(ppc);
     test(arr);//Ok,首元素 char* 的地址 - 也是一级指针的地址
     return 0;
}

16.5 函数指针

函数指针:指向函数的指针 - 存放函数地址的一个指针

首先看一段代码:

#include <stdio.h>
void test()
{
 	printf("hehe\n");
}
int main()
{
                      //输出的结果:
     printf("%p\n", test);//013211DB
     printf("%p\n", &test);//013211DB
     return 0;
}

输出的是两个地址,这两个地址都是 test 函数的地址,这两个是一模一样的。即 &函数名 和 函数名 都是函数的地址

那我们的函数的地址要想保存起来,怎么保存?

下面我们看代码:

void test()
{
 	printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();

首先,能给函数存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?

答案是:

pfun1可以存放。pfun1先和*结合,说明pfun1是指针,() 表示指针指向的是一个函数,指向的函数无参数,返回值类型为void。

举例:

#include <stdio.h>
//例1
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}
int main()
{
    int a = 10;
    int b = 20;
    printf("%d", Add(a, b));//30
    printf("%p", Add);
    printf("%p", &Add);
    
    //存储函数的地址
    int (*pa)(int, int) = Add;
    //(*pa)说明pa是一个指针,后面一个()表示pa指向的是一个函数,指向的函数有两个为int类型的参数,返回值类型是int
    
    //验证- 调用函数
    printf("%d",(*pa)(2, 3));//5
    
    printf("%d", pa(2, 3));//5
    //说明了pa前面的那个 * 号有没有都可以
    //两种都好理解
    //第一种:先对pa进行解引用,找到pa所指向的对象,然后传参调用
   	//第二种:把Add的地址传给了pa,说明Add和pa是一回事
    //即:printf("%d", Add(2, 3));
    //*只是一个摆设,看你怎么理解
    return 0;
}
#include <stdio.h>
//例2
void Print(char* str)
{
    printf("%s", str);
}
int main()
{
    void (*p)(char*) = Print;
    //首先让*和p结合,表示是一个指针,()表示它指向的是一个函数,char* 是函数的参数类型,返回类型是void
    //此时p就是一个函数指针
    (*p)("hello");//调用函数
    //或:不需要解引用:
    p("hello");
    //如何调用
    //p里面存的是地址,*p找到这个函数,这是调用函数就传参
    return 0;
}

阅读两段有趣的代码:

//代码1
(*(void (*)())0)();
//void(*)() - 函数指针类型
//一个括号里面放一个类型 - 说明是强制类型转换 - 放在0前面 - 说明是对 0 进行强制类型转换
//0原来的类型是int,现在把它强制强制转换成函数指针类型 - 意味着把0当成一个函数的地址
//前面加一个*号,解引用这个地址,找到这个函数了后面再给一对括号,即调用这个函数
//总结:这段代码其实是一次函数调用
//CSDN:
第一步:void(*)(),可以明白这是一个函数指针类型。这个函数没有参数,没有返回值。

第二步:(void(*)())0,这是将0强制转换为函数指针类型,0是一个地址,也就是说一个函数存在首地址为0的一块区域内。

第三步:((void()())0),这是取0地址开始的一段内存里面的内容,其内容就是保存在首地址为0的一段区域内的函数。

第四步:((void()())0)(),这是函数调用。

最后加了一个分号,就变成了原来的样子,所以最终的结果是,这是一条函数调用语句。


//代码2
//代码解释:
//signal 是一个函数声明
//signal函数的参数有2个,第一个是int,第二个是函数指针,该函数指针指向的函数的参数是int,返回类型是void
//signal函数的返回类型也是一个哈金属指针,该函数指针指向的函数的参数是int,返回类型是void
void (*signal(int , void(*)(int)))(int);
//void (*      signal(int , void(*)(int))      )(int);
//返回类型:void(*)(int) -- void (*p)(char*) = Print;---把(*p)(char*)去掉,剩下的就是这个函数的返回类型
//函数:signal()
//函数名:signal
//参数:int, void(*)(int)

//另一种写法typedef unsigned int uint;
得:
//简化:
typedef void(*pfun_t)(int);//错误的写法:typedef void (*)(int) pfun_t;
//pfun_t :是一个函数指针类型
pfun_t signal(int, pfun_t);

注 :推荐《C陷阱和缺陷》

这本书中提及这两个代码。

代码2太复杂,如何简化:

typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

16.6 函数指针的数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,

比如:

int *arr[10];
//数组的每个元素是int*

那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[10])();//函数指针的数组 - parr1[10]:数组
int *parr2[10]();//语法错误
int (*)() parr3[10];//int (*)()-函数指针类型 - 语法错误 - 正确写法:int (*parr3)();

答案是:parr1

parr1 先和 [ ] 结合,说明 parr1是数组,数组的内容是什么呢?

是 int (*)() 类型的函数指针。

#include <stdio.h>
int Add(int a, int b)
{
 	return a + b;
}
int Sub(int a, int b)
{
 	return a - b;
}
int Mul(int a, int b)
{
 	return a * b;
}
int Div(int a, int b)
{
 	return a / b;
}
int Main()
{
    //指针数组
    int* arr[5];
    int(*pa)(int, int) = Add;
    //需要一个数组,这个数组可以存放4个函数的地址--函数指针的数组
    int(*parr[4])(int, int) = {Add, Sub, Mul, Div};//函数指针的数组 - parr[4]
    int i;
    for (i = 0; i < 4; i++)
    {
        printf("%d ", parr[i](2, 3));//5 -1 6 0
	}
    return 0;
}

函数指针数组的用途:转移表

例子:(计算器)

#include <stdio.h>
int add(int a, int b)
{
 	return a + b;
}
int sub(int a, int b)
{
 	return a - b;
}
int mul(int a, int b)
{
 	return a * b;
}
int div(int a, int b)
{
 	return a / b;
}
int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
    do
   {
        printf( "*************************\n" );
        printf( " 1:add           2:sub \n" );
        printf( " 3:mul           4:div \n" );
        printf( "*************************\n" );
        printf( "请选择:" );
        scanf( "%d", &input);
        switch (input)
       {
        case 1:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = add(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 2:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = sub(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 3:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = mul(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 4:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = div(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 0:
                printf("退出程序\n");
 breark;
        default:
              printf( "选择错误\n" );
              break;
       }
 } while (input);
    
    return 0;
}

使用函数指针数组的实现:

#include <stdio.h>
int add(int a, int b)
{
    return a + b;
}
int sub(int a, int b)
{
    return a - b;
}
int mul(int a, int b)
{
    return a * b;
}
int div(int a, int b)
{
    return a / b;
}
int main()
{
 	 int x, y;
     int input = 1;
     int ret = 0;
     int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //函数指针的数组 //转移表
     while (input)
     {
          printf( "*************************\n" );
          printf( " 1:add           2:sub \n" );
          printf( " 3:mul           4:div \n" );
          printf( "*************************\n" );
          printf( "请选择:" );
      scanf( "%d", &input);
          if ((input <= 4 && input >= 1))
         {
          	printf( "输入操作数:" );
            scanf( "%d %d", &x, &y);
            ret = (*p[input])(x, y);
         }
          else
            printf( "输入有误\n" );
          printf( "ret = %d\n", ret);
     }
      return 0;
}

16.7 指向函数指针数组的指针

指向函数指针数组的指针是一个 指针

指针指向一个 数组 ,数组的元素都是 函数指针 ;

如何定义?

#include <stdio.h>
int Add(int x, int y)
{
    return x + y;
}
int main()
{
    int arr[10] = {0};
    int (*p)[10] = &Arr;//取出数组的地址,放到数组指针里面
    
    int* (*pf)(int, int);//函数指针
    int (*pfArr[4])(int ,int);//pfArr 是一个数组 - 函数指针的数组
    //ppfArr是一个指向[函数指针数组]的指针
    int (*(*ppfArr)[4])(int ,int) = &pfArr//指向函数指针数组的指针
    //(*ppfArr) - 指针
    //(*ppfArr)[4] - 指针指向的是有4个元素的数组
    //剩下的就是元素的类型:int (*)(int ,int) - 函数指针
    //即:ppfArr 是一个数组指针,指针指向的数组有4个元素,指向的数组的每个元素的类型是一个函数指针 - int(*)(int, int)
    return 0;
}
void test(const char* str)
{
 	printf("%s\n", str);
}
int main()
{
     //函数指针pfun
     void (*pfun)(const char*) = test;
     //函数指针的数组pfunArr
     void (*pfunArr[5])(const char* str);
     pfunArr[0] = test;
     //指向函数指针数组pfunArr的指针ppfunArr
     void (*(*ppfunArr)[5])(const char*) = &pfunArr;
     return 0;
}

16.8 回调函数

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

#include <stdio.h>
void print(char* str)
{
    printf("hehe:%S", str);
}
void test(void(*p)(char*))
{
    printf("test\n");
    p("bit");
}
int main()
{
    test(print);
    return 0;
}

首先演示一下 qsort 函数的使用:

qsort - quike sort - 快速排序 - 可以排序任意类型的数据 - 头文件:#include <stdlib.h>

void qsort(void *base, 
           size_t num, 
           size_t width, 
           int (*cmp)(const void* e1, const void* e2));
//base -- 指向要排序的数组的第一个元素的指针。
//num -- 由 base 指向的数组中元素的个数。
//width -- 数组中每个元素的大小,以字节为单位。
//cmp -- 用来比较两个元素的函数。
//第一个参数:待排序数组的首元素地址
//第二个参数:待排序数组的元素个数
//第三个参数:待排序数组的每个元素的大小 - 单位是字节
//第四个参数:是函数指针比较两个元素所用函数的地址(这个函数使用者自己实现)
//      	函数指针的参数是:带比较的两个元素的地址
//返回值:
// > 0 ----- 第一个元素e1大于第二个元素e2
// = 0 ----- 第一个元素e1等于第二个元素e2   
// < 0 ----- 第一个元素e1小于第二个元素e2  
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2)
{
    //比较两个整型值
  	return (*( int *)p1 - *(int *) p2);//强制类型转换,将p1和p2转换成整型的地址
}
//封装成一个函数,专门排序整型数组
void test1()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int sz = sizeof(arr) / aizeof(arr[0]);
    qsort(arr, sz, sizeof (arr[0]), int_cmp);
    int i = 0;
    for (i = 0; i< sz; i++)
    {
       printf( "%d ", arr[i]);
    }
}

int cmo_float(const void* e1, const void* e2)
{
    // return *(float*)e1 - *(float*)e2;//因为要返回整型
    if (*(float*)e1 == *(float*)e2)
    {
        return 0;
    }
    else if(*(float*)e1 > *(float*)e2)
    {
        return 1;
    }
    else
    {
        return -1;
    }
    //或者
    return ((int)(*(float*)e1 - *(float*)e2));
}
void test2()
{
    float f[] = {9.0, 8.0, 7.0, 5.0, 4.0};
    int sz = sizeof(f) / sizeof(f[0]);
    qsort(f, sz, sizeof(f[0]), cmp_float);
    int j = 0;
    for (j = 0; j < sz; j++)
    {
        printf("%f ", f[i]);
    }
}

//年龄来比较
int cmp_stu_by_age(const void* e1, const void* e2)
{
    //年龄比较可以相减
    return ((struct Stu*)e1) -> age - ((struct Stu*)e2) -> age;
}
//名字来比较
int cmp_stu_by_name(const void* e1, const void* e2)
{
    //两个字符串比较不能直接用大于小于号进行比较
    //得用字符串比较函数 - strcmp()函数
    return strcmp(((struct Stu*)e1) -> name ((struct Stu*)e2) -> name);
}
void test3()
{
    //两个结构体不能用大于小于号比
    //两个结构体对象是复杂对象
    //首先得确定你是按照名字来排序还是年林来排序
	struct Stu s[3] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 10} };
    int sz = sizeof(s) / sizeof(s[0]);
    //用年龄排序
    qsort(s, sz, sizeof(s[0]), cmp_struct_by_age);
    //用名字排序
    qsort(s, sz, sizeof(s[0]), cmp_struct_by_name);
}

struct Stu
{
  	char name[20];
    int age;
};
int main()
{
    test1();
    test2();
    test3();
    return 0;
}

使用回调函数,模拟实现qsort(采用冒泡的方式)。

注意:这里第一次使用 void 的指针*,讲解 void* 的作用。

  • void* 类型的指针,可以接收任意类型的地址。
  • void* 类型的指针,不能进行解引用操作。
  • void* 类型的指针,不能进行 ± 整数的操作
#include <stdio.h>
int int_cmp(const void * e1, const void * e2)
{
    //比较两个整型值
  	return (*( int *)e1 - *(int *) e2);//强制类型转换,将p1和p2转换成整型的地址
}

//年龄来比较
int cmp_stu_by_age(const void* e1, const void* e2)
{
    //年龄比较可以相减
    return ((struct Stu*)e1) -> age - ((struct Stu*)e2) -> age;
}
//名字来比较
int cmp_stu_by_name(const void* e1, const void* e2)
{
    //两个字符串比较不能直接用大于小于号进行比较
    //得用字符串比较函数 - strcmp()函数
    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;
        *buf2 = *buf1;
        *buf2 = tmp;
        buf++;
        buf2++;
    }
}
//实现bubble_sort函数的程序员,他是否知道未来排序的数据类型 - 不知道
//那程序员也不知道,待比较两个元素的类型
void bubble_sort(void* base, int sz, int width, int (*cmp)(void* e1, void* e2))//sz - 数组的元素个数
{
    //比较的轮数
    int i =0;
    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(cmp((char*)base + j * width, (char*)base + (j + 1) * width));
            }
		}
    }
}
void test4()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int sz = sizeof(arr) / sizeof(arr[0]);//元素个数
    //使用bubble_sort的程序员一定知道自己排序的是什么数据
    //就应该知道如何比较待排序数组中的元素
    bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
    //打印
    int i = 0;
    for (i = 0; i< sz; i++)
    {
       printf( "%d ", arr[i]);
    }
}
void test5()
{
    struct Stu s[3] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 10} };
    int sz = sizeof(s) / sizeof(s[0]])
    bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);
    bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);
    //打印
    int i = 0;
    for (i = 0; i< sz; i++)
    {
       printf( "%d ", s[i]);
    }
}
int main()
{
    test4();
    test5();
    return 0;
}

16.9 指针和数组笔试题解析

//一维数组
//数组名是手元素的地址
//1. sizeof(数组名) - 数组名表示整个数组
//2. &数组名 - 数组名表示整个数组
int a[] = {1, 2, 3, 4};
printf("%d\n",sizeof(a));//16 - sizeof(数组名) - 计算的是数组总大小 - 单位是字节 - 4 * 4 = 16
printf("%d\n",sizeof(a+0));//4/8 - 看你平台:32位结果就是4,64位结果就是4, 
						//数组名这里表示首元素的值,a+0还是首元素地址,地址的大小就是4/8个字节
printf("%d\n",sizeof(*a));//4 - 数组名表示首元素地址,*a 就是首元素,sizeof(*a)就是4
printf("%d\n",sizeof(a+1));//4/8 - 数组名这里表示首元素的值,a+1是第二个元素的地址,地址的大小就是4/8个字节
printf("%d\n",sizeof(a[1]));//4 - 第二个元素的大小
printf("%d\n",sizeof(&a));//4/8 - &a取出的是数组的地址,但是数组的地址也是地址,地址的大小就是4/8个字节
printf("%d\n",sizeof(*&a));//16 - &a是数组的地址,数组的地址解引用访问的数组,sizeof计算的就是数组的大小,单位是字节
printf("%d\n",sizeof(&a+1));//4/8 - &a是数组的地址,&a+1虽然地址跳过整个数组,但还是地址,所以是4/8个字节
printf("%d\n",sizeof(&a[0]));//4/8 - &a[0]是第一个元素的地址
printf("%d\n",sizeof(&a[0]+1));//4/8 - &a[0]+1是第二个元素的地址
//字符数组
char arr[] = {'a', 'b','c','d','e','f'};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
char *p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p+1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p+1));
printf("%d\n", sizeof(&p[0]+1));
printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));
//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));

总结:

数组名的意义:

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。

  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。

  3. 除此之外所有的数组名都表示首元素的地址。

16.10 指针笔试题

笔试题1:

int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1);
    printf( "%d,%d", *(a + 1), *(ptr - 1));
    return 0;
}
//程序的结果是什么?

笔试题2

//由于还没学习结构体,这里告知结构体的大小是20个字节
struct Test
{
     int Num;
     char *pcName;
     short sDate;
     char cha[2];
     short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
     printf("%p\n", p + 0x1);
     printf("%p\n", (unsigned long)p + 0x1);
     printf("%p\n", (unsigned int*)p + 0x1);
     return 0;
}

笔试题3

int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int *ptr1 = (int *)(&a + 1);
    int *ptr2 = (int *)((int)a + 1);
    printf( "%x,%x", ptr1[-1], *ptr2);
    return 0;
}

笔试题4

#include <stdio.h>
int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    int *p;
    p = a[0];
    printf( "%d", p[0]);
 	return 0;
}

笔试题5

int main()
{
    int a[5][5];
    int(*p)[4];
    p = a;
    printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
    return 0;
}

笔试题6

int main()
{
    int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    int *ptr1 = (int *)(&aa + 1);
    int *ptr2 = (int *)(*(aa + 1));
    printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
    return 0;
}

笔试题7

#include <stdio.h>
int main()
{
     char *a[] = {"work","at","alibaba"};
     char**pa = a;
     pa++;
     printf("%s\n", *pa);
     return 0;
}

笔试题8

int main()
{
     char *c[] = {"ENTER","NEW","POINT","FIRST"};
     char**cp[] = {c+3,c+2,c+1,c};
     char***cpp = cp;
     printf("%s\n", **++cpp);
     printf("%s\n", *--*++cpp+3);
     printf("%s\n", *cpp[-2]+3);
     printf("%s\n", cpp[-1][-1]+1);
     return 0;
}

第17 章、字符函数和字符串函数

17.0 前言

C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在常量字符串 中或者 字符数组 中。

字符串常量 适用于那些对它不做修改的字符串函数.

17.1 函数介绍

17.1.1 strlen
size_t strlen ( const char * str );//ujnsigned int == size_t
  • 字符串已经 ‘\0’ 作为结束标志,strlen函数返回的是在字符串中 ‘\0’ 前面出现的字符个数(不包含 ‘\0’ )。
  • 参数指向的字符串必须要以 ‘\0’ 结束。
  • 注意函数的返回值为size_t,是无符号的( 易错 )
  • 学会strlen函数的模拟实现

注:

#include <stdio.h>
int main()
{
    //字符串如何比较
     if(strlen("abc") - strlen("abcdef"))
     {
         printf("hehe\n");
     }
    else
    {
        printf("haha\n");
    }
    //最终结果是hehe
    //因为strlen()的返回类型是size_t - unsigned int
     return 0;
}
17.1.2 strcpy
char* strcpy(char * destination, const char * source);
//拷贝从目标字符串的第一个字符开始被替换
  • Copies the C string pointed by source into the array pointed by destination, including the
  • terminating null character (and stopping at that point).
  • 源字符串必须以 ‘\0’ 结束。
  • 会将源字符串中的 ‘\0’ 拷贝到目标空间。
  • 目标空间必须足够大,以确保能存放源字符串。
  • 目标空间必须可变。
  • 学会模拟实现。
17.1.3 strcat
char * strcat ( char * destination, const char * source );
#include <stdio.h>
int main()
{
    //错误的写法:arr1的空间放不下拼接后的arr1
    char arr1[] = "hello";
    
    char arr1[30] = "hello";
    char arr2[] = "world";
    ctrcat(arr1, arr2);
    printf("%s", arr1);
    return 0;
}
  • Appends a copy of the source string to the destination string. The terminating null character
  • in destination is overwritten by the first character of source, and a null-character is included
  • at the end of the new string formed by the concatenation of both in destination.
  • 源字符串必须以 ‘\0’ 结束。
  • 目标空间必须有足够的大,能容纳下源字符串的内容。
  • 目标空间必须可修改。
  • 字符串自己给自己追加,如何?
17.1.4 strcmp
int strcmp ( const char * str1, const char * str2 );

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(const char *str1, const char * str2) 这里面只能比较字符串,即可用于比较两个字符串常量,或比较数组和字符串常量,不能比较数字等其他形式的参数。

ANSI 标准规定,返回值为正数,负数,0 。而确切数值是依赖不同的C实现的。

当两个字符串不相等时,C 标准没有规定返回值会是 1 或 -1,只规定了正数和负数。

有些会把两个字符的 ASCII 码之差作为比较结果由函数值返回。但无论如何不能以此条依据作为程序中的流程逻辑。

17.1.5 strncpy
char * strncpy ( char * destination, const char * source, size_t num );
  • Copies the first num characters of source to destination. If the end of the source C string

    (which is signaled by a null-character) is found before num characters have been copied,

    destination is padded with zeros until a total of num characters have been written to it.

  • 拷贝num个字符从源字符串到目标空间。

  • 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。

17.1.6 strncat
char * strncat ( char * destination, const char * source, size_t num );
  • 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

/* strncat example */
#include <stdio.h>
#include <string.h>
int main ()
{
     char str1[20];
     char str2[20];
     strcpy (str1,"To be ");
     strcpy (str2,"or not to be");
     strncat (str1, str2, 6);
     puts (str1);
     return 0;
}
17.1.7 strncmp
int strncmp ( const char * str1, const char * str2, size_t num );
  • 比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完。
/* strncmp example */
#include <stdio.h>
#include <string.h>
int main ()
{
  char str[][5] = { "R2D2" , "C3PO" , "R2A6" };
  int n;
  puts ("Looking for R2 astromech droids...");
  for (n=0 ; n<3 ; n++)
  if (strncmp (str[n],"R2xx",2) == 0)
 {
    printf ("found %s\n",str[n]);
 }
  return 0;
}
17.1.8 strstr
//查找字符串
//在字符串 str1 中查找第一次出现字符串 str2 的位置,不包含终止符 '\0'。
char * strstr ( const char *str1, const char * str2);
//读英文文档基本的认识
//NULL - 空指针
//NUL / Null - '\0'
  • Returns a pointer to the first occurrence of str2 in str1, or a null pointer if str2 is not part of str1.
  • 该函数返回在 str1 中第一次出现 str2 字符串的位置/地址,如果未找到则返回 null。
/* strstr example */
#include <stdio.h>
#include <string.h>
int main ()
{
 	char* p1 = "abcdefghidef";
    char* p2 = "def";
    char* ret = strstr(p1, p2);
    if (ret == NULL)
    {
        printf("子串不存在\n");
    }
    else
    {
        printf("%s", ret);//defghidef
    }
}
17.1.9 strtok
char * strtok ( char * str, const char * sep );
  • sep参数是个字符串,定义了用作分隔符的字符集合

  • 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。

  • strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容

    并且可修改。)

  • strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。

  • strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。

  • 如果字符串中不存在更多的标记,则返回 NULL 指针。

/* strtok example */
#include <stdio.h>
#include <string.h>
int main ()
{
      char str[] ="- This, a sample string.";
      char * pch;
      printf ("Splitting string \"%s\" into tokens:\n",str);
      pch = strtok (str," ,.-");
      while (pch != NULL)
     {
        printf ("%s\n",pch);
        pch = strtok (NULL, " ,.-");
     }
      return 0;
}
#include <stdio.h>
int main()
{
     char *p = "zhangpengwei@bitedu.tech";
     const char* sep = ".@";
     char arr[30];
     char *str = NULL;
     strcpy(arr, p);//将数据拷贝一份,处理arr数组的内容
     for(str=strtok(arr, sep); str != NULL; str=strtok(NULL, sep))
     {
     	printf("%s\n", str);
     }
     return 0;
}
17.1.10 strerror
char * strerror ( int errnum );
//从内部数组中搜索错误号 errnum,并返回一个指向错误消息字符串的指针。strerror 生成的错误字符串取决于开发平台和编译器。

返回错误码,所对应的错误信息。

/* strerror example : error list */
#include <stdio.h>
#include <string.h>
#include <errno.h>//必须包含的头文件
int main ()
{
     FILE * pFile;
     pFile = fopen ("unexist.ent","r");
     if (pFile == NULL)
        printf ("Error opening file unexist.ent: %s\n",strerror(errno));
        //errno: Last error number
      return 0;
    //错误码     错误信息
    //0 - 		No error
    //1 - 		Operation not permitted
    //2 - 		No such file or directory
    //3 - 
    //...
    //errno 是一个全局的错误码的变量
    //当C语言的库函数在执行过程中,发生了错误,就会把对应的错误码,赋值到errno中
}
Edit & Run

字符分类函数:

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

字符转换:

int tolower ( int c );
int toupper ( int c );
/* isupper example */
#include <stdio.h>
#include <ctype.h>
int main ()
{
  int i=0;
  char str[]="Test String.\n";
  char c;
  while (str[i])
 {
    c=str[i];
    if (isupper(c)) 
        c=tolower(c);
    putchar (c);
    i++;
 }
  return 0;
 }
17.1.11 memcpy
//操作对象:字符串,\0
strcpy() strcat() strcmp() strncat() sstrncmp()
//操作对象:整型数组、浮点型数组、结构体数组
memcpy() memmove() memcmp()
void * memcpy ( void * destination, const void * source, size_t num );
  • 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
  • 这个函数在遇到 ‘\0’ 的时候并不会停下来。
  • 如果source和destination有任何的重叠,复制的结果都是未定义的。
/* memcpy example */
#include <stdio.h>
#include <string.h>
struct {
	char name[40];
	int age;
} person, person_copy;
int main ()
{
  	char myname[] = "Pierre de Fermat";
  	/* using memcpy to copy string: */
  	memcpy ( person.name, myname, strlen(myname)+1 );//+1 - \0 //或者世界使用sizeof(myname) - 字节数
  	person.age = 46;
 	/* using memcpy to copy structure: */
  	memcpy ( &person_copy, &person, sizeof(person) );
  	printf ("person_copy: %s, %d \n", person_copy.name, person_copy.age );
  	return 0;
}
17.1.12 memmove
void * memmove ( void * destination, const void * source, size_t num );
  • 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
  • 如果源空间和目标空间出现重叠,就得使用memmove函数处理。
/* memmove example */
#include <stdio.h>
#include <string.h>
int main ()
{
  char str[] = "memmove can be very useful......";
  memmove (str+20, str+15, 11);
  puts (str);
  return 0;
}
17.1.13 memcmp
int memcmp ( const void * ptr1, 
 const void * ptr2, 
 size_t num );
  • 比较从ptr1和ptr2指针开始的num个字节

  • 返回值如下:

  • 如果返回值 < 0,则表示 str1 小于 str2。

  • 如果返回值 > 0,则表示 str1 大于 str2。

  • 如果返回值 = 0,则表示 str1 等于 str2。

/* memcmp example */
#include <stdio.h>
#include <string.h>
int main ()
{
      char buffer1[] = "DWgaOtP12df0";
      char buffer2[] = "DWGAOTP12DF0";
      int n;
      n=memcmp ( buffer1, buffer2, sizeof(buffer1) );
      if (n>0) printf ("'%s' is greater than '%s'.\n",buffer1,buffer2);
      else if (n<0) printf ("'%s' is less than '%s'.\n",buffer1,buffer2);
      else printf ("'%s' is the same as '%s'.\n",buffer1,buffer2);
      return 0;
}

17.2 库函数的模拟实现

17.2.1 模拟实现strlen

三种方式:

方式1:

#include <stdio.h>
#include <assert.h>
//计数器方式
int my_strlen(const char * str)//这里使用了int,与size_t各有利弊
{
     int count = 0;
     //因为你传递的是指针,指针得保证有效性
     assert(str != NULL);
     while(*str)
     {
         count++;
         str++;//*str++;多此一举
     }
     return count;
}
int main()
{
    int len = my_strlen("abcdef");
    printf("%d\n", len);
}

方式2:

//不能创建临时变量计数器
//即递归的方式:不擦行间临时变量求字符串长度
int my_strlen(const char * str)
{
     if(*str == '\0')
     return 0;
     else
     return 1+my_strlen(str+1);
}

方式3:

//指针-指针的方式
int my_strlen(char *s)
{
       char *p = s;
       while(*p != ‘\0)
              p++;
       return p-s;
}
17.2.2 模拟实现strcpy

参考代码:

//1.参数顺序
//2.函数的功能,停止条件
//3.assert
//4.const修饰指针
//5.函数返回值
//6.题目出自《高质量C/C++编程》书籍最后的试题部分
char *my_strcpy(char *dest, const char*src)
{ 
     char *ret = dest;
     assert(dest != NULL);
     assert(src != NULL);
	//拷贝src指向的字符串到dest指向的空间,包含'\0'
     while((*dest++ = *src++))
     {
        ;
     }
    //返回目的空间的起始位置
     return ret;
}
int main()
{
    char arr1[] = "abcdefghi";
    char arr2[] = "bit";
    //错误的写法
    char* arr1 = "abcdefghi";//常量字符串
    char* arr2[] = { 'b', 'i', 't'};
    my_strcpy(arr1, arr2);
    return 0;
}
17.2.3 模拟实现strcat

参考代码:

char *my_strcat(char *dest, const char*src)
{
     char *ret = dest;
     assert(dest != NULL);
     assert(src != NULL);
     //1.找到目的字符串的'\0'
     while(*dest)
     {
        dest++;
     }
     //2.拼接
     while((*dest++ = *src++))
     {
        ;
     }
     return ret;
}
17.2.4 模拟实现strstr

注:让他们下去自己研究一下KMP算法。

#include <stdio.h>
#include <assert.h>
char* my_strstr(const char* p1, const char* p2)
{
    assert(p1 != NULL);
    assert(p2 != NULL);
    char *s1 = p1;
    char *s2 = p2;
    char *cur = p1;
    if (*p2 == '\0')
    {
        return p1;
    }
    while (*cur)
    {
        s1 = cur;
        s2 = (char*)p2;
        while (*s1 && *s2 && (*s1 == *s2))
        {
            s1++;
            s2++;
        }
        if (*s2 == '\0')
        {
            return cur;//找到子串
        }
        if (*s1 == '\0')
        {
            return NULL;//提前终止//当p1的字符串长度小于p2的长度//abc, abcdef
        }
        cur++;
    }
    return NULL;//找不到子串
}
int main()
{
    char* p1 = "abcdedef";
    char* p2 = "def";
    char* ret = strstr(p1, p2);
    retuen 0;
}
   
char *  strstr (const char * str1, const char * str2)
{
        char *cp = (char *) str1;
        char *s1, *s2;
        if ( !*str2 )
            return((char *)str1);
        while (*cp)
       {
                s1 = cp;
                s2 = (char *) str2;
                while ( *s1 && *s2 && !(*s1-*s2) )
                        s1++, s2++;
                if (!*s2)
                        return(cp);
                cp++;
       }
        return(NULL);
}     
17.2.5 模拟实现strcmp

参考代码:

int my_strcmp(const char * src, const char * dst)
{
    assert(*str1 && *str2);
    //比较
    while (*str1 == *str2)
    {
        if(*str1 == '\0')
        {
            return 0;//相等
        }
        str1++;
        str2++;
    }
    return (*str1 - *str2);
}
int main()
{
    char* p1 = "abcdef";
    char* p2 = "qwert";
    int ret = my_strcmp(p1, p2);	
    return 0;
}
//写法二
int my_strcmp (const char * src, const char * dst)
{
    int ret = 0 ;
 	assert(src != NULL);
   	assert(dest != NULL);
    while( ! (ret = *(unsigned char *)src - *(unsigned char *)dst) && *dst)
        ++src, ++dst;
    if ( ret < 0 )
        ret = -1 ;
    else if ( ret > 0 )
        ret = 1 ;
    return( ret );
}
17.2.6 模拟实现memcpy

参考代码:

#include <stdio.h>
#include <assert.h>
void * memcpy ( void * dest, const void * src, size_t num)
{
    void * ret = dest;
 	assert(dest);//assert(dest != NULL);
   	assert(src);//assert(src != NULL);
    /*
    * copy from lower addresses to higher addresses
    */
    while (num--) 
    {
        *(char *)dest = *(char *)src;
        ++(char *)dest;
        ++(char *)src;
    }
    return ret;
}
struct S
{
	char name[20];
	int age;
};
int main()
{
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    my_memcpy(arr + 2, arr);//拷贝的原数据的空间跟目标数据有关联的话拷贝会失误 - 内存重叠 - 得使用memmove()
    //my_memcpy()不能处理重叠拷贝
    //但你发现,使用memcpy可以处理内存重叠的拷贝
 
    /*
        C语言标准:
        memcpy 只要处理不重叠的内存拷贝就可以了
        memmove 处理重叠内存的拷贝
        当下发现:VS2013环境下的memcpy函数可以处理重叠拷贝
        你要求memcpy 考60就可以了,但它考了100分
        你要求么么move考100分,它就只能考100分
     */   
    
    int arr1[] = {1, 2, 3, 4, 5};
    int arr2[5] = {0};
    my_memcpy(arr2, arr1, sizeof(arr1));
    struct S arr3[] = { {"张三", 20}, {"李四", 30} };
    struct S arr4[3] = {0};
    my_memcpy(arr2, arr1, sizeof(arr1));
    return 0;
}
17.2.7 模拟实现memmove

参考代码:

void * my_memmove ( void * dst, const void * src, size_t count)
{
    assert(dst != NULL);
    assert(src != NUll)
    void * ret = dst;
    if (dst <= src || (char *)dst >= ((char *)src + count)) {
        /*
        * Non-Overlapping Buffers
        * copy from lower addresses to higher addresses
        */
        //前 -> 后
        while (count--) 
        {
            *(char *)dst = *(char *)src;
            ++(char *)dst;
            ++(char *)src;
        }
    }
    else {
        /*
        * Overlapping Buffers
        * copy from higher addresses to lower addresses
        */
        //后 -> 前
        dst = (char *)dst + count - 1;
        src = (char *)src + count - 1;
        while (count--) 
        {
            *(char *)dst = *(char *)src;
            dst = (char *)dst - 1;
            src = (char *)src - 1;
        }
    }
    return(ret);
}

第18章、自定义类型:结构体,枚举,联合

18.1.1 结构的基础知识

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

18.1.2 结构的声明
struct tag
{
 	member-list;
}variable-list;

例如描述一个学生:

struct Stu
{
     char name[20];//名字
     int age;//年龄
     char sex[5];//性别
     char id[20];//学号
}s4, s5, s6; //全局变量 //分号不能丢
struct Stu s3;//全局变量
int main()
{
    //局部变量
    struct Stu s1;
    struct Stu s2;
    return 0;
}
18.1.3 特殊的声明

在声明结构的时候,可以不完全的声明。

比如:

//匿名结构体类型 - 不建议经常用
//匿名结构体类型 只能使用一次,你用了一次不想用了,就可以声明匿名结构体类型
struct
{
     int a;
     char b;
     float c;
}x;
struct
{
     int a;
     char b;
     float c;
}a[20], *p;//结构体指针

上面的两个结构在声明的时候省略掉了结构体标签(tag)。

那么问题来了?

//在上面代码的基础上,下面的代码合法吗?
p = &x;

警告:

编译器会把上面的两个声明当成完全不同的两个类型。

所以是非法的。

18.1.4 结构的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

//代码1
struct Node
{
     int data;//4
     struct Node next;//不能包含自己类型的结构体成员变量
};
//可行否?不行
如果可以,那sizeof(struct Node)是多少?

正确的自引用方式:

//代码2
struct Node
{	//链表
     int data;//4 - 数据域
     struct Node* next;//存放一个指针 - 指针的大小可以确定 - 4/8 - 存放下一个节点的地址 - 指针域
};

注意:

//代码3
//typedef - 对类型重命名
typedef struct
{
     int data;//4
     Node* next;//4/8
}Node;//把类型名简化成了Node
//这样写代码,可行否?不行
//你Node还没有创建,结构体里面怎么能使用呢?
//解决方案:
typedef struct Node
{
     int data;
     struct Node* next;
}Node;//类型名
18.1.5 结构体变量的定义和初始化

有了结构体类型,那如何定义变量,其实很简单。

struct Point
{
     int x;
     int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
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};//结构体嵌套初始化
18.1.6 结构体内存对齐

我们已经掌握了结构体的基本使用了。

现在我们深入讨论一个问题:计算结构体的大小。

这也是一个特别热门的考点: 结构体内存对齐

//练习1
struct S1
{
     char c1;
     int i;
     char c2;
};
printf("%d\n", sizeof(struct S1));//12
//练习2
struct S2
{
     char c1;
     char c2;
     int i;
};
printf("%d\n", sizeof(struct S2));//8
//练习3
struct S3
{
     double d;
     char c;
     int i;
};
printf("%d\n", sizeof(struct S3));//16
//练习4-结构体嵌套问题
struct S4
{
     char c1;
     struct S3 s3;
     double d;
};
printf("%d\n", sizeof((struct s4));//32    

考点

如何计算

首先得掌握结构体的对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

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

  • VS中默认的值为8

  • gcc编译器没有对齐数,相当于成员的大小就是它的对齐数

  1. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

  2. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

为什么存在内存对齐?

大部分的参考资料都是如是说的:

  1. 平台原因(移植原因)

​ 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

  1. 性能原因

​ 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。

​ 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总体来说:

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

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:

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

//例如:
struct S1
{
     char c1;
     int i;
     char c2;
};
struct S2
{
     char c1;
     char c2;
     int i;
};

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

18.1.7 修改默认对齐数

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

#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
     char c1;
     int i;
     char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

#pragma pack(1)//设置默认对齐数为1
struct S2
{
     char c1;
     int i;
     char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

int main()
{
    //输出的结果是什么?
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));
    return 0;
}

结论:

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

百度笔试题:

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

考察: offsetof 宏的实现

注:这里还没学习宏,可以放在宏讲解完后再实现。

/*
C 库宏 offsetof(type, member-designator) 会生成一个类型为 size_t 的整型常量,它是一个结构成员相对于结构开头的字节偏移量。成员是由 member-designator 给定的,结构的名称是在 type 中给定的。

声明
offsetof(type, member-designator);
参数
type -- 这是一个 class 类型,其中,member-designator 是一个有效的成员指示器。
member-designator -- 这是一个 class 类型的成员指示器。
返回值
该宏返回类型为 size_t 的值,表示 type 中成员的偏移量。
*/
#include <stdio.h>
#include <stddef.h>
struct S
{
  	char c;
    int i;
    double d;
};
int main()
{
    printf("%d\n", offsetof(struct S, c));//c的偏移量 - 0
    printf("%d\n", offsetof(struct S, i));//4
    printf("%d\n", offsetof(struct S, d));//8
    return 0;
}
18.1.8 结构体传参

直接上代码:

struct S
{
     int data[1000];
     int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S tmp)
{
 	printf("%d\n", tmp.num);
}
//结构体地址传参
//如果要改变结构体的内容,得使用指针传参
//这里只是打印,传值传参都可以
void print2(struct S* ps)
{
 	printf("%d\n", ps->num);
}
int main()
{
     print1(s);  //传结构体
     print2(&s); //传地址
     return 0;
}

上面的 print1 和 print2 函数哪个好些?

答案是:首选print2函数。

原因:

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

结论:

结构体传参的时候,要传结构体的地址。

18.2 位段

结构体讲完就得讲讲结构体实现 位段 的能力。

18.2.1 什么是位段

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

1.位段的成员必须是 int、unsigned int 或signed int 。

2.位段的成员名后边有一个冒号和一个数字。

位段就是用来节省空间的。

比如:

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

A就是一个位段类型。

那位段A的大小是多少?

printf("%d\n", sizeof(struct A));//8个字节
/*
先开辟了第一个int - 32个bit
_a分配两个bit,_b,分配了5个bit,_c分配了10个bit,此时已经17个bit了,剩下的不够存放_d了,
第一个int剩下的bit就不用了,舍弃掉了。然后再开辟第二个int,用来存放_d
所以是8个字节
注:int类型的变量初始化不能超过32,即int _d : 33;是错误的。
*/
18.2.2 位段的内存分配
  1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型

  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

//一个例子
struct S
{
     char a:3;
     char b:4;
     char c:5;
     char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
//空间是如何开辟的?

18.2.3 位段的跨平台问题
  1. int 位段被当成有符号数还是无符号数是不确定的。

  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机

​ 器会出问题。

  1. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

  2. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是

​ 舍弃剩余的位还是利用,这是不确定的。

总结:

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

18.2.4 位段的应用

18.3 枚举

枚举顾名思义就是一一列举。

把可能的取值一一列举。

比如我们现实生活中:

  • 一周的星期一到星期日是有限的7天,可以一 一列举。
  • 性别有:男、女、保密,也可以一一列举。
  • 月份有12个月,也可以一 一列举

这里就可以使用枚举了。

18.3.1 枚举类型的定义
enum Day//星期
{
     Mon,
     Tues,
     Wed,
     Thur,
     Fri,
     Sat,
     Sun
};
enum Sex//性别
{
     MALE,
     FEMALE,
     SECRET
};
enum Color//颜色
{
     RED,
     GREEN,
     BLUE
};
int main()
{
    enum Sex s = Male;//不能赋值枚举变量以外的值
    enum Color c = RED;
    return 0;
}

以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。

{}中的内容是枚举类型的可能取值,也叫 枚举常量 。

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

例如:

enum Color//颜色
{
     RED = 1,
     GREEN = 2,
     BLUE = 4
};
18.3.2 枚举的优点

为什么使用枚举?

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

枚举的优点:

  1. 增加代码的可读性和可维护性

  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。

  3. 防止了命名污染(封装)

  4. 便于调试

  5. 使用方便,一次可以定义多个常量

18.3.3 枚举的使用
enum Color//颜色
{
     RED = 1,
     GREEN = 2,
     BLUE = 4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5;               //ok??

18.4 联合(共用体)

18.4.1 联合类型的定义

联合也是一种特殊的自定义类型

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

比如:

//联合类型的声明
union Un
{
     char c;
     int i;
};
//联合变量的定义
union Un u;
//计算连个变量的大小
printf("%d\n", sizeof(u));//4
18.4.2 联合的特点
  • 联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

  • 在同一时刻,联合体的成员变量是不能同时使用的,只能用一个。

union Un
{
     int i;
     char c;
};
union Un u;//开辟空间
printf("%p\n", &u);//00F1F90C
printf("%p\n", &(u.i));//00F1F90C
printf("%p\n", &(u.c));//00F1F90C

面试题:

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

#include <stdio.h>
int check_sys()
{
    int a = i;
    //返回1,表示小端
    //返回0,表示大端
    return *(char*)&a;
}
//或:
int check_sys()
{
    union Un
    {
      	char c;
        int i;
    }u;
    u.i = 1;
    //返回1,表示小端
    //返回0,表示大端
    return u.c;
}
int main()
{
    int a = 1;
    int ret = check_sys();
    if (ret ==1)
    {
        printf("小端\n");
    }
    else
    {
        printf("大端\n");
    }
    
    
    //int a = 0x11223344;
    /*
    假设
    低地址------------>高地址
    ......[][][11][22][33][44][][][]......大端字节序存储模式
    ......[][][44][33][22][11][][][]......小端字节序存储模式
    讨论一个数据,放在内存中存放的字节序数
    大小端字节序问题
    */
    return 0;
}
18.4.3 联合大小的计算
  • 联合的大小至少是最大成员的大小。
  • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

比如:

union Un1
{
     char arr[5];//5 看char , 1, 默认对齐数8 对齐数:1
     int i;//4 对齐数:4
     //arr[5]有5个字节,4的一杯不够---->8
};
union Un2
{
     short c[7];
     int i;
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));//8
printf("%d\n", sizeof(union Un2));//8

18.5 练习

通讯录程序

第19章、动态内存管理

19.1 为什么存在动态内存分配

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

内存分为:

  • 栈区:存放局部变量、函数的形式参数、局部数组
  • 堆区:动态内存分配
  • 静态区:全局变量、静态变量、全局数组
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

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

  1. 空间开辟大小是固定的。

  2. 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,

那数组的编译时开辟空间的方式就不能满足了,这时候就只能试试动态存开辟了。

19.2 动态内存函数的介绍

19.2.1 malloc 和 free

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

void* malloc (size_t size);

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

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
#include <stdio.h>
#include <stdlib.h>
int main()
{
    //向内存申请10个空间
    int* p = malloc(10*sizeof(int));
    int* p = (int*)malloc(10*sizeof(int));//严格上说,要强制转换成(int*),但有些编译器不会触发警告,但加上好一点。
    //开辟失败
    int* p = (int*)malloc(INT_MAX);
    if (p == NULL)//开辟失败
    {
        //打印错误原因的一个方式
        printf("%s\n", strerror(errno));
    }
    else//开辟成功
    {
        //正常使用空间
        int i = 0;
        for (i = 0; i < 10; i++)
        {
            *(p + 1) = i;
        }
        for (i = 0; i < 10; i++)
        {
            printf("%d", *(p + i));
        }
    }
    //当动态申请的空间不再使用的时候
    //就应该还给操作系统
    free(p);
    //程序结束的时候,即生命周期到了,也会把malloc函数开辟的空间还给操作系统,即使你没有使用free函数也会被你操作系统回收。
    //但当你的程序还有其他任务要执行的时候,假设要执行2天时间,但是malloc函数开辟的空间已经不使用了。
    //但malloc函数还在占用这块空间,使用free函数,执行别的任务的时候就可以申请这块空间。
    p = NULL;
    //调试时发现,free之后,p之前指向的那块空间已经不属于p了,释放了,但还是指向了这块空间的地址,使用p还能找到这块空间
    //找到空间就有肯能破坏它,这个指针依然还是相当危险的,此时可以主动将p改成空指针 
    return 0;
}

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

void free (void* ptr);

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

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

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

举个例子:

#include <stdio.h>
#include <string,h>
#include <stdlib.h>
int main()
{
     //代码1
     int num = 0;
     scanf("%d", &num);
     int arr[num] = {0};
     //代码2
     int* ptr = NULL;
     ptr = (int*)malloc(num*sizeof(int));
     if(NULL != ptr)//判断ptr指针是否为空
     {
     	int i = 0;
     	for(i=0; i<num; i++)
     	{
     		*(ptr+i) = 0;
     	}
     }
     free(ptr);//释放ptr所指向的动态内存
     ptr = NULL;//是否有必要?有必要
     return 0;
}
19.2.2 calloc

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

void* calloc (size_t num, size_t size);
  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。举个例子:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
 	int *p = (int*)calloc(10, sizeof(int));
 	if(NULL != p)
 	{
 		//使用空间
        int i = 0;
        for (i = 0; i < 10; i++)
        {
            printf("%d", *(p + i));//0 0 0 0 0 0 0 0 0 
        }
 	}
    else
    {
        printf("%s\n", strerror(errno));
    }
    //释放空间
    //free函数是用来释放动态开辟的空间的
	free(p);
 	p = NULL;
 	return 0;
}

所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。

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

函数原型如下:

void* realloc (void* ptr, size_t size);
#include <stdio.h>
#inclued <string .h>
int main()
{
    int *p = (int*)malloc(20);
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
    }
    else
    {
        int i = 0;
        for (i = 0; i < 5; i++)
        {
            *(p + i) = i;
        }
    }
    //在使用malloc函数开辟的20个字节的空间
    //假设这里。20个字节不能满足我们的使用了
    //现在希望能够拥有40个字节的空间
    //这里可以使用realloc来调整动态开辟的内存 
    int* p2 = realloc(p, 40);
    //问题:后面变成p2来维护这块空间了
    //只用p行不行?
    //可以,但有很大的风险
    /* 	realloc函数使用的注意事项:
    	1. 如果 p 指向的空间之后有足够的内存空间可以追加,则直接追加,然后返回p的地址
    	2. 如果 p 指向的空间之后没有足够的内存空间可以追加,则realloc函数会重新找
    	   一个新的内存区域来开辟一块满足需求的空间,并且把原来内存中的数据拷贝回
    	   来,释放旧的内存空间,最后返回新开辟的内存空间的地址
        3. 如果开辟一个新的地址还是不行,即追加失败,会返回一个空指针,如INT_MAX
           则会导致p访问不到原来的空间,需要进行判断。
           所以需要一个新的变量来接收realloc函数的返回值
    */
    p = realloc(p, 40);
    int* ptr = realloc(p, INT_MAX);
    if (ptr != NULL)
    {
        p = ptr;
    }
    int i = 0;
    for (i = 5; i < 5; i++)
        {
            *(p2 + i) = i;//*(p + i) = i;
        }
    for (i = 0; i < 10; i++)
    {
        printf("%d ", *(p2 + i));//1 2 3 4 5 6 7 8 9 10
        //printf("%d ", *(p + i));
    }
    //释放空间
    free(p);
    p =NULL;
    //有个疑问
    //如果realloc函数需要开辟一个新的空间,访问新的地址,原来的空间怎么办?
    //realloc函数会将原来的空间释放掉,不用free函数动手
    return 0;
}
  • ptr 是要调整的内存地址
  • size 调整之后新大小
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
  • realloc在调整内存空间的是存在两种情况:
    • 情况1:原有空间之后有足够大的空间
    • 情况2:原有空间之后没有足够大的空间

情况1

当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

情况2

当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。

由于上述的两种情况,realloc函数的使用就要注意一些。

举个例子:

#include <stdio.h>
int main()
{
 	int *ptr = (int*)malloc(100);
 	if(ptr != NULL)
 	{
     	//业务处理
 	}
 	else
 	{
     	exit(EXIT_FAILURE);    
 	}
 	//扩展容量
 	//代码1
 	ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
 
 	//代码2
 	int*p = NULL;
 	p = realloc(ptr, 1000);
 	if(p != NULL)
 	{
 		ptr = p;
 	}
 	//业务处理
 	free(ptr);
 	return 0;
}

19.3 常见的动态内存错误

19.3.1 对NULL指针的解引用操作
#include <stdio.h>
int main()
{
    int*p = (int*)malloc(40;);
    //1. 万一maloloc函数开辟失败了,p被赋值为NULL
    //*2. p = 0;
    //对空指针进行解引用操作都是非法操作
    
    //需要判断p是否为空指针
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        *(p + i) = i;
    }
    free(p);
    p = NULL:
    return 0;
}
19.3.2 对动态开辟空间的越界访问
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);
    p = NULL;
}
19.3.3 对非动态开辟内存使用free释放
void test()
{
 	int a = 10;//在栈区开辟的空间,动态内存管理在堆区
 	int *p = &a;
    *p = 20;
 	free(p);//ok?
     p = NULL}
19.3.4 使用free释放一块动态开辟内存的一部分
#include <stdio.h>
#include <stdlib.h>
int main()
{
    int* p = (int*)malloc(40);
    if (p == NULL)
    {
        return 0;
    }
    int i = 0;
    for (i = 0; i < 10; i++)
    {
     	*p++ = i;   
    }
    //回收空间
    //此时p已经不是指向最初的位置了, p移动到了指向了动态内存的第10个元素的后面一个元素的地址,p不再指向动态内存的起始位置
    //使用free函数只能回收p指向动态内存的起始位置的那块空间
    free(p);
    p = NULL;
    return 0;
}
19.3.5 对同一块动态内存多次释放
#include <stdio.h>
#include <stdlib.h>
int main()
{
    int* p = (int*)malloc(40);
    if (p == NULL)
    {
        return 0;
    }
    //使用
    //释放
    free(p);
    //...
    free(p);
    return 0;
    /*
    如何避免多次释放的这种现象呢?
    1. 谁开辟了谁回收
    2. 使用free函数,一定要跟 变量 = 空指针; 这条语句
    	这样子的话,再使用free函数,回收一个空指针,其实什么也没有干。
    */
}
19.3.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
 	int *p = (int *)malloc(100);//内存泄漏,而且出了test找不到,p是临时变量
 	if(NULL != p)
     {
        *p = 20;
     }
}
int main()
{
 	test();
	while(1);
}
#incldue <stdio.h>
#include <windows.h>
int main()
{
    while(1)
    {
        malloc(1);
        Sleep(1000);
    }
    return 0;
}

忘记释放不再使用的动态开辟的空间会造成内存泄漏。

切记:

动态开辟的空间一定要释放,并且正确释放 。

19.4 几个经典的笔试题

题目1:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void GetMemory(char *p)//接收的不是地址,是变量p,存的是NULL
{
 	p = (char *)malloc(100);//假设malloc函数开辟的空间地址为0x0012ff40
    //此时p存放的不再是NULL,而是0x0012ff40
    //p是GetMemory函数的形参,只能在函数内部有效,等GetMemory函数返回后,动态开辟内存并未释放,
    //所以会造成内存泄漏
}
void Test(void)
{
     char *str = NULL;
     GetMemory(str);//值传递
     //此时str还是空指针,str并没有指向一个有效的空间,你非要把hello world拷贝过去,相当于解引用空指针
     //遍历的话就是访问非法内存,程序崩溃。
     strcpy(str, "hello world");//在这里程序崩溃
     printf(str);//语法没问题,但是上面的代码已经有问题了,执行不了
}
//程序崩溃
int main()
{
    Test();
    return 0;
}
//拓展
char*str = "abcdef";
printf("%s", str);
printf(str);
printf("abcdef");
//这三种的输出结果都是一样的

请问运行Test函数会有什么样的结果?

  1. 运行代码会出现崩溃的现象
  2. 程序存在内存泄漏的问题
//如何将代码修改正确
//改法1 - 参数方式
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void GetMemory(char **p)//要接收的是一个指针,而不是一个地址;一个*则是接收一个变量的地址
{
 	*p = (char *)malloc(100); //*p - 即指向str的内容
}
void Test(void)
{
     char *str = NULL;
     GetMemory(&str);//&str,即对char*的指针解引用
     strcpy(str, "hello world");
     printf(str);
     free(str);
     str = NULL:
}
int main()
{
    Test();
    return 0;
}
//改法2 - 返回值方式
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
char* GetMemory(char *p)
{
 	p = (char *)malloc(100);
    return p;
}
void Test(void)
{
     char *str = NULL;
     str = GetMemory(str);
     strcpy(str, "hello world");
     printf(str);
     free(str);
     str = NULL:
}
int main()
{
    Test();
    return 0;
}

题目2:

#inclued <stdio.h>
//返回栈空间的地址的问题  -注:栈空间的地址不要随便返回,会导致问题
char *GetMemory(void)
{
     char p[] = "hello world";//栈区 - p[] - 局部变量,这个数组空间只能在这个函数内使用
     return p;//结束之前,把地址返回了
}
void Test(void)
{
     char *str = NULL;
     str = GetMemory();//当函数调用完,把返回的地址赋给str后,这个函数算调完了,
    				 //p[]还给操作系统了,这个空间里面的值是什么已经不清楚了。
     printf(str);
}
int main()
{
    Test();//输出的是一个随机值 
    return 0;
}

请问运行Test函数会有什么样的结果?

题目3:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void GetMemory(char **p, int num)
{
 	*p = (char *)malloc(num);//堆区 - 只有free才能回收
}
void Test(void)
{
     char *str = NULL;
     GetMemory(&str, 100);
     strcpy(str, "hello");
     printf(str);
    //忘记释放动态开辟的内存,导致内存泄漏了
    //改正
    free(str);
    str = NULL;
}
int main()
{
    Test();//hello  但内存会泄漏
    return 0;
}

请问运行Test函数会有什么样的结果?

题目4:

void Test(void)
{
     char *str = (char *) malloc(100);
     strcpy(str, "hello");
     free(str);//考查点:free释放str指向的空间后,并不会把str置为NULL;而不是free的早晚问题
     str = NULL;//哥在这呢过添加的代码
 	if(str != NULL)
 	{
         strcpy(str, "world");
         printf(str);
 	}
}
int main()
{
    Test();//world   非法访问内存
    return 0;
}

请问运行Test函数会有什么样的结果?

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

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

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结

    束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是

    分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

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

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

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

有了这幅图,我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了。

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。

但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长。

19.6 柔性数组

也许你从来没有听说过**柔性数组(flexible array)**这个概念,但是它确实是存在的。

C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

例如:

typedef struct st_type
{
     int i;
     int a[0];//未知大小的 - 柔性数组成员 - 数组的大小是可以调整的
}type_a;

有些编译器会报错无法编译可以改成:

typedef struct st_type
{
     int i;
     int a[];//柔性数组成员
}type_a;
//与上面的方式一样,能编译过去就用哪个
19.6.1 柔性数组的特点
  • 结构中的柔性数组成员前面必须至少一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

例如:

//code1
typedef struct st_type
{
 int i;
 int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//输出的是4
19.6.2 柔性数组的使用
//代码1
#include <stdio.h>
struct S
{
  	int n;  
    int arr[0];//未知大小的 - 柔性数组成员 - 数组的大小是可以调整的
};
int main()
{
    struct S s;
    printf("%d\n", sizeof(s));//4
    //sizeof(struct S)的大小给变量n
    //5 * sizeof(int) - 追加的大小给数组arr[]开辟的
    struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
    ps -> n = 100;
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        ps -> arr[i] = i;//0 1 2 3 4
    }
    //调整大小
    struct S* ptr = realloc(ps, 44);
    if (ptr  != NULL)
    {
        ps = ptr;
    }
    for (i = 5; i < 10; i++)
    {
        ps -> arr[i] = i;//5 6 7 8 9
    }
    //打印
    for (i = 5; i < 10; i++)
    {
        printf("%d ", ps -> arr[i]);//0 1 2 3 4 5 6 7 8 9 10
    }
    //释放
    free(ps);
    ps = NULL;
    return 0;
}

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

19.6.3 柔性数组的优势

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

//代码2
#include <stdio.h>
struct S
{
  	int n;  
    int *arr;//sizeof 返回的这种结构大小包括 int *arr 的内存。
};
int main()
{
   	struct S*ps = (struct S*)malloc(sizeof(struct S));
    ps -> arr = malloc(5 * sizeof(int));
    int i = 0;
    for ( i = 0; i < 5; i++)
    {
        ps -> arr[i] = i;
    }
    for (i = 0; i < 5; i++)
    {
        printf("%d ", ps -> arr[i]);
    }
    //调整大小
    int *ptr = realloc(ps -> arr, 10 * sizeof(int));
    if (ptr != NULL)
    {
        ps -> arr = ptr;
    }
    for (i = 5; i < 10; i++)
    {
        ps -> arr[i] = i;
    }
    for (i = 0; i < 10; i++)
    {
        printf("%d ", ps -> arr[i]);
    }
    //释放内存
    free(ps -> arr);
    ps -> arr = NULL;
    free(ps);
    ps = NULL;
    return 0;
}

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

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

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

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

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

扩展阅读:

C语言结构体里的数组和指针 ---- [C语言结构体里的成员数组和指针 | 酷 壳 - CoolShell]

第20章、文件操作

20.1 为什么使用文件

我们前面学习结构体时,写了通讯录的程序,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据是存放在内存中,当

程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难

受。我们在想既然是通讯录就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。

这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。

使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。

20.2 什么是文件

磁盘上的文件是文件。

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

20.2.1 程序文件

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

20.2.2 数据文件

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

本章讨论的是数据文件。

在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。

其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。

20.2.3 文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。

文件名包含3部分:文件路径+文件名主干+文件后缀

例如: c:\code\test.txt

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

20.3 文件的打开和关闭

20.3.1 文件指针

缓冲文件系统中,关键的概念是 “文件类型指针”,简称 “文件指针”。

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置

等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE.

例如,VS2013编译环境提供的 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 指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息

就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件

比如:

20.3.2 文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件

在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。

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

//打开文件
//使用给定的模式 mode 打开 filename 所指向的文件。
FILE * fopen ( const char * filename, const char * mode );
/*
参数
    filename -- 字符串,表示要打开的文件名称。
    mode -- 字符串,表示文件的访问模式,可以是以下表格中的值:
返回值
	该函数返回一个 FILE 指针。否则返回 NULL,且设置全局变量 errno 来标识错误。
*/
//关闭文件
//关闭流 stream。刷新所有的缓冲区。
int fclose ( FILE * stream );
/*
参数
    filename -- 字符串,表示要打开的文件名称。
    mode -- 字符串,表示文件的访问模式,可以是以下表格中的值:
返回值
	该函数返回一个 FILE 指针。否则返回 NULL,且设置全局变量 errno 来标识错误。    
*/
#include<stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
    FILE* pf = fopen("test.txt", "w");
    if (pf == NULL)
    {
        printf("%s\n", strerror(errno));
        return 0;
    }
    //写文件
    
    //关闭文件
    fclose(pf);
    pf = NULL;
    return 0;
}

打开方式如下:

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

实例代码:

#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
    //写文件
    FILE* pf = fopen("text.txt", "w");
    //读文件
    FILE* pf = fopen("text.txt", "r");
    if (pf == NULL)
    {
        printf("%s\n", strerror(errno));
    }
    //写文件
    fputc('b', pf);
    fputc('i', pf);
    fputc('t', pf);
    
    //读文件
    int ch = fgetc(pf);
    printf("%c", ch);//b
    ch = fgetc(pf);
    printf("%c", ch)//i
    ch = fgetc(pf);
    printf("%c", ch)//t
    //关闭文件
    fclose(pf);
    pf = NULL;
    return 0;
}

20.4 文件的顺序读写

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

scanf/fscanf/sscanf

printf/fprintf/sprintf

这里演示讲解这句函数的使用和对比

scanf / printf: 是针对标准输入流/标准输出流的 格式化输入/输出语句

fscanf / fprintf: 是针对所有输入流/所有 输出流的 格式化输入/输出语句

sscanf / sprintf:

  • sscanf是从字符串中读取格式化的数据
  • sprintf是把格式化数据输出成(存储到)字符串

20.5 文件的随机读写

20.5.1 feek

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

//设置流 stream 的文件位置为给定的偏移 offset,参数 offset 意味着从给定的 whence 位置查找的字节数。
int fseek ( FILE * stream, long int offset, int whence );
/*
参数
stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
offset -- 这是相对 whence 的偏移量,以字节为单位。
whence -- 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
常量			描述
SEEK_SET	  文件的开头
SEEK_CUR	  文件指针的当前位置
SEEK_END	  文件的末尾

返回值
	如果成功,则该函数返回零,否则返回非零值。
*/

例子:

/* fseek example */
#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 );
      return 0;
}
20.5.2 ftell

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

//返回给定流 stream 的当前文件位置。
long int ftell(FILE *stream);
/*
参数
	stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
返回值
	该函数返回位置标识符的当前值。如果发生错误,则返回 -1L,全局变量 errno 被设置为一个正值。
*/

例子:

#include <stdio.h>
int main()
{
    FILE* pf = foprn("text.txt", "r");
    if (pf == NULL)
    {
		return 0;
    }
    //定义文件位置
    fgetc(pf);//起始位置
    //fseek(pf, -2, SEEK_END);
    int ch = fgetc(pf);
    printf("%d", ch)
        
    int pos = ftell(pf);
    printf("%d", pos);//1
    
    rewind(pf);
    ch = fgetc(pf);
    printf("%d", ch);
    
    fclose(pf);
    pf = NULL;
    return 0;
}
20.5.3 rewind

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

例子:

/* rewind example */
#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;
}

20.6 文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件或者二进制文件

数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件

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

一个数据在内存中是怎么存储的呢?

字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占

4个字节(VS2013测试)。

测试代码:

#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;
}

20.7 文件读取结束的判定

20.7.1 被错误使用的feof

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。

而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束

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

    //声明
    int fgetc(FILE *stream);
    /*
    参数
    	stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了要在上面执行操作的流。
    返回值
    	该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。
    */
    
    //声明
    char *fgets(char *str, int n, FILE *stream);
    /*
    参数
        str -- 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
        n -- 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
        stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。
    返回值
        如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。
        如果发生错误,返回一个空指针。
    */
    

​ 例如:

  • fgetc 判断是否为 EOF .

  • fgets 判断返回值是否为 NULL .

  1. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。

​ 例如:

  • 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);
}

二进制文件的例子:

#include <stdio.h>
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);
}

20.8 文件缓冲区

ANSIC 标准采用**“缓冲文件系统”**处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一

块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则

从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小

根据C编译系统决定的。

#include <stdio.h>
#include <windows.h>
//VS2013 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语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。

如果不做,可能导致读写文件的问题。

第21章、程序的编译(预处理操作)和 + 链接

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

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

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

21.2 详解编译+链接

21.2.1 翻译环境

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

看代码:

sum.c

int g_val = 2016;
void print(const char *str)
{
 	printf("%s\n", str);
}

test.c

#include <stdio.h>
int main()
{
     extern void print(char *str);
     extern int g_val;
     printf("%d\n", g_val);
     print("hello bit.\n");
     return 0;
}

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

test.c

#include <stdio.h>
int main()
{
 int i = 0;
 for(i=0; i<10; i++)
 {
 printf("%d ", i);
 }
 return 0;
}
  1. 预处理 选项 gcc -E test.c -o test.i

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

​ 编译完成之后就停下来,结果保存在test.s中。

  1. 汇编 gcc -c test.c

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

VIM学习资料

简明VIM练级攻略:

https://coolshell.cn/articles/5426.html

给程序员的VIM速查卡

https://coolshell.cn/articles/5479.html

21.2.3 运行环境

程序执行的过程:

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序

​ 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

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

  2. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(

​ stack),存储函数的局部变量和返回

​ 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。

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

注:

介绍一本书《程序员的自我修养》

21.3 预处理详解

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

这些预定义符号都是语言内置的。

举个栗子:

printf("file:%s line:%d\n", __FILE__, __LINE__);
21.3.2 #define
21.3.2.1 #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;

这里会出现语法错误。

21.3.2.2 #define 定义宏

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

下面是宏的申明方式:

#define name( parament-list ) stuff

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

注意:

参数列表的左括号必须与name紧邻。

如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

如:

#define SQUARE( x ) x * x

这个宏接收一个参数 x .

如果在上述声明之后,你把

SQUARE( 5 );

置于程序中,预处理器就会用下面这个表达式替换上面的表达式:

5 * 5

警告:

这个宏存在一个问题:

观察下面的代码段:

int a = 5;
printf("%d\n" ,SQUARE( a + 1) );

乍一看,你可能觉得这段代码将打印36这个值。

事实上,它将打印11.

为什么?

替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:

printf (“%d\n”,a + 1 * a + 1 );

这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。

在宏定义上加上两个括号,这个问题便轻松的解决了:

#define SQUARE(x) (x) * (x)

这样预处理之后就产生了预期的效果:

printf ("%d\n",(a + 1) * (a + 1) );

这里还有一个宏定义:

#define DOUBLE(x) (x) + (x)

定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。

int a = 5;
printf("%d\n" ,10 * DOUBLE(a));

这将打印什么值呢?

warning:

看上去,好像打印100,但事实上打印的是55.

我们发现替换之后:

printf ("%d\n",10 * (5) + (5));

乘法运算先于宏定义的加法,所以出现了

55 .

这个问题,的解决办法是在宏定义表达式两边加上一对括号就可以了。

#define DOUBLE( x)   ( ( x ) + ( x ) )

提示:

所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中

的操作符或邻近操作符之间不可预料的相互作用。

21.3.2.3 #define 替换规则

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

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先

被替换。

  1. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。

  2. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。

  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

21.3.2.4 #和##

如何把参数插入到字符串中?

首先我们看看这样的代码:

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

这里输出的是不是

hello bit ?

答案是确定的:是。

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

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

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

  1. 另外一个技巧是:

使用 # ,把一个宏参数变成对应的字符串

比如:

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

## 的作用

##可以把位于它两边的符号合成一个符号。

它允许宏定义从分离的文本片段创建标识符。

#include <stdio.h>
#define CAT(X, Y) X##Y
int mian()
{
    int Class84 = 2019;
    //printf("%d\n", Class84);//2019
    printf("%d\n", CAT(Class, 84));//2019
    //1. printf("%d\n",Class##84);
    //2. printf("%d\n",Class84);
    return 0;
}
#define ADD_TO_SUM(num, value) \
 sum##num += value;
...
ADD_TO_SUM(5, 10);//作用是:给sum5增加10.

注:

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

21.3.2.5 带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能

出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。

例如:

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

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

#include <stdio.h>
#define MAX(X, Y)   ((X) > (Y) ? (x) : (Y))
int main()
{
    int a = 10;
    int b = 11;
    int max = MAX(a++, b++);
    //替换后的结果:宏的参数是直接替换进去,而不是算好之后替换进去
    //int max = ((a++) > (b++) ? (a++) : (b++));
    printf("%d\n", max);//12
    printf("%d\n", a);//11
    printf("%d\n", b);//13
    return 0;
}
#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
21.3.2.6 宏和函数对比

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

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

命名约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。

那我们平时的一个习惯是:

把宏名全部大写

函数名不要全部大写

21.3.3 undef

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

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

许多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
21.3.5 条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件

编译指令。

比如说:

调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

#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
21.3.6 文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方

一样。

这种替换的方式很简单:

预处理器先删除这条指令,并用包含文件的内容替换。

这样一个源文件被包含10次,那就实际被编译10次。

21.3.6.1 文件包含的方式:
  • 本地文件包含
#include "filename"

查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标

准位置查找头文件。

如果找不到就提示编译错误。

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

/usr/include

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

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

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

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

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

这样是不是可以说,对于库文件也可以使用 “” 的形式包含?

答案是肯定的,可以

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

21.3.6.2 嵌套文件包含

如果出现这样的场景:

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

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

注:

推荐《高质量C/C++编程指南》中附录的考试试卷(很重要)。

笔试题:

1. 头文件中的 ifndef/define/endif是干什么用的?
2. #include <filename.h> 和 #include "filename.h"有什么区别?

21.4 其他预处理指令

#error
#pragma
#line
...
不做介绍,自己去了解。
#pragma pack()在结构体部分介绍。

参考《C语言深度解剖》学习

  • 77
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值