c语言基础

一、初阶

1、vs基础

创建新项目-------->C+±-空项目------->创建 ------>源文件处右键—添加新建项----->代码、C++文件------>名称:test.c

编译:Fn + ctrl +F7

运行:Fn + ctrl +F5

调试:Fn+F10

进入函数:Fn+F11

3、 %d

//%c  字符串格式(char)

//%d 打印十进制有符号(int)
//%u 打印十进制无符号

//%f 单精度浮点型(float)

//%lf 双精度浮点型(double)

//%p 以地址的形式打印【数字在内存中补码的值】

//%x  1进制
//%s 打印16进制
#include <stdio.h>   //std-标准 i-输入 o-输出  standard input output 

int main() //int表示main函数调用返回一个整型-----return 0;0是整型
{
    float f = 5.1;
	printf("%f\n",f);
    printf("hello 111\n");  //print function 打印函数--库函数,得引入  \n换行
	return 0;
}

4、计算机单位

1、比特位 bit 计算机是硬件,在通电时有正电(1)和负电(0),一个1(或一个0)就是一个比特位。二进制 0 1

2、字节 byte 1字节 = 8比特位

3、kb 1kb = 1024 byte字节

4、mb 1mb = 1024 kb

5、gb 1gb = 1024 mb

6、tb 1tb = 1024 gb

7、pb 1pb = 1024 tb

#include <stdio.h>   

int main() 
{
	printf("%d\n", sizeof(char));   //1个字节  8个比特位    字符串
	printf("%d\n", sizeof(short));  //2个字节  16个比特位   2^16   0-65535
	printf("%d\n", sizeof(int));    //4个字节  32个比特位   2^32   0-4294967295
    //所以当定义age时,就可以用 short age = 20;  若用int貌似太大了,浪费
	printf("%d\n", sizeof(long));  //4/8个字节
	printf("%d\n", sizeof(long long));  //8个字节
	printf("%d\n", sizeof(float));  //4个字节
	printf("%d\n", sizeof(double));  //8个字节
	return 0;
}

char ch = 'w';
int weight = 120;  //申请3个字节用于存放体重
int salary = 20000;

5、变量

1、局部变量 全局变量

#include <stdio.h>
int a = 2019;//全局变量
int main()
{    
	int a = 2018;//局部变量            
	printf("a = %d\n", a);     //2018    局部变量优先
	return 0;
}

2、scanf输入函数 &取地址符

#define _CRT_SECURE_NO_WARNINGS 1 //取消scanf的报错,不要使用scanf_s 因为其余的编译器不认识
#include <stdio.h>
int main()
{
	//计算两个数的和
	int num1 = 0;
	int num2 = 0;
	int sum = 0;
	//输入两个数
	scanf("%d%d", &num1, &num2); //&取地址符  输入函数scanf  		
	//int sum = 0;   C语言规定变量的定义要放在最前面
	sum = num1 + num2;
	printf("sum = %d\n", sum);   //%d 前面的会全部输出
	return 0;
}

3、变量的作用域和作用周期

  1. 局部变量的作用域是变量所在的局部范围。局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。
  2. 全局变量的作用域是整个工程。全局变量的生命周期是:整个程序的生命周期。

4、extern声明(引入)外部符号

int main()
{
	extern int g_val;  //这个g_val变量是在另一个.c文件中
    printf("g_val = %d",g_val);
}

6、经典面试题

1、交换变量的值(^ 按位异或)

写代码交换两个int变量的值,不能使用第三个变量,即a=3,b=5,交换后,b=3,a=5

法一:^ 按位异或

按二进制进行位异或,相同为0,不同为1。 3的二进制是011,5的二进制是101(0是000,1是001,2是010,3是011,4是100,5是101)

#include <stdio.h>
int main()
{
    //交换两个int变量的值
    //法二:^ 按位异或
    int a = 3;   //3的二进制是011
    int b = 5;   //5的二进制是101
    a = a^b;     //a与b进行异或(011与101),1和1异或为0,1和0异或为1.a为110-----6
    b = a^b;     //(110与101异或)b为011-----3
    a = a^b;     //(110与011异或)a为101-----5
    printf("a=%d b=%d",a,b); 
    return 0;
}

法二:使用第三个变量

#include <stdio.h>
int main()
{
    //交换两个int变量的值
    int a = 3;
    int b = 5;
    int c = 0;
    c = a;
    a = b;
    b = c;
    printf("a=%d b=%d",a,b);
    return 0;
}

法三:和------问题:a+b可能整型溢出

#include <stdio.h>
int main()
{
    //交换两个int变量的值
    //法一:和------问题:a+b可能整型溢出
    int a = 3;
    int b = 5;
    a = a + b; //a是和8,b还是5
    b = a - b; //3给b
    a = b; //5给a
    printf("a=%d b=%d",a,b);  //问题:a+b可能整型溢出
    return 0;
}

2、找出只出现一次的数

题:给定一个非空整形数组,除了某个元素只出现一次以外,其余每个元素均出现两次,找出那个只出现了一次的元素。

样例: int a[ ] = {1,2,3,4,5,1,2,3,4},该数组中只有5出现了一次,其余数字都是成对出现,需要找出5

法一:暴力求解

#include <stdio.h>

int main()
{
	//找单身狗
	int arr[] = { 1,2,3,4,5,1,2,3,4 };
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);   //计算数组的元素个数
	for (i = 0; i < sz; i++) {
		//统计arr[i]在arr数组中出现的次数
		int count = 0;
		int j = 0;
		for (j = 0; j < sz; j++) {
			if (arr[i] == arr[j]) {
				count++;
			}
		}
		if (count == 1) {
			printf("单身狗是:%d\n", arr[i]);
			break;
		}
	}
	return 0;
}

法二:代码优化

异或运算满足交换律

//异或讲解
//3^3 = 0 ------a^a = 0
//0^5 = 5 ------0^a = a
//3^5^3 = 5
//3^3^5 = 0^5 = 5  说明异或运算满足交换律
int main()
{
    //因为a^a =0,0^a =a。只要把所有的数都异或在一起,得到的数就是单身狗
    //1^2^3^4^1^2^3^4^5 = 1^1^2^2^3^3^4^4^5 = 0^5 = 5
	int arr[] = { 1,2,3,4,5,1,2,3,4 };
	int i = 0;
    int ret = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);   //计算数组的元素个数
	for(i=0; i < sz; i++){
        ret = ret^arr[i];
    }
    printf("单数是:%d\n",ret);
	return 0;
}

3、关机程序

关机 shutdown -s -t 60 (-s关机 -t时间 60秒)

取消关机 shutdown -a

#define _CRT_SECURE_NO_WARNINGS 1 //取消scanf的报错,不要使用scanf_s 因为其余的编译器不认识
//这句话写在F:\vsanzhuangdifang\Common7\IDE\VC\vcprojectitems\newc++file.cpp里
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>  //system
#include <string.h>  //strcmp

int main() 
{
	char input[20] = { 0 };  //存储数据
	system("shutdown -s -t 60"); //system() 专门用来执行系统命令   -s关机 -t时间 60

again:
	printf("您的电脑将在1分钟后关机,如果输入:我是猪,就取消关机\n");
	scanf("%s", input);  //%s - 字符串
	if (strcmp(input, "我是猪") == 0)  //判断input中放的是不是“我是猪”  string compare 比较字符串
	{
		system("shutdown -a");  //取消关机
	}
	else {
		goto again;
	}
	return 0;
}

7、常量

C语言中的常量和变量的定义的形式有所差异。

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

1、字面常量

int main() {
	3;//字面常量
	100;
	return 0;
}

2、const修饰的常变量

1、变量
int main() {
	int num = 4;
	printf("%d\n", num);  //4
	num = 8;
	printf("%d\n", num);  //8
	return 0;
}
2、常量----const 常属性

num虽然被const成了常量,但它本质上还是一个变量,所以常变量

int main() {
	//const 常属性
	const int num = 5;
	printf("%d\n", num);
	//num = 8;  //这个时候num的值就不能更改,已经报错了
	//printf("%d\n", num);
	return 0;
}

3、#define 定义的标识符常量

#include <stdio.h>
#define MAX 10    //define定义的标识符常量  

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

4、枚举常量

枚举:一一列举。 enum 名{ , , };

性别:男 女 保密

三原色:红 绿 蓝

星期:1,2,3,4,5,6,7

#include <stdio.h>
//性别
enum Sex
{
    male,   //枚举常量
    female,
    secret
};
int main()
{
    //枚举
	//enum Sex s = male;
	printf("%d\n", male); //0
	printf("%d\n", female); //1
	printf("%d\n", secret); //2
	return 0;
}

#include <stdio.h>
enum Color  //首字母大写
{
    RED,
    BLUE,
    YELLOW
};
int main()
{
    enum Color color = BLUE;  //color是个变量
    printf("%d\n",color);    //1  //不能%c
    color =  YELLOW;  //color是个变量,值可以更改
    BLUE = 6; //BLUE是枚举常量1,这个不能更改
    return 0;
}

8字符串+转义字符+注释

1、字符串 \0

1、字符串的结束标志’\0’

这种由双引号引起来的一串字符称为字符串字面值,或者简称字符串。

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

#include <stdio.h>
int main()
{
    char arr1[] =  "abc" ;  //引号
    char arr2[] = { 'a','b','c' };  //括号
    printf("%s\n", arr1);  //abc
    printf("%s\n", arr2);  //abc烫烫烫---bc
    return 0;
}
2、调试 Fn+F10

Fn+F10让代码继续走

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M7XcpObr-1631092637284)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201008151157747.png)]

发现在arr1里多了一个0’\0’字符串的结束标志,若是给arr2也加一个0结果就一样了

#include <stdio.h>
int main()
{
    char arr1[] =  "abc" ;  //引号
    char arr2[] = { 'a','b','c',0 };  //括号  这个0是字符串的结束标志'\0'
    printf("%s\n", arr1);  //abc
    printf("%s\n", arr2);  //abc
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bwRIHBoS-1631092637286)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201008151421991.png)]

//结束标志'\0'  他的值是0  a---97  A----65【ASCII编码】
//arr1[]里放的 "abc"包含---'a','b','c','\0'    遇到'\0'就结束了,不打印了
//arr2[]里放的 ---'a','b','c'                  打印完'c',还没有遇到'\0'就随便打印了
3、字符串长度strlen
#include <stdio.h>
#include <string.h>
int main()
{
	char arr1[] = "abc";
	char arr2[] = { 'a','b','c' };
    char arr3[] = { 'a','b','c','\0' };
	printf("%d\n", strlen(arr1));  //3---a b c遇到\0 就3个
	printf("%d\n", strlen(arr2));  //15随机值----a b c还没结束,一直随机到\0   ----随机值
    printf("%d\n", strlen(arr3));  //3
	return 0;
}
4、转义字符 \
#include <stdio.h>
int main()
{
	printf("abc");  //abc
    printf("abc\n"); //\n换行
    printf("c:\test\32\test.c");  //  \t水平制表符(tab键)
	return 0;
}
image-20210311210150059

转义

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hCmd6uAt-1631092637287)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210311210810665.png)]

#include <stdio.h>
int main()
{
	printf("abc");  //abc
    printf("abc\\n"); //abc\n
    printf("c:\\test\32\\test.c");  // c:\test\32\test.c   因为\\会解析为一个\
	return 0;
}
#include <stdio.h>
int main()
{
	 //printf("%c\n",''') ; // 报错,前两个一对,后一个落单了
     printf("%c\n",'\''); //   '
     printf("%s\n","\""); //   "
	return 0;
}
5 \ddd 里面的ddd是八进制 0-7
#include <stdio.h>
#include <string.h>
int main()
{
	printf("%d\n", strlen("c:\test\32\test.c"));   //13 \t是一个字符 c : \t e s t \32 \t e s t . c
	return 0;
    /*
    * \32 ----32是2个8进制的数字
    * 32作为8进制代表的那个十进制数字,作为ASCII码值,对应的字符
    * 32  里的【2是:2个8^0  3是:3个8^1】 3*8^1 + 2*8^0 = 26 即为10进制的26
    *10进制的26 作为ASCII码值代表的字符为 →
    */
    /*
    *\ddd  里面的ddd是八进制 0-7
    *若是 \382就不行,因为8进制  0-7
    */
}
6 \xdd \x固定 dd两个16进制

14、关键字

auto (省略)  case  char   default  do   double else float  for  goto  if  int   long  return   short sizeof  switch  while
continue  循环语句里的    停止本次循环  不在switchbreak    switch语句中     跳出循环
const (常变量)     
enum (枚举) 
extern (引入外部.c文件的符号)   
register (寄存器关键字)     
signedsigned int 有符号的整型) 
unsignedunsigned int 无符号的整型)    
static (静态) 
struct (结构体关键字)  
typedef (类型定义) 
union  (联合体、共用体)  
void (无、空) 
volatile  

define 不是关键字

register 寄存器

register int a = 10; //把这个很重要的变量放在寄存器中,比访问内存拿数据要快得多----建议---就几十个寄存器

typedef 别名

int main()
{    
    //将unsigned int 重命名为u_int, 所以u_int也是一个类型名、别名
    typedef unsigned int u_int;
    //unsigned int num1 = 0;    
    u_int num1 = 0;    
    return 0;
}

static 修饰变量

static修饰局部变量,局部变量的生命周期变长(不销毁)
static改变了变量的作用域,让静态的全局变量只能在自己所在的源文件内部使用
static修饰函数,让函数只能在自己所在的源文件内部使用

static 修饰局部变量

#include <stdio.h>
void test()
{
	int a = 1;  //a 是局部变量----执行一次test  里面的a就被销毁了
	a++;     //a++ a就变成2 ------这里的a一直创建销毁,++ 一直是2
	printf("a= %d\n",a);
}
int main()
{
	int i =0;
	while (i < 5)
	{
		test();
		i++;
	}
	return 0;
}
//输出5个 a=2
#include <stdio.h>
void test()
{
	static int a = 1;  //a是静态的局部变量  static修饰局部变量,局部变量的生命周期变长
	a++;     
	printf("a= %d\n",a); //输出 a=2 a=3 a=4 a=5 a=6
}
int main()
{
	int i =0;
	while (i < 5)
	{
		test();
		i++;
	}
	return 0;
}

static 修饰全局变量

int g_val = 2020;  //全局变量------在add.c文件中
int main()   //在test.c文件中
{
	extern int g_val;
    printf("g_val = %d\n",g_val);  //g_val = 2020
    return 0;
}

若用 static修饰全局变量

//全局变量------在add.c文件中
static int g_val = 2020;   //用 static修饰全局变量
//在test.c文件中
int main()  
{
	extern int g_val;   //报错了,无法解析外部符号---说明static改变了变量的作用域,让静态的全局变量只能在自							己所在的源文件内部使用
    printf("g_val = %d\n",g_val);  //g_val = 2020
    return 0;
}

static修饰函数

//add.c文件
int Add(int x , int y)
{
	int z = x + y;
    return z;
}
//test.c文件
extern int Add(int , int)  //引入外部函数
int main()
{
    int a = 10;
    int b = 20;
    int sum = Add(a , b);
    printf("sum = %d\n",sum);  // sum = 30
    return 0;
}

若用static修饰函数的话,会报错

//add.c文件
static int Add(int x , int y)  //报错了,无法解析外部符号---说明static改变了函数的链接属性,(把外部链接属性变成了内部链接属性)
    //让函数只能在自己所在的源文件内部使用
{
	int z = x + y;
    return z;
}

#define 定义常量和宏(不是关键字)

//define 定义常量
#define MAX 100   //没有分号 
int main()
{
	int a = MAX;
	return 0;
}
//define 定义宏
#define MAX(X,Y) (x>y?x:y)
int main()
{
    int a = 10;
    int b = 20;
    max = MAX(a,b);
    printf("max= %d\n",max);
    return 0;
}
/**********************用函数的方法*******************************/
int Max(int x, int y)
{
    if(x > y)
        return x;
    else 
        return y;
}
int main()
{
    int a = 10;
    int b = 20;
    int max = Max(a,b);
    printf("max= %d\n",max);
    return 0;
}

二、分支和循环

1、分支语句

1、if

#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;
}

1、else悬空

(else和最近的未匹配的if进行匹配),不能因为位置看。(就近原则)

2、if书写形式
if (condition) 
{    
	return x;
}
return y;
//条件成立输出x , 否则输出y
/**************************/
if(condition)
{    
    return x;
}
else
{    
    return y;
}

//代码2
int num = 1;
if(num = 5)   //赋值
{    
    printf("hehe\n");  //会打印hehe
}
//代码3
if(num == 5)
{    
    printf("hehe\n");  //啥也不打印
}
//代码4
int num = 1;
if(5 == num)  //优化,代码应该这样写。。。把 5 放左边   少个等号就报错
{    
    printf("hehe\n");  //啥也不打印
}

2、switch

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

1、常规 switch(){case 1 : break; default: break}
 switch()
 {
     case 1 : 
         break; 
     default: //可有可无,最好写上
         break;
     //不要有continue 没意义
 }
#include <stdio.h>
int main()
{    
    int day = 0;    
    switch(day)   //day ------ 必须给整型、整形常量表达式   
   	{
        case 1printf("星期一\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; 
        default:
            printf("输入错误:\n");
            break; 
    }    
    return 0;
}
2、case后没有break的
//有时候我们的需求变了:
//1. 输入1-5输出的是“weekday”;
//2. 输入6-7输出“weekend”
#include <stdio.h>
int main()
{    
    int day = 0;    
    switch(day)    
    {
        case 1case 2:
        case 3:
        case 4:
        case 5:
            printf("weekday\n");
            break;
        case 6:
        case 7:
            printf("weekend\n");
            break;    //最后一个语句也要加break,以后要是来了个 case 8 呢
        default:
            printf("输入错误:\n");
            break; 
    }    
    return 0;
}
break语句的实际效果是把语句列表划分为不同的部分。编程好习惯在最后一个 case 语句的后面加上一条 break语句。(之所以这么写是可以避免出现在以前的最后一个 case 语句后面忘了添加 break语句)。
3、没有break 继续执行下面的case
#include <stdio.h>
int main()
{    
    int n = 1;    
    int m = 2;    
    switch (n)    //n=1,就进入case1
    {    
        case 1:
            m++;    //没有break 继续执行case 2  m=3
        case 2:
            n++;   // n=2 
        case 3:
            switch (n)    //n=2,进入case2        
            {//switch允许嵌套使用
                case 1:  //不执行
                    n++;
                case 2:
                    m++;   //m=4
                    n++;   //n=3
                    break;    //结束这个小switch,但是外面的大case3没有break,进入case4       
            }    
        case 4:
            m++;   //m=5
            break;   //结束 
        default:
            break;    
    }    
    printf("m = %d, n = %d\n", m, n);    //m = 5 , n = 3 
    return 0;
}

2、循环语句

1、while

while(表达式)
	循环语句;
break介绍

其实在循环中只要遇到break,就停止后期的所有的循环,直接终止循环。所以:while中的break是用于永久终止循环的。

//break 代码实例
#include <stdio.h>
int main()
{    
    int i = 1;    
    while(i<=10)
    {
     	if(i == 5)
            break;
        printf("%d ", i);
        i++;    
    }    
    return 0;
}
//输出
1 2 3 4
continue 介绍

continue是用于终止本次循环的,也就是本次循环中continue后边的代码不会再执行,而是直接跳转到while语句的判断部分。进行下一次循环的入口判断

//continue 代码实例1
#include <stdio.h>
int main()
{    
    int i = 1;    
    while(i<=10)    
    {
        if(i == 5)
            continue;  //n==5,continue,结束本次循环,进入下一次循环,判断while(i<=10) 又n==5,continue
        printf("%d ", i);
        i = i+1;    
    }    
    return 0;
}
//输出
1 2 3 4 死循环
//continue 代码实例2
#include <stdio.h>
int main()
{    
    int i = 0;    
    while(i<=10)    
    {
        i++; 
        if(i == 5)
            continue;  //跳出本次循环 重新判断
        printf("%d ", i);   
    }    
    return 0;
}
//输出
1 2 3 4 6 7 8 9 10
getchar 接受字符
putchar()输出字符
EOF(end of file) 文件结束标志 本质是-1
#include <stdio.h>
int main()
{
    int ch = getchar();  //getchar() 接受键盘输入的字符
    //putchar(ch); //putchar()输出字符  和 printf 是一样的
    while((ch=getchar()) != EOF)  
    {
       putchar(ch); //键盘输入什么,程序就打印什么。只有在键盘输入 ctrl+z 才会停止
    }
    return 0;
}
#include <stdio.h>
int main()
{    
    while ((ch = getchar()) != EOF)   //如果获得的字符不是 ctrl+z
    {
        if (ch <0|| ch >9)     //判断字符不在0-9
            continue;                 //结束本次循环,进入下次判断------
        putchar(ch);                  //只有输入0-9,才会打印,输入其他的字符都会重新判断
    }    
    return 0;
}
int main()
{
	int ret = 0;
    int password[20] = {0};
    printf("请输入密码:\n");  //输入123456后敲回车
    //在输入缓冲区 123456\n
    scanf("%s",password);  
    //scanf把123456拿走  还剩下 \n
    printf("请确认(Y/N):");
    ret = getchar();  //  Y/N
    //输入缓冲区还有 \n。 getchar()直接拿走了 ----if判断 \n != Y 所以确认失败
    if(ret == "Y")
    {
        printf("确认成功\n");
    }
    else
    {
        printf("确认失败\n");
    }
    return 0;
}

只要把剩余的 “\n” 给拿走就可以了,再加一个 getchar() 就可以了

int main()
{
	int ret = 0;
    int password[20] = {0};
    printf("请输入密码:\n");  //输入123456后敲回车
    //在输入缓冲区 123456\n
    scanf("%s",password);  
    //scanf把123456拿走  还剩下 \n
    getchar(); 
    //把剩余的 “\n” 给拿走
    printf("请确认(Y/N):");
    ret = getchar();  //  Y/N
    //这里getchar() 就可以拿到 Y/N
    if(ret == "Y")
    {
        printf("确认成功\n");
    }
    else
    {
        printf("确认失败\n");
    }
    return 0;
}

但是若是输入的密码为 123456 abc 又有问题了

因为scanf 只会拿走空格前的内容,出问题的地方一样

解决问题的方法在于,要不断的读取密码的内容,直到把 \n 也读走

while((ch=getchar()) != '\n')//循环取密码里的内容,直到把 \n 也读走
    {
        ;
    }
int main()
{
	int ret = 0;
    int ch = 0;
    int password[20] = {0};
    printf("请输入密码:\n");  //输入123456后敲回车
    //在输入缓冲区 123456\n
    scanf("%s",password);  
    //scanf把123456拿走  还剩下 \n
    while((ch=getchar()) != '\n')  //循环取密码里的内容,直到把 \n 也读走
    {
        ;
    }
    printf("请确认(Y/N):");
    ret = getchar();  //  Y/N
    //这里getchar() 就可以拿到 Y/N
    if(ret == "Y")
    {
        printf("确认成功\n");
    }
    else
    {
        printf("确认失败\n");
    }
    return 0;
}

2、for

for(表达式1;表达式2;表达式3)
    循环语句;
continue和break
/代码1
#include <stdio.h>
int main()
{    
    int i = 0;    
    for(i=1; i<=10; i++)    
    {
        if(i == 5)
            break;         //break直接退出循环
        printf("%d ",i);   // 1 2 3 4   
    }    
    return 0;
}
//代码2
#include <stdio.h>
int main()
{    
    int i = 1;    
    while(i<=10)    
    {
        if(i == 5)        //当i=5时,跳过下面的。在判断,可此时i仍然=5,死循环了
            continue;     //continue跳出本次循环
        printf("%d ",i);  // 1 2 3 4 进入死循环
        i++;
    }    
    return 0;
}
for循环的一些建议
1、不可在for 循环体内修改循环变量,防止 for 循环失去控制。
2、建议for语句的循环控制变量的取值采用“前闭后开区间”写法
int i = 0;
//前闭后开的写法
for(i=0; i<10; i++)   //10次循环
{}
//两边都是闭区间
for(i=0; i<=9; i++)   //可读差
{}
for循环的变种
#include <stdio.h>
int main()
{    
//变种1 -------省略    
    for(;;)            //恒为真 
    {
        printf("hehe\n");    //一直 hehe 死循环了
    }    
// 省略 --------- 
int main()
{
    int i = 0;
    int j = 0;
    for( ; i<10; i++)
    {
        for( ; j<10; j++)  //当i=0,第一次进入j的循环时,打印10次,j=10.出去i=1时,再进入j时,此时j=10,退出.j并没有被销毁,因为j=0是在外面的
        {
            printf("hehe\n");  // 10个hehe
        }
    }
    return 0;
}
#include <stdio.h>
int main()
{  
//变种2    
    int x, y;    
    for (x = 0, y = 0; x<2 && y<5; ++x, y++)   //当x=2时,条件为假 
    {
        printf("hehe\n");    //2个hehe
    }    
    return 0;
}
for循环笔试题
//请问循环要循环多少次? 0
#include <stdio.h>
int main()
{    
    int i = 0;    
    int k = 0;    
    for( i=0,k=0; k=0; i++,k++)   // k=0 是赋值语句 。 k的值是0 。 0是假 循环压根就不进去
        k++;                      //若是 k=1 非0 就是死循环了
    return 0;
}

3、do while

//不经常用
do
    循环语句    //上来先干,干完在判断。所以至少要执行一次
while(表达式);

2.1分支循环作业

选择题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xfyPfW9j-1631092637291)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201012113246793.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gmGXUhvp-1631092637294)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201012113425368.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fpiEXE88-1631092637297)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201012113623777.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kIdVbX5o-1631092637298)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201012114333594.png)]

switch里没有break,会继续执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zrAmO3aT-1631092637300)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201012114126140.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H3JGtTMk-1631092637303)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201012144439987.png)]

代码题

1、阶乘
//1. 计算 n的阶乘。
#include <stdio.h>
int main()
{
    int n = 0;     //这里不考虑溢出的问题
    int i = 0;
    int ret = 1;        //这里的初始值是1  不能是0
    printf("请输入n:\n");
    scanf("%d",&n);    //这里是%d
    for(i=1; i<=n; i++)
    {
        ret = ret * i;
    }
    printf("结果是:%d\n",ret); //不要忘记 %d
    return 0;
}
2、阶乘和
//2. 计算 1!+2!+3!+......+10!
#include <stdio.h>
int main()
{
    int i = 0;
    int n = 0;
    int ret = 1;
    int sum = 0;
    for (n = 1; n <= 3; n++)
    {
        ret = 1;    //放这里
        for (i = 1; i <= n; i++)
        {
            ret = ret * i;
        }
        sum += ret;  //
    }

    printf("结果是:%d\n", sum); //不要忘记 %d
    return 0;
}
/*******************代码优化**************************/
int main()
{
    int i = 0;
    int n = 0;
    int ret = 1;
    int sum = 0;
    for (n = 1; n <= 3; n++)
    {
        ret = ret * n;   //先阶乘,然后加起来
        sum += ret;
    }
    printf("结果是:%d\n", sum); //不要忘记 %d
    return 0;
}
3、二分查找法
/*3. 在一个有序数组中查找具体的某个数字n。
	编写int binsearch(int x, int v[], int n); 
    功能:在 v[0]<=v[1]<=v[2]<= ....<=v[n-1] 的数组中查找x.  */
#include <stdio.h>
int main()
{
    int arr[] = {1,2,3,4,5,6,7,8,9,10};
    int k = 7;
    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);  //数组个数
    for(i=0; i<sz; i++)
    {
        if( k == arr[i])
        {
            printf("找到了,下标是:%d\n",i);
            break;   //找打了就结束循环
        }
    }
    //找不到呢
    if( i == sz)   //i和个数一样时,找到头了
    {
        printf("找不到\n");
    }
}
/*******************代码优化*   二分法 次数 log以2为底的n  *************************/
//太简单了,不会考
#include <stdio.h>
int main()
{
    int arr[] = {1,2,3,4,5,6,7,8,9,10};  //下标 0 ------ 9
    int sz = sizeof(arr) / sizeof(arr[0]);  //元素个数
    int left = 0;   //左下标
    int right = sz - 1;    //右下标
    int k = 7;   //让找到7
    //根据左右下标算出中间元素的下标
    //int mid = (right + left) / 2;   //(0+9)/2=4  中间元素的下标  向下取整
    while(left <= right)
    {
        int mid = (right + left) / 2;   //每次都要用中间元素进行比较
        if(arr[mid] > k)
        {
            right = mid - 1;   //中间元素>目标值  他在左边  右-1
        }
        else if(arr[mid] < k)
        {
            left = mid +1 ;
        }
        else
        {
            
            printf("找到了,下标是:%d\n",mid);    //%d
            break;   //找到了就跳出去
        }
    }
    if(left > right)
    {
        printf("找不到\n");
    }
    return 0;
}
4、动态打印
//4. 编写代码,演示多个字符从两端移动,向中间汇聚。
#include <stdio.h>
#include <string.h>
#include <windows.h>
#include <stdlib.h>
int main()
{
    char arr1[] = "welcome to bit!!!!!";  //用 char 哦
    char arr2[] = "###################";
    //char arr2[] = "                    ";
    int left = 0;   //左下标
    //int right = sizeof(arr1)/sizeof(arr1[0]) -2;  因为arr1 里最后面还有 \0 占一个位置
    int right = strlen(arr1) - 1;   //右下标 用字符串长度-1 ----  #include <string.h>

    while (left <= right)
    {
        arr2[left] = arr1[left];   //把arr1的东西拿下来覆盖掉arr2
        arr2[right] = arr1[right];
        printf("%s\n", arr2);   //%s   16进制
        Sleep(100);  //休息  ---------  #include <windows.h>
        system("cls");  //执行系统命令的一个函数 cls  清空屏幕 ---- #include <stdlib.h>
        left++;
        right--;
    }
    printf("%s\n", arr2);
    return 0;
}
5、strcmp比较两个字符串
/*5. 编写代码实现,模拟用户登录情景,并且只能登录三次。(只允许输入三次密码,如果密码正确则提示登录成,如果三次      均输入错误,则退出程序*/
// 只循环3次就可以
#define _CRT_SECURE_NO_WARNINGS 1 
#include <stdio.h>
int main()
{
    int i = 0;
    char password[20] = {0};  //用户输入的密码
    for(i=0; i<3; i++)
    {
        printf("请输入密码:");
        scanf("%s",password);
        if(strcmp(password, "123456") == 0) // == 不能用于比较两个字符串是否相等  得用 strcmp
        {
            printf("登录成功\n");
            break;
        }
        else
        {
            printf("密码错误\n");
        }
	}
    if(i == 3)
    {
        printf("三次密码错误,退出程序\n");
    }
    return 0;
}
1、写代码将三个数按大到小输出
//1、写代码将三个数按大到小输出
#include <stdio.h>
int main()
{
    int a = 0;
    int b = 0;
    int c = 0;
    printf("请输入三个数:\n");
    scanf("%d%d%d", &a, &b, &c);
    //算法实现 -- a最大值  b--中间值  c--最小值
    //要求从大到小,我们判断 a < b   a<c  b<c
    //先在a b中找到较大值,用这个较大值与c 比,大的是最大值a  
    //然后比剩下的俩
    if (a < b)
    {
        int temp = b;
        b = a;
        a = temp;
    }
    if (a < c)
    {
        int temp = a;
        a = c;
        c = temp;
    }
    if (b < c)
    {
        int temp = b;
        b = c;
        c = temp;
    }
    printf("从大到小排序是:%d %d %d", a, b, c);
        return 0;
}
2、打印3的倍数的数
//2、打印3的倍数的数
//简单--跳过
#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 0;i<100; i++)
	{
		if (i % 3 == 0)
		{
			printf("%d", i);
		}
	}
	return 0;
}
3、最大公约数
//3、最大公约数
#include <stdio.h>
int main()
{
    int m = 24;
    int n = 18;
    int r = 0;
    scanf("%d%d",&m,&n);
    while( r = m % n )
    {
        //r = m % n;  //判断和这都要算m%n 直接在判断处给一个结果即可
        m = n;
        n = r;
    }
    printf("%d\n",n);
    return 0;
}
4、打印闰年
//4、打印闰年
#include <stdio.h>
int main()
{
    int year = 0;
    int count = 0;
    for (year = 1000; year <= 2000; year++)
    {
        //判断是否为闰年
        //1、能被4整除且不能被100整除
        //2、能被400整除
        if (((year % 4 == 0) && (year % 100 != 0))||(year % 400 == 0))
        {
            count++;
            printf("%d\n", year);
        }
    }
    printf("%d\n", count);
    return 0;
}
5、打印100-200之间的素数
//5、打印100-200之间的素数
#include <stdio.h>
int main()
{
    int i = 0;
    int count = 0;
    for (i = 100; i <= 200; i++)
    {
        //素数?  
        //1、试除法:能被1和他本身整除 试一下2到i-1的数
        int j = 0;
        for (j = 2; j < i; j++)
        {
            if (i % j == 0)  //如果有一个能被整除,就说明i不是素数,赶紧跳出去
            {
                break;  //跳出里层的for循环 --- 到1.2处
            }
        }
        //1.2 
        if (j == i)
        {
            count++;
            printf("%d ", i);
        }
    }
    printf("\n count = %d", count);
    return 0;
}
/********************  代码优化  *****************************/
//开平方的库函数 sqrt()
#include <stdio.h>
#include <math.h>
int main()
{
    int i = 0;
    int count = 0;
    for (i = 100; i <= 200; i++)
    {
        //素数?  
        //1、试除法:要是在开平方i 之前都找不到能被整除的,那就没了   sqrt() 
        int j = 0;
        for (j = 2; j <= sqrt(i); j++)
        {
            if (i % j == 0)  //如果有一个能被整除,就说明i不是素数,赶紧跳出去
            {
                break;  //跳出里层的for循环 --- 到1.2处
            }
        }
        //1.2 
        if (j > sqrt(i))  //如果j大于开平方i -- 不满足循环条件
        {
            count++;
            printf("%d ", i);
        }
    }
    printf("\n count = %d", count);
    return 0;
}
/********************  代码再次优化  *****************************/
//偶数绝对不是素数
//100-200之间 那就101 103 105 ...判断,从101开始,每次加2
#include <stdio.h>
#include <math.h>
int main()
{
    int i = 0;
    int count = 0;
    for (i = 101; i <= 200; i+=2)
    {
        //素数?  
        //1、试除法:要是在开平方i 之前都找不到能被整除的,那就没了   sqrt() 
        int j = 0;
        for (j = 2; j <= sqrt(i); j++)
        {
            if (i % j == 0)  //如果有一个能被整除,就说明i不是素数,赶紧跳出去
            {
                break;  //跳出里层的for循环 --- 到1.2处
            }
        }
        //1.2 
        if (j > sqrt(i))  //如果j大于开平方i -- 不满足循环条件
        {
            count++;
            printf("%d ", i);
        }
    }
    printf("\n count = %d", count);
    return 0;
}
1、数9的个数
//1、数9的个数
//、编写程序数一下 1 - 100 的所有整数中出现多少个数字9 
//个位是9(模10余9) 或 十位是9(除以10商9)
int main()
{
	int i = 0;
	int count = 0;
	for (i = 1; i <= 100; i++)
	{
		//个位是9   (模10余9)
		if (i % 10 == 9)
			count++;
		//十位是9 (除以10商9)
		if (i / 10 == 9)  //这里都是 if  因为99算两个9,若是 else if的话会少一个9
			count++;
	}
	printf("%d", count);
	return 0;
}
2、分数求和
//2、分数求和
//计算 1/1 - 1/2 + 1/3 - 1/4 + 1/5 ........... +1/99 -1/100 的值,打印出结果
int main()
{
	int i = 0;
	double sum = 0.0;  //最后的结果是小数
	int flag = 1;      //变号的
	for (i = 1; i <= 100; i++)
	{
		sum += flag * 1.0 / i;  //为了结果sum是小数,1和i最少有一个是小数  +=
		flag = -flag;  //变号
	}
	printf("%lf\n", sum);  //这里是 lf
	return 0;
}
3、求最大值
//3、求最大值
//  求10个整数中的最大值
//下面是个有问题的代码
int main()
{
	int arr[] = { 1,2,3,4,6,5,7,8,9,10 };
	int max = 0;
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		if (arr[i]  > max)
		{
			max = arr[i];
		}
	}
	printf("max = %d\n", max);
	return 0;
}
/***********代码改进*************************/
//若10个负数呢????  { -1,-2,-3,-4,-6,-5,-7,-8,-9,-10 }  结果是0
//为什么max是0呢??
//所以 max = arr[0]
//i从1 开始,第二个数开始比较
int main()
{
	int arr[] = { -1,-2,-3,-4,-6,-5,-7,-8,-9,-10 };
	int max = arr[0];
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 1; i < sz; i++)  // 0开始 和 1开始 都行
	{
		if (arr[i] > max)
		{
			max = arr[i];
		}
	}
	printf("max = %d\n", max);  //-1
	return 0;
}
4、输出乘法口诀表 %-2d 打印两位左对齐数
//4、输出乘法口诀表
int main()
{
    int i = 0;
    int j = 0;
    //int sum = 0;  直接用i*j
    for (i = 1; i <= 9; i++)
    {
        for (j = 1; j <= i; j++)
        {
            //sum = i * j;
            printf("%d*%d=%-2d ", i, j, i*j);  //这里用%-2d(左对齐) %2d(右对齐)  是因为打印两位数
        }
        printf("\n");  //这个换行打印
    }
    return 0;
}



//4、乘法口诀表
//要求:实现一个函数,打印惩罚口诀表,口诀表的行数和列数自己指定
//如:输入9,输出9*9的口诀表。输入12,输出12*12的乘法口诀表
void print_table(int m)
{
	int i = 0;
	for (i = 1; i <= m; i++)  //行
	{
		int j = 0;
		for (j = 1; j <= i; j++)//列
		{
			printf("%d*%d=%-3d  ", i, j, i * j);
		}
		printf("\n");
	}
}
int main()
{
	int m= 0;
	scanf("%d", &m);
	print_table(m);
	return 0;
}
5、二分查找(重复)
//5、二分查找
//写一个代码在一个整形有序数组中查找具体的某个数
//要求:找到了就打印数字所在的下标,找不到则输出:找不到
#include <stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 7;   //要查找的数
	int left = 0; //左下标0
	int sz = sizeof(arr) / sizeof(arr[0]);
	int right = sz-1;
	//int mid = (left + right) / 2;
	while (left<=right)   //这里用的while循环  条件是 left<=right
	{
		int mid = (left + right) / 2;   //每次循环都要判断一次中间,所以放循环里面
		if (k < arr[mid])
		{
			right = mid - 1;   //这里是 mid-1 , 不是 arr[mid]-1
		}
		else if (k > arr[mid])
		{
			left = mid + 1;
		}
		else                //最后一个只是 else
		{
			printf("找到了,下标是:%d\n",mid);   //下标是mid 不是arr[mid]
			break;          //这里要有 break 
		}
	}
	if (left > right)
	{
		printf("找不到\n");
	}
	return 0;
}

3、猜数字

//猜数字游戏
/*
	*思路:希望游戏玩完一把不过瘾,再来一把。每一把游戏都是一次代码的执行--循环
	* 这个游戏至少进入一次,所以用 do while 结构
	* 进入游戏,有一个简单的菜单函数 menu  1就是玩游戏 0就是退出游戏
	* 用户可能输入1 或输入0 或输入其他值(default) ---- 多分支结构  switch 结构
	* 若是输入1 , 就进入游戏,-- game函数
	* 若是输入0 ,退出游戏
	* 其他数字 ,输入错误, 再给你一次机会
	* 猜数字猜对了可能还想再来一把  输入0 再给你一次机会,再来一把---所以循环的判断条件是 看你输入的input是几
	*   输入0 为假,1或其他值为真--进入循环
	* 猜数字函数: 1、生成一个随机数  2、猜数字
	* 1、生成一个随机数  rand (引入stdlib.h头文件) 
	*  srand随机数的生成器   用时间戳来设置一个随机起始点--math.h    srand((unsigned int)time(NULL));
	*   rand生成的随机数在0-32767之间,%100的余数在0-99, 在+1 就1-100之间
	* 2、猜数字 循环猜 for不行   用while while(1)
*/
#define _CRT_SECURE_NO_WARNINGS 1 
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
void menu()   //函数的返回值是空
{
	printf("******************************\n");
	printf("**   1、play   0、exit   *****\n");
	printf("******************************\n");
}
//RAND_MAX  32767
void game()
{
	//1、生成一个随机数
	int ret = 0;     // 引入stdlib.h头文件
	//srand((unsigned int)time(NULL));  //用时间戳来设置一个随机起始点--math.h   
										//地方放错了  随机数生成器生成一次就可以 ,放在主函数里
	int guess = 0;   //接收猜的数字
	ret = rand()%100+1;        //rand生成的随机数在0-32767之间,%100的余数在0-99, 在+1 就1-100之间
    					     //这个rand不能放在while里,不然永远猜不到
	//printf("%d\n", ret);
	//2、猜数字 循环猜 for不行  
	while (1)
	{
		printf("请猜数字:>");
		scanf("%d", &guess);
		if (guess > ret)
		{
			printf("猜大了\n");
		}
		else if (guess < ret)
		{
			printf("猜小了\n");
		}
		else
		{
			printf("猜对了\n");
			break;
		}
	}
}
int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));   //放在主函数里
	do
	{
		menu();  //菜单函数,执行完菜单函数后,让用户去选择 1 或 0
		printf("请选择:>");
		scanf("%d", &input);  //& 把取到的值放到变量input里
		switch (input)        //判断是看你input输入的是几
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("输入错误哟\n");
			break;
		}
	} while (input);      //问问你是否再来一把
	return 0;
}

4、goto语句

goto语句 能不用就不用

但是某些场合下goto语句还是用得着的,最常见的用法就是终止程序在某些深度嵌套的结构的处理过程,例如一次跳出两层或多层循环。
这种情况使用break是达不到目的的。它只能从最内层循环退出到上一层的循环。
下面是使用goto语句的一个例子:一个关机程序
//一个关机程序  goto语句
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{    
    char input[10] = {0};    
    system("shutdown -s -t 60");  // <stdlib.h>
again:    
    printf("电脑将在1分钟内关机,如果输入:我是猪,就取消关机!\n请输入:>");    
    scanf("%s", input);    //字符 s
    
    if(0 == strcmp(input, "我是猪"))    //比较两个字符串  strcmp -- <string.h>
    {
        system("shutdown -a");    
    }
    else
    {
        goto again;    
    }    
    return 0;
}
/********************   不用goto语句  可用while代替 ********************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{    
    char input[10] = {0};    
    system("shutdown -s -t 60");  // <stdlib.h>
	while(1)
    {   
        printf("电脑将在1分钟内关机,如果输入:我是猪,就取消关机!\n请输入:>");    
        scanf("%s", input);    //字符 s

        if(0 == strcmp(input, "我是猪"))    //比较两个字符串  strcmp -- <string.h>
        {
            system("shutdown -a");   
            break;
        }   
    }
    return 0;
}

三、函数和递归

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

1、库函数

#include <stdio.h>
//函数----定义函数
int Add(int x, int y)
{
    int z = x + y;
    return z; //因为返回的z是整型,所以 int Add(int x, int y)
}
int main()
{    
	int num1 = 10;
	int num2 = 20;   
    int sum = Add(num1, num2);  //调用函数
	printf("sum = %d\n", sum);   
	return 0;
}

2、根据参考文档学习库函数

MSDN
www.cplusplus.com
http://en.cppreference.com

2.1 strcpy 字符串拷贝

#include <stdio.h>
#include <string.h>
int main()
{
	char arr1[] = "bit";   // 里面有 b i t \0  
	char arr2[20] = "########";
	strcpy(arr2, arr1); //字符串拷贝,把后面的拷给前面的
	printf("%s\n", arr2);   //输出 bit\0  遇到 \0 就结束打印了
	return 0;
}

2.2 memset内存设置

memory-内存 set-设置

//把arr前五个字符设置为#
int main()
{
	char arr[] = "hello world";
	memset(arr, '#', 5);
	printf("%s\n", arr);  //##### world
	return 0;
}

3、自定义函数 --交换两个值(用地址、指针)

//自定义函数---交换两个值
//失败的原因在于 a、b 和 x、y 压根就是两个内存,交换了x、y,但是关人家a、b啥事啊 
void swap1(int x, int y)  //返回值是空
{
	int temp = 0;
	temp = x;
	x = y;
	y = temp;
}
int main()
{
	int a = 10;
	int b = 20;
	printf("a=%d b=%d\n", a, b);  // a=10 b=20
	swap1(a, b);
	printf("a=%d b=%d\n", a, b);  // a=10 b=20
	return 0;
}
//失败的原因在于 a、b 和 x、y 压根就是两个内存,交换了x、y,但是关人家a、b啥事啊 
//引子 int* pa = &a;  *pa = 20;
int main()
{
	int a = 10;
	int* pa = &a; //&a取a的地址, pa是指针变量, pa的类型是 int*
	//*pa; //*pa 是解引用操作 -- 找到里面存的内容 --这里*pa里存的就是10
	*pa = 20; //借助*pa 就把a的值改为20
	return 0;
}
//自定义函数---交换两个值---swap2
//我们不把a、b的值传过去, 把a、b的地址传过去
void swap2(int* pa, int* pb)  //接受a、b的地址 -- int* pa
{
	int temp = 0;
	temp = *pa;
	*pa = *pb;
	*pb = temp;         //返回值依然是空, void
}
int main()
{
	int a = 10;
	int b = 20;
	printf("a=%d b=%d\n", a, b);  
	swap2(&a, &b);  //我们不把a、b的值传过去, 把a、b的地址传过去
	printf("a=%d b=%d\n", a, b);  
	return 0;
}

4、实参、形参

实参 swap2(&a, &b) 这个&a, &b就是实参 --- 确定的值
形参 swap2(int* pa, int* pb) 这个int* pa, int* pb就是形参 【形参实例化之后其实相当于实参的一份临时拷贝】

5、函数调用

5.1、传值调用

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

swap1(a, b);
int Add(int x, int y)

5.2、传址调用

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

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

swap2(&a, &b);
void swap2(int* pa, int* pb)

5.3、练习题

//1. 写一个函数可以判断一个数是不是素数。
#include <math.h>
int is_prime(int n)
{
	//素数(只能被自己和1整除的) ----- 在2--sqrt之间找
	int j = 0;
	for (j = 2; j <= sqrt(n); j++)  //这里是 <= 
	{
		if (n % j == 0)   //这里 ==
		{
			return 0;   //return 0 ... 不是 !=  return 0
			//break;    没有
		}
	}
	return 1;
}
int main()
{
	//打印100-200之间的素数
	int i = 0;
	int count = 0;
	for (i = 100; i<= 200; i++)
	{
		//判断i是否为素数
		if (is_prime(i) == 1)
		{
			count++;
			printf("%d\n", i);
		}
	}
	printf("count=%d\n", count);
	return 0;
}
//2. 写一个函数判断一年是不是闰年。
int is_leap_year(int x)
{
	if (((x % 4 == 0) && (x % 100 != 0)) || (x % 400 == 0))
	{
		return 1;
	}
	else    /*************   不能少了else    *************/
	{
		return 0;
	}
}
int main()
{
	int year = 0;
	for (year = 1000; year <= 2000; year++)
	{
		if (1 == is_leap_year(year))
		{
			printf("%d\n", year);
		}
	}
	return 0;
}

//3. 写一个函数,实现一个整形有序数组的二分查找。
//去哪查找,找谁,找到了返回什么
int binary_search(int arr[],int k,int sz)  //接收数组 int arr[]
    					//binary_search接收到的是一个指针,那么sz= 4/4 =1
{
	int left = 0;
	//int sz = sizeof(arr) / sizeof(arr[0]);
	int right = sz - 1;
	int i = 0;
	//int mid = (left + right) / 2;
	//for (i = 0; i <= sz; i++) // 不是for----用的是while
	while(left<=right)   // =  不能少,二分到最后还剩一个数,=时能进去
	{
		int mid = (left + right) / 2;  //mid决不能放在循环外面,每次循环都会有一个新的mid
		if (arr[mid] > k )
		{
			right = mid - 1;
		}
		else if (arr[mid] < k)
		{
			left = mid + 1;
		}
		else
		{
			return mid; // mid
		}
	}
	return -1; //找不到 -1
}

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 7;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int ret = binary_search(arr, k,sz); //传过去要查找的数组arr,要查找的数k, 
									 //找到了返回下标0-9,找不到返回-1 返回值ret
					//形参实例化之后其实相当于实参的一份临时拷贝----浪费内存
					//这里传过去的arr是数组首元素的地址,binary_search接收到的是一个指针,那么sz= 4/4 =1
		//那我直接把sz算好,传过去
	//先去写函数怎么用,再去写函数怎么实现
	if (ret == -1)
	{
		printf("没找到\n");
	}
	else
	{
		printf("找到了,下标是%d\n", ret);
	}
	return 0;
}
//4. 写一个函数,每调用一次这个函数,就会将num的值增加1。
void add(int* p)  //传址接收 接收一个地址 指针变量 用int* 地址
{
	(*p)++;  // ++的优先级高于* 所以加()---- *解引用 --(*p)++ ---相当于num++
}
int main()
{
	int num = 0;
	add(&num);    //传址  &
	printf("num=%d\n", num);
	add(&num);
	printf("num=%d\n", num);
	add(&num);
	printf("num=%d\n", num);
	return 0;
}

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

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

//链式访问
int main()
{
    //1
	int len = 0;
    len = strlen("abc");
    printf("%d\n",len);
    //2 链式访问
    printf("%d\n",strlen("abc"));
    return 0;
}
int main()
{
	printf("%d", printf("%d", printf("%d", 43)));  //4321
	return 0;
}
//printf 的返回值 是 打印的字符个数
//printf("%d", printf("%d", printf("%d", 43)));  43    最里层的printf返回2
//printf("%d", printf("%d", 2));   2    中间层的printf返回1
//printf("%d", 1);   1
//结果 43 2 1 ---- 4321

7、函数的定义和声明

工作中的用法是:
将函数的声明放在add.h中,
将函数的定义(实现)放在add.c中,
在test.c中使用加法,引入头文件#include "add.h"   使用Add()函数

8、函数递归

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

8.1递归

递归的两个必要条件

​ 存在限制条件,当满足这个限制条件的时候,递归便不再继续。

​ 每次递归调用之后越来越接近这个限制条件

//练习1
//接受一个整型值(无符号),按照顺序打印它的每一位。例如:输入:1234,输出 1 2 3 4
void print(int n)
{
	if (n > 9)
	{
		print(n/10);
	}
	printf("%d ", n%10);
}
int main()
{
	unsigned int num = 0;
	scanf("%d", &num);
	print(num);
	return 0;
}
//练习2
//编写函数不允许创建临时变量,求字符串的长度  方法3
//1、使用临时变量
#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = "bit";
	int len = strlen(arr);
	printf("len=%d\n",len);
	return 0;
}
//2、函数+临时变量
#include <stdio.h>
#include <string.h>
int my_strlen(char* str) //传过来的是地址(str存的是b的地址), char*的指针变量  返回的是长度int
{
	int count = 0;
	//if (*str != '\0')  //*str相当于b i t \0
	while (*str != '\0')  //if不能循环,改成while
	{
		count++;  //计数
		str++;    //指针后移一位
	}
	return count;
}
int main()
{
	char arr[] = "bit";
	//int len = strlen(arr);
	//printf("len=%d\n", len);
	//使用函数
	int len = my_strlen(arr); //数组在传参的时候,其实是把数组首元素的地址传过去
	printf("len=%d\n", len);
	return 0;
}
//3、递归+不适用临时变量
#include <string.h>

//3递归的方法
/*把大事化小
* my_strlen("bit")
*  1 + my_strlen("it")
*  1 + 1 + my_strlen("t")
*  1 + 1 + 1 + my_strlen("")
*  1 + 1 + 1 + 0
*  3
*/
int my_strlen(char* str) //str---> b的地址
{
	if (*str != '\0')  // *str 就是 b i t \0
	{
		return 1 + my_strlen(str + 1);   //str指向b。str+1就指向i
	}
	else
		return 0;
}
int main()
{
	char arr[] = "bit";
	//int len = strlen(arr);
	//printf("len=%d\n", len);
	//使用函数
	int len = my_strlen(arr); //数组在传参的时候,其实是把数组首元素的地址传过去
	printf("len=%d\n", len);
	return 0;
}

/********************************/
//4、指针
int my_strlen(char* str)   //传过来字符串的首元素的地址 
{
	char* start = str;
	char* end = str;
	while (*end != '\0')
	{
		end++;
	}
	return end- start;
}
int main()
{
	char arr[] = "bit";  //双引号
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}

8.2、递归与迭代

//求n的阶乘
//迭代
#include <stdio.h>
int Fac1(int n)
{
	int i = 0;
	int ret = 1;
	for (i = 1; i <= n; i++)
	{
		ret *= i;
	}
	return ret;
}
int main()
{
	//求n的阶乘
	int n = 0;
	int ret = 0;
	scanf("%d", &n);
	ret = Fac1(n); //循环的方式
	printf("%d\n", ret);
	return 0;
}


//递归
#include <stdio.h>
int Fac2(int n)
{
	if (n <= 1)
		return 1;
	else
		return n * Fac2(n - 1);
}
int main()
{
	//求n的阶乘
	int n = 0;
	int ret = 0;
	scanf("%d", &n);
	ret = Fac2(n); //循环的方式
	printf("%d\n", ret);
	return 0;
}

8.3斐波那锲数列–递归和迭代

//斐波那锲数列
// 1 1 2 3 5 8 13   前两个数之和等于第三个数
//递归 --倒推 慢
#include <stdio.h>
int Fib(int n)
{
	if (n <= 2)
		return 1;
	else
		return Fib(n - 1) + Fib(n - 2);
}
int main()
{
	int ret = 0;
	int n = 0;
	scanf("%d", &n);
	ret = Fib(n);
	printf("ret= %d\n", ret);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vFxvv3t2-1631092637306)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201015213359744.png)]

当求第50个斐波那锲数列时,贼慢--------用递归就不合适-------用迭代

//迭代 -- 正推---合适
int Fib1(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;  //灵魂  当n<=2时,返回c 是1
	while (n > 2)
	{
		c = a + b;
		a = b;
		b = c;
		n--;   //灵魂
	}
	return c;
}
int main()
{
	int ret = 0;
	int n = 0;
	scanf("%d", &n);
	ret = Fib1(n);
	printf("ret= %d\n", ret);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VM1zfiBv-1631092637310)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201015215217757.png)]

8.4、栈溢出

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-msJBRESl-1631092637312)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201015102639923.png)]

8.5递归经典题

8.5.1、汉诺塔问题
//
8.5.2、青蛙跳台阶问题
//

函数递归作业

1、选择题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2HxRNixB-1631092637313)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020101452285.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hcHTK2qZ-1631092637316)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020101711088.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RiAaEY3U-1631092637319)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020101953039.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y2Ncn0GB-1631092637321)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020102308454.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1zMFIWXK-1631092637324)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020102522233.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-afhl879Z-1631092637326)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020104313961.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-izw58S29-1631092637330)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020104653366.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-swAGiSS6-1631092637332)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020104919464.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TjReVSlr-1631092637333)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020104953351.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RftnInNL-1631092637335)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020105507217.png)]

函数传参作业

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g0yGqgea-1631092637336)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201021163013615.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QGzeBEgF-1631092637340)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201021163202494.png)]

2、代码题

//1、函数判断素数
//要求:实现一个函数,判断一个数是不是素数。利用上面实现的函数打印100-200之间的素数

//2、判断闰年
//要求:实现函数判断year是不是闰年
//3、交换两个整数
//要求:实现一个函数来交换两个整数的内容

//4、乘法口诀表
//要求:实现一个函数,打印惩罚口诀表,口诀表的行数和列数自己指定
//如:输入9,输出9*9的口诀表。输入12,输出12*12的乘法口诀表
void print_table(int m)
{
	int i = 0;
	for (i = 1; i <= m; i++)  //行
	{
		int j = 0;
		for (j = 1; j <= i; j++)//列
		{
			printf("%d*%d=%-3d  ", i, j, i * j);
		}
		printf("\n");
	}
}
int main()
{
	int m= 0;
	scanf("%d", &m);
	print_table(m);
	return 0;
}

//5、打印一个数的每一位
//要求:递归方式实现打印一个整数的每一位

//6、求阶乘
//要求:递归和非递归分别实现求n的阶乘(不考虑溢出的问题)

//7、strlen的模拟(递归实现)
//要求:递归和非递归分别实现strlen

//8、字符串逆序(递归实现)
//内容:编写一个函数reverse_string(char* string) (递归实现)
//实现:将参数字符串中的字符反向排序
//要求:不能使用C库函数中的字符串操作函数  【把abcdef 编程fedcba】

/**********非递归**************/
#include <string.h>
int my_strlen(char* str)
{
	int count = 0;
	while (*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}
int reverse_string(char arr[]) 
{
	int left = 0;
	int right = my_strlen(arr) - 1;

	while (left < right)
	{
		int temp = arr[left];
		arr[left] = arr[right];
		arr[right] = temp;
		left++;
		right--;
	}
}
int main()
{
	char arr[] = "abcdef";  //fedcba
	reverse_string(arr);
	printf("%s\n", arr);
	return 0;
}

/***************递归**********************/
int my_strlen(char* str)
{
	int count = 0;
	while (*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}
void reverse_string(char arr[])
{
	char temp = arr[0];
	int len = my_strlen(arr);
	arr[0] = arr[len - 1];
	arr[len - 1] = '\0';
	if (my_strlen(arr + 1) >= 2)
		reverse_string(arr + 1);
	arr[len - 1] = temp;
}
int main()
{
	char arr[] = "abcdef";
	reverse_string(arr);
	printf("%s\n", arr);
	return 0;
}
//9、计算一个数的每位之和(递归实现)
//内容:写一个递归函数DigitSum(n),输入一个非负整数,返回组成它的数字之和
//如:利用DigitSum(1729),则应该返回1+7+2+9,它的和是19
//输入:1729,返回19

/*
DigitSum(1729)
DigitSum(172) + 1729%10
DigitSum(17) + 172%10 + 1729%10
DigitSum(1) + 17%10 + 172%10 + 1729%10
1 + 7 + 2 + 9
*/
int DigitSum(unsigned int num)
{
	if (num > 9)
	{
		return DigitSum(num / 10) + num % 10;
	}
	else
		return num;
}
int main()
{
	unsigned int num = 0;
	scanf("%d", &num); //1729
	int ret = DigitSum(num);
	printf("ret = %d\n", ret);
	return 0;
}
//10、递归实现n的k次方
//内容:编写一个函数实现n的k次方,用递归
double Pow(int n, int k)
{
	// n^k = n * n^(k-1)
	if (k < 0)
		return (1.0 / (Pow(n, -k)));  //若是负数,用1除,有小数  double
	else if (k == 0)
		return 1;
	else
		return n * Pow(n, k - 1);
}
int main()
{
	int n = 0;
	int k = 0;
	scanf("%d%d", &n, &k);
	double ret = Pow(n, k);
	printf("ret = %lf\n", ret);  //lf
	return 0;
}

//11、计算斐波那锲数列
//内容:递归和非递归分别实现求第n个斐波那锲数列

四、数组

1、一维数组的创建

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

#include <stdio.h>
int main()
{
    int arr[10];  //定义一个存放10个整数数字的数组
    char ch[20];  //定义一个存放20个字符串的数组
    char arr2[];
    //char ch[n]; 不能放n哟
    return 0;
}

1.1、数组初始化

//数组初始化
#include <stdio.h>
int main()
{
    //int a = 1;   存储1-100
    //int b = 2;
    //int c = 3;
    //int d = 4;
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};  //赋值
	char arr2[20] = {'a','b'}; //放了两个字符  a b 
    char arr3[20] = "ab"; //放了三个,a b \0
    return 0;
}

1.2sizeof 和 strlen 对比

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fEyeqyuK-1631092637342)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201015234107951.png)]

//sizeof 和 strlen 对比
int main()
{
	char arr[] = "abcdef";  // a b c d e f \0
	printf("%d\n",sizeof(arr));  //7
    //sizeof计算 arr所占控件的大小  7个元素-char 7*1 = 7 
	printf("%d\n",strlen(arr));  //6
    //strlen 求字符串的长度 --- '\0'之前的字符个数   6
}
//sizeof 和 strlen 对比
//strlen 只能针对字符串求长度 ---'\0'之前的字符长度
//sizeof 计算变量、数组、类型的大小 --单位字节
//sizeof 和 strlen 对比
int main()
{
	char arr1[] = "abc";  // a b c \0
    char arr2[] = {'a','b','c'};  // a b c 
        
	printf("%d\n",sizeof(arr1));  //4  
	printf("%d\n",sizeof(arr2));  //3
    printf("%d\n",strlen(arr1));  //3
	printf("%d\n",strlen(arr2));  //随机值
       
     return 0;
     // {}  没有 \0
     // ""    有 \0
    //sizeof 计算变量、数组、类型的大小 --单位字节       (会管 \0)
    //strlen 只能针对字符串求长度 ---'\0'之前的字符长度  (不管 \0)
   
}

2、一维数组的使用

用 arr[i]

int main()
{
	char arr1[] = "abc";  
        
	int i = 0;
    int len = strlen(arr);
    for(i=0; i<len; i++)
    {
		printf("%c ", arr[i]);
    }
    return 0;             
}

3、数组作为函数传参(冒泡)

​ 往往我们在写代码的时候,会将数组作为参数传个函数,比如:我要实现一个冒泡排序(这里要讲算法思想)函数将一个整形数组排序 -------- 效率低

#include <stdio.h>

void bubble_sort(int arr[], int sz)  //传过来的是地址。只是交换顺序,不需要返回值   实际是int*
{
	//确定冒泡排序的趟数
	int i = 0;
	for (i = 0; i < sz - 1; i++)  //10个数交换9趟, sz - 1
	{
		//每一趟冒泡排序
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)   //sz - 1 - i
		{
			if (arr[j] > arr[j + 1])
			{
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
}
int main()
{
	int i = 0;
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz);   //数组传过去的是首元素的地址
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

/*          改进   flag      */

void bubble_sort(int arr[], int sz)  //传过来的是地址。只是交换顺序,不需要返回值
{
	//确定冒泡排序的趟数
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
        int flag = 1;  //假设这一趟要排序的数据已经有序
		//每一趟冒泡排序
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)   //sz - 1 - i
		{
			if (arr[j] > arr[j + 1])
			{
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
                 flag = 0;   //本趟排序的数据其实不完全有序
                //当无序的时候,会一直进入for循环,flag会置为0
			}
		}
        if (flag == 1)  //说明已经是有序了,退出去
        {
            break;
        }
	}
}
int main()
{
	//sizeof(数组名)  --数组名表示整个数组, sizeof(数组名) 计算的是整个数组的大小,单位字节
	//&数组名, 数组名代表整个数组, &数组名,取出的是整个数组的地址
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	printf("%p\n", arr);
	printf("%p\n", arr[0]);
	printf("%p\n", &arr);
    //三个结果看着相同,其实前两个都是首元素的地址(是一样的),但是第三个是整个数组的地址
	return 0;
}

4、数组的应用实例1:三子棋

game.h
game.c
test.c
//game.h
#pragma once
#include <stdio.h>
#define COL 3   //行
#define ROW 3   //列

//初始化棋盘 ----- InitBoard(board, ROW, COL)
//函数声明 在game.h中 ---- 实现在 game.c中  ---- 调用在test.c中
void InitBoard(char board[ROW][COL], int row, int col); //把数组传过去、这个数组有几行几列

void DisplayBoard(char board[ROW][COL], int row, int col);  //打印函数


//game.c
#define _CRT_SECURE_NO_WARNINGS 1 
#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] = " ";  //初始化棋盘  -- 打印空格
		}
	}
}

/* 为了打印下面的九宫格
		   |   |
		---|---|---        分割行
		   |   |
		---|---|---
		   |   |
*/
void DisplayBoard(char board[ROW][COL], int row, int col)
{
	int i = 0;
	for (i = 0; i < row; i++)
	{
		//1、打印一行数据
		int j = 0;
		//printf(" %c | %c | %c \n",board[i][0], board[i][1], board[i][2]);
		for (j = 0; j < col; j++)
		{
			printf(" %c ", board[i][j]);
			if (j < col - 1)
				printf("|");
		}
		printf("\n");   //一次循环后换行
		//2、打印分割行
		if (i < row - 1)   //去掉最后一行的分割行
		{
			//printf("---|---|---\n");
			for (j = 0; j < col; j++)
			{
				printf("---");
				if (j < col - 1)
					printf("|");
			}
			printf("\n");
		}
	}
}
//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 };

	//初始化棋盘 --- 最开始打印空
	//函数声明 在game.h中 ---- 实现在 game.c中  ---- 调用在test.c中
	InitBoard(board,ROW,COL); //把数组传过去、这个数组有几行几列

	//打印初始化的棋盘 -- 用函数 (打印的是个数组)
	DisplayBoard(board,ROW,COL);
	/* 为了打印下面的九宫格
			   |   |
			---|---|---        分割行
			   |   |
			---|---|---
			   |   |
	*/
}
void test()
{
	int input = 0;
	//menu();
	//printf("请输入:");  在循环里
	//scanf("%d", &input);
	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);  //while 后有分号
}
int main()
{
	test();
	return 0;
}


16分钟

5、数组的应用实例2:扫雷游戏

数组作业

1、选择题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZJEUlluf-1631092637344)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020105743439.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z0wnWXLP-1631092637346)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020105948362.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1U8BRNIG-1631092637348)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020110210705.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sAkw72pa-1631092637349)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020110427598.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hw1ltvtc-1631092637351)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020110815321.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vz3O3G2m-1631092637353)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020111808556.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-haeNiO7n-1631092637357)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020112251199.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2dArHuCf-1631092637359)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020112433793.png)]

2、编程题

//1、实现对一个整型的冒泡排序
#include <stdio.h>

void bubble_sort(int arr[], int sz)  //传过来的是地址。只是交换顺序,不需要返回值
{
	//确定冒泡排序的趟数
	int i = 0;
	for (i = 0; i < sz - 1; i++)  //10个数交换9趟  sz - 1趟
	{
        int flag = 1;  //假设这一趟要排序的数据已经有序
		//每一趟冒泡排序
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)   //sz - 1 - i
		{
			if (arr[j] > arr[j + 1])
			{
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
                 flag = 0;   //本趟排序的数据其实不完全有序
                //当无序的时候,会一直进入for循环,flag会置为0
			}
		}
        if (flag == 1)  //说明已经是有序了,退出去
        {
            break;
        }
	}
}
int main()
{
	int i = 0;
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz);   //数组传过去的是首元素的地址
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}
//2、创建一个整型数组,完成对数组的操作
/*
	1、实现函数init()初始化数组全为0
	2、实现print()打印数组每个元素
	3、实现reverse()函数完成数组的反转
*/
void Init(int arr[], int sz) //接收数组是 int arr[]
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		arr[i] = 0;
	}
}
void Print(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ",arr[i]);
	}
	printf("\n");
}
void Reverse(int arr[], int sz)
{
	int left = 0;  //第一个和最后一个交换 第二个和倒数第二个交换 ......
	int right = sz - 1;

	while (left < right)  //while循环 条件 左<右  <= 也行
	{
		int temp = arr[left];
		arr[left] = arr[right];
		arr[right] = temp;
		left++;
		right--;
	}
}
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);

	Print(arr, sz);
	Reverse(arr,sz);
	Print(arr,sz);
	Init(arr, sz);
	Print(arr, sz);
	return 0;
}
//3、数组交换----将数组A中的内容和数组B中的内容交换。(数组一样大)
int main()
{
	int arr1[] = { 1,3,5,7,9 };
	int arr2[] = { 2,4,6,8,10 };
	int temp = 0;
	int sz = sizeof(arr1) / sizeof(arr1[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		temp = arr1[i];
		arr1[i] = arr2[i];
		arr2[i] = temp;
	}
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr1[i]);
	}
	printf("\n");
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr2[i]);
	}
	return 0;
}

//4、三子棋
//5、扫雷

五、C语言操作符详解

1、算术操作符

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

double a = 5 / 2.0; // 2.500000
printf("a = %lf\n",a);  //2.500000

