快速上手C语言【下】(非常详细!!!)

CSDN 2024-10-27 08:35:02 阅读 89

目录

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 ——> pc+1:往后走一个 char 型大小,1字节

  pi ——> pi+1:往后走一个 int 型大小,4字节

  ...... 

1.2.2 指针解引用

  指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。

  比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。 

  如下示例:

1.3 const修饰 

  关于const关键字,在【上】篇 常量 中已经介绍过了;和修饰变量一样,不同的是: 

  举例说明:

<code>int main()

{

//1:

int 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中的地址指向b

pa2 = pa1;

printf("b=%d, b=%d\n", *pa1, *pa2);

//2:

int* 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;

}

  示例输出:

   

  还有一点是大家经常会有的疑问:如下: 

<code>const int c = 10;

int* pc = &c;

*pc = 300;

printf("c=%d\n", c);

  输出:

   有的同学疑惑:常变量c不是const修饰吗,为什么其内容还是被修改了?

   但如果,你仔细注意我的 措词 就会发现,我说的是 “不能通过变量名” 对其内容进行修改;既然常变量c的本质还是 变量,那么通过其它方式对变量进行修改就是合理合法的,这个方式就是 指针。

1.4 字符指针

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

  一般使用: 

<code>int main()

{

    char ch = 'w';

    char *pc = &ch;

    *pc = 'h';

     return 0;

}

  还有一种使用方式如下:

 int main()

{

        //字符串”hello word."在常量区,占据一整块连续的内存

//不可修改,所以要用const修饰*pstr,即其指向的内容

//取其首字母地址给pstr

const char* pstr = "hello word.";  

return 0;

}

   (关于什么是常量区,现阶段你只需要知道的是 其内容不可更改) 

    如下图:

  所以会有,如下代码:

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

1.5 指针-指针

  得到数值的绝对值是:两指针之间的元素个数。(前提是两指针同类型且指向同一块内存) 

  比如,前面我们提过 strlen() 计算字符串的长度,不包含'\0'

  现在我们来自己实现一下:

<code>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

  比如:

<code>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 };//大小为3

char arr8[] = "hello world!";//大小为13,包含'\0'

char arr9[] = { 'h', 'e', 'l', 'l', '0', ' ', 'w', 'o', 'r', 'l', 'd', '!' };//大小为12,没有'\0'

return 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:通过操作符 [ ] 和 下标,就能实现 快速 且 随机的操作数组元素,方式为:数组名[下标] 

  比如:

<code>int main()

{

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

//循环遍历数组

int i = 0;

for (i = 0; i < 5; i++)//不要越界

{

arr[i] += 1;//每个元素加1

printf("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

  如下示例:

<code>int main()

{

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

size_t sz = sizeof(arr) / sizeof(int);

printf("&arr[0]=%p;arr=%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, *(arr+i));

}

int(*p)[5] = &arr;

printf("\np=%p\np+1=%p\n(p+1)-p的十进制=%d\n", p, p + 1, (int)(p+1)-(int)p);

return 0;

}

  示例输出: 

   此外,字符数组 打印输出时,可以用 %s,字符串的形式,比如:

<code>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 二维数组 

  区别于一维数组地方是:逻辑结构上。

  举个例子:

<code>int arr[3][3];//定义一个二维数组,三行三列,元素个数==行数 * 列数

  逻辑结构:

 

  存储结构 和 每个元素的访问:

   数组都是连续的存储空间! 

   如果要初始化二维数组的元素,有两种方式:

<code>int main()

{

//方式1:

int 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行:", row+1);

for (col = 0; col < 3; col++)

{

printf("%d, ", arr1[row][col]);

}

printf("\n");

}

//方式2:

int arr2[3][3] = { {1, 2}, {3, 4, 5}, {6}};//把每行当成一维数组

printf("\n数组arr2[3][3]:\n");

row = 0;

while (row < 3)

{

printf("第%d行:", row+1);

for (col = 0; col < 3; col++)

{

printf("%d, ", arr2[row][col]);

}

printf("\n");

++row;

}

return 0;

}

   示例输出:

  特别注意: 如果不指定二维数组的元素个数,行可以省略,列不能省略,编译器根据初始化 { }自动推断。

  如下示例:

<code>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(数组名)计算整个数组的大小 

  3:arr[i]表示:*(arr +i),即第i行的起始地址,类型是type (*p)[],也表示第 i 行的数组名

        那么,sizeof(arr[i])计算的就是第 i 行元素的整体大小,单位为字节

  4:arr[i][j] 表示:*(*(arr+i) + j),即第 i 行,j列的元素

  仔细看下面的示例:

<code>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\np1+1 = %p\n", &arr5[0][0], p1, p1+1);

printf("(int)(p1+1) - (int)p1 = %d\n\n", (int)(p1 + 1) - (int)p1);

//2:

printf("arr5=%p\narr5+1=%p\n", arr5, arr5 + 1);

printf("(int)(arr5+1) - (int)arr5 = %d\n", (int)(arr5 + 1) - (int)arr5);

printf("sizeof(arr5) = %d\n\n", sizeof(arr5));

//3:

printf("&arr5[0] = %p\n&arr5[1] = %p\n&arr5[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 声明和定义

  语法结构:

  返回值类型    函数名( 参数列表,用逗号分隔)

  {

        //定义,具体的实现逻辑,就像你做数学题的计算过程

  } 

   声明:告诉编译器有一个函数叫什么,参数是什么,返回类型是什么;

        格式:返回值类型    函数名( 参数列表,用逗号分隔);

        注意:声明也是一条语句,要加结束符 冒号(;)

  和变量一样,先声明,后使用。因为编译器默认向上查找。

  其次,声明可以是全局的,也可以是局部的,但局部声明的函数,只能在对应的局部使用。

  如下示例:

<code>//实现两个数的相加

//定义

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;然后把返回的结果拷贝给b

printf("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()函数也是如此 

  如下错误示例: 

<code>int test(int x)

{

int Test()

{

;//......

}

return ++x;

}

int main()

{

void Print()

{

;//......

}

return 0;

}

3.2 传值调用和传址掉用

  首先明确 实参形参 的概念:

  输出:

  这个就叫做 传值调用,形参的改变 不会影响 实参。 

  举个常见的例子:交换两个变量的值

  错误的写法:传值

<code>void Swap(type x, type y)

{

type temp = x;

x = y;

y = tmp;//y = x;

}

  而正确的写法是:传地址。如下: 

3.3 static静态变量 

  普通的局部变量 在函数返回后就销毁。

  但是 static修饰的变量的生命周期是整个项目程序代码的生命周期;并且初始化1

  举个例子:

<code>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();

}

//输出静态变量n

printf("Test() :: n = %d\n", *p);

return 0;

}

   输出:

  此外,同 const 一样,static修饰全局变量时,默认 内部链接属性,此处不再赘述。 

  不同的是,<code>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):

  另一种常用的写法:

<code>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):

   另外的常用写法:

<code>//列不能省略

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  来示例: 

  所以,我可以这样写代码: 

<code>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)  

  

  学着自己看文档也是一项必备技能! 

  另外,常用的《C语言---字符串+内存函数的详解整理》 ,小编也给你准备好了,可点击此跳转!​​​​​​​

3.6 嵌套调用和链式访问 

  函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。

  嵌套调用 其实 我们之前一直在用,比如 main 函数调用其它函数,这里就不赘述了

  这里,我们着重看一下 链式访问:把一个函数的返回值作为另外一个函数的参数

  举个例子:

<code>//结果是什么?

printf("%d", printf("%d", printf("%d", 43)));

 思路:从内到外

 printf("%d", 43)首先在屏幕上打印43,返回字符数2 作为下个printf 的参数,继续打印2,返回字符数1 作为最外层printf的参数,继续打印1,返回字符数1

  所以最后的结果是:4321

  ......

  其它更丰富的场景就留给你探索吧!

3.7 声明和定义分离在多文件 

  实际的项目生产中,都是多文件分离的,因为这样逻辑更清晰,可读性好,可维护性也强。 

  示例:在同一文件目录下

  test.h头文件:

//头文件的包含

#include<stdio.h>

//......

//宏

#define NUM 10

//函数声明

int Add(int, int);

   test.c源文件:

#include"test.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("factorial(n) = %d\n", answer);

return 0;

}

  递归:n的阶乘等于 n * (n - 1)

int factorial(int n)

{

if (1 == n)

return 1;

else

return n * factorial(n - 1);

}

int main()

{

int n = 0, answer = 0;

scanf("n=%d", &n);

answer = factorial(n);

printf("factorial(n) = %d\n", answer);

return 0;

}

  画一下 递归展开图: 

  再举个例子: 依次打印一个无符号整数的每一位

<code>void Print(size_t x)

{

//如果是多位数,就继续拆分

if (x > 9)

{

Print(x / 10);

}

printf("%d ", x % 10);

}

  同样的,跟小编一起画下 递归展开图:

  下面,再来一个:用递归模拟实现 strlen() 

<code>size_t strlen(const char* str)

{

if ('\0' == *str)

return 0;

else

return 1 + strlen(str + 1);

}

  同样的道理,递归展开图交给你吧。 

  ......

  虽然许多问题 用递归来写更清晰和简洁,但是这些问题的迭代实现往往比递归实现效率更高 ,因为 每一次的函数调用 都需要 一定的性能开销,递归层次太深就会造成开销过大,效率降低!

  举个例子:求第n个斐波那契数

  说明:1,1, 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;

else

return fib_r(n - 1) + fib_r(n - 2);

}

  循环迭代写法:

   代码实现:

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

}

  现在写段代码来测试一下:

#include<time.h>

int main()

{

size_t n = 0;

scanf("n=%u", &n);//输入n > 0

int 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): 访问了不属于自己的内存地址,通常是访问了未初始化的指针(野指针)或者数组越界;但是数组越界不一定会报错,但还是应该避免。

  比如:

<code>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文库

  本文到此结束,如果对您有所帮助,就是对小编最大的鼓励,可以的话,点赞,关注+收藏并分享给你的好友一起学习吧;当然,也欢迎您在评论区积极交流,这将转化为我的不懈动力!

  关注小编,持续更新中! 



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。