C语言进阶篇

进阶篇

进阶篇

一、数据的存储

二、指针的进阶

三、阶段习题详解

四、字符函数和字符串函数

五、自定义类型:结构体,枚举,联合

六、动态内存管理

七、C语言文件操作

八、程序环境和预处理

一、数据的存储

深度解析数据在内存中的存储。

数据类型的介绍

C语言的类型分为两类

  1. 内置类型

char int long (long long) float double

  1. 自定义类型(构造类型)

1. 内置类型

基本的内置类型

char		//字符数据类型
short		//短整型
int 		//整型
long		//长整形
long long	//更长的整型
float		//单精度浮点数
double		//双精度浮点数

类型的意义:

  1. 使用的时候开辟空间的大小。(大小一定意义上决定了使用的范围)。
  2. 如何看待内存空间的视角。(eg:我存放的是整型那我存放的是补码)。
类型的基本归类:

整型分组:

char:(因为内存中是ASCII码)
	unsigned char   --无符号字符
	signed char		--有符号字符
short:
	unsigned short [int]	--无符号短整型
	signed short [int]		--有符号短整型
int:
	unsigned int		--无符号整型
	signed int			--有符号整型
long:
	unsigned long [int]		--无符号长整型
	signed long [int]		--有符号长整型

浮点型分组:

float			//单精度浮点型
double			//双精度浮点型

2. 构造类型(自定义类型)

1. 数组类型
2. 结构体类型  	struct
3. 枚举类型		enum
4. 联合类型		union

3. 其他类型

指针类型
int* pi;
char* pc;
float* pf;
void* pv;

指针类型的大小都是确定的4或者8字节。用来存放变量的地址的变量。

空类型
# void 表示空类型(无类型)
# 通常应用于函数的返回类型、函数的参数、指针类型<无具体类型的指针>。

整型在内存中的存储

变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。

但是具体是怎么进行存储的呢?

原码、反码、补码

在计算机中整型的有符号数有三种表示方式,原码、反码及补码。(无符号数也是这三种,但是原反补相同)。正数的原反补也是相同的。

三种表示方法均有符号位和数值位两部分,符号位是用0表示正,1表示负。但是数值位的三种表示方式是不相同的。

原码

直接将二进制按照正负数的形式得到的二进制。

反码

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

补码

反码+1得到的就是补码。

# eg:20
	原码:00000000000000000000000000010100    (共32位)
	反码:00000000000000000000000000010100
	补码:00000000000000000000000000010100
	16进制:0x00000014
# eg:-10
	原码:10000000000000000000000000001010
	反码:11111111111111111111111111110101
	补码:11111111111111111111111111111010
	16进制:0xFFFFFFF6(补码--计算机中存储)

对于整型,数据存放在内存中其实存放的是补码。

为什么?

在计算机系统中,数值(整数)一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码和原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

image-20220107155626568

总结:补码可以直接带着符号位进行计算。

image-20220107160508028

大小端介绍

什么是大端小端?

所谓的大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;所谓的小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中。全称:大端字节存储模式  小端字节存储模式。

image-20220107161456962

注:44是低地址。

为什么要存在大端和小端?

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

注:计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的,所以,计算机的内部处理都是小端字节序。但是,人类还是习惯读写大端字节序,所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

设计程序来判断当前使用计算机的字节序是什么。

//判断是否小端存储,大端存储返回1  其他语言这里可以直接写成bool类型的函数// int checkEndian_S(){//     int a = 1;//     char* p = (char*)&a;//     if(*p == 1){//         return 1;//     } else{//         return 0;//     }// }// 简化1:// int checkEndian_S(){//     int a = 1;//     char* p = (char*)&a;//     return *p;// }//简化2:int checkEndian_S(){    int a = 1;    return *(char*)&a;}int main(){    int res = checkEndian_S();    if(res == 1){        printf("小端存储模式");    } else{        printf("大端存储模式");    }    return 0;}

代码解析:需要判断存入内存的数据中第一个字节放的是什么。只访问一个字节,想到的就是指针类型访问类型变量所占空间的内存,使用的就是char类型的指针。

指针类型的意义:

  1. 指针类型决定了指针解引用操作符能访问几个字节:char* p; *p访问了一个字节,int* p; *p访问的是4个字节。
  2. 指针类型决定了指针加减的是几个字节;char* p ; p+1,跳过的是一个字节, int* p; *p跳过的是4个字节。
练习时间到:
1. 输出的是什么?
int main(){    char a = -1;    signed char b = -1;    unsigned char c = -1;    printf("a = %d, b = %d, c = %d",a,b,c);    return 0;}

解析:结果是a = -1, b = -1, c = 255。

-1的补码1000000000000000000000001,补码11111111111111111111111111111111,char内存中截断为11111111。

a输出的时候整形提升(前面补符号位)11111111111111111111111111111111,且为有符号(第一位为符号位剩下的7位代表的是数字)的情况结果为-1。

b输出的时候整形提升,且为有符号的情况,同a,结果为-1。

c输出的时候整形提升00000000000000000000000011111111,且为无符号的情况(不存在符号位,所有位都代表数字)结果为255。

2. 下面代码输出的结果是什么?
int main(){    char a = -128;    printf("%u\n",a);    return 0;}

解析:结果为4294967168。232-27

-128的原码10000000000000000000000010000000,补码是11111111111111111111111110000000。char存储截断10000000。

%u是打印十进制的无符号数。整型提升11111111111111111111111110000000。结果为4294967168。

3. 下面代码输出的结果是什么?
int main(){    char a = 128;    printf("%u\n",a);    return 0;}

解析:结果为4294967168。232-27

128的原码00000000000000000000000010000000,补码是00000000000000000000000010000000。char存储截断10000000。

%u是打印十进制的无符号数。整型提升11111111111111111111111110000000。结果为4294967168。

另一种理解方式:只有8位这是个循环的情况127的下一位就是-128。而我们本身是没有128的,那么128就是127的下一位。

插曲–>对char的总结

image-20220107182416360

4. 下面的结果是
int i = -20;
unsigned int j = 10;
printf("%d\n",i+j);

解析:结果为-10。

image-20220107184341555

5.下面的结果是
int main(){
    unsigned int i;
    for (i = 9; i >=0 ; i--) {
        printf("%d\n",i);
    }
    return 0;
}

image-20220107194927870

解析:输出的是死循环,先9 8 7 6 5 4 3 2 1 0,然后是-1 -2······

因为无符号数i永远大于0所以循环永远进行。

6.以下代码输出的结果是
int main(){
    unsigned int i;
    for (i = 9; i >=0 ; i--) {
        printf("%u\n",i);
    }
    return 0;
}

image-20220107195055542

解析:输出的是死循环,先9 8 7 6 5 4 3 2 1 0,然后是4294967295 4294967294······

因为无符号数i永远大于0所以循环永远进行。在0输出完之后,-1的补码是全1,此时输出是按照%u输出也就是还是无符号数的输出,将从-1开始的数减一一直输出。

7.以下代码执行的结果是
int main(){
    char a[1000];
    for(int i=0;i<1000;i++){
        a[i] = -1-i;
    }
    printf("%d",strlen(a));
    return 0;
}

解析:结果是255。

i每次加一,在负一的基础上开始减i。因为数组是char类型,所以会根据ASCII码对应的字符进行填充。在减去到-128之后因为补码的截断和整型提升的原因,补码的值得到相应的数就是127(进入了循环圈)。之后继续126、125······知道减到0此时ASCII码表对应的字符是’\0’,也就是我们字符串结束的标志。所以在执行strlen的时候就得到的字符串的长度是255+1-1 = 255。(加一是因为0没有计算,减一是因为’\0’不计算到字符串内)。

image-20220107200223418

注:在255之后仍然有值。

8.以下代码执行结果是
unsigned unsigned char i = 0;
int main(){
    for (i = 0; i <= 255; i++) {
        printf("hello world\n");
    }
    return 0;
}

解析:死循环。无符号char :255+1 = 0。

总结:在使用无符号数作为判断条件的时候一定要注意。很容易导致死循环。

浮点型在内存中的存储

常见的浮点数

3.1415926······

1E10

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

浮点数表示的范围,在float.h文件中定义。

浮点数使用的例子:

int main(){
    int n = 9;
    float* pFloat = (float *)&n;
    printf("n的值为:%d\n",n);
    printf("*pFloat值为:%f\n",*pFloat);
    *pFloat = 9.0;
    printf("n的值为:%d\n",n);
    printf("*pFloat值为:%f\n",*pFloat);
    return 0;
}

image-20220107210958852

结论:整型的存储方式和浮点数在内存中的存储形式或读取形式不一样

那么是怎么存储的呢?

根据国际标准IEEE,任意一个二进制的浮点数v都可以表示成下面的形式。

V = ( − 1 ) S ∗ M ∗ 2 E V = (-1)^S *M*2^E V=(1)SM2E ----(相当于二进制的科学计数法)

S表示的是符号位,S = 0时V是正数,当S = 1 时,V是负数。

M表示的是有效数字。

2 E 2^E 2E表示的是指数位。

举例:9.0
补码 1001.0
科学计数法 (-1)^0 * 1.001 * 2^3
科学计数法公式 (-1)^S *M *2^E

现在我们可以确定的是S = 0 M = 1.001 E = 3。因为有了相关的规定,我们只需要将S M E 三个值存起来,需要的时候将其还原即可得到我们的浮点数。

IEEE 754 规定:对于32位的浮点数,最高1位是浮点数,接着的8位是E,剩下的23位是有效数字M。

image-20220107225003529

对于64位的浮点数,最高的一位是符号位S,然后的11位是指数E,剩下的52位是有效数字M。

image-20220107225540458

**IEEE 754对有效数字M规定1≤M<2。**也就是说1.xxxxxx的形式。IEEE规定,在计算及内部保存M的时候,默认第一位总是1,所以可以舍弃不写,只保存后面的xxxxxx即可,等读取的时候再吧这个1加上。这样可以节省一位有效数字。

指数E,情况比较复杂

**首先,E是一个无符号的整数(unsigned int)**也就是,对于E的这8位,他的取值范围是0-255;double中E的取值范围是0-2047。但是,在指数的部分有负数的情况,这怎么处理呢?答案是,我们在范围内找到一个中间数,来区分正负数,float中使用的是127;double中使用的是1023。这样我们举个例子:存储2^10的时候E = 127+10 =137,2^(-10) 的时候E = 127-10 = 117。

**图解

image-20220108000155057

举例:
5.5的存储
转化为二进制:101.1 (小数点后面的数字比较难计算的)
写成科学计数法的形式 (-1)^0 * 1.011 * 2^2 S = 0 M = 1.011 E = 2+127
写成标准二进制:0|10000001|01100000000000000000000 (在M部分的时候从高位往地位写。)
0100 0000 1011 0000 0000 0000 0000 0000 -->> 0x40b00

但是读取很麻烦

指数从E中取出还可以分为三种情况:

E全为0

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

E全为1

此时,有效数字M全为0,表示±∞(正负号取决于符号位s);

E不为全0或者E不为全1

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

**图解

image-20220108003751962

举例:

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。

解释最初的问题

int main(){
    int n = 9;
    float* pFloat = (float *)&n;
    printf("n的值为:%d\n",n);
    printf("*pFloat值为:%f\n",*pFloat);
    *pFloat = 9.0;
    printf("n的值为:%d\n",n);
    printf("*pFloat值为:%f\n",*pFloat);
    return 0;
}

image-20220107210958852

9的补码00000000000000000000000000001001
用%d的形式读取:9
用%f读取:0 | 00000000 | 00000000000000000001001。
–>(-1)^0 * 0.00000000000000000001001 * 2^-126 无限接近于0。

*pFloat = 9.0 --> 1001.0 --> 1.001*2^3 --> (-1)^0*1.001 * 2^3 --> 0(0) 10000010(127+3) 00100000000000000000000
用%f读取:9.0
用%d读取0100 0001 0001 0000 0000 0000 0000 0000 --> 1091567616

二、指针的进阶

  1. 字符指针
  2. 数组指针
  3. 指针数组
  4. 数组传参和指针传参
  5. 函数指针
  6. 函数指针数组
  7. 指向函数指针数组的指针
  8. 回调函数
  9. 指针和数组面试题解析

声明:该文是学习C语言进阶时的笔记。学习内容:B站鹏哥C语言,p34-p51部分。文中有任何不懂的地方可以观看视频。

指针的基本概念

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

字符指针

使用形式:

int main(){
    char ch = 'w';
    char* pc = &ch;
    *pc = 's';//需要解引用才可以访问到地址,然后对地址进行修改。
    return 0;
}

字符数组用来存字符串时:

int main(){
    char arr[]="abcdef";
    char* pc = arr;//此时指针使用的是数组的首元素地址。
    return 0;
}

其他情况:(注意观察字符串和字符输出格式和指针的调用方式)。

int main(){
    const char* p = "abcdef";//此时"abcdef"是常量字符串。
    printf("%c\n",*p);
    printf("%s",p);
    return 0;
}

image-20220108103649704

解析:如果将第一个printf中的*去掉会警告 Format specifies type ‘int’ but the argument has type ‘char *’。
如果将第二个printf中p改为*p会警告 Format specifies type ‘char *’ but the argument has type ‘char’。
p表示的是地址内容。*p表示的是地址内容指向的东西。

int main() {
    char arr1[] = "abcdef";
    char arr2[] = "abcdef";
    if (arr1 == arr2) {
        printf("hehe");
    } else {
        printf("haaa");
    }
    return 0;
}

解析:答案是haaa;因为arr1、arr2存放的是首元素的地址,而第一个abcdef和第二个abcdef是存放在不同位置的字符串。

int main() {
    char* p1 = "abcdef";
    char *p2 = "abcdef";
    if (p1 == p2) {
        printf("hehe");
    } else {
        printf("haaa");
    }
    return 0;
}

解析:这次的答案是hehe;因为这次的"abcdef"是常量,在内存中仅存储了一份。所以p1和p2指向的其实是同一个地方。biao

标准的写法是

const char* p1 = "abcdef";
const char *p2 = "abcdef";

指针数组

其实是数组

int* parr[] = {arr1,arr2,arr3};

基本使用

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};
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 5; ++j) {
            printf("%d ", *(parr[i])+j);//访问初始地址后几个的int单元的地址。
        }
        printf("\n");
    }
    return 0;
}

image-20220108125032475

int* arr1[10];//整型指针数组
char* arr2[4];//字符指针数组
char** arr3[8];//二级字符指针数组

数组指针

数组指针是什么?是指针。

整形指针,是能够指向整型的指针。浮点型指针,是能够指向浮点型的指针。

那么数组指针就是要能指向数组的指针。

定义方式

int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int (*p)[10] = &arr ;//数组的地址要存起来。

首先p和*结合,说明是一个指针,然后指向的是一个大小为10 的整型的数组,所以p是一个指针指向数组。就是数组指针。

image-20220109001151570

小练习

//补全代码
int main(){
	char* arr[5];
    pa = &arr;
}
int main(){
    char* arr[5];
    char* (*pa)[5] = &arr;
    return 0;
}

image-20220108181238163