2、移位操作符 << >>

 << 左移(左移一位有乘2的效果)      >> 右移(右移一位有除2的效果)    
 
左移操作符: 补码左边丢弃,右边补0
右移操作符: 补码右边丢弃,左边补符号位  

整数在内存中存储的时候:存储的是二进制的补码 (移位移的都是补码)  (正整数的原码、反码、补码是一样的)

把-1用二进制写出32位的原码(负数开头是1 正数开头0)
原码取反(符号位不变)得到反码 0->1 1->0 
反码+1得补码
对补码进行移动
#include <stdio.h>

int main()
{
	int a = 16;
	int b = a >> 1;   // 8 右移除2   高位少一个
	int c = a << 1;   // 32 左移乘2  高位多一个
	printf("b = %d\n", b);
	printf("c = %d\n", c);
	return 0;
}
//右移 16的二进制(32位)为---10000  右移-1000  左边补0(正数) --- 2^3=8   右移除2  高位少一个
//左移 16的二进制(32位)为---10000  左移-10000 右边补0       --- 2^4=32  左移乘2  高位多一个

3、位操作 & | ^

& 按位与(全真为真,两个都是1才为1,有一个0就是0) 
| 按位或(有一个真就为真,有一个1就是1)  
^ 按位异或(相同为0,相异为1)   
int main()
{
	int a = 3;   //011
   	int b = 5;   //101
    int c = a&b;  //与and(全真为真,两个都是1才为1,有一个0就是0)  001 --->1 ---0*2^2+0*2^1+1*2^0
    int d = a|b; // |或(有一个真就为真,有一个1就是1)  111 --->7--  1*2^2+1*2^1+1*2^0---4+2+1
    int e = a^b; // ^(相同为0,相异为1)  110-----> 6----4+2
}

1、交换变量的值(^ 按位异或)

//写代码交换两个int变量的值,不能使用第三个变量,即a=3,b=5,交换后,b=3,a=5
//方法: 1、临时变量 2、加减法(可能会溢出)  3、按位异或(因为它不会进位)
//^(相同为0,相异为1) 
#include <stdio.h>
int main()
{
	//^(相同为0,相异为1) 
	int a = 1;   // 001
	int b = 2;   // 010
	a = a ^ b;   // 011
	b = a ^ b;   // 001  1
	a = a ^ b;   // 010  2
	printf("a = %d\n", a);
	printf("b = %d\n", b);
	return 0;
}

2、求二进制中1的个数

//统计二进制中1的个数
//要求:写一个函数返回参数二进制中1的个数
//如:15 0000 1111   4个1

//法一: n & (n - 1)
int count_bit_one(int n)  
{
	int count = 0;
	while (n)
	{
		n = n & (n - 1);
		count++;
	}
	return count;
}
int main()
{
	int a = 0;
	scanf("%d", &a);
	//写一个函数求a的二进制补码表示里有几个1
	//这个函数,把a传过去,返回有几个1
	int count = count_bit_one(a);
	printf("有%d个1\n", count);
	return 0;
}

//2、法二 unsigned int a + %2 /2
int count_bit_one(unsigned int a)   //把 int a 改为 unsigned int a 就可以统计负数了
{
    int count = 0;
    while (num)
	{
		if (num % 2 == 1)
		{
			count++;
		}
        num = num / 2;
	}
    return count;
}

//法三、 ((a >> 1)&1)  右移&1
int count_bit_one(int a)  
{
    int count = 0;
    int i = 0;
    for(i=0; i<32; i++)
    {
        if( 1 ==    ((a >> 1)&1)     )
        {
            count++;
        }
    }
    return count;
}

4、赋值操作符

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

5、单目操作符~*&sizeof

!           逻辑反操作 0假 1真
-           负值
+           正值
sizeof      操作数的类型长度(变量所占内存空间的大小,以字节为单位)
~           对一个数的二进制按位取反(0变1,1变0)
--          前置、后置--
++a         a在后,先++,再使用a的值   前置
a++		   a在前,先使用a的值,再++
&           取地址
*           间接访问操作符(解引用操作符)
(类型)       强制类型转换比特

1、sizeof () 操作数的类型长度(以字节为单位)

 sizeof(char);   //1个字节  8个比特位    字符串
 sizeof(short);  //2个字节  16个比特位   2^16   0-65535
 sizeof(int);    //4个字节  32个比特位   2^32   0-4294967295
//所以当定义age时,就可以用 short age = 20;  若用int貌似太大了,浪费
 sizeof(long);  //4/8个字节
 sizeof(long long);  //8个字节
 sizeof(float);  //4个字节
 sizeof(double);  //8个字节
int a = 10;
int* p = &a;  //&取地址  int*指针类型
*p = 20;      //*解引用操作符
//sizeof 和 strlen 对比
//strlen 只能针对字符串求长度 ---'\0'之前的字符长度
//sizeof 计算变量、数组、类型的大小 --单位字节
//sizeof()   操作数的类型长度(变量所占内存空间的大小,以字节为单位)
int main()
{
    int a = 10; // 一个变量占4个字节
	int arr[10] = {0}; //10个整型元素的数组  10 * sizeof(int) = 40
    //个数 = 数组总大小 / 每个元素的大小
    int sz = 0;  
    int arr1[] = {1,2,3,4,5,6};
    sz = sizeof(arr) / sizeof(arr[0]);  //10
    printf("%d\n",sizeof(arr)); //40
    printf("%d\n",sizeof(a));   //4
    printf("%d\n",sizeof(int)); //4
    printf("%d\n",sizeof(arr1)); //4*6=24
	return 0;
}

坑的题

short s = 0;   //short只能存2个字节
int a = 10;
printf("%d\n",sizeof(s = a +5));  //2    不论s是几,他里面只能存2个字节
printf("%d\n",s);         //0    sizeof里面并不真正的参与运算
printf("%d\n",sizeof(s))  //2 short 占两个字节

2、按位取反(0变1,1变0)~

//~   对一个数的二进制按位取反(0变1,1变0) 如一个二进制1010,按位取反0101
int main()
{
    int a = 0;   //0,4个字节,32个bit位 00000000000000000000000000000000   最高位是0表示正数
    int b = ~a;  //0取反为1,4个字节,32个bit位  11111111111111111111111111111111  最高位是1表示负数
    printf("%d\n",b); //-1
    //源码、反码、补码
    //正数的源码、反码、补码是相同的
    //负数在内存中存储的时候,存储的是二进制的补码
    //b是有符号的整型    11111111111111111111111111111111  最高位是1表示负数   说明是补码
    //我们使用的,打印的b是这个数的原码
    //原码符号位不变,其他位按位取反得到反码,反码加1得到补码
    //所以补码-1得到反码  11111111111111111111111111111110
    //反码符号位不变,其他位按位取反得到原码   10000000000000000000000000000001  所以-1
    return 0;
}
2.1、~的应用

1、要求把二进制 1011 改成 1111

//第三位的0 只要 | 上1 就可以, 1左移两位
//00000000000000000000000000001011
//00000000000000000000000000000100
//1<<2   |
//00000000000000000000000000001111
int a = 11; 
a = a | (1 << 2); // 15 --- 1111

2、要求把二进制 1111 改成 1011

//把第三位的1改成0 , 只要第三位的1 & 上0,,1左移两位取反
//00000000000000000000000000001111
//11111111111111111111111111111011
//1<<2    ~  &
//00000000000000000000000000000100    ~
//00000000000000000000000000001011
int a = 11; 
a = a & (~(1<<2)); // 11 --- 1011

3、a++ a在前先使用a,再++

int main()
{
	int a = 10;
    int b = a++;  //先使用a的值,b为10. a再++,a为11
    printf("a= %d b=%d",a,b); //11  10.
    return 0;
}

4、() 强制类型转换

int a = (int)3.14;
4.1、题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HP61c30y-1631092637364)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201018140237613.png)]

6、关系操作符

>
>=
<
<=
!=      用于测试“不相等”
==      用于测试“相等    数字放左边

7、逻辑操作符 && ||

只关注本身是真还是假,不是二进制
&&          逻辑与  两个都真为真
||          逻辑或  有一个真为真
int i = 0; a = 0; b = 2; c = 3; d = 4;
i = a++ && ++b && d++;
//a++ 先使用a在++  所以用a=0 ---假    &&后面的就不算了
//所以 a=1   b c d 还是原来的值,因为&&后面的就没运算
int i = 0; a = 0; b = 2; c = 3; d = 4;
i = a++ || ++b || d++;
//有一个真就真, 后面就不算了  第一个||算了, 第二个||就不算了
//a=1 b=3 c=3 d=4

8、条件操作符

exp1 ? exp2 : exp3

9、逗号表达式

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

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

[]  ()  .   ->
struct Book   //创建结构体类型
{
    char name[20];
    short price;
};
int main()
{
    struct Book b1 = { "C语言程序设计",55 };
    b1.price = 100;
    //printf("书名 = %s\n价格 = %d\n", b1.name, b1.price);  // %s  书名 = C语言程序设计  价格 = 100
    
	struct Book* pb = &b1;  //把b1的地址拿出来放在pb里,pb就是一个指针变量,指针类型是struct Book*
    printf("%s\n",pb->name);  //C语言程序设计
    return 0;
}

11、表达式求值

1、整型提升

1、整型提升(类型<整型的)

【整型提升看这个数的类型,按符号位提升】

char 或者 short 参与运算时,应先转换为 int

C的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升

//整型提升
int main()
{
	char a = 3;
	char b = 127;
	char c = a + b;
	printf("%d\n", c);  //-126
	return 0;
}
/*
char只能放一个字节,short 2个,int 4个,float 4个, double 8个, long 4/8个, long long 8个
  a  3的二进制:00000000 00000000 00000000 00000011 但a是char类型,只有一个字节,所以得截断,00000011
  b  同理127: 00000000 00000000 00000000 01111111 截断得 01111111

a和b如何相加
	00000011 + 01111111 (表达式中的字符和短整型操作数在使用之前被转换为普通整型) ---整型提升
整型提升(正数左边补0,负数补1)
	 00000000 00000000 00000000 00000011
+    00000000 00000000 00000000 01111111    (逢二进一,借一当二)
=    00000000 00000000 00000000 10000010   这个是c ,但是c是char,一个字节,得截断,所以:10000010

要打印的c是%d,所以得对c进行整型提升,  10000010 首字符1说明是负数  按高位进行补充(左边补1) (首字符是0就补0)
c  11111111 11111111 11111111 10000010  ---补码
得求原码
反码(补码-1得反码):                11111111 11111111 11111111 10000001
原码(符号位不变,其余取反得原码)     10000000 00000000 00000000 01111110
这个数是负的  -(2^6+2^5+2^4+2^3+2^2+2^1) = -126
*/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xi0tYmeN-1631092637367)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201018214029918.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DY2BYmCL-1631092637370)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201018214715597.png)]

!逻辑反操作,不是运算符,没有变量提升

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MOIgGyV9-1631092637375)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201018214932258.png)]

2、算术转换(类型>=整型)

任意两个类型运算,先转换为上面的类型

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

操作符作业

1、选择题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8wylBqvI-1631092637380)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020163944700.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VR9SYUGY-1631092637381)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020164318487.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uX0uN8zP-1631092637384)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020164424163.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2rK6xYvU-1631092637386)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020164854873.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IluAB2ce-1631092637388)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020170448456.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dJUkSruL-1631092637390)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020171140550.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zkgdQdbk-1631092637391)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020172253501.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8iUdkzKj-1631092637392)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020232854994.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gt1Z5CLZ-1631092637395)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201020233013963.png)]

2、程序题

//统计二进制中1的个数
//要求:写一个函数返回参数二进制中1的个数
//如:15 0000 1111   4个1

//法一: n & (n - 1)
int count_bit_one(int n)  
{
	int count = 0;
	while (n)
	{
		n = n & (n - 1);
		count++;
	}
	return count;
}
int main()
{
	int a = 0;
	scanf("%d", &a);
	//写一个函数求a的二进制补码表示里有几个1
	//这个函数,把a传过去,返回有几个1
	int count = count_bit_one(a);
	printf("有%d个1\n", count);
	return 0;
}

//2、法二 unsigned int a + %2 /2
int count_bit_one(unsigned int a)   //把 int a 改为 unsigned int a 就可以统计负数了
{
    int count = 0;
    while (num)
	{
		if (num % 2 == 1)
		{
			count++;
		}
        num = num / 2;
	}
    return count;
}

//法三、 ((a >> 1)&1)  右移&1
int count_bit_one(int a)  
{
    int count = 0;
    int i = 0;
    for(i=0; i<32; i++)
    {
        if( 1 ==    ((a >> 1)&1)     )
        {
            count++;
        }
    }
    return count;
}
//2、求二进制中不同位的个数
/*编程实现:两个int (32位) 整数 m 和 n 的二进制表达中,有多少个位(bit)不同?
 *输入例子: 1998 2299
 *输出例子:  7
*/
//法一、按位异或^ + n & (n - 1)
int get_diff_bit(int m, int n)
{
	int temp = m ^ n;
	int count = 0;
	while (temp)
	{
		temp = temp & (temp - 1);
		count++;
	}
	return count;
}

int main()
{
	int m = 0;
	int n = 0;
	scanf("%d%d", &m, &n);
	int count = get_diff_bit(m, n);
	printf("%d\n", count);
	return 0;
}

//3、交换两个变量(不创建临时变量)
int main()
{
	int x = 2;
	int y = 3;
	printf("交换前,x=%d y=%d\n", x, y);
	x = x ^ y;
	y = x ^ y;
	x = x ^ y;
	printf("交换后,x=%d y=%d\n", x, y);
	return 0;
}
//4、打印二进制的奇数位和偶数位
//获取一个整数二进制序列中所有的偶数位和奇数位,分别打印出二进制序列
//  (m >> i) & 1
void print(int m)
{
	int i = 0;
	printf("奇数位:\n");
	for (i = 30; i >= 0; i -= 2)
	{
		printf("%d ", (m >> i) & 1);
	}
	printf("\n");
	printf("偶数位:\n");
	for (i = 31; i >= 1; i -= 2)
	{
		printf("%d ", (m >> i) & 1);
	}
	printf("\n");
}
int main()
{
	int m = 0;
	scanf("%d", &m);
	print(m);
	return 0;
}
//5、用指针打印数组内容
//要求:写一个函数打印arr数组的内容,不能使用数组下标,使用指针。arr是一个整型一维数组
// *(p+i)
void print(int* p, int sz)  //数组名arr传参,传过来的是首元素的地址---整型的地址,放在整型指针里 int*
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
}
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print(arr, sz);
	return 0;
}

六、指针

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

int main()
{
	int a = 10;
	int* pa = &a;   // int *pa = &a; 也是一样的
	*pa = 20;
	printf("%d\n", *pa);  //20  打印a的值也是一样的
	return 0;
}
1. 指针是什么        指针就是个变量,指针里面存放的是地址,所以说指针就是个地址
2. 指针和指针类型     p就是指针变量,类型是int* 
3. 野指针
4. 指针运算
5. 指针和数组
6. 二级指针
7. 指针数组

1、指针是什么

指针就是个变量,里面存放的是地址,所以说指针就是个地址

1、内存

//指针大小
一个内存是一个字节byte

32位的机器上,一个地址是4个字节。在64位平台是8个字节(64个bit位)

指针的大小在32位的电脑是4个字节(4*8=32个bit位),在64位平台是8个字节(8*8=64个bit位)

一个内存是一个字节byte

int main()
{
    int a = 10; //4个字节
    printf("%p\n",&a); //&a 取a的地址   %p打印地址   0094FDA4十六进制
	return 0;
}

2、指针变量

//整型
int main()
{
    int a = 10;
        //--------------把a的地址存起来----------
    int* p = &a;  //&a 取a的地址   把a的地址存起来
                //有一种变量是用来存放地址的-----指针变量 p是指针变量  它的类型是 int*
                //-------------把a的地址拿出来------------------
                //*p;  //* 解引用操作符  *p 对p进行解引用操作,找他所指向的哪个对象a
    *p = 20; //把p指向的那个对象a 改成20
    printf("a = %d\n",a);  //a = 20
    return 0;
}

小结

int a = 10;
int* p = &a;
*p = 20;
在内存中创建了一个变量a  他有一个地址0x0012ff40 里面的值是10
把a的地址存放在一个变量p里,p里面存的是a的地址,这个p就是指针变量,它的类型是 int*
想通过地址找回去,就是用*p, *p就是a , 把10改成20
//字符型
int main()
{
    char ch = "w";
    char* pc= &ch;
    *pc = 'a';
    printf("%c\n",ch);   //a
    return 0;
}
//双精度型
int main()
{
    double d = 3.14;
    double* pd= &d;
    *pd = 5.5;
    printf("%lf\n",d);   //5.500000
    printf("%lf\n",*pd);   //5.500000
    printf("%d\n", sizeof(pd)); //32位平台--4。64位平台--8
    return 0;
}

2、指针和指针类型

指针的大小都是4个字节(32位机器),但指针类型是具有意义的

//指针大小
int main()
{
	char ch = 'w';
	char* p = &ch;
	printf("%d\n", sizeof(p)); //4 说明是32位平台。 
    //在debug -配置管理器里-新建 *64  ---这时指针就变成8个字节了
    //32位平台
    printf("%d\n", sizeof(char*)); //4    char*只能动一个字节
    printf("%d\n", sizeof(short*));//4
    printf("%d\n", sizeof(int*));//4      int* 可以动四个字节
    printf("%d\n", sizeof(double*));//4
	return 0;
}

指针的大小都是4个字节(32位机器),但指针类型是具有意义的

1、指针类型的意义

1 解引用操作的时候,能够访问空间的大小
//指针类型决定了指针进行解引用操作的时候,能够访问空间的大小int* p; *p 能够访问4个字节
   char* p; *p 能够访问1个字节
 double* p; *p 能够访问8个字节
 
 char*只能动一个字节    int* 可以动四个字节
2、指针 + - 整数
int* 往后走4个字节
char* 往后走1个字节
double* 往后走8字节
3、指针意义的价值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6sf8T0L4-1631092637397)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201019105828190.png)]

3、野指针

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

1、野指针成因

1、指针未初始化
#include <stdio.h>
int main()
{       
    int *p;   //局部变量指针未初始化,默认为随机值    
    *p = 20;  //局部的变量指针,就会被初始化为随机值
    return 0;
}
2、指针越界访问

当指针指向的范围超出数组arr的范围时,p就是野指针 人家就10个你13个

#include <stdio.h>
int main()
{    
    int arr[10] = {0};    
    int *p = arr;   //指向数组首元素的地址 
    int i = 0;    
    for(i=0; i<=12; i++)    
    {
        //当指针指向的范围超出数组arr的范围时,p就是野指针   人家就10个你13个
        *(p++) = i;    
        printf("%d ", arr[i]);  //0 1 2 3 4 5 6 7 8 9
    }    
    return 0;
}
3、指针指向的空间释放

局部变量的空间被释放了

int* test()
{
    int a = 10;
    return &a;    //局部变量程序结束后内存就被释放了,你下面更改的20 鬼知道放在哪个内存地址里了
}
int main()
{
    int *p = test();
    *p = 20;
    
    return 0;
}

2、如何规避野指针

1、指针初始化    
    			int a = 10; 
			   int* pa = &a; // 初始化 &a
			// 不知道初始化为啥的时候就NULL   
			 int a = 10;  
			 int* pa = NULL;
2、小心指针越界
3、指针指向空间释放即使置NULL 
    	 int a = 10; 
		int* pa = &a; 
		*pa = 20;  //这个时候指针 pa 已经使用完了,不想维护这个指针了,释放这个指针时,置NULL
		 pa = NULL;  //不用了,释放这个指针时,置NULL
4、指针使用之前检查有效性
    	if(pa != NULL)
        {
            
        }

4、指针运算

1、指针 + - 整数

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	int* p = arr;  //arr 是数组首元素的地址  p里面放的是首元素的地址 , *p 解引用后就是首元素
	for (i = 0; i < sz; i++)  //用指针来访问数组
	{
		printf("%d ", *p);  //1 2 3 4 5 6 7 8 9 10
		p++;   //p是首元素的地址, +1 就是下一个元素的地址, *p 就是下一个元素
        /*      这个 p++ 也可以 ----->  【p = p + 1】;  指针 + - 整数      */
        //printf("%d ", *(p+i));
	}
	return 0;
}

/************************/
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	int* p = arr; 
	for (i = 0; i < 5; i++) 
	{
		printf("%d ", *p);  // 1 3 5 7 9
		p = p + 2;   //指针 + - 整数 
	}
	return 0;
}
/****************************/
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	int* p = &arr[9];  //arr[9] 是数组最后一个元素的地址  p里面放的是最后一个元素的地址 , *p 解引用后就是最后一个元素
	for (i = 0; i < sz; i++) //用指针来访问数组
	{
		printf("%d ", *p);  // 10 9 8 7 6 5 4 3 2 1
		p--;   //p是最后一个元素的地址, -1 就是前面一个元素的地址, *p 就是前面一个元素
	}
	
	return 0;
} 

2、指针 - 指针

指针相减得到的是指针之间的元素个数

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//&arr[9] - &arr[0]; //下标为9的元素地址-下标为0的元素号地址----就是:指针相减    大指针-小指针
	printf("%d", &arr[9] - &arr[0]);  // 9  指针相减得到的是中间的元素个数
	return 0;
}
strlen 求字符串长度的方法3
int my_strlen(char* str)   //传过来字符串的首元素的地址 
{
	char* start = str;
	char* end = str;
	while (*end != '\0')
	{
		end++;
	}
	return end- start;
}
int main()
{
	char arr[] = "bit";  //双引号
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}

3、指针的关系运算(比较大小)

#define N_VALUES 5
float values[N_VALUES];
float *vp
for (vp = &values[0]; vp < &values[N_VALUES];)
{
    *vp++ = 0;
}
for(vp = &values[N_VALUES]; vp > &values[0];)
{    
	*--vp = 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0cGLwMQ-1631092637399)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201019231818876.png)]

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

5、指针和数组

1、数组名是什么

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

/*数组名是首元素地址,但是有两个例外
	*1、&arr - &数组名 -这个时候的数组名不是首元素的地址,数组名表示整个数组 - &数组名 取出的是整个数组的地址
	* 2、sizeof(arr) - sizeof(数组名) - 数组名表示整个数组, sizeof(数组名)计算的是整个数组的大小
 */
int arr[10] = { 0 };
printf("%p\n", arr);  //数组名是首元素地址
printf("%p\n", &arr[0]); //arr[0]是首元素  &arr[0]就是首元素的地址
int arr[10] = { 0 };

printf("%p\n", arr);  //数组名是首元素地址
printf("%p\n", arr+1);  //地址+4

printf("%p\n", &arr[0]);  //&arr[0]就是首元素的地址
printf("%p\n", &arr[0]+1);  //地址+4

printf("%p\n", &arr);   //整个元素的地址
printf("%p\n", &arr+1); // //地址+40    数组的地址 + 1 , 要跳过一个数组 4*10

2、数组可以通过指针来访问 *(p + i)

int main()
{
	int arr[10] = { 0 };
	int* p = arr;//数组名-首元素的地址 p-首元素的地址
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%p ====== %p\n", p + i, &arr[i]);  //p-首元素的地址 p+i 下一个元素的地址。&arr[i]取出第i													个元素的地址
	}
	return 0;
}

这样就可以用指针来访问数组了

int main()
{
	int arr[10] = { 0 };
	int* p = arr;//数组名-首元素的地址 p-首元素的地址
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;  //把i依次赋给数组
	}
	for (i = 0; i < 10; i++)
	{
		printf("%p ", *(p + i));
	}
	return 0;
}

3、二级指针

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

int main()
{
	int a =  10 ;
	int* pa = &a; // pa就是一级指针变量 int* 就是一级指针类型

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

	int* * ppa = &pa; //取一级指针变量pa的地址,放在ppa(二级指针)里, 二级指针变量的类型是 int**
	**ppa = 20;
	printf("%d",**ppa);// 20  ppa就是pa的地址,*ppa就是pa *(*ppa)就是a
	return 0;
}

4、指针数组

指针数组 - 是数组 --数组里放的是地址

int main()
{
	//指针数组 - 是数组  --- 数组:存放同一类型元素
	//数组指针 - 是指针

	int a = 10;
	int b = 20;
	int c = 30;
	//int* pa = &a;
	//int* pb = &b;
	//int* pc = &c;
	//整型数组 - 存放整型
	//字符数组 - 存放字符
	//指针数组 - 存放指针
	int* arr2[3] = { &a,&b,&c }; //指针数组 放的每一个元素的地址  arr[i]就是每一个元素的地址
	
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", *(arr2[i]));  //10 20 30
	}
	return 0;
}

6、作业

1、单选题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0zcN3Mg1-1631092637401)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201118143814507.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8OxcPtdl-1631092637405)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201118144002677.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JMbJ7KA6-1631092637407)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201118144140319.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uCI3Vu1w-1631092637410)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201118144319934.png)]

2、编程题

1、题目:字符串逆序

内容:写一个函数,可以逆序一个字符串的内容。【牛客网在线OJ链接】

#include <stdio.h>
#include <string.h>
#include <assert.h>
void reverse(char* str) //数组名传过来是首元素地址,地址用指针接收
{
    assert(str); //为保证指针的有效性,用assert断言一下  刷题的时候assert慎用
	int len = strlen(str);
	char* left = str;   //str是数组名,首元素地址,用指针接收
	char* right = str + len - 1; //str+0是首元素,+1是第二个元素,+len就已经野指针了
	while (left < right)
	{
		char temp = *left; //因为left是地址  temp存字符用char
		*left = *right;
		*right = temp;
		left++;
		right--;
	}
}
int main()
{
	char arr[256] = { 0 };
	printf("请输入一段字符串:\n");
	//scanf("%s", arr);  //遇到空格,就不会逆序空格后的字符了 用gets
    gets(arr); //读取一行
	reverse(arr); //不用sz,因为可以用strlen
	printf("%s ", arr);
	return 0;
}

自己的错误

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J38mxwT5-1631092637412)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201118155258846.png)]

#include <stdio.h>
#include <string.h>
void reverse(char arr[])
{
	int left = 0;
	int right = strlen(arr) - 1;  //这里得-1
	while (left < right)
	{
		int temp = arr[left];
		arr[left] = arr[right];
		arr[right] = temp;
		left++;
		right--;
	}
}
int main()
{
	char arr[] = "abcdef";
	reverse(arr); 
	printf("%s ", arr);
	return 0;
}
2、题目:计算求和

内容:求 Sn = a + aa +aaa +aaaa + aaaaa 的前5项之和,其中a是一个数字

例如; 2+22+222+2222+22222

int main()
{
	int a = 0;
	int n = 0;
	scanf("%d%d", &a, &n);
	int i = 0;
	int sum = 0;
	int ret = 0;
	for (i = 0; i < n; i++)
	{
		ret = ret * 10 + a;  //2 22 222 2222 是怎么来的
		sum += ret;          //怎么求和
	}
	printf("%d\n", sum);
	return 0;
}
3、题目:打印水仙花数

内容:求出0-100000之间的所有 “水仙花数” 并输出

“水仙花数” 是指一个n位数,其各位数字的n次方之和正好等于该数本身,如:153 = 1^3 + 5^3 + 3^3, 则153是一个“水仙花数”

例如:153 。 这个次方3是位数、还得知道每一位数、加起来判断

#include <math.h>
#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 0; i < 100000; i++)
	{
		int n = 1;  //一个数至少是一位数
		int temp = i;  //上面i正在循环呢、如果这里继续用i做while循环,可能会出问题。故赋值给temp
		int sum = 0;
		//这里求i是几位数
		while (temp / 10) // 除10是去掉一位
		{
			n++;
			temp /= 10;
		}
		temp = i;
		while (temp)
		{
			sum += pow(temp % 10, n);  //temp%10求最低位
			temp /= 10;
		}
		if (i == sum)
		{
			printf("%d ", i);
		}
	}
	return 0;
}
4、题目:打印菱形

内容:用C语言在屏幕上输出菱形图形

/*
   *
  ***
 *****
*******
 *****
  ***
   *
 */
//菱形一定是奇数行
//输入上半部分的行数 下半部分就-1
//有空格 有*
int main()
{
	int i = 0;
	int line = 0;
	printf("请输入菱形上半部分的行数:");
	scanf("%d", &line);   //4
	//上半部分
	for (i = 0; i < line; i++)  //4行
	{
		int j = 0;
        //打印空格
		for (j = 0; j < line-1-i ; j++) // 3 2 1 0
		{
			printf(" ");
		}
        //打印*
		for (j = 0; j < 2*i+1; j++)  // 1 3 5 7 
		{
			printf("*");
		}
		printf("\n");
	}
	//下半部分
	for (i = 0; i < line-1; i++)  //3行
	{
		int j = 0;
		for (j = 0; j < i+1; j++) // 1 2 3
		{
			//打印空格
			printf(" ");
		}
		for (j = 0; j < 2*(line-1-i)-1; j++)  // 5 3 1
		{
			//打印*
			printf("*");
		}
		printf("\n");
	}
	return 0;
}

七、结构体

.    结构体变量.成员
->   结构体指针->成员
*    解引用操作符
&    取地操作符

1、结构体的声明

//创建书的结构体类型
//struct 结构体关键字 Book 结构体标签 struct Book 结构体类型
struct Book   
{
    //成员变量
    char name[20];  
    short price;    
}b1,b2,b3;  //b1,b2,b3变量列表【全局变量】少用  分号不能少

int main()
{
    //利用结构体类型-创建结构体变量,b1 一本书
    struct Book b1 = {"C语言程序设计",55};  //b1 局部变量
    return 0;
}

1.1、typedef起小名

//创建结构体小名
typedef struct Book   //用typedef给结构体类型struct Book起了个小名,叫Book,下面直接就可以用
{
    //成员变量
    char name[20];  
    short price;   
}Book;  //小名Book【类型】

int main()
{
    struct Book b1 = {"C语言程序设计",55};  //利用结构体类型-创建结构体变量,b1 局部变量 
    Book b2 = {"B语言程序设计",45};  //利用结构体小名-创建结构体变量 b2
    return 0;
}

2、结构的声明、初始化和访问

2.1、声明和初始化

typedef struct Book   //typedef 起小名叫Book
{
    char name[20];  
    short price;   
}Book;  //小名Book【类型】

int main()
{
    struct Book b1 = {"红楼梦",105}; //初始化
    Book b2 = {"C语言程序设计",55}; //初始化
    return 0;
}

2.2、嵌套结构体初始化

struct S
{
	int a;
	char c;
	char arr[20];
	double d;
};

struct T
{
	char ch[10];
	struct S s;  //结构体嵌套 
	char* pc;
};

int main()
{
	//结构体嵌套 {  { }  }
	char arr[] = "hello bit\n";
	struct T t = { "hehe",{100,"w","hello world",3.14},arr }; //这里arr是首元素的地址,也可以给NULL
    
    //访问 .  ->
    printf("%s\n", t.ch); // hehe   %s哟
	printf("%s\n", t.s.arr); //hello world
	return 0;
}

2.3、结构体访问

//访问 .  ->
printf("%s\n", t.ch); // hehe   %s哟
printf("%s\n", t.s.arr); //hello world

3、结构体传参

传地址--------传的是结构体地址,这里用结构体指针来接收,类型是Stu*

typedef struct Stu  //typedef 小名
{
	char name[20];
	short age;
	char tele[12];
	char sex[5];
}Stu; //Stu  类型

void Print1(Stu tmp)  //传的是结构体参数,这里用结构体来接收
{
	printf("name: %s\n", tmp.name);
	printf("short: %d\n", tmp.age);
	printf("tele: %s\n", tmp.tele);
	printf("sex: %s\n", tmp.sex);
}

void Print2(Stu* ps)  //传的是结构体地址,这里用结构体指针来接收,类型是Stu*
{
	printf("name: %s\n", ps->name);
	printf("short: %d\n", ps->age);
	printf("tele: %s\n", ps->tele);
	printf("sex: %s\n", ps->sex);
}

int main()
{
	Stu s = { "李四",40,"12360281456","男" };
	Print1(s);  //结构体传参
	Print2(&s);  //结构体传址
    /*Print2 比 Print1 好
   原因:函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
   结论:结构体传参的时候,要传结构体的地址*/
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9TIgXxv4-1631092637414)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201028112945964.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FmpOpQBv-1631092637417)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201028112930196.png)]

4、作业

单选题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P4hfL4fS-1631092637420)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201120224647861.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QN5NHuoi-1631092637425)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201121145844007.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u0JcMQpL-1631092637428)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201121152055867.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2TqPRlEv-1631092637429)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201121152145530.png)]

编程题:喝汽水

内容:喝汽水,一瓶汽水一元,两个空瓶可以换一瓶汽水。给20元,可以多少汽水(编程实现)


八、调试

0、Debug和Release的区别

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

1、调试案例

//求阶乘和 ---- 错误的代码---用上面的方法调试
int main()
{
	int i = 0;
	int n = 0;
	int ret = 1;
	int sum = 0;
	scanf("%d", &n);
	for (i = 1; i <= n; i++)
	{
		int j = 0;
        //ret = 1;
		for (j = 1; j <= i; j++)
		{
			ret *= j;
		}
		sum += ret;
	}
	printf("%d\n", sum);
	return 0;
}
/*
	先Fn+F10把代码走起来,然后在调试里把监视窗口调出来,继续Fn+F10走代码找错误。。发现i==3时,初始的ret为2,而当计算3的阶乘时,ret应该从1开始,所以得在里层循环里ret=1,在int j = 0;处添加。

	但是调试的时候发现i==3已经过去了,所以可以设置断点,Fn+F9 右键条件 i==3 ,然后Fn+F5 一步走到断点处,【断点要和F5配合使用,不然没效果】
*/
//死循环
#include <stdlib.h>
int main()
{
	int i = 0;
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    //int i = 0; 放在这就不会死循环了,但会报错越界【不会把i放后面】
	for (i = 0; i<= 12; i++)
	{
		printf("hehe\n");
		arr[i] = 0;   //arr[10]=0  arr[11]=0  arr[12]=先12->后0 【越界访问】----- 然后i变为0了--循环
	}
	system("pause");
	return 0;
}
/*
		arr[10]=0  arr[11]=0  arr[12]=先12->后0 【越界访问】----- 然后i变为0了--循环
	说明arr[12] 和 i 是同一个地址,
	   &arr[12] 和 &i 是一样的
*/
因为i和arr都是局部变量,会存放在栈里,而栈的存储是先使用高地址,后使用低地址。所以i和arr的地址位置就确定了(如下图)。 数组是随下标的增长,地址由低到高变化。 循环会把i从0-9的值都改为0, i=10时,越界改为0. i=11时,越界改为0. i=12时,越界改为0.i=0(重新循环)。而此时i的地址和arr[12]的地址是同一个地址【vs2013就是这么布局的】
    
    把i=0放后面,不会死循环,但是会报错越界访问。没这么写的
    
    在不同的版本中,会出现<=11 <=10 就会进入死循环。因为不同的版本,内存布局不同。
    
 在release里就不会死循环

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AXeelccc-1631092637432)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201104233434801.png)]

//在release里就不会死循环
在debug版本里,我们查看&arr和&i, 发现i的地址 大于 arr的地址,(如上图),i在高地址 
但是在release版本中,我们发现arr的地址  大于 i的地址,说明release对内存也进行了优化(把arr放在高地址,就不会死循环了)

2、快捷键

启动调试 Fn+F5
逐过程调试(不看函数内部细节) Fn+F10
逐语句调试(看函数内部细节) Fn+F11
断点(条件)和 Fn+F5 一起用
停止调试 shift+Fn+F5
跳出逐语句 shift+Fn+F11
先断点,条件【断点和F5一起用】,Fn+F10把代码走起来,点击调试 --窗口 --监视--Fn+F10把代码走起来
窗口: 
    监视:手动添加,但不会消失,nice
    自动窗口:把上下文变量自动显示,但会一会出现一会消失
    局部变量:把上下文局部变量自动显示

3、如何写出易于调试的代码

优秀的代码: 
    1. 代码运行正常 
    2. bug很少 
    3. 效率高 
    4. 可读性高 
    5. 可维护性高 
    6. 注释清晰 
    7. 文档齐全 
常见的coding技巧:
	1. 使用assert 
	2. 尽量使用const 
	3. 养成良好的编码风格 
	4. 添加必要的注释 
	5. 避免编码的陷阱

示例:字符串拷贝

assert+ const

//字符串拷贝 --- 10分只给6分
void my_strcpy(char* dest,char* src) //用指针接收数组,dest是地址
{
	while (*src != '\0') //解引用,值不为'\0'
	{
		*dest = *src; //解引用,把值赋过去
		src++;  //地址,指针后移
		dest++;
	}
	*dest = *src; //把\0给arr1
    //把 字符串 和 \0 分开拷贝了,没必要
}
int main()
{
	char arr1[] = "########";
	char arr2[] = "bit";
	my_strcpy(arr1, arr2); //数组名,传过去的是首元素的地址
	printf("%s\n", arr1);
	return 0;
}
//优化****************************************
void my_strcpy(char* dest,char* src)
{
	while (*src != '\0')
	{
		*dest++ = *src++;  //先解引用,然后后置++
	}
	*dest = *src; //把\0给arr1
}
//再优化-----7分
void my_strcpy(char* dest,char* src)
{
	while (*dest++ = *src++) //既解引用拷贝又指针后移了,又 当解引用到\0时,判断为假
	{
		;
	}
}
//再再优化---
   //若arr2传一个空指针就出问题了
//空指针意味着没有指向任何有效的地址,空指针传给src,然后对空指针进行解引用*src,就是访问0地址处的空间。程序就直接挂了
//所以得判断一下,是不是有空指针
void my_strcpy(char* dest,char* src)
{
	if(dest != NULL && src != NULL)
    {
        while (*dest++ = *src++) //
        {
            ;
        }
    }
}
int main()
{
	char arr1[] = "########";
	char arr2[] = "bit";
	my_strcpy(arr1, NULL); //空指针传给src,NULL是0
	printf("%s\n", arr1);
	return 0;
}
//再再再优化---8分
//用if的话直接把出问题给跳过去了,虽然不报错但是把问题给规避过去了,不易发现问题
//所以用assert断言,断言里表达式的结果如果为真就啥也不发生。若表达式的结果为假,assert就报错
#include <assert.h>
void my_strcpy(char* dest,char* src)
{
    assert(dest != NULL); //断言
    assert(src != NULL);
	while (*dest++ = *src++) //即拷贝又指针后移了,
	{
		;
	}
}
//10分  const char*
//const保证*src不可改变,即你拷贝就拷贝,别改变我原来src的值。就可以保证while处不会写错
//返回的是目标字符串,把目的地的地址返回回来,(看到变化),不能返回的dest,因为的dest已经变化了。
//char* ret = dest;---- return ret; ---- char*
//因为返回的是地址,那么main里就可以链式调用了
char* my_strcpy(char* dest,const char* src)
{
    char* ret = dest;
    assert(dest != NULL); 
    assert(src != NULL);
    while (*dest++ = *src++) //把src指向的字符串拷贝到dest指向的空间,包含'\0'
    {
        ;
    }
    return ret; //返回地址
}

int main()
{
	char arr1[] = "########";
	char arr2[] = "bit";
	printf("%s\n", my_strcpy(arr1, arr2));  //返回完事后的起始地址
	return 0;
}

const作用

const修饰指针变量的时候:

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

//p是地址 *p是值

int num = 10;

const int* p = &num; // const 修饰的是 *p,*p(解引用)就是num的值,所以不能通过p来修改num的值。*p 值不可改
//*p = 20; //直接报错
int num = 10;
int n = 30;
int* const p = &num;//const修饰的是p指针变量,p是地址,所以p的地址不可修改。p 地址不可改
//p = &n; //直接报错
const int* const p = &num; //p 和 *p 都不可修改

注意

1. 分析参数的设计(命名,类型),返回值类型的设计
2. 这里讲解野指针,空指针的危害。
3. assert的使用,这里介绍assert的作用 
4. 参数部分 const 的使用,这里讲解const修饰指针的作用 
5. 注释的添加

模拟实现字符长度

#include <assert.h>
int my_strlen(const char *str)  //不想改变字符串的内容,加const
{
	int count = 0;
	assert(str != NULL); //断言,保证指针的有效性
	while (*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	char arr[] = "abcdef";
	int len = my_strlen(arr); //arr是数组名,传过去的是字符首元素的地址
	printf("%d\n", len);
	return 0;
}

编程常见的错误

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

一、数据的存储

1、数据类型详细介绍

在 limits.h中查整型的范围

在float.h中查找浮点数的范围

1、内置类型:
	1、整型家族:
        char //字符数据类型 【char存的是ASCII值(ASCII值也是整数),字符类型分有符号和无符号】
             unsigned char 无符号
             signed char
        short //短整型 
            unsigned short [int]
            signed short [int]
        int //整形 
            unsigned int   0 -> 255
            signed int     -128 -> 127
        long //长整型 
            unsigned long [int]
            signed long [int]
        long long //更长的整形 
     2、浮点数家族:
        float //单精度浮点数 
        double //双精度浮点数 
//C语言有没有字符串类型
2、自定义类型(构造类型):
	> 数组类型 
	> 结构体类型 struct 
	> 枚举类型 enum 
	> 联合类型 union
3、指针类型:
	int *pi; 
	char *pc; 
	float* pf; 
	void* pv;
4、空类型:
    void 表示空类型(无类型)
    通常应用于函数的返回类型、函数的参数、指针类型。
类型的意义: 
	1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。 
	2. 如何看待内存空间的视角

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

计算机中的整型有三种表示方法,即原码、反码和补码。【正数的原反补相同】
三种表示方法均有 符号位 和 数值位 两部分,符号位都是用0表示“正”,用1表示“负”,而数值位三种表示方法各不相。

原码
	直接将整数翻译成二进制就可以。
反码
	将原码的符号位不变,其他位依次按位取反就可以得到了
补码
	反码+1就得到补码。
对于整形来说:数据存放内存中其实存放的是补码。
	在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理; 同时,加法	  和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需 要额外的硬件电路
4个二进制位转换为1个16进制位。
如
    二进制: 00000000 00000000 00000000 0001 0100 【0001就是1,0100就是4】
    16进制:  00 00 00 14
如
    二进制:1111 1111 1111 1111 1111 1111 1111 0110 【1111就是15,1+2+4+8=15】
    16进制:FFFFFFF6

3、大小端字节序介绍及判断

概念

大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位, 保存在内存的高地址中
	小端存储:倒着存进去,倒着读出来。如: 0x11223344 放到内存中是: 44 33 22 11 【44是低位】【前面的内存是				低地址】

为什么会有大小端模式之分呢?

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
例如一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式

判断

请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10)

大端字节序,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端字节序,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中
int main()
{
	int a = 1;//整型4个字节,a的16进制表示为:00000001 。若是小端存储则为:01 000000 
    											 //若是大端存储则为:00 000001
	char* p = (char*)&a; //只拿开头的第一个字节,则用char*,强制类型转换一下
	if (*p == 1)  //*p解引用一下,就是地址里存的值【因为指针只往后走一步】
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

不够完美,应该把代码封装成一个函数:

int check_sys()
{
	int a = 1;
	char* p = (char*)&a;
	if (*p == 1)
		return 1;
	else
		return 0;
}
int main()
{
	int ret = check_sys();
	//返回1,小端。返回0,大端
	if (ret == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

在代码精简一下

int check_sys()
{
	int a = 1;
	char* p = (char*)&a;
    
	//return *p; //*p要么是1,要么是0 
    //在简化
    return *(char*)&a;
}
int main()
{
	int ret = check_sys();
	//返回1,小端。返回0,大端
	if (ret == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

习题:

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);    
	return 0; 
}
// -1 -1 255
image-20201107212337451

2、输出结果

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

有符号的char,首位是符号位 【范围: -128 -> 127】 认为 10000000 是 -128

image-20201107213435112

无符号的char,首位不再是符号位,原反补相同 【范围 0-255】

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QCUK3rVD-1631092637436)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201107214034468.png)]

char范围 这个圈

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HWvF0g1w-1631092637438)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201107215225926.png)]

3、输出结果 【和2不同于: 一个-128 一个128】

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

4、输出结果

//按照补码的形式进行运算,最后格式化成为有符号整数
#include <stdio.h> 
int main() 
{
    int i = -20;
    unsigned int j = 10;
    printf("%d\n", i+j); 
    return 0;
}
image-20201107222724835

5、输出结果

unsigned int i;
for(i = 9; i >= 0; i--)
{
	printf("%u\n",i);
}
//9 8 7 6 5 4 3 2 1 0 死循环

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A8eHmMeC-1631092637442)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201107232945122.png)]

6、输出结果

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

7、输出结果

#include <stdio.h>
unsigned char i = 0;
int main()
{
	for(i = 0;i<=255;i++)
    {
    	printf("hello world\n");
    }
    return 0;
}
//死循环
因为unsigned char 里能存放的数是 0 -> 255 。 当i=256时,会转换为0->255之间的数进行循环。所以for循环恒成立,死循环

无符号数很可能会导致死循环哦

4、浮点型在内存中的存储

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

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

//输出结果
int main()
{
    int n = 9;
    float *pFloat = (float *)&n;
    printf("n的值为:%d\n",n);
    printf("*pFloat的值为:%f\n",*pFloat);
    
    *pFloat = 9.0;
    printf("num的值为:%d\n",n);
    printf("*pFloat的值为:%f\n",*pFloat);
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CIMdGsyu-1631092637443)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210315215909092.png)]

1、浮点数的存储

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CORJxrnc-1631092637445)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210316144918265.png)]

2、浮点数的读取

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zY5VWRs0-1631092637448)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210316144903113.png)]

第一部分

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e42qv948-1631092637450)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210316150716428.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZxLSsGmF-1631092637454)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210316150749772.png)]

练习

5.5的表示形式与存储

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eQ6cj5GE-1631092637457)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210316145449181.png)]

0.5的表示形式与存储

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XOavGiGQ-1631092637464)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210316145459510.png)]

二、指针详解

回顾:

指针大小:
	sizeof(p)  sizeof(arr)  sizeof(char*)  sizeof(double*)  sizeof(int*) 等都是 4 个字节---32位
指针类型意义:解引用时,能访问空间的大小
	int* 4   char* 1  float* 4   double* 8
指针运算:
    指针+-整数: 指针往后走一步的步长
    指针-指针 : 指针之间元素的个数

arr  首元素的地址
arr[0]  首元素的地址
&arr 整个数组元素的地址
sizeof(arr) 整个数组的大小

野指针

1.字符指针char*

常量字符串【把字符串的首地址放在p里】

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JOFqBZff-1631092637470)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210316153619531.png)]

面试题:

str1和str2是两个数组,在内存中开辟2个空间来存储str1和str2数组。

这个是问数组str首元素的地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T6pONoB3-1631092637473)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210316153801933.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-umXhrdIC-1631092637475)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210316153824935.png)]

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

但是里面存放的内容都是一样的,还都是常量字符串,还不能修改。所以hello bit只存一份就可以。

这个是问常量字符串首元素的地址

既然是不可修改的常量字符串,就应该加上 const

2. 指针数组

指针数组是个数组,里面放的是指针变量

int arr[10] = {0}; //整型数组
char ch[5] = {0}; //字符数组
int* parr[10]; //存放整型指针的数组 -简称 指针数组
char* pch[5]//存放字符指针的数组 -简称 指针数组

如:低级用法

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int* parr[3] = { &a, &b, &c }; //指针数组
	int i = 0;
	for (i = 0; i < 3;i++)
	{
		printf("%d ", *(parr[i]);  // 10 20 30
	}
	return 0;
}

如:正常用法 *( parr[ i ] + j )

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));//parr[i]就是数组arr1、arr2、arr3的首元素的地址, +j往后走几步
		}
		printf("\n");
	}
	return 0;
}

3、数组指针

去掉名字剩下的就是他的类型
[ ] 的优先级高于 *
arr 和 p 是一回事 : arr[ i ] == p[ i ] == *(p + i) == *(arr + i)
这里 * ( * ( p + i ) + j ) ) = (*(p + i ) ) [ j ] = p[ i ] [ j ] = * ( p[ i ] + j )
int main()
{
	//int* p = NULL;   //p 是整形指针 - 指向整型的指针 - 可以存放整型的地址
	//char* pc = NULL; //pc是字符指针 - 指向字符的指针 - 可以存放字符的地址
					   //数组指针 - 指向数组的指针 - 存放数组的地址
	//int arr[10] = { 0 };
	//  arr   - 首元素的地址
	//&arr[0] - 首元素的地址
	// &arr   - 数组的地址

	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int (*p)[10] = &arr; //(*p)说明是个指针,(*p)[10]说明这个指针指向数组。 p就是数组指针
	return 0;
}
int (*p)[10] = &arr; //(*p)说明是个指针,(*p)[10]说明这个指针指向数组。 p就是数组指针
int *p[10] //首先p[10]说明是个数组,然后int* 说明数组中存的是地址。 p是指针数组

写出pa的类型

int main()
{
	char* arr[5];
	char* (*pa)[5] = &arr;
}

&arr 拿到的是数组的地址,所以这个地址要存到数组指针里  【数组+指针】
首先,(*pa)说明是个指针。 (*pa)[5]说明指针指向的数组是5个元素的。 
因为每个数组元素的类型是 char*, 所以pa所指向的数组元素类型也是char的。
    故: char* (*pa)[5] = &arr;
***********************************************************************
同理定义pa2的类型
int arr2[10] = {0};
int (*pa2)[10] = &arr2;

&arr2,取出数组的地址,放在数组指针里
首先(*pa2)是个指针,指向10个数组元素 (*pa2)[10], 每个元素类型是 int 。 故 int (*pa2)[10] = &arr2;

数组指针的用法

引子

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int(*pa)[10] = &arr;  //pa数组指针
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", (*pa)[i]);
		//pa是数组的地址, *pa 数组的地址解引用(拿到了整个数组) (*pa)[i] 拿到数组的每个元素
	}
	return 0;
}
比较别扭
		printf("%d ", *(*pa + i));
这里 *pa 就是arr 【&arr 再 *pa】
  *pa + i 指针后移
  *(*pa + i) 解引用
还是比较麻烦 
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int *p = &arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ",*(p+i));
	}
	return 0;
}

数组指针的用法:多级指针

//参数是数组的形式
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");
	}
}
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); //把数组传过去、行数、列数
	//arr 数组名 首元素地址 --- 首元素是谁?  把arr想象成一维数组,就3个元素  
    //所以第一行就是首元素的地址  ---- 看版本2
    
    print2(arr, 3, 5);
	return 0;
}

这里 * ( * ( p + i ) + j ) ) = (*(p + i ) ) [ j ] = p[ i ] [ j ] = * ( p[ i ] + j )

//参数是指针的形式
void print2(int (*p)[5], int x, int y)  //(*p)指针 (*p)[5]数组指针 每个元素都是int型
{
	int i = 0;
	int j = 0;
	for (i = 0; i < x; i++)
	{
		for (j = 0;j < y; j++)
		{
			printf("%d ", *(*(p + i) + j));
            //这里 p+1 就跳过一行, p+i 就跳过i行。  *(p+i) 拿到这一行的数组名  *(p+i)+j 拿到i行j列
            //若是整型 p+1 就跳过一个整型
            //若是数组 p+1 就跳过一个数组
           printf("%d ", (*(p + i))[j]); 
            //p + i 跳过一行数组,*(p+i) 拿到这一行, 在[j]就是 i行j列的元素。 要加括号,因为[]结合性高
            printf("%d ",p[i][j]);
            printf("%d ",*(p[i]+j));
		}
		printf("\n");
	}
}

arr 和 p 是一回事 : arr[ i ] == p[ i ] == *(p + i) == *(arr + i)

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;  //把arr付给了p;那么arr 和 p 是一回事
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
		//printf("%d ", *(arr + i));
		//printf("%d ", arr[i]);
		//printf("%d ", p[i]);
        
		//arr 和 p 是一样的
		// arr[i]  == p[i] == *(p+i) == *(arr+i)
	}
	return 0;
}

学了指针数组和数组指针我们来一起回顾并看看下面代码的意思: [ ] 的优先级高于 *

去掉名字剩下的就是他的类型

int arr[5];  //整形数组 - arr是一个有5个元素的整型数组
int *parr1[10];  //指针数组 - parr1是一个数组,数组有10个元素,每个元素的类型是 int*
int (*parr2)[10]; //数组指针 - paar2是一个指针,该指针指向一个数组,数组有10个元素,每个元素的类型是 int
int (*parr3[10])[5]; 
	/* parr3[10] */parr3 是一个数组,该数组有10个元素。 /* int (* )[5] */每一个元素都是数组指针
	该数组指针指向的 数组 有5个元素,每个元素都是int	 /* int (* )[5] */		

4、数组传参和指针传参

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

一维数组传参

用数组、指针接收

#include <stdio.h>
void test(int arr[])//ok
{}
void test(int arr[10])//ok
{}
void test(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);
}

二维数组传参

用二维数组接收

传过来的是第一行的地址,第一行有5个元素,用数组指针接收

#include <stdio.h>
void test(int arr[3][5])//ok  二维数组传过来,我就用二维数组接收。参数行可以省、列不可省
{}
void test(int arr[][5])//ok
{} 
void test(int arr[][])//ok?× 列不可省
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。

void test(int *arr)//ok?X 二维数组数组名传参,传过来的是第一行的地址【就是一维数组的地址】
   				 //而int 存的是整型的地址,不能存一维数组的地址
{}
void test(int **arr)//ok? X 数组名是首元素的地址,是第一行的地址,是数组的地址。而二级指针存放的是一级指针变量的地址
{}
void test(int* arr[5])//ok? X 这个是用指针数组来接收 首元素的地址肯定不行
{}

void test(int (*arr)[5])//ok 传过来的是第一行的地址,第一行有5个元素,每个元素都是int
    				  //用指针接收,指针指向5个元素,每个元素都是int 故:int (*arr)[5]
{} 
int main()
{
    int arr[3][5] = {0};
    test(arr);  //传过来的是第一行的地址,第一行有5个元素,每个元素都是int。  int (*arr)[5]
}

一级指针传参

#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;
}

思考:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

比如:

void test1(int *p)
{}
int main()
{
    int a = 10;
    int* p = &a;
    test1(  );
    return 0;
}
//test1函数能接收什么参数?----参数是一级指针,可以传 一个变量的地址&a、存放一级指针变量的地址p
// test1(&a);     test1(p);
void test2(char* p)
{}
int main()
{
    char ch = 'w';
    char* pc = &ch;
    test2( );
    return 0;
}
//test2函数能接收什么参数?  ---- 可以传过去一个字符的地址 &ch、一级字符指针变量 pc
// test2(&ch);     test2(pc);

二级指针传参

二级指针传过去、就用二级指针接收

#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;
} 

思考:

当函数的参数为二级指针的时候,可以接收什么参数?

传二级指针变量本身、一级指针变量的地址、指针数组【传过去是首元素地址,首元素是指针的地址】 都ok

void test(char **p)
{
}
int main()
{
    char c = 'b';
    char*pc = &c;
    char**ppc = &pc;
    char* arr[10];
    test(&pc);  //一级指针变量的地址
    test(ppc);  //二级指针变量本身
    test(arr); //Ok 指针数组
    return 0;
}

5、函数指针

数组指针 —— 指向数组的指针

函数指针 —— 指向函数的指针 【存放函数地址 的指针】 int (*p)( )

​ 调用函数 (*pa)(2,3) 里面的 * 没有实际意义

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int a = 10;
	int b = 20;
	printf("%d\n", Add(a, b));  //30
    
	//取函数的地址: &函数名(&Add) 和 函数名(Add) 都是拿到函数的地址,一样的,没有任何区别
	//打印函数的地址【函数可没有首地址哦】
	printf("%p\n", &Add);  //008113AC
	printf("%p\n", Add);   //008113AC

	//函数的地址怎么存? ---- 即函数指针变量pa怎么定义
	//首先是个指针-->(*pa)  得加括号,不然p就会先和后面的括号结合,那就是个函数了,就不是指针了 
    //然后函数参数的类型是int、int -->(*pa)(int x,int y) 其中x、y可以省略
	//函数的返回类型是int型 故: int (*pa)(int ,int) = Add  或  int (*pa)(int ,int) = &Add
	int (*pa)(int, int) = Add;
	//int (*pa)(int, int) = &Add;

	//既然pa【函数指针】是Add函数的地址,那么(*pa)就是Add函数。(*pa)(2,3)就调用Add函数,并传参2 3
	printf("%d\n", (*pa)(2, 3)); //5
    // 这个 * 是什么意思
    printf("%d\n", (**pa)(2, 3)); //5
    printf("%d\n", (***pa)(2, 3)); //5
    //说明 这个 * 没有用,那我不写发现也可以
    printf("%d\n", (pa)(2, 3)); //5   把Add传给pa,说明Add和pa是一回事  Add(2,3) == pa(2,3)
	return 0;
}

void Print(char* str)
{
	printf("%s\n", str);
}

int main()
{
	void (*p)(char*) = Print;  //去掉p,剩下的 void (*)(char*) 是类型
	(*p)("hello world");
	return 0;
}

阅读两段有趣的代码:

//代码1
(*(void (*)())0)();

()00前面加个括号,说明是对整型0进行强制类型转换,转换成什么类型看括号里面的
	void (*)()  这是0强制类型转换后的形式,这是一个函数指针.该指针指向一个函数,函数的返回类型为void0 强制类型转换为一个函数地址
*( void (*)() )0  对这个地址进行解引用,就找到这个函数
(*( void (*)() )0)() 就调用 0 地址处的该函数
    
把0强制类型转换为函数指针类型,该指针指向的函数是无参、返回类型是void0变成一个函数地址之后呢,对他进行解引用操作,调用以0为地址处的该函数    
//代码2
void (*signal(int , void(*)(int)))(int);
void (*             signal(int , void(*)(int))            )(int);
首先signal后面跟了括号,说明signal是个函数,有2个参数,第一个参数是int型、第二个参数是void(*)(int)函数指针类型
    去掉函数名和函数参数,剩下的就是返回类型: void (* )(int)   这是个函数指针类型
signal函数的返回类型是函数指针类型,    

typedef 函数指针的 * 要靠近函数名字

//对代码2进行精简
typedef void(*pfun_t)(int);  //注意pfun_t的位置  关键字起个小名
pfun_t signal(int , pfun_t);

typedef unsigned int uint;

6、函数指针数组

转移表

要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢

int ( *parr1 [10] ) ( );

首先是个数组,去掉parr1 [10] 剩下的是类型 int ( * ) ( ) ,是函数指针的类型。 故数组里存的函数指针 - 函数指针数组

int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
int main()
{
	//指针数组
	int* arr[5];
	//需要一个数组,这个数组可以存放4个函数的地址 - 函数指针的数组
	int (*pa)(int, int) = Add; //Mul Sub Div

	//int (*parr[4])(int, int); 
		//首先parr和[4]结合,是个数组,有4个元素。把数组名去掉,剩下的就是数组的类型
		//int (*)(int, int)  是函数指针的类型。  即函数指针的数组
	//对函数指针的数组进行初始化
	int (*parr[4])(int, int) = { Add,Sub ,Mul ,Div };  //放进去4个函数的地址

	//函数指针怎么用呢
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%d\n", parr[i](2, 3)); //parr[i]找到数组的每个元素,每个元素都是函数的地址,解引用前面的*									可以不加
						            // 5 -1 6 0
	}
	return 0;
}

练练手

char* my_strcpy(char* dest, const char* src);
//写一个函数指针pf,能够指向my_strcpy
首先是个指针 (*pf) 还是个函数指针 (*)()  函数的参数char*, const char* 和 返回类型 char*
    char* (*pf)(char* , const char* )  // 可以不用dest src
//写一个函数指针数组pfArr,能够存放4个my_strcpy函数的地址
函数指针数组: 首先是个数组pfArr[4]   数组里存的是函数指针 (*pfArr[4])() 函数的参数 char*, const char* 
返回类型 char*
      char* (*pfArr[4])(char*, const char*)  

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

void menu()   //看一眼就过   版本1
{
	printf("\n");
	printf("**************************\n");
	printf("****1、Add    2、Sub  ****\n");
	printf("****3、Mul    4、Div  ****\n");
	printf("****     0、exit      ****\n");
	printf("**************************\n");
	printf("\n");
}
int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
int Xor(int x, int y)
{
	return x ^ y;
}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	do  // 放在循环里。 do{循环体} while(条件);
	{
		menu();
		printf("请选择\n");
		scanf("%d", &input);
		/*printf("请输入两个操作数\n");
		scanf("%d%d", &x, &y);*/
		switch (input)
		{
		case 1:
			printf("请输入两个操作数\n");
			scanf("%d%d", &x, &y);
			printf("%d\n", Add(x, y));
			break;
		case 2:
			printf("请输入两个操作数\n");
			scanf("%d%d", &x, &y);
			printf("%d\n", Sub(x, y));
			break;
		case 3:
			printf("请输入两个操作数\n");
			scanf("%d%d", &x, &y);
			printf("%d\n", Mul(x, y));
			break;
		case 4:
			printf("请输入两个操作数\n");
			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;
}
//代码冗余,若是计算机功能多,那么case就会很多

把函数指针数组叫做转移表

//函数指针数组存 算法   版本2
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div }; //用函数指针数组存放 加减乘除 函数的地址
    //当多一个算法时,就加一个算法,在初始化后面加上函数名地址,把5直接删了就可以 。菜单改一下,if改为5
    //int (*pfArr[ ])(int, int) = { 0,Add,Sub,Mul,Div,Xor }; 
	do
	{
		menu();
		printf("请选择:\n");
		scanf("%d", &input);
		
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:\n");
			scanf("%d%d", &x, &y);

			int ret = pfArr[input](x, y);  
			//pfArr数组中放着几个函数的地址,pfArr[input]正好对应加减法下标。所以函数指针数组初始化时 0
			//pfArr[input](x, y) 调用这个函数,并传参
			printf("%d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出\n");
		}
		else
		{
			printf("输入错误\n");
		}
	} while (input);
	return 0;
}

如何对版本1 进行代码简化 -------> 回调函数

void Calc(int (*pf)(int, int))
//下面调用Calc函数就可以使用Add、Sub函数,那么Calc函数的参数就是Add、Sub函数的地址
//即函数指针 int (*)(int,int),函数指针的类型是 int  参数类型是int,int 函数指针名 pf 
//Calc函数  返回类型是 void 
{
	int x = 0;
	int y = 0;
	printf("请输入两个操作数\n");
	scanf("%d%d", &x, &y);
	printf("%d\n", pf(x, y));
}
int main()
{
	int input = 0;

	do  // 放在循环里。 do{循环体} while(条件);
	{
		menu();
		printf("请选择\n");
		scanf("%d", &input);
		/*printf("请输入两个操作数\n");
		scanf("%d%d", &x, &y);*/
		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;
}

7、指向函数指针数组的指针

指向函数指针数组的指针是一个 指针, 指针指向一个 数组 ,数组的元素都是 函数指针 
int arr[10] = { 0 };
	int(*p)[10] = &arr;
	//pfArr函数指针数组
	int (*pfArr[4])(int, int);
	//指向函数指针数组的指针ppfArr
	//首先是个指针 *ppfArr,这个指针指向的是函数指针数组,所以指向的是个数组 *ppfArr[4],这个数组里存的是函数指针
	//函数指针:int (*)(int,int)
	int (*(*ppfArr[4])(int, int)) = &pfArr;

8、回调函数

回调函数就是一个通过 函数指针 调用的函数。
	如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
	回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应
void print(char* str) //接受指针p传过来的字符地址
{
	printf("hehe:%s\n",str);
}
void test(void (*p)(char*))  //test 函数接收的是print函数的地址 
							 //函数指针 void (*p)(char*) 指针(*p) 参数(char*)返回类型void
    	//p就是print函数的地址,解引用调用它。回调函数就是一个通过 函数指针 调用的函数
{
	printf("test\n");
	(*p)("bit");  //p指向一个函数地址,要调用它,解引用【用指针】来调用 (*p)(参数)  这个*没啥意义
    p("bit");  //p可以直接调用他,因为*没啥意义,就是为了好理解
}
int main()
{
	test(print);
	return 0;
}

冒泡排序:只能排整数,不能排浮点数

void bubble_sort(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz-1; i++)
	{
		int j = 0;
		int flag = 1; //假设这一趟有序
		for (j = 0; j < sz - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
				flag = 0;
			}
		}
		if (1 == flag) //说明已经有序了
		{
			break;
		}
	}
}
int main()
{
	int arr[10] = { 9,8,7,4,5,6,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	bubble_sort(arr, sz);
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

qsort 排序函数

qsort( void *base,  //要排序数组的首元素地址 - 要排序的是那个数组 arr
      size_t num,   //数组的长度 - sz = sizeof(arr) / sizeof(arr[0])
      size_t width,  //数组每个元素的大小【字节】 - sizeof(arr[0])
      cmp_fangfa //比较的方法  自己写。是个函数指针,比较两个元素的所用函数的地址-这个函数使用者自己写
      			//函数指针的两个参数是:待比较的两个元素的地址
     );
int cmp_fangfa(const void *e1, const void *e2 ) //全都是这么写
{
    return *(int*)e1 - *(int*)e2;
    return ((int)(*(float*)e1) - (int)(*(float*)e2));
    return ((Stu*)e1)->age - ((Stu*)e2)->age; //箭头优先于强制转化
    return strcmp(  ((struct Stu*)e1)->name, ((struct Stu*)e2)->name  );
}void* 可以接收任意类型元素的地址,
但void* 不知道往后走一步是几个字节,故不可进行解引用和+-整数的操作,得先强制类型转换,然后在解引用。

1、比较两个整型

int cmp_int(const void* e1, const void* e2) //int返回值
{
	return *(int*)e1 - *(int*)e2;//但不知道走一步是几个字节,所以不可解引用,得先强制类型转换后再解引用
	//若 e1 > e2 返回正数
	//若 e1 = e2 返回0
	//若 e1 < e2 返回负数
}
void test1()
{
	int arr[10] = { 9,8,7,6,5,4,1,2,3,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	//arr要排序数组的起始位置  sz数组长度  元素大小  比较的方法
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
int main()
{
	test1();
	return 0;
}

2、比较两个浮点数

int cmp_float(const void* e1, const void* e2)  //比较方法都是这么写
{
	return ((int)(*(float*)e1) - (int)(*(float*)e2));
	//e1指针现在是void*的指针【不能子介涌】,得先转换为float*,在解引用,进行比较大小。
	//因为这个函数的返回值得是int,所以得对最后的结果进行强制转换为int  在相减
}
void test2()
{
	float f[] = { 9.0,8.0,7.0,6.0,5.5,4.3,2.0,1.3 };
	int sz = sizeof(f) / sizeof(f[0]);
	qsort(f, sz, sizeof(f[0]), cmp_float);  //都是这么写
	//f要比较数组的起始位置  sz数组长度  f数组每一个元素的大小  浮点型的比较大小方法
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%f ", f[i]); //%f 浮点数
	}
}
int main()
{
	test2();
	return 0;
}

3、比较两个结构体------按年龄

typedef struct Student
{
	char name[20];
	short age;
}Stu;
int cmp_struct_age(const void* e1, const void* e2)
{
	return ((Stu*)e1)->age - ((Stu*)e2)->age;   // ->
	//把void* e1 转换为结构体指针类型 Stu* ,找到age, 再相减
}
void test3()
{
	Stu s[3] = { {"zhangsan",20},{"lisi",30},{"wangwu",15} }; //结构体数组 s[3]  利用小明创建结构体数组
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_struct_age);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("name:%s age:%d \n", s[i].name,s[i].age);  // .
	}
}
int main()
{
	test3();
	return 0;
}

4、比较两个结构体------按名字

#include <string.h>
struct Stu
{
	char name[20];
	short age;
};
int cmp_struct_name(const void* e1, const void* e2)
//因为这里按照名字【字符串比较】,不能直接用> = <,得用strcmp函数比较
//得转换为结构体指针类型,这里直接用函数进行比较,就不用相减,也就不用解引用了
//转换后在找到name 比较
{
	return strcmp(  ((struct Stu*)e1)->name, ((struct Stu*)e2)->name  );
}
void test4()
{
	struct Stu s[3] = { {"zhangsan",20},{"lisi",30},{"wangwu",15} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_struct_name);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("name:%s age:%d \n", s[i].name, s[i].age);
	}
}
int main()
{
	test4();
	return 0;
}

