说明:由于所有内容放在一个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);
- n-1 的合理性:
-
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函数
-
查看汇编代码:
-
查看寄存器信息