C语言基础篇及进阶

C 语 言

目录

基础篇

​ [前言 (初识、概述)](#前言 C学习笔记(初识、概述))

一、分支和循环

二、C语言数组

三、函数

四、阶段实践

五、操作符和表达式

六、C语言指针

七、C语言初识结构体

进阶篇

一、数据的存储

二、指针的进阶

三、阶段习题详解

四、字符函数和字符串函数

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

六、动态内存管理

七、C语言文件操作

八、程序环境和预处理

基础篇

将之前所有的文章进行汇总,方便收藏阅读。

前言 C学习笔记(初识、概述)

常见的数据类型

类型名称大小
char字符通常1字节(8位)1
short短整型2
int整形-2147483648~21474836474
long长整形4或8
long long更长的整形8
float单精度浮点1位符号 8为指数,23位小数4
double双精度浮点1位符号 11位指数 52位小数8

C语言标准规定sizeof(long)>=sizeof(int) sizeof()表示占内存的大小

常见的打印格式

格式意思
%d打印整型
%c打印字符
%f打印浮点数字
%p以地址形式打印
%x打印16进制数字
%o打印8进制数字
%lf打印双精度小数

小插曲

short age = 20; 		//向内存申请2个字节空间
float weight=95.6;		//编译器会警告,95.6默认是double型
float weight=95.6;		//编译器不警告了,  但是这两句不能同时出现

全局变量与局部变量

全局变量: 定义在代码块之外
局部变量: 定义在代码块之中
// 局部变量与全局变量的名字建议不要相同,容易误会产生bug
// 当局部变量和全局变量的名字相同的时候局部变量优先。
{
    {int a ;}
    printf();
}
// 在这里int a相对于printf来说就是局部的。

注意 C语言语法规定,变量定义在当前代码块的最前面(规范)

变量的作用域和生命周期

作用域: 变量在哪里可以使用哪里就是作用域
	局部变量作用域:变量所在的局部范围
	全局变量作用域:整个工程
生命周期:
	局部变量的生命周期:进入作用域生命周期就开始,出作用域生命周期就结束
	全局变量的生命周期:整个程序的生命周期
		全局变量的生命周期=main函数生命周期=工程的生命周期

源文件 .c 头文件 .h

  • scanf是C语言提供的(但是不安全)

  • scanf_s不是C语言提供的 VS编译器提供的(这样就没有可移植性了)

    像scanf,strcopy,strlen,strcat… C语言提供的库函数好多不安全

常量

常量和变量的定义形式有所差异

常量: 1. 字面常量	 2. const修饰的常变量  3. #define定义的标识符常量  4. 枚举常量
3;     //  字面常量,直接写出来的值
const int num = 4;    // const修饰 将变量变为常量  -> 常变量
#define MAX 10      // 没有“=”  没有“;”
//枚举关键字
enum Sex{
    MALE,FEMALE,SECRET     //最后一个没有逗号
};		// 最后是分号
int main(){
	enum Sex s = FEMALE;
    return 0;
}

字符串

由双引号引起的一串字符
存储:
	存储在数组中
	char a[]="abc";   // 实际上数组内是{'a','b','c',0} 最后的0实际是"\0"
	输出时  printf("%s\n",a);

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

转义字符

把原来的意思转变了
"\t" 水平制表符 "\n" 回车 "\v"垂直制表  "\ddd" ddd表示1-3个八进制数 "\xdd" dd表示2个十六进制数字

注释

//
/* */

函数

int Add(int x,int y){     //第一个int是函数的返回值类型
    int z = x + y;
    return z;
}
int main(){
    int a=2,b=3;
    c=Add(2,3); // 调用自定义函数
    return 0;
}

数组

int arr[10];
int arr2[3]={1,2,3};
// 数组下标从0开始  通过下标访问元素

操作符

算数运算符   + - * / %
移位操作符  <<  >>   左移  右移  移的是二进制位 //移完之后右边补0左边舍弃
位操作符    &  ^  |  按位与  按位异或   按位或
	异或:对应的二进制位相同为0  相异为1
a = a + 10  <=>  a += 10             (变量)+(运算符)+(=)+(值)
单目操作符	1个操作数
双目操作符	2个操作数
三目操作符	3个操作数
"&" 取地址符  
sizeof 操作数类型长度  
"~" 对二进制按位取反  
"--" 前置与后置的   
"++" 前置与后置的  
"(类型)" 强制类型转化
"\*" 间接访问提示符(解引种操作符)

对于sizeof

int arr[10]={0}; //10个整型元素的数组

printf("%d\n",sizeof(arr));//10*sizeof(int)=40

获取数组大小:sizeof(arr)/sizeof(arr[0]); //数组大小=数组的总大小/每个元素的大小

关系操作符  >  <  >=  <=  !=  ==
逻辑操作符  && ||    // 真:非0   假:0
条件操作符:(三目运算符)
	表达式1 ? 表达式2 : 表达式3;
下标引用操作符   []
函数引用操作符   ()

“&” “*” “.” “->” 这4个之后写

最高位是1是负数   最高位是0是正数
对于正数:原码、反码、补码相同
对于负数:原码:直接按照正负,写出的二进制序列
		-》反码:原码的符号位不变其他位取反
		-》补码:反码+1
只要是整数内存中存储的就是二进制的补码
		例:-2(假设是8位,实际上有32或64位)
			10000010  原码
			11111101  反码
			11111110  补码

关键字

register:

register int a = 10; //把a定义成寄存器变量(只是一个建议操作) 

signed 有符号

signed int ;  // signed一般省略
unsigned int num = 0; // 无符号数

struct 结构体关键字

union 联合体/共用体

typedef 类型定义

typedef unsigned int u_int;//类型重定义  将unsigned int重命名为u_int
unsigned int num1 = 20;//原来的定义方式可以使用
u_int num2 = 10;//其实就是相当于别名

关键字static

static int a = 1;// a是一个静态的局部变量

static修饰局部变量,局部变量的声明周期变长

static修饰全局变量,改变了变量的作用域–让静态的全局变量智能在自己的源文件内部使用

注:
外部写函数如 a.c中写的函数int Add(int x,int y)在b.c中调用时  
extern int Add(int,int);//声明方式,声明后可以理解为在a.c中自己定义函数

static修饰外部函数a.c中static int Add(int x, int y)在b.c中不能通过编译。也就是相当于改变了Add()的作用域

static修饰函数改变了函数的链接属性

#define 定义标识符、宏定义

#define MAX 100                   //将代码中的MAX替换为100
#define MAX(X,Y) (X>Y ? A : B)    //将代码中的MAX(X,Y)替换成(X>YA:B)

指针

关于地址
printf("%p \n",&a);//%p地址格式   &a取a的地址
int* p = &a;
//int*指针类型 
//p 指针变量 
//&a 取地址
使用
*p //解引用操作符
int a =10;  //在内存中存储10   还有char*等类型
int* p = &a;//定义指针,位置为a的内存
*p = 20;    //更改指针指向内存的 值
printf("a= %d",a);//结果为a=20
指针大小
指针在32位平台上是4个字节4×8=24,在64位平台上是8字节8×8=64

int* p的理解 p是int类型的一个指针(仅此而已),一般*p指向的也是一个int型的

结构体

描述复杂对象——我们自己创造出来的一个类型

struct Book{
    char name[20];
    short price;
};    //这里有个分号
//在main中使用
struct Book b1 = {"C语言程序设计",55};//创建一个该类型的结构体的变量

结构体变量的使用

printf("书名:%s",b1.name);
printf("价格:%d",b1.price);

重新赋值

#include <string.h>
strcpy(b1.name,"C++");   //因为name是一个数组不是一个变量
b1.price = 15;

指针定义

struct Book* pb = &b1;

使用指针

printf("%s\n",pd->name);//一般使用这种形式
printf("%d\n",(*pd).price);//这种比较啰嗦
// .  结构体变量.成员
// -> 结构体变量->成员

一、分支和循环

C语言是一门结构化的程序设计语言

1. 顺序结构
2. 选择结构
3. 循环结构
这三种结构就是生活中存在的三种结构。

顺序结构就是一句一句的向下执行

选择结构 --> 分支语句

  • if
  • switch

循环结构 --> 循环语句

  • while
  • do while
  • for

分支语句

if
// 种类一
if (条件){
	语句;
}

// 种类二
if (条件){
    语句;
}else{
    语句;
}

// 种类三
if (条件)
    语句;
else if(条件)
    语句;
else
    语句;
// 如果执行的语句比较多需要加上{}

可以嵌套使用 { }表示一个代码块–为了可读性比较强,一般建议加上 { }

悬空的else

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

结果为空 ;else的就近原则,所以(a==1)不正确代码就结束了。这个悬空的else和(b==2)是一组的。

switch----常用于多分支
switch(整型表达式){
        语句项;
}
//语句项是一些case语句
case 整型常量表达式:语句;

int main(){
    int nowDay=0;
    scanf("%d",&nowDay);
    switch(nowDay){  // nowDay必须是整型常量表达式float 1.0等都是不可以的
        case 1:    // case后面也必须是整型常量表达式 float 1.0等都是不可以的
            printf("现在是星期一\n");
            break;
        case 2:
            printf("现在是星期二\n");
            break;
        case 3:
            printf("现在是星期三\n");
            break;
        case 4:
            printf("现在是星期四\n");
            break;
        case 5:
            printf("现在是星期五\n");
            break;
        case 6:
            printf("现在是星期六\n");
            break;
        case 7:
            printf("现在是星期日\n");
            break;
    }
}

int main(){
    int nowDay=0;
    scanf("%d",&nowDay);
    switch(nowDay){  
        case 1:    
        case 2:
        case 3:
        case 4:
        case 5:
            printf("现在是工作日\n");
            break;
        case 6:
        case 7:
            printf("现在是休息日\n");
            break;  // 编程的好习惯最好是加上,之后的可塑性会很好
        default:   //不满足所有的条件时执行可以写在switch中的任何地方
            printf("输入错误\n");
            break;
    }
}

case只管进入 离开是由break管理的。在最后一个case语句后面加上break语句。

default可有可无,如果存在一般是处理非法的情况。

switch语句中只要没有break就一直向下执行。

循环语句

while
while(表达式为真){
    执行语句;
}
//在想要跳出去的情况可以使用break

插曲

int main(){
    int ch = 0;
    while((ch=getchar()) != EOF){  //getchar()在键盘中获取值,输入函数
        putchar(ch);			//putchar()在显示器打印值,输出函数
    }							//EOF是文件结束标志end of file  ->  -1
    return 0;
}

break

结束循环,终止循环中的所有的内容。

continue

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

for
// 在使用while的时候如果代码量比较大那么整体就比较乱
初始化内容
    ...
while(条件判断){
    执行语句
        ...
    调整语句
        ...
}
// for循环就诞生了
for(表达式1;表达式2;表达式3){
    循环语句;
}
表达式1为初始化部分,用于初始化循环变量,表达式2条件判断部分,用于判断循环什么时候终止。表达式3为调整部分,用于循环条件的调整。

for循环使用建议

不可以在for循环内修改循环变量,防止for循环失去控制。 可以写在表达式3中。

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

for循环的变化

for循环的三个表达式都可以省略,但是判断部分如果被省略,恒为真。

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

特点:先执行一遍循环语句。

练习
  1. 计算n的阶乘
int main(){
    int n;
    int res=1;//阶乘
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        res = res*i;
    }
    printf("res = %d",res);
}
  1. 计算1!+2!+3!+…+10!
