第四次课
一、上节课的回顾
1. 如何使用函数
1.1 如何定义一个函数?
返回值类型 函数名(传入参数) {
方法体
}
// 例如无返回值且无传入参数
void fun1() {
printf("hello world");
}
// 有传入参数而无返回值
void fun2(int a, int b) {
printf("%d + %d = %d", a, b, a + b);
}
// 有传入参数且有返回值
int fun3(int a, int b) {
return a + b;
}
1.2 函数的作用有哪些?
回顾上节课打印图形的案例(直接看三角)
打印正的等腰三角形
*
***
*****
*******
*********
***********
*************
***************
首先我们看到一个三角是由一行一行组成的,所以为了简化问题,我们首先完成打印一行的功能
// len 是整行的长度(星号加空格)
// count 是星号的长度
void printLine(int len, int count) {
// 计算空格的数量
int countOfSpace = len - count;
// 打印开头的空格
for (int i = 0; i < countOfSpace / 2; i++) {
printf(" ");
}
// 打印中间的星号
for (int i = 0; i < count; i++) {
printf("*");
}
// 打印结尾的空格
for (int i = 0; i < countOfSpace / 2; i++) {
printf(" ");
}
printf("\n");
}
int main() {
// 试一下打印效果
printLine(10, 6);
}
在完成打印一行的基础上怎么打印三角?
不就是利用循环语句多打印几行不就行了。
// bottom 是三角形底边的长度
// hight 是三角形高的长度
void printTriangle(int bottom, int hight) {
for (int i = 0; i < hight; i++) {
// 观察发现规律:
// 第一行 1 个星号
// 第二行 3 个星号
// 第三行 5 个星号
//...
// 所以应该知道如何传入参数了吧
printLine(bottom, 2 * i + 1);
}
}
// 试一下
int main() {
printTriangle(15, 8);
}
至于怎么打印倒三角,不就只需将打印正三角的步骤倒过来不就行了,所以我们反向循环
void printReverseTriangle(int bottom, int hight) {
// 这次的循环和上次有什么不同?
for (int i = hight - 1; i >= 0; i--) {
printLine(bottom, 2 * i + 1);
}
}
// 试一下
int main() {
printReverseTriangle(15, 8);
}
最后怎么打印菱形应该很简单了吧!只需先打印正三角再打印倒三角
// len1 是水平对角线的长度
// len2 是垂直对角线的长度
void printDiamond(int len1, int len2) {
// 正三角
printTriangle(len1, (len2 - 1) / 2);
// 对角线
printLine(len1, len1);
// 倒三角
printReverseTriangle(len1, (len2 - 1) / 2);
}
// 试一下
int main() {
printDiamond(11, 11);
}
观察以上的例子?总结函数有什么意义:
- 模块划分(代码复用性高)
- 问题拆分(问题就变成一些简单的问题)
2. 如何使用数组
2.1 为何需要数组
请人回答
2.2 如何定义数组
//第一种声明形式([]内的长度不要乱写)
int arrFirst[3] = {1,2,3};
//第二种声明形式
int arrSecond[] = {1,2,3};
//第三种声明形式
int arrThird[3];
2.3 如何索引数组
// 索引数组
printf("arrFirst[0] = %d", arrFirst[0]);
// 获取数组长度
printf("int 类型占有 %d byte 的空间", sizeof(int));
printf("arrSecond 长度为:%d byte", sizeof(arrSecond) / sizeof(int));
二、今日内容
1. 指针
1.1 什么是指针?
指针也是个变量,就和int
一样是内存里一段连续的空间用来保存数据
只不过这个指针它保存的数据是一个地址
什么是地址?
地址就是一个编号,根据这个编号我们就能找到这个内存单元,所以就是个数字
请记住:指针就是用来保存内存地址的变量
1.2 如何使用指针
int *ip; /* 一个整型的指针 */
double *dp; /* 一个 double 型的指针 */
float *fp; /* 一个浮点型的指针 */
char *ch; /* 一个字符型的指针 */
思考:为什么这个指针还分什么 int、double 什么的?
首先一个指针变量所占的内存空间是多大?
#include <stdio.h>
int main() {
int* a;
double* b;
char* c;
printf("指针 a 的大小:%d byte\n", sizeof(a));
printf("指针 b 的大小:%d byte\n", sizeof(b));
printf("指针 c 的大小:%d byte\n", sizeof(c));
}
我们发现不管什么类型的指针,它的大小都是固定的(64 位机器 8 byte,32 位机器 4 byte)
我们都知道一个int
占有多少个字节?
现在我们以 int 为例,看一下计算机是如何使用指针来工作的:
#include <stdio.h>
int main() {
int num = 123456;
// 将 num 第一个字节的地址给 point
int* point = #
// 输出 num 第一个字节的地址
printf("%#x\n", point);
// 找到 point 所指向的内存单元的内容
printf("%x\n", *point);
// 刚刚说了,point 会根据自己的类型去判断应该将多少个byte当成一个整体
// 这里强转成 char 类型指针再解引用会如何输出呢?
printf("%x\n", *(char*)point);
}
我们可以看到转成 char * 类型后只读取了 num 的一个字节的内容,而且我们还发现我们的操作系统是从数字的低位开始保存的
// 我们声明一个指针时,应当给它一个初值
// 比如我们就习惯赋一个 0,即 NULL
int* p = NULL;
思考:为什么要给一个初值呢?
#include <stdio.h>
int main() {
int * p;
printf("%#x\n", p);
printf("%d\n", *p);
}
我们注意到控制台并没有打印*p
的内容,而且注意到这句话:Process exited after 0.354 seconds with return value 3221225477,这句话的意思是我们的程序运行出错了。
如果我们没有初始化一个指针,那么它就是一个野指针,让我们百度以下野指针:
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。
所以我们这个指针 p 可能会访问一块并不属于这个程序的一块内存,比如是操作系统的一个关键参数,这岂不是很危险?你如果修改这个值,可能会引发操作系统出问题。
不过大家不用担心,现在已经不是 DOS 时代了,你这样的操作,现代的操作系统是有备而来,是不会允许你使用不属于你的内存的,所以程序报错结束了。
思考:为什么要赋值 NULL
因为 NULL 就是 0,这块内存是操作系统占有的,程序不可访问。既然如此以前的程序员就习惯将用它来初始化了。现在虽然指针没那么危险了,不过用 NULL 还是可以提高程序的可读性的。
1.3 特别的指针
函数指针:
我们定义了一个函数:
double fun(int a, char b) {
printf("hello world?\n");
}
大家可能很惊讶居然函数名也能将它看成变量,不过大家回想一下之前说过的——程序是如何运行起来的应该知道我们的程序也是会保存在内存中的某个位置。什么是变量?变量不就是一块内存空间可以用来保存变量嘛!所以函数也可以称之为一个变量。
不过函数这个变量它有点特殊,不像 int 那样固定占多少个字节的空间,函数所占用的空间是不定的,所以我们需要一个指针来表述这个变量。
函数名就可以看作一个指针变量,这个指针指向的是函数里面第一条指令的所在位置。
让我们看一下下面的例子:
#include <stdio.h>
double fun(int a, char b) {
printf("hello world?\n");
}
int main() {
// 定义了一个函数类型的指针
// 格式如下:
// 函数返回值类型 (*指针变量名)(接收的参数类型)
// 一定要小括号,不要写成 double *function(int, char)
double (*function)(int, char) = fun; // 这里的 fun 前可以加 &,不过我认为这样更好理解
// 通过函数指针调用函数
function(1, 2.3);
// 打印函数的入口地址
printf("%#x\n", fun);
printf("%#x\n", function);
}
思考:函数指针有什么用?
有了函数指针,我们就可以将函数当成一个变量作为传递参数使用了。我们在其他编程语言里应该学过回调函数,C 语言通过函数指针就可以实现回调函数。
#include <stdio.h>
void sayHello() {
printf("hello world!\n");
}
void destruct() {
printf("启动自毁程序,3,2,1...");
}
// 可以执行传入的函数
void excuteCommand(void (*command)()) {
printf("接收到指令,即将执行。。。\n");
command();
}
int main() {
excuteCommand(sayHello);
excuteCommand(destruct);
}
是不是很有趣(‾◡◝)
void 类型的指针:
-
void*
是一种特别的指针,因为它没有指向的类型,或者说不能根据这个类型判断出指向对象的长度。 -
定义方式:
void* p;
-
注意事项:
- 任何指针(包括函数指针)都可以赋值给void指针
- void指针赋值给其他类型的指针时都要进行转换
- void指针在强制转换成具体类型前,不能解引用
- void指针不能参与指针运算,除非进行转换
-
作用:
- 用来做函数的传递参数,因为它可以接收任何指针嘛
2. 字符串
字符串是什么?
"hello world!"
上面就是一个字符串
观察这个字符的组成:一系列字符,是不是想到了字符数组
没错!字符串就是一个字符数组。
如何定义一个字符串:
#include <stdio.h>
int main() {
char temp[] = "hello world";
char array[] = {'h','e','l','l','o',' ','w','o','r','l','d','\0'};
printf("%s\n", temp);
printf("%s\n", array);
}
大家喜欢那种定义方式呢?
需要注意的是:第二种定义方式最后有一个\0
,这是很有必要的。这代表了一个字符串的结束。
每一个字符串都是以\0
结尾,即使是第一种定义方式。
#include <stdio.h>
int main() {
char temp[] = "hello world";
printf("%d\n", sizeof(temp)/sizeof(char));
}
注意到输出的是 12,不是 11
现在我们已经学完了指针,可以透过现象看本质:
- 数组的数组名本质就是一个指针变量,这个指针变量指向了数组的第一个元素
所以字符串也有以下定义方式:
#include <stdio.h>
int main() {
char * temp = "hello world";
printf("%s\n", temp);
// 还可以通过指针索引
printf("%c\n", *(temp + 1)); // +1 会根据指针的类型加相应的字节,这里是 char 类型,所以是加 1 个字节
printf("%c\n", temp[1]);
}
还有更多指针的妙用,等待同学的发掘!
三、课堂练习
有n个整数,使得最后m个数变成最前面的m个数。
例子:
-
输入:{1,2,3,4,5,6},m = 1
-
输出:{6,1,2,3,4,5}
要求:空间复杂度O(1),就是不要使用新的数组。