C语言基础
C语言基础
第一周:
解释型语言与编译型语言:
- 语言本无解释与编译之分,只是常用的执行方式而已
- 解释型语言有特殊的计算能力
- 编译型语言有确定的运算性能
- 编译型语言相对较快,目前已不是主要差异
为什么学C:
- C语言是众多高级语言的基础,一通百通
- 运用领域广
- 更贴近操作系统底层
简单历史:
C语言用在哪里:
操作系统
嵌入式系统
驱动程序
底层驱动
图形引擎、图像处理、声音效果
第一个C语言程序:
1 |
|
第二周:计算
一、变量:
1 |
|
1 |
|
1 |
|
常量:
1 | const int NAME = 100; //常量一般为全大写 |
输入输出函数
1 | //对于整数 |
注意事项
- C语言中两个整数的运算结果只能是整数,会自动忽略小数位,如10/3*3=9
- 出现在scanf里的东西一定是要你输入的东西
- 10和10.0在C语言中是完全不同的数
- 当浮点数和整数放到一起运算时,C语言中会将整数转换为浮点数,然后进行浮点数的运算
浮点数
即带小数点的数值。浮点本意是指小数点是浮动的,是计算机内表达非整数(包含分数和无理数)的一种方式。
二、表达式:
运算符优先级:
注意事项:
赋值也是运算,也有结果
如a=6的结果是a被赋予的值,也就是6
a=b=6 —> a=(b=6)
交换变量:
1 |
|
注意事项:
- 程序是依次执行的
复合赋值和递增递减:
复合赋值
1 | total += (sum+ 100)/2; . |
递增递减
1 | “++”和“-”是两个很特殊的运算符,它们是单目运算符,这个算子还必须是变量。这两个运算符分别叫做递增和递减运算符, |
第三周:判断
if…else…条件语句:
1 | if (条件) { |
注意事项
- else总是和最近的那个if匹配
关系运算符:
- 所有关系运算符的优先级比算术运算符低,但比赋值运算符高
- 判断是否相等的运算符 == 和!= 的优先级比其他的关系运算符低,二连续的关系运算是从左到右进行的
Switch-case语句:
1 | switch ( 控制表达式 ){ |
注意事项
- 控制表达式只能是整数型结果
- 常量可以是常数,也可以是常数计算表达式,即运行时能得到结果的
- switch语句可以看作是一种基于计算的跳转,计算控制表达式的值后,程序会跳转到相匹配的case (分支标号)处。分支标号只是说明switch内部位置的路标,在执行完分支中的最后一条语句后,如果后面没有break,就会顺序执行到下面的case里去,直到遇到一个break,或者switch结束为止。
- 一般来说,每一个case都会配一个break
1 | switch ( type ){ |
第四周:循环
while语句:
1 | while ( 条件 ){ |
do…while循环:
1 | do{ |
注意事项
- 循环体内要有能改变条件的机会否则为死循环
第五周:循环控制
for循环:
1 | for(初值条件;继续执行条件;每轮执行完后执行语句){ |
如求阶乘
1 |
|
注意事项
- for中的每一个表达式都是可以省略的 for(;条件;)== while(条件)
循环小tips:
- 如有固定次数,用for
- 如果必须执行一次,用do…while…
- 其他情况用while
循环控制:
break:直接跳出循环
continue:仅跳过这一轮循环剩下的语句直接进入下一轮
1 | // 输出前50位素数 |
1 | // 接力break |
goto语句:
1 |
|
循环应用:
前n项求和:
整数分解:
最大公约数:
第六周:数据类型
数据类型:
C语言的类型:
- 整数:char、short、int、long、long long
- 浮点数:float、double、long double
- 逻辑:bool
- 指针
- 自定义类型
sizeof 运算符:
- 给出某个类型或变量在内存中所占据的字节数
整数的内部表达:
- 计算机内部一切都是二进制:
- 18 –> 00010010
- 0 –> 00000000
- 如何表示负数?
- 仿照十进制,加上一个特殊符号表示正负
- 取中间的某一个数为0,以它作为正负的分界线
- 补码
补码
- 考虑-1,我们希望-1+1=0,如何做到?
- 0 -> 00000000
- 1 -> 00000001
- 11111111+00000001 -> 100000000
- 因为0-1 -> -1,所以-1=
- (1)00000000-00000001 -> 11111111(假设该数据类型为8位,则最起始多出来的1被舍去)
- 11111111被当做纯二进制看待时为255,而被当做补码时为-1
- 同理,对于-a,其补码就是0-a,实际是2^n-a,n是这种类型的位数
整数的输入输出:
- 只有两种形式:int 或 long long
- %d: int
- %u: unsigned
- %ld: long long
- %lu:unsigned long long
1 |
|
8进制和16进制
- 8进制以0开头,16进制以0x开头
- %o用于8进制,%x用于16进制
- 8进制与16进制只是如何把数字表达为字符串,与内部如何表达数字无关
- 16进制很适合表达2进制数据,因为4位2进制正好是一个16进制位
如何选择整数类型:无特别需要都用int
- 现代CPU普遍为32位和64位,一次内存读写就是一个int,一次计算也是一个int,选择更短的类型不会更快,甚至更慢
- 现代编译器一般会设计内存对齐,所以更短的类型实际在内存中也有可能占据一个int的大小(虽然sizeof告诉你更小)
- unsigned与否只是输出的不同,内部计算是一样的
浮点类型:
类型 | 字长(byte) | 范围 | 有效数字 | scanf | printf |
---|---|---|---|---|---|
float | 32 | ±(1.20x10^-38~3.40x10^38),0,±inf(∞),NAN | 7 | %f | %f,%e |
double | 64 | ±(2.2x10^-308~1.79x10^308),0,±inf(∞),NAN | 15 | %lf | %f,%e |
科学计数法
输出精度
- 在%和f之间加上.n可以指定输出小数点后几位,这样的输出是做四舍五入的
- printf(“%.3f\n”,-0.0049);
- printf(“%.30f\n”,-0.0049);
- printf(“%.3f\n”,-0.00049);
- 计算机是没有办法表示所有小数的,大多时候都是四舍五入有误差
输出超过范围的浮点数
1 |
|
浮点运算没有精度(在一定位数以后)
1 |
|
解决方法
- 作浮点数之间的比较一般使用 fabs(f1-f2) < 1e-n 【n根据实际情况选取,用一个很小的值和差值作比较】
tips
- 带小数点的字面量是double而非float,float需要用f或F后缀来表明
- 浮点数在计算时是由专门的硬件部件实现的(CPU集成),计算double和float所用的部件是一样的
- 没有特殊需要只使用double
- 现代CPU能直接对double做硬件运算,性能不会比float差,在64位的机器上,数据存储的速度也不比float慢
字符类型:
- char也是一种整数,一种特殊的类型:字符
- 用单引号表示的字符字面量:”a”,”1”
- “”也是一个字符
- printf和scanf里用%c来输入输出字符
逃逸字符:
- 用来表达无法打印出来的控制字符或特殊字符,它由一个反斜杠”\“开头,后面跟上另外一个字符,这两个字符合起来组成了一个字符
字符 | 意义 | 字符 | 意义 |
---|---|---|---|
\b | 回退一格 | \‘’ | 双引号 |
\t | 到下一个表格位 | \‘ | 单引号 |
\n | 换行 | \\ | 反斜杠本身 |
\r | 回车 |
tips
- \b 回退一格并不是指按一下删除键
- 制表位:每行的固定位置,并不是指向前进固定字符长度
类型转换:
自动类型转换
- 当运算符两边出现不一致的类型时,会自动转换为较大的类型
- 大的意思是指能表达的数的范围更大
- char -> short -> int -> long -> long long
- int -> float -> double
- 对于printf,任何小于int的类型会被转换为int,float会转换为double,但scanf不会,要输入short,用%hd
强制类型转换
- 要把一个量强制转换为另一个类型(通常是较小类型),用:(类型)值,比如:
- (int)3.2
- (short)32
- 要注意此时的安全性,小范围的变量不能总能够表达大的量:
- (short)32768
- 只是从那个变量计算出了一个新的类型的值(副本),它并不改变那个变量,无论是值还是类型都不改变
- 强制类型转换的优先级高于四则运算
其他运算:
bool类型:
- #include <stdbool.h>
- 之后就可以使用bool,true和false
与或非逻辑运算:
逻辑运算是对逻辑量进行的运算,结果只有0或1
逻辑量是关系运算或逻辑运算的结果
运算符 描述 示例 ! 逻辑非 !a && 逻辑与 a && b || 逻辑或 a || b
表达区间
- 如x∈(4,6):x>4 && x<6
- 像4<x<6这样的式子,不是C 能正确计算的式子,因为4<x的结果是一个逻辑值(0或1)
- 判断大小写字母:c >= “A” && c <= “Z”
优先级
优先级 | 运算符 | 结合性 |
---|---|---|
1 | () | 从左到右 |
2 | ! + - ++ – | 从右到左(单目的+和-) |
3 | * / % | 从左到右 |
4 | + - | 从左到右 |
5 | < <= > >= | 从左到右 |
6 | == != | 从左到右 |
7 | && | 从左到右 |
8 | || | 从左到右 |
9 | = += -= *= /= %= | 从右到左 |
短路
- 逻辑运算是自左向右进行的,如果左边的结果已经能决定结果了,则不会进行右边的运算
- 因此不要把赋值,包括复合赋值组合进表达式
条件运算和逗号运算:
条件运算
- count = (count > 20) ?count - 10: count +10,括号中为条件,冒号前为满足条件时候的值,后者反之
- 条件运算符优先级高于赋值运算符,但是低于其他运算符
- 条件运算符自右向左结合的,可以嵌套(十分不推荐使用)
逗号运算
- 目前基本只用于for循环中:for(i = 0,j = 10;i < j;i++, j–)….
第七周:函数
函数的定义和使用:
函数定义、调用:
定义
调用
- 函数名(参数值)
- ()起到了表示函数调用的作用,没有参数也要加
- 参数要注意数量以及顺序
没有返回值的函数
- void 函数名(参数表)
- 不能使用带值的return,也可以没有return
- 调用的时候不能做返回值的赋值
函数的参数和变量:
函数原型:
- 函数头,以分号结尾,就构成了函数的原型(声明)
- 目的是为了告诉编译器该函数的名称,参数以及返回类型
- 原型里可以不写参数的名字,但一般建议写上
参数传递:
- 如果函数有参数,调用函数时必须传递给它数量、类型正确的值
- 可以传递给函数的值是表达式的结果,包括:
- 字面量
- 变量
- 函数的返回值
- 计算的结果
参数类型不匹配
- 调用函数时给的值与参数的类型不匹配是C语言传统上最大的漏洞
- 编译器总是悄悄替你把类型转换好,但是这很可能不是你所期望的
- 后续的语言,C++/Java在这方面很严格
传过去的是什么
- 对于函数参数表中的参数,叫做“形式参数”,调用函数时给的值,叫做“实际参数
- 我们更习惯理解为他们是参数和值的关系
- 每个函数都有自己的变量空间,参数也位于这个独立的空间中,和其他函数没有关系
本地变量:
- 函数每次运行,就产生一个独立的变量空间,在这个空间中的变量,是函数此次运行所独有的,称作本地变量
- 定义在函数内部的变量就是本地变量
- 参数也是本地变量
- 每进入一个函数,就进入该函数的变量空间,只能调用该空间里的变量
变量的生存期和作用域
- 生存期:什么时候这个变量开始出现了,到什么时候它消亡了
- 作用域:在(代码的)什么范围内可以访问这个变量(这个变量可以起作用)
- 对于本地变量,,这两个问题的答案是统一的:大括号内—-即‘块’
本地变量的规则
- 本地变量是定义在块内的
- 它可以是定义在函数的块内
- 也可以定义在语句的块内
- 甚至可以随便拉一对大括号来定义变量
- 程序运行进入这个块之前,其中的变量不存在,离开这个块,其中的变量就消失了
- 块外面定义的变量在里面仍然有效
- 块里面定义了和外面同名的变量则掩盖了外面的
- 不能在一个块内定义同名的变量
- 本地变量不会被默认初始化
- 参数在进入函数的时候被初始化了
函数庶事:
函数没有参数时
- void f(void);【建议写法】
- 直接表示没有参数
- 还是?
- void f();
- 在传统C中,它表示f函数的参数表未知,并不表示没有参数
逗号运算符
- 调用函数时的逗号和逗号运算符怎么区分?
- 调用函数时的圆括号里的逗号是标点符号,不是运算符
- f(a,b)
- f((a,b))【再加一层括号则为逗号运算符】
关于main函数
- int main()也是个函数
- 要不要写成int main(void)?
- return的0有人看吗?
- Windows : if error level 1…
- Unix Bash : echo $?
- Csh : echo $status
第八周:数组
数组:
数组的使用:
何为数组
- 是一种容器(放东西的东西):
- 其中所有的元素具有相同的数据类型
- 一旦创建,不能改变大小
- 数组中的元素在内存中是连续依次排列的
定义数组
- <类型> 变量名称[元素数量];
- int grades[100];
- double weight[20];
- 元素数量必须是整数
- C99之前:元素数量必须是编译时刻确定的字面量,目前已可以使用变量定义数组的大小
- int a[10];
- 一个int的数组
- 10个单元:a[0],a[1],…,a[9]
- 每个单元就是一个int类型的变量
- 可以出现在赋值的左边或右边:a[2] = a[1]+6;
- 在赋值左边的叫做左值
数组的单元
- 数组的每个单元就是数组类型的一个变量
- 使用数组时放在[]中的数字叫做下标或索引,下标从0开始计数
有效的下标范围
- 编译器和运行环境都不会检查数组下标是否越界,无论是对数组单元做读还是写旦程序运行,越界的数组访问可能造成问题,导致程序崩溃:segmentation fault
- 但是也可能运气好,没造成严重的后果
- 所以这是程序员的责任来保证程序只使用有效的下标值:[0,数组的大小-1]
长度为0的数组
- int a[0];
- 可以存在,但是无用
数组例子:
- 写一个程序,输入数量不确定的[0,9]范围内的整数,统计每种数字出现的次数,输入-1表示结束
1 |
|
数组运算:
数组的集成初始化
- int a[]={2,4,6,7,1,3,5,9,11,13,23,14,32};
- 直接用大括号给出数组的所有元素的初始值
- 不需要给出数组的大小,编译器替你数数
集成初始化时的定位
- int a[10] = { [0]=2, [2]=3, 6};
- 用[n]在初始化数据中给出定位
- 没有定位的数据接在前面的位置后面
- 其他位置的值补零
- 也可以不给出数组大小,让编译器算
- 特别适合初始数据稀疏的数组
数组的大小
- sizeof给出整个数组所占据的内容的大小,单位是字节
sizeof(a) / sizeof(a[0]) - sizeof(a[0])给出数组中单个元素的大小,于是相除就得到了数组的单元个数
- 这样的代码:一旦修改数组中初始的数据,不需要修改遍历的代码
数组的赋值
- int a[10]={1,2,3,4,5,6,7,8,9,0}; 【错误写法】:int b[]=a;
- 数组变量本身不能被赋值
- 要把一个数组的所有元素交给另一个数组,必须采用遍历
遍历数组
- 通常都是使用for循环,让循环变量i从0到<数组的长度,这样循环体内最大的i正好是数组最大的有效下标
- 常见错误:
- 循环结束条件是<=数组长度
- 离开循环后,继续用i的值来做数组元素的下标!
tips
- 数组作为函数参数时
- 不能在[]中给出数组的大小,往往需要用另一个参数来传入数组的大小
- 不能再利用sizeof来计算数组的元素个数
数组例子:
- 构造n以内的素数表
- 1、令x为2
- 2、将2x、3x、4x直至ax<n的数标记为非素数
- 3、令x为下一个没有被标记为非素数的数,重复2;直到所有的数都已经尝试完毕
1 |
|
二维数组:
- int a[3] [5]
- 通常理解为a是一个3行5列的矩阵
二维数组的遍历
1 | for (i=0;i<3;i++){ |
- a[i] [j]是一个int,表示第i行第j列上的单元
- 不能写成a[i,j],根据逗号运算符,这表示a[j]
二维数组的初始化
1 | int a[][5] = { |
- 列数是必须给出的,行数可以由编译器来数
- 每行一个{},逗号分隔
- 最后的逗号可以存在,有古老的传统
- 如果省略,表示补零
- 也可以用定位 ( C99 ONLY)
第九周:指针
指针:
取地址符:
运算符 &
- scanf(“%d”, &i);里的&
- 获得变量的地址,它的操作数必须是变量
- int i; printf(“%x”,&i);
- 地址的大小是否与int相同取决于编译器
- int i; printf(“%p”,&i);
- &不能对没有地址的东西取地址
- &(a+b)
- &(a++)
- &(++a)
指针变量:
scanf
如果能够将取得的变量的地址传递给一个函数,能否通过这个地址在那个函数内访问这个变量?
scanf(“%d”, &i);
scanf()的原型应该是怎样的?我们需要一个参数能保存别的变量的地址,如何表达能够保存地址的变量?
指针
- 就是保存地址的变量,变量的值是内存的地址
1 | int i; |
- 普通变量的值是实际的值
- 指针变量的值是具有实际值的变量的地址
作为参数的指针
- void f(int *p);
- 在被调用的时候得到了某个变量的地址:
- int i=0; f(&i);
- 在函数里面可以通过这个指针访问外面的这个i
访问某地址的变量
*是一个单目运算符,用来访问指针的值所表示的地址上的变量
可以做右值也可以做左值
- int k = *p;
- *p = k+1;
左值之所以叫左值
- 是因为出现在赋值号左边的不是变量,而是值,是表达式计算的结果:
- a[0] = 2;
- *p = 3;
- 是特殊的值,所以叫做左值
- 是因为出现在赋值号左边的不是变量,而是值,是表达式计算的结果:
指针的运算符& *
指针的使用:
应用场景一:交换两个变量的值
1 | void swap(int *pa,int *pb){ |
应用场景二:
- 函数返回多个值(return 只能返回一个值),某些值就只能通过指针返回
- 传入的参数实际是需要保存带回的结果的变量
应用场景二+:
- 函数返回运算的状态,结果通过指针返回
- 常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错:
- -1或0(在文件操作会看到大量的例子)
- 但是当任何数值都是有效的可能结果时,就得分开返回了
1 | int divide(int a,int b,int *result); |
tips
- 指针未初始化时,里面为随机地址值,此时不能作*p=123的操作,因为你不知道p具体指向内存中的什么位置,很有可能导致程序崩溃
指针与数组:
- 函数参数表中的数组实际是指针
- sizeof(a) == sizeof(int*)
- 但是可以用数组的运算符[]进行计算
数组变量是特殊的指针
- 数组变量本身表达地址,所以
- int a[10];int *p =a; 无需用&取地址
- 但是数组的单元表达的是变量,需要用&取地址
- a = &a[0]
- []运算符可以对数组做,也可以对指针做
- p[0] <==> a[0]
- *运算符可以对指针做,也可以对数组做:
- *a = 25;
- 数组变量是const的指针,所以不能被赋值
- int a[] <==> int *const a = …
指针与const:
当指针是const
- 表示一旦得到了某个变量的地址,不能再指向其他变量
- int *const q = &i; // q是const
- *q = 26; //OK
- q++; //ERROR
当指针指向一个const
- 表示不能通过这个指针去修改那个变量(不能使那个变量成为const,即并不是说i不可更改,除非i定义为const)
- const int *p = &i; // 并不是说i为const
- *p =26; //ERROR! ( * p)是const
- i = 26; //OK
- p = &j; //OK
examples
- 判断哪个被const了的标志是const在*的前面还是后面
- const在*前面则所指的东西不能被修改(即前两种情况一样)
- const在*后面则指针不能被修改
转换
const 数组
- const int a[] = {1,2,3,4,5,6};
- 数组变量已经const 的指针了,这里的const表明数组的每个单元都是const
- 所以必须通过初始化进行赋值
保护数组值
- 因为把数组传入函数时传递的是地址,所以那个函数的内部可以修改数组的值
- 为了保护数组不被函数破坏,可以设置参数为const
- int sum(const int a[],int length);
指针运算:
指针运算:
给一个指针加1表示要让指针指向下一个变量
- int a[10];
- int *p = a;
- *(p+1) -> a[1];
如果指针不是指向一片连续分配的空间,如数组,则这种运算没有意义
这些算术运算可以对指针做:
- 给指针加、减个整数(+,+=,-,-=)
- 递增递减(++/–)
- 两个指针相减 // 地址差除以sizeof,即中间存在的该类型的单元数
*p++
- 取出p所指的那个数据来,完事之后顺便把p移到下个位置去
- *的优先级虽然高,但是没有++高
- 常用于数组类的连续空间操作
- 在某些CPU上,这可以直接被翻译成一条汇编指令
指针比较
- <,<=,==,>,>=,!=都可以对指针做
- 比较它们在内存中的地址
- 数组中的单元的地址肯定是线性递增的
0地址
- 当然你的内存中有0地址,但是0地址通常是个不能随便碰的地址
- 所以你的指针不应该具有0值
- 因此可以用0地址来表示特殊的事情:
- 返回的指针是无效的
- 指针没有被真正初始化(先初始化为0)
- NULL是个预定定义的符号,表示0地址
- 有的编译器不愿意你用0来表示0地址
- 进程运行时,操作系统会为其分配虚拟内存空间,所有程序运行时都认为自己有从0开始的一片连续地址空间
指针的类型转换
- void* 表示不知道指向什么类型东西的指针
- 计算时与char*相同(但不相通)
- 指针也可以转换类型
- int * p=&i; void* q =(void*)p;
- 这并没有改变p所指的变量的类型,而是让后人用不同的眼光通过p看它所指的变量
- 我不再当你是int啦,我认为你就是个void!
总结:指针作用
- 需要传入较大的数据时用作参数
- 传入数组后对数组做操作
- 函数返回不止一个结果
- 需要用函数来修改不止个变量
- 动态申请的内存.
动态内存分配:
malloc
- #include <stdlib.h>
- void* malloc(size_t size);
- 向malloc申请的空间大小是以字节为单位的
- 返回的结果是void*,需要类型转换为自己需要的类型
- (int* )malloc(n* sizeof(int)
- 申请失败则返回0,或者叫做NULL
free()
- 把申请得来的空间还给“系统’
- 申请过的空间,最终都应该要还
- 只能还申请来的空间的首地址
- free(NULL)没有问题,因为0地址不可能是malloc得来的,free()接受到参数NULL 则什么事都不做,因此一旦定义一个指针就最好赋值为NULL,假如此后由于某些原因没有malloc,可以直接free(p),相当于free(NULL),不会报错
1 | int main(){ |
常见问题
- 申请了没有free –> 长时间运行内存逐渐下降
- 新手:忘了
- 老手:找不到合适的free时机
- free过了再free
- 地址变过了,直接free
第十周:字符串
字符串:
字符串:
字符数组
- 这不是C语言的字符串,因为不能用字符串的方式做计算
字符串
- 末尾的’\0’就是数字0 的意思,可以直接替换为0
- 字符串定义
- 以0(整数0)结尾的一串字符
- 0或”0’’是一样的,但是和’0’不同
- 0标志字符串的结束,但它不是字符串的一部分
- 计算字符串长度的时候不包含这个0
- 字符串以数组的形式存在,以数组或指针的形式访问
- 更多的是以指针的形式
- string.h 里有很多处理字符串的函数
- 以0(整数0)结尾的一串字符
字符串变量
字符串常量
总结
- C语言的字符串是以字符数组的形态存在的
- 不能用运算符对字符串做运算
- 通过数组的方式可以遍历字符串
- 唯一特殊的地方是字符串字面量可以用来初始化字符数组
- 以及标准库提供了一系列字符串函数
字符串变量:
字符串常量
1 |
|
- char *s=”Hello,world!”;
- s 是一个指针,初始化为指向一个字符串常量
- 由于这个常量所在的地方,实际上s是const char*s ,但是由于历史的原因,编译器接受不带const的写法
- 但是试图对s所指的字符串做写入会导致严重的后果
- 如果需要修改字符串,应该用数组定义:
- char s[]=”hello”;意思是我这个字符串就在本地变量s这里
- 而char *s意思是我要指向某个地方(程序代码段)的字符串