实现原理:

image-20201113214112171

9、指针和数组面试题的解

地址的大小不分贵贱 地址大小都是4/8

关键在于数组名是首元素还是整个数组的判断

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

一维整型数组

  //数组名是首元素的地址  但仅sizeof(数组名)和&数组名时,数组名表示整个数组
int a[] = {1,2,3,4};  //整形的一维数组 有4个元素
printf("%d\n",sizeof(a));  //*16 这里的数组名是整个数组的总大小 sizeof(数组名) 4*4=16
printf("%d\n",sizeof(a+0)); //4 这里有+0就不是sizeof(数组名)了,那么这就是首元素的地址 +0还是首元素的地址  地址的大小就是4/8
printf("%d\n",sizeof(*a)); //4 a是首元素地址,*a就是首元素,首元素的大小-整形的大小 sizeof(int) 4
printf("%d\n",sizeof(a+1)); //4 a+1和a+0是一样的,+1就是第二个元素的地址,地址的大小就是4/8
printf("%d\n",sizeof(a[1])); //4 第二个元素的大小 整形的大小 sizeof(int) 4
printf("%d\n",sizeof(&a));  //*4 &数组名时,拿到的是整个数组的地址,但是数组的地址也是地址,地址的大小就是4/8
printf("%d\n",sizeof(*&a)); //16 数组的地址解引用,拿到的就是整个数组,整个数组的大小  相当于*&抵消了
printf("%d\n",sizeof(&a+1)); //4 &a取到的数组的地址,+1就跳过这一个数组 &a+1拿到的还是一个地址  4/8
printf("%d\n",sizeof(&a[0])); //*4 a[0]数组的第一个元素,&a[0]第一个元素地址的大小 4/8
printf("%d\n",sizeof(&a[0]+1)); //*4 第二个元素的地址 4/8

一维字符数组

sizeof 求长度 会包括 \0

char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr)); //6 sizeof(数组名)-字符数组的大小1*6
printf("%d\n", sizeof(arr+0)); //4 有+0就不是整个数组了,那就是首元素的地址 4/8
printf("%d\n", sizeof(*arr)); //*1 首元素地址,解引用就是首元素,字符大小 
printf("%d\n", sizeof(arr[1]));  //1 第二个字符的大小
printf("%d\n", sizeof(&arr)); //4 &数组名,数组名表示整个数组 -&- 整个数组的地址 4/8
printf("%d\n", sizeof(&arr + 1)); //4 整个数组的地址+1 还是地址 4/8
printf("%d\n", sizeof(&arr[0] + 1)); //4 第一个字符元素的地址 + 1 那就是第二个字符元素的地址 4/8

strlen 是找到 \0 之前的

char arr[] = {'a','b','c','d','e','f'}; //这里面没有 \0
printf("%d\n", strlen(arr));  //*随机值 因为没有\0 数组名首元素地址
printf("%d\n", strlen(arr + 0)); //*随机值 因为没有\0 数组名首元素地址 +0还是首元素地址
printf("%d\n", strlen(*arr));   //err  strlen的参数要的是个地址 你这里传的首元素 字符'a' ASCII值是97 
								//他就把97当成地址往后数找\0 可97的地址 你未必能访问啊,野指针啊
printf("%d\n", strlen(arr[1])); //err  arr[1]就是字符'b' ASCII就是98 野指针
printf("%d\n", strlen(&arr)); //随机值 &arr 表示整个数组 -&- 整个数组的地址  随机 与最前面的随机值一样
printf("%d\n", strlen(&arr + 1));  //随机值-6 &arr 整个数组的地址 +1 跳过这个数组  后面的地址  哈时候遇到\0也不知道,但是这个随机值比前三个随机值小6
printf("%d\n", strlen(&arr[0] + 1)); //随机值-1 与最前面三个随机值差1  从第二个元素的地址往后找
char arr[] = "abcdef";  //  abcdef\0
printf("%d\n", sizeof(arr));  //7 整个素组的长度+\0
printf("%d\n", sizeof(arr + 0)); //4 首元素地址 4/8 
printf("%d\n", sizeof(*arr)); //1 arr首元素地址 * 首元素 'a' 字符的大小
printf("%d\n", sizeof(arr[1])); //1 第二个字符的大小
printf("%d\n", sizeof(&arr)); //4 &arr 整个数组的地址 4/8
printf("%d\n", sizeof(&arr + 1)); //4 跳过这个数组的地址 4/8
printf("%d\n", sizeof(&arr[0] + 1)); //4 首元素的地址 +1 地址
char arr[] = "abcdef";  //  abcdef\0
printf("%d\n", strlen(arr)); //6 arr首元素地址 找\0之前的
printf("%d\n", strlen(arr + 0)); //6

printf("%d\n", strlen(*arr)); //err 首元素地址解引用-首元素 'a' 97往后找 97已经不是你的空间了 野指针 报错
printf("%d\n", strlen(arr[1])); //err 第二个元素'b' 98 野指针 报错

printf("%d\n", strlen(&arr));  //6 &arr 数组名表示整个数组 & 整个数组的地址 6个 
//这里会有一个警告,&arr是整个数组的地址 数组的地址就应该存到数组指针里 char (*p)[7] = &arr。所以&arr的类型应该是char (*)[7] ,但现在&arr的类型是const char*
printf("%d\n", strlen(&arr + 1)); //随机值 整个数组的地址+1 就是\0后面的那个地址 鬼知道啥时候遇到\0 随机值   也有警告
printf("%d\n", strlen(&arr[0] + 1)); //5 首元素的地址+1 第二个元素的地址 5
char* p = "abcdef";  //p的类型是字符指针 只是把首元素a的地址存到字符指针变量p里  a b c d e f \0
printf("%d\n", sizeof(p)); //4  p是指针变量 sizeof(指针变量)  指针的大小  4/8
printf("%d\n", sizeof(p + 1)); //4 p是a的地址 +1 就是b的地址  地址的大小 4/8
printf("%d\n", sizeof(*p)); //1 p是a的地址 *解引用就是a 字符串的大小
printf("%d\n", sizeof(p[0])); //*1  arr[0] == *(arr+0)拿到第一个元素,首元素地址解引用  那么p[0] == *(p+0)第一个元素 字符a的大小
printf("%d\n", sizeof(&p)); //4 p里面存的是a的地址  p也是变量 &p就是地址  地址的大小 4/8
printf("%d\n", sizeof(&p + 1)); //4  跳过p之后那个位置的地址  4/8
printf("%d\n", sizeof(&p[0] + 1)); //4	 b的地址	4/8
char* p = "abcdef";  //p的类型是字符指针 只是把首元素a的地址存到字符指针变量p里  a b c d e f \0
//strlen是找\0之前的
printf("%d\n", strlen(p)); //6  a的地址
printf("%d\n", strlen(p + 1)); //5  b的地址
printf("%d\n", strlen(*p)); //err p是a的地址 *p就是字符'a' strlen的参数是地址 'a'是97 这个地址不是你的 野指针 
printf("%d\n", strlen(p[0]));  //err p[0]是第一个元素 'a'  97 野指针
printf("%d\n", strlen(&p));  //随机值 p里面存的是a的地址,也是变量  &p就是p的地址 随机值  小端存储
printf("%d\n", strlen(&p + 1));   //随机值
printf("%d\n", strlen(&p[0] + 1)); //*5  p[0]就是'a' &p[0]四a的地址  +1  就是'b'的地址  5  【这个不是98哈,98是p[1]】

二维数组

int a[3][4] = {0};  //3行4列都是0
printf("%d\n",sizeof(a)); //48  sizeof(数组名) a表示整个数组 整个数组的大小  4*3*4
printf("%d\n",sizeof(a[0][0]));  //4 第一行第一列的整型元素0的大小 
printf("%d\n",sizeof(a[0])); //16 第一行的大小 a[0]相当于把第一行作为一维数组的数组名  4*4
printf("%d\n",sizeof(a[0]+1)); //*4 a[0]没有单独放在sizeof里,那就是第一行首元素的地址 +1 就是第一行第二个元素的地址  地址4/8
 //他可不是第二行的地址
printf("%d\n",sizeof(*(a[0]+1))); //*4  这里a[0]是第一行第一个元素的地址,+1就是第一行第二个元素的地址  *一下解引用 就是第一行第二个元素 整型
printf("%d\n",sizeof(a+1));  //4 a是首元素的地址【第一行的地址】 a+1 就是第二行的地址 4/8
printf("%d\n",sizeof(*(a+1)));  //16  a+1是第二行的地址 *解引用就是第二行 4*4   就等价于a[1] 第二行的大小
printf("%d\n",sizeof(&a[0]+1)); //4    a[0]第一行  &a[0]第一行地址  +1 第二行地址  地址 4/8
printf("%d\n",sizeof(*(&a[0]+1))); //16  第二行的地址解引用,就是第二行 4*4
printf("%d\n",sizeof(*a));   //16  a数组名是数组首元素的地址,第一行的地址, *一下 第一行4*4 
printf("%d\n", sizeof(a[3]));  //*16  a[3]第四行的大小  但sizeof内部的参数并不真正的参与运算,就是说并不真正的去访问第四行的元素,只是根据他的类型来计算大小。他与a[0]一样  a[3]相当于把第四行作为一维数组的数组名
总结: 数组名的意义:
1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3. 除此之外所有的数组名都表示首元素的地址

10、指针笔试题

1、程序的结果是什么

int main() 
{
	int a[5] = { 1, 2, 3, 4, 5 };
	int* ptr = (int*)(&a + 1);
	printf("%d\n%d\n", *(a + 1), *(ptr - 1));
	return 0;
} 
//2 a是首元素的地址 +1就是第二个元素的地址 解引用 就是第二个元素2 
//5 &a取到整个数组的地址,+1就跳过这个地址, ptr整型指针,里面存5后面的地址。 ptr-1的地址就是5的地址,解引用就是5 

2、程序的结果是什么

由于还没学习结构体,这里告知结构体的大小是20个字节
假设p 的值为0x100000。 如下表表达式的值分别为多少?

//结构体指针创建了一个p变量
struct Test 
{
    int Num;
    char *pcName;
    short sDate;
    char cha[2];
    short sBa[4]; 
}*  p;

int main()
{
    p = (struct Test*)0x100000;
    printf("%p\n", p + 0x1);   // 00100014    
    //p是结构体指针类型,p+1【加一个结构体指针的大小】取决于一个结构体有多大,说了是20,人家是0x16进制,20的16进制是0x14,故  0x100000 + 0x14 = 0x00100014
    printf("%p\n", (unsigned long)p + 0x1); //00100001   把p强制转换为整型,整数+1就是加1
    printf("%p\n", (unsigned int*)p + 0x1);  //00100004  
    //把p强制转换为无符号的整形指针,+1就要跳过一个无符号的整型字节,一个整形字节就是4,就是+4
    return 0;
}
指针 +- 整数  取决于指针类型

3、笔试3

int main()
{
	int a[4] = { 1, 2, 3, 4 };
	int* ptr1 = (int*)(&a + 1);  //&a取到的是数组的地址,+1跳过一个数组。ptr1指针里存的是4后面的地址
	int* ptr2 = (int*)((int)a + 1); //a是首元素1的地址   (int)a把首元素地址强制转换为整型 假设转换为数字5                                        :00000005
		                            //那么(int)a+1 就是6  即:00000006 
									//那么5和6的内存地址就差一个字节   
	printf("%x,%x", ptr1[-1], *ptr2); //%x是啥?--- 16进制打印
	return 0;
}
整数+1就是 +1  它的地址向后偏了一个字节
在内存中,最小的内存单元是字节,每个字节都给1个地址,两个相邻地址之间差1个字节
所以 地址+1 向后偏移1个字节
image-20201116210152937

4、笔试4

#include <stdio.h>
int main()
{
	int a[3][2] = { (0, 1), (2, 3), (4, 5) };  //逗号表达式  坑
	int *p;
	p = a[0];  //a[0] 是第一行的数组名  代表第一行首元素的地址即1的地址,存到p里
	printf( "%d", p[0]);  //p[0] 等价于 *(p+0)  p里面的第一个元素
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ofjkK2MQ-1631092637480)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201116212708250.png)]

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;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ff79FOJu-1631092637482)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201116223607353.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DXwBk4Hx-1631092637485)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201116233635803.png)]

6、笔试6 easy

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
    //ptr1是整形指针  -1  向前挪动一个整型
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qj73GnVb-1631092637486)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201117001126529.png)]

7、笔试7

#include <stdio.h>
int main()
{
	char* a[] = { "work","at","alibaba" }; //a啥类型?字符指针数组 a是数组 里面存的字符指针 w a a三个地址
	char**pa = a;  //右边a是数组首元素地址 就是w的地址。把w的地址放在二级指针pa里
    //后面的*表示pa是个指针,前面的*表示指针里的元素是char*字符指针类型
	pa++; //pa指针后移 跳过一个char*的变量  就是w指针后移  就移到了a的地址(at)
	printf("%s\n", *pa);  //at
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sftDDntQ-1631092637489)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201117004823563.png)]

8、笔试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);    //POINT
	printf("%s\n", *-- * ++cpp + 3);  //ER
	printf("%s\n", *cpp[-2] + 3);    //ST
	printf("%s\n", cpp[-1][-1] + 1);  //EW
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PiXLtQRF-1631092637492)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201117112113985.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pJUbofU0-1631092637494)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20201117112052874.png)]

11、小结

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	//指针数组
	int* arr[10];

	//数组指针
	int* (*arr)[5] = &arr;

	//函数指针
	int (*pAdd)(int,int) = &Add;//Add
	//调用Add这个函数
	printf("%d\n", pAdd(1, 2)); //Add(1,2)
	printf("%d\n", (*pAdd)(1, 2)); //*只是方便解读,没啥实际意义

	//函数指针的数组    返回类型(*)(参数)
	int (*pArr[5])(int, int);

	//指向函数指针数组的指针 (*ppArr)  (*ppArr)[5]  返回类型(*)(参数)
	int (*(*ppArr)[5])(int, int);
	return 0;
}

三、字符串+内存函数的介绍

本章重点

#include <string.h>
#include <errno.h>
求字符串长度 
	strlen
长度不受限制的字符串函数   不安全  以\0为限制
    strcpy
    strcat
    strcmp
长度受限制的字符串函数介绍  
    strncpy
    strncat
    strncmp
字符串查找
    strstr
    strtok
错误信息报告
	strerror
字符操作
内存操作函数
    memcpy
    memmove
    memset
    memcmp

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

或者 字符数组 中。 字符串常量 适用于那些对它不做修改的字符串函数

1、strlen函数

找\0之前的
{"a","b"}里面没有\0
"a,b" 里面有\0

1、字符串以 '\0' 作为结束标志,strlen函数返回的是在字符串中 '\0' 前面出现的字符个数(不包含 '\0' )。
2、参数指向的字符串必须要以 '\0' 结束。
3、注意函数的返回值为size_t,是无符号的( 易错 )
4、学会strlen函数的模拟实现
int my_strlen(char* str)  //指针接收a的地址,str是个指针 
{
	int count = 0;
	while (*str != '\0')
	{
		count++; //计数+1
		str++;   //指针后移
	}
	return count;
}

int main()
{
	int len = my_strlen("abcdef");  //传过去的是a的地址  有\0
	printf("%d\n",len);  //6
	return 0;
}

注意函数的返回值为size_t,是无符号的( 易错 )

int main()
{
	if (strlen("abc") - strlen("abcdef") > 0)  //strlen()返回一个无符号的数,两个无符号的数相减得到的依然是无符号的   3-6为-3  但是-3被当成无符号的数来看待    
        //如果my_strlen相减得到的就是 -3 小于0的负三
	{
		printf("hehe\n");
	}
	else
	{
		printf("haha\n");
	}
	return 0;
}
//结果hehe

2、strcpy 字符串拷贝

char* strcpy(char * destination, const char * source );
strcpy(arr1目的地地址,arr2源数据) //把arr2拷贝到arr1里
源字符串必须以 '\0' 结束。 arr2不可是{"b","i","t"}
会将源字符串中的 '\0' 拷贝到目标空间。
目标空间必须足够大,以确保能存放源字符串。
目标空间必须可变。而常量字符串【把字符串的首地址放在p里】常量字符串不可被修改
学会模拟实现。
    
int main()
{
    char arr1[]="abcdef";
    char arr2[]="bit";
    strcpy(arr1,arr2);  //把arr2拷贝到arr1里
    printf("%s\n",arr1); 
    return 0;
}
#include <stdio.h>
#include <assert.h>
char* my_strcpy(char* dest, const char* src)  //不修改arr2 用const
{
	//保证指针有效性
	assert(dest != NULL);
	assert(src != NULL);
	char* ret = dest;
	while (*dest++ = *src++)  //拷贝src指向的字符串到dest指向的空间,包含'\0'
	{
		;
	}
    //返回目的地起始位置
	return ret;
}

int main()
{
	char arr1[] = "abcdef";
	char arr2[] = "bit";
	my_strcpy(arr1, arr2);  //传过去首元素地址
	printf("%s\n", arr1);
	return 0;
}

3、strcat 字符串追加

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

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

目标空间必须有足够的大,能容纳下源字符串的内容。char arr1[30]

目标空间必须可修改。目的地要有\0, 从\0的位置开始追加

字符串自己给自己追加,如何?

int main()
{
	char arr1[30] = "hello";  //目的地要足够大
	char arr2[] = "world";
	strcat(arr1, arr2);  //把 arr2追加到arr1后
	printf("%s\n", arr1);  //helloworld
	return 0;
}

实现

#include <stdio.h>
#include <assert.h>
char* my_strcat(char* dest,const char* src) //原字符串不变 const
{
	char* ret = dest;
	assert(dest != NULL);  //assert(dest && src)
	assert(src);
	while (*dest != '\0') //从字符串的末尾开始追加、找到目的字符串的\0
	{
		//1、找到目的字符串的\0  得有\0  在追加
		dest++;
	}
	//2、追加  ---strcpy
	while (*dest++ = *src++)
	{
		;
	}
	//返回什么  想看到dest的变化
	return ret; //这是个字符串
}
int main()
{
	char arr1[30] = "hello";  //目的地要足够大
	char arr2[] = "world";
	my_strcat(arr1, arr2);  //把 arr2追加到arr1后
	printf("%s\n", arr1); //helloworld
	return 0;
}

4、strcmp 字符串ASCII值比较

int strcmp ( const char * str1, const char * str2 );

比的是对应字符ASCII值的大小

第一个字符串大于第二个字符串,则返回大于0的数字

第一个字符串等于第二个字符串,则返回0

第一个字符串小于第二个字符串,则返回小于0的数字

#include <string.h>
int main()
{
	char* p1 = "abcdef";
	char* p2 = "sqwer";
	int ret = strcmp(p1, p2);
	printf("%d\n", ret);  //-1 比的是对应字符ASCII值的大小
	return 0;
}
int my_strcmp(const char* str1, const char* str2)
{
	assert(str1 && str2);
	//比较 先比较第一个字符,若相等就比较第二对
	while (*str1 == *str2)
	{
		if (*str1 == '\0')
		{
			return 0;
		}
		str1++;
		str2++;
	}
                if (*str1 > *str2)
                    return 1;
                else
                    return -1;
    //或不用if else 直接
    //return (*str1 - *str2);
}
int main()
{
	char* p1 = "abcdef";
	char* p2 = "sqwer";
	int ret = my_strcmp(p1, p2);
	printf("ret = %d\n", ret);  //-1
	return 0;
}

5、strncpy 指定字符长度拷贝

拷贝num个字符从源字符串到目标空间。

如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。

int main()
{
	char arr1[10] = "abcdef";
	char arr2[] = "he";
	strncpy(arr1, arr2, 4); //把arr2前4个字符拷给arr1,只有2个he剩下的2个为\0\0
	printf("%s\n", arr1); //he
	return 0;
}

6、strncat 指定字符长度追加

int main()
{
	char arr1[10] = "abcdef";
	char arr2[] = "hello";
	strncat(arr1, arr2, 4); //把arr2前3个字符追加给arr1,只追加前三个
	printf("%s\n", arr1); //abcdefhel
	return 0;
}

7、strncmp 字符串比较

比较指定前几个字符

int strncmp ( const char * str1, const char * str2, size_t num );
int main()
{
	const char* p1 = "abcdef";
	char* p2 = "abcqwer";
	//int ret = strcmp(p1,p2);
	int ret = strncmp(p1, p2, 4); //比较前4个 
	printf("%d\n", ret);  //返回负数 -1
	return 0; 
}

8、strstr 查找字符串

返回找到第一次的地址,找不到返回NULL

char * strstr ( const char *, const char * );
int main()
{
	const char* p1 = "abcdefghi";
	const char* p2 = "def";
	char* ret = strstr(p1, p2); //在p1中查找是否有p2,有就返回地址,没有就返回NULL
	if (ret == NULL)
		printf("没找到\n");
	else
		printf("%s\n", ret); //返回d的地址,然后开始往后打印  defghi
	return 0; 
}

9、strtok 分割字符串

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

	//zpw\0bitedu\0tech
	char buf[1024] = {0}; //在p1中查找是否有p2,有就返回地址,没有就返回NULL
	strcpy(buf, arr);
	//切割buf中的字符串
	
	char* ret = NULL;

	for (ret = strtok(arr,p); ret != NULL; ret = strtok(NULL,p)) //初始化ret只执行一次,判断不为空,循环继续以空往后走
	{ 
		printf( "%s\n",ret);
	}

	//char* ret = strtok(arr, p);
	//printf("%s\n",ret);   //zpw
	//
	//ret = strtok(NULL, p);  //第二次用NULL
	//printf("%s\n", ret);  //bitedu

	//ret = strtok(NULL, p);
	//printf("%s\n", ret);   //tech
	return 0; 
}

10、strerror 错误报告函数

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

#include <errno.h>
char * strerror ( int errnum );
char* str = strerror(errno);
printf("%s\n",str);

11、字符分类函数

#include <ctype.h>
函数如果他的参数符合下列条件就返回真
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任何可打印字符,包括图形字符和空白字符
char ch = "2";
int ret = isdigit(ch); //是数字嘛,是数字就返回一个非零的数字,不是数字就返回0
printf("%d\n",ret);

12、字符转换

int tolower(int c);
int toupper(int c);
char ch = tolower('q');
char ch = toupper('q');
putchar(ch);
char arr[] = "I AM A Student";
int i = 0;
while(arr[i])
{
	if(isupper(arr[i]))
	{
		arr[i] = tolower(arr[i]);
	}
	i++;
}
printf("%s\n",arr); //i am a student

小结

strcpy strcat strcmp strncpy strncat strncmp  操作的都是字符串 与\0有关
但是对于 整形数组、浮点型数组、结构体数组就不行了

13、memcpy 内存拷贝

void * memcpy ( void * destination, const void * source, size_t num );  //拷贝几个字节--整形4个字节
函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
这个函数在遇到 '\0' 的时候并不会停下来。
如果source和destination有任何的重叠,复制的结果都是未定义的。
    如果源数据和目的地是同一个、可能会出问题