数组指针要素:1. 是一个指针 2. 指针名 3. 指针指向数组的大小 4. 指针指向元素的类型。

数组名 和 &数组名

对于int arr[10];arr和&arr分别是什么。数组名表示数组首元素的地址。&数组表示整个数组的地址。

所以就可以将&数组名存入数组指针当中去。

数组指针使用理解代码(实际不这样使用)

int mian(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int (*pa)[10] = &arr;
    //数组元素的遍历
    for(int i = 0;i<10;i++){
        printf("%d ",*(*pa+i));//需要理解*pa其实就是arr
    }
    //遍历方式2
    for(int i = 0;i<10;i++){
        printf("%d ",(*pa)[i]);
    }
    return 0;
}

实际使用中一般在二维数组以上。

//参数是指针
void print(int (*p)[5],int x,int y){
    int i = 0;
    for (i = 0; i < x; ++i) {
        int j = 0;
        for (j = 0; j < y; ++j) {
            printf("%d ",*(*(p+i)+j));//地址表示法
            //下面的这种也是可以的。
            // printf("%d ",(*(p+i))[j]);//数组访问的方法
            // printf("%d ",p[i][j]);
            // printf("%d ",*(p[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}};
    print(arr,3,5);
    return 0;
}

数组的四种访问形式

int main() {    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};    int i = 0;    int *p = arr;    for (i = 0; i < 10; ++i) {        printf("%d", p[i]);        printf("%d", *(p + i));        printf("%d", *(arr + i));        printf("%d", arr[i]);    }    return 0;}//p[i]==*(p + i)==*(arr + i)==arr[i]

小练习

解释下面的代码的意思。

int arr[5];int *parr1[10];int (*parr2)[10];int (*parr3[10])[5];

解析:第一行:arr是一个有5个元素的整型数组。
第二行:parr1是一个数组,该数组有10个元素,每个元素的类型时int*,综述parr1是一个指针数组。
第三行:parr2是一个指针,指针指向一个数组,数组有10个元素,每个元素的类型是int,综述,parr2是一个数组指针。
第四行:parr3是一个数组,该数组有10个元素,每个元素是一个数组指针,该数组指针指向的数组有5个元素,每个元素是int类型。(图解)

image-20220109002740510

数组参数、指针参数

在函数设计的时候难免会把数组或指针传给函数,那么函数应该怎么接收呢?

一维数组传参

怎么接收参数呢?

int main(){    int arr[10];    int* arr2 = {0};    test(arr);    test2(arr2);    return 0;}

test()接收参数

void test(int arr[]){}void test(int arr[10]){}//有没有定义大小都可以。void test(int *arr){}//直接接受传过来的地址。

test2()接受参数

void test2(int *arr[20]){}//有没有定义大小都可以。void test2(int *arr[]){}void test2(int **arr){}

二维数组传参

怎么接收参数呢?

int main(){    int arr[3][5] = {0};    test(arr);//二维数组传参    return 0;}

test()接受参数

void test(int arr[3][5]){}void test(int arr[][5]){}//注意,二维数组在传参的时候,行可以省略,但是列绝对不能省略。

因为在二维数组中,可以不知道有多少行,但是必须知道一行多少元素。

接受参数的为指针的形式

void test(int (*arr)[5]){}//指针参数应该指向首元素的地址,二维数组的首元素是一个数组。//又因为必须是指针,所以采用的是数组指针,指向数组的大小为5数组元素的类型为int。

不能直接使用一级指针,也不能直接使用二级指针。也不能直接用第一个数组的第一个元素的地址。
指针参数应该指向首元素的地址,二维数组的首元素是一个数组。

一级指针传参

接收参数

void test(int* p){}void test2(char* p){}//什么类型的指针接受什么类型的参数

怎么传参呢?倒着往回推:函数接收指针可以向函数传地址或者存放指针的变量。同时还要注意指针的类型。

int main(){    int a = 10;    int* p1 = &a;    test1(&a);    test1(p1);    char ch = 'h';    char* pc = &ch;    test2(&ch);    test2(pc);    return 0;}

二级指针传参

自定义函数形式,同时理解怎么接收参数。

void test(int** p){}

怎么给函数传参呢?思考什么是二级指针,二级指针的表现形式。
解析:二级指针变量,一级指针的地址,一级指针数组的数组名。

int main(){    int *p;//一级指针    int **pp = &*p;//一级指针的地址    int* arr[10];//指针数组。数组中存放的是指针。此时的数组名就相当于一个二级指针。    test(&*p);    test(pp);    test(arr);    }

函数指针

数组指针是指向数组的指针,函数指针是指向函数的指针是存放函数的地址的一个指针

int Add(int x,int y){    int z =0;    z = x + y;    return z;}int main(){    int a = 10;    int b = 20;    printf("%p\n",&Add);    printf("%p\n",Add);    return 0;}

image-20220110093215356

我们发现这就像数组名 和 &数组名是一样的道理。函数名和&函数名都是函数的地址。

对于数组指针我们有

int arr[10] = { 0 };int (*p)[10] = &arr;

那么函数指针是怎么回事呢?

int Add(int x,int y){}int (*pa)(int,int) = Add;//*先和pa结合说明是个指针。
函数指针的使用
int Add(int x,int y){    int z =0;    z = x + y;    return z;}int main(){    int a = 10;    int b = 20;	int (*pa)(int,int) = Add;    printf("%d\n",(*pa)(2,3));//(*pa)==Add    return 0;}

当然不同类型的函数指针定义的方式是不同的。

使用实例2

void Print(char* str){ printf("%s\n",str);}int main(){    void (*p)(char*) = Print;    (*p)("hello world");    return 0;}

简单分析一下:pfun1和pfun2哪个可以存放函数的地址。

void (*pfun1)();void* pfun2()

解析:首先第一步,要存放地址,需要找什么,需要找指针,哪个是指针,pfun1是指针。

pfun1存放的是指针。pfun1先和*结合。指针指向的是一个函数,这个函数是没有参数的,这个函数的返回值类型是void(相当于没有返回值。)。

阅读有趣的代码
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int, void(*)(int)))(int);

解析:代码1,首先void (*p)()这是一个函数指针,p是这个函数指针的变量名。void (*)()这是一个指针函数的类型。在0前面的括号中放一个类型,那么就是对0进行强制类型转化,转化成一个函数指针的地址。然后再用*解引用。解引用的就是void (*)()类型的函数。(*(类型)0)()。这样理解下来就是调用0地址处的函数。

代码2, void(*)(int)是一个函数指针类型,这个函数接收的参数是int类型。signal(int, void(*)(int))这是一个函数的声明,里面有两个参数,一个是int类型,一个是 void(*)(int)。(*signal(int, void(*)(int)))(int)所以这又是一个函数指针参数是int,返回类型是void。

//代码二的另一种理解。
typedef void(*pfun_t)(int);//pfun_t将void(*)(int)这种指针类型重命名
pfun_f signal(int,pfun_f);

对代码2的综述:signal是一个函数声明;signal函数的参数有2个,第一个是int。第二个是函数指针,该函数指针指向的函数的参数是int,返回类型是void;signal函数的返回类型也是一个函数指针:该函数指针指向的函数的参数是int,返回值是void。

函数指针补充
int Add(int x,int y){
    int z =0;
    z = x + y;
    return z;
}
int main(){
    int a = 10;
    int b = 20;
	int (*pa)(int,int) = Add;
    printf("%d\n",(pa)(2,3));
    printf("%d\n",(*pa)(2,3));
    printf("%d\n",(**pa)(2,3));
    return 0;
}

三种调用形式都是同样的正确的输出结果。那么这里的*就没有起到解引用的作用。但是我们一般使用前两种,比较好理解:第一种,pa==Add,Add本身就相当于一级指针,可以直接使用。第二种*pa解引用变量名使用。这里的*其实就是个摆设而已。但是注意的是如果有*这个括号是必须加的。

函数指针数组

数组是一个存放在相同类型数据的存储空间,我们可不可以将函数指针存放在里面呢?

指针数组我们已经学过了,那么函数指针也存在数组中是不是就和指针函数一个道理了。

那么怎么实现呢?

int (*parr[10])();

parr先和[10]结合,说明parr是一个数组,int(*)()是一个函数指针。

函数指针的用途:转移表

//加法
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() {
    //函数指针的数组的定义parr[4]数组变量名
    int (*parr[4])(int, int) = {Add, Sub, Mul, Div};
    for (int i = 0; i < 4; ++i) {
        // 函数指针数组的调用
        printf("%d\n", parr[i](2, 3));
    }
    return 0;
}
小练习

char* my_strcpy(char* dest,const char* src){}

  1. 写一个函数指针pf,能够指向my_strcpy
  2. 写一个函数指针数组pfArr,能够存放4个my_strcpy函数的地址
//1. 
char* (*pf)(char* ,const char*);
//2.
char* (*pfArr[4])(char* ,const char*) = {my_strcpy};
微型计算器
// 1. 不使用指针计算器
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 res = 0;
    do {
        printf("*************************\n");
        printf(" 1:add     2:sub \n");
        printf(" 3:mul     4:div    0.exit\n");
        printf("*************************\n");
        printf("请选择:");
        scanf("%d", &input);
        switch (input) {
            case 1:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                res = add(x, y);
                printf("res = %d\n", res);
                break;
            case 2:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                res = sub(x, y);
                printf("res = %d\n", res);
                break;
            case 3:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                res = mul(x, y);
                printf("res = %d\n", res);
                break;
            case 4:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                res = div(x, y);
                printf("res = %d\n", res);
                break;
            case 0:
                printf("退出程序\n");
                break;
            default:
                printf("选择错误\n");
                break;
        }
    } while (input);

    return 0;
}
//使用函数指针数组
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 res = 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);
            res = (*p[input])(x, y);
            printf("res = %d\n", res);
        } else if(input == 0) {
            printf("退出程序");
        }
        else printf("输入有误\n");
    }
    return 0;
}

指向函数指针数组的指针

数组+指针 --> 数组指针
函数+指针 --> 函数指针
函数指针+数组 --> 函数指针数组
函数指针数组+指针 --> 指向函数指针数组的指针

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

void test(char* str){ printf("%s\n",str); }
int main(){
    //函数指针
    void (*pf)(char*) = test;
    //函数指针数组pfArr
    void (*pfArr[4])(char*);
    pfArr[0] = test;//函数指针数组的使用
    //指向函数指针数组的指针ppfArr。
    void (*(*ppfArr)[4])(char*) = &pfArr;
    return 0;
}

void (*(*ppfArr)[4])(char*) = &pfArr;重新解析:(*ppfArr)是一个指针,(*ppfArr)[4]指向的是一个数组,(*(*ppfArr)[4])是一个函数指针数组,数组中每个函数的参数是(char*),返回值是void。

回调函数

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

在微型计算器的第一版中,switch语句中其实比较冗余,冗余的部分就是除了调用函数的其他语句,那么我们可不可以把这些冗余的部分封装成一个函数呢?然后函数调用的部分来用指针访问。

微型计算器
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;
}
void Calc(int (*pf)(int,int)){
    int x = 0;
    int y = 0;
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    printf("res = %d\n", pf(x,y));
}
void Calc(int (*pf)(int, int)) {
    int x = 0;
    int y = 0;
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    printf("res = %d\n", pf(x, y));
}

int main() {
    int x, y;
    int input = 1;
    int res = 0;
    do {
        printf("*************************\n");
        printf(" 1:add     2:sub \n");
        printf(" 3:mul     4:div    0.exit\n");
        printf("*************************\n");
        printf("请选择:");
        scanf("%d", &input);
        switch (input) {
            case 1:
                Calc(add);
                break;
            case 2:
                Calc(sub);
                break;
            case 3:
                Calc(mul);
                break;
            case 4:
                Calc(div);
                break;
            case 0:
                printf("退出程序\n");
                break;
            default:
                printf("选择错误\n");
                break;
        }
    } while (input);

    return 0;
}

Calc()函数接收传过来的指针参数,然后再通过这个参数调用传过来的那个函数的功能,那么传过来(被调用功能)的函数就被叫做回调函数。

image-20220110232952125

用qsort理解回调函数
  1. 冒泡函数回顾
//这里的冒泡排序是优化后的冒泡排序
void bubble_sort(int arr[],int sz){
    for(int i = 0; i<sz-1;i++){
        //定义标记
        int isSort = 1;
        //每一次的冒泡
        for(int j = 0;j<sz-i-1;j++){
            if(arr[j]>arr[j+1]){
                int temp = arr[j];// 交换
                arr[j]=arr[j+1];
                arr[j+1] = temp;
                isSort = 0;
            }
        }
        if (isSort == 1) break;		//最后的判断
    }
}
int main(){
	int arr[10] = {9,8,7,6,5,4,3,2,1,0};
    int sz = sizeof(arr)/sizeof(arr[0]);
    bubble_sort(arr,sz);
    int i = 0;
    for(i = 0;i < sz; i++){
        printf("%d",arr[i]);
    }
}

在原来我们可能觉得使用起来是够用的,但是我们仔细想一想,如果说我们传入的是一个浮点数,或者我们传入的是结构体,我们还要重新写一次函数吗?我们肯定是不愿意的。那有什么办法呢?这时我们想到了,在做微型计算器的时候我们使用了回调函数的概念。这里可不可以使用呢?

在解决上面的问题之前,我们先来看看别人是怎么做的。

  1. qsort()库函数,使用的是快速排序。
void qsort (void* base, size_t num, size_t size,
            int (*compar)(const void*,const void*));
//  void* base		目标数组的起止位置
//  size_t num		数组的大小
//  size_t size		元素的大小
//  int (*compar)(const void*,const void*)		比较函数

void*类型的指针可以接收任意类型的地址。但是void类型的指针是不能进行解引用操作的。这种类型的指针也不能进行加减整数的运算。所以需要运算的时候可以进行强制类型转化。

qsort()函数的使用

#include <stdlib.h>
#include <string.h>
typedef struct Stu {
    char name[20];
    int age;
} Stu;

int compare_int(const void *elem1, const void *elem2) {
    return *(int *) elem1 - *(int *) elem2;
}

void test1() {
    int arr[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    //compare_int()要求这个函数的返回值是a大返回>0,a小返回<0,一样大返回0
    qsort(arr, sz, sizeof(arr[0]), compare_int);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

//qsort要求这个函数的返回值为int
int compare_float(const void *elem1, const void *elem2) {
    return (int) (*(float *) elem1 - *(float *) elem2);
}

void test2() {
    float arr[] = {9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, sz, sizeof(arr[0]), compare_float);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%f ", arr[i]);
    }
    printf("\n");
}
//通过年龄排序
int compare_Stu_by_age(const void *elem1, const void *elem2){
    //(Stu*)elem1强制类型转化。
    return (int )(((Stu*)elem1)->age - ((Stu*)elem2)->age);
}
//通过名字排序
int compare_Stu_by_name(const void *elem1, const void *elem2){
    // 比较字符串需要使用strcmp
    return strcmp(((Stu*)elem1)->name, ((Stu*)elem2)->name);
}
void test3() {
    Stu arr[3] = {{"zhangsan", 20},
                {"lisi",     30},
                {"wangwu",   18}};
    int sz = sizeof(arr) / sizeof(arr[0]);
    // 通过年龄排序
    qsort(arr, sz, sizeof(arr[0]), compare_Stu_by_age);
    // 打印
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%s ", arr[i].name);
        printf("%d ", arr[i].age);
    }
    printf("\n");
    // 通过名字排序
    qsort(arr, sz, sizeof(arr[0]), compare_Stu_by_name);
    // 打印
    i = 0;
    for (i = 0; i < sz; i++) {
        printf("%s ", arr[i].name);
        printf("%d ", arr[i].age);
    }
    printf("\n");
}

