C语言基础

第一周:

解释型语言与编译型语言:

image-20220211121636024

  • 语言本无解释与编译之分,只是常用的执行方式而已
  • 解释型语言有特殊的计算能力
  • 编译型语言有确定的运算性能
  • 编译型语言相对较快,目前已不是主要差异

为什么学C:

  • C语言是众多高级语言的基础,一通百通
  • 运用领域广
  • 更贴近操作系统底层

简单历史:

C语言用在哪里:

  • 操作系统

  • 嵌入式系统

  • 驱动程序

  • 底层驱动

  • 图形引擎、图像处理、声音效果

第一个C语言程序:

1
2
3
4
5
6
#include <stdio.h>

int main(){
printf("hello,world!\n");
return 0;
}

第二周:计算

一、变量:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
int price = 0;
printf("请输入金额:(元)");
scanf("%d",&price); // & 取址符 f:format
int change = 100 - price;
printf("找您%d元\n",change);

return 0;
}
1
2
3
4
5
6
7
8
9
#include <stdio.h> 
int main(){
int i; //未赋初值的变量所在内存原本为什么值则为什么值
int j;

j=i+10 ;
printf("%d",j);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main()
{
printf("请分别输入身高的英尺和英寸,"
"如输入\"5 7\"表示5英尺7英寸:");
double foot;
double inch;
scanf("%lf %lf", &foot, &inch);

printf("身高是%f米。\n",
((foot + inch / 12) * 0.3048));

return 0;
}

常量:

1
2
const int NAME = 100;  //常量一般为全大写
--const为修饰符,指明该变量不可被修改

输入输出函数

1
2
3
4
5
6
//对于整数
printf("%d %d",a,b);
scanf("%d %d",&a,&b);
//对于浮点数
printf("%f %f",a,b);
scanf("%lf %lf",&a,&b);

注意事项

  • C语言中两个整数的运算结果只能是整数,会自动忽略小数位,如10/3*3=9
  • 出现在scanf里的东西一定是要你输入的东西
  • 10和10.0在C语言中是完全不同的数
  • 当浮点数和整数放到一起运算时,C语言中会将整数转换为浮点数,然后进行浮点数的运算

浮点数

即带小数点的数值。浮点本意是指小数点是浮动的,是计算机内表达非整数(包含分数和无理数)的一种方式。

二、表达式:

运算符优先级:

image-20220211151713032

注意事项:

  • 赋值也是运算,也有结果

  • 如a=6的结果是a被赋予的值,也就是6

  • a=b=6 —> a=(b=6)

    image-20220211151940402

交换变量:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
int a = 1;
int b = 2:
c = a;
a = b;
b = c;

return 0;
}

注意事项:

  • 程序是依次执行的

复合赋值和递增递减:

复合赋值

1
2
3
4
5
total += (sum+ 100)/2; .
total = total + (sum+ 100)/2;
total* = sum+12;
total = total*(sum+12); .
total /= 12+6;

递增递减

1
2
3
4
5
6
7
8
9
“++”和“-”是两个很特殊的运算符,它们是单目运算符,这个算子还必须是变量。这两个运算符分别叫做递增和递减运算符,
他们的作用就是给这个变量+1或者-1
count++;
count += 1;
count= count + 1;
前缀和后缀:
● ++和--可以放在变量的前面,叫做前缀形式,也可以放在变量的后面,叫做后缀形式。
● a++的值是a加1以前的值,而++a的值是加了1以后的值,无论哪个,a自己的值都加了1了。
即前后缀的表达式值不同

第三周:判断

if…else…条件语句:

1
2
3
4
5
6
7
if (条件) {
...
}else if (条件){ //if和else后的大括号都可以省略,但仅有一句有效
...
}else{
...
}

注意事项

  • else总是和最近的那个if匹配

关系运算符:

image-20220211165130380
  • 所有关系运算符的优先级比算术运算符低,但比赋值运算符高
  • 判断是否相等的运算符 == 和!= 的优先级比其他的关系运算符低,二连续的关系运算是从左到右进行的

Switch-case语句:

1
2
3
4
5
6
7
8
9
10
11
12
switch ( 控制表达式 ){
case 常量:
...
break;
case 常量:
...
break;
...
...
default:
...
}

注意事项

  • 控制表达式只能是整数型结果
  • 常量可以是常数,也可以是常数计算表达式,即运行时能得到结果的
  • switch语句可以看作是一种基于计算的跳转,计算控制表达式的值后,程序会跳转到相匹配的case (分支标号)处。分支标号只是说明switch内部位置的路标,在执行完分支中的最后一条语句后,如果后面没有break,就会顺序执行到下面的case里去,直到遇到一个break,或者switch结束为止。
  • 一般来说,每一个case都会配一个break
1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch ( type ){
case 1:
case 2:
printf("你好\n");
break;
case 3:
printf("晚上好\n"); //若type=3,则会输出“晚上好”和“再见”才结束
case 4:
printf("再见\n");
break;
default:
printf("啊,什么啊? \n");
break;
}

第四周:循环

while语句:

1
2
3
while ( 条件 ){
...
}

do…while循环:

1
2
3
do{
...
}while ( 条件 );

注意事项

  • 循环体内要有能改变条件的机会否则为死循环

第五周:循环控制

for循环:

1
2
3
for(初值条件;继续执行条件;每轮执行完后执行语句){
...
}

如求阶乘

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
int fact = 1;
int i;
for (i = 1;i < 6 ;i++){
fact *= i;
}
printf("%d",fact);
return 0;
}

注意事项

  • for中的每一个表达式都是可以省略的 for(;条件;)== while(条件)

循环小tips:

  • 如有固定次数,用for
  • 如果必须执行一次,用do…while…
  • 其他情况用while

循环控制:

break:直接跳出循环

continue:仅跳过这一轮循环剩下的语句直接进入下一轮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 输出前50位素数
#include <stdio.h>
int main() {
int x;
int cnt = 0; //计数君
x = 1;

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",i);
if (cnt%5==0){
printf("\n");
}
}
x++;
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 接力break 
#include <stdio.h>
int main(){
int x;
int one, two, five;
int exit = 0;

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;
break; // break只能跳出当前最近的循环
}
}
if ( exit ) break;
}
if ( exit) break;
}
return 0;
}

goto语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
int main(){
int x;
int one, two, five;
int exit = 0;

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);
goto out;
}
}
}
}
out:
return 0;
}

循环应用:

前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
2
3
4
5
6
7
#include <stdio.h>
int main(){
char c = -1;
int i = -1;
printf("c=%u,i=%u\n",c,i);
return 0;
}

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
2
3
4
5
6
7
8
#include <stdio.h>
int main(){
printf("%f\n",12.0/0.0); --> inf(正无穷大)
printf("%f\n",-12.0/0.0); --> -inf(负无穷大)
printf("%f\n",0.0/0.0); --> nan(不存在的数)
return 0;
}
//若为整数会报错,±inf和nan只作为3种特殊的值定义在浮点数中

浮点运算没有精度(在一定位数以后)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(){
float a,b,c;
a=1.345f;
b=1.123f;
c = a + b;
if(c==2.468){
printf("相等");
} else{
printf("不相等,c=%.10f,或%f\n",c,c); ---> 不相等,c=2.46799993522.468000
} 所以基本不用浮点数作相等比较
return 0;
}

解决方法

  • 作浮点数之间的比较一般使用 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–)….

第七周:函数

函数的定义和使用:

函数定义、调用:

定义

image-20220306233225157

调用

  • 函数名(参数值)
  • ()起到了表示函数调用的作用,没有参数也要加
  • 参数要注意数量以及顺序

没有返回值的函数

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int main(){
const int length = 10; //定义数组大小
int a[10]; //定义数组
int i;

for (i=0;i<length;i++){
a[i]=0; // 初始化数组
}
scanf("%d",&i);
while (i != -1) {
if (i>=0 && i<=9) {
a[i] += 1;
}
scanf("%d",&i);
}
for (i=0;i<10;i++) {
printf("%d\n",a[i]); // 遍历数组输出
}
return 0;
}

数组运算:

数组的集成初始化

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

int main(){
const int maxNumber = 25;
int isPrime[maxNumber];
int i;
int x;
for(i=0;i<maxNumber;i++){
isPrime[i]= 1;
}
for (x=2;x<maxNumber; x++) {
if ( isPrime[x]) {
for (i=2;i*x<maxNumber;i++) {
isPrime[i*x] = 0;
}
}
}
for(i=2; i<maxNumber;i++) {
if ( isPrime[i] ) {
printf("%d\t",i);
}
}
printf("\n");
return 0;
}

二维数组:

  • int a[3] [5]
  • 通常理解为a是一个3行5列的矩阵

二维数组的遍历

1
2
3
4
5
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],根据逗号运算符,这表示a[j]

二维数组的初始化

