目录
前言:
学好指针是学好c语言的一个好的起点,也是一个重要的基础,想要学好c语言,指针必不可少。
一.指针的初识
通俗的讲:内存单元的编号==地址==指针
我们口语上常说的指针,其实指的就是指针变量。
规定:一个内存单元就是一个字节,每一个内存单元都对应着一个地址。
1.解引用操作符和取地址操作符
取地址操作符(&)
例:&a取出的是a所占4个字节中地址较小的字节的地址。
解引用操作符(*)
取地址操作符(&)拿到的地址是⼀个数值,这个数值有时候也是需要存储起来,方便后期再使用的,而我们将这个数值存放在指针变量中。 指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
}
2.指针的类型
像int double float char 类型一样,指针也有自己相应的类型,
而指针的类型就是指针所指向元素是什么类型,
指向int的指针就是整型指针,指向char类型的就是字符型指针等等。
指针的大小都是确定的
在32位的编译器下指针变量的大小都是4个字节,在64位编译器下指针的大小都是8个字节
意义:
1)决定了在解引用时访问几个字节如果是 int* 类型的就访问4个字节
2) 决定了对指针解引用时有多大的权限(⼀次能操作几个字节)
3.特殊类型
void* 指针为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,void* 类型的指针不能直接进行指针的+-整数和解引用的运算。
作用:
⼀般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以 实现泛型编程的效果。
4.const修饰指针
#include <stdio.h>
//代码1
void test1()
{
int n = 10;
int m = 20;
int* p = &n;
*p = 20;//ok?
p = &m; //ok?
}
//代码2
void test2()
{
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;//ok?
p = &m; //ok?
}
//代码3
void test3()
{
int n = 10;
int m = 20;
int* const p = &n;
*p = 20; //ok?
p = &m;
}
结论:
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。
const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
5指针运算
指针的基本运算有三种
指针+-整数
指针-指针
指针的关系运算
1)指针+-整数
因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,就能找到后⾯的所有元素
int main()
{
int arr[10] = { 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));//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)指针的关系运算
本质是比较指针的大小
指针和指针比较大小==地址和地址比较大小
6.野指针
成因:
1. 指针未初始化、
如 int* p;
*p = 10;
没有初始化,意味着没有明确地指向
一个局部变量不初始化,存放的是随机值,如0x000ff00a
此时这个地址是一个非法的地址。
然后 *p = 10;非法访问内存,此时p就是野指针。
2. 指针越界访问
当指针指向的范围超出数组的范围时,该指针就是野指针。
3. 指针指向的空间释放
#include <stdio.h>
int* test()
{
int a = 0;
return &a;
}
int main()
{
int* p = test();
return 0;
}
因为a是局部变量,离开自己的作用域就被销毁,*p记住了a的地址,但不可以使用,a的空间已经被释放了,所以p成为野指针。
预防方法:
1.指针初始化:
在我们使用指针时应明确知道自己应该给他赋上一个什么值
若不知道赋什么值 则 int* p = NULL;
2.避免返回局部变量的地址。
3.小心指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问
7.assert 函数
assert()函数被包含在<assert.h>头文件中
用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。
常被称为“断言”。
assert(p != NULL);
上面代码在程序运行到这⼀行语句时,验证变量p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运行,否则就会终止运行,并且给出报错信息提示。
好处:
它不仅能自动标识文件和出问题的行号,还有⼀种无需更改代码就能开启或关闭 assert() 的机制。
就在 #include 语句的前⾯,定义⼀个宏
#include NDEBUG
#include < assert.h >
缺点:因为引入了额外的检查,增加了程序的运行时间。
8. 指针的使用和传址调用
传值调用和传址调用
例如:写⼀个函数,交换两个整型变量的值
#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;
}
但是我们发现运行结果并没有发生变化
解释:相当于x和y是独立的空间,那么在swap1函数内部交换x和y的值, 自然不会影响a和b。
结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。
#include <stdio.h>
void swap2(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);
swap2(&a, &b);
printf("交换后:a = % d b = % d\n", a, b);
return 0;
}
运行结果是正确的。
这里调用Swap2函数的时候是将变量的地址传递给了函数,这种函数调用方式叫:传址调用。
传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量。