int main() {
    test1();//比较int
    test2();//比较float
    test3();//比较结构体
    return 0;
}

qsort(参数1,参数2,参数3,参数4);
第一个参数:待排序数组的首元素地址
第二个参数:待排序数组的元素大小
第三个参数:待排序数组的每个元素的大小-单位是字节
第四个参数:是函数指针,比较两个元素的所用函数的地址-这个函数使用者自己实现。
函数指针的两个参数是:待比较的两个元素的地址。函数返回值为int类型。

我们发现库函数中的qsort函数可以实现除了整型之外的几乎所有类型的排序,我们的冒牌排序是不是也可以设计成这样呢?

首先我们来考虑函数的参数。1.需要知道首元素的地址(从哪里开始的),2.整个数组的大小(来判断需要几轮,每轮运算多少。)3.运算的是什么类型的,但是类型是不能传参的,所以我们可以传这个类型的大小(宽度)。4.两个值进行比较的函数地址。

主要功能实现

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++;
    }
}
//这里的冒泡排序是优化后的冒泡排序
// (*compare)(void* elem1,void* elem2)函数指针。用来比较两个数的参数。
void bubble_sort(void *base, int sz, int width, int (*compare)(const void *elem1, const void *elem2)) {
    //大 轮
    for (int i = 0; i < sz - 1; i++) {
        //定义标记
        int isSort = 1;
        //每一次的冒泡
        for (int j = 0; j < sz - i - 1; j++) {
            // 两个元素的比较
            //如果是结构体我们没有通用的方法。那么怎么办????
            //我们可以学qsort让用户自己来写比较的方法。
            if (compare((char *) base + j * width, (char *) base + (j + 1) * width) > 0) {
                // 交换
                Swap((char *) base + j * width, (char *) base + (j + 1) * width, width);
                isSort = 0;
            }
        }
        if (isSort == 1) break;        //最后的判断
    }
}

解析:整体使用的是冒泡排序的思想。难点:本人认为共有两个难点:1.在冒泡排序进行两个元素判断的时候,因为传进来的是void类型的指针是不能直接使用的。但是我们有被比较变量的宽度,通过char指针控制步长可以精确匹配到首地址然后进行量元素的比较。(在这里本来有个疑问,访问到变量的首地址,此时还是char怎么可以得到变量的全部部分呢?答案是这里是个回调函数,回调到用户编写的函数后又进行了一次强制类型转化,这时我们就访问到了变量的全部。)比较完之后就要进行两值的交换。2.两值的交换就是第二个问题了:步长就相当于两个变量的间隔,只要通过这个间隔从左开始,向右逐个交换。就可以将两个变量所在的地址中的内容全部交换。(下面是我的图解)。

image-20220111182358747

完整代码展示

#include <string.h>

void bubble_sort(void *base, int sz, int width, int (*compare)(const void *elem1, const void *elem2));

typedef struct Stu {
    char name[20];
    int age;
} Stu;

int compare_int(const void *elem1, const void *elem2) {
    return *(int *) elem1 - *(int *) elem2;
}

void test1() {
    int arr[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    //compare_int()要求这个函数的返回值是a大返回>0,a小返回<0,一样大返回0
    bubble_sort(arr, sz, sizeof(arr[0]), compare_int);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

//qsort要求这个函数的返回值为int
int compare_float(const void *elem1, const void *elem2) {
    return (int) (*(float *) elem1 - *(float *) elem2);
}

void test2() {
    float arr[] = {9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    bubble_sort(arr, sz, sizeof(arr[0]), compare_float);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%f ", arr[i]);
    }
    printf("\n");
}

//通过年龄排序
int compare_Stu_by_age(const void *elem1, const void *elem2) {
    //(Stu*)elem1强制类型转化。
    return (int) (((Stu *) elem1)->age - ((Stu *) elem2)->age);
}

//通过名字排序
int compare_Stu_by_name(const void *elem1, const void *elem2) {
    // 比较字符串需要使用strcmp
    return strcmp(((Stu *) elem1)->name, ((Stu *) elem2)->name);
}

void test3() {
    Stu arr[3] = {{"zhangsan", 20},
                  {"lisi",     30},
                  {"wangwu",   18}};
    int sz = sizeof(arr) / sizeof(arr[0]);
    // 通过年龄排序
    bubble_sort(arr, sz, sizeof(arr[0]), compare_Stu_by_age);
    // 打印
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%s ", arr[i].name);
        printf("%d ", arr[i].age);
    }
    printf("\n");
    // 通过名字排序
    bubble_sort(arr, sz, sizeof(arr[0]), compare_Stu_by_name);
    // 打印
    i = 0;
    for (i = 0; i < sz; i++) {
        printf("%s ", arr[i].name);
        printf("%d ", arr[i].age);
    }
    printf("\n");

}


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

//这里的冒泡排序是优化后的冒泡排序
// (*compare)(void* elem1,void* elem2)函数指针。用来比较两个数的参数。
void bubble_sort(void *base, int sz, int width, int (*compare)(const void *elem1, const void *elem2)) {
    //大 轮
    for (int i = 0; i < sz - 1; i++) {
        //定义标记
        int isSort = 1;
        //每一次的冒泡
        for (int j = 0; j < sz - i - 1; j++) {
            // 两个元素的比较
            //如果是结构体我们没有通用的方法。那么怎么办????
            //我们可以学qsort让用户自己来写比较的方法。
            if (compare((char *) base + j * width, (char *) base + (j + 1) * width) > 0) {
                // 交换
                Swap((char *) base + j * width, (char *) base + (j + 1) * width, width);
                isSort = 0;
            }
        }
        if (isSort == 1) break;        //最后的判断
    }
}

int main() {
    test1();//比较int
    test2();//比较float
    test3();//比较结构体
    return 0;
}

指针和数组面试题解析

  1. 一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
//sizeof(数组名),此时的数组名代表的是整个数组。计算的是数组总大小  应该输出4*4 = 16字节(之后省略)
printf("%d\n",sizeof(a+0));
//a+0代表的是首元素地址+0  计算的是首元素地址的大小。32位平台输出4 64位平台输出8
printf("%d\n",sizeof(*a));
//首先解引用,a代表的是首元素地址,*a就是首元素。 计算的是首元素的大小  int型大小为4。
printf("%d\n",sizeof(a+1));
//同sizeof(a+0)   32位平台输出4 64位平台输出8
printf("%d\n",sizeof(a[1]));
//a[1]代表的是第二个元素,计算的是第二个元素的大小。  int型为4。
printf("%d\n",sizeof(&a));
//&a是取出数组的地址。地址的大小还是32位平台输出4 64位平台输出8。
printf("%d\n",sizeof(*&a));
//*&a对数组地址进行解引用,就是整个数组。计算的就是整个数组的大小。应该输出4*4 = 16
//理解二:*&a中*&两个操作符作用相互抵消,最后就相当于sizeof(a)。
printf("%d\n",sizeof(&a+1));
//&a数组的地址(&a+1)是整个数组地址的下一个地址,只要是地址,大小就是32位平台4, 64位平台8
printf("%d\n",sizeof(&a[0]));
//&a[0]取第一个元素的地址。只要是地址,大小就是32位平台4, 64位平台8
printf("%d\n",sizeof(&a[0]+1));
//&a[0]第一个元素的地址,+1就是第二个元素的地址。只要是地址,大小就是32位平台4, 64位平台8
  1. 字符数组

(1)直接在数组中初始化存放

char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
//sizeof(数组名),此时的数组名代表的是整个数组。计算的是数组总大小  应该输出1*6 = 6
printf("%d\n", sizeof(arr+0));
//arr+0代表的是首元素地址+0  计算的是首元素地址的大小。32位平台输出4 64位平台输出8
printf("%d\n", sizeof(*arr));
//首先解引用,arr代表的是首元素地址,*arr就是首元素。 计算的是首元素的大小  char型大小为1
printf("%d\n", sizeof(arr[1]));
//第二个元素的大小。char型大小1.
printf("%d\n", sizeof(&arr));
//&arr数组存放的地址,地址,32位平台4,64位平台8。
printf("%d\n", sizeof(&arr+1));
//存放整个arr数组地址之后的下一个地址。地址,32位平台4,64位平台8。
printf("%d\n", sizeof(&arr[0]+1));
//第二个元素的地址。地址,32位平台4,64位平台8。


printf("%d\n", strlen(arr));
//遇到'\0'结束计算。但是使用数组初始化存放的时候,应该是随机值。
printf("%d\n", strlen(arr+0));
//strlen(arr)和strlen(arr+0)其实是完全一样的。从首元素地址开始往后找'\0'。
printf("%d\n", strlen(*arr));
//strlen()参数应该是地址。*arr是首元素,是元素。元素a转化为ASCII码97,然后去访问97这个地址。
//程序可能直接崩溃。
printf("%d\n", strlen(arr[1]));
//同上
printf("%d\n", strlen(&arr));
//整个元素的地址,就是首元素的地址,向后寻找'\0',是个随机值。
printf("%d\n", strlen(&arr+1));
//从整个数组结束后的下一个地址开始找'\0'。随机值
printf("%d\n", strlen(&arr[0]+1));
//从第二个元素开始向后寻找'\0'。随机值。
//但是上面的随机值之间是有联系的。 
//strlen(arr)=strlen(arr+0)=strlen(&arr)= strlen(&arr+1)+6 = strlen(&arr[0]+1)+1

(2)使用字符串进行初始化

char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
//实际存储的时候在最后还有'\0'所以结果:7*1 = 7
printf("%d\n", sizeof(arr+0));
//首元素地址+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));
//仍然是地址


printf("%d\n", strlen(arr));
//6
printf("%d\n", strlen(arr+0));
//6
printf("%d\n", strlen(*arr));
//首元素访问失败,访问第一个值的ASCII码对应的地址。非法访问内存,程序可能直接错误。
printf("%d\n", strlen(arr[1]));
//同上理
printf("%d\n", strlen(&arr));
//数组的地址,仍旧是6。这里会有一个警告,数组的地址 char(*p)[7] = &arr,&arr的类型是char(*)[7]
//但是因为传过来的是一个地址所以程序可以运行。
printf("%d\n", strlen(&arr+1));
//这个数组结束之后的下一个数组的开始地址。会有警告原因同上。结果是随机值。
printf("%d\n", strlen(&arr[0]+1));
//该数组的第二个元素到'\0'的长度。6-1 = 5
  1. 常量字符串地址传递给指针。
char *p = "abcdef";
printf("%d\n", sizeof(p));
//指针的大小,地址的大小 4/8
printf("%d\n", sizeof(p+1));
//得到的是字符b的地址,仍旧是地址 4/8
printf("%d\n", sizeof(*p));
//解引用地址,访问到的是字符char的大小为1
printf("%d\n", sizeof(p[0]));
//int arr[10],arr[0]==*(arr+0),p[0]==*(p+0)。
//p[0]访问到的就是a,大小为1
printf("%d\n", sizeof(&p));
//地址4/8
printf("%d\n", sizeof(&p+1));
//地址4/8
printf("%d\n", sizeof(&p[0]+1));
//取到a的地址然后加一 得到的其实就是b的地址。地址4/8


printf("%d\n", strlen(p));
//传进来的是地址所以可以运算,从第一个元素到'\0'的长度6
printf("%d\n", strlen(p+1));
//p是a的地址,p+1是b的地址。到'\0'的长度5
printf("%d\n", strlen(*p));
//*p访问第一个值的ASCII码对应的地址。非法访问内存,程序可能直接错误。
printf("%d\n", strlen(p[0]));
//同上
printf("%d\n", strlen(&p));
//p是一个指针,内容是地址,&p找到的是这个内容的地址。不是这个内容指向的地址。
//但是因为求的是长度,得看在内容中有没有一组结果刚好是0,可能会结束。总体而言还是随机值。
//这里注意的是它会读取内存内容。
printf("%d\n", strlen(&p+1));
//p的地址的下一个地址,原理同上。随机值
printf("%d\n", strlen(&p[0]+1));
//先p[0]就是a,+1就是b长度为6-1=5
  1. 二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));
//整个数组的大小3*4*4=48
printf("%d\n",sizeof(a[0][0]));
//第一行第一个元素大小int 4
printf("%d\n",sizeof(a[0]));
//第一行的数组的大小4*4=16
//a[0]相当于第一行作为一维数组的数组名,sizeof(arr[0])把数组名单独放在sizeof()内,计算的是第一行的大小
printf("%d\n",sizeof(a[0]+1));
//a[0]相当于第一行的数组名,是第一行首元素的地址,a[0]+1是第一行第二个元素的地址。地址:4/8
printf("%d\n",sizeof(*(a[0]+1)));
//根据上个解析可知,a[0]+1是第一行第二个元素的地址,解引用就是这个位置的元素,大小int为 4
printf("%d\n",sizeof(a+1));
//a是二维数组的数组名,没有sizeof(a),也没有&(a),所以a是首元素地址。而把二维数组看成一维数组时,二维数组的首元素是他的第一行,a就是第一行(首元素)的地址。a+1就是第二行的地址。大小为4/8
printf("%d\n",sizeof(*(a+1)));
//sizeof(a[1])计算第二行的大小,单位是字节  4*4 = 16
printf("%d\n",sizeof(&a[0]+1));
//第二行的地址,因为&a[0]取到的是第一行的地址。地址大小4
printf("%d\n",sizeof(*(&a[0]+1)));
//计算第二行的大小。第二行的地址解引用就是第二行。
printf("%d\n",sizeof(*a));
//a是首元素地址-第一行地址 *a就是第一行,这里就是计算第一行的大小。16
printf("%d\n",sizeof(a[3]));
//由于sizeof内部的是不参与计算的,只是看他的形式,就假设有第四行,大小为16

小结

1.sizeof(数组名),这里的数组名代表的是整个数组,计算的是数组的大小。
2.&数组名,这个数组名代表的是整个数组,取出的是整个数组的地址
3.除此之外所有的数组名都是首元素的地址。

指针笔试题

笔试题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,5。&a + 1是跳过整个数组的下一个地址。数组指针加一还是数组指针类型,不能存放进整型指针中,所以进行了强制类型转化。在输出的时候,输出的第一个值是数组首元素地址加一也就是第二个元素的地址,然后解引用得到的就是第二个元素,所以第一个输出的就是2;ptr是跳过整个数组的下一个地址,且ptr是int类型的指针,那么指针减一就是他的上一个地址,所以指向的应该是数组a的最后一个元素,所以第二个输出的是5

笔试题2

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

解析:0x100014 0x100001 0x100004。首先p是什么,p是结构体指针;0x1就是十进制的1。第一个打印中要跳过这个结构体的下一个地址是什么?0x100000(16)+20(10)=0x100014(16)。第二个打印,先将p转换成无符号长整型然后加一。按照地址打印输出:0x100000(16)+1=0x100001(16)。第三个打印,先将p转换成无符号的int型指针,加一就是跳过一个指针的大小就是0x100000(16)+4(10)=0x100004(16)

笔试题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,2000000。首先解释ptr1[-1],同笔试题1中的内容ptr1是跳过数组后的第一个地址,ptr1[-1]就是这个地址的上一个int。那就是a[3]。对于第二个我们直接图解(黄色是第一步,蓝色第二步,红色第三步)。因为我们的电脑是小端存储模式,所以存储数组具体元素的时候,按照如下的存储。(int *)((int)a + 1)。首先是将a的地址强制转化为int,然后对其加1,再转化为int型的指针,最后就是就是表示红色框框柱的部分。读取这部分,根据小端存储模式:0x02000000。输出时去掉高位的0。

image-20220112165219090

笔试题4:

int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    int *p;
    p = a[0];
    printf( "%d", p[0]);
	return 0;
}

解析:答案是1。(0, 1), (2, 3), (4, 5)先要想到这是逗号表达式,所以int a[3][2] = { (0, 1), (2, 3), (4, 5) };这句话相当于是int a[3][2]={1,3,5}。在内存中存储的时候,第一行第一个是1,第一行第二个是3,第二行第一个是5,其余的全都是0。a[0]是第一行的数组,输出的时候没有sizeof,没有&所以代表的是首元素地址。p[0]相当于*(p+0)就是1。

笔试题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;
}

解析:答案是FFFFFFFFFFFFFFFC,-4。int(*p)[4],是指针数组。将a[5][5]强制赋值到p中,结果如图下所示(数组分布)。图中量蓝点的地址进行作差,因为体重是低地址减去高地址,答案是-4。但是用%p的形式输出的时候应该是输出-4对应补码,所对应的16进制数FFFFFFFFFFFFFFFC(32位是FFFFFFFC) , -4。

image-20220112174851821

笔试题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;}

解析:答案是10,5。&arr是整个数组,经过计算后的ptr是整个数组后面的第一个地址。在输出的时候int指针-1然后取地址,得到的应该是aa数组的最后一个元素,10。aa + 1是第二行的地址,然后解引用得到的就是第二行,再强制类型转化为指针。输出的时候,ptr2 - 1,地址减一,减去的是int*,此时取到的就是第二行的前一行的最后一个的地址,再解引用,得到的就是该位置的元素为5。

补充:

int arr[] = {1,2,3,4};int* p = arr;

在这种情况下,*(p+2) <=>p[2]<=>*(arr+2)<=>arr[2]

笔试题7

int main()
{
	char* a[] = {"work","at","alibaba"};
	char**pa = a;
	pa++;
	printf("%s\n", *pa);
	return 0;
}

解析:答案是at。a是指针数组。a代表的是首元素的地址,就是第一个字符串"work"的地址。因为a本身就是一个指针,然后将这个指针放在二级指针pa中存储。pa++就相当于与a+1;就是第二个字符串的地址,解引用后就是第二个字符串也就是"at"。

笔试题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;
}

解析:答案是:POINT,ER,ST,。

注意:1.自增之后值就已经改变了。2.取的是字符串的地址还是字符的地址。虽然是一个地址,但是运算不同。

首先char *c[] = {“ENTER”,“NEW”,“POINT”,“FIRST”};是一个指针数组(数组中存放的指针。)。char**cp[] = {c+3,c+2,c+1,c};中c是首元素的地址。这样的话,cp[]就相当于是将数组c的逆序。cp是首元素地址存放到cpp二级指针变量中。

image-20220112201941100

第一个printf中先++cpp,这个指针要加一,那就是cp中的第二个元素的地址,第一次解引用得到是c中第三个元素的地址。第二次解引用就该字符串的第一个字符的地址,输出"%s"得到"POINT"。

image-20220112202453225

第二个printf中根据优先级,先++再解引用再–再解引用再加三。但是,这里在第一次printf的时候我们经cpp指到了cp中的第二个元素的地址。所以1.先++指向的是cp中的第三个元素的地址。再解引用得到的是c中的第二个元素的地址(不懂把第一个printf再看一遍),再–得到的是c中第一个元素的地址,再解引用就是c中第一个字符串的第一个字符的地址。最后加3得到的就是从从第四个字符开始的字符串"ER"。

image-20220112202949537

第三个printf中*cpp[-2]+3等价于*(*(cpp-2))+3,然后+3。首先计算之前强调cpp已经指到了cp的第三个元素,(cpp-2)指到了cp的第一个元素,解引用得到的是c的第四个元素字符串,再解引用,取到的是c中第四个字符串中的第一个字符的地址。输出"%s"得到"ST"。

image-20220112203841919

第四个printf中cpp[-1][-1]+1等价于*(*(cpp-1)-1)+1。因为我们知道cp在第二次printf的时候更新了位置(更新到了cp的第三个元素的位置),但是在第三个printf中并没有作出修改,所以(cpp-1)代表的是cp中第二个元素的位置,解引用访问到的是c中的第三个元素-字符串的地址。在这个基础上先-1取到的就是c中第二个字符串的地址,然后解引用操作,就是c中第二个字符串的第一个字符的地址,再+1,就是c中第二个字符串的第二个字符。输出"%s"得到"EW"。

image-20220112204246660

三、阶段习题详解

字符串逆序

写一个函数,可以逆序一个字符串的内容。

//逆序函数
#include <string.h>
#include <assert.h>
void reverse(char *str) {
    /*通过做指针和右指针互换。*/
    assert(str);
    int len = strlen(str);
    char *left = str;  //左指针是传进来的首元素的地址
    char *right = str + len - 1;//右指针是左指针地址加上字符串长度,然后减去1。
    while (left < right) {
        // 两值交换
        char tmp = *left;
        *left = *right;
        *right = tmp;
        left++;
        right--;
    }
}

int main() {
    char arr[200] = {0};
    scanf("%s", arr);
    //逆序函数
    reverse(arr);
    printf("%s", arr);
}

计算求和

求sum = a +aa+aaa+…的前n项的和,其中的a只是一个数字。

int main() {
    int a = 0;
    int n = 0;//前n项的n
    scanf("%d%d", &a, &n);
    int sum = 0;//求和
    int i = 0;//计数
    int res = 0;//计算出一个数。
    for (i = 0; i < n; ++i) {
        res = res * 10 + a;
        sum += res;
    }
    printf("%d\n", sum);
    return 0;
}

打印水仙花数

求出0~100000之间的所有水仙花数并输出。
“水仙花数”是指一个n位数,其各位数字的n次方之和恰好等于该数本身,如153 = 13+53+33,则153是一个水仙花数。

int main(){
    //从0开始到100000进行遍历
    for (int i = 0; i < 100000; i++) {
        //判断是不是水仙花数
        //1.统计位数。n位数。
        int n = 1;
        int tmp = i;
        while (tmp/=10){
            n++;
        }
        //2. 计算i的每一位的n次方之和sum
        tmp = i;
        int sum = 0;
        while (tmp){
            sum+=pow(tmp%10,n);
            tmp/=10;
        }
        //3.比较i和sum
        if(i == sum){
            printf("%d ",i);
        }
    }
    return 0;
}

打印菱形

用c语言在屏幕上打印以下的图案。

image-20220113101000682

int main(){
    int line = 7;
    //打印上半部分
    for (int i = 0; i < line; ++i) {
        //空格部分
        for (int j = 0; j < line-1-i; ++j) {
            printf(" ");
        }
        //*部分
        for (int j = 0; j < 2 * i + 1; ++j) {
            printf("*");
        }
        //换行
        printf("\n");
    }
    //下半部分
    for (int i = 0; i < line - 1; ++i) {
        //空格
        for (int j = 0; j < i+1; ++j) {
            printf(" ");
        }
        //"*"
        for (int j = 0; j < 2*(line-i-1)-1; ++j) {
            printf("*");
        }
        printf("\n");
    }
    return 0;
}

解析:这个题中的每次for循环的起始值为1或者0的时候,后面的条件判断也会有很大的变化。

喝汽水

喝汽水,1瓶汽水1元,2个空瓶可以换一瓶汽水,给20元,可以得多少汽水。

int main() {
    int num = 20;//可以买(换)到汽水数
    // scanf("%d",&num);//可以改为任意数,放开之前将num的初始值改为0。
    int sum = 0;//计算总数
    int bottle = 0;//空瓶数
    sum = num;//总数累计开始
    bottle = num;//喝完后当前的空瓶子
    while (bottle >= 2) {
        num = bottle / 2;//由空瓶换的汽水数
        //更新空瓶数
        if (bottle % 2 == 1) {
            bottle = 1;
        } else {
            bottle = 0;
        }
        //更新总数
        sum += num;
        // 喝完已有的更新空瓶数
        bottle += num;
        //更新现有可乐数
        num = 0;
    }
    printf("%d", sum);
    return 0;
}

解析:将当前有的可乐数,当前空瓶数,已经有过的可乐数按照实际情况走一遍流程然后思考终止条件。

调整奇偶数顺序

调整数组使奇数全部位于偶数的前面。

输入一个整形数组,实现一个函数,
来调整该数组中数字的顺序使得数组中所有的技术位于数组的前半部分,所有的偶数位于数组的后半部分。

void move(int *arr, int sz) {
    int *left = arr;
    int *right = arr + sz - 1;//最右边的元素
    while (1) {
        //左边找到偶数停止。
        while (*left % 2 == 1) {
            left++;
        }
        //右边找到奇数停止。
        while (*right % 2 == 0) {
            right--;
        }
        // 如果找到的不是一个位置上的值交换两个值。
        //这里的条件不能用不等于,在计算最后两个交换之后,会陷入死循环。
        if (left < right) {
            int tmp = *right;
            *right = *left;
            *left = tmp;
        } else break;
    }
}

