指针学习记录
1. 内存
每个内存单元大小取1字节
并且有编号,👈这个就叫做地址,也叫指针
即:内存单元编号==地址==指针
内存中字节很多,需要给内存编址,这并不是把每个字节的地址记录下来,而是通过硬件设计完成的
硬件编址也是如此
计算机内有很多硬件单元,而硬件单元要相互协同工作,👈这至少需要能够进行数据传递
如何通信呢?就是用“线”连起来,这里主要讲一个地址总线
可以简单理解为:32位的机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么就能表示2^32种含义,每种含义都代表一种地址
地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据通过数据总线传入CPU内的寄存器
2. 指针变量和地址
取地址操作符(&)
在c语言中创建变量其实就是向内存申请空间:
比如,上述代码就是创建了整形变量a,内存中申请4个字节,用于存放整数10,其中每个字节都有地址,他们的地址分别是
0x006FF85C
0X006FF85D
0X006FF85E
0X006FF85F
那么怎么获取a的地址呢?
那就用到了取址符&,&a取出的是a所占4个字节中地址较小的字节的地址(每次运行结果不同👇)
解引用操作符(*)
有时取出了地址之后,想要存起来后面用怎么办呢?就是放在指针变量中
int main()
{
int a = 10;
int* pa = &a;//取出a的地址并存放在指针变量pa中
return 0;
}
指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址
如何理解指针变量的类型:
pa左边写的是 int* , * 是在说明pa是指针变量,而前面的 int 是在说明pa指向的是整型(int) 类型的对象
既然指针的值已经通过指针变量存储起来了,那要如何使用呢?
就要用*啦
int main(){
int a = 10;
int* pa = &a;
*pa = 0;
return 0;
}
上面代码中第4行就是用了解引用操作符,*pa
的意思就是通过pa中存放的地址,找到指向的空间,*pa
其实就是a变量了,所以*pa=0
就是把a改成了0
虽然目前看起来好像有些多此一举(明明直接写a=0
不就行了)但其实不然,后面就能理解了
指针变量的大小
前面我们知道,32位机器产生的2进制序列当成一个地址,那么一个地址就是32个bit位,需要4字节才能存储
如果指针变量是用来存放地址的,那么指针变量的大小就得是4个字节才可以
同理64位机器就需要8个字节
看如下代码
#include <stdio.h>
int main(){
printf("%zd\n",sizeof(char*));
printf("%zd\n",sizeof(short*));
printf("%zd\n",sizeof(int*));
printf("%zd\n",sizeof(double*));
return 0;
}
输出都是相同的
- 32位平台下地址32个bit位,指针变量大小4字节,64位则是8字节
- 指针变量大小与类型无关,只要是指针变量,在相同平台下,大小都是相同的
3. 指针变量类型的意义
指针的解引用
对比下面2段代码,主要在调试时观察内存变化
//代码1
#include <stdio.h>
int main(){
int n = 0x11223344;
int* pi = &n;
*pi = 0;
return 0;
}
//代码2
#include <stdio.h>
int main(){
int n = 0x11223344;
char* pc = (char*)&n;
*pc = 0;
return 0;
}
经过调试可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第一个字节改为0
所以指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字节)
比如:char*
的指针解引用就只能访问一个字节,而int*
的指针解引用就能访问4个字节
指针±整数
先看一段代码,调试观察地址的变化
#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;
}
代码运行结果如下:
我们可以看出,char*
类型的指针变量+1跳过一个字节,int*
类型的指针变量+1跳过了4个字节,这就是指针变量的类型差异带来的变化,决定了指针向前或向后走一步有多大距离
泛型指针:void*类型
在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进行指针的±整数和解引用的运算
用处:使用在函数参数的部分,用来接收不同类型数据的地址
4. const修饰指针
int /*const*/ * /*const*/ p=&n;
const如果放在*左边,修饰的是指针指向的内容,保证数据不能通过指针来改变,但是指针变量本身内容可变
如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变
5. 指针运算
基本分三种:
指针±整数,指针-指针(指针中间的元素个数,注意不是字节数!),指针的关系运算(大小比较啥的)
6. 野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
成因:
-
指针未初始化(局部变量未初始化,默认为随机值)
-
指针越界访问:超出范围时就认为是野指针
-
指针指向的空间被释放
如何规避野指针:
-
如果明确知道指针指向哪里就直接赋值地址,如果不知道就赋NULL。NULL 是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错
-
小心指针越界
-
指针变量不再使用时,及时置NULL,指针使用之前要检查有效性
-
避免返回局部变量的地址(出生命周期后该地址就收回了)
当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不用的时候,就可以把指针变量置为NULL,因为约定俗成的一个规则就是:只要是NULL指针就不去访问,同时使用指针之前需要判断指针是否为NULL
7. assert断言
assert.h
头文件定义了宏assert()
,用于在运行时确保程序符合条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”
assert(p != NULL);
上面代码在运行到这一句时,会验证p
是否等于NULL
。如果确实不等于NULL
程序继续运行,否则就会终止运行,并给出报错信息提示
assert()
接受一个表达式作为参数。如果该表达式为真(返回值非0),assert()
不会产生任何作用,程序继续运行。如果表达式为假(返回值为0),assert()
就会报错,在标准错误流stderr
中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号
assert()
的使用有几个好处:能自动识别文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()
的机制。如果已经确认程序没有问题,不需要再做断言,就在#include<assert.h>
语句前面,定义一个NDEBUG
。
#define NDEBUG
#include <assert.h>
然后重新编译程序,编译器就会禁用文件中所有的assert()
语句。如果程序又出现问题,可以移除这条#define NDEBUG
语句,再次编译就重新启用了assert()
语句了
assert()
的缺点:引入了额外的检查,增加了程序的运行时间
一般在Debug
版本中使用,在Release
版本中禁用
8. 二级指针
指针变量也有地址,那么如果要存储指针变量的地址,应该放在哪呢?
这就是二级指针👇
对于二级指针的运算有:
-
*ppa
通过对ppa
中的地址解引用,这样找到的是pa
,*ppa
其实访问的就是pa
int b = 20; *ppa = &b;//等价于 pa = &b;
-
**ppa
先通过*ppa
找到pa
,然后对pa
进行解引用操作,*pa
找到的是a
**ppa = 30; //等价于*pa = 30; //等价于a = 30;
9. 指针数组
指针作定语,这里指针数组本质上是数组,存放的是指针
int* arr[5]
指针数组模拟二维数组,一般常用的地方也就是二维数组了
int arr1[] = {1,2,3,4,5};
int arr2[] = {2,3,4,5,6};
int arr3[] = {3,4,5,6,7};
//数组名是首元素地址,类型是int*,就可以存放在parr数组中
int* parr[3] = {arr1, arr2, arr3};
for(int i = 0; i < 3; i++){
for(int j = 0; j < 5; j++){
printf("%d", parr[i][j]);
}
printf("\n");
}
parr[i]
是访问parr
数组的元素parr[i]
找到的数组元素指向了整形一维数组,parr[i][j]
就是整形一维数组中的元素
上述代码模拟出二维数组的效果,但实际上并非完全是二维数组,因为每一行并非是连续的
10. 字符指针
一般使用如下:
int main(){
char ch = 'w';
char *pc = &ch;
*pc = 's';
return 0;
}
还有一种方式使用如下:
int main(){
const char* pstr = "hello world";
printf("%s", pstr);
return 0;
}
这里并不是把“hello world”
放进pstr
里面了,本质上是把这个常量字符串的首字符(即h)的地址放进了pstr
中。
#include <stdio.h>
int main(){
char str1 = "hello";
char str2 = "hello";
const char *str3 = "hello";
const char *str4 = "hello";
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");
else
printf("str3 and str4 are not same");
return 0;
}
这里str3
和str4
指向的是同一个常量字符串。c/c++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块,所以str1
和str2
不同,str3
和str4
相同
11. 数组指针
这里本质上就是指针了,存放数组的地址,指向数组的指针
int *p1[10];//指针数组
int (*p2)[10];//数组指针
这里给两者做了一个区分
p先和*结合,说明p是一个指针变量,然后指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针
这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
在使用之前,我们需要重新理解一下数组名
数组名在大部分情况中表示的都是首元素地址,但是有两个例外:
sizeof(数组名)
,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
&数组名
,这里的数组名也表示整个数组,取出的整个数组的地址(注意区别首元素地址和整个数组地址的区别:只是长得一样,但是碰到别的情况,比如加减整数时就有差别了)除了这两个,任何地方使用数组名,表示的都是首元素地址
int arr[10] = {0};
int (*p)[10] = &arr;
通过调试可以看到&arr和p的类型是完全一致的
12. 函数指针
函数是有地址的,函数名就是函数的地址,不过&函数名也是函数地址
函数指针怎么创建呢
int Add(int x, int y)
{
return x + y;
}
int main(){
int (*p)(int x, int y)=Add;
return 0;
}
这里x,y是可以省略的,然后Add写成&Add也是一样的
其中,int指的是p指向的函数的返回类型,(*p)这里p是变量名,跟*表示这个是一个指针变量,然后(int x, int y)表示p指向的函数的参数类型以及个数等等
使用👇
int Add(int x, int y)
{
return x + y;
}
int main(){
int (*p)(int x, int y)=Add;
int c = p(2,3);
int d = (*p)(3,5);
return 0;
}
这两种写法都是可以的
看两段有趣的代码
(*(void(*)())0)();
这个呢是先把0强制类型转换成void(*)()类型,然后解引用(这步可省,可以不写*),再进行了一次调用
void (*signal(int, void(*)(int)))(int);
signal是一个名字,后面跟着括号里面(int, void(*)(int))表明signal是个函数,参数是一个int和一个函数指针,返回类型就是剩下的外面这一套,即void(*)(int),所以本质上这个代码是一次函数声明。
如果觉得一个数组指针,或者一个函数指针,写起来太长了,在很多时候搞得很麻烦,可以试试重命名typedef
typedef关键字
typedef unsigned int uint;
//将unsigned int重命名为unit
如果想把int(*)[5]
类型改为parr_t
typedef int(*parr_t)[5]
跟第一个稍有不同,不过如果要重命名 函数指针类型的话,跟这个是差不多的
int Add(int x, int y)
{
return x + y;
}
typedef int(*p_fun)(int, int);
int main() {
p_fun p = Add;
int c = p(2, 3);
return 0;
}
13. 函数指针数组
int (*parr[3])(int, int);
parr
先跟[]结合表明是个数组,然后再跟外面结合,表明存的变量是函数指针
如果觉得这样不好理解,可以试试上面的typedef
typedef int(*p_fun)(int, int);
int main(){
//int (*parr[3])(int, int);
p_fun parr[3];
}
写成这样是不是就比较好理解了
同样还可以嵌套,比如指向函数指针数组的指针
int (*(*p)[3])(int, int);
然后这种指针是不是也可以搞出一个数组呢?
总之可以是可以的,但是搞的人脑子都晕了,所以理论上存在,但用的就比较少了
函数指针数组一般用于转移表
比如实现一个简单的计算器功能
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int Add(int x, int y) { return x + y; }
int Sub(int x, int y) { return x - y; }
int Mul(int x, int y) { return x * y; }
int Div(int x, int y) { return x / y; }
int main() {
int x = 0, y = 0, input = 1, ret = 0;
int (*p[5])(int, int) = { 0,Add,Sub,Mul,Div };
do {
printf("************************\n");
printf("*** 1.Add 2.Sub ***\n");
printf("*** 3.Mul 4.Div ***\n");
printf("*** 0.exit ***\n");
printf("************************\n");
printf("请选择:");
scanf("%d", &input);
if (input >= 1 && input <= 4) {
printf("输入两个整数:");
scanf("%d%d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0) {
printf("退出\n");
}
else {
printf("输入有误\n");
}
} while (input);
return 0;
}
14. 回调函数
通过函数指针调用的函数
如果把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应
对于之前的简易计算器,也可以有如下这种写法
int Add(int x, int y) { return x + y; }
int Sub(int x, int y) { return x - y; }
int Mul(int x, int y) { return x * y; }
int Div(int x, int y) { return x / y; }
void calc(int(*pf)(int, int)){
int ret = 0;
int x = 0, y = 0;
scanf("%d%d",&x,&y);
ret = pf(x, y);
printf("%d", ret);
}
int main(){
calc(Add);
calc(Sub);
calc(Mul);
calc(Div);
return 0;
}
使用回调函数,模拟实现qsort
(采用冒泡方式)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int int_cmp(const void* p1, const void* p2) {
return *(int*)p1 - *(int*)p2;
}
void swap(void* p1, void* p2, int size) {
for (int i = 0; i < size; i++) {
char tmp = *((char*)p1 + i);
*((char*)p1 + i) = *((char*)p2 + i);
*((char*)p2 + i) = tmp;
}
}
void my_qsort(void* base, int count, int size, int(*cmp)(void*, void*)) {
for (int i = 0; i < count - 1; i++) {
for (int j = 0; j < count - i - 1; j++) {
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0) {
swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
int main() {
int arr[] = { 1,3,5,7,9,2,4,6,8,0, };
my_qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
printf("%d ", arr[i]);
}
return 0;
}
对比sizeof
和strlen
**
sizeof
**是操作符,绝对不是函数!这几种写法都是对的
int a=10; int b=sizeof(a); int c=sizeof a; int d=sizeof(int);
👆也能证明
sizeof
不是函数计算操作数所占内存的大小,单位是字节
不关注内存中存放什么数据
注意
sizeof
中就算放表达式,也不会去计算,因为在编译时就已经确定了表达式的结果的类型,就直接获得值了,而表达式的计算是在程序运行时才进行的,这个时候已经编译完了,外面的表达式还能算,但sizeof
里面就不算了
**
strlen
**是库函数,使用需要包含头文件string.h
求的是字符串长度,统计的是
\0
之前的字符个数关注内存中是否有
\0
,如果没有,就会持续往后找,可能会越界