int main(){
    int sum = 0;
    //控制10次和
    for(int n=1;n<=3;n++){
        //计算阶乘  每次算的时候应该res从0开始
        int res=1;//阶乘
        for(int i=1;i<=n;i++){
            res = res*i;
        }
        sum+=res;
    }
    printf("sum = %d",sum);
}
  1. 在一个有序数组中查找具体的某个数字n。编写int binsearch(int x,int v[],int n);功能:在v[0]<=v[1]<=v[2]<=…<=v[n-1]的数组中查找x。
//可以遍历  但是没有进步性  这里使用的是二分查找
int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int find = 7;
    int sz = sizeof(arr) / sizeof(arr[0]);
    int left = 0;			//左下标
    int right = sz - 1;		//右下标
    while (left <= right) {      //二分查找的条件
        int mid = (left + right) / 2;
        if (arr[mid] > find) {
            right = mid - 1;
        } else if (arr[mid] < find) {
            left = mid + 1;
        } else {
            printf("找到了,下标是:%d", mid);
            break;    
        }
    }
    if (left>right){     // 如果因为没有跳出来的情况是这样的
        printf("找不到啊!\n");
    }
    return 0;
}
  1. 编写代码,演示多个字符从两端移动,并向中间汇聚。
//举例打印hello
//#####
//h###o
//he#lo
//hello
#include <string.h>
#include <windows.h>
#include <stdlib.h>

int main() {
    char arr1[] = "hello world!";   //后期可以改成键盘输入
    char arr2[] = "************";   //后期可以通过计算arr1的长度然后生成一个这样的字符串
    int left = 0;
    int right = sizeof(arr1) / sizeof(arr1[0]) - 2;  // 因为获取到的是长度而且最后还有一个“\0”的结束标志,所以在这里要减2
    // int right = strlen(arr1)-1;    //使用函数获取长度  这时仅仅需要-1就可以了
    while (left <= right) {
        arr2[left] = arr1[left];
        arr2[right] = arr1[right];
        printf("%s \n", arr2);
        Sleep(500);    //休息一秒
        system("cls");       //#include <stdlib.h>中的函数,执行系统命令"cls"
        left++;
        right--;
    }
    return 0;
}
  1. 编写代码实现,模拟登录情景,并且只能登录三次(只允许输入三次密码,如果密码正确则提示登录,如果三次输入错误,则退出程序)
#include <string.h>
int main() {
    int i = 3;
    char password[20] = {0};
    for (i = 0; i < 3; i++) {
        printf("请输入密码:");
        scanf("%s", password);
        if (strcmp(password, "123456") == 0) {   
            //双等号不能用来比较两个字符串相等,应该使用库函数-strcmp  如果相同返回值是0
            //如果左边大于右边返回大于0的数,小于返回小于0的数,等于返回0
            printf("登录成功\n");
            break;
        } else{
            printf("密码错误!\n");
        }
    }
    if (i == 3) {
        printf("三次密码输入错误,退出程序");
    }
    return 0;

goto语句

int main(){
    printf("hhh");
    goto A;
    printf("hhh123");
    A:
    printf("xxx");
    return 0;
}

goto语句一般用于跳出深层循环

一个关机程序

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

int main() {
    char input[20] = {0};
    //shutdown -s -t 60
    //system()- 执行系统命令的函数
    system("shutdown -s -t 60");    //需要#include <stdlib.h>
    again:
    printf("请注意,你的电脑在1分钟内关机。在这输入\"I'm a pig.\"我可以取消关机\nplease input:");
    scanf("%s", input);
    //C语言不能直接进行等于,需要借助函数
    if (strcmp(input, "I'm a pig.")) {//需要#include <string.h>
        system("shutdown -a");
    } else {
        goto again;
    }
    return 0;
}
//可以不适用goto实现
int main() {
    char input[20] = {0};
    //shutdown -s -t 60
    //system()- 执行系统命令的函数
    system("shutdown -s -t 60");   
    while (1) {
        printf("请注意,你的电脑在1分钟内关机。在这输入\"I'm a pig.\"我可以取消关机\nplease input:");
        scanf("%s", input);
        if (strcmp(input, "I'm a pig.")) {
            system("shutdown -a");
            break;
        }
    }
    return 0;
}

分支循环常见编程题

找大小

三数按照大小排序

int main(){
    int a;
    int b;
    int c;
    scanf("%d %d %d",&a,&b,&c);
    if (a<b){
        int temp = a;
        a = b;
        b = 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);
}
判断闰年
int main(){
    int year = 0;
    int count = 0;
    for(year=1000;year<=2000;year++){
        //判断year是否是闰年
        //1. 能被4整除但是不能被100整除是闰年
        //2. 能被400整除的是闰年
        if(((year%4==0)&&(year%100!=0))||(year%400==0)){
            printf("%d ",year);
            count++;
        }
    }
    printf("\n count = %d",count);
    return 0;
}
素数判断
#include <math.h>
int main() {
    int i = 0;
    int count = 0;
    for (i = 100; i < 200; i++) {
        //判断i是否为素数
        //试除法
        int j = 0;
        for ( j = 2; j < sqrt(i); j++) {
            if(i%j==0){
                break;  // 说明不是
            }
        }
        if (j > sqrt(i)){
            printf("%d ",i);
            count++;
        }
    }
    printf("\ncount = %d",count);
    return 0;
}

还可以将将计算的数从101开始,然后每次运算加2(只计算奇数)。

1-100中7出现的次数
int main(){
    int i = 0;
    int count = 0;
    for (i = 1;i<=100;i++){
        if (i%10==7){        //个位出现9
            count++;
        }
        if (i/10==7){        //十位出现9
            count++;
        }
    }
    printf("count=%d",count);
    return 0;
}
计算最大公因数

辗转相除法(两个数)

int main(){
	int m = 24;
	int n = 18;
	int r = 0;
	while(m%n){
		r = m % n;
		m = n;
		n = r;
	}
	printf("%d\n",n);
	return 0;
}

求n个数的最大公约数。2<=n<50

输入:n个正整数,以0作为数的结束。用空格隔开。

输出:最大公约数和这n个数,用一个空格隔开。

样例:

210 54 24 0

6 210 54 24

int main() {
   //1.输入
   int a[51] = {1};    //最多50个所以使用51
   int n = 1;     //记录长度数组  至少要有一位是0
   while (a[n - 1] != 0) {    // 0是最后一位
       scanf("%d", &a[n]);
       n = n + 1;
   }  // n-2是输入的个数

   // 2. 计算  使用辗转相除法   先算1和2 结果和3算 结果再和4算 以此类推  得到的就是多个数的最大公约数
   //(1) 固定左边的值   (2) 一个值一个值的向右移动  (3)终止条件是:所有的值都算完了
   int left = a[1], right = a[2];
   for (int i = 2; i <= n - 2; i++) {
       int midNum;
       //辗转相除法过程
       while (right != 0) {
           midNum = left % right;
           left = right;
           right = midNum;
       }
//        一次公约数计算完成,下一次计算时左值就是现在的left右值就是a[i+1]
       left = left;
       right = a[i];
   }
   printf("%d ",left);
   // 3.处理输出
   int m = n - 1;   //n加上了最后的0实际上使用的时候应该先减回来。
   while (m > 1) {
       printf("%d ", a[n - m]);
       m--;
   }
}
最小公倍数

求n个数的最小公倍数,2<=n<10。

输入:n个正整数,以0作为数的结束。用空格隔开。

输出:最小公倍数和这n个数,用一个空格隔开。

样例:

210 54 24 0

7560 210 54 24

int main() {
   //1.输入
   int a[11] = {-1};    //最多10个所以使用11
   int n = 1;     //记录长度数组  至少要有一位是0
   while (a[n - 1] != 0) {    // 0是最后一位
       scanf("%d", &a[n]);
       n = n + 1;
       if (a[0] < a[n - 1]) {
           a[0] = a[n - 1];       //将第一个值存为最大的值
       }
   }  // n-2是输入的个数

   // 2. 计算  最小公倍数   先算1和2 结果和3算 结果再和4算 以此类推  得到的就是多个数的最大公约数
   //(1) 固定左边的值   (2) 一个值一个值的向右移动  (3)终止条件是:所有的值都算完了
   int left = a[0], right = a[1];
   int timesNum = left;
   for (int i = 1; i <= n - 1; i++) {
         //最小公倍数
       //计算最小公倍数
       while (timesNum % left != 0 || timesNum % right != 0) {
           timesNum+=1;
       }
   //    一次公约数计算完成,下一次计算时左值就是现在的left右值就是a[i+1]
       left = timesNum;
       right = a[i];
   }
   printf("%d ", timesNum);
   // 3.处理输出
   int m = n - 1;   //n加上了最后的0实际上使用的时候应该先减回来。
   while (m > 1) {
       printf("%d ", a[n - m]);
       m--;
   }
}
分数求和

计算1/1-1/2+1/3-1/4+1/5…+1/99-1/100的值,打印出结果

int main() {
    int i;
    double sum = 0.0;
    int flag = 1;
    for (i = 1; i <= 100; i++) {
        sum += flag * 1.0 / i;
        flag = -flag;
    }
    printf("%lf\n", sum);
    return 0;
}
求最大值

求10个数的最大值

int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int max = a[0];//最大值   这个地方不要用自定义的值,因为有可能自定义的值为最大值
    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (i = 1; i < sz; i++) {  //因为arr[0]为最开始的max所以从1开始就可以了
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    printf("max = %d\n", max);
    return 0;
}
乘法口诀表
int main(){
    int i = 0;
    //打印九行
    for ( i = 1; i <= 9; i++) {
        //打印其中一行
        int j;
        for(j=1;j<=i;j++){
            printf("%d*%d=%-2d ",i,j,i*j);//-2左对齐 占两位
        }
        printf("\n");
    }
}
二分查找

在一个有序数组中查找具体的某个数字n。编写int binsearch(int x,int v[],int n);功能:在v[0]<=v[1]<=v[2]<=…<=v[n-1]的数组中查找x。

//可以遍历  但是没有进步性  这里使用的是二分查找
int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int find = 7;
    int sz = sizeof(arr) / sizeof(arr[0]);
    int left = 0;			//左下标
    int right = sz - 1;		//右下标
    while (left <= right) {      //二分查找的条件
        int mid = (left + right) / 2;
        if (arr[mid] > find) {
            right = mid - 1;
        } else if (arr[mid] < find) {
            left = mid + 1;
        } else {
            printf("找到了,下标是:%d", mid);
            break;    
        }
    }
    if (left>right){     // 如果因为没有跳出来的情况是这样的
        printf("找不到啊!\n");
    }
    return 0;
}
猜数字游戏

