c语言知识点

文章目录

1. 什么是c语言

C语言是一门通用计算机编程语言,广泛应用于底层开发。C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。

尽管C语言提供了许多低级处理的功能,但仍然保持着良好跨平台的特性,以一个标准规格写出的C语言程序可在许多电脑平台上进行编译,甚至包含一些嵌入式处理器(单片机或称MCU)以及超级电脑等作业平台。

二十世纪八十年代,为了避免各开发厂商用的C语言语法产生差异,由美国国家标准局为C语言制定了一套完整的美国国家标准语法,称为[ANSI C](https://baike.baidu.com/item/ANSI C),作为C语言最初的标准。 [1] 目前2011年12月8日,国际标准化组织(ISO)和国际电工委员会(IEC)发布的C11****标准是C语言的第三个官方标准,也是C语言的最新标准,该标准更好的支持了汉字函数名和汉字标识符,一定程度上实现了汉字编程。

C语言是一门面向过程的计算机编程语言,与C++,Java等面向对象的编程语言有所不同。

其编译器主要有Clang、GCC、WIN-TC、SUBLIME、MSVC、Turbo C等。

2. 数据类型

基本的内置类型:

char	//字符数据类型
short	//短整型
int	//整形
long	//长整型
long long	//更长的整形
float	//单精度浮点数
double	//双精度浮点数

2.1 类型的基本归类:

整形家族:

char
	unsigned char 
    signed char
short
	unsigned short [int]
	signed short [int]
int
	unsigned int 
    signed int
long
	unsigned long [int]
	signed long [int]

浮点数家族:

float
double

构造类型:

>  数组类型
> 结构体类型 struct
>  枚举类型 enum
>  联合类型 union

指针类型:

int *pi; 
char *pc; 
float* pf; 
void* pv;

空类型:

void 表示空类型(无类型)

通常应用于函数的返回类型、函数的参数、指针类型。

2.2 整形在内存中的存储

2.2.1 源码反码补码

计算机中的整数有三种2进制表示方法,即原码、反码和补码。

三种表示方法均有符号位数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位正数的原、反、补码都相同。

负整数的三种表示方法各不相同。

原码

直接将数值按照正负数的形式翻译成二进制就可以得到原码。

反码

将原码的符号位不变,其他位依次按位取反就可以得到反码。

补码

反码+1就得到补码

对于整形来说:数据存放内存中其实存放的是补码。

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;

同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

2.2.2 大小端介绍

什么是大端小端:

大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;

小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。

为什么会有大端小端:

什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的short

型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。

例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

设计一个小程序来判断当前机器的字节序

//代码1
#include <stdio.h> int check_sys()
{
    int i = 1;
    return (*(char *)&i); }
int main() {
    int ret = check_sys(); if(ret == 1)
    {
        printf("小端\n");
    }
    else
    {
        printf("大端\n");
    }
    return 0; }

//代码2
int check_sys() {
    union
    {
        int i; char c;
    }un;
    un.i = 1; 
    return un.c;
}

2.2.3 浮点数的存储规则

常见浮点数:

3.14159 1E10

浮点数家族包括: float、double、long double 类型。

浮点数表示的范围:float.h中定义

根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:

(-1)^S * M * 2^E

(-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。M表示有效数字,大于等于1,小于2。

2^E表示指数位。

举例来说:

十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。那么,按照上面V的格式,可以得出S=0,M=1.01,E=2。

十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,S=1,M=1.01,E=2。

Single-precision 32 bit IEEE 754
1

Double-precision 64 bit IEEE 754
2

IEEE 754对有效数字M和指数E,还有一些特别规定。

前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分

IEEE 754规定,在计算机内部保存 M 时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,

将第一位的1舍去以后,等于可以保存24位有效数字。至于指数E,情况就比较复杂。

首先,E为一个无符号整数(unsigned int

这意味着,如果E为8位,它的取值范围为0-255;如果E为11位,它的取值范围为0-2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间

数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

然后,指数E从内存中取出还可以再分成三种情况:

E不全为0或不全为1

这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。

比如:

0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其阶码为-1+127=126,表示为

01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制表示形式为:

E全为0

这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,

有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。

E全为1

这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)。

3. 变量、常量

3.1 变量的命名

  • 只能由字母(包括大写和小写)、数字和下划线( _ )组成。

  • 不能以数字开头。

  • 长度不能超过63个字符。

  • 变量名中区分大小写的。

  • 变量名不能使用关键字。

3.2 变量的分类

  • 局部变量
  • 全局变量
#include <stdio.h>

int global = 2022;//全局变量
int main()
{
    int local = 2021;//局部变量
    int global = 2023;//局部变量
    printf("global = %d\n", global); return 0;
}

上面的局部变量global变量的定义其实没有什么问题的

当局部变量和全局变量同名的时候,局部变量优先使用。

3.3 变量的作用域和生命周期

作用域

作用域(scope)是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的而限定这个名字的可用性的代码范围就是这个名字的作用域。

  1. 局部变量的作用域是变量所在的局部范围。

  2. 全局变量的作用域是整个工程。

生命周期

变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段

  1. 局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。

  2. 全局变量的生命周期是:整个程序的生命周期。

3.4 常量

C语言中的常量分为以下以下几种:

  • 字面常量

  • const 修饰的常变量

  • #define定义的标识符常量

  • 枚举常量

#include <stdio.h>
//举例
enum  Sex 
{
	MALE, 
    FEMALE, 
    SECRET
};
//括号中的MALE,FEMALE,SECRET是枚举常量

int main() {
	//字面常量演示
    3.14;
    //字面常量
    1000;
    //字面常量

	//const 修饰的常变量
	const float pai = 3.14f;	//这里的pai是const修饰的常变量
    pai = 5.14;//是不能直接修改的!

	//#define的标识符常量 演示
    #define MAX 100
	printf("max = %d\n", MAX);
	
    return 0;
};

注:

上面例子上的 pai 被称为 const 修饰的常变量, const 修饰的常变量在C语言中只是在语法层面限制了变量 pai 不能直接被改变,但是 pai 本质上还是一个变量的,所以叫常变量。

4. 字符串+转义字符+注释

"attack on titan"

这种由双引号(Double Quote)引起来的一串字符称为字符串字面值(String Literal),或者简称字符串。

注:字符串的结束标志是一个 \0 的转义字符。在计算字符串长度的时候 \0 是结束标志,不算作字符串内容。

转义字符释义
?在书写连续多个问号时使用,防止他们被解析成三字母词
用于表示字符常量’
\“用于表示一个字符串内部的双引号
\用于表示一个反斜杠,防止它被解释为一个转义序列符。
\a警告字符,蜂鸣
\b退格符
\f进纸符
\n换行
\r回车
\t水平制表符
\v垂直制表符
\dddddd表示1~3个八进制的数字。 如: \130 X
\xdddd表示2个十六进制数字。 如: \x30 0

5. 注释

  1. 代码中有不需要的代码可以直接删除,也可以注释掉
  2. 代码中有些代码比较难懂,可以加一下注释文字

注释有两种风格:

  • C语言风格的注释 /*xxxxxx*/

    • 缺陷:不能嵌套注释
  • C++风格的注释 //xxxxxxxx

    • 可以注释一行也可以注释多行

6. 选择语句

6.1 if

语法结构:
if(表达式)语句;
if(表达式)语句1;
else
语句2;

//多分支if(表达式1)
语句1;
else if(表达式2)语句2;
else
语句3;

6.2 switch

switch语句也是一种分支语句。

常常用于多分支的情况。

switch(整型表达式) 
{
	case  整形常量表达式:
        语句;
}
6.2.1 switch中的break

break语句 的实际效果是把语句列表划分为不同的分支部分。

在最后一个 case 语句的后面加上一条 break语句。

(之所以这么写是可以避免出现在以前的最后一个 case 语句后面忘了添加 break语句)。

6.2.2 default

default:

写在任何一个 case 标签可以出现的位置。

switch 表达式的值并不匹配所有 case 标签的值时,这个 default 子句后面的语句就会执行。所以,每个switch语句中只能出现一条default子句。

在每个 switch 语句中都放一条default子句是个好习惯,甚至可以在后边再加一个 break 。

7. 循环语句

7.1 while

while(表达式)
    循环语句;
7.1.1 while语句中的break接continue

其实在循环中只要遇到break,就停止后期的所有的循环,直接终止循环。

所以:while中的break是用于永久终止循环的。

continue是用于终止本次循环的,也就是本次循环中continue后边的代码不会再执行,

而是直接跳转到while语句的判断部分。进行下一次循环的入口判断。

#include <stdio.h> int main()
{
	int ch = 0;
	while ((ch = getchar()) != EOF)
		putchar(ch); 
    return 0;
}
//清理缓冲区

7.2 for

for(表达式1;  表达式2;  表达式3)
    循环语句;

表达式1为初始化部分,用于初始化循环变量的。

表达式2为条件判断部分,用于判断循环时候终止。

表达式3为调整部分,用于循环条件的调整。

在for循环中也可以出现breakcontinue,他们的意义和在while循环中是一样的。

  1. 不可在for 循环体内修改循环变量,防止 for 循环失去控制。

  2. 建议for语句的循环控制变量的取值采用“前闭后开区间”写法。

for(;;) 
{
	printf("战斗吧\n"); 
}

7.3 do while

do
	循环语句; 
while(表达式);

7.4 goto语句

C语言中提供了可以随意滥用的 goto语句和标记跳转的标号。

从理论上 goto语句是没有必要的,实践中没有goto语句也可以很容易的写出代码。

但是某些场合下goto语句还是用得着的,最常见的用法就是终止程序在某些深度嵌套的结构的处理过程。

例如:一次跳出两层或多层循环。

多层循环这种情况使用break是达不到目的的。它只能从最内层循环退出到上一层的循环。

8. 函数

8.1 函数是什么?

维基百科中对函数的定义:子程序

  • 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。

  • 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。

8.2 库函数

为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。

库函数查询网址:

www.cplusplus.com

http://en.cppreference.com

简单的总结,C语言常用的库函数都有:

  • IO函数

  • 字符串操作函数

  • 字符操作函数

  • 内存操作函数

  • 时间/日期函数

  • 数学函数

  • 其他库函数

使用库函数,必须包含 #include 对应的头文件。

8.3 自定义函数

ret_type fun_name(para1, * ) 
{
	statement;//语句项
}

ret_type  返回类型
fun_name  函数名
para1	  函数参数

8.4 函数的参数

8.4.1 实际参数(实参):

真实传给函数的参数,叫实参。

实参可以是:常量、变量、表达式、函数等。

无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

8.4.2 形式参数(形参):

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

8.5 函数的调用

8.5.1 传值调用

函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。

8.5.2 传址调用

传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。

这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。

8.6 函数的嵌套调用和链式访问

函数可以嵌套调用,但是不能嵌套定义

链式访问

把一个函数的返回值作为另外一个函数的参数。

8.7 函数的声明和定义

8.7.1 函数声明:
  1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。

  2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用

  3. 函数的声明一般要放在头文件中的。

8.7.2 函数定义:

函数的定义是指函数的具体实现,交待函数的功能实现。

test.h的内容

放置函数的声明

test.c的内容

放置函数的实现

8.8 函数递归

####8.8.1 什么是递归

程序调用自身的编程技巧称为递归( recursion)。

递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的

一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略

只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的主要思考方式在于:把大事化小

8.8.2 递归的两个必要条件
  • 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
  • 每次递归调用之后越来越接近这个限制条件。
8.8.3 递归与迭代
  1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。

  2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。

  3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。

8.8.4 函数递归的经典题目
  1. 汉诺塔问题 https://leetcode.cn/problems/hanota-lcci/

  2. 青蛙跳台阶问题 https://leetcode.cn/problems/qing-wa-tiao-tai-jie-wen-ti-lcof/

9. 数组

9.1 数组的创建和初始化

数组是一组相同类型元素的集合。

数组的创建方式:

type_t	arr_name	[const_n];
//type_t 是指数组的元素类型
//const_n  是一个常量表达式,用来指定数组的大小

注:数组创建,在C99标准之前, [] 中要给一个常量才可以,不能使用变量。在C99标准支持了变长数组的概念,数组的大小可以使用变量指定,但是数组不能初始化。

9.2 数组的初始化

数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值(初始化)。

数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定。

int arr1[10] = {1,2,3}; 
int arr2[] = {1,2,3,4};
int arr3[5] = {12345}char arr4[3] = {'a',98, 'c'}; 
char arr5[] = {'a','b','c'}; 
char arr6[] = "abcdef";

9.3 一维数组的使用

  1. 数组是使用下标来访问的,下标是从0开始。

  2. 数组的大小可以通过计算得到。

int arr[10];
int sz = sizeof(arr)/sizeof(arr[0]);

###9.4 一位数组在内存中的存储

#include <stdio.h>
int main() 
{
    int arr[10] = {0}; int i = 0;
    int sz = sizeof(arr)/sizeof(arr[0]);

for(i=0; i<sz; ++i) 
{
	printf("&arr[%d] = %p\n", i, &arr[i]); 
}
	return 0; 
}

仔细观察输出的结果,我们知道,随着数组下标的增长,元素的地址,也在有规律的递增。由此可以得出结论:数组在内存中是连续存放的。

9.5 二维数组

9.5.1 二维数组的创建
int arr[3][4]; 
char arr[3][5]; 
double arr[2][4];
9.5.2 二维数组的初始化
//数组初始化
int arr[3][4] = {1,2,3,4};
int arr[3][4] = {{1,2},{4,5}};
int arr[][4] = {{2,3},{4,5}};//二维数组如果有初始化,行可以省略,列不能省略
9.5.3 二维数组在内存中的存储

过结果我们可以分析到,其实二维数组在内存中也是连续存储的。

9.6 数组越界

数组的下标是有范围限制的。

数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。

所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。

C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就是正确的,

所以程序员写代码时,最好自己做越界的检查。

9.7 数组作为函数参数

当数组传参的时候,实际上只是把数组的首元素的地址传递过去了。

所以即使在函数参数部分写成数组的形式: int arr[] 表示的依然是一个指针: int *arr 。那么,函数内部的 sizeof(arr) 结果是4。

9.8 数组名

数组名是数组首元素的地址。(有两个例外)

  1. sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
  2. &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。

10. 操作符

10.1 算术操作符:

+    -    *    /    %
  1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。

  2. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。

  3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。

10.2 移位操作符

>>   <<

注:移位操作符的操作数只能是整数。

10.2.1 左移操作符

<<
移位规则:

左边抛弃、右边补0

10.2.2 右移操作符

>>
首先右移运算分两种:

  1. 逻辑移位
    左边用0填充,右边丢弃
  2. 算术移位
    左边用原该值的符号位填充,右边丢弃

10.3 位操作符

&	//按位与 对应的二进制位有0,则为0,两个同时为1,才为1
|	//按位或 对应的二进制位有1则为1,两个同时为0则为0
^	//按位异或 对应的二进制位:相同为0,相异为1
注:他们的操作数必须是整数。

练习1 不能创建临时变量(第三个变量),实现两个数的交换。

#include <stdio.h> int main()
{
	int a = 10; 
    int b = 20;
	a = a^b;
	b = a^b; 
    a = a^b;
	printf("a = %d   b = %d\n", a, b); 
    return 0;
}

练习2 编写代码实现:求一个整数存储在内存中的二进制中1的个数。

参考代码:
//方法1
#include <stdio.h> 
int main()
{
    int num   = 10;
    int count = 0;//计数while(num)
{
	if(num%2  ==  1)
    count++;
    num  =  num/2; 
}
	printf("二进制中1的个数 = %d\n", count); 		return 0;
}
//方法2:
#include <stdio.h> int main()
{
    int num = -1; 
    int i = 0;
    int count = 0;
	//计数
    for(i=0; i<32; i++) 
    {
		if( num & (1 << i) ) 
            count++;
	}
	 printf("二进制中1的个数 = %d\n",count); 		 return 0;
}
//获取一个二进制的第i位 (n >> i) & 1

//方法3:
#include <stdio.h> 
int main()
{
    int num = -1; 
    int i = 0;
    int count = 0;
    //计数
    while(num)
	{
        count++;
        num  =  num&(num-1); 
	}
	printf("二进制中1的个数 = %d\n",count); 
    return 0;
}

10.4 赋值操作符

=    +=  -=  *=  /=  &=    ^=    |=     >>=     <<=

10.5 单目操作符

!		逻辑反操作
-		负值
+		正值
&		取地址
sizeof	操作数的类型长度(以字节为单位)
~		对一个数的二进制按位取反
--		前置、后置-- //先使用,再自减
++		前置、后置++ //先自增,再使用
*		间接访问操作符(解引用操作符)
(类型)   强制类型转换

10.6 关系操作符

> 
>=
< 
<=		用于测试不相等
!=		用于测试相等

10.7 逻辑操作符

&&		逻辑与
||  	逻辑或

10.8 条件操作符

exp1 ? exp2 : exp3

10.9 逗号表达式

exp1, exp2, exp3, .expN 

逗号表达式,就是用逗号隔开的多个表达式。

逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。

10.10 下标引用、函数调用和结构成员

[] () .  ->

10.11 表达式求值

表达式求值的顺序一部分是由操作符的优先级和结合性决定。

同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。

10.11.1 隐式类型转换

C的整型算术运算总是至少以缺省整型类型的精度来进行的。

为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升

整形提升的意义:

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。

因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。

通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

//实例1
char a,b,c;
a=b+c;

b和c的值被提升为普通整型,然后再执行加法运算。

加法运算完成之后,结果将被截断,然后再存储于a中。

如何进行整体提升呢?

整形提升是按照变量的数据类型的符号位来提升的

//负数的整形提升char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char  为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:11111111111111111111111111111111

//正数的整形提升char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char  为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:00000000000000000000000000000001

//无符号整形提升,高位补0

整形提升的例子

//实例1
int main() 
{
    char a = 0xb6; 
    short b = 0xb600;
    int c = 0xb6000000; 
    if(a==0xb6)
    	printf("a"); 
    if(b==0xb600)
    	printf("b"); 
    if(c==0xb6000000)
    	printf("c"); 
    return 0;
}

实例1中的a,b要进行整形提升,但是c不需要整形提升

a,b整形提升之后,变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果是假,但是c不发生整形提升,则表达式 c==0xb6000000 的结果是真.

所程序输出的结果是: c

//实例2
{
    char c = 1;
    printf("%u\n", sizeof(c)); 
    printf("%u\n", sizeof(+c)); 
    printf("%u\n", sizeof(-c)); 
    return 0;
}

实例2中的,c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字节.

表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof© ,就是1个字节.

10.11.2 算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换

long double 
double
float
unsigned long int 
long int
unsigned int 
int

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。

警告:

但是算术转换要合理,要不然会有一些潜在的问题。

float f = 3.14;
int  num  =  f;//隐式转换,会有精度丢失
10.11.3 操作符的属性

复杂表达式的求值有三个影响的因素。

  1. 操作符的优先级

  2. 操作符的结合性

  3. 是否控制求值顺序。

两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。

操作符优先级

操作 描述用****法示例结果类 结合 是否控****制求值 顺序
()聚组(表达式)与表达 式同N/A
()函数调用rexp(rexp,…,rexp)rexpL-R
[ ]下标引用rexp[rexp]lexpL-R
.访问结构成员lexp.member_namelexpL-R
->访问结构指针成员rexp->member_namelexpL-R
++后缀自增lexp ++rexpL-R
后缀自减lexp –rexpL-R
!逻辑反! rexprexpR-L
~按位取反~ rexprexpR-L
+单目,表示正值+ rexprexpR-L
-单目,表示负值- rexprexpR-L
++前缀自增++ lexprexpR-L
前缀自减– lexprexpR-L
*间接访问* rexplexpR-L
&取地址& lexprexpR-L
sizeof取其长度,以字节表示sizeof rexp sizeof(类型)rexpR-L
(类型)类型转换(类型) rexprexpR-L
*乘法rexp * rexprexpL-R
/除法rexp / rexprexpL-R
%整数取余rexp % rexprexpL-R
+加法rexp + rexprexpL-R
-减法rexp - rexprexpL-R
<<左移位rexp << rexprexpL-R
>>右移位rexp >> rexprexpL-R
>大于rexp > rexprexpL-R
>=大于等于rexp >= rexprexpL-R
<小于rexp < rexprexpL-R
<=小于等于rexp <= rexprexpL-R
操作 描述用****法示例结果类 结合 是否控****制求值 顺序
==等于rexp == rexprexpL-R
!=不等于rexp != rexprexpL-R
&位与rexp & rexprexpL-R
^位异或rexp ^ rexprexpL-R
|位或rexp | rexprexpL-R
&&逻辑与rexp && rexprexpL-R
||逻辑或rexp || rexprexpL-R
? :条件操作符rexp ? rexp : rexprexpN/A
=赋值lexp = rexprexpR-L
+=以…加lexp += rexprexpR-L
-=以…减lexp -= rexprexpR-L
*=以…乘lexp *= rexprexpR-L
/=以…除lexp /= rexprexpR-L
%=以…取模lexp %= rexprexpR-L
<<=以…左移lexp <<= rexprexpR-L
>>=以…右移lexp >>= rexprexpR-L
&=以…与lexp &= rexprexpR-L
^=以…异或lexp ^= rexprexpR-L
|=以…或lexp |= rexprexpR-L
逗号rexp,rexprexpL-R

11. 常见关键字

auto   break	case   char   const	continue   default   do	double else   enum extern float   for	goto   if	int	long   register	return	short   signed sizeof	static struct   switch   typedef union   unsigned	void   volatile   while

C语言提供了丰富的关键字,这些关键字都是语言本身预先设定好的,用户自己是不能创造关键字的。

11.1 typedef

typedef 顾名思义是类型定义,这里应该理解为类型重命名。

//将unsigned int 重命名为uint_32, 所以uint_32也是一个类型名
typedef unsigned int uint_32;

int main() 
{
    //观察num1和num2,这两个变量的类型是一样的unsigned int num1 = 0;
    uint_32 num2 = 0; return 0;
}

11.2 关键字static

在C语言中:

static是用来修饰变量和函数的

  1. 修饰局部变量-称为静态局部变量

  2. 修饰全局变量-称为静态全局变量

  3. 修饰函数-称为静态函数

11.2.1 修饰局部变量
//代码1
#include <stdio.h> 
void test()
{
	int i = 0; 
    i++;
	printf("%d ", i); 
}

int main() 
{
    int i = 0;
    for(i=0; i<10; i++) 
	{
		test(); 
	}
	return 0; 
}

//代码2
#include <stdio.h> 
void test()
{
    //static修饰局部变量
    static int i = 0; 
    i++;
    printf("%d ", i); 
}

int main() 
{
    int i = 0;
    for(i=0; i<10; i++) 
    {
    	test(); 
    }
    return 0;
}

对比代码1和代码2的效果理解static修饰局部变量的意义。

结论:

static修饰局部变量改变了变量的生命周期

让静态局部变量出了作用域依然存在,到程序结束,生命周期才结束。

11.2.2 修饰全局变量

代码1正常,代码2在编译的时候会出现连接性错误。

结论:

一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。

11.2.3 修饰函数
//代码1
//add.c
int Add(int x, int y) 
{
	return c+y; 
}
//test.c 
int main() 
{
	printf("%d\n", Add(2, 3)); 
    return 0;
}

//代码2
//add.c
static int Add(int x, int y) 
{
	return c+y; 
}
//test.c 
int main() 
{
	printf("%d\n", Add(2, 3)); 
    return 0;
}

一个函数被static修饰,使得这个函数只能在本源文件内使用,不能在其他源文件内使用。

11.3 const

11.3.1 const修饰常量
int const a = 100;
const int a = 100; //与上面等价
int const arr [3] = {1,2,3};
const int arr [3] = {1,2,3};//与上面等价
11.3.2 const修饰指针

指针常量

int a = 5;
const int *p =&a;
*p = 20;   //error  不可以通过修改所指向的变量的值

int b =20;
p = &b; //right  指针可以指向别的变量

常量指针

int a = 5;
int *const p = &a;
*p = 20;     //right 可以修改所指向变量的值

int b = 10;
p = &b;      //error 不可以指向别的变量

11.4 assert

assert()可以检测非空指针

assert(p1&&p2);
//p1,p2有效,就可以通关assert

12. #define 定义的常量和宏

//define定义标识符常量
#define MAX 1000

//define定义宏
#define ADD(x, y) ((x)+(y))

#include <stdio.h>

int main() 
{
	int sum = ADD(2, 3); 
    printf("sum = %d\n", sum);

	sum = 10*ADD(2, 3); 
    printf("sum = %d\n", sum);

	return 0; 
}

13. 指针

13.1 指针是什么?

  1. 指针是内存中一个最小单元的编号,也就是地址
  2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量

内存

内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。

所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节

为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址

内存
一个字节0xFFFFFFFF
一个字节0xFFFFFFFE
一个字节0x0000002
一个字节0x0000001
一个字节0x0000000

指针变量

我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量

#include <stdio.h> 
int main()
{
    int a = 10;//在内存中开辟一块空间
    int  *p  =  &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
					//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个之指针变量。
	return 0; 
}

指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。

对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);

那么32根地址线产生的地址就会是:

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

11111111 11111111 11111111 11111111

这里就有2的32次方个地址。

每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB == 2^32 /1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空间进行编址。

64位同理

  • 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
  • 那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

13.2 指针和指针的类型

char *pc = NULL; 
int	*pi = NULL; 
short *ps = NULL; 
long *pl = NULL; 
float *pf = NULL; 
double *pd = NULL;

这里可以看到,指针的定义方式是: type + * 。

13.2.1 指针±整数

指针的类型决定了指针向前或者向后走一步有多大(距离)。

13.2.2 指针的解引用
//演示实例
#include <stdio.h>
int main() 
{
    int n = 0x11223344; 
    char *pc = (char *)&n; 
    int *pi = &n;
    *pc  =  0;	//重点在调试的过程中观察内存的变化。
    *pi =  0;	//重点在调试的过程中观察内存的变化。
    return 0;
}

指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。

比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

13.3 野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

13.3.1 野指针的成因

指针未初始化

#include <stdio.h> int main()
{
    int *p;//局部变量指针未初始化,默认为随机值
    *p = 20;
    return 0; 
}

指针越界访问

#include <stdio.h> 
int main()
{
	int arr[10] = {0}; 
    int *p = arr;
	int i = 0;
	for(i=0; i<=11; i++) 
	{
		//当指针指向的范围超出数组arr的范围时,p就是野指针
    	*(p++) = i;
	}
	return 0; 
}

指针指向的空间释放

ptr = (int*)malloc(num*sizeof(int));
free(ptr);
13.3.2 如何规避野指针
  1. 指针初始化

  2. 小心指针越界

  3. 指针指向空间释放,及时置NULL

  4. 避免返回局部变量的地址

  5. 指针使用之前检查有效性

#include <stdio.h> 
int main()
{
	int *p = NULL;
    //....
    int a = 10; 
    p = &a;
    if(p != NULL) 
    {
    	*p = 20;
	}
	return 0; 
}

13.4 指针运算

13.4.1 指针±整数
#define  N_VALUES  5
float values[N_VALUES]; 
float *vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];) 
{
	*vp++ = 0; 
}
13.4.2 指针-指针
int my_strlen(char *s) 
{
	char *p = s; 
    while(*p != '\0' )
	p++;
	return p-s; 
}
13.4.3 指针的关系运算
for(vp = &values[N_VALUES]; vp > &values[0];) 
{
	*--vp = 0; 
}

标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

13.5 指针和数组

数组名表示的是数组首元素的地址

int main() 
{
	int arr[] = {1,2,3,4,5,6,7,8,9,0}; 
    int *p = arr; //指针存放数组首元素的地址
    int sz = sizeof(arr)/sizeof(arr[0]); 
    for(i=0; i<sz; i++)
	{
		printf("&arr[%d] = %p	<====> p+%d = %p\n", i, &arr[i], i, p+i); 
    }
	return 0; 
}

13.6 二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?这就是 二级指针

13.7 字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ;

使用:

int main() 
{
    char ch = 'w'; 
    char *pc = &ch; 
    *pc = 'w'; 
    const char* pstr = "attack";
    return 0;
}

但是本质是把字符串 “attack”. 首字符的地址放到了pstr中。

#include <stdio.h>
int main() 
{
    char str1[] = "hello bit."; 
    char str2[] = "hello bit.";
    const char *str3 = "hello bit."; const char *str4 = "hello bit.";

    if(str1 ==str2)
        printf("str1 and str2 are same\n"); else
        printf("str1 and str2 are not same\n");

    if(str3 ==str4)
        printf("str3 and str4 are same\n"); else
        printf("str3 and str4 are not same\n");

    return 0; 
}

这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。

13.8 指针数组

int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组

13.9 数组指针

13.9.1 数组指针的定义
int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
13.9.2 数组名VS&数组名

&arr 表示的是数组的地址,而不是数组首元素的地址。

&arr 的类型是: int(*)[] ,是一种数组指针类型

数组的地址+1,跳过整个数组的大小

13.10 函数指针

void (*pfun1)();

13.11 函数指针数组

int (*parr1[10])();
//parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?是 int (*)() 类型的函数指针。

函数指针数组的用途:转移表

#include <stdio.h>
int add(int a, int b) {
    return a + b; }
int sub(int a, int b) {
    return a - b; }
int mul(int a, int b) {
    return a*b; }
int div(int a, int b) {
    return a / b; }
int main() {
    int x, y;
    int input = 1; int ret = 0;
    int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表while (input)
    {
        printf( "*************************\n" ); printf( "   1:add	2:sub   \n" );
        printf( "  3:mul	4:div  \n" ); printf( "*************************\n" ); printf( "请选择:" );
        scanf( "%d", &input);
        if ((input <= 4 && input >= 1)) {
            printf( "输入操作数:" ); scanf( "%d %d", &x, &y); ret = (*p[input])(x, y);
        }
        else
            printf( "输入有误\n" );
        printf( "ret = %d\n", ret);
    }
    return 0;
}

13.12 指向函数指针数组的指针

void test(const char* str) {
    printf("%s\n", str); }
int main() {
    //函数指针pfun
    void (*pfun)(const char*) = test;
    //函数指针的数组pfunArr
    void (*pfunArr[5])(const char* str); pfunArr[0] = test;
    //指向函数指针数组pfunArr的指针ppfunArr
    void (*(*ppfunArr)[5])(const char*) = &pfunArr; return 0;
}

13.13 回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

首先演示一下qsort函数的使用:

#include <stdio.h>

//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2) {
    return (*( int *)p1 - *(int *) p2); }

int main() {
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 }; int i = 0;

    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp); for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
    {
        printf( "%d ", arr[i]); }
    printf("\n"); return 0;
}

使用回调函数,模拟实现qsort(采用冒泡的方式)。

#include <stdio.h>

int int_cmp(const void * p1, const void * p2) {
    return (*( int *)p1 - *(int *) p2); }

void _swap(void *p1, void * p2, int size) {
    int i = 0;
    for (i = 0; i< size; i++) {
        char tmp = *((char *)p1 + i);
        *(( char *)p1 + i) = *((char *) p2 + i); *(( char *)p2 + i) = tmp;
    }
}

void bubble(void *base, int count , int size, int(*cmp )(void *, void *)) {
    int i = 0; int j = 0;
    for (i = 0; i< count - 1; i++) {
        for (j = 0; j<count-i-1; j++) {
            if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0)
            {
                _swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
            }
        }
    }
}
int main() {
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    //char *arr[] = {"aaaa","dddd","cccc","bbbb"}; int i = 0;
    bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp); for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
    {
        printf( "%d ", arr[i]); }
    printf("\n"); return 0;
}

14. 自定义类型:结构体,枚举,联合

14.1 结构体

####14.1.1 结构体的基础知识

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

14.1.2 结构体的声明
struct tag 
{
    member-list; 
}variable-list;
14.1.3 特殊的声明

在声明结构的时候,可以不完全的声明。

//匿名结构体类型
struct
{
    int a; 
    char b; 
    float c;
}x; 
struct 
{
    int a; 
    char b; 
    float c;
}a[20], *p;

上面的两个结构在声明的时候省略掉了结构体标签(tag)。

//在上面代码的基础上,下面的代码合法吗?
p = &x;

警告:

编译器会把上面的两个声明当成完全不同的两个类型。

所以是非法的。

14.1.4 结构体的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

struct Node 
{
    int data;
    struct Node* next; 
};

typedef struct Node 
{
    int data;
    struct Node* next; 
}Node;
14.1.5 结构体变量的定义和初始化
struct Point 
{
    int x; 
    int y;
}p1;	//声明类型的同时定义变量p1
struct Point p2;	//定义结构体变量p2

//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};

struct Stu	//类型声明
{
    char name[15];//名字
	int age;	//年龄
};
struct Stu s = {"zhangsan", 20};//初始化

struct Node {
    int data;
    struct Point p; 
    struct Node* next;
}n1 = {10, {4,5}, NULL};	//结构体嵌套初始化

struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
14.1.6 结构体内存对齐

我们已经掌握了结构体的基本使用了。

现在我们深入讨论一个问题:计算结构体的大小。这也是一个特别热门的考点: 结构体内存对齐

//练习1 
struct S1
{
    char c1; 
    int i; 
    char c2;
};
printf("%d\n", sizeof(struct S1));

//练习2 
struct S2 
{
	char c1; 
    char c2; 
    int i;
};
printf("%d\n", sizeof(struct S2));

//练习3 
struct S3
{
	double d; 
    char c; 
    int i;
};
printf("%d\n", sizeof(struct S3));

//练习4-结构体嵌套问题struct S4
{
    char c1; 
    struct S3 s3; 
    double d;
};
printf("%d\n", sizeof(struct S4));

考点

如何计算

首先得掌握结构体的对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值

    • VS中默认的值为8
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

为什么存在内存对齐?

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

  2. **性能原因:**数据结构(尤其是栈)应该尽可能地在自然边界上对齐。

    原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

总体来说:

结构体的内存对齐是拿空间来换取时间的做法。

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:

让占用空间小的成员尽量集中在一起。

//例如:
struct S1 {
    char c1; 
    int i; 
    char c2;
};
struct S2 {
    char c1; 
    char c2; 
    int i;
};

S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。

14.1.7 修改默认对齐数

之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8 
struct S1
{
    char c1; 
    int i; 
    char c2;
};
#pragma  pack()//取消设置的默认对齐数,还原为默认

#pragma pack(1)//设置默认对齐数为1 
struct S2
{
    char c1; 
    int i; 
    char c2;
};
#pragma  pack()//取消设置的默认对齐数,还原为默认
int main()
{
    //输出的结果是什么?
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));
    return 0;
}

结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

14.1.8 结构体传参
struct S 
{
    int data[1000]; 
    int num;
};

struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s) 
{
    printf("%d\n", s.num); 
}
//结构体地址传参
void print2(struct S* ps) 
{
    printf("%d\n", ps->num); 
}

int main() {
    print1(s);   //传结构体
    print2(&s);  //传地址
    return 0;
}

首选print2函数。

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

14.2 位段

14.2.1 什么是位段

位段的声明和结构是类似的,有两个不同:

1.位段的成员必须是 int、unsigned int 或signed int 。

2.位段的成员名后边有一个冒号和一个数字。

struct A {
    int _a:2; 
    int _b:5; 
    int _c:10; 
    int _d:30;
};
//A就是一个位段类型。
//那位段A的大小是多少?
printf("%d\n", sizeof(struct A));
14.2.2 位段的内存分配
  1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型

  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

14.2.3 位段的跨平台问题
  1. int 位段被当成有符号数还是无符号数是不确定的。

  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。

  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

14.2.4 位段的应用

3

14.3 枚举

14.3.1 枚举类型的定义
enum Day//星期
{
	Mon, 
    Tues, 
    Wed, 
    Thur, 
    Fri, 
    Sat, 
    Sun
};
enum Sex//性别
{
	MALE, 
    FEMALE, 
    SECRET
}enum Color//颜色
{
	RED, 
    GREEN, 
    BLUE
};

以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。{}中的内容是枚举类型的可能取值,也叫 枚举常量 。

这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。

例如:

enum Color//颜色
{
    RED=1, 
    GREEN=2, 
    BLUE=4
};
14.3.2 枚举的优点

为什么使用枚举?

我们可以使用 #define 定义常量,为什么非要使用枚举?枚举的优点:

  1. 增加代码的可读性和可维护性

  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。

  3. 防止了命名污染(封装)

  4. 便于调试

  5. 使用方便,一次可以定义多个常量

14.3.3 枚举的使用
enum Color//颜色
{
	RED=1, 
    GREEN=2, 
    BLUE=4
};

enum  Color  clr  =  GREEN;
//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。

14.4 联合(共用体)

14.4.1 联合类型的定义‘

联合也是一种特殊的自定义类型

这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。

比如:

//联合类型的声明
union Un
{
	char c; 
    int i;
};

//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));
14.4.2 联合的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

