程序设计基础

本文只是在考研复试中进行的复习,对程序设计基础的知识进行整理的笔记。

第一章

程序的样子

一个程序主要构成如下所示

1
2
3
4
5
6
#include <stdio.h>
int main()
{
printf("hello,world!");//这是注释
return 0;
}

其中#include <stdio.h>.h是头文件,头文件是包含函数声明和定义的文件。一般写程序时用到的一些函数如printf()都是在这个文件里面被声明好的。而#include是编译预处理指令,就是在编译前将stdio.h这个文件里的函数都添加到你写的cpp文件中,然后参与编译,生成.obj文件。

如果没有这个指令,你用到的printf()scanf()编辑器就会报错

int main()是主函数。其中int表示函数的返回值类型,表示该主函数的返回值是一个int类型的值。main表示主函数,是C语言约定的程序执行入口,其标准的定义格式为int main(int argc, char *argv[])。如果括号里面没有东西,如int main()即等价于int main(void)

{}是c语言的一个重要组成部分,{}将代码进行打包,即将花括号里面的内容打个包包起来,使之形成一个整体,并与外界绝缘。这样就方便程序员了解哪些语句是哪部分的。

//两个斜杠表示注释,这部分的内容不会被编译器所执行,可以方便对代码进行文字说明,让自己或者其他人阅读了解这是干嘛用的

return 0是一个是函数的返回值,函数中要写明return 0,表示返回。因为函数(除void)在遇到return时才能返回。在函数中不一定要有这个,但是在主函数中一定要有,因为它代表程序结束。

;表示着一个语句的结束,记住每句话后面加个;就好了(分支语句和循环语句不要加

转制

十进制转二进制

除以2逆向取余数,直到商为0

如:37

37/2=18….1

18/2=9

9/2=4…..1

4/2=2

2/2=1

1/2=0……1

所以最终是00100101

二进制转十进制

如101

就是

1
1*2^2+0*2^1+1*2^0

所以为5

八进制转二进制

每一个八进制位转换成3哥二进制位,左边不足3个的则补0

如:567->101 110 111

234->010 011 100

二进制转八进制

从右向左,每三个二进制位为一组,每一组转成一个八进制数

如:

011 010 111 100 110->32746

011 010 111 101 110->32756

011 101 110 101 011->35653

十六进制转二进制

每1个十六进制位转换成4个二进制位,左边不足4个的则补0

如:

109CBE->0001 0000 1001 1100 1011 1110

5A7DF8->0101 1010 0111 1101 1111 1000

二进制转十六进制

从右往左,每4个2进制位为一组,每一组转换成1个十六进制数

如:

1110 1101 1111 1001 1110-> EDF9E

1011 0010 0101 1000 1100->B258C

原码、反码、补码

数据在计算机里面都是以补码的形式进行存储的。

举个例子:

2

这个数字的原码如下:

0000 0010

反码如下:

0000 0010

补码如下:

0000 0010

再举个例子:

-2

原码如下:

1000 0010

反码如下:

1111 1101

补码如下:

1111 1110

如此可以看出,正数的原码、反码、补码都是一样

而负数中的反码为:在原码的基础上“符号位不变,数值位取反

补码为:在反码的基础上“符号位不变,数值位+1”(如上面的1111 1101,+1后,最右边就从01变成10,因为倒数第二位是0,后面1+1后,他变成了1,没有在多一位,所以前面的1就不用继续加,所以最终的补码为1111 1110)

(符号位指最左边的第一位,既最高位,如果是1则为负数,为0则为正数)

第二章

变量

什么是变量?

变量就是一个数值能够发生改变的量

(就像打游戏赢了能上的分一样,存储那个分的东西就叫一个变量,这一把我们score=90,下一把打完就变成105,这个score就是变量)

数据类型

c语言中数据类型主要分为两种,一种是基本类型,另一种是构造类型

基本类型

基本类型主要分为字符型(char),整型(int),实型(又称浮点型)【其中又分为单精度实型(float),双精度实型(double)和长双精度实型(long double)】

构造类型

构造类型主要是数组类型,结构类型(struct),联合类型(union),枚举类型(enum)

除以上两个之外,数据类型还有指针类型空类型(即无值类型)

字符型(char)

字符型是一个特殊的类型,它占据1个字节(8bit或1byte)

有符号字符类型:signed char/char(最高位为符号位)

取值范围为1000 0000(-128)——0111 1111(127)

无符号字符类型:unsigned char(没有符号位,都是数值位)

取值范围:0000 0000(0)——1111 1111(255)

整型(int)

有符号的整型:signed int/int(占据4byte,即32位)

取值范围:1000 0000 0000 0000 0000 0000 0000 0000——0111 1111 1111 1111 1111 1111 1111 1111

无符号的整型:unsigned int(占据4byte)

取值范围:0000 0000 0000 0000 0000 0000 0000 0000——1111 1111 1111 1111 1111 1111 1111 1111

short int:占据2byte

取值范围:1000 0000 0000 0000(-32768)——0111 1111 1111 1111(32768)

unsigned short int:占据2byte

取值范围:0000 0000 0000 0000(0)——1111 1111 1111 1111(65535)

long int:占据4byte

long long int:占据8byte

浮点型(float)

没有符号的浮点数数据(实际上就是小数,如1.9,2.0)

float:单精度浮点型,占据4byte,精准到6位小数

double:双单精度浮点型,占据8byte,精准到15位小数

long double:长双单精度浮点型,占据8byte或16byte,精准到15位小数

(PS:一般如果要求输出是2个小数的话,会在输出处进行处理,如:%2f【即控制输出为float类型两位小数】)

变量的定义

定义语法

类型名 变量名 ,变量名,变量名;

类型名 变量名=初值,变量名=初值;

1
2
3
4
int a,b,c;
char a;
char abc = 'abc';
int a=0,b=1,c=2;

PS:注意每一行的结束后面要有;,这个分号一定是英文的,不能是中文的。

变量的命名规则

变量名一般以字母、数字、下划线构成,且第一个字符不能是数字,同时变量名不能是系统占用的单词,如不能是int,char这些。(变量区分大小写字母,所以a变量和A变量不是同一个变量)

还有,变量需要先定义才能使用

1
2
3
4
int a;
a = 11;
a++;
printf("%d",a);

而不能

1
2
3
a = 15;
int a;
printf("%d",a);

上面这个是错误的。因为编译器是从上往下进行读取的,它先扫到第一行,读取到a=15,但是a是什么它并不清楚,于是它就报错:a未命名(a不知道是什么东西)。

PS:以上为C语言的情况,实际上大多数语言都是这样的,但也有例外,如python语言里就不需要声明一个变量,可以直接用,如

1
2
a = 15
print(a)

上面这个是正确的。

变量的内存结构

变量是占据内存的,占据的大小由变量的数据类型来决定。

char a = 10;

这一句里面,a变量占据的内存大小为10

变量a占据的1byte
0 0 0 0 1 0 1 0
变量的读写

变量本质就是一块内存空间,变量名就是这块空间的名字。所以实际上变量的读写就是对内存空间的读写。

1
2
3
4
int a,b;
a = 15;
b = a;
printf("%d",b);

如这段代码,首先定义了两个变量a,b,然后给a变量赋值15,这时就会把15写入a的内存块中。

然后b先是读取a的数值,然后再把这个数值赋值给b,把这个15写入b的内存块中。

1
2
3
4
5
6
7
int main()
{
char a = 170;
printf("%d",a);

return 0;
}

这段代码中,最终输出的a并不是170,为什么呢?因为溢出了。变量char的取值范围是-127—127,而我们的存储的值是170,大于127,因此就会造成内存溢出,输出的数值就不是我们存储的数值了。所以如何我们要输出170,只要把char改为int就行了。

PS:关于上面输出为啥是-86,实际上这跟计算机的底层有关。计算机的底层是以二进制补码形式存储的,取数时会还原成原码,因为char占据1byte(字节),也就是八位,其中最高位为符号位,所以把170转成二进制就是1010 1010,而正数的原码反码补码是一样的,但这个时候最高位为1,机器会读取为负数,所以就成了-86.

第三章

运算符与表达式

运算符就是+-*/%这几个,表达式则是由操作数和运算符组成的式子

1
2
a-b
1+2

以上两个就是表达式

PS:在C语言里面,单一的一个变量或者单一的一个常量是最简单的表达式。

优先级与结合性

在程序运算过程中,表达式中的运算符是具有优先级的。

优先级的排序如下

算术运算符>关系运算符>赋值运算

优先级 运算符 结合方向 优先级 运算符 结合方向
1 [] () . -> ++ –(后) 从左到右 9 ^ 从左到右
2 -(负)(类型)++ –(前) * & ! ~ sizeof 从右到左 10 | 从左到右
3 / * % 从左到右 11 && 从左到右
4 + - 从左到右 12 || 从左到右
5 << >> 从左到右 13 ?: 从右到左
6 > >= < <= 从左到右 14 = /= *= %= += -= <<= >>= &= ^= |= 从右到左
7 == != 从左到右 15 , 从左到右
8 & 从左到右

其中一些运算符的含义如下表所示

运算符 名称或含义 使用形式 说明
[] 数组下标 数组名[整型表达式]
() 圆括号 (表达式)/函数名(形参表)
. 成员选择(对象) 对象.成员名
-> 成员选择(指针) 对象指针->成员名
- 负号运算符 -算术类型表达式 单目运算符
(type) 强制类型转换 (纯量数据类型)纯量表达式 单目运算符
++ 自增运算符 ++纯量类型可修改左值表达式 单目运算符
自减运算符 –纯量类型可修改左值表达式 单目运算符
* 取值运算符 *指针类型表达式 单目运算符
& 取地址运算符 &表达式 单目运算符
! 逻辑非运算符 !纯量类型表达式 单目运算符
~ 按位取反运算符 ~整型表达式 单目运算符
sizeof 长度运算符 sizeof表达式【还有一种是sizeof(类型)】
/ 表达式/表达式 双目运算符
* 表达式*表达式 双目运算符
% 取模(余数) 整型表达式%整型表达式 双目运算符
+ 表达式+表达式 双目运算符
- 表达式-表达式 双目运算符
<< 左移 整型表达式<<整型表达式 双目运算符
>> 右移 整型表达式>>整型表达式 双目运算符
> 大于 表达式>表达式 双目运算符
>= 大于等于 表达式>=表达式 双目运算符
< 小于 表达式<表达式 双目运算符
<= 小于等于 表达式<=表达式 双目运算符
== 等于 表达式==表达式 双目运算符
!= 不等于 表达式!=表达式 双目运算符
& 按位与 整型表达式&整型表达式 双目运算符
^ 按位异或 整型表达式^整型表达式 双目运算符
| 按位或 整型表达式|整型表达式 双目运算符
&& 逻辑与 表达式&&表达式 双目运算符
|| 逻辑或 表达式||表达式 双目运算符
?: 条件运算符 表达式1?表达式2:表达式3 三目运算符
= 赋值运算符 可修改左值表达式=表达式
/= 除后运算符 可修改左值表达式/=表达式
*= 乘后运算符 可修改左值表达式*=表达式
%= 取模后运算符 可修改左值表达式%=表达式
+= 加后运算符 可修改左值表达式+=表达式
-= 减后运算符 可修改左值表达式-=表达式
<<= 左移后运算符 可修改左值表达式<<=表达式
>>= 右移后运算符 可修改左值表达式>>=表达式
&= 按位与后运算符 可修改左值表达式&=表达式
^= 按位异或后运算符 可修改左值表达式^=表达式
|= 按位或后运算符 可修改左值表达式|=表达式
, 逗号运算符 表达式,表达式 从左向右顺序结合
1
2
3
4
5
6
7
PS:
单目运算符:运算对象只有一个,如++
双目运算符:运算符两侧各有一个运算对象,如+
三目运算符:运算对象有三个,如?:
?:的例子如下:
表达式1?表达式2:表达式3
10>15?10:15

总结

1.单目运算>算数运算单目运算>算术运算>关系运算>位运算>逻辑运算>条件运算>赋值运算(算术>关系>逻辑>赋值);

2.位运算中的按位求反~有单目运算的高优先级,左移<<和右移<<运算优先级高于关系运算,按位与&高于按位异或^高于按位或|

3.逻辑与运算高于逻辑或运算;

4.条件运算符?:结合顺序为从右到左,先解决冒号两边的条件运算再解决最左边的问号所处的条件运算。

下面举个例子

1
1+2*3-4/2

上面这个先算2*3,然后再算4/2,之后再算1+6-2,原因是因为*/的优先级比+-要高,所以先算乘法和除法,再算加法和减法

1
(1+2)*3-4/2

而这个,则是先算1+2,然后再算3*34/2,最后再算9-2。因为()的优先级比*/的要高。

各种运算符的介绍

算术运算符

+-*/

只要有一个运算对象是浮点型的,则整个表达式的值为double型

1
2
3
4
5
float a = 0,c = 0;
int d = 5;
a = 7.5;
c = 5.0/2;
printf("%f",c);

输出为2.500000

1
2
3
4
5
float a = 0,c = 0;
int d = 5;
a = 7.5;
c = 5/2;
printf("%f",c);

输出为2.000000

为什么同样是5除以2,一个是2.500000,一个却是2.000000呢?

其实是因为第一个是5.0/2,5.0是小数,所以他会显示2后面的小数。而5/2,5是整数,就不会显示2后面的小数之所以输出是2.000000,是因为我们用的输出是%f。为什么不用%d呢,因为printf/scanf 等函数由于使用了可变参数列表,编译器无法验证参数的类型,比如第一个参数格式是%d,那么不论你传入的是什么类型,统统都按i整数处理。此处推荐这篇文章

%

如果算术运算符为%,则%的两个运算对象必须都是整数,而且表达式结果的正负由被除数决定

1
2
3
4
int a = 10,c = 0;
int d = 5;
c = a%2;
printf("%d",c);

输出为0

1
2
3
4
int a = 10,c = 0;
int d = 5;
c = d%2;
printf("%d",c);

输出为1

1
2
3
4
int a = 10,c = 0;
int d = 5.3;
c = d%2;
printf("%d",c);

这串代码直接报错

1
2
3
4
int a = 10,c = 0;
int d = -5;
c = d%2;
printf("%d",c);

输出为-1

1
2
3
4
int a = 10,c = 0;
int d = -5;
c = d%-2;
printf("%d",c);

输出为-1

++和–

++和–是自增和自减

1
2
3
int a = 1;
a++;
printf("%d",a);

输出为2

这里有一个难点,就是++(或者–)的使用地方。

如果是a++

那就是先读取a,然后再对a+1

如果是++a

则是先a+1,然后再读取a

–同理

关系运算符

> < == >= <= !=

关系运算表达式的值为“真”或“假”时用“1”或者“0”来表示。

1
2
3
4
int a = 1;
int b = 2;
printf("%d\n",a>b);
printf("%d\n",a<b);

第一个输出为0(a>b是假的,所以为0)

第二个输出为1(a<b是真的,所以为1)

逻辑运算符

&&

逻辑运算表达式的值为“真”或“假”,用1或者0表示。

PS:在&&情况下,参与运算的对象只要不为0,则运算对象为真。即只要有一个条件是假的,那么整个结果都是假的

1
2
3
4
5
int a = 1;
int b = 2;
printf("%d\n",a > b && b < a);
printf("%d\n",a > b && b > a);
printf("%d\n",a < b && b > a);

第一个输出为0(a>b是假的,b<a也是假的,所以输出直接是0)

第二个输出为0(a>b是假的,b>a是真的,但因为中间的逻辑运算符是&&,可以翻译成且,则整句话可以看成a>b且b>a是否是真的。所以第一个是假的那后面的不管是真是假,整个表达式都算是假。所以输出是0)

第三个输出为1(a<b是真的,b>a是真的,所以输出是1)

||

跟&&差不多,但&&是且的意思,而||则是或的意思

PS:在||情况下,只要有一个条件是真的,那么整个结果都是真的。即只有两个都是假的,结果才是假的。

1
2
3
4
5
int a = 1;
int b = 2;
printf("%d\n",a > b || b < a);
printf("%d\n",a > b || b > a);
printf("%d\n",a < b || b > a);

第一个输出是0(a>b是假的,b<a也是假的,所以输出直接是0)

第二个输出是1(a>b是假的,b>a是真的,但因为中间的逻辑运算符是||,可以翻译成或,整句话的意思就是a>b或者b>a是否为真。所以因为第二个b>a是真,所以整句话可以看成是真,因此输出为1)

第三个输出是1(a<b是真的,b>a是真的,所以输出是1)

PS:如果对真假表示很难理解的话,可以考虑把真假替换成对错,意思也是一样的,而且用对错去理解可能比用真假的理解要好,不过官方术语是不用对错而是用真假的。

赋值运算符

简单的赋值运算符:=

用法:变量 = 表达式

1
a = 45;

复合的赋值运算符:+= -= /= *= %=

sizeof(表达式)

sizeof(表达式)结果为表达式的数据类型占据的字节数

1
printf("%d",sizeof(int));

输出为4

PS:为了保证运算的精度,计算机会自动转向精度高的数据类型进行转换,如char一定会转成int,float会转成double

1
2
3
char a = 1;
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a+1));

第一个输出为1(因为a是char类型,char占一个字节,所以输出为1)

第二个输出为4(a虽然是char类型,占据一个字节,但是后面的1是int类型,占据四个字节,所以计算机会自动转向精度高的数据类型进行转换,因此输出为4)

1
2
char a = 1;
printf("%d\n",sizeof(a+1.2));

输出为8(因为1.2是float类型)

第四章

算法的基本构成

算法的基本构成分为三种,分别是顺序结构选择结构循环结构。其中选择结构也称为分支结构。

顺序结构

顺序结构就是按语句书写的先后顺序依次执行的结构。

1
2
3
int a = 1;
a++;
printf("%d",a);

可以看到上面的代码就是一个顺序结构,它按照语句的书写从上往下依次进行执行,先定义一个变量a,然后对a的数值进行+1,最后输出a

复合语句

由花括号(也叫大括号)中的0个或多个声明和语句列表共同构成。

选择结构

根据条件判断来选择执行哪一条语句。

其中有if条件语句switch多路分支语句

if语句
1
2
3
4
int a = 1;
if(a>2){
printf("YES");
}

上面是一个典型的if语句。

当语句来到if的时候,它会进行判断括号里的条件是真还是假。如果是真,则执行花括号里的内容。然后继续执行下面的语句。

1
2
3
4
5
6
int a = 1;
if(a>2){
printf("YES");
}
a++;
printf("%d",a);

输出为2。因为a>2是假,因此if里面的输出不执行,继续执行下面的a++以及输出,因此输出为2.

if else语句
1
2
3
4
5
6
7
int a = 1;
if(a>2){
printf("YES");
}
else{
printf("NO");
}

输出为No

这是一个典型的if else语句。程序来到if的时候会判断括号里面的条件表达式是真还是假,如果是真就执行if花括号里面的内容(printf("YES");),如果是假则执行下面else的花括号里面的内容(printf("NO");)

1
2
3
4
5
6
7
int a = 3;
if(a>2){
printf("YES");
}
else{
printf("NO");
}

输出为YES。

PS:如果if的括号里面的条件表达式是真,则执行完if的花括号里面的内容后会直接跳过下面else语句。

if else if 语句
1
2
3
4
5
6
7
8
9
10
int a = 1;
if(a>2){
printf("1");
}
else if(a<0){
printf("2");
}
else{
printf("3");
}

执行过程:依次判断条件表达式的值,当某个值为真时,则执行相应的语句,然后跳出整个if语句之外,继续执行后面的程序。如果所有的表达式都为假,则执行最后的else花括号里的内容,然后继续执行后面的程序。

switch语句
1
2
3
4
5
6
int a = 2;
switch(a/2){
case 1:printf("1");break;
case 2:printf("2");break;
default:printf("NO!");
}

输出为1

1
2
3
4
5
6
int a = 4;
switch(a/2){
case 1:printf("1");break;
case 2:printf("2");break;
default:printf("NO!");
}

输出为2

执行过程:首先计算表达式(即括号内的表达式)的值,与常量表达式(即case 1,case 2)进行比较,如果与其中一个常量表达式的值相等,就执行其后的语句,直到遇到break才结束。如果case后面没有break,则继续执行case后面的所有语句。如果没有找到与表达式的值匹配的常量表达式,则执行default后的语句。

1
2
3
4
5
6
int a = 10;
switch(a/2){
case 1:printf("1");break;
case 2:printf("2");break;
default:printf("NO!");
}

输出为NO

1
2
3
4
5
6
int a = 2;
switch(a/2){
case 1:printf("1");
case 2:printf("2");
default:printf("NO!");
}

输出为12NO

PS:什么是常量表达式呢?其实就是字面意思,常量。换句话说就是这里是填写常量的。如case 2case 3,不能填写case a

条件表达式

表达式1?表达式2:表达式3

1
2
3
int a = 1;
int b = 3;
printf("%d",a?2:b);

输出为3

这里的意思是a是不是2,如果不是2那就输出b,如果是2那就输出a

换句话来说就是表达式1是否跟表达式2相等,如果相等就输出表达式1,否则就输出表达式3.

循环结构

循环结构主要分为for循环while循环以及do while循环

while循环

执行过程:只有循环条件表达式的值为真才执行循环语句,先判断后执行,直到循环条件表达式的值为假,才跳出循环,继续执行后面的语句。(如果一开始就为假,就直接跳出循环)

1
2
3
4
5
int a = 3;
while(a>0){
a--;
printf("%d",a);
}

输出为210

上面的代码先判断a>0是否为真,是真就执行花括号里面的内容。然后他会一直执行循环的内容,直到a>0为假。

PS:记得在循环里面对条件表达式的判断变量进行增或减,否则如果不做改变的话它会进入一个死循环。

整个while循环可以用下面几行字来概括需要做些什么

  1. 定义变量i和n(也可以是其他)
  2. 输入n的值
  3. 循环语句(循环要做的事情)
  4. 思考循环的开始条件:例如,i从1开始
  5. 思考循环的结束条件:例如i++,i>n
  6. 循环的条件:例如i<=n
1
2
3
4
5
6
7
int i,n;//定义变量i和n
i = 1//输入值
n = 100;
while(i<=n){//循环的条件
printf("111");//循环语句
i++;
}
do while循环

执行过程:先执行循环体语句,再检查循环条件的值是否为真,如果为真则继续执行循环体语句,否则结束循环。

1
2
3
4
5
int a = 3;
do{
a--;
printf("%d",a);
}while(a>0);

输出为210

PS:记得最后的while那里需要加;

do while循环至少无条件会进行一次循环体语句,而while不是。

for循环
1
2
3
for(int i=0;i<n;i++){
printf("%d",i);
}

for循环主要的语句如下

for(表达式1;表达式2;表达式3){

循环语句;

}

其中表达式1为设置初始条件,这个条件只会执行一次,可以为0个或者多个变量设置初值。

表达式2是循环条件表达式,用来判断是否继续循环。在每次执行循环体之前,它都会执行一遍表达式2,然后再决定是否执行循环体语句。

表达式3是作为循环的调整,比如循环体变量是否增加或者减少,这个表达式是在循环体语句执行后才执行的。

for( : : )这个等价于while(1)

PS:

  1. for循环里面的三个表达式并不是必须的。
  2. 当表达式2的值为假时,for循环就会结束
  3. 可以在循环体内执行break,continue,goto语句
  4. 表达式2是空的,这说明表达式2的值一直为真,然后就会进行死循环

中断语句

break语句:跳出本层循环,执行循环后的语句

continue语句:跳出本次循环,继续执行下一次循环

goto语句:跳出到指定的标号位

1
2
3
4
5
6
int i = 13;
while(i>10){
printf("123");
i--;
break;
}

输出为123

1
2
3
4
5
int i = 13;
while(i>10){
printf("123");
continue;
}

输出为123123123

1
2
3
4
5
6
7
8
9
10
int n;
pos_1:
printf("请输入一个正整数:");
scanf("%d",&n);
if(n<0)
{
printf("输入错误!\n");
goto pos_1;
}
printf("成功输入正整数:%d\n",n);

输出结果如下:

1
2
3
4
5
6
请输人一个正整数:-2
输入错误!
请输入一个正整数:-6
输入错误!
请输入一个正整数:3
成功输入正整数:3

第五章

什么是函数

函数是一个个特定任务的语句集合,它能够完成你所想要的某种特定任何或者功能。

简单地说你有一个代码,里面有一个功能是乘法功能。因为你整个代码要用很多次这个功能,你如果每次用的时候都去写一个这个功能进去,就十分麻烦,而函数就是你写了一个乘法功能的函数,在你的代码里要使用这个功能时,直接调用这个函数就可以了,不需要再费神地去重复一遍这个功能,同时在后期维护或者修改的时候也很方便。

函数的好处

  1. 减少程序的代码量
  2. 代码复用
  3. 使程序具有良好的结构

函数的样子

上面提过int main()是主函数,其实这个就是一个函数,当然,main()也可以不是函数,这个就不展开解释了。

1
2
3
4
5
6
7
8
9
10
int abc(){
int i =2;
i++;
return i;
}

int main(){
printf("%d",abc());
return 0;
}

输出为3

其中int abc()就是一个函数,而main()里面使用的abc()就是调用这个函数。

实际上abc()的括号里面还可以加入参数。

1
2
3
4
5
6
7
8
9
10
11

int abc(int i ){
i++;
return i;
}

int main(){
int i=2;
printf("%d",abc(i));
return 0;
}

输出为3

当然,函数也可以放在主函数后面,不过这时就要在主函数前面声明这个函数

1
2
3
4
5
6
7
8
9
10
11
12
int abc(int i);

int main(){
int i=2;
printf("%d",abc(i));
return 0;
}

int abc(int i ){
i++;
return i;
}

函数的定义

函数包括两部分,一个是函数头,另一个是函数体

它的形式如下所示

返回值数据类型 函数名(数据类型 形参1,数据类型 形参2,……)//函数头

{

//函数体(可以有多条语句)

}

PS:这里的函数不一定最后都需要return。

1
2
3
4
5
6
7
8
9
10
11
void abc(int i);

int main(){
int i=2;
abc(i);
return 0;
}

void abc(int i ){
printf("%d",i);
}

输出为2

这里因为返回值数据类型为void,void的意思是空的,因此不需要return语句。

具体的void使用方法可以看这里

PS:

  1. 函数的返回值和参数可以是任意类型,包括空类型。当函数无返回值或无形参列表时,建议用void指明
  2. 函数名是一种标识符,必须符合标识符的命名规则。
  3. 函数的参数可以没有,也可以有多个
  4. 函数中不能带有另外的函数

关于第四点,给个例子

1
2
3
4
5
6
int fun(){
void abc(){
printf("hello world");
}
return 0;
}

这样写会报错

1
2
3
4
5
6
int main(){
void abc(){
printf("hello world");
}
return 0;
}

这样也是会报错的

函数的调用

函数的调用十分简单,它的语法如下:

函数名(实际参数1,实际参数2,......)

1
2
3
4
5
6
7
8
9
10
int abc(int i ){
i++;
return i;
}

int main(){
int i=2;
printf("%d",abc(i));
return 0;
}

这里的main()主函数里面的abc(i)就是调用abc()这个函数,而i则是实际的参数。

一般函数设置了几个参数,那么调用函数的时候里面的实际参数也要是这么多个。

PS:

  1. 如果A函数调用B函数,则A函数叫做主调函数,B叫被调用函数。上面的例子中main()就是主调函数,abc()就是被调函数
  2. 如果函数返回类型是void,那么函数中的return可以省略

函数的调用过程如下所示

  1. 找到主函数,从主函数{后面执行
  2. 程序执行到了abc(这是个例子)函数时,就会去调用abc()
  3. 程序跳到abc(),从{开始执行
  4. 输出
  5. 运行到return,将要返回的值返回到主函数,同时在主函数abc()的位置继续往下执行
  6. 直到运行到主函数的return 0,整个程序才会结束。

函数的声明

函数的声明一般都是放在头文件后面的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
int abc(int i);
void aaa();


int main(){
int i=2;
printf("%d",abc(i));
return 0;
}

int abc(int i ){
i++;
return i;
}

void aaa(){
printf("hello world");
}

函数声明的好处有一下几点

  1. 函数列表更加一目了然,方便使用
  2. 便于多人合作共同开发
  3. 可以用于商业,保护知识产权(因为一般只会把调用方法告诉别人,而函数中具体的算法会被隐藏起来)

第六章

什么是数组

数组就是一些列具有相同类型的数据的集合,这些数据在内存中依次挨着存放,彼此之间没有缝隙。(这个相同类型中的类型,可以是基本的数据类型,也可以是构造类型

1
int a[10];

这就是一个数组,其大小为10。

数组可以如下图所示,每一个空格都可以存放一个数据。

数组的定义

数组的定义十分简单,语法如下:

数据类型 变量名[大小];

其有几种定义方式,如下所示

1
2
3
4
int a[10];
int a[10] = {1,2,3,4,5,6,7,8,9,0};
int a[10] = {0};
int a[10] = {1,2,3};

第一个是定义了一个int类型的a数组,大小为10

第二种是定义了一个int类型的a数组,并且为其分配了每个元素的具体数据

第三种是定义了一个int类型的a数组,并且为其初始化了所有元素为0

第四种是定义了一个int类型的a数组,并且为其前面三个元素初始化数据为1,2,3

其中第一种定义是没有初始化数据的,而第四种虽然只是初始化了前三个元素,但实际上后面的元素它都是初始化为0.

当然,还有一种定义数组元素的方法

1
2
3
4
5
#define size 50
int main(){
int a[size];
return 0;
}

#define size 50是定义一个名为size的常量,它的数据是50。这样后面在数组的括号中就不用输入具体数字而是输入size了。

PS:

  • 数组中其实是可以输入变量的,前提是这个变量有具体值(即有定义),否则的话就会报错
  • 如果数组的初始化过程中,数组大小是5,初始化的元素却又6个,也是会报错的。它初始化可以少,但是不能多。
  • 数组的下标是从0开始的,即我定义了一个a[10],但是输出的时候它第一个是a[0]而不是a[1],同理最后一个是a[9]而不是a[10]

数据的存储

当定义一个数组a时,编译器根据指定的元素个数和元素的类型分配确定大小(元素类型大小×元素个数)的一块内存,并把这块内存的名字命名为 a,名字 a 一旦与这块内存匹配就不能再改变。其中,a[0]、a[1]、a[2]、a[3] 与 a[4] 都为 a 的元素,但并非元素的名字(数组的每一个元素都是没有名字的)

在 32 位系统中,由于 int 类型的数据占 4 字节单元,因此该数组 a 在内存中共占据连续的 4×5=20 字节单元,依次保存 a[0]、a[1]、a[2]、a[3] 与 a[4] 共 5 个元素。如果这里假设元素 a[0] 的地址是 10000,则元素 a[1] 的地址是 10000+1×4=10004; 元素 a[2] 的地址是 10000+2×4=10008; 元素 a[3] 的地址是 10000+3×4=10012; 元素 a[4] 的地址是 10000+4×4=10016。

总结一下数据的特点:

  • 索引从 0 开始
  • 数组在内存中占据连续的字节单元
  • 数组占据的字节单元数等于数组元素个数乘以该数组所属数据类型的数据占据的字节单元数(元素个数乘以元素类型大小)
  • 数组元素按顺序连续存放

PS:一个数组元素其实是可以存放多个数据的,但需要用到结构体。

数据的引用

实际上数据的引用上面已经有涉及地讲了,因此这里只是给出常用的数组遍历代码

1
2
3
4
int a[10];
for(int i=0;i<10;i++){
printf("%d",a[i]);
}

一维数组作为函数参数

数据作为函数参数可以理解为形参数组和实参数组共享一块内存空间,形参数据的长度可以省略

1
2
3
4
5
6
7
8
9
10
11
12
13
void abc(int a[], int n){
a[0]=10;
a[1]=11;//形参数组改变,实参数组值也会一起改变
}

int main(){
int a[50]={0};
abc(a,10);
for(int i=0;i<2;i++){
printf("%d",a[i]);
}
return 0;
}

输出为1011

什么叫形参数组改变,实参数组值也会一起改变呢。

1
2
3
4
5
6
7
8
9
10
11
12
13
void abc(int a){
a=2;
printf("%d",a);
}

int main(){
int a=4;
printf("%d",a);
abc(a);
printf("%d",a);

return 0;
}

这里的输出为424

可能有人好奇为什么不是422

因为第一个输出时a我们定义为4,所以输出为4

第二个输出是在abc()里面的输出,这时我们在abc()里面定义了a为2,所以输出是2

第三个输出是在主函数里面的,但是abc()里面定义的a只是在abc()里面定义了,而回到主函数里面,我们的a依旧还是4,所以输出是4

实际上这就是局部变量全局变量,具体可以看看这个文章

C语言全局变量和局部变量(带实例讲解)

总结一下:

  • 局部变量的有效作用范围巨岩在于所在函数的内部。形参是局部变量。
  • 除了作用于函数的局部变量外,C语言还允许定义作用于复合语句中的局部变量,其有效使用范围被局限于复合语句中,一般用作小范围内的临时变量。
  • 定义在函数外而不属于任何函数的变量成为全局变量。全局变量的作用范围是从定义开始到程序所在文件的结束,它对作用范围的所有函数都起作用。
  • 由于全局变量和局部变量的范围不同,允许它们同名。当某函数的局部变量与全局变量同名时,在该函数中全局变量不起作用,而全局变量起作用。对于其他不存在同名变量的函数全局变量仍然有效。同样,当函数局部变量与复合语句的局部变量同名时,以复合语句为准。

全局变量的定义如下所示

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int a,b;//全局变量
int abc(int a){
//TODO
}

int main(){
//TODO
return 0;
}

但并不建议过多使用全局变量。

而数组则不会发生这种情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void abc(int a[]){
a[0]=2;
printf("%d",a[0]);
}

int main(){
int a[10]={0};
a[0]=4;
printf("%d",a[0]);
abc(a);
printf("%d",a[0]);

return 0;
}

输出为422

第七章

二维数组的定义

二维数组的定义跟一维数组很像,语法如下

类型名 变量名[大小1][大小2];

同理,多维数组的语法如下:

类型名 变量名[大小1][大小2][大小3]......;

在二维数组中,大小1表示的是行数,大小2表示的是列数,具体表示如下表所示

a[0][0] a[0][1]
a[1][0] a[1][1]

二维数组的初始化

二维数组的初始化如下所示

1
2
int a[2][2] = {{1,2},{2,3}};
int a[2][2] = {1,2,2,3};

这两种初始化是等价的

同时可以只对部分元素赋值,未赋值的元素自动取“零”值

1
int a[3][3] = {{1}, {2}, {3}};

此时a数组的各个元素值如下

1 0 0
2 0 0
3 0 0

同样也可以这样赋值

1
int a[3][3] = {{0,1}, {0,0,2}, {3}};

此时a数组的各个元素值如下

0 1 0
0 0 2
3 0 0

如果要对全部元素进行赋值,则大小1可以不写

1
2
int a[2][2] = {1,2,2,3};
int a[][2] = {1,2,2,3};

以上两个数组是等价的

二维数组的存储

二维数组占用多个连续的内存空间。

总字节数=sizeof(数组类型)*行数*列数

int a[2][3]这个数据总字节数为4*2*3=24

计算元素a[i][j]在数组中位置的公式(从0开始)

i*m+j(m为数组的列数)

a[1][2]是在数组a中的第1*3+2=5位

二维数组的引用

方法同一维数组差不多,唯一区别是他是双下标a[1][2],而一维数组是单下标a[1]

二维数组的遍历

二维数组的遍历实际就是遍历一维数组,然后再遍历每个数组里的信息

所以一般使用两个for循环

1
2
3
4
5
6
int a[2][2] = {1,2,3,4};
for(int i=0;i<2;i++){
for(int j =0;j<2;j++){
printf("%d",a[i][j]);
}
}

输出为:1234

二维数组作为函数参数

二维数组作为函数参数跟一维数组作为函数参数基本一样

1
2
void abc(int a[2][3]);
void abc(int a[][3]);

这两个是等价的,但是二维数组作为函数参数是不能省略第二个的

1
void abc(int a[4][]);

这个是错误的

PS:数组名传递个给函数的是一个地址值,再函数中引用的将是实参的数组元素,编译器只为形参开辟一个存放地址的存储单元。

第八章

什么是结构体

在c语言中,我们可以自己构造出自己想要的数据类型,这种类型叫做复合数据类型。而结构体则是由一系列具有相同类型或不同类型的数据构成的数据集合

简单地说:结构体是一种可以由我们自己定义的数据类型

1
2
3
int age[2] = 10;//存放年龄
int num[2] = 001;//存放编号
int score[2] = 100;//存放成绩

这是一个存放学生成绩年龄和编号的代码,但是这样写过于繁琐。

于是我们改用结构体来定义一个数组,这个数组就能把这三个东西都存放进去。

1
2
3
4
5
6
7
8
9
10
11
struct student{
int num;
int age;
int score;
};
int main(){
struct student abc;
abc.age=10;
abc.num=001;
return 0;
}

这就是一个结构体,使用的时候只需要声明一个这个结构体的变量,通过.来进行更改里面的一些东西的值。

结构体的定义

语法如下

struct 结构体名{

成员列表

};

PS:

  • 定义一个结构体类型的关键字是struct,student只是结构体名,而这个结构体里面可以包括多个不同的基本数据类型,如int,float,char等。
  • 结构体需要先声明,后定义
  • 省略结构体名,直接定义结构体变量也是可以的,不过只能定义一次,接下来就不能再定义结构体变量了
  • 也可以在声明类型的同时,定义变量

关于三四点点的例子如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
//第三点
struct{
int age;
int num;
}abc;
//abc是结构体变量

//第四点
struct student{
int age;
int num;
}abc;
//其中abc为变量名。这个相当于struct student abc;

结构体访问

如果结构体是普通变量,则通过.来进行访问

1
abc.age;

如果结构体是指针变量,则通过->来进行访问

1
abc->age;

typedef的作用

typedef不是用来创造类型的,而是给已有的类型取一个别名

1
2
3
4
int a;//定义一个int类型的变量a
typedef int student;
student a;//定义一个int类型的变量a,只不过通过typedef对int的名字进行了一个别名话,这样的话我们就可以用student来代替int。
//所以student a等价于int a

在结构体上typedef经常被使用

1
2
3
4
5
6
7
8
9
10
11
typedef struct student{
int age;
int num;
}STU;//STU是struct student的别名

int main(){
struct student A;
STU B;
//这里的STU B其实是跟struct student A是等价的。在定义了别名后,原本的结构体名也可以继续使用
return 0;
}

结构体数组

结构体数组依旧是先声明后定义

语法如下

struct 结构体名{

数据类型 成员变量名;

数据类型 成员变量名;

……

};

struct 结构体名 数组名[长度大小];

1
2
3
4
5
6
7
8
9
struct student{
int age;
int num;
};//定义结构体

int main(){
struct student abc[100];//abc为结构体数组
return 0;
}

当然,也可以在声明的同时定义数组

1
2
3
4
struct student{
int age;
int num;
}abc[100];//定义结构体,其中abc为数组名

结构体数组初始化

语法:

struct 结构体名 数组名[长度] ={初值列表};

1
2
3
4
5
6
7
8
9
struct student{
int age;
int num;
};//定义结构体

int main(){
struct student abc[4] = {1,2,3,4},{1,2,3,4},{1,2,3,4};
return 0;
}

第九章

函数的递归调用

什么是递归

递归就是某一个函数可以直接或间接地自己调用自己

1
2
3
4
5
int abc(int i){
i++;
abc(i);
return i;
}

这就是一个递归,abc第一次执行到abc(i)后把算了一遍的i代入到abc()中重新算。而这个递归实际上是一个死循环来的,无限递归。

因此在使用递归的时候应该思考递归结束的条件,而不是像上面那样。

所以总结一下递归:

  1. 找到规律
  2. 递归调用的条件
  3. 结束递归调用的条件

所以,在使用递归的时候,一般都是配合if语句来控制的

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

输出为9

这样就不会出现无限递归的情况了。

作用域

作用域是一个代码空间,它主要分成几类,分别是文件作用域函数作用域语句块作用域结构体作用域

文件作用域:从文件头到文件尾的代码空间

函数作用域:函数的参数和函数体属于函数作用域,函数的返回值和函数名属于文件作用域

语句块作用域:在{}内的语句块属于语句块作用域

结构体作用域:在结构体内的属于结构体作用域

1
2
3
4
5
6
7
8
9
10
int main(){
int a = 2;
a++;
return 0;
}

int abc(int a){
a = 30;
return a;
}

上面代码中,main()里面的a只能在main()进行作用,而无法去到abc()中进行作用,这个在前面全局变量和局部变量中有讲过,所以main()里面的a的作用域就是main(),同理,abc()里面的a的作用域就是abc()。(即a只能在这片空间里其作用,其他空间不能)

同理,函数作用域就是只能在函数中使用,不能在函数外使用.(简单地说就是函数里面定义的变量,包括abc(int i)里面的那个i,都只能在这个函数中使用,不能在外面或者其他函数中使用。)

而在函数外面定义的变量,也就是全局变量,他的作用域是文件作用域,即整个文件都可以使用

1
2
3
4
5
6
7
8
9
10
11
int c = 10;//文件作用域
int main(){
int a = 2;//函数作用域
a++;
return 0;
}

int abc(int a){//a也是函数作用域
a = 30;
return a;
}

同理我们就能很好理解什么是语块作用域和结构体作用域了

1
2
3
4
5
6
7
8
9
int main(){
int a = 2;//函数作用域
a++;
{
int j = 0;//语句块作用域
j++;
}
return 0;
}

所以在作用域这里,最大的是文件作用域,其次是函数作用域,然后是语句作用域。

PS:

  1. 同一个作用域里,不能出现相同的标识符,同一个作用域内,不能重复定义变量名
  2. 一个标识符不能同时属于两个作用域,使用就近原则

存储类别

存储类别解决的是标识符的”生命周期“或者变量内存的开辟时间和销毁时间。

在学习这块知识的时候不要去考虑作用域,因为这两个没有关系

1
2
3
4
5
6
7
8
9
void abc(int a){
a = 30;
}
int main(){
int b = 10;
abc(b);
printf("%d",b);
return 0;
}

用上面的代码来讲解一下

我们一开始定义了一个变量b,赋值为10.这时候就开辟了一个内存空间b,存放10

这时我们调用abc,并把b作为参数传进去,这时就会开辟一个内存空间a,然后存放b的值10。然后执行abc的语句,对b的数值进行更改,更改为30,然后返回给main()。这时就会销毁这个内存空间a。(注意,销毁其实是指将内存还回去,也就是说其实他内存还在,只是没了权限而已。)

栈变量:auto声明的变量

变量内存开辟时间:栈变量是在变量定义语句时开辟内容

变量内存销毁时间:所在作用域结束作用后销毁

1
2
3
4
5
6
7
8
9
10
11
12
13
void abc(int a){
int b = 0;//开辟内存
{
int i = 0;//i开辟内存
}//i内存销毁
i = 10; //报错,因为i内存已经销毁
}//b内存销毁,a内存销毁

int main(){
foo(19);//a开辟内存
foo(30);//a重新开辟内存
return 0;
}

第十章

什么是指针

内存是有地址的,每一个字节都有一个唯一的整数作为编号,这个整数就是地址。这时我们将内存中字节的编号称为地址,也可以叫做指针。所以实际上指针上就是地址

一般我们获取地址都会使用&符号,这是一个取地址符号。

地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。

C语言用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供 CPU 使用。

数据和代码都以二进制的形式存储在内存中,计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据。

CPU 只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。如果程序不小心出错,或者开发者有意为之,在 CPU 要写入数据时给它一个代码区域的地址,就会发生内存访问错误。这种内存访问错误会被硬件和操作系统拦截,强制程序崩溃,程序员没有挽救的机会。

CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。

数据类型与内存大小之间的关系

char —>1byte

int —->4byte

float —->4byte

double —->8byte

指针常量的写法

1
2
3
4
5
6
7
8
9
int a = 0;
float f = 3.14f;
double d = 3.14;
char c = 'M';
&a;&f;&d;&c;//指针常量
printf("%d",a);
printf("%d",&a);
printf("%x",&a);
printf("%p",&a);

这四个输出,每次获得的内存都不一样,可能第一个输出时a变量的内存给的是A地址,那么下一个输出时给的地址可能就是B地址了。

PS:&a=&b是错误的,因为变量的地址是常量,是不能被修改的。

指针变量

1
2
3
4
5
int a ;//int是类型名,a是变量名
//定义了int类型的指针,这个变量p可以存放int类型的地址
int *p;//int *是类型名,p是变量名
a = 10;//把10存放到a的内存块中
p = &a;//把a的地址存放到p的内存中

按照上面的代码,如果这时写一句p=10,那么就会报错,报错内容是:不能将int类型的值分配到int *类型的实体

所以p不能存放整数,只能存放地址数据,同样,p也不能存放char *类型的地址。

指针变量的使用

1
2
3
4
5
6
7
8
9
int a = 10;
int *p;
p = a;
printf("%d",a);//输出为10
printf("%d",&a);//输出为a的地址
printf("%d",p);//输出为p中存储的a的地址
printf("%d",&p);//输出为p的地址
printf("%d",*p);//输出就是10
*p = 20;//相当于a=20,实际上这个算是间接访问,先是访问&a,然后再访问a中的值

指针的运算

基本规则:

int * —->4个byte

float* —->4个byte

double * —->8个byte

char * —->1个byte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
int main(){
int a = 10, *pa = &a, *paa = &a;
double b = 99.9, *pb = &b;
char c = '@', *pc = &c;
//最初的值
printf("&a=%#X, &b=%#X, &c=%#X\n", &a, &b, &c);
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//加法运算
pa++; pb++; pc++;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//减法运算
pa -= 2; pb -= 2; pc -= 2;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//比较运算
if(pa == paa){
printf("%d\n", *paa);
}else{
printf("%d\n", *pa);
}
return 0;
}

我们可以看一下这个代码

1
2
3
4
5
&a=0X1FB9319C, &b=0X1FB93190, &c=0X1FB9318F
pa=0X1FB9319C, pb=0X1FB93190, pc=0X1FB9318F
pa=0X1FB931A0, pb=0X1FB93198, pc=0X1FB93190
pa=0X1FB93198, pb=0X1FB93188, pc=0X1FB9318E
0

运行结果如上所示

这时我们会发现pa、pb、pc 每次加 1,它们的地址分别增加 4、8、1,正好是 int、double、char 类型的长度

但到了-2时,他们的地址却分别分别减少 8、16、2,正好是 int、double、char 类型长度的 2 倍

为什么不是-1而是减少2倍呢

实际上是这样的。

以 a 和 pa 为例,a 的类型为 int,占用 4 个字节,pa 是指向 a 的指针,如下图所示:

刚开始的时候,pa 指向 a 的开头,通过 *pa 读取数据时,从 pa 指向的位置向后移动 4 个字节,把这 4 个字节的内容作为要获取的数据,这 4 个字节也正好是变量 a 占用的内存。

如果pa++;使得地址加 1 的话,就会变成如下图所示的指向关系:

这个时候 pa 指向整数 a 的中间,*pa 使用的是红色虚线画出的 4 个字节,其中前 3 个是变量 a 的,后面 1 个是其它数据的,把它们“搅和”在一起显然没有实际的意义,取得的数据也会非常怪异。

如果pa++;使得地址加 4 的话,正好能够完全跳过整数 a,指向它后面的内存,如下图所示:

我们知道,数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加 1 就表示指向下一个元素,减 1 就表示指向上一个元素,这样指针的加减运算就具有了现实的意义

来源文章

所以实际上可以看成:int类型中,一共有四个格子(4byte),其中只有第一个格子是存放数据的,其余三个格子是0.

如果这个时候我们有一个int *p,然后有一个char ch=1;,然后p=ch;*p=20;虽然会报错,但是ch的值还是被更改成了20。可是这个时候,ch是1byte,但是p是int *,是4byte,所以他的前面会有三个格子,而且值也是0.但这个时候,这三个格子跟ch都没关系。所以这就是为什么会报错的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int a;
char ch;
float b;
double c;
char *p1;
int *p2;
double *p3;
float *p4;
printf("%d",sizeof(b));
printf("%d",sizeof(a));
printf("%d",sizeof(ch));
printf("%d",sizeof(c));
printf("%d",sizeof(p1));
printf("%d",sizeof(p2));
printf("%d",sizeof(p3));
printf("%d",sizeof(p4));

输出如下

1
2
3
4
5
6
7
8
4
4
1
8
4
4
4
4

为什么最后四个都是4呢?

因为p1,p2,p3,p4都是用来存放地址的,所以他的指针变量都是4.

1
2
3
int a;
printf("%d\n",&a);
printf("%d\n",&a+1);

输出如下

1826845660
1826845664

可以清晰地看到他+1后地址增加了4,也就是增加了一个int类型的大小,同理可以得到char是加1,float是加4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char a;
printf("%d\n",&a);
printf("%d\n",&a+1);
//输出
//-815422417
//-815422416

float a;
printf("%d\n",&a);
printf("%d\n",&a+1);
//输出
//-737165764
//-737165760

double a;
printf("%d\n",&a);
printf("%d\n",&a+1);
//输出
//364145640
//364145648

所以实际上地址+1,是整体+1(整体向后偏移了一个存储单元)

PS:如果是(*p)++,则实际上是a的值+1,其地址并不会发生偏移。如果是*(p++),则实际上是地址发生了偏移,而不是值+1

空指针和野指针

野指针

野指针指的是指针变量中的值是非法内存地址,进而形成也指针。值得注意的是,野指针并不是NULL指针,是指向不可用内存地址的指针。

而NULL指针并无危害,好判断好调试。

那么野指针是怎么来的呢?

  1. 局部指针变量没有初始化
  2. 指针所指向的变量在指针之前被销毁
  3. 使用已经释放过的指针
  4. 进行了错误指针运算
  5. 进行了错误的强制类型转换
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
char *a(){
char p[] = 'abc';
return p;//返回局部数组地址,导致野指针
}

int main(){
char *p = a();
return 0;
}

那么如何避免野指针呢?

  1. 绝对不要返回局部变量和局部数组的地址
  2. 任何变量在定义后记得进行初始化(赋值0)
  3. 字符数组必须确定0结束符后才能成为字符串
  4. 任何使用与内存操作相关的函数必须指定长度信息

空指针

空指针常量:不同的系统规定的不同,一般c中是0或者(void*) 0

为了避免系统不同带来的不便,一般会把空指针常量定义成宏的形式来实现空指针常量

如:

define NULL 0

PS

当 NULL 定义为“((void )0)”时,即 NULL 是可以赋值给任何类型指针的值,它的类型为 void,而不是整数 0,因此初始化“FILE*fp=NULL;”是完全合法的.而为了区分整数 0 和空指针 0,当需要其他类型的 0 时,即使可能工作,也不能使用 NULL,因为这样处理其格式是错误的,这种类型在非指针上下文中是不能工作的。特别需要注意的是,不能在需要 ASCII 空字符(NUL)的地方使用 NULL。

所以常数 0 是一个空指针常量,而 NULL 仅仅是它的一个别名。NULL 可以确保是 0,但空(null)指针却不一定。

指针变量作为函数参数

下面是一个简单的交换代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Swap(int a, int b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
return;
}

int main()
{
int i = 3, j = 5;
Swap(i, j);
printf("i = %d, j = %d\n", i, j);
return 0;
}

这个时候我们执行一下,会发现i和j还是3和5没有发生变化。为什么呢?

因为实参和形参之间的传递是单向的,只能由实参向形参传递。被调函数调用完之后系统为其分配的内存单元都会被释放。所以虽然将 i 和 j 的值传给了 a 和 b,但是交换的仅仅是内存单元 a 和 b 中的数据,对 i 和 j 没有任何影响。

所以如果想要直接对内存单元进行交换,那么就要使用指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Swap(int *p, int *q)
{
int tmp;
tmp = *p;
*p = *q;
*q = tmp;
return;
}

int main()
{
int i = 3, j = 5;
Swap(&i, &j);
printf("i = %d, j = %d\n", i, j);
return 0;
}

这样一改,就能交换i和j了。而此时实参向形参传递的不是变量 i 和 j 的数据,而是变量 i 和 j 的地址。其实传递指针也是拷贝传递,只不过它拷贝的不是内存单元中的内容,而是内存单元的地址

总结:

如果希望在另外一个函数中修改本函数中变量的值,那么在调用函数时只能传递该变量的地址。如果这个变量是普通变量,那么传递它的地址就可以直接操作该变量的内存空间

实际上在现实中一般都是传指针而不是传数据的,因为传指针的话可以节约内存,而传数据的话,如果数据很多则在拷贝的时候要给他们分配内存,这样就会消耗效率(浪费很多时间)

所以只有达到以下两个条件时,我们才会传数据而不是传指针

  • 数据很小,例如一个int类型变量
  • 不需要改变他的值,只是使用它的值

第十一章

数组的地址

c语言中,在函数体中和函数外部定义的数组名可以认为时一个存放地址值的指针变量名,其中地址值为数组第一个元素的地址(首地址),也就是数组所占一串连续存储单元的起始地址。

更重要的是:这个指针变量中的地址值是不可以改变的,也就是说不可以给数组名重新赋值,因此可以认为数组名是一个地址常量

1
2
3
4
int a[10];
int x;
a = &x;//报错,因此数组名为地址常量,不可以重新赋值。
a++;

其中,数组元素之间的地址是相连的

1
2
3
4
5
6
7
8
9
10
11
int a[5] = {1,2,3,4,5};
printf("%d\n",&a[0]);
printf("%d\n",&a[1]);
printf("%d\n",&a[2]);
printf("%d\n",&a[3]);
printf("%d\n",&a[4]);
printf("a代表的地址:%d\n",a);//数组名代表数组的首地址(首元素地址),即&a[0]
printf("a+1代表的地址:%d\n",a+1);
printf("a+2代表的地址:%d\n",a+2);
printf("a+3代表的地址:%d\n",a+3);
printf("a+4代表的地址:%d\n",a+4);

输出如下

1
2
3
4
5
6
7
8
9
10
-1907547856
-1907547852
-1907547848
-1907547844
-1907547840
a代表的地址:-1907547856
a+1代表的地址:-1907547852
a+2代表的地址:-1907547848
a+3代表的地址:-1907547844
a+4代表的地址:-1907547840

指针和一维数组

1
2
3
int a[5] = {10,20,30,40,50};
printf("%d\n",*(a+3));//这个*(a+3)是指a[3]的值
printf("%d\n",*a+3);//这个是a[0]+3

输出如下

1
2
40
13

PS:a=&a[3]是会报错的,因此数组名代表数组首元素的地址即&a[0],是一个常量,不可以被修改。

总结:

引用数组元素的方法有三种:

  • 数组名[下标]
  • *(数组名+下标)
  • *(指针变量+下标)

事实上,在c语言中,一对方括号[]并不是仅仅用来表示数组元素的记号,而是一种运算符。因此当p指向s数组的首地址时,表示数组元素s[i]的表达式应该有四种:

  1. s[i]
  2. *(s+i)
  3. *(p+i)
  4. p[i]

所以指针和数组的关系可以如下表所示

地址关系 下标法 数组名 指针法 下标法
&a[0] a p &p[0]
&a[1] a+1 p+1 &p[1]
内容关系 a[0] *a *p p[0]
a[1] *(a+1) *(p+1) p[1]

二维数组和指针

在c语言中,二维数组实际上是一个一维数组,而这个一维数组的每个元素又是一维数组

所以我们根据上文,能够得到二维数组各元素地址的关系

1
int a[3][4] = {{1,3,5,7},{9,11,13,15},{17,19,21,23}};

一维数组

a[0] 1 3 5 7
a[1] 9 11 13 15
a[2] 17 19 21 23

二维数组各元素地址的关系

a[0] a[0]+0 a[0]+1 a[0]+2 a[0]+3
a[1] a[1]+0 a[1]+1 a[1]+2 a[1]+3
a[2] a[2]+0 a[2]+1 a[2]+2 a[2]+3
a[0] *(a+0)+0 *(a+0)+1 *(a+0)+2 *(a+0)+3
a[1] *(a+1)+0 *(a+1)+1 *(a+1)+2 *(a+1)+3
a[2] *(a+2)+0 *(a+2)+1 *(a+2)+2 *(a+2)+3
1
2
3
4
5
6
7
8
//a[i][j]的地址可以有以下几种表达式:
&a[i][j]
a[i]+j
*(a+i)+j
&a[0][0]+4*i+j//4*i是第几行,j是第几列
a[0]+4*i+j
//&a表示二维数组地址,绑定一个二维数组;a表示首行地址,绑定一行;&a[0]表示首行地址,绑定一行;a[0]表示首行首元素地址,绑定一个元素;a[0][0]表示首 行首元素的值;&a[0][0]表示首行首元素的地址,绑定一个元素
//实际上就是通过计算首地址的偏移量来表达a[i][j]。

二维数组各元素内容的关系

a[0] *(a[0]+0) *(a[0]+1) *(a[0]+2) *(a[0]+3)
a[1] *(a[1]+0) *(a[1]+1) *(a[1]+2) *(a[1]+3)
a[0] *(*a+0)+0) *(*a+0)+1) *(*a+0)+2) *(*a+0)+3)
a[1] *(*a+1)+0) *(*a+1)+1) *(*a+1)+2) *(*a+1)+3)

