C语言基础学习

一、C语言入门

1、软件安装

这里使用的是Visual C++2010

下载安装即可,建议不勾选任何东西,轻量运行

2、编写hello world

// 区分大小写,注意
// 我的第一个C程序
#include <stdio.h>  // 引入头文件,预编译

void main() {

	// 使用函数要引入头文件
	// printf是在<stdio.h>,需要引入该头文件
	printf("hello world");
	getchar();	// 让窗口停留
    // 每条语句都要以分号(;)结尾
}

3、运行过程

  1. 编码:首先编写文件代码,也就是hello.c源文件
  2. 编译:将hello.c文件翻译成目标文件(hello.obj),由cl.exe完成这个操作
  3. 链接:将目标文件(hello.obj)和库文件(系统提供的)进行链接生成可执行文件(hello.exe)由link.exe完成
  4. 运行:执行.exe文件,我们写好的程序就运行起来了

注意:①cl.exe和link.exe是在我们软件安装目录下的VS/bin目录下的

​ ②我们的库文件它是由C程序提供的

​ ③修改过的程序要重新编译内容才会发生改变

​ ④编译到连接的过程中只要发生错误都会生成失败

文件存放的位置

  • E:\Work\VS2010\MyProject01\Debug下有一个MyProject01.exe文件
  • E:\Work\VS2010\MyProject01\MyProject01下有一个hello.c文件,这个就是我们的源文件
  • E:\Work\VS2010\MyProject01\MyProject01\Debug下有一个hello.obj是我们编译过的文件

4、换行符的使用

  • /t 制表符
  • /n 换行符
  • \\ 一个\
  • \" 一个"
  • \’ 一个’
  • \r 回车,将当前位置移到本行开头

代码实现

#include <stdio.h>  // 引入头文件,预编译

void main() {

	// 使用函数要引入头文件
	// printf是在<stdio.h>,需要引入该头文件
	// printf("hello world");

	// 转义字符的运用
	printf("小智\r很帅\n");
	printf("姓名\t年龄\t籍贯\t住址\njohn\t12\t河北\t北京");

	getchar();	// 让窗口停留
}

5、注释

c语言有两种注释方式:

  1. 单行注释 //
  2. 多行注释 /**/

6、标准库的基本使用

#include <stdio.h>  // 引入头文件,预编译
#include <math.h>
#include <stdlib.h>

void main() {

	// 求2的三次幂
	double res = pow(2.0,3.0);
	printf("res=%2f",res);
	system("peuse");		// 函数表示暂停
}

二、变量

变量时程序的基本组成单位,它想相当于是内存中一个数据存储空间的表示

1、快速入门

#include <stdio.h>

void main() {
	int a = 1;
	double b = 1.1;
	char c = 'A';
	char name[] = "小智哥";	
	// 说明:%开头的为占位符
	// 输出整数%d
	// 输出浮点数%f
	// 输出字符%c
	// 输出字符串%s
	// 一定要一一对应,不然程序无法执行
	printf("num=%d source=%f.2f gender=%c name=%s", a, b, c, name);
	getchar();
}

image-20210720214444949

2.1 概念

变量相当于内存中一个数据存储空间的表示,你可以把变量看做是一个门牌号,通过门牌号找到我们的房间,而通过变量名可以访问到变量(值)

变量可以在声明的时候赋值,也可以先声明,后赋值,和java的一致

2、变量的数据类型

image-20210806204439512

注意:①在C语言中没有字符串类型,使用字符数组表示字符串

​ ②在不同系统上,部分数据类型字节长度不一样,int2个字节或者4个字节

2.1 整型类型

C语言的整数类型就是用于存放整数值的,比如12 , 30, 3456等等

image-20210806204505411

整数使用的细节

  1. 各种类型的存储大小与操作系统、系统位数和编译器有关,目前通用的以64位的为主

    image-20210813202241111

  2. 在实际工作中,c程序通常运行在linux/unix操作系统下.二级考试,使用windows

  3. C 语言的整型类型,分为有符号signed 和无符号unsigned 两种,默认是signed

  4. C 程序中整型常声明为int型,除非不足以表示大数,才使用long或者long long

    #include <stdio.h>
    
    void main() {
    
    	long num1 = 12147483647;		// -737418241
    	long long num2 = 12147483647;	// 12147483647
    
    	// 如果输出的是long,则格式%ld 
    	// 如果输出的是long long,则格式%lld
    	printf("%lld", num2);
        // sizeof()方法可以获取数据类型的长度
        printf("\nlength=%d", sizeof(int));	// 4
    	getchar();
    }
    
  5. bit(位): 计算机中的最小存储单位。byte(字节):计 算机中基本存储单元。

    1byte = 8bit [二进制再详细说,简单举例一个short3 和int 3 ]

    示意图:

    short 3 在内存中占有2字节

    int3 在内存中占有 4个字节

    image-20210813205508888

2.2 浮点类型

C语言的浮点类型可以表示一一个小数,比如123.4,7.8,0.12等等

image-20210813210247038

注意:①关于浮点数在机器中存放形式的简单说明,浮点数=符号位+指数位+尾数位,浮点数是近视值

​ ②尾数部分可能丢失,造成精度损失。

浮点型使用细节

  1. 浮点型常量默认为double型,声明float 型常量时,须后加‘f’ 或‘F’。

  2. 浮点型常量有两种表示形式

    十进制数形式:如: 5.12 512.0f .512 (必须有小数点)

    科学计数法形式:如: 5.12e2 、5. 12E-2

  3. 通常情况下,应该使用double型,因为它比float型更精确。

  4. printf(“d1=%f”, d1);//在输出时,默认保留小数点6位

代码实现

#include <stdio.h>

void main() 
{
	float d1 = 1.18923432;
	float d2 = 1.1f;
	double d3 = 1.3;

	double d4 = 5.12;
	double d5 = .512;		// 等价于0.512

	double d6 = 5.12e2;		// 等价于 5.12 * (10^2)=512
	double d7 = 5.12e-2;	// 等价于 5.12 * (10^-2)=5.12/100=0.0512

	// 在输出时,如果%f默认保留小数点后6位
	printf("d1=%.15f d2=%f d3=%f d4=%f d5=%f d6=%f d7=%f", d1, d2, d3, d4, d5, d6, d7);
	getchar();
}

image-20210813211611707

2.3 字符类型

字符类型可以表示单个字符,字符类型是char,char 是1个字节(可以存字母或者数字),多个字符称为字符串,在C语言中使用char数组表示,数组不是基本数据类型,而是构造类型[关于数组我们后面详细讲解.]

字符类型使用细节

  1. 字符常量是用单引号(")括起来的单个字符。例如: charcl = ‘a’; charc3= ‘9’;
  1. C中还允许使用转义字符‘\’ 来将其后的字符转变为特殊字符型常量。例如: charc3= ‘\n’ ; // "\n’表示换行符
  2. 在C中,char的本质是- 一个整数,在输出时,是ASCII码对应的字符。
  3. 可以直接给char赋一个整数,然后输出时,会按照对应的ASCII字符输出[97]
  4. char类型是可以进行运算的,相当于-一个整数,因为它都对应有Unicode码.

代码演示

#include <stdio.h>

void main()
{
	char c1 = 'a';
	char c2 = 97;
	// 这里会自动转换成int类型
	int sum = c1 + 10;	// 97 + 10 = 107

	// 如果是%c输出char类型的值的话就是本身
	// 如果是%d输出的话就会对照ASCLL编码表找到对应的数值输出
	printf("c1=%c c2=%c sum = %d", c1, c2, sum);
	getchar();

}

结果显示

image-20210813212637962

2.4 布尔类型

1)C语言标准(C89)没有定义布尔类型,所以C语言判断真假时以0为假,非0为真[案例]

2)但这种做法不直观,所以我们可以借助C语言的宏定义。

  1. C语言标准(C99)提供了_ Bool 型,Bool仍是整数类型,但与- -般整型不同的是, Bool变量只能赋值为0或1,非0的值都会被存储为1,C99 还提供了一个头文件<stdbool.h> 定义了bool 代表_ Bool, true 代表1,false 代表0。只要导入stdbool.h ,就能方便的操作布尔类型了,比如bool flag = false;[了解]

    ➢条件控制语句; if

    ➢循环控制语句; while …

代码演示

#include <stdio.h>

// 宏定义
#define BOOL int
#define TURE 1
#define FALSE 0

void main() {
	int isPass = -1;
	// 可以使用宏定义来完成
	// 定义一个布尔变量
	BOOL isOk = TURE;	// 等价于int isOK = 1
	if (isPass) {	// 0表示假的,1表示真的
		printf("通过考试");
	}
	if (isPass) {
		printf("ok");
	}
	int i = 1;
	int sum = i + TURE + FALSE;	// 2
	printf("sum=%d", sum);
	getchar();
}

2.5 基本数据类型转换

自动类型转换

image-20210816202523727

image-20210816202534685

1)有多种类型的数据混合运算时,系统首先自动将所有数据转换成精度最大的那种数据类型,然后再进行计算(如int型和short型运算时,先把short转成int型后再进行运算)。

2)若两种类型的字节数不同,转换成字节数大的类型,若两种类型的字节数相同,且一种有符号,-种无符号,则转换成无符号类型

3)在赋值运算中,赋值号两边量的数据类型不同时,赋值号右边的类型将转换为左边的类型,如果右边变量的数据类型长度比左边长时,将丢失一部分数据, 这样会降低精度,丢失的部分按四舍五入向前舍入

代码演示

#include <stdio.h>

void main() {

	char c1 = 'a';
	int num1 = c1;
	double d1 = num1;	// 它会自动类型提升

	float f1 = 2.2222f;
	double d2 = 4.2342342322;
	f1 = d2;	// 4.234234精度丢失

	printf("d1 = %.2f f1 = %f", d1, d2);
	getchar();
}
强制类型转换

将精度高的数据类型转换为精度小的数据类型。使用时要加上强制转换符(),但可能造成精度降低或溢出,格外要注意。

➢强制类型转换一般格式如下:

​ (类型名)表达式

​ 什么是表达式:任何有 值都可以称为表达式,比如1+2, int num=2

​ 这种强制类型转换操作并不改变操作数本身

要注意的是:不加强制转换符的是四舍五入,加了强转符就是直接截断小数点后面的

代码演示

#include <stdio.h>

void main() {

	double d1 = 2.34343;
	int num = (int)d1;	// 这里注意,它不是四舍五入,而是直接阶段小数点后的部分

	// 强制转换支队最近的数有效,如果希望针对更多的表达式转换,使用()
	int num2 = (int)(3.5 * 20 + 6 * 1.5);	// (int)(79)
	int num3 = (int)3.5 * 20 + 6 * 1.5;		// 60 + 9 = 69,所以要注意这个问题

	// d1依然是double
	printf("num=%d d1=%f num2=%d num3 = % d", num, d1, num2, num3);
	getchar();

}

3、指针入门

简单的来说,指正表示一个地址(存放的就是一个地址)

写法:int* ptr或者int *ptr都可以

image-20210816205701239

注意:指针的类型一定要和数据类型是一致的,int类型的指针只能存放int类型的地址

代码演示

#include <stdio.h>

void main() {

	int num = 1;
	// 定义一个指针变量
	// 说明
	// 1 int*表示类型为指针类型
	// 2 名称ptr,ptr就是一个int*类型
	// 3 ptr指向一个int类型的变量的地址
	int* ptr = &num;

	// 说明1:如果要取出一个变量的地址,使用格式是%p
	// 说明2: &num表示取出num这个变量对应地址
	printf("num的值=%d num地址=%p", num, ptr);	// num的值=1 num地址=00BCFE6C

	// 1 指针变量,本身也有地址	&ptr
	// 2 指针变量,存放的地址	ptr
	// 3 获取指针指向的值	*ptr
 
	//ptr的地址是006FFA9C ptr存放的值为006FFAA8 ptr指向的值=1
	printf("\nptr的地址是%p ptr存放的值为%p ptr指向的值=%d", &ptr, ptr, *ptr);

	// 修改num的值
	*ptr = 99;
	printf("\nptr指向的值=%d", *ptr);	// 99

	float a = 22;
	float* ptr2 = &a;
	
	double sum = *ptr + *ptr2;
	printf("\nsum=%.2f", sum);	// 99

	getchar();
}

4、值传递和地址传递

C 语言传递参数(或者赋值)可以是值传递(pass by value), 也可以传递指针(a pointer passedbyvalue),传递指针也叫地址传递。

  1. 默认传递值的类型:基本数据类型(整型类型、小数类型,字符类型),结构体,共用体。
  2. 默认 传递地址的类似:指针、数组

值传递和地址传递使用特点

1)值传递:将变量指向的存储内容,在传递/赋值时,拷贝- -份给接收变量.

2)地址传递也叫指针传递:如果是指针,就将指针变量存储的地址,传递给接收变量,如果是数组,就将数组的首地址传递给接收变量。

image-20210816212925366

代码演示

#include <stdio.h>

void main() {

	int num = 100;
	int* p = &num;
	int* p2 = p;
	*p2 = 66;
	printf("num=%d", num);	// 66

	getchar();
}

三、常量

3.1 基本介绍

1)常量是固定值, 在程序执行期间不能改变。这些固定的值,又叫做字面量

2)常 量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量。

3)常量的值在定义后不能进行修改.

3.2 经常使用的常量

1 整数常量

1)整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数: 0x或0X表示十六进制,0表示八进制,不带前缀则默认表示十进制。整数常量也可以带一个后缀,后缀是U和L的组合,∪表示无符号整数( unsigned),L表示长整数(long)。后缀可以是大写,也可以是小写,U和L的顺序任意

2)整数常量举例

image-20210818200831547

int n1 = 0213;	// 八进制
int n2 = 0x4b;	// 十六进制

2 浮点常量

image-20210818200924634

char c1 = 'a';	
char c2 = '\t';		//'\t'是字符常量

3 字符常量

char c1 = 'a';
char c2 = '\t';

4 字符串常量

char str1[20] = "我是靓仔";
char str2[30] = "春暖花开,世界和平";

3.3 常量的定义

1 定义常量的两种方式

① 使用#define预处理器

② 使用const关键字

2 #define预处理器

