C语言从入门到进阶

C语言从入门到进阶

1.认识C语言

  • C语言作为一门面向过程的程序设计语言,与数据库系统语言(SQL、Sybasede等)、面向对象的程序设计语言(Java、C++等)有所不同,在程序设计过程中必须按照一定的方法将所需要解决的问题分解成相应的步骤,然后用语言表述出每一步的操作过程,可让用户轻松完成自上而下的规划、结构化编程和模块化设计
  • C语言凭借着执行效率高、可移植性好,并且允许直接访问物理地址与操作硬件,广泛应用于应用底层开发、设备驱动程序编写、操作系统开发、嵌入式系统开发、图形图像系统开发等。
    -当然,人无完人,金无足赤。C语言也存在一些缺点。例如,C语言的指针可以访问到物理地址,这给C语言提供了极大的自由,当然也会凸显出许多问题。有句话说得好,“想要拥有自由就必须时刻保持警惕”!
  • C语言程序设计的一般步骤:
    (1)定义程序的目标:明确程序可以干什么,需要哪些信息
    (2) 设计程序:怎么依靠程序完成相应的功能,通常需要借助程序流程图来实现
    (3) 编写代码
    (4) 编译
    (5)运行程序
    (6)测试和调试程序:成功运行后,看是否达到预期效果;如果存在bug或者未达到,则需要一步步进行调试
    (5)维护和修改代码:想要拓展功能或者找到更好的解决方案,需要在此基础上进行修改
  • C语言作为编译型语言,编译的主要过程为:编辑生成源程序(.c)——>预处理,执行以#开头的指令——>编译生成二进制的目标程序(.obj)——>连接生成可执行程序(.exe)——>运行
    图1-1 C程序编译过程示意图
    可执行程序与目标程序都包含机器语言代码,但目标程序不能直接运行该文件,因为目标程序中仅有编译的源代码缺少库代码(库文件中包含所引用函数的目标代码)以及启动代码(充当程序和操作系统之间的接口),不是一个完整的程序。

2. 第一个C语言程序

#include <stdio.h>             //编译预处理,引用头文件,因为主函数里面的printf()是由
                              // 库函数里面提供的,stdio:standard input&output的缩写

int main(void)                 //主函数,void表示main函数没有参数
{
	printf("Hello World\n");   //调用打印函数,\n表示换行
	return 0;                 // 因为主函数是int型,所以要给操作系统返回一个int型的值
}

由此可见,C程序有以下组成特点

  1. 一个C源程序有且仅有一个main()函数,且程序总是从main()函数开始执行
  2. 函数是C程序的基本组成单位,每个源程序可有一个或多个函数组成,且所有函数都是独立的
  3. 函数名后面必须要带有(),且函数都必须有函数体——大括号{ }
  4. 每一个语句都必须以分号结尾,作为结束标志

3.C语言的方方面面

3.1基本数据类型

C语言的数据类型如下图所示,基本数据类型包括整型、浮点型型以及字符型,其余类型后面介绍:

在这里插入图片描述

  • 不同类型的数据占用的存储空间长度是不一样的,同一类型的数据也因计算机字长的差异占用不同长度的存储空间
    (1)计算机的存储单位
单位单位换算说明
bit/b(位)\计算机中最小的单位,可以存储一个二进制数0或1
Byte (字节)1Byte=1024bit(210计算机中的基本单位,每八位组成一个字节。各种信息在计算机中存储、处理至少一个字节
Word(字)1Word=2Byte(210汉字的储存单位都是字
KB1KB=1024Byte(210\
MB1MB=1024KB(210\
GB1GB=1024MB(210\
TB1TB=1024GB(210\
PB1PB=1024TB(210\

(2)基本数据类型占用的空间
求所占空间大小,需要使用到单目运算符sizeof(),同时也是一个关键字。可求出一个对象的所占内存大小,其结果以字节为单位

一般形式:sizeof(类型名)
括号里面可为:数据类型、常量、变量、表达式
其返回类型为size_t,当采用printf打印输出时,要用%zu

#include <stdio.h>

int main()
{
	printf("char:%zu\n", sizeof(char));   //%zu是sizeof()输出的标准打印格式
	printf("short:%zu\n",sizeof(short));
	printf("unsgned short:%zu\n", sizeof(unsigned short));
	printf("int:%zu\n", sizeof(int));
	printf("unsigned int:%zu\n", sizeof(unsigned int));
	printf("long:%zu\n", sizeof(long));
	printf("unsigned long:%zu\n", sizeof(unsigned long));
	printf("long long:%zu\n", sizeof(long long));
	printf("unsigned long long:%zu\n", sizeof(unsigned long long));
	printf("float:%zu\n", sizeof(float));
	printf("double:%zu\n", sizeof(double));
	printf("long double:%zu\n", sizeof(long double));
 
	return 0;
}

在这里插入图片描述

  • 根据运行结果可知:无符号整型与有符号整型,所占内存空间大小一样,但所表示的范围不一样

3.1.1整型

整型:整型数据没有小数部分,根据有无符号位,又分为有符号类型(默认)和无符号类型(unsigned)
(1)所有数据在内存中都是以二进制形式存放的,整形数据在内存中以补码的形式存放
(2)有符号位的整数,左面第一位表示符号,0为正,1为负
补码的求法:
(1)正数和无符号数:原码=补码;负数:符号位不变,原码求反(0变1,1变0),之后加1
(2) 扫描法:从右至左,遇见第一个1不变,其他按位取反,符号位不变

  • 使用补码的原因:使用补码,可以将符号位和数值域统一处理;
    同时,加法和减法也可以统一处理(CPU只有加法器)。
    此外,补码与原码相互转换(符号位不变,求反加1),其运算过程是相同的,不需要额外的硬件电路。
数据类型字节数(位数)取值范围
short / unsigned short2(16)-215 ~215 -1 /0 ~216 -1
int / unsigned int4(32)-231 ~231 -1 /0 ~232 -1
long / unsigned long4(32)-231 ~231 -1 /0 ~232 -1
  • 注:
    (1)C语言中规定long类型变量所占字节大于等于int类型变量所占字节,即sizeof(long)>=sizeof(int)
    (2)在使用过程中,一定要注意无符号数的使用,很容易造成死循环

有符号的数据类型的最小值

以short类型为例
00000000000000000   ——>  0
00000000000000001   ——>  1
00000000000000010   ——>  3
........
0111111111111111    ——>  32727
1000000000000000    ——> - 32728
1000000000000001    ——> - 32727
...........
1111111111111111    ——>  -1

有符号的short:
1000000000000000在计算机中是以补码的形式存放的
换成原码是:1 0000 0000 0000 0000
1为进位,被舍弃掉,所以结果为0?

不,有符号的数据类型中(1000000000000000)直接转化为最小值

3.1.2浮点型

浮点型:带小数部分的数据,也称为实型;在C99中新增复数类型:float _Complex,double _Complex等

关键字字节数(位数)有效字数绝对值的范围
float4(32)6~7-3.4•10-37 ~3.4•1038
double8(64)15~16-1.7•10-307 ~1.7•10308
long double16(128)18~19-1.2•10-4931 ~1.2•104932

注:对于小数,比如12.43,可能不会被精确保存,二进制存储的值可能无限接近但不等于,会有一定的精度误差

  • 在IEEE754标准中,float=(-1)S•(1.M)•2e ,其中:32位浮点数float:e=E-127(28-1-1)或64位浮点数double:e=E-1023(211-1-1)
    在这里插入图片描述

其中,S:符号位,E:指数位,M:尾数位
由于尾数域表示1.M,其左边恒为1,故1不储存,当取数时在尾数前加1即可

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

当从内存中去除时,分为三种情况:

  1. E不为全0,也不为全1
    这时,浮点数就采用上述规则的逆过程,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1,化成公式
  2. E全为0
    这时,浮点数的指数E等于1-127(或者1-1023)即为真实值
    有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
  3. E全为1
    如果有效数字M全为0,表示±无穷大(正负取决于符号位s)

实例演示:9.221——>float的二进制存储形式
(1)十进制化为二进制:(9.25)10=(1001.01)2
(2)转化尾数:(1001.01)2=1.M=1.00101*23(这里并不是8,而是移位阶码为3,也就是小数点左移3位)
(3)求E:E=127+3=130,(130)10=(10000010)2
在计算机存储的形式为:01000001000101000000000000000000
转化为十进制:float=(-1)0•(1.00101000000000000000000)•23=1*(1001.01)2=(9.25)10


(1)取值范围取决于指数部分:float对应的指数范围为-128~128,所以取值范围为:-2128 ~2128
(2)精度取决于尾数位:223 =8388608,一共七位,这就意味着有效字数至少为6,最多为7

3.1.3字符型

字符型(char):在内存中占一个字节,存储的实际上是字符的ASCII码。当然,也分为有符号型和无符号型

  • (1)‘0’:48
    (2)‘A’:65
    (3)‘a’:97
#include <stdio.h>
#include <string.h>

int main() 
{
	char a[500] ;
	int i = 0;
	for(i = 0;i < 500;i++)
	{
		a[i] = -1 - i;
	}
	
	printf("%d\n",strlen(a));
    return 0;
}

在这里插入图片描述

对于上述代码而言,char类型变量可存范围:-128~127
上述代码在一个循环里,a[i]的值依次是-1、-2…-128、127、126…1、0
就是在其存储范围内,一直逆时针旋转,如下图所示
而strlen()遇到字符串中的结束标志’\0’(ASCII:0),就结束查找

在这里插入图片描述

3.1.4数据在内存中的存放方式

在计算机系统中,内存空间是以字节为单位,除char类型外,其他类型的变量所占空间都大于1个字节。这里就涉及到了字节的存放顺序

  • 大端【字节序】存储模式,是指数据的高位字节序中的内容保存在内存的低地址中;而数据的低位字节序中的内容保存在内存的高地址中。
  • 小端【字节序】存储模式,是指数据的低位字节序中的内容保存在内存的低地址中,而数据的高位字节序中的内容保存在内存的高地址中。

3.1.5数据类型转换

3.1.5.1自动类型转换

①自动类型转换
在这里插入图片描述

3.1.5.2强制类型转换

②强制类型转换
一般形式:(类型说明符) (表达式)

float)  dividend/divisor;
((float)  dividend)/divisor;
  • 强制转换形式位一元运算符,其优先级高于二元运算符。所以上述两条语句等价

说明:无论是自动转换或是强制转换,都只是对变量的数据进行临时性转换,而不改变数据说明时对该变量定义的类型

在涉及到计算时,一定一定要注意数据类型是否相同以及是否溢出

3.1.6类型定义(type definition)

一般形式:typedef 原类型名 新类型名

typedef unsidned char uint8;
  • 语句的含义:用uint8代替unsigned char,即unsigned char=uint8

注:
(1)语句后面需要添加分号
(2)本过程中并没有产生新的数据类型,而是给仅有的数据类型名,起了一个更加简洁的名字。方便代码的移植和可阅读性。
(3)typedef是在编译中完成的,#define宏定义则是在预处理中完成的

  • 通俗的讲:typedef语句就是专门给数据类型重新起名,将后面的对象替换为前面的对象

3.2标识符与关键字

3.2.1标识符

标识符:关键字、特定标识符(大都是预处理命令)、用户自定义标识符
其命名规则:
(1)只能是大小写字母、数字、下划线组成的字符串
(2)第一个字符必须是字母或下划线,区分大小写字母

  • (1)标识符中区分大小写
  • (2)关键字和特定标识符不能作为用户自定义标识符,同样也也不要和库函数重名

3.2.2关键字

在这里插入图片描述

static:

对于定义的变量,其前面是有auto(自动的)修饰的,一般都将其省略,存放栈区,由操作系统创建和销毁
(1)修饰局部变量,当出作用域时,变量不销毁;,对于未初始化的变量,会初始化为0
实质:改变了存储位置(存储到静态区),生命周期为整个工程,但作用域没变
在这里插入图片描述

(2)修饰全局变量:全局变量具有外部链接属性(可以被工程内其他源文件使用)。但是当使用static修饰全局变量后,就变为内部链接属性,其他源文件(.c)不可使用,作用域变小。同样,该全局变量也被放在静态区中。
(3)修饰函数:函数具有外部链接属性,当使用static修饰全局变量后,就变为内部链接属性,其他源文件无法使用。

extern:

声明工程内其他文件中的外部符号(全局变量、函数),不分配存储空间
(1)声明全局变量的一般形式:

extern 数据类型 变量名;

①int a;
②int a = 0;
③extern int a = 0;
④extern int a;

①②③均是对变量a进行定义,但是定义方式尽量不要采用③
④对变量a进行声明
(2)声明函数
声明函数时,extern可有可无

注:声明可以有多次,但定义只能有一次
声明:告诉告诉编译器变量的名称和类型,而不分配内存,不赋初值
定义:给变量分配内存,可以为变量赋初值。

定义要为变量分配内存空间;而声明不需要为变量分配内存空间

总结:

  • 对变量而言,如果你想在本源文件中使用另一个源文件的变量,就需要在使用前用extern声明该变量,或者在头文件中用extern声明该变量;
  • 对函数而言,如果你想在本源文件中使用另一个源文件的函数,就需要在使用前用声明该函数,声明函数加不加extern都没关系,所以在头文件中函数可以不用加extern。
const:

const常用来修饰变量,使其称为常变量,其本质仍未变量,但具有常量的属性

与预编译指令相比,const修饰符有以下的优点:
1、预编译指令只是对值进行简单的替换,不能进行类型检查

2、可以保护被修饰的东西,防止意外修改,增强程序的健壮性

const的用法:

① 修饰变量

const int a = 1;
int const a = 1;

两种写法是一样的,但使用const修饰变量时,一定要给变量初始化,否则之后就不能再进行赋值了

虽然不能直对const修饰的常变量赋值,但可以通过指针进行赋值

int main()
{
    const int a = 1;
    int*  n = &a;
    *n = 8;
    printf("%d",*n);
    return 0;
}

② 常量指针与指针常量
如果我们将**星号读作‘指针’,将const读作‘常量’**的话,那么上面的两个定义就迎刃而解

常量指针:指针指向的内容是常量

const int * p;
int const * p;

需要注意的以下两点:
(1)常量指针是不能通过这个指针改变变量的值,但是还是可以通过其他的方法来改变变量的值的。

int a = 1;
const int* p = &a;
a = 2;

(2)常量指针指向的值不能改变,但是常量指针可以指向其他的地址。

int a = 1;
int b = 2;
const int* p = &a;
p = &b;

简而言之,常量指针不能通过解引用常量指针来改变所指向变量的值

指针常量:指针本身是个常量,不能在指向其他的地址

int *const p;

需要注意的是,指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向改地址的指针来修改。

int a = 5;
int* const p = &a;
*p = 8;

(3)指向常量的常指针
指针指向的位置不能改变并且也不能通过这个指针改变变量的值,但是依然可以通过其他的普通指针改变变量的值。

const int* const p;

③ 修饰函数的参数

根据常量指针与指针常量,const修饰函数的参数也是分为三种情况
(1)防止修改指针指向的内容

void test(char *s, const char *d);

给形参指针变量d 加上 const 修饰后,如果函数体内的语句试图改动 d 的内容,编译器将会报错

(2)防止修改指针指向的地址

void swap ( int * const p1 , int * const p2 );

指针p1和指针p2指向的地址都不能修改。

(3)以上两种的结合。

④ 修饰函数的返回值

如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针

const char * test(void);

下列赋值语句错误:

char *s = test();

正确的用法:

const char *s = test();
typedef与define(define并不是关键字,而是预处理命令)
  • define用法:
    (1)无参宏定义
    一般形式:
#define 标识符 内容

功能:用指定标识符来代替后面的内容,在对源程序进行编译时,由预处理程序进行宏替换,即用后面的内容代替与之对应的标识符,然后再进行编译

  • 内容出现运算表达式时,需要加上括号,防止产生错误
#define M ((x) + (x))
  • 宏定义必须写在函数之外,其作用域:宏定义命令起到源程序结束,如果要终止作用域可以使用
#undef 标识符
  • 宏定义允许嵌套,但不允许递归
  • 宏定义不是语句,不需要再末尾加分号,如果加上分号则一起替换
    (2)有参宏定义
    一般形式:
#define 宏名(形参表) 内容
#define MAX(x,y) (x>y)?x:y
  • 宏定义可以定义多条语句
  • 有参宏与函数的区别:
    (1)有参宏定义中,形参不分配内存单元不必作类型定义,只是符号替换
    函数中,形参和实参是两个不同的变量,都有各自的作用域,调用时:实参把值赋予形参,进行“值传递”
    (2)宏定义中,对于实参表达式直接替换形参
    函数要把实参表达式的值求出来再赋予形参

define的其他用途:
(1)防止重复包含头文件

#ifndef __XXX__H_
#define __XXX__H_
…
文件内容
…
#endif

(2)条件编译

#ifdef WINDOWS
......
(#else)
......
#endif
#ifdef LINUX
......
(#else)
......
#endif
  • typedef用法:
    功能:为已存在的数据类型取一个新的名称,只在指定数据类型和作用域内有效,对其他类型不产生任何影响

一般形式:typedef 原类型名 新类型名

typedef unsidned char uint8;
  • 语句的含义:用uint8代替unsigned char,即unsigned char=uint8

注:
(1)语句后面需要添加分号
(2)本过程中并没有产生新的数据类型,而是给仅有的数据类型名,起了一个更加简洁的名字。方便代码的移植和可阅读性。

两者的区别:
(1)typedef是在编译中完成的,有类型检查功能,#define宏定义则是在预处理中完成的,仅进行内容的替换,没有类型检查和语法分析,只有在编译时才能检查出错误
(2)定义指针变量不同

#define X int*
typedef int* Y
X p1, p2;
等同于int *p1,p2;//一个指针变量,一个整形变量

Y p1,p2; //两个都是指针变量

(3)#define是预处理命令,不需要加分号;而typedef语句需要加分号
(4)#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而typedef有自己的作用域。

  • 通俗的讲:typedef语句就是专门给数据类型重新起名,将后面的对象替换为前面的对象,#define宏定义就是纯粹的替换将前面的对象替换为后面的对象

3.3常量

常量:在程序执行过程中,其值不能改变的量

  • 常量可分为:直接常量、#define定义的标识符常量、const修饰的常变量、枚举常量

3.3.1直接常量

3.3.1.1整型常量

整型常量:十进制、八进制(0作为前缀)、十六进制(0X作为前缀)等

  • 十进制转化其他进制:
    (1)除以进制数,直至商为0,取余数(从上往下)
    (2)先转化为二进制,以小数点为界,向左、向右三位(八进制)\四位(十六进制)为一组,用10进制数字替换
  • 其他进制转化为十进制:位权展开求和

注:

  1. 当明确一个整型常量为一个无符号数,可以加上后缀U/u。例:15U、0251U均表示无符号常量
  2. 当明确一个整型常量作为一个长整数处理,可以加上后缀L/l。例:15L、0251l均表示长整数常量
  3. 由于每一种数据类型都有取值范围,当进行运算时,要注意是否发生溢出
3.3.1.2浮点型常量

浮点型常量:十进制小数形式和指数形式

  • 小数形式:由正负号、数字、小数点组成
    注:小数点前面、后面可以没有数字,但小数点必须有
  • 指数形式:aEn=aX10n ,a可以是十进制小数或整数,n必须为整数
    注:字母E前面必须有数字,后面必须为整数
  • 浮点型常量不加任何后缀默认为double类型。在常量的末尾处加上字母 F 或 f(如 57.0F)表示float;在常量的末尾处加上字母 L 或 l(如 57.0L)表示long double
3.3.1.3字符常量

字符常量:普通字符(用单引号括起来的一个字符)和转义字符

3.3.1.3.1 转义字符
  • 转义字符以反斜线开头,具有特定的含义
    在这里插入图片描述
  • 同时,还包括\ddd(1~3位八进制所代表的字符)和\xhh(1~2位十六进制所代表的字符x必须小写

注:
(1)字符常量必须用单引号括起来,包括转义字符
(2)字符常量是单个字符,不能是多个

3.3.1.4字符串常量

字符串常量由一对双引号括起来的字符序列
例:“abcde”、“CHINA"、”+a12.0“等等

注:
(1)字符串常量可包含0个或多个字符
(2)字符串常量在内存中所占字节数=字符个数+1,这是因为:\0作为字符串结束标志,占用内存,但不作为内容。所以使用strlen()计算字符串长度不包括\0(ASCII码值为0),当遇到\0停止
(3)字符串使用数组保存
在这里插入图片描述

3.3.2#define定义的符号常量

符号常量:若某个常量多次被使用,则可以用一个标识符来表示该常量

  • 定义符号常量:#define 标识符 直接常量,#define为预处理命令,将标识符定义为后面的直接常量
#define PI 3.14
  • 语句含义:用PI代替3.14,即PI==3.14

定义宏:#define 宏名(宏的参数) 宏体

在这里插入图片描述

注:
(1)末尾不加分号
(2)宏定义在预处理中完成的
(3)符号常量中的标识符默认都为大写

  • 通俗的讲:#define宏定义就是纯粹的替换将前面的对象替换为后面的对象

3.3.3const修饰的常变量

#include <stdio.h>

int main(void)
{
	const int a=5;
	return 0;
}
  • 变量a为常变量,其本质为变量,但是其值不能直接修改,具有常量的属性

3.3.4枚举常量

enum week
{
	MON,
	TUE, 
	WED,
	THU, 
	FRI, 
	SAT, 
	SUN
}

int main()
{
	enum week a = MON;  //创建一个枚举变量,赋值为MON
	return 0;
}
  • 注: 第一个枚举成员的默认值为整型的 0,后续枚举成员的值依次加1。

3.4变量

3.4.1 定义变量

变量:在程序的运行过程中,其值可以改变的量。

  • 变量定义:数据类型说明符 变量名1,变量名2,变量名3,……;
    例:int a,b,c;
    注:
    (1)变量必须先定义后使用
    (2)同一函数中变量不能重复定义
  • 变量初始化:变量在定义时赋初值
    例:int a=5,b=6;
    注:
    (1)在变量初始化时,不允许对多个未定义的同类型变量连续初始化
    即,int a=b=c=5;,此初始化不合法
    (2)变量初始化时,应注意类型的一致性
  • 变量赋值:在变量定义后,可以用赋值号(=)将一个表达式的值赋给一个变量

3.4.2 全局变量、局部变量的作用域和生命周期

#include <stdio.h>
int c=0;//全局变量————定义在大括号{}外的变量
int main()
{
	int a=0;//局部变量————定义在大括号{}里的变量
	return 0;
}
  • 当全局变量与局部变量命名相同时,局部优先原则
  • 变量作用域:变量可用性的代码范围
    (1)全局变量的作用域为整个工程
    (2)局部变量的作用域为变量所在的局部范围
  • 变量的生命周期:变量从创建到销毁的时间段
    (1)全局变量的生命周期为整个程序的生命周期
    (2)局部变量的生命周期为从进入作用域开始,离开作用域结束

3.5运算符和表达式

在这里插入图片描述

在这里插入图片描述

3.5.1基本算术运算符

  • 加、减、乘、除、取余(%)
    ①如果参与加、减、乘、除运算的有一个浮点型,结果就是double类型,这是因为所有实数都按double类型运算,对于除运算而言,当参与运算的数均为整型,结果也为整型
    取余(%)要求参与运算的量均为整型
  • 自增、自减运算符
    ①前置:++i,–i:先加或减1,再使用i的值
    ②后置:i++,i–:先使用i的值,再加或减1
    注:自增、自减运算符只能用于变量,不能用于常量或表达式
  • 正(+)负(-)号运算符
    优先级与自增、自减同级,高于基本算术运算符,结合方向自右向左

3.5.2位运算符

  • &(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)、>>(右移)
    ① &:双目运算符,两个二进位全1为1,否则为0
    功能:清零(a & 0)、保留(a & 1)
    ② |:双目运算符,两个二进位有1为1,否则为0
    功能:置1(a | 1)
    ③ ^:双目运算符,两个二进位相异为1,否则为0
    功能:置0(a ^ a)、保留(0 ^ a)不用中间变量交换两个变量的值(符合交换律)
    ④ ~:单目运算符,具有右结合性,对二进位按位求反
    功能:0的反码为-1,1的反码为-2,以此为对称轴
    ⑤ <<:双目运算符,高位丢弃,低位补0
    功能:左移n位相当于乘以2n
    ⑥ >>:双目运算符,低位丢弃,正数(无符号数)高位补零;负数高位补1(不同编译器负数高位补的不同)
    功能:右移n位相当于除以2n
    注:
  • 在计算机中,二进制是以补码形式存在的(正数·:正码=补码,负数:详见3.1.1)
  • 位运算符的对象只能是整型或字符型的数据
  • 对于不同位数的运算,将两个运算数右对齐,再将位数少的(正数和无符号数左侧补0,负数左边补1)
不用中间变量交换两个变量的值
#include <stdio.h>

int main()
{
	//按位异或运算
	int a = 3;
	int b = 5;
	a = a ^ b;  //a = 3 ^ 5
	b = a ^ b;  //b = 3 ^ 5 ^ 5 ——>b = 3
	a = a ^ b;	//a = 3 ^ 5 ^ 3 ——>a = 5
	return 0;
}

3.5.3赋值运算符

  • =、复合赋值运算符(+=、-=、*=、<<=、>>=、&=)
    赋值运算符左侧必须是变量,具有右结合性

3.5.4逻辑运算符

  • !(非)、&&(与)、||(或)
    其优先级由高到低:!> && > ||
    (1)a&b&c,只有a为真时才会判断b,也就是只有表达式为才会判断下一表达式**
    (2)a||b||c,只有a为假时才会判断b,也就是只有表达式为才会判断下一表达式**

3.5.5关系运算符

  • <、>、<=、>=、!=、==
    关系表达式的值为逻辑值,即“真”或“假”

3.5.6逗号运算符

  • 逗号运算符:
    一般形式:表达式1,表达式2
    从左向右依次计算表达式的值,并以最后一个表达式的值作为整个表达式的值
    结合性:自左向右

3.5.7条件运算符

  • exp1?exp2:exp3
    如果表达式1为真,则只计算表达式2,并作为结果值;
    如果表达式1为假,则只计算表达式3,并作为结果值;

3.5.8取地址和间接访问运算符

  • &、*
    &:取地址操作符,取到某一对象所在内存的地址
    *:解引用操作符,通过地址在内存中找到存放的对象

3.5.9其他运算符

  • 求字节运算符:sizeof,计算的是变量或类型创建变量时所占空间大小,但不计算括号里面的表达式,单位:字节
  • 强制类型转换运算符:(类型)
  • 下标运算符:[下标],操作数:数组名,下标值
  • 函数调用操作符:(),操作数:函数名,若干参数
  • 分量运算符:. 、->
    结构体.成员名
    结构体指针->成员名

在这里插入图片描述

3.5.10表达式

  • 表达式:由常量、变量、函数和运算符组合起来的式子
  • 求值顺序由操作符的优先级和结合性确定
    例:a+b、(a/b)-9等
3.5.10.1整型提升

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

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

  • 无符号和正数:高位全补0;负数:高位全补1

在这里插入图片描述
只要参与表达式运算就会发生整型提升,例如:+c、-c

#include <stdio.h>

int main() 
{
	char a = -1;
	
	signed char b = -1;
	
	unsigned char c = -1;
	
	printf("a=%d,b=%d,c=%d\n",a,b,c);
    return 0;
}

在这里插入图片描述
这里涉及到截断和整型提升的问题:

%d是打印有符号的整型:
-1是一个整型,放在字符型变量中需要截断:
原码:1000 0000 0000 0000 0000 0000 0000 0001
补码:1111 1111 1111 1111 1111 1111 1111 1110
反码:1111 1111 1111 1111 1111 1111 1111 1111
所以,a中放的是:1111 1111

当对a打印时,需要整型提升(无符号和正数:高位全补0;负数:高位全补1),
则整型提升之后:
1111 1111 1111 1111 1111 1111 1111 1111
由于补码和原码相互转换是相同的,取反加1,得到原码
原码:1000 0000 0000 0000 0000 0000 0000 0001
则打印出来的就是-1,同理b也是如此

对于c而言:
-1是一个整型,放在字符型变量中需要截断:
原码:1000 0000 0000 0000 0000 0000 0000 0001
补码:1111 1111 1111 1111 1111 1111 1111 1110
反码:1111 1111 1111 1111 1111 1111 1111 1111
所以,c中放的是:1111 1111

当对c打印时,需要整型提升(无符号和正数:高位全补0;负数:高位全补1),
则整型提升之后:
0000 0000 0000 0000 0000 0000  1111 1111
由于符号位为0,是正数
原码:0000 0000 0000 0000 0000 0000  1111 1111
则打印出来的就是255

3.6输入和输出

C语言本身并不提供上输入、输出语句,只能调用标准函数库中提供的printf函数和scanf函数等库函数等

3.6.1 printf输出函数

一般形式:printf("格式控制字符串“,输出列表);

格式控制字符串=非格式字符串+格式字符串(%格式类型符)

  • 格式字符串的一般形式:%[标志字符][m][.][n]<类型格式符>

在这里插入图片描述

(1)标志字符

  1. -:输出结果左对齐,右边填空格;如果省略,则左对齐
  2. +:输出正、负号
  3. #:%#o输出前缀0(无符号八进制);%#x输出前缀0x(无符号十六进制) ;%#e/g/f输出小数点(无符号十六进制)
  4. 空格:如果输出值为正,则冠以空格;如果输出值为负则冠以负号

(2)最小宽度格式符m:表示输出的宽度(含小数点)。如果实际宽度>m,则按实际宽度输出;反之,则补空格或0
(3)精度格式符n:只对f(以小数形式输出)和s(字符串)有效。
(4)长度格式符:h表示按短整型输出;l表示按长整型输出
(5)类型格式符如下图所示:

在这里插入图片描述

注:对于字符串而言,若精度n小于实际位数,则保留的位数应从左向右数
例:printf(“s=%-5.2s”,hello);
结果:s=he

3.6.2 输入函数scanf

一般形式:scanf(“格式控制字符串”,地址列表);

格式控制字符串=非格式字符串+格式字符串
格式字符串:%[标志字符][宽度][长度]<类型格式符>
在这里插入图片描述在这里插入图片描述

(1)星号(*)表示输入项读入后不赋值给对应的变量,即跳过该输入值
(2)宽度:表示用十进制整数指定输入的宽度
(3)长度:l表长,h表短
在这里插入图片描述
注:

  1. scanf()没有精度控制
  2. 如果格式控制串中没有非格式字符作为数据间的间隔,输入数据时可以用空格、Tab或回车符作间隔;反之,存在非格式符,则输入时也要输入同样的格式。

地址列表:由地址运算符(&)加变量名组成
例:

int a=0,b=0;
scanf("%d %*d %2d",&a,&b);

输入:100 1000 10000
输出:a=100,b=10

3.6.3 字符输出与输入函数

getchar():从键盘上输入一个函数

int getchar( void );

putchar():在显示器上输出单个字符

int putchar( int c );

注:scanf() 与 getchar()作为输入函数,并不是直接从键盘上获取,而是在输入缓冲区中接收输入的字符,通常在键盘上输入数据后,需要加上回车,才可以将数据送入输入缓冲区
scanf()以空格作为结束符,对于换行符(仅当scanf(“%c”,&ch),才可以输入成功,其他形式无法输入换行符

清空缓冲区(防止后续输入语句接收错误):

int ch = 0;
while((ch = getchar()) != '\n')
{

}

4.分支与循环语句

C语言的语句可分为五大类:
(1)表达式语句:表达式;
(2)函数调用语句:函数名(参数表);
(3)控制语句:用于控制程序的执行流程
(4)复合语句::把多个语句用大括号{ }括起来的语句
(5)空语句:只有分号组成的语句

控制语句用于控制程序的执行流程,以实现程序的各种结构方式(C语言支持三种结构:顺序结构、选择结构、循环结构),它们由特定的语句定义符组成,C语言有九种控制语句
可分成以下三类:

  1. 条件判断语句也叫分支语句:if语句、switch语句;
  2. 循环语句:do while语句、while语句、for语句;
  3. 转向语句:break语句、goto语句、continue语句、return语句。

4.1分支语句

4.1.1 if语句

  1. if 形式
一般形式:
if(表达式)
    语句;
  • 执行过程:求表达式,若非0,则执行语句;否则,跳过该语句,执行后面的语句。

2.if…else形式

 一般形式:
 if(表达式)
     语句1;
 else
     语句2;
  • 执行过程:求表达式,若非0,则执行语句1;否则,执行语句2。

3.if…else if…else

一般形式:
if(表达式1)
    语句1;
else if(表达式2)
    语句2;
else if(表达式3)
    语句3;
…
else if(表达式n-1)
    语句n-1;
[else
    语句n;]
注:else子句有时候可能没有
  • 执行过程:求表达式1,若非0,则执行语句1;否则,求表达式2,若非0,则执行语句2;否则继续向下执行。如果前n-1个表达式均为假,则执行语句n
    4.if语句的嵌套
一般形式:
if (表达式1)
	if(表达式2)
		语句1;
	else
		语句2;
else
	if(表达式3)
		语句3;
	else
		语句4;
  • 执行过程:求表达式1,若非0,则执行if子句内嵌if语句;否则,执行else子句内嵌if语句。

其中,要注意以下方面:
(1)else 与离其最近的 if 匹配,且if与else默认只能控制一条语句,若中间若含多条语句,应使用{ },否则编译器会报错。

①当if中的条件为真,进入嵌套if语句中,此时条件为假,进入与其匹配的else中,执行语句

在这里插入图片描述
②当if中的条件为假,由于没有与之匹配的else,便结束该if语句

在这里插入图片描述
(2)当遇到将某一变量与某一常数比较时,常常将常数放于变量之前

在这里插入图片描述

虽然上面两个运行结果相同,但第代码2与所想要表达的思想并不相同,
想要判断变量a是否与1相等,结果很多初学者易将其写为a=1(将1赋值给a)。
此时,编译器并不会报错,但可能会给后期带来不必要的麻烦。
所以,代码1更好,逻辑更加清晰,不会出错

(3)对于子句而言,都是到第一个分号处结束;
if与else默认只能控制一条语句,当子句不止有一条,需要用的大括号({ })将其括起来构成复合语句

4.1.2 switch语句

switch语句也是分支语句的一种,常用于多分支情况

一般形式:
switch(表达式)
{
	case 常量表达式1:语句1;
	case 常量表达式2:语句2;
	……
	case 常量表达式n:语句n;
	[default:语句n+1;]
}
  • 执行过程:
    (1)求解表达式的值
    (2)将表达式与各常量表达式的值依次比较
    (3)如果遇到的值与表达式的值相等,则执行该常量表达式后面的语句,以及其余常量表达式后面的语句,包括default后面的语句,并且不再进行比较
    (4)如果比较,没有与常量表达式的值相等,则执行default后面的语句

其中,要注意以下方面:
(1)表达式与常量表达式只能为整型或字符型

(2)各常量表达式的值不能相同

(3)case后面的语句可以有多条,且不需要加花括号{ }

(4)在一个switch语句中最多只能出现一条default子句

(5)使用break语句可中断所在switch语句的执行,同时多个case语句可以公用一组操作语句

在这里插入图片描述

4.2循环语句

循环语句包括while语句,do…while语句,for语句。
其共性包含“循环条件”:用于判断循环是否往下进行;“使循环趋向于结束的语句”:用于逐渐使循环条件不满足。
若没有循环条件,则无法构成循环;没有是循环趋于结束的语句则造成死循环

4.2.1 while循环

一般形式:
while(表达式)
{
     语句;
}
  • 执行步骤:
  1. 求解表达式,若非0,则执行循环体;否则跳出循环
  2. 转到转到步骤1
  3. 一直循环往复,直至表达式求解为0,跳出循环
  • 执行特点:先判断表达式的值,后执行循环语句。

4.2.2 do…while循环

一般形式:
do
{
	语句;
}
while(表达式);
  • 执行步骤:
  1. 执行循环体
  2. 求解表达式,若非0,则继续执行循环体;否则跳出循环
  3. 转到转到步骤2
  4. 一直循环往复,直至表达式求解为0,跳出循环
  • 执行特点:先执行循环语句,后判断表达式的值。

4.2.3 for循环

一般形式:
for(表达式1;表达式2;表达式3)
   语句;
   
注:表达式1:循环变量初始化、表达式2:循环条件,表达式3:循环变量调整
  • 执行步骤:
  1. 求解表达式1
  2. 求解表达式2。若非0,则执行循环体;否则跳出循环
  3. 求解表达式3,转到步骤2
  4. 一直循环往复,直至表达式2求解为0,跳出循环
  • 执行特点:
  1. 表达式1仅执行一次
  2. 表达式2至少执行一次
  3. 一般情况下,循环体与表达式3执行的次数相同,但比表达式2执行的次数少一次

特殊情况:

(1)表达式1、表达式2、表达式3均可以省略。循环变量初始化要在循环前体现,循环变量调整需要在循环体中体现,循环条件(表达式2)省略默认为真

判断部分省略:默认条件为真,恒成立
死循环:

for(;;)
{

}

(2)表达式1可以是实现循环变量初始化以外的表达式
for(s=1,i=1;i<=2;i++)
(3)如果同时省略表达式1和表达式3,则for循环=while循环

4.3break语句

一般形式:break;
作用:结束switch语句或循环语句,且不在执行

  • 1.break语句只能用于switch语句或while语句、do…while语句、for语句
  • 2.break语句只能中断最近一层的循环语句或switch语句

4.4continue语句

一般形式:continue;
作用:结束本次循环语句,不在执行continue后面的语句,直接进行下一次是否循环的判断

  • 1.continue语句只能用于while语句、do…while语句、for语句
  • 2.continue语句只能j结束最近一层的循环语句

4.5goto语句

一般形式:

语句标号:	语句;
		    goto 语句标号:

执行过程:语句标号为标识符,当执行goto语句,将无条件转到语句标号标识的位置。
作用:中止程序的深度循环嵌套

5.函数

5.1函数概述

  • 在C语言中,函数是程序组成的基本单位
  • 函数是整个程序中的子程序,由一条或多条语句组成,可完成某中特定功能,并且所有函数相对独立
  • 分类:
    (1)库函数
    (2)自定义函数

5.2函数的定义

一般形式:
函数返回值类型  函数名(类型标识符  形参,类型标识符  形参......)
{
		函数体;
}

设计函数追求:高内聚、低耦合

  1. 参数:
  • 形参:函数定义时函数名后面括号中的参数
  • 实参:在主函数中,调用该函数时函数名后面括号中的参数

(1)函数调用时,形参分配内存单元,实参将值赋给形参,为单向值传递

①传值:将变量的由实参传向给形参,形参是对实参的一份临时拷贝单向传递,不能够改变实参
②传址:将变量的地址由实参传向给形参,可双向传递,能够改变实参

(2)实参可以是具有值的常量、变量、表达式或函数,形参只能时变量
(3)实参与形参的类型应相同或兼容
(4)形参当调用函数完成后自动销毁,只在函数中有效
2. 函数的返回值:

一般形式: return (表达式);
(1)一个函数只能返回一个值。一个函数中可以有多条return语句,但执行一个return语句后,该函数就执行结束。
(2)函数返回值的类型应与定义时的类型一致,若不一致,则以定义类型为准

注:
(1)所有函数之间是平行关系,不能嵌套定义
(2)函数之间可以嵌套调用,但main函数不能被其他函数调用

5.3函数的声明和调用

5.3.1函数的声明

在调用函数之前:先声明后使用

一般形式:
函数返回值类型 函数名(类型标识符 形参,类型标识符 形参…)

(1)函数声明可以省略参数
(2)在同一源文件(.c)中,被调函数定义在main函数之前,可以不声明

头文件格式:

#ifndef __TEST_H__  //头文件的名字
#define __TEST_H__

int Add(int x, int y);//函数的声明

#endif 
  • 一般情况下:
  • 将函数定义放在新的源文件中(.c),函数声明放在新的源文件中(.h),在含有main函数的源文件中引用头文件 #include “头文件名.h”

5.3.2 函数的嵌套调用

嵌套调用:在一个函数的定义中出现对另一个函数的调用

void test(void)
{
	printf("H\n");
}

void Fun_A(void)
{
	test();
}

5.3.3函数的链式访问

所谓链式访问,使用一个函数的返回值作为另外一个函数的参数

int main()
{
	 printf("%d", printf("%d", printf("%d", 43)));
	return 0;
}

要想得到上述程序结果,首先要知道printf( )函数的返回值以及返回值类型
在这里插入图片描述

在这里插入图片描述
printf( )返回类型为int,返回值为打印字符个数
因此,上述程序结果:4321

5.4函数递归

递归:一个函数直接或间接地调用函数本身,可分为两部:递推 + 回归

  • 存在限制条件,当满足这个限制条件的时候,递归中止
  • 每次递归调用之后越来越接近这个限制条件

它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,把大事化小

例如:按照从高位到低位打印1234
原问题相似的规模较小的问题:很容易打印个位数字(取余)
递推:将数据除以10,经过有限次,即满足限制条件,回推结束
回归对参数取余,即可打印个位数据,则打印完成后,返回上一次函数调用,继续打印(最后的数字先打印

即:先得到1,之后开始打印

流程:
(1)print(123) + 4
(2)print(12) + 3 + 4
(3)print(1) + 2 + 3 + 4
(4)1 + 2 + 3 + 4
由于调用自身,并不会先打印4,而是先进入函数。直到函数传入的参数小于10,则递归终止,开始回推(最后的数字先打印

#include <stdio.h>

/*1234
(1)print(123) + 4 
(2)print(12) + 3 + 4
(3)print(1) + 2 + 3 + 4
(4)1 + 2 + 3 + 4
*/
void print(int x)
{
    if(x > 9)
        print(x/10);
    printf("%d ",x%10);
}


int main()
{
    print(1234);
    return 0;
}

6.数组

6.1一维数组

6.1.1一维数组的定义

一般形式:
		 数据类型   数组名   [整型常量表达式]

例如:
int a[100]; 表示定义一个整形数组,数组名为a,包含100个元素

(1)在内存中,数组各元素占据一段连续的内存
(2)方括号[ ]:定义的对象时数组,不可以省略

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

6.1.2一维数组的初始化

  1. 完全初始化
    int a[5] = {0,1,2,3,4};
    char a[5] = {‘a’,‘b’,‘c’,‘d’,‘e’};
  2. 部分初始化
    int a[5] = {0,1,2};
    数组a前3个元素的值依次为0,1,2;其余元素的值默认为0
    char a[5] = {‘a’,‘b’,‘c’};
    char b[10] = “Hello”;
    对于字符数组而言,不完全初始化,其余元素的值默认为\0
  3. 初始化给出所有元素值,方括号中的 [整型常量表达式] 可省略
    int a[ ] = {0,1,2,3,4};
    char a[ ] = {‘a’,‘b’,‘c’,‘d’,‘e’};
    char b[ ] = “Hello”;

注:字符数组a中不含有\0

6.1.3一维数组的引用

使用下标引用操作符:[]
一般形式: 数组名 [下标]
  • 数组元素的下标从0开始,第n个元素的下标为n-1
  • 数组元素的个数可以通过计算得到
    int a[10]={0};
    int length = sizeof(a) / sizeof(a[0]);
    元素个数 = 整个数组大小 / 一个元素的大小

6.2二维数组

6.2.1二维数组的定义

一般形式:
		 数据类型   数组名   [整型常量表达式] [整型常量表达式]

例如:
int a[3] [4]; 表示定义一个整形数组,数组名为a,包含3行、4列共12个元素

(1)在内存中,数组各元素占据一段连续的内存,先存放第一行元素,接着存放第二行元素,以此类推
(2)二维数组可以看成特殊的一维数组a[3] [4]包含三个元素a[0]、a[1]、a[2],a是数组名;3个元素都是一维数组,a[1]中含有a[1][0]、a[1][1]、a[1][2]、a[1][3]四个元素,a[1]是数组名

6.1.2一维数组的初始化

  1. 整体初始化
    int a[2][2] = {1,2,3,4};
  2. 分行初始化
    int a[2][2] = {{1,2},{3,4}};
  3. 部分初始化
    int a[2][2] = {{1},{3}};
    整型数组部分元素初始化;其余元素的值默认为0
    字符数组而言,不完全初始化,其余元素的值默认为\0
  4. 初始化给出所有元素值,行数可省略,但列数不可以省略
    int a[ ][2] = {1,2,3,4};

6.2.3二维数组的引用

使用下标引用操作符:[ ] [ ]
一般形式: 数组名 [下标] [下标]
  • 数组元素的下标从0开始,第n个元素的下标为n-1
  • 数组的行和列可以通过计算得到
    int a[3][4]={0};
    int row = sizeof(a) / sizeof(a[0])
    行数 = 整个数组的大小 / 某一行的大小
    int column = sizeof(a[0]) / sizeof(a[0][0]);
    列数 = 一行的大小 / 一个元素的大小

6.3字符数组

6.3.1字符数组的定义以及初始化

一般形式:
		 char  数组名 [整型常量表达式];

使用字符常量进行初始化:

  1. 完全初始化
    char a[5] = {‘a’,‘b’,‘c’,‘d’,‘e’};
    [ ] 中的整型常量表达式可以省略
  2. 不完全初始化化
    char a[5] = {‘a’,‘b’,‘c’};
    其余元素默认为’\0’

使用字符串进行初始化:

  1. char a[5] = “abcd";
    要注意字符串结束标志’\0’,字符串与数组长度匹配的问题

6.3.2字符串处理函数

6.3.2.1字符串输入函数gets( )

char *gets( char *buffer );
返回值:如果成功,会返回其参数。NULL指针表示错误或文件结束条件。

使用一般形式:
gets(地址);
  • gets()根据地址将字符串中的字符依次存入内存单元,并在最后一个字符后面自动添加一个’\0’

说明:

  1. gets()以回车符作为输入结束标志
    scanf()以空格作为输入结束标志
6.3.2.2字符串输出函数puts( )

int puts( const char *string );
如果成功,返回一个非负值。如果失败,则返回EOF

使用一般形式:
puts(地址);
  • puts()根据地址将字符串中的字符依次输出,当遇到’\0’,将其作为回车输出

说明:

  1. puts()参数可以是字符数组,也可以是字符串常量
  2. puts()输出的字符串可以包含转义字符
6.3.2.3字符串长度检测函数strlen( )

size_t strlen( const char *string );
函数返回字符串中的字符个数(无符号数),不包括‘\0’

使用一般形式:
strlen(地址);
  • strlen()根据地址依次输向后查找,当遇到’\0’便结束,返回’\0’(不包含’\0’)之前的字符个数

说明:

  1. strlen()返回值类型是无符号整型,如果
    strlen(“abc”) - strlen(“abcdef”) > 0
    其结果必定为正数,这里就出现错误,避免这样使用
sizeof与strlen()的区别

相同点:返回值均为size_t,即unsigned int类型
不同点:
(1)sizeof为运算符
参数:数据类型、变量、数组名(整个数组所占的空间大小)、结构体、联合体、指针(指针变量的本身大小)
功能:计算的是某数据类型或某数据类型创建变量时所占空间大小但不计算括号里面的表达式,单位:字节
适用对象:sizeof适用于任何已知类型和变量,包括编译时已知和运行时已知的对象
计算时刻:sizeof是在编译时进行计算的,不会计算动态分配的内存
在这里插入图片描述
(2)strlen()函数
需要包含头文件:<string.h>
参数类型:char* 的指针
功能:用于计算从起始i地址到’\0’(ASCII码值为0),之间的字符个数不包括’\0’
适用对象:仅适用于以NULL字符结尾的C字符串(字符数组),并且要求字符串的地址已知
计算时刻:strlen是在运行时计算的,它通过逐个检查字符,直到遇到NULL字符来确定字符串的长度,因此它适用于动态分配的字符串

6.3.2.4字符串复制函数strcpy( )和strncpy( )

char *strcpy( char *strDestination, const char *strSource );
函数返回目标字符串的起始地址,无返回值则表示错误

使用一般形式:
strcpy(目标字符串的起始地址,源字符串的起始地址);
  • strcpy()根据源字符串的起始地址,开始复制给目标字符串,当遇到第一个’\0’时,将其复制给目标字符串中便结束

说明:

  1. strcpy()源字符串必须以 ‘\0’ 结束
  2. 源字符串中的’\0’会复制到目标字符串中
  3. 目标字符串不能是字符常量,因为常量不可修改
  4. 目标空间必须足够大

char *strncpy( char *strDest, const char *strSource, size_t count );
函数返回目标字符串的起始地址,无返回值则表示错误

使用一般形式:
strncpy(目标字符串的起始地址,源字符串的起始地址,拷贝字符的个数);
  • strncpy()根据源字符串的起始地址,开始复制给目标字符串,当复制count个字符便结束
    说明:
  1. 如果源字符串的长度小于count,则拷贝完源字符串之后,在目标的后边追加’\0’,直到count个
  2. 目标字符串不能是字符常量,因为常量不可修改
  3. 目标空间必须足够大
6.3.2.5字符串连接函数strcat( )和strncat()

char *strcat( char *strDestination, const char *strSource );
函数返回目标字符串的起始地址,无返回值则表示错误

使用一般形式:
strcat(目标字符串的起始地址,源字符串的起始地址);
  • 将源字符串连接到目标字符串后面,连接结果存放在目标字符串中

说明:

  1. strcat()源字符串必须以 ‘\0’ 结束
  2. 连接过程中目标字符串的’\0’会被源字符串覆盖,连接完成后在目标字符串中自动添加一个’\0’
  3. 目标字符串不能是字符常量,因为常量不可修改
  4. 目标空间必须足够大

char *strncat( char *strDest, const char *strSource, size_t count );
函数返回目标字符串的起始地址,无返回值则表示错误

使用一般形式:
strcat(目标字符串的起始地址,源字符串的起始地址,连接字符个数);
  • 将源字符串连接到目标字符串后面,连接结果存放在目标字符串中

说明:

  1. 如果源字符串的长度小于count,则追加实际长度,并在目标字符串中自动添加一个’\0’
  2. 连接过程中目标字符串的’\0’会被源字符串覆盖,连接完成后在目标字符串中自动添加一个’\0’
  3. 目标字符串不能是字符常量,因为常量不可修改
  4. 目标空间必须足够大
6.3.2.6字符串比较函数strcmp( )和strncmp()

int strcmp( const char *string1, const char *string2 );
如果两个字符串全部字符都相同,则两字符串相等,返回值为0
如果两字符串中有不同字符,以第一个不相同字符的比较结果为准;如果如果字符串1中该位置上字符的ASCII码值较大,则认为字符串1大,返回值为大于0(一般值为1);反之,则认为字符串2大,返回值为小于0(一般值为-1)

使用一般形式:
strcmp(字符串1,字符串2);
  • 从左至右依次比较,当出现不同字符或遇到’\0’时停止

int strncmp( const char *string1, const char *string2, size_t count );
如果两个字符串全部字符都相同,则两字符串相等,返回值为0
如果两字符串中有不同字符,以第一个不相同字符的比较结果为准;如果如果字符串1中该位置上字符的ASCII码值较大,则认为字符串1大,返回值为大于0(一般值为1);反之,则认为字符串2大,返回值为小于0(一般值为-1)

使用一般形式:
strcmp(字符串1,字符串2,比较字符个数);
  • 从左至右依次比较,当出现不同字符或遇到’\0’时停止或前count个字符比较完
6.3.2.7字符串寻找子串函数strstr( )

char *strtok( char *strToken, const char *strDelimit );
如果找到字串,函数返回一个指向字串在字符串中第一次出现的指针,如果没有找到子串,则返回NULL。如果子串为一个长度为零的字符串,则返回string。

使用一般形式:
strstr(字符串,子串);
  • 在字符串中从左向右寻找子串
6.3.2.8字符串分割串函数strtok( )

char *strstr( const char *string, const char *strCharSet );
strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。

使用一般形式:
strstr(字符串地址,分隔符集合的地址);
  • sep参数是个字符串,定义了用作分隔符的字符集合
    第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
    strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
    strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
    如果字符串中不存在更多的标记,则返回 NULL 指针。
#include <string.h>
#include <stdio.h>

int main() 
{
	char arr[ ] = "abchh@kjlll@jjjj.jslij";
	char s[] = "@.";
	char* ret;
	for(ret = strtok(arr,s);ret != NULL;ret = strtok(NULL,s))
	{
		printf("%s\n",ret);
	}
    return 0;
}

在这里插入图片描述

6.3.2.9字符串分割串函数strerror( )

char *strerror( int errnum );
strerror()函数返回指向错误信息字符串的指针

使用一般形式:
#include <errno.h>
strerror(errno);

errno是C语言设置的全局变量,用来存放错误码

6.3.2.10字符串大小写变换函数strlwr( )和strupr( )

char *_strlwr( char *string );
strlwr()函数返回一个指向转换后的字符串的指针

使用一般形式:
strlwr(字符串地址);
  • 将字符串中的大写字母变换为小写字母

char *_strupr( char *string );
strupr()函数返回一个指向转换后的字符串的指针

使用一般形式:
strupr(字符串地址);
  • 将字符串中的小写字母变换为大写字母
6.3.2.11内存拷贝函数memcpy( )

void *memcpy( void *dest, const void *src, size_t count );
memcpy()函数返回目标空间的起始地址
count:拷贝的字节数

使用一般形式:
memcpy(目标空间地址,源空间地址,拷贝字节数);
  • 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置

不是用来进行所占内存空间重叠的数据拷贝

6.3.2.12内存拷贝函数memmove( )

void *memmove( void *dest, const void *src, size_t count );
memmove()函数返回目标空间的起始地址
count:拷贝的字节数

使用一般形式:
memmove(目标空间地址,源空间地址,拷贝字节数);
  • 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置

适用于所占内存空间重叠的数据拷贝

6.3.2.13内存比较函数memcmp( )

int memcmp( const void *buf1, const void *buf2, size_t count );
如果两个内存空间中的前count个字节都相同,则相等,返回值为0
如果两个内存空间中的前count个字节出现不同,以第一个不相同字节的比较结果为准;如果如果buf1中该位置上字节中的值较大,则认为buf1大,返回值为大于0(一般值为1);反之,则认为buf2大,返回值为小于0(一般值为-1)
count:比较的字节数

使用一般形式:
memcpy(目标空间地址,源空间地址,比较字节数);
6.3.2.14内存设置函数memset( )

void *memset( void *dest, int c, size_t count );
memset()函数返回目标空间的起始地址
count:设置的字节数

使用一般形式:
memset(目标空间地址,设置的值,设置字节数);

常常用来初始化,但是以字节为单位进行操作

6.4 数组作为参数

6.4.1一维数组传参

作为实参数组名:表示数组首元素的地址,如需要数组元素个数,一定要在函数外部实现)

作为形参

  1. int arr[元素个数]
  2. int arr[ ]
  3. int* arr
  • 数组名[ ](本质是一个指针),也可以是同类型的指针变量
  • 当数组作为参数时,其为指针,如果需要计算数组元素个数必须在传参之前进行,否则计算的是指针变量的大小
int arr_sum(int a[5],int length)
//int arr_sum(int* a,int length)
{
	a[1]等价于*(a+1)
	可以使用a[i]对数组元素调用
}

int arr_sum(int a[1],int length)
{
	可以使用a[i]对数组元素调用
}

int arr_sum(int* a,int length)
{
	*(a+1)等价于a[1]
}

int main(void)
{
	int a[5] = {1,2,3,4,5};
	arr_sum(a,length);  //求数组元素之和
}

补充:
数组名是首元素的地址,但以下两个例子除外

  1. sizeof(数组名)计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组;如果sizeof(数组名+整数),那此时数组名就表示首元素的地址
  2. &数组名取出的是整个数组的地址。&数组名,数组名表示整个数组
    在这里插入图片描述

(1)数组名+1:第二个元素的地址
(2)&数组名+1:表示数组尾元素地址加1

同样,也可以这样理解:

int arr[5] = {1,2,3,4,5};
int i = 0;
int (*p1)[5] = &arr;
int* p2 = &i;
对于指针变量p1而言,其类型为  int*
对于指针变量p2而言,其类型为  int(*)[5]
p1+1:跳过4个字节(一个整型 int)
p2+1:跳过20个字节(一个整型数组 int[5])

6.4.2二维数组传参

作为实参数组名:表示数组首行元素的地址
作为形参

  1. int arr[行数][列数]
  2. int arr[ ][列数]
  3. int (*p)[5]

int (*p)[5]:数组指针,指向一维数组,其长度为5,为整形的数组

int arr_sum(int a[2][3],int row,int col)
//int arr_sum(int* a,int length)
{
	可以使用a[i][j]对数组元素调用
}

int arr_sum(int a[ ][3],int row,int col)
{
	可以使用a[i][j]对数组元素调用
}

int arr_sum(int (*p)[5],int row,int col)
{
	*(*(p+i)+j)对i行j列元素访问
	和p[i][j]等价
}

int main(void)
{
	int a[2][3] = {1,2,3,4,5,6};
	arr_sum(a,row,col);  //求数组元素之和
}

补充:
数组名是首行元素的地址,但以下两个例子除外

  1. sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组;如果sizeof(数组名+整数),那此时数组名就表示首元素的地址
  2. &数组名,取出的是整个数组的地址。&数组名,数组名表示整个数组

7.指针

7.1指针和指针变量

在计算机中,把一个字节大小的存储空间称为一个内存单元
一个内存单元的地址就是指针指针变量是用来存储内存单元的地址

  • 一个指针是一个地址,是常量;一个指针变量可以被赋予不同的指针值,是变量,口语中常讲的指针为指针变量
  • 指针变量的大小取决于地址的大小,在32位平台是4个字节,在64位平台是8个字节

7.2指针变量的定义

一般形式:类型说明符 *   变量名

说明:

  • 类型说明符:本指针变量所指向的变量的数据类型

  • *:表示是一个指针变量

  • 变量名:指针变量名

  • 指针变量的类型:类型说明符 *

指针指向变量的类型的作用·:

  • (1)决定了引用时访问的字节数
  • (2)决定了+1、-1跳过的字节数

7.2.1指针变量的赋值

使用指针变量之前,不仅要定义,还要赋值。未经赋值的指针变量不能使用
(1)初始化

int a = 0;
int* p = &a;

(2)赋值语句

int a = 0;
int* p ;
p = &a;

如果指针指向的位置是不可知的,则会出现野指针

  1. 指针未初始化
int* p;
*p = 20;
  1. 指针访问越界
int arr[10] = {0};
int* p = arr;
int i = 0;
for(i = 0;i <= 10;i++)
{
	*p=1;
	p++;   //数组只有10个元素,但访问了11个内存单元
}
  1. 指针指向的空间已经被释放
int* test()
{
	int a = 5;
	return &a;
}

int main()
{
	int* p = NULL;
	p = test();test()调用完成后,其局部变量a所占空间便会还给操作系统,此时属于非法访问
}

7.2.2指针变量的引用

一般形式:* 指针变量
意义:通过指针变量中存放的地址,找到p所指向的对象

指针±整数:指向前、后的内存空间
指针-指针(同类型的指针,也就是地址):两指针相距元素的个数

7.3二级指针

一般形式:
数据类型** 指针变量名;

例如:int** a;
int** a相当于*(*p),括号里面的*表示a为指针变量,int*表示指针变量a指向一个整型指针变量

二级指针是用来存放一级指针变量的地址

7.4指针与数组

首先,要先搞明白指针数组和数组指针的概念

int* p1[5];
int (*p2)[5];
  • int* p1[5]:指针数组,数组中的各元素都是指针
  • int (*p2)[5]:数组指针,与字符指针含义类似,指向一维数组,该一维数组的长度为5

数组指针:用来存放数组的地址,在6.3中了解到,&数组名表示取出的是整个数组的地址

int arr[5] = {0};
int (*p)[5] = &arr;

&arr是一个地址,用指针变量*p存放,其指向整个一维数组,长度为5,其元素类型为int

 [ ]里面元素的个数不能省略
  • 对于数组指针的引用:
    p是指向整个数组,*p就找到整个数组,就相当于数组名arr,而数组名又是数组首元素的地址,所以*p本质上是数组首元素的地址,*p+i可以找到各个元素的地址,*(*p+i)(相当于p[0][i])可以访问到数组中的各个元素

7.4.1指向一维数组元素的指针变量

int arr[10] = {0};
int* p = NULL;
p = arr;

一维数组的数组名表示数组的首地址,即 arr = &arr[0]

(1)p + i、arr + i 就是arr [i]的地址
(2)* (p + i)、* (arr + i) 就是arr [i],*(*(arr + i)+j) = arr[i] [j]
(3)指针变量p的值可以改变,但数组名不可以改变,它是数组的首地址,是地址常量

7.4.2指向二维数组元素的指针变量

a [2][3] ={{1,2,3},{4,5,6}} 

二维数组可以看成一维数组的数组:
数组a可以理解由2个元素组成:a[0]、a[1],
而每个元素又是一个一维数组,且含有3个元素:
a[0]的元素:a[0][0]、a[0][1]、a[0][2],数组名a[0]是这一数组的首地址
a[1]的元素:a[1][0]、a[1][1]、a[1][2],数组名a[1]是这一数组的首地址

数组名a是数组{a[0]、a[1]、a[2]}的首地址,a+1表示第二行的首地址
a[0]是第一行数组的首地址,a[0]+1表示第一行第二列元素的地址

int a[2][3] , (*p)[3];
pi = a;
int (*p)[3]:说明指针变量p指向“包含3个整型元素的一维数组”

数组名a和指针变量表达意义相同
因此,指针变量p为行指针,也称指向一维数组的指针

7.4.3指针数组

一般形式:
数据类型* 数组名[数组长度]
作用:用来存储指向相同数据类型的指针

利用指针数组模仿二维数组:

int arr1[] = {1,2,3};
int arr2[] = {4,5,6};
int arr3[] = {7,8,9};

int* arr[3] = {arr1,arr2,arr3};
7.4.3.1指针数组传参

作为实参:数组名:表示数组首元素的地址,如需要数组元素,必须在传参之前进行==,否则计算的是指针变量的大小
作为形参

  1. int* arr[元素个数]
  2. int* arr[ ]
  3. int** arr

指针数组arr,表示首元素的地址,而每个元素又是一个指针。也就是说,数组名arr是指针的地址,那么就可以使用二级指针去接收

int test(int* a[5])
//int arr_sum(int* a,int length)
{
	可以使用a[i]对数组元素调用
}

int test(int* a[ ])
{
	可以使用a[i]对数组元素调用
}

int test(int** a)
{
	
}

int main()
{
	int a[5] = {1,2,3,4,5};
	test(a);  //求数组元素之和
}

7.5指针和字符串

毫无疑问,字符指针 char*可以存放字符变量的地址

对于字符串而言,不仅可以通过字符数组访问,还可以通过字符指针访问
一般形式:
char* 指针变量名 = 字符串常量;

char* str = "I am boy!";
printf("%s\n",str);
  • 上述代码,虽然没有定义字符数组,但还是在内存中开辟了一个连续区域存放字符串常量,将字符串的首字符的地址(I 的地址)赋给指针变量
  • 由于字符指针指向字符串常量,为了避免通过指针修改字符串,用const修饰字符指针

const修饰指针变量的时候

  1. const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。
  2. const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

eg.

int main()
{
	 char str1[] = "abcde";
	 char str2[] = "abcde";
	 const char *str3 = "abcde";
	 const char *str4 = "abcde";
	 if(str1 ==str2)
	 	printf("str1 == str2\n");
	 else
	 	printf("str1 != str2\n");
	 
	 if(str3 ==str4)
	 	printf("str3 == str4\n");
	 else
	 	printf("str3 != str4\n");
	 
	 return 0; 
	 }

在这里插入图片描述

  1. 相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块
  2. 由于常量字符串存储到单独的一个内存区域,当多个指针指向同一个字符串的时候,实际会指向同一块内存

7.6指针和函数

7.6.1函数指针

一个函数总是占用一段连续的内存区,而函数名就是该函数的入口地址或首地址

函数指针:指向函数的指针变量

一般形式:
类型说明符  (*指针变量名)(形参类型)

例:int  (*pf) (int,int);

其中,pf是指向函数入口的指针变量,类型为int (*) (int,int)
可以看出,除函数名用(*指针变量名)代替外,其他定义形式均与函数的原型相同

typedef int  (*pf) (int);int (*)(int)类型重命名为pf
7.6.1.1用函数指针变量调用函数
一般形式:
(*指针变量名)  (实参) ;
int add(int x,int y)
{
	return (x+y);
}

int maain()
{
	int a = 5,b = 2;sum = 0;
	int (*p)(int,int) = add;
	//int (*p)(int,int) = &add;
	sum = (*p)(a,b);
	return 0;
}

说明:

  1. 函数指针不进行算术运算,其运算毫无意义
  2. 前文说到,函数名就是函数的入口地址,而函数指针变量存放的也是函数的入口地址,所以sum = (*p)(a,b)与sum = p(a,b)等价
7.6.1.2函数指针变量作为函数参数
int add(int x,int y)
{
	return (x+y);
}
int sub(int x,int y)
{
	return (x-y);
}
int mul(int x,int y)
{
	return (x*y);
}
int div(int x,int y)
{
	return (x/y);
}
int computer(int (*p)(int,int),int x,int y)
{
	int k;
	k = (*p)(x,y);
	return k;
}

int maain()
{
	int a = 5,b = 2,sum = 0;
	int result = 0;
	result = computer(add,a,b);
	result = computer(sub,a,b);
	result = computer(mul,a,b);
	result = computer(div,a,b);
	return 0;
}

7.6.2函数指针数组

函数指针数组与指针数组类似,其里面存放的是函数指针

int (*arr[4])(int,int) = {add,sub,mul,div};

理解:arr先和 [] 结合,说明arr是数组,是 int (*)(int,int) 类型的函数指针
示例:

int add(int x,int y)
{
	return (x+y);
}
int sub(int x,int y)
{
	return (x-y);
}
int mul(int x,int y)
{
	return (x*y);
}
int div(int x,int y)
{
	return (x/y);
}

int maain()
{
	int a = 5,b = 2,input = 0;
	int result = 0;
	int (*arr[5])(int,int) = {0,add,sub,mul,div}:
	scanf("%d",&input);
	
	//利用数组下标与其功能相对应
	result = arr[input](a,b);
	return 0;
}

7.6.3指向函数指针数组的指针

int (*arr[5])(int,int) = {0,add,sub,mul,div}:

int (*(*p)[5])(int,int) = &arr;

理解:首先,p是一个指针,接着和[5]结合,说明指向一个一维数组,其长度为5,元素类型为int (*)(int,int)。
&arr取出整个数组的地址,放在指针变量p里面,指向长度为5,类型为int (*)(int,int)的函数指针数组

7.6.4指针函数

一般形式:
函数类型*  函数名(形参);

*表明该函数是一个指针函数,返回值是一个指针

由此可见,与前面所学习的函数定义没有根本区别,只是函数的返回值类型为指针

7.6.5回调函数

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

qsort( )快速排序函数,可实现不同类型数据的排序
数组按照比较函数定义的递增顺序排序。要按降序对数组进行排序,请颠倒比较函数中“大于”和“小于”的含义。

在这里插入图片描述
在这里插入图片描述

  • void* base:进行排序数据的起始位置
  • size_t num:需要排序的数据个数
  • size_t width:每个待排序数据所占的字节数
  • int (__cdecl *compare )(const void *elem1, const void *elem2 ):函数指针,是一个比较函数,不同类型数据的比较方法
    elem1:第一个比较元素的地址
    elem2:第一个比较元素的地址
    这两个指针类型为void*,void*是无具体类型的指针,可接收任意数据类型的地址,但不能解引用和+、-整数操作,若需要解引用时,需要强制类型转换
#include <stdio.h>
#include <stdlib.h>

int cmp_int(const void *e1, const void *e2)
{
	//比较函数定义的是递增顺序排序
	return (*(int*)e1 - *(int*)e2);

	//降序
	return (*(int*)e2 - *(int*)e1);
}

int main()
{
	int arr[] = {9,8,7,6,5,4,3,2,1};
	int length = sizeof(arr) / sizeof(arr[0]);
	qsort(arr,length,sizeof(arr[0]),cmp_int);
}

8.结构体

8.1结构体类型的声明(定义类型)

一般形式:
struct  结构体名
{
	成员列表
};

成员列表是由若干个成员组成,每个成员都有相应的数据类型,其数据类型可以不同

例如:
struct student
{
char name[20];
int age;
char sex[5];
};

注:

  1. 大括号后面的分号必不可少
  2. 声明仅仅是指明了数据类型的名称,是对其一种抽象的说明,编译时,对声明并不分配内存空间

8.2结构体变量

8.2.1结构体变量的定义

  • 先声明结构体,再定义结构体变量
一般形式:
struct  结构体名
{
	成员列表
};

struct  结构体名 变量名;
struct  student
{
	char name[20];
	int age;
	char sex[5];
};

struct student stu1,stu2;
  • 在声明结构体的同时定义结构体变量
一般形式:
struct  结构体名
{
	成员列表
} 变量名;

struct  student
{
	char name[20];
	int age;
	char sex[5];
}stu1,stu2;

  • 直接定义结构体变量
一般形式:
struct  
{
	成员列表
} 变量名;

与第二种方法相比,省去了结构体名

struct 
{
	char name[20];
	int age;
	char sex[5];
}stu1,stu2;

匿名结构体类型只能使用一次

说明:

  1. 结构体中的成员,也可以是一个结构体变量
  2. 结构体类型变量分配的存储空间是连续的
  3. 成员名可与程序中其他变量同名,互不干扰
  4. 结构体变量的成员作用和普通变量一样,可以进行各种运算

8.2.2结构体变量的初始化和成员的访问

初始化:

struct  student
{
   char name[20];
   int age;
   char sex[5];
}stu1 = {“Li Hua”,18,“male”};

成员的访问:

  1. 结构体变量 . 成员名
  2. (*结构体指针变量). 成员名
  3. 结构体指针变量->成员名

结构体的自引用

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

8.3结构体内存对齐

结构体的对齐规则:

  1. 第一个成员变量与结构体地址偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    对齐数 = 编译器默认的一个对齐数 与 该成员变量类型所占字节数的较小值
    VS中默认的值为8,其他编译器没有默认值
    Linux中没有默认对齐数,对齐数就是成员自身的大小
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
struct S1
{
	 char c1; 
	 char c2;
	 int i;
};
printf("%d\n", sizeof(struct S1));  //结果为8

在这里插入图片描述

8.4结构体传参

struct  student
{
   char name[20];
   int age;
   char sex[5];
}stu1 = {“Li Hua”,18,“male”};

test(stu1);

test(&stu1);
  • 传值传参:形参是对实参的一份临时拷贝,如果结构体过于复杂,参数压栈的的系统开销比较大,所以会导致性能的下降。
  • 传址传参:将结构体的地址传给形参,利用指针可以访问结构体中的各个成员

8.5位段

位段(或称位域)是结构体中以位(bit)为单位指定其成员所占的内存长度。由此可以看出,位段是一种节省空间的用法。

一般形式:
struct 位段结构名
{
	类型说明符 位域名:位域长度;
};
struct s
{
	int a:8;
	int b:3;
};

说明:

  1. 冒号后面数字的大小是不能超过前面成员类型大小的
  2. 位段成员的类型只能是整型家族的,例如:int, unsigned int, signed int, char。
  3. 位段的空间是以一次4个字节(成员为int)或1个字节(成员为char)的方式来开辟的
  4. 位段的使用存在跨平台使用问题
  5. int 位段被当成有符号数还是无符号数是不确定的。
  6. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
  7. 在不同环境下,位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  8. 在不同环境下,当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的

在VS2019、X86环境下,当一个结构体中包含多个位段时,当第一个位段有剩余的位,且无法容纳第二个位段时,舍弃剩余的位,且成员在内存中从右向左分配

9.枚举

枚举:可以把所有可能取的值一一例举

9.1枚举类型的定义

一般形式:
enum  枚举类型名
{
	枚举常量,
};

(1){ }中的内容是枚举类型的可能取值,也叫 枚举常量 。
(2)这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。

enum  Day
{
	sun = 1,
	mon = 3,
	tue = 5,
	wed,
	thu,
	fri,
	sat
};

9.2枚举变量的定义以及初始化

  • 枚举变量的定义与结构体的一样,可以先定义类型后定义变量、同时定义类型和变量或直接定义变量
  • 可以用枚举常量或枚举变量进行初始化,但是用整数时必须强制类型转换
enum  Day
{
	sun,
	mon,
	tue,
	wed,
	thu,
	fri,
	sat
};

	enum Day a = tue;enum Day a = (enum Day)2;

说明:

  1. 可以将枚举值赋给枚举变量和整型变量
  2. 可以将整数(包括整形表达式的值)赋给枚举变量,但赋值应进行类型转换
  3. 可以对枚举值进行关系运算(比较元素序号大小)
  4. 可以输出枚举值的序号

9.3枚举的优点

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
  3. 便于调试
  4. 使用方便,一次可以定义多个常量

10.共用体

10.1共用体类型的定义

一般形式:
union  共用体名
{
	类型说明符 成员;
};
union s
{
	int class;
	char office[10];
};

10.2共用体变量的定义及引用

  • 共用体变量的定义与结构体的一样,可以先定义类型后定义变量、同时定义类型和变量或直接定义变量
  • 共用体变量的引用:共用体变量名.成员名
union s
{
	int class;
	char office[10];
};

union s stu1;
stu1.class = 1;

10.3共用体的特点

  • 全体成员公用同一块空间
  • 联合的大小至少是最大成员的大小。
    当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
    对齐规则与结构体对齐规则相同,不过对齐位置都是从共用体首地址开始
  • 共用体的存储单元只能存放一个成员的数据,而不同时刻可以存放不同成员的数据
    在这里插入图片描述

11.动态内存管理

11.1动态内存存在的合理性

int a = 4;
char c = 'a';

上述定义的变量,是在栈区开辟固定大小的空间

但很多时候,存在着内存剩余或内存不足的情况,甚至有些情况下只有程序运行时才能知道具体使用多少内存空间
这时候动态内存开辟就显得尤为重要

11.2有关动态内存的函数

11.2.1malloc( )和free( )

在这里插入图片描述

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

  1. malloc( ) 申请的空间是以字节为单位
  2. 如果开辟成功,则返回一个指向开辟好空间的指针。
    如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
  3. 返回值的类型是 void* ,如果成功开辟空间且要进行访问,要对函数返回值进行强制类型转换

在这里插入图片描述

free( )函数用来释放动态开辟的内存,其参数是动态开辟空间的起始位置

  1. 如果参数是NULL指针,则函数什么都不做
  2. 如果参数指向的空间不是动态开辟的,可能出现错误
  3. 所指向的内存空间全部被释放,但指针所指向的地址依然保留,调用free()函数后,一定要给指针赋NULL,否则会出现野指针的情况
  4. 动态开辟的空间一定要释放,并且正确释放,否则会出现动态内存泄露

使用范例:

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


int main()
{
	char* p;
	p = (char*)malloc(INT_MAX+1);
	if(NULL == p)
	{
		//申请空间失败,打印错误信息 
		printf("%s\n",strerror(errno));
		return 1;
	}
	
	//释放动态内存的空间 
	free(p);
	
	//所指向的空间已被释放,指针置空 
	p = NULL;
	return 0;
}

在这里插入图片描述

11.2.2calloc( )

在这里插入图片描述

函数的功能是开辟num 个元素,每个元素为 size 大小字节空间,并且把空间的每个字节初始化为0

  1. 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

11.2.3realloc( )

在这里插入图片描述

realloc函数用来更改动态开辟内存的空间,其中memblock是要调整的内存地址,size 调整之后新空间大小

  • realloc在调整内存空间的是存在两种情况:
    (1)情况1:原有空间之后有足够大的空间
    (2)情况2:原有空间之后没有足够大的空间
    重新开辟空间,并将原来内存中的数据移动到新的空间,原空间进行释放
  1. 返回值为调整之后的内存起始位置,该函数存在两种情况,如果处于情况2时,并且申请失败会返回空指针,不可将返回值赋给源指针,应采用中间变量转换
  2. realloc(NULL,10)和malloc(10)等价
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>


int main()
{
	char* p;
	p = (char*)malloc(5);
	if(NULL == p)
	{
		//申请空间失败,打印错误信息 
		printf("%s\n",strerror(errno));
		return 1;
	}
	
	char* c;
	
	//增加新空间
	c = (char*)realloc(p,10);
	if(NULL != c)
	{
		p = c;
		c = NULL;
	}
	//释放动态内存的空间 
	free(p);
	
	//所指向的空间已被释放,指针置空 
	p = NULL;
	return 0;

11.3柔性数组

C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

struct s
{
	int n;
	int a[];
};

特点:

  1. 结构中的柔性数组成员前面必须至少一个其他成员
  2. sizeof 返回的这种结构大小不包括柔性数组的内存。
  3. 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
    在这里插入图片描述

柔性数组的使用:

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

struct s
{
	int n;
	int k[]; //柔性数组成员	
};

int main()
{
	struct s* a;
	//开辟动态内存空间
	a = (struct s*)malloc(sizeof(struct s) + 20); 
	
	if(NULL == a)
	{
		printf("%s\n",strerror(errno));
		return 1;
	}
	a->n = 1;
	int i =1;
	for(i = 0;i < 5;i++)
	{
		a->k[i] = i;	
	} 
	for(i = 0;i < 5;i++)
	{
		printf("%d ",a->k[i]);	
	}
	
	//释放开辟的内存 
	free(a);
	a=NULL; 
	return 0;
}

12.文件

12.1关于文件

  • 在前面的学习中,当程序运行起来,从键盘输入数据,进行处理,此时数据是存放在内存中。当程序结束,数据和·运行结果随之丢失,无法长期保存,给实际数据带来很多不便。
  • 文件是存储在每部介质的集合,操作系统通过文件对数据进行管理。把数据存储在文件中,使用时,操作系统根据文件名将文件调入内存中,对文件中的数据进行存取和处理。
  • 在程序设计中,文件的分类:程序文件和数据文件
    程序文件包括源文件(.c)、目标文件(.obj)、可执行文件(.exe)
    数据文件主要包括程序运行时读写的数据以及程序输出的内容
  • 文件名:一个文件要有一个唯一的文件标识,以便用户识别和引用,文件标识常被称为文件名。
    文件名包含3部分:文件路径+文件名主干+文件后缀
    例如: c:\code_files\test.c

12.2文件的打开与关闭

C语言提供了一个结构体类型的指针,称为文件指针,可通过它与文件建立联系,对文件进行操作和控制

struct _iobuf
{
	char* _ptr;       文件输入的下一位置
	int _cnt;         当前缓冲区的相对位置
	char *_base;      文件的起始位置
    int   _flag;      文件状态标志
    int   _file;      文件的有效性验证
    int   _charbuf;   检查缓冲区状况,如果无缓冲区则不读取
    int   _bufsiz;    文件的大小
    char *_tmpfname;  临时文件名

};
typedef struct _iobuf FILE;
  • 文件的打开
    将文件调入内存,并于文件指针建立联系,从而对文件进行操作
调用格式:
FILE* fp;
fp = fopen("文件名","使用文件方式");

FILE *fopen( const char *filename, const char *mode );
如果成功打开,返回文件结构体变量的起始地址;如果失败,返回一个空指针NULL

文件使用方式含义
r / rb(只读)只读,打开一个文本/二进制文件
w / wb(只写)只写,打开或建立一个文本/二进制文件
a / ab(追加)追加,打开一个文本/二进制文件
r+ / rb+(读写)为读写打开一个文本/二进制文件
w+ / wb+(读写)为读写打开或建立一个文本/二进制文件
a+ / ab+(读写)为读写打开或建立一个文本/二进制文件

说明:

  1. 文件名可以是字符常量、字符数组、字符指针(实质都是把文件位置存放其中),也可用盘符和路径来指明文件的存放位置
  2. r表示只读(read only),w表示只写(write only),a表示追加(append),b表示二进制(binary),+表示可读可写
  3. 用r的方式打开的文件必须是已经存在的
    用w的方式打开的文件无论是否存在,都将重新建立,且内容清空
    用a的方式打开的文件,如果文件不存在则建立;反之则在文件的末尾追加数据
  • 文件的关闭
    文件使用完之后,应当关闭文件,断开文件与指针的联系,同时将内存数据写入磁盘
调用格式:
fclose(文件指针);

int fclose( FILE *stream );
如果成功关闭,返回0;如果失败,返回EOF(-1)

fclose( )函数与free( )函数类似,对所有打开的文件,不再使用时都要及时调用fclose( )关闭,避免数据的丢失

12.3文件的读写

首先,先搞清楚输入和输出的的关系,输入和输出都是对于内存而言,文件存储在硬盘中,当对文件进行操作时,先要调入内存中,对文件读,就是进行输入操作;对文件写,就是输出操作

在这里插入图片描述
在这里插入图片描述
所谓流,就是输入缓冲区,C程序运行起来,默认打开三个流:

FILE* stdin   标准输入流(键盘)
FILE* stdout  标准输出流(屏幕)
FILE* stderr  标准错误流(屏幕)

12.3.1 fputc( )

int fputc( int c, FILE *stream );

调用格式:
fputc(字符,文件指针);

功能:将内存中的一个字符写入到文件指针所指向的文件中;每写入一个字符,文件内的位置指针向后移动一个字节
返回值:成功,返回该字符的ASCII码值;否则返回文件结束标志EOF

#include <stdio.h> 


int main()
{
	//打开文件 
	FILE* pf = fopen("D:\\DEV\\DEV Files\\test\\test.txt","w");
	
	if(NULL == pf)
	{
		//printf("%s\n",strerror(errno));
		//先把错误码转化为错误信息,再打印
		
		//直接打印错误信息
		perror("fopen");
		//输出错误信息,fopen为非格式串,perror()会打印冒号和错误信息,非格式串原样打印
		return 1;
	}
	
	//写文件
	fputc('a',pf);
	
	//关闭文件
	fclose(pf);
	pf = NULL; 
	return 0;
}

12.3.2 fgetc( )

int fgetc( FILE *stream );

调用格式:
字符变量 = fgetc(文件指针);

功能:从文件指针所指向的文件中读出一个字符到内存;每读出一个字符,文件内的位置指针向后移动一个字节
返回值:成功,返回该字符的ASCII码值;否则返回文件结束标志EOF

12.3.3 fputs( )

int fputs( const char *string, FILE *stream );

调用格式:
fputs(字符串,文件指针);

功能:将字符串写到文件指针所指向的文件中
返回值:成功,返回0;否则返回文件结束标志EOF

其中,字符串可以是字符串常量、字符数组名或字符指针变量

12.3.4 fgets( )

char *fgets( char *string, int n, FILE *stream );

调用格式:
fgets(字符数组名,n,文件指针);

功能:从文件指针所指向的文件中读出n-1个字符到字符数组中
返回值:成功,返回字符数组的首地址;否则返回NULL

说明:

  1. fgets( )从文件读出n-1个字符后,自动在字符串末尾加上’\0’
  2. 如果在读出n-1个字符前遇到换行符或EOF,结束读取

12.3.5 fprintf( )

int fprintf( FILE *stream, const char *format [, argument ]…);

调用格式:
fprintf(文件指针,格式字符串,打印列表);

功能:将表达式中的数据按格式要求写入到文件指针指向的文件中
返回值:成功,返回写入的字节数;否则返回一个负值

12.3.6 fscanf( )

int fscanf( FILE *stream, const char *format [, argument ]… );

调用格式:
fscanf(文件指针,格式字符串,地址列表);

功能:按格式要求将文件指针所指向的文件中读出数据到指定的内存地址中
返回值:成功,返回1;否则返回-1

说明:fscanf()和fprintf()除了多一个文件指针外,其他参数与scanf()和printf()完全相同

12.3.7 fwrite( )

size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );

调用格式:
fwrite(buffer,size,n,fp);

功能:将buffer所指的内存空间中的n个size大小的数据写入fp指向的文件中
返回值:成功,返回n;否则返回非n值

12.3.8 fread( )

size_t fread( void *buffer, size_t size, size_t count, FILE *stream );

调用格式:
fread(buffer,size,n,fp);

功能:从fp指向的文件中读取n个size大小的数据到buffer所指的内存空间中
返回值:成功,返回n;否则返回非n值

说明:

  1. buffer是指针,表示存放读写数据的首地址;n表示要读写的数据块个数;size表示每个数据块的字节数;fp为文件指针
  2. fread()和fwrite()用二进制方式打开文件

12.3.9 scanf和printf、fscanf和fprintf、sscanf和sprintf对比

  1. scanf:针对标准输入的格式化输入语句
  2. printf:针对标准输出的格式化输出语句
  3. fscanf:针对所有输入流的格式化输入语句
  4. fprintf:针对所有输出流的格式化输出语句
  5. sscanf:把一个字符串转化为一个格式化的数据
  6. sprintf:把一个格式化的数据转为为一个字符串

int sscanf( const char *buffer, const char *format [, argument ] … );
把buffer指向的字符串转化为一个格式化的数据

调用格式:
sscanf(字符指针,格式字符串,地址列表);

int sprintf( char *buffer, const char *format [, argument] … );
把一个格式化的数据转为为一个字符串,存储在buffer指向的内存空间中

调用格式:
sprintf(文件指针,格式字符串,打印列表);

12.4文件的定位

前面对文件的读写方式都是从头开始读写文件,称为顺序读写。但有时,只需要读写文件的指定部分,就需要从任意位置开始读写数据,称为随机读写。随机读写首先要移动文件内部的位置指针到需要读写的位置,然后进行读写。移动位置指针的过程称为文件定位

12.4.1随机定位函数fseek()

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

调用格式:
fseek(文件指针,相对起始位置的偏移量,起始位置);

功能:在文件指针所指向的文件中,从起始位置将位置指针移动偏移量个字节
文件起始位置有三种取值:

  1. SEEK_SET:文件的开始
  2. SEEK_END:文件的结束
  3. SEEK_END:文件位置指针指向的当前位置

12.4.2检测指针位置函数ftell()

long ftell( FILE *stream );

调用格式:
ftell(文件指针);

功能:检测位置指针相对于文件头的偏移量

12.4.2复位函数rewind()

void rewind( FILE *stream );

调用格式:
rewind(文件指针);

功能:将文件内部的位置指针移动到文件的开头

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值