第一章 C语言介绍
目录
1.1 基本的程序框架
#include <stdio.h>
int main(){
//我们所写的内容
return 0;
}
1.2 printf
1.2.1 输出
printf("Hello World!\n");
- printf 是一个函数
- " " 里面的内容叫做字符串
- \n 表示换行
1.2.2 计算
- 示例
printf ("%d\n", 1+1);
- 其他四则运算
C符号 意义 + 加
- 减 * 乘 / 除 % 取余 () 括号
1.3 scanf
int a = 0;
int b = 0;
scanf ("%d %d", &a, &b);
- scanf 是一个函数
- "%d" 表示要读入整数
- 注意后面的开头必须写 &
1.4 变量
1.4.1 变量定义的格式
<变量的数据类型> <变量的名称>
1.4.2 变量的数据类型
- 详见第四章
- int 整型
scanf: %d
printf: %d
整数和整数的运算只会得到整数,若有小数部分会直接被舍弃。但当浮点数和整数放到一起运算时,C会把整数转换为浮点数,然后进行浮点数运算int a = 10; printf("%f", a/2.0*0.333);
- float 单精度浮点数型
scanf: %f
printf: %f - double 双精度浮点数型
scanf: %lf
printf: %f
1.4.3 变量的名称
- 变量的名称是一种标识符。标识符有其构成的规则,基本原则是:只能由字母、数字和下划线组成。数字不可以出现在第一个位置上,C语言的关键字不能作为标识符。1.4.2 变量的赋值
- a=b:将右边的值交给左边的变量
(注:不是a和b相等的意思) - 初始化:
1. 当赋值发生在定义变量的时候,就是变量的初始化
2. <类型名称> <变量名称> = <初始值>;
3. 组合变量定义时,也可在这个定义中单独给单个变量赋值,如:int price = 0, amount = 100;
1.4.4 常量
- 定义常量
const int a = 100;
const是一个修饰符,用来给变量加上一个不变的属性。表示这个变量一旦被初始化,就不可再修改。
1.5 表达式
1.5.1 定义
⼀个表达式是⼀系列运算符和算子的组合,用来计算⼀个值。
- 运算符:进行运算的动作,比如加法运算符“+”,减法运算符 “-”
- 算子:是指参与运算的值,这个值可能是常数,也可能是变量,还可能是⼀个方法的返回值
1.5.2 运算符
- 运算符的优先级
单目运算符:只有⼀个算子的运算符。单目运算的+和-表示取整数或取负数 - 复合赋值
• 5个算术运算符“ + ”、“ - ”、“ * ”、“ / ”、“ % ”(加、减、乘、除、取余),可以和“ = ”结合起来,形成复合赋值运算符:“ += ”、“ -= ”、“ *= ”、“ /= ”、“ %= ”
• total += 5;
• total = total + 5;
• 注意两个运算符中间不要有空格 - 递增递减运算符
“ ++ ”和“ -- ”是单目运算符,算子必须是变量,作用是给这个变量+1或者-1
• count++;
• count += 1;
• count = count + 1;
前缀形式:++和--放在变量的前面
后缀形式:++和--放在变量的后面
区别:
示例:int a = 0; //测试a++的情况 a=10; printf ("a++ = %d", a++); printf ("a = %d", a); //此时a++为10,a为11 //测试++a的情况 a = 10; printf ("++a = %d", ++a); printf ("a = %d", a); //此时++a为11,a为11
1.6 注释
单行注释:// 需要注释的内容
多行注释:/* 需要注释的内容 */
1.7 代码tips
- 单一出口原则
第二章 判断语句
2.1 判断
2.1.1 if语句
- 普通形式
if ( /*通过关系运算判定条件是否成立*/ ){ //执行内容 该语句前缩进一个tab的位置 }
- 与else
if ( /*通过关系运算判定条件是否成立*/ ){ //执行内容 } else { //若不满足if内的条件 则执行此内容 }
- 去掉{ }
if (/*判断条件*/) /*执行内容*/ ; //该句在此分号结束 //else如下同理 if (/*判断条件*/) /*执行内容*/ ; else /*执行内容*/ ;
2.1.2 关系运算符
- 关系运算:计算两个值之间的关系
- 关系运算符
关系运算符 意义 == 相等 != 不相等 > 大于 < 小于 <= 小于或等于 >= 大于或等于 - 当两个值的关系符合关系运算符的预期时, 关系运算的结果为整数1,否则为整数0
printf("%d\n", 5>3); //显示结果为1 printf("%d\n", 5<3); //显示结果为0
- 所有关系运算符的优先级低于算术运算符,但是高于赋值运算符;其中,==和!=的优先级低于其他关系运算符
//算术运算符 7 >= 3+4 //计算右侧3+4的结果 再与左侧7作比较 //赋值运算符 int r = a>0 //计算a>0的结果(1或0) 将值赋给r
2.2 分支
2.2.1 if嵌套
- 当if的条件满足或者不满足时,要执行的语句也是一条if或if-else语句,称为嵌套
- 其中,else一般和最近的if匹配,见写法2(除非前面的if语句有大括号,见写法3)
- 缩进格式不能暗示else的匹配(见写法4)
- 建议:即使只有一条语句,if和else后面也要用大括号;大括号内的语句缩进一个tab的位置
//写法1
if (code == READY){
if (count < 20){
printf("OK\n");
}else{
printf("NO\n");
}
}
//写法2 (else和最近的if匹配)
if (code == READY)
if (count < 20)
printf("OK\n");
else //此时这个else与上一个if匹配
printf("NO\n");
//写法3 (else与大括号)
if (code == READY)
if (count < 20){
printf("OK\n");
}else //此时这个else与最外面的if匹配
printf("NO\n");
//写法4(else缩进)
if (code == READY)
if (count < 20)
printf("OK\n");
else //此时这个else依然与上一个if匹配,不会因为缩进就和最外面的if匹配
printf("NO\n");
2.2.2 级联的if-else if语句
- 形式
if (/*exp1*/){
/*st1*/ ;
}else if (/*exp2*/){
/*st2*/ ;
}else{
/*st3*/ ;
}
2.2.3 switch-case多路分支
switch (/*控制表达式*/){
case /*常量*/:
/*语句*/;
break; //根据情况考虑是否加入break,一般都要加
……
case /*常量*/:
/*语句*/;
break;
default:
/*语句*/;
break;
……
}
//示例
int type;
scanf ("%d", &type);
switch ( type ) {
case 1:
printf("你好");
break;
case 2:
printf("早上好");
break;
case 3:
printf("晚上好");
break;
case 4:
printf("再⻅");
break;
default:
printf("啊,什么啊?");
break;
}
- 控制表达式只能是整数型的结果
- 常量可以是常数,也可以是常数计算的表达式
- 根据表达式的结果,寻找匹配的case,并执行case后面的语句,一直到break为止
- 如果所有的case都不匹配,那么就执行default后面的语句
如果没有default,那么就什么都不做 - switch语句可以看作是⼀种基于计算的跳转,计算控制表达式的值后,程序会跳转到相匹配的case(分支标号)处。 分支标号只是说明switch内部位置的路标,在执行完分支中的最后一条语句后, 如果后面没有break,就会顺序执行到下面case中的内容去(即使控制表达式与下一个case的常量不匹配),直至遇到break,否则会到switch结束为止
第三章 循环语句
3.1 三种循环
3.1.1 while循环
-
基本形式
while (/*循环条件*/){ /*循环体语句*/; }
-
如果把while翻译作“当”,那么一个while循环的意思就是:当条件满足时,不断地重复循环体内的语句
-
循环执行之前判断是否继续循环,所以有可能循环⼀次也没有被执行
-
条件成立是循环继续的条件
3.1.2 do-while循环
- 基本形式
do { /*循环体语句*/; } while (/*循环条件*/);
- 在进入循环的时候不做检查,而是在执行完⼀轮循环体的代码之后,再来检查循环的条件是否满足,如果满足则继续下⼀轮循环,不满足则结束循环
- 与while循环的区别
do-while循环是在循环体语句执行结束时才判断条件。也就是说,无论如何,循环体语句都会执行少一遍,然后再来判断条件
3.1.3 for循环
- 基本形式
for (/*初始动作*/; /*条件*/; /*每轮的动作*/) { //循环体语句 } //示例 for (i=0; i<100; i++){ printf("%d", i); } //读作:对于⼀开始的i=0,当i<100时,重复做循环体,每⼀轮循环在做完循环体内语句后,使得i++
- for中的每一个表达式都是可以省略的
//例 for (; /*条件*/; ) == while (/*条件*/)
- for循环像⼀个计数循环:设定一个计数器,初始化它,然后在计数器到达某值之前,重复执行循环体,而每执行一轮循环,计数器值以一定步进进行调整,比如加1或者减1
- 小技巧:
做求和的程序时,记录结果的变量应该初始化为0,而做求积的变量时,记录结果的变量应该初始化为1 - 循环次数:
在上例中,循环的次数是n,而循环结束以后,i的值也是nfor (i=0; i<n; i++)
循环的控制变量i从0开始或是从1开始,判断i<n或是i<=n,对循环的次数和循环结束后变量的值都有影响
3.1.4 三种循环的用法
- 如果有固定次数,用for循环
- 如果必须执行一次,用do-while循环
- 其他情况用while循环
3.2 循环控制
3.2.1 break和continue
- break:跳出循环
- continue:跳过本轮循环剩下的语句,进入下一轮
- 在循环嵌套中,break和continue只能对其所在的那一层循环做
- 接力break:
在循环嵌套中使用break时,如果想用break跳出所有循环,可以使用接力break的方法//接力break示例 int x; int one, two, five; int exit = 0; //用于判断接力break 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个五角得到%d元\n", one, two, five, x); exit = 1; //用于执行接力break break; //跳出最内层循环 } } if (exit == 1) break; //利用接力break跳出第二层循环 } if (exit == 1) break; //利用接力break跳出最外层循环 }
3.2.2 goto
格式:
goto /*自己设立的标号*/; //开始跳转
/*自己设立的标号*/: /*跳转的位置*/ //利用自己设立的标号跳转到想要的位置
//示例
goto out;
out:
可以在需要离开整个循环的位置放一个goto语句代替上述提及的接力break
//goto示例
int x;
int one, two, five;
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个五角得到%d元\n", one, two, five, x);
goto out; //goto语句
}
}
}
}
out: //直接从循环内跳到此处
return 0;
3.3 循环应用
3.3.1 猜数游戏
题目:让计算机来想⼀个数,然后让用户来猜,用户每输入一个数,就告诉它是大了还是小了,直到用户猜中为止,最后还要告诉用户它猜了多少次。
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(){
srand(time(0));
int number = rand()%100 + 1;
// 上述语句中,rand()表示生成随机整数;x%n的结果是[0, n-1]的⼀个整数
int count = 0;
int a = 0;
printf("我已经想好了一个1-100之间的数\n");
do{
printf("请猜这个1到100之间数:\n");
scanf("%d", &a);
count++;
if ( a > number ){
printf("你猜的数大了\n");
}else if ( a < number ){
printf("你猜的数小了\n");
}
}while (a != number);
printf("太好了,你用了%d次就猜到了答案\n", count);
return 0;
}
- rand()表示生成随机整数
- x%n的结果是[0, n-1]的一个整数
3.2.2 整数的分解
- 对⼀个整数做%10的操作,就得到它的个位数
- 对⼀个整数做/10的操作,就去掉了它的个位数
- 然后再对第二步的结果做%10,就得到原来数的十位数了
- 之后依此类推
3.2.3 整数逆序
要求:输入⼀个正整数,输出逆序的数(注意对输出0的要求)
情况一:当有0时,不保留0。例如700,输出7。
#include <stdio.h>
int main(){
int x;
scanf("%d", &x);
int digit; //表示每一位的数字
int ret = 0; //存放结果
while(x>0){
digit = x%10;
x /= 10;
ret = ret*10 + digit;
}
printf("%d", ret);
return 0;
}
情况二:当有0时,保留0。例如700,输出007。
#include <stdio.h>
int main(){
int x;
scanf("%d", &x);
int digit; //表示每一位的数字
while(x>0){
digit = x%10;
x /= 10;
printf("%d", digit);
}
return 0;
}
3.2.4 分数求和(1)
题目:
代码实现:
#include <stdio.h>
int main(){
int n;
int i;
double ret = 0.0;
scanf("%d", &n);
for (i=1; i<=n; i++){
ret += 1.0/i; //因为结果是浮点数,而i是整型,所以要写1.0来进行转换
}
printf("%f\n", ret);
return 0;
}
3.2.5 分数求和(2)
题目:
代码实现:
#include <stdio.h>
int main(){
int n;
int i;
double ret = 0.0;
int sign = 1;
scanf("%d", &n);
for (i=1; i<=n; i++){
ret += 1.0*sign/i; //因为结果是浮点数,而i是整型,所以要写1.0来进行转换
sign = -sign;
}
printf("%f\n", ret);
return 0;
}
3.2.6 正序分解整数
题目:输入⼀个非负整数,正序输出它的每一位数字
代码实现:
#include <stdio.h>
int main(){
int x;
scanf("%d", &x);
int mask = 1;
int t = x;
while(t>=10){
t /= 10;
mask *= 10;
}
do{
int d = x / mask;
printf("%d", d);
if (mask >= 10){
printf(" ");
}
x %= mask;
mask /= 10;
}while (mask>0);
printf("\n");
return 0;
}
3.2.7 求最大公约数
题目:输入两个数a和b,输出它们的最大公约数
- 辗转相除法
1. 如果b等于0,计算结束,a就是最大公约数
2. 否则,计算a除以b的余数,让a等于b,而b等于那个余数
3. 回到第一步
代码实现:
#include <stdio.h>
int main(){
int a,b;
int t;
scanf("%d %d", &a, &b);
while(b != 0){
t = a%b;
a = b;
b = t;
}
printf("%d\n",a);
return 0;
}
第四章 数据类型
4.1 基本类型介绍
4.1.1 基本类型
- 整数型
char、short、int、long、long long - 浮点数型
float、double、long double - 逻辑(可以放到整数型中)
bool - 指针
- 自定义类型
注:紫色表示C99的类型
4.1.2 不同类型之间的区别
- 类型名称:int、long、double
- 输入输出的格式:%d、%ld、%lf
- 表达的数的范围:char < short < int < float < double
- 内存中所占据的大小:1个字节到16个字节
- 内存中的表达形式:二进制数(补码)、编码
4.1.3 sizeof
- 是⼀个运算符,给出某个类型或变量在内存中所占据的字节数
- 是静态运算符,它的结果在编译时刻就决定了
- 不要在sizeof的括号里做运算,这些运算不会做的
4.2 整数型
4.2.1 内存占据
- char:1字节(8比特)
- short:2字节
- int:取决于编译器(CPU),表达计算机的字长,也就是用来表达寄存器宽度的
- long:取决于编译器(CPU),表达计算机的字长
- long long:8字节
计算机的字长:
CPU与内存RAM之间有一个总线,而在CPU中有寄存器,计算机的字长指这个寄存器的宽度,换言之,是指这个寄存器是多少比特的。同时也是在说,在CPU和RAM之间传递数据的时候,每一次所传的数据大小是计算机的字长(目前常见的是32位或者64位的系统)。
4.2.2 整数在计算机内部的表达
- 计算机内部一切都是二进制的表达
① 补码:计算机如何表达负数
- 拿补码和原码相加可以得到一个溢出的“零”。
- 实际上,从数学来看,负数意味着某个数与它的负数相加为零,即a+(-a)=0。
- 用补码思想体现的话,以一个字节(8位)为例,二进制数11111111与00000001相加,可以得到100000000,由于是八位,首位的1会被舍去,即是(1)00000000,括号里的1去掉,则得到00000000,即溢出的“零”。
- 因此,00000001的补码就是11111111。在11111111被当作纯二进制看待时,是十进制的255,被当作补码看待时则是-1。
- 同理,对于-a,其补码就是0-a,实际是2^n-a,n是这种类型的位数
② 整数的范围
- 数的范围
对于一个字节(8位),可以表达的二进制数有00000000至11111111
其中,00000000表示十进制的0
11111111至10000000表示十进制的-1至128
00000001至01111111表示十进制的1至127 - 整数的范围
1、char:1字节(8-bit):能表达的范围是-128到127
2、short:2字节(16-bit):能表达的范围是-32768到32767
3、int:取决于编译器(CPU),如32位,则是-2^(32-1)到2^(32-1)-1
4、long:取决于编译器(CPU)
5、long long:8字节
③ unsigned
- 如果一个整数型不以补码的形式表示负数,它没有负数,只有0和正整数部分,就是unsigned。它可以使表达的范围在正数部分扩大一倍再加1,但不能表达负数了。
例如在char当中,如果char a = 255(char是8位),在计算机内部会转换为11111111,即实际上这种情况下,a的值其实是-1,而不是255。但如果是unsigned char a = 255,由于取消了补码的形式,a就是255。原本char表达的范围是-128到127,在unsigned char下就会变成0到255(即127*2+1)。 - 如果⼀个字面量常数想要表达的是unsigned,可以在后面加u或U,如255U。
- 加l或L可以表示字面量是long (long)
- *unsigned的初衷并非是扩展数能表达的范围,而是为了做纯二进制运算,主要是为了移位
④ 整数越界
整数是以纯二进制进行计算的,因此:
- 11111111 + 1 —> 100000000 —> 0
- 01111111 + 1 —> 10000000 —> -128
- 10000000 - 1 —> 01111111 —> 127
4.2.3 整数的输入输出
- 整数的输入输出(scanf、printf)
%d:int、char、short
%ld:long long
%u:unsigned
%lu:unsigned long long - 8进制和16进制
1、8进制:一个以0开始的数字,其字面量是8进制
16进制:一个以0x开始的数字,其字面量是16进制
2、%d表示我们想要输入输出的是十进制数,如果我们想要输入输出8进制,则用%o,想要16进制则用%x。
3、8进制和16进制只是如何把数字表达为字符串,与计算机内部如何表达数字无关。实际上,计算机内部只有二进制,我们写代码输入十进制数后,编译器会帮我们转换为二进制,8进制和16进制也是同理。
4、16进制很适合表达二进制数据,因为4位二进制正好是一个16进制。
例如二进制的00010010表达为16进制时,前四位0001转换为16进制是1,后四位0010转换为16进制是2,刚好00010010转换为16进制就是12(在0001010转换为十进制是17,0x12转换为十进制也是17)。因此现在程序员喜欢用16进制做一些二进制的工作,比较方便转换。
5、8进制的一位数字正好表达3位二进制(因为早期计算机的字长是12的倍数,而非8,那时候喜欢用8进制)。
4.2.4 选择整数类型
为了准确表达内存和做底层程序的需要,C语言中整数有很多种类型。但如今,若没有特殊需要(例如做底层面对硬件时),就选择int。由于现在的CPU的字长普遍是32位或64位,一次内存读写就是一个int,一次计算也是⼀个int,选择更短的类型不会更快,甚至可能更慢。此外,现代的编译器⼀般会设计内存对齐,所以更短的类型实际在内存中有可能也占据⼀个int的大小(虽然sizeof告诉你更小),而unsigned与否只是输出的结果不同,内部计算是一样的。
4.3 浮点数型
4.3.1 浮点类型
类型 | 字长 | 范围 | 有效数字 |
float | 32 | ±(1.20*10^-38 ~ 3.40*10^38), 0, ±inf,nan | 7 |
double | 64 | ±(2.2*10^-308 ~ 1.79*10^308), 0, ±inf,nan | 15 |
- inf表示无穷大
- nan表示非有效数字
- 有效数字表示只有x个数字是有效的,多出来的数字就不准确了
4.3.2 浮点数的输入输出
类型 | scanf | |
float | %f | %f, %e |
double | %lf | %f, %e |
- 科学计数法(%e)
- 输出精度
在%和f之间加上.n可以指定输出小数点后n位,这样的输出是做4舍5入的。printf("%.3f\n", -0.0049); //输出-0.005 printf("%.30f\n", -0.0049); //输出0.004899999999999999841793218991 /*输出上述是因为计算机内部实际无法精确表达-0.0049, 因为数学上数是连续的,但计算机内部是离散的。 这个数和实际的0.0049的距离其实就是浮点数的误差*/ printf("%.3f\n", -0.00049); //输出-0.000
4.3.3 超过范围的浮点数(inf & nan)
- printf输出inf表示超过范围的浮点数:±∞
- printf输出nan表示不存在的浮点数
- 示例
printf("%f\n", 12.0/0.0); //输出inf printf("%f\n", -12.0/0.0); //输出-inf printf("%f\n", 0/0); //输出nan
4.3.4 浮点运算的精度
- 浮点数的运算实际是没有精度的。
两个浮点数直接用“==”来判断是否相等可能会失败,所以应该求两个浮点数之间的差是否小于一个很小的数。
通常是求两数之差的绝对值,即fabs(f1-f2) < 1e^-8或者1e^-12 - 带小数点的字面量是double而非float,float需要用f或者F后缀来表明身份
float a, b; a = 1.169f; b = 1.573f;
4.3.5 浮点数的内部表达
- 浮点数在计算时是由专用的硬件部件实现的
- 计算double和float所用的部件是⼀样的
4.3.6 选择浮点类型
- 如果没有特殊需要,只使用double
- 现代CPU能直接对double做硬件运算,性能不会比float差,在64位的机器上,数据存储的速度也不比float慢
4.4 字符型
4.4.1 字符类型
char是⼀种整数,也是⼀种特殊的类型:字符。这是因为:
- 我们可以用单引号来表示的字符字面量,我们把单引号括起来的一个部分叫做一个字符。
例如'a'和'1'也是字符(''也是一个字符) - 我们可以在printf和scanf里用%c来输入和输出字符
4.4.2 字符的输入输出
- 如何输入'1'这个字符给char c?
见下例
'1'的ASCII编码是49,所以当c==49时,它代表'1'例1: char c; scanf("%c", &c); //输入1 printf ("%d\n", c); //输出49 prinf ("%c/n", c); //输出1 例2: char c; int i; scanf("%d", &i); //输入49 c = i; printf ("%d\n", c); //输出49 prinf ("%c/n", c); //输出1
- 混合输入
scanf("%d %c", &i, &c);//有空格 //在输入过程中间加空格的前提下,可以正确读到两个数 scanf("%d%c", &i, &c);//无空格 //在输入过程中间加空格的前提下,字符会直接读空格
4.4.3 字符计算
- 一个字符加一个数字得到ASCII码表中那个数之后的字符
char c = 'A'; c+=1; printf("%c\n", c); //输出B
- 两个字符相减,得到它们在表中的距离
int i = 'Z' - 'A'; printf("%d\n", i); //输出25
4.4.4 大小写转换
- 字母在ASCII表中是顺序排列的
- 大写字母和小写字母是分开排列的,并不在一起
- 'a'-'A'可以得到两段之间的距离,因此:
a+'a'-'A'可以把⼀个大写字母变成小写字母
a+'A'-'a'可以把一个小写字母变成大写字母
4.5 逻辑型
4.5.1 bool介绍
- #include <stdbool.h>
- 之后就可以使bool和true、false
#include <stdio.h> #include <stdbool.h> int main() { bool a = true; bool b = false; bool c = 1 > 0; bool d = 1 < 0; printf("%d %d %d %d", a, b, c, d); return 0; } //输出:1 0 1 0
4.5.2 bool运算
- bool实际上还是以int的手段实现的,所以可以当作int来计算,也只能当作int来输入输出
- 任何非零值都将被视为真实值
4.6 逃逸字符
4.6.1 逃逸字符的类型
- 用来表达无法印出来的控制字符或特殊字符,它由⼀个反斜杠“\”开头,后面跟上另一个字符,这两个字符合起来,组成了一个字符
字符 | 意义 | 字符 | 意义 |
\b | 回退一格 | \" | 双引号 |
\t | 到下一个表格位 | \' | 单引号 |
\n | 换行 | \\ | 反斜杠本身 |
\r | 回车 |
4.6.2 回退一格:\b
让下一个输出的东西回到前一个的位置,(例1);但如果回退一格的后面不输出东西,那就什么也没有了(例2)。\b通常做的事情是回去但不删除,但部分终端软件可能会把\b解释为删除,看具体的软件。
//例1
printf("123\bA\n456\n");
/*输出样式为:
12A
456 */
//例2
printf("123\b\n456\n");
/*输出样式为:
123
456 */
4.6.3 到下一个表格位:\t
制表位是行当中某些固定的位置(参考tab键),\t的作用是到一行中具体的某个位置上,而不是固定数量的字符。
- 每行的固定位置
- 一个\t使得输出从下一个制表位开始
- 用\t才能使得上下两行对齐
printf("123\t456\n");
printf("12\t456\n");
/*输出样式为:
123 456
12 456 */
4.6.4 回车\r和换行\n
回车和换行起源于早期的打字机,回车是把输出的位置调到行首,换行是直接从当前位置换到下一行。
但是现在的shell一般会直接把\n翻译为要做回车和换行这两个动作。
4.7 类型转换
4.7.1 自动类型转换
- 当运算符的两边出现不一致的类型时,会自动转换成较大的类型,大的意思是能表达的数的范围更大
- char => short => int => long => long long
在整数中,char会转换为short,short转换为int,以此类推 - int => float => double
当整数遇到浮点数,int会转换为float,而两种浮点数相遇,float会转换为double - printf:任何小于int的类型会被转换成int;float会被自动转换成double
- scanf:需要明确知道后面变量的大小。例如要输入short,需要%hd
4.7.2 强制类型转换
- 把一个量强制转换成另一个类型(通常是更大的数的类型转换为小的类型)
- 格式:(类型名称) 值;
(int) 10.2;
-
注意这时候的安全性,小的变量不总能表达大的量,例如:
(short) 32768; //这时候已经超过了short能表达最大范围,输出结果可能会是-32768
-
强制类型转换只是从那个变量计算出了⼀个新的类型的值,并不改变那个变量,无论是值还是类型都不改变
int i = 32768; short s = (short) i; printf = ("%d\n", i); //此时输出的结果仍然是32768,i的类型和值并没有变化
-
强制类型转换的优先级高于四则运算
double a = 1.0; double b = 2.0; int i = (int) a / b; /*此时是把a转换为int然后除以b 由于b是double,这时候运算的结果仍然是double 然后把这个double的值赋给i */ int i = (int) (a / b) //这时候才是把a/b的值转换为了int
4.8 逻辑运算
4.8.1 逻辑运算符介绍
- 逻辑运算是对逻辑量进行的运算,结果只有0或1
- 逻辑量是关系运算或逻辑运算的结果
示例:
/*例1:如果要表达数学中的区间,如:x ∈(4,6),应该如何写C的表达式?*/
//错误写法
4 < x < 6; // 因为4<x的结果是一个逻辑值(0或1)
//正确写法
x > 4 && x < 6;
/*例2:如何判断⼀个字符c是否是⼤写字⺟?*/
//正确写法
c >= 'A' && c<= 'Z';
4.8.2 逻辑运算符的优先级
- 顺序:! > && > ||
- 示例:!age<20
先算!age,若age是0,!age为1;若age非零,!age为0
因此0或1和20作比较,!age<20始终是1
如果想表达不是age<20,应该写!(age<20)
4.8.3 短路
- 逻辑运算是自左向右进行的,如果左边的结果已经能够决定结果了,就不会做右边的计算
- 对于&&,左边是false时就不做右边了
- 对于||,左边是true时就不做右边了
- 不要把赋值,包括复合赋值组合进表达式
4.9 条件运算
4.9.1 条件运算符
- ?前面是条件
- ?后面是条件满足时的值
- :后面是条件不满足时的值
- 示例
int count; count = (count>20) ? count-10 : count+10; //等价于如下 if (count>20){ count = count-10; }else{ count = count+10; }
4.9.2 条件运算的优先级
- 条件运算符的优先级高于赋值运算符,但是低于其他运算符
- 条件运算符是自右向左结合的
- 嵌套条件表达式
会使程序阅读太复杂太困难,不建议使用int count; count = (count > 20) ? (cout < 50) ? count - 10 : count - 5 : (count < 10 ) ? count + 10 : count + 5;
4.10 逗号运算
4.10.1 逗号运算符
逗号用来连接两个表达式,并以其右边的表达式的值作为它的结果
4.10.2 逗号运算的优先级
- 所有的运算符中最低的,所以它两边的表达式会先计算
- 逗号的组合关系是自左向右,所以左边的表达式会先计算
- 右边的表达式的值会留下来作为逗号运算的结果
int i = 3+4, 5+6;
//i的结果会是7,因为逗号运算符优先级最低,会先做i=3+4
int i = (3+4, 5+6);
//i的结果会是11,因为括号的优先级更高,会先算括号里的表达式,而逗号表达式的结果是逗号右边的值
4.10.3 在for中使用
示例:
for (i=0, j=10; i<j; i++, j--)
//表示为i赋值0,j赋值10,之后每一轮i++,j--
第五章 函数
5.1 函数的定义和使用
5.1.1 函数的定义
- 函数是一块代码,接收零个或多个参数,做一件事情,并返回零个或一个值
5.1.2 调用函数
- 函数名(参数值);
- ()起到了表示函数调用的重要作用
- 即使没有参数也需要()
- 如果有参数,则需要给出正确的数量和顺序
- 这些值会被按照顺序依次用来初始化函数中的参数
5.1.3 从函数中返回值
① return介绍
- return语句做两件事:1、停止函数的执行;2、送回一个值
- return语句两种写法:1、return;(后面什么也不加);2、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; scanf("%d %d", &a, &b); printf("%d\n", max(a,b)); return 0; }
② 返回值的作用
- 可以赋值给变量
- 可以再传递给函数
- 也可以直接丢弃,因为有的时候要的是函数执行过程中的一些其他作用,不一定非要返回值
5.1.4 没有返回值的函数
- void 函数名(参数表)
- 不能使用带值的return,可以没有return
- 调用的时候不能做返回值的赋值
void sum(int begin, int end) { int i; int sum = 0; for (i=begin; i<=end; i++){ sum += 1; } printf("%d到%d的和是%d\n", begin, end, sum); } //在本例中,求和函数sum不需要返回值,我们只需要它执行printf
5.2 函数的参数与变量
5.2.1 函数原型
① 函数的先后关系
- 像这样把sum()写在上面,是因为C的编译器是自上而下的顺序分析代码
- 在看到下面的sum(1,10)的时候,它需要知道 sum()的样子
- 也就是sum()要几个参数,每个参数的类型如何,返回什么类型
- 这样它才能检查你对sum()的调用是否正确
- 如果把上面的部分整体写到下面,旧标准会先假设你所调用的函数的参数都是int,返回值也是int,但如果你实际在下面写的不是int,那就会产生error
② 函数的原型/声明
5.2.2 参数传递
① 调用函数
如果函数有参数,调用函数时必须传递给它数量和类型都正确的值
可以传递给函数的值是表达式的结果,这包括:
- 字面量
- 变量
- 函数的返回值
- 计算的结果
如果给的值和参数类型不匹配:调用函数时给的值与参数的类型不匹配是C语言传统上最大的漏洞,C的编译器总是悄悄替你把类型转换好,但是这很可能不是你所期望的 ,在后续的语言,C++/Java在这方面很严格
② 传值
C语言在调用函数时,永远只能传值给函数
#include<stdio.h>
void swap (int a, int b);
int main(){
int a = 5;
int b = 6;
swap (a, b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
void swap (int a, int b){
int t = a;
a = b;
b = t;
}
//这样的代码并不能交换a和b的值
- 每个函数有自己的变量空间,参数也位于这个独立的空间中,和其他函数没有关系
- 过去,对于函数参数表中的参数,叫做“形式参数”,调用函数时给的值,叫做“实际参数”
- 由于容易让初学者误会实际参数就是实际在函数中进行计算的参数,误会调用函数的时候把变量而不是值传进去了,所以我们不建议继续用这种古老的方式来称呼它们
- 我们认为,它们是参数和值的关系
5.2.3 本地变量(local varible)
① 本地变量介绍
- 函数的每次运行,就产生了一个独立的变量空间,在这个空间中的变量,是函数的这次运行所独有的,称作本地变量
- 定义在函数内部的变量就是本地变量
- 参数也是本地变量
② 变量的生存期和作用域
- 变量的生存期:从变量出现到变量消亡
- 作用域:在(代码的)什么范围内可以访问这个变量 (这个变量可以起作用)
- 对于本地变量而言,它的生存期和作用域都在大括号{ }内,我们把这个部分称为“块”
③ 本地变量的规则
- 本地变量是定义在块内的:它可以是定义在函数的块内,也可以定义在语句的块内,甚至可以随便设立一对大括号来定义变量
- 程序运行进入这个块之前,其中的变量不存在,离开这个块,其中的变量就消失了
- 块外面定义的变量在里面仍然有效
- 块里面若定义了和外面同名的变量,则在块内会掩盖外面的
- 不能在同一个块内定义多个同名的变量
- 本地变量不会被默认初始化
- 参数在进⼊函数的时候被初始化了
5.2.4 函数的其他细节
① 当函数没有参数时
- void f(void):括号内写void,即明确告诉编译器该函数不接收任何参数
- void f( ):括号内什么都不写的情况,在传统C中,它表表示f函数的参数表未知,并不表示没有参数
② 逗号运算符
- 调用函数时,圆括号内的逗号是标点符号,不是运算符
- 调用函数时的逗号和逗号运算符的区分:
f(a,b):标点符号
f((a,b)):逗号运算符
③ C语言不允许函数嵌套定义
我们可以在一个函数里放另一个函数的声明,但不能放另一个函数的定义(body)
④ 其他写法(不建议这样写)
- int i,j,sum(int a, int b);
定义i和j,声明sum函数 - return (i);
这个圆括号没有什么意义,可以这样写,但可能会让人误解return是个函数
⑤ main函数
- int main()也是一个函数,如果你觉得main不要任何参数,可以写成int main(void)
- return的0是有意义的,会返回给调用main函数的地方,来检查main并报告给操作系统
- 传统上,如果一个应用程序如果返回0,表示正常运行结束;返回非0则表示出现错误
第六章 数组
6.1 数组的定义和使用
6.1.1 数组的定义
① 基本定义
- <类型> 变量名称[元素数量];,例如:
int grades[100]; //说明gardes里面每一个单元都是int,它有100个这样的int double weight[20]; //说明gardes里面每一个单元都是double,它有20个这样的double
- 元素数量必须是整数
- C99之前,元素数量必须是编译时刻确定的字面量。但从C99开始就可以是变量了
② 数组的特点
- 其中所有的元素具有相同的数据类型
- 一旦创建,不能改变数组的大小
- 数组中的元素在内存中是连续依次排列的
③ 示例:int a[10];
- 一个int的数组
- 10个单元:a[0],a[1],…,a[9]
- 连续排列
- 每个单元就是一个int类型的变量
- 每个单元都可以出现在赋值的左边或右边
- 在赋值左边的叫做左值
④ 数组的单元
- 数组的每个单元就是数组类型的一个变量
- 使用数组时放在[]中的数字叫做下标或索引,下标从0开始计数,数组最大的下标是数组的个数减1
⑤ 有效的下标范围
- 编译器和运行环境都不会检查数组下标是否越界,无论是对数组单元做读还是写
- 一旦程序运行,越界的数组访问可能造成问题,导致程序崩溃(segmentation fault),但是也可能运气好,没造成严重的后果
- 所以这是程序员的责任来保证程序只使用有效的下标值:[0, 数组的大小-1]
⑥ 长度为0的数组
- int a[0];
- 可以存在,但是无用
6.1.2 数组的使用范例:投票统计
题目:写一个程序,输入数量不确定的[0,9]范围内的整数,统计每一种数字出现的次数, 并输入-1表示结束
const int num = 10; //决定数组的大小
int x;
int cnt[num]; //定义数组
int i;
for (i=0; i<num; i++){
cnt[i] = 0;
} //初始化数组
scanf("%d", &x);
while ( x != -1){
if (x>=0 && x<=9){
cnt[x]++; //数组参与运算
}
scanf("%d", &x);
}
for (i=0; i<num; i++){
printf("%d出现了%d次\n", i, cnt[i]);
}
6.2 数组的运算
6.2.1 数组的集成初始化
① 集成初始化的形式
int a[] = {6, 3, 7, 23, 67, 12, 1, 2, 15, 4};
- 直接用大括号给出数组的所有元素的初始值
- 不需要给出数组的大小,编译器会替你数数
int a[10] = {2}; //第一个元素是2,后面的都是0
int a[10] = {0}; //第一个元素和后面的都是0
- 如果给出了数组的大小,但是后面的初始值数量不足, 则其后的元素被初始化为0
- 可以用来对数组进行初始化
② 集成初始化时的定位
int a[10] = {[0] = 2, [4] = 3, 6}; //C99 ONLY
int i;
for (i=0; i<10; i++){
printf("%d\t", a[i]);
}
//输出为:2 0 0 0 3 6 0 0 0 0
- 在大括号内用[n]在初始化数据中给出定位
- 没有定位的数据接在前面位置的后面,其他位置的值补零
- 可以不给出数组大小,让编译器算,数组下标的最大值是大括号内具体给出的定位的值
- 特别适合初始数据稀疏的数组
③ 数组的大小
sizeof(a)/sizeof(a[0])
/* sizeof(a)计算整个数组的大小,sizeof(a[0]计算单个元素的大小
相除就可以得到数组内元素的个数 */
- sizeof给出整个数组所占据的内容的大小,单位是字节
- sizeof(a[0])给出数组中单个元素的大小,于是相除就得到了数组的单元个数
- 这样的代码,一旦修改数组中初始的数据,不需要修改遍历的代码
6.2.2 遍历数组
① 数组的赋值
int a[] = {2, 3, 4, 5, 6};
int b[] = a;
//这样并不能将数组a的值赋给数组b
- 数组变量本身不能被赋值
- 要把一个数组的所有元素交给另一个数组,必须采用遍历
② 数组的遍历赋值
for (i=0; i<length; i++){
b[i] = a[i];
}
- 通常都是使用for循环,让循环变量i从0到小于数组的长度,这样循环体内最大的i正好是数组最大的有效下标
- 常见错误是:
循环结束条件是<=数组长度
离开循环后,继续用i的值来做数组元素的下标
6.2.3 查找数组中的元素
#include <stdio.h>
int search(int key, int a[], int length);
int main()
{
int a[] = {2, 4, 6, 7, 1, 3, 5, 9, 11, 12, 23};
int key; //我们需要查找的数
int loc;
printf("输入一个待查找的数字:");
scanf("%d", &key);
loc = search(key, a, sizeof(a)/sizeof(a[0]));
if (loc != -1){
printf ("%d在第%d个位置上\n", key, loc);
}else{
printf("%d不存在\n", key);
}
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;
}
数组作为函数的参数时:
- 不能在[]中给出数组的大小,是没有意义的
- 不能再利用sizeof来计算数组的元素个数,往往必须再用另一个参数来传入数组的大小
6.3 示例:判断素数
思路:
- 构造n以内的素数表
- 令x为2
- 将2x、3x、4x直至ax<n的数标记为非素数
- 令x为下一个没有被标记为非素数的数,重复上一步,直到所有的数都已经尝试完毕
伪代码:
- 开辟isPrime[n],初始化其所有元素为1,isPrime[x]为1表示x是素数,prime[x]为0则表示不是素数
- 令x=2,如果x是素数,则对于(i=2; x*i<n; i++),即所有的x的倍数prime[i*x],赋值为0
- 令x++,如果x<n,重复上一步,否则结束
const int num;
printf("请问您要查找多少以内的素数:");
scanf("%d", &num);
int isPrime[num];
int i;
int x;
for (i=0; i<num; i++){
isPrime[i] = 1;
}//将数组的每个单元初始化为1
for (x=2; x<num; x++){
if(isPrime[x]){
for (i=2; i*x<num; i++){
isPrime[i*x] = 0;
}
}
} //将不是素数的单元下标标记为0
for (i=2; i<num; i++){
if (isPrime[i]){
printf("%d\t", i);
}//从下标为2开始(因为2是已知的第一个素数),若数组的元素为1,则该单元的下标为素数,将其输出
}
6.4 二维数组
6.4.1 二维数组介绍
- 示例
int a[3][5];
通常理解为a是一个3行5列的矩阵
a[0][0] | a[0][1] | a[0][2] | a[0][3] | a[0][4] |
a[1][0] | a[1][1] | a[1][2] | a[1][3] | a[1][4] |
a[2][0] | a[2][1] | a[2][2] | a[2][3] | a[2][4] |
- 其中,每一个元素a[i][j]是一个整数
- a[i][j]表示第i行,第j列的单元
6.4.2 二维数组的初始化
- 列数是必须给出的,行数可以由编译器来数
- 每行一个{},用逗号分隔
- 最后的逗号可以存在,有古老的传统,如果省略,表示补零
- 也可以用定位的初始化(C99 ONLY)
int a[][5] = { {0,1,2,3,4}, {2,3,4,5,6}, };
6.4.3 二维数组的遍历
示例:tic-tac-toe游戏
- 读入一个3X3的矩阵,矩阵中的数字为1则表示该位置上有一个X,为0表示为O
- 程序判断这个矩阵中是否有获胜的一方, 输出表示获胜一方的字符X或O,或输出无人获胜
const int size = 3;
int board[size][size];
int i, j;
int numX; //X的数量
int mumO; //O的数量
int result = -1; // -1:无人获胜 1:X赢 0:O赢
//读入board矩阵
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++){
numO = numX = 0;
for (j=0; j<size; j++){
if (board[i][j] == 1){
numX++;
}else{
numO++;
}
}
if (numO == size){
result = 0;
}else if (numX == size){
result = 1;
}
}
//检查列
if (result == -1){
for (j=0; j<size && result == -1; j++){
numO = numX =0;
for (i=0; i<size; i++){
if (board[i][j] == 1){
numX++;
}else{
numO ++;
}
}
if (numO == size){
result = 0;
}else if (numO == size){
result = 1;
}
}
}
// 检查正对角线
numO = numX = 0;
for (i=0; i<size; i++){
if (board[i][i] == 1){
numX++;
}else{
numO++;
}
}
if (numO == size){
result = 0;
} else if (numX == size){
result = 1;
}
// 检查反对角线
numO = numX = 0;
for (i=0; i<size; i++){
if (board[i][size-i-1] == 1){
numX++;
}else{
numO++;
}
}
if (numO == size){
result = 0;
} else if (numX == size){
result = 1;
}
第七章 指针
7.1 指针介绍
7.1.1 取址运算符:&
- 取得变量的地址,它的操作数必须是地址
- 地址的大小与int的大小是否相同取决于编译器、取决于是32位架构还是64位架构
- 查看变量i的地址:
int i; printf(“%p”,&i); //以16进制的形式输出i的地址
- &不能对没有地址的东西取地址
7.1.2 指针
① 概述
- 指针实际就是保存地址的变量
int* p = &i; //p是i的指针
- *号靠近int和靠近p都可以,*p是一个int,于是p是一个指针。我们是把*交给了p,而不是交给int,因此在下面两行代码中,都表示p是指针,而q只是一个int型的变量
int* p,q; int *p,q;
② 指针变量
- 变量的值是内存的地址
- 普通变量的值是实际的值
- 指针变量的值是具有实际值的变量的地址
③ 作为参数的指针
- 当我们把一个指针作为参数时,当我们调用函数时,我们需要交给它一个地址(用&符号),而不能交一个变量
void f(int *p); int i = 0; f(&i); //不可以写f(i);
-
在函数里面可以通过这个指针访问外面的这个i
7.1.3 访问地址上的变量的运算符:*
- *是一个单目运算符,用来访问指针的值所表示的地址上的变量
- 可以做右值也可以做左值
int k = *p;
*p = k+1;
左值之所以叫做左值:
- 是因为出现在赋值号左边的不是变量,而是值,是表达式计算的结果
- a[0] = 2;
- *p = 3;
- 是特殊的值,所以叫做左值
指针的运算符 & 和 *:
- 互相反作用
- *&yptr -> * (&yptr) -> * (yptr的地址)-> 得到那个地址上的变量 -> yptr
- &*yptr -> &(*yptr) -> &(yptr) -> 得到yptr的地址 -> yptr
7.2 指针的使用
7.2.1 交换两个变量的值
void swap(int *pa, int *pb)
{
int t = *pa;
int *pa = *pb;
int *pb = t;
{
7.2.2 函数需要返回多个值
- 因为函数最多只能带回一个值,所以某些值就只能通过指针返回
- 这种情况下,传入的部分参数的作用,实际上是把需要保存的结果带回来
//示例:求某个数组中最小和最大的元素 void minmax(int a[], int len, int *min, int *max); int main() { int a[] = {1, 3, 4, 6, 7, 9, 11, 12, 18, 21}; 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]; } }
7.2.3 函数需要返回运算的状态,结果需要通过指针返回
- 常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错:-1或0(在文件操作会看到大量的例子)
- 但是当任何数值都是有效的可能结果时,就得分开返回了
- 后续的语言(C++,Java)采用了异常机制来解决这个问题
//做除法 int divide(int a, int b, int *result); int main() { int a; int b; scanf("%d %d", &a, &b); int result; if (divide(a, b, &result)){ printf("%d/%d=%d\n", a, b, result); } return 0; } int divide(int a, int b, int *result) { int ret = 1; if (b == 0){ ret = 0; //如果除数为0, 则表示除法不成立 }else{ *result = a/b; } return ret; }
7.2.4 指针最常见的错误
- 定义了指针变量,还没有指向任何变量,就开始使用指针
7.4 指针与数组
7.4.1 传入函数的数组
- 函数参数表中的数组实际上是指针
- sizeof(a) == sizeof(int*)
- 但是可以用数组的运算符[]进行运算
数组参数
以下四种函数原型是等价的:
- int sum(int *ar, int n);
- int sum(int *, int);
- int sum(int ar[], int n);
- int sum(int [], int);
7.4.2 数组变量是特殊的指针
- 数组变量本身表达地址
int a[10]; int*p=a; // ⽆需用&取地址
- 数组的每个单元表达的是变量,需要用&取地址
a == &a[0];
- []运算符可以对数组做,也可以对指针做
实际上,如果我们把一个指针p指向某个变量,从数组的角度看,也可以视作该变量的地址存在于数组p,而p[0]这个单元里就是这个变量,因此*p和p[0]表达的都是这个变量的值int min = 2; int *p = &min; printf("*p=%d\n", *p); //输出p=2 printf("p[0]=%d\n", p[0]); //输出p=2
- *运算符可以对指针做,也可以对数组做
int a[0] = {1, 3, 5, 7}; printf("%d\n", *a); //输出1
- 数组变量是const的指针,所以数组之间不能相互赋值
int b[]; //可以看作为: int * const b;
7.5 指针与const // C99 ONLY
7.5.1 指针是const
- 表示一旦得到了某个变量的地址,不能再指向其他变量
int * const q = &i; //q是const,也就是i的地址不能被改变了,q不能再指向其他变量了 *q = 26; // OK,可以修改变量i的地址 q++; //ERROR
7.5.2 指针所指的是const
- 表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const)
const int *p = &i; *p = 26; //ERROR,(*p)是const,不能通过p堆对变量进行赋值 i = 26; //OK,i本身是可修改的,除非i也是const p = &j; //OK
7.5.3 判断const的位置
- 判断哪个被const了的标志:const在*的前面还是后面
int i; const int* p1 = &i; //*p1不可修改 int const* p2 = &i; //*p2不可修改 int *const p3 = &i; //p3不可修改
7.5.4 转换
- 我们总是可以把一个非const的值转换成const的
- 当要传递的参数的类型比地址大的时候,这是常用的手段:既能用比较少的字节数传递值给参数,又能避免函数对外面的变量进行修改
void f(const int* x); int a = 15; f(&a); // ok const int b = a; f(&b); // ok b = a + 1; // Error!
7.5.5 const数组
- const int a[] = {1,2,3,4,5,6,};
- 数组变量已经是const的指针了,这里的const表明数组的每个单元都是const int
- 所以必须通过初始化进行赋值
保护数组值:
- 因为把数组传入函数时传递的是地址,所以那个函数内部可以修改数组的值
- 为了保护数组不被函数破坏,可以设置参数为
const int sum(const int a[], int length);
7.6 指针计算
7.6.1 指针计算介绍
- 给一个指针加1,表示要让指针指向下一个变量
int a[10]; int *p = a; // *(p+1)表示指针p从a[0]指向了a[1]
- 如果指针不是指向一片连续分配的空间,如数组,则这种运算没有意义
例如一个指向int型数组的指针,在32位系统上,a[0]和a[1]的地址相差4个字节,即从a[0]指向a[1]时,地址应该+4,但如果我们直接修改指针p指向的地址,把地址+2、+3的话,就是没有意义的
7.6.2 指针运算
这些算术运算可以对指针做:
- 给指针加、减某个整数(+, +=, -, -=)
表示将指针向前或者向后移动某几个单元 - 递增递减(++/--)
- 两个指针相减
表示这两个指针之间还有几个单元
① *p++
- p+1,但p++不变
- 取出p所指的那个数据来,完事之后顺便把p移到下一个位置去
- *的优先级虽然高,但是没有++高
- 常用于数组类的连续空间操作
- 在某些CPU上,这可以直接被翻译成一条汇编指令
//*p++常用于数组遍历 char a[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1}; //将-1设为停止的标志 char *p = &a[0]; while( *p!=-1 ){ printf("%d\t", *p++); }
② 指针比较
- <,<=, ==,>, >=, != 都可以对指针做
- 实际是比较它们在内存中的地址
- 数组中的单元的地址肯定是线性递增
③ 0地址
- 现在的操作系统都是多进程的,基本的管理单元叫做进程(例如你双击一个东西,它运行起来就是一个进程)。对于进程来说,操作系统会给它一个虚拟的内存空间。也就是说,所有的程序运行起来时,都以为是从0开始的一片连续空间,如果是32位的话,那个顶就是4G。
- 当然你的内存中有0地址,但是0地址通常是个不能随便碰的地址。所以你的指针不应该具有0值 。
- 因此可以用0地址来表示特殊的事情:
1、返回的指针是无效的
2、指针没有被真正初始化(先初始化为0)
3、NULL是⼀个预定定义的符号,表示0地址(有的编译器不愿意你用0来表示0地址)
7.6.3 指针的类型
- 无论指向什么类型,所有的指针的大小都是一样的,因为都是地址
- 但是指向不同类型的指针是不能直接互相赋值的
- 这是为了避免用错指针
① 指针的类型转换
- void* 表示不知道指向什么东西的指针(计算时与char*相同、但不相通)
- 指针也可以转换类型
这并没有改变p所指的变量的类型,而是让后人用不同的眼光通过p看它所指的变量(不再当作是int,而是把它当作void)int *p = &i; void*q = (void*)p;
7.6.4 指针的作用
- 需要传入较大的数据时用作参数(例如传入数组后对数组做操作)
- 函数返回不止一个结果,需要用函数来修改不止一个变量
- 当需要动态申请内存的时候
7.7 动态内存分配
7.7.1 malloc函数
- 头文件:#include <stdlib.h>
- void* malloc(size_t size);
- 向malloc申请的空间的大小是以字节为单位的
- 返回的结果是void*,需要类型转换为自己需要的类型
(int*)malloc(n*sizeof(int))#include <stdio.h> #include<stdlib.h> //malloc的头文件 int main(){ int number; int *a; int i; printf("请输入数组元素数量:"); scanf("%d", &number); a = (int*)malloc(number*sizeof(int)); //申请一块内存空间 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; }
① 假如没空间
- 如果申请失败则返回0,或者叫做NULL
#include <stdio.h> #include<stdlib.h> int main(void){ void *p; int cnt = 0; while ((p=malloc(100*1024*1024))){ //100*1024*1024表示100M cnt++; } //如果p得到地址不是0,那么cnt++ //如果p得到的地址是0,那么循环终止 printf("分配了%d00MB的空间\n", cnt); free(p); return 0; }
② free()
- 把申请得来的空间还给“系统”
- 申请过的空间,最终都应该要还
- 只能还申请来的空间的首地址
③ 常见的问题
- 申请了没free—>长时间运行内存逐渐下降
1、新生:忘了
2、老手:找不到合适的free的时机 - free过了再free
- 地址变过了,直接去free
第八章 字符串
8.1 字符串介绍
8.1.1 字符串
- 字符串指以0(整数0)结尾的一串字符。写成0或‘\0’是一样的,但是和‘0’(这是个字符)不同
- 0标志字符串的结束,但它不是字符串的一部分(计算字符串长度的时候不包含这个0)
- 字符串以字符数组的形式存在,以数组或指针的形式访问(更多的是以指针的形式)。
- 不能用运算符对字符串做运算,通过数组的方式可以遍历字符串
- 唯一特殊的地方是字符串常量(双引号括起来的东西)可以用来初始化字符数组
- 头文件string.h里有很多处理字符串的函数
8.1.2 字符串变量
① 字符串变量的书写
//注意是双引号
char *str = "Hello";
char word[] = "Hello";
char line[10] = "Hello"; //实际占了6个字节的位置,因为还有结尾的0(编译器会生成结尾的0)
8.1.3 字符串常量
- "Hello"这样一个被双引号括起来的东西就叫做字符串常量,或者字符串的字面量
- "Hello"会被编译器变成一个字符数组放在某处,这个数组的长度是6,结尾还有表示结束的0
- 两个相邻的字符串常量会被自动连接起来,行末的\表示下一行还是这个字符串常量
printf("请输入你的号码," "比如12345"); //这两行会被自动连接起来 printf("请输入你的号码,\ 比如12345"); \\上面的那个反斜杠会把这两行连在一起
- char *s和char s[]
char *s = "Hello"; /*s是⼀个指针,初始化为指向⼀个字符串常量。 由于这个常量所在的地方是一个只读的代码段, 所以实际上s是const char* s 。 但是由于历史的原因,编译器接受不带const的写法。 如果试图对s所指的字符串做写⼊会导致严重的后果。*/ char s[] = "Hello"; //如果需要修改字符串,应该⽤数组。
指针或数组的选择:
- 数组:表示这个字符串就在此处
作为本地变量,空间会被自动回收 - 指针:这个字符串不知道在哪里,只读即可
处理参数
动态内存分配 - 如果要构造一个字符串:数组
如果要处理一个字符串:指针
char *:
- 字符串可以表达为char*的形式,但char*不一定是字符串
- char *本意是指向字符的指针,可能指向的是字符的数组(就像int*一样)
- 只有它所指的字符数组有结尾的0,才能说它所指的是字符串
8.2 字符串的使用
char *t = "title";
char *s;
s = t;
- 并没有产生新的字符串,只是让指针s指向了t所指的字符串,对s的任何操作就是对t做的
8.2.1 字符串输入与输出
char s[10];
scanf("%9s", s); //读⼊一个单词(到空格/tab/回车为止)
//写成scanf("%s", s);是不安全的,因为不知道要读⼊的内容的⻓度
printf("%s", s);
- 在%和s之间的数字表示最多允许读入的字符数量,这个数字应该比数组的大小小1
8.2.2 常见错误
char *string;
scanf("%s", string);
- 以为char*是字符串类型,定义了一个字符串类型的变量string就可以直接使用了
- 由于没有对string初始化为0,所以不一定每次运行都出错
8.2.3 空字符串
char buffer[100]="";
//这是⼀个空的字符串,buffer[0] == '\0'
char buffer[] = "";
//这个数组的⻓度只有1,这个里面放不下任何字符串
8.2.4 字符串数组
- a是一个指针,指向另一个指针,那个指针指向一个字符(串)
char **a
- a是一个二维数组,第⼆个维度的大小不知道,不能编译
char a[][]
- a是一个二维数组,a[x]是一个char[10]
char a[][10]
- a是一个一维数组,a[x]是一个char*
char *a[]
8.2.5 程序参数
int main(int argc, char const *argv[])
//argv[0]是命令本⾝
//当使⽤Unix的符号链接时,反映符号链接的名字
8.3 字符输入与输出函数
8.3.1 putchar
int putchar(int c);
- 向标准输出(即那个黑窗口)写一个字符
- 返回写了几个字符,EOF(-1)表示写失败
8.3.2 getchar
int getchar(void);
- 从标准输入读入一个字符
- 返回类型是int,是为了返回EOF(-1)
8.4 标准库中的字符串函数 string.h
8.4.1 strlen 求字符串长度
- size_t strlen(const char *s);
- 返回s的字符串长度(不包括结尾的0)
#include <stdio.h> #include <string.h> int main(){ char line[] = "Hello"; printf("strlen=%lu\n", strlen(line)); return 0; }
- 自己用代码实现strlen
#include <stdio.h> size_t strlen(const char *s); int main(){ char line[] = "Hello"; printf("strlen=%lu\n", strlen(line)); return 0; } size_t strlen(const char *s){ int cnt = 0; while (s[cnt] != '\0'){ cnt++; } return cnt; }
8.4.2 strcmp 比较字符串大小
- int strcmp(const char *s1, const char *s2);
- 比较两个字符串:
返回0:s1==s2
返回>0:s1>s2
返回<0:s1<s2 - 比较的是两个字符串中不相同字符的ASCII码,可能返回的是ASCII码的差值,也可能只是用0、1、-1来表示比较的结果
#include <stdio.h> #include <string.h> int main(){ char s1[] = "abc"; char s2[] = "Abc"; printf("%d\n", strcmp(s1, s2)); return 0; }
- 自己用代码实现
#include <stdio.h>
int strcmp(const char *s1, const char *s2);
int main(){
char s1[] = "abc";
char s2[] = "Abc";
printf("%d\n", strcmp(s1, s2));
return 0;
}
//写法1
int strcmp(const char *s1, const char *s2){
int idx = 0;
while (s1[idx] == s2[idx] && s1[idx]!='\0'){
idx ++;
}
return s1[idx] - s2[idx];
}
//写法2
int strcmp(const char *s1, const char *s2){
while (*s1 == *s2 && *s1!='\0'){
s1++;
s2++;
}
return *s1-*s2;
}
8.4.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);
strcpy(dst, src);
- 自己用代码实现
//写法1
int strcpy(char *dst, const char *src){
int idx = 0;
while(src[idx]){ //括号内实际是src[idx] != '0'
dst[idx] = src[idx];
idx++;
}
dst[idx] = '\0';
return dst;
}
//写法2
int strcpy(char *dst, const char *src){
char *ret = dst;
while(*src){ //括号内实际是*src != '\0'
*dst++ = *src++;
} //也可以直接写为 while(*dst++ = *src++);
*dst = '\0';
return ret;
}
8.4.4 strcat 连接字符串
- char * strcat(char *restrict s1, const char *restrict s2);
- 把s2拷贝到s1的后面,接成一个长的字符串,然后返回s1
- s1必须具有足够的空间
8.4.5 安全问题
- strcpy和strcat都可能出现安全问题:目的地没有足够的空间
- 安全版本:
char *strncpy (char *restrict dst, const char *restrict src, size_t n); char *strncat (char *restrict s1, const char *restrict s2, size_t n); int strncmp (const char *s1, const char *s2, size_t n); //函数名和参数表中,多出来的n表示能用的最多的字符数 //strncmp中的n表示需要判断前n个字符 //n要写具体的数字
8.4.6 strchr 字符串中找字符
- char * strchr(const char *s, int c);
从左边开始找所查找的字符第一次出现的位置 - char * strrchr(const char *s, int c);
从右边开始找所查找的字符第一次出现的位置 - 返回NULL表示没有找到
#include <stdio.h>
#include <string.h>
int main(){
char s[] = "hello";
char *p = strchr(s, 'l');
printf("%s\n", p); //结果是llo
//如果想找到l并把l后面的东西复制到另一个字符串
char *t = (char*)malloc(strlen(p)+1);
strcpy(t, p);
printf("%s\n", t); //结果是llo
free (t);
//如果想找到l并要l前面的那一段he
char c = *p;
*p = '\0'; //把s中的l改为了\o,于是现在s只剩下了he
char *t1 = (char*)malloc(strlen(s)+1);
strcpy(t, s);
printf("%s\n", t1); //结果是he
*p = c; //把p恢复
//如果想要找第二个l
p = strchr(p+1, 'l');
printf("%s\n", p); //结果是lo
return 0;
}
8.4.7 strstr 字符串中找字符串
- char *strstr (const char *s1, const char *s2);
用来在字符串中寻找字符串 - char * strcasestr(const char *s1, const char *s2);
寻找字符串的过程中忽略大小写
第九章 结构类型
9.1 enum 枚举
9.1.1 枚举介绍
- 枚举是一种用户定义的数据类型,它用关键字enum以如下语法来声明:
enum 枚举类型名字 {名字0, …, 名字n} ; - 枚举类型名字通常并不真的使用,要用的是在大括号里的名字, 因为它们就是就是常量符号,它们的类型是int,值则依次从0 到n
- 创建三个常量,red的值是0,yellow是1,而green是2
enum colors { red, yellow, green };
-
当需要一些可以排列起来的常量值时,定义枚举的意义就是给 了这些常量值名字
9.1.2 枚举的使用
#include <stdio.h>
enum color {red, yellow, green}; //声明新的数据类型 color
void f(enum color c); //参数是enum类型的color
int main(){
enum color t = red;
scanf("%d", &t);
f(t);
return 0;
}
void f(enum color c){
printf("%d\n", c);
- 枚举量可以作为值赋给变量
- 枚举类型可以跟上enum作为类型
- 但是实际上C语言内部,enum就是以整数int来做计算的,所以一个枚举的变量是可以当作int来进行输入和输出的
9.1.3 自动计数的枚举
#include <stdio.h>
enum color {red, yellow, green, numcolors};
//因为枚举中的各个量的值是从0开始依次递增的,所以numcolors的值就是前面数字个数的计数
int main(int argc, char const *argv[]){
int color = -1;
char *colornames[numcolors] = {
"red", "yellow", "green",
};
char *colorname = NULL; //NULL必须是大写
printf("输入你喜欢的颜色的代码:");
scanf("%d", &color);
if (color >=0 && color <numcolors){
colorname = colornames[color];
} else {
colorname = "unknown";
}
printf("你喜欢的颜色是%s\n", colorname);
return 0;
}
- 这样需要遍历所有的枚举量,或者需要建立一个用枚举量做下标的数组的时候就很方便了
9.1.4 枚举量
#include <stdio.h>
enum COLOR {red = 5, yellow, green = 5}; //声明枚举量的时候可以指定值,可以是离散的
int main(int argc, char const *argv[]){
enum COLOR color = 0;
printf("code for green is %d\n", green); //输出5
printf("and color is %d\n", color);//枚举量只是int
//虽然COLOR里面没有0,但上述代码在如今的编译器里是可以实现的(以前不行,需要做类型转换)
return 0;
}
- 即使给枚举类型的变量赋不存在的整数值也没有任何warning或error
- 虽然枚举类型可以当作类型使用,但是实际上很(bu)少(hao)用
- 除非是需要排比的符号量,用枚举比const int方便
- 枚举比宏(macro)好,因为枚举有int类型
9.2 struct 结构
9.2.1 声明结构类型
//写法1:在函数内部的声明结构类型
//和本地变量⼀样,在函数内部声明的结构类型只能在函数内部使用
#include <stdio.h>
int main(int argc, char const *argv[])
{
struct date{
int month;
int day;
int year;
}; //记得这里有一个分号
struct date today;
today.month = 10;
today.day = 31;
today.year = 2021;
printf("Today's date is %i-%i-%i.\n",today.year, today.month, today.day);
return 0;
}
//写法2:在函数外部的声明结构类型
//通常在函数外部声明结构类型,这样就可以被多个函数所使用了
#include <stdio.h>
struct date{
int month;
int day;
int year;
};
int main(int argc, char const *argv[])
{
struct date today;
today.month = 10;
today.day = 31;
today.year = 2021;
printf("Today's date is %i-%i-%i.\n",today.year, today.month, today.day);
return 0;
}
9.2.2 声明结构的形式
- p1和p2都是point里面有x和y的值
//TYPE 1 struct point { int x; int y; }; struct point p1, p2; //使用结构的时候前面要写上struct,后面要跟上变量的名字
- p1和p2都是一种无名结构,里面有x和y
//TYPE 2 struct { int x; int y; } p1, p2;
- p1和p2都是point,里面有x和y的值t
//TYPE 3 struct point { int x; int y; }; p1, p2;
对于第一和第三种形式,都声明了结构point。但是第⼆种形式没有声明point,只是定义了两个变量。
9.2.3 结构变量
① 定义结构类型的变量
struct date today;
today.month = 10;
today.day = 31;
today.year = 2021;
② 结构的初始化
struct date
{
int month;
int day;
int year;
};
struct date today = {07, 31, 2014};//依次赋值给month、day、year
struct date thismonth = {.month = 7, .year = 2014}; //此时day初始化为0
③ 结构成员
- 结构和数组有点像
- 数组用[]运算符和下标访问其成员
a[0] = 10; - 结构用.运算符和名字访问其成员
today.day
student.firstName - p1.x
p1.y
④ 结构运算
- 要访问整个结构,直接用结构变量的名字
- 对于整个结构,可以做赋值、取地址,也可以传递给函数参数
p1 = (struct point){5, 10}; // 进行一个强制类型转换,相当于p1.x = 5; p1.y = 10 p1 = p2; // 相当于p1.x = p2.x; p1.y = p2.y
⑤ 结构指针
- 和数组不同,结构变量的名字并不是结构变量的地址,必须使用&运算符
struct date *pDate = &today;
9.2.5 结构与函数
① 结构作为函数参数
int numberofdays(struct date d);
- 整个结构可以作为参数的值传入函数
- 这时候是在函数内新建一个结构变量,并复制调用者的结构的值
- 也可以返回一个结构
- 这和数组完全不同
② 输入结构
由于没有直接的办法可以一次性scanf一个结构,因此我们可以做一个函数来读入结构
结构指针:用->表示指针所指的结构变量中的成员
*p.month = 12;
//可写作
p->month = 12;
- 结构指针参数:相比直接传入结构(占空间),用结构指针作为参数,就只需要传入传出一个指针的大小
如果需要保护传入的结构不被函数修改,可以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)); } struct point *getstruct(struct point *p) { scanf("%d", &p->x); scanf("%d", &p->y); printf("%d, %d", p->x, p->y); return p; } void output(struct point p) { printf("%d, %d", p.x, p.y); } void print(const struct point *p) { printf("%d, %d", p->x, p->y); }
9.2.6 结构中的结构
① 结构数组
struct date dates[100]; //数组中的每一个元素是date类型的结构变量
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;
}
}
}
return now;
}
② 结构中的结构
struct dateAndTime{
struct date sdate;
struct date stime;
} //结构里面的成员变量可以是另外的结构
③ 嵌套的结构
- 用两个点坐标表示矩形
struct point //用point表达点
{
int x;
int y;
};
struct rectangle{ //用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不是指针)
- 结构中的结构的数组
#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]);
}
9.3 union 联合
9.3.1 typedef 类型定义
typedef int Length;
// 使得Length成为int类型的别名
// Length这个名字可以代替int出现在变量定义和参数声明之类的地方
// 如下:
Length a, b, len;
Length numbers[10];
- 当用于结构中时
typedef struct Adate{ //重载已有的类型名字;新名字的含义更清晰,具有可移植性
int month;
int day;
int year;
} Date; //Date可以替代struct Adate
/*eg.*/Date d = {9, 1, 2005};
typedef int Length; // Length就等价于int类型
typedef *char[10] Strings; // Strings 是10个字符串的数组的类型
//用aNode代替struct node
//写法1
typedef struct node {
int data;
struct node *next;
} aNode;
//写法2
typedef struct node aNode;
9.3.2 union 联合
union AnElt{
int i;
char c;
} elt1, elt2;
elt1.i = 4;
elt2.c = ’a’;
elt2.i = 0xDEADBEEF;
- union在形式上和struct非常相似
- 但union是一种选择:
成员是一个int i
或者成员是一个char c - sizeof(union …) = sizeof(每个成员)的最大值
- 存储
1、所有的成员共享⼀个空间
2、同一时间只有一个成员是有效的
3、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;
}
- 通过这种方式得到一个整数之类的内部的字节
- 文件操作、当要把一个整数以二进制的方式写到文件里的时候,可以作为读写中间的媒介