需求:

  1. 电脑会生成一个随机数

  2. 猜数字

  3. 重开

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

int game(){
    //生成随机数
    int randNum = 0;
    int guessNum = 0;//存储猜的数字
    //用时间戳来设置随机数的起点
    // srand((unsigned int)time(NULL));  //这个应该写在外面 一个程序设置一次就可以了
    randNum = rand()%100+1;//生成随机数  1-100   这个函数本身生成的是0-32767
    // printf("%d",randNum);
    //2.猜数字
    while (1){
        printf("你猜的数字是:");
        scanf("%d", &guessNum);
        if(guessNum>randNum){
            printf("猜大了\n");
        }else if(guessNum<randNum){
            printf("猜小了\n");
        }else{
            printf("恭喜你,猜对了\n");
            break;
        }
    }
    return 0;
}
int main(){
    int input = 0;
    //用时间戳来设置随机数的起点
    srand((unsigned int)time(NULL));
    do {
        printf("#############################################\n");
        printf("########  1.开始游戏     2.退出游戏    ########\n");
        printf("#############################################\n");
        printf("->请选择:");
        scanf("%d",&input);
        switch (input) {
            case 1:
                game();
                // 猜数字
                break;
            case 2:
                printf("退出游戏\n");
                break;
            default:
                printf("游戏结束,再见!\n");
                break;
        }
    } while (input);
    return 0;
}

二、C语言数组

数组

  • 一维数组的创建和初始化
  • 一维数组的使用
  • 一维数组在内存中的存储
  • 二维数组的创建和初始化
  • 二维数组的使用
  • 二维数组在内存中的存储
  • 数组作为函数参数

一维数组的创建和初始化

数组的创建

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

数组中元素的种类 数组名[数组的大小]={初始化};
int arr[12];

数组可以不初始化。数组的大小必须是一个常量。

//正确的创建方式
int arr[10]={0};
char arr2[5];
//错误的创建方式
int n = 4;
int arr3[n];// 必须是常量
数组的初始化

数组的初始化是指,在创建数组的同时给数组赋值。

int arr[10] = {1,2,3};//不完全初始化,没有初始化的部分默认为0
char arr2[5] = {'a','b'};// 存储为a b 0 0 0
char arr3[5] = "ab";// 这种初始化存储为 a b \0 0 0
//上面的两个存储结果看起来一样,使用的时候有差异
char arr4[] = "abcdef";//定义数组长度,但是给了初始化值的默认数组长度为长度加1,有'\0'

