主页链接: LSR的主页
专栏链接: 《C语言》
前言
以下是对指针的见解,希望可以帮助到你。
一、指针变量和地址
1.取地址操作符(&)
顾名思义,&就是用来取地址的,示例代码如下:
#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
&a;//取出a的地址
printf("%p\n", &a);
return 0;
}
}
说明:a在内存中开辟一块空间,因为是int类型,所以占用4个字节,而这四个字节在内存中一定是连续的,而&a所取出来的地址就是最小的(首地址)地址。
示例:
//假设上述a的地址是:
0x006FFD70
0x006FFD71
0x006FFD72
0x006FFD73
//printf打印结果就为0x006FFD70
提醒:地址用%p打印。
2.指针变量和解引用操作符(*)
2.1 指针变量
指针变量的作用和变量相似,本质就是用来储存地址的变量,示例代码如下:
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
2.2解引用操作符(间接访问操作符)
先看代码:
#include <stdio.h>
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
printf("%d", a);
return 0;
}
pa的意思就是通过pa中存放的地址找到指向的空间,即a,所以pa = 0,就是将a改为了0。
可以把*操作符看作一把万能钥匙,只要你能给我地址,我就能根据地址打开你想要的那个变量的门。
2.3指针变量的大小
指针变量是用来储存地址的,所以指针变量的大小本质上就是地址的大小,这里引入一个概念,32位机器有32根地址总线,那我们把32根地址线产生的2进制序列当做一个地址,那么一个地址就是32个bit位,根据1byte = 8bit可知需要4个字节才能储存。
同理,64位机器,假设有64根地址线,那就需要8个字节。
结论:
32位平台下地址为32gebit位,指针变量大小是4个字节。
64位平台下地址为64gebit位,指针变量大小是8个字节。
值得注意的是,指针变量大小和类型无关,相同平台下大小都相同。
二、指针变量类型的意义
指针变量的大小和类型无关,只要是指针变量,在同⼀个平台下,大小都是⼀样的,为什么还要有各
种各样的指针类型呢?
1.指针±整数
来看一段代码:
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;//强制类型转换
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
//运行结果如下:
&n = 00AFF974
pc = 00AFF974
pc + 1 = 00AFF975
pi = 00AFF974
pi + 1 = 00AFF978
可以看到,char类型的指针+1跳过1个字节,int类型的指针变量+1跳过了4个字节。
结论:指针类型决定了指针向前或向后操作时一步的距离。
2.void* 指针
这种类型的指针被称作泛指型指针(无具体类型),可以用来接收任意类型的地址。
局限性:不能直接进行指针的±整数和解引用操作。
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pb = &a;
void* pc = &a;
*pa = 100;
*pc = 0;
return 0;
}
说明:char接收时会报错而void 不会,但是void*赋值时也仍会报错。
三、const修饰指针
1.const修饰变量
我们知道,无论是变量还是指针变量,都是可以通过赋值修改的,而const的作用就是给变量加上限制,使其不能被修改。示例如下:
#include<stdio.h>
int main()
{
int m = 0;
m = 20;//m的值可以被修改
const int n = 0;
n = 20;//n的值不能被修改,会报错
return 0;
}
2.const修饰指针变量
#include<stdio.h>
void test1()
{
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;
p = &m;
}
void test2()
{
int n = 10;
int m = 20;
int* const p = &n;
*p = 20;
p = &m;
}
void test3()
{
int n = 10;
int m = 20;
const int* const p = &n;
*p = 20;
p = &m;
}
int main()
{
//测试const在*左边的情况
test1();
//测试const在*右边的情况
test2();
//测试const在两边的情况
test3();
return 0;
}
结论:
const修饰p,那么p指向的变量的内容就无法修改,p本身所存地址可以修改;
const修饰p, 那么p所存的地址就无法修改,而*p指向的变量的内容可以修改。
四、指针运算
指针的基本运算有三种:
· 指针±整数
· 指针-指针
· 指针的关系运算
1.指针±整数
指针打印数组每个值示例代码:
incldue<stdio.h>
int main()
{
itn arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int* p = arr[0];
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i = 0;i < sz;i++)
{
printf("%d", *(p + i));//指针+-整数
}
return 0;
}
数组在内存中是连续存放的,所以只要知道首元素的地址,存入指针变量,再通过指针±整数就可以找到所有的元素。
2.指针-指针
//指针-指针
#include <stdio.h>
int my_strlen(char *s)
{
char *p = s;
while(*p != '\0' )
p++;
return p-s;
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
说明:指针-指针,就是两个地址之间的计算,而计算结果就是两个指针之间的字节大小。
3.指针的关系运算
同样以打印数组为例:
incldue<stdio.h>
int main()
{
itn arr[] = {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;
}
五、野指针
概念:野指针就是指指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
1.野指针的成因
1.1指针未初始化
include<stdio.h>
int main()
{
int* p;//未初始化,默认为随机值
*p = 20;
return 0;
}
1.2指针越界访问
#include<stdio.h>
int main()
{
int arr[] = {0};
int* p = &arr[0];
int i = 0;
for(i = 0;i<=11;i++)
{
//指针指向的范围最终会超出数组arr的范围,p就为野指针
*(++p) = i;
}
return 0;
}
1.3指针指向的空间释放
#include<stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%d", *p);//因为调用结束后,函数空间释放,那么*p就无权限访问n的空间。
return 0;
}
2.如何避免野指针的出现
2.1指针初始化
如果明确知道指针指向的地址,就直接以地址进行初始化,如果暂时不知道,可以给指针赋值NULL(值为0),但是NULL不能直接读写,会报错。
#include<stdio.h>
int main()
{
int num = 10;
int* p1 = #
int* p2 = NULL;
return 0;
}
2.2避免指针越界
一个程序向内存申请了哪些空间,那么通过指针也就只能访问哪些空间,不能超出范围,否则造成越界访问。
2.3指针使用前检查其有效性
如果指针已经越界或者不再使用时,我们可以直接置NULL,而我们使用指针前,也要确保指针不为NULL,因为NULL不能被读写访问。
2.3不要返回无效地址
如造成野指针的第三个例子,返回一些局部变量的地址也会造成野指针的产生。
六、assert断言
assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,那么程序就会报错,这个宏常常被称为“断言”。
assert(p != NULL);
代码在运行到这一行语句的时候,就会验证指针变量p是否为NULL,如果不等于,程序就会继续运行,否则就会终止运行,并标出出现问题的行号。
原理:assert()宏接受一个表达式作为参数,表达式为真,程序继续进行;表达式为假,assert()就会报错。
使用assert():
如果确认程序没有问题,不需要再做断言,那么可以在#incldue<stdio.h>前定义一个宏NDEBUG:
#define NDEBUG
#include<assert.h>
如此一来,编译器就会禁用文件中所有的assert()语句。而当程序又出现问题,可以移除(注释)#define NDEBUG指令,就可以重新启用assert()语句。
assert的缺点也很明显:
因为引入了额外的检查,所以会增加程序的运行时间。
所以一般我们可以在Debug中使用,而在Release版本中选择禁用assert,这样在VS这样的集成开发环境中,在Release版本下可以直接优化。
这样在Debug版本有利于我们排查问题,而在Release版本下不影响用户使用时程序的效率。
七、指针的使用和传址调用
1.strlen的模拟实现
库函数strlen()的功能是求字符串的长度,统计的是字符串中‘\0’之前的字符的个数。
函数原型:
size_t strlen(const char* str);
模拟实现strlen:
int my_strlen(const char * str)
{
int count = 0;
assert(str != NULL);//判断传入的指针是否为空,为空就中断程序
while(*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
int len = my_strlen("abcdef");
printf("%d\n", len);
return 0;
}
2.传值调用和传址调用
我们学习指针的目的就是为了解决问题,那么什么样的问题非指针不可呢?
例:写一个函数,交换两个整型变量的值:
我们可能会这样写:
#include <stdio.h>
void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
但是遗憾的是,输出结果为:
10 20
交换前: a = 10 b = 20
交换后: a = 10 b = 20
可以发现,这样并未产生交换的效果。调试之后发现,a和b的地址并不会随着实参的传递传递给形参,而形参x和y会重新创建一份临时空间来储存a和b的值,而地址和a和b是截然不同的,所以本质上只有x和y发生了交换,而我们的a和b并未发生交换,这个就叫传值调用。
结论:实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参,所以交换失败。
解决办法:
我们可以通过指针的形式将a和b的地址传入Swap函数内部,Swap函数里面通过地址间接的操作main函数中的a和b,并达到交换的目的。
#include <stdio.h>
void Swap1(int* px, int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
输出结果为:
10 20
交换前: a = 10 b = 20
交换后: a = 20 b = 10
这里我们是将变量的地址传递给了函数,这种函数调用方式就叫做:传址调用。
以上就是初入指针的相关内容,希望对你有所帮助。
1043

被折叠的 条评论
为什么被折叠?



