C语言学习笔记(第三部份)

news2025/3/19 2:14:06

说明:由于所有内容放在一个md文件中会非常卡顿,本文件将接续C_1.md文件的第三部分

整型存储和大小端

引例:

int main(void) {
	// printf("%d\n", SnAdda(2, 5));
	// PrintDaffodilNum(10000);
	// PrintRhombus(3);
	int i = 0;
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	for (i = 0; i <= 12; i++) {
		arr[i] = 0;
		printf("Nihao\n");
	}
	return 0;
}

上述代码会死循环,因为数组越界了

数据类型介绍

C语言的基本数据类型:
char        // 字符数据类型
short		// 短整型
int			// 整型
long		// 长整型
long long	// 更长的整型(C99)
float		// 单精度浮点型
double		// 双精度浮点型
  • 类型的意义:

    • 类型决定了内存空间的大小
    • 类型决定了编译器如何看待内存空间里的数据
  • 整型家族

    char
    	unsigned char
    	signed char
    short
    	unsigned short
    	signed short
    int 
    	unsigned int
    	signed int
    long 
    	unsigned long
    	signed long
    long 
    	unsigned long long
    	signed long long
    
  • 浮点型家族

    float 
    double
    
  • 构造类型

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

    int* pi;
    char* pc;
    float* pf;
    void* pv;
    

整型在内存中的存储

  • 正数 原码、反码、补码相同
  • 负数 原码最高位符号位为1 || 原码的符号位不变,其他位置按位取反就得到反码 || 反码末位加1就得到补码,反码的符号位也不变
  • 整数在内存中都是按补码存放的。因为使用补码,可以将符号位和数值位统一处理,同时,加法和减法也可以统一处理(CPU只有加法器),此外,补码和原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

大小端

int a = 20;  // 20的补码为:0 000 0000 0000 0000 0000 0000 0001 0100
			// 对应的十六进制: 0x 00 00 00 14

在这里插入图片描述

  • 大端字节序存储:把数据的高位字节序的内容存放在内存的低地址处,把低位字节序的内容放在内存的高地址处。
  • 小端字节序存储:把数据的高位字节序的内容存放在内存的高地址处,把低位字节序的内容放在内存的低地址处。

记:高位放在高地址是小端(高高小)

  • 在VS中都是按照小端的形式存储的

文件操作(I/O)

表示任意输入的源或任意输出的目的地,C语言中对文件的访问是通过文件指针(即:FILE *)来实现的。

文件名

文件名包含:文件路径+文件名+文件后缀

如:c:\code\test.c

文件指针

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

FILE* fp;

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

标准流

<stdio.h>中提供了三个标准流,可以直接使用,不用声明,也不用打开或关闭:

标准流作用文件指针
标准输入流用于接收用户输入(通常来自键盘)stdin
标准输出流用于向屏幕打印输出信息(屏幕)stdout
标准错误流用于输出错误信息(屏幕,通常不受缓冲影响)stderr

任何一个C程序,在运行时都会默认打开上述三个流,流的类型都是 FILE*

如使用stdin,gets(str, sizeof(str), stdin);

默认情况下,stdin表示键盘,而stdout和stderr表示屏幕,但是可以通过重定向修改输入输出的地方,输入重定向(<),输出重定向(>)。

文本文件和二进制文件

  • <stdio.h>支持两种类型的文件,包括文本文件和二进制文件。
  • 在文本文件中,字节表示字符,C语言的源代码就是存储在文本文件中的,文本文件之后会给你的内容可以被检查或编辑。
    • 文本文件分为若干行,每一行通常都以一两个特殊的字符结尾,Windows系统中行末的表示是回车符(‘\x0d’),其后紧接一个换行符(‘\x0a’)。
    • 文本文件可以包含一个特殊的文件末尾标记(一个特殊的字节),Windows系统中标记为(‘\x1a’, 即Ctrl+z),这个标记不是必须的,但是如果存在该标记,它就标记着文件的结束,其后的所有字节的都会被忽略。
  • 在二进制文件中,字节不一定表示字符。二进制文件不分行,也没有行末标记或文件末尾标记,所有字节都是平等的。
  • 文件操作可以节省更多的空间,但是要把文件内容输出到屏幕显示,还是要选择文本文件。

打开文件

  • 对于任何需要用文件作为流的地方,都必须先用fopen打开文件。也就是说使用文件前必须先打开文件
