这篇笔记是我大二期末后,为学习嵌入式做准备时,学习的C语言笔记。内容从比特鹏哥视频学习,特别感谢他对我笔记习惯的培养!
此文档记录我初阶学习C语言可以编写的程序以及个人的理解记录
初认C语言
printf打印小飞机
2024-07-09
输入两个数,输出商和余数
2024-07-09
根据ASCII码输出字符
2024-07-09
- 输出时常用到的如下:
- %c 字符型格式符,作用是读入一个字符。
- %d 表示输出十进制有符号的整数。
- %u 十进制无符号整数。
- %f 表示输出浮点数。
- %s 表示输出 字符串。
- %c 表示输出单个字符。
- %p 表示输出指针的值。
- %e 表示输出指数形式的浮点数。
- %0x 表示输出无符号以十六进制表示的整数。
- .%0 表示输出无符号以八进制表示的整数。
出生日期输入输出
2024-07-09
输入描述: 输入只有一行,出生日期,包括年月日,年月日之间的数字没有分隔符。
输出描述: 三行,第一行为出生年份,第二行为出生月份,第三行为出生日期。输出时如果月份或天数为1位数,需要在1位数前面补0。
输出printf的返回值
2024-07-11
printf会打印出字符的个数,等等也算一个字符。
也可以使用
输出printf(“hello world”);
2024-07-11 这个题考验的是转义字符 的运用
四个数找出最大数
输入: 四个整数,用空格隔开
输出: 一个整数,为四个数的最大的
2022-07-11
方法一:存起来再比较
方法二:输入的时候直接比较
计算球的体积
2024-07-11
输入:一个浮点值0-15
输出:球的体积,小数点保留三位
这个题考察的是float和double的区别
float占4字节, double占8字节,所以double的精度会更高
float在初始化的时候是float = 0.0f:
double在初始化的时候是float = 0.0lf;或者 0.0 因为0.0默认使用的是double
代码如下:
分支部分
学生基本信息输入输出
2024-07-09
输入描述: 学号以及3科成绩,学号和成绩之间用英文分号隔开,成绩之间用英文逗号隔开。
输出描述: 学号,3科成绩,保留两位小数
#include<stdio.h>
- 格式字符串 “%d;%f,%f,%f” 规定了输入数据的格式,&num、&x、&y、&z 分别表示将输入的值存储到对应的变量地址中。
- %.2f 表示将浮点数按照小数点后保留两位的格式进行输出。
判断输入数字是否为5整除
2024-07-10
循环部分
计算n的阶乘
2024-07-11
1. 的阶乘可以先定义一个为1的变量num,用来累加
2. 定义一个n,用来表示n的阶乘
2! = 21
5! = 5432*1
在这个代码的基础上,可以去探究一下阶乘相加,例如n=5时,计算5!+4!+3!+2!+1!
上述代码已经有了计算一次的结果,需要在计算五次就可以了,这需要累减5到1就可以了
使用二分查找(折半查找)查找有序数组中某个数字的下标
2024-07-11
比如数组为{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20}
二分查找的应用范围是:被查找的数组是一个有序的数组,上数组符合要求
二分查找过程如下
1. 二分查找要定义 左范围值( 下标为0 ) 和 右范围值( 下标为元素-1 ) 和 查到的找值(下标为左值与右值中间)
2. 通过备查找到的值与待找到值比较。
- 如果查找到的值 大于待找到的值,也就表示 待查找值 在 左范围值和查找到的值之间 。即 此次查找到的值的下标就是新的右范围值
- 如果查找到的值 小于待找到的值,也就表示 待查找值 在 查找到的值和右范围值之间 。即 此次查找到的值的下标就是新的左范围值
3.定义新的左值或者右值,再算出新的中间值之后,继续回到第二步,直到找到为止(通过left和right来判断是否没找到)。
当然,如果是倒着的数组要另说,方法同上
输出多个字符从两端移动,向中间汇聚
2024-07-11
例如:
##############
h############!
hel#########!!
…..
hello world!!!
char arr[] = “hello world!!!”
- 这个 属于字符串 ,可以用到 strlen 来计算字符串的长度
- 当然也可以继续使用sizeof来计算字符串的长度,即 sz = sizeof(arr)/sizeof(arr[0])。但是计算出的长度不是字符串的长度,他需要减去1。因为 字符串的结尾处有\0 。会多计算一个字符。
他们的下标则是计算出的长度-1
- 使用strlen函数时,right的下标为 strlen(arr)-1
- 使用sizeof函数时,right的下标为 sizeof(arr)/sizeof(arr[0]) - 2
下面是实现的方法
写一个猜数字大小的游戏
2024-07-11
1. 系统随机出一个 1-100 的数字
2. 用户猜,大了就反馈大了,小了就反馈小了。
这个需要利用rand()函数来生成一个随机数
rand需要定义一个种子,也就是srand
- rand的用法是直接int a = rand();
- srand的用法是srand(一个随机数); srand函数期望输入的数字是unsigned int 类型 (可以在手册中查到)
在这里我们用的是系统读取当前的时间戳time()函数,因为time() 返回的值是一个 time_t 类型 的。所以我们需要将返回值 强制转换为unsigned int类型 的.而且time不需要输入值,即 输入NULL
所以这个随机数可以写:
其中 srand 定义随机数种子应放到主函数中,这样在多次利用时,才不会因种子定义的起点相同而生成的随机数相差不大。
1. 如果在程序中多次调用srand(),并且传入相同的种子值,那么rand()函数每次都会从 相同的起点开始生成随机数 ,导致得到的“随机”数实际上是重复的。
2. srand()函数的作用是设置rand()函数的种子。 种子是伪随机数生成器的起始点 ,决定了随机数序列的起点。 如果每次运行程序时都用相同的种子,那么每次产生的随机数序列都会是一样的。
3. rand()函数是一个 伪随机数生成器 ,它通过一个确定的算法来生成一系列看起来像是随机的数字。这些数字实际上是可预测的,如果知道算法和种子值,就可以计算出整个序列。
他们的头文件要求是
- srand <stdlib.h>
- time <time.h>
下面是猜数字游戏的程序
模拟用户登录
2024-07-11
模拟用户登录情景,只能登录三次,三次内成功则提示登陆成功,三次都收入错误,则退出程序
此题考验的是字符串相等
比较两个字符串是否相等,不能使用 == ,而是应使用一个库函数:strcmp
关机程序
2024-07-11
电脑运行起来后,一分钟内关机,如果输入我是猪,就取消关机
- 这个题考察了goto 语法的使用
- goto 语法一般不常用,但他可以用来跳深度嵌套的语句。
- 在windows中关机命令是
- 取消关机是:-a
当然,最好还是不要用goto语句,这段可以改成while语句来代替
冒泡排序
这个冒泡排序顺便复习了一下函数的调用,复习了一下指针。
冒泡排序是两两对比,大的放右边,可以看作是一个大括号从下标为0 和1开始括,
然后往右移动,变成括起来 下标为 1 和 2 的两个数进行比较, 比较完成之后 最右边的数字就是最大值了,
这时候就不需要再比较最右边的数了。
所以第二次重新开始比较循环的时候,就可以不管最右边的数了, 也就是 第二层for循环中的len-1 -i
判断三角形
说一下我对指针地址和下标的一些理解
- 数组下标应该是从0开始的,所以arr[10]={0};这个代码中,有0到9共十个房间号。例如,printf("%d", arr[2]);打印的是第三个元素,即arr[2]的值
- 指针
指针是一种特殊的变量,全名叫指针变量。它存储的是另一个变量的内存地址。例如,
int a = 10; int *p = &a; 这段代码将a的地址赋值给指针变量p。
1. 内存分为堆区、栈区和静态区。堆区由程序员显式分配和释放,栈区由编译器管理,存储局部变量和参数,静态区存储全局和静态变量。- 栈区:由编译器管理,存储局部变量和参数,内存地址由高到低递减。栈空间小但速度快,适合函数调用和临时数据存储。比如临时变量,返回值,形参这些 他的使用习惯是先使用高地址处的空间、再使用低地址的空间
- 堆区:它是由操作系统管理的,但分配和释放操作由程序员通过代码显式完成,通常使用
malloc()、calloc()分配内存,使用free()释放内存。堆内存地址由低到高增长,适合存储大数据或生命周期长的对象,但需注意内存泄漏问题。
- 静态区:存储全局和静态变量,生命周期贯穿程序运行。已初始化变量在
.data段,未初始化变量在.bss段,由系统初始化。
我们现在要打印变量a的地址,就需要这样的代码
比如a的地址是0x0123
- 指针
指针,他存储的是内存地址,它的表示是int* 或者char*比如这个代码
这段代码的意思就是把a的地址0x0123赋值给p这个指针变量
但是需要注意,指针变量p在内存中是有自己的内存地址的,它里面存储的值是a的地址
如果输入
printf(”%p”,“&p);就可以显示出p在内存中的地址现在这个情况
- p 里是a的地址0x0123 他的类型是int*
现在如果打印p这个值的地址:
printf(”%p”,p); 就会输出0x0123- *p 指的是 对p进行解引用 得到 a的值 他的类型是int
对*p赋值相当于给a赋值。例如,
*p = 20;等价于a = 20;如果我们输入
printf(”%d”,*p); 就相当于输入printf(”%d”,a); 他的结果就是10这个时候输入printf(”%p”,&*p);*p是使用解引用操作符*来访问指针变量p指向的内存位置中的值。 *p指向的是a ,所以&*p 输出就等于&a输出就还是0x0123
函数部分
写一个函数,实现对xy的交换
2024-07-12
这部分考验了实参和形参。
- 形参在用完就销毁了
代码1:
这个代码是错误代码。
- 因为main函数中是实参,而void中的a b为形参
- 当代码执行到这一部分的时候, 形参是实参的临时拷贝 ,它有了自己的地址。 对形参的修改不能改变实参
要想正确的交换,则应用到指针。
- 先把实参的地址传到子程序中,在子程序中通过地址准确的找到main中的实参,然后完成精准的控制。
在往函数传递内容的时候需要用指针
2024-07-12
线在上面的函数基础上,添加一个add函数
可以看到在add函数中,并没有用到指针去传递地址。为什么呢?
- 因为在使用add时,并不需要改变实参a或b的值,只需要将他们相加再返回就可以
- 所以形参的值只要一样就可以了。
结论: 综上,可以看出函数的调用有两种
1. 传 值 调用
- 函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参
2. 传 址 调用
- 传址调用是吧函数外部创建的变量的内存地址传递给函数参数的一种调用方式
- 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
1. 写一个函数,可以判断一个数是不是素数。
素数时只能被他本身和1整除的数字
当然这个代码还有优化的空间,
1. 可以使用sqrt库来对输入的数字进行开方,这样i只需要累加到sqrt(x)就可以了。因为当i累加到sqrt(x)时还没被x整除,那么想必,后面的肯定数中肯定也没有数是x的因子了。
2. 素数一定是一个奇数,所以++可以变成+=2
因此循环部分的代码可以改成这样,其余不变
需要注意的是sqrt属于数学库,需要添加头文件#include <math.h>
2. 写一个函数,判断一年是不是闰年
2024-07-12
闰年的要求
1. 能被4整除,且不能被100整除
2. 能被400整除
这个函数是返回1 或 0 相比上一题的函数,直接打印出来,他的功能更为单一,更容易让别人去复用(比如别人不想打印,他只想判断)
3. 写一个函数,实现一个整形有序数组的二分查找
这个函数主要是考察到 了数组在传参的过程中,传递的是数组首元素的地址,而不是整个数组。
比如有
在这段代码中,arr元素个数的计算是在Binary_Search中计算的
但是由于arr传过来的只是一个首地址,所以他的元素个数打印出来就是1
所以在函数内部计算函数参数部分的数组的元素个数是不靠谱的
所以正确的代码是在函数外部计算数组的元素个数
所以完成二分查找的数组的程序是
4. 写一个函数,每调用一次这个函数,就会将 num 的值增加1
2024-07-11
- 指针法
- static修饰法//写函数的时候!最好不要用这种东西!!
其他的就不多写了 也可以直接return num + 1;
函数的嵌套
- 函数可以嵌套调用,但是不能嵌套定义
函数的链式访问
2024-07-12
函数的返回值可以作为另一个函数的参数
未嵌套
嵌套后
一道链式访问的例题
printf的返回值是输出的字符个数所以输出43 2 1
函数的声明
1.函数的声明是告诉提前告诉编译器,有一个函数叫什么,返回类型是什么,但是具体存不存在函数声明是决定不了的。
2.函数的声明 一般出现在函数使用之前,要满足先声明后使用。
3.函数的声明一般放到头文件中
函数的声明一般放到.h头文件中,在main.c文件中使用#include <xxxx.h>。函数部分可以放到xxxx.c文件中。
导出导入静态库
导出:导出可以把你写好的如add.c文件导出为add.lib的库文件,配合add.h的头文件声明。可以一定程度的隐藏代码,且能给别人使用。
导入:在导入静态库时,需要输入 #pragma comment(lib,“add.lib”)
递归
1.什么是递归?
- 程序调用自身的编程技巧称为递归(recursion)
- 递归做为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解递归策略
- 只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在于:把大事化小
2.递归的两个必要条件
- 存在限制条件,满足这个限制条件就不在递归
- 每次递归调用后越来越接近这个限制条件
一道简单的递归练习题,输出输入的字符
输入12345
输出1 2 3 4 5
简单讲一下:
num=12345 进到函数之后,因为n>9所以进入函数,且被除10 变为
num=1234 同理
num=123
num=12
num=1 到1之后,因为小于10,所以取模打印,打印1,然后跳出这次循环 到
num=12 的循环中,取模打印,打印2,到
num=123 的循环中,取模打印,打印3,到
num=1234 ,打印4
num=12345,打印5
递归用到了大括号中的数只在大括号内有效。
一道练习题,求字符串长度
输入一串字符串
自己编写一个函数输出字符串的长度,但是!不允许使用临时变量
所以我们要想一想如何在不创建临时变量的情况下计算出字符串长度
可以把他拆解成如下的步骤
所以,我们可以把代码改成这样
再按照这个规律写一个阶乘的题吧!怎么弄呢?
输入5,输出需要是5x4x3x2x1 也就是说,加到这个数为0时,停止,5要依次递减1
其实阶乘的这个函数还能再简单:
太强了!
练习,求第n个柏菲拿起书
迭代
如递归的最后一题,实现阶乘的代码是
递归和迭代的使用方法(斐波那契数列)
斐波那契数列: 1 2 3 5 8 13 21 34 55 … 前两个数字相加等于后者
那我们先用递归的方式想想这题怎么做
设我们要求第n个斐波那契数列
那么在n <=2 时,F(n) =1
在n > 2 时,F(n) = F( n - 1 ) + F( n - 2 )
根据这个想法,我们就可以很方便的写出递归了
比如n现在等于4
那么他的值就等于F3 +1 = F1+F2+1
但是当n数值很大的时候,比如n = 1000的时候
就需要递归2^1000次方。内存很容易爆炸!
而且可以看到,效率非常的慢
这时我们用循环迭代来解决这道题
在使用递归的时候,如果递归层次太深,我们可以使用static对象来修饰局部对象为静态的,这样就不会创建很多的局部变量导致栈溢出
递归: 汉诺塔
汉诺塔,有三个柱子,柱子上有n个盘子,大盘子不能放到小盘子上
盘子在a 要到c上
思考一下
如果现在盘子都在 a 上, b c 为空盘, 目的是把所有盘子放到c上
- 1个盘子的话,也就是 a → c
- 2个盘子的话,也就是 a → b a → c b → c
- 3个盘子测话,也就是 先把上面两个盘子放到b上,a → c 再把两个盘子放到c上
也就是先把n-1个盘子先放到借助柱上,再把n盘子放到目标柱子上
用公式来表示的话可以这样写:其中a是起始地,b是借助,c是目标地
- f(1,a,b,c) = a → c
- f(2,a,b,c) = a → b a → c b → c
- f(3,a,b,c) = f(2,a,c,b) a → c f(2,b,a,c)
我们可以把f(3,a,b,c)等式右边的拆解成:
( a → c a → b c → b ) a → c ( b → a b → c a → c )
需要注意,当我们将变量代入函数时,这些变量代表的是具体的对象或状态。别弄混了
所以,我们也可以轻易地把f(4,a,b,c)换算成最终的步骤
也可以吧f(n,a,b,c)变成最终的步骤
f(n,a,b,c) = f(n-1,a,c,b) a → c f(n-1,b,a,c)
向下拆解时到n - 1,只需要把f(n-1,a,b,c )带入到f(n,a,b,c) 得到
f(n-2) a → c f(n-2) a → c f(n-2) a → c f(n-2)
直到带入到最底层 f(1) 或者f(2)时, 带入就可,下面是代码步骤
递归:小青蛙上楼梯
有一只小青蛙,一次能跳2个台阶或者一个台阶。
现在有n个台阶。问小青蛙跳上台阶有多少种可能的情况
有了上一题的洗礼,这一题应该不难了.
分析一波:
现在1个台阶,他跳一下就可以了。
现在有2个台阶,他跳1+1或者2 就可以了 有两种情况
有n个台阶,他跳了一次之后,计数器加一,然后现在有两种情况1. 还剩n-1个台阶, 2. 还剩n-2 个台阶 。
再跳一次之后,又有了这两次情况,直到他还剩最后一次跳跃所以可以写如下代码:
写一个函数,返回参数中二进制数中1的个数的一道很妙的解法
同理。我们也可以通过这个算法。去判断一个数是不是2的n次方
因为2的n次方有一个特点,他们的二进制数中只有一个1
所以只需要判断一下他们的二进制数中是否为一个1。
数组
一维数组
没啥说的
二维数组
int arr[][4] 列必须有,他们在内存地址上是连续的,他们在可以写成
int arr[3][4]{{1,2,3,4},{1,2,3,4},{1,2,3,4}};
数组的指针
对于一维数组
数组名除了两种情况都是代表数组的首地址
- sizeof(数组名) 这里的数组名代表的是整个数组,计算的是整个数组的大小
- &数组名,这里的数组名是整个数组的地址,不是首地址的地址
现在如果打印,可以到打印的是一模一样的地址。但是让他们同时+1。就会发现。数组的地址跳了10*4(用二进制算过之后)。
对于二维数组
二维数组的数组名表示的是 一行的地址,而不是首元素的地址
但是二维数组arr+1 表示的是二维数组的第二行,不是第二个
其实可以感觉的到,一维数组和二维数组,他们的arr在+1的时候都是跳过整个数组
根据
int = arr[3][4]={ { 1,2,3,4}, {2,3,4,5 }, {3,4,5,6 }} 可以看出来其实
{1,2,3,4} 就可以看成是一个二维数组的元素 所以我们在取地址的时候,取的其实是整个二维数组的第一个元素,也就是这第一行。我猜测同理,三维数组也是这样。
计算二维数组的行列
自适应棋盘+简单电脑+困难电脑+双人对战的三子棋小游戏
主函数
test.c可以看到有三个模式,输入1、 2、 3进入对应的模式,输入0则退出游戏.
输入别的就提示输入错误
在进入game函数中的时候会带入这个数字的值
#include "3ziqi_game.h" 包含的这个头文件中有#include <stdio.h>等必要头文件,所以主函数文件不需要再重复的包含头文件
下面我们看一下头文件:
3ziqi_game.h 可以看到,我们把所有的函数的头文件全部丢到这个文件里了, 可以方便移植。
下面看一下游戏的函数文件:
3ziqi_game.c 写的可能有点麻烦,但是都是自己敲出来的.花了我一下午的时间!!
看代码然后跳转函数的对应位置自己看吧!!
字符串逆序(递归实现)
编写一个函数reverse_string(char* string)(递归实现)
将参数字符串中的字符反向排列,不是逆序打印。实现 : 要求 : 不能使用C函数库中的字符串操作函数.
比如
char arr[] = "abcdef"
逆序之后数组的内容变成 : fedcba- 迭代解决方法
- 转换成函数的解决方法
- 递归函数解决方法
计算所有数字之和(递归实现)
实现输出n的k次方(递归实现)
数组的类型
扫雷游戏带0-9棋盘扩展和雷自定义
test.c
test.cgame.c
game.cgame.h
game.h删除10个数字中某个数字,并打印
输入10个整数成绩,求最高和最低分之差
写一个程序,输入大写字母输出小写的字母,反之亦然
水仙花变种题
奇数放前,偶数放后的函数
两个数组合并升序
操作符
原码,反码,补码
左移右移操作符只针对整数
正整数的原码、反码、补码相同
负整数的原码、反码、补码需要计算
7的
原码是000000000000000000000000000001117的
反码是000000000000000000000000000001117的
补码是00000000000000000000000000000111整形需要占四个字节, 一个字节是8个bit 因为7是整数,所以他的最高位为
-7的
原码是10000000000000000000000000000111-7的
反码是符号位不变,其余取反:111111111111111111111111 11111000-7的
补码是反码加1,11111111111111111111111111111001补码算反码的话就是先减1,然后符号位不变,其余取反
整数在内存中存储的是补码
左移操作符
左移操作符
<< 左边丢弃,右边补0对7左移
补码: 00000000000000000000000000000111
左移后:00000000000000000000000000001110
转换为原码:(正整数相同)
00000000000000000000000000001110
输出14
对-7左移
补码: 11111111111111111111111111111001
左移后:11111111111111111111111111110010
转换为原码:(负整数计算)
减1取反(符号位不变)
11111111111111111111111111110001
100000000000000000001110
=8+4+2=14
输出-14
综上:左移操作符的特点是乘2
右移操作符
右移操作符分为
算术移位和逻辑移位算数移位是 右边丢弃 左边补原符号逻辑移位是 右边丢弃 左边补0
编译器都是按照算数右移来操作的。
不管是左移还是右移。都不要出现 << -2 或者 2.5这种弱智行为
按位与 &,按位或 | 、按位异或 ^
- 按位与
先写出a、b的二进制补码
所以c的值就是3了!
同理
或 和 异或 就可以理解了不用临时变量,交换两个数字
因为异或支持交换律
那么如果有
牛逼
写一个程序,整数存储在内存中1的个数
也就是说求补码中1 的个数
赋值操作符
赋值操作符是可以连续赋值的
由右向左。语法支持,但是不建议这样写
复合付赋值符
+= -= *= /= %= >>= <<= &= |= ^=
单目操作符
对一个数操作的叫单目操作符
! 逻辑反操作 - 负值 + 正值 & 取地址 sizeof 操作数的类型长度,字节为单位 ~ 对一个数的二进制位按位取反 — 前置、后置— ++ 前置、后置++ * 间接访问操作符(解引用操作符) (类型) 强制类型转换
把某一位bit从0变成1 或者从1变成0
从0变1 用按位或1 其余为0 就可以
从1变0. 用按位与0,其余为1
++ , —操作符
++a #前置++,先++ 后使用a++ #后置++,先使用,后++间接访问操作符 *(解引用操作符)
sizieof的再理解
关系操作符
没什么可讲的,> < > >= <= != =
注意字符串不能用== 。应该用
strcmp 并添加string头文件
如果用== 实际上比的是两个字符串的呃首地址 abc == abcde逻辑操作符|| &&
逻辑与 、逻辑或
他们只关注真假,大于0则为真 不关注按位 || &&
没啥好说,但可以有在这道题来出
这里的运算是 && 为从左到右计算
短路求值类题
这道题因为a++ && ++b
a++是先使用再++ 所以相当于 0&& b 所以后面的就相当于不用算了
所以这道题只有a++了,和后面的都没加 也叫做
短路求值所以这道题的结果是 1 2 3 4
如果让a 初始值为 1
那么输出值就是 2 3 3 5
如果题中的&&变成 ||
因为a 为1 所以只有a++运算了, 后面直接就为1 了 不需要算了。
所以答案是 2 2 3 4
如果题中a的初始值为0 ,那么
a++ 和 ++b 就需要算了 他们或之后为1 后面的就不需要算了
所以答案是1 3 3 4
条件操作符
又称为三目操作符
表达式1 ? 表达式2: 表达式3
如果表达式1 为真,那么表达式2计算
如果表达式1 未假,那么表达式3计算
逗号表达式
逗号表达式,是从左到右依次计算,最后一个为表达式的结果
比如
那么a 就等于 3+b = 3+(a-c) = 3+(a-a-b) = 3+0-5=-5 .
利用逗号表达式可以很方便的让我们的代码更加的整洁
比如,原本的一个递归函数是
这个函数就是,一直递归,直到a=0
但是看着还是有一些臃肿的
我们用逗号表达式可以这样来写
下表引用、函数调用和结构成员
下表引用
其中
[] 为下表引用操作符 他的操作数是 arr 和 7所以你也可以写成
7[arr] = 8他的结果是一样的其实啊。
arr[7]其实等于*(arr+7)其中 arr是数组首元素的地址 arr+7就是arr的第八个元素,而*就是解引用。对他的第八个地址解引用。
函数调用
可以看到
( ) 是函数调用操作符
他的操作数函数Add和a ,b 函数在调用的时候,操作数至少是一个
结构成员
代码1
这里的函数在传参的时候,只是传送了一个s
那么形参就相当于是实参s的一份临时拷贝,
我们再函数中对ss的一切操作都不会影响到实参的s
所以我们应该用指针去指到实参s的地址
代码2:
这里我们把s的指针传送过去,&s。
对应的形参也要修改成保存地址的指针类型
在使用的时候,我们应该对他进行解引用,来正确的指到我们想要的变量的身上。
其中。printf_stu就不用指针传参了,因为他只负责打印,是不是临时拷贝不重要。只要值一样就OK。但是为了不浪费内存空间,我们可以把& 加上~~
但是这个代码还可以这样写, 这样他的观赏性就会好很多, 不用* 了
代码三
在这里我们用到了
箭头操作符箭头操作符使用时 是 结构体指针 -> 成员
点操作符使用时 是 结构体对象 . 成员
表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性来决定 同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型
优先级问题, 乘除的优先级是超过加减的
结合性问题,优先级相同的情况下不同的符号他的计算顺序不一样(L→ R 或者R→ L)
隐式类型转换
C的整数算数运算总是包含至少以缺省整形类型的精度来精度来进行的
缺省(是至少默认用整形的)
为了获得这个精度,表达式中的字符或短整形操作数在使用之前被转换为普通整形,这种整形称为整形提升
举个栗子
比如 5 + 126 但是char的字节大小是1 所以他需要进行整形提升 变成字节大小为4 的int型
下面我把这个计算的步骤写出来
cahr类型的bit位是8个,意味着他只能存储8位二进制数字。
char a = 5;的阶段中 因为5的的二进制序列为00000000000000000000000000000101但是char类型只能存储8个bit 所以他存储的就是00000101
- 同理,在
char b = 126;的阶段中 因为126的的二进制序列为00000000000000000000000001111110char b 中存储的是01111110
在char c = a + b;中,a和b的大小才8个bit 不足以进行整形相加 所以此时就有发生了整形提升 a 变为了00000000000000000000000000000101b 变为了00000000000000000000000001111110那么他们在相加后c就是000000000000000000000000100000011但是char c不足以放那么多,只能取8位,也就是10000011
printf("%d\n",c);时,因为c的值只有8为,不能打印整形 所以又发生了整形提升。char c在提升为整形的时候,取了第bit位最左边的值为符号位。所以这个值就变成了11111111111111111111111110000011在把他变成反码(减1 取反.符号位不变)10000000000000000000000001111101打印出来也就是 负的 125
再举个例子
其中 char short 字节是2 和4 那么他们都会进行整形提升 所以最后只会输出c
再举个栗子
这里的结果是1 4 4 因为-c +c 都进行了整形提升
整形提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度-般就是int的字节长度,同时也是CPU的通用寄存器的长度。 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。 通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
算数转换
long double double float unsigned long int long int unsigned int int
如上表, 在进行运算的时候,排名低的会转换为排名高的类型,然后进行运算
double f = 3.14;
int num = f //隐式转换, 会精度丢失
操作符的属性
复杂的表达式有三个影响因素
- 操作符优先级:C语言运算符优先级(超详细)-CSDN博客
- 操作符的结合性
- 是否控制求值顺序
两个相邻的操作符先执行那个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性
不然在有相互影响的表达式时,就会存在问题
比如a*b + c*d + e*f
不管 1 3 2 5 4
还是 1 4 2 5 3
的计算顺序,都复合我们的优先级和结合性、但是在复杂表达式中的计算结果是不一样的!或者干脆在不同编译器下的结果是不同的
在写代码的时候,一定要让表达式的计算有唯一路径的 不然就是有问题的!!!
写一个函数,输入两个数字。输出他们的二进制数有几位不同。异或法 和 移位对比法
写法1 按位比较
写法2 异或比较
打印数字中二进制序列的奇数位和偶数位
偶数位从0开始
打印可以根据输入动态变化的x
输入年份,月份,输出这个月天数
指针
指针是什么
指针是内存中最小单元的一个编号,也就是地址 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
针对
int a = 10 运行时就会在内存中开辟4个字节的内存
而取a的地址时。&a 所拿出来的地址时第一个字节的地址
如果我们要把这个地址存起来,就要用int* pa = &a;
其中pa是指针变量,是用来存放地址的
本质上指针就是地址
口语中说的指针其实是指针变量,指针变量就是一个变量而已。
指针变量是用来存放地址的一个变量!
通过指针变量中存放的地址,可以找到一个内存单元- 一个内存大小是1字节
- 编址空间
在一个32位的机器上,有32根地址线,就有2^32次方种可能。也就是2^32次方bit经过换算,也就是4GB。而指针变量就是记录这32跟线的高低电平。对于32位的,就需要32bit的空间,也就是4字节
同理64位的机器也是一样。他则需要8字节才能存储
综上:32位的电脑的指针变量的大小是4个字节。 64位的电脑的指针变量的大小为8个字节
指针和指针类型
指针 + - 整数
int* char* double* …..
这么多指针变量。他们在32位环境下都是存放4个字节。那为什么还要有这么多的分类呢?
他们能混用吗?
在这里拿int*和char*举个栗子
如果我们定义了
int a = 0x11223344 0x指的是16进制
此时我们通过监控窗口可以看到在a的地址下存放的4字节是44 33 22 11
注意这里是16进制的每个数字是都是用四个bit来表示。
- 我们现在定义int*类型的指针变量pa赋值为a的地址 int* pa = &a;
在运行的过程中,我们可以看到pa和 &a 所取得的地址是一样的。
如果此时我们令*pa 赋值为 0, *pa = 0; 可以看到。变量a被赋值为 00 00 00 00
所有bit都被赋值为0,代表16进制的0- 但是如果我们在这里用
char* 来创建指针变量pc 来赋值为a的地址。编译器会报警告。
但我们如果强制把&a来转换为int*类型的指针类型。cahr* pc =(int*)&a;
这次在运行的过程中,我们可以看到pc和 &a 所取得的地址是仍然一样的。
但是如果我们此时令*pc赋值为 0, *pc = 0; 可以看到。变量a被赋值为 00 33 22 11
只有存储44的这1个字节被赋值为0。因为char*类型的变量他操作的是一个字节!我们如果用
%p打印pc和pa 所存储的地址会发现他们是相同的,都是a的地址&a
但是如果用%p打印pc+1 和pa+1 我们会看到int*类型的指针变量 pa+1 比pa大了4个字节
而char*类型的指针变量pc+1比pc只大了1个字节总结:指针的类型决定了指针向前或向后的步长有多大 指针的类型决定了指针的权限(操作几个字节)
指针的解引用
!!注意,即使是类似int*和float*的字节大小一样的也不能混用。
因为不同的指针类型他们对字节内容的解读是不一样的。
比如
int* = 100时。他在内存中存放的是64 00 00 00
而float* = 100.0,他在内存中存放的是 00 00 c8 42野指针
野指针的概念:
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
- 指针未初始化
如上代码,p没有初始化,就意味着没有明确的指向
一个局部变量如果不初始化的话,放的是随机值
这里P就是野指针了,他非法访问内存
- 指针越界访问
- 指针指向的空间释放
这里放在动态的内存开辟的时候讲解,这里做一个简单的演示
在这个演示中,p在接受a的地址后,因为a是局部变量。在函数结束后就被释放了。如果你在下面再用a的地址去找到这片区域时它并不能访问。那么p就是野指针了。
如何避免野指针的出现
当一个指针不知道初始化什么值的时候,
就把他赋值为空指针NULL
在下次使用的时候,添加if判断。这样就可以避免出现问题
如果对空指针赋值,会程序报错
- 指针初始化
- 小心指针越界
- 指针指向空间释放即使置NULL
- 避免返回局部变量的地址
- 指针使用之前检查有效性
指针的运算
指针的+-运算
这个示例代码中。
*vp++ = 0; 为先使用后++所以第0 1 2 3 4 个指针都会被赋值为0
这里的++指的是地址++
这里的++指的是地址++
并且这里的for循环在检测vp内存储的地址是否小于&valueS[N_VALUES]时。
虽然看了一眼数组之外的数值,但是实际并没有操作这块没有权限的空间,
所以也不是野指针指针-指针
在这个举例中。
p2 为最后一个元素的地址,p1为第一个元素的地址。他们中间的元素有9个。所以p2 - p1 的值打印出来为9指针-指针实现字符串求长度
start指向的是第一个字符 在while循环之后str指向’\0’指针的关系运算
这个代码也可以实现把数组中的五个元素赋值为0 这里的
--vp是先--后解引用赋值为0,可以避免对下标 为5的没有权限控制的内存空间操作。避免野指针可以简化一下如上代码。
这里是先判断。然后赋值为0.然后 -- 注意 vp的初始位置改了哈
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,
但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
指针和数组
二级指针
二级指针变量 是 用来存放一级指针变量的 地址
指针数组
指针数组可以存放多个变量的指针
指针数组可以把多个数组模拟为二维数组
结构体
1. 结构体的声明
1.1 结构的基础知识
结构是一些值的集合,这些值称为成员变量,每个成员可以是不同类型的变量
1.2 结构体的声明
1.3 结构成员的类型
结构体的成员类型可以是标量,数组,指针。甚至是其他的结构体
1.4 结构体的定义和初始化
结构体的创建
结构体后面直接跟变量,则是用他创建了一个全局变量(因为一般结构体都是在
main函数外边放着的。) 比如这里代码中的p1,p2,p3,p4; 就是创建结构体时同时创建的结构体变量。也可以在
main函数或者别的函数中 用struct Peo p5 来创建新的局部变量也可以在
main函数外边 用struct Peo p5 来创建新的全局变量。这些都是可以的结构体的初始化
对于一个结构体,我们在初始化的时候可以直接
struct peo p5 = {”张三”,“123456789”,“男”,“181”};对于嵌套了结构体的结构体。我们在初始化时需要把结构体的结构体括起来
struct peo p5 = {{”李四”,“987654321”,“女”,“161”}, 100, 3.14};2. 结构体的访问
结构体变量访问成员
结构变量的成员是通过点操作符
.访问的。点操作符接受两个操作数。如果我们接受的是结构体的指针。那么我们在访问的时候就需要用箭头操作符 ->
printf("%s %s %s %d\n",sp->name,sp->tele,sp->sex, sp->high);
结构体指针 -> 成员变量3. 结构体传参
函数传参的时候,参数是需要压栈的。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。 所以结构体传参的时候尽量去传结构体的地址
学会调试
2.1 调试是什么?
调试(英语:Debugging/Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序
错误的一个过程。
2.2 调试的基本步骤
- 发现程序错误的存在
- 以隔离、消除等方式对错误进行定位
- 确定错误产生的原因
- 提出纠正错误的解决办法
- 对程序错误予以改正,重新测试
2.3 Debug和Release的介绍。
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。 Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
2.4 调试快捷键
F5
启动调试,经常用来直接跳到下一个断点处。 创建断点和取消断点
F9
断点的重要作用,可以在程序的任意位置设置断点。 这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。
F10
逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)
CTRL + F5
开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用
一些小技巧
- 用的最多的一般是调试,窗口和内存、调用堆栈的监视功能。
断点可以右键暂时关闭,也可以右键断点设置触发断点的条件
- 数组传参后输入
数组名,数字。就可以看到数组的n个元素了。而不止是首元素
如何写出优秀的代码
- 代码运行正常
- bug很少
- 效率高
- 可读性高
- 可维护性高
- 注释清晰
- 文档齐全
常见的coding技巧:
- 使用assert ,//断言。判断然后警告
- 尽量使用const //这个是用来放置被修改,来保护他的内容 const 修饰指针时的一些理解 const 修饰int* p 则*p所指向的对象不能修改,但是p所存的地址可以修改 int* const 修饰 P 则 p所存的地址不能修改,但可以修改*p所指向的对象
- 养成良好的编码风格
- 添加必要的注释
- 避免编码的陷阱。
求数组长度。优秀代码版
常见的一些错误
- 编译型错误 直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。
- 链接型错误 看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
- 运行时错误 借助调试,逐步定位问题。最难搞。
- 作者:L_Z_J
- 链接:https://www.mcoi.top/article/Post-C-firststage
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。










