1.C语言编程预备知识
1.1数据类型
基本数据类型
整数
整型 int 4
短整型 short int 2
长整型 long int 8
浮点数 (实数)
单精度浮点数 float 4
双精度浮点数 double 8
字符
字符 char 1
复合类型数据
结构体
枚举
共用体
1.2常量在C语言中的表示
整数
十进制:传统的写法
十六进制:前面加0X或0x
八进制:前面加0 注意:是数字0不是字母o
浮点数
传统的写法
float x = 3.2;//传统
科学计数法
float x = 3.2e3;//x的值是 3200
float x = 123.45e-2;//x的值是1.2345
字符
单个字符用单引号括起来
'A’表示字符A
‘AB’ 错误
“AB” 正确
字符串用双引号括起来
“A” 正确,因为“A”表示‘A’和‘\0’的组合(‘\0’结束符)
1.3常量在计算机的存储
整数是以补码的形式转化为二进制代码存储在计算机中的;
实数以IEEE754标准转化为二进制代码存储在计算机中的;
字符本质实际也是与整数的存储方式相同。
1.4 ASCII码
ASCII码不是一个值 ,而是一种规定;
ASCII规定了不同的字符是使用哪个整数值去表示;
它规定了
‘A’——65
‘B’——66
‘a’——97
‘b’——98
‘0’——48
2.基本的输入输出函数
printf四种用法
- printf(“字符串”);
- printf(“输出控制符”,输出参数);
- printf(“输出控制符1 输出控制符2…”,输出参数1,输出参数2…); //输出控制符和输出参数的个数必须一一对应
- printf(“输出控制符 非输出控制符”,输出参数);
输出控制符包含如下
%d——int
%ld——long int
%c——char
%f——float
%lf——double
%x( %X或%#X或%#x)——int或long int或short int (推荐使用%#X)
%o——同上
%s——字符串
scanf的用法
通过键盘将数据输入到变量中
两种用法:
用法一:scanf(“输入控制符”,输入参数); //‘&’取址运算符
功能:将从键盘输入的字符转化为输入控制符所规定格式的数据,然后存入以输入参数的值为地址的变量中;
用法二:scanf(“非输入控制符 输入控制符”,输入参数);
功能:将从键盘输入的字符转化为输入控制符所规定格式的数据,然后存入以输入参数的值为地址的变量中; 非输入控制符必须原样输入,包括逗号。
如何使用scanf编写出高质量代码
- 使用scanf之前最好先使用printf提示用户以什么样的方式来输入
- scanf中尽量不要使用非输入控制符,尤其是不要用 ‘\n’.应该编写代码对用户的非法输入做适当的处理【非重点】
char ch;
while ( (ch = getchar()) != ’\n’)
continue;
3.运算符
算术运算符
+ - * /(除) %(取余数)
除法‘/’的运算结果只和运算对象的数据类型有关,两个数都是int,则商就是int,若商有小数,则截取小数部分;被除数和除数中只要有一个或两个都是浮点型数据,则商也是浮点型,不截取小数部分。
如 :16/3== 3 16/5.0 == 3.20000
int main()
{
float i = 10;
i = 1 + 1 / 2;
printf("%f \n", i);
return 0;
}
/*
在VSCode中的输出结果是:
------------------------
1.000000
------------------------
*/
取余‘%’的运算对象必须是整数,结果是整除后的余数,其余数的符号与被除数相同。
如: 13%3== 1 -13%3 == -1 13%-3==1
关系运算符
> >= < <= !=(不等于) ==(等于)
逻辑运算符
! (非) &&(并且) ||(或)
!真 假
!假 真
真&&真 真
真&&假 假
假&&真 假
假&&假 假
真||假 真
假||真 真
真||真 真
假||假 假
C语言对真假的处理
非零 真
零 假
真是1表示
假是0表示
&&左边的表达式为假时,右边的表达式肯定不会执行
||左边的表达式为真时,右边的表达式肯定不会执行
赋值运算符
=    +=    *=    /=    -=
优先级别
算术 > 关系 > 逻辑 > 赋值
其他运算符知识
自增 自减 三目运算符 逗号表达式
自增/自减
分类
前自增 - - i + + i
后自增 i - - i + +
前自增和后自增的异同:
相同:
最终都使i的值加1
不同:
前自增:整体表达式的值是i加1之后的值
后自增:整体表达式的值是i加1之前的值
# 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;
}
/*
在VSCode中的输出结果是:
----------------------
i = 4, j = 4, k = 3, m = 4
----------------------
总结:
前自增整体表达式的值是i加1之后的值
后自增整体表达式的值是i加1之前的值
*/
为什么会出现自增
代码更精练自增的速度更快
学习自增要明白的几个问题
1.我们编程时应该尽量屏蔽掉前自增和后自增的差别
2.自增表达式最好不要作为一个更大的表达式的一部分来使用
或者说
i++和++要i单独成一个语句,不要把它作为一个完整复合语句的一部分来使用。
如:
int m = i++ + ++i + i + ++i //这样写不但是不规范的代码
//而且是不可移植的代码
printf("%d %d %d", i++, ++i,i); //同上
三目运算符
格式
A ? B : C
等价于
if (A)
B;
else
c;
逗号表达式
格式
(A, B, C, D)
功能:
从左到右执行
最终表达式的值是最后一项的值
流程控制
流程顺序的分类
1.选择
if语句
switch语句
2.循环
for语句
格式:for(1;2;3)
{语句A;}
while语句
while (表达式)
语句;
do…while语句
do
{
吧 语句…
}while(表达式);
先执行一次语句,然后再判断表达式是否成立;
do…while 并不等价与for,当然也不等价于while。
主要用于人机交互。
#include <stdio.h>
#include <math.h>
int main(void)
{
float a = 1, b = 5, c = 6;
float delta; //delta 存放的是b*b-4*a*c
float x1, x2; //两个解
char ch;
do
{
printf("请输入一元二次方程的三个系数:\n");
scanf("%f %f %f", &a, &b, &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 (delta==0)
{
x1 = (-b + sqrt(delta)) / 2 / a;
x2 = (-b - sqrt(delta)) / 2 / a;
printf("只有一个解 x1 = x2 = %f\n",x1);
}
else
{
printf("方程无解 \n");
}
// getchar();
// getchar();
printf("您想继续吗(Y/N):");
scanf(" %c", &ch); //%c前面必须加一个空格
} while ('y'==ch || 'Y'==ch);
return 0;
}
break和continue用法
break
1.break如果用于循环是用来终止循环
2.break如果用于switch,则是用于终止switch
3.break不能直接用于if,除非if属于循环内部的一个子句
4.在多层循环中,break只能终止最里面包裹它的那个循环
5.在多层switch嵌套中,break只能终止距离它最近的switch
continue
用于跳过本次循环余下的语句,转去判断是否需要执行下次循环
例子:1.
for (1;2; 3)
{
A;
B;
continue;//如果执行该语句,则执行完该语句后,会执行语句3,c和D都会被跳过去,C和D不会被执行
C;
D;
}
2.
while (表达式)
{
A;
B;
continue;//如果执行该语句,则执行完该语句后,
//会执行表达式,C和D都会被跳过去, C和D不会被执行
C;
D;
}
变量的作用域和存储方式:
按作用域分:
全局变量
在所有函数外部定义的变量叫全局变量
全局变量使用范围:从定义位置开始到整个程序结束
局部变量
在一个函数内部定义的变量或者函数的形参都统称为局部变量
void f (int i)
{
int j = 20;
}
i和j都属于局部变量
局部变量使用范围:只能在本函数内部使用
注意的问题:
全局变量和局部变量命名冲突的问题
在一个函数内部如果定义的局部变量的名字和全局变量名一样时,
局部变量会屏蔽掉全局变量
按变量的存储方式
静态变量
自动变量
寄存器变量
指针
指针的重要性
表示一些复杂的数据结构
快速的传递数据,减少了内存的耗用
使函数返回一个以上的值
能直接访问硬件
能够方便的处理字符串
是理解面向对象语言中引用的基础
总结:指针是C语言的灵魂
指针的定义
地址:
1.内存单元的编号
2.从零开始的非负整数
3.范围:4G(内存条) 【0 - 4G-1】
指针
指针就是地址,地址就是指针
指针变量就是存放内存单元编号的变量,
或者说指针变量就是存放地址的变量
指针和指针变量是两个不同的概念
但是要注意: 通常我们叙述时会把指针变量简称为指针,实际它们含义并不一样指针的本质就是一个操作受限的非负整数
指针的分类
1.基本类型指针 ☆
int * p;
/
/
i
n
t
∗
p
;
不
表
示
定
义
了
一
个
名
字
叫
做
∗
p
的
变
量
\color{#FF3030} { //int *p;不表示定义了一个名字叫做*p的变量}
//int∗p;不表示定义了一个名字叫做∗p的变量
/
/
i
n
t
∗
p
;
应
该
这
样
理
解
:
p
是
变
量
名
,
p
变
量
的
数
据
类
型
是
i
n
t
∗
类
型
,
i
n
t
表
示
该
数
据
占
4
个
字
节
\color{#FF3030} { //int *p;应该这样理解 : p 是变量名,p变量的数据类型是int * 类型,int 表示该数据占4个字节}
//int∗p;应该这样理解:p是变量名,p变量的数据类型是int∗类型,int表示该数据占4个字节
/
/
即
p
指
向
一
个
i
n
t
类
型
的
数
据
的
首
地
址
,
包
括
此
地
址
及
其
后
面
的
三
个
地
址
才
能
完
整
表
示
该
i
n
t
类
型
数
据
\color{#FF3030} { //即p指向一个int类型的数据的首地址,包括此地址及其后面的三个地址才能完整表示该int类型数据}
//即p指向一个int类型的数据的首地址,包括此地址及其后面的三个地址才能完整表示该int类型数据
/
/
所
谓
i
n
t
∗
;
类
型
\color{#FF3030} { // 所谓int * ; 类型 }
//所谓int∗;类型
实
际
就
是
存
放
i
n
t
变
量
地
址
的
类
型
\color{#FF3030} {实际就是存放int变量地址的类型 }
实际就是存放int变量地址的类型
int i = 3;
int j;
p = &i;
/*
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的内容为地址的变量
*/
j
=
∗
p
;
/
/
等
价
与
j
=
i
;
\color{red}{j = *p; //等价与 j=i;}
j=∗p;//等价与j=i;
附注:
‘*’ 的含义
1.乘法
2.定义指针变量
int * p;
// * 和指针名之间的空格可有可无。通常程序员在声明时使用空格,在解引用变量时省略空格。
//定义了一个名字叫p的变量, int *表示p只能存放int变量的地址
3.指针运算符
该运算符放在已经定义好的指针变量的前面
如果p是一个已经定义好的指针变量
则 * p表示 以p的内容为地址的变量
如何通过被调函数修改主调函数一个或多个普通变量的值
1.实参必须为该普通变量的地址
2.形参必须为指针变量
3.在被调函数中通过
*形参名=…
的方式就可以修改主调函数相关变量的值
典型错误
> #include <stdio.h>
> int main(void)
> {
> int i = 5;
> int * p ;
> int * q;
>
> p = &i ;
> // *q = p ; //p的类型是 int * 而*q的类型是int
> //*q = *p; //error
> p = q; //q是垃圾值,q赋给q,p也变成垃圾值
> print f("%d \n ",*q); //13行
> /*
> q的空间是属于本程序的,所以本程序可以读写的q的内容,
> 但是如果q内部是垃圾值,则本程序不能读写*q的内容
> 因为*q所代表的内存单元的控制权限并没有分配给本程序
> 所以本程序运行到13行就会立即出错
> */
> return 0;
}
> void swap(int * p, int * q)//形参名字是p和q,接收实参数据的是p和q,而不是*p和*q。
> {
>
>}
2.指针和数组
指针和一维数组
1.一维数组名
一维数组名是个指针常量
它存放的是一维数组第一个元素的地址
2.下标和指针的关系
如果 p 是个指针变量,则
p[ i ] 永远等价于 * (p+i)
确定一个一维数组需要几个参数【如果一个函数要处理一个一维数组,则需要接收该数组的哪些信息】
需要两个参数:
数组第一个元素的地址
数组的长度
# 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] 也等价于 *(b+i)
printf("\n");
}
int main(void)
{
int b[6] = {-1,-2,-3,4,5,-6};
//b=&b[2]; error 因为b是常量
f(b, 6);
b[i]
return 0;
}
3.指针变量的运算
指针变量不能相加 不能相乘 也不能相除
如果两个指针变量指向的是同一块连续空间中的不同存储单元,
则这两个指针变量才可以相减
4.一个指针变量到底占几个字节【非重点】
预备知识:
1.sizeof(数据类型)
功能:返回值就是该数据类型所占的字节数例子:
sizeof(int)=4
sizeof(char)=1
sizeof(double)=8
2.sizeof(变量名)
功能:返回值是该变量所占的字节数
假设p指向char类型变量(1个字节)
假设q指向int类型变量(4个字节)
假设r指向double类型变量(8个字节)
请问:p q r 本身所占的字节数是否一样 ?
答案: p q r本身所占的字节数是一样的
总结:
一个指针变量,无论它指向的变量占几个字节该指针变量本身只占四个字节
一个变量的地址是用该变量首字节的地址来表示
指针和二维数组
3.指针和函数
4.指针和结构体☆
5.多级指针
# include <stdio.h>
int main(void)
{
int i = 10;
int * p = &i; //p只能存放int类型变量的地址
int ** q = &p; //q是int **类型, 所谓int **类型就是指q只能存放int *类型变量的地址,
int *** r = &q; //r是int ***类型, 所谓int ***类型就是指r只能存放int ** 类型变量的地址,
//r = &p; //error 因为r是int *** 类型,r只能存放int **类型变量的地址
printf("i = %d\n", ***r); //输出结果是10 只有 ***r才表示的是i, *r或 **r或 ****r代表的都不是i
return 0;
}
void f(int ** q)
{
//*q就是p
}
void g()
{
int i = 10;
int * p = &i;//类型
f(&p); //p是int *类型 , &p是int ** 类型
}
int main(void)
{
g();
return 0;
}
动态内存分配
传统数组的缺点:
1.数组长度必须事先制定,且只能是常整数,不能是变量
例子:
int a[5]; //ok
int len = 5; int a[len]; //error
2.传统形式定义的数组,该数组的内存程序员无法手动释放
在一个函数运行期间,系统为该函数中数组所分配的空间会一直存在,直到该函数运行完毕时,数组的空间才会被系统释放
3.数组的长度一旦定义,其长度就不能再更改
数组的长度不能在函数运行的过程中动态的扩充或缩小
4.A函数定义的数组,在A函数运行期间可以被其它函数使用,但A函数运行完毕之后,A函数中的数组将无法在被其他函数使用。
传统方式定义的数组不能跨函数使用
5.为什么需要动态分配内存
动态数组很好的解决了传统数组的这4个缺陷
传统数组也叫静态数组
动态内存的分配
动态构造一个int类型 的一维数组
int * p = (int *)malloc(int len);
1.本语句分配了两块内存
一块是:动态内存分配 总共 len 个字节
另一块是:静态内存分配: 这块静态内存是 p 变量本身所占的内存,总共 4 个字节
2.malloc 是 memory (内存) allocate (分配) 的缩写,要使用malloc函数,必须添加 <malloc.h> 这个头文件
3.malloc只有一个 int 类型的形参,表示要系统分配的字节数
4.其功能是:请求系统 len 个字节的内存空间,如果请求分配成功,则返回第一个字节的地址,如果分配不成功,则返回NULL。
5.malloc 只能返回第一个字节的地址,无法说明该地址数据的类型,因此 malloc 之前必须加 (数据类型 * ),表示这个地址表示的数据类型。
如:int * p = (int * ) malloc( sizeof(int) * 50); //sizeof(int) *50 保持分配的字节数是 int 类型的整数倍
double * p = (double * ) malloc( sizeof(double ) * 50);
6.释放动态内存
free ( p );
表示把p所指向的内存给释放掉p本身的内存是静态的,不能由程序员手动释放,p本身的内存只能在p变量所在的函数运行终止时由系统自动释放
7.静态内存和动态内存的比较【重点】
静态内存是由系统自动分配,由系统自动释放静态内存是在栈分配的
动态内存是由程序员手动分配,手动释放动态内存是在堆分配的
8.跨函数使用内存的问题【重点】
静态内存不可以跨函数使用
所谓静态内存不可以跨函数使用更准确的说法是:
静态内存在函数执行期间可以被其它函数使用,
静态内存在函数执行完毕之后就不能再被其他函数使用了
# include <stdio.h>
void f(int ** q) //q是个指针变量,无论q是什么类型的指针变量,都只占4个字节
{
int i = 5;
//*q等价于p q和**q都不等价于p
//*q = i; //error 因为*q = i; 等价于 p = i; 这样写是错误的
*q = &i; // q = &p; q的内容是p的地址 --->即*q==p;--->p = &i;
}
int main(void)
{
int *p; //13行
f(&p);
printf("%d\n", *p); //16行 本语句语法没有问题,但逻辑上有问题,非法访问i变量,
// 此时f函数已经执行完毕,i已经被释放
return 0;
}
结构体
1.为什么需要结构体
为了表示一些复杂的事物,而普通的基本类型无法满足实际要求
2.什么叫结构体
把一些基本类型数据组合在一起形成的一个新的复合数据类型,这个叫做结构体
3.如何定义结构体
3种方式,推荐使用第一种:
//第一种方式 这只是定义了一个新的数据类型,并没有定义变量
struct Student
{
int age;
float score;
char sex;
};
//第二种方式
struct Student2
{
int age;
float score;
char sex;
} st2;
//第三种方式
struct
{
int age;
float score;
char sex;
} st3;
4.怎样使用结构体变量
赋值和初始化
定义的同时可以整体赋初值
如果定义完之后,则只能单个的赋初值
如何取出结构体变量中的每一个成员【重点】
1.结构体变量名.成员名
2.指针变量名 -> 成员名 (第二种方式更常用)
指针变量名 -> 成员名在计算机内部会被转化成 (*指针变量名).成员名的方式来执行,所以说这两种方式是等价的
例子
# include <stdio.h>
//第一种方式
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 这个成员
结构体变量和结构体指针变量作为函数参数传递的问题
推荐使用结构体指针变量作为函数参数来传递
结构体变量的运算
结构体变量不能相加,不能想减,也不能相互乘除
但结构体变量可以相互赋值
例子:
struct Student{
int age;char sex;
char name[100];}; //分号不能省
struct Student st1, st2;
st1+st2 st1*st2 st1/st2 都是错误的
st1 = st2 或者 st2 = st1 都是正确的
/*
通过函数完成对结构体变量的输入和输出
*/
# include <stdio.h>
# include <string.h>
struct Student
{
int age;
char sex;
char name[100];
}; //分号不能省
void InputStudent(struct Student *);
void OutputStudent(struct Student ss);
int main(void)
{
struct Student st; //15行
InputStudent(&st); //对结构体变量输入 必须发送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';
}
/*
//本函数无法修改主函数15行st的值 所以本函数是错误的
void InputStudent(struct Student stu)
{
stu.age = 10;
strcpy(stu.name, "张三"); //不能写成 stu.name = "张三";
stu.sex = 'F';
}
*/
/*
示例:
发送地址还是发送内容
目的:
指针的优点之一:
快速的传递数据,
耗用内存小
执行速度快
*/
# include <stdio.h>
# include <string.h>
struct Student
{
int age;
char sex;
char name[100];
}; //分号不能省
void InputStudent(struct Student *);
void OutputStudent(struct Student *);
int main(void)
{
struct Student st ; //15行
//printf("%d\n", sizeof(st));
InputStudent(&st); //对结构体变量输入 必须发送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';
}
枚举
1.什么是枚举
把一个事物所有可能的取值一一列举出来
2.怎样使用枚举
3.枚举的优缺点
代码更安全
书写麻烦
链表
算法:
通俗定义:
解题的方法和步骤
狭义定义:
对存储数据的操作
对不同的存储结构,要完成某一个功能所执行的操作是不一样的
比如:
要输出数组中所有的元素的操作和要输出链表中所有元素的操作肯定是不一样的
这说明:
算法是依附于存储结构的
不同的存储结构,所执行的算法是不一样的
广义定义:
广义的算法也叫泛型
无论数据是如何存储的,对该数据的操作都是一样的
我们至少可以通过两种结构来存储数据
数组
优点:
存取速度快
缺点:
需要一个连续的很大的内存
插入和删除元素的效率很低
链表
专业术语:
首节点
存放第一个有效数据的节点
尾节点
存放最后一个有效数据的节点
头结点
头结点的数据类型和首节点的类型是一模一样的
头结点是首节点前面的那个节点
头结点并不存放有效数据
设置头结点的目的是为了方便对链表的操作
头指针
存放头结点地址的指针变量
确定一个链表需要一个参数
头指针
优点:
插入删除元素效率高
不需要一个连续的很大的内存
缺点:
查找某个位置的元素效率低