c语言程序设计
浙江大学翁恺教授的c语言网课学习笔记。
1.数据类型
1.基本类型
整型(int)
字符型(char)
浮点型(float,double)
2.构造类型
数组类型
结构体类型
共用体类型
枚举类型
3.指针类型
4.空类型
指针是c语言的灵魂。
2.输入输出
printf(“%d”,a);
scanf(“%d,%d”,&a,&b ) //scanf中“里的”东西 是要你输入的 例如%d %d 则输入 1 2 例如%d,%d 则输入 1,2
3.常量与变量
1.const
const是一个修饰符,加在int前面,给这个变量加上一个const(不变的)的属性。这个const属性表示这个变量的值一旦初始化了,就不能再修改了。(const 变量要求全大写用来区分)
例如:const int AMOUNT = 100;
AMOUNT = 90; //报错 read-only variable is not assignable
4.浮点数(带小数点的数)
1.float单精度
两个整数的运算结果只能是整数
用int定义变量时,在除法中容易出错,当出现小数点时,会自动舍去小数点。
当浮点数与整数放在一起计算时,c语言会将整数转换成浮点数,然后进行浮点数的运算。
浮点数的输出 printf(“%f“,a);
2.double双精度
当用scanf输入double型的变量时,不能再使用%d(整数),而是换成**%lf**
例如:scanf(“%lf”,&a);
总结
整数:int printf(“%d”,…); scanf(“%d”,…)
浮点数:double printf(“%f”,…); scanf(“%lf”,…);
5.表达式
一个表达式就是一系列的运算符和算子的组合,用来计算一个数。
1.运算符(operator)是指进行运算的动作,比如 “+” “-”
2.算子(operand) 是指参与运算的值,这个值可能是常数,也可能是变量,还可以是一个方法的返回值。
6.运算符的优先级
1.赋值运算符
赋值也是运算,也有结果
a=6的结果是a被赋予的值,也就是6
a=b=6。 // a=(b=6)
2.结合关系
一般自左向右
单目+- 和赋值= 自右向左
7.复合赋值和递增递减
1.复合赋值
5个算术运算符,+-*/%,可以和赋值运算符= 结合起来,形成复合赋值运算符
例如:+= -= *= /= %=
注意两个运算符中间不要有空格
total += (sum+100)/2
total = total + (sum+100)/2
total *= sum+12
total = total *(sum+12)
2.递增递减运算符
++和– 是两个很特殊的运算符,它们是单目运算符,这个算子和必须是变量。
count++;
count +=1;
count = count + 1;
前缀与后缀
–a 与 a++
a++ 是给a加上1 但表达式的值是 a原来的值
++a 是给a加上1 但表达式的值是a+1后的值
8.判断语句
1.判断条件
计算两个值之间的关系,所以叫关系运算
== != > < >= <=
关系运算的值只有0和1
所有关系运算符的优先级比算数预算符的低,但是比赋值运算的高
2.else的匹配
else就近原则匹配if
3.多路分支
1.switch-case
1 | switch(控制表达式){ //控制表达式必须是int类型的 |
2.break
switch语句可以看作是一种基于计算的跳转,计算控制表达式的值后,程序回跳转到相匹配的case(分支标号)。分支标号只是说明switch内部位置的路标,在执行完分支中的最后一条语句后,如果后面没有break,就会顺序执行到下面的case里面去,直到遇到一个break或switch为止。
9.循环
1.while循环
当条件满足的时候,不断的重复循环体内的语句。
2.do-while循环
在进入循环的时候不做检查,而是在执行完一轮循环体的代码之后,再来检查训话的条件是否满足,如果满足则继续下一轮循环,不满足则结束循环。
1 | do |
3. for循环
1 | for(初始动作;条件;每轮的动作){ |
循环次数
for(i = 0; i < n; i++)
则循环次数是n次,而循环结束后,i的值是n。循环的控制变量i,是选择从0开始还是从1开始,是判断i<n还是i<=n,对循环的词数,循环的结束后变量的值都有影响。
Tips
1. 如果有固定次数,用for
1. 如果必须执行一次,用do-while
1. 其他情况用while
10. 循环控制
1. 素数判断
素数:只能被1和自己整除的树,1不是素数。
1 |
|
2. break和continue
输出100以内的素数
1 |
|
输出前50个素数(使用while循环与for循环的相互替代)
1 |
|
3. 从循环嵌套中跳出
break和continue都只能对它所在的那层循环起作用。
在多层for循环嵌套中,break只能使程序跳出最内层的for循环。
解决办法是使用接力break或者使用goto。
接力break
1 | int main(){ |
goto(特别适用于需要从嵌套循环的内层跳到最外面去)
1 | int main(){ |
11. 编程训练
1.求前n项和
f(n) = 1 + 1/2 + 1/3 + …+ 1\n
1 |
|
f(n) = 1 - 1/2 + 1/3 - 1/4 …+ 1\n
1 |
|
2. 整数分解
1 |
|
3. 求最大公约数(gcd)
辗转相除法
1. 如果b等于0,计算结束,a就是最大公约数;
1. 否则,计算a除以b的余数,让a等于b,而b等于那个余数;
1. 回到第一步。
1 | /* |
4. 水仙花数
1 |
|
5. 打印九九乘法表
1 |
|
6. 统计素数并求和
1 |
|
7. n项求和
1 |
|
8. 约分最简分式
上下同时除以最大公约数
1 |
|
9.念数字
1 |
|
12. C语言的类型
C是有类型的语言
C语言的变量,必须:
1. 在使用前定义
1. 确定类型
C以后的语言向两个方向发展:
- C++/Java更强调类型,对类型的检查更严格。
- JavaScript、Python、PHP不看重类型,甚至不需要事先定义。
类型安全
- 支持强类型的观点认为明确的类型有助于尽早发现程序中的简单错误。
- 反对强类型的观点认为强类型迫使程序员面对底层、实现而非事务逻辑。
- 总结,早期的语言强调类型,面向底层的语言强调类型。
- C需要类型,但是对类型的检查并不够。
C语言的类型
- 整数
char、short、int、long、long long(C99)
- 浮点数
float、double、long double(C99)
逻辑
bool(C99)
指针
自定义类型
类型有何不同
- 类型名称:int、long、double
- 输入输出时的格式:%d、%ld、%lf
- 所表达的数的范围:char < short < int <float < double;
- 内存中所占据的大小:1字节(char)到16字节(long double)
- 内存中的表达形式:二进制数(补码:整型)、编码(浮点型)
sizeof
- 是一个运算符,给出某个类型或变量在内存中所占据的字节数
sizeof(int);
sizeof(i);
- 是一个静态运算符,它的结果在编译时刻就决定了
- 不再在sizeof括号里做运算,这些运算不会执行。
int a;
sizeof(a++);
整数的范围
- char:1字节 -128~127
- shor:2字节 -32768~32768
- int:取决于编译器(cpu),通常的意义是“1个字”,-2^(32-1) ~ 2^(32-1) -1
- long:4字节
- long long:8字节
unsigned
如果一个字面量常数想要表达自己是unsigned,可以在后面加u或着U
255U
用 l 或者 L 表示long
unsigned的初衷并非扩展数能表达的范围,而是为了做纯二进制运算,使最高位的1不再表示负号, 主要是为了移位。
1 | char c = 255; |
整数的输入输出
只有两种形式:int或者long long
1. %d: int
1. %u: unsigned
1. %ld: long long
1. %lu: unsigned long long
整型用int 就完事了
字符类型
char是一种整数,也是一种特殊的类型:字符。这是因为
1. 用单引号表示的字符字面量:‘a' ,‘1’
1. ‘‘ 也是一个字符
1. printf和scanf里用%c来输入输出字符
1 |
|
字符计算
1 | char c = 'A'; |
1. 一个字符加一个数字得到ASCll码表中那个数之后的字符
1. 两个字符相减,得到它们在表中的距离。
大小写转换
1. 字母在ASCll表中是顺序排列的
1. 大写字母与小写字母是分开排列的,并不在一起。
1. 'a' - ‘A' 可以得到两段直接的距离,于是
1. a + ‘a’ - ‘A’可以把一个大写字母变成小写字母
2. a + ‘A’ - ‘a’ 可以吧一个小写字母变成大写字母
逃逸字符
1. \b 回退一格(不是删除)
1. \t 到下一个表格位
1. \n 换行
1. \r 回车
1. \ " 双引号
1. \ ' 单引号
1. \ \ 反斜杠本身
制表位
- 每行的固定位置
- 一个\t 使得输出从下一个制表位开始
- 用\t才能使得上下两行对齐
1 |
|
自动类型转换
当运算符的两边出现不一致时,会自动转换成较大的类型
1. 大的意思是能表达的数范围更大
char -> short -> int -> long ->long long
int -> float ->double
对于printf,任何小于int的类型会被自动转换成int;float会转换成double
但是scanf不会,要输入short,需要%hd;
强制类型转换
需要吧一个量强制转换成另一个类型(通常是较小的类型),需要:
(类型)值
例如: (int)9.2
注意这时候的安全性,小的变量不总能表达大的量
例如:(short)32768 - > -32768
强制类型转换的优先级高于四则运算
bool类型
- #include<stdbool.h>
- 之后就可以使用bool和true,false
逻辑运算
- 逻辑运算是对逻辑量进行的运算,结果只有0或1
- 逻辑量是关系运算或逻辑运算的结果
- !逻辑非、&& 逻辑与、||逻辑或
如果要表达数学中的区间,如:(4,6)或者[4,6],应如何写C的表达式?
错误:4< x < 6,因为 4<x 的结果是一个逻辑值(0或1)
正确:x > 4 && x < 6
判断一个字符c是否是大写字母?
c >= ‘A’ && c <= ‘Z’
逻辑运算符的优先级
! > && > ||
例如: !done && (count > MAX)
短路
逻辑运算是自左向右进行的,如果左边的结果已经能够决定结果了,就不会做右边的计算
a == 6 && b == 1,如果a==6 不成立 就不会判断b是否等于1
a == 6 && b += 1,如果a==6 不成立,b+=1 就不会执行
对于&&, 左边是false就不会做右边了
对于||, 左边是true时就不做右边了
条件运算符
count = (count > 20) ? count -10 : count+10
条件、条件满足时的值和条件不满足时的值
逗号运算
在for循环中使用
for( i=0, j=10; i<j; i++, j–)
13. 函数
1.前言
求1到10,30,50的和
1 |
|
代码复制是程序质量不良的表现
编写一个函数
1 |
|
2. 什么是函数?
1 | void sum(int begin, int end) //函数头 void:返回类型 sum:函数名 |
3. 调用函数
函数名(参数值)
sum(1,10);
()起到了表示函数调用的重要作用,即使没有参数也需要()
如果有参数,则需要给出正确的数量和顺序
这些值会被按顺序依次用来初始化函数中的参数
4. 函数返回
1 | int max(int a, int b){ |
没有返回值的函数
void 函数名(参数表)
不能使用带值的retrun
可以没有return
调用的时候不能做返回值的赋值
5. 函数原型
在C99的标准中,编译器是从上到下编译你的代码,当你的函数在主函数下边,编译时会报错函数未定义。
这时使用函数的声明就可以解决。
函数原型:
1.函数头,以“ ; ”结尾,就构成了函数的原型。 void sum(int begin, int end);
2.函数原型的目的是告诉编译器这个函数长什么样子
名称、参数、返回类型
1 |
|
6. 参数传递
传递的类型与定义的类型不匹配:
1. 调用函数时给的值与参数的类型不匹配是C语言传统上最大的漏洞
1. 编译器总是悄悄替你把类型转换好,但是这很可能不是你所期望的
1. 后续的语言,C++/Java在这方面很严格。
例如:void sum(int i) { }
sum(2.3); 编译器会自动把2.3转变成2。
传过去的是什么?
C语言在调用函数时,永远只能传值给函数
1 | void swap(int a, int b); |
这个代码并不能交换 main函数中 a和b的值。传递给swap()的只是参数。
7. 传值
每个函数都有自己的变量空间,参数也位于这个独立空间中,和其他函数没有关系
对于函数参数表中的参数,叫做“形式参数”, 调用函数时给的值,叫做“实际参数”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void swap(int a, int b); // 形参
int main(){
int a = 5;
int b = 6;
swap(a,b); //实参
return 0;
}
void swap(int a, int b){ //形参
int t = a;
a = b;
b = t;
}- 不建议继续使用这种古老的方式来称呼它们。
- 它们是参数和值的关系
8. 本地变量(局部变量)
- 函数每次运行,就产生了一个独立的变量空间,在这个空间中的变量,是函数这次运行独有的,称作本地变量。
- 定义在函数内部的变量就是本地变量。
- 参数也是本地变量。
9. 关于main
int main()也是一个函数
不需要写成int main(void)
return的0有人看吗?
是给操作系统看的
windows:if errorlevel 1…
Unix Bash: echo $?
14. 数组
1. 前言
如何写一个程序计算用户输入的数字的平均数,并输出所有大于平均数的数?
1 |
|
2. 定义数组
- <类型> 变量名称[元素数量]
- int grades[100];
- double weight[20];
- 元素数量必须是整数
- C99之前:元素数量必须是编译时刻确定的字面量
数组
- 是一种容器(放东西的东西),特点是:
- 其中所有元素具有相同的数据类型;
- 一旦创建,不能改变大小。
- 数组中的元素在内存中时连续依次排列的
数组的单元
- 数组的每个单元就是数组类型的一个变量
- 使用数组时放在[]中的数字叫做下标或索引,下标从0开始计数。
有效的下标范围
编译器和运行环境都不会检查数组下标是否越界,无论是对数组单元做读还是写。
一旦程序运行,越界的数组访问可能造成问题,导致程序崩溃
segmentation fault
运气好也可能不造成严重后果
但是必须保证只使用有效的下标值:[0, 数组大小-1]
3.统计数字
写一个程序,输入数量不确定的[0, 9]范围内的整数,统计每一种数字出现的次数,输入-1表示结束。
1 |
|
4. 数组的大小
sizeof给出整个数组所占据的内容的大小,单位是字节
sizeof(a)/sizeof(a[0]) //用数组的大小除以数组第一个单元的大小得到的就是数组有多少个元素
这样的代码,一旦修改数组中初始的数据,不需要修改遍历的代码
5. 数组的赋值
数组变量本身不能被赋值
要把一个数组的所有元素交给另一个数组,必须采用遍历。
for( i=0; i<length; i++){
b[i] = a[i];
}
数组作为函数时,往往必须再使用另一个参数来传入数组的大小。
- 不能在[]中给出数组的大小
- 不能再利用sizeof来计算数组的元素个数
1
2
3int search(int key, int a[], int length){
}数组存储素数
1 |
|
7. 二维数组
- int a[3] [5];
- 通常理解为一个3行5列的矩阵
二维数组的初始化
- int a[ ] [5] = { {0, 1, 2, 3, 4}, {2, 3, 4, ,5, 6}};
- 列数时必须给出的,行数可由编译器来数
- 每行一个{ }, 逗号分隔
- 最后的逗号可以存在,是古老的传统
- 如果省略,表示补零
- 也可以用定位 a[1] [3]
15. 指针
1.取地址运算
运算符&
scanf(“%d”, &i);里的&
获得变量的地址,它的操作数必须是变量
int i; printf(“%x”, &i);
地址的大小是否与int相同取决于编译器
Int i; printf(“%p”, &i);
2.指针
就是保存地址的变量
1 | int i; |
指针变量
变量的值是内存的地址
1. 普通变量的值是实际地址
1. 指针变量的值是具有实际值的变量的地址
作为参数的指针
void fun(int *p); //当函数的参数位为指针时
在被调用的时候得到某个变量的地址:
int i = 0; f(&i); //调用的时候要给函数传入地址
在函数里面可以通过这个指针访问外面的这个i
1 |
|
访问那个地址上的变量 *
- 是一个单目运算符,用来访问指针的值所表示的地址上的*变量
- 得到的变量可以做右值也可以做左值
- int k = *p;
- *p = k+1;
1 |
|
通过*我们访问到了指针p所指向的地址的值
指针的应用场景
- 交换两个变量的值
1 |
|
- 函数返回多个值,某些值就只能通过指针返回
找一个数组中的最大最小值,函数需返回两个值。
1 |
|
- 函数返回运算状态,结果通过指针返回
- 通常的套路时让函数返回特殊的不属于有效范围的值来表示出错: -1 或 0
1 |
|
指针最常见的错误
定义了指针,还没有指向任何变量就开始使用指针
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);
在参数表中出现,这四个是等价的。
数组变量是特殊的指针
数组变量本身表达地址,所以
int a[10]; int *p = a; //无需用&取地址
但是数组的单元表达的是变量,需要用&取地址
a == &a[0];
[ ]运算符可以对数组做,也可以对指针做:
p[0] <==> a[0]; //当指针指向一个数组后,可以把这个指针当作数组使用
*运算符可以对指针做,也可以对数组做
*a = 25 //数组变量也可以当指针用
数组变量是const的指针,所以不能被赋值
1 | int a[]; |
3.指针与const
指针 –可以是const, 值 –也可以是const,当指针指向了这个值,那它们之间有什么联系?
指针是const
表示一旦得到了某个变量的地址,不能再指向其他变量
1
2
3int * const q = &i; //q 是 const
*q = 26; // it's ok 只改变了q指向的地址所保存的值
q++; // error. q指向i不可被改变所指是const
表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const)
1
2
3
4const int *p = &i;
*p = 26; //error. *p是const 不能通过 p 去修改 i,
i = 26; //ok i本身并没有变成const
p = &j; //ok各种情况
判断哪个被const了的标志是const在 * 的前面还是后面
1
2
3
41. int i;
2. const int* p1 = &i; //const在*前面 表示 *p是const 不能通过 *p去修改 i
3. int const* p2 =&i; //const在*前面 表示 *p是const 不能通过 *p去修改 i
4. int *const p3 = &i; //const在*后面 表示 指针不能被修改const数组
const int a[ ] = {1,2,3,4,5,6,};
前面我们说到,数组变量本身已经是const的指针了,这里的const int a[ ]表明数组的每个单元都是const int
所以必须通过初始化进行赋值
保护数组值
因为把数组传入函数是传递的是数组变量的地址,所以那个函数内部可以修改数组的值
为了保护数组不被函数破坏,可以设置参数为const
1
int sum(const int a[], int length); //这样在函数中就无法对数组的值进行修改
4. 指针运算
1 |
|
p+1使得地址从 C8 –> C9 只加了1
sizeof(char)=1,所以说当给指针+1时,其实是加上了sizeof(所指向的数据的类型)
q+1使得地址从 d0 –> d4 却加了4
sizeof(int)=4
*(p+n) <–> ac[n] 二者是等价的
指针计算
这个写个运算是可以对指针做:
1. 给指针加、减一个整数(+、+=、-,-=)
1. 递增递减(++/--)
*p++
- 取出p所指的那个数据来,完事之后顺便把p移到下一个位置去
- *的优先级虽然高,但是没有++高
- 常用于数组累的连续空间操作
- 在某些cpu上,这可以直接被翻译成一条汇编指令
指针比较
- <, <=, ==, >, >=, != 都可以对指针做
- 比较的是它们在内存中的地址
- 数组中的单元的地址肯定是现行递增的
5. 动态内存分配
- 输入数据
- 如果输入数据时,先告诉你个数,然后再输入,要记录每个数据
- C99可以用变量做数组定义的大小,C99之前呢?
- int *a = (int *) malloc(n * sizeof(int)); 动态内存分配
- 动态内存分配来实现数组大小可变(C99之前)
1 |
|
malloc
#include<stdlib.h>
void* malloc(size_t size);
向malloc申请空间的大小是以字节为单位的
返回的结果是void*,需要类型转换为自己需要的类型
(int*)malloc(n * sizeof(int));
free( )
- 把申请得来的空间还给系统
- 申请过的空间,最终都应该要还
- 只能还申请来的空间的首地址
1
2
3
4
5
6
7
8
9
10
11
int main(){
void *p
int cnt = 0;
p = malloc(100*1024*1024);
p++;
free(p); //会报错 p 此时已经不是当初申请的那个地址来
return 0;
}
作者: Lee