《C语言速成》

目录

目录也可以在侧栏可以找到QWQ

目录

目录

C语言课程课前准备

养成写博客的习惯

github的重要性

一些有逼格的工具

如何学好C语言

初识C语言

本章目标:

本章重点:

什么是C语言?

第一个C语言程序

数据类型

变量、常量

字符串+转义字符+注释

选择语句

循环语句

 函数

数组

操作符

指针

结构体

分支语句和循环语句

什么是语句?

分支语句(选择结构)

if语句

悬空else

switch语句

循环语句

while循环

for循环

do...while()循环

折半查找算法

二分查找函数:

猜数字游戏实现

goto语句

函数

函数是什么?

库函数:

自定义函数

函数的参数

函数的调用

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

函数的声明和定义

函数递归

数组

一维数组的创建和初始化

一维数组的使用

一维数组在内存中的存储

 二维数组的创建和初始化

二维数组的使用

二维数组在内存中的存储

数组作为函数参数

冒泡排序函数的正确设计

C语言实现三子棋

C语言实现一个经典扫雷小游戏

一次性搞定解析GitHub

github是什么?

为什么选择GitHub?

GitHub使用手册

操作符详解

各种操作符的介绍

表达式求值

 指针

指针是什么?

指针和指针类型

野指针

指针运算

指针和数组

二级指针 

指针数组

结构体

结构体的声明

结构体变量的定义和初始化

结构体成员的访问

结构体传参

实用调试技巧

什么是bug?​

调试是什么?有多重要?

Debug和Release的介绍

windows环境调试介绍

最常使用的几个快捷键:

一些调试的实例

如何写出好(易于调试)的代码

编程常见的错误

数据的存储目录

数据类型介绍

整形在内存中的存储:原码、反码、补码

大小端字节序介绍及判断

char相关知识

七道例题加深char理解

浮点型在内存中的存储

指针的进阶目录

字符指针

指针数组

数组指针

数组参数、指针参数

函数指针

函数指针数组

回调函数

冒泡排序

qsort-快速排序任意类型的数据

指针和数组笔试题解析

指针笔试题

赛马问题:36匹马,6个跑道,没有计时器,请赛马确定36匹马的前三名

烧香问题 有一种香材质不均匀,但是每一根这样的香,烧完恰好1小时 给两根香,帮我确定15分钟的时间

字符函数和字符串函数

字符串求长度

strlen字符串求长度

长度不受限制的字符串函数

strcpy字符串拷贝

strcat字符串追加

strcmp字符串比较

长度受限制的字符串函数介绍

strncpy指定个数的字符串拷贝

strncat指定个数的字符串追加

strncmp指定长度的字符串比较

字符串查找

strstr字符串查找

strtok字符串删除分隔符函数

错误信息报告

strerror翻译错误码的函数

字符分类函数

字符转化函数:

内存操作函数

memcpy不重叠内存拷贝

memmove重叠内存拷贝

memcmp内存比较函数

memset内存设置函数

动态内存管理

为什么存在动态内存分配

动态内存函数的介绍

malloc动态内存开辟函数

free动态内存的释放回收函数

calloc动态内存开辟数组函数

realloc调整开辟动态内存大小函数

 常见的动态内存错误

几个经典的笔试题

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

柔性数组-C99

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

结构体

结构的自引用

结构体变量初始化

结构体内存对齐

结构体传参

位段

枚举    即为一 一列举

 联合(共用体)

C语言文件操作

本章重点

什么是文件

文件名

文件类型

 ​​文件缓冲区

文件指针

文件的打开和关闭

文件的顺序读写

文件的随机读写

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

文件结束判定

程序的编译(预处理操作)+链接

程序环境和预处理

本章重点:

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

图解编译+链接

详解编译+链接

翻译环境

运行环境

预处理详解

预定义符号

预处理指令

#define定义标识符

#define 定义宏

#define 替换规则

#和##

带副作用的宏参数

宏和函数对比

命令行定义

条件编译

常见的条件编译指令:

文件包含

<---- To Be Continued


C语言课程课前准备

养成写博客的习惯

博客的重要性:
1. 自己写博客,是对所学知识的总结
2. 写博客可以记录你学习的一个过程和心得,给面试官更多了解你的机会,同时增加面试的谈资。
3. 写博客说明你是一个愿意分享的人。
CSDN
博客园
51CTO-比较推荐
开源中国
自己搭建(服务器+域名; github博客)

github的重要性

https://github.com/
1. 大公司喜欢的东西
2. 了解git --->我是教程
3. 下去先了解,再注册一下github账号

一些有逼格的工具

印象笔记(有道云笔记)-笔记可以检索,笔记丢不了,随时随地方便复习。
xmind-思维导图,整理一门课程学完后的框架。


如何学好C语言

1.鼓励你,为你叫好。

C生万物 编程之本 长远IT职业发展的首选 C语言是母体语言,是人机交互接近底层的桥梁 学会C/C++,相当于
掌握技术核心 知识点一竿子打通。 IT行业,一般每10年就有一次变革 40年间,在TIOBE 排行榜中,C/C++位
置长期霸占前三名,没有丝毫撼动,可谓经典永不过时!

2.学习不能叫苦叫累,抱怨没时间
欲戴王冠,必承其重。
如果你总是和别人走一样的路怎么才能保证超越别人,那就得付出不一样的努力。
时间就像乳沟,只要你肯挤,就一定会有

3.拒绝做伸手党
遇到问题,先尝试自己解决

4.学好编程,不仅仅是学好C语言
必须要学好:
语言、算法和数据结构、系统调用(操作系统)和计算机网络。
学习语言:
选择一门好的语言,深入学习,完成所有的课后作业。
学习算法、数据结构:
课堂上一定注意听讲,下课注重实践。
学习操作系统和网络
课堂认真听讲+课后实践+看几本好书。


初识C语言

本章目标:

基本了解C语言的基础知识,对C语言有一个大概的认识。
每个知识点就是简单认识,不做详细讲解,后期课程都会细讲。


本章重点:

什么是C语言
第一个C语言程序
数据类型
变量、常量
字符串+转义字符+注释
选择语句
循环语句
函数
数组
操作符
常见关键字
define 定义常量和宏
指针
结构体

什么是C语言?

C语言是一门通用计算机编程语言,广泛应用于底层开发。C语言的设计目标是提供一种能以简易
的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程
语言。
尽管C语言提供了许多低级处理的功能,但仍然保持着良好跨平台的特性,以一个标准规格写出的
C语言程序可在许多电脑平台上进行编译,甚至包含一些嵌入式处理器(单片机或称MCU)以及
超级电脑等作业平台。
二十世纪八十年代,为了避免各开发厂商用的C语言语法产生差异,由美国国家标准局为C语言制
定了一套完整的美国国家标准语法,称为ANSI C,作为C语言最初的标准。 [1] 目前2011年12月8
日,国际标准化组织(ISO)和国际电工委员会(IEC)发布的C11标准是C语言的第三个官方标
准,也是C语言的最新标准,该标准更好的支持了汉字函数名和汉字标识符,一定程度上实现了汉
字编程。
C语言是一门面向过程的计算机编程语言,与C++,Java等面向对象的编程语言有所不同。
其编译器主要有Clang、GCC、WIN-TC、SUBLIME、MSVC、Turbo C等。

第一个C语言程序

#include <stdio.h>

int main()
{
   printf("hello bit\n");
   printf("he he\n");
   return 0;
}
//解释:
//main函数是程序的入口
//一个工程中main函数有且仅有一个

数据类型

char        //字符数据类型
short       //短整型
int         //整形
long        //长整型
long long   //更长的整形
float       //单精度浮点数
double      //双精度浮点数
//C语言有没有字符串类型?

为什么出现这么的类型?
每种类型的大小是多少?

#include <stdio.h>
int main()
{
   printf("%d\n", sizeof(char));
   printf("%d\n", sizeof(short));
   printf("%d\n", sizeof(int));
   printf("%d\n", sizeof(long));
   printf("%d\n", sizeof(long long));
   printf("%d\n", sizeof(float));
   printf("%d\n", sizeof(double));
   printf("%d\n", sizeof(long double));
   return 0;
}

注意:存在这么多的类型,其实是为了更加丰富的表达生活中的各种值。
类型的使用:

char ch = 'w';
int weight = 120;
int salary = 20000;

变量、常量

生活中的有些值是不变的(比如:圆周率,性别(?),身份证号码,血型等等)
有些值是可变的(比如:年龄,体重,薪资)。
不变的值,C语言中用常量的概念来表示,变得值C语言中用变量来表示。

定义变量的方法

int age = 150;
float weight = 45.5f;
char ch = 'w';

变量的分类

局部变量
全局变量

#include <stdio.h>

int global = 2019;//全局变量
int main()
{
   int local = 2018;//局部变量
   //下面定义的global会不会有问题?
   int global = 2020;//局部变量
   printf("global = %d\n", global);
   return 0;
}

总结:
上面的局部变量global变量的定义其实没有什么问题的!
当局部变量和全局变量同名的时候,局部变量优先使用。

变量的使用
 

#include <stdio.h>
int main()
{
   int num1 = 0;
   int num2 = 0;
   int sum = 0;
   printf("输入两个操作数:>");
   scanf("%d %d", &a, &b);
   sum = num1 + num2;
   printf("sum = %d\n", sum);
   return 0;
}
//这里介绍一下输入,输出语句
//scanf
//printf

变量的作用域和生命周期
作用域
作用域(scope),程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的
而限定这个名字的可用性的代码范围就是这个名字的作用域。
1. 局部变量的作用域是变量所在的局部范围。
2. 全局变量的作用域是整个工程。


生命周期
变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段
1. 局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。
2. 全局变量的生命周期是:整个程序的生命周期。

常量

C语言中的常量和变量的定义的形式有所差异。
C语言中的常量分为以下以下几种:
字面常量
const 修饰的常变量
#define 定义的标识符常量
枚举常量

#include <stdio.h>
#define MAX 100            //#define的标识符常量    
//举例
enum Sex
{
MALE,
FEMALE,
SECRET
};
//括号中的MALE,FEMALE,SECRET是枚举常量
int main()
{
   3.14;//字面常量
   1000;//字面常量
   const float pai = 3.14f;   //const 修饰的常量
   pai = 5.14;//ok?
   return 0;
}

字符串+转义字符+注释

字符串

"hello bit.\n"

这种由双引号(Double Quote)引起来的一串字符称为字符串字面值(String Literal),或者简称字符串。
注:字符串的结束标志是一个 \0 的转义字符。在计算字符串长度的时候 \0 是结束标志,不算作字符串
内容。
 

#include <stdio.h>
//下面代码,打印结果是什么?为什么?(突出'\0'的重要性)
int main()
{
   char arr1[] = "bit";
   char arr2[] = {'b', 'i', 't'};
   char arr3[] = {'b', 'i', 't', '\0'};
   printf("%s\n", arr1);
   printf("%s\n", arr2);
   printf("%s\n", arr3);
   return 0;
}

转义字符

加入我们要在屏幕上打印一个目录: c:\code\test.c
我们该如何写代码?

#include
int main()
{
printf("c:\code\test.c\n");
   return 0;
}

实际上程序运行的结果是这样的:


这里就不得不提一下转义字符了。转义字符顾名思义就是转变意思。
下面看一些转义字符。
 
转义字符    释义
\?    在书写连续多个问号时使用,防止他们被解析成三字母词
\'    用于表示字符常量'
\“    用于表示一个字符串内部的双引号
\\    用于表示一个反斜杠,防止它被解释为一个转义序列符。
\a    警告字符,蜂鸣
\b    退格符
\f    进纸符
\n    换行
\r    回车
\t    水平制表符
\v    垂直制表符
\ddd    ddd表示1~3个八进制的数字。 如: \130 X
\xdd    dd表示2个十六进制数字。 如: \x30 0


 

#include <stdio.h>
int main()
{
   //问题1:在屏幕上打印一个单引号',怎么做?
   //问题2:在屏幕上打印一个字符串,字符串的内容是一个双引号“,怎么做?
   printf("%c\n", '\'');
   printf("%s\n", "\"");
   return 0;
}

笔试题:

//程序输出什么?
#include <stdio.h>

int main()
{
   printf("%d\n", strlen("abcdef"));
   // \32被解析成一个转义字符
   printf("%d\n", strlen("c:\test\328\test.c"));
   return 0;
}

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

#include <stdio.h>

int Add(int x, int y)
{
   return x+y;
}
/*C语言风格注释
int Sub(int x, int y)
{
   return x-y;
}
*/
int main()
{
   //C++注释风格
   //int a = 10;
   //调用Add函数,完成加法
   printf("%d\n", Add(1, 2));
   return 0;
}

注释有两种风格:
C语言风格的注释 /*xxxxxx*/
缺陷:不能嵌套注释
C++风格的注释 //xxxxxxxx
可以注释一行也可以注释多行

选择语句

如果你好好学习,校招时拿一个好offer,走上人生巅峰。
如果你不学习,毕业等于失业,回家卖红薯。
这就是选择!

#include <stdio.h>
int main()
{
   int coding = 0;
   printf("你会去敲代码吗?(选择1 or 0):>");
   scanf("%d", &coding);
   if(coding == 1)
{
prinf("坚持,你会有好offer\n");
}
   else
{
printf("放弃,回家卖红薯\n");
}
   return 0;
}

循环语句

有些事必须一直做,比如我日复一日的讲课,比如大家,日复一日的学习。


C语言中如何实现循环呢?
while语句
for语句
do ...     while语句

//while循环的实例
#include <stdio.h>
int main()
{
   printf("加入比特\n");
   int line = 0;
   while(line<=20000)
{
       line++;
       printf("我要继续努力敲代码\n");
}
   if(line>20000)
       printf("赢取白富美\n");
   return 0;
}


 函数

#include <stdio.h>
int main()
{
   int num1 = 0;
   int num2 = 0;
   int sum = 0;
   printf("输入两个操作数:>");
   scanf("%d %d", &a, &b);
   sum = num1 + num2;
   printf("sum = %d\n", sum);
   return 0;
}

上述代码,写成函数如下:

#include <stdio.h>
int Add(int x, int y)
{
int z = x+y;
return z;
}
int main()
{
   int num1 = 0;
   int num2 = 0;
   int sum = 0;
   printf("输入两个操作数:>");
   scanf("%d %d", &num1, &num2);
   sum = Add(num1, num2);
   printf("sum = %d\n", sum);
   return 0;
}

函数的特点就是简化代码,代码复用。

数组

要存储1-10的数字,怎么存储?
C语言中给了数组的定义:一组相同类型元素的集合
数组定义

int arr[10] = {1,2,3,4,5,6,7,8,9,10};//定义一个整形数组,最多放10个元素数组的使用
 

#include <stdio.h>
int main()
{
    int i = 0;
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    for(i=0; i<10; i++)
    {
    printf("%d ", arr[i]);
    }
    printf("\n");
   return 0;
}

操作符

简单介绍
算术操作符
+
-
*
/
%
移位操作符
>>
<<

二进制向左右移动n位 a<<2不改变a的值
位操作符
&   按位与
^    按位异或
|    按位或
赋值操作符
=
+=
-=
*=
/=
&=
^=
|=   

>>=

<<=
单目操作符

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

关系操作符
>
>=
<
<=
!=  用于测试“不相等”
==      用于测试“相等”

逻辑操作符
&&    逻辑与
||          逻辑或

条件操作符

exp1 ? exp2 : exp3

逗号表达式

exp1, exp2, exp3, …expN

下标引用、函数调用和结构成员
[]
()
.
->
常见关键字

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

注:关键字,先介绍下面几个,后期遇到讲解。

关键字 typedef
typedef 顾名思义是类型定义,这里应该理解为类型重命名。
比如:

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

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

关键字static
在C语言中:
static是用来修饰变量和函数的
1. 修饰局部变量-静态局部变量
2. 修饰全局变量-静态全局变量
3. 修饰函数-静态函数
修饰局部变量

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

对比代码1和代码2的效果理解static修饰局部变量的意义。
结论:
static修饰局部变量改变了变量的生命周期,让静态局部变量出了作用域依然存在,到程序结束,
生命周期才结束。


修饰全局变量

//代码1
//add.c
int g_val = 2018;
//test.c
int main()
{
   printf("%d\n", g_val);
   return 0;
}
//代码2
//add.c
static int g_val = 2018;
//test.c
int main()
{
   printf("%d\n", g_val);
   return 0;
}

代码1正常,代码2在编译的时候会出现连接性错误。
结论:
一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用


修饰函数

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

代码1正常,代码2在编译的时候会出现连接性错误.
结论:
一个函数被static修饰,使得这个函数只能在本源文件内使用,不能在其他源文件内使用。

剩余关键字后续陆续会讲解。

#define 定义常量和宏
//define定义标识符常量
#define MAX 1000
//define定义宏
#define ADD(x, y) ((x)+(y))

#include <stdio.h>

int main()
{
   int sum = ADD(2, 3);
   printf("sum = %d\n", sum);
   sum = 10*ADD(2, 3);
   printf("sum = %d\n", sum);
   return 0;
}

指针

内存
内存是电脑上特别重要的存储器,计算机中所有程序的运行都是在内存中进行的 。
所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。
为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。

变量都有地址,取出变量地址如下:
 

int main()
{
int num = 10;
&num;//取出num的地址
printf("%p\n", &num);//打印地址,%p--以地址的形式打印
return 0;
}

 
那地址如何存储,需要定义指针变量。

int num = 10;
int *p;//p为一个整形指针变量
p = &num;

指针的使用实例:

#include <stdio.h>
int main()
{
int num = 10;
int *p = &num;
*p = 20;
   return 0;
}


以整形指针举例,可以推广到其他类型,如:

#include <stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'q';
printf("%c\n", ch);
   return 0;
}

指针变量的大小

#include <stdio.h>
int main()
{
   printf("%d\n", sizeof(char *));
   printf("%d\n", sizeof(short *));
   printf("%d\n", sizeof(int *));
   printf("%d\n", sizeof(double *));
   return 0;
}

结论:指针大小在32位平台是4个字节,64位平台是8个字节。
 

结构体

结构体是C语言中特别重要的知识点,结构体使得C语言有能力描述复杂类型。
比如描述学生,学生包含: 名字+年龄+性别+学号 这几项信息。
这里只能使用结构体来描述了。
例如:

struct Stu
{
   char name[20];//名字
   int age;      //年龄
   char sex[5];  //性别
   char id[15]; //学号
};

结构体的初始化:
 

//打印结构体信息
struct Stu s = {"张三", 20, "男", "20180101"};
//.为结构成员访问操作符
printf("name = %s age = %d sex = %s id = %s\n", s.name, s.age, s.sex, s.id);
//->操作符
struct Stu *ps = &s;
printf("name = %s age = %d sex = %s id = %s\n", ps->name, ps->age, ps->sex, ps->id);

分支语句和循环语句

本章重点学习分支语句和循环语句的使用
分支语句
if
switch
循环语句
while
for
do while
goto语句


什么是语句?

C语言中由一个分号 ; 隔开的就是一条语句。 比如:

printf("hehe");
1+2;

分支语句(选择结构)

如果你好好学习,校招时拿一个好offer,走上人生巅峰。
如果你不学习,毕业等于失业,回家卖红薯。
这就是选择!

if语句

那if语句的语法结构是怎么样的呢?语法结构:

if(表达式)
   语句;

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

演示代码:

#include <stdio.h>
//代码1
int main()
{
   int age = 0;
   scanf("%d", &age);
   if(age<18)
   {
       printf("未成年\n");
   }
   return 0;
}

//代码2
#include <stdio.h>
int main()
{
    int age = 0;
    scanf("%d", &age);
    if (age < 18)
    {
        printf("未成年\n");
    }
    else
    {
        printf("成年\n");
    }
}


//代码3

#include <stdio.h>
int main()
{
    int age = 0;
    scanf("%d", &age);
    if (age < 18)
    {
        printf("少年\n");
    }
    else if (age >= 18 && age < 30)
    {
        printf("青年\n");
    }
    else if (age >= 30 && age < 50)
    {
        printf("中年\n");
    }
    else if (age >= 50 && age < 80)
    {
        printf("老年\n");
    }
    else
    {
        printf("老不死\n");
    }
    return 0;
}


解释一下: 如果表达式的结果为真,则语句执行。
在C语言中如何表示真假?
0表示假,非0表示真。
 
如果条件成立,要执行多条语句,怎应该使用代码块。

#include <stdio.h>
int main()
{
    if (表达式)
    {
        语句列表1;
    }
    else
    {
        语句列表2;
    }
    return 0;
}

这里的一对 { } 就是一个代码块。

悬空else

当你写了这个代码:

#include <stdio.h>
int main()
{
    int a = 0;
    int b = 2;
    if (a == 1)
        if (b == 2)
            printf("hehe\n");
        else
            printf("haha\n");
    return 0;
}

改正:
//适当的使用{}可以使代码的逻辑更加清楚。
//代码风格很重要

#include <stdio.h>

int main()
{
    int a = 0;
    int b = 2;
    if (a == 1)
    {
        if (b == 2)
        {
            printf("hehe\n");
        }
    }
    else
    {
        printf("haha\n");
    }
    return 0;
}

if书写形式的对比

//代码1
if (condition) {
   return x;
}
return y;

//代码2
if(condition)
{
   return x;
}
else
{
   return y;
}

//代码3
int num = 1;
if(num == 5)
{
   printf("hehe\n");
}

//代码4
int num = 1;
if(5 == num)
{
   printf("hehe\n");
}
 


代码2和代码4更好,逻辑更加清晰,不容易出错。


练习

1. 判断一个数是否为奇数
2. 输出1-100之间的奇数
 

switch语句

switch语句也是一种分支语句。 常常用于多分支的情况。
比如:
输入1,输出星期一
输入2,输出星期二
输入3,输出星期三
输入4,输出星期四
输入5,输出星期五
输入6,输出星期六
输入7,输出星期七
那我没写成 if...else if ...else if 的形式太复杂,那我们就得有不一样的语法形式。 这就是
switch 语句。

switch(整型表达式)
{
   语句项;
}

而语句项是什么呢?

//是一些case语句:
//如下:
case 整形常量表达式:
   语句;

在switch语句中的 break
在switch语句中,我们没法直接实现分支,搭配break使用才能实现真正的分支。
比如:

#include <stdio.h>
int main()
{
    int day = 0;
    switch (day)
    {
        case 1:
            printf("星期一\n");
            break;

        case 2:
            printf("星期二\n");
            break;
        case 3:
            printf("星期三\n");
            break;
        case 4:
            printf("星期四\n");
            break;
        case 5:
            printf("星期五\n");
            break;
        case 6:
            printf("星期六\n");
            break;
        case 7:
            printf("星期天\n");
            break;
    }
    return 0;
}

有时候我们的需求变了:
1. 输入1-5输出的是“weekday”;
2. 输入6-7输出“weekend”
所以我们的代码就应该这样实现了:

#include <stdio.h>
//switch代码演示
int main()
{
    int day = 0;
    switch (day)
    {
        case 1:
        case 2:
        case 3:
        case 4:
        case 5:
            printf("weekday\n");
            break;
        case 6:
        case 7:
            printf("weekend\n");
            break;
    }
    return 0;
}

break语句的实际效果是把语句列表划分为不同的部分。
编程好习惯
在最后一个 case 语句的后面加上一条 break语句。 (之所以这么写是可以避免出现在以前的最
后一个 case 语句后面忘了添加 break语句)。
 
default子句
如果表达的值与所有的case标签的值都不匹配怎么办?
其实也没什么,结构就是所有的语句都被跳过而已。
程序并不会终止,也不会报错,因为这种情况在C中并不认为适合错误。
但是,如果你并不想忽略不匹配所有标签的表达式的值时该怎么办呢?
你可以在语句列表中增加一条default子句,把下面的标签

default:

写在任何一个case标签可以出现的位置。
当 switch表达式的值并不匹配所有case标签的值时,这个default子句后面的语句就会执行。
所以,每个switch语句中只能出现一条default子句。
但是它可以出现在语句列表的任何位置,而且语句流会像贯穿一个case标签一样贯穿default子句。
编程好习惯
在每个 switch 语句中都放一条default子句是个好习惯,甚至可以在后边再加一个 break 。
练习

#include <stdio.h>

int main()
{
    int n = 1;
    int m = 2;
    switch (n)
    {
    case 1:
        m++;
    case 2:
        n++;
    case 3:
        switch (n)
        {//switch允许嵌套使用
        case 1:
            n++;
        case 2:
            m++;
            n++;
            break;
        }
    case 4:
        m++;
        break;
    default:
        break;
    }
    printf("m = %d, n = %d\n", m, n);
    return 0;
}

循环语句

  • while
  • for
  • do while
     

while循环

我们已经掌握了,if语句:

if(条件)
    语句;

当条件满足的情况下,if语句后的语句执行,否则不执行。但是这个语句只会执行一次。
但是我们发现生活中很多的实际的例子是:同一件事情我们需要完成很多次。
那我们怎么做呢? C语言中给我们引入了:while语句,可以实现循环。
 

//while 语法结构
while(表达式)
循环语句;


比如我们实现:
在屏幕上打印1-10的数字。

#include <stdio.h>
int main()
{
	int i = 1;
	while (i <= 10)
	{
		printf("%d ", i);
		i = i + 1;
	}
	return 0;
}

这个代码已经帮我了解了while语句的基本语法,那我们再了解一下:
while语句中的break和continue
break介绍

//break 代码实例
#include <stdio.h>
int main()
{
	int i = 1;
	while (i <= 10)
	{

		if (i == 5)
			break;
		printf("%d ", i);
		i = i + 1;
	}
	return 0;
}

这里代码输出的结果是什么?
1 2 3 4
1 2 3 4 5
1 2 3 4 5 6 7 8 9 10
1 2 3 4 6 7 8 9 10
总结: break在while循环中的作用:
其实在循环中只要遇到break,就停止后期的所有的循环,直接终止循环。 所以:while中的
break是用于永久终止循环的。


continue介绍

//continue 代码实例1

#include <stdio.h>
int main()
{
	int i = 1;
	while (i <= 10)
	{
		if (i == 5)
			continue;
		printf("%d ", i);
		i = i + 1;
	}
	return 0;
}

