一、结构成员
##引子##
如果我么想规划一个区域来存储我们的相关信息,但又不想很杂乱的存储,那我们可以用到结构体这个函数,来帮助我们结构化管理信息,我们暂时只介绍“.”
1.结构体
它可以用于复杂的、多个对象的,属于自定义数据类型,你可以自定义一些值的集合,值可以是不同类型的变量,甚至是嵌套结构体
我们给出示例:
struct tag(函数名)
{
//一些成员列表
}(变量的列表,可有可无,可以在创建结构化函数时候创建变量,默认是全局变量);
##我们给出一段代码,参照上面的注释,我想你应该能明白,这里不过多赘述啦
struct adr
{
char name[10];
};
struct stu//先规定好结构体内部信息
{
struct adr one [10];//这里的one是结构体名称
char name[10];
int age;
float score;
};
int main()
{
//struct stu s1 = {"xiao_san",20,95.5f};//我们可以利用这个结构体存储不同的信息,这里s1代表一号学生
//我们也可以不按照顺序初始化,可以指定初始化内容,利用“.”
/*struct stu s1 = { .name = "zlh",.age=18,.score=95.5 };*/
//如果结构体里面套一个结构体呢?这个时候我们如何初始化呢?
struct stu s3 = { {"heihei"},"wangwu",20,99.9f };//这里我们的括号里的括号就是针对adress来初始化的,如果里面还有数组,我们再加括号即可
printf("%s\n", s3.one[0].name);//你可以添加多个“.”来访问多层结构体里的元素,注意结构体中你规定的存了10个信息,下标也是从0开始
return 0;
}
二、操作符优先级
1.优先级
表达式含有各个运算符,优先级高的先执行,例如3+4*5=23;
2.结合性
假设优先级相同,那这个时候结合性说了算,究竟是左结合还是右结合,大部分都是左结合,少部分右结合,例如5*6/2=15,具体参照下表:
##你肯定想到这么多,我们懒得记,我们就记一些常用的吧
1.圆括号( () ),自增运算符( ++ ),自减运算符(-- )
2.单目运算符( + 和 ) • 乘法( * ),除法( / ) • 加法( + ),减法( )
3.关系运算符( < 、 > 等)
4.• 赋值运算符( = )
如果你不想这么麻烦,有一个终极办法,你想哪个部分先计算,就把哪个部分用括号括起
三、表达式求值
1.整型提升
整型默认是int类型,如果你想用long long类型你只需在数据后加上字母“ll”,为了获得这种精度,表达式中字符和短整型使用之前要被转化为普通整型
整型提升规则
1.有符号整型按照变量的数据类型的符号位提升,即其他位要么补0要么补1
2.无符号整数提升高位直接补0
为什么要这样做,这是因为CPU难以用除4字节或者8字节其他非4字节类型计算
##我们举个例子:
int main()
{
char a = 20;
char b = 120;
char c = a + b;
printf("%d", c);//为什么结果是-116?
return 0;
}
为什么结果是-116?为什么你正数结果后面给我搞出个负数?诶,我们娓娓道来
##拓展:%d我们也可以十进制形式打印一个有符号整型int
1.首先我们要知道char类型的取值范围是-128~127,而你变量C求出来后是140,存不下呀
2.而且你char类型的变量只能接收一个字节大小,即8个比特位,对于二进制8位,那必然要对你二进制表示进行取舍
3.我们开始存取数据
变量a的值是20,转化成二进制后的补码是0000 0000 0000 0000 0000 0000 0001 0100,而你char类型只能存8比特位,即存入0001 0100,相当于把20这个值截断后存入
同理我们来存入b变量值120,转化成二进制补码是0000 0000 0000 0000 0000 0000 0111 1000,同理char类型变量我们存入8比特位,即0111 1000
4.要想相加,你只有8比特位显然是不行的,要进行整型提升,按照规则,a提升后为(高位补0):0000 0000 0000 0000 0000 0000 0001 0100,同理b为:0000 0000 0000 0000 0000 0000 0111 1000,我们再把两个整型提升后的变量相加,结果是:0000 0000 0000 0000 0000 0000 1000 1100
5.我们再把这个变量存入char类型中,截断后为1000 1100,但是你打印类型是整型,我们得再次发生整型提升(根据规则,符号位是1,我们高位补1)之后再转换成原码进行打印:1000 0000 0000 0000 0000 0000 0111 0100,打印的结果就是-116了
##怎么样,看似简单的算式背后可是有很复杂的逻辑
2.算数转化:如果你的各个操作数属于不同类型,我们要将其中一个操作数类型转化成另外一个操作数类型,我们根据下面这个表由下到上转化优先级(int是起点)
感谢比特教育科技提供的图解,感谢
四、问题表达式
##有时候有些人在写程序,也不知道脑回路是怎样的,写的程序让编译器都不知所措
1.加法优先级问题
a*b+c*d+e*f;
这个表达式如果是你,你肯定先算乘法再加起来
但是机器不管这些,都有自己独特算法
比如我们先计算前两个乘法,相加后,第三个乘法计算后,再相加
又或是先计算三个乘法,把前两个结果相加后得到的结果,再和第三个乘法结果相加
如果是变量值还好,但假设是表达式呢,是不是会影响最终结果
2.C到底何时加
C+ ++C;
这个代码问题在于到底是什么时候加上C,是我++之后还是++之前呢?
3.逻辑混乱
int i = 10;
i = i-- - --u*(i=3)*i++ + ++i;
第一眼你看到这个代码是不是想这个表达式到底要传达什么意思,如果你用不同编辑器去计算,会有不同结果,这个算式出自于一本书《C和指针》作者都抓狂了这个代码
感谢比特教育机构提供的图
4.Fun函数究竟什么时候开始调用
int fun()
{
static int count = 1;
return ++ count;
}
int main()
{
int answer;
answer = fun()-fun()*fun()
printf("%d\n",answer);
return 0;
}
##如果你把这个表达式放在VS2022中,结果就是2-3*4=-10
5.这位更是重量级
int main()
{
int i = 1;
int ret = (++i) + (++i)+(++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;//VS中是12 4,而你用Dev是10 4
}
为什么是截然不同的结果
在VS中是算出三个++i之后的i的结果再把三个i相加
而在Dev中是先算出前两个++i之后,先把前两个i相加,最后再i再加1,之后再把上一次相加值加到这一次
我搞不明白为什么有的学校把这个当成期末考试题还给出选项
因此,我们不要写以上这些难看的low代码,想先算哪个加括号就好
五、内存
1.我们都知道1byte(比特)=8字节,其他都是1024进制转化
2.我们把内存划分成一个个小的空间,每一个空间都是一个字节,为一个内存单元,我们可以给内存单元编号,就是指针,就是地址
3.各个硬件之间进行数据传递,CPU与内存之间有很多“线”连接起来,一般有“地址总线”,“数据总线”,“控制总线”,我们读写都是通过这些进行的
4.CPU访问内存的时候需要知道其位置,我们就给内存编址,给每个内存单元编号(硬件设计的时候已经编好)
32位机器有32根地址总线,每根总线有0和1两张状态,攻击2的32次方中编号,64位机器择优2的64次方种编号
六、指针变量和地址(初阶)
1.取地址操作符(&)
我们回忆下,变量创建的本质是什么,就是在内存中申请4个字节空间,以此来存储数据
例如int a =10,其中这个a不是给编译器和计算机看的,是给你看的,它们看的是地址
你可以在调试中找到监视输入取地址操作符查看变量a的地址,中间那一行是十六进制存储的,比如10=1011=啊,我们就可以看到000a
但是a它有四个字节啊,不应该有四个地址吗,如果你通过取地址操作符,你会看到是地址中较小的地址,为什么?
因为这样可以顺理成章求出其他地址,存放地址的变量就成为指针变量,如:
int *p=&a,这里的int *就为a的类型,而且指针变量是专门存放地址的,在它的眼里什么都是地址
2.解引用操作符(“*”)
int a =10;
int *p=&a;
*p=0;
我们注意区分下面两个式子区别,下面那个式子中,“*”是用来间接访问的,通过p的地址找到地址所对应的对象,这是另一种放大求变量
3.指针变量的大小
因为指针也是变量,也是需要向内存中申请空间的
我们根据地址存储大小,32位的机器通过32跟地址线传输,需要32bit=4字节空间存储
因此指针变量大小是4字节,不管你指针变量里面的数据是什么类型
统统都是4字节,而六十四位机器就是8字节
七、指针变量类型的意义
1.指针解引用
话说每个指针变量大小都相等
那我们为啥还要规定指针的变量呢?这么做的目的是什么?我们可以举个例子来看看
int main()
{
int a = 0x11223344;//我们这么做的意义是让每两个十六进制位各占一个字节,从而占满我们int类型的四个字节
int* pa = &a;//把a的地址存入指针变量pa中
*pa = 0;
return 0;
}
a中地址就全为0了
通过调试我们看看,的确都变成了0
但若换成char呢
你会发现只有第一个字节变成了0,其他没有变化
因此我们规定指针变量类型的意义就在于
每种类型决定了指针变量它的权限有多大,它们各自的“范围” 不同,产生的效果也就不同
如果你换成double等其他大空间类型,会发生越界访问
所以,权力越大越危险(开玩笑的)
2.指针加减
我们先举个例子,我们把下列程序结果打印
int a = 10;
int *pa = &a;
char *pc = &a;
我们发现无论是int还是char类型的指针变量,打印的地址都是一致的
但是如果你打印printf+1或者-1呢?
你就会发现,怎么结果不一样啊,所以,这就是我们要说的
你的指针变量类型就决定了你指针向前/向后能够走多远
3.Void*指针
##它是指没有具体类型的指针,它也叫泛型指针
通常用途函数参数部分,或者是泛型编程,无论你是什么数据类型都可以用
##但是它也是有缺点,不能进行直接的加减整数和解引用
八、指针运算
1.“++”“--”整数型运算
对于数组元素,我们只需要知道第一个元素地址即可顺着下去知道其他元素地址
结合我们用到的指针知识,我们可以通过指针来循环打印数组
以下只是一只示例,还有其他很多种写法,就不一一列举了,你可以用while循环来写
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int sz = sizeof(arr)/sizeof(arr[0]);
for(int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
通过观察我们发现,你p加上i,就是数组第i元素,也就是数组第i元素地址,很巧妙吧
2.小练习
试着规定一个数组,用循环打印hello world,打印遇到“\0”我们就停止
char arr[20] = "hello world";
char* pa = arr;
while(*pa != '\0')//不等于要这么写,打印字符用%c
{
printf("%c",*pa);
pa++;
}
我们发现我们也可以利用指针特性来打印字符串了,相信你还有其他更多点子
3.指针-指针
##指针相减前提是两个指针指向同一块空间,否则无法相减
比如我们举个数组例子
int arr[10]={0};
printf("%lld",&arr[9]-&arr[0]);
我们可以看到结果是9,为什么?
这个结果好像我们的数组的元素个数减去1啊
不难发现,我们根据&arr[0]+9==&arrr[9],通过移项变号
我们就可求得两个指针之间的元素个数,地址较高的在前在后相减会有正负
因此|&arr[0]-&arrr[i]|==i
4.指针关系的运算,我们以打印数组为例
##如果你输入的数组元素下标超过了数组本身大小,就会产生越界
指针也是一样,通过指针我们判断数组元素边界,就为首元素地址加上数组大小
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int sz = sizeof(arr)/sizeof(arr[0]);
while(p<arr+sz)//通过指针我们可以比较数组的边界
{
printf("%d ",*p);
p++;
}
return 0;
}
九、Const修饰指针
##如果你在变量前加入const,变量就赋予了常属性
即变量不能再改变,但本质上还是变量,只是在语法层次上进行了修改
但是如果我们非要修改,怎么办?诶,我们可以找指针这个中间商!
const int a = 100;
int *pa = &a;
*pa = 20;
通过打印结果我们发现,还真改了
假设我们在指针变量左边加入const呢?
我们不难发现,我无法通过指针*pa改变这个变量a的值了,但是我可以改变pa中存放的地址
同理我们再指针变量右边加入const呢?结果就反过来了
我们得出结论
const如果放在指针变量左边,修饰的是指针指向的内容,这个内容不能通过指针改变, 但指针变量本⾝的内容可改变
const如果放在指针变量右边,修饰的是指针变量本⾝,使得指针变量的内容不可改,但指针指向的内容,可以通过指针改变
##你还可以两边都加入const,让其两张方式都不可改
作者学术不精,难免有疏漏,若有错误欢迎指出,评论区讨论
END