目录
c语言之父---丹尼斯 .里奇
丹尼斯--里奇(Dennis Ritchie),c语言之父,unix之父。与肯.汤普逊一起发明了c语言,开发了unix操作系统。里奇一生为人低调,终身未娶。和c语言一样,保持简单,是她的生活哲学。
计算机语言的发展历史:
机器语言
1和0
CPU:译码单元和执行单元
汇编语言
用英文字母或符号串来代替机器语言的二进制码
汇编器
高级语言
便于人类阅读和编辑的编程语言
编译器
c语言标准的发展:k&R C(传统c时代)
1987年,贝尔实验室的steve johnson 编写了可移植的c编译器--PCC编译器(Protable C Compiler)并开始逐渐流行开来,被广泛地移植到不同的处理器上,成为c编译器的共同基石。这一年,c语言的经典著作《The C Programming Language》也出版了,为了表示对该书的两位作者Dennis Ritchie和Brain Kernighan的敬意,这本书也被成为“K&R C".
c语言标准的发展:ANSI C(C89)
到了80年代,c语言越来越流行,广泛被业界使用,各个厂商群雄并起,推出了c语言的多个实现和版本,根据行业和自己的特定需求,进行各种扩展,c语言进入了春秋战国时代,逐渐演变成一个松散杂乱的大家族。为了统一标准,让c语言有更好发展,1983年,美国国家标准化协会(ANSI)对c语言进行了标准化,并颁布了第一个c语言草案(83ANSI C),针对c语言问世以来,各个版本对c语言的发展和扩充指定了ANSI C标准。1989年12月该标准再次修订并得到了国际标准化组织(ISO)的确认。
c89相比传统c:
增加了真正的标准函数库
新的预处理器和命令
函数原型,允许程序员在函数声明中指定参数的类型
增加了一些新的关键字,包括const,volatile和signed
宽字符,宽字符串和多字节字符
在转换规则,声明和类型检查方面的许多小改动和澄清
c语言标准的发展:c99
最新的c语言标准c99于1999年颁布,并在2003年3月被ANSI采用,但是由于没有得到主流编译器厂商的支持,c99并未得到广泛地应用。所以我们现在说的ANSI标准,通常是指c89标准,即1989年颁布的c语言标准。目前对c99标准支持最好的是GCC。
c99相比c89:
增加了新关键字restrict,inline,_Complex,_Imaginary,Bool
支持//注释,从c++引进这种注释方式。
对编译器限制增加:源程序每行4095字节,变量名最大支持63字节
支持不定长数组
变量的声明不必放在语句块的开头
增加了一些标准库和头文件
支持16进制浮点数
取消了不写函数返回类型默认就是int的规定
除了_ _file_ _,_ _line_ _外,增加了_ _ func _ _获取当前的函数名
复数运算
布尔类型
对非英语字符集提供了更好的支持
对浮点类型提供了更好的支持,包括所有类型的数学函数
整数类型的扩展,包括更长的标准类型
一个奇葩的标准:clean c
c++语言是在c语言的基础上进行了很多扩展。c++语言本身也被标准化,满足了程序员开发大型程序的需要,包括增强的类型检查以及面向对象编程的支持。标准c++几乎是标准的c超集,用标准c和标准c++的公共子集编写c代码也是可行的,有人把这种代码称为clean c。
kiss(keep it simple,stupid)原则:保持简单和直接
四大特点:
c语言经历了十几年的风风雨雨,见识了很多编程语言后背的崛起和衰落,每年能依然保持世界编程语言排行榜上前三甲,可见其是最有旺盛的生命力的。作为目前最受欢迎的程序设计语言之一,c语言主要有四大特点:
1.强大的控制结构
2.快速
3.紧凑的代码-程序更小
4.可移植到其他计算机
强大的控制结构:
数据结构丰富: 32+5个关键字
9中控制语句:顺序,选择,循环,跳转
运算多样化:40多种运算符,15个优先级
紧凑的结构:
表达式简练
程序结构简洁
流程结构化
程序模块化
库函数的实现
快速:
目标代码质量高
程序执行效率高
编译器优化成熟
代码检查限制少
可移植性高:
只要遵循ANSI C标准的c语言程序,理论上可以在多个平台和操作系统上运行,包括windows
MacOS,Unix,linux等。
我们在移植时只要注意某些编译器的一些扩展特性就可以很轻松地在各个平台进行移植。
c语言在移植性上优于其他编程语言,c编译器在大约40多种系统上可用,包括从使用8位的单片机,16位的单片机,32位的arm,64位的arm以及pc上,都有c语言的身影。
c语言的缺点:
代码检查少
指针的使用:野指针的很危险
c的简洁性和丰富的运算符相结合,可能会使程序很难读懂
学习c语言不仅仅是c语言,还需要学习以下内容:
计算机系统与组成原理
程序的编译,链接,运行机制
标准的理解
编译器的特性
存储的理解
计算机的基本组成:
cpu | 内部存储器 | 外部存储器 | 输入设备 | 输出设备 |
算术逻辑部件 | 内存 | 硬盘,U盘 | 键盘 | 显示器 |
控制器 | Flash | 鼠标 |
计算机组成部分如下图所示:
图灵机原理与冯.诺伊曼结构
图灵机:
抽象计算机模型:为计算机设备指明了方向
基本思想是用机器来模拟人们用笔纸进行数学运算
通过将复杂事情分解成简单步骤,可以干所有事情
冯.诺伊曼体系结构:
指令和数据用二进制表示
存储程序方式,指令和数据不加区别混合存储在同一个存储器中
指令和数据都可以送到运算器进行运算
存储器按地址访问的线性编写地址
指令由操作码和地址组成
通过执行指令直接发出控制信号控制计算器的操作
指令在存储器中按其执行顺序存放,由指令计数器指明要执行的指令所在的单元地址。指令计数器只有一个,一般按顺序递增,但执行顺序可按运算结果或当时的外界条件改变
以运算器为中心,I/O设备与存储器间的数据传送都要经过运算器
冯诺依曼结构的特点:
指令和数据二进制存储在同一个存储器中
顺序执行程序
计算机由5大部分组成
程序存储原理:
程序和数据通过输入输出设备送入内存
内存划分很多存储单元,线性编址。程序和数据在内存中按照一定顺序存放。
内存分为若干区域:程序区和数据区
cpu从第一条指令开始,不断地取指令,移动取址地址,一条一条地执行指令
机器语言:
采用二进制代码表示
计算机能直接识别和执行的机器指令集合
机器语言就是指令集
-----cpu可以直接读取,译码,执行(硬件电路实现)
机器语言的优点:
------直接执行,速度快,占用资源少
机器语言的缺点:
------可读性差,编程痛苦
汇编语言:
符号语言
-----用英文字母或符号串来代替机器语言
-----用助记符代替机器指令的操作码
-----用地址符号或标号代替指令或操作数的地址
汇编语言与一种机器语言指令集一一对应
------不同架构的cpu,有不同的指令集(51单片机/x86/ARM)
------不同的指令集对应不同的汇编语言
汇编语言应用场合
-------底层驱动及启动代码
-------硬件操作
-------高要求的程序优化场合
优点
-------保持了机器语言的优点
缺点
--------面向机器编程,可移植性差
--------开发效率低,可维护性差
高级语言
适合人类阅读和编辑,高度封装的语言
常见的高级语言
-------c,java,c++
-------python,
优点
----易学,易掌握,可读性好,可维护性强
----可移植性高,复用率高
----将底层和繁杂的事务交给编译器,精力集中在创造性劳动,提高程序质量,缩短开发周期
编译器
----将高级语言翻译成机器语言
高级语言分类
-----编译型语言
-----解释型语言
编译型语言在运行之前,首先要单独编译,将程序翻译为机器语言。如c/c++
解释型语言在运行时将程序翻译成机器语言,速度相对较慢。如java,python。
图解如下:
链接过程图解:
c语言编译全过程:
使用gcc进行分别编译,如果记不到具体的步骤可以使用指令gcc --help来查看如何进行分步编译。
gcc的编译参数来控制程序的编译过程:
-E 只对c源程序进行预处理(预处理包括展开头文件,删除注释),不编译
-S 只编译生成汇编文件,不进行汇编
-c 只编译生成目标文件,不进行链接
-o 使用链接器进行链接 目标文件,其他目标文件,库文件等---最后生成可执行的二进制文件
程序加载过程:
指令执行过程:
取指令
翻译指令
取数据
执行指令
取下一条指令
。。。(往复循环)
将可执行程序文件拷贝到目标目录位置
往系统目录拷贝必要得库文件(可选)(有些代码比较机密以库的形式进行了封装)
向系统注册程序或库的安装信息(可选)
在开始菜单或者桌面上建立快捷方式(可选)
制作过程:
准备文件
制作,添加安装说明和信息
测试安装
运行
第一个程序如下:
gcc编译参数-O2:代码编译优化等级,一般选择2
判断一个标识符是否为关键字,可以直接定义成变量;如果是关键字则会报错。例如:
int include;(不会报错)
int int;(会报错)
main函数的几种写法:
标识符
-----用来标识程序中某个对象的名字,这对象可以是变量,常量,数组,函数,数据类型,语句,标号等。
标识符的命名规则
-----c语言规定,只能由字母,数字,下划线组成
-----且第一个字符必须为字母或下划线
-----标识符长度限制:c89:31个有效字符:c99:63个有效字符
-----标识符大小谢敏感
-----自定义标识符最好不要下划线开头(编译系统专用)
-----简洁明了,含义清晰,望文知意
关键字
-----c语言保留了一些特定意义的标识符
-----这些关键字已经被编译系统本身使用,用户不能再使用这些关键字作为标识符
-----c89一共32个关键字,c99新增5个关键字
分类
------数据类型关键字
char/int/double/short/long/float/signed/unsigned/struct/union/enum/void
------控制语句关键字
if/else/switch/case/default/while/do/break/continue/for/return/goto
-------存储类型关键字
auto/register/static/extern
-------其他关键字
const/sizeof/typedef/volatile
-------c99新增关键字
Bool/_Complex/_Imaginary/inline/restrict
-------其它编译器扩展关键字
预定义标识符
什么是预定义标识符
-------c语言中系统预先定义的标识符:系统类库名,系统常量名(__func__),系统函数名(printf/sacnf/sin)
特点
--------性质和关键字类似,但不是关键字
--------与预定义宏不同,预定义标识符遵循代码块作用域规则
--------用户可以把预定义标识符作为普通标识符使用,但是这样会混淆系统规定的意愿,使用不当甚至会出错
什么是常量,什么是变量?
----值不能改变的量称为常量,可以被改变和重复赋值的量称为变量
----基本数据类型可分为常量和变量,构造数据类型一般为变量的形式存在
常量分类
直接常量:整型/字符型/浮点型常量,符号常量
标识符:变量名,函数名,数组名,文件名,类型名的有效字符序列
符号常量:宏
例如: int i = 5;
i可以重新赋值,但是5不可以重新赋值。如下:
i = 11(正确)
5 = 11(错误)
变量
特点:
----在运行过程中,值可以改变的量
----有自己的类型,名字,在内存中有自己的存储空间
----程序编译时,会为每个变量分配空间和位置,不同类型的变量在内存中占用的空间大小不同。
-----使用sizeof求变量的大小,例如sizeof(int),sizeof(a)。注:里面括号里面可以是变量也可以是数据类型
----变量的读写
-根据变量名,查找内存中对应的存储空间地址,对该地址进行读写操作
变量的声明
-----通过声明告诉编译器变量的相关属性
变量的定义
-----定义就是创建一个对象,并为这个对象分配内存,起个名字,将空间地址和名字建立关联。
变量与声明的区别
-----定义分配了内存,声明没有。
-----声明的两重含义:变量声明与函数声明
-----一个对象只能定义一次,但可以多次声明
多文件编程的时候,在一个文件中定义了一个变量,如果在另一个文件需要使用这个变量,那就要先声明一下。
变量的命名规则:
遵循标识符的命名规则
----不要使用关键字和预定义标识符作为变量名
----变量最好望文知义,简洁
----变量名使用名词词组,函数名使用动词词组
----尽量不要缩写,采用统一的风格(linux/windows)
----避免出现数字编号,除非特殊情况
变量命名其他需要注意的地方
----数字1和l,数字0和o容易混淆,注意区别
----c语言的一些命名习惯
-----循环变量使用:i,j,k
-----整型变量:i,j,k,m,n
------字符变量:c,ch
-------数组:a
-------指针变量:p,q
-------字符串:s,str...
在计算机内部,所有数据都是采用二进制存储
-----基数与权重
权重转换如下图所示:
c语言二进制的表示
-----二进制:不支持(一般通过移位操作完成)
-----八进制:01,017
-----10进制:0x0E,0xFF(数字0到9,字母A到F)
二进制,10进制,16进制对应关系
二进制和10进制转换
10进制转换为2进制
------除2取余法(从下往上取)
例如:
61十进制得到二进制111101
二进制转换为10进制
------基数
------权:基数的幂
例如:
111101二进制数转换为十进制数61
10进制小数转换为二进制
------乘2取整
例如:十进制0.25转换为二进制0.01
二进制0.01转换为十进制0.25
原码,反码和补码
-----有符号数和无符号数
例如:-1和+1
带符号数的补码
------最高位为符号位:0表示正数,1表示负数
例如:
由此可见:
一个无符号的8位数可以表示的范围0~255
一个有符号的8位数可以表示的范围-128~127
------正数和0的补码和原码相同
------正数和0的反码和原码相同
------负数的反码:最高符号位不变,其余位取反
------负数的补码:最高符号位不变,其余位取反加1,即反码加1
如下图所示:
计算机中有符号数的加减运算采用补码表示法实现
------负数的补码与对应的正数的补码转换方便,使用同一种方法----求补运算,可以简化硬件
------可将减法变加法,省去减法器
------无符号数及带符号数的加减运算可以用同一电路完成
补码转换为原码
------补码的逆运算(先减一,再取反)
------或再做一次补码操作
有符号数和无符号数的区别:
-----有符号数最高位是用来表示正负,无符号数最高位是用来表示数据的
-----有符号字符型数据-128~127,无符号字符型数据:0~255
-----整数自动转换原则
-当表达式中有无符号数和有符号数时,所有的操作数都自动转换为无符号类型
例如:
有符号数的0表示成 0000 0000,而1000 0000表示的是-128
大端模式与小端模式
----根据高低字节在内存中的组织方式划分
---- 小端模式:高地址存放高字节,低地址存放低字节
---- 大端模式:高地址存放低字节,低地址存放高字节
不同架构的CPU采用的模式可能不同
-ARM
-X86
如何判断当前系统是大端还是小端:
将一个整型变量的值赋给一个字符型变量,通常会发生“截断”,将低8位的一字节赋值给字符型变量。通过打印知道整型变量a在内存中的存储模式,进而得知当前的处理器是大端模式还是小端模式。
需要注意的一些问题
浮点数只有有符号型
边界轮回问题
边界条件的判断
例如示例:因为定义的是无符号的a,所以会一直循环下去
程序调试技巧
使用一些预定义标识符,例如__func__ __LINE__ __FILE__
整型常量
十进制常数
-------- -0 ,-1,-200
八进制
-------- -01234567 ,07654321
16进制
-------- 0xFE,0XFE,0x3fff
后缀
-------- 长整型:100L,07711L,0X3FL
-------- 无符号:100U,07711U,0X3FL
-------- 混合: 100UL,07711UL
整型变量
声明和定义
-------short,int,long,long long
-------signed unsigned
-------默认缺省类型(默认是signed类型的)
变量与数据对象
-------变量的本质:一块连续的存储单元(内存,寄存器),不一定有地址。
-------变量有名字,用来表示存储单元(左值)或存储单元的内容(右值)
-------数据对象没有名字,是一种具有数据类型的连续存储单元
整型变量的存储
无符号数-----以原码的形式存储
有符号数-----计算机中任何有符号数都是以补码的形式进行存储和运算
不同类型的整型变量所占内存空间大小不同
=========在不同的编译器环境下结果可能不一样=======
short int占两个字节
int占四个字节
long int 占4个字节
long long int 占8个字节
==============================================
整数的溢出
整型数据的表示范围
整型数据的溢出
-----在计算过程中超出数据的表示范围会导致溢出
ASCII码
-美国标准信息交换代码
-------Ameriacan Standard Code for Information Interchange
-------通用电脑编码系统,适用于所有拉丁字母
标准ASCII码
-------使用7位二进制组合来表示128个字符
-------最高位是校验位(奇校验,偶校验)
-------常用的ASCII码
-A:65 -a:97 回车:13 换行:10 制表:9
-------分类
0~31是控制字符或通信专用字符(不可显示)
32-126:可可显示字符,包括数字,字母,标点符号等
字符常量
------字符型数据在内存中以ASCII码形式存储
------使用单引号引起来如:‘A'
分类
------普通字符常量
------转义字符
---制表符:\t 回车符:\t 换行符:\n
字符变量
声明和定义
--------每个变量分配一个字节
--------允许对变量赋整数值,低8位赋值给字符变量,高位截断丢弃
字符和整数之间可以直接进行数值运算
--------直接将字符ASCII值与整数进行运算
--------当右值运算时,存在整数提升,先转换成int
--------与浮点型运算,转换顺序:char--- int---float
字符的输入和输出
%d格式打印
%c格式打印
printf函数在打印char时做了什么
-------------一个字符可以以%d和%c的形式输出
-------------printf函数无论是%d和%c输出,参数都是int类型(printf函数的参数类型要求是int类型,会把char转换成int类型)
scanf格式输入需要注意地方
-----%c只接收一个字符,其余的丢弃(输入abc,会把bc丢弃掉)
-----%d接收数据转换为二进制,存放在变量起始的连续四个存储单元中
为什么要类型转换?
----计算机没人脑聪明,只会简单的运算
----两个数要类型相同,大小相同
----c语言运算表达式中允许混合各种数据类型
----要求操作符两边的操作数必须相同的类型
----编译器来做这些转换工作
----这些工作由编译器自动完成,也成为隐式转换
隐式转换
什么时候后发生隐式转换
----当算术或逻辑表达式中操作数类型不相同
----当赋值运算符两侧类型不匹配
----函数调用过程中形参,实参不匹配
----当return语句中表达式类型与函数返回类型不匹配
算术运算转换规则
空间占用小的数据转换为空间大的数据
-----浮点数:float>double>long double
-----整数:char>int>usigned>long>unsigned long>long long
转换规则
-----在c99中,可以将任何等级低于int或unsigned的类型转换到此类型,运行结束后再进行转换。比如char。
赋值运算转换规则
字符转换为整型:高位补零
整数转换为字符型:截断
整型转换为浮点型:添加小数位
浮点型转换为整型:截断小数位
字符型转换为浮点型(中间经过整数)
--先提升为整型,然后再添加小数位
浮点型转换为字符型(中间经过整型)
--截断小数位,然后截断
强制类型转换
程序员在程序中用强制运算符转换
(类型名)表达式
转换的是值,而不是变量的类型!
整型int和字符型char是否可以通用?
--所谓通用,就是指可以互换
--char在运算过程中会做整数提升
--整型是指一系列数据的统称
-Bool
-char ,unsigned char
-char
-int unsigned int
-longlong long ,unsigned long long
-unsigned long
c99新增数据类型_Bool
为了与c++兼容:bool,true,false,在头文件中stdbool.h定义了bool宏。
基本概念
半个关键字,需要与其它类型关键字组合
-float _Complex
-double _Complex
-long double _Complex
运算符
--c语言定义的一些运算符号,列如:加减乘除 +,-,×,÷
什么是表达式?
----表达式就是由运算符和操作数组成的一个符合c语法规则的式子序列
例如:3+5;
----单个常量或变量也是一个表达式
例如: a ,b;(a,b是整型变量)
----根据运算符种类,可分为不同种类表达式
例如: 3+5(算术表达式);3<5;(关系表达式);3&&5(逻辑表达式)
----表达式运算后的结果为表达式的值
----值的类型为表达式的类型
什么是语句?
---- 表达式 + ; = 语句
---- 语句是对计算机的指令:声明语句,赋值,函数调用,结构化语句,空语句,复合语句。。。
例如:
声明语句: int a ;
赋值语句: a = 10;
函数调用: a = test(); (test是一个返回类型为整数的函数)
结构化语句: if(xxxx)
{
}
空语句:;(直接一个;号,什么都不做)
---- 语句可分为简单语句和复合语句,简单语句以 ; 结束
简单语句例如:
2;
1;
i;
(这些语句没有什么意义,但是也算是语句的一种)
复合语句是用{}括起来的语句,例如:
{
3+4;
;
43-1;
}
运算符的分类
一种48种运算符:
-ANSI C:44种运算符
-c90:增加了一元+运算符:区分前后缀++/--
-c99:增加了一个复合字面值运算符
运算符的分类(根据需要的操作数)
---单目运算符 ,例如:i++;
---双目运算符,例如:i = 5;
---三目运算符 ,例如:c =a +b;
运算符的优先级
什么是优先级?
----表达式中,优先级高的运算符先选择操作对象
例如: 2+3*4+5
----优先级用来确定运算符的操作对象,而不是用来确定运算次序的,后者是由编译器去决定的。
----c语言优先级一共16级,16级最高,1级最低
----位逻辑运算符优先级高于逻辑运算符
运算符的结合性
什么是结合性?
-----结合性就是一串运算符是从左往右依次执行还是从右往左依次执行
-----当表达式中运算符优先级相同,那么运算符就会根据结合性来挑选操作数
左结合与右结合
-----左边的运算符先挑运算对象,依次向右执行
-----右边的运算符先挑运算对象,依次向左执行
-----求值顺序和结合性无关,和编译器有关
右结合型的运算符
-----单目运算符
例如:*p++;其实就等于 *(p++);
-----赋值运算符
例如: a = b =c;其实就等于 a = (b = c);
-----条件运算符
例如: a>b ? a :b >c ? b:c(复合运算,先判断b和c的大小,再判断a和b的大小)
优先级练习题:判断某年是否为闰年
基本运算符
-+,-,+,*,/,%,++,--(++,--在c99标准中增加了前缀自增自减)
求余运算符
-----求余的优先级和乘除相同
-----操作数是必须是整数
-字符型数据也可以求余运算
-浮点数不能进行求余运算
-指针也可以进行求余运算
-负数也可以求余运算
自增,自减运算符
-----优先级大于乘除
-----后缀自增,自减
-----前缀自增,自减
运算符使用需要注意的地方
------c99对新标准的/和%的修改
-c99中/的结果总是向零取整
-c99中%的值符号跟被除数相同
例 如:
100%3 =1
100%-3=1
-100%3= -1
-------不清楚的地方,复杂的表达式加括号
-------避免产生未定义的行为,不要在一条语句中改变同一个变量一次以上
数据对象
----用于保存变量或数组的数据存储区
----数据存储区的术语,作用是用来保存数据
左值
-----用于标识一个特定数据对象的名字或表达式
-----对象指的是实际的数据存储,左值用来表示存储在计算机内存中的对象。左值是一个用来识别或定位这个存储对象的标识符。
右值
-----指的是能赋给可修改的左值的量
变量
-----变量既可以当左值,又可以当右值
-----变量的名字是一个左值,用来标识存储的数据对象
-----变量的值是一个右值,可以给可修改的左值赋值
-----关系运算符
-<,<=,>,>=,==,!=
------关系表达式
-由关系运算符将操作数连接而成的式子
-表达式的结果是一个整型值,而不是布尔值
-一般用在if或while语句中作为条件判断
关系运算的结合性
----- a<b<c 相当于(a<b)<c
----- a == b == c同上
与,或,非
-----双目运算符:&&,||,左结合性
-----单目运算符:!,右结合性
-----优先级:!>&&>||
------位逻辑运算符
--按位与 &
--按位或 |
--按位取反 ~
使用宏定义位掩码
--按位异或 ^ , 位相同为0,位不相同位1
查询位的状态:
使用位掩码查询第2位的状态如下:
左移,右移运算符
----属于逻辑移位
对移出位进行零填充
-----移位计数的合法范围是【0,31】,等于0相当于没有移动,等于32全部移除没有意义。
位清0操作:
操作思路:对位掩码取反 ,然后与上需要清零的数。
例如:将111清0成101
按位清零和位状态查询demo如下:
#include<stdio.h>
/*0到7位的bit mask*/
#define bit_0 0x01
#define bit_1 0x02
#define bit_2 0x04
#define bit_3 0x08
#define bit_4 0x10
#define bit_5 0x20
#define bit_6 0x40
#define bit_7 0x80
int main ()
{
/*将1101第三位清零得到1001*/
/*思路:将1101按位与上1011得到1001,将bit_2按位取反得到1011进行运算*/
printf("0x%x\n",0x0d & ~bit_2);//结果为0x09
/*查询1101的第一位的状态*/
if( 0x0d & bit_1)
{
printf("1101的bit_1为1\n");
}
else
{
printf("1101的bit_1为0\n");//打印结果
}
return 0;
}
右结合性的三种运算符:单目运算符,赋值运算符,条件运算符。
表达式?结果1:结果2
说明:如果表达式成立,取结果1;否则,取结果2
条件运算符的使用示例:
使用条件运算符比较3,4,5得出最大值
#include<stdio.h>
int main ()
{
int a = 3;
int b = 4;
int c = 5;
int max;
max =(a<b?b:a)<c?c:a;
printf("max = %d\n",max);
return 0;
}
-----表达式1,表达式2,表达式3,,,,,表达式n;
-----最后一个表达式的值是作为整个逗号表达式的值
------逗号表达式的优先级最低
--注意优先级的问题,加括号
使用示例:
#include<stdio.h>
int main ()
{
int b = (1,2,3,4,5*7,9%3);//结果为9%3的结果,也就是0
printf("b = %d\n",b);
return 0;
}
副作用与序列点
----副作用
--副作用是指对数据对象或文件的修改
--定义一个表达式的目的可能是取得这个表达式的值,有的表达式有副作用,有的没有。
例如: int i = 10;
int j ;
j = i;(j的值被修改了,专业的说就是产生了副作用)
----未定义
--包含多个不确定副作用的代码的行为称为未定义
序列点
----程序运行中一个特殊的点。c标准规定:在两个序列点之间,一个对象所保存的值最多只能被修改一次。而且前一个值只能用于解决将要保存的值。如果要写入某个对象,则在同一表达式中,对该对象的访问应该只局限于计算将要写的值。只有能在确保修改之前才访问变量的表达式为合法表达式。
----在两个序列点之间,连续两次修改,并且访问该变量,会产生不确定性,产生未定义行为
c语言中的序列点
----引入序列点的目的
--消除编译器解释表达式时的歧义
--若序列点不能解决歧义,就是未定义行为,编译器自行解释并计算结果
----主要序列点
--完整表达式的尾部
--&&,||,?:,逗号操作符处
--函数调用时,实参表内全部参数求值结束,函数执行第一条指令之前。
如何避免未定义行为
----尽量不要在同一个语句中改变同一个变量2次以上
库函数使用:
----在头文件中声明,然后直接使用
----printf(“控制字符串”,输出列表)
----控制字符串:普通字符串和转换格式说明
printf家族:
----printf,fprintf,sprintf,snprintf,vsprintf
输出格式说明:
控制字符:
----转义字符:\r \n \a \t
%【标志】【输出宽度】【.精度】【长度】类型
----其中【】是可选项
----标志:表述输出数据的特征:比如正负,空格
----输出宽度:输出固定的宽度位数,不足的位数使用空格或者0填充
----精度:以.开头,后跟十进制整数
-若输出的是数字,表示小数的位数
-若输出的是字符,表示输出字符的个数
-若实际位数大于定义的精度,超过部分将被截取,但不会改变实际变量的值
----长度:分h\l两种,分别代表短整型和长整型格式输出
输入函数scanf的使用:
调用方式:
-scanf(“控制字符串”,地址列表);
-地址列表,需要的参数是变量的地址
-通过取地址运算符&来获取变量的地址
-各个变量用逗号隔开,与输入控制字符串中的转换说明占位符一一对应
-scanf以回车键作为输入结束标志
输入控制格式:
%【*】【输入数据宽度】【长度】类型
说明:
-【*】表示该输入项读入后不赋值给相应变量
- 宽度:输入的最大十进制宽度
-长度:格式分为l和h,表示长整型,双精度,短整型
-类型:
%d 输入十进制数
%o输入八进制数
%x输入十六进制数
%c输入单个字符
%s输入字符串
%f输入浮点型数
需要注意的地方:
--------------当控制字符串中各个变量占位符使用逗号隔开时,我们在输入时,各个变量也要使用相 同的符号隔开
-------------如果没有使用逗号隔开,可以使用空格,tab,或者enter键作为各个数据输入的结束,最后输入回车进入scanf输入的结束。
-------------浮点型数据输入时,float要使用%f格式;double类型要使用%lf格式输入,这点跟printf不同
-------------%c格式输入时,只接收一个字符,其余丢弃掉
字符输出函数:putchar
字符输入函数:getchar
使用示例:
注意点:
getchar()是stdio.h中的库函数,它的作用是从stdin流中读入一个字符,也就是说,如果stdin有数据的话不用输入它就可以直接读取了,第一次调用getchar()时,确实需要人工的输入,但是如果你输了多个字符,以后的getchar()再执行时就会直接从缓冲区中读取了
getchar是从标准输入stdin中读取一个字符
---getch非缓冲输入,是直接从键盘读取一个字符
---getchar会等到回车键后从stdin中读取一个字符;下次如果stdin缓冲区不空的话会继续读取
---getchar只接收单个字符,输入数字也按字符处理,输入多个字符时,只接受第一个字符
检测输入到回车符停止循环:
while( (ch = getchar()) !=‘\n’ )
函数getchar()的返回值类型是一个整型
输出函数:puts
输入函数: gets
注意点:
1.字符串结束判断:
---scanf函数读取字符串时以空格,tab或enter键结束,但此时空格和回车还停留在缓冲区,会影响后面的输入
---gets函数以enter键结束,可以读取有空格的字符串,而且此时回车符没有停留在缓冲区,被转换为字符串结束标志\0保存在字符串中。
---函数puts相比printf会自动换行
---puts效率比printf高,printf函数会先对字符串进行格式胡转换,然后才进行输出
---如果打印一个换行符,直接使用putchar(‘\n’);效率更高
---gets函数有造成数据越界的风险,需小心使用
---最新标准c11已经废弃了gets函数的使用
fprintf:格式化输出到一个流/文件中
例如:
sprintf:格式化写入到某个字符串中
例如:
fscanf从指定流或文件中 读入数据
示例:
语句与程序快
--表达式语句
--函数调用语句
--流程控制语句
--复合语句
--空语句
三种控制流程
--顺序结构
--选择结构
--循环结构
--跳转结构
算法
--算法+数据结构 =程序
--流程图
结构化程序设计
--自顶向下
--逐步细化
--模块化设计
--结构化编码
问题:输入圆的半径,计算并输出圆的面积
思路:该问题可以看成顺序流程设计,分步求解如下
流程分解:
1.输入半径
2.计算面积
3.输出
代码如下:
#include<stdio.h>
#define PI 3.14
/*求圆面积函数*/
void getAreaOfCircle()
{
float radius;
printf("请输入圆的半径:\n");
scanf("%f",&radius);
printf("圆的面积为:%f\n",PI*radius*radius);
}
int main ()
{
getAreaOfCircle();
return 0;
}
if....else语句 和 if语句的嵌套
注意点:
----相同的嵌套层次采用相同的缩进:一般4个字符
----执行概率高,判断逻辑简单的分支放在前面
switch语句
注意点:
----case后面各个常量或常量表达式的值必须各不相同,类型一般为整型
----case后面要加break,否则程序会继续往下执行
----执行概率高的分支可以放在前面
----default分支一般用于错误处理。不要作为其中一个处理分支
while语句 和 do...while语句
注意点:
do...while后面注意有分号
for语句
for语句表达式:
for(表达式1;表达式2;表达式3)
关键字break的作用:退出循环体
关键字continue的作用:结束本次循环,提前进入下一次循环
注意点:
-----for括号内的表达式是可以省略的,语句之间用逗号隔开
-----for语句可以多重循环,注意效率问题,可以将长的循环放在内层,短的循环放在外层。循环嵌套一般不要超过3层
-----建议for循环空间使用半开半闭区间的写法,或者坚持同一种写法,for循环体内一般不要修改循环变量,防止循环失控
-----新标准c99可以在for循环中定义变量(在for循环中定义的变量作用域只在for循环里面)
goto语句
goto语句很有可能破环程序的结构,建议少使用
使用实例:
数学中的函数与c语言中的函数类似,实现一个基本的运算。
函数的表达式:
返回类型标识符 函数名(类型1 形参1,类型2 形参2,,,,)
注意点:
函数名一般为动词词组
为什么要使用函数:
-----省去代码的重复编写
-----代码的模块化,通过任务划分可以大大提高开发效率
函数如何被执行的?
函数执行过程:
---传递参数---函数体运算---返回值(有些函数不一定需要传递参数)
main函数的分析:
Windows系统和嵌入式中的main是入口函数
空函数:
实际开发过程中会定义一个空函数搭建程序的框架,空函数运行是不会出错的,后期的模块化填充将空函数进行填充。
void void_function(void)
{
}
函数的原型,声明与定义
函数原型:
------原型就是一个函数声明
函数的声明:
------用来描述函数的参数类型和返回值
------告诉编译器以作代码检查
例如:
extern int function(int n);(函数的声明)
函数的定义:
------实现函数的功能
------函数只能定义一次,但可以多次声明
函数的调用与返回
函数调用的过程要把实际参数的值进行传参,然后通过函数运算返回一个值。
注意点:
c99不再支持隐含int
---在c89中,若没有明确声明,将默认为int型,比如常见的参数声明,函数返回类型等。
对返回值的界定
---在c89中,返回类型非void类型的函数可以使用一个不带返回值的return语句,c99中,非void型函数必须使用return语句,且每个return语句必须带返回值。
给函数的形式参数进行传参时,本质就是将值拷贝一份给形式参数,也就是值调用。
而传址调用可以看成是一种特殊的值调用。
注意:
-----传参是值都拷贝,将值拷贝形式参数
-----参数声明中,c99不再支持隐含int
-----参数的数量c99新标准从给31个增加到127个
-----传址调用的本质也是值调用,只不过这个值比较特殊,是一个地址变量
-----利用断言作参数的检查,更有利于编写更稳定,高质量的c语言程序
使用assert函数要包含头文件<assert.h>
使用实例:
#include<stdio.h>
#include<assert.h>
int add(int a, int b)
{
assert(a>0);//使用assert函数对形式参数检查,如果条件不满足就会向标准输出流打印错误信息
return a+b;
}
int main()
{
int a;
a = add(-1,3);
return 0;
}
函数嵌套:
-----c语言中,除了main之外,函数之间的关系是平等的
-----在一个函数中不能定义另外一个函数
-----在一个函数中可以调用另外一个函数
递归函数:
------一个函数在它的函数体定义内又调用该函数本身
------直接调用:直接递归
------间接调用:间接递归
优点和缺点:
-------可以帮助解决一些算法问题
-------运行效率较低,系统开销大
递归函数需要注意的地方:
-------防止递归层数太多而引起栈溢出
-------汉诺塔,二叉树的遍历
什么是作用域scope:
----变量在程序中的有效范围
----即程序在什么范围内变量可以被识别和使用
分类:
----代码块作用域
----文件作用域
----函数作用域
----原型作用域
外部变量与局部变量:
外部变量(全局变量):
----函数外部定义的变量
局部变量:
----在函数或复合语句内部定义的变量
----函数的形参也属于局部变量
例如:
在复合语句内部定义的变量是局部变量,如下图
外部变量和局部变量的作用域:
----变量的作用域与其定义的位置有直接关系
全局变量作用域虽然是整个文件,但是要在全局变量定义的位置之后到整个文件结束之间才有效。
变量作用域屏蔽:
代码块中的变量会屏蔽代码块之外变量的作用域,但是代码块中可以访问代码块外面的变量
例如:
#include<stdio.h>
void print_num(int num)
{
printf("%d\n",num);//打印结果为1
{
int num = 2;
printf("%d\n",num);//因为num是局部变量,会把外面的num作用给屏蔽,打印结果为2
}
printf("%d\n",num);//打印结果为1
}
void print_num1(int num)
{
printf("%d\n",num);//打印结果为1
{
num = 2;
printf("%d\n",num);//对外部的num进行了修改,打印结果为2
}
printf("%d\n",num);//因为在代码块中对num进行了该该,打印结果为2
}
int main ()
{
print_num(1);
print_num1(1);
return 0;
}
使用全局变量的利弊:
使用全局变量尽量不要使用,虽然方便了各个函数之间的通信,但是它破坏了函数的耦合性。
c99标准中新的修改:
在for语句中可以申明变量,写法如下:
for(int i = 0,i<10;i++)
{
}
示例:
为什么要设置变量的链接属性?
在一个工程文件中,如果有多个c文件的话,编译器首先会将每个c文件生成目标文件,最后编译器将各个目标文件链接起来。过程如下图:
如果在helloworld.c文件定义了一个全局变量int a =10;在other.c文件中定义了一个全局变量 int a =20。单独编译两个文件都没有问题,但是在链接过程的时候,这个变量到底是10还是20就产生冲突了,为了防止链接过程中出现冲突,就设置了变量的链接属性。
不同的变量有不同的链接属性,有以下几种分类:
外部链接属性extern
-----外部变量(全局变量默认是外部的,所有的文件都可以使用该全局变量),函数
内部链接属性internal
-----使用static修饰的外部变量,函数
无链接属性none
-----局部变量,auto类型变量
示例:工程文件main.c和link.c里面都有全局变量global_val;为了防止链接的时候发生冲突,将link.c里面的全局变量使用static修饰
main.c文件:
#include<stdio.h>
int global_val = 10;
void print_function(void);
int main ()
{
printf("main: global_val = %d\n",global_val);
print_function();
return 0;
}
link.c文件:
#include<stdio.h>
static int global_val = 20;
void print_function(void)
{
printf("print_function:global_val = %d\n",global_val);
}
变量的本质:
------一段连续内存空间的别名
声明变量的意义:
------建立变量符号表
------变量的数据类型指示系统分配不同大小内存
------变量的数据类型指示系统如何存储,运算
------变量的数据类型决定变量的取值范围
程序存储分布
代码段:存放着程序的机器码和只读数据,可执行指令就是这里取得的。
数据段:存放已经初始化的全局变量和静态变量
bbs段:存放未初始化的全局变量和静态变量
堆:用来存储程序运行时分配的变量
堆的大小并不是固定,可动态扩张或缩减。其分配由实时内存分配函数来实现。当进程调用malloc函数分配内存时,新分配的内存就被动态添加到堆上;当使用free函数释放内存时,被释放的内存从堆中被剔除。如果程序员没有释放掉,在程序结束后操作系统会自动回收。
栈:栈用来存储函数调用时的临时信息,如函数调用传递的参数,函数的返回地址,函数的局部变量。在程序运行时由编译器在需要的时候分配,在不需要的时候自动清除。
-程序代码区:程序代码和只读数据
-静态存储区:全局变量和静态变量
-动态存储区:堆栈区:局部变量,函数参数
自动变量:auto
----在函数体内定义的变量默认都是自动变量
----作用域在函数体或代码块内
----动态存储:存储在栈内
----编译器自动分配和回收
外部变量:extern
----全局变量默认都是外部的
----全局变量与外部变量:
全局变量:作用域的角度
外部变量:存储方式的角度(存储在静态存储区)
----其他文件使用外部变量:需要先声明,然后在使用
静态变量:static
----静态变量:
-存储在静态存储区
-全局变量默认存储方式为:static
----静态全局变量:
-改变了变量的作用域,只限定在本文件使用该静态全局变量
-存储方式不变
----静态局部变量:
-编译时初始化一次
-改变了变量的存储方式,由原来存放在栈区变成了静态存储区
-作用域不变,静态局部变量的作用域还是在定义的函数里面才有效。
寄存器变量:register
----寄存器变量:
-没有地址
-声明为寄存器类型的变量不一定存放在寄存器中(因为cpu的寄存器可能被填满,只能放在内存当中)
-局部自动变量和形参也可以定义为寄存器变量,如:循环控制变量,循环体内反复使用的变量
例如:
#include<stdio.h>
int main ()
{
register int i ;//该变量存放在寄存器中
for(i=0;i<10;i++)
{
printf("第%d次循环\n",i);
}
return 0;
}
----优点:
-速度快,效率高
volatie的作用:
----告诉编译器,该变量随时会发生变化,每次使用该变量直接到内存中去取而不是采用暂存在寄存器中的值。
----函数本质上,默认是外部的:extern
----内部函数:使用static修饰
----使用内部函数的好处
--也叫静态函数
--局限在本文件内,不同文件里可以有同名的函数。互不影响,利于分工写作,模块化编程。
注意:使用外部函数时,需要先声明;或者将外部函数写入头文件;
1.修饰函数,该函数变为静态函数,静态函数作用域仅局限于在本文件,因此不同文件里可以有同名的函数。互不影响,利于分工写作,模块化编程。
2.修饰局部变量,该静态局部变量只初始化一次 ,存储方式由堆栈区变成静态存储区,但是作用域不变,作用域仍然限定在定义该变量的函数之内。
3.修饰全局变量,该静态全局变量的作用域只限定在本文件中,但是存储方式不变,仍然存放在静态存储区。
使用内联函数的目的:提高函数的效率,在一个程序中,需要反复调用某个函数,但是这个函数又比较短小,就可以将该函数定义为内联函数
1.函数入栈,出栈的时间开销:
--内联省去了函数入栈,出栈的时间开销
--执行速度比一般的函数快
2.函数扩展带来的内存空间开销:
--越简单短小的内联函数,效率越高
--太长的内联函数编译器可能当一般函数处理
内联函数与宏的区别:
--宏是由预处理器进行替代,有时候容易出错,宏不能有返回值,而内联函数可以。
--内联函数是通过编译器控制来实现,在使用的时候像宏一样展开
库函数介绍:
标准c = c语言标准 + 标准库函数
-库函数源代码不可见,一般通过头文件引出接口
-库函数一般分为两种:
-根据c标准实现的库函数
-编译器自己扩展的库函数
不同编译器对标准库函数实现有差异
-底层环境,软硬件方面存在差异
-不同架构的cpu的i/o实现上的差异
动态链接库与静态链接库
:链接器会将静态链接库的代码加到我们要编译的程序当中,生成可执行文件。
:链接器不会将动态链库的代码加到我们要编译的程序当中,而是当运行程序需要的时候再去动态链接库中找。
如何使用库函数
头文件包含#include
标准库函数对应的标准头文件
-c89标准一共15个头文件
-编译器自己的头文件
什么是数组:
使用数组简化程序:
---对于相同类型的多个数据,可以使用数组代替
---相同类型的多个参数,可以使用数组代替
什么是数组:
---一组相同类型的数据集中在一起保存的一种方式,数组用来存储一组相同类型的数据对象
---数组元素:数组中的单个数据对象
---数据分类:字符数组,整型数组,指针数组,二维数组
---数组的引用:使用下标【】或者指针,索引从0开始
数组的使用:
数组的初始化:
1.完全初始化
2.不完全初始化,未初始化的数组元素默认初始化为0
3.c99新增标准,指定初始化数组
代码如下:
#include<stdio.h>
int main ()
{
/*数组的完全初始化*/
int a[10] = {1,2,3,4,5,6,7,8,9,10};
int i;
for(i=0;i<10;i++)
{
printf("%d",a[i]);
}
putchar('\n');
/*数组的不完全初始化,未初始化的数组元素默认初始化为0*/
int b[10] = {1,2,3,4,5};
for(i=0;i<10;i++)
{
printf("%d",b[i]);
}
putchar('\n');
/*c99新增标准,指定初始化*/
int c[10] = {[0] = 100,[9] = 900};
for(i=0;i<10;i++)
{
printf("%d",c[i]);
}
putchar('\n');
return 0;
}
数组的大小:
使用sizeof关键字求数组的大小,同时可以使用sizeof求数组元素的大小。
数组的存储:
数组在内存属于一种线性的存储,如下图:
数组名与数组地址:
什么是数组名:
---使用数组可以为一组相同类型的变量命名同一个名称。c语言把数组名解释为指向该数组首元素的地址,数组名和指向首元素的指针等价。
---当引用数组时,编译器会把数组名的值看做一个指针常量
---数组名不能用指针常量来表示的情况
-当数组名用作sizeof或&的操作数时,sizeof返回的是整个数组的长度
-取一个数组名的地址&产生的是一个指向数组的指针(数组指针),而不是指向某个指针常量值的指针。
数组名作为左值和右值时的区别
---数组名不能作为左值:它代表的是一个指针常量,不能被修改!
---作为右值时:代表数组首元素的地址,而不是数组的首地址,注意,这里并没有一个地方来存储这个地址,编译器并没有为数组名分配一个内存空间来存储这个地址,这个是与指针不一样的地方。
数组名和地址的区别:
---数组名代表的是首元素a【0】的地址,而不是数组的首地址&a
---数组名a+1表示数组下一个元素的首地址&a【0】+1,&a+1指向下一个数组的首地址
使用数组需要注意的地方:
1.数组的索引从0开始
2.数组越界问题,编译时不会报错,运行时可能会导致程序有问题。
3.不指定数组大小时,编译器会根据初始化列表中元素的个数来决定。
4.C语言只支持一维数组
二维数组的定义
二维数组的初始化
二维数组的引用
通过下面的代码说明以上三个知识点:
#include<stdio.h>
int main ()
{
int i;
int j;
int b[2][3];
/*定义了一个两行三列的数组*/
int a[2][3] = {
{1,2,3},
{4,5,6}
};
/*打印二维数组a*/
for(i=0;i<2;i++)
{
for(j=0;j<3;j++)
{
printf("%d ",a[i][j]);
}
putchar('\n');
}
/*二维数组的初始化*/
for(i=0;i<2;i++)
{
for(j=0;j<3;j++)
{
scanf("%d",&b[i][j]);
}
}
/*打印二维数组b*/
for(i=0;i<2;i++)
{
for(j=0;j<3;j++)
{
printf("%d ",b[i][j]);
}
putchar('\n');
}
return 0;
}
二维数组的存储仍然是线性存储
字符数组:
字符数组跟整型数组是一样的,只不过一个存放的是字符型数据,一个存放的是整型数据。字符数据是连续线性的存储,和整型数组是一样的。
#include<stdio.h>
int main ()
{
char array[20] = {'h','e','l','l','o','w','o','r','l','d'};
int i;
for(i=0;i<20;i++)
{
printf("%c",array[i]);
}
putchar('\n');
printf("%s\n",array);
puts(array);
return 0;
}
字符串:
字符串的定义:char [] = "hello world"
char *p = "hello world"
字符串的存储,会在末尾加上一个字符串结束标志,‘\0’.
可以看出字符串所占空间大小比字符数组多1,因为字符串后面后自动加一个结束标识符‘\0'.
strlen()和sizeof()的区别:
sizeof是c语言的一种运算符,一字节的形式给出操作数的存储空间大小。strlen是一个函数,由c语言标准函数库提供,用来计算字符串的长度。
数组元素作为函数的参数:数组元素也是一个值,作为参数就是值的拷贝。
#include<stdio.h>
int add(int a,int b)
{
return a+b;
}
int main ()
{
int a [10] = {[0]=1,[1]=2 };
printf("%d\n",add(a[0],a[1]));
return 0;
}
数组名作为函数的参数,本质上是将该数组的首元素地址拷贝给形参变量
#include<stdio.h>
int add(int *a,int len)
{
int sum ;
int i;
for(i=0;i<len;i++)
{
sum = sum + a[i];
}
return sum;
}
int main ()
{
int a[10] = {1,2,3,4,5,6,7,8,9,10};
int sum = 0;
int size =sizeof(a)/sizeof(a[0]);
sum =add(a,size);
printf("sum = %d\n",sum);
return 0;
}
注意点:
1.数组名作为函数的参数时,其实传递的是一个地址指针,形参和实参的类型必须一致。
2.形参和实参数组的长度可以不相同,关于数组的长度可以通过另外的参数传递进来
3.多维数组作为函数的参数时,在函数定义时对形参数组可以指定每一维的长度,但是可以省去第一维的长度
在c89标准中,定义数组时需要指定数组的长度。而在c99的标准中可以定义变长的数组。
变长数组的存储及作用域:
动态存储
在函数体或代码块内声明
不能使用static或extern修饰
作用域:从声明处到代码块结束
-----下次再执行这个代码块会重新创建一个新的数组
地址与指针:
---c语言中,内存单元的地址称为指针。通俗来说,指针变量是存放其他地址的变量。
---变量类型与指针类型需要匹配。
---取址运算符&,与取值运算符*
由上图可以看出,指针变量p存放的是a的地址。并且我们可以通过取值运算符*修改变量a的值。
变量的存储:
c文件在编译的时候会将代码(指令)和数据分开,生成可执行的二进制文件
当把存放在硬盘的可执行文件,拷贝到内存中执行,会产生如下逻辑地址空间
变量名:
-----对于一个变量名,编译器通过符号表为变量名和变量的地址之间建立关联
-----对变量进行读写时,系统通过符号表找到该变量的地址,然后直接对该地址进行读写操作。
指针的用途:
----提供函数修改实参的方法,模拟引用调用
----支持动态内存分配
----支持动态分配的数组
----支持动态数据结构:二叉树,链表等
----指针常用于数组操作,遍历数组,而且效率比数组下标快
----高效地,按引用“复制”数组和结构,特别是作为函数参数时
我们平时所说的计算机是32位,64位,指的是计算机的cpu中寄存器的最大存储长度,如果寄存器中最大存储32bit的数据,就称之为32位系统。
在计算机中,数据一般都是在硬盘,内存和寄存器之间进行来回存取。cpu通过3种总线把各组成部分联系在一起:地址总线,数据总线和控制总线。地址总线的宽度决定了cpu的寻址能力,也就是cpu能达到的最大地址范围。
内存是通过地址来管理的,那么cpu想从内存中的某个地址空间上存取一个数据,那么cpu就需要在地址总线上输出这个存储单元的地址。假如地址总线宽度是8位,能表示的最大地址空间就是256个字节(2的8次方),能找到内存中最大的存储单元是255这个格子(从0开始)。即使内存条的实际空间是2G字节,cpu也没法使用后面的内存地址空间。如果地址总线的宽度是32位,那么能表示的最大地址就是2的32次方,也就是4G字节的空间。
demo:
#include<stdio.h>
int main()
{
int i = 10;
int *p = &i;
printf("%d %d\n",i,*p);
*p = 20;
printf("%d %d\n",i,*p);
printf("%p %p\n",&i,p);
printf("%p %p\n",&i+1,p+1);
printf("%ld %ld\n",sizeof(i),sizeof(&i));
printf("%ld %ld\n",sizeof(p),sizeof(*p));
return 0;
}
指针变量作为函数的参数:
demo如下:
#include<stdio.h>
void swap1(int a,int b)
{
int temp = a;
a = b;
b = temp;
}
void swap2(int *a,int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int main ()
{
int a = 3;
int b = 4;
printf("调用交换函数之前:\n");
printf("a = %d,b = %d\n",a,b);
swap2(&a,&b);
printf("调用交换函数之后:\n");
printf("a = %d,b = %d\n",a,b);
return 0;
}
swap1函数并不能达到值交换的效果,将实际参数的值拷贝一份给函数swap1局部变量a,b.经过运算局部变量值交换了,但是函数退出,局部变量也出栈消失。并不能影响实际参数的值。
而swap2函数,将a,b的地址拷贝一份给形式参数,通过取值符号*对a,b的值进行修改。
指针变量要先初始化,然后在引用。
指针变量要先初始化,然后在引用。
指针变量要先初始化,然后在引用。
全局变量如果没有初始化,会默认为0.放在bss段。局部变量如果没有初始化,系统会随机分配一个值。这个时候指针变量会随机指向一个内存地址,如果指向操作系统的核心代码。如果修改该内存存放的值,直接造成系统崩溃。所以指针变量要先初始化,然后在引用。
数组与指针的关系:
数组与指针的联系:
---数组名可以看做是一个常量指针
---数组作为参数时,会退化为一个指针(c语言将数组名解释为指向该数组的首元素的地址)
数组与指针的区别:
---数组是一种数据结构,而指针是一种
---保存的值不同:指针保存数据的地址,数组保存数据
---访问值的方式不同:指针通过*间接访问,数组通过下标直接访问或通过*间接访问
数组指针的定义,初始化,引用:
demo:
int main ()
{
int a[10] = {1,2,3,4,5,6,7,8,9,10};
int i;
// int *p = &a[0];
int *p = a;//数组名被c语言解释为指向该数组首元素的地址
/*通过数组下标访问数组元素*/
for(i=0;i<10;i++)
{
printf("%d ",a[i]);
}
putchar('\n');
for(i=0;i<10;i++)
{
printf("%d ",p[i]);
}
putchar('\n');
/*通过指针访问该数组元素*/
for(i=0;i<10;i++)
{
printf("%d ",*(a+i));
}
putchar('\n');
for(i=0;i<10;i++)
{
printf("%d ",*(p+i));
}
putchar('\n');
return 0;
}
指针与数组下标效率分析:
数组下标的访问最终都会转换成指针取值的访问。并且数组下标访问涉及乘法,乘法非常复杂。而指针访问涉及到加法,加法比较快。所以指针访问比数组下标访问效率高。
指向数组首元素的指针作为函数的参数:
a和&a的区别:
a指向数组首元素的指针
&a指向整个数组的指针
区别:
a是数组名,是数组首元素的地址,+1表示地址值加上一个int类型的大小,如果a的值是0x00000001,加1操作后变为0x00000005。
&a是整个数组的指针,其类型为int(*)[10],加1时,系统会认为是数组首地址加上整个数组的偏移(10个int类型变量)
例如:
#include<stdio.h>
int main()
{
int a[10] = {1,2,3,4,5,6,7,8,9,10};
/*指向数组首元素的地址*/
int *p = a;
printf("%p\n",p);
printf("%p\n",p+1);
/*指向整个数组的地址*/
int (*p1)[10] = &a;
printf("%p\n",p1);
printf("%p\n",p1+1);
return 0;
}
二维数组指针与地址的关系:
指向二维数组指针定义,初始化,引用:
#include<stdio.h>
int main ()
{
int a[2][3] = {
{1,2,3},
{4,5,6},
};
int i,j;
/*指向二维数组元素的指针*/
int (*p)[3] = a;
/*遍历二维数组*/
for(i=0;i<2;i++)
{
for(j=0;j<3;j++)
{
printf("%d ",a[i][j]);
}
putchar('\n');
}
/*遍历二维数组*/
for(i=0;i<2;i++)
{
for(j=0;j<3;j++)
{
printf("%d ",*(*(p+i)+j));
}
putchar('\n');
}
return 0;
}
二维数组指针作为函数的参数:
二维数组名作为函数的参数 和 二维数组指针作为函数的参数等价。
#include<stdio.h>
/*二维数组名作为函数的参数*/
int get_sum1(int rows,int a[][3])
{
int sum = 0;
int i,j;
for(i=0;i<rows;i++)
{
for(j=0;j<3;j++)
{
sum += a[i][j];
}
}
return sum;
}
/*二维数组指针作为函数的参数*/
int get_sum2(int rows,int (*p)[3])
{
int sum =0;
int i,j;
for(i=0;i<rows;i++)
{
for(j=0;j<3;j++)
{
sum += p[i][j];
}
}
return sum;
}
int main()
{
int a[2][3] = {
{1,2,3},
{4,5,6},
};
int sum1;
int sum2;
int (*p)[3] = a;
sum1 = get_sum1(2,a);
printf("sum1 = %d\n",sum1);
sum2 = get_sum2(2,p);
printf("sum2 = %d\n",sum2);
return 0;
}
指针数组里面的每一个元素都是指针类型;
指针数组的定义,初始化,使用:
#include<stdio.h>
int main ()
{
/*指针数组的定义和初始化*/
char *p[5] = {
"better",
"late",
"than",
"never",
"!"
};
int i;
/*指针数组的遍历*/
for(i=0;i<5;i++)
{
printf("%s ",p[i]);
}
putchar('\n');
printf("size:%ld\n",sizeof(p));//该数组的大小为40
return 0;
}
指针数组作为函数的参数:
----传递的是数组地址
----字符串排序只需要交换指针
demo:
#include<stdio.h>
#include<string.h>
void array_sort(int len,char *a[])
{
char *temp = NULL;
int i,j;
for(i=0;i<len-1;i++)
{
for(j=i+1;j<len;j++)
{
if(strlen(a[i]) < strlen(a[j]) )
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
}
void array_print(int len,char *a[])
{
int i;
for(i=0;i<len;i++)
{
printf("%s ",a[i]);
}
putchar('\n');
}
int main ()
{
/*指针数组定义初始化*/
char *p[5] = {
"a",
"bb",
"ccc",
"dddd",
"eeeee"
};
printf("排序前:\n");
array_print(5,p);
array_sort(5,p);
printf("排序后:\n");
array_print(5,p);
return 0;
}
指针数组和数组指针的区别:
-----一个是数组,一个是指针整型变量
-----定义:
指针数组 int *a[10] 数组指针 int (*a)[10]
定义一个指针指向字符串:
char *str = "hello world!";(注:str存放的是字符串的首个字符的地址.)
demo:
#include<stdio.h>
#include<string.h>
int main ()
{
char *p = "hello world!";
int i;
/*直接使用字符串打印*/
printf("%s\n",p);
/*数组下标访问字符串*/
for(i=0;i<strlen(p);i++)
{
printf("%c",p[i]);
}
putchar('\n');
/*地址取值访问字符串*/
for(;*p!='\0';p++)
{
printf("%c",*p);
}
putchar('\n');
return 0;
}
练习题:strcpy函数的实现
原型声明:char *strcpy(char* dest, const char *src);
功能:把从src地址开始且含有'\0'结束符的字符串复制到以dest开始的地址空间
说明:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。
返回指向dest的指针。
代码如下:
#include<stdio.h>
char *string_copy(char *dest,char *src)
{
if(dest == NULL || src == NULL)
{
printf("the parameter is NULL\n");
return NULL;
}
char *temp = dest;
while((*temp++ = *src++) != '\0');
return dest;
}
int main ()
{
char a[5] = {'x','x','x','x','x'};
char *b = "abc";
string_copy(a,b);
for(int i=0;i<5;i++)
{
printf("%c",a[i]);
}
putchar('\n');
return 0;
}
练习题:strcat字符串拼接函数的实现
功能
把src所指向的字符串(包括“\0”)复制到dest所指向的字符串后面(删除*dest原来末尾的“\0”)。要保证*dest足够长,以容纳被复制进来的*src。*src中原有的字符不变。返回指向dest的指针。
说明
src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。
代码如下:
include<stdio.h>
char *string_cat(char *dest,char*src)
{
if( dest == NULL || src == NULL)
{
printf("the parameter is NULL\n");
return NULL;
}
char *temp = dest;
while( *temp != '\0')
{
temp++;
}
while(*temp++ = *src++ );
return dest;
}
int main ()
{
char a[30] = "happy";
char b[10] = "everyday";
string_cat(a,b);
printf("%s\n",a);
return 0;
}
指向指针的指针也叫二级指针,二级指针的定义初始化和访问如下:
#include<stdio.h>
int main ()
{
int a = 10;
int *p = &a;
int **pp = &p;
printf("a的地址为:%p ,%p\n",&a,p);
printf("p的地址为:%p ,%p\n",&p,pp);
/*通过变量名访问a*/
printf("a = %d\n",a);
/*通过指针p访问a的值*/
printf("a = %d\n",*p);
/*通过二级指针pp访问a的值*/
printf("a = %d\n",**pp);
return 0;
}
内存分布图:
使用二级指针操作数组:
#include<stdio.h>
int main ()
{
int a[10] = {0,1,2,3,4,5,6,7,8,9};
int *p = a;
int i;
int **pp = &p;
/*通过数组下标遍历数组*/
for(i=0;i<10;i++)
{
printf("%d ",a[i]);
}
putchar('\n');
/*通过指针p遍历数组*/
for(i=0;i<10;i++)
{
printf("%d ",*(p+i));
}
putchar('\n');
/*通过指针p遍历数组*/
for(i=0;i<10;i++)
{
printf("%d ",*p++);/*++的优先级比*高,但是先用后加,p先和*结合*/
}
putchar('\n');
p = a;
/*使用二级指针遍历数组*/
for(i=0;i<10;i++)
{
printf("%d ", *(*pp)++);
}
putchar('\n');
return 0;
}
使用二级指针操作指针数组:
#include<stdio.h>
int main ()
{
char *month[] = {
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
};
char **pp;/*指向指针的指针*/
pp = month;
int i;
puts("input month:");
scanf("%d",&i);
printf("%s\n",*(pp+i-1));
printf("%s\n",pp[i-1]);
return 0;
}
指向函数的指针的定义,初始化,引用:
#include<stdio.h>
int my_func()
{
printf("this function name:%s\n",__func__);
return 0;
}
int main ()
{
/*定义一个函数指针*/
int (*p)();
/*函数指针的初始化*/
p = my_func;
/*函数指针的调用*/
p();
return 0;
}
指向函数的指针作为函数的参数:
include<stdio.h>
/*加法函数*/
int add(int a,int b)
{
return (a+b);
}
/*函数指针作为函数的参数*/
void calc(int a,int b,int(*p)(int x,int y))
{
int result = 0;
result = p(a,b);
printf("result:%d\n",result);
}
int main ()
{
int a = 5;
int b = 6;
calc(a,b,add);
return 0;
}
什么是指针函数?
如果一个函数的返回值类型是int,我们就叫他为整形函数。类似的,如果一个函数的返回值类型是指针型,我们就叫他为指针函数。
1.const修饰的变量只能进行读操作
2.const修饰指针:
--指向常量的指针:指向的变量的值不可修改
例如: int const *p = &a;
--常指针: 该指针是一个常量
例如: int * const p = &a;
--指向常量的常指针:
例如: int const * const p = &a
修饰函数参数:
例如:
int a[10];
int array_add(int len,int const a[]); 不能对a进行修改。
编译一个程序:将main的参数打印出来
代码如下:
#include<stdio.h>
int main (int argc,char *argv[])
{
int i;
printf("main函数的参数个数为:%d\n",argc);
for(i=0;i<argc;i++)
{
printf("argc[%d] = %s\n",i,argv[i]);
}
return 0;
}
--标准c99新增关键字:修饰限定指针
--规定其所修饰的指针是访问指针指向的数据对象的唯一方式:
-想要修改该指针指向内存单元的内容,只能通过该指针进行修改
-其他指向这块内存的指针都是未定义的
--使用restri修饰指针的好处
-帮助编译器更好地优化代码
-帮助用户要使用满足restrict要求的参数
什么是结构体:
---结构体是一种构造数据类型,自定义数据类型
---将不同类型的数据组成为一个整体
定义:
--先定义结构体类型,再定义结构体变量
--数据类型定义不分配内存,变量才分配内存
--结构体名,结构体成员
初始化:
--定义变量时直接初始化
--先定义变量,然后对每个成员变量逐个初始化
结构体的存储:
--内存对齐:
--字,双字和四字在内存中没有强制必须对齐
--访问未对齐的数据访问需要两次总线周期
--编译器默认将结构体中的成员数据内存对齐
--结构体只保证成员变量在内存中的存放顺序,但不能保证所占内存的大小
结构体的总大小为结构体最宽基本类型成员大小的整数倍
例1:
该结构体的大小为8
例2:
该结构体的大小为12
例3:
成员包含数组的结构体 (不会以数组的大小来对齐,仍然使用最宽数据类型对齐)
该结构体的大小为20
例4:
嵌套结构体的结构体
该结构体的大小为20
例如5:
该结构体的大小为32
例6:
成员包含联合体的结构体 (联合体大小就是成员中最大类型的大小)
该结构体的大小为12
指定对齐值 #pragma pack(n)//指定向n对齐
对齐值小于最大类型成员值 :
该结构体的大小为20
对齐值大于最大类型成员值:(当指定对齐值大于自身对齐值时,向自身对齐值对齐)
该结构体的大小为24
使用结构体指针构建动态数据结构-链表
代码如下:
#include<stdio.h>
struct node{
int data;
struct node *next;
};
int main ()
{
struct node node1,node2;
node1.data = 1;
node2.data = 2;
node1.next = &node2;
node2.next = NULL;
printf("data1 = %d,data2 = %d\n",node1.data,node1.next->data);
return 0;
}
如果函数的返回值的类型是结构体类型,那我们就称这个函数为结构体函数
共用体也是一种构造数据类型,其内存大小由其最大类型成员决定。
共用体的成员共享同一片内存,所用可以用共用体判断大小端模式,代码如下:
#include<stdio.h>
union test
{
char c;
int a;
};
int main ()
{
union test t1;
t1.a = 0x11223344;
printf("c = %x\n",t1.c);
printf("a = %x\n",t1.a);
return 0;
}
注意点:
1.公用体变量中的值是最后一次存放的成员的值
2.共用体变量的地址和各成员地址相同
3.共用体不能作为函数参数和返回值
4.公用体和结构体可以相互嵌套
枚举数据类型也是属于构造数据类型
使用枚举数据类型确定了取值范围,阅读代码直观
代码如下:
#include<stdio.h>
enum week
{
sunday ,
monday = 10,
tuesday,
wensday,
thusday,
friday,
saturday
};
int main ()
{
enum week a,b,c;
a = sunday;
b = monday;
c = tuesday;
printf("%d\n",a);
printf("%d\n",b);
printf("%d\n",c);
printf("%d\n",saturday);
return 0;
}
注意点:
枚举是常量,不能作为左值被重新赋值
枚举类型不是字符型或字符串,使用时不能加单引号或双引号
枚举可以用在关系表达式中
枚举从0开始自动赋值
枚举变量的取值范围要在枚举类型定义的范围内
几种使用情况如下:
#include<stdio.h>
int main ()
{
/*为基本数据类型定义新的类型名*/
typedef unsigned int uint;
uint i;
/*为构造数据类型定义简洁的类型名称*/
typedef struct
{
char name [20];
int age;
}student;
student stu = {"lizhiwen",20};
printf("%s,%d\n",stu.name,stu.age);
/*为数组定义简洁的名称*/
typedef int int_array_10[10];
int_array_10 a;//相当于 int a[10];
/*为指针,函数指针定义一个新名称*/
typedef char *charp;
charp name = "hello world";
puts(name);
typedef int(*myfunc)(int a,int b);
int func(int a,int b);
myfunc p;
p = func;
printf("%d\n",p(100,300));
return 0;
}
int func(int a,int b)
{
return (a+b);
}
typedef 也是用作用域的,如果在函数内,它的作用就在函数当中。
typedef与#define的区别:
1.关键字typedef只是为数据类型增加一个新的名称,并没有增加新的数据类型
2.只适用于类型名称定义,不能用来定义变量,可以简化复杂而繁杂的声明
3.typedef由编译器处理,宏由预处理器处理
柔性数组我们可以理解为大小不定的数组
----零长度数组不占用结构体空间
----使用时将数组作为结构体的成员
----GCC是通过非标准扩展零长度数组来实现的
----为什么要定义柔性数组:USB驱动代码分析
代码示例如下:
#include<stdio.h>
#include<stdlib.h>
typedef struct
{
int i;
int array[0];//不战用结构体空间
}test;
int main ()
{
printf("%ld\n",sizeof(test));
test *p = (test *)malloc(sizeof(test)+2*sizeof(int));
p->array[0] = 100;
p->array[1] = 200;
p->i = 300;
printf("%d %d %d\n",p->array[0],p->array[1],p->i);
free(p);
return 0;
}
所谓字面量就是固定数值表示:
---基本数据类型字面量:100,12.34,“hello”
复合字面量:
---构造数据类型:数组字面量,结构体字面量
优点:
---使得函数构造数据类型参数传递可以不再定义一个变量,直接使用复合字面量,省去赋值操作,使得参数传递更加简洁。
示例代码如下:
#include<stdio.h>
typedef struct {
char a[10];
int age;
}student;
void print(student stu)
{
printf("name = %s\nage = %d\n",stu.a,stu.age);
};
int main ()
{
/*复合字面量*/
print((student){"lizhiwen",20});
return 0;
}
程序的生成过程:
预处理——编译——汇编——链接——可执行文件
图解如下:
预处理过程:
-----处理程序中以#开头的预处理命令
-----执行#include命令,将使用该文件包含的头文件内容替换该指令
-----执行#define命令,将程序中使用#define定义的宏,替换为具体的数字
-----执行条件编译及其它预处理命令
预处理过后,生成的文件 还是c源文件
常见的预处理命令:
文件包含:
-----#include
宏定义:
-----#define #undef(#undef取消宏定义)
-----预定义的宏:__LINE__,__FILE__,__FUNC__
条件编译:
-----#if #else #endif
-----#ifdef #ifndef
其他命令:
-----重置行号和文件名:#line
-----产生错误信息:#error
-----修改编译器设置:#pragma
示例代码:
include<stdio.h>
float sfunc(int r);
#define PI 3.1415
int main ()
{
float pi;
pi = PI;
printf("%6.4f\n",pi);
#define s2
float s;
s = sfunc(2);
printf("s = %f\n",s);
return 0;
}
#ifdef s2
float sfunc(int r)
{
float s;
s = r*r;
return s;
}
#else
float sfunc(inr r)
{
float s;
s = r*r*r;
return s;
}
#endif
无参数的宏定义:
宏定义:
----用一些标识符作为宏名来代替一些符号或常量的命令
----在预处理阶段,预处理器会将程序中所有出现的宏名用宏定义中的字符串替换,这个过程称为宏替换或宏展开
常见的定义格式:
----#define 宏名 字符串
使用实例:
#include<stdio.h>
#define PI 3.1415926
#define STR "hello world"
#define STR2 "hello world\
Great wall"
//如果字符串过长可以使用\进行分割
int main ()
{
printf("%f\n",PI);
printf("%s\n",STR);
printf("%s\n",STR2);
return 0;
}
带参数的宏定义:
形式参数和实际参数:
----在宏定义中的参数称为形式参数
----在宏调用中的参数称为实际参数
----在展开时,不仅要宏展开,还要用实参替换形参
定义格式:
----#define 宏名(形参表)字符串
----#和##操作符的使用
----#undef的使用
需要注意的地方当字符串为较复杂的表达式时记得加括号,防止因优先级而带来运算错误
使用实例:
#include<stdio.h>
#define ADD(A,B) A+B
#define MAX(A,B) A<B?B:A
int main ()
{
printf("%d\n",ADD(2,3));
printf("%d\n",MAX(1,2));
return 0;
}
#操作符的使用:
#include<stdio.h>
#define SUM(A,B) printf("A+B = %d\n",A+B)
#define SUM2(A,B) printf(""#A" + "#B" = %d\n",A+B)
int main ()
{
SUM(2,3);
SUM2(2,3);
return 0;
}
##操作符的使用:
#include<stdio.h>
#define INT(n) a##n
int main ()
{
int INT(1),INT(2);
printf("%d %d\n"a1,a2);
return 0;
}
#undef的使用(#undef的作用是取消宏定义):
#include<stdio.h>
#define INT(n) a##n
int main ()
{
int INT(1),INT(2);
/*取消宏定义*/
#undef INT
return 0;
}
使用宏需要注意的地方:
----一般使用大写字母来表示宏名,可以放在头文件中,宏定义也可以嵌套
----使用宏定义时不要吝啬加括号,除非你考虑到了所有可能出现错误的情况
----使用圆括号括住每个参数,保证每个参数在定义表达式中能正确分组
----宏定义的作用域:定义开始到文件结尾或#undef处
----宏定义要单独占一个逻辑行,若定义字符串太长,可以使用\换行续接
预定义宏名:
#include<stdio.h>
int main ()
{
printf("当前源程序的创建日期:%s\n",__DATE__);
printf("当前远程序的文件名称(包括盘符和路径):%s\n",__FILE__);
printf("当前被编译代码的行号:%d\n",__LINE__);
printf("当前源程序的创建时间%s\n",__TIME__);
if(__STDC__ == 1)
{
printf("标准c编译器\n");
}
else
{
printf("非标准c编译器\n");
}
printf("%s\n",__func__);//__func__是预定义标识符,不是宏名
return 0;
}
头文件包含指令#include
-----函数声明
-----宏定义
-----常量定义
使用头文件的好处:
-----避免函数原型的重复声明,利于程序的模块化设计
-----避免了宏定义的多次重复声明
-----不会明显增加程序的大小:头文件的内容是编译器需要的信息,而不是加到最终代码里的具体语句
头文件搜索路径:
-----集成开发环境IDE标准路径
-----项目工程当前路径
头文件包含需要注意的一些地方:
-----头文件可以在任何地方使用#include引入
---仅仅做文本替换操作,不一定放在文件开头
-----避免多次包含同一头文件
---可以采用条件编译来避免
-----全局变量一般不要放在头文件定义
什么是条件编译:
------对程序源代码的各部分有选择地进行编译
为什么要条件编译:
------提高程序的适用性,减少目标代码的体积
------使用条件编译制作程序的不同客户版本
------使程序移植更加方便
常用的条件编译命令:
#if #else #endif
#if #elif #endif
例1: #ifdef 标识符
程序段1
#else
程序段2
#endif
例2:
#ifndef 标识符
程序段1
#else
程序段2
#endif
与第一种形式的区别是将“ifdef”改为“ifndef”。它的功能是,如果标识符未被#define命令定义过则对程序段1进行编译, 否则对程序段2进行编译。这与第一种形式的功能正相反。
重置行号和文件名#line
#include<stdio.h>
#line 1000 "hello"
int main ()
{
printf("当前代码行号:%d\n",__LINE__);
printf("当前程序文件名:%s\n",__FILE__);
return 0;
}
产生错误编译信息#error:
include<stdio.h>
//#define PI 3.14
int main ()
{
#ifdef PI
printf("pi = %f\n",PI);
#else
#error "NO PI define"
#endif
return 0;
}
修改编译器设置#pragma:
#pragma message("编译信息")
结构体对齐#prama:
对齐模数:
-----跟处理器相关,强制内存对齐
-----可以简化处理器和内存之间传输系统的设计
结构体内成员按各自对齐模数对齐
结构体按结构体模数对齐
对齐模数大小的确定:
------结构体成员对齐模数是#pragma指定值(或默认值)和该成员所占空间长度的较小值
------结构体对齐模数是#pragma指定值(或默认值)和结构体内最大的成员数据类型长度的较小值
#pragma pack(4)
静态内存分配
动态内存分配:堆式存储分配,栈式存储分配
堆与栈的区别:
内存分配和回收方式不同:
------栈由操作系统自动分配和释放,速度快
------堆一般由程序员自己分配和释放,一般速度比较慢,而且容易产生碎。若程序员不释放,程序结束时可能可能由OS回收
存放的内容不同:
------栈一般存放函数的参数,局部变量
------堆的存放内容没有限制,由程序员决定
大小限制不同:
------栈是由高地址向低地址扩展的一片连续内存区域,栈顶的地址和栈的容量是系统预先定义好的
------堆是由低地址向高地址扩展,区域可以不连续,空间大,使用比较灵活,一般由内存管理来管理这片区域
数据结构不同:
------栈是一种先进后出的数据结构
------堆的分配方式类似于链表,整个内存结构可以看做是一棵树
内存分配函数malloc和内存释放函数free函数的使用:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main ()
{
char *p = (char *)malloc(100);
if(!p)
{
printf("malloc failed!\n");
return -1;
}
strcpy(p,"helloworld!");
printf("%s\n",p);
free(p);
return 0;
}
内存分配函数calloc和的使用:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct student{
int num;
int age;
float score;
};
int main ()
{
struct student *p;
p = (struct student *)calloc(10,sizeof(struct student));
if(!p)
{
printf("calloc failed\n");
return -1;
}
return 0;
}
调整已分配内存函数realloc的使用:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main ()
{
int *p = (int *)malloc(100);
if(!p)
{
printf("malloc failed !\n");
return -1;
}
printf("p = %p\n",p);
p = realloc(p,200);
if(!p)
{
printf("realloc failed!\n");
return -1;
}
printf("p = %p\n",p);
free(p);
return 0;
}
使用动态内存常见的一些问题:
----内存未分配成功而直接使用
----内存分配成功,尚未初始化而直接使用
----内存分配成功且已初始化,但操作越过了内存的边界
----使用完毕后忘记释放内存,造成内存泄漏
----野指针
以下程序回造成内存泄漏,malloc一直在申请内存,同时指向这片内存的指针p一直在改变。
#include<stdio.h>
#include<stdlib.h>
int main ()
{
int *p;
while(1)
{
p = (int *)malloc(1000000);
if(!p)
{
printf("malloc failed !\n");
return -1;
}
printf("malloc success!\n");
}
return 0;
}
如何防止内存泄漏:
------ 使用malloc申请的内存使用结束后及时释放
------注意保存malloc函数返回的指针
------防止内存越界,可能会破坏其他动态内存的信息,造成free失败
------对于对人开发,要坚持谁申请谁释放
------每次申请后要先检查是否成功然后在使用
------分配空间时可以多申请一点
------函数错误退出时,不要忘了其他已经分配的空间
如何避免野指针:
-----内存被释放后,要及时将指针赋值为NULL,避免对这个指针的继续使用
-----指针初始化时要记得给一个初值,随机值很危险
流的概念:
输入输出I\O流 (流可以看做是一个逻辑设备)
流与缓冲区
标准输入输出:
-----标准输入流(设备):stdin:与键盘相连
-----标准输出流(设备):stdout:与显示器相连
-----标准错误流(设备):stderr:与显示器设备相连
当使用键盘输入数据的时候,数据不是直接存放标准输入流里面的,先存放在缓冲区里面,随后更新到标准输入流里面。
什么是文件:
-----文件是一组相关数据的有序集合
-----数据集的名称叫做文件名
-----文件一般存储在磁盘等外部介质上,使用时读取到内存
文件的分类:
----用户角度:普通文件和设备文件
----编码方式:文本文件和二进制文件
文件缓冲区
文件指针:
----指向一个文件的指针变量称为文件指针
----通过该指针可以对文件进行各种操作
----FILE *fp
常见的文件打开方式:
----只读:r(以r打开的文件必须已经存在)
----只写:w(以w打开的文件不存在时会自动创建)
----读和写方式:+
----追加:a(在文件的内容末尾编译)
----文本文件:t(默认打开方式,可省略不写)
----二进制文件:b
#include<stdio.h>
int main ()
{
FILE *fd;
fd = fopen("test.bin","wb");
if(!fd)
{
printf("fopen failed !\n");
return -1;
}
fclose(fd);
return 0;
}
字符读写函数:fgetc(),fputs()
字符串读写函数:fgets(),fputs()
数据块读写函数:fread(),fwrite()
格式化读写函数:fscanf(),fprintf()
fgetc和fputc函数的使用:
#include<stdio.h>
int main ()
{
FILE *fd;
int ch;
fd = fopen("test.c","wt");
if(!fd)
{
printf("fopen failed !\n");
return -1;
}
fputc('a',fd);//写入一个字符
fclose(fd);
fd = fopen("test.c","rt");
if(!fd)
{
printf("fopen failed !\n");
return -1;
}
ch = fgetc(fd);//读取一个字符
while(ch != EOF)
{
putchar(ch);
ch = fgetc(fd);
}
putchar('\n');
fclose(fd);
return 0;
}
fgets和puts的使用:
#include<stdio.h>
int main ()
{
FILE *fd;
fd = fopen("./test.c","wt");
if(!fd)
{
printf("fopen failed!\n");
return -1;
}
fputs("#include<stdio.h>",fd);//写入一个字符串
fclose(fd);
char str[50];
fd = fopen("./test.c","rt");
if(!fd)
{
printf("fopen failed!\n");
return -1;
}
fgets(str,20,fd);//读取一个字符串
printf("%s\n",str);
fclose(fd);
return 0;
}
fread和fwrite的使用:
fwrite()函数从数组buffer(缓冲区)中,写count个大小为size(大小)的对象到stream(流)指定的流,返回值是已写的对象的数量
fprintf和fscanf的使用:
fprintf函数根据指定的format(格式)发送信息(参数)到由stream指定的文件。fprintf只能一样工作,fprintf的返回值是输出的字符数,发生错误时返回一个负值。
fscanf从指定的stream里面获取数据
文件检测函数:
feof()函数
---检测文件指针是否已经到达文件末尾
---返回0:文件指针没有到达文件末尾
---文件结束标志:EOF(end of file)
ferror()函数
---检测文件的操作状态是否出错
---文件操作出错返回真, 否则返回0
clearrr()函数
---文件错误标志非0时,所有的文件操作均无效
虽然我们可以关闭然后重新打开文件,重置文件指针,但是这样非常的不方便。c标准库提供了文件定位函数fseek()
---fseek(文件指针,偏移量,起始位置)
---起始位置:0-文件头 1-当前位置 2-文件末尾
获取当前位置函数:ftell()
---ftell(文件指针)
---返回值当前文件指针距离文件头部的偏移
重置文件位置指针:rewind()
---rewind(文件指针)
---并清除文件错误标志
文章浏览阅读2.5w次,点赞6次,收藏50次。官方解释是,docker 容器是机器上的沙盒进程,它与主机上的所有其他进程隔离。所以容器只是操作系统中被隔离开来的一个进程,所谓的容器化,其实也只是对操作系统进行欺骗的一种语法糖。_docker菜鸟教程
文章浏览阅读5.7k次,点赞3次,收藏14次。该如何避免的,今天小编给大家推荐两个下载Windows系统官方软件的资源网站,可以杜绝软件捆绑等行为。该站提供了丰富的Windows官方技术资源,比较重要的有MSDN技术资源文档库、官方工具和资源、应用程序、开发人员工具(Visual Studio 、SQLServer等等)、系统镜像、设计人员工具等。总的来说,这两个都是非常优秀的Windows系统镜像资源站,提供了丰富的Windows系统镜像资源,并且保证了资源的纯净和安全性,有需要的朋友可以去了解一下。这个非常实用的资源网站的创建者是国内的一个网友。_msdn我告诉你
文章浏览阅读1.2k次。vue2封装对话框el-dialog组件_
文章浏览阅读4.7k次,点赞5次,收藏6次。MFC 文本框换行 标签: it mfc 文本框1.将Multiline属性设置为True2.换行是使用"\r\n" (宽字符串为L"\r\n")3.如果需要编辑并且按Enter键换行,还要将 Want Return 设置为 True4.如果需要垂直滚动条的话将Vertical Scroll属性设置为True,需要水平滚动条的话将Horizontal Scroll属性设_c++ mfc同一框内输入二行怎么换行
文章浏览阅读832次。检查Linux是否是否开启所需端口,默认为6379,若未打开,将其开启:以root用户执行iptables -I INPUT -p tcp --dport 6379 -j ACCEPT如果还是未能解决,修改redis.conf,修改主机地址:bind 192.168.85.**;然后使用该配置文件,重新启动Redis服务./redis-server redis.conf..._redis-server doesn't support auth command or ismisconfigured. try
文章浏览阅读4.9k次。济大数电实验报告_数据选择器及其应用
文章浏览阅读236次。1研究内容消费在生产中占据十分重要的地位,是生产的最终目的和动力,是保持省内经济稳定快速发展的核心要素。预测河南省社会消费品零售总额,是进行宏观经济调控和消费体制改变创新的基础,是河南省内人民对美好的全面和谐社会的追求的要求,保持河南省经济稳定和可持续发展具有重要意义。本文建立灰色预测模型,利用MATLAB软件,预测出2019年~2023年河南省社会消费品零售总额预测值分别为21881...._灰色预测模型用什么软件
文章浏览阅读1.2k次。12.4-在Qt中使用Log4Qt输出Log文件,看这一篇就足够了一、为啥要使用第三方Log库,而不用平台自带的Log库二、Log4j系列库的功能介绍与基本概念三、Log4Qt库的基本介绍四、将Log4qt组装成为一个单独模块五、使用配置文件的方式配置Log4Qt六、使用代码的方式配置Log4Qt七、在Qt工程中引入Log4Qt库模块的方法八、获取示例中的源代码一、为啥要使用第三方Log库,而不用平台自带的Log库首先要说明的是,在平时开发和调试中开发平台自带的“打印输出”已经足够了。但_log4qt
文章浏览阅读786次。全局观思维模型,一个教我们由点到线,由线到面,再由面到体,不断的放大格局去思考问题的思维模型。_计算机中对于全局观的
文章浏览阅读330次。一、CountDownLatch介绍CountDownLatch采用减法计算;是一个同步辅助工具类和CyclicBarrier类功能类似,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。二、CountDownLatch俩种应用场景: 场景一:所有线程在等待开始信号(startSignal.await()),主流程发出开始信号通知,既执行startSignal.countDown()方法后;所有线程才开始执行;每个线程执行完发出做完信号,既执行do..._countdownluach于cyclicbarrier的用法
文章浏览阅读508次。Prometheus 算是一个全能型选手,原生支持容器监控,当然监控传统应用也不是吃干饭的,所以就是容器和非容器他都支持,所有的监控系统都具备这个流程,_-自动化监控系统prometheus&grafana实战
文章浏览阅读4.7k次。输入关键字,可以通过键盘的搜索按钮完成搜索功能。_react search