int arr5[5] = {1,2,3,4,5};
char arr6[3] = {'a',98,'b'};  //完全初始化
char arr7[] = {'a','b''c};
#include <string.h>
char arr4[] = "abcdef";  //   数组长这样  [a][b][c][d][e][f][\0]
printf("%d\n",sizeof(arr4)); //结果是7    一个char是一个字节
printf("%d\n",strlen(arr4)); //结果是6
char arr1[] = "abc";
char arr2[] = {'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)); //随机值  因为没有\0所以不知道后面有没有东西

char arr1[] = "a\0c";
char arr2[] = {'a','\0','c'};
printf("%d\n",sizeof(arr1));  	// 4
printf("%d\n",sizeof(arr2));	// 3
printf("%d\n",strlen(arr1));	// 1
printf("%d\n",strlen(arr2));	// 1

小插曲

strlen 和 sizeof

strlen是求字符串长度的,只针对字符串求长度,是一个库函数,需要引用头文件

strlen计算的是‘\0’之前有多少位

sizeof是计算变量,数组,类型的大小–单位是字节 - 这是一个操作符

数组的使用

通过[ ]使用下标来访问 [ ]下标引用操作符

下标从0开始

#include <s>
char arr[] = "abcdef";  //   数组长这样  [a][b][c][d][e][f][\0]
printf("%c\n",arr[3]); // d
int len = strlen(arr);
for (int i=0;i<len;i++){
    printf("%c ",arr[i]);
}
//字符串有strlen对于int型的数组应该怎么办呢
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int len = sizeof(arr)/sizeof(arr[0]);
for (int i=0;i<len;i++){
    printf("%d ",arr[i]);
}

一维数组在内存中的存储

int arr[] = {1,2,3,4,5,6,7,8,9,0};
int len = sizeof(arr)/sizeof(arr[0]);
for (int i=0;i<len;i++){
    printf("%p ",&arr[i]);
}
//000000000061FDF0 000000000061FDF4 000000000061FDF8 000000000061FDFC 000000000061FE00 000000000061FE04 000000000061FE08 000000000061FE0C 000000000061FE10 000000000061FE14
int4字节
观察可得是 连续的空间

数组在内存中是连续存放的。

二维数组的创建和初始化

二维数组的创建
int arr1[3][4];//三行四列
/*[int][int][int][int]*/
/*[int][int][int][int]*/
/*[int][int][int][int]*/
//我的理解:  先创建arr[4]是一个元素,存储在arr[3]中
char arr2[3][5];
double arr3[3][5];
image-20211228152049507
二维数组的初始化
int arr1[3][4] = {1,2,3,4,5};

自动填充到下一行

image-20211228153014057
char arr2[3][5]={{1,2},{4,5}};
image-20211228153225834
double arr3[][4]={{2,3},{4,5}};

行可以省略,列不能省略否则会报错

image-20211228153554986

二维数组的使用

二维数组的使用依然是通过下标访问

下标|下标012
0
1
2
3
int main(){
    int arr[3][4] = {{1,2,3},{4,5}};
    for(int i = 0;i<3;i++){
        for(int j = 0;j<4;j++){
            printf("%d ",arr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kVOHaHE0-1645521025989)(C语言数组.assets/image-20211228154512359.png)]

二维数组在内存中的存储

int main(){
    int arr[3][3] = {{1, 2, 3}, {4, 5}};
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%p ", &arr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

并没有那么复杂,其实就是像一维数组一样连续的。

image-20211228155116780

二维数组就是由一维数组组成的数组

在内存中二维数组是连续的空间

image-20211228160031396

数组作为函数参数

可以将整个数组作为参数传入函数。如排序的时候。

冒泡排序

思路:1、比较相邻元素大小;2、按一定顺序交换两个元素(由小到大、由大到小)3,不重复的彻底比较全部元素·,每一次比较后整体的有序度增加,完全比较后整个序列就排序成功。

n个元素n-1趟大循环,第k趟小循环交换(判断)k-1次。

void bubble_sort(int arr[],int sz){
    //这里不能进行计算,传进来的是首元素的地址
    // int sz =sizeof(arr)/sizeof(arr[0]);
    for(int i = 0; i<sz-1;i++){
        //每一次的冒泡
        for(int 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;
            }
        }
    }
}
int main(){
    int arr[] = {9,8,7,6,5,4,3,2,1,0};
    int sz = sizeof(arr)/ sizeof(arr[0]);
    //对arr进行升序排序
    // arr是数组,我们对数组arr进行传参,实际上传递过去的是数组arr首元素的地址&arr[0]
    bubble_sort(arr,sz);//冒泡排序函数
    //打印排序后的数组
    for (int i = 0; i < sz; ++i) {
        printf("%d ",arr[i]);
    }
    return 0;
}

数组作为形式参数被接受时,实际上收到的只是第一元素arr[0]。

想要获取数组的大小只能在自定义函数的外部进行计算再传到自定义函数中。

想要获取数组的大小只能在自定义函数的外部进行计算再传到自定义函数中。

想要获取数组的大小只能在自定义函数的外部进行计算再传到自定义函数中。

冒泡排序的优化

添加是否有序的标记,如果已经排序完成则直接跳出循环。

void bubble_sort(int arr[],int sz){
    for(int i = 0; i<sz-1;i++){
        //定义标记
        int isSort = 1;
        //每一次的冒泡
        for(int 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;
                isSort = 0;
            }
        }
        if (isSort == 1) break;		//最后的判断
    }
}
数组名是什么?
int a[]={1,2,3,4};
printf("%p\n",a);
printf("%p\n",&a[0]);
printf("%d\n",*a);

可以发现前两个printf得到的是一个值,第三个printf得到的是数组的首元素。

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

int a[]={1,2,3,4}
1. sizeof(a),计算的是整个数组的大小,单位是字节。
2. &a,代表整个数组。  ** &数组名代表整个数组**
int a[]={1,2,3,4};
printf("%p\n",a);
printf("%p\n",a+1);
printf("%p\n",&a[0]);
printf("%p\n",&a[0]+1);
printf("%p\n",&a);
printf("%p\n",&a+1);
printf("%d\n",*a);
image-20211228235627614

&a,代表整个数组。 ** &数组名代表整个数组** (原因是第三个红色框的两地址差是一个数组的总大小)

数组习题

第一题

创建一个整形数组,完成对数组的操作
1.实现函数Init()初始化组为全0
2.实现 print()打印数组的每个元素
3.实现reverse()函数完成数组元素的逆置

//初始化
void Init(int arr[], int sz) {
    for (int i = 0; i < sz; ++i) {
        arr[i] = 0;
    }
}
//打印
void Print(int arr[], int sz) {
    for (int i = 0; i < sz; ++i) {
        printf("%d ", arr[i]);
    }
}
//y
void Reverse(int arr[], int sz) {
    int left = 0;
    int right = sz - 1;
    int temp;
    while (left < right) {
        temp = arr[left];
        arr[left] = arr[right];
        arr[right] = temp;
        left++;
        right--;
    }
}

int main() {
    int arr[10];
    Init(arr, sizeof(arr) / sizeof(arr[0]));//初始化
    Print(arr, sizeof(arr) / sizeof(arr[0]));//打印
    printf("\n");
    int arr2[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    Reverse(arr2, sizeof(arr2) / sizeof(arr2[0]));
    Print(arr2, sizeof(arr2) / sizeof(arr2[0]));
    return 0;
}

image-20220104000513687

第二题

将数组A中的内容和数组B中的内容进行交换。(两个数组一样大)。

void Print(int arr[], int sz) {
    for (int i = 0; i < sz; ++i) {
        printf("%d ", arr[i]);
    }
}
void Change(int* arr1,int* arr2,int sz){
    int temp;
    for (int i = 0; i < sz; ++i) {
        temp = arr1[i];
        arr1[i] = arr2[i];
        arr2[i] = temp;
    }
}
int main(){
    int arr1[]={1,3,5,7,9};
    int arr2[] = {2,4,6,8,0};
    int sz = sizeof(arr1)/ sizeof(arr1[0]);
    Change(arr1,arr2,sz);
    Print(arr1,sz);
    printf("\n");
    Print(arr2,sz);
}

三、函数

  • 函数是什么
  • 库函数
  • 函数的参数
  • 函数的调用
  • 函数的嵌套调用和链式调用
  • 函数的声明和定义
  • 函数递归

函数是什么

具有某种功能的代码块
一般有返回值,提供对过程的封装和细节的隐藏。

分类

库函数

自定义函数

库函数

www.cplusplus.com

http://en.cppreference.com

频繁使用的功能就被写成了库函数

C语言常见的库函数

  • IO函数
  • 字符串操作函数
  • 内存操作函数
  • 时间/日期函数
  • 数学函数
  • 其他库函数

**注:**使用库函数之前需要#include 对应的头文件

自定义函数

比库函数重要自己完成自己需要的功能

函数的组成

返回类型 函数名(){
	语句内容;
	return 返回值;//有的没有
}
/*两数相加*/
int add(int x, int y){
    int z = 0;
    z = x+y;
    return z;
}
/*两数的较大值*/
int get_max(int x, int y){
    return (x>y)?x:y;
}
/*交换两值*/
void Swap(int *x, int *y) {
    int temp = 0;//中间变量
    temp = *x;
    *x = *y;
    *y = temp;
}

int main() {
    int a = 10;
    int b = 20;
    Swap(&a, &b);//这里必须传地址否则不能对a和b进行操作
}

这里如果不使用指针的话相当于重新定义了两个值,从而不是对a和b进行操作。

函数的参数

实参(实际参数)

真实的传给函数的参数是实参。实参必须是确定的量。实参是传给形参的。

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

形参(形式参数)

在自定义函数时定义的参数,即在函数名后面的()中的参数是形参。形参只在自定义函数中生效。形参只有在函数被调用的时候才会实例化。

形参其实是实参的一份临时拷贝,对形参的修改不会改变实参的。所以要对值操作的时候需要传的是变量的地址
传值调用

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

传址调用
  1. 传址调用是把函数外面创建的变量的内存地址传递给函数参数的一种调用方式。
  2. 这种方式让函数与外部的变量建立起真正的联系,也就是函数内部也可以直接操作函数外部的变量。

练习

  1. 写一个函数判断一个数是不是素数。
#include "math.h"
int is_prime(int n){
    for (int i = 2; i < sqrt(n); ++i) {
        if(n%i==0) return 0;
    }
    return 1;
}
int main(){
    //100-200之间的素数
    for (int i = 100; i < 200; ++i) {
        //判断是否是素数
        if (is_prime(i)==1) printf("%d ",i);
    }
}
  1. 写一个函数判断一年是不是闰年。
int is_leap_year(int year){
    if ((year%4==0&&year%100!=0)||(year%400==0)) return 1;
    else return 0;
}
int main(){
    for (int year = 1000; year <= 2000; ++year) {
        //判断是否是闰年
        if (1 == is_leap_year(year)) printf("%d \n",year);
    }
    return 0;
}
  1. 写一个函数实现对一个整形有序数组的二分查找。
                    // ###实际上这里的arr是一个指针
int binary_search(int arr[], int find, int sz) {
    int left = 0;
    int right = sz - 1; //长度减一
    while (left <= right) {
        int mid = (left + right) / 2;
        if (arr[mid] < find) left = mid + 1;
        else if (arr[mid] > find) right = mid - 1;
        else return mid;
    }
    return -1;
}
int main() {
    //在一个有序数组中查找具体的某个值,找到返回这个值的下标没找到返回-1;
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int find = 7;
    int sz = sizeof(arr) / sizeof(arr[0]);
                    //这里传递数组传递过去的是首元素的地址
    int res = binary_search(arr, find, sz);
    if (res == -1) printf("找不到指定的数字\n");
    else printf("找到了,下标是:%d", res);
    return 0;
}
  1. 写一个函数,每调用一次这个函数就会将num的值加一。
void UpNum(int* p){
    *p+=1;
}
int main(){
    int num = 0;
    UpNum(&num);
    printf("%d ",num);
    UpNum(&num);
    printf("%d ",num);
    UpNum(&num);
    printf("%d ",num);
    return 0;
}

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

函数和函数之间可以结合使用。

嵌套使用

函数套用函数

void print_new_line(){
    printf("hehe\n");
}
void print_three_line(){
    for (int i = 0; i < 3; ++i) new_line();
}
int main(){
    three_line();
    return 0;
}
链式访问

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

printf("%d \n",strlen("abcdef"));
int main(){
    char a[20] = "hello";
    //链式访问
    int res = strlen(strcat(a,"world"));//strcat()连接字符串
    printf("%d\n",res);
    return 0;
}

函数声明

  1. 告诉你的编译器有个函数(函数名,参数,返回值类型)。但是存在与否不重要了。
  2. 函数的声明是出现在函数使用之前的,要满足先声明后使用
  3. 函数的声明一般是在头文件中。

当函数的定义在main函数之后的时候直接编译是不能通过的。这时就需要函数的声明了。

//函数的声明
int Add(int, int);
int main(){
    int a=20,b=30;
    int sum = 0;
    sum = Add(a,b);
    printf("%d",sum);
    return 0;
}
//函数的定义
int Add(int x,int y){
    int z = x+y;
    return z;
}

在做开发的时候函数的定义在自定义.c文件中。函数的声明写在头文件.h文件中。

以下是文件中的存放即代码示例

add.h中

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

add.c中 //功能模块

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

main.c中//主模块

#include <stdio.h>
#include "add.h"  	//引用库函数用的是<>,引用自定义的头文件用的是""
int main(){
    int a=20,b=30;
    int sum = 0;
    sum = Add(a,b);
    printf("%d",sum);
    return 0;
}

分模块写代码效率高

.h文件放置函数的声明。.c文件放置函数具体实现的功能。

函数的递归

程序调用自身的编程技巧叫递归。通常用来将大型复杂的问题转化为与原问题相似的规模较小的问题来求解。大大减少程序的代码量。递归主要思考方式是大事化小事。

递归的两个必要条件
  1. 一定要存在限制条件,满足这个限制条件的时候,递归就不再进行了。
  2. 每次递归之后越来越接近这个限制条件。
一个简单的递归函数
int main(){
    printf("hehehe\n");
    main();
    return 0;
}
递归常见的问题----->>>>栈溢出

image-20211229125005759

在每一次函数调用时候都要在栈区申请内存空间。eg:上面函数执行main()时申请一次,执行printf()申请一次,main()又申请一次。当申请到栈区耗干的时候。抛出错误:stack overflow(栈溢出了)。

栈溢出的原因一般是因为没有终止条件。

https://stackoverflow.com/ (程序员的知乎)。

通过练习理解递归

练习1:

接受一个整型值(无符号),按照顺序打印它的每一位。

例如:输入:1234 输出:1 2 3 4.

void print_number(int n){
    /*思路:print_number(123)+printf(4)
     *     print_number(12)+printf(3)*
     *     print_number(1)+printf(2)*
     *     printf(1)*/
    if (n>=10) print_number(n/10);  //-------->>>>>终止条件
    printf("%d ",n%10);				//-------->>>>>每一次要执行的功能
}
int main(){
    unsigned int num = 0;
    scanf("%d",&num);
    print_number(num);//递归函数
    return 0;
}

练习2:

编写一个函数,求字符串长度。

要求:不建立新的内存空间。

/*  ******错误示范  错误示范  错误示范******  */
int my_strlen(char* str)    //这里接受第一个元素的地址
{
    int len  = 0;
    while (*str != '\0'){
        len++;
        str++;
    }
    return len;
}
int main(){
    char arr[] = "hello";
    int len = my_strlen(arr);//这里传参数传的是第一个元素的地址
    printf("strlen = %d",len);
    return 0;
}
/*  ************  正  解  ************    */
int my_strlen(char* str)    //这里接受第一个元素的地址
{
    if (*str!='\0')
        return 1+ my_strlen(str+1);
    return 0;
}
int main(){
    char arr[] = "hello";
    int len = my_strlen(arr);//这里传参数传的是第一个元素的地址
    printf("strlen = %d",len);
    return 0;
}
递归的两个必要条件
    • 一定要存在限制条件,满足这个限制条件的时候,递归就不再进行了。
    • 每次递归之后越来越接近这个限制条件。

递归与迭代

练习3:

求n的阶乘。

int Facl(int n){
    int res = 1;
    for (int i = 1; i <= n; ++i) {
        res *=i;
    }
    return res;
}
int Facl(int n){
    if (n<=1) return 1;
    else return n* Facl(n-1);
}
int main(){
    int n = 0;
    int res = 0;
    scanf("%d",&n);
    res = Facl(n);
    printf("%d\n",res);
    return 0;
}

练习4 :

求第n个斐波那契数

注:斐波那契数列:1 1 2 3 5 8 13 21 34 55 … 第n项的值为前两项之和。

int Fib(int n){
    if (n<=2) return 1;
    else return Fib(n-1)+ Fib(n-2);
}
int main(){
    int n;
    scanf("%d",&n);
    int res = Fib(n);
    printf("%d",res);
    return 0;
}

斐波那契数列的优化。

		优化思想:
# 每次的重复计算太多这样就想着把他存起来。
# 每次计算其实只需要n-2和n-1项,我们将其叫为first,second
这样计算斐波那契数就是first+second
int Fib(int first,int second,int n){    //---->>>需要修改参数的量
    if (n<=2) return 1;
    if (n==3) return first+second;
    else return Fib(second,first+second,n-1);
}
int main(){
    int n;
    scanf("%d",&n);
    int res = Fib(1,1,n);
    printf("%d",res);
    return 0;
}

注:TTD开发方式:测试驱动开发–先看整体的流程。再开发细节的功能。

递归和迭代

无论使用什么方式写代码异地昂要正确。问题就是有可能会出现栈溢出。

四、阶段实践

三子棋

这个项目是B站鹏哥C语言的课程中。数组部分的实战代码。

博主只是跟着学跟着写的。

整体其实并不是很复杂,将最复杂的game模块进行了独立编写,所以出现了game.c和game.h。

刚入手时需要注意的是一定不要想这个项目有多复杂。

编写步骤
  1. 理思路(整体流程)

项目进入需要的界面。+ 功能选择。 将这些写完再考虑下一步。

功能选择: 进入游戏 || 退出游戏 || 输入错误。

  1. 游戏准备 (将以下出现的小模块都函数来写。)

    (1) 需要棋盘,+ 初始化棋盘。 -->二维数组

    (2) 棋盘的显示。

  2. 游戏开始

    (1) 玩家下棋

    (2) 电脑下棋

    (3)每次下完展示棋盘

  3. 结果判断

    (1) 玩家赢返回"*"

    (2) 电脑赢返回"#"

    (3) 平局返回"Q"

    (4) 继续返回"C"

  4. 整理、解决问题(debug)

三子棋代码

以下是我通过调试的代码。(环境:win10,编译器:Clion)

运行文件main.c
//
// Created by Tian_baby on 2021/12/29.
//

#include "game.h"
//打印初识目录界面
void menu(){
    printf("*****************************\n");
    printf("****  1. play   0. exit  ****\n");
    printf("*****************************\n");
}

//游戏的实现
void game(){
    // 定义数组来存储棋盘
    char board[ROW][COL] = {0};
    char res = 0;
    //初始化棋盘都是空格
    InitBoard(board,ROW,COL);
    //打印棋盘
    DisplayBoard(board,ROW,COL);
    while(1){
        PlayerMove(board,ROW,COL);//玩家下棋
        DisplayBoard(board,ROW,COL);//展示棋盘情况
        res = JudgeWin(board,ROW,COL);
        if(res!='C') break; //结果不等于C代表结束了
        ComputerMove(board,ROW,COL);// 电脑下棋
        DisplayBoard(board,ROW,COL);//展示棋盘的情况
        res = JudgeWin(board,ROW,COL);
        if(res!='C') break;
    }
    if(res == '*') printf("玩家赢。 \n");
    else if(res == '#') printf("电脑。 \n");
    else printf("平局了");

}

// 游戏流程
void test(){
    int input = 0;//功能选择输入项
    // 随机数
    srand((unsigned int)time(NULL));
    // 控制游戏的重复
    do {
        menu();
        printf("请选择: ");
        scanf( "%d",&input);
        switch (input) {
            case 1:
                printf("\n********    三 子 棋   ********\n");
                game();//游戏内容
                break;
            case 0:
                printf("退出游戏\n");
                break;
            default:
                printf("输入错误,请重新输入\n");
                break;
        }
    } while (input);
}

// 主函数
int main() {
    test();
    return 0;
}

在此文件中,test()是整体的流程控制。test()中的srand()是因为要使用rand()函数所以提前准备使用的;menu()是进入功能界面的函数;game()是游戏最主要的内容,其内容写在另一个game.c文件中(后面详细解说)。

game.c的头文件game.h
//
// Created by Tian_baby on 2021/12/29.
//

#ifndef ARR_GAME_GAME_H
#define ARR_GAME_GAME_H
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

#define ROW 3
#define COL 3
/* 函数的声明 */
//棋盘的初始化
void InitBoard(char board[ROW][COL],int row,int col);

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

// 玩家下棋
void PlayerMove(char board[ROW][COL],int row,int col);

// 电脑下棋
void ComputerMove(char board[ROW][COL],int row,int col);

// 判断输赢
char JudgeWin(char board[ROW][COL],int row,int col);
//在game中还有一个函数IsFull()是在game.c中自己使用了所以可以不在这里声明。
#endif //ARR_GAME_GAME_H

该文件主要是对game()中函数使用前的声明。

功能文件game.c
//
// Created by Tian_baby on 2021/12/29.
//

#include "game.h"

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

// 展示棋盘
void DisplayBoard(char board[ROW][COL], int row, int col) {
    for (int i = 0; i < row; i++) {
        // 打印一行数据
        for (int j = 0; j < col; ++j) {
            if (j == col - 1) printf(" %c ", board[i][j]);
            else printf(" %c |", board[i][j]);
        }
        // 打印分隔行
        printf("\n");
        if (i < row - 1) {
            for (int j = 0; j < col; ++j) {
                (j < col - 1) ? printf("---|") : printf("---");
            }
        };
        printf("\n");
    }
}

// 玩家下棋
void PlayerMove(char board[ROW][COL], int row, int col) {
    int x = 0, y = 0;
    printf("玩家下棋-->>\n");
    printf("请输入你要走的位置:\n");
    while (1) {
        scanf("%d %d", &x, &y);
        // 判断用户输入的合法性
        if (x >= 1 && x <= row && y >= 1 && y <= col) {
            if (board[x - 1][y - 1] == ' ') {
                board[x - 1][y - 1] = '*';
                break;
            } else {
                printf("该坐标被占用,请重新输入。\n");
            }

        } else {
            printf("坐标不存在,请重新输入。");
        }
    }
}

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

//判断是否填满   返回一棋盘慢了  返回0棋盘没有满
int IsFull(char board[ROW][COL],int row, int col){
    for (int i = 0; i < ROW; ++i) {
        for (int j = 0; j < COL; ++j) {
            if (board[i][j]==' ') return 0;
        }
    }
    return 1;
}

// 判断输赢
char JudgeWin(char board[ROW][COL], int row, int col) {
    // 这个函数返回四种状态--玩家赢"*"  电脑赢"#"  平局"Q"  继续"C"
    // 行相等
    for (int i = 0; i < row; ++i) {
        if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][0] != ' ')
            return board[i][0];
    }
    // 列相等
    for (int i = 0; i < col; ++i) {
        if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ')
            return board[0][i];
    }
    //斜线相等
    if(board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[1][1] != ' ')
        return board[1][1];
    if(board[2][0] == board[1][1] && board[1][1] == board[0][2] && board[1][1] != ' ')
        return board[1][1];
    // same_count = 0;
    // for (int i = 0; i < col-1; ++i) {
    //     if (board[i][i] == board[i+1][i+1]&&board[i][i] != ' ')
    //         same_count+=1;
    // }  //-->>这是我自己想的另一种思路  就是判断总次数如果斜线上的所有的值都相等
    //          那么same_count的值应该==ROW-1

    //是否平局
    if(1 == IsFull(board,ROW, COL)) return 'Q';
    return 'C';
}

棋盘的初始化InitBoard( ); 打印棋盘DisplayBoard( ); 玩家下棋PlayerMove( ); 电脑下棋ComputerMove( ); 判断输赢JudgeWin()

C语言实现扫雷

这是一个经典的项目

思路及步骤

  1. 基本流程

游戏进入界面–>进入选项–>进入游戏

  1. 游戏内容及准备

游戏使用棋盘为9*9。

存有雷的棋盘(有雷是1) >>> 数组11*11(加一圈好计算)

展示周围雷数的棋盘 (因为有可能出现1所以需要新的)>>> 数组11*11(这个是为了和存有雷的棋盘相对应)

初始化棋盘
void InitBoard(char board[ROWS][COLS],int rows,int cols,char full){
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            board[i][j] = full;//填充传过来的要求字符。
        }
    }
}
展示棋盘
void DisplayBoard(char board[ROWS][COLS],int row,int col){
    printf("\n");
    //打印列号
    for (int i = 0; i <= col; ++i) {
        printf(" %d ",i);
    }
    printf("\n");
    for (int i = 1; i <= row; ++i) {
        printf(" %d ",i);
        for (int j = 1; j <= col; ++j) {
            printf(" %c ",board[i][j]);
        }
        printf("\n");
    }
}
布置雷
void SetMine(char board[ROWS][COLS],int row,int col){
    int count = EASY_COUNT;//雷的总个数
    while(count){
        int x = rand()%row+1;// 我们需要的是 1-9
        int y = rand()%col+1;//随机生成0-8,加一得
        if (board[x][y] == '0'){
            board[x][y] = '1';
            count--;
        }
    }
}
扫雷