FILE *fopen(
   const char *filename,
   const char *mode
);

参数说明:

  • 返回一个文件指针,通常将该文件指针存储在一个变量中,以便接下来进行操作。无法打开文件时返回空指针,因为不能保证总是能打开文件,因此每次打开文件都要测试fopen函数的返回值是否为空

    FILE* fp = fopen(FILE_NAME, "r");
    if(fp==NULL){
    	printf("can not open %s\n", FILE_NAME);
    	exit(EXIT_FAILURE);
    }
    
  • filename:含有打开文件名的字符串,该文件名可能含有文件的路径信息,如果含\,要用两个\对其进行转义。

  • mode: 文件访问模式

    文件访问模式字符串含义解释若文件已存在的动作若文件不存在的动作
    “r”打开文件以读取从头读打开失败
    “w”打开文件以写入(文件无需存在)销毁内容创建新文件
    “a”后附打开文件以追加(文件无需存在)写到结尾创建新文件
    “r+”读扩展打开文件以读/写从头读错误
    “w+”写扩展创建文件以读/写覆盖原内容创建新文件
    “a+”后附扩展打开文件以读/写写到结尾创建新文件
    文件访问模式字符串含义解释若文件已存在的动作若文件不存在的动作
    “rb”以二进制模式打开文件以读取从头读打开失败
    “wb”以二进制模式创建文件以写入销毁内容创建新文件
    “ab”后附以二进制模式打开文件以追加写到结尾创建新文件
    “r+b” 或 “rb+”读扩展以二进制模式打开文件以读/写从头读错误
    “w+b” 或 “wb+”写扩展以二进制模式创建文件以读/写覆盖原内容创建新文件
    “a+b” 或 “ab+”后附扩展以二进制模式打开文件以读/写写到结尾创建新文件
  • 总结:只有含 r 的打开模式,文件必须已经存在,其他模式打开文件时,文件可以不存在。

  • 如果用w内容打开文件,如果文件里有内容,在fopen打开文件时,文件里的内容就被清理掉了

  • 注意,当打开文件用于读和写时(模式字符串中含有+),有一些特定的规则。如果没有调用文件定位函数,就不能从读模式转换成写模式,除非遇到了文件的末尾。如果即没有调用fflush函数,也没有调用文件定位函数,也不能从写模式转换成读模式。

关闭文件

  • 必须及时关闭一个不会再使用的文件
int fclose( FILE* stream );

参数说明:

  • stream: 需要关闭的文件流,必须是一个文件指针,该指针只能来自于fopen或freopen的调用。
  • 关闭成功返回0,否则返回EOF

基本的文件操作流程

#inlcude<stdio.h>
#include<string.h>
#include<errno.h>
int main(int argc, char* argv[])
{
	// 打开文件
	FILE* fp = fopen("test.txt", "r");
	if (fp == NULL) {
		printf("%s\n", strerror(errno)); // 这句话可以将错误信息输出到屏幕上,包含在<errno.h>
        
         // 或者用下边这句话
         perror("fopen:");  // 同样是打印错误信息,但是会在错误信息前添加上自己写的字符串 fopen,这样可以提示自己哪里出错了
		return 1;
	}

	// 操作文件(读,写)
	...

	// 关闭文件
	fclose(fp);
	fp = NULL;

	return 0;
}

