1. c语言概述
c语言是计算机编程语言的一种,编程语言用于人和机器交流。
1.1 c语言特点
简洁
c语言的语法简单,语句清晰明了,使得程序易于阅读和理解
高效
c语言的执行效率高,可以用于开发需要高性能的应用程序
可移植
c语言可以在不同硬件平台和操作系统上运行,具有较高的可移植性
模块化
c语言支持函数和结构体等模块化编程方法,使得程序的复杂性得到有效控制。
标准化
c语言的语法和标准库已经被ISO和ANSI标准化,具有广泛的应用和兼容性。
1.2 c语言应用领域
系统软件
嵌入式系统
网络设备
游戏开发
1.3 c语言编译器
GCC
MinGW
Cygwin
MSVC
2. 基础语法
2.1 数据类型
c语言中数据类型有3种,分别为基本数据类型,构造数据类型,指针数据类型
基本类型:
整型: int、short、long
字符型:char
浮点型:float(单精度浮点)、double(双精度浮点)
构造类型:
数组类型
结构类型
联合类型
枚举类型
指针类型:
char * int* int**等
数据类型的作用: 编译器预算数据分配的内存空间大小。
2.2 变量
变量是用来存储数据的一个内存区域,并用一个名字来表示这个区域。
特点:
变量在使用前必须先定义,定义变量前必须要有相应的数据类型;
在程序运行过程中,它的值可以改变。
语法说明:
2.2.1 char类型
char表示为字符类型,用于存储单个字符,每个字符变量都有8个bit位构成,在内存中就是1字节。
2.2.2 布尔类型
布尔类型是一种处理逻辑的类型,有两个值,true和false,在内存中只占用1个字节。
早期c语言没有布尔类型数据,以0代表false,非0代表真。
C99标准定义了新的关键字_Bool,提供了布尔类型,或者也可以使用stdbool.h中的bool
2.3 数据类型的长度
字节:就是计算机存储的最小单位,以字节(byte)为单位。
2.3.1 基本数据类型长度
数据类型的长度会受到操作系统平台的影响。所以在不同平台下基本数据类型的长度是不一样的。
ps:在单片机开发中,int在8位的单片机中长度为2个字节,在32位的单片机中长度为4个字节
2.3.2 可移植类型
- C语言在可移植类型头文件 stdint.h 和 inttype.h 中规定了精确宽度整数类型,以确保C语言的类型在各系统内功能相同。
2.4 常量
与变量不同,常量的值在程序运行时不会改变
2.4.1 自定义常量
常量的定义方式有两种:
// 预处理常量
#define PI 3.14// const常量
const double pi2 = 3.14;
2.5 数值表示
✨2.5.1 进制
十进制 | 二进制 | 八进制 | 十六进制 |
---|---|---|---|
0 | 0 | 0 | 0 |
1 | 1 | 1 | 1 |
2 | 10 | 2 | 2 |
3 | 11 | 3 | 3 |
4 | 100 | 4 | 4 |
5 | 101 | 5 | 5 |
6 | 110 | 6 | 6 |
7 | 111 | 7 | 7 |
8 | 1000 | 10 | 8 |
9 | 1001 | 11 | 9 |
10 | 1010 | 12 | A |
11 | 1011 | 13 | B |
12 | 1100 | 14 | C |
13 | 1101 | 15 | D |
14 | 1110 | 16 | E |
15 | 1111 | 17 | F |
16 | 10000 | 20 | 10 |
2.5.1.1 二进制
二进制用0和1两个数来表示
基数为2,进位规则是逢二进一,借位规则是借一当二
数据在计算机中主要是以补码的形式存储的
十进制转换二进制的方法:
用十进制除以2,分别取余数和商数,商数为0时,余数倒着数就是转化后的结果
除二取余,倒序排列
2.5.1.2 八进制
以8为基数的计数法,采用0,1,2,3,4,5,6,7八个数字,逢八进1
-
八进制的数和二进制数可以按位对应(八进制一位对应二进制三位)
-
十进制转化八进制的方法:
2.5.1.2 十六进制
由0-9,A-F组成,字母不区分大小写
10进制的对应关系是:0-9对应0-9,A-F(或a-f)对应10-15
十六进制的数和二进制数可以按位对应(十六进制一位对应二进制四位)
- 十六进制和二进制互转:
十进制转化十六进制的方法
2.6 c语言表示相应进制数
十进制 | 以正常数字1-9开头,如15 |
---|---|
八进制 | 以数字0开头,如017 |
十六进制 | 以0x或0X开头,如0xf |
二进制 | 以0b或0B开头,如0b1111 |
#include <stdio.h>int main() {// 十进制方式赋值int a = 15;// 八进制方式赋值int b = 017;// 十六进制方式赋值int c = 0xf;// 二进制方式赋值int d = 0b1111;printf("%d, %d, %d, %d\n", a, b, c, d);return 0;
}
// 十进制以正常数字1-0开头 如15;
// 八进制以数字0开头,如 015;
// 十六进制 以0x或0X开头,如 0x0F;
// 二进制以0b或0B开头,如 0b0001;
✨2.7 数值的存储方式
2.7.1 原码
十进制数按照除二取余、倒序排列得到的就是原码
例如:
-
10 -> 0000 1010
-
-10 -> 1000 1010
-
-1 -> 1000 0001
-
1 -> 0000 0001
**问题:**正负数的加法运算,以及零的问题
2.7.2 反码
正数的反码就是原码本身
负数的反码就是按位取反(但符号位不变,符号位就是最左边第一个数字)
例如:
- 1 -> 0000 0001 -> 0000 0001
- -1 -> 1000 0001 -> 1111 1110
0000 0001
+ 1111 1110
-----------------1111 1111
1111 1111 是运算完之后的结果,但要注意,这时还是反码,需要重新返回来:1000 0000 。
反码解决了正负数加法问题,但正负零的问题还是存在。
2.7.3 补码
正数的补码就是原码本身
负数的补码就是在反码的基础上+1;
例如:
- 1 -> 0000 0001 -> 0000 0001 -> 0000 0001
- -1 -> 1000 0001 -> 1111 1110 -> 1111 1111
0000 0001
+ 1111 1111
----------------0000 0000
补码在正负数加减法运算时没问题,也不会出现正负零占两个二进制。但 1000 0000 不表示为负零,计算机默认把8位有符号二进制 1000 0000 表示为 -128 ;
总结:
十进制数转化为二进制就是原码
正数的反码和补码就是原码本身
负数的反码就是按位取反(符号位不变,符号位就是最左边第一个数字)
负数的补码就是在反码的基础上+1
2.8 输入和输出
2.8.1 输出:
- 输出:将程序的运行结果输出到控制台或终端窗口中
语法格式:
#include <stdio.h>int main() {// %d %c %f %s字符串int num = 15;char a = 's';float f = 154.223;short mm = 123123;// %#X 占位符 八进制和十六进制可以加上前缀#printf("%d\n", num);printf("%d\n", sizeof(int));printf("%c\n", a);printf("%p\n", &num);return 0;
}
-
格式化占位符:
下面为总的占位符
-
打印格式 对应数据类型 含义 %c char 字符型,输入的数字按照ASCII码相应转换为对应的字符 %hd short int 短整数 %hu unsigned short 无符号短整数 %d int 接受整数值并将它表示为有符号的十进制整数 %u unsigned int 无符号10进制整数 %ld long 接受长整数值并将它表示为有符号的十进制整数 %f float 单精度浮点数 %lf double 双精度浮点数 %e,%E double 科学计数法表示的数,此处"e"的大小写代表在输出时用的"e"的大小写 %s char * 字符串。输出字符串中的字符直至字符串中的空字符(字符串以’\0‘结尾,这个’\0’即空字符) %p void * 以16进制形式输出指针 %o unsigned int 无符号8进制整数 %x,%X unsigned int 无符号16进制整数,x对应的是abcdef,X对应的是ABCDEF -
打印格式 对应数据类型 含义 %c char 字符型,输入的数字按照ASCII码相应转换为对应的字符 %d int 接受整数值并将它表示为有符号的十进制整数 %f float 单精度浮点数 %lf double 双精度浮点数 %s char * 字符串。输出字符串中的字符直至字符串中的空字符(字符串以’\0‘结尾,这个’\0’即空字符) %p void * 以16进制形式输出指针
只需记住第二张表的几个就行。
2.8.2 输入:
-
输入:接收用户输入的数据的过程
-
scanf语法格式:
#include <stdio.h>int main() {// scanf("格式化字符串", &变量1, &变量2,...);// 输入年龄然后打印// 数据是给定变量的地址int age;printf("请输入你的年龄:");scanf("%d",&age);printf("你的年龄是:%d\n",age); return 0;
}
2.9 运算符:
2.9.1 算术运算符
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
+ | 加 | 10 + 5 | 15 |
- | 减 | 10 - 5 | 5 |
* | 乘 | 10 * 5 | 50 |
/ | 除 | 10 / 5 | 2 |
% | 取模(取余) | 10 % 3 | 1 |
++ | 前自增 | a=2; b=++a; | a=3; b=3; |
++ | 后自增 | a=2; b=a++; | a=3; b=2; |
– | 前自减 | a=2; b=–a; | a=1; b=1; |
– | 后自减 | a=2; b=a–; | a=1; b=2; |
#include <stdio.h>int main() {float m = 5;int n = 2;// 整数类型printf("res = %d\n", m / n);printf("res = %f\n", m / n);// 强烈不建议使用强制类型转换// 前++ 先加再运算int i = 0;++i;int d = ++i;printf("d = %d\n", i);// 后++ 先运算再加int data = 0;int sum = data++;printf("sum = %d\n", sum);printf("data = %d\n", data);return 0;
}
2.9.2 赋值运算符
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
= | 赋值 | a=2; b=3; | a=2; b=3; |
+= | 加等于 | a=0; a+=2;等同于 a = a + 2; | a=2; |
-= | 减等于 | a=5; a-=3;等同于 a = a - 3; | a=2; |
*= | 乘等于 | a=2; a*=2;等同于 a = a * 2; | a=4; |
/= | 除等于 | a=4; a/=2;等同于 a = a / 2; | a=2; |
%= | 模等于 | a=3; a%=2;等同于 a = a % 2; | a=1; |
#include <stdio.h>int main() {int a = 10;int b = a;a += 10;b += 20;printf("%d\n", b);return 0;
}
2.9.3 比较运算符
C 语言的比较运算中, “真”用数字“1”来表示, “假”用数字“0”来表示。
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
== | 相等于 | 4 == 3 | 0 |
!= | 不等于 | 4 != 3 | 1 |
< | 小于 | 4 < 3 | 0 |
> | 大于 | 4 > 3 | 1 |
<= | 小于等于 | 4 <= 3 | 0 |
>= | 大于等于 | 4 >= 1 | 1 |
#include <stdio.h>int main() {// 比较的结果是 0 1int m= 10;int n = 30;printf("m>n = %d\n", m>n);printf("m<n = %d\n", m<n);printf("m>=n = %d\n", m>=n);printf("m<=n = %d\n", m<=n);printf("m!=n = %d\n", m!=n);printf("m==n = %d\n", m==n);return 0;
}
2.9.4 逻辑运算符
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
! | 非 | !a | 如果a为假,则!a为真;如果a为真,则!a为假。 |
&& | 与 | a && b | 如果a和b都为真,则结果为真,否则为假。 |
|| | 或 | a || b | 如果a和b有一个为真,则结果为真,二者都为假时,结果为假。 |
✨✨✨**3. 位运算符
常见的位运算符号有&、|、^、~、>>、<<,分别代表着如下含义:
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
& | 按位与运算 | 011 & 101 | 2个都为1才为1,结果为001 |
| | 按位或运算 | 011 | 101 | 有1个为1就为1,结果为111 |
^ | 按位异或运算 | 011 ^ 101 | 不同的为1,结果为110 |
~ | 取反运算 | ~011 | 100 |
<< | 左移运算 | 1010 << 1 | 10100 |
>> | 右移运算 | 1010 >> 1 | 0101 |
ps:取反、左右位移运算需要在补码的基础上运算。
& 按位与运算
011 3
101 5
---------------
001
结论: 按位与运算 相同为1 结果为1| 按位或运算
011 3
101 5
---------------
111
结论: 按位或运算 只要有一个为1 结果就为1^ 按位异或运算
011 3
101 5
---------------
110
结论: 按位异或运算 不同的为1 相同为0~ 按位取反运算
~ 011100
结论: 取反运算 1为0 0为1<< 左移运算
1010 << 1
10100
结论:左移就是所有位 向左移动 后面补0>> 右移运算
1010 >> 1
101
结论:右移就是所有位 向右移动 前面补0
#include <stdio.h>
#include <inttypes.h>
int main() {// & 按位与运算// 例子: // 011 3 // 101 5// ---------------printf("%d\n", 3 & 5); // 1// 按位或printf("%d\n", 3 | 5); // 7// 按位异或printf("%d\n", 3 ^ 5); // 6 // 取反printf("%d\n", ~3); // -4// 需要注意: 做高位取反
int8_t num = 3;
printf("%d\n", ~num); // -4// 左移
// 0001 1000
printf("%d\n", 1 << 3); // 8// 右移
// 0101 0010
printf("%d\n", 5 >> 1); // 2return 0;
}
#include <stdio.h>
#include <inttypes.h>int main() {// 将变量a的第2位设置为1,其他位保持不变
// uint8_t a = 0b10110011; // 0xb3;
// a = ob1011 0111
// 0b0000 0100uint8_t a = 0xb3;printf("置位结果:%#x\n", a | 0x4); // 0xb7
printf("置位结果:%#x\n", a | (1 << 2)); // 0xb7// 将变量b的第2位、第6位设置为1,其他位保持不变
// uint8_t b = 0b10110011; // 0xb3;
// 0b1011 0011 0xb3
// 0b0100 0100 0x44
// 0b1111 0111 0xF7
uint8_t b = 0xb3;
printf("置位结果:%#x\n", b | 0x44); // 0xf7
printf("置位结果:%#x\n", a | (1 << 2 | 1 << 6)); // 0xf7// 将变量c的第5位设置为0,其他位保持不变
// uint8_t c = 0b1011 0011; // 0xb3;
// 0b1011 0011 0xb3
// 0b1101 1111 0xdf
// 0b1001 0011 0x93
uint8_t c = 0xb3;
printf("置位结果:%#x\n", c & 0xdf); // 0x93
printf("置位结果:%#x\n", c & ~(1 << 5)); // 0x93// 将变量d的第0~3位设置为0,其他位保持不变
// uint8_t d = 0b11111111; // 0xff;
// 0b1111 1111 0xff
// 0b1111 0000 0xf0
// 0b0000 1111 0x0fuint8_t d = 0xff;
printf("置位结果:%#x\n", d & 0xf0); // 0xf0
printf("置位结果:%#x\n", d & ~(1<< 0 | 1 << 1 |1<< 2| 1<< 3)); // 0xf0// 将变量e的第2位取反,其他位保持不变
// uint8_t e = 0b1011 0011; // 0xb3;
// 0b1011 0011 0xb3
// 0b0000 0100 0x4
// 0b1011 0111 0xb7
uint8_t e = 0xb3;printf("置位结果:%#x\n", e ^ 0x4); // 0xb7
printf("置位结果:%#x\n", e ^ (1 << 2)); // 0xb7// 将变量f取出8-15位
// uint32_t f = 0x12345678;
// 0001 0010 0011 0100 0101 0110 0111 1000 0x12345678
// 0101 0110 5 6 0x56 0x00000ff00
uint32_t f = 0x12345678;
printf("取出8-15位:%#x\n", (f & 0x00000ff00) >> 8); // 0x56return 0;
}
总结:
& -(与运算)
按位与(&)运算:位与位进行比较,如果都为1,则为1,否则为0;
| -(或运算)
按位或(|)运算:位与位进行比较,如果都为0,则为0,否则为1;
^ -(异或运算)
按位异或运算:位与位进行比较,相同为0,不同为1;
~ -(取反运算)
按位取反运算:补码取反,再将取反后的补码转为原码;
ps:无符号的数据,取反后最高位为1,也不需要逆运算。
/*** 按位取反运算:补码取反,再将取反后的补码转为原码。* 1、正数取反:由于正数的原码和补码是相同的,取反的方式简便了些。* 补码(原码) -> 取反 -> 补码逆运算 -> 反码逆运算(符号位不变) -> 取反后的原码* 2、负数取反:* 原码 -> 反码 -> 补码 -> 取反 -> 取反后的补码即原码* 示例:* 原码(补码) 取反的补码 补码逆运算-1 反码逆运算* ~40 = 0010 1000 -> 1101 0111 -> 1101 0110 -> 1010 1001 = -41** 原码(补码) 取反的补码 补码逆运算-1 反码逆运算* ~15 = 0000 1111 -> 1111 0000 -> 1110 1111 -> 1001 0000 = -16** 原码 反码 补码 取反* ~-15 = 1000 1111 -> 1111 0000 -> 1111 0001 -> 0000 1110 = 14*/
printf("~40 = %d\n", ~40);
printf("~15 = %d\n", ~15);
printf("~-15 = %d\n", ~(-15));
<< -(左移运算符)
将数字的二进制补码全部向左移动,空出来的位置补0,超出范围的二进制数丢弃;
有符号的数据左移后最高位如果为1,则需要进行逆运算
- 无符号的数据,左移后最高位为1,也不需要逆运算;
- -128:1000 0000 特殊情况也不需要逆运算
/*** 示例:* 40 << 4 = 0010 1000 << 4 = 1000 0000 = -128 (特殊的不需要逆运算)* 41 << 4 = 0010 1001 << 4 = 1001 0000 = 1000 1111 = 1111 0000 = -112* 7 6 5 4 3 2 1 0* 1 0 0 1 0 0 0 0*/int8_t p = 40;p <<= 4; // p = p << 4;printf("40 << 4 = %d\n", p);
>> -(右移运算符)
将数字的二进制补码全部向右移动,空出来的位置补什么,取决于原来的最高位是什么。原来的最高是1就补1, 原来的最高位是0 就补0 。也可以转化成这样的一句话: 正数补0, 负数补1
/*23: 0001 0111【原码】 ---- 0001 0111【反码】 ---- 0001 0111 【补码】>> 2-----------------------------------------------0000 0101【补码】 ---> 5*/printf(" 23 >> 2 = %d \n" , 23 >> 2) ; /*123: 1001 0111【原码】 ---- 1110 1000【反码】---- 1110 1001【补码】>> 2-----------------------------------------------1111 1010【补码】 ---> 1111 1001【反码】- ----- 1000 0110 【原码】===> -6*/printf(" -23 >> 2 = %d \n" , -23 >> 2) ;
#include <stdio.h>
#include <inttypes.h>int main() {uint8_t a = 3; // 0000 0011uint8_t b = 10; // 0000 1010// 打印显示2个字符,个数不够,左边补0printf("%02x\n", a & b); // 0000 0010,16进制为02printf("%02x\n", a | b); // 0000 1011,16进制为0bprintf("%02x\n", a ^ b); // 0000 1001,16进制为09uint8_t c = 10; // 0000 1010uint8_t temp = ~c; // 1111 0101printf("%02x\n", temp); // 1111 0101,16进制为f5printf("%02x\n", c << 1); // 0001 0100,16进制为14printf("%02x\n", c >> 1); // 0000 0101,16进制为05return 0;
}
用这个方法进行进制间的计算较为通俗易懂
3.1 进制转换
数据有不同的类型,不同类型数据之间进行混合运算时涉及到类型的转换问题。
-
转换的方法有两种:
-
- 自动转换(隐式转换):遵循一定的规则,由编译系统自动完成
- 强制类型转换:把表达式的运算结果强制转换成所需的数据类型
-
-
- 语法格式: (类型)变量或常量
-
-
类型转换的原则:
-
- 占用内存字节数少(值域小)的类型,向占用内存字节数多(值域大)的类型转换,以保证精度不降低。
#include <stdio.h>int main() {// 隐式转换(自动转换):编译器自动转换int a = 11;double b = a; //将a的值,11,转换为11.0000,再给b赋值 printf("b = %lf\n", b);printf("a = %d\n", a);// 强制类型转换,用户转换, (类型)变量或常量int m = 3;int n = 2;b = (double)m/n;printf("b = %lf\n", b);b = (double)3/2;printf("b = %lf\n", b);// 类型转换原则:数据类型小的往数据类型大的转int c = 100;char d = (char)c; //没有问题printf("d = %d\n", d);// 大的往小的转,数据可能会丢失c = 129;d = (char)c;printf("d = %d\n", d);// 小的往大的转long long big = (int)c;printf("big = %lld\n", big);return 0;
}
运行结果:
b = 11.000000
a = 11
b = 1.500000
b = 1.500000
d = 100
d = -127
big = 129
#include <stdio.h>int main() {// 1、二进制 逢二进一 除二取余法 // 0 0// 1 1// 2 10// 3 11// 4 100// 5 101// 6 110// 7 111// 8 1000// 转换 8 《 = 》 2// 01 4 5// 001 100 101// 转换 16进制 = 2// A B C// 1010 1011 1100return 0;
}
4. 控制语句
4.1 程序执行的三大流程
- 顺序 : 从上向下, 顺序执行代码
- 分支 : 根据条件判断, 决定执行代码的分支
- 循环 : 让特定代码重复的执行
4.2 分支语句
条件语句用来根据不同的条件来执行不同的语句,C语言中常用的条件语句包括if语句和switch语句
4.2.1 if 语句
#include <stdio.h>int main() {// 根据条件去执行某些代码int a = 998;if (a > 98.8) {printf("回家睡觉");}return 0;
}
4.2.2if…else 语句
基本语法
#include <stdio.h>int main() {/*
● 定义一个整数变量记录年龄
● 判断是否满 18 岁 (>=)
● 如果满 18 岁,允许进网吧嗨皮
● 否则,提示回家写作业*/int age;printf("请输入你的年龄:");if (scanf("%d", &age)) {if (age >= 18) {printf("玩游戏");} else {printf("回家写作业");}}return 0;
}
4.2.3 三目运算符
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
?: | 三目运算符 | a>b?a:b; | 如果a>b,整体为结果a,否则整体结果为b |
#include <stdio.h>int main() {// ?: 三目运算符 a>b?a:b; 如果a>b,整体为结果a,否则整体结果为bint a = 30;
int b = 40;
int c= a > b ? a : b;
printf("最大值=%d\n",c);return 0;
}
4.2.4if…else if…else语句
if (条件1) {条件1成立时,要做的事……
} else if(条件2) {条件2成立时,要做的事 ……
} else {条件不成立时,要做的事 ……
}
#include <stdio.h>int main() {/* ● 天猫超市双 11 推出以下优惠促销活动:○ 购物满 50 元,打 9 折;○ 购物满 100 元,打 8 折;○ 购物满 200 元,打 7 折;○ 购物满 300 元,打 6 折;● 编程计算当购物满 s 元时,实际付费多少*/// 1.定义变量记录购买金额 定义变量记录实际费用float money, real_money;// 2.输入购买金额printf("请输入购买金额:");scanf("%f", &money);// 3.根据购买金额判断折扣if (money >= 50 && money < 100) {// 购物满 50 元,打 9 折;real_money = money * 0.9;} else if (money >= 100 && money < 200) {// 购物满 100 元,打 8 折;real_money = money * 0.8;} else if (money >= 200 && money < 300) {// 购物满 200 元,打 7 折;real_money = money * 0.7;} else if (money >= 300) {// 购物满 300 元,打 6 折;real_money = money * 0.6;} else {// 不满50 原价real_money = money;}printf("购买金额: %f 实际价格: %f\n", money, real_money);return 0;
}
4.2.5 switch语句
-
测试一个表达式是否等于一些可能的值,并根据表达式的值执行相应的代码块,可以使用switch语言实现
-
switch可以支持数据类型:
-
- int
- 枚举类型
- char类型
-
switch和if区别:
-
- 需要根据布尔条件来执行不同的代码块,则应使用if语句
- 需要根据表达式的值来执行不同的代码块,则应使用switch语句
语法格式:
switch (expression) {case value1:// 代码块1break;case value2:// 代码块2break;default:// 代码块3
}
案例:
#include <stdio.h>int main() {/* ○ 输入1:输出Monday○ 输入2:输出Tuesday○ 输入3:输出Wednesday○ 输入4:输出Thursday○ 输入5:输出Friday○ 输入6:输出Saturday○ 输入7:输出Sunday○ 输入其它:输出error*/int day;printf("请输入星期:");scanf("%d", &day);switch (day) {case 1:printf("Monday\n");break;case 2:printf("Tuesday\n");break;case 3:printf("Wednesday\n");break;case 4:printf("Thursday\n");break;case 5:printf("Friday\n");break;case 6:printf("Saturday\n");break;case 7:printf("Sunday\n");break;default:printf("error\n");}return 0;
}
4.2.6 分支综合案例
1.定义变量保存年份、月份、天数
2.输入年份和月份
3.根据月份输出天数
1、3、5、7、8、10、12月 31天
4、6、9、11月 30天
2月 非闰年 28天 闰年 29天
闰年判断:能被4整除,但不能被100整除的;或者能被400整除的年份
代码实现:
#include <stdio.h>int main() {/* ● 输入:年份(整数)和月份(整数)● 输出:该月份的天数(整数)*/int year, month, day;printf("请输入年份:");scanf("%d", &year);printf("请输入月份:");scanf("%d", &month);switch (month) {case 1:case 3:case 5:case 7:case 8:case 10:case 12:day = 31;break;case 4:case 6:case 9:case 11:day = 30;break;case 2:if (year % 4 == 0 && year % 100 != 0 || year %400 == 0) {day = 29;} else {day = 28;}break;default:printf("输入的月份有误!");}printf("%d 年 %d 月 有 %d 天\n", year, month, day);return 0;
}
4.3 循环语句
循环就是重复执行代码
流程图:
循环的实现方式:
while
do…while
for
4.3.1 while语句
示例代码:
#include <stdio.h>int main() {// 终止条件 循环变量int i = 0;while (i < 5 ) {printf("玛卡巴卡\n");i++;}return 0;
}
4.3.2 do…while语句
示例代码:
#include <stdio.h>int main() {// 至少执行一次int i = 0;do {printf("大大怪\n");i++;} while (i < 10);return 0;
}
- do-while 循环语句是在执行循环体之后才检查 条件 表达式的值
- 所以 do-while 语句的循环体至少执行一次,do…while也被称为直到型循环
4.3.3 for语句
-
for ( init; condition; increment ) {循环体 }
-
init会首先被执行,且只会执行一次
-
接下来,会判断 condition,如果条件condition为真
-
在执行完 for 循环主体后,控制流会跳回上面的 increment 语句
-
条件再次被判断
示例代码:
#include <stdio.h>int main() {int a = 4;// 初始化变量 条件 循环变量 增加/减少for (int i = 0; i < a; i++) {printf("小小怪\n");}return 0;
}
4.3.4 循环案例
#include <stdio.h>int main() {// 求 1- 100 的和int sum = 0;for(int i= 1; i<= 100; i++) {sum += i;}printf("%d\n", sum);int a = 0;int b = 0;while(a <= 100) {b += a;a++; }printf("%d\n", b);int c = 0;int n = 0;do {n += c;c++;} while(c <= 100);printf("%d\n", n);return 0;
}
4.4 跳转关键字
- 循环和switch专属的跳转:break
- 循环专属的跳转:continue
- 无条件跳转:goto
- // break 跳出整个循环
- // continue 跳出本次循环
#include <stdio.h>int main() {// break 跳出整个循环// continue 跳出本次循环// int i= 0;
// while(i < 6) {// if(i == 3) {
// printf("找到了\n");
// break;
// }
// i++;
// printf("i = %d\n", i);
// }int i= 0;while(i < 6) {if(i == 3) {printf("找到了\n");continue;}i++;printf("i = %d\n", i);}return 0;
}
#include <stdio.h>// ● goto用于无条件跳转
// ○ 在一种情况下可以使用goto语句:从一组嵌套的循环中跳出
// ● goto语句可以导致代码不易理解和维护,并且容易引入不必要的错误。因此,除非必要,最好不要使用goto语句int main() {int i = 0;while(i < 3) {if (i == 3) {goto End; //条转到End标签}printf("i = %d\n", i);i++;}End: printf("End\n");return 0;
}
5. 函数
5.1 概述
-
函数是一种可重用的代码块,用于执行特定任务或完成特定功能
-
函数作用:对具备相同逻辑的代码进行封装,提高代码的编写效率,实现对代码的重用
-
函数分类
-
- 系统函数,即库函数:这是由编译系统提供的,用户不必自己定义这些函数,可以直接使用它们,如我们常用的打印函数printf()。
- 自定义函数:用以解决用户的专门需要。
5.2 函数的使用
5.2.1 无参无返回值
#include <stdio.h>// 函数没有参数 也没有返回值
// 定义函数
add() {printf("阿巴阿巴\n");
}
int main() {// 调用函数add();return 0;
}
5.2.2有参无返回值
#include <stdio.h>
// 编写一个函数,实现2个数相加,2个数通过参数传递
// 定义函数
void add(int a,int b){// 形参相加,打印int res = a + b;printf("相加结果为:%d\n",res);
}int main() {// 调用函数add(10,20);return 0;
}
5.2.3 有参有返回值
#include <stdio.h>// 函数有参数有返回值
// 返回使用return返回
// 定义函数返回值类型 int int sum(int a, int b) {return a +b;
}
int main() {// 调用函数
int num = sum(10, 20);
printf("num = %d\n", num);return 0;
}
5.2.4 返回值注意点
-
return的作用是结束函数
-
- 函数内,return后面的代码不会执行
示例代码:
#include <stdio.h>// 函数定义
void func() {printf("11111111111111111\n");return; // 结束函数,函数内后面代码不会执行printf("222222222222222222\n");
}int main() {// 函数调用func();return 0;
}
5.2.5 函数的声明
- 如果函数定义代码没有放在函数调用的前面,这时候需要先做函数的声明
- 所谓函数声明,相当于告诉编译器,函数是有定义的,再别的地方定义,以便使编译能正常进行
- ❤️注意:一个函数只能被定义一次,但可以声明多次
示例代码:
#include <stdio.h>
// 函数的声明 分号不能省略
// 函数声明的前面可以加extern关键字,也可以不加int add(int a, int b);
// 另一种方式,形参名可以不写
// int add(int, int);
int main() {// 函数的调用int temp = add(10, 20);printf("temp = %d\n", temp);return 0;
}// 函数的定义
int add(int a, int b) {// 实现两个形参相加,并返回检索结果int c = a + b;return c;
}
5.3 局部变量和全局变量
5.3.1 局部变量
-
定义在代码块{}里面的变量称为局部变量(Local Variable)
-
局部变量的作用域(作用范围)仅限于代码块{}, 离开该代码块{}是无效的
-
- 离开代码块{}后,局部变量自动释放
5.3.2 全局变量
在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件
5.4 多文件编程
5.4.1 多文件编程
- 把函数声明放在头文件xxx.h中,在主函数中包含相应头文件
- 在头文件对应的xxx.c中实现xxx.h声明的函数
5.4.2 防止头文件重复包含
- 当一个项目比较大时,往往都是分文件,这时候有可能不小心把同一个头文件 include 多次,或者头文件嵌套包含。
- 为了避免同一个文件被include多次,C/C++中有两种方式。
- 方法一:
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__ // 声明语句 #endif
方法二(嵌入式不能用):
#pragma once // 声明语句
头文件包含的区别:
- <> 表示系统直接按系统指定的目录检索
- “” 表示系统先在 “” 指定的路径(没写路径代表当前路径)查找头文件,如果找不到,再按系统指定的目录检索
多文件编程运行结果出现中文乱码:在命令行输入:chcp 65001
再编译执行。
✨✨✨✨❤️6. 指针(重难点)
6.1 指针基本语法
6.1.1 指针变量的定义和使用(重点)
-
指针也是一种数据类型,指针变量也是一种变量
-
指针变量指向谁,就把谁的地址赋值给指针变量
-
指针是C语言的精华,没有掌握指针,就是没有掌握C语言。
指针让 C 语言更像是结构化的底级语言,所有在内存中的数据结构均可用指针来访问,指针让 C 语言带来了
更简便的操作,更优效率,更快的速度。是天使也是魔鬼。
示例代码:
#include <stdio.h>int main() {// 指针就是一种数据类型 可以用来存储内存的地址
// 指针 用来操作内存的
// 查看变量的地址
int a = 30;
printf("a = %d\n", a);
// & 按位与
// 取地址符
printf("a address = %p\n", &a);// 指针地址
// 定义指针
// 指针就是数据类型
// int * 代表的就是指针, ptr代表的是指针变量名 &a 取a的地址给ptr
int* ptr = &a;
// %p 取地址, ptr 就是a的地址 *ptr 获取地址内容的值(取a的值)
printf("a == %p\n, ptr == %p, a == %d, ptr == %d\n" , &a, ptr, a, *ptr);// 指针的实质就是内存地址return 0;
}
类型 变量;
类型 * 指针变量 = &变量;
-
- & 叫取地址,返回操作数的内存地址
- * 叫解引用,指操作指针所指向的变量的值
- 在定义变量时,* 号表示所声明的变量为指针类型
-
-
- 指针变量要保存某个变量的地址,指针变量的类型比这个变量的类型多一个*
-
-
- 在指针使用时,* 号表示操作指针所指向的内存空间
#include <stdio.h>int main() {// 定义一个int类型的变量,同时赋值为10int a = 10;// 打印变量的地址printf("&a = %p\n", &a);// 定义一个指针变量,int *保存int的地址// int *代表是一种数据类型,int *指针类型,p才是变量名int* p;// 指针指向谁,就把谁的地址赋值给这个指针变量p = &a;// 打印p, *p, p指向了a的地址,*p就是a的值printf("p = %p, *p = %d\n", p, *p);return 0;
}
6.1.2 通过指针间接修改变量的值
- 指针变量指向谁,就把谁的地址赋值给指针变量
- 通过 *指针变量 间接修改变量的值
6.1.3const修饰的指针变量
语法格式
int a = 1;
const int *p1 = &a; // 等价于 int const *p1 = &a;
int * const p2 = &a;
const int * const p3 = &a;
从左往右看,跳过类型,看修饰哪个字符
- 如果是*, 说明指针指向的内存不能改变
- 如果是指针变量,说明指针的指向不能改变,指针的值不能修改
6.1.4 指针大小
-
使用sizeof()测量指针的大小,得到的总是:4或8
-
sizeof()测的是指针变量指向存储地址的大小
-
-
在32位平台,所有的指针(地址)都是32位(4字节)
-
在64位平台,所有的指针(地址)都是64位(8字节)
-
6.1.5 指针步长
-
指针步长指的是通过指针进行递增或递减操作时,指针所指向的内存地址相对于当前地址的偏移量。
-
指针的步长取决于所指向的数据类型。
-
- 指针加n等于指针地址加上 n 个 sizeof(type) 的长度
- 指针减n等于指针地址减去 n 个 sizeof(type) 的长度
6.1.6 野指针和空指针
- 指针变量也是变量,是变量就可以任意赋值
- 任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针
-
- 此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)
- 野指针不会直接引发错误,操作野指针指向的内存区域才会出问题
- 为了标志某个指针变量没有任何指向,可赋值为NULL
-
- NULL是一个值为0的宏常量
6.1.7 多级指针
C语言允许有多级指针存在,在实际的程序中一级指针最常用,其次是二级指针。
6.2 指针和函数
6.2.1 函数参数传值
传值是指将参数的值拷贝一份传递给函数,函数内部对该参数的修改不会影响到原来的变量
✨✨✨❤️6.2.2 函数参数传址(重点)
传址是指将参数的地址传递给函数,函数内部可以通过该地址来访问原变量,并对其进行修改。
总结:
无论是前面学习的变量还是后面我们要学习的数组,都是逻辑上得表现,最终都要保存到内存中,而内存是线性的,这是物理基础。
内存是以字节为单位进行编址的,内存中的每个字节都对应一个地址,通过地址才能找到每个字节。
6.3 函数指针
6.3.1 函数名
一个函数在编译时被分配一个入口地址,这个地址就称为函数的指针,函数名代表函数的入口地址
6.3.2 函数指针
函数指针:它是指针,指向函数的指针
语法格式:
返回值 (*函数指针变量)(形参列表);
函数指针变量的定义,其中返回值、形参列表需要和指向的函数匹配
#include <stdio.h>int func(int a, int b) {return a + b;
}int main() {// 函数在编译的时候 会有一个入口地址// 这个地址就是函数指针地址// 一个函数在编译时被分配一个入口地址,这个地址就称为函数的指针,函数名代表函数的入口地址// printf("func = %#x\n", &func);// printf("func = %#x\n", *func);// printf("func = %#x\n", func);// int res = func(1, 3);// 函数调用 最常见的形式 推荐使用printf("res = %d\n", func(1, 3));// 地址的形式printf("res = %d\n", (&func)(1, 5));// 指针的形式printf("res = %d\n", (*func)(5, 5));return 0;
}
✨✨✨❤️6.3.3 回调函数(重点)
-
函数指针变量做函数参数,这个函数指针变量指向的函数就是回调函数
-
回调函数可以增加函数的通用性
-
- 在不改变原函数的前提下,增加新功能
/****#include <stdio.h>int func(int a, int b) {return a + b;
}int main() {// 函数在编译的时候 会有一个入口地址// 这个地址就是函数指针地址// 一个函数在编译时被分配一个入口地址,这个地址就称为函数的指针,函数名代表函数的入口地址// printf("func = %#x\n", &func);// printf("func = %#x\n", *func);// printf("func = %#x\n", func);// int res = func(1, 3);// 函数调用 最常见的形式 推荐使用printf("res = %d\n", func(1, 3));// 地址的形式printf("res = %d\n", (&func)(1, 5));// 指针的形式printf("res = %d\n", (*func)(5, 5));return 0;
}*/#include <stdio.h>int add(int a, int b) {return a + b;
}int minus(int a, int b) {return a - b;
}// 定义calac 方法 接受的参数 int int 接受计算类型的函数名称 int calac(int a, int b, int(*func)(int, int)) {return func(a, b);
}int main() {// int res = add(10, 20);// 可以接受 参数 接受我们的计算类型 (加减……)// int res = calac(5, 3, add);int res = calac(5, 3, minus);printf("sum = %d\n", res);return 0;
}
6.4 案例(计算圆的周长和面积)
// 计算圆的周长和面积// 使用回调函数的形式 计算// 圆的周长 pi * 2R
// 面积 pi *r *r
#include <stdio.h>#define pi 3.14double len(double r) {return 2 * r * pi;
}
double size(double r) {return pi * r * r;
}double calac(double r, double(*p)(double)) {return p(r);
}int main() {double r = 5;double length = calac(r, len);double area = calac(r, size);printf("len = %lf\n", length);printf("size = %lf\n", area);return 0;
}#parse("C File Header.h")
#if (${HEADER_FILENAME})
#[[#include]]# "${HEADER_FILENAME}"
#end
7. 数组
7.1数组的基本使用
7.1.1什么是数组
数组是 C 语言中的一种数据结构,用于存储一组具有相同数据类型的数据
数组中的每个元素可以通过一个索引(下标)来访问,索引从 0 开始,最大值为数组长度减 1。
7.1.2.数组的使用
基本语法:
类型 数组名[元素个数];
int arr[5];
注意:
-
- 数组名不能与其它变量名相同,同一作用域内是唯一的
- 其下标从0开始计算,因此5个元素分别为arr[0],arr[1],arr[2],arr[3],arr[4]
示例代码:
#include <stdio.h>int main() {// 定义了一个数组,名字叫a,有10个成员,每个成员都是int类型int a[10]; // a[0]…… a[9],没有a[10]// 没有a这个变量,a是数组的名字,但不是变量名,它是常量a[0] = 0;// ……a[9] = 9;// 数据越界,超出范围,错误// a[10] = 10; // errfor (int i = 0; i < 10; i++) {a[i] = i; // 给数组赋值}// 遍历数组,并输出每个成员的值for (int i = 0; i < 10; i++) {printf("%d ", a[i]);}printf("\n");return 0;
}
7.1.3数组的初始化
- 在定义数组的同时进行赋值,称为初始化
- 全局数组若不初始化,编译器将其初始化为零
- 局部数组若不初始化,内容为随机值
int a1[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // 定义一个数组,同时初始化所有成员变量int a2[10] = { 1, 2, 3 }; // 初始化前三个成员,后面所有元素都设置为0int a3[10] = { 0 }; // 所有的成员都设置为0// []中不定义元素个数,定义时必须初始化int a4[] = { 1, 2, 3, 4, 5 }; // 定义了一个数组,有5个成员
7.1.4数组名
- 数组名是一个地址的常量,代表数组中首元素的地址
示例代码:
#include <stdio.h>int main() {// 定义一个数组,同时初始化所有成员变量int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 数组名是一个地址的常量,代表数组中首元素的地址printf("a = %p\n", a);printf("&a[0] = %p\n", &a[0]);int n = sizeof(a); // 数组占用内存的大小,10个int类型,10 * 4 = 40int n0 = sizeof(a[0]); // 数组第0个元素占用内存大小,第0个元素为int,4int num = n / n0; // 元素个数printf("n = %d, n0 = %d, num = %d\n", n, n0, num);return 0;
}
7.2数组案例
7.2.1一维数组的最大值
#include <stdio.h>int main() {// 定义一个数组,同时初始化所有成员变量int a[] = {1, -2, 3, -4, 5, -6, 7, -8, -9, 10};// 假设第0个元素就是最大值int temp = a[0];for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++) {// 如果有元素比临时的最大值大,就交换值if (a[i] > temp) {temp = a[i];}}printf("数组中最大值为:%d\n", temp);return 0;
}
7.3数组和指针
✨✨✨❤️7.3.1通过指针操作数组元素(重点)
- 数组名字是数组的首元素地址,但它是一个常量
- * 和 [] 效果一样,都是操作指针所指向的内存
1.数组名是一个常量,不允许重新赋值。
2.指针变量是一个变量,可以重新赋值。
3.p+i 和 array+i 均表示数组元素 array[i]的地址,均指向 array[i]
4.(p+i)和(array+i)均表示 p+i 和 array+i 所指对象的内容 array[i]。
5.p++:等价于(p++)。其作用:先得到*p,再使 p=p+1。
#include <stdio.h>int main() {int array[10] = { 1,2,3,4,65,6,12,13,14,15 };int* p = &array;printf("array=== %p\n", &array);printf("p === %p\n", p);int len = sizeof(array) / sizeof(array[0]);for (int i = 0; i < len; i++){// array+i 和p+i 指向的都是地址 array[i]的地址// *(array+i) 和*(p+i) 指向的内容 ====array[i]// printf("arr[%d] = %d\n", i, array[i]);// printf("arr[%d] = %d\n", i, *(array + i));// printf("arr[%d] = %d\n", i, *(p + i));// *(p++) == *p++ 先获取*p p=p+1// printf("arr[%d] = %d\n", i, *(p++));printf("arr[%d] = %d\n", i, *p++);/* code */}return 0;
}
7.3.2 指针数组
指针数组,它是数组,数组的每个元素都是指针类型
7.3.3 数组名做函数参数
- 数组名做函数参数,函数的形参本质上就是指针
7.4 字符数组与字符串
✨✨✨❤️7.4.1 字符数组与字符串区别(重点)
C语言中没有字符串这种数据类型,可以通过char的数组来替代
数字0(和字符 ‘\0’ 等价)结尾的char数组就是一个字符串,字符串是一种特殊的char的数组
✨✨如果char数组没有以数字0结尾,那么就不是一个字符串,只是普通字符数组
7.4.2 字符串的输入输出
- 由于字符串采用了’\0’标志,字符串的输入输出将变得简单方便
#include <stdio.h>int main()
{char str[100];printf("input string1: ");// scanf("%s",str) 默认以空格分隔// 可以输入空格gets(str);printf("output: %s\n", str);return 0;
}
✨✨✨❤️7.4.3 字符指针(重点)
-
字符指针可直接赋值为字符串,保存的实际上是字符串的首地址
-
这时候,字符串指针所指向的内存不能修改,指针变量本身可以修改
7.4.4 字符串常用字符库
7.4.4.1 strlen
示例代码:
#include <string.h>
size_t strlen(const char *s);
功能:计算指定指定字符串s的长度,不包含字符串结束符‘\0’
参数:s:字符串首地址
返回值:字符串s的长度,size_t为unsigned int类型,不同平台会不一样
#include <stdio.h>
#include <string.h>int main() {char str[] = "abcdefg";int n = strlen(str);printf("n = %d\n", n); // n = 7return 0;
}
7.4.4.2 strcpy
示例代码:
#include <string.h>
char *strcpy(char *dest, const char *src);
功能:把src所指向的字符串复制到dest所指向的空间中,'\0'也会拷贝过去
参数:dest:目的字符串首地址,如果参数dest所指的内存空间不够大,可能会造成缓冲溢出的错误情况src:源字符首地址
返回值:成功:返回dest字符串的首地址失败:NULL
#include <stdio.h>
#include <string.h>int main() {char dest[20] = "123456789";char src[] = "hello world";strcpy(dest, src);printf("%s\n", dest); // hello worldreturn 0;
}
7.4.4.3 strcat
示例代码
#include <string.h>
char *strcat(char *dest, const char *src);
功能:将src字符串连接到dest的尾部,‘\0’也会追加过去
参数:dest:目的字符串首地址src:源字符首地址
返回值:成功:返回dest字符串的首地址失败:NULL
#include <stdio.h>
#include <string.h>int main() {char str[20] = "123";char *src = "hello world";strcat(str, src);printf("%s\n", str); // str = 123hello worldreturn 0;
}
7.4.4.4 strcmp
示例代码:
#include <string.h>
int strcmp(const char *s1, const char *s2);
功能:比较 s1 和 s2 的大小,比较的是字符ASCII码大小。
参数:s1:字符串1首地址s2:字符串2首地址
返回值:相等:0大于:>0小于:<0
#include <stdio.h>
#include <string.h>int main() {char *str1 = "hello world";char *str2 = "hello mike";if (strcmp(str1, str2) == 0) {printf("str1==str2\n");} else if (strcmp(str1, str2) > 0) {printf("str1>str2\n"); // str1>str2} else {printf("str1<str2\n"); }return 0;
}
7.5 字符串案例
自定义一个函数my_strlen(),实现的功能和strlen一样
示例代码:
#include <stdio.h>// 函数定义
int my_strlen(char * temp) {// 定义一个累加个数的变量,初始值为0int i = 0;// 循环遍历每一个字符,如果是'\0'跳出循环while (temp[i] != '\0') {// 下标累加i++;}return i;
}int main() {char *p = "hello";// 函数调用int n = my_strlen(p);printf("n = %d\n", n);return 0;
}
8. 复合类型(自定义类型)
8.1 结构体
✨✨✨❤️8.1.1 结构体的使用(重点)
示例代码:
#include <stdio.h>struct stu {char name[20];int age;char sex[6];
};
// struct 结构体名 变量名;
struct stu s = { "mike",18,'nan' };// 定义多个结构体数据
struct stu s1[] = {{"mike",18,'nan'},{ "mike",18,'nan'},{ "mike",18,'nan'},{ "mike",18,'nan'}
};// struct stu2 {
// char name[50];
// int age;
// }s2 = { "yoyo", 19 };int main() {return 0;
}
#include <stdio.h>
#include <string.h>struct ST {int age;int a;int b;
};
struct ST s = { 1,2,3 };// struct ST s[] = {
// {1,2,3},
// {1,2,3},
// {1,2,3}
// };
int main() {// 结果变量的 我们可以通过.操作结构体成员// char* p = &s;// printf("name = %s age = %d sex = %s\n", s.name, s.age, s.sex);// strcpy(s.name, "李四");// s.name = "李四";// printf("name = %s age = %d sex = %s\n", s.name, s.age, s.sex);// 结构体的大小// 结构体的大小 是由内部数据决定的 如果是空的结构 是由内部的数据类型决定的printf("大小=%d\n", sizeof(s));return 0;
}
8.1.2结构体做函数参数
8.1.2.1结构体值传参
传值是指将参数的值拷贝一份传递给函数,函数内部对该参数的修改不会影响到原来的变量
示例代码:
#include <stdio.h>#include <string.h>
struct stu {char name[50];int age;
};// 值传递 值传递 只在本函数内容 生效
void show_struct(struct stu s) {strcpy(s.name, "李四");printf("%s %d\n", s.name, s.age);
}int main() {struct stu s = { "lisi",30 };show_struct(s);printf("%s %d\n", s.name, s.age);return 0;
}
8.1.2.2结构体地址传递
传址是指将参数的地址传递给函数,函数内部可以通过该地址来访问原变量,并对其进行修改。
示例代码:
#include <stdio.h>#include <string.h>
struct stu {char name[50];int age;
};// 地址传递 地址传递
void show_struct(struct stu* s) {// 如果此时结构体是指针类型的 就不能使用.操作符 需要->操作符strcpy(s->name, "李四");printf("%s %d\n", s->name, s->age);
}int main() {struct stu s = { "lisi",30 };show_struct(&s);printf("%s %d\n", s.name, s.age);return 0;
}
8.2共用体
8.2.1共用体的语法
- 共用体union是一个能在同一个存储空间存储不同类型数据的类型
- 共用体所占的内存长度等于其最长成员的长度,也有叫做共用体
- 同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用
- 共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖
- 共用体变量的地址和它的各成员的地址都是同一地址
8.2.2共用体和结构体区别
-
存储方式:
-
结构体:结构体中的每个成员都占据独立的内存空间,成员之间按照定义的顺序依次存储
-
共用体:共用体中的所有成员共享同一块内存空间,不同成员可以存储在同一个地址上
-
-
内存占用:
- 结构体:结构体的内存占用是成员变量占用空间之和,每个成员变量都有自己的内存地址
- 共用体:共用体的内存占用是最大成员变量所占用的空间大小,不同成员变量共享同一块内存地址
示例代码:
#include <stdio.h>
union UN
{char ch;short sh;int i;
};// ● 共用体所占的内存长度等于其最长成员的长度,也有叫做共用体
// ● 同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用
// ● 共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖
// ● 共用体变量的地址和它的各成员的地址都是同一地址int main() {union UN u;// 查看共用体的长度printf("u == %d\n", sizeof(u));// 数据的地址 成员和共用体的地址 是一致的 printf("ch = %p\n sh = %p\n i = %p\n s=%p\n", &u.ch, &u.sh, &u.i, &u);u.i = 0x12345678;// int 4 字节 char 1字节 short 2字节 printf("== %0x\n ===%0x\n", u.ch, u.sh);return 0;
}
8.2.3大小端判断:
所谓的大端模式,是指数据的低位(就是权值较小的后面那几位)保存在内存的高地址中,而数据的高位,保存在内存的低地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;所谓的小端模式,是指数据的低位保存在内存的低地址中,而数 据的高位保存在内存的高地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。1)大端模式:低地址 -----------------> 高地址0x12 | 0x34 | 0x56 | 0x782)小端模式:低地址 ------------------> 高地址0x78 | 0x56 | 0x34 | 0x12
示例代码:
#include <stdio.h>
#include <string.h>
union un
{char ch;int s;/* data */
}un;int main() {un.s = 0x12345678;printf("%p\n", un.ch);if (un.ch == 0x78) {printf("小端对齐\n");}else {printf("大端对齐\n");}return 0;
}
8.3枚举
-
枚举:将变量的值一一列举出来,变量的值只限于列举出来的值的范围内
-
语法格式:
enum 枚举名 { 枚举值表 };
在枚举值表中应列出所有可用值,也称为枚举元素
- 枚举值是常量,不能在程序中用赋值语句再对它赋值
- 枚举元素本身由系统定义了一个表示序号的数值从0开始顺序定义为0,1,2 …
示例代码:
#include <stdio.h>enum Week {mon = 9, tue, wed, thu, fri, sat
};enum bool{flase, true
};
// 枚举是值的罗列 0.1.2.3.4 所有的值都是在前面的基础上进行累加的 9 10 11 1 2 3
int main() {printf("==%d\n", tue);// enum bool flag;// flag = true;if (true) {printf("flag为真\n");}return 0;
}
8.4 typedef
typedef为C语言的关键字,作用是为一种数据类型(基本类型或自定义数据类型)定义一个新名字,不能创建新类型。
示例代码:
#include <stdio.h>#include <inttypes.h>#define NUM 999
// gcc -E hello.c -o hello.i //处理文件包含,宏和注释
// gcc -S hello.i -o hello.s //编译为汇编文件
// gcc -c hello.s -o hello.o //经汇编后为二进制的机器指令
// gcc hello.o -o hello //链接所用的到库typedef int INNT;
typedef char int_8;
typedef short int_16;// typedef 不是创作新的类型 给类型 进行重新命名
int main() {INNT a = 8807;printf("---%d\n", a);return 0;
}
#include <stdio.h>// 类型起别名
typedef int INT;
typedef char BYTE;
typedef BYTE T_BYTE;
typedef unsigned char UBYTE;// struct type 起别名
// TYPE为普通结构体类型,PTYPE为结构体指针类型
typedef struct type {UBYTE a;INT b;T_BYTE c;
} TYPE, * PTYPE;int main() {TYPE t;t.a = 254;t.b = 10;t.c = 'c';PTYPE p = &t;printf("%u, %d, %c\n", p->a, p->b, p->c);return 0;
}
9.内存管理
9.1C代码编译过程
-
预处理
-
- 宏定义展开、头文件展开、条件编译,这里并不会检查语法
-
编译
-
- 检查语法,将预处理后文件编译生成汇编文件
-
汇编
-
- 将汇编文件生成目标文件(二进制文件)
-
链接
-
- 将目标文件链接为可执行程序
gcc -E hello.c -o hello.i //处理文件包含,宏和注释
gcc -S hello.i -o hello.s //编译为汇编文件
gcc -c hello.s -o hello.o //经汇编后为二进制的机器指令
gcc hello.o -o hello //链接所用的到库1 预处理:预处理相当于根据预处理命令组装成新的 C 程序,不过常以 i 为扩展 名。
2 编 译:将得到的 i 文件翻译成汇编代码 .s 文件。
3 汇 编:将汇编文件翻译成机器指令,并打包成可重定位目标程序的 O 文件。 该文件是二进制文件,字节编码是机器指令。
4 链 接:将引用的其他 O 文件并入到我们程序所在的 o 文件中,处理得到最终 的可执行文件
gcc -E zy.c -o z.i
gcc -S z.i -o z.s
gcc -C z.s -o z.o
gcc z.o -o z
9.2进程的内存分布
-
对于一个C语言程序而言,内存空间主要由五个部分组成 代码区(text)、数据区(data)、未初始化数据区(bss),堆(heap) 和 栈(stack) 组成
-
- 有些人直接把data和bss合起来叫做静态区或全局区
-
代码区(text segment)
-
- 加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。
-
未初始化数据区(BSS)
-
- 加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。
-
全局初始化数据区/静态数据区(data segment)
-
- 加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。
-
栈区(stack)
-
- 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
-
堆区(heap)
-
- 堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
9.3堆区内存的使用
9.3.1malloc函数说明:
#include <stdlib.h>
void *malloc(size_t size);
功能:在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型。分配的内存空间内容不确定。
参数:size:需要分配内存大小(单位:字节)
返回值:成功:分配空间的起始地址失败:NULL
9.3.2free函数说明:
#include <stdlib.h>
void free(void *ptr);
功能:释放ptr所指向的一块内存空间,ptr是一个任意类型的指针变量,指向被释放区域的首地址。对同一内存空间多次释放会出错。
参数:ptr:需要释放空间的首地址,被释放区应是由malloc函数所分配的区域。
返回值:无
示例代码:
#include <stdlib.h>
#include <stdio.h>int main() {int i, *arr, n;printf("请输入要申请数组的个数: ");scanf("%d", &n);// 堆区申请 n * sizeof(int) 空间,等价int arr[n]arr = (int *)malloc(n * sizeof(int));if (arr == NULL) { // 如果申请失败,提前中断函数printf("申请空间失败!\n");return -1;}for (i = 0; i < n; i++){// 给数组赋值arr[i] = i;}for (i = 0; i < n; i++) {// 输出数组每个元素的值printf("%d, ", *(arr+i));}// 释放堆区空间free(arr);return 0;
}
9.4内存分布代码分析
9.4.1返回栈区地址
address of stack memory associated with local variable 'a' returned [-Wreturn-stack-address]这个警告意味着你的代码中有一个函数返回了指向本地变量的地址,而本地变量通常是分配在栈上的。这样做可能会导致未定义的行为,因为在函数调用结束后,本地变量的内存空间将被释放,指向它们的指针就会变成无效。要解决这个警告,你需要确保函数返回的指针不指向本地变量,或者在函数内部动态分配内存,并返回分配的内存地址。如果你需要返回本地变量的值,可以通过函数参数传递给调用者,而不是返回指向本地变量的指针。
#include <stdio.h>int* func() {int a = 10;return &a; // 函数调用完毕,因为a是局部变量,a释放
}int main() {int* p = NULL;p = func();*p = 100; // 操作野指针指向的内存,errprintf("11111111111111111\n"); // 这句话可能执行不到,因为上一句话报错return 0;
}// [{
// "resource": "/Users/doudou/Desktop/workspace/C语言基础/day06/代码/07-返回栈区的地址.c",
// "owner": "cpptools",
// "severity": 4,
// "message": "address of stack memory associated with local variable 'a' returned [-Wreturn-stack-address]",
// "source": "gcc",
// "startLineNumber": 5,
// "startColumn": 13,
// "endLineNumber": 5,
// "endColumn": 13
// }]
9.4.2 返回data区地址
- 在函数内部使用static修饰的变量称为静态局部变量
- 它在程序运行期间只被初始化一次,并且在函数调用结束后也不会被销毁
#include <stdio.h>// static 修饰的局部变量 只会初始化一次 函数调用完成之后 不会销毁int* func() {static int a = 10;return &a;
}int main() {// 只定义 未初始化的数据 值 不确定 // 只能赋值 int age = 666;int* p = NULL;p = func();*p = 100;printf("%d\n", age);printf("11111111111111111\n");return 0;
}// [{
// "resource": "/Users/doudou/Desktop/workspace/C语言基础/day06/代码/07-返回栈区的地址.c",
// "owner": "cpptools",
// "severity": 4,
// "message": "address of stack memory associated with local variable 'a' returned [-Wreturn-stack-address]",
// "source": "gcc",
// "startLineNumber": 5,
// "startColumn": 13,
// "endLineNumber": 5,
// "endColumn": 13
// }]
-
普通局部变量和静态局部变量区别
-
- 存储位置:
-
-
- 普通局部变量存储在栈上
- 静态局部变量存储在静态存储区
-
-
- 生命周期:
-
-
- 当函数执行完毕时,普通局部变量会被销毁
- 静态局部变量的生命周期则是整个程序运行期间,即使函数调用结束,静态局部变量的值也会被保留
-
-
- 初始值:
-
-
- 普通局部变量在每次函数调用时都会被初始化,它们的初始值是不确定的,除非显式地进行初始化
- 静态局部变量在第一次函数调用时会被初始化,然后保持其值不变,直到程序结束
-
示例代码:
#include <stdio.h>void normal_func() {int i = 0;i++;printf("局部变量 i = %d\n", i);
}void static_func() {static int j = 0;j++;printf("static局部变量 j = %d\n", j);
}int main() {// 调用3次normal_func()normal_func();normal_func();normal_func();// 调用3次static_func()static_func();static_func();static_func();return 0;
}
结果:
局部变量 i = 1
局部变量 i = 1
局部变量 i = 1
static局部变量 j = 1
static局部变量 j = 2
static局部变量 j = 3
9.4.2返回堆区地址
示例代码:
#include <stdio.h>
#include <stdlib.h>int* func() {int* tmp = NULL;// 堆区申请空间tmp = (int*)malloc(sizeof(int));*tmp = 100;return tmp; // 返回堆区地址,函数调用完毕,不释放
}int main() {int* p = NULL;p = func();printf("*p = %d\n", *p); // ok// 堆区空间,使用完毕,手动释放if (p != NULL) {free(p);p = NULL;}printf("*p = %d\n", *p);return 0;
}
10.案例-学生信息管理系统
代码实现:
//
// Created by admin on 2024/4/17.
//#include <stdio.h>
#include <string.h>struct stu {char name[30];int age;char sex[5];
};struct stu s[100] = {{"大大怪下士",22,"女"},{"小小怪将军",23,"男"},{"玛卡巴卡",18,"男"}
};
// 帮助菜单显示函数定义
void help_menu() {printf("\n");printf(" 欢迎使用本学生信息管理系统\n");printf("* ================================ *\n");printf("* 1. 添加 *\n");printf("* 2. 显示 *\n");printf("* 3. 查询 *\n");printf("* 4. 修改 *\n");printf("* 5. 删除 *\n");printf("* 6. 退出 *\n");printf("* ================================ *\n");
}int n = 3;// 添加
void add() {printf("请输入用户名:");scanf("%s",s[n].name);printf("请输入年龄:");scanf("%d",&s[n].age);printf("请输入性别:");scanf("%s",s[n].sex);printf("添加成功 姓名:%s\n, 年龄:%d\n, 性别:%s\n",s[n].name,s[n].age,s[n].sex);n++;
}
// 显示
void show() {for (int i = 0; i < n; ++i) {printf("姓名:%s, 年龄:%d, 性别:%s\n",s[i].name,s[i].age,s[i].sex);}
}
// 抽离代码
int find_index(char* p) {char sname[30];scanf("%s",sname);for (int i = 0; i < n; ++i) {if(strcmp(sname,s[i].name) == 0) {return i;}}return -1;
}
// 查询
void find() {printf("请输入需要查询的用户名:");int index = find_index(s[0].name);if (index == -1) {printf("未找到该用户");} else {printf("姓名:%s, 年龄:%d, 性别:%s\n",s[index].name,s[index].age,s[index].sex);}}
// 修改
void edit() {printf("请输入要修改的用户名:");int index = find_index(s[0].name);if(index == -1) {printf("未找到该用户");} else {printf("修改前的用户信息:姓名:%s, 年龄:%d, 性别:%s\n",s[index].name,s[index].age,s[index].sex);printf("请输入修改后的用户名:");scanf("%s",s[index].name);printf("请输入修改后的年龄:");scanf("%d",&s[index].age);printf("请输入修改后的性别:");scanf("%s",s[index].sex);printf("修改后的用户信息:姓名:%s, 年龄:%d, 性别:%s",s[index].name,s[index].age,s[index].sex);}}
// 删除
void dele() {printf("请输入要删除的用户名:");int index = find_index(s[0].name);if(index != -1) {if(index != -1) {s[index] = s[n-1];}n--;printf("删除成功",s[0].name);} else {printf("删除失败,未找到该用户",s[0].name);}
}int main() {while (1) {help_menu();printf("请输入对应的数字:");int num;scanf("%d",&num);switch (num) {case 1:add();break;case 2:show();break;case 3:find();break;case 4:edit();break;case 5:dele();break;}if(num == 6) {printf("程序退出");break;}if(num !=1 && num !=2 && num !=3 && num != 4 && num != 5 && num != 6) {printf("指令输入错误请重新输入!\n");}}return 0;
}