扫雷的过程中需要知道周围的雷数。也是比较复杂的代码。独立成函数模块

// 获取周围的雷数
int get_mine_count(char mine[ROWS][COLS],int x,int y){
    //这里可以int count  然后遍历周围的八个。
    //但是这里使用的思路周围所有数字总和为雷数。但是这里是char类型利用ASCII码的性质与'0'求差。
    int count = mine[x-1][y-1]+mine[x-1][y]+mine[x-1][y+1]+
    mine[x][y-1]+mine[x][y+1]+mine[x+1][y-1]+
    mine[x+1][y]+mine[x+1][y+1] - 8*'0';
    return count;
}
// 扫雷
void FindMine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col){
    int x;
    int y;
    int win =0;
    while (win<row*col-EASY_COUNT){
        printf("请输入排查的雷的坐标:\n");
        scanf("%d%d",&x,&y);
        if (x>=1 && x<=row && y>=1 && y<=col){
            //坐标合法
            //1.踩雷了
            if (mine[x][y]=='1'){
                printf("很遗憾,你被炸死了。\n");
                DisplayBoard(mine,row,col);
            } else{
                win++;
                //计算周围有几个雷
                int count = get_mine_count(mine,x,y);
                show[x][y] = count+'0';
                DisplayBoard(show,row,col);

            }

        } else{
            printf("您输入的坐标有误,请重新输入。\n");
        }
    }
    if (win==row*col-EASY_COUNT){
        printf("恭喜你,排雷成功");
        DisplayBoard(mine,row,col);
    }
}

一点打开一大片可以使用递归函数。

# 终止条件,周围有雷。
# 每次解决:
 1. 周围没有雷show数组在该位置改为' '。
 2. 周围的8个中有没有雷。--》提供思路。

代码展示

main.c
//
// Created by Tian_baby on 2021/12/30.
//

#include "game.h"
//打印初识目录界面
void menu(){
    printf("*****************************\n");
    printf("********   扫   雷   *********\n");
    printf("****  1. play   0. exit  ****\n");
    printf("*****************************\n");
}

//游戏的实现
void game(){
    // 1.布置好雷的棋盘
    char mine[ROWS][COLS] = {0};
    // 2.有每点周围雷的棋盘
    char show[ROWS][COLS] = {0};
    char res = 0;
    //初始化棋盘都是空格
    InitBoard(mine,ROWS,COLS,'0');
    InitBoard(show,ROWS,COLS,'*');

    //打印棋盘
    DisplayBoard(show,ROW,COL);
    //布置雷
    SetMine(mine,ROW,COL);
    // 查找雷
    FindMine(mine,show,ROW,COL);

}

// 游戏流程
void test(){
    int input = 0;//功能选择输入项
    // 随机数
    srand((unsigned int)time(NULL));
    // 控制游戏的重复
    do {
        menu();
        printf("请选择: ");
        scanf( "%d",&input);
        switch (input) {
            case 1:
                game();//游戏内容
                break;
            case 0:
                printf("退出游戏\n");
                break;
            default:
                printf("输入错误,请重新输入\n");
                break;
        }
    } while (input);
}

// 主函数
int main() {
    test();
    return 0;
}

game.h
//
// Created by Tian baby on 2021/12/30.
//

#ifndef TEST_12_31_GAME_H
#define TEST_12_31_GAME_H

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 9       //显示的行
#define COL 9       //显示的列
#define ROWS ROW+2      //实际使用的行
#define COLS COL+2      //实际使用的列
#define EASY_COUNT 10   //简单难度雷的总数
//初始化棋盘
void InitBoard(char board[ROWS][COLS],int rows,int cols,char full);
//展示棋盘
void DisplayBoard(char board[ROWS][COLS],int row,int col);
//布置雷区
void SetMine(char board[ROWS][COLS],int row,int col);
//扫雷
void FindMine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col);


#endif //TEST_12_31_GAME_H

game.c

//
// Created by Tian baby on 2021/12/30.
//

#include "game.h"
//初始化棋盘
void InitBoard(char board[ROWS][COLS],int rows,int cols,char full){
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            board[i][j] = full;//填充传过来的要求字符。
        }
    }
}
//展示棋盘
void DisplayBoard(char board[ROWS][COLS],int row,int col){
    printf("\n");
    //打印列号
    for (int i = 0; i <= col; ++i) {
        printf(" %d ",i);
    }
    printf("\n");
    for (int i = 1; i <= row; ++i) {
        printf(" %d ",i);
        for (int j = 1; j <= col; ++j) {
            printf(" %c ",board[i][j]);
        }
        printf("\n");
    }
}

//布置雷
void SetMine(char board[ROWS][COLS],int row,int col){
    int count = EASY_COUNT;//雷的总个数
    while(count){
        int x = rand()%row+1;// 我们需要的是 1-9
        int y = rand()%col+1;//随机生成0-8,加一得
        if (board[x][y] == '0'){
            board[x][y] = '1';
            count--;
        }
    }
}
// 获取周围的雷数
int get_mine_count(char mine[ROWS][COLS],int x,int y){
    //这里可以int count  然后遍历周围的八个。
    //但是这里使用的思路周围所有数字总和为雷数。但是这里是char类型利用ASCII码的性质与'0'求差。
    int count = mine[x-1][y-1]+mine[x-1][y]+mine[x-1][y+1]+
    mine[x][y-1]+mine[x][y+1]+mine[x+1][y-1]+
    mine[x+1][y]+mine[x+1][y+1] - 8*'0';
    return count;
}
// 扫雷
void FindMine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col){
    int x;
    int y;
    int win =0;
    while (win<row*col-EASY_COUNT){
        printf("请输入排查的雷的坐标:\n");
        scanf("%d%d",&x,&y);
        if (x>=1 && x<=row && y>=1 && y<=col){
            //坐标合法
            //1.踩雷了
            if (mine[x][y]=='1'){
                printf("很遗憾,你被炸死了。\n");
                DisplayBoard(mine,row,col);
            } else{
                win++;
                //计算周围有几个雷
                int count = get_mine_count(mine,x,y);
                show[x][y] = count+'0';
                DisplayBoard(show,row,col);
            }

        } else{
            printf("您输入的坐标有误,请重新输入。\n");
        }
    }
    if (win==row*col-EASY_COUNT){
        printf("恭喜你,排雷成功");
        DisplayBoard(mine,row,col);
    }
}

运行结果

image-20211231012446853

image-20211231012722725

image-20211231012921590

因为没有实现一点空白出现一大片的功能所以就直接设置了80个雷,并且显示棋盘获胜,从而测试代码的正确性。

五、操作符和表达式

操作符

分类:

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

算数操作符

+	-	*	/	%
  1. 除了%都可以用于整数和浮点数之间的运算
  2. 除法操作符"/":如果两个操作数都是整数,执行整数除法。只要其中有浮点数执行运算那整个表达式就是浮点数的除法。
  3. %的两个操作数必须是整数。

移位操作符

<< 左移位 			>> 右移位	

左移位 左边抛弃右边补0

右移位

  1. 算术右移

右边丢弃,左边补符号位(*)

  1. 逻辑右移

右边丢弃,左边补0

警告:对于移位运算符,不要移动负数位,这个标准没有定义。

int num = 10;
num>>-1;//错误的

位操作符

位操作符:

&		按位与			 有0就为0
|		按位或			 有一就为1
^		按位异或		相同为0相异为1

操作数必须是整数。

小练习

int num1 = 1;
int num2 = 2;
printf("%d  ",num1&num2);
printf("%d  ",num1|num2);
printf("%d  ",num1^num2);

image-20211231115342529

image-20211231115746523

题:不创建临时变量实现两数的交换

方法一 思路 (加减法可能会溢出)image-20211231120741442

代码实现

int a = 2;
int b = 3;
a = a+b;
b = a-b;
a = a-b;

方法二 (思路) 一个数对同一个数两次异或回到本身。而且异或运算有交换律

image-20211231122117758

代码实现

int a = 2;
int b = 3;
a = a^b;
b = a^b;
a = a^b;
printf("%d  ",a);
printf("%d  ",b);

image-20211231123937838

练习:

一个整数存储在内存中的二进制中的1的个数。

思路:1. 短除法思想。(只能计算正数)。

  1. 按位与1进行计算可以得到最后一位是什么,再加上右移操作。

代码实现:

int main(){
    int number = 0;
    int count = 0;
    scanf("%d",&number);
    //不能计算负数
    // while(number){
    //     if (number%2 == 1){
    //         count++;
    //     }
    //     number = number /2;
    // }
    for(i = 0;i<32;i++){		//32是因为int占32位
        if(1==((num>>i)&1))
            count++;
    }
    printf("%d",count);
    return 0;
}

赋值操作符(=)

初始化赋值、重新赋值、连续赋值。

复合赋值操作符
+=    -=    *=    /=    %=    >>=    <<=    &=    |=    ^=	

单目操作符

操作符作用
!逻辑取反
-
+
&取地址
sizeof操作数的类型长度(字节位单位)
~按位取反
前置、后置自减
++前置、后置自加
*间接访问操作符(解引用操作符)
(类型)强制类型转化

正、负是单目运算符,加、减是双目运算符

1是真但是真不一定是1。

计算数组长度 = sizeof(arr)/sizeof(arr[0])

坑题:

int main(){
    short s = 0;
    int a = 10;
    printf("%d\n",sizeof(s = a + 5));
    printf("%d\n",s);
}

image-20211231191549968

解析:sizeof(s = a+5)的大小最后取决于s的类型。

​ s==5是因为sizeof运算符中的表达式仅仅是表达一下,并不会真正参与运算。

sizeof和数组
void test1(int arr[]){
    printf("%d  ",sizeof(arr));//(3)
}
void test2(char ch[]){
    printf("%d\n",sizeof(ch));//(4)
}
int main(){
    int arr[10] = {0};
    char ch[10] = {0};
    printf("%d  ",sizeof(arr));//(1)
    printf("%d\n",sizeof(ch));//(2)
    test1(arr);
    test2(ch);
    return 0;
}

image-20211231194035664

解析:

(1)处 = sizeof(int)*10 = 40

(2)处 = sizeof(char)*10 = 10

(3)处 = sizeof(arr第一个元素的指针) = 8或者4 (64位电脑和32位电脑)

(4)处 = sizeof(ch第一个元素的指针) = 8或者4 (64位电脑和32位电脑)

关系操作符

关系操作符
>    >=    <    <=    !=    ==
逻辑操作符
&&    ||

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

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

逻辑是两个,按位是一个。

笔试题

程序运行结果是?

int main(){
    int i = 0,a=0,b=2,c=3,d=4;
    i = a++ && ++b &&d++;
    //i = a++||++b||d++;
    printf("a=%d  b=%d  c=%d  d=%d",a,b,c,d);
    return 0;
}

运行的结果是:a=1 b=2 c=3 d=4

解析:逻辑与左边为假右边就不计算了。在a++是先使用后运算。相当于i式第一部分就为假了。

程序运行结果是?

int main(){
    int i = 0,a=0,b=2,c=3,d=4;
    i = a++||++b||d++;
    printf("a=%d  b=%d  c=%d  d=%d",a,b,c,d);
    return 0;
}

运行结果:a=1 b=3 c=3 d=4

解析:逻辑或左边为真。右边就不计算了。a++为假运行,++b为真运行。d++就不运行了。

条件操作符(三目操作符)
表达式1?表达式2:表达式3

逗号表达式

表达式1,表达式2,表达式3,表达式4……,表达式n

从左向右以此计算。整个表达式的结果是最后一个表达式的结果。

int a = 1;
int b = 2;
int c = (a>b,a = b+10,a,b=a+1);
printf("%d",c);//结果为13

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

1.[ ]下标引用操作符

操作数:一个数组名+一个索引值

int arr[10];//创建数组
arr[9] = 10;//使用下标引用操作符
[]的两个操作数是arr和9.

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

3.访问一个结构的成员

. 结构体对象.成员

-> 结构体指针->成员名

//创建了一个结构体类型  Stu    C语言中叫成员就类似于面向对象中的属性
struct Stu{
    char name[10];
    int age;
    char id[20];
};

int main(){
    //使用结构体struct Stu这个类型创建了一个学生对象,并初始化
    struct Stu s1 = {"张三",20,"201808520"};
    printf("%s\n",s1.name);
    printf("%d\n",s1.age);
    printf("%s\n",s1.id);
    struct Stu* ps = &s1;
    printf("%s\n",ps->name);
    printf("%d\n",ps->age);
    printf("%s\n",ps->id);
    return 0;
}

image-20211231231333109

表达式求值

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

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

隐式类型转化

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

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

​ 整型提升的意义:

	表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的自己长度,同时也是CPU的通用寄存器的长度。
	因此,及时两个char类型相加,在CPU执行时实际上也要先转换为CPU内存整型操作数的标准长度。
	通用CPU是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加的指令)。所以表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能进入CPU去执行运算。

实例:

问:输出的结果是什么?

char a = 3;
char b = 127;
char c = a + b;
printf("%d\n",c);
//输出-126

解析

char a = 3;
//00000000000000000000000000000011
//截断  ->  00000011  ->  存给a
char b = 127;
//00000000000000000000000001111111
//截断  ->  01111111  ->  存给b
char c= a+b;
//a:00000011  整型提升  ->  (按符号位提升)
//00000000000000000000000000000011
//b:01111111  整型提升  ->  
//00000000000000000000000001111111
//相加
//00000000000000000000000010000010
//截断  ->  10000010  ->存给c
printf("%d\n",c);
//a:10000010  整型提升  ->  (按符号位提升)
//11111111111111111111111110000010 -补码
//11111111111111111111111110000001 -反码
//10000000000000000000000001111110 -原码
//转化为十进制:-126

b和c的值被提升为普通整型,然后执行加法运算。加法运算完成之后,结果将被阶段,然后再存储于a中。

那么,什么是整型提升呢?

整型提升是按照变量的数据类型的符号位来提升的。无符号数直接在前面补0即可。

# 负数的整型提升
char a = -1;
整型提升   11111111  (补码)  补码-1-> 反码  正数不变 负数取反
# 正数的整形提升
char = 1;
整型提升   00000001(补码)   正数的三种码都一样

整型提升的例子

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

image-20220101003741340

说明整型提升的存在。

算数转换

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

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

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

注意:算术转化要合理进行,要不然会有一些潜在的问题。

float f = 3.14;
int num = f;//因式转换,会精度丢失。
操作符属性

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

  1. 操作符的优先级
  2. 操作符的结核性
  3. 是否控制求值顺序

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

操作符的优先级

img

注:图片引自 [https://blog.csdn.net/zhanghong056/article/details/76667298]

问题代码:

int main(){
    int i = 1;
    int ret = (++i) + (++i) +(++i);
    printf("%d",ret);
    printf("%d",i);
    return 0;
}
//初步判定计算顺序是三个数的加法 但是是先第一个++i存储下来再计算还是 先改变值再计算。这是又歧义的。

划重点:

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

操作符、函数习题

第一题

以下代码的输出结果

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    short *p = (short *) arr;
    for (int i = 0; i < 4; i++) {
        *(p + i) = 0;
    }
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

image-20220104004117392

解析:这里的重难点就是short *p = (short *) arr;因为short只有2字节,所以他每次只能操作两个字节的地址。这样的话第一个for循环就可以将两个int的内容变为0。输出的时候就前两个值为0后面的不变。

第二题

以下代码的输出结果

int main() {
    int a = 0x11223344;
    char *pc = (char*)&a;
    *pc = 0;
    printf("%x \n",a);//十六进制的输出形式
    return 0;
}

image-20220104004852646

解析:0x11223344在内存中存储内容是44332211,char* 每次只能操作一个字节的内容。所以44->0,输出的时候再逆序输出就是11223300。

第三题

以下代码的输出结果

int i;
int main() {
    i--;
    if(i> sizeof(i)){
        printf("> \n");
    } else{
        printf("<\n");
    }
    return 0;
}

image-20220104005537888

解析:全局变量没有初始化的时候默认是0;

sizeof()计算变量/变量所占内存的大小。大小>=0,是无符号数。

当无符号数与正常数字进行比较时先将正常数转化为无符号数。-1转化为无符号数是一个很大的值。所以结果就是 “>”。

第四题

以下代码的输出结果

int main() {
    int a=0,b=0,c=0;
    a = 5;
    c = ++a;
    b = ++c,c++,++a,a++;
    b += a++ + c;
    printf("a = %d b = %d c = %d\n",a,b,c);
    return 0;
}

image-20220104011710562

解析:"=“的优先级高于”,",1. a = 5;c = 6;c = 7;b = 7;c = 8;a = 6;a = 7; “+“的优先级高于”=” 2. 计算b += a++ + c; -> a = 8, c = 8 b = 7->>b = 23

第五题

统计一个数二级制中1的个数

写一个函数返回参数二级制中1的个数。

方法一:
int Count_1(unsigned int n) {
    int count = 0;
    while (n) {
        if (n % 2 == 1) {
            count++;
        }
        n = n / 2;
    }
    return count;
}

int main() {
    int n = 0;
    scanf("%d", &n);
    //函数  a的二级制补码中的1的个数
    int count = Count_1(n);
    printf("count = %d", count);
    return 0;
}

解析:计算二进制为1的位数时每次对2取余就是获取最后一位的数字。除以二就是将这个数最后一位删去。对于负数只需要将其按照无符号数来对待就可以了。

方法二:
int Count_1( int n) {
    int count = 0;
    for(int i = 0;i<32;i++){
        if(((n>>i)&1)==1){
            count++;
        }
    }
    return count;
}

解析:使用移位的方式,和位计算且的原理,只要和1与运算的结果为一说明当前数的最后一位为1,每次多移一位进行运算。出现1count+1。32次运算,结果就是答案。

方法三:
int Count_1( int n) {
    int count = 0;
	while(n){
        n = n&(n-1);
        count++;
    }
    return count;
}

解析:一个数的二进制可以通过(n)&(n-1)将n最右边的1消除掉。这样计算时间复杂度是最低的。

第六题

题目:求二进制中不同位的个数

内容: 两个int(32位)整数m和n的二进制表达式中,有多少个位(bit)不同。

int Count_1( int n) {
    int count = 0;
	while(n){
        n = n&(n-1);
        count++;
    }
    return count;
}
int GetDiff(int m,int n){
    int count=0;
    count = Count_1(m^n);
    return count;
}
int main(){
    int m=0,n=0;
    scanf("%d %d",&m,&n);
    int count = GetDiff(m,n);
    printf("count=%d",count);
    return 0;
}

解析:先将两数进行异或计算。这样不相同的位数就可以由异或后的值进行表达,然后再将此值进行位为1的个数统计。

第七题

题目:打印二进制的奇数位和偶数位(从低位往高位数)

获取一个整数的二进制序列中所有的偶数位和奇数位,分别打印出二进制序列。

void Print(int m){
    for (int i = 30; i >= 0; i-=2) {
        printf("%d",(m>>i)&1);
    }
    printf("\n");
    for (int i = 31; i >= 0; i-=2) {
        printf("%d",(m>>i)&1);
    }
}
int main(){
    int m = 0;
    scanf("%d",&m);
    Print(m);
    return 0;
}

解析:从最高位开始右移每次只要最后一位。通过奇数位偶数位的控制进行打印。

第八题

题目:交换两个变量(不创建临时变量)

方法一 思路 (加减法可能会溢出)

image-20211231120741442

代码实现

int a = 2;
int b = 3;
a = a+b;
b = a-b;
a = a-b;

方法二 (思路) 一个数对同一个数两次异或回到本身。而且异或运算有交换律

image-20211231122117758

代码实现

int a = 2;
int b = 3;
a = a^b;
b = a^b;
a = a^b;
printf("%d  ",a);
printf("%d  ",b);

image-20211231123937838

函数传参

第一题

字符串逆序(递归实现)

编写一个函数reverse_string(char* string)。

非递归:

void reverse_string(char* arr,int sz){
    int left= 0;
    int right = sz-2;
    int temp;
    while (left<right){
        temp = arr[left];
        arr[left] = arr[right];
        arr[right] = temp;
        left++;
        right--;
    }
}
int main(){
    char arr[] = "abcdef";
    int sz = sizeof(arr)/ sizeof(arr[0]);
    // printf("%d",sz);
    reverse_string(arr,sz);
    printf("%s",arr);
}

递归:

int my_strlen(char *str) {
    char *start = str;
    char *end = str;
    while (*end != '\0') {
        end++;
    }
    return (int)(end - start);
}
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[] = "abcdefg";
    printf("%d\n", my_strlen(arr));
    reverse_string(arr);
    printf("%s\n", arr);
    return 0;
}

解析:问题分解。交换最外层的两个值,当当前字符串长度大于等于2时递归。

实现步骤:1.将数组第一个值传给临时变量temp,将最后一个值传给第一个值。2.将最后一个位置先修改为’\0’用来表示字符串暂时结束。3.将数组下标从第二个开始reverse_string()。4.递归完之后将临时变量的值传给最后一个位置。

第二题

计算一个数的每位之和(递归实现)

写一个递归函数DigitSum(a),输入一个非负整数,返回组成他的数字之和。

int DigitSum(unsigned int num){
    if(num/10 == 0){
        return num;
    }
    return num % 10 +DigitSum(num/10);
}

int main(){
    unsigned int num = 0;
    scanf("%d",&num);
    int res = DigitSum(num);
    printf("res = %d",res);
    return 0;
}

解析:终止条件就剩下一位数的的时候。举例1234的每位之和等于4+123的每位之和。

第三题

递归实现n的k次方

double Pow(int n,int k){
    if (k==0) return 1;
    else if (k>0) return n* Pow(n,k-1);
    else return (1.0/n)* Pow(n,k+1);
}

int main(){
    int n = 2;
    int k = -3;
    double res = Pow(n,k);
    printf("res = %lf \n",res);
    return 0;
}

解析:Pow(n,k) = n * Pow(n,k-1)。终止条件,k == 0的时候等于1。

第四题

计算斐波那契数列(递归实现)

int Fib(int first,int second,int n){    //---->>>需要修改参数的量
    if (n<=2) return 1;
    if (n==3) return first+second;
    else return Fib(second,first+second,n-1);
}
int main(){
    int n;
    scanf("%d",&n);
    int res = Fib(1,1,n);
    printf("%d",res);
    return 0;
}

解析:经过优化的斐波那契数列。

六、C语言指针

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

指针是什么?

指针(Pointer)是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。

换句话说就是可以通过指针找到以它为地址的内存单元

理解:内存图解。

image-20220101191356913

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

int main(){
    int a = 10;//在内存中开辟空间存储
    int* p = &a;//先对变量a取出它的地址,可以使用&操作。
    			//将a的地址存放在p变量中国,p就是一个指针变量
}

总结:指针就是变量,内容是地址。(存放在指针中的值被当做地址处理)

指针的大小

在32为计算机上指针大小4字节。

在64为计算机上指针大小8字节。

指针和指针变量

关于地址
printf("%p \n",&a);//%p地址格式   &a取a的地址
int* p = &a;
//int*指针类型 
//p 指针变量 
//&a 取地址
使用
*p //解引用操作符
int a =10;  //在内存中存储10   还有char*等类型
int* p = &a;//定义指针,位置为a的内存
*p = 20;    //更改指针指向内存的 值
printf("a= %d",a);//结果为a=20

int* p的理解 p是int类型的一个指针(仅此而已),一般*p指向的也是一个int型的

1. 指针类型决定了指针进行解引用操作的时候,能访问空间的大小
int main(){
    int n = 0x112233;
    char* p = (char*)&n;
    int* pi = &n;
    *pc = 0;  //在调试的过程中观察内存的变化。
    *pi = 0;
    return 0;
}
int*;  *p可以访问4个字节。
char*; *p可以访问1个字节。
double*;  *p可以访问8个字节。

原因 是类型本身所需的内存空间就是指针可以控制的空间。

意义:使用时选用合适的指针类型进行定义

2. 指针加减整数
int main(){
    int a = 0x11223344;
    int* p1 = &a;
    char* p2 = &a;
    printf("%p\n",p1);
    printf("%p\n",p1+1);
    printf("%p\n",p2);
    printf("%p\n",p2+1);
    return 0;
}

image-20220103173241655

