深入理解指针 (1)
1.内存和地址
1.1内存
1.1.1内存的使用和管理
(1)内存划分为一个个的内存单元,每个内存单元的大小是1个字节,一个内存单元可以存放8个bit。
(2)每个内存单元有一个编号,内存单元的编号在计算机中也叫地址,C语言中称为指针
内存单元的编号 == 地址 == 指针
1.2编址
CPU访问内存中的某个字节空间,必须要知道内存单元的位置(给内存单元编号),地址信息被下达给内存,在内存上就可以找到该地址的对应数据,将数据通过数据总线传入CPU内寄存器
地址总线:传递信息,通过该信息定位内存单元
2.指针变量和地址
2.1变量创建的本质
int a = 20;
//变量创建的本质是在内存中申请空间
//向内存申请4个字节的空间,用来存放20这个数值
//这4个字节每个字节都有编号(地址)
//变量的名字是给程序员看的,编译器是通过地址找内存单元
2.2取地址操作符(&)
int a = 20;//&a, &--取地址操作符,拿到变量a的地址printf("%p\n", &a);int* pa = &a;//pa是一个变量,用来存放地址(指针),pa叫指针变量
理解int* pa = &a;
pa的类型是int*
pa是指针变量的名字
* 表示pa是指针变量
int表示pa指向的变量a的类型是int
2.3解引用操作符(*)
int a = 20;int* pa = &a;*pa = 300;//*--解引用操作符(间接访问操作符)printf("%d\n", a);
2.4指针变量大小
指针变量用来存放地址。
一个32位机器,有32根地址总线,每根地址总线传递一个信号,32根地址总线产生32bit的地址,需要4字节存放。
一个64位机器,有64根地址总线,每根地址总线传递一个信号,64根地址总线产生64bit的地址,需要8字节存放。
指针变量的大小只与机器的位长有关,与本身类型无关。
理解:一个char类型的地址是地址,同时一个int类型的地址同样也是地址,char*, int* 取地址时取的都是地址,所以与类型无关(通俗理解:苹果是水果,香蕉也是水果,当不论是拿苹果还是香蕉时都是拿的水果)
3.指针变量类型的意义
3.1指针的解引用
指针的类型决定了对指针解引用权限有多大。
如:char* 解引用能访问1个字节,int* 解引用能访问4个字节
3.2指针+-整数
指针类型决定了指针向前或向后一步走多远(距离)
如:一个char* 跳过1个字节,一个int* 跳过4个字节
理解:int* pa; pa + 1——> + 1 * sizeof(int)
pa + n——> + n * sizeof(int)
int a = 10;int* pa = &a;char* pb = &a;printf("a = %p\n", &a);printf("pa = %p\n", pa);printf("pb = %p\n", pb);printf("a + 1 = %p\n", &a + 1);printf("pa + 1 = %p\n", pa + 1);printf("pb + 1 = %p\n", pb + 1);
3.3void* 指针
无具体类型的指针(泛型指针),可以用来接收任何类型的地址,但是不能直接进行解引用操作和指针的加减整数操作。即:可接收不同类型地址但不能进行指针运算
void* 一般在函数参数的部分使用,用来接收不同数据类型的地址。使得一个函数能够处理多种类型数据。
4.const修饰指针
4.1const修饰变量
const修饰变量的时候叫:常变量。这个被修改的变量本质上还是变量,只是不能被修改
当使用指针变量可以对其const修饰的变量进行修改(门被关起来了,从窗户跳进去)
const int n = 20;int* p = &n;*p = 200;
4.2const修饰指针变量
4.2.1.const位于*左边
const放在*左边,限制的是指针指向的内容,即,不能通过指针变量来修改它所指向的内容
但是指针变量本身可以被修改
4.2.2.const位于*右边
const放在*右边,限制的是指针变量本身,即,指针不能改变它的指向
但是可以通过指针变量修改它所指向的内容
4.2.3.const位于*左右两侧
此时指针指向的内容和指针变量本身都不能被修改
注:关于指针p有3个相关的值
1.p,p里面存放着一个地址
2.*p,p指向的那个对象
3.&p,表示的是p变量的地址
5.指针运算
5.1指针+-整数
1.指针类型决定了指针+1的步长
2.数组在内存中是连续存放
//顺序打印数组
int a[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(a) / sizeof(a[0]);int* p = &a[0];for (int i = 0; i < sz; i++){printf("%d ", *p);p++;//printf("%d ", *(p + i));}
//逆序打印数组int a[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(a) / sizeof(a[0]);int* p = &a[sz - 1];for (int i = 0; i < sz; i++){//printf("%d ", *p);//p--;printf("%d ", *(p - i));}
5.2指针-指针
指针-指针的绝对值是指针之间元素的个数
指针-指针,计算的前提条件是两个指针指向的是同一个空间
int a[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("%d\n", &a[9] - &a[0]);printf("%d\n", &a[0] - &a[9]);
实例:写一个函数,求字符串长度
1.通过strlen函数求字符串长度
#include <string.h>char ch[] = "abcdef";size_t len = strlen(ch);printf("%zd\n", len);
2.写一个函数,模拟strlen函数实现求字符串长度
方法1:(指针+-整数操作)
size_t my_strlen(char* p)
{int count = 0;while (*p != '\0') //while(*p){count++;p++;}return count;
}int main()
{char ch[] = "abcdef";size_t len = my_strlen(ch); //数组名其实是数组首元素的地址 ch==&ch[0]printf("%zd\n", len);return 0;
}
方法2:(指针-指针操作)
size_t my_strlen(char* p)
{char* start = p;char* end = p;while (*end != '\0') //while(*end){end++;}return end - start;
}int main()
{char ch[] = "abcdef";size_t len = my_strlen(ch); //数组名其实是数组首元素的地址 ch==&ch[0]printf("%zd\n", len);return 0;
}
5.3指针的关系运算
当一个指针指向一个数组时,指针向后移动,指针指向的地址由小变大
int a[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(a) / sizeof(a[0]);int* p = a;//while(p < a + sz)while (p < &a[sz]) //指针的大小比较{printf("%d ", *p);p++;}
6.野指针
指针指向的位置不可知(随机、不正确、无明确限制)
6.1野指针的成因
(1)指针未初始化
一个局部变量的值不初始化,值是随机的
如果将p中存放的值当作地址,解引用操作符会形成非法访问
int* p; //指针未初始化
*p = 10;
(2)指针越界
注:数组下标是从0开始的,不要越界
(3)将指针指向的空间释放
局部变量进函数创建,出函数销毁,当它出函数时,内存空间归还给操作系统
int* test()
{int n = 10;return &n;
}int main()
{int* p = test();printf("%d\n", *p);return 0;
}
6.2如何规避野指针
(1)指针初始化
如果明确知道指向哪里就直接赋值地址,不知道指向哪里赋值NULL,NULL是C语言中的标识符常量,值为0,0也是地址,这个地址无法使用,读写该地址会报错。
int a = 10;
int* p = &a; //直接复制地址
int* s = NULL; //赋值NULL
*s = 100; //error
(2)指针越界
一个程序申请了多少内存空间,指针也只能访问这些空间,超出范围访问就是指针越界。
(3)指针变量不再使用,及时置NULL,使用前检查有效性
只要指针是NULL就不进行访问,指针在使用之前先进行判空再进行解引用操作。
int a = 20;
int* p = &a;
if(*p != NULL) //判空
{*p = 200;
}
(4)避免返回局部变量的地址
不要返回局部变量的地址
7.assert断言
assert.h头文件定义了宏assert(),用于在运行确保程序符合指定条件,如果不符合就会报错终止运行,这个宏叫做断言。
assert()接收一个表达式作为参数,如果表达式为真,不会产生任何作用,程序继续运行,如果表达式为假,assert()会报错,在标准错误流stderr(屏幕上)写入一条错误信息,显示没通过的表达式,以及包含这个表达式的文件名和行号。
优点:(1)自动识别文件和出错的行号
(2)无需更改代码就能开启或关闭assert()机制
(3)如果不需要使用时,在头文件定义宏NDEBUG
#define NDEBUG
#include <assert.h>assert(*p != NULL);
缺点:增加了运行时间
注:在VS的Release版本中会优化掉assert(),即不会起作用
8.指针的调用
8.1传值调用和传址调用
8.1.1传值调用
传值调用就是在函数使用时,把变量本身的值直接传递给函数。实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,此时对形参的修改不会影响对实参的改变。
//传值调用(不能实现变量数值的交换)
void swap1(int x, int y)
{int z = 0;z = x;x = y;y = z;
}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;
}
8.1.2传址调用
能建立函数和主调函数之间的联系,可以在函数内部修改主调函数的变量,从而使主调函数中的变量发生改变。
void swap1(int* pa, int* pb)
{int z = 0;z = *pa;*pa = *pb;*pb = z;
}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;
}
注:如果只是需要主调函数的变量值来实现计算,就可以采用传值调用。如果函数内部要修改主调函数中的变量的值,就需要传址调用。
未完待续……