这里代码输出的结果是什么?
1 2 3 4
1 2 3 4 5
1 2 3 4 5 6 7 8 9 10
1 2 3 4 6 7 8 9 10

//continue 代码实例2

#include <stdio.h>
int main()
{
	int i = 1;
	while (i <= 10)
	{
		i = i + 1;
		if (i == 5)
			continue;
		printf("%d ", i);
	}
	return 0;
}

这里代码输出的结果是什么?
1 2 3 4
1 2 3 4 5
1 2 3 4 5 6 7 8 9 10
1 2 3 4 6 7 8 9 10
2 3 4 6 7 8 9 10
总结: continue在while循环中的作用就是:
continue是用于终止本次循环的,也就是本次循环中continue后边的代码不会再执行,而是直接
跳转到while语句的判断部分。进行下一次循环的入口判断。
再看几个代码:
/

/ 代码什么意思?
//代码1
#include <stdio.h>
int main()
{
    int ch = 0;
    while ((ch = getchar()) != EOF)
        putchar(ch);
    return 0;
}

//代码2
#include <stdio.h>
int main()
{
    while ((ch = getchar()) != EOF)
    {
        if (ch < ‘0’ || ch > ‘9’)
            continue;
        putchar(ch);
    }
    return 0;
}

for循环

我们已经知道了while循环,但是我们为什么还要一个for循环呢? 首先来看看for循环的语法:
语法

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

表达式1 表达式1为初始化部分,用于初始化循环变量的。 表达式2 表达式2为条件判断部分,用于判断
循环时候终止。 表达式3 表达式3为调整部分,用于循环条件的调整。
实际的问题:
使用for循环 在屏幕上打印1-10的数字。

#include <stdio.h>
int main()
{
	int i = 0;
	//for(i=1/*初始化*/; i<=10/*判断部分*/; i++/*调整部分*/)
	for (i = 1; i <= 10; i++)
	{
		printf("%d ", i);
	}
	return 0;
}

现在我们对比一下for循环和while循环。

int i = 0;
//实现相同的功能,使用while
i = 1;//初始化部分
while (i <= 10)//判断部分
{
	printf("hehe\n");
	i = i + 1;//调整部分
}

//实现相同的功能,使用while
for (i = 1; i <= 10; i++)
{
	printf("hehe\n");
}

可以发现在while循环中依然存在循环的三个必须条件,但是由于风格的问题使得三个部分很可能偏离
较远,这样查找修改就不够集中和方便。所以,for循环的风格更胜一筹。 for循环使用的频率也最高。
break和continue在for循环中
我们发现在for循环中也可以出现break和continue,他们的意义和在while循环中是一样的。 但是还是
有些差异:

//代码1
#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 1; i <= 10; i++)
	{
		if (i == 5)
			break;
		printf("%d ", i);
	}
	return 0;
}

//代码2
#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 1; i <= 10; i++)
	{
		if (i == 5)
			continue;
		printf("%d ", i);
	}
	return 0;
}

for语句的循环控制变量


一些建议:
1. 不可在for 循环体内修改循环变量,防止 for 循环失去控制。
2. 建议for语句的循环控制变量的取值采用“前闭后开区间”写法。

int i = 0;
//前闭后开的写法
for(i=0; i<10; i++)
{}

//两边都是闭区间
for(i=0; i<=9; i++)
{}

一些for循环的变种

#include <stdio.h>
int main()
{
    //变种1
    for (;;)
    {
        printf("hehe\n");
    }
    //变种2
    int x, y;
    for (x = 0, y = 0; x < 2 && y < 5; ++x, y++)
    {
        printf("hehe\n");
    }
    return 0;
}

一道笔试题:

//请问循环要循环多少次?
#include <stdio.h>
int main()
{
    int i = 0;
    int k = 0;
    for (i = 0, k = 0; k = 0; i++, k++)
        k++;
    return 0;
}

do...while()循环

do语句的语法:

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

do语句的特点
循环至少执行一次,使用的场景有限,所以不是经常使用。

#include <stdio.h>
int main()
{
	int i = 10;
	do
	{
		printf("%d\n", i);
	} while (i < 10);
	return 0;
}

do while循环中的break和continue
 

#include <stdio.h>
int main()
{
    int i = 10;
    do
    {
        if (5 == i)
            break;
        printf("%d\n", i);
    } while (i < 10);
    return 0;
}

#include <stdio.h>
int main()
{
    int i = 10;
    do
    {
        if (5 == i)
            continue;
        printf("%d\n", i);
    } while (i < 10);
    return 0;
}

练习

1. 计算 n的阶乘。
2. 计算 1!+2!+3!+……+10!
3. 在一个有序数组中查找具体的某个数字n。 编写int binsearch(int x, int v[], int n); 功能:在v[0]
<=v[1]<=v[2]<= ….<=v[n-1]的数组中查找x.
4. 编写代码,演示多个字符从两端移动,向中间汇聚。
5. 编写代码实现,模拟用户登录情景,并且只能登录三次。(只允许输入三次密码,如果密码正确则
提示登录成,如果三次均输入错误,则退出程序。
练习参考代码:

//代码1
//编写代码,演示多个字符从两端移动,向中间汇聚
#include <stdio.h>
#include <string.h>
#include <windows.h>
int main()
{
    char arr1[] = "welcome to bit...";
    char arr2[] = "#################";
    int left = 0;
    int right = strlen(arr1) - 1;
    printf("%s\n", arr2);
    //while循环实现
    while (left <= right)
    {
        Sleep(1000);
        arr2[left] = arr1[left];
        arr2[right] = arr1[right];
        left++;
        right--;
        printf("%s\n", arr2);
    }
    //for循环实现
    for (left = 0, right = strlen(arr1) - 1;
        left <= right;
        left++, right--)
    {
        Sleep(1000);
        arr2[left] = arr1[left];
        arr2[right] = arr1[right];
        printf("%s\n", arr2);
    }
    return 0;
}


//代码2

int main()
{
    char psw[10] = "";
    int i = 0;
    int j = 0;
    for (i = 0; i < 3; ++i)
    {
        printf("please input:");
        scanf("%s", psw);
        if (strcmp(psw, "password") == 0)
            break;
    }
    if (i == 3)
        printf("exit\n");
    else
        printf("log in\n");
    return 0;
}

折半查找算法

比如我买了一双鞋,你好奇问我多少钱,我说不超过300元。你还是好奇,你想知道到底多少,我就让你猜,你会怎么猜?
答案:你每次猜中间数。
代码实现:

实现在主函数内:
 

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int left = 0;
	int right = sizeof(arr) / sizeof(arr[0]) - 1;
	int key = 7;
	int mid = 0;
	while (left <= right)
	{
		mid = (left + right) / 2;
		if (arr[mid] > key)
		{
			right = mid - 1;
		}
		else if (arr[mid] < key)
		{
			left = mid + 1;
		}
		else
			break;
	}
	if (left <= right)
		printf("找到了,下标是%d\n", mid);
	else
		printf("找不到\n");

}


如果实现一个

二分查找函数:


 

int bin_search(int arr[], int left, int right, int key)
{
	int mid = 0;
	while (left <= right)
	{
		mid = (left + right) >> 1;
		if (arr[mid] > key)
		{
			right = mid - 1;
		}
		else if (arr[mid] < key)
		{
			left = mid + 1;
		}
		else
			return mid;//找到了,返回下标
	}
	return -1;//找不到
}

猜数字游戏实现

参考代码:

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

void menu()
{
	printf("**********************************\n");
	printf("***********    1.play  ***********\n");
	printf("***********    0.exit  ***********\n");
	printf("**********************************\n");
}
//TDD-测试驱动开发。
//RAND_MAX--rand函数能返回随机数的最大值。
void game()
{
	int random_num = rand() % 100 + 1;
	int input = 0;
	while (1)
	{
		printf("请输入猜的数字>:");
		scanf("%d", &input);
		if (input > random_num)
		{
			printf("猜大了\n");
		}
		else if (input < random_num)
		{
			printf("猜小了\n");
		}

		else
		{
			printf("恭喜你,猜对了\n");
			break;
		}
	}
}
int main()
{
	int input = 0;
	srand((unsigned)time(NULL));
	do
	{
		menu();
		printf("请选择>:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			break;
		default:
			printf("选择错误,请重新输入!\n");
			break;
		}
	} while (input);
	return 0;
}

goto语句

C语言中提供了可以随意滥用的 goto语句和标记跳转的标号。
从理论上 goto语句是没有必要的,实践中没有goto语句也可以很容易的写出代码。
但是某些场合下goto语句还是用得着的,最常见的用法就是终止程序在某些深度嵌套的结构的处理过程,例如一次跳出两层或多层循环。
这种情况使用break是达不到目的的。它只能从最内层循环退出到上一层的循环。
下面是使用goto语句的一个例子:

一个关机程序

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
    char input[10] = { 0 };
    system("shutdown -s -t 60");
again:
    printf("电脑将在1分钟内关机,如果输入:我是猪,就取消关机!\n请输入:>");
    scanf("%s", input);
    if (0 == strcmp(input, "我是猪"))
        system("shutdown -a");
    else
        goto again;
    return 0;
}

而如果不适用goto语句,则可以使用循环:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
    char input[10] = { 0 };
    system("shutdown -s -t 60");
    while (1)
    {
        printf("电脑将在1分钟内关机,如果输入:我是猪,就取消关机!\n请输入:>");
        scanf("%s", input);
        if (0 == strcmp(input, "我是猪"))
        {
            system("shutdown -a");
            break;
        }
    }
    return 0;
}


goto语言真正适合的场景如下:

for (...)
for (...)
{
    for (...)
    {
        if (disaster)
            goto error;
    }
}
…
error :
if (disaster)
// 处理错误情况

函数

本章主要掌握函数的基本使用和递归
1. 函数是什么
2. 库函数
3. 自定义函数
4. 函数参数
5. 函数调用
6. 函数的嵌套调用和链式访问
7. 函数的声明和定义
8. 函数递归

函数是什么?

数学中我们常见到函数的概念。但是你了解C语言中的函数吗? 维基百科中对函数的定义:子程序
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,
subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组
成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。

C语言中函数的分类:
1. 库函数
2. 自定义函数

库函数:

为什么会有库函数?
1. 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想
把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格
式打印到屏幕上(printf)。
2. 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
3. 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。
像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,
为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员
进行软件开发。
那怎么学习库函数呢?
这里我们简单的看看:www.cplusplus.com

简单的总结,C语言常用的库函数都有:
IO函数
字符串操作函数
字符操作函数
内存操作函数
时间/日期函数
数学函数
其他库函数
我们参照文档,学习几个库函数:
strcpy

char * strcpy ( char * destination, const char * source );

memset

void * memset ( void * ptr, int value, size_t num );

 
注: 但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。
这里对照文档来学习上面几个库函数,目的是掌握库函数的使用方法。
如何学会使用库函数?

需要全部记住吗?No 需要学会查询工具的使用:
MSDN(Microsoft Developer Network)
www.cplusplus.com
http://en.cppreference.com
英文很重要。最起码得看懂文献。
 

自定义函数

如果库函数能干所有的事情,那还要程序员干什么?
所有更加重要的是自定义函数。
自定义函数和库函数一样,有函数名,返回值类型和函数参数。

但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
函数的组成:

ret_type fun_name(para1, * )
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1    函数参数

我们举一个栗子:
写一个函数可以找出两个整数中的最大值。

#include <stdio.h>

//get_max函数的设计
int get_max(int x, int y)
{
	return (x > y) ? (x) : (y);
}

int main()
{
	int num1 = 10;
	int num2 = 20;
	int max = get_max(num1, num2);
	printf("max = %d\n", max);
	return 0;
}

写一个函数可以交换两个整形变量的内容

#include <stdio.h>
void Swap1(int x, int y)
{
	int tmp = 0;
	tmp = x;
	x = y;
	y = tmp;
}
void Swap2(int* px, int* py)
{
	int tmp = 0;
	tmp = *px;
	*px = *py;
	*py = tmp;
}
int main()
{
	int num1 = 1;
	int num2 = 2;
	Swap1(num1, num2);
	printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
	Swap2(&num1, &num2);
	printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
	return 0;
}

函数的参数

实际参数(实参):
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类
型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数(形参):
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配
内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在
函数中有效。


上面Swap1和Swap2函数中的参数 x,y,px,py 都是形式参数。在main函数中传给Swap1的num1,
num2和传给Swap2函数的&num1,&num2是实际参数。
这里我们对函数的实参和形参进行分析:
代码对应的内存分配如下:

 代码对应的内存分配如下


这里可以看到Swap1函数在调用的时候,x,y拥有自己的空间,同时拥有了和实参一模一样的内容。所
以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。

函数的调用

传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起正真的联系,也就是函数内部可以直接操
作函数外部的变量。
练习

1. 写一个函数可以判断一个数是不是素数。
2. 写一个函数判断一年是不是闰年。
3. 写一个函数,实现一个整形有序数组的二分查找。
4. 写一个函数,每调用一次这个函数,就会将num的值增加1。

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

函数和函数之间可以有机的组合的。


嵌套调用

组装汽车,将多个零件组装起来

#include <stdio.h>
void new_line()
{
    printf("hehe\n");
}
void three_line()

{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        new_line();
    }
}
int main()
{
    three_line();
    return 0;
}

链式访问
把一个函数的返回值作为另外一个函数的参数。

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

int main()
{
	char arr[20] = "hello";
	int ret = strlen(strcat(arr, "bit"));//这里介绍一下strlen函数
	printf("%d\n", ret);
	return 0;
}

#include <stdio.h>
int main()
{
	printf("%d", printf("%d", printf("%d", 43)));
	//结果是啥?
	return 0;
}

函数的声明和定义

函数声明:
1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,无关紧要。
2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3. 函数的声明一般要放在头文件中的。
函数定义:
函数的定义是指函数的具体实现,交待函数的功能实现。


test.h的内容 放置函数的声明

#ifndef __TEST_H__
#define __TEST_H__
//函数的声明
int Add(int x, int y);
#endif //__TEST_H__

test.c的内容 放置函数的实现

#include "test.h"
//函数Add的实现
int Add(int x, int y)
{
return x+y;
}

函数递归


什么是递归?

程序调用自身的编程技巧称为递归( recursion)。 递归做为一种算法在程序设计语言中广泛应
用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复
杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可
描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在
于:把大事化小


递归的两个必要条件
存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件。


练习1:(画图讲解)
接受一个整型值(无符号),按照顺序打印它的每一位。 例如: 输入:1234,输出 1 2 3 4.
参考代码:

#include <stdio.h>
void print(int n)
{
	if (n > 9)
	{
		print(n / 10);
	}
	printf("%d ", n % 10);
}
int main()
{
	int num = 1234;
	print(num);
	return 0;
}

练习2:(画图讲解)
编写函数不允许创建临时变量,求字符串的长度。
参考代码:

#include <stdio.h>
#include <string.h>
int Strlen(const char* str)
{
	if (*str == '\0')
		return 0;
	else return 1 + Strlen(str + 1);
}
int main()
{
	char* p = "abcdef";
	int len = Strlen(p);
	printf("%d\n", len);
	return 0;
}

递归与迭代

练习3:
求n的阶乘。(不考虑溢出)
参考代码:

int factorial(int n)
{
	if (n <= 1)
		return 1;
	else
		return n * factorial(n - 1);
}

练习4:
求第n个斐波那契数。(不考虑溢出)
参考代码:

int fib(int n)
{
	if (n <= 2)
		return 1;
	else
		return fib(n - 1) + fib(n - 2);
}

但是我们发现有问题;
在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。
为什么呢?
我们发现 fib 函数在调用的过程中很多计算其实在一直重复。 如果我们把代码修改一下:
 

int count = 0;//全局变量
int fib(int n)
{
	if (n == 3)
		count++;
	if (n <= 2)
		return 1;
	else
		return fib(n - 1) + fib(n - 2);
}

最后我们输出看看count,是一个很大很大的值。
那我们如何改进呢?
在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: `stack overflow(栈溢出) 这样的信息。

系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),

这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。

那如何解决上述的问题:
1. 将递归改写成非递归。
2. 使用static对象替代nonstatic局部对象。在递归函数设计中,可以使用static对象替代nonstatic局
部对象(即栈对 象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,
而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。
比如:

//求n的阶乘
int factorial(int n)
{
    int result = 1;
    while (n > 1)
    {
        result *= n;
        n -= 1;
    }
    return result;
}
//求第n个斐波那契数
int fib(int n)
{
    int result;
    int pre_result;
    int next_older_result;
    result = pre_result = 1;
    while (n > 2)
    {
        n -= 1;
        next_older_result = pre_result;
        pre_result = result;
        result = pre_result + next_older_result;
    }
    return result;
}


提示:
1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
 
函数递归的几个经典题目(自主研究):
1. 汉诺塔问题
2. 青蛙跳台阶问题
 

数组

本章重点

1. 一维数组的创建和初始化
2. 一维数组的使用
3. 一维数组在内存中的存储
4. 二维数组的创建和初始化
5. 二维数组的使用
6. 二维数组在内存中的存储
7. 数组作为函数参数
8. 数组的应用实例1:三子棋
9. 数组的应用实例2:扫雷游戏

一维数组的创建和初始化

数组的创建
数组是一组相同类型元素的集合。 数组的创建方式:

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

素组创建的实例:

//代码1
int arr1[10];

//代码2
int count = 10;
int arr2[count];//数组时候可以正常创建?

//代码3
char arr3[10];
float arr4[1];
double arr5[20];

注:数组创建, [] 中要给一个常量才可以,不能使用变量。

数组的初始化

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

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

数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确
定。 但是对于下面的代码要区分,内存中如何分配。

char arr1[] = "abc";
char arr2[3] = {'a','b','c'};

一维数组的使用

对于数组的使用我们之前介绍了一个操作符: [] ,下标引用操作符。它其实就数组访问的操作符。

我们来看代码:

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };//数组的不完全初始化
	   //计算数组的元素个数
	int sz = sizeof(arr) / sizeof(arr[0]);
	//对数组内容赋值,数组是使用下标来访问的,下标从0开始。所以:
	int i = 0;//做下标
	for (i = 0; i < 10; i++)//这里写10,好不好?
	{
		arr[i] = i;
	}
	//输出数组的内容
	for (i = 0; i < 10; ++i)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

总结:
1. 数组是使用下标来访问的,下标是从0开始。
2. 数组的大小可以通过计算得到。

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

一维数组在内存中的存储

接下来我们探讨数组在内存中的存储。 看代码:

#include <stdio.h>

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

输出的结果如下:

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

 
二维数组的创建和初始化

二维数组的创建

//数组创建
int arr[3][4];
char arr[3][5];
double arr[2][4];

二维数组的初始化

//数组初始化
int arr[3][4] = {1,2,3,4};
int arr[3][4] = {{1,2},{4,5}};
int arr[][4] = {{2,3},{4,5}};

二维数组的使用

二维数组的使用也是通过下标的方式。 看代码:

#include <stdio.h>
int main()
{
	int arr[3][4] = { 0 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 4; j++)
		{
			arr[i][j] = i * 4 + j;
		}
	}
	for (i = 0; i < 3; i++)
	{
		int j = 0;

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

二维数组在内存中的存储

像一维数组一样,这里我们尝试打印二维数组的每个元素。

#include <stdio.h>
int main()
{
	int arr[3][4];
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 4; j++)
		{
			printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]);
		}
	}
	return 0;
}


输出的结果是这样的:
 

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

数组作为函数参数

往往我们在写代码的时候,会将数组作为参数传个函数,比如:我要实现一个冒泡排序

(这里要讲算法思想)函数将一个整形数组排序。 那我们将会这样使用该函数:
冒泡排序函数的错误设计

//方法1:
#include <stdio.h>
void bubble_sort(int arr[])
{
    int sz = sizeof(arr) / sizeof(arr[0]);//这样对吗?
    int i = 0;
    for (i = 0; i < sz - 1; i++)
    {
        int j = 0;
        for (j = 0; j < sz - i - 1; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

int main()
{
    int arr[] = { 3,1,7,5,8,9,0,2,4,6 };
    int i = 0;
    bubble_sort(arr);//是否可以正常排序?
    for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

方法1,出问题,那我们找一下问题,调试之后可以看到 bubble_sort 函数内部的 sz ,是1。
难道数组作为函数参数的时候,不是把整个数组的传递过去?


数组名是什么?

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5 };
	printf("%p\n", arr);
	printf("%p\n", &arr[0]);
	printf("%d\n", *arr);
	//输出结果
	return 0;
}

结论:
数组名是数组首元素的地址。(有两个例外)
如果数组名是首元素地址,那么:

int arr[10] = {0};
printf("%d\n", sizeof(arr));

为什么输出的结果是:40?
补充:
1. sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
2. &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。

除此1,2两种情况之外,所有的数组名都表示数组首元素的地址。


冒泡排序函数的正确设计

当数组传参的时候,实际上只是把数组的首元素的地址传递过去了。
所以即使在函数参数部分写成数组的形式: int arr[] 表示的依然是一个指针: int *arr 。

那么,函数内部的 sizeof(arr) 结果是4。
如果 方法1 错了,该怎么设计?

//方法2
#include<stdio.h>
void bubble_sort(int arr[],int sz)//本质int* arr
{
	//确定冒泡排序的趟数
	int i=0;
	int flag=0;
	for(i=0;i<sz-1;i++)
	{
		int falg=1;//假设这一趟要排序的数据已经有序
		//每一趟冒泡排序
		int j=0;
		for(j=0;j<sz-1-i;j++)
		{
			if(arr[j]>arr[j+1])
			{
				int tmp=arr[j];
				arr[j]=arr[j+1];
				arr[j+1]=tmp;	
				flag=0;//本趟冒泡排序其实不完全有序					
			}
		}
		if(flag==1)
		{
			break;//跳去外层循环
		}
	}
	
}
/*冒泡排序
一趟冒泡排序
10 9 8 7 6 5 4 3 2 1
9 10 8 7 6 5 4 3 2 1
9 8 10 7 6 5 4 3 2 1
。。。
9 8  7 6 5 4 3 2 1 10
二趟冒泡排序
。。。
8 7  6 5 4 3 2 1 9 10
。。。
九趟冒泡排序
1 2 3 4 5 6 7 8 9 10
比较次数9+8+7+6+5+4+3+2+1
*/

int main()
{
	int i=0;
	int arr[]={9,8,7,6,5,4,3,2,1,0};
	int sz=sizeof(arr)/sizeof(arr[0]);
	//对arr进行排序,排成升序
	//arr是数组,我们对数组arr进行传参,实际传递过去的是arr首元素的地址 &arr[0]
	bubble_sort(arr,sz);//冒泡排序
	for(i=0;i<sz;i++)
	{
		printf("%d ",arr[i]);
	}
	return 0;
}

C语言实现三子棋

将使用test.c和game.c和game.h来增加代码的条理性
test.c        来存放菜单,main主程序,游戏主程序,测试主程序
game.c    来存放游戏相关函数实现代码
game.h    来存放头文件,声明,定义
BUG1         以后请注意printf格式问题,不要把board[i][j]放在)外面导致输出随机字符,printf(" %c ",board[i][j]);
BUG2        数组传输问题:传递整个数组不要加上[][],这样默认传输一个元素而非整个数组


test.c

//#define _CRT_SECURE_NO_WARNINGS 1 编译器不安全
#include"game.h"
void menu()
{
    printf("************************\n");
    printf("****1.play  0.exit******\n");
    printf("************************\n");
}

//游戏的实现
void game()
{
    //创建一个数组:存放走出的棋盘信息
    char board[ROW][COL] = {'0'};//全部空格
    char ret = 0;

    //初始化棋盘
    InitBoard(board, ROW, COL);//游戏的相关代码
    DisplayBoard(board, ROW, COL);//打印棋盘
    while (1)
    {
        //玩家下棋
        PlayerMove(board, ROW, COL);
        DisplayBoard(board, ROW, COL);
        //判断玩家是否赢
        ret = IsWin(board,ROW,COL);//判断游戏的四种情况: * 玩家赢  #电脑赢   Q平局  C继续下
        if (ret != 'C')
        {
            break;
        }
        //电脑下棋
        ComputerMove(board, ROW, COL);
        DisplayBoard(board, ROW, COL);
        //判断电脑是否赢
        ret = IsWin(board, ROW, COL);
        if (ret != 'C')
        {
            break;
        }
    }
    if (ret == '*')
    {
        printf("玩家赢\n");
    }
    else if (ret == '#')
    {
        printf("电脑赢\n");
    }
    else if (ret == 'Q')
    {
        printf("平局\n");
    }
}


void test()
{
    int input = 0;
    srand((unsigned int)time(NULL));
    do
    {
        menu();
        printf("请选择:>");
        scanf("%d", &input);

        switch(input)
        {
        case 1:
            printf("三子棋\n");
            game();
            break;
        case 0:
            printf("退出游戏\n");
            break;
        default:
            printf("选择错误,请重新选择!\n");
            break;
        }
    } while (input);
}

int main()
{
    test();
    return 0;
}


game.c


#include"game.h"

void InitBoard(char board[ROW][COL], int row, int col)
{
    int i = 0;
    int j = 0;
    for (i = 0; i < row; i++)
    {
        for (j = 0; j < col; j++)
        {
            board[i][j] = ' ';
        }
    }
}

//只能打印n*3
// void DisplayBoard(char board[ROW][COL],int row ,int col)
// {
    // int i=0;
    // int j=0;
    // for(i=0;i<row;i++)
    // {
        // //1.打印一行的数据
        // printf(" %c | %c | %c \n",board[i][0],,board[i][1],,board[i][2];
        // //2.打印分割行
        // if(i<row-1)
            // printf("---|---|---");        
    // }


// }
//期望打印成这个样子:
//   |   |
//---|---|---
//   |   |
//---|---|---
//   |   |

//优化:可以打印多列
void DisplayBoard(char board[ROW][COL], int row, int col)
{
    int i = 0;
    int j = 0;
    for (i = 0; i < row; i++)
    {
        //1.打印一行的数据
        int j = 0;
        for (j = 0; j < col; j++)
        {
            printf(" %c ", board[i][j]);
            if (j < col - 1)
                printf("|");
        }
        printf("\n");
        //2.打印分割行
        if (i < row - 1)
        {
            for (j = 0; j < col; j++)
            {
                printf("---");
                if (j < col - 1)
                    printf("|");
            }
            printf("\n");
        }
    }


}

void PlayerMove(char board[ROW][COL], int row, int col)
{
    int x = 0;
    int    y = 0;//横纵坐标
    printf("玩家走:>\n");

    while (1)
    {
        printf("请输入要下的坐标:>");
        scanf("%d%d", &x, &y);
        //判断坐标的合法性
        if (x >= 1 && x <= row && y >= 1 && y <= col)
        {
            if (board[x - 1][y - 1] == ' ')
            {
                board[x - 1][y - 1] = '*';
                break;
            }
            else
            {
                printf("该坐标被占用\n");
            }
        }
        else
        {
            printf("坐标非法,请重新输入!\n");
        }
    }
}

void ComputerMove(char board[ROW][COL], int row, int col)
{
    int x = 0;
    int y = 0;//横纵坐标
    printf("电脑走:>\n");
    while (1)
    {
        x = rand() % row;
        y = rand() % col;
        if (board[x][y] == ' ')
        {
            board[x][y] = '#';
            break;
        }
    }
}


//返回1表示棋盘满了
//返回0表示棋盘没满
int IsFull(char board[ROW][COL], int row, int col)
{
    int i = 0;
    int j = 0;
    for (i = 0; i < row; i++)
    {
        for (j = 0; j < col; j++)
        {
            if (board[i][j] == ' ')
            {
                return 0;//没满
            }
        }
    }
    return 1;//满了
}


char IsWin(char board[ROW][COL], int row, int col)
{
    int i = 0;
    //横三行
    for (i = 0; i < row; i++)
    {
        if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][1] != ' ')
        {
            return board[i][1];
        }
    }
    //竖三列
    for (i = 0; i < col; i++)
    {
        if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ')
        {
            return board[1][i];
        }
    }
    if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[1][1] != ' ')
        return board[1][1];
    if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[1][1] != ' ')
        return board[1][1];
    if (1 == IsFull(board, ROW, COL))
    {
        return 'Q';
    }
    else
    {
        return 'C';
    }
}
//玩家*电脑#平局Q继续C


game.h

#define _CRT_SECURE_NO_WARNINGS
#define ROW 3
#define COL 3

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

//声明
void InitBoard(char board[ROW][COL], int row, int col);
void DisplayBoard(char board[ROW][COL], int row, int col);
void PlayerMove(char board[ROW][COL], int row, int col);
char IsWin(char board[ROW][COL], int row, int col);
int IsFull(char board[ROW][COL], int row, int col);
void ComputerMove(char board[ROW][COL], int row, int col);

C语言实现一个经典扫雷小游戏

将使用test.c和game.c和game.h来增加代码的条理性
test.c 来存放菜单,main主程序,游戏主程序,测试主程序
game.c 来存放游戏相关函数实现代码
game.h 来存放头文件,声明,定义
扫雷小程序待改进的地方:
目前扫雷没用办法做到当输入坐标计算周围的雷数为0时自动展开周围为0和1的格子
实现思路:展开的思想-递归
1.坐标不是雷
2.该坐标周围也没有雷

test.c

#include "game.h"
void menu()
{
    printf("**************************************\n");
    printf("*******     1.play    ****************\n");
    printf("*******     0.exit    ****************\n");
    printf("**************************************\n");
}

void game()
{
    //雷的信息存储
    //1.布置好的雷的信息;防止数组越界,数组扩大一圈
    char mine[ROWS][COLS] = { '0' };//11*11
    //2.存放排查出来的雷的信息,给玩家打印的数组;防止数组越界,数组扩大一圈
    char show[ROWS][COLS] = { '0' };
    InitBoard(mine, ROWS, COLS, '0');
    InitBoard(show, ROWS, COLS, '*');
    DisplayBoard(show, ROW, COL);
    //布置雷
    SetMine(mine, ROW, COL);
    //DisplayBoard(mine, ROW, COL);//可用于观测雷的位置
    //扫雷
    FindMine(mine, show, ROW, COL);
}

void test()
{
    int input = 0;
    srand((unsigned int)time(NULL));
    do
    {
        menu();
        printf("请选择:>\n");
        scanf("%d", &input);
        switch (input) {
        case 1:
            game();
            break;
        case 0:
            printf("退出游戏\n");
            break;
        default:
            printf("选择错误,请重新选择!:>");
            break;
        }
    } while (input);
}

int main()
{
    test();
    return 0;
}

game.c

#include "game.h"
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
    int i = 0;
    int j = 0;
    for (i = 0; i < rows; i++)
    {
        for (j = 0; j < cols; j++)
        {
            board[i][j] = set;
        }

    }
}

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
    int i = 0;
    int j = 0;
    //打印列号
    for (i = 0; i <= col; i++)
    {
        printf(" %d ", i);
    }
    printf("\n");
    for (i = 1; i <= row; i++)
    {
        printf(" % d", i);
        for (j = 1; j <= col; j++)
        {
            printf(" %c ", board[i][j]);
        }
        printf("\n");
    }
}

void SetMine(char board[ROWS][COLS], int row, int col)
{
    int count = EASY_COUNT;
    while (count)
    {
        int x = rand() % row + 1;//1-9
        int y = rand() % col + 1;//1-9
        if (board[x][y] == '0')
        {
            board[x][y] = '1';
            count--;
        }
    }
}

int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
    //解决char转int的数字问题
    //'1'-'0'=1
    //'3'-'0'=3
    return mine[x - 1][y] +
        mine[x - 1][y - 1] +
        mine[x][y - 1] +
        mine[x + 1][y - 1] +
        mine[x + 1][y] +
        mine[x + 1][y + 1] +
        mine[x][y + 1] +
        mine[x - 1][y + 1] - 8 * '0';
}

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
    int x = 0;
    int y = 0;
    int win = 0;
    //9*9-10=71
    while (win < row * col - EASY_COUNT)
    {
        printf("请输入排查雷的坐标:>");
        scanf("%d%d", &x, &y);
        if (x >= 1 && x <= row && y >= 1 && y <= col)
        {
            //坐标合法
            //1.踩雷
            if (mine[x][y] == '1')
            {
                printf("很遗憾,你被炸死了\n");
                DisplayBoard(mine, row, col);
                break;
            }
            else //不是雷
            {
                //计算x,y周围有几个雷
                int count = get_mine_count(mine, x, y);
                show[x][y] = count + '0';
                DisplayBoard(show, row, col);
                win++;
            }
        }
        else
        {
            printf("输入坐标非法,请重新输入!\n");
        }

    }
    if (win == row * col - EASY_COUNT)
    {
        printf("恭喜你排雷成功\n");
        DisplayBoard(mine, row, col);
    }
}

game.h

#define _CRT_SECURE_NO_WARNINGS
#define ROW 9
#define COL 9
#define ROWS 11
#define COLS 11
#define EASY_COUNT 10

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

void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);

void DisplayBoard(char board[ROWS][COLS], int row, int col);

void SetMine(char board[ROWS][COLS], int row, int col);

void FindMine(char mind[ROWS][COLS], char show[ROWS][COLS], int row, int col);

int get_mine_count(char board[ROWS][COLS], int x, int y);

一次性搞定解析GitHub

github是什么?

代码托管网站

代码仓库 //类比QQ空间
C练习 数据结构 //类比QQ空间里不同的相册
代码1 代码2 //类比QQ空间里不同的相册里不同的照片

为什么选择GitHub?

1.里面有大量的开源项目可以参考学习
2.我是学习者 Github可以记录我的整个学习,为将来找工夫奠定基础
3.本日有代码上传会显示绿块,方便面试官看看你的勤分程度

GitHub使用手册

1.注册(谷歌浏览器/火狐浏览器)网址:https://github.com/
2.创建一个仓库-网上的仓库test
3.克隆项目
【安装软件Git-2.22.0.64.bit.exe—>TortoiseGit-2.8.0.0.64.exe】—>鼠标右击出现两个程序则安装成功
建立文件夹,右击GitClone–> 将拷贝的仓库网址放在URL里
代码放在这个文件夹里面

4.提交代码
右击TortoiseGit–>add //预添加代码到本地仓库(没有到云端)
–>Git commit–>message输入解释内容–>commit //添加到本地仓库(没有到云端)
TortoiseGit–>push–>输入用户名和密码—>ok //上传到网络仓库(到云端)

操作符详解

操作符和表达式

本章重点

1. 各种操作符的介绍
2. 表达式求值

各种操作符的介绍

操作符分类

算术操作符
移位操作符
位操作符
赋值操作符
单目操作符
关系操作符
逻辑操作符
条件操作符
逗号表达式
下标引用、函数调用和结构成员

算术操作符

+    -   *   /   %

1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
2. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。

移位操作符

<< 左移操作符
>> 右移操作符

左移操作符 移位规则:
左边抛弃、右边补0

右移操作符 移位规则:
首先右移运算分两种:
1. 逻辑移位 左边用0填充,右边丢弃
2. 算术移位 左边用原该值的符号位填充,右边丢弃


警告⚠: 对于移位运算符,不要移动负数位,这个是标准未定义的。

例如:

int num = 10;
num>>-1;//error

位操作符
 
位操作符有:

    &操作符:按2进制位与
                int a = 3;
                int b = 5;
                int c = a&b;//1
                //a:011
                //b:101
                //c:001
    |操作符:按2进制位或
                int a = 3;
                int b = 5;
                int c = a|b;//7
                //a:011
                //b:101
                //c:111            
    ^操作符:按2进制位异或(相同为0,相异为1)
                int a = 3;
                int b = 5;
                int c = a^b;//6
                //a:011
                //b:101
                //c:110             

注:他们的操作数必须是整数。

练习一下:

#include <stdio.h>
int main()
{
	int num1 = 1;
	int num2 = 2;
	num1& num2;
	num1| num2;
	num1^ num2;
	return 0;
}

一道变态的面试题:
不能创建临时变量(第三个变量),实现两个数的交换。

例题1:不创建临时变量,交换两个整数的值
    int a = 3;
    int b = 5;

    //加减法
    a = a+b;//a等于ab之和8
    b = a-b;//b=3;
    a = a-b;//a=5;
    缺陷:如果a和b存储较大的整形时,二进制位可能会溢出

    //异或法
    a = a^b;//a=6 b=5
    b = a^b;//a=6 b=3
    a = a^b;//a=5 b=3
    //a^b产生一个密码,密码和a异或可出b,密码和b异或可出a
    //可读性较差

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

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

例题2:求一个整数的二进制位中有几个1
    int main()
    {
        int num = 0;
        int count = 0;
        scanf("%d",&num);
        for(i=0;i<32;i++)
        {
        if (1==((num>>i)&1))
        count++;
        }
        printf("%d",num);
    }
     //32bit
     //num&1==1;则一定为1
    //统计num补码有几个1

    //整数正确,负数则错误
    /*int main()
    {
        int num = 0;
        int count = 0;
        scanf("%d",&num);
        while(num)
        {
            if (num % 2 ==1)
            count++;
            num = num / 2;
        }
        printf("%d",num);
    }*/

赋值操作符=

赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。

int weight = 120;//体重
weight = 89;//不满意就赋值
double salary = 10000.0;
salary = 20000.0;//使用赋值操作符赋值。

赋值操作符可以连续使用,比如:

int a = 10;
int x = 0;
int y = 20;
a = x = y+1;//连续赋值


这样的代码感觉怎么样?

那同样的语义,你看看:

x = y+1;
a = x;


这样的写法是不是更加清晰爽朗而且易于调试。


复合赋值符

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


这些运算符都可以写成复合的效果。 比如:

int x = 10;
x = x+10;
x += 10;//复合赋值
//其他运算符一样的道理。这样写更加简洁。

单目操作符

单目操作符有哪些呢?

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

演示代码:

#include <stdio.h>
int main()
{
	int a = -10;
	int* p = NULL;
	printf("%d\n", !2);
	printf("%d\n", !0);
	a = -a;
	p = &a;
	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(int));
	printf("%d\n", sizeof a);//这样写行
	printf("%d\n", sizeof int);//这样写不行
	return 0;
}

关于sizeof其实我们之前已经见过了,可以求变量(类型)所占空间的大小。
sizeof和数组

#include <stdio.h>
void test1(int arr[])
{
	printf("%d\n", sizeof(arr));//(2)
}
void test2(char ch[])
{
	printf("%d\n", sizeof(ch));//(4)
}
int main()
{
	int arr[10] = { 0 };
	char ch[10] = { 0 };
	printf("%d\n", sizeof(arr));//(1)
	printf("%d\n", sizeof(ch));//(3)
	test1(arr);
	test2(ch);
	return 0;
}

问:结果为?

答:

40
10
4
4


++和--运算符

前置++和--
前置++和--:

#include <stdio.h>
int main()
{
	int a = 10;
	int x = ++a;
	//先对a进行自增,然后对使用a,也就是表达式的值是a自增之后的值。x为11。
	int y = --a;
	//先对a进行自减,然后对使用a,也就是表达式的值是a自减之后的值。y为10;
	return 0;
}

//后置++和--
#include <stdio.h>
int main()
{
	int a = 10;
	int x = a++;
	//先对a先使用,再增加,这样x的值是10;之后a变成11;
	int y = a--;
	//先对a先使用,再自减,这样y的值是11;之后a变成10;
	return 0;
}

关系操作符

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

这些关系运算符比较简单,没什么可讲的,但是我们要注意一些运算符使用时候的陷阱。
警告: 在编程的过程中== 和=不小心写错,导致的错误。
 

逻辑操作符

逻辑操作符有哪些:
&&    逻辑与
||       逻辑或

区分逻辑与和按位与 区分逻辑或和按位或

1&2----->0
1&&2---->1

1|2----->3
1||2---->1

逻辑与和或的特点:360笔试题

#include <stdio.h>
int main()
{
	int i = 0, a = 0, b = 2, c = 3, d = 4;
	i = a++ && ++b && d++;
	//i = a++||++b||d++;
	printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
	return 0;
}
//程序输出的结果是什么?
a = 1
b = 2
c = 3
d = 4

条件操作符

exp1 ? exp2 : exp3
 
练习:

1.

if (a > 5)
       b = 3;
else
       b = -3;


转换成条件表达式,是什么样?

a>5?b=3:b=-3

2.使用条件表达式实现找两个数中较大值。

逗号表达式

exp1, exp2, exp3, …expN

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

//代码1
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式
c是多少?

//代码2
if (a = b + 1, c = a / 2, d > 0)

//代码3
a = get_val();
count_val(a);
while (a > 0)
{
    //业务处理
    a = get_val();
    count_val(a);
}

如果使用逗号表达式,改写:
while (a = get_val(), count_val(a), a > 0)
{
    //业务处理
}

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

1.[ ] 下标引用操作符
操作数:一个数组名 + 一个索引值

int arr[10];//创建数组
arr[9] = 10;//实用下标引用操作符。

[ ]的两个操作数是arr和9。

2.( ) 函数调用操作符 接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。

#include <stdio.h>
void test1()
{
	printf("hehe\n");
}
void test2(const char* str)
{
	printf("%s\n", str);
}
int main()
{
	test1();            //实用()作为函数调用操作符。
	test2("hello bit.");//实用()作为函数调用操作符。
	return 0;
}

3.访问一个结构的成员
. 结构体.成员名
-> 结构体指针->成员名

#include <stdio.h>
struct Stu
{
	char name[10];
	int age;
	char sex[5];
	double score;
};

void set_age1(struct Stu stu)
{
	stu.age = 18;
}
void set_age2(struct Stu* pStu)
{
	pStu->age = 18;//结构成员访问
}
int main()
{
	struct Stu stu;
	struct Stu* pStu = &stu;//结构成员访问

	stu.age = 20;//结构成员访问
	set_age1(stu);

	pStu->age = 20;//结构成员访问
	set_age2(pStu);
	return 0;
}

表达式求值

表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。


隐式类型转换

C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度
一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令
中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转
换为int或unsigned int,然后才能送入CPU去执行运算。

实例1

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
    char a = 3;
    //000000000000000000000000000000000011
    //4字节-->1字节
    //00000011
    //整型提升:按符号位提升(0则前面补0)
    //000000000000000000000000000000000011
    char b = 127;
    //000000000000000000000000000001111111
    //4字节-->1字节
    //01111111
    //整型提升:按符号位提升(0则前面补0)
    //000000000000000000000000000001111111
    char c = a + b;
    //a+b
    //000000000000000000000000000010000010
    //4字节-->1字节
    //10000010
    printf("%d\n",c);
    //%d打印的是整形
    //整型提升(1则前面补1)
    //111111111111111111111111111110000010
    //由于是复数:补码-1——>反码
    //111111111111111111111111111110000001
    //反码——>原码
    //100000000000000000000000000001111110
    //原码——>十进制打印-126
    return 0;
}

b和c的值被提升为普通整型,然后再执行加法运算。加法运算完成之后,结果将被截断,然后再存储于a中。
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的

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

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

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

整形提升的例子:

//实例1
#include<stdio.h>
int main()
{
	char a = 0xb6;
	short b = 0xb600;
	int c = 0xb6000000;
	if (a == 0xb6)
		printf("a");
	if (b == 0xb600)
		printf("b");
	if (c == 0xb6000000)
		printf("c");
	return 0;
}
//结果为c

实例2

#include<stdio.h>
int main()
{
	char c = 1;
	printf("%u\n", sizeof(c));
	printf("%u\n", sizeof(+c));
	printf("%u\n", sizeof(!c));
	return 0;
}

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

算术转换

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

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

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
警告: 但是算术转换要合理,要不然会有一些潜在的问题。

float f = 3.14;
int num = f;//隐式转换,会有精度丢失
 

操作符的属性

复杂表达式的求值有三个影响的因素。
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
操作符优先级

操作

描述

用法示例

结果类

是否控制求值

顺序

()

聚组

(表达式)

与表达

式同

N/A

()

函数调用

rexprexp...,rexp

rexp

L-R

[ ]

下标引用

rexp[rexp]

lexp

L-R

.

访问结构成员

lexp.member_name

lexp

L-R

->

访问结构指针成员

rexp->member_name

lexp

L-R

++

后缀自增

lexp ++

rexp

L-R

--

后缀自减

lexp --

rexp

L-R

!

逻辑反

! rexp

rexp

R-L

~

按位取反

~ rexp

rexp

R-L

+

单目,表示正值

+ rexp

rexp

R-L

-

单目,表示负值

- rexp

rexp

R-L

++

前缀自增

++ lexp

rexp

R-L

--

前缀自减

-- lexp

rexp

R-L

*

间接访问

* rexp

lexp

R-L

&

取地址

& lexp

rexp

R-L

sizeof

取其长度,以字节

表示

sizeof rexp sizeof(

)

rexp

R-L

(

型)

类型转换

(类型) rexp

rexp

R-L

*

乘法

rexp * rexp

rexp

L-R

/

除法

rexp / rexp

rexp

L-R

%

整数取余

rexp % rexp

rexp

L-R

+

加法

rexp + rexp

rexp

L-R

-

减法

rexp - rexp

rexp

L-R

<<

左移位

rexp << rexp

rexp

L-R

>>

右移位

rexp >> rexp

rexp

L-R

>

大于

rexp > rexp

rexp

L-R

>=

大于等于

rexp >= rexp

rexp

L-R

<

小于

rexp < rexp

rexp

L-R

<=

小于等于

rexp <= rexp

rexp

L-R

==

等于

rexp == rexp

rexp

L-R

!=

不等于

rexp != rexp

rexp

L-R

&

位与

rexp & rexp

rexp

L-R

^

位异或

rexp ^ rexp

rexp

L-R

|

位或

rexp | rexp

rexp

L-R

&&

逻辑与

rexp && rexp

rexp

L-R

||

逻辑或

rexp || rexp

rexp

L-R

? :

条件操作符

rexp ? rexp : rexp

rexp

N/A

=

赋值

lexp = rexp

rexp

R-L

+=

...

lexp += rexp

rexp

R-L

-=

...

lexp -= rexp

rexp

R-L

*=

...

lexp *= rexp

rexp

R-L

/=

...

lexp /= rexp

rexp

R-L

%=

...取模

lexp %= rexp

rexp

R-L

<<=

...左移

lexp <<= rexp

rexp

R-L

>>=

...右移

lexp >>= rexp

rexp

R-L

&=

...

lexp &= rexp

rexp

R-L

^=

...异或

lexp ^= rexp

rexp

R-L

|=

...

lexp |= rexp

rexp

R-L

逗号

rexprexp

rexp

L-R


一些问题表达式

表达式的求值部分由操作符的优先级决定。


表达式1
a*b + c*d + e*f

注释:代码1在计算的时候,由于*比+的优先级高,只能保证,*的计算是比+早,但是优先级并不能决定第三个*比第一个+早执行。
所以表达式的计算机顺序就可能是:

a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f

或者:
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f

表达式2
c + --c;

注释:同上,操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得
知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
 

代码3-非法表达式

int main()
{
	int i = 10;
	i = i-- - --i * (i = -3) * i++ + ++i;
	printf("i = %d\n", i);
	return 0;
}

表达式3在不同编译器中测试结果:非法表达式程序的结果

编译器

—128

Tandy 6000 Xenix 3.2

—95

Think C 5.02(Macintosh)

—86

IBM PowerPC AIX 3.2.5

—85

Sun Sparc cc(K&C编译器)

—63

gccHP_UX 9.0Power C 2.0.0

4

Sun Sparc acc(K&C编译器)

21

Turbo C/C++ 4.5

22

FreeBSD 2.1 R

30

Dec Alpha OSF1 2.0

36

Dec VAX/VMS

42

Microsoft C 5.1

代码4

#include <stdio.h>
int fun()
{
    static int count = 1;
    return ++count;
}
int main()
{
    int answer;
    answer = fun() - fun() * fun();
    printf("%d\n", answer);//输出多少?
    return 0;
}

这个代码有没有实际的问题?
有问题!
虽然在大多数的编译器上求得结果都是相同的。
但是上述代码 answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。
函数的调用先后顺序无法通过操作符的优先级确定。
 

代码5

#include <stdio.h>
int main()
{
	int i = 1;
	int ret = (++i) + (++i) + (++i);
	printf("%d\n", ret);
	printf("%d\n", i);
	return 0;
}


//尝试在linux 环境gcc编译器,VS2013环境下都执行,看结果。

Linux环境的结果:

 
VS2019环境的结果:


看看同样的代码产生了不同的结果,这是为什么?
简单看一下汇编代码.就可以分析清楚.
这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级
和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。

 
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。


 
指针

本章重点

1. 指针是什么
2. 指针和指针类型
3. 野指针
4. 指针运算
5. 指针和数组
6. 二级指针
7. 指针数组

指针是什么?

在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向
(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以
说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址
的内存单元。


那我们就可以这样理解:
内存

指针
指针是个变量,存放内存单元的地址(编号)。


那对应到代码:

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

总结:指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
那这里的问题是:
一个小的单元到底是多大?(1个字节)
如何编址?
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电/负电(1或者0)
那么32根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
...
11111111 11111111 11111111 11111111
 
这里就有2的32次方个地址。
每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空闲进行编址。
同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算。
这里我们就明白:
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
总结:
指针是用来存放地址的,地址是唯一标示一块地址空间的。
指针的大小在32位平台是4个字节,在64位平台是8个字节。

指针和指针类型

这里我们在讨论一下:指针的类型 我们都知道,变量有不同的类型,整形,浮点型等。
那指针有没有类型呢? 准确的说:有的。

当有这样的代码:

int num = 10;
p = &num;

要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢? 我们
给指针变量相应的类型。

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

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

其实: char* 类型的指针是为了存放 char 类型变量的地址。

short* 类型的指针是为了存放 short 类型变量的地址。

int* 类型的指针是为了存放int 类型变量的地址。


那指针类型的意义是什么?
指针+-整数

#include <stdio.h>
//演示实例
int main()
{
	int n = 10;
	char* pc = (char*)&n;
	int* pi = &n;

	printf("%p\n", &n);
	printf("%p\n", pc);
	printf("%p\n", pc + 1);
	printf("%p\n", pi);
	printf("%p\n", pi + 1);
	return  0;
}
/*结果
010FFBF0
010FFBF0
010FFBF1
010FFBF0
010FFBF4
*/

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


指针的解引用

//演示实例
#include <stdio.h>

int main()
{
	int n = 0x11223344;
	char* pc = (char*)&n;
	int* pi = &n;
	*pc = 0;   //重点在调试的过程中观察内存的变化。
	*pi = 0;   //重点在调试的过程中观察内存的变化。
	return 0;
}


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

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

野指针

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


野指针成因

1. 指针未初始化

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

2. 指针越界访问

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

3. 指针指向的空间释放

int* test()
{
	int a=10;//局部变量,出{}的时候被释放,假设a的地址0x0012ff44
	return &a;//0x0012ff44
}
int main()
{
	int* p=test();//0x0012ff44
	printf("%d\n",*p);//*p访问回去已经不是a了
	return 0;
}


如何规避野指针

1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放即使置NULL
4. 指针使用之前检查有效性
 

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

指针运算

指针+- 整数
指针-指针
指针的关系运算

指针+-整数
p++; p--; p+=2; p-=2;都可以

指针-指针

要求:1.指向同一个空间 2.两个指针类型要相同
结果:中间的元素个数(大-小为正数,小-大为负数)
例题:
指针求字符串长度

#include<stdio.h>
int my_strlen(char str)
{
    char* start = str;
    char* end = str;
    while(*end!='\0')
    {
        end++;
    }
    return end - start;
}

int main()
{
    char arr[]="abcd";
    int i=my_strlen(arr);
    printf("%d\n",i);
    return 0;
}

指针的关系运算

由于数组的地址是从低地址指向高地址 , 指针可以进行比较:p<&arr[5]
注意:C语言语法允许指向元素的指针和指向数组后面的指针比较,不允许和指向数组前面的指针比较
    例如内存里:     p3 arr[0] arr[1] arr[2] p2
                                                        p1
                              p1可以和p2比较,不可以和p3比较

指针和数组

数组名是什么?我们看一个例子:

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	printf("%p\n", arr);
	printf("%p\n", &arr[0]);
	return 0;
}

运行结果:
00B2F8E4
00B2F8E4

 
可见数组名和数组首元素的地址是一样的。
结论:数组名表示的是数组首元素的地址。


那么这样写代码是可行的:

int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址

 
既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。

例如:
 

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

运行结果:

所以 p+i 其实计算的是数组 arr 下标为 i 的地址。
那我们就可以直接通过指针来访问数组。
如下:

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

二级指针 

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

int main()
{
    int a=10;            //           a   的值10                a的地址0x0012ff40
    int* pa=&a;          //一级指针    pa  的值0x0012ff40       pa的地址0x0012ff48
    int* * ppa=&pa;      //二级指针    ppa 的值0x0012ff48      ppa的地址0x0012ff56
    int** * ppa=&ppa;    //三级指针    pppa的值0x0012ff56     pppa的地址0x0012ff68
    return 0;
}
  int*              *                 ppa
指针指向的类型      说明ppa是个指针    变量名

对于二级指针的运算有:
*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa .

**ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a .

**ppa = 30;       等价于*pa = 30;       等价于a = 30;

指针数组

指针数组是指针还是数组?
答案:是数组。是存放指针的数组。
数组我们已经知道整形数组,字符数组。

int arr1[5];
char arr2[6];

那指针数组是怎样的?

int* arr3[5];//是什么?

arr3是一个数组,有五个元素,每个元素是一个整形指针。

结构体

  • 结构体类型的声明
  • 结构体初始化
  • 结构体成员访问
  • 结构体传参

结构体的声明

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

struct tag
{
	member - list;
}variable - list;

例如描述一个学生:

typedef struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
}Stu;//分号不能丢

结构成员的类型
结构的成员可以是标量、数组、指针,甚至是其他结构体。

结构体变量的定义和初始化

有了结构体类型,那如何定义变量,其实很简单。

struct Point
{
	int x;
	int y;
}p1;		    //声明类型的同时定义变量p1
struct Point p2;   //定义结构体变量p2

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

例子1:

struct Stu        //类型声明
{
	char name[15];//名字
	int age;      //年龄
};

struct Stu s = { "zhangsan", 20 };//初始化

例子2:结构体嵌套初始化

struct Node
{
	int data;
	struct Point p;
	struct Node* next;
}n1 = { 10, {4,5}, NULL };


struct Node n2 = {20, {5, 6}, NULL};

结构体成员的访问

结构体变量访问成员 结构变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数。 例如:

 
我们可以看到 s 有成员 name 和 age ; 那我们如何访问s的成员?

struct S s;
strcpy(s.name, "zhangsan");//使用.访问name成员
s.age = 20;//使用.访问age成员

结构体指针访问指向变量的成员 有时候我们得到的不是一个结构体变量,而是指向一个结构体的指针。


那该如何访问成员。 如下:

#include <stdio.h>
struct Stu
{
	char name[20];
	int age;
};

void print(struct Stu* ps)
{
	printf("name = %s   age = %d\n", (*ps).name, (*ps).age);
	//使用结构体指针访问指向对象的成员
	printf("name = %s   age = %d\n", ps->name, ps->age);
}

int main()

{
	struct Stu s = { "zhangsan", 20 };
	print(&s);//结构体地址传参
	return 0;
}

结构体传参

直接上代码:

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

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

上面的 print1 和 print2 函数哪个好些?
答案是:首选print2函数。 原因:
函数传参的时候,参数是需要压栈的。 如果传递一个结构体对象的时候,结构体过大,参数压栈
的的系统开销比较大,所以会导致性能的下降。
结论: 结构体传参的时候,要传结构体的地址。

实用调试技巧

  • 什么是bug?
  • 调试是什么?有多重要?
  • debug和release的介绍。
  • windows环境调试介绍。
  • 一些调试的实例。
  • 如何写出好(易于调试)的代码。
  • 编程常见的错误。

什么是bug?

第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误。
 

调试是什么?有多重要?

所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心有
愧,就必然需要掩盖,那就一定会有迹象,迹象越多就越容易顺藤而上,这就是推理的途径。顺
着这条途径顺流而下就是犯罪,逆流而上,就是真相。
一名优秀的程序员是一名出色的侦探。
每一次调试都是尝试破案的过程。
 
调试是什么?

我们是如何写代码的?


又是如何排查出现的问题的呢?

 
拒绝-迷信式调试!!!!
调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。

调试的基本步骤

  • 发现程序错误的存在
  • 以隔离、消除等方式对错误进行定位
  • 确定错误产生的原因
  • 提出纠正错误的解决办法
  • 对程序错误予以改正,重新测试

Debug和Release的介绍

Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。

代码:

#include <stdio.h>
int main()
{
	char* p = "hello bit.";
	printf("%s\n", p);
	return 0;
}

上述代码在Debug环境的结果展示:

上述代码在Release环境的结果展示:


 

所以我们说调试就是在Debug版本的环境中,找代码中潜伏的问题的一个过程。
那编译器进行了哪些优化呢? 请看如下代码:

#include <stdio.h>
int main()
{
    int i = 0;
    int arr[10] = { 0 };
    for (i = 0; i <= 12; i++)
    {
        arr[i] = 0;
        printf("hehe\n");
    }
    return 0;
}

如果是 debug 模式去编译,程序的结果是死循环。 如果是 release 模式去编译,程序没有死循环。

那他们之间有什么区别呢? 就是因为优化导致的。

数组越界访问的时候,将i的值归零,导致死循环

release 模式,内存位置选择最优,所以i的位置不确定,数组越界访问的时候,不一定将i的值归零

变量在内存中开辟的顺序发生了变化,影响到了程序执行的结果。

windows环境调试介绍

注:linux开发环境调试工具是gdb

1. 调试环境的准备
在环境中选择 debug 选项,才能使代码正常调试。

2. 学会快捷键

最常使用的几个快捷键:

F5
启动调试,经常用来直接调到下一个断点处。


F9
创建断点和取消断点 断点的重要作用,可以在程序的任意位置设置断点。这样就可以使得程序在
想要的位置随意停止执行,继而一步步执行下去。

F5和F9配合使用


F10
逐过程
,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。


F11
逐语句
,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)


CTRL + F5
开始执行不调试
,如果你想让程序直接运行起来而不调试就可以直接使用。


shift+F5结束调试

3. 调试的时候查看程序当前信息
查看临时变量的值

在调试开始之后,用于观察变量的值。

1.开始调试(点击逐语句)

2.

 3.


查看内存信息
在调试开始之后,用于观察内存信息。

 
查看调用堆栈

调试--->窗口--->调用堆栈

通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置。


查看汇编信息
在调试开始之后,右击鼠标,选择【转到反汇编】:

或者调试--->窗口--->反汇编
可以切换到汇编代码。


查看寄存器信息

调试--->窗口--->寄存器
可以查看当前运行环境的寄存器的使用信息。

4.多多动手,尝试调试,才能有进步。
一定要熟练掌握调试技巧。
初学者可能80%的时间在写代码,20%的时间在调试。但是一个程序员可能20%的时间在写程序,但是80%的时间在调试。
我们所讲的都是一些简单的调试。 以后可能会出现很复杂调试场景:多线程程序的调试等。
多多使用快捷键,提升效率。

一些调试的实例

实例一

实现代码:求 1!+2!+3! ...+ n! ;不考虑溢出。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
	int i = 0;
	int sum = 0;//保存最终结果
	int n = 0;
	int ret = 1;//保存n的阶乘
	scanf("%d", &n);
	for (i = 1; i <= n; i++)
	{
		int j = 0;
		for (j = 1; j <= i; j++)
		{
			ret *= j;
		}
		sum += ret;
	}
	printf("%d\n", sum);
	return 0;
}

这时候我们如果3,期待输出9,但实际输出的是15。
 
why? 这里我们就得找我们问题。
1. 首先推测问题出现的原因。初步确定问题可能的原因最好。
2. 实际上手调试很有必要。
3. 调试的时候我们心里有数。

实例二

#include <stdio.h>
int main()
{
    int i = 0;
    int arr[10] = { 0 };
    for (i = 0; i <= 12; i++)
    {
        arr[i] = 0;
        printf("hehe\n");
    }
    return 0;
}

研究程序死循环的原因。
 

如何写出好(易于调试)的代码

优秀的代码:

1. 代码运行正常
2. bug很少
3. 效率高
4. 可读性高
5. 可维护性高
6. 注释清晰
7. 文档齐全


常见的coding技巧:
1. 使用assert
2. 尽量使用const
3. 养成良好的编码风格
4. 添加必要的注释
5. 避免编码的陷阱。

示范:

模拟实现库函数:strcpy

/***
*char *strcpy(dst, src) - copy one string over another
*
*Purpose:
*       Copies the string src into the spot specified by
*       dest; assumes enough room.
*
*Entry:

*       char * dst - string over which "src" is to be copied
*       const char * src - string to be copied over "dst"
*
*Exit:
*       The address of "dst"
*
*Exceptions:
*******************************************************************************/

char* strcpy(char* dst, const char* src)
{
    char* cp = dst;
    assert(dst && src);

    while (*cp++ = *src++)
        ;     /* Copy src over dst */
    return(dst);
}


例题2:实现字符串拷贝

#include<atdio.h>
void my_strcpy(char* dest ,char* src)
{
    while(*src !='\0')
    {
    *dest = *src;
    src++;
    dest++;
    }
    *dest = *src;//'\0'
}
int main()
{
    //strcpy
    //字符串拷贝
    char arr1[]="##########";
    char arr2[]="bit";
    my_strcpy(arr1,arr2);//arr2拷贝到arr1中
    printf("%s\n",arr2);//结果为bit
    return 0;
}


优化1:

void my_strcpy(char* dest ,char* src)
{
    while(*src !='\0')
    {
    *dest++ = *src++;
    }
    *dest = *src;//'\0'
}


优化2:

void my_strcpy(char* dest ,char* src)
{
    while(*dest++ = *src++)//'\0'的ASCII码为0
    {
        ;
    }
}


优化3:规避空指针导致程序崩溃的问题
 

void my_strcpy(char* dest ,char* src)
{
    if(dest!=NULL && src!=NULL)
    {
        while(*dest++ = *src++)//'\0'的ASCII码为0
        {
            ;
        }
    }
}

优化4:利用断言报错,来方便程序员查找问题修改代码

#include<assert.h>
void my_strcpy(char* dest ,char* src)
{
    assert(dest!=NULL);
    assert(src!=NULL);
    while(*dest++ = *src++)//'\0'的ASCII码为0
    {
        ;
    }
}

优化5:const保护源头的数据,避免拷贝错误
 

#include<assert.h>
void my_strcpy(char* dest ,const char* src)
{
    assert(dest!=NULL);
    assert(src!=NULL);
    while(*dest++ = *src++)//'\0'的ASCII码为0
    {
        ;
    }
}

优化6:改变文件类型,返回目标字符串的地址
 

#include<assert.h>
char* my_strcpy(char* dest ,const char* src)
{
    char* ret = dest;
    assert(dest!=NULL);
    assert(src!=NULL);
    while(*dest++ = *src++)
    {
        ;
    }
    return ret;
}

优化7:添加注释
 

#include<assert.h>
char* my_strcpy(char* dest ,const char* src)
{
    char* ret = dest;
    assert(dest!=NULL);
    assert(src!=NULL);
    //把src指向的字符串拷贝到dest指向的空间,包含'\0'字符
    while(*dest++ = *src++)
    {
        ;
    }
    return ret;
}

注意:
1. 参数的设计(命名,类型),返回值类型的设计
2. 野指针,空指针的危害巨大,可能使程序崩溃
3. assert的使用:可以让程序员容易找到哪里出错
4. 参数部分 const 的使用,const修饰指针的作用
5. 注释的添加


const的作用

例子讲解:const语法限制


无const情况:

int main()
{
    int num=10;
    int* p=&num;
    *p=20;
    printf("%d\n",num);//20
}

const修饰num

int main()
{
    const int num=10;//num不能改变
    int* p=&num;
    *p=20;
    printf("%d\n",num);//但是还是改变了20
}

//报错:*p不能改变num的值

const修饰*p
 

int main()
{
    const int num=10;
    const int* p=&num;//p可以改
    //err const放在指针变量的*左边时,修饰的*p,也就是说,不能通过p来改变*p(num)的值
    *p=20;
    printf("%d\n",num);
}

const修饰p

int main()
{
    const int num=10;
    //const放在指针变量的*右边时,修饰的指针变量p,也就是说,p不能被改变
    int* const p=&num;//*p可以改
    *p=20;//结果为20
    printf("%d\n",num);
}

优化8:最终版

#include<stdio.h>
#include<assert.h>
int my_strlen(const char* str)
{
    int count = 0;
    assert(str!=NULL);//保证指针的有效性
    
    while(*str!='\0')
    {
        count++;
        str++;
    }
    return count;
}

int main()
{
    char arr[]="abcdef";
    int len =my_strlen (arr);
    printf("%d\n",len);
    return 0;
}

结论:
const修饰指针变量的时候:
1. const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改
变。但是指针变量本身的内容可变。
2. const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指
针指向的内容,可以通过指针改变。
注: 介绍《高质量C/C++编程》一书中最后章节试卷中有关 strcpy 模拟实现的题目。


练习:
模拟实现一个strlen函数 参考代码:

#include <stdio.h>
#include <assert.h>
int my_strlen(const char* str)
{
    int count = 0;
    assert(str != NULL);
    while (*str)//判断字符串是否结束
    {
        count++;
        str++;
    }
    return count;
}
int main()
{
    const char* p = "abcdef";
    //测试
    int len = my_strlen(p);
    printf("len = %d\n", len);
    return 0;
}

编程常见的错误

常见的错误分类:

编译型错误
直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。
链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
运行时错误
借助调试,逐步定位问题。最难搞。


温馨提示:
做一个有心人,积累排错经验。
 

数据的存储目录

1. 数据类型详细介绍
2. 整形在内存中的存储:原码、反码、补码
3. 大小端字节序介绍及判断
4. 浮点型在内存中的存储解析

数据类型介绍

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

类型的意义:
1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
2. 如何看待内存空间的视角

类型的基本归类:

整形家族:
char
    存储是ASCII码,就是一个数字
    unsigned char    (没有符号位,可以表示的数更大)
    signed char     (最高位为符号位)
short
    unsigned short [int]
    signed short [int]
int
    unsigned int
    signed int
long
    unsigned long [int]
    signed long [int]


浮点数家族:
float
double

构造类型(自定义类型):
> 数组类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union

指针类型
int* pi;
char* pc;
float* pf;
void* pv;(无具体类型的指针)

空类型:
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型。
例如:test()可以传参也可以传参,void表示不需要参数

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

整形在内存中的存储:原码、反码、补码

        1. 有符号整数 正数:原码、反码和补码相同
                               负数:原码、反码和补码不同,要进行计算
        2. 无符号整数 原码、反码和补码相同

计算规则:
原码:直接将二进制按照正负数的形式翻译成二进制就可以。
反码:将原码的符号位不变,其他位依次按位取反就可以得到了。
补码:反码 +1 就得到补码。


整形在内存中的存储
变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
那接下来我们谈谈数据在所开辟内存中到底是如何存储的?


int a = 20;//4个字节32bit
//00000000000000000000000000010100原码
//00000000000000000000000000010100反码
//00000000000000000000000000010100补码
//内存展书的是16进制
//0x00000014
//0x表示16进制

int b = -10;//4个字节32bit
//1000 0000 0000 0000 0000 0000 0000 1010原码
//1111 1111 1111 1111 1111 1111 1111 0101反码
//1111 1111 1111 1111 1111 1111 1111 0110补码
//0xFFFFFFF6
//0x表示16进制

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

为什么使用补码?
原因在于,使用补码,可以将符号位和数值域统一处理;

同时,加法和减法也可以统一处理(CPU只有加法器)

此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

一切运算都可以通过加法实现

例子:
1+(-1)
00000000000000000000000000000001(1的补码)
10000000000000000000000000000001(-1的原码)
11111111111111111111111111111110(-1的反码)
11111111111111111111111111111111(-1的补码)
100000000000000000000000000000000(1+(-1)的补码)
00000000000000000000000000000000(1+(-1)的补码:去掉前面超出范围的,得到0的补码)

大小端字节序介绍及判断

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

例如:
0x                               11                  22 33    44
表示数字是16进制     数据的高位                 数据的低位
内存块
|-------------------------------------------------------------------------------------|
|    11 22 33 44(大端存储模式)         44 33 22 11(小端存储模式)    |
|-------------------------------------------------------------------------------------|
低地址                                                                                  高地址

百度2015年系统工程师笔试题:
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)

概念:大端(字节序存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
          小端(字节序存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中。

小程序:

int a=1;
0x00 00 00 01
           小端:
           01 00 00 00
低地址------------------>高地址
          大端:
          00 00 00 01

指针类型的意义:
1.决定*可以访问几个字节char* 一个字节  int* 四个字节
2.决定+1 -1时,加减几个字节


解题思路:取内存中的第一个字节,14是小端,00是大端

解法1:
 

#include<stdio.h>
int main()
{
    //写一段代码告诉我们当前机器的字节序是什么
    int a=1;
    char* p=(char*)&a;
    if(*p==1)
    {
    printf("小端\n");
    }
    else
    {
    printf("大端\n");
    }
    return 0;
}

解法2:函数版

#include <stdio.h>
int check_sys()
{
    int i = 1;
    return (*(char *)&i);
}
int main()
{
    //写一段代码告诉我们当前机器的字节序是什么
    //返回1,小端
    //返回0,大端
    int ret = check_sys();
    if(ret == 1)
    {
    printf("小端\n");
    }
    else
    {
    printf("大端\n");
    }
return 0;
}

char相关知识

signed char
unsigned char
1个字节
8个bit位
2^8=256

内存看char

内存补码      有符号数
0000 0000    0
0000 0001    1
0000 0010    2
0000 0011    3
…………
0111 1111    127
----------正数负数分界线----------------
1000 0000    -128
1000 0001    -127
…………
1111 1101    -3
1111 1110    -2
1111 1111    -1
有符号的char的范围-128-->127

内存补码      无符号数
0000 0000    0
0000 0001    1
0000 0010    2
0000 0011    3
…………
0111 1111    127
1000 0000    128
1000 0001    129
…………
1111 1101    253
1111 1110    254
1111 1111    255
无符号的char的范围0-->255

七道例题加深char理解

例题1:

输出什么?
 

#include <stdio.h>
int main()
{
   char a= -1;
   signed char b=-1;
   unsigned char c=-1;
   printf("a=%d,b=%d,c=%d",a,b,c);//-1 -1 255
   return 0;
}

解析版:
#include
int main()
{
   char a= -1;
//11111111111111111111111111111111
//11111111
//整形提升:按符号位提升,由于char是有符号数,所以符号位是1,所以在前面补1
//11111111111111111111111111111111

   signed char b=-1;
//11111111
//整形提升:按符号位提升,由于char是有符号数,所以符号位是1,所以在前面补1
//11111111111111111111111111111111

   unsigned char c=-1;
//11111111
//整形提升:按符号位提升,由于unsigned char是无符号数,所以没有符号位,所以在前面补0
//00000000000000000000000011111111
//正数:255

   printf("a=%d,b=%d,c=%d",a,b,c);//-1 -1 255
   return 0;
}


例题2:

输出什么?
 

#include <stdio.h>
int main()
{
   char a = -128;
   printf("%u\n",a);
   return 0;
}

解析版:
#include
int main()
{
   char a = -128;
//10000000000000000000000010000000
//01111111111111111111111101111111
//01111111111111111111111110000000-补码
//10000000
//11111111111111111111111110000000整形提升

   printf("%u\n",a);
//打印的是无符号数,直接看其为原码
//4294967168
//%d 有符号10进制整数
//%u 无符号10进制整数
   return 0;
}

例题3:

解析版:
 

#include <stdio.h>
int main()
{
char a = 128;//127+1相当于-128,和例题2一样
printf("%u\n",a);
return 0;
}

例题4:

输出什么?

int i= -20;
unsigned int j = 10;
printf("%d\n", i+j);

解析版:
int i= -20;
unsigned int j = 10;
printf("%d\n", i+j);//-10
//按照补码的形式进行运算,最后格式化成为有符号整数
11111111 11111111 11111111 11101100//-20的补码
00000000 00000000 00000000 00001010//10的补码
11111111 11111111 11111111 11110110//结果的补码
10000000 00000000 000000000 00001010//原码-10

例题5:

输出什么?

unsigned int i;
for(i = 9; i >= 0; i--)
{
printf("%u\n",i);
}

解析版:
9876543210然后死循环-1对应的超大的数

例题6:

输出什么?
 

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

解析版:

int main()
{
char a[1000];//0-999,但char只能放-128到127
int i;
for(i=0; i<1000; i++)
{
   a[i] = -1-i;
}//-1 -2 …… -127 -128 127 126 125……3 2 1 0(strlen读到这停止) -1 -2……
printf("%d",strlen(a));//strlen读到'\0'停止:255
return 0;
}

例题7:

输出什么?

#include <stdio.h>

unsigned char i = 0;
int main()
{
for(i = 0;i<=255;i++)
{
   printf("hello world\n");
}
return 0;
}

解析版:
结果:死循环
原因:无符号数255+1=0

浮点型在内存中的存储

常见的浮点数:
3.1415926535

E10(科学计数法:E前面的数10^10)
浮点数家族包括: float、double、long double 类型。
浮点数表示的范围:float.h中定义
整数表示的范围:      limits.h中定义

浮点数存储的例子:

整数存储浮点拿出差别大

浮点存储整数拿出差别大

#include<stdio.h>
int main()
{
	int n = 9;
	float* pFloat = (float*)&n;
	printf("n的值为: % d\n", n);//9
	printf("*pFloat的值为:%f\n", *pFloat);//0.000000

	*pFloat = 9.0;
	printf("num的值为: % d\n", n);//1091567616
	printf("*pFloat的值为:%f\n", *pFloat);//9.000000
	return 0;
}

浮点数存储和读取详细解读:

存储:

根据国际标准IEEE(电气和电子工程协会) 754,

任意一个二进制浮点数V可以表示成下面的形式:

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

  • (-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
  • M表示二进制形式的有效数字,大于等于1,小于2。
  • 2^E表示指数位。

例子
9.0
二进制形式1001.0
(-1)^0*1.001*2^ 3
(-1)^S * M * 2^E
S=0
M=1.001
E=3

IEEE 754规定: 对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
*###########&&&&&&&&&&&&&&&&&&&&&&&
S(1bit)E(11bit)M(23bit)

对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
*###########&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
S(1bit)E(11bit) M(52bit)

M

计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分,可以节省1位有效数字

E

至于指数E,情况就比较复杂。
首先,E为一个无符号整数(unsigned int)
这意味着,如果E为8位,它的取值范围为0~255;(32位机器)
                  如果E为11位,它的取值范围为0~2047。(64位机器)
但是,我们知道,科学计数法中的E是可以出现负数的,所以规定,存入内存时E的真实值必须再加上一个中间数,
                  对于8位的E,这个中间数是127;
                  对于11位的E,这个中间数是1023。
比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
2^-1的E是-1,所以保存成32位浮点数时,必须保存成-1+127=126

例子:

5.5
二进制形式101.1
(-1)^0 * 1.011 * 2^2
S=0
M=1.011
E=2-->129-->10000001
0 10000001 011 00000000000000000000
0100 0000 1011 0000 0000 0000 0000 0000
0x40b00000

十进制的小数怎么转换成二进制

将0.125换算为二进制,结果为:0.001

第一步,将0.125乘以2,得0.25,则整数部分为0,小数部分为0.25。

第二步, 将小数部分0.25乘以2,得0.5,则整数部分为0,小数部分为0.5。

第三步, 将小数部分0.5乘以2,得1.0,则整数部分为1,小数部分为0.0。

第四步,读数,从第一位读起,读到最后一位,即为0.001。

读取:

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


E不全为0或不全为1(正常情况)
E的计算值减去127(或1023),得到真实值,
再将有效数字M前加上第一位的1


E全为0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值, 有效数字M不再加上第一位的1,而是还原为
0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
例子
0 00000000 011 00000000000000000000
+/-   0.011 * 2^-126
//以及接近于0的很小的数字

E全为1
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);
例子
0 11111111 011 00000000000000000000
E+127=255
E=128
1.xxxx * 2^128
//表示正负无穷的数字


上面例题解析
 

#include <stdio.h>
int main()
{
	int n = 9;
	//0 00000000 0000000000000000000001001-补码
	float* pFloat = (float*)&n;
	printf("n的值为: % d\n", n);//9
	printf("*pFloat的值为:%f\n", *pFloat);//0.000000
	//(-1)^0 0.0000000000000000000001001 * 2^ -126

	*pFloat = 9.0;
	//1001.0
	//(-1)^0 1.001*2^3
	//0 100010 001000000000000000000000
	printf("num的值为: % d\n", n);//把它当原码1091567616
	printf("*pFloat的值为:%f\n", *pFloat);//9.000000
	return 0;
}

指针的进阶目录

1. 字符指针
2. 数组指针
3. 指针数组
4. 数组传参和指针传参
5. 函数指针
6. 函数指针数组
7. 指向函数指针数组的指针
8. 回调函数
9. 指针和数组面试题的解析

指针

1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
3. 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候访问的字节个数。
4. 指针的运算。

字符指针

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

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

通过指针打印字符串

int main()
{
    char arr[]="abcdef";
    char* pc=arr;
    printf("%s\n",arr);
    printf("%s\n",pc);
    return 0;
}

/*
结果:
abcdef
abcdef
*/

常量字符串

int main()
{
    char* p="abcdef";//"abcdef\0"是一个常量字符串,p中存的是a的地址
    //本质是把字符串 abcdef 首字符的地址放到了p中
    printf("%c\n",*p);//a
    printf("%s\n",p);//abcdef
    return 0;
}

错误写法

尝试修改常量字符串
err:Segmentation fault - 段错误

int main()
{
    char* p="abcdef";//"abcdef\0"是一个常量字符串,p中存的是a的地址
    *p = 'W';//常量字符串不可以改,正确的写法应该在char* p前面加const
    printf("%s\n",p);
    return 0;
}


例题:

请问打印结果是什么?
 

int main()
{
    char arr1[]="abcdef";
    char arr2[]="abcdef";
    char * p1="abcdef";
    char * p2="abcdef";
    if(arr1==arr2)
    {
        printf("hehe\n");
    }
    else
    {
        printf("haha\n");
    }
    if(p1==p2)
    {
        printf("hehe\n");
    }
    else
    {
        printf("haha\n");
    }
}

结果:

haha,首元素的地址

hehe,常量字符串只有一份

解析:
这里p1和p2指向的是一个同一个常量字符串。
C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。
指向同一个字符串的时候,他们实际会指向同一块内存。

但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。

指针数组

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


使用实例

#include<stdio.h>
int main()
{
    int arr1[] = { 1,2,3,4,5 };
    int arr2[] = { 2,3,4,5,6 };
    int arr3[] = { 3,4,5,6,7 };

    int* parr[] = { arr1,arr2,arr3 };
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        int j = 0;
        for (j = 0; j < 5; j++)
        {
            printf("%d",*(parr[i] + j));
        }
        printf("\n");
    }
    return 0;
}
/*结果:
12345
23456
34567
*/

数组指针

数组指针的定义
数组指针是指针?还是数组?
答案是:指针。

指针的类型

整形指针: int * p; 能够指向整形数据的指针,可以存放整形的地址
字符指针: char * p;能够指向字符数据的指针,可以存放字符的地址
浮点型指针: float * p; 能够指向浮点型数据的指针,可以存放浮点型的地址
数组指针:能够指向数组的指针,可以存放数组的地址

arr不同地方不同意义

int arr[10]={0};
arr-首元素的地址
&arr[0]-首元素的地址
&arr-数组的地址
sizeof(arr)-数组的大小

数组指针

int arr[10]={1,2,3,4,5,6,7,8,9,10};
int(*p)[10] = &arr;//数组的地址要存起来
//上面的p就是数组指针
//p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组
//[]的优先级要高于*号的,所以必须加上()来保证p先和*结合

例子:

char* arr[5];
char*(*pa)[5]=&arr;

pa-变量名字
*pa-指针
[5]-pa指向的数组有5个元素
char*-pa指向的数组的类型是char*


&数组名VS数组名

arr是数组名,数组名表示数组首元素的地址。
&arr和arr,虽然值是一样的,但是意义应该不一样的。
&arr 表示的是数组的地址,而不是数组首元素的地址
数组的地址+1,跳过整个数组的大小// &arr+1


 
数组指针的使用:二维数组以上才方便些
两种方式打印二维数组示例

#include<stdio.h>
//参数是数组的形式
void print1(int arr[3][5], int x, int y)
{
    int i = 0;
    int j = 0;
    for (i = 0; i < x; i++)
    {
        for (j = 0; j < y; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

//参数是指针的形式
void print2(int(*p)[5], int x, int y)
{
    int i = 0;
    for (i = 0; i < x; i++)
    {
        int j = 0;
        for (j = 0; j < y; j++)
        {
            printf("%d ", *(*(p + i) + j));
            //写法2:printf("%d ",(*(p+i))[j]);
            //写法3:printf("%d ",*(p[i]+j);
            //写法4:printf("%d ",p[i][j];
        }
    }
}
/*
类比解析
int i=0
int* p =arr;
*(p+i)==*(arr+i)==arr[i]==p[i]

解析
*(*(p+i)+j)==(*(p+i))[j])==*(p[i]+j)==p[i][j]
p+i是数组指针,*可以拿到这个数组首元素地址
*/


int main()
{
    int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
    print1(arr, 3, 5);//数组名就是首元素地址(sizeof和&arr除外)
    print2(arr, 3, 5);//把arr想象成一维数组,首元素为{1,2,3,4,5}的地址,传过去是一维数组的地址
}


 深入理解


int arr[5];//arr是一个5个元素的整形数组
int *parr1[10];//parr1是一个10个元素的数组,每个元素类型为int*,parr1是指针数组
int (*parr2)[10];//parr2是指针,指向10个元素的数组,数组类型是int,parr2是数组指针
int (*parr3[10])[5];//parr3是一个10个元素的数组,每个元素是一个数组指针,该数组指针指向的数组有5个元素,该数组的类型是int
详解:
int (*                )[5];//数组类型:数组指针
        parr3[10]//数组名[元素个数]

数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

一维数组传参

#include <stdio.h>
void test(int arr[])//ok
{}
void test(int arr[10])//ok
{}
void test(int *arr)//ok,传过去的也是首元素的地址
{}
void test2(int *arr[])//ok
{}
void test2(int *arr[20])//ok
{}
void test2(int **arr)//ok,二级指针
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}

二维数组传参

void test(int arr[3][5])//ok
{}
void test(int arr[][])//err列不能省略
{}
void test(int arr[3][])//err列不能省略
{}
void test(int arr[][5])//ok行可以省略
{}
void test(int *arr)//err二维数组的首元素地址是第一行的地址
{}
void test(int* arr[5])//err指针数组
{}
void test(int (*arr)[5])//ok指针,指向五个元素的数组,数组里面是int类型
{}
void test(int **arr)//err
{}
int main()
{
int arr[3][5] = {0};
test(arr);//二维数组传参
}


一级指针传参

#include <stdio.h>
void print(int* p, int sz)//变量地址,一级指针
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d\n", *(p + i));
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//一级指针p,传给函数
	print(p, sz);
	return 0;
}


二级指针传参

#include <stdio.h>
void test(int** ptr)
{
	printf("num = %d\n", **ptr);//一级指针变量地址,二级指针,接受指针数组数组名
}
int main()
{
	int n = 10;
	int* p = &n;
	int** pp = &p;
	test(pp);
	test(&p);
	return 0;
}

函数指针

数组指针-指向数组的指针
函数指针-指向函数的指针-存放函数地址的一个指针

函数

int Add(int x,int y)
{
    return x+y;
}

打印函数地址

printf("%p\n",&Add);
printf("%p\n",Add);

结果一样:&函数名 和 函数名都是函数的地址

存储函数的地址
 

int (*pa) (int int)=Add;

调用函数指针
(pa)(2,3)
(*pa)(2,3)
(**pa)(2,3)
(***pa)(2,3)
*pa(2,3)//err:pa和(2,3)结合==5,对5解引用报错
*是摆设,只是方便理解
结果都是5

阅读两段《C陷阱和缺陷》的有趣的代码:
//代码1

(*(void (*)())0)();

解释:
去掉变量名字以后就是类型

void (*)()         -函数指针
(void (*)())       -强制类型转化,转化成函数指针
(void (*)())0      -0是某函数的地址
*(void (*)())0     -*进行解引用-找到0为地址函数
(*(void (*)())0)() -调用地址为0的函数

//代码2

void (*signal(int , void(*)(int)))(int);

解释:
 

signal                            -------是一个函数声名
signal()                          -------signal函数
signal(int , void(*)(int))        -------siganl函数参数有整形和函数指针
void (*                    )(int) -------里面函数返回值的类型也是一个函数指针

简化:

typedef void(*pfun_t)(int);//typedef 起名字:给void (*        )(int)起名字为pfun_t
pfun_t signal(int, pfun_t);//signal(int, pfun_t)函数的类型是pfun_t

函数指针数组

什么是函数指针数组?

1.函数指针数组是一个数组

2.数组里面存放的是函数指针

3.函数指针是指向函数的指针,也就是存放函数地址的变量


数组是一个存放相同类型数据的存储空间,指针数组int *arr[10];//数组的每个元素是int*

函数的地址存到一个数组中,就叫函数指针数组
 

int (*pa)(int,int)=Add;//函数指针
parr//指针名字
parr[4]//四个指针的数组
int (* )(int,int)//数组元素的类型为函数指针
int (*parr[4])(int,int)={Add,Sub,Mul,Div};//函数指针数组

函数指针数组调用:下标调用
 

int i=0;
for(i=0;i<4;i++)
{
    printf("%d\n",parr[i](2,3));
}

例题练习:

char* my_strcpy(char* dest,const char* src);

1.写一个函数指针pf,能够指向my_strcpy
 

//类型-变量名=赋值的错误写法
//char* (*)(char* ,const char*)              pf=&my_strcpy

//正确写法
char* (*pf)(char* ,const char*) =&my_strcpy;


2.写一个函数指针数组pfArr,能够存放4个my_strcpy函数的地址
 

//类型-变量名=赋值的错误写法
//char*(*)(char* ,const char*)              pfArr[4]={my_strcpy,my_strcpy,my_strcpy,my_strcpy}

//正确写法
char*(*pfArr[4])(char* ,const char*) ={my_strcpy,my_strcpy,my_strcpy,my_strcpy};

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

例子:(计算器)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void menu()
{
    printf("*********************\n");
    printf("1:add           2:sub\n");
    printf("3:mul           4:div\n");
    printf("0.exit               \n");
    printf("*********************\n");
}

int Add(int a, int b)
{
    return a + b;
}
int Sub(int a, int b)
{
    return a - b;
}
int Mul(int a, int b)
{
    return a * b;
}
int Div(int a, int b)
{
    return a / b;
}
int main()
{
    int x, y;
    int input = 0;
    do
    {
        menu();
        printf("请选择:");
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            printf("%d\n", Add(x, y));
            break;
        case 2:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            printf("%d\n", Sub(x, y));
            break;
        case 3:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            printf("%d\n", Mul(x, y));
            break;
        case 4:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            printf("%d\n", Div(x, y));
            break;
        case 0:
            printf("退出程序\n");
            break;
        default:
            printf("选择错误\n");
            break;
        }
    } while (input);
    return 0;
}


代码冗余:
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
回调函数改进:
 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void menu()
{
	printf("*********************\n");
	printf("1:add           2:sub\n");
	printf("3:mul           4:div\n");
	printf("0.exit               \n");
	printf("*********************\n");
}

int Add(int a, int b)
{
	return a + b;
}
int Sub(int a, int b)
{
	return a - b;
}
int Mul(int a, int b)
{
	return a * b;
}
int Div(int a, int b)
{
	return a / b;
}
int Calc(int (*pf)(int x, int y))
{
	int x = 0;
	int y = 0;
	printf("输入操作数:");
	scanf("%d %d", &x, &y);
	printf("%d\n", pf(x, y));
}
int main()
{
	int x, y;
	int input = 0;
	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			Calc(Add);
			break;
		case 2:
			Calc(Sub);
			break;
		case 3:
			Calc(Mul);
			break;
		case 4:
			Calc(Div);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}


使用函数指针数组的实现:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void menu()
{
    printf("*********************\n");
    printf("1:add           2:sub\n");
    printf("3:mul           4:div\n");
    printf("0.exit               \n");
    printf("*********************\n");
}

int Add(int a, int b)
{
    return a + b;
}
int Sub(int a, int b)
{
    return a - b;
}
int Mul(int a, int b)
{
    return a * b;
}
int Div(int a, int b)
{
    return a / b;
}

int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    int(*pfArr[5])(int x, int y) = { 0, Add, Sub, Mul, Div }; //函数指针数组:转移表
    while (input)
    {
        menu();
        printf("请选择:");
        scanf("%d", &input);
        if ((input <= 4 && input >= 1))
        {
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = pfArr[input](x, y);
            printf("%d\n", ret);
        }
        else if (input == 0)
        {
            printf("退出");
        }
        else
        {
            printf("选择错误");
        }
        return 0;
    }
}

回调函数

详细定义:

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

方便理解的例子:

#include<stdio.h>
void print()//被指针指向的函数
{
    printf("结果是:\n");
}
int Add(int x, int y,void(*p)())
{
    (*p)();//这个指针被用来调用其所指向的函数时,就是回调函数
    return x + y;
}

int main()
{
    int a = 2, b = 3;
    void (*p)() = &print;//函数指针
    int sum = Add(2,3,p);//把函数的指针(地址)作为参数传递给另一个函数
    printf("%d", sum);
    return 0;
}

冒泡排序

冒泡排序的原理

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。

  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。

  3. 针对所有的元素重复以上的步骤,除了最后一个。

  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

回忆之前写过的冒泡排序

#include<stdio.h>
void bubble_sort(int arr[], int sz)
{
    int i = 0;
    //趟数
    for (i = 0; i < sz - 1; i++)
    {
        //一趟冒泡排序
        int j = 0;
        for (j = 0; j < sz - 1 - i; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

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

    bubble_sort(arr, sz);
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ",arr[i]);
    }
    return 0;
}

qsort-快速排序任意类型的数据

1.头文件stdlib.h
2.两个元素的比较方法不一样,需要使用者自己输入一个函数来比较两个数据

3.使用方法:

#include<stdlib.h>

int cmp_XXX (const void *e1,const void *e2)
{
    整形  return (int)(*(int*)e1    -   *(int*)e1);
    浮点型return (int)(*(float*)e1    -   *(float*)e1);
    结构体return (int)(((struct Stu*)e1)->指向结构体内部的元素-((struct Stu*)e2)->指向结构体内部的元素);
}

int sz=sizeo数组名(数组名)/sizeo数组名(数组名[0]);

qsort(      数组首元素的地址也就是数组名      ,        数组的元素个数sz           ,          单个元素的字节数sizeo(数组名[0])          ,         使用者自己编写的函数cmp_XXX      );

函数原型

void qsort(void* base,//快速排序的目标,待排序数组的首元素的地址
                size_t num,//待排序数组的元素的个数
                size_t width,//待排序数组的单个元素的字节数
                int(*compare)(const void *e1,const void *e2)//比较两个目标的地址,相等返回0,小于返回负数,大于返回正数,
                //这个函数需要使用者自己实现
                );

void* 类型的指针

void* 类型的指针可以接受任意类型的地址
void* 类型的指针 不能进行解引用操作
void* 类型的指针 不能进行加减整数的操作
存储例如:void* p=&a;(a是任意类型的数据)


下面通过代码来体验一下qsort函数和自己编写的冒泡排序升级(可以接受任意类型的数据)

附有较为详细的注释

    test1();//qsort排序整形
    test2();//qsort排序浮点型
    test3();//qsort排序结构体
    test4();//冒泡排序整形
    test5();//冒泡排序结构体

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//比较整数数据
int cmp_int(const void* e1, const void* e2)
{
    //比较两个整形值的
    return *(int*)e1 - *(int*)e1;
}

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

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

//比较浮点型数据
int cmp_float(const void* e1, const void* e2)
{
    return (int)(*(float*)e1 - *(float*)e1);
}

void test2()
{
    float f[] = { 9.0,8.0,7.0,6.0,5.0,4.0,3.0,2.0,1.0 };
    int sz = sizeof(f) / sizeof(f[0]);

    qsort(f, sz, sizeof(f[0]), cmp_float);

    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%f\n", f[i]);
    }
}

//比较结构体型数据
struct Stu
{
    char name[20];
    int age;
};

int cmp_stu_by_age(const void* e1, const void* e2)
{
    return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}

int cmp_stu_by_name(const void* e1, const void* e2)
{
    //比较名字就是比较字符串,应该用strcmp函数,比较字符串的,头文件为string.h
    return strcmp(((struct Stu*)e1)->name , ((struct Stu*)e2)->name);
}

void test3()
{
    struct Stu s[3] = { {"zhangsan",20},{"lisi",30},{"wangwu",10} };
    int sz = sizeof(s) / sizeof(s[0]);
    qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
    qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}


// void qsort(void* base,//快速排序的目标,待排序数组的首元素的地址
                // size_t num,//待排序数组的元素的个数
                // size_t width,//待排序数组的单个元素的字节数
                // int(*compare)(const void *e1,const void *e2)//比较两个目标的地址,相等返回0,小于返回负数,大于返回正数,
                // //这个函数需要使用者自己实现
                // );


//改造冒泡排序
//实现bubble_sort的程序员,他不知道未来排序的数据类型
//那程序员也不知道,待比较两个元素的类型
void(Swap)(char* buf1, char* buf2, int width)
{
    int i = 0;
    for (i = 0; i < width; i++)
    {
        char tmp = *buf1;
        *buf1 = *buf2;
        *buf2 = tmp;
        buf1++;
        buf2++;
    }
}

void bubble_sort(void* base, int sz, int width, int (*cmp)(const void* e1, const void* e2))
{
    int i = 0;
    //趟数
    for (i = 0; i < sz - 1; i++)
    {
        //一趟冒泡排序
        int j = 0;
        for (j = 0; j < sz - 1 - i; j++)
        {
            //两个元素的比较
            //*void无法直接加减
            //(char*)将其强制转成字符指针可以加减,加减1跳过一个字节
            if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
            {
                //交换
                Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
            }
        }
    }
}


void test4()
{
    int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    //使用bubble_sort函数的程序员一定知道自己排序的是什么数据
    //就应该知道如何比较待比较的数据的方式
    bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
}

void test5()
{
    struct Stu s[3] = { {"zhangsan",20},{"lisi",30},{"wangwu",10} };
    int sz = sizeof(s) / sizeof(s[0]);
    bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);
    bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}

int main()
{
    test1();
    test2();
    test3();
    test4();
    test5();
    return 0;
}

指针和数组笔试题解析

本章总结

数组名的意义:
sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
除此之外所有的数组名都表示首元素的地址。

一维数组

整形数组

int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a+0));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(a[1]));
printf("%d\n",sizeof(&a));
printf("%d\n",sizeof(*&a));
printf("%d\n",sizeof(&a+1));
printf("%d\n",sizeof(&a[0]));
printf("%d\n",sizeof(&a[0]+1));

解析:
 

//地址字节数:
//win32:4
//win64:8
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
//sizeof(数组名)-计算的是整个数组的大小-单位字节
//16
printf("%d\n",sizeof(a+0));
//数组名是首元素的地址
//1.sizeof(数组名)-计算的是整个数组的大小
//2.&数组名-数组名表示的是整个数组
//这里的a+0还是首元素的地址
//4/8
printf("%d\n",sizeof(*a));
//*首元素的地址=首元素的大小
//4
printf("%d\n",sizeof(a+1));
//第二个元素的地址
//4/8
printf("%d\n",sizeof(a[1]));
//第二个元素的大小=整形大小=int=4字节
//4
printf("%d\n",sizeof(&a));
//&a取出数组的地址
//地址的大小为4
//4/8
printf("%d\n",sizeof(*&a));
//&a取出数组的地址
//*&a访问数组==a
//16
printf("%d\n",sizeof(&a+1));
//&a是一个数组的地址
//&a+1虽然地址跳过一个数组的地址,但还是一个地址
//4/8
printf("%d\n",sizeof(&a[0]));
//第一个元素的地址
//4/8
printf("%d\n",sizeof(&a[0]+1));
//第二个元素的地址
//4/8

字符数组
 

char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));


解析:
 

char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
//sizeof计算的是数组的大小,6*1=6
//6
printf("%d\n", sizeof(arr+0));
//arr首元素的地址,arr+0也还是首元素的地址
//4/8
printf("%d\n", sizeof(*arr));
//arr是首元素的地址,*arr是首元素
//1
printf("%d\n", sizeof(arr[1]));
//arr[1]是第二个元素
//1
printf("%d\n", sizeof(&arr));
//数组的地址还是地址
//4/8
printf("%d\n", sizeof(&arr+1));
//地址1+1=地址2
//地址大小为4/8
//4/8
printf("%d\n", sizeof(&arr[0]+1));
//第二个元素的地址
//4/8


字符数组
 

char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));

解析:
 

char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));
//strlen找到'\0'才停止
//随机值
printf("%d\n", strlen(arr+0));
//随机值
printf("%d\n", strlen(*arr));
//strlen要地址
//ASCII码中'a'=97
//读取0x00000061的地址然后向后非法读取
//代码错误
printf("%d\n", strlen(arr[1]));
//strlen要地址
//ASCII码中'b'=98
//读取98的地址然后向后非法读取
//代码错误
printf("%d\n", strlen(&arr));
//&arr是首元素的地址,也是'a'的地址
//随机值
printf("%d\n", strlen(&arr+1));
//&arr+1跳过数组的地址向后走
//随机值
printf("%d\n", strlen(&arr[0]+1));
//从b开始向后找'\0'
//随机值

字符数组

char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));

解析:
 

char arr[] = "abcdef";
//'a' 'b' 'c' 'd' 'e' 'f' '\0'
printf("%d\n", sizeof(arr));
//sizeof计算的是整个空间的大小,strlen整个空间的大小-1('\0')
//sizeof计算的是数组的大小,7*1=7
//7
printf("%d\n", sizeof(arr+0));
//地址
//4/8
printf("%d\n", sizeof(*arr));
//'a'的大小
//1
printf("%d\n", sizeof(arr[1]));
//'b'的大小
//1
printf("%d\n", sizeof(&arr));
//地址
//4/8
printf("%d\n", sizeof(&arr+1));
//地址
//4/8
printf("%d\n", sizeof(&arr[0]+1));
//地址
//4/8

字符数组

char arr[] = “abcdef”;
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));

解析:

char arr[] = “abcdef”;
printf("%d\n", strlen(arr));
//'a' 'b' 'c' 'd' 'e' 'f' '\0'
//strlen不算'\0'
//6
printf("%d\n", strlen(arr+0));
//首元素地址
//从首元素地址向后数
//6
printf("%d\n", strlen(*arr));
//strlen接受的是地址const char* str
//*arr是'a'ASCII码为97
//地址为97==0x00000061发生访问错误
//err
printf("%d\n", strlen(arr[1]));
//err
printf("%d\n", strlen(&arr));
//warning:const char和char(*)[7]的类型不同
//&arr是数组的地址-数组指针-char(*)[7]
//6
printf("%d\n", strlen(&arr+1));
//warning:const char和char(*)[7]的类型不同
//随机值
printf("%d\n", strlen(&arr[0]+1));
//'b'向后读取
//5

字符数组

char *p = “abcdef”;
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p+1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p+1));
printf("%d\n", sizeof(&p[0]+1));

解析:
 

char *p = “abcdef”;
//p存放"abcdef"的地址
printf("%d\n", sizeof(p));
//地址
//计算指针变量的大小
//4/8
printf("%d\n", sizeof(p+1));
//p存的是'a'的地址
//p+1是'b'的地址
//4/8
printf("%d\n", sizeof(*p));
//*p=首元素
//'a'
//1
printf("%d\n", sizeof(p[0]));
//int arr[10];arr[0] == *(arr+0)
//p[0] == *(p+0) == 'a'
//1
printf("%d\n", sizeof(&p));
//地址
//4/8
printf("%d\n", sizeof(&p+1));
//地址
//4/8
printf("%d\n", sizeof(&p[0]+1));
//'b'的地址
//4/8

字符数组

char *p = “abcdef”;
printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));

解析:
 

char *p = “abcdef”;
printf("%d\n", strlen(p));
//p的地址传过去='a'的地址
//6
printf("%d\n", strlen(p+1));
//5
printf("%d\n", strlen(*p));
//err
printf("%d\n", strlen(p[0]));
//err
printf("%d\n", strlen(&p));
//a b c d e f \0
//取p的地址向后读
//地址不可知
//随机值
printf("%d\n", strlen(&p+1));
//随机值
printf("%d\n", strlen(&p[0]+1));
//'b'的地址
//5

二维数组


int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof((a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof((a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));

解析:
 

//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));
//数组总大小
//48
printf("%d\n",sizeof(a[0][0]));
//一个整形元素的大小
//4
printf("%d\n",sizeof(a[0]));
//a[0]相当于第一行作为一维数组的数组名
//sizeof(数组名)=第一行的数组的大小
//16
printf("%d\n",sizeof(a[0]+1));
//a[0]相当于第一行作为一维数组的数组名 == 第一行第一个元素的地址
//a[0]+1第一行第二个元素的地址
//4/8
printf("%d\n",sizeof(*(a[0]+1)));
//4
printf("%d\n",sizeof(a+1));
//二维数组看成一维数组
//二维数组的首元素是他第一行
//a == 第一行的地址
//a+1 ==第二行的地址
//4/8
printf("%d\n",sizeof(*(a+1)));
//a+1 ==第二行的地址
//计算第二行的大小
//16
printf("%d\n",sizeof(&a[0]+1));
//第二行的地址
//4/8
printf("%d\n",sizeof(*(&a[0]+1)));
//第二个行的大小
//16
printf("%d\n",sizeof(*a));
//a是首元素的地址 == 第一行的地址
//16
printf("%d\n",sizeof(a[3]));
//sizeof不会访问()内的内容,不参加真实运算,只会通过括号内的类型判断大小
//16

指针笔试题

笔试题1:
 

int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf( “%d,%d”, *(a + 1), *(ptr - 1));
return 0;
}
//程序的结果是什么?

解析:
 

int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf( “%d,%d”, *(a + 1), *(ptr - 1));
return 0;
}
//做题方法:画图
//2,5

笔试题2

//已知结构体的大小是20个字节
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}* p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
int main()
{
printf("%p\n", p + 0x1);

printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int)p + 0x1);
return 0;
}

解析:
指针+-整数

 

//结构体的大小是20个字节
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}* p;//结构体指针
//假设p 的值为0x100000。
int main()
{
printf("%p\n", p + 0x1);
// 123456789abcdefg
// 0x1==1
// 20->16+4->14
// 0x100014
printf("%p\n", (unsigned long)p + 0x1);
// 整数+1=整数
// 0x00100014->1048576
// 1048576+1==1048577==0x00100001
// 0x100001
printf("%p\n", (unsigned int)p + 0x1);
// 整形跳过4
// 0x00100004
return 0;
}


笔试题3

int main()
{
   int a[4] = { 1, 2, 3, 4 };
   int *ptr1 = (int *)(&a + 1);
   int *ptr2 = (int *)((int)a + 1);
   printf( "%x,%x", ptr1[-1], *ptr2);
   return 0;
}

解析:
内存(小端存储)
01 00 00 00 | 02 00 00 00  | 03 00 00 00 | 04 00 00 00
低地址                                                                高地址

//%x意思是16进制输出( 以16进制输出变量地址)
int main()
{
   int a[4] = { 1, 2, 3, 4 };
   int *ptr1 = (int *)(&a + 1);
   int *ptr2 = (int *)((int)a + 1);
   //(int)a-->首元素地址转整形,例如0x00000005
   //(int)a+1-->整形+1=整形(向后访问一个字节),例如5+1=6
   //地址,例如0x00000006
   printf( "%x,%x", ptr1[-1], *ptr2);
   //ptr1[-1]==*(ptr1+(-1))=*(ptr1-1)
   //4->0x00 00 00 04
   
   //00 00 00 | 02
   //0x02000000
   //0x02 00 00 00
   return 0;
}


笔试题4

#include <stdio.h>
int main()
{
   int a[3][2] = { (0, 1), (2, 3), (4, 5) };
   int *p;
   p = a[0];
   printf( "%d", p[0]);
return 0;
}

解析:

#include <stdio.h>
int main()
{
   int a[3][2] = { (0, 1), (2, 3), (4, 5) };//逗号表达式{1,3,5}
   int *p;
   p = a[0];//1 3
   printf( "%d", p[0]);//1
return 0;
}
// right
// 1 3
// 5 0
// 0 0

//err idea
// 0 1 --p--p[0]==0
// 2 3
// 4 5


笔试题5

int main()
{
   int a[5][5];
   int(*p)[4];
   p = a;
   printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
   return 0;
}

解析:
 

int main()
{
   int a[5][5];
   int(*p)[4];//指向四个元素数组的指针
   p = a;//首元素a[0]的地址
   printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
   //结果FFFFFFFC和-4
   return 0;
}

 a[0]     a[1]        a[2]      a[3]            a[4]  
|****|*   ***|**      **|***    *|**|**         **|***
|      |        |            |         |   |               |
P    p+1   p+2       p+3   p+4 p[4][2]   a[4][2]

p[4][2]==*(*(p+4)+2)
&p[4][2] - &a[4][2]//指针相减==指针中间有几个元素==4
//小-大==-4
//%d打印-4原码10000000 00000000 00000000 00000100
10000000 00000000 00000000 00000100原码
11111111 11111111 11111111 11111011反码
11111111 11111111 11111111 11111100补码
//%p打印地址,-4在内存中用补码存储,直接看成地址打印
11111111 11111111 11111111 11111100
F   F    F    F     F   F    F   C
FFFFFFFC

笔试题6

int main()
{
   int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
   int *ptr1 = (int *)(&aa + 1);
   int *ptr2 = (int *)(*(aa + 1));
   printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
   return 0;
}


解析:

int main()
{
   int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
   int *ptr1 = (int *)(&aa + 1);
   int *ptr2 = (int *)(*(aa + 1));
   printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
   //10和5
   return 0;
}


1 2 3 4 5 |6 7 8 9 10|
               |                |
               |                &aa+1
              aa+1
*(aa + 1)==aa[1]第二行首元素的地址
第二行的地址 解引用 等于 第二行的数组名aa[1]

笔试题7

#include <stdio.h>

int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}

解析:
 

#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
//char* p="abcdef";//存的是a的地址
//a
// char* "work"
// char* "at"
// char* "alibaba"
char* *pa = a;
//a == 首元素的地址 == 首元素是指针是"work"的地址 == 指针的地址是二级指针
//*pa表明是指针
//char*是指向的类型
pa++;
//跳过一个char*的字节数
//指向"at"的指针
printf("%s\n", *pa);
//at
return 0;
}


笔试题8

int main()
{
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char**cp[] = {c+3,c+2,c+1,c};
char***cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *--*++cpp+3);
printf("%s\n", *cpp[-2]+3);
printf("%s\n", cpp[-1][-1]+1);
return 0;
}


解析:

//%s在C语言中代表字符串型格式符。
int main()
{
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char* *cp[] = {c+3,c+2,c+1,c};
char** *cpp = cp;
printf("%s\n", **++cpp);//打印POINT
printf("%s\n", *--*++cpp+3);//打印出ER
printf("%s\n", *cpp[-2]+3);//打印出ST
printf("%s\n", cpp[-1][-1]+1);//打印出EW
return 0;
}


c数组
char* 存放"ENTER"首字符地址
char* 存放"NEW"首字符地址
char* 存放"POINT"首字符地址
char* 存放"FIRST"首字符地址

cp[]数组
char**指针 c+3"FIRST"
char**指针 c+2"POINT"
char**指针 c+1"NEW"
char**指针 c"ENTER"

cpp指针指向cp[]数组
cpp指向cp首元素的地址

**++cpp
++cpp//c+2的地址
*++cpp//拿到c+2的内容
**++cpp//通过c+2的内容,拿到"POINT"
//打印POINT

*--*++cpp+3
cpp//c+2的地址
++cpp//指向c+1的地址(由于上一条语句已经改变了)
*++cpp//拿到c+1的内容
--*++cpp//拿到c的内容
*--*++cpp//拿到ENTER的内容
*--*++cpp+3//拿到E的地址
//打印出ER

*cpp[-2]+3
cpp//c+1的地址
cpp-2//c+3的地址
cpp[-2] == *(cpp+(-2)) == *(cpp-2)//拿到c+3
*cpp[-2]+3//拿到S的地址
//打印出ST

cpp[-1][-1]+1
cpp[-1] == *(cpp-1)
cpp[-1][-1] == *(*(cpp-1)-1)
cpp[-1][-1]+1 == *(*(cpp-1)-1)+1
cpp//c+1的地址
cpp-1//c+2的地址
*(cpp-1)//拿到c+2
*(cpp-1)-1//拿到c+1
*(*(cpp-1)-1)//拿到NEW,就是N的地址
*(*(cpp-1)-1)+1//拿到E的地址
//打印出EW

赛马问题:36匹马,6个跑道,没有计时器,请赛马确定36匹马的前三名


请问赛几次?
8次

步骤一:36匹马平均分成6份赛一次
123456
123456
123456
123456
123456
123456


步骤二:取6次比赛的第一的马赛一次


步骤三:取步骤二中第一名的马所在步骤一中组成的前三名马
和步骤二中第二名的马所在步骤一中组成的前二名马
和步骤二中第三名的马赛一次


步骤四:步骤三中的前三名就是所求前三名

烧香问题 有一种香材质不均匀,但是每一根这样的香,烧完恰好1小时 给两根香,帮我确定15分钟的时间

烧香问题
有一种香材质不均匀,但是每一根这样的香,烧完恰好1小时
给两根香,帮我确定15分钟的时间

1.两头点燃第一根香,一头点燃第二根香

*-----------------------*

------------------------*

2.30mins后第一根香烧完,第二根香剩下一半
-------*
3.再点燃另一边后就是还能烧15mins

*-------*

字符函数和字符串函数

本章重点

重点介绍处理字符和字符串的库函数的使用和注意事项
求字符串长度
strlen
长度不受限制的字符串函数
strcpy
strcat
strcmp
长度受限制的字符串函数介绍
strncpy
strncat
strncmp
字符串查找
strstr
strtok
错误信息报告
strerror
字符操作
内存操作函数
memcpy
memmove
memset
memcmp

前言

C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,
字符串通常放在 常量字符串 中或者 字符数组 中。
字符串常量 适用于那些对它不做修改的字符串函数.

函数介绍

字符串求长度

strlen字符串求长度

库函数内的声明
size_t strlen ( const char * str );

简单例子
 

#include<stdio.h>
#include<string.h>
int main()
{
    char arr[]="hello";
    int len=strlen(arr);
    printf("%d",len);
}
//结果:5

使用:

1.字符串已经 ‘\0’ 作为结束标志,strlen函数返回的是在字符串中 ‘\0’ 前面出现的字符个数(不包含 ‘\0’ )。
2.参数指向的字符串必须要以 ‘\0’ 结束。
3.注意函数的返回值为size_t,是无符号的( 易错 )
size_t == unsigned int
两个strlen相减结果为负数的时候,结果存放负数的补码,当成无符号整形,一定大于等于0
例如:
 

#include <stdio.h>
int main()
{
const charstr1 = “abcdef”;
const charstr2 = “bbb”;
if(strlen(str2)-strlen(str1)>0)
{
printf(“str2>str1\n”);
}
else
{
printf(“srt1>str2\n”);
}
return 0;
}
//结果:str2>str1

4.学会strlen函数的模拟实现

my_strlen三种实现方法
1.计数器的方法

#include<assert.h>
int my_strlen(const char* str)
{
    int count=0;
    assert(str!=NULL);
    
    while(*str!='\0')
    {
        count++;
        str++;
    }
    return count;
}


2.递归
3.指针-指针

长度不受限制的字符串函数

strcpy字符串拷贝

库函数内的声明
char* strcpy(char * destination, const char * source );

简单例子
 

#include<stdio.h>
#include<string.h>
int main()
{
    char arr1[]="abcdef";
    char arr2[]="ghi";
    strcpy(arr1,arr2);
    printf("%s",arr1);
}
//结果:ghi

使用:
1.源字符串必须以 ‘\0’ 结束。
2.会将源字符串中的 ‘\0’ 拷贝到目标空间。
3.目标空间必须足够大,以确保能存放源字符串。
4.目标空间必须可变,也就是说目标空间不能为常量字符串,或者const修饰
5.学会模拟实现。

my_strcpy实现方法

char* my_strcpy( char* dest,const char* src)
{
    assert(dest!=NULL);
    assert(src!=NULL);
    char* ret =dest;
    //拷贝src指向的字符串到dest指向的空间,包含'\0'
    while(*dest++ = *src++)
    {
        ;
    }
    //返回目的空间的启示地址
    return ret;
}

strcat字符串追加

库函数内的声明
char * strcat ( char * destination, const char * source );

简单例子
 

#include<stdio.h>
#include<string.h>
int main()
{
    char arr1[30]="hello";
    char arr2[]="world";
    strcat(arr1,arr2);
    printf("%s",arr1);
}
//结果:helloworld

使用:
1.源字符串必须以 ‘\0’ 结束。
2.目标空间必须有足够的大,能容纳下源字符串的内容。
3.目标空间必须可修改。
4.字符串自己给自己追加,找不到'\0',导致死循环,使得程序崩溃

my_strcat实现方法

#include <stdio.h>
#include <assert.h>
char* my_strcat(char* dest ,const char* src)
{
    assert(dest!=NULL);
    assert(src!=NULL);
    char* ret=dest;
    //1.找到目的字符串的'\0'
    while(*dest!='\0')
    {
        dest++;
    }
    //2.追加
    while(*dest++=*src++)
    {
        ;
    }
    return ret;
}

int main()
{
    char arr1[30]="hello";
    char arr2[]="world";
    my_strcat(arr1,arr2)
    printf("%s",arr1);
}

strcmp字符串比较

库函数内的声明
int strcmp ( const char * str1, const char * str2 );

简单例子:
 

#include<stdio.h>
#include<string.h>
int main()
{
    char* p1="abcdef";//p1拿到的是a的地址
    char* p2="sqwer";//p2拿到的是b的地址
    if(strcmp(p1,p2)>0)
    {
        printf("p1>p2");
    }
    else if(strcmp(p1,p2)==0)
    {
        printf("p1=p2");
    }
    else
    {
        printf("p1<p2");
    }
    return 0;
}

标准规定:
第一个字符串大于第二个字符串,则返回大于0的数字
第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字

例子解释如何进行字符串比较
abcf\0
abce\0
1.ASCII码下,'a'=='a'比较下一对
2.ASCII码下,'b'=='b'比较下一对
3.ASCII码下,'c'=='c'比较下一对
4.ASCII码下,'f'>'e',第一个ASCII码大,返回大于1的数据

abc\0
abc\0
1.ASCII码下,'a'=='a'比较下一对
2.ASCII码下,'b'=='b'比较下一对
3.ASCII码下,'c'=='c'比较下一对
4.ASCII码下,'\0'=='\0',ASCII码相同,返回0的数据

abc\0
abcd\0
1.ASCII码下,'a'=='a'比较下一对
2.ASCII码下,'b'=='b'比较下一对
3.ASCII码下,'c'=='c'比较下一对
4.ASCII码下,'\0'<'d',第二个ASCII码大,返回小于1的数据my_strcat自我实现

#include<stdio.h>
#include<assert.h>
int my_strcmp(const char* str1,const char* str2)
{
    assert(str1&&str2);
    while(*str1==*str2)
    {
        if(*str1=='\0')
        {
            return 0;
        }
        str1++;
        str2++;
    }
    return (*str1-*str2);

}

int main()
{
    char* p1="abcdef";//p1拿到的是a的地址
    char* p2="sqwer";//p2拿到的是b的地址
    int ret=my_strcmp(p1,p2);
    printf("%d\n",ret);
    return 0;
}

长度受限制的字符串函数介绍

strncpy指定个数的字符串拷贝

库函数内的声明
char * strncpy ( char * destination, const char * source, size_t num );
简单例子

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int main()
{
    char arr1[10] = "abcdefgh";
    char arr2[] = "bit";

    strncpy(arr1, arr2, 6);

    printf("%s", arr1);
    return 0;
}
//内存中arr1里面是bit\0\0\0gh\0
//结果:bit

使用:
1.拷贝num个字符从源字符串到目标空间。
2.如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加\0,直到num个。
my_strncpy实现方法

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<assert.h>
char* my_strncpy(char* dest, const char* source, int count)
{
    assert(dest != NULL);
    assert(source != NULL);
    char* ret = dest;
    while (count && (* dest++ = *source++))
    {
        count--;
    }
    if (count)
    {
        while (--count)
        {
            *dest++ = '\0';
        }
    }
    return ret;
}

int main()
{
    char arr1[10] = "abcdefgh";
    char arr2[] = "bit";

    my_strncpy(arr1, arr2, 6);

    printf("%s", arr1);
    return 0;
}

strncat指定个数的字符串追加

库函数内的声明
char * strncat ( char * destination, const char * source, size_t num );


简单例子

#include <stdio.h>
#include <string.h>
int main ()
{
char arr1[20]="hello\0xxxxxxx";
char arr2[]="world";
strncat(arr1,arr2,8);
printf("%s",arr1);
return 0;
}
//内存:helloworld\0xx
//结果:helloworld

使用:
1.自动追加\0
2.如果num>源字符串,则追加完后就补\0,之后不变
my_strncat实现方法

#include <stdio.h>
#include<assert.h>
char* my_strncat(char* dest, const char* source,int num)
{
    assert(dest != NULL);
    assert(source != NULL);
    char* ret = dest;

    while (*dest != '\0')
    {
        dest++;
    }
    while (num && (*dest++ = *source++))
    {
        num--;
    }
    return ret;
}

int main()
{
    char arr1[20] = "hello\0xxxxxxx";
    char arr2[] = "world";
    my_strncat(arr1, arr2, 8);
    printf("%s", arr1);
    return 0;
}

strncmp指定长度的字符串比较

库函数声明
int strncmp ( const char * str1, const char * str2, size_t num );
比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完。

简单例子

#include <stdio.h>
#include <string.h>
int main()
{
    const char* p1="abcdef";
    const char* p2="abcqwer";
    //int ret=strcmp(p1,p2);//结果小于0
    int ret=strncmp(p1,p2,3);
    printf("%d",ret);
}
//结果=0

字符串查找


strstr字符串查找

库函数声明
char * strstr ( const char *p1, const char *p2 );
从p1中查找有没有出现p2,出现返回p1中出现相同的第一个p2的地址,没出现返回空指针

简单例子

int main()
{
    char* p1="abcdefabcdef";
    char* p2="def";
    char* ret=strstr(p1,p2);
    if(ret==NULL)
    {
        printf("字串不存在\n");
    }
    else
    {
        printf(ret);
    }
}
//结果:defabcdef



文档里
NULL-空指针
Null/NUL-'\0'


my_strncat实现方法

#include<stdio.h>
#include<assert.h>
char* my_strstr(const char* p1, const char* p2)
{
    assert(p1 != NULL);
    assert(p2 != NULL);
    //直接用p1,p2找不到字符串的起始地址
    char* s1 = p1;//例子abbbcdef
    char* s2 = p2;//例子bbc
    char* cur = (char*)p1;//记录有可能匹配成功的起始地址

    if (*p2 == '\0')
    {
        return (char*)p1;
    }
    while (*cur)
    {
        s1 = cur;
        s2 = (char*)p2;

        while ((*s1 != '\0') && (*s2 != '\0') && (*s1 == *s2))
        {
            s1++;
            s2++;
        }
        if (*s2 == '\0')
        {
            return cur;//找到子串
        }
        if (*s1 == '\0')
        {
            return NULL;//找不到,提前终止
        }
        cur++;
    }
    return NULL;//找不到
}

int main()
{
    char* p1 = "abbbcdef";
    char* p2 = "bbc";
    char* ret = my_strstr(p1, p2);
    if (ret == NULL)
    {
        printf("字串不存在\n");
    }
    else
    {
        printf("字串存在\n");
        printf(ret);
    }
}

my_strncat实现方法改进:KMP算法

strtok字符串删除分隔符函数

库函数内的声明
char * strtok ( char * str, const char * sep );
str指针指向一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。
sep指针指向一个字符串,字符串是分隔符的字符集合


应用场景:
192.168.1.107
192 168 1 107-strtok处理后
sjxz@qq.com
sixj qq com-strtok处理后简单例子介绍:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int main()
{
    //要切的字符串
    char arr[]="sjxz@qq.com";
    //分隔符的字符集合
    char* p="@.";
    
    //strtok函数会改变被操作的字符串
    //使用strtok函数前需要临时拷贝,例如buf
    char buf[1024]={0};
    //临时拷贝
    strcpy(buf,arr);
    
    //切割buf中字符串
    char* ret = strtok(arr,p);
    //strtok函数找到arr中的下一个标记,并将@改为 \0 ,返回一个指向s的指针
    //strtok函数的第一个参数不为 NULL ,函数将找到arr中第一个标记,strtok函数将保存arr在字符串中的位置
    printf("%s\n",ret);
    
    //strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记
    ret=strtok(NULL,p);
    printf("%s\n",ret);
    
    //strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记
    ret=strtok(NULL,p);
    printf("%s\n",ret);
    
    //如果字符串中不存在更多的标记,则返回 NULL 指针。
    ret=strtok(NULL,p);
    printf("%s\n",ret);
    
    return 0;
}

/*结果:
sjxz
qq
com
(null)*/


成熟例子介绍:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int main()
{
    char arr[] = "sjxz@qq.com";
    char* p = "@.";

    char buf[1024] = { 0 };
    strcpy(buf, arr);

    char* ret = NULL;
    //利用for循环语法实现
    for (ret = strtok(arr, p); ret != NULL; ret = strtok(NULL, p))
    {
        printf("%s\n", ret);
    }
    return 0;
}

错误信息报告

strerror翻译错误码的函数

库函数内的声明
char * strerror ( int errnum );


简单例子

#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
    //错误码    错误信息
    //strerror(错误码)
    //0 -        No error
    //1 -        Operation not permitted
    //2 -         No such file or directory
    //………………
    //errno 是一个全局的错误码的变量
    //当C语言的库函数在执行过程中,发生了错误,就会把对应的错误码,赋值到errno中
    
    //char* str=strerror(errno);
    //printf("%s\n",str);
    
    //打开文件
    FILE* pf=fopen("test.txt","r");
    
    if(pf==NULL)
    {
        //如果项目文件夹里面没有test.txt文件
        //结果:No such file or directory-没有这个文件或文件夹
        printf("%s\n",strerror(errno);)
    }
    else
    {
        //如果项目文件夹里面有test.txt文件
        //结果:open file success-打开文件成功
        printf("open file success\n");
    }
    return 0;
}

字符分类函数

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

简单例子

#include <stdio.h>
#include <ctype.h>//islower头文件
int main()
{
    char ch='w';
    //如果传进去的是小写字母a~z,就返回真
    //如果传进去的不是小写字母a~z,就返回0
    int ret = islower(ch);
    printf("%d\n",ret);
    return 0;
}

字符转化函数:

int tolower ( int c );
大写转小写,小写不动


int toupper ( int c );
小写转大写,大写不动

简单例子

#include <stdlib.h>//tolower,toupper头文件
#include <stdio.h>
#include <ctype.h>//islower头文件
int main()
{
    char arr[] = "I Am A Student";
    //isupper 大写字母A~Z
    int i = 0;
    while (arr[i])
    {
        if (isupper(arr[i]))
        {
            arr[i]=tolower(arr[i]);
        }
        i++;
    }
    printf(arr);
    return 0;
}

原型:extern int isupper(char c);

头文件:ctype.h

功能:判断字符c是否为大写英文字母

说明:当参数c为大写英文字母(A-Z)时,返回非零值,否则返回零。

附加说明: 此为宏定义,非真正函数。

内存操作函数

memcpy不重叠内存拷贝

库函数声明
void * memcpy ( void * destination, const void * source, size_t num );
                    拷贝到目的地        拷贝的来源        来源的字节数简单例子

#include <stdio.h>
#include <string.h>//memcpy头文件
struct S
{
    char name[20];
    int age;
};

int main()
{
    int arr1[] = { 1,2,3,4,5 };
    int arr2[5] = { 0 };
    memcpy(arr2, arr1, sizeof(arr1));

    struct S arr3[] = {{"张三",18} ,{"李四",20}};
    struct S arr4[3] = { 0 };
    memcpy(arr4, arr3, sizeof(arr3));

    return 0;
}

使用:
1.函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
2.这个函数在遇到 '\0' 的时候并不会停下来。
3.如果source和destination有任何的重叠,复制的结果都是未定义的。my_memcpy自己实现

#include <stdio.h>
#include <string.h>
#include <assert.h>
struct S
{
    char name[20];
    int age;
};

//C语言标准:
//memcpy 只要处理 不重叠的内存拷贝就可以
//memmove 处理重叠内存的拷贝
void* my_memcpy(void* dest, const void* src, size_t num)
{
    void* ret = dest;
    assert(dest != NULL);
    assert(src!=NULL);
    
    //一个一个拷贝:从前向后拷贝
    while(num--)
    {
    *(char*)dest = *(char*)src;
    ++(char*)dest;
    ++(char*)src;
    }
    return ret;
}

int main()
{
    int arr1[] = { 1,2,3,4,5 };
    int arr2[5] = { 0 };
    my_memcpy(arr2, arr1, sizeof(arr1));

    struct S arr3[] = {{"张三",18} ,{"李四",20}};
    struct S arr4[3] = { 0 };
    my_memcpy(arr4, arr3, sizeof(arr3));

    return 0;
}


memmove重叠内存拷贝

库函数声明
void * memmove ( void * destination, const void * source, size_t num );
从source的位置开始向后复制num个字节的数据到destination的内存位置

使用
和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
如果源空间和目标空间出现重叠,就得使用memmove函数处理。

my_memmove自我实现

思路解析:
1 2 3 4 5 6 7 8 9 10
    3 4 5 6 7(来源:src指向3)
1 2 3 4 5(目的地:dest指向1)
3 4 5 6 7 6 7 8 9 10(预期结果)
从前向后拷贝
3->1
4->2
...
7->5
一个一个拷贝:从前向后拷贝

1 2 3 4 5 6 7 8 9 10
    3 4 5 6 7(来源:src指向3)
            6 7 8 9 10(目的地:dest指向6)
1 2 3 4 5 3 4 5 6 7(预期结果)
从后向前拷贝
7->10
6->9
...
3->6
一个一个拷贝:从后向前拷贝

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

void* my_memmove(void* dest, const void* src, size_t count)
{
    void* ret = dest;
    assert(dest != NULL);
    assert(src != NULL);

    if (dest < src)
    {
        //从前向后拷贝
        while (count--)
        {
            *(char*)dest = *(char*)src;
            ++(char*)dest;
            ++(char*)src;
        }
    }
    else
    {
        //后->前
        while (count--)
        {
            *((char*)dest + count) = *((char*)src+count);
        }
    }

    return ret;
}

int main()
{
    int arr[] = { 1, 2 ,3, 4 ,5 ,6, 7, 8, 9, 10 };
    int i = 0;
    //前->后
    my_memmove(arr, arr + 2, 5 * sizeof(int));
    for (i = 0; i < 10; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
    /*
    //后->前
    my_memmove(arr+5, arr+2, 5*sizeof(int));
    for (i = 0; i < 10; i++)
    {
        printf("%d ",arr[i]);
    }
    */
    return 0;
}


memcmp内存比较函数

库函数声明
int memcmp ( const void * ptr1,
const void * ptr2,
size_t num );
比较从ptr1和ptr2指针开始的num个字节
简单例子

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

int main ()
{
char buffer1[] = “DWgaOtP12df0”;
char buffer2[] = “DWGAOTP12DF0”;

int n;

n=memcmp ( buffer1, buffer2, sizeof(buffer1) );

if (n>0) printf ("’%s’ is greater than ‘%s’.\n",buffer1,buffer2);
else if (n<0) printf ("’%s’ is less than ‘%s’.\n",buffer1,buffer2);
else printf ("’%s’ is the same as ‘%s’.\n",buffer1,buffer2);

return 0;
}
//结果:’DWgaOtP12df0’ is greater than ‘DWGAOTP12DF0’.


memset内存设置函数

库函数声明
void* memset(void* dest,int c,size_t count);
            目的地地址,要改的ASCII码,改动的字节数
简单例子
 

#include <stdio.h>
#include <string.h>
int main ()
{
    char arr[10]="";
    memset(arr,'#',10);
    return 0;
}

动态内存管理

本章重点

为什么存在动态内存分配
动态内存函数的介绍
malloc
free
calloc
realloc
常见的动态内存错误
几个经典的笔试题
柔性数组


为什么存在动态内存分配

内存
{
    栈区{局部变量,函数的形式参数}
        {
        例如:
        int a = 20;//在栈空间上开辟四个字节
        char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
        }
    堆区{动态内存分配}
    静态区{全局变量,静态变量}
        {
        例如:
        static int a=4;
        }
}
栈区,静态区
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度

相关知识点:变长数组
C语言是可以创建变长数组的-C99标准
但很多编译器不支持C99标准
gcc编译器支持
gcc test.c -std=c99
变长数组例子:

int n=0;
scanf("%d",&n);
int arr[n]={0};

有时候我们需要的空间大小在程序运行的时候才能知道,就需要动态存开辟

动态内存函数的介绍

malloc动态内存开辟函数

库函数声明
void* malloc (size_t size);

简单例子
 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
    //向内存申请10个整形的空间
    //p中存放开辟空间的地址
    int* p=(int*)malloc(10*sizeof(int));
    //开辟失败,内存不够,返回空指针NULL
    if(p==NULL)
    {
        //打印错误原因的一个方式
        printf("%s\n",strerror(errno));
        //Not enough space
    }
    else
    {
        //正常使用空间
        int i=0;
        for(i=0;i<10;i++)
        {
            *(p+i)=i;
        }
        for(i=0;i<10;i++)
        {
            printf("%d ",*(p+i));
        }        
    }
    return 0;//程序结束,被动归还空间
}

相关说明:
这个函数向内存申请一块 连续可用 的空间,并返回指向这块空间的指针。
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
malloc和free都声明在 stdlib.h 头文件

free动态内存的释放回收函数

库函数声明
void free (void* ptr);


简单例子:
 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
    int* p=(int*)malloc(10*sizeof(int));
    if(p==NULL)
    {
        printf("%s\n",strerror(errno));
    }
    else
    {
        //正常使用空间    
        ;
    }
    //当动态申请的空间不再使用的时候
    //就应该还给操作系统
    free(p);//主动归还空间,但p还是可以找到这块空间,危险
    p = NULL;
    return 0;
}


使用:
free对象如果不是 动态开辟 的,那free函数的行为是未定义的
free NULL指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h 头文件中

calloc动态内存开辟数组函数

开辟一个num个元素的数组,元素初始化为0

库函数声明

void* calloc (size_t num, size_t size);
            //元素的个数  元素的字节大小


简单例子:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
    int* p=(int*)calloc(10,sizeof(int));    
    if(p==NULL)
    {
        printf("%s\n",strerror(errno));
    }
    else
    {
        //正常使用空间
        for(i=0;i<10;i++)
        {
            printf("%d ",*(p+i));
        }        
    }
    free(p);
    p=NULL;
    return 0;//程序结束,被动归还空间
}


使用:
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。

realloc调整开辟动态内存大小函数

库函数声明
void* realloc (void* ptr, size_t size);
//ptr指针指向要调整的内存地址
//size为调整之后新大小
//返回值为调整之后的内存起始位置,失败,返回NULL

简单例子

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
    int* p = (int*)malloc(20);
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
    }
    else
    {
        int i = 0;
        for (i = 0; i < 5; i++)
        {
            *(p + i) = i;
        }
        for (i = 0; i < 5; i++)
        {
            printf("%d ", *(p + i));
        }
        printf("\n");
    }
    //使用malloc开辟的20个字节的空间
    //假设20个字节不够,想要40个字节,用realloc
    int* ptr = realloc(p, 40);
        if (ptr != NULL)
        {
            p = ptr;
            int i = 0;
            //之后空间初始化
            for (i = 5; i < 10; i++)
            {
                *(p + i) = i;
            }
            for (i = 0; i < 10; i++)
            {
                printf("%d ", *(p + i));
            }
        }
    free(p);
    p = NULL;
    return 0;
}
/*结果:
0 1 2 3 4
0 1 2 3 4 5 6 7 8 9*/

realloc也可以直接开辟内存

int* p=realloc(NULL,40);

等于int* p=(int*)malloc(40);


realloc返回值注意事项:

情况1:如果p指向的空间之后有足够的内存空间可以追加,
    则直接追加,后返回p
        
情况2:如果p指向的空间之后没有足够的内存空间可以追加,
    则realloc函数会重新找一块新的满足需求的内存区域
    并且把原来内存中的数据拷贝回来,释放旧的内存空间
    最后返回新开辟内存空间的地址
 
情况3:realloc追加失败,返回NULL

 
常见的动态内存错误

对空指针NULL指针的解引用操作

void test()
{
int *p = (int *)malloc(INT_MAX);//内存不够,返回空指针NULL
*p = 20;//err:不能对空指针解引用
free(p);
}

解决方法:对p进行NULL的相关判断

对动态开辟空间的越界访问

void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));//下标0-9
if(p == NULL)
{
    return 0;
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//i=10则越界访问
}
free(p);
}

解决方法:算好边界

对非动态开辟内存使用free释放

void test()
{
int a = 10;
int *p = &a;
free(p);//err:free非动态开辟内存
}

使用free释放一块动态开辟内存的一部分

void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}

解决方法:建议不改变p的位置,用p+i的方法来避免p被改变

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

void test()
{
int *p = (int *)malloc(100);
free(p);
//…………
free(p);//重复释放
}


解决方法:1.谁开辟谁回收  2.free完后将p设置为NULL,则第二次free等于没做事


动态开辟内存忘记释放(内存泄漏)
忘记释放不再使用的动态开辟的空间会造成内存一直被消耗,直到死机

while(1)
{
    malloc(1);
}


解决方法:动态开辟的空间一定要释放,并且正确释放

几个经典的笔试题

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

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


解析:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char* p)//str以值的形式传递给p,p只是str的临时拷贝
{
    p = (char*)malloc(100);//p拿到开辟的地址
}//p是GetMemory函数的形参,动态开辟内存尚未释放
//且离开函数后p被销毁,无法找到开辟的地址,所以存在内存泄漏
void Test(void)
{
    char* str = NULL;
    GetMemory(str);//str仍为NULL,并不指向有效空间
    strcpy(str, "hello world");//*NULL导致内存非法访问,引起程序崩溃
    printf(str);//写法正确
}

int main()
{
    Test();
    return 0;
}

结果:运行代码程序崩溃,存在内存泄漏问题
改正1:

void GetMemory(char* *p)
{
*p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);

free(str);
str=NULL;
}
//结果:hello world

改正2:
 

char* GetMemory(char *p)
{
    p = (char *)malloc(100);
    return p;
}
void Test(void)
{
    char *str = NULL;
    str = GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
    
    free(str);
    str=NULL;
}
//结果:hello world

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

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

解析:返回栈空间地址的问题

char *GetMemory(void)
{
char p[] = "hello world";//栈区
return p;//栈空间地址无法返回
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}


结果:随机值:VS2019一次运行的结果:烫烫烫烫烫烫烫烫D齇
非法访问内存


改正:
 

char *GetMemory(void)
{
static char p[] = "hello world";//静态区
return p;//静态区空间地址可以返回
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
//PS堆空间地址也可以返回

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

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

解析:

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


结果:hello(存在内存泄漏问题:忘记释放动态开辟的内存)

改正:
 

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

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

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

解析:

void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);//free释放str指向的空间后,并不会把str置为NULL
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}


结果:world
问题:非法访问内存,free提前释放动态内存
    str任然存着动态内存的地址,再访问被释放的内存,就是非法的
改进:
 

void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
str=NULL;
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
//结果:什么都没有

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

1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些
存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有
限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由操作系统回收 。分配方式类似
于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

5.内核空间:分配给操作系统的空间

普通的局部变量-栈区
被static修饰的变量存放在静态区

柔性数组-C99

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

柔性数组的建立
 

typedef struct st_type
{
int i;
int a[0];//未知大小的-柔性数组成员-数组大小是可以调整的
}type_a;

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

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

柔性数组的优势

#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
	int n;
	int a[0];//柔性数组成员
}type_a;


int main()
{
	printf("%d\n", sizeof(type_a));//输出的是4
	int i = 0;
	//这样柔性数组成员a,相当于获得了5个整型元素的连续空间
	type_a* p = (type_a*)malloc(sizeof(type_a) + 5 * sizeof(int));
	p->n = 5;
	for (i = 0; i < 5; i++)
	{
		p->a[i] = i;
	}
	free(p);
	return 0;
}

柔性数组的特点:

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


柔性数组的好处

1.方便内存释放,用户做一次free就可以把所有的内存也给释放掉,非柔性数组要free多次
2.内存连续,内存访问效率高,连续的内存有益于提高访问速度,也有益于减少内存碎片


非柔性数组的例子-C89

上述的 type_a 结构也可以设计为:

#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
    int n;
    int* arr;
}type_a;

int main()
{
    //ps指向动态开辟的结构体空间
    type_a* ps = malloc(sizeof(type_a));
    //结构体中的arr指针指向malloc开辟的5个整形大小的空间
    ps->arr = (int*)malloc(5 * sizeof(int));

    //初始化
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        ps->arr[i] = i;
    }
    //使用
    for (i = 0; i < 5; i++)
    {
        printf("%d ", ps->arr[i]);
    }
    //调整大小
    int* ptr = realloc(ps->arr, 10 * sizeof(int));
    if (ptr != NULL)
    {
        ps->arr = ptr;
    }
    //初始化新增加的空间
    for (i = 5; i < 10; i++)
    {
        ps->arr[i] = i;
    }
    //使用新增加的空间
    for (i = 5; i < 10; i++)
    {
        printf("%d ", ps->arr[i]);
    }
    //释放空间,先释放后开辟的malloc
    //如果先free先开辟的空间,则第二块空间找不到了
    free(ps->arr);
    ps->arr = NULL;
    free(ps);
    ps = NULL;
}
/*结果
0 1 2 3 4 5 6 7 8 9
*/

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

本章重点

结构体
    结构体类型的声明和变量的定义
    结构的自引用
    结构体初始化
    结构体内存对齐
    结构体传参
    结构体实现位段(位段的填充&可移植性)
枚举
    枚举类型的定义
    枚举的优点
    枚举的使用
联合
    联合类型的定义
    联合的特点
    联合大小的计算


结构体

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

结构体类型的声明和变量的定义

struct tag
{
member-list;//成员列表
}variable-list;//变量列表

简单例子:

创建学生s1,s2,s3,s4,s5

例如描述一个学生:
      声明一个结构体类型
      声明一个学生类型,是想通过学生类型来创建学生变量(对象)
      描述学生:名字+年龄+性别+学号

struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}s4,s5,s6;//分号不能丢
//s4,s5,s6全局变量

    struct Stu s3;//s3全局变量

int main()
{
    //创建的结构体变量
    //s1,s2局部变量
    struct Stu s1;
    struct Stu s2;    
}

特殊的声明

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

使用场景:通常在只需要一次的结构体使用
 

简单例子:

//匿名结构体类型:没有名字,省略掉了结构体标签(tag)
struct
{
int a;
char b;
float c;
}x;//这能在这里创建变量

struct
{
int a;
char b;
float c;
}* px;//匿名结构体指针

int main()
{
px=&x;//警告: 编译器会把上面的两个声明当成完全不同的两个类型,是非法的!!!
}

结构的自引用

数据结构:数据在内存中存储的结构
链表:1 -> 2 -> 3 -> 4 -> 5

|-------------------------------------------------------|
|数值域  指针域(存放下一个数据的地址)    |
|-------------------------------------------------------|

//err代码1
结构中包含一个类型为该结构本身的成员是 错误 的

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

否则sizeof(struct Node)=无穷大


正确的自引用方式:

//代码2
struct Node
{
    int data;
    struct Node* next;
};


重命名:给struct Node起一个小名叫Node
 

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


结构体变量初始化

//初始化:定义变量的同时赋初值。
#include <stdio.h>
struct Point
{
    int x;
    int y;
};

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

struct Node n2 = { 10, {4,5}, NULL };//初始化

int main()
{
    struct Node n3 = { 10, {4,5}, NULL };//初始化
    printf("%d %d %d %s", n1.data, n1.p.x, n1.p.y, n1.next);
    return 0;
}
//结果:10 4 5 (null)

结构体内存对齐

问题:请计算结构体的大小,4个printf的结果是多少?

struct S1
{
char c1;
int i;
char c2;
};

struct S2
{
char c1;
char c2;
int i;
};

struct S3
{
double d;
char c;
int i;
};

struct S4//结构体嵌套问题
{
char c1;
struct S3 s3;
double d;
};

int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
printf("%d\n", sizeof(struct S3));
printf("%d\n", sizeof(struct S4));
return 0;
}

答案:
12
8
16
32


解析:求结构体大小标准步骤

struct S1//12
{
char c1;
int i;
char c2;
};


1.求出成员大小

char c1;            //1字节
int i;              //4字节
char c2;            //1字节

2.求出各自对齐数
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
gcc没有默认对齐数
以VS为例:
char c1;          //1字节/8字节-->1字节
int i;                 //4字节/8字节-->4字节
char c2;          //1字节/8字节-->1字节

以gcc为例:

char c1;            //1字节
int i;                  //4字节
char c2;            //1字节

3.求出结构体总大小
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
结构体总大小:4的整数倍

4.画出内存结构
第一个成员在与结构体变量偏移量为0的地址处
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
char c1;       //1的整数倍
int i;             //4的整数倍
char c2;       //1的整数倍

5.算出结构体总大小

结构体总大小:4的整数倍
存完char c2后偏移量为9,不是4的整数倍,所以为12

|            |
|......      |
|------------|<----假设从这里开始放(偏移量为0)
| char c1    |
|------------|偏移量为1
|  浪费      |
|------------|偏移量为2
|  浪费      |
|------------|偏移量为3
|  浪费      |
|------------|偏移量为4<----4的整数倍
|  int i     |
|------------|偏移量为5
|  int i     |
|------------|偏移量为6
|  int i     |
|------------|偏移量为7
|  int i     |
|------------|偏移量为8<----1的整数倍
| char c2    |
|------------|偏移量为9
|  浪费      |
|------------|偏移量为10
|  浪费      |
|------------|偏移量为11
|  浪费      |
|------------|偏移量为12
|......      |
|            |


S2求法同上:

struct S2//8
{
char c1;
char c2;
int i;
};

struct S3//16
{
double d;
char c;
int i;
};


1.求出成员大小
double d;    //8
char c;        //1
int i;        //4
2.求出对齐数
double d;    //4
char c;        //1
int i;        //4
3.求出结构体总大小
    4的整数倍
4.画出内存结构
double d;    //4
char c;        //1
int i;        //4

|         |
|......   |
|---------|<----假设从这里开始放(偏移量为0)
|double d |
|---------|偏移量为1
|double d |
|---------|偏移量为2
|double d |
|---------|偏移量为3
|double d |
|---------|偏移量为4
|double d |
|---------|偏移量为5
|double d |
|---------|偏移量为6
|double d |
|---------|偏移量为7
|double d |
|---------|偏移量为8<----1的整数倍
|char c   |
|---------|偏移量为9
|  浪费   |
|---------|偏移量为10
|  浪费   |
|---------|偏移量为11
|  浪费   |
|---------|偏移量为12<----4的整数倍
|int i    |
|---------|偏移量为13
|int i    |
|---------|偏移量为14
|int i    |
|---------|偏移量为15
|int i    |
|---------|偏移量为16


 

struct S4//32
{
char c1;
struct S3 s3;
double d;
};

1.求出成员大小
char c1;//1
struct S3 s3;//16
double d;//8
2.求出对齐数
char c1;//1
struct S3 s3;//8
double d;//4
3.求出结构体总大小
    8的整数倍
4.画出内存结构
char c1;//1
struct S3 s3;//8
double d;//4

|            |
|......      |
|------------|<----假设从这里开始放(偏移量为0)
|char c1     |
|------------|偏移量为1
|  浪费      |
|------------|偏移量为2
|  浪费      |
|------------|偏移量为3
|  浪费      |
|------------|偏移量为4<----4的整数倍
|  浪费      |
|------------|偏移量为5
|  浪费      |
|------------|偏移量为6
|  浪费      |
|------------|偏移量为7
|  浪费      |
|------------|偏移量为8<----8的整数倍
|struct S3 s3|
.............
|struct S3 s3|
|------------|偏移量为24<----4的整数倍
| double d   |
..............
| double d   |
|--over------|偏移量为32<----8的整数倍

int main()
{
printf("%d\n", sizeof(struct S1));//12
printf("%d\n", sizeof(struct S2));//8
printf("%d\n", sizeof(struct S3));//16
printf("%d\n", sizeof(struct S4));//32
return 0;
}

 
为什么存在内存对齐?

1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;
某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地对齐
访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问。
例如:win32平台,32根地址线,32根数据线-->一次读4字节

01   01 00 00 |00
char int      |
一次读取       |二次读取
01   00 00 00 01 00 00 00
char --浪费-- int      
一次读取

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

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

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

修改默认对齐数

#pragma 这个预处理指令可以改变我们的默认对齐数

例子:
 

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


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

offsetof
偏移量求取宏

offsetof简单例子
 

#include <stdio.h>
#include<stddef.h>//offsetof头文件
struct S
{
    char c;
    int i;
    double d;
};
int main()
{
    printf("%d\n",offsetof(struct S,c));//0
    printf("%d\n",offsetof(struct S,i));//4
    printf("%d\n",offsetof(struct S,d));//8
    return 0;
}

百度笔试题:
写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
考察: offsetof 宏的实现

结构体传参

#include <stdio.h>
struct S
{
    char c;
    int i;
    double d;
};

//初始化传地址
void Init(struct S* ps)
{
    ps->c = 'w';
    ps->i = 100;
    ps->d = 3.14;
}

//结构体传值
void Print1(struct S tmp)
{
    printf("%d %c %lf\n",tmp.c,tmp.i,tmp.d);
}

//传地址
void Print2(struct S* ps)
{
    printf("%d %c %lf\n", ps->c ,ps->i ,ps->d);
}

int main()
{
    struct S s;
    Init(&s);
    //不推荐:传结构体
    //若结构体过大,临时拷贝的系统开销大,会导致性能的下降
    Print1(s);

    //推荐:传地址,4-8字节
    Print2(&s);
    return 0;
}

位段

节省空间的结构体

什么是位段?

结构体实现 位段

位段的声明和结构是类似的,有两个不同
1.位段的成员必须是 int、unsigned int 或signed int   (其实char也可以)   (通常位段的类型是相同或相似的成员)
2.位段的成员名后边有一个冒号和一个数字。
 
位段简单例子

struct A
{
//类型 变量名:所占的bit位
//所占的bit位<=类型bit位(win32:int-32bit)
int a:2;
int b:5;
int c:10;
int d:30;
};


位段的内存分配

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


例子1:

struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
};

共计47bit
1字节==8bit


由于位段类型为int,所以一次开辟4字节==32bit


开辟32bit
{
    a-2bit
    b-5bit
    c-10bit
    剩余32-17=15bit    <   d所需要的30bit
    在VS编译器下浪费掉15bit
    再开辟32bit
}
再开辟32bit
{
    d-30bit
    剩余2bit被浪费
}

printf("%d\n", sizeof(struct A));//共计8字节


例子2
 

struct S
{
    char a:3;
    char b:4;
    char c:5;
    char d:4;
};

int main()
{
    struct S s = {0};
    s.a = 10;
    s.b = 12;
    s.c = 3;
    s.d = 4;
    return 0;
}

调试->窗口->内存
 


位段的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

总结:
跟结构相比,位段可以达到同样的效果,

但是可以很好的节省空间,但是有跨平台的问题存在。

位段的应用:网络数据的传输

枚举    即为一 一列举

枚举类型的定义

//颜色
enum Color//枚举类型
{
    //枚举常量
    RED,
    GREEN,
    BLUE
};
int main()
{
    //枚举类型 变量名 = 赋值;
    enum Color c =BLUE;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异
    
    printf("%d %d %d\n",RED,GREEN,BLUE);
    //结果0 1 3

    return 0;
}

枚举常量取值都是有值的,默认从0开始,一次递增1

当然在定义的时候也可以赋初值,下一个默认为前一个加1

例如

enum Color//颜色
{
    RED = 1,
    GREEN,//(默认为2)
    BLUE = 4
};

枚举的大小为4字节,枚举类型无法自己指定

枚举的优点

我们可以使用 #define 定义常量,为什么非要使用枚举?
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名冲突(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量

#define 定义常量,例如:
 

#define    RED 0
#define    GREEN 1
#define    BLUE 2


 
联合(共用体)

联合类型的定义

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


简单例子

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

//联合变量的定义
union Un un;

//计算连个变量的大小
printf("%d\n", sizeof(un));//4字节

联合的初始化和内存分布

#include <stdio.h>
union Un
{
    int i;
    char arr[3];
};
int main()
{
    //联合的初始化
    union Un un;
    un.i = 0x11223344;
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("%x ",un.arr[i]);
    }
}
/*
VS(小端存储)下结果为:
44 33 22
*/


联合的特点

联合的成员是共用同一块内存空间的,&u == &(un.i) == &(un.c)
一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
 

百度面试题:
判断当前计算机的大小端存储


方法一:强制类型转化

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


方法二:联合

#include<stdio.h>
int check_sys()
{
    union Un
    {
        char c;
        int i;
    }u;
    u.i=1;
    return u.c;
}

int main()
{
    if(check_sys())
    {
        printf("小端\n");
    }
    else
    {
        printf("大端\n");
    }
    return 0;
}

联合大小的计算

1)大小足够容纳最宽的成员;

2)大小能被其包含的所有基本数据类型的大小所整除。
简单例子:

#include <stdio.h>
union Un
{
    int i;
    //自身大小4字节
    //默认对齐数8
    //min{自身大小,默认对齐数}==4
    //对齐数==4
    char arr[5];
    //自身大小5字节
    //元素类型大小1字节
    //默认对齐数8
    //min{元素类型大小,默认对齐数}==1
    //对齐数==1

    //最大成员大小(5)不是最大对齐数(4)的整数倍
    //对齐到最大对齐数的整数倍(8)
};
int main()
{
    printf("%d\n", sizeof(union Un));
}
/*
结果:8
*/

C语言文件操作

本章重点

什么是文件
文件名
文件类型
文件缓冲区
文件指针
文件的打开和关闭
文件的顺序读写
文件的随机读写
文件结束的判定

什么是文件

磁盘上的文件是文件
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件
程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj,Linux环境后缀为.o),可执行程序(windows环境后缀为.exe)
数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件


本章讨论的是数据文件
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。

文件名

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

文件路径+文件名主干+文件后缀

c:\code\  +      test      +   .txt
为了方便起见,文件标识常被称为文件名。

文件类型

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

文本文件:全是ASCII字符存储

二进制文件:二进制存储,16进制显示
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。

一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。

二进制文件写入10000,文件里是10 27 00 00,小端存储:十六进制的7210==十进制的10000

文本文件写入10000,文件里是10000,是字符'1'  '0'  '0'  '0'  '0'
 
测试代码:

#include <stdio.h>
int main()
{
    int a = 10000;
    //以二进制的写入形式打开test.txt文件,通过指针可以找到这个文件
    FILE* pf = fopen("test.txt", "wb");
    //将a写入,a占4个字节,写入1份,写到test.txt中
    fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
    //结束文件
    fclose(pf);
    pf = NULL;
    return 0;
}

 
文件缓冲区

ANSIC 标准采用“缓冲文件系统”处理的数据文件的,

所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。

内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。

如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),

然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。


一秒结束后并没有输出,因为输出缓冲区没有满

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <windows.h>
int main()
{
    while (1)
    {
        Sleep(1000);//休息一秒
        printf("hehe");
    }
    return 0;
}


文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区

用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。

这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
例如,VS2008编译环境提供的 stdio.h 头文件中有以下的文件类型申明:

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

不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。

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

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

定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文
件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
比如:

文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。

FILE * fopen ( const char * filename, const char * mode );
int fclose ( FILE * stream );

 文件打开路径

绝对路径:为防止转移字符干扰,要加一个转义字符将其转义

FILE* fopen("C:\\Users\\93983\\source\\repos\\test_1_19.test.txt","r");

 相对路径:

..   表示上一级路径

.    表示当前路径

FILE* fopen("test.txt","r");//在源代码所在的目录

打开方式如下:

文件使用方式

含义

如果指定文件不存在

“r”(只读)

为了输入数据,打开一个已经存在的文本文件

出错

“w”(只写)

为了输出数据,打开一个文本文件

建立一个新的文件

“a”(追加)

向文本文件尾添加数据

出错

“rb”(只读)

为了输入数据,打开一个二进制文件

出错

“wb”(只写)

为了输出数据,打开一个二进制文件

建立一个新的文件

“ab”(追加)

向一个二进制文件尾添加数据

出错

“r+”(读写)

为了读和写,打开一个文本文件

出错

“w+”(读写)

为了读和写,建议一个新的文件

建立一个新的文件

“a+”(读写)

打开一个文件,在文件尾进行读写

建立一个新的文件

“rb+”(读写)

为了读和写打开一个二进制文件

出错

“wb+”(读写)

为了读和写,新建一个新的二进制文件

建立一个新的文件

“ab+”(读写)

打开一个二进制文件,在文件尾进行读和写

建立一个新的文件

 
实例代码:

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

文件的顺序读写

功能

函数名

适用于

字符输入函数

fgetc

所有输入流

字符输出函数

fputc

所有输出流

文本行输入函数

fgets

所有输入流

文本行输出函数

fputs

所有输出流

格式化输入函数

fscanf

所有输入流

格式化输出函数

fprintf

所有输出流

二进制输入

fread

文件

二进制输出

fwrite

文件

 fgetc得到字符c函数(get是得到的意思)

函数原型

从文件指针指向的文件中获取一个字符,返回读取为int的字符或返回EOF以指示错误或文件结束

int fgetc( FILE *stream );

代码举例

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
	FILE* pFile = fopen("test.txt", "r");
	if (pFile == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}

	int ch=fgetc(pFile);
	printf("%c",ch);
	ch = fgetc(pFile);
	printf("%c", ch);

	fclose(pFile);
	pFile = NULL;
	return 0;
}
/*
test.txt内容hello
程序结果:he
*/

fputc输出字符c函数(put是给出的意思)

函数原型

向文件中输出一个字符,成功返回输出字符的ASCII码值,失败返回EOF

int fputc( int c, FILE *stream );

代码举例

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
	FILE* pFile = fopen("test.txt", "w");
	if (pFile == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}
	fputc('A',pFile);
	

	fclose(pFile);
	pFile = NULL;
	return 0;
}
//跑过程序后test.txt中有A

fgets得到一行函数

从文件中得到一行字符串

函数原型

char *fgets( char *string, int n, FILE *stream );
  • 返回值:NULL :EOF或成功结束 可以用 feof或 ferror判断错误位置
  • string:存储文件中字符串的自己创建的字符串
  • n:读取的最大数值
  • stream:指向文件的指针

代码举例

test.txt内容

hello
world

test.c代码

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
	char arr[1024] = { 0 };
	FILE* pFile = fopen("test.txt", "r");
	if (pFile == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}
	fgets(arr,1024,pFile);
	printf("%s",arr);

	fclose(pFile);
	pFile = NULL;
	return 0;
}
/*结果
hello
*/

fputs字符串写入函数

函数原型

int fputs( const char *string, FILE *stream );
  • 返回值:如果成功,返回一个非负值。出错时,返回EOF
  • string:要输出字符串
  • stream:指向文件结构的指针

代码:

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
	FILE* pFile = fopen("test.txt", "w");
	if (pFile == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}
	fputs("hello world",pFile);

	fclose(pFile);
	pFile = NULL;
	return 0;
}
/*结果
test.txt内容
hello world
*/

fscanf从文件中读取格式化数据

格式:就是类型,例如int float char double型

函数原型

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

类比scanf:多了文件来源

int scanf( const char *format [,argument]... );

fscanf从文件读取格式化数据

scanf从标准输入设备-键盘(stdin)读取数据

PS:标准输出设备-显示屏(stdout)

返回值:返回成功转换和分配的字段数;返回值不包括已读取但未分配的字段。返回值0表示未分配任何字段。发生错误,或者在第一次转换之前到达文件流的结尾,则返回值为EOF
代码实践:

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>

struct Student
{
	char name[20];
	int age;
	float score;
};

int main()
{
	struct Student s = { 0 };

	FILE* pFile = fopen("test.txt", "r");
	if (pFile == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}
	fscanf(pFile,"%s %d %f",&s.name,&s.age,&s.score);
	printf("%s %d %f", s.name, s.age, s.score);

	fclose(pFile);
	pFile = NULL;
	return 0;
}
/*
test.txt内容
zhangsan 18 98.5
结果
zhangsan 18 98.500000
*/

fprintf向文件输出格式化数据

函数原型

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

对比printf,只多了文件输出的对象

int printf( const char *format [, argument]... );

返回值:fprintf返回写入的字节数。当发生输出错误时,返回一个负值。
 

代码实践:

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>

struct Student
{
	char name[20];
	int age;
	float score;
};

int main()
{
	struct Student s = { "zhangsan" ,18, 98.5f };

	FILE* pFile = fopen("test.txt", "w");
	if (pFile == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}
	fprintf(pFile,"%s %d %f",s.name,s.age,s.score);

	fclose(pFile);
	pFile = NULL;
	return 0;
}
/*
结果txt中内容为
zhangsan 18 98.500000
*/

fread二进制输入

函数原型

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

返回值:fread返回实际读取的完整项目数,如果发生错误或在达到count之前遇到文件结尾,则该值可能小于count。使用feof或feror函数来区分读取错误和文件结束条件。如果size或count为0,则fread返回0且缓冲区内容不变。

buffer缓冲器:数据的存储位置
size:项目大小(字节)
count:要读取的最大项目数
stream:指向文件结构的指针

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main()
{
	char arr[1024] = { 0 };
	FILE* pFile = fopen("test.txt", "rb");
	if (pFile == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}
	fread(arr, sizeof(char), 9, pFile);
	printf("%s\n",arr);

	fclose(pFile);
	pFile = NULL;
	return 0;
}
/*
txt中内容为
abcdef123
结果为
abcdef123
*/

fwrite二进制输出

写一个数据进文件

size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
  • buffer缓冲器:指向要写入的数据的指针
  • size:项目大小(字节)
  • count:要写入的最大项目数
  • stream:指向文件结构的指针
#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main()
{
	char arr[1024] = { 0 };
	char str[] = "abcdef123";
	FILE* pFile = fopen("test.txt", "wb");
	if (pFile == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}
	fwrite(str, sizeof(char), sizeof(str), pFile);
	//fread(arr, sizeof(char), 4, pFile);
	//printf("%s\n",arr);

	fclose(pFile);
	pFile = NULL;
	return 0;
}
/*
结果txt中内容为
abcdef123
*/

区别scanf/fscanf/sscanf

scanf是从标准输入设备,也就是键盘上获取格式化内容

fscanf是从数据文件中获取格式化内容

sscanf是从字符串中获取格式化内容

#define  _CRT_SECURE_NO_WARNINGS
//函数原型
//int sscanf( const char *buffer, const char *format [, argument ] ... );
#include <stdio.h>
int main()
{
	char str[10] = { 0 };
	int a = 0;
	float b = 0.0f;
	char arr[] = "abc 123 3.14";
	sscanf(arr,"%s %d %f",&str,&a,&b);
	printf("%s\n",str);
	printf("%d\n",a);
	printf("%f\n",b);
	return 0;
}
/*
结果:
abc
123
3.140000
*/

区别printf/fprintf/sprintf

printf是向标准输出设备,也就是显示屏,输出格式化内容

fprintf是向文件中输出格式化内容

sprintf是向字符串输出格式化内容

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//函数原型
//int sprintf(char* buffer, const char* format[, argument] ...);
int main()
{
	char str[10] ="hello";
	int a = 2021;
	float b = 3.14f;
	char arr[1024] = {0};
	sprintf(arr,"%s %d %f",str,a,b);
	printf("%s\n",arr);
	return 0;
}
/*
结果:
hello 2021 3.140000
*/

文件的随机读写

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

函数原型:

int fseek ( FILE * stream, long int offset, int origin );
  • 返回值:如果成功,fseek返回0。否则,它将返回一个非零值。在无法查找的设备上,返回值未定义。
  • stream:指向文件结构的指针
  • offset:偏移量
  • origin:指针初始位置 
  • SEEK_SET   第1个字符,指向接下来要操作的首字节位置  
  • SEEK_CUR   当前字符位置
  • SEEK_END   最后一个字符的后一个位置

例子:

#define  _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

int main()
{
	FILE* pFile;
	pFile = fopen("test.txt", "wb");
	fputs("This is an apple.", pFile);
	fseek(pFile, 9, SEEK_SET);
	fputs("sam", pFile);
	fclose(pFile);
	return 0;
}
/*结果
test.txt内容This is asampple.
This is an apple.
T偏移量为0
h偏移量为1
i偏移量为2
......
n偏移量为9  <--------文件指针pFile指向此处
 偏移量为10
......

*/


ftell
返回文件指针相对于起始位置的偏移量

函数原型

long int ftell ( FILE * stream );

例子:
 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	FILE* pFile;
	long size;

	pFile = fopen("test.txt", "rb");
	if (pFile == NULL) perror("Error opening file");
	else
	{
		fseek(pFile, 0, SEEK_END);   //将指针移动到末尾
		size = ftell(pFile);
		fclose(pFile);
		printf("Size of test.txt: %ld bytes.\n", size);
	}
	return 0;
}

PS:错误报告函数

方法一:
perror("Error opening file");
结果:Error opening file: XXXXXX

方法二:
#include <string.h>
#include <errno.h>
printf("%s\n", strerror(errno));
结果:XXXXXX


rewind
让文件指针的位置回到文件的起始位置

void rewind ( FILE* stream );

例子:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	FILE* pFile=fopen("test.txt", "r");
	if (pFile == NULL) perror("Error opening file");
	fseek(pFile,-1,SEEK_END);//指针移动到o
	rewind(pFile);//指针移动到h
	int ch=fgetc(pFile);
	printf("%c",ch);
	fclose(pFile);
	pFile = NULL;
	return 0;
}
/*
txt:hello
结果:h
*/

文件结束判定

被错误使用的 feof

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
1. 文本文件读取是否结束,判断返回值是否为文件尾结束EOF (fgetc),或者读取失败结束NULL(fgets)
例如:
fgetc判断是否为EOF.
fgets判断返回值是否为NULL.
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数。
正确的使用:
文本文件的例子:

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

int main(void)
{
    int c; // 注意:int,非char,要求处理EOF
    FILE* fp = fopen("test.txt", "r");
    if (!fp) {
        perror("File opening failed");
        return EXIT_FAILURE;//stdlib.h中有#define EXIT_FAILURE 1
    }
    //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
    while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
    {
        putchar(c);
    }

    //判断是什么原因结束的
    if (ferror(fp))//如果fp中没有发生错误,ferror将返回0。否则,它将返回一个非零值
        puts("I/O error when reading");
    else if (feof(fp))
        //feof函数在第一次读取操作后返回一个非零值,
        //该操作试图读取文件的末尾。
        //如果当前位置不是文件结尾,则返回0
        puts("End of file reached successfully");

    fclose(fp);
}

二进制文件的例子:
 

#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
    double a[SIZE] = { 1.0,2.0,3.0,4.0,5.0 };
    double b = 0.0;
    size_t ret_code = 0;
    FILE* fp = fopen("test.bin", "wb"); // 必须用二进制模式
    fwrite(a, sizeof(*a), SIZE, fp); // 写 double 的数组
    fclose(fp);

    fp = fopen("test.bin", "rb");
    // 读 double 的数组
    while ((ret_code = fread(&b, sizeof(double), 1, fp)) >= 1)
    {
        printf("%lf\n", b);
    }
    if (feof(fp))
        printf("Error reading test.bin: unexpected end of file\n");
    else if (ferror(fp)) {
        perror("Error reading test.bin");
    }
    fclose(fp);
    fp = NULL;
}

程序的编译(预处理操作)+链接

程序环境和预处理

本章重点:

程序的翻译环境
程序的执行环境
详解:C语言程序的编译+链接
预定义符号介绍
预处理指令 #define
宏和函数的对比
预处理操作符#和##的介绍
命令定义
预处理指令 #include
预处理指令 #undef
条件编译

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

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

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

图解编译+链接

C代码(文本文件)                                                                                   二进制的信息(二进制文件)

test.c(源文件)---------编译---目标文件test.obj ---|--链接----->test.exe----------运行-------->

add.c(源文件)---------编译---目标文件add.obj----|

|-----------------------------------------------------------------------------------------------------------------------|

|                                    翻译环境                                     |                   执行环境                                |

|-----------------------------------------------------------------------------------------------------------------------|

         编译(编译器)                          链接(链接器)                         运行

1.预编译    2.编译     3.汇编

详解编译+链接

编译本身也分为几个阶段:Linux系统下举例

1. 预处理 文本处理:选项 gcc -E test.c -o>test.i 预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中

  • #include头文件包含(头文件代码替换#include这一条语句)
  • 注释删除(使用空格替换注释)
  • #define

2. 编译 C语言代码翻译成汇编语言:选项 gcc -S test.c 编译完成之后就停下来,结果保存在test.s

  1. 语法分析
  2. 词法分析
  3. 语义分析
  4. 符号汇总(函数,main,全局变量,函数声明)

3. 汇编 把汇编代码转换成二进制指令:gcc -c test.c 汇编完成之后就停下来,结果保存在test.o中。(Linux下的.o文件是windows下的.obj文件)

  • 形成符号表(符号 | 地址)函数声明的地址为无效的地址

链接:

  1. 合并段表(不同源文件的.obj文件合并到一起)
  2. 符号表的合并和重定位(不同源文件符号表合并,重复的重定位有效的)


翻译环境

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code) .obj(英文全称为object)
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

VIM学习资料:类似linux下的Notepad++
简明VIM练级攻略:
https://coolshell.cn/articles/5426.html
给程序员的VIM速查卡
https://coolshell.cn/articles/5479.html


运行环境

程序执行的过程:

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  2. 程序的执行便开始。接着便调用main函数。
  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
  4. 终止程序。正常终止main函数;也有可能是意外终止。

注: 介绍一本书《程序员的自我修养》

预处理详解

预定义符号

  • __FILE__      //进行编译的源文件
  • __LINE__     //文件当前的行号
  • __DATE__    //文件被编译的日期
  • __TIME__    //文件被编译的时间
  • __FUNCTION__ //语句所在的函数名字
  • __STDC__    //如果编译器遵循ANSI C标准,其值为1,否则未定义

VS2019未定义

Linux gcc 遵循ANSI C标准,其值为1

代码应用:写文件日志

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
	int i = 0;
	int arr[10] = {0};
	FILE* pf = fopen("log.txt","w");
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
		fprintf(pf,"file:%s line:%d data:%s time:%s i=%d\n",__FILE__,__LINE__,__DATE__,__TIME__,i);
	}
	fclose(pf);
	pf = NULL;
	for (i = 0; i < 10; i++)
	{
		printf("%d ",arr[i]);
	}
	return 0;
}

找到文件存放的目录

预处理指令

#define定义标识符

语法:

#define name stuff

举个栗子:

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


提问:
在define定义标识符的时候,要不要在最后加上 ; ?
比如:

#define MAX 100;
#define MAX 100
 
建议不要加上 ; ,这样容易导致问题。 比如下面的场景:

    if (condition)
        max = MAX;
    else
        max = 0;

这里会出现语法错误。

#define 定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(definemacro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
如:
 

#define _CRT_SECURE_NO_WARNINGS
#define SQUARE( x ) x*x
#include <stdio.h>
int main()
{
	int ret = SQUARE(5);
	//int ret = 5 * 5;
	printf("%d",ret);//25
	return 0;
}

这个宏接收一个参数 x . 如果在上述声明之后,你把SQUARE( 5 );置于程序中,预处理器就会用下面这个表达式替换上面的表达式:5 * 5

警告: 这个宏存在一个问题: 观察下面的代码段:

#define _CRT_SECURE_NO_WARNINGS
#define SQUARE( x ) x*x
#include <stdio.h>
int main()
{
	int ret = SQUARE(4+1);
	printf("%d",ret);
	return 0;
}

乍一看,你可能觉得这段代码将打印25这个值。 事实上,它将打印9. 为什么?
替换文本时,参数x被替换成4 + 1,所以这条语句实际上变成了: printf ("%d\n",4 + 1 * 4 + 1 );
这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。
在宏定义上加上两个括号,这个问题便轻松的解决了:
#define SQUARE(x) (x) * (x)
这样预处理之后就产生了预期的效果:25

#define _CRT_SECURE_NO_WARNINGS
#define SQUARE(x) (x)*(x)
#include <stdio.h>
int main()
{
	int ret = SQUARE(4+1);
	printf("%d",ret);
	return 0;
}

定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。

#define _CRT_SECURE_NO_WARNINGS
#define DOUBLE(x) (x) + (x)
#include <stdio.h>
int main()
{
	int a = 5;
	printf("%d\n", 10 * DOUBLE(a));
	return 0;
}

这将打印什么值呢?
warning: 看上去,好像打印100,但事实上打印的是55. 我们发现替换之后:printf ("%d\n",10 * (5) + (5));

乘法运算先于宏定义的加法,所以出现了55
这个问题的解决办法是在宏定义表达式两边加上一对括号就可以了

#define _CRT_SECURE_NO_WARNINGS
#define DOUBLE( x) ( ( x ) + ( x ) )
#include <stdio.h>
int main()
{
	int a = 5;
	printf("%d\n", 10 * DOUBLE(a));
	return 0;
}

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

#define 替换规则

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

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索;例如printf("MAX=%d",MAX);双引号中的MAX不被替换

#和##

#把参数插入到字符串中

C语言中字符串是有自动连接的特点的

代码例子:

#include <stdio.h>
int main()
{
	printf("hello world\n");
	printf("hello " "world\n");
	printf("hel""lo " "world\n");
	return 0;
}
/*结果
hello world
hello world
hello world
*/

要解决print函数无法改变字符串的问题

例如:

void print(int a)
{
	printf("the value of a is %d\n",a);
}

#include <stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	print(a);
	print(b);
	return 0;
}
/*结果
the value of a is 10
the value of a is 20
*/

想要结果为

the value of a is 10
the value of b is 20

利用函数做不到,所以使用宏

#define PRINT(X) printf("the value of "#X" is %d\n",X)
#include <stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	PRINT(a);
	PRINT(b);
	return 0;
}
/*结果
the value of a is 10
the value of b is 20
*/

##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符

代码例子:

#define CAT(X,Y) X##Y
#include <stdio.h>
int main()
{
	int NEWyear = 2021;
	printf("%d\n", NEWyear);//2021
	printf("%d\n", CAT(NEW,year));//2021
}
/*结果
2021
2021
*/

带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导
致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。 例如:

x+1;             //不带副作用
x++;             //带有副作用
MAX宏可以证明具有副作用的参数所引起的问题。

副作用代码:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
	int a = 1;
	int b = a + 1;	//代码1
	int b = ++a;	//代码2:副作用:对b一样,但是改变了a的值
	return 0;
}

问题:输出的结果是什么?

#define _CRT_SECURE_NO_WARNINGS
#define MAX(X,Y)  ((X)>(Y)?(X):(Y))
#include <stdio.h>
int main()
{
	int a = 10;
	int b = 11;
	int max = MAX(a++,b++);
	printf("%d\n",max );
	printf("%d\n",a );
	printf("%d\n",b );
	return 0;
}

 结果和解析:

#define _CRT_SECURE_NO_WARNINGS
#define MAX(X,Y)  ((X)>(Y)?(X):(Y))
#include <stdio.h>
int main()
{
	int a = 10;
	int b = 11;
	int max = MAX(a++,b++);
	//((X)>(Y)?(X):(Y))
	//int max=((a++)>(b++)?(a++):(b++));
	//(a++)>(b++)   10>11?  --->0  a=11 b=12
	//(b++)   max=12   b=13
	//12
	//11
	//13
	printf("%d\n",max );
	printf("%d\n",a );
	printf("%d\n",b );
	return 0;
}


宏和函数对比

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

为什么不用函数来完成这个任务? 原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。

反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的

#define _CRT_SECURE_NO_WARNINGS
//宏-类型无关-更加灵活些
#define MAX(X,Y)  ((X)>(Y)?(X):(Y))
//函数要定义多个类型函数
int Max(int x,int y)
{
	return (x > y ? x : y);
}
float Max2(float x, float y)
{
	return (x > y ? x : y);
}

#include <stdio.h>
int main()
{
	int a = 10;
	int b = 11;
	float c = 3.14f;
	float d = 6.48f;
	int max = MAX(a, b);
	printf("%d\n",max );
	max = Max(a, b);
	printf("%d\n", max);

	float max2 = Max2(c,d);
	printf("%.2f\n", max2);
	return 0;
}

程序的规模和速度:每一句C语言代码都对应一列或多列汇编代码

//宏对应的汇编代码
	int max = MAX(a, b);
00CD52B0  mov         eax,dword ptr [a]  
00CD52B3  cmp         eax,dword ptr [b]  
00CD52B6  jle         main+63h (0CD52C3h)  
00CD52B8  mov         ecx,dword ptr [a]  
00CD52BB  mov         dword ptr [ebp-10Ch],ecx  
00CD52C1  jmp         main+6Ch (0CD52CCh)  
00CD52C3  mov         edx,dword ptr [b]  
00CD52C6  mov         dword ptr [ebp-10Ch],edx  
00CD52CC  mov         eax,dword ptr [ebp-10Ch]  
00CD52D2  mov         dword ptr [max],eax 

//函数对应的汇编代码
	max = Max(a, b);
00CD52E6  mov         eax,dword ptr [b]  
00CD52E9  push        eax  
00CD52EA  mov         ecx,dword ptr [a]  
00CD52ED  push        ecx  
00CD52EE  call        _Max (0CD13B6h)  //调用函数Max
00CD52F3  add         esp,8  
00CD52F6  mov         dword ptr [max],eax 

_Max:
00CD13B6  jmp         Max (0CD1840h) 

int Max(int x,int y)
{
00CD1840  push        ebp  
00CD1841  mov         ebp,esp  
00CD1843  sub         esp,0C4h  
00CD1849  push        ebx  
00CD184A  push        esi  
00CD184B  push        edi  
00CD184C  lea         edi,[ebp-0C4h]  
00CD1852  mov         ecx,31h  
00CD1857  mov         eax,0CCCCCCCCh  
00CD185C  rep stos    dword ptr es:[edi]  
00CD185E  mov         ecx,offset _9DFA4ABB_test@c (0CDC003h)  
00CD1863  call        @__CheckForDebuggerJustMyCode@4 (0CD1316h)  
	return (x > y ? x : y);
00CD1868  mov         eax,dword ptr [x]  
00CD186B  cmp         eax,dword ptr [y]  
00CD186E  jle         _main+1Bh (0CD187Bh)  
00CD1870  mov         ecx,dword ptr [x]  
00CD1873  mov         dword ptr [ebp-0C4h],ecx  
00CD1879  jmp         _main+24h (0CD1884h)  
00CD187B  mov         edx,dword ptr [y]  
00CD187E  mov         dword ptr [ebp-0C4h],edx  
00CD1884  mov         eax,dword ptr [ebp-0C4h]  
}
00CD188A  pop         edi  
00CD188B  pop         esi  
00CD188C  pop         ebx  
00CD188D  add         esp,0C4h  
00CD1893  cmp         ebp,esp  
00CD1895  call        __RTC_CheckEsp (0CD123Fh)  
00CD189A  mov         esp,ebp  
00CD189C  pop         ebp  
00CD189D  ret  


当然和宏相比函数也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。(预编译时已经完成了替换)
3. 宏由于类型无关,也就不够严谨。(没有类型检查)
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。(传递表达式会有副作用,函数没有)


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

#define SIZEOF(type) sizeof(type)
int main()
{
	int ret = SIZEOF(int);
	//替换为int ret=sizeof(int);
	return 0;
}
#include <stdio.h>
#include <stdlib.h>
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
	int* p = (int*)malloc(10*sizeof(int));
	int* p = MALLOC(10, int);
	return 0;
}

宏和函数的一个对比
 

#define定义宏

函数

每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长

函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码

更快

存在函数的调用和返回的额外开销,所以相对慢一些

宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。

函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。

参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。

函数参数只在传参的时候求值一次,结果更容易控制。

宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。

函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。

宏是不方便调试的

函数是可以逐语句调试的

宏是不能递归的

函数是可以递归的


一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。

那我们平时的一个习惯是:把宏名全部大写 函数名不要全部大写

C99  C++  inline - 内联函数   结合宏和函数的优点

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

#define MAX 100
#include <stdio.h>
int main()
{
	printf("%d",MAX);
#undef MAX
//C:\Users\93983\source\repos\test_1_19\test.c(8,18): error C2065: “MAX”: 未声明的标识符
	printf("%d", MAX);
	return 0;
}

命令行定义

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

例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。

(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)

Linux  test.c文件下

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

编译指令:

gcc test.c -D SZ=10

条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

#include <stdio.h>
#define __DEBUG__ 

int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
#ifdef __DEBUG__//如果__DEBUG__定义了,下面语句参加编译,否则不参加编译
		printf("%d\n", arr[i]);//为了观察数组是否赋值成功
#endif //__DEBUG__
	}
	return 0;
}

常见的条件编译指令:

1.单个分支的条件编译

#if 常量表达式
//...
#endif


//常量表达式由预处理器求值。

常量表达式为真则下列语句参加编译,为假下列语句就不参加编译
如:

#define __DEBUG__ 1
#if __DEBUG__
//..
#endif

2.多个分支的条件编译
 

#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

3.判断是否被定义

如果定义过symbol,下面语句参加编译,否则不参加

#if defined(symbol)		 //写法1
语句;
#endif

#ifdef symbol			//等价写法2
语句;
#endif

如果没有定义过symbol,下面语句参加编译,否则不参加

#if !defined(symbol)	        //写法1
语句;
#endif

#ifndef symbol			//等价写法2
语句;
#endif

4.嵌套指令

#if defined(OS_UNIX)
	#ifdef OPTION1
	unix_version_option1();
	#endif
	#ifdef OPTION2
	unix_version_option2();
	#endif
#elif defined(OS_MSDOS)
	#ifdef OPTION2
	msdos_version_option2();
	#endif
#endif

文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次。
 
头文件被包含的方式:

  • 本地文件包含
#include "filename"

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

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

/usr/include

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

VS2013:C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\include

VS2019:C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt(我的路径)

普遍方法:https://blog.csdn.net/aiqq136/article/details/112808553

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


库文件包含

#include <filename.h>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用  " " 的形式包含? 答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

嵌套文件包含

如果出现这样的场景:

  • comm.h和comm.c是公共模块。
  • test1.h和test1.c使用了公共模块。
  • test2.h和test2.c使用了公共模块。
  • test.h和test.c使用了test1模块和test2模块。
  • 这样最终程序中就会出现两份comm.h的内容。
  • 这样就造成了文件内容的重复。


如何解决这个问题? 答案:条件编译


每个头文件的开头写:

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
int Add(int x,int y);

#endif

或者:写法2

#pragma once
//头文件的内容
int Add(int x,int y);

就可以避免头文件的重复引入。
注: 推荐《高质量C/C++编程指南》中附录的考试试卷(很重要)。

笔试题:

1. 头文件中的 ifndef/define/endif是干什么用的?防止头文件被多次包含
2. #include 和 #include "filename.h"有什么区别?查找位置,查找策略不一样

其他预处理指令

#error
#pragma
#line
...
不做介绍,自己去了解。

#pragma pack()在结构体部分介绍。

参考《C语言深度解剖》学习
 

<---- To Be Continued

  • 8
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
引用\[1\]:在学习C语言时,除了在教室听课外,增加上机练习的机会非常重要。这样可以提高写代码的流畅度,减少在上机时的紧张感,并且能够更好地应对代码运行出错的情况。所以建议平时要腾出时间去上机练习,不仅限于课程指定的时间。\[1\] 引用\[2\]:此外,建议找一个考过的同学或者专门培训的老师一起学习C语言。因为C语言不仅仅是靠背诵和操作就能解决问题的,它需要理解和掌握。如果一个人学习,可能会因为某个问题而停滞不前,浪费很多时间。所以与他人一起学习可以相互帮助和解决问题。\[2\] 综上所述,要速成C语言计算机二级考试,你可以增加上机练习的时间,找一个考过的同学或专门培训的老师一起学习。这样可以提高你的编程能力和理解C语言的能力,帮助你更好地应对考试。 #### 引用[.reference_title] - *1* *2* [全国计算机二级C语言考试难不难?应该怎么备考?](https://blog.csdn.net/Hsuesh/article/details/108811167)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [计算机二级C语言怎么过](https://blog.csdn.net/qq_34997333/article/details/81234225)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值