int类型时0C->10 变化4, char类型时0C->0D 变化1。

理解:指针加一不是指向下一个紧挨着的地址,是指向下一个指针变量对应的类型变量开始的地址。

意义 指针类型决定了:指针走一步走多远(指针的步长)

野指针

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

野指针的成因
  1. 指针未初始化
int main(){
    int a;//局部变量不初始化,默认是随机值
    int *p;//局部的指针变如果没有初始化,就被初始化为随机值。
}
  1. 指针越界访问
int main(){
    int arr[10];
    int *p = arr;
    for(int i = 0;i<12;i++){
        p++;
    }
    //当指针的范围超出数组的范围时,p就是野指针。
}
  1. 指针指向的空间释放
int* test(){
    int a = 10;
    return &a;
}
int main(){
    int *p = test();
    return 0;
}

解析:在main函数调用test()时,进入test()函数,int a语句开辟临时的内存空间并将这个内存空间存储为10;返回函数的时候返回的临时的a的地址给*p,然后test函数已经在执行完test函数后结束,a的内存空间被销毁。这时的*p就是指向的地址正确但是内容已经改变。

将未知位置的值进行修改是非常危险的

如何避免野指针
  1. 指针初始化
  2. 小心指针越界
  3. 指针指向内存释放 即 指向NULL
  4. 指针只用之前检查有效性

指针运算

  1. 指针加减整数
  2. 指针-指针
  3. 指针的关系运算
指针加减数字
int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int sz = sizeof(arr)/sizeof(arr[0]);
    int* p = arr;
    for(int i=0;i<sz;i++){
        printf("%d ",*p);
        p = p+1;// p++
    }
    int* p = &arr[9];
    for(int i=0;i>0;i++){
        printf("%d ",*p);
        p-=1;// p++
    }
    return 0;
}
指针-指针
int main(){
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
    printf("%d",&arr[9]-&arr[0]);//输出9   中间元素的个数。
    printf("%d",&arr[0]-&arr[9]);//输出-9  
    return 0;
}

指针减指针必须是自己减去自己。否则结果不可预知。

指针实现strlen()

int my_strlen(char* str){
    char* start = str;
    char* end = str;
    while(*end != '\0'){
        end++;
    }    
    return start - end;
}
int main(){
    char arr[] = "hello";
    int len = my_strlen(arr);
    printf("%d\n",len);
    return 0;
}
指针的关系运算
int main(){
    float values[5];
    for(float* vp=&values[5];vp>&values[0];){
        printf("haha ");
        *--vp = 0;
    }
    return 0;
}

这里碰到了两个问题 1. values[5]本身不属于数组的部分。但是可以使用。经测试values[5]不会警告,但是values[-1]及以下或values[6]及以上都会报错。2.指针的加减是类型位置的移动数组总也就是一个一个往过走。

for(float* vp=&values[5-1];vp>=&values[0];vp--){
    printf("haha ");
    *vp = 0;
}

这里在绝大多数的编译器上是可以顺利完成任务的,然而我们应该避免这第二种写法,因为标准不能保证他是可行的。

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

指针和数组

int main(){
    int arr[10]={0};
    printf("%p\n",arr);  //地址-首元素地址
    printf("%p\n",&arr[0]);
}

一般情况数组名都代表首元素的地址

除了:1. &数组名 这时数组名代表整个数组的地址

​ 2. sizeof(数组名) 这时也是代表整个数组。

二级指针

将第一层指针1想成变量,再取这个变量的地址存为一个指针2。那么指针2指向指针1,指针1指向原变量。原变量的地址存在了指针1中,指针1的地址存在了指针2中。

int main(){
    int a = 10;
    int* pa = &a;
    int** ppa = &pa;//ppa就是二级指针。
    //存在三级及以上指针,(无限套娃)
}

指针数组、数组指针

指针数组其实是个数组,数组指针是个指针

指针数组:存放指针的数组

int a = 10;
int b = 20;
int c = 30;
int* arr[3] = {&a,&b,&c};//指针数组

数组指针:指向数组的指针。

自己实现strcpy、strlen

strcpy的实现

思路:

通过指针访问地址然后将要copy的文本逐一复制到目的地。

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

//自己实现strcpy
int main(){
    char arr1[]="$$$$$$$$$$";
    char arr2[]="hello";
    my_strcpy(arr1,arr2);//将arr的内容复制到arr1中
    printf("%s\n",arr2);
    return 0;
}
优化1:

在my_strcpy()函数中*dest和*src直接在表达式中自加,先试用后加所以使用后置++

void my_strcpy(char* dest, char* src){
    while (*src !='\0'){
        *dest++ = *src++;
    }
    *dest = *src;
}
优化2:

在while循环中,因为最后的终止条件是赋值到了绝对0的时候停止循环。那么最后一次赋值就是赋值0。那我们直接可以将赋值作为我们的终止条件。

void my_strcpy(char* dest, char* src){
    while (*dest++ = *src++);
    *dest = *src;
}
优化3:

如果传入的是空指针我们应该告诉这个输入有问题。

引入assert();断言–>如果输入错误显示错误。 个人理解就像java,python中的异常处理。

#include <assert.h>
void my_strcpy(char* dest, char* src){
    assert(dest != NULL);
    assert(src != NULL);
    while (*dest++ = *src++);
    *dest = *src;
}
//自己实现strcpy
int main(){
    char arr1[]="$$$$$$$$$$";
    char arr2[]="hello";
    my_strcpy(arr1,NULL);//将arr的内容复制到arr1中
    printf("%s\n",arr2);
    return 0;
}
优化4:

在程序员将dest 和 src写反的情况。我们应该怎么处理呢?

加const 使得我们的原来的数据不能被拷贝数据不能进行修改。从强制检测。另一方面解释:源头的数据的安全性得到保证。

#include <assert.h>
void my_strcpy(char* dest,const char* src){
    assert(dest != NULL);
    assert(src != NULL);
    while (*dest++ = *src++);  //这里写反的话将不能进行赋值   因为c
    *dest = *src;
}
对于const的讲解
const int* p = &num;
int* const p = &num;
const int* const p = &num;

在第一行中:即const 放在指针变量* 的左边的时候修饰的是*p 也就是说也就是说不能通过*p来改变*p(num )的值。

在第二行中:即const放在指针变量* 的右边的时候修饰的是指针本身p,p不能被改变。

在第三行中:两边都进行const修饰,p与*p都不能改变了。

我的理解:const只能修饰关键字本身右边的第一个东西(像int*就直接理解为指针)。const修饰的东西被限制。

优化5:

在官方提供的库函数中strcpy是有返回值的。返回什么呢?我们应该返回copy执行完成之后的字符串中首字母的地址。但是我们发现之前已经在执行的过程中就将dest值修改了。又怎么办呢?在刷算法题的时候有个经验就是,在对地址进行操作之前先提前备份一份。然后将备份的这个地址位置返回即可。

#include <assert.h>
char* my_strcpy(char* dest,const char* src){
    char* res = dest;
    assert(dest != NULL);
    assert(src != NULL);
    while (*dest++ = *src++);
    *dest = *src;//将'\0'进行赋值。
    return res;
}

总结:1. 分析参数的设计(命名,类型),返回值的情况。

  2. 关于野指针问题,空指针的危害。
  3. assert的使用方式和作用
  4. 参数部分使用const,以及const的作用。
  5. 注释的重要性。

strlen的实现

#include <assert.h>
int my_strlen(const char* str){//不希望我的字符串被修改。
    int count = 0;
    assert(str != NULL);//用来保证指针的有效性
    while(*str !='\0'){
        count++;
        str++;
    }
    return count;
}

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

七、C语言初识结构体

  1. 结构体类型的声明
  2. 结构体初始化
  3. 结构体成员访问
  4. 结构体传参

结构体的声明

结构

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

结构体的声明
struct 结构体标签{
    结构体成员列表;
}结构列表;

注:定义结构体实际上就是在声明,所以最后一定要有分号

举例

// struct 结构体关键字   Stu- 结构体标签  struct Stu -结构体类型
struct people{
    //成员变量
    char name[20];
    short age;
    char tele[12];
    char sex[5];
}p1,p2;//p1,p2是两个全局的结构体变量
int main(){
    // 创建结构体变量
    struct people pe;//局部的
    return 0;
}
使用typedef简化使用
typedef struct people{
    //成员变量
    char name[20];
    short age;
    char tele[12];
    char sex[5];
}People;//People是typedef后struct people的别名
int main(){
    // 创建结构体变量
    People pe;//直接使用别名
    return 0;
}

注意:使用了typedef那么创建结构体最后的分号前只能是别名,不能用来定义结构体变量。

结构体成员的类型

结构体的成员可以使变量,数组,指针,甚至是其他的结构体。

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

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

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

struct people pe = {"Tom", 20, "15885458520", "男"};

总结:1. 声明类型的时候初始化。 2. 直接定义结构体变量 3. 定义变量的同时初始化

嵌套使用
struct A{
    int a;
    char c;
    char arr[20];
    double d;
};

struct B{
    char ch[10];
    struct A a;
    char *pc;
};
int main(){
    char arr[10];
    struct B b = {"hello", {100,'w',"world",2.22}, arr};
    return 0;
}

结构体中的成员变量的类型可以是其他的结构体。初始化时使用的其他的结构体也使用{}进行初值的赋值。

结构体成员访问

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

int main(){
    char arr[]="tian";
    struct B b = {"hello", {100,'w',"world",2.22}, arr};
    printf("%s\n",b.ch);
    printf("%s\n",b.a.arr);
    printf("%lf\n",b.a.d);
    printf("%s\n",b.pc);
    return 0;
}

image-20220104234053888

2.结构体指针访问变量的成员 有的时候我们得到的不是一个结构体变量,而是指向一个结构体的指针。(使用(->))

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

结构体传参

struct Stu{
    char name[20];
    int age;
};
void print(struct Stu* ps){
    printf("name = %s  age = %d\n",(*ps).name,(*ps).age);
    //使用结构体指针访问指向对象的成员
    printf("name = %s  age = %d\n",ps->name,ps->age);
}
void print2(struct Stu ps){
    printf("name = %s  age = %d\n",ps.name,ps.age);
}
int main(){
    struct Stu s = {"zhang", 20};
    print(&s);
    print2(s);
    return 0;
}

两种传参方式达到了同样的效果。但是真的没有区别吗?

使用指针访问不需要开辟新的空间。直接访问地址。时间空间上都占优。

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

结论:结构体传参的时候,最好(要)传结构体的地址。



由于加上进阶篇文字内容过多无法发布,只能拆开。在文章开头的链接可以跳转进阶内容。
  • 9
    点赞
  • 90
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

黎丶辰

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

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

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

打赏作者

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

抵扣说明:

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

余额充值