形式:#define 常量名 常量值

代码演示

#include <stdio.h>

#define PI 3.14	// 定义常量PI 常量值为3.14
void main() {

	//PI = 3.1415926;	常量不能被修改

	double area;

	double r = 1.2;	// 半径
	area = PI * r * r;
	printf("面积:%.2f", area);
	getchar();
}

3 const关键字

#include <stdio.h>

const double PI = 3.14;		// 类似于java的final关键字
void main() {

	double area;
	double r = 1.2;	// 半径
	area = PI * r * r;
	printf("面积:%.2f", area);
	getchar();
}

4 const和#define的区别

  1. const 定义的常量时,带类型,define 不带类型
  2. const是在编译、运行的时候起作用,而define是在编译的预处理阶段起作用
  3. define 只是简单的替换,没有类型检查。简单的字符串替换会导致边界效应 [案例演示].
  4. const常量可以进行调试的,define是不能进行调试的,主要是预编译阶段就已经替换掉了,调试的时候就没它了
  5. const 不能重定义,不可以定义两个一样的,而define通过undef取消某个符号的定义,再重新定义[案例]
  6. define 可以配合#ifdef、#ifndef、 #endif 来使用,可 以让代码更加灵活,比如我们可以通过#define来启动或者关闭调试信息。[案例]

案例

#define A 1;
#define B A + 3;
#define B2 (A + 3)
// define只是简单地替换,直接换
#define C A/B*3;	// 相当于是1/1+3*3 = 10
#define C2 A/B*3	// 相当于是1/(1+3)*3=0.75
#include <stdio.h>

#define DEBUG

void main() {

#ifdef DEBUG
	printf("ok, 调试信息");
#endif
#ifdef DEBUG
	printf("hello,另外的信息");
#endif

#undef DEBUG	// 取消DEBUG的定义
#define DEBUG 123	// 重新定义DEBUG
	printf("DEBUG=%d", DEBUG);
	getchar();
}

四、运算符

1 算数运算符

image-20210822154820131

代码演示

#include <stdio.h>

void main() {

	int i = 10;
	int j = 3;
	int sum = i / j;	// sum = 3

	double i2 = 10;
	int j2 = 4;
	double sum2 = i2 / j2;	// sum2 = 2.50

	printf("sum=%d,sum2=%.2f", sum, sum2);
	getchar();
}

注意事项

对于除号“1”,它的整数除和小数除是有区别的:整数之间做除法时,只保留整数部分而舍弃小数部分

​ 例如: intx=10/3,结果是3

标识符命名规范

1)程序中不得出现仅靠大小写区分的相似的标识符,intx,X;变量x与X容易混淆

2)所有宏定义、 枚举常数、常量(只读变量)全用大写字母命名,用下划线分隔单词

比如:const double TAX RATE = 0.08; //TAX RATE只读变量

​ #define FILE_ PATH “/usr/tmp”

3)定义变量别忘了初始化。定义变量时编译器并不一定清空了这块内存,它的值可能是无效的数据,运行程序,会异常退出.

4)变量名、 函数名:多单词组成时,第-一个单词 首字母小写,第二个单词开始每个单词首字母大写: xxxYyyZzz [驼峰法,小驼峰,比如short stuAge = 20;]

比如:tankShotGame tankShotGame

关键字

image-20210822161058806

键盘输入语句

scanf,类似于java的scanner,控制台输入

一、scanf和gets

1.scanf()

语法:scanf(“格式控制字符串”,变量地址列表);

接受字符串时:scanf(“%s”,字符数组名或指针);

2.gets()

语法:gets(字符数组名或指针);

不同点

scanf不能接受空格、制表符Tab、回车等;

而gets能够接受空格、制表符Tab和回车等;

scanf :当遇到回车,空格和tab键会自动在字符串后面添加’\0’,但是回车,空格和tab键仍会留在输入的缓冲区中。

gets:可接受回车键之前输入的所有字符,并用’\0’替代 ‘\n’.回车键不会留在输入缓冲区中

二、printf和puts

不同点:puts方法输出完字符串后会自动换行。

五、二进制和位运算符

进制

对于整数,有四种表示方式:

  1. 二进制: 0,1,满2进1,C语言中没有二进制常数的表示方法。

  2. 2)十进制: 0-9 ,满10进1。

  3. 3)八进制: 0-7,满8进1.以数字0开头表示。

  4. 十六进制: 0-9及A-F,满16进1.以0x或0X开头表示。此处的A-F不区分大小写。[A->10B->11C->12D->13E->14 F->15 ]

    如: 0x21AF +1= 0X21B0

➢举例说明:

int num2 = 210; //十进制
int num3 = 01010; //八进制
int num4 = 0x1010;//十六进制

进制的图示

image-20210822165826536

image-20210822165838023

进制转换

1 其他进制转换成十进制

①二进制转十进制

规则:从最低位开始,将每个位上的数提取出来,乘以2的(位数-1)次方,然后求和。

案例:将二进制1011转成十进制的数

1011 = 1 * 2^0 + 1 * 2^1 + 0 * 1^2 + 1 * 2^3 = 1 + 2 + 0 + 8 = 11

②八进制转十进制

规则:从最低位开始,将每个位上的数提取出来,乘以8的(位数-1)次方,然后求和。

案例:将0123转成十进制的数

0123 = 3 * 8^0 + 2 * 8^1 + 1 * 8^2 = 3 + 16 + 64 = 83

③十六进制转十进制

规则:从最低位开始,将每个位上的数提取出来,乘以16的(位数-1)次方,然后求和

案例:将0X34A转成十进制的数

0X34A = 10 * 16^0 + 4 * 16^1 + 3 * 16^2 = 10 + 64 + 768 = 842

课后练习

110001100转成干进制

2^2 + 2^3 + 2^7 + 2^8 = 4 + 8 + 128 + 256 = 396

02456转成十进制

6 * 8^0 + 5 * 8^1 + 4 * 8^2 + 2 * 8^3 = 6 + 40 + 256 + 1024 = 1326

0xA45转成十进制

5 * 16^0 + 4 * 16^1 + 10 * 16^2 = 5 + 64 + 2560 = 2629

2 十进制转其他进制

①十进制转二进制

规则:将该数不断除以2,直到商为0为止,然后将每步得到的余数倒过来,就是对应的二进制。

案例:将56转成二进制

image-20210822172658171

②十进制转八进制

规则:将该数不断除以8,直到商为0为止,然后将每步得到的余数倒过来,就是对应的八进制。

案例:请将156转成八进制

234

③十进制转十六进制

规则:将该数不断除以16,直到南为0为止,然后将每步得到的余数倒过来,就是对应的十六进制。

案例:请将356转成十六进制

164

课堂练习

image-20210822172901400

123 -> 0111 1011

678 -> 01246

8912 -> 0X22D0

3 二进制转八进制和十六进制

①二转八

规则:从低位开始,将=二进制数每三位一组, 转成对应的八进制数即可。

案例:请将11010101转成八进制

11 010 101 -> 0325

②二转十六

规则:低位开始,将二进制数每四位一组, 转成对应的十六进制数即可。

案例:请将11010101转成十六进制

1101 0101 -> 0XD5

练习

image-20210822174855460

11 100 101 -> 0345

11 1001 0110 -> 0X396

4 八进制和十六进制转二进制

①八转二

规则:将八进制数每1位,转成对应的一一个3位的二进制数即可。

案例:请将0237转成二进制

0237 -> 01 011 111

②十六转二

规则:将十六进制数每1位,转成对应的4位的-一个二进制数即可。

案例:请将0x23B转成二进制

0x23B -> 0010 0011 1011

练习

image-20210824213455239

01230 -> 1 010 011 000

0XAB29 -> 1010 1011 0010 1001

原码、反码、补码

必须要记住,很重要

image-20210825200833011

这里演示用四个字节
正数三码合一
2
原码: 00000000 00000000 00000000 00000010
反码: 00000000 00000000 00000000 00000010
补码:	00000000 00000000 00000000 00000010
    
-2
原码: 10000000 00000000 00000000 00000010
反码: 11111111 11111111 11111111 11111101
补码: 11111111 11111111 11111111 11111110

位运算符

image-20210825195950914

示例

image-20210825200035242

~2

~2
2的补码: 00000000 00000000 00000000 00000010

补码取反: 11111111 11111111 11111111 11111101
然后将上面取反的补码转成原码就是我们的结果了

最高符号位为1,它是一个负数,所以要按照负数的规则来转
思路:先转成反码,反码再转成原码
转反码 = 补码 - 1
-> 11111111 11111111 11111111 11111100
反码 -> 原码
-> 10000000 00000000 00000000 00000011
所以~2的结果是 -3

~-5

~-5
-5的原码 -> 10000000 00000000 00000000 00000101
    反码 -> 01111111 11111111 11111111 11111010
    补码 -> 01111111 11111111 11111111 11111011
    
    取反 -> 10000000 00000000 00000000 00000100
    
    反码 -> 01111111 11111111 11111111 11111011
    原码 -> 00000000 00000000 00000000 00000100
结果是 4

2&-3

2&-3
-3的原码: 10000000 00000000 00000000 00000011
    反码: 11111111 11111111 11111111 11111100
    补码: 11111111 11111111 11111111 11111101
 2的补码: 00000000 00000000 00000000 00000010
      & : 00000000 00000000 00000000 00000000
    结果为0

2|3

 2|3
  2的补码: 00000000 00000000 00000000 00000010
  3的补码: 00000000 00000000 00000000 00000011
       | : 00000000 00000000 00000000 00000011
       结果为3

2^3

2^3
  2的补码: 00000000 00000000 00000000 00000010
  3的补码: 00000000 00000000 00000000 00000011
       ^ : 00000000 00000000 00000000 00000001
      结果为1

4&-5

-5的原码: 10000000 00000000 00000000 00000101
    反码: 11111111 11111111 11111111 11111010
    补码: 11111111 11111111 11111111 11111011
  4的补码:00000000 00000000 00000000 00000100
      & : 00000000 00000000 00000000 00000000
          结果为0

左移和右移

image-20210825215019063

正数的右移就是除以2,又移几位就是除于几个2,左移就是乘于2,左移几位就是乘于几个2

负数的要通过推

举例子

2的补码: 00000000 00000000 00000000 00000010
   2>>1: 00000000 00000000 00000000 00000001
   就是整体向右移动一位,符号位为0,所以用符号位补全溢出的高位

-1 >> 2

-1的原码: 10000000 00000000 00000000 00000001
    反码: 11111111 11111111 11111111 11111110
    补码: 11111111 11111111 11111111 11111111
   -1>>2: 11111111 11111111 11111111 11111111
    反码: 11111111 11111111 11111111 11111110
    原码: 10000000 00000000 00000000 00000001
        结果是-1

-1 << 2

-1的补码: 11111111 11111111 11111111 11111111
   -1<<2: 11111111 11111111 11111111 11111100
    反码: 11111111 11111111 11111111 11111011
    原码: 10000000 00000000 00000000 00000100
        结果为-4

六、程序流程控制

while和for死循环

image-20210912220452836

switch要注意的细节

image-20210827162347236

goto

类似于水门的飞雷神,通过标志然后进行传送

image-20210827151853517

image-20210827151907129

#include <stdio.h>

void main() {
	printf("start\n");
	goto lablel; //lable1称为标签
	printf("ok l\n");
	printf("ok2\n");
	lablel:
	printf("ok3\n");
	printf("ok4\n");
		getchar();
} //输出 ok3和ok4

练习

水仙花数

image-20210827152946275

#include <stdio.h>

void main() {

	int num = 154;	// 不是水仙花数
	int num1 = num / 100;		// 百位数
	int num2 = num % 100 / 10;	// 十位数
	int num3 = num % 10;		// 个位数
	if (num == num1 * num1 * num1 + num2 * num2 * num2 + num3 * num3 * num3) {
		printf("%d是水仙花数",num);
	}
	else {
		printf("%d不是水仙花数", num);
	}
	getchar();
}

求天数

image-20210827153826637

#include <stdio.h>

// 求月的天数,要考虑闰年和平年
// 思路:31天的月份和30的月份区分开,还要判断是否是闰年

void main () {
	// 31天的月份分别是:1月、3月、5月、7月、8月、10月、12月
	int year = 2021;
	int month = 2;

	switch (month)
	{
	case 1:
	case 3:
	case 5:
	case 7:
	case 8:
	case 10:
	case 12:
		printf("%d年的%d月份是%d天", year, month, 31);
		break;
	case 2:
		// 判断是否是闰年
		if (year % 4 == 0 && year / 100 != 0) {
			printf("%d年的%d月份是%d天", year, month, 29);
		}
		printf("%d年的%d月份是%d天", year, month, 28);
		break;
	default:
		printf("%d年的%d月份是%d天", year, month, 30);
		break;
	}
	getchar();
}

判断星期

image-20210827155233914

#include <stdio.h>

// 判断星期,星期一到星期三,打印AAA,星期四到星期五打印BBB,星期六到星期日打印CCC

void main() {
	int week = 1;
    
	switch (week)
	{
	case 1:
	case 2:
	case 3:
		printf("AAA");
		break;
	case 4:
	case 5:
		printf("BBB");
		break;
	case 6:
	case 7:
		printf("CCC");
		break;
	}
	getchar();
}

输出a-z和A-Z

image-20210827155839298

#include <stdio.h>

void main() {

	for (char c1 = 'a'; c1 <= 'z'; c1++)
	{
		printf("%c\n", c1);
	}
	for (char c2 = 'A'; c2 <= 'Z'; c2++)
	{
		printf("%c\n", c2);
	}
	getchar();
}

七、枚举

  1. 枚举是C语言中的一种构造数据类型,它可以让数据更简洁,更易读,对于只有几个有限的特定数据,可以
    使用枚举.

  2. 枚举对应英文(enumeration,简写enum)

  3. 枚举是一组常量的集合,包含- -组有限的特定的数据

  4. 枚举语法定义格式为

    enum
    枚举名{枚举元素 1,枚举元素...;.
    

快速入门

#include <stdio.h>

// 枚举快速入门
enum DAY
{
	MON = 1,TUE = 2, WED = 3, THU = 4, FRI = 5, SAT = 6,SUN = 7
};

void main() {
	// 也可以在方法内部定义
	//enum DAY
	//{
	//	MON = 1, TUE = 2, WED = 3, THU = 4, FRI = 5, SAT = 6, SUN = 7
	//};
	enum DAY day = WED;
	printf("%d", day);
	getchar();
}

遍历枚举

for循环

#include <stdio.h>

// for循环遍历枚举
enum DAY 
{
	// 如果没有赋值,就会按照顺序赋值,如果第一个也没有赋值,那么就会从0开始赋值
	MON , TUE, WED, THU, FRI, SAT, SUN	
}day;	// 表示定义了一个枚举类型,同时定义了一个变量day(enum DAY类型)
void main() {
	for (day = MON; day <= SUN; day++) {
		printf("%d\n", day);
	}
	getchar();
}

switch循环

#include <stdio.h>

enum SEASONS
{
	SPRING=1, SUNMMER, AUTUMN, WINTER
}season;	// 定义枚举类型,变量名season

void main() {
	printf("请输入你喜欢的季节:(1.spring 2.summer 3.autumn 4.winter):");
	scanf_s("%d", &season);
	switch (season)
	{
	case SPRING:
		printf("你喜欢的是春天");
		break;
	case SUNMMER:
		printf("你喜欢的是夏天");
		break;
	case AUTUMN:
		printf("你喜欢的是秋天");
		break;
	case WINTER:
		printf("你喜欢的是的冬天");
		break;
	default:
		printf("没有你喜欢的天");
		break;
	}
	getchar();
	getchar();
}

枚举的注意事项和细节

  1. 第一个枚举成员的默认值为整型的0, 后续枚举成员的值在前一个成员上加1。 我们在这个实例中把第一一个枚
    举成员的值定义为1,第二个就为2,以此类推. [看案例]

  2. 在定义枚举类型时改变枚举的元素的值

    enum DAY {
    	MON, TUE, WED, THU=9, FRI, SAT, SUN //如果没有给赋值,就会按照顺序赋值.
    } day;  // 表示定义了一- 个枚举类型enum Day,同时定义了一个变量day(类型是enum DAY)
    // MON, TUE, WED 为 0 , 1 , 2
    //说明FRI, SAT, SUN就是10, 11, 12
    

    所以后面数的赋值要根据前面数来定

  3. 枚举变量的定义也可以先定义枚举类型,再定义枚举变量

    enum DAY {
    MON=1, TUE, WED, THU, FRI, SAT, SUN
    };
    enum DAY day; 
    
  4. 枚举变量的定义可以省略枚举名称,直接定义枚举变量

    enum {
    	MON=1, TUE, WED, THU, FRI, SAT, SUN
    } day;	// 这样使用枚举,该枚举类型只能使用- - -次.
    

    注意:这种定义只能用一次

  5. 可以将整数转换成对应的枚举值

    #include <stdio.h>
    int main() {
    	enum SEASONS { SPRING = 1, SUMMER, AUTUMN, WINTER };
    	enum SEASONS season;
    	int n = 4;
    	season = (enum SEASONS)n;	// 转换成枚举值
    	printf("season = %d", season);
    		getchar();
    	return 0;
    }
    

八、函数

头文件概述

  1. 头文件是扩展名为.h的文件,包含了C函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:
    程序员编写的头文件和C标准库自带的头文件
  2. 在程序中要使用头文件, 需要使用C预处理指令#include来引用它。前面我们已经看过stdio.h 头文件,它是
    C标准库自带的头文件
  3. #include叫做文件包含命令,用来引入对应的头文件(.h文件)。#include也是C语言预处理命令的一-种。#include
    的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成-一个源
    文件,这与复制粘贴的效果相同。但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,
    特别在程序是由多个源文件组成的时候。
  4. 建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件

代码实现

首先我们要定义头文件,定义好头文件之后我们就编写对应函数的代码

在头文件文件夹中创建 myfun.h 文件
int sum(int a, int b);

void sayHello();

编写对应的代码

#include <stdio.h>

int sum(int a, int b) {
	return a + b;
}

void sayHello() {
	printf("hello");
}

测试函数

#include <stdio.h>
#include "myfun.h"	// 引入自定义的函数

void main() {
	int a = 2;
	int b = 3;
	int s = sum(a, b);
	printf("%d\n", s);
	sayHello();		// 调用sayHello方法
	getchar();
}

头文件的注意事项和细节说明

  1. 引用头文件相当于复制头文件的内容

  2. 源文件的名字可以不和头文件一样,但是为了好管理,一**-般头文件名和源文件名一样.**

  3. C语言中include<> 与include ""的区别
    include <>:引用的是编译器的类库路径里面的头文件,用于引用系统头文件。
    include"":引用的是你程序目录的相对路径中的头文件,如果在程序目录没有找到引用的头文件则到编译器的
    类库路径的目录下找该头文件,用于引用用户头文件。
    所以:
    *引用系统头文件,两种形式都会可以,include<> 效率高
    *引用用户头文件,只能使用include " "

:" "它会先去找自定义的函数,找不到自定义的就会去找系统提供的

  1. 一个#include 命令只能包含一个头文件,多个头文件需要多个#include 命令

  2. 同一个头文件如果被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引
    入的机制

  3. 在一个被包含的文件(.c)中又可以包含另一个文件头文件(.h)

  4. 不管是标准头文件,还是自定义头文件,都只能包含变量和函数的声明,不能包含定义,否则在多次引入时会
    引起重复定义错误(!!!

因为include是替换头文件里面的内容,如果是多次引入函数的定义的话就没事,因为它有查重的机制,如果是多次引入函数的声明的话就会出问题。

函数的调用过程

函数的调用规则(适用于java,c++,php)

  1. 当调用(执行)一个函数时,就会开辟一个独立的空间(栈)

  2. 每个栈空间都是相互独立的

  3. 当函数执行完毕后(或者执行到return语句),会返回调用函数的位置,继续执行下面的代码

  4. 如果函数有返回值,则将返回值赋给接收的变量

    ①如果方法返回的数据是double类型的,我们可以ruturn比他小的类型,比如int类型,它会自动类型提升到double类型

    ②如果方法返回的数据是char类型的,return 一个int类型就会报错,需要进行强制类型转换才能return

  5. 当一个函数返回后,该函数对应的栈空间也就销毁了

image-20210828164211734

举例说明

image-20210828163647936

函数-递归调用

我调我自己,在方法内部调用自己

#include <stdio.h>

void test(int n) {
	if (n > 2) {
		test(n - 1);
	}
	printf("n=%d\n", n);
}
void main() {
	test(4);
	getchar();
}

image-20210828170422426

函数递归需要遵守的重要规则

  1. 执行一个函数时,就创建一个独立的空间(栈)
  2. 函数的局部变量时独立的,不会相互影响
  3. 递归必须有退出递归的条件,如果没有就会无限递归,造成内存溢出
  4. 当一个函数执行完毕,或遇到return,就会返回,谁调用那么结果就返回给谁

递归练习题

➢题1: 斐波那契数
请使用递归的方式,求出斐波那契数1,1,2,3,5,8,13…
给你-一个整数n,求出它的斐波那契数是多少?

#include <stdio.h>

int fbn(int n) {
	if (n == 1 | n == 2) {
		return 1;
	}
	else {
		return fbn(n - 1) + fbn(n - 2);
	}
}

void main() {

	// 求斐波那契数
	int res = fbn(5);
	printf("res=%d", res);
	getchar();
}

➢题2:求函数值
已知f(1)=3; f(n)= 2*f(n-1)+1;
请使用递归的思想编程,求出f(n)的值?

#include <stdio.h>

int f(int n) {
	if (n == 1) {
		return 3;
	}
	else {
		return 2 * f(n - 1) + 1;
	}
}

void main() {
	// 求函数值
	int res = f(7);
	printf("res=%d", res);
	getchar();
}

➢题3:猴子吃桃子问题
有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!以后每天猴子都吃其中的一-半,然后再多吃一一个。
当到第十天时,想再吃时(还没吃),发现只有1个桃子了。问题:最初共多少个桃子?

#include <stdio.h>
// 有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!以后每天猴子都吃其中的一 - 半,然后再多吃一一个。
// 当到第十天时,想再吃时(还没吃),发现只有1个桃子了。问题:最初共多少个桃子 ?
int peach(int day) {
	if (day == 10) {
		return 1;
	}
	else {
		return (peach(day + 1) + 1) * 2;
	}
}

void main() {
	// 猴子吃桃子问题
	int res = peach(1);
	printf("res=%d", res);
	getchar();
}

函数的注意事项和细节讨论

  1. 函数的形参列表可以是多个。

  2. C 语言传递参数可以是值传递(pass by value),也可以传递指针(a pointer passed by value)也叫引用传递。

  3. 函数的命名遵循标识符命名规范,首字母不能是数字,可以采用驼峰法或者下划线法,比如getMax()
    get_ max().

  4. 函数中的变量是局部的, 函数外不生效

  5. 基本数据类型默认是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。

  6. 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果
    上看类似引用(即传递指针) [ 案例演示:]

    #include <stdio.h>
    void test(int* p) {
    	(*p)++;
    }
    
    void main() {
    	int p = 3;
    	test(&p);
    	printf("p=%d", p);
    	getchar();
    }
    
  7. C语言不支持函数重载。

  8. C语言支持可变参数函数1/知道即可[案例演示]

    #include <stdio.h>
    #include <stdarg.h>
    
    void test(int* p) {
    	(*p)++;
    }
    int fun(int num, ...) {		// 可变形参就是将一堆参数放入到一个数组中
    	int i, totalSum = 0;	// totalSum一定要初始化
    	int val = 0;
    	va_list v1;
    	va_start(v1, num);
    	printf("*v=%d\n", v1);
    	for (i = 0 ; i < num; i++)
    	{
    		val = va_arg(v1, int);
    		printf("val=%d\n", val);
    		totalSum += val;
    	}
    	va_end(v1);
    	return totalSum;
    }
    
    void main() {
    	// 传入指针参数
    	//int p = 3;
    	//test(&p);
    	//printf("p=%d", p);
    
    	// 可变形参
    	int res = fun(8, 1, 2, 3, 4, 5, 5, 6, 9);	// 第一个参数是它这个可变形参数组的长度
    	printf("res=%d", res);
    	getchar();
    }
    

练习题

请编写一个函数swap(intnl, int **n2) 可以交换nl和:n2的值

#include <stdio.h>

void swap(int *n1, int *n2) {
	int temp = *n1;
	*n1 = *n2;
	*n2 = temp;
}

void main() {
	int n1 = 2;
	int n2 = 3;
	swap(&n1, &n2);
	printf("n1=%d, n2=%d", n1, n2);
	getchar();
}

函数参数的传递方式

两种传递方式

  1. 值传递
  2. 引用传递(传递指针、地址)

值传递和引用传递的特点

  • 值传递:变量直接存储值,内存通常在栈中分配

    默认是值传递的数据类型有: 1.基本数据类型2. 结构体3.共用体4.枚举类型

  • 引用传递:变量存储的是地址,这个地址对应的空间才是具体的值

    默认是引用传递的数据类型有: 指针和数组

如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量(*指针)。

变量的作用域

变量作用域就是指变量的有效范围

  1. 函数内部声明/定义的局部变量,作用域仅限于函数内部。
  2. 函数的参数, 形式参数,被当作该函数内的局部变量,如果与全局变量同名它们会优先使用局部变量(编译器
    使用就近原则)
  3. 在一个代码块,比如for/if中的局部变量,那么这个变量的的作用域就在该代码块
  4. 4)在所有 函数外部定义的变量叫全局变量,作用域在整个程序有效

初始化局部变量和全局变量

  1. 局部变量,系统不会对其默认初始化,必须对局部变量初始化后才能使用,否则,程序运行后可能会异常退出.

  2. 全局变量,系统会自动对其初始化,如下所示

    image-20210829092943836

  3. 正确地初始化变量是一个良好的编程习惯,否则有时候程序可能会产生意想不到的结果,因为未初始化的变量
    会导致一些在内存位置中已经可用的垃圾值

作用域的注意事项和细节

  1. 全局变量(Global Variable)保存在内存的全局存储区中,占用静态的存储单元,它的作用域默认是整个程序,也
    就是所有的代码文件,包括源文件(.c 文件)和头文件(.h 文件)。[c 程序内存布局图!!!]

    image-20210829094611925

  2. 局部变量(IocalVariable)保存在栈中,函数被调用时才动态地为变量分配存储单元,它的作用域仅限于函数内
    部。[内存布局分析]

  3. C语言规定,只能从小的作用域向大的作用域中去寻找变量,而不能反过来,使用更小的作用域中的变量

  4. 在同一个作用域,变量名不能重复,在不同的作用域,变量名可以重复,使用时编译器采用就近原则.

  5. 由{ }包围的代码块也拥有独立的作用域

练习

思考:下面的代码输 出什么内容?

#include <stdio.h>

double price = 200.0;	// 全局变量

void test01() {
	printf("%.2f\n", price);
}
void test02() {
	// 编译器采用就近原则
	double price = 250.0;
	printf("%.2f\n", price);
}

void main() {
	printf("main price = %.2f\n", price);
	test01();	// 200
	test02();	// 250
	test01();	// 200	因为调用test02的时候已经修改的是局部变量price的值,所以并没有修改全局变量
	getchar();
}

思考:下面的代码输 出什么内容?

#include <stdio.h>

int n = 10;

void fun1() {
	int n = 20;
	printf("fun1 n:%d\n", n);	// 局部变量
}

void fun2(int n) {
	printf("fun2 n:%d\n", n);	// 形参n
}

void fun3() {
	printf("fun3 n:%d\n", n);	// 全局变量n
}

void main() {
	int n = 30;
	fun1();		// 20
	fun2(n);	// 30
	fun3();		// 10
	getchar();
}

static关键字

c语言的static和java的是相反的,它和java中的private关键字的作用一样

局部变量使用static修饰

  1. 局部变量被static 修饰后,我们称为静态局部变量
  2. 对应静态局部变量在声明时未赋初值,编译器也会把它初始化为0。
  3. 静态局部变量存储于进程的静态存储区(全局性质),只会被初始一-次,即使函数返回,它的值也会保持不变

代码演示

void main() {
	static int n;
	printf("n=%d", n);	// 0
	getchar();
}
#include <stdio.h>

//void main() {
//	static int n;
//	printf("n=%d", n);	// 0
//	getchar();
//}

void fun(void) {	// void就是告诉编译器没有形参,可写可不写
	int n = 10;
	printf("n=%d\n", n);
	n++;
	printf("n++=%d\n", n);
}

void fun_static(void) {
	static int n = 10;
	printf("\nstatic n=%d\n", n);
	n++;
	printf("\nn++=%d\n", n);
}

void main() {
	fun();
	printf("-----------\n");
	fun_static();
	printf("-----------\n");
	fun();
	printf("-----------\n");
	fun_static();
	getchar();
}

全局变量使用static修饰

  1. 普通全局变量对整个工程可见,其他文件可以使用exterm外部声明后直接使用。也就是说其他文件不能再定义
    一个 与其相同名字的变量了( 否则编译器会认为它们是同一个变量),静态全局变量仅对当前文件可见,其他
    文件不可访问,其他文件可以定义与其同名的变量,两者互不影响[案例]
file01
int n = 10;
static int n2 = 20;
file02
extern int n2;
// extern int n;
void main() {
    //printf("%d", n);	// 10
	printf("%d", n2);
	getchar();
}

image-20210829104215962

  1. 定义不需要与其他文件共享的全局变量时,加上static关键字能够有效地降低程序模块之间的耦合,避免不同
    文件同名变量的冲突,且不会误使用

函数用static修饰

和修饰全局变量一个作用,都是只能在本文件中使用,其他文件不能使用

  1. 函数的使用方式与全局变量类似,在函数的返回类型前加上static,就是静态函数

  2. 非静态函数可以在另一个文件中通过extern 引用[ 案例]

  3. 静态函数只能在声明它的文件中可见,其他文件不能引用该函数[案例]

  4. 不同的文件 可以使用相同名字的静态函数,互不影响[案例]

    如果引用了其他文件的全局变量,那么就不能定义和这个相同的全局变量了

代码演示

file03
#include <stdio.h>

void fun1() {
	printf("我是靓仔智");
}

static void fun2() {
	printf("我是傻逼智");
}
file04
#include <stdio.h>

extern void fun1();
// extern void fun2();

void main() {
	//fun1();
	fun2();
	getchar();
}

image-20210829105442480

常用的系统函数

字符串常用的系统函数

  1. 得到字符串的长度
    size_ t strlen( const char *str)
    计算字符串str 的长度,直到空结束字符,但不包括空结束字符。
  2. 拷贝字符串
    char *strcpy(char *dest, const char *src)
    把SrC所指向的字符串复制到dest。
  3. 连接字符串(类似java的append)
    char *strcat( char * dest, const char *src)
    把src所指向的字符串追加到dest所指向的字符串的结尾。

代码演示

#include <stdio.h>
#include <string.h>		// 使用字符串要引入对应的函数库

// 测试常用的字符串函数
void main() {
	char str[30] = "abcde";
	char a[20] = "asd";
	char b[30] = "世界上最靓仔的是";
	int length = strlen(str);
	strcpy(a, "cccc");
	strcat(b, "靓仔智");
	printf("length=%d\n", length);
	printf("a=%s\n", a);	// cccc,覆盖了原来的数据
	printf("b=%s\n", b);	// 世界上最靓仔的是靓仔智
	getchar();
}

时间和日期相关函数

  1. 获取当前时间
    char *ctime( const time t *timer)

    返回一个表示当地时间的字符串,当地时间是基于参数timer。

    void main() {
    	time_t curtime;	// time_h是一个结构体类型
    	time(&curtime);	// time()完成初始化
    	// ctime返回一个表示当前时间的字符串,当前时间是基于参数timer
    	printf("当前时间:%s", ctime(&curtime));
    	getchar();
    }
    
  2. 编写一段代码来统计函数test执行的时间
    double difftime(time_ t timel, time_ _t time2)
    返回timel 和time2之间相差的秒数(timel-time2)。 .

    #include <stdio.h>
    #include <time.h>
    
    void test() {
    	printf("test函数开始执行\n");
    	int sum = 0;
    	for (int i = 0; i < 66666; i++)
    	{
    		sum = 0;
    		for (int j = 0; j < 10; j++)
    		{
    			sum += j;
    			printf("%d", i);
    		}
    
    	}
    }
    void main() {
    	//time_t curtime;	// time_h是一个结构体类型
    	//time(&curtime);	// time()完成初始化
    	 ctime返回一个表示当前时间的字符串,当前时间是基于参数timer
    	//printf("当前时间:%s", ctime(&curtime));
    	//getchar();
    
    	// 执行test()前的时间
    	time_t start_t, end_t;
    	double diff_t;	// 存放时间差
    	printf("程序启动\n");
    	time(&start_t);	// 初始化当前时间
    
    	test();	// 执行test
    
    	// 执行test()后的时间
    	time(&end_t);	// 得到当前时间
    	diff_t = difftime(end_t, start_t);	// 时间差,结束时间-开始时间
    	printf("执行test函数耗用了%.2f秒", diff_t);
    	getchar();
    }
    

数学相关函数

math.h头文件定义了各种数学函数和一个宏。在这个库中所有可用的功能都带有-一个double类型的参数,且都返
回double类型的结果
举例说明:

  1. double exp(double x)
    返回e的x次幂的值。
  2. double log(double x)
    返回x的自然对数(基数为e的对数)
  3. double pow(double x, double y)
    返回x的y次幂。
  4. double sqrt(double x)
    返回x的平方根。
  5. double fabs(double x)
    返回x的绝对值。

代码实现

#include <stdio.h>
#include <math.h>

void main() {
	double res = fabs(-3);	// 绝对值
	double res2 = sqrt(2);	// 平方根
	double res3 = exp(0);	// e的几次幂
	double res4 = log(1);	// 返回x的自然对数
	double res5 = pow(4, 5);// x的y次幂,第一个参数为x
	printf("res=%.2f res2=%.2f res3=%.2f res4=%.2f res5=%.2f", res, res2, res3, res4, res5);
	getchar();
}

基本数据类型和字符串类型的转换

在程序开发中,我们经常需要将基本数据类型转成字符串类型(即char数组)。或者将字符串类型转成基本数
据类型。

sprintf 函数的用法
  1. sprintf和平时我们常用的printf函数的功能很相似。sprintf函数打印到字符串中,而printf函数打印输出到屏幕
    上。sprintf函数在我们完成其他数据类型转换成字符串类型的操作中应用广泛
  2. 该函数包含在stdio.h的头文件中

基本数据类型转字符串类型

#include <stdio.h>

void main() {
	char str1[30];
	char str2[30];
	char str3[30];
	int a = 20;
	double b = 30.0;
	sprintf(str1, "%d", a);
	sprintf(str2, "%.2f", b);
	sprintf(str3, "%8.2f", b);	// %8.2f 含义是输出8位,小数点占用两位,如果不够8位,就会用空格在前面占位
	printf("str1=%s str2=%s str3=%s", str1, str2, str3);
	getchar();
}

image-20210830102041894

字符串类型转基本数据类型

语法:通过<stdlib.h>的函数调用atoi .atof即可

注意事项

  1. 在将 char数组类型转成基本数据类型时,要确保能够转成有效的数据,比如我们可以把"123",转成一一个
    整数,但是不能把"hello"转成一-个整数
  2. 如果格式不正确,会默认转成0或者0.0

代码实现

#include <stdio.h>
#include <stdlib.h>		// 引入需要的函数

void main() {
	char str1[20] = "23423";
	char str2[20] = "343.3";
	char str3[20] = "ab";
	char str4[20] = "1111";
	int a = atoi(str1);
	double b = atof(str2);
	char c = str3[0];		// 索引
	long d = atol(str4);
	printf("a=%d b=%.2f c=%c d=%d", a, b, c, d);
	getchar();
}

九、预处理命令

1 基本介绍

预处理是在编译之前执行的操作

  1. 使用库函数之前,应该用#include引入对应的头文件。这种以#号开头的命令称为预处理命令。
  2. 这些在编译之前对源文件进行简单加工的过程,就称为预处理(即预先处理、提前处理)
  3. 预处理 主要是处理以#开头的命令,例如#include <stdio.h> 等。预处理命令要放在所有函数之外,而且-般都放
    在源文件的前面
  4. 预处理是C语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程
    序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译
  5. C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、
    修改、移植和调试,也有利于模块化程序设计

2 快速入门

具体要求

开发-一个C语言程序,让它暂停5秒以后再输出内容"春暖花开,世界和平",并且要求跨平台,在Windows和
Linux、下 都能运行,如何处理

提示

  1. Windows 平台下的暂停函数的原型是void Sleep(DWORD dwMilliseconds), 参数的单位是“毫秒”,位
    于<windows.h>头文件。
  2. Linux 平台下暂停函数的原型是unsigned int sleep (unsigned int seconds),参数的单位是“秒”,位于<unistd.h>
    头文件
  3. #if、 #elif、 #endif 就是预处理命令,它们都是在编译之前由预处理程序来执行的。

代码实现

#include <stdio.h>
#if _WIN32	// 如果是windows平台,执行#include <Windows.h>
#include <Windows.h>
#elif _linux_	// 如果是linux平台,#include <unistd.h>
#include <unistd.h>
#endif

void main() {
	// 不同的平台下调用不同的函数
	#if _WIN32
	Sleep(5000);	// 毫秒
	#elif _linux_
	Sleep(5);		// 秒
	#endif
	puts("春暖花开,世界和平");	// puts和printf一样的功能
	getchar();
}

3 C语言宏定义

  1. #define 叫做宏定义命令,它也是C语言预处理命令的- -种。所谓宏定义,就是用一个标识符来表示一个字符
    串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串
  1. 宏定 义我们在讲解常量时,做过介绍,这里我们再系统的讲解一下.

#defineN 100就是宏定义,N为宏名,100 是宏的内容(宏所表示的字符串)。在预处理阶段,对程序中所有出现
的“宏名”,预处理器都会用宏定义中的字符串去代换,这称为**“宏替换”或“宏展开”**。
宏定义是由源程序中的宏定义命令#define完成的,宏替换是由预处理程序完成的

4 宏定义的形式

  1. #表示这是一条预处理命令,所有的预处理命令都以#开头。宏名是标识符的一种,命名规则和变量相同。字.
    符串可以是数字、表达式、if语句、函数等
  2. 这里所说的字符串是- -般意义.上的字符序列,不要和C语言中的字符串等同,它不需要双引号
  3. 程序中反复使用的表达式就可以使用宏定义

代码演示

#include <stdio.h>

#define M (n*n+3*n)	// 只是简单的替换,如果不带括号又是另一种结果了
void main() {
	int sum, n;
	printf("请输入一个数字");
	scanf("%d", &n);
	sum = 3 * M + 4 * M + 5 * M;	// 宏展开	3*(n*n+3*n) + 4*(n*n+3*n) + 5*(n*n+3*n) 
	printf("sum=%d\n", sum);
	getchar();
	getchar();
}

宏定义注意事项和细节

  1. 宏定义是用宏名来表示-一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单的替换。字符串中可
    以含任何字符,它可以是常数、表达式、if语句、函数等,预处理程序对它不作任何检查,如有错误,只能在
    编译已被宏展开后的源程序时发现。

  2. 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换

  3. 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令[案
    例]

#define PI 3.14159
int main(){
printf("PI=%f", PI); .
return 0;
}
#undefPI //取消宏定义
void func({
// Code
printf("PI=%f", PD);//错误,这里不能使用到PI了
  1. 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替[案例]
#include <stdio.h>
#define OK 100
int main(){
	printf("OK\n");	// 这里就相当于是字符串输出,不能进行替换
	return 0;
}
  1. 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换[ 案例]
#define PI 3.1415926
#define S PI*y*y
/* PI是已定义的宏名*/
print("%f", S);
// 在宏替换后变为:
print("%f", 3.1415926*y*y);
  1. 习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母

  2. 可用宏定义表示数据类型,使书写方便[案例]

    #define UINT unsigned int
    void main() {
    	UINT a,b;	//宏替换 unsigned int a, b;
    }
    
  3. 宏定义表示数据类型和用typedef定义数据说明符的区别:宏定义只是简单的字符串替换,由预处理器来处理;
    typedef是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一-个新的名字,
    将它作为–种新的数据类型。

5 带参数的宏定义

  1. C语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和
    函数有些类似
  2. 对带 参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参
  3. 带参宏定义的一般形式为**#define 宏名(形参列表)字符串**,在字符串中可以含有各个形参
  4. 带参宏调用的一般形式为:宏名(实参列表); [案例+说明]

代码演示

#include <stdio.h>

// 1 MAX就是带参数的宏
// 2 (a, b)就是形参
// 3 (a>b)?a: b是带参数的宏对应字符串,该字符串中可以使用形参.
#define MAX(a, b) (a > b) ? a : b
void main() { 
	int x, y, max;
	printf("input two numbers\n");
	scanf("%d%d", &x, &y);
	max = MAX(x, y);	// 宏替换	max = (x > y) ? x : y
	printf("max=%d", max);
	getchar();
	getchar();
}

带参宏定义的注意事项和细节

  1. 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现

    #define MAX(a,b) (a>b)?a:b
    如果写成了#define MAX (a, b) (a>b)?a:b
    将被认为是无参宏定义,宏名MAX代表字符串(a,b) (a>b)?a:b
    而不是: MAX(a,b)代表(a>b)?a:b 了
    
  2. 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,
    要用它们去替换形参,因此实参必须要指明数据类型

  3. 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。

    #include <stdlib.h>
    #define SQ(y) (y)*(y)//带参宏定义,字符串内的形参通常要用括号括起来以避免出错
    int main(){
        int a, sq;
        printf("input a number: "); .
        scanf("%d", &a);
        // 如果没有括号就会变成:a + 1 * a + 1 = a + a + 1	就不是我们想要的结果了
        sq= SQ(a+1);//宏替换(a+1)* (a+1)
        printf("sq=%d\n", sq);
        system("pause");
        return 0;
    }
    

6 带参宏定义和函数的差别

  1. 宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也
    不会占用内存。
  2. 函数是一 段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码.
  3. 案例说明 :要求使用函数计算平方值,使用宏计算平方值, 并总结二者的区别
#include <stdio.h>
#include <stdlib.h>

//int SQ(int y) {
//	return y * y;
//}

#define SQ(y) (y)*(y)
void main() {
	int i = 1;
	while (i <= 5) {
		//printf("%d^2=%d\n", i - 1, SQ(i++));
		printf("%d^2=%d\n", i - 2, SQ(i++));	// 宏展开 (i++) * (i++)
		// 宏定义它是直接替换,因此在这里它++了两次,所以i的变化时1 , 3 ,5
		// 函数它的i的变换是1,2,3,4,5 所以这就是两者的区别,要注意一下
	}
	system("pause");
}

7 预处理命令总结

预处理指令是以#号开头的代码行,#号必须是该行除了任何空白字符外的第-一个字符。#后是指令关键字,
在关键字和#号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编
译之前对源代码做某些转换

image-20210830115920840

预处理指令使用注意事项

  1. 预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理
    命令来调用这些功能。
  2. 宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传送”。
  3. 为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。
  4. 文件包含是预处理的-一个重要功能,它可用来把多个源文件连接成一- 个源文件进行编译,结果将生成-一个 目标
    文件。
  5. 条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程
    序的效率。
  6. 使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计

十、数组

数组可以存放多个同–类型数据。数组也是一种数据类型,是构造类型。传递是以引用的方式传递(即传递的
是地址)

1 快速入门

#include <stdio.h>

void main() {
	/*一个养鸡场有6只鸡,它们的体重分别是3kg, 5kg, 1kg,
		3.4kg, 2kg, 50kg 。请问这六只鸡的总体重是多少? 
		平均体重是多少 ? 请你编一一个程序。
	*/

	double arr[6];
	double total = 0.0;
	double avg = 0.0;
	int i, length;
	
	arr[0] = 3;
	arr[1] = 5;
	arr[2] = 1;
	arr[3] = 3.4;
	arr[4] = 2;
	arr[5] = 50;

	// 数组长度 = 数据的总字节 / 每个数据的字节
	length = sizeof(arr) / sizeof(double);
	for (i = 0; i < length; i++)
	{
		total += arr[i];
	}
	avg = total / length;
	printf("total=%.2f avg=%.2f", total, avg);
	getchar();
}

2 数据的定义和内存布局

①数据的定义

数据类型 数组名[数组大小];
inta[5]; //a 数组名,类型int,[5] 大小,即a数组最多存放5个int数据
赋初值a[0]= 1;a[1]= 30; …

②数据的内存分布

image-20210903102324822

说明:1.数组名就代表该数组的首地址,既a[0]的地址

​ 2.数组各个元素是连续分布的

​ 例:int类型的数组 -> a[0] 的地址为0x1233 ,那么a[1]的就是0x1233 + int的字节数 = 0x1237, a[2]的地址0x123B,后面以此类推。。。;是什么类型的数组后面就加对应的字节数

小案例

#include <stdio.h>

void main() {

	double arr[5];
	int i, length;
	length = sizeof(arr) / sizeof(double);
	for (i = 0; i < length; i++)
	{
		printf("\n 请输入你的分数");
		scanf("%lf", &arr[i]);	// 是l不是1
	}

	for (i = 0; i < length; i++)
	{
		// 输出成绩
		printf("%d=%.2f", i, arr[i]);
	}

	getchar();	// 过滤回车
	getchar();
}

③三种初始化数组的方式

// 第一种
int arr[2];
arr[0] = 200;
// 第二种
int arr2[3] = { 1, 2, 3 };
// 第三种
int arr3[] = { 5, 4, 6 };

3 数据使用注意事项和细节

  1. 数组是多个相同类型数据的组合,一个数组一-旦声明/定义了,其长度是固定的,不能动态变化
  2. 数组创建后, 如果没有赋值,则遵守如下规则
    全局数组默认值0
    非全局数组初值是机器垃圾值(即:原来系统分配给这块空间的值)
  3. 使用 数组的步骤1. 定义数组2给数组各个元素赋值3使用数组,也可以一一步到位
  4. 数组的 下标是从0开始的,不是从1开始。
  5. 数组下标必须在指定范围内使用,编译通过,在运行时会因为数组越界而异常中断:
    比如intarr[5] 有效下标为0-4
  6. C的数组属构造类型,是引用传递(传递的是地址),因此当把一个数组传递给–个函数时/或者变量,函数/变
    量操作数组会影响到原数组(因为传参的时候传过去的是指针,也就是地址)

4 数组应用案例

1)创建一个 char类型的26个元素的数组,分别放置’A’-Z‘。使用for循环访问所有元素并打印出来。提示:字符数据运算’A’+1 -> ‘B’

#include <stdio.h>

void main() {
	/*
		创建一个 char类型的26个元素的数组,分别放置'A' - Z‘。使用for循环访问所有元素并打印出来。
	    提示:字符数据运算'A' + 1 -> 'B'
	*/
	char arr[26];
	int i;
	for (i = 0; i < 26; i++)
	{
		arr[i] = 'A' + i;
	}
	for ( i = 0; i < 26; i++)
	{
		printf("%c ", arr[i]);
	}
	getchar();
}

2)请求出一个数组的最大值,并得到对应的下标。

#include <stdio.h>

void main() {
	// 请求出一个数组的最大值,并得到对应的下标。
	int arr[] = {2, 3, 456, 554, 34, 5464};
	int i, length, maxIndex;
	double max = arr[0];
	length = sizeof(arr) / sizeof(int);
	for (i = 1; i < length; i++)
	{
		if (arr[i] > max) {
			max = arr[i];
			maxIndex = i;
		}
	}
	printf("最大值为%.2f, 对应的下角标为%d", max, maxIndex);
	getchar();
}

5 字符数组与字符串

字符数组实际上是一系列字符的集合,也就是字符串(String) 。在C语言中,没有专门的字符串变量,没有
string类型,通常就用一个字符数组来存放-一个字符串

#include <stdio.h>

void main() {

	char str[] = "莫个超是靓仔";
	printf("str=%s", str);
	getchar();
}

本 次

1 字符数组注意事项

  1. 在C语言中,字符串实际上是使用null 字符(\0’) 终止的一维字符数组。因此,一个以null 结尾的字符串,
    包含了组成字符串的字符。
  2. ^\0’是 ASCII码表中的第0个字符,用NUL表示,称为空字符。该字符既不能显示,也不是控制字符,输出该
    字符不会有任何效果,它在C语言中仅作为字符串的结束标志。
  3. 字符数组(字符 串)在内存中的布局分析[案例]

image-20210903115355323

说明:"?"表示是不知道的东西,可能是垃圾值,也可能是其他的东西

结论如果在给某个字符数组赋值时,(1 )赋给的元素的个数小于该数组的长度,则会自动在后面加\0’, 表示
字符串结束,(2)赋给的元素的个数等于该数组的长度,则不会自动添加\O’
char str2[]= {‘t,m’,‘o’} 输出什么?输出的是tmo 乱码.

image-20210903191323933

补充

char str2[] = { 't', 'o', 'm' };	// 这个也是不会自动添加'/0'的

2 字符串的访问和遍历

因为字符串的本质就是字符数组,因此可以按照数组的方式遍历和访问某个元素

代码演示

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

void main() {
	char str[] = "hello";
	int i;
	int length = strlen(str);
	printf("str=%s", str);
	printf("\n str的长度是%d", length);
	printf("\n 字符串第三个字是%c", str[2]);
	// 遍历字符数组
	for ( i = 0; i < length; i++)
	{
		printf("\n %c", str[i]);
	}
	system("pause");
}

6 字符串的表现形式

① 用字符数组存放字符串

char str[] = "我是靓仔智";
char str2[] = {'h','e','l','l','o'}

② 用字符指针指向一个字符串

  1. C语言对字符串常量"hellotom"是按字符数组处理的,在内存中开辟了一一个字符数组用来存放字符串常量,程
    序在定义字符串指针变量str时==只是把字符串首地址(即存放字符串的字符数组的首地址)赋给pStr==.

  2. printf("%s\n",str); 可以输出str 指向的字符串

  3. 对应的内存布局图(!!)

    image-20210903194802994

③ 两种方法表示字符串的讨论

  1. 字符数组由若千个元素组成,每个元素放一一个字符;而字符指针变量中存放的是地址(字符串/字符数组的首地址),绝不是将字符串放到字符指针变量中(是字符串首地址)[图]

  2. 对字符数组只能对各个元素赋值,不能用以下方法对字符数组赋值

    char str[5];
    str = "hello";	// 错误
    str[2] = 'i';	// 正确
    system("pause");
    

    image-20210903200418514

  3. 对字符指针变量,采用下面方法赋值,是可以的

    char* str = "我是靓仔智";
    str = "世界和平,春暖花开";		// 指向一个新开的空间呗
    
  4. 如果定义了一个字符数组,那么它有确定的内存地址(即字符数组名是一个常量);而定义一个字符指针变量时,
    它并未指向某个确定的字符数据,并且可以多次赋值[代码+图解]

7 字符串相关函数

① 常用函数

image-20210903200756352

② 字符串函数应用案例

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

void main() {
	char str[] = "hello";
	char str2[] = "world";
	char str3[] = "";
	strcpy(str3, str2);		// s2复制到s1
	printf("复制的是%s\n", str);
	strcat(str, str2);	// 追加
	printf("append为%s\n", str);
	int result = strcmp(str, str2);	// 判断两个字符串是否相同,相同返回0
	printf("result=%d\n", result);
	char* p = strchr(str, 'e');		// 要用指针类型接收,没有找到返回null
	printf("p=%s\n", p);
	char* p2 = strstr(str, "ll");	// 要用指针类型接收,没有找到返回null
	printf("p2=%s\n", p2);
	getchar();
}

8 字符串使用注意项和细节

  1. 程序中往往依靠检测\0’ 的位置来判定字符串是否结束,而不是根据数组的长度来决定字符串长度。因此,字
    符串长度不会统计"\0’, 字符数组长度会统计[案例]

    #include <stdio.h>
    #include <string.h>
    
    void main() {
    	char str[] = "hello";
    	int len = strlen(str);		// 这个输出的就是5
    	int arrLen = sizeof(str);	// 数组输出就是6,因为还有一个'\0'
    	printf("%d  %d", len, arrLen);	// 5	6
    	getchar();
    }
    
  2. 在定 义字符数组时应估计实际字符串长度,保证数组长度始终大于字符串实际长度,否则, 在输出字符数组
    时可能出现未知字符.

    image-20210904095856417

  3. 系 统对字符串常量也自动加一个\0’作为结束符。例如"C Program”共有9个字符,但在内存中占10个字节,
    最后一个字节\0’是系统自动加上的。( 通过sizeof()函数可验证)

  4. 定 义字符数组时,如果给的字符个数比数组的长度小,则系统会默认将剩余的元素空间,全部设置为’\0’, 比
    如char str[6] = “ab” , str内存布局就是 [a] [b] [\0] [\0] [\0] [\0]

字符数组练习

image-20210904100659937

9 多维数组 - 二维数组

数组里面放了个数组(套娃)

语法:类型 数组名[大小] [大小]

①快速入门

image-20210905102026679

代码演示

#include <stdio.h>

void main() {
	int i, j;
	int arr[4][6];

	for (i = 0; i < 4; i++)
	{
		for (j = 0; j < 6; j++)
		{
			arr[i][j] = 0;
		}
	}

	arr[1][2] = 1;
	arr[2][1] = 2;
	arr[2][3] = 3;

	for (i = 0; i < 4; i++)
	{
		for (j = 0; j < 6; j++)
		{
			printf("%d", arr[i][j]);
		}
		printf("\n");
	}

	getchar();
}

②二维数组内存分布

#include <stdio.h>

void main() {
	int i, j;
	int arr[4][6];

	for (i = 0; i < 4; i++)
	{
		for (j = 0; j < 6; j++)
		{
			arr[i][j] = 0;
		}
	}
	printf("二维数组arr的首地址=%p\n", arr);
	printf("二维数组arr[0]的地址=%p\n", arr[0]);
	printf("二维数组arr[0][0]的地址=%p\n", &arr[0][0]);
	printf("二维数组arr[0][1]的地址=%p\n", &arr[0][1]);
	getchar();
}

说明:arr、arr[0]和&arr[0] [0]的地址是一样的,都是首地址

内存分析图

image-20210905104849477

注意:他们并不是一行行分开的,而是连续的地址

③直接初始化数组

  1. 定义 类型 数组名[大小] [大小] = {{值1, 值2, 值3},{值1, 值2, 值3},{值1, 值2, 值3}};
  2. 或者 类型 数组名[大小] [大小] = 值1,值2,值3,值4,值5,值…};

image-20210905104938760

④二维数组练习

#include <stdio.h>

/*	
	定义二维数组,用于保存三个班,每个班三名同学成绩,
	并求出每个班级平均分、以及所有班级平均分
*/

void main() {
	int i, j;
	// 定义一个二维数组
	double score[3][3];
	// 行数 = 总字节数 / 每一行的字节数
	int rows = sizeof(score) / sizeof(score[0]);
	int cols = sizeof(score[0]) / sizeof(double);
	double totalScore = 0.0;	// 所有学生总成绩
	double avgScore = 0.0;		// 各班平均分

	// 初始化
	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++)
		{
			score[i][j] = 0;	
		}
	}
	// 从键盘输入成绩
	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++)
		{
			printf("请输入%d班第%d个学生的成绩:", i + 1, j + 1);
			scanf("%lf", &score[i][j]);
		}
	}
	printf("\n\n");
	// 打印所有学生的成绩
	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++)
		{
			printf("%.2f ", score[i][j]);
		}
		printf("\n");
	}
	printf("\n\n");
	// 计算所有学生总分和各班平均分
	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++)
		{
			totalScore += score[i][j];
			avgScore += score[i][j];
		}
		printf("%d班的平均分为:%.2f\n", i + 1, avgScore / 3);
		avgScore = 0.0;
	}
	printf("\n\n所有学生的总分:%.2f", totalScore);
	getchar();
	getchar();
}

⑤二维数组使用细节和注意事项

  1. 可以只对部分元素赋值,未赋值的元素自动取“零”值[案例]

    int main({
        int a[4][5]= {{1}, {2}, {3},{}};
        int ij;
        for(i=0;i<4;i++) {
        for(j=0;j<5;j++){
        printf("%d ",a[i][j]);
        }
        print("\n"); .
        getchar(;
    }
    
  2. 如果对全部元素赋值,那么第一维的长度可以不给出。比如:

    int a[3][3]= {1,2,3,4,5,6, 7,8, 9};
    可以写为:
    inta[][3]= {1,2,3,4,5,6, 7,8, 9};
    
  3. 二维数组可以看作是由一维数组嵌套而成的;如果- - 个数组的每个元素又是-一个数组,那么它就是二维数组。

    二维数组a[3][4]可看成三个- -维数组,它们的数组名分别为a[0]、 a[1]、 a[2]。
    这三个-维数组都有4个元素,如,一维数组 a[0] 的元素为a[0][0]、 a[0][1]、 a[0][2]、 a[0][3]
    

十一、排序和查找

1 冒泡排序

image-20210904110128294

代码实现

#include <stdio.h>

// 冒泡排序函数
void bubbleSore(int arr[], int len) {
	int i, j, t;
	for (i = 0; i < len - 1; i++)
	{
		for (j = 0; j < len - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				t = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = t;
			}
		}
	}
}

void main() {
	int arr[] = {65, 43, 75, 23, 100};
	
	int i, len;
	len = sizeof(arr) / sizeof(int);
	bubbleSore(arr, len);
	printf("排序后\n");
	for (i = 0; i < len; i++)
	{
		printf("%d\n", arr[i]);
	}
	getchar();
}

2 查找

①顺序查找

逐个比较

#include <stdio.h>

int searchSeq(int arr[], int length, int index) {
	int i;
	for (i = 0; i < length; i++)
	{
		if (arr[i] == index)
		{
			return arr[i];
		}
	}

}

void main() {
	int length;
	int arr[] = {2, 4, -2, 22, 90};
	length = sizeof(arr) / sizeof(int);
	int index = searchSeq(arr, length, 90);
	printf("index=%d", index);
	getchar();
}

②二分查找

前提:必须是有序数组

说明:从中间开始查找,大于中间的数就往右边查找,小于就往左边查

代码实现

#include <stdio.h>

// 二分法查找
int binarySearch(int arr[], int leftIndex, int rightIndex, int findVal) {
	int i;
	int midIndex = (leftIndex + rightIndex) / 2;	// 中间值的索引
	int midVal = arr[midIndex];						// 中间值
	if (midVal > findVal)	// 中间值大于要找的值,往左边找
	{
		for (i = midIndex - 1; i > leftIndex; i--)
		{
			if (arr[i] == findVal)
			{
				return i;
			}
		}
	}
	else if (midIndex < findVal) {	// 中间值小于要找的值,往右边找
		for (i = midIndex + 1; i < rightIndex; i++)
		{
			if (arr[i] == findVal)
			{
				return i;
			}
		}
	}
	else
	{
		return 0;	// 返回该数的下标
	}
}

void main() {
	int arr[] = { 2, 30, 34, 45, 90 };
	int arrLen = sizeof(arr) / sizeof(int);
	int index = binarySearch(arr, 0, arrLen, 90);
	if (index != 1) {
		printf("找到了,它的索引值是:%d", index);
	}
	else
	{
		printf("没有找到");
	}
	getchar();
}

十二、断点调试

image-20210905111057936

十三、指针

1 指针的基本介绍

和java中的引用数据类型类似,指针就是一个可以操作对应地址的变量的东西

  1. 指针是C语言的精华,也是C语言的难点。
  2. 指针, 也就是内存的地址;所谓指针变量,也就是保存了内存地址的变量。关于指针的基本使用,在讲变量的
    时候做了入门级的介绍
  3. 获取 变量的地址,用&,比如:
    int num= 10,获取num的地址: &num
  4. 指针类型, 指针变量存的是一个地址,这个地址指向的空间存的才是值
    比如: intptr = # ptr 就是指向int 类型的指针变量,即ptr是int 类型。
  5. 获取指针类 型所指向的值,使用: (取值符号), 比如: int ptr,使用*ptr 获取ptr指向的值

什么是指针

指针是一一个变量,其值为另一个变量的地址(前示意图已经说明),即,内存位置的直接地址。就像其他变量或
常量一样,在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:

int *ip;	/*一个整型的指针*/
double *dp;	/*一个double 型的指针*/
float *fp; 	/*一个浮点型的指针*/
char *ch;  	/*一个字符型的指针*/

image-20210816205701239

2 指针的算数运算

指针是一个用数值表示的地址。可以对指针执行算术运算。可以对指针进行四种算术运算: ++、-、+. -。

就是对指针中的地址进行算数运算,以单位来计算的,是什么类型就是什么单位,比如int类型的指针就是一个单位4个字节

①指针递增操作(++)

代码演示

#include <stdio.h>

void main() {
	int i, length, *ptr;
	int val[] = {23, 53, 45};
	length = sizeof(val) / sizeof(int);
	ptr = val;
	for (i = 0; i < length; i++)
	{
		printf("val[%d] 地址=%p \n", i, ptr);
		printf("val[%d]=%d \n", i, *ptr);
		ptr++;	// ptr = ptr的地址值 + 一个单位(1个int类型的字节数)
	}
 	getchar();
}

内存示意图

image-20210905185552048

②指针递减操作(–)

原理和++一样

#include <stdio.h>

void main() {
	int i, *ptr, length;
	int var[] = {23, 34, 54};
	length = sizeof(var) / sizeof(int);
	ptr = &var[length - 1];	// 指针指向最后一个值
	for ( i = length - 1; i >= 0; i--)
	{
		printf("val[%d] 地址=%p \n", i, ptr);
		printf("val[%d]=%d \n", i, *ptr);
		ptr--;	// ptr = ptr的地址值 - 一个单位(1个int类型的字节数)
	}
	getchar();
}

注意:指针接收的是地址,不是值

③指针+、-操作

和普通的类型,不过指针是以单位来计算的,单位也就是对应指针类型的字节数

#include <stdio.h>

void main() {
	int arr[] = {10, 30, 230};
	int i, * ptr;
	ptr = arr;
	ptr += 2;	// ptr的地址 + 2个int字节(8个字节)
	printf("arr[2]=%d var[2]的地址=%p\nptr存储的地址=%p ptr指向的值=%d", arr[2], &arr[2], ptr, *ptr);
	getchar();
}

④练习

#include <stdio.h>

// 指向第三个元素
void main() {
	int i, * ptr;
	int var[] = { 20, 30, 50 };
	ptr = &var[2];
	printf("ptr指向的值为%d, 地址为%p a[2]的地址为%p", *ptr, ptr, &var[2]);
	getchar();
}

3 指针的比较

指针存储地址的比较,可以使用关系运算符进行比较,类型要一致,一定是两个指针类型,同时还要是相同的类型

比如:int类型的指针要和int类型的指针比较

代码演示

#include <stdio.h>

void main() {
	int var[] = {10, 20, 30};
	int* ptr ;
	ptr = var;
	if (ptr == var)		// 可以
	{
		printf("ok1");	// 输出
	}
	//if (ptr == var[0]);	// 类型不同,不能比较,报错
	//{
	//	printf("\nok2");
	//}
	if (ptr == &var[0])		// 可以
	{
		printf("\nok3");	// 输出
	}
	if (ptr >= &var[1])		// 可以
	{
		printf("\nok4");	// 不输出,为false
	}

	getchar();
}
#include <stdio.h>

const int MAX = 3;
void main() {
	int var[] = { 10, 20, 300 };
	int i, * ptr;
	ptr = var;
	i = 0;
	while (ptr <= &var[MAX - 2])	// &var[1]
	{
		printf("Address of var[%d]=%x\n", i, ptr);
		printf("Value of var[%d]=%d\n", i, *ptr);
		ptr++;
		i++;
	}	// 输出10、20
	getchar();
}

4 指针数组

存放指针的数组呗

①指针数组的定义

数据类型 *指针数组名[大小];

  1. 比如:int *ptr[3];
  2. ptr声明为一个指针数组
  3. 由3个整数指针组成。因此,ptr中的每个元素,都是一个指向int类型值的指针

②快速入门和内存布局图

#include <stdio.h>

const int MAX = 3;
void main() {
	int i, *ptr[3];
	int arr[] = { 10, 20, 30 };
	// 将指针数组中的指针指向值
	for ( i = 0; i < MAX; i++)
	{
		ptr[i] = &arr[i];
	}
	// 遍历通过指针输出各个值
	for ( i = 0; i < MAX; i++)
	{
		printf("Value of arr[%d]= %d ptr[%d]本身的地址%p\n", i, *ptr[i], i, &ptr[i]);
	}
	getchar();
}

内存布局图

image-20210908112301869

③应用实例

请编写程序,定义一个指向字符的指针数组来存储字符串列表(四大名著书名),并通过遍历 该指针数组,显
示字符串信息,(即: 定义一个指针数组,该数组的每个元素,指向的是- -个字符串)

代码实现

#include <stdio.h>

void main() {
	/*	请编写程序,定义一个指向字符的指针数组来存储字符串列表(四大名著书名),
		并通过遍历 该指针数组,显
		示字符串信息,(即: 定义一个指针数组,该数组的每个元素,指向的是 - -个字符串)
	*/
	int i;
	char* books[4] = {
		"三国演义",
		"红楼梦",
		"水浒传",
		"西游记"
	};

	for ( i = 0; i < 4; i++)
	{
		printf("\nbooks[%d]=%s 指向的地址是%p", i, books[i], &books[i]);
	}
	getchar();
}

5 指向指针的指针(多重指针)

就是套娃,指针里面存放这另外一个指针的地址,也就是相当于两个指针都可以操作那个数据,但是多级的麻烦一点

比如:二级指针存放的是一级的指针地址,一级指针存放的是对应变量的地址,二级指针要操作对应的变量要通过一级指针,然后一级指针来操作对应的变量

代码实现

#include <stdio.h>

void main() {
	int var;
	int *ptr;	// 一级指针
	int **pptr;	// 二级指针
	int ***ppptr;
	var = 8;
	ptr = &var;
	pptr = &ptr;
	ppptr = &pptr;

	printf("var的地址 %p var=%d\n", &var, var);
	printf("ptr 本身的地址=%p ptr存放的地址=%p *ptr=%d\n", &ptr, ptr, *ptr);
	printf("pptr 本身的地址=%p pptr存放的地址=%p **ptr=%d\n", &pptr, pptr, **pptr);
	printf("ppptr 本身的地址=%p ppptr存放的地址=%p **ppptr=%d\n", &ppptr, ppptr, ***ppptr);
	getchar();
}

内存示意图

image-20210908204207094

6 传递指针(地址)给函数

①传地址或指针给指针变量

#include <stdio.h>

void test(int* p);	// 函数声明
void main() {
	int i, num = 90;
	int* p = &num;
	test(p);
	printf("main中的num=%d", num);		// 91
	test(p);
	printf("\nmain中的num=%d", num);		// 92
	getchar();
}
void test(int* p) {
	*p += 1;
}

内存示意图

image-20210908212705497

② 传数组给指针变量

#include <stdio.h>

// 传入数组
double getAverage(int* arr, int size);	// 函数声明
double getAverage2(int* arr, int size);	// 函数声明
void main() {
	// 带有5个整数的整型数组
	int balance[5] = { 1000, 2, 34, 54, 332 };
	double avg;
    // 这两个函数的目的是一样的
	//avg = getAverage(balance, 5);
	avg = getAverage2(balance, 5);
	printf("\nAverage value is:%f\n", avg);
	getchar();
}

double getAverage(int* arr, int size) {
	int i, sum = 0;
	double avg;
	for ( i = 0; i < size; i++)
	{
		sum += arr[i];	// arr[0]
		printf("\n arr存放的地址=%p", arr);
	}
	avg = (double)sum / size;
	return avg;
}

double getAverage2(int* arr, int size) {
	int i, sum = 0;
	double avg;
	for ( i = 0; i < size; i++)
	{
		sum += *arr;
		printf("\narr存放的地址=%p", arr);
		arr++;
	}
	avg = (double)sum / size;
	return avg;
}

7 函数返回值为指针

C语言允许函数的返回值是-一个指针(地址),这样的函数称为指针函数。返回值为指针呗

① 快速入门

代码实现

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

// 比较两个字符串长度的函数
char* strlong(char* str1, char* str2) {
	printf("\n str1的长度 %d str2的长度 %d", strlen(str1), strlen(str2));
	if (strlen(str1) >= strlen(str2))
	{
		return str1;
	}
	else {
		return str2;
	}
}

void main() {
	char str1[30], str2[30], * str;	// str是一个指针,指向字符串
	printf("\n 请输入第1个字符串");
	gets(str1);
	printf("\n 请输入第2个字符串");
	gets(str2);
	str = strlong(str1, str2);
	printf("\n Longer string:%s \n", str);
	getchar();
}

② 指针函数注意事项和细节

  1. 用指针作为函数返回值时需要注意,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局
    部数组和形式参数,函数返回的指针不能指向这些数据[案例演示]
  2. 函数运行结束后会销毁该函数所有的局部数据,这里所谓的销毁并不是将局部数据所占用的内存全部清零,而
    是程序放弃对它的使用权限,后面的代码可以使用这块内存[案 例演示]
  3. C语言不支持在调用函数时返回局部变量的地址,如果确实有这样的需求,需要定义局部变量为static变量
    [案例演示]

案例演示

#include <stdio.h>

// 返回一个指针类型
int* fun() {
	//int n = 10;	// 局部变量,在func返回时,就会销毁,但是引用还是在的
	
	static int n = 10;	// static修饰的变量存放在静态数据区
	return &n;
}

void main() {
	int* p;
	p = fun();
	printf("得到的值为%d", *p);
	getchar();
}

③ 应用案例

编写一个函数,它会生成10个随机数,并使用表示指针的数组名(即第-一个数组元素的地址)来返回它们。

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

// 随意生成一个10个元素的数组返回
int* returnArr() {
	int i;
	static int arr[10];
	for ( i = 0; i < 10; i++)
	{
		arr[i] = rand();
	}
	return arr;
}

void main() {
	int i, *p;
	p = returnArr();
	for ( i = 0; i < 10; i++)
	{
		printf("%d\n", *(p + i));
	}

	getchar();
}

8 函数指针

① 基本介绍

  1. 一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,
    这和数组名非常类似。
  2. 把函数的这个首 地址(或称入口地址)赋予-一个指针变量,使指针变量指向函数所在的内存区域,然后通过指
    针变量就可以找到并调用该函数。这种指针就是函数指针。

指向函数的指针

② 函数指针定义

returnType (*pointerName)(param list);

  1. returnType 为函数指针指向的函数返回值类型
  2. pointerName 为函数指针名称
  3. paramlist为函数指针指向的函数的参数列表
  4. 参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称
  5. 注意( )的优先级高于*,第-一个括号不能省略,如果写作returnType pointerName(param list);就成了函数原型,
    它表明函数的返回值类型为returnType

③ 应用案例

用函数指针来实现对函数的调用,返回两个整数中的最大值.

代码实现

#include <stdio.h>

int max(int x, int y) {
	return x > y ? x : y;
}

void main() {
	int x, y, maxVal;

	// 说明	函数指针
	// 1 该函数指针的名字 pmax
	// 2 int表示该函数指针的函数时返回int类型
	// 3 (int, int)表示该函数指向的函数形参时接收两个int
	// 4 在的定义函数指针时,也可以写上参数名 int (*pmax)(int x, int y) = max;
	// 也可以int (*pmax)(int x, int y)
	int (*pmax)(int, int) = max;
	printf("请输入两个数:");
	scanf("%d %d", &x, &y);
	maxVal = (*pmax)(x, y);
	printf("max value为:%d pmax的值为:%p pmax本身的地址为:%p", maxVal, pmax, &pmax);
	getchar();
	getchar();
}

9 函数回调

就是使用函数指针

  1. 函数指针变量可以作为某个函数的参数来使用的,回调函数就是-一个通过函数指针调用的函数。
  2. 简单的讲:回调函数是由别人的函数执行时调用你传入的函数(通过函数指针完成)

代码演示

/* 回调函数 */
#include <stdio.h>
#include <stdlib.h>

// 初始化数组
void initArr(int *arr, int length, int (*r)()) {	// 也可以这样int r()
	int i;
	for ( i = 0; i < length; i++)
	{
		// 也可以写成 arr[i] = (*r)();
		arr[i] = r();
	}
}

// 随机生成一个数字返回
int getRand() {
	return rand();
}

void main() {
	int i, arr[10];
	// 函数名就是它的地址
	initArr(arr, 10, getRand);

	for ( i = 0; i < 10; i++)
	{
		printf("%d\n", arr[i]);
	}
	getchar();
}

10 指针的注意事项和细节

  1. 指针变量存放的是地址,从这个角度看指针的本质就是地址。
  2. 变量声明的时候,如果没有确切的地址赋值,为指针变量赋-一个NULL值是好的编程习惯。
  3. 赋为NULL值的指针被称为空指针,NULL指针是一个定义在标准库<stdio.h>中 的值为零的常量#define
    NULL 0 [案例]
  4. 指针使用一览(见后)

代码演示

#include <stdio.h>

void main() {
	int* p = NULL;	// p是空指针
	int i = 32;
	p = &i;
	printf("%d", *p);
	getchar();
}

11 动态内存分配

  1. 全局变量一一内存中的静态存储区
  2. 非静态的局部变量一—内存中的动态存储区一–stack栈
  3. 临时使用的数据–建立动态内存分配区域,需要时随时开辟,不需要时及时释放一–heap堆
  4. 根据需要向系统申请所需大小的空间,由于未在声明部分定义其为变量或者数组,不能通过变量名或者数组名
    来引用这些数据,只能通过指针来引用)

image-20210912103733775

① 动态内存分配的相关函数

头文件#include <stdlib.h> 声明了四个关于内存动态分配的函数中。

  1. void* malloc(usigned int size)

    在内存的动态存储区(堆区)中分配一个长度为size的连续空间,返回值为所分配空间的第一个字节的地址

    eg:malloc(100); 开辟了100字节的临时空间,返回值为其第一个字节的地址

  2. void *calloc(unsigned n, unsigned size)

    在堆中分配n个长度为size的连续空间,一般用来存储数组,分配不成功,返回NULL

    eg:p = calloc(50,4); // 开辟50 * 4 个字节临时空间,把起始地址分配给指针变量p

  3. void free(void* p)

    释放变量p指向的动态空间

  4. void *realloc(void *p, unsigned int size)

    重新分配malloc或calloc函数获得的动态空间大小,将p指向的动态空间大小改变为size,p的值不变,分配失败返回NULL

    eg:realloc(p, 50); // 将p所指向的已分配的动态空间改为50字节

  5. void指针类型

    不指向具体的类型数据,只是提供一个纯地址,不指向任何具体的对象,就是存放地址的指针呗

    image-20210912105626435

    说明:当吧void指针赋给不同类型的指针变量(或相反时),编译系统会自动进行转换

    image-20210912105736901

② 应用案例

动态创建数组,输入5个学生的成绩,另外一个函数检测成绩低于60分的,输出不合格的成绩。

代码演示

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

void cheek(int*);	// 函数声明
void main() {
	int i, *p;
	// 堆区中开辟一个5 * 4的空间,并将地址(void*)转成(int*)赋给p
	p = (int*)malloc(5 * sizeof(int));	// 也可以写成malloc(5 * sizeof(int))
	for ( i = 0; i < 5; i++)
	{
		scanf("%d", p + i);
	}
	cheek(p);
	free(p);	// 销毁堆区中p指向的空间
	getchar();
	getchar();
}
void cheek(int* p) {
	int i;
	printf("\n不及格成绩的有:");
	for ( i = 0; i < 5; i++)
	{
		if (p[i] < 60)
			printf("%d\t", p[i]);
	}
}

内存示意图

image-20210912112215015

③动态分配内存的基本原则

  1. 避免分配大量的小内存块。 分配堆上的内存有一些系统开销,所以分配许多小的内存块比分配几个大内存块的
    系统开销大

  2. 仅在需要时分配内存。只要使用完堆上的内存块,就需要及时释放它(如果使用动态分配内存,需要遵守原则:
    谁分配,谁释放),否 则可能出现内存泄漏

  3. 总是确保释放以分配的内存。在编写分配内存的代码时,就要确定在代码的什么地方释放内存

  4. 在释放内存之前, 确保不会无意中覆盖堆上已分配的内存地址,否则程序就会出现内存泄漏。在循环中分配内
    存时,要特别小心

  5. 指针使用- -览

    image-20210912112333203

十四、结构体和共用体

结构体

结构体类似java的类

1 快速入门

#include <stdio.h>

void main() {
/*	张老太养了两只猫猫:
	一只名字叫小白,今年3岁, 白色。
	还有一只叫小花,今年100岁, 花色。
	请编写一个程序输出猫的名字,年龄,颜色。
*/
	struct Cat
	{
		char* name;	// 名字
		int age;	// 年龄
		char* color;// 颜色
	};
	// 使用结构体创建变量
	struct Cat cat1;	// 就是struct Cat的一个变量
	struct Cat cat2;
	// 给cat1的各个成员赋值
	cat1.name = "小白";
	cat1.age = 3;
	cat1.color = "白色";

	// 给cat2的各个成员赋值
	cat2.name = "小花";
	cat2.age = 100;
	cat2.color = "花色";

	// 输出两只猫的信息
	printf("第一只猫 name=%s age=%d coloe=%s", cat1.name, cat1.age, cat1.color);
	printf("\n 第二只猫 name=%s age=%d coloe=%s", cat2.name, cat2.age, cat2.color);
	getchar();
}

2 内存布局

image-20210912174102063

3 创建结构体和结构体变量

struct 结构体名称 { //结构体名首字母大写,比如Cat, Person
	成员列表;
};

①方式1-先定义结构体,然后再创建结构体变量

这个在快速入门使用过,这里不演示

②方式2-在定义结构体的同时定义结构体变量

struct Stu
{
    char* naem;
    int age;
} stu1, stu2;

③方式3-匿名结构体

// 这种声明方式只能有stu1和stu2两个结构体变量,想声明也没法声明了
struct
{
    char* naem;
    int age;
} stu1, stu2;

4 成员

  1. 从叫法上看:有些书上称为成员,有些书说结构体包含的变量
  2. 成员是结构体的一个组成部分,- -般是基本数据类型、也可以是数组、指针、结构体等。比如我们前面定义
    Cat结构体的intage就是一个成员。
  3. 成员的类型可以是:基本类型、数组、指针或结构体等等

注意事项

  1. 创建一个结构体变量后,要对成员进行赋值,如果没有赋值的话会导致程序异常终止
  2. 不同的结构体变量的成员是独立的,互相不影响,和java中从新new一个是一样的,两者互不影响

成员的获取和赋值

通过结构体变量名.成员名

5 结构应用案例

盒子案例

编程创建一个Box结构体,在其中定义三个成员表示一个立方体的长、宽和高,长宽高可以通过控制台输入。.
定义一个函数获取立方体的体积(volume)。
创建- - 个结构体,打印给定尺寸的立方体的体积。

代码实现

#include <stdio.h>

// 打印给定尺寸的立方体的体积
struct Box
{
	double width;	// 宽
	double height;	// 高
	double chang;	// 长
};

/*
	获取立方体的体积
*/
double getVolume(struct Box *box) {
	return (*box).chang* (*box).width* (*box).height;
}

void main() {
	struct Box box;
	scanf("%lf", &box.height);
	scanf("%lf", &box.width);
	scanf("%lf", &box.chang);
	double volume = getVolume(&box);
	printf("体积为:%.2f", volume);
	getchar();
	getchar();
}

景区门票案例

  1. 一个景区根据游人的年龄收取不同价格的门票。
  2. 请编写游人结构体(Visitor),根据年龄段决定能够购买的门票价格并输出
  3. 规则:年龄>18,门票为20元,其它情况免费。
  4. 可以循环从控制台输入 名字和年龄,打印门票收费情况,如果名字输入n,则退出程序。

代码实现

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

/*
	1) 一个景区根据游人的年龄收取不同价格的门票。
	2) 请编写游人结构体(Visitor),根据年龄段决定能够购买的门票价格并输出
	3) 规则:年龄>18,门票为20元,其它情况免费。
	4) 可以循环从控制台输入 名字和年龄,打印门票收费情况,如果名字输入n,则退出程序。
*/
struct Visitor {
	char name[20];
	int age;
	double pay;	// 票价
};

void ticket(struct Visitor *visitor) {
	if ((*visitor).age > 18)
	{
		(*visitor).pay = 20;
	}
	else {
		(*visitor).pay = 0;
	}
}

void main() {
	// 声明一个结构体变量
	struct Visitor visitor;
	while (1)
	{
		printf("\n请输入一个你的名字:");
		scanf("%s", visitor.name);
		if (!strcmp("n", visitor.name))
		{
			break;	// 程序结束
		}
		printf("\n请输入一个你的年龄:");
		scanf("%d", &visitor.age);
		ticket(&visitor);
		printf("\n该游客的票价为:%.2f", visitor.pay);
	}
	printf("程序结束");
	getchar();
	getchar();
}

共用体

1 为什么需要结构体

现有一张关于学生信息和教师信息的表格。学生信息包括姓名、编号、性别、职业、分数,教师的信息包括姓
名、编号、性别、职业、教学科目。请看下面的表格:

image-20210913113928554

传统方式来解决

定义结构体,根据人员的职业,使用对应的成员变量

问题:造成空间的浪费

解决方案

① 分别定义学生和老师的结构体,但是这种方式不利管理

② 使用共用体

2 基本介绍

共同使用同一块内存,也就是说指向同一个地址,一个变量的值发生了变化,其他值也要发生变化,但是不同的类型大小不同,所以按最大的来开辟空间,不同的类型输出的值也是不一定相同的

例如:int、short、char三种类型,那么这个共同体的占用大小就是4个字节

  1. 共用体(Union) 属于构造类型,它可以包含多个类型不同的成员。和结构体非常类似,但是也有不同的地方.
    共用体有时也被称为联合或者联合体

  2. 声明

union 共用体名{	// 也可以使用匿名共用体
	成员列表
};
  1. 结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员
    占用同一段内存,修改一个成员会影响其余所有成员

3 快速入门和内存布局

快速入门

#include <stdio.h>

union Data
{
	int n;
	char ch;
	short m;
};

void main() {
	union Data a;	// 定义共用体变量a
	printf("%d,%d\n", sizeof(a), sizeof(union Data));	// 4,4
	a.n = 0x40;
	printf("%d, %c, %d\n", a.n, a.ch, a.m); // 64 @ 64
	a.ch = '9';
	printf("%d, %c, %d\n", a.n, a.ch, a.m);	// 57 9 57
	a.m = 0x2059;
	printf("%d, %c, %d\n", a.n, a.ch, a.m);	// 8281 Y 8281
	a.n = 0x3E25AD54;
	printf("%d, %c, %d\n", a.n, a.ch, a.m);	// 1042656596, T, -21164
    printf("内存分布%p,%p,%p", &a.n, &a.ch, &a.m);	// 三个变量指向同一块内存
	getchar();
}

内存布局

image-20210913120812261

注意:虽然是4个字节,但是short是2个字节,所以,它只能是输出前面两个字节的数据,char也是只能输出1个字节的数据,所以数据类型的大小决定能输出什么样的值

4 最佳实践

现有一张关于学生信息和教师信息的表格。学生信息包括姓名、编号、性别、职业、分数,教师的信息包括姓
名、编号、性别、职业、教学科目。请看下面的表格:

image-20210913113928554

代码实现

#include <stdio.h>

struct Person
{
	char name[20];
	int num;
	char sex;
	char profession;
	union {
		float score;	// 成绩	
		char course[20];	// 科目
	} sc;
};

void main() {
	int i;
	// 定义一个结构体数组
	struct Person persons[2];
	for ( i = 0; i < 2; i++)
	{
		printf("Input Info");
		scanf("%s %d %c %c", persons[i].name, &(persons[i].num), &(persons[i].sex), &(persons[i].profession));
		if ('s' == persons[i].profession)
		{
			printf("请输入该学生的成绩:");
			scanf("%f", &(persons[i].sc.score));
		}
		else
		{
			printf("请输入该老师课程:");
			scanf("%s", persons[i].sc.course);
		}
		fflush(stdin);
	}
	printf("nName\t\tNum\tSex\tProfession\tScore/ Course\n");
	for (i = 0; i < 2; i++) {
		if (persons[i].profession == 's') {  //如果 是学生
			printf("%s\t\t%d\t%c\t%c\t\t%f\n", persons[i].name, persons[i].num, persons[i].sex, persons[i].profession, persons[i].sc.score);
		}
		else { //如果 是老师
			printf("%s\t\t%d\t%c\t%c\t\t%s\n", persons[i].name, persons[i].num, persons[i].sex, persons[i].profession, persons[i].sc.course);
		}
	}
	getchar();
	getchar();
}

十五、文件操作

1 基本介绍

文件,对我们并不陌生,文件是数据源(保存数据的地方)的一种,比如大家经常使用的word文档,txt文件,excel文件…都是文件。文件最主要的作用就是保存数据,它既可以保存- -张图片,也可以保持视频,声音…

文件在程序中以流的形式来操作的

image-20210914092833058

相关函数在stdio.h

2 标准文件

  1. C语言把所有的设备都当作文件。所以设备(比如显示器)被处理的方式与文件相同。以下三个文件会在程序
    执行时自动打开,以便访问键盘和屏幕
  2. 文件指针是访问文件的方式
  3. C语言中的I/O (输入/输出)通常使用printf() 和scanf() 两个函数。scanf() 函数用于从标准输入(键盘)读取并格式化,printf() 函数发送格式化输出到标准输出(屏幕)

3 getchar()和putchar函数

  • int getchar(void)

    函数从屏幕读取下一一个可用的字符,并把它返回为一一个整数。这个函数在同一个时间内一次只能读一个。您可以在循环内使用这个方法,以便从屏幕上读取多个字符。

  • int putchar(int c)

    函数把字符输出到屏幕上,并返回相同的字符。这个函数在同-一个时间内只会输出一个单- -的字符。您可以在循环内使用这个方法,以便在屏幕上输出多个字符

代码演示

#include <stdio.h>

void main() {
	int c;
	printf("Enter a value");
	c = getchar();	// 从键盘上读取一个键,并返回该键的键值

	printf("\nYou entered");
	putchar(c);		// 字符输出在屏幕的当前位置。
	printf("\n");
	getchar();
}

4 gets()&puts函数

  1. char gets(chars) 函数从stdin 读取一行到s所指向的缓冲区,直到一个终止符或EOF。所以它是可以读取多个字符的
  2. **int puts(const char *s)**函数把字符串s和一个尾随的换行符写入到stdout。
#include <stdio.h>

void main() {
	char str[100];
	printf("Enter a value");
	gets(str);

	printf("\nYou entered");
	puts(str);
	getchar();
}

5 文件读写

  1. 讲解了C语言处理的标准输入和输出设备。我们将介绍如何创建、打开、关闭文本文件或二进制文件。
  2. 一个文件,无论它是文本文件还是二进制文件,都是代表了一.系列的字节。C语言不仅提供了访问顶层的函数,
    也提供了底层(OS)调用来处理存储设备.上的文件。

①打开文件

使用fopen( )函数来创建- - 个新的文件或者打开-一个已有的文件,这个调用会初始化类型FILE的-一个对象,
类型FILE包含了所有用来控制流的必要的信息。下面是这个函数调用的原型:
*FILE fopen( const char * filename, const char * mode );

  1. 说明:在这里,filename 是字符串,用来命名文件,访问模式mode的值可以是下列值中的-一个

  2. 如 果处理的是二进制文件(图片,视频.),则需使用下面的访问模式: “rb”, “wb”, “ab”, “rb+”, “r+b”, “wb+”, “w+b”,
    “ab+”, “a+b” //b :binary二 进制

  3. 函数的说明

    image-20210915090259384

②关闭文件

关闭文件,使用fclose() 函数。函数的原型如下:
*int fclose( FILE fp );

  1. 如果成功关闭文件,fclose( )函数返回零,如果关闭文件时发生错误,函数返回EOF。这个函数实际上,会清
    空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF是一个定义在头文件stdio.h中的常量。
  2. C标准库提供了各种函数来按字符或者以固定长度字符串的形式读写文件。
  3. 使用 完文件后(读,写),一定要将该文件关闭

③写入文件

下面是把字符写入到流中的函数
*int fputc( intc, FILE fip );
说明:函数fputc()把参数c的字符值写入到fp所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回EOF。您可以使用下面的函数来把-一个以null 结尾的字符串写入到流中:
int fjputs( const char s, FILE fp );
说明:函数fiputs(把字符串s写入到fp所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生.错误,则会返回EOF。您也可以使用
int fprintf(FILE *fp,const char *format, …)**函数来写把一个字符串写入到文件中

代码演示

#include <stdio.h>
// 读取文件

void main() {
	// 1 创建一个文件指针
	FILE* f = NULL;

	// 2 打开文件
	f = fopen("D:\\test.txt", "w+");	// 文件会被清空
	//f = fopen("D:\\test.txt", "w+");	// 追加到原有文件的内容的后面

	// 3 将内容写入到文件中
	fprintf(f, "hello workld2");
	//fputc("helloworld2", f);

	// 4 关闭文件
	fclose(f);
	getchar();
}

④读取文件

下面是从文件读取单个字符的函数
int fgetc( FILE * fp );
说明: fgetc( 函数从fip 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回EOF。

下面的函数从流中读取-一个字符串:
**char *fgets( char buf, int n, FILE fp );

  1. 说明: 函数fgets() 从fp所指向的输入流中读取n- 1个字符。它会把读取的字符串复制到缓冲区buf,并在最后追加一个null字符来终止字符串。如果这个函数在读取最后-一个字符之 前就遇到-一个换行符\n’ 或文件的末尾EOF, 则只会返回读取到的字符,包括换行符。
  2. 也 可以使用**int fscanf(FILE *fp, const char *format, …)**函数来从文件中读取字符串,但是在遇到第一个空格字符
    时,它会停止读取。

代码演示

#include <stdio.h>

void main() {
	// 1 定义一个文件指针
	FILE* f = NULL;
	// 2 定义一个缓冲区
	char buff[1024];
		// 3 打开文件
	f = fopen("d:\\test2.txt", "r");
	// 4 读取文件
	// 方式一 --> 遇到空格就会停止读取
	fscanf(f, "%s", buff);
	printf("%s\n", buff);
	// 方式二 --> 读取整个文件的内容
	while (fgets(buff, 1024, f) != NULL)
	{
		printf("%s", buff);
	}

	// 5 关闭文件
	fclose(f);
	getchar();
}

十六、项目实战

[20]; // 科目
} sc;
};

void main() {
int i;
// 定义一个结构体数组
struct Person persons[2];
for ( i = 0; i < 2; i++)
{
printf(“Input Info”);
scanf("%s %d %c %c", persons[i].name, &(persons[i].num), &(persons[i].sex), &(persons[i].profession));
if (‘s’ == persons[i].profession)
{
printf(“请输入该学生的成绩:”);
scanf("%f", &(persons[i].sc.score));
}
else
{
printf(“请输入该老师课程:”);
scanf("%s", persons[i].sc.course);
}
fflush(stdin);
}
printf(“nName\t\tNum\tSex\tProfession\tScore/ Course\n”);
for (i = 0; i < 2; i++) {
if (persons[i].profession == ‘s’) { //如果 是学生
printf("%s\t\t%d\t%c\t%c\t\t%f\n", persons[i].name, persons[i].num, persons[i].sex, persons[i].profession, persons[i].sc.score);
}
else { //如果 是老师
printf("%s\t\t%d\t%c\t%c\t\t%s\n", persons[i].name, persons[i].num, persons[i].sex, persons[i].profession, persons[i].sc.course);
}
}
getchar();
getchar();
}




# 十五、文件操作

## 1	基本介绍

文件,对我们并不陌生,文件是数据源(保存数据的地方)的一种,比如大家经常使用的word文档,txt文件,excel文件...都是文件。文件最主要的作用就是保存数据,它既可以保存- -张图片,也可以保持视频,声音..



**文件在程序中以流的形式来操作的**

[外链图片转存中...(img-06MAnqxx-1632406293773)]



**相关函数在stdio.h**





## 2	标准文件

1) C语言把所有的设备都当作文件。所以设备(比如显示器)被处理的方式与文件相同。以下三个文件会在程序
    执行时自动打开,以便访问键盘和屏幕
2) 文件指针是访问文件的方式
3) C语言中的I/O (输入/输出)通常使用printf() 和scanf() 两个函数。scanf() 函数用于从标准输入(键盘)读取并格式化,printf() 函数发送格式化输出到标准输出(屏幕)



## 3	getchar()和putchar函数

-   **int getchar(void)**
    
    函数从屏幕读取下一一个可用的字符,并把它返回为一一个整数。这个函数在同一个时间内一次只能读一个。您可以在循环内使用这个方法,以便从屏幕上读取多个字符。
    
-   **int putchar(int c)**

    函数把字符输出到屏幕上,并返回相同的字符。这个函数在同-一个时间内只会输出一个单- -的字符。您可以在循环内使用这个方法,以便在屏幕上输出多个字符

**代码演示**

```c
#include <stdio.h>

void main() {
	int c;
	printf("Enter a value");
	c = getchar();	// 从键盘上读取一个键,并返回该键的键值

	printf("\nYou entered");
	putchar(c);		// 字符输出在屏幕的当前位置。
	printf("\n");
	getchar();
}

4 gets()&puts函数

  1. char gets(chars) 函数从stdin 读取一行到s所指向的缓冲区,直到一个终止符或EOF。所以它是可以读取多个字符的
  2. **int puts(const char *s)**函数把字符串s和一个尾随的换行符写入到stdout。
#include <stdio.h>

void main() {
	char str[100];
	printf("Enter a value");
	gets(str);

	printf("\nYou entered");
	puts(str);
	getchar();
}

5 文件读写

  1. 讲解了C语言处理的标准输入和输出设备。我们将介绍如何创建、打开、关闭文本文件或二进制文件。
  2. 一个文件,无论它是文本文件还是二进制文件,都是代表了一.系列的字节。C语言不仅提供了访问顶层的函数,
    也提供了底层(OS)调用来处理存储设备.上的文件。

①打开文件

使用fopen( )函数来创建- - 个新的文件或者打开-一个已有的文件,这个调用会初始化类型FILE的-一个对象,
类型FILE包含了所有用来控制流的必要的信息。下面是这个函数调用的原型:
*FILE fopen( const char * filename, const char * mode );

  1. 说明:在这里,filename 是字符串,用来命名文件,访问模式mode的值可以是下列值中的-一个

  2. 如 果处理的是二进制文件(图片,视频.),则需使用下面的访问模式: “rb”, “wb”, “ab”, “rb+”, “r+b”, “wb+”, “w+b”,
    “ab+”, “a+b” //b :binary二 进制

  3. 函数的说明

    [外链图片转存中…(img-pvFfC5gV-1632406293774)]

②关闭文件

关闭文件,使用fclose() 函数。函数的原型如下:
*int fclose( FILE fp );

  1. 如果成功关闭文件,fclose( )函数返回零,如果关闭文件时发生错误,函数返回EOF。这个函数实际上,会清
    空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF是一个定义在头文件stdio.h中的常量。
  2. C标准库提供了各种函数来按字符或者以固定长度字符串的形式读写文件。
  3. 使用 完文件后(读,写),一定要将该文件关闭

③写入文件

下面是把字符写入到流中的函数
*int fputc( intc, FILE fip );
说明:函数fputc()把参数c的字符值写入到fp所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回EOF。您可以使用下面的函数来把-一个以null 结尾的字符串写入到流中:
int fjputs( const char s, FILE fp );
说明:函数fiputs(把字符串s写入到fp所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生.错误,则会返回EOF。您也可以使用
int fprintf(FILE *fp,const char *format, …)**函数来写把一个字符串写入到文件中

代码演示

#include <stdio.h>
// 读取文件

void main() {
	// 1 创建一个文件指针
	FILE* f = NULL;

	// 2 打开文件
	f = fopen("D:\\test.txt", "w+");	// 文件会被清空
	//f = fopen("D:\\test.txt", "w+");	// 追加到原有文件的内容的后面

	// 3 将内容写入到文件中
	fprintf(f, "hello workld2");
	//fputc("helloworld2", f);

	// 4 关闭文件
	fclose(f);
	getchar();
}

④读取文件

下面是从文件读取单个字符的函数
int fgetc( FILE * fp );
说明: fgetc( 函数从fip 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回EOF。

下面的函数从流中读取-一个字符串:
**char *fgets( char buf, int n, FILE fp );

  1. 说明: 函数fgets() 从fp所指向的输入流中读取n- 1个字符。它会把读取的字符串复制到缓冲区buf,并在最后追加一个null字符来终止字符串。如果这个函数在读取最后-一个字符之 前就遇到-一个换行符\n’ 或文件的末尾EOF, 则只会返回读取到的字符,包括换行符。
  2. 也 可以使用**int fscanf(FILE *fp, const char *format, …)**函数来从文件中读取字符串,但是在遇到第一个空格字符
    时,它会停止读取。

代码演示

#include <stdio.h>

void main() {
	// 1 定义一个文件指针
	FILE* f = NULL;
	// 2 定义一个缓冲区
	char buff[1024];
		// 3 打开文件
	f = fopen("d:\\test2.txt", "r");
	// 4 读取文件
	// 方式一 --> 遇到空格就会停止读取
	fscanf(f, "%s", buff);
	printf("%s\n", buff);
	// 方式二 --> 读取整个文件的内容
	while (fgets(buff, 1024, f) != NULL)
	{
		printf("%s", buff);
	}

	// 5 关闭文件
	fclose(f);
	getchar();
}

十六、项目实战

部分资料来自尚硅谷,如有侵权请联系删除

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值