C语言程序设计
一、程序设计与C语言
程序,是用特殊编程语言写出来指导计算机如何解决问题的,即描述要求它如何去做的过程或方法,告诉计算机怎么一步一步去做(how to do)。
选择一种语言,主要是其所带的库和传统所决定的。
二、数据存储与运算
2.1 标识符const
标识符的定义规则:
- 不能以数字开头
- 不能是关键字
如_HJ是标识符,但9_s、long均不是
const int amount=100
const表示只读,因此常量amount不可修改不可赋值
2.2 输入输出
当scanf出错时,变量的值没有发生变化
2.3 浮点数
两个整数运算的结果只能是整数,2与2.0意义不一样,2.0表示浮点数(带小数点的数)
当整数与浮点数运算时,c会将整数转化为浮点数再计算
例题2.1:计算时间差
scanf("%d %d",&h1,&min1);
scanf("%d %d",&h2,&min2);
int t1=h1*60+min1;//将时间转化为整数
int t2=h2*60+min2;
int t=t2-t1;//t表示时间差有多少分钟
printf("时间差是%d小时%d分钟",t/60,t%60);
// t/60两个整数运算,结果四舍五入取整数——>小时,
// t%60,取模运算,计算余数——>分钟
例题2.2:两个整数计算平均数
double c=(a+b)/2.0;//2.0这样得到的平均数包含小数部分
三、判断
3.1 if-else语句
if(1){
……
}//满足1的表达式执行
else if(2){
……
}//不满足1但满足2执行
else if(3){
……
}//不满足1和2但满足3执行
else{
……
}//123都不满足,执行
3.2 switch-case语句
switch语句可以看作是基于计算的跳转,计算表达式后跳转到相匹配的case处,执行后面的代码,直到遇到break才跳出switch,或者switch结束。
switch(a){//a只能是整型的结果
case *://*可以是常数,也可以是常数计算的表达式
……
break;
default:……
}
四、循环
4.1 while 与do-while
do-while 与while一个是先执行再判断条件,一个是先判断条件再执行循环体。do-while进入循环的时候不做检查,先执行一轮循环体代码后再检查条件是否满足。
例题2.3:猜随机数
①生成100以内的随机数
#include<stdlib.h>//此库包含随机数函数
#include<time.h>//让每次运行的代码生成的随机数不一样srand(time(0));
int number=rand()%100+1;//rand()%100得到的是0~99的数
#include<stdio.h>
#include<stdlib.h>//此库包含随机数函数
#include<time.h>//让每次运行的代码生成的随机数不一样
int main(){
srand(time(0));
int number=rand()%100+1;//rand()%100得到的是0~99的数
int count=0;
int a=0;
printf("我已经想好一个1~100的整数了\n");
printf("请猜这个数是多少:");
do{
scanf("%d",&a);
count++;//猜的次数加一
if(a>number)
printf("大了哦,再猜:");
else if(a<number)
printf("小了哦,再猜:");
else
printf("恭喜你猜对了,是%d",a);
}while(a!=number);
return 0;
}
例题2.4:算平均数
用do-while循环,number!=-1判断了两次,while循环优于do-while
//sum求和(作被除数),count计数输入了多少个数(作除数),number保存每次输入的数
//用while循环
scanf("%d",&number);
while(number!=-1){
count++;
sum+=number;
scanf("%d",&number);
}
printf("平均数是%f",sum/(double)count);
/*用do-while循环
do{
scanf("%d",&number);
if(number!=-1){
count++;
sum+=number;
}
}while(number!=-1);*/
例题2.5:整数逆序
取出每一位的数字:
②<—依次分解整数
1 对一个整数%10得到其个位数
2 对一个整数/10丢掉他的个位数
3 对2的结果%10得到他的十位数
4 对2的结果/10丢掉他的个位数....
//从低位到高位依次求解
while(x>0){
digit=x%10;//每轮循环得到最后一位的数字用digit存储
//若要求100输出为001
// printf("%d",digit);//依次输出每一位即可实现逆序输出
//若要求100输出为1
ret=ret*10+digit;//相当于在已经逆序完的几位数后加一个最新需要逆序的数,比如说123,已经逆序完成32,最新需要逆序的是1,即32后添加0再加上1(32*10+1=321)
x/=10; //去掉最后一位
}
printf("%d",ret);
4.2 for
for(int i=1;i<=n;i++)//i 为局部变量
for(i=n;i>1;i--)//i从n一直循环到1
for(n=n;n>1;n--)//n从n一直循环到1
for(;n>1;n--)//省略n=n
任何一个for循环都可以用while循环表示出来
tip for loops
- 固定次数用for
- 必须执行一次用do-while
- 其他情况用while
4.3 break与continue
例题2.6:判断素数
③ 枚举法:x能被i整除
x%i==0,可以用来判别是不是素数,求最大公约数等
break跳出整个循环体
还可以设置一个布尔变量(初始设置为true),不是素数是将变量值设置为false。
for(int i=2;i<x;i++){
if(x%i==0){//即x能被除1和本身整除,x不是素数
printf("%d不是素数",x);
break;
}
continue:这一轮continue之后未执行的语句不再执行,转而执行下一轮循环语句
举个例子:音乐课上让一排的同学依次完整地唱一首歌,第一个同学唱完之后,第二个同学唱的时候忘词没唱完(continue),后面的同学需要接着唱,但第三个同学唱到一半的时候下课了,此时唱歌结束(break)。
4.4 嵌套循环的退出
4.4.1 接力break
break只能跳出当前一层的循环,若当嵌套循环最内层循环break结束时要求整个循环也结束,需要连续使用break,但后续外层循环不能简单地break,需要判断最内侧break是否执行。
例题2.7:凑硬币
用1角、2角、五角的硬币凑10元以下的钱,只要求有一种组合就可以。
int x;
int one,two,five;
bool exit=false;
scanf("%d",&x);
for(one=1;one<x*10;one++){
for(two=1;two<x*10/2;two++){
for(five=1;five<x*10/5;five++){
if(one*1+two*2+five*5==x*10){
printf("%d枚1角%d枚2角%d枚5角能凑出%d元\n",one,two,five,x);
exit=true;
break;
}
}//第一个break退出此循环,此时不能直接break,要判断第一个break是否执行
if(exit) break;
}
if(exit) break;
}
4.4.2 goto
在多层嵌套循环内层跳到最外层时非常适合goto,goto **会跳到**的位置执行后面的代码
for(;;){
for(;;){
for(;;){
if(){
goto out;
}
}
}
}
out://goto跳到此处执行后面的代码
…………
例题2.8 求前n项和
1/2+1/3+1/4+……1/n
for(int i=1;i<=n;i++){
sum+=1.0/i;
}
1/2-1/3+1/4-……1/n
int sign=1;
for(int i=1;i<=n;i++){
sum+=sign*1.0/i;
sign=-sgin;
}
例题2.9 整数分解
12345—>1 2 3 4 5
一次逆序再一次逆序输出,这种方式遇到200时,结果只有2
//将数165逆序得到数561
do{
int d=x%10;
set=set*10+d;
x/=10;
}while(x>0);
x=t;
//再从右往左输出最后一位,得到1 6 5
do{
printf("%d ",x%10);
x/=10;
}
改进:
④—>依次分解整数
1 while循环求mask(比如x=1234,mask=1000)
1 x/mask得到最左边一位,x%mask去掉最左边一位
2 mask/=10,重复2
#include<stdio.h>
int main(){
int x,d,mask=1,t;
scanf("%d",&x);
t=x;
//假设x为n位,得到mask=10^(n-1);
while(t/10>0){//t>9也行
mask*=10;
t/=10;
}
do{
int d=x/mask;
printf("%d",d);
if(mask>0)
printf(" ");
x%=mask;
mask/=10;
}while(mask>0);
return 0;
}
例题2.10 求最大公约数
枚举法:
1 i 从1到min{a,b}开始循环,每次循环结束+1
2 若a、b均能被i整除,公约数r=i,直至循环结束
for(int i=1;i<=a&&i<=b;i++){
if(a%i==0&&b%i==0){
r=i;
}
}//最大公约数为r
⑤辗转相除法:
1 如果b=0,计算结束,a为最大公约数
2 否则 计算余数c=a%b,让a=b,b=余数c
3 回到第一步
a b c
6 9 6
9 6 3
6 3 0
3 0
#include<stdio.h>
int main(){
int a,b,c;
scanf("%d %d",&a,&b);
while(b){
c=a%b;
a=b;
b=c;
}
printf("最大公约数是%d",a);
return 0;
}
五、类型
5.1 类型分类
整数:
- char,1字节,8比特,能表示个整数,一半负数,另一半正数+0,即-128~127
- short(2),-32768~32767,
- int(取决于CPU)
- long(4)
- long long(8)
unsigned(无符号)只是输出不同,内部计算一样的
浮点数:float(输入%f,输出%f、%e科学计数法)、double(输入%lf,输出%f、%e)、long double
inf:+∞,-inf:-∞;nan:不存在
tips:带小数点字面量是double而非float,可以用3.145f来表明是float 两个相等的浮点数比较可能会失败,可以用fab(f1-f2)<1e-12
字符类型:
char 还能表示字符,' '=32,'0'=48,'A'=65,'a'=97
逃逸字符:\b,\n,\t
逻辑:bool
指针
自定义类型
sizeof(int) :int 占多少字节(byte)
5.2 自动类型转换
对于printf任何小于int类型的会被转换成int,float会被转换成double
但scanf不会,要输入short需要%hd
5.3 强制类型转换
一般是由大转小,优先级高于四则运算
5.4 逻辑运算(优先级)
短路:表达式1&&表达式2,当左边表达式已经不成立,此时不会再执行右边的表达式,此时称右边的表达式被短路了。
条件运算符和逗号表达式
逗号是运算符,优先级最低,通常在for中使用
- for(i=0,j=0;i<j;i++,j++)……
六、函数
6.1 函数定义、调用、声明
for(int i=0;i<number;i++){
count[i]=0;
}
当调用函数给的值与参数类型不匹配时,编译器会自动转换类型
c 语言在调用函数时,实参永远只能传值给形参,而并不是变量的传递
6.2 变量的生存期、作用域
对于本地变量其生存期和作用域在块内,即大括号内,本地变量只存在于块内的变量空间,当块执行结束,变量也随之消失
七、数组
编译器和运行环境不会检查数组的下标是否越界,a[0]存在但无用
7.1 定义及初始化
//数组定义初始化
const int number=10; //方便修改和代码阅读
int count[number]={0};
//数组的集成初始化
int a[]={1,2,3,4,5,6};
还可以 a[10]={[1]=2,4,[5]=6}; 表示给a[1]=2,a[2]=4,a[5]=6,其余全是0
7.2 数组的大小
数组长度 length=sizeof(a)/sizeof(a[0]);
7.3 数组的赋值
数组变量本身不能被赋值,如int b[]=a;只能遍历,将每一个元素赋值给另一个数组
当数组作为函数参数时
- 不能在[]中给出数组的大小
- 不能再利用sizeof来计算数组的元素个数!
往往必须再用另一个参数来传入数组的大小
数组实例:素数
算法1:做加法,将素数一个一个写入数组
1.依次将每一个素数写入prime数组
2.在判断x是否为素数时,只需要看x能不能被x之前的素数整除,不能即为素数
/*判断x能不能被x前面的素数整除,
a[]存储了x前面的素数,length表示数组的素数的个数*/
int IsPrime(int x,int a[],int length){
int ret=1;
for(int i=0;i<length;i++){
if(x%a[i]==0){
ret=0;
break;
}
}
return ret;
}
算法2:做减法,将不是素数的一个一个删除
构造素数表
1.开辟prime[n],初始化为1,表示假设从0~n-1每个数都是素数
2.令x=2
3.如果x是素数,则将x~n内的所有x的倍数逻辑删除,即这些数都不是素数
(i=2,x*i<n;i++) prime[i*x]=0;4.令x++,如果x<n,重复执行3,否则结束
//遍历数组实现初始化之后
for(int x=2;x<n;x++){
if(isprime[x]){
for(int i=2;i*x<n;i++)
isprime[i*x]=0;
}
}
2.6.4 二维数组
int a[][5]={
{0,1,2,3,4},
{2,3,4,5,6},
};
列数必须给出,每行一个{},逗号分隔,最后一个逗号可以省略
八、指针
8.1 运算符&
获得变量的地址,printf("%p",&i)
8.2 指针变量
指针:就是保存地址的变量
int i; int *p=&i;//这里*是声明,*p即声明p是一个指针变量,p保存i的地址,p指向i的地址
而后续的*可以理解为运算符,*p对p做运算找到p所指位置的内容,因此*p代表了i的值,对*p修改,就是通过*p找到i的位置对i的值修改
函数调用时,通过指针能保存带回的结果,因此传入参数就是需要保存带回的结果的变量
8.3 指针应用
场景一:调用swap函数,交换a、b的值
两种实现方式:
(1)直接交换a,b的地址,如
swap(int &pa,int &pb){
交换pa和pb的值
}
swap(a,b);
(2)通过指针直接修改a,b的值
swap(int *pa,int *pb){
交换*pa和*pb的值
}
swap(&a,&b);
场景二a:函数的返回值只有一个,因此当函数返回多个值,某些值只能通过指针返回
场景二b:函数返回运算的状态,结果通过指针返回
8.4 野指针
野指针是指向位置随机的、不正确的指针,系统无法对其进行操作,野指针指向的位置是随机的, 危害也是随机的,不一定会产生错误。
一种情况是,创建指针时没有对指针进行初始化,导致指针指向一个随机的位置,此时该指针就是一个野指针。后续我们对该指针进行操作时,比如说赋值,是将值写入该指针指向的那个随机位置,如果那个位置不能写,程序就会发生崩溃。
另一种情况是,释放指针指向的内存之后,指针没有置空,此时指针会指向“垃圾”内存
避免野指针
- 在创建指针时必须进行初始化;
- 在释放指针指向的内存之后必须将指针置空;
8.5 数组与指针
函数参数表里的数组其实是个指针,void f(int a[])与void f(int *a)等价
数组变量是特殊的指针,因为数组变量本身表达地址(就是a[0]的地址),因此
- int a[10];int *p=a;//无需用&取地址
- 但数组的单元表达的是变量,需要用&取地址
- a==&a[0]
- []可以对数组做也可以对指针做: p[0]<==>a[0]
- *可以对指针做也可以对数组做:*a<==>a[0]
- 数组变量是const的指针,因此不能被赋值:int a[] <==>int * const a=…
#include<stdio.h>
int main(){
int a[10];
int *p=a;
printf("%d\n%d\n%d\n%d\n%d",a,&a[0],p[0],a[0],*a);
//运行结果a=&a[0],p[0]=a[0]=*a
}
8.6 指针与const
(1)指针是const p是const int *const p=&i;
一旦得到了某个变量的地址,不能再指向其他变量:
- 指针变量p是const,不能修改p的值,即指针p指向的位置是固定的
(2)所指是const *p是const const int *p=&i;
表示不能通过这个指针去修改那个变量(并不能使那个变量成为const):
- *p是const,无法通过*p=26去给i赋值为26
- 但i并不是const,因此仍能通过i=26给i赋值
- p也不是const,因此也可以修改p的值,让p指向其他变量
区分(1)、(2)通过判断const在*前面还是后面
转换:总是可以吧一个非const的值转换成const
保护变量:void fun(const int *x){}//无法在fun函数f里对传入的变量进行修改
保护数组值:int sum(const int a[]){}//无法在sum函数里对传入的数组进行修改
8.7 指针的运算
p-q:二者之间能存放多少个数,并不是二者的地址差
*P++:表示取出p所指位置的内容,再把p移到一下一位置,同理*++P,取出下一位置的内容
当不知道i是什么类型时,需要取出他的地址,可以用void *p=&i;
8.8 动态内存分配
int *p=malloc(number*sizeof(int));//动态申请一片空间(借)
free(p);//有借又还,free释放必须是malloc分配的初始地址
常见问题:
- 申请了没free
- free过了再free
- 地址变了,直接free
九、字符串
char str={'a','b','c'}是一个字符数组,但不是一个字符串,char str={'a','b','c','\0'}是字符串
- 字符串是以0结尾的一串字符
- 0和'\0'一样,但与'0'不同
- 0标志字符串的结束,但不是字符串的一部分,因此计算字符串长度时不包括这个0
- 字符串以数组的形式存在,以数组或指针(更多以指针)的形式访问
9.1 字符串常量
“Hello”会被编译器变成字符数组,这个数组的长度时6,包含0,因为字符串是数组,因此不能直接通过运算符计算
9.2 字符串变量
字符串指针与数组的区别
指针:char *str="Hello World";
- str是一个指针,初始化为指向一个字符串常量
- 这个常量所在的地方是只读,实际上str 是const char* str
- 不能对str所指的字符串修改
数组:char str[]="Hello World";
- Hello World所在的地方仍是只读,但str[]相当于把Hello World拷贝到数组里
- 此时,能对Hello World进行修改
构造字符串—>数组
处理字符串—>指针
char*并不是字符串,如果他所指的字符数组以\0结尾才是字符串
9.3 字符串输入输出
char string[8];
scanf(“%s”,string);
scanf读到空格、tab或者回车为止,但不安全,因为不知道要读入的内容长度,当超出string容量时程序可能崩溃
char word[8];
cahr word2[8];
printf(%s,word);
printf("##%s##",word2);
为什么输入12345678 12345678
输出是##12345678##?
scanf("%7s",string);//告诉scanf函数最多允许读入7个字符,当读入7个字符后停止读入,不再依据空格、tab和回车
常见错误:
- char *string;scanf("%s",string);//string未初始化,是一个野指针,可能指向不可修改的地址
- char buffer[]=" ";//数组长度只有1,即'\0',不能放任何一个字符,应修改为char buffer[100]=" ";
9.4 字符串数组
char a[][10]={"LiMing","ZhangSan","LiSi"};
char *a[ ]={"LiMing","ZhangSan","LiSi"};
a[i]=%s
:存储在a[i]
指向内存位置的字符串内容a[i]=%p
:a[i]
指针指向的内存地址&a[i]=%p
:a[i]
指针本身的地址*a[i]=%c
:a[i]
指向的第一个字符
字符串数组 a[][10]与*a[ ]的区别(图解):
9.5 getchar与putchar
- int putchar(int c);
- 向标准输出写一个字符
- 返回写了几个字符(1),EOF(-1)表示写失败
- int getchar(void);
- 从标准输入读入一个字符
- 返回类型时int而不是char是为了返回EOF(-1)
问:为什么使用getchar()时,我们能键入多个字符?而不是输入一个字符之后就停止。
答:程序和键盘之间有一个中间平台shell窗口,当我们从键盘上输入时,其实是在向shell输入,然后shell再把我们的输入交给程序。当我们输入数据按下回车之前,我们的输入会一直停留在shell里,直到按下回车。在shell里有一个很大的缓冲区,当按下回车后,会将输入(包括回车)填入缓冲区,程序可以开始读入数据,此时若程序并未结束运行(比如说还需要数据),还可以继续输入数据到缓冲区,直到程序运行结束,或(CTRL+Z:在缓冲区键入EOF,强制结束程序)或(CTRL+D:键入-1,通过shell终止程序)
总的来说,getchar和scanf都是从shell的缓冲区里读数据,而用户输入是让shell将数据写入缓冲区
9.6 字符串函数
#include<string.h>
9.6.1 Strlen(串长)
- size_t strlen(const char *s);
- 返回s的字符串长度(不包括结尾的0)
9.6.2 Strcmp(比较大小)
- int strcmp(const char*s1,const char *s2);
- 比较两个字符串,返回0:s1==s2 1:s1>s2 -1:s1<s2(有的编译环境会返回差值)
9.6.3 Strcpy(拷贝)
- char * strcpy(char *restrict dst, const char *restrict src);
- 把src的字符串拷贝到dst
- restrict表明src和dst不重叠(C99),返回dst
复制一个字符串:
char *dst=(char*)malloc(strlen(src)+1);//+1表示末尾的\0
stcpy(dst,src);
9.6.4 Strchr、Strrhr、Strstr、 Strcasestr(搜索)
- char * strchr(const char *s, int c);//从左往右找到c第一次出现的位置
- char * strrchr(const char *s, int c);//从右往左找到c第一次出现的位置
- char * strstr(const char *s1, const char *s2);//字符串中找字符串
- char * strcasestr(const char *s1, const char *s2);//忽略大小写
返回NULL表示没有找到
char s[] ="hello";
找到l第一次出现的位置,并把后续的字符串拷贝到t(输出llo)
tchar *p=strchr(s,'l');//p指向s从左往右第一次出现l的位置,p表示字符串'llo'
char *t=(char*)malloc(strlen(p)+1);
strcpy(t,p);
找到l第一次出现的位置,并把前续的字符串拷贝到t(输出he)
char c=*p;//用c暂存指针p指向的地址里的内容'l'
*p='\0';//将指针p指向的地址里的内容赋值为0,此时s[]所表示的字符串是"he"
char *t=(char*)malloc(strlen(s)+1);
strcpy(t,s);
*p=c;//把c里寄存的字符写回去
怎么找第二次出现的位置(输出lo)
p=strchr(p,'l');//p指向p所指向的字符串('llo')从左往右第一次出现l的位置,输出lo
十、枚举
枚举是一种用户定义的数据类型,用关键字enum来声明:
enum 枚举类型名字 {名字0,……,名字n};
大括号里的名字,它们是常量符号,类型为int,值依次从0到n。
enum COLOR {RED,YELLOW,GREEN=5,NumCOLORS};
- 声明枚举量的时候可以指定值,如GREEN=5
- 当不指定值时,可以用NumCOLORS表达前面一共有多少个枚举量
十一、结构类型
十二、程序结构
十三、文件
13.1 格式化输入输出
printf:%[flags][width][.prec][hlL]type
printf("%d%n",a,&num);用num去记录输出a的长度
scanf:%[flage]type
返回值>0输入成功
13.2 文件输入输出
FILE *fp=fopen("file","w");
if(fp){
fsacnf(fp,…);
fprintf(fp,…);
fclose(fp);
}else
printf(“读取失败”);