1
2
3
4
int a[][5] = {
{0,1,2,3,4},
{2,3,4,5,6},
};
  • 列数是必须给出的,行数可以由编译器来数
  • 每行一个{},逗号分隔
  • 最后的逗号可以存在,有古老的传统
  • 如果省略,表示补零
  • 也可以用定位 ( 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
2
3
4
int i;
int* p = &i;
int* p,q; // 后两种写法意思相同,p是一个指针指向一个int变量,q为一个int变量,并不会因为星号的位置改变
int *p,q; // c语言没有int*这种类型,而是*p,若要表示q也为指针,则改为*q
  • 普通变量的值是实际的值
  • 指针变量的值是具有实际值的变量的地址

作为参数的指针

  • void f(int *p);
  • 在被调用的时候得到了某个变量的地址:
    • int i=0; f(&i);
  • 在函数里面可以通过这个指针访问外面的这个i

访问某地址的变量

  • *是一个单目运算符,用来访问指针的值所表示的地址上的变量

  • 可以做右值也可以做左值

    • int k = *p;
    • *p = k+1;
  • 左值之所以叫左值

    • 是因为出现在赋值号左边的不是变量,而是值,是表达式计算的结果:
      • a[0] = 2;
      • *p = 3;
    • 是特殊的值,所以叫做左值

指针的运算符& *

  • 互相反作用

    image-20220308222416126

指针的使用:

应用场景一:交换两个变量的值

1
2
3
4
5
void swap(int *pa,int *pb){
int t = *pa;
*pa = *pb;
*pb = t;
}

应用场景二:

  • 函数返回多个值(return 只能返回一个值),某些值就只能通过指针返回
    • 传入的参数实际是需要保存带回的结果的变量

应用场景二+:

  • 函数返回运算的状态,结果通过指针返回
  • 常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错:
    • -1或0(在文件操作会看到大量的例子)
  • 但是当任何数值都是有效的可能结果时,就得分开返回了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int divide(int a,int b,int *result);
int main(){
int a=6;
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;
}

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:

image-20220315094235855

当指针是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

image-20220315102712336

  • 判断哪个被const了的标志是const在*的前面还是后面
    • const在*前面则所指的东西不能被修改(即前两种情况一样)
    • const在*后面则指针不能被修改

转换

image-20220315105538855

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
2
3
4
5
6
7
8
9
10
int main(){
void *p;
int cnt = 0;
while ((p=malloc(100*1024*1024))){
cnt ++;
}
printf("分配了%d00MB的空间\n",cnt);

return 0;
}

常见问题

  • 申请了没有free –> 长时间运行内存逐渐下降
    • 新手:忘了
    • 老手:找不到合适的free时机
  • free过了再free
  • 地址变过了,直接free

第十周:字符串

字符串:

字符串:

字符数组

image-20220323163501987

  • 这不是C语言的字符串,因为不能用字符串的方式做计算

字符串

image-20220323163639690

  • 末尾的’\0’就是数字0 的意思,可以直接替换为0
  • 字符串定义
    • 以0(整数0)结尾的一串字符
      • 0或”0’’是一样的,但是和’0’不同
    • 0标志字符串的结束,但它不是字符串的一部分
      • 计算字符串长度的时候不包含这个0
    • 字符串以数组的形式存在,以数组或指针的形式访问
      • 更多的是以指针的形式
    • string.h 里有很多处理字符串的函数

字符串变量

image-20220323164944123

字符串常量

image-20220323165037138

总结

  • C语言的字符串是以字符数组的形态存在的
    • 不能用运算符对字符串做运算
    • 通过数组的方式可以遍历字符串
  • 唯一特殊的地方是字符串字面量可以用来初始化字符数组
  • 以及标准库提供了一系列字符串函数

字符串变量:

字符串常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main(){
int i=0;
char *s="hello";
char *s2="hello";

printf("%p\n",&i);
printf("%p\n",s);
printf("%p\n",s2);
printf("%p\n",&s); // 输出指针自身的地址
printf("%p\n",&s2);

return 0;
}

image-20220323172336258

  • char *s=”Hello,world!”;
  • s 是一个指针,初始化为指向一个字符串常量
    • 由于这个常量所在的地方,实际上s是const char*s ,但是由于历史的原因,编译器接受不带const的写法
    • 但是试图对s所指的字符串做写入会导致严重的后果
  • 如果需要修改字符串,应该用数组定义:
    • char s[]=”hello”;意思是我这个字符串就在本地变量s这里
    • 而char *s意思是我要指向某个地方(程序代码段)的字符串

字符串输入输出:

字符串数组以及程序参数: