解决方案的配置文件( .sln)
一个解决方案包含多个项目,一个项目包含多个用户编写的文件。
[调试-生产解决方案] 生成的应用文件其路径为 *\Debug\*.exe
项目
每个项目的所有文件都存储在对应的项目文件夹中。其中3个xml类型的配置文件( .vcxproj & .vcxproj.user & .vcxproj.filters )保存了组成该项目的详细信息。
VS2022 的IDE
在添加新项目时,若不指明源文件扩展名,默认为.cpp 。所以为源文件取一个主文件名,扩展名须为 .c
如果修改了某文件尚未保存,则会在"编辑窗口上的文件名"右上方出现一个星号* 只要保存了文件,星号就会立即消失。
尽量从第一条错误语句开始修改。
警告warning也是可以忽略的,但可能得不到正确的结果。
严重错误error必须要改正,否则无法编译成obj目标文件。
运行环境的不同也导致得到不同的结果。
采用错误的算法或数据结构,语句使用不当,粗心大意 也会导致结果错误。
当一个文件“从项目中排除”后,只是不在隶属于当前项目,仍保留在磁盘原位置上
C语言的标识符严格区分大小写。 int不可写为 INT
驼峰命名法(最基本) printEmployeeName 要“见名知意”
C语言中用户定义的标识符只能由 [英文字母 下划线 数字] 组成并且只能( 英文字母、下划线 )开头。
除循环变量中使用 i、j、y、k单字符,其它场景避免使用单字符
宏常量&枚举 都要全部大写
int g_num //全局变量
static int s_initSize //静态变量
函数名结合功能命名,且首字母大写
编译原理
源文件(.c) —{编译 }——> 链接文件(.obj) —{链接}——> 可执行文件(.exe)
{ 翻译环境 }
其中编译包括 (预编译、编译、汇编)
预编译(扫描识别): 扫描源代码是从上到下、从左到右进行的。
把所有的 #define 删除,并展开所有的宏定义,以后就不再包含宏定义了
把 #include<头文件>的内容插入到预编译指令的位置
删除所有的注释,添加行号和文件名标识
保留所有的#pragma的编译器指令,以便后续使用————————————
编译: 经过 (词法分析、语法分析、语义分析、代码优化) 把源代码转换为汇编代码——————————————
汇编: 把每一个汇编代码转变成对应的二进制指令;
把二进制文件与 "链接库" 结合在一起形成一个 "符号表" ,且不做指令优化 ——
链接: 多函数模块、全局变量之间的互相调用,合并在一起、进行地址与空间的分配&符号决议&重定位操作(重新计算各个目标地址的过程)组成一个可执行文件(.exe)
——————
编译器CL.exe编译目标代码obj
链接器LINK.exe把目标代码和库链接生成 最终文件exe
资源编译器RC.exe编译资源文件,通过链接器存入最终文件exe
可执行文件(.exe)——>输出结果
{ 运行环境 }
1. 程序载入内存中
2. 调用main函数,一个项目程序只有1个main函数,即程序的唯一入口
3. 使用内存堆栈,存储局部变量和返回地址
4. 停止程序
[生成]命令实际上把编译&链接俩功能合并在一起。
例如
1> hello.c 对源文件进行编译的提示
1> helloworld.vcxproj -> Y:\项目Pr7\x64\Debug\Pr1.exe 若没有语法错误就链接生成 .exe
计算机系统中的二进制
在计算机系统中,数值一律用 补码来表示和存储。原因在于,1使用补码 ,可以将(+ -)符号位和数值域统一处理;2同时,加法和减法也可以统一处理(CPU只有加法器,减法运算要依靠补码的符号位进行)3此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
对于int来说:数据存放内存中其实存放的是补码·
负整数的补码 <==== 取反,+1 ====>负整数的原码
整型&浮点型 在内存中的存储
当某 >=2个Byte的整型数据在内存中存储的时候,就会有存储顺序的问题
<大端字节序(存储)模式: 例:KEIL,C51
数据的高权值字节内容保存在内存的低地址处,而数据的低权值字节内容保存在内存的高地址。
<小端字节序(存储)模式: 例:ARM,DSP
数据的低权值字节内容保存在内存的低地址处,而数据的高权值字节内容保存在内存的高地址。
#include <stdio.h>
int main() {
char a = -1;
// -1的补码 11111111 11111111 11111111 11111111 取char的一个字节11111111
// 整型的char再用原码打印输出 10000000 000000000 00000000 00000001 = -1
signed char b = -1; //与上同理
unsigned char c = -1;
// -1的补码 11111111 11111111 11111111 11111111 取char的一个字节11111111
// 无符号的char再用原码打印输出 00000000 000000000 00000000 11111111 = 255
printf("a为%d,b为%d,c为%d", a, b, c);
return 0; }
>>>a为-1,b为-1,c为255
有符号char的范围 (-128 ~ 127 ) 规定:-0(10000000) 等价于 -128
#include <stdio.h>
int main() {
char a = -128;
// 10000000 00000000 00000000 10000000 原码 -128
// 11111111 11111111 11111111 10000000 补码 -128
// 10000000 截取的char a
// 11111111 11111111 11111111 10000000 有符号位的char,前补符号位
// 11111111 11111111 11111111 10000000 以U的角度,无符号整数a的补码、反码 4294967168
char b = 128;
// 00000000 00000000 00000000 10000000 原码、补码 128
// 10000000 截取的char b
// 11111111 11111111 11111111 10000000 有符号位的char,前补符号位
// 11111111 11111111 11111111 10000000 以U的角度,无符号整数a的补码、反码 4294967168
printf("%u\n%u\n", a,b);
return 0;
}
>>>4294967168
>>>4294967168
#include <stdio.h>
#include<string.h>
int main() {
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{ a[i] = -1 - i; }
// -1、-2、-3……-128、127、126……3、2、1、0……-1、-2……
printf("%d", strlen(a)); //再0之前有255个字符数
return 0;
}
>>>255
浮点数的小数部分无限循环时,在内存中可能不会精确保存,有误差。
国际标准IEEE 754,对任意一个二进制浮点数 V 可以表示成下面的形式:
• 当S=0,V为正数;当S=1,V为负数
• M 表示有效数字, 1<= M <2
• E 表示指数位<对32位的浮点数,最高1位存储符号位S,后8位存储指数E,剩下的23位存储有效数字M
例如:32位浮点数:
那么,第一位的符号位S=0,有效数字M为001后面再加20个0,凑满23位,指数E等于3+127=130,即10000010
所以,写成二进制形式,应该是S+E+M,即
0 10000010 001 0000 0000 0000 0000 0000以%d打印这个32位二进制数,这是内存中的补码,原码为1091567616
以%f打印这个浮点数为 9.000000
<对64位的浮点数,最高1位存储符号位S,后11位存储指数E,剩下的52位存储有效数字M
IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023
输出格式
\n 换行
\r 控制键”home"
\f 换页符
\t 制表符"补够8位一个\t“
\b 退格符
\' 常量(') //printf("%s\n","\'")
\" 常量(") //printf("%s\n","\"")
\\ 常量(\) //printf("D:\\VS\\") 路径
\0 字符串结束标志
\a 触发win报警声
占位符
%u 输出unsigned整数
%d 输出signed整数(十进制原码)
%zd 输出sizeof的返回值
%c 输出单字符
%s 输出字符串
%f 输出float小数(小数点后6位)
%lf 输出double小数(小数点后6位)
%Lf 输出long double小数
%% 输出(%)
%p 输出指针地址
1.除了%c 以外,都会自动忽略起首的空白字符。如果要强制跳过字符前的空白字符,可以写成
scanf("_%c", &ch);
即%c 前加上一个空格,表示跳过零个或多个空白字符。
2.%s 从当前第一个非空白字符开始读起,直到遇到空白字符
scanf("%10s", name); //限定输入的长度
3.赋值忽略符,把* 加在任何占位符的百分号后面,该占位符就不会返回值,解析后将被丢
scanf("%d%*c%d%*c%d", 2024, 9, 5); >>2024 9 5 //防止格式错误读取失败
关键字(只能用这些预设的)
auto break case char const continue default
do double else enum extern float for goto
if int long register return short signed
sizeof static struct switch typedef union
unsigned void volatile while
数据类型
-Bool //0为false,非0为true
字符型可以理解为整形
数据类型的长度(sizeof中的表达式不参与计算)
#include <stdio.h>
int main()
{ //长度(byte)
printf("%zd\n", sizeof(char)); //1
printf("%zd\n", sizeof(_Bool)); //1
printf("%zd\n", sizeof(short)); //2
printf("%zd\n", sizeof(int)); //4
printf("%zd\n", sizeof(long)); //4
printf("%zd\n", sizeof(long long)); //8
printf("%zd\n", sizeof(float)); //4
printf("%zd\n", sizeof(double)); //8
printf("%zd\n", sizeof(long double)); //8
return 0;}
变量
变量创建的本质:再内存中申请存储空间
定义一个变量的同时对它进行初始化。
<当局部变量和全局变量同名的时候,局部变量优先使用
<局部变量未初始化时,值是随机的。 全局变量未初始化时,值是0
<本来一个局部变量 存储在内存的栈区,但是被static 修饰后存储到了静态区。
存储在静态区的变量和全局变量是一样的:生命周期就和程序的生命周期一样了,执行的结果会保留下来留给下一次计算,只有程序结束,变量才销毁,内存才回收。但是作用域不变的。
一个全局变量/函数(默认是具有外部链接属性的)被static修饰,外部链接属性就变成了内部链接属性,使得这个全局变量/函数只能在本源文件内使用,不能在其他源文件内使用 如果一个全局变量/函数,只想在所在的源文件内部使用,不想被其他文件发现,就可以使用 static修饰
//函数定义文件
static int Add(int x, int y)
{
return x+y;
}
//源文件
#include <stdio.h>
extern int Add(int, int);
int main() {
int a =0;
int b =0;
scanf("%d %d",&a,&b)
printf("%d\n", Add(2, 3));
return 0; }
extern 是用来声明外部变量的 如:extern int Add(int, int);
操作符
算术操作符(算数运算符) + -- * / %
/ 除 //低类型会自动转换为高类型参与运算。
printf("%f\n", 5.655 / 9); >>0.628333 //双方需有1个小数类型
% 取余数 //参与运算的2个数必须是整型或字符型,负数求模的规则是,结果的正负号由第一个运算数的正负号决定
//printf("%d\n", 11 % -5); >> 1
//printf("%d\n",-11 % -5); >> -1
a = a+3; >> a += 3;
a = a-2; >> a -= 2;
(二进制补码)移位操作符: 参数只能是正整数,否则error
<<左移操作符,左移1位有乘2的效果
#include <stdio.h>
int main()
{
int a = 10;
int n = a<<1;
// <<左移规则:左边抛弃、右边补0
// 00000000 00000000 00000000 00001010 int a =10
// 00000000 00000000 00000000 00010100 int n =20
printf("n= %d\n", n);
printf("a= %d\n", a);
return 0;
}
>>> n= 20
>>> a= 10
>>右移操作符,右移1位有除2的效果
#include <stdio.h>
int main()
{
int a = -10;
int n = a>>1;
// <<算术右移:左边用原符号位填充,右边丢弃
// 10000000 00000000 00000000 00001010 int a = -10 (原码)
// 11111111 11111111 11111111 11110110 int a = -10 (补码)
// 11111111 11111111 11111111 11111011 int n = -10 (补码)
// 10000000 00000000 00000000 00000101 int n = -5 (源码)
printf("n= %d\n", n);
printf("a= %d\n", a);
return 0;
}
>>>>> n= -5
>>>>> a= -10
位操作符 (操作数必须是整数)
& | 按位与 | 见0则0,全1则1 |
| | 按位或 | 见1则1,全0则0 |
^ | 按位异或 | 相同为0,不同为1 |
~ | 按位取反 |
#include <stdio.h>
int main(){
int num1 = -7;
// 10000000 00000000 00000000 00000111 int num1 = -7 (原码)
// 11111111 11111111 11111111 11111001 int num1 = -7 (补码)
int num2 = 6;
// 00000000 00000000 00000000 00000110 int num2 = 6 (补码)
int yu = num1 & num2;
int huo = num1 | num2;
int yihuo = num1 ^ num2;
printf("%d\n", yu);
// 00000000 00000000 00000000 00000000 int yu = 0 (补码、反码)
printf("%d\n", huo);
// 11111111 11111111 11111111 11111111 int huo = (补码)
// 10000000 00000000 00000000 00000001 int huo = -1 (原码)
printf("%d\n", yihuo);
// 11111111 11111111 11111111 11111111 int yihuo = (补码)
// 10000000 00000000 00000000 00000001 int yihuo = -1 (原码)
printf("%d\n", ~0);
// 11111111 11111111 11111111 11111111 int ~0 = (补码)
// 10000000 00000000 00000000 00000001 int ~0 = -1 (原码)
return 0;
}
>>> 0
>>> -1
>>> -1
>>> -1
关系运算符
a == b; //一个=是赋值操作,2个==才是判断
a != b; //返回0为flase 或1为true
a < b;
a > b;
a <= b;
a >= b;
<作为运算结果: true 1 false 0 。在判断变量是否为真时: 非0值为true ,0值为false
<多个关系运算符不宜连用
单目操作符
!、++、--、&、*、+、-、~ 、sizeof、(类型)
& | 取地址 |
* | 解引用 |
前置-- (先--后使用)
int a = 10;
int b = --a; //先a = 9 ,后赋给b
printf("a=%d b=%d\n",a , b); >> 9 9
后置-- (先使用后--)
int a = 10;
int b = a--; //先a = 10赋给b ,后a -1= 9
printf("a=%d b=%d\n",a , b); >> 9 10
printf("%zd\n", sizeof(long long)); //类型长度为8
int a = (int)3.14; // 将double型3.14强制类型转换为int类型,会丢失数据
逗号表达式
从左向右依次执行。整个表达式的结果 是最后一个表达式的结果
int main(){
a = get_val();
count_val(a);
while (a > 0){
//程序处理...
a = get_val();
count_val(a);
}
//先判断,后处理
用逗号表达式,改写为:
while (a = get_val(), count_val(a), a>0)
//先处理 exp1 和 exp2 后判断;若 a的值 >0才返回
{
//程序处理...
}
}
条件操作符
b = a>5 ? 3:-3;
//为真就3,为假救-3
逻辑操作符: && (与) ||(或) !(非)
判读是否为闰年
int year = 0;
scanf("%d", &year);
if((year%4==0 && year%100!=0) || (year%400==0))
printf("是闰年\n");
赋值操作符= 等号左侧必须为变量,不能是常量&表达式
强制类型转换符 (int) qure 得到的是一个中间值,对原变量没有影响。
结构体成员访问操作符 . ->
结构体变量 . 成员名
结构体指针 -> 成员名
定义结构体
#include <stdio.h>
struct Student {
char name[20];
int age;
char sex[5];
};
struct Student s2 = { "覃靠谱",19,"女" };struct Student s7 = { .age=18, .name="lisa", .sex="女" };
int main() {
printf("姓名为 %s 的性别是 %s \n", s2.name, s2.sex);struct Student* ps = &s2;
printf("姓名为 %s 的年龄是 %d \n", (*ps).name, (*ps).age);
printf("姓名为 %s 的性别是 %s \n", ps->name, ps->sex);
return 0;
}>>>姓名为 覃靠谱 的性别是 女
>>>姓名为 覃靠谱 的年龄是 19>>>姓名为 覃靠谱 的性别是 女
sizeof操作符 和 strlen函数的对比
#include <stdio.h>
int main() {
char arr2[] = "lmn"; //隐藏着 "lmn\0";
char arr1[] = { 'a', 'b', 'c' };
printf("%zd\n", strlen(arr1)); //找不到\0就会越界,随价值
printf("%zd\n", strlen(arr2)); //3 Byte
printf("%zd\n", sizeof(arr1)); //3 Byte
printf("%zd\n", sizeof(arr2)); //4 Byte
return 0;
}
sizeof | strlen |
1. sizeof是操作符 2. 计算操作数所占内存的大小,单位是字节 3. 不关注内存中存放什么数据,也不计算其中的值 | 1. strlen是库函数,使用需要包含头文件string.h 2. 求字符串长度的,统计的是\0 之前字符的个数 3. 关注内存中是否有\0 ,如果没有\0 ,就会持续往后找,可能会越界 |
短路:
仅根据左操作数的结果就能知道整个表达式的结果,不再对右操作数进行计算
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++||++b||d++;
// 0 3
// true
//a++为0 往右算++b为3 ;前2个结果为true不再执行后面内容(未执行d++)
printf("i =%d a = %d\n b = %d\n c = %d\nd = %d\n", i, a, b, c, d);
return 0;
}
>>1 1 3 3 4
计算中不同类型的转换
寻常算术转换:多个操作数属于不同的类型,将低字节类型转换为高字节类型后计算
^ long double
| double
| float
| unsigned long int
| long int
| unsigned int
| int
优先级
计算表达式时,依然有可能 不能通过操作符的属性 确定唯一的计算路径,不同的编译器计算顺序顺序无法通过操作符的优先级确定。
VS2022中
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
//先执行3次 ++i ;后4+4+4=12
printf("%d %d\n", ret,i);
return 0;
}
>>> 12 4
gcc中
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
//先执行2次 ++i 3 + 3
//先前2个操作数相加,后第三个 ++i ,再执行最后一个操作数 6 + 4 =10
printf("%d %d\n", ret,i);
return 0;
}
>>> 10 4
printf (格式,变量列表)
printf("%5d\n", 123); >>_ _123 //指定最小宽度,左边补空格,右对齐
printf("%lf\n", 3.1648964); >>3.164896 //6位小数
printf("%10.4lf\n", 3.1648964); >>_ _ _ _3.1649 //10位宽,指定4小数位数,四舍五入
<<==>>printf("%*.*lf\n",10,4,3.1648964);
printf("%+d\n", 12); >> +12
printf("%+d\n", -12); >> -12 //自动带正负号
printf("%.5s\n", "alehuodha"); >>alehu //输出部分字符串
printf返回打印在屏幕上的字符的个数 :
#include <stdio.h>
int main() {
printf("-%d", printf("~%d", printf("%d", 43)));
// 打印43,在屏幕上打印"43"这2个字符,返回2
// 在屏幕上打印 “~2” 这2个字符,返回2
// 在屏幕上打印 “-2” 这2个字符,返回2
return 0; }
>>43~2-2
Scanf (格式,&地址列表)
变量前面必须加上& 运算符(指针变量、数组除外)
返回值是一个整数,表示成功读取的变量个数。如果没有读取任何项,则返回0
按ctrl+z ,提前结束输入
%d%d%d没有规定分隔符,则默认用空格、Enter、Tab作为分隔符,不能用逗号作分隔符。、
%d%c%d不能用 空格、Enter、Tab作为分隔符,会被%c读取为字符造成逻辑错误。
scanf() 输入float型数据;用%f格式控制 | scanf() 输入double型数据;用%lf格式控制 |
printf() 输入float型数据;用%f格式控制 | printf() 输入double型数据;用%f格式控制 |
if分支
在没有大括号的时候,if后边只能有一条语句。
有多个if …else…嵌套时,else 总是跟最近的、未配对的if 匹配。因此注重大括号的作用!
上代码什么都不打印,改进后为:
#include <stdio.h>
int main()
{
int a = 0;
int b = 2;
if(a == 1)
{
if(b == 2)
printf("hehe\n");
}
else
printf("haha\n");
return 0;
}
>>haha
switch分支
switch语句中的default子句可以放在任意位置,也可省略
case语句最好放在default之前,且case后的表达式只能是整形常量表达式
int func(int a)
{
int b;
switch (a)
{
case 1: b = 30;
case 2: b = 20;
case 3: b = 16;
default: b = 0;
}
return b; }
>>0
/*
该switch语句中所有分支下都没有增加break语句,
因此会从上往下顺序执行,最后执行default中语句返回。
*/
深入解读switch
switch (expression) //expression必须是int\char表达式
{
case value1: statement
case value2: statement
default: statement //值无法匹配每个case的时候,执行default
}
/*根据表达式expression 不同的值,执行相应的case 分支。
如果找不到对应的值,就执行 default 分支
case 和value之间必须有空格。且每个分支的value互不相同。
case后的常量为整形或字符型,且类型&大小要和switch括号中的值相同
每一个case 语句中的代码执行完成后,若要跳出这个switch语句,需加break 。
*/
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
switch(n%3)
{
case 0:
printf("整除,余数为0\n");
break;
case 1:
printf("余数是1\n");
break;
case 2:
printf("余数是2\n");
break;
}
return 0; }
#include <stdio.h>
int main() {
int day = 0;
scanf("%d", &day);
switch(day)
{
case 1:
case 2:
case 3:
case 4:
case 5:
printf("工作日\n"); //一个case分支中没有任何语句,表示这一情况与下一情况采用同一处理方式
break;
case 6:
case 7:
printf("休息日\n");
break;
}
return 0; }
while循环
条件表达式的执行次数总是比循环体的执行次数多一次
//例题:逆序打印(分离倒序数码)
int main()
{
int n = 4131;
while(n)
{
printf("%d ", n%10);
n /= 10; //只取整数部分
}
return 0;
}
>>1 3 1 4
#include <stdio.h>
int main()
{
int i = 1;
while(i<=10)
{ if(i == 5)
break; //当i等于5后,就执行break,循环终止了
printf("%d ", i);
i = i+1; }
return 0; }
>>1 2 3 4
#include <stdio.h>
int main()
{ int i = 0;
while (i < 10)
{
i++;
if (i == 5)
continue; //当i等于5时,直接跳过continue的(i=5)转入执行语句i++
printf("%d ", i); }
return 0; }
>>1 2 3 4 6 7 8 9 10
for循环
for(初始化; 结束条件的判断; 循环变量的调整) 用于循环次数确定的情况
只执行一条 循环语句;
//初始化-条件判断为true-执行循环语句-变量调整-条件判断…………条件为flase结束
#include <stdio.h>
int main()
{
int i = 0;
for (i = 1; i <= 10; i+=4)
{
printf("%d~", i);
}
return 0; }
>>1~5~9~
//计算1~100之间3的倍数的数字之和
#include <stdio.h>
int main()
{
int i = 0;
int sum = 0;
for (i = 3; i <= 10; i += 3) //接产生3的倍数
{
sum += i; //sum =sum + i;
}
printf("%d\n", sum);
return 0;}
>>18 //3+6+9=18
do while循环
//先执行语句,后判断while
//至少执行一次循环语句,在判断
#include <stdio.h>
int main()
{ int n = 0;
scanf("%d", &n);
int count = 0;
do {
count++;
n = n / 10;
} while (n);
//count n
//0 123 (初始时)
//1 12
//2 1
//3 0
printf("%d\n", count);
return 0; }
找出100~200之间的素数
#include <stdio.h>
int main()
{ int i = 0;
for (i = 100; i <= 200; i++) //循环产生100~200的数字
{
//判断 i是否为素数
int j = 0; //初始化
int flag = 1; //默认 i是素数
for (j = 2; j < i; j++) //j =1不行,任何数都可整除1就都不是素数
{
if (i % j == 0)
{ flag = 0; //不是素数,就break结束
break;
}
}
if (flag == 1) //是素数
printf("%d ", i);
}
return 0; }
goto跳转语句
实现在同一个函数内跳转到标号点。
#include <stdio.h>
int main()
{
again:
printf("\a\n");
goto again; //连续发出win警告
return 0;
}
优势:一次跳出多层嵌套循环,如简化3个break 才能跳出循环
¥关机项目
/*
shutdown
-s 关机
-t 关机倒计时(秒)
-a 取消关机
system (stdlib.h)
strcmp (string.h)
格式:strcmp(str1,str2)==0 则为true
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int main()
{
int b;
int n = 7;
for (b = 1; b < n; b++)
{
printf("\a\n");
printf("公主看这里!");
}
begin:
printf("\a");
system("shutdown -s -t 100");
printf("\n正在支配电脑100秒后关机,请输入英文版'LVy'表示'我喜欢你'进行取消关机\n");
char ywb[3];
scanf("%s", ywb);
printf("\a");
if (strcmp("LVy", ywb) == 0)
{
system("shutdown -a");
printf("我也喜欢你勒\n哈哈哈哈\n");
}
else goto begin;
printf("\a");
return 0;
}
¥猜数字游戏项目
#define _CRT_SECURE_NO_WARNINGS //D:\VS\IDE\Common7\IDE\VC\VCProjectItems(原文件)
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void game() //猜数字环节
{
int r = rand() % 100 + 1; //需要<stdlib.h>
/*
rand() % 100; //生成0~99
rand() % 100 + 1; //生成1~100之间的随机数
100 + rand() % (200 - 100 + 1) //生成100~200的随机数
a + rand() % (b - a + 1) //生成a~b的随机数
*/
int guess = 0;
int count = 5; //机会次数
while (count)
{
printf("你还有%d次机会\n", count);
printf("请猜数字>");
scanf("%d", &guess);
if (guess < r)
{
printf("猜小了\n");
}
else if (guess > r)
{
printf("猜大了\n");
}
else
{
printf("恭喜你,猜对了\n");
break;
}
count--; //跳出来后自减1
}
if (count == 0) //为0时结束游戏
{
printf("你失败了,正确值是:%d\n", r);
}
}
void menu()
{
printf("***********************\n");
printf("****** 1. play ******\n");
printf("****** 0. exit ******\n");
printf("***********************\n");
}
int main() //选择游戏模式
{
int input = 0;
srand((unsigned int)time(NULL)); //利用时间戳生成随机数序列
/* 在调用 rand 函数之前先调用 srand 函数,
通过 srand 函数的参数结果来设置rand函数生成随机数的时候的序列,
只要序列在变化,每次生成的随机数也就变化起来了*/
/* time(NULL);需要<time.h >
调用time函数返回时间戳,提供不同的种子值。
这里没有接收返回值,只返回这个时间的差值*/
do {
menu(); //调用meau函数
printf("请选择>");
scanf("%d", &input);
switch (input)
{
case 1:
game(); //调用game函数
break;
case 0:
printf("游戏结束\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
}
while (input);
return 0;
}
字符串&普通字符数组的区别
字符串 | 无法通过指针或下标修改某个字符,存储在内存的只读数据段,存储时自动在末尾添加\0 | |
普通字符数组 | 元素不能修改 | 不能整体输入输出 |
字符串数组 | 存储在栈区且多1个“\0”的Byte,是可以通过指针或下标对某个字符修改。 | 且"\0"前的字符串可以整体输入输出 |
注:当输入的字符串含有空格,输入语句会在空格处结束输入。用gets("字符 串“);把空格当作字符串的组成部分
一维数组 array
• 数组中存放的是>=1个数据,但是数组元素个数不能为0。
• 数组中存放的多个数据,类型是相同的。
• 数组名对应首元素地址。scanf("%s", array); 不用加地址符号”&“
• 数组在逻辑结构&物理存储中是连续、线性的。且C语言采用行优先得存储方式。
int math[20]; //存储某个班的20人的数学成绩
int arr[5] = {1,2,3,4,5}; //完全初始化
double arr2[6] = {1.5}; //不完全初始化 ,第一个元素初始化为1.5,剩余元素默认初始化为0.0
int arr3[3] = {1, 2, 3, 4}; //错误的初始化 ————初始化项太多
< 一维数组下标引用操作符:从0开始的,假设数组有n个元素,最后一个元素的下标是n-1
printf("%d",arr[7]); //输出的数为8
< 一维数组和二维数组 在内存中都是连续存放的 :int 数组类型的每两个相邻的元素之间地址相差4,由小到大。
#include <stdio.h>
int main()
{
int arr[] = { 1,2,(3,4),5 };
/*
里面总共有4个元素,(3,4)为逗号表达式,取后者 4,
因此数组中元素分别为:1,2,4,5
*/
printf("%d\n", sizeof(arr));
//sizeof(arr)求的是整个数组所占空间的大小,即:4*sizeof(int)=4*4=16
return 0;
}
>>16
printf("%zd\n", sizeof(int)); >>4
手动输入输出数组
#include <stdio.h>
int main() {
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int i = 0;
for(i=0; i<10; i++) //给数组输入想要的数据
{
scanf("%d", &arr[i]); //作为数组元素时需要取 &地址
}
for(i=0; i<10; i++) //使用for循环输出0~9的下标
{
printf("%d ", arr[i]);
}
return 0; }
计算数组元素个数:sizeof
数组所占内存空间,单位是byte
#include <stido.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17};
int sz = sizeof(arr)/sizeof(arr[0]); //计算出数组的元素个数
/*
sizeof(arr) 计算的是 数组所占内存的总空间,单位是byte
sizeof(arr[0]) 计算一个元素所占的存储空间,单位是byte
*/
for( int i =0; i <sz; i++)
{
printf("%d ", arr[i]); //循环打印数组元素
}
return 0;
}
数组越界
(以下代码是vs2022,debug ,x86环境下)
int main()
{
int i = 0;
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
for(i=0; i<=12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
>>>hehe(死循环打印ing)
越界的数组也赋值为 0
cause { i 和 arr 数组之间恰好空出2个int 的存储空间 ;arr[12] = 0 时覆盖 i 的值,导致 i 一直循环 } (环境不同内存使用细节也不同)
solve: {先赋值arr[i]再赋值 i 就不会覆盖}
:
:
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int i = 0;
:
:
二维数组
初始化时,一维数组可以省略数组大小 ;二维数组可以省略行,但是不能省略列
如 int arr[][5] = { {1,2}, {3,4}, {5,6,7,8,9} };
二维下标引用操作符:行号与列号也是从0开始的
#include <stdio.h>
int main()
{
int arr[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7};
printf("%d\n", arr[2][4]);
return 0;
}
>> 7
#include <stdio.h>
int main()
{
int arr[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7};
//输入
int i = 0;
for(i=0; i<3; i++) //产生行号
{
int j = 0;
for(j=0; j<5; j++) //产生列号
{
scanf("%d", &arr[i][j]); //输入数据
}
}
/*
先第0行输入5个值到第4列跳出“ j ”的循环
先第1行输入5个值到第4列跳出“ j ”的循环
先第2行输入5个值到第4列跳出“ j ”的循环
*/
//输出
for(i=0; i<3; i++) //产生3个行号
{
int j = 0;
for(j=0; j<5; j++) //产生5个列号
{
printf("%d ", arr[i][j]); //输出数据
}
printf("\n"); //每行满5个值后换行
}
return 0; }
变长数组长度只有运行时才能确定,所以变长数组不能初始化
int n = 0;
scanf("%d", &n);//根据输入数值确定数组的大小
int arr[n];
数组练习
#include <stdio.h>
#include <stdlib.h> //cls函数
#include <string.h> //strlen函数
#include <windows.h> //Sleep函数
int main() {
char arr1[] = "welcome to qhgdx";
char arr2[] = "#################";
int left = 0; //左起始上标
int right = strlen(arr1) - 1; //右起始下标
printf("%s\n", arr2);
while (left <= right) //保证在中心结束循环
{
Sleep(1000); //优先每次休眠1000毫秒,有停顿
system("cls"); //后每次清屏
arr2[left] = arr1[left];
arr2[right] = arr1[right];
left++;
right--;
printf("%s\n", arr2);
}
printf("%s\n", arr2); //打印最终结果
return 0; }
>>
#################
w###############x
we#############dx
wel###########gdx
welc#########hgdx
welco#######qhgdx
welcom##### qhgdx
welcome### qhgdx
welcome #o qhgdx
welcome to qhgdx
函数
各类库函数说明:https://zh.cppreference.com/w/c/header
loadimage( &img, 文件路径, 宽, 高,是否自适应); //加载图像<graphics.h>
double pow( double x, double y ); //计算x的y次幂<math.h>
int rand(); //生成随机数<stdlib.h>
void srand(usigned int seed); //产生不同的随机数序列<stdlib.h>
数据类型必须时整形,默认为srand(1);
time_t time(time_t * timer); //<time.h>
当timer赋值为NULL时,返回当前时间到1970-01-01 00:00:00的秒数
当timer赋值为 0 时,每秒会得到不同的随机数序列
getchar( ); //实现程序的暂停<stdio.h>
暂停程序,等待用户输入任意键后+Enter结束此函数
fflush( stdin); //清空当前的键盘缓冲区<string.h>
getche <conio.h> getch <conio.h> getchar <stdio.h> 从键盘接收一个字符并显示出来 从键盘接收一个字符但不会显示出来 即便没有键盘的输入,也要从stdin流中读入一个字符
system( ); //发出一条DOS命令<stdlib.h>
system(”pause"); 冻结屏幕
system("CLS"); 清屏操作
void Sleep(毫秒数); //让程序进程挂起一段时间<windows.h>
sleep(7000); 暂停7秒
自定义函数,用于复用,功能要单一
执行程序过程中,遇到函数调用,先计算各实参的值,再传递给对应形参;而后顺序执行函数体内部语句。函数执行结束返回调用点,继续执行后面语句,最终返回main()函数的调点;当main()函数结束,整个程序执行结束。
#include <stdio.h>
int Add(int x, int y) //函数头部包括(返回值类型、函数名、形参类型、形参名称)
{
return x+y;
}
int main() {
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int r = Add(a, b); //把a+b的值赋值给 r
printf("%d\n", r);
return 0; }
函数参数的“单向值传递”基本原则
只能由实参的值复制一份给形参,不能反过来。这样执行时对形参修改,而对实参没有影响。
通过形参指针变量可以修改main()函数中的普通变量。因为形参的地址变,实参的地址也跟着变。
函数名也代表内存空间中的起始地址。则可以定义一个指针变量指向函数,通过它就能调用这个函数。且指向函数的指针变量不做加减运算。
<只是形式上存在的,不会向内存申请空间,不会真实存在的,====形参
<函数被调用时,为了存放实参传递过来的值,才向内存申请空间 ===实参
<调用函数时,形参和实参的个数&类型要对应,
<形参操作的数组和实参的数组是同一数组
<形参和实参都有自己的内存地址空间,地址是不一样的。
int main( int argc , char * argv[ ], char * envp[ ] );
数组长度 | |
命令行上每个字符串的首地址 |
当前系统中定义的每个环境的信息
return 语句
<return后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回。 |
<return后边为 null ,适合返回类型是void的函数 |
<return返回的值和函数返回类型不一致,系统会自动将转换为函数的返回类型 |
<return语句执行后,函数就彻底返回,后边的代码不再执行 |
<函数中存在if等分支的语句,则要保证每种情况下都有return返回,否则会出现编译错误(判断语句为真为假 都要考虑return) |
<函数的返回类型没有指定时,编译器默认返回 int类型 |
<一个函数中可以有多个return语句,但只有一个return语句起作用。 |
<函数中没有 return语句,返回值就不确定了。如果不需要返回值,就把函数定义为void类型。 |
函数嵌套使用
#include <stdio.h> //计算某年某月有多少天
#include <stdbool.h>
bool is_leap_year(int y) {
if(((y%4==0)&&(y%100!=0))||(y%400==0)) //判断是否为闰年
return true;
else
return false; }
int get_days_of_month(int y, int m) {
int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// 月份 1 2 3 4 5 6 7 8 9 10 11 12
int day = days[m]; //得到每月的天数
if (is_leap_year(y) ==true && m == 2)
day += 1;
return day; }
int main() {
int year = 0;
int month = 0;
scanf("%d %d", &year, &month);
int d = get_days_of_month(year, month);
printf("%d\n", d);
return 0; }
<函数之间可以嵌套调用,但不能调用main()函数,也不能嵌套定义函数
<C语言编译器对源代码进行逐行编译,函数先声明后使用 :
#应先定义函数,后调用函数 ;
#或者声明函数(包括返回类型、函数名称、形参类型、形参名称),调用函数,定义函数
VS调试
程序中存在的问题 ==bug
找出并解决问题的过程叫称为调试,英文叫debug(消灭bug)针对程序的逻辑错误进行检查和纠正,即差错和排错。
Debug (调试版本) |
包含调试信息,并且不作任何优化,这个版本是便于程序员直接调试程序 |
Release (发布版本) |
进行了各种优化,使得程序在代码大小和运行速度上都是最优的,这个版本是用户使用的,无需包含调试信息 |
>开始执行(不调试) Ctrl +F5 [调试] [工具栏-空心小三角] :
程序一直运行下去,结束后不会自动关闭命令提示符窗口,便于观察结果,任意键关闭窗口;相当于程序结束前多执行了一条 system( "pause" ); 语句。
>启动调试F5 :
直接跳到下一个断点处; 若源文件中没有设置断点或暂停语句,窗口会一闪而过
>(创建、取消) 断点 [F9、切换断点] 红色圆饼状
执行程序F5后,系统对断定之前的语句片段连续执行,遇到断点会自动停下来等待程序员调试
>逐过程单步执行 [F10 、调试-逐过程](最常用、最主要) 不进入函数内部,而是把函数当一个整体一步执行完成。从main函数开始执行,不断按F10键,程序一条一条地往下执行,黄色箭头位置不断变化。
>逐语句单步执行 [F11 、调试-逐语句] 遇到函数调用时要进入函数体内部,执行函数体内部每条语句的细节。但不适合调用库函数,因为库函数模块体量复杂
>结束单步执行 [Shift +F11 、调试-跳出]
监视窗口 | 内存 | 调用堆栈 |
F10调试后,【调试】->【窗口】->【监视】 观察(变量、表达式)的值变化 | F10调试后,【调试】->【窗口】->【内存】 观察内存中的值变化(地址、数据、解析) | Alt +7 【调试】->【窗口】->【调用堆栈】 查看函数之间的调用信息 |
函数的 递归&迭代
通过main()函数调用其他函数,其他函数间可以互相调用,但程序必须回到main()函数中结束。
函数自己调用自己来解决问题,就是把复杂运算==>少量代码的过程;
每一次函数调用,会在内存中建立栈区;递归层次越深,冗余计算就会越多;
先递推后回归。
若_无限递归调用函数===>栈溢出(Stack overflow)
规范_每次递归调用之后越来越接近某个限制条件,当满足限制条件的时候,递归便结束
用递归求n的阶乘:
#include <stdio.h>
int Fact(int n)
{
if(n==0)
return 1; // 0! = 1
else if(n > 0)
return n*Fact(n-1); // n! =n * (n-1)!
} //只定义 >=0的数 ,防止无限递归调用函数
int main()
{
int n = 0;
scanf("%d", &n);
int result = Fact(n);
printf("%d\n", result);
return 0;
}
用迭代求n的阶乘:
#include <stdio.h>
int Fact(int n){
int i = 0;
int ret = 1;
for(i=1; i<=n; i++)
{
ret *= i; //循环累乘
}
return ret;
}
int main(){
int n = 0;
scanf("%d", &n);
int result = Fact(n);
printf("%d\n", result);
return 0;
}
指针pointer (内存单元( 1个byte)的编号 == 地址 == 指针)
CPU和内存之间大量的数据交互用总线线连起来
CPU==通过AddressBus把内存单元编号==>内存==通过DataBus把内存单元数据==>CPU
内存中每个字节(内存单元)都有编号;编译器通过 地址找内存单元,而不是在内存中找变量名
• 32位平台下地址是32个bit,指针变量大小=4个Byte
• 64位平台下地址是64个bit,指针变量大小=8个Byte
& 取地址操作符
取出int类型所占4个字节中地址较小的字节地址。
指针变量:存储指针地址的变量。因为是变量,则可以自增自减运算。
* 解引用操作符
< 地址解引用==>得到一个值
#include <stdio.h>
int main() {
char a =' h ';
char * pa = &a; //解引用:取出a的地址并存储到指针变量pa中//必须指明指针变量指向的数据类型,才能确定指针运算时所需的内存大小
*pa = 'u';
//用pa中存放的地址,找到其指向的空间,*pa其实就是a变量了;
//所以*pa ='u',这个操作符是把a改成了u.
return 0;}
*pa == *&pa == a
< int * 的解引用操作符就能操纵4个Byte,而char * 的解引用操作符就只能操纵1个Byte
#include <stdio.h> #include <stdio.h>
int main() int main()
{ {
int n = 0x11223344; int n = 0x11223344;
int * pi = &n; char * pc = (char *)&n;
*pi = 0; *pc = 0;
return 0; return 0;
} }
//把4个地址字节全部改为0 //只把第1个地址字节改为0
void * 指针
接受任意类型地址(不能直接进行 指针的+ -计算 & 解引用运算)
在不知道接受何种类型时使用,用于泛型编程
const 限制指针
修饰变量就变为 不可修改变量
#include <stdio.h>
int main() {
int m = 0;
m = 20; //m是可以修改的
const int n = 0;
n = 20; //n是不能被修改的,会报错
const int e = 0;
printf("e = %d\n", e); // e = 0
int * p = &e; //用 *解引用操作 符打破const规则,通过 p 改 e
*p = 20;
printf("e = %d\n", e); // e = 20
return 0;
}
//const修饰 p 指针变量,限制的是指针所指向的内容,不能修改指针指向的内容
//但是指针变量本身的 (地址)时可改变的
void test2() {
int n = 10;
int m = 20;
const int * p = &n; //锁死变量
*p = 20; // 变量报错
p = &m; // 可改地址
}
//const修饰 p 地址,限制的是指针变量(地址)本身,不能修改指针变量的指向
//但可以通过指针改变 指向的内容,
void test3() {
int n = 10;
int m = 20;
int * const p = &n; //锁死地址
*p = 20; // 可改内容
p = &m; // 地址报错
}
指针运算
指针—整数
#include <stdio.h>
int main() {
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0]; //指向数组第1个元素的地址 ( *p =1 )
int sz = sizeof(arr)/sizeof(arr[0]);
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i)); //步长=4 Byte (int)
}
return 0;
}
>>>1 2 3 4 5 6 7 8 9 10
//数组要连续存放才能, 指针+1才能顺序打印
//指针变量的类型 决定了指针+1的步长、解引用的权限
指针—指针
<(指向同一块内存空间的2个指针)其 | 指针低地址—指针低地址 | = 指针地址之间元素的个数
int main() {
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf( "%d ", &arr[0] - &arr[9] );
return 0;
}
>>> -9 // 9 (sz -1)个元素
求字符串的个数
#include <stdio.h>
#include <assert.h>
size_t slen(char * n) //size_t 返回无符号整数
{
assert(n != NULL); //防止输入空字符串,否则error
char* start = n; //arr[0]的地址
char* end = n;
while (*end != '\0') //取到\0的低地址结束
{
end++; //取到arr[5]的地址
}
return end - start; // | \0的低地址 - arr[0]=a的地址 | =之间的元素个数
}
int main() {
char arr[] = "adcdef\0";
size_t length = slen(arr); //数组传参时slen(arr) = slen(arr[0])
printf("%zd\n", length);
return 0;
}
指针比较大小
#include <stdio.h>
int main()
{
int arr[7] = { 1,3,1,4,5,2,0 };
// [i] 0 1 2 3 4 5 6
int * p = &arr[0]; //把 1的地址存入p变量中,且( *p =1)
int sz = sizeof(arr) / sizeof(arr[0]); //有7个元素
while (p < &arr[sz]) // p的低地址 < arr[7]的低地址
{
printf("%d ", *p);
p++; //步长为4个Byte逐个访问arr的地址
}
return 0;
}
>>> 1 3 1 4 5 2 0
野指针
int * p2 = NULL; //指向Null的指针就丢失了写入权限,就保障准确性
< 指针变量不再使用后,及时置为NULL ;指针使用之前检查是否是NULL
assert 断言=assert 宏 (需要 <assert.h> )
<在运行时判断是否符合条件,如果不符合,就报错终止运行,并给出报错信息提示
如果已经确认程序没有问题,不需要再做断言,就在#include <assert.h> 语句的前面,定义一个宏NDEBUG
#define NDEBUG
#include <assert.h>int main() {
int m =20;
int * p = null;
assert( p != NULL); //会报错提示
return 0;
}
<assert() 的缺点:因为引入了额外的检查,增加了程序的运行时间;Release 版本中,assert()直接就是优化掉
传址调用
让次函数和主调函数之间建立真正的联系,在次函数内部可以修改主调函数的变量
把变量的地址传递给了函数===传址调用
#include <stdio.h>
void Switch(int *px, int *py) //传入地址
{
int z = 0;
z = *px; // z= a
*px = *py; //x = y
*py = z; //y = z
}
int main() {
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Switch(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
>>>8 6
>>>交换前:a=8 b=6
>>>交换后:a=6 b=8
<次函数中只想需要主函数中的变量值来实现计算,就可用传值调用。
<如果次函数内部要修改主调函数中的变量,就需要传址调用。
指针与数组
数组指针==>数组的地址
整形数组
#include <stdio.h>
int main() {
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));
//数组名单独放入sizeof中单独放,表示整个数组的字节大小 16Byte
printf("%d\n", sizeof(a + 0));
// a+0 是首元素的地址 4Byte
printf("%d\n", sizeof(*a));
// *a 等价于 *(a+0) 等价于 a[0] ==>首元素的大小 4Byte
printf("%d\n", sizeof(a + 1));
// a是首元素地址,sizeof(a +1)是第二个元素的大小 4Byte
printf("%d\n", sizeof(a[1]) );
//第二个元素的值 4Byte
printf("%d\n", sizeof(&a));
//名为 a数组的地址 4Byte
printf("%d\n", sizeof(*&a));
// sizeof(*&a)=sizeof(a) 16Byte
printf("%d\n", sizeof(&a + 1));
//另一个数组的地址 4Byte
printf("%d\n", sizeof(&a[0]));
//第一个元素的地址 4Byte
printf("%d\n", sizeof(&a[0] + 1));
//第二个元素的地址 4Byte
}
字符数组
#include <stdio.h>
#include <string.h>
int main() {
//字符数组
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr)); // 6 Byte
printf("%d\n", sizeof(arr + 0)); //首元素的地址 4Byte
printf("%d\n", sizeof(*arr)); //1Byte
printf("%d\n", sizeof(arr[1])); //1Byte
printf("%d\n", sizeof(&arr)); //4Byte
printf("%d\n", sizeof(&arr + 1)); //4Byte
printf("%d\n", sizeof(&arr[0] + 1)); //4Byte
printf("%d\n", strlen(arr)); //随价值
printf("%d\n", strlen(arr + 0)); //随机值
//printf("%d\n", strlen(*arr)); //bug:会找不到首元素a=97的地址值,崩溃
//printf("%d\n", strlen(arr[1])); //bug:会找不到第二个元素b=98的地址值,崩溃
//printf("%d\n", strlen(&arr)); //bug:找不到地址值
//printf("%d\n", strlen( &arr + 1)); //bug:找不到地址值
printf("%d\n", strlen(&arr[0] + 1)); //随价值
//字符串
char arr[] = "abcdef";
printf("%d\n", sizeof(arr)); // 7 Byte
printf("%d\n", sizeof(arr + 0)); // arr+0 是首元素的地址 4Byte
printf("%d\n", sizeof(*arr)); //*arr 等价于 *(arr+0) 等价于 arr[0] 首元素的大小1Byte
printf("%d\n", sizeof(arr[1])); //第二个元素的大小 1Byte
printf("%d\n", sizeof(&arr)); //名为 arr数组的地址 4Byte
printf("%d\n", sizeof(&arr + 1)); //另一个数组的地址 4Byte
printf("%d\n", sizeof(&arr[0] + 1)); //第二个元素的地址 4Byte
printf("%d\n", strlen(arr)); //从第一个元素统计\0之前的字符数 6Byte
printf("%d\n", strlen(arr + 0)); // arr + 0 是本身数组的大小 6Byte
//printf("%d\n", strlen(*arr)); //bug:会找不到首元素a=97的地址值,崩溃
//printf("%d\n", strlen(arr[1])); //bug:会找不到第二个元素b=98的地址值,崩溃
//printf("%d\n", strlen(&arr)); //bug:找不到地址值
//printf("%d\n", strlen(&arr + 1)); //bug:找不到地址值
printf("%d\n", strlen(&arr[0] + 1)); //从第2个统计\0之前的字符数 5Byte
//字符指针
const char* p = "abcdef";
printf("%d\n", sizeof(p)); //指针变量p的地址 4Byte
printf("%d\n", sizeof(p + 1)); //第2个元素的地址 4Byte
printf("%d\n", sizeof(*p)); //只能访问首元素的大小 1Byte
printf("%d\n", sizeof(p[0])); //p[0] 等价于 *(p+0) 等价于 *p 1Byte
printf("%d\n", sizeof(&p)); //变量自己的地址 4Byte
printf("%d\n", sizeof(&p + 1)); //后1个变量的地址 4Byte
printf("%d\n", sizeof(&p[0] + 1)); //第2个元素 b的地址 4Byte
printf("%d\n", strlen(p)); //从第1个元素统计\0之前的字符数 6Byte
printf("%d\n", strlen(p + 1)); //从第2个元素统计\0之前的字符数 5Byte
//printf("%d\n", strlen(*p)); //bug:会找不到首元素a=97的地址值,崩溃
//printf("%d\n", strlen(p[0])); //bug:会找不到第二个元素b=98的地址值,崩溃
//printf("%d\n", strlen(&p)); //bug:找不到地址值
//printf("%d\n", strlen(&p + 1)); //bug:找不到地址值
printf("%d\n", strlen(&p[0] + 1)); //从第2个元素统计\0之前的字符数 5Byte
}
二维数组
#include <stdio.h>
#include <string.h>
int main() {
int a[3][4] = { 0 }; //连续的int(4 Byte)内存空间
printf("%d\n", sizeof(a)); //sizeof(数组名),是整个数组的大小 48 Byte
printf("%d\n", sizeof(a[0][0])); //第1个元素的大小 4 Byte
printf("%d\n", sizeof(a[0])); //第1组数组的大小 16 Byte
printf("%d\n", sizeof(a[0] + 1)); //第1组数组第2个元素的大小 4 Byte
printf("%d\n", sizeof(*(a[0] + 1)) ); // a[0][1]的大小 4 Byte
printf("%d\n", sizeof(a + 1)); // (a+0)为第1组数组的地址(a+1)为第2组数组的地址 4 Byte
printf("%d\n", sizeof(*(a + 1))); // *(a + 1) 等价于 a[1] 第2组int数组的大小 16 Byte
printf("%d\n", sizeof(&a[0] + 1)); //第2组数组的地址 4 Byte
printf("%d\n", sizeof(*(&a[0] + 1)) ); //第2组数组的地址解引用,就是第2组数组的大小 16 Byte
printf("%d\n", sizeof(*a)); // *(a+0)为第1组数组的地址解引用,就是第1组数组的大小 16 Byte
printf("%d\n", sizeof(a[3])); //越界,sizeof不会计算越界的内容;推断第4组数组的大小 16 Byte
}
除2个例外(整个数组的地址大小)==>
sizeof(数组名): 数组名单独放入sizeof中单独放,就表示整个数组的字节大小
&数组名: 取出的是 整个数组的地址
其余情况都是数组首元素的地址
printf(" &arrange[0] = %p\n", &arr[0]); 等价于 printf(" arrange = %p\n", arr);
用指针访问数组
#include <stdio.h>
int main() {
int arr[10] = {0};
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
//输入
int* p = arr; //取首元素地址
for(i=0; i<sz; i++)
{
scanf("%d", p+i); // 也可以这样写 scanf("%d", arr+i);
//需要输入值,而不是地址,就不需要解引用
}
//输出
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i)); // 也可以这样写 printf("%d ", p[i]);
//参数为变量,需要解引用,变为地址变量
}
return 0;
}
>>>6 6 9 1 36 45 5 1 6 8
>>>6 6 9 1 36 45 5 1 6 8
arr[ i ] 等价于 * ( arr+i )
p [ i ] 等价于 * ( p+i )
数组的元素 地址解引用==>得到一个元素
数组传参的本质
<一维数组传参 传递的是数组首元素的地址
<二维数组传参的本质:传递的是 首个一维数组的地址
指针数组:
<指针数组名 ==> 二级指针
<具有数组的特性,只是数组元素的类型是指针类型。指针数组存放的不是字符串本身,而是每个字符串的首地址
char arr [10] ;
char (*p) [10] = &arr;
| | |
| | |
| | *p指向数组的元素大小
| *p是数组指针变量名
*p指向的数组的元素类型
int ( *p ) [10]; // 加上()来保证*p是一个指针、指向一个大小为10个 char的数组,叫 数组指针
用指针数组创建 不连续二维数组
#include <stdio.h>
int main() {
int arr_1[] = { 1,2,3,4,5 };
int arr_2[] = { 2,3,4,5,6 };
int arr_3[] = { 3,4,5,6,7 };
int * arr[3] = { arr_1, arr_2, arr_3 };
//把每个数组首元素的地址,以类型为int*,就可以存放在 arr 数组中
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]); //等价于 printf("%d ", * (* (arr +i)+j) );
//须要解引用,取出的是值; 而不是地址
// *(arr +i) 等价于 arr[i]
}
printf("\n"); //每5个元素换行
}
return 0;
}
>>>1 2 3 4 5
>>>2 3 4 5 6
>>>3 4 5 6 7
二级指针
应用场景:须要把一级指针变量存起来时
*ppa 通过对ppa中的地址进行解引用得到的是pa
**ppa 先通过*ppa 解引用得到*pa ,然后对*pa 进行解引用得到的是a
**ppa == *pa == a
字符指针变量
#include <stdio.h>
int main() {
const char * pstr = "hello bit.";
printf("%s\n", pstr); //打印字符串
printf("%c\n", *pstr); // 把常量字符串的首字符h 的地址存放到指针变量pstr中。
return 0;
}>>>hello bit.
>>>h
char s[ ] = "I love you"; 用数组存储字符串,可修改数组中的字符。
char * t = "I love you"; 用字符指针指向字符串,存储的是字符串首地址,不可修改。而"I love you"存储在 t 所表示的那个首地址开始的一块连续内存中。
函数指针变量 【 int (*pf3) (int x, int y) 】
函数在内存中有存储空间,所以也是有地址的
#include <stdio.h>
int test ( int x ,int y ) {
return x + y ;
}
int main () {
printf ("test : %p\n", test);
printf ("&test: %p\n", &test);
//函数的取出地址都是一样的int (*pf3) ( int x, int y) = test; //或者 =&test 即把函数的地址存放起来
printf("%d\n", (*pf3)(5, 3) ); //用函数指针调用函数
printf("%d\n", test (3, 5) );
printf("%d\n", pf3 (3, 5) );
// ( *pf3 )== pf3 == test 结果都是 5+3=8return 0;
}>>>test : 009713CF
>>>&test: 009713CF
>>>8
>>>8
>>>8
int (*pf3) (int x, int y)
| | ------------
| | |
| | *pf3指向函数的参数类型和个数
| 函数指针变量名 *pf3
*pf3指向函数的返回类型
//为 pf3的 函数指针变量的类型 int (*) (int x, int y)
typedef 是用来重定义类型的
typedef unsigned int u_int; //将 unsigned int 重命名为 u_int
typedef int * p_int; //把 int * 重命名为 p_int
int main () {
u_int x; // 等价于 unsigned int x;
p_int x; // 等价于 int * x
}
数组和函数的 typedef
#include <stdio.h>
typedef int ( *p_arr ) [5]; //新的类型名必须在*的后面typedef int ( *p_fiction ) ( int , int ); //新的类型名必须在*的后面
int test ( int x ,int y ) {
return x + y ;
}int main () {
int arr[5] ={0};
p_arr arr9 = &arr; //等价于 int ( *arr9 ) [5] = &arr;
p_fiction test7 = &test; //等价于 int ( *test7) ( int ,int )= &test;
}
函数指针数组
数组的每个元素 是函数的地址
#include <stdio.h>
void menu() {
//add 加 subtract 减 multiple 乘 divide 除
printf("*************************\n");
printf("***** 1:add 2:sub *****\n");
printf("***** 3:mul 4:div *****\n");
printf("***** 0:exit *****\n");
printf("*************************\n");
}
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int mul(int a, int b) {
return a * b;
}
int div(int a, int b) {
return a / b;
}
int main() {
menu();
int x = 0;
int y = 0;
int input = 1;
int result = 0;
//int( *calc_add )(int x, int y)= add;
//int( *calc_sub )(int x, int y)= sub;
//int( *calc_mul )(int x, int y)= mul;
//int( *calc_div )(int x, int y)= div;
int( *calc[5] )(int x, int y) = { 0, add, sub, mul, div };
// 0 1 2 3 4
//数组中存放的每个元素都是函数的地址
//函数指针数组的 参数与类型 要保证一样
do
{
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输入2个操作数:");
scanf("%d %d", &x, &y);
result = ( *calc[input] )( x, y );
printf("结果是———> %d\n", result);
}
else if (input == 0) {
printf("退出calc,已结束\n");
}
else {
printf("输入有误,重新输入\n");
}
} while (input);
return 0;
}
通过其地址被调用的函数:回调函数
#include <stdio.h>
void menu() {
//add 加 subtract 减 multiple 乘 divide 除
printf("*************************\n");
printf("***** 1:add 2:sub *****\n");
printf("***** 3:mul 4:div *****\n");
printf("***** 0:exit *****\n");
printf("*************************\n");
}
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int mul(int a, int b) {
return a * b;
}
int div(int a, int b) {
return a / b;
}
void calc( int(*huidiao)(int, int) ) //解引用,指向回调函数的地址
{
int result = 0;
int x, y;
printf("输入操作数:");
scanf("%d %d", &x, &y);
result = huidiao(x, y); //按地址和参数进行函数计算:回调函数
printf("ret = %d\n", result);
}
int main() {
menu();
int input = 1;
do {
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
qsort函数(quick sort) <stdlib.h>
比较结构体数组
void qsort 利用指针给各种类型的数组排序
( arr, 待排序数组的首地址
sizeof(arr) / sizeof(arr[0]), 元素个数
sizeof(int), 一个元素的单位大小Byte
int ( *compare) (const void * exp1 , const void * exp2 ) 用来比较2个元素的函数指针;此处void * 要强制转换类型才能进行比较
若exp1 > exp2 就返回 >0的数值
若exp1 = exp2 就返回 0
若exp1 < exp2 就返回 <0的数值
);
int strcmp(const char* str1, const char* str2) 字符串比较 须<string.h>
若str1 > str2 就返回 >0的数值
若str1 = str2 就返回 0
若str1 < str2 就返回 <0的数值
#include <stdio.h>
#include <stdlib.h>
#include<string.h>
#include <assert.h>
struct Stu {
char name[20];
int age;
};
struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
int sz = sizeof(s) / sizeof(s[0]);
/* 按照年龄来比较结构体数据
int cmp_stu_by_age(const void* e1, const void* e2) { //void*不能解引用操作,须强制为所需类型
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
按照年龄来排升序
void test2() {
qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
} */
//按照名字来比较结构体数据
int cmp_stu_by_name(const void* e1, const void* e2) {
return strcmp( ((struct Stu*)e1)->name, ((struct Stu*)e2)->name );
}
//按照名字来排升序
void test3() {
qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main() {
// test2();
test3();
return 0;
}
结果:给结构体数组排了个升序
指针面试题
#include <stdio.h>
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*) (&a + 1); //下一个元素的地址
printf("%d,%d", *(a + 1), *(ptr - 1));
// 第2元素地址解引用 第5个元素地址解引用
return 0;
}
>>>2,5
#include <stdio.h>
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000; //强制转换为 结构体指针类型,并赋值地址0x100000=32D
int main()
{
printf("%p\n", p + 0x1); //跳过一个结构体大小
printf("%p\n", (unsigned long)p + 0x1); //强制转换为 int 无符号整形+1=33D
printf("%p\n", (unsigned int*)p + 0x1); //跳过1个无符号整形变量,0x100000+4=36D
return 0;
}
>>> 0x00100014 = 0x100000 + 20D
>>> 0x00100001 = 0x100000 + 1D
>>> 0x00100004 = 0x100000 + 4D
#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
//因为内部不是{},所以系统选取{ 1,3,5,0 ,0,0 }
int* p;
p = a[0]; //首元素地址
printf("%d", p[0]); // p[0] 等价于 *(p+0)
return 0;
}
>>>1
#include <stdio.h>
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* ptr1 = (int*)(&aa + 1); // aa[2][5]后的地址
int* ptr2 = (int*)(*(aa + 1)); // *(aa[0]+1)第2组数组的地址
printf("%d,%d", *(ptr1 - 1) , *(ptr2 - 1) );
// aa[2][5]的地址解引用:10 aa[1][0]前一个元素的地址解引用:5
return 0;
}
#include <stdio.h>
int main()
{
const char* a[] = { "work","at","alibaba" };
const char** pa = a; // a的首元素地址传给(char**)pa
pa++; // 等价于 *(a[0]+1) 等价于 a的第2组数组地址
printf("%s\n", *pa); //第2组数组地址解引用
return 0;
}
>>>at
#include <stdio.h>
int main() {
const char* c[] = { "ENTER","NEW","POINT","FIRST" };
const char** cp[] = { c + 3,c + 2,c + 1,c };
// 解引用为 frist ,point, new, enter
const char*** cpp = cp;
// 解引用为 *(c + 3)
printf("%s\n", **++cpp);
// **++cpp解引用为 *(c+2)再解引用为 ==>POINT 保留cpp指向(c+2)的值
printf("%s\n", *-- * ++cpp + 3);
// *(++cpp) 找到(c+1)的地址 ,并保留cpp指向 c+1 的值
// -- *(++cpp) 找到 (c+1) -1= c 的地址 ,并保留(c+1)修改为 c
// * (-- *(++cpp) 解引用为ENTER
// ( * (-- *(++cpp)+3 ) c[0]+ 3个char 统计\0前的字符串 ==>ER
printf("%s\n", *cpp[-2] + 3);
// *cpp[-2] 找到 (c+3) 的地址,解引用为char* c[3][0],再char* c[3]+3 ==>ST
printf("%s\n", cpp[-1][-1] + 1);
// cpp[-1][-1] 等价于 *( *(cpp-1) -1) 找到 *(c+2)再 *(c+2)-1 =*(c+1),
// 再 *(c+1)解引用为NEW,首元素地址+1==>EW
return 0;
}
>>>POINT
>>>ER
>>>ST
>>>EW
字符函数 <ctype.h>
为true就返回非0值,为false就返回0 | |
iscntrl | 是否为 任何控制字符 |
isspace | 是否为 空格、换页\f、换行\n、回车\r、制表符\t、垂直制表符\v |
isdigit | 是否为 十进制字符 |
isxdigit | 是否为 十六进制字符 0~F或0~f |
islower | 是否为 小写字母 |
isupper | 是否为 大写字母 |
isalpha | 是否为 字母a~z或A~Z |
isalnum | 是否为 字母或数字 0~ 9 或 a~z 或 A~Z |
isunct | 是否为 任何可打印字符 |
isgraph | 是否为 任何图形字符 |
isprint | 是否为 任何可打印字符(图形字符、空白字符) |
tolower | 将参数传进去的大写字母转小写 |
toupper | 将参数传进去的小写字母转大写 |
法一:把字符串中的小写字母转大写,其他字符不变 ASCII码值-32
#include <stdio.h>
#include <ctype.h>
int main()
{
int i = 0;
char str[] = "Test String.\n";
while (str[i] != '\0')
{
if (islower(str[i]))
{
str[i] -= 32;
}
i++;
}
printf("%s\n", str);
return 0;
}
>>>TEST STRING.
法二:把字符串中的大写字母转小写,其他字符不变 tolower函数
#include <stdio.h>
#include <ctype.h>
int main()
{
int i = 0;
char str[] = "Test String.\n";
while (str[i] != '\0')
{
if (isupper(str[i])) //如果是大写字母
str[i] = tolower(str[i]);
i++;
}
printf("%s\n", str);
return 0;
}
>>>test string.
字符串函数 <string.h>
size_t strlen ( const char * str );求字符串长度
字符串以'\0' 作为结束标志,返回的是 '\0' 前面出现的字符个数(不包含'\0' )。
参数指向的字符串必须要以'\0' 结束。否则为随机值
注意函数的返回值为 size_t,是 >=0 的( 防止负数)
#include <stdio.h>
#include <string.h>
int main() {
if (strlen("abc") - strlen("abcdef") > 0 )
printf("abc>abcdef\n");
// (无符号整数 -无符号整数) >0
else
printf("abc<abcdef\n");
return 0; }>>>abc>abcdef
其他表现形式
//计数器遍历的方式
#include <stdio.h>
#include <string.h>
int my_strlen(const char* str) //防止str指针把内容修改
{
int count = 0;
while (*str != '\0') {
count++;
str++; //指针往后移 1个Byte
}
return count; //返回数值
}
int main() {
char str[] = "abcdefghijk";
size_t length = my_strlen(str);
printf("%zd", length);
return 0; }
>>>11
//高指针-低指针 的方式
#include <stdio.h>
#include <assert.h>
size_t slen(char * n) //size_t 返回无符号整数
{
assert(n != NULL); //防止输入空字符串,否则error
char* start = n; //arr[0]的地址
char* end = n;
while (*end != '\0') //取到\0的低地址结束
{
end++; //取到arr[5]的地址
}
return end - start; // | \0的低地址 - arr[0]=a的地址 | =之间的元素个数
}
int main() {
char arr[] = "adcdef\0";
size_t length = slen(arr); //数组传参时slen(arr) = slen(arr[0])
printf("%zd\n", length);
return 0;
}
>>>6
//递归方式 函数未创建第三方变量
#include <stdio.h>
#include <assert.h>
int slen(const char* str) {
if (*str == '\0') //地址解引用,取字符
return 0;
else
return 1 + slen(str + 1); //后移1个字符的地址,传递给函数slen递归
}
int main() {
char str[] = "adcdefafgweryryeuurhhafgh\0";
size_t length = slen(str); //数组传参时slen(str) = slen(str[0])
printf("%zd\n", length);
return 0; }
>>>25
char* strcpy (char * destination, const char * source );字符串的覆盖
源字符串必须以'\0' 结束 且会把源字符串中的'\0' 拷贝到目标空间;
替换后在目标空间里,见 \0 就停止计算,实现覆盖效果。
目标空间必须>=源空间,以确保能存放源字符串。否则崩溃
目标空间必须可修改。仅用于字符串的操作;返回目标空间的起始地址,\0结束(char * )
attention: \0的ASCII值为0
其他表现形式
//指针传值覆盖
#include <stdio.h>
char* self_strcpy(char* b, const char* a) //源字符串不能被修改,目标空间没有被限制
{
char* ret = b; //把目标空间的起始地址存起来
while ( *b++ = *a++ ) //把 *a解引用得到的值赋值给 *b解引用的值,\0拷贝后判断为false结束
{
;
}
return ret; } //返回被赋值的那些值
int main() {
char a[] = "adgdfxbdve";
char b[30] = "#############################";
self_strcpy(b, a);
char * pchar = self_strcpy(b, a);
printf("%s\n", b);
printf("%s\n", pchar);
return 0; }
>>>adgdfxbdve
>>>adgdfxbdve
char * strncpy ( char * destination, const char * source, size_t num ) ;
从源字符串拷贝num个字符到目标空间
若源字符串的长度 < num,在剩余的空间里追加 \0
char* strcat ( char * destination, const char * source ); 字符串的追加
• 源字符串也必须以'\0' 结束; 目标字符串中也得以\0 结束,否则没办法知道从哪里开始追加到哪里结束
• 找到目标空间末尾的 \0 处,再拷贝源字符串到目标空间 \0后。
• 目标空间必须有足够的大,能容纳下 目标空间与源字符串 的内容。
• 目标空间必须可修改,源字符串不能被修改。• 字符串自己给自己追加,可能会死循环。
其他表现形式
#include <stdio.h>
char* self_strcat(char* dest, const char* src) //分别指向2个字符串的地址
{
char* result = dest; //返回初始时目标空间的起始地址
while (*dest != '\0')
{
dest++; // 遍历,往后移找到 \0
}
while ( *dest++ = *src++ ) //再 \0后替换
;
return result; }
int main() {
char a[] = "adgdfxbdve";
char b[30] = "##### %";
self_strcat(b, a);
printf("%s\n", b);
return 0; }
>>>##### %adgdfxbdve
char * strncat ( char * destination, const char * source, size_t num ) ;
把source指向字符串的前num个字符追加到destination指向的字符串\0处,再在末尾追加一个 \0 字符
若source 指向的字符串的长度小于num的时候,只会将字符串中到\0 的内容追加到destination指向的字符串\0处;
可以自己追加自己
int strcmp ( const char * str1, const char * str2 ) ; 比较2个字符串
str1 > str2,则返回 >0的数字
str1 = str2,则返回 0
str1 < str2,则返回 <0的数字
其他表现形式
#include <stdio.h>
int self_strcmp(const char* str1, const char* str2)
{
while (*str1 == *str2) //相同找下一队
{
if (*str1 == '\0')
return 0; //再 \0前都相同就返回0
str1++;
str2++;
}
return *str1 - *str2; } //不同就计算对应2个字符的ASCII差值
int main() {
char a[] = "adgdfxbdve";
char b[] = "adgk";
int result = self_strcmp(a, b);
printf("%d\n", result);
return 0; }
>>>-7
int strncmp ( const char * str1, const char * 1 str2, size_t num );
最多比较num个字母
char * strstr ( const char * str1, const char * str2) ;
找str2在str1中首次出现的位置,并返回str1中该位置后的数据;找不到返回 NULL
字符串的比较匹配不包含\0 字符,以\0 作为结束标志
char * strtok ( char * str, const char * sep) ; 分割字符串
sep定义了用作分隔符的字符集合;str由多个分隔符分割的字符串
找到str中的下一个标记,并将其用 \0 替换,并返回一个指向该标记的指针。
当str != NULL 时,将找到str中第一个标记,并保存它在字符串中的位置;并查找返回字符串
当str = NULL 时,将在同一个字符串中被保存的位置开始,查找返回下一个字符串
若字符串中不存在更多的标记,则返回 NULL 值
char * strerror ( int errnum ) ; 打印错误码对应的错误信息
须要<errno.h>。
每一个错误码都是有对应的错误信息;程序启动的时候就会用一个全局的变量errno来记录程序的当前错误码,只不过程序启动的时候errno是0。
其他表现形式
函数 void perror ( const char * str ) ; 直接把errno中错误码的对应错误信息打印出来
先打印 str 后打印一个 冒号与空格 最后接错误码的对应错误信息
内存块函数
void * memcpy ( void * destination, const void * source, size_t num ) ;
• 在遇到'\0' 不会停下来,复制整个结果。
• size_t num 的单位时Byte。
• 不负责任何重叠的拷贝,复制的结果都是未确定的、交叉覆盖。
其他表现形式
#include <stdio.h>
#include <string.h>
#include <assert.h>
void* self_memcpy(void* dst, const void* src, size_t count) {
void* result = dst; //把原数组首元素地址保存起来
assert(src);
while (count--) {
*(char*)dst = *(char*)src; //逐个字节地转载,防止占用空间冗余
dst = (char*)dst + 1;
src = (char*)src + 1;
}
return result;
}
int main() {
int a1[] = { 1,2,3,4,5,6,7,8,9,10 };
int a2[10] = { 0 };
self_memcpy(a2, a1 + 2 , 20);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", a2[i]);
}
return 0; }
>>>3 4 5 6 7 0 0 0 0 0
void * memmove ( void * destination, const void * source, size_t num ) ;
源内存块和目标内存块 重叠,就用memmove
其他表现形式
#include <stdio.h>
#include <string.h>
void* self_memmove(void* dst, const void* src, size_t num)
{
void* ret = dst;
if (dst <= src || (char*)dst >= ((char*)src + num))
{ //从前向后拷贝
while (num--) {
*(char*)dst = *(char*)src;
dst = (char*)dst + 1;
src = (char*)src + 1;
}
}
else { //从后向前拷贝
while (num--) {
*( (char*)dst +num ) = *( (char*)src+ num );
}
}
return(ret);
}
int main() {
int a1[] = { 0,1,2,3,4,5,6,7,8,9 };
//int a2[10] = { 0 };
self_memmove(a1 +2, a1 , 16);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", a1[i]);
}
return 0; }
>>>0 1 0 1 2 3 6 7 8 9
void * memset ( void * pointer , int value, size_t num ) ;
把内存中的值以单字节为单位设置成想要的内容
int memcmp ( const void *pointer1, const void *pointer2, size_t num ) ;
比较从ptr1和ptr2指针指向的位置开始,向后的num个字节
pointer1 > pointer2,则返回 >0的数字
pointer1 = pointer2,则返回 0
pointer1 < pointer2,则返回 <0的数字
自定义类型:结构体、联合体、枚举
*******结构体
数组的类型必须相同;结构体的类型可以不同。
<结构体的自引用:链接到同类型的另一个结构体变量
struct Node
{
int data; //数据域
struct Node* next; //指针域
};
<定义结构体时,不能使用匿名结构体
typedef struct Node //必须要有名称
{
int data;
struct Node* next;
} ReNode;
对齐偏移规则:提高结构体的访问效率
1. 第一个成员对齐到 结构体变量起始位置偏移量为0的地址处。
2. 其他成员变量要从 对齐数的倍数 的地址处后偏移。
对齐数 = 编译器默认对齐数 与 该成员变量类型大小 两者的较小值。
- VS 编译器默认对齐数值为 8
- Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
3. 每个成员变量都有一个对齐数,结构体总大小=最大对齐数的整数倍(含嵌套结构体)。
4. 在嵌套结构体中,被嵌套的结构体成员对齐到本身成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
#include <stdio.h>
#include <stddef.h> //offsetof宏 得到成员相对于结构体起始位置的偏移量
struct S1 {
char c1;
int m;
char c2;
};
struct S7 {
char c1;
char c2;
int m;
};
struct S5 {
double d;
char c;
int m;
};
struct S3 {
char c1;
struct S5 s3; //嵌套其它的结构体变量
double d;
};
int main() {
printf("%d ", offsetof(struct S1,c1) );
printf("%d ", offsetof(struct S1, m) );
printf("%d \t", offsetof(struct S1,c2) );
printf("%zd\n", sizeof(struct S1) );
printf("%d ", offsetof(struct S7,c1) );
printf("%d ", offsetof(struct S7,c2) );
printf("%d \t", offsetof(struct S7, m) );
printf("%zd\n", sizeof(struct S7) );
printf("%d ", offsetof(struct S5, d));
printf("%d ", offsetof(struct S5, c));
printf("%d \t", offsetof(struct S5, m));
printf("%zd\n", sizeof(struct S5) );
printf("%d ", offsetof(struct S3, c1));
printf("%d ", offsetof(struct S3, s3));
printf("%d \t", offsetof(struct S3, d));
printf("%zd\n", sizeof(struct S3) );
}
>>> 0 4 8 12
>>> 0 1 4 8
>>> 0 8 12 16
>>> 0 8 24 32
即使 对齐内存会产生存储空间浪费;但 ( 1.硬件规定须要对齐; 2. 对于没有对齐的内存数据,处理器需要两次访问)
结构体内存对齐 的目的:拿空间来减少访问时间,提高效率。
既要满足对齐,又要节省空间:让占用空间小的成员尽量放在一起(如 struct S7比struct S1更节省空间,效率更高)
修改默认对齐数
>内存对齐可以方便许多平台移植
>若内存不对齐,CPU访问内存时可能需要2次,但对齐后只需访问内存1次。以空间换时间的做法
#include <stdio.h>
#include <stddef.h>
#pragma pack(1) //设置默认对齐数为1,最好为2的倍数
struct S2 {
char c1;
int m;
char c2;
};
// #pragma pack() //重置对齐数
int main() {
printf("%d ", offsetof(struct S2,c1) );
printf("%d ", offsetof(struct S2, m) );
printf("%d \t", offsetof(struct S2,c2) );
printf("%zd\n", sizeof(struct S2) );
return 0;
}
>>> 0 1 5 6
结构体传参
#include <stdio.h>
int i = 0;
struct S {
int data[1000];
int num;
};struct S sample = { {1,2,3,4,7}, 990 };
void print3(struct S s) { //结构体传参
for (i = 0; i < 10; i++) {
printf("%d ", s.data[i]);
}
printf("%d\n", s.num);
}
void print4(struct S* ps) { //结构体地址传参
for (i = 0; i < 10; i++) {
printf("%d ", ps->data[i]);
}
printf("%d\n", ps->num);
}
int main() {
print3(sample); //传结构体
print4(&sample); //传地址
return 0;
}
>>> 1 2 3 4 7 0 0 0 0 0 990
>>> 1 2 3 4 7 0 0 0 0 0 990
结构体传参的时候,首选结构体地址,因为:
1.函数传参的时候,参数是需要压栈空间,会有时间和空间上的系统开销。
2.如果传递一个结构体时,结构体过大,参数压栈的的系统开销比较大,访问时间增加,所 会导致效率的下降;而传递结构体地址,只需4或8个字节。
位段:减少结构体存储空间
位段的成员必须是 int、unsigned int 、signed int 、 char
每个成员名后边有一个冒号和一个bit大小值
位段很节省空间。但存在跨平台的问题,注重可移植的程序应该避免使用位段:
1int 位段被当成有符号数还是无符号数是不确定的。
2位段中最大位的数目不能确定。
3位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
4当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位 时,是舍弃剩余的位还是利用,这是不确定的。
注:位段的多个成员共有同一个字节时; 不能在 scanf(); 中使用&操作符(不知是谁的地址,会报错)
柔性数组:结构中的最后一个元素允许是未知大小的数组
• 结构中,柔性数组成员前面必须至少一个其他成员。
• sizeof 返回的这种结构大小 不包括柔性数组的大小。#include <stdio.h> #include <string.h> struct feix_array { int i; //对对齐为4 double h; //对齐数为8 int array[]; //未知大小的array——柔性数组成员 //或者 int a[0]; 也是柔性数组成员 } type_a; int main() { printf("%zd\n", sizeof(type_a));//输出的是16(有内存对齐的现象) return 0; } >>> 16
• 包含柔性数组成员的结构中:用malloc ()函数进行内存的动态分配,并且分配的内存应该 > 结构体的大小,以适应柔性数组的预期大小。
#include <stdio.h> #include <stdlib.h> struct st_tp { int n; int array[]; }; int main() { //扩容结构体大小 struct st_tp* pointer7 = (struct st_tp*)malloc(sizeof(struct st_tp) +10*sizeof(int) ); if (pointer7 == NULL) { perror("malloc"); return 1; //非正常返回 } //使用空间 pointer7->n = 96; for (int i = 0; i < 10; i++) { pointer7->array[i] = i+1; } //调整柔性数组大小 struct st_tp* pointer8 = (struct st_tp*)realloc( pointer7, sizeof(struct st_tp) + 20 * sizeof(int)); //另起pointer8变量,防止调整失败,造成数据丢失 if (pointer8 != NULL) { pointer7 = pointer8; //调整成功给把地址返回给 pointer7 pointer8 = NULL; //防止形成野指针 } //使用空间 printf("%d\n", pointer7->n); for (int i = 0; i < 20; i++) { printf("%x ", pointer7 -> array[i]); } //释放空间 free(pointer7); pointer7 = NULL; return 0; } // malloc();使用次数多了:须把所有的动态空间释放,且每个空间是不连续的,导致访问速度减慢 //只有一次开辟,多次调整的话:连续的内存有益于提高访问速度,也有益于减少内存碎片 // 一次 free(); 就可以把所有的内存也给释放掉
>>> 96
>>>> 1 2 3 4 5 6 7 8 9 a cdcdcdcd cdcdcdcd cdcdcdcd cdcdcdcd cdcdcdcd cdcdcdcd cdcdcdcd cdcdcdcd cdcdcdcd cdcdcdcd
enum枚举
枚举常量的取值都是有值的,默认第1个从0开始,后面依次递增1
在声明枚举类型的时候可以赋初值
#include <stdio.h>
enum Color
{
RED = 2 ,
GREEN = 4 ,
BLUE
};
int main() {
printf("%d\n", BLUE);
return 0;
}>>> 5
枚举与 #define的优缺点
枚举 | #define |
可读性、可维护性 好 | |
便于调试,有类型检查,更加严谨 | |
预处理阶段会删除#define 定义的常量 | |
一次可以定义多个常量 | |
枚举声明在函数内,只能在函数内使用 |
union联合体: 在一些情况下节省空间
* 只为最大的成员分配足够的内存空间。联合体的特点是所有成员共用同一块内存空间。也存在内存对齐,对齐到最大对齐数的整数倍。所以一个联合变量的大小,至少是最大成员的大小。
* 同一时间其中一个成员修改值后,其他成员的值也跟着变化。但不能同时更改多个成员的值。
#include <stdio.h> struct Un7 { char c; //对齐数为1 int i; //对齐数为4 }; //共占8个字节 union Un6 { char c; int i; }; int main() { union Un6 dhs = { 0 }; printf("%zd\n", sizeof(struct Un7) ); printf("%zd\n", sizeof(dhs) ); //只为最大的成员分配内存空间 printf("%p\n", &(dhs.i)); // i的地址 占4个字节 printf("%p\n", &(dhs.c)); // c的地址 只占第一个字节 printf("%p\n", &dhs); //联合体的地址 //都是首元素地址,一样的地址 printf("\n"); dhs.i = 0x11223344; dhs.c = 0x55; //修改小端字节(44 33 22 11)的低地址变为(55 33 22 11) printf("%x\n", dhs.i); return 0; }
>>> 8
>>> 4
>>> 008FFE50
>>> 008FFE50
>>> 008FFE50>>>
>>> 11223355
结构体和联合体的内存对比:
联合体的大小不一定是最大成员的大小,联合体大小也存在内存对齐:
联合体的最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
#include <stdio.h> union Un1 { //大小 默认对齐数 对齐数 char c[5]; // 5 8 1 int i; // 4 4 4 }; //4个字节放不下5个字节,8个字节可以 union Un2 { //大小 默认对齐数 对齐数 short c[7]; // 14 8 2 int i; // 4 8 4 }; //4个字节放不下14个字节,16个字节可以 int main() { printf("%d\n", sizeof(union Un1)); printf("%d\n", sizeof(union Un2)); return 0; }
>>>8
>>>16
在结构体中使用联合体(有公共属性与特殊属性的模式下):节省存储空间
![]() | ![]() |
右边中书、杯子、衬衫 的地址都是一样的,共用同一块空间;比结构体的9个成员块占用的空间更节省
联合体的存放顺序是所有成员从低地址到高地址存放 | |
大端存储模式 | 高位数据放入低地址 |
小端存储模式 | 高位数据放入高地址 |
例子:判断当前机器是大端?还是小端?
#include <stdio.h>
int main() {
union un
{
int i;
char c;
}; //4个字节的联合体
union un sample = { 0 };
if (sample.i == 1)
printf("小端\n");
else
printf("小端\n");
return 0;
}
// 01 00 00 00 小端
// 00 00 00 01 大端
程序内存区域划分
1. 栈区(stack):函数的局部变量都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
2. 堆区(heap):由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式 ==> 链表。
3. 数据段 or 静态区(static):程序结束后由系统释放。
4. 代码段:存放函数体 (成员函数和全局函数)的二进制代码
动态内存分配
动态内存 | 数组 | |
占用大小 | 可以调整 | 不可调整 |
开辟空间的位置 | 堆区 | 栈区、静态区 |
释放空间的方法 | 手动 | 由操作系统决定 |
void* malloc (size_t size) ; 开辟一块连续可用的空间,并返回这块空间的指针
• 须要<stdlib.h>
• 如果开辟成功,则返回一个指向空间的起始地址。
• 如果开辟失败,则返回NULL 指针,因此malloc的返回值一定要用 perror("malloc") ;检查• 返回值的类型由用户自己来决定,可强制转换类型。
• size_t size为0时,该行为是标准是未定义的,取决于编译器。
void free (void* pointer) ; 释放 动态开辟的整个内存块
• 须要<stdlib.h>
• 对象只能是 动态开辟的空间。
• 对一块动态空间不能连续释放多次。
• 如果参数 pointer 指向的地址不是动态开辟的,那free函数的行为是未定义的。
• 释放完后 pointer 指向的地址仍保留,形成野指针,容易造成错误;可把它赋为NULL。• 如果参数 pointer 是NULL指针,则 free(); 什么事都不做。
• 动态开辟的空间不要忘记去正确释放,否则会造成内存泄漏。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* kj = (int*)malloc(5 * sizeof(int)); //申请5个int的空间
if (kj == NULL)
{
perror("malloc");
//对NULL指针的解引用操作
return 1; //非正常返回,结束运行
}
printf("%p\n", kj);
int i = 0;
for (i = 0; i < 5; i++) {
*(kj + i) = i + 1; //使用这5个int的空间
printf("%d ", i + 1);
}
printf("\n");
printf("%p\n", kj);
free(kj);
kj = NULL; //防止形成野指针
printf("%p\n", kj);
return 0;
}
>>> 014E94C0
>>> 1 2 3 4 5
>>> 014E94C0
>>> 00000000
void* calloc ( size_t num, size_t size ) ;
开辟num 个元素大小为size 的空间,并且把空间的每个字节初始化为0。
#include <stdio.h>
#include <stdlib.h>
int main() {
int* p = (int*)calloc(13, sizeof(int)); //强制类型转换为int指针
if (NULL == p)
{
perror("calloc");
return 1; //非正常返回,结束运行
}
else {
int i = 0;
for (i = 0; i < 13; i++)
{
printf("%d ", *(p + i)); //打印这13个int的内容
}
}
//free(p);
//p = NULL;
return 0;
}>>> 0 0 0 0 0 0 0 0 0 0 0 0 0
void* realloc ( void* pointer , size_t size) ; 对动态开辟的内存大小进行调整。灵活
• 返回值为调整之后的内存起始地址。
• 也可以开辟空间。
在扩容时的2种状况:
$情况1:原有空间之后的空间 足够,直接在原有内存之后直接追加空间,返回旧的地址
$情况2:原有空间之后的空间 不够,则在堆区上另找一个满足总大小的连续空间来使用, 将原数据内容拷贝到新空间,旧空间被释放,返回一个新的内存地址。
#include <stdio.h>
#include <stdlib.h>
int main() {
int* point1 = (int*)calloc(13, sizeof(int)); //强制类型转换为int指针
int i = 0;
if (NULL == point1)
{
perror("calloc");
return 1; //非正常返回,结束运行
}
else {
for (i = 0; i < 13; i++)
{
printf("%d ", *(point1 + i) ); //打印这13个int的内容
}
}
printf("\n");
int* point2 = (int*)realloc(point1, 20); //扩容为20个字节
if (point2 != NULL)
{
point1 = point2; //把 point2 的地址赋给 point1
}
for (i = 0; i < 20; i++) {
*(point1 + i) = i + 1; //给这20个int的空间赋值
//保证pointer1 始终指向动态内存的起始位置
printf("%d ", *(point1 + i) ); //打印这20个int的内容
}
printf("\n");
free(point1);
point1 = NULL; //防止形成野指针
return 0;
}
>>> 0 0 0 0 0 0 0 0 0 0 0 0 0
>>> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
关于内存空间的面试题
题目1: 程序会崩溃,无法拷贝,且会有内存泄漏问题
#include <stdlib.h> void GetMemory(char* p) //把str的NULL指针传来 { p = (char*)malloc(100); //给p开辟空间 } void Test(void) { char* str = NULL; GetMemory(str); strcpy(str, "hello world"); //对str指向的NULL指针解引用,程序会崩溃,无法拷贝 printf(str); } int main() { Test(); return 0; }
改正:
#include <stdlib.h>
#include <string.h>
void GetMemory(char** str)
{
*str = (char*)malloc(100); //给str开辟空间,得到100个字节大小地址
}
void Test(void) {
char* str = NULL;
GetMemory(&str); //传递地址
strcpy(str, "AIstone Global Limited\n"); //不是对NULL解引用,能成功
printf(str);
free(str);
str = NULL; //对地址进行释放
}
int main() {
Test();
return 0;
}
>>> AIstone Global Limited
题目2: 返回随价值,有 栈空间地址访问权限问题
#include <stdlib.h> #include <string.h> char* GetMemory(void) { char p[] = "hello world"; //局部数组 return p; //返回局部数组首元素地址 返回变量的时候不会出问题 } void Test(void) { char* str = NULL; //放入NULL指针 str = GetMemory(); //得不到局部数组首元素的地址 printf(str); //成为野指针,打印出随价值 } int main() { Test(); return 0; } >>>烫烫烫烫烫烫烫烫鳃?
题目3: 存在内存泄露问题
#include <stdlib.h>
#include <string.h>
void GetMemory (char** str, int num ) {
*str = (char*) malloc (num);
}
void Test(void) {
char* str = NULL;
GetMemory (&str, 100);
strcpy(str, "AIstone Global Limited\0hello");
printf(str);
}
int main() {
Test();
return 0;
}>>> AIstone Global Limited
改正:
#include <stdlib.h>
#include <string.h>
void GetMemory(char** str, int num) {
*str = (char*)malloc(num);
} //开辟100个字节大小的空间
void Test(void) {
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "AIstone Global Limited\0hello");
printf(str);
free(str); //把不再使用的地址释放掉
str = NULL;
}
int main() {
Test();
return 0;
}
>>> AIstone Global Limited
题目4: 非法访问野指针
#include <stdlib.h>
#include <string.h>
void Test(void) {
char* str = (char*)malloc(100);
strcpy( str, "hkftk\n");
printf(str);
free(str); //开辟的地址无法使用了,成为野指针
if (str != NULL)
{
strcpy(str, "world\n"); //非法访问野指针
printf(str);
}
}
int main() {
Test();
return 0;
}
改正:
#include <stdlib.h>
#include <string.h>
void Test(void) {
char* str = (char*)malloc(100);
strcpy(str, "hkftk\n");
printf(str);
free(str); //开辟的地址无法使用了,成为野指针
str = NULL;
if (str != NULL) //赋值为NULL指针,判断就执行了
{
strcpy(str, "world\n"); //非法访问野指针
printf(str);
}
}
int main() {
Test();
return 0;
}
>>> hkftk
C语言的文件操作
文件:数据进行持久化的保存
文件功能的角度 | ||
程序文件: | 数据文件: | |
源程序文件.c 目标文件.obj 可执行程序.exe | 程序运行需要从文件的内容中读取数据的文件 | |
根据数据的组织形式 | ||
二进制文件 | 文本文件 | |
二进制补码的形式不加转换地存储 | 以ASCII字符的形式存储的文件 |
字符型数据只能以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以用二进制形式存储。根据情况选择最优的方式
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb"); //以二进制写的方式打开文件
fwrite(&a, 4, 1, pf); //以二进制的形式写入文件
//提高a的地址 写4个字节 写1次 写入关联的文件
fclose(pf);
pf = NULL; //把指针释放
return 0;
}
二进制小端字节序的结果
文件的打开和关闭
通过文件指针变量&文件操作函数实现对文件的 打开、读写、访问、关闭
C程序都是通过打开流对不同的外部设备进行输入输出操作
C语言中,就是通过 FILE* 的文件指针来维护流的各种操作;
文件指针变量 FILE * fp ;
那C语言程序在启动的时候,默认打开 控制端了3个标准文件指针 由系统自动打开或关闭:
• stdin - 标准输入流(键盘),在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
• stdout - 标准输出流(显示器),大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出流。
• stderr - 标准错误流(显示器),大多数环境中输出错误信息到显示器界面。
每个被使用的文件都在内存中开辟了一个相应的文件信息区(文件的名字,文件状态及文件当前的位置)
其保存在一个 由系统声明的 FILE类型 的结构体变量中;不同的C编译器系统会根据文件的情况自动创建一个FILE结构的变量其中的信息,使用者不必关心细节
可以用 FILE* pointer来访问与维护这个FILE结构的变量
FILE * fopen ( “const char * filename” , “const char * mode” ) ; 打开文件
文件打开成功返回的是文件的地址,可用 FILE*pointer来接收;若文件打开失败就返回NULL指针
mode文件的打开模式 | 若指定文件不存在 | 若指定文件存在 | |
“r”(只读) | 输入数据,打开一个已经存在的文本文件 | 出错 | |
“w”(只写) | 输出数据,打开一个文本文件 | 建立一个新的文件 | 每次打开会清空内容 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 | |
“rb”(只读) | 输入数据,打开一个二进制文件 | 出错 | |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 | |
wb+”(读写) | 读和写,新建一个新的二进制文件 | 建立一个新的文件 |
fclose ( FILE*pointer ) ;
FILE*pointer = NULL ; 关闭文件
文件的有序读写(文件内的插入点逐次往后移)
int fgetc ( FILE*pointer ) ; | 读出单字符;读取失败时返回EOF(文件结束标志) | 文本信息 |
int fputc ( ’int char‘ ,FILE*pointer ) ; | 写入单字符 | |
char* fgets ( "char* string” , int num, FILE*pointer ) ; | 读出文件中指定长度的字符串 到string的地址处; 在遇到 \n 时就截止长度;读取错误返回NULL指针 | |
int fputs ( “const char* string” ,FILE*pointer ) ; | 写入字符串;写入失败时返回 EOF与 Ferror | |
int fscanf ( FILE*pointer , const char* format ,…) ; | 各种格式类型的读出,存放至结构体的成员中 | |
int fprintf ( FILE*pointer , const char* format ,…) ; | 各种格式类型的写入 |
#include <stdio.h>
struct cjk
{
char name[13];
int age;
float acore;
};
int main() {
struct cjk chenpl = {"chenpenling " , 18, 67.87f}; //定义一个成员
FILE* pointer = fopen("cjk.txt","w"); //写文件,每次打开会清空内容
if (pointer == NULL) {
perror("fopen");
return 1;
}
fprintf(pointer,"%.3f _ %d _ %s" ,chenpl.acore, chenpl.age, chenpl.name); //写入文件
//读取到结构体成员中
fscanf(pointer, " %d _ %.3f _ %s", &(chenpl.age), &(chenpl.acore), chenpl.name);
//在终端标准输出流中打印
fprintf(stdout ,"年龄为 %d _ 成绩为 %.3f _ 姓名是 %s \n", (chenpl.age), (chenpl.acore), chenpl.name);
fclose(pointer); //关闭文件
pointer = NULL; //防止形成野指针
return 0;
}
int sprintf ( char* string, const char* format,…); | 把格式化的数据转换为字符串string中 |
int sscanf ( const char* s, const char* format,…); | 从字符串string中提取格式化数据 |
#include <stdio.h>
#include<string.h>
struct cjk {
char name[13];
int age;
float score;
};
int main() {
char string[76] = { 0 };
struct cjk ydan6 = { "yangdan " , 20, 87.87f };
//从结构体ydan6中把格式化的数据转换为字符串保存至string中
sprintf(string, "%d %s %f\n", ydan6.age, ydan6.name, ydan6.score);
struct cjk ydan5 = { 0 }; //临时结构体变量
//从字符串string中提取格式化数据 保存至到结构体 ydan5 中
sscanf(string, "%d %s %f\n", &(ydan5.age), ydan5.name, &(ydan5.score) );
//2次的格式化数据顺序要一致且相同
printf("成员一:%s", string); //以字符串方式打印
printf( "%d岁的 %s 分数是%f\n", ydan5.age, ydan5.name, ydan5.score );
return 0;
}
>>> 成员一:20 yangdan 87.870003
>>> 20岁的 yangdan 分数是87.870003
size_t fread ( void* pointer, size_t size, size_t count,FILE*pointer ) ; | 从文件中读取count个大小为size个字节的数据以二进制形式存放在pointer地址处,返回实际读到的元素个数 | 二进制信息 |
size_t fwrite ( const void* pointer, size_t size, size_t count,FILE*pointer ) ; | 把pointer地址处的count个大小为size个字节的数据以二进制形式写入文件 |
#include <stdio.h>
#include<string.h>
struct cjk
{
char name[13];
int age;
float score;
};
int main() {
struct cjk chenp7 = {"chenpenling\0" , 18 , 67.97f }; //定义一个成员
struct cjk chenp2 = { 0 };
FILE* pointer = fopen("cjk.txt","rb"); //现在改为读取,打开一个二进制文件
if (pointer == NULL) {
perror("fopen");
return 1;
}
//fwrite( &chenp7, sizeof(struct cjk), 1, pointer); //之前以二进制写入文件过
fread(&chenp2, sizeof(struct cjk), 1, pointer); //以二进制读出文件至chenp2地址处
printf("%d %s %f \n", chenp2.age, chenp2.name, chenp2.score);
fclose(pointer); //关闭文件
pointer = NULL; //防止形成野指针
return 0;
}
文件的随机读写(改变文件内的插入点位置)
用位置和偏移量来定位文件里的插入点
int fseek ( FILE* pointer, long int offset(偏移量,正负都可), int origin(起始位置) ) ;
origin(起始位置) 有3种值 :
SEEK_SET文件开始处、 SEEK_CUR插入点当前位置、 SEEK_END文件尾
返回文件插入点相对于文件开始处的偏移量
long int ftell ( FILE* pointer ) ;
让文件插入点移动到文件的起始位置
void rewind ( FILE* pointer );
判断文件是否读取结束
提醒:feof函数:判断读取结束的原因:是否遇到文件尾结束(遇到非0值就算遇到文件末尾)
判断文本文件读取是否结束
1• fgetc 判断是否为EOF .
2• fgets 判断返回值是否为NULL .二进制文件读取是否结束:判断返回值是否小于实际要读的个数
* 完成一个复制txt文件的操作
#include <stdio.h>
int main() {
FILE* pointer_read = fopen("cjk.txt", "r"); //读出cjk.txt的内容
if (pointer_read == NULL) {
perror("fopen");
return 1;
}
FILE* pointer_write = fopen("test.txt", "w"); //把内容写入test.txt
if (pointer_read == NULL) {
perror("fopen");
return 1;
}
int charudian = 0; //中间交换量
while ((charudian = fgetc(pointer_read)) != EOF) //返回值为EOF就结束读取
{
fputc(charudian, pointer_write); //把读取至charudian的内容写入test.txt
}
fclose(pointer_read); //关闭cjk.txt文件
pointer_read = NULL; //防止形成野指针
fclose(pointer_write); //关闭test.txt文件
pointer_write = NULL; //防止形成野指针
return 0;
}
文件翻译过程中的预处理
预处理符号:在预处理期间可以直接被处理的
#define 名字 内容 (最好末尾不加分号) 用来定义常量
#define 宏名字(任*-意参数) 内容 用来定义宏
#undef NAME 移除宏定义
当宏参数在宏的定义中出现超过一次的时,会导致不可预测的后果(副作用参数)
并且宏不能递归
对比:宏 & 函数
函数 (适用于复杂的运算) | 宏 (适用于小型的运算) | |
宏的优势 | 须要调用函数与函数返回的过程 | 代码直接替换到指定位置,没有调用与返回的过程;只有执行运算的时间。 在速度方面更胜一筹 |
函数的参数 必须是规定的类型 | 宏的参数是 类型任意 | |
函数的实参不能是类型 | 宏的实参可以为类型 如 char* pointer =malloc(10 , char ); 开辟10个字节的空间 | |
宏的劣势 | 不能递归 | |
无法调试,类型也不严谨 | ||
有时带来运算符优先级的问题;也会有(副作用参数)的问题 | ||
宏代码过长时,会大幅度增加程序的长度 |
#运算符: 在带参数的宏的替换列表中把参数字符化
## 记号粘合 把两边的标识符合成生一个合法的标识符
条件编译
#if 常量表达式 (为假就不编译,且被注释掉)
………………
#elif 常量表达式
………………
#elif 常量表达式
………………
#else
………………
#endif
用条件编译解决头文件被重复引入的问题
算法要素(时间复杂度、空间复杂度、稳定性)
当待排序数组元素 | |||
较大时 | 是随机数时快速排序平均时间最短 | 内存允许又要求稳定性就归并排序 | 其余情况用堆排序 |
较小时 | 要求稳定性,直接插入排序的比较&交换次数少 | 不考虑稳定性,采用直接选择排序 |
排序算法:冒泡排序、快速排序……
查找算法:顺序查找、折半查找、斐波那契查找、数表查找、分块查找、哈希查找
链表
把元素串联起来形成一种线性结构 ==>链式结构 利用结构体类型来实现,既包含数据又包含链接
静态链表 | 类似于数组的实现方式,要预先分配空间,长度固定 | |
动态链表 | 长度没有限制,结点肯不连续,通过指针顺序访问 | |
链表 | 数组 | ||
存储密度 | 数据域 / (数据域+指针域) =密度<=1 | 元素个数确定且变化不大,为节约空间,采用数组 | |
存储空间 | 不预先分配空间大小,根据需要申请空间大小 | 要预先分配空间大小,扩充会受到限制,造成空间浪费或空间溢出 | 元素个数难以估计且变化较大时,采用链表 |
存取效率 | 顺序存取结构,从表头到表尾访问,效率低 | 随机存取结构,效率高 | 元素位置紧密且很少插入或删除元素,采用数组 |
增删效率 | 元素移动只需修改指针即可 | 对数组增删首元素时,需要移动的数组元素是最多的 | 要常对元素做增删操作,采用链表 |
#2024-9-4# —— #2024-10-30# 挺漫长勒~