常量的定义
常量的定义可以有两种方式
const
格式:const 数据类型 常量名 = 常量值
示例:const double PI = 3.14
#define
格式:#define 常量名 常量值
示例:#define SUM = 1 + 2
const 和 #define 的区别
- const 定义的常量时带类型,#define 不带类型
- const 是在 编译、运行的时候起作用,而 #define 是在编译的预处理阶段起作用
- #define 只是简单的替换,没有类型检查。简单的字符串替换会导致边界效应
- const 常量可以进行调试的,#define 是不能进行调试的,主要是预编译阶段就已经替换掉了,调试的时候就没它了
- const 不能重定义,不可以定义两个一样的,而 #define 通过 #undef 取消某个符号的定义,再重新定义
- define 可以配合
#ifdef
、#ifndef
、#endif
来使用, 可以让代码更加灵活,比如我们可以通过 #define 来启动或者关闭调试信息。
代码示例:
1 |
|
输出结果:
从上述的输出结果,可以看出#define
仅仅是预编译之后将常量替换成原来的表达式。
取模运算符
除法运算符的底层原理:
A % B
在编译器中会自动翻译为:A - ((A) / (B)) * (B)
。
示例:
1 |
|
输出结果:
从取模的底层原理可以得出结论:取模数不可以为零,形如:2 % 0
这样的表达式是非法的。
算术运算中的自动类型转换
在基本运算过程中,一定要注意 C 语言中不带小数点的数字默认类型是 int 类型,带小数点的数字默认类型是 double 类型,在计算中可能会存在类型自动转换问题,如下面的代码示例:
1 |
|
输出结果:
自增/自减运算
如果变量仅仅是自己自增或者自减操作,那么仅仅影响变量本身。
如果自增或自减并赋值给另外一个变量,那么会有赋值顺序。
示例:
1 |
|
输出结果:
变量和指针
指针是 C 语言的灵魂,指针就是地址,地址就是指针:
1 |
|
输出结果:
上述的执行结果,可能存在不一样,但是num 变量本身的值所在的内存地址
和ptr 变量本身的值
两者一定是相等的。
上述执行结果解释:
在源码被编译之后,所有的变量都会变成了汇编指令中的开辟一块内存空间的意思,而这个赋值操作就是对这块开辟的内存空间进行设置值。
好比一位客人要进入酒店入住:
这位客人要告诉前台,我要住什么类型的房间,是单人间还是双人间,这里的房间类型就是变量的类型,也就是
int
类型。这位客人还要告诉前台,你选择的这个房间要住具体的人是谁,这里的具体的人就是变量被赋值的值,也就是数字
10
。
以上过程中前台还需要考虑一下这位客人要求开的房间类型和要住的人是不是匹配,也就是变量申明的类型和赋值的值类型是否匹配
经过以上过程之后,这位客人就可以办理入住手续并顺利入住了。
注意的是客人说你们的房间号都是一大串数字记忆起来太麻烦了,我出门之后很容易忘记我的房间号,能不能我给我这个房间号起一个别名。于是前台说,那你自己起一个自己能记住的房间号别名吧,我帮你做好映射关系。于是这位客人给自己的房间号起了个别名叫num
,也就是变量的名字。
当这个人被入住进入这个房间之后,酒店老板了来问前台,刚才开的房间里住着是谁啊,前台想那么多的房间号都是数字,我要是顺着数字找我也费劲,刚才的客人不是给自己起了个房间号别名叫num
么,我也做好了映射,直接&num
就能知道这个人的房间号是多少,并且这个别名就是这个人起的,所以这个别名就代表了这个房间里住的人。也是前台很爽快的回复老板,这个人是num
,他住的房间号是&num
。
这时老板再问,人既然住在了这个房间里,那么这个房间号你是记在哪里了呢,总不会用脑袋记忆吧,并且这个人所住的房间类型,我也不知道啊。聪明的前台说,这个已经记住了,就是在客人起房间号别名的时候就做好了映射记录了。我专门设置了一个专门用来存房间号的房间,叫ptr
,这特殊的房间里面不住客人,只把某位客人所住的房间号码放在这个房间里,光记住这个客人的房间号不够,这个特殊的房间还是特殊的类型,叫客人房间号的类型,也就是int *
。
前台很爽快的回复老板,这个ptr
房间里存的某位客人的房间号就是:ptr
,这位客人就是:* ptr
。
从上述比喻可以看出,ptr
房间里的东西存的就是客人的房间号码,只要谁拿到了这个客人的房间号码,那么就可以对这位客人为所欲为了。
如果某个杀手知道了这个房间号码,带把枪把这个房间号码里住的人给枪杀了,那么是个危险的事情。当然如果某个医生知道这个房间号码,就能在第一时间及时抢救这位客人。
变量的作用域
局部变量,系统不会对其默认初始化,必须对局部变量初始化之后才能使用,否则程序运行可能会异常退出。
全局变量,系统会自动对其初始化,如下所示:
数据类型 | 初始化默认值 |
---|---|
int | 0 |
char | ‘\0’ |
float | 0.0 |
double | 0.0 |
pointer 指针 | NULL |
正确地初始化变量是一个良好的编程习惯。否则有时程序可能产生意想不到的结果,因为未初始化的变量会保存一些内存位置中已经可用的垃圾值。
static 关键字
static
关键字在 C 语言中比较常用,使用恰当能够大大提高程序的模块化特性,有利于扩展和维护。
局部变量使用 static
局部变量被 static 修饰后,我们称为静态局部变量
对应静态局部变量在声明时未赋初值,编译器也会把它初始化为默认值。
数据类型 | 初始化默认值 |
---|---|
int | 0 |
char | ‘\0’ |
float | 0.0 |
double | 0.0 |
pointer 指针 | NULL |
静态局部变量存储于进程的静态存储区(全局性质), 只会被初始化一次,即使函数返回,它的值也会保持不变
代码示例:
1 |
|
输出结果:
全局变量使用 static
普通全局变量对整个工程可见,其他文件可以使用extern
外部声明后直接使用。也就是说其他文件不能再定义一个与其相同名字的变量了(否则编译器会认为它们是同一个变量),静态全局变量仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响。
定义不需要与其他文件共享的全局变量时,加上static
关键字能够有效地降低程序模块之间的耦合,避免不同文件同名变量的冲突,且不会误使用。
代码示例:
file01.c 文件中定义全局变量:
1 | int num1 = 10; // 普通全局变量 |
file02.c 文件中引入普通全局变量:
1 |
|
输出结果:
代码示例:
file01.c 文件中定义全局变量:
1 | int num1 = 10; // 普通全局变量 |
file02.c 文件中引入普通全局变量:
1 |
|
输出结果:
从第二个示例可以看出,一般在文件中定义全局变量的时候,如果不想其他文件使用,那么就使用static
修饰,其他文件引用了也没关系,因为它们要想使用这个变量,就得自己重新定义一个相同名字的全局变量。从而使得自己的全局变量”很安全”。
函数使用 static
函数的使用方式与全局变量类似,在函数的返回类型前加上static,就是静态函数。
非静态函数可以在另一个文件中通过extern
引用。
静态函数只能在声明它的文件中可见,其他文件不能引用该函数。
不同的文件可以使用相同名字的静态函数,互不影响。
代码示例:
file03.c 文件中定义函数
1 |
|
file04.c 文件中使用外部文件中的函数:
1 |
|
输出结果:
和静态全局变量一样,本文件中引入了外部文件中的静态函数,如果想使用相同名称的函数名,那么需要自己重新定义:
file03.c 文件中定义函数:
1 | #include <stdio.h> |
file04.c 文件中使用外部文件中的函数:
1 |
|
输出节果:
系统函数库
string.h 字符串函数库
字符串操作常常依赖系统依赖提供的字符串函数:
得到字符串的长度
size_t strlen(const char *str)
计算字符串 str 的长度,直到空结束字符,但不包括空结束字符 。
拷贝字符串
char *strcpy(char *dest, const char *src)
把 src 所指向的字符串复制到 dest。
连接字符串
char *strcat(char *dest, const char *src)
把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。
代码示例:
1 |
|
输出节果:
time.h 时间日期函数库
获取当前时间
char *ctime(const time_t *timer)
返回一个表示当地时间的字符串,当地时间是基于参数 timer。
时间差计算
double difftime(time_t time1, time_t time2)
返回 time1 和 time2 之间相差的秒数 (time1-time2)。
代码示例:
1 |
|
输出节果:
计算某个函数的执行时间:
1 |
|
输出节果:
预处理命令
使用库函数之前,应该用#include
引入对应的头文件。这种以#
号开头的命令称为预处理命令。
这些在编译之前对源文件进行简单加工的过程,就称为预处理
(即预先处理、提前处理)。
预处理主要是处理以#
开头的命令,例如 #include <stdio.h> 等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面。
预处理是 C 语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时, 系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。
C 语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
应用场景:
开发一个C语言程序,让它暂停 5 秒以后再输出内容 “hello world”,并且要求跨平台,在 Windows 和 Linux 下都能运行,如何处理?
提示:
Windows 平台下的暂停函数的原型是void Sleep(DWORD dwMilliseconds)
,参数的单位是”毫秒”,位于<windows.h>
头文件。
Linux 平台下暂停函数的原型是unsigned int sleep (unsigned int seconds)
,参数的单位是”秒”,位于<unistd.h>
头文件。
#if
、#elif
、#endif
就是预处理命令,它们都是在编译之前由预处理程序来执行的。
代码:
1 |
|
上述源文件在预编译之后的源码中,会根据不同的平台生成对应的源码,如 windows 平台则会生成:
1 |
|
宏定义
宏定义命令
#define
叫做宏定义命令,它也是 C 语言预处理命令的一种。所谓宏定义, 就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。
代码示例:
1 |
|
输出节果:
从上述示例代码输出结果可以得出结论:
int sum = 20 + N
,N
被 100 代替了。#define N 100
就是宏定义,N
为宏名,100 是宏的内容(宏所表示的字符串)。在预处理阶段,对
程序中所有出现的宏名
,预处理器都会用宏定义中的字符串去代换,这称为宏替换
或宏展开
。- 宏定义是由源程序中的宏定义命令
#define
完成的,宏替换是由预处理程序完成的。
宏定义的形式
#define 宏名 字符串
#
表示这是一条预处理命令,所有的预处理命令都以 # 开头。宏名是标识符的一种,命名规则和变量相同。字符串可以是数字、表达式、if 语句、函数等
这里所说的字符串是一般意义上的字符序列,不要和 C 语言中的字符串等同,它不需要双引号
程序中反复使用的表达式就可以使用宏定义
代码示例:
1 |
|
输出节果:
宏定义的注意事项
宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单的替换。字符串中可以含任何字符, 它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换
宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef
命令
1 |
|
代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替
1 |
|
宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换
1 |
习惯上 宏名用大写字母表示,以便于与变量区别。但也允许用小写字母
可用宏定义表示数据类型,使书写方便
1 |
|
宏定义表示数据类型和用typedef
定义数据说明符的区别:宏定义只是简单的字符串替换,由预处理器来处理;而 typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。
带参数的宏定义
C 语言允许宏带有参数。在宏定义中的参数称为形式参数
,在宏调用中的参数称为实际参数
,这点和函数有些类似
对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参
带参宏定义的一般形式为#define 宏名( 形参列表) 字符串
,在字符串中可以含有各个形参
带参宏调用的一般形式为:宏名( 实参列表)
代码示例:
1 |
|
输出结果:
带参宏定义的注意事项
带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现
1 |
|
在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型
在宏定义中,字符串内的形参通常要用括号括起来以避免出错。
1 |
|
输出结果:
带参宏定义和函数的区别
宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。
函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码
代码示例:
1 |
|
输出结果:
代码示例:
1 |
输出结果:
常见预处理命令
预处理指令是以#
号开头的代码行,#
号必须是该行除了任何空白字符外的第一个字符。
#
后是指令关键字,在关键字和#
号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。
指令 | 说明 |
---|---|
# | 空指令,无任何效果 |
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#if | 如果给定条件为真,则编译下面代码 |
#ifdef | 如果宏已经定义,则编译下面代码 |
#ifndef | 如果宏没有定义,则编译下面代码 |
#elif | 如果前面的#if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个#if … #else 条件编译块 |
字符串的表示
字符串可以使用字符数组表示,也可以使用字符指针指向一个字符串:
用字符数组存放一个字符串
1 | char str1[]="hello world"; |
用字符指针指向一个字符
1 | char * pStr = "yes"; |
C语言对字符串常量”hello world”是按字符数组处理的,在内存中开辟了一个字符数组用来存放字符串常量,程序在定义字符串指针变量pStr
时只是把字符串首地址(即存放字符串的字符数组的首地址)赋给pStr
,图示:
注意上图中的内存单元格中本质存储的不是字符,而是字符对应的 ASCII 码值。
细节注意:使用字符指针变量指向字符串时,首先这个指针变量会有自己的地址空间,在自己的地址空间中存储着字符数组的首地址。
示例代码:
1 |
|
输出结果:
注意事项:
字符数组由若干个元素组成,每个元素放一个字符;而字符指针变量中存放的是地址(字符串/字符数组的首地址),绝不是将字符串放到字符指针变量中(是字符串首地址)
对字符数组只能对各个元素赋值,不能用以下方法对字符数组赋值
1 | char str[14]; |
对字符指针变量,采用下面方法赋值是可以的:
1 | char * PStr = "yes"; |
图示:
小结:如果定义了一个字符数组,那么它有确定的内存地址(即字符数组名是一个常量);而定义一个字符指针变量时,它并未指向某个确定的字符数据,并且可以多次赋值。
指针
指针是一个变量,其值为另一个变量的地址( 上一小节的意图已经说明 ),即内存位置的直接地址。就像其他变量或常量一样,在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:
1 | int *intPtr; /* 一个整型的指针 */ |
指针的算术运算
指针是一个用数值表示的地址。可以对指针执行算术运算。可以对指针进行四种算术运算:++、–、+、-。
指针递增操作(++)
数组在内存中是连续分布的。当对指针进行++时,指针会按照它指向的数据类型字节数大小增加,比如int *
指针,每++
一次就增加 4 个字节。
代码示例:
1 |
|
输出节果:
从上图输出的结果可以看出,地址的自增操作,其地址的值是按照对应的指针类型的大小进行自增的。
指针递减操作(–)
递减操作和递增操作同理。数组在内存中是连续分布的。当对指针进行–时,指针会按照它指向的数据类型字节数大小减少,比如 int *
指针,每--
一次 就减少 4 个字节。
代码示例:
1 |
|
输出节果:
指针的+/-操作
当可以对指针按照指定的字节数大小进行 + 或者 – 的操作,可以快速定位你要的地址。
代码示例:
1 |
|
输出节果:
指针数组
要让数组的元素 指向 int 或其他数据类型的地址(指针)。可以使用指针数组。
指针数组定义:数据类型 *指针数组名[大小]。
1 | int *ptr[3]; |
ptr 声明为一个指针数组
由 3 个整数指针组成。因此,ptr 中的每个元素,都是一个指向 int 值的指针。
代码示例:
1 |
|
输出节果:
内存布局图:
指针数组应用实例
定义一个指向字符的指针数组来存储字符串列表(四大名著书名), 并通过遍历 该指针数组,显示字符串信息,即:定义一个指针数组,该数组的每个元素,指向的是一个字符串。
代码示例:
1 |
|
输出节果:
二级及多重指针
指向指针的指针是一种 多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。
一个指向指针的指针变量必须如下声明,即在变量名前放置两个星号。
声明了一个指向 int 类型指针的指针:
1 | int **ptr; // ptr 的类型是:int ** |
当一个目标值被一个指针间接指向到另一个指针时,访问这个值需要使用两个星号运算符,比如**ptr
代码示例:
1 |
|
输出节果:
指针函数
当函数的形参类型是指针类型时,是使用该函数时,需要传递指针,或者地址,或者数组给该形参
传递指针(地址)给函数
代码示例:
1 |
|
输出结果:
返回指针的函数
C语言 允许函数的返回值是一个指针(地址),这样的函数称为指针函数。
代码示例:
1 |
|
输出结果:
返回指针函数的注意事项
用指针作为函数返回值时需要注意,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针不能指向这些数据。
函数运行结束后会销 毁该函数所 有的局部数据 ,这里所谓的销毁并不是将局部数据所占用的内存全部清零,而是程序放弃对它的使用权限,后面的代码可以使用这块内存。
C 语言不支持在调用函数时返回局部变量的地址,如果确实有这样的需求,需要定义局部变量为 static 变量。因为被 static 修饰的局部变量会存储在静态数据区,而不是栈中。
代码示例(危险的使用示例):
1 |
|
输出结果:
代码示例(正确的使用示例,使用 static 修饰局部变量):
1 |
|
输出结果:
应用实例
编写一个函数,它会生成 10 个随机数,并使用表示指针的数组名(即第一个数组元素的地址)来返回它们。
代码示例:
1 |
|
输出结果:
函数指针
概念
一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。
把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指 针。
函数指针定义
1 | returnType (*pointerName)(param list) |
说明:
- returnType 为函数返回值类型
- pointerName 为指针名称
- param list 为函数指针指向的函数参数列表
- 参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称
- 注意
()
的优先级高于*
,第一个括号不能省略,如果写作returnType *pointerName(param list);
就成了函数原型,它表明函数的返回值类型为returnType *
应用实例
用 函数指 针来实现对函数的调用,返回两个整数中的最大值。
代码示例:
1 |
|
输出结果:
回调函数
概念
函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。
简单的讲:回调函数是由别人的函数执行时调用你传入的函数(通过函数指针完成)。
应用实例
使用回调函数的方式,给一个整型数组int arr[10]
赋 10 个随机数。
代码示例:
1 |
|
输出结果:
空指针
指针变量存放的是地址,从这个角度看指针的本质就是地址。
变量声明的时候,如果没有确切的地址赋值,为指针变量赋一个 NULL 值是好的编程习惯。
赋为 NULL 值的指针被称为空指针,NULL 指针是一个定义在标准库<stdio.h>
中的值为零的常量#define NULL 0
代码示例:
1 |
|
输出结果:
内存动态分配
C 程序中,不同数据在内存中分配说明:
全局变量:内存中的静态存储区
非静态的局部变量:内存中的动态存储区(stack 栈)
临时使用的数据:建立动态内存分配区域,需要时随时开辟,不需要时及时释放(heap 堆)
根据需要向系统申请所需大小的空间,由于未在声明部分定义其为变量或者数组,不能通过变量名或者数组名来引用这些数据,只能通过指针来引用)
内存动态分配的相关函数
头文件#Include <stdlib.h>
声明了四个关于内存动态分配的函数:
malloc
函数原型:void * malloc (usigned int size)
作用:在内存的 动态存储 区( 堆区)中分配一个长度为size
的连续空间。
形参size
的类型为无符号整型,函数返回值是所分配区域的第一个字节的地址,即此函数是一个指针型函数,返回的指针指向该分配域的开头位置。
malloc(100);
表示开辟 100 字节的临时空间,返回值为其第一个字节的地址。
calloc
函数原型:void * calloc (unsigned n, unsigned size)
作用:在内存的动态存储区中分配n
个长度为size
的连续空间,这个空间一般比较大,足以保存一个数组。
用 calloc 函数可以为一维数组开辟动态存储空间,n 为数组元素个数,每个元素长度为 size。
函数返回指向所分配域的起始位置的指针,分配不成功,返回NULL。
p = calloc(50, 4);
表示开辟50*4
个字节临时空间,把起始地址分配给指针变量 p
free
函数原型:void free (void *p)
作用:释放变量p
所指向的动态空间,使这部分空间能重新被其他变量使用。
p
是最近一次调用calloc
或malloc
函数时的函数返回值。
free 函数无返回值
free(p);
表示释放p
所指向的已分配的动态空间
realloc
函数原型:void *realloc (void *p, unsigned int size)
作用:重新分配malloc
或calloc
函数获得的动态空间大小,将p指向的动态空间大小改变为size
,p
的值不变,分配失败返回NULL。
realloc(p, 50);
表示将p
所指向的已分配的动态空间改为 50 字节。
返回类型说明
C99 标准把以上的malloc
,calloc
,realloc
函数的基类型定为void
类型,这种指针称为无类型指针(typeless pointer),即不指向哪一种具体的类型数据,只表示用来指向一个抽象的类型的数据,即仅提供一个纯地址,而不能指向任何具体的对象。
void指针类型
C99 允许使用基类型为 void 的指针类型。可以定义一个基类型为 void 的指针变量(即 void * 型变量),它不指向任何类型的数据。请注意:不要把“指向 void 类型”理解为能指向“任何的类型”的数据,而应该理解为“指向空类型”或“不指向确定的类型”的数据。在将它的值赋给另一指针变量时由系统对它进行类型转换,使之适合于被赋值的变量的类型。例如:
1 | int a = 3; // 定义 a 为整型变量 |
说明:当把 void 指针赋值给不同基类型的指针变量(或相反)时,C99 及以上的编译系统会自动进行转换,不必用户自己强制转换。例如:
1 | p3 = &a; |
相当于p3 = (void *)&a;
,赋值后 p3 得到 a 的纯地址,但并不指向 a,不能通过 *p3 输出 a 的值。
应用实例
动态创建数组,输入 5 个学生的成绩,另外一个函数检测成绩低于 60 分的,输出不合格的成绩。
代码示例:
1 |
|
输出结果:
动态分配内存的基本原则
避免分配大量的小内存块。分配堆上的内存有一些系统开销,所以分配许多小的内存块比分配几个大内存块的系统开销大
仅在需要时分配内存。只要 使用完堆上的内存块, 就需要及时释放它(如果使用动态分配内存,需要遵守原则:谁分配,谁释放), 否则可能出现内存泄漏
总是确保释放以分配的内存。在编写分配内存的代码时,就要确定在代码的什么地方释放内存
在释放内存之前,确保不会无意中覆盖堆上已分配的内存地址,否则程序就会出现内存泄漏 。在循环中分配内存时,要特别小心。
结构体
结构体的声明方式:
1 | struct 结构体名称 { |
注意:结构体名称一般首字母大写,结构体的花括号后面紧跟;
分号。
形如:
1 | struct Student { |
成员
从叫法上看:有些书上称为成员,有些书说 结构体包含的变量
成员是结构体的一个组成部分,一般是基本数据类型
、也可以是数组
、指针
、结构体
等 。
声明细节
成员声明语法同变量一声明方式一样,示例: 数据类型 成员名;
字段的类型可以为:基本类型、数组或指针、结构体等。
在创建一个结构体变量后,需要给成员赋值,如果没有赋值就使用可能导致程序异常终止。
不同结构体变量的成员是独立,互不影响,一个结构体变量的成员更改,不影响另外一个。
定义方式
方式一:先定义结构体,再创建结构体变量
示例:
1 | struct Student { |
方式二:在定义结构体的同时定义结构体变量
示例:
1 | struct Student { |
方式三:如果只需要 stu1 和 stu2 两个变量,后面不需要再使用该结构体数据类型去定义其他变量,在定义结构体时可以不给出结构体名称。这种结构体称为匿名结构体。
示例:
1 | struct { |
成员值的获取和赋值
结构体和数组类似,也是一组数据的集合,整体使用没有太大的意义。数组使用下标[]
获取单个元素,结构体使用点号.
获取单个成员。获取结构体成员的一般格式为:
1 | 结构体变量名.成员名; |
赋值操作示例 1:
1 | struct Student { |
赋值操作示例 2:
1 | struct Student { |
说明:结构体声明时带不带结构体名称不会影响赋值上述俩种赋值操作。
应用实例
小狗案例
编写一个 Dog 结构体,包含 name(char[10])、age(int)、weight(double) 属性
编写一个 say 函数,返回字符串,方法返回信息中包含所有成员值。
在 main 方法中,创建 Dog 结构体变量,调用 say 函数,将调用结果打印输出 。
代码示例:
1 |
|
输出结果:
景区门票案例
一个景区根据游人的年龄收取不同价格的门票。
请编写游人结构体(Visitor),根据年龄段决定能够购买的门票价格并输出
规则:年龄大于18,门票为20元,其它情况免费。
可以循环从控制台输入名字和年龄,打印门票收费情况, 如果名字输入 n,则退出程序。
代码示例:
1 |
|
输出结果:
共用体
共用体(Union )属于 构造类型,它可以包含多个类型不同的成员。和结构体非常类似,但是也有不同的地方。
共用体有时也被称为联合或者联合体,定义格式为:
1 | union 共用体名 { |
结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
定义方式
方式一:先定义共用体,再创建共用体变量
1 | union data { |
方式二:在定义共用体的同时定义共用体变量
1 | union data { |
方式三:如果只需要 a、b、c 三个变量,后面不需要再使用该共用体数据类型去定义其他变量,在定义共用体时可以不给出共用体名称。这种共用体称为匿名共用体。
示例:
1 | union { |
共用体成员
占用空间细节注意
代码示例:
1 |
|
输出结果:
内存占用详解
上述输出结果的内存示意图:
共用体的占用空间是以占用空间最大的成员为准,因此初始化的共用体内存占用情况为:
成员变量的占用空间均是从低位对齐,赋值操作也是从低位填充:
读取共用体中的成员变量,也是根据该成员变量的类型大小,从低位开始读取:
因此共用体内存状态处于上述图示所示时,依次读取成员变量的值为:
char 类型读取到的十进制数值为:64,对应 ASCII 码表为@
符号。
short 类型读取到的十进制数值为:64。
int 类型读取到的十进制数值为:64。
再次对 int 类型的成员变量进行赋值操作:
再次读取共用体中成员变量的值,图示:
char 类型读取到的十进制数值为:89,对应 ASCII 码表为Y
符号。
short 类型读取到的十进制数值为:8281。
int 类型读取到的十进制数值为:8281。
再次赋值,次读取:
其中 short 类型首先读取到的二进制为:1010,1101,0101,0100
,这个二进制对计算机来说是补码,因此计算之前要进行转码,将符号位不变,其他位取反并加 1,得到的值是个正数,再加上负号,就是十进制原码数值。
char 类型读取到的十进制数值为:84,对应 ASCII 码表为T
符号。
最佳实践
现有一张关于学生信息和教师信息的表格:
学生信息包括姓名、编号、性别、职业、分数。
教师的信息包括姓名、编号、性别、职业、教学科目。
请看下面的表格:
name | num | gender | profession | score/course |
---|---|---|---|---|
孙二娘 | 501 | 女(f) | 学生(s) | 90.5 |
吴用 | 302 | 男(m) | 老师(t) | math |
顾大嫂 | 109 | 女(f) | 老师(t) | english |
林冲 | 982 | 男(m) | 学生(s) | 95.5 |
请使用共用体编程完成。
代码示例:
1 |
|
输出结果:
文件
文件是数据源(保存数据的地方)的一种,比如大家经常使用的 word 文档,txt 文件,excel 文件等都是文件。文件最主要的作用就是保存数据,它既可以保存一张图片,也可以保持视频,声音等。
文件在程序中是以流的形式来操作的。
流:数据在数据源(文件)和程序(内存)之间经历的路径
输 入流:数据从数据源(文件)到程序(内存)的路径
输 出流:数据从程序(内存)到数据源(文件)的路径
C 标准库stdio.h
该头文件定义了三个变量类型、一些宏和各种函数来执行输入和输出,在开发过程中,可以查询手册。
输入&输出
当我们提到输入
时,这意味着要向程序写入一些数据。输入可以是以文件的形式或从命令行中进行。C 语言提供了一系列内置的函数来读取给定的输入,并根据需要写入到程序中。
当我们提到输出
时,这意味着要在屏幕上、打印机上或任意文件中显示一些数据。C 语言提供了一系列内置的函数来输出数据到计算机屏幕上和保存数据到文本文件或二进制文件中。
标准文件
C 语言把所有的设备都当作文件。所以设备(比如显示器)被处理的方式与文件相同。以下三个文件会在程序执行时自动打开,以便访问键盘和屏幕。
标准文件 | 文件指针 | 设备 |
---|---|---|
标准输入 | stdin | 键盘 |
标准输出 | stdout | 屏幕 |
标准错误 | stderr | 您的屏幕 |
文件指针是访问文件的方式,我们会讲解如何从屏幕读取值以及如何把结果输出到屏幕上。
C 语言中的 I/O (输入/输出)通常使用printf()
和scanf()
两个函数。scanf()
函数用于从标准输入(键盘)读取并格式化,printf()
函数发送格式化输出到标准输出(屏幕)。
代码示例:
1 |
|
常用函数
getchar() & putchar() 函数
int getchar(void)
函数从屏幕读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字符。您可以在循环内使用这个方法,以便从屏幕上读取多个字符。
int putchar(int c)
函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符。您可以在循环内使用这个方法,以便在屏幕上输出多个字符。
代码示例:
1 |
|
输出结果:
gets() & puts() 函数
char *gets(char *s)
函数从stdin
读取一行到 s 所指向的缓冲区,直到一个终止符或EOF
。
int puts(const char *s)
函数把字符串 s 和一个尾随的换行符写入到stdout
。
代码示例:
1 |
|
输出结果:
scanf() 和 和 printf() 函数
int scanf(const char *format, ...)
函数从标准输入流stdin
读取输入,并根据提供的format
来浏览输入。
int printf(const char *format, ...)
函数把输出写入到标准输出流stdout
,并根据提供的格式产生输出。
format
可以是一个简单的常量字符串,但是您可以分别指定 %s、%d、%c、%f 等来输出或读取字符串、整数、字符或浮点数。还有许多其他可用的格式选项,可以根据需要使用。如需了解完整的细节,可以查看这些函数的参考手册。
代码示例:
1 |
|
输出结果:
文件读写
一个文件,无论它是文本文件还是二进制文件,都是代表了一系列的字节。C 语言不仅提供了访问顶层的函数,也提供了底层(OS)调用来处理存储设备上的文件。
打开文件
使用fopen()
函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型FILE
的一个对象,类型FILE
包含了所有用来控制流的必要的信息。
1 | FILE *fopen( const char * filename, const char * mode ); |
在这里,filename 是字符串,用来命名文件,访问模式 mode 的值可以是下列值中的一个:
模式 | 描述 |
---|---|
r | 打开一个已有的文本文件,允许读取文件。 |
w | 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。 |
a | 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。 |
r+ | 打开一个文本文件,允许读写文件。 |
w+ | 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。 |
a+ | 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。 |
如果处理的是二进制文件(图片、视频等),则需使用下面的访问模式来取代上面的访问模式:
1 | "rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b" |
b
是指 binary,代表二进制的意思。
综上,对于w
或w+
模式一定要慎重小心使用。
关闭文件
为了关闭文件,请使用fclose()
函数。函数的原型如下:
1 | int fclose( FILE *fp ); |
如果成功关闭文件,fclose() 函数返回零,如果关闭文件时发生错误,函数返回 EOF。这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。
使用完文件(读或写文件)之后,一定要将该文件关闭。
写入文件
把字符写入到流中的最简单的函数:
1 | int fputc( int c, FILE *fp ); |
函数 fputc() 把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOF。您可以使用下面的函数来把一个以 null 结尾的字符串写入到流中:
1 | int fputs( const char *s, FILE *fp ); |
说明:函数 fputs() 把字符串 s 写入到 fp 所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF。您也可以使用int fprintf(FILE \*fp, const char \*format, ...)
函数来写把一个字符串写入到文件中。
代码示例:
以下程序执行之前,D 盘符的根目录下没有一个叫:hello.txt 的文件,以下程序使用了w
模式打开并操作文件,那么在指定目录下没有该文件就会创建这个文件。如果该文件已存在,那么就会清空该文件原有的内容。因此谨慎测试本程序。
1 |
|
输出效果:
写入文件切记要关闭文件,否则文件中新写的内容不会被成功写入文件。
读取文件
下面是从文件读取单个字符的最简单的函数:
1 | int fgetc( FILE * fp ); |
fgetc() 函数从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回 EOF。
下面的函数允许您从流中读取一个字符串:
1 | char *fgets( char *buf, int n, FILE *fp ); |
说明:
函数 fgets() 从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。
如果这个函数在读取最后一个字符之前就遇到一个换行符 ‘\n’ 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。
也可以使用 int fscanf(FILE *fp, const char *format, …) 函数来从文件中读取字符串,但是在遇到第一个回车字符时,它会停止读取。
代码示例:
以下代码执行之前,在 D 盘符已经存在一个名称叫:hello.txt 的文件,里面存储了一些文本内容。
1 |
|
输出结果: