C 语言快速入门
oj网站地址:http://oj.lgwenda.com/
一、环境搭建
1.1 MinGw
- 安装
MinGw编译器
通过网盘分享的文件:mingw64.zip
链接: https://pan.baidu.com/s/1O6b00EEllcJ4RvlfpgmZDw?pwd=u8jb 提取码: u8jb- 下载
mingw.zip压缩包,放到 C 盘根目录下,直接解压即可,如下图所示

1.2 Clion
- 安装
Clion开发环境
通过网盘分享的文件:CLion-2021.1.3.exe
链接: https://pan.baidu.com/s/1KYdGzU7Knkf57BPF4N_IkQ?pwd=mssv 提取码: mssv- 可以改路径但切记安装路径不要有中文 !!!





- 确保
MinGw环境没问题

- “ 优化 ”
Clion

- 保存如下压缩包:
通过网盘分享的文件:ide-eval-resetter-2.1.13.zip
链接: https://pan.baidu.com/s/1Kbibno_EhI5SQkCehPaAqw?pwd=w5fa 提取码: w5fa


二、数据类型&输入输出
2.1 数据类型
裁缝做衣服需要用到 化纤、纯棉、丝绸等不同类型的布料
那么程序员在编写程序时也会用到不同类型的数据类型

2.2 关键字
C 语言中有许多的关键字,后面会逐渐讲到这些关键字(不用去记),
下面罗列出来,目的是大概了解有哪些关键字,以避免命名变量的时候与关键字重名
(完全不用担心,因为
Clion会有自动提示)

2.3 常量
常量是指在程序运行过程中,其值不发生变化的量,常量又可分为
整型、浮点型(实型)、字符型和
字符串型, 如下图所示:

2.4 变量
变量代表内存中具有特定属性的一个存储单元,它用来 存放数据,即 变量的值 。
这些值在程序的执行过程中是可以改变的
变量名实际上以一个名字代表一个对应的存储单元地址。
编译、链接程序时,由 编译系统为每个变量名分配对应的内存地址(就是空间)。
从变量中取值实际上是通过变量名找到内存中存储单元的地址,并从该存储单元中读取数据
如下图所示:

变量的命名规定如下:C语言规定标识符只能由
字母、数字&下划线三种字符组成,并且
第一个字符必须为字母或下划线, 例如:
sum_total,month,Student_name,lotus_1_2_3,BASIC,li_ling是正确的;而
M.D.John(不能有.) ¥123(不能有¥) 3D64(不能数字开头) a>b(不能有>)这些是错误的。
编译系统认为
大写字母和小写字母是不同的字符, 因此 C 语言要求对所有用到的变量做强制定义,即
“先定义,后使用”。同时在选择变量名和其它标识符时,应尽量做到"见名知意",即选择具有含义的英文单词(或其缩写)作为标识符。
注意:变量名不能与关键字同名!
2.5 整型数据
2.5.1 符号常量
- 定义一个符号常量
#define 符号常量名 常量的值注意,这条定义语句是没有分号的
;示例:
#include <stdio.h>
#define IRAI 3+2
int main(){
int i = IRAI*2;
printf("i=%d\n",i); // i = 7
}最终的输出结果是
7。 而不是常见错误的想着3+2是5, 然后5 * 2是10啊实际上,符号常量是直接替换的效果,也就是代码中其实是
i = 3 + 2 * 2(先计算乘法)
2.5.2 整型变量
- 定义一个整型变量时要使用关键字
int - 一个整型变量的大小是
4个字节
这里掌握到这样足以,其它后面再补充
2.6 浮点型数据
2.6.1 浮点型常量
- 表示浮点型常量的形式有两种,如下表所示,其中
e代表10的幂次, 幂次可正可负。

📌 注意:
字母 e (或 E) 之前必须有数字,且 e 后面的指数必须为整数
- 正确示例:
1e3 、1.8e-3、-123e-6、-.1e-3 - 错误示例:
e3 、2.1e3.5、.e3、e
#include <stdio.h>
int main(){
float a = 3e-3; // 3 * 10的-3次方
printf("a = %f\n",a); //a = 0.003000
}2.6.2 浮点型变量
- 定义一个浮点型变量时要使用关键字
float - 一个浮点型变量的大小是
4个字节 - 浮点型变量打印的时候默认是
小数点后六位,不足用0补足(如上面0.003000)
2.6.3 关于除号 /
#include <stdio.h>
int main(){
int i = 5;
float j = i/2; //这里做的是整型运算,因为左右操作数都是整型
float k = (float)i/2; //强制类型转换
float z = 1/3;
printf("%f\n",j); //2.000000
printf("%f\n",k); //2.500000
printf("%f\n",z); //0.000000
}2.6.4 关于 %f 格式控制
#include <stdio.h>
int main(){
// %f 格式控制
int i = 10;
float score = 96.21;
//默认右对齐,加负号左对齐
//%5.2f 即为宽度为 5,保留 2 位小数
printf("student number=%-3d,score=%5.2f\n",i,score);
i = 100;
score = 98.34;
printf("student number=%3d,score=%5.2f\n",i,score);
/* 打印效果:
student number=10 ,score=96.21
student number=100,score=98.34
*/
}2.7 字符型数据
2.7.1 字符型常量
用
单引号括起来的一个字符是字符型常量,且只能包含一个字符!!!例如
'a'、'A'、'1'、' '是正确的字符型常量而
'abc'、"a"、""是 错误的字符型常量。以
"\"开头的特殊字符称为转义字符, 转义字符可用来表示 回车 、 退格 等功能键。

2.7.2 字符型变量
- 字符型变量使用关键字
char进行定义 - 一个字符型变量占用
1字节大小的空间
一个字符常量存放到一个字符型变量中时,实际上并不是把该字符的值放到内存中,
而是把该字符的
ASCII码值放到存储单元中,每个字符的ASCII码值如下表:

打印字符型变量时,如果以字符形式打印,那么计算机会到
ASCII码表中查找字符型变量的
ASCII码表值,查到对应的字符后会显示对应的字符。如下图:

这样,字符型数据和整型数据之间就可以通用。字符型数据既可以以字符形式输出,
又可以以整数形式输出,还可以通过运算获取想要的各种字符。看下面示例代码:
#include <stdio.h>
int main(){
char c = 'A';
printf("%c\n",c); //A
printf("%d\n",c); //65
printf("%c\n",c+32); //a
printf("%d\n",c+32); //97
}对于字符型变量,无论是赋
ASCII值还是赋字符,使用%c打印输出时得到的都是字符,使用
%d打印输出时得到的都是ASCII码值。将小写字母转换为大小字母时,由上面的
ASCII码表可以知道小写字母与大写字母的差值为32因此将 字符变量 c (值为
a)减去 32 就可以得到大写字母 A
2.8 字符串型数据
字符串型常量是由
一对双引号括起来的字符序列。例如 :
"How do you do"、"China"、"a"、"$123.45"都是合法的字符串型常量。我们可以用语句
printf("How do you do")输出一个字符串。但要注意的是:
'a'是字符型常量,而"a"是字符串型常量,二者是不同的!!
例如,如果先用语句
char c定义一个字符型常量c,后令c="a"或c="CHINA"都是非法的。原因是不可以将字符串型常量赋值给字符型变量,C 语言中没有定义字符串型变量的关键字,
介绍字符数组的时候讲会介绍如何存放字符串。
📌 注意:
C 语言规定,在每个字符串型常量的结尾会加一个字符串结束标志,以便系统根据此来
判断出字符串是否结束。C 语言规定以字符
'\0'作为字符串的结束标志。例如,字符串型常量
"CHINA"在内存中的存储结果如下图所示,它占用的内存单元不是5个字符,
而是6个字符。 即大小为 6 个字节,最后一个字符为'\0'。 然而在输出时不输出'\0'因为无法显示

2.9 输出 - printf
2.9.1 混合运算
说明:
- 整型,字符型,浮点型可以混合运算
- 在接收字符型的时候要注意:它会接收空格字符!
示例代码:
#include <stdio.h>
int main(){
// 混合运算
int i;
char j;
float k;
scanf("%d %c %f",&i,&j,&k);
printf("i=%d,j=%c,k=%.2f\n",i,j,k);
printf("sum=%.2f",i+j+k);
return 0;
/*
input: 10 a 98.3
output:
i=10,j=a,k=98.30
sum=205.30
*/
}2.9.2 进制转换
- 示例代码:
#include <stdio.h>
int main(){
//进制转换
int i = 123;
printf("%d\n",i); //十进制 123
printf("%o\n",i); //八进制 173
printf("%x\n",i); //十六进制 7b
return 0;
}2.9.3 printf 函数介绍
printf函数可以输出各种类型的数据,包括整型、浮点型、字符型、字符串型等...📌 实际原理是 :
printf函数将这些类型的数据全部格式化为字符串后,放入标准输出缓冲区,然后将结果显示到屏幕上。
语法如下:
#include <stdio.h>
int printf(const char *format,...)
printf函数根据format给出的格式打印输出到stdout(标准输出) 和其他参数中。
- 示例代码:
#include <stdio.h>
int main(){
int age = 21;
printf("Hello i am %s , i am %d year old.","irai",age);
// output: Hello i am irai , i am 21 year old.
return 0;
}
📌 说明:(2.6.4 演示过)
位于
%和格式化命令之间的一个整数被称为最小字段宽度说明符,通常会加上空格来控制格式。用
%f精度修饰符指定想要的小数位数。例如%5.2f会至少显示 5 位数字并带有两位小数的浮点数用
%s表示输出一个字符串,字符串实际多长,就会输出多长。printf函数的所有输出都是右对齐的 , 除非在%符号后面放置了负号。例如
%-5.2f会显示宽度为 5 位字符、2 位小数位的浮点数 并且 左对齐。
2.10 scanf 函数
2.10.1 scanf 原理
C 语言未提供 输入/输出关键字,其输入和输出是通过标准函数库来实现的。
C 语言通过
scanf函数来读取键盘输入,键盘输入又被称为标准输入。当scanf读取标准输入时,如果还没有输入任何内容,那么
scanf函数会被卡住 (专业用语为阻塞) 。
📌 本质上:
实际对于一条语句,比如 scanf("%d",&i) ,我们读取标准输入缓冲区都是以 字符串 的形式读取,
然后在语句的第一个参数 %d 告诉函数,要把这个字符串 强转 成什么类型,在第二个参数 &i
告诉函数,强转后要把这个值存储到哪块内存空间里,所以要有取地址符 & .
接下来看一个常见错误的例子:
#include <stdio.h>
int main(){
int i;
char c;
scanf("%d",&i);
printf("i=%d\n",i);
//fflush(stdin);//清空标准输入缓冲区
scanf("%c",&c);
printf("c=%c",c);
return 0;
/*
input: 20
output:
i=20
c=
*/
}📌 注意:
执行时输入 20 , 然后回车,显示结果如上 output ( c 没获取到值 且 第二个 scanf 没有阻塞) 。
为什么第二个 scanf 函数不会被阻塞呢?其实是因为第二个 scanf 函数读取了缓冲区的 '\n' ,
即 scanf("%c",&c) 实现了读取,打印其实输出了换行,所以不会阻塞。
❓ 但是如果我们将上面例子中注释的
fflush(stdin)打开,就会发现第二个scanf就会阻塞,这是什么原因????下面介绍缓冲区原理:
行缓冲:在这种情况下,当在输入和输出中遇到换行符时,将执行真正的I/O操作。这时,我们输入的字符先存放到缓冲区中,等按下回车键换行时才进行实际的
I/O操作,典型代表是标准输入缓冲区(
stdin) 和 标准输出缓冲区(stdout),printf就是stdout如上面例子所示,我们向标准输入缓冲区中放入的字符为
'20\n',输入'\n'(回车)后,scanf函数才开始匹配,scanf函数中的%d匹配整型数 20 ,然后放入变量i中,接着进行打印输出,这时候
'\n'依旧在stdin中,如果第二个scanf函数是scanf("%d",&i)的话,那么依然会发生阻塞,
因为scanf函数在读取整型数、浮点数、字符串时会忽略 '\n' 、空格符等字符。(忽略是指
scanf函数执行时会首先删除这些字符,然后再阻塞)。因为例子里是执行
scanf("%c",&c)语句,不会忽略任何字符,所以读取了还在缓冲区的'\n'。
2.10.2 多种数据类型混合输入
其实这个在
2.9.1 混合运算的时候已经演示过了。前面我们提到,
%c会读取空格和'\n',那么我们在读取的时候有%c就要小心一点。比如
%d %c这样,两者之间要加一个空格才能匹配正确。
三、运算符&表达式
3.1 运算符分类

3.2 算术运算符&表达式
| 运算符 | 说明 |
|---|---|
+ | 加法 |
- | 减法 |
* | 乘法 |
/ | 除法 (两个操作数都是整数时整除) 在其他情况执行浮点数除法 ... |
% | 取余 / 取模 (接收两个整型操作数 , 将左操作数除右操作数取余数而不是商) |
📌 说明:
乘、除、取余运算符的优先级高于加、减运算符。
- 示例代码:
#include <stdio.h>
int main(){
int result = 4+5*2-6/3+10%4;
printf("result=%d",result); //result=14
return 0;
}3.3 关系运算符&表达式
| 运算符 | 说明 |
|---|---|
> | 大于 |
< | 小于 |
== | 等于 |
>= | 大于等于 |
<= | 小于等于 |
!= | 不等于 |
说明:
关系表达式的值只有真和假,对应的值为1和0。由于C语言中没有布尔类型,
所以在C语言中0值代表假,非0值代表真。
关系运算符的优先级低于算术运算符
常常在写判断等于的时候,比如
i==3, 容易会漏掉一个等号写成i=3,那么我们可以写成
3==i,即把常量写在前面, 变量写后面。这样如果漏一个等号的话就会编译不通(因为常量不能作为赋值运算符的左操作数),
从而快速发现错误(这是属于华为公司内部的一条编程规范)
在编写程序时,如果要判断三个数是否相等,绝对不可以写成
5==5==5,这种写法一般都为假,为什么❓ 因为
5==5得到结果为 1 ,然后判断
1==5得到的结果就是 0 了。所以要写成a==b && b==c
- 示例代码:
#include <stdio.h>
int main(){
// 关闭标准输出流的缓冲
setvbuf(stdout, NULL, _IONBF, 0);
int a;
while(scanf("%d",&a)){
//不能用数学上的连续判断大小来判断某个数
//if(3<a<10) 错误的
//正确的:
if(a > 3 && a < 10){
printf("a is between 3 and 10\n");
}else{
printf("a is not between 3 and 10\n");
}
}
}3.4 运算符优先级表


3.5 逻辑运算符&表达式
3.5.1 逻辑运算符
| 运算符 | 说明 |
|---|---|
! | 逻辑非 |
&& | 逻辑与 |
| ` |
- 说明:
- 逻辑非的优先级高于算术运算符
- 逻辑与和逻辑或的优先级低于关系运算符
- 逻辑表达式的值只有真和假,对应的值为 1 和 0
- 下面是一个判断闰年的例子,因为要重复测试,所以加了个
while
#include <stdio.h>
int main() {
int year;
while (scanf("%d", &year)) {
if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) {
printf("%d is leap year.\n",year);
} else {
printf("%d is not leap year.\n",year);
}
}
return 0;
}3.5.2 短路运算
int main() {
int i=0;
i && printf("you can't see me");
i || printf("you can see me");
return 0;
}- 说明:
- 逻辑与短路运算当前面一个表达式为假时,后面的表达式不会得到执行
- 逻辑或短路运算是当前面一个表达式为真时后面的表达式不会得到执行
📌 原理:
对于逻辑与来说,逻辑与要求左操作数和右操作数都为真的时候才会整体为真。
那么如果逻辑与的左操作数为假了,那么无论右操作数是什么,
整体表达式都是为假的,所以就不会执行右操作数位置的代码
对于逻辑或来说,逻辑或要求左操作数和右操作数只需要一个为真,整体就为真。
那么如果左操作数已经为真了,那么右操作数无论是什么,
整体的表达式都是真的,所以就不会执行右操作数位置的代码
3.6 赋值运算符
- 赋值运算符
=的作用 :用来改变某一个变量的值
3.6.1 左值&右值
为了理解有些操作符存在的限制:必须理解 左值(
L-value) 和 右值(R-value)的区别比如下面的例子,左值为
变量,右值为常量或者表达式就是正确的
a = b + 25;
a = 20而如果交换位置
3==a;
b+25==a;这样的赋值语句就是非法的,因为左值不论是 3 还是 b+25 都并未标识一个特定的位置(并不对应特定的内存空间)
非法的赋值语句会导致以下的报错(要学会看懂):

- 但是在某些场景下经常会这么写(在
3.3中提到,这是一条编程规范),如
if(3==i){
... ...
}- 这样就不容易把
==写成=, 因为漏掉一个等号会导致赋值语句非法,从而编译报错。
3.6.2 复合赋值运算符
- 复合赋值运算符操作是一种缩写形式,例如:
num = num + 5;- 对变量
num进行赋值操作,新值为这个变量的旧值与一个整型常量 5 相加的结果。就可以写成如下:
num += 5;- 复合赋值运算符优点:
- 简化了程序,使程序更精炼,提升阅读速度
- 提高了编译效率
同理还可以有
num -= 5 ; num /= 5 ; num *= 5;3.7 求字节运算符 sizeof
很多初学者会认为
sizeof是一个函数,这种理解是错误的,实际上sizeof是一个运算符,不像其他运算符那样是一个符号,
sizeof是字母组成的,用于求常量或变量所占用的空间大小,看下面示例:
int main() {
int i = 0;
printf("i size is %d\n",sizeof(i)); //i size is 4
return 0;
}运行结果为
i size is 4, 可以求得整型变量占用的空间大小是 4 个字节
四、选择&循环
4.1 选择结构
4.1.1 关系&逻辑表达式
程序员控制程序执行逻辑运算需要
选择&循环结构,在介绍这两个结构之前,先来了解一下关系&逻辑表达式的计算过程。在第三章中,我们了解到 :
- 算术运算符的优先级高于关系运算符
- 关系运算符的优先级高于逻辑与和逻辑或运算符
相同优先级的运算符
从左至右进行结合, 那么表达式5>3&&8<4-!0的最终值是多少?看下图:

4.1.2 if-else 语句
当你打开衣柜拿出最上面的一件衣服的时候,你就会判断这件衣服是不是你想穿的。如果是,那么
你就会穿上;如果不是,那么你就会去找其他衣服。那么换成计算机语言的伪代码可能会如下:
拿出一件衣服();
if(这件衣服是我想穿的){
穿上();
}else{
找其他衣服();
}所以,在计算机中,我们用
if判断语句来实现这样的效果:if判断条件(表达式) 为真,就执行某个语句,反之不执行这个语句。当然也可以
if为真执行一个语句else执行另一个语句。

- 看下面判断一个输入值是否大于 0 的示例代码:
#include <stdio.h>
int main() {
int i = 0;
while(scanf("%d",&i)){
if(i>0){ //不要在括号后面加分号
printf("i is bigger than 0\n");
}else{
printf("i is not bigger than 0\n");
}
}
return 0;
}if-else语句也可以多个同时使用(多分支语句),如下图 3 所示,但是,无论有多少个
else if语句,程序都只会执行其中的一个语句。

- 下面是一个关于用电量的例子:用电量越高,电的单价越高,但最终
cost只会被赋值一次
if(number > 500){
cost=0.15;
}else if(number > 300){
cost=0.10;
}else if(number > 100){
cost=0.075;
}else if(number > 50){
cost=0.05;
}else{
cost=0;
}同时,
if语句也支持多层嵌套,在if语句中又包含一个或多个if语句,如上面图 4 所示。注意(嵌套
if): 为了使我们可以不用考虑悬空的else等问题(表达式为一句可以不加括号)那么我们就得保持良好的编程习惯,即使表达式只有一句,也要加括号,这样会使层次清晰。
if(i>1){
if(i<10){
printf("i>1 and i<10\n");
}
}else{
printf("i < 1\n")
}4.2 循环结构
4.2.1 while 循环
while语句用来实现“当型”循环结构,其一般形式为"while(表达式){语句}",当表达式的值非 0 时,执行while语句中的内嵌语句。其特点是:先判断表达式,后执行语句。具体流程如下图所示:

❗ 注意:在语句中需要有让表达式趋近于假的操作来使程序跳出循环,否则会形成死循环。
- 【例】用
while语句计算 1 到 100 之间所有整数之和
#include <stdio.h>
int main() {
//使用while语句计算1到100所有整数之和
int num = 1;
int sum = 0;
while(num <= 100){
sum += num;
num++;
}
printf("sum=%d",sum); //sum=5050
return 0;
}📌 注意:
注意
while后面不能加分号,虽然编译会通过,但是会死循环通常
while语句即使只有一句,我们也用花括号括起来,这么做是因为程序往往会经过多次修改,使用花括号可以让程序更加清晰,避免向循环内添加语句时出错。
4.2.2 for 循环
C 语言中的
for循环语句使用非常灵活,不仅可以用于循环次数已经确定的情况,而且可以用于循环次数不确定而只给出循环结束条件的情况,它完全可以代替 while 循环
语句,其一般形式为:
for(表达式1;表达式2;表达式3){语句};for循环语句的执行过程如下:1). 先求解 表达式1 (初始化)
2). 求解 表达式2,若其值为真(值为非0), 则先执行 for 语句中指定的内嵌语句,
后执行第 3) 步。若其值为假(值为 0),则结束循环,转到第 5) 步。
3). 求解表达式 3
4). 转回第 2) 步继续执行
- 循环结束,执行 for 语句下面的语句

❗ 注意: for 循环语句中必须且只有 两个分号
- 【例】用
for语句计算 1 到 100 之间所有整数之和
#include <stdio.h>
int main() {
//使用for语句计算1到100所有整数之和
int sum=0;
for(int num=1;num<=100;num++){
sum += num;
}
printf("sum=%d",sum); //sum=5050
return 0;
}- for 循环的可读性要比 while 的好,所以能使用 for 循环时不要强制改为 while 循环
4.2.3 continue 语句
continue语句的作用为结束本次循环,即跳过循环体中下面尚未执行的语句,接着进行是否执行下一次循环的判断,其一般形式为:
continue;- 【例】1到100之间的奇数求和
#include <stdio.h>
int main() {
//1到100的奇数求和
int sum=0;
for(int num=1;num<=100;num++){
if(num%2==0){
continue;
}
sum += num;
}
printf("sum=%d",sum); //sum=2500
return 0;
}4.2.4 break 语句
break语句的作用是结束整个循环过程,不再判断执行循环的条件是否成立。
break;- 【例】从 1 开始累加,当累加的和大于 2000 时结束 for 循环
#include <stdio.h>
int main() {
//从 1 开始累加,当累加的和大于 2000 时结束 for 循环
int sum=0,num;
for(num=1;sum<=2000;num++){
sum += num;
}
printf("sum=%d,num=%d",sum,num); //sum=2016,num=64
return 0;
}4.2.5 嵌套循环
嵌套循环,常见的是嵌套 for 循环,即外面一层 for 循环的循环体里面又有一层 for 循环,
因此通过下面的例子来体会一下两层 for 循环:
#include <stdio.h>
int main() {
//两层 for 循环
int i,j;
for (i = 0; i < 5; i++) {
printf("i=%d\n",i); //每次内层循环开始,输出 i 的值
for (j = 0; j < 5; j++) {
printf("%d",j); //j 从 0 开始,每次加1,直到 j = 5时,循环结束
}
printf("\n");
}
return 0;
}观察程序可以感受到,先是打印
i=?, 表面这是第几次外层循环,内层再有一个 5 次的 for 循环来打印
j, 内层是没有换行的,也就是说,一次i的循环,有 5 个j的数打出来一行,然后 for循环结束后下面有一个换行。可以感受到
i 是行号,j是列号的感觉,记住这种感觉,后面常用到 ...打印效果:
/*
i=0
01234
i=1
01234
i=2
01234
i=3
01234
i=4
01234
*/- 再通过一个例子来练习一下双层循环,即输出下图的三角形:

- 示例代码:
#include <stdio.h>
int main() {
//两层 for 循环 - 输出三角形
int i,j;
for(i=0;i<5;i++){
//第i行打印i个星,如第一行打印一个,第二行打印两个
for(j=0;j<=i;j++){
printf("*");
}
printf("\n");
}
return 0;
}五、一维数组&字符数组
5.1 数组的定义
生活中:为了存放鞋子,假设你把衣柜最下面的一层分成了 10 个连续的格子,此时,
让他人帮你拿鞋子就会很方便,例如你可以直接告诉他拿衣柜最下面一层第三个格子中的鞋子
同样在计算机中现在有 10 个整数存储在内存中,为了方便存取,我们可以借助 C 语言
提供的数组,通过一个符号来访问多个元素 。
- 某班的学生学习成绩、一行文字、一个矩阵等数据的特点如下:(适合使用 -- 数组)
- 具有相同的数据类型
- 使用过程中需要保留原始数据
- 一维数组的定义格式为:
类型说明符 数组名 [常量表达式];- 例如:
int a[10];定义一个整型数组,数组名为 a , 它有 10 个元素
声明数组时要遵循以下规则:
数组名的命名规则和变量名的相同,即遵循标识符命名规则。
在定义数组时,需要指定数组中元素的个数,方括号(中括号)中的
常量表达式用来表示元素的个数,即数组长度。
常量表达式中可以包含常量和符号常量,但不能包含变量。也就是说,C 语言
不允许对数组的大小做动态定义,即数组的大小不依赖于程序运行过程中变量的值
- 以下是
错误的声明示例(最新的 C 标准可能支持,但最好不要这么写):
int n;
scanf("%d",&n); /* 在程序中临时输入数组的大小 */
int a[n]数组声明的其他常见错误如下:
float a[0]; /* 数组大小为 0 没有意义 */
int b(2)(3); /* 不能使用圆括号 */
int k=3,a[k]; /* 不能用变量说明数组大小 */5.2 内存中的存储&初始化
- 语句
int mark[100];定义的一维数组mark在内存中的存放情况如下图所示:

每个元素都是整型元素,占用 4 个字节, 数组元素的引用方式是 数组名[下标] 。
所以访问 mark 数组中的元素就是 mark[0],mark[1] ... mark[99] ,
注意没有 mark[100] , 因为下标 0-99 就是 100 个数 , 从 0 开始编号
- 调试窗口的内存视图::

一维数组初始化:
- 1). 在定义数组的时候给数组元素赋初值:
cint a[10] = {0,1,2,3,4,5,6,7,8,9}不能写成下面这种
cint a[10]; a[10] = {0,1,2,3,4,5,6,7,8,9}- 2). 可以给一部分元素赋值:(其它默认的赋值是
0):
cint a[10] = {0,1,2,3,4}- 3). 那么既然默认是
0, 如果要初始化一个全0数组就用以下这种:
cint a[10] = {0};千万不要写成下面这种低效的:(低效的意思是,语法没问题,但是看出来你没有好好学)
cint a[10] = {0,0,0,0,0,0,0,0,0,0}- 4). (不推荐使用) 如果对数组全部元素赋初值,那么编译器会计算数组大小,但是不建议这样写代码:
cint a[] = {0,1,2,3,4};
5.3 数组访问越界
- 下面用一个例子来展示一下数组的访问越界:
#include <stdio.h>
int main() {
int a[5] = {1,2,3,4,5}; //定义数组时,数组长度必须固定
int j = 20;
int i = 10;
a[5] = 6; //越界访问
a[6] = 7; //越界访问会造成数据异常
printf("i = %d\n",i);
return 0;
}下图显示了代码运行情况。如下图所示,在第 10 行左键打上断点,然后单击 “小虫子” 按钮,
在内存视图中依次输入
&a、&i、&j来查看整型数组 a 、 整型变量 i 、 整型变量 j 的地址,即可看到三个变量的内存地址,这里就像我们给衣柜的每个格子的编号,第一格、第二格 ....
一直到柜子的最后一格。操作系统对内存中的每个位置也给予一个编号,对于
Windows32位控制台应用程序来说,这个编号的范围是
0x00 00 00 00到0xFF FF FF FF,总计为 2 的32 次方,大小为
4G。这些编号称为地址(我们是 64 位程序,地址显示的是 64 位)


此外:在变量窗口中输入
sizeof(a), 可以看到数组 a 的大小为 20 字节,计算方法其实就是
sizeof(int)*5: 数组中有 5 个整型元素,每个元素的大小为 4 个字节,所以共有 20 字节。访问元素的顺序是依次从
a[0]到a[4],a[5] = 6和a[6] = 7均为越界访问。从上图我们也能看到 -- 从未对变量 i 赋值,其值却发生了改变!
另一个值得关注的地方是 :
编译器并不检查程序对数组下标的引用是否在数组的合法范围内。这种不加检查的行为有好处也有坏处。好处是不需要浪费时间对已知正确的数组下标进行检测,
坏处是这样做将无法检测出无效的下标引用。一个良好的经验法则是:如果下标值是通过
那些已知正确的值计算得来的,那么就无须检查;如果下标值是由用户输入的数据产生的,
那么在使用它们之前就必须进行检查,以确保它们位于有效范围,不会越界访问。
5.4 数组的传递
- 通过一个例子来介绍数组的传递:
#include <stdio.h>
//一维数组的传递,数组长度无法传递给子函数
//C 语言的函数调用方式是值传递
void print(int b[],int len)
{
int i;
for(i=0;i<len;i++){
printf("%3d",b[i]);
}
b[4] = 20; //在子函数中修改数组元素
printf("\n");
}
//数组越界
//一维数组的传递
int main() {
int a[5] = {1,2,3,4,5}; //定义数组时,数组长度必须固定
print(a,5);
printf("a[4]=%d",a[4]); //a[4] 发生改变
return 0;
}如下图所示,在
print函数那行点击向下箭头,进入print函数,这时会发现数组 b 的大小变为8 字节,这是因为一维数组在传递时,其长度是传递不过去的,所以我们通过
len来传递数组中的元素个数。
实际数组名中存储的是数组的首地址, 在调用函数传递时,是将数组的首地址给了变量 b(其实变量 b 是指针类型,具体原理会在指针章节讲解),在
b[]的方括号中填写任何数字都是没有意义的。这时我们在
print函数内修改元素b[4]=20, 可以看到数组 b 存储的值和main函数中数组 a 的起始地址相同,即两个值指向内存中的同一个位置,当函数执行结束,数组 a
中的元素
a[4]就得到了修改。

5.5 字符数组初始化&传递
- 字符数组的定义方法与前面介绍的一维数组类似。例如:
char c[10];字符数组的初始化可以采用以下方式:
- 1). 对每个字符单独赋值进行初始化。例如:
cc[0]='I';c[1]='';c[2]='a';c[3]='m';c[4]='';c[5]='h';c[6]='a';c[7]='p';c[8]='p';c[9]='y';- 2). 对整个数组进行初始化、例如:
cchar c[10] = {'I','a','m','h','a','p','p','y'}
但是一般实际的时候都不用以上的初始化方式,因为字符数组一般用来存取字符串。
通常采用的初始化方法如下:
cchar c[10] = "hello"说明:因为 C 语言规定字符串的结束标志为
'\0', 而系统会对字符串常量自动加一个'\0',为了保证处理方法一致,一般会人为地在字符数组中添加
'\0', 所以字符数组存储的字符串长度必须比字符数组少 1 个字节。例如:
char c[10]最长存储 9 个字符,剩余的 1 个存'\0'。
- 【例】:字符数组初始化&传递
#include <stdio.h>
void print(char c[])
{
int i = 0;
while(c[i]){
printf("%c",c[i]);
i++;
}
}
//字符数组存储字符串必须存储结束符'\0'
int main() {
char c[5] = {'h','e','l','l','o'};
char d[6] = "hello";
printf("%s\n",c); //会发现打印了乱码
printf("%s\n",d); //有\0可以正常打印
print(d); // 模拟实现 printf 的 %s 效果
return 0;
}- 上例中代码的执行结果如下图所示:

❓ 为什么对数组赋值
c[5] = {'h','e','l','l','o'};却打印出乱码这是因为
printf通过%s打印字符串时,原理是依次输出每个字符,当读到结束符'\0'的时候才结束打印;而上面这个赋值语句没有手动地添加上
'\0',所以会一直打印到内存\0位置我们也通过
print函数模拟实现了printf的%s的打印效果,当c[i]为'\0'时,其值是 0 , 循环结束。我们也可以写成
c[i] != '\0'
5.6 scanf 读取字符串
- 【例】:
scanf读取字符串
#include <stdio.h>
int main() {
char c[10];
char d[10];
scanf("%s",c);
printf("%s\n",c);
scanf("%s%s",c,d);
printf("c=%s,d=%s",c,d);
return 0;
}scanf通过%s读取字符串,对 c 和 d 分别输入"are"和"you"(中间加一个空格),scanf在使用%s读取字符串时,会忽略空格和回车(这一点和%d和%f类似)。
输入顺序及执行结果如下图:

5.7 gets函数&puts函数
gets函数类似于scanf函数,用于读取标准输入。📌 但是前面我们已经知道了
scanf函数在读取字符串时遇到空格就认为读取结束,所以当输入的字符串存在空格时,我们需要使用
gets函数进行读取
gets函数格式如下:
char *gets(char *str);gets函数从STDIN(标准输入)读取字符并把它们加载到str(字符串) 中,直到遇到换行符
'\n'。如下面例子中所示:执行后,我们输入"how are you",共 11 个字符,可以看到
gets会读取空格,同时可以看到我们并未给数组进行初始化赋值,但是最后有
'\0', 这是因为gets遇到\n后,不会存储\n而是将其翻译为空字符
'\0'。
puts函数类似于printf函数,用于输出标准输出。格式如下:
int puts(char *str);函数
puts把str(字符串)写入STDOUT(标准输出)。puts会将数组 c 中存储的
"how are you"字符串打印到屏幕上,同时打印换行,相对于printf函数puts只能用于输出字符串,同时多打印一个换行符,等价于printf("%s\n",c)。
- 示例代码:
#include <stdio.h>
int main() {
char c[20];
gets(c);
puts(c); //相当于 printf("%s\n",c)
return 0;
}- 执行结果:

5.8 fgets&fputs
📌 本节说明:
在新标准的 C 语言中,不推荐使用
gets&puts, 原因如下:gets函数不检查目标缓冲区的大小,可能导致缓冲区溢出,从而引发安全漏洞,因此在
C11标准中已被移除puts函数会在输出字符串后自动添加换行符,这在某些情况下可能不是我们期望的效果。
此外:
OJ可能不支持gets,因为C11标准去掉了- 部分学校机试可以用
gets,部分不可以,因此建议使用fgets
5.8.1 fgets
功能:从指定的输入流中读取一个字符串,并保存到字符数组中
函数格式:
char *fgets(char *str, int n, FILE *stream);参数:
str: 字符数组,用于存储读取的字符串。n:要读取的字符数目,实际读取的字符数为n-1。stream: 输入流,可以是stdin或其它文件流
返回值:读取成功时返回字符数组的首地址,读取失败时返回
NULL。注意事项:如果读取到换行符,会将其包含在字符串中,可以通过检测末尾的换行符来判断是否读取了一整行
- 示例代码:
#include <stdio.h>
int main() {
char c[20];
printf("请输入一行文字:");
// 使用 fgets 从标准输入流(stdin)读取一行
fgets(c,sizeof(c),stdin);
printf("您输入的内容是:%s\n", c);
return 0;
}5.8.2 fputs
- 功能:将字符串写入指定的输出流(如
stdout)。 - 函数格式:
int fputs(const char *str, FILE *stream);- 参数:
str:要写入的字符串。stream:输出流,可以是stdout或其他文件流。
- 返回值:成功时返回非负数,失败时返回
EOF。 - 注意事项:不会在字符串末尾自动添加换行符。
- 示例代码:
#include <stdio.h>
int main() {
char c[20];
printf("请输入一行文字:");
// 使用 fgets 从标准输入流(stdin)读取一行
fgets(c,sizeof(c),stdin);
// 使用 fputs 将字符串输出到标准输出流(stdout)
fputs(c,stdout);
return 0;
}5.9 str 系列操作函数
str系列字符串操作函数主要包括strlen、strcpy、strcmp、strcat等。strlen函数:用于统计字符串长度strcpy函数 : 用于将某个字符串复制到字符数组中strcmp函数:用于比较两个字符串的大小strcat函数:用于将两个字符串连接到一起。
各个函数使用的格式:
#include <string.h>
size_t strlen(char *str);
char *strcpy(char *to,const char *from);
int strcmp(const char *str1,const char *str2);
char *strcat(char *str1,const char *str2);对于传参类型
char*,直接放入字符数组的数组名即可,因为前面说过,数组名其实就是数组的起始地址。
- 例:
#include <stdio.h>
#include <string.h>
int mystrlen(char c[]) {
int i=0;
while(c[i]){ //找到结束符后循环结束
i++;
}
return i;
}
int main() {
int len; // 用于存储字符串的长度
char c[20];
char d[100] = "world";
gets(c);
puts(c);
len = strlen(c); //获取字符串c的长度
printf("len=%d\n",len);
len = mystrlen(c);
printf("mylen=%d\n",len);
strcat(c,d); //把d连接到c的后面
puts(c);
strcpy(d,c); //把c中的字符串复制给d
puts(d);
//字符串的比较是一个一个字符比较,一样返回0,前者大返回正数,后者大返回负数
printf("c?aaa => %d\n", strcmp(c,"aaa"));
printf("c?d => %d\n", strcmp(c,d));
printf("c?how => %d\n", strcmp(c,"how"));
return 0;
}- 运行效果:

六、指针
6.1 指针の定义
内存区域中的每字节都对应一个编号,这个编号就是 “地址” 。如果在程序中定义了一个变量,那么在对程序进行
编译时,系统就会给这个变量分配内存单元,通过变量名直接访问变量的值称为
"直接访问", 如printf("%d",i)、
"scanf("%d",&i)"等;另一种通过指针来访问变量的值称为"间接访问"。即将变量 i 的地址存放到另一个变量中,在 C 语言中,指针变量是一种特殊的变量,它用来存放变量地址。
- 指针变量的定义格式如下:
基类型 *指针变量名- 例如:
int *i_pointer指针与指针变量是两个概念,一个变量的
地址称为该变量的"指针"。例如,地址 2000 是变量 i的指针。如果有一个变量专门用来存放另一变量的地址(即指针),那么称它为
"指针变量"。例如,下图中的
i_pointer就是一个指针变量。

📌 说明:
那么指针变量 i_pointer 本身占多大的内存空间呢?
本章中编写的程序都是 64 位的应用程序,寻址范围为 64 位 即 8 字节,所以对于
本章来说
sizeof(i_pointer)=8。但是如果编写的程序是 32 位的,那么寻址范围就是 4 字节
(考研中往往会强调程序是 32 位的程序)。
6.2 取地址操作符&取值操作符
取地址操作符为
&, 也称引用, 通过该操作符我们可以获取一个变量的地址值;取值操作符为
*, 也称解引用, 通过该操作符我们可以得到一个地址对应的数据。如下例所示,我们通过
&i获取整型变量i的地址值,然后对整型指针变量p进行初始化,p中存储的是整型变量i的地址值,所以通过*p(printf函数中的*p) 就可以获取到整型变量
i的值。p中存储的是一个绝对地址值❓ 那为什么取值时会获取 4 字节大小的空间呢
这是因为
p为整型变量指针,每个int型数据占用 4 字节大小的空间,所以p在解引用时会访问 4 字节大小的空间。并且这也是为什么指针变量需要指定类型的原因,虽然它们在 64 位程序里
都是占用 8 个字节,但是指定了类型,它会按照基类型去访问特定大小空间,从而获取值。
- 例:直接访问&间接访问
#include <stdio.h>
int main() {
int i = 5;
//定义了一个指针变量,i_pointer 就是指针变量名
//指针变量的初始化是某个变量取地址来赋值,不能随便写个数
int *i_pointer=&i;
printf("i=%d\n",i); //直接访问
printf("i_pointer=%d\n",*i_pointer); //间接访问
return 0;
}- 运行效果:

📌 注意:
- 1). 指针变量前面的
"*"表示声明这个变量是指针变量,不要想什么 “这个不是取值操作符吗???”
float *pointer_1注意指针变量名是 pointer_1 , 在声明完指针变量后 *pointer_1 是按这个地址去取值
2). 在定义指针变量时必须指定其类型。需要注意的是,只有整型变量的地址才能放到指向
整型变量的指针变量中。例如:下面的赋值是错误的:
float a;
int *pointer_1;
pointer_1 = &a; // 毫无意义而且会出错,完全没有必要浪费时间- 3). 如果已执行了语句
pointer_1 = &a;那么 &*pointer_1 的含义是什么呢?
答案:
"&"和"*"两个运算符的优先级别相同,但要自右向左的方向结合。因此,&*pointer_1与&a相同,都表示变量 a 的地址,也就是pointer_1
类似地,
*&a的含义是什么呢?首先进行
&a运算,得到 a 的地址,再进行*运算。*&a和*pointer_1的作用是一样的,他们都等价于变量 a , 即
*&a与 a 等价
4). C 语言本质上是一种自由形式的语言,这很容易诱使我们把
"*"写在靠近类型的一侧,如
int* a这个声明与int *a这个声明具有相同的意思,而且看上去更清晰, a 被声明成类型为
int*的指针。但是,这并不是一个好习惯,因为类似int *a,b,c的语句会使人们很自然地认为这条语句把所有三个变量声明为指向整型的指针,但事实上并非如此,
*实际上是
*a的一部分,只对 a 标识起作用,但其余两个变量只是普通的整型变量。要声明三个指针变量正确的语句应该是下面这样 :
int *a,*b,*c;6.3 指针の应用场景
很多初学者不喜欢使用指针,觉得使用指针容易出错,其实这只是因为没有掌握指针的使用场景。
一般来说,指针的使用场景通常只有两个,即
传递&偏移, 读者应时刻记得只有在这两种场景下使用指针,才能准确地使用指针。
6.3.1 指针の传递
- 【例1】:指针的传递使用场景:
#include <stdio.h>
void change(int j){
j = 5;
}
int main() {
int i = 10;
printf("before change i=%d\n",i); //这里打断点
change(i); //在这一步按 向下箭头,进入 change 函数
printf("after change i=%d\n",i);
return 0;
}在上面的 例1 中,定义了整型变量 i ,其初始化为 10 ,然后通过子函数试图修改整型变量 i 的值。但是
我们发现执行语句
printf("after change i=%d\n",i);后,打印 i 的值仍为 10 。
- 在主函数中添加一个监视看看 i 的地址 :

- 进入子函数再添加一个监视看看 j 的地址:

- 显然两个地址空间不一样,那么子函数改变的值是另一块地址空间的值,自然就跟 i 无关了。
- 接下来看一下上面示例的原理图:

说明:如上图所示,程序的执行过程其实就是内存的变化过程,我们需要关注的是栈空间的变化。
当
main函数开始执行时,系统会为main函数开辟一片函数栈空间,当程序走到int i时,main函数的栈空间就会为变量 i 分配 4 字节大小的空间。调用change函数时,系统会为change函数重新分配一片新的函数栈空间,并为形参变量 j 分配 4 字节大小的空间。在调用
change(i)时,实际上是将 i 的值赋值给 j,我们把这种效果称为值传递(C 语言的函数调用均为值传递)。因此,当我们在
change函数的函数栈空间内修改变量 j 的值后,
change函数执行结束,其栈空间就会被释放,j 就不再存在,i 的值不会改变
❓ 那么难道子函数就修改
main函数内某个变量的值了吗 答案是可以的,这就需要到指针了!【例2】:在子函数中修改
main函数中某个变量的值
#include <stdio.h>
void change(int *j){ //形参位置的 * 意思是声明指针变量
*j = 5; //这里的 * 是取值运算符,按传过来的地址取值
}
//指针的传递
int main() {
int i = 10;
printf("before change i=%d\n",i);
change(&i); //传递变量 i 的地址
printf("after change i=%d\n",i);
return 0;
}我们可以看到程序执行后,语句
printf("after change i=%d\n",i);打印的 i 的值为 5,难道 C 语言函数调用值传递的原理变了❓ 并非,我们这里是将变量 i 的地址传递
给
change函数,也就是实参位置写的&i, 实际效果是j=&i,依然是值传递,只是这时我们的 j 是一个指针变量,内部存储的是变量 i 的地址,所以通过
*j就间接访问到了与变量 i 相同的区域,通过
*j=5就实现了对变量 i 的值的改变。通过单步调试,我们依然可以看到变量 j 自身的地址是与变量 i 的地址不相等的。
6.3.2 指针の偏移
前面介绍了指针的传递。指针即地址,就像我们找到了一栋楼,这栋楼的楼号是
B,那么往前就是
A, 往后就是C, 所以应用指针的另一个场景就是对其进行加减,但对指针进行乘除是没有意义的,就像家庭住址乘以 5 没有意义那样。在工作中,
我们把对
指针的加减称为指针的偏移,加就是向后偏移,减就是向前偏移。
📌 说明:关于 a 、*a、 a+1 & *(a+1)
(a 是一个数组名,直接在它前面加一个取值运算符 *)

a: 单独一个数组名,前面说过了,这代表这个数组的起始地址(只不过在上面监视里要是只敲 a 看不出地址,所以只能显式的
&a, 但我们自己要知道a就是起始地址)*a: 根据数组的起始地址,往后取值,取的位置是数组起始地址走一个基类型的大小, 这里说的基类型其实就是
int, 那么基类型的大小其实就是sizeof(int), 也就是 4 字节 。 那么从上图可以得知a 的起始地址是
0x61fe30那么走一个基类型的大小,取值的范围就是0x61fe30 - 0x61fe33这个范围,就可以取到一个整型值
1a+1: 这里就涉及到了数组的偏移,前面说单独一个 a 是数组 a 的起始地址,那么a+1的含义就是基于数组的起始地址加一个基类型的大小,也就是
0x61fe30 + 4 => 0x61fe34,那么一般地,
a+i的含义就是 数组的起始地址加i个基类型的大小*(a+1): 前面说取值运算符会往后取一个基类型大小的空间,那么这里a+1知道是0x61fe34,那么
*(a+1)取的范围就是0x61fe34 - 0x61fe37这个范围对应的值,那么取到的就是一个整型值
2。再补充一句:*(a+1)等价于写a[1], 底层编译后的汇编代码其实是一样的。那么我们可以延伸
*(a+i)其实就等价于a[i]
- 【例】:指针偏移使用场景
#include <stdio.h>
//复习使用符号常量
#define N 5
//指针的偏移
int main() {
int a[N] = {1,2,3,4,5};
int *p;
int i;
p = a; //保证等号两边的数值类型一致
for(i=0;i<N;i++){ //正序输出
printf("%3d",*(p+i));
}
printf("\n---------------\n"); //division
p = &a[4]; //让p指向最后一个元素
for(i=0;i<N;i++){
printf("%3d",*(p-i));
}
printf("\n");
return 0;
}- 运行效果:

6.3.3 指针&数组传递
在前面
5.4小节已经讲过了数组传递,那么在那节我们就知道,通过形参传过去的地址,或者说指针,它存储的是数组的首地址,它在 64 位程序中永远就是 8 个字节,无法从此得到数组的长度。那么在
5.4的时候我们通过传了个len参数来告诉子函数对应数组的大小。那么我们学过了取地址运算符和取值运算符,也可以再次重复一下那个例子:
#include <stdio.h>
void change(char *d){ //前面使用 d[] 这种形式,但其实和 *d 是等价的 ,都是声明参数是指针
*d = 'H';
d[1] = 'E';
*(d+2) = 'L';
}
int main() {
char c[10] = "hello";
change(c); //数组名存储的是数组起始地址
puts(c); // output : HELlo
return 0;
}6.4 malloc 动态内存申请
在前面我们学习过 C 语言的数组后,相信不少人都会觉得数组长度固定很不方便,
其实 C 语言的数组长度固定是因为其定义的 整型、浮点型、字符型、数组变量都在
栈空间中,而栈空间的大小在编译时是确定的。如果使用的空间大小不确定,那么就要使用
堆空间, 请看下面示例:
- 【例1】 动态内存申请:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int size; //size代表我们要申请多大字节的空间
char *p; //void*类型的指针是不能偏移的,因此不会定义无类型指针
scanf("%d",&size); //输入要申请的堆空间大小
//malloc 返回的是 void* 无类型指针
p = (char*)malloc(size);
strcpy(p,"malloc success");
puts(p);
free(p); //释放申请的堆空间
printf("free success\n");
return 0;
}📌 说明:
1).
malloc函数需要导入头文件#include <stdlib.h>2). 对于
malloc函数void* malloc(size_t size)需要传递一个整型变量指定申请的堆空间的大小,返回值为
void*无类型指针。3).
void*类型的指针只能用来存储一个地址而不能偏移,因为malloc向操作系统申请一片堆空间的地址,它并不知道我们申请的空间用来存放什么
类型的数据,所以确定要用来存储什么类型的数据后,都会将
void*强制类型转换为对应的类型,如上例我们转换为
char*4). 需要注意:指针本身大小,和其指向的空间大小,是两码事!!!
如下图所示:定义的整型变量 i 、指针变量 p 均在 main 函数的栈空间中,
通过 malloc 申请的空间会返回一个堆空间的首地址,我们把首地址存入
变量 p 。知道了首地址,就可以通过 strcpy 函数往对应空间存储字符数据

