❀本篇是翁恺老师-C语言听课笔记。
C语言
- 0 前言
- 第一章 基础知识
- 1.1 程序框架
- 1.2 变量
- 1.3 常量
- 1.4 数据类型
- 1.5 运算符
- 1.6 判断与循环
- 1.7 数组
- 1.8 函数
- 1.9 指针
- 1.10 枚举
- 1.11 结构
- 1.12 自定义数据类型 typedef
- 1.13 联合
- 1.14 可变数组
- 1.15 链表
- 1.16 文件
- 第二章 ACLLib图像界面程序设计
- 第三章 交互图形设计
0 前言
0.1 计算机语言
- 程序是用特殊的编程语言写出来表达如何解决问题的。
- 不是用编程语言来和计算机交谈的,而是描述要求它如何做事情的过程和方法。
0.2 算法
- 要让计算机做计算,就需要像这样找出计算的步骤,然后用编程语言写出来。
- 计算机做的所有事情都叫做计算。
- 计算的步骤就是算法。
0.3 程序的执行
- 解释:借助一个程序,那个程序能试图理解你的程序,然后按照你的要求执行。
- 编译:借助一个程序,就像一个翻译,把你的程序翻译成计算机真正能懂的语言——机器语言——写的程序,然后,这个机器语言写的程序就能直接执行了。
解释型语言有特殊的计算能力。
编译型语言有确定的运算性能。
0.4 测试/调试程序
0.4.1 测试程序的常用值
0.4.2 调试程序
❗注意:调试好后注意将相关的代码注释掉。
(1)法1
(2)法2
printf("in loop\n");
ps1:只是一个字符串,没有带变量的任何一个值。目的是为了在程序的输出中看到程序到达此句代码了。
(3)法3
printf("hr\n");
ps1:经常用来调试的手段,用来表示程序到达的位置。可以在hr后面添加数字,如hr1、hr2…
0.5 编译预处理指令
0.6 宏
0.6.1 #define
如下语句即定义一个宏:
ps1:PI是宏的名字,3.14159是宏的值。
ps2:在编译预处理过程中,会把程序中所有的PI都替换成3.14159。
0.6.2 宏
0.6.2.1 定义有值的宏
0.6.2.2 定义没有值的宏
0.6.2.3 预定义的宏
- _ LINE_: 当前所在行的行号。
- _ FILE_:源代码文件的文件名。
- _ DATE_:编译时的日期。
- _ TIME_:编译时的时间。
- _ STDC_
0.6.2.4 带参数的宏
(1)像函数的宏
❗注意:【错误定义的宏】
(2)带参数的宏
(3)带参数的宏的原则
❗注意:后面千万不要加分号
❀例题
注意此时x没有带括号。实际是(2*1+2),因此结果为4。
注意三目运算符 ? : 的运算法则。
TOUPPER宏是用来检测一个字符c是否在’a’与’z’之间,即c是否为小写字母。如果是小写字母则执行第二个条件,即计算字符c所对应的大写字母;如果不是小写字母则执行第三个条件,即原样输出。
#include <stdio.h>
#include <string.h>
#define TOUPPER(c) ('a'<=(c)&&(c)<='z'?(c)-'a'+'A':(c))
int main()
{
int i;
char s[20] = {0};
strcpy(s, "abcd");
i = 0;
putchar(TOUPPER(s[i]));
return 0;
}
0.7 大程序结构
0.7.1 多个源代码文件
0.7.1.1 项目
0.7.1.2 编译单元
0.7.2 头文件
❀示例
max.c:
max.h:
main.c:
0.7.2.1 #include
0.7.3 声明
❀示例:全局变量的声明
max.h:【全局变量的声明】
main.c:
max.c:【全局变量的定义】
0.7.3.1 声明和定义
第一章 基础知识
1.1 程序框架
1.1.1 输入与输出
1.1.2.0 格式化输入输出
(1)printf
(2)scanf
(3)printf和scanf的返回值
1.1.2.1 输入
(1)scanf
❗注意:出现在scanf字符串中的东西,是它要求用户一定要输入的东西。
(2)getchar单字符输入
1.1.2.2 输出
(1)printf
(2)putchar单字符输出
1.1.2 简单计算程序
1.1.3 注释
注释插入在程序代码中,用来向读者提供解释信息。对于程序功能没有任何影响。
单行注释//
以/ / 开头
多行注释/**/
也可以用于一行内的注释。
1.2 变量
1.2.1 变量定义
1.2.2 变量的名字——标识符
(1)C语言关键字
1.2.2 赋值和初始化
1.2.2.1 赋值
1.2.2.2 变量初始化
![在这里插入图片描述](https://img-blog.csdnimg.cn/af110a132bd342a89ab4d3de
❀例题:变量的初始值判断
例题:
1.2.3 变量类型
1.2.4 全局变量
1.2.4.1 全局变量初始化
这样会编译错误:
这样编译可以:
但是不建议这样初始化。【全局变量的值不应该和另一个全局变量的值联系在一块】
1.2.4.2 返回指针的函数,使用全局变量
1.2.5 静态本地变量
1.3 常量
(1)const
1.4 数据类型
综述
1.4.1 浮点数类型
ps:此处foot、inch均为定义的整型变量(具体含义可见下方代码)。
1.4.1.1 double
❀例题:计算式子的运算结果
单精度的运算中,如果没有指定小数位数,那么第七位就要开始四舍五入,保留六位小数。双精度也类似。但是如果指定了小数位数,就可能不会进行四舍五入。
❀代码:关于身高的double数据类型应用
#include <stdio.h>
int main()
{
printf("请分别输入身高的英尺和英寸,"
"如输入\"5 7\"表示5英尺7英寸:");
double foot;
double inch;
scanf("%lf %lf", &foot, &inch);
printf("身高是%f米。\n",
((foot + inch / 12) * 0.3048));
return 0;
}
1.4.2 布尔类型bool
❀示例:bool类型的应用
#include<stdio.h>
#include<stdbool.h>
int main(){
bool b = 6>5;
printf("%d\n",b);
bool c = false;
printf("%d\n",c);
return 0;
}
1.4.3 字符类型char
1.4.3.1 字符的输入输出
(1)scanf与%c
ps1:即一个数49,如果类型是整数则它就是49,若类型是字符型则它是1
ps1:d输出的49是ASCII码。
测试1:输入的%d后有空格
测试数据:
(下面输入数据是中间输入了两个空格)
测试2:输入的%d后没有空格
测试数据:
可知上述中,字符读到的是空格。
ps1:不同之处在于%d后面有无空格。
ps2:如果%d后面没有空格,则整数的读取只读到整数结束为止。如果%d后有空格,则还要读取后面的空格。
(2)putchar与getchar
ps1:输入的参数是int类型,但实际上它int所接受的只是一个字符而已。
ps2:返回类型也是int,表示这一次到底写出去来的几个字符。【因为每次写入一个,正常情况下应该返回1】
ps3:特殊情况下输出不能用了,会返回特殊符号EOF(文件结束)。
ps4:EOF是C语言中定义的宏,这个宏的值是-1,返回-1表示不能再输出东西。
ps1:返回EOF表示输入结束。
❀示例
ps1:代码主要功能:将getchar()得到的结果交给ch变量,用ch值取判断它是不是EOF。如果不是EOF则在循环里输出ch值;如果是EOF则输出EOF。
ps2:测试数据
- 输入123456回车,输出结果仍是123456;输入EOF回车仍是输出EOF;输入-1回车还是输出-1;【输入的任何东西都会被原封不动地输出】
- 敲出ctrl+c,程序结束,但是没有输出显示EOF,说明只是强制结束程序,并没有正确让其知道输入结束。
- 敲出ctrl+D,得到了EOF的输出,表明程序读到了EOF的状态,即getchar返回了EOF。【Windows中输入ctrl+z,Unix中输入ctrl+D】→Windows和Unix对于EOF的定义是不一样的,但只是具体的键不一样,其余是一样的。
ps3:分析
注意是要按下回车,才能得到回答。这是因为程序和用户的键盘、显示器间还有另一个程序shell。shell会把键盘中输入的东西形成行编辑的工作。
- 比如说你在键盘按下123,在按下回车之前,这些字都没有被送到程序处,而是停留在shell。直到按下回车后,shell中有一个很大的缓冲区,把输入的东西填充。
- 如输入123,会有个’\0’标志表示结束的地方(结束标志可能是其他的形式);
- 如果程序还没有读,用户还输入了345并回车,则345的3会出现在’\0‘(结束标志)处
- 接下来便是程序的事情了。如果程序用的getchar,则依次读1、2、3;如果用的scanf,比如用的%d,则会把345变成一个整数。
- 无论是scanf还是getchar遇到回车后的结束标志(结束标志表示到达了缓冲区的结尾)时,并没有结束程序,而是等着用户的继续输入。
- 知道用户按下ctrl+d或CTRl+z,shell接收到后,则在结束标志后填入一个-1。(不同的编译器、操作系统可能有不同的表现方式,原理类似)。然后程序再用getchar取读的时候,会读到-1的结果。
- 如果输入的是ctrl+c,shell不会把ctrl+C放到任何地方,不会再改任何东西,shell会直接把程序关闭掉。
ps4:getchar、scanf是在shell的缓冲区中读东西;而用户的输入是让用户填充缓冲区。这是在标准输入和程序中,shell所做的事情。
1.4.3.2 字符计算
得到结果:B
得到结果:25
1.4.3.3 大小写转换
1.4.3.4 逃逸字符
常用逃逸字符:
\b通常做的工作是回去但不删除。
\t:
1.4.3.5 字符数组
❗注意:它不是C语言的字符串,它只是字符数组,因为不能用字符串的方式做计算。
1.4.3.5 字符串
注意区别是结尾的\0。
(1)字符串
ps1:整数0表示的是int,可能是4字节(计算机不同可能不同);‘\0’一定是1字节。‘0’是字符,表达的是ASCII的那个0,是人可以读到的0。‘0’是0x30,即十进制的48。
(2)字符串变量
有以下不同的表现形式:
ps1:第一个表达的是 指针str指向的是一个字符数组,字符中存放的内容是Hello。
ps2:第二个表达的是 已有一个字符数组word,它里面的内容是Hello。
ps3:第三个表达的是 字符数组line的数组大小是10个字节,往里面放了Hello,Hello这个字符串是5个字符占据了6个字节的位置。【写出了字符串的字面量表达形式之后,编译器会自动生成结尾0】
(3)字符串常量
ps1:s和s2用相同的字面量“Hello World”来初始化。输出结果是s和s2指向了相同的地方。
ps2: i是本地变量。【本地变量储存在一坨地方】
ps3:可看出i和字面量输出的地址相差很大(字面量的地址很小,i的地址很大)。【字面量的地址位于程序的代码段,且它是只读的。】
- 因为它是只读的,因此如果试图作注释代码的s[0]=‘B’,操作系统会有保护机制,让程序崩溃掉。
ps1:可看出s3的数组也在本地变量那儿(地址很大),是可以修改的。
由上述引起的思考:当作一个字符串的时候,应该写成指针形式还是数组形式?
(4)字符串数组
引入:如果想写入一个数组去表达很多个字符串,应该怎么写呢?
a[][]即a是一个二维数组的变量。
- 注意二维数组的第二维一定要有确切的大小,否则编译不能通过。
- 如a[][10],表示a是一个数组,这个数组中的每一个单元是char [10];也就是说a[0]相当于一个char[10]。
- 用这种方式写的时候需要确定这当中每个字符串的长度。如果字符串长度超过10则会出现warning。
- 另一种写法如下图。这样写相当于 a[0]即char*。其意思是,a[0]是个指针,指向外面某处存放字符串的地方。这样便是正常运行。
字符串数组应用的地方:main函数的参数
ps1:其实main函数的参数表有两个参数,一个是整数(告诉我们后面的数组到底有多少个字符串),一个是字符串数组。
ps2:测试——输出这个数组里所有的字符串。
- main函数可以读到执行这个程序的时候,在名字后面所跟上的东西。
- 而第一个参数,即argv[0]就是执行的时候所输入的命令的名字,或者说输入的那个可执行程序的名字。【Unix中】
(5)字符串计算
I.字符串赋值
II.字符串输入输出
ps1:%s读入的是一个单词(读到空格、tab、回车为止)。
ps2:这里的string最多能接受的只有7个字符【别忘了还有结尾的0】。
ps3:如果读入的长度超出了数组长度,会导致程序崩溃。
- 解决方法:
- 这个数字的目的是为了告诉scanf最多可以读入的位数。超过的就丢弃。
ps1:是根据输入的位数来划定单词。多余的内容会交给下一个%s或下一个其他的scanf去阅读。
ps1:char *string是定义了一个指针变量。而string是将来要指向某个字符串数组的某个内存空间的指针。
- 【就第一行来说,这个指针没有被初始化,即没有指向一个实际的地址。而它也是本地变量,而本地变量是没有初始值的。
- 即原来在那个内存里有什么,那它就是什么→如果正好等于的地方是有害的地方时则程序就崩溃了】
III.空字符串
IV.字符串函数
使用前开头需要加上 #include<string.h>
strlen:返回字符串长度
ps1:作为参数,数组的形式和指针的形式一样,所以此处全用指针的方式表达。
ps2:此处const是保证strlen函数不会修改字符串。
ps3:示例
- sizeof不要忽略了’\0’。
拓展:自己构造一个函数返回字符串长度
strcmp:比较两个字符串
ps1:示例
- 返回的结果0表示两个字符串相等。
ps2:注意作为条件判断时,一定不能省略==0。(因为它返回的0的结果,刚好就是if条件不满足的结果)
ps3:不可以直接用s1= =s2表达它们是相等的。 + 【因为数组的比较永远是false→两个数组的地址一定不会相同】 + (用==去比较的时候表达的是他俩的地址是否相同)。
ps4:当不相等的时候,输出的结果就是其差值(s1和s2不相等的字符的差值)
ps5:若相同字符串后边跟的是空格
拓展:自己构造一个函数比较两个字符串
ps1:注释是数组方式,后面是指针方式。
strcpy:拷贝
ps1:(如图所示src存储如下位置,希望把hello拷贝到第一行的所显示位置,即把一段内存往前移)即dst和src实际上是重叠了的,这时候便不能用strcpy作拷贝。
ps2:dst是目的,src是源。
ps3:这个函数是有返回的,即dst。
----典型应用:复制一个字符串
ps1:在程序静态的时候不知内存具体有多大,因此需要使用malloc动态申请一块内存。
ps2:strlen指的是存的内容的长度,并不包含结尾的’\0’。因此需要+1。
拓展:自己构造一个函数实现拷贝
【数组版本】
【指针版本】
ps1:注意需要读第二个参数src指向的地方,然后写入第一个参数dst所指的地方,因此dst不可以是const。
ps2:分析:作拷贝,实际上做的事是逐一从src往dst写,写到src是’\0’为止。
strcat:拷贝拼接
拓展:自己构造一个函数实现拷贝拼接
strncpy、strncat、strncmp
安全问题:(尽可能不使用)
在中间多了一个n,以及参数里面多了n:n表示能够拷贝过去多少个字符。
多了就掐掉。(这样便不会出现越界问题)
strncmp是指定只比较开头n个字符
strchr:在字符串中找字符
ps1:第一行是在字符串s中寻找c 从左数 第一个出现的位置,找到了返回的是指针。
ps2:第二行是在字符串s中寻找c 从右数 第一个出现的位置,找到了返回的是指针。
ps3:返回的是非NULL表示找到了并且给你的指针指向的是要找的字符。
ps3:示例
- p返回了指向第一个l的指针,从这个地方开始,把后面的当作一个字符串输出,便得到了llo。
ps4:指向第二个(即在llo里找到第二个l)
ps5:找到l后,把l后面的东西,复制到另一个字符串中。
- malloc()函数分配所需的内存空间,并返回一个指向它的指针。如果分配失败,则返回⼀个空指针(NULL)。
- free(t);是把malloc(t)给释放。
- 关于t指针可详见strcpy的典型应用。
ps6:找到第一个l,但是想要前面那一段。
- 总思路:用一个变量c临时存储p,然后让p=‘\0’,接着malloc s开头的字符串,然后将s开头的字符串拷贝给t。就可以得到第一个l前面的。(相当于把找到的清除,留下剩余的)
- 分析:
strchr找到字符串s中第一个l的位置;用一个变量c临时存储p,即值为‘l’;然后让p=‘\0’
令p=‘\0’后,s所剩余的便只有he了
把s拷贝给t,便得到字符串“he”
令p=c,即将c中记录的’l’写回去,把s恢复回来。
strstr:在字符串中找字符串
ps1:strcasestr即在寻找的时候忽略大小写。
1.5 运算符
❗注意:对于C语言来说,赋值是一个运算符。
1.5.1 定义
如下
1.5.2 相关运算符
1.5.2.1 赋值运算符
ps:第三条是计算机所理解的。
例如
1.5.2.2 递增递减运算符
1.5.2.3 关系运算符
❗注意:其中有两个字符的运算符:==、>=和<=的两个字符必须紧紧连在一起,中间不能插入空格。
关系运算的结果是一个逻辑值,逻辑值只有两种可能的值:true(真,表示成立)或false(假,表示不成立)。
❀例题:关于关系运算符的表达式求结果
1.5.2.4 逻辑运算符
优先级:!> && > ||
(1)逻辑运算的短路
❀示例:逻辑运算符的应用
表达x∈【4,6】则应写成x>4&&x<6
1.5.2.5 条件运算符
1.5.2.6 逗号运算
基本上是在for语句中使用,如
❀示例:逗号运算符的应用
输出结果为7。
赋值运算符的优先级高于逗号运算符,因此先计算i=3+4,而5+6没有赋给任何。
输出结果为11。括号提升了优先级,因此先计算逗号表达式,再赋值给i。
1.5.2.7 位运算
(1)按位运算
- &&逻辑运算。&按位运算
- 利用对同一个变量用一个值异或两次,可以做加密。(比较弱)
❀示例:按位与运算
❀示例:让某一位为0,如x & 0xFE
❀示例:取一个数中的一段,如x & 0xFF
❀示例:按位或运算
❀示例:按位取反运算
❀示例:按位异或运算
(2)移位运算
最多可以移动的位数取决于int的大小。
❗注意:
❀示例:左移运算
(3)位运算例子
❀示例:输出一个数的二进制
- 1u中的u表示他是一个unsigned。
(4)位段
1.5.3 运算符的优先级及结合关系
1.5.3.1 优先级
1.5.3.2 结合关系
❀例题:表达式值的运算结果
❀代码:计算时间差
🐇1:关于计算时间差的运算符“/”、“%”的应用
代码——计算时间差
🐇2:关于计算时间差的判断的应用
计算时间差
#include <stdio.h>
int main()
{
int hour1, minute1;
int hour2, minute2;
scanf("%d %d", &hour1, &minute1);
scanf("%d %d", &hour2, &minute2);
int ih = hour2 - hour1;
int im = minute2 - minute1;
if ( im <0 ) {
im = 60 + im;
ih --;
}
printf("时间差是%d小时%d分。\n", ih, im);
return 0;
}
❀代码:求两个数的平均值
写一个程序,输入两个整数,输出它们的平均值
ps1:除以2.0是保证结果的准确(如2+3的平均值是2.5)。
ps2:整型a、b除以2.0后注意要用double类型的变量c来存储计算结果(输出时注意要用%f)。
#include <stdio.h>
int main()
{
int a,b;
scanf("%d %d", &a, &b);
double c = (a+b)/2.0;
printf("%d和%d的平均值=%f\n", a, b, c);
return 0;
}
❀代码:关于交换两个变量的值的运算符的赋值的应用
有两个变量a、b,且其中各储存两个数,要求将两变量交换数值。
ps:主要思路是设置一个中间变量暂时存储交换的值。
1.6 判断与循环
1.6.1 判断
1.6.1.1 if语句
(1)基本格式
(2)常见错误
❀例题:if语句格式
ps1:编译可以通过,但是C语言没有这样的写法;程序运行结果很可能不是正确结果。
❀代码:找零计算器
ps1:注意考虑票面是否足够减去所用金额,因此使用判断语句
流程图:
int main()
{
// 初始化
int price = 0;
int bill = 0;
// 读入金额和票面
printf("请输入金额:");
scanf("%d", &price);
printf("请输入票面:");
scanf("%d", &bill);
// 计算找零
if ( bill >= price ) {
printf("应该找您:%d\n", bill - price);
} else {
printf("你的钱不够\n");
}
return 0;
}
❀代码:比较两个数的值
🐇1
🐇2
#include <stdio.h>
int main()
{
int a,b;
printf("请输入两个整数:");
scanf("%d %d", &a, &b);
int max = b;
if ( a > b ) {
max = a;
}
printf("大的那个是%d\n", max);
return 0;
}
❀代码:根据每小时薪水及工作时间计算最终薪水
ps1:RATE表示每小时工作的薪水,STANDARD表示一周标准的工作时间。
#include <stdio.h>
int main()
{
const double RATE = 8.25;
const int STANDARD = 40;
double pay = 0.0;
int hours;
printf("请输入工作的小时数: ");
scanf("%d", &hours);
printf("\n");
if (hours > STANDARD)
pay = STANDARD * RATE +
(hours-STANDARD) * (RATE * 1.5);
else
pay = hours * RATE;
printf("应付工资: %f\n", pay);
return 0;
}
❀代码:判断成绩是否及格
#include <stdio.h>
int main()
{
const int PASS=60;
int score;
printf("请输入成绩: ");
scanf("%d", &score);
printf("你输入的成绩是%d.\n", score);
if ( score < PASS )
printf("很遗憾,这个成绩没有及格。");
else {
printf("祝贺你,这个成绩及格了。");
printf("再见\n");
}
return 0;
}
❀代码:计算一个4位及以下整数的位数
思路:判断数的范围来决定它的位数
❀代码:计算一个整数的位数
思路:想想人如何数位数(数一个化去一个),那么计算机就可以同样采取此思路。 用整数 / 10,则可以去掉一个个位数(此时计数+1)。以此循环,直到结果为0,则数到最后一位数。
ps1:注意输入整数的范围,因为整型变量是有取值范围的。
🐇1:while循环的应用
🐇2:do-while循环的应用
1.6.1.2 嵌套的if语句
1.6.1.3 级联的if-else if
1.6.1.4 switch-case
(1)break
❀例题
注意没有break。就会进入下一个case语句。
❀代码:成绩百分制转为ABCDE等级制
编写程序将一个百分制成绩转换为五分制成绩。转换规则:
+大于等于90为A
+小于90且大于等于80为B
+小于80且大于等于70为C
+小于70且大于等于60为D
+小于60为E
❀代码:根据输入的月份数字输出对应的英文单词
🐇1:switch-case
🐇2:数组
1.6.2 循环
1.6.2.1 while循环
❗注意:循环体内要有改变条件的机会,否则就会成为一个死循环,无限循环下去。
1.6.2.2 do-while循环
while和do-while的区别
1.6.2.3 for循环
🥥小技巧:求和时,记录结果的变量初始为0;求积时,记录结果的变量初始为1。
(1)结构
ps1:分号是不能省略的。
(2)解释
(3)循环次数
(4)其实for循环和while循环是等价的
【任何一个while循环都可以改写成for循环】
❀代码:求n的阶乘
🐇1:while循环的应用
#include<stdio.h>
int main(){
int n;
scanf("%d",&n);
int fact=1;
int i=1;
while(i<=n){
fact *= i;
i++;
}
printf("%d!=%d\n",n,fact);
return 0;
}
🐇2:for循环的应用
1.6.2.4 break和continue
❗注意:两者都只能对它所在的那层循环做。
❀例题
1.6.2.5 goto
在需要离开整个循环的地方放上一个goto语句。
goto语句后面需要有一个标号,这个标号是需要自己编写的。
如下:即out是一个地方,而goto需要跳转到out所指的地方。
1.6.2.n 三种循环的选择
- 起点和终点的值明确,用for
❀例题
选项C执行条件时执行i自增,而选项ABD是条件满足输出后执行循环体时才自增。
于是选项C的第一个输出值是自增后为1。
#include<stdio.h>
int main(){
int i,j,k,l;
for(i=0;i<10;i++) printf("i=%d ",i);
printf("\n");
for(j=0;j<10;++j) printf("j=%d ",j);
printf("\n");
for(k=0;k++<10;) printf("k=%d ",k);
printf("\n");
for(l=0;l<=9;l++) printf("l=%d ",l);
return 0;
}
for👉先赋值,再判断条件,为真进入表达式循环
step1:i=10,满足条件10>1,输出10,执行i++后i=10,执行i/2=5;
step2:满足条件5>1,输出5,执行i++后i=6,执行i/2=3;
step3:满足条件3>1,输出3,执行i++后i=4,执行i/2=2;
step4:满足条件2>1,输出2,执行i++后i=3,执行i/2=1;
step5:不满足条件,终止循环。
❀代码:算平均数
🥥小技巧1:计算之前先保存原始的值,后面可能有用。(即首先将原始值赋给某个变量)
🥥小技巧2:如果要模拟运行一个很大次数的循环,可以模拟较少的循环次数,然后作出推断。
让用户输入一系列的正整数,最后输入-1表示输入结束,然后程序计算出这些数字的平均数,输出输入的数字的个数和平均数。
分析:变量→算法→流程图→程序
- 变量
+算法
+流程图
🐇1:
#include<stdio.h>
int main(){
int number;
int count=0;
int sum=0;
scanf("%d",&number);
while(number!=-1){
sum+=number;
count++;
scanf("%d",&number); //再次读书输入的number,改变循环条件(否则进入死循环)
}
printf("%f\n",1.0*sum/count); //乘1.0是为了将整数改为浮点数
return 0;
}
🐇2:
#include<stdio.h>
int main(){
int number;
int count=0;
int sum=0;
do{
scanf("%d",&number);
if(number!=-1){
sum+=number;
count++;
}
}while(number!=-1); //while再次判断循环条件,但是这样就会和前面if的判断相同,产生浪费
printf("%f\n",1.0*sum/count); //乘1.0是为了将整数改为浮点数
return 0;
}
❀代码:猜数游戏
让计算机来想一个数,然后让用户来猜,用户每输入一个数,就告诉它是大了还是小了,直到用户猜中为止,最后还要告诉用户它猜了多少次。
分析:
代码:
ps1:rand():得到一个随机的整数。与srand搭配使用保证每次得到不同的随机数。
❀代码:输出整数的逆序的数
输入一个正整数,输出逆序的数。
🐇1:保留开头的0(如700输出逆序数为007)
ps1:digit存储的是取出的每一位的数。
🐇2:去除开头的0(如700输出逆序数为7)
#include<stdio.h>
int main(){
int x;
scanf("%d",&x);
int digit;
int ret; //储存结果
// 3.考虑循环及循环条件
while(x>0){
digit=x%10; //1.取出每一位上的数
// printf("%d",digit); //0.调试所用
ret=ret*10+digit; //4.原结果左移1位,再加上digit(取出来的个位数)
// printf("x=%d,digit=%d,ret=%d\n",x,digit,ret); //0.调试所用
x/=10; //2.取出后丢掉个位数
}
printf("%d",ret); //5.程序输出
return 0;
}
❀代码:判断素数
只能被1和自身整除的数叫素数,不包括1。
利用反证法,如果能被其他数整除,则这个数不是素数。
注意考虑0和1的情况。
🐇1:循环——从2到x-1测试是否可以整除
#include<stdio.h>
int main(){
int x,i;
scanf("%d",&x);
int isPrime=1; //若isPrime=1,则x是素数
if(x==0 || x==1){
printf("%d不是素数\n",x);
}else{
for(i=2;i<x;i++){
if(x%i==0){
isPrime=0;
break; //设置跳出循环
}
}
if(isPrime==1){
printf("%d是素数\n",x);
}else{
printf("%d不是素数\n",x);
}
}
return 0;
}
🐇2:循环——去掉偶数后,从3到x-1,每次加2
可发现:除了2以外的偶数都不是素数。
然后后面就是从3开始的奇数来判断。
#include<stdio.h>
int isPrime(int x);
int main(){
int x,i;
scanf("%d",&x);
if(isPrime(x)==1){
printf("%d是素数\n",x);
}else{
printf("%d不是素数\n",x);
}
return 0;
}
int isPrime(int x){
int ret=1;
int i;
if(x==1 || (x%2==0&&x!=2) ){//当输入值为1或者不为2的偶数时,直接判断不是素数
ret=0;
}
for(i=3;i<x;i+=2){
if(x%i==0){
ret=0;
break; //设置跳出循环
}
}
return ret;
}
🐇3:循环——去除除2以外偶数后,循环到sqrt(x)
#include<stdio.h>
#include<math.h>
int isPrime(int x);
int main(){
int x,i;
scanf("%d",&x);
if(isPrime(x)==1){
printf("%d是素数\n",x);
}else{
printf("%d不是素数\n",x);
}
return 0;
}
int isPrime(int x){
int ret=1;
int i;
if(x==1 || (x%2==0&&x!=2) ){//当输入值为1或者不为2的偶数时,直接判断不是素数
ret=0;
}
for(i=3;i<=sqrt(x);i+=2){
if(x%i==0){
ret=0;
break; //设置跳出循环
}
}
return ret;
}
🐇4:数组——判断是否能被已知的且<x的素数整除
前提是我们已有一张素数表,然后根据这张表来判断输入值是否能被已知的且<x的素数整除。
因此需要思考在构造这张素数表的时候,如何把新的数据项再添加进这张表中。
ps1:从3开始测试该数是否为素数 。
构造素数表代码1:
#include<stdio.h>
int main(){
const int maxNumber = 25;
int isPrime[maxNumber];
int i;
int x;
for ( i=0; i<maxNumber; i++ ) {
isPrime[i] = 1;
}
for ( x=2; x<maxNumber; x++ ) {
if ( isPrime[x] ) {
for ( i=2; i*x<maxNumber; i++) {
isPrime[i*x] = 0;
}
}
}
for ( i=2; i<maxNumber; i++ ) {
if ( isPrime[i] ) {
printf("%d\t", i);
}
}
printf("\n");
return 0;
}
构造素数表代码2:
#include<stdio.h>
int isPrime(int x, int knownPrimes[], int numberofKnownPrimes);
int main(void){
const int number = 100; //假设先算出前100个素数
int prime[number]; //构造素数表 .。初始化为2
prime[0] = 2;
int count = 1; //因为数组里有一个值2,因此count=1
int i = 3; //从3开始测试该数是否为素数
while ( count < number ) {
if ( isPrime(i, prime, count) ) { //若发现i是一个素数,便把i加进isPrime函数中,并将其放在count索引的数组位置中
prime[count++] = i; //count表示的是下一次素数可以写入进的数组中的位置 (写入后又移向下一个待储存位置)
}
i++;
}
//打印素数表
for ( i=0; i<number; i++ ) {
printf("%d", prime[i]);
if ( (i+1)%5 ) printf("\t");
else printf("\n");
}
return 0;
}
int isPrime(int x, int knownPrimes[], int numberofKnownPrimes){
int ret = 1;
int i;
for ( i=0; i<numberofKnownPrimes; i++ ) {
if ( x % knownPrimes[i] == 0 ) {
ret = 0;
break;
}
}
return ret;
}
❀代码:输出100以内的素数
#include<stdio.h>
int main(){
int x,i,isPrime;
for(x=2;x<=100;x++){
isPrime=1; //若isPrime=1,则x是素数
for(i=2;i<x;i++){
if(x%i==0){
isPrime=0;
break; //设置跳出循环
}
}
if(isPrime==1){
printf("%d ",x);
}
}
return 0;
}
❀代码:输出前50个素数
#include<stdio.h>
int main(){
int x,i,isPrime;
int cnt=0; //定义一个计数变量
for(x=2;cnt<50;x++){
isPrime=1; //若isPrime=1,则x是素数
for(i=2;i<x;i++){
if(x%i==0){
isPrime=0;
break; //设置跳出循环
}
}
if(isPrime==1){
printf("%d ",x);
cnt++;
}
}
return 0;
}
❀代码:凑硬币——用1角、2角、5角的硬币凑出10元以下金额
ps1:设置一个exit变量,是为了给break设置一个跳出循环的条件。当exit=1时,就能实现接力break,以便跳出整个嵌套循环。也可以使用goto。
❀代码:前n项和
🐇1
#include<stdio.h>
int main(){
int n,i;
scanf("%d",&n);
double sum=0; //注意1/i的每一项基本都带了小数点,最好用double
for(i=1;i<=n;i++){
sum+=1.0/i; //注意一定要用1.0,这样才会得到浮点数的运算结果
}
printf("f(%d)=%f",n,sum); //注意sum是double类型,因此的用%f
return 0;
}
🐇2
可以定义一个符号变量,然后循环一次便取反一次。最后用这个符号变量乘以1题中的sum。
- 写法1:
#include<stdio.h>
int main(){
int n,i;
scanf("%d",&n);
double sum=0; //注意1/i的每一项基本都带了小数点,最好用double
int sign =1; //定义一个变量,表示前正负
for(i=1;i<=n;i++){
sum+=sign*1.0/i; //注意一定要用1.0,这样才会得到浮点数的运算结果
sign=-sign; //每循环以此,便将正负号取反
}
printf("f(%d)=%f",n,sum); //注意sum是double类型,因此的用%f
return 0;
}
- 写法2:(直接将sign写成double类型,便省去了sum计算公式中1.0去除以i的写法)
#include<stdio.h>
int main(){
int n,i;
scanf("%d",&n);
double sum=0; //注意1/i的每一项基本都带了小数点,最好用double
// int sign =1; //定义一个变量,表示前正负
double sign =1.0;
for(i=1;i<=n;i++){
sum+=sign/i; //注意一定要用1.0,这样才会得到浮点数的运算结果
sign=-sign; //每循环以此,便将正负号取反
}
printf("f(%d)=%f",n,sum); //注意sum是double类型,因此的用%f
return 0;
}
❀代码:输出两个数的最大公约数
🐇1:枚举法
ps1:首先判断出a、b中最小的那个数。
ps2:i 的条件是,数值不能大于a、b中最小的数。
ps3:如果是求最小公约数,直接在ret=i;后面加上break;即可。(此时已经找到最小的那个公约数)
🐇2:辗转相除法
- 分析:
- 列式:
- 代码:
#include<stdio.h>
int main(){
int a,b;
scanf("%d %d",&a,&b);
int t;
while(b!=0){
t=a%b;
a=b;
b=t;
}
printf("gcd=%d",a);
return 0;
}
❀代码:正序分解整数
输入一个非负整数,正序输出它的每一位数字。
- 输入:13425
- 输出:1 3 4 2 5
🐇1:思路1先得这个数的逆序数,再逆序输出
但是这个解法的不足是:700输出是7
#include<stdio.h>
int main(){
int x;
scanf("%d",&x);
int t;
//首先得到输入的数值的逆序数
do{
int d=x%10;
t=t*10+d;
x/=10;
} while(x>0);
x=t; //此时t为逆序数,但x=0;因此需要将t的值赋给x,才能再进行下面的运算。
//然后再逆序+空格输出
do{
int d=x%10;
printf("%d",d);
if(x>=10){ //保证每一轮输出数字时后面都有空格(最后一个数字后没有空格)
printf(" ");
}
x/=10;
}while(x>0);
printf("\n");
return 0;
}
🐇2:思路2依次取出最高位
思路:(假设输入153)
- 153/100=1 -> 1 ; 153%100=53
- 53/10 ->5 ; 53%10=3
- 3/10 ->3
- 通过依次取出最高位,得到分解的正序数
#include<stdio.h>
int main(){
int x;
scanf("%d",&x);
int mask=1; //mask即每轮除以或取余的数值 (mask等于10的 位数减1 次方)
int t=x; //储存原始的输入值
//→→→找出第一轮mask值←←←
while(t>9){ //当x>10,即x是两位数时,才需要作出变化(这样也就保证了位数减1次方,便不用做其他处理)
t/=10; //通过不断除以10,可知该数的位数,而
mask *= 10; //相当于位数加1,就乘以10(即取一次10的1次方)
}
// printf("x=%d,mask=%d\n",x,mask); //注意经过此轮计算,消耗了x的值,因此需要再最开始令一个变量等于输入值,才好有原值再进行下面计算
//→→→依次取出最高位的数值←←←
do{
int d=x/mask;
printf("%d",d); //取得最高位数值
if(mask>9){ //保证每一轮输出数字时后面都有空格(最后一个数字后没有空格)
printf(" "); //当mask是一位数时,即已经到了最后一轮
}
x%=mask;
mask /= 10; //改变mask值
}while(mask>0);
return 0;
}
1.7 数组
1.7.1 数组的基本概念
ps1:在赋值号右边,即读取;在赋值号左边,即存储。如上式即表示读出a[1]的值,加上6再赋给a[2]。
ps2:在赋值右边的叫做右值。
1.7.2 一维数组
1.7.2.1 定义数组
❗注意:C99开始,已经可以用变量来定义数组的大小了。
1.7.2.2 初始化数组
1.7.2.2 数组的大小
(1) sizeof
1.7.2.3 数组的赋值
1.7.2.4 遍历数组
❀代码:输入一串数,并输出所有大于平均数的数
让用户输入一系列的正整数,最后输入-1表示输入结束。然后程序计算出这些数字的平均数,输出所有大于平均数的数。
🐇1
#include<stdio.h>
int main(){
int x;
scanf("%d",&x);
int cnt=0; //记录个数
int number[100]; //定义一个数组,存放输入值 (对数组中的元素赋值)
double sum=0;
while(x!=-1){
number[cnt]=x; //让输入值依次储存在数组number中
sum+=x;
cnt++;
scanf("%d",&x); //再次读入输入值
}
if(cnt>0){
int i;
printf("平均数=%f\n",sum/cnt);
for(i=0;i<cnt;i++){ // 遍历出所有大于平均数的数
if(number[i] > sum/cnt){ //使用数组中的元素
printf("%d\n",number[i]);
}
}
}
return 0;
}
注意此代码有个问题是,没有限制输入的整数的个数,但是却定义了数组的长度为100。一旦输入的个数超过了长度,那么可能程序会运行崩溃。
🐇2
改进:令数组长度等于输入整数的个数。
#include<stdio.h>
int main(){
int x;
int cnt=0; //记录个数
double sum=0;
printf("请输入您所输入的数字的数量:");
scanf("%d",&cnt);
if(cnt>0){
int number[cnt]; //定义一个数组,存放输入值 (对数组中的元素赋值)..这样保证了输入的数不会超过数组的存储容量(如果超过了,会导致程序崩溃)
scanf("%d",&x);
cnt=0; //初始数组的下标。否则运行出错。
while(x!=-1){
number[cnt]=x; //让输入值依次储存在数组number中
sum+=x;
cnt++;
scanf("%d",&x); //再次读入输入值
}
int i;
double average=sum/cnt;
printf("平均数=%f\n",average);
for(i=0;i<cnt;i++){ // 遍历出所有大于平均数的数
if(number[i] > average){ //使用数组中的元素
printf("%d\n",number[i]);
}
}
}
return 0;
}
❀代码:统计数量不确定的[0,9]范围内的整数中,每一种数字出现的次数
写一个程序,输入数量不确定的[0,9]范围内的整数,统计每一种数字出现的次数。输入-1即表示输入结束。
#include<stdio.h>
int main(){
const int number=10; //10表示0-9有10位数。【也是决定了数组的大小为10】
int x;
scanf("%d",&x);
int count[number]; //如count[0]表示0出现的次数
int i;
for(i=0;i<number;i++){ //初始化数组
count[i]=0;
}
while(x!=-1){
if(x>=0&&x<=9){
count[x]++; //数组参与运算
}
scanf("%d",&x);
}
for(i=0;i<number;i++){ //遍历数组输出
printf("%d出现的次数是%d\n",i,count[i]);
}
return 0;
}
❀代码:查找某个数在数组中的位置
描述:找出某个数key在数组a中的位置。如果找到则返回其在a中的位置;如果找不到则返回-1。
思路:搜索(查找)的基本方法是遍历。
🐇1:线性搜索
#include<stdio.h>
//key表示要查找的数,a表示数组,length表示数组a的长度
int search(int key,int a[],int length); //定义search函数
int main(){
int a[]={2,4,6,7,1,3,5,9,11,13,23,14,32}; //初始化数组a
int x;
int loc;
printf("请输入一个数字:");
scanf("%d",&x);
loc=search(x,a,sizeof(a)/sizeof(a[0])); //给函数值。length表达式可看 1.7.2.2数组的大小处
if(loc!=-1){
printf("%d在数组的第%d个位置上\n",x,loc+1); //因为数组下标初始值为0,因此在这里需要+1才是实际的位置
}else{
printf("%d不存在\n",x);
}
return 0;
}
int search(int key,int a[],int length){
int i;
int ret=-1; //初始化结果值,假设找不到
for(i=0;i<length;i++){ //遍历数组
if(a[i]==key){ //将遍历出来的每一个数与要查找的值比较
ret=i; //如果相等则表明找到该数,则返回下标索引号
break; //找到则退出循环
}
}
return ret; //返回函数结果值 【单一出口】
}
❗注意:数组作为函数参数时,往往必须再用另一个参数来传入数组的大小。
- 数组作为函数的参数时:
- 不能在[]中给出数组的大小;
- 不能再用sizeof来计算数组的元素个数。
🐇2:二分搜索
对于一个数组,将索引0处设为left, 索引-1处设为right, 每一次将中值a[mid]与k比较。若不相同,将left向右移动,直到 right < left ,输出匹配到的索引值。
前提是已排好序的数组。
#include <stdio.h>
int main()
{
int a[] = {2,4,7,11,13,16,21,24,27,32,36,40,46,};
int len = sizeof(a)/sizeof(a[0]);
int ret = -1;
int left = 0;
int right = len - 1;
int k =36; //想要知道36在数组a中的位置
while ( right > left)
{
int mid = (left+right)/2;
if ( a[mid] == k ) {
ret = mid;
break; }
else if ( a[mid] > k ) {
right = mid-1;
ret = right; }
else {
left = mid+1;
ret = left; }
}
int i;
printf("数组a的值如下:\n");
for(i=0;i<len;i++){
printf("%d ",a[i]);
}
printf("\n\n\n其中%d在数组中的第%d个位置上\n",k,ret+1);
return ret;
}
❀代码:排序1——选择排序
每轮先选出最大的,然后从右往左依次放置(需要交换位置)
#include<stdio.h>
int max(int a[],int len){
int maxid=0;
int i;
for(i=0;i<len;i++){
if(a[i]>a[maxid]){
maxid=i;
}
}
return maxid;
}
int main(){
int a[]={1,300,2,4,45,32,5,87};
int len=sizeof(a)/sizeof(a[0]);
int i;
for(i=len-1;i>0;i--){ //从右往左依次放置找到的最大值
int maxid=max(a,i+1);
//交换
int t=a[maxid];
a[maxid]=a[i];
a[i]=t;
}
//遍历输出排序后的数组
for(i=0;i<len;i++){
printf("%d ",a[i]);
}
return 0;
}
1.7.3 二维数组
1.7.3.1 二维数组的遍历
需要两重循环。外面一层遍历行号,内层遍历列号。
a[i] [j]表示第i行第j列上的单元。
a[i,j]即a[j] (注意,是逗号运算符)
1.7.3.2 二维数组的初始化
❀例题
解析:a[2][3]是一个2行3列的数组,注意下标范围。
1>2是个表达式,在C语言里表达式只有2个值,真或假也就是0或1。那么C就相当于a[0][0]。因此是正确的。
❀代码:井字棋游戏
读入一个3X3的矩阵,矩阵中的数字为1表示该位置上有一个X,为0表示为O;
程序判断这个矩阵中是否有获胜的一方,输出表示获胜一方的字符X或O,或输出无人获胜。
提供测试数:
- O赢了:0 1 0 1 1 0 1 0 0
- X赢了:1 0 0 0 0 1 1 1 1
- 没有人赢:1 0 1 0 0 1 1 1 0
#include<stdio.h>
int main(){
const int size=3; //将矩阵设置为3x3
int board[size][size]; //定义一个矩阵
int i,j;
int row_numOfX,col_numOfX, row_numOfO,col_numOfO; //row_num0fX表示同一行x数量, row_num0f0表示同一行o数量
int dia_numfX1,dia_numfX2,dia_numfO1,dia_numfO2; //dia_numfX1表示对角线上x数量
int result = -1; // -1 :没人赢,1:X赢,0:O赢
//→→→→→→读入矩阵→作出数组的遍历 ←←←←←
printf("请输入井字棋结果,格式如下:\n 1 0 1\n 0 0 1\n 1 1 0\n");
for(i=0;i<size;i++){
for(j=0;j<size;j++){
scanf("%d",&board[i][j]);
}
}
//→→→→→→检查行和列 ←←←←←
for(i=0;i<size&&result==-1;i++){
row_numOfX=col_numOfX=0; //初始化为0
row_numOfO=col_numOfO=0;
for(j=0;j<size;j++){
//判断行
if(board[i][j]==1){
row_numOfX++; //如果是1则num0fX加1
}else{
row_numOfO++; //如果是0则num0f0加1
}
//判断列
if(board[j][i]==1){
col_numOfX++;
}else{
col_numOfO++;
}
}
if(row_numOfX==size||col_numOfX==size){ //如果o的数量=3,则o赢
result=1;
}else if(row_numOfO==size||col_numOfO==size){
result=0;
}
}
//→→→→→→检查对角线 ←←←←←
dia_numfX1=dia_numfX2=dia_numfO1=dia_numfO2=0; //初始化为0
for(i=0;i<size&&result==-1;i++){
if(board[i][i]==1){ //对角线如[0][0]、[1][1]、[2][2]
dia_numfX1++;
}else{
dia_numfO1++;
}
if(board[i][size-i-1]==1){
dia_numfX2++;
}else{
dia_numfO2++;
}
if(dia_numfX1==size||dia_numfX2==size){ //如果o的数量=3,则o赢
result=1;
}else if(dia_numfO1==size||dia_numfO2==size){
result=0;
}
}
//→→→→→→输出结果←←←←←
switch(result){
case -1:
printf("没有人赢!\n");
break;
case 0:
printf("O赢了!\n");
break;
case 1:
printf("X赢了!\n");
break;
default:
printf("Error!\n");
break;
}
return 0;
}
1.8 函数
1.8.1 定义函数
函数由函数头和函数体组成。
函数头又包括返回类型、函数名、参数表。
C语言不允许函数嵌套定义:C语言不允许在函数内部定义函数,但是可以放函数的声明。
1.8.2 调用函数
调用方式:函数名(参数值);
函数知道每一次是哪里调用它,会返回到正确的地方。
对于f(a,b)和f((a,b)) 第一个传了两个参数,第二个传递了一个参数。
1.8.3 函数返回
1.8.3.1 从函数中返回值
函数中得到的值可以有以下途径:
❗注意:如果函数有返回值,则必须使用带值的return。
1.8.3.2 没有返回值的函数
如果函数没有返回值,那么前面的返回类型需要是void,具体用法如下:
1.8.4 函数原型
1.8.5 参数传递
❗注意:C语言在调用函数时,永远只能传值给函数。
C语言函数调用时发生的参数的转移,是一种值的传递,把值传进了函数。在函数里,函数的参数和调用它的地方没有任何联系。
❀示例:将交换的代码封装成一个函数,是否可以完成两个变量值的交换?
#include <stdio.h>
void swap(int a, int b);
int main()
{
int a = 5;
int b = 6;
swap(a, b);
printf("a=%d b=%d\n", a,b);
return 0;
}
void swap(int x, int y)
{
int t = x;
x = y;
y = t;
}
ps1:调试运行,可以看出:
- a和b的初始值为5和6,进入函数时,没有a和b这两个变量;跳出函数后,没有x和y这两个变量;最终a和b的值仍为5和6。
- 因此,C语言在调用函数时,永远只能传值给函数。
通常认为,是参数和值的关系:
当函数没有参数时,是写成void f(void)还是void f()呢?当你在参数表内放个void明确代表这个函数不接受任何参数。当你在参数表里不放东西,在传统C中,它表示f函数的参数表未知,并不表示没有参数。
不要用void swap()这种。如果确定函数里面没参数,就用void swap(void)。
1.8.6 本地变量
1.8.6.1 变量的生存期和作用域
1.8.6.2 本地变量的规则
1.8.7 补充
在Windows中,一个程序返回0是正确的。返回任何非0的值是有错的。
❀例题
解析:D是函数定义,不属于原型。
解析:错在函数的返回值。没有返回值的函数的类型标识符为void,不需要写return语句。函数都有自己的类型,除void类型的函数外都有自己的返回值。
解析:C选项是声明一个函数名字为f,没有返回值,函数参数为int类型。
可见1.8.5的示例。
❀代码:求和
求出1到10、20到30、35到45的三个和
- 解法1:
ps1:有三段几乎一模一样的代码,应该尝试简洁化。- 解法2:即用函数将共同出现的表达出来。
1.9 指针
1.9.0 指针的用途
1.9.1 相关运算符
1.9.1.1 取地址运算符&
ps1:printf(“%x”,&i); 同 printf(“%p”,&i);
ps2:用printf输出一个地址,要用的应该是%p。
ps1:必须对一个变量取地址。如果右边不是一个变量,则不能取地址。
ps2:以上3种均不能取地址。
&可以取的地址:
1.9.1.2 访问地址上的变量*
1.9.2 指针变量
指针就是保存地址的变量。
ps1:*表示p是一个指针。
ps2:int *p表示指针p指向的是一个int。
ps3:int *p=&i表示把i的地址交给了指针p。
ps4:第三四个表达式本质一样,只是写法不一样(都可以):均表示p是一个指针,而q只是个普通的int型的变量。
❗写指针的好习惯:一旦构造出一个指针,便初始化为0.
1.9.2.1 作为参数的指针
ps1:调用f()函数时,需要交给它变量的地址——必须用&取得变量的地址再传给指针。
ps1:f(&i)得到的是i的地址;g(i)获得的是i的值。
ps2:*p取得的是值。
ps1:在经历了f()函数的调用后,i的值发生了变化。(注意此处发生的仍然是值的传递,可以对照1.8.5函数中参数值的传递)
ps2:main函数中i的地址值被传进了函数f,通过地址,在函数内部可以通过*p的方式访问到外面i变量的值。(p即i的地址,p就代表了i)。便可以通过修改p去修改i。
1.9.2.2 指针与const
指针本身可以是const,指针所指向的变量也可以是const。
ps1:i不是const,因此可以执行*p=26。
ps2:因为q是const,所以q++不可以执行。
ps1:不能变的是通过p去赋值,即*p=26不可以执行。【i、p可以变】
ps1:const在*前面则所指的东西不可以被修改;const在 * 后面便是指针不能被修改。
(1)非const值转换成const
ps1:f()需要一个const的int指针作为输入,此时给他一个非const值,是没问题的。
ps2:把const值给f(),也没问题。
(2)const数组
数组实质上可以看成const的指针。
1.9.3 数组变量
ps1:取数组的地址可以不用加&,直接拿数组变量的名字即可取到数组地址。
ps2:数组的每个单元表示单个的变量,取单元的地址需要用&。
ps3:a的地址便等于a[0]的地址←→a==a[0]
ps4:对于指针变量,可以把p写成p[0]
ps5:对于数组变量,可以写成a。【当把数组赋给一个指针后,可以拿指针像数组一样操作,也可以拿数组像指针一样操作。】
ps6:数组变量间不可以作互相赋值。
- int b[]可以被看作int * const b,const说明b是一个常数,不可以被改变。(即b是一个常量指针)。因此b不能被赋值;也不能再去等于另外一个数组(即两个数组之间不能直接作赋值)。
- 数组与指针有区别。如int a[10],*p; p=a。 p是一个可以变化的指针变量,而a是一个常数,因为数组一但被说明,数组的地址也就确定下来了。因此a是不能变化的,而p可以。
1.9.4 指针的类型
指针类型,如char、int、double等。
❀示例:指向不同的指针不能直接赋值
ps1:会报错——类型不匹配。
1.9.4.1 指针的类型转换
ps1:void*往往用在底层程序/系统程序里。(直接去访问某个内存地址或是直接访问某个内存地址所代表的一些外部设备、控制寄存器等等的时候需要用到此类型)。
ps2:第四行中并没有改变p所指的变量的类型,即i还是int型,只不过通过q看i的时候变成了void。
1.9.5 指针常见错误
❀例题
解析:p指向a[5],也就是说p[0]的内容就是a[5],那么p[-2]就是a[3]。
- 注意下标从0开始。
解析:
- B:a的地址便等于a[0]的地址,而p就是a的地址。
1.9.n 应用场景
1.9.n.0 指针计算
注意:作乘除没有任何的意义。
(1)给指针加一个整数
ps1:sizeof(char)=1;sizeof(int)=4。
ps2:指针+1不是在地址值上+1,而是在地址值上+sizeof(指针所指类型)。
ps3:对指针+1的动作意味着让指针指向下一个单元。
(2)两个指针相减
两个指针相减返回的是 地址的差/sizeof(类型)。【即在这俩地址中间能放几个这样类型的东西】
(3)*p++
经常用于数组类的连续空间操作,如遍历。
遍历
ps1:-1作为结尾的标志。用旧遍历方法会输出-1,用新方法则不会输出-1。
(4)指针比较
(5)0地址
现代的操作系统,都是多进程操作系统,基本的管理单元是进程。对于进程,操作系统会给它一个虚拟的地址空间。即所有的程序在运行的时候,都以为自己具有从0开始的一片连续空间。【任何程序里都有0地址】
ps1:不能碰即不能做任何事情,甚至是读。否则程序崩溃。
ps2:有的编译器里NULL和0是不相等的。因此表示0地址最好用NULL。
1.9.n.1 输入数据——动态内存分配
引入:程序要输入数据,则先告诉个数,然后再输入(在输入的过程中需要记录每个数据)。
- C99可以用变量做数组定义的大小。
- C99之前则是通过malloc()函数作动态内存分配。
(1)malloc()函数
需要添加头文件#include<stdlib.h>
malloc()参数需要的是数组占据多少空间(以字节为单位)。
malloc返回的结果是void。
malloc给系统去要了一块空间,用完了必须得释放。free()
引入:malloc是跟系统要空间,空间总是有限的,如果空间用完了,malloc会返回0(也就是NULL)。
引入:malloc得到的空间是连续的吗?你得到的空间的实际大小是否就是你要求的大小呢?如果你malloc零长度会得到什么结果呢?
相邻两次malloc得到的空间一般不是连续的,malloc从空闲内存列表中寻找合适的空间进行分配。
得到的空间的实际大小一般大于你要求的大小,malloc以最小分配的空间作为基本单位分配空间。
malloc零长度会得到一个最小分配的空间。
(2)free()函数
ps1:如果p++后直接释放。程序会说要释放的指针不是申请来的,程序会异常终止。
ps1:p是个指针,但它不是malloc来的,它里面是另外变量的地址。
ps1:free(NULL)没问题。
因此,由free(NULL)没问题可以知道如下:构造指针时最好初始化为0。如果没有malloc或者malloc得到一个失败的结果,这时候free这个指针得到的结果都没有问题。
如果free(1)是不行的,1这个地址不可以被free。
解决方法:
- 牢牢记住free!!一旦malloc,就对上free。
- 对程序的架构有良好的设计,保证程序有恰当的时机去free。
- 积累经验,编程靠经验。
❀例题
❀示例:测试你的系统能给你多大的空间
#include<stdio.h>
#include<stdlib.h>
int main(void){
void *p;
int cnt=0;
while( p=malloc(100*1024*1024) ){
cnt++;
}
printf("分配了%d00MB的空间",cnt);
return 0;
}
ps1:1024是1K,1024×1024是1M,1024×1024×100是100M。所以p=malloc(1024×1024×100)表示每次申请100M的空间,然后把申请到的空间交给p。
ps2:如果p得到的结果不是0,即它得到了一个有效的地址,那么循环继续。如果得到的结果是0,则while会退出循环。
❀代码:逆序输出所有数字
ps1:参数需要数组占据多少空间,因此numbersizeof(int)
ps2:malloc返回的结果是void,因此需要类型转换,即(int)
ps3:指针可以看作数组,一旦用malloc得到一块空间并交给a后,之后对a的所有操作便可以把a当作数组使用。
ps4:malloc给系统去要了一块空间,用完了必须得释放。free(a)
1.9.n.1 交换两个变量的值
传两个地址:
❀代码:交换两个变量的值
ps1:像swap()函数,用指针来完成两个变量的交换。
1.9.n.2 函数返回多个值
❗注意:函数的返回值只能返回一个。
❀代码:找出数组中的最大值和最小值
ps1:通过指针把要接收的结果的变量的地址传进去,让函数在里面把变量填好,把值传回来。
ps2:int *min和int *max两个参数是从main传进来的参数,但其作用是得到结果。
1.9.n.3 函数返回运算的状态,结果通过指针传回
分开返回:
- 状态用函数的返回即return来返回。
- 而实际的值通过指针参数返回。
如上做的好处是容易把函数的返回结果放在if语句。
❀代码:两个整数做除法
#include<stdio.h>
//如果除法成功,返回1;否则返回0
int main(){
int a=6;
int b=2;
int c;
if(divide(a,b,&c)){
printf("%d/%d=%d\n",a,b,c);
}
return 0;
}
int divide(int a,int b,int *result){
int ret=1;
if(b==0){ //若除数等于0,则直接返回结果0
ret=0;
}else{
*result=a/b;
}
return ret;
}
ps1:如果b是0,程序就没有计算,则c里没有任何有意义的结果。
1.10 枚举
1.10.0 引入:常量符号化
常量符号化:用符号而不是具体的数字来表示程序中的数字。
1.10.1 枚举
1.10.1.1 关于枚举
ps1:enum color就是在声明一种新的数据类型color,声明了之后可以当作int、float这些数据类型来使用。但是使用的时候必须要带上enum。例如:enum color c(表示叫做color的枚举类型)。
ps2:实际上C语言内部enum枚举就是int,每个枚举的变量是可以当作int输入输出的(%d)。
1.10.1.2 枚举量
ps1:YELLOW值是2。
即使给枚举类型的变量赋不存在的整数值也没有任何warning或error。
ps1:直接给color赋初值0,但是COLOR初始化的时候是没有0的。因此0是没有意义的值。
1.10.2 枚举应用
(1) 技巧:自动计数的枚举
ps1:借用了在枚举的定义中,所有的量的值都是依次递增1的。最后一个量的值就是对前面所含内容的计数(便可表示这个枚举类型中到底有多少个符号→)。
ps2: 便可用这最后一个量定义数组,或是判断输入的整数值是否在有效范围内,再或是做遍历。
1.11 结构
1.11.0 思考:结构和数组的区别与联系
1.11.1 声明结构类型
通常在函数外部声明结构类型。
这段代码是在声明一种新的类型
这段代码是在定义这种结构类型的一个变量。
1.11.1.1 声明结构的形式
ps1:第三种是常用的。
1.11.1.2 结构变量
ps1:today是结构的变量。
1.11.2 结构的初始化
放在函数内部的变量叫做本地变量,本地变量是没有默认的初始值的,因此需要初始化。
ps1:方式2没有给day赋值,因此输出是0。
1.11.2.1 结构成员
1.11.2.2 结构指针
ps1:数组变量本身表达地址。
1.11.3 结构运算
ps1:第三行的代码表示 给出两个值5、10,然后强制类型转换把这两个值强制转换成point这种结构的变量,然后赋给p1。
ps2:结论:两个结构变量是可以赋值的。
1.11.4 结构与函数
1.11.4.1 结构作为函数参数
1.11.4.2 输入结构
ps1:分析:
- 让inputPoint()不需要参数,而返回是struct point,即返回一个结构变量。然后在这个输入函数中创建一个临时的结构变量temp(这是一个本地变量),这个变量本身在离开这个函数后会消失。在函数中对temp的x和y做了赋值,最后将temp值返回。
- 在main()调用inputPoint()的地方,让temp的值赋给y(利用两个结构变量可以赋值),这样y就可以得到在inputPoint()传进去的值。
1.11.4.3 结构指针作为函数参数
更好的方式是传指针。
(1)指向结构的指针
(2)结构指针参数
对比1.11.4.2中解决方案中的代码
这是可行的。
1.11.5 结构数组
示例:
1.11.6 结构中的结构
1.11.6.1 嵌套的结构
1.11.6.2 结构中的结构的数组
❀例题
1.12 自定义数据类型 typedef
1.12.1 声明新的类型
ps1:原来的类型是struct,新的名字是Date。于是Date就等价于struct ADate。
1.13 联合
ps1:所有的变量联合起来使用同一个空间。
1.13.1 union的用处
可以得到一个整数/double/float等内部的各个字节。→可以用作如文件操作的工具(如把一个整数以二进制形式写到文件时作读写的中间媒介)
1.14 可变数组
引入:用C语言代码去实现可以变大小的数组。其特点应是可以长大的;可以知晓是多大的;能够访问其中单元。
做法:实现一个函数库,定义一些函数,这些函数可以提供可变数组。
这个函数库里首先应该考虑如下函数:
- Array array_create(int init_size); //创建数组
- void array_free(Array *a); //把数组空间回收
- int array_size(const Array *a); //指明数组目前有多少个单元可以使用
- int* array_at(Array *a,int index); //访问数组中某个单元
- void array_inflate(Array *a,int more_size); //让数组增长
1.14.1 可变数组的实现
1.14.1.1 定义出可变数组Array
定义一个结构。
1.14.1.2 编写头文件array.h
1.14.1.3 编写文件array.c
其实就是要逐一实现array.h中声明的各个函数。
首先在头部:
#include “array.h”
(1)Array array_create(int init_size);
ps1:思考为什么返回的是Array变量的本身a,而不是指针?
- 因为a是本地变量,返回指针的话,本地变量便无效了。
- 如果是返回指针的话,就如下图写(但是在这种做法里有潜在的风险,如果使用函数时传给的a有两种可能会造成问题——a=NULL;a已经指向了有效的曾经已经制作的数组时。)因此一般直接用上图方法写create函数。
(2)void array_free(Array *a);
ps1:防止别人调2次。malloc后free0或者NULL是无害的。
(3)int array_size(const Array *a);
封装:用以下做法将a->size保护起来。
(4)int* array_at(Array *a,int index);
使用array_at()函数:
get()和set()示例:
(5)void array_inflate(Array *a,int more_size);
(6)main()
1.14.2 可变数组的缺陷
效率极低:每一次可变数组的增长都要进行数据的全部复制,那么假设有几百万个数据,每次增长都重新复制,那在很大程度上降低了程序的效率。
浪费内存空间。
1.15 链表
(1)关于结点的C定义
(2)插入一个新元素
(3)搜索
(4)删除某结点
(5)链表的清除
1.16 文件
1.16.1 文件输入输出
-
来把它的输出写入另一个文件。
- <指定用一个文件来作为输入。
1.16.1.1 fopen
❀代码:打开文件
1.16.2 二进制文件
1.16.2.1 文本文件VS二进制文件
1.16.2.2 二进制读写
1.16.2.3 在文件中定位
第二章 ACLLib图像界面程序设计
2.1 简单介绍
2.1.1 ACLLib
2.1.2 Windows API
2.1.3 main()与WinMain()
2.2 DEV C++建ACLLib项目
(1)新建一个项目
保存好后,会看见有个默认的main.c,经保存运行即可看见出现了一个窗口。
第三章 交互图形设计
【未学待续…】