文件的读写

  • int fputc(int c, FILE *stream) 每次写入一个字符

    fputc('c', fp);
    char i;
    for (i = 'a'; i <= 'z'; i++) {
        fputc(i, fp);
    }
    
  • int fgetc(FILE *stream); 每次读取一个字符

    fgetc 返回作为 int 读取的字符或返回 EOF 指示错误或文件结尾

    char i;
    i = fgetc(fp);
    printf("%c\n", i);
    
    while ((i = fgetc(fp)) != EOF) {
        printf("%c", i);
    }
    
  • int fputs( const char *str, FILE *stream ); 写一行数据

    fputs("你好", fp);
    fputs("亲爱的", fp);   // 文件里实际上是在一行,如果需要在两行上,需要手动加上 \n
    
    fputs("你好\n", fp);
    fputs("亲爱的", fp);   // 文件里就是在两行
    
  • char *fgets( char *str, int n, FILE *stream ); 读一行数据

    fgets 读取从当前流位置的字符,并且包括第一个字符,到流的末尾,或直至读取 字符数 - 1 与 n 相等。也就是最多读 n-1个字符,因为会在最后添加‘\0’

    • n-1 的合理性
      fgets 的设计目标是确保缓冲区不会溢出。通过最多读取 n-1 个字符,它留出最后一个位置给 \0,从而保证字符串始终有效终止。
      • 即使输入包含换行符 \n,它也被视为有效字符,占用 n-1 中的一席之地。
      • 若用户输入恰好 n-1 个字符(不含 \n),fgets 会读取全部字符,并添加 \0,此时换行符仍留在输入流中。
    char str[20];
    fgets(str, 10, fp);
    printf("%s\n", str);
    
  • int fprintf( FILE *stream, const char *format [, argument ]...);

    struct STU s = { "张三丰", 25, 390.2f };
    fprintf(fp, "%s\t %s\t %s\n", "姓名", "年龄", "分数");
    fprintf(fp, "%s\t %d\t %.2f\n", s.name, s.age, s.score);
    
  • int fscanf( FILE *stream, const char *format [, argument ]...);

    struct STU s = { 0 };
    fscanf(fp, "%s %d %f", s.name, &s.age, &s.score);
    printf("%s\t %d\t %.2f\n", s.name, s.age, s.score);
    
  • size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );

    struct STU s = { "张三丰", 56, 96.25f };
    fwrite(&s, sizeof(s), 1, fp);
    
  • size_t fread( const void *buffer, size_t size, size_t count, FILE *stream );

    struct STU s = { 0 };
    fread(&s, sizeof(s), 1, fp);
    printf("%s %d %.2f\n", s.name, s.age, s.score);
    
  • int sprintf( char *buffer, const char *format [, argument] ... );

    把一个格式化的数据写到字符串中,本质上是把一个格式化的数据转换成字符串

    struct STU s = { "张三", 25, 56.65f };
    char buf[100];
    sprintf(buf,"%s %d %.2f", s.name, s.age, s.score);
    printf("%s\n", buf);  // buf中存的是这样的字符串:"张三 25 56.65"
    
  • int sscanf( char *buffer, const char *format [, argument] ... );

    从字符串中获取一个格式化的数据

    struct STU s = { "张三", 25, 56.65f };
    struct STU temp = { 0 };
    char buf[100];
    sprintf(buf,"%s %d %.2f", s.name, s.age, s.score);  // 把s中的格式化数据转换成字符串当道buf中
    sscanf(buf,"%s %d %.2f", temp.name, &temp.age, &temp.score);   // 从buf中获取一个格式化的数据到temp中
    
  • 比较几个函数的差异

    scanf   是针对 标准输入流(stdin) 的格式化 输入 语句
    printf  是针对 标准输出流(stdout) 的格式化 输出 语句
    
    fscanf  是针对 所有输入流 的格式化 输入 语句
    fprintf 是针对 所有输出流 的格式化 输出 语句
    
    sscanf  从一个字符串中转换成一个格式化的数据
    sprintf 是把一个格式化的数据转换成字符串
    

从命令行获取到文件名给程序

当执行名为FileOperation.exe的程序时,可以通过把文件名放入命令行的方式为程序提供文件名:

FileOperation EnglishArticle.txt

这样对于程序FileOperation的main函数来说:

int main(int argc, char* argv[]){...}

argc是命令行参数的数量,而argv是只想参数字符串的指针数组。argv[0]指向程序名,从argv[1]到argv[argc-1]都指向剩余的实际参数,而argv[argc]是空指针。在上述例子中:

在这里插入图片描述

例如,检查文件是否可以被打开,只需在命令行执行:

FileOperation EnglishArticle.txt

int main(int argc, char* argv[])
{
	FILE* fp;
	if (argc != 2) {
		printf("usage: canopen filename\n");
		exit(EXIT_FAILURE);
	}
	if ((fp = fopen(argv[1], "r")) == NULL) {
		printf("%s can't be opened\n", argv[1]);
		exit(EXIT_FAILURE);
	}
	printf("%s can be opened\n", argv[1]);
	fclose(fp);
	return 0;
}

文件缓冲

int fflush( FILE* stream );
void setbuf( FILE* stream, char* buffer );
int setvbuf( FILE* stream, char* buffer, int mode, size_t size );

由于对磁盘的读写都是比较缓慢的操作,因此不能在程序想读想写的时候都去访问磁盘,可以用缓冲来获得一个较好的性能。

  • 将写入流的数据存储在内存的缓冲区内,当缓冲区满(或者流被关闭)的时候,对缓冲区进行冲洗(将缓冲区的内容写入实际的输出设备)
  • 缓冲区包含来自输入设备的数据,从缓冲区读取数据而不是从输入设备本身读取数据。
  • <stdio.h>中的函数会在缓冲有用时,自动进行缓冲操作,缓冲是在后台自己完成的,但是有时需要我们手动的去进行缓冲的相关操作。

int fflush( FILE* stream ):

​ - 对于输出流(及最后操作为输出的更新流),从 stream 的缓冲区写入未写的数据到关联的输出设备。

​ - 对于输入流(及最后操作为输入的更新流),行为未定义。

​ - 若 stream 是空指针,则冲入所有输出流,包括操作于库包内者,或在其他情况下程序无法直接访问者。

指针的高级应用

数组指针

数组指针是指向数组的指针,本质上还是指针,存放的是地址。eg: int (*p)[]

char * arr[5] = {0};  	// 指针数组
char * (*pc)[5] = &arr; // 指向指针数组的指针
  • 示例:

    int main(void) {
    	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    	int(*p)[10] = &arr; // p指向数组arr, *p就相当于数组名,数组名又是数组首元素的地址
    	int i = 0;
    	int size = sizeof(arr) / sizeof(arr[0]);
    	for (i = 0; i < size; i++) {
    		printf("%d\n", (*p)[i]); // 也可以写成*(*p+i)
    	}
    	return 0;
    }
    

    上述例子说明用在一维数组上,数组指针非常的抽象而且很难用,一般地,数组指针至少都是用在二维数组上。

  • 示例:对二维数组而言,数组名表示数组首元素的地址,二维数组的首元素是它的第一行

    void my_printf(int(*p)[5], int r, int c) { // 参数是指向一维数组的指针,p指向一个含五个int元素的数组
    	int i = 0;
    	// p就指向传入数组的第一行,p + 1 就是第二行, p + i 就是第 i 行
        // 解引用,*(p + i)就拿到第 i 行的地址,也就是第 i 行首元素的地址
        // *(p + i) + j 就是第 i 行第 j 个元素的地址
        // 再解引用,*(*(P + i) + j) 就拿到第 i 行的第 j 个元素
    	for (i = 0; i < r; i++) {
    		int j = 0;
    		for (j = 0; j < c; j++) {
    			// *(p + i) 相当于 arr[i]
                 // *(*(p + i) + j) 相当于 arr[i][j]
    			printf("%d ", *(*(p + i) + j));  // 相当于打印arr[i][j]
                 // 也可以写成 printf("%d ", arr[i][j]);
    		}
    		printf("\n");
    	}
    }
    
    int main(void) {
        // arr表示第一行的地址,是一个一维数组的地址
    	int arr[3][5] = { 1,2,3,4,5,
                         21,22,23,24,25,
                         31,32,33,34,35 };
    	int i = 0;
    	int row = sizeof(arr) / sizeof(arr[0]);  // 获取行数
    	int col = sizeof(arr[0]) / sizeof(arr[0][0]);  // 获取列数
    	my_printf(arr, row, col);	
    
    	return 0;
    }
    
  • 解释 int (*p)[5]:

    • p 是一个数组指针
    • p 的类型是 int (*)[5]
    • p 指向的是一个含有 5 个元素的整型数组
    • p + 1 就是跳过一个含有 5 个 int 元素的数组,指向下一个 含有 5 个 int 元素的地址
    • int arr[5]; &arr 的类型就是 int (*)[5]
  • 示例:

    int arr[5];		// arr是整型数组
    int *p[5];		// p 是整型指针数组,有5个元素,每个元素都是一个整型指针
    int (*p)[5];	// p 是数组指针,即 p 是指向一个含有 5 个 int 元素的数组的指针
    int (*p[10])[5]; // p 是一个存放数组指针的数组,数组有10个元素,每个元素是 int (*)[5]的指针,
    

