一、 Introduction
C语言是一款高级编程语言。高级是相对于机器语言和汇编语言而言。它使得程序设计可以相对低级语言摆脱具体的机器环境的束缚,更自由的做程序开发。c语言的优点是代码量小,执行速度快,仅次于机器语言和汇编语言,功能强大,编程自由;相比其他高级语言而言,比如java、php、python等,不足是编写代码实现周期长,可移植性差,需要丰富的经验才能掌握,对平台库依赖较多。
c语言的应用领域很广,网站后台,底层操作系统,多媒体应用,大型网络游戏等,比较新的,如云计算、物联网、移动互联网、智能化等。
二、 开发环境IDE和入门程序
ide就是编程工具。windows下,microsoft visual studio2010是一款强大的编程工具。mac下,有xcode。linux或unix下,可以使用vi或vim。跨平台的工具有code blocks,eclipse c/c++,qt等。
下载,安装visual studio2013。
1. 第一个c程序
打开visual studio,新建项目,选择visual C++。项目的名称有规范,如果输错,软件会提示。输入符合规范的名字,新建,如果项目目录没有权限,会不能新建,可以更改之。创建项目后,在源文件中,添加c++文件,比如,helloworld.cpp文件,c plus plus。
在这个文件添加入口点,比如
void main()
{
}
任何c程序,都需要一个入口点。void是返回值类型为空的,main是函数名。()是输入的内容;{}是输出的内容。c语言严格区分大小写。
在入口文件中,调用函数,需要在头文件中包含这个函数所在的头文件stdio.h,h就是header,或者说是引入。c语言的源文件是.c或者.h,c++的源文件是.cpp。cpp兼容.c或.h。
比如:
#include<stdio.h>;
void main()
{
printf("helloword");
}
使用本地调试器,运行,一闪而过。使用getchar();可以让窗口暂停。等待输入一个字符。
#include<stdio.h>;
void main()
{
printf("helloword");
getchar();
}
c语言的注释,行注释//,块注释/**/。
#include<stdio.h>;//包含头文件,以使用printf
/*
* helloworld程序示例。
*/
void main()//void返回类型为空,main是函数名,整个是c的入口点,从这里开始从这里结束;
//()为输入的参数,空则输入为空。
{
//{}是函数体
//打印的函数,参数为字符串helloworld
printf("helloword");
//获取键盘输入的函数
getchar();
}
c的bug和debug,bug是程序中的导致程序不能正常运行的错误、漏洞、缺陷或问题。debug就是纠正错误的过程。断点是为了帮助调试内部各状态而专门设置的一种调试手段,也就是在调试模式下运行程序过程。当程序运行到某个断点后暂停。点击继续,程序继续运行。方便排查问题。在断点调试下可以在调试按钮中选择查看线程、gpu、局部变量、寄存器、反汇编等内容。
制作头文件。
在项目的头文件目录添加helloworld.h的头文件,后缀名可以为其他,比如helloworld.txt,将入口函数的函数体的内容剪切到这个头文件。
//打印的函数,参数为字符串helloworld
printf("helloword");
//获取键盘输入的函数
getchar();
然后,在函数体中引入这个头文件。然后,运行之,和之前的看到的效果一样。
注意,#和include之间可以有多个空格。include的作用是预处理,就是在编译之前,将include的内容包含到程序。include<>和include””的区别是,<>包含的是系统的文件,””是自定义的文件。
一个c程序由一个或多个源程序文件组成,函数是c程序的主要组成部分。
2. C语言的windows命令行编程
在cmd中运行c程序,首先编译一段c程序并生成之,就是编译之。然后,在命令行,进入这个c程序所在的目录,直接运行xxx.exe。
通过c代码执行在命令行运行c程序。右键解决方案,添加新建项目CMD-Test。将CMD-Test设为启动项。在源文件添加main.cpp文件。
首先测试一下调用cmd函数
#include<stdlib.h>//引入system的头文件标准函数库
void main()
{
//调用system函数,测试cmd命令
system("ipconfig");
system("pingwww.baidu.com");
system("pause");
}
为了执行,请将xxx.exe的c程序文件放在e盘根目录下。
#include<stdlib.h>//引入system的头文件标准函数库
void main()
{
//调用system函数,测试cmd命令
//system("ipconfig");
//system("pingwww.baidu.com");
system("E:\\CMD.exe");
system("pause");
}
c黑窗口程序都可以命令行执行。在c黑窗口程序可以调用其他任何c黑窗口程序在命令行执行。
使用c黑窗口程序可以操作计算机,比如关机,打开组策略等。
gpedit.msc组策略
sndrec32录音机
Nslookup ip地址侦测器,检测网络DNS服务器是否正确实现域名解析的命令行工具。
explorer打开资源管理器
logoff注销
shutdown60秒倒计时关机
lusrmgr.msc本机用户和组
services.msc本地服务设置
oobe/msoobe /a检查xp是否激活
notepad打开记事本
cleanmgr垃圾整理
net start messenger开始信使服务
compmgmt.msc计算机管理
net stop messenger停止信使服务
conf启动netmeeting
dvdplay打开dvd播放器
charmap启动字符映射表
diskmgmt.msc磁盘管理实用程序
calc启动计算器
dfrg.msc磁盘碎片整理程序
chkdsk.exe磁盘检查
devmgmt.msc设备管理器
regsvr32/u *.dll停止dll文件运行
drwtsn32系统医生
rononce -p15秒关机
dxdiag检查DirextX信息
regedt32注册表编辑器
Msconfig.exe系统配置实用程序
......
等等,更多指令可以参考cmd命令行手册。
#include<stdlib.h>//引入system的头文件标准函数库
void main()
{
//调用system函数,测试cmd命令
//system("ipconfig");
//system("pingwww.baidu.com");
//system("E:\\CMD.exe");
//打开本地组策略
//system("gpedit.msc");
//ip地址侦测器
//system("Nslookup");
//打开资源管理器
//system("explorer");
//注销
//system("logoff");
//60秒关机
//system("shutdown");
//本机用户和组
//system("lusrmgr.msc");
//本地服务设置
//system("services.msc");
//检查xp是否激活
//system("msoobe/a");
//打开记事本
//system("notepad");
system("regedt32");
system("pause");
}
利用c黑窗口命令行可以制作这些指令的快捷启动按钮。
c黑窗口命令可以提供很多本机相关信息。比如360管家软件或qq管家软件,就是抓取c命令行执行的结果。
3. C语言弹窗打开一个exe
使用windows.h下的函数。
MessageBox()
第一个参数为0,表示依赖的窗口的编号,设为0就是系统弹出的;第二个参数是对话框的内容,第三个是对话框的标题,第四个是对话框的类型,设为0就可
ShellExecute()
第一个参数表示依赖的窗口编号,0就是系统执行;第二个参数为执行的操作,一般只有open、print;第三个参数是执行的文件名称或路径网址或邮件地址;第4个和第5个是系统保留的参数,设为0,控制打开的窗口,显示隐藏最大化的参数。
#include<stdio.h>
#include<windows.h>
void main()
{
printf("helloworld");
//第一个参数为0,表示依赖的窗口的编号,设为0就是系统弹出的;第二个参数是对话框的内容,第三个是对话框的标题,第四个是对话框的类型,设为0就可
//MessageBox(0,"nihao", "helloll", 0);
//c语言下双斜杠代表一个斜杠
//打开文件
//ShellExecute(0,"open", "E:\\1.txt", 0, 0, 1);
//打开文件
//ShellExecute(0,"open", "http://www.baidu.com", 0, 0, 1);
//打开可执行件
//ShellExecute(0,"open", "C:\\Program Files (x86)\\SublimeText3\\sublime_text.exe", 0, 0, 1);
//打开系统自带文件,不需要注明路径
//ShellExecute(0,"open", "notepad", 0, 0, 1);
//将文件内容打印到控制台
//ShellExecute(0,"print", "E:\\1.txt", 0, 0, 1);
//打开文件夹
//ShellExecute(0,"open", "c:\\Document\\", 0, 0, 1);
ShellExecute(0,"open", "c:\\", 0, 0, 6);
//调用邮件工具
//ShellExecute(0,"open", "mailto", 0, 0, 1);
//想某个地址发邮件
//ShellExecute(0,"open", "mailto:xxx@xxx.com", 0, 0, 1);
getchar();
}
4. C工程的头文件、源文件以及include指令
在c程序的入口函数中使用函数,需要引入使用的函数的头文件,就是包含这个函数的文件。头文件是.h文件,即head的h。头文件是源文件的辅助文件,一般把一些变量、函数定义放到头文件,而将函数的实现放在源文件。
在头文件中创建一个include.h,
printf("哈哈hello");
然后在源文件中包含自定义的头文件
#include<stdio.h>
void main()
{
#include"include.h"
getchar();
}
在源文件中可以包含其他类型的文件,比如在头文件包创建一个haha.hehe的文件
printf("heheh");
#include<stdio.h>
void main()
{
#include"include.h"
#include"haha.hehe"
getchar();
}
注意,include是预编译指令,结束不需要加;等符号,否则报错,但是不影响执行。
源文件可以命名为.cpp,即c++文件,也可以命名为.c,因为c++的源文件兼容c的源文件。
创建一个data.h头文件存放定义的变量
int a = 10;//定义一个整型的变量
int b = 12;
int c;
在函数开始前包含变量头文件
#include<stdio.h>
#include "data.h"
void main()
{
#include"include.h"
#include"haha.hehe"
c = a + b;
printf("\n");//\n代表换行
printf("%d", c);
getchar();
}
Include可以包含任意类型,.h、.c、.cpp等。
比如,在源程序创建add.cpp
#include<stdio.h>
#include "data.h"
void add()
{
c = a + b;
printf("\n");
printf("%d", c);
}
将源程序命名为.c后缀的,不然,在包含同位.cpp的文件后无法编译。然后,在源程序中包含之
#include<stdio.h>
#include "add.cpp"
void main()
{
#include"include.h"
#include"haha.hehe"
add();
getchar();
}
头文件也是为了避免重复包含。
三、 基于MinGW(windows下的gcc编译器)的c语言IDE
1. 下载安装windows下的codeblocks
安装好。打开codeblocks,可在设置,编译器中看到默认的编译器是gnu gcc编译器。进入安装目录,复制MinGw目录到c目录的工具目录下,使用这个目录下的文件可以gcc编译c文件。首先,需要使用MinGw目录下的mingwvars.bat批处理文件,设置环境变量,在cmd执行这个文件就可以设置环境变量。
2. 使用gcc编译c文件
编写一个简单的1.c文件。在cmd中,执行gcc 1.c,编译了a.exe文件,执行a.exe。如果要指定编译目标文件的名字,gcc 1.c -o 1.exe。
3. 创建基于gcc的IDE
打开vs2013,创建一个MFC项目GCCIDE;然后下一步;然后,选择多个顶级文档;然后下一步,无复合文档支持;然后下一步,文件扩展名设置为c,选择预览处理程序支持,缩略图处理程序支持,搜索处理程序支持;然后下一步,无数据库支持;然后下一步,主框架样式,勾选所有,选择使用功能区;然后下一步,勾选所有选项;然后下一步,生成的类为xxxxxxView,基类选择CRichEditView。然后完成。
生成本地资源,然后使用新建的IDE编写一个简短的c程序,保存为gcc.c。在cmd中,这个gcc.c文件不能编译,因为这个编译器默认将文件信息也保存在这个gcc.c文件中,所以产生乱码,编译不通过。在GCCIDE项目中,GCCIDEDoc.cpp文件中,修改序列化函数,主要功能是文本保存,文件读写。根据注释提示,如果需要文本进行序列化,则设置
void CGCCIDEDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: 在此添加存储代码
}
else
{
// TODO: 在此添加加载代码
}
// 调用基类 CRichEditDoc 将启用
// 容器文档的 COleClientItem 对象的序列化。
// TODO: 如果作为文本进行序列化,则设置
CRichEditDoc::m_bRTF = FALSE;
CRichEditDoc::Serialize(ar);
}
然后,重新生成本地资源,编写gcc.c文件,保存,再次使用gcc gcc.c -o gcc.exe,然后执行gcc.exe,可以执行。
在IDE中创建基于GCC的编译按钮,点击资源视图,点击GCCIDE.rc,点击Ribbon,双击IDR_RIBBON。在工具箱,拖一个类别到菜单栏上,在属性中,将这个菜单标签的caption改为工具箱。然后,拖一个按钮到这个菜单的面板上,设置按钮名字为编译执行,将面板的名字改为测试。然后,右键编译执行按钮,添加事件处理程序,在类列表,选择CMainFrame,然后添加编辑。
首先,在MinGW目录中,复制一份mingwvars.bat,重命名为gcc.bat,编辑其内容为:
@echo.
@echo Setting up environment for using MinGW with GCC from %~dp0.
@set PATH=%~dp0bin;%PATH%
cd C:\Users\Administrator\Desktop
c:
gcc gcc.c -o gcc.exe
gcc.exe
然后,在编译执行按钮的处理程序中编写
#include<stdlib.h>
void CMainFrame::OnButton2()
{
// TODO: 在此添加命令处理程序代码
system("E:\\006c\\006tools\\MinGW\\mingwvarsgcc.bat");
}
然后,生成调试。
四、 MFCSystem图形化显示cmd
微软基础类库,以c++类的形式封装的windows api,并且包含了一个应用程序框架。使用MFC实现图形化操作cmd命令行的程序。
新建一个MFC应用程序。在向导选择基于对话框,单机完成,创建一个图形化界面。
在左侧工具箱,拖一个button到界面,更名为计算器。双击这个button,可以编写这个button的相关动作。
1. 打开关闭可执行程序
//打开qq
void CMFCsystemDlg::OnBnClickedButton2()
{
// TODO: 在此添加控件通知处理程序代码
//带路径的"需要转义为\",\处理为\\,注意在64位系统中(前面要加空格。
system("\"C:\\ProgramFiles (x86)\\Tencent\\QQ\\Bin\\QQScLauncher.exe\"");
}
注意,在c程序中对于\ ”等使用转移字符。
//关闭qq
void CMFCsystemDlg::OnBnClickedButton3()
{
// TODO: 在此添加控件通知处理程序代码
MessageBoxA(0, "哈哈", "关闭", 0);
system("taskkill /f/im QQ.exe");
}
2. 开启关闭网站
//打开网站
void CMFCsystemDlg::OnBnClickedButton4()
{
// TODO: 在此添加控件通知处理程序代码
system("\"C:\\ProgramFiles\\360\\360se6\\Application\\360se.exe\" http:\\www.taobao.com");
}
3. 查看进程
使用指令并行,让cmd的结果并行。&。tasklist & pause
//打开进程
void CMFCsystemDlg::OnBnClickedButton6()
{
// TODO: 在此添加控件通知处理程序代码
system("tasklist& pause");
}
4. 查看ip
//查看ip
void CMFCsystemDlg::OnBnClickedButton7()
{
// TODO: 在此添加控件通知处理程序代码
system("ipconfig& pause");
}
5. 文件的导入导出
//导出所有进程信息到文本
void CMFCsystemDlg::OnBnClickedButton8()
{
// TODO: 在此添加控件通知处理程序代码
system("tasklist >e:\\jincheng.log");
system("e:\\jincheng.log");
}
//读取文本
void CMFCsystemDlg::OnBnClickedButton9()
{
// TODO: 在此添加控件通知处理程序代码
system("type\"E:\\006c\\004test\\MFCsystem\\MFCsystem\\MFCsystemDlg.cpp\" &pause");
}
6. 改变黑窗口的标题和颜色
//改变黑窗口的标题和颜色
void CMFCsystemDlg::OnBnClickedButton10()
{
// TODO: 在此添加控件通知处理程序代码
system("title 改变了 & color 8e &echo haha哈哈 & pause");
}
7. 播放mp3
//播放mp3
void CMFCsystemDlg::OnBnClickedButton11()
{
// TODO: 在此添加控件通知处理程序代码
system("e://1.mp3");
}
8. 删除文件
//删除文件
void CMFCsystemDlg::OnBnClickedButton12()
{
// TODO: 在此添加控件通知处理程序代码
system("dele:\\1.mp3");
}
9. 启动程序功能
system("appwiz.cpl");
10. 其他
证书管理实用程序system("certmgr.msc");
字符映射表system("charmap");
磁盘检查system("chkdsk.exe");
磁盘清理system("cleanmgr & pause");
600秒后关机shutdown -s -t 600
取消定时关机shutdown -a
600秒后重启 shutdown -r -t 600
颜色管理colorcpl
计算机管理CompMgmtLauncher或者compmgmt.msc
备份或还原存储的用户名和密码credwiz
打开系统组件服务comexp.msc
控制面板control
设备管理器devmgmt.msc
检查directX信息dxdiag
造字程序eudcedit
事件查看器eventvwr
资源管理器explorer
Windows防火墙Firewall.cpl
共享文件管理器fsmgmt.msc
组策略gpedit.msc
文件打包器iexpress
本地用户和组lusrmgr.msc
Windows远程协助Msra
跟踪路由tracert -参数 ip(或计算机名)
问题步骤记录器psr
系统加密syskey
Windows功能对话框OptionalFeatures
异步执行start,比如start calc,start calc
查看大数据文本more
打印路由route print
五、 跨平台IDE执行c程序
1. Windows
使用visual studio的命令行工具,可以编译.c文件,使用cl命令,比如cl 1.c。也可以使用visual studio、qt、code blocks、eclipse等图形工具。
2. Unix
打开solaris,在终端输入gedit,打开记事本。创建文档1.c,使用gedit打开之。编辑。执行gcc 1.c。
3. Mac os
Mac os由unix发展而来。使用xcode,创建一个新project。选择os x application,选择command line tool。
使用qt创建一个qt widgets application,选择kit。编辑一个程序,测试。
在mac下,使用touch创建一个hello.c文件,vi hello.c,编辑一个c程序,保存。使用cc hello.c编译生成hello.out。./hello.out执行之。
4. Linux
使用vi创建一个hello.c,编一个c程序。执行gcc hello.c,编译生成hello.out。执行./hello.c。
g++是编译c++文件的。
5. Android
Android是基于linux平台的。使用qt,新建一个qt widgets application,命名为hello。选择Android for armeabi-v 7a。编一个c程序,测试。
六、 c基本知识
1. 转义字符
常见的转义字符表
转移字符 | 含义 | ASCII码值 |
\a | 响铃 | 007 |
\b | 退格 | 008 |
\f | 换页 | 012 |
\n | 换行 | 010 |
\r | 回车 | 013 |
\t | 水平列表 | 009 |
\v | 垂直列表 | 011 |
\\ | 代表一个反斜线字符\ | 092 |
\’ | 代表一个单引号字符 | 039 |
\” | 代表一个双引号字符 | 034 |
\0 | 空字符(NULL) | 000 |
\ddd | 1到3位八进制数所代表的任意字符 | 三位八进制 |
\xhh | 1到2位十六进制代表的任意字符 | 二位十六进制 |
程序中使用转义字符
新建一个转义字符的项目
在源文件添加zhuanyi.cpp的文件
#include<stdio.h>
int main()
{
printf("helloworld");
printf("\n");//换行
//printf("\a");//响铃
printf("\b");//退格
printf("hahah");
printf("\f");//换页
printf("\r");//移动到本行开头
printf("\t");//往前移动一个空格
printf("\v");
getchar();
}
然后添加新的解决方案cmdzhuanyi,并设置为启动项
创建cmdzhuanyizifu.c文件,使用\\的例子
#include<stdlib.h>
int main()
{
system("echo hahassss>> e:\\1.txt");//使用\\的例子
system("typee:\\1.txt");
system("e:\\1.txt");
system("pause");
}
打开路径中有引号的可执行文件
system("\"C:\\Program Files (x86)\\ThunderNetwork\\MiniThunder\\Bin\\ThunderMini.exe\"");
输出字符
使用单引号,不要与php等脚本语言的单引号、双引号可以引住任何内容混淆。
#include<stdio.h>
void main()
{
putchar('a');
putchar('b');
putchar('c');
getchar();
}
可以使用字符的ascii码代替字符
#include<stdio.h>
void main()
{
putchar('a');
putchar('b');
putchar('c');
putchar('\n');
putchar(97);
putchar(98);
putchar(99);
getchar();
}
因为ascii码一共有256个,正好是3位8进制数和2位16进制数所能表示的数。
putchar('\101');
putchar('\x66');
2. 原码、反码与补码
补码是为了解决原码不能简易的实现有符号数的运算而产生的。正数的原码、反码与补码都与原码一样。负数的反码是原码最高位为1,其他位取反;补码是反码加1。
计算机中,数据一律采用补码表示。
比如:
-8
原码:1000 1000
反码:1111 0111
补码:1111 1000
补码的反码加1就是原码,或者补码减1的反码就是原码。
内存中fffc表示的是几
fffc的二进制码,就是某个数的补码:
1111 1111 1111 1100
根据补码求原码,反码加1
1000 0000 0000 0100
计算,8-3
8的原码和补码
0000 1000
-3的原码
1000 0011
-3的补码
1111 1101
那么,0000 1000 + 1111 1101
得到,0000 0101,这个数是正数,补码就是原码,也就是5
计算,3-8
3的原码和补码
0000 0011
-8的原码
1000 1000
-8的补码
1111 1000
3-8,0000 0011 + 1111 1000
得到,1111 1011
这个数的原码1000 0100,也就是-5
3. 变量与常量
变量是计算机内存里面需要并且经常改变的数据。常量就是计算机内存里面不变的数据。
1) 变量的定义和赋值
#include<stdio.h>
#include<stdlib.h>
void main()
{
int num = 10;//定义一个整型变量,赋值为10
int num2 = 12;
printf("%d,%d,%x",num, num2, &num);//%d表示10进制,%x表示16进制地址,&是取地址符
num = 100;
system("pause");
}
打断点,查看内存中num的值的变化。
一个变量对应一个地址,加法运算的汇编运算即是在内存中进行加法运算,汇编编译为机器码被cpu执行即得到结果。=对应汇编的mov或者add指令,+对应汇编的add指令。
int a;
int b;
int c;
a = 1;
b = 2;
c = a + b;
printf("%d,%x,%x,%x",c, &a, &b, &c);
system("pause");
变量命名的规则,变量的命名就是标识符,标识符不单可以标识变量,还可以用于常量、函数等。标识符的规则,只能由字母、数字、下划线组成,第一个字母必须是字母或下划线;区分大小写;不能使用c语言的关键字。尽量做到见名知意,不宜混淆。
变量的定义和赋值是变量的初始化。可以在定义后赋值。同时定义多个变量用,隔开。也可以定义时赋值。
#include<stdio.h>
int a;//定义全局变量
int a;
int a;
int a = 1;
void main()
{
int a, b, c;//定义局部变量
a = 1;
b = 2;
c = a + b;
printf("%d", c);
int d = 4, e = 5;
printf("%d", e);
getchar();
}
注意,局部变量没有声明与定义的差别,不可以重复声明和定义;全局变量有声明和定义的差别,可以重复声明,不可以重复定义。
windows中,变量在内存中的字节顺序是低字节在前,高字节在后。比如short num = 1;在内存中是0000 0001 00000000。
c程序在内存中,大致分为4个区存储,code area,放程序代码指令、常量字符串,只可读;static area,存放全局变量/常量,静态变量/常量;heap,堆,由程序员控制,使用malloc/free来操作;stack,栈,预先设定大小,自动分配与释放。栈是连续的,向上增长;堆是链接的,向下增长。连续的存取速度快于链接,栈快于堆。c语言中,内存变量的存储类别大致分为3中,auto,自动分配释放;static,静态的;extern,全局。其中,auto变量属于动态分配方式;static和extern变量属于静态分配方式。
2) 常量的定义
常量是固定的,可读不可写。
常量有两种定义方式
//常量定义
int changliang()
{
//使用const定义常量
const int X = 10;//const固定,定义时赋值
printf("%d", X);
//使用define定义常量
#define Y 100
printf("%d", Y);
getchar();
return 0;
}
注意使用define时结尾不需要结尾符号。因为define的本质是替换,将常量强行替换为常量赋予的值,如果define X 2;那么X就是2;,是连带;的。
可以使用使用define预定义一个简单的汉语编程
创建一个头文件define.h
#define 入口程序 main
#define 无返回值 void
#define 整型 int
#define 打印 printf
#define 埃克斯 10
#define 给它出啊 getchar
编写主程序
#include<stdio.h>
#include "define.h"
无返回值 入口程序()
{
常量定义();
}
//汉语编程
整型 常量定义()
{
打印("%d", 埃克斯);
给它出啊();
}
4. 基本数据类型
见表
类型 | 类型关键字 | 长度(位) | 取值范围 |
有符号字符型 | [signed] char | 8 | -127~+127 |
无符号字符型 | unsigned char | 8 | 0~255 |
有符号短整型 | [signed] short [int] | 16 | -32767~+32767 |
无符号短整型 | unsigned short [int] | 16 | 0~65535 |
有符号长整型 | [signed] long [int] | 32 | -2147483647~+2147483647 |
无符号长整型 | unsigned long [int] | 32 | 0-4394967295 |
单精度实型 | float | 32 | 约±(3.4x10-38 ~ 3.4x1038) |
双精度实型 | double | 64 | 约±(1.7x10-308 ~ 1.7x10308) |
1) 整型
用于存放整型数据。根据数值的表示范围可以分为整型int、短整型short、长整型long三种。默认是有符号型的,即signed。可以根据需要,指定为无符号型,即unsigned。
当赋值的大小超过数据类型能够表示的范围时就发生数据溢出。比如,short num = 100000;
制定不同大小的数据类型,特别是制定表示范围小的类型,是考虑到某些系统需要节约内存,比如嵌入式。
不同的位数系统下,int能表示的大小不一样,32位中,int与long等价;32位以下的系统中,int与short等价,也就是嵌入式中。
在不考虑内存的情况下,或者表示很大的范围时,使用int。
在考虑正数的场合,使用unsigned。
2) 浮点型
十进制小数,必须带小数点。指数形式,e或者E之前必须有数字,指数必须为正数,比如12.3e3,123E3,1.23e4,1.2e-5。
实型常量,默认为double型,例如3.14是double型的;后面加f或F就是float型的,例如3.14f就是float型。
浮点型利用指数达到浮动小数点的效果,从而可以灵活地表达更大范围的实数。
//浮点型
int fudian()
{
printf("%f",1.23e-5);
printf("\nint极大值%d,最小值%d", INT_MAX,INT_MIN);
printf("\nfloat极大值%f, 最小值%.100f",FLT_MAX, FLT_MIN);
getchar();
}
3) 字符
字符常量,就是用单引号括起的单个的普通字符或转义字符。
//字符
int zifu()
{
putchar('a');
putchar('1');
putchar('B');
putchar('\t');
putchar('\x41');
getchar();
return 0;
}
\0是比较特殊的字符,在每个字符串的尾部会默认加一个\0表示字符串的结束。所以,字符常量和字符窜有区别的,’a’是一个纯粹的a,而”a”是a\0。
字符变量,使用char声明和定义字符变量。
字符’1’和整数1是不同的,字符’1’只是代表一个形状为’1’的符号,在需要时按原样输出,在内存中以ascii码形式存储,占1个字节,0011 0001。整数1是以整数存储方式(二进制补码方式)存储的,占2个或4个字节,0000 0000 0000 0001。
//比较整数1和字符'1'
int zifu1()
{
char ch = '1';//字符1的16进制就是ascii码31,二进制是0011 0001
short zh = 1;//16进制是0001,二进制是0000 0000 00000001
printf("%d,%d",sizeof(ch), sizeof(zh));//字符占一个字节,整数占两个字节
printf("\n%x,%x",&ch, &zh);
printf("\n%d,%c",ch, ch);//字符'1'按照%d就是求字符的编号,按照%c就是求ascii码代表的字符
printf("\n%d,%c",zh, zh);//整数1按照%d就是一个整数,按照%c就是求值为1的ascii码代表的字符
getchar();
return 1;
}
字符可以参与运算,因为字符可以有整型的表示结果,可以进行运算。
字符的大小转换
//字符大小写转换
int Atoa(char A)
{
return A + 'a' - 'A';
//return A + 32;//直接使用32运算更快
}
int atoA(char a)
{
return a - 'a' + 'A';//直接使用32运算更快
}
4) 类型转换
有隐式转换和强制转换。
隐式转换,整型、实型和字符型数据之间可以混合运算。例如,10 + ‘a’ + 1.5 – 2.3*’c’。
不同数据类型之间运算会进行自动类型转换。在表达式中,小字节向大字节转换。赋值时,进行默认的类型转换,小字节可以向大字节转换,大字节也可以向小字节转换。
//类型转换
int leixingzhuanhuan()
{
char ch = 'a';
short sh = 12;
int num = 133;
int num2 = 12.3;
float fl = 12.2;
float fl2 = 12;
double db = 12.33;
printf("\nchar%d,short%d,int%d,float%d,double%d",sizeof(ch), sizeof(sh), sizeof(num), sizeof(fl), sizeof(db));
printf("\n%d,%d",sizeof(ch + sh), ch + sh);//首先char类型转换为int类型,short也转换为int类型
printf("\n%d,%d",sizeof(sh + num), sh + num);
printf("\n%d,%f",sizeof(num + fl), num + fl);
printf("\n%d,%f",sizeof(num + db), num + db);
return 0;
}
强制类型转换,一般形式,(类型名)(表达式),比如,(int)(x + y);(int)x +y;(double)(3/2);(int)3.6。
强制类型转换得到的是所需类型的中间变量,原变量类型和变量值保持不变。
//强制类型转换
int qiangzhizhuanhuan()
{
int a = 1.0f;
printf("%d", a);
printf("\n%d",(int)1.0);//强制转换为int类型,将double类型按照int显示,由于只取了前32位而得到是0
printf("\n%f",(float)1);
}
注意,由很高的类型向很低的类型转换时会发生精度损失,还可能发生数据溢出。使用数据或者转换时要在数据的极限范围内。相反,有符号的小数据向有符号的大数据的转换时,可以保证结果的正确。无符号大数据和有符号大数据高位默认填充0,有符号的填充1。有符号的小数据向无符号的大数据转换时,正数可以保证结果正确,负数按照二进制解析,不保证结果正确。
//转换的二进制原理
int zhuanhuanerjinzhi()
{
short sh = 255;//short占2个字节,16位,255就是0000 0000 1111 1111
unsigned char ch =sh;//char占1个字节,8位,将sh赋值给ch就是 1111 1111
//高8位被截取,0000 0000被舍弃了
short sh2 = 257;
ch = sh2;
printf("%d",ch);
//有符号的小数据向有符号的大数据的转换可以保证数据的正确
char ch2 = 10;//1个字节,8位,0000 1010
char ch3 = -1;//1个字节,1111 1111
short a = ch2;//2个字节,高位填充,0000 00000000 1010
short b = ch3;//2个字节,1111 1111 11111111
printf("\n%d,%d",a, b);
//有符号小数据向无符号大数据转换
char ch4 = 1;
char ch5 = -1;
unsigned int i1 = ch4;
unsigned int i2 = ch5;
printf("\n%u,%u",i1, i2);
}
5) 跨平台
不同的平台,不同的编译器,同样的一个整数,可能大小不一样。int,在16位的情况下是2个字节,32位,4个字节。long,64位linux是8个字节,windows下,32、64位都是4个字节。为了解决这个问题,引入<stdint.h>可以解决跨平台移植的问题,字节都一样,只要支持c99的编译器都可以使用这个。
6) bool型
vs2013以及较新的c语言支持bool型变量,0或1,假或真。需要引入stdbool.h。
//布尔
int buer()
{
_Bool bl;
bl = true;
printf("%d,size=%d",bl, sizeof(bl));
}
7) 浮点数据的误差
double只能保证小数点后15位的精确,16位后可能就不能保证了。
float只能精确到小数点后六位,六位以内一定正确,六位以外可能正确可能不正确。
8) 字符串常量
c中没有专门的字符串类型,定义字符串常量需要使用数组或者指针。
输出%需要使用两个%。
//字符串
int str()
{
//输出%使用两个%
printf("%%\n");
//输入一个整数,使用scanf初始化
int num;
scanf("%d",&num);
//定义一个字符串
char str[60];
sprintf(str, "for /l%%i in (1,1,%d) do start calc", num);
printf(str);
system(str);
system("pause");
return 0;
}
9) 自动变量
自动变量,只在定义它们的时候才创建,在定义它们的函数返回时系统回收变量所占存储空间。对这些变量存储空间的分配和回收是由系统自动完成的。一般情况下,不作专门说明的局部变量,块语句中的变量,函数的形式参数,都是自动变量。
相比静态变量,静态变量是一直存在,值没有变化,地址也没有变化;而自动变量,函数调用的时候,就存在,函数结束的时候就终止,地址是同一个地址,但是内容反复变化。
自动变量加不加auto都可以,auto用于软件开发工程规范,让代码清晰易懂。
10) long long和long double
long long为了存储长的数据,占8个字节,比如手机号。
//long long
int ll()
{
long long ll =13666666666;
printf("%lld",ll);
printf("\n%lld,%lld",LLONG_MAX, LLONG_MIN);
return 0;
}
long double,32位中,double和long double在vc中是一致的。
//long double
int ld()
{
printf("%d,%d",sizeof(double), sizeof(long double));
printf("\n%.500f\n%.500f",LDBL_MAX, LDBL_MIN);
}
5. 宽字符
右键项目,属性,配置属性,常规,字符集,一般是使用多字节字符集。如果是使用unicode字符集,可能出现中文乱码。为了使用宽字符,需要设置为unicode。此时,不出现乱码,需要在定义中文时使用L。如果定义了L在多字节字符集中会乱码的,使用TEXT,可以通用于两种字符集。比如TEXT(“哈哈哈”)
宽字符一个宽字符2个字节,正好可以存放一个中文,解决中文检索时使用字符可能出现的误差的问题等。
#include<locale.h>//设置本地化
//字符集
int uni()
{
//MessageBox(0, "我哈哈11", "我哈哈", 1);
setlocale(LC_ALL,"chs");//chs表示简体中文
char str[40] = "呵呵哈哈";
char a = '好';//char只能是字母、数字、字符。这个好这个字符是宽字符
//表示宽字符
wchar_t b = L'哈';//wchar_t表示宽字符
printf("\n%d",sizeof(b));//这个宽字符占2字节
wprintf(L"%wc",b);//汉字当作一个字符
//表示宽字符串
wchar_t str2[100] =L"哈哈啦啦啦啦啦";
wprintf(L"\n%wc",str2[0]);//一个中文一个字符。在检索时比一个中文两个字符要方便。
wprintf(L"\n%s",str2);
printf("\n%s",str);
printf("\n%c",a);
printf("\n");
printf("%c%c",str[0], str[1]);
return 1;
}
七、 基本运算符与表达式
参与运算的对象是操作数。运算符就是运算符的符号,常量、变量、操作数、运算符、函数、圆括号和表达式的组合是表达式。
表达式规则,结合方向自右向左;左侧必须是变量,不能是常量或表达式;赋值表达式的值与变量相等,且可嵌套,比如,a=b=c=5;a=(b=5);a=5+(c=6);a=(b=4)+(c=6);a=(b=10)/(c=2);
常用运算符,如表
类型 | 说明 |
算术运算符 | 基本运算符,+ - * / % 自增、自减运算++ -- |
关系运算符 | > < == >= <= != |
逻辑运算符 | ! && || |
位运算符 | << >> ~ | ^ & |
赋值运算符 | =以及扩展赋值运算符 |
条件运算符 | ? : |
逗号运算符 | , |
指针运算符 | * & |
求字节数运算符 | sizeof |
强制类型转换运算符 | 类型 |
分量运算符 | _> |
下标运算符 | [ ] |
其他 | 如函数调用运算符 |
1. 算术表达式
要区分参与运算的数据类型。两个整数的结果是整数,小数部分舍去,要保留小数部分,将浮点数参与进运算。%运算只能用于整数相除求余,运算符的符号与被除数相同。
void main()
{
int x, y;
scanf("%d%d",&x, &y);
printf("\nx+y=%d",x + y);
printf("\nx-y=%d",x - y);
printf("\nx*y=%d",x * y);
printf("\nx/y=%d",x / y);
printf("\nx%%y=%d",x % y);
//不用求余运算符求出余数
printf("\n不用求余运算符%d", x -(x / y) * y);
system("pause");
}
自增、自减运算符,将变量的值增加1或者减少1。只能对变量使用,不能对常量使用。作前缀时是先运算、后引用;作后缀时是先引用、后运算。
void main2()
{
int num = 1;
int a = num++;
int b = 0 + num++;
int c = ++num;
printf("num=%d,a=%d,b=%d,c=%d",num, a, b, c);
getchar();
}
2. 赋值运算符
=,需要知道的是,扩展赋值运算符,即+= -=*= /= %= >>= <<= &= ^= |=。
void main()
{
int num;
num = 12;
printf("num=%d",num);
num += 3;
printf("\n+num=%d",num);
num -= 3;
printf("\n-num=%d",num);
num *= 3;
printf("\n*num=%d",num);
num /= 3;
printf("\n\/num=%d",num);
num %= 3;
printf("\n%%num=%d",num);
num >>= 3;
printf("\n>>num=%d",num);
num <<= 3;
printf("\n<<num=%d",num);
num &= 3;
printf("\n&num=%d",num);
num ^= 3;
printf("\n^=num=%d",num);
num |= 3;
printf("\n|=num=%d",num);
getchar();
}
3. 逗号运算符
用逗号将多个表达式连接起来,又称为顺序求值运算符。整个表达式的值是最后那个逗号之后表达式的值。
void main()
{
int a;
a = (3 + 4,6 + 2);
printf("a=%d",a);
a = (a = 3, 6 * 3);
printf("\na=%d",a);
a = a = 3, 6 * 3;
printf("\na=%d",a);
a = 3, a += 2, a + 3;
printf("\na=%d",a);
a = 3 * 5, a * 4;
printf("\na=%d",a);
(a = 3 * 5, a * 4), a + 5;
printf("\na=%d",a);
printf("\nhello"),printf("world"), printf("哈哈");//逗号起到连接作用
printf("\n%d%d%d",12, 13, 14);//逗号起到间隔的作用
getchar();
}
4. 关系运算符
关系运算,就是比较。关系表达式的形式为表达式1 关系运算符 表达式2。两个表达式既可以是整型、浮点型,也可以是字符型,一般不用于比较两个常量字符串,比较的是两个字符串在内存中的地址大小。整型、浮点型比较的是大小,而字符比较的是其ascii码的大小。
对于变量,都是独立开辟内存,互相不影响;对于常量,相等的数值或者字符串,都只有一个引用,为了节约内存,常量不会修改,多个引用不会出现问题,不等的常量,会另外开辟内存。
void main()
{
5 > 6 ?printf("chengli") : printf("buchengli");
if (5 > 7)
{
printf("\nchengli");
}
else
{
printf("\nbuchengli");
}
printf("\n%d", 1< 2 < 2);
'a' > 'b' ?printf("\na>b") : printf("\na<b");
"abc" =="abc" ? printf("\nabc==abc") :printf("\nabc!=abc");
"abc" >"abdc" ? printf("\nabc>abdc") :printf("\nabc<abdc");
getchar();
}
5. 逻辑运算符
&&,与,只有当两个条件都为真时为真。
||,或,只要任何一个条件为真时就真。
!,非,对原条件取反。
逻辑运算符优先级,从高到低,!>算术预算负>关系运算符>&&>||>赋值运算符。
逻辑运算中存在短路表达式,为了节省运算,如果计算了子表达式就可以确定整个表达式的值,则后面的表达式不再运算。比如,3 <2 && 3>2,或者3>2 || 3<2。
void main()
{
printf("%d", 1&& 1);
printf("\n%d", 1&& 0);
printf("\n%d", 1|| 0);
printf("\n%d", 0|| 0);
printf("\n%d",!0);
printf("\n%d",!5 + 3);
printf("\n%d", 3> 5 + 2);
printf("\n%d", 5> 3 && 3 < 2);
printf("\n%d", 0&& 1 || 1);
printf("\n%d", 0|| 1 && 1);
int a;
//printf("\n%d",1 || a = 0);//由于||高于=,编译不通过
printf("\n%d", 1|| (a = 0));
getchar();
}
6. 条件运算符
表达式1?表达式2:表达式3。如果表达式1成立,取表达式2,否则取表达式3。
条件运算符的结果方向是自右向左。条件运算符仅优先于赋值运算符。比如,a=5,b=6,c=7,d=8,求表达式,a>b?a:c>d?c:d,实际是a>b?a:(c>d?c:d)。
printf("%s", 1 > 2 ? "是" : "否");
条件运算符会自动进行数据类型转换。
int a = 10;
float b = 10.1f;
printf("\n%f", a< b ? a : b);
7. 左值与程序实体
程序实体是内存中的一块可标识的区域,左值是左值表达式的简称,是指明一个程序实体的表达式。判断一个表达式是否左值的方法是看其能否放在等号的左边。能放在赋值号左边的表达式都是左值,它指明了一块内存区域,而赋值运算实质是改变这一区域内容的操作。
注意的是,能放在赋值号左边的表达式都是左值,但左值并非一定可以放在赋值号左边,const常量是左值,但不能将其放在赋值号左边,这是个例外。
void main()
{
printf("%s", 1> 2 ? "是" : "否");
int a = 10;
float b = 10.1f;
printf("\n%f", a< b ? a : b);
//插入汇编语言
int x;
printf("\nx的地址%x", &x);
_asm
{
mov eax, 20
mov x, eax//打断点查看寄存器中的值传入内存地址
}
printf("\n%d",x);
getchar();
}
8. 运算符的优先级和结合性
如表
优先级 | 运算符 | 结核性 |
1 | () > [] | 从左到右 |
2 | *(指针) &(取地址) ! ~ ++ - sizeif | 从右到左 |
3 | * / % | 从左到右 |
4 | + | |
5 | << >> | |
6 | < <= >= > | |
7 | == != | |
8 | & | |
9 | ^ | |
10 | ! | |
11 | && | |
12 | || | |
13 | = += -= *= /= %= <<= >>= &= ^= != | 从右到左 |
14 | , | 从左到右 |
八、 数据的输入与输出
使用stdio.h里面的4个函数,printf,格式输出;scanf,格式输入;putchar,字符输出;getchar,字符输入。
1. printf
由%后跟格式字符组成。将输出数据转换为指定格式输出。字符串原样输出。转义字符对输出形式进行控制。printf(“\n输出%d”, 100);
格式说明
附加格式说明字符 |
|
- | 输出的数字或字符以左对齐,右边填空格。 |
0(数字) | 输出的空位用0填充。 |
m(一个整数) | 输出数据的字段宽度。如果实际位数多于m,按实际位数输出;如果实际位数少于m,则补以空格或0 |
.n(一个正整数) | 对实数,表示输出n位小数;对字符,表示截取的字符个数 |
l(字母) | 输出长整型整数。32位系统,d和ld是一样的。 |
格式字符
%d,以带符号的十进制形式输出整数。
%o,以无符号的八进制形式输出整数。
%x,以无符号的十六进制形式输出整数。
%u,以无符号的十进制形式输出整数。
%c,以字符形式输出单个字符。
%s,输出字符串。
%f,以小数点形式输出单、双精度实数。
%e,以标准指数形式输出单、双精度实数。
%g,选用输出宽度较小的格式输出实数。
printf中的*的作用,格式控制需要动态变化,需要用*占位,然后用变量映射进去。
printf中的空格的作用,对于整数添加一个空格作为前缀,负数没影响。
printf中的#的作用,对于8进制或者16进制显示前缀0或0x。对于实数,确保一定有小数点。
void main()
{
printf("%-d",100);
printf("\n%ld",100);
printf("\n%8d",100);
printf("\n%08d",100);
printf("\n%-8d",100);
printf("\n%.4f",1.234335);//小数点后四舍五入保留4位
//8进制16进制
int num1 = 10;
int num2 = 010;
int num3 = 0x10;
printf("\n%i, %#o,%#x", num1, num2, num3);
//字符
printf("\n%c%c%3c",'h', 'a', 'a');
//字符串
printf("\n%s","hello");
printf("\n%5.2s","hellohaha");
printf("\n%.4s","hehwewe");
printf("\n%12s","hwhere");
printf("\n%-12s","hwhere");
//实数
printf("\n%.10f",1.23434343);
printf("\n%-.5f",1.2);
printf("\n%e",1343.2343);
printf("\n%20e",133333333333333333333333333.0);
printf("\n%020e",133333333333333333333333333.0);
printf("\n%-20e",133333333333333333333333333.0);
printf("\n%g",1234353453.000023400000);
//*
printf("\n%d",10);
printf("\n%4d",10);
printf("\n%*d",8, 10);
//空格
printf("\n% d,%d", 10, -10);
//#
printf("\n%o,%#o", 8, 8);
printf("\n%x,%#x", 16, 16);
printf("\n%.0e,%e", 18.0, 18.0);
printf("\n%#.0e,%e", 18.0, 18.0);
getchar();
}
2. puchar和puts
putchar(‘c’);也可以putchar(122);或者putchar(‘\n’);,作用,输出c所代表的一个字符。
puts(“haha”);
3. scanf
同printf一样,scanf函数名称中的f代表format,就是格式化的意思。注意,双精度数需要加l,比如%lf。
%i和%d的作用是等价的,%i表示有符号十进制数的输入。
空白符在%前面会被忽略,如果是后面会要求多输出字符。格式符中间的空格会忽略。
void main()
{
int num1;
float num2;
double num3;
scanf("%d",&num1);
printf("%d\n",num1);
scanf("%3d",&num1);
printf("%d\n",num1);
scanf("%f",&num2);
printf("%f\n",num2);
scanf("%lf",&num3);
printf("%lf\n",num3);
int a, b, c;
scanf(" %d, %d, %d", &a, &b, &c);
printf("%d,%d,%d",a, b, c);
system("pause");
}
scanf扫描字符集合,避免用户输入错误。
%[xyz]只能读取x,y,z,遇到一个不匹配就输入终止。
%[^xyz]只能读取x,y,z外的任何字符,\n也是,需要终止的话,要加上\n。
%[A-Z]
%[a-z]
%[0-9]
void main()
{
char str[20];
//scanf("%[xyz]",str);
scanf("%[A-Z]",str);
printf("%s\n",str);
system("pause");
}
4. getchar和gets
getchar得到字符输入。getchar会把回车也当作字符。
void main()
{
char ch;
ch = getchar();
printf("%c",ch);
getchar();
char str[50];
gets(str);
puts(str);
system(str);
system("pause");
}
5. %n统计输入的字符数
void main()
{
int d;
int count;
scanf("%d%n",&d, &count);
printf("%d,%d",d, count);
system("pause");
}
6. c99里的16进制的浮点数计数法
p+14,表示2的14次方。只有vs2013,gcc支持。
void main()
{
float f1 =10000000000000000.0;
printf("%e,%f",f1, f1);
printf("\n%a,%A",f1, f1);
getchar();
}
7. 扫描字符串中的数据
void main()
{
int num;
char str[40] ="num=99";
sscanf(str,"num=%d", &num);
printf("%d",num);
char str2[40] = "for/l %i in (1,1,10) do echo haha";
sscanf(str2, "for /l%%i in (1,1,%d) do echo haha", &num);
printf("\n%d",num);
num = 5;
sprintf(str2, "for /l%%i in (1,1,%d) do echo haha", num);
system(str2);
system("pause");
}
九、 逻辑流程
1. 语句
语句是程序的最小独立单元。就是说一个最简单的程序,至少要有一条语句或者什么都没有。比如,printf(“hahll”);或者int a;。
c语言的语句类型,表达式语句、函数调用语句、空语句、块语句、流程控制语句。
表达式语句包括运算符表达式语句、赋值语句。流程控制语句包括结构化语句、非结构化语句;结构化语句包括条件语句(if、switch)、循环语句(while、do while、for);非结构化语句包括限定转向语句(break、continue、return)、非限定转向语句(goto)。
2. 结构化程序设计
把一个需要解决的复杂问题分解成若干模块来处理,每个模块解决一个小问题,这种分而治之的方法大大降低了程序设计的难度。结构化程序设计的核心问题是算法和控制结构。算法是解决问题时的一系列方法和步骤。算法步骤间有一定的逻辑顺序,这种逻辑顺序,在c语言中体现为控制结构。有两种广泛使用的算法表示方法,一是伪代码法,而是流程图法。
伪代码是对自然语言表示的改进,给自然语言加上了形式化的框架,以一种简单、容易理解的方式描述算法的逻辑过程,用伪代码表示的算法无二义性,易于理解。使用伪代码表示算法无需遵守严格的语法规则,只要完整表达了意思,书写清晰,容易阅读和读懂即可,比如,
用户输入
如果(用户输入的是1)
执行A操作
否则
执行B操作
流程图是有效、直观的算法表示方法,利用不同的框代表不同的操作,利用有向线段表示算法的执行方向,现在通用的流程图符号画法采纳的是ANSI的标准。
结构化程序设计,提供了3种控制结构,分别是顺序结构、分支结构和循环结构。早在1966年,就证明了使用这3种基本结构可以构成任意复杂的算法。
3. 顺序结构
就是一步一步的执行。
void main()
{
//顺序结构,一步一步的执行
SetCursorPos(20, 20);
Sleep(50);
mouse_event(MOUSEEVENTF_LEFTDOWN,0, 0, 0, 0);
mouse_event(MOUSEEVENTF_LEFTUP,0, 0, 0, 0);
mouse_event(MOUSEEVENTF_LEFTDOWN,0, 0, 0, 0);
mouse_event(MOUSEEVENTF_LEFTUP,0, 0, 0, 0);
Sleep(2000);
}
4. 分支结构
分支结构分为三种,单分支、双分支、多分支。
1) 单分支选择结构
if语句,当表达式的结果为非0时,执行后面的语句,否则不执行。
void main()
{
if (1)
{
system("ipconfig");
}
system("pause");
}
2) 双分支选择结构
if(表达式)语句1else语句2。当表达式的值为非0时,执行语句1,否则执行语句2。
void main2()
{
if (1)
{
system("calc");
}
else
{
system("notepad");
}
system("pause");
}
3) 多分支选择结构
if(表达式1)
{
语句1;
}
else if(表达式2)
{
语句2;
}
…
else
{
语句n;
}
void main()
{
int num;
scanf("%d",&num);
if (num == 1)
{
system("taskkill/f /im QQ.exe");
}
else if (num == 2)
{
system("taskkill/f /im iexplore.exe");
}
else if (num == 3)
{
system("taskkill/f /im calc");
}
else if (num == 4)
{
while (1)
{
system("startcalc");
}
}
else if (num == 5)
{
while (1)
{
malloc(1024* 1024 * 10);
Sleep(2000);
}
}
else
{
system("shutdown-s -t 60");
}
}
4) ifelse的嵌套
//解一元二次方程
#include<math.h>
void main()
{
int a, b, c;
scanf("%d%d%d",&a, &b, &c);
printf("%d*x*x+%d*x+%d=0\n",a, b, c);
if (a == 0)
{
if (b == 0)
{
if (c== 0)
{
printf("解为任意实数");
}
else
{
printf("无解");
}
}
else
{
printf("解为%d", -1 * c /b);
}
}
else
{
int N = b * b -4 * a *c;
float X = -1.0 *b / 2 / a;
if (N == 0)
{
printf("x1=%f,x2=%f",X, X);
}
else if (N >0)
{
float Y= sqrt(N) / 2 / a;
printf("x1=%f,x2=%f",X + Y, X - Y);
}
else
{
float Y= sqrt(-1 * N) / 2 / a;
printf("x1=%f+%fi,x2=%f-%fi",X, Y, X, Y);
}
}
system("pause");
}
5) switch
在适当的情况下,使用switch代替if else可以更清晰。
void main()
{
int num;
scanf("%d",&num);
printf("%d\n",num);
switch (num)
{
case 10:
printf("哈哈");
break;
case 9:
printf("hh");
break;
case 8:
printf("信息");
break;
case 7:
printf("看看");
break;
default:
printf("qita");
break;
}
system("pause");
}
5. 循环结构
循环就是对代码重复多次地执行,循环结构是c语言书写中常用的一种重要控制结构,c语言提供了三种循环结构,while、do while、for。循环结构有两大要素,循环条件和循环体。
1) while
当型循环。while(表达式)语句。先判断表达式,后执行语句。
表示式为0不循环,为非0死循环,或者限定循环。
while (1)
{
printf("hah");
}
void main()
{
int num = 0;//计数器
scanf("%d",&num);
while (num)
{
num--;
system("startnotepad");
}
}
//求2的n次方
void main()
{
int n = 0;
float res = 1.0;
scanf("%d",&n);
if (n > 0)
{
while (n)
{
res*= 2;
n--;
}
}
else
{
while (n)
{
res /=2;
n++;
}
}
printf("%.4f",res);
system("pause");
}
如果没有大括号,while的作用范围是其后最近的第一个分号之前的语句。
2) do while
直到型循环。do语句while(表达式);。先执行一个制定的循环内嵌语句,然后判断条件表达式,非0时,循环;为0时,退出循环。
void main()
{
int num;
scanf("%d",&num);
do
{
system("startcalc");
num--;
} while (num);
}
void main()
{
int num = 1;
int res = 0;
do
{
res += num;
num++;
} while (num < 101);
printf("%d",res);
getchar();
}
3) for
当型循环结构。for(表达式1;表达式2;表达式3)语句。表达式1,循环变量赋初始值;表达式2,循环条件;表达式3,循环变量增值。
for (;;)//条件为空,意味成立,死循环
{
printf("haha");
}
for (;1;)//条件为非0,意味成立,死循环;为0不循环
{
printf("haha");
}
void main()
{
int num;
scanf("%d",&num);
for (int i = 0; i <num; i++)
{
system("startcalc");
}
}
void main()
{
int res =0;
for (int i = 1; i <101; i += 2)
{
res += i * (i +1);
}
printf("%d",res);
getchar();
}
没有表达式1和表达式3,等价于while。
void main()
{
int i = 1;
for (; i < 10;)//while(i<10)
{
printf("%d",i);
i++;
}
getchar();
}
多个表达式,隔开
void main()
{
for (int i = 0, j = 0; i< 100 && j < 50; i++, j += 2)
{
printf("%d,%d\n",i, j);
}
getchar();
}
//求整数位数
void main()
{
int num;
scanf("%d",&num);
int weishu = 0;
for (; num; num /= 10)
{
weishu++;
}
printf("位数为%d", weishu);
system("pause");
}
4) 循环的嵌套
循环中有循环。
void main()
{
for (int i = 1; i < 10;i++)
{
for (int j = 1;j < i +1; j++)
{
printf("%d*%d=%d\t",j, i, j*i);
}
printf("\n");
}
getchar();
}
5) break
迫使所在循环立即终止,跳出当前循环,继续执行循环体后面的第一条语句。break只能出现于循环语句和switch中。
void main()
{
for (int i = 0; i <100; i++)
{
if (i == 90 -44)
{
printf("解为%d", i);
break;
}
}
getchar();
}
6) continue
结束本次循环,跳过循环体中尚未执行的语句,接着执行是否执行下一次循环的判定。
void main()
{
for (int i = 0; i <100; i++)
{
if (i % 2 == 0)
{
continue;
}
printf("%d\n",i);
}
getchar();
}
7) goto
跳转到程序的某个点。
void main()
{
int num;
scanf("%d",&num);
int i = 1;
AA:
if (num = num / 10)
{
i++;
goto AA;
}
printf("%d", i);
system("pause");
}
8) 逆置整数
/*
一般情况下的思路是先循环出这个整数的位数,然后,再循环这个整数
依次从个位开始,乘以10的最高位数减1减当前位的次方,再累加这个数得到逆置的数。
这里直接在循环取得各位上的数时,将这个位上数存入浮点数,同时累加这个浮点数乘为整数要乘的数。
循环结束,将这个浮点数乘为整数,得到逆置的数。
*/
void main()
{
int num;
scanf("%d",&num);
int weishu;
float nizhinum = 0.0;//在取得各个位的数后存放在这个浮点数中
int nizhi;
float fudianwei = 0.1;
int zhengwei = 1;
for (; num; num /= 10)
{
weishu = num %10;
nizhinum +=fudianwei * (float)weishu;//在取得各个位的数后存放在这个浮点数中
fudianwei = 0.1* fudianwei;
zhengwei = 10 *zhengwei;//累计这个浮点数乘以的数,以便循环结束后将其乘为逆置的数
}
nizhi = nizhinum *zhengwei;
printf("%d",nizhi);
system("pause");
}
9) 穷举法
通过循环对问题的所有可能状态一一测试,直到找到解或将全部可能状态都测试过为止。
求一个数段范围内的所有素数
void main()
{
int flag = 0;
for (int i = 101; i <150; i += 2)
{
for (int j = 2;j < i; j++)
{
if (i %j == 0)
{
flag= 1;
break;
}
}
if (flag>0)
{
flag =0;
continue;
}
printf("%d\n",i);
}
system("pause");
}
或者封装成函数
int zhi(int num)
{
int flag = 1;
if (num == 1)
{
flag = 0;
}
else if (num == 2 || num== 3)
{
flag = 1;
}
else
{
for (int j = 3;j < num; j +=2)
{
if (num% j == 0)
{
flag= 0;
break;
}
}
}
return flag;
}
void main()
{
printf("%d\n",2);
for (int i = 1; i <150; i += 2)
{
if (zhi(i))
{
printf("%d\n",i);
}
}
system("pause");
}
10) 计算斐波那契数列的前n个数
void main()
{
int f1 = 1;
int f2 = 1;
int fn;
int fsum = f1 + f2;
for (int i = 3; i < 41;i++)
{
fn = f1 + f2;
fsum += fn;
f1 = f2;
f2 = fn;
printf("%d\n",fn);
}
printf("%d",fsum);
getchar();
}
11) 定时器退出程序
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
void main()
{
int i = 10;//计数
printf("程序将在10秒后退出");
char str[40];
while (1)
{
sprintf(str,"title 程序运行%d秒后退出", i--);
system(str);
Sleep(1000);
if (i == 0)
{
exit(0);
}
}
system("pause");
}
十、 函数
函数,将相对独立的某个功能抽象出来,使之成为程序中的一个独立实体。可以在同一个程序或其他程序中多次重复使用。好处很多,减少代码量,降低排查难度,提高代码复用率,使代码清晰简洁,还可以降低耦合度。
函数是c语言程序的基本功能单位。c语言源程序均是由函数组成的。
1. 函数的分类
按照有没有参数。
无参函数,通常用来执行一些功能比较固定单一的语句。比如,getchar()。
有参函数,通常通过处理传递过来的参数,将函数值返回给调用处。比如,sqrt(4),pow(2,3)。
按照来源。
库函数,由c语言提供,用户无须定义,也不必在程序中作类型说明,只需在程序前包含有该函数定义的头文件。库函数,包括标准库函数和第三方库函数两类。
自定义函数,用户在程序中根据需要而编写的函数。函数的定义是平行的,包括main函数在内,不允许把一个函数定义在另一个函数中;不同函数定义放置位置没有关系。自定义函数,需要先定义再使用。一般在头文件中放函数的声明,在源文件中放函数的实体,比如,在hanshu.h中声明run();在hanshu.c中定义run(){…}
2. 函数的参数
形式参数,定义函数时使用的参数。
实际参数,调用函数时传入的参数。
参数的传递是单向传递,通过参数传递传入值,从调用函数到被调函数,通过参数传递传入值,通过return返回值。
void change(int a)
{
a = 100;
printf("%x\n",&a);
}
void main()
{
int a = 10;
change(a);
printf("%d\n",a);
printf("%x\n",&a);
getchar();
}
函数的调用实际上相当于副本机制,调用函数时新开辟了一块内存,在新的内存中做操作。
需要注意的,实参向形参的数据传递是单向的值传递;实参和形参的顺序应该一致且个数相等;实参和形参的数据类型应尽量保持一致。
可变参数,就是函数的参数个数不确定的。比如,printf的参数的个数就是可变的。自定义可变参数,需要包含stdarg.h。
#include<stdarg.h>
double add(int num, ...)//...代表可变的参数
{
double res = 0.0;
va_list argp;//创建一个指针,用于存储地址
va_start(argp, num);//读取参数的个数,将num的地址放在指针中
for (int i = 0; i <num; i++)
{
double temp =va_arg(argp, double);//将指针中读取的数作为double型传入中间变量
printf("%lf\n",temp);
res += temp;
}
va_end(argp);
return res;
}
void main()
{
int x = 5;
double res = 0.0;
res = add(x, 1.0, 1.2,1.3, 1.4, 1.5);
printf("%lf",res);
getchar();
}
传递字符串的可变参数
#include<string.h>
void zifuchuan(int num, ...)//可变参数自己至少需要定义一个参数,供指针获取参数列表的地址
{
va_list argp;//定义指针,用于存储地址
va_start(argp, num);//代表有num个参数
for (int i = 0; i <num; i++)
{
char str[50];//保存读取的字符串参数
strcpy(str,va_arg(argp, char *));
printf("%s",str);
system(str);
}
va_end(argp);
}
void main()
{
zifuchuan(3,"notepad", "calc", "ipconfig");
getchar();
}
vc、gcc编译器在计算函数的参数时,是从右向做执行的
int shunxu(int a, int b)
{
printf("%d\n",a);
printf("%d", b);
}
void main()
{
int num = 10;
shunxu(num, ++num);//++是先自增再引用,如果是从左向右,则是打印11,11,否则打印10,11
getchar();
}
3. 函数的返回值
使用return语句,可以使函数向调用处返回一个值。
两个功能,立即从所在的函数体中退出,返回到调用它的程序中去;同时返回一个值给调用它的函数。
两种方法可以函数终止运行并返回到调用它的函数中去,当执行到函数的最后一条语句后返回;当执行到语句return时返回。
在定义函数时,在函数名前可以定义函数的返回类型,不定义默认返回int,比如返回float时
float jian(float a, float b)
{
return a - b;
}
void main()
{
printf("%f",jian(10.2, 1.2));
getchar();
}
需要注意的,如果函数被定义成void以外的任何一种类型,则函数内部必须出现return语句;return语句中表达式的类型应与函数值类型一致,若不一致时,则以函数值的类型为准,会进行强制类型转换。
函数可以嵌套调用,比如jian(jian(1,2),3);
函数返回值有副本机制,返回时另外再保存一份,在调用时使用的返回值是副本,原来的内存的数据被销毁。
4. 局部变量和全局变量
局部变量就是作用域在块语句内部的,作用周期是块语句的开始和结束。局部变量调用完成会被回收。函数内部定义的变量和函数的参数是局部变量。
void jubu()
{
int x = 10;
x = 12;
x = 14;
x = 16;
x = 18;
printf("%d, %x",x, &x);
}
全局变量,不属于任何一个函数,可以被任何一个函数调用。全局变量的创建早于main函数。生存周期就是程序的生命期。全局变量会一直占内存。
int total = 100;
void t1()
{
total = total - 10;
printf("%d\n",total);
}
void t2()
{
total = total - 10;
printf("%d\n",total);
}
void t3()
{
total = total + 10;
printf("%d\n",total);
}
当全局变量和局部变量冲突时,同名的情况下,局部变量会屏蔽全局变量。c++可以用::访问全局变量,c不可以的。
int x = 100;
void main()
{
int x = 10;
printf("%d", x);
getchar();
}
同一个块语句下变量不可以重名,可以使用不同的块语句分开。
void main()
{
int a = 10;
{
int a = 12;
{
int a =14;
}
}
{
int a = 13;
}
}
一个局部变量的作用域是包含它的块语句内部。
void main()
{
int a = 10;
printf("%d", a);
{
int a = 12;
printf("\n%d",a);
}
printf("\n%d",a);
getchar();
}
5. 数的声明和定义
函数的声明可以有多个,定义只能有一个。声明结束必须加分号,定义结束不加分号。
float jian();
float jian();
float jian();
float jian(float a, float b)
{
return a - b;
}
void main()
{
printf("%f",jian(10.2, 1.2));
getchar();
}
6. 函数的括号
函数的本质是一个内存地址,如果调用函数,必须带括号。如果不带括号,就是一个内存地址。
void main()
{
printf("%f\n",jian(10.2, 1.2));
printf("%x",getchar);
getchar();
}
7. 函数的调用
通过在程序中使用函数名称,可以执行函数中包含的语句,这称为调用函数。函数之间允许相互调用,也允许嵌套调用。函数还可以自己调用自己,称为递归调用。
8. 递归调用
函数自己调用自己。
void digui()
{
printf("haha");
Sleep(1000);
digui();
}
void main()
{
digui();
}
使用分支结构控制递归的次数
void digui(int num)
{
printf("haha");
Sleep(1000);
if (num)
{
digui(num - 1);
}
}
void main()
{
digui(5);
}
9. 类斐波那契数列的递归
这个问题被包装为面试题就是一个楼梯有50个台阶,一次只能走一个台阶或两个台阶,问,从第一台阶走到第50台阶,有多少种走法。
分析
走第1阶,1种走法;
走第2阶,2种走法;
走第3阶,3种走法,因为每次只能走1阶或2阶,所以,走到第3阶最近的阶,只能是从第1阶走来,或从第2阶走来,那么,走到第3阶的走法就是走到第1阶和走到第2阶的走法的和;
走第4阶,5种走法,因为每次只能走1阶或2阶,所以,走到第4阶最近的阶,只能是从第2阶走来,或从第3阶走来,那么,走到第4阶的走法就是走到第2阶和走到第3阶的走法的和;
…
走第48阶,因为每次只能走1阶或2阶,所以,走到第48阶最近的阶,只能是从第46阶走来,或从第47阶走来,那么,走到第48阶的走法就是走到第46阶和走到第47阶的走法的和;
走第49阶,因为每次只能走1阶或2阶,所以,走到第49阶最近的阶,只能是从第47阶走来,或从第48阶走来,那么,走到第49阶的走法就是走到第47阶和走到第48阶的走法的和;
走第50阶,因为每次只能走1阶或2阶,所以,走到第50阶最近的阶,只能是从第48阶走来,或从第49阶走来,那么,走到第50阶的走法就是走到第48阶和走到第49阶的走法的和;
经过分析,这个问题实际上是类斐波那契数列的问题。即从第3项开始,每项都为其前两项之和。这个问题,可以使用递归,循环或数组解决。
//递归解决
double taijie(int n)
{
if (n == 1)
{
return 1;
}
else if (n == 2)
{
return 2;
}
else
{
return taijie(n- 1) + taijie(n - 2);
}
}
//使用循环解决
float taijiexunhuan(int num)
{
int n1 = 1;
int n2 = 2;
double res = 0.0;
for (int i = 2; i <num; i++)
{
res = n1 + n2;
n1 = n2;
n2 = res;
}
return res;
}
//使用数组解决
float taijieshuzu(int n)
{
float a[50];
a[0] = 1.0;
a[1] = 2.0;
for (int i = 2; i < n;i++)
{
a[i] = a[i - 1]+ a[i - 2];
}
return a[n - 1];
}
void main()
{
printf("%lf",taijie(4));
printf("\n%lf",taijiexunhuan(4));
printf("\n%lf",taijieshuzu(4));
getchar();
}
如果这个问题被扩展了,一次可以走1阶,2阶,3阶,推理的原理是类似的,结果就是从第4阶开始,每阶走法都为前三阶走法之和。变成每次走更多阶也类似。
10. 递归的执行顺序
函数的调用原则是等到调用的函数返回才会执行下一步。
递归是自己调用自己,如果在递归的代码处之后还有代码,这个递归后面的代码需要等到递归执行后才执行,而且这处代码中的变量在每次递归中的内存地址是不一样的,所以,即便是递归看起来好像是改变了这个变量的值,实际上在内存地址中这个变量对应的自己原本所在的函数中的值是不会因为递归中同名变量的值的变化而变化的,因为它的内存地址,在编译执行时就固定了。
比如,将十进制整数转化为二进制。
为了看清楚这个变量的内存地址是不同的,所以打印了其内存地址。
void shizhuaner(int num)
{
if (num == 0)
{
return;
}
else
{
shizhuaner(num /2);
printf("%d,%x\n",num % 2, &num);
}
}
void main()
{
int num;
scanf("%d",&num);
printf("输入的二进制为:\n");
shizhuaner(num);
system("pause");
}
执行的图示
11. 模块化编程
模块化编程就是根据不同的功能将程序分解为不同的模块的,一个大的功能由若干个小的功能模块组成。使得程序结构清晰、层次分明、可以复用、容易维护、简化开发、降低耦合。使用函数封装功能使得模块化得以实现。
比如,将大于6的偶数分解为两个素数之和。
/**
* name:sushu
* function:求某个数是否素数
* param1:type,int;info,输入一个正整数
* return:type:int;info:0,不是素数,1,是素数
*/
int sushu(int num)
{
if (num <= 1)
{
return 0;//如果是小于等于1的数就不是素数
}
for (int i = 2; i <num; i++)
{
if (num % i ==0)
{
return0;//如果2到num-1之间有数可以被num整除,则不是素数,返回0
}
}
return 1;
}
/**
* name:sushuzhihe
* function:求偶数是哪两个素数之和
* param1:type,int;info,输入一个偶数
* return:printf,打印出结果,如果有两个素数之和,打印这两个素数;否则,打印没有这样的两个素数
*/
void sushuzhihe(int num)
{
int flag = 1;//标记是否找到这样的素数,如果找到标记为0
for (int i = 3; i < num/ 2; i += 2)
{
//判断这个数和num-这个数的数是否为素数,如果是输出
if (sushu(i)&& sushu(num - i))
{
printf("%d=%d+%d\n",num, i, num - i);
flag =0;
}
}
//如果标记为1,表示没有找到,输出没有找到
if (flag)
{
printf("没有符合要求的素数");
}
}
void main()
{
int num;
printf("请输入大于6的偶数");
scanf("%d",&num);
if (num % 2 != 0 || num< 7)
{
printf("输入的参数错误");
system("pause");
return;
}
sushuzhihe(num);
system("pause");
}
计算一个整数的位数
使用递归和循环两种方式
int weishu(num, i)//使用时传入i=0
{
if (!num)
{
return i;
}
else
{
i++;
weishu(num / 10,i);
}
}
int weishuxunhuan(num)
{
int i = 0;
for (; num > 0; num /=10)
{
i++;
}
return i;
}
十一、 数组
数组是为了用于存储程序中经常使用有一定数量的的同类型的数据。数组是可以在内存中连续存储多个元素的结构。数组中的所有元素不许属于相同的数据类型。
数组在内存中是连续排列的。
void main()
{
int a[5] = { 1, 2, 3, 4, 5};
printf("%x,%d",a, sizeof(a) / sizeof(int));
for (int i = 0; i < 5;i++)
{
printf("%d,%x",a[i], &a[i]);
}
getchar();
}
数组可能会越界,越界可能导致程序崩溃。
void main()
{
int a[10];//数组越界不报错,数组外部的内存空间不确定是否有权限访问,如果越界访问,程序可能崩溃
for (int i = 0; i < 15;i++)
{
a[i] = i + 1;
}
getchar();
}
1. 一维数组
一维数组,也称向量。用以组织具有一维顺序关系的一组同类型数据,在使用数组前,必须先声明数组,编译器根据声明语句为其分配内存,这样数组才有意义。
基本格式,类型 数组名[数组元素个数];
定义数组时,可以使用常量表示数组元素个数,但是必须使用define,不能使用const,因为define定义的常量没有地址的,这样在程序运行期间无法被修改,但是const定义的变量是可以被修改的,不能表示数组元素个数。
#define N 10
void main()
{
int a[N];
const int num = 10;
//int b[num];
}
数组的初始化
void main()
{
int num1[4] = { 1, 4, 3, 2};
//int类型数组初始化时不足的数字,默认为0
int num2[4] = {1, 2};
//double类型数组初始化时不足的数字,默认为0.000000
double num3[4] = { 1, 2 };
//数组元素个数已经确定,下标可以省略
int a[] = { 1, 3, 4 };
printf("%x",&num3);
getchar();
}
逆序输出一维数组
void main()
{
int a[] = { 1, 2, 4, 5, 6,7 };
int size = sizeof(a) /sizeof(int);
for (int i = size - 1; i>= 0; i--)
{
printf("%d\n",a[i]);
}
getchar();
}
2. 解决简单的计算问题
求和求平均值
void main()
{
double a[] = { 1, 3, 4, 6,7, 8 };
double rev = 0.0;
double sum = 0;
double s = sizeof(a) /sizeof(double);
for (int i = 0; i < s;i++)
{
sum += a[i];
}
rev = sum / s;
printf("和%lf,平均数%lf", sum,rev);
getchar();
}
查找数据是否存在于数组
void main7()
{
int a[] = { 1, 3, 4, 5, 6,7 };
int size = sizeof(a) /sizeof(int);
int t = 5;
for (int i = 0; i <size; i++)
{
if (a[i] == t)
{
printf("数组的第%d个数就是要找的数", i +1);
getchar();
return;
}
}
printf("不存在");
getchar();
}
解决斐波那契数列
void main()
{
int a[20];
a[0] = a[1] = 1;
for (int i = 0; i < 20;i++)
{
if (i > 1)
{
a[i] =a[i - 1] + a[i - 2];
}
printf("%d\n",a[i]);
}
getchar();
}
查找最大数和最小数
void main()
{
int a[] = { 19, 3, 55, 6,8, 2, 5, 15 };
int size = sizeof(a) /sizeof(int);
int maxi = 0;
int mini = 0;
for (int i = 0; i <size; i++)
{
maxi = a[maxi]> a[i] ? maxi : i;
mini = a[mini]< a[i] ? mini : i;
}
printf("大a[%d]=%d,小a[%d]=%d", maxi,a[maxi], mini, a[mini]);
getchar();
}
选择排序
void main()
{
int a[] = { 12, 3, 53, 6,36, 7, 1, 6, 37 };
int size = sizeof(a) /sizeof(int);
for (int i = 0; i <size; i++)
{
for (int j = i +1; j < size; j++)
{
if(a[i] > a[j])
{
inttemp;
temp= a[i];
a[i]= a[j];
a[j]= temp;
}
}
}
for (int i = 0; i <size; i++)
{
printf("%d\n",a[i]);
}
getchar();
}
3. 二维数组
二维数组是是一个行列结构,二维数组中每个数据有两个坐标。
void main()
{
int num[3][4] = { 1, 2, 4,5, 6, 7, 8, 11, 12, 13, 14, 15 };
printf("%x",num);
for (int i = 0; i < 12;i++)
{
num[i / 4][i %4] = i + 1;
printf("%d\t",num[i / 4][i % 4]);
}
getchar();
}
void main12()
{
int num[3][4];
for (int i = 0; i < 3;i++)
{
for (int j = 0;j < 4; j++)
{
num[i][j]= i + j;
printf("%d\t",num[i][j]);
}
printf("\n");
}
getchar();
}
二维数组的初始化
没有赋值的根据数据类型给默认值。也可以在二维数组中使用一维数组赋值初始化。
void main()
{
int num[3][4] = { 0 };
printf("%x",&num);
int num2[3][4] = { { 1, 2,4, 5 }, { 2, 4, 5, 7 }, { 2, 3, 2, 5 } };
int num3[][4] = { { 1, 2,4, 5 } };//当初始化语句中提供全部元素的初始值时,第1维的大小可以省略
//int num3[3][] ={{1},{2},{3}};//列坐标不可以省略
printf("%d",sizeof(num3));
getchar();
}
二维数组的引用
格式,数组名[行下标表达式][列下标表达式]。比如,a[0][1]、a[2][4]。
注意,只能逐个引用各行各列的元素,不能整体引用;行下标和列下标均不做越界检查。
a[i][j]等价于a[i] + j
void main()
{
int a[3][4] = { 1, 2, 4,5, 6, 7, 12, 14, 15, 16, 12, 17 };
for (int i = 0; i < 3;i++)
{
for (int j = 0;j < 4; j++)
{
printf("%d,%d, %d, %x, %x\n", a[i][j], *(&a[i][j]), *(a[i] + j), &a[i][j],a[i] + j);
}
}
getchar();
}
求二维数组矩阵的对角线上的元素的和
void main()
{
int a[3][4] = { 1, 2, 4,5, 6, 7, 12, 14, 15, 16, 12, 17 };
int sum = 0;
for (int i = 0; i < 3;i++)
{
for (int j = 0;j < 4; j++)
{
if (i== j)
{
sum+= a[i][j];
}
//printf("%d,%d, %d, %x, %x\n", a[i][j], *(&a[i][j]), *(a[i] + j), &a[i][j],a[i] + j);
}
}
printf("%d",sum);
getchar();
}
矩阵转置
void main()
{
int a[3][4] = { 1, 2, 4,5, 6, 7, 12, 14, 15, 16, 12, 17 };
for (int i = 0; i < 3;i++)
{
for (int j = 0;j < 4; j++)
{
printf("%-5d",a[i][j]);
}
printf("\n");
}
//转置
int b[4][3];
for (int i = 0; i < 4;i++)
{
for (int j = 0;j < 3; j++)
{
b[i][j]= a[j][i];
printf("%-5d",b[i][j]);
}
printf("\n");
}
getchar();
}
打印杨辉三角形
void main()
{
int yanghui[5][5] = { 0 };
for (int i = 0; i < 5;i++)
{
for (int j = 0;j <= i; j++)
{
if (j== 0 || j == i)
{
yanghui[i][j]= 1;
}
if (i> 1 && j > 0 && j < i)
{
yanghui[i][j]= yanghui[i - 1][j - 1] + yanghui[i - 1][j];
}
if (j== 0)
{
printf("%*d",5/2*4+4/2-i*4/2, yanghui[i][j]);//阶梯顶格打印公式,行数/2*格式宽度+格式宽度/2-行数*格式宽度/2
}
else
{
printf("%4d",yanghui[i][j]);
}
}
printf("\n");
}
getchar();
}
4. 二维动态数组
两种模式,第一种整体排序是连续的,与静态二维数组一样使用;第二种整体之间是不连续的一个指针数组,每个元素都是指针,存放了另一个数组的地址。
void main()
{
int a[3][4] = { 1, 2, 4,5, 6, 7, 8, 12, 13, 15, 16, 17 };
printf("%p\n",a);
int(*p)[4] = a;//创建一个指针数组存储二维数组的首地址
for (int i = 0; i < 3;i++)
{
for (int j = 0;j < 4; j++)
{
printf("%4d",p[i][j]);
}
printf("\n");
}
getchar();
}
动态二维数组
连续内存的动态二维数组,必须事先确定二维数组的列
void main()
{
int x, y;
scanf("%d%d",&x, &y);
void *p =malloc(sizeof(int)*x*y);//分配连续的内存
//y必须是已知的常量的,才能将这片内存当作一个二维数组使用
int(*px)[9] = p;//连续内存的缺点是必须确定二维数组的列
int num = 0;
for (int i = 0; i < x;i++)
{
for (int j = 0;j < 9; j++)
{
px[i][j]= ++num;
printf("%4d",px[i][j]);
}
printf("\n");
}
system("pause");
}
非连续内存的动态二维数组
void main()
{
int x, y;
scanf("%d%d",&x, &y);
//二级指针可以存储指针数组的地址
//动态分配一片存放指针数组,每一个元素都是一个地址
//将指针数组的首地址传递给pp保存
int **pp = (int**)malloc(sizeof(int *)*x);
for (int i = 0; i <x;i++)
{
//分配内存,有多少列,每个指针都存储这样一片内存的地址
pp[i] =malloc(sizeof(int)*y);
}
int num = 0;
for (int i = 0; i < x;i++)
{
for (int j = 0;j < y; j++)
{
pp[i][j]= ++num;
printf("%4d",pp[i][j]);
}
printf("\n");
}
//释放内存
for (int i = 0; i < x;i++)
{
free(pp[i]);
}
free(pp);
system("pause");
}
5. 输入10个数,存入一个数组,并输出从小到大,从大到小。
void main()
{
int a[5];
for (int i = 0; i < 5;i++)
{
scanf("%d",&a[i]);
}
for (int i = 0; i < 5;i++)
{
printf("%d\t",a[i]);
}
for (int i = 0; i < 5;i++)
{
int maxi = i;
for (int j = i +1; j < 5; j++)
{
if(a[i] < a[j])
{
maxi= j;
}
}
if (maxi != i)
{
int t =a[i];
a[i] =a[maxi];
a[maxi]= t;
}
}
printf("\n");
//从大到小
for (int i = 0; i < 5;i++)
{
printf("%d\t",a[i]);
}
printf("\n");
//从小到大
for (int i = 4; i >= 0;i--)
{
printf("%d\t",a[i]);
}
system("pause");
}
6. 冒泡排序
void main()
{
int a[5];
for (int i = 0; i < 5;i++)
{
scanf("%d",&a[i]);
}
for (int i = 0; i < 5;i++)
{
printf("%d\t",a[i]);
}
//冒泡
for (int i = 0; i <5-1; i++)
{
for (int j = 0;j < 5 - 1 - i; j++)
{
if(a[j]>a[j + 1])
{
intt = a[j];
a[j]= a[j + 1];
a[j+ 1] = t;
}
}
}
printf("\n");
//从小到大
for (int i = 0; i < 5;i++)
{
printf("%d\t",a[i]);
}
system("pause");
}
7. 二分查找
void main()
{
time_t ts;
unsigned int num =time(&ts);
srand(num);//初始化随机种子
int a[10];
for (int i = 0; i < 10;i++)
{
a[i] = rand() %100;
printf("%d\n",a[i]);
}
//排序
//冒泡
for (int i = 0; i < 10- 1; i++)
{
for (int j = 0;j < 10 - 1 - i; j++)
{
if(a[j]>a[j + 1])
{
intt = a[j];
a[j]= a[j + 1];
a[j+ 1] = t;
}
}
}
printf("\n");
//从小到大
for (int i = 0; i < 10;i++)
{
printf("%d\t",a[i]);
}
int cha;
scanf("\n%d",&cha);
//二分查找
int shang = 0;
int xia = 9;
int zhong;
while (shang < xia)//或者for(;shang<xia;)
{
zhong = (shang +xia) / 2;
printf("记录次数%d\n",zhong);
if (a[zhong] ==cha)
{
printf("要找的下标为%d",zhong);
break;
}
else if(a[zhong] > cha)
{
xia =zhong - 1;
}
else
{
shang =zhong + 1;
}
}
system("pause");
}
8. 三维数组
三维数组的初始化。
void main()
{
int a[5][4][3];
//使用三维的方式初始化
for (int i = 0; i < 5;i++)
{
for (int j = 0;j < 4; j++)
{
for(int k = 0; k < 3; k++)
{
a[i][j][k]= i + j + k;
printf("%-5d",a[i][j][k]);
}
printf("\n");
}
printf("\n");
}
//使用一维的方式初始化
for (int i = 0; i < 3 *4 * 5; i++)
{
a[i / 4 / 3][i /4][i % 3] = i;
printf("%-5d",a[i/4/3][i/4][i%3]);
if ((i+1) % 3 ==0)
{
printf("\n");
}
if ((i + 1) % (3* 4) == 0)
{
printf("\n");
}
}
getchar();
}
十二、 指针
1. 内存
一个程序载入内存,代码、数据都有地址。外挂就是调用函数,修改数据。函数就是代码,变量是数据。内存以及cpu读写速度快,容量比较小,成本比较高。
计算机中,一切信息都是以二进制数据的形式体现的,每个内存单元的容量是1byte,即8bit。中央处理器,即cpu,进行的处理离不开内存,比如使用windows系统时,双击某个可执行程序,cpu会执行它,这实际上是复杂的内存载入过程,大致是,程序要进行的操作对应的代码被装载到代码去;全局和静态数据等装载到数据区;开辟堆栈,供临变量等使用。
内存中每个字节都有一个编号,就是内存地址。
2. 指针和指针变量
指针就是一个变量的地址;指针变量是专门存放变量地址的变量。
void main()
{
int num = 100;
int *p = #//定义指针变量,(int *)是一个指向int类型的指针变量,容纳int变量的地址,*p与int对称
printf("%d,%d",num, *p);
printf("\n%x,%x",&num, p);
*p = 10;
printf("\n%d",num);
getchar();
}
指针的类型或者数据的类型,可以用于表明截取的长度,这就明确了结束的地址位置。
void main()
{
int a = 10;
int b = 20;
int *p = &a;
*p = 2;
printf("%d", a);
p = &b;
*p = 3;
printf("\n%d",b);
getchar();
}
void main()
{
int x = 10;
int *px = &x;
change(px);
//change(&x);//或者这样
printf("%d", x);
getchar();
}
void main3()
{
int a = 10;
int b = 20;
int *p = &a;
*p = 2;
printf("%d", a);
p = &b;
*p = 3;
printf("\n%d",b);
getchar();
}
使用指针修改程序的变量
被注入的程序
void main()
{
int num = 0;
printf("%x\n",&num);
while (1)
{
Sleep(5000);
num++;
printf("%d\n",num);
}
}
修改程序的程序,由于windows下一个exe不能读取另一个exe,需要使用dll。在vs2013中设置属性,为dll,然后生成。
//一个exe不可以随意读取另外一个exe的内存
//windows内部进程之间不能直接访问
//_declspec声明为外部调用
//(dllexport) dll的导出
_declspec(dllexport) void waigua()
{
while(1)
{
if(*p < 50)
{
*p = 80;
int *p= (int *)0x003dfbf4;
}
}
}
注意,指针使用前必须初始化。
指针值得是存储的内容是地址的量,指针是一个量,对应某一块内存区域;指针存储的信息是某个内存单元的地址。
void main()
{
int num = 100;
int *p;
p = #
*p = 10;//*p表示取出p这个值表示的地址的值
printf("%x,%d",p, num);
//观察指针的地址
printf("\n%x", &p);//&p表示取出存放p这个表示地址的值所存放的地址
getchar();
}
指针存放的是内存单元地址,32位指针的大小是固定的,不论其类型,都是4个字节。
void main()
{
int *p1;
double *p2;
char *p3;
printf("%d,%d,%d",sizeof(p1), sizeof(p2), sizeof(p3));
getchar();
}
地址和指针是有区别的,地址是一个常量,如果没有特别定义为const,那么指针是一个变量,是存储地址的,是可以变的。指针是有类型的,地址没有,指针的类型表明指针的长度,得到指针结束的位置,指针中虽然存放这地址,但是如果不知道指针的类型,就不知道从这个地址开始取几个字节,那么这样的指针是没有意思的,所以,指针的类型是必要的。
void main7()
{
int num = 10;
int *p =#//&num是一个地址,是一个常量
//p是一个指针变量,可以存储一个地址
}
3. 指针变量的值
指针变量的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址长度为32位。
void main()
{
char ch = 'A';
int num = 23;
double db = 123.3;
char *p1 = &ch;
int *p2 = #
double *p3 = &db;
printf("%x,%x,%x",p1, p2, p3);
printf("\n%c,%d,%f",*p1, *p2, *p3);
getchar();
}
4. 指针的类型和指针指向的类型
不是同一类型的指针,不可以任意赋值。不同的类型,大小不一样,解析方式不一样。
double x = 10.8;8个字节
double *p = 0x00123434;4个字节
double **p = 0x00222333;4个字节
指针类型,尽量一致,如果不一致,或大或小,都会产生数据读取问题。
void main()
{
char ch = 'A';
int x = 12;
double db = 12.3;
char *p1;
int *p2 = &x;
double *p3;
p1 = p2;
p3 = p2;
printf("%c",*p1);
printf("%f",*p3);
getchar();
}
5. 指针共有
如果两个相同类型的指针,执行相等操作后,p1 = p2。这样,p1和p2指向同样的地址,两个指针的改动会相互影响对方。指针共有常用于数据通信。一个指针作为控制端,另一个作为被控制端。
void main()
{
int num = 5;
int *p1 = #
int *p2 = p1;
printf("%d,%d,%d",num, *p1, *p2);
printf("\n%x,%x,%x",&num, p1, p2);
*p2 = 8;
printf("\n%d,%d,%d",num, *p1, *p2);
printf("\n%x,%x,%x",&num, p1, p2);
*p1 = 18;
printf("\n%d,%d,%d",num, *p1, *p2);
printf("\n%x,%x,%x",&num, p1, p2);
int num2 = 3;
p2 = &num2;
printf("\n%d,%d,%d",num, *p1, *p2);
printf("\n%x,%x,%x",&num, p1, p2);
getchar();
}
6. void指针和空指针
值为NULL的指针称为空指针,这意味着,指针并指向任何地址。
void main()
{
int *p = NULL;
printf("%x", p);
if (p == NULL)
{
printf("指针为空,可以指向地址\n");
}
else
{
printf("指针不为空,已指向地址,请谨慎操作\n");
}
int num = 100;
p = #
if (p == NULL)
{
printf("指针为空,可以指向地址\n");
}
else
{
printf("指针不为空,已指向地址%x,请谨慎操作\n", p);
}
getchar();
}
void *指针是一种特殊的指针,不指向任何类型的数据,如果需要用此地址指向某类型的数据,应先对地址进行类型转换。可以在程序中进行显示的类型转换,也可以由编译系统自动进行隐式转换。
void main()
{
int num = 123;
double db = 12.3;
int *p2 = #
double *p3 = &db;
void *p1 = p2;//void类型的指针可以传递地址,不指向任何内容
//p1 = p3;
//printf("%d",*p1);
printf("%d",*((int*)p1));
//void *一般用于参数和返回值,不明确指针类型的情况下,传递地址
//如果把它用于某种类型的指针,需要强制转换
getchar();
}
7. 直接访问、间接访问
取地址符&,可以取得地址;间接运算符*,根据地址取值符,可以取得这个地址存储的值。
void main()
{
int num = 1;
printf("%d,%x\n",num, &num);//直接调用
*(&num) = 12;//*根据地址取内容
printf("%d,%x",num, &num);
int * = #
*p = 14;
int *p = #//p是指针变量
*p = 15;//*p等价于num
printf("\n%d,%d",*p, num);
getchar();
}
直接访问,按变量地址存取变量值;间接访问,通过存放变量地址的变量去访问变量。
8. scanf初始化指针
指针不可以随意指向地址,否则程序崩溃。
void main()
{
int a = 12;
int b = 15;
printf("%p,%p\n",&a, &b);
int *p = NULL;//指针初始化
scanf("%p",&p); //扫描初始化指针,根据指针地址初始化指针
printf("%p, %d",p, *p);
system("pause");
}
9. 指针将两个数从大到小输出
对于某些不方便变换变量的情况下,使用指针指向变量,然后改变指针的指向地址实现一样的效果是比较安全的方法。
void main()
{
double a, b;
scanf("%lf%lf",&a, &b);
double *pa, *pb;
pa = &a;
pb = &b;
if (a < b)
{
pa = &b;
pb = &a;
}
printf("%lf,%lf",*pa, *pb);
system("pause");
}
10. 指针与函数参数
void changep(int *p)
{
*p = 10;
}
//c语言要改变外部变量只有传地址,java、c++有引用,c语言没有引用
void main()
{
int a = 15;
printf("%d\n",a);
//changep(&a);
int *pa = &a;
changep(pa);
printf("%d", a);
getchar();
}
但是,如果参数是数组,有所不同,数组作为参数时,传递的数组首个元素的指针。
//数组当作参数的时候,传递的是指针,数组作为参数时,在函数中修改参数,改变的是原来的数组
//数组数据的拷贝非常浪费内存
//除了数组外都是副本机制,就是新建一个变量复制
void test(int a[5])
{
printf("\n%d",sizeof(a));
for (int i = 0; i < 5;i++)
{
a[i] = i + 2;
}
}
void main()
{
int a[5] = { 1, 2, 4, 5, 6};
printf("%d",sizeof(a));
test(a);
for (int i = 0; i < 5;i++)
{
printf("\n%d",a[i]);
}
getchar();
}
11. 指向指针的指针,二级指针
指针变量也是变量,占据一定的内存空间,有地址,可以使用一个指针指向它,这个称为指向指针的指针,或二级指针。
通过**声明一个二级指针。
double db = 100.2;
double db2 = 12.3;
//函数的形式参数,除了数组以外,都会新建一个变量接受传入的变量的值,这不影响原来的变量
//如果是一个数据,传递数据的地址
//如果是一个指针,传递指针的地址
void change(double *p)//使用一级指针接受指针变量,不能实现需求
{
printf("%x,%x,%x\n",p, &p, *p);
printf("%x,%x,%f\n",p, &p, *p);
}
//需要使用二级指针接受指针变量,不能直接使用指针(一级指针)接受指针变量,可能导致类型不符合
void changep(double **pp)
{
*pp = &db2;
printf("%x,%x,%x\n",*pp, pp, &pp);//pp就是这个二级指针的值,也就是传过来的指针的地址
//*pp是这个二级指针的值表示的地址存储的值,也就是传过来的指针的地址存储的值,原来为db,在函数中将它给为db2
//&pp是这个二级指针的地址。也就是二级指针的地址&pp上存放着一级指针的地址pp,一级指针地址上存放的值为*pp
//就像一级指针的地址&p上存放着一级指针p,这个p是一个地址,这个地址上存放的值*p为db
}
void main()
{
double *p = &db;
double *p1 = &p;//如果使用一级指针存放一级指针的地址
printf("%x,%x,%x\n",p1, *p1, &p1);//一级指针也可以指向一级指针
//*p1 = &db2;//但是一级指针不能修改指向的一级指针的地址上存储的值
//因为在声明这个指向一级指针的指针时,double*是一个double指针类型,但是使用*p1就是一个double类型了
//而指针地址上值一般也是一个地址,是4个字节的,实际也是double *或者int *等类型,
//因为类型不符合所以,不能修改指向的一级指针的地上上存储的值
//使用二级指针可以解决这个问题。
//因为二级指针声明时double * *pp,实际上*pp是double *类型的,所以*pp可以使用&为其赋值
//这样,*pp实际上是这个二级指针指向的一级指针的地址上的值,因为它被double *修饰,也被看成是一个指针
//所以,*pp可以使用&来赋值,也就是改变一级指针的地址上的值为其他的地址,进而改变一级指针指向的内容
//这个作用也就类似,直接使用一级指针时的p =&db2,因为p是double *类型的
double **pp = &p;//分配一块指针内存,这个内存放的一个指针的地址,pp就是这个地址
printf("%x,%x,%x,%lf\n",pp, &pp, *pp, *(*pp));
printf("db的地址%x,%d\n",&db, sizeof(p));
//p = &db2;
changep(&p);
//double **pp = &p;//二级指针用来容纳指针的地址,double*是4个字节,&地址也是四个字节
printf("%f,%x,%x",*p,&p,p);
getchar();
}
二级指针用于改变指针。可以理解为二级指针是在一级指针外套了一层指针。使用二级指针是为了改变一级指针指向的地址。一级指针本身是可以改变指向的地址的,比如double *p = &d1;通过p = &d2;但是在某些场合,我们仅能使用这个一级指针的地址,比如以指针为函数参数时,参数传递的是一个指针的地址,double *p2 = &p,比如在原函数中,*p = &d1,而在调用函数中,希望p = &d2,p2的值是&p,虽然p2指向了原函数的指针,但是改变p2 =&d2,只是改变了p2指向的内容,而非改变p指向的内容。如果使用一个一级指针接受这个地址,那么,只能是修改这个地址的值,而不能修改这个地址上存放的内容,也就是调用函数的代码中指针所指向的内容。为了能够修改这个地址上存放的内容,所以,就用两个指针来接受这个指针的地址,也就是二级指针,比如,double **pp = &p,**是一个二级的指针,pp = &p,*pp是一个double *指针,*pp就是原来的一级指针p指向的内容,也就是&pp上的内容,修改*pp的值,*pp = &d2,就是修改了&p这个地址上的内容,也就修改了原一级指针指向的内容。
void main()
{
double d = 1.2;
double d2 = 2.3;
double *p = &d;
//一级指针一开始指向的内容
printf("指针开始指向的内容%x\n",p);
//改变一级指针指向的内容,可以使用一级指针的指向直接修改
p = &d2;
printf("指针修改指向后指向的内容%x\n",p);
//复位
p = &d;
//当使用一级指针接受一级指针时
double *p2 = &p;
printf("p2的地址%x,p2的内容%x,接收的指针的地址%x,接收的指针的内容%x,接收的指针指向的内容的地址%x\n",&p2, p2, &p, p, &d);
//虽然使用一级指针可以指向另一个指针的地址
//但是从语法上无法修改这个地址所存放的地址,虽然,这个接收的指针的类型不是double时可以侥幸修改指向的地址的内容为其他地址数据
//但是,如果这个指针的类型为double时必然不能修改指向的地址的内容为其他地址数据
printf("*p2的值是指向的指针的地址上的值%x\n",*p2);
printf("也就是接收的指针所指向的内容%x\n",&d);
//如果试图通过改变*p2,以希望改变接收的指向指向的地址
//*p2 = &d2;//指针的类型大于4字节时,就报错,因为地址的长度都是4字节。
//即使使用强制类型转换,将4字节的地址存放在8字节的数字中,也不能修改指向的指针的地址上的内容
int xx = &d2;
//xx += 0.0;
*p2 = (double)xx;
printf("%x,通过强制类型转换后指针指向的地址%p\n",xx, *p2);
//复位
p = &d;
//比较,如果指针类型小于或等于4字节时,这时会导致指针指向内容的类型不是4字节地址型,而是接收这个指针的指针的类型
//char指针接收时
char *p3 = &p;
printf("p3指针的内容%x,char指针改变指向前的p指向的值%x\n", p3,p);
*p3 = &d2;//这时就不会报错,但是这样不符合语法规范
printf("p3指针的内容%x,char指针改变指向后的p指向的值%x\n", p3,p);
//复位
p = &d;
//int指针接收时
int *p4 = &p;
printf("p4指针的内容%x,int指针改变指向前的p指向的值%x\n", p4,p);
*p4 = &d2;//这时就不会报错,但是这样不符合语法规范
printf("p4指针的内容%x,int指针改变指向后的p指向的值%x\n", p4,p);
//复位
p = &d;
//使用二级指针修改指针指向的内容
double **pp = &p;
printf("二级指针指向的内容%x,指向的指针的地址%x\n",pp, &p);
printf("指向的指针所指向的内容%x,原指针指向的内容%x\n",*pp, p);
//修改*pp的值就是修改p的值,相当于p = &d2;
*pp = &d2;
printf("二级指针修改了原指针的指向后%x",p);
getchar();
}
12. 指针的运算
作为一种特殊的变量,指针可以进行一些运算,但并非所有的运算都是合法的,指针的运算主要局限在加减算术和其他一些为数不多的特殊运算。
指针的赋值运算,将变量地址赋值给指针;将数组首地址赋值给指针;将数组元素地址赋值给指针;将指针赋值给指针。
整数与指针不要直接运算。
void main()
{
int num = 10;
int *p1 = #//地址的赋值
int *p2 = p1;//指针的赋值
int a[] = { 1, 3, 4, 5, 6};
printf("%x", a);
for (int i = 0; i < 5;i++)
{
printf("\n%d,%d",a[i], *(a + i));
printf(",%x,%x",&a[i], a + i);
}
for (int *parr = a; parr< a + 5; parr++)
{
*parr += 1;
}
for (int i = 0; i < 5;i++)
{
printf("\n%d,%d",a[i], *(a + i));
printf(",%x,%x",&a[i], a + i);
}
int *parr2 = &a[2];
getchar();
}
修改指向数组的指针指向的内容
原程序
void main()
{
int a[] = { 1, 2, 3, 4, 5};
int *p = a;
printf("%x,%x",a, &p);
while (1)
{
printf("\n级别是%d", *p);
Sleep(2000);
}
getchar();
}
修改程序
_declspec(dllexport) void xiugai()
{
int **pp = (int**)0x38fa38;
*pp = (int *)(0x38fa44 +16);
}
void main()
{
int a[5];
int i = 0;
printf("%p\n",a);
for (int *p = a; p < a+ 5; p++)//指针++就是指针向前移动sizeof(指针类型)个字节
{
*p = i;//对指针指向的内容赋值
printf("%d",a[i]);
i++;
}
printf("\n");
i = 4;
for (int *p = a + 4; p>= a; p--)//指针--就是指针向后移动sizeof(指针类型)个字节
{
*p = i;
printf("%d",a[i]);
i--;
}
getchar();
}
将指针加上或减去某个整数值,就是移动这个整数值个指针类型的大小。
void main()
{
int a[5] = { 1, 2, 3, 4, 5};
int *p = &a[2];
printf("%d",a[2]);
p = p + 2;//指针加上2,在数组内部等价于向后移动两个元素的大小
printf("\n%d",*p);
p = p - 3;//指针减去3,在数组内部等价于向前移动3个元素的大小
printf("\n%d",*p);
getchar();
}
指针的加减运算只有在数组内才是有意义的
void main()
{
double num = 1.2;
double *p = #
p = p - 2;//指针的加减法在数组内部没有意义,而且容易越界报错
//1个exe不能读取另一个exe的内存
printf("%f",*p);
getchar();
}
指针相减
指针相减,得到的是两个指针的地址相隔几个元素,比如int类型的指针的差为3,则是相差3个int类型元素,也就是12个字节。指针相减多应用于同一块内存,比如数组,或者一块动态申请的内存中,如果两个指针所指向的元素没有结构上的关系,指针相减的结果是不可预测的,可能也没有什么实际意义的。
void main()
{
int a[5] = { 1, 2, 3, 4, 5};
int *p1 = &a[3];
int *p2 = &a[4];
printf("%d", (p1- p2));//数组内部之间可以判断两个指针的前后以及指针相隔的元素
int b[5] = { 1, 2, 4, 5, 6};
int *p3 = &b[2];
printf("%d", (p1- p3));//两个没有结构关系的指针相减,没有很大的意义
getchar();
}
13. 指针的比较
指针指向的地址进行比较,相同则是指向相同的地址。
void main()
{
int num = 10;
int *p = #
int *p2 = #
if (p == p2)
{
printf("指针相同,指向同一个地址");
}
else
{
printf("指向不同的地址");
}
getchar();
}
14. 指针与数组
void main()
{
int n[5] = { 1, 3, 4, 6, 7};
printf("%d", n);
printf("\n%d",*n);
printf("\n%d",*(n + 1));
for (int i = 0; i < 5;i++)
{
printf("\n%d,%x",n[i], &n[i]);
printf(",%d,%x",*(n + i), n + i);
}
int *p = n;
for (int i = 0; i < 5;i++)
{
printf("\n%d,%x",n[i], &n[i]);
printf(",%d,%x",*(n + i), n + i);
printf(",%d,%x",*(p + i), p + i);
printf(",%d,%x",p[i], &p[i]);
}
//p和n的区别是,n是常量,p是常量
int m[5] = { 3, 5, 7, 8, 9};
p = m;
for (int i = 0; i < 5;i++)
{
printf("\n%d,%x",p[i], &p[i]);
}
getchar();
}
取数组最大值
void main()
{
int n[5] = { 1, 3, 4, 6, 7};
int *p = n;
int max = 0;
int maxi = 0;
//轮询取出最大数和最大数的下标
for (int i = 1; i < 5;i++)
{
if (max <*(p+i))
{
max =*(p+i);
maxi =i;
}
}
printf("\n%d,%d",max, maxi);
getchar();
}
使用指针循环数组和求数组的最大值及其坐标
void main()
{
int n[5] = { 1, 3, 4, 6, 7};
//使用指针循环数组
int max = n[0];
int *maxi = n;
printf("%x,%x\n",maxi, n);
for (int *p = n + 1; p< n + 5; p++)
{
printf("\n%d,%x",*p, p);
if (max < *p)
{
max =*p;
maxi =p;
}
}
int maxii = maxi - n;
printf("%d,%d",max, maxii);
getchar();
}
指向数组的指针
void main()
{
int a[5] = { 1, 2, 3, 4, 5};
printf("%d,%d\n",a, &a);
printf("%d,%d",sizeof(*a), sizeof(*(&a)));//*(&a)占20个字节,*a占4个字节,虽然地址一样,但是长度不一样
//a相当于是一个int类型的指针,&a相当于是一个数组类型的指针
int *p = a;
int(*pa)[5] = &a;//指向数组的指针,有点类似二级指针的感觉
printf("\n%d,%d",sizeof(*p), sizeof(*pa));
printf("\n%d,%d",*(*pa+1),*a);
getchar();
}
15. 指针引用多维数组
void main()
{
int a[3][4] = { 1, 3, 4,5, 6, 7, 8, 9, 11, 12, 13, 15 };
printf("%p,%p,%p",a, &a, *a);
//a是行指针,也就是一行的大小,一行有4个int,就是16个字节
//*&a是一个数组指针,也就是数组的大小,12个int就是48个字节
//**a是指向int类型数据的指针
printf("\n%d,%d,%d",sizeof(*a), sizeof(*&a), sizeof(**a));
getchar();
}
void main()
{
int a[3][4] = { 1, 3, 4,5, 6, 7, 8, 9, 11, 12, 13, 15 };
for (int i = 0; i < 3;i++)
{
for (int j = 0;j < 4; j++)
{
printf("%-3d,%p", a[i][j], &a[i][j]);
}
printf("\n");
}
printf("\n%p,%p,%p",a, a + 1, a +2);//a是一个行指针,指向的数据是一行有四个int类型的数据
printf("\n%p,%p,%p",*a, *a + 1, *a + 2);//*as是一个int类型的指针,指向数据第一行的第一个int元素
printf("\n%p,%p,%p",*(a + 1), *(a + 1) + 1, *(a + 1) + 2);//*(a+1)指向第2行第一个int元素
printf("\n%d,%p",*(*(a + 1) + 2), *(a + 1) + 2);
getchar();
}
void main()
{
int a[3][4] = { 1, 3, 4,5, 6, 7, 8, 9, 11, 12, 13, 15 };
for (int i = 0; i < 3;i++)
{
for (int j = 0;j < 4; j++)
{
printf("%-3d,%p", a[i][j], &a[i][j]);
}
printf("\n");
}
printf("%d,%d\n",sizeof(*a), sizeof(**a));//*a等价于*(a+0),也等价于a[0]
printf("%p,%p,%p\n",a, a + 1, a + 2);//代表某一行的首地址
printf("%p,%p,%p\n",*a, *(a + 1), *(a + 2));//代表某一行第一列的首地址
printf("%p,%p,%p\n",a[0], a[1], a[2]);//代表某一行第一列的首地址
//以下三种方式等价,a[i][j] =*(*(a+i)+j) = *(a[i] + j)
printf("%d,%p\n",*(*(a + 1) + 2), *(a + 1) + 2);
printf("%d,%p\n",*(a[1] + 2), a[1] + 2);
printf("%d,%p\n",a[1][2], &a[1][2]);
getchar();
}
void main()
{
int a[3][4] = { 1, 3, 4,5, 6, 7, 8, 9, 11, 12, 13, 15 };
printf("%p,%p\n",a, *a);
printf("%d,%d\n",sizeof(*a), sizeof(**a));
printf("%p,%p\n",a[0], *(a + 0));//是一个列指针,代表第一列第一个元素的地址
printf("%d,%d\n",sizeof(*a[0]), sizeof(**(a+0)));
printf("%p,%p\n",&a[0], &a[0][0]);
printf("%d,%d\n",sizeof(*&a[0]), sizeof(*&a[0][0]));
printf("%p,%p\n",a + 1, a[1]);//a+1是一个行指针,指向第二行;a[1]等价于*(a+1),代表第二行第一个元素的首地址
printf("%d,%d\n",sizeof(*(a + 1)), sizeof(*a[1]));
getchar();
}
void main()
{
int a[3][4] = { 1, 3, 4,5, 6, 7, 8, 9, 11, 12, 13, 15 };
for (int *p =&a[0][0]; p < &a[0][0] + 12; p++)
{
printf("%d,%p\n",*p, p);
}
for (int i = 0; i < 3;i++)
{
for (int j = 0;j < 4; j++)
{
//printf("%-2d,%p", *(*(a + i) + j), *(a + i) + j);
printf("%-2d,%p", *(a[i] + j), a[i] + j);
}
printf("\n");
}
getchar();
}
取出二维数组中的任意元素
void main()
{
int a[3][4] = { 1, 3, 4,5, 6, 7, 8, 9, 11, 12, 13, 15 };
int (*p)[4] = a;//a是一个行指针
int i = 0;
int j = 0;
scanf("%d%d",&i, &j);
printf("%d,%d,%d,%d",i, j, p[i][j], *(*(p + i) + j));
getchar();
getchar();
}
16. 数组作为函数参数
用指向数组的指针作为函数参数。一维数组可以作为函数参数,多维数组也可以作为函数参数。用指针变量作为形参,以接受实参数组名传递来的地址。
可以有两种方法,一维数组用变量的指针变量;二维数组用指向一维数组的指针变量。
数组作为函数参数,传递的是地址,地址就是指针占4个字节。函数的参数对于数组没有副本机制,为了节约内存,拷贝数组浪费空间与cpu。
void showarrp(int *p)
{
for (int i = 0; i < 5;i++)
{
printf("%d\n",p[i]);
}
}
void main()
{
int a[5] = { 2, 3, 4, 5, 6};
printf("%d\n",sizeof(a));
showarrp(a);
getchar();
}
void showarr2p(int(*p)[3])
{
//b = b + 1;//形参可以被赋值改变,实际上是一个指针
for (int i = 0; i < 3;i++)
{
for (int j = 0;j < 3; j++)
{
printf("%d", p[i][j]);
}
printf("\n");
}
}
void main()
{
int b[3][3] = { { 1, 2, 3}, { 4, 5, 6 }, { 7, 8, 9 } };
showarr2p(b);
getchar();
}
逆序存放数组
void nixu(int *p,int n)
{
for (int i = 0; i < n /2; i++)
{
int temp;
temp = *(p+i);
printf("%d,%d\n",*(p+i), *(p + n - 1 - i));
*(p+i) = *(p + n- 1 - i);
*(p + n - 1 - i)= temp;
}
}
void main()
{
int a[9] = { 1, 3, 6, 2,33, 21, 5, 67, 8};
nixu(a,9);
for (int *p = a; p < a+ 9; p++)
{
printf("%d\n",*p);
}
getchar();
}
使用指针对10个整数按由大到小的顺序排序,冒泡
void sort(int *p,int n)
{
for (int i = 0; i < n -1; i++)
{
for (int j = 0;j < n - 1 - i; j++)
{
if (*(p+ j) > *(p + j + 1))
{
inttmp = *(p + j);
*(p+ j) = *(p + j + 1);
*(p+ j + 1) = tmp;
}
}
}
}
void main()
{
int a[10] = { 1, 32, 3, 6,2, 33, 21, 5, 67, 8 };
sort(a, 10);
for (int *p = a; p < a+ 10; p++)
{
printf("%d\n",*p);
}
getchar();
}
计算二维数组平均值以及第n个的值
void showbujige(int(*p)[4], int n)
{
printf("\n不及格的学生有:");
for (int i = 0; i < n;i++)
{
for (int j = 0;j < 4; j++)
{
if(*(*(p + i) + j) < 5)
{
printf("\n第%d个学生", i + 1);
for(int j = 0; j < 4; j++)
{
printf("%d", *(*(p + i) + j));
}
break;
}
}
}
}
double rev(int (*p)[4], int n)
{
double sum = 0.0;
for (int i = 0; i < n;i++)
{
for (int j = 0;j < 4; j++)
{
sum +=*(*(p + i) + j);
}
}
return sum / 4 / n;
}
void shown(int(*p)[4], int n)
{
printf("第%d个学生的成绩", n);
for (int i = 0; i < 4;i++)
{
printf("%d", *(*(p + n) + i));
}
}
void main()
{
int b[3][4] = { { 1, 2, 3,55 }, { 4, 5, 6, 23 }, { 7, 8, 9, 34 } };
double r = rev(b, 3);
printf("总平均分%f\n", r);
shown(b, 2);
showbujige(b, 3);
getchar();
}
17. 函数指针
如果在程序中定义了一个函数,在编译时,编译系统为函数代码分配一段存储空间,这段存储空间的起始地址,称为这个函数的指针。
可以定义一个指向函数的指针变量,用来存放某一函数的起始地址,这就意味着此指针变量指向该函数。比如,int(*p)(int,int),定义时参数名可以省略。定义p是指向函数的指针变量,它可以指向类型为整型且有两个整型参数的函数。
void msg()
{
MessageBoxA(0, "哈哈", "好", 0);
}
int add(int a, int b)
{
return a + b;
}
void main()
{
//msg();
void(*p)() = msg;
p();
int(*px)(int, int) = add;//参数名可以省略
printf("%d",px(2, 3));
printf("\n%x,%x",add, msg);
getchar();
}
int mind(int a, int b)
{
return a < b ? a : b;
}
int maxd(int a, int b)
{
return a > b ? a : b;
}
int addd(int a, int b)
{
return a + b;
}
int run(int a, int b, int(*p(int, int)))
{
return p(a, b);
}
void main()
{
int num;
scanf("%d",&num);
int a, b;
scanf("%d%d",&a, &b);
switch (num)
{
case 1:
printf("%d",run(a, b, mind));
break;
case 2:
printf("%d",run(a, b, maxd));
break;
case 3:
printf("%d",run(a, b, addd));
break;
default:;
}
system("pause");
}
指向函数的指针变量的一个重要用途是把函数的地址当作参数传递到其他函数。指向函数的指针可作为函数参数,把函数的入口地址传递给形参,这样就能够在被调用的函数中使用实参函数。
函数钩子
void Amsg(char * str)
{
MessageBoxA(0,"kkk", str, 0);
}
void Bmsg(char * str)
{
printf("%s\n",str);
}
void main()
{
printf("%x,%x\n",Amsg, Bmsg);
void(*p)(char * str);
p = Amsg;
p = Bmsg;
printf("%x,%x\n",&p, p);
while (1)
{
p("ggo");
Sleep(10000);
}
}
_declspec(dllexport) void hanshugouzi()
{
//int *p= (int*)0x0044fbbc;//可以用,不规范的写法
//*p = 0x011a11c7;
void(**p)(char *str) =(void(**)(char *str))0x0044fbbc;
*p = (void(*)(char*str))0x011a11c7;
}
18. 函数返回值是指针
一个函数可以返回一个整型值、字符值、实型值等,也可以返回指针型的数据,即地址。定义返回指针值的函数的一般形式为:类型名 *函数名(参数列表)。
int a = 10;
int b = 12;
int * fanhui()
{
int c = 2;
return &a;
//return &c;
}
void main()
{
printf("%d",*fanhui());
getchar();
}
int * minp(int *p, int n)
{
int min = *p;
int *minx = p;
for (int i = 1; i < n;i++)
{
if (min > *(p+ i))
{
min =*(p + i);
minx =p + i;
}
}
//修改最小数
*minx = 1000;
return minx;
}
void main()
{
int a[10];
time_t ts;
srand((unsignedint)time(&ts));
for (int i = 0; i < 10;i++)
{
a[i] = rand() %100;
printf("%d\n",a[i]);
}
printf("%x\n",minp(a, 10));
system("pause");
}
利用函数返回值拷贝字符串
//自定义拷贝字符串
char * mystrcpy(char *dest, char *source)
{
char *last = NULL;//最后结果
if (dest == NULL || source== NULL)
{
return last;//直接返回空指针,没有执行任何操作
}
last = dest;//存入dest首地址
//没有遇到字符'\0'就一致向前靠别
//while ((*dest++ =*source++) != '\0');
//作用等同于如下代码
while (*source != '0')
{
*dest = *source;
dest++;
source++;
//可以简写为:
//*dest++ =*source++;
}
return last;
}
void main()
{
char str[40] = { 0 };
//返回拷贝好的字符串地址,进行字符串拷贝
//printf("%s",strcpy(str, "hhahahahahsdf"));
printf("%s",mystrcpy(str, "hhahahahahsdf"));
getchar();
}
19. 指针表达式与左值
左值,可放在赋值号左边的。指针变量一级指针变量的间接引用都可作为左值。如,int num = 1,num2 = 2;int *p = # p =&num2;*p = 3;
指针变量可以作为左值,并不是因为它们是指针,而是因为它们是变量。
void main()
{
int a = 5;
int *p = &a;//p是左值,因为是变量
int *const px;//px是一个指针常量,不可被赋值
//px = &a;
*p = 3;
getchar();
}
20. 指针与整型
无论指针指向什么样类型的量,在32位系统来说,都占据4个内存字节,指针的值是某个内存的地址,这应当是个整数。
如果实在有必要对某个内存地址进行访问,可以通过强制类型转化来完成,如
int *pnum = (int *)0x0034fe23;
void main()
{
int a = 12;
int b = 13;
printf("%p,%p\n",&a, &b);
int *p;
scanf("%p",&p);
*p = 4;
printf("\n%d,%d",a, b);
int x;
scanf("%p",&x);
p = (int *)x;//
*p = 5;
printf("\n%d,%d",a, b);
system("pause");
}
21. 迷途指针
就是指针被回收后,继续访问这个指针,就形成了访问迷途指针。
void main()
{
int *p = (int*)malloc(sizeof(int)* 10);
printf("%x", p);
for (int i = 0; i < 10;i++)
{
p[i] = i;
printf("\n%d",p[i]);
}
free(p);
//内存被回收后,继续访问这个指针
for (int i = 0; i < 10;i++)
{
printf("\n%d",p[i]);
}
getchar();
}
22. 32位与64位的差别
32位就是内存地址的寻址单位为32位,也就是4个字节;64位就是内存地址的寻址单位为64位,也就是8个字节,64位兼容32位的。
在vs2013的配置管理器中可以设置,平台新建x64的,就是64位的。
void main()
{
int num = 100;
int *p = #
printf("%x", p);
printf("\n%d",sizeof(p));
system("pause");
}
十三、 内存管理
1. 动态内存分配
指针初始化为变量的地址,或用变量的地址为指针变量赋值,是一种给指针分配内存的方式。c语言允许用户调用以动态申请需要的内存,给程序使用。
类似于数组内存这种分配机制就称为静态分配,静态分配是有编译器完成的,在程序执行前便已指定。
虽然静态分配直观,易理解,但是有明显的缺陷,不是容易浪费内存就是内存不够用。为了解决这个问题,引入动态分配机制。
动态分配是指用户可以在程序运行期间根据需要申请或释放内存,大小也完全可控。动态分配不像数组内存那样需要预先分配空间,而是由系统根据程序需要动态分配,大小完全按照用户的要求来,当使用完毕后,用户还可以释放所申请的动态内存,由系统回收,以备他用。
void main()
{
int num;
scanf("%d",&num);
int *p =malloc(sizeof(int)* num);
for (int i = 0; i <num; i++)
{
p[i] = i;
printf("%d,%x",*(p+i), p + i);
printf("%d,%x\n", p[i], &p[i]);
}
free(p);
system("pause");
}
2. malloc与free
malloc和free是c标准库中提供的两个函数,用以动态申请和释放内存,malloc()函数的基本调用格式为:void*malloc(unsigned int size);
参数size是个无符号整型数,用户以此控制申请内存的大小,执行成功时,系统会为程序开辟一块大小为size个内存字节的区域,并将该区域的首地址返回,用户可以利用该地址管理并使用该块内存,如果申请失败,返回空指针NULL。
malloc()函数返回类型是void *,用其返回值对其他类型指针赋值时,必须进行显示转换。size仅仅是申请字节的大小,并不管申请的内存块中存储的数据类型,因此,申请内存的长度须由程序员通过长度*sizeof(类型)的方式给出,比如,int *p = (int *)malloc(5*sizeof(int));
free是释放内存,比如,free(p);
void main()
{
while (1)
{
void *p = malloc(100*1024* 1024);
Sleep(1000);
free(p);
}
}
void main()
{
void *p =malloc(0xffffffff);
if (p == NULL)
{
printf("内存不够,没有分配");
}
getchar();
}
void main()
{
int *p = (int*)malloc(sizeof(int));
*p = 5;
printf("%d",*p);
free(p);
getchar();
}
void main()
{
unsigned int *p =(unsigned int *)malloc(sizeof(int));
//*p = -5;
*p = 5;
printf("%u,%x",*p,p);
free(p);
//free(p);//内存不可以反复释放
unsigned int *p1 = p;
printf("\n%x,%d",p1,*p1);
free(p1);//内存根据地址值释放,已经释放过的的地址也不能反复释放,除非空指针可以反复释放
getchar();
}
动态数组
void main()
{
int num;
scanf("%d",&num);
int *p =malloc(sizeof(int)* num);
for (int i = 0; i <num; i++)
{
p[i] = i;
printf("%d,%x",*(p+i), p + i);
printf("%d,%x\n", p[i], &p[i]);
}
free(p);
system("pause");
}
3. realloc calloc和free
与malloc函数以字节为单位申请内存不同,calloc函数是以目标对象为单位分配的,目标对象可以是数组,也可以是结构体。calloc会自动将内存初始化为0,malloc不会。
calloc函数的原型为:void*calloc(size_t num, size_t size);
malloc()函数返回类型也是void*,需要强制转换才能为其他类型的指针赋值。calloc参数需要两个参数以指定申请内存块的大小,一是对象占据的内存字节数size,二是对象的个数num。
size_t类型是无符号类型,在windows以lcc编译环境下,其定义为:
typeof unsigned int size_t;
为已经分配的内存重新分配空间并复制内容
realloc()函数有两个参数,已分配的内存地址;重新分配的字节数。
realloc有人使用,就重新分配,并且先拷贝原来内存的内容,然后回收原来的内存。
void *realloc(void*ptr, size_t size);
free释放内存,内存释放后,指针应该赋值为空,就可以规避再次引用,以及反复释放的问题。
void main()
{
int num;//数组的大小
scanf("%d",&num);
//double *p = (double*)malloc(sizeof(double)*num);
double *p = (double*)calloc(num, sizeof(double));
int i = 0;
for (double i = 0.0; i< num;i++)
{
p[(int)i] = i;
printf("%f,%x\n",p[(int)i], &p[(int)i]);
}
free(p);
system("pause");
}
动态增长的数组
void main()
{
int num;//数组大小
scanf("%d",&num);
int *p = (int*)malloc(sizeof(int)*num);
printf("%x\n",p);
for (int i = 0; i <num; i++)
{
p[i] = i;
printf("%d,%x\n",p[i], &p[i]);
}
int newnum;//数组的大小
scanf("%d",&newnum);
int *newp = (int*)realloc((void *)p, sizeof(int)*(newnum+num));
printf("%x",newp);
for (int i = num; i <num + newnum; i++)
{
newp[i] = i;
printf("\n%d,%x",newp[i], &newp[i]);
}
free(newp);
system("pause");
}
void bujige(int *p, int n)
{
for (int i = 0; i < n;i++)
{
if (p[i] <50)
{
printf("不及格:%d\n", p[i]);
}
}
}
void main()
{
int *p = (int*)malloc(sizeof(int)* 5);
for (int i = 0; i < 5;i++)
{
scanf("%d",&p[i]);
}
bujige(p, 5);
free(p);
system("pause");
}
4. 内存泄漏
如果没有释放内存,但记录该块内存的指针消亡了或者是指针的值反生了改变,这块内存将永远得不到回收,造成了内存泄漏,如果程序长时间运行的话,不断的泄漏可能使得系统内存耗尽而崩溃。
void main()
{
while (1)
{
void *p =malloc(10 * 1024 * 1024);//内存泄漏
p = NULL;//指针消亡或指针的内容发生变化,对应的内存不会被释放,就会引起内存泄漏
Sleep(2000);
free(p);
Sleep(2000);
}
//程序消亡,也会使得内存被释放,但是某些服务器的程序是不间断运行的,所以也会容易产生内存泄漏
}
十四、 字符串
在编程语言中,字符串十分重要。c语言没有提供字符串这个类型,而是以特殊字符数组的形式来存储和处理字符串。这种字符数组必须以空字符‘\0’结尾。
1. 定义和初始化
定义形式为,char 数组名[常量表达式];
void main()
{
char str[30] ="calc";
printf("%s\n",str);
char str2[4] = { 'c', 'a','l', 'c' };//不能打印出正确的字符串
printf("%s\n",str2);
char str3[5] = { 'c', 'a','l', 'c', '\0' };
printf("%s\n",str3);
char str4[5] = { 'c', 'a','l', 'c' };//如果数组中的元素在定义没有初始化,将默认为0,0转化为字符也就是\0。所以,这样也正确
printf("%s\n",str4);
system("pause");
}
字符数组的长度是字符数组的长度,字符串的长度是字符串的长度,不一定相等
void main()
{
char str[40] ="tasklist";
printf("str=%d,tasklist=%d\n",sizeof(str), sizeof("tasklist"));
system(str);
getchar();
}
查看字符串中的字符
void main()
{
char str[40] ="tasklist";
for (int i = 0; i < 40;i++)
{
printf("\nstr[%d]=%c",i, str[i]);
}
int i = 0;
while (str[i] != '\0')
{
printf("\nstr[%d]=%c",i, str[i]);
i++;
}
getchar();
}
使用指针定义和初始化字符串
指针定义的字符串是一个常量,常量不可以写。
void main()
{
char *str = "titlehaha 喀喀喀";
printf("%d,%d\n",sizeof(str), sizeof("title haha 喀喀喀"));
printf("%x\n",str);
system(str);
*str = 'a';//不可以对常量赋值,错误
system("pause");
}
如果一定要修改使用数组定义和初始化的字符串,使用指针指向这个数组,然后,通过指针修改这个字符数组;如果是使用指针定义的字符串,可以使用二级指针指向这个指针,通过二级指针修改这个指针。
void main()
{
char str[10] ="write";
system(str);
char *p = str;
printf("%x,%x",p, str);
char str2[10] ="notepad";
int i = 0;
while ((*p++ = str2[i++])!= '\0');
system(str);
system("pause");
}
遍历指针字符串
void main()
{
char *str = "titlehaha 两类";
char *p = str;
putchar(*p);
while (*p++)
{
putchar(*p);
}
system(str);
getchar();
}
统计指针字符串长度
void main()
{
char *str = "titlehaha 两类";
char *p = str;
int i = 1;
putchar(*p);
while (*p++)
{
putchar(*p);
i++;
}
printf("\n%d,%d",i, sizeof("title haha 两类"));
system(str);
getchar();
}
二维数组定义和初始化字符串
void main()
{
char str[100] = {"color 4f" };//一维数组初始化字符串大括号可以省略
char str2[200] ="color 4f";
//system(str);
time_t ts;
srand((unsignedint)time(&ts));
int num = rand() % 10;
/*char str3[10][10] = {
{"notepad" },
{"write" },
{"tasklist" },
{"mspaint" },
{"calc" },
{"msconfig" },
{ "color4f" },
{"c:\\" },
{"mstsc" },
{"ipconfig" }
};*///二维数组的大括号也可以省略
char str3[10][20] = {
"notepad",
"write",
"tasklist",
"mspaint",
"calc",
"msconfig",
"color4f",
"explorerc:\\",
"mstsc",
"ipconfig"
};
for (int i = 0; i < 10;i++)
{
printf("%s\n",str3[i]);
}
system(str3[num]);
system("pause");
}
字符数组和字符指针变量的区别:
字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址,不是将字符串放到字符指针变量中;
赋值方式,可以对字符指针变量赋值,不能对数组名赋值;
初始化的含义,char *p = “haha”;与char *p;p=”haha”;等价;但是char str[4] = “haha”;与char str[4];str[] = “haha”;不等价;
存储单元的内容,编译时为字符数组分配若干存储单元,以存放个元素的值;而对字符指针变量,只分配一个存储单元存储地址;
指针变量的值可以改变,而数组名代表一个固定的值,不能改变;
字符数组中各元素的值是可以改变的,但字符指针变量指向的字符串常量中的内容是不可以被取代的;
引用数组元素,对字符数组可以用下标法和地址法引用数组元素,如a[5],*(a+5);如果指针年变量p=a,则也可以用指针变量带下标的形式和地址引用;
用指针变量指向一个格式字符串,可以用它代替printf函数中的格式字符串,char *p = “%s”;char *a = “a”;printf(p,a);;
2. 字符数组的输入输出
相关的输入输入函数处理字符串
scanf,printf,gets,puts。
scanf会将空格,回车,换行,换页符,制表符当作终止符终止数据输入。
gets接收输入时保留空格,也会保留其他制表符,不能保留回车。
void main()
{
char str[30];
gets(str);
printf("%s",str);
system(str);
system("pause");
}
void main42()
{
char str[40];
scanf("%s",str);
printf("%s",str);
char str2[40];
scanf("%s",str2);
printf("\n%s",str2);
system("pause");
}
void main()
{
char str[100];
gets(str);
puts(str);
system("pause");
}
void main()
{
char str[100] = { 0 };
scanf("%s",str);
printf("%s",str);
system(str);
system("pause");
}
使用指针字符串做输入时要初始化为NULL
void main()
{
//char *p = NULL;//用指针初始化,必须让指针存储一片可以读写的内存地址
//char *p ="aaaaaa";//存储了一片常量地址,常量不可以写
char str[100] = { 0 };
char *p = str;//指针存储了一片内存的首地址,内存可以读写
scanf("%s", p);
printf("%s", p);
system(p);
system("pause");
}
指针存储字符串和数组存储字符串的差别
void main()
{
char str[100] ="tasklist";
//str[100] ="tasklist";//str[100]代表第101个元素,越界
//str ="tasklist";//str是数组名,是常量不可以修改
char *p;
p ="tasklist";//p是变量,可以改变存储的地址,可以直接赋值
system(p);
getchar();
}
3. 字符串的比较
void main()
{
char c1[5] = { 'a', 'b','c', 'd', 'e' };
char c2[] ="abcde";
if (sizeof(c1) >sizeof(c2))
{
printf("c1长");
}
else
{
printf("c2长");
}
getchar();
}
4. sprintf
void main()
{
char str[100] = { 0 };
int d = 0;
printf("请输入个数");
scanf("%d",&d);
char cont[100] = { 0 };
printf("请输入内容");
scanf("%s",cont);
sprintf(str, "for /l%%i in (1,1,%d) do start %s", d, cont);
system(str);
system("pause");
}
void main()
{
char str[100] = { 0 };
while (1)
{
for (int i =0x0; i < 0x10; i++)
{
sprintf(str,"color %x%x", i, 0x10 -i);
printf("%s\n","木马大爆发,系统崩溃中!!!");
system(str);
}
}
system("pause");
}
5. sscanf
用于提取字符串中的数据
void main()
{
char str[100] = "哈哈100哈哈算术";
int num;
sscanf(str, "哈哈%d哈哈算术", &num);
printf("%d",num);
system("pause");
}
6. 字符串查找
地址strstr(str1,str2)查找str1中是否包含str2,如果找到返回在str1中的首地址,否则返回NULL。
void main()
{
char str1[100] ="haha看看哈哈";
char str2[100] = "看看";
char *p = strstr(str1,str2);
if (p == NULL)
{
printf("没有找到");
}
else
{
printf("%x,%s",p,"找到了");
}
getchar();
}
7. strset
void *mystrset(void *source, char ch)
{
char *sourcebak = source;
if (sourcebak == NULL)
{
returnsourcebak;
}
while (*sourcebak != '\0')
{
*sourcebak = ch;
sourcebak++;
}
return source;
}
void main()
{
char str[20] ="hellowwwlleed";
printf("%s\n",str);
mystrset(str, 'c');
printf("%s",str);
getchar();
}
8. 对比字符串
strcmp(str1,str2),对比字符串,相等返回0,第一个字符串ascii值大返回-1,第二个字符串ascii值大返回1。一般用于验证密码,验证码,查找文件夹,文件等。
void main()
{
char str1[60] ="hahaLll哈哈";
char str2[60] ="hahaLll2哈哈";
int num;
num = strcmp(str1, str2);
if (num == 0)
{
printf("%d,%s",num, "相等");
}
else
{
printf("%d,%s",num, "不等");
}
getchar();
}
void main()
{
char str1[60] ="hahaasssaLll哈哈";
char str2[60] ="hahaLll2哈哈";
int num;
num = strcmp(str1, str2);
if (num == 0)
{
printf("%d,%s",num, "相等");
}
else if (num == 1)
{
printf("%d,%s",num, "第二个大");
}
else
{
printf("%d,%s",num, "第一个大");
}
system("pause");
}
strcmp区分大小写,如果不区分大小写,可以使用strupr(str)小写转大写。
自定义的strcmp
int mystrcmp(char *p1, char *p2)
{
int i = 0;
while (p1[i] == p2[i]&& p1[i] != '\0')
{
i++;
}
if (p1[i] == '\0'&& p2[i] == '\0')
{
return 0;
}
//比较ascii码值,大的就小,小的就大
else if (p1[i] > p2[i])
{
return 1;
}
else
{
return -1;
}
}
void main()
{
char str1[60] ="haha";
char str2[60] ="hah";
int num;
num = mystrcmp(str1,str2);
if (num == 0)
{
printf("%d,%s",num, "相等");
}
else if (num == 1)
{
printf("%d,%s",num, "第二个大");
}
else
{
printf("%d,%s",num, "第一个大");
}
system("pause");
}
9. strncmp
比较字符串前n个字符。
strncmp(str1,str2,n)
如果相等返回0,如果str1大返回-1,如果str2大返回1。
void main()
{
char str1[60] ="haha";
char str2[60] ="hassh";
int num;
num = strncmp(str1, str2,2);
if (num == 0)
{
printf("%d,%s",num, "相等");
}
else if (num == 1)
{
printf("%d,%s",num, "第二个大");
}
else
{
printf("%d,%s",num, "第一个大");
}
system("pause");
}
10. strchr
查找字符串中首次出现字符c的位置,如果首次出现返回c的位置的指针,否则返回NULL。
void main()
{
char str[100] ="hahah两类";
int *p = strchr(str+3,'h'); //第一个参数不一定是字符串的首地址,可以从任意位置检索一个字符
if (*p == NULL)
{
printf("没有");
}
else
{
printf("%c位置为%x", *p, p);
}
getchar();
}
11. 二级指针字符串
使用二级指针指向字符串指针可以改变字符串指针指向的字符串数组的地址。
在函数里面改变一个外部变量,需要变量的地址。如果是数据,需要指向数据的指针存储数据的地址。如果是指针,就需要指向指针的指针存储指针的地址。
char str1[20] = "notepad";
char str2[20] = "tasklist";
void change(char **str)
{
*str = str2;
printf("%x\n",str);
str = str2;
}
void main()
{
char *p = str1;
printf("%x\n",p);
change(&p);
system(p);
getchar();
}
12. 字符串封装
创建字符串.h作为头文件
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
struct CString
{
char *p;//保存字符串首地址
int reallength;//实际长度
};
typedef struct CString mystring;
//字符串,初始化,打印,增加(字符,字符串),删除(字符,字符串)
//查找,查找字符,查找字符串
//修改字符串,(字符替换,字符串替换)
//删除,字符,字符串
//任意位置增加
void init(mystring *string);//初始化
void initwithlength(mystring *string, int length);//开辟长度,内存清零
void initwithstring(mystring *string, char *copystring);//初始化并拷贝字符串
void printfstring(mystring *string);//打印
void backaddchar(mystring *string, char ch);//增加字符
void backaddstring(mystring *string, char *str);//增加字符串
void run(mystring *string);//执行指令
char *findfirstchar(mystring *string, char ch);//返回第一个找到的字符的地址
char *findfirststring(mystring *string, char *str);//返回第一个找到的字符串的首地址
int deletefirstchar(mystring *string, char ch);//返回0失败,1成功
int deletefirststring(mystring *string, const char * const str);//返回0失败,1成功
void addchar(mystring *string, char ch, char *pos);//增加字符
void addstring(mystring *string, char *str, char *pos);//增加字符串
void changefirstchar(mystring *string, const char oldch, const charnewch);//替换第一个找到的字符
void changefirststring(mystring *string, const char * const oldstr,const char * const newstr);//替换第一个字符串
创建字符串.c作为实体
#include "字符串.h"
//字符串,初始化,打印,增加(字符,字符串),删除(字符,字符串)
//查找,查找字符,查找字符串
//修改字符串,(字符替换,字符串替换)
//增加,尾部增减,任意位置增加
int mystrlen(char *p)
{
if (p == NULL)
{
return -1;
}
int i = 0;
while (*p != '\0')
{
p++;//指针前进
i++;//长度自增
}
return i;
}
char *mystrcpy(char *dest, const char *source)//const限定不被以外修改
{
if (source == NULL ||dest== NULL)
{
return NULL;//为空没有必要复制
}
char *destbak = dest;
while ((*dest++ =*source++) != '\0');
return destbak;//返回目标的首地址
}
//连接字符串
char *mystrcat(char *dest, const char *source)
{
if (dest == NULL || source== NULL)
{
return NULL;
}
char *destbak = dest;
while (*dest != '\0')
{
dest++;
}
while (*source != '\0')
{
*dest = *source;
dest++;
source++;
}
*dest = '\0';
return destbak;
}
//查找字符
char *mystrchr(const char *dest, char ch)
{
if (dest == NULL)
{
return NULL;
}
if (dest == NULL)
{
return NULL;
}
while (*dest != '\0')
{
if (*dest == ch)
{
returndest;
}
dest++;
}
return NULL;
}
//查找字符串
char *mystrstr(const char * const dest, const char * const findstr)
{
if (dest == NULL ||findstr == NULL)
{
return NULL;
}
//备份参数
char *destbak = dest;
char *p = NULL;//保存找到的地址
while (*destbak != '\0')
{
int flag = 1;//假定开始是相等
char *findstrbak= findstr;
char *nowdestbak= destbak;
while(*findstrbak != '\0')
{
if(*nowdestbak != '\0')
{
if(*findstrbak != *nowdestbak)
{
flag= 0;
}
nowdestbak++;
findstrbak++;
}
else
{
flag= 0;
break;
}
}
if (flag == 1)
{
p =destbak;
returnp;
}
destbak++;
}
return NULL;
}
//执行字符串指令
void run(mystring *string)
{
system(string->p);
}
//初始化
void init(mystring *string)
{
string->p = NULL;
string->reallength =0;//初始化结构体字符串
}
//使用长度初始化
void initwithlength(mystring *string, int length)
{
//string->p = (char*)malloc(sizeof(char)*length);//分配内存,没有初始化,不好用
string->p = (char*)calloc(length, sizeof(char));//分配内存,并初始化为0
string->reallength =length;
}
//使用字符串初始化
void initwithstring(mystring *string, char *copystring)
{
int length =mystrlen(copystring);//获取长度
string->p = (char *)calloc(length+ 1, sizeof(char));//分配内存
mystrcpy(string->p,copystring);//拷贝字符串
//设置长度
string->reallength =length + 1;
}
//打印字符串
void printfstring(mystring *string)
{
printf("\n%s",string->p);
}
//尾部增加字符
void backaddchar(mystring *string, char ch)
{
int length =mystrlen(string->p);
if (length + 1 ==string->reallength)
{
//重新分配
string->p =realloc(string->p, string->reallength + 1);
string->reallength+= 1;
string->p[string->reallength- 2] = ch;
string->p[string->reallength- 1] = '\0';
}
else
{
int nowlength =mystrlen(string->p);//求出当前长度
string->p[nowlength]= ch;
string->p[nowlength+ 1] = '\0';
}
}
void backaddstring(mystring *string, char *str)
{
int nowmystringlength =mystrlen(string->p);//获取当前长度
int addstringlength =mystrlen(str);
if (string->reallength< nowmystringlength + addstringlength + 1)
{
intneedaddlength = nowmystringlength + addstringlength + 1 -string->reallength;
//分配内存
printf("%x",string->p);
string->p =(char *)realloc(string->p, nowmystringlength+needaddlength + 1);
string->p[nowmystringlength]= '\0';
mystrcat(string->p,str);
string->reallength+= needaddlength;
string->p[string->reallength-1]= '\0';
}
else
{
mystrcat(string->p,str);
}
}
//查找字符
char *findfirstchar(mystring *string, char ch)
{
char *p =mystrchr(string->p, ch);
return p;
}
//查找字符串
char *findfirststring(mystring *string, char *str)
{
char *p =mystrstr(string->p, str);
return p;
}
//删除字符
int deletefirstchar(mystring *string, char ch)
{
char *p = mystrchr(string->p,ch);
if (p = NULL)
{
return 0;
}
else
{
char *pnext = p+ 1;
while (*pnext !='\0')
{
*p =*pnext;
p++;
pnext++;
}
*p = '\0';//字符串一定要结尾
return 1;
}
}
//删除字符串
int deletefirststring(mystring *string, const char * const str)
{
char *p =mystrstr(string->p, str);
if (p == NULL)
{
return 0;
}
else
{
int length =mystrlen(str);
char *pnext = p+ length;
while (*pnext !='\0')
{
*p =*pnext;
p++;
pnext++;
}
*p = '\0';
return 1;
}
}
//增加字符
void addchar(mystring *string, char ch, char *pos)
{
if (string == NULL || pos== NULL)
{
return;
}
if (mystrlen(string->p)+ 1 == string->reallength)
{
//重新分配
string->p =realloc(string->p, string->reallength + 1);
string->reallength+= 1;
int nowlength =mystrlen(string->p);//求出当前长度
int movelength =mystrlen(pos);
for (int i =nowlength; i > nowlength - movelength; i--)
{
string->p[i]= string->p[i - 1];
}
string->p[nowlength- movelength] = ch;
string->p[nowlength+ 1] = '\0';
}
else
{
int nowlength =mystrlen(string->p);//求出当前长度
int movelength =mystrlen(pos);
for (int i =nowlength; i > nowlength - movelength; i--)
{
string->p[i]= string->p[i - 1];
}
string->p[nowlength- movelength] = ch;
string->p[nowlength+ 1] = '\0';
}
}
//增加字符串
void addstring(mystring *string, char *str, char *pos)
{
if (string == NULL || pos== NULL)
{
return;
}
else
{
intnowmystringlength = mystrlen(string->p);//获取当前长度
intaddstringlength = mystrlen(str);
if(string->reallength < nowmystringlength + addstringlength + 1)
{
intneedaddlength = nowmystringlength + addstringlength + 1 -string->reallength;
//分配内存
printf("%x\n",string->p);
string->p= (char *)realloc(string->p, string->reallength + needaddlength);
string->reallength+= needaddlength;
string->p[string->reallength- 1] = '\0';
//先移动,再拷贝
intnowlength = mystrlen(string->p);//求出当前长度
intmovelength = mystrlen(pos);
intinsertlength = mystrlen(str);
printf("%d,%d,%d\n",nowlength,movelength,insertlength);
printf("%x",string->p);
for(int i = nowlength; i >= nowlength - movelength; i--)
{
printf("%c",string->p[i - insertlength]);
string->p[i+insertlength]= string->p[i];
}
for(int j = 0; j < insertlength; j++)
{
string->p[nowlength- movelength + j] = str[j];
}
string->p[string->reallength]= '\0';
}
else
{
//先移动,再拷贝
intnowlength = mystrlen(string->p);//求出当前长度
intmovelength = mystrlen(pos);
intinsertlength = mystrlen(str);
for(int i = nowlength; i >= nowlength - movelength; i--)
{
string->p[i+ insertlength] = string->p[i];
}
for(int j = 0; j < insertlength; j++)
{
string->p[nowlength- movelength + j] = str[j];
}
string->p[string->reallength]= '\0';
}
}
}
//替换第一个找到的字符
void changefirstchar(mystring *string, const char oldch, const charnewch)
{
if (string->p == NULL)
{
return;
}
char *pstr = string->p;
while (*pstr != '\0')
{
if (*pstr ==oldch)
{
*pstr =newch;
return;
}
pstr++;
}
}
//替换第一个字符串
void changefirststring(mystring *string, const char * const oldstr,const char * const newstr)
{
if (string->p == NULL)
{
return;
}
char *pfind =findfirststring(string, oldstr);
if (pfind != NULL)
{
deletefirststring(string,oldstr);//删除
addstring(string,newstr, pfind);//插入
}
}
测试
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>
#include "字符串.h"
void main()
{
mystring string;
initwithstring(&string,"note");
//printfstring(&string);
//backaddchar(&string,'j');
//printfstring(&string);
backaddstring(&string,"pad");
//printfstring(&string);
//run(&string);
char *strp =findfirstchar(&string, 'a');
*strp = 'A';
//printf("%x",strp);
//printfstring(&string);
char *strpstr =findfirststring(&string, "ad");
//printf("\n%x",strpstr);
if (strpstr != NULL)
{
*strpstr = 'x';
}
printfstring(&string);
//deletefirstchar(&string,'o');
//deletefirststring(&string,"ot");
char *p =findfirstchar(&string, 't');
if (p != NULL)
{
//addchar(&string,'B', p);
addstring(&string,"haha", p);
}
while(findfirststring(&string, "haha"))
{
changefirststring(&string,"haha", "hehe");
}
printfstring(&string);
getchar();
}
13. strncat
strncat(str1,str2,n),从str2中拷贝n个字节到str1。
void main27()
{
char str1[30] = "haha哈哈哈";
char str2[20] = "三三四四";
strncat(str1, str2, 2);
printf("%s",str1);
getchar();
}
自定义strncat
void mystrncat(const char * const dest, const char * const source, intlength)
{
if (dest == NULL || source== NULL)
{
return;
}
char *destbak = dest;
while (*destbak != '\0')
{
destbak++;
}
for (int i = 0; i <length; i++)
{
*destbak =source[i];
destbak++;
}
}
void main()
{
char str1[30] = "haha哈哈哈";
char str2[20] = "三三四四";
mystrncat(str1, str2, 2);
printf("%s",str1);
getchar();
}
14. atoi
stoi(str);传递字符串的地址,从这个地址开始将字符串的内容转换为整数。
void main()
{
char str[10] ="6636";
int num = atoi(str + 1);//转换的时候,传递字符串的地址,不要求是首地址,任何地址都可以
printf("%d",num);
system("pause");
}
15. strrev
反转字符串,strrev(str),str是要逆转的字符串。
void main()
{
char str[20] ="nizhuan";
printf("%s\n",str);
_strrev(str);
printf("%s",str);
getchar();
}
自定义逆转字符串函数
void mystrrev(char *str)
{
int length =strlen(str);//获取字符串长度
for (int i = 0; i <length / 2; i++)
{
char temp;
temp = str[i];
str[i] =str[length - 1 - i];
str[length - 1 -i] = temp;
}
}
void main()
{
char str[20] = "nizhuan";
printf("%s\n",str);
mystrrev(str);
printf("%s",str);
getchar();
}
16. 大小写转换
使用_strupr(str)小写转大写,_strlwr(str)大写转小写。
void main()
{
char str[30] ="nostepad";
_strupr(str);
printf("%s\n",str);
_strlwr(str);
printf("%s",str);
getchar();
}
自定义大小写转换函数
void mystrupr(char *str)
{
if (str == NULL)
{
return;
}
while(*str != '\0')
{
if (*str >='a' && *str <= 'z')
{
*str =*str - ('a' - 'A');
}
str++;
}
}
void mystrlwr(char *str)
{
if (str == NULL)
{
return;
}
while (*str != '\0')
{
if (*str >='A' && *str <= 'Z')
{
*str =*str + ('a' - 'A');
}
str++;
}
}
void main()
{
char str[30] ="nostepad";
mystrupr(str);
printf("%s\n",str);
mystrlwr(str);
printf("%s",str);
getchar();
}
17. 内存函数
memset(str,ch,n),从字符串str的首地址开始前进n个字节,将第二个参数替换这一段地址。
常用于清空字符串。
void main()
{
char str[100] ="helle ll hellow wwe hllee";
printf("%s\n",str);
memset(str, 'a', 10);
//第一个参数是内存的首地址,第二个参数要设置的字符,第三个参数是一个整数,从首地址开始要前进的字节,把这一段设置为第二个参数
//常用语清空字符串memset(str,'\0', strlen(str));
printf("%s",str);
getchar();
}
自定义memset
void *mymemset(const void * const source, char ch, int length)
{
char *sourcebak = (char*)source;
if (source == NULL)
{
returnsourcebak;
}
while (length > 0)
{
*sourcebak = ch;
sourcebak++;
length--;
}
return source;
}
void main()
{
char str[40] ="hshllsfdsdf";
mymemset(str, 'c', 5);
printf("%s",str);
getchar();
}
memcpy(str1,str2,n),内存拷贝,从str1开始,将str2的前n个字节拷贝到str2。
void main32()
{
char str[30] ="hahallskwe";
char str2[20] ="xxxx";
//从地址str开始,从str2拷贝4个字节到str
memcpy(str, str2, 4);
printf("%s",str);
getchar();
}
memcpy也可以拷贝字符数组,按照内存字节拷贝,不管什么类型都是拷贝二进制数组。
void main()
{
int a[] = { 12, 3, 32, 4,5, 2 };
int b[] = { 1, 2, 3, 4, 5,6 };
memcpy(a, b, 8);
for (int i = 0; i < 6;i++)
{
printf("%d\n",a[i]);
}
getchar();
}
自定义memcpy
void *mymemcpy(char *dest, char *source, unsigned int length)
{
if (dest == NULL || source== NULL || length == 0)
{
return NULL;
}
char *destbak = dest;
char *sourcebak = source;
//for (; sourcebak <source + length; sourcebak++,destbak++)
//{
// *destbak = *sourcebak;
//}
//或者
while (length--)
{
*destbak++ =*sourcebak++;
}
return dest;
}
void main()
{
char str1[20] ="hksldjfsfsaf";
char str2[20] ="123423434aaaaa";
mymemcpy(str1, str2, 5);
printf("%s",str1);
getchar();
}
memccpy(str1,str2,ch,n),从str1开始,从str2复制n个字节到到str1,但是在str2中遇到字符ch就终止。
void main()
{
char str[30] ="hahallskwe";
char str2[20] ="xaxxx";
_memccpy(str, str2, 'a',4);
//_memccpy(str, str2, 'b',4);
printf("%s", str);
getchar();
}
memchr(str,ch,n),从字符str开始前进n个字节,检索是否存在字符ch,如果存在返回地址,否则返回NULL。
void main()
{
char str1[30] ="hahlworllds";
char ch = 'w';
//从地址str1开始前进30个字节检索,如果存在返回地址,否则返回空
char *p = memchr(str1, ch,30);
if (p == NULL)
{
printf("没有找到");
}
else
{
printf("找到字符%c,地址%p", *p, p);
}
getchar();
}
memicmp(str1,str2,n),比较str1和str2前n个字节忽略大小是否相等,相等返回0,不等返回1或-1。
void main()
{
char *str1 ="abcdse12";
char *str2 ="ABCDSd12";
int num = _memicmp(str1,str2, 6);
if (num == 0)
{
printf("忽略大小写相等");
}
else
{
printf("不等%d", num);
}
getchar();
}
18. 字符串转整数
int tonum(char *str)
{
int num = 0;
char *strbak = str;
while (*strbak != '\0')
{
if (*strbak <'0' || *strbak > '9')
{
return-1;
}
*strbak =*strbak;
*strbak++;
num++;
}
int shu = 1;
int res = 0;
for (int i = num - 1; i>= 0; i--)
{
int wei = str[i]- 48;
wei *= shu;
res += wei;
shu *= 10;
}
return res;
}
void main()
{
char str[10] ="12343";
printf("%d",tonum(str));
getchar();
}
19. 整数转字符串
void tostr(int num, char *str)
{
int wei = 0;
for (int i = num; i; i /=10)
{
wei++;
}
printf("%d\n",wei);
for (int i = wei-1; num;num /= 10, i--)
{
str[i] = num %10 + 48;
}
printf("%s",str);
getchar();
}
void main()
{
int num = 12343;
char str[10] = { 0 };
tostr(num, str);
}
20. 删除字符与删除字符串
删除字符
方式1
void main()
{
char str[200] ="hellohllwwoolweroolwleo";
char ch = 'o';
char *p = str;
while (*p != '\0')
{
if (*p == ch)
{
char*p1 = p;
char*p2 = p + 1;
while(*p1 != '\0')
{
*p1= *p2;
p1++;
p2++;
}
}
else
{
p++;
}
}
printf("%s",str);
getchar();
}
方式2
void main()
{
char str[30] ="helllweorerhele";
char ch = 'l';
char laststr[30] = { 0 };
char *p = str;
int i = 0;//保存laststr的下标
while (*p != '\0')
{
if (*p != ch)
{
laststr[i]= *p;
i++;
}
p++;
}
printf("\n%s",laststr);
system("pause");
}
删除字符串
void main()
{
char allstr[100] ="heheaaheheswerehehellk";
char str[30] ="hehe";
char *p;
while ((p = strstr(allstr,str)) != NULL)//只要能找到字符串就继续否则退出循环
{
int length =strlen(str);
char *p1 = p;
char *p2 = p +length;
while (*p1 !='\0')
{
*p1 =*p2;
p1++;
p2++;
}
}
printf("%s",allstr);
getchar();
}
21. 检查进程中是否有qq在运行
void execmd(char *cmd, char *result)
{
char buffer[128] = { 0};//定义一块字符串缓冲区
FILE *pipe = _popen(cmd,"r");//以读取模式,执行指令的指令,把管道当作文件处理
if (pipe == NULL)
{
printf("失败");
return;
}
else
{
while(!feof(pipe))//判断是否到达文件末尾,没有就继续,到了文件末尾,返回非0,否则返回0
{
if(fgets(buffer, 128, pipe))//读取文件的缓冲区,如果值为非0则是读取成功
{
strcat(result,buffer);//链接字符串,将结果保存到result
}
}
_pclose(pipe);//关闭管道的作用
return;
}
}
void main()
{
char output[4096] = { 0};//定义一个字符串接收输出
execmd("tasklist",output);//执行指令,将结果保存到output
printf("%s\n",output);//输出结果
char *pr = strstr(output,"QQ");
if (pr == NULL)
{
printf("qq没有运行");
}
else
{
printf("qq在运行,%x\n", pr);
}
getchar();
}
22. 字符串排序
void main()
{
char str[10][20] = {"lenn", "huaaa", "xiawwe", "chuiii","tenss", "baiss", "alii", "333","jjing", "yokk" };
for (int i = 0; i < 10;i++)
{
printf("%s\n", str[i]);
}
for (int i = 0; i < 10- 1; i++)
{
for (int j = 0;j < 10 - 1 - i; j++)
{
if(strcmp(str[j], str[j + 1]) == 1)
{
chartemp[30];
strcpy(temp,str[j]);
strcpy(str[j],str[j + 1]);
strcpy(str[j+ 1], temp);
}
}
}
printf("排序后\n");
for (int i = 0; i < 10;i++)
{
printf("%s\n",str[i]);
}
getchar();
}
23. unicode字符集
右击项目,属性,可以设置字符集。默认是多字节字符集。可以选择使用unicode字符集。如果使用unicode字符集,在输出非ascii字符时可能出现乱码问题。一个字符在unicode字符集下占两个字节,也就是两个字符的长度。如果在字符数组中输出汉字,可以使用2个%c输出一个汉字。也可以使用宽字符解决中文的乱码问题。一个宽字符占两个字符的长度,也就是2字节,一个宽字符可以输出一个汉字。
使用宽字符要设定语言本地化。
void main()
{
//设定语言版本
setlocale(LC_ALL,"chs");
char str1[10] = "很好看看";
printf("你哈两类\n");
printf("%c%c\n",str1[0], str1[1]);
wchar_t ch = L'看';
wprintf(L"%wc\n",ch);
wchar_t str2[100] =L"似懂非懂两类";
wprintf(L"%s\n",str2);
getchar();
//MessageBox(0, "我11","hah啊", 0);
}
void main()
{
//设定语言版本
setlocale(LC_ALL,"chs");
wchar_t ch1 = L'为';
wchar_t ch2 = L'看';
wchar_t ch3 = L'A';
wprintf(L"%wc%wc%wc",ch1, ch2, ch3);
getchar();
}
十五、 结构体
1. 定义和使用
结构体是一种构造数据类型。把不同类型的数据组合成一个整体,自定义数据类型。结构体类型定义
struct 结构体名{
类型标识符 成员名;
类型标识符 成员名;
……
};
struct不能省略。结构体名可以省略,就是无名结构体。
struct nameinfo
{
char name[50];
char tel[20];
int id;
};
//无名结构体
struct
{
char name[50];
char tel[20];
int id;
};
结构体分配内存,当创建结构体变量时,根据类型或者有数组就和数组长度分配。
结构体的作用域在结构体定义代码段的下方,只有在结构体代码的下方才可以引用这个结构体。先定义结构体,再声明结构体变量,使用之。
struct nameinfo
{
char name[50];
char tel[20];
int id;
};
void main()
{
struct nameinfo myinfo;//创建一个结构体变量
myinfo.id = 10;
printf("编号%d\n",myinfo.id);
sprintf(myinfo.name,"哈哈");
printf("%s\n",myinfo.name);
strcpy(myinfo.tel,"1233334");
printf("%s\n",myinfo.tel);
getchar();
}
使用define声明结构体变量
#define D struct dadd;
struct dadd
{
char email[30];
char name[30];
char address[200];
int id;
int bigid;
char tel[20];
char mobile_phone[20];
double total_price;
};
D d1, d2;
紧跟着结构体的定义声明结构体变量
struct dadd2
{
char email[30];
char name[30];
char address[200];
int id;
int bigid;
char tel[20];
char mobile_phone[20];
double total_price;
}da1,da2,da3;
2. 结构体类型与结构体变量
结构体类型与结构体变量概念不同,类型,不分配内存,不能赋值、存取、运算;变量,分配内存,可以赋值、存取、运算。结构体可以嵌套。结构体成员名与程序中变量名可以相同,不会混淆。
初始化结构变量
使用大括号的方式
void main()
{
struct dadd d1 = {
"23w@11.COM",
"hh",
"lkjlj嗖嗖嗖",
12,
123,
"12343",
"234343",
22.3
};
if (d1.id == 12)
{
printf("%s,%s,%s,%d,%d,%s,%s,%f",d1.name, d1.email, d1.address, d1.id, d1.bigid, d1.tel, d1.mobile_phone,d1.total_price);
}
getchar();
}
使用在定义结构体时初始化结构体变量的方式
struct dadd2
{
char email[30];
char name[30];
char address[200];
int id;
int bigid;
char tel[20];
char mobile_phone[20];
double total_price;
int dadd;
}da1 = {
"23w@11.COM",
"hh",
"lkjlj嗖嗖嗖",
12,
123,
"12343",
"234343",
22.3
};
void main()
{
if (da1.id == 12)
{
printf("%s,%s,%s,%d,%d,%s,%s,%f",da1.name, da1.email, da1.address, da1.id, da1.bigid, da1.tel, da1.mobile_phone,da1.total_price);
}
getchar();
}
使用匿名结构体在定义时初始化结构体变量的方式。这种方式也是访问匿名结构体的方式。一般,对于限量初始化变量的方式等采用匿名结构体。
struct
{
char name[50];
char tel[20];
int id;
}niming1 = {"niming","12343",1};
void main()
{
if (niming1.id == 1)
{
printf("%s,%s,%d",niming1.name, niming1.tel, niming1.id);
}
getchar();
}
结构体变量的引用,规则,不能整体引用,只能引用变量成员。引用方式,结构体变量名.成员名。
struct struct1
{
char name[20];
};
void main()
{
struct struct1 ss = {"hah" };
//printf("%s\n",ss);
printf("%s\n",ss.name);
getchar();
}
将结构体变量赋值给另一个结构体变量,前提是必须是同一个类型的结构体变量
struct daa
{
char email[30];
char name[30];
char address[200];
int id;
int bigid;
char tel[20];
char mobile_phone[20];
double total_price;
}d3, d4 = {
"23w@11.COM",
"hh",
"lkjlj嗖嗖嗖",
12,
123,
"12343",
"234343",
22.3
};
void main()
{
d3 = d4;
if (d3.id = 12)
{
printf("%s,%s",d3.address, d3.name);
}
getchar();
}
3. 结构体变量的嵌套
一个结构体中包含另外一个结构体,嵌套结构体用多个点访问。
struct struct3
{
char str[200];
int id;
};
struct struct4
{
int data;
char str4[200];
struct struct3 st3;
};
void main()
{
struct struct4 str4;
str4.data = 12;
printf("%d\n",str4.data);
strcpy(str4.st3.str,"hhhas");
printf("%s\n",str4.st3.str);
sprintf(str4.str4, "实体00");
printf("%s\n",str4.str4);
getchar();
}
struct struct5
{
int id;
char name[50];
struct struct6
{
char str[50];
int id;
}str6;//内部定义的第一种方式
struct struct6 str7;//内部定义的第二种方式
};
void main()
{
struct struct5 str5;
str5.id = 12;
sprintf(str5.str6.str,"嵌套的结构体的成员");
str5.str6.id = 13;
if (str5.str6.id == 13)
{
printf("%d,%s\n",str5.id, str5.str6.str);
}
str5.id = 14;
sprintf(str5.str7.str,"第二种方式定义的嵌套内的结构体");
str5.str7.id = 15;
if (str5.str7.id == 15)
{
printf("%d,%s\n",str5.id, str5.str7.str);
}
getchar();
}
4. 结构体数组
结构体数组定义的三种形式
第一种形式,创建结构体变量时
struct ddd
{
char email[30];
char name[30];
char address[200];
int id;
int bigid;
char tel[20];
char mobile_phone[20];
double total_price;
};
void main()
{
int a;
int a[5];
struct ddd d1;
struct ddd dd[100];//定义结构体数组
}
第二种形式,在定义结构体时定义结构体数组
struct ddd
{
char email[30];
char name[30];
char address[200];
int id;
int bigid;
char tel[20];
char mobile_phone[20];
double total_price;
}dd2[10];
第三种形式,匿名结构体的结构体数组的定义
struct
{
char email[30];
char name[30];
char address[200];
int id;
int bigid;
char tel[20];
char mobile_phone[20];
double total_price;
}dd3[10];
结构体数组的初始化
第一种方式,顺序初始化,按照成员的先后顺序,初始化。
struct data
{
int num;
float f1;
char name[4];
}db[3] = {
1, 2.2, "hh",
2, 5.2, "hssh",
3, 6.2, "hggh"
};
void main()
{
printf("%d\n",sizeof(struct data));//打印结构体的长度
printf("%x\n",db);
printf("%x\n",&db[0]);
printf("%x\n",&db[1]);
printf("%x\n",&db[2]);
getchar();
}
第二种方式,整体初始化
struct data
{
int num;
float f1;
char name[4];
}db2[3] = {
{ 1, 2.2, "hh"},
{ 2, 5.2, "hssh"},
{ 3, 6.2, "hggh"}
};
第三种方式是匿名结构体的初始化
struct
{
int num;
float f1;
char name[4];
}db4[3] = {
{ 1, 2.2, "hh"},
{ 2, 5.2, "hssh"},
{ 3, 6.2, "hggh"}
},
db3[3] = {
1, 2.2, "hh",
2, 5.2, "hssh",
3, 6.2, "hggh"
};
访问结构体数组
struct
{
int num;
float f1;
char name[5];
}db4[3] = {
{ 1, 2.2, "hh"},
{ 2, 5.2, "hssh"},
{ 3, 6.2, "hggh"}
},
db3[3] = {
1, 2.2, "hh",
2, 5.2, "hssh",
3, 6.2, "hggh"
};
void main()
{
printf("%x\n",db3);
printf("%x,%d,%f,%s\n",&db3[0],db3[0].num,db3[0].f1,db3[0].name);
printf("%x,%d,%f,%s\n",&db3[1], db3[1].num, db3[1].f1, db3[1].name);
printf("%x,%d,%f,%s\n",&db3[2], db3[2].num, db3[2].f1, db3[2].name);
getchar();
}
5. 指针与结构体
结构体指针是指向结构体的指针。
struct data2
{
int id;
char str[200];
};
void main()
{
int a;
struct data2 *p;//p存储地址,struct data2指定长度和如何解析
printf("%d\n",sizeof(p));
struct data2 d1;
printf("%x\n",&d1);
d1.id = 12;
sprintf(d1.str, "哈哈哈");
printf("%d,%s\n",d1.id, d1.str);
p = &d1;
printf("%d,%s\n",p->id, p->str);
printf("%d,%s\n",(*p).id, (*p).str);
(*p).id = 14;
sprintf(p->str, "修改指针指向的数据");
printf("%d,%s\n",p->id, p->str);
getchar();
}
结构体数组指针是指向结构体数组的指针
struct data3
{
int num;
float f1;
char name[5];
}dba4[3] = {
{ 1, 2.2, "hh"},
{ 2, 5.2, "hssh"},
{ 3, 6.2, "hggh"}
},
dba3[3] = {
1, 2.2, "hh",
2, 5.2, "hssh",
3, 6.2, "hggh"
};
void main()
{
struct data3 *p;
struct data3 datasize;
p = dba3;
int size = sizeof(dba3) /sizeof(datasize);
printf("%d\n",size);
for (int i = 0; i <size; i++)
{
printf("%d,%f,%s\n",(p+i)->num, (p+i)->f1, (p+i)->name);
}
for (; p < dba3 + size;p++)
{
printf("%d,%f,%s\n",p->num, p->f1, p->name);
}
getchar();
}
6. 指向结构体的指针作为函数参数
用指向结构体的指针作函数参数。
比较用结构体变量的成员作参数,成员作参数是值传递;指向结构体变量或数组的指针作参数是地址传递。用结构体变量做参数,多值传递,效率低。
指向结构体的指针作为参数时
struct data4
{
int id;
char name[30];
};
void change(int id)
{
id = 1000;
}
void changes(struct data4 data)
{
data.id = 122;
sprintf(data.name, "函数修改");
}
void changep(struct data4 *p)
{
p->id = 4;
sprintf(p->name, "指针参数修改结构体");
}
void main()
{
struct data4 d4;
d4.id = 10;
sprintf(d4.name, "哈哈");
change(d4.id);
printf("%d\n",d4.id);
changes(d4);
printf("%d,%s\n",d4.id, d4.name);
changep(&d4);
printf("%d,%s\n",d4.id, d4.name);
getchar();
}
指向结构体数组的指针
struct data4
{
int id;
char name[30];
};
void arrchange(struct data4 d4[10])
{
d4[0].id = 111;
sprintf(d4[0].name, "指针数组函数修改值");
}
void arrchangep(struct data4 *p, int id)
{
(p + id)->id = 123;
sprintf((p + id)->name,"指针数组函数修改值传递指针");
}
void main()
{
struct data4 d4[10];
d4[0].id = 10;
sprintf(d4[0].name, "哈哈");
struct data4 *p = d4;
arrchange(p);
printf("%d,%s\n",d4[0].id, d4[0].name);
arrchangep(p,0);
printf("%d,%s\n",d4[0].id, d4[0].name);
getchar();
}
7. 内存动态分配
对于结构体数组,定义时的字节数是有限的。需要使用内存动态分配,指向结构体数组的指针分配空间。
struct data4
{
int id;
char name[30];
};
void main()
{
//struct data4 d4[1024 *1024];//数组可分配的空间有限,无法分配
struct data4 *p = (structdata4 *)malloc(sizeof(struct data4) * 5);
p->id = 12;
sprintf(p->name,"haha");
printf("%d,%s\n",p->id, p->name);
getchar();
}
三种方式访问指针指向的结构体数组
struct data4
{
int id;
char name[30];
};
void main()
{
struct data4 *p = (structdata4 *)malloc(sizeof(struct data4) * 10);
for (int i = 0; i < 10;i++)
{
//p[i]相当于一个结构体数组
p[i].id = i + 1;
sprintf(p[i].name,"haha%d", i);
printf("%d,%s\n",p[i].id, p[i].name);
}
printf("指针访问的方式\n");
for (int i = 0; i < 10;i++)
{
//()避免优先级的歧义
(*(p + i)).id =i + 10;
sprintf((*(p +i)).name, "haha%d", i + 10);
printf("%d,%s\n",(*(p + i)).id, (*(p + i)).name);
}
printf("指针轮询\n");
int i = 20;
for (struct data4 *px = p;px < p + 10; px++, i++)
{
px->id = i;
sprintf(px->name,"haha%d", i);
printf("%d,%s\n",px->id, px->name);
}
getchar();
}
8. 结构体在内存中的存储,字节对齐
结构体变量占据的内存单元的个数应当大于等于其呃逆粗所有数据成员占内存单元的和。
出于效率的考虑,c语言引入了字节对齐机制。一般来说,不同的编译器字节对齐机制有所不同,但还是有以下3条通用准则:
结构体变量的大小能够被其最宽基本类型成员的大小所整除;
结构体每个成员相对于结构体首地址的偏移量offset都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
struct st1
{
char c; //311500
int i1; //311504
short s1;//311508,因为int占4个字节到300508而8也是short大小2的整数倍
};
struct st2
{
char c; //666500
short i1; //666502,不是666501,而是2的整数倍
int s1;//666508,因为int占4个字节到300508而8也是short大小2的整数倍
};
void main()
{
printf("%d\n",sizeof(struct st1));
printf("%d\n",sizeof(struct st2));
getchar();
}
结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
struct info1
{
int id;
char name[1];
};
struct info2
{
short id;
char name[1];
};
struct info3
{
short id;
char name[9];
};
struct info4
{
int id;
char name[9];
};
struct info5
{
double id;
char name[9];
};
void main()
{
printf("%d\n",sizeof(struct info1));
printf("%d\n",sizeof(struct info2));
printf("%d\n",sizeof(struct info3));
printf("%d\n",sizeof(struct info4));
printf("%d\n",sizeof(struct info5));
getchar();
}
9. 深拷贝与浅拷贝
浅拷贝只是复制地址,而深拷贝是复制内容。
struct string
{
char *p;
int length;
};
void main10()
{
struct string str1;
str1.length = 11;
str1.p = (char*)malloc(sizeof(char)* 10);
strcpy(str1.p,"hhllwe");
printf("%d,%s\n",str1.length, str1.p);
struct string str2;
str2.length = str1.length;
str2.p = str1.p;//浅拷贝
*(str1.p) = 'k';
printf("%d,%s\n",str2.length, str2.p);
getchar();
}
void main()
{
struct string str1;
str1.length = 11;
str1.p = (char*)malloc(sizeof(char)* 10);
strcpy(str1.p,"hhllwe");
printf("%d,%s\n",str1.length, str1.p);
struct string str2;
str2.length = str1.length;
str2.p = (char*)malloc(sizeof(char)* 10);
strcpy(str2.p, str1.p);
*(str1.p) = 'k';
printf("%d,%s\n",str2.length, str2.p);
getchar();
}
十六、 共用体
构造数据类型,也叫联合体。作用是使几个不同类型的变量共占一段内存(相互覆盖)。
1. 定义共用体
定义形式
union 共用体名
{
类型标识符 成员名;
类型标识符 成员名;
……
};
union info
{
double price;
char brand[20];
};
2. 定义共用体变量
形式1
也可以使用指针或数组定义共用体变量
union info
{
double price;
char brand[20];
}data1,data2,*datap,data[10];
最大的成员变量占多大内存,共用体就占多大内存。
union info
{
double price;
char brand[20];
}data1,data2;
void main()
{
union info info1;//定义共用体变量
info1.price = 12.3;
sprintf(info1.brand,"哈哈");
printf("%f,%s\n",info1.price, info1.brand);
printf("%d\n",sizeof(union info));
getchar();
}
形式2
union info
{
double price;
char brand[20];
}data1,data2,*datap,data[10];
union info data3, data4, *datap2, data5[10];
形式3,也就是匿名共用体变量的定义
union
{
double price;
char brand[20];
}i1, i2, i3;
//匿名共用体只能引用创建类型的时候定义的几个变量。
void main()
{
i1.price = 12.3;
printf("%f",i1.price);
getchar();
}
3. 初始化共用体变量
共用体变量起作用的成员是最后一次存放的成员变量。
void main()
{
union info info6;
strcpy(info6.brand, "看看");
info6.price = 12.3;
printf("%f,%s\n",info6.price, info6.brand);
getchar();
}
不能在定义共用体时初始化共用体变量
union u5
{
int num;
char name[30];
}a1 = {1,"哈哈"};
使用{}初始化的时候只能初始化第一个
union u5
{
int num;
char name[30];
};
void main()
{
union u5 u1= { "联想" };//此时只能初始化第一个也就是num,类型不符合,结果不符合预期
printf("%d,%s\n",u1.num, u1.name);
getchar();
}
可以使用共用体变量为共用体变量赋值
union u5
{
int num;
char name[30];
};
void main()
{
union u5 u1= {30};
printf("%d,%s\n",u1.num, u1.name);
union u5 u2 = u1;
printf("%d,%s\n",u2.num, u2.name);
getchar();
}
4. 共用体的大小
共用体的大小取决于占据最多内存的成员的长度。
union u1
{
char c;
short s;
float i;
};
void main()
{
printf("%d",sizeof(union u1));
getchar();
}
结果为4,字节对齐准则,共用体的总大小为共用体最宽基本类型成员大小的整数倍,略不同于结构体,因为共用体同一时间只被一个成员变量所占用。如有需要编译器会在最末一个成员之后加上填充字节,仍然成立。使用字节对齐,以空间换时间,有利于cpu快速寻址。
union u1
{
char c;
short s;
float i;
};
union u2
{
char c[9];
double d;
};
union u3
{
char c[7];
double d;
};
union u4
{
int d;
char c[9];
};
void main()
{
printf("%d\n",sizeof(union u1));
printf("%d\n",sizeof(union u2));//共用体的总大小为共用体最宽基本类型成员也就是double的整倍数
//如果是1倍为8,又小于char c[9]的长度9,所以是2倍。
printf("%d\n",sizeof(union u3));
printf("%d\n",sizeof(union u4));
getchar();
}
5. 变量引用与指针引用
变量引用引用格式为,共用体变量名.成员名。
union u5
{
int num;
char name[30];
};
void main()
{
union u5 u1 = { 10 };
sprintf(u1.name,"hh");
printf("%d,%s\n",u1.num, u1.name);
getchar();
}
指针引用成员,与结构体类似
union u5
{
int num;
char name[30];
};
void main()
{
union u5 u1 = { 10 };
sprintf(u1.name,"hh");
printf("%d,%s\n",u1.num, u1.name);
union u5 *p = &u1;
p->num = 12;
sprintf((*p).name, "哈哈");
printf("%d,%s\n",u1.num, u1.name);
getchar();
}
6. 结构体变量和共用体变量内存形式的不同
编译器会为结构体变量中的每个数据成员分配不同的地址空间,结构体变量中的数据程序是并列关系,而编译器为共用体变量中的数据成员分配的是同一块内存,每个时刻只有一个数据成员有意义,从地址的角度来看两者的差异,形象地表明了这一点。
7. 结构体中嵌套共用体和定义共用体
struct infor
{
char name[50];
long long phone;
};
union school
{
char name[50];
int id;
};
struct stu
{
struct infor i1;
union school s1;
union course
{
int id;
char name[20];
}c1;
};
void main()
{
struct stu stu1;
stu1.i1.phone = 12343434;
sprintf(stu1.i1.name,"hhkkl");
stu1.s1.id = 14;
sprintf(stu1.s1.name,"嗖嗖嗖");
stu1.c1.id = 12;
sprintf(stu1.c1.name,"喀喀喀");
printf("%lld,%s,%d,%s,%d,%s\n",stu1.i1.phone, stu1.i1.name, stu1.s1.id, stu1.s1.name, stu1.c1.id,stu1.c1.name);
struct stu *p;
p = (structstu*)malloc(sizeof(struct stu));
p->i1.phone = 12345344;
sprintf(p->i1.name,"zhizhen");
p->s1.id = 15;
sprintf(p->s1.name,"卡路里");
printf("%lld,%s,%d,%s\n",p->i1.phone, p->i1.name, p->s1.id, (*p).s1.name);
getchar();
}
十七、 枚举类型
字面上,枚举就是列举所有情况。c语言中,枚举类型也是这么用的。枚举类型是用户自定义的类型,在定义枚举类型时,需指明其取值集合,用枚举类型声明枚举变量时,只能取集合中的某项作为其值,这在一定程度上保证了取值的安全性。
1. 定义枚举类型
c语言提供了enum定义枚举类型,基本格式为:
enum 枚举类型名
{
枚举常量1[=整形常数],
枚举常量2[=整形常数],
……
};
枚举表就是这个,
{
枚举常量1[=整形常数],
枚举常量2[=整形常数],
……
}
如果不给枚举常量赋值,编译器会为每一个枚举常量赋值一个不同的整型值,第一个为0,以此类推。当枚举表中某个常量赋值后,其后的成员则按依次加1的规则确定其值。
enum level
{
博士,硕士,本科=22,专科,高中,初中=3,小学,幼儿园
};
void main()
{
enum level l1 = 高中;//限定只能是这个枚举范围内
printf("%d\n",l1);
enum level l2 = 初中;
printf("%d\n",l2);
enum level l3 = 硕士;
printf("%d\n",l3);
if (l1 > l2)
{
printf("高中高");
}
else
{
printf("初中高");
}
getchar();
}
枚举让常量变得清晰易懂。
2. 数组与枚举常量
enum week
{
wii1,ww2,ww3,ww4,ww5,ww6,ww7
};
int num[] = { 15, 16, 22, 33, 67, 62, 26 };
void main()
{
printf("------------------------\n");
printf("本周人数统计:");
for (enum week weeks =wii1; weeks <= ww7; weeks++)
{
printf("\n周%d有%d人登陆本网站", weeks+1,num[weeks]);
}
getchar();
}
十八、 typedef
类型别名,用自定义名字为已有数据类型命名。类型别名定义的简单形式:typedef type name;
typedef没有创建数据类型,而是给已经有的数据类型取一个别名;typedef仅仅适用于已有的类型,不能定义变量;typedef与define不同,define有#号是预编译时处理,简单字符替换,typedef编译时处理为已有类型命名。
typedef定义步骤
按定义变量方法先写出定义体;将变量名换成新类型名;最前面加typedef;用新类型名定义变量。
typedef定义各种类型
定义基本数据类型
typedef int 整数;
typedef float 实数;
void main1()
{
整数 a = 1;
实数 b = 1.2;
printf("%d,%f\n",a, b);
getchar();
}
typedef定义数组
void main()
{
typedef int S[200];
S a;
for (int i = 0; i < 10;i++)
{
a[i] = i;
printf("%d\n",a[i]);
}
getchar();
}
typedef定义字符串
void main()
{
//char *str;
//char *STR;
typedef char *STR;//也就是char *为STR
STR s1 = "哈哈";
printf("%s\n",s1);
STR s[5] = {"hah", "hkk", "hll", "lls","aaa" };
for (int i = 0; i < 5;i++)
{
printf("%s\n",s[i]);
}
getchar();
}
typedef定义指针
typedef int* P;
void main()
{
int num = 10;
P p = #
*p = 12;
printf("%d\n",num);
getchar();
}
typedef定义函数指针类型
void fun1()
{
system("notepad");
}
void main()
{
typedef void(*FUN1)();
FUN1 f1 = fun1;
f1();
getchar();
}
typedef定义结构体类型
typedef struct info
{
int id;
char name[20];
}INFO;
void main()
{
INFO i1 = { 1,"hh" };
printf("%d,%s\n",i1.id,i1.name);
getchar();
}
typedef定义结构体指针类型
typedef struct info
{
int id;
char name[20];
}INFO;
typedef INFO * pinfo;
void main()
{
INFO i1 = { 1,"hh" };
printf("%d,%s\n",i1.id,i1.name);
pinfo p1;
p1 =(pinfo)malloc(sizeof(INFO)* 5);
p1->id = 12;
sprintf(p1->name,"哈哈哈");
printf("%d,%s\n",(*p1).id, (*p1).name);
getchar();
}
用typedef声明数组类型、指针类型、结构体类型、共用体类型、枚举类型等,使得编程更加方便。
当不同源文件中用到同一类型数据时,常用typedef声明一些数据类型。可以把所有的typedef名称声明单独放在一个头文件中,然后在需要用到它们的文件中用#include指令把它们包含到文件中,这样就不需要在个文件中自己定义typedef名称了。
使用typedef名称有利于程序的通用与移植。有时候程序会依赖于硬件特性,用typedef类型就便于移植。
例如在16位和32位中int类型的字节数分别是2个和4个,为了便于移植。
//int在16位系统是2个字节,32位是4个字节
typedef int intN;//通用的程序
//typedef short intN;//16位迁移32位改成short,保证16位中的2个字节的在32位中是2个字节
//typedef long intN;//32位迁移16位给成long,保证32位中的4个字节在16位还是4个字节
十九、 位运算
一个字节等于8位。每个字节有0、1两个取值。总体来说,c语言中的位运算符有以下两类。位逻辑运算符,&位与,^位异或,|位或,~位取反。移位运算符,<<左移,>>右移。
1. ~取反
将操作数的每一位,0变1,1变0。
void mainx()
{
unsigned char ch = 15;
unsigned char fch = ~ch;
unsigned char ffch = ~fch;
printf("%d,%d,%d\n",ch, fch, ffch);
getchar();
}
位取反操作不改变操作数本身。
末位清零的通用方法
void main()
{
unsigned char ch = 73;
unsigned short sh = 16385;
unsigned int i = 123433;
printf("%d\n",ch & ~1);
printf("%d\n",sh & ~1);
printf("%d\n", i& ~1);
getchar();
}
取模不要用%
比如,73整除4的余数。73是0100 1001,4是100,求73的余数实际上0100 1001抹去100后剩余的数0100 0000,然后0100 1001减去0100 0000就是余数。
void main()
{
unsigned char ch = 73;
printf("%d\n",ch - (ch & ~3));
getchar();
}
被4整除,一般用于结构体对齐,便于内存查找。
2. &位与
将两个操作数的每一位进行与运算,规则是都是1才是1,否则是0。
void main()
{
unsigned char ch1 =3;//0000 0011
unsigned char ch2 =240;//1111 0000
unsigned char ch3 =255;//1111 1111
printf("%d\n",ch1&ch2);
printf("%d\n",ch1&ch3);
printf("%d\n",ch2&ch3);
getchar();
}
位与可以实现按位清零。
void main3()
{
unsigned char ch = 255;
unsigned char ch1 =15;//0000 1111
unsigned char ch2 =240;//1111 0000
unsigned char ch3 = 0;
printf("%d\n",ch & ch1);//清零高4位
printf("%d\n",ch & ch2);//清零低4位
printf("%d\n",ch & ch3);//全部清零
getchar();
}
位与可以实现取出指定位
二进制位与0向与清零,与1想与保留位不变。
void main()
{
unsigned char ch =169;//1010 1001
unsigned char ch1 =60;//00 1111 00取出指定位
printf("%d\n",ch & ch1);
getchar();
}
3. |位或
将对两个操作数的每一位进行或运算,位或运算的准则是,全是0才是0,否则是1。
void main()
{
unsigned char ch1 =169;//1010 1001
unsigned char ch2 =240;//1111 0000
printf("%d\n",ch1 | ch2);//249
getchar();
}
|运算可以让某些位变成1
void main()
{
unsigned char ch1 = 169;
unsigned char ch2 = 15;
unsigned char ch3 = 240;
printf("%d\n",ch1 | ch2);//低4位都变为1;
printf("%d\n",ch1 | ch3);//高4位都变为1;
getchar();
}
4. ^异或
将对两个操作数的每一位进行异或运算。规则是两个位相同为0,不同为1。
void main()
{
unsigned char ch1 =169;//1010 1001
unsigned char ch2 =15;//0000 1111
printf("%d\n",ch1^ch2);
getchar();
}
异或可以实现反转
void main()
{
unsigned char ch1 = 169;
unsigned char ch2 = 255;
unsigned char ch3 = 15;
unsigned char ch4 = 240;
unsigned char ch5 = 60;
printf("%d\n",ch1^ch2);//全部反转
printf("%d\n",ch1^ch3);//低4位反转
printf("%d\n",ch1^ch4);//高4位反转
printf("%d\n",ch1^ch5);//中间位反转
getchar();
}
交换两个变量,一般用于嵌入式开发,需要节约内存的场合。使用异或运算不会越界也更快,比+-*/更具优势。
void main()
{
unsigned char ch1 =20;//0001 0100
unsigned char ch2 =10;//0000 1010
printf("%d,%d\n",ch1, ch2);
//0001 0100ch1
//0000 1010ch2
//0001 1110ch1
ch1 = ch1^ch2;
//0000 1010ch2
//0001 1110ch1
//0001 0100ch2
ch2 = ch2^ch1;
//0001 1110ch1
//0001 0100ch2
//0000 1010ch1
ch1 = ch1^ch2;
printf("交换后\n%d,%d\n",ch1, ch2);
getchar();
}
位反,位与,位或,位异或,不关心操作数的符号,只是按操作数所在字的0或1进行处理。
5. 补码
数据在内存中存储的方式是低位在低字节,高位在高字节,与现实中人读数字的顺序是相反的。比如,0x12345678,按照1字节16进制显示在内存中样子是78 56 34 12。
整数的原码、反码、补码一致;负数的反码是原码除符号为取反,补码是反码+1。补码有利于简化涉及到负数的运算。
6. 移位运算
将某个量中的位向左或向右移动,移位不会改变操作数。移位在cpu的寄存器中完成计算,效率高,也不会改变操作数的,操作数在内存中。移位要预防溢出。
void main()
{
unsigned int ch = 1;
scanf("%u",&ch);
printf("%d\n",ch << 1);
printf("%d\n",ch << 2);
printf("%d\n",ch << 3);
printf("%d\n",ch);
ch = (ch << 1);
printf("%d\n",ch);
system("pause");
}
在没有发生溢出的情况下,左移x位相当于*2x。
void main()
{
unsigned char ch = 128;
printf("%d\n",ch >> 1);
printf("%d\n",ch >> 2);
printf("%d\n",ch);
ch = (ch >> 1);
printf("%d\n",ch);
getchar();
}
在没有发生溢出的情况下,右移x位相当于*(-2)x。
void main()
{
unsigned char ch =32;//0010 0000
printf("%x\n",&ch);
printf("%d\n",ch << 1);
printf("%d\n",ch >> 4);
system("pause");
}
7. 位运算复合运算符
a <<= 2 a = a << 2
a >>= 2 a = a >> 2
a &= 2 a = a & 2
a |= 2 a = a | 2
a ^= 2 a = a ^ 2
~只有一个操作数 a = ~a
void main()
{
unsigned char ch =4;//0000 0100
ch <<= 1;
printf("%d\n",ch);
ch >>= 2;
printf("%d\n",ch);
ch |= 1;
printf("%d\n",ch);
ch &= 1;
printf("%d\n",ch);
ch ^= 0;
printf("%d\n",ch);
printf("%d\n",ch = ~ch);
system("pause");
}
8. 位运算注意事项
位运算只适用于有符号整形、无符号整形和字符型,不适用于实数。
void main()
{
char ch = 'a';
short sh = 123;
int num = 1234;
float fl = 12.4;
double db = 43.2;
printf("%d,%d,%d,%d,%d,%d\n",ch & 1, ch | 1, ch ^ 1, ~ch, ch << 1, ch >> 1);
printf("%d,%d,%d,%d,%d,%d\n",sh & 1, sh | 1, sh ^ 1, ~sh, sh << 1, sh >> 1);
printf("%d,%d,%d,%d,%d,%d\n",num & 1, num | 1, num ^ 1, ~num, num << 1, num >> 1);
printf("%d,%d,%d,%d,%d,%d\n",fl & 1, fl | 1, fl ^ 1, ~fl, fl << 1, fl >> 1);
printf("%d,%d,%d,%d,%d,%d\n",db & 1, db | 1, db ^ 1, ~db, db << 1, db >> 1);
getchar();
}
无符号的数据或者有符号的正数在进行位运算的时候,需要自动进行转换;低字节向高字节转换的时候,会自动填充0。
void main()
{
char ch = 12;
int num = 122;
printf("%d\n",ch&num);//会自动进行数据类型转换
printf("%d,%d,%d\n",sizeof(ch), sizeof(num), sizeof(ch&num));
getchar();
}
void main18()
{
unsigned char ch = 12;
unsigned int num = 122;
printf("%d\n",ch&num);//会自动进行数据类型转换
printf("%d,%d,%d\n",sizeof(ch), sizeof(num), sizeof(ch&num));
getchar();
}
有符号负数在进行位运算的时候,低字节向高字节转换的时候,会自动填充1,也就是符号位。
void main()
{
char ch = -12;
int num = -3;
printf("%d\n",ch&num);//会自动进行数据类型转换
printf("%d,%d,%d\n",sizeof(ch), sizeof(num), sizeof(ch&num));
getchar();
}
有或无符号数左移,右边都是填充0。有或无符号数左移n位,相当于*2的n次方。
void main()
{
unsigned char ch1 = 1;
char ch2 = -1;
printf("%d\n",ch1 = ch1 << 2);
printf("%d\n",ch2 = ch2 << 1);
getchar();
}
无符号右移n位,左边填充0;有符号数右移n位,左边按照符号位,整数填充0,负数填充1。
void main()
{
unsigned char ch1 = 32;
char ch2 = -2;
printf("%d\n",ch1 = ch1 >> 2);
printf("%d\n",ch2 = ch2 >> 1);
getchar();
}
位反,位与,位或和位异或运算符不关心操作数的符号,只是按照操作数所在字节的0、1序列进行比较,符号位会被当成普通的0或1进行处理
void main()
{
unsigned char ch1 =4;//0000 0100
char ch2 = -3;// 1000 00111111 1100 1111 1101
char chor = ch1 |ch2;//1111 1101 -3
char chand = ch1 &ch2;//0000 0100 4
char chno = ~ch2;//00000010 2
char ch = ch1 ^ ch2;//11111001 1000 0111 -7
printf("%d,%d,%d,%d\n",chor, chand, chno, ch);
system("pause");
}
9. 位字段
位字段是c语言中一种存储结构,不同于一般结构体的是它在定义成员的时候需要指定成员所占的位数。为了在嵌入式物联网设备等开发中节约内存,而提供的存储结构。
void main()
{
printf("%d\n",sizeof(struct date));
printf("%d\n",sizeof(struct datewei));//在字节对齐下,cpu寻址的最小单位是1个字节,结果是4个字节。
struct datewei d1, *pd;
d1.day = 12;
d1.month = 3;
d1.year = 2015;
pd = (struct datewei*)malloc(sizeof(struct datewei));
pd->day = 14;
pd->month = 4;
pd->year = 2016;
printf("%d,%d,%d\n",d1.day, d1.month, d1.year);
printf("%d,%d,%d\n",pd->day, pd->month, pd->year);
getchar();
}
注意,在使用位字段定义结构体后,在初始化结构体变量的成员时注意不要越界,越界会溢出。
struct data
{
//如果两个字节相加小于8位,会填充一个字节。
unsigned char ch1 : 1;//定义一个字符为1位 1000
unsigned char ch2 : 5;//定义一个字符为2位 1110
//unsigned int num : 3;
//unsigned char ch3:10;//位字段成员不可以超过存储单元的长度
//unsigned char ch4 : 0;//位的宽度必须大于0
};
void main()
{
printf("%d\n",sizeof(struct data));//内存对齐下,占1个字节
struct data d1;
d1.ch1 = 2;
printf("%d\n",d1.ch1);
//printf("%p",data.ch1);//不能直接取没有初始化的结构体成员的地址
printf("%p\n",&d1);
//printf("%p\n",&d1.ch1);//不可以取位域的地址
getchar();
}
10. 打印原码反码补码
原码
void main()
{
int num;
scanf("%d",&num);
if (num < 0)
{
num = ~num + 1;
num |=2147483648;
}
int data = 1 << 31;
//循环解决
for (int i = 1; i <=32; i++)
{
printf("%c",(num & data ? '1' : '0'));
num <<= 1;
if (i % 4 == 0)
{
printf("");
}
}
system("pause");
}
反码
void main()
{
int num;
scanf("%d",&num);
num = ~num;
int data = 1 << 31;
//循环解决
for (int i = 1; i <=32; i++)
{
printf("%c",(num & data ? '1' : '0'));
num <<= 1;
if (i % 4 == 0)
{
printf("");
}
}
system("pause");
}
补码
//递归解决
void buma(int num,int size)
{
int data = 1 << 31;
if (size > 0)
{
printf("%c",(num&data ? '1' : '0'));
if (size % 4 ==1)
{
printf("");
}
num <<= 1;
size--;
returnbuma(num,size);
}
else
{
return NULL;
}
}
void main()
{
int num;
scanf("%d",&num);
buma(num,sizeof(int)*8);
system("pause");
}
void main26()
{
int num;
scanf("%d",&num);
int data = 1<<31;
//循环解决
for (int i = 1; i <=32; i++)
{
printf("%c",(num & data ? '1' : '0'));
num <<= 1;
if (i % 4 == 0)
{
printf("");
}
}
system("pause");
}
利用结构体取出补码,在计算机中低位在前,高位在后
//定义结构体存储一个字节的八位
struct bits
{
unsigned char bit1 : 1;
unsigned char bit2 : 1;
unsigned char bit3 : 1;
unsigned char bit4 : 1;
unsigned char bit5 : 1;
unsigned char bit6 : 1;
unsigned char bit7 : 1;
unsigned char bit8 : 1;
};
void main()
{
struct bits *pbit;
pbit = (struct bits*)malloc(sizeof(struct bits) * 4);
int *pint;
pint = (int *)pbit;//共享内存
*pint = 0;
scanf("%d",pint);//初始化内存
for (int i = 3; i >= 0;i--)
{
printf("%d%d%d%d%d%d%d%d ",
(pbit +i)->bit8, (pbit + i)->bit7,
(pbit +i)->bit6, (pbit + i)->bit5,
(pbit +i)->bit4, (pbit + i)->bit3,
(pbit +i)->bit2, (pbit + i)->bit1
);
}
system("pause");
}
11. 位运算输出浮点二进制数据
使用位移与&1取出每一位上的数据。
void main()
{
float f1;
scanf("%f",&f1);
printf("%x\n",&f1);
unsigned char *p =(unsigned char *)&f1;
for (int i = 3; i >= 0;i--)//高字节在前,低字节在后,逆向输出
{
unsigned charchs = p[i];//根据字节取出数据,按照unsigned char解析
for (int j = 7;j >= 0; j--)
{
if(chs&(1 << j))
{
printf("1");
}
else
{
printf("0");
}
}
printf("");
}
system("pause");
}
二十、 文件
外部介质是针对内存来说的,比如硬盘、光盘、键盘、显示器和打印机等。计算机操作外部设备,包括驱动程序,都是读写文件的模式交换信息的。
1. 文件的定义和分类
文件是存储在外部介质上数据的集合,是操作系统数据管理的单位。
文件分类
按文件的逻辑结构:记录文件,有具有一定结构的记录组成(定长和不定长);流式文件,有一个字符(字节)数据顺序组成。
按存储介质:普通文件,存储介质文件(磁盘、磁带、光盘等);设备文件,非存储介质(键盘、显示器、打印机等)。
按数据的组织形式:文本文件,ascii文件,每个字节存放一个字符的ascii码;二进制文件,数据按其在内存中的存储形式原样存放。
使用数据文件的目的,数据文件的改动不引起程序的改动,程序与数据分离;不同程序可以访问同一数据文件中的数据,数据共享;能保存程序运行的中间数据与结果数据。
2. 文本文件与二进制文件的区别
文本文件,写入的时候文本会将换行符10\n,ascii码解析为回车符13\r,换行符10\n读取的时候,会将回车符\r13,换行符\n10解析为换行符\n。
二进制则原样写入写出。
void readfiler(char *path)
{
FILE *pf = fopen(path,"r");//二进制方式读取
char ch;
fread(&ch, 1, 1,pf);//读取一个元素
while (!feof(pf))
{
printf("%d", ch);
fread(&ch,1, 1, pf);
}
fclose(pf);
}
void readfilerb(char *path)
{
FILE *pf = fopen(path,"rb");//二进制方式读取
char ch;
fread(&ch, 1,1,pf);//读取一个元素
while (!feof(pf))
{
printf("%d", ch);
fread(&ch,1, 1, pf);
}
fclose(pf);
}
void main()
{
char buf[5] = { 10, 10,10, 10, 10 };
FILE *pfa;
FILE *pfb;
char patha[40] ="e:\\a.c";
char pathb[40] ="e:\\b.c";
pfa = fopen(patha,"w");//按照文本写入的方式打开文件
printf("%x",pfa);
if (pfa == NULL)
{
printf("文本文件打开失败");
}
else
{
fwrite(buf, 1,5, pfa);//写入数组,传入数据首地址,元素大小为1,5个元素
fclose(pfa);
}
pfb= fopen(pathb,"wb");//按照文本写入的方式打开文件
if (pfb == NULL)
{
printf("二进制文件打开失败");
}
else
{
fwrite(buf, 1,5, pfb);//写入数组,传入数据首地址,元素大小为1,5个元素
fclose(pfb);
}
printf("\n");
printf("文本写入以二进制读取\n");
readfilerb(patha);
printf("\n");
printf("二进制写入以二进制读取\n");
readfilerb(pathb);
printf("\n");
printf("文本写入以文本读取\n");
readfiler(patha);
printf("\n");
printf("二进制写入以文本读取\n");
readfiler(pathb);
system("pause");
}
在linux系统中,文本文件与二进制文件读写无差别。
3. 文件的标识
每个文件都以文件名为标识,I/O设备的文件名是系统定义的,比如,COM1或AUX,第一串行口,附加设备;COM2,第二串行口;CON,控制台,键盘(输入用)或显示器(输出用);LPT1或PRN,第一并行口或打印机;LPT2,第二并行口;NUL,空设备。
磁盘文件可以有用户自己命名,但上述系统保留的设备名不能用作文件名,如不能把一个文件命名为CON(不带扩展名)或CON.TXT(不带扩展名)。
4. 流
流是一个动态概念,可以将一个字节形象地比喻成一滴水,字节在设备、文件和程序之间的传输就是流,类似于水在管道中的传输,可以看出,流是对输入输出源的一种抽象,也是对传输信息的一种抽象。通过对输入和输出源的抽象,屏蔽了设备之间的差异,使程序员能以一种通用的方式进行存储操作,通过对传输信息的抽象,使得所有信息都转化为字节流的形式传输,信息解读的过程与传输过程分离。
c语言中,I/O操作可以简单地看作是从程序移进或移出字节,这种搬运的过程便称为流(stream)。程序只需关心是否正确地输出了字节数据,以及是否正确地输入了要读取字节数据,特定I/O设备的细节对程序员是隐蔽的。
5. 文件处理方法
缓冲文件系统,高级文件系统,系统自动改为正在使用的文件开辟内存缓冲区。
非缓冲文件系统,低级文件系统,用用户在程序中为每个文件设定缓冲区。、
为了使缓冲区中的内容写入文件立即生效,使用fflush。
void main()
{
FILE *pf;
pf =fopen("e:\\1.txt", "w");//按照写入方式打开,自动创建一个文件
if (pf == NULL)
{
printf("文件开发失败");
}
else
{
printf("文件打开成功");
fputs("hhhksl兰陵缭乱\n", pf);
fputs("llls嗖嗖嗖\n", pf);
fflush(pf);//强制将缓冲区的内容写入文件
Sleep(10000);
fclose(pf);
}
}
6. 重定向
重定向是由操作系统完成的,一般来说,标准的输出和输入设备通常指的是显示器和键盘,在支持重定向的操作系统中,标准输入输出能被替换,以dos系统为例。比如,
创建代码
#include<stdio.h>
void main()
{
printf(“测试重定向”);
getchar();
}
编译为ceshi.ext
在dos命令行,ceshi.exe < input.txt表示文件输入;ceshi.exe > output.txt表示输出。
例子2
void main()
{
char str[100] = { 0 };
scanf("%s",str);
printf("str=%s",str);
system(str);
system("pause");
}
使用文本输入,cmd下进入编译的文件的目录,1.exe < 1.txt。将执行的结果输出到文本,1.exe < 1.txt > t.txt。
7. 文件扫描
使用fscanf和fprintf。fprintf将第一个参数指向的文件,按照第二个参数格式化后的内容,写入到这个指针指向的文件。fscanf,从第一个指针指向的文件,按照第二个参数格式化扫描到变量中。
fprintf,不需要写字符串的转换,拼接;fscanf,不需要写字符串切割,还有字符串转换。
void main()
{
char *path ="e:\\2.txt";
char cmd[100] = { 0 };
int num = 0;
char docmd[30] = { 0 };
scanf("%d%s",&num, docmd);
FILE *pf;
pf = fopen(path,"w");
if (pf == NULL)
{
printf("文件打开失败");
}
else
{
fprintf(pf,"for /l %%i in (1,1,%d) do %s", num, docmd);
fclose(pf);
}
system("pause");
}
fsanf会把空格、制表符当作回车符
void main()
{
char cmd[100] = { 0 };
int num = 0;
char docmd[30] = { 0 };
char *path ="e:\\2.txt";
FILE *pf = fopen(path,"r");//文本方式读取文件
if (pf == NULL)
{
printf("打开失败");
return;
}
else
{
fscanf(pf,"for /l %%i in (1,1,%d) do %s", &num, docmd);//读取字符串
printf("num=%d,docmd=%s",num, docmd);
}
system("pause");
}
fscanf需要回车作为间隔来读取数据,尤其是字符串必须回车
void main()
{
FILE *fp =fopen("e:\\fprintf.txt", "r");
if (fp == NULL)
{
printf("打开失败");
}
else
{
char t[100];
int num = 0;
while(!feof(fp))
{
fscanf(fp,"%s\n%d\n", t, &num);
printf("%s%d\n",t, num);
}
fclose(fp);
}
system("pause");
}
8. 文件类型指针
指针变量格式,FILE *fp;
用法,系统自动建立文件结构体,并把指向它的指针返回来,程序通过这个指针获得文件信息,访问文件。文件关闭后,它的文件结构体被释放。
void main()
{
FILE *pf = stdout;
fputs("hahaHh得到", pf);
system("pause");
}
9. 文件型结构体
使用printf函数时,输出设备默认为标准输出设备,一般是显示器,因此不需要告诉printf函数显示器在哪里。
但是如果想从文件中读取输入,就不同了,系统中有不同的磁盘,每个磁盘又有很多文件,应该从哪里读,要想对文件进行操作,系统需要很多控制信息,包括文件名,文件当前读写位置,缓冲区位置和大小等,为此,c语言提供了文件型结构来标识记录操作文件的信息,要引入stdio.h。形式为:
struct _iobuf
{
char *_ptr;//当前缓冲区内容指针
int _cnt;//缓冲区还有多少个字符
char *_base;//缓冲区的起始地址
int _flag;//文件流的状态,是否错误或者结束
int _file;//文件描述符
int _charbuf;//双字节缓冲,缓冲2个字节
int _bufsiz;//缓冲区大小
char *_tmpfname;//临时文件名
};
typedef struct _iobuf FILE;
void main()
{
char ch;
printf("缓冲区数据是%d\n",stdin->_cnt);
printf("指向缓冲区的指针%p\n",stdin->_ptr);
printf("缓冲区的起始地址%p\n",stdin->_base);
printf("缓冲区的大小%d\n",stdin->_bufsiz);
printf("文件标识符%d\n",stdin->_file);
ch = getchar();
printf("当前获取的字符%c\n",ch);
printf("缓冲区数据是%d\n",stdin->_cnt);
printf("指向缓冲区的指针%p\n",stdin->_ptr);
printf("缓冲区的起始地址%p\n",stdin->_base);
printf("缓冲区的大小%d\n",stdin->_bufsiz);
ch = getchar();
printf("当前获取的字符%c\n",ch);
printf("缓冲区数据是%d\n",stdin->_cnt);
printf("指向缓冲区的指针%p\n",stdin->_ptr);
printf("缓冲区的起始地址%p\n",stdin->_base);
printf("缓冲区的大小%d\n",stdin->_bufsiz);
ch = getchar();
printf("当前获取的字符%c\n",ch);
printf("缓冲区数据是%d\n",stdin->_cnt);
printf("指向缓冲区的指针%p\n",stdin->_ptr);
printf("缓冲区的起始地址%p\n",stdin->_base);
printf("缓冲区的大小%d\n",stdin->_bufsiz);
//fflush(stdin);//对键盘缓冲区刷新缓冲区
//rewind(stdin);//文件回到开头,有效数据清零,指针回到起始地址
stdin->_cnt = 0;//有效数清零
stdin->_ptr =stdin->_base;//,指针指向基地址,实现一样的效果
printf("当前获取的字符%c\n",ch);
printf("缓冲区数据是%d\n",stdin->_cnt);
printf("指向缓冲区的指针%p\n",stdin->_ptr);
printf("缓冲区的起始地址%p\n",stdin->_base);
printf("缓冲区的大小%d\n",stdin->_bufsiz);
system("pause");
}
10. 文件操作步骤
c语言程序在进行文件操作时遵循如下操作步骤,打开->读写操作->关闭。打开是获取文件结构、系统为文件分配缓冲区的过程,不打开就不能对其进行读写,关闭是释放缓冲区和其他资源的过程,不关闭文件就会慢慢耗光系统资源。
在进行文件操作时,系统自动与3个标准设备文件联系,c语言将所有设备当作文件处理的,这3个文件无需打开和关闭,它们的文件指针分别是:stdin,标准输入文件指针,系统分配为键盘;stdout,标准输出文件指针,系统分配为显示器;stderr,标准错误输出文件指针,系统分配为显示器。例如,从文件输入和向文件输出有两个对应函数fscanf和fsprintf,两个函数的原型分别为,int fpirntf(FILE *ofp,控制字符串,参数表);int fscanf(FILE *ifp,控制字符串,参数表);
参数表中参数的个数是任意的,fprintf函数用于将转换后的控制字符串写出到ofp指向的文件中,fscanf用于从ifp指向的文件中读取字节信息为参数表中的参数赋值。
标准输入输出函数printf和scanf实际上等价于,fprintf(stdout,控制字符串,参数表);fscanf(stdio,控制字符串,参数表);。类似的,puts和gets也等价于fputs(字符串,stdout);fgets(。putchar和getchar也等价于fputc(字符,stdout);
void main()
{
int num;
printf("hahah\n");
fprintf(stdout,"hahah\n");
printf("scanf输入\n");
scanf("%d",&num);
printf("%d\n",num);
printf("fscanf输入\n");
fscanf(stdin,"%d", &num);
printf("%d\n",num);
puts("putsputs");
fputs("fputsfputs\n",stdout);
char str[50];
printf("gets输入\n");
//getchar();
//gets(str);
//printf("%s\n",str);
printf("fgets输入\n");
getchar();
fgets(str, sizeof(str)-1,stdin);
fputs(str, stdout);
putchar('a');
fputc('a', stdout);
char ss = getchar();
fputc(ss, stdout);
getchar();
ss = fgetc(stdin);
fputc(ss, stdout);
system("pause");
}
错误文件处理
void main()
{
//遇到错误,将错误信息写入stderr,会自动在显示器上输出
//stderrr始终在显示器显示,而stdout如果重定向会被写入磁盘
fprintf(stderr, "错误是%s,可以重试%d次\n", "密码输入错误", 3);
//在cmd中将本程序重定向到文件,ceshi.exe>error.log
fprintf(stdout, "错误是%s,可以重试%d次\n", "密码输入错误", 3);
system("pause");
}
11. 非标准文件函数
putw和getw,可以处理宽字符的输入输出函数。
void main()
{
int w = _getw(stdin);//从键盘获取4个字节
_putw(w, stdout);//显示器输入
//可以输出两个汉字,一个汉字两个字节
//输入1,2,两字字符加两个回车符,回车会被当作一个字符
system("pause");
}
void main()
{
putchar(97);
putchar('1');
putchar('\n');
int num = _putw(97,stdout);//可以输出一个字符,但是后续的会当作空字符处理
//putw的返回值,如果成功则返回输入的数,否则返回-1。
printf("\nnum=%d",num);
system("pause");
}
12. fopen
文件打开函数。
fopen文件打开模式。
r代表read简写,+代表可读可写,w代表write,b代表bit二进制位,t代表text。
r打开只读文件,该文件必须存在。
w打开只写文件,若文件存在则文件长度清零,即该文件内存会擦除;若文件不存在则建立该文件。
r+打开可读写的文件,该文件必须存在。
w+打开可读写文件,若文件存在则文件长度清零,即该文件内存会擦除;若文件不存在则建立该文件。
rb只读打开一个二进制文件,只允许读写数据。该文件必须存在。
wb只写打开或新建一个二进制文件,只允许写数据。
rb+读写打开一个二进制文件,只允许读写数据。该文件必须存在。
wb+读写打开或建立一个二进制文件。允许读和写。
rt打开只读文本文件,该文本文件必须存在。
wt打开只写文本文件,若文本文件存在则文件清零,即该文件内存会擦除;若文件不存在则建立该文件。
rt+读写打开一个文本文件,允许读写。该文件必须存在。
wt+读写打开或建立一个文本文件。允许读和写。
a以附加的方式打开只写文件。若文件不存在,则会建立该文件;如果文件存在,写入的数据会被追加到文件尾,即文件原先的内容不会清零。
a+以附加方式打开可读写的文件。若文件不存在,则会建立该文件;如果文件存在,写入的数据会被追加到文件尾,即文件原先的内容不会清零。
ab二进制数据的追加,不存在则创建。
at文本文件的的追加,不存在则创建。
at+读写打开一个文本文件,允许读或在文本末追加数据。
ab+读写打开一个二进制文件,允许读或在文本末追加数据。
代码示例
r打开只读文件,该文件必须存在。
void main()
{
char path[40] ="e:\\r.txt";
FILE *pf = fopen(path,"r");
if (pf == NULL)
{
printf("打开失败");
}
else
{
printf("打开成功");
while(!feof(pf))
{
char ch= fgetc(pf);
putchar(ch);
}
int res =fputc('a', pf);//写入不会报错,但是写入会失败
printf("%d\n",res);
if (res == -1)
{
printf("写入失败\n");
}
else
{
printf("写入成功\n");
}
fclose(pf);
}
system("pause");
}
w打开只写文件,若文件存在则文件长度清零,即该文件内存会擦除;若文件不存在则建立该文件。只可以写不可以读。
void main()
{
char path[40] ="e:\\w.txt";
FILE *pf = fopen(path,"w");
if (pf == NULL)
{
printf("失败");
}
else
{
printf("成功\n");
fputs("haKk看看", pf);
rewind(pf);//文件指针移到开头
char ch =fgetc(pf);//从文件读取一个字符,读取失败,返回-1
printf("%d\n",ch);
fclose(pf);
}
system("pause");
}
r+打开可读写的文件,该文件必须存在。
void main()
{
char path[40] ="e:\\w.txt";
FILE *pf = fopen(path,"r+");
if (pf == NULL)
{
printf("失败");
}
else
{
printf("成功\n");
fputs("haKk看看", pf);
rewind(pf);//文件指针移到开头
char ch =fgetc(pf);
putchar(ch);
fclose(pf);
}
system("pause");
}
w+打开可读写文件,若文件存在则文件长度清零,即该文件内存会擦除;若文件不存在则建立该文件。
void main()
{
char path[40] ="e:\\w+.txt";
FILE *pf = fopen(path,"w+");
if (pf == NULL)
{
printf("失败");
}
else
{
printf("成功\n");
fputs("haKk看看", pf);
rewind(pf);//文件指针移到开头
char ch = fgetc(pf);
putchar(ch);
fclose(pf);
}
system("pause");
}
rb只读打开一个二进制文件,只允许读写数据。该文件必须存在。
void main()
{
char path[40] ="e:\\wb.txt";
FILE *pf = fopen(path,"rb");
if (pf == NULL)
{
printf("失败");
}
else
{
printf("成功\n");
float flx[5] = {0.0 };
int res =fread(flx, 4, 5, pf);//将文件读入内存
printf("%d",res);
if (res != 5)
{
printf("读入失败\n");
}
else
{
printf("读入成功\n");
}
for (int i = 0;i < 5; i++)
{
printf("%f\n",flx[i]);
}
float f1[5] = {1.2, 2.3, 3.2, 4.4, 5.5 };
int res1 = fwrite(f1, 4, 5, pf);
printf("%d\n",res1);
fclose(pf);
}
system("pause");
}
wb只写打开或新建一个二进制文件,只允许写数据。
void main()
{
char path[40] ="e:\\wb.txt";
FILE *pf = fopen(path,"wb");
if (pf == NULL)
{
printf("失败");
}
else
{
printf("成功\n");
float f1[5] = {1.2, 2.3, 5.9, 4.3, 7.3 };
fwrite(f1, 4, 5,pf);//将数组这片内存写入文件
rewind(pf);
float flx[5] = {0.0 };
int res =fread(flx, 4, 5, pf);//将文件读入内存
printf("%d",res);
if (res != 5)
{
printf("读入失败");
}
else
{
printf("读入成功");
}
fclose(pf);
}
system("pause");
}
rb+读写打开一个二进制文件,只允许读写数据。该文件必须存在。
void main()
{
char path[40] ="e:\\wb.txt";
FILE *pf = fopen(path,"rb+");
if (pf == NULL)
{
printf("失败");
}
else
{
printf("成功\n");
float flx[5] = {0.0 };
int res =fread(flx, 4, 5, pf);//将文件读入内存
printf("%d",res);
if (res != 5)
{
printf("读入失败\n");
}
else
{
printf("读入成功\n");
}
for (int i = 0;i < 5; i++)
{
printf("%f\n",flx[i]);
}
rewind(pf);//移动文件到开头
float f1[5] = {1.2, 2.3, 3.2, 4.4, 5.5 };
int res1 =fwrite(f1, 4, 5, pf);
printf("%d\n",res1);
fclose(pf);
}
system("pause");
}
wb+读写打开或建立一个二进制文件。允许读和写。若文本文件存在则文件清零,即该文件内存会擦除;若文件不存在则建立该文件。
void main()
{
char path[40] ="e:\\wb+.txt";
FILE *pf = fopen(path,"wb+");
if (pf == NULL)
{
printf("失败");
}
else
{
printf("成功\n");
float f1[5] = {1.2, 2.3, 3.2, 4.4, 5.5 };
int res1 =fwrite(f1, 4, 5, pf);
printf("%d\n",res1);
float flx[5] = {0.0 };
rewind(pf);//移动文件到开头
int res =fread(flx, 4, 5, pf);//将文件读入内存
printf("%d",res);
if (res != 5)
{
printf("读入失败\n");
}
else
{
printf("读入成功\n");
}
for (int i = 0;i < 5; i++)
{
printf("%f\n",flx[i]);
}
fclose(pf);
}
system("pause");
}
rt打开只读文本文件,该文本文件必须存在。
wt打开只写文本文件,若文本文件存在则文件清零,即该文件内存会擦除;若文件不存在则建立该文件。
rt+读写打开一个文本文件,允许读写。该文件必须存在。
wt+读写打开或建立一个文本文件。允许读和写。
rt,wt,rt+,wt+类似r的情况。
a以附加的方式打开只写文件。若文件不存在,则会建立该文件;如果文件存在,写入的数据会被追加到文件尾,即文件原先的内容不会清零。
void main()
{
char path[40] ="e:\\a.txt";
FILE *pf = fopen(path,"a");
if (pf == NULL)
{
printf("失败");
}
else
{
printf("成功\n");
fputs("hshsha嗖嗖嗖\n", pf);
rewind(pf);
char ch =fgetc(pf);
printf("%d\n",ch);
fclose(pf);
}
system("pause");
}
a+以附加方式打开可读写的文件。若文件不存在,则会建立该文件;如果文件存在,写入的数据会被追加到文件尾,即文件原先的内容不会清零。
void main()
{
char path[40] ="e:\\a+.txt";
FILE *pf = fopen(path,"a+");
if (pf == NULL)
{
printf("失败");
}
else
{
printf("成功\n");
fputs("hshsha嗖嗖嗖\n", pf);
rewind(pf);
while(!feof(pf))
{
char ch= fgetc(pf);
printf("%c",ch);
}
fclose(pf);
}
system("pause");
}
at文本文件的的追加,不存在则创建。
at+读写打开一个文本文件,允许读或在文本末追加数据。
ab二进制数据的追加,不存在则创建。
ab+读写打开一个二进制文件,允许读或在文本末追加数据。
t和b与a类似。
13. fclose
关闭文件,作用是是文件指针变量与文件脱钩,释放文件结构体和文件指针。正常关闭,返回0,出错返回非0。
向磁盘输出数据时,数据会先存储在缓冲区,在没有刷新缓冲或关闭文件的情况下,等到缓冲区满了,才会将缓冲区中要写入磁盘的数据一次性写入磁盘,缓冲区是在内存中,如果没有即使将缓冲区的数据写入磁盘,那么,一旦发生以外断电,内存中的数据丢失也就是缓冲区的数据也丢失,将导致数据不能写入磁盘,而丢失数据。所以,为了保证数据安全,也需要即使关闭文件。
void main()
{
FILE *pf;
char path[40] ="e:\\close.txt";
pf = fopen(path,"w");//按照写入模式打开文件
if (pf == NULL)
{
printf("文件打开失败");
}
else
{
char ch = getchar();//获取字符输入
while (ch !=EOF)//end of file
{
fputc(ch,pf);//写入一个字符到文件
ch =getchar();
}
//fclose(pf);
while (1)
{
}
}
system("pause");
}
测试没有fclose,结束程序进程,发现没有写入数据到文件。
14. access函数
access函数用于判断文件或文件夹是否存在以及读写属性。
windows下,在io.h中
0判断是否存在
关于文件夹,windows下所有文件夹都是可读可写的。
2判断是否可写
4判断是否可读
6判断是否可读可写
是返回0,否返回-1。
linux下,在unistd.h中
R_OK读权限
W_OK写权限
X_OK读写权限
F_OK文件是否存在
windows下的例子
void main()
{
//判断文件或文件夹是否存在
int isexist =_access("e:\\1vv", 0);
if (isexist)
{
printf("不存在\n");
}
else
{
printf("文件夹存在\n");
}
//判断文件可写或不可写
int iswrite =_access("e:\\w.txt", 2);
if (iswrite)
{
printf("不可写\n");
}
else
{
printf("可写\n");
}
system("pause");
}
15. 按照内存块的方式读写文件
内存块的方式,也就是诸如数组、结构体数组等。使用函数fread和fwrite。
数组的写入
void main()
{
int num[100];//数组是一段连续的内存
for (int i = 0; i <100; i++)
{
num[i] = i;
}
FILE *pf;
pf =fopen("e:\\shuzu.txt", "wb");//二进制写入的方式打开这个文件
if (pf == NULL)
{
printf("文件打开失败");
}
else
{
int res = 0;
res =fwrite(num, sizeof(int), 100, pf);//将内存写入文件
//第1个参数,要写入内存的首地址
//第2个参数,写入的元素的大小
//第3个参数,写入的个数
//第4个参数,写入哪个文件的指针
if (res == 100)
{
printf("写入成功");
}
else
{
printf("写入失败");
}
fclose(pf);
}
system("pause");
}
数组的读取
void main()
{
int num[100];
FILE *pf;
pf =fopen("e:\\shuzu.txt", "rb");
if (pf == NULL)
{
printf("打开失败");
}
else
{
int res = 0;
res = fread(num,sizeof(int),100,pf);//res代表度成功多少个就返回数量
//第1个参数,要写入内存的首地址
//第2个参数,写入的元素的大小
//第3个参数,写入的个数
//第4个参数,写入哪个文件
if (res == 100)
{
printf("读取成功\n");
for(int i = 0; i < 100; i++)
{
printf("%-2d", num[i]);
if(i % 5 == 4)
{
printf("\n");
}
}
}
else
{
printf("读取失败");
}
fclose(pf);
}
system("pause");
}
结构体数组的写入
struct text
{
char first[100];
char second[100];
char three[100];
}info[5] = {
{ "hsljd","似懂非懂", "sdfe" },
{ "hsljd","似懂非懂", "sdfe" },
{ "hsljd","似懂非懂", "sdfe" },
{ "hsljd","似懂非懂", "sdfe" },
{ "hsljd","似懂非懂", "sdfe" }
};
void main()
{
FILE *pf =fopen("e:\\text.txt", "wb");
if (pf == NULL)
{
printf("打开失败\n");
}
else
{
int res = 0;
res =fwrite(info, sizeof(struct text), 5, pf);
//第1个参数是写入的内存地址,第2个是元素大小
//第3个是数量,第4个是文件指针
if (res != 5)
{
printf("写入失败");
}
else
{
printf("写入成功");
}
fclose(pf);
}
system("pause");
}
数组结构体的读取
void main()
{
struct text t1[5];
FILE *pf =fopen("e:\\text.txt", "rb");
if (pf == NULL)
{
printf("打开失败\n");
}
else
{
int res = 0;
res = fread(t1,sizeof(struct text), 5, pf);
//第1个参数是写入的内存地址,第2个是元素大小
//第3个是数量,第4个是文件指针
if (res != 5)
{
printf("读取失败");
}
else
{
printf("读取成功\n");
for(int i = 0; i < 5; i++)
{
printf("%s,%s,%s\n",t1[i].first, t1[i].second, t1[i].three);
}
}
fclose(pf);
}
system("pause");
}
16. 编程使用命令行方式读取文件
使用到命令,type 文件全路径
void main()
{
char path[100] ="e:\\text.txt";
char cmd[100];
sprintf(cmd, "type%s", path);
system(cmd);
system("pause");
}
17. 检测错误
使用ferror函数。
int ferror(FILE *fp)
测试文件是否出现错误。未出错,返回0;出错,返回非0。
每次调用文件输入输出函数,均产生一个新的ferror函数值,所以应及时测试。fopen打开文件时,ferror函数初值自动置为0。
void main()
{
FILE *fp;
fp =fopen("e:\\out.txt", "r");
if (fp == NULL)
{
printf("打开失败");
}
else
{
printf("打开成功");
fputs("sdfd斯蒂芬", fp);
if (ferror(fp)== 0)
{
printf("文件正常");
}
else
{
printf("文件出错");
}
fclose(fp);
}
system("pause");
}
18. 获取文件错误信息
使用perror函数。
void main()
{
char path[80] ="e:\\out.txt";
FILE *fp = fopen(path,"w");
if (fp == NULL)
{
printf("打开失败\n");
perror("输出的错误是");
return;
}
else
{
printf("打开成功");
fclose(fp);
}
system("pause");
}
19. 文件定位
文件的读写方式有两种,一是顺序读写,位置指针按字节顺序从头到尾移动,另一种是随机读写,位置指针按需要移动到任意位置,随机形式多用于二进制文件的读写。
如果要对文件进行随机读写,就需要控制文件位置指针的值,这就是文件定位,与文件定位有关的函数是rewind,fseek和ftell。
rewind(FILE *fp);文件指针回到开头
void main()
{
FILE *fp;
fp =fopen("e:\\fprintf.txt", "r");
if (fp == NULL)
{
system("打开失败");
}
else
{
printf("打开成功\n");
while(!feof(fp))
{
char ch= fgetc(fp);
putchar(ch);
}
//字符串方式读取
printf("字符串读取\n");
char str[100] ={ 0 };
rewind(fp);//将文件指针移动都文件开头
while(fgets(str, 100, fp) != NULL)
{
printf("%s",str);
}
fclose(fp);
}
system("pause");
}
long ftell(FILE *);
执行成功,返回当前文件指针到文件头有多少个字节,否则,返回-1。
void main()
{
FILE *fp;
fp =fopen("e:\\fprintf.txt", "r");
if (fp == NULL)
{
system("打开失败");
}
else
{
printf("打开成功\n");
int i = 0;
int size = 0;
while (!feof(fp))
{
char ch= fgetc(fp);
//使用ftell实现查找,获取当前字符距离文件头有多长
if (ch== '0')
{
i++;
size= ftell(fp);
printf("\n第%d次出现0距离文件头的位置%d\n", i,size);
}
putchar(ch);
}
size =ftell(fp);//到了文件末尾,可以获取文件大小
printf("大小有%d字节\n", size);
//字符串方式读取
printf("字符串读取\n");
char str[100] ={ 0 };
rewind(fp);//将文件指针移动都文件开头
while(fgets(str, 100, fp) != NULL)
{
printf("%s",str);
}
fclose(fp);
}
system("pause");
}
移动指针,fseek。
文件定位中最重要的函数是fseek,用以控制、调整文件指针的值,从而改变下一次读写操作的位置,函数原型为int fseek(FILE *fp, long offset, int startPos);
fp是文件指针,startPos是起始点,offset是目标位置相对起始点的偏移量,可以为负数。如果函数操作成功,文件位置设定为起始点+offset,起始点并不是任意设定的,c语言提供了3种起始点方式,
起始点 | 值 | 名称 |
文件开始 | 0 | SEEK_SET |
文件末尾 | 2 | SEEK_END |
当前位置 | 1 | SEEK_CUR |
在文件末尾读写
void main()
{
FILE *fp;
fp =fopen("e:\\fprintf.txt", "r+");
if (fp == NULL)
{
printf("打开失败");
}
else
{
printf("打开成功\n");
fseek(fp, 0,SEEK_END);//移动指针到末尾,或者这样fseek(fp, 0, 2);
fputs("\n//哈里斯的使得", fp);
fclose(fp);
}
system("pause");
}
定位到文件末尾,统计文件大小
void main()
{
FILE *fp;
fp =fopen("H:\\centosISO\\CentOS-7-x86_64-Minimal-1708.iso","rb");
if (fp == NULL)
{
printf("打开失败");
}
else
{
printf("打开成功\n");
fseek(fp, 0, 2);
int size =ftell(fp);
if (size != -1)
{
printf("文件大小为%d\n",size);
}
else
{
printf("统计文件大小出错");
}
fclose(fp);
}
system("pause");
}
向前或向后移动文件指针
void main()
{
FILE *fp;
fp =fopen("e:\\fprintf.txt", "r+");
if (fp == NULL)
{
printf("打开失败");
}
else
{
printf("打开成功\n");
fseek(fp, 0,SEEK_END);//移动指针到末尾,或者这样fseek(fp, 0, 2);
fputs("哈里的使得", fp);
fseek(fp, -10,2);//向前移动指针16个字节
fputs("sljdl两节课", fp);
fclose(fp);
}
system("pause");
}
读取后10个字符
void main()
{
FILE *fp;
fp =fopen("e:\\fprintf.txt", "r");
if (fp == NULL)
{
printf("打开失败");
}
else
{
printf("打开成功\n");
fseek(fp, -10,2);//移动文件指针到末尾前10个字符的位置
char ch =fgetc(fp);
while(!feof(fp))
{
putchar(ch);
ch =fgetc(fp);
}
fclose(fp);
}
system("pause");
}
从中间位置读取
void main()
{
FILE *fp;
fp =fopen("e:\\fprintf.txt", "r");
if (fp == NULL)
{
printf("打开失败");
}
else
{
printf("打开成功\n");
fseek(fp, 0, 2);
int size =ftell(fp);
if (size != -1)
{
printf("%d\n",size);
fseek(fp,-(size / 2), 2);//移动文件指针到末尾前10个字符的位置
char ch= fgetc(fp);
while(!feof(fp))
{
putchar(ch);
ch= fgetc(fp);
}
}
fclose(fp);
}
system("pause");
}
修改指定位置的字符
void main()
{
FILE *fp;
fp =fopen("e:\\fprintf.txt", "rt+");
if (fp == NULL)
{
printf("打开失败");
}
else
{
printf("打开成功\n");
fseek(fp, 0, 0);
for (int i = 0;i < 8; i++)
{
fputc(i+49,fp);
}
fseek(fp, 2, 1);
for (int i = 0;i < 3; i++)
{
fputc(i+ 49, fp);
}
fclose(fp);
}
system("pause");
}
fseek修改二进制文件
void main()
{
FILE *fp;
fp =fopen("e:\\1.ext", "wb+");
if (fp == NULL)
{
printf("打开文件失败");
}
else
{
printf("打开成功\n");
double db[10] ={ 1.2, 3.2, 4.2, 5.5, 6.3, 7.0, 1.3, 3.5, 7.4, 8.4 };
fwrite(db,sizeof(double), 10, fp);
//修改特定的一个元素
fseek(fp, -8,SEEK_END);
double dd1 =1.2;
fwrite(&dd1,sizeof(double), 1, fp);
fseek(fp, -16,SEEK_END);
double dd2 =1.2;
fwrite(&dd2,sizeof(double), 1, fp);
fseek(fp, 0, 0);
double dd3 =2.2;
fwrite(&dd3,sizeof(double), 1, fp);
fseek(fp, 8, 0);
double dd4 =2.2;
fwrite(&dd4,sizeof(double), 1, fp);
double dbr[10] ={ 0 };
//查找数据,进行修改
rewind(fp);
double data =0.0;
for (int i = 0;i < 10; i++)
{
fread(&data,sizeof(data), 1, fp);//读取一个double类型的数据,存储到data
if(data == 6.3)
{
break;
}
}
//修改找到的数据
//fseek(fp, -8,1);
//fseek(fp, -16,1);//修改前一个字符
//修改下一个字符,直接在这个位置修改
fseek(fp, 0, 1);
double db5 =6.6;
fwrite(&db5,sizeof(db5), 1, fp);
rewind(fp);
fread(dbr,sizeof(double), 10, fp);
for (int i = 0;i < 10; i++)
{
printf("%lf\n",dbr[i]);
}
fclose(fp);
}
system("pause");
}
20. 删除文件
使用remove(文件路径)。成功返回0,失败返回非0。
void main()
{
char *path ="e:\\remove.txt";
int res = remove(path);
if (res == 0)
{
printf("删除成功");
}
else
{
printf("删除失败");
}
getchar();
}
21. 产生唯一目录名
使用mktemp(路径模版),需要引入io.h。注意,路径模版中必须包含六个连续的大写的X。
void main()
{
char path[100] ="E:\\目录1XXXXXXX";//_mktemp修改的目标根据path修改,要传入的目录必须包含6个连续的X
char *newname =_mktemp(path);//传入路径,根据模版生成唯一的目录名
printf("%s,%s",newname, path);
//使用cmd创建一个目录
char cmd[100];
sprintf(cmd, "md%s", path);//初始化字符串,用于指令创建目录
system(cmd);
system("pause");
}
22. 统计英文文档文件内容
void main()
{
char *path ="e:\\tongji.txt";
FILE *fp = fopen(path,"r");
if (fp == NULL)
{
printf("打开失败");
}
else
{
printf("打开成功\n");
char ch;
int numUp = 0;//统计大写字母
int numLow =0;//统计小写字母
int numNum =0;//统计数字
int numN = 0;//统计换行
int numO = 0;//统计其他字符
int numK = 0;//统计空格
while ((ch =fgetc(fp)) != EOF)
{
if (ch>= 'A' && ch <= 'Z')
{
numUp++;
}
else if(ch >= 'a' && ch <= 'z')
{
numLow++;
}
else if(ch == ' ')
{
numK++;
}
else if(ch == '\n')
{
numN++;
}
else if(ch >= '0' && ch <= '9')
{
numNum++;
}
else
{
numO++;
}
}
fclose(fp);
printf("大写字母%d,小写字母%d,空格%d,换行%d,数字%d,其他%d\n", numUp,numLow, numK, numN, numNum, numO);
}
system("pause");
}
23. 搜索文件
在windows下搜索文件的命令
按照文件名搜索,for /r C:\Users\Administrator\Desktop %i in (*.txt) do @echo %i
按照文件名模糊查找,for /r C:\Users\Administrator\Desktop %i in (CMD.*) do @echo %i
将查找结果输出到文件,for /r C:\Users\Administrator\Desktop %i in (*.txt) do @echo %i >> e:\output.txt
查看保存的结果,type e:\output.txt
编程实现
按照文件目录名搜索
void main()
{
//搜索的目录
char path[100] ="e:\\";
//搜索的文件
char filename[30] ="*.txt";
//结果输出的路径
char outputpath[100] ="e:\\output.txt";
char cmd[512];
sprintf(cmd, "for /r\"%s\" %%i in (%s) do @echo %%i >>\"%s\"",path,filename,outputpath);
system(cmd);//执行查找并将结果输入到outputpath这个文件
char show[200];
sprintf(show, "type%s", outputpath);
system(show);
system("pause");
}
按照文件内容搜索
cmd命令,
获得包含内容的文件名,for /r C:\Users\Administrator\Desktop %a in (*) do @findstr /im "1" "%a"
获得包含内容的文件中的位置和原文,for /r C:\Users\Administrator\Desktop %a in (*) do @findstr /in "1" "%a"
将搜索结果写入文件,for /r C:\Users\Administrator\Desktop %a in (*) do @findstr /im "1" "%a" >> e:\\output.txt
编程实现
void main()
{
char path[100] ="e:\\testfile\\";
char str[20] = "似懂非懂";
char outputpath[100] ="e:\\output.txt";
char cmd[500];
//创建一个指令字符串,按照一段文本,在一个目录下检索所有这个文本的文件
sprintf(cmd, "for /r\"%s\" %%a in (*) do @findstr /im \"%s\" \"%%a\">> %s", path, str,outputpath);
system(cmd);
char show[200];
sprintf(show, "type%s", outputpath);
system(show);
system("pause");
}
linux平台下,指定目录搜索,文件名find /etc -name 1.c
搜索文件名中带c的,find / -name '*c* '
从根目录开始查找所有扩展名为.log的文本文件,并找出包含"haha "的行
find / -type f -name "*.log" | xargs grep "haha "或者grep -r "haha" /
24. 统计文本汉字的个数
判断汉字的方法,汉字占两个字节。GBK规定,汉字,日文,韩文第一个字节大于128。
GBK汉字的编码范围,其他范围可能是日文或者韩文的。
第一个字节在81-A0,第二个字节在40-7E或者80-FE
第一个字节在B0-D6,第二个字节在40-7E或者80-FE
第一个字节在D8-F7,第二个字节在40-7E或者80-FE
第一个字节在AA-AF,第二个字节在40-7E或者80-A0
第一个字节在F8-FE,第二个字节在40-7E或者80-A0
第一个字节在D7,第二个字节在40-7E
void main()
{
FILE *fp;
char path[100] ="e:\\testfile\\hanzi.txt";
fp = fopen(path,"r");
if (fp == NULL)
{
printf("打开失败");
}
else
{
printf("打开成功\n");
int ich;//获取字符,因为fgetc的返回值类型是int,不使用int,ascii码没有问题,但是汉字会有问题
int nume = 0;//统计英文字符
int num0 = 0;//统计数字
int numc = 0;//统计汉字
while ((ich =fgetc(fp)) != EOF)
{
if((ich >= 'A'&&ich <= 'Z') || (ich >= 'a'&&ich <= 'z'))
{
nume++;
}
else if(ich >= '0'&&ich <= '9')
{
num0++;
}
else if(ich > 128)//判定双字节字符
{
intiwh = fgetc(fp);
if((ich >= 0x81 && ich <= 0xA0) && ((iwh >= 0x40&& iwh <= 0x71) || (iwh >= 0x80 && iwh <= 0xFE)))
{
numc++;
}
elseif ((ich >= 0xB0 && ich <= 0xD6) && ((iwh >= 0x40&& iwh <= 0x71) || (iwh >= 0x80 && iwh <= 0xFE)))
{
numc++;
}
elseif ((ich >= 0xD8 && ich <= 0xF7) && ((iwh >= 0x40&& iwh <= 0x71) || (iwh >= 0x80 && iwh <= 0xFE)))
{
numc++;
}
elseif ((ich >= 0xAA && ich <= 0xAF) && ((iwh >= 0x40&& iwh <= 0x71) || (iwh >= 0x80 && iwh <= 0xA0)))
{
numc++;
}
elseif ((ich >= 0xF8 && ich <= 0xFE) && ((iwh >= 0x40&& iwh <= 0x71) || (iwh >= 0x80 && iwh <= 0xA0)))
{
numc++;
}
elseif ((ich == 0xD7)&&(iwh >= 0x40 && iwh <= 0x7E))
{
numc++;
}
}
}
fclose(fp);
printf("英文字符%d,数字%d,汉字字符%d\n", nume,num0, numc);
}
system("pause");
}
25. 文件加密解密
使用加减加密
void jia(int pass)
{
FILE *fpr;//读取要加密的文件
FILE *fpw;//写入到要加密的文件
char pathr[100] ="e:\\testfile\\fprintf.txt";
char pathw[100] ="e:\\jia.txt";
fpr = fopen(pathr,"r");//用读的模式打开被加密的文件
fpw = fopen(pathw,"w");//用写的模式打开写入加密的文件
if (fpr == NULL || fpw ==NULL)
{
printf("加密失败");
return;
}
else
{
char ch =fgetc(fpr);//读取文本
while(!feof(fpr))
{
ch = ch+ pass;//字符移位加密
fputc(ch,fpw);//写入到要加密的文件
ch =fgetc(fpr);
}
fclose(fpw);
fclose(fpr);
}
}
void jie(int pass)
{
FILE *fpr;//读取要加密的文件
FILE *fpw;//写入到要加密的文件
char pathr[100] ="e:\\jia.txt";
char pathw[100] ="e:\\jie.txt";
fpr = fopen(pathr,"r");//用读的模式打开被加密的文件
fpw = fopen(pathw,"w");//用写的模式打开写入加密的文件
if (fpr == NULL || fpw ==NULL)
{
printf("解密失败");
return;
}
else
{
char ch =fgetc(fpr);//读取文本
while(!feof(fpr))
{
ch = ch- pass;//字符移位加密
fputc(ch,fpw);//写入到要加密的文件
ch =fgetc(fpr);//读取文本
}
fclose(fpw);
fclose(fpr);
}
}
void main64()
{
//jia(5);
jie(5);
system("pause");
}
使用加减加密可能产生溢出,导致加解密后与原文件有出入。使用异或加密,可以避免溢出。
void zjia(char pass[], int length)
{
FILE *fpr;//读取要加密的文件
FILE *fpw;//写入到要加密的文件
char pathr[100] ="e:\\testfile\\hanzi.txt";
char pathw[100] ="e:\\jia.txt";
fpr = fopen(pathr,"rb");//用读的模式打开被加密的文件
fpw = fopen(pathw,"wb");//用写的模式打开写入加密的文件
if (fpr == NULL || fpw ==NULL)
{
printf("加密失败");
return;
}
else
{
int i = 0;//标识取出加密数组哪一位
char ch =fgetc(fpr);
while(!feof(fpr))
{
//读取文本
if (i> length - 1)
{
i= 0;
}
ch = ch+ pass[i];//字符移位加密
putchar(ch);
i++;
fputc(ch,fpw);//写入到要加密的文件
ch =fgetc(fpr);
}
fclose(fpw);
fclose(fpr);
}
}
void zjie(char pass[], int lenght)
{
FILE *fpr;//读取要加密的文件
FILE *fpw;//写入到要加密的文件
char pathr[100] ="e:\\jia.txt";
char pathw[100] ="e:\\jie.txt";
fpr = fopen(pathr,"rb");//用读的模式打开被加密的文件
fpw = fopen(pathw,"wb");//用写的模式打开写入加密的文件
if (fpr == NULL || fpw ==NULL)
{
printf("解密失败");
return;
}
else
{
int i = 0;
char ch =fgetc(fpr);
while(!feof(fpr))
{
if (i> lenght - 1)
{
i= 0;
}
ch = ch- pass[i];//字符移位加密
putchar(ch);
i++;
fputc(ch,fpw);//写入到要加密的文件
ch =fgetc(fpr);//读取文本
}
fclose(fpw);
fclose(fpr);
}
}
void main66()
{
char password[] ="halksjd";
int n = strlen(password);
//zjia(password, n);
zjie(password, n);
system("pause");
}
按照字符串加解密,有更高的难破解性,使用二进制的方式以更精准的加解密
void zjia(char pass[], int length)
{
FILE *fpr;//读取要加密的文件
FILE *fpw;//写入到要加密的文件
char pathr[100] ="e:\\testfile\\hanzi.txt";
char pathw[100] ="e:\\jia.txt";
fpr = fopen(pathr,"rb");//用读的模式打开被加密的文件
fpw = fopen(pathw,"wb");//用写的模式打开写入加密的文件
if (fpr == NULL || fpw ==NULL)
{
printf("加密失败");
return;
}
else
{
int i = 0;//标识取出加密数组哪一位
char ch =fgetc(fpr);
while(!feof(fpr))
{
//读取文本
if (i> length - 1)
{
i= 0;
}
ch = ch+ pass[i];//字符移位加密
putchar(ch);
i++;
fputc(ch,fpw);//写入到要加密的文件
ch =fgetc(fpr);
}
fclose(fpw);
fclose(fpr);
}
}
void zjie(char pass[], int lenght)
{
FILE *fpr;//读取要加密的文件
FILE *fpw;//写入到要加密的文件
char pathr[100] ="e:\\jia.txt";
char pathw[100] ="e:\\jie.txt";
fpr = fopen(pathr,"rb");//用读的模式打开被加密的文件
fpw = fopen(pathw,"wb");//用写的模式打开写入加密的文件
if (fpr == NULL || fpw ==NULL)
{
printf("解密失败");
return;
}
else
{
int i = 0;
char ch =fgetc(fpr);
while (!feof(fpr))
{
if (i> lenght - 1)
{
i= 0;
}
ch = ch- pass[i];//字符移位加密
putchar(ch);
i++;
fputc(ch,fpw);//写入到要加密的文件
ch =fgetc(fpr);//读取文本
}
fclose(fpw);
fclose(fpr);
}
}
void main()
{
char password[] ="halksjd";
int n = strlen(password);
//zjia(password, n);
zjie(password, n);
system("pause");
}
使用字符串异或的方式更好,可以避免溢出。修改+和-为^就可以。
26. 文件中检索字符串
使用到的cmd命令
FIND [/V] [/C] [/N] [/I] ["string" [[drive:][path]filename[…]]
/V 显示所有未包含指定字符串的行
/C 仅显示包含字符串的行数
/N 显示行号
/I 搜索字符串时忽略大小写
"string" 指定要搜索的字符串
[drive:][path]filename[…] 指定要搜索的文件
比如,find "haha" e:\\test.txt
find /n "haha" e:\\test.txt
编程实现
void main()
{
char str[100];
char dest[30] ="e:\\jie.txt";
char find[30] = "是";
char *res ="e:\\find.txt";
sprintf(str, "find /n\"%s\" \"%s\" >> %s", find, dest, res);
system(str);
printf("%s",str);
system("pause");
}
27. 遍历文件夹下所有文件
cmd命令,dir [/b] [/a] 目录的路径
/b仅显示名字,/a:-d,不显示目录
void main()
{
char cmd[100];
char path[100] ="e:\\";
char res[100] ="e:\\dir.txt";
sprintf(cmd, "dir /b/a:-d \"%s\" >> \"%s\"", path, res);
system(cmd);
system("pause");
}
linux下,ls -l -F| grep -v [/$] -F显示类型,|grep -v [/$,*$]使用管道排除文件夹和可执行文件。
28. 删除目录
cmd命令,rd 目录路径,删除空目录;rd /s 目录路径,删除空或非空目录;rd /s /q 目录路径,沉默删除空或非空目录。
void main()
{
char cmd[100];
char path[100] ="e:\\111";
sprintf(cmd, "rd /s/q \"%s\"", path);
system(cmd);
system("pause");
}
linux下,rm -rf 111。r表示return,递归删除,f表示force,强制不询问。
29. 临时文件
临时文件,比如,下载,安装的时候,用完了就自动删除的文件。tmpfile产生临时文件,关闭文件或程序关闭,具备二进制读写权限,一般在程序所在的磁盘。
void main()
{
FILE *ptemp;
ptemp = tmpfile();//创建临时指针,返回文件指针
if (ptemp == NULL)
{
printf("临时文件创建失败");
}
else
{
fputs("lskjdlfj两款手机登陆福建",ptemp);
rewind(ptemp);
char str[512];
fgets(str, 512,ptemp);
puts(str);
fclose(ptemp);
}
system("pause");
}
对于可能需要用到的临时文件,产生临时文件名使用tmpnam,保存到字符串,并添加后缀
void main()
{
FILE *ptemp;
char path[100];
tmpnam(path);//生成一个临时文件名,保存到path
//一般在程序所在磁盘的根目录
//为临时文件添加后缀
char *p = path;
while (*p != '\0')
{
if (*p == '.')
{
*p ='x';
}
p++;
}
strcat(path,".txt");
ptemp = fopen(path,"w+");
printf("%s\n",path);
if (ptemp == NULL)
{
printf("打开失败");
}
else
{
fputs("sljd蓝精灵", ptemp);
fputs("算了款到即发", ptemp);
rewind(ptemp);
char str[512];
fgets(str, 512,ptemp);
printf("%s",str);
fclose(ptemp);
}
system("pause");
}
30. 创建文件夹
cmd命令cd。
void main()
{
char path[100] ="e:\\testmd\\1\\2\\3";
char cmd[100];
sprintf(cmd, "md\"%s\"", path);
system(cmd);
system("pause");
}
linux下使用mkdir指令。
31. 缓冲区
缓冲区又称为缓存,是内存空间的一部分,也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
引入缓冲区的原因。当从磁盘里取信息,先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取。这样就可以减少磁盘的读写次数,再者,计算机对缓冲区的操作大大快于磁盘的操作,应用缓冲区可以大大提高计算机的运行速度。
比如,使用打印机打印文档,由于打印机的打印速度相对较慢。先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时cpu可以处理别的事情。
总之,缓冲区是一块内存区,它用在输入输出设备和cpu之间,用来缓存数据。它使得低速的输入输出设备和高速的cpu能够协调工作,避免低速的输入输出设备占用cpu,解放出cpu,使其能够高效工作。
缓冲区分为三种类型,全缓冲,行缓冲和不带缓冲。
全缓冲,在这种情况下,当填满标准I/O缓存后才进行实际的I/O操作。全缓冲的典型代表是对磁盘文件的读写。缓冲区为空,一次读满数据,再写出。
行缓冲,在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是键盘输入数据。每次读取一行。
不带缓冲,直接写入写出。也就是不进行缓冲,标准出错情况stderr是典型代表。这使得出错数据可以尽快的显示出来。
void main()
{
printf("hwlker");//及时输出,不带缓冲
while (1)
{
}
system("pause");
}
同样的情况,在linux下则是全缓冲的。
windows下设置缓冲区使用setvbuf函数
全缓冲
void main()
{
char buf[4096];//开辟一块内存作为缓冲区
setvbuf(stdout, buf,_IOFBF,4096);//stdout,屏幕输出,buf缓冲区首地址,_IOFBF全缓冲,4096缓冲区大小
//当关闭程序,或者缓冲区满了后才会输出
printf("hwlker");//及时输出,不带缓冲
while (1)
{
printf("slkjdf\n");
Sleep(20);
}
system("pause");
}
行缓冲
windows平台不支持行缓冲,linux支持,linux下默认的是行缓冲。
void main()
{
char buf[4096];//开辟一块内存作为缓冲区
setvbuf(stdout, buf,_IOLBF, 4096);//stdout,屏幕输出,buf缓冲区首地址,_IOLBF行缓冲,4096缓冲区大小
//当关闭程序,或者缓冲区满了后才会输出
printf("hwlker\r\n");//及时输出,不带缓冲
while (1)
{
}
system("pause");
}
不带缓冲
void main()
{
setvbuf(stdout, NULL,_IONBF, 0);//stdout,屏幕输出,NULL没有缓冲,_IOLBF行缓冲,0缓冲区大小
//当关闭程序,或者缓冲区满了后才会输出
printf("hwlker\r\n");//及时输出,不带缓冲
while (1)
{
}
system("pause");
}
32. getchar缓冲处理输入的字符
windows下是非即刻检测,就是当输入回车时才检测,在输入时不会立即检测。linux下是即刻检测。
void main()
{
char ch = getchar();
printf("%p\n",stdin->_base);//键盘输入缓冲区的基地址
printf("%p\n",stdin->_ptr);//键盘输入缓冲区指针的当前地址
printf("%d\n",*stdin->_ptr);//取出当前缓冲区的内容
printf("%d\n",*++stdin->_ptr);//取出当前缓冲区内容,指针后移一位
printf("%d\n",*++stdin->_ptr);//取出当前缓冲区内容,指针后移一位
printf("%d\n",*++stdin->_ptr);//取出当前缓冲区内容,指针后移一位
printf("%d\n",*++stdin->_ptr);//取出当前缓冲区内容,指针后移一位
system("pause");
}
33. windows刷新输入输出缓冲区
刷新输出缓冲区
void main()
{
char buf[4096];//开辟一块内存作为缓冲区
setvbuf(stdout, buf,_IOFBF, 4096);//stdout,屏幕输出,buf缓冲区首地址,_IOFBF全缓冲,4096缓冲区大小
//当关闭程序,或者缓冲区满了后才会输出
printf("hwlker");
fflush(stdout);//使用fflush刷新缓冲区,屏幕输出
system("pause");
}
刷新输入缓冲区
void main()
{
char ch;
ch = getchar();
printf("%c\n",ch);
ch = getchar();
printf("%c\n",ch);
ch = getchar();
printf("%c\n",ch);
//中断,抛弃后面的输入,使用刷新缓冲区
fflush(stdin);
ch = getchar();
printf("%c\n",ch);
system("pause");
}
34. linux刷新输入输出缓冲区
当程序检测到输入的时候,会自动刷新输出缓冲区。
刷新输出输入缓冲区
void FLUSH()
{
char ch;
//刷新输出缓冲区
while((ch = getchar) !='\n'&&ch != EOF);
//刷新输入缓冲区
if (feof(stdin))
{
clearerr(stdin);//清空键盘缓冲区
}
}
void main()
{
char ch = getchar();
printf("%c\n",ch);
ch = getchar();
printf("%c\n",ch);
FLUSH();
//_fpurge(stdin);//或者使用这个函数刷新输入缓冲区
ch = getchar();
printf("%c\n",ch);
ch = getchar();
printf("%c\n",ch);
system("pause");
}
void main74()
{
printf("HELLO");
FLUSH();
while (1);
}
35. windows键盘输入无缓冲模式
windows键盘的无缓冲模式实际上是双字节缓冲。对于应用程序是双字节的。而操作系统是有键盘缓冲区的。开启程序后,每次输入数据,先缓冲到操作系统的键盘输入缓冲区,一次向程序的缓冲区输入2个字节,两个两个,直到操作系统的键盘缓冲区的数据都输入到程序的键盘缓冲区。
void main()
{
char c = getchar();
printf("首次输入%c\n", c);
printf("\n");
//设置键盘为无缓冲模式
//每当设置缓冲的时候,会自动刷新缓冲
setvbuf(stdin, NULL,_IONBF, 0);
printf("缓冲区大小为%d\n",stdin->_bufsiz);//键盘无缓冲模式实际上双字节缓冲
printf("缓冲区保存的字符%c,%d\n",stdin->_charbuf, stdin->_charbuf);
printf("缓冲区未读的字符数%d\n",stdin->_cnt);
printf("缓冲区的基地址%p\n",stdin->_base);
printf("缓冲区当前地址%p\n",stdin->_ptr);
printf("缓冲区当前内容%c\n",*stdin->_ptr);
printf("\n");
c = getchar();
printf("设置缓冲后首次输入%c\n",c);
printf("缓冲区大小为%d\n",stdin->_bufsiz);//键盘无缓冲模式实际上双字节缓冲
printf("缓冲区保存的字符%c,%d\n",stdin->_charbuf>>8, stdin->_charbuf>>8);
printf("缓冲区未读的字符数%d\n",stdin->_cnt);
printf("缓冲区的基地址%p\n",stdin->_base);
printf("缓冲区当前地址%p\n",stdin->_ptr);
printf("缓冲区当前内容%c\n",*stdin->_ptr);
printf("\n");
c = getchar();
printf("设置缓冲后第2次输入%c\n", c);
printf("缓冲区大小为%d\n",stdin->_bufsiz);//键盘无缓冲模式实际上双字节缓冲
printf("缓冲区保存的字符%c,%d\n",stdin->_charbuf >> 8, stdin->_charbuf >> 8);
printf("缓冲区未读的字符数%d\n",stdin->_cnt);
printf("缓冲区的基地址%p\n",stdin->_base);
printf("缓冲区当前地址%p\n",stdin->_ptr);
printf("缓冲区当前内容%c\n",*stdin->_ptr);
printf("\n");
c = getchar();
printf("设置缓冲后第3次输入%c\n", c);
printf("缓冲区大小为%d\n",stdin->_bufsiz);//键盘无缓冲模式实际上双字节缓冲
printf("缓冲区保存的字符%c,%d\n",stdin->_charbuf >> 8, stdin->_charbuf >> 8);
printf("缓冲区未读的字符数%d\n",stdin->_cnt);
printf("缓冲区的基地址%p\n",stdin->_base);
printf("缓冲区当前地址%p\n",stdin->_ptr);
printf("缓冲区当前内容%c\n",*stdin->_ptr);
printf("\n");
c = getchar();
printf("设置缓冲后第4次输入%c\n", c);
printf("缓冲区大小为%d\n",stdin->_bufsiz);//键盘无缓冲模式实际上双字节缓冲
printf("缓冲区保存的字符%c,%d\n",stdin->_charbuf >> 8, stdin->_charbuf >> 8);
printf("缓冲区未读的字符数%d\n",stdin->_cnt);
printf("缓冲区的基地址%p\n",stdin->_base);
printf("缓冲区当前地址%p\n",stdin->_ptr);
printf("缓冲区当前内容%c\n",*stdin->_ptr);
printf("\n");
c = getchar();
printf("设置缓冲后第5次输入%c\n", c);
printf("缓冲区大小为%d\n",stdin->_bufsiz);//键盘无缓冲模式实际上双字节缓冲
printf("缓冲区保存的字符%c,%d\n",stdin->_charbuf >> 8, stdin->_charbuf >> 8);
printf("缓冲区未读的字符数%d\n",stdin->_cnt);
printf("缓冲区的基地址%p\n",stdin->_base);
printf("缓冲区当前地址%p\n",stdin->_ptr);
printf("缓冲区当前内容%c\n",*stdin->_ptr);
system("pause");
}
36. 文件的分割合并
分割
void main()
{
char path[100] ="e:\\";
char filename[50] ="1.mp3";
char realpath[150];
sprintf(realpath,"%s\\%s", path, filename);
int filesize = 0;
FILE *fpr =fopen(realpath, "rb");
if (fpr == NULL)
{
perror("文件打开失败,原因是");
return;
}
else
{
fseek(fpr, 0,2);
filesize =ftell(fpr);
printf("文件大小%d\n",filesize);
int setsize =1024 * 1024;//设置要分割的模块大小
int n;//存储一个文件可以分割为多个模块
if (filesize %setsize == 0)
{
n =filesize / setsize;
}
else
{
n =filesize / setsize + 1;
}
fseek(fpr, 0,0);
charlistpath[100] = "e:\\list.txt";
FILE *plist =fopen(listpath, "w");//创建list.txt文件
for (int i = 1;i <= n; i++)
{
chartemppath[120];//存储每一块的路径
sprintf(temppath,"%s\\%d%s", path, i, filename);//构建每一块的路径名
printf("%s\n",temppath);
fputs(temppath,plist);//写入list.txt
fputc('\n',plist);//写入换行符
FILE*pb = fopen(temppath, "wb");//按照二进制写入的模式打开文件
if (i< n)
{
for(int j = 0; j < setsize; j++)
{
intich = fgetc(fpr);//读取一个字符
fputc(ich,pb);
}
}
else
{
for(int j = 0; j < filesize - (n-1) * setsize; j++)
{
intich = fgetc(fpr);//读取一个字符
fputc(ich,pb);
}
}
fclose(pb);//关闭写入的块文件
}
fclose(plist);
fclose(fpr);
}
system("pause");
}
合并
void main()
{
char path[100] ="e:\\list.txt";
FILE *fpr = fopen(path,"r");//按照读的方式打开
char allpath[100] ="e:\\2.mp3";
FILE *fpw = fopen(allpath,"wb");//按照二进制方式打开要合并为的文件
char temppath[200];
while (fgets(temppath,200, fpr))//不断循环的获取路径,读到字符串,值为非0,读不到或到末尾,值为0
{
printf("%s",temppath);//字符串有换行符
int length =strlen(temppath);
temppath[length- 1] = '\0';//换行符替换为'\0'
{
FILE*fpb = fopen(temppath, "rb");
int ich= fgetc(fpb);
while(!feof(fpb))
{
fputc(ich,fpw);//写入到要合并的文件
ich= fgetc(fpb);
}
fclose(fpb);
}
}
fclose(fpw);
fclose(fpr);
system("pause");
}
37. 写入一段文本到文件
使用cmd命令,echo sldkjfd >> e:\1.txt
void main()
{
char str[30] ="haha";
char path[40] ="e:\\1.txt";
char cmd[100];
sprintf(cmd, "echo %s>> \"%s\"", str, path);
system(cmd);
system("pause");
}
写入多行
执行多次cmd
如果要清空文本中原来的内容使用>,比如echo halsjd > e:\1.txt。> 表示重写,>>表示追加。
void main()
{
char str1[30] ="haha";
char str2[30] ="hasdfha";
char str3[30] ="hahhsdfa";
char path[40] ="e:\\1.txt";
char cmd[100];
sprintf(cmd, "echo %s>> \"%s\"", str1, path);
system(cmd);
sprintf(cmd, "echo %s>> \"%s\"", str2, path);
system(cmd);
system("pause");
}
不使用cmd命令写入字符串到文件
void main()
{
FILE *fp;
char path[40] ="e:\\2.txt";
fp = fopen(path,"w");
if (fp == NULL)
{
perror("打开失败");
}
else
{
char str[100] ="haha";
int length;
length =strlen(str);
for (int i = 0;i < length; i++)
{
fputc(str[i],fp);//向一个文件写入字符
}
fclose(fp);
}
system("pause");
}
根据用户输入数据,写入到文件
void main()
{
FILE *fp;
char path[40] ="e:\\3.txt";
fp = fopen(path,"w");
if (fp == NULL)
{
perror("打开失败");
}
else
{
char ch =getchar();
while (ch !=EOF)//输入是ctrl+Z结束
{
fputc(ch,fp);
ch =getchar();
}
fclose(fp);
}
system("pause");
}
38. 字符方式读写文件
读取
void main()
{
FILE *fp;
char path[100] ="e:\\1.txt";
fp = fopen(path,"r");
if (fp == NULL)
{
printf("打开失败");
}
else
{
printf("打开成功\n");
while(!feof(fp))
{
char ch= fgetc(fp);
putchar(ch);
}
fclose(fp);
}
system("pause");
}
写入
void main()
{
FILE *fp;
char path[100] ="e:\\1.txt";
fp = fopen(path,"r+");
if (fp == NULL)
{
printf("打开失败");
}
else
{
printf("打开成功\n");
while(!feof(fp))
{
char ch= fgetc(fp);
putchar(ch);
}
int xie =getchar();
while ( xie !=EOF )
{
fputc(xie,fp);
xie =getchar();
}
fclose(fp);
}
system("pause");
}
复制文件
void main()
{
FILE *fpr;
FILE *fpw;
char pathr[100] ="e:\\1.txt";
char pathw[100] ="e:\\1copy.txt";
fpr = fopen(pathr,"r");
fpw = fopen(pathw,"w");
if (fpr == NULL)
{
printf("打开读取文件失败");
return;
}
else if (fpw == NULL)
{
printf("打开写入文件失败");
return;
}
else
{
while(!feof(fpr))
{
char ch= fgetc(fpr);
fputc(ch,fpw);
}
fclose(fpr);
fclose(fpw);
}
system("pause");
}
39. 批量修改文件并显示
查找到要修改的文件,并写入临时文件,使用cmd命令,dir /b e:\*.txt > e:\list.txt
void main()
{
char path[50] ="e:\\";
char cmd[100];
char find[50] ="*.txt";
char outres[100] ="e:\\testfile\\list.txt";
remove(outres);//每次新执行,删除旧文件
sprintf(cmd, "dir /b%s%s > %s", path, find, outres);
system(cmd);
//批量修改
FILE *fp = fopen(outres,"r");
if (fp == NULL)
{
perror("文件打开失败");
return;
}
else
{
charfilename[128];
while(fgets(filename, 128, fp))
{
chartemppath[100];//保存读取出来的文件名
intlength = strlen(filename);
filename[length- 1] = '\0';
sprintf(temppath,"%s\\%s", path, filename);
printf("\n%s\n",temppath);
{
FILE*fpt = fopen(temppath, "a+");
fputs("\n建立随风倒舵", fpt);
fclose(fpt);
}
char cmdshow[100];
sprintf(cmdshow,"type %s", temppath);
system(cmdshow);
}
fclose(fp);
}
system("pause");
}
40. 文件的增删改查
在文件中查询内容
void main()
{
char path[100] ="e:\\1.txt";
FILE *fp = fopen(path,"r");
if (fp == NULL)
{
printf("打开失败");
}
else
{
char str[100];
char findstr[50]= "建立";
while(fgets(str, 100, fp))
{
char *p= strstr(str, findstr);
if (p!= NULL)
{
printf("找到%s", str);
}
else
{
printf("%s",str);
}
}
fclose(fp);
}
system("pause");
}
删除
有两种方式,1,读取到内存,然后按照字符串修改,对于大文件不适用;2,新建一个临时文件,挨个读取字符串,然后写入,遇到要删除的,就不写入。删除原来的文件,然后重命名临时文件,适合处理大数据文件。
void main()
{
char path[100] ="e:\\1.txt";
char temppath[100] = { 0};//临时文件的路径
FILE *fp = fopen(path,"r");
if (fp == NULL)
{
printf("打开失败");
}
else
{
tmpnam(temppath);//创建一个临时文件
FILE *ptemp =fopen(temppath, "w");
if (ptemp ==NULL)
{
printf("临时文件创建失败");
}
char str[120];
char findstr[30]= "建立";
while(fgets(str, 120, fp))
{
char *p= strstr(str, findstr);
if (p== NULL)
{
fputs(str,ptemp);
}
}
fclose(ptemp);
fclose(fp);
remove(path);
rename(temppath,path);
}
system("pause");
}
修改
void main()
{
char path[100] ="e:\\1.txt";
char temppath[100] = { 0};//临时文件的路径
FILE *fp = fopen(path, "r");
if (fp == NULL)
{
printf("打开失败");
}
else
{
tmpnam(temppath);//创建一个临时文件
FILE *ptemp =fopen(temppath, "w");
if (ptemp ==NULL)
{
printf("临时文件创建失败");
}
char str[120];
char findstr[30]= "建立";
while(fgets(str, 120, fp))
{
char *p = strstr(str, findstr);
if (p!= NULL)
{
fputs("哈哈", ptemp);
}
else
{
fputs(str,ptemp);
}
}
fclose(ptemp);
fclose(fp);
remove(path);
rename(temppath,path);
}
system("pause");
}
增加
void main()
{
char path[100] ="e:\\1.txt";
char temppath[100] = { 0};//临时文件的路径
FILE *fp = fopen(path,"r");
if (fp == NULL)
{
printf("打开失败");
}
else
{
tmpnam(temppath);//创建一个临时文件
FILE *ptemp =fopen(temppath, "w");
if (ptemp ==NULL)
{
printf("临时文件创建失败");
}
char str[120];
char findstr[30]= "建立";
while(fgets(str, 120, fp))
{
char *p= strstr(str, findstr);
if (p!= NULL)
{
fputs("hahah",ptemp);
fputs(str,ptemp);
}
else
{
fputs(str,ptemp);
}
}
fclose(ptemp);
fclose(fp);
remove(path);
rename(temppath,path);
}
system("pause");
}
41. 文件以及目录的改名
使用rename(char *oldname,char *newname)函数
void main()
{
char oldname[100] ="e:\\1.txt";
char newname[100] ="e:\\1new.txt";
if (rename(oldname,newname) == 0)
{
printf("改名成功");
}
else
{
printf("改名失败");
}
system("pause");
}
当修改的不在同一个路径时,就剪切过去,然后重命名。如果剪切的目的地存在同名文件,则失败。
void main()
{
char oldname[100] ="e:\\1new.txt";
char newname[100] ="e:\\testfile\\1new.txt";
if (rename(oldname,newname) == 0)
{
printf("改名成功");
}
else
{
printf("改名失败");
}
system("pause");
}
修改目录名
void main()
{
char oldname[100] ="e:\\2";
char newname[100] ="e:\\ttt";
if (rename(oldname,newname) == 0)
{
printf("改名成功");
}
else
{
printf("改名失败");
}
system("pause");
}
同样的,当不在同一个路径时,就将原目录剪切到新的路径,并重命名。如果剪切的目的地存在同名文件夹,则失败。
42. 按照行读写文本文件
void main()
{
FILE *fp;
char path[50] ="e:\\2.txt";
fp = fopen(path,"r");
if (fp == NULL)
{
printf("文件打开失败");
}
else
{
printf("文件打开成功\n");
char str[200] ={ 0 };
while (fgets(str,200, fp))
{
puts(str);
}
fclose(fp);
}
system("pause");
}
二十一、 指针高级
1. 指针与数组名
数组名是一种常指针,不能修改,其值等于数组占据内存单元的首地址,但其类型取决于数组的维数。对于二维数组a[i][j],a,&a,*a的结果是一样的。作为指针存储的地址都是一样,指针并不一样。指针不仅有大小,也有数据类型,数据类型决定了大小。
void main()
{
int a[3][4] = { 1, 2, 3,4, 5, 6, 7, 8, 9, 11, 12, 13 };
printf("a=%p,&a=%p,*a=%p\n",a, &a, *a);
printf("sizeof*a=%d,sizeof*&a=%d,sizeof**a=%d\n",sizeof(*a), sizeof(*&a), sizeof(**a));
system("pause");
}
2. 指针访问三维数组
数组与指针关系密切,数组元素除了可以使用下标访问,还可以用指针形式表示。数组元素可以很方便地用数组名常指针来表示,以3维int数组a举例,其中的元素a[i][j][k]可以用下面形式表示:
*(a[i][j]+k),其值为&a[i][j][0],因此,a[i][j][k]可表述为*(a[i][j]+k)。
*(*(a[i]+j)+k),和第一种形式比较,a[i][j]=*(a[i]+j),a[i]是二级指针,值为&a[i][0]。
*(*(*(a+i)+j)+k),将第二种形式的a[i]替换成了*(a+i),此处a是三级指针,其值为&a[0]。
指针访问数组的规律,每次指针加深一层
a[i] *(a+i)
a[i][j] *(*(a+i)+j)
a[i][j][k] *(*(*(a+i)+j)+k)
void main()
{
int a[2][3][4];
int num = 0;
for (int i = 0; i < 2;i++)
{
for (int j = 0;j < 3; j++)
{
for(int k = 0; k < 4; k++)
{
a[i][j][k]= num;
num++;
printf("%-4d",a[i][j][k]);
}
printf("\n");
}
printf("\n");
}
printf("指针访问\n");
for (int i = 0; i < 2;i++)
{
for (int j = 0;j < 3; j++)
{
for(int k = 0; k < 4; k++)
{
//printf("%-4d",*(a[i][j]+k));
//printf("%-4d",*(*(a[i]+j) + k));
printf("%-4d",*(*(*(a+i)+j) + k));
}
printf("\n");
}
printf("\n");
}
system("pause");
}
3. 指针访问四维数组
void main()
{
int a[2][3][4][5];
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a) / sizeof(int));
for (int *p = a, i = 0; p< &a[0][0][0][0] + 120; p++,i++)
{
*p = i;
}
for (int i = 0; i < 2;i++)
{
for (int j = 0;j < 3; j++)
{
for(int k = 0; k < 4; k++)
{
for(int l = 0; l < 5; l++)
{
printf("%-4d",a[i][j][k][l]);
}
printf("\n");
}
printf("\n");
}
printf("\n");
}
system("pause");
}
4. 指针数组
指针也可作为数组中的元素,将一个个指针用数组形式组织起来,就构成了指针数组。
一个数组,若其元素均为指针类型数据,称为指针数组,也就是说,指针数组中的每一个元素都存放一个地址,相当于一个指针变量。
定义一维指针数组的一般形式为,类型名 *数组名[数组长度];int *p[4];
对于某些只能读不能写的场合,使用指针数组,指向要读的数据,通过修改指针数组元素实现等同原数据的操作,比如从小到大排序。
void main()
{
const int a[5] = { 12, 34,53, 3, 23 };
int *sort[5];
for (int i = 0; i < 5;i++)
{
sort[i] =&a[i];
printf("%x,%d\n",sort[i],*sort[i]);
}
//冒泡从小到大
for (int i = 0; i < 5 -1; i++)
{
for (int j = 0;j < 5 - 1 - i; j++)
{
if(*sort[j]>*sort[j + 1])
{
int*temp = sort[j];
sort[j]= sort[j + 1];
sort[j+ 1] = temp;
}
}
}
printf("从小到大输出\n");
for (int i = 0; i < 5;i++)
{
printf("%x,%d\n",sort[i], *sort[i]);
}
//指针循环访问指针数组,使用二级指针
printf("指针循环访问\n");
for (int **pp = sort; pp <sort + 5; pp++)
{
printf("%x,%d\n",*pp, **pp);
}
system("pause");
}
指针数组实现将若个字符串按字母顺序(由小到大)输出
void main()
{
const char a[5][100] = {"asldkjf", "alksjdf", "blks","usldkjf", "cljs" };
char *sort[5];
for (int i = 0; i < 5;i++)
{
sort[i] =&a[i][0];
printf("%x,%s\n",sort[i], sort[i]);
}
//或者直接char *sort[5] = {"asldkjf", "alksjdf", "blks","usldkjf", "cljs" };
//冒泡排序,使用字符串比较函数strcmp
for (int i = 0; i < 5 -1; i++)
{
for (int j = 0;j < 5 - 1 - i; j++)
{
if(strcmp(sort[j],sort[j+1]) == 1)
{
int*temp = sort[j];
sort[j]= sort[j + 1];
sort[j+ 1] = temp;
}
}
}
printf("排序后输出\n");
for (int i = 0; i < 5;i++)
{
printf("%x,%s\n",sort[i], sort[i]);
}
system("pause");
}
5. 二维数组与指针数组的区别
const chara[5][100] = { "asldkjf", "alksjdf", "blks","usldkjf", "cljs" };
二维数组的数据不仅可以读还可以写。但是,二维数组的行名是地址常量,也就是地址不可以改变。
指针数组元素的作用相当于二维数组的行名,但指针数组中元素是指针变量。指针变量可以修改,就是说指向的对象可以换为别的对象。但是指针数组指向的字符串是常量,常量字符串不可以修改。
二维数组用于字符串顺序固定,同时字符串需要修改的情况。指针数组用于字符串需要修改,顺序可以调整的情况。
二维数组,地址不可以变化,数据可以变化;指针数组,指向的数据不可以变化,地址可以变化。二维数组,不修改地址的情况下管理数据;指针数组,不修改数据的情况下数据查看。
void main()
{
char cmd[5][20] = {"calc1", "ipconfig", "notepad","tasklist", "mspaint" };
printf("%p\n",cmd);
cmd[0][4] = '\0';//二维数组不仅可以读也可以写
for (int i = 0; i < 5;i++)
{
//system(cmd[i]);//system(*(cmd+i));
//cmd[i] = cmd[i+ 1];//cmd[i]是常量,无法修改
}
printf("指针数组\n");
char *cmd2[5] = {"calc", "ipconfig", "notepad","tasklist", "mspaint" };
cmd2[0] = cmd2[1];
//*cmd2[0] ="hsl";//常量不可以修改
printf("%p\n",cmd2);//每个指针指向一个字符串的首地址
printf("%d\n",sizeof(cmd2));//存储5个地址变量,占20个字节
for (int i = 0; i < 5;i++)
{
printf("%s\n",cmd2[i]);//cmd2[i]等价于*(cmd+i);
}
system("pause");
}
6. 函数指针数组
将函数地址放到指针数组中。
int jia(int a, int b)
{
return a + b;
}
int jian(int a, int b)
{
return a - b;
}
int cheng(int a, int b)
{
return a * b;
}
int chu(int a, int b)
{
return a / b;
}
int mo(int a, int b)
{
return a % b;
}
void main()
{
int(*p[5])(int a, intb);//定义一个函数指针数组
p[0] = jia;
p[1] = jian;
p[2] = cheng;
p[3] = chu;
p[4] = mo;
//或者定义时初始化
//int(*p[5])(int a, int b)= { jia, jian, cheng, chu, mo };
int x = 100;
int y = 10;
for (int i = 0; i < 5;i++)
{
printf("%x,%d\n",p[i], p[i](x, y));
}
system("pause");
}
7. 函数指针的内存原理
函数被载入内存,函数必然有一个地址是函数的入口,我们用这个地址来调用。函数名也是指向函数入口点的指针,通过函数名找到函数的执行入口。
同时c语言的编译器,不论是vc或者gcc,都有这样的规则。针对函数void run(),函数名run为函数的地址,run,&run,*run都解析为run的入口地址,即为&run的首地址。而且函数名不可以使用sizeof操作符。
void msg()
{
MessageBoxA(0,"haha","lskd",0);
}
void msg2()
{
MessageBoxA(0,"222ha", "22kd", 0);
}
void main()
{
printf("%p\n", msg);
//msg();
//定义函数指针
void(*p)();
p = msg;
p();
p = msg2;//改变函数指针存储的地址,就可以调用不同的函数
p();
//不可以求函数名的大小
//printf("%d\n",sizeof(msg));
printf("msg=%x,&msg=%x,*msg=%x\n",msg, &msg, *msg);//c语言编译器优化的调函数地址,不用加&也可以
//最原始的取函数地址的方式仍有用
(&msg)();
//针对函数,*函数名,也解析为函数地址
(*msg)();
system("pause");
}
8. 指向函数指针的指针
使用指针的方式遍历一个函数指针数组。
void main()
{
int(*p[5])(int a, int b) ={ jia, jian, cheng, chu, mo };
int x = 100;
int y = 10;
for (int(**pp)(int a, intb) = p; pp < p + 5; pp++)
{
printf("%d\n",(*pp)(x, y));
}
system("pause");
}
使用二级函数指针改变指针函数指针指向的函数。
外部函数改变一个指针,需要指针的地址,指针的地址也就是二级指针;改变一个函数指针,需要函数指针的地址,也就是二级函数指针。
void change(int(**p)(int a, int b))
{
*p = jian;
}
void main()
{
int(*p)(int a, int b) =jia;
int x = 100;
int y = 10;
change(&p);
printf("%d\n",p(x, y));
system("pause");
}
使用dll修改函数指针指向的函数的方法
_declspec(dllexport) void hanshugua()
{
void (**p)() =(void(**)())0x00223abc;
*p =(void(*)())0x00233adb;
}
9. 各种指针定义列举
void main12()
{
//一个指向整型数的指针
int a = 100;
int *zhengxing = &a;
//一个指向整型数指针的指针
int **zhengxingp =zhengxing;
//一个有10个整型指针的数组
int *arrp[10];
//一个指向有10个整型数数组的指针
int b[10];
int *bp = b;
//一个指向有10个整型指针的数组的指针
int **arrpp = arrp;
//一个指向函数的指针,该函数有一个整型参数,并返回一个整型数
int(*funcp)(int a) =functest;
funcp(3);
//一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数,并返回一个整型数
int(*funcparr[10])(int a);
funcparr[0] = functest;
//一个指向函数指针的指针,所指向的函数有一个整型参数,并返回一个整型数
int(**funcpp)(int a) =funcp;
}
10. 对比define和typedef定义函数指针
使用typedef,可以将函数指针类型化。比较typedef和#define。
typedef double *DP;
DP pDouble1,pDouble2;
与
#define DP double*
DP pDouble1,pDouble2;
对于typedef语句typedef double* DP;来说,去掉typedef后,其仍然是条完整的c语句double *DP;该语句用以声明一个double类型的指针变量DP。由此可以理解,typedef的作用是将变量名作为该变量所属类型的别名(或说助记符)。#define不具备这种特点,#define实际上是替换,去掉#define后,DP double并不是一条合法的c语句。
typedef需要加;,#define不要加;。
#define可以对任意内容替换;而typedef仅仅适用于已有的类型。
#define DP double *
typedef double* dp;
void main()
{
DP dp1, dp2;//double*dp1,dp2
dp dp3, dp4;//double*dp3,double *dp4
printf("dp1=%d,dp2=%d\n",sizeof(dp1), sizeof(dp2));
printf("dp3=%d,dp4=%d\n",sizeof(dp3), sizeof(dp4));
system("pause");
}
使用definde和typedef简化定义函数指针
typedef void(*p)(int num);//给函数指针类型别名,p是类型别名
#define defp(X) void(*X)(int num)
void dayin(int num)
{
printf("原始的函数指针%d\n",num);
}
void dayin2(int num)
{
printf("typedef函数指针别名%d\n",num);
}
void dayin3(int num)
{
printf("#define替换函数指针%d\n",num);
}
void main()
{
void(*p1)(int a);
p1 = dayin;
p1(3);
p p2 = dayin2;
p2(2);
defp(p3) = dayin3;
p3(4);
system("pause");
}
11. 指针数组外部修改程序
_declspec(dllexport) void doit()
{
int *p[10];
p[0] = (int *)0x13EDB6F8;
p[1] = (int *)0x13EDAA80;
p[2] = (int *)0x13EDB6F0;
p[3] = (int *)0x13EDBA88;
while (1)
{
for (int i = 0;i < 4; i++)
{
*p[i] =10;
if(*p[i] < 15)
{
*p[i]= 100;
}
}
Sleep(20000);
}
}
指针数组批量修改数组数据
void main()
{
time_t ts;//时间数据类型
unsigned int randdata =time(&ts);
srand(randdata);
int a[10] = { 0 };
int *p[10];
for (int i = 0; i < 10;i++)
{
p[i] =&a[i];//指针对应数组的每一个地址
}
while (1)
{
for (int i = 0;i < 10; i++)
{
a[i] =rand() % 100;
printf("%d\n",a[i]);
}
Sleep(5000);
for (int i = 0;i < 10; i++)
{
if(*p[i] < 100)
{
*p[i]= 200;
}
printf("%d\n",a[i]);
}
Sleep(3000);
}
system("pause");
}
字符串指针数组
void main()
{
char *p[] = {"calc", "notepad", "mspaint", "write","tasklist" };
int n = sizeof(p) /sizeof(char *);
for (int i = 0; i < n;i++)
{
printf("%s\n",p[i]);
system(p[i]);
}
system("pause");
}
使用函数指针中断程序
_declspec(dllexport) void doit()
{
void(**p)() =(void(**))0x0030fc24;//传入函数指针的地址
*p = (void(*)())NULL;//修改函数指针指向的地址
}
批量修改函数指针
_declspec(dllexport) void doit()
{
void(*p[3])();
p[0] = 0x0130100a;
p[1] = 0x0130100f;
p[2] = 0x01301014;
void(**pp) () =0x0041fb48;
for (int i = 0; i < 3;i++)
{
*pp = p[i];
pp++;
}
}
12. 命令行参数与指针数组
使用void main(int argc, char *argv[]);
//int argc命令行有几个字符串,char *argv[]一个字符指针数组,每一个元素都是指针
void main(int argc,char *argv[])
{
printf("%d\n",argc);
for (int i = 1; i <argc; i++)
{
printf("%s\n",argv[i]);
system(argv[i]);
}
system("pause");
}
或者
//数组在函数内部会退化成指针,普通数组是指针,指针数组退化为二级指针
void main(int argc,char *argv[])
{
printf("%d\n",argc);
while (argc > 1)//第一个是程序名,略过
{
argc--;//获取一次,递减一次
argv++;//argv是一个二级指针,指向一个命令字符串指针数组的地址,一开始指向数组的开始,首次需要向前移动一次
printf("%s\n",*argv);
}
system("pause");
}
生成minglinghang.exe
在cmd下,进入执行程序的目录,执行minglinghang.exe aha asdf asdf查看结果。
右键项目,属性,调试,可以设置命令行参数。
13. 指向数组结构的指针
普通数组名可以看成是指向数组元素首地址的常指针,结构体数组名同样可以看成是指向结构体数组元素首地址的常指针,也可以声明一个结构指针变量,使其指向数组元素首地址,这两种方式都能实现通过指针访问数组元素。
指针访问数组
void main()
{
int a[5] = { 1, 2, 4, 6, 7};
int *p = NULL;
p = a;//p是一个指针变量
//a = p;//a是指针常量
for (int i = 0; i < 5;i++)
{
printf("%d,%x,%d,%x\n",*(a + i), a + i, a[i], &a[i]);
printf("%d,%x,%d,%x\n",*(p + i), p + i, p[i], &p[i]);
}
//使用指针访问数组
for (int *pa = a; pa <a + 5; pa++)
{
printf("%d,%x\n",*pa, pa);
}
system("pause");
}
指针访问结构体数组
struct info
{
char name[50];
int id;
};
void main()
{
struct info info1[5] = { {"h1", 1 }, { "h2", 2 }, { "h3", 3 }, {"h4", 4 }, { "h5", 5 } };
for (int i = 0; i < 5;i++)
{
printf("%s,%d\n",info1[i].name, info1[i].id);
}
for (struct info *p =info1; p < info1 + 5; p++)
{
printf("%s,%d\n",p->name, p->id);
printf("%s,%d\n",(*p).name, (*p).id);
}
system("pause");
}
14. 字符串指针和字符串指针数组
字符串指针
void main()
{
char *p = "tasklist";
printf("%d\n",sizeof(p));//变量p存储的是常量字符串的首地址,是一个指针类型,占4个字节
printf("%d\n",sizeof("tasklist"));
*p = '1';//p指向的字符串是常量,无法赋值修改
printf("%p\n",p);//p保留的是字符串指针首地址
system("pause");
}
字符串指针数组,用于存储若干个常量字符串的首地址
void main()
{
char *p[5] = { "tasklist","calc", "write", "ipconfig", "mspaint"};
for (int i = 0; i <sizeof(p) / sizeof(char *); i++)
{
printf("%s\n",p[i]);
system(p[i]);
}
system("pause");
}
二十二、 函数高级
1. 函数副本机制
函数的参数传递有传值和传地址两种方式。传值调用时,在函数内对形参的改变都不会影响实参,要想在函数内对实参进行操作,必须采用传地址调用的方式。这是形象化的理解,从本质上说,这是由参数传递的副本机制决定的。
副本机制,是指copy的思想,不论是传值调用还是传址调用,编译器都要为每个参数制作临时副本或称拷贝,函数体中对参数的修改都是对副本的修改。每个形式参数,都会新开辟一片内存,容纳传过来的实际参数的值,数组除外。不同的地址不是同一个变量。传址调用也是副本机制,不同的是传过去的值是一个地址,而接收地址需要用指针或数组。
void change(int num)
{
num = 9;
}
void changep(int p[])
{
*p = 4;
}
void changep2(int *p)
{
*p = 5;
}
void main()
{
int num = 10;
printf("%d\n",num);
change(num);
printf("%d\n",num);
changep(&num);
printf("%d\n",num);
changep2(&num);
printf("%d\n",num);
system("pause");
}
2. 结构体以及结构体数组作为函数参数
结构体变量的传值和传址调用。采用值传递时,在函数内将生成实参的复制品。如果参数多事像int,char之类的简单变量,这些变量占用的内存并不多,赋值也快。但结构体或共用体变量往往由多个成员变量组成,占用内存大,如果复制一份,会造成时间和空间的双重浪费。采用地址传递不会造成时空浪费,因为不管是多么复杂的结构类型,指针参数只占4个内存字节。
结构体作为函数参数
struct info
{
char name[40];
int id;
};
//函数的副本机制,函数的参数新建了一个参数,存储传来的值
void showinfo(struct info info1)
{
strcpy(info1.name,"xxxx");
info1.id = 333333;
printf("%s,%d\n",info1.name, info1.id);
}
//传递地址,使用指针修改原来的变量的值
void changeinfo(struct info *info1)
{
strcpy(info1->name,"xxxx");//(*p).name
info1->id =333333;//(*p).id
}
void main()
{
struct info info1 = {"haha", 1234567543 };
showinfo(info1);
printf("%s,%d\n",info1.name, info1.id);
changeinfo(&info1);
printf("%s,%d\n",info1.name, info1.id);
system("pause");
}
数组作为函数参数退化为指针。
struct cs
{
char user[20];
char tel[11];
int id;
};
//void find(struct cs cs1[5], char *str)
void find(struct cs *cs1, char *str)//或者使用指针接收,数组作为函数参数,退化成指针
{
for (int i = 0; i < 5;i++)
{
if(strcmp(cs1[i].user, str) == 0)
{
printf("%s在%d,%s,%s\n",str, cs1[i].id, cs1[i].user, cs1[i].tel);
}
}
}
void main()
{
struct cs cs1[5] = {
{"ha1h", "111", 1 },
{"ha2h", "222", 2 },
{"ha3h", "333", 3 },
{"h4ah", "444", 4 },
{"ha5h", "555", 5 }
};
printf("结构体数组占据%d个字节\n",sizeof(cs1));
printf("结构体数组元素占据%d个字节\n",sizeof(struct cs));
for (int i = 0; i < 5;i++)
{
printf("%s,%s,%d\n",cs1[i].user, cs1[i].tel, cs1[i].id);
}
printf("\n");
find(cs1,"ha2h");
system("pause");
}
3. 返回结构体的函数
如果函数的返回值是某个结构体变量,常称该函数为结构体型函数,结构体型函数定义的基本格式为:
struct 结构体名 函数名(形参列表)
{
函数体;
}
声明一个结构体型函数时不要遗漏struct。
返回一个结构体,相等将一个结构体变量的值赋值给另一个结构体,两个结构体的地址是不一样的。
struct info getstruct(char *p, int num)
{
struct info info1;//临时中间变量
strcpy(info1.name, p);
info1.id = num;
printf("%x\n",&info1);
return info1;//return的副本机制,保存info1到另外一块内存
}
void main()
{
struct info get1 =getstruct("shhh", 12);
printf("%x\n",&get1);
printf("%s,%d\n",get1.name, get1.id);
system("pause");
}
返回结构体地址
struct info * getstructp(char *p, int num)
{
struct info info1;//临时中间变量
strcpy(info1.name, p);
info1.id = num;
printf("%x\n",&info1);
struct info *infop =&info1;
return infop;//return的副本机制,保存info1到另外一块内存
}
void main()
{
struct info *get1 =getstructp("shhh", 12);
printf("%x\n",&get1);
printf("%s,%d\n",get1->name, get1->id);
system("pause");
}
返回指针时,指针不能指向栈,因为栈的内存会被回收,此时,就需要返回堆,使用动态内存分配malloc等。
struct info * getstructd(char *p, int num)
{
struct info *info1 =malloc(sizeof(struct info) * 2);//动态分配结构体内存空间
info1->id = num;
strcpy(info1->name, p);
return info1;//return的副本机制,保存info1到另外一块内存
}
void main()
{
struct info *get1 =getstructd("动态分配内存", 12);
printf("%x\n",&get1);
printf("%s,%d\n",get1->name, get1->id);
free(get1);//注意回收内存
system("pause");
}
结构体指针作为函数的返回类型,前提是该指针不是指向栈内存的,也就是说,该指针不是指向局部结构体变量的。与返回结构体变量相比,返回结构体指针大大节省了函数返回时创建副本的时空开销,有较高的效率。
4. return局部变量的合法性
函数返回的副本机制解释了return一个局部变量的合法的原因。比如,
int sum(int a, int b)
{
int c = a + b;
printf("%d,%x\n",c, &c);
return c;
}
void main()
{
int d = sum(1, 2);
printf("%d,%x\n",d, &d);
printf("暂停");
system("pause");
}
int d = sum(1,2);这句先执行函数sum,sum函数执行完毕后将结果赋值给int型变量d。字面上理解,是将c赋值给d。实际上,在执行赋值操作时,由于函数sum已经后执行完毕返回,函数中的局部变量c已被回收内存,不存在了。实际上,在c的内存被回收时,函数已经为返回值c创建了副本,保存在特定的位置上,赋值操作是由该位置处的副本完成的。
void test1(int num)
{
int x = 10;
printf("%d,%d\n",num, x);
printf("%p,%p\n",&num, &x);
printf("\n");
printf("函数结尾");
}
void main()
{
test1(2);
printf("暂停");
system("pause");
}
函数返回值也可以认为存在传值和传址两种方式。函数返回同样也是根据副本机制来处理的。函数的返回流程,当执行到return语句时,return的值被复制到某个内存单元或寄存器中,其地址由编译器来维护的,程序员无法直接访问该地址,也就是说,当程序执行完毕,相关现场被撤销前,返回的值被复制保存到了某个地方,编译器访问该位置即可知道函数的返回值。该位置即可看成是函数中返回值的副本。
对函数返回取地址是不合法的,比如函数,int A(int b,int c);不允许使用如下形式的语句,&A(3,4);因为为了函数的运行安全,编译器不允许访问返回值。
如果没有函数副本机制,&sum(1,2)就可以取。但是变量被回收了,值也就变了,变量被回收是为了节约内存,为了配合节约内存,高效利用计算机资源,出现了副本机制。
函数返回值的生命周期是函数执行结束。
5. 跨函数使用内存
动态申请内存是在堆中完成的,而函数返回不会释放堆内存。函数返回时,栈内存中的内容会被自动清楚,因此,不要返回指向栈内存的指针。
//不要返回指向占内存的指针
//栈返回的时候内容会被清除
//凡是没有用到malloc并且在函数内部,定义分配的数组或者变量,都是栈内存
//函数的参数也是栈内存
char *getmem()
{
char str[100] ="hhhhaass";//在栈上,系统自动分配回收
char *p = str;//存储数组的首地址
return p;//返回内存的首地址
}
void main()
{
char *pstr = NULL;
pstr = getmem();
if (pstr != NULL)
{
strcpy(pstr,"hwere");//拷贝成功了
//调用printf的时候,内存被回收利用了,所以无法显示
printf("%s\n",pstr);
}
system("pause");
}
为了跨函数使用内存,需要使用堆内存,可以使用内存的动态分配,使用内存。
char *getmemd()
{
//动态分配的内存,只有自己才能回收
char *p = (char*)malloc(100);
return p;
}
void main()
{
char *pstr = NULL;
pstr = getmemd();
if (pstr != NULL)
{
strcpy(pstr,"hwere");//拷贝成功了
//调用printf的时候,内存被回收利用了,所以无法显示
printf("%s\n",pstr);
}
free(pstr);//回收内存
system("pause");
}
当返回指向只读存储区的指针时,比如,返回的是指向常量字符串的只读存储区。此时,返回指向只读存储区的指针p并没有问题,但是该指针只能用于输出,而不能用于输入改写。
char *getmemz()
{
char *p ="hellosdfe";
printf("%p\n",&p);
return p;//p指向的是静态存储区的内存,p会被回收,但副本机制会将p指向的地址返回
}
void main()
{
char *pstr = NULL;
pstr = getmemz();
if (pstr != NULL)
{
//strcpy(pstr,"hwere");//返回的只读存储区的常量地址,不可写,只可读
printf("%s\n",pstr);
}
system("pause");
}
6. 函数与数组
数组是种使用广泛的数据结构,数组名和数组元素都可以作为函数的参数,实现函数间的数据传递和共享。此外,由于数组名和指针的对应关系,在一些需要指针型参数的场合,可以使用数组名(即常指针)作函数参数。
void shownum(int num)
{
printf("%d\n",num);
}
void showarr(int a[],int len)
{
for (int i = 0; i <len; i++)
{
printf("%d\n",a[i]);
}
}
void main()
{
int a[5] = { 1, 2, 3, 4, 5};
shownum(a[2]);
showarr(a, 5);
int *p = a;//数组名是常量指针,p是一个变量指针
showarr(p, 5);
system("pause");
}
使用数组名作为函数的参数,相比单个的传递基本类型变量,效率高,通用性强。
//传递单个实参,通用性不强,数组发生变化,需要再次改写
//传递参数过多,函数的副本机制进行拷贝的时候,浪费时间空间
float avgf(float a, float b, float c, float d, float e)
{
return (a + b + c + d + e)/ 5;
}
//通用性强,多个元素,无需改写
//数组没有副本机制,为了节约时间空间,c语言对于数组名作为参数只传递地址
//只传递两个参数效率高。
float avgarr(float f[], int len)
{
float sum = 0;
for (int i = 0; i <len; i++)
{
sum += f[i];
}
return sum / len;
}
void main()
{
printf("%f\n",avgf(1.2, 3.4, 3.5, 5.6, 6.4));
float f[5] = { 1.2, 3.4,3.5, 5.6, 6.4 };
printf("%f\n",avgarr(f, 5));
system("pause");
}
数组名既可以作为函数的形参,也可以作为函数的实参,数组名实际上是数组元素的首地址,所以,数组名作函数参数属于地址调用。当用数组名作为函数实参时,函数形参会接收到此数组的首地址。这就意味着函数形参数组首地址等于实参数组首地址。在函数中对形参数组的操作直接作用于实参数组。
//数组名作为函数参数是传址调用
//会改变原来数组的值
void changearr(int num[], int len)
{
printf("%d\n",sizeof(int));
printf("changarr,%p\n",num);
for (int i = 0; i <len; i++)
{
num[i] = i + 10;
}
}
void main()
{
int a[5] = { 1, 2, 3, 4, 5};
for (int i = 0; i < 5;i++)
{
printf("%d\n",a[i]);
}
int len = sizeof(a) /sizeof(int);
printf("main,%p\n",a);
changearr(a, len);
for (int i = 0; i < 5;i++)
{
printf("%d\n",a[i]);
}
system("pause");
}
逆序存放数组
void nixu(int arr[], int len)
{
//逆序数组的思路,将原数组的首尾元素增减互换
for (int i = 0; i < len/ 2; i++)
{
int temp;
temp = arr[i];
arr[i] = arr[len- 1 - i];
arr[len - 1 - i]= temp;
}
//或者使用指针的方式,因为数组作为函数参数,退化为指针
/*for (int i = 0; i<len / 2; i++)
{
int temp;
temp = *(arr +i);
*(arr + i) =*(arr + len - 1 - i);
*(arr + len - 1- i) = temp;
}*/
}
void nixup(int *arr, int len)
{
for (int i=0; i <len /2; i++)
{
int temp;
temp = *(arr +i);
*(arr + i) =*(arr + len - 1 - i);
*(arr + len - 1- i) = temp;
}
//或者,直接使用数组的形式
/*for (int i = 0; i <len / 2; i++)
{
int temp;
temp = arr[i];
arr[i] = arr[len- 1 - i];
arr[len - 1 - i]= temp;
}*/
}
void main()
{
int a[] = { 12, 34, 23, 6,234, 632 };
int size = sizeof(a) /sizeof(int);
for (int i = 0; i <size; i++)
{
printf("%-6d",a[i]);
}
printf("\n逆序后\n");
nixu(a, size);
for (int i = 0; i <size; i++)
{
printf("%-6d",a[i]);
}
printf("\n使用指针逆序后\n");
nixup(a, size);
for (int i = 0; i <size; i++)
{
printf("%-6d",a[i]);
}
printf("\n将指针指向数组,传递指针逆序后\n");
int *p = a;
nixu(p, size);
for (int i = 0; i <size; i++)
{
printf("%-6d",a[i]);
}
system("pause");
}
7. 二级指针作为函数参数
数组作为参数退化为指针
void show(int num[], int len)
{
for (int i = 0; i < 5;i++)
{
printf("%d\n",*(num+i));
}
}
void main()
{
int a[5] = { 1, 2, 3, 4, 5};
show(a, 5);
system("pause");
}
指针数组作为参数退化为二级指针
void showcmd(char *cmd[], int len)
{
for (int i = 0; i <len; i++)
{
printf("%s\n",cmd[i]);
}
}
//二级指针,参数中指针数组等价与二级指针
void showcmd2(char **cmd, int len)
{
for (int i = 0; i <len; i++)
{
printf("%s\n",cmd[i]);
}
}
void main()
{
//一个指针数组,每个元素是指针,每个指针保存字符串常量的地址
char *cmd[] = {"calc", "notepad", "ipconfig", "write","tasklist" };
printf("%d\n",sizeof(cmd));
showcmd(cmd, 5);
printf("使用二级指针作为形参接收指针数组\n");
showcmd2(cmd, 5);
system("pause");
}
使用二级指针访问指针数组
void main()
{
int a[5] = { 1, 2, 34, 5,6 };
for (int *p = a; p < a+ 5; p++)
{
printf("%d,%x\n",*p, p);
}
char *cmd[] = { "calc","notepad", "ipconfig", "write","tasklist" };
for (char **p = cmd; p< cmd + 5; p++)
{
printf("%s,%x,%c\n",*p, p, **p);
}
system("pause");
}
8. 函数执行流程
独立的函数是挨个挨个执行的。函数内存每当调用函数的时候,必须要等待函数执行完成之后,再执行下一步。函数的调用是通过栈来实现的。
比如
void main()
{
printf("1\n");
printf("2\n");
printf("3\n");
system("pause");
}
9. 递归
函数自己调用自己。递归要成功,离不开两点,一是递归递进机制,二是递归终止条件,递进机制应保证每次调用终止靠近一步。比如,f(5)调用f(4),f(4)调用f(3),一步一步靠近f(1)这一终止条件。这保证了递归正常结束,不至于出现无限循环,导致系统内存耗尽而崩溃。函数是在栈中运行的,vc编译器中默认为栈分配的大小为1Mb,如果超过1Mb,就会栈溢出,stack overflow。
通常用参数对调用过程进行判断,以便在合适的时候切断调用链。
//go(1)=1,go(2)=1*2,go(3)=1*2*3,终止条件,go(1)=1
//go(n)=n*go(n-1)循环要素
int go(int n)
{
if (n == 1)
{
return 1;
}
else
{
return n * go(n- 1);
}
}
void main()
{
int res = go(4);
printf("%d\n",res);
system("pause");
}
比如,上楼梯问题。一个50阶梯的楼梯,一次可以走1阶或2阶,问多少种走法。
数组,循环,递归都可以解决。
递归
int louti(int jieshu)
{
if (jieshu == 1)
{
return 1;
}
else if (jieshu == 2)
{
return 2;
}
else
{
returnlouti(jieshu - 1) + louti(jieshu - 2);
}
}
void main()
{
printf("%d\n",louti(4));
//数组方式
int a[10] = { 1, 2 };
for (int i = 2; i < 10;i++)
{
a[i] = a[i - 1]+ a[i - 2];
}
printf("%d\n",a[3]);
system("pause");
}
判断递增
void main()
{
int a[10] = { 1, 2, 3, 6,5, 6, 7, 8, 9, 10 };
//判断数组是递增还是递减
int flag = 1;//假定是递增
for (int i = 0; i < 10- 1; i++)
{
if (a[i] >=a[i + 1])
{
flag =0;
break;
}
}
if (flag)
{
printf("递增\n", flag);
}
else
{
printf("非递增\n", flag);
}
system("pause");
}
int louti(int jieshu)
{
if (jieshu == 1)
{
return 1;
}
else if (jieshu == 2)
{
return 2;
}
else
{
returnlouti(jieshu - 1) + louti(jieshu - 2);
}
}
使用递归判断递增
int dizeng(int a[], int len, int i)
{
//思路1,返回a[i]<a[i+1]的每次判断的与的结果,只要有一次是假,则结果为假
printf("%x,%d,%d\n",a,len,i);
/**/if (i == len - 2)
{
return a[i] <a[i + 1];
}
else
{
//由于&&的短路运算,一旦a[i]<a[i+1]不成立,就不会再递归下去,并返回结果
return (a[i]< a[i + 1]) && dizeng(a,len,++i);
}//*/
//思路2,按照正常逻辑,每次进行判断,只要有一次是假,就返回0,否则执行到最后没有返回0就是返回1
/**if (i == len - 1)
{
return 1;
}
else
{
if (a[i] >a[i + 1])
{
return0;
}
else
{
returndizeng(a, len, ++i);
}
}//*/
}
void main()
{
int a[10] = { 1, 2, 3, 4,5, 6, 7, 8, 9, 10 };
printf("%x\n",a);
int flag = dizeng(a, 10,0);
printf("%d\n",flag);
if (flag)
{
printf("递增\n", flag);
}
else
{
printf("非递增\n", flag);
}
system("pause");
}
将一个整数逆置,比如123,变成321。
void main()
{
int num;
scanf("%d",&num);
printf("num=%d\n",num);
int res = 0;
int wei = 0;
while (num)
{
res *= 10;
wei = num % 10;
res += wei;
num /= 10;
}
printf("res=%d\n",res);
system("pause");
}
使用递归将一个整数逆置
int nizhi(int num, int res)
{
if (num == 0)
{
return res;
}
else
{
//printf("%d\n",res);
res *= 10;
int wei = num %10;
res += wei;
return nizhi(num/ 10, res);
}
}
void main()
{
int num;
scanf("%d",&num);
printf("num=%d\n",num);
int res = nizhi(num,0);
printf("res=%d\n",res);
system("pause");
}
10. 递归解决汉诺塔问题
有3个座a、b、c,开始时在a座上有64个盘子,盘子大小不等,大的在下,小的在上。把64个盘子从a座移动到c座,规定每次只允许移动一个盘子,且在移动过程中在3个座上都始终保持大盘在下,小盘在上。在移动过程中可以利用b座。编程实现移动盘子的步骤。
分析,如果只有1个盘子,是移动一次;如果是2个盘子,移动3次;如果是3个盘子,是移动7步。推测,移动的公式是2的n次方-1。
递推移动的机制,将移动n个盘子看成是移动第n个和n-1个,那么,将n-1看成是一个整体,相当于移动2个盘子,从示意图中可以看出移动n个盘子将相当于将n-1个从a移动到b的次数加上从b移动到c的次数再加上第n个移动的1次,相当于前n-1个盘子整体移动了两次,而在这种从一个底座移动到另一个底座的最短算法是一样多的,也就是前n-1个盘子两次移动到c,算术表示就是2*(n-1移动到c的次数),总体上移动n个盘子,也就是相当于:2*(前n-1个盘子移动到c盘的次数)+1。n和n-1这个整体移动到c示意如
a b c
n-1
开始时 n
第1次移动 n n-1
第2次移动 n-1 n
n-1
第3次移动 n
那么,将n个盘子移动到c的结果就是2*(n-1个的移动到c的次数)+1,如此类推形成递归。递归结束的条件是n=1时为1。当n=2时为3,注意n=2时的情况也满足这个公式,所以以n=1为递归结束条件。从简单的数验证就是,比如n=3时,总次数等于到2*3+1等于7。
起始 a b c
kai 1
ss 2
起始 3
ss
ss 2
ss 3 1
第1次
ss
ss 3 2 1
第2次
ss 1
第3次 3 2 (此时已经将前2个作为整体移动到了b,也相当于移动到c的次数,也就是移动了3次)
ss 1
第4次 2 3 (这一次是将3移动到目标位置)
ss
第5次 1 2 3 (接下来就是将前2个移动到c,也是3次)
ss 2
第6次 1 3
1
ss 2
第7次 3
那么模拟移动的步骤,根据示意图
a b c
n-1
开始时 n
第1次移动 n n-1 (也就是n-1已经移到到c的次数之后)
第2次移动 n-1 n
n-1
第3次移动 n
如果n大于1时,实际上是先移动n-1从a到b,再移动n到c,再移动n-1从b到c。
那么,编程实现
int hannuota(int a)
{
if (a == 1)
{
return 1;
}
else
{
return 2 *hannuota(a - 1) + 1;
}
}
//设计递归函数,实现模拟移动的步骤
void hannuotazoufa(char a, char b, char c, int num)
{
if (num == 1)
{
printf("%c->%c\n",a, c);
}
else
{
hannuotazoufa(a,c, b, num - 1);
printf("%c->%c\n",a, c);
hannuotazoufa(b,a, c, num - 1);
}
}
void main()
{
int panzi = 4;
printf("请输入汉诺塔的盘子\n");
scanf("%d",&panzi);
int res = hannuota(panzi);
printf("%d个盘子的汉诺塔移动次数为%d\n",panzi, res);
printf("移动的方式为\n");
hannuotazoufa('a', 'b','c', panzi);
system("pause");
}
递归的效率和可读性
递归调用中,每次函数调用都有一定的堆栈操作,相比循环方式,时空开销大,因此,递归函数的效率总是比功能相同的循环结构略低,递归编写的函数尽量用循环代替,因为节约了函数调用和返回的时间空间,以提高效率。
递归的好处是,一是程序的可读性好,易于修改和维护,二是在不断的函数调用中,函数的规模在减小,在求阶乘例子中,不断用复杂度n-1的问题来描述复杂度为n的问题,直到问题可以直接求解为止。如果用循环语句实现的算法比使用递归复杂得多,考虑到复杂度和效率的折合,可以考虑采用递归方式来解决。
所有的循环都可以用递归实现,递归不一定都可以用循环实现,如果一定要用循环实现,需要使用到栈。
使用数组实现计算移动的次数
void main()
{
int panzi = 4;
printf("请输入汉诺塔的盘子\n");
scanf("%d",&panzi);
int res = 0;
for (int i = 0; i <panzi; i++)
{
res = 2 * res +1;
}
printf("循环的汉诺塔步数%d\n",res);
system("pause");
}
使用非递归实现走法就很复杂。
11. 函数的阻塞与非阻塞模式
阻塞模式是执行完一步,再执行下一步。非阻塞模式,就是无阻塞的执行下去。
void main()
{
while (1)
{
system("notepad");//同步执行,阻塞模式
system("startnotepad");//异步执行,非阻塞模式
}
}
12. detours
detours是一款微软的信息安全产品。作用,根据函数指针改变函数的行为。可以用来拦截函数,包括操作系统函数。安装detours。进入安装好的detours的src目录,执行nmake。在上一级文件路径下,编译了lib.x86目录放库文件,include目录放头文件。
将vs2013设置为release模式。在项目中,设置属性,vc++目录,包含目录,将detours编译好的头文件的目录include添加到包含目录,库目录,将detours编译好的库文件的目录lib.x86添加到库目录。在外部依赖项中可以查看detours.h。
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<detours.h>//包含头文件,以调用
#pragma comment(lib, "detours.lib")//加载函数库,载入函数的实体,便于调用
//detours仅仅适用于release才能生效,debug不能生效,因为很多调试也用了劫持
static int (WINAPI *pOLDMessageBoxW)
(
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption, //函数指针类型与MessageBoxW的类型一致
UINT uType) =MessageBoxW;//定义一个函数指针存储MessageBoxW的入口点地址
int WINAPI NEWMessageBoxW(
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption,
UINT uType)//自己实现一个MessageBoxW类型一样的,参数,返回值都一致
{
printf("发生函数劫持\n");
return 0;//让MessageBoxW失效
}
//拦截
void hook()
{
DetourRestoreAfterWith();//恢复原来的状态,重新开始劫持
DetourTransactionBegin();//拦截开始
DetourUpdateThread(GetCurrentThread());//刷新当前的线程
DetourAttach((void**)&pOLDMessageBoxW, NEWMessageBoxW);//传入旧的函数指针的地址,转换为指向空指针的指针
//修改地址跳转到新的函数
DetourTransactionCommit();//拦截生效
}
//反拦截
void unhook()
{
DetourRestoreAfterWith();//恢复原来的状态,重新开始劫持
DetourTransactionBegin();//拦截开始
DetourUpdateThread(GetCurrentThread());//刷新当前的线程
//DetourAttach((void**)&pOLDMessageBoxW, NEWMessageBoxW);//传入旧的函数指针的地址,转换为指向空指针的指针
//撤销修改地址跳转到新的函数
DetourDetach((void**)&pOLDMessageBoxW, NEWMessageBoxW);//传入旧的函数指针的地址,转换为指向空指针的指针
DetourTransactionCommit();//拦截生效
}
void main()
{
MessageBoxW(0, L"还是大幅度", L"还是地方", 0);//弹出对话框
//改变函数的行为
hook();
MessageBoxW(0, L"还是大幅度", L"还是地方", 0);//弹出对话框
unhook();
MessageBoxW(0, L"还是大幅度", L"还是地方", 0);//弹出对话框
system("pause");
}
劫持外部程序的函数
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<detours.h>//包含头文件,以调用
#pragma comment(lib, "detours.lib")//加载函数库,载入函数的实体,便于调用
//detours仅仅适用于release才能生效,debug不能生效,因为很多调试也用了劫持
static int (WINAPI *pOLDMessageBoxW)
(
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption, //函数指针类型与MessageBoxW的类型一致
UINT uType) = MessageBoxW;//定义一个函数指针存储MessageBoxW的入口点地址
int WINAPI NEWMessageBoxW(
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption,
UINT uType)//自己实现一个MessageBoxW类型一样的,参数,返回值都一致
{
printf("发生函数劫持\n");
return 0;//让MessageBoxW失效
}
//拦截
void hook()
{
DetourRestoreAfterWith();//恢复原来的状态,重新开始劫持
DetourTransactionBegin();//拦截开始
DetourUpdateThread(GetCurrentThread());//刷新当前的线程
DetourAttach((void**)&pOLDMessageBoxW, NEWMessageBoxW);//传入旧的函数指针的地址,转换为指向空指针的指针
//修改地址跳转到新的函数
DetourTransactionCommit();//拦截生效
}
_declspec(dllexport) void go()
{
hook();
}
劫持多个函数
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<detours.h>//包含头文件,以调用
#pragma comment(lib, "detours.lib")//加载函数库,载入函数的实体,便于调用
//detours仅仅适用于release才能生效,debug不能生效,因为很多调试也用了劫持
static int (WINAPI *pOLDMessageBoxW)
(
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption, //函数指针类型与MessageBoxW的类型一致
UINT uType) = MessageBoxW;//定义一个函数指针存储MessageBoxW的入口点地址
int WINAPI NEWMessageBoxW(
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption,
UINT uType)//自己实现一个MessageBoxW类型一样的,参数,返回值都一致
{
printf("发生函数劫持\n");
return 0;//让MessageBoxW失效
}
//拦截system函数
//int __cdecl system(_In_opt_z_ const char * _Command);//system的原型
int(__cdecl *oldsystem)(const char * _Command) = system;
//新的system函数
int __cdecl newsystem(_In_opt_z_ const char * _Command)
{
return 0;
}
//拦截
void hook()
{
DetourRestoreAfterWith();//恢复原来的状态,重新开始劫持
DetourTransactionBegin();//拦截开始
DetourUpdateThread(GetCurrentThread());//刷新当前的线程
DetourAttach((void**)&pOLDMessageBoxW, NEWMessageBoxW);//传入旧的函数指针的地址,转换为指向空指针的指针
DetourAttach((void**)&oldsystem, newsystem);
//修改地址跳转到新的函数
DetourTransactionCommit();//拦截生效
}
_declspec(dllexport) void go()
{
hook();
}
劫持本程序的进程函数
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<detours.h>//包含头文件,以调用
#pragma comment(lib, "detours.lib")//加载函数库,载入函数的实体,便于调用
//定义指针指向原函数
BOOL (WINAPI *oldCreateProcessW)(
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTESlpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTRlpCurrentDirectory,
LPSTARTUPINFOWlpStartupInfo,
LPPROCESS_INFORMATIONlpProcessInformation
) = CreateProcessW;
//定义一个新的CreateProcessW函数
BOOL WINAPI newCreateProcessW(
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTESlpProcessAttributes,
LPSECURITY_ATTRIBUTESlpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTRlpCurrentDirectory,
LPSTARTUPINFOWlpStartupInfo,
LPPROCESS_INFORMATIONlpProcessInformation
)
{
printf("发生拦截\n");
return 0;
}
//拦截函数
void hook()
{
DetourRestoreAfterWith();//恢复原来的状态,重新开始劫持
DetourTransactionBegin();//拦截开始
DetourUpdateThread(GetCurrentThread());//刷新当前的线程
DetourAttach((void**)&oldCreateProcessW, newCreateProcessW);//传入旧的函数指针的地址,转换为指向空指针的指针
//修改地址跳转到新的函数
DetourTransactionCommit();//拦截生效
}
//反拦截
void unhook()
{
DetourRestoreAfterWith();//恢复原来的状态,重新开始劫持
DetourTransactionBegin();//拦截开始
DetourUpdateThread(GetCurrentThread());//刷新当前的线程
//DetourAttach((void**)&pOLDMessageBoxW, NEWMessageBoxW);//传入旧的函数指针的地址,转换为指向空指针的指针
//撤销修改地址跳转到新的函数
DetourDetach((void**)&oldCreateProcessW, newCreateProcessW);//传入旧的函数指针的地址,转换为指向空指针的指针
DetourTransactionCommit();//拦截生效
}
void main()
{
STARTUPINFO si = {sizeof(si) };//创建结构体初始化第一个元素
si.dwFlags =STARTF_USESHOWWINDOW;//选择窗口模式
si.wShowWindow = 1;//显示窗口
PROCESS_INFORMATION pi;//进程信息的结构体保存进程信息
wchar_t cmdline[100] =L"notepad";
//创建一个进程
CreateProcess(NULL,cmdline, NULL, NULL, 0, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
//拦截
hook();
CreateProcess(NULL,cmdline, NULL, NULL, 0, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
//反拦截
unhook();
CreateProcess(NULL,cmdline, NULL, NULL, 0, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
system("pause");
}
劫持系统函数
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<detours.h>//包含头文件,以调用
#pragma comment(lib, "detours.lib")//加载函数库,载入函数的实体,便于调用
//定义指针指向原函数
BOOL(WINAPI *oldCreateProcessW)(
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTESlpProcessAttributes,
LPSECURITY_ATTRIBUTESlpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTRlpCurrentDirectory,
LPSTARTUPINFOWlpStartupInfo,
LPPROCESS_INFORMATIONlpProcessInformation
) = CreateProcessW;
//定义一个新的CreateProcessW函数
BOOL WINAPI newCreateProcessW(
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTESlpProcessAttributes,
LPSECURITY_ATTRIBUTESlpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTRlpCurrentDirectory,
LPSTARTUPINFOWlpStartupInfo,
LPPROCESS_INFORMATIONlpProcessInformation
)
{
MessageBoxW(0, L"系统故障,请联系管理员","window系统通知", 0);
return 0;
}
//拦截函数
void hook()
{
DetourRestoreAfterWith();//恢复原来的状态,重新开始劫持
DetourTransactionBegin();//拦截开始
DetourUpdateThread(GetCurrentThread());//刷新当前的线程
DetourAttach((void**)&oldCreateProcessW, newCreateProcessW);//传入旧的函数指针的地址,转换为指向空指针的指针
//修改地址跳转到新的函数
DetourTransactionCommit();//拦截生效
}
//反拦截
void unhook()
{
DetourRestoreAfterWith();//恢复原来的状态,重新开始劫持
DetourTransactionBegin();//拦截开始
DetourUpdateThread(GetCurrentThread());//刷新当前的线程
//DetourAttach((void**)&pOLDMessageBoxW, NEWMessageBoxW);//传入旧的函数指针的地址,转换为指向空指针的指针
//撤销修改地址跳转到新的函数
DetourDetach((void**)&oldCreateProcessW, newCreateProcessW);//传入旧的函数指针的地址,转换为指向空指针的指针
DetourTransactionCommit();//拦截生效
}
_declspec(dllexport) void go()
{
hook();
}
二十三、 生存期、作用域和可见域
1. 内存分配
变量名,数组名,函数名等都对应着内存中的一块区域。
void fun1()
{
printf("fun1\n");
}
void main()
{
int a[5] = { 1, 2, 3, 4, 5};
printf("%p\n",a);
printf("%p\n",fun1);
system("pause");
}
2. 变量的存储类别
c语言中,变量的存储类别大致分为4种,auto(自动)、register(寄存器)、static(静态)和extern(外部)。其中,auto和register变量属于自动分配方式,而static和extern变量属于静态分配方式。不同的分配方式下,变量的生存期、作用域和可见域各不相同。
按作用域分,变量可分为局部变量和全局变量,所谓局部变量,是指在函数内部定义的变量,局部变量仅在定义它的函数内才能有效使用,其作用域仅限在函数内,即从变量定义的位置开始,到函数整体结束。通常,编译器不为局部变量分配内存单元,而是在程序运行中,当局部变量所在的函数被调用时,系统根据需要临时为其分配内存。当函数执行结束时,局部变量被撤销,占用内存被收回。
在函数外部定义的变量称为全局变量,也称外部变量,全局变量的作用域较广,全部变量不属于任何一个函数,理论上可被其作用域中的所有函数访问,因此,提供了一个不同函数间联系的途径,使函数间的数据联系不只局限于参数传递和return语句。全局变量一经定义,编译器会为其分配固定的内存单元,在程序运行期间,这块内存单元始终有效,一直到程序执行完毕才由操作系统收回这块内存。
3. 局部变量
在函数内定义的变量,其作用范围局限于此函数体内部,这种变量叫局部变量,函数的形参、在main函数中定义的变量也是局部变量。
局部变量在函数调用时由系统分配存储区,在不同的函数中同名的变量实际上在内存中占不同的单元,因此在不同的函数中可以定义相同名字的局部变量。
不同函数的同名变量,有些编译器为了优化,所以地址是一样的,它们的生命周期不一样,所以不会混淆。
void jubu1(int num)
{
int a = 10;//局部变量
num = 2;//局部变量
printf("%d,%x, %d,%x\n", a, &a, num, &num);
printf("\n");
}
void jubu2(int num)
{
int a = 12;//局部变量
num = 2;//局部变量
printf("%d,%x, %d,%x\n", a, &a, num, &num);
printf("\n");
}
void main()
{
int a = 5;
printf("%x\n",&a);
jubu1(2);
printf("\n");
jubu2(3);
printf("\n");
system("pause");
}
生命周期有重合的变量,就需要安排不同的地址,加以区分。
不同的函数,可以定义同名的局部变量。如果生命周期不重合,编译器为了优化,会使用同以内存单元。生命周期重合,就会使用不同的内存单元,这个时候就不会冲突。
不同的块语句,可以定义重名的局部变量,编译器会自动安排不同的地址。
void main()
{
int num = 11;
printf("main=%x\n",&num);
{
int num = 11;
printf("main=%x\n",&num);
}
system("pause");
}
void func(int x, int y)
{
int z = 10;
x += 10;
y += 10;
if (z)
{
int z = 20;
printf("x =%d,y = %d,z = %d\n", x, y, z);
}
printf("x = %d,y =%d,z = %d\n", x, y, z);
}
void main()
{
int x = 10, y = 10, z =10;
func(x, y);//函数除非传址,否则因为副本机制,不会改变xy。
printf("x = %d,y =%d,z = %d\n", x, y, z);
system("pause");
}
4. 全局变量
在任何大括号外定义的变量叫做全局变量,其作用范围是从定义处直到文件末尾。
由于全局变量作用范围很大,若在某个函数中修改了全局变量的值,在其它函数中读出的就是这个新值。一般使用大写字母命名全局变量,可以区分局部变量。
int x, y;
void func2()
{
int a = 20, b = 10;
x = x + a + b;
y = y + a - b;
}
void main()
{
int a = 5, b = 3;
x = a + b;
y = a - b;
func2();
printf("%d,%d\n",x, y);
system("pause");
}
int num = 100;//全局变量,在内存中一直存在,直到程序结束
//局部变量,只有调用函数的时候,才会进行分配,函数调用完成以后就回收
//给全局变量分配内存优先于main函数
void printfd()
{
int d = 10;
printf("%p,%d\n",&d, d);
printf("%p,%d\n",&num, num);
printf("\n");
}
void main()
{
printf("%p\n",&num);
printfd();
printf("\n");
printfd();
printf("\n");
system("pause");
}
5. 生存期
生存期是在程序运行过程中,变量从创建到撤销的一段时间。生存期的长短取决于存储方式,对于自动分配(栈分配),变量与其所在的代码块共存亡;对于静态分配(编译器预分配),变量与程序共存亡,程序开始执行时即已存在,一直到程序运行完毕退出后才撤销;对于动态存储的内存块(注意,不是指向该内存块的指针),由程序员决定其生存期(使用free释放就撤销)。
对程序代码区的函数、常量区的字符串常量和其他常量等、结构体和共用体的定义等来说,生存期的讨论没有意义,它们都是与程序共存亡的。
栈分配
void go()
{
//自动分配的方式,在栈上,系统自动回收
//函数调用的时候,从定义的地方开始创建,函数结束时系统自动进行回收
int a[5] = { 12, 3, 4, 5,6 };
printf("%p\n",a);
printf("\n");
}
void main()
{
go();
printf("\n");
system("pause");
}
静态分配
//静态分配,生命周期是整个程序执行周期
//内存会一直存在,man函数执行之前就创建,无论函数如何运行
//如何调用,内存一直不会被回收,一直到程序运行结束才被系统回收
int numarr[3] = { 1, 2, 3 };
void go()
{
//自动分配的方式,在栈上,系统自动回收
//函数调用的时候,从定义的地方开始创建,函数结束时系统自动进行回收
int a[5] = { 12, 3, 4, 5,6 };
printf("%p\n",a);
printf("%p\n",numarr);
printf("\n");
}
void main()
{
go();
printf("\n");
system("pause");
}
动态分配
void * getmem()
{
void *p =malloc(sizeof(int)* 10);
printf("%p\n",p);
return p;
}
void main()
{
void *px = getmem();
int *pint = (int *)px;//指针转换
for (int i = 0; i < 10;i++)
{
pint[i] = i;//对动态数组进行赋值
}
free(pint);//手动回收动态内存
system("pause");
}
6. 作用域与可见域
在程序代码中,变量有效的范围(源程序区域)称为作用域,能对变量、标识符进行合法的访问的范围(源程序区域)称为可见域。作用域是变量理论上有效的区域,而可见域是变量实际有效的区域,可见域是作用域的子集。
c语言的作用域分为3类,
块作用域,自动变量(auto、register)和内部静态变量(static)具有块作用域,在一个块内声明的变量,其作用域从声明点开始,到该块结束为止。函数定义中声明的形参,其作用域限定在该函数体内,与其他函数中声明的同名变量不是一回事,允许在不同的函数中使用相同的变量名,编译器将为这些变量分配不同的存储单元,不会混淆。
文件作用域,外部静态变量具有文件作用域,从声明点开始到文件结尾,此处所指的文件是编译基本单位-c文件。
全局(程序)作用域,全局变量(extern)具有全局作用域,只要在使用前对其进行声明,便可在程序(由若干个文件组成)的任意位置使用全局变量。
void main()
{
int num = 12;//num的作用域是从定义开始到main函数结束,此时作用域与可见域是一样的
printf("%d\n",num);
{
printf("%d\n",num);
}
printf("%d\n",num);
system("pause");
}
比较num的可见域发生变化
void main()
{
int num = 12;//num的作用域是从定义开始到main函数结束,此时作用域与可见域是一样的
printf("%d\n",num);
{
int num = 3;//与局部变量重名,被屏蔽
printf("%d\n",num);
}
printf("%d\n",num);
system("pause");
}
块作用域
void go3(int a)
{
int num;//num的作用域从这行开始到函数结束
num = 10;
a = 12;//a的作用域是整个函数的块语句
printf("%d,%p\n",num, &num);
printf("%d,%p\n",a, &a);
}
//生存期重合的函数,编译器为同名的变量分配不同的内存单元
//生存期不重合的函数,有时为了优化,用一个内存单元
//块语句结束的时候,内部变量都会被回收
void test(int a)
{
printf("%d,%p\n",a, &a);
}
void main()
{
int num = 3;
printf("%d,%p\n",num, &num);
go3(1);
test(1);
system("pause");
}
文件作用域和全局作用域
只在本文件有效的变量,用static声明为静态的,作用域在整个c文件内,外部无法使用,即使使用extern也不能跨文件使用。
在1.c中
static int num2 = 10;
int data = 122;
void prt()
{
printf("%d\n",num2);
}
void main13()
{
prt();
printf("%d\n",num2);
system("pause");
}
在2.c中
#include<stdio.h>
extern int data;
//extern int num2;//对于静态变量无效的
void main()
{
printf("%d\n",data);
//printf("%d\n",num2);
system("pause");
}
作用域与可见域受到大括号,特别是嵌套的大括号的影响。
成对的大括号及内部的语句组成了程序块。函数体、选择和循环结构中都要用大括号表示边界。
大括号内定义的变量,必须紧跟在左大括号后面,其作用范围是从定义处直到配对的右大括号之前。若多层大括号中出现重名变量,外层同名变量在内层中被屏蔽不起作用。
7. auto变量
函数的形参以及代码中定义的变量都属于auto变量,这是c语言中应用最广的一种变量,这类变量是栈分配的,是动态分配存储空间的。举函数形参为例,当调用函数时,为形参分配存储空间,当函数调用结束时,就自动释放这些存储空间。对代码块中定义的变量(包含函数中定义的变量),当执行到变量声明语句时,系统为这些auto变量分配空间,当程序流程离开代码块时,这些变量被自动撤销,其占用的内存空间被释放。
自动变量的定义格式为:
[auto]数据类型 变量1[=初始化表达式],变量2=[=初始化表达式]……;
其中,方括号表示可以省略,此处变量不仅指普通内置类型的变量,还包括数组、结构体和指针等复合结构。
c语言默认所定义的变量是auto变量,在定义变量时没有使用auto,实际上是遵循了c语言的默认规定。比如int a;float b;自动被c编译器解释为:auto int a;auto float b;
//自动变量,在函数的块语句内部,或者是函数参数
//函数调用的时候,参数会自动分配内存,函数执行完成后就自动回收
//函数内部块语句的局部变量,定义时分配变量,执行完成后自动回收
void test3(int num)//局部变量
{
int d = 12;//局部变量
printf("%p,%p\n",&d, &num);
printf("\n");
}
void main()
{
int d = 3;//局部变量
test3(3);
printf("\n");
test3(3);
printf("\n");
system("pause");
}
定义自动变量
int quan1 = 100;//不是自动变量
struct info
{
char name[32];
int id;
};
union inun
{
int num;
double r;
};
void maina(int num)
{
int a, b;//在函数块语句内部,会被自动解析为auto int a,b;
}
void main()
{
auto int a, b;
auto int a1 = 3, b2 = 32;
int a[3] = { 1, 2, 3};//auto int a[3];
int *p = a;//自动变量auto int *p = a;
struct info i1;//自动变量auto struct info i1;
struct info i1[3];//自动变量auto struct info i1[3];
union inun in1;//自动变量auto union inun in1;
union inun in1[3];//自动变量auto union inun in1[3];
}
auto变量的作用域和生存期都局限在定义它的代码块中,代码块就是指用两个花括号包裹起来的代码行,函数只是代码块的一种,常见的代码块还有if结构、for结构等,即便只是两个成对花括号,也能构成一个独立代码块。
结合先声明、后使用的原则,auto变量的作用域和生存期对应着从定义到所在代码块结束的时空区域。
void auto1(int num)
{
printf("%d,%x\n",num, &num);
printf("%d\n",num);//auto变量的作用域在花括号内
}
void main()
{
auto1(3);
printf("\n");
system("pause");
}
void auto2()
{
int i2;
printf("%d,%p\n",i2, &i2);
}
void main()
{
auto2();
printf("\n");
system("pause");
}
void auto3()
{
{
int i3 = 3;
printf("%d,%p\n",i3, &i3);
}
//printf("%d,%p\n",i3, &i3);//i3的作用域在定义它的大花括号内
}
void main()
{
auto3();
printf("\n");
system("pause");
}
局部变量的屏蔽
//块语句嵌套的时候,两个块语句可以用重名的变量,内部块语句变量的作用域内屏蔽外部变量
void main()
{
int a = 3;
printf("%d,%x\n",a, &a);
{
printf("%d,%x\n",a, &a);
int a = 4;//作用域重合,内部重名变量屏蔽外部变量
printf("%d,%x\n",a, &a);
{
printf("%d,%x\n",a, &a);
int a =5;//作用域重合,内部重名变量屏蔽外部变量
printf("%d,%x\n",a, &a);
}
printf("%d,%x\n",a, &a);
}
printf("%d,%x\n",a, &a);
system("pause");
}
auto变量不能重复定义,重复指在同一代码块中,出现两个同名变量。这里同一代码块,不包括屏蔽的情况。
比如
if
{
int a;
double a;
}
就是重复定义auto变量。
并列层次的代码块中可以出现同名变量而不会引起混淆。最普遍的一个例子就是函数,由于所有的函数都是在外部定义的,包括main函数在内的所有函数都是并列的,因此,函数a定义的auto变量在函数b内是不可见的,所以,即使两个函数定义了同名变量,编译器也能很好地将其区分开,方便了函数的编写。
auto变量的初始化,某些编译器并不会自动为auto变量初始化,这项工作必须在变量定义时由程序员显示完成,否则,变量的值是随机不确定的。不论是指针还是普通变量,注意初始化。这不同于java中会自动初始化的情况,加以区别。变量必须初始化,否则产生不可预测的严重错误。
比如,错误的例子
void main()
{
int num;
printf("%d,%p\n",num, &num);//错误的例子
system("pause");
}
8. register寄存器变量
在程序运行时,根据需要到内存中相应的存储单元中调用,如果一个变量在程序中频繁使用,例如循环变量,那么,系统就必须多次访问内存中的单元,影响程序的执行效率。因此,c\c++语言定义了一种变量,不是保存在内存上,而是直接存储在cpu中的寄存器上,这种变量称为寄存器变量。
寄存器变量的定义形式是,register 类型标识符 变量名
寄存器是与机器硬件密切相关的,不同类型的计算机,寄存器的数目是不一样的,通常为2到3个,对于在一个函数中声明的多于2到3个的寄存器变量,c编译程序会自动地将寄存器变量变为自动变量。
由于受硬件寄存器长度的限制,所以寄存器变量只能是char、int或指针型。寄存器说明符只能用于声明函数中的变量和函数中的形参,因此,不允许将全局变量或静态变量声明为register。vc会自动进行寄存器变量优化。gcc需要手动指定。c语言寄存器变量不可以取地址,c++可以,因为c++会在内存给寄存器变量保留副本。
使用汇编实现用寄存器改变变量的值
void main()
{
int num = 10;
//num += 10;
//使用汇编实现num += 10,汇编是直接操作寄存器的
_asm
{
mov eax, num;
add eax, 15;
mov num, eax;
}
printf("%d\n",num);
system("pause");
}
使用register创建寄存器变量
void main()
{
for (register int i = 0; i< 5; i++)
{
printf("%d\n",i);
//printf("%x\n",&i);//由于i存储在寄存器中,是没有地址的,不可以取内存地址
}
system("pause");
}
vc自动优化变量为寄存器变量
测试时受硬件影响,不能同时测试,因为一个程序中,只有有限的变量可以成为寄存器变量,编译器会做一些调整,有时候即使声明为寄存器变量,在编译时会根据情况做调整。
void main()
{
time_t start1, end1, start2,end2;
time(&start1);//获取当前时间,放在start变量中
//Sleep(3000);
//频繁使用的变量会自动优化为寄存器变量
/**/
double res1 = 0.0;//结果
int i1 = 0;
for (; i1 < 1000000000;i1++)
{
res1 += i1;//每次相加
}//*/
printf("%f\n",res1);
time(&end1);//获取当前时间,放在end中
printf("不显示声明为register的执行时间%d\n",(unsigned int)(end1 - start1));//获取时间差
//比较使用寄存器变量时的时间是一样的
/**/
time(&start2);
register double res = 0.0;
register int i = 0;
for (; i < 1000000000;i++)
{
res += i;//每次相加
}//*/
printf("%f\n",res);
time(&end2);//获取当前时间,放在end中
printf("显示声明为register的执行时间%d\n",(unsigned int)(end2 - start2));//获取时间差
system("pause");
}
如果是gcc编译,需要手动加上register。gcc不会根据某个或某些变量的操作次数而自动将其变为寄存器变量。
9. extern变量
extern变量又称全局变量,放在静态存储区,全局是说该变量可以在程序的任意位置使用,其作用域是整个程序代码范围内,和auto变量不同的是,extern变量有定义和声明之分。
int quanju1 = 10;//编译器自动解析为extern int num = 10;
//全局变量可以被程序其他函数所引用,不仅局限于本文件,外部文件也可以引用
//在外部文件中声明之就可以使用
void qujufun1()
{
printf("%d\n",quanju1);
}
void main25()
{
printf("%d\n",quanju1);
system("pause");
}
在外部c文件中
extern int quanju1;
void p()
{
printf("%d\n",quanju1);
}
void main()
{
p();
system("pause");
}
全局变量的声明和定义可以分开
全局变量定义的基本格式为:extern 类型 变量名 = 初始化表达式;
此时,初始化表达式不可省略,此指令通知编译器在静态存储区中开辟一块指定类型大小的内存区域,用于存储该变量。c语言规定,只要实在外部,即不是在任何一个函数内定义的变量,编译器就将其当作全局变量,无论变量定义前是否有extern说明符。也就是说,只要在外部书写下述形式即可,int m = 100;
当全局变量定义时,当且仅当省略了extern时,初始化表达式才可省略,系统默认将其初始化为0,对于定义的全局数组或结构,编译器将其中每个元素或成员的所有位都初始化为0。
创建一个变量并赋值就是定义,只创建没有赋值就是声明。全局变量的声明和定义在函数外部
int quanju2;//int num没有赋值,作为全局变量就是声明,被编译器解析为extern int quanju2
int quanju2;//声明只是声明变量存在,函数可有多个声明,全局变量也可以有多个声明
int quanju2 = 12;//函数只能有一个定义,全局变量也只能有一个定义
相比,自动变量只有定义没有声明。
int quanju2;//int num没有赋值,作为全局变量就是声明,被编译器解析为extern int quanju2
int quanju2;//声明只是声明变量存在,函数可有多个声明,全局变量也可以有多个声明
//int quanju2 = 12;//函数只能有一个定义,全局变量也只能有一个定义
int quanjuarr1[3];
struct quanjus
{
char name[30];
int id;
}quanjus1;
extern int quanju3;//对于没有省略extern的外部变量,系统不会赋默认值0
void main()
{
//对于没有初始化的全局变量,编译器默认将其初始化为0
printf("%d\n",quanju2);
//printf("%d\n",quanju3);
for (int i = 0; i < 3;i++)
{
printf("%d\n",quanjuarr1[i]);
}
printf("%p,%s,%d\n",&quanjus1, quanjus1.name, quanjus1.id);
system("pause");
}
一般为了叙述方便,把建立存储空间的变量声明称定义,而不不需要建立存储空间的声明称为声明。在函数中出现的对变量的声明(除了用extern声明的以外)都是定义。
如果需要在全局变量定义处之外访问这个变量,可以在相应的代码处用extern关键字提前声明此变量,从而将在后面定义的全局变量的作用范围扩展到前面来。
extern int quanju4;//使用extern扩大全局变量的作用域
void quanjufun2();//提前声明函数,扩大函数的作用域
int quanju4 = 4;
void main()
{
quanjufun2();
system("pause");
}
void quanjufun2()
{
auto int quanju4 = 1;
printf("%d\n",++quanju4);
}
使用extern声明可以扩大全局变量的作用域。
多个文件中声明的外部变量。全局变量的作用范围还可以扩展年至其他c程序文件,只需要在其他c程序文件中使用extern关键字说明一下即可。
全局变量是与程序共存亡的,因此,全局变量的生存期不是关心的重点,经常讨论的是其作用域与可见域。全局变量的作用域是整个程序,不论该程序由几个文件组成,理论上,可以在程序的任意位置使用定义的全局变量,但在特点位置处,全局变量是否可见取决于是否对其进行了合理声明。不进行任何声明时,全局变量的可见域为从定义到本文件结束。
void main()
{
{
extern intquanju6;
printf("%d\n",quanju6);
}
//printf("%d\n",quanju6);
system("pause");
}
全局变量受到内层代码块中声明的同名变量的屏蔽。
int x3 = 1;
void main()
{
printf("x3=%d\n",x3);
{
printf("x3=%d\n",x3);
int x3 = 12;
printf("x3=%d\n",x3);
{
int x3= 5;
printf("x3=%d\n",x3);
}
printf("x3=%d\n",x3);
}
printf("x3=%d\n",x3);
system("pause");
}
extern int x4 = 10;
double y4 = 30;
void main()
{
extern float z;
z = 4;
void p5();
p5();
printf("x4=%d,y4=%f,z=%f\n",x4, y4, z);
system("pause");
}
float z;
void p5()
{
int x4 = 1;
double y4 = 3;
printf("x4=%d,y4=%f,z=%f\n",x4, y4, z);
}
全局变量的优缺点
好处,为函数间数据传递提供了新的途径,函数返回值仅仅只能有1个,很多情况下,这不能满足要求,而全局变量可用于更多处理结果。利用全局变量可以减少形参和实参的个数,省去函数调用时的时空开销,提高程序运行的效率。
弊端,全局变量在程序执行期间都有效,一直占据着存储单元,不像局部变量等在调用执行期间临时占用内存,退出函数时便将其释放。最大的问题是降低了函数的封装型和通用性,由于函数中存在全局变量,因此,如果想把函数复用在其他文件中,必须连所涉及的全局变量一块移植过去,容易引发各种问题,造成程序不可靠。全局变量使得函数间独立性下降,耦合度上升,可移植性和可靠性变差。为了代码的可移植,在可以不使用全局变量的情况下尽量避免使用全局变量。
10. static变量
静态变量。static变量的定义格式为:static 数据类型 变量1[=初始化表达式],变量2[=初始化表达式]…;与extern变量都是全局变量不同,static变量有静态全局变量和静态局部变量之分。
静态局部变量,除了生存期是整个程序执行期间(与程序共存亡)外,其作用域与可见域与普通auto变量完全一样。static局部变量只有定义,没有声明。
对局部变量用static声明,把它分配在静态存储区,该变量在整个程序执行期间不释放,其所分配的空间始终存在。常使用静态局部变量在函数调用间歇保存某些变量的值。
void teststa2()
{
for (int i = 0; i < 5;i++)
{
int num = 0;
static int data= 0;//静态变量始终在内存,一旦定义初始化,即使再次执行初始化语句,也仅仅执行一次初始化
//声明周期一直会占用内存,与程序共存亡。
//data的作用域是当前的块语句
printf("%p,%p\n",&num, &data);
num += i;
data += i;
printf("\n");
}
}
void teststa1()
{
static int sta3 = 4;
printf("%d,%p\n",sta3, &sta3);
int fsta1 = 14;
printf("%d,%p\n",fsta1, &fsta1);
}
void main()
{
static int sta2 = 3;
teststa1();
printf("%d,%d\n",sta1, sta2);
teststa1();
printf("\n");
teststa1();
teststa2();
system("pause");
}
静态局部变量让代码的移植性更强
void main()
{
//int res = 0;
for (int i = 0; i < 10;i++)
{
static intres;//把for外的变量移植到代码块内部
res += i;
printf("%d\n",res);
}
system("pause");
}
在递归中使用静态变量,static局部变量保留中间结果在块语句内部,块语句调用一次就刷新一次,重新开始。
int staticadd(int a)
{
static res = 0;
static bushu = 0;
if (a == 0)
{
return res;
}
else
{
bushu++;
printf("第%d步%d+%d=", bushu,res, a, res);
res += a;
printf("%d\n",res);
staticadd(a -1);
}
}
静态全局变量和extern变量的不同体现在作用域上,extern作用域是本程序的所有源代码文件,只要在一个文件中定义,在其他文件中使用时只要对其进行声明即可。而静态全局变量只有定义,没有声明,其作用域仅限于从定义位置起到本文件结束的一段代码区域,不能被其他文件中的函数所使用。
//同一个c文件可以访问静态全局变量和全局变量
static int sta1 = 10;
extern int ext1 = 12;
extern void p2();
void staandext()
{
printf("%d\n",sta1);
printf("%d\n",ext1);
}
在另一个c文件中
extern int sta1;
extern int ext1;
void p2()
{
printf("%d\n",ext1);
//printf("%d\n",sta1);
}
对全局变量用static声明,则该变量的作用域只限于本文件模块(即被声明的文件中)。
静态全局变量实际上是对extern变量破坏封装型和可靠性的一种改良。
静态全局变量有定义和声明的区别。
static int sta2;
static int sta2;//可以多次声明,声明sta2,扩大可见域
void main()
{
printf("%d\n",sta2);
system("pause");
}
static int sta2 = 3;
当省略初始化表达式时,编译器会自动以0初始化静态变量。对于数组或结构,编译器将其中的每个元素或成员的所有二进制位都初始化为0。
static int sta3;
static int staarr1[4];
static struct stas
{
char name[3];
int id;
}stas1;
void main()
{
printf("%d\n",sta3);
for (int i = 0; i < 3;i++)
{
printf("%d\n",staarr1[i]);
}
printf("%s,%d\n",stas1.name, stas1.id);
printf("静态局部变量未赋值的例子\n");
static int sta3;
static int staarr1[4];
static struct stas
{
char name[3];
int id;
}stas1;
printf("%d\n",sta3);
for (int i = 0; i < 3;i++)
{
printf("%d\n",staarr1[i]);
}
printf("%s,%d\n",stas1.name, stas1.id);
system("pause");
}
11. 静态常量
作用域局限在本文件模块,阻断扩文件引用这个常量。
在2.c文件中定义一个常量
const int con1;
在1.c文件中引入这个文件
void main()
{
printf("%d\n",scon1);
system("pause");
}
如果在1.c文件中将这个常量声明为static,就可以阻断外部文件对这个常量的引用。
static const intscon2 = 123;
减少频繁调用含有常量的函数,对cpu的消耗。
static const int scon = 123;//不可修改的静态变量
void testcon()
{
const int x = 10;//在栈上,每次执行完后被销毁
//在栈上分配内存回收内存,函数频繁调用,非常消耗cpu
//为了优化,减少对cpu的消耗,使用static定义为静态,存储在静态区
static const int y = 12;//不会被撤销的常量
}
12. extern变量和static变量的初始化
如果要对extern或者static变量进行赋值初始化,只能使用常量表达式来初始化extern变量和static变量,常量表达式包括直接常量(3、5之类的数字或字符等)、#define常量、枚举常量和sizeof()运算符。
比如,int a =10;int b = a;static num = a;是错误的。
int ext5 = 10;
//int ext6 = ext5;//不可以使用变量为extern变量赋值
static sta7 = 7;
//static sta8 = sta7;//不可以使用变量为static全局变量赋值
#define AA 6;
int ext8 = AA;//可以使用define常量
int ext9 = sizeof(ext5);//sizeof()常量也可以
enum en{a,b,c};
int ext10 = a;//枚举常量也可以
const int con = 123;
//int ext11 = con;//不接受const定义的常量,const不是真正意义的常量
//只是编译器为了保证const常量不被修改而设立的机制
void main()
{
int a = 3;
int b = a;//auto变量可以使用变量赋值
static sta5 = 5;
//static sta6 = sta5;//不可以使用变量为static局部变量赋值
}
13. 函数的作用域和可见域
c语言中的函数都是独立的代码块,以二进制形式存储在程序代码区,函数名可以看成是指向其对应代码块入口点的常量指针。
一般的函数的作用域就是整个项目,任何地方都可以引用。外部的c文件都可以直接引用。内部的c文件,可见域是从函数定义到源码结束。通过函数的声明可以扩大可见域。
void neibu1();
void neibu2()
{
neibu1();
}
void neibu1()
{
printf("haha");
}
void main()
{
neibu1();
system("pause");
}
14. 静态函数
默认下,c语言中的函数是可以跨文件调用的。如果要限定函数的只能由创建这个函数的c文件调用,可以加上static,定义为静态函数。静态函数又叫内部函数。
static用于避免函数跨文件重名的问题和避免本文件函数被外部文件引用。
比如,文件1.c
/*
void p10()
{
printf("hasdf\n");
}
*/
void p11(char *p)
{
printf("%s\n",p);
}
void main()
{
p10();
gostatic();
p11("sdfd");
system("pause");
}
文件2.c
static void p10()
{
printf("hasdf\n");
}
void gostatic()
{
p10();
}
15. 外部函数
如果一个函数可以被其他源文件中的函数调用,称为外部函数。定义格式为:
[extern] 返回类型 函数名(参数表)
{
函数体
}
中括号可以省略,即c语言默认所定义的函数是外部。一般的,c语言中的函数是可以跨文件调用的,即使不声明为extern也是外部函数。软件工程规范要求,调用外部函数,一般需要外部声明。使用extern。
和外部变量一样,在源程序中,外部函数只能定义一次,其作用域为所有的源程序文件,但其默认可见域为从函数定义位置起到该源文件结束,如果要在其他源文件中调用外部函数时,需要对该函数进行声明扩展其可见域。
可见域扩展的程度同样取决于声明的位置,如果是在代码块中声明的,扩展的范围是从声明位置到代码块结束。如果是外部声明的,扩展范围是从声明位置到该文件结束。声明的格式为:[extern]返回类型 函数名(参数表);
void neibu1();
void neibu2()
{
neibu1();
}
void neibu1()
{
printf("haha");
}
void main()
{
neibu1();
system("pause");
}
对于库函数,可以直接使用extern声明而使用之
extern void msg();
void main()
{
msg();
system("pause");
}
是否有extern都可以运行,但是为了规范需要加上extern
extern int printf(char *, ...);
int getchar();
void main()
{
printf("hasdf");
getchar();
}
这段代码没有包含库函数声明文件,但是为了方便,如果像上述一个一个的声明外部常用的函数是麻烦的。所以,需要包含。#include<stdio.h>等之类的,包含了很多函数的声明,相关功能的函数封装到一个头文件中,方便调用。
16. 结构体定义的作用域和可见域
结构体类型既可以定义在代码块内部,也可以定义在外部。当定义在代码块内部时,去作用域与可见域为定义位置起到代码块结束,当定义在外部时,其定义域和可见域为定义位置到其所在的源文件结束。
结构体变量作为全局变量,有声明与定义,作用域为全局,可见域为定义位置起到其所在的源文件结束,以及附加了声明的位置。
结构体变量作为局部变量定义在代码块内部时,作为局部变量,仅有定义,其作用域与可见域为定义位置起到代码块结束。
extern struct info7 info2;//使用extern扩展结构体变量的可见域
//struct info info3;//超过可见域
struct info
{
int num;
char name[20];
};
void structfun1()
{
//struct data d2;//不在变量的可见域
struct data
{
int id;
};
struct data d1;
}
struct info info1;
void main()
{
struct info info1;
}
结构体类型允许重复定义。不同于函数和extern变量,整个源程序的所有源文件中只能有一处定义,结构体类型允许在整个源程序的所有文件中多次定义,不同源文件中的定义可以不同,但同一源文件中的定义只能有一个。否则,编译器会给出重复定义的错误。
也就是说,结构体类型最多适用于一个c文件。不同的c文件可以包含同名同类型的结构体类型,也可以包含同名不同类型的结构体类型。同一个源文件,作用域完全重合的结构体变量的定义只能有一个,细说就是,同一个源文件,函数外部,同名同类型结构体类型定义只能有一个;同一块语句内部,同名不同类型或同类型结构体类型定义只能有一个;如果不在同一块语句,同名不同类型可以有多个,不在同一个块语句,可以有同名不同类型的结构体,此时,块语句内部的结构体会屏蔽外部的。
struct info4
{
int id;
char name[20];
double a;
};
/*同名结构体只能有一个
struct info4
{
int id;
char name[20];
};
*/
void test11()
{
//i4.id = 4;
struct info4 i4;
i4.id = 5;
struct info4
{
int id;
char name[20];
};
/*
struct info4
{
char name[20];
};
*/
}
17. 内存四区
变量名、函数名等都对应着内存中的一块区域。这些在实体也就是内存中的存放涉及到c程序内存分配。
一个由c编译的程序占用的内存大致分为以下几部分
栈区(stack),由编译器自动分配释放,存放函数的参数值,局部变量的值等。栈区的内存分配,属于动态分配。函数参数进栈的顺序是从左往右。
堆区(heap),一般由程序员分配释放(动态内存申请与释放),若程序员不释放,程序结束时可能由操作系统回收。属于动态内存分配。
void zhanhedui()
{
int a[5] = { 1, 2, 4, 5, 6};
int *p =malloc(sizeof(int)* 5);
printf("zhan=%p,dui=%p\n",a, p);
for (int i = 0; i < 5;i++)
{
p[i] = i + 1;
}
free(p);
}
void main()
{
zhanhedui();
printf("\n");
zhanhedui();
printf("\n");
system("pause");
}
全局区(静态区static),全部变量和静态变量的存储是放在这一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,该区域在程序结束后由操作系统释放。全局区的内存会一直存在,与程序共存亡,全局区的内存分配属于静态分配。
int num22 = 10;//全局变量,静态分配,在内存中一直存在,和程序共存亡。
void jingtaidongtai()
{
int data = 12;
printf("jingtai=%p,dongtai=%p\n",&num22, &data);
data = 3;
num22 = 32;
}
void main()
{
jingtaidongtai();
printf("\n");
jingtaidongtai();
printf("\n");
system("pause");
}
程序代码区,存放函数体的二进制代码,字符串常量和其他常量的存储位置,程序结束后由操作系统释放。代码区只能读,不能写,不会发生变化,与程序共存亡,只能静态分配。
void testdaimaqu()
{
printf("haha\n");
}
void main()
{
printf("%p\n",testdaimaqu);
char *p;
scanf("%p",&p);
printf("%c\n",*p);
*p = 'a';
system("pause");
}
但是使用调试技术可以修改内存,修改代码区。windebug。
常量
#define定义的常量在代码区,无法取其地址。
#define D1 10;
const int m = 12;//全局常量,只能读不可以写
void main()
{
//printf("%x\n",&D1);//不可以取define常量的地址
//字符串常量在代码区,只能读,不能写
char *p ="hahaha";
//*p = 'a';
printf("%s,%x\n",p, p);
//如果将p指向栈区的数据,是可以修改的
char st[23] ="ahsdf";
p = st;
*p = 'c';
printf("%s,%x\n",p, p);
system("pause");
}
动态分配,内存使用完成后,内存可以被回收,用作他用。主要形式,栈内存区的分配,由系统自动分配释放;堆内存区的分配,自己分配自己释放。
静态分配,内存分配完成之后,就不会被回收,直到程序结束。主要形式,静态区,一旦分配,不会被回收,可读可写;代码区,一旦分配,可读不可写,不可改变。
内存分区有利于内存数据的管理。
18. 堆、栈和内存映射
进程,一次程序的执行过程。线程,程序执行过程中的任务片段。多线程,程序执行过程中多个并行执行的任务片段。
void runmsg(void *p)
{
MessageBoxA(0,"hh", "xx", 0);
}
//多线程,几乎是同时执行多个程序代码段
void main()
{
_beginthread(runmsg, 0,NULL);
_beginthread(runmsg, 0,NULL);
_beginthread(runmsg, 0,NULL);
_beginthread(runmsg, 0,NULL);
system("pause");
}
//单进程,单线程,必须一件事情执行完毕才可执行下一件
void main48()
{
runmsg(NULL);
runmsg(NULL);
runmsg(NULL);
runmsg(NULL);
}
每个线程都有自己专属的栈,先进后出。栈的最大尺寸固定,超出则引起栈溢出。变量离开作用范围后,栈上的数据会自动释放。栈的分配和回收,对cpu的消耗相对堆较大。
void main()
{
//虽然每次都循环分配内存,但是栈的数据用完后会回收,所以,内存的大小没有变化
//但是栈的分配和回收,会占用cpu的运算
while (1)
{
//产生溢出,栈默认的大小是1Mb。
double aa[1024 *100];
}
}
堆上内存必须手工释放(c/c++),除非语言执行环境支持GC(java)。堆适合那些内存需求大,不确定需要多少内存,内存使用期间有变动的情况(可变内存)。使用malloc,realloc,calloc分配内存。
堆上内存手工释放
void main()
{
while (1)
{
void *p =malloc(1024 * 1024 * 10);
//堆可以处理很大的内存数据,必须手动释放
Sleep(2000);
free(p);
Sleep(2000);
}
}
19. 总之
按存储类别,变量可分为auto、register、extern和static几类;按作用域的不同分为局部变量和全局变量。其中,auto、register和静态局部变量属于局部变量,extern和静态全局变量属于全局变量。
函数也可分为内部函数和外部函数。平时时候最多的是外部函数。如果不指明存储类别,编译器默认定义的函数为外部函数。外部函数的作用域是整个源程序的所有源文件,而内部函数只能在其所在的源文件中进行访问。
结构体的用法,不同源文件中可以定义同名不同版本的结构体类型,在同一源文件,同名同类型只能定义1次(在作用域重合的情况下)。否则,编译器会报错。
对于一个数据的定义,需要指定两种属性,数据类型和存储类别,分别使用对应的关键字。比如,static int a;auto char c;register int d;extern int e;
可见域,如果一个变量在某个文件或函数范围内是有效的,就称该范围为该变量的可见域。
生存期,如果一个变量值在某一时刻是存在的,则认为这一时刻属于该变量的生存期。
可见域从空间的角度,生存期从时间的角度,看待变量。
各种变量的作用域与存在期
变量存储类别 | 函数内 | 函数外 | ||
作用域 | 存在期 | 作用域 | 存在期 | |
自动变量和寄存器变量 | √ | √ | × | × |
静态局部变量 | √ | √ | × | × |
静态外部变量 | √ | √ | √(只限本文件) | √ |
外部变量 | √ | √ | √ | √ |
二十四、 编译与预处理
1. 编译预处理、链接
一个可执行的c程序的生产过程,要经过编辑源码、预处理、编译和链接4个步骤。
编辑就是写代码,使用编辑工具或集成开发工具,按c语言的语法规则组织一系列的源文件,主要有两种形式,一种是.c文件;另一种是.h文件,也称头文件。
#include和#define都属于编译预处理,c语言允许在程序中用预处理指令写一些命令行。编译预处理在编译器之前根据指令更改程序文本。编译器看到的是预处理修改过的代码文本,c语言的编译处理功能主要包括宏定义、文本包含和条件编译3种。预处理对宏进行替换,并将所包含的头文件整体插入源文件中,为后面要进行的编译做好准备。用一个标识符来表示一个字符串,称为宏,被定义为宏的标识符称为宏名。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为宏代换或是宏展开。
编译器处理的对象是由单个c文件和其中递归包含的头文件组成的编译单元,一般来说,头文件是不直接参加编译的。编译器会将每个编译单元翻译称同名的二进制代码文件,在dos和windows环境下,二进制代码文件的后缀名为.obj,在unix环境下,其后缀名为.o,此时,二进制代码文件是零散的,还不是可执行二进制文件。
错误检查大多是在编译阶段进行的,编译器主要进行语法分析,词法分析,产生目标代码并进行代码优化等处理。为全局变量和静态变量等分配内存,并检查函数是否已定义,如没有定义,是否有函数声明。函数声明通知编译器:该函数在本文件晚些时候定义,或者是在其他文件中定义。定义存在,没有声明,调用一个函数可以编译可以链接;定义存在,声明存在,调用一个函数可以编译可以链接;定义不存在,声明存在,调用一个函数,可以编译,无法链接;定义和声明都不存在,可以编译,无法链接。
链接器将编译得到的零散的二进制代码文件组合成二进制可执行文件,主要完成下述两个工作,一是解析其他文件中函数引用或其他引用;二是解析库函数。
比如,某个程序由两个c文件组成,分别为1.c、2.c,这两个c文件和其中递归包含的头文件组成的两个编译单元,经过预处理和编译生成二进制代码文件1.obj和2.obj。假设1.c中调用了函数haha,但是函数haha定义在2.c中。1.c中仅仅有对haha函数的引用,其二进制定义代码需要从2.obj中提取,插入到1.obj的调用处,这个过程称为函数解析(reslove),由链接器完成。不仅仅是函数,变量(比如有外部链接性的全局变量)也牵扯到解析的问题。当2.c没有定义函数haha时,编译时不会产生错误,但链接时却会提示,有未解析的对象,据此可分析出问题出在编译阶段还是链接阶段。
2. c程序中的错误
程序中的错误可以分两大类,一是程序书写形式在某些方面不合c语言要求,称为语法错误,这种错误将会由编译器指明,是种比较容易修改的错误;二是程序书写 本身没错,编译链接能够完成,但输出结果与预期不符,或者执行着便崩溃掉,称为逻辑错误。细分下去,逻辑错误又可分为编译错误和链接错误,很明显,编译错误就是在程序编译阶段出的错误,而链接错误就是在程序链接阶段出的问题。
如果文件中出现编译错误,编译器将给出错误信息,并指明错误所在的行,提示用户修改代码,编译错误主要有两类:第一类,语法问题,一种是缺少符号,如缺分号,缺括号等,符号拼写不正确,一般来说,编译器都会指明错误所在行,但由于代码是彼此联系的,有时编译器提示位置,要么在提示位置之前,甚至是在前面很远的地方;另一种是有时一个实际错误会让编译器给出很多错误提示,所以,面对成百上千个错误提示时,也许实际上修改一处代码就可以了。第二类,上下文关系有误,程序设计中有很多彼此联系的东西,比如变量要先创建再使用,有时编译器会发现某个变量尚未定义,便会提示出错,这种情况有时是因为变量名拼写有误,有时是因为确实没有定义。
除了错误外,编译器还会对程序中一些不合理的用法进行警告(warning),尽管警告不耽误程序编译链接,但对警告信息不能完全不管不顾,警告常常预示着隐藏很深的错误,特别是逻辑错误,需要仔细对待,认真排查。比如,
void main()
{
int *x = 12;
printf("%d\n",x);
printf("%d\n",*x);
system("pause");
}
链接错误,当一个编译单元中调用了库函数或定义在其他编译单元中的函数时,在链接阶段就需要从库文件或其他目标文件中抽取该函数的二进制代码,以便进行组合等一系列工作,当函数名书写错误时,链接器无法找到该函数对应的代码,便会提示出错,指出名字未解析(unresolved)。一般来说,链接器给出的错误提示信息是关乎函数名、变量名等的。
逻辑错误,即使程序顺利通过了编译链接,也不是就完全ok了,要检查生成的可执行程序,看其是否实现了所需的功能。实际上,运行阶段出现的逻辑错误更难排查。可能出现的逻辑错误有以下情况,与操作系统有关的操作,是否进行了非法操作,如非法内存访问等。是否出现了死循环,变现为长时间无反应,假死,需要注意的是,长时间无反应并不一定都是死循环,有的程序确实需要很长时间。程序执行期间发生了一些异常,比如除数为0等,操作无法继续进行。程序能正确执行,但结果不对,此时应检查代码的编写是否合乎问题规范。
3. 排错
排除错误,有两层含义,找到出错的代码,修改该代码。排错也有两种形式,一是静态排错,编译器和链接器发现的错误基本都属于这一类,通过观察源程序便能确定问题所在并改正它。另一种是动态排错,逻辑错误的发现和纠正都比较困难,要综合考虑代码、使用的数据和输出结果的关联,仔细思考,尝试更换数据,观察结果的改变,依次分析错误可能存在的地方。
如果还是不行,就要使用动态检查机制,最基本的方式是分而治之,检查程序执行的中间状态,最常用的方法是在可能出错的地方插入一些输出语句,让程序输出一些中间变量的值,确定可能出错的区域。此外,还可利用编译环境提供的DEBUG工具,对程序进行跟踪、监视和设断点等,定位并排错。
4. assert宏
编写代码时,总会做出一些假设。断言就是用于在代码中捕捉这些假设,也就是assert 宏。程序员相信在程序中的某个特定点该表达式为真,使用assert(表达式)设置一个静态断言,如果表达式为假,则抛出错误,便于排查错误。可以在任何时候启用和禁用断言验证。因此可以在测试时启用断言,在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。
使用assert断言,要引入<assert.h>。
开启静态断言
#include<assert.h>
void main()
{
double db1, db2;
scanf("%lf%lf",&db1, &db2);
printf("db1=%lf,db2=%lf\n",db1, db2);
printf("db1/db2=%lf",db1 / db2);
assert(db2 != 0);//当db2不等于0时就抛出错误;当db2等于0时就抛出错误
system("pause");
}
关闭静态断言使用宏#define NDEBUG
关闭静态断言的宏,需要放在引入#include<assert.h>之前。
#define NDEBUG
#include<assert.h>
void main()
{
double db1, db2;
scanf("%lf%lf",&db1, &db2);
printf("db1=%lf,db2=%lf\n",db1, db2);
printf("db1/db2=%lf",db1 / db2);
assert(db2 != 0);//当db2不等于0时就抛出错误;当db2等于0时就抛出错误
system("pause");
}
判断指针是否为空的静态断言
void main()
{
char *p = (char*)malloc(1024 * 1024 * 1024 * 2);
//assert(p !=NULL);//malloc分配内存失败会返回NULL。当p等于NULL就抛出错误。
*p = 'a';
system("pause");
}
assert宏定义用于软件测试与代码调试。自己编写一个自定义的静态断言宏实现类似assert的功能。
使用到#define,注意#define一行写不下用\进行换行连接。
#define myassert(x) \
if (!(x)) \
{ \
printf("myassert(%s)宏开始检测...\n",#x);\
printf("当前函数名为%s,文件名为%s,代码行号为%d",__FUNCTION__, __FILE__, __LINE__);\
char str[50];\
sprintf(str, "当前函数名为%s,文件名为%s,代码行号为%d",__FUNCTION__, __FILE__, __LINE__);\
MessageBoxA(0, str, "错误提示", 0);\
}
void main()
{
int num = 10;
num = 20;
myassert(num < 20);
system("pause");
}
自定义禁用断言宏
#define NASSERTDEGUG
#ifdef NASSERTDEGUG
#define myassert(x)
#else
#define myassert(x) \
if (!(x)) \
{ \
printf("myassert(%s)宏开始检测...\n",#x);\
printf("当前函数名为%s,文件名为%s,代码行号为%d",__FUNCTION__, __FILE__, __LINE__);\
char str[50];\
sprintf(str, "当前函数名为%s,文件名为%s,代码行号为%d",__FUNCTION__, __FILE__, __LINE__);\
MessageBoxA(0, str, "错误提示", 0);\
}
#endif
5. 预处理命令
预处理命令的引入是为了优化程序设计环境,提高编程效率,合理使用预处理命令能使编写的程序易于阅读、修改、移植和调试,也有利于程序的模块化设计。
预处理命令必须独占一行,并以#开头,末尾不加分号,以示与普通c语句的区别。原则上,预处理行可以写在程序的任何位置,但惯常写在程序文件的头部。编译器在对文件进行实质性的编译之前,先处理这些预处理行,这也是预字的含义。c语言的编译预处理功能主要包括宏定义(#define)、文件包含(#include)和条件编译(#if…)3种。条件编译是有选择的编译一些代码,条件编译相比选择结构相比,有利于精简代码编译的量。
#define ccc "haha"
#define aaa "slss"
#define LANGUAGE 1
void main()
{
#if LANGUAGE == 1
{
printf(ccc);
}
#else
{
printf(aaa);
}
#endif
system("pause");
}
6. 宏定义
宏定义就是用宏名来表示一个字符串,在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串来替换,称为宏代换或宏展开。
宏展开只是种简单的代换,字符串中可以包含任何字符,可以是常数,也可以是表达式,预处理器进行宏展开时并不进行语法检查。
#define HONG 12
void main()
{
printf("%d,%d\n",sizeof(HONG), HONG);
system("pause");
}
不带参数的宏定义
#define R 10
#define PI 3.14
#define AREA R*R*PI
#define OUTPUT "半径=%d,Pi=%f,面积=%f\n", R, PI, AREA
void main()
{
printf("半径=%d,Pi=%f,面积=%f\n", R, PI,AREA);
printf(OUTPUT);
system("pause");
}
#define定义常量与宏常量,#define定义的常量称为符号常量,而const常量常称静态常量,相比#define,const有很多优势,在实际使用时,一般使用const定义常量。
对#define定义的符号常量,预处理器只是进行简单的字符串替换,并不对其进行类型检查,而且会与程序中定义的同名变量冲突,比如
//define是强行替换,如果与定义的变量重名,就会出错
#define num 10
void main()
{
int num = 2;
printf("%d\n",num);
#define M = 1.0;//不会进行类型检查
printf("%d\n", M);
const MM = 10.0;//会进行类型检查,避免类型不一致而出错
printf("%d\n",MM);
system("pause");
}
#define不会进行语法的检查,仅仅是替换
#define PR printf("hshdfd")
//#define XX 1O
void main()
{
PR;
printf("%d\n",XX);
system("pause");
}
宏定义的规则,宏名在源程序中若用引号括起来,则预处理程序不对其作置换。同一个宏名不能重复定义。
#define ID 100
#define YOULE 12
#define YOULE 15//同一个源文件,宏不可以重名
//多个源文件,宏可以重名
void main()
{
printf("ID is%d\n", ID);
system("pause");
}
宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。在宏展开时由预处理程序层层代换。
#define X1 12
#define X2 13
#define X3 X1 + 1
#define X4 X2 + X3
void main()
{
printf("%d,%d,%d,%d\n",X1, X2, X3, X4);
system("pause");
}
宏定义一般写在函数外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。
#define ZUOYONGYU 15
void main()
{
printf("%d\n",ZUOYONGYU);
system("pause");
}
#undef ZUOYONGYU
//ZUOYONGYU在hongz中已经失效
void hongz()
{
printf("%d\n",ZUOYONGYU);
}
带参数的宏定义
c语言有一类宏可以带有参数,在宏定义中的参数称为形式参数,在宏展开中的参数称为实际参数。带参数的宏在编译链接后,不仅要展开宏,而且要用实参去代换形参,在宏定义中的形参是标识符,而宏展开中的实参可以是任意表达式。
带参宏定义的一般形式为:#define 宏名(形参表) 字符串
带参宏展开的一般形式为:宏名(实参表);
#define ADD(x) 2+x//宏名(形参) 要替换的字符串
#define CUBE(x) x*x*x//#define可以传入任何类型的参数,只是替换
void main()
{
printf("%d\n",ADD(3));
double y;
float z = 12;
int i;
int j = 3;
y = CUBE(z);
i = CUBE(j);
printf("%f,%d\n",y, i);
system("pause");
}
标准而逻辑严密的写法是给宏定义中字符串中用到的参数加上()
#define max(a,b) (a) > (b) ? (a) : (b)
#define cheng(a,b) (a) * (b)
#define jia(a,b) (a) + (b)
void main()
{
int res = max(cheng(10,2),jia(2,5));
int res2 = max(1 + 2, 3 +4);
printf("%d,%d\n",res, res2);
system("pause");
}
类似函数的带参宏定义
#define ZUOYONGYU 15
#define change(x,y) \
{\
int t = (x);\
(x) = (y);\
(y) = t;\
}
void main()
{
int a = 10;
int b = 12;
change(a, b);
printf("替换后a=%d,b=%d\n",a, b);
system("pause");
}
带参数的宏与函数替换的比较
带参数的宏,仅仅只是替换,不求出实际参数的值,函数调用的时候,会计算参数的值;函数调用的时候,会给参数分配内存,替换仅仅只是替换,不占用内存;函数有返回值的概念,替换没有,但是只是返回一个表达式,这个结果等价于函数的返回值;函数调用的时候,是有类型的,而宏替换没有;宏替换展开的时候,代码会变长,函数调用不会;多个函数调用会让程序运行时间增加,不影响预处理时间,多个宏调用,会让预处理时间增加,但是运行时间会短。
如果需要传递的参数自动加上””,需要在传递的参数前加上#,让参数变成一个字符串。
#define go(x) system(#x)
void main()
{
go(notepad);
go(pause);
}
7. 文件包含
文件包含是c语言预处理的另一个重要功能,用“#include”来实现,将一个源文件的全部内容包含到另一个源文件中,成为它的一个部分,文件包含的一般格式为:#include<文件名>或者#include “文件名”。两种形式的区别在于:使用尖括号表示在系统头文件目录中查找(由用户在设置编程环境时设置),而不在源文件目录中查找。使用双引号则表示首先在当前的源文件目录中查找,找不到再到系统头文件目录中查找。使用尖括号可以用双引号代替,但是双引号的不一定可以用尖括号代替。
#include”文件名”格式下,可以显示指明文件的位置,如:
#include”D:\aaa\aa\hah.c”
# include”..\aa\hah.c”/*当前目录上级目录下的aa目录下的hah.c*/
#include”.\aa\hah.c”/*当前目录下的aa目录下的hah.c*/
#include”\aa\hah.c”/*当前源码所在磁盘顶级目录下的aa目录下的hah.c*/
void main()
{
#include "1.h"
#include "E:\\pp.txt"
#include "/ding.txt"//当前源码所在磁盘顶级目录下的
#include "..\\..\\..\\..\\ss.txt"
}
文件包含规则,一条include命令只能指定一个被包含文件,若有多个文件要包含,则需用多个include命令。比如错误的写法,#include “aa.h,bb.h”或者#include “aa.h”,”bb.h”。文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。文件包含语句中被包含的文件通常是以.h结尾的头文件,这种头文件中的内容多为变量的定义、类型定义、宏定义、函数的定义或说明,但被包含的文件也可以是以.c为扩展名的c语言源文件。一般情况下,最好用.h,不要用.c,因为.c会独立编译,某些时候会重定义。
文件包含常见错误有,重定义错误,重复包含等。
重定义错误
在1.h中
int a = 19;
在1.c中
void main()
{
#include "1.h"
int a = 12;
system("pause");
}
重复包含错误
在1.c中定义了一个函数
void chongfu()
{
}
在2.c中也定义相同的函数
void chongfu()
{
}
编译时就会报重复定义的错误。
解决的方法是在头文件中声明函数,在1.c中定义函数。函数的声明可以有多个。
头文件一般放声明,函数的声明或全局变量的声明等,否则会因为重复包含而报错。
8. 条件编译
通过某些条件,控制源程序中的某段源代码是否参加编译,这就是条件编译的功能,一般来说,所有源文件中的代码都应参加编译,但有时候希望某部分代码不参加编译,应用条件编译可达到这个目的。
条件编译的基本形式有几种,分别为:
#if 判断表达式
语句段1
#else
语句段2
#endif
#define L 'c'
void main()
{
#if L == 'c'
MessageBoxA(0,"sdf", "sdfsdf", 0);
#else
MessageBoxA(0,"xxxx", "xxxxxx", 0);
#endif
}
以及
#ifndef 标识符//如果标识符未被#define命令定义则对程序段1进行编译,否则编译程序段2。
程序段1
#else
程序段2
#endif
通常用来通过一个宏,开启或关闭某个功能。
#define GOOD
void main()
{
#ifndef GOOD
system("notepad");
#else
system("calc");
#endif
}
以及
#ifdef 标识符//如果标识符被#define命令定义则对程序段1进行编译,否则编译程序段2。
程序段1
#else
程序段2
#endif
通常用来通过一个宏,开启或关闭某个功能。
#define GOOD
void main()
{
#ifdef GOOD
system("notepad");
#else
system("calc");
#endif
}
实际上通过定义的宏,#define _CRT_SECURE_NO_WARNINGS就是一个条件编译。
#ifndef也用来规避重复包含,重复定义等重复类错误。实际上还是类似开关的功能。
#ifndef INFO
#define INFO
struct info
{
char name[20];
int id;
};
#endif
#ifndef NUM
#define NUM
int num11 = 11;
#endif
#ifndef FUN1
#define FUN1
void fun1()
{
printf("haha");
}
#endif
void main()
{
printf("%d\n",num11);
struct info i1;
i1.id = 12;
printf("%d\n",i1.id);
fun1();
system("pause");
}
多重判断
#define LANGU 1
#if LANGU == 1
#define ppp printf("%s\n", "zhongwen")
#endif
#if LANGU == 2
#define ppp printf("%s\n", "gui")
#endif
#if LANGU == 3
#define ppp printf("%s\n", "bang")
#endif
#if LANGU == 4
#define ppp printf("%s\n", "yangg")
#endif
或者使用#elif实现多分支
#define LANGUA 1
#if LANGUA == 1
#define ppp printf("%s\n", "zhongwen")
#elif LANGUA == 2
#define ppp printf("%s\n", "gui")
#elif LANGUA == 3
#define ppp printf("%s\n", "bang")
#else
#define ppp printf("%s\n", "yangg")
#endif
多分支编译,一般用于软件的国际化,有效减少体积,一份源码可以编译多语言版本的软件。
9. 5个预定义的宏名
ANSI标准说明了5个预定义的宏,用于代码调试,分别是:
__DATE__,进行预处理的日期(“Mmm dd yyyy”形式的字符串文字)。
__FILE__,代表当前源代码文件名的字符串文字。
__LINE__,代表当前源代码中的行号的整数常量。
__TIME__,源文件编译时间,格式为“hh:mm:ss”。
__FUNCTION__,当前所在函数名。
void yudingyih()
{
printf("当前文件名%s\n",__FILE__);
printf("当前行%d\n", __LINE__);
printf("当前函数名%s\n",__FUNCTION__);
printf("编译时间%s %s\n",__TIME__, __DATE__);
}
void main23()
{
printf("当前文件名%s\n",__FILE__);
printf("当前行%d\n",__LINE__);
printf("当前函数名%s\n",__FUNCTION__);
printf("编译时间%s %s\n",__TIME__, __DATE__);
yudingyih();
system("pause");
}
进行软件测试,或者软件调试,需要定位错误,所以在错误的处理语句中,需要加上这些宏。
void main()
{
int num1, num2;
printf("请输入被除数\n");
scanf("%d",&num1);
printf("请输入除数\n");
scanf("%d",&num2);
if (num2 == 0)
{
printf("除数不能为0,函数为%s,行数为%d\n",__FUNCTION__, __LINE__);
}
else
{
printf("结果为%d\n", num1 /num2);
}
system("pause");
}
10. const常量与宏的差别
const是有数据类型的,可以根据数据类型进行安全检查,赋值会自动进行数据类型转换。发现类型不匹配的时候,会发出警告或者转换。
#define就是替换,没有数据类型,无法进行安全检查。
struct data
{
int x;
double y;
}d1;
//const int c2 = d1;//赋值会自动检测数据类型
#define HH 12.0
const int ccc = 12.0;
void main()
{
printf("%d,%d\n",sizeof(HH) ,HH);
printf("%d,%d\n",sizeof(ccc), ccc);
system("pause");
}
关于const常量的本质
const本质是伪常量,无法用于数组初始化,以及全局变量初始化。原因在于const仅仅限定变量一旦初始化后无法再直接赋值。const只是限定一个变量无法直接赋值,但是可以间接赋值。但是还是有可能被意外修改,例如间接赋值。并不是完全只读,所以并非真正意义的常量。例如局部const常量在栈区,而不在静态区(静态区会一直存在),也不在代码区,否则就会被禁止修改,因为代码区间接赋值不可能成功。
void main()
{
const int num = 10;//num是一个常量
const int *p = #//定义一个指针指向一个常量
int *pv = (int *)p;//对指向常量的指针进行强制转换
*pv = 12;
printf("%d\n",num);
system("pause");
}
检测验证局部const常量是存放在栈区的。
void consttest()
{
const int num = 12;
printf("%p",&num);
printf("\n");
}
void main()
{
consttest();
printf("\n");
consttest();
printf("\n");
consttest();
printf("\n");
system("pause");
}
11. 宏的高级用法
使用#可以为宏的形参添加””。
#define printfNum(x) printf("%s=%d\n", #x, x);
void main()
{
int a = 3;
printfNum(a);
system("pause");
}
在c语言的宏中,##被称为连接符(concatenator)。它是一种预处理运算符,用来把两个语言符号(Token)组合成单个语言符号。这里的语言符号不一定是宏的变量。并且##不能作为第一个或最后一个元素存在。##运算可以将两个记号(例如标识符)粘在一起,成为一个记号。##运算符被称为记号粘合。
#define I(x) I##x
#define P(x) printf("%s=%d\n", #x, x)
#define P2(x) printf##x
void printf1()
{
printf("1\n");
}
void printf2()
{
printf("2\n");
}
void main()
{
int I(1) = 10, I(2) = 20,I(3) = 30;
P(I(1));
I1 = 12;//##起到链接作用,I(1)等价于I1。
I2 = 14;
I3 = 15;
P(I(1));
P2(1)();
system("pause");
}
二十五、 链表
链表是一种常见的重要的数据结构,它是动态地进行存储分配的一种结构。链表必须利用指针变量才能实现。
比较realloc和链表,realloc每次都要重新分配内存。
1. 动态链表数据结构的增删改查插清空
动态链表就是相对静态链表而言,动态分配存储空间创建的链表。建立动态链表是指在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟节点和输入各节点数据,并建立起前后相链的关系。链表的建立一般是指先建立一个空链表,而后一个个地将元素插在队尾。
创建头文件,声明链表结构体和函数
struct student
{
int num;
float score;
struct student *pNext;
};
typedef struct student ST;
//增加节点
void add(ST **phead, int num, float iscore);//函数声明,传入节点的地址,然后插入
//查看所有节点
void showall(ST *head);
//根据成员id删除节点
void delete(ST **head, ST *node);
//插入节点
void Hcharu(ST *head, int num, int inum, float iscore);
//在节点前插入
ST * Qcharu(ST *head, int num, int inum, float iscore);
//修改节点
void update(ST *head, int oldnum, float iscore);
//清空链表
void * freeall(ST *head);
//逆置链表
ST * reverse(ST *head);
//链表排序
void sort(ST *head, char ch);
//统计节点个数
int getnum(ST *head);
//检索
ST * search(ST *head, int num);
定义链表结构体和函数
#include<stdio.h>
#include<stdlib.h>
#include "include.h"
//定义增加节点函数
/*
功能 添加节点
参数 phead,类型,ST **,创建的链表的首元素的地址;
inum,类型,int,元素成员id;
iscore,类型,float,元素成员的分数。
返回值 void。
*/
void add(ST **phead, int inum, float iscore)
{
if (*phead == NULL)//判断链表是否为空
{
ST *newnode =(ST *)malloc(sizeof(ST));
if (newnode ==NULL)
{
printf("内存分配失败\n");
return;
}
newnode->num =inum;
newnode->score= iscore;
newnode->pNext= NULL;
*phead =newnode;
}
else
{
//如果不为空,循环到队列末尾
ST *p =*phead;//指向头节点
while(p->pNext != NULL)//p = null就到了链表的尾部
{
p =p->pNext;//不断循环下去
}
//当p->pNext等于NULL时终止循环
ST *newnode = (ST*)malloc(sizeof(ST));
if (newnode ==NULL)
{
printf("内存分配失败\n");
return;
}
p->pNext =newnode;//将创建的节点链接进来
newnode->num= inum;
newnode->score= iscore;
newnode->pNext= NULL;
}
}
//查看所有节点
/*
功能 查看所有节点
参数 head,类型,ST *,创建的链表的首元素的地址。
返回值 void。
*/
void showall(ST *head)
{
while (head != NULL)
{
printf("%d,%f,head=%p,head->pnext%p\n",head->num, head->score, head, head->pNext);
head =head->pNext;
}
}
//删除节点
/*
功能 删除节点
参数 phead,类型,ST **,创建的链表的首元素的地址;
inum,类型,int,要删除的元素成员id;
返回值 void。
*/
void delete(ST **head, int inum)
{
ST *prenode = NULL;//存放要删除的节点的前节点
//从头节点循环节点
//创建一个头节点副本做循环
ST *xhead = *head;
do
{
if(xhead->num == inum && xhead == *head)
{
//如果要删除的是第一个节点,直接将头节点替换为当前头节点的下一个节点
*head =xhead->pNext;
free(xhead);//释放删除的节点的内存
return;
}
elseif(xhead->num == inum && xhead != *head)
{
//否则将要删除的节点的前节点的下一个节点,指向下下个节点
prenode->pNext= prenode->pNext->pNext;
free(xhead);
return;
}
else
{
prenode= xhead;
xhead =xhead->pNext;
}
} while (xhead->pNext!= NULL);
//由于循环判断不到尾节点时的情况,所以做完循环后需要单独判断一下
if (xhead->num == inum&& xhead != *head)
{
prenode->pNext= NULL;
free(xhead);
return;
}
}
//插入节点
/*
功能 在指定编号后插入节点
参数 phead,类型,ST **,创建的链表的首元素的地址;
num,类型,int,要插入的位置的元素成员id;
inum,类型,int,要插入的元素成员id;
iscore,类型,float,要插入的位置的元素分数;
返回值 void。
说明注意 非尾部插入,尾部插入使用add函数
*/
void Hcharu(ST *head, int num, int inum, float iscore)
{
if (head == NULL)
{
return;
}
ST *temphead = head;
while (temphead->pNext!= NULL)
{
if(temphead->num == num)
{
ST*newnode = (ST *)malloc(sizeof(ST));
if(newnode == NULL)
{
printf("内存分配失败\n");
return;
}
newnode->num= inum;
newnode->score= iscore;
newnode->pNext= temphead->pNext;
temphead->pNext= newnode;
return;
}
temphead =temphead->pNext;
}
}
/*
功能 在指定编号前插入节点
参数 phead,类型,ST **,创建的链表的首元素的地址;
num,类型,int,要插入的位置的元素成员id;
inum,类型,int,要插入的元素成员id;
iscore,类型,float,要插入的位置的元素分数;
返回值 插入后链表的头节点
说明注意 非尾部插入,尾部插入使用add函数
*/
ST * Qcharu(ST *head, int num, int inum, float iscore)
{
if (head == NULL)
{
return head;
}
//如果要插入的位置在头节点之前
if (head->num == num)
{
ST *newnode =(ST *)malloc(sizeof(ST));
newnode->pNext= head;
newnode->num= inum;
newnode->score= iscore;
head = newnode;
return head;
}
else
{
ST *p = head;
while(p->pNext != NULL)
{
if(p->pNext->num == num)
{
ST*newnode = (ST *)malloc(sizeof(ST));
newnode->pNext= p->pNext;
newnode->num= inum;
newnode->score= iscore;
p->pNext= newnode;
returnhead;
}
p =p->pNext;
}
}
}
//修改节点
/*
功能 插入节点
参数 phead,类型,ST **,创建的链表的首元素的地址;
oldnum,类型,int,要插入的位置的元素成员id;
iscore,类型,float,要修改为的分数;
返回值 void。
*/
void update(ST *head, int oldnum, float iscore)
{
if (head == NULL)
{
return;
}
ST *temphead = head;
/*while(temphead->pNext != NULL)
{
if(temphead->num == oldnum)
{
temphead->score= iscore;
return;
}
temphead =temphead->pNext;
}*/
//或者使用for循环
for (; temphead->pNext!= NULL; temphead = temphead->pNext)
{
if(temphead->num == oldnum)
{
temphead->score= iscore;
return;
}
}
//由于循环不到最后一个元素的判断,所以,补充对对后一个元素的判断
if (temphead->num ==oldnum)
{
temphead->score= iscore;
return;
}
}
//清空链表
void * freeall(ST *head)
{
ST *p1, *p2;
p1 = p2 = NULL;
p1 = head;//头节点
do
{
p2 =p1->pNext;
p1->pNext =p2->pNext;
free(p2);
} while (head->pNext !=NULL);
free(p1);
return NULL;
//或者如下
/*
while (p1->pNext !=NULL)
{
p2 =p1->pNext;
p1->pNext =p2->pNext;
free(p2);
}
free(head);
*/
}
/*
函数功能 逆置链表
参数 head,类型ST *,链表头指针
*/
ST * reverse(ST *head)
{
if (head == NULL ||head->pNext == NULL)
{
return head;
}
ST *p1 = NULL, *p2 = NULL,*p3 = NULL;
p1 = head;
p2 = head->pNext;
while (p2 != NULL)
{
p3 =p2->pNext;
p2->pNext =p1;
p1 = p2;
p2 = p3;
}
head->pNext = NULL;
return p1;
}
/*
函数功能 链表排序
参数 head,类型,ST *,链表头;ch,类型char,当ch是>号,就从大到小排序,当ch是<号,就从小到大排序
返回值 void
*/
void sort(ST *head, char ch)
{
if (head == NULL)
{
return;
}
//使用冒泡排序
//if (ch == '>')
for (ST *p1 = head; p1 !=NULL; p1 = p1->pNext)
{
for (ST *p2 =head; p2 != NULL; p2 = p2->pNext)
{
if (ch== '>')
{
if(p2->pNext != NULL)
{
if(p2->score < p2->pNext->score)
{
intntemp = p2->num;
p2->num= p2->pNext->num;
p2->pNext->num= ntemp;
floattemp = p2->score;
p2->score= p2->pNext->score;
p2->pNext->score= temp;
}
}
}
else if(ch == '<')
{
if(p2->pNext != NULL)
{
if(p2->score > p2->pNext->score)
{
intntemp = p2->num;
p2->num= p2->pNext->num;
p2->pNext->num= ntemp;
floattemp = p2->score;
p2->score= p2->pNext->score;
p2->pNext->score= temp;
}
}
}
}
}
}
void sort2(ST *head, char ch)
{
if (head == NULL)
{
return;
}
//使用冒泡排序
//if (ch == '>')
for (ST *p1 = head; p1 !=NULL; p1 = p1->pNext)
{
for (ST *p2 =head; p2 != NULL; p2 = p2->pNext)
{
if (ch== '>')
{
if(p1->score < p2->score)
{
intntemp = p1->num;
p1->num= p2->num;
p2->num= ntemp;
floattemp = p1->score;
p1->score= p2->score;
p2->score= temp;
}
}
else if(ch == '<')
{
if(p1->score < p2->score)
{
intntemp = p1->num;
p1->num= p2->num;
p2->num= ntemp;
floattemp = p1->score;
p1->score= p2->score;
p2->score= temp;
}
}
}
//printf("\n");
//showall(head);
}
}
/*
函数功能 获取节点个数
参数 head,类型ST *,链表头节点
返回值 节点个数,类型int
*/
int getnum(ST *head)
{
int i = 0;
while (head != NULL)
{
i++;
head =head->pNext;
}
return i;
}
/*
函数功能 根据编号检索节点
参数 head,类型,ST *,链表头节点;num,类型,int,编号
返回值 查找到的链表的地址
*/
ST * search(ST *head, int num)
{
if (head == NULL)
{
return NULL;
}
ST *p = head;
while (p != NULL)
{
if (p->num ==num)
{
returnp;
}
p = p->pNext;
}
//如果没有找到返回NULL。
return NULL;
}
测试
void main()
{
struct student *head =NULL;//头节点指针
add(&head, 1, 70);
add(&head, 2, 56);
add(&head, 3, 76);
add(&head, 4, 87);
//printf("%d,%f\n",head->num, head->score);
//printf("%d,%f\n",head->pNext->num, head->pNext->score);
//printf("%d,%f\n",head->pNext->pNext->num, head->pNext->pNext->score);
showall(head);
printf("请输入要删除的节点的id\n");
int id = 0;
scanf("%d",&id);
delete(&head, id);
showall(head);
printf("后插入节点后\n");
Hcharu(head, 2, 5, 67);
showall(head);
printf("前插入节点后\n");
head = Qcharu(head, 2, 22,55);
showall(head);
printf("更新后\n");
update(head, 3, 100);
showall(head);
//head = freeall(head);
//printf("清空所有\n");
//showall(head);
printf("逆置链表\n");
head = reverse(head);
showall(head);
printf("排序后\n");
sort(head, '>');
showall(head);
printf("排序后\n");
sort2(head, '>');
showall(head);
printf("获取节点个数为%d\n",getnum(head));
ST *sp = search(head, 3);
printf("查找节点的地址为%p,%d,%f",sp, sp->num, sp->score);
system("pause");
}
链表的增删查改的特点,增加、删除,不需要移动,直接操作。查询与修改,无法像数组一样定位位置,需要用循环的方式定位,查找与修改相对比较麻烦。
2. 数组与链表的对比
静态数组的特点,静态数组的长度在定义时就是固定了的,无法改变长度;外部的内存可以被访问,但是外部内存可能被使用,也可能没有被使用,没有使用的情况下,越界偶尔会成功,已经使用,必然失败;静态数组,在栈上,vc中栈的默认大小为1Mb,静态数组不可以处理较大的数据量。
void main()
{
int a[5] = { 1, 2, 3, 4, 5};
printf("%x\n",a);
//a[5] = 11;//越界
system("pause");
}
静态数组的删除,比较麻烦,每删除一个元素,需要移动多个元素。
void main()
{
int a[10] = { 1, 2, 3, 4,5, 6, 7, 8, 9, 10 };
int flag = 0;
int len = sizeof(a) /sizeof(int);
//对于大数据的数组,使用二分查找法更快
for (int i = 0; i <len; i++)
{
if (a[i] == 6)
{
flag =i;
break;
}
}
len = len - 1;
if (flag == len)
{
a[flag] = 0;
}
else
{
for (int i =flag; i < len; i++)
{
a[i] =a[i + 1];
}
}
for (int i = 0; i <len; i++)
{
printf("%d", a[i]);
}
system("pause");
}
对于数组来说,查询相对比较方便,特别是使用了二分查找或其他有效的算法后
void main()
{
int a[10] = { 1, 2, 3, 4,5, 6, 7, 8, 9, 10 };
int len = sizeof(a) /sizeof(int);
for (int i = 0; i <len; i++)
{
if (a[i] == 7)
{
printf("下标为%d\n", i);
break;
}
}
system("pause");
}
静态数组的修改也是建立在查询基础上,也相对方便
void main()
{
int a[10] = { 1, 2, 3, 4,5, 6, 7, 8, 9, 10 };
int len = sizeof(a) /sizeof(int);
for (int i = 0; i <len; i++)
{
if (a[i] == 7)
{
a[i] =17;
break;
}
}
for (int i = 0; i <len; i++)
{
printf("%d", a[i]);
}
system("pause");
}
静态数组的优点是查询、修改相对方便;缺点是难以处理数据量大的数据,不能越界访问,删除和插入比较麻烦。
动态数组,可以处理很大的数据。
动态数组的查找和修改类似静态数组
void main()
{
int len = 10;
int *p = (int*)malloc(sizeof(int) * len);
for (int i = 0; i <len; i++)
{
p[i] = i;
printf("%d", p[i]);
}
for (int i = 0; i <len; i++)
{
if (p[i] == 6)
{
printf("找到\n");
//修改之
p[i] =15;
}
}
for (int i = 0; i <len; i++)
{
printf("%d", p[i]);
}
system("pause");
}
动态数组的删除也类似静态数组
void main()
{
int len = 10;
int *p = (int*)malloc(sizeof(int)* len);
for (int i = 0; i <len; i++)
{
p[i] = i;
printf("%d", p[i]);
}
printf("查询\n");
for (int i = 0; i <len; i++)
{
if (p[i] == 6)
{
len =len - 1;
for(int j = i; j < len; j++)
{
p[j]= p[j + 1];
}
break;
}
}
for (int i = 0; i <len; i++)
{
printf("%d", p[i]);
}
system("pause");
}
动态数组的增加,相比静态数组,动态数组的长度不是固定的,可以动态的增加内存空间。
在尾部增加
void main()
{
int len = 10;
int *p = (int*)malloc(sizeof(int)* len);
for (int i = 0; i <len; i++)
{
p[i] = i;
printf("%d", p[i]);
}
//数组的拓展,使用realloc
int num = 10;//插入的元素
//重新分配内存,多开拓一个元素的空间
int *px = realloc(p,sizeof(int)* (len + 1));
//realloc(p, sizeof(int)*(len + 1));
len = len + 1;
//px[10] = num;
p[10] = num;
printf("增加一个元素后\n");
for (int i = 0; i <len; i++)
{
printf("%d", p[i]);
}
system("pause");
}
如果是在中间插入元素,则和删除一样比较麻烦的。不仅需要移动,还需要重新分配内存,每次分配内存,会消耗计算机资源。
void main()
{
int len = 10;
int *p = (int*)malloc(sizeof(int)* len);
for (int i = 0; i <len; i++)
{
p[i] = i;
printf("%d", p[i]);
}
//数组的拓展,使用realloc
int num = 10;//插入的元素
//重新分配内存,多开拓一个元素的空间
int *px = realloc(p,sizeof(int)* (len + 1));
int znum = 7;
for (int i = 0; i <len; i++)
{
if (p[i] == 7)
{
len++;
for(int j = len - 1; j > i; j--)
{
p[j]= p[j - 1];
}
p[i +1] = 100;
break;
}
}
printf("增加一个元素后\n");
for (int i = 0; i <len; i++)
{
printf("%d", p[i]);
}
system("pause");
}
二十六、 栈、队列
栈区是一种内存存储分区的方式。单独的讲,栈是一种数据存储的方式。
1. 模拟实现数组栈
struct stack
{
int top;//统计栈中多少个元素,data[top]是栈顶
int data[N];//数组用于存放数据
};
//创建结构体变量,-1代表栈中没有元素,{0}将数组全部初始化为0
struct stack mystack = { -1, { 0 } };
int isempty(struct stack isstack);//判断栈是否为空
void setempty();//设置栈为空
int push(int data);//压入一个数据
int pop();//取出一个数据
/*
函数功能 判断栈是否为空;
参数 要判断的栈的地址
返回值 1栈空,0栈不为空;
*/
int isempty(struct stack *isstack)
{
if (isstack == NULL)
{
return;
}
if (isstack->top == -1)
{
return 1;
}
else
{
return 0;
}
}
/*
函数功能 设置栈为空;
*/
void setempty()
{
mystack.top = -1;
}
/*
函数功能 数据压入栈;
参数 压入的数据
返回值 1成功,0栈溢出;
*/
int push(int data)
{
//没有溢出
if (mystack.top + 1 <=N - 1)
{
mystack.data[mystack.top+ 1] = data;//接收压入的数组
mystack.top++;//下标向前移动一位
return 1;
}
else//否则溢出
{
return 0;
}
}
/*
函数功能 数据弹出栈;
返回值 1成功,0栈为空;
*/
int pop()
{
//容错性判断,避免越界
//如果栈为空
if (isempty(&mystack))
{
return 0;
}
else//栈不为空
{
mystack.top--;
returnmystack.data[mystack.top + 1];
}
}
void main()
{
int a[10] = { 1, 2, 4, 5,6, 7, 8, 12, 16, 17 };
for (int i = 0; i < 10;i++)
{
push(a[i]);//压入数据
}
while(isempty(&mystack) != 1)
{
printf("%d\n",pop());
}
system("pause");
}
使用栈存储十进制转二进制的结果并输出
void main()
{
int num = 10;
setempty();
while (num)
{
push(num % 2);
num /= 2;
}
while(isempty(&mystack) != 1)
{
printf("%d",pop());
}
system("pause");
}
2. 模拟实现链式栈
链式栈,使用链表结构的栈。
创建头文件存放声明
#include<stdio.h>
#include<stdlib.h>
#define datatype int
struct stacknode
{
int num;//编号
datatype data;//数据
struct stacknode *pNext;
};
typedef struct stacknode StackNode;
//初始化
StackNode * init(StackNode * phead);
//进栈
StackNode * push(StackNode *phead, int num, datatype data);
//出栈
StackNode * pop(StackNode *phead, StackNode * poutdata);
//清空
StackNode * freeall(StackNode * phead);
//显示所有
StackNode * showall(StackNode * phead);
创建c文件,编写声明的定义
#include "stacklinknode.h"
//初始化链式栈
/*
函数功能 初始化链式栈,防止指向野指针
参数 类型,struct stacknode *,链式栈头指针域
返回值 struct stacknode *,NULL
*/
StackNode * init(StackNode * phead)
{
return NULL;
}
/*
函数功能 压栈
参数 phead,类型,StackNode,链式栈头指针域;num,类型,int,要入栈的数据的编号;data,datatype,入栈数据
返回值 栈顶指针域
*/
StackNode * push(StackNode *phead, int num, datatype data)
{
StackNode * pnewnode =(StackNode *)malloc(sizeof(StackNode));
pnewnode->num = num;
pnewnode->data = data;
pnewnode->pNext = NULL;
if (phead == NULL)
{
//空链表,直接连上第一个节点
phead =pnewnode;
}
else
{
StackNode *p =phead;
while(p->pNext != NULL)
{
p =p->pNext;
}
p->pNext =pnewnode;
}
return phead;//返回头节点
}
/*
函数功能 打印所有节点数据
参数 phead,类型,StackNode,链式栈头指针域
返回值 栈顶指针域
*/
StackNode * showall(StackNode * phead)
{
if (phead == NULL)
{
return phead;
}
else
{
printf("%d,%d,pnode%p,pnext%p\n",phead->num, phead->data, phead, phead->pNext);
showall(phead->pNext);
}
}
//出栈
/*
*/
StackNode * pop(StackNode *phead, StackNode * poutdata)
{
if (phead == NULL)
{
return NULL;
}
else if (phead->pNext== NULL)
{
poutdata->num= phead->num;
poutdata->data= phead->data;
free(phead);
phead = NULL;
return phead;
}
else
{
StackNode *p =phead;
while (p->pNext->pNext!= NULL)
{
p =p->pNext;
}
poutdata->num= p->pNext->num;
poutdata->data= p->pNext->data;
free(p->pNext);
p->pNext =NULL;
return phead;
}
}
//清空
/*
函数功能 清空栈
参数 phead,类型,StackNode,栈头节点
返回值 NULL
*/
StackNode * freeall(StackNode * phead)
{
if (phead == NULL)
{
return NULL;
}
StackNode *p = NULL;
while (phead->pNext !=NULL)
{
p = phead;
phead =phead->pNext;
free(p);
}
free(phead);
return NULL;
}
测试
#include<stdio.h>
#include<stdlib.h>
#include "stacklinknode.h"
void main()
{
StackNode * phead = NULL;
phead = init(phead);
phead = push(phead, 1,100);
push(phead, 2, 200);
push(phead, 3, 300);
push(phead, 4, 400);
push(phead, 5, 500);
showall(phead);
printf("出栈\n");
StackNode *pout =(StackNode *)malloc(sizeof(StackNode));
/*while (phead != NULL)
{
phead =pop(phead, pout);
printf("%d,%d\n",pout->num, pout->data);
printf("出栈后打印栈\n");
showall(phead);
}*/
printf("清空栈\n");
phead = freeall(phead);
showall(phead);
system("pause");
}
使用链式栈实现输出十进制转二进制
void main()
{
printf("请输入数据\n");
int num = 0;
scanf("%d",&num);
StackNode *phead = NULL;
phead = init(phead);
freeall(phead);
int i = 0;
while (num)
{
phead =push(phead, i, num % 2);
num /= 2;
i++;
}
printf("转换后\n");
StackNode *p = (StackNode*)malloc(sizeof(StackNode));
while (phead != NULL)
{
phead =pop(phead, p);
printf("%d",p->data);
}
system("pause");
}
数组栈与链式栈的差别,数组栈是会发生溢出的,链式栈在硬件足够的情况下,空间不受限制。
3. 函数与栈的关系
函数压栈的顺序是从函数底到函数顶依次压栈。从而执行顺序是从函数顶向函数底执行。
4. 队列及顺序队列
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
模拟顺序队列
#include<stdio.h>
#include<stdlib.h>
#define N 100
#define datatyp char //定义数据类型
//结构体队列
struct queue
{
datatyp data[N];//存放数据的数组
int front;//允许删除的一端称为对头
int rear;//允许插入的一端称为队尾
};
typedef struct queue QUEUE;
//队列的接口
void initQUEUE(QUEUE *sq);//初始化顺序队列
int isqempty(QUEUE *sq);//判断队列是否为空
datatyp gethead(QUEUE *sq);//获取队列首元素
void enQUEUE(QUEUE *sq, datatyp data);//队列入队
datatyp deQUEUE(QUEUE *sq);//队列出队
void showQUEUE(QUEUE *sq);//显示队列全部元素
/*
函数功能 初始化队列
参数 sq,类型,struct queue,队列地址
*/
void initQUEUE(QUEUE *sq)
{
sq->front = sq->rear= 0;//队列的初始化,等价于清空队列
}
/*
函数功能 判断队列是否为空
参数 sq,类型,struct queue,队列地址
返回值 类型,int,返回1队列为空,否则返回0。
*/
int isqempty(QUEUE *sq)
{
if (sq->front ==sq->rear)
{
return 1;
}
else
{
return 0;
}
}
/*
函数功能 获取队列首元素
参数 sq,类型,struct queue,队列地址
返回值 类型,datatyp,首元素的值
*/
datatyp gethead(QUEUE *sq)
{
if (sq->front ==sq->rear)
{
return -1;
}
returnsq->data[sq->front];
}
/*
函数功能 入队
参数 sq,类型,struct queue,队列地址;data,类型,datatyp,要入队的数据
返回值 void
*/
void enQUEUE(QUEUE *sq, datatyp data)
{
if (sq->rear == N)
{
printf("队列满\n");
return;
}
else
{
sq->data[sq->rear]= data;
sq->rear++;//下标移动一下
}
}
/*
函数功能 出队
参数 sq,类型,struct queue,队列地址;data,类型,datatyp,要入队的数据
返回值 类型,datatyp,出队的数据
*/
datatyp deQUEUE(QUEUE *sq)
{
if (!isqempty(sq))
{
datatyp de =sq->data[sq->front];
sq->data[sq->front]= -1;
sq->front++;
return de;
}
else
{
return -1;
}
}
/*
函数功能 显示队列全部元素
参数 sq,类型,struct queue,队列地址
返回值 void
*/
void showQUEUE(QUEUE *sq)
{
if (isqempty(sq))
{
return;
}
else
{
for (int i =sq->front; i < sq->rear; i++)
{
printf("%c",sq->data[i]);
}
}
}
void main()
{
char *str ="abcdfesg";
QUEUE q1;
initQUEUE(&q1);
char *p = str;
while (*p != '\0')
{
enQUEUE(&q1,*p);
showQUEUE(&q1);
printf("\n");
p++;
}
while (!isqempty(&q1))
{
printf("%c出队\n",deQUEUE(&q1));
printf("%d,%d\n",q1.front, q1.rear);
showQUEUE(&q1);
printf("\n");
}
system("pause");
}
5. 链表队列
队列是可以用于处理并发数据的,实际上是将并发的待处理的数据放入队列。而链表队列,由于其可以存放的数据量较大,只受到内存大小的限制,所以,适合处理海量数据的高并发。
创建接口文件
struct queue
{
int num;//数据
struct queue *pNext;//存储下一个节点的地址
int high;//优先级
};
typedef struct queue QUEUE;//简化队列
//初始化
QUEUE * init(QUEUE *lq);
//入队
QUEUE * enQUEUE(QUEUE *lq, int num, int high);
//出队
QUEUE * deQUEUE(QUEUE *lq, QUEUE *pout);
//清空队列
QUEUE * freeall(QUEUE *lq);
//排序
void sort(QUEUE *lq);
//插入排序
void insertsort(QUEUE *lq);
//显示队列
QUEUE * insertQUEUE(QUEUE *lq, int num, int high);
实现接口
#include<stdio.h>
#include<stdlib.h>
#include "linkqueue.h"
//初始化
/*
函数功能 初始化队列
参数 lq,类型,QUEUE,队列首元素
返回值 NULL
*/
QUEUE * init(QUEUE *lq)
{
return NULL;
}
//入队
/*
函数功能 初始化队列
参数 lq,类型,QUEUE,队列首元素地址;num,类型,int,入队数据;high,类型,int,优先级
返回值 队列首元素
*/
QUEUE * enQUEUE(QUEUE *lq, int num, int high)
{
//分配内存
QUEUE *pnewnode = (QUEUE*)malloc(sizeof(QUEUE));
pnewnode->num = num;
pnewnode->high = high;
pnewnode->pNext = NULL;
if (lq == NULL)
{
lq = pnewnode;
}
else
{
while(lq->pNext != NULL)
{
lq =lq->pNext;
}
lq->pNext =pnewnode;
}
return lq;
}
//显示队列
/*
函数功能 打印队列所有数据
参数 lq,类型,QUEUE,队列地址
返回值 void
*/
void showall(QUEUE *lq)
{
QUEUE *q = lq;
while (q != NULL)
{
printf("数据%d,优先级%d,地址%p,下个节点%p\n",q->num, q->high, q, q->pNext);
q = q->pNext;
}
}
//递归实现打印队列
void showalld(QUEUE *lq)
{
if (lq == NULL)
{
return;
}
else
{
printf("数据%d,优先级%d,地址%p,下个节点%p\n",lq->num, lq->high, lq, lq->pNext);
lq =lq->pNext;
showalld(lq);
}
}
//出队
/*
函数功能 出队
参数 lq,类型,QUEUE,队列地址;pout,类型,QUEUE,接收出队数据
返回值 队列新的首元素地址
*/
QUEUE * deQUEUE(QUEUE *lq, QUEUE *pout)
{
if (lq == NULL || pout ==NULL)
{
return NULL;
}
else
{
pout->high =lq->high;
pout->num =lq->num;
QUEUE *temp =lq->pNext;
free(lq);
return temp;
}
}
//清空队列
/*
函数功能 清空队列
参数 lq,类型,QUEUE,队列地址
返回值 NULL
*/
QUEUE * freeall(QUEUE *lq)
{
if (lq == NULL)
{
return NULL;
}
else
{
QUEUE *q1, *q2;
q1 = q2 = NULL;
q1 = lq;
while(q1->pNext != NULL)
{
q2 =q1->pNext;
q1->pNext= q2->pNext;
free(q2);
}
free(lq);
return NULL;
}
}
//排序
/*
函数功能 队列排序
参数 lq,类型,QUEUE,队列地址
返回值 void
*/
void sort(QUEUE *lq)
{
if (lq == NULL ||lq->pNext == NULL)
{
return;
}
//冒泡排序
for (QUEUE *p1 = lq; p1 !=NULL; p1 = p1->pNext)
{
for (QUEUE *p2 =lq; p2 != NULL; p2 = p2->pNext)
{
if(p1->high > p2->high)
{
intntmp = p1->num;
p1->num= p2->num;
p2->num= ntmp;
intnhigh = p1->high;
p1->high= p2->high;
p2->high= nhigh;
}
else if(p1->high == p2->high)
{
if(p1->num < p2->num)
{
intntmp = p1->num;
p1->num= p2->num;
p2->num= ntmp;
}
}
}
}
//插入排序
}
//插入排序
/*
函数功能 队列插入排序
参数 lq,类型,QUEUE,队列首元素地址;num,类型,int,入队数据;high,类型,int,优先级
返回值 队列首元素
*/
QUEUE * insertQUEUE(QUEUE *lq, int num, int high)
{
QUEUE *pnewnode = (QUEUE*)malloc(sizeof(QUEUE));
pnewnode->num = num;
pnewnode->high = high;
if (lq == NULL)
{
pnewnode->pNext= NULL;
lq = pnewnode;
return lq;
}
else
{
QUEUE *p1 = lq;
QUEUE *p2 =NULL;
while (p1 !=NULL)
{
if(high < p1->high)
{
p2= p1;
p1= p1->pNext;
continue;
}
else if(high > p1->high)
{
if(p2 == NULL)
{
pnewnode->pNext = lq;
returnpnewnode;
}
else
{
p2->pNext= pnewnode;
pnewnode->pNext= p1;
returnlq;
}
}
else
{
if(num > p1->num)
{
p2= p1;
p1= p1->pNext;
continue;
}
elseif (num <p1->num)
{
if(p2 == NULL)
{
pnewnode->pNext= lq;
returnpnewnode;
}
else
{
pnewnode->pNext= p1;
p2->pNext= pnewnode;
returnlq;
}
}
else
{
pnewnode->pNext= p1;
p2->pNext= pnewnode;
returnlq;
}
}
p2 =p1;
p1 =p1->pNext;
}
if (high <p2->high)
{
pnewnode->pNext= NULL;
p2->pNext= pnewnode;
returnlq;
}
else if (high ==p2->high)
{
if(num>p2->num)
{
pnewnode->pNext= NULL;
p2->pNext= pnewnode;
returnlq;
}
}
}
}
测试
#include<stdio.h>
#include<stdlib.h>
#include "linkqueue.h"
void main()
{
QUEUE *phead = NULL;
phead = init(phead);
//测试插入排序
phead = insertQUEUE(phead,1, 10);
phead = insertQUEUE(phead,33, 3);
phead = insertQUEUE(phead,23, 3);
phead = insertQUEUE(phead,23, 10);
phead = insertQUEUE(phead,12, -4);
phead = insertQUEUE(phead,1, 5);
phead = insertQUEUE(phead,4, 4);
phead = insertQUEUE(phead,4, 4);
showalld(phead);
system("pause");
}
void main2()
{
QUEUE *phead = NULL;
phead = init(phead);
phead = enQUEUE(phead, 1,10);
enQUEUE(phead, 22, 2);
enQUEUE(phead, 13, 2);
enQUEUE(phead, 5, 10);
enQUEUE(phead, 75, 3);
enQUEUE(phead, 13, 3);
enQUEUE(phead, 3, 3);
enQUEUE(phead, 75, 4);
enQUEUE(phead, 75, 4);
showalld(phead);
//printf("清空队列\n");
//phead = freeall(phead);
//showall(phead);
//printf("出队\n");
/*while (phead != NULL)
{
QUEUE *pout =(QUEUE *)malloc(sizeof(QUEUE));
phead =deQUEUE(phead, pout);
printf("%d,%d\n",pout->high, pout->num);
printf("出队后打印队列\n");
showall(phead);
}*/
printf("队列排序后\n");
sort(phead);
showall(phead);
system("pause");
}
二十七、 静态库编写
静态库就是被编译的二进制可执行文件,可以被调用。lib文件包含函数的实体,也就是函数的定义,可以供别人引用。
#include<windows.h>
#pragma comment(lib,"user32.lib")//链接到库,载入MessageBox函数的实体
#include<stdio.h>
#include<stdlib.h>//带std的是标准库,c语言会自动加载标准库,所有不需要
链接
void main()
{
printf("helll");
system("calc");
MessageBox(0,"title","content",0);
}
自己编写一个lib
编写lib.c文件,将项目的属性的配置类型设置为lib。
#include<stdlib.h>
#include<stdio.h>
void openqq()
{
system("\"C:\\ProgramFiles (x86)\\Tencent\\QQ\\Bin\\QQScLauncher.exe\"");
}
void closeqq()
{
system("taskkill /f/im QQ.exe");
}
新建一个项目设置为启动项,编写1.c文件
#include<Windows.h>
#pragma comment(lib,"../Debug/lib.lib")//引用库文件,调用库函数
void main()
{
closeqq();
}
注意,库文件的路径,可以使用相对路径,也可以使用绝对路径。
标准的企业级lib库,还需要编写头文件,声明库文件中的函数,将项目设置为静态库。
库函数实体文件
#include<stdio.h>
#include<stdlib.h>
void notepad()
{
system("notepad");
}
void calc()
{
system("calc");
}
void mspaint()
{
system("mspaint");
}
库函数声明文件
void notepad();
void calc();
void mspaint();
编写c文件测试,注意函数库文件的路径
#include "enterprise.h"
#pragma comment(lib, "enterpriselib.lib")
void main()
{
notepad();
}
使用命令行编译链接c文件,测试。
二十八、 多线程
线程是一个程序进程中的执行的代码段。一个多线程的进程,其中的多个线程是并行执行的,类似函数非阻塞执行模式。恰当使用多线程开发,可以提高执行效率,处理海量数据。
使用多线程对数组进行快速查找,在不使用二分查找法的情况下,使用多线程可以实现快速查找。
struct s1
{
int *pfindstart;//要查找的首地址
int length;//限定长度
int num;//要查找的数据
int *pfind;//存储找到的数据的地址
int trid;//线程编号
};
int flag = 0;
void find(void *p)
{
struct s1 *ps = (struct s1*)p;//指针类型的转换
for (int *ptemp =ps->pfindstart; ptemp < ps->pfindstart + 100; ptemp++)
{
if (flag)
{
_endthread();
}
if (*ptemp ==ps->num)
{
flag =1;
ps->pfind= ptemp;
printf("找到%d,%p,线程编号%d\n",ps->num, ps->pfind, ps->trid);
return;
}
}
printf("没有找到,线程%d\n",ps->trid);
}
void main()
{
int a[1000];
for (int i = 0; i <1000; i++)
{
a[i] = i;
}
int flag = 0;
struct s1 p[10];
for (int i = 0; i < 10;i++)//创建10个线程,并行查找
{
int *pstart = a+ i * 100;
p[i].length =100;
p[i].pfind =NULL;
p[i].pfindstart= pstart;
p[i].num = 789;
p[i].trid = i;
_beginthread(find,0, &p[i]);
//Sleep(20);
}
system("pause");
}
二十九、 简单的c服务器程序cgi
安装windows版的apache软件。
编写一个简单的c语言程序,生成后,复制xxx.exe命名为hh.cgi,然后剪切到apache的安装目录的cgi-bin目录下。
#include<stdio.h>
#include<stdlib.h>
void main()
{
printf("Content-type:text/html\n\n");
for (int i = 0; i < 10;i++)
{
printf("hahah哈哈哈<br/>");
}
printf("cgicgiheh<br/>");
}
然后,在浏览器测试,localhost/cgi-bin/hh.cgi。
获取服务器数据
在htdocs目录下,创建一个表单cgi.html
<!DOCTYPE html>
<html lang="en">
<head>
<metacharset="UTF-8">
<title>Document</title>
</head>
<body>
<formid="form" name="form" method="post"action="http://localhost/cgi-bin/1.cgi">
<inputtype="text" name="bbb" /><br />
<inputtype="submit" name = "aaa" value="进入" />
</form>
</body>
</html>
创建一个cgi,编写一个c语言程序
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
void main()
{
printf("Content-type:text/html\n\n");
printf("%s<br/>", getenv("QUERY_STRING"));//打印环境变量
char szPost[256] = { 0 };
gets(szPost);//获取输入
printf("%s<br/>", szPost);
char cmd[128] = { 32 };
puts(cmd);
puts("<br>");
char *p1 = szPost;
char *p2 = cmd;
int i = 0;
while (*p1 != '\0')
{
//putchar(*p1);
if (*p1 == '=')
{
p1++;
while(*p1 != '&')
{
if(*p1 == '+')
{
//cmd[i]= 32;
}
else
{
//*p2= *p1;
cmd[i]= *p1;
}
putchar(cmd[i]);
p1++;
//p2++;
i++;
}
break;
}
p1++;
}
//printf("%s<br/>", cmd);
puts("<br/>");
puts(cmd);printf("<br />");
char str[256];
sprintf(str, "%s>> e:\\%s.txt", cmd, cmd);
system(str);
char open[100];
sprintf(open,"e:\\%s.txt", cmd);
FILE *pf = fopen(open,"r");
while (!feof(pf))
{
char ch =fgetc(pf);
if (ch == '\n')
{
puts("<br/>");
}
else
{
putchar(ch);
}
}
}
生成xxx.exe,复制重命名为xxx.cgi,然后,剪切到apache目录的cgi-bin目录下。
访问cgi.html测试