union Un 
{
	int i; 
    char c;
};
union Un un;

//  下面输出的结果是一样的吗?
printf("%d\n", &(un.i)); 
printf("%d\n", &(un.c));

//下面输出的结果是什么?
un.i = 0x11223344; 
un.c = 0x55; 
printf("%x\n", un.i);
14.4.3 联合大小的计算
  • 联合的大小至少是最大成员的大小。
  • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un1 
{
    char c[5]; 
    int i;
};
union Un2 
{
    short c[7]; 
    int i;
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1)); 
printf("%d\n", sizeof(union Un2));

15. 字符函数和字符串函数

C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在常量字符串中或者字符数组中。

字符串常量适用于那些对它不做修改的字符串函数.

15.1 strlen

size_t strlen ( const char * str );
  • 字符串已经 ‘\0’ 作为结束标志,strlen函数返回的是在字符串中 ‘\0’ 前面出现的字符个数(不包含 ‘\0’ )
  • 参数指向的字符串必须要以 ‘\0’ 结束
  • 注意函数的返回值为size_t,是无符号的

15.2 strcpy

char* strcpy(char * destination, const char * source );
  • Copies the C string pointed by source into the array pointed by destination, including the terminating null character (and stopping at that point).

  • 源字符串必须以 ‘\0’ 结束。

  • 会将源字符串中的 ‘\0’ 拷贝到目标空间。

  • 目标空间必须足够大,以确保能存放源字符串。

  • 目标空间必须可变。

15.3 stract

char * strcat ( char * destination, const char * source );
  • Appends a copy of the source string to the destination string. The terminating null character in destination is overwritten by the first character of source, and a null-character is included at the end of the new string formed by the concatenation of both in destination.
  • 源字符串必须以 ‘\0’ 结束。
  • 目标空间必须有足够的大,能容纳下源字符串的内容。
  • 目标空间必须可修改。

15.4 strcmp

int strcmp ( const char * str1, const char * str2 );
  • This function starts comparing the first character of each string. If they are equal to each other, it continues with the following pairs until the characters differ or until a terminating null-character is reached.
  • 标准规定:
    • 第一个字符串大于第二个字符串,则返回大于0的数字
    • 第一个字符串等于第二个字符串,则返回0
    • 第一个字符串小于第二个字符串,则返回小于0的数字

15.5 strncpy

char * strncpy ( char * destination, const char * source, size_t num );
  • Copies the first num characters of source to destination. If the end of the source C string (which is signaled by a null-character) is found before num characters have been copied, destination is padded with zeros until a total of num characters have been written to it.
  • 拷贝num个字符从源字符串到目标空间。
  • 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。

15.6 strncat

char * strncat ( char * destination, const char * source, size_t num );
  • Appends the first num characters of source to destination, plus a terminating null-character.
  • If the length of the C string in source is less than num, only the content up to the terminating null-character is copied.

15.7 strncmp

int strncmp ( const char * str1, const char * str2, size_t num );

比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完。

15.8 strstr

char * strstr ( const char *str1, const char * str2);

Returns a pointer to the first occurrence of str2 in str1, or a null pointer if str2 is not part of str1.

15.9 strtok

char * strtok ( char * str, const char * sep );

sep参数是个字符串,定义了用作分隔符的字符集合

  • 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。
  • strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
  • strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
  • strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
  • 如果字符串中不存在更多的标记,则返回 NULL 指针。

15.10 strerror

char * strerror ( int errnum );

返回错误码,所对应的错误信息。

15.11 字符函数

字符分类函数:

函数如果他的参****数符合下列条件就返回真
iscntrl任何控制字符
isspace空白字符:空格‘ ’,换页‘\f’,换行’\n’,回车‘\r’,制表符’\t’或者垂直制表符’\v’
isdigit十进制数字 0~9
isxdigit十六进制数字,包括所有十进制数字,小写字母af,大写字母AF
islower小写字母a~z
isupper大写字母A~Z
isalpha字母az或AZ
isalnum字母或者数字,az,AZ,0~9
ispunct标点符号,任何不属于数字或者字母的图形字符(可打印)
isgraph任何图形字符
isprint任何可打印字符,包括图形字符和空白字符

字符转换:

int tolower ( int c ); 
int toupper ( int c );

15.12 memcpy

void * memcpy ( void * destination, const void * source, size_t num );
  • 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
  • 这个函数在遇到 ‘\0’ 的时候并不会停下来。
  • 如果source和destination有任何的重叠,复制的结果都是未定义的。

15.13 memmove

void * memmove ( void * destination, const void * source, size_t num );
  • 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
  • 如果源空间和目标空间出现重叠,就得使用memmove函数处理。

15.14 memcmp

int memcmp ( const void * ptr1, const void * ptr2, size_t num );

比较从ptr1和ptr2指针开始的num个字节

16. 动态内存管理

16.1 为什么存在动态内存分配

我们已经掌握的内存开辟方式有:

int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个特点:

  1. 空间开辟大小是固定的。

  2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。

这时候就只能试试动态存开辟了。

16.2 动态内存函数介绍

16.2.1 malloc和free
void* malloc (size_t size);

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:

void free (void* ptr);

free函数用来释放动态开辟的内存。

  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptr 是NULL指针,则函数什么事都不做。

malloc和free都声明在 stdlib.h 头文件中。

#include <stdio.h>

int main() {
    //代码1
    int num = 0; 
    scanf("%d", &num); 
    int arr[num] = {0};
    //代码2
    int* ptr = NULL;
    ptr = (int*)malloc(num*sizeof(int));
    if(NULL != ptr)//判断ptr指针是否为空
    {
        int i = 0;
        for(i=0; i<num; i++) 
        {
            *(ptr+i) = 0}
    }
    free(ptr);//释放ptr所指向的动态内存
    ptr = NULL;//是否有必要?
    return 0; 
}
16.2.2 calloc

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:

void* calloc (size_t num, size_t size);
  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
#include <stdio.h> #include <stdlib.h>

int main() 
{
    int *p = (int*)calloc(10, sizeof(int)); 
    if(NULL != p)
    {
        //使用空间
    } 
    free(p);
    p  =  NULL; 
    return 0;
}

所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。

16.2.3 realloc
  • realloc函数的出现让动态内存管理更加灵活。

  • 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。

    函数原型如下:

void* realloc (void* ptr, size_t size);
  • ptr 是要调整的内存地址
  • size 调整之后新大小
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间
  • realloc在调整内存空间的是存在两种情况:
    • 情况1:原有空间之后有足够大的空间
    • 情况2:原有空间之后没有足够大的空间

情况1

当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

情况2

当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。

由于上述的两种情况,realloc函数的使用就要注意一些。

#include <stdio.h>

int main() 
{
    int *ptr = (int*)malloc(100); if(ptr != NULL)
    {
        //业务处理
    }
    else
    {
        exit(EXIT_FAILURE); 
    }
    //扩展容量
    //代码1
    ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)

    //代码2
    int*p  =  NULL;
    p = realloc(ptr, 1000); if(p != NULL)
    {
        ptr = p; 
    }
    //业务处理free(ptr); return 0;
}

realloc扩容失败直接返回NULL

16.3 常见的动态错误

####16.3.1 对NULL指针的解引用操作

void test() 
{
    int *p = (int *)malloc(INT_MAX/4); 
    *p  =  20;//如果p的值是NULL,就会有问题
    free(p);
}
16.3.2 对动态开辟空间的越界访问
void test() 
{
    int i = 0;
    int *p = (int *)malloc(10*sizeof(int)); 
    if(NULL  ==  p)
    {
        exit(EXIT_FAILURE); 
    }
    for(i=0; i<=10; i++) 
    {
        *(p+i) = i;//当i是10的时候越界访问
    }
    free(p); 
}
16.3.3 对非动态开辟内存使用free释放
void test() 
{
    int a = 10; 
    int *p = &a; 
    free(p);//ok?
}
16.3.4 使用free释放一块动态开辟内存的一部分
void test() 
{
    int *p = (int *)malloc(100); p++;
    free(p);//p不再指向动态内存的起始位置
}

####16.3.5 对同一块动态内存多次释放

void test() 
{
    int *p = (int *)malloc(100); 
    free(p);
    free(p);//重复释放
}
16.3.6 动态开辟内存忘记释放(内存泄漏)
void test() 
{
    int *p = (int *)malloc(100); 
    if(NULL != p)
    {
        *p = 20; 
    }
}

int main() 
{
    test(); 
    while(1);
}

忘记释放不再使用的动态开辟的空间会造成内存泄漏。

切记:

动态开辟的空间一定要释放,并且正确释放 。

16.4 几个经典的笔试题

16.4.1 题目1:
void GetMemory(char *p) 
{
    p = (char *)malloc(100); 
}
void Test(void) 
{
    char *str = NULL; 
    GetMemory(str);
    strcpy(str, "hello world"); 
    printf(str);
}

请问运行Test函数会有什么样的结果?

16.4.2 题目2:
char *GetMemory(void) 
{
    char p[] = "hello world"; 
    return p;
}
void Test(void) 
{
    char *str = NULL; 
    str = GetMemory(); 
    printf(str);
}

请问运行Test函数会有什么样的结果?

16.4.3 题目3:
void GetMemory(char **p, int num) 
{
*p = (char *)malloc(num); 
}
void Test(void) 
{
char *str = NULL; 
    GetMemory(&str, 100); 
    strcpy(str, "hello"); 
    printf(str);
}

请问运行Test函数会有什么样的结果?

16.4.4 题目4:
void Test(void) 
{
    char *str = (char *) malloc(100); 
    strcpy(str, "hello");
    free(str);
    if(str != NULL) 
    {
        strcpy(str, "world"); 
        printf(str);
    }
}

请问运行Test函数会有什么样的结果?

16.5 C/C++程序的内存开辟

C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。

  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。

  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁

所以生命周期变长。

16.6 柔型数组

也许你从来没有听说过**柔性数组(flexible array)**这个概念,但是它确实是存在的。C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

typedef struct st_type 
{
    int i;
    int a[0];//柔性数组成员
}type_a;

有些编译器会报错无法编译可以改成:

typedef struct st_type 
{
    int i;
    int a[];//柔性数组成员
}type_a;
16.6.1 柔性数组的特点
  • 结构中的柔性数组成员前面必须至少一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
typedef struct st_type 
{
    int i;
    int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//输出的是4
16.6.2 柔性数组的优势
  1. 方便内侧释放

如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。

  1. 有利于访问速度

连续的内存有益于提高访问速度,也有益于减少内存碎片。

17. 文件操作

17.1 什么是文件

磁盘上的文件是文件。

但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。

17.1.1 程序文件

包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。

17.1.2 数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

在以前所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。

其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。

17.1.3 文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。文件名包含3部分:文件路径+文件名主干+文件后缀

例如: c:\code\test.txt

为了方便起见,文件标识常被称为文件名

17.2 文件的打开和关闭

17.2.1 文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名

字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.

例如,VS2013编译环境提供的 stdio.h 头文件中有以下的文件类型申明:

struct _iobuf { 
    char *_ptr; 
    int	_cnt;
    char *_base; 
    int	_flag; 
    int   _file;
    int	_charbuf;
    int	_bufsiz; 
    char *_tmpfname; 
};
typedef struct _iobuf FILE;

不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。

每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。

一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。下面我们可以创建一个FILE*的指针变量:

FILE* pf;//文件指针变量

定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变

量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件

17.2.2 文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件

在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。

ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。

//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );

打开方式如下:

文件使****用方式含义如果指定****文件不存在
“r”(只读)为了输入数据,打开一个已经存在的文本文件出错
“w”(只写)为了输出数据,打开一个文本文件建立一个新的文件
“a”(追加)向文本文件尾添加数据建立一个新的文件
“rb”(只读)为了输入数据,打开一个二进制文件出错
“wb”(只写)为了输出数据,打开一个二进制文件建立一个新的文件
“ab”(追加)向一个二进制文件尾添加数据出错
“r+”(读写)为了读和写,打开一个文本文件出错
“w+”(读写)为了读和写,建议一个新的文件建立一个新的文件
“a+”(读写)打开一个文件,在文件尾进行读写建立一个新的文件
“rb+”(读写)为了读和写打开一个二进制文件出错
“wb+”(读写)为了读和写,新建一个新的二进制文件建立一个新的文件
“ab+”(读写)打开一个二进制文件,在文件尾进行读和写建立一个新的文件

实例代码:

/* fopen fclose example */
#include <stdio.h> int main ()
{
    FILE * pFile;
    //打开文件
    pFile = fopen ("myfile.txt","w");
    //文件操作
    if (pFile!=NULL) 
    {
        fputs ("fopen example",pFile);
        //关闭文件
        fclose (pFile);
    }
    return 0; 
}

17.3 文件的顺序读写

功能函数名适用于
字符输入函数fgetc所有输入流
字符输出函数fputc所有输出流
文本行输入函数fgets所有输入流
文本行输出函数fputs所有输出流
格式化输入函数fscanf所有输入流
格式化输出函数fprintf所有输出流
二进制输入fread文件
二进制输出fwrite文件

scanf/fscanf/sscanf

printf/fprintf/sprintf

17.4 文件的随机读写

根据文件指针的位置和偏移量来定位文件指针。

int fseek ( FILE * stream, long int offset, int origin );

例子:

/* fseek example */
#include <stdio.h>
int main () 
{
    FILE * pFile;
    pFile = fopen ( "example.txt" , "wb" ); 
    fputs ( "This is an apple." , pFile ); 
    fseek ( pFile , 9 , SEEK_SET );
    fputs ( " sam" , pFile ); 
    fclose ( pFile );
    return 0; 
}

17.5 文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件或者二进制文件

数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件

如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件

一个数据在内存中是怎么存储的呢?

字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。

#include <stdio.h> int main()
{
    int a = 10000;
    FILE* pf = fopen("test.txt", "wb"); 
    fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
    fclose(pf);
    pf = NULL; 
    return 0;
}

17.6 文件读取结束的判定

17.6.1 被错误使用的 feof

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束

  1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )例如:

    • fgetc 判断是否为 EOF .
    • fgets 判断返回值是否为 NULL .
  2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。例如:

    • fread判断返回值是否小于实际要读的个数。

文本文件的例子:

#include <stdio.h> #include <stdlib.h>

int main(void) 
{
    int c; // 注意:int,非char,要求处理EOF 
    FILE* fp = fopen("test.txt", "r"); if(!fp) {
        perror("File opening failed"); 
        return EXIT_FAILURE;
    }
    //fgetc  当读取失败的时候或者遇到文件结束的时候,都会返回EOF 
    while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
    {
        putchar(c);
    }
    //判断是什么原因结束的
    if (ferror(fp))
        puts("I/O error when reading");
    else if (feof(fp))
        puts("End of file reached successfully");
    fclose(fp);
}

二进制文件的例子:

#include <stdio.h> enum { SIZE = 5 };
int main(void) {
    double a[SIZE] = {1.,2.,3.,4.,5.};
    FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
    fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组
    fclose(fp);

    double b[SIZE];
    fp = fopen("test.bin","rb");
    size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组
    if(ret_code == SIZE) {
        puts("Array read successfully, contents: "); for(int n = 0; n < SIZE; ++n) printf("%f ", b[n]); putchar('\n');
    } else { // error handling
        if (feof(fp))
            printf("Error reading test.bin: unexpected end of file\n");
        else if (ferror(fp)) {
            perror("Error reading test.bin");
        }
    }

    fclose(fp); }

17.7 文件缓冲区

ANSIC 标准采用**“缓冲文件系统”**处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。

#include <stdio.h> #include <windows.h>
//VS2013  WIN10环境测试
int main()
{
    FILE*pf = fopen("test.txt", "w");
    fputs("abcdef", pf);//先将代码放在输出缓冲区
    printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n"); Sleep(10000);
    printf("刷新缓冲区\n");
    fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
    //注:fflush 在高版本的VS上不能使用了
    printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n"); Sleep(10000);
    fclose(pf);
    //注:fclose在关闭文件的时候,也会刷新缓冲区
    pf = NULL;

    return 0; }

这里可以得出一个结论

因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。

如果不做,可能导致读写文件的问题。

18. 程序环境和预处理

18.1 程序的翻译环境和执行环境

在ANSI C的任何一种实现中,存在两个不同的环境。

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。

第2种是执行环境,它用于实际执行代码。

18.2 详细编译+链接

18.2.1 翻译环境
  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。

  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。

  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

18.2.2 编译的不同阶段

看代码:

sum.c

int g_val = 2016;
void print(const char *str) 
{
    printf("%s\n", str); 
}

test.c

#include <stdio.h> int main()
{
    extern void print(char *str); 
    extern int g_val; 
    printf("%d\n", g_val); 
    print("hello bit.\n");
    return 0; 
}

编译期间的每一步发生了什么呢?

  1. 预处理 选项 gcc -E test.c -o test.i

预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。

  1. 编译 选项 gcc -S test.c

编译完成之后就停下来,结果保存在test.s中。

  1. 汇编 gcc -c test.c

汇编完成之后就停下来,结果保存在test.o中。

18.2.3 运行环境

程序执行的过程:

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

  2. 程序的执行便开始。接着便调用main函数。

  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。

  4. 终止程序。正常终止main函数;也有可能是意外终止。

18.3 预处理详解

18.3.1 预定义符号
__FILE__	//进行编译的源文件
__LINE__	//文件当前的行号
__DATE__	//文件被编译的日期
__TIME__	//文件被编译的时间
__STDC__	//如果编译器遵循ANSI  C,其值为1,否则未定义

这些预定义符号都是语言内置的。

printf("file:%s line:%d\n", __FILE__, __LINE__);
18.3.2 #define
18.3.2.1 #define 定义标识符
语法:
#define name   stuff
#define MAX 1000
#define reg register	//为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)	//用更形象的符号来替换一种实现
#define CASE break;case	//在写case语句的时候自动把 break写上。
//  如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n" ,\ __FILE__,__LINE__ ,\ __DATE__,__TIME__  )

在define定义标识符的时候,最后不用加上 ;

18.3.2.2 #define 定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

#define name( parament-list ) stuff

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

注意:

参数列表的左括号必须与name紧邻。

如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

ex:

#define DOUBLE( x)	( ( x ) + ( x ) )

所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

18.3.2.3 #define 替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。

  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。

  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。

  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

#####17.3.2.4 #和##

如何把参数插入到字符串中?

char* p = "hello ""bit\n"; 
printf("hello"" bit\n"); 
printf("%s", p);
  1. 使用 # ,把一个宏参数变成对应的字符串。比如:
int i = 10;
#define  PRINT(FORMAT,  VALUE)\
printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
...
PRINT("%d", i+3);//产生了什么效果?

最终的输出的结果应该是:

the value of i+3 is 13
  1. ## 的作用
#define ADD_TO_SUM(num, value) \
sum##num += value;
...
    ADD_TO_SUM(5,  10);//作用是:给sum5增加10.

注:

这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

18.3.2.5 宏和函数对比

宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。

#define MAX(a, b) ((a)>(b)?(a):(b))

#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不用函数来完成这个任务?原因有二:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹

  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的

**宏的缺点:**当然和函数相比宏也有劣势的地方:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

  2. 宏是没法调试的。

  3. 宏由于类型无关,也就不够严谨。

  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

#define MALLOC(num, type)\
(type *)malloc(num * sizeof(type))
...
    //使用
    MALLOC(10, int);//类型作为参数

//预处理器替换之后:
(int *)malloc(10 * sizeof(int));

#define****定义宏函数
代 码 长 度每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执 行 速 度更快存在函数的调用和返回的额外开销,所以相对慢一些
操 作 符 优 先 级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括 号。函数参数只在函数调用的时候求值一次,它的结果值传递给函 数。表达式的求值结果更容易预测。
带 有 副 作 用 的 参 数参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一次,结果更容易控制。
参 数 类 型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。
调 试宏是不方便调试的函数是可以逐语句调试的
递 归宏是不能递归的函数是可以递归的
18.3.2.6 命名的约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的一个习惯是:

把宏名全部大写

函数名不要全部大写

18.3.3 #undef
这条指令用于移除一个宏定义。
#undef  NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
18.3.4 命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)

#include <stdio.h> int main()
{
    int array [ARRAY_SIZE]; int i = 0;
    for(i = 0; i< ARRAY_SIZE; i ++) {
        array[i] = i;
    }
    for(i = 0; i< ARRAY_SIZE; i ++) {
        printf("%d " ,array[i]);
    }
    printf("\n" ); return 0;
}

编译指令:

//linux 环境演示
gcc  -D  ARRAY_SIZE=10  programe.c
18.3.5 条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

比如说:

调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

#include <stdio.h> #define  __DEBUG__

int main() {
    int i = 0;
    int arr[10] = {0}; for(i=0; i<10; i++) {
        arr[i] = i;
        #ifdef __DEBUG__
        printf("%d\n", arr[i]);//为了观察数组是否赋值成功。#endif //__DEBUG__
    }
    return 0;
}

常见的条件编译指令:

//1
#if   常量表达式
//... 
#endif
//常量表达式由预处理器求值。如:
#define  __DEBUG__  1 
#if  __DEBUG__
//.. 
#endif

//2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//... 
#endif


//3.判断是否被定义
#if defined(symbol) 
#ifdef symbol

#if !defined(symbol) 
#ifndef symbol

//4.嵌套指令
#if defined(OS_UNIX) 
	#ifdef OPTION1
		unix_version_option1();
	#endif
	#ifdef OPTION2 
		unix_version_option2();
	#endif
#elif defined(OS_MSDOS)
	#ifdef OPTION2 
		msdos_version_option2();
	#endif 
#endif
18.3.6 文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。

种替换的方式很简单:

预处理器先删除这条指令,并用包含文件的内容替换。这样一个源文件被包含10次,那就实际被编译10次。

18.3.6.1 头文件被包含的方式:
  • 本地文件包含

查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。

如果找不到就提示编译错误。

Linux环境的标准头文件的路径:

/usr/include

注意按照自己的安装路径去找。

  • 库文件包含
18.3.6.2 嵌套文件包含

条件编译

#ifndef __TEST_H__ 
#define __TEST_H__
//头文件的内容
#endif	//__TEST_H__

或者

#pragma  once

就可以避免头文件的重复引入。

18.4 其他预处理指令

#error 
#pragma 
#line
...
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小豪GO!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值