数组参数

  • 一维数组传参

    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    void test(int arr[]){...}    // right
    void test(int arr[10]){...}  // right
    void test(int* arr){...}	 // right
    
    int * arr[10] = {0};
    
    void test2(int* arr[10]){...}    // right
    void test2(int** arr){...}		// right    
    
  • 二维数组传参

    int arr[3][5] = {0};
    
    void test(int arr[3][5]){...}    // right
    void test(int arr[][]){...}		// wrong, 形参的二维数组行可以省略,列不可以省略
    void test(int arr[][5]){...}     // right
    
    void test(int* arr){...}		// wrong, 二维数组的数组名是首元素的地址,也就是第一行(一维数组)的地址,一维数组的地址不能放在一级指针里
    void test(int* arr[5]){...}		// wrong, 这个形参是一个指针数组,需要的是能够接收地址的指针,而不是数组
    void test(int (*arr)[5]){...}   // right, 这个arr是指针(数组指针),指针指向的是一个含有五个元素的数组
    void test(int** arr){...}	    // wrong, 这个arr是二级指针,是用来存放一级指针变量的地址
    

指针传参

  • 一级指针传参

    int arr[10] = {0};
    int *p = arr;
    int size = sizeof(arr)/sizeof(arr[0]);
    int a = 10;
    int* pa = &a;
    
    void test(int* p){...}
    
    test(arr); // right
    test(&a);  // right
    test(pa);  // right
    
  • 二级指针传参

    void test(int** p){...}
    
    int main(void){
        int n = 10;
        int* p = &n;
        int** pp = &p;
        test(pp);   // right
        test(&p);   // right
        
        return 0;
    }
    

    如果函数的形式参数是二级指针,调用函数的时候可以调用的参数类型:

    • int *p; // test(&p); 一级指针的地址
    • int** p; // test(p); 二级指针变量
    • int* arr[10]; // test(arr); 指针数组的数组名

函数指针

  • 函数指针是指向函数的指针,它里边存放的是函数的地址

  • &函数名 - 取出的就是函数的地址, 每一个函数都有自己的地址,函数也是有地址的

  • 对函数来说,&函数名函数名都是函数的地址

  • 函数指针的写法:

    int Add(int x, int y){
        return x + y;
    

}

int (*p)(int, int) = &Add; // 第一个 int 是函数的返回值类型,第二三个 int 是函数的参数类型
int (*p)(int, int) = Add;  // 这样写也可以,因为 &函数名和函数名都是函数的地址
// 以下三个等价
Add(2,3);
(*p)(2,3);
p(2,3);
```
  • 用法:

    int my_add(int x, int y) { return x + y; }
    
    int main(void) {
    	// &函数名 - 取出的就是函数的地址
    	int (*p)(int, int) = &my_add; // 解引用指针 *p 相当于是函数名 my_add
    	int res = (*p)(2, 3);  // (*p)的括号必须写,这里的 * 就是一个摆设,可以写很多个*  (***p)也可以
        // int res = p(2,3);  // * 也可以不写
    	printf("%d\n", res);
    
    	return 0;
    }
    
  • 函数名作为函数参数

    int Add(int x, int y){
        return x + y;
    }
    
    void calc(int (*P)(int, int)){
        int a = 3, b = 4;
        int res = p(a,b); // Add(a,b)
        printf("%d\n", res);
    }
    
    int main(void){
        calc(Add);
    }
    

回调函数

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

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

double Div(int a, int b) {
	if (b == 0) return 0;
	return 1.0 * a / b;
}

// p 是一个函数指针,Calc就是一个回调函数
void Calc(int (*p)(int, int)) {
	int x = 0, y = 0, res = 0;
	printf("请输入两个操作数——>");
	scanf("%d %d", &x, &y);
	res = p(x, y);
	printf("Answer is %d\n", res);
}

int main(void) {
	int mode = 1;

	while (mode) {
		printf("********Menu*********\n");
		printf("******* 1. 加法******\n");
		printf("******* 2. 减法******\n");
		printf("******* 3. 乘法******\n");
		printf("******* 4. 除法******\n");
		printf("*********************\n");
		scanf("%d", &mode);
		switch (mode)
		{
		case 1:
			Calc(Add);
			break;
		case 2:
			Calc(Sub);
			break;
		case 3:
			Calc(Mul);
			break;
		case 4:
			Calc(Div);
			break;
		case 0:
			return;
		default:
			printf("非法!请重新输入:");
			break;
		}
	}

	return 0;
}

函数指针数组(转移表)

把许多函数指针放在一个数组中,就是一个函数指针数组

  • 回顾函数指针的写法:int (*pf)(int, int) = Add;

  • 函数指针数组的写法:

    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;
    }
    
    // pfArr就是一个函数指针的数组
    int (*pfArr[4])(int, int) = { Add, Sub, Mul, Div };
    
    int i = 0;
    
    for (i = 0; i < 4; i++) {
        int res = pfArr[i](6, 2);   // 访问函数指针数组的元素
        printf("%d: %d\n", i + 1, res);
    }
    
  • 函数指针数组的示例:

    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(void) {
    	int x = 0, y = 0, res = 0, input = 0;
    	// pfArr就是一个函数指针的数组
    	int (*pfArr[5])(int, int) = {0, Add, Sub, Mul, Div };   // 也可以叫做转移表
    
    	do {
    		printf("********Menu*********\n");
    		printf("******* 0. 退出******\n");
    		printf("******* 1. 加法******\n");
    		printf("******* 2. 减法******\n");
    		printf("******* 3. 乘法******\n");
    		printf("******* 4. 除法******\n");
    		printf("*********************\n");
    		printf("请选择-->: ");
    		scanf("%d", &input);
    		if (input == 0) {
    			printf("退出!\n");
    			return 0;
    		}
    		if (input >= 1 && input <= 4) {
    			printf("请输入两个操作数——>");
    			scanf("%d %d", &x, &y);
    			res = pfArr[input](x, y);
    			printf("Answer is %d\n", res);
    		}
    		else {
    			printf("输入错误!\a\n");
    		}
    	} 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(void) {
	int x = 0, y = 0, res = 0, input = 0;
	// pfArr就是一个函数指针的数组
	int (*pfArr[5])(int, int) = {0, Add, Sub, Mul, Div };   // 也可以叫做转移表
    
     &pfArr; // 对函数指针数组取地址
    
    // PpfArr 就是指向函数指针数组的指针
    // 1. PpfArr 首先和 * 结合,说明他是一个指针
    // 2. 再往外层看,[5], 说明该指针指向的是一个含有五个元素的数组
    // 3. 去掉1.2步分析了的东西,剩下:int (*)(int, int)  ,这是函数指针类型, 说明数组元素是函数指针
    int (*(*PpfArr)[5])(int, int) = &pfArr;  // 相较于函数指针数组,多了一个括号和*
    int (*pfArr[5])(int, int); //  对比函数指针数组的写法
    
    return 0;
}

&y);
res = pfArr[input](x, y);
printf(“Answer is %d\n”, res);
}
else {
printf(“输入错误!\a\n”);
}
} 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(void) {
	int x = 0, y = 0, res = 0, input = 0;
	// pfArr就是一个函数指针的数组
	int (*pfArr[5])(int, int) = {0, Add, Sub, Mul, Div };   // 也可以叫做转移表
    
     &pfArr; // 对函数指针数组取地址
    
    // PpfArr 就是指向函数指针数组的指针
    // 1. PpfArr 首先和 * 结合,说明他是一个指针
    // 2. 再往外层看,[5], 说明该指针指向的是一个含有五个元素的数组
    // 3. 去掉1.2步分析了的东西,剩下:int (*)(int, int)  ,这是函数指针类型, 说明数组元素是函数指针
    int (*(*PpfArr)[5])(int, int) = &pfArr;  // 相较于函数指针数组,多了一个括号和*
    int (*pfArr[5])(int, int); //  对比函数指针数组的写法
    
    return 0;
}

调试

Debug和Release

  • Debug称为调试版本,是程序员自己写代码过程中用的版本,它包含调试信息,并且不作任何优化,便于程序员调试程序,对应的exe文件更大
  • Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用,对应的exe文件更小

VS中的快捷键

  • F5 开始调试

  • F9 创建断点 、取消断点

  • F10 逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。

  • F11 逐语句,一步一步的走,就是每次都执行一条语句,但是这个快捷键可以让我们的执行进入到函数内部

  • CTRL+F5 开始执行,不调试

  • 条件断点:

    在循环内部,比如有一个循环,我知道第五次后可能会出问题,可以右击断点,设置条件断点:

在这里插入图片描述

​ 当且仅当i==5时,才会触发这个断点

调试过程中的操作

F10启动调试后,点击VS上方工具栏,调试->窗口,会有许多功能:
在这里插入图片描述

  • 自动窗口:会自动把程序运行过程中的变量信息在窗口中显示,但是这些局部变量会自动调整,不方便观察

  • 监视:手动输入变量,想观察哪个变量就输入哪个变量

    void test(int a[]){
    	...
    }
    
    int main(void){
    	int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    	test(arr);
    	return 0;
    }
    

    在这里插入图片描述

这样,程序进来过后,因为是传的数组首地址,所以只能看一个元素,为了能够看多个元素,可以用逗号:

在这里插入图片描述

  • 查看内存状态:

    在这里插入图片描述

  • 查看调用堆栈(当一个函数调用另一个函数,而该函数又调用其他函数时):

    在这里插入图片描述

void test2() {
	printf("nihao\n");
}
void test(int a[]) {
	test2();
}
int main(void) {
	int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
	test(a);
	return 0;
}

在这里插入图片描述

main函数调用了test函数,test函数调用了test2函数

  • 查看汇编代码:

    在这里插入图片描述

  • 查看寄存器信息

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2317520.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入理解蒸馏、Function Call、React、Prompt 与 Agent

AI基础概念与实操 一、什么是蒸馏二、如何理解Function Call、React、Prompt与Agent&#xff08;一&#xff09;Function Call与Agent&#xff08;二&#xff09;Agent中的React概念&#xff08;三&#xff09;Prompt与Agent的关联 实操演练function callprompt 一、什么是蒸馏…

记录一个SQL自动执行的html页面

在实际工作场景中&#xff0c;需要运用到大量SQL语句更新业务逻辑&#xff0c;对程序员本身&#xff0c;写好的sql语句执行没有多大问题&#xff08;图1&#xff09;&#xff0c;但是对于普通用户来说还是有操作难度的。因此我们需要构建一个HTML页面&#xff08;图2&#xff0…

qt介绍图表 charts 一

qt chartsj基于Q的Graphics View框架&#xff0c;其核心组件是QChartView和QChart.QChartView是一个显示图表的独立部件&#xff0c;基类为QGraphicsView.QChar类管理图表的序列&#xff0c;图例和轴示意图。 绘制一个cos和sin曲线图&#xff0c;效果如下 实现代码 #include…

Transformer:GPT背后的造脑工程全解析(含手搓过程)

Transformer&#xff1a;GPT背后的"造脑工程"全解析&#xff08;含手搓过程&#xff09; Transformer 是人工智能领域的革命性架构&#xff0c;通过自注意力机制让模型像人类一样"全局理解"上下文关系。它摒弃传统循环结构&#xff0c;采用并行计算实现高…

S32K144入门笔记(十):TRGMUX的初始化

目录 1. 概述 2. 代码配置 1. 概述 书接上回&#xff0c;TRGMUX本质上是一个多路选择开关&#xff0c;根据用户手册中的描述&#xff0c;它可以实现多个输入的选择输出&#xff0c;本篇文章将验证如何通过配置工具来生成初始化配置代码。 2. 代码配置 笔者通过配置TRGMUX实现…

有了大模型为何还需要Agent智能体

一、什么是Agent&#xff1f; Agent&#xff08;智能体&#xff09; 是一种能感知环境、自主决策、执行动作的智能实体&#xff0c;当它与大语言模型&#xff08;如通义千问QWen、GPT&#xff09;结合时&#xff0c;形成一种**“增强型AI系统”**。其核心架构如下&#xff1a;…

DNS主从服务器

1.1环境准备 作用系统IP主机名web 服务器redhat9.5192.168.33.8webDNS 主服务器redhat9.5192.168.33.18dns1DNS 从服务器redhat9.5192.168.33.28dns2客户端redhat9.5192.168.33.7client 1.2修改主机名和IP地址 web服务器 [rootweb-8 ~]# hostnamectl hostname web [rootweb-8…

Flume详解——介绍、部署与使用

1. Flume 简介 Apache Flume 是一个专门用于高效地 收集、聚合、传输 大量日志数据的 分布式、可靠 的系统。它特别擅长将数据从各种数据源&#xff08;如日志文件、消息队列等&#xff09;传输到 HDFS、HBase、Kafka 等大数据存储系统。 特点&#xff1a; 可扩展&#xff1…

【Linux系列】文件压缩

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

微服务架构中10个常用的设计模式

​在当今的微服务架构中&#xff0c;常见的十种设计模式&#xff0c;分别是服务发现模式、API网关模式、断路器模式、边车模式、负载均衡模式、Saga事务模式、CQRS模式、分片模式、分布式日志跟踪模式、熔断与降级模式 。其中&#xff0c;服务发现模式十分关键&#xff0c;通过…

Vue3组件+leaflet,实现重叠marker的Popup切换显示

一、前言 GIS开发过程中&#xff0c;经常需要绘制marker&#xff0c;这些marker很大概率会有坐标相同导致的叠加问题&#xff0c;这种情况下会降低使用体验感。所以我们可以将叠加的marker的popup做一个分页效果&#xff0c;可以切换显示的marker。 二、技术要点 我们以leaf…

机器学习之距离度量方法

常见的距离度量方法及相关函数、图示如下: 1. 欧几里得距离(Euclidean Distance) 函数公式:对于两个 ( n ) 维向量 ( x = ( x 1 , x 2 , ⋯   ,

3.1 在VisionPro脚本中添加CogGraphicLabel

本案例需要实现如下功能&#xff1a; 1.加载toolBlock 2.加载图片&#xff0c; 3.运行Block 4.VisionPro中添加脚本显示数值。 见下图&#xff1a;详细代码&#xff08;C#以及visionPro&#xff09;见下面链接&#xff1a; https://download.csdn.net/download/qq_340474…

AI:Machine Learning Data Science

机器学习与数据科学 左侧 机器学习 Machine Learning 机器学习是一门多领域交叉学科&#xff0c;涉及概率论、统计学、逼近论、凸分析、算法复杂度理论等多门学科。专门研究计算机怎样模拟或实现人类的学习行为&#xff0c;以获取新的知识或技能&#xff0c;重新组织已有的知…

软件需求分类、需求获取(高软46)

系列文章目录 软件需求分类&#xff0c;需求获取 文章目录 系列文章目录前言一、软件需求二、获取需求三、真题总结 前言 本节讲明软件需求分类、需求获取的相关知识。 一、软件需求 二、获取需求 三、真题 总结 就是高软笔记&#xff0c;大佬请略过&#xff01;

嵌入式Linux | 什么是 BootLoader、Linux 内核(kernel)、和文件系统?

01 什么是 BootLoader 呢&#xff1f; 它是个引导程序&#xff0c;也就是硬件复位以后第一个要执行的程序&#xff0c;它主要工作就是初始化操作系统运行的环境&#xff0c;比如说内存、定时器、缓冲器等&#xff0c;当这个工作做完以后&#xff0c;再把操作系统的代码加载…

函数(函数的概念、库函数、自定义函数、形参和实参、return语句、数组做函数参数、嵌套调用和链式访问、函数的声明和定义、static和extern)

一、函数的概念 •C语⾔中的函数&#xff1a;⼀个完成某项特定的任务的⼀⼩段代码 •函数又被翻译为子函数&#xff08;更准确&#xff09; •在C语⾔中我们⼀般会⻅到两类函数&#xff1a;库函数 ⾃定义函数 二、库函数 1 .标准库和头文件 •C语⾔的国际标准ANSIC规定了⼀…

ImGui 学习笔记(五) —— 字体文件加载问题

ImGui 加载字体文件的函数似乎存在编码问题&#xff0c;这一点可能跟源文件的编码也有关系&#xff0c;我目前源文件编码是 UTF-16。 当参数中包含中文字符时&#xff0c;ImGui 内部将字符转换为宽字符字符集时候&#xff0c;采用的 MultiByteToWideChar API 参数不太对&#…

OpenCV计算摄影学(20)非真实感渲染之增强图像的细节函数detailEnhance()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 此滤波器增强特定图像的细节。 cv::detailEnhance用于增强图像的细节&#xff0c;通过结合空间域和频率域的处理&#xff0c;提升图像中特定细节…

Android PC 要来了?Android 16 Beta3 出现 Enable desktop experience features 选项

在之前的 《Android 桌面窗口新功能推进》 我们就聊过&#xff0c;Google 就一直在努力改进 Android 的内置桌面模式&#xff0c;例如添加了适当的窗口标题、捕捉窗口的能力、悬停选项、窗口大小调整、最小化支持、app-to-web 等。 比如在搭载 Android 15 QPR 1 Beta 2 的 Pix…