在上篇内容中,我们学习了关于指针变量、野指针和部分指针运算的知识,对于指针有了一个初步的了解。本篇文章中将延续上文展开剩余篇幅。
指针的关系运算实际上就是指针比较大小(地址比较大小)
当我们想要打印一个数组的内容的时候,通常会这么做
#include <stdio.h>
int main()
{
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
但是当我们学了指针的关系运算之后,我们就可以将指针的比较作为循环条件来打印数组
#include <stdio.h>
int main()
{
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int sz = sizeof(arr) / sizeof(arr[0]);
int *p = &arr[0];
while (p < arr + sz)
{
printf("%d ", *p);
p++;
}
return 0;
}
我们知道,变量是可以被修改的。但是有时候在程序中我们并不希望创建的变量被修改,此时就可以用const修饰这个变量
上图中,变量x没有被const修饰,所以可以后续进行修改数值;变量y被const修饰后,再对它进行修改就会报错。
虽然我们无法修改被const修改的变量y,但是const仅仅是在语法上做了限制,y的本质还是变量,所以习惯上,我们称呼y为常变量。
但是尽管const修饰了变量y,我们还是可以通过指针使用y的地址去修改y的数值
接上文,如果我们要想避免别人通过地址来修改被const修饰的变量的数值,就要用const将指针变量也修饰了
此时*p被const修饰后,再想通过地址修改变量y的值就会报错
另外的,const可以放在 * 的前面,也可以放在 * 的后面
1.const int *p
2.int const *p
3.int * const p
1和2中,const都在 * 的左边,此时修饰的是*p,也就是此时我们不能使用*p修改其指向空间的内容,但是我们仍然可以改变*p指向的空间
3中const在 * 的右边,此时修饰的是p,也就是此时我们不能改变*p指向的空间了,但是仍然可以使用*p修改其指向空间的内容
有点绕,但是不难理解。
C语言中有宏assert(),被称为断言。我们可以在括号中输入一个表达式作为参数,若表达式的结果为真,程序继续执行,结果为假则报错并终止运行。
例如在上面const的讲解中,假设const只修饰了变量y,我们希望避免误操作导致使用了地址修改其数值,此时就可以使用assert()宏,使用之前记得包含<assert.h>头文件。
我们将y初始化为0并且使用const修改后,不希望后续操作中修改其数值,就使用assert(y==0)来验证变量y的数值是否被修改,此时再使用地址来修改其数值就会报错
assert()的使用对程序员是十分友好的,它不仅能给出出错的行号,同时当我们已经确认程序无误后,不需要再做断言,我们就可以在头文件#include <assert.h>前面定义一个宏NDEBUG
#define NDEBUG
#include <assert.h>
先举个例子,我们写一个Add函数实现整型相加
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 2;
int b = 3;
int ret = Add(a, b);
return 0;
}
此处,我们将a和b的值传到了Add函数中,此时为传值调用,Add的形参和实参占用的是不一样的空间
在一些情况中,我们使用传值调用就可以解决问题,但是当我们在解决一些交换变量内容之类的问题的时候,传值调用就没有办法得到我们想要的效果,例如:
这是因为,传值调用中,实参传递给形参的时候,形参会创建一块临时空间来存放实参的值,所以此时形参和实参并不位于同一块空间,对形参的修改也影响不到实参
要解决这个问题,我们只需要把地址作为参数传入函数中
就顺利实现了我们想要的效果,这种方式就叫传址调用。
如果我们只需要使用变量值来进行计算,可以使用传值调用;但是当我们要在函数中修改实参的内容时,就要使用传址调用了
实际上,数组名就是数组首元素的地址。验证如下:
#include <stdio.h>
int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
return 0;
}
我们创建一个数组,然后分别取arr和&arr[0]并打印地址,接着我们会发现二者完全一样
但是例外的,当我们运行这段代码时
#include <stdio.h>
int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
printf("%d",sizeof(arr));
return 0;
}
输出的结果是40,也就是整个数组的大小,但是arr不是首元素地址吗?输出的应该是4或8才对
事实上数组名的理解有两个例外:
除此之外,其他任何地方在使用数组名的时候都表示首元素地址
在学习指针之前,我们访问数组的内容通常是这样的
#include <stdio.h>
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
学习指针之后,我们就可以用这种方式
#include <stdio.h>
int main()
{
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int sz = sizeof(arr) / sizeof(arr[0]);
int *p = arr;
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
我们会发现,现在arr和p中存放的都是首元素的地址,所以arr和p是等价的,那么我们是否可以把arr[i]换成p[i]呢
完全没问题。而且我们也可以将*(p+i)替换成*(arr+i)
也就是说,arr[i]和*(arr+i)其实是等价的,按照交换律
* ( arr + i ) == * ( i + arr )
∴ arr [ i ] == i [ arr ]
这种方法虽然可以,但是并不推荐这么做。平时我们还是最好按照习惯去写
我们知道,数组也可以作为参数传递给函数。但是实际上传参的时候并不是传递一整个数组,而是传递数组的首元素地址。
例如我们分别在函数外和函数内分别求一个数组的元素个数
因为只是把数组的首元素地址传入了函数中,所以无法得到正确的数组元素个数。另外,函数的形参既可以写成上图中数组的形式,也可以写成指针的形式
我们知道,指针变量内部存放了变量的地址,但是指针变量本身也是个变量,也有自己的地址,那么:指针变量的地址该存到哪里呢?
这里就引入二级指针的概念了
#include <stdio.h>
int main()
{
int a = 10;
int *pa = &a;
int **ppa = &pa;
return 0;
}
二级指针的一个显著标志就是有两颗 *,后一个 * 说明ppa是指针变量,前一个 * 和 int 组合说明ppa指向的类型是 int * 类型的变量。
(地址随便编的)
对二级指针解引用就像剥洋葱一样,一层解完解下一层
*ppa = pa
*pa = a
学习指针数组的时候很多人都容易混淆:指针数组是数组还是指针?
实际上很容易区分,就像整型数组是存放整型的数组、字符数组是存放字符的数组一样:指针数组就是存放指针的数组。
指针数组的每个元素都是一个指针,也就是一个地址,分别指向不同的区域。
这么说来,前面提到数组名就是数组首元素地址,那么我们是否能使用指针数组模拟二维数组呢?
#include <stdio.h>
int main()
{
int arr1[5] = {1, 2, 3, 4, 5};
int arr2[5] = {2, 3, 4, 5, 6};
int arr3[5] = {3, 4, 5, 6, 7};
int *arr[3] = {arr1, arr2, arr3};
for (int i = 0; i < 3;i++)
{
for (int j = 0; j < 5;j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
上面这段代码中,arr是一个指针数组,操作符[ ]的优先级比 * 高,所以arr先和[ ]结合,代表arr是一个数组,arr前面的int *说明了数组内的元素类型
虽然我们可以使用指针数组模拟二维数组,但是二维数组中的元素都是连续的,而指针数组中的元素并不连续。
同上,数组指针变量就是指向数组的指针变量,存放了数组的地址,实质上是一个指针变量
那么问题来了,你能认得出哪个是指针数组哪个是数组指针吗
int *p [10]
int (*p)[10]
上面刚提到,操作符[ ] 的优先级比 * 高,要创建一个数组指针变量,就要用括号将*和指针变量名括起来,变量名先和*结合说明是一个指针变量,否则就会先和[ ]结合变成一个数组,所以第一个是指针数组,第二个是数组指针变量
现在我们知道了数组指针变量是用来存放数组的地址的,现在试试使用&操作符将数组的地址存到数组指针中吧
数组指针的类型很好区分,我们去掉指针变量名,剩下的就是数组指针的类型:
type (*p ) [ ]
<type>是数组指针指向的数组的元素类型,方括号中的数字就是数组指针指向的数组的元素个数
前面我们已经使用指针数组模拟过二维数组了,实际上,二维数组传参的本质和其十分相似。
二维数组,可以看作每个元素都是一个一维数组的数组,前面我们在学习中也知道了数组名就是数组的首元素地址。所以二维数组传参的时候实际上就是传递了首元素的地址——也就是第一个一维数组的地址。
那么我们用二维数组名加减整数,就可以访问其内部的一维数组;再对其内部一维数组的地址加减整数,就可以访问一维数组中的每一个元素。
通过这两层关系,我们就可以实现访问二维数组的每一个元素。
我们知道指针变量的类型有int *,char *等等,而类型为char *的字符指针变量也有一些知识需要我们了解
平时我们可能很少使用到char *类型的指针变量,顶多可能偶尔会写到这样的代码
#include <stdio.h>
int main()
{
char ch = 'w';
char *pc = &ch;
return 0;
}
很简单,很表层,但是字符指针变量并不仅限于这点功能
还有一种使用方法如下:
#include <stdio.h>
int main()
{
char *pc = "hello";
printf("%s\n", pc);
return 0;
}
可能有些同学会以为我将“hello”这一个字符串都放在字符指针中了,实际上,当我们想将一个字符串存到字符指针中时,其实只是把首字符的地址放在了这个字符指针中
《剑指offer》中有一道和字符指针、字符串相关的笔试题,通过这道题我们再对字符指针有一个深入的了解
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
if(str1 ==str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if(str3 ==str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
运行这段代码,输出如下
当我们使用相同的字符串去初始化两个数组的时候,他们会开辟出不同的空间;而当几个不同的指针指向同一个字符串的时候,实际上这些指针指向了同一块内存。
根据之前学习不同指针变量的经验,我们不难理解:函数指针变量就是存放函数的地址的指针变量
难道函数也有地址吗?我们可以测试一下
#include <stdio.h>
void test()
{
printf("hello");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
随便创建一个函数,我们尝试打印它的地址,输出结果如下
确实打印出来了地址,而且我们发现直接用函数名和&函数名都可以获得函数的地址
说明:函数名就是函数的地址
知道了怎么取出函数的地址,我们就要知道怎么存放函数地址。函数指针变量的指针类型长得其实和数组指针非常相似:
type (*p ) ( )
其中,<type>是函数指针指向的函数的返回类型,*后面跟着函数指针变量名,第二个括号内填入函数指针指向的函数的不同参数类型
例如:
#include <stdio.h>
void test()
{
printf("hello");
}
int Add(int x,int y)
{
return x + y;
}
int main()
{
void (*p1)() = test;
int (*p2)(int, int) = Add; //第二个括号中可以只写类型不写参数名
return 0;
}
我们知道,平时调用函数的时候一般只会写函数名,而函数名也是函数的地址,我们将函数地址存到指针变量中后,指针变量名是否就和函数名等价了呢?
#include <stdio.h>
int Add(int x,int y)
{
return x + y;
}
int main()
{
int (*p)(int, int) = Add;
printf("%d\n", (*p)(2, 3));
printf("%d\n", p(3, 5));
return 0;
}
事实上,不管是否对函数指针解引用,我们都可以调用函数指针指向的函数
输出结果:
typedef 可以帮助我们将一些复杂的类型重命名
例如我觉得 unsigned int 这个类型太长了,写代码不方便,能不能改简单一点呢?
我们只需要:
typedef unsigned int uint;
上面,我们就使用了 typedef 将 unsigned int 重命名为了 uint ,在后续的代码中我们只需要使用 uint 就可以实现 unsigned int 的功能了
当然,指针变量也能修改
typedef int* pty_t;
但是对于数组指针和函数指针,修改的方式就有所不同了,我们要在第一个括号内部修改新类型名
typedef int(*parr_t)[10];
typedef void(*pfun_t)(int,int);
存放函数的地址的数组就是函数指针数组,我们已经学习了指针数组和函数指针,那么你知道函数指针数组该怎么定义吗?
type (*parr [ ]) ( )
其中,parr先和 [ ] 结合,说明是一个数组,其内部元素类型是 type(* )( )
学习函数指针数组后,我们就可以用它制作转移表
例如我们写一个计算器,没学函数指针数组之前会这样写
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf("*************************\n");
printf("****** 1:add 2:sub ******\n");
printf("****** 3:mul 4:div ******\n");
printf("****** 0:exit ******\n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
会发现,其中有很多段代码是重复的,太长了且太冗余
当我们使用函数指针数组后,就可以省去switch中的大段重复代码
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int (*p[5])(int x, int y) = {0, add, sub, mul, div}; // 转移表
do
{
printf("*************************\n");
printf("****** 1:add 2:sub ******\n");
printf("****** 3:mul 4:div ******\n");
printf("****** 0:exit ******\n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输⼊错误\n");
}
} while (input);
return 0;
}
上面这段代码中的函数指针数组就是转移表。转移表是一种数据结构,主要用于存储预先计算的结果,以便在需要时进行快速查找。
回调函数就是通过函数指针调用的函数
例如上面的实现计算机功能中有这么一段代码
switch (input)
{
case 1:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
我们可以写一个函数来避免重复的代码,将上面的add、sub、mul、div函数作为参数传入
void calc(int (*pf)(int, int))
{
int ret = 0;
int x, y;
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y); //通过指针调用函数
printf("ret = %d\n", ret);
}
此时pf被用来调用其指向的函数,被调用的函数就是回调函数
优化后的代码:
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;
}
明显更加简洁,避免了代码的重复
觉得有用的话可以点点关注点点赞,你的支持是我创作的动力
文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文
文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作 导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释: cwy_init/init_123..._达梦数据库导入导出
文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js
文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6
文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输
文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...
文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure
文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割
文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答
文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。
文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入
文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf