视频链接:
1_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1hx411h73g?p=1&vd_source=dc81e66a7eee703a56627110163b53ef视频版本可能有些老,但是不影响,还有一个更清晰的版本
【 C语言程序设计精髓】 国家精品课 哈尔滨工业大学 苏小红教授_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1su4y1W7CR/?spm_id_from=333.337.search-card.all.click&vd_source=dc81e66a7eee703a56627110163b53ef无论是哪个版本内容都差不多,核心的东西都是一样的,以下是我自己的笔记,可能比较适合自己,因此会有一些地方比较简陋。
笔记下载链接:【因为复制过来的笔记有些文本上的问题】
链接:https://pan.baidu.com/s/1GSrLT4g0bsw50HZJJVVD_A
提取码:tf7s
第一章 为什么学习C语言
1.计算机=硬件+软件
2.程序是软件重要组成部分,软件还包括数据和文档
第二章 C数据类型
1.#include <stdio.h>
#开头,编译预处理指令
-
main()
主函数,任何程序都是从主函数开始执行 -
{ }
表示函数的函数体,执行函数的一系列操作 -
printf(“”)
表示打印输出xxx内容 -
/n
表示换行操作,注意每一行结尾的;
-
关键字,又称保留字,譬如
return,int
-
标识符,
main()
主函数,printf()
打印输出,scanf()
输入 -
运算符,+等
-
分隔符, ``空格、回车/换行、逗号等
-
其他符号,
{ }
表示函数体或语句块、/* */
、//
程序注释的定界符
一、常量
包括整型(如: 0,-2,123L表示长整型,123u表示无符号整型,022表示八进制,0x12表示十六进制)
默认 int
**实型(如:2.3表示十进制小数,1.2e-5表示指数形式,2.73F表示单精度,2.73L表示长双精度)**
默认double
字符型,用`' '`表示,譬如`'/n'`,表示换行
注意:
'd\'
表示以十进制整型来输出值
'f\'
表示以单精度实型来输出值
'c\'
表示以字符型的格式来输出值
'%lf'
表示以双精度实型来输出值
二、变量
1.必须先声明,随后再使用
不给变量赋初值时,其运算结果是**随机数**
或者采用赋值表达式,注意赋值的过程是从**右往左**进行的;【多个赋值亦是】
2.内存特点:速度快,可随机访问,但掉电即失,按照字节编址,八位;
是一个十六进制无符号整数,字长与主机相同,32位计算机的内存地址编码是32位,从
0x00000000到0xFFFFFFFF;
3.衡量存储数据的最基本单元是位,也叫比特
Q&A:为什么用二进制存储,而不用十进制?
双稳态再电器元件中容易实现;
计算机二进制计算简单;
4.计算机存储单位
b-位(比特) Byte(B)-字节-**1B=8b** KB-K-**1KB=1024B**
MB-兆-**1MB=1024KB** GB-G-**1GB=1024MB** TB-T-**1TB=1024GB**
5.变量的类型决定了:
①占用内存空间的大小;②数据的存储形式;③合法的取值范围;④可参与的运算种类
三、C数据类型
-
整型:int 占用4个字节,32位; short占用2个字节,16位;long int 占用4个字节,32位;unsigned
实型:单精度,4个字节,32位;double,8个字节,64位;long double,与系统相关,在 VC++上占8个字节;char,1个字节
2.实型的存储
定点数:12.123
浮点数:1.2123×10^1
N = S X r^j r为基数,通常取2或10【几进制】
阶码j(指数部分) 尾数S(小数部分)
阶码符号 阶码的数值 尾数符号 尾数的数值
3.奇偶校验:校验代码输出过程是否有错误,规定1的个数为奇数 → 校验位置为1,否则置为0.偶校
验正好相反。
4.为实现语言的统一,采用unicode字符集,设定统一且唯一的数字编号用2个字节保存,宽字节字
符65536个字符。
5.数值溢出的危害:编译器对其熟视无睹
措施:选择取值范围更大的变量类型;用**sizeof(运算符)**获得变量或者数据类型的长度
-
sizeof 两种语法形式:sizeof(类型),结果为类型占用的字节数;sizeof(表达式),结果为表达式值所属类型占用的字节数
-
注意:
int a,b;
这种可以int a=b=0;
语句错误 -
C语言的语言标识符不能以数字开头!只能用大小写字母或者_开头[数字地位太低]
第三章 简单的算术运算和表达式
-
例如:
W + Z
运算符是+
,操作数是W
和Z
,叫二元操作数;操作数包括+
-
*
/
,%
求余。 -
分离百位、十位、个位:求余 (个位) 153%10 = 3 取整(百位) 153/100=1 (十位)去掉最高位/最低位 153-1*100=53 53/10=5
-
三种赋值形式:①简单赋值;②多重赋值;③复合的赋值
变量x 运算符op = 表达式;
变量x = 变量x 运算符op 表达式;
4.复合的赋值运算
a+=10→a=a+10→把a+10的值赋给a a-=5→a=a-5→把a-5的值赋给a
a=3→a=a3→把a*3的值赋给a a/=4→a=a/4→把a/4的值赋给a
5.增1和减1运算
a++【后缀形式】:先取a的值,然后再加1;m=a++,先让m=a,然后a++
++a【前缀形式】:先加1,再取a的值;n=++a,先让a++,然后n=a加1后的值
a--, --a亦然
6.程序中直接使用的常数叫做幻数。
缺点:可读性差;容易发生书写错误;当常数需要更改时,工作大,容易遗忘
解决办法:定义为宏常量,#define 标识符 字符串
宏常量也叫符号常量,一般采用全大写字母表示;宏常量不是语句,而是一种编译预处理指令。
例子:
#include <stdio.h>
#defind PI 3.1415 //宏常量,减少出错次数
main()
{
double r;
double circum;
double area;
printf("Input r:");
scanf("%lf",&r); //%lf 表示双精度实型输出,因为上面double所以采用双精度实型输出
circum = 2 * PI * r;//宏替换
area = PI * r * r;
printf("circumperence = %f\n",circum);//%f\n表示按照单精度实型输出,也叫按照浮点数进行输出
printf("area = %f\n",area);
}
同时还可以定义const常量,比宏常量好在有类型了,譬如上述例子,可以写成
#include <stdio.h>
main(){
const double pi = 3.1415;
...后续与上文一致
}
7.从低向高的顺序为:
chart、short → int →unsigned → long → double
**float** → **double**
(类型)表达式
#include <stdio.h>
int x = 10;
float y;
y = (float)x;//强制转换,避免编译器报错
x = 10, y = 10.000000
- 标准数值函数
sqrt(x) 计算x的平方根,x应该大于或等于0; exp(x) 计算e^x的值;
fabs(x) 计算x的绝对值; pow(x,y)计算x^y的值;
log(x) 计算lnx的值,x要大于0;sin(x),cos(x);
log10(x)计算lgx的值,x要大于0
sortedSquares
第四章 键盘输入与屏幕输出
1.常见字符常量
'\n' :换行 ****'\a' : *响铃报警提示音* '\r':回车(不换行) '\"':一个双引号
'\0':空字符,通常用作程序结束标志 '\'':单引号
'\t':水平制表 '\':反斜线 '\v':垂直制表 '\?':问号
'\b':退格 '\ddd':1~3位八进制ASCII码值所表示的字符
'\f':走纸换页 '\xhh':1~2位十六进制ASCII码值所表示的字符
2.putchar(ch) (字符输出函数)向屏幕输出一个字符,字符型变量ch的值。
getchar( ) (字符输入函数)无参数,函数值从输入设备接收的字符。
例子:小写字母ASCII码值要比大写字母ASCII码值大32
#include <stdio.h>
main()
{
char ch;
printf("Press a key and then press Enter:");
ch = getchar();// getchar函数无参数,表示接受字符
ch = ch + 32;
putchar(ch);//显示变量ch中的字符,表示输入字符
putchar('\n');//换行
}
总结:putcahrt(ch)表示,输入ch字符的值;
getchart()表示,接受设备字符
3.printf()输出
%d 十进制整数形式输出 %f以浮点数形式(小数点后六位)输出
%s 输出一个字符串 %c 以字符形式输出单个字符
%u 以十进制无符号整数输出 %o 以八进制无符号输出
%x 以十六进制无符号输出
m 右对齐 -m左对齐 I长整型整数 L长双精度实型数
4.scanf()输入
scanf("%d,%f",&a,&b); &a表示可变长度的输入地址表
输入无法控制精度
5.抑制字符 %2d %2d %2d 输入123456,输出12, 56 ,%*2d表示跳过中间两位
6.指定类型和格式输出:printf() 指定类型和格式输入:scanf()
输出单个字符:putchar() 输入单个字符:getchar()
第五章 选择控制结构
1.算法的特性:确定性、有效性、必须有输出【输入有无均可】
2.算法描述:自然语言描述、传统流程图、N-S结构化流程图、伪码描述【学术经常用到】
3.C语言中关系运算符及其优先级
<、>、<=(大于或等于)、>=【优先级高】
==(判断等于)、!=(等于≠)【优先级低】
结果只有1和0,0是假,1是真
例子:3>2>1 → (3>2)>1 → 1 > 1 → 0
4.选择结构:单分支(if)、双分支(if-else)、多分支(else-if)
Pay more attention on below words!!!
if语句
表达式1 ? 表达式2 : 表达式3
表示的含义是判断表达式1的值如果为真,则取表达式2的值;若为假,则取表达式3的值多分支
还可以采用开关语句 switch(表达式)
switch(表达式)
{
case value1:
语句1;
break;
default:
printf("Others\n");
break;
}
注意: if(0=data)语句,错误
5.逻辑运算符
①&& 与 当且仅当两者都为真,结果为真
②|| 或 只要有一个为真,结果为真
③! 非 取补集
优先级:③>①>②
逻辑运算符&&和|| 都是对操作数进行"短路"计算,即表达式的值可有先计算的做操作数的值单独推导出来,那么将不计算有操作数的值。
6.常见错误
①编译错误;②链接错误;③运行时错误;
7.软件测试方法的分类
①白盒测试(结构测试),测试早期;②黑盒测试(功能测试),测试后期
第六章 循环控制结构
一、循环控制结构和循环语句
1.循环控制方法
条件控制;标记控制
2.for循环,当型循环,先判断是否为真
适合于计数控制
for(表达式1;表达式2;表达式3)
{
语句1
语句2
}
表达式1表示 循环初始条件
表达式2表示 循环结束条件,控制循环次数
表达式3表示 循环转化条件
3.while 循环
语句0 //循环初始条件
while(表达式) //循环结束条件
{
语句1 //循环变量增值
语句2
}
4.do while循环
语句0 //循环初始条件
do{
语句1 //循环变量增值
语句2
}while(表达式); //循环结束条件
二、计数控制的循环
1.可以单独计算,也可以利用前后两个数进行计算
2.猜数游戏用到的库函数:rand()
产生[0,RABD_MAX]间的随机数:-magic = rand(); #include <stdlib.h>:-RAND_MAX在stdlib.h中定义,不大于双字节整数的最大值32767 产生[0,b-1]之间的随机数:-magic = rand()%b; 产生[a,a+b-1]之间的随机数:-magic = rand()%b + a;
3.rand()是伪随机,将生成的随机数足够随机
随机函数srand():为函数rand()设置随机数种子,使产生的随机数“随机化”
通过键入随机数种子,产生[1,100]之间的随机数
scanf("%u" , &seed);//seed随机数,unsigned
srand(seed);//设置种子值
magic = rand() % 100 + 1;
4.与此同时,更改为用系统时间作为随机数种子更好
#include <stdio.h>
srand(time(NULL));
magic = rand() % 100 + 1;
函数time()趣得系统时间的方法:①函数参数;②函数返回值
5.scanf()的返回值是表示正确读入的个数
注意:while(); 等价于 while(){…}循环体为空语句
i = 1;
do{
i++;
}while(i<=n);
for和while只要条件判断为假,一次都不执行;do…while会执行一次
三、循环控制结构
1.计数控制循环[for];条件控制循环[while、do…while];标记控制循环
2.流程转移控制:break和continue语句对for、while、do - while循环进行内部手术
continue与break的区别:①continue表示跳出本次循环,而进行下一次的循环;②break表示跳出循环,不在执行内层循环;
goto语句举例: goto error; 空语句
break、continue本质上是受限的goto
include … exit(0); 终止程序
3.标志变量:程序可读性更好
四、补充知识
1.结构化程序设计:1965年,E·W·Dijkstra在一次国际会议首次提出。
2.结构化程序设计是一种进行程序设计的原则和方法。避免使用goto语句,采用“自顶向下、逐步求精”的方法进行程序设计。
3.衡量程序质量的首要条件:结构清晰、容易阅读、容易修改、容易验证。
4.混乱的根源不在goto语句,儿在语句标号。某些情况下使用goto语句可以使程序更加清晰。
5.两种情况适合goto语句:
①跳向共同的出口位置,进行退出前的处理工作;
②跳出多重循环的一条捷径。
6.代码规范:
①良好的注释;②整齐的缩进;③适当的空行;④见名知意的含义;⑤行内空格,单行清晰度;⑥每行最多一条语句;
7.常用的程序调试与排错方式
(1)调试的基本方法:
粗分析找,定位大致的范围。 采用注释的办法切掉一些代码,减少有关的代码区域。 缩减输入数据,设法找到能导致失败的最小输入。 插入打印语句,观看中间输出结果。
(2)选择循环语句的原则:
如果循环次数已知,用for;
如果循环次数未知,条件或标记控制的循环用while语句;
如果循环体至少执行一次,用do-while语句
(3)使用循环的注意事项
在for、while语句之后一般无分号,有分号表示循环体为空语句。表示循环体内什么都不用做
for(i = 0; i < 100; i++);//用于延时
while(i < 100);
i++;
//将一直运行到世纪末日,陷入死循环
(4)使用嵌套循环的注意事项
内层和外层循环控制变量不能同名,防止造成混乱使用复合语句,保证逻辑上的正确性。
采用右缩进格式书写,来保证层次的清晰性。
(5)流程转移控制的注意事项
主张少用、慎用,而不是禁用goto;
保证使用之后,程序仍然是单入口和单出口;
不要使用一个以上的goto语句标号;
使用goto不要往回跳,要往下跳;
不要让goto语句制造出永远不会被执行的代码;
8.本章常用算法
(1)统计:设置一个计数变量K;初始化为1,每技术一次将其加1,K++;
(2)累乘求积(求阶乘):p = p * term;【p的初始值要为1】
(3)累加求和:sum = sum + term;
无论是累乘求积还是累加求和,关键都是要找出通项;
int sum = 0;
for(int i = 1; i <= 99; i = i + 2){
term = i * (i + 1) * (i + 2);
sum += term;
}
int sum = 1;
for(int n = 2 ; n <= 100 ; n = n + 2)
{
term = n * n / ((n - 1) * (n + 1));
sum *= term;
}
do{
int sum = 0;
for(int n = 1; n = n + 1)
{
sign = -sign;
term = sign / n;
sum += term;
}
}while(fabs(term) >= 1e-4);
第七章 函数
一、函数
1.一个C语言程序是由一个或多个源程序文件组成,而一个源程序文件又是由一个或多个函数组成。
2.main() → 调用函数fun → 输出结果
→fun函数
3.函数的分类
①标准库函数
②第三方库函数,e.g. Average()函数【整型一定有返回值return】
int main()
{
......
return 0;
}
【注意】
int Average(int x, int y)//函数名(形参表)
{
int result;//内部变量
result = (x + y)/2;
return result;
}
1).函数名可以方便调用函数;
2).形参表,接受调用者传入的参数;
3).函数内部定义只能自己使用的变量,成内部变量;
4).return语句只能返回**一个**值给调用者,int Average...与返回值类型result类型一致
4.形式参数,简称形参
int Average(int x, int y) { int result; result = (x + y)/2; return result; }
实际参数,简称实参
main()
{
int a = 12, b =24;
…
printf("%d\n",Average a,b);
…
}
函数调用时,参数传递方向是单向的。 实参 → 形参
要求:①数目一致;②类型匹配;③顺序一致。【否则发生自动类型转换】
开始执行函数内的语句,当执行到return语句或者}时函数退出。
实参的类型和数量以及顺序必须与形参一致。
函数退出时,返回值给调用者,作为函数调用表达式的结果;同时程序控制权交给调用者,收回分配给函数内所有变量(包括形参)的内存
5.计算两个数的最大值
**两种调用形式:**
-
c = max(a , b);
-
printf("%d\n", max(a,b) );
拓展:
计算三个数
c = max( max ( a , b ) , c );
6.函数封装是指使外的影响仅限于入口参数。函数对外界的影响仅限于一个返回值和数组、指针的形参。
7.assert断言:用于测试某种不可能发生的状况确实不会发生,在使用时,应该加入
#include <stdio.h>
void assert(int experession);
expression为真,无声无息;为假,中断程序。
断言在Debug版下有效,Release版下失效,它仅用于调试程序,不能作为程序的功能。
8.使用断言的几种情况:
①检查函数入口参数的合法性;②在一段计算的结束处检验计算结果是否在合理的范围内;③检查程序中的各种假设的正确性;④证实或测试某种不可能发生的状况确实不会发生。
9.嵌套调用是在调用一个函数的过程中又调用另一个函数。
递归调用:函数自己调用自己
递归函数 = 基本条件 + 一般条件
unsigned long fact(unsigned int n)
{
if (n == 0 || n == 1)//①
return 1;//② ①~② 基本条件
else//
return n * fact(n - 1);//一般条件
}
总结:
任何一个递归调用程序必须包括两部分:
if(递归终止条件成立)
return 递归公式的初值;//基本条件控制递归调用结束
else
return 递归函数调用返回的结果值;//一般条件控制递归调用向着基本条件的方向进行
二、函数与变量的作用域
1.递归算法的优缺点
-
优点:递归方法简洁直观、容易编写、逻辑清晰、可读性好、与数学公式接近;
-
缺点:占用空间大、重复计算多、效率低
-
因此要尽量避免使用
2.例题解析
#include <stdio.h>
long Fib (int n)
{
long f;
if (n == 0) f = 0;
else if (n == 1) f = 1;
else f = Fib( n - 1 ) + Fib ( n - 2 );
return f;
}
3.一下使用递归的情况:
-
定义的本身就是递归的,譬如计算阶乘;
-
数据结构是递归的,譬如单向链表;
-
问题的解法是递归的;
4.例题分析
#include <stdio.h>
void Hanoi( int n, char a, char b, char c);//将n个圆盘借助C从a移动到b
void Move( int n, char a, char b);
{
if (n == 1)
{
Move (n, a, b);//将n号圆盘从a移动到b
}
else
{
Hanoi(n-1, a, c, b);//将n-1个圆盘从a移动到c
Move(n, a, b);
Hanoi(n-1, c, b, a);//将n-1个圆盘从c移动到b
}
printf("Move %d: form %c to %c\n", n, a, b);
}
int main()
{
int n;
printf("Input the number of diaks:");
scanf("%d",&n);
printf("Steps of moving %d disks from A to B by means of C:\n", n);
Hanoi(n, 'A', 'B', 'C');//调用递归函数Hanoi()将n个圆盘借助于C由A移动到B
}
三、变量的作用域和存储类型
1.变量的作用域,指在源程序中定义变量的位置及其能被读写访问的范围。
2.局部变量,在语句块内(函数、复合语句)定义的变量。【静态/动态】
特点:有效范围仅为该语句块(函数、复合语句),仅能由语句块内语句访问,退出语句块时释放内存,不再有效。
3.全局变量,在所有函数之外定义的变量。定义点之前使用全局变量,需要extern声明。【静态外部变量/外部变量】
特点:有效范围是从定义变量的位置开始,到本程序结束。
4.同一作用域内变量不能重名。【全局变量没有初始化时,自动初始化为0】
变量同名的情况:
①并列语句块内各自定义(不同作用域)的变量同名;
②局部变量与全局变量同名;【局部变量会隐藏全局变量】;
【注意】
-
加入变量同名,只要作用域不同,新的声明将隐藏旧的声明。
-
编译器通过将同名变量映射到不同的内存地址来实现对作用域的划分;但是假设同名变量出现在一个作用域之内,编译器也将束手无策;
-
局部变量存储在动态存储区,全局变量则在静态存储区;
-
全局变量的优点:使得函数之间的数据交换更加容易,更加高效;
5.例子:费伯纳西数列
#include <stdio.h>
long Fib(int a);
int count;
int main()
{
int n, i, x;
printf("Input n:");
scanf("%d",&n);
for(i = 1, i <= n; i++)
{
count = 0;
x = Fib(i);
printf("Fib(%d) = %d, count = %d\n", i, x, count);
}
return 0;
}
long Fib(int n)
{
count++;
if (n == 0) return 0;
else if (n == 1) return 1;
else return (Fib(n - 1) + Fib(n - 2));
}
尽量不用全局变量,原因:
① 全局变量的作用域是全局的,所有的函数里都可改写它,所以很难确定是谁改写了它,在哪里修改了它;
②很难在其他程序中复用;
③修改程序时,若改变全局变量(如类型),则需检查同一文件中的每个函数,已确认改变化对函数的影响程度;
- 同名变量在不同的作用域下的表现,传递给形参的是值,而不是地址
四、变量的存储类型
1.内存中供用户使用的存储空间可分为:
①代码区;②常量区;③静态储存区;④动态储存区
静态是表示事情发生在程序构建的便宜时和链接时,而非程序实际开始运行的载入时和运行时。
动态表示事情发生在程序载入时和运行时。【宾馆】
变量的存储类型指在内存中存储(编译器为变量分配内存)的方式。
静态存储方式,是指在程序运行期间分配固定的存储空间的方式。【分配宿舍】
2.声明变量的存储类型
存储类型 数据类型 变量名;
C存储类型关键字
-
auto(自动变量) ——动态局部变量:离开函数,值消失。
auto 数据类型 变量名;auto可以省略
-
static(静态变量)
static 数据类型 变量名;
-
extern(外部变量)
-
register(寄存器变量,CPU内部)
register 类型名 变量名;
五、模块化程序设计——信息隐藏
-
1.优点:便于单个模块的设计、开发、调试、测试与维护。
模块各司其职:每个模块只负责一件事情,诸葛模块完成,最后将他们集成。
开发人员各司其职:按模块分配任务,职责明确,并行开发,缩短开发时间。
2.模块分解的过程:自顶向下,逐步求精的过程。由不断的自底向上修正所补充的自顶向下的程序设计方法。
3.模块分解的基本原则:保证模块的相对独立性。高聚合(自身),低耦合;模块的实现细节对外不可见。
4.参数被声明为const,保护参数不被改写
5.函数规模要小,函数功能要单一,函数接口定义要清楚;入口参数有效性检查,敏感操作前的检查,调用成功与否的检查,
适当位置写注释
/* 函数功能: 实现......功能
函数参数: 参数1,表示...
参数2,表示...
函数返回值: ......
*/
6.Linux/UNIX风格的函数名命名:function_name;
Windows风格的函数名命名是用大写字母开头、大小写混排的单词组合而成。如FunctionName;
变量名形式为“n.”或者“adj.+n.”如oldValue与newValue等;
函数名形式为“v.”或者“v.+n.”(动宾词组),如GetMax()等。
第八章 数组
一、数组
数组是一种保存大量同类型的相关数据的数据类型。
1.一维数组的定义:
int a[10]; int 数据类型 a数组名字 [n]代表元素个数;表示定义一个有10个int类型元素的一维数组。系统在内存分配连续的10个int空间。
2.一维数组的定义:
int a[10]; 大小最好用宏定义,以事应位来的变化。
#define N 10
int a[N];
在一维数组引时,数组名[下标];引用时下标允许是int型变量或表达式;这种方式允许快速随机访问,如使用a[i]这样的形式访问每一个元素,i = 0, 1, 2, … , 9
3.一维数组的初始化
4.如何使两个数组的值相等
方法一:逐个元素赋值 b[0] = a[0];
方法二:通过循环语句赋值
int i;
for (i = 0; i < 4; i++){
b[i] = a[i];
}
5.更高效的数组初始化和赋值
-
更高效的数组初始化方法:
memset (a, 0, sizeof(a));
-
更高效的数组赋值方法:
memcpy (b, a, sizeof(a));
-
用sizeof (a)来获得数组a所占的内存字节数;
-
使用以上两个函数需要包含相应的头文件:
#include <string.h>
注意:sizeof()是用来计算数组在内存中占用的字节数
6.二维数组: int b[2][3];两行三列,按行存取
二、向函数传递一维数组
1.普通变量作函数参数——按值传参:拷贝实参的副本给形参,实参和形参占据不同的存储空间;
2.数组传参——按引用传参:拷贝一个地址比拷贝全部数组元素高效,实参与形参数组占同一段内存;
3.常见算法:
-
Sort算法(排序算法):交换法排序、选择法排序、插入排序
①交换法排序,通过n - 1次比较
#include <stdio.h>
#define N 40
int ReadScore(int score[]);
void DataSort(int score[], int n);
void PrintScore(int score[],int n);
int main()
{
int o, k, temp;
for(i = 0; i < n - 1; i++)
{
for(j = i + 1; j < n; j++)
{
if(score[j] > score[i])//成绩按照降序排序
//"交换score[j]和score[i]"
temp = score[j];
score[j] = score[i];
score[i] = temp;
}
}
②选择法排序,
#include <stdio.h>
for(i = 0; i < n - 1; i++)
{
k = i;
for(j = i + 1; j < n; j++)
{
if(score[j] > score[i]){
/*记录此轮比较中最高分的元素下表 k = i;*/
k = j;
}
}
if(k != i)
{
temp1 = score[k];
score[i] = score [j];
score[i] = temp1;
temp2 = num[k];
num[k] = num[i];
num[i] = temp2;
}
/*若k中记录的最大数不在位置i,则
“交换成绩score[k]和score[i]”
"加平缓学号num[k]和num[i]"*/
}
const变量表示的是,此变量的值是只读的,不应该被改变。const变量必须被初始化;如果在程序中试图修改const变量的值,在编译的时候,编译器将给出错误提示。
-
查找算法:顺序查找、折半查找
①顺序查找
int LinSearch(long num[], long x, int n){
int i;
for(i = 0; i < n; i++)
{
if (num[i] == x)
{
return i;
}
}
return -1;
}
②折半查找(要求一定排好序)
int BinSearch(long num[], long x, int n)
{
int low, high, mid;
low = 0;
high = n - 1;
while(low <= high)
{
mid = (low + high) / 2; /*容易溢出*/
if(x > num[mid])
{
low = mid + 1;
}
else if (x < num[mid])
{
high = mid - 1;
}
else
{
return mid;
}
}
return -1;
}
上述代码存在问题:mid = (low + high) / 2;这句话容易产生溢出,譬如,(50+60)/2 = 55;
等价于50 + (60 - 50)/2 = 50 + 5 = 55,也就是将代码改写成:mid = low + (high - low) / 2;改变过程之中的中间值,因此解决了溢出的问题。
第九章 指针
一、数组与指针
1.如何定义数组
①给出数组存储类型、类型、名字和长度;
static int a[10];【int 表示每一个占4位;在静态存储区;数组的名字表示数组首位的地址&a】
int b[2][3];【第一个[]表示行,第二个[]表示列,是按行进行存储的;二维长度必须给出】
2.直接寻址:按变量的地址直接访问;
scanf语句常见错误:
①不给地址值,会导致系统默认i的值就是地址值
int i;
scanf("%d",i);
②char占一个字节,%d占用4个字节,会导致程序溢出
char c;
scanf("%d",&c);
3.间接寻址:通过存放变量地址的变量间接访问。【指针类型变量】
指针基类型用于去理解这些数据int , char …
指针变量在使用之前必须进行初始化操作,实在不知道初始值是啥,因此可以初始化为NULL, pa = &a; 表示pa指针指向a
二、指针
1.指针变量只能指向同一基类型的变量。
2.在书写程序时候,只能写成 int *pa, *pb;
而不能写成int *pa,pb;
前者表示正常指针,后者表示仅有pa是指针;在间接寻址方法,可以通过指针变量在去输出值,int *pa = &a, *pb = &b; char *pc = &c;
****pa表示指针运算符,和&a取地址运算符是一对,前者取变量内容,后者取变量的地址 ****
关注两个点:①指针指向哪里;②指针指向内存单元之中的内容
必须进行初始化,并且必须是同一种类型
3.形参和实参是单项传递的,形参改变不会影响实参的值;想要改变实参的值,采用指针作为函数实参进行按引用调用
把一个地址传递给指针形参,拷贝的是地址值
按引用调用,即指针变量作函数形参,可以修改相应实参的值为函数提供修改变量值的手段。【即修改实参,必传地址】
4.函数指针:【函数入口地址】
数据类型 (*指针名) (形参列表);
就是之下昂函数的指针,譬如int (*f) (int a, int b);
函数指针f指向的函数原型为:int 函数名 (int a, int b);
令f = fun就是让f指向函数fun();
不建议int (*f) ();
5.swap交换函数: Swap(&a[i], &a[k]);
6.取地址&
第十章 字符串
一、字符串
1.字符串常量:一串以'\0'结尾的字符在C语言中被看作字符串;用双引号括起来的遗传字符就是字符串常量,C语言自动会田间'\0'结束符。【只读,不可修改】
2.字符数组:每个元素都是字符类型的数组,例:char str[80];
【注意:字符数组要想表示字符串,必须在其尾部添加\0】
3.字符数组初始化:
用字符型数据对数组进行初始化 char str[6] ={'C','h','i','n','a','\0'};
用字符串常量直接对数组初始化 char str[6]={"China"};char str[6] = "China"; char str[] = "China";
4.例如:char *pStr;p Str = "Hello China";
pStr的值(指向)可以修改,但它所指向的字符串常量是不允许修改的
倘若在函数内定义,保存在动态存储区(栈),可以修改;
倘若在函数外定义,或者定义为statistics静态数组,保存在静态存储区,也可以修改,但是只能修改内容,数组名(表示数组首地址)不允许修改,是地址常量
5.基本原则:①明确字符串保存到哪里;②明确字符指针指向哪里
6.字符串的输入输出:char str[10]
for(i=0; str[i]!= '\0'; i++){
putchar(str[i]);
}
putchar('\n');
str[i]!= '\0' 判断是否到达字符串结尾
char str[10]
#include <stdio.h>
//不能输入带空格的字符串
scanf("%s",str);
printf("%s",str);
//可以输入带空格的字符串
gets(str);
puts(str);
【注意】
scanf();
为什么不能输入有空格的字符串?
%d输入数字或者%s输入字符串,忽略空格、回车、制表符等字符,系统读到会默认程序已经结束
并且scanf()不读走回车,回车仍在缓冲区
gets();
为什么可以输入带空格的字符串?
空格和制表符都是字符串的一部分,以回车作为字符串的终止符,同时将回车从缓冲区读走,但是不作为字符串的一部分。
7.在字符串的输入输出过程之中,假如要求输入数字,用户输入的是非数字的时候,非数字字符并不会被读走,程序会反复执行从缓冲区读数据、退出….即陷入死循环
解决方法:在程序中添加”清除“语句用来清理缓冲区
Plain Text while ((ch = getchar()) != '\n');
表示读入字符并判断是否是换行,不是的话,继续读入
因此,**判断scanf()返回成功读取的数据的个数,可以避免死机的问题,但是一定要清空缓冲区**
**scanf(" ");表示清空缓冲区的空白符**
8.`scanf(" ");`
`scanf("%c", &ch);`
等价于 `scanf(" %c", &ch);`
9.fgets(name, sizeof(name), stdin); fgets(保存到哪里,限制字符串长度,)
10.str2 == str1,这句话错误,因为数组名代表数组首元素的地址,地址没办法进行比较
表示读入字符并判断是否是换行,不是的话,继续读入
因此,**判断scanf()返回成功读取的数据的个数,可以避免死机的问题,但是一定要清空缓冲区**
**scanf(" ");表示清空缓冲区的空白符**
8.`scanf(" ");`
`scanf("%c", &ch);`
等价于 `scanf(" %c", &ch);`
9.fgets(name, sizeof(name), stdin); fgets(保存到哪里,限制字符串长度,)
10.str2 == str1,这句话错误,因为数组名代表数组首元素的地址,地址没办法进行比较
C
include
strlen(字符串); //string length,计算的是不包括、0的实际字符个数 strcpy(目的字符串,源字符串);//string copy,将后面的字符串赋值给前面的,因此要求前面的必须足够大,交换 strcat(目的字符串,源字符串);//string combination,字符串结合,要求前面的必须足够大 strcmp(字符串1,字符串2);//string comparsion,字符串比较
**在进行字符串比较大小的时候,当出现第一队不相等的字符时,就由两个字符决定所在字符串的大小,返回其ASCII码比较的结果值**
11.容易引发缓冲区溢出攻击、不安全的函数:**gets()、 scanf()、 strcpy()**等不限制字符串长度,不对数组越界进行检查和限制,导致用的堆栈数据被覆盖,给黑客攻击有了可乘之机。
轻者程序运行失败、系统崩溃和重启等;重者利用缓冲区溢出,执行非授权指令,甚至趣得系统特权,进而进行各种非法操作。
12.**向函数传递字符串时,既可用字符数组作函数参数,也可以用字符指针作函数参数,【引用调用】【用字符指针的时候就是判断是不是结束字符;用字符数组的时候采用下标】**
## 二、字符串的相关操作
包括输入输出、拷贝、比较和链接以及如何限制字符串处理长度
1.一个字符串拷贝函数,可以实现后一个拷贝到前一个的操作;
字符串结束标志必须有,才代表字符串
2.计算字符串长度【const指针指向内容、数组元素不可以修改的】
与之前的整型数组不同,之前的知道元素个数,直接`i < n`即可;
但对于字符串来说,不知道元素个数,因此采用`str[i] !='\0';`进行判断
## 三、字符串与指针与数组
1.假如`char *pStr = "China";`相当于把China这个字符串的地址赋值给指针,指针指向China,但是字符串保存在常量存储区【只读】;
2.假如`char str[] = "China";`这时候字符串保存在哪里,取决于函数在哪里定义,要是局部变量【函数内部定义】,字符串就存储在动态存储区;要是全局变量【函数外部定义】,字符串就存储在静态存储区。但均可以修改
3.`char str[] = "China";` `char *pStr =str;` `gets(pStr);`因为定义的数组,让指针指向的是数组元素的首地址,所以是可以修改的。
# 第十一章 指针和数组
**明确一个X型的指针指向一个X型变量的地址。**
## 一、指针和一维数组间的关系
> **a[i] 等价于 *(a + i) p[i] 等价于 *(p + i)**
> **&a[i]** 等价于 &*(a + i)也就是 **a + i**
## 二、二维数组
1.
***(a + i) + j**即**a[i] + j**代表第i行第j列的地址
*(*a + i) + j)表示地址的内容
【注意】
**a + i = &a[i] = *(a+i) = &a[i][0]** 值相等,含义不同
**a + i = &a[i]** 表示第i行首地址,指向行
***(a+i) =a[i] = &a[i][0]** 表示第i行第0列元素地址,指向列
2.指针数组的定义形式:存储多个字符串
**数据类型 *数组名[数组长度];**
## 三、指针数组及其应用
1.指针数组的定义形式:存储多个字符串
**数据类型 *数组名[数组长度];**
2.表示命令行参数:`main (int argc, char* argv[]);`其中,argc表示**参数的数目+1**;argv[0]为**指向命令名的字符指针**;argv[x](x>1)为指向每个参数的字符指针。
3.定长数使用的基本原则:存储长度固定的数据集合时,使用定长数组。无法确切知道要多大存储空间,那就只能在运行时,采用动态分配内存
4.栈:采用后进先出,向低地址扩展;
堆:顺序随意,向高地址扩展。
## 四、动态数组
1.动态内存分配函数:常用于一维数组`void* malloc (unsigned int size);`譬如:p = (int*)malloc(sizeof(int));
用于二维数组`void* calloc (unsigned int num,unsigned int size);`
【注意】
- 在使用动态内存分配函数的时候要使用`#include <stdlib.h>`头文件
- void* 型指针不指定其指向哪一种类型,可以指向任意类型的变量,是一种generic和typeless类型的指针,使用时需强转(Type*)为其他类型。
- **`numsSize = sizeof(nums)/sizeof(nums[0]);`**表示C语言之中,sizeof(nums)表示数组之中元素的个数,sizeof(nums[0])表示数组之中首个元素的个数,两者相除表示整个数组的个数
2.空指针的作用:
①定义指针时进行初始化,避免对未赋值指针的引用;
②作为状态比较;null
**空指针是无效指针,无法被访问**
3.动态内存分配函数的释放:例如:`void* free(void* p);`
free释放malloc()和calloc()申请的内存块
4.在使用二维动态数组的时候要将二维数组当成一维数组去用`i*n=j;`
## 五、常见的内存错误分类
### 1.非法访问内存的错误
- **内存分配未成功却用了**
> 在使用内存之前检查指针是否为空指针
- **内存分配成功,但是尚未初始化就引用了**
> 误以为内存的默认值就全是0
- **内存分配成功,且已初始化,但操作越界**
> 使用数组时经常有多1,少1的操作
用循环语句遍历数组元素时,注意下标从0开始。使用strcpy()、gets()以及memcpy()等函数要注意
- **释放内存后,但还在用**
> 内存释放不代表指针消亡;只是该地址对应的内存是垃圾;不要把局部变量的地址作为函数return的值,因为该内存在函数体结束之后被自动释放;应该尽量把free集中在函数的出口处,若不能就让指针在**free后马上置为NULL**。
- **忘记释放内存,导致内存泄漏**
程序分配了内存块,然后又丢失了 这些内存卡的追踪路径。
在需要时才malloc,并尽量减少malloc次数;
重复使用malloc申请到的内存,尽量让malloc和于之配套的free在一个函数或模块内;
### 2.重复释放,也会产生程序报错!!!
3.const常量:
①**`void f(const int *p);`** p是指向整型常量的指针不允许修改,p指向哪里可以修改;
②**`void f(int * const p);`** p是常量指针,指向整型的常量指针,p指针的指向不允许修改,但是p指针指向的内容是可以修改的【数组名】
③**`void f(const int * const p);`** p是常量指针,指向整形常量,且都不允许修改
4.指针的应用:
①做函数参数,穿引用调用;②动态分配内存,实现动态数组和动态数据结构。
5.数组的使用原则:
①永远清楚每个数组有多大;②永远不要让下标越界;③字符数组永远留意'\0'。
6.指针使用原则:
①永远知道指针指向哪里;②永远清楚指针指向的位置中的内容;③永远让一个x型的指针指向x型变量的地址。
# 第十二章 结构体和共用体
## 一、结构体
有些语言(PL/1等)中试图规定较多的类型,譬如数组、树、栈等。
1.结构体模板
C struct student{//定义一个叫student的结构体类型 ….结构体的成员…. }
2.结构体变量的定义
- 先定义结构体类型再定义变量名:struct student stu1;
- 在定义类型的同时定义变量:
C struct student{ … }stu1;
- 直接定义结构体变量:[不建议]
C struct{ … }stu1;
3.关键字typedef:是一种已存在的类型定义一个别名,并没有定义新类型
C typedef struct student{ … }STUDENT;
4.sizeof(变量或者表达式)/sizeof(类型)
内存对齐,提高性能
## 二、结构体数组
1.定义指向结构体变量的指针
C STUDENT stu1; STUDENT *pt; pt = &stu1; 等价于 STUDENT *pt = &stu1;
通过成员选择运算符访问结构体指针变量所指向的结构体成员
`(*pt).studentID = 1; stu1,studentID = 1;`
通过指向运算符访问结构体指针变量所指向的结构体成员
**`pt -> studentID = 1;`**
2.当结构体嵌套时,访问结构体指针变量所指向的结构体成员
结构体的名字 **`stu1. birthday . year = 1999;`**
结构体的指针变量 **`(*pt).birthday. year = 1999;`**
指向运算符 **`pt->birthday. year = 1999;`**
3.结构体数组的指针
C STUDENT stu[30]; STUDENT *pt; pt = stu; 等价于 STUDENT *pt = stu; 等价于 STUDENT *pt = &syu[0]; 使用pt++,使pt指向stu[1] pt-> studentID等价于stu[1].studentID
4.数组不能作为函数返回值
## 三、共用体【占用位数由最长的决定】
1.占用位数由最长的决定,起作用的是最后一个起作用,初始化操作仅仅对第一个成员进行初始化,共用体与结构体不同,不能作为函数参数、进行比较操作
C union maritalSatate marital;
2.枚举数据类型:描述一组有限个数据值组成的整型值的集合
【注意】结构体声明时不能包含本结构体类型成员,系统将无法为这样的结构体类型分配内存
## 四、链表
1.线性表的链式存储结构;为表示每个元素与后继元素的逻辑关系,除存储元素本身信息外,还要存储其直接后继信息
C struct link{ int data;//数据域 struct link *next;//指针域 };
2.链表常见操作:
- 增:pr -> next = p; pr = p; pr -> next = NULL;
- 删: head = p -> next; free(p);
【删除的不是头结点时】pr ->next = p ->next; free(p);
- 插入:
【头节点】p = (struct link *) malloc (sizeof (struct link));
p -> next = NULL;
p -> data = nodeDate;
【中间插入】 p -> next = pr -> next;
pr -> next = p;
【末尾插入】pr -> next = p;
p -> data = NULL;
- 遍历链表的所有节点
struct link *p = head;
int j = i;
while (p !=NULL)
{
printf("%5d%20d\n", j, p -> data);
p = p -> next;
j++;
}
# 第十三章 文件操作
## 一、
1.文件的好处:①使程序与数据分离;②实现数据共享;③可以实现长期保存数据【断电不易失】。
2.文件分为:流式文件、记录文件
3.流:任意输出的源或者任意输出的目的地。
- 通过一个流(和键盘相关)获得输入;
- 通过另一个流(和屏幕相关)写出输出;
- 较大规模的程序肯需要额外的流如:磁盘文件、网络端口、打印机等
4.C语言中stdio.h提供了三种标准流:
stdin 标准输入 键盘
stdout 标准输出 终端显示器屏幕
stderr 标准错误输出 终端显示器屏幕
- scanf(),getchar(),gets()等通过**stdin**获得输入。
- printf(),putchar(),puts()等用**stdout**进行输出。
5.输入重定向与输出重定向
- 输入重定向 `D:\>demo < infile.txt`从终端(键盘)输入数据改成从文件中读入数据
- 输出重定向 `D:\>demo > outfile.txt`从终端(显示器)输出数据改成文件中写数据
6.按数据的组织形式:**文本文件、二进制文件**
可执行的C程序(二进制文件) C程序的源代码(文本文件)
**【注意】**文本文件和二进制文件有什么区别:
文本文件:用字节表示字符的字符序列,存储每个字符的ASCII码
【如整数123在文本文件中占3个字节,分别存放这3个字符的ASCII码;短整型数123,在内存占2个字节,在二进制文件中也占2个字节】
7.文本文件按行划分,二进制文件不按行划分。【UNIX/Linux二者无区别,Window/DOS用\r\n表示换行,写入自动扩展\r\n,读出自动转换\n;Mac用\r表示换行,写入\n时,自动转换为\r。读出\r时,自动转换\n】
## 二、文件指针
1.C程序中流的打开和关闭是通过文件指针实现的,文件指针的类型为FILE*.【符号变量】
2.filename是文件名:包含路径。如果不含路径,表示打开当前目录下的文件
C fp = fopen ("D:\newproject\test.txt", "r");
结果是无法打开,因为编译器会将\n和\t看待成转义字符
正确的打开方式是
C fp = fopen("D:\newproject\test.txt","r");
C FILE *fopen(const char *filename, const char *mode); FILE *fp; fp = fopen("test.txt", "r");
返回值为指向此文件的指针-如果打开失败(文件损失活不存在),返回值为NULL
文件打开后一定要检查是否打开成功
3.关闭文件用 int fclose(FILE *fp);
执行成功返回0;执行失败返回非零【磁盘空间不足,容易引起数据丢失】。
4.为什么必须关闭文件?
①容易引起数据丢失;②有可能影响其他文件打开;
5.filename是文件名;返回值为指向此文件的指针;mode是文件打开方式
- **文件打开方式(mode):**
- **r **只读 必须是存在的文件**
- **w 只写 不论该文件是否存在,都新建一个文件**
- **a 追加 向文本文件尾添加数据,该文件不许存在【更安全】**
- **r+ 读写 打开一个已存在的文件,用于读写**
- **w+ 读写 建立一个新文件,可读可写**
- **a+ 读写 向文件尾追加数据,也可读**
要是以二进制方式打开文件则需要在上文基础之上听啊加一个字母b
6.文本文件
字符读写:
①fgetc(),
C int fgetc(FILE *fp);//从fp读出一个字符,将位置指针指向下一个字符;读取成功,返回该字符(返回值为int,非char);读到文件尾,则返回EOF,EOF在stdio.h中定义为-1,读文件时自动添加到缓冲区
②fputc(),
C int fputc(int c, FILE *fp);//向fp输出字符c;若写入错误,则返回EOF,否则返回c ```
③fgets(), fputs()
格式化读写:fscanf(), fprintf()
二进制文件
数据块读写:fread(), fwrite()
7.字符串(行)读:
char *fgets(char *s, int n, FILE *fp);
从fp所指的文件中读取字符串,并在字符串末尾添加'\0',然后存入s中,最多读n - 1个字符。
fgets(buf, sizeof(buf), stdin);从标准输入流(键盘)中读数据
8.字符串(行)写:
int fputs(const char *s,FILE ***fp);
9.fprintf()的最普遍的应用就是向标准错误读写出错信息;输出调试信息,报告程序运行到了哪里,那里的状态是什么
三、文件其他操作
1.按数据块读写:
num = fread(buffer, size, count, fp);
从fp所指的文件中读取数据块存到buffer指向的内存。【buffer是存储数据块的内存首地址,如数组的地址;size是每个数据块大小(元素的大小、字节数);count 是要读入的数据块个数,如数组元素的个数;返回值num表示实际读入的数据块个数,应等于count,除非达到了文件末尾,或出现了错误】
2.按数据块读写:
num = fread (butter, size, count, fp);
num = fread (a, sizeof(char), n, fp);
//n个块,每个块占1字节
num = fread (a, sizeof(a[0]), sizeof(a) / sizeof(a[0]), fp);
//最优解
3.按数据块写:
num = fwrite (butter, size, count, fp);
将buffer指向的内存中的数据块写入fp所指的文件。
4.文件随机读写
采用文件定位函数,
设定文件位置指针的函数:
void remind (FILE *fp);
【知道当前位置】
long ftell(FILE *fp);
【不知道当前位置】
int fseek (FILE *fp, long offset, int fromwhere);
【改变文件位置指针,实现随机读写】将文件位置指针从fromwhere开始移动offset个字节,指示下一个要读取的数据的位置。
5.起始位置的取值:
SEEK_SET或0 —— 文件开始
SEEK_CUR或1 —— 当前位置
SEEK_END或2 —— 文件末尾
缓冲区可以加快读写速度
fflash用来清理缓冲区
6.文件系统的分类
缓冲文件系统
非缓冲文件系统