同样,a[i][j]的内容可以有以下几种表达式

1
2
3
4
5
a[i][j]
*(a[i]+j)
*(*(a+i)+j)
*(&a[0][0]+4*i+j)
(*(a+i))[j]

行指针和列指针

行指针:存放一维数组首地址,绑定一行

列指针:存放数组元素的地址,绑定一个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//使用指针操作二维数组
#include <stdio.h>
#define M 3
#define N 4
int main(){
int a[M][N]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int *p = &a[0][0];
for(int i=0;i<M;i++){
for(int j=0;j<N;j++){
printf("%d",*(p++));
}
printf("\n");
}
return 0;
}

输入如下:

1
2
3
1234
5678
9101112
1
2
3
4
5
6
7
8
9
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int *p;//绑定一个变量的地址
int (*p2)[N];//定义了一个行指针,是指针变量,只能存放长度为N的int型数组的地址(绑定的是一行)
p = &a[0][0];
p=a[0];//相当于p=&a[0][0]
printf("%d\n",p);

p2 = a;//p2=&a[0],如果是p2=&a[0][0]则是错误的
printf("%d",p2);

输出如下

1
2
-1245692288
-1245692288

那么他们有什么区别了,我们继续看下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int *p;
int (*p2)[N];
p = &a[0][0];
p=a[0];
printf("p:%d\n",p);
p++;
printf("p:%d\n",p);

p2 = a;
printf("p2:%d\n",p2);
p2++;
printf("p2:%d",p2);

输入如下

1
2
3
4
p:-1040645104
p:-1040645100
p2:-1040645104
p2:-1040645088

我们可以++之前,p和p2都是指向a[0][0]的地址,但++之后,p偏移了一个,所以他指向下一个地址,但p2++并不是偏移到下一个(a[0][1])处,而是到了下一行,即a[1][0]。这也就是绑定一个跟绑定一行的区别

PS:如果是二维数组的话,下面这个代码用法是错误的。

1
2
3
4
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int *p;
p=a[0];
printf("%d",*(*(p+1)+1));

int a[][N],int a[M][N],int (*a)[N]这三个实际上是等价的,都是同一个东西

如果函数参数是指针数组,那么调用这个函数之前,一定要对这个指针数组进行初始化(即对所有元素赋值)

int **a,int *a[N],int *a[]这三个实际上是等价的

结构体指针

结构体指针访问结构体成员变量有以下两种方法:

结构体指针->成员变量;

(*结构体指针).成员变量;

结构体指针的定义方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct student{
int age;
int num;
}STU;

int main(){
//常用方法
STU a = {10,1};
a.num = 2;
//指针方法
STU *p;//结构体指针变量,存放STU型结构体变量的地址
p = &a;
p->num = 2;//相当于a.num=2;
(*p).num = 2;//相当于a.num=2;
//以前输入数据的时候一般都是这样写的
scanf("%d",&a.num);
//如果用结构体指针的话,就是这样了
scanf("%d",&p->num);

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct student{
int age;
int num[3];
}STU;

int main(){
STU a = {10,{1,2,3}};
STU *p;
p = &a;
a.num[0] = 4;
p->num[0] = 5;
(*p).num[0] = 6;

//如果是数组的情况
STU b[2] = {0};
p = b;//相当于p=&b[0]
b[0].num = 100;
p->num = 120;//相当于b[0].num=120;
p[0].num = 130;//相当于b[0].num=130
p++;//相当于p=p+1=&b[1];
return 0;
}

第十二章

什么是字符串

所谓的字符串就是由数字、字母、下划线组成的一串字符。其中又有字符常量和字符串常量

字符常量

字符常量:用一对单引号引起来的字符,要求单引号里面仅有一个字符,在内存中以ASCII码的形式存储

PS:

普通字符常量:注意单引号、双引号和斜杠三个特殊字符的写法'\\' '\" '\"'' 即必须在前面加\字符

转义字符常量:数值转义:'\xhh':表示后面跟1~2位16进制数,最大为'\FF''\ddd':ddd跟1~3位八进制数,不能超过unsigned char的最大值,因此最大为'\377'。转义字符:'\n','\t'

字符串常量:用一对双引号包括的字符,在内存中存放时,以'\0'为结束标识符(\0是数值转义字符,对应ASCII码的数值0)

1
2
3
char a = 65;
printf("%d\n",a);
printf("%c",a);

输出为

1
2
65
A

为什么第二个输出的是A呢?

%d是以十进制整数的形式输出,而%c是以字符的形式输出。这时我们需要去查一查一个名叫ASCII码的表,我们可以看到在那里65对应的是字符A。同理,如果是97,则输出的是a。那么这时我们对这个数++,他就会根据+1后对应的ASCII码进行输出

1
2
3
char a = 65;
a++;
printf("%c",a);

输出为B

如果我们a的值不是65,而是’A’呢?

1
2
3
char a = 'A';
printf("%d\n",a);
printf("%c",a);

输出为

1
2
65
A

他是没有任何变化的。

PS:char a =' ';这里是空格,如果我们用%d输出,他确实会输出空格,但如果用的是%c,则会输出32,因为空格这个字符的ASCII码中十进制就是32。但如果我们输入的是’65’那么就会报错,同理,’AB’也会报错。

一对单引号中不能是0个字符或多个字符,中文也不行(因为中文占据两个字符,而char只是占据一个字符,所以输入’65’也是因为这个原因报错的)。

所以利用这个ASCII码,我们可以轻易做到大小写转换

1
2
3
4
//大小写转换,只需要+32就可以了
char a = 65;
a = a + 32;
printf("%c",a);

转义字符

转义字符就是特定字母前加了\的字符,如\n。加了\后,计算机就不再把他识别成n,而是规定好的东西(\n是换行)

其他的转义字符如下所示

字符 意义
\n 换行
\t 横向跳格(跳到下一制表位置)
\v 竖向跳格
\b 退格
\r 回车
\f 走纸换页
\ 反斜杠字符’\’
\’ 单引号字符
\a 鸣玲
\ddd 1到3位八进制数所代表的字符
\xhh 1到2位十六进制数所代表的字符。

PS:

  1. \v垂直制表和\f换页符对屏幕没有任何影响,但会影响打印机执行响应操作
  2. \n其实应该叫回车换行。换行只是换一行,不改变光标的横坐标;回车只是回到行首,不改变光标的纵坐标。
  3. \t 光标向前移动四格或八格,可以在编译器里设置。
  4. ' 在字符里(即单引号里)使用。在字符串里(即双引号里)不需要,只要用 ‘ 即可
  5. 转义字符中只能使用小写字母,每个转义字符只能看作一个字符

字符串常量

1
printf("%s","abcdef");//输出字符串,输出的是一串,而%c是输出一个字符

字符串在内存里面是一个字符存放一个空间的,但是它的结尾有一个存放’\0’的空间('\0'是结束标识符,标志这串字符串到这里位置就结束了),所以像上面代码有6个字符,但实际上它占用了7个内存空间。

字符串长度:从字符串的左边开始计数,到第一个'\0'为止,且'\0'不参与计数

1
2
3
4
5
6
#include <stdio.h>
#include <string.h>//使用strlen()方法要有这个头文件,因为这个方法是在这个头文件里面被定义好的。如果不用这个头文件,又想要使用strlen()方法,只能自己写一个strlen()函数。
int main(){
printf("%d",strlen("ab"));//strlen()是用来统计字符串长度的方法
return 0;
}

这时候就有一些骚操作可以玩了

1
2
3
4
printf("%d\n",sizeof("ab\0cd"));
printf("%d\n",strlen("ab\0cd"));
printf("%d\n",sizeof(""));
printf("%d\n",strlen(""));

输出为

1
2
3
4
6//字符串是"ab\0cd",虽然已经有了一个\0,但它还是会在末尾自动添加一个\0,所以整个大小就是6
2//字符串识别到第一个\0时就会停止,返回这个\0之前的字符数量,所以是2
1//字符串的尺寸,里面只有一个\0,所以是1
0//\0并不算是长度,然后里面又没其他东西,所以长度为0

字符数组

传统的字符集合方式(并不推荐)

1
2
3
char a[4] = {'a','b','c','d'};//全部赋值,但这个不是字符串,因为没有'\0'
char a[4] = {'a','b','c'};//部分赋值,这个是字符串,因为后面有个数组给它自动补上\0
char a[] = {'a','b','c','d'};//省略数组长度赋值,长度根据字符的个数确定,不会自动补上\0,所以不是字符串
1
2
3
char a[2] = {'A','B'};
printf("%s\n",a);
printf("%s","AB");

输出如下

1
2
ABp@
AB

为什么第一个后面会有几个奇怪的东西呢?

“AB”中最后一个是自动补上\0,所以它一共就只有3个。而char a[2] = {'A','B'};虽然可以看成是字符串,但是它跟’AB’并不等价,它一共只有2个,最后没有\0,所以他并不知道什么时候结束,于是就有后面那几个奇怪的东西跟着一起输出了。

所以只要把最后一个改成\0就可以了

1
2
3
4
5
6
char a[3] = {'A','B','\0'};
printf("%s\n",a);
printf("%s","AB");
//输出如下
//AB
//AB

总结:

用字符集合的方式赋值时,要养成习惯再末尾补充’\0’

字符串字面值方式(推荐)

1
2
3
4
char a[3] = "ab";//系统会自动在后面添加\0,即a[2]='\0'
char a[3] = "abc";//错误,超出数组长度
char a[3] = "abcd";//错误,超出数组的长度
char a[] = "abc";//推荐!系统会在后面添加\0,即a[3]='\0',所以该数组的元素个数是4个

PS:

1
2
3
4
5
6
char a[] = "ab";
a = 'cde';
printf("%s",a);
//报错,因为数组名代表的是首元素地址,是地址常量,常量是不能修改的。
a[2]='cde';
//这个也会报错,因为数组越界。

字符串和指针

1
2
3
char a[5] = {"abcd"};
printf("%s\n",a);
printf("%s\n",&a[2]);

输出如下

1
2
abcd//输出全部,直到第一个'\0'
cd//从下标第二个开始输出,直到'\0'
1
2
3
4
5
6
7
8
9
10
11
12
13
char a[5] = {"abcd"};
char *p;//char型指针变量
p = a;//p=&a[0]
*p = 'X';//等价于a[0]='X';
printf("%s\n",a);
printf("%s\n",p);
//a[2] -= 32;//对数组内的字符进行修改
p[2] -= 32;//等价于上面那句话,也等价于*(p+2)-=32;
printf("%s\n",a);
printf("%s\n",p);
char *b;
b = "AAAAA";//"AAAAA"是字符串常量,b指向这个常量的首地址,所以是不能修改的。
printf("%s\n",b);

输出如下

1
2
3
4
5
abcd
abcd
abCd
abCd
AAAAA

字符串的输入和输出

1
2
3
printf("%s\n","abc");
printf("abc\n");
puts("abc");

输出如下

1
2
3
abc
abc
abc

puts()和printf()的区别:puts会自动输出换行而printf不会

1
2
3
char a;
a='x';
putchar(a);

putchar是输出单个字符的,而puts是输出字符串(多个字符)

1
2
3
4
5
6
7
char a;
scanf("%c",&a);//单个字符的输入
char c;
c = 'X';
c = getchar();//单个字符的输入
scanf("%s",a);//字符串的输入
gets(a);//字符串的输入

scanf(),getchar()跟gets()的区别:gets能输入带空格的字符串,而scanf不能。而getchar则会把上一个输入的回车符号吃掉

字符串处理函数

以下函数都需要<string.h>头文件

统计长度

int strlen(const char* pStr)

参数:字符串指针

返回值:长度

交换

char* strcpy(char* pDes, const char* pSrc)

确保str1足够长,否则产生运行时可能会造成缓冲溢出的情况。

参数:pDes:指向用于存储复制内容的目标数组;pSrc:要复制的字符串

返回值:该函数返回一个指向最终的目标字符串pDes的指针

1
2
3
4
5
char a[10] = "ABCD";
char b[10] = "AAAA";
strcpy(a,b);
printf("%s\n",a);
printf("%s",b);

输出如下

1
2
AAAA
AAAA

连接

char *strcat(char* pDes, const char* pSrc)

确保str1足够长,否则产生运行时会错误

参数:pDes:指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串;pSrc:指向要追加的字符串,该字符串不会覆盖目标字符串

返回值:该函数返回一个指向最终的目标字符串 dest 的指针

1
2
3
4
char a[10] = "ABCD";
char b[10] = "AAAA";
strcat(a,b);
printf("%s\n",a);

输出如下

1
ABCDAAAA

实际上就是在字符串b后面拼接字符串a

int strcmp(const char* pDes,const char* pSrc)

假设这两个字符串为a和b,若a==b,则返回0。若a<b,则返回正数。若a>b,则返回负数

比较规则:从左向右的字符两两比较,如果相同则继续向后比较,直到遇到第一个不相同的字符就以该不同的字符作为比较的依据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
char str1[15];
char str2[15];
int ret;


strcpy(str1, "abcdef");
strcpy(str2, "ABCDEF");

ret = strcmp(str1, str2);

if(ret < 0)
{
printf("str1 小于 str2");
}
else if(ret > 0)
{
printf("str1 大于 str2");
}
else
{
printf("str1 等于 str2");
}
printf("\n%d",ret);

输出如下

1
2
str1 大于 str2
32

遍历

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
//1
while(str[i]!='\0'){
i++;
}
//2
while(str[i]){
i++;
}
//3
while(*p!='\0'){
p++;
i++;
}
//4
while(*p){
p++;
i++;
}
//5
while(*(p+i))
{
i++;
}
//6
while(*(p+i)!='\0'){
i++;
}

小写字母转换成大写字母

char *strupr(char* pStr)

大写转小写

char *strlwr(char* pStr)

第十三章

存储区分类

c/c++中的变量、常量、函数都是占据内存的。系统一般会将内存划分为五个区域,分别是常量区代码区栈区堆区全局/静态存储区,这些存储区是系统划定的,是真实存在的,不需要认为干预。当我们写好一段程序后,系统会自动将变量、常量和函数自动划分到相应的存储区中。不同的存储区内存的开辟时间和销毁时间是不同的,但每一个存储区占据的内存是连续的(这样是为了提高程序的运行效率)

常量区

c中常量是占据内存的,系统将常量集中存储在一块连续的内存空间中,常量区的内存具备“只读性”,是不能改写的。

开辟时间:编译时

释放时间:程序结束后由系统释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
int a = 10;//全局变量,这个时候就会在全局区(也就是静态区)开辟一个空间
int abc(int b);
int main(){//代码区
int b = 20;//局部变量,在栈区开辟
abc(b);
return 0;
}
int abc(int b){//代码区
static int c = 1;//局部变量,但因为是static修饰,也就是静态变量,所以他也会在这个时候在静态区开辟一个内存空间
c += b;
return c;
}

//以上代码,会在常量区开辟三个空间,存放1、20、10这三个常量

上面的代码的大致运行过程:

一开始在静态区开辟一个空间给a,然后在常量区开辟一个空间存储常量10,然后把这个时复制到静态区的a内存中。

接着会在代码区开辟一个空间给abc()作为其执行入口

然后会在代码区开辟一个空间给main()作为其执行入口。

运行到int b =20时,在栈区开辟一个内存块b,把20复制过来给b。运行到abc(b)时,会从代码区中main()的内存跳到abc()的内存中。

abc函数执行结束后就会跳回main()中,继续执行接下来的代码。

代码区

存放函数体的二进制代码

开辟时间:编译时

释放时间:程序结束后由系统自动释放

函数指针

c的函数也是要占据内存的,系统会为这些函数的内存分配地址,我们把这个地址叫做函数指针,c中的函数名称称为函数指针常量(也叫做函数的入口地址)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void a(void);
void b(void);
void c(void);
int main(){
int i = 0;
p = &i;
*p = 10;//相当于i = 10
printf("%d\n",a);
printf("%d\n",b);
printf("%d",c);

return 0;
}
void a(void){
int i =2;}
void b(void){
int i =2;}
void c(void){
int i =2;}

输出如下

1
2
3
4198761
4198775
4198789

实际上这三个地址是没任何关联的,他只是入口地址。

通过函数指针常量(函数名称)调用函数的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void abc(void);

int main(){
void (*p_abc)(void);//定义一个函数指针变量,可以存放函数的地址。PS,括号不能省掉
//其中返回值类型为void,形参为void的函数
p_abc = abc;//指向函数abc的入口地址
p_abc();//调用,相当于abc();
(*p_abc)();//等价于上面那句。
return 0;
}

void abc(void){
printf("11111");
}

输出如下

1
11111
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void abc(void);
int main(){
int *pstr[3];//定义了一个指针数组,相当于定义了3个int型指针变量
void (*p_a[3])(void);//定义了一个函数指针数组,相当于定义了3个函数指针变量

p_a[0] = abc;
p_a[0]();

return 0;
}

void abc(void){
printf("11111");
}

输出如下

1
11111

堆区

用malloc()或者calloc()函数创建的变量

区别:

1.两者都是动态分配内存,主要的不同是malloc不初始化分配的内存,已分配的内存中可以是任意的值。而calloc初始化已分配的内存为0

2.calloc分配的是一个数组,malloc分配的是一个对象

要点:malloc和calloc分配的内存在堆区,堆内存必须通过free函数回收内存,否则造成内存泄漏

在使用这两个函数前,要先包含头文件#include<stdlib.h>(有些编译器可以不用,但绝大多数都是要这个的)

开辟时间:执行到malloc或calloc函数时分配内存

销毁时间:执行到free函数时释放内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stido.h>
#include <stdlib.h>
int main(){
int a = 0;//位于栈区
int *p;//位于栈区
int *p1;//位于栈区
p = &a;//指向a的内存地址
*p = 10;//相当于a=10
p1 = (int *)malloc(sizeof(int));//申请了一块int型(4字节)堆内存,里面的数据是未知的,不会赋初值。
if(p1 == NULL){
printf("申请失败");//如果一直申请却没有释放内存的话,内存是会被消耗完的,这时它就会返回一个NULL,无法对他进行申请。所以一般情况都会对他进行一个判断。
}
//向堆区申请了4个字节的内存,给p1,即p1指向了堆区的一个4字节大小的内存块,然后通过(int *)进行强制转换,即把堆区的内存块的类型转成int *类型。
*p1 = 20;//把常量区的20复制给堆区的那个内存块。
free(p1);//释放p1指向的堆内存
return 0;
}

malloc(sizeof(int))实际上就是申请了4个字节的内存块,但因为不知道它是怎么操作的(即不知道是2字节2字节的操作还是4字节4字节的操作),所以在前面对他进行强制转换,转成(int *)类型,明确等下操作是一次性4个字节进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stido.h>
#include <stdlib.h>
int main(){
int a[5] = {0};
int *p2;
int *p;
p = a;
*p = 20;//a[0]=20
//申请了一块长度为5,每个为4个字节的堆内存
//里面的数据赋初值为0
p2 = (int *)calloc(5sizeof(int));//第一个参数是长度,第二个是每块占多少个字节
p2[0] = 10;
free(p2);
return 0;
}

第十四章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(){
FILE *fp;//定义了一个文件指针
//打开文件
//绝对路径打开
//文件打开成功,则返回该文件的地址,打开失败则返回NULL
fp = fopen("C:\\User\\abc.txt","r");//第一个参数是地址,第二个参数是打开模式(即只读,只写等)
if(fp==NULL){
printf("打开失败");
}
fclose(fp);//关闭文件,每次打开文件后一定要关闭文件。
//相对路径打开文件,但要求文件和代码文件在同一个目录下
fopen("abc.txt","r");
fclose(fp);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(){
FILE *fp;//定义了一个文件指针
char *ch = 0;
//打开文件

fp = fopen("C:\\User\\abc.txt","r");
if(fp==NULL){
printf("打开失败");
}
ch = fgetc(fp);//从文件中读出一个字符。读出后光标会往后移动一个。如果读取失败,会返回EOF值或者error值,这两个都是-1
printf("读出来的字符%c\n",ch);
while(!feof(fp)){//读取文件里的全部数据,其中fepf(fp)是判断是否读到这个文件fp的结束标志,如果不是则返回0,当读取到结束标志时,则返回非零值
printf("读出来的是%c\n",ch);
ch = fgetc(fp);
}
ch = fputc('A',fp);//写入一个字符‘A’进去,如果失败,则返回EOF,如果成功则返回字符的ASCII码值
if(ch == EOF){
printf("写入失败\n");
}
fclose(fp);//关闭文件
return 0;
}

打开模式有以下几种

对文本文件

w:只写方式,文件存在时,截短为0,不存在,则创建—>创建一个新的空文件

r:只读方式,文件必须存在,否则报错

r+:读写方式,文件必须存在,否则报错

w+:读写方式,文件存在时,截短为0,不存在,则创建—>创建一个新的空文件

a:追加(写),文件存在时,从文本末尾开始写,文件不存在时,则创建一个文件

a+:追加(读写),文件存在时,从文件末尾开始写,文件不存在时,则创建一个文件

对二进制文件

rb、rb+、wb、wb+、ab、ab+

返回值

成功:FILE指针

失败:NULL

读和写

1
2
3
4
5
6
7
8
9
10
11
char str[20] = "abcdef";
FILE *fp;//定义了一个文件指针
char *ch = 0;
//打开文件
fp = fopen("C:\\User\\abc.txt","r+");
//从文件中读取一行字符
fgets(str,5,fp);//第一个参数:读到那里。第二个参数:最多读几个字符。第三个填文件地址
puts(str);//最多读取n-1个,因为读了5个,但有一个是\0,所以最终只有4个。但如果只有3个字符,我填5个,那么他就会把3个全部读取出来
//从文件中写入一行字符
fputs(str,fp);//fputs不会将\0进行输出,遇到\0就输出
fclose(fp);

按对象来进行操作(二进制读写)

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct{
char name[24];
char tel[20];
}NODE;

int main(){
FILE *fp;//定义了一个文件指针
NODE cl[5] = {{"TOM","189222"},{"JACK","1828282"},{"李四","12312"}};
fp = fopen("C:\\User\\abc.txt","rb+");
fwrite(&cl[0],sizeof(NODE),1,fp);//写入数据,第一个参数:从哪里拿的数据;第二个参数是要写多大的(他是按照块来写的);第三个参数是要写几块;第四个参数是文件指针
fread(&cl[3],sizeof(NODE),1,fp);//从文件读出数据。参数跟上面的参数一样
fclose(fp);
}

fwrite的返回值:成功返回操作的对象个数,失败返回0

定位读写

int fseek(FILE *stream,long offset, int whence);

功能:设置offset的值,实现定位

参数:

stream:要定位的文件指针

offset:偏移值

whence:相对的偏移起点

其中

SEEK_SET 从文件开头偏移 offset>=0

SEEK_CUR 当前位置 offset 可正可负

SEEK_END 文件结尾 offset可正可负

返回值:

成功 0

失败 -1&errno 置位

1
fseek(fp,100,SEEK_SET);//从文件开头偏移100个字节

获得文件大小

long ftell(FILE *stream);获得offset的值

重新定位到文件开头

void rewind(FILE *stream);<—–>fseek(fp,0,SEEK_SET)