注:本文为跟学中国大学mooc翁恺老师C语言程序设计课程整理。
01 程序设计与C语言
1.1 第一个C程序
#include<stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}
printf("Hello World!\n");
(1)""号内的内容为要输出的字符串,printf会把其中的内容原封不动地输出。
(2)\n表示换行符,在插入\n处会执行换行命令。
输出结果:
1.2 暂停
在一些编译环境下,程序执行换成后会立即退出窗口,为使得窗口能够留下,可添加下列语句。
system("pause");
案例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Hello World!\n");
printf("你好世界!\n");
system("pause"); // 防止运行后自动退出,需头文件stdlib.h
return 0;
}
1.3 输出算数结果
#include<stdio.h>
int mian()
{
printf("12+34=%d",12+34);
return 0;
}
%d表示字符串后面的计算结果以整数形式输出在这个位置上。
结果:
1.4 四则运算
四则运算 | C符号 | 意义 |
+ | + | 加 |
- | - | 减 |
× | * | 乘 |
÷ | / | 除 |
% | 取余 | |
() | () | 括号 |
案例:
#include<stdio.h>
int main()
{
int price = 0;
printf("请输入金额(元):");
scanf("%d",&price); //使用scanf函数接收用户输入的数据
int change = 100-price;
printf("找您%d元。\n",change);
return 0;
}
输出结果:
02 计算
2.1 变量
2.1.1 变量定义
变量定义的一般形式就是:
<类型名称><变量名称>;
int price;
int amount;
int price,amount;
2.1.2 变量命名
(1)变量需要一个名字,变量的名字是一种“标识符”,意思是它是用来识别这个和那个的不同的名字。
(2)标识符有标识符的构造规则。基本的原则是:标识符只能由字母、数字和下划线组成,数字不可以出现在第一个位置上,C语⾔言的关键字(有的地方叫它们保留字)不可以用做标识符。
2.1.3 C语言的保留字(关键字)
auto | 声明自动变量 |
break | 跳出当前循环 |
case | 开关语句分支 |
char | 声明字符型变量或函数返回值类型 |
const | 声明只读变量 |
continue | 结束当前循环,开始下一轮循环 |
default | 开关语句中的“其它”分支 |
do | 循环语句的循环体 |
double | 声明双精度浮点型变量或函数返回值类型 |
else | 条件语句否定分支(与 if 连用) |
enum | 声明枚举类型 |
extern | 声明变量或函数是在其它文件或本文件的其他位置定义 |
float | 声明浮点型变量或函数返回值类型 |
for | 一种循环语句 |
goto | 无条件跳转语句 |
if | 条件语句 |
int | 声明整型变量或函数 |
long | 声明长整型变量或函数返回值类型 |
register | 声明寄存器变量 |
return | 子程序返回语句(可以带参数,也可不带参数) |
short | 声明短整型变量或函数 |
signed | 声明有符号类型变量或函数 |
sizeof | 计算数据类型或变量长度(即所占字节数) |
static | 声明静态变量 |
struct | 声明结构体类型 |
switch | 用于开关语句 |
typedef | 用以给数据类型取别名 |
unsigned | 声明无符号类型变量或函数 |
union | 声明共用体类型 |
void | 声明函数无返回值或无参数,声明无类型指针 |
volatile | 说明变量在程序执行中可被隐含地改变 |
while | 循环语句的循环条件 |
2.1.4 赋值
和数学不同,a=b在数学中表示关系,即a和b的值一样;而在程序设计中,a=b表示要求计算机做一个动作:将b的值赋给a。关系是静态的,而动作是动态的。在数学中,a=b和b=a是等价的,⽽而在程序设计中,两者的意思完全相反。
2.1.5 变量初始化
<类型名称><变量名称>=<初始值>;
int price = 0;
int amount = 100;
组合变量定义的时候,也可以在这个定义中单独给单个变量赋初值,如:
int price = 0;amount = 100;
变量定义以后若不进行初始化便直接使用,系统会随机赋一个值给变量。
2.1.6 表达式
“=”是赋值运算符,有运算符的式⼦子就叫做表达式。
price = 0;
change = 100-price;
2.1.7 变量类型
int price = 0;
这一行,定义了一个变量。变量的名字是price,类型是int,初始值是0。
C是一种有类型的语⾔言,所有的变量在使用之前必须定义或声明,所有的变量必须具有确定的数据类型。数据类型表示在变量中可以存放什么样的数据,变量中只能存放指定类型的数据,程序运行过程中也不能改变变量的类型。
C99允许在代码的任何位置定义变量,传统的C语言(ANSI C)只允许在代码开头定义变量。
C99:
int price = 0;
printf("请输入金额(元):");
scanf("%d",&price);
int change = 100-price;
printf("找您%d元。\n",change);
ANSI C:
int price = 0;
int change = 0;
printf("请输入金额(元):");
scanf("%d",&price);
change = 100-price;
printf("找您%d元。\n",change);
2.1.8 读整数
scanf("%d",&price);
要求scanf这个函数读入下一个整数,读到的结果赋值给变量price。
2.1.9 常量
int change = 100 - price;
100这个固定不变的数,叫做常数。直接写在程序⾥里,我们称作直接量(literal,字面量)。
更好的方式,是定义一个常量变量:
const int AMOUNT = 100;
#include<stdio.h>
int main()
{
const int AMOUNT = 100;
int price = 0;
printf("请输入金额(元):");
scanf("%d",&price); //使用scanf函数接收用户输入的数据
int change = AMOUNT-price;
printf("找您%d元。\n",change);
return 0;
}
2.1.10 const
const是一个修饰符,加在int的前面,用来给这个变量加上一个const(不变的)的属性。这个const的属性表示这个变量的值一旦初始化,就不能再修改了。
int change = AMOUNT - price;
2.1.11 scanf
如果你试图对常量做修改,把它放在赋值运算符的左边,就会被编译器发现,指出为一个错误。
使用scanf一次读入多个数值:
#include<stdio.h>
int main()
{
int a,b;
printf("请输入两个整数:");
scanf("%d %d",&a,&b); //用户输入两个整数,分别赋值给a和b,中间用空格隔开
printf("%d + %d = %d\n",a,b,a+b);
return 0;
}
printf("%d",...);
scanf("%d",...);
出现在scanf里面的东西都是要进行输入的东西, 若不进行输入会出错。
2.1.12 浮点数
带小数点的数值。浮点这个词的本意就是指小数点是浮动的,是计算机内部表达非整数(包含分数和无理数)的一种方式。另一种方式叫做定点数,不过在C语⾔言中你不会遇到定点数。人们借用浮点数这个词来表达所有的带小数点的数。
2.1.13 double
inch是定义为int类型的变量,如果把int换成double,我们就把它改为double类型的浮点数变量了。
double的意思是“双”,它本来是“双精度浮点数”的第一个单词,⼈人们⽤用来表示浮点数类型。除了double,还有float(意思就是浮点!)表示单精度浮点数。
2.1.14 整数和浮点数的输入输出
int类型:
printf("%d",...);
scanf("%d",...);
double类型:
printf("%f",...);
scanf("%lf",...);
2.2 表达式
一个表达式是一系列运算符和算子的组合,用来计算一个值
amount = x*(1+0.033)*(1+0.033)*(1+0.033);
total = 57;
count = count + 1;
value = (min/2)*lastValue;
2.2.1 运算符
运算符(operator)是指进行运算的动作,比如加法运算符“+”,减法运算符“-”。
算子(operand)是指参与运算的值,这个值可能是常数,也可能是变量,还可能是一个方法的返回值。
2.2.2 四则运算
四则运算 | C符号 | 意义 |
+ | + | 加 |
- | - | 减 |
× | * | 乘 |
÷ | / | 除 |
% | 取余 | |
() | () | 括号 |
2.2.3 案例:计算时间差
#include<stdio.h>
int main()
{
int hour1,minute1;
int hour2,minute2;
printf("请输入第1个时间:");
scanf("%d %d",&hour1,&minute1);
printf("请输入第2个时间:");
scanf("%d %d",&hour2,&minute2);
int t1 = hour1 * 60 + minute1; //将时间转换为min
int t2 = hour2 * 60 + minute2;
int t = t2 - t1;
printf("时间差是%d时%d分。",t/60,t%60);
return 0;
}
2.2.4 运算符优先级
优先级 | 运算符 | 运算 | 结合关系 | 举例 |
1 | + | 单目不变 | 自右向左 | a*+b |
1 | - | 单目取负 | 自右向左 | a*-b |
2 | * | 乘 | 自左向右 | a*b |
2 | / | 除 | 自左向右 | a/b |
2 | % | 取余 | 自左向右 | a%b |
3 | + | 加 | 自左向右 | a+b |
3 | - | 减 | 自左向右 | a-b |
4 | = | 赋值 | 自右向左 | a=b |
2.2.5 交换变量值
#include<stdio.h>
int main()
{
int a = 5, b=6;
printf("交换前a的值是%d,交换前b的值是%d\n",a,b);
int temp;
temp = a;
a = b;
b = temp;
printf("交换后a的值是%d,交换后b的值是%d",a,b);
return 0;
}
2.2.6 复合赋值
5个算术运算符,+ - * / %,可以和赋值运算符“=”结合起来,形成复合赋值运算符:“+=”、“-=”、“*=”、“/=”和“%=”
total += 5;
total += (sum+100)/2;
total *= sum + 12;
total /= 12 +6;
等效于:
total = total + 5;
total = total + (sum+100)/2;
total = total*(sum+12);
total = total/(12=6);
注意两个运算符中间不要有空格。
2.2.7 递增递减运算符
“++”和“--”是两个很特殊的运算符,它们是单目运算符,这个算子还必须是变量。这两个运算符分别叫做递增和递减运算符,他们的作⽤用就是给这个变量+1或者-1。
count++;
等效于:
count += 1;
等效于:
count = count + 1;
2.2.8 前缀后缀
++和--可以放在变量的前⾯面,叫做前缀形式,也可以放在变量的后⾯面,叫做后缀形式。
a++的值是a加1以前的值,而++a的值是加了1以后的值,无论哪个,a自己的值都加1了。
表达式 | 运算 | 表达式的值 |
count++ | 给count+1 | count原来的值 |
++count | 给count+1 | count+1以后的值 |
count-- | 给count-1 | count原来的值 |
--count | 给count-1 | count-1后的值 |
03 判断
案例:计算时间差
#include<stdio.h>
int main()
{
int hour1,minute1;
int hour2,minute2;
printf("请输入第1个时间:");
scanf("%d %d",&hour1,&minute1);
printf("请输入第2个时间:");
scanf("%d %d",&hour2,&minute2);
int ih = hour2 - hour1;
int im = minute2 - minute1;
if(im<0)
{
im = 60 + im;
ih --;
}
printf("两个时间差是%d小时%d分钟",ih,im);
}
3.1 判断
3.1.1 if(如果)
if(条件成立)
{
...
}
3.1.2 条件
计算两个值之间的关系,所以叫做关系运算。
运算符 | 意义 |
== | 相等 |
!= | 不相等 |
> | 大于 |
>= | 大于或等于 |
< | 小于 |
<= | 小于或等于 |
3.1.3 关系运算符的结果
当两个值的关系符合关系运算符的预期时,关系运算的结果为整数1,否则为整数0;
printf("%d\n",5==3);
printf("%d\n",5>3);
printf("%d\n",5<=3);
输出:
0
1
0
3.1.4 优先级
(1)所有的关系运算符的优先级比算术运算符的低,但是比赋值运算的高。
(2)判断是否相等的==和!=的优先级比其他的低,而连续的关系运算是从左到右进行的。
#include<stdio.h>
int main()
{
printf("%d\n",7>=3+4);
return 0;
}
输出:
1
3.1.5 注释
“//”:单行注释
“/**/”:多行注释
3.1.6 if else语句
案例:
#include<stdio.h>
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:
#include<stdio.h>
int main()
{
int a,b;
printf("请输入两个整数:");
scanf("%d %d",&a,&b);
int max = 0;
if(a>b)
{
max = a;
}
if(a<b)
{
max = b;
}
printf("两个数中的较大值是%d。",max);
return 0;
}
方案2:
#include<stdio.h>
int main()
{
int a,b;
printf("请输入两个整数:");
scanf("%d %d",&a,&b);
int max = 0;
if(a>b)
{
max = a;
}
else
{
max = b;
}
printf("两个数中的较大值是%d。",max);
return 0;
}
方案3:
#include<stdio.h>
int main()
{
int a,b;
printf("请输入两个整数:");
scanf("%d %d",&a,&b);
int max = a;
if(a<b)
{
max = b;
}
printf("两个数中的较大值是%d。",max);
return 0;
}
在if和else后面,要么跟随的是{多条/单条语句},要么只能是单条语句。
3.2 分支
3.2.1 嵌套的if
案例:
#include<stdio.h>
int main()
{
int a,b,c;
printf("请输入3个整数:");
scanf("%d %d %d",&a,&b,&c);
int max = 0;
if(a>b)
{
if(a>c)
{
max = a;
}
else
max = c;
}
else
{
if(b>c)
{
max = b;
}
else
max = c;
}
printf("三个数中较大的数是%d。",max);
}
3.2.2 级联的if-else if
if(exp1)
st1;
else if(exp2)
st2;
else:
st3;
3.2.3 switch-case
语法:
switch(控制表达式)
{
case 常量:
语句
...
case 常量:
语句
...
default:
语句
...
}
控制表达式只能是整数型的结果。
常量可以是常数,也可以是常数计算的表达式。
根据表达式的结果,寻找匹配的case,并执行case后面的语句,一直到break为止。
如果所有的case都不匹配,那么就执行default后面的语句;
如果没有default,那么就什么都不做。
案例:
#include<stdio.h>
int main()
{
int type;
printf("请输入type的值:");
scanf("%d",&type);
if(type==1)
{
printf("您好!");
}
else if(type==2)
{
printf("早上好!");
}
else if(type==3)
{
printf("晚上好!");
}
else if(type==4)
{
printf("再见!");
}
else
{
printf("这是什么?");
}
return 0;
}
#include<stdio.h>
int main()
{
int type;
printf("请输入type的值:");
scanf("%d",&type);
switch(type)
{
case 1:
printf("您好!");
break;
case 2:
printf("早上好!");
break;
case 3:
printf("晚上好!");
break;
case 4:
printf("再见!");
break;
default:
printf("这是什么啊?");
}
return 0;
}
3.2.4 break
switch语句可以看作是一种基于计算的跳转,计算控制表达式的值后,程序会跳转到相匹配的case(分支标号)处。分支标号只是说明switch内部位置的路标,在执行完分支中的最后一条语句后,如果后面没有break,就会顺序执行到下面的case里去,直到遇到一个break,或者switch结束为止。
#include<stdio.h>
int main()
{
int type;
printf("请输入type的值:");
scanf("%d",&type);
switch(type)
{
case 1:
printf("您好!");
break;
case 2:
printf("早上好!\n");
case 3:
printf("晚上好!");
break;
case 4:
printf("再见!");
break;
default:
printf("这是什么啊?");
}
return 0;
}
案例1:成绩转换
编写一个程序将一个百分制成绩转换成五分成绩。转换规则:
大于等于90分为A;
小于90且大于等于80为B;
小于80且大于等于70为C;
小于70且大于等于60为D;
小于60为E。
#include<stdio.h>
int main()
{
int grade;
printf("请输入一个成绩:");
scanf("%d",&grade);
grade /= 10;
switch(grade)
{
case 10:
case 9:
printf("A\n");
break;
case 8:
printf("B\n");
break;
case 7:
printf("C\n");
break;
case 6:
printf("D\n");
break;
default:
printf("F\n");
break;
}
return 0;
}
案例2:月份显示
#include<stdio.h>
int main()
{
int month;
printf("请输入月份:");
scanf("%d",&month);
switch(month)
{
case 1:printf("January\n");break;
case 2:printf("February\n");break;
case 3:printf("March\n");break;
case 4:printf("April\n");break;
case 5:printf("May\n");break;
case 6:printf("June\n");break;
case 7:printf("July\n");break;
case 8:printf("August\n");break;
case 9:printf("September\n");break;
case 10:printf("October\n");break;
case 11:printf("November\n");break;
case 12:printf("December\n");break;
}
return 0;
}
04 循环
4.1 循环
4.1.1 while循环
判断一个正整数的位数。
#include<stdio.h>
int main()
{
int x;
int n = 0;
printf("请输入一个整数:");
scanf("%d",&x);
printf("%d是",x);
while(x>0)
{
n++; //只要x大于0,说明x至少剩1为,n+1
x /= 10;
}
printf("%d位数",n);
return 0;
}
以上代码无法对x=0的时候进行判断,进行如下处理:
方案1:
#include<stdio.h>
int main()
{
int x;
int n = 0;
printf("请输入一个正整数:");
scanf("%d",&x);
printf("%d是",x);
n++;
x /= 10;
while(x>0)
{
n++; //只要x大于0,说明x至少剩1为,n+1
x /= 10;
}
printf("%d位数",n);
return 0;
}
方案2:直接使用if判断x=0的时候让n+1。
4.1.2 do while循环
在进入循环的时候不做检查,而是在执行完一轮循环体的代码之后,再来检查循环的条件是否满足,如果满足则继续下一轮循环,不满足则结束循环。
do
{
<循环语句>
}while(<循环条件>);
#include<stdio.h>
int main()
{
int x;
int n = 0;
printf("请输入一个正整数:");
scanf("%d",&x);
printf("%d是",x);
do
{
n++; //只要x大于0,说明x至少剩1为,n+1
x /= 10;
}while(x>0);
printf("%d位数",n);
return 0;
}
4.2 循环应用
4.2.1 循环计算
案例:
/*计算log以2为底x的值*/
#include<stdio.h>
int main()
{
int x;
int ret = 0;
printf("请输入x的值:");
scanf("%d",&x);
int t = x; //使用t存放x的初始值
while(x>1)
{
x /= 2;
ret ++;
}
printf("log2 of %d is %d.",t,ret);
return 0;
}
4.2.2 猜数游戏
1.计算机随机想一个数,记在变量number里;
2. 一个负责计次数的变量count初始化为0;
3. 让用户输入一个数字a;
4. count递增(加一);
5. 判断a和number的大小关系,如果a大,就输出“大”;如果a小就输出“小”;
6. 如果a和number是不相等的(无论大还是小),程序转回到第3步;
7. 否则,程序输出“猜中”和次数,然后结束。
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main()
{
srand(time(0));
int number = rand()%100 +1; //将随机生成的数求100的余数再加1,保证number在1-100之间
int count = 0;
int a = 0;
printf("我已经想好了一个1到100之间的数。\n");
printf("number的值是%d\n",number);
do
{
printf("请猜猜这个1到100之间数:");
scanf("%d",&a);
count++;
if(a>number)
{
printf("你猜的数大了。\n");
}
else if(a <number)
{
printf("你猜的数小了。\n");
}
}while(a!=number);
printf("太好了,你用了%d次就猜到了答案。\n",count);
return 0;
}
4.2.3 随机数
每次召唤rand()就得到一个随机的整数。使用时需导入相应头文件。
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main()
{
srand(time(0));
int a = rand();
printf("%d\n",a);
return 0;
}
4.2.4 算平均数
让用户输入一系列的正整数,最后输入-1表示输入结束,然后程序计算出这些数字的平均数,输出输入的数字的个数和平均数。
#include<stdio.h>
int main()
{
int number;
int sum = 0;
int count = 0;
printf("请输入要进行求和的值:");
scanf("%d",&number);
while(number != -1)
{
sum += number;
count ++;
printf("请输入要进行求和的值:");
scanf("%d",&number);
}
printf("%f\n",1.0*sum/count);
return 0;
}
4.2.5 整数的分解与求逆
一个整数是由1至多位数字组成的,如何分解出整数的各个位上的数字,然后加以计算:
(1)对一个整数做%10的操作,就得到它的个位数;
(2)对一个整数做/10的操作,就去掉了它的个位数;
(3)然后再对(2)的结果做%10,就得到原来数的十位数了;
(4)依此类推。
#include<stdio.h>
int main()
{
int x;
printf("请输入x的值:");
scanf("%d",&x);
int digit;
int ret = 0;
while(x>0)
{
digit = x%10; //获得x的个位
printf("%d",digit);
ret = ret*10+digit; // 求逆,还可以处理结尾为0的数
printf("x=%d,digit=%d,ret=%d\n",x,digit,ret);
x /= 10; //取出x的个位
}
printf("%d",ret);
return 0;
}
05 循环控制
5.1 for循环
for ( 初始动作; 条件; 每轮的动作 )
{
...
}
for中的每一个表达式都是可以省略的
for (; 条件; ) == while ( 条件 )
5.1.1 阶乘
n! = 1×2×3×4×…×n
• 写一个程序,让用户输入n,然后计算输出n!
• 变量:
• 显然读用户的输入需要一个int的n,然后计算的结果需要⽤用一个变量保存,可以是int的factor,在计算中需要有一个变量不断地从1递增到n,那可以是int的i。、
#include<stdio.h>
int main()
{
int n;
printf("请输入一个整数:");
scanf("%d",&n);
int fact = 1;
int i = 1;
for(i=1;i<=n;i++)
{
fact *= i;
}
printf("%d!=%d\n",n,fact);
return 0;
}
for循环像一个计数循环:设定一个计数器,初始化它,然后在计数器到达某值之前,重复执行循环体,而每执行一轮循环,计数器值以一定步进行调整,比如加1或者减1。
for(int i=1;i<=n;i++)
以上语法规则再某些C语言标准下不可用(C99和C11下可用)。
5.1.2 循环次数
for (i=0; i<n; i++)
则循环的次数是n,而循环结束以后,i的值是n。循环的控制变量i,是选择从0开始还是从1开始,是判断i<n还是判断i<=n,对循环的次数,循环结束后变量的值都有影响。
5.2 循环控制
5.2.1 素数
只能被1和自己整除的数,不包括1。
#include<stdio.h>
int main()
{
int x;
printf("请输入一个整数:");
scanf("%d",&x);
int i;
int isPrime = 1; //x是素数
for(i=2;i<x;i++)
{
if(x%i == 0)
{
isPrime = 0;
break; //只要识别到x有一个非1和自身因素,跳出循环
}
}
if(isPrime == 1)
{
printf("%d是素数\n",x);
}
else
{
printf("%d不是素数\n",x);
}
return 0;
}
5.2.2 break VS continue
break | 跳出循环(只能跳出它所在的那层循环) |
continue | 跳过循环这一轮剩下的语句进入下一轮 |
5.2.3 嵌套的循环
循环里面还是循环。
5.2.4 100以内的素数
100以内的素数。输出100以内的素数。
#include<stdio.h>
int main()
{
int x;
for(x=2;x<=100;x++)
{
int i;
int isPrime = 1; //x是素数
for(i=2;i<x;i++)
{
if(x%i == 0)
{
isPrime = 0;
break;
}
}
if(isPrime == 1)
{
printf("%d是素数\n",x);
}
}
return 0;
}
5.2.5 前50个素数
#include<stdio.h>
int main()
{
int x = 2;
int cnt = 0;
while(cnt<50)
{
int i;
int isPrime = 1;
for(i=2;i<x;i++)
{
if(x%i==0)
{
isPrime = 0;
break;
}
}
if(isPrime == 1)
{
cnt++;
printf("%d\t",x);
if(cnt%5 == 0) //每显示5个素数换一行
{
printf("\n");
}
}
x++;
}
return 0;
}
5.2.6 凑硬币
用1角、2角和5角的硬币凑出10元以下的金额。
#include<stdio.h>
int main()
{
int x;
int one,two,five;
printf("请输入您需要的金额:");
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+two*2+five*5==x*10)
{
printf("可以用%d个1角加%d个2角加%d个5角得到%d元\n",one,two,five,x);
}
}
}
}
return 0;
}
5.2.7 接力break
上节案例若只需要找到第一种方案即可,进行如下修改。
#include<stdio.h>
int main()
{
int x;
int exit = 0;
int one,two,five;
printf("请输入您需要的金额:");
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+two*2+five*5==x*10)
{
printf("可以用%d个1角加%d个2角加%d个5角得到%d元\n",one,two,five,x);
exit = 1; //找到1个结果时候跳出循环
break;
}
}
if(exit == 1) break; //找到1个结果时候跳出循环
}
if(exit == 1) break; //找到1个结果时候跳出循环
}
return 0;
}
5.2.8 goto
使用goto直接跳出所有循环。
#include<stdio.h>
int main()
{
int x;
int exit = 0;
int one,two,five;
printf("请输入您需要的金额:");
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+two*2+five*5==x*10)
{
printf("可以用%d个1角加%d个2角加%d个5角得到%d元\n",one,two,five,x);
exit = 1; //找到1个结果时候跳出循环
goto OUT;
}
}
}
}
OUT:
return 0;
}
5.3 循环应用
5.3.1 前n项求和
f(n)=1+1/2+1/3+1/4+...+1/n
#include<stdio.h>
int main()
{
int n;
int i;
double sum = 0.0;
printf("请输入1个整数:");
scanf("%d",&n);
for(i=1;i<=n;i++)
{
sum += 1.0/i;
}
printf("f(%d)=%f\n",n,sum);
return 0;
}
f(n)=1-1/2+1/3-1/4+...+1/n
#include<stdio.h>
int main()
{
int n;
int i;
double sum = 0.0;
int sign = 1;
printf("请输入1个整数:");
scanf("%d",&n);
for(i=1;i<=n;i++)
{
sum += sign*1.0/i;
sign = -sign;
}
printf("f(%d)=%f\n",n,sum);
return 0;
}
#include<stdio.h>
int main()
{
int n;
int i;
double sum = 0.0;
double sign = -1.0;
printf("请输入1个整数:");
scanf("%d",&n);
for(i=1;i<=n;i++)
{
sum += sign/i;
sign = -sign;
}
printf("f(%d)=%f\n",n,sum);
return 0;
}
5.3.2 逆序分解整数
#include<stdio.h>
int main()
{
int x;
printf("请输入一个整数:");
scanf("%d",&x);
do
{
int d = x%10;
printf("%d",d);
//控制最后一位不输出空格
if(x>9)
{
printf(" ");
}
x /= 10;
}while(x>0);
return 0;
}
5.3.3 正序分解整数
方案1:先逆序再逆序
#include<stdio.h>
int main()
{
int x;
printf("请输入一个整数:");
scanf("%d",&x);
int t = 0;
do
{
int d = x%10;
t = t*10+d;
x /= 10;
}while(x>0);
x = t;
do
{
int d = x%10;
printf("%d",d);
//控制最后一位不输出空格
if(x>9)
{
printf(" ");
}
x /= 10;
}while(x>0);
return 0;
}
以上代码无法处理末尾为0的数字。
方案2:
#include<stdio.h>
int main()
{
int x;
printf("请输入一个整数:");
scanf("%d",&x);
int mask = 1;
int t = x;
while(t>9)
{
t /= 10;
mask *= 10;
}
printf("x = %d,mask = %d\n",x,mask);
do
{
int d = x/mask;
printf("%d",d);
if(mask>9)
{
printf(" ");
}
x %= mask;
mask /= 10;
}while(mask>0);
printf("\n");
return 0;
}
5.3.4 最大公约数
5.3.4.1 枚举
#include<stdio.h>
int main()
{
int a,b;
int min;
printf("请输入两个正整数:");
scanf("%d %d",&a,&b);
if(a<b)
{
min = a;
}
else min = b;
int ret = 0;
int i;
for(i = 1;i<min;i++)
{
if(a%i == 0)
{
if(b%i == 0)
{
ret = i;
}
}
}
printf("%d和%d的最大公约数是%d。\n",a,b,ret);
return 0;
}
5.3.4.2 辗转相除法
1. 如果b等于0,计算结束,a就是最大公约数;
2. 否则,计算a除以b的余数,让a等于b,而b等于那个余数;
3. 回到第一步。
#include<stdio.h>
int main()
{
int a,b;
int m,n;
int t;
printf("请输入两个整数:");
scanf("%d %d",&a,&b);
m = a,n = b;
while(b!=0)
{
t = a%b;
a = b;
b = t;
printf("a=%d,b=%d,余数=%d\n",a,b,t);
}
printf("%d和%d的最大公约数是%d\n",m,n,a);
return 0;
}
06 数据类型
6.1 编程练习
6.1.1 给定条件的整数集
#include<stdio.h>
int main()
{
int n;
printf("请输入一个不超过6的正整数:");
scanf("%d",&n);
int i,j,k;
int need;
int cnt = 0;
for(i = n;i <= n+3;i++)
{
for(j = n;j <= n+3;j++)
{
for(k = n;k <= n+3;k++)
{
if(i != j && i!=k && j != k)
{
cnt ++;
need = i*100 + j*10 + k;
printf("%d",need);
if(cnt == 6 )
{
printf("\n");
cnt = 0;
}
else
{
printf(" ");
}
}
}
}
}
return 0;
}
6.1.2 水仙花数
#include<stdio.h>
int main()
{
int n;
printf("您需要几位数的水仙花数:");
scanf("%d",&n);
int first = 1;
int i = 1;
while(i<n)
{
first *= 10;
i++;
}
// printf("first=%d\n",first);
i = first;
while(i<first*10)
{
int t = i;
int sum = 0;
do
{
int d = t%10;
t /= 10;
int p = d;
int j = 1;
while(j<n)
{
p *= d;
j++;
}
sum += p;
}while(t>0);
if(sum == i)
{
printf("%d\n",i);
}
i++;
}
return 0;
}
6.1.3 水仙花数
#include<stdio.h>
int main()
{
int n = 9;
int i,j;
i = 1;
while(i<=n)
{
j=1;
while(j<=i)
{
printf("%d*%d=%d",j,i,j*i);
if(i*j<10)
{
printf(" ");
}
else
{
printf(" ");
}
j++;
}
printf("\n");
i++;
}
return 0;
}
6.1.4 统计素数并求和
#include<stdio.h>
int main()
{
int m,n; //要进行素数统计的起始和终止位置
int i;
int cnt = 0; //计数素数的个数
int sum = 0; //素数和
printf("请输入要进行素数统计的起始和终止位置:");
scanf("%d %d",&m,&n);
if(m == 1)
m = 2;
for(i = m;i<=n;i++)
{
int isPrime = 1;
int k;
for(k=2;k<i-1;k++)
{
if(i%k == 0)
{
isPrime = 0;
break;
}
}
//判断i是否是素数
if(isPrime)
{
cnt++;
sum += i;
}
}
printf("%d %d\n",cnt,sum);
return 0;
}
6.1.5 猜数字游戏
#include<stdio.h>
int main()
{
int number,n; //输入需要猜的数以及最多猜测次数
int inp;
int finished = 0;
int cnt = 0;
printf("请输入需要猜的数以及最多猜测次数,用空格隔开:");
scanf("%d %d",&number,&n);
do
{
printf("请输入您所猜测的数:");
scanf("%d",&inp);
cnt++;
if(inp<0)
{
printf("Game Over\n");
finished = 1;
}
else if(inp > number)
{
printf("Too big\n");
}
else if(inp < number)
{
printf("Tool small\n");
}
else
{
if(cnt == 1)
{
printf("Binggo!\n");
}
else if(cnt <= 3)
{
printf("Lucky You!\n");
}
else
{
printf("Good Guess!\n");
}
finished = 1;
}
}while(!finished);
return 0;
}
6.1.6 求序列前N项和
#include<stdio.h>
int main()
{
int n;
int dividend,divisor;
double sum = 0.0;
int i;
double t;
printf("请输入您需要求和的最高项:");
scanf("%d",&n);
// n=20000;
dividend = 2;
divisor = 1;
for(i = 1;i<=n;i++)
{
sum += dividend/divisor;
t = dividend;
dividend = dividend + divisor;
divisor = t;
}
printf("%d %d\n",dividend,divisor);
printf("%.2f\n",sum);
return 0;
}
6.1.7 其他编程练习
后续补充。
6.2 数据类型
6.2.1 C语言的类型
整数 | char、short、int、long、long long |
浮点数 | float、double、long double |
逻辑 | bool |
指针 | |
自定义类型 |
6.2.2 类型有何不同
(1)类型名称:int、long、double
(2)输入输出时的格式化:%d、%ld、%lf
(3)所表达的数的范围:char<short<int<float<double
(4)内存中所占据的大小:1个字节到16个字节
(5)内存中的表达形式:二进制数(补码)、编码
整型的变量在内存中表现形式是二进制码、自然二进制码或者二进制的补码,可以直接在加法器里进行运算。浮点类型变量无论是float、double还是long double是一种编码形式,不能直接拿来做运算。
6.2.3 sizeof
是一个运算符,给出某个类型或变量在内存中所占据的字节数。
是静态运算符,它的结果在编译时刻就决定了。
不要在sizeof的括号里做运算,这些运算不会做的。
sizeof(int);
sizeof(i);
6.2.4 整型
char | 1字节(8比特) |
short | 2字节 |
int | 取决于编译器(CPU),通常的意义是“1个字” |
long | 取决于编译器(CPU),通常的意义是“1个字” |
long long | 8字节 |
#include<stdio.h>
int main()
{
printf("sizeof(char)=%d\n",sizeof(char));
printf("sizeof(short)=%d\n",sizeof(short));
printf("sizeof(int)=%d\n",sizeof(int));
printf("sizeof(long)=%d\n",sizeof(long));
printf("sizeof(long long)=%d\n",sizeof(long long));
return 0;
}
6.2.4.1 二进制负数
1个字节可以表达的数:
00000000 — 11111111 (0-255)
三种方案:
1. 仿照十进制,有一个特殊的标志表示负数
2. 取中间的数为0,如1000000表⽰示0,比它小的是负数,比它大的是正数。
(以上两种方式都比较难实现)
3. 补码
6.2.4.2 补码
考虑-1,我们希望-1 + 1 —> 0。如何能做到?
0 —> 00000000
1 —> 00000001
11111111 + 00000001 —> 100000000
因为0 - 1 —> -1,所以,-1 =
(1)00000000 - 00000001 —> 11111111
11111111被当作纯⼆二进制看待时,是255,被当作补码看待时是-1。
同理,对于-a,其补码就是0-a,实际是2的n次方 - a,n是这种类型的位数。
补码的意义就是拿补码和原码相加可以溢出一个“零”。
6.2.4.3 整数的范围
对于一个字节(8位),可以表达的是:
00000000 - 11111111
其中
00000000 —> 0
11111111 ~ 10000000 —> -1 ~ -128
00000001 ~ 01111111 —> 1 ~ 127
char | 1字节:-128~127 |
short | 2字节:-32768~32767 |
int | 取决于编译器(CPU),通常的意义是“1个字” |
long | 4字节:-2^(32-1)~2^(32-1)-1 |
long long | 8字节 |
6.2.4.4 unsigned
在整形类型前加上unsigned使得它们成为无符号的整数内部的二进制表达没变,变的是如何看待它们。如何输出 11111111:
对于char,是-1;•对于unsigned char,是255。
如果一个字⾯面量常数想要表达自己是unsigned,可以在后面加u或U,如255U
• 用l或L表示long(long)
• *unsigned的初衷并非扩展数能表达的范围,而是为了做纯二进制运算,主要是为了移位。
6.2.4.5 整数越界
整数是以纯二进制方式进行计算的,所以:
• 11111111 + 1 —> 100000000 —> 0
• 01111111 + 1 —> 10000000 —> -128
• 10000000 - 1 —> 01111111 —> 127
6.2.4.6 整数的输入输出
只有两种形式:int或long long
• %d:int
• %u:unsigned
• %ld:long long
• %lu:unsigned long long
6.2.4.7 8进制和16进制
•一个以0开始的数字面量是8进制。
• 一个以0x开始的数字面量是16进制。
• %o用于8进制,%x用于16进制。
• 8进制和16进制只是如何把数字表达为字符串,与内部如何表达数字无关。
• 16进制很适合表达二进制数据,因为4位二进制正好是一个16进制位。
• 8进制的一位数字正好表达3位二进制。
• 因为早期计算机的字长是12的倍数,而非8。
6.2.4.8 选择整数类型
• 为什么整数要有那么多种?
• 为了准确表达内存,做底层程序的需要。
• 没有特殊需要,就选择int。
• 现在的CPU的字长普遍是32位或64位,一次内存读写就是一个int,一次计算也是一个int,选择更短的类型不会更快,甚至可能更慢。
• *现代的编译器一般会设计内存对齐,所以更短的类型实际在内存中有可能也占据一个int的大小(虽然sizeof告诉你更小)。
• unsigned与否只是输出的不同,内部计算是一样的。
6.2.5 浮点类型
类型 | 字长 | 范围 | 有效数字 |
float | 32 | ±(1.20×10^(-38)~3.40×10^38)【在靠近0的一小部分区域无法表达】,0,±inf【正负无穷大】,nan【不是一个有效数字】 | 7 |
double | 64 | ±(2.20×10^(-308)~1.79×10^308),0,±inf,nan | 15 |
6.2.5.1 浮点的输入输出
类型 | scanf | printf |
float | %f | %f,%e或%E(科学计数法) |
double | %lf | %f,%e或%E |
6.2.5.2 输出精度
• 在%和f之间加上.n可以指定输出⼩小数点后几位,这样的输出是做4舍5入的:
printf("%.3f\n", -0.0049);
printf("%.30f\n", -0.0049);
printf("%.3f\n", -0.00049);
#include<stdio.h>
int main()
{
printf("%.3f\n",-0.0049);
printf("%.30f\n",-0.0049);
printf("%.3f\n",-0.00049);
}
6.2.5.3 超范围的浮点数
无穷大无法用整数来表达。
• printf输出inf表示超过范围的浮点数:±∞。
• printf输出nan表示不存在的浮点数。
#include<stdio.h>
int main()
{
printf("%f\n",12.0/0.0);
printf("%f\n",-12.0/0.0);
printf("%f\n",0.0/0.0);
}
6.2.5.4 浮点运算的精度
• 带小数点的字⾯面量是double而非float
• float需要用f或F后缀来表明身份
使用浮点数进行运算精度低,因此进行运算时候最好将浮点数转换为整数运算后再转换为浮点数。
例如:计算0.123+0.456可先计算123+456在除以1000。
6.2.5.5 *浮点数的内部表达
• 浮点数在计算时是由专用的硬件部件实现的
• 计算double和float所用的部件是一样的
6.2.5.6 选择浮点类型
•如果没有特殊需要,只使用double。
•现代CPU能直接对double做硬件运算,性能不会比float差,在64位的机器上,数据存储的速度也不比float慢。
6.2.6 字符类型
• char是一种整数,也是一种特殊的类型:字符。这是因为:
• 用单引号表示的字符字面量:'a', '1'。
• ''也是一个字符。
• printf和scanf里用%c来输入输出字符。
6.2.6.1 字符的输入输出
如何输入'1'这个字符给char c?
• scanf("%c", &c);—>1
• scanf("%d", &i); c=i; —>49
• '1'的ASCII编码是49,所以当c==49时,它代表'1'
• printf("%i %c\n", c,c );
• 一个49各自表述!
#include<stdio.h>
int main()
{
char c;
char d;
c = 1;
d = 'A';
if(c==d)
{
printf("相等\n");
}
else
{
printf("不相等\n");
}
printf("c=%d\n",c);
printf("d=%d\n",d);
return 0;
}
#include<stdio.h>
int main()
{
char c;
printf("请输入一个字符:");
scanf("%c",&c);
printf("c=%d\n",c);
printf("d=%c\n",c);
return 0;
}
6.2.6.2 字符计算
#include<stdio.h>
int main()
{
char c = 'A';
c++;
printf("%c\n",c);
printf("%c\n",--c);
int i = 'Z'-'A';
printf("%d\n",i);
}
6.2.6.3 ASCII码表
字母在ASCII表中是顺序排列的
• 大写字母和小写字母是分开排列的,并不在一起
• ‘a’-‘A’可以得到两段之间的距离,于是
• a+’a’-‘A”可以把一个大写字母变成小写字母,而
• a+’A’-‘a’可以把一个小写字母变成大写字母
6.2.7 逃逸字符
字符 | 意义 | 字符 | 意义 |
\b | 回退一格 | \" | 双引号 |
\t | 到下一个表格位 | \' | 单引号 |
\n | 换行 | \\ | 反斜杠本身 |
\r | 回车 |
6.2.7.1 制表位
• 每行的固定位置
• 一个\t使得输出从下一个制表位开始
• 用\t才能使得上下两行对齐
6.2.7 自动类型转换
• 当运算符的两边出现不一致的类型时,会自动转换成较大的类型
• 大的意思是能表达的数的范围更大
• char —> short —> int —> long —> long long
• int —> float —> double
• 对于printf,任何小于int的类型会被转换成int;float会被转换成double
• 但是scanf不会,要输入short,需要%hd
6.2.9 强制类型转换
• 要把一个量强制转换成另一个类型(通常是较小的类型),需要:
• (类型)值
• 比如:
(int)10.2;
(short)32;
• 注意这时候的安全性,小的变量不总能表达大的量
(short)32768;
• 只是从那个变量计算出了一个新的类型的值,它并不改变那个变量,无论是值还是类型都不改变。
6.3 其他数据类型
6.3.1 逻辑类型(BOOL)
#include <stdbool.h>
之后就可以使⽤用bool和true、false
#include<stdio.h>
#include<stdbool.h>
int main()
{
bool b = 6>5;
bool t = true;
printf("%d\n",t);
t = 2;
printf("%d\n",b);
printf("%d\n",t);
return 0;
}
6.3.2 逻辑运算
• 逻辑运算是对逻辑量进行的运算,结果只有0或1
• 逻辑量是关系运算或逻辑运算的结果
运算符 | 描述 | 示例 | 结果 |
! | 逻辑非 | !a | 如果a是true结果就i是false,如果a是false结果就是true |
&& | 逻辑与 | a&&b | 如果a和b都是true,结果就是true,否则就是false |
|| | 逻辑或 | a||b | 如果a和b有一个是true,结果为true;两个都是false,结果为false |
6.3.2.1 优先级
!>&&>||
优先级 | 运算符 | 结合性 |
1 | () | 从左到右 |
2 | ! + - ++ -- | 从右到左(单目的+和-) |
3 | * / % | 从左到右 |
4 | + - | 从左到右 |
5 | < <= > >= | 从左到右 |
6 | == != | 从左到右 |
7 | && | 从左到右 |
8 | || | 从左到右 |
9 | = += -= *= /= %= | 从右到左 |
6.3.2.2 短路
逻辑运算是自左向右进⾏行的,如果左边的结果已经能够决定结果了,就不会做右边的计算
• a==6 && b==1
• a==6 && b+=1
• 对于&&,左边是false时就不做右边了
• 对于||,左边是true时就不做右边了
不要把赋值,包括复合赋值组合进表达式!
6.3.3 条件运算
count = (count > 20) ? count -10 : count+10;
条件、条件满足时的值和条件不满足时的值。相当于:
if ( count >20 )
count = count-10;
else
count = count+10;
6.3.3.1 优先级
条件运算符的优先级高于赋值运算符,但是低于其他运算符。
6.3.4 逗号运算
6.3.4.1 逗号运算符
逗号用来连接两个表达式,并以其右边的表达式的值作为它的结果。逗号的优先级是所有的运算符中最低的,所以它两边的表达式会先计算;逗号的组合关系是自左向右,所以左边的表达式会先计算,而右边的表达式的值就留下来作为逗号运算的结果。
#include<stdio.h>
int main()
{
int i,j;
i = 3+4,5+6;
j = (3+4,5+6);
printf("i = %d\n",i);
printf("j = %d\n",j);
return 0;
}
6.3.4.2 在for中使用
for ( i=0, j=10; i<j; i++, j- - )...
07 函数
7.1 函数定义与使用
7.1.1 函数定义
7.1.2 调用函数
• 函数名(参数值);
• ()起到了表⽰示函数调用的重要作用
• 即使没有参数也需要()
• 如果有参数,则需要给出正确的数量和顺序
• 这些值会被按照顺序依次⽤用来初始化函数中的参数
#include<stdio.h>
void cheer()
{
printf("cheer\n");
}
void sum(int begin,int end)
{
int i;
int sum = 0;
for(i = begin;i<=end;i++)
{
sum += i;
}
printf("%d到%d的和是%d\n",begin,end,sum);
}
int main()
{
cheer();
sum(1,10);
sum(20,30);
sum(35,45);
return 0;
}
7.1.3 从函数中返回值
int max(int a, int b)
{
int ret;
if(a>b)
{
ret = a;
}
else
{
ret = b;
}
return ret;
}
return停止函数的执⾏行,并送回一个值
• return;
• return 表达式;
• 一个函数里可以出现多个return语句。
如果函数有返回值,则必须使用带值的return。
• 可以赋值给变量;
• 可以再传递给函数;
• 甚至可以丢弃;
• 有的时候要的是副作用。
#include<stdio.h>
int max(int a, int b)
{
int ret;
if(a>b)
{
ret = a;
}
else
{
ret = b;
}
return ret;
}
int main()
{
int a,b,c;
a = 5;
b = 6;
c = max(10,12);
c = max(a,b);
printf("%d\n",max(c,23));
printf("%d\n",max(a,b));
return 0;
}
#include<stdio.h>
int max(int a, int b)
{
int ret;
if(a>b)
{
return a;
}
else
{
return b;
}
//return ret;
}
int main()
{
int a,b,c;
a = 5;
b = 6;
c = max(10,12);
c = max(a,b);
printf("%d\n",max(c,23));
printf("%d\n",max(a,b));
return 0;
}
7.1.4 没有返回值的函数
• void 函数名(参数表);
• 不能使⽤用带值的return;
• 可以没有return;
• 调用的时候不能做返回值的赋值。
7.2 函数的参数和变量
7.2.1 函数先后关系
C的编译器自上而下顺序分析代码,因此被调用函数应该放在调用函数的上方。
若被调用函数放在调用函数后方,调用函数中运行到调用处时,编译器会根据函数参数自行猜测函数返回值等等(此时编译器会警告),当执行到被调函数本身时,若函数本身返回值等于编译器所猜测的不同会导致报错,当然,有的编译环境下也有可能不报错。
函数原型声明里参数可不写或者不一致。
#include<stdio.h>
void sum(int begin,int end); // 函数原型声明,参数可不写,或者可不一致
int main()
{
sum(1,10);
sum(20,30);
sum(35,45);
return 0;
}
void sum(int begin,int end) //函数的定义
{
int i;
int sum = 0;
for(i = begin;i<=end;i++)
{
sum += i;
}
printf("%d到%d的和是%d。",begin,end,sum);
printf("\n");
}
7.2.2 参数传递
7.2.2.1 调用函数
如果函数有参数,调用函数时必须传递给它数量、类型正确的值
可以传递给函数的值是表达式的结果,这包括:字⾯面量、变量、函数的返回值、计算的结果。
如果传递类型不匹配:
• 调用函数时给的值与参数的类型不匹配是C语言传统上最大的漏洞。
• 编译器总是悄悄替你把类型转换好,但是这很可能不是你所期望的。
#include<stdio.h>
void cheer(int i)
{
printf("cheer %d\n",i);
}
int main()
{
cheer(2.4);
return 0;
}
7.2.2.2 传值
• 每个函数有自己的变量空间,参数也位于这个独立的空间中,和其他函数没有关系
• 过去,对于函数参数表中的参数,叫做“形式参数”,调用函数时给的值,叫做“实际参数”
• 由于容易让初学者误会实际参数就是实际在函数中进行计算的参数,误会调⽤用函数的时候把变量而不是值传进去了,所以我们不建议继续用这种古老的方式来称呼它们。
7.2.3 本地变量
• 函数的每次运行,就产生了一个独立的变量空间,在这个空间中的变量,是函数的这次运行所独有的,称作本地变量
• 定义在函数内部的变量就是本地变量
• 参数也是本地变量
7.2.3.1 变量的生存期和作用域
• 生存期:什么时候这个变量开始出现了,到什么时候它消亡了
• 作用域:在(代码的)什么范围内可以访问这个变量(这个变量可以起作用)
• 对于本地变量,这两个问题的答案是统一的:大括号内——块
#include<stdio.h>
void swap(int x,int y);
int main()
{
int a=5,b=6;
swap(a,b);
printf("a=%d,b=%d\n",a,b);
return 0;
}
void swap(int x,int y)
{
int swap;
int t = x;
x = y;
y = t;
printf("x=%d,y=%d\n",x,y);
}
7.2.3.2 本地变量的规则
• 本地变量是定义在块内的
• 它可以是定义在函数的块内
• 也可以定义在语句的块内
• 甚至可以随便拉一对大括号来定义变量
• 程序运行进入这个块之前,其中的变量不存在,离开这个块,其中的变量就消失了
• 块外面定义的变量在里面仍然有效
• 块里面定义了和外面同名的变量则掩盖了外面的
• 不能在一个块内定义同名的变量
• 本地变量不会被默认初始化
• 参数在进入函数的时候被初始化了
7.2.4 其他细节
7.2.4.1 没有参数时
void f(void);
• 还是
void f();
• 在传统C中,它表示f函数的参数表未知,并不表示没有参数
#include<stdio.h>
void swap();
int main()
{
int a=5,b=6;
swap(a,b);
printf("a=%d,b=%d\n",a,b);
return 0;
}
void swap(double x,double y)
{
int swap;
int t = x;
x = y;
y = t;
printf("x=%d,y=%d\n",x,y);
}
7.2.4.2 逗号运算符?
• 调用函数时的逗号和逗号运算符怎么区分?
• 调用函数时的圆括号里的逗号是标点符号,不是运算符
f(a,b); //标点符号
f((a,b)); //逗号运算符
7.2.4.3 函数里的函数?
C语言中不允许函数嵌套定义。
7.2.4.5 int i,j,sum(int a,int b);
int i,j,sum(int a,int b);
//定义了两个int型变量,以及需要a和b两个参数的sum函数并返回int型
不建议这么写。
7.2.4.6 return(i);
return (i);
会让人误解return是一个需要传参的函数。
7.2.4.7 关于main
• int main()也是一个函数
• 要不要写成int main(void)?
• return的0有人看吗?
返回0表示程序正常退出,没有错误。
• Windows:if errorlevel 1 …
• Unix Bash:echo $?
• Csh:echo $status
08 数组
8.1 定义数组
• <类型> 变量名称[元素数量];
int grades[100];
double weight[20];
• 元素数量必须是整数;
• C99之前:元素数量必须是编译时刻确定的字面量。
#include<stdio.h>
int main()
{
int x;
double sum = 0;
int cnt = 0;
int number[100];
scanf("%d",&x);
while( x!= -1)
{
number[cnt] = x;
{
int i;
printf("%d\t",cnt);
for(i=0;i<=cnt;i++)
{
printf("%d\t",number[i]);
}
printf("\n");
}
sum += x;
cnt ++;
scanf("%d",&x);
}
if(cnt > 0)
{
printf("%f\n",sum/cnt);
int i;
for(i = 0 ;i<cnt;i++)
{
if(number[i]>sum/cnt)
{
printf("%d\n",number[i]);
}
}
}
return 0;
}
8.2 数组
是一种容器(放东西的东西),特点是:
• 其中所有的元素具有相同的数据类型;
• 一旦创建,不能改变大小;
• *(数组中的元素在内存中是连续依次排列的)。
int a[10];
• 一个int的数组
• 10个单元:a[0],a[1],…,a[9]!
• 每个单元就是一个int类型的变量
• 可以出现在赋值的左边或右边:
a[2] = a[1]+6;
• *在赋值左边的叫做左值。
8.2.1 数组的单元
• 数组的每个单元就是数组类型的一个变量
• 使用数组时放在[]中的数字叫做下标或索引,下标从0开始计数:
grades[0];
grades[99];
average[5];
8.2.2 有效的下标范围
编译器和运行环境都不会检查数组下标是否越界,无论是对数组单元做读还是写
• 一旦程序运行,越界的数组访问可能造成问题,导致程序崩溃
segmentation fault
• 但是也可能运气好,没造成严重的后果
• 所以这是程序员的责任来保证程序只使用有效的下标值:[0,数组的大小-1]
8.2.3 长度为0的数组
int a[0];
可以存在,但是无用。
8.2.4 数组案例:统计个数
#include<stdio.h>
int main()
{
const int number = 10; //数组大小
int x;
int count[number]; //定义数组
int i;
for(i=0;i<number;i++) //初始化数组
{
count[i] = 0;
}
printf("请开始输入值,使用-1结束输入:");
scanf("%d",&x);
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;
}
8.3 数组运算
8.3.1 数组运算
8.3.1.1 数组的集成初始化
int a[] = {2,4,6,7,1,3,5,9,11,13,23,14,32};
• 直接用大括号给出数组的所有元素的初始值。
• 不需要给出数组的大小,编译器替你数数。
int b[20] = {2};
如果给出了数组的大小,但是后面的初始值数量不足,则其后的元素被初始化为0。
int b[20] = {0}; //20个元素均被初始化为0
int a[10] = {[0] = 2,[2] = 3,6,};
• 用[n]在初始化数据中给出定位
• 没有定位的数据接在前面的位置后面
• 其他位置的值补零
• 也可以不给出数组大小,让编译器算
• 特别适合初始数据稀疏的数组
8.3.2 数组的大小
• sizeof给出整个数组所占据的内容的大小,单位是字节!
sizeof(a)/sizeof(a[0]);
• sizeof(a[0])给出数组中单个元素的大小,于是相除就得到了数组的单元个数。
• 这样的代码,一旦修改数组中初始的数据,不需要修改遍历的代码。
8.3.3 数组的赋值
• 数组变量本身不能被赋值
• 要把一个数组的所有元素交给另一个数组,必须采用遍历。
for(i = 0;i<length;i++)
{
b[i] = a[i];
}
8.3.4 遍历数组
for(i = 0;i<length;i++)
{
b[i] = a[i];
}
for(i=0;i<number;i++)
{
count[i] = 0;
}
for(i=0;i<length;i++)
{
if(a[i] == key)
{
ret = i;
break;
}
}
for(i=0;i<cnt;i++)
{
if(number[i]>average)
{
printf("%d ",number[i]);
}
}
for(i=0;i<number;i++)
{
printf("%d:%d\n",i,count[i]);
}
• 通常都是使用for循环,让循环变量i从0到<数组的长度,这样循环体内最大的i正好是数组最大的有效下标
• 常见错误是:
• 循环结束条件是<=数组长度,或离开循环后,继续用i的值来做数组元素的下标!
int search(int key,int a[],int length)
{
int ret = -1;
int i;
for(i=0;i<length;i++)
{
if(a[i] == key)
{
ret = i;
break;
}
}
return ret;
}
数组作为函数参数时,往往必须再用另一个参数来传入数组的大小。
数组作为函数的参数时:
• 不能在[]中给出数组的大小
• 不能再利用sizeof来计算数组的元素个数!
#include<stdio.h>
int search(int key,int a[],int lenget);
int main()
{
int a[] = {2,4,6,7,1,3,5,9,11,13,23,14,32};
{
int i;
for(i = 0;i<sizeof(a)/sizeof(a[0]);i++)
{
printf("%d\t",a[i]);
}
printf("\n");
}
int x;
int loc;
printf("请输入一个需要寻找的数字:");
scanf("%d",&x);
loc = search(x,a,sizeof(a)/sizeof(a[0]));
if(loc != -1)
{
printf("%d在第%d个位置上\n",x,loc);
}
else
{
printf("%d不存在\n",x);
}
return 0;
}
int search(int key,int a[],int length)
{
int ret = -1;
int i;
for(i=0;i<length;i++)
{
if(a[i] == key)
{
ret = i;
break;
}
}
return ret;
}
8.3.5 数组案例:素数
在判断一个数是否是素数时
#include<stdio.h>
int isPrime(int x);
int main()
{
int x;
printf("请输入一个整数:");
scanf("%d",&x);
if(isPrime(x))
{
printf("%d是素数\n",x);
}
else
{
printf("%d不是素数\n",x);
}
return 0;
}
int isPrime(int x)
{
int ret = 1;
int i;
if(x == 1) ret = 0;
for(i=2;i<x;i++)
{
if(x%i == 0)
{
ret = 0;
break;
}
}
return ret;
}
改进:去掉偶数后,从3到x-1,每次加2
int isPrime(int x)
{
int ret = 1;
int i;
if(x == 1 || (x%2 == 0 && x != 2))
{
ret = 0;
}
for(i = 3;i<x;i+=2)
{
if(x%i == 0)
{
ret = 0;
break;
}
}
return ret;
}
改进:只需要循环sqrt(x)遍
int isPrime(int x)
{
int ret = 1;
int i;
if(x == 1 || (x%2 == 0 && x != 2))
{
ret = 0;
}
for(i = 3;i<sqrt(x);i+=2)
{
if(x%i == 0)
{
ret = 0;
break;
}
}
return ret;
}
改进:判断是否能被已知的且<x的素数整除。
使用比x小的素数判断x是否是素数,需要一张已有的素数表来判断。
#include<stdio.h>
int isPrime(int x,int knownPrimes[],int numberOfKnownPrimes);
int main(void)
{
const int number = 10;
int prime[10] = {2}; //初始化数组
int count = 1;
int i = 3;
{
int i;
printf("\t\t");
for(i = 0;i<number;i++)
{
printf("%d\t",i);
}
printf("\n");
}
while(count<number)
{
if(isPrime(i,prime,count))
{
prime[count++] = i;
}
{
printf("i=%d \tcnt=%d\t",i,count);
int i;
for(i = 0;i<number;i++)
{
printf("%d\t",prime[i]);
}
printf("\n");
}
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;
}
8.3.6 构造素数表
• 欲构造n以内的素数表
1. 令x为2;
2. 将2x、3x、4x直至ax<n的数标记为非素数;
3. 令x为下一个没有被标记为非素数的数,重复2;直到所有的数都已经尝试完毕
• 欲构造n以内(不含)的素数表
1. 开辟prime[n],初始化其所有元素为1,prime[x]为1表示x是素数;
2. 令x=2;
3. 如果x是素数,则对于(i=2;x*i<n;i++)令prime[i*x]=0;
4. 令x++,如果x<n,重复3,否则结束。
#include<stdio.h>
int main()
{
const int maxNumber = 25;
int isPrime[25];
int i;
int x;
for(i=0;i<maxNumber;i++)
{
isPrime[i] = 1;
}
printf("\t");
for(i =2;i<maxNumber;i++)
{
printf("%d\t",i);
}
printf("\n");
for(x=2;x<maxNumber;x++)
{
if(isPrime[x])
{
for(i=2;i*x<maxNumber;i++)
{
isPrime[i*x] = 0;
}
}
printf("%d\t",x);
for(i=2;i<maxNumber;i++)
{
printf("%d\t",isPrime[i]);
}
printf("\n");
}
for(i=2;i<maxNumber;i++)
{
if(isPrime[i])
{
printf("%d\t",i);
}
}
printf("\n");
return 0;
}
8.4 二维数组
• int a[3][5];
• 通常理解为a是一个3行5列的矩阵
8.4.1 二维数组的遍历
for(i=0;i<3;i++)
{
for(j=0;j<5;j++)
{
a[i][j] = i*j;
}
}
• a[i][j]是一个int
• 表示第i行第j列上的单元
• a[i,j]是什么?
8.4.2 二维数组的初始化
int a[][5] = {
{0,1,2,3,4},
{2,3,4,5,6},};
• 列数是必须给出的,行数可以由编译器来数
• 每行一个{},逗号分隔
• 最后的逗号可以存在,有古老的传统
• 如果省略,表示补零
• 也可以用定位(*C99 ONLY)
8.4.3 井字棋游戏
• 读入一个3X3的矩阵,矩阵中的数字为1表示该位置上有一个X,为0表示为O。
• 程序判断这个矩阵中是否有获胜的一方,输出表示获胜一方的字符X或O,或输出无人获胜。
#include<stdio.h>
int main()
{
const int size = 3;
int board[size][size];
int i,j;
int numofx;
int numofo;
int result = -1; // -1:没人赢,1:x赢,0:o赢
//读入矩阵,下棋
printf("开始井字棋游戏,请用x和o输入共9个字符:");
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++)
{
numofo = numofx = 0;
for(j = 0;j<size;j++)
{
if(board[i][j] == 1)
{
numofx++;
}
else
{
numofo ++;
}
}
if(numofo == size)
{
result = 0;
}
else if(numofx == size)
{
result = 1;
}
}
//检查列
if(result == 1)
{
for(j = 0;j<size && result == -1;j++)
{
numofo = numofx = 0;
for(i = 0;i<size;i++)
{
if(board[i][j] == 1)
{
numofx ++;
}
else
{
numofo ++;
}
}
if(numofo == size)
{
result = 0;
}else if(numofx == size)
{
result = 1;
}
}
}
//检查对角线
//主对角线
numofo = numofx = 0;
for(i=0;i<size;i++)
{
if(board[i][i] == 1)
{
numofx ++;
}
else
{
numofo ++;
}
}
if(numofo == size)
{
result = 0;
}
else if(numofx == size)
{
result = 1;
}
//逆对角线
numofo = numofx = 0;
for(i = 0;i<size;i++)
{
if(board[i][size-i-1] == 1)
{
numofx ++;
}
else
{
numofo ++;
}
}
return 0;
}
09 指针
9.1 取地址运算
9.1.1 运算符 &
scanf(“%d”, &i);
里的&获得变量的地址,它的操作数必须是变量
int i;
printf(“%x”,&i);
地址的大小是否与int相同取决于编译器
int i;
printf(“%p”,&i);
#include<stdio.h>
int main()
{
int i = 0;
int p;
p = (int)&i; //将i的地址强制转换为int类型
printf("0x%x\n",&i);
printf("%p\n",&i);
printf("0x%x\n",p);
printf("%lu\n",sizeof(int));
printf("%lu\n",sizeof(&i)); //64位架构下与32位架构下不一致
return 0;
}
9.1.2 &不能取得地址
&不能对没有地址的东⻄西取地址
&(a+b);
&(a++);
&(++a);
均不能取。
9.1.3 数组的地址
数组的地址指向的是数组中第一个元素的地址,数组中其余元素地址依次递增。
9.2 指针
9.2.1 scanf
• 如果能够将取得的变量的地址传递给一个函数,能否通过这个地址在那个函数内访问这个变量?
• scanf(“%d”, &i);
• scanf()的原型应该是怎样的?我们需要一个参数能保存别的变量的地址,如何表达能够保存地址
的变量?
9.2.2 指针
指针就是保存地址得变量。
int i;
int* p = &i; //*表示p是一个指针,指向的是一个int,把i的地址交给p
int* p,q; //指针p和int类型q
int *p,q; //指针p和int类型q
9.2.3 指针变量
• 变量的值是内存的地址
• 普通变量的值是实际的值
• 指针变量的值是具有实际值的变量的地址
9.2.4 作为参数的指针
void f(int *p);
int i=0;
f(&i);
在被调用的时候得到了某个变量的地址:在函数里面可以通过这个指针访问外面的这个i。
#include<stdio.h>
void f(int *p);
int main()
{
int i = 6;
printf("&i=%p\n",&i);
f(&i);
return 0;
}
void f(int *p)
{
printf(" p=%p\n",p);
}
9.2.5 访问地址上的变量
*是一个单目运算符,用来访问指针的值所表示的地址上的变量
• 可以做右值也可以做左值
int k = *p;
*p = k+1;
#include<stdio.h>
void f(int *p);
void g(int k);
int main()
{
int i = 6;
printf("&i=%p\n",&i);
f(&i);
g(i);
return 0;
}
void f(int *p)
{
printf(" p=%p\n",p);
printf("*p = %d\n",*p);
}
void g(int k)
{
printf("k=%d\n",k);
}
#include<stdio.h>
void f(int *p);
void g(int k);
int main()
{
int i = 6;
printf("&i=%p\n",&i);
f(&i);
g(i);
return 0;
}
void f(int *p)
{
printf(" p=%p\n",p);
printf("*p = %d\n",*p);
*p = 26;
}
void g(int k)
{
printf("k=%d\n",k);
}
9.2.6 左值
*左值之所以叫左值
• 是因为出现在赋值号左边的不是变量,而是值,是表达式计算的结果:
a[0] = 2;
*p = 3;
• 是特殊的值,所以叫做左值
9.2.7 指针的运算符&*
互相反作用
• *&yptr -> * (&yptr) -> * (yptr的地址)-> 得到那个地址上的变量 -> yptr!
• &*yptr -> &(*yptr) -> &(y) -> 得到y的地址,也就是yptr -> yptr
9.2.8 scanf未写&符号时没报错
int i;
scanf(“%d”, i);
没有报错是因为在32位架构中,整数和地址大小一样,这种情况下scanf无法判断传入的是否是一个地址。
9.3 指针作用
9.3.1 指针应用场景一
交换两个变量的值
void swap(int *pa, int *pb)
{
int t = *pa;
*pa = *pb;
*pb = t;
}
#include<stdio.h>
void swap(int *pa,int *pb);
int main()
{
int a = 5;
int b = 6;
swap(&a,&b);
printf("a=%d,b=%d\n",a,b);
return 0;
}
void swap(int *pa,int *pb)
{
int t = *pa;
*pa = *pb;
*pb = t;
}
9.3.2 指针应用场景二
• 函数返回多个值,某些值就只能通过指针返回
• 传入的参数实际上是需要保存带回的结果的变量
从一个数组中找出最大最小值。
#include<stdio.h>
void minmax(int a[],int len, int *max,int *min);
int main(void)
{
int a[]={1,2,3,4,5,6,7,8,9,12,13,14,16,17,21,23,55};
int min,max;
minmax(a,sizeof(a)/sizeof(a[0]),&min,&max);
printf("min = %d,max = %d\n",min,max);
return 0;
}
void minmax(int a[],int len,int *min,int *max)
{
int i;
*min = *max = a[0];
for(i=1;i<len;i++)
{
if(a[i]<*min)
{
*min = a[i];
}
if(a[i]>*max)
{
*max = a[i];
}
}
}
9.3.3 指针应用场景三
• 函数返回运算的状态,结果通过指针返回
• 常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错:
• -1或0(在文件操作会看到大量的例子)
• 但是当任何数值都是有效的可能结果时,就得分开返回了
• 后续的语言(C++,Java)采用了异常机制来解决这个问题。
#include<stdio.h>
/*
@return 如果除法成功,返回1;否则返回0;
*/
int divide(int a,int b,int *result);
int main(void)
{
int a=5;
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)
{
ret = 0;
}
else
{
*result = a/b;
}
return ret;
}
9.3.4 指针最常见的错误
定义了指针变量,还没有指向任何变量,就开始使用指针。
9.4 指针与数组
9.4.1 数组参数
int isPrime(int x,int knownP1rimes[],int numberOfKnownPrimes)
{
int ret = 1;
int i;
for(i=0;i<numberofKnownPrimes;i++)
{
if(x%knownPrimies[i]==0)
{
ret = 0;
break;
}
}
return ret;
}
• 函数参数表中的数组实际上是指针
sizeof(a) == sizeof(int*);
• 但是可以用数组的运算符[]进行运算
#include<stdio.h>
void minmax(int a[],int len, int *max,int *min);
int main(void)
{
int a[]={1,2,3,4,5,6,7,8,9,12,13,14,16,17,21,23,55};
int min,max;
printf("main sizeof(a) = %lu\n",sizeof(a));
printf("main a = %p\n",a);
minmax(a,sizeof(a)/sizeof(a[0]),&min,&max);
printf("a[0]=%d\n",a[0]);
printf("min = %d,max = %d\n",min,max);
return 0;
}
void minmax(int a[],int len,int *min,int *max)
{
int i;
printf("minmax sizeof(a)=%lu\n",sizeof(a));
printf("minmax a=%p\n",a);
a[0] = 1000;
*min = *max = a[0];
for(i=1;i<len;i++)
{
if(a[i]<*min)
{
*min = a[i];
}
if(a[i]>*max)
{
*max = a[i];
}
}
}
#include<stdio.h>
void minmax(int *a,int len, int *max,int *min);
int main(void)
{
int a[]={1,2,3,4,5,6,7,8,9,12,13,14,16,17,21,23,55};
int min,max;
printf("main sizeof(a) = %lu\n",sizeof(a));
printf("main a = %p\n",a);
minmax(a,sizeof(a)/sizeof(a[0]),&min,&max);
printf("a[0]=%d\n",a[0]);
printf("min = %d,max = %d\n",min,max);
return 0;
}
void minmax(int *a,int len,int *min,int *max)
{
int i;
printf("minmax sizeof(a)=%lu\n",sizeof(a));
printf("minmax a=%p\n",a);
a[0] = 1000;
*min = *max = a[0];
for(i=1;i<len;i++)
{
if(a[i]<*min)
{
*min = a[i];
}
if(a[i]>*max)
{
*max = a[i];
}
}
}
以下四种函数原型是等价的:
int sum(int *ar,int n);
int sum(int *,int);
int sum(int ar[],int n);
int sum(int [],int);
9.4.2 数组变量是特殊的指针
• 数组变量本身表达地址,所以
int a[10]; int*p=a; // 无需用&取地址
• 但是数组的单元表达的是变量,需要用&取地址
a == &a[0];
• []运算符可以对数组做,也可以对指针做:
p[0] <==> a[0];
• *运算符可以对指针做,也可以对数组做:
*a = 25;
• 数组变量是const的指针,所以不能被赋值
int a[] <==> int * const a=….
9.5 指针与const
9.5.1 指针是const
• 表示一旦得到了某个变量的地址,不能再指向其他变量
int * const q = &i; //q是const
*q = 26; // OK,指针q所指向的i可以进行运算。
q++; // ERROR
9.5.2 所指是const
表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const)
const int *p = &i;
*p = 26; // ERROR! (*p) 是 const
i = 26; //OK
p = &j; //OK
int i;
const int* p1 = &i;
int const* p2 = &i;
int *const p3 = &i;
const在*前面,表示指针所指向的东西不能通过指针去修改;const在*后面,表示指针不能被修改。
9.5.3 转换
总是可以把一个非const的值转换成const的。
void f(const int* x); //保证在函数内部不会去修改指针所指的值
int a = 15;
f(&a);//ok
const int b = a;
f(&b);//ok
b = a+1; //Error
当要传递的参数的类型比地址大的时候,这是常用的手段:既能用比较少的字节数传递值给参数,又能避免函数对外面的变量的修改。
9.6 const数组
const int a[] = {1,2,3,4,5,6,};
• 数组变量已经是const的指针了,这里的const表明数组的每个单元都是const int。
• 所以必须通过初始化进行赋值。
9.7 保护数组值
• 因为把数组传入函数时传递的是地址,所以那个函数内部可以修改数组的值。
• 为了保护数组不被函数破坏,可以设置参数为const。
int sum(const int a[], int length);
9.8 指针运算
给一个指针加1表示要让指针指向下一个变量
int a[10];
int *p = a;
*(p+1) —> a[1]
• 如果指针不是指向一片连续分配的空间,如数组,则这种运算没有意义。
#include<stdio.h>
int main()
{
char ac[] = {0,1,2,3,4,5,6,7,8,9};
char *p =ac;
printf("p =%p\n",p);
printf("p+1=%p\n",p+1);
printf("*(p+1)=%d\n",*(p+1));
int ai[]={0,1,2,3,4,5,6,7,8,9};
int *q = ai;
printf("q =%p\n",q);
printf("q+1=%p\n",q+1);
printf("*(q+1)=%d\n",*(q+1));
return 0;
}
9.8.1 指针计算
• 这些算术运算可以对指针做:
• 给指针加、减一个整数(+, +=, -, -=)
• 递增递减(++/—)
• 两个指针相减
#include<stdio.h>
int main()
{
char ac[] = {0,1,2,3,4,5,6,7,8,9};
char *p =ac;
char *p1 = &ac[5];
printf("p =%p\n",p);
printf("p+1=%p\n",p+1);
printf("*(p+1)=%d\n",*(p+1));
printf("p1-p=%d\n",p1-p);
int ai[]={0,1,2,3,4,5,6,7,8,9};
int *q = ai;
int *q1 = &ai[6];
printf("q =%p\n",q);
printf("q+1=%p\n",q+1);
printf("*(q+1)=%d\n",*(q+1));
printf("q1-q=%d\n",q1-q); //相减的数为两个地址之间的元素个数
return 0;
}
9.8.2 *p++
• 取出p所指的那个数据来,完事之后顺便把p移到下一个位置去
• *的优先级虽然高,但是没有++高
• 常用于数组类的连续空间操作
• 在某些CPU上,这可以直接被翻译成一条汇编指令
#include<stdio.h>
int main()
{
char ac[] = {0,1,2,3,4,5,6,7,8,9,-1};
char *p =&ac[0];
int i;
for(i=0;i<sizeof(ac)/sizeof(ac[0]);i++)
{
printf("%d\n",ac[i]);
}
// for(p=ac;*p!=-1)
while(*p != -1)
{
printf("%d\n",*p++);
}
int ai[]={0,1,2,3,4,5,6,7,8,9};
int *q = ai;
return 0;
}
9.8.3 指针比较
• <, <=, ==, >, >=, != 都可以对指针做
• 比较它们在内存中的地址
• 数组中的单元的地址肯定是线性递增的
9.8.4 0地址
• 当然你的内存中有0地址,但是0地址通常是个不能随便碰的地址。
• 所以你的指针不应该具有0值
• 因此可以用0地址来表示特殊的事情:
• 返回的指针是无效的
• 指针没有被真正初始化(先初始化为0)
• NULL是一个预定定义的符号,表示0地址
• 有的编译器不愿意你用0来表示0地址
9.10 指针的类型
• 无论指向什么类型,所有的指针的大小都是一样的,因为都是地址。
• 但是指向不同类型的指针是不能直接互相赋值的。
• 这是为了避免用错指针。
char ac[] = {0,1,2,3,4,5,6,7,8,9,-1};
char *p =&ac[0];
int ai[]={0,1,2,3,4,5,6,7,8,9};
int *q = ai;
q = p; //ERROR
9.11 指针类型的转换
• void* 表示不知道指向什么东西的指针
• 计算时与char*相同(但不相通)
• 指针也可以转换类型
int *p = &i;
void*q = (void*)p;
• 这并没有改变p所指的变量的类型,而是让后人用不同的眼光通过p看它所指的变量
• 我不再当你是int啦,我认为你就是个void!
9.12 用指针来做什么
• 需要传入较大的数据时用作参数
• 传入数组后对数组做操作
• 函数返回不止一个结果
• 需要用函数来修改不止一个变量
• 动态申请的内存...
9.13 动态内存分配
9.13.1 输入数据
• 如果输入数据时,先告诉你个数,然后再输入,要记录每个数据。
• C99可以用变量做数组定义的大小,C99之前呢?
int *a = (int*)malloc(n*sizeof(int));
使用malloc之前需要导入:
#include<stdlib.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
int number;
int *a;
int i;
printf("输入数量:");
scanf("%d",&number);
a = (int*)malloc(number*sizeof(int)); //动态申请一块内存空间,(int *)强制将void类型指针转换为int类型
printf("请输入需要存放的数:");
for(i =0;i<number;i++)
{
scanf("%d",&a[i]);
}
for (i = number -1;i>=0;i--)
{
printf("%d ",a[i]);
}
free(a); //释放内存
return 0;
}
9.13.2 malloc
#include <stdlib.h>
void* malloc(size_t size);
• 向malloc申请的空间的大小是以字节为单位的
• 返回的结果是void*,需要类型转换为自己需要的类型
(int*)malloc(n*sizeof(int));
9.13.3 申请失败
没空间了?
• 如果申请失败则返回0,或者叫做NULL
• 你的系统能给你多大的空间?
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
void *p;
int cnt = 0;
while((p = malloc(100*1024*1024)))
{
cnt++;
}
printf("分配了%d00MB的空间\n",cnt);
return 0;
}
9.13.4 free()
• 把申请得来的空间还给“系统”
• 申请过的空间,最终都应该要还
• 混出来的,迟早都是要还的
• 只能还申请来的空间的首地址
• free(0)?
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
int i;
void *p = 0;
int cnt = 0;
free(p);
return 0;
}
9.13.5 常见的问题
• 申请了没free—>长时间运行内存逐渐下降
• 新手:忘了
• 老手:找不到合适的free的时机
• free过了再free
• 地址变过了,直接去free
10 字符串
10.1 字符串
• C语言的字符串是以字符数组的形态存在的
• 不能用运算符对字符串做运算
• 通过数组的方式可以遍历字符串
• 唯一特殊的地方是字符串字面量可以用来初始化字符数组
• 以及标准库提供了一系列字符串函数
10.1.1 字符数组
char word[] = {'H','e','l','l','o','!'};
word[0] | H |
word[1] | e |
word[2] | l |
word[3] | l |
word[4] | o |
word[5] | ! |
10.1.2 字符串
char word[] = {'H','e','l','l','o','!','\0'};
word[0] | H |
word[1] | e |
word[2] | l |
word[3] | l |
word[4] | o |
word[5] | ! |
word[6] | \0 |
• 以0(整数0)结尾的一串字符
• 0或’\0’是一样的,但是和’0’不同
• 0标志字符串的结束,但它不是字符串的一部分
• 计算字符串长度的时候不包含这个0
• 字符串以数组的形式存在,以数组或指针的形式访问
• 更多的是以指针的形式
• string.h 里有很多处理字符串的函数
10.1.3 字符串变量
char *str = "Hello";
char word[] = "Hello";
char line[10] = "Hello";
10.1.4 字符串常量
"Hello"
• ″Hello″ 会被编译器变成一个字符数组放在某处,这个数组的长度是6,结尾还有表示结束的0。
• 两个相邻的字符串常量会被自动连接起来
• 行末的\表示下一行还是这个字符串常量
10.2 字符串变量
char* s = "Hello, world!";
• s是一个指针,初始化为指向一个字符串常量
• 由于这个常量所在的地方,所以实际上s是 const char* s ,但是由于历史的原因,编译器接受不带const的写法。
• 但是试图对s所指的字符串做写入会导致严重的后果
• 如果需要修改字符串,应该用数组:
char s[] = "Hello, world!";
#include<stdio.h>
int main()
{
int i=0;
char *s = "Hello World";
char *s2 = "Hello World";
char s3[] = "Hello World";
// s[0] = 'B';
printf("&i=%p\n",&i);
printf("s = %p\n",s);
printf("s2 = %p\n",s2);
printf("s3 = %p\n",s3);
s3[0] = 'B';
printf("Here!s3[0]=%c\n",s3[0]);
return 0;
}
i和s3 位于很大的地方,但s和s2位于很小的地方。
10.2.1 使用指针还是数组
char *str = “Hello”;
char word[] = “Hello”;
• 数组:这个字符串在这里
• 作为本地变量空间自动被回收
• 指针:这个字符串不知道在哪里
• 处理参数
• 动态分配空间
• 如果要构造一个字符串—>数组
• 如果要处理一个字符串—>指针
10.2.2 char*是否是字符串?
• 字符串可以表达为char*的形式
• char*不一定是字符串
• 本意是指向字符的指针,可能指向的是字符的数组(就像int*一样)
• 只有它所指的字符数组有结尾的0,才能说它所指的是字符串
10.3 字符串运算
10.3.1 字符串赋值
char *t = "title";
char *s;
s = t;
并没有产生新的字符串,只是让指针s指向了t所指的字符串,对s的任何操作就是对t做的。
10.3.2 字符串输入输出
char string[8];
scanf("%s",string);
printf("%s",string);
scanf读入一个单词(到空格、tab或回车为止)。
#include<stdio.h>
int main(void)
{
char word[8];
char word2[8];
printf("请输入一个字符串:");
scanf("%s",word);
scanf("%s",word2);
printf("%s##%s##\n",word,word2);
return 0;
}
#include<stdio.h>
void f(void)
{
char word[8];
char word2[8];
printf("请输入一个字符串:");
scanf("%s",word);
scanf("%s",word2);
printf("%s##%s##\n",word,word2);
}
int main(void)
{
f();
return 0;
}
在32位下编译:数组越界
为了确保安全性,做如下修改:
#include<stdio.h>
void f(void)
{
char word[8];
char word2[8];
printf("请输入一个字符串:");
scanf("%7s",word); //最多读入7个字符,最后一个是"\0" ,超过部分不要
scanf("%7s",word2);
printf("%s##%s##\n",word,word2);
}
int main(void)
{
f();
return 0;
}
10.3.3 安全的输入
char string[8];
scanf(“%7s”, string);
• 在%和s之间的数字表示最多允许读入的字符的数量,这个数字应该比数组的大小小一。
10.3.4 常见错误
char *string;
scanf(“%s”, string);
• 以为char*是字符串类型,定义了一个字符串类型的变量string就可以直接使用了
• 由于没有对string初始化为0,所以不一定每次运行都出错
10.3.5 空字符串
char buffer[100] = "";
这是一个空字符串,buffer[0] ='\0'。
char buffer[] = "";
这个数组的长度只有1!
10.4 字符串数组
char **a;
• a是一个指针,指向另一个指针,那个指针指向一个字符(串)
char a[][];
char a[][0]={
"Hello",
}; //不可行
• a是一个二维数组,第二个维度的大小不知道,不能编译
char a[][10];
#include<stdio.h>
int main()
{
char a[][10]={
"Hello",
"World",
}; //a是一个数组,a里面每一个单元是一个char [10],a[0]-->char [10]
return 0;
}
• a是一个二维数组,a[x]是一个char[10]
char *a[];
• a是一个一维数组,a[x]是一个char*
#include<stdio.h>
int main()
{
char *a[]={
"Hello",
"World",
"asdfghjklqwertyuiop",
}; // 可行,a[0]相当于char *
return 0;
}
10.5 字符串参数
int main(int argc, char const *argv[]);
• argv[0]是命令本身。
• 当使用Unix的符号链接时,反映符号链接的名字。
#include<stdio.h>
int main(int argc,char const *argv[])
{
int i;
for(i = 0;i<argc;i++)
{
printf("%d:%s\n",i,argv[i]);
}
return 0;
}
了解Linux下busybox。
10.6 字符串函数
10.6.1 putchar
int putchar(int c);
• 向标准输出写一个字符
• 返回写了几个字符,EOF(-1)表示写失败
10.6.2 getchar
int getchar(void);
• 从标准输入读入一个字符
• 返回类型是int是为了返回EOF(-1)
• Windows—>Ctrl-Z
• Unix—>Ctrl-D
#include<stdio.h>
int main(int argc,char const *argv[])
{
int ch;
while((ch = getchar())!=EOF)
{
putchar(ch);
}
printf("EOF\n");
return 0;
}
10.6.3 标准库中的字符串函数
10.6.3.1 string.h
• strlen
• strcmp
• strcpy
• strcat
• strchr
• strstr
10.6.3.2 strlen
size_t strlen(const char *s);
• 返回s的字符串长度(不包括结尾的0)。
#include<stdio.h>
#include<string.h>
int mylen(const char* s)
{
int idx = 0;
while(s[idx] != '\0')
{
idx++;
}
return idx;
}
int main(int argc,char const *argv[])
{
char line[] = "Hello";
printf("strlen = %lu\n",strlen(line));
printf("mylen = %lu\n",mylen(line));
printf("sizeof = %lu\n",sizeof(line));
return 0;
}
10.6.3.3 strcmp
int strcmp(const char *s1, const char *s2);
• 比较两个字符串,返回:
• 0:s1==s2
• >0:s1>s2
• <0:s1<s2
#include<stdio.h>
#include<string.h>
int mycmp(const char* s1, const char* s2)
{
int idx = 0;
while(1)
{
if(s1[idx] != s2[idx])
{
break;
}
else if(s1[idx]=='\0')
{
break;
}
idx++;
}
return s1[idx]-s2[idx];
}
int main()
{
char s1[] = "abc";
char s2[] = "abc";
char s3[] = "bbc";
char s4[] = "ab";
char s5[] = "Abc";
//printf("%d\n",s1 == s2); //直接这么比较相当于比较两个数组的地址
printf("%d\n",strcmp(s1,s2));
printf("%d\n",strcmp(s1,s3));
printf("%d\n",strcmp(s1,s4));
printf("%d\n",strcmp(s1,s5));
printf("%d\n",mycmp(s1,s2));
return 0;
}
#include<stdio.h>
#include<string.h>
int mycmp(const char* s1, const char* s2)
{
int idx = 0;
while(s1[idx] == s2[idx]&&s1[idx]!='\0')
{
idx++;
}
return s1[idx]-s2[idx];
}
int main()
{
char s1[] = "abc";
char s2[] = "abc";
char s3[] = "bbc";
char s4[] = "ab";
char s5[] = "Abc";
//printf("%d\n",s1 == s2); //直接这么比较相当于比较两个数组的地址
printf("%d\n",strcmp(s1,s2));
printf("%d\n",strcmp(s1,s3));
printf("%d\n",strcmp(s1,s4));
printf("%d\n",strcmp(s1,s5));
printf("%d\n",mycmp(s1,s2));
return 0;
}
#include<stdio.h>
#include<string.h>
int mycmp(const char* s1, const char* s2)
{
while(*s1 == *s2 && *s1 != '\0')
{
s1++;
s2++;
}
return *s1-*s2;
}
int main()
{
char s1[] = "abc";
char s2[] = "abc";
char s3[] = "bbc";
char s4[] = "ab";
char s5[] = "Abc";
//printf("%d\n",s1 == s2); //直接这么比较相当于比较两个数组的地址
printf("%d\n",strcmp(s1,s2));
printf("%d\n",strcmp(s1,s3));
printf("%d\n",strcmp(s1,s4));
printf("%d\n",strcmp(s1,s5));
printf("%d\n",mycmp(s1,s2));
return 0;
}
10.6.3.4 strcpy
char * strcpy(char *restrict dst, const char *restrictsrc);
• 把src的字符串拷贝到dst
• restrict表明src和dst不重叠(C99)
• 返回dst
• 为了能链起代码来
10.6.3.4.1 复制一个字符串
char *dst = (char*)malloc(strlen(src)+1);
strcpy(dst, src);
#include<stdio.h>
#include<string.h>
char* mycpy(char* dst,const char* src)
{
int idx = 0;
while(src[idx])
{
dst[idx] = src[idx];
idx++;
}
dst[idx] = '\0';
return dst;
}
int main(int argc,char const *argv[])
{
char s1[] = "";
char s2[] = "abc";
char s3[] = "";
strcpy(s1,s2);
mycpy(s3,s2);
printf("s1 = %s\n",s1);
printf("s3 = %s\n",s3);
return 0;
}
#include<stdio.h>
#include<string.h>
char* mycpy(char* dst,const char* src)
{
while(*dst++ = *src++)
{
}
*dst = '\0';
return dst;
}
int main(int argc,char const *argv[])
{
char s1[] = "";
char s2[] = "abc";
char s3[] = "";
strcpy(s1,s2);
mycpy(s3,s2);
printf("s1 = %s\n",s1);
printf("s3 = %s\n",s3);
return 0;
}
10.6.3.5 strcat
char * strcat(char *restrict s1, const char *restricts2);
• 把s2拷贝到s1的后面,接成一个长的字符串。
• 返回s1
• s1必须具有足够的空间
10.6.3.6 复制和连接字符串时候的安全问题
(1)安全问题
如果目的地没有足够的空间,strcpy和strcat都可能出现安全问题。
(2)安全版本
char * strncpy(char *restrict dst, const char *restrictsrc, size_t n);
//通过n告诉函数你能够拷贝过去多少个字符
char * strncat(char *restrict s1, const char *restricts2, size_t n);
//通过n告诉函数你能够连接(拷贝)过去多少个字符
int strncmp(const char *s1, const char *s2, size_t n);
//只比较两个字符串中的前几个
10.6.3.7 字符串搜索函数
10.6.3.7.1 字符串找字符
char * strchr(const char *s, int c); \\从左找,返回指针
char * strrchr(const char *s, int c); \\从右找,返回指针
返回NULL表示没有找到。
字符串中有多个相同字符串时候,如何寻找第2个。
#include<stdio.h>
#include<string.h>
int main(int argc,char const *argv[])
{
char s[] = "hello";
char *p = strchr(s,'l'); //返回第一个l的位置
printf("p=%s\n",p);
char *t = (char*)malloc(strlen(p)+1);
strcpy(t,p);
printf("t=%s\n",t);
free(t);
p = strchr(p+1,'l'); //在第一个基础上寻找第二个
printf("p=%s\n",p);
return 0;
}
将字符串中所找到的字符前的部分提取出来。
#include<stdio.h>
#include<string.h>
int main(int argc,char const *argv[])
{
char s[] = "hello";
char *p = strchr(s,'l'); //返回第一个l的位置
printf("p=%s\n",p);
char c = *p;
*p = '\0';
char *t = (char*)malloc(strlen(s)+1);
strcpy(t,s);
printf("t=%s\n",t);
free(t);
return 0;
}
10.6.3.7.2 字符串中找字符串
char * strstr(const char *s1, const char *s2);
char * strcasestr(const char *s1, const char *s2);
11 结构类型
11.1 枚举
11.1.1 常量符号化
用符号而不是具体的数字来表示程序中的数字。
#include<stdio.h>
const int red = 0;
const int yellow = 1;
const int green = 2;
int main(int argc,char const *argv[])
{
int color = -1;
char *colorName = NULL;
printf("输入你喜欢的颜色代码:");
scanf("%d",&color);
switch(color)
{
case red:colorName = "red";break;
case yellow:colorName = "yellow";break;
case green:colorName = "green";break;
default:colorName = "unKnown";break;
}
printf("你喜欢的颜色是%s\n",colorName);
return 0;
}
11.1.2 枚举
#include<stdio.h>
enum COLOR{RED,YELLOW,GREEN};
int main(int argc,char const *argv[])
{
int color = -1;
char *colorName = NULL;
printf("输入你喜欢的颜色代码:");
scanf("%d",&color);
switch(color)
{
case RED:colorName = "red";break;
case YELLOW:colorName = "yellow";break;
case GREEN:colorName = "green";break;
default:colorName = "unKnown";break;
}
printf("你喜欢的颜色是%s\n",colorName);
return 0;
}
枚举是一种用户定义的数据类型,它⽤用关键字 enum 以如下语法来声明:
enum 枚举类型名字 {名字0, …, 名字n} ;
枚举类型名字通常并不真的使用,要用的是在大括号里的名字,因为它们就是就是常量符号,它们的类型是int,值则依次从0到n。如:
enum colors { red, yellow, green } ;
就创建了三个常量,red的值是0,yellow是1,而green是2。
当需要一些可以排列起来的常量值时,定义枚举的意义就是给了这些常量值名字。
枚举量可以作为值。
枚举类型可以跟上enum作为类型。
但是实际上是以整数来做内部计算和外部输入输出的。
#include<stdio.h>
enum color{red,yellow,green};
void f(enum color c);
int main(void)
{
enum color t = red;
scanf("%d",&t);
f(t);
return 0;
}
void f(enum color c)
{
printf("%d\n",c);
}
11.1.3 自动计数的枚举
#include<stdio.h>
enum COLOR{RED,YELLOW,GREEN,NumCOLORS};
int main(int argc,char const *argv[])
{
int color = -1;
char *ColorNames[NumCOLORS] = {
"red","yellow","green",};
char *colorName = NULL;
printf("输入你喜欢的颜色的代码:");
scanf("%d",&color);
if(color >= 0 && color < NumCOLORS)
{
colorName = ColorNames[color];
}
else
{
colorName = "unknown";
}
printf("你喜欢的颜色是%s\n",colorName);
return 0;
}
11.1.4 枚举量
声明枚举量的时候可以指定值
enum COLOR{RED=1,YELLOW,GREEN=5};
#include<stdio.h>
enum COLOR{RED=1,YELLOW,GREEN=5,NuumCOLORS};
int main()
{
printf("code for GREEN is %d\n",GREEN);
return 0;
}
11.1.5 枚举只是int
#include<stdio.h>
enum COLOR{RED=1,YELLOW,GREEN=5,NuumCOLORS};
int main(int argc,char const *argv[])
{
enum COLOR color = 0;
printf("code for GREEN is %d\n",GREEN);
printf("and color is %d\n",color);
return 0;
}
即使给枚举类型的变量赋不存在的整数值也没有任何warning或error。
11.1.6 枚举的使用
• 虽然枚举类型可以当作类型使用,但是实际上很(bu)少(hao)用
• 如果有意义上排比的名字,用枚举⽐比const int方便
• 枚举比宏(macro)好,因为枚举有int类型
11.2 结构类型
#include<stdio.h>
int main(int argc,char const *argv[])
{
struct date{
int month;
int day;
int year;
}; //需要注意此分号
struct date today;
today.month = 07;
today.day = 25;
today.year = 2024;
printf("Today's date is %i-%i-%i.\n",today.year,today.month,today.day);
return 0;
}
• 和本地变量一样,在函数内部声明的结构类型只能在函数内部使用
• 所以通常在函数外部声明结构类型,这样就可以被多个函数所使用了
11.2.1 申明结构的形式
struct Point{
int x;
int y;
};
struct Point p1,p2;
p1和p2都是point里面有x和y的值。
struct {
int x;
int y;
}p1,p2;
p1 和 p2都是一种无名结构,里面有x和y。
struct Point{
int x;
int y;
}p1,p2;
p1和p2都是point里面有x和y的值t。
对于第一和第三种形式,都声明了结构point。但是第二种形式没有声明point,只是定义了两个变量。
11.2.2 结构的初始化
#include<stdio.h>
struct date{
int month;
int day;
int year;
};
int main(int argc, char const *argv[])
{
struct date today = {07,25,2024};
struct date thismoth = {.month = 7,.year = 2024};
printf("Today's date is %i-%i-%i.\n",today.year,today.month,today.day);
printf("This month is %i-%i-%i.\n",thismoth.year,thismoth.month,thismoth.day);
return 0;
}
11.2.3 结构成员
• 结构和数组有点像
• 数组用[]运算符和下标访问其成员
a[0] = 10;
• 结构用.运算符和名字访问其成员
today.day;
student.firstName;
p1.x;
p1.y;
11.2.4 结构运算
• 要访问整个结构,直接用结构变量的名字
• 对于整个结构,可以做赋值、取地址,也可以传递给函数参数
p1 = (struct point){5, 10};// 相当于p1.x = 5,p1.y = 10;
p1 = p2; // 相当于p1.x = p2.x; p1.y = p2.y;
数组无法做这两种运算!需要通过遍历赋值。
#include<stdio.h>
struct date{
int month;
int day;
int year;
};
int main(int argc, char const *argv[])
{
struct date today;
today = (struct date){07,25,2024};
struct date day;
day = today;
day.day = 26;
printf("Today's date is %i-%i-%i.\n",today.year,today.month,today.day);
printf("The day's date is %i-%i-%i.\n",day.year,day.month,day.day);
return 0;
}
11.2.5 结构指针
• 和数组不同,结构变量的名字并不是结构变量的地址,必须使用&运算符
struct date *pDate = &today;
#include<stdio.h>
struct date{
int month;
int day;
int year;
};
int main(int argc, char const *argv[])
{
struct date today;
today = (struct date){07,25,2024};
struct date day;
struct date *pDate = &today;
day = today;
day.day = 26;
printf("Today's date is %i-%i-%i.\n",today.year,today.month,today.day);
printf("The day's date is %i-%i-%i.\n",day.year,day.month,day.day);
printf("The address of today is %p\n",pDate);
return 0;
}
11.3 结构与函数
11.3.1 结构作为函数参数
int numberofDays(struct date d);
• 整个结构可以作为参数的值传入函数
• 这时候是在函数内新建一个结构变量,并复制调用者的结构的值
• 也可以返回一个结构
• 这与数组完全不同
#include<stdio.h>
#include<stdbool.h>
struct date{
int month;
int day;
int year;
};
bool isLeap(struct date d);
int numberofDays(struct date d);
int main(int argc,char const *argv[])
{
struct date today,tomorrow;
printf("Enter today's date(mm dd yyyy):");
scanf("%i %i %i",&today.month,&today.day,&today.year);
if(today.day != numberOfDays(today))
{
tomorrow.day = today.day +1;
tomorrow.month = today.month;
tomorrow.year = today.year;
}
else if(today.month == 12)
{
tomorrow.day = 1;
tomorrow.month = 1;
tomorrow.year = today.year +1;
}
else
{
tomorrow.day = 1;
tomorrow.month = today.month + 1;
tomorrow.year = today.year;
}
printf("Tomorrow's date is %i-%i-%i.\n",tomorrow.year,tomorrow.month,tomorrow.day);
return 0;
}
int numberOfDays(struct date d)
{
int days;
const int daysPerMonth[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
if(d.month == 2 && isLeap(d))
days = 29;
else
days = daysPerMonth[d.month-1];
return days;
}
bool isLeap(struct date d)
{
bool leap = false;
if((d.year %4 == 0 && d.year %100 != 0)||d.year%400 == 0)
leap = true;
return leap;
}
11.3.2 输入结构
• 没有直接的方式可以一次scanf一个结构
• 如果我们打算写一个函数来读入结构
• 但是读入的结构如何送回来呢?
• 记住C在函数调用时是传值的
• 所以函数中的p与main中的y是不同的
• 在函数读入了p的数值之后,没有任何东西回到main,所以y还是 {0, 0}
#include<stdio.h>
struct point{
int x;
int y;
};
void getStruct(struct point);
void output(struct point);
void main()
{
struct point y = {0,0};
getStruct(y);
output(y);
}
void getStruct(struct point p)
{
scanf("%d", &p.x);
scanf("%d", &p.y);
printf("%d, %d\n", p.x, p.y);
}
void output(struct point p)
{
printf("%d, %d\n", p.x, p.y);
}
解决方案:
• 之前的方案,把一个结构传入了函数,然后在函数中操作,但是没有返回回去
• 问题在于传入函数的是外面那个结构的克隆体,而不是指针
• 传入结构和传入数组是不同的
• 在这个输入函数中,完全可以创建一个临时的结构变量,然后把这个结构返回给调用者
void main( )
{
struct point y = {0, 0};
y = inputPoint( );
output(y);
}
struct point inputPoint( )
{
struct point temp;
scanf(“%d”, &temp.x);
scanf(“%d”, &temp.y);
return temp;
}
也可以把y的地址传给函数,函数的参数类型是指向一个结构的指针。
不过那样的话,访问结构的成员的方式需要做出调整。
#include<stdio.h>
struct point{
int x;
int y;
};
struct point getStruct(void);
void output(struct point);
int main(int argc, char const *argv[])
{
struct point y = {0,0};
y = getStruct();
output(y);
}
struct point getStruct(void)
{
struct point p;
scanf("%d", &p.x);
scanf("%d", &p.y);
printf("%d, %d\n", p.x, p.y);
return p;
}
void output(struct point p)
{
printf("%d, %d\n", p.x, p.y);
}
11.3.3 结构指针作为参数
11.3.3.1 指向结构的指针
struct date{
int month;
int day;
int year;
}myday;
struct date *p = &myday;
(*p).month = 12;
p->month = 12;
用->表示指针所指的结构变量中的成员。
11.3.3.2 结构指针参数
void main( )
{
struct point y = {0, 0};
inputPoint(&y);
output(y);
}
struct point* inputPoint(struct point *p)
{
scanf(“%d”, &(p->x));
scanf(“%d”, &(p->y);
return p;
}
• 好处是传入传出只是一个指针的大小
• 如果需要保护传入的结构不被函数修改
const struct point *p;
• 返回传入的指针是一种套路
#include<stdio.h>
struct point{
int x;
int y;
};
struct point* getStruct(struct point*);
void output(struct point);
void print(const struct point *p);
int main(int argc, char const *argv[])
{
struct point y = {0,0};
getStruct(&y);
output(y);
output(*getStruct(&y));
print(getStruct(&y));
getStruct(&y)->x = 0;
*getStruct(&y) = (struct point){1,2};
}
struct point* getStruct(struct point *p)
{
scanf("%d", &p->x);
scanf("%d", &p->y);
printf("%d, %d\n", p->x, p->y);
return p;
}
void output(struct point p)
{
printf("%d, %d\n", p.x, p.y);
}
void print(const struct point *p)
{
printf("%d,%d\n",p->x,p->y);
}
11.3.4 结构中的结构
11.3.4.1 结构数组
struct date dates[100];
struct date dates[] = {
{4,5,2005},{2,4,2005}};
#include<stdio.h>
struct time{
int hour;
int minutes;
int seconds;
};
struct time timeUpdate(struct time now);
int main(void)
{
struct time testTimes[5]={
{11,59,59},{12,0,0},{1,29,59},{23,59,59},{19,12,27}
};
int i;
for(i=0;i<5;++i)
{
printf("Time is %.2i:%.2i:%.2i\n",
testTimes[i].hour,testTimes[i].minutes,testTimes[i].seconds);
testTimes[i] = timeUpdate(testTimes[i]);
printf("...one second later it's %.2i:%.2i:%.2i\n",
testTimes[i].hour,testTimes[i].minutes,testTimes[i].seconds);
}
return 0;
}
struct time timeUpdate(struct time now)
{
++now.seconds;
if(now.seconds == 60)
{
now.seconds = 0;
++now.minutes;
if(now.minutes == 60)
{
now.minutes = 0;
++now.hour;
if(now.hour == 24)
{
now.hour = 0;
}
}
}
}
11.3.4.2 结构中的结构
struct dateAndTime {
struct date sdate;
struct time stime;
};
11.3.4.3 嵌套的结构
struct point {
int x;
int y;
};
struct rectangle {
struct point pt1;
struct point pt2;
};
如果有变量
struct rectangle r;
就可以有:
r.pt1.x、r.pt1.y,
r.pt2.x 和 r.pt2.y
如果有变量定义:
struct rectangle r, *rp;
rp = &r;
那么下面的四种形式是等价的:
r.pt1.x;
rp->pt1.x;
(r.pt1).x;
(rp->pt1).x;
但是没有rp->pt1->x (因为pt1不是指针)。
11.3.4.4 结构中的结构的数组
#include <stdio.h>
struct point{
int x;
int y;
};
struct rectangle {
struct point p1;
struct point p2;
};
void printRect(struct rectangle r)
{
printf("<%d, %d> to <%d, %d>\n", r.p1.x, r.p1.y, r.p2.x, r.p2.y);
}
int main(int argc, char const *argv[])
{
int i;
struct rectangle rects[ ] = {{{1, 2}, {3, 4}},
{{5, 6}, {7, 8}}}; // 2 rectangles
for(i=0;i<2;i++) printRect(rects[i]);
}
11.4 联合
11.4.1 自定义数据类型(typedef)
C语言提供了一个叫做 typedef 的功能来声明一个已有的数据类型的新名字。比如:
typedef int Length;
使得 Length 成为 int 类型的别名。
这样, Length 这个名字就可以代替int出现在变量定义和参数声明的地方了:
Length a, b, len ;
Length numbers[10] ;
声明新的类型的名字
新的名字是某种类型的别名
改善了程序的可读性
typedef int Length; // Length就等价于int类型
typedef *char[10] Strings; // Strings 是10个字符串的数组的类型
typedef struct node {
int data;
struct node *next;
} aNode;
//或
typedef struct node aNode;// 这样用aNode 就可以代替struct node
11.4.2 联合(union)
• 存储
• 所有的成员共享一个空间
• 同一时间只有一个成员是有效的
• union的大小是其最大的成员
• 初始化
• 对第一个成员做初始化
#include<stdio.h>
typedef union{
int i;
char ch[sizeof(int)];
}CHI;
int main(int argc,char const *argv[])
{
CHI chi;
int i;
chi.i = 1234;
for(i=0;i<sizeof(int);i++)
{
printf("%02hhX",chi.ch[i]);
}
printf("\n");
return 0;
}
12 程序结构
12.1 全局变量
12.1.1 全局变量
• 定义在函数外面的变量是全局变量
• 全局变量具有全局的生存期和作用域
• 它们与任何函数都无关
• 在任何函数内部都可以使用它们
#include<stdio.h>
int f(void);
int gAll = 12;
int main(int argc,char const *argv[])
{
printf("in %s gAll = %d\n",__func__,gAll);
f();
printf("in %s gAll = %d\n",__func__,gAll);
return 0;
}
int f(void)
{
printf("in %s gAll = %d\n",__func__,gAll);
gAll += 2;
printf("agn in %s gAll = %d\n",__func__,gAll);
return gAll;
}
12.1.2 全局变量初始化
• 没有做初始化的全局变量会得到0值
• 指针会得到NULL值
• 只能用编译时刻已知的值来初始化全局变量
• 它们的初始化发生在main函数之前
#include<stdio.h>
int f(void);
int gAll;
int main(int argc,char const *argv[])
{
printf("in %s gAll = %d\n",__func__,gAll);
f();
printf("in %s gAll = %d\n",__func__,gAll);
return 0;
}
int f(void)
{
printf("in %s gAll = %d\n",__func__,gAll);
gAll += 2;
printf("agn in %s gAll = %d\n",__func__,gAll);
return gAll;
}
12.1.3 被隐藏的全局变量
如果函数内部存在与全局变量同名的变量,则全局变量被隐藏。
12.2 静态本地变量
• 在本地变量定义时加上static修饰符就成为静态本地变量
• 当函数离开的时候,静态本地变量会继续存在并保持其值
• 静态本地变量的初始化只会在第一次进入这个函数时做,以后进入函数时会保持上次离开时的值
• 静态本地变量实际上是特殊的全局变量
• 它们位于相同的内存区域
• 静态本地变量具有全局的生存期,函数内的局部作用域
• static在这里的意思是局部作用域(本地可访问)
#include<stdio.h>
int f(void);
int gAll=12;
int main(int argc,char const *argv[])
{
f();
f();
f();
return 0;
}
int f(void)
{
int all = 1;
printf("in %s all = %d\n",__func__,all);
all += 2;
printf("agn in %s all = %d\n",__func__,all);
return all;
}
#include<stdio.h>
int f(void);
int gAll=12;
int main(int argc,char const *argv[])
{
f();
f();
f();
return 0;
}
int f(void)
{
static int all = 1;
printf("in %s all = %d\n",__func__,all);
all += 2;
printf("agn in %s all = %d\n",__func__,all);
return all;
}
#include<stdio.h>
int f(void);
int gAll=12;
int main(int argc,char const *argv[])
{
f();
return 0;
}
int f(void)
{
int k = 0;
static int all = 1;
printf("&gAll = %p\n",&gAll);
printf("&all = %p\n",&all);
printf("&k = %p\n",&k);
printf("in %s all = %d\n",__func__,all);
all += 2;
printf("agn in %s all = %d\n",__func__,all);
return all;
}
12.3 *返回指针的函数
• 返回本地变量的地址是危险的
• 返回全局变量或静态本地变量的地址是安全的
• 返回在函数内malloc的内存是安全的,但是容易造成问题
• 最好的做法是返回传入的指针
//返回本地变量的地址是危险的,因为程序一旦离开这个函数,本地变量的地址就被销毁了
#include<stdio.h>
int* f(void);
void g(void);
int main(int argc,char const *argv[])
{
int *p = f();
printf("*p = %d\n",*p);
g();
printf("*p=%d\n",*p);
return 0;
}
int* f(void)
{
int i =12;
printf("i的地址是%p\n",&i);
return &i;
}
void g(void)
{
int k = 24;
printf("k=%d\n",k);
printf("k的地址是%p\n",&k);
}
i的地址使用完后被销毁后又重新将该地址分配给k。
12.4 tips
• 不要使用全局变量来在函数间传递参数和结果
• 尽量避免使用全局变量
• 丰田汽车的案子:*使用全局变量和静态本地变量的函数是线程不安全的
12.5 编译预处理和宏
12.5.1 编译预处理指令
• #开头的是编译预处理指令
• 它们不是C语言的成分,但是C语言程序离不开它们
• #define用来定义一个宏
12.5.2 #define
#define <名字> <值>
• 注意没有结尾的分号,因为不是C的语句
• 名字必须是一个单词,值可以是各种东西
• 在C语言的编译器开始编译之前,编译预处理程序(cpp)会把程序中的名字换成值
• 完全的文本替换
gcc —save-temps
12.5.3 宏
• 如果一个宏的值中有其他的宏的名字,也是会被替换的
• 如果一个宏的值超过一行,最后一行之前的行末需要加“\”
• 宏的值后面出现的注释不会被当作宏的值的一部分
#include<stdio.h>
#define PI 3.14159
#define FORMAT "%f\n"
#define PI2 2*PI //pi*2
#define PRT printf("%f ",PI);\
printf("%f\n",PI2)
//const double PI = 3.14159;
int main(int argc,char const *argv[])
{
// printf("%f\n",2*3.14159*3.0);
printf("%f\n",2*PI*3.0);
printf(FORMAT,2*PI*3.0);
printf(FORMAT,PI2*3.0);
PRT;
return 0;
}
12.5.4 没有值的宏
#define _DEBUG
这类宏是用于条件编译的,后面有其他的编译预处理指令来检查这个宏是否已经被定义过了。
12.5.5 预定义的宏
• __LINE__
• __FILE__
• __DATE__
• __TIME__
• __STDC__
#include<stdio.h>
int main(int argc,char const *argv[])
{
printf("%s:%d\n",__FILE__,__LINE__); //文件名路径:第几行
printf("%s,%s\n",__DATE__,__TIME__); //日期,时间
return 0;
}
12.5.6 带参数的宏
12.5.6.1 像函数的宏
#define cube(x) ((x)*(x)*(x))
• 宏可以带参数
#include<stdio.h>
#define cube(x) ((x)*(x)*(x))
int main(int argc,char const *argv[])
{
printf("%d\n",cube(5)); //等于5*5*5
int i;
printf("请输入一个整数:");
scanf("%d",&i);
printf("%d\n",cube(i));
printf("%d\n",cube(i+2));
return 0;
}
12.5.6.2 错误定义的宏
#define RADTODEG(x) (x * 57.29578)
#define RADTODEG(x) (x) * 57.29578
#include<stdio.h>
#define RADTODEG1(x) (x * 57.29578) //将弧度转换为角度的宏
#define RADTODEG2(x) (x) * 57.29578
int main(int argc,char const *argv[])
{
printf("%f\n",RADTODEG1(5+2)); //该式被替换为(5+2* 57.29578)
printf("%f\n",180/RADTODEG2(1)); //该式被替换为180/(1)*57.29578
return 0;
}
运行结果与我们期待的不一致。
12.5.6.3 带参数的宏的原则
• 一切都要括号
• 整个值要括号
• 参数出现的每个地方都要括号
#define RADTODEG(x) ((x) * 57.29578)
12.5.6.4 可以带多个参数
可以带多个参数
#define MIN(a,b) ((a)>(b)?(b):(a))
也可以组合(嵌套)使用其他宏。
12.5.6.5 分号
注意定义宏的结尾不加分号。
12.5.6.6 带参数宏的使用
• 在大型程序的代码中使用非常普遍
• 可以非常复杂,如“产生”函数
• 在#和##这两个运算符的帮助下
• 存在中西方文化差异
• 部分宏会被inline函数替代
12.5.7 其他编译预处理指令
• 条件编译
• error
• …
12.5.8 案例
#define TOUPPER(c) ('a'<=(c)&&(c)<='z'?(c)-'a'+'A':(c))
设s是一个足够大的字符数组,i是int型变量,则以下代码输出的是:
strcpy(s,"abcd");
i = 0;
putchar (TOUPPER(s[++i]));
12.6 大程序结构
12.6.1 多个.c文件
• main()里的代码太长了适合分成几个函数
• 一个源代码文件太长了适合分成几个文件
• 两个独立的源代码文件不能编译形成可执行的程序
12.6.2 头文件
12.6.2.1 头文件
把函数原型放到一个头文件(以.h结尾)中,在需要调用这个函数的源代码文件(.c文件)中#include这个头文件,就能让编译器在编译的时候知道函数的原型。
12.6.2.2 #include
• #include是一个编译预处理指令,和宏一样,在编译之前就处理了
• 它把那个文件的全部⽂文本内容原封不动地插入到它所在的地方
• 所以也不是一定要在.c文件的最前面#include
12.6.2.3 “”还是<>
• #include有两种形式来指出要插入的文件
• “”要求编译器首先在当前目录(.c文件所在的目录)寻找这个文件,如果没有,到编译器指定的目录去找
• <>让编译器只在指定的目录去找
• 编译器自己知道自己的标准库的头文件在哪里
• 环境变量和编译器命令行参数也可以指定寻找头文件的目录
12.6.3 #include的误区
• #include不是用来引入库的
• stdio.h里只有printf的原型,printf的代码在另外的地方,某个.lib(Windows)或.a(Unix)中
• 现在的C语言编译器默认会引入所有的标准库
• #include <stdio.h>只是为了让编译器知道printf函数的原型,保证你调用时给出的参数值是正确的类型
12.6.4 不对外公开的函数
• 在函数前面加上static就使得它成为只能在所在的编译单元中被使用的函数。
• 在全局变量前面加上static就使得它成为只能在所在的编译单元中被使用的全局变量。
12.7 声明
12.7.1 变量的声明
int i; //变量的定义
extern int i; //变量的声明
12.7.2 声明和定义
• 声明是不产生代码的东西
• 函数原型
• 变量声明
• 结构声明
• 宏声明
• 枚举声明
• 类型声明
• inline函数
• 定义是产生代码的东西
12.7.3 头文件
• 只有声明可以被放在头文件中
• 是规则不是法律
• 否则会造成一个项目中多个编译单元里有重名的实体
• *某些编译器允许几个编译单元中存在同名的函数,或者用weak修饰符来强调这种存在
12.7.4 重复声明
• 同一个编译单元里,同名的结构不能被重复声明
• 如果你的头文件里有结构的声明,很难这个头文件不会在一个编译单元里被#include多次
• 所以需要“标准头文件结构”
12.7.5 标准头文件结构
#ifndef __LIST_HEAD__
#define __LIST_HEAD__
#include "node.h"
typedef struct _list{
Node* head;
Node* tail;
}List;
#endif
• 运用条件编译和宏,保证这个头文件在一个编译单元中只会被#include一次。
• #pragma once也能起到相同的作用,但是不是所有的编译器都支持。
12.7.6 *前向声明
#ifndef __LIST_HEAD__
#define __LIST_HEAD__
struct Node;
typedef struct _list{
struct Node* head;
struct Node* tail;
}List;
#endif
因为在这个地方不需要具体知道Node是怎样的,所以可以用struct Node来告诉编译器Node是一个结构。
13 文件
13.1 文件
13.1.1 格式化输入输出
• printf
• %[flags][width][.prec][hlL]type
• scanf
• %[flag]type
13.1.1.1 %[flags][width][.prec][hlL]type
Flag | 含义 |
- | 左对齐 |
+ | 在前面放+或- |
(space) | 正数留空 |
0 | 0填充 |
#include<stdio.h>
int main(int argc,char const *argv[])
{
printf("%9d\n",123);
printf("%+9d\n",123);
printf("%-9d\n",123);
printf("%-+9d\n",123);
printf("%+-9d\n",123);
printf("%09d\n",123);
return 0;
}
width或prec | 含义 |
number | 最小字符数 |
* | 下一个参数是字符数 |
.number | 小数点后的位数 |
.* | 下一个参数是小数点后的位数 |
#include<stdio.h>
int main(int argc,char const *argv[])
{
int len = 6;
printf("%*d\n",len,123); //*用来代替字符串长度数,会将6传递给*
printf("%9.2f\n",123.0);
return 0;
}
类型修饰 | 含义 |
hh | 单个字节 |
h | short |
l | long |
ll | long long |
L | long double |
#include<stdio.h>
int main(int argc,char const *argv[])
{
int len = 6;
printf("%hhd\n",(char)12345);
printf("%9.2f\n",123.0);
return 0;
}
type | 用于 | type | 用于 |
i或d | int | g | float |
u | unsigned int | G | float |
o | 八进制 | a或A | 十六进制浮点 |
x | 十六进制 | c | char |
X | 字母大写的十六进制 | s | 字符串 |
f或F | float,6 | P | 指针 |
e或E | 指数 | n | 读入/写出的个数 |
#include<stdio.h>
int main(int argc,char const *argv[])
{
int num;
printf("%d%n\n",12345,&num); // %n出表示在此处获得printf已经输出了多少个字符并赋值给num。
printf("%d\n",num);
// printf("%hhd%n\n",(char)12345,&num);
// printf("%dty%n\n",(char)12345,&num);
return 0;
}
13.1.1.2 scanf:%[flag]type
flag | 含义 | falg | 含义 |
* | 跳过 | l | long,double |
数字 | 最大字符数 | ll | long long |
hh | char | L | long double |
h | short |
#include<stdio.h>
int main(int argc,char const *argv[])
{
int num;
scanf("%*d%d",&num);
printf("%d\n",num);
return 0;
}
#include<stdio.h>
int main(int argc,char const *argv[])
{
int num;
scanf("%i",&num);
printf("%d\n",num);
return 0;
}
13.1.1.3 [^.]
$GPRMC,004319.00,A,3016.98468,N,12006.39211,E,0.047,,130909,,,D*79 //GPS模块所产生的1083协议数据
scanf("%*[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]",
sTime,sAV,sLati,&sNW,sLong,&sEW,sSpeed,sAngle,sDate);
13.1.1.4 printf和scanf的返回值
• 读入的项目数
• 输出的字符数
• 在要求严格的程序中,应该判断每次调用scanf或printf的返回值,从而了解程序运行中是否存在问题.
#include<stdio.h>
int main(int argc,char const *argv[])
{
int num;
int i1 = scanf("%i",&num);
int i2 = printf("%d\n",num);
printf("%d:%d\n",i1,i2);
return 0;
}
13.1.2 文件输入输出
用>和<做重定向。
13.1.2.1 输入结束
• getchar读到了EOF
• scanf返回小于要求读的数量
13.1.2.2 FILE
FILE* fopen(const char * restrict path, const char *restrict mode);
int fclose(FILE *stream);
fscanf(FILE*, ...);
fprintf(FILE*, ...);
13.1.2.3 打开文件的标准代码
FILE* fp = fopen(“file”,“r”);
if ( fp ) {
fscanf(fp,...);
fclose(fp);
} else {
...
}
#include<stdio.h>
int main(int argc,char const *argv[])
{
FILE *fp = fopen("12.in","r");
if(fp){
int num;
fscanf(fp,"%d",&num);
printf("%d\n",num);
fclose(fp);
}
else{
printf("无法打开文件\n");
}
return 0;
}
13.1.2.4 fopen
r | 打开只读 |
r+ | 打开读写,从文件头开始 |
w | 打开只写。如果不存在则新建,如果存在则清空 |
w+ | 打开读写。如果不存在则新建,如果存在则清空 |
a | 打开追加。如果不存在则新建,如果存在则从文件尾开始 |
..x | 只新建,如果文件已经存在则不能打开 |
13.1.3 二进制文件
• 其实所有的文件最终都是二进制的
• 文本文件无非是用最简单的方式可以读写的文件
more、tail
cat
vi
• 而二进制文件是需要专门的程序来读写的文件
• 文本文件的输入输出是格式化,可能经过转码
13.1.3.1 文本 vs 二进制
• Unix喜欢用文本文件来做数据存储和程序配置
• 交互式终端的出现使得人们喜欢用文本和计算机“talk”
• Unix的shell提供了一些读写文本的小程序
• Windows喜欢用二进制文件
• DOS是草根文化,并不继承和熟悉Unix文化
• PC刚开始的时候能力有限,DOS的能力更有限,二进制更接近底层
• 文本的优势是方便人类读写,而且跨平台
• 文本的缺点是程序输入输出要经过格式化,开销大
• 二进制的缺点是人类读写困难,而且不跨平台
• int的大小不一致,大小端的问题...
• 二进制的优点是程序读写快
13.1.3.2 程序为什么要文件
• 配置
• Unix用文本,Windows用注册表
• 数据
• 稍微有点量的数据都放数据库了
• 媒体
• 这个只能是二进制的
• 现实是,程序通过第三方库来读写文件,很少直接读写二进制文件了
13.1.3.3 二进制读写
size_t fread(void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);
size_t fwrite(const void *restrict ptr, size_t size,size_t nitems, FILE *restrict stream);
• 注意FILE指针是最后一个参数.
• 返回的是成功读写的字节数。
13.1.3.4 为什么nitem
• 因为二进制文件的读写一般都是通过对一个结构变量的操作来进行的。
• 于是nitem就是⽤用来说明这次读写⼏几个结构变量!
13.1.3.5 在文件中定位
long ftell(FILE *stream);
int fseek(FILE *stream, long offset, int whence);
• SEEK_SET:从头开始
• SEEK_CUR:从当前位置开始
• SEEK_END:从尾开始(倒过来)
13.1.3.6 可移植性
• 这样的二进制文件不具有可移植性
• 在int为32位的机器上写成的数据文件无法直接在int为64位的机器上正确读出
• 解决方案之一是放弃使用int,⽽而是typedef具有明确大小的类型
• 更好的方案是用文本
13.2 位运算
13.2.1 按位运算
•C有这些按位运算的运算符:
•& 按位的与
•| 按位的或
•~ 按位取反
•^ 按位的异或
•<< 左移
•>> 右移
13.2.1.1 按位与 &
•如果 (x)i == 1 并且(y)i == 1,那么 (x & y)i =1
•否则的话 (x & y)i = 0!
•按位与常用于两种应用:
•让某一位或某些位为0:x & 0xFE
•取一个数中的一段:x & 0xFF
13.2.1.2 按位或 |
•如果 (x)i == 1 或 (y)i == 1,那么 (x | y)i = 1
•否则的话, (x | y)i == 0
•按位或常用于两种应用:
•使得一位或几个位为1:x | 0x01
•把两个数拼起来:0x00FF | 0xFF00
13.2.1.3 按位取反 ~
• (~x)i = 1 - (x)i
•把1位变0,0位变1
•想得到全部位为1的数:~0
•7的二进制是0111,x | 7使得低3位为1,而
•x & ~7,就使得低3位为0
#include<stdio.h>
int main(int argc,char const *argv[])
{
unsigned char c = 0xAA;
printf(" c=%hhx\n",c);
printf("~c=%hhx\n",(char)~c);
printf("-c=%hhx\n",(char)-c);
return 0;
}
13.2.1.4 逻辑运算vs按位运算
• 对于逻辑运算,它只看到两个值:0和1
• 可以认为逻辑运算相当于把所有非0值都变成1,然后做按位运算
• 5 & 4 —>4而 5 && 4 —> 1 & 1 —> 1
• 5 | 4 —> 5而 5 || 4 —> 1 | 1 —> 1
• ~4 —> 3而 !4 —> !1 —> 0
13.2.1.5 按位异或^
•如果(x)i == (y)i ,那么(x ^ y)i = 0
•否则的话,(x ^ y)i == 1
•如果两个位相等,那么结果为0;不相等,结果为1
•如果x和y相等,那么x ^ y的结果为0
•对一个变量用同一个值异或两次,等于什么也没做
•x ^ y ^ y —> x
13.2.2 移位运算
• 移位的位数不要用负数,这是没有定义的行为
x << -2 //!!NO!!
13.2.2.1 左移<<
•i << j
•i中所有的位向左移动j个位置,而右边填入0
•所有小于int的类型,移位以int的方式来做,结果是int
•x <<= 1 等价于 x *= 2
•x <<= n 等价于 x *= 2n。
#include<stdio.h>
int main(int argc,char const *argv[])
{
unsigned char c = 0xA5;
printf(" c=%d\n",c);
printf("c<<2=%d\n",c<<2);
return 0;
}
13.2.2.2 右移>>
•i >> j
•i中所有的位向右移j位
•所有小于int的类型,移位以int的方式来做,结果是int
•对于unsigned的类型,左边填入0
•对于signed的类型,左边填入原来的最高位(保持符号不变)
•x >>= 1 等价于 x /= 2
•x >>= n 等价于 x /= 2n.
#include<stdio.h>
int main(int argc,char const *argv[])
{
int a = 0x80000000;
unsigned int b = 0x80000000;
printf("a=%d\n",a);
printf("b=%u\n",a);
printf("a>>1=%d\n",a>>1);
printf("b>>1=%u\n",a>>1);
return 0;
}
13.2.3 输出一个数的二进制
#include<stdio.h>
int main(int argc,char const *argv[])
{
int number;
scanf("%d",&number);
unsigned mask = 1u<<31; //左移31bit
for(;mask;mask>>=1)
{
printf("%d",number&mask?1:0);
}
printf("\n");
return 0;
}
13.2.4 位段
•把一个int的若干位组合成一个结构
struct {
unsigned int leading : 3;
unsigned int FLAG1: 1;
unsigned int FLAG2: 1;
int trailing: 11;
};
• 可以直接用位段的成员名称来访问
• 比移位、与、或还方便
• 编译器会安排其中的位的排列,不具有可移植性
• 当所需的位超过一个int时会采用多个int
#include<stdio.h>
void prtBin(unsigned int number);
struct U0{
unsigned int leading: 3;
unsigned int FLAG1: 1;
unsigned int FLAG2: 1;
int trailing: 27;
};
int main(int argc,char const *argv[])
{
struct U0 uu;
uu.leading = 2;
uu.FLAG1 = 10;
uu.FLAG2 = 1;
// uu.trailing = 0;
printf("sizeof(uu)=%lu\n",sizeof(uu));
prtBin(*(int*)&uu);
return 0;
}
void prtBin(unsigned int number)
{
unsigned mask = 1u<<31;
for(;mask ;mask>>=1)
{
printf("%d",number & mask ? 1:0);
}
printf("\n");
}
14 链表
本章参考书籍:《C Primer Plus》
推荐课程: