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

目录

[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 终极测试

如果你觉得自己行了,不妨来做做小编精心给你准备的 "大餐",相信小编,只要你 仔细+耐心 做完,肯定会有所收获的!

点击此前往小编的 gitee 仓库自取。

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就是对 自定义函数 **(即:函数返回类型,函数名,参数列表,具体的定义实现逻辑,是否返回值 ,......,等 全部由程序员控制,有很大的发挥空间)**的简单讲解。

但是在实际的开发过程中,有些基础功能可能是 频繁大量被使用的,比如:

格式化输入和输出(scanfprintf);

常见的数学计算:三角函数(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. 自定义类型

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

比如:

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

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

关注小编,持续更新中!

相关推荐
余额不足1213814 分钟前
C语言基础十六:枚举、c语言中文件的读写操作
linux·c语言·算法
众拾达人15 分钟前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
皓木.17 分钟前
Mybatis-Plus
java·开发语言
不良人天码星17 分钟前
lombok插件不生效
java·开发语言·intellij-idea
源码哥_博纳软云39 分钟前
JAVA同城服务场馆门店预约系统支持H5小程序APP源码
java·开发语言·微信小程序·小程序·微信公众平台
学会沉淀。1 小时前
Docker学习
java·开发语言·学习
西猫雷婶1 小时前
python学opencv|读取图像(二十一)使用cv2.circle()绘制圆形进阶
开发语言·python·opencv
kiiila1 小时前
【Qt】对象树(生命周期管理)和字符集(cout打印乱码问题)
开发语言·qt
罗伯特祥1 小时前
C调用gnuplot绘图的方法
c语言·plot
小_太_阳2 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea