目录
[1. 指针](#1. 指针)
[1.1 指针是什么](#1.1 指针是什么)
[1.2 指针类型](#1.2 指针类型)
[1.2.1 指针+-整数](#1.2.1 指针+-整数)
[1.2.2 指针解引用](#1.2.2 指针解引用)
[1.3 const修饰](#1.3 const修饰)
[1.4 字符指针](#1.4 字符指针)
[1.5 指针-指针](#1.5 指针-指针)
[1.6 二级指针](#1.6 二级指针)
[2. 数组](#2. 数组)
[2.1 定义和初始化](#2.1 定义和初始化)
[2.2 下标引用操作符[ ]](#2.2 下标引用操作符[ ])
[2.3 二维数组](#2.3 二维数组)
[2.4 终极测试](#2.4 终极测试)
[3. 函数](#3. 函数)
[3.1 声明和定义](#3.1 声明和定义)
[3.2 传值调用和传址掉用](#3.2 传值调用和传址掉用)
[3.3 static静态变量](#3.3 static静态变量)
[3.4 数组传参](#3.4 数组传参)
[3.5 库函数](#3.5 库函数)
[3.6 嵌套调用和链式访问](#3.6 嵌套调用和链式访问)
[3.7 声明和定义分离在多文件](#3.7 声明和定义分离在多文件)
[3.8 函数递归](#3.8 函数递归)
[4. 自定义类型](#4. 自定义类型)
[5. 常用调试技巧(重要!!!)](#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关键字,在【上】篇 常量 中已经介绍过了;和修饰变量一样,不同的是:
举例说明:
cpp
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;
}
示例输出:
还有一点是大家经常会有的疑问:如下:
cpp
const int c = 10;
int* pc = &c;
*pc = 300;
printf("c=%d\n", c);
输出: 有的同学疑惑:常变量c不是const修饰吗,为什么其内容还是被修改了?
但如果,你仔细注意我的 措词 就会发现,我说的是 "不能通过变量名" 对其内容进行修改;既然常变量c的本质还是 变量,那么通过其它方式对变量进行修改就是合理合法的,这个方式就是 指针。
1.4 字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char* ;
一般使用:
cpp
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'h';
return 0;
}
还有一种使用方式如下:
cpp
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'
现在我们来自己实现一下:
cpp
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
比如:
cpp
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等)编写的源代码转换为计算机能够直接执行的机器代码(或中间代码)的过程。
"运行时"就是执行这些机器代码 的过程。但是 这些机器代码 是给计算机看的,作为程序员,我们能看懂 且 最熟悉的是自己用高级语言编写的源代码,所以我们根据这些源代码就能知道程序在运行时的逻辑。
举个例子:
cpp
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: 通过操作符 [ ] 和 下标,就能实现 快速 且 随机的操作数组元素,方式为:数组名[下标]
比如:
cpp
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
如下示例:
cpp
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,字符串的形式,比如:
cpp
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字节。
如下示例:
cpp
int arr[10] = { 0 };
printf("%d, %d, %d\n", sizeof(arr), sizeof(arr + 1), sizeof(&arr));
输出(x64):
上述就是 一维数组 的简单讲解。
2.3 二维数组
区别于一维数组地方是:逻辑结构上。
举个例子:
cpp
int arr[3][3];//定义一个二维数组,三行三列,元素个数==行数 * 列数
逻辑结构:
存储结构 和 每个元素的访问:
数组都是连续的存储空间!
如果要初始化二维数组的元素,有两种方式:
cpp
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;
}
示例输出:
特别注意: 如果不指定二维数组的元素个数,行可以省略,列不能省略,编译器根据初始化 { }自动推断。
如下示例:
cpp
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列的元素
仔细看下面的示例:
cpp
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 终极测试
如果你觉得自己行了,不妨来做做小编精心给你准备的 "大餐",相信小编,只要你 仔细+耐心 做完,肯定会有所收获的!
3. 函数
简单点的概念:就是把一段执行特定功能的代码块(比如交换两个变量值,......),进行打包,只提供一个使用 接口。
使用时直接调用这个接口就行,避免了 程序代码中出现大量此操作 造成的 冗余重复代码 的编译,同时也增强了代码的可读性 和可维护性。
接着往下看:
3.1 声明和定义
语法结构:
返回值类型 函数名( 参数列表,用逗号分隔)
{
//定义,具体的实现逻辑,就像你做数学题的计算过程
}
声明:告诉编译器有一个函数叫什么,参数是什么,返回类型是什么;
格式:返回值类型 函数名( 参数列表,用逗号分隔);
注意:声明也是一条语句,要加结束符 冒号(;)
和变量一样,先声明,后使用。因为编译器默认向上查找。
其次,声明可以是全局的,也可以是局部的,但局部声明的函数,只能在对应的局部使用。
如下示例:
cpp
//实现两个数的相加
//定义
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()函数也是如此
如下错误示例:
cpp
int test(int x)
{
int Test()
{
;//......
}
return ++x;
}
int main()
{
void Print()
{
;//......
}
return 0;
}
3.2 传值调用和传址掉用
首先明确实参 和形参 的概念:
输出:
这个就叫做传值调用,形参的改变 不会影响 实参。
举个常见的例子:交换两个变量的值
错误的写法:传值
cpp
void Swap(type x, type y)
{
type temp = x;
x = y;
y = tmp;//y = x;
}
而正确的写法是:传地址。如下:
3.3 static静态变量
普通的局部变量 在函数返回后就销毁。
但是 static修饰的变量的生命周期是****整个项目程序代码的生命周期;并且仅初始化1****次!
举个例子:
cpp
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修饰全局变量时,默认 内部链接属性,此处不再赘述。
不同的是,const
主要用于强调 变量的值不可随意修改,而 static
则用于控制变量生命周期,二者的使用场景有明显的侧重。
3.4 数组传参
数组名传参**,本质是 拷贝数组首元素的地址 给 形式参数,所以 这个形参 是一个 指针变量,在函数内部 的sizeof(形参)计算的是一个指针变量的大小,为4或8字节!**
如下示例:
cpp
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):
另一种常用的写法:
cpp
void Test(int arr[])
{
//......
}
也是如此。
下面看一下,二维数组:
cpp
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):
另外的常用写法:
cpp
//列不能省略
void Test2(int arr[][3])
{
//......
}
void Test3(int arr[2][3])
{
//......
}
3.5 库函数
上面的 3.1和3.2就是对 自定义函数 **(即:函数返回类型,函数名,参数列表,具体的定义实现逻辑,是否返回值 ,......,等 全部由程序员控制,有很大的发挥空间)**的简单讲解。
但是在实际的开发过程中,有些基础功能可能是 频繁大量被使用的,比如:
常见的数学计算:三角函数(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来示例:
所以,我可以这样写代码:
cpp
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 函数调用其它函数,这里就不赘述了
这里,我们着重看一下 链式访问:把一个函数的返回值作为另外一个函数的参数
举个例子:
cpp
//结果是什么?
printf("%d", printf("%d", printf("%d", 43)));
思路:从内到外
printf("%d", 43)首先在屏幕上打印43,返回字符数2 作为下个printf 的参数,继续打印2,返回字符数1 作为最外层printf的参数,继续打印1,返回字符数1
所以最后的结果是:4321
......
其它更丰富的场景就留给你探索吧!
3.7 声明和定义分离在多文件
实际的项目生产中,都是多文件分离的,因为这样逻辑更清晰,可读性好,可维护性也强。
示例:在同一文件目录下
test.h头文件:
cpp
//头文件的包含
#include<stdio.h>
//......
//宏
#define NUM 10
//函数声明
int Add(int, int);
test.c源文件:
cpp
#include"test.h"//非标准库的头文件使用 双引号 " "
int main()
{
printf("%d\n", Add(NUM, 20));
return 0;
}
function.c源文件:
cpp
//函数定义
int Add(int left, int right)
{
return left + right;
}
至于为什么,可点击跳转小编的另一篇文章《程序环境和预处理详解》。
3.8 函数递归
简单点说,就是:自己在函数内部调用自己
有两个必要条件:1:存在终止条件,停止递归
2:每次递归调用之后越来越接近这个结束条件
直接上示例:求 n 的阶乘(不考虑溢出)
结果等于 1 * 2 * 3 * ...... * n
常用方法:循环
cpp
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)
cpp
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;
}
画一下 递归展开图:
再举个例子: 依次打印一个无符号整数的每一位
cpp
void Print(size_t x)
{
//如果是多位数,就继续拆分
if (x > 9)
{
Print(x / 10);
}
printf("%d ", x % 10);
}
同样的,跟小编一起画下 递归展开图:
下面,再来一个:用递归模拟实现 strlen()
cpp
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)
【不考虑溢出】
递归的写法:
cpp
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);
}
循环迭代写法: 代码实现:
cpp
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;
}
现在写段代码来测试一下:
cpp
#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. 自定义类型
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): 访问了不属于自己的内存地址,通常是访问了未初始化的指针(野指针)或者数组越界;但是数组越界不一定会报错,但还是应该避免。
比如:
cpp
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文库
本文到此结束,如果对您有所帮助,就是对小编最大的鼓励,可以的话,点赞,关注+收藏并分享给你的好友一起学习吧;当然,也欢迎您在评论区积极交流,这将转化为我的不懈动力!
关注小编,持续更新中!