一: 指针
- 内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。
- 所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。
- 为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地 址。
一一个小格为一个单位,一个字节就是8个小格
因为一个二进制位可以存放0和1,有两种组合方式,一个字节有8位,所以一个字节可以有28= 256 中组合方式
在每一个字节的起始位置处,都有一个地址,我们通过这个地址就可以找到这个空间在内存中的位置
注意,每个地址都是唯一的
1.1: 指针和地址的大小
目前计算机主要分为32位计算机和64位计算机两种,
- 在32位计算机中,指针的大小为4字节,
- 在64位计算机中,指针的大小为8字节
指针是用于存放地址的变量
在计算机上,有地址线电线产生的高低电平信号,这些信号会转换成数字信号0 和 1,32位机器顾名思义就是有32根地址线,而64位机器就是有64根地址线
对于一根地址线,它有两种组合,即0和1
对于两根地址线,它有4种组合,即00 01 10 11
对于三根地址线,它有8种组合,即 000 001 010 011 100 101 110 111
……
以此类推
那么32根地址线,就有232种组合,它能够管理232个地址
64根地址线,就有264种组合,它能够管理264个地址
所以我们要想储存一个变量的地址,
- 在32位机器种只需要32个二进制位
- 在64位机器种只需要64个二进制位即可
而一个字节又等于8个比特位,所以
- 在32位机器中,指针的大小为4字节
- 在64位机器中,指针的大小为8字节
注意,指针和地址是两个不同的概念,指针是用于存放地址的变量,而地址是变量中的很重要的一个属性
1.2: 指针的使用
那么我们该如何通过指针来存放一个变量的地址呢?
#include <stdio.h> int main() { int num = 10; int* p = # *p = 20; printf("%d", num); return 0; }
运行结果如图所示
接下来我将通过画图的形式讲解
10的二进制是1010,如果补齐64位的话,就是
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00001010
这就是10在内存中存储的数据
如图所示,我们定义了一个数值为10的空间,接着我们通过取地址符&,将num的地址取出来,注意,取地址取出来的是开头处的地址,然后把地址赋给了专门存放地址的变量->指针int *p,此时p就存放了num开头的地址了
那么此时我们就有了num的地址,并将地址存放在指针变量p中,我们该如何通过p来找到num呢?
这时又另外一种操作符
解引用操作符 *
注意,这个解引用操作符和p上的*意义完全不同,不要混淆
解引用操作符就是取地址操作符的逆运算
&num 就是通过num找到num的地址
而*p 则是通过num的地址,找到num(此时p存放的是num的地址)
我们说过,一个变量它有值属性和址属性两种属性
那么,这两个属性有什么关联吗?
答案是有的,我们可以通过值属性找到址属性,也可以通过址属性找到值属性
那么我们再来解释一下上面的代码
首先我们定义了一个int 类型的num变量,并赋值为10,接着将num的地址取出来,并将地址赋给了指针p,接着我们通过解引用操作符 * 来找到num的值,并将20赋给它,所以num的值也就被改成了20
1.3 指针和指针类型
我们都知道,对于一个变量,变量可以有不同的类型,比如说整型浮点型等等,那么指针有没有类型呢?答案是有的:
当有这样的代码:
int num = 10; p = #
要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢?
我们给指针变量相应的类型。
char *pc = NULL; int *pi = NULL; short *ps = NULL; long *pl = NULL; float *pf = NULL; double *pd = NULL;
这里可以看到,指针的定义方式是: type + * 。
其实:
- char* 类型的指针是为了存放 char 类型变量的地址。
- short* 类型的指针是为了存放 short 类型变量的地址。
- int*类型的指针是为了存放 int 类型变量的地址。
那指针类型的意义是什么?
下面通过代码例子讲解:
#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; }
运行结果如下:
总结:
- 指针的类型决定了指针向前或者向后走一步有多大(距离),即 指针+1的意义是跳过当前指针所指空间的类型大小
- 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
int main() { int arr[10] = { 0 }; int* p = arr; return 0; }
所以我们应该很容易知道,
- *( p+i ) == arr[ i ]
对于数组,数组的名字表示首元素的地址,所以*( p+i )也可以写成*(arr + i),即:
- *( p+i ) == *(arr + i)
所以:
- arr [ i ] == *(arr + i)
1.4野指针
在c语言中对于野指针的定义是:
- 一个指针指向已被释放的内存地址,或一个指针未被赋值一个地址
而null则是一个特殊的指针,
- null表示指针变量不指向任何有效的内存地址
下面通过代码举例:
#include <stdio.h> int main() { int *p;//局部变量指针未初始化,默认为随机值 *p = 20; return 0; }
在这个例子中,我们并未将任何地址赋值给p,所以p并未指向任何有效的空间,所以这个指针是一个野指针
#include <stdio.h> int* test() { int a = 10; return &a; } int main() { int* p = test(); printf("%d\n", *p); return 0; }
这段代码看似没问题,实则问题很大,因为在test函数中,a是一个局部变量,当出了函数这个作用域之后,a这个空间就会被释放了,而test函数又返回a的地址,此时p也是一个野指针
那么我们该如何规避野指针呢?
- 指针初始化
- 小心指针越界
- 指针指向空间释放,及时置NULL
- 避免返回局部变量的地址
- 指针使用之前检查有效性
1.5两个指针的相减
在c语言中,两个指针相减,表示的是在两个指针内,所含有的指针类型所指空间类型的个数 (绝对值就是个数了,因为可能是负数)
1.6字符指针
下面看下代码:
int main() { const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗? printf("%s\n", pstr); return 0; }
代码 const char* pstr = “hello bit.”; 特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr 里了,但是/本质是把字符串 hellobit. 首字符的地址放到了pstr中
那就有可这样的面试题:
#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; }
这里最终输出的是:
str1 and str2 are not same
str3 and str4 are same
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。
因为对于被const修饰的常量字符串,它是不能被修改的,所以编译器会认为这种字符串存一份就够了,当你需要使用的时候只需要用指针指向它即可
1.7数组指针和指针数组
在c语言中,数组指针和指针数组是两个不同的概念
- 数组指针是指针
- 指针数组是数组
数组指针是指针很好理解,我们可以通过类比的方式,整型指针是指针,字符指针也是指针,整型指针指向的空间是整型,字符指针指向的空间是字符,类似的,数组指针指向的是一个数组
指针数组是数组也很好理解,整型数组是存放整型的数组,字符数组是存放字符的数组,那么指针数组就是存放指针的数组
1.7.1数组指针和指针数组的定义
那么数组指针和指针数组该怎么定义呢?
指针数组:
- int *p1[10];
数组指针:
- int (*p2)[10];
因为【】下标引用操作符比 * 间接访问操作符的优先级高
所以在int *p1[10]中,p1会先和【】下标引用操作符结合,这说明p1是一个数组把数组名和【10】删掉,就是数组内存储元素的类型,所以这个数组存储的类型是int *的也就是存储指针的数组,简称指针数组
对于int (*p2)[10];p2先和 * 结合,这说明p2是一个指针,将( *p2)删掉就是这个指针所指向的空间类型,很明显,这个p2指向了一个数组,是一个指向数组的指针,叫做数组指针
1.8数组名的意义
在c语言中,数组名在一般情况下都代表着数组首元素的地址,但是也存在着两个例外:
- sizeof(数组名)
此时数组名表示整个数组,计算的是整个数组的大小,单位是字节
- &数组名
这里的数组名表示整个数组,取的是整个数组的地址,虽然在值上和数组首元素的地址相同,但是意义不一样(指针类型决定指针+1走的距离)所以说,数组首元素的地址+1跳过的大小是一个元素,数组的地址+1跳过的是整个数组的大小
1.8.1二维数组数组名的含义:
二维数组的数组名代表的是第一个一维数组的数组地址,下面通过一个代码来说明:
int main() { int arr[2][3] = { {1,2,3},{4,5,6} }; printf("%p\n", arr); printf("%p\n", arr[0]); printf("%p\n", arr[1]); printf("\n"); printf("%p\n", arr+1); printf("%p\n", arr[0]+1); printf("%p\n", arr[1]+1); return 0; }
这段代码的输出结果是:
这是因为 arr 是一个二维数组,它的数组名代表着该数组的首地址,也就是第一个一维数组 arr[0] 的地址。
当我们对指针进行一些地址运算时,指针的值会改变。arr+1 表示指针向后移动一个一维数组的大小。在这里,一个一维数组的大小是 3 个整型元素,所以 arr+1 结果的地址为原始地址0019FD8C再增加 3 个 int 的大小,即0019FD98
类似地,arr[0]+1 表示 arr[0] 的地址向后移动一个整型的大小,即0019FD8C+ sizeof(int) =0019FD90
1.9数组传参
一维数组传参:
#include <stdio.h> void test(int arr[])//ok {} void test(int arr[10])//ok {} void test(int* arr)//ok {} void test2(int* arr[20])//ok {} void test2(int** arr)//ok {} int main() { int arr[10] = { 0 }; int* arr2[20] = { 0 }; test(arr); test2(arr2); return 0; }
arr代表的是数组首元素的地址,所以在传参的时候,我们可以通过int* arr来接收参数,但是为什么int arr[]也行呢?
在 C 语言中,数组作为函数参数时,它在本质上会被转换为指针传递给函数。这意味着形如 int arr[] 的数组参数和 int* arr 的指针参数实际上是等价的(加一级地址)。
当你在函数声明或定义中使用 int arr[] 时,它只是一种简化的语法形式,用于表示数组类型的指针。编译器会将其转换为 int* arr。
所以,在函数声明或定义中,void test(int arr[]) 和 void test(int* arr) 执行的操作是等价的。这两种写法都用于接收一个指向 int 类型的指针,该指针可以表示一个数组。
那么既然int arr[]可以,int arr[10]自然也可以啦
那么接下来再看一个例子:
void test(int arr[3][5])//ok {} void test(int arr[][])//no {} void test(int arr[][5])//ok {} //总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。 //因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。 //这样才方便运算。 void test(int* arr)//no {} void test(int* arr[5])//no {} void test(int(*arr)[5])//ok {} void test(int** arr)//on {} int main() { int arr[3][5] = { 0 }; test(arr); }
1.10函数指针
顾名思义,函数指针就说指向函数的一个指针,和数组指针的定义非常类似:
数组指针的定义:
- int (*p)【】
而函数指针的定义是:
- int (*p)()
p先和*结合,说明p是一个指针,()说明指针指向的是一个函数,并且这个函数没有参数,而int则是这个函数的返回值类型
同理,有了函数指针,我们同样可以有函数指针数组,这样可以无线套娃,函数指针数组的定义方式如下:
- int (*parr1[10])();
parr1先和【】结合,说明parr1是一个数组,将数组名和【】删除,剩下的就是元素的类型,很明显这是一个函数指针,所以int (*parr1[10])();是一个函数指针数组
注意:在c语言中,void * 的指针不能直接解引用,也不能直接进行指针运算,因为不知道+1会跳多远,void * 指针表示无具体类型的指针,可以接收任意类型的地址,类似与java中的泛型,我们在使用void *类型时只能进行强转
#include <stdio.h> int add(int x, int y) { return x + y; } int main() { int (*pf)(int, int) = &add;//等价于 int (*pf)(int, int) = add; int m = (*pf)(4, 5); //等价于 int m = pf(4, 5); return 0; }
对于一个函数来说,函数名代表的是函数的地址,&函数明也是函数的地址,所以:
- 函数名 == &函数名
并且对于函数指针来说可以不用取地址,直接引用即可
所以
- int m = (*pf)(4, 5); 等价于 int m = pf(4, 5);
1.11sizeof和strlen关于数组和指针的习题
在做题前记住三个规则:
数组名代表的是首元素的地址,但是有两个例外:
- sizeof(数组名)
此时数组名表示整个数组,计算的是整个数组的大小,单位是字节
- &数组名
这里的数组名表示整个数组,取的是整个数组的地址
- arr [ i ] == *(arr + i)
int main() { //一维数组 int a[] = { 1,2,3,4 }; printf("%d\n", sizeof(a));//16 printf("%d\n", sizeof(a + 0));//4或者8(首元素的地址) printf("%d\n", sizeof(*a));//4(对地址解引用,即int的大小) printf("%d\n", sizeof(a + 1));//4或8(第二个元素的地址) printf("%d\n", sizeof(a[1]));//4 (第二个元素的大小) printf("%d\n", sizeof(&a));//4或8 (数组的地址,数组的地址也是地址,是地址就是4或8) printf("%d\n", sizeof(*&a));//16 *和&互相抵消了,相当于sizeof(a) printf("%d\n", sizeof(&a + 1));//4或8 &a表示数组的地址,+1跳过整个数组,但此刻还是地址 printf("%d\n", sizeof(&a[0]));//4或8 首元素地址 printf("%d\n", sizeof(&a[0] + 1));//4或8 第二个元素地址 //字符数组 char arr[] = { 'a','b','c','d','e','f' };//字符数组没有 /0 printf("%d\n", sizeof(arr));//6 6个字符 printf("%d\n", sizeof(arr + 0));//4或8 首元素的地址 printf("%d\n", sizeof(*arr));//1 首元素大小 printf("%d\n", sizeof(arr[1]));//1 第二个元素的大小 printf("%d\n", sizeof(&arr));//4或8 数组的地址 printf("%d\n", sizeof(&arr + 1));//4或8 数组的地址+1跳过整个数组,还是地址 printf("%d\n", sizeof(&arr[0] + 1));//4或8 第二个元素的地址 printf("%d\n", strlen(arr));//随机值,strlen的计算以 /0的出现结束 printf("%d\n", strlen(arr + 0));//随机值 printf("%d\n", strlen(*arr));//错误 strlen(地址)中需要的是地址 printf("%d\n", strlen(arr[1]));//错误 a变成了b而已,strlen('a')的意思是将地址为97的数据进行传参 printf("%d\n", strlen(&arr));//随机值 printf("%d\n", strlen(&arr + 1));//随机值 printf("%d\n", strlen(&arr[0] + 1));//随机值 char arr[] = "abcdef";//a b c d e f /0 /0算一个字符 printf("%d\n", sizeof(arr));//7 printf("%d\n", sizeof(arr + 0));//4或8 首元素地址 printf("%d\n", sizeof(*arr));//1 首元素大小 printf("%d\n", sizeof(arr[1]));//1 第二个元素大小 printf("%d\n", sizeof(&arr));//4或8 数组的地址 printf("%d\n", sizeof(&arr + 1));//数组的地址+1跳过整个数组,还是地址 printf("%d\n", sizeof(&arr[0] + 1));//4或8 第二个元素的地址 printf("%d\n", strlen(arr));//6 printf("%d\n", strlen(arr + 0));//6 printf("%d\n", strlen(*arr));//错误 printf("%d\n", strlen(arr[1]));//错误 printf("%d\n", strlen(&arr));//6 printf("%d\n", strlen(&arr + 1));//随机值 printf("%d\n", strlen(&arr[0] + 1));//5 char* p = "abcdef";//把a的地址放在了p中 printf("%d\n", sizeof(p));//4或8 地址 printf("%d\n", sizeof(p + 1));//4或8 地址 printf("%d\n", sizeof(*p));//1 a的大小 printf("%d\n", sizeof(p[0]));//1 p [ 0 ] == *(p + 0) printf("%d\n", sizeof(&p));//4或8 二级指针也是指针 printf("%d\n", sizeof(&p + 1));//4或8 printf("%d\n", sizeof(&p[0] + 1));//4或8 第二个元素的地址 printf("%d\n", strlen(p));//6 printf("%d\n", strlen(p + 1));//5 printf("%d\n", strlen(*p));//错误 printf("%d\n", strlen(p[0]));//错误 printf("%d\n", strlen(&p));//随机值 printf("%d\n", strlen(&p + 1));//随机值 printf("%d\n", strlen(&p[0] + 1));//5 //二维数组 int a[3][4] = { 0 }; printf("%d\n", sizeof(a));//48 printf("%d\n", sizeof(a[0][0]));//4 printf("%d\n", sizeof(a[0]));//16 a[0]是第一个一维数组的数组名 sizeof(数组名) printf("%d\n", sizeof(a[0] + 1));//4或8 第一行第二个元素的地址 printf("%d\n", sizeof(*(a[0] + 1)));//4 printf("%d\n", sizeof(a + 1));//4或8 第二行的地址 printf("%d\n", sizeof(*(a + 1)));//16 printf("%d\n", sizeof(&a[0] + 1));//4或8 第二行的地址 printf("%d\n", sizeof(*(&a[0] + 1)));//16 printf("%d\n", sizeof(*a));//16 a是第一行的地址,*a则是第一行 printf("%d\n", sizeof(a[3]));//16 return 0; }