中国建设银行网站软件下载,重庆网站建设软件,微信小游戏开发软件,百度浏览器主页网址目录
1. 指针
1.1 指针是什么
1.2 指针类型
1.2.1 指针-整数
1.2.2 指针解引用
1.3 const修饰
1.4 字符指针
1.5 指针-指针
1.6 二级指针
2. 数组
2.1 定义和初始化
2.2 下标引用操作符[ ]
2.3 二维数组
2.4 终极测试
3. 函数
3.1 声明和定义
3.2 传值调用…目录
1. 指针
1.1 指针是什么
1.2 指针类型
1.2.1 指针-整数
1.2.2 指针解引用
1.3 const修饰
1.4 字符指针
1.5 指针-指针
1.6 二级指针
2. 数组
2.1 定义和初始化
2.2 下标引用操作符[ ]
2.3 二维数组
2.4 终极测试
3. 函数
3.1 声明和定义
3.2 传值调用和传址掉用
3.3 static静态变量
3.4 数组传参
3.5 库函数
3.6 嵌套调用和链式访问
3.7 声明和定义分离在多文件
3.8 函数递归
4. 自定义类型
5. 常用调试技巧重要 1. 指针
1.1 指针是什么 C代码中的变量函数等在运行时要在内存上开辟空间。 而平时口语中所说的指针通常指的是指针变量是用来存放内存地址的变量属于C语言的内置数据类型。 内存地址是内存中一个最小单元的编号经过仔细的计算和权衡发现一个字节给一个对应的地址是比较合适的。 对于32位的机器假设有32根地址线那么假设每根地址线在寻址的时候产生高电平高电压和低电平低电压就是1或者0. 那么32根地址线产生的地址就会是 所以在32位的机器上地址是32个0或者1组成二进制序列那地址就得用4个字节的空间来存储所以 一个指针变量的大小就应该是4个字节。 那如果在64位机器上如果有64根地址线那一个指针变量的大小是8个字节才能存放一个地 址 。 紧接着就可以通过取地址操作符取出对象的内存起始地址把它存放到一个变量中这个变量就是指针变量然后通过 *解引用操作符就可以找到并访问或编辑对象的数据。指针变量里的数据在解引用时都会被当成地址处理。 定义方法指向数据的类型* 指针变量名 对象 使用示例 如果定义 一个指针变量但是暂时没有合适的指向一般初始化为 NULL 空指针0否则就是 野指针即指向是随机的此时的解引用是 非法的可能引发程序 结果错误甚至终止因为可能造成原内容的覆盖 像VS检查比较严格连编译都不给通过 其次对空指针的解引用也是 非法的会造成运行时终止 1.2 指针类型 通过上面的示例我们发现指针也是有类型的。 char* 类型的指针是为了存放 char 类型变量的地址。 short* 类型的指针是为了存放 short 类型变量的地址。 int* 类型的指针是为了存放 int 类型变量的地址。 ...... 那指针类型的意义是什么
1.2.1 指针-整数 指针的类型决定了指针向前或者向后走一步有多大距离单位字节十进制。 如下示例 注意示例的地址输出都是十六进制 pc —— pc1往后走一个 char 型大小1字节 pi —— pi1往后走一个 int 型大小4字节 ......
1.2.2 指针解引用 指针的类型决定了对指针解引用的时候有多大的权限能操作几个字节。 比如 char* 的指针解引用就只能访问一个字节而 int* 的指针的解引用就能访问四个字节。 如下示例 1.3 const修饰 关于const关键字在【上】篇 常量 中已经介绍过了和修饰变量一样不同的是 举例说明
int main()
{//1int a 10;const int* pa1 a;//const 修饰的是 *pa1即pa1中的地址指向的内存就是a可以使用比如printf(a%d, a 10%d\n, *pa1, *pa1 10);/*不可通过 *指针变量名 对其内容进行修改比如*pa1 10;//相当于a 10*///同样的道理const的位置还可以这样放int const* pa2 a;//但是指针变量pa1, pa2本身可以修改int b 30;pa1 b;//用b的地址 覆盖 a的地址从此以后变量pa1中的地址指向bpa2 pa1;printf(b%d, b%d\n, *pa1, *pa2);//2int* const pa3 a;//const修饰的是变量pa3即不能通过 变量pa3名 来修改其本身的内容比如//pa3 b; pa3 a//但是 pa3中的地址指向的内容即a可以修改比如*pa3 100;printf(a%d\n, a);//3:const int* const pb1 b;//不能通过变量名 / *变量名 改变其本身和其指向的内存return 0;
} 示例输出 还有一点是大家经常会有的疑问如下
const int c 10;
int* pc c;
*pc 300;
printf(c%d\n, c); 输出 有的同学疑惑常变量c不是const修饰吗为什么其内容还是被修改了 但如果你仔细注意我的 措词 就会发现我说的是 “不能通过变量名” 对其内容进行修改既然常变量c的本质还是 变量那么通过其它方式对变量进行修改就是合理合法的这个方式就是 指针。
1.4 字符指针 在指针的类型中我们知道有一种指针类型为字符指针 char* ; 一般使用:
int main()
{char ch w;char *pc ch;*pc h;return 0;} 还有一种使用方式如下 int main()
{//字符串”hello word.在常量区占据一整块连续的内存//不可修改所以要用const修饰*pstr即其指向的内容//取其首字母地址给pstrconst char* pstr hello word.; return 0;} 关于什么是常量区现阶段你只需要知道的是 其内容不可更改 如下图 所以会有如下代码 这里str3和str4指向的是一个同一个常量字符串。C/C会把常量字符串存储到单独的一个内存区域当几个指针 指向同一个字符串的时候他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会 开辟出不同的内存块。所以str1和str2不同str3和str4相同。
1.5 指针-指针 得到数值的绝对值是两指针之间的元素个数。前提是两指针同类型且指向同一块内存 比如前面我们提过 strlen() 计算字符串的长度不包含\0 现在我们来自己实现一下
int main()
{const char* str hello world!;int sln1 strlen(str);//模拟一下const char* p_end str;while (*p_end ! \0)//结束条件*pc \0{p_end;//pc pc 1向后移动一个char大小即一个字节}int sln2 p_end - str;//末 - 初//输出printf(str的开始地址%p\nstr的结束地址%p\n, str, p_end);printf(sln1%d, sln2%d\n, sln1, sln2);return 0;
} 示例输出 图示一下
1.6 二级指针 指针变量也是变量是变量就有地址那么存放一级指针变量的地址的指针就称为二级指针。 比如int a 10 b 20; int* pa a; //pa是一级指针 int** paa pa; //paa是二级指针 *paa b; //二级指针解引用找到一级指针pa即pa b **paa 30; //即*pa 30即b 30; 多级指针亦是如此如果你搞不明白就学着小编画画图。
2. 数组
2.1 定义和初始化 概念一组相同类型元素的集合 语法结构type_t arr_name [N] {exp1, exp2, ...... , expN}; 说明 type_t数组的元素类型 arr_name数组名遵循变量的命名规范作用域和生命周期 N指定数组的大小即元素个数必须是大于0的整数如果不写将根据{...}自动推断 expN表达式的结果 依次 初始化 元素可能发生数据类型转换)其个数N 比如
int main()
{//1定义一个大小为5的整形数组不初始化每个元素是随机值int arr1[5];#define SIZE 10//2定义一个大小为 SIZE 的字符数组并初始化前5个元素分别为A a B b C【十进制整形转化为字符型发生截断】//剩下的元素全部为 \0 是ASCII表中的第一个字符表示空对应整数十进制为0属于 语法特性char arr2[SIZE] { 65, 97, 66, 98, 67};char arr3[SIZE] { A, a, B, b, C };//等价arr2//3初始化字符数组的另一种常用方式char arr4[10] AaBbC;//字符串的结束标志\0也要算进去//4警告//char arr5[2] AaBbC; // 可以运行虽然只分配了 2 个字节的空间但编译器仍然将 AaBbC 这个字符串存储到可能会覆盖紧挨着 arr5 数组后面的内存空间//但这是一种C语言的“未定义行为”可能导致 内存覆盖程序崩溃在不同的编译器、不同的优化级别或者不同的操作系统下结果可能完全不同等潜在问题//所以请遵守expN表达式个数 N数组大小//5对于内置类型当声明一个数组但是没有合适的值立即初始化时好的编程习惯是比如int arr6[10] { 0 };//全部初始化为0//6如果不指定数组大小必须初始化int arr7[] { 1, 2, 3 };//大小为3char arr8[] hello world!;//大小为13包含\0char arr9[] { h, e, l, l, 0, , w, o, r, l, d, ! };//大小为12没有\0return 0;
} 上面的例子中指定数组大小时N都是 常量表达式 事实上N 还可以是 变量表达式即其大小在运行时而非编译时确定所以 不可以在定义的同时对其初始化。这个就叫变长数组Variable Length Array, VLA由C99 标准引入的特性但在 C11 中变成了一个可选特性具体取决于编译器的实现比如微软的VS就不支持gcc和g就可以。 解释一下编译是指将高级编程语言比如C, C, Java等编写的源代码转换为计算机能够直接执行的机器代码或中间代码的过程。 “运行时”就是执行 这些机器代码 的过程。但是 这些机器代码 是给计算机看的作为程序员我们能看懂 且 最熟悉的是自己用高级语言编写的源代码所以我们根据这些源代码就能知道程序在运行时的逻辑。 举个例子
int a 10;
printf(%d, a); 关于这段代码你知道 运行时首先定义并初始化了一个整形变量a紧接着就以整数的形式打印到屏幕上然后程序就结束了。你不需要知道计算机看到的是什么但你可以确定计算机一定是这么干的。 而我们平常说的 “调试代码” 就是把这个执行过程 拆分成 逐语句/逐阶段 的执行以方便找Bug。 现在我们来验证一下对下面的示例代码进行调试 环境Linux下gcc编译gdb调试 另外这里再补充一下之前的内容验证一下const修饰的常变量其本质是变量 利用VS不支持变长数组的特性
2.2 下标引用操作符[ ] C语言规定数组的每个元素都有一个下标下标是从0开始的。 比如 这既是数组的 逻辑结构也是数组在内存中真实的物理存储结构。 这是一段连续的空间 1大小为 sizeof(数组名) 元素个数 * sizeof(元素类型 比如上面示例的数组arr的大小就是int sz_arr sizeof(arr); 2通过操作符 [ ] 和 下标就能实现 快速 且 随机的操作数组元素方式为数组名[下标] 比如
int main()
{int arr[5] {5, 4, 3, 2, 1}; //循环遍历数组int i 0;for (i 0; i 5; i)//不要越界{arr[i] 1;//每个元素加1printf(arr[%d]%d, 地址%p\n, i, arr[i], arr[i]);}/*const int arr[] { 1, 2, 3 };//const修饰必须初始化且元素不可修改*/return 0;
} 示例输出 并且 1数组名就是: 第一个元素的地址 类型就是指针变量: type* pointer pointer[i] 的本质是*(pointer i) 2数组名得到数组的起始地址就是第一个元素的地址 但是类型为数组指针即 指向一个数组的指针变量 1往后走 sizeof(arr) 字节大小的距离 语法结构type (*p)[] //*先和p结合说明p是一个指针变量[ ]说明指向一个数组每个元素类型是 type 如下示例
int main()
{int arr[] { 1, 2, 3, 4, 5 };size_t sz sizeof(arr) / sizeof(int);printf(arr[0]%parr%p\n\n, arr[0], arr);size_t i 0;for (; i sz; i){printf(arr[%d]%p, arr[%d]%d\n, i, arr i, i, *(arri));}int(*p)[5] arr;printf(\np%p\np1%p\n(p1)-p的十进制%d\n, p, p 1, (int)(p1)-(int)p);return 0;
} 示例输出 此外字符数组 打印输出时可以用 %s字符串的形式比如
char arr1[] hello world;
printf(%s\n, arr1);char arr2[6] { h, e, l, l, o, \0 };
printf(arr2);//如果没有结束标志\0可能一直输出程序崩溃 还有注意sizeof的使用 sizeof(数组名)数组名单独放在sizeof()内部这里的数组名表示整个数组计算的是整个数组的大小。 除此之外计算的都是一个指针变量的大小4或8字节。 如下示例
int arr[10] { 0 };
printf(%d, %d, %d\n, sizeof(arr), sizeof(arr 1), sizeof(arr)); 输出x64): 上述就是 一维数组 的简单讲解。
2.3 二维数组 区别于一维数组地方是逻辑结构上。 举个例子
int arr[3][3];//定义一个二维数组三行三列元素个数行数 * 列数 逻辑结构 存储结构 和 每个元素的访问 数组都是连续的存储空间 如果要初始化二维数组的元素有两种方式
int main()
{//方式1int arr1[3][3] { 1, 2, 3, 4, 5 };//依次初始化逻辑结构的每一行int row 0;//行int col 0;//列printf(数组arr1[3][3]\n);for (row 0; row 3; row){printf(第%d行, row1);for (col 0; col 3; col){printf(%d, , arr1[row][col]);}printf(\n);}//方式2int arr2[3][3] { {1, 2}, {3, 4, 5}, {6}};//把每行当成一维数组printf(\n数组arr2[3][3]\n);row 0;while (row 3){printf(第%d行, row1);for (col 0; col 3; col){printf(%d, , arr2[row][col]);}printf(\n);row;}return 0;
} 示例输出 特别注意 如果不指定二维数组的元素个数行可以省略列不能省略编译器根据初始化 { }自动推断。 如下示例
int arr3[][3] { 1, 2, 3, 4 };
int arr4[][4] {{1, 2}, {3, 4, 5}, {6}, {7, 8}} 同样的输出一下 接着往下看 1数组名也是取出整个二维数组的地址类型是type (*p)[][]1往后移动整个二维数组大小字节的距离。 2数组名表示 “第一个元素的地址”即第一行元素一维数组的起始地址类型是 type (*p)[]1往后移动第一行元素的整体大小字节的距离 sizeof(数组名计算整个数组的大小 3arr[i]表示*(arr i)即第i行的起始地址类型是type (*p)[]也表示第 i 行的数组名 那么sizeof(arr[i]计算的就是第 i 行元素的整体大小单位为字节 4arr[i][j] 表示*(*(arri) j)即第 i 行j列的元素 仔细看下面的示例
int main()
{int arr5[3][3] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };//1:int (*p1)[3][3] arr5;printf(arr5[0][0] %p\np1 %p\np11 %p\n, arr5[0][0], p1, p11);printf((int)(p11) - (int)p1 %d\n\n, (int)(p1 1) - (int)p1);//2:printf(arr5%p\narr51%p\n, arr5, arr5 1);printf((int)(arr51) - (int)arr5 %d\n, (int)(arr5 1) - (int)arr5);printf(sizeof(arr5) %d\n\n, sizeof(arr5));//3:printf(arr5[0] %p\narr5[1] %p\narr5[2] %p\n, arr5[0], *(arr5 1), *(arr5 2));printf(sizeof(arr5[0]) %d\nsizeof(arr5[1]) %d\nsizeof(arr5[2]) %d\n\n, sizeof(*(arr5 0)), sizeof(arr5[1]), sizeof(arr5[2]));//4:printf(数组arr5\n);int row sizeof(arr5) / sizeof(arr5[0]);//行int col sizeof(arr5[0]) / sizeof(int);//列int i 0, j 0;for (i 0; i row; i){printf(第%d行, i 1);for (j 0; j col; j){printf(%d , *(*(arr5 i) j));}printf(\n);}return 0;
} 示例输出 如果搞不懂自己画一下 存储结构图。
2.4 终极测试 如果你觉得自己行了不妨来做做小编精心给你准备的 “大餐”相信小编只要你 仔细耐心 做完肯定会有所收获的 点击此前往小编的 gitee 仓库自取。
3. 函数 简单点的概念就是把一段执行特定功能的代码块比如交换两个变量值......进行打包只提供一个使用 接口。 使用时直接调用这个接口就行避免了 程序代码中出现大量此操作 造成的 冗余重复代码 的编译同时也增强了代码的可读性和可维护性。 接着往下看
3.1 声明和定义 语法结构 返回值类型 函数名( 参数列表用逗号分隔) { //定义具体的实现逻辑就像你做数学题的计算过程 } 声明告诉编译器有一个函数叫什么参数是什么返回类型是什么 格式返回值类型 函数名( 参数列表用逗号分隔); 注意声明也是一条语句要加结束符 冒号; 和变量一样先声明后使用。因为编译器默认向上查找。 其次声明可以是全局的也可以是局部的但局部声明的函数只能在对应的局部使用。 如下示例
//实现两个数的相加
//定义
int Add(int left, int right)//接受两个整形参数
{return left right;//返回相加结果整形
}void Print();//声明为全局int main()
{int a 10, b 20;int b Add(a, b);//传参把变量a, b的值拷贝给Add()函数的参数x, y然后把返回的结果拷贝给bprintf(a b %d\n, b);//这里把Sub声明为局部int Sub(int, int);//声明的参数表可以只写类型甚至随便取名都行比如int Sub(int a或b或val1或...... , int b或a或val2或......) 只要不重名就行int c Sub(a, b);printf(a - b %d\n, a - b);Print();//调用return 0;
}//有的教科书喜欢写在后面真的很鸡肋极其不推荐
//实现两个数的相减
//定义
int Sub(int left, int right)
{return left - right;
}//定义
void Printf()//void 可以不用返回
{printf(hello world!\n);return;//也可以不写
} 输出 推荐函数的声明一般都是 全局的定义一般都在main函数之前 注意函数不能嵌套定义即一个函数体的 { } 里面不能再定义其它函数main()函数也是如此 如下错误示例
int test(int x)
{int Test(){;//......}return x;
}int main()
{void Print(){;//......}return 0;
}
3.2 传值调用和传址掉用 首先明确 实参和形参 的概念 输出 这个就叫做 传值调用形参的改变 不会影响 实参。 举个常见的例子交换两个变量的值 错误的写法传值
void Swap(type x, type y)
{type temp x;x y;y tmp;//y x;
} 而正确的写法是传地址。如下 3.3 static静态变量 普通的局部变量 在函数返回后就销毁。 但是 static修饰的变量的生命周期是整个项目程序代码的生命周期并且仅初始化1次 举个例子
int* Test()
{static int n 0;//n;return n;//变量不销毁返回其地址是合法的
}int main()
{int i 0;int* p NULL;for (; i 10; i)//循环调用10次Test函数{p Test();}//输出静态变量nprintf(Test() :: n %d\n, *p);return 0;
} 输出 此外同 const 一样static修饰全局变量时默认 内部链接属性此处不再赘述。 不同的是const 主要用于强调变量的值不可随意修改而 static 则用于控制变量生命周期二者的使用场景有明显的侧重。
3.4 数组传参 数组名传参本质是 拷贝数组首元素的地址 给 形式参数所以 这个形参 是一个 指针变量在函数内部 的 sizeof(形参计算的是一个指针变量的大小为4或8字节 如下示例
void Test(int* arr)
{printf(sizeof(arr) %d\n, sizeof(arr));//元素访问的方式依旧是arr[下标] 或者 *(arr i)
}int main()
{int arr[] { 1, 2, 3 };Test(arr);//首元素类型为int地址是int*return 0;
} 输出x64) 另一种常用的写法
void Test(int arr[])
{//......
} 也是如此。 下面看一下二维数组
void Test(int (*arr)[3])
{printf(sizeof(arr) %d\n, sizeof(arr));//元素的访问方式依旧是arr[下标][下标] 或者 *(arr i)[下标] 或者 *(arr[下标] i或者 *(*(arr i) j)
}int main()
{int arr[][3] {1, 2, 3, 4};Test(arr);//首元素的地址即第一行 “一维数组”的地址类型为数组指针int*p)[3]return 0;
} 输出x64) 另外的常用写法
//列不能省略
void Test2(int arr[][3])
{//......
}void Test3(int arr[2][3])
{//......
}
3.5 库函数 上面的 3.1和3.2就是对 自定义函数 即函数返回类型函数名参数列表具体的定义实现逻辑是否返回值 ......等 全部由程序员控制有很大的发挥空间的简单讲解。 但是在实际的开发过程中有些基础功能可能是 频繁大量 被使用的比如 格式化输入和输出scanf 和 printf) 常见的数学计算三角函数(sin, cos, tan)pow(n次幂)sqrt(平方根)abs(计算绝对值) ...... 字符串操作strlen(字符串的长度)strcmp(比较字符串是否相等)strcpy(拷贝)...... 内存操作memcpy(以字节为单位将内容拷贝到另一块内存块...... ...... 所以为了提高开发效率C语言提前将这些功能写好并打包归类到特定的库中就叫 C库不同的功能实现就叫 库函数 使用方式#include特定库.h头文件 因为在你配置本地C/C开发环境的同时C/C库就被下载到你的本地PC上被编译链接的代码程序根据相应的路径就能找到并使用这个就叫 动态链接。 所以在《快速上手C语言【上】》一文中 小编说过如果你要把你本地编译好的可执行程序发送给别人运行就要 静态链接 即把我们这里说的库文件打包编译到一起因为别人的PC设备上不一定有对应的运行环境即使有所需库文件路径也和你的不一样导致程序找不到。 关于 动静态链接再举个形象的例子网吧属于公共场合大家只需要知道 它的地址随时都能去这就是 动态链接“大家” 都能用但如果你的大学在荒郊野外方圆几十里找不到一家网吧此时大家的做法是 每人都自备一台个人PC那么以后不管你去到哪里都可以独自使用这就是 静态链接相当于“绑定”了。 搞清楚上面的东西后现在的重点是怎么使用 和自定义函数一样我们关注的东西依旧是 1功能是什么 2 参数列表 3是否有返回如果有返回什么 外加一个4在哪个库文件中 这里用大家熟悉的 scanf和printf 来示例 所以我可以这样写代码
int main()
{int a, b, num_in 0;#define Format_in %d %d//宏常量输入格式const char Format_out[] %d %d %d; ;//常量字符串输出格式int* p1 a, * p2 b;while ((num_in scanf(Format_in, p1, p2)) num_in ! EOF)//实现循环输入{getchar();//把\n读走避免可能发生错误//成功读到num个数printf(成功写入的数据个数num_in %d\n, num_in);//相加输出int num_out printf(Format_out, *p1, *p2, *p1 *p2);printf(本行输出字符数num_out %d\n, num_out);}//输入结束printf(num_in scanf(......) %d, 结束\n, num_in);return 0;
}//所以字符串的输出可以直接printf(str); 示例输出 其它的库函数怎么学这里给大家贴一个查询浏览文档的网页版 C library - C Reference (cplusplus.com) 学着自己看文档也是一项必备技能
3.6 嵌套调用和链式访问 函数和函数之间可以根据实际的需求进行组合的也就是互相调用的。 嵌套调用 其实 我们之前一直在用比如 main 函数调用其它函数这里就不赘述了 这里我们着重看一下 链式访问把一个函数的返回值作为另外一个函数的参数 举个例子
//结果是什么
printf(%d, printf(%d, printf(%d, 43))); 思路从内到外 printf(%d, 43)首先在屏幕上打印43返回字符数2 作为下个printf 的参数继续打印2返回字符数1 作为最外层printf的参数继续打印1返回字符数1 所以最后的结果是4321 ...... 其它更丰富的场景就留给你探索吧
3.7 声明和定义分离在多文件 实际的项目生产中都是多文件分离的因为这样逻辑更清晰可读性好可维护性也强。 示例在同一文件目录下 test.h头文件
//头文件的包含
#includestdio.h
//......//宏
#define NUM 10//函数声明
int Add(int, int); test.c源文件
#includetest.h//非标准库的头文件使用 双引号 int main()
{printf(%d\n, Add(NUM, 20));return 0;
} function.c源文件
//函数定义
int Add(int left, int right)
{return left right;
} 至于为什么可点击跳转小编的另一篇文章《程序环境和预处理详解》。
3.8 函数递归 简单点说就是自己在函数内部调用自己 有两个必要条件1存在终止条件停止递归 2每次递归调用之后越来越接近这个结束条件 直接上示例求 n 的阶乘不考虑溢出 结果等于 1 * 2 * 3 * ...... * n 常用方法循环
int main()
{int i 0, answer 1, n 0;scanf(n%d, n);for (i 1; i n; i){answer * i;}printf(factorialn) %d\n, answer);return 0;
} 递归n的阶乘等于 n * (n - 1)
int factorial(int n)
{if (1 n)return 1;elsereturn n * factorial(n - 1);
}int main()
{int n 0, answer 0;scanf(n%d, n);answer factorial(n);printf(factorialn) %d\n, answer);return 0;
} 画一下 递归展开图 再举个例子 依次打印一个无符号整数的每一位
void Print(size_t x)
{//如果是多位数就继续拆分if (x 9){Print(x / 10);}printf(%d , x % 10);
} 同样的跟小编一起画下 递归展开图 下面再来一个用递归模拟实现 strlen()
size_t strlen(const char* str)
{if (\0 *str)return 0;elsereturn 1 strlen(str 1);
} 同样的道理递归展开图交给你吧。 ...... 虽然许多问题 用递归来写更清晰和简洁但是这些问题的迭代实现往往比递归实现效率更高 因为 每一次的函数调用 都需要 一定的性能开销递归层次太深就会造成开销过大效率降低 举个例子求第n个斐波那契数 说明11 2 3 5......从第三个数开始每个数是前两个数的 和 所以第n个斐波那契数 fib(n) fib(n - 1) fib(n - 2) 【不考虑溢出】 递归的写法
size_t count 0;
size_t fib_r(size_t n)
{count;//记录这个函数的调用次数if (n 3)return 1;elsereturn fib_r(n - 1) fib_r(n - 2);
} 循环迭代写法 代码实现
size_t fib_it(size_t n)
{int a 1, b 1, c 1;while (n 2)//循环n-2次{c a b;a b;b c;--n;}return c;
} 现在写段代码来测试一下
#includetime.h
int main()
{size_t n 0;scanf(n%u, n);//输入n 0int start1 time(0);//简单记录开始结束时间戳用于计算时间消耗单位为 秒size_t fib1 fib_r(n);int end1 time(0);int start2 time(0);size_t fib2 fib_it(n);int end2 time(0);//输出printf(递归\nfib_r(%u) %u, 函数调用次数%u, 时间%d\n\n, n, fib1, count, end1 - start1);printf(循环迭代\nfib_it(%u) %u时间%d\n, n, fib2, end2 - start2);return 0;
} 示例输出 才计算第50个数递归的函数调用了接近37亿次小编的机器本次花了1分12秒千万不要用我们的感觉来衡量计算机的 速度 但是迭代没有函数调用时间连1秒都不到 所以不是所有的问题都适合用 递归来解决还是要根据具体的场景来决定用哪个。
4. 自定义类型 点击此跳转小编的另一篇文章《C语言---自定义类型详解》
5. 常用调试技巧重要 对于新手小白而言遇到问题的第一的反映是看书查资料或者去各大网络平台上发帖求助让别人 帮 自己找问题。 这其中除了 基础语法知识的掌握不牢靠外更为重要的原因是 缺乏自主定位问题再解决问题的 思想觉悟和能力这就叫 “调试代码” 而限制其的一个重大因素就是不会使用 和 不能充分使用 编译开发工具 这就是为什么小编经常建议新手使用 Visual Studio 的原因有以下三点 1. 官方的长期维护更新可靠 2. 集成的开发环境可按需勾选下载需要的组件自动配置降低开发环境搭建成本 简单展示一下 如果你是第一次安装 如果你以后还要安装其它的服务可找到先前下载的 VisualStudioSetup.exe 程序 3. 丰富的功能按钮 并支持可视化 这里小编重点对点3进行举例说明大家常用的功能 新建项目 创建 .c/.h文件写代码 首先模式的选择 所以我们日常写代码找bug 是在Debug模式下运行。 其次快捷键的使用 有的机器需搭配Fn F9打断点/删除断点。即程序运行到有断点的一行就停下 F5开始调试。遇到断点就停下 F10: 逐过程。不会进入到具体的函数体 F11: 逐语句。进入函数体查看具体的实现逻辑 Ctrl F5: 直接运行。忽略所有断点 Ctrl Shift F9删除所有断点 开始调试后常用的两个窗口 举例说明 有的时候需要调试大量的循环不可能一次一次的走此时可以用 条件断点 如下 其次可以通过 汇编 代码来查看底层实现比如 这下你可以直观的感知到 Debug和Release的 区别了吧。 所以学会看汇编代码其实有助于帮助我们 理解和掌握 知识。 举个例子C中的 引用在语法上 就是取别名不占内存空间但是通过汇编代码发现其本质是用 指针实现的要开辟内存空间。 ...... 更多的体会和感悟 还需要你自己的深入学习和实践。 下面就是 常见的错误信息 1. 编译型错误 常见的就是语法错误比如变量的作用域和生命周期中文书写忘记写语句的结束符 函数传参不对赋值类型不匹配强制转换也没有用...... 如下示例 或者 2. 链接型错误 常见函数只有声明没有定义 3. 运行时错误 常见 1. 段错误Segmentation fault 访问了不属于自己的内存地址通常是访问了未初始化的指针野指针或者数组越界但是数组越界不一定会报错但还是应该避免。 比如
int* p;
*p 10;int arr[10] { 0 };
int i 0;
for (i 0; i 12; i)
{arr[i] 1;
} 2. 缓冲区溢出Buffer overflow向数组写入超过其容量的数据导致覆盖了其他内存区域的数据。 比如 3. 除零错误Division by zero在除法运算中除数为零导致运行时错误。 比如 4. 栈溢出Stack overflow递归调用层数过多导致栈空间不足。 比如 5. 空指针错误Null pointer dereference对空指针进行解引用操作导致运行时错误。 比如 6. 内存泄漏Memory leak未正确释放动态分配的内存导致内存使用量不断增加最终导致系统资源不足。 7.重复释放已经释放的空间野指针 6和7涉及到动态内存的管理如果你有兴趣可点击此跳转小编的另一篇文章。 ...... 现在你知道小编为什么推荐使用VS了吧更多的功能留给大家自己探索因为不管干什么只看不练也是白搭。 如果你和小编一样喜欢折腾喜欢探索新事物那么 VsCode 小编也是推荐的因为它更轻量可扩展性更丰富。对应的C/C开发环境的配置指南和所需组件源小编 也为大家准备好了点击以下链接免费下载。 【免费】VsCode配置C/C环境_vscode配置c/c环境资源-CSDN文库 本文到此结束如果对您有所帮助就是对小编最大的鼓励可以的话点赞关注收藏并分享给你的好友一起学习吧当然也欢迎您在评论区积极交流这将转化为我的不懈动力 关注小编持续更新中