C语言基础学习(Day12)
笔者有话说:
从Day1到Day12跟着笔者每天打卡到的朋友们,恭喜你们超前完成了C语言学习任务!🎉大家没有看错!虽然原计划是在15天内带领大家学完C语言,但是,我们在12天内,在不经意间,学完了C语言。为避免部分朋友认为来回翻阅较为麻烦,笔者在Day12为大家精心准备了这段时期以来所有的C语言笔记,本篇内容为完结篇,语言通俗易懂,小白也能学会!祝愿大家认真阅读Day12的内容,更好地掌握C语言,打卡第十二天!
文章目录
- C语言基础学习(Day12)
- 一、C语言学习预备知识
- 1、C语言的优缺点
- 2、数据类型
- 3、变量
- 4、进制(计算机只能识别二进制)
- 5、常量在C语言中是如何表示的
- 6、常量以什么样的二进制代码存储在计算机中
- 7、代码规范化
- 8、字节
- 9、ASCII码
- 二、C语言基础知识
- 1、基本输入与输出函数的用法
- 2、运算符
- 3、运算符补充
- 4、标识符(有名有姓的C)
- 5、枚举
- 6、冒泡排序
- 7、进制转换
- 8、 * 的含义
- 9、补码
- 10、算法
- 11、位运算符
- 12、NULL
- 13、数组
- 三、C语言重点难点
- 1、流程控制
- 2、选择——选择执行
- 3、循环
- 4、函数(C语言的第二个重点)
- 四、C语言的灵魂——指针
- 1、指针基础知识
- 2、指针的分类
- 3、专题:动态内存分配(重点难点)
一、C语言学习预备知识
1、C语言的优缺点
(1)优点:代码量小、速度快、功能强大
(2)缺点:危险性高、开发周期长、可移植性不强
2、数据类型
(1)基本数据类型
整型:int(4个字节) , short int(2个字节) , long int(8个字节)——%d输出
实数:float(4个字节) , double(8个字节)——%f输出
单个字符:char(占1个字节)——%c输出
(2)复合数据类型(由基本类型构成,后续深入学习)
结构体
枚举
共用体
3、变量
(1)变量的本质:内存中的一段存储空间
(2)变量的作用:利用变量可以方便我们对于数据的存储
#include <stdio.h>
int main(void)
{
int i; //定义变量i
i = 3; //3是存放在内存中,程序终止之后3所占用的空间被释放
printf("i = %d", i);
return 0;
}
说明:
int i;——在内存条中找到一个空闲的、没有被其他单元占用的空间,分配给变量i使用,即把该单元地址与变量i产生一种关联。
i = 3;——将3存储到变量i所对应的存储单元中
(3)变量为什么必须初始化?——所谓初始化就是赋值的意思
①必须初始化,只是编译器有时代替人工初始化而已。
②当全局变量没有初始化时,系统会自动初始化。(正常情况下)
③局部变量没有初始化时,系统会报错。
(4)软件运行与内存关系(垃圾数据)——内存是在操作系统的统一管理下使用的!
①软件在运行前需要向操作系统申请存储空间,在内存空间足够时 ,操作系统将分配一段内存空间,并将外存中的软件拷贝一份存入该内存中,并启动该软件的运行;
②在软件运行期间,该软件所占用的内存空间不再分配给其他软件;
③当软件运行完毕后,操作系统将回收该内存空间(注意:操作系统并不清空该内存空间遗留下来的数据),以便再次分配给其他软件使用。
总结:综上所述,一个软件所分配到的空间中极可能存在着以前其他软件使用后的残留数据,这些数据称为垃圾数据。所以通常情况下,我们为一个变量、一个数组分配好存储空间之后,都要对该内存空间初始化。
(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;
4、进制(计算机只能识别二进制)
(1)举例:
十进制:逢十进一
二进制:逢二进一
(2)进制转化
Tip1:C语言规定八进制前要加0(注意是数字0,不是字母O),十六进制前要加0x或0X,十进制前不用加。
例如:
int 3;——表示十进制
int 05;——表示八进制
int 0x8;——表示十六进制
Tip2:汇编语言中,数字后加字母B表示二进制数,加字母O表示八进制数,加字母D表示十进制数,加字母H表示十六进制数。
八进制:1~7
十六进制:1~9 A~F
Tip3:C语言代码实现
#include <stdio.h>
int mian(void)
{
//int i = 88;
int i = 0x32C; //注意:前面是数字0不是字母O
printf("i = %d\n", i);
printf("i = %o\n", i);
printf("i = %x\n", i);
printf("i = %X\n", i); //多行注释用:/*......*/
return 0;
}
(3)注意:
①区分单行注释与多行注释使用方法
②printf的用法:
%d表示以十进制输出
%o表示以八进制输出(o是小写字母)
%x或%X表示以十六进制输出
5、常量在C语言中是如何表示的
(1)整数:
十进制:传统写法
八进制:前面加0(数字0)
十六进制:前面加0x或0X(数字0)
(2)浮点数:
传统的写法
float = 3.2;
科学计数法
float x = 3.2e3; //x的值为3200
float x = 123.45e-2; //x的值为1.2345
e3表示10^3, e-2表示10^(-2)
对于代码:“float x = 123.45e-2;”, 若在程序运行中提示警告,表示数字123.45e-2默认为double类型(实数在C语言中默认为double类型)。
解决方法:①忽视警告,继续运行;②在数字后添加字母"F",即写为:“float x = 123.45e-2F;”,将其当作float处理。
(3)字符:
单个字符用单引号括起来
‘A’表示字符A,"A"也正确,"A"代表了’A’与’0’的组合
字符串用双引号括起来
"AB"正确,'AB’错误
6、常量以什么样的二进制代码存储在计算机中
(1)整数以补码的形式转化为二进制代码存储在计算机中;
(2)实数以IEEE754标准转化为二进制代码存储在计算机中;
(3)字符的本质实际也是与整数的存储方式相同。
7、代码规范化
(1)整齐,便于自己与他人检查观看
(2)不容易出错
int main(void)
{
//定义变量
//对变量进行操作
//输出值
}
规范化参考书籍:《高质量C/C++》——林瑞
8、字节
字节是存储数据的单位,并且是硬件所能访问的最小单位,CPU只能控制到字节,通过位运算符才能控制到位。
1个字节 = 8位
1K = 1024字节
1M = 1024K字节
1G = 1024M字节
1T = 1024G字节
注意:C语言中char占1个字节,在Java中字符占两个字节。
9、ASCII码
ASCII不是一个值,而是一种规定,ASCII码规定了不同的字符使用哪个整数值表示。
规定:
‘A’——65
‘B’——66
‘C’——67
‘a’——97
‘b’——98
‘0’——48
代码实现:
#include <stdio.h>
int main(void)
{
char ch = 'A';
ch = 'C';
printf("ch = %c", ch);
return 0;
}
输出结果:ch = 67
(1)单独定义
char ch = ‘A’; //√
char ch = “AB”; //× error 因为"AB"是字符串,不能把字符串赋给单个字符
char ch = “A” ; //× "A"代表’A’与’\0’两个字符的组合,不能把字符串赋给单个字符。
char ch = ‘AB’; //× 单引号只能括单个字符
(2)同时定义
char ch = ‘A’;
char ch = ‘B’;
× error,因为ch变量已经定义过,这样会导致变量名被重新定义
char ch = ‘A’; //等价于char ch; ch = ‘A’;
ch = ‘C’;
√ ch为变量,第一行定义单个字符变量ch,并将其赋值为’A’,可以对变量反复赋值
代码实现:
#include <stdio.h>
int main(void)
{
char ch = 'A';
ch = 'C';
printf("ch = %c", ch);
return 0;
}
输出结果:ch = C
(3)字符的存储
字符的存储本质上与整数的存储方式相同
二、C语言基础知识
1、基本输入与输出函数的用法
(1)输出——printf()——将变量的内容输出到显示器上
四种用法:
①printf(“字符串\n”);
#include<stdio.h>
int main(void)
{
printf("Hello!\n"); //"\n"——转义字符,表示换行
return 0;
}
②printf(“输出控制符”, 输出参数);
#include<stdio.h>
int main(void)
{
int i = 10;
printf("%d\n",i); //%d是输出控制符,i为输出参数
return 0;
}
Tip:d代表以十进制输出
③printf(“输出控制符1 输出控制符2 …”, 输出参数1, 输出参数2, …);
#include<stdio.h>
int main(void)
{
int i = 2;
int j = 4;
printf("%d %d\n", i, j); //%d是输出控制符,i为输出参数
return 0;
}
Tip:输出控制符与输出参数的个数必须一一对应
④printf(“输出控制符 非输出控制符”, 输出参数);
输出控制符包含如下:%d %ld %c %f %lf %x 或 %X 或 %#X %o
另外:%x在调试中的使用较为重要,因此需要掌握printf中%x及其相关的用法
int x = 47; //100前没有加0、0x或0X,表示100为十进制
#include<stdio.h>
int main(void)
{
int x = 47;
printf("%x\n", x); //输出结果为2f
printf("%X\n", x); //输出结果为2F
printf("%#x\n", x); //输出结果为0x2f
printf("%#X\n", x); //输出结果为0X2F
return 0;
}
Tip:%#X推荐使用!——可以一目了然看出输出结果为十六进制
输出控制符把参数以其规定的格式输出,而非输出控制符则以原样输出
例如:
#include<stdio.h>
int main(void)
{
int i = 4;
int j = 9;
printf("i = %d, j = %d/n", i, j);
return 0;
}
输出结果:i = 4, j = 9
说明:为什么需要输出控制符?
①0、1组成的代码可以表示数据也可以表示指令
②如果0、1组成的代码表示的是数据,同样的0、1代码组合以不同的输出格式输出,会有不同的输出结果
(2)输入——scanf()——通过键盘将数据输入到变量中
①scanf(“输入控制符”, 输入参数);
功能:将从键盘输入的字符转化为输入控制符所规定格式的数据,存入以输入参数的值为地址的变量中。
举例:
#include <stdio.h>
int main(void)
{
int i;
scanf("%d", &i);
printf("i = %d\n", i); //&i表示i的地址,&表示取地址符
return 0;
}
②scanf(“非输入控制符 输入控制符”, 输入参数);
Tip:非输入控制符必须原样输出
举例:
#include <stdio.h>
int main(void)
{
int i;
scanf("mm%d", &i); //mm为非输入控制符 %d为输入控制符
printf("i = %d\n", i);
return 0;
}
mm123为正确输入,而123为非法的输入
③scanf(”输入控制符1 输入控制符2 …", 输入参数1, 输入参数2, … );
一次给多个变量键盘赋值
```c
#include <stdio.h>
int main(void)
{
int i, j, k;
scanf("%d, %d, %d", &i, &j, &k);
printf("i = %d, j = %d, k = %d\n", i, j, k);
return 0;
}
Tip:","为非输入控制符,要原样输入,才能保证是合法输入
④如何使用scanf编写出高质量代码?
使用scanf前最好先使用printf提示用户以什么样的方式来输入;
scanf中尽量不要使用非控制符,尤其不要使用\n。
⑤编写代码对scanf中用户的非法输入做适当处理
while ( (ch = getchar()) != ‘\n’ )
continue;
——此处用到循环语句,后期深入学习
#include <stdio.h>
int main(void)
{
int i,j;
char ch;
scanf("%d", &i);
printf("i = %d\n", i);
//......
while ( (ch = getchar()) != '\n')
continue;
scanf("%d", &j);
printf("j = %d\n", j);
return 0;
}
2、运算符
(1)算术运算符:+ - * /(除) %(取余)
(2)关系运算符:> >= < <= !=(不等于) ==(等于)
(3)逻辑运算符:!(非) &&(并且) ||(或者)
(4)赋值运算符:= += -= *= /=
(5)优先级别:算术 > 关系 > 逻辑 > 赋值
注意:
①除法运算中,运算结果与运算对象的数据类型有关,两个数都为整数,则商为int,若商中有小数,则截取整数部分;被除数和除数中只要有一个或者两个为浮点型数据,则商也是浮点型数据,不截取小数部分。
②取余的运算对象必须是整数,结果是整除后的余数,其余数的符号与被除数相同,例如:13%-3=1, -13%3=-1, -13%3=-1。
③C语言对真假的处理:非零是真,零是假,真用1表示,假用0表示。
#include<stdio.h>
int main(void)
{
int i = 1;
int j = 2;
int k = 3;
int m;
//m = (3>2) && (k = 8); //=表示赋值 ——m为1, k为8
//m = (3>2) && (k = 0); //m为0, k为0
m = (2>3) && (k = 20); //m为0, k为3
//不含有";"的是表达式,含有";"的是语句
printf("m = %d, k = %d", m, k);
}
若&&左边为假,右边的表达式肯定不会执行;
若||左边为真,右边的表达式肯定不会执行。
3、运算符补充
附录的一些琐碎运算符知识
自增 自减 三目运算符 逗号表达式
(1)自增(或者自减)
①分类:
前自增 :++i --i
后自增:i++ i–
②前自增与后自增的异同:
相同点:最终都使i的值加1
不同点:前自增整体表达式的值是i加1之后的值;后自增整体表达式的值是i加1之前的值。
③为什么出现自增?
代码更精炼;自增的速度更快。
④学习自增要明白的几个问题
编程应尽量屏蔽前自增和后自增的区别;
自增最好不要作为一个更大的表达式来使用(或者说:++i 和 i++ 单独成一个语句,不要把它作为一个完整符合语句的一部分来使用)。
例如:
int m = i++ + ++i + i + i++ //这样写不但是错误的,而且不可移植
(2)三目运算符
格式:
A ? B : C
——问号与冒号合起来叫做三目运算符,用的不多
等价于:
if (A)
B;
else
C;
示例:
#include <stdio.h>
int main(void)
{
int i;
i = (3>2 ? 5 : 1);
printf("%d\n", i);
return 0;
}
输出结果:5
(3)逗号表达式
格式:
(A, B, C, D);
功能:
从左到右执行,最终表达式的值是最后一项的值
示例:
#include <stdio.h>
int main(void)
{
int i;
i = (3, 4, 2, 8);
printf("%d\n", i);
return 0;
}
输出结果:8
4、标识符(有名有姓的C)
(1)定义
C语言规定,标识符可以是字母A-Z、a-z、数字0-9、下划线**_**组成的字符串。第一个字符必须是字母或下划线。
(2)注意点
①标识符长度最好不要超过8位,在某些版本的C中规定标识符前8位有效,若两个标识符前8位相同,会被认为是同一个标识符;
②标识符严格区分大小写。Student与student是两个不同的标识符;
③标识符最好选有意义的英文单词,做到“见名知意”,不要使用中文;
④标识符不能是C语言的关键字。
(3)不可改变的常量
在程序执行过程中,值不发生改变的量称为常量,C语言的常量可以分为直接常量和符号常量。直接常量也称为字面常量,可以直接使用,无需说明;符号常量是可以使用一个标识符来表示一个常量。符号常量在使用前必须先定义,其一般形式为:
#define 标识符 常量值
#include <stdio.h>
#define POCKETMONEY 10 //定义常量及常量值
int main(void)
{
//POCKETMONEY = 12;——符号常量不可以被更改,会报错
printf("小明今天又得到了%d零花钱\n", POCKETMONEY);
return 0;
}
Tip:符号常量不可以被更改,会报错!
5、枚举
(1)什么是枚举
把一个事物所有可能的取值一一列举出来
(2)怎样使用枚举
以代码说明:
#include <stdio.h>
enum WeekDay
{
MonDay, TuesDay, WednesDay, ThursDay, FriDay, SaturDay, SunDay
};
int main(void)
{
enum WeekDay day = WednesDay;
printf("%d", day);
return 0;
}
(3)枚举的优缺点
优点:使代码更安全;
缺点:书写麻烦
6、冒泡排序
#include <stdio.h>
void sort(int * a, int len)
{
int i, j, t;
for (i=0; i<len-1; ++i)
{
for (j=0; j<len-1-i; ++j)
{
if (a[j] > a[j+1]) //">"表示升序,"<"表示降序
{
t = a[j];
a[j] = a[j+1];
a[j+1] = t;
}
}
}
}
int main(void)
{
int a[6] = {1, 4, 8, -5, 10, 11};
int i = 0;
sort(a, 6);
for (i=0; i<6; ++i)
{
printf("%d\t", a[i]);
}
printf("\n");
return 0;
}
7、进制转换
在汇编中:在数字后加字母B表示二进制数,加字母O表示八进制数,加字母D表示十进制数,加字母H表示十六进制数。
在C语言中:规定八进制前要加0(注意是数字0,不是字母O),十六进制前要加0x或0X,十进制前不用加。
十进制转r进制方法:除r取余,直至商0,余数倒序排列。
8、 * 的含义
(1)乘法运算
(2)定义指针变量
int * p; //表示定义了一个名字叫做p的变量,int *表示p只能存放一个普通变量的地址
(3)指针运算符
该运算符放在已经定义好的指针变量前,如果p是一个已经定义好的指针变量,则*p表示以p的内容位地址的变量。
9、补码
学习目标:
在**Dev-C++**中
一个int类型的变量所能存储的数字的范围是多少?
int类型变量所能存储的最大正数用十六进制表示是:7FFFFFFF
int类型变量所能存储的绝对值最大的负整数用十六进制表示是:80000000
最小负数的二进制代码是多少?
最大正数的二进制代码是多少?
已知一个整数的二进制代码怎样求出原始的数字?
数字超过最大正数会怎样?
(1)原码
原码也叫:符号-绝对值码
最高位0表示正,1表示负,其余二进制位是该数字的绝对值的二进制位
原码简单易懂
加减运算复杂
在加减乘除四种运算中,增加了CPU的复杂度
零的表示不唯一
(2)反码
符号位不变,数值位取反
反码运算不便
没有在计算机中应用
(3)移码
表示数值平移n位,n称为移码量
移码主要用于浮点数的阶码的存储
(4)补码
①十进制转二进制:
正整数转二进制:除2取余,直至商为零,余数倒叙排序;
负整数转二进制:先求与该负数相对应的正整数的二进制代码,然后将所有位取反,末尾加1,不够位数时,左边补1。
示例:
#include <stdio.h>
int main(void)
{
int i = -100;
/*
分析——————————————————————————————————————————
十进制:-100 --> 十六进制:64
十六进制:64--> 二进制:0110 0100
对二进制数 0110 0100 取反,得到:1001 1011
对二进制数 1001 1011 加一,得到;1001 1100
int型在Dev-C++中占4个字节,即4*8=32位,所以需要在 1001 1100 左边补24个1(负数左边补1,整数左边补0)
再将其转换为十六进制数,得到:FFFFFF9C
*/
printf("%#X", i); //以16进制的形式输出
return 0;
}
零的二进制:全是零
②二进制转十进制:
如果首位是0,则表明是正整数,按普通方法来求;
如果首位是1,则表明是负整数,应将所有位取反,末位加1,所得数字就是该负数的绝对值 。
示例:
#include <stdio.h>
int main(void)
{
int i = 0xFFFFFFCA;
/*
分析:
十六进制:CA-->二进制:1100 1010
二进制:1100 1010-->取反:0011 0101
二进制:0011 0101-->加1:0011 0110
二进制:0011 0110即原负数的绝对值
二进制:0011 0110-->十六进制:3 6
十六进制:3 6-->十进制:54
所以原负数为:-54
*/
printf("%d", i);
return 0;
}
如果全是零,则对应的十进制数就是零。
③拓展:在原码的基础上,符号位不变,数值位取反——>得到反码;反码加1——>得到补码。
10、算法
(1)通俗定义:解题的方法和步骤;
(2)狭义定义:
对存储数据的操作;
对不同的存储结构,要完成某一个功能所执行的操作是不一样的。
比如:
要输出数组中所有的元素和要输出链表中所有的元素的操作肯定是不一样的。
这说明:
算法是依附存储结构的;
不同的存储结构,所执行的算法是不一样的。
(3)广义定义:广义的算法也叫做泛型(无论数据是如何存储的,对数据的操作都一样)。
我们至少可以通过以下两种结构来存储数据:
①数组
优点:存取速度快
缺点:插入和删除元素的效率很低
②链表
优点:插入删除元素效率高;不需要一个连续的很大的内存
缺点:查找某个位置的元素效率低
11、位运算符
(1)&——按位于
1&0=0 1&1=1 0&1=0 0&0=0
&&:逻辑与 也叫 并且
&&与&的含义完全不同
例如:
5&7=5
5&&7=1
-5&10=10
-5&&10=1
(2)|——按位或
1|0=1 1|1=1 0|1=1 0|0=0
|| :逻辑或
(3)~——按位取反
~i就是把i变量所有的二进制位取反
(4)^——按位异或
1^0=1 1^1=0 0^1=1 0^0=0
相同为零;
不同为1。
(5)按位左移与按位右移
<<——按位左移
i<<1 表示把i的所有二进制位左移1位
i<<3 表示把i的所有二进制位左移3位
左移n位相当于乘以2的n次方(前提是数据不能丢失)
面试题:请问下列两个语句,哪个语句执行的速度快?
A) i = i * 8;
B) i = i << 8;
答案:B快
>>——按位右移,左边一般是补0,当然也可能补0
i>>1 表示把i的所有二进制位右移1位
i>>3 表示把i的所有二进制位右移3位
右移n位相当于除以2的n次方(前提是数据不能丢失)
总结:
位运算符的现实意义:通过位运算符我们可以对数据的操作精确到每一位。
12、NULL
二进制全部为零的含义——0000000000000000000000000000000的含义:
(1)数值0
(2)字符串结束标记符’\0’
(3)空指针NULL
NULL本质也是零,而这个零不代表数字零,而表示的是内存单元的编零;
我们计算机规定,以零位编号的存储单元的内容不可读、不可写。
13、数组
(1)示例:
#include <stdio.h>
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
//a是数组的名字,5表示数组元素的个数
//并且这5个元素分别用a[0] a[1] a[2] a[3] a[4]表示
int i;
for (i=0; i<5; ++i)
printf("%d\n", a[i]);
printf("%d\n", a[0]); //a[0]为1
return 0;
}
(2)为什么需要数组
①为了解决大量同类型数据的存储和使用问题;
②为了模拟现实世界。
(3)数组的分类
①一维数组
怎样定义一个一维数组:
为n个变量连续分配存储空间;
所有的变量数据类型必须相同;
所有的变量所占的字节大小必须相等。
示例:
int a[5];
有关一维数组的操作:
初始化:
完全初始化:int a[5] = {1, 2, 3, 4, 5};
不完全初始化:未被初始化的元素自动为0
int a[5] = {1, 2, 3}; //a[4]与a[5]自动为0
不初始化:int a[5]; //所有元素是垃圾值
清零:int a[5] = {0}; //所有元素为0
错误写法1:
int a[5];
a[5] = {1, 2, 3, 4, 5}; //错误,只有a[0] a[1] a[2] a[3] a[4]这5个元素,没有a[5]元素
只有在定义数组的同时才可以整体赋值,其他情况下整体赋值都是错误的。
错误写法2:
int a[5] = {1, 2, 3, 4, 5};
a[5] = 100; //错误,没有a[5]这个元素,最后一个元素是a[4]
错误写法3:
int a[5] = {1, 2, 3, 4, 5};
int b[5];
如果要把a数组中的值全部复制给b数组,错误的写法:
b = a; //错误,a和b代表的是数组名,数组名代表的是数组第一个元素a[0] b[0]的地址
正确的写法:
for (i=0; i<5; ++i)
b[i] = a[i];
#include <stdio.h>
int main(void)
{
int a[5];
scanf("%d", &a[0]);
printf("a[0] = %d\n", a[0]);
scanf("%d", &a[3]);
printf("a[3] = %d\n", a[3]);
return 0;
}
一维数组的元素互换:
#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等价
--j;
}
for (i=0; i<7; ++i)
{
printf("%d\n",a[i]);
}
return 0;
}
②二维数组
例如:
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+1行第i+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("a[%d][%d] = %d\t", i, j, a[i][j]);
}
printf("\n");
}
return 0;
}
③多维数组
是否存在多维数组?——不存在多维数组
因为内存是线性唯一的
n维数组可以当作每个元素是n-1维数组的一维数组,比如:
int[3][4];
//该数组是含有3个元素的一维数组,只不过每个元素都可以再分成4个小元素。
int a[3][4][5];
//该数组是含有三个元素的一维数组,只不过每个元素都是4行5列的二维数组
三、C语言重点难点
1、流程控制
(1)什么是流程控制
程序代码执行的顺序
(2)流程控制的分类
①顺序执行(按顺序从上往下执行)
②选择执行(选择性执行某部分代码)
③循环执行(反复执行某部分代码)
2、选择——选择执行
某些代码可能执行,也可能不执行,有选择地执行某些代码。
(1)if
①简单方法
格式:
if (表达式)
语句
#include <stdio.h>
int main(void)
{
if (3)
printf("AAAA\n"); //会输出
if (0)
printf("BBBB\n"); //不会输出
if (0 == 0)
printf("CCCC\n"); //表达式为真,会输出
return 0;
}
输出结果为:
AAAA
CCCC
功能:
如果表达式为真,执行语句;
如果表达式为假,不执行语句。
②if的范围问题
格式一:
if(表达式)
语句A;
语句B;
#include <stdio.h>
int main(void)
{
if(1>2)
printf("AAAA\n");
printf("BBBB\n");
return 0;
}
输出结果为:BBBB
说明一:if默认只能控制语句A的执行或不执行,无法控制语句B的执行或不执行(或者说:语句B一定会执行)。
格式二:
if(表达式)
{
语句A;
语句B;
}
#include <stdio.h>
int main(void)
{
if(1>2)
{
printf("AAAA\n");
printf("BBBB\n");
}
printf("CCCC\n");
return 0;
}
输出结果为:CCCC
说明二:此时if可以控制语句A和语句B
综上所述:if默认只能控制一个语句的执行或不执行,如果想控制多个语句的执行或不执行就必须把这些语句用{}括起来。
③if…else…与if…else if…else
格式:
if (1)
A;
else if (2)
B;
else if (3)
C;
else
D;
说明:如果1成立,则执行A;如果1不成立2成立,则执行2;如果1、2不成立3成立,则执行C;如果1、2、3都不成立,则执行D。
#include <stdio.h>
int main(void)
{
double delta = 3;
if (delta > 0)
printf("有两个解\n");
else if (delta == 0)
printf("有唯一解\n");
else
printf("无解\n");
return 0;
}
输出结果:有两个解
④C语言对真假的处理
非零是真,零是假;
真用1表示,假用0表示。
⑤实例
源代码:
#include <stdio.h>
int main(void)
{
float score;
scanf("%f", &score);
if (score > 100)
printf("你在做梦!\n");
else if (score >= 90 && score <= 100) //不能写成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 = 5;
//以下两行无法完成i、j的互换
i = j; //j = 5 => i = 5
j = i; //i = 5 => j = 5
printf("i = %d, j = %d\n", i, j);
return 0;
}
输出结果:i = 5, j = 5
正确方法:
#include <stdio.h>
int main(void)
{
int i = 3;
int j = 5;
//以下方法正确实现i、j的互换
int k;
k = i;
i = j;
j = k;
printf("i = %d, j = %d\n", i, j);
return 0;
}
输出结果:i = 5, j = 3
⑦对任意三个数字进行排序
#include <stdio.h>
int main(void)
{
int a, b, c;
int t;
printf("请输入三个整数(中间以空格分隔):\n");
scanf("%d %d %d", &a, &b, &c);
//编写代码进行排序
if (a < b)
{
t = a;
a = b;
b = t;
}
if (a < c)
{
t = a;
a = c;
c = t;
}
if (b < c)
{
t = b;
b = c;
c = t;
}
printf("%d > %d > %d\n", a, b, c);
return 0;
}
⑧看懂一个程序的三个步骤
梳理流程;
弄懂每个语句的功能;
不断试数。
(2)switch
①switch的用法
#include <stdio.h>
int main(void)
{
int val;
printf("请输入您要进入的楼层:");
scanf("%d", &val);
switch(val)
{
case 1:
printf("一层开!\n");
break;
case 2:
printf("二层开!\n");
break;
case 3:
printf("三层开!\n");
break;
default:
printf("没有盖到这一层!\n");
break;
}
return 0;
}
②switch的说明
switch是选择,不是循环;
如果在switch语句中出现了break语句,该语句的功能只是退出switch语句转去执行它下面的语句;
在switch语句中出现continue是错误的,除非switch本身就属于for或while循环的一部分。
③break的用法
break如果用于循环则是用来终止循环;
break用于switch则是用于终止switch;
break不能直接用于If,除非if属于循环内部的一个子句;
当break出现在多层循环内部时,break只能终止距离它最近的循环;
在多层switch嵌套中,break只能终止距离它最近的switch。
④continue的用法
用于跳过本次循环余下的语句,转去判断是否需要执行下一次循环。
情况1:
for (1; 2; 3)
{
A;
B;
continue; //如果执行该语句,则执行完该语句后,会执行语句3,C和D会被跳过,不执行。
C;
D;
}
情况2:
while (表达式)
{
A;
B;
continue; //如果执行该语句,则执行完该语句后,会执行表达式,C和D会被跳过,不执行。
C;
D;
}
3、循环
定义:某些代码会被重复执行
分类:
for(用的最多)
while
do…while
(1)for循环
①引入:
#include <stdio.h>
int main(void)
{
int i;
int sum = 0;
for (i=1; i<=10; ++i)
sum = sum + i;
printf("sum = %d\n", sum);
return 0;
}
说明:
标记1:i = 1;
标记2:i <= 10;
标记3:++i;
标记4:sum = sum + i;
第一次循环:按照1243的顺序执行,3结束标志循环一次结束;
下一次循环:根据2判断,2成立则执行43;2不成立则退出循环,执行for循环下面的printf语句。
②for循环实现1-10奇数之和
#include <stdio.h>
int main(void)
{
int i;
int sum = 0;
for (i=1; i<10; i+=2) //i+=2等价于i = i+2
{
sum = sum + i;
printf("我今天很高兴!\n"); //可以验证一共循环5次
}
printf("sum = %d\n", sum);
printf("i = %d\n", i);
return 0;
}
③for与if的嵌套使用
求1~100之间所有的能被3整除的数字之和
#include <stdio.h>
int main(void)
{
int i;
int sum = 0;
for (i=1; i<=100; ++i)
{
if (i%3 == 0) //i能被3整除
sum = sum + i;
}
printf("sum = %d\n", sum);
return 0;
}
④格式
for(1; 2; 3)
语句A;
⑤强制类型转化
——(数据类型)(表达式)
功能:把表达式的值强制转化为前面所执行的数据类型
举例:
(int)(4.5+2.2) 最终值为6
(float)(5) 最终值为5.000000
应用:
#include <stdio.h>
int main(void)
{
int i;
float sum = 0;
for (i=1; i<=10; ++i)
{
sum = sum + 1.0/i; //推荐使用
//或者写为sum = sum + 1/(float)(i); 不推荐使用
}
printf("sum = %f\n", sum);
return 0;
}
⑥浮点数的存错带来的问题
float和double都不能保证可以精确地存储每一个小数(但也并不代表所有小数都不能精确存储)。
举例:
有一个浮点型变量x,如何判断x的值是否为零?
if (|x-0.000001| <= 0.000001)
是0;else
不是0;
⑦多个for循环的嵌套使用
第一种:
for (1; 2; 3) //1
for (4; 5; 6) //2
A; //3
B; //4
整体是两个语句,第1 2 3行 是第一个语句,第4行是第二个语句
执行流程:
1->2->4->5->A->6 6->判断5
若5不成立,则执行3 3->判断2
若2成立,则再次进入内部循环:2->4->5->A->6 6->判断5
若2不成立,则退出循环,执行B
第二种:
for (1; 2; 3)
for (4; 5; 6)
{
A;
B;
}
整体是1个语句
示例
```c
#include <stdio.h>
int main(void)
{
int i, j;
for (i=0; i<3; ++i)
{
printf("111\n");
for(j=2; j<5; ++j)
{
printf("222\n");
printf("333\n");
}
printf("444\n");
}
return 0;
}
(2)while循环
①执行顺序
格式:
while (表达式)
语句;
②与for的相互比较
示例:
#include <stdio.h>
int main(void)
{
int i;
int sum = 0;
/*
for (i=1; 1<=10; ++i)
{
sum = sum + i;
}
*/
i = 1;
while (i<=10)
{
sum = sum + i;
++i;
}
printf("sum = %d\n", sum);
return 0;
}
for和while可以互相转换
Tip:
for (1; 2; 3)
A;
等价于:
1;
while (2)
{
A;
3;
}
for逻辑性更强,更不容易出错,推荐使用for循环
③练习
从键盘输入一个数字,如果该数字是回文数,则返回yes,否则返回no
回文数:正着写和倒着写都一样
比如:121 12321 都是回文数
#include <stdio.h>
int main(void)
{
int val; //存放带判断的数字
int m;
int sum = 0;
printf("请输入您需要判断的数字:");
scanf("%d", &val);
m = val;
while(m)
{
sum = sum * 10 + m%10;
m /= 10;
}
if (sum == val)
printf("Yes!\n");
else
printf("No!\n");
return 0;
}
④斐波拉契序列代码阅读
/*
斐波拉契序列 :
1 2 3 5 8 13 21 34
*/
#include <stdio.h>
int main(void)
{
int n;
int f1, f2, f3;
int i;
f1 = 1;
f2 = 2;
printf("请输入您需要求的项的序列:");
scanf("%d", &n);
if (1 == n)
{
f3 = 1;
}
else if (2 == n)
{
f3 = 2;
}
else
{
for (i=3; i<=n; ++i)
{
f3 = f1 + f2;
f1 = f2;
f2 = f3;
}
}
printf("%d\n", f3);
return 0;
}
(3)do…while循环
①主要用于人机交互
格式:
do
{
......
}while(表达式);
Tip:
先执行内部语句,再判断表达式;表达式成立则再次执行内部语句;
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 = %lf, x2 = %lf\n", x1, x2);
}
else if (delta == 0)
{
x1 = x2 = (-b) / (2*a);
printf("该方程有唯一解:x1 = x2 = %lf", x1);
}
else
{
printf("该方程无实数解\n");
}
printf("您想继续吗?(Y/N):\n");
scanf(" %c", &ch); //%c前面必须加一个空格!!!(原因很复杂)
}while(ch == 'Y' || ch == 'y');
return 0;
}
4、函数(C语言的第二个重点)
回忆:C语言的第一个重点是流程控制
引入:在面向过程的C语言中,基本单位是函数
#include <stdio.h>
//void表示函数没有返回值,max是函数的名字,i和j是形式参数,简称形参
void max(int i, int j)
{
if (i > j)
printf("%d\n", i);
else
printf("%d\n", j);
}
int main(void)
{
int a, b, c, d, e, f, g;
a = 1, b = 2, c = 3, d = 9, e = -5, f = 100;
max(a, b); //把a的值发送给i,把b的值发送给j,程序跳到max函数内部执行
max(c, d);
max(e, f);
return 0;
}
(1)为什么需要函数
①避免了重复性操作
②有利于程序的模块化
(2)什么叫函数
逻辑上:能够完成特定功能的独立的代码块
物理上:
能够接收数据(也可以不接收);
能够对接收的数据进行处理(也可以不处理);
能够将数据处理的结果返回(也可以不返回)。
总结:函数是个工具,是为了解决大量类似问题而设计的,可以当作一个黑匣子
(3)如何定义一个函数
格式:
函数的返回值 函数的名字 (函数的形参列表)
{
函数的执行体
}
本质:
函数的本质是详细描述函数之所以能够实现某个特定功能的具体方法。
说明:
函数返回值的类型也称为函数的类型,因为如果函数名前的返回值类型和函数执行体中的return表达式的类型不同的话,则最终函数返回值的类型以函数名前的返回值类型为准,举例:
#include <stdio.h>
int f(void)
{
return 10.5; //因为函数的返回值类型是int,所以最终f返回的是0,而不是10.5
}
int main(void)
{
double x = 6.6;
x = f();
printf("%lf", x);
return 0;
}
(4)return与break的区别
①return用来终止被调函数,向主调函数返回表达式的值;
②如果返回值为空,则return只终止函数,不向被调函数返回任何值;
③break终止循环或switch,return是用来终止函数的。
(5)函数的分类
①有参函数 与 无参函数
②有返回值函数 与 无返回值函数
③库函数 与 用户自定义函数
④值传递函数 与 地址传递函数(实际上只有值传递函数,但是很多书上这样分类)
⑤普通函数 与 主函数(main函数)
一个程序必须有且只能有一个主函数(整个程序从主函数进去,无主函数程序无法进行);
主函数可以调用普通函数,普通函数不能调用主函数(因为main函数无法实现递归);
普通函数可以相互调用;
主函数是程序的入口也是函数的出口。
示例:
#include <stdio.h>
//void表示函数没有返回值,max是函数的名字,i和j是形式参数,简称形参
void max1(int i, int j)
{
if (i > j)
printf("%d\n", i);
else
printf("%d\n", j);
}
int max2(int i, int j)
{
if (i>j)
return i;
else
return j;
}
int main(void)
{
int a, b, c, d, e, f, g;
a = 1, b = 2, c = 3, d = 9, e = -5, f = 100;
// max(a, b); //把a的值发送给i,把b的值发送给j,程序跳到max函数内部执行
// max(c, d);
// max(e, f);
printf("%d\n", max2(a, b));
printf("%d\n", max2(c, d));
printf("%d\n", max2(e, f));
return 0;
}
判断一个数字是否为素数:
#include <stdio.h>
//判断一个数字是否为素数
int main(void)
{
int val;
int i;
scanf("%d", &val);
for (i=2; i<val; ++i)
{
if (val%i == 0)
break; //终止for循环
}
if (i == val)
printf("该数字是素数");
else
printf("该数字不是素数");
return 0;
}
弊端:此程序只能对一个变量进行判断,若数据量庞大,需要对每个变量重复编写,代码利用率不高。==>通过函数实现,函数可以对任意一个整数进行判断。
源代码(函数实现):
#include <stdio.h>
#include <stdbool.h>
//返回值为真假——真假可以用0、1表示,在C语言中可以用bool类型表示
//C语言中的bool数据类型只有两个值,真值(true)与假值(false)
bool IsPrime(int val) //Prime:素数
{
int i;
for (i=2; i<val; ++i)
{
if (val%i == 0)
break; //终止for循环
}
if (i == val)
return true;
else
return false;
}
int main(void)
{
int m;
scanf("%d", &m);
if (IsPrime(m))
printf("该数字是素数");
else
printf("该数字不是素数");
return 0;
}
(6)需要注意的问题
函数调用和函数定义的顺序:
函数定义应写在函数调用前面,如果函数调用写在了函数定义的前面,则必须加函数前置申明,否则会报错。
函数前置申明:
①告诉编译器即将可能出现的若干个字母代表的是一个函数
②告诉编译器即将可能出现的若干个字母所代表的函数的形参和返回值的具体情况
③函数申明是一个语句,末尾必须加分号
④对库函数的申明是通过头文件#include <库函数所在的文件的名字.h>来实现的
#include <stdio.h>
void f(void); //函数申明,分号不能丢掉
int main(void)
{
f();
return 0;
}
void f(void)
{
printf("哈哈!");
}
形参和实参:
①个数相同
②位置一一对应
③数据类型必须相互兼容
(7)如何在软件开发中合理的设计函数来解决实际问题
①一个函数的功能尽量独立、单一;
②多学习、多模仿牛人的代码。
示例:用两个函数来实现求1到某个数字之间所有的素数,并将其输出
#include <stdio.h>
#include <stdbool.h>
//用两个函数来实现求1到某个数字之间所有的素数,并将其输出,本程序代码量少,可重用性较高
//本函数的功能是判断m是否为素数,是则返回true,不是则返回false
bool IsPrime(int m)
{
int i;
for (i=2; i<m; ++i)
{
if (m%i == 0)
break;
}
if (i == m)
return true;
else
return false;
}
//本函数的功能是把1到n之间所有的素数在显示器上输出
void TraverseVal(int n)
{
int i;
for (i=2; i<=n; ++i)
{
if (IsPrime(i))
printf("%d\n", i);
}
}
int main(void)
{
int val;
scanf("%d", &val);
TraverseVal(val);
return 0;
}
(8)函数是C语言的基本单位,类是Java、C#、C++的基本单位
(9)常用的系统函数
double sqrt(double x); 求x的平方根
int abs(int x); 求x的绝对值
double fabs(double x); 求x的绝对值
(10)变量的作用域和存储方式
①按作用域分:
全局变量:在所有的函数外部定义的变量叫做全局变量
全局变量的使用范围:从定义位置开始到整个程序结束
局部变量:在一个函数内部定义的变量或者函数形参,都统称为局部变量。例如:
voie f(int i)
{
int j = 20;
}
i, j都属于局部变量
局部变量使用范围:只能在本函数内部使用
注意的问题:局部变量和全局变量命名冲突问题
在一个函数内部,如果定义的局部变量的名字和全局变量的名字一样时,局部变量会屏蔽掉全局变量。
②按变量的存储方式(后期详细介绍):
静态变量
自动变量
寄存器变量
四、C语言的灵魂——指针
1、指针基础知识
(1)引入
include <stdio.h>
int main(void)
{
int * p;
int i = 3;
p = &i;//ok
//p = i; //error 因为类型不一致,p只能存放int类型变量的地址,不能存放int类型的变量
//p = 55; //error 原因同上
return 0;
}
①p是变量的名字,int *** 表示 p变量存放的是int类型变量的地址**;
②int * p 不表示定义了一个名字叫做 *p 的变量,该理解为:p是变量名,p变量的类型是int * 类型 ;
③所谓 int * 类型,实际上就是存放int变量地址的类型;
④*p 就是以p的内容为地址的变量。
(2)p = &i;
①p保存了i的地址,因此p指向i
②p不是i,i也不是p
更准确的说:修改p的值不影响i的值,修改i的值也不影响p的值
③如果一个指针变量指向了某个普通变量,则:
*指针变量 就完全等同于 普通变量
例子:
如果p是个指针变量,且p存放了普通变量i的地址,则p指向了普通变量
*p 就完全等同于 i
或者说:在所有出现 *p 的地方,都可以替换成 i ;在所有出现 i 的地方,都可以替换成 *p
#include <stdio.h>
int main(void)
{
int * p;
int i = 3;
int j = 5;
p = &i;
j = *p; //等价于 i = j;
//* p 就是以p的内容为地址的变量
printf("i = %d, j = %d\n", i, j, p);
return 0;
}
(3)指针和指针变量的区别
①指针就是地址,地址就是指针;
②地址就是内存单元的编号;
③指针变量是存放地址的变量;
④指针和指针变量是两个不同的概念(但是要注意,通常我们叙述时,会把指针变量简称为指针,实际它们的含义并不一样)。
(4)指针的重要性
①通过指针可以表示一些复杂的数据结构
②快速地传递数据
③使函数返回一个以上的值
④能直接访问硬件
⑤能够方便处理字符串
⑥使理解面向对象语言中引用的基础
总结:指针使C语言的灵魂
(5)指针的定义
①地址:内存单元的编号;从0开始的非负整数
(对于32位机器)地址的范围:4G(0—4G-1)
若机器为32位,即有32跟地址线,可以控制2^32个单元(字节)
2^32B=2^30B*4=1G*4=4GB
机器内存条到底有多大 取决于硬件和软件的结合
②指针:
指针就是地址,地址就是指针;
指针变量就是存放内存单元编号的变量,或者说指针变量就是存放地址的变量;
指针和指针变量是两个不同的概念(但是要注意,通常我们叙述时,会把指针变量简称为指针,实际它们的含义并不一样);
指针的本质就是一个操作受限的非负整数(指针【地址】只能相减,不能进行加、乘、除)。
(6)链表
①专业术语:
首节点:
存放第一个有效数据的节点
尾节点:
存放最后一个有效数据的节点
头结点:
头结点的数据类型和首节点的类型是一模一样的;
头结点是首节点前面的那个节点;
头结点并不存放有效数据;
设置头结点的目的是为了方能对链表的操作。
头指针:
存放头结点地址的指针变量
②确定一个链表只需要一个参数:头指针
③优缺点
优点:插入删除元素效率高;不需要一个连续的很大的内存
缺点:查找某个位置的元素效率低
2、指针的分类
基本指针类型
指针和数组
指针和函数
指针和结构体
多级指针
(1)基本类型指针
①示例:
#include <stdio.h>
int main(void)
{
int * p;
int * q;
int i = 5;
p = &i;
//*q = p; error,语法编译会出错
//*q = *p; error,q是垃圾值,没有给q赋值
//p = q; //q是垃圾值,q赋给p,p也会变成垃圾值
printf("%d\n", *q); //13行
/*
q的空间是属于本程序的,所以本程序可以读写q内容;
但是如果q内部是垃圾值,则本程序不能读写*q的内容;
因为此时的*q所代表的内存单元的控制权限并没有分配给本程序;
所以本程序运行到13行时就会出错。
*/
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_1(a, b);
//huhuan_2(*p, *q);错误, 在main函数中没有定义p与q
//huhuan_2(a, b);错误, 将a发送给p, b发送给q, 报错 因为变量p与变量q是int * 类型的变量,只能存放普通变量的地址
//huhuan_2(&a, &b); //正确写法 , 但函数内部写法有误
huhuan_3(&a, &b);
printf("a = %d, b = %d", a, b);
return 0;
}
//可以实现互换功能
void huhuan_3(int * p, int *q)
{
int t;
t = *p;
*p = *q;
*q = t;
}
//函数huhuan_2也不能完成互换功能,只能实现变量p和变量q内容的互换
//void huhuan_2(int * p, int * q)
//{
// int * t; //如果要互换p和q的值,t必须也是int * 类型,不能是int类型,否则会报错
//
// t = p;
// p = q;
// q = t;
//
//}
//函数huhuan_1不能完成互换功能
//void huhuan_1(int a, int b)
//{
// int t;
// t = a;
// a = b;
// b = t;
//
// return; //此语句可写可不写,推荐写
//}
(2)指针和数组(指针和一维数组)
①一维数组名:
一维数组名是个指针常量;
它存放的是一维数组第一个元素的地址。
验证:
#include <stdio.h>
int main(void)
{
int a[5]; //a是数组名 5是数组元素个数 元素就是变量a[0]——a[4]
printf("%#X\n", &a[0]);
printf("%#X\n", a);
return 0;
}
输出结果相同,说明一维数组名存放的是一维数组第一个元素的地址。
②下标和指针的关系:
如果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]);
//或者printf("%d ", *(pArr+i));
printf("\n");
}
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
int b[6] = {-1, -2, -3, 0, 4, 5};
int c[100] = {1, 99, 22, 33};
f(a, 5); //a 是 int * 类型
f(b, 6);
f(c, 100);
return 0;
}
在被调函数中对pArrp的处理,等价于在主调函数中对数组的处理,示例:
#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),等价于 a[i],也等价于*(a+i)
printf("\n");
}
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
f(a, 5); //a 是 int * 类型
return 0;
}
④指针变量的运算:
指针变量不能相加,不能相乘,不能相除;
如果两个指针变量指向的是同一块连续空间的不同存储单元,则这两个指针才可以相减。
⑤一个指针变量到底占几个字节?
预备知识:sizeof(数据类型)
也可以写成:sizeof(变量名)
功能:返回值就是该数据类型所占的字节数
即:
sizeof(int) = 4, sizeof(char) = 1, sizeof(double) = 8
假设p指向char类型变量(1个字节)
假设p指向int类型变量(4个字节)
假设p指向double类型变量(8个字节)
p q r 本身所占的字节数是否一样?
答案:p q r 本身所占的字节数是一样的
总结:
一个指针变量,无论它指向的变量占几个字节,该指针本身只占8个字节。
一个变量的地址使用该变量首字节的地址来表示
示例:
#include <stdio.h>
int main(void)
{
char ch = 'A';
int i = 99;
double x = 66.6;
char * p = &ch;
int * q = &i;
double * r = &x;
printf("%d %d %d\n", sizeof(p), sizeof(q), sizeof(r));
return 0;
}
(3)指针和函数
如何通过被调函数修改主调函数普通变量的值?
①实参必须为普通变量的地址
②形参必须为指针变量
③在被调函数中通过
“*形参名 = ……”的方式
即可修改主调函数相关变量的值
总结:指针可以使函数返回一个以上的值,若没有指针,函数不可能返回一个以上的值
(4)指针和结构体
①为什么需要结构体
为了表示一些复杂的事物,而普通的基本类型无法满足实际要求。
②什么叫结构体
把一些基本类型数据组合在一起,形成的一个新的复合数据类型,这个叫做结构体。
③如何定义结构体
第一种方式:
#include <stdio.h>
struct Student
{
int age;
float score;
char sex;
};
int main (void)
{
struct Student st = {80, 66.6, 'F'};
......
return 0;
}
说明:
以下代码只是定义了一个新的数据类型,并没有定义变量。
struct Student
{
int age;
float score;
char sex;
};
定义变量语句为:struct Student st = {80, 66.6, ‘F’};其中,变量名为st。
第二种方式:
#include <stdio.h>
struct Student
{
int age;
float score;
char sex;
} st;
//在定义结构体的同时直接定义变量名,但此方法只能定义一次
int main (void)
{
......
return 0;
}
说明:第二种方法在定义结构体的同时也定义了变量名。
第三种方式:
#include <stdio.h>
struct
{
int age;
float score;
char sex;
} st;
int main (void)
{
......
return 0;
}
Tip:推荐使用第一种方式定义结构体。
④怎样使用结构体变量
赋值和初始化:
定义的同时可以给整体赋值;
如果定义完之后再赋值,则只能单个的赋初值。
举例:
#include <stdio.h>
struct Student
{
int age;
float score;
char sex;
};
int main(void)
{
struct Student st1 = {80, 66.6F, 'F'};
struct Student st2;
st2.age = 10;
st2.score = 88;
st2.sex = 'A';
printf("%d %f %c\n", st1.age, st1.score, st1.sex);
printf("%d %f %c\n", st2.age, st2.score, st2.sex);
return 0;
}
输出结果为:
80 66.599998 F 10 88.000000 A
提示与拓展:
66.6在C语言中默认是double类型,如果希望实数是float类型,则必须在末尾加f或F,因此66.6是double类型,66.6f或66.6F就是float类型;
输出结果为66.599998则是因为,float并不能保证每一个数字都能准确存储。
⑤如何取出结构体变量中的每一个成员(重点!):
格式1:
结构体变量名.成员名
格式2:
指针变量名->成员名
【格式2在计算机内部会被转化成 (*指针变量名).成员名 的方式来执行,所以这两种方式是等价的】
例子:
#include <stdio.h>
struct Student
{
int age;
float score;
char sex;
};
int main(void)
{
//取出结构体变量中的成员 方法1:
struct Student st;
st.age = 10;
st.score = 88;
st.sex = 'A';
// 取出结构体变量中的成员 方法2:
struct Student * pst = &st;
pst->age = 88;
printf("%d %f %c\n", st.age, st.score, st.sex);
printf("%d %f %c\n", pst->age, pst->score, pst->sex);
return 0;
}
Tip1:pst->age 在计算机内部会被转化成 (*pst).age,这就是->的含义,也是一种硬性规定。
所以:
pst->age 等价于 (*pst).age 也等价于 st.age
我们之所以知道pst->age等价于st.age,是因为pst->age是被转化成了(*pst).age来执行
Tip2:pst->age的含义:pst所指向的结构体变量中的age成员
结构体变量和结构体变量指针作为函数参数传递的问题:*
推荐使用结构体指针变量作为函数参数来传递
以代码的形式说明问题:
#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;
//对结构体变量输入
InputStudent(&st); //若要改变st的值,应向被调函数发送 &st
//printf("%d %c %s\n", st.age, st.sex, st.name);
//对结构体变量输出
OutputStudent(st); //不用改变st的值,直接输出即可,可以不用写成 &st
//但为了减少内存的消耗,也为了提高执行速度,推荐发送地址
return 0;
}
void InputStudent(struct Student * pstu)
{
(*pstu).age = 10; //或写成pstu->age = 10;
strcpy((*pstu).name, "张三"); //姓名要用strcpy括起来
(*pstu).sex = 'A';
}
void OutputStudent(struct Student ss)
{
printf("%d %c %s\n", ss.age, ss.sex, ss.name);
}
/*
//本函数无法修改主函数中st的值,所以本函数是错误的
void InputStudent(struct Student stu)
{
stu.age = 10;
stu.sex = 'A';
strcpy(stu.name, "张三"); //姓名不能写成stu.name = "张三",要写成strcpy(stu.name, "张三")的形式
}
*/
说明1:发送地址还是发送内容?
目的:体现了指针的部分优点
指针的优点之一:快速的传递数据,好用内存小,执行速度快
说明2:一般函数发送地址较多,即便发送地址会导致函数不安全,但这一问题可以通过在指针前加const解决,const表示可以接收数据内容,但不能对数据内容进行修改。
说明3:对于本例中的 OutputStudent() 函数,不用修改st的值时,可以发送其地址&st,也可以直接发送st,但为了减少内存的消耗,也为了提高执行速度,推荐发送地址。
struct Student st;
printf("%d\n", sizeof(st));输出结果为:108,说明st变量占108个字节 ,若发送内容,需要发送108个字节,好用内存太多,并且浪费时间,若发送指针可以解决此问题。
⑥结构体变量的运算
结构体变量不能相加 不能相减 也不能相乘除
但结构体变量可以相互赋值
举例:
- struct Student
- {
- int age;
- char sex;
- char name[100];
- };
- struct Student st1, st2;
st1+st2 st1-st2 st1*st2 st1/st2 都是错误的
st1 = st2 或者 st2 = st1 都是正确的
⑦综合应用
练习:利用结构体相关知识编写一个简单的学生管理系统。
提示:动态构造一个数组,存放学生信息,然后按分数输出。
源代码:
#include <stdio.h>
#include <malloc.h>
#include <string.h>
struct Student
{
int age;
float score;
char name[100];
};
int main(void)
{
int len;
struct 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个学生的信息:\n", i+1);
printf("age = ");
scanf("%d", &pArr[i].age);
printf("score = ");
scanf("%f", &pArr[i].score);
printf("name = ");
scanf("%s", pArr[i].name); //name是数组名,本身就已经是数组首元素的地址了,所以不用写取地址符&
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;
}
}
}
//输出
printf("\n\n学生的信息是:\n");
for (i=0; i<len; ++i)
{
printf("第%d个学生的信息是:\n", i+1);
printf("age = %d\n", pArr[i].age);
printf("score = %f\n", pArr[i].score);
printf("name = %s\n", pArr[i].name);
printf("\n");
}
return 0;
}
(5)多级指针
示例:
#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", ***r);
}
Tip:
*p = i;
*q = p; **q = i;
*r = q; **r = p; ***r = i;
3、专题:动态内存分配(重点难点)
(1)传统数组的缺点
①数组长度必须事先指定,且只能是常整数,不能是变量。
举例:
int a[5]; //ok
int len = 5; int a[len]; //error
②传统形式定义的数组,该数组的程序员无法手动释放。数组一旦定义,系统为该数组分配的存储空间就会一直存在,除非数组所在的函数运行结束(或者说:在一个函数运行期间,系统为该函数中数组所分配的空间会一直存在,直到该函数运行完毕时,数组空间才会被系统释放)。
举例:
void f(void)
{
int a[5] = {1, 2, 3, 4, 5};
//以上40个字节所占用的存储空间程序员无法手动编程释放它
//它只能在本函数运行完毕时由系统自动释放
}
③数组的长度一旦定义,其长度就不能再改变。数组的长度不能在函数运行的过程中动态的扩充或缩小。
④A函数定义的数组,在A函数运行期间可以被其他函数使用,但A程序运行完毕之后,A函数中的数组将无法再被其他程序使用。即:传统方式定义的数组不能跨函数使用。
(2)为什么需要动态分配内存
动态数组很好地解决了传统数组的这四个缺陷。
Tip:传统数组也叫静态数组
(3)动态内存分配举例:动态数组的构造
①malloc函数的使用
Tip:malloc是memory(内存) allocate(分配) 的缩写
示例1:
#include <stdio.h>
#include <malloc.h> //不能省
void f(void)
{
int a[5] = {1, 2, 3, 4, 5};
}
int main(void)
{
int i = 5; //分配了4个字节,静态分配
int * p = (int *)malloc(4); //13行
/*
1.要使用malloc函数,必须添加malloc.h这个头文件
2.malloc函数只有一个形参,并且形参是整形
3.4表示请求系统为本程序分配4个字节
4.malloc函数只能返回第一个字节的地址
5.13行分配了12个字节:4 + 8 = 12 ,p变量占8个字节,p变量所指向的内存占4个字节
6.p本身所占的内存是静态分配的,p所指向的内存是动态分配的
*/
free(p); //free(p)表示把p所指向的动态内存给释放掉
//p本身的内存是静态的,不能由程序员手动释放,p本身的内存只能在p变量所在的函数运行终止时,由系统自动释放
printf("同志们好!\n");
return 0;
}
示例2:
#include <stdio.h>
#include <malloc.h>
void f(int * q)
{
*q = 200;
}
int main(void)
{
int * p = (int *)malloc(sizeof(int));
//sizeof(int)返回值是int所占的字节数
*p = 10;
printf("%d\n", *p);
f(p);
printf("%d\n", *p);
return 0;
}
②动态数组的构造
格式:
pArr = (int *)malloc(4*len);
上述语句动态地构造了一个一维数组,该一维数组的长度是len,该数组的数组名是pArr,该数组的每个元素是int类型;类似于 int pArr[len]。
示例:
#include <stdio.h>
#include <malloc.h>
int main(void)
{
int a[5]; //如果int占4个字节,则本数组共包含20个字节,每4个字节被当作一个int变量来使用
int len;
int * pArr;
int i;
//动态地构造一维数组
printf("请输入你要存放的元素个数:");
scanf("%d", &len);
pArr = (int *)malloc(4 * len);
//对一维数组进行操作,如:对一维数组进行赋值
for(i=0; i<len; ++i)
scanf("%d", &pArr[i]);
//对一维数组进行输出
printf("一维数组的内容是:");
for(i=0; i<len; ++i)
printf("%d\t", pArr[i]);
free(pArr);//释放被动态分配的数组
return 0;
}
拓展:动态地扩大或缩小内存,需要调用realloc函数,格式如:”realloc(pArr,100);”,此函数不必深入研究。
(4)静态内存和动态内存的比较
静态内存是由系统自动分配,由系统自动释放;静态内存是由栈分配的。
动态内存由程序员手动分配,手动释放;动态内存是在堆分配的。
(5)跨函数使用内存
#include <stdio.h>
#include <malloc.h>
void f(int ** q)
{
*q = (int *)malloc(sizeof(int));
//sizeof(数据类型)返回值是该数据类型所占的字节数
//int型变量所占字节数并不一定是4,C语言并没有规定int型变量占4个字节,在不同软件中所占字节数可以不一样
//*q = 5; //error, *q = p
**q = 5; //等价于*p = 5, 成立
}
int main(void)
{
int * p;
f(&p);
printf("%d\n", *p);
return 0;
}
本文全面介绍了C语言的学习路线,覆盖预备知识、基础知识、重点难点等内容,详解指针概念与动态内存分配,适合初学者及进阶者阅读。
4866

被折叠的 条评论
为什么被折叠?