void print(int *arr, int size) {
    for (int i = 0; i < size; ++i) {
        printf("%d ", arr[i]);
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int sz = sizeof(arr) / sizeof(arr[0]);
    move(arr, sz);
    print(arr, sz);
    return 0;
}

解析:整体的思路就是从左边开始找奇数从右端开始找偶数,找到就交换。这样就可以将偶数放在后面,奇数放在前面了

杨辉三角

在屏幕上打印杨辉三角

image-20220114180641001

int main() {
    //初始化
    int arr[10][10] = {0};
    //杨辉三角形的建立
    for (int i = 0; i < 10; ++i) {
        for (int j = 0; j < 10; ++j) {
            //将第一列赋值为1
            if (j == 0) {
                arr[i][j] = 1;
            }
            //将斜线赋值为1
            if (i == j) {
                arr[i][j] = 1;
            }
            //中间赋值
            if ((i > 0) && (j > 0)) {
                arr[i][j] = arr[i - 1][j - 1] + arr[i - 1][j];
            }
        }
    }
    //打印杨辉三角
    for (int i = 0; i < 10; ++i) {
        for (int j = 0; j < 10-1-i; ++j) {
            printf("  ");
        }
        for (int j = 0; j < 10; ++j) {
            (j > i) ? printf(" ") : printf("%3d ", arr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

解析:第一步,想到应该使用什么来存放数据(数组),左边的空白就是最后打印的时候输出相应大小的空格长度即可。找出数之间的规律,先初始化,无法计算的数据,然后计算可以计算的数据并填入数组。最后进行打印即可。

猜凶手

日本某地发生异常谋杀案,警察通过排查确定杀人凶手四个嫌疑犯的一个。已知三人说了真话,1人说了假话。
以下是供词:A说:不是我。B说:是C。C说:是D。D说:C在胡说。
现在根据这些信息,写一个程序来确定谁是凶手。

int main(){
    int killer = 0;
    for(killer = 'a';killer<='d';killer++){
        if((killer=='a')+(killer=='c')+(killer=='d')+(killer!='d')==3){
            printf("killer is %c",killer);
        }
    }
    return 0;
}

解析:将4个人说的话转化为逻辑判断语句。假设凶手是谁,然后判断此时四条语句的正确数。如果是3,那就说明我们要的答案是当前的条件上表示的谁是凶手。

猜名次

5位运动员参加了10米跳水比赛,有人让他们预测比赛结果:A选手说:B第二,我第三。B选手说:我第二,E第四。C选手说:我第一,D第二。D选手说:C最后,我第三。E选手说:我第四,A第一。比赛结束后每位选手都说对了一半,请编程确定比赛的名次。

int main() {
    int a = 0;
    int b = 0;
    int c = 0;
    int d = 0;
    int e = 0;
    for (a = 1; a <= 5; ++a) {
        for (b = 1; b <= 5; ++b) {
            for (c = 1; c <= 5; ++c) {
                for (d = 1; d <= 5; ++d) {
                    for (e = 1; e <= 5; ++e) {
                        if (((b == 2) + (a == 3) == 1) &&
                            ((b == 2) + (e == 4) == 1) &&
                            ((c == 1) + (d == 2) == 1) &&
                            ((c == 5) + (d == 3) == 1) &&
                            ((e == 4) + (a == 1) == 1) &&
                            (a * b * c * d * e == 120))
                            printf("a=%d,b=%d,c=%d,d=%d,e=%d", a, b, c, d, e);

                    }
                }
            }
        }
    }
    return 0;
}

解析:这个思路就相当于枚举,将所有的情况列举出来,筛选出满足条件的。

字符串左旋

实现一个函数,可以左旋转k个字符。

例如:
ABCD左旋转一个字符,BCDA
ABCD左旋转两个字符,CDAB

#include <string.h>
void left_move(char *arr,int k){
    int len = strlen(arr);
    //左旋k次
    for (int i = 0; i < k; ++i) {
        //左旋操作
        //1.将第一个元素取出放入临时变量
        char tmp = *arr;
        //2.将剩余的变量进行向左移动一个单位。
        for (int j = 0; j < len-1; ++j) {
            *(arr+j) = *(arr+j+1);
        }
        //3.将第一个元素放在最后
        *(arr+len-1) = tmp;
    }
}

int main(){
    char arr[] = "abcdef";
    int k = 2;
    left_move(arr,k);
    printf("%s",arr);
    return 0;
}

解析:这是暴力求解法。每一次向左移动一个字符。执行n次。
如果是右旋转,我们可以先将最右边的值放在临时变量中然后向右移动。

#include <string.h>
//逆序字符串的函数
void reverse(char* start, char *end) {
    while (start<end){
        char tmp = *start;
        *start = *end;
        *end = tmp;
        start++;
        end--;
    }
}

void left_move(char *arr, int k) {
    int len = strlen(arr);
    k = k % len;
    //1.逆序左边
    reverse(arr, arr + k - 1);
    //2.逆序右边
    reverse(arr + k, arr + len - 1);
    //3.逆序整体
    reverse(arr, arr + len - 1);
}

解析:将字符串进行三次翻转。因为旋转后的字符串在小范围内都是存在一定顺序的,然后,对于一段字符而言,经过两次就可以恢复顺序,所以有了总共翻转三次的思想。

image-20220115165558614

字符串旋转结果

写一个函数,判断一个字符串中是否为另一个字符串旋转后的字符串。
例如:给定s1=AABCD和s2=BCDAA,返回1

给定s1 = abcd 和 s2 = acbd,返回0。

#include <string.h>

//逆序字符串的函数
void reverse(char *start, char *end) {
    while (start < end) {
        char tmp = *start;
        *start = *end;
        *end = tmp;
        start++;
        end--;
    }
}

void left_move(char *arr, int k) {
    int len = strlen(arr);
    k = k % len;
    //1.逆序左边
    reverse(arr, arr + k - 1);
    //2.逆序右边
    reverse(arr + k, arr + len - 1);
    //3.逆序整体
    reverse(arr, arr + len - 1);
}

int is_move(char *s1, const char *s2) {
    int len = strlen(s1);
    for (int i = 0; i < len - 1; ++i) {
        left_move(s1, 1);
        int res = strcmp(s1, s2);
        if (res == 1) {
            return 1;
        }
    }
    return 0;
}

int main() {
    char arr1[] = "abcdef";
    char arr2[] = "cdefab";
    int res = is_move(arr1, arr2);
    if (res == 1) {
        printf("%s是%s旋转后的字符串。", arr2, arr1);
    } else printf("%s不是%s旋转后的字符串。", arr2, arr1);
}

解析:这题的主要功能是实现is_move函数,剩下的功能都和上一题字符串左旋相同。这个解法也是相当于将所有的旋转后的情况拿出来和给定字符串进行对比。其实还有其他的方法。

int is_move(char *s1, char *s2) {
    // 直接解决掉长度不符合的情况
    int len1 = strlen(s1);
    int len2 = strlen(s2);
    if(len1!=len2) return 0;
    //1.在str1字符串中追加一个str1字符串
    strncat(s1,s1, len1);//字符产拼接函数,不能使用strcat
    // 2.判断str2指向的字符串是否是str1指向的子串
    // strstr是找子串的
    char * res = strstr(s1,s2);
    if(res == NULL)return 0;
    else return 1;
}

int main() {
    char arr1[30] = "abcdef";
    char arr2[] = "cdefacb";
    int res = is_move(arr1, arr2);
    if (res == 1) {
        printf("%s是%s旋转后的字符串。", arr2, arr1);
    } else printf("%s不是%s旋转后的字符串。", arr2, arr1);
}

解析:在原字符串的最后追加一个自己。然后,在新生成的字符串中寻找另一个字符串是否为新生成的子串。

杨氏矩阵

有一个数字数组,矩阵的每行从左到右是递增的,矩阵从上到下是递增的,请编写程序在这样的矩阵中查找某个数字是否存在。

要求:时间复杂度是小于O(n)。

提示:一行中的最大值,或者一列中的最大值可以直接排除一行或者一列。当然最小值也是可以的。那我我们为了一下子可以排除掉一行我们可以取左下角或者右上角,因为这两个位置,同时具有一个最大值和一个最小值的特性。

image-20220115235339609

int FindNum(int arr[4][4], int k, int row, int col) {
    //首先定位到的是右上角的元素。
    int x = 0;
    int y = col - 1;
    while ((x >= 0 && x < row) && (y >= 0 && y < col)) {
        //相等的情况->找到了
        if (arr[x][y] == k) return 1;
            //大于的情况
        else if (arr[x][y] > k) y--;
            //小于的情况
        else if (arr[x][y] < k) x++;
    }
     // 没找到
    return 0;
}

//杨氏矩阵
int main() {
    int arr[4][4] = {{1, 2, 3,  4},
                     {4, 5, 6,  7},
                     {7, 8, 9,  10},
                     {8, 9, 12, 13}};
    int k = 8;//我们要找的值
    int row = sizeof(arr) / sizeof(arr[0]);//获取行数
    int col = sizeof(arr[0]) / sizeof(arr[0][0]);//获取列数
    int res = FindNum(arr, k, row, col);
    if (res == 1)printf("找到了\n");
    else printf("没有找到\n");
    return 0;
}

注:如果要在函数外部进行访问下标的时候,可以将x、y定义在函数之外,将其指针传进函数,然后在外部访问输出。(这就是基本功的练习了。)

四、字符函数和字符串函数

函数部分的解析来源于( C++资源网络)

  • 求字符串长度
    strlen
  • 长度不受限制的字符串函数
    strcpy
    strcat
    strcmp
  • 长度受限制的字符串函数介绍
    strncpy
    strncat
    strncmp
  • 字符串查找
    strstr
    strtok
  • 错误信息报告
    strerror
  • 字符操作
  • 内存函数操作
    memcpy
    memmove
    memset
    memcmp

C语言中的字符串问题

写代码时对字符和字符串的处理很频繁,但是C语言中本身没有字符串类型,字符串通常存储在常量字符串中或者字符数组中。常量字符串适用于那些对它不做修改的字符串函数。

函数介绍

1. strlen

1.1 功能:

Get string length获取字符串长度

返回C语言中字符串的长度。

1.2 库函数形式
size_t strlen ( const char * str );
1.3 注意事项:

字符串的长度由终止字符确定。字符串‎‎的长度与字符串开头和终止空字符之间的字符数一样长(不包括终止空字符本身)。

注: 1. 字符串以’\0’作为结束标志。 2. 函数的返回值为size_t,是无符号的。 size_t==unsigned int

1.4 实例
int main ()
{
  char szInput[256];
  printf ("Enter a sentence: ");
  gets (szInput);
  printf ("The sentence entered is %u characters long.\n",(unsigned)strlen(szInput));
  return 0;
}

2. strcpy

2.1 功能:

Copy block of memory复制字符串

所指向的 C 字符串复制到**目标**所指向的数组中,包括终止空字符(并在该点停止)。

2.2 库函数形式
char * strcpy ( char * destination, const char * source );
2.3 注意事项:

为避免溢出,目标所指向的数组的大小应足够长,以包含与源相同的 C 字符串(包括终止空字符),并且不应在内存中与 source 重叠。

注:1. 源字符串必须以’\0’结束。2. 结束标志’\0’也会被拷贝。 3. 目标空间必须足够大。 4. 目标空间必须可变。

2.4 实例
int main ()
{
  char str1[]="Sample string";
  char str2[40];
  char str3[40];
  strcpy (str2,str1);
  strcpy (str3,"copy successful");
  printf ("str1: %s\nstr2: %s\nstr3: %s\n",str1,str2,str3);
  return 0;
}

3. strcat

3.1 功能:

Concatenate strings连接字符串

将‎‎‎‎字符串的副本追加到‎‎目标‎‎字符串。‎‎目标‎‎中的终止空字符被‎‎源‎‎的第一个字符覆盖,并且在‎‎目标‎‎中由两者串联形成的新字符串的末尾包含一个空字符。‎

3.2 库函数形式
char * strcat ( char * destination, const char * source );
3.3 注意事项:

‎目的地‎‎和‎‎来源‎‎不得重叠。自己不能给自己追加。

注:1. 源数组必须以’\0’结束。2. 目标空间必须足够大,能容纳下源字符串的内容。 3. 目标空间必须是可修改的。**重点:**strcat不能自己给自己追加。

3.4 实例
int main ()
{
  char str[80];
  strcpy (str,"these ");
  strcat (str,"strings ");
  strcat (str,"are ");
  strcat (str,"concatenated.");
  puts (str);
  return 0;
}

4. strcmp

4.1 功能:

Compare two strings比较两个字符串

将 C 字符串‎str1‎‎与 C 字符串‎‎str2进行比较‎‎。

返回值表明
<0第一个不匹配的字符在ptr1中的值低于ptr2中的值
0两个字符中的内容相等
>0第一个不匹配的字符在ptr1中的值低于ptr2中的值
4.2 库函数形式
int strcmp ( const char * str1, const char * str2 );
4.3 注意事项:

此函数开始比较每个字符串的第一个字符。如果它们彼此相等,则继续使用以下对,直到字符不同或达到终止空字符。

4.4 实例
int main ()
{
  char key[] = "apple";
  char buffer[80];
  do {
     printf ("Guess my favorite fruit? ");
     fflush (stdout);
     scanf ("%79s",buffer);
  } while (strcmp (key,buffer) != 0);
  puts ("Correct answer!");
  return 0;
}

5. strncpy

5.1 功能:

Copy characters from string从字符串中复制字符

的前num字符复制到目标。如果在复制num个字符之前找到C 字符串(由空字符指示)的末尾,则目标将填充零,直到向其写入总共num个字符。

5.2 库函数形式
char * strncpy ( char * destination, const char * source, size_t num );
5.3 注意事项:

‎如果源的长度超过 num,则不会在目标末尾隐式追加空字符。
因此,在这种情况下,目标不应被视为以空值结尾的 C 字符串(这样读取它会溢出)。
目的地和来源不得重叠(重叠时,请参阅 memmove 以了解更安全的替代方案)。

5.4 实例
int main ()
{
  char str1[]= "To be or not to be";
  char str2[40];
  char str3[40];

  /* copy to sized buffer (overflow safe): */
  strncpy ( str2, str1, sizeof(str2) );

  /* partial copy (only 5 chars): */
  strncpy ( str3, str2, 5 );
  str3[5] = '\0';   /* null character manually added */

  puts (str1);
  puts (str2);
  puts (str3);

  return 0;
}

6. strncat

6.1 功能:

Append characters from string

的前num个字符追加到目标,加上一个终止空字符。

6.2 库函数形式
char * strncat ( char * destination, const char * source, size_t num );
6.3 注意事项:

如果源中 C 字符串的长度小于num,则仅复制直到终止空字符的内容。
如果source拷贝进去之后desination还有原本存在的字符会主动追加一个’\0’。
如果追加长度num大于strlen(source),则只追加source原本的字符串和一个’\0’

6.4 实例
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;
}

7. strncmp

7.1 功能:

Compare characters of two strings 比较两个字符串的字符

将 C 字符串str1最多 num个字符与 C 字符串str2的字符数进行比较。

7.2 库函数形式
int strncmp ( const char * str1, const char * str2, size_t num );
7.3 注意事项:

此函数开始比较每个字符串的第一个字符。如果它们彼此相等,则继续向后比较,直到字符不同,直到达到终止空字符,或者直到两个字符串中的num字符匹配,以先发生的情况为准。

7.4 实例
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;
}

8. strstr

8.1 功能:

**Locate substring ** 定位子字符串

返回一个指向str1中第一次出现的str2的指针,如果str2 不是 str1的一部分,则返回空指针。

8.2 库函数形式
const char * strstr ( const char * str1, const char * str2 );
char * strstr (char * str1, const char * str2 );
8.3 注意事项:

匹配过程不包括终止空字符,但碰到它就此停止。

8.4 实例
int main (){  
    char str[] ="This is a simple string";  
    char * pch;  pch = strstr (str,"simple");  
    if (pch != NULL)
        strncpy (pch,"sample",6);
    puts (str);
    return 0;
}

9. strtok

9.1 功能:

Split string into tokens 将字符串拆分为标记

对该函数的一系列调用将str拆分为标记,这些标记是由分隔符中的任何字符分隔的连续字符序列。

9.1 库函数形式
char * strtok ( char * str, const char * delimiters );
9.3 注意事项:

在第一次调用时,函数需要一个C字符串作为str的参数,str的第一个字符用作扫描标记的起始位置。在随后的调用中,函数需要一个空指针,并使用最后一个标记结束后的位置作为扫描的新起始位置。

要确定标记的开头和结尾,函数首先从起始位置扫描分隔符中未包含的第一个字符(即标记的开头)。然后从标记的这个开头开始扫描分隔符中包含的第一个字符,它将成为标记的结尾。如果找到终止的空字符,扫描也会停止。

令牌的这一端将自动替换为空字符,函数将返回令牌的开头。

在对strtok的调用中找到str的终止null字符后,所有后续对该函数的调用(以null指针作为第一个参数)都会返回null指针。

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

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

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

strtok函数的第一个参数为NULL,函数将在同一个字符串(我的理解是上次传递的字符串)中被保存的位置开始,查找下一个标记。

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

9.4 实例
int main ()
{
  char str[] ="- This, a sample string.";
  char * pch;
  printf ("Splitting string \"%s\" into tokens:\n",str);
  pch = strtok (str," ,.-");
  //因为每次只切割出一部分所以要使用while循环。
  while (pch != NULL)
  {
    printf ("%s\n",pch);
    pch = strtok (NULL, " ,.-");
  }
  return 0;
}

10. strerror

10.1 功能:

Get pointer to error message string获取指向错误消息字符串的指针

错误码是程序员知道的,但是要转化为用户能看懂的错误信息,就需要使用函数将其转化为错误信息。

解释errnum的值,生成一个字符串,其中包含一条描述错误条件的消息,就好像被库的函数设置为errno一样。

10.2 库函数形式
char * strerror ( int errnum );
10.3 注意事项:

字符串。对此函数的进一步调用可能会覆盖其内容(不需要特定的库实现来避免数据争用)。strerror 生成的错误字符串可能特定于每个系统和库实现。

10.4 实例
#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));
  else{
      printf("Win opening file!")
  }
  return 0;
}
//errno在头文件<errno.h>中
//errno是一个全局的错误码变量
//当C语言的库函数在执行过程中,发生错误,就会把对应的错误码,赋值到errno中

字符分类函数

函数使用需要的头文件

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

一定是字符转换,不是字符串转化。

tolower(c);//转小写字母
toupper(c);//转大写字母

使用案例

int main(){
    char str[] = "I Am A Good Student";
    int i = 0;
    while(str[i]){
        if(isupper(str[i])){
            str[i] = tolower(str[i]);
        }
        i++;
    }
    printf("%s\n",str);
    return 0;
}

字符串函数主要功能是拷贝字符串,而拷贝数字的时候可能会出现提前结束的情况(1在内存中01 00 00 00,在拷贝的时候00等价于字符串结束的标志)。那么我们想要拷贝除了字符串,像数字这种会随时出现字符串终止标志的应该怎么处理呢?我们就出现了基于内存的操作。

11. memcpy

11.1 功能:

Copy block of memory 复制内存块

num字节的值从指向的位置直接复制到目标所指向的内存块。源指针和目标指针所指向的对象的基础类型与此函数无关。

11.2 库函数形式
void * memcpy ( void * destination, const void * source, size_t num );
11.3 注意事项:

结果是数据的二进制副本。该函数不检查中是否有任何终止空字符 - 它始终精确地复制数字字节。

为避免溢出,目标参数和源参数所指向的数组的大小应至少为 num 字节,并且不应重叠(对于重叠的内存块,memmove 是一种更安全的方法)。

11.4 实例

简单理解

int main(){
    int arr[] = {1,2,3,4,5};
    int arr2[5] = {0};
    memcpy(arr2,arr1,sizeof(arr1));
    return 0;
}

进阶理解

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

12. memmove

12.1 功能:

Move block of memory 移动内存块

num字节的值从所指向的位置复制到目标所指向的内存块。

注:处理重叠拷贝的情况

12.2 库函数形式
void * memmove ( void * destination, const void * source, size_t num );
12.3 注意事项:

复制就像使用中间缓冲区一样进行,允许目标重叠。
源指针和目标指针所指向的对象的基础类型与此函数无关;结果是数据的二进制副本。该函数不检查中是否有任何终止空字符 - 它始终精确地复制数字字节。

为避免溢出,目标参数和源参数所指向的数组的大小应至少为 num 字节。

12.4 实例
int main ()
{
  char str[] = "memmove can be very useful......";
  memmove (str+20,str+15,11);
  puts (str);
  return 0;
}

C语言标准规定

memcpy 只要处理,不重叠的内存拷贝就可以
memmove处理重叠内存的拷贝。

但是现在好多的编译器memcpy都可以进行重叠拷贝

13. memcmp

13.1 功能:

比较两个字符串

ptr1指向的内存块的第一个字节数与ptr2指向的第一个数字字节进行比较

返回值表明
<0在两个内存块中不匹配的第一个字节在ptr1中的值低于ptr2中的值(如果计算为无符号字符值)
0两个内存块的内容相等
>0两个内存块中不匹配的第一个字节在ptr1中的值大于ptr2中的值(如果计算为无符号 char值)
13.2 库函数形式
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
13.3 注意事项:

ptr1指向的内存块的第一个字节数与ptr2指向的第一个数字字节进行比较,如果它们都匹配,则返回零,或者一个值不同于零,表示如果它们不匹配,则表示哪个值更大。请注意,与strcmp不同,该函数在找到空字符后不会停止比较。

13.4 实例
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;
}

14.memset

14.1功能

Fill block of memory填充内存块

14.2库函数形式
void * memset ( void * ptr, int value, size_t num );
14.3注意事项

将ptr指向的内存块 的前num字节设置为指定值(解释为unsigned char)。

ptr指向要填充的内存块的指针。num指的是字节数

14.4使用实例
#include <stdio.h>
#include <string.h>

int main ()
{
  char str[] = "almost every programmer should know memset!";
  memset (str,'-',6);
  puts (str);
  return 0;
}

函数的模拟实现

strlen的模拟实现

//计数器方式
int my_strlen(const char *str) {
    int count = 0;
    while (*str) {
        count++;
        str++;
    }
    return count;
}
//不能创建临时变量计数器 --递归方式实现
int my_strlen(const char *str) {
    if (*str == '\0')
        return 0;
    else
        return 1 + my_strlen(str + 1);
}
//指针的方式实现
int my_strlen(char *s) {
    char *p = s;
    while (*p != '\0')
        p++;
    return p - s;
}

strcpy的模拟实现

//1.参数顺序//2.函数的功能,停止条件//3.assert//4.const修饰指针//5.函数返回值char *my_strcpy(char *dest, const char *src) {    //保留原来的地址。    char *ret = dest;    //保证输入的是有效的指针。    assert(dest != NULL);    assert(src != NULL);	//赋值过程    while ((*dest++ = *src++)) { ;    }    return ret;}

strcat的模拟实现

char *my_strcat(char *dest, const char *src) {
    //备份初始位置
    char *ret = dest;
    //保证指针的有效性
    assert(dest != NULL);
    assert(src != NULL);
    //将dest的指向位置改为,当前字符串的'\0'位置,即拼接开始的地方
    while (*dest) {
        dest++;
    }
    //开始拼接操作 要将最后'\0'也拼接上
    while ((*dest++ = *src++)) ;
    return ret;
}

strstr的模拟实现

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

strcmp的模拟实现

//只返回0,1,-1型
int my_strcmp(const char *word1,const char *word2) {
    //保证指针的有效性
    assert(word1!=NULL);
    assert(word2!=NULL);
   //相等的情况
    while (*word1==*word2){
        if(*word2=='\0'){
            return 0;
        }
        word1++;
        word2++;
    }
    //不相等的情况
    if (*word1 > *word2){
        return 1 ;
    } else{
        return -1;
    }
}
//返回动态值的方法
int my_strcmp(const char *word1,const char *word2){
    assert(word1!=NULL);
    assert(word2!=NULL);
    while (*word1==*word2){
        if(*word2=='\0'){
            return 0;
        }
        word1++;
        word2++;
    }
    return (*word1-*word2);
}

strncpy的模拟实现

char *my_strncpy(char *dest, const char *src, unsigned int count) {
    //保证指针的有效性
    assert(dest != NULL);
    assert(src != NULL);
    //备份返回值
    char *res = dest;
    //复制字符串
    while (count && (*src)) {
        *dest = *src;
        src++;
        dest++;
        count--;
    }
    //超出的部分补位'\0'
    if (count) {
        while (count) {
            count--;
            *dest= '\0';
            dest++ ;
        }
    }
    return res;
}

strncat的模拟实现

char* my_strncat(char* front,const char * back,unsigned int count){
    //保证指针的有效性
    assert(front != NULL);
    assert(back != NULL);
    //备份返回值
    char * res = front;
    //找到被拼接的末尾位置的结束标志
    while(*front++);
    front--;//多计算了一次
    //拷贝
    while(count--){
        if (!(*front++ = * *back++)){
            return r;
        }
    }
    *front = '\0';
    return (res);
    
}

strstr的模拟实现

char *my_strstr(const char *str1, const char *str2) {
    assert(str1 != NULL);
    assert(str2 != NULL);
    //拷贝
    char *cp = (char *) str1;
    //运算指针
    char *s1 = NULL;
    char *s2 = NULL;
    //如果是空指针的话直接返回
    if (!*str2)
        return ((char *) str1);
    //查找子集的过程
    while (*cp) {
        s1 = cp;//str1的运算指针
        s2 = (char *) str2;//str2的运算指针
        while ((*str1 != '\0') && (*str2 != '\0') && (*str1 == *str2)) {
            s1++;
            s2++;
        }
        // 找到子串
        if (!*s2)
            return cp;
        cp++;
    }
    // 找不到子串
    return (NULL);
}

注:NULL是空指针的意思,Null是’\0’。

memcpy的模拟实现

不能实现重叠拷贝

//使用无类型指针接受参数
void *memcpy(void *dst, const void *src, size_t count) {
    void *res = dst;
    assert(dst);
    assert(src);
    //每次拷贝一个字节
    while (count--) {
        *(char *) dst = *(char *) src;
        dst = (char *) dst + 1;//地址加一,还可以写成++(char*)dst
        src = (char *) src + 1;//地址加一,还可以写成++(char*)src
    }
    return res;
}

memmove的模拟实现

可以实现重叠拷贝。

void *my_memmove(void *dest, const void *src, size_t count) {
    //count是字节的个数
    //有效性
    assert(dest != NULL);
    assert(src != NULL);
    //备份返回值,因为不知道用于何处返回的是void类型的指针。
    void *res = dest;
    //判断dest和src的位置关系并决定拷贝方向
    //从前往后  重叠时dest在src的左边,和不重叠的情况
    if (dest < src || (char *) dest >= ((char *) src + count)) {
        while (count--) {
            *(char *) dest = *(char *) src;//一个字节一个字节的拷贝
            dest = (char *) dest + 1;//这里不能使用内置加加的方法
            src = (char *) src + 1;
        }
    }
        // 从后往前
    else {
        dest = (char *) dest + count - 1;//更改开始位置
        src = (char *) src + count - 1;//更改开始位置
        while (count--) {
            *(char *) dest = *(char *) src;
            dest = (char *) dest - 1;
            src = (char *) src - 1;
        }
    }
    return res;
}

注:void*是无具体类型的指针。这样的函数是需要返回值的。

五、自定义类型:结构体,枚举,联合

大纲

  1. 结构体

    1. 结构体类型的声明
      1. 结构的自引用
      2. 结构体变量的定义和初始化
      3. 结构体内存对齐
      4. 结构体传参
      5. 结构体实现位段(位段的填充、可移植性)
  2. 枚举

    1. 枚举的定义
    2. 枚举的优点
    3. 枚举的使用
  3. 联合

    1. 联合类型的定义
    2. 联合的特点
    3. 联合大小的计算
讲在前面
	我们学过的char、short、int、long、float、double是叫C语言中的内置类型是C语言自己的函数类型;然而还有一些复杂对象我们无法描述,比如人,书籍等事物。这时我们就出现了结构体、枚举、联合。这些被叫做自定义类型。

结构体

结构体类型的声明

结构的基础知识

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

结构的声明
struct tag{
    member_list;//成员列表
}variable_list;//变量列表

实例:

//声明一个结构体类型
//声明一个学生类型,想通过学生类型来创建学生变量(对象)
//描述学生:名字 电话 性别 年龄
struct Stu{
    char name[20];
    int age;
    char sex[5];
    char tele[20];
};//这个分号不能丢

image-20220126000454664

创建结构体变量

image-20220126001050062

特殊的一种声明

在声明的时候可以完全声明也可以不完全声明。

//匿名结构体声明
struct{
    int a;
    int b;
    float c;
}x;
struct{
	int a;
    int b;
    float c;
}a[20],*p;//匿名结构体指针

在使用匿名结构体声明的时候,必须创建结构体变量。否则无法使用。

提问:

//在上面的代码中两种结构体的形式是一样的。那么下面的语句合法吗?
p = &x;

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

结构体的自引用

方式一:

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

方式二:

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

注意:在方式一种成员变量处的struct Node应该是指针类型。
方式二中的结构体名称的两处都得写,并且在成员变量处必须是带有struct的名称。

两种错误的方式

struct Node{
	int data;
	struct Node next;
};
//不可行原因,sizeof(struct Node)无法计算
//无限套娃
typedef struct{
	int data;
	Node* next; 
}Node;
//不可行原因:这个结构是匿名结构体声明,不存在Node这种类型。

在结构体创建的时候成员变量不能是自己。但是可以是自己的指针。(指针有大小4/8)。

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

现在有了结构体类型,怎么定义变量呢?

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 Point{
    int x;
    int y;
};
struct Node{
    int data;
    struct Point p;
    struct Node* next;
};
struct Node n1 = {15,{2,3},NULL};//结构体嵌套初始化

结构体内存对齐

在之前学习的时候就一直存在一个关于结构体的疑问:结构体大小的计算。

先试着猜一下答案:

struct S1 {
    char c1;
    int a;
    char c2;
};

struct S2 {
    char c1;
    char c2;
    int a;
};

int main() {
    struct S1 s1 = {0};
    printf("%llu\n", sizeof(s1));
    struct S2 s2 = {0};
    printf("%llu\n", sizeof s2);
    return 0;
}

结果:image-20220126121220785

具体怎么计算呢?

首先要掌握对齐规则:

# 对齐数=编译器默认的一个对齐数与该成员大小的较小值。# vs默认对齐数是8,gcc没有默认对齐数,对齐数是成员大小。1. 第一个成员在结构体变量偏移量为0的地址处存放2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处存放3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
image-20220126124945144

如果出现结构体嵌套的情况呢?

//以下结构体的大小是?
struct S2 {
    char c1;
    char c2;
    int a;
};
struct S3{
    char c1;
    struct S2 s2;
    double d;
};
int main() {
    struct S3 s3 = {0};
    printf("%llu\n", sizeof(s3));
    return 0;
}

image-20220126162156738

为什么要存在内存对齐呢?

大部分资料说:

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

总结:结构体内存对齐就是用空间来换时间的思想实现的。

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

解决方式:让占用空间小的成员集中在一起。

举个例子:
在上面的结构体定义的时候都是两个char和一个int但是定义顺序不同,得到的结构体的大小就不相同。

修改默认对齐数

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

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

小结:在对齐方式不合适的时候我们可以修改默认的对齐数。

计算某个变量的偏移量

使用<stddef.h>中的offsetof宏。

该函数形式的宏返回数据结构或联合类型中成员成员的字节偏移值。 返回的值是size_t类型的无符号整数值,其中包含指定成员与其结构开头之间的字节数。

官网实例:

/* offsetof example */
#include <stdio.h>      /* printf */
#include <stddef.h>     /* offsetof */

struct foo {
      char a;
      char b[10];
      char c;
};

int main ()
{
      printf ("offsetof(struct foo,a) is %d\n",(int)offsetof(struct foo,a));
      printf ("offsetof(struct foo,b) is %d\n",(int)offsetof(struct foo,b));
      printf ("offsetof(struct foo,c) is %d\n",(int)offsetof(struct foo,c));
      return 0;
}

结构体传参

先看一下代码

struct S {
    int a;
    char b;
    double c;
};
// 直接初始化结构体变量
void Init(struct S ps) {
    ps.a = 100;
    ps.b = 'a';
    ps.c = 3.1415;
}
// 使用地址初始化结构体变量
void Init2(struct S *ps) {
    ps->a = 101;
    ps->b = 'b';
    ps ->c = 1.6782;
}

int main() {
    struct S s = {0};
    Init(s); // 传结构体
    Init2(&s);// 传结构体地址
    return 0;
}

初始化时使用的应该是Init还是Init2函数呢?

答: 应该使用的是Init2函数。

原因:使用Init达不到修改参数的目的。打印的时候,也是传地址,因为传参的时候,参数是需要压栈的,会有时间空间上的系统开销(会临时拷贝)。

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

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

结构体实现位段

什么是位段

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

  1. 位段的成员必须是int、unsigned int 或者 signed int。(不仅限,只要是整型就可以了)
  2. 位段的成员后边有一个冒号和一个数字。

举例:

struct A {
    int a: 2;
    int b: 5;
    int c: 10;
    int d: 30;
};

int main() {
    struct A a;
    printf("%d \n", (int) sizeof(a));//8个字节
    return 0;
}

位段中的位是二进制位。

如果在使用int类型的时候,你使用的仅仅是0、1、2、3四个数字,你就没有必要使用int这么大的空间,这时只需要两个bite位就可以表示了。以此类推,a、b、c、d就可以自己定义需要的bite位的大小。

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

举个例子:

struct A {
    int a: 2;
    int b: 5;
    int c: 10;
    int d: 30;
};

图解:

image-20220208120732227

没有使用的空间就直接浪费掉了。但是比起来直接使用4个int的空间已经节省了很多了。

注意位段后面的数字是不能大于前面类型的字节大小的。

实例:

struct B{
    char a:3;
    char b:4;
    char c:5;
    char d:4;
};
int main() {
    struct B b = {0};
    b.a = 10;
    b.b = 20;
    b.c = 3;
    b.d = 4;
    return 0;
}

存储结果:image-20220208174607889

因为已经锁定了可以占用的位数,固定的位数是代表存储时会有一定的限度值。如果超出这个限定范围就像之前的存储情况一样发生截断现象,从而不是你自己想要存入的值。

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

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

位段的应用

网络传输的时候,数据前面的首部部分就是用位段来定义的。如过不使用位段的话,就会产生大量的空间浪费。

image-20220208202223282

枚举

枚举是什么:枚举就是我们经常说的列举。

把可能出现的值一一列举出来。

枚举的定义及使用

//定义一个星期
enum Day{
    //内容是可能的取值-常量。
    Mon,
    Tues,
    Web,
    Thur,
    Fri,
    Sat,
    Sun
};
//定义一个性别枚举类型
enum Sex{
    //枚举常量
    Male,//0
    Female,//1
    secret//2
};
enum Color{
  	//枚举常量第一个值默认为0,但是是可以修改的。
    //枚举常量赋予初值
    RED = 2,
    GREEN = 4,
    //如果不赋予初始值,则该枚举常量的默认值为上一个枚举变量的值+1。
    BLUE
};
int main() {
    enum Sex s = Male;
    s = Female;
    enum Day d = Mon;
    return 0;
}

在上面的代码中enum Day,enum Sex,enum Color都是枚举类型。
在大括号中包括的内容都是枚举类型的可能取值,也叫枚举常量。

可能的取值都是从0开始的,一次递增1,在定义的时候也可以赋予初值。

枚举的优点

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

枚举使用的时候有以下的有点

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,相对而言更加的严谨。
  3. 防止命名污染(封装)。
    在定义的时候多种类型的都写成#define的形式可能会出现枚举常量的命名相同的问题。
  4. 便于调试
    define是完全替换的。所以在调试的时候没有枚举方便。
  5. 使用方便,一次可以定义多个常量。

枚举的使用

enum Color
{
    RED = 1,
    GREEN = 2,
    BLUE = 3
};
int main()
{
    //只能拿枚举常量给枚举变量赋值,才不会出现类型差异
    enum Color clr = GREEN;
    clr = BLUE;
    clr = 1;//???这样的方式是否可行呢?
    return 0;
}

解答:右边时int类型的数据左边是枚举类型,是不能进行赋值的。

枚举变量的大小就是一个整形的大小。

联合(共用体)

联合类型的定义

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

举例:

union Un {
    char c;
    int i;
};

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

联合的地址

    printf("%p\n", &un);
    printf("%p\n", &un.c);
    printf("%p\n", &un.i);

结果是一个地址。

联合的特点

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

联合体使用的时候,就上面的例子而言c和i是不能同时使用的。

面试题:

判断计算机大小端存储。

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

解析:利用联合体的特性。
将1的值存入一个联合体的int中char用来使用,如果是小端存储char类型变量的值为1,大端存储的情况是char类型变量的值为0。

联合大小的计算

联合的大小至少是最大成员的大小。

当最大的成员大小不是最大对齐数的整数倍的时候,就于要对齐到最大对齐数的整数倍。

union Un {
    int a ;
    char arr[11];
};

int main() {
    union Un u;
    printf("%d\n", (int)sizeof(u));//12
    return 0;
}

计算的时候一定注意联合的特性,第一个地址是公用的。

六、动态内存管理


动态内存分配的意义

我们已经指到的内存开辟的方式:

//开辟四个字节的空间
int a = 5;
//开辟连续的一个字节的十个空间
char arr[10] = {0};

但是这两种方式开辟的空间有两个特点:

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

但是对空间的需求,不仅仅是上述的情况,有的时候我们需要多少的空间是不能直接知道的,需要程序运行开始,或者是用户使用的时才能知道,这时数组在编译时开辟空间的方式就不能满足了。

这时候就可以试试动态内存开辟了。

存储空间的使用

image-20220214095959271

动态内存函数介绍

malloc和free

C语言提供的开辟内存的函数:malloc

void* malloc(size_t size);//malloc函数的声明

函数作用:

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

1. 如果开辟成功,返回一个指向开辟好空间的指针。
2. 如果开辟失败,则返回一个NULL指针,所以使用malloc,要对返回值进行检查。
3. 返回值类型是void\* ,即malloc函数不知道开辟空间的类型,在具体使用的时候,强制转化一下指针类型。
4. 如果参数size为0,malloc的行为是标准是未定义的,取决于编译器。

开辟空间后不能让他一直存在吧,这样内存就一直存放着。这时我们还有一个free函数用来释放或回收动态开辟的内存。

void free(void* ptr);

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

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

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

函数使用实例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(){
    //向内存申请10个整型的空间
    int* p = (int *) malloc(10* sizeof(int ));//改成INT_MAX测试就可以执行if部分
    if (p == NULL){
        //打印错误的原因
        printf("%s\n", strerror(errno));
    }
    else{
        //正常使用空间
        int i = 0;
        for (i = 0; i < 10; ++i) {
            *(p+i) = i;
        }
        for (i = 0; i < 10; ++i) {
            printf("%d",*(p+i));
        }
    }
    //当动态申请的空间不再使用的时候就应该还给操作系统。
    //回收空间
    free(p);
    //free掉之后仍然p仍然指向那个内存地址。
    p = NULL;//这样更加安全。
    return 0;
}

calloc

C语言除了malloc还提供了一个函数叫calloc,calloc函数也用来动态内存分配。

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

具体使用同malloc和free的使用方式。

int main() {
    int *p = (int *) calloc(10, sizeof(int));
    if (NULL != p) {
        //使用空间
    }else{
        //打印错误信息
        printf("%s\n",strerror(errno));
    }
    free(p);
    p = NULL;
    return 0;
}

realloc

realloc函数让动态内存管理更加灵活。

有时我们觉得内存申请小了或者大了,就可以使用realloc函数进行调整。

void* realloc (void* ptr,size_t size);

参数解释:

  • ptr是要调整的内存地址
  • size是调整时候的新的大小
  • 返回值为调整后内存起始位置
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

realloc使用注意事项:

  1. 如果p指向的空间后面有足够的空间可以追加,则直接追加,返回p
  2. 如果p指向的空间后面没有足够的内存空间可以追加,则realloc函数会重新找一个新的区域开辟一块满足需求的空间,并且把原来内存中的数据拷贝回来,释放旧的空间,最后返回新开辟的内存空间地址。
  3. 追加失败,会返回空指针。–不能直接赋值到原来的指针上,防止数据丢失。

内存中情况:

image-20220214160357780

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;
        }
    }
    int *ptr = realloc(p, 4000);
    if (ptr != NULL) {
        int i = 0;
        for (i = 5; i < 10; i++) {
            *(ptr + i) = i;
        }
        for (i = 0; i < 10; i++) {
            printf("%d\n", *(p + i));
        }
    }
    //释放空间 
    //-- 如果出现不够用的情况,另起空间ptr原来的空间在realloc的时候就被释放了
    free(ptr);
    ptr = NULL;
    return 0;
}

常见的动态内存错误

对NULL指针的解引用操作

访问空指针,就是非法访问。

在动态内存分配之后一定要进行返回值的判断。

int main(){
    int *p = (int*)malloc(40);
    //万一malloc失败了,p就被赋值为NULL
    *p = 0;
    free(p);
    return 0;
}

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

程序崩溃

int main() {
    int a = 10;
    int *p = &a;
    free(p);//可以吗?
}

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

程序崩溃

int main() {
    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释放一块动态开辟内存的一部分空间

释放的时候必须从申请空间的起始位置开始释放。

程序崩溃。

所以一般不对这个起始地址的指针p进行修改性质的运算。

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

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

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

避免方式:free完之后,将p置空。

int main() {
    int *p = (int *) malloc(100);
    free(p);
    p = NULL;
    free(p);//无效释放,程序不会崩溃。
}

动态开辟内存后没有释放(内存泄漏)

int main() {
    while (1){
        malloc(1);
    }
}

下面的这种情况就是,出了test函数想释放都没法释放了。

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

int main() {
    test();
    while (1);
}

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

有些语言就有自己的内存回收机制。

申请动态内存或者使用动态内存时出现的错误,大都会导致程序直接崩溃。

free之后,记得将指针置空。

几个经典的笔试题

题目一

测试函数的执行结果是什么。

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

void Test(void) {
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
}
  1. 运行代码程序会崩溃
  2. 程序存在内存泄漏的问题

str以值传递的形式给了p。p是GetMemory函数的形参,只能函数内部有效,等GetMemory函数返回之后,动态开辟内存尚未释放并且无法找到,所以会内存泄漏。

void GetMemory(char *p) {
    p = (char *) malloc(100);//2. 内存未释放
}

void Test(void) {
    char *str = NULL;
    GetMemory(str);//1. 这里传的是str的地址,并不是*str的地址。
    strcpy(str, "hello world");//3. 程序是在这里崩溃的,非法访问内存
    printf(str);//这个printf没有问题
}

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

提示:str是一个变量,向函数传变量,在函数运行的时候是变量的临时拷贝,要想对变量操作必须传地址。但是由于str本身就是一个地址,那在函数GetMemory中必须使用二级指针来接受。

这样是可以运行的。不至于崩溃,但是仍然存在内存泄漏的问题。

image-20220214181342732

修改方式2

image-20220214184854015

题目二

下面的测试函数运行结果是什么

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

void Test(void) {
    char *str = NULL;
    str = GetMemory();
    printf(str);
}
int main() {
    Test();
    return 0;
}

程序可能会崩溃,可能是随机值。(补充:局部变量在栈区存储。)

产生原因:在GetMemory中创建的变量p在结束函数后就被销毁了,之后由函数返回的内存地址发生了什么是不知道的(有点非法访问的意思)。

解决方式:一、因为函数中变量在函数结束后被销毁,那么延长该变量的生命周期就可以了,也就是在函数中定义变量的时候用static修饰一下。(将变量存在静态区)

二、使用动态内存分配进行定义空间,然后不要在函数中free,这样在函数外仍然可以访问这个地址的空间。但是这样写的话会出现内存泄漏的问题。(将变量存在堆区)

题目三

下面的测试函数的运行结果是什么

void GetMemory(char **p, int num) {
    *p = (char *) malloc(num);
}

void Test(void) {
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}
int main() {
    Test();
    return 0;
}

程序可以运行出结果,但是程序的问题是内存泄漏,在使用完str后没有将空间释放掉。

修改错误:

void Test(void) {
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf("%s", str);
    //代码修改
    free(str);
    str = NULL;
}

题目四

下面的测试函数的运行结果是什么?

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

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

可以运行出预想的结果,但是代码仍然存在问题。free之后继续使用之前开辟的空间的地址,就是非法访问。

注:free释放空间后并不会将内容置空。

修改:

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

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

C程序的内存开辟

image-20220214224432183

(图片来自于视频截图)

内存分配区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

通过这个图,我们就可以更好的理解static关键字修饰局部变量的例子了。

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

但是被static修饰的变量存放在数据段(静态区),数据段的特点是在这里创建的变量,直到程序结束才销毁。

所以生命周期变长。

柔型数组

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

举例说明:

// struct S{
//     int n;
//     int arr[0];
// };
struct S{
    int n;
    int arr[];//未知大小的柔性数组成员-数组的大小是可以调整的
};
int main(){
    struct S s;
}

柔性数组的特点

  1. 结构中的柔性数组成员前面必须至少一个其他成员
  2. sizeof返回的这种结构大小是不包括柔性数组的内存。
  3. 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔型数组的预期的大小。
struct S{
    int n;
    int arr[];//未知大小的柔性数组成员-数组的大小是可以调整的
};
int main(){
    struct S s;
    printf("%d\n", sizeof(s));//结果是4
}

柔性数组的使用

struct S{
    int n;
    int arr[];//未知大小的柔性数组成员-数组的大小是可以调整的
};
int main(){
    //创建空间
    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;
    }
    //空间不够了继续开辟
    struct S* ps2 = realloc(ps,sizeof(struct S)+10*sizeof(int));
    if(ps2!=NULL){
        ps = ps2;
    }
    for (i = 5; i < 10; ++i) {
        ps->arr[i] = i;
    }
    for (i = 0; i < 10; ++i) {
        printf("%d ",ps->arr[i]);
    }
    free(ps);
    ps = NULL;
    return 0;
}

