一.什么是指针?
1.1指针的概念
本质上指针就是地址,口语中所说的指针,其实就是指针变量,指针变量是用来存放地址的一个指针。
我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是内存中读取的,处理后的数据也会放回内存中。
但在如今电脑内存越来越大的时代中,这些内存空间如何高效的管理?
那么就要把内存划分成一个一个的内存单元,每个内存单元的大小取1字节,每个内存单元都有一个编号。
有了内存单元的编号,CPU就可以快速找到一个内存空间。
内存单元的编号 == 地址 == 指针
二. 指针变量和地址
2.1 取地址操作符
在c语言中,创建一个变量就是向内存申请空间
上述代码创建整型变量a,向内存申请了4个字节,用于存放整数10,其中每个字节都是有地址的。
那我们如何取到a的地址呢 ?
这里就可以用到取地址符号&
#include<stdio.h>
int main() {
int a = 10;
printf("%p\n", &a);
return 0;
}
上面的代码运行以后就可以看到
打印出了0000003EC84FFA44。这个便是a地址最小地址的字节地址。
在c语言中,创建的变量比如int类型占据4个字节,那么它会开辟连续四个字节的空间,地址就是a地址最小字节的地址。
2.2 解引用操作符 * 和指针变量
2.2.1解引用操作符
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;//取出a地址,并存放在指针变量中
//int说明pa指向的对象是int类型 *说明pa是指针变量
*pa=100;
printf("%d",*pa);
return 0;
}
那么这里的输出就是100,会改变原来a的值。
1。*的作用是引用指针指向的变量值,引用其实就是引用该变量的地址,“解”就是把该地址对应的东西解开,解出来,就像打开一个包裹一样,那就是该变量的值了,所以称为“解引用”。 也就是说,解引用是返回内存地址中对应的对象。
解引用也可以改变该变量的数值。
2.需要注意的是,在变量声明的时候,*不能当做解引用使用,只是表示你声明的变量是一个指针类型。
2.2.2 指针变量
我们通过取地址操作符(&)拿到的地址是一个数组,比如:0000003EC84FFA44,这个数组有时候需要存储起来,方便后期使用,那我们就可以把地址值存放在指针变量中。
就是把一个指针变量用作与存放a的地址 。
指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。
2.3 指针变量的大小
在不同的环境中其指针的大小也不同:
在64位环境中是8个字节,而在32位环境中是4个字节 ,并且与类型是无关的。
三. 指针类型变量的意义
3.1 指针的解引用
#include <stdio.h>
int main()
{
int n = 0x11223344;
int* p1 = &n;
*p1 = 0;
return 0;
}
#include <stdio.h>
int main()
{
int n = 0x11223344;
char* p2 = (char *) & n;
*p2 = 0;
return 0;
}
这两段代码有什么区别呢?
通过运行就会发现第一个就会把四个字节都改成0了,但第二个只会把指向n的地址的那一个字节改成0.
结论:指针类型决定了对指针解引用的时候有多大权限(一次可以操作几个字节)。
比如:char*的指针解引用只能访问一个字节,而int*的指针解引用就可以访问4个字节。
3.2指针的加减运算
#include <stdio.h>
int main()
{
int n = 10;
char* pc = (char*)&n;
int* pi = &n;
printf("&n = %p\n", &n);
printf("pc = %p\n", pc);
printf("pc+1 = %p\n", pc+1);
printf("pi = %p\n", pi);
printf("pi+1 = %p\n", pi+1);
}
运行结果:
看的出来,指针的类型决定了向前或向后访问距离有多大。
3.3 void指针
void* 类型,无具体类型的指针(泛型指针),void* 类型的指针大部分使用在函数参数的部分,用来接收不同类型数据的地址。
但是void* 类型的指针不能直接进行指针的+,-整数和解引用运算。
四. const修饰指针
1.在C语言中,const是constant的缩写,翻译是“恒定不变的”。它是定义只读变量的关键字。或者说const是定义常变量的关键字
2.const修饰的变量被称为常变量,具有常属性,但是本质上还是变量。
3.保护被修饰的东西,防止被意外修改,增强程序的健壮性。
4.提高程序的运行效率。编译器通常不为普通const常量分配存储空间,而是将他们保护在符号表中,使得它成为一个编译期间的常量,没有存储和读取内存的操作,使得它的运行效率也很高。
#include <stdio.h>
void test1()
{
int n = 10;
int m = 10;
/* const放在*左边,修饰的是*p,表示的是指针指向的内容,不能通过指针来改变,
但是指针变量本身是可以被修改的 */
const int* p = &n;//const放在*的左边
*p = 20;//err *p不可改变(指针指向的内容不能被修改)
p = &m; //ok 指针变量p可以修改
}
void test1()
{
int n = 10;
int m = 10;
/* const放在*右边,修饰的是指针变量p本身,表示的是指针变量不可以被修改,
但是指针指向的内容是可以被修改的 */
int* const p = &n;//const放在*的右边
*p = 20;//ok //指针指向的内容可以被修改
p = &m; //err p不可改变(指针变量p不能被修改)
}
int main()
{
test1();//const放在*的右边
test2();//const放在*的左边
return 0;
}
五. 指针运算
5.1 指针-指针
#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;
}
两个指针相减,得到的就是指针之间的元素个数。指针s是字符串首地址,第一次p=s,随着p++,p指向最后一个字符后面的位置。所以p-s,就得到字符串的个数。
5.2 指针的关系运算
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int* p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
while (p < arr + sz) //指针的大小比较
{
printf("%d ", *p);
p++;
}
return 0;
}
指针的比较,依赖于指针所指向的两个元素的相对位置,若指针p指向arr[i],指针q指向arr[j],p和q的结果由i和j的大小决定。
六. 野指针
6.1 野指针的成因
1.指针未初始化
2.指针越界访问
3.指针指向的空间释放
6.2 如何规避野指针
1.指针初始化
2.小心指针越界
3.指针变量不再使用,及时置NULL,指针使用之前检查有效性
4.避免返回局部变量的地址
七. 指针的使用和传址调用
7.1 strlen的模拟实现
库函数strlen的功能是求字符串长度,统计的是字符串\0之前的字符个数。
库函数原型:
size_t strlen ( const char* str );
模拟实现,从起始地址开始向后逐个字符的遍历,只要不是\0字符,计算器就+1,直到\0就停止。
int my_strlen(const char* s)
{
int count = 0;
while (*s != '\0')
{
count++;
s++;
}
return count++;
}
int main()
{
int len = my_strlen("abcdef");
printf("%d\n", len);
return 0;
}
7.2传值调用和传址调用
例如:写一个函数,交换两个变量的值
#include <stdio.h>
void Swap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换之前a = %d b = %d\n", a, b);
Swap(a, b);
printf("交换之后a = %d b = %d\n", a, b);
}
运行结果:
看的出来这两个值是没有交换的,因为传的是值,并不会改变main函数里面的a和b的值,所以用传值交换失败了。
但是传址呢?
#include <stdio.h>
void Swap(int *x, int *y)
{
int tmp = 0;
tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换之前a = %d b = %d\n", a, b);
Swap(&a, &b);
printf("交换之后a = %d b = %d\n", a, b);
}
运行结果:
看到这里是交换成功了,因为传地址后在函数那做出更改时会改变在main函数里的a和b所指向地址的值,因此传址交换是可以的。
这种也叫做:传址调用。