C语言笔记
1.数据的类型和输入输出
数据类型-常量-变量(整型-浮点-字符)
- 数据类型:
- 常量
值不能改变
分为整型(int),浮点型(float),字符型(char),字符串型(char)
数据类型 | 分类 | 定义/表示方式 | 占用空间 | 示例 | 注意事项 |
---|---|---|---|---|---|
浮点型数据 | 浮点型常量 | - 小数形式:直接写小数 - 指数形式:aeb (a×10^b) | — | 0.125 、3e-3 (即 0.003) | <font style="color:#DF2A3F;">e</font> 或 <font style="color:#DF2A3F;">E</font> 前必须有数字,后面指数必须是整数。 正确示例:1e3 、1.8e-3 、-123e-6 、-1e-3 错误示例:e3 、2.1e3.5 、.e3 、e |
浮点型变量 | 使用 float 定义 | 4 字节 | float f = 3.14; | float 精度有限,如需更高精度可用 double | |
字符型数据 | 字符型常量 | 单引号括起来的单个字符 | 1 字节 | 'a' 、'A' 、'1' 、' ' | 错误示例:'abc' 、"a" 、" " (多字符或字符串) |
转义字符 | 以 \ 开头的特殊字符 | 1 字节 | \n 换行、\b 退格、\\ 反斜杠 | 常用于控制输出格式 | |
字符型变量 | 使用 char 定义 | 1 字节 | char c = 'x'; | 实际存放的是字符的 ASCII码 |
整型之符号常量
#include <stdio.h>
#define PI 3+2
int main(){int i=PI*2; printf("i=%d\n",i); //printf用于输出printf("i size = %d\n",sizeof(i)); //sizeof可以用来计算某个变量的空间大小return 0;
}输出:
7
因为符号变量PI是直接替换的效果,因此会变成int i = 3+2*2
字符型数据之字符型常量
用单引号括起来的一个字符是字符型常量,且只能包含一个字符,如’a’,‘A’
一个字符常量存到一个字符型变量中,实际上并不是把该字符的字型存到内存中,而是把该字符的ASCII码值放到存储单元中
大写字母A是65,小写字母a是97
// 大写变小写char c='A';printf("%c\n",c+32); //以字符形式输出,aprintf("%d\n",c); //以数值形式输出,65
字符型数据之字符串型常量
由一对双引号括起来
以字符’\0’作为字符串结束标志
- 变量
变量代表内存中具有特定属性的一个存储单元,值可以改变
由编译系统为每个变量名分配对应的内存地址(就是空间)
变量命名:C语言规定标识符只能由字母,数字,下划线组成,且第一个字符必须为字母或下划线
区分大小写
变量名不能与关键字同名
混合运算
- 强制类型转换
#include <stdio.h>
int main(){int i=5;float j=i/2;float k=(float)i/2; //这里做的是整型运算,因为左右操作数都是整型printf("%f\n",j);printf("%f\n",k);return 0;
}输出:
2.000000
2.500000
在 C 语言中,类型转换遵循“由低精度向高精度”原则:
- 所有
char
和short
在表达式计算时会先自动提升为int
; - 之后按精度从低到高依次是:
int → long → float → double → long double
; - 在混合运算时,类型会自动向精度更高的方向转换;
- 如果使用强制类型转换
(type)
,则表达式结果会临时变为指定类型,可能发生截断或精度丢失。
** char/short → int → long → float → double → long double **
- printf
printf函数将这些类型的数据格式化为字符串后,放入标准输出缓冲区,然后将结果显示到屏幕上
I/O即input/output
格式化输出:
- 格式化输出的基本概念
printf
可以在输出字符串时,把变量的值插入到字符串里。
格式:
printf("格式控制字符串", 参数1, 参数2, ...);
格式控制字符串:普通文字 + 特殊的格式符号(占位符)。
参数:依次替换这些占位符,按顺序匹配
- 示例讲解
int age = 21;
printf("Hello %s, you are %d years old\n", "Bob", age);
"Hello %s, you are %d years old\n"
是格式控制字符串
- `%s` → 输出字符串 `"Bob"`
- `%d` → 输出整数 `21`
- `\n` → 换行
最终输出:
Hello Bob, you are 21 years old
- 常用格式控制符(图里表格)
占位符 | 含义 | 示例 |
---|---|---|
%c | 输出单个字符 | 'A' → A |
%d | 输出带符号十进制整数 | -123 → -123 |
%f | 输出浮点数(默认 6 位小数) | 3.14 → 3.140000 |
%s | 输出字符串 | "Hello" → Hello |
%u | 输出无符号十进制整数 | 3000000000U → 3000000000 |
%x | 输出无符号十六进制整数(小写字母) | 255 → ff |
%X | 输出无符号十六进制整数(大写字母) | 255 → FF |
%p | 输出指针(地址) | 0x7ffeefbff5c0 |
- 常见扩展用法
- 限制小数位数
printf("%.2f", 3.14159); // 输出 3.14
- 设置输出宽度
printf("%5d", 42); // 输出 " 42"(右对齐,占5位)
printf("%-5d", 42); // 输出 "42 "(左对齐)
- 组合
printf("%8.3f", 3.14159); // 占8位宽,小数保留3位 → " 3.142"
int i=10;
float f=96.3;
printf("student number=%3d score=%5.2f\n",i,f);
printf("student number=%-3d score=%5.2f\n",i,f);int age=19;
printf("hello %s ,you are %d years old\n","zxy",age);
输出:
student number= 10 score=96.30
student number=10 score=96.30
hello zxy ,you are 19 years old
整形进制转换
一字节(byte)为8位(bit),1位即二进制的1位,它存储0或1
1K=1024字节
1Mb=1024KB
1GB=1024MB
CPU采取了小端方式进行数据存储,即低位在前,高位在后
十进制 0-9
八进制 0-7
十六进制 0-9 a-f
二进制 0000 0001
- 给定的二进制数
0100 1100 0011 0001 0101 0110 1111 1110
这是一个 32 位的二进制数(前面可能省略了 0)。
- 最低位(最右边)是 2 的 0 次方;
- 最高位(最左边)是 2 的 31 次方;
- 若最高位为 1,则代表负数(补码形式)。这里最高位是 0,说明是正数。
- 转换成 八进制
规则:二进制每 3 位分一组,转换为 1 位八进制。
步骤:
- 将上面的二进制从右到左,每 3 位分组:
010 011 000 011 000 101 011 011 111 110
- 每 3 位换算成八进制 0~7 的数:
010 = 2, 011 = 3, 000 = 0, 011 = 3, 000 = 0, 101 = 5, 011 = 3, 011 = 3, 111 = 7, 110 = 6
- 得到八进制数:
011414253376
(前面加 0,是为了保证完整性。)
- 转换成 十六进制
规则:二进制每 4 位分一组,转换为 1 位十六进制。
步骤:
- 将二进制分组:
0100 1100 0011 0001 0101 0110 1111 1110
- 每 4 位换算:
0100 = 4
1100 = C
0011 = 3
0001 = 1
0101 = 5
0110 = 6
1111 = F
1110 = E
- 得到十六进制数:
0x4C3156FE
- 转换成 十进制
规则:按权展开,每位上的二进制数 × 对应的 2 的幂次。
即:
010011000011000101011011111110
经过计算,结果为:
1278301950
#include <stdio.h>
int main(){int i=123;printf("%d\n",i); //十进制输出printf("%o\n",i); //八进制printf("%x\n",i); //十六进制return 0;
}输出:
123
173
7b
scanf读取标准输入
- scanf输入规则
scanf用来读取标准输入,scanf把标准输入内的内容,需要放到某个变量空间里,因此变量必须取地址&
scanf会阻塞,是因为输入缓冲区是空的
scanf("%d",&i);
printf("i=%d\n",i);char c;
scanf("%c\n",&c); // <- 这里出问题
printf("%c\n",c);输出:
12
i=12
1
现象复现(两种常见输入)
- 情形A:你在一行里输入
123 a
然后回车%d
读走123
,停止在空格;缓冲区还剩" a\n"
%c
直接读下一个字符——那就是空格' '
;c=' '
,printf
打印空格,你几乎看不见- 格式串中的
\n
(见下文)会继续吞掉后面的空白(含回车)并等待下一个非空白;所以程序像“卡住”,直到你再键入一个非空白字符
- 情形B:你在两行输入:第一行
123
回车;第二行a
回车%d
读走123
;第一行的回车\n
留在缓冲区%c
立刻把这个残留的\n
当字符读进c
;打印看起来像“啥都没输出”- 末尾的
\n
又会等待下一个非空白,导致继续“等输入”
小结:要么
c
读到了空格/回车(你看不见),要么被**末尾的 **\n
搞得阻塞。
根本原理:scanf
的三条关键规则
- 数字类转换(%d、%f…)会跳过前导空白
它先丢掉空白,再从第一个数字开始读,遇到第一个不匹配的字符就停下,那个不匹配字符会留在输入缓冲区。 %c
、%[...]
、%n
** 不会跳过空白**
%c
是“原样读一个字节”,下一个是什么就读什么(可能是空格/回车)。- 格式串里的“空白”含义(包括空格和
\n
)
在scanf
的格式串中,任意空白字符(空格、\t
、\n
)都表示:跳过任意数量(0 个或更多)的空白输入。- 若空白在中间:只是帮你把接下来的空白都吃掉
- 若空白在格式串末尾(如你写的
"%c\n"
):它会一直吞空白,并等待直到看到一个非空白字符才算这次调用结束,于是看起来像卡住 - 这也是为什么把
\n
放在末尾很容易出问题的原因
为什么看起来“没输出 c”
- 第一:
%d
后的换行(或空格)常常被%c
读走,c
其实是空白字符,肉眼看不到 - 第二:
"%c\n"
的末尾\n
会继续吃空白并等待非空白,导致阻塞,你误以为没打印
正确写法(任选其一)
① 在 %c
前加一个空格(最简单、最推荐)
scanf(" %c", &c); // 注意前面的空格
含义:先把所有空白(空格/回车/Tab)都跳过,再读一个真实字符给 c
。
② 手动清空到行尾(在读 %c
前清掉残留回车)
scanf("%d",&i);
int ch; while ((ch = getchar()) != '\n' && ch != EOF) { } // 清掉本行剩余的字符(含回车)
scanf("%c",&c);
③ 先读整行,再解析(更可控,适合复杂输入)
char line[128];
fgets(line, sizeof(line), stdin); // 把一行读进来
sscanf(line, "%d", &i);fgets(line, sizeof(line), stdin); // 再读下一行
sscanf(line, " %c", &c); // 这里仍建议在 %c 前放空格
常见误区
- 不要在
scanf
里把\n
放在末尾(如"%c\n"
):极易导致“等待非空白”而阻塞 - 不要用
fflush(stdin)
清输入:这是未定义行为(MSVC 的扩展除外),跨平台不可靠 - 记住:
%c
不会跳过空白;想跳过就在格式串里显式写一个空格(" %c"
)
“数字转换会跳过前导空白,
%c
不会;要跳过,就写" %c"
;不要在scanf
的末尾写\n
。”
- 多种类型混合输入
#include <stdio.h>
int main(){int i,ret;float f;char c;ret=scanf("%d %c%f",&i,&c,&f);printf("i=%d,c=%c,f=%5.2f\n",i,c,f);
}输出:
1 c 12.8
i=1,c=c,f=12.80
OJ网站的使用
oj是online judge的缩写,也就是在线判题,以下为链接
OnlineJudge
2.运算符与表达式
算术运算符与关系运算符
- 算术运算符
#include <stdio.h>
int main(){int result=4+5*2-6/3+10%4;printf("result=%d\n",result);
}输出:
result=14
- 关系运算符
#include <stdio.h>
int main(){int a;while (scanf("%d",&a)){if (a>3&&a<10) {printf("a在3和10中间\n");}else{printf("a不在3和10中间\n");}}
}
逻辑运算符与赋值运算符,求字节运算符
逻辑运算符
#include <stdio.h>
int main(){int i=0,j=1;while (scanf("%d",&i)){if (i%4==0 && i%100!=0 || i%400==0){printf("i is leap year\n");}else{printf("i is not leap year\n");}}i=!!j;printf("i value=%d\n",i);
}输出:
m
i value=1
短路运算:
当i为假时,不会执行逻辑与后面的表达式,称为短路运算
#include <stdio.h>
int main(){// 逻辑与和逻辑或的运算int i=1;i&&printf("you can see me\n");// 前面的值为真,则日志不运行int j=0;j||printf("you can see me\n");
}输出:
you can see me
you can see me
- 逻辑与
&&
的短路规则
- 表达式:
expr1 && expr2
- 计算顺序:
- 先算
expr1
。 - 如果
expr1
为 假(0),那结果一定是 假,不会再算expr2
(直接“短路”)。 - 如果
expr1
为 真(非0),才会去算expr2
,结果取决于expr2
。
- 先算
所以:
0 && anything
→ 直接是 0,不计算anything
。1 && anything
→ 结果等于anything
的真假。
- 逻辑或
||
的短路规则
- 表达式:
expr1 || expr2
- 计算顺序:
- 先算
expr1
。 - 如果
expr1
为 真(非0),结果一定是真,expr2
根本不会计算。 - 如果
expr1
为 假(0),才会算expr2
。
- 先算
所以:
1 || anything
→ 直接是 1,不计算anything
。0 || anything
→ 结果等于anything
的真假。
- 总结
短路运算的意义:
- 提高效率(不用多算)。
- 避免错误(常用于防止非法操作)。
例如:
if (p != NULL && p->value == 10) { ... }
如果 p == NULL
,第一个条件为假,第二个 p->value
不会执行,就不会导致程序崩溃。
赋值运算符
也就是"="
等号左边放变量
符合赋值运算符:+=,-=,*=,/=
求字节运算符-----sizeof
用于求常量或变量所占用的空间大小
int i=0;
printf("i size is %d\n",sizeof(i)); 输出:
i size is 4
算术运算符优先级高于关系运算符,关系运算符优先级高于逻辑与和逻辑或运算符
- 单目运算符:只需要一个操作数,比如
!a
,-a
,++a
,--a
。 - 双目运算符:需要两个操作数,比如
a + b
,a && b
。 - 三目运算符:需要三个操作数,比如
a > b ? a : b
。
3.选择+循环
选择if-else
5>3&&8<1-!0
if ----else if----else循环
#include <stdio.h>int main() {int number; // 输入的数量double cost; // 单价double total; // 总价// 输入部分printf("请输入购买数量:");scanf("%d", &number);// 判断逻辑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.0;}// 计算总价total = number * cost;// 输出部分printf("购买数量: %d\n", number);printf("总价: %.3f 元\n", total);return 0;
}
可嵌套:
循环while,for,continue,break
- while循环
先判断表达式,后执行语句
#include <stdio.h>
// 计算1到100的和
int main(){int i=1,total=0;while (i<=100){total+=i;i++;}printf("total=%d\n",total);
}
辨析:
写法 | 表达式返回值 | 自增发生时间 |
---|---|---|
i++ | 自增前的值 | 先返回,再加 1 |
++i | 自增后的值 | 先加 1,再返回 |
- for循环
#include <stdio.h>
int main(){int i,total=0;for (int i = 0; i <= 100; i++){total+=i;}printf("total=%d\n",total);
}
- continue语句
跳过本次循环
#include <stdio.h>
// 求1-100偶数和
int main(){int i,total;total=0;for (int i = 0; i <= 100; i++){if (i%2==0){continue;}total+=i;}printf("total=%d\n",total);
}
while循环慎用continue,小心死循环
- break语句
#include <stdio.h>
int main(){int i,total;for (int i = 0; i <= 100; i++){if (total>2000){break;}total+=i;}printf("total=%d\n",total);
}
- 循环嵌套
最常见的是for循环嵌套for循环
#include <stdio.h>int main() {int i, j;// 外层循环控制行for (i = 1; i <= 9; i++) {// 内层循环控制列for (j = 1; j <= i; j++) {printf("%d×%d=%-3d ", j, i, i * j); // %-3d 表示结果左对齐,占 3 个宽度,保证整齐}printf("\n"); // 换行}return 0;
}
打印这个图:
#include <stdio.h>int main() {int i, j;// 控制行数(5 行)for (i = 1; i <= 5; i++) {// 每一行打印 i 个 *for (j = 1; j <= i; j++) {printf("*");}printf("\n"); // 换行}return 0;
}
4.一维数组和字符数组
一维数组
数组,是指一组具有相同数据类型的数据的有序集合
int a[10]={0}; //所有元素值为0
一维数组int mark[100],在内存中的存放情况如图所示,都是整型元素,占用4字节,数组的引用方式是"数组名[下标]",所以访问数组mark中的元素是mark[0],mark[1]…
初始化方法:
int a[10]={0,1,2,3,4,5,6,7,8,9};
int a[10]={0,1,2,3,4}; //只给前五个元素赋值,后五个元素值为0
数组访问越界与数组的传递
- 数组越界
#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);
}输出:
i=7
- C 语言本身不检查边界,所以编译器不会报错。
- 运行时也不会报错(不像 Java、Python 那样会抛异常)。
- 如果越界访问的地址还在当前进程能访问的内存范围内,就不会崩溃,但数据可能被篡改。
- 如果越界到非法内存地址,才会出现段错误 (Segmentation fault)。
因为 a[5]
、a[6]
已经超出数组定义的范围,它们写到的地址就有可能覆盖 i
或 j
的内存,也可能覆盖别的内容。这就是未定义行为
程序可能看起来「正常运行」,也可能「数据错乱」,甚至「直接崩溃」
- 数组的传递
经典的错误:
void print(int a[5]) {for (int i = 0; i < sizeof(a)/sizeof(int); i++) {printf("%d\n", a[i]);}
}
看似在遍历 5 个元素,其实只会遍历 1 或 2 个,输出不完整。
看起来 a[5]
是长度为 5 的数组,但在函数参数里,数组会退化(decay)成指针,即 int *a
。
void f(int a[5]) {sizeof(a); // 实际是 sizeof(int*),因为 a 是指针
}
C 语言规定:在大多数表达式中,数组名会自动转换为指向其首元素的指针。
所以:
sizeof(a)
得到的是 指针的大小(在 32 位系统是 4 字节,在 64 位系统是 8 字节),而不是整个数组的大小。sizeof(int)
一般是 4。- 所以
sizeof(a)/sizeof(int)
在 64 位系统下是8/4 = 2
,循环只会跑 2 次。
在 32 位系统下是4/4 = 1
,只会跑 1 次。
这就导致实际输出的个数不对。
正确写法:
#include <stdio.h>
// 子函数把某一个常用功能,封装起来的作用
// 数组名传递到子函数后,子函数的形参接收到的就是数组的起始地址,因此不能把数组的长度传递给子函数
void print(int a[], int length){for (int i = 0; i < length; i++){printf("%d\n",a[i]);}
}int main(){int a[5] = {1,2,3,4,5};// 想知道数组的长度,必须自己传进去print(a, 5);return 0;
}
字符数组与scanf读取字符串
- 字符数组
#include <stdio.h>
int main(){char c[3]={'I','a','d'}; //没有结束符,输出会乱码char b[4]="Iad";printf("%s",b);
}
输出字符串乱码时,要去看字符串数组中是否存储了结束符’\0’
字符数组初始化和传递
#include <stdio.h>
// 模拟printf("%s",c)操作
void print(char d[]){int i=0;while (d[i]) //当走到结束符时,循环结束{printf("%c",d[i]);i++;}printf("\n");
}int main(){char c[6]="hello";char d[5]="how";printf("%s\n",c);print(d);
}
- scanf读取字符串
#include <stdio.h>
int main(){char c[10];char d[10];// scanf会自动往字符数组中放结束符scanf("%s",c); //字符数组名c中存储了数组的起始地址,故无需加&printf("%s\n",c);scanf("%s%s",c,d);printf("c=%s,d=%s\n",c,d);
}
scanf在使用%s读取字符串时,还会忽略空格和回车,上述程序
输入:how are
输出:how
那如果想一次性读取到how are怎么办?看下一节
fgets与puts讲解,strlen-strcmp-st
1. gets
函数
gets
用于从标准输入读取一行字符串。它的原型如下:
char *gets(char *str);
- 功能:
**gets**
** 从标准输入读取一行字符,直到遇到换行符(**\n**
)或者文件结束符(EOF)**。然后将该字符串存储在传入的字符数组中,换行符会被替换为字符串的终止符(\0
)。 - 缺点:
gets
不会检查输入字符串的长度,因此如果输入的字符串超出了预分配的内存空间,可能会导致缓冲区溢出,这是非常危险的,会引发程序崩溃或安全漏洞。
char str[100];
gets(str); // 从用户输入读取字符串
建议:由于 <font style="color:#DF2A3F;">gets</font>
存在严重的安全隐患(缓冲区溢出),它已经被 C11 标准废弃,应该避免使用。可以使用 <font style="color:#DF2A3F;">fgets</font>
来代替。
2. puts
函数
puts
用于向标准输出打印一个字符串,并自动在字符串末尾加上换行符。它的原型如下:
int puts(const char *str);
等价于:
printf("%s\n",c);
- 功能:
puts
打印传入的字符串,并在末尾自动添加一个换行符,表示输出结束。 - 返回值:如果输出成功,
puts
返回一个非负值;如果发生错误,返回EOF
。
puts("Hello, World!"); // 输出:Hello, World!
fgets
函数
char *fgets(char *str, int n, FILE *stream);
参数:
str
:指向存储读取内容的字符数组的指针。读取的数据将存储在这个数组中。n
:指定最大读取字符数(包括末尾的\0
),即最多读取n-1
个字符。确保数组空间不会溢出。stream
:指定输入流,通常是stdin
(标准输入),但也可以是其他文件流。
返回值:
- 成功时:返回
str
(指向字符数组的指针),即返回读取到的字符串。 - 失败时:返回
NULL
,通常是由于读取错误或遇到文件结束(EOF)。
- 避免缓冲区溢出:
<font style="color:#DF2A3F;">fgets</font>
读取字符时会保证最多读取<font style="color:#DF2A3F;">n-1</font>
个字符,且自动在末尾添加字符串终止符’\0’。这避免了像<font style="color:#DF2A3F;">gets</font>
函数那样引起的溢出问题。 - 读取换行符:如果用户在输入时按下回车(即换行符),
fgets
会将换行符(\n
)包括在字符串中,除非读取的字符数达到了n-1
,此时换行符会被丢弃。 - 可以读取空行:与
scanf
不同,fgets
允许读取空行,即用户直接按回车时,返回一个仅包含换行符和终止符的字符串。
#include <stdio.h>int main() {char str[100];// 使用 fgets 读取一行输入,最多读取 99 个字符,保留 1 个字符位置给 '\0'printf("请输入一行文字: ");if (fgets(str, sizeof(str), stdin) != NULL) {printf("你输入的是: %s\n", str);} else {printf("读取输入失败!\n");}return 0;
}
请输入一行文字: Hello, World!
你输入的是: Hello, World!
需要注意的几点:
- 换行符的处理:
// 去掉字符串末尾的换行符
str[strcspn(str, "\n")] = '\0';
- 如果用户输入了换行符,`fgets` 会将其保留在字符串中。如果不希望字符串包含换行符,可以在处理字符串后去掉它。
- 最大字符数:
fgets
读取的字符数最多是n-1
,即数组的大小减 1,这样才能为字符串的结束符\0
留出空间。
- 文件结束:
- 如果
fgets
读取到文件的结束标志(EOF),它将返回NULL
,表示没有更多数据可供读取。
- 如果
str系列
strcmp
的规则复习
strcmp(a,b)
会逐字符比较:- 如果第一个不相等的字符
a[i] < b[i]
,返回 负数 - 如果
a[i] > b[i]
,返回 正数 - 如果完全相同,返回 0
- 如果第一个不相等的字符
#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";char e[200];gets(c);puts(c);len=strlen(c);printf("len=%d\n",len);len=mystrlen(c);printf("my len=%d\n",len);strcat(c,d); //把d中的字符串拼接到c中puts(c);strcpy(d,c); //把c中的字符串复制给dputs(d);strcpy(e,c);puts(e);//用ASCII码去比较。c和e相等输出0printf("c?d=%d\n",strcmp(c,e));// c大于"how",返回是正值,反之返回负值puts(c);printf("c?d=%d\n",strcmp(c,"tow"));return 0;
}输入:a
输出:
a
len=1
my len=1
aworld
aworld
aworld
c?d=0
aworld
c?d=-1
最后一个值是 -1 的原因是 **"a"**
的 ASCII 码比 **"t"**
小,strcmp 返回负数,刚好实现返回了 -1。
5.指针
指针的本质(间接访问原理)
通俗的讲,指针就是地址,*
是取值,&
是取址
数组名是首地址,arr[i]
等于*(arr+i)
我们常说的指针其实是指针变量,也就是存储地址的变量
#include <stdio.h>
int main(){int i=5;char c='a';int *i_pointer=&i; //定义指针变量i_pointer,并初始化为i的地址char *c_pointer;c_pointer=&c;*i_pointer=10; //间接访问---通过指针修改 iprintf("i=%d\n",i); //直接访问----直接使用 i
}
直接访问 就是通过 变量名 来访问内存里的值。
间接访问 就是通过 指针(保存的地址)去访问/修改变量。
指针变量大小:
注意事项:
指针的传递使用场景
指针使用场景分为传递和偏移
C语言的函数参数传递,永远是值传递。
但如果传递的是“指针变量”,那么它拷贝的是地址值。
#include <stdio.h>void change(int *x) { //等价于x=&a*x = 20; // 通过间接访问拿到了变量a的地址,修改x指向的变量
}int main() {int a = 10;change(&a); // 把a的地址传进去,实参是&aprintf("%d\n", a); // 输出20return 0;
}
&a
把 a 的地址传递给函数change
change
内部*x = 20
,相当于直接修改了a
指针的偏移使用场景
- 偏移
指针可以进行加减运算,单位不是字节,而是指向类型的大小。
单位是“类型大小”,p+1
表示跳到下一个元素
int arr[5] = {10, 20, 30, 40, 50}; //数组名内存储了数组的起始地址,arr中存储的就是一个地址值
int *p = arr; // 定义指针变量p,p指向arr[0]printf("%d\n", *p); // 10
printf("%d\n", *(p+1)); // 20
printf("%d\n", *(p+2)); // 30// 让p直接指向数组末尾
p=&arr[4];
printf("%d\n",*p);
- 偏移的长度是其基类型的长度,也就是偏移sizeof(int),这样通过*(p+1)就可以得到元素a[1]。
<font style="color:#DF2A3F;">p+1</font>
不是地址+1字节,而是地址+<font style="color:#DF2A3F;">sizeof(int)</font>
字节
如果int
是4字节,那么p+1
= 原地址+4 - 同理,
p+2
= 原地址+8
数组取下标的操作正是通过指针偏移实现的
- 指针与一维数组
#include <stdio.h>
void change(char *d){*d='H';d[1]='E';*(d+2)='L';
}
int main(){char c[10]="hello";change(c);puts(c);return 0;
}
数组名作为实参传递给子函数时,是弱化为指针的
指针与malloc动态内存申请,栈与堆的差异
C语言的数组长度固定是因为其定义的整型,浮点型,字符型变量都在栈空间中,而栈空间的大小在编译时是确定的。如果使用的空间大小不确定,那么就要使用堆空间
// 动态内存申请----malloc
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){int i; //代表我们要申请多大字节的空间char *p; //void*类型的指针不能偏移的,因此不会定义无类型指针scanf("%d",&i); //输入要申请的空间大小// malloc返回的void*代表无类型指针,要强转成charp=(char*)malloc(i); //使用malloc动态申请堆空间strcpy(p,"malloc success");puts(p);free(p); //free时必须使用malloc申请时返回的指针值,不能进行任何偏移 printf("free success\n");return 0;
}输入:100
输出:
malloc success
free success
注意!指针本身大小和其指向空间的大小,是两码事
6.函数
函数的声明与定义-嵌套调用
- 函数的声明与定义
举个例子:
//func.h
#include <stdio.h>
#include <stdlib.h>
int printstar(int i); //函数声明
void print_message();//func.c
#include "func.h"
int printstar(int i){ //i为形式参数printf("***********\n");printf("printstar %d\n",i);return i+3;
}
void print_message(){ //可以调用pirntstarprintf("how do you do\n");printstar(3);
}// main.c
// ""是指在当前目录下找头文件
#include "func.h"
int main(){int a=10;a=printstar(a);print_message();printstar(a);return 0;
}
运行:
gcc main.c func.c -o main
.\main.exe
函数声明与定义的差异:
整合成一个
#include <stdio.h>
#include <stdlib.h>// 函数声明
int printstar(int i);
void print_message();// 函数定义
int printstar(int i) { // i 为形式参数printf("***********\n");printf("printstar %d\n", i);return i + 3;
}void print_message() { // 可以调用 printstarprintf("how do you do\n");printstar(3);
}// 主函数
int main() {int a = 10;a = printstar(a); // 调用 printstar(10),返回 13print_message(); // 打印 how do you do,并调用 printstar(3)printstar(a); // 调用 printstar(13)return 0;
}
- 函数的分类与调用
函数的递归调用
函数自身调用自身的操作,称为递归函数。递归函数一定要有结束条件,否则会产生死循环
核心就是找公式:f(n)=n*f(n-1)
5!=5*4!
举个简单的例子:
#include <stdio.h>int f(int n){//一定要有结束条件,一定在return之前if (1==n){return 1;}return n*f(n-1);}
int main(){int n;scanf("%d",&n);printf("f(%d)=%d\n",n,f(n));
}
上台阶问题: 找公式:step(n)=step(n-1)+step(n-2)
总共有 5 级台阶,每次只能上 1 阶或者 2 阶,问有多少种走法。
#include <stdio.h>// 递归函数
int climb(int n) {if (n == 1) return 1; // 只有一级台阶 → 一种走法if (n == 2) return 2; // 两级台阶 → 两种走法return climb(n-1) + climb(n-2); // 递归公式
}int main() {int n = 5;printf("一共有 %d 种走法\n", climb(n));return 0;
}
局部变量与全局变量
#include <stdio.h>
// 全局变量,不建议使用
int i=10;
void pirnt(int a){printf("pirnt i=%d\n",i);
}int main(){{// 局部变量有效范围是离自己最近的花括号int j=5;}printf("main i=%d\n",i);i=5;printf("%d",i);return 0;
}
形参与实参:
局部变量与外部变量
7.结构体以及C++引用讲解
结构体-结构体数组-结构体对齐
- 结构体
C语言提供结构体来管理不同类型的数据组合
#include <stdio.h>
struct student
{int num;char name[10];char sex;int age;float score;
}; //结构体类型声明,注意最后一定要加分号
struct student s1,s2; //定义多个结构体变量int main(){// 初始化struct student s={1001,"zxy",'M',20,98};
}
- 结构体数组
#include <stdio.h>
struct student
{int num;char name[10];char sex;int age;float score;
}; //结构体类型声明,注意最后一定要加分号
struct student s1,s2; //定义多个结构体变量int main(){// 初始化struct student s={1001,"zxy",'M',20,98};struct student sarr[3]; //定义一个结构体数组变量int i;//结构体输出必须单独取访问内部的每个成员s.age=122;printf("%d %s %c %d %f\n",s.num,s.name,s.sex,s.age,s.score);printf("--------------\n");scanf("%d %s %c %d %f",&s.num,s.name,&s.sex,&s.age,&s.score);printf("%d %s %c %d %f\n",s.num,s.name,s.sex,s.age,s.score); //读取内容后在输出printf("--------------\n");// 把 s 赋值到结构体数组 sarr[0]i=0;sarr[i]=s;// 输出数组中的第一个结构体printf("%d %s %c %d %f\n",sarr[i].num,sarr[i].name,sarr[i].sex,sarr[i].age,sarr[i].score);
}
- 结构体对齐
#include <stdio.h>
struct student_type1
{double score; //8个字节int height; //4个字节short age; //2个字节
}; //占用16字节大小的空间
struct student_type2
{int height;char sex;short age;
}; //占用8个字节空间大小// 结构体对齐
int main(){struct student_type1 s1={1,2,3};struct student_type2 s2={7,'m',8};printf("s1 size = %d\n",sizeof(s1)); //16printf("s2 size = %d\n",sizeof(s2)); //8return 0;
}
结构体指针与typedef的使用
- 结构体指针的使用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 结构体指针
struct student
{int num;char name[10];char sex;
};int main(){struct student s={1001,"zxy",'M'};struct student sarr[3]={1009,"ylp",'M',1002,"qingwei",'M',1003,"ouyang",'F'};struct student *p; //定义结构体指针变量int num;p=&s;printf("%d %s %c\n",p->num,p->name,p->sex);p=sarr;printf("%d %s %c\n",(*p).num,(*p).name,(*p).sex); //方式一获取成员printf("%d %s %c\n",p->num,p->name,p->sex); //方式二获取成员printf("------------------\n");p+=1;printf("%d %s %c\n",(*p).num,(*p).name,(*p).sex); // 给结构体指针p通过malloc申请空间(一个结构体大小的空间),并对其成员赋值,再访问p=(struct student*)malloc(sizeof(struct student));p->num=100;p->sex='M';strcpy(p->name,"happy");printf("------------------\n");printf("%d %s %c\n",p->num,p->name,p->sex); return 0;
}
- typedef的使用
#include <stdio.h>
// typedef起别名
// 结构体指针
typedef struct student {int num;char name[20];char sex;
} stu, *pstu;// typedef给已有类型起别名
typedef int INTEGER;
int main() {stu s1 = {1001, "Tom", 'M'}; // 用别名 stupstu ps = &s1; // 用别名 pstuINTEGER num=10;printf("%d\n",num);printf("%d %s %c\n", ps->num, ps->name, ps->sex);return 0;
}
C++引用的讲解
文件后缀:.cpp
#include <stdio.h>
void aaa(int &b){ //形参中写&,要称为引用b=b+1;
}
int main(){int a=10;aaa(a);printf("afer mdify a=%d\n",a);
}
- 在 C++ 中,
int &b
表示 b 是一个引用,它和实参a
绑定。 - 所以在函数里对
b
的修改,直接作用到a
。 - 效果相当于 传址调用(call by address),但语法比指针更直观。
等价于
#include <stdio.h>
void aaa(int *b){*b=*b+1;
}
int main(){int a=10;aaa(&a);printf("after modify a=%d\n",a);
}
总结:子函数中修改主函数的变量的值,就用C++的引用
重点!子函数内修改主函数的一级指针变量
#include <stdio.h>// p 是“指针的引用”,q 是普通指针
void modif_pointer(int* &p, int *q){ //引用必须和变量名紧邻p = q; // 改变 p 的指向
}int main(){int *p = NULL; // p 初始化为空指针int i = 10;int *q = &i; // q 指向变量 imodif_pointer(p, q); // 把 p 修改成和 q 一样printf("after modify_pointer *p=%d\n", *p);return 0;
}