柔性数组的优势

实现上面同样的功能但是不使用柔性数组

struct S {
    int n;
    int *arr;//这个指针指向一个动态开辟的空间存放数组
};

int main() {
    //申请空间,申请到的是一个int和一个指针的空间
    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 *ps2 = realloc(ps->arr, 10 * sizeof(int));
    if (ps2 != NULL) {
        ps->arr = ps2;
    }
    for (i = 5; i < 10; ++i) {
        ps->arr[i] = i;
    }
    for (i = 0; i < 10; ++i) {
        printf("%d ", ps->arr[i]);
    }
    //释放空间  z
    free(ps->arr);
    ps->arr = NULL;
    free(ps);
    ps = NULL;
    return 0;
}

上面的代码我认为可以将ps创建为结构体变量,ps.arr创建为动态申请空间的数组变量。

小笔记:指针变量分配动态空间后,其指向的变量可以直接当数组使用。

对比两种实现方式

image-20220215112216243

第一种的好处:方便内存的释放

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

  2. 连续的内存有益于提高访问的速度,也有益于减少内存碎片。

第二种的好处:有利于访问速度

​ 连续的内存有益于提高访问的速度,也有益于减少内存碎片。

小结

柔性数组是结构体中的最后一个元素。(首先有结构体,然后最后一个0长度的数组),并且这个结构体至少有两个成员。

在结构体中柔性数组成员是不指定数组大小的。

使用柔性数组的目的:

  1. 想操控一块连续的空间,被当成数组使用。
  2. 内存释放的时候只需要释放一次。

七、C语言文件操作


什么是文件

磁盘上的文件是文件。

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

程序文件

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

数据文件

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

我们文件操作主要是说的数据文件。

文件名

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

文件名包括三个部分:文件路径 + 文件名主干 + 文件后缀

举例:c:\code\test.c

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

文件类型

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

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

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

总结:外存中不转化就是二进制文件,转化就是文本文件。

一个数据在内存中的存储形式

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

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

image-20220217134845672

代码测试:

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

这时在文件目录下会产生一个test.txt的文件。直接打开内容是无法阅读的。但是程序以二进制阅读的时候会产生以下结果。

image-20220217140134445

但是如果使用文本的方式存储打开文件后直接就是10000的形式。

文件缓冲区

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

image-20220217142044693

文件指针

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

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

image-20220217144840216

不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。

每当打开一个文件的时候,系统会根据文件的情况会自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。

一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。

创建一个FILE*的指针变量

FILE* pf;//文件指针变量的定义

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

理解:就像数组一样,通过首元素地址就可以管理数组。

image-20220219103605966

文件的打开和关闭

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

在编写程序的时候,在打开文件的同时,都会返回一个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+” 读写打开一个二进制文件,在文件尾部进行读和写建立一个新的文件

"r"表示的是读,"w"表示的是写,“a"表示追加,”+"表示为了读写

文件操作

int main() {
    FILE *pf = fopen("test.txt","r");//将r换位其他的文件使用方式测试
    if (pf == NULL){
        printf("%s",strerror(errno));
        return 0;
    }
    //打开成功
    //读写文件
    //关闭文件  -- 类似于动态内存中的free
    fclose(pf);
    pf = NULL;
    return 0;
}

路径解析

相对路径

相对于当前运行文件,或者当前工程的文件目录的,相对的路径。

..表示上一级目录
.表示当前目录
/表示进入下一级目录
绝对路径

从此电脑(我的电脑的目录开始),一般是以C、D、F、G盘开头的文件目录

文件的顺序读写

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

image-20220219153616662

实例:

/* fgetc example: money counter */
#include <stdio.h>
int main ()
{
  FILE * pFile;
  int c;
  int n = 0;
  pFile=fopen ("myfile.txt","r");
  if (pFile==NULL) perror ("Error opening file");
  else
  {
    do {
      c = fgetc (pFile);
      if (c == '$') n++;
    } while (c != EOF);
    fclose (pFile);
    printf ("The file contains %d dollar sign characters ($).\n",n);
  }
  return 0;
}
fputc

image-20220219153659448

实例:

/* fputc example: alphabet writer */
#include <stdio.h>

int main ()
{
  FILE * pFile;
  char c;

  pFile = fopen ("alphabet.txt","w");
  if (pFile!=NULL) {

    for (c = 'A' ; c <= 'Z' ; c++)
      fputc ( c , pFile );

    fclose (pFile);
  }
  return 0;
}
fgets

image-20220219153549029

实例:

/* fgets example */
#include <stdio.h>

int main()
{
   FILE * pFile;
   char mystring [100];

   pFile = fopen ("myfile.txt" , "r");
   if (pFile == NULL) perror ("Error opening file");
   else {
     if ( fgets (mystring , 100 , pFile) != NULL )
       puts (mystring);
     fclose (pFile);
   }
   return 0;
}
fputs

image-20220219153840677

实例:

/* fputs example */
#include <stdio.h>

int main ()
{
   FILE * pFile;
   char sentence [256];

   printf ("Enter sentence to append: ");
   fgets (sentence,256,stdin);
   pFile = fopen ("mylog.txt","a");
   fputs (sentence,pFile);
   fclose (pFile);
   return 0;
}
fscanf

image-20220219153931360

实例:

/* fscanf example */
#include <stdio.h>

int main ()
{
  char str [80];
  float f;
  FILE * pFile;

  pFile = fopen ("myfile.txt","w+");
  fprintf (pFile, "%f %s", 3.1416, "PI");
  rewind (pFile);
  fscanf (pFile, "%f", &f);
  fscanf (pFile, "%s", str);
  fclose (pFile);
  printf ("I have read: %f and %s \n",f,str);
  return 0;
}
fprintf

image-20220219153958342

实例:

/* fprintf example */
#include <stdio.h>

int main ()
{
   FILE * pFile;
   int n;
   char name [100];

   pFile = fopen ("myfile.txt","w");
   for (n=0 ; n<3 ; n++)
   {
     puts ("please, enter a name: ");
     gets (name);
     fprintf (pFile, "Name %d [%-10.10s]\n",n+1,name);
   }
   fclose (pFile);

   return 0;
}
fread

image-20220219154021409

实例:

/* fread example: read an entire file */
#include <stdio.h>
#include <stdlib.h>

int main () {
  FILE * pFile;
  long lSize;
  char * buffer;
  size_t result;

  pFile = fopen ( "myfile.bin" , "rb" );
  if (pFile==NULL) {fputs ("File error",stderr); exit (1);}

  // obtain file size:
  fseek (pFile , 0 , SEEK_END);
  lSize = ftell (pFile);
  rewind (pFile);

  // allocate memory to contain the whole file:
  buffer = (char*) malloc (sizeof(char)*lSize);
  if (buffer == NULL) {fputs ("Memory error",stderr); exit (2);}

  // copy the file into the buffer:
  result = fread (buffer,1,lSize,pFile);
  if (result != lSize) {fputs ("Reading error",stderr); exit (3);}

  /* the whole file is now loaded in the memory buffer. */

  // terminate
  fclose (pFile);
  free (buffer);
  return 0;
}
fwrite

image-20220219154043938

实例:

/* fwrite example : write buffer */
#include <stdio.h>

int main ()
{
  FILE * pFile;
  char buffer[] = { 'x' , 'y' , 'z' };
  pFile = fopen ("myfile.bin", "wb");
  fwrite (buffer , sizeof(char), sizeof(buffer), pFile);
  fclose (pFile);
  return 0;
}

输入流是从文件向计算机导入信息,输出流是从计算机向文本导出信息。

使用时注意函数返回值。

个人样例感受。

int main(){
    int ch = fgetc(sedin);
    fputc(ch,stdout);
    return 0;
}
typedef struct S{
  int n;
  float score;
  char arr[10];
}S;
int main(){
    S s = {0};
    fscanf(stdin,"%d %f %s",&(s.n),&(s.score),s.arr);
    fprintf(stdout,"%d %.2f %s",s.n,s.score,s.arr);
    return 0;
}
键盘、屏幕

我们日常使用的键盘屏幕也是输入输出流。一般讲从键盘输入,输出到屏幕上。但是键盘和屏幕都是外部设备。

键盘被称之为 标准输入设备;屏幕是标准输出设备。对于两个设备,是程序默认打开的两个流设备。

程序默认打开的三个流

stdin(键盘)、stdout(屏幕)、stderr。这三个流是默认打开的。

对比函数

scanf/fscanf/sscanf

printf/fprintf/sprintf

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

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

sscanf是从字符串中读取格式化的数据
sprintf是把格式化数据存储到字符串中。

typedef struct S{
  int n;
  float score;
  char arr[10];
}S;
int main(){
    S s = {100,3.14f,"abcdef"};
    S tmp = {0};
    char buf[1024] = {0};
    //把格式化的数据转换成字符串存储到buf
    sprintf(buf,"%d %f %s",s.n,s.score,s.arr);
    // printf("%s\n",buf);
    // 从buf中读取格式化的数据到tmp中。
    sscanf(buf,"%d %f %s",&(s.n),&(s.score),s.arr);
    printf("%d %f %s",s.n,s.score,s.arr);
    return 0;
}

文件的随机读写

fseek

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

int fseek(FILE* stream,long int offset,int origin);
int fseek(文件指针,偏移量,文件指针的当前位置)

origin可以写成SEEK_CUR(文件指针的当前位置)、SEEK_END(文件指针的末尾位置)、SEEK_SET(文件指针的其实位置)。

有了这个函数,就可以随机读取文件内容了。

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

返回文件指针相对于其实位置的偏移量

long int ftell(FFILE * 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;
}

文件结束判定

被错误使用的feof

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

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

理解:文件读取结束后,知道读取结束的原因。

文件读取结束判断:

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

    例如:

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

    例如:

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

正确使用的例子:

int main(){
    int c;
    FILE* fp = fopen("test.txt""r");
    //perror
    if(!fp){
        //错误信息
        perror("Failed");
        return EXIT_FAILURE;
    }
    //打开成功处理
    //fgetc当读取失败的时候或者遇到文件结束的时候,都会返回EOF
    while((c = fgetc(fp))!=EOF){//读取结束后结束循环
        putchar(c)
    }
    //判断结束原因
    if (ferror(fp)) puts("I/o error when reading");
    //feof一定是文件读取结束后才使用。
    else if(feof(fp))
        puts("End of file reached successfully");
    //置空
    fclose(fp);
    fp = NULL;
}

feof不是判断结束,是判断结束的原因的。所以使用的条件是文件读取结束。

二进制的例子:(直接截图了)

image-20220220185444678

八、程序环境和预处理

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

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

第一种是翻译环境,这个环境中源代码被转换为可执行的机器指令。(二进制代码)
第二种是执行环境(运行环境),用于实际执行代码。

image-20220221125852074

详解编译+链接

翻译环境

image-20220221131051856

编译使用的是编译器,链接使用的是链接器。

过程:

  1. 组成一个程序的每个源文件都要通过编译过程转换为目标代码。
  2. 每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。
  3. 链接器同时也会引入标准C函数库中的所有该程序使用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

编译还可以分为几个阶段

image-20220221145946858

查看编译期间的每一步发生了什么

在linux环境下

  1. 预处理 选项 gcc -E test.c -o test.i

预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。

  1. 编译 选项 gcc -S test.c

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

  1. 汇编 gcc -c test.c

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

编译每个阶段的事情

预处理

  1. #include 头文件的包含
  2. 注释删掉 使用空格来替换注释
  3. #define

编译 将test.i翻译为test.s

​ 将c语言代码翻译为汇编代码

	1. 语法分析
	2. 词法分析
	3. 语义分析

汇编

1. 生成可重定位目标文件.o文件,
2. 行成符号表。
3. 汇编指令转换为二进制指令。

链接做的事情

  1. 合并段表
  2. 符号表的合并和符号表的重定位

运行环境

程序执行的过程:

  1. 程序必须载入到内存中。在有操作系统的环境中:一般由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  2. 程序的执行便开始。接着调用main函数。
  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态内存(static),存储于静态内存中的变量在程序的整个执行过程一直保留着他们的值。
  4. 终止程序,正常终止main函数;也有可能是意外终止。

预处理详解

预定义符号

本身就有的符号

__FILE__    //进行编译的源文件,目录及文件名展示%s输出
__LINE__    //文件当前的行号,代码当先所在行   %d输出
__DATE__    //文件被编译的日期,月日年 其中月是英文  %s输出
__TIME__    //文件被编译的时间 正常时分秒  %s输出
__STDC__    //如果编译器遵循ANSI C,这个值为1,如果没有遵循则显示这个值未定义。

使用举例:写日志。

#define 预处理指令

#define、#include都是预处理指令,还有#pragma、#if、#end等

#define定义标识符
#define name stuff
#define MAX 100
#define STR "hehe"

在预编译处理完之后直接将代码中的MAX全部替换为100了。

举例:

#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定义宏

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

宏的申明方式:

#define name(parament-list) stuff 
其中的parament-list是一个逗号隔开的符号表,他可能出现在stuff中。

注意: 参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

举个错误的例子

#define SQUARE( x ) x * x   //宏定义没有问题,左括号紧跟<name>

这个宏接收一个参数x。在声明之后将SQUARE( 5 );置于程序中,预处理器就会用下面这个表达式替换上面的表达式。5 * 5

但是,我说过,这个宏存在问题:我们仔细看一个实例

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

正常思路这个结果应该是36.但是他打印的是11。

image-20220221224300274

这是为什么呢?

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

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

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

问题解决

在宏定义上加上两个括号,确定优先级就可以了。

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

另一个问题

定义一个新的宏

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

定义中使用了括号,感觉应该可以避免之前的问题,但是这个宏还是可能会出现新的问题。

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

先自己思考结果是什么?

你是不是想的是100,但是实际上是55。

image-20220222091737081

这是为什么呢?

我们将宏替换代码后

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

这样乘法的运算就先于宏定义的加法了,所以输出为55

问题解决

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

总结:用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免使用宏时参数中的操作符或者邻近操作符之间不可预估的相互作用。(只要记住,宏不是传参,是替换。)

#define替换原则

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

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

注意:

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

现在有个需求:

void print(int a){
    printf("the value of a is %d\n",a)
}
int main(){
    int a = 10;
    print(a);//这里打印的是 the value of a is 10
    int b = 20;
    print(b);//这里打印的是 the value of a is 20
}

问题就在其中,print(b)的时候打印的还是the value of a is 20。这里我们应该是b,但是他打印的是a。

函数不能做到这样的功能(把参数插入到字符串中。)。但是宏可以。

首先我们要知道,才C语言中存在下面的情况。

int main(){
    printf("hello world");
    printf("hello " "world");
    printf("hel" "lo " "wor" "ld");
}

这三个printf语句都会有hello world的结果。

还有一个技巧:使用#,把一个宏参数变成对应的字符串。

有了这个思路我们就有了一个思路。中间需要插入的部分用宏的方式插入。

#define PRINT(X) printf("the value of " #X " is %d\n",X)
int main(){
    int a = 10;
    PRINT(a);//printf("the value of " "a" " is %d\n",X)
    int b = 20;
    PRINT(b);//printf("the value of " "b" " is %d\n",X)
}

image-20220222095712642

##的作用

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

#define CAT(X, Y) X##Y

int main() {
    int Class12 = 2020;
    // printf("%d\n",Class12);//正常思路的输出
    printf("%d\n", CAT(Class,12));
    //宏定义后printf("%d\n",Class##12);
    //即printf("%d\n",Class12);
}

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

带有副作用的宏参数

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

//举例
x+1;//没有副作用
x++;//带有副作用

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

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main(){
    int x = 5;
    int y = 8;
    int z = MAX(x++, y++);
    printf("x=%d y=%d z=%d\n", x, y, z);
}

我们本身预想的结果是x=6 y=9 z=9,但是实际输出为x=6 y=10 z=9

这是为什么呢?

我们将预处理后的代码写出

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

这样,就明白了,我们计算了两次y++。

宏个函数对比

前面写了好几个demo都是将宏当做函数来使用。实现简单的运算。

那么这些东西为什么不直接使用函数来完成呢?

原因:

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

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 programe.c  -D ARRAY_SIZE=10 

这里的文件名可以放前面也可以放后面。

条件编译

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

比如:

测试性质的代码,删除了可惜,保留下来有比较妨碍事情。所以我们可以选择性的编译。

下面代码将最上面的#define __DEBUG__注释掉和不注释掉结果是不一样的。

#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__
        //如果DEBUG被定义过,那就参与编译,如果没有没定义过就不参与编译
    }
    return 0;
}

在平时写代码的时候,我们总是写printf测试我们的代码。当写在多个地方的时候,注释起来会比较麻烦。写成这样的话,只需要把开头#define __DEBUG__处理一下就可以了。

常见的条件编译指令:

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

使用案例

#define DEBUG 0

int main() {
#if defined(DEBUG)
    printf("#if defined(DEBUG)");
#endif
    //和上面的写法是等价的。
#ifdef DEBUG
    printf("#ifdef DEBUG");
#endif
//如果定义的是编译,没有定义不编译
//那么如果是定义了不编译,没有定义编译的话如下
#if !defined(DEBUG)
    printf("#if !defined(DEBUG)");
#endif
#ifndef DEBUG  //注意:这里加了一个n
    printf("#ifndef DEBUG");
#endif
// 嵌套指令和前面说的差不多。
    return 0;
}

文件包含

我们在引用头文件的时候经常使用#include指令,使另一个文件被编译。就像它实际出现于#include指令的地方一样。

这样的额替换方式很简单:预处理先删除这条指令,并用包含头文件的内容替换。
这样一个源文件被包含10次就编译10次。

头文件被包含的方式:

包含本地文件

#include "filename"

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

标准头文件目录:

Linux环境下

/user/include

我们大部分时候都是在安装路径中可以找到。

包含库文件

#include <filename.h>

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

这样我们就发现,对于库文件也可以使用双引号的形式包含。但是这样做的话,查找的效率就低些,当然这样也不容易区分是本地文件,还是库文件了。

嵌套文件包含

比如在我们开发的时候,文件关系如图所示。

image-20220222161432818

A.hB.hC.h调用,B.hC.h又被D.h调用,这样我们的A.h就被调用了两次,编译的时候就多出了一份A.h这样就造成了文件的重复。

怎么解决这个问题呢?使用条件编译。

方式一:

每个头文件开头写。

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

方式二:

#pragma once   //可以避免头文件的重复引入

其他的预处理指令

#error
#pragma
#line
...
#pragma pack()在结构体部分介绍。

其他的预处理指令

  • 50
    点赞
  • 401
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

黎丶辰

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

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

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

打赏作者

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

抵扣说明:

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

余额充值