int arr1[] = {1,2,3,4,5};
int arr2[5] = {0};
memcpy(arr2,arr1,sizeof(arr1));
#include <stdio.h> 
#include <string.h> 
struct { 
    char name[40]; 
    int age; 
} person, person_copy; 
int main () 
{ 
     char myname[] = "Pierre de Fermat"; 
     /* using memcpy to copy string: */ 
     memcpy ( person.name, myname, strlen(myname)+1 ); 
     person.age = 46; 
     /* using memcpy to copy structure: */ 
     memcpy ( &person_copy, &person, sizeof(person) ); 
     printf ("person_copy: %s, %d \n", person_copy.name, person_copy.age ); 
     return 0; 
}

14、memmove 拷贝重叠

void * memmove ( void * destination, const void * source, size_t num );
和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
如果源空间和目标空间出现重叠,就得使用memmove函数处理。
int main()
{
    int arr[] = {1,2,3,4,5,6,7,8,9,10};
    int i = 0;
    memmove(arr+2,arr,20); //把arr的20个字节,即20/4=5 五个数拷贝给 arr+2的地方
    for(i=0;i<10;i++)
    {
        printf("%d ",arr[i]);  // 1 2 1 2 3 4 5 8 9 10
    }
    return 0;
}

memcpy 只要处理不重叠的内存拷贝就可以

memmove 处理重叠内存的拷贝

15、memcmp 比较内存

int memcmp ( const void * ptr1, const void * ptr2, size_t num );
比较从ptr1和ptr2指针开始的num个字节
    若ptr1>ptr2 返回正数       = 返回0      < 返回负数
int main()
{
    int arr1[] = {1,2,3,4,5};
    int arr2[] = {1,2,5,4,3};
    int ret = memcmp(arr1,arr2,8);  //比较arr1和arr2前8个字符的大小(8/4=2),比较前两个数 
    printf("%d\n",ret);  //0
    int ret1 = memcmp(arr1,arr2,9); //比较前9个字符
    // 01 00 00 00 02 00 00 00 03 00 00 00 ...    小端存储、倒着存进去,倒着读出来
    // 01 00 00 00 02 00 00 00 05 00 00 00...    因为03比05小,所以返回负数
    printf("%d\n",ret1);  //-1
    return 0;
}

16、memset 内存设置

int main()
{
    char arr[10] = "";
    memset(arr,"#",10);  //把arr设置10个字节的#
    return 0;
}

17、库函数的模拟实现 - 见PPT

四、自定义类型详解

4.1、结构体

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

4.1.1、结构体类型的声明

常规声明

struct Stu         //struct 结构体关键字  Stu结构体标签、首字母大写
{
 char name[20];
 int age;
 char sex[5];
 char id[20];
}s1,s2,s3;    //s1 s2 s3 是全局变量   分号不能丢
struct Stu s3;  //定义全局变量
int main()
{
    struct Stu s1; // 局部变量
    struct Stu s2;
    return 0;
}

特殊声明【少用】

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

4.1.2、结构的自引用

在结构中包含一个类型为该结构本身的成员

struct Node
{
 int data;  //数据域
 struct Node* next;  //指针域
};
int main()
{
    struct Node s1;
    return 0;
}

//或
typedef struct Node
{
 int data;
 struct Node* next; 
}Node; //起个小名叫Node
int main()
{
    Node s2; //直接用小明创建
    return 0;
}

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

struct T
{
    double weight;
    int age;
};
struct S
{
    char c;
    struct T st;  //结构体套娃
    int a;
    double d;
    char arr[20];
};

int main()
{
    struct S s = { 'c',{55.6,25},100,3.14,"hello bit" };  //初始化
    printf("%c %lf %d %lf %s\n", s.c,s.st.weight ,s.a, s.d, s.arr);//c 55.600000 100 3.140000 hello bit
    return 0;
}

4.1.4、结构体内存对齐 *

热门的考点: 结构体内存对齐–计算结构体的大小

struct S1
{
	char c1;
	int a;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int a;
};
int main()
{
	struct S1 s1 = { 0 };
	printf("%d\n", sizeof(s1)); //12
	struct S2 s2 = { 0 };
	printf("%d\n", sizeof(s2)); //8
	return 0;
}
考点 如何计算? 首先得掌握结构体的对齐规则:
1. 第一个成员存储在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
	对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
	VS中默认的值为8  Linux中的默认值为4
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所
有最大对齐数(含嵌套结构体的对齐数)的整数倍。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PU7l3idB-1631092637497)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210323211432772.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BpRfk90j-1631092637499)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210323212616224.png)]

struct S4
{
	char c1;
    struct S3 s3;
    double d;
};
printf("%d\n",sizeof(struct S4)); //32

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GPRBl1Q2-1631092637500)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210323220544524.png)]

结构体的内存对齐是拿空间来换取时间的做法
\1. **平台原因****(****移植原因****)**: 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址

处取某些特定类型的数据,否则抛出硬件异常。

\2. **性能原因**: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器

需要作两次内存访问;而对齐的内存访问仅需要一次访问。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
//让占用空间小的成员尽量集中在一起。
struct S1
{
 char c1;
 int i;
 char c2;
};
struct S2
{
 char c1;
 char c2;
 int i;
};
修改默认对齐数
#include <stdio.h>
#pragma pack(4)//设置默认对齐数为4
struct S1
{
 char c1;
 int i;
 char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

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

offsetof 偏移量

offsetof是求结构体成员相对结构体的偏移量,需要引入头文件,是宏。

用法 offsetof (struct S , c)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x5tBNfIv-1631092637502)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210323223054515.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UQhBS0ce-1631092637505)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210323223122806.png)]

offsetof宏的实现 -百度笔试

4.1.5、结构体传参

struct S
{
	int a;
	char c;
	double d;
};
void Init(struct S* pa) //传址用结构体指针struct S*接收
{
	pa->a = 100;  //传址的时候用 ->
	pa->c = "w";
	pa->d = 3.14;
}
void Print1(struct S pa) //传值 用结构体接收
{
	printf("%d %c %lf\n", pa.a, pa.c, pa.d); //用 . 100 0 3.140000
}
void Print2(const struct S* ps) //传址,只是打印 不改变源数据,用const
{
	printf("%d %c %lf\n", ps->a, ps->c, ps->d); //100 0 3.140000
}
int main()
{
	struct S s = { 0 };
	Init(&s);  //传址
	Print1(s); //传值
	Print2(&s); //传址
	return 0;
}

上面的 print1 和 print2 函数哪个好些?

答案是:首选print2函数。 原因:

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

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

结论:结构体传参的时候,要传结构体的地址。

4.1.6、结构体实现位段(位段的填充&可移植性)

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

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

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

struct A {
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};
printf("%d\n", sizeof(struct A));  //8

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fOB4KU12-1631092637506)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210324102123375.png)]

位段的内存分配

\1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型

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

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

//一个例子
struct S {
 char a:3;
 char b:4;
 char c:5;
 char d:4;
};
struct S s = {0};
s.a = 10; s.b = 12; s.c = 3; s.d = 4;
//空间是如何开辟的?

位段先用低位、从右向左存储

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VoLS70I5-1631092637508)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210324105849883.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-onituH8P-1631092637510)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210324103954870.png)]

位段的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。

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

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

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

还是利用,这是不确定的。

总结:

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

4.2、枚举

4.2.1、枚举类型的定义

枚举顾名思义就是一一列举。把可能的取值一一列举。
比如我们现实生活中:
    一周的星期一到星期日是有限的7天,可以一一列举。
    性别有:男、女、保密,也可以一一列举。
    月份有12个月,也可以一一列举
    颜色也可以一一列举。
    
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。 {}中的内容是枚举类型的可能取值,也叫枚举常
量 。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。 例如:
//枚举类型
enum Day//星期
{   //枚举的可能取值
     Mon = 1,
     Tues = 2,
     Wed = 3,
     Thur,  //默认是4
     Fri,
     Sat,
     Sun  //无逗号
};
enum Sex//性别
{
     MALE,
     FEMALE,
     SECRET
}int main()
{
    enum Sex s = MALE;
    enum Day d = Mon;
    
    //enum Day a = 5;  //左边是枚举类型  右边是int
    
    printf("%d %d %d\n",MALE,FEMALE,SECRET); //0 1 2
    printf("%d %d %d\n",Mon,Tues,Wed); // 1 2 3
    return 0;
}

4.2.2、枚举的优点

为什么使用枚举?
我们可以使用 #define 定义常量,为什么非要使用枚举? 枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量

4.2.3、枚举的使用

enum Color//颜色
{ 
 RED=1, 
 GREEN=2, 
 BLUE=4 
}; 
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5; //ok?? 左边是枚举类型  右边是int

4.3、联合(共用体)

4.3.1、联合类型的定义

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

联合也叫共用体)。 比如:

//联合类型的声明
union Un 
{ 
 char c; 
 int i; 
}; 
//联合变量的定义
union Un un; 
//计算连个变量的大小
printf("%d\n", sizeof(un));

4.3.2、联合的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员
因为i和c共用一块空间,所以i和c不能同时使用,改一个另一个也会改
union Un 
{ 
 int i; 
 char c; 
}; 
union Un un; 
// 下面输出的结果是一样的吗?   一样 同一块地址
printf("%d\n", &(un.i)); 
printf("%d\n", &(un.c));

//下面输出的结果是什么?
un.i = 0x11223344; 
un.c = 0x55; 
printf("%x\n", un.i);  //11223355

面试题:判断大小端存储

int check_sys()
{
	int a = 1; // 00 00 00 01 小端就是 01 00 00 00
	//char* p = (char*)&a;   
    
	//return *p; //*p要么是1,要么是0 
    //在简化
    return *(char*)&a;   //拿到a的地址,取一个字节(char*)
}
int main()
{
	int ret = check_sys();
	//返回1,小端。返回0,大端
	if (ret == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

法二

int check_sys()
{
	union Un
    {
        char c;
        int i;
    }u;
    u.i = 1;
    return u.c; //返回1 小端    返回0 大端
}
int main()
{
	int ret = check_sys();
	//返回1,小端。返回0,大端
	if (ret == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-roaF9ARN-1631092637512)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210324155157829.png)]

4.3.3、联合大小的计算

联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un1
{
	char c[5];  //占5个字节,对齐数: char 1 vs 8 较小值:1
	int i;    //占4个字节,对齐数: int 4 vs 8 较小值:4 
};  //5个字节,对齐数为4,5不是4的整数倍,往后浪费。 所以是 8
union Un2
{
	short c[7];  //14 对齐数:short 2 
	int i;  //4 对齐数 :4
};  //14 而14不是4的整数倍,浪费 故16
//下面输出的结果是什么?
int main()
{
	printf("%d\n", sizeof(union Un1));  //8
	printf("%d\n", sizeof(union Un2));  //16
	return 0;
}

通讯录

五、动态内存管理

1、为什么存在动态内存分配

我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6ZG7qCke-1631092637513)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210324165935061.png)]

2、动态内存函数的介绍

2.1、malloc 和 free

#include <stdlib.h>
void* malloc (size_t size);  //返回类型是void*   使用时强制转换一下
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
malloc(10 * sizeof(int))
calloc(10,sizeof(int))
#include <stdlib.h>   //malloc头文件
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
	//向内存申请10个整形的空间
	int* p = (int*)malloc(10 * sizeof(int));  //因为malloc返回的是void*的指针,所以转化一下

	if (p == NULL) // 申请失败、返回NULL
	{
		printf("%s\n", strerror(errno));  //打印错误
	}
	else  //申请成功
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(p + i) = i; //赋值
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i)); //0 1 2 3 4 5 6 7 8 9
		}
	}
    
    //申请的空间用完释放 -- free
	free(p);  //p虽然被释放了,但是还会找到p的地址
    p = NULL;  //为了别破坏p
	return 0;
}

2.2、calloc

 calloc 函数也用来动态内存分配
 void* calloc (size_t num, size_t size);
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
int main()
{
	int* p = (int*)calloc(10, sizeof(int));  //calloc动态开辟10个int内存空间
	if (p == NULL)
	{
		printf("%s\n",strerror(errno));
	}
	else
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			printf("%d ",*(p + i));  // 0 0 0 0 0 0 0 0 0 0   初始化为0
		}
	}
	free(p);
	p = NULL;
	return 0;
}

2.3、realloc

有时我们会发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了, realloc 可对内存的大小做灵活的调整。
void* realloc (void* ptr, size_t size);
ptr 是要调整的内存地址
    若ptr为NULL,就相当于malloc
size 调整之后新大小
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
realloc在调整内存空间的是存在两种情况:
	情况1:原有空间之后有足够大的空间
    情况2:原有空间之后没有足够大的空间
realloc前不用强制转换
image-20210325232807857
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。 
当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。 
由于上述的两种情况,realloc函数的使用就要注意一些。
#include <stdio.h>
#include <stdlib.h>

int main()
{
	int* p = (int*)malloc(20);
	if (*p == NULL)  //malloc开辟失败
	{
		printf("%s\n", strerror(errno));  //打印错误
	}
	else
	{
		int i = 0;
		for (i = 0; i < 5; i++)
		{
			*(p + i) = i;
		}
	}
	//用malloc开辟20个字节空间
	//突然不够用了,我们希望能有40个或更多的空间
	//这里就可以用realloc来调整动态开辟的内存
	//int* p2 = realloc(p, INT_MAX);
	//realloc 使用注意事项:
	//1、如果p指向的空间之后有足够的内存空间可以追加,则直接追加,后返回p
	//2、如果p指向的空间之后没有足够的内存空间可以追加,则realloc函数会重新找一个新的内存区域开辟一块
	//   满足需求的空间,并且把原来内存的数据拷贝过来,并释放旧的内存空间
	//   最后返回新开辟的内存空间地址
	//3、得用一个新指针变量来接收realloc函数的返回值。
	int* ptr = realloc(p, 40);

	if (ptr != NULL)
	{
		p = ptr;  //用同一个指针p来维护
		int i = 0;
		for (i = 5; i < 10; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i)); //0 1 2 3 4 5 6 7 8 9
		}
	}
	
	free(p);  //因为是同一块内存,释放p就可以
	p = NULL;
	return 0;
}

3、常见的动态内存错误

3.1、对NULL指针的解引用操作

void test()
{
     int *p = (int *)malloc(INT_MAX/4);  //如果malloc失败了,会返回空指针,对NULL指针解引用会出错,所以得判断是否为NULL
     *p = 20;//如果p的值是NULL,就会有问题
     free(p);
     p=NULL;
}

3.2、对动态开辟空间的越界访问

void test()
{
     int i = 0;
     int *p = (int *)malloc(10*sizeof(int));  //开辟了10个整形内存空间
     if(NULL == p)
     {
     exit(EXIT_FAILURE);
     }
     for(i=0; i<=10; i++)   //这里有11个 -- 越界了
     {
     *(p+i) = i;//当i是10的时候越界访问
     }
     free(p);
     p=NULL;
}

3.3、对非动态开辟内存使用free释放

void test()
{
 int a = 10; //局部变量,在栈区开辟的
 int *p = &a;
 free(p);//ok? × 动态内存是在堆区开辟的、你栈区的释放个屁
 p = NULL;
}

3.4、使用free释放一块动态开辟内存的一部分

void test()
{
 int *p = (int *)malloc(100);
 p++;  //++之后 p就指向9后面的地址了,  所以要用 *(p + i) = i; 的方式
 free(p);//p不再指向动态内存的起始位置
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QdI855bB-1631092637515)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210326152223373.png)]

3.5、对同一块动态内存多次释放

void test()
{
 int *p = (int *)malloc(100);
 free(p);
 free(p);//重复释放
}
//原则:谁申请空间谁释放
//  free完后, 在p = NULL; 下面在重复的free就没有意义了

3.6、动态开辟内存忘记释放(内存泄漏)

while(1)
{
    malloc(1);  //一直在开辟内存却不回收、内存就被耗干了。故:内存申请完就要释放
}

4、几个经典的笔试题

题目1:

void GetMemory(char *p)  //形参p是实参的一份临时拷贝
{
 p = (char *)malloc(100);  //动态内存未释放,而且p出了函数就找不到了,没办法释放内存
}
void Test(void) {
 char *str = NULL;  //
 GetMemory(str); //str传的是变量本身,传值
 strcpy(str, "hello world");  //str还是空, hello world 复制不到空里,崩溃了
 printf(str);
}
int main()
{
    Test();
    return 0;
}
//请问运行Test 函数会有什么样的结果?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F5eoR615-1631092637517)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210326170439541.png)]

//改正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;
}
int main()
{
    Test();
    return 0;
}
//改正2
char* GetMemory(char* p)  //char*
{
    p = (char*)malloc(100); 
    return p; // 把p的地址传出去  上司在死前把卧底传出去
}
void Test(void) 
{
    char* str = NULL;  
    str = GetMemory(str); // 用str接收p的地址
    strcpy(str, "hello world");  
    printf(str);

    free(str); //
    str = NULL;
}
int main()
{
    Test();
    return 0;
}

题目2:返回栈空间地址

char* GetMemory(void)
{
    char p[] = "hello world";  //局部变量【栈区】、出了函数 p就被销毁了
    return p;  //p的地址确实返回了、但是里面的内容已经被销毁了。
                //【谁知道谁在用这块内存-非法访问内存】
}
void Test(void)
{
    char* str = NULL;
    str = GetMemory();  //str拿到了GetMemory的返回值,但是里面p的内容已经被销毁了,可能别人在用
    printf(str);   //随机值
}
int main()
{
    Test();
    return 0;
}
//请问运行Test 函数会有什么样的结果?
//非法访问内存

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2QCKep95-1631092637518)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210327103601869.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BtQ3gggh-1631092637519)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210327105110924.png)]

题目3:

void GetMemory(char **p, int num) 
{
 *p = (char *)malloc(num);
}
void Test(void) 
{
 char *str = NULL;
 GetMemory(&str, 100);
 strcpy(str, "hello");
 printf(str);
}
int main()
{
    Test();
    return 0;
}
//请问运行Test 函数会有什么样的结果?
//输出hello,但是动态内存未释放,存在内存泄漏
//在输出后,加上 free(str); str = NULL;

题目4:

void Test(void) 
{
     char *str = (char *) malloc(100);
     strcpy(str, "hello");
     free(str);  //释放后,str依然指向那块地址,但是里面的内容hello被释放了,不能在访问了
     if(str != NULL)  //因为 没有str=NULL 所以没置空
     {
         strcpy(str, "world"); //str依然指向那块地址,把world复制进去
         printf(str); //打印world, 但是str不能访问那块空间了,非法访问了
     }
}
int main()
{
    Test();
    return 0;
}
//请问运行Test 函数会有什么样的结果?
//输出world 但是非法访问内存了 if判断不起作用  【指针不为空的时候采取使用她】
//改正: free完,把str置空  str=NULL;  下面的判断才能拦得住

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xgXeF13w-1631092637522)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210327113314506.png)]

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

1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS操作系统回收 。分配方式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

有了这幅图,我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了。

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

5、柔性数组

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

typedef struct st_type
{
 int i;
 int a[0];//或写成 int a[];   未知大小的数组,柔性数组成员
}type_a;

柔性数组的特点:

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

柔型数组的大小

struct S
{
     int i;
     int a[0];//柔性数组成员
};
int main()
{
    struct S s;
    printf("%d\n", sizeof(s));//输出的是4   结构大小不包括柔型数组的大小
    return 0;
}

柔型数组的使用 代码1

struct S
{
    int n;
    int arr[0];//柔性数组成员
};
int main()
{
    struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int)); // 4+5*4=24
    //动态开辟struct S 和 柔型数组的空间   转换为结构体指针类型
    ps->n = 100;
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        ps->arr[i] = i; //a[i] 为 0 1 2 3 4 
    }
    //结构体太小了、扩充  扩充到44个字节,原来24个 多了20个20/4=5
    struct S* ptr = realloc(ps, 44);
    //开辟完之后得判断 是否开辟成功
    if (ptr != NULL)
    {
        ps = ptr;
    }
    for (i = 5; i < 10; i++)
    {
        ps->arr[i] = i;
    }
    for (i = 0; i < 10; i++)
    {
        printf("%d ", ps->arr[i]);  //0 1 2 3 4 5 6 7 8 9
    }
    //释放空间
    free(ps);
    ps = NULL;
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gquIqKYx-1631092637524)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210327195356075.png)]

也可以这样动态可变 代码2

struct S
{
    int n;
    int* arr;//指针  用指针指向一块区域
};
int main()
{
    struct S* ps = (struct S*)malloc(sizeof(struct S)); //动态开辟结构体指针内存
    ps->arr = malloc(5 * sizeof(int)); //arr指针指向一块空间  ps共24×错 只有20 
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        ps->arr[i] = i; //赋值,指针[i] 依次取
    }
    for (i = 0; i < 5; i++)
    {
        printf("%d ", ps->arr[i]);
    }
    //动态调整arr指向那块地址的大小  调到40  realloc前不用强制转换
    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 = 0; i < 10; i++)
    {
        printf("%d ", ps->arr[i]); //0 1 2 3 4 0 1 2 3 4 5 6 7 8 9
    }
    //释放空间  ps和ps->arr 都是动态申请的 先内后外
    free(ps->arr);
    ps->arr = NULL;
    free(ps);
    ps = NULL;
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e3MEeZJ0-1631092637526)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210327202659528.png)]

上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处

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

第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

7、通讯录

六、C语言文件操作

6.1、概念

什么是文件

磁盘上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件
程序文件
	包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
数据文件
	文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。

文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名。

文件类型

根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件【人看得懂】

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cSAWn3u1-1631092637528)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210327232018144.png)]

文件缓冲区

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yhbBF15d-1631092637530)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210328110351328.png)]

文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们可以创建一个FILE*的指针变量:
FILE* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。

6.2、使用

文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。

FILE * fopen ( const char * filename, const char * mode );
int fclose ( FILE * stream );

绝对路径加个转义字符 \
    
/* 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; }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LJ9HWbe9-1631092637532)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210329101117480.png)]

文件的顺序读写

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7SzXU3dT-1631092637536)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210329141656663.png)]

文件的随机读写

文件结束的判定

七、C语言预处理

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

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

详解:C语言程序的编译+链接

组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DgHLzLJa-1631092637538)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210329143911805.png)]

运行环境

程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。

处理操作符#和##的介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qwkEKamj-1631092637542)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210329154223746.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uGTr3r92-1631092637544)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210329154514597.png)]

宏和函数的对比

宏是替换进去的,不是计算好在替换进去

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gp5Tv8Ym-1631092637546)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210329155343807.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3wdkOJGU-1631092637548)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210329161236818.png)]

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

#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务? 原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可
以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
当然和宏相比函数也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TUmY2nTS-1631092637550)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210329160911959.png)]

#undef

这条指令用于移除一个宏定义。

命令定义

预处理指令 #include

预处理指令 #undef

条件编译

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值