c语言
一、C语言概述
大纲
1、为什么学习C语言
1.C的起源和发展
高级语言: a + b
汇编语言: ADD AX,BX
机器语言: 0000 0001 1101 1000
C语言产生和发展过程
产生时间:1972-1973
产生地点:美国贝尔实验室(Bell)
创始人:Dennis.M.Ritchie和 Ken.Thompson
目的:改写 UNIX 操作系统
C语言发展过程
1983年 ANSIC
1987年 ANSIC 87
1994年 C99
2.C的特点
优点
代码量小
速度快
功能强大
缺点
危险性高
开发周期长
可移植性不强
3.C的应用领域
系统软件开发
操作系统:Windows、Linux、Unix
驱动程序:主板驱动、显卡驱动、摄像头驱动
数据库:DB2、Oracle、Sql Server
应用软件开发
办公软件:Wps
图形图像多媒体:ACDSee、Photoshop、MediaPlayer
嵌入式软件开发:智能手机、掌上电脑
游戏开发:2D、3D 游戏
4.C的重要性
有史以来最重要语言
所有大学工科和理科学生必修课程
最重要系统软件:windows、linux、unix 均适用 c 开发
一名合格黑客必须掌握的语言
任何一个想终生从事程序设计和开发人员必须熟练掌握的语言
大企业、外企招聘程序员必考的语言
为学习数据结构、C++、Java、C#奠定基础
2、怎样学习C语言
参考资料
谭浩强《C语言程序设计》清华
《The C programming language》机械工业
《C Primer Plus》60 元 人名邮电
《C 和指针》65 元 人名邮电
《C专家编程》绝版
《C 陷阱与缺陷》人名邮电 30
《C 科学与艺术》机械工业
3、学习的目标
了解程序语言及发展历史
熟悉掌握C语言的语法规则
掌握简单的算法
理解面向过程的思想,这非常有助于将来对面向对象思想的学习
能看懂程序
会调试程序
掌握将大问题转化为一系列小问题来求解的思想
为学习C++、数据结构、C#、Java 打下良好的基础
二、C语言编程预备知识
1、什么是数据类型
基本类型数据
整数
整形 -- int -- 4
短整型 -- short -- 2
长整形 -- long int -- 8
浮点数【实数】
单精度浮点数 -- float -- 4
双精度浮点数 -- double -- 8
字符
char -- 1
复合类型数据
结构体
枚举
共用体
2、什么是变量
变量的本质就是内存中一段存储空间
3、CPU 内存条 VC++ 6.0 操作系统 之间的关系
VC++6.0编写了一段程序int i = 3; 请求操作系统将内存中的一段空闲的内存空间与 i 用字母的形式绑定在一起,操作这个变量 i 实际是操作这一块内存中的空间
4、变量为什么必须的初始化
所谓初始化就是赋值的意思
软件运行与 内存关系(垃圾数据) 内存是在操作系统的统一管理下使用的!
软件在运行前需要向操作系统申请存储空间,在内存空闲空间足够时,操作系统将分配一段内存空间并将外存中软件拷贝一份存入该内存空间中,并启动该软件的运行!
在软件运行期间,该软件所占内存空间不再分配给其他软件
当软件运行完毕后,操作系统将回收该内存空间(注意:操作系统并不清空该内存空间中遗留下来的数据),以便再次分配给其他软件使用。
综上所述,一个软件所分配到的空间中极可能存在着以前其他软件使用过后的残留数据,这些数据被称之为垃圾数据。所以通常情况下我们为一个变量,为一个数组,分配好存储空间之后都要对该内存空间初始化!最后一句话听不懂也没有关系,等我们讲到C语言时再讲这个问题!
5、如何定义变量
数据类型 变量名 = 要赋的值;
等价于
数据类型 变量名;
变量名 = 要赋的值;
举例子:
int i = 3; 等价于 int i; i = 3;
int i, j; 等价于 int i; int j;
int i, j = 3; 等价于 int i; int j; j = 3;
int i = 3, j = 5; 等价于 int i; int j; i = 3; j = 5;
int i, j; i = j = 5; 等价于 int i, j; i = 5; j = 5;
6、什么是进制
十进制就是逢十进一
二进制逢二进一
进制转换
什么叫进制
进制就是逢几进一
我们说N进制实际就是指逢N进一
我们计算机只识别二进制
人类最习惯使用的是十进制
为了实际需要,我们又建立了八进制和十六进制
C语言规定八进制前要加 0(注意是零不是字母 o),十六进制前要加 0x 或 0X,十进制前什么都不加!
不同进制数的表示
在汇编中:在数字后加字母B表示二进制数,加字母O表示八进制教,加字母口表示十进制数,加字母H表示十六进制教。
例:
1011B为二进制数1011,也记为(1011)₂
1357O为八进制数1357,也记为(1357)₈
2049D为十进制数2049,也记为(2049)₁₀
3FB9H[为十六进制数3FB9,也记为(3FB9)₁₆
1.什么叫n进制
十进制
十个基数,逢10进一
基数:0 1 2 3 4 5 6 7 8 9
二进制
二个基数,逢二进一
基数:0 1
2->10 3->11 4->100 5->101
八进制
8 个基数 逢8进一
基数: 0 1 2 3 4 5 6 7
8 -> 10 9->11 10->12
十六进制
16 个基数 逢16进一
基数: 0 1 2 3 4 5 6 7 8 9 A B C D E F 或
0 1 2 3 4 5 6 7 8 9 a b c d e f
16-> 10 17->11 18->12 19->13
常用计数制对照表
注意看八进制的 17,对应的十进制的 15,为什么呢? 八进制右边的7对应的就是十进制的 7,左边的1,因为 8 进制是逢 8 进1,这个 1 代表的就是 10 进制的 8,所以 7+8 等于15。
再比如八进制的 23 , 3 代表的就是 10 进制的 3, 2 代表的是2 个 10 进制的 8,即 2*8=16,16+3=19,所以 23 代表的是 10 进制的 19
再看看16 进制的,如 32C 转换成 10 进制, C表示十进制的12,因 16 进制是逢 16 进 1,所以 2 转换成 10 进制是 2*16=32, 那么 3 就是 3*162
即32C转换成 10 进制计算 》 12*160+2*161+3*162 = 812
二进制的 1001 转成10 进制 : 1 20 + 0 21 + 0 22 + 1 23 = 1+0+0+8 = 9
总结
各进制转换为十进制计算可以总结公式为: 个位数 自身进制0 + 十位数 自身进制的1 + ...
补充
1.十进制转换为R进制
- 都是使用除数取余法来转换,结果按倒序来
a.十进制转换为二进制,就一直除以2,直到余数比2小,商为0为止。
b.十进制转换为八进制,就一直除以8,直到余数比8小,商为0为止。
c.十进制转换为十六进制,就一直除以16,直到余数比16小,商为0为止。
(28)10 = (11100)2 = (34)8 = (1C)16
2.R进制转换为十进制
- 都是按位乘以R的N次方相加的和,R=2/18/16
3.二进制和八进制的相互转换,二进制和十六进制的相互转换
最简单的8421记忆法。1111 = 8+4+2+1 = 15; 1010 = 8+0+2+0=10;0101 = 0+4+0+1=5;
在此基础上,再往上加,128,64,32,16,8,4,2,1
你们知道这是怎么来的吗?参看上面二进制转十进制。
刚好是255,再想想我们的ip地址最大值也是255,对吧?IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”
- A. 二进制转十六进制,二进制转八进制: 都是取位数合一法。
二进制转十六进制是,取4位合1位: 1111 0110 = F6
二进制转八进制是,取3位合1位,不足位就往左补个0: 11 110 110 = 366
- B. 十六进制转二进制,八进制转二进制: 都是 取1 分 位数 法。
十六进制转二进制是,取1位分4位: F6 ——F = 1111 ,6 = 0110, D6 = 11110110
八进制转二进制是,取1位分3位: 367 ——3 = 011 ,6 = 110, 7 = 111, 367= 11110111
4.补充:小数点怎么转换
- 整数部分按上面的转换法来转换,小数部分,乘以R,结果跟整数部分相反,是按顺序来
十进制转八进制: (3.23)10 = (3.165)8;
如果有要求取3位小数点,就取3位小数点就行了,否则可以一直取下去,直到变成0为止。
# 还有一种十进制转二进制比较快速的方法。就是拆数法。尽量用2的N次方相加。对2除不尽的十进制数转换为二进制,必定最后一位是1.希望对你们有启发。
7、常量在C语言中是如何表示的
整数
十进制: 传统的写法
十六进制: 前面加 0x 或 0X
八进制: 前面加0(注意是数字零不是字母O)
浮点数
传统的写法
float x = 3.2; //传统
科学计数法
float x = 3.2e3; //x的值是 3200
float x = 123.45e-2; //x 的值是 1.2345 (科学计数法的 e 等同于 10,e 后面跟的数是幂, e-2表示10-2, e3表示 103) ,所以 123.45e-2 = 123.45 * 10-2 = 1.2345
字符
单个字符用单引号括起来
'A' 表示字符A
'AB' 错误的
"AB"正确
字符串用双引号括起来
"A" 正确,因为"A"包含了'A' ,'\0' 的组合,'A' 和'\0'都是"A"的子集
8、常量以什么样的二进制代码存储在计算机中
整数是以补码的形式转化为二进制代码存储在计算机中的
实数是以IEEE754标准转化为二进制代码存储在计算机的
字符的本质实际也是与整数的存储方式相同
9.代码规范化
代码的可读性更强【容易让自己和别人更清楚的看懂程序】
使程序更不容易出错
10、什么是字节
字节是存储数据的单位,并且是硬件所能访问的最小单位
1 个字节 = 8位(8 个比特(bit))
1K = 1024 字节 (210 = 1024)
1M = 1024 k
1G = 1024 M
11、不同类型数据之间相互赋值的问题
int i = 45;
long j = 102345;
i = j;
printf("%ld %d\n",i ,j);
float x = 6.6;
double y = 8.8;
printf*("%f %lf\n", x , y);
12、什么是 ASCII
ASCII不是一个值,而是一种规定,
ASCII规定了不同的字符使用哪个整数值去表示
它规定了
'A' -- 65
'B' -- 66
'a' -- 97
'b' -- 98
'0' -- 48
13、字符的存储
字符本质上与整数的存储方式相同
14、基本的输入和输出函数的用法
printf()
将变量的内容输出到显示器
四种用法
1.printf("字符串\n");
2.printf("输出控制符", 输出参数);
3.printf("输出控制符 1 输出控制符 2...", 输出参数 1, 输出参数 2, ...);
printf("输出控制符 非输出控制符", 输出参数);
输出控制符包含如下
%d -- int
%ld -- long int
%c -- char
%f -- float
%lf - double
%x(或者%X或者%#X) -- int 或者 long int 或 short int
%o -- 同上
%s -- 字符串
为什么需要输出控制符
01 组成的代码可以表示数据也可以表示指令
如果 01 组成的代码表示的是数据的话,那么同样的 01 代码组合以不同的输出格式输出就会有不同的输出结果
scanf()
通过键盘将数据输入到变量中
两种用法
用法一(不含非输入控制符的用法):
scanf("输入控制符", 输入参数);
功能:将从键盘输入的字符转化为输入控制符所规定格式的数据,然后存入以输入参数的值为地址的变量中
# include <stdio.h>
int main(void)
{
int i;
scanf("%d", &i); //&i 表示i的地址 &是一个取地址符
printf("i = %d\n", i);
return 0;
}
用法二(含有非输入控制符)
scanf("非输入控制符 输入控制符", 输入参数);
功能:将从键盘输入的字符转化为输入控制符所规定格式的数据,然后存入以输入参数的值为地址的变量中
非输入控制符必须原样输入
# include <stdio.h>
int main(void)
{
int i;
scanf("m%d", &i); // m123 正确的输入 123是非法的输入
printf("i = %d\n", i);
return 0;
}
如何使用scanf编写出高质量代码
1.使用scanf之前最好先使用printf提示用户以什么样的方式输入
2.scanf中尽量不要使用非输入控制符,尤其是不要用 \n
3.应该编写代码对用户的非法输入做适当的处理【非重点】
while((ch=getchar())!='\n')
continue;
# include <stdio.h>
//int main(void)
//{
//int i, j, k;
/**
* 使用scanf之前最好先使用printf提示用户以什么样的方式输入
*/
//scanf("%d %d %d", &i, &j, &k);
//printf("请输入三个值,中间以逗号分隔:");
//scanf("%d,%d,%d", &i, &j, &k);
//printf("i = %d, j = %d, k = %d\n" , i, j, k);
/**
* scanf中尽量不要使用非输入控制符,尤其是不要用 \n
*/
//scanf("%d\n" , &i) // 非常不好的格式,不要加 \n
//scanf("%d" , &i)
//printf("i = %d\n", i);
/**
* scanf对用户非法输入的处理
*/
// int i;
// scanf("%d" , &i);
// printf("i = %d\n", i);
// //....
// int j;
// scanf("%d", &j);
// printf("j = %d\n", j);
// // 上面输入123m ,得到的结果是i = 123 j = 1, 123赋值给了i, 然后从m开始读赋值给j,发现m不是一个数字(以%d的格式),所以赋值会异常
// return 0;
// }
int main(void)
{
int i;
char ch;
scanf("%d", &i);
printf("i = %d\n",i);
//...............
while ( (ch = getchar()) != '\n')
continue;
int j;
scanf("%d",&j);
printf("j = %d\n", j);
return 0;
}
运算符
算术运算符
+ - * /(除) %(取余数)
关系运算符
> >= < <= !=(不等于) ==(等于)
逻辑运算符
!(非) &&(并且) ||(或)
!真 > 假
!假 > 真
真&&真 > 真
真&&假 > 假
假&&真 > 假
假&&假 > 假
真 || 假 > 真
假 || 真 > 真
真 || 真 > 真
假 || 假 > 假
C语言对真假的处理
非零是真
零是假
真是1表示
假是0表示
&& :短路与, &&左边的表达式为假,右边的表达式肯定不会执行(因为&&是两个为真才是真,左边为假整个都为假了)
||:短路或, || 左边的表达式为真,右边的表达式肯定不会执行(因为||是有一个为真就是真,左边为真后面的就不用计算了)
# include <stdio.h>
int main(void)
{
int i = 10;
int k = 20;
int m;
//m = (3 > 2) && (k = 8);
//printf("m = %d, k = %d\n", m, k); // m = 1 , k = 8
//m = (3 > 2) && (k = 0);
//printf("m = %d, k = %d\n", m, k); // m = 0 , k = 0
//m = (1 > 2) && (k = 5); //不含有分号的是表达式, 含有分号的是语句
//printf("m = %d, k = %d\n", m, k); // m = 0 , k = 20 &&是短路与, 判定前面是假就直接短路了,后面的5赋值给k就不走了,所以k还是20
m = (1 > 2) || (k = 5);
printf("m = %d, k = %d\n", m, k); // m = 1 , k = 5 ||有一个是真那结果就是真, 前面是假后面是真,所以整个结果是真 m=1, k=5
return 0;
}
赋值运算符
= += *= /= -=
优先级别:
算法 > 关系 > 逻辑 > 赋值
附录的一些琐碎的运算符知识
自增 自减 三目运算符 逗号表达式
除法/ 的运算结果和运算对象的数据类型有关,两个数都是int,则商就是int,若商有小数,则截取小数部分;被除数和除数中只要有一个或两个都是浮点型数据,则商也是浮点型,不截取小数部分。
如: 16/5 == 3 ~ 16/5.0 3.20000 -13/4 -3
-13/-3 4 3/5 0 5/3 == 1
最典型的例题就是求s=1+1/2+1/3+1/4+1/5.......+1/100的值(具体程序后面讲)
取余%的运算对象必须是整数,结果是整除后的余数,其余数的符号与被除数相同
如: 13%3 1 13%-3 1 -13%3 == -1
-13%23 -13 3%5 3
测试取模运算符的例子
# include <stdio.h>
int main(void)
{
printf("%d %d %d %d %d\n", 3%3, 13%-3, -13%3, -13%-3, -13%23, 3%5);
return 0;
}
/*
输出结果是
************************
0 1 -1 -1 -13 3
************************
总结:取余%的运算对象必须是整数,结果是整除后的余数,其余数的符号与被除数相同
*/
流程控制
流程控制是我们学习C语言的第一个重点
1.什么是流程控制
程序代码执行的顺序
2.流程控制的分类
顺序
选择
定义
某些代码可能执行,也可能不执行,有选择的执行某些代码
分类
if
if最简单的用法
格式:
if (表达式)
语句
功能:
如果表达式为真,执行语句
如果表达式为假,语句不执行
if的范围问题
1.
if (表达式)
语句A;
语句B;
解释:if默认只能控制语句A的执行或不执行
if无法控制语句B的执行或不执行
或者讲:语句B一定会执行
2.
if (表达式)
{
语句A;
语句B;
}
此时if可以控制语句A和语句B
由此可见:if默认只能控制一个语句的执行或不执行,如果想控制多个语句的执行或不执行,就必须把这些语句用 {} 括起来
# include <stdio.h>
int main(void)
{
if (1 > 2)
printf("AAAA\n");
printf("BBBB\n");
return 0;
/*
输出的结果是:
--------------
BBBB
--------------
总结:
if默认的只能控制一个语句的执行或不执行
*/
}
# include <stdio.h>
int main(void)
{
if (1 > 2)
{
printf("AAAA\n");
printf("BBBB\n");
}
printf("CCCC\n");
return 0;
/*
输出: CCCC
*/
}
if..else...的用法
# include <stdio.h>
int main(void)
{
int i, j;
scanf("%d %d", &i, &j);
if (i > j)
printf("i大于j\n");
else
printf("i小于j\n");
printf("CCCC/n");
return 0;
}
if..else if...else..的用法
格式:
if (1)
A;
else if (2)
B;
else if (3)
C;
else
D;
# include <stdio.h>
int main(void)
{
double delta = -1;
if (delta > 0)
{
printf("有两个解!\n");
}
else if (delta == 0)
{
printf("有一个唯一解!\n");
}
else
{
printf("无解!\n");
}
return 0;
}
C语言对真假的处理
非零是真
零就是假
真用1表示
假用0表示
if举例 -- 求分数的等级
# include <stdio.h>
int main(void)
{
float score; //score分数
printf("请输入您的考试成绩:");
scanf("%f", &score);
if (score > 100)
printf("做梦呢~\n");
else if (score >= 90 && score <= 100)
printf("优秀\n");
else if (score >= 80 && score < 90)
printf("良好\n");
else if (score >= 60 && score < 80)
printf("及格\n");
else if (score > 0 && score < 60)
printf("不及格\n");
else
printf("输入非法!\n");
return 0;
}
# include <stdio.h>
int main(void)
{
int i = 3;
int j = 4;
int temp; // 定义临时变量
temp = i; // 将 i 赋值给临时变量temp
i = j; // 将 j 赋值给 i 变量
j = temp; // 将 temp 变量的值复制给j
printf("i = %d, j = %d\n", i, j); // i = 4, j = 3
return 0;
}
# include <stdio.h>
int main(void)
{
int a, b, c; // 等价于: int a; int b; int c;
int t; //定义临时变量t
printf("请输入三个整数(中间以空格分隔): ");
scanf("%d %d %d", &a, &b, &c);
// 编写代码完成a最大值 b是中间值 c是最小值
if (a < b)
{
t = a;
a = b;
b = t;
}
if (a < c)
{
t = a;
a = b;
b = t;
}
if (b < c)
{
t = b;
b = c;
c = t;
}
printf("%d %d %d\n", a, b, c);
return 0;
}
if的常见问题解析
1.空语句的问题
# include <stdio.h>
int main(void)
{
if (3 > 2)
; // 这是一个空语句
printf("AAAA\n");
printf("BBBB\n");
return 0;
}
if (表达式1)
A;
else
B;
是正确的
if (表达式1);
A;
else
B;
是错误的
2.
# include <stdio.h>
int main(void)
{
if (3 >2)
printf("AAAA\n");
else if (3 > 1)
printf("BBBB\n");
else
printf("CCCC\n");
return 0;。。
}
/*
输出结果
AAAA
=======================
总结
if (表达式1)
A;
else if (表达式2)
B;
else if (表达式3)
C;
else
D;
即使表达式1和2都成立,也只会执行A语句
*/
if (表达式1)
A;
else if (表达式2)
B;
else if (表达式3)
C;
这样写语法不会出错,但逻辑上有漏洞
if (表达式1)
A;
else if (表达式2)
B;
else if (表达式3)
C;
else (表达式4) // 7行
D;
这样写是不对的,正确的写法是:
要么去掉7行的(表达式4)
要么在7行的else后面加if
if (表达式1)
A;
else if (表达式2)
B;
else if (表达式3)
C;
else (表达式4);
D;
这样写语法不会出错,但逻辑上是错误的
else (表达式4);
D;
等价于
else
(表达式4);
D;
如何看懂一个程序,分三步:
1.流程
2.每个语句的功能
3.试数
对学习一些小算法的程序
尝试自己取编程解决它,大部分人都自己无法解决
如果解决不了,就看答案
关键是把答案看懂,这个要花很大的精力,也是我们学习的重点
看懂之后尝试自己去修改程序,并且知道修改之后程序的不同输出结果的含义
照着答案去敲
调试错误
不看答案,自己独立把答案敲出来
如果程序实在无法彻底理解,就把它背会
但是可能一时会了,过了一段时间就忘记了,所以每过几天就敲一次,周而复始,慢慢就会了~
强制类型转换
格式:
(数据类型)(表达式)
功能:
把表达式的值强制转换为前面所执行的数据类型
例子:
(int)(4.5 + 2.2) 最终值是6
(float)(5) 最终值是5.00000
# include <stdio.h>
int main(void)
{
int i;
float sum = 0;
for (i = 1; i <= 100; i++)
{
sum = sum + 1.0 / i; // 是OK的 推荐使用
// sum = sum + (float)(1/i); 这样写是不对的
//也可以这样写: sum = sum + 1 / (float)(i); 不推荐
}
printf("sum = %f\n", sum); // float必须用%f输出
return 0;
}
试数详细步骤举例
1 -> i=1 1<=100 成立
sum = 0+1/1.00 = 1 ++i i = 2
2 -> i=2 2<=100 成立
sum = 1+1/2.0 ++i i = 3
3 -> i=3 3<=100
sum = 1+1/2.0+1/3.0 ++i i = 4
............
............
switch
后面讲
循环
定义:
某些代码会被重复执行
分类
for
1. 格式
for(1; 2; 3)
语句A;
2. 执行的流程【重点】
单个for循环的使用
多个for循环的嵌套使用
1.
for (1; 2; 3) //1
for (4; 5; 6) //2
A; //3
B; //4
整体是两个语句, 1 2 3 是第一个语句
4 是第二个语句
2.
for (1; 2; 3)
for (4; 5; 6)
{
A;
B;
}
整体是一个语句
3.
for (7; 8; 9)
for (4; 5; 6)
{
A;
B;
for (4; 5; 6)
C;
}
整体是一个语句
3. 范围问题
4. 举例:
1 + 2 + 3 + .... + 100
1 + 1/2 + 1/3 + .... + 1/100
浮点数的存储所带来的问题
float 和 double 都不能保证可以精确的存储一个小数
举例:
有一个浮点型变量x,如何判断x的值是否是零
if (| x - 0.000001 | <= 0.000001 )
是零
else
不是零
为什么循环中更新的变量不能定义成浮点型
# 1. 自增[或者自减]
- 分类:
- 前自增 -- ++i
- 后自增 -- i++
- 前自增后后自增的异同:
- 相同:
- 最终都使i的值加1
不同:
- 前自增整体表达式的值是i加1之后的值
- 后自增整体表达式的值是i加1之前的值
- 为什么会出现自增
- 代码更精炼
- 自增的速度更快
- 学子自增要明白的几个问题
1. 我们编程时应该尽量屏蔽掉前自增和后自增的差别
2. 自增表达式最好不要作为一个更大的表达式的一部分来使用
或者说
i++ 和 ++i 单独成一个语句, 不要把它作为一个完整复合语句的一部分来使用
如:
int m = i++ + ++i + i + i++; //这样写不但是不规范的代码,而且是不可移植的代码
printf("%d %d %d", i++, ++i, i); //同上
# 2. 三目运算符
格式:
- A ? B : C
等价于
if (A)
B;
else
C;
# 3. 逗号表达式
格式:
- (A, B, C, D)
功能:
从左到右执行
最终表达式的值是最后一项的值
# include <stdio.h>
int main(void)
{
int i;
int j;
int k;
int m;
i = j = 3; //等价于 i = 3; j = 3;
k = i++;
m = ++j;
printf("i = %d, j = %d, k = %d, m = %d\n", i, j, k, m);
return 0;
}
/**
* 输出结果:i = 4, j = 4, k = 3, m = 4
*
* 总结:
* 前自增整体表达式的值是i加1之后的值
* 后自增整体表达式的值是i加1之前的值
*/
# include <stdio.h>
int main(void)
{
int i;
int j = 2;
i = (j++, ++j, j+2, j-3); //j++ > 3, ++j > 4, j+2 整体是6,但是没有赋值给j所以j还是4,最后执行j-3 > 4-3 = 1 ,赋值给i,所以i=1
printf("%d\n", i); //1
return 0;
}
while
1. 执行顺序
格式:
while (表达式)
语句;
2. 与for的相互比较
for和while可以相互转换
for (1; 2; 3)
A;
等价于
1;
while (2)
{
A;
3;
}
while和for可以相互转换
但for的逻辑性更强,更不容易出错,推荐多使用for
3. 举例
从键盘输入一个数字,如果该数字是回文数,则返回yes,否则返回no
回文数:正着写和倒着写都一样
比如:121 12321 都是回文数
4. 什么时候使用while,什么时候使用for
没法说,用多了自然而然就知道了
do...while
1. 格式
do
{
....
} while (表达式);
do...while 并不等价于for,当然也不等价于while
主要用于人机交互
一元二次方程
# include <stdio.h>
# include <math.h>
int main(void)
{
double a, b, c;
double delta;
double x1, x2;
char ch;
do
{
printf("请输入一元二次方程的三个系数:\n");
printf("a = ");
scanf("%lf", &a);
printf("b = ");
scanf("%lf", &b);
printf("c = ");
scanf("%lf",&c);
delta = b*b - 4*a*c;
if (delta > 0)
{
x1 = (-b + sqrt(delta)) / (2*a);
x2 = (-b + sqrt(delta)) / (2*a);
printf("有两个解,x1 = %f, x2 = %f\n", x1, x2);
}
else if (0 == delta)
{
x1 = x2 = (-b) / (2*a);
printf("有唯一解,x1 = x2 = %f\n", x1 ,x2);
}
else
{
printf("无实数解!\n");
}
printf("您想继续么(Y/N):");
scanf(" %c", &ch); //%c前面必须得加一个空格 原因略(空白符的问题,可以看C primer Plus这本书)
} while('Y' == ch || 'y' == ch);
return 0;
}
switch
把电梯程序看懂就ok了
# include <stdio.h>
int main(void)
{
int val;
printf("请输入您要进入的楼层: ");
scanf("%d", &val);
switch(val)
{
case 1:
printf("1层开!\n");
//break;
case 2:
printf("2层开!\n");
break;
case 3:
printf("3层开!\n");
break;
default:
printf("没有盖到这一层!\n");
break;
}
return 0;
}
break和continue
break
break如果用于循环是用来终止循环
break如果用于switch,则是用于终止switch
break不能直接用于if,除非if属于循环内部的一个子句
# include <stdio.h>
int main(void)
{
int i;
/*
switch (2)
{
case 2:
printf("哈哈!\n");
break; //OK,break可以用于switch
}
*/
for (i=0; i<3; i++)
{
if (3 > 2)
break; //break虽然是if内部的语句,但break终止的却是外部的for循环
printf("嘿嘿!\n"); //上面break终止掉了for循环,这个语句永远不会输出
}
return 0;
}
在多层循环中,break只能终止距离它最近的循环
# include <stdio.h>
int main(void)
{
int i, j;
for(i=0; i<3; i++)
{
for(j=1; j<4; ++j)
break; //break只能终止距离它最近的循环
printf("同志们好!\n");
}
return 0;
}
在多层switch嵌套中,break只能终止距离它最近的switch
# include <stdio.h>
int main(void)
{
int x = 1, y = 0, a = 0, b = 0;
switch(x) // 第一个switch
{
case 1:
switch(y) // 第二个switch
{
case 0:
a++;
break; //x为1进入到内层的switch, y=0进入到这个case,a++后break终止内层的switch
case 1:
b++;
break;
}
b = 100; //内层switch终止了,然后执行到这里,将100赋值给b
break; //然后执行这个break,这个break终止的是外层的switch , 所以结果是a=1, b=100
case 2:
a++;
b++;
break;
}
printf("%d %d\n", a , b); //1 100
return 0;
}
continue
用于跳过本次循环余下的语句,转去判断是否需要执行下次循环
1.
for (1; 2; 3)
{
A;
B;
continue; //如果执行该语句,则执行该语句后,会执行语句3,C和D都不会被执行
C;
D;
}
2.
while (表达式)
{
A;
B;
continue; //如果执行该语句,则执行该语句后,会执行表达式,C和D都不会被执行
C;
D;
}
三、数组
一. 为什么需要数组
为了解决大量同类型数据的存储和使用问题
为了模拟现实世界
二. 数组的分类
1. 一维数组
怎样定义一维数组
为n个变量连续分配存储空间
所有的变量数据类型必须相同
所有变量所占的字节大小必须相等
例子
int a[5];
一维数组名不代表数组中所有的元素,一维数组名代表数组第一个元素的地址
有关一维数组的操作
初始化
完全初始化
int a[5] = {1, 2, 3, 4, 5};
不完全初始化, 未被初始化的元素自动为零
int a[5] = {1, 2, 3};
不初始化, 所有元素是垃圾值
int a[5];
清零
int a[5] = {0};
错误写法:
int a[5];
a[5] = {1,2,3,4,5}; //错误
只有在定义数组的同时才可以整体赋值,其它情况下整体赋值都是错误的
int a[5] = {1, 2, 3, 4, 5};
a[5] = 100; // error ,因为没有a[5] 这个元素,最大只有a[4]
int a[5] = {1, 2, 3, 4, 5};
int b[5];
如果要把a数组中的值全部赋值给b数组
错误的写法 :
b = a; //error
正确的写法:
for (i=0; i<5; ++i)
b[i] = a[i];
赋值
排序
求最大/小值
倒置
# include <stdio.h>
int main(void)
{
int a[7] = {1, 2, 3, 4, 5, 6, 7};
int i, j;
int t;
i = 0;
j = 6;
while (i < j)
{
t = a[i];
a[i] = a[j];
a[j] = t;
i++;
--j;
}
for (i=0; i<7; ++i)
printf("%d\n", a[i]);
return 0;
}
查找
插入
删除
2. 二维数组
int a[3] [4]
总共是12个元素,可以当做3行4列看待, 这12个元素的名字依次是
a[0][0] a[0][1] a[0][2] a[0][3]
a[1][0] a[1][1] a[1][2] a[1][3]
a[2][0] a[2][1] a[2][2] a[2][3]
a[i][j] 表示第i+i行第j+1列的元素
int a[m][n]; 该二维数组右下角位置的元素只能是a[m-1][n-1]
初始化
int a[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
int a[3] [4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
操作
输出二维数组内容:
# include <stdio.h>
int main(void)
{
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int i, j;
//输出数组内容
for (i=0; i<3; ++i)
{
for (j=0; j<4; ++j)
printf("%-5d ",a[i][j]); //%-5d 的含义, -表示左对齐, 5表示占5个位置
printf("\n");
}
return 0;
}
对二维数组排序
求每一行的最大值
判断矩阵是否对称
矩阵的相乘
3. 多维数组
是否存在多维数组
不存在
因为内存是线性一维的
n维数组可以当做是每个元素是 n-1 维数组的一维数组
比如:
int a[3] [4];
该数组是含有3个元素的一维数组,只不过每个元素都可以再分成4个小元素
int a[3] [4] [5];
该数组是含有3个元素的一维数组,只不过每个元素都是4行5列的二维数组
四、函数【C语言的第二个重点】
一、为什么需要函数
避免了重复性操作
有利于程序的模块化
c语言的基本单位是函数
二、什么叫函数
逻辑上:
能够完成特定功能的独立的代码块
物理上:
能够接收数据[当然也可以不接收数据]
能够对接收的数据进行处理
能够将数据处理的结果返回[当然也可以不返回任何值]
总结:
函数是个工具, 它是为了解决大量类似问题而设计的
函数可以当做一个黑匣子
三、如何定义函数
函数的返回值 函数的名字 (函数的形参列表)
{
函数的执行体
}
1. 函数定义的本质是详细描述函数之所以能够实现某个特定功能的具体方法
2. return 表达式; 的含义:
1. 终止被调函数,向主调函数返回表达式的值
2. 如果表达式为空,则只终止函数,不向主调函数返回任何值
3. break是用来终止循环和switch的,return是用来终止函数的
例子:
void f()
{
return; //return只用来终止函数,不向主调函数返回任何值
}
int f()
{
return 10; //第一:终止函数, 第二:向主调函数返回10
}
3. 函数返回值的类型也称为函数的类型, 因为如果函数名前的返回值类型和函数执行体中的 return表达式; 中表达式的类型不同的话,则最终函数返回值的类型以函数名前的返回值类型为准
例子:
# include <stdio.h>
int f()
{
return 10.5; //因为函数的返回值类型是int 所以最终f返回的是10而不是10.5
}
int main(void)
{
int i = 99;
double x = 6.6;
x = f();
printf("%lf\n", x);
return 0;
}
四、函数的分类
有参函数 和 无参函数
有返回值函数 和 无返回值函数
库函数 和 用户自定义函数
值传递函数 和 地址传递函数 (严格意义上只有值传递)
普通函数 和 主函数(main)
一个程序必须有且只能有一个主函数
主函数可以调用普通函数。 普通函数不能调用主函数
普通函数可以相互调用
主函数是程序的入口, 也是程序的出口
判断一个数字是否是素数
# include <stdio.h>
bool isPrime(int val)
{
int i;
for (i=2; i<val; ++i)
{
if(val % i == 0)
break;
}
if (i == val)
return true;
else
return false;
}
int main(void)
{
int m;
scanf("%d", &m);
if (isPrime(m))
printf("Yes!\n");
else
printf("No!\n");
return 0;
}
五、注意的问题
1、函数的声明
函数调用和函数定义的顺序
如果函数调用写在了函数定义的前面,则必须加函数前置声明
函数前置声明:
1. 告诉编译器即将可能出现的若干个字母代表的是一个函数
2. 告诉编译器即将可能出现的若干个字母所代表的函数的形参和返回值的具体情况
3. 函数声明是一个语句,末尾必须加分号
4. 对库函数的声明是通过 # include <库函数所在的文件的名字.h> 来实现的 (如stdio.h , 是standard input output 标准的输入输出的意思, h表示头文件,head的意思。 总体就是标准的输入输出头文件的意思)
# include <stdio.h>
void f(void); //函数的声明, 分号不能丢掉
int main(void)
{
f();
return 0;
}
void f(void)
{
printf("哈哈!\n");
}
# include <stdio.h>
void g(void)
{
f(); //因为函数f的定义放在了调用f语句的后面,所以语法出错
}
void f(void)
{
printf("哈哈!\n")
}
int main(void)
{
g();
return 0;
}
# include <stdio.h>
/*
2023年01月29日21:05:47
一定要明白该程序为什么是错误的
一定要明白该程序第7行生效之后程序为什么就正确了
*/
void f(void); //声明f函数 顺序: main函数主入口进来,执行g函数,g
//函数在main函数上面声明了所以不会出错,然后g函数执行,
//内部执行f函数,f函数在g函数上面声明了,执行成功
void g(void)
{
f();
}
void f(void)
{
printf("哈哈!\n")
}
int main(void)
{
g();
return 0;
}
2、形参和实参
个数相同
位置一一对应
数据类型必须相互兼容
# include <stdio.h>
void f(int i, float x) //i , x是形参
{
printf("%d %f\n", i, x);
}
int main(void)
{
f(5, 6.6); //5 , 6.6是实参
return 0;
}
3、如何在软件开发中合理的设计函数来解决实际问题
一个函数的功能尽量独立,单一
多学习,多模仿牛人的代码
4、函数是C语言的基本单位,类是Java,C#,C++的基本单位
六、常用的系统函数
double sqrt (double x);
求的x的平方根
int abs (int x);
求x的绝对值
double fabs (double x);
求x的绝对值
七、专题
递归
可以参考郝斌老师的数据结构视频
八、变量的作用域和存储方式
按作用域分
全局变量
在所有函数外部定义的变量叫全局变量
全局变量使用范围:从定义位置开始到整个程序结束
局部变量
在一个函数内部定义的变量或者函数的形参, 都统称为局部变量
void f (int i)
{
int j = 20;
}
i 和 j 都属于局部变量
局部变量使用范围:只能在本函数内部使用
注意的问题:
全局变量和局部变量命名冲突的问题
在一个函数内部如果定义的局部变量的名字和全局变量名一样时,局部变量会屏蔽掉全局变量
按变量的存储方式
静态变量
自动变量
寄存器变量
五、指针(重点!)
# include <stdio.h>
int main(void)
{
int * p; //p是变量的名字 , int * 表示p变量存放的是 int 类型变量的地址
int i = 3;
p = &i; //OK
//p = i; //error ,因为类型不一致, p只能存放int类型变量的地址,不能存放int类型变量的值
//p = 55; //error 原因同上
return 0;
}
# include <stdio.h>
int main(void)
{
int * p; //p是变量的名字 , int * 表示p变量存放的是 int 类型变量的地址
// int * p; 不表示定义了一个名字叫 *p 的变量
// int * p; 应该这样理解: p 是变量名, p 变量的数据列是int *类型
// 所谓 int *; 类型就是存放int变量地址的类型
int i = 3;
int j;
p = &i;
/**
* @description
* 1. p 保存了 i 的地址,因此 p 指向 i
* 2. p 不是 i , i 也不是 p ,更准确的说: 修改 p 的值不影响 i 的值, 修改 i 的值也不影响 p 的值
* 3. 如果一个指针变量指向了某个普通变量, 则 *指针变量 就完全等同于 普通变量
* 例子:
* 如果 p 是个指针变量, 并且 p 存放了普通变量 i 的地址
* 则 p 指向了普通变量 i
* *p 就完全等同于 i
* 或者说: 在所有出现 *p 的地方都可以替换成 i
* 在所有出现 i 的地方都可以替换成 *p
*
* *p 就是以 p的内容 为地址的变量 (p相当于盒子的位置, *p是盒子里的东西; &取地址, *解地址)
*
*
* 理解:
* *p 是 p 所指向的对象的值, 即 i 的值
* p 是用来存放要读取数据的地址
* *p 是让编译器从指定的地址中读取出数据
* p 是一个指针变量的名字,表示此指针变量指向的内存地址
*
*/
j = *p;
printf("i = %d, j = %d\n", i, j); //i = 3, j = 3
return 0;
}
一、指针和指针变量的关系
指针就是地址, 地址就是指针
地址就是内存单元的编号(换句话说指针就是一个内存地址编号)
指针变量是存放地址的变量
指针和指针变量是两个不同的概念
但是要注意:
通常我们叙述时会把指针变量简称为指针,实际它们含义并不一样
二、指针的重要性
指针:
表示一些复杂的数据结构
快速的传递数据,减少了内存的耗用【重点】
使函数返回一个以上的值【重点】
能直接访问硬件
能够方便的处理字符串
是理解面向对象语言中引用的基础
总结: 指针是 C语言的灵魂
三、指针的定义
地址
内存单元的编号
从零开始的非负整数
范围:4G 【0 -- 4G-1】
指针
指针就是地址, 地址就是指针
指针变量就是存放内存单元编号的变量。或者说指针变量就是存放地址的变量
指针和指针变量是两个不同的概念
但是要注意: 通常我们叙述时会把指针变量简称为指针,实际它们含义并不一样
指针的本质就是一个操作受限的非负整数 (只能相减运算)
四、指针的分类
1、基本类型指针
int * p; //p是变量的名字 , int * 表示p变量存放的是 int 类型变量的地址
// int * p; 不表示定义了一个名字叫 *p 的变量
// int * p; 应该这样理解: p 是变量名, p 变量的数据列是int *类型
// 所谓 int *; 类型就是存放int变量地址的类型
int i = 3;
int j;
p = &i;
/**
* @description
* 1. p 保存了 i 的地址,因此 p 指向 i
* 2. p 不是 i , i 也不是 p ,更准确的说: 修改 p 的值不影响 i 的值, 修改 i 的值也不影响 p 的值
* 3. 如果一个指针变量指向了某个普通变量, 则 *指针变量 就完全等同于 普通变量
* 例子:
* 如果 p 是个指针变量, 并且 p 存放了普通变量 i 的地址
* 则 p 指向了普通变量 i
* *p 就完全等同于 i
* 或者说: 在所有出现 *p 的地方都可以替换成 i
* 在所有出现 i 的地方都可以替换成 *p
*
* *p 就是以 p的内容 为地址的变量 (p相当于盒子的位置, *p是盒子里的东西; &取地址, *解地址)
*
*
* 理解:
* *p 是 p 所指向的对象的值, 即 i 的值
* p 是用来存放要读取数据的地址
* *p 是让编译器从指定的地址中读取出数据
* p 是一个指针变量的名字,表示此指针变量指向的内存地址
*
*/
j = *p;
printf("i = %d, j = %d\n", i, j); //i = 3, j = 3
基本类型指针常见错误解析
# include <stdio.h>
int main(void)
{
int i = 5;
int * p;
int * q;
p = &i;
//*q = p; //error 语法编译会出错
//*q = *p; //error 语法没错,但是 q 还没赋值,没有指向。 程序将内存空间分配给了q, 你可以读q这个空间的值,此时是垃圾值。 但是你不能用 *q , 因为 *q 代表的是,读 q 指向的地址的内容,但是此时 q 还没有指向。所以你不能用 *q。 如果你读写了就代表你获取了一个你不知道的单元的值,相当于获取了别人的隐私
p = q; //q是垃圾值, q赋给p, p也变成垃圾值
printf("%d\n", *q); //q的空间是属于本程序的, 所以本程序可以读写q的内容,
//但是如果q内部是垃圾值,则本程序不能读写 *p 的内容
//因为此时 *q 所代表的内存单元的控制权限并没有分配给本程序
//所以本程序运行到 printf("%d\n", *q); 时就会立即出错
return 0;
}
经典指针程序_互换两个数字
# include <stdio.h>
// 前置声明的形参名可以不写
void huhuan_1(int , int);
void huhuan_2(int *, int *);
void huhuan_3(int *, int *);
int main(void)
{
int a = 3;
int b = 5;
huhuan_3(&a, &b); // huhuan_2(*p, *q); 是错误的, huhuan_2(a, b); 也是错误的
printf("a = %d, b = %d\n", a, b);
return 0;
}
//不能完成互换功能
void huhuan_1(int a, int b)
{
int t;
t = a;
a = b;
b = t;
return;
}
// 不能完成互换功能
void huhuan_2(int * p, int * q)
{
int * t; //如果要互换p和q的值,则t必须是int * ,不能是int,否则会出错
t = p;
p = q;
q = t;
// 这个程序结果仍然是 a = 3, b = 5; a和b的值没有互换
// 因为互换的是形参p 和 q的地址, 实参a 和 b将地址赋给了形参p 和 q, 形参地址的改变不会影响到实参a 和 b , p和q可以看作是中间变量
}
// 可以完成互换功能
void huhuan_3(int * p, int * q)
{
int t; //如果要互换 *p 和 *q 的值,则t必须定义成int, 不能定义成int *, 否则语法会出错
t = *p; // p是 int *类型, *p 是 int类型,因为 *p 代表的是以p的内容为地址的变量,*p为内容值
*p = *q;
*q = t;
}
附注(*号的含义):
* 的含义
1. 乘法
2. 定义指针变量
int p; //定义了一个名字叫p的变量, int 表示p只能存放int变量的地址
3. 指针运算符
该运算符放在已经定义好的指针变量的前面
如果p是一个已经定义好的指针变量,则 *p 表示 以p的内容为地址的变量
如何通过被调函数修改主调函数普通变量的值
1.实参必须为该普通变量的地址
2.形参必须为指针变量
3.在被调函数中通过 *形参名 = ..... 的方式就可以修改主调函数相关变量的值
2、指针和数组
指针和一维数组
一维数组名
一维数组名是个指针常量
它存放的是一维数组第一个元素的地址
# include <stdio.h>
int main(void)
{
int a[5]; //a是数组名 5是数组元素的个数 元素就是变量 a[0] - a[4]
// int a[3][4]; //3行4列 a[0][0] 是第一个元素 a[i][j] 第i+1行j+1列
int b[5];
//a = b; //error a是常量
printf("%#X\n",&a[0]);
printf("%#X\n",a);
return 0;
}
/*
输出结果是:
0X6DC7A934
0X6DC7A934
总结:
● 一维数组名
○ 一维数组名是个指针常量
○ 它存放的是一维数组第一个元素的地址
*/
下标和指针的关系
如果p是个指针变量,则p[i] 永远等价于 *(p+i)
界定一个一维数组需要几个参数【如果一个函数要处理一个一维数组,则需要接收该数组的那信息】
需要两个参数:
数组的第一个元素的地址
数组的长度
# include <stdio.h>
//f函数可以输出任何一个一维数组的内容
void f(int * pArr, int len)
{
int i;
for (i=0; i<len; ++i)
printf("%d ", *(pArr+i) ); //*pArr *(pArr+1) *(pArr+2)
printf("\n");
}
int main(void)
{
int a[5] = {1,2,3,4,5};
int b[6] = {-1,-2,-3,4,5,-6};
int c[100] = {1,99,22,33};
f(a, 5); //a 是 int *
f(b, 6);
f(c,100);
return 0;
}
/**
* @file 确定一个一维数组需要几个参数_2.cpp
* @author Ricardo
* @brief 一定要明白 15行的pArr[3] 和 21行 23行的a[3] 是同一个变量
* @version 0.1
* @date 2023-05-21
*
* @copyright Copyright (c) 2023
*
*/
# include <stdio.h>
void f(int * pArr, int len)
{
pArr[3] = 88; //15行 pArr[3] 等价于 *(pArr+3)
}
int main(void)
{
int a[6] = {1,2,3,4,5,6};
printf("%d\n", a[3]); //21行
f(a, 6);
printf("%d\n", a[3]); //23行
return 0;
}
/**
* @brief 输出结果
* 4
* 88
*
*/
# include <stdio.h>
void f(int * pArr, int len)
{
int i;
for (i=0; i<len; ++i)
printf("%d ", pArr[i]); //*(pArr+i) 等价于 pArr[i] 也等价于 b[i]
printf("\n");
}
int main(void)
{
int a[5] = {1,2,3,4,5};
int b[6] = {-1,-2,-3,4,5,-6};
int c[100] = {1,99,22,33};
f(b, 6);
return 0;
}
指针变量的运算
指针变量不能相加 不能相乘 也不能相除
如果两个指针变量指向的是同一块连续空间中的不同存储单元,则这两个指针变量才可以相减
/**
* @author : Ricardo
* @date : 2023/10/6
* @description : 指针变量的运算
*/
# include <stdio.h>
int main(void)
{
int i = 5;
int j = 10;
int * p = &i;
int * q = &j;
int a[5];
p = &a[1];
q = &a[4];
printf("p和q所指向的单元相隔%d个单元\n", q-p); //p和q所指向的单元相隔3个单元
// p - q 没有实际意义
return 0;
}
一个指针变量到底占几个字节
预备知识
sizeof(数据类型)
功能:返回值就是该数据类型所占的字节数
例子:sizeof(int) = 4 sizeof(char) = 1 sizeof(double) = 8
sizeof(变量名)
功能:返回值是该变量所占的字节数
假设p指向char类型变量(1个字节)
假设q指向int类型变量(4个字节)
假设r指向double类型变量(8个字节)
p q r 本身所占的字节数是否一样
答案: p q r 本身所占的字节数是一样的
总结:
一个指针变量,无论它指向的变量占几个字节,该指针变量本身只占四个字节
一个变量的地址使用该变量首字节的地址来表示
指针和二维数组
3、指针和函数
4、指针和结构体
5、多级指针
/**
* @author : Ricardo
* @date : 2023/10/8
* @description : 多级指针
*/
# include <stdio.h>
int main(void)
{
int i = 10;
int * p = &i;
int ** q = &p;
int *** r = &q;
//r = &p; //error 因为r是int *** 类型, r只能存放int ** 类型变量的地址
printf("i = %d\n", ***r); //10 ***r 等价于 i
return 0;
}
如图:
i 只能存放整数, 所以 i 是 int类型
p 存放的是 int 的地址, int的地址即 int , 所以 p 是 int 类型的指针变量
q 存放的是 p 的地址,p 是 int 类型的指针变量, 所以 q 是 int* 类型的指针变量
r 存放的是 q 的地址, q 是 int** 类型的指针变量, 所以 r 是 int*** 类型的指针变量
如代码 ***r 等价于 i;
*r 是以 r 的内容为地址的变量,即 q
那 **r 就是以 q 内容为地址的变量 ,q 存放的是 p 的地址
那么 ***r 就是 以p的内容为地址的变量, p 存放的是 i 的地址
所以 ***r 等价于 i
/**
* @author : Ricardo
* @date : 2023/10/8
* @description : 多级指针_2
*/
# include <stdio.h>
void f(int ** q)
{
//在f函数里面修改g函数里面的一个指针变量的值
//*q就是p //*q即以q的内容为地址的变量,q是 int ** 类型指针变量,里面只能存放int * 类型的指针变量地址,q的内容就是p,所以 *q就是p
}
void g()
{
int i = 10;
int * p = &i;
f(&p); //p是 int * 类型, &p 是int ** 类型
}
int main(void)
{
g();
return 0;
}
专题:(重点)
动态内存分配【重点难点】
传统数组的缺点:
数组长度必须事先指定,且只能是常整数,不能是变量
例子:
int a[5]; //OK
int len = 5; int a[len]; //error
传统形式定义的数组,该数组的内存程序员无法手动释放
数组一旦定义,系统为该数组分配的存储空间就会一直存在,除非数组所在的函数运行结束
在一个函数运行期间,系统为该函数中数组所分配的空间会一直存在,直到该函数运行完毕时,数组的空间才会被系统释放
数组的长度一旦定义,其长度就不能再更改
数组的长度不能在函数运行的过程中动态的扩充或缩小
传统方式定义的数组不能跨函数使用
A函数定义的数组,在A函数运行期间可以被其它函数使用,但A函数运行完毕之后,A函数中的数组将无法再被其它函数使用
为什么需要动态分配内存
动态数组很好的解决了传统数组的这4个缺陷
传统数组也叫静态数组
动态内存分配举例_ 动态数组的构造
1.malloc函数
/**
* @author : Ricardo
* @date : 2023/10/7
* @description : malloc 是 memory(内存) allocate(分配)的缩写
*/
//在mac上写cpp程序,无法使用malloc函数解决方法
//1. 导入 stdlib.h
//2. 在malloc.h前加上sys/ 即 # include <sys/malloc.h>
# include <stdio.h>
# include <stdlib.h>
# include <sys/malloc.h>
int main(void)
{
int i = 5; //分配了4个字节 静态分配
int * p = (int *)malloc(4); //16行
/*
* 1. 要使用malloc函数,必须添加malloc.h这个头文件
* 2. malloc函数只有一个形参,并且形参是整型
* 3. malloc(4) ,4表示请求系统为本程序分配4个字节
* 4. malloc函数只能返回第一个字节的地址 (所以需要强制类型转换,如转换为int *类型,来告知malloc函数返回第一个字节的地址的这个变量到底占几个字节)
* 5. 16行分配了8个字节,p变量占4个字节,p指向的内存也占4个字节
* 6. p本身所占的内存是静态分配的,而p所指向的内存是动态分配的
*/
*p = 5; //*p 代表的就是一个int变量,只不过 *p 这个整形变量的内存分配方式和15行的i变量的分配方式不同
free(p); //free(p) 表示把p所指向的内存给释放掉 p本身的内存是静态的,不能由程序员手动释放,p本身的内存只能在p变量所在的函数运行终止时由系统自动释放
printf("逆流河上万仙退,爱情不敌减坚持泪\n");
return 0;
}
2.动态构造一维数组
/**
* @author : Ricardo
* @date : 2023/10/8
* @description : 动态一维数组
*/
# include <stdlib.h>
# include <stdio.h>
int main(void)
{
int a[5]; //如果int占4个字节的话,则本数组总共包含20个字节,每四个字节被当做一个int变量来使用
int len;
int * pArr;
int i;
printf("请输入你要存放的元素的个数:");
scanf("%d", &len);
pArr = (int *)malloc(4 * len); //本行动态的构造了一个一维数组 ,该数组的数组名是pArr,该数组的每个元素时int类型。 类似于 int pArr[len];
//对一维数组进行操作, 如:对动态一维数组进行赋值
printf("请对数组元素进行赋值:\n");
for (i = 0; i < len; ++i)
scanf("%d", &pArr[i]);
//对一维数组进行输出
printf("一维数组的内容是:\n");
for (int i = 0; i < len; ++i)
printf("%d",pArr[i]);
//可以使用 realloc(pArr指向的内存, 要扩充字节数); 这个函数对动态分配的数组进行扩充 《reallocated重新分配》
//realloc(pArr, 20);
free(pArr); //释放掉动态分配的数组
return 0;
}
静态内存和动态内存的比较
静态内存
静态内存是由系统自动分配,由系统自动释放
静态内存是在栈分配的
动态内存
动态内存是由程序员手动分配,手动释放
动态内存是在堆分配的
跨函数使用内存的问题
/**
* @author : Ricardo
* @date : 2023/10/14
* @description : 跨函数使用内存_静态变量不能跨函数使用
*/
# include <cstdio>
void f(int ** q) //q是指针变量,无论q是什么类型的指针变量,都只占4个字节
{
int i = 5;
//*q等价于p q和**q都不等价于p
//*q = i; //error 因为 *q = i;等价于 p = i; 这样写是错误的
*q = &i; // p = &i;
}
int main(void)
{
int * p;
f(&p);
printf("%d\n",*p); // 本语句语法没有问题,但逻辑上有问题
//21行的f函数执行完毕之后,p指向的那个变量i已经不存在了,或者说p指向的那个i变量的访问权限已经返回给操作系统了,不能再让你去使用和访问
//而22行 *p 把i的空间给读出来了,这样是不对的
//p去保存i的地址是可以的,但是f函数执行完,i变量被释放后,你不能去访问i空间,因为i空间被释放,这个程序是有问题的
//这个程序说明一个问题,我们静态变量,不能跨函数去使用。更严格的讲,静态变量是当这个函数被终止的之后,不能再被其它函数所使用了
return 0;
}
/**
* @author : Ricardo
* @date : 2023/10/14
* @description : 动态内存可以跨函数使用
*/
# include <cstdio>
# include <cstdlib>
void f(int ** q)
{
*q = (int *)malloc(sizeof(int)); //sizeof(数据类型) 返回值是该数据类型所占的字节数
//等价于 p = (int *)malloc(sizeof(int));
//q = 5; //error q就是
//*q = 5; //*q就是p, q里面存的是p的地址,这样就相当于把 5 赋给 p了, p里面只能存地址不能存整数。 所以*q = 5是错误的
**q = 5; //*q = p; **q = *p; *p = 5; 正确的
}
int main(void)
{
int * p;
f(&p);
printf("%d\n", *p);
free(p);
return 0;
}
五、结构体
一、为什么需要结构体
为了表示一些复杂的事物,而普通的基本类型无法满足实际要求
二、什么叫结构体
把一些基本类型数据组合在一起形成的一个新的复合数据类型,这个叫做结构体
三、如何定义结构体
三种方式
推荐使用第一种
/**
* @author : Ricardo
* @date : 2023/10/15
* @description : 如何定义结构体
*/
# include <cstdio>
//第一种方式 这只是定义了一个新的数据类型,并没有定义变量
struct Student
{
int age;
float score;
char sex;
};
//第二种方式
struct Student2
{
int age;
float score;
char sex;
} st2;
//第三种方式
struct
{
int age;
float score;
char sex;
} stu3;
int main(void)
{
struct Student st = {80, 66.6, 'F'};
return 0;
}
四、怎么使用结构体变量
赋值和初始化
定义的同时可以整体赋初始值
如果定义完之后,则只能单个的赋初始值
/**
* @author : Ricardo
* @date : 2023/10/15
* @description : 结构体变量的赋值和初始化
*/
# include <cstdio>
struct Student
{
int age;
float score;
char sex;
};
int main(void)
{
struct Student st = {80, 66.6, 'F'}; //初始化 定义的同时赋初始值
struct Student st2;
st2.age = 10;
st2.score = 88;
st2.sex = 'F';
printf("%d %f %c\n", st.age, st.score, st.sex);
printf("%d %f %c\n", st2.age, st2.score, st2.sex);
}
如何取出结构体变量中的每一个成员 (重点)
结构体变量名.成员名
指针变量->成员名 (第二种方式更常用)
指针变量->成员名 在计算机内部会被转化成 (*指针变量名).成员名 的方式来执行
所以说这两种方式是等价的
/**
* @author : Ricardo
* @date : 2023/10/15
* @description :
*/
/**
* @author : Ricardo
* @date : 2023/10/15
* @description : 如何取出结构体变量中的每一个成员
*/
# include <cstdio>
struct Student
{
int age;
float score;
char sex;
};
int main(void)
{
struct Student st = {80, 66.6F, 'F'}; //初始化 定义的同时赋初始值
struct Student * pst = &st; //&st不能改成st
pst -> age = 88; //第二种方式
st.score = 66.6f; //第一种方式 66.6在C语言中默认是double类型,如果希望一个实数是float类型,则必须在末尾加f或F,因此66.6是double,66.6f或66.6F是float
printf("%d %f\n", st.age, pst -> score);
return 0;
}
pst -> age 在计算机内部会被转化成 (*pst).age ,没有什么为什么,这就是 -> 的含义,这是一种硬性规定
所以 pst -> age 等价于 (*pst).age 也等价于 st.age
我们之所以知道 pst -> age 等价于 st.age ,是因为 pst -> age 是被转化成了 (*pst).age 来执行
pst - age 的含义:
pst 所指向的那个结构体变量中的 age 这个成员
结构体变量和结构体指针变量作为函数参数传递的问题
/**
* @author : Ricardo
* @date : 2023/10/15
* @description : 通过函数完成对结构体变量的输入和输出
*/
# include <cstdio>
# include <cstring>
struct Student
{
int age;
char sex;
char name[100];
};
void inputStudent(struct Student * pstu);
void outputStudent(struct Student ss);
int main(void)
{
struct Student st;
inputStudent(&st); //对结构体变量输入
printf("%d %c %s\n", st.age, st.sex, st.name);
outputStudent(st); //对结构体变量输出 可以发送st的地址也可以直接发送st的内容
return 0;
}
void outputStudent(struct Student ss)
{
printf("%d %c %s\n", ss.age, ss.sex, ss.name);
}
void inputStudent(struct Student * pstu) //pstu只占4个字节
{
(*pstu).age = 10;
strcpy(pstu -> name, "路明非");
pstu -> sex = 'F';
}
/*
//本函数无法修改主函数中 st 的值,所以本函数是错误的
void inputStudent(struct Student stu)
{
// stu.age = 10;
// strcpy(stu.name, "路明非"); //不能写成 stu.name = "张三";
// stu.sex = 'F';
}
*/
发送内容还是地址
/**
* @author : Ricardo
* @date : 2023/10/15
* @description : 示例:
* 发送地址还是发送内容
* 目的:
* 指针的优点之一:
* 快速的传递数据,减少了内存的耗用
* 耗用内存小
* 执行速度快
*/
# include <cstdio>
# include <cstring>
struct Student
{
int age;
char sex;
char name[100];
};
void inputStudent(struct Student * pstu);
void outputStudent(struct Student * pst);
int main(void)
{
struct Student st;
//printf("%d\n", sizeof(st)); //108
inputStudent(&st); //对结构体变量输入
outputStudent(&st); //对结构体变量输出 可以发送st的地址也可以直接发送st的内容 但为了减少内存的耗费,也为了提高执行速度,推荐发送地址
return 0;
}
void outputStudent(struct Student * pst)
{
printf("%d %c %s\n", pst -> age, pst -> sex, pst -> name);
}
void inputStudent(struct Student * pstu) //pstu只占4个字节
{
(*pstu).age = 10;
strcpy(pstu -> name, "路明非");
pstu -> sex = 'F';
}
推荐使用结构体指针变量作为函数参数来传递
结构体变量的运算
结构体变量不能相加,不能相减,也不能相互乘除
但结构体变量可以相互赋值
例:
struct Student
{
int age;
char sex;
char name[100];
}; //分号不能省
int main(void)
{
struct Student st1, st2;
//st1 + st2; //错误的
//st1 * st2; //错误的
//st1/st2; //错误的
st1 = st2; //正确的
st2 = st1; //正确的
return 0;
}
五、冒泡排序
/**
* @author : Ricardo
* @date : 2023/10/21
* @description : 冒泡排序
*/
# include <cstdio>
void sort(int * pArr, int len)
{
int i, j, t;
for (i = 0; i < len -1; ++i)
{
for (j = 0; j < len -1; ++j)
{
if (pArr[j] > pArr[j+1]) // >表示升序, <表示降序
{
t = pArr[j];
pArr[j] = pArr[j+1];
pArr[j+1] = t;
}
}
}
}
int main(void)
{
int a[6] = {10, 2, 8, -8, 11, 0};
int i;
sort(a, 6); //确定一个数组,需要2个参数。 首地址(数组的变量指向的就是数组的第一个元素地址) 和 元素个数(数组长度)
for (i = 0; i < 6; ++i)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
六、举例:动态的构造存放学生信息的结构体数组
动态构造一个数组,存放学生的信息。然后按分数排序输出
/**
* @author : Ricardo
* @date : 2023/10/29
* @description :
*/
# include <cstdio>
# include <cstdlib>
struct Student
{
int age;
float score;
char name[100];
};
int main(void)
{
int len;
Student * pArr;
int i, j;
struct Student t;
//动态构造一维数组
printf("请输入学生的个数:\n");
printf("len = ");
scanf("%d", &len);
pArr = (struct Student *)malloc(len * sizeof(struct Student));
for (i = 0; i < len; ++i)
{
printf("请输入第%d个学生的信息: ", i + 1);
printf("age = ");
scanf("%d", &pArr[i].age);
printf("name = ");
scanf("%s", pArr[i].name);// name是数组名,本身就已经是数组首元素的地址,所以 pArr[i].name 不能改成 &pArr[i].name
printf("score = ");
scanf("%f", &pArr[i].score);
}
printf("\n\n学生的信息是:\n");
//输出
for (i = 0; i < len; ++i)
{
printf("第%d个学生的信息是 :\n", i + 1);
printf("age = %d\n", pArr[i].age);
printf("name = %s\n", pArr[i].name);
printf("score = %f\n", pArr[i].score);
printf("\n");
}
//按学生成绩升序排序 冒泡排序
for (i = 0; i < len - 1; ++i)
{
for (j = 0; j < len - 1 -i; ++j)
{
if (pArr[j].score > pArr[j+1].score) //升序
{
t = pArr[j];
pArr[j] = pArr[j+1];
pArr[j+1] = t;
}
}
}
return 0;
}
七、枚举
1、什么是枚举
把一个事物所有可能的取值一一列举出来
2、怎样使用枚举
/**
* @author : Ricardo
* @date : 2023/10/29
* @description : 枚举
*/
# include <stdio.h>
//只定义了一个数据类型,并没有定义变量, 该数据类型的名字是 enum WeekDay
enum WeekDay
{
MonDay, TuesDay, WedensDay, ThursDay, FriDay, SaturDay, SunDay
};
int main(void)
{
//int day; //day定义成int类型不合适
enum WeekDay day = SunDay;
printf("%d\n",day);
return 0;
}
3、枚举的优缺点
代码更安全
书写麻烦
八、进制转换
进制
什么叫进制
进制就是逢几进一
我们说N进制实际就是指逢N进一
我们计算机只识别二进制
人类最习惯使用的是十进制
为了实际需要,我们又建立了八进制和十六进制
C语言规定半女成前要加0(注意是零不是字母o),十六进制前要加0x或0X,十进制前什么都不加!
不同数制数的表示
在汇编中:在数字后加字母B表示二进制数,加字母O表示八进制数,加字母D表示十进制数,加字母H表示十六进制数。
例:
1011B为二进制数1011,也记为(1011)2
1357O为八进制数1357,也记为(1357)8
2049D为十进制数2049,也记为(2049)10
3FB9H为十六进制数3FB9,也记为(3FB9)16
(一)什么叫n进制
十进制
十个基数,逢10进一
基数:0 1 2 3 4 5 6 7 8 9
二进制
二个基数,逢二进一
基数:0 1
2 -> 10
3 -> 11
4 -> 100
5 -> 101
八进制
8个基数,逢8进一
基数:0 1 2 3 4 5 6 7
8 -> 10
9 -> 11
10 -> 12
十六进制
16个基数,逢16进一
基数:0 1 2 3 4 5 6 7 8 9 A B C D E F 或 0 1 2 3 4 5 6 7 8 9 a b c d e f
16 -> 10
17 -> 11
18 -> 12
19 -> 13
常用计数制对照表
(二)进制转换
预备知识
小数除以大叔 则商为零 余数是小数本身
如:
1/2 = 0 余数1
2/2 = 1 余数0
3/2 = 1 余数1
十进制整数化为二进制
(185)10 = (?)2
(185)10 = (?)8
(3981)10 = (?)16
总结:十进制转r进制
方法:除r取余,直至商0,余数倒序排序
r进制转换为十进制
预备知识
先来复习一下十进制的1234是怎么被计算出来的
(三)二进制与十六进制的转化
二进制到十六进制
十六进制到二进制
(四)二进制与八进制相互转化
二进制到八进制
八进制到二进制
(五)十六进制与八进制相互转化
不存在十六进制与八进制的直接相互转化,都是以二进制为中间进制来进行转化的
补码
原码
也叫 符号-绝对值码
最高位0表示正 1表示负 ,其余二进制位是该数字的绝对值的二进制位
原码简单易懂,加减运算复杂,存在加减乘除的四种运算,增加了CPU的复杂度
零的表示不唯一
反码
反码运算不便,也没有在计算机中应用
移码
移码表示数值平移n位,n称为移码量
移码主要用于浮点数的阶码的存储
补码
已知十进制求二进制
求正整数的二进制
除2取余,直至商为零,余数倒序排序
求负整数的二进制
先求与该负数相对应的正整数的二进制代码 ,然后将所有位取反,末位加1,不够位数时左边补1(注意是加1之后补的1)
如(-3)的补码
先求的-3对应的正整数3的二进制代码是 011,取反得到100,末位加1变成101。
不够位数时指:如果把-3放到一个整形int里面,int 占4个字节,每个字节8个bit,一共就是32位。所以前面要补29个1
求零的二进制
全是零
已知二进制求十进制
如果首位是0,则表明是正整数,按普通方法来求
如果首位是1,则表明是负整数
将所有位取反,末位加1,所得数字就是该负数的绝对值
例:1001010
先取反 0110101 ,然后加1 是 0110110,对应16 进制是 (3 6)16 ,对应10进制是6*160 + 3*161 = 54 ,所以最终结果是 -54
如果全是零,则对应的十进制数字就是零
学习目标:
在Vc++6.0中一个int类型的变量所能存储的数字的范围是多少
int类型变量所能存储的最大整数用十六进制最大的整数:7FFFFFFF
int类型变量所能存储的绝对值最大的负整数用十六进制最大的整数:80000000
具体可以参见
绝对值最小负数的二进制代码是多少
最大正数的二进制点是多少
已知一个整数的二进制代码求出原始的数字
数字超过最大正数会怎样
不同类型数据的相互转化
六、链表
1、算法:
通俗定义:
解题的方法和步骤
狭义定义:
对存储数据的操作
对不同的存储结构,要完成某一个功能所执行的操作是不一样的
比如:
要输出数组中所有的元素的操作和要输出链表中所有元素的操作肯定是不一样的
这说明:
算法是依附于存储结构的
不同的存储结构,所执行的算法是不一样的
广义定义:
广义的算法也叫泛型
无论数据是如何存储的,对该数据的操作都是一样的
我们至少可以通过两种结构来存储数据
数组
优点:
存储速度快
缺点:
需要一个连续的很大内存
插入和删除元素的效率很低
链表
专业术语:
头节点
头节点的数据类型和首节点的类型是一模一样的
头节点是首节点前面的那个节点
头节点并不存放有效数据
设置头节点的目的是为了方便对链表的操作
头指针
存放头节点地址的指针变量
首节点
存放第一个有效数据的节点
尾节点
存放最后一个有效数据的节点
确定一个链表需要一个参数
头指针
优点:
插入删除元素效率高
不需要一个连续的很大的内存
缺点:
查找某个位置的元素效率低
/**
* @author : Ricardo
* @date : 2023/11/14
* @description :
*/
# include <cstdio>
# include <cstdlib>
// 定义了一个链表结点的数据类型
struct Node
{
int data; //数据域
struct Node * pNext; //指针域
}; //NODE等价于 struct Node PNODE等价于struct Node *
//函数声明
struct Node * create_list(void);
void traverse_list(struct Node * pHead);
int main(void)
{
struct Node * pHead = NULL; //pHead用来存放链表头节点的地址。 等价于 struct Node * pHead = NULL;
pHead = create_list(); //create_list()功能:创建一个非循环单链表,并将链表信息(头节点地址)返回给主函数
traverse_list(pHead);
return 0;
}
struct Node * create_list(void)
{
int len; //用来存放有效节点的个数
int i;
int val; //用来临时存放用户输入的节点的值
//分配了一个不存放有效数据的头节点
struct Node * pHead = (struct Node *)malloc(sizeof(struct Node));
if (NULL == pHead)
{
printf("分配失败,程序终止!\n");
exit(-1); //终止程序
}
struct Node * pTail = pHead;
pTail -> pNext = NULL;
printf("请输入您需要生成的链表节点的个数: len = ");
scanf("%d", &len);
for (i = 0; i < len; ++i)
{
printf("请输入第%d个节点的值:", i+1);
scanf("%d", &val);
struct Node * pNew = (struct Node *) malloc(sizeof(struct Node));
if (NULL == pNew)
{
printf("分配失败,程序终止!\n");
exit(-1); //终止程序
}
pNew -> data = val;
pTail -> pNext = pNew;
pNew -> pNext = NULL;
pTail = pNew;
}
return pHead;
}
void traverse_list(struct Node * pHead)
{
struct Node * p = pHead -> pNext;
while (NULL != p)
{
printf("%d ", p -> data);
p = p -> pNext;
}
printf("\n");
}
七、位运算符
&
-- 按位与
&& 逻辑与,也叫并且
&&与&的含义完全不同
1&1=1
1&0=0
0&1=0
0&0=0
5&7=5 21&7=5
5&1=1 5&10=0
/**
* @author : Ricardo
* @date : 2023/11/19
* @description : 按位与
*/
# include <cstdio>
int main(void)
{
int i = -5;
int j = 7;
int k;
k = i & j;
printf("%d\n",k); //3
k = i && j; //1 k的值只能是1或0, 因为&&是逻辑运算符,逻辑运算符的结果只能是真或假,在C中,真用1表示,假用0表示
printf("%d\n",k);
return 0;
}
|
|| 逻辑或
| 按位或
1 | 0 = 1
1 | 1 = 1
0 | 1 = 1
0 | 0 = 0
~
-- 按位取反
~i 就是把i变量所有的二进制位取反
^
-- 按位异或
相同为零
不同为1
1^0=1
0^1=1
1^1=0
0^0=0
<<
-- 按位左移
i<<3
表示把i的所有二进制位左移3位,右边补零
左移n位相当于乘以2的n次方
面试题:
A) i = i*8;
B) i = i<<3;
请问上述两个语句,哪个语句执行的速度快
答案:B快
>>
-- 按位右移
i>>3
表示把i的所有二进制位右移3位,左边一般是补0,当然也可能补1
右移n位相当于除以2的n次方,前提是数据不能丢失
面试题:
A) i = i/8;
B) i = i>>3;
请问上述两个语句,哪个语句执行的速度快
答案:B快
位运算符的显示意义
通过位运算符我们可以对数据的操作精确到每一位