指针
借鉴大佬 https://blog.csdn.net/weixin_44966641/article/details/120456141
字节
计算机内部数据的最小单位为位(bit),用二进制数0或1表示。如果把8根电线并排在一起并行传输数据,一次可以同时传输8种电信号状态,即8位二进制数,称为一个字节(byte),用大写字母B表示。
换算:
1 B=8 bits
1 KB=1024 B
1 MB=1024 KB
1 GB =1024 MB
指针
在内存中,一个字节(8位)有一个地址。当我们在程序中声明一个变量时,,系统会为这个变量分配内存空间。常见的int(4B),char(1B),float(4B)等。其他数据类型可通过`
sizeof
查看。
指针是一个变量,它存放的是另一个变量的地址。因为指针的值是一个内存地址,所以指针所占据的内存空间的大小与其指向的数据类型无关,而与当前机器类型所能寻址的位数有关。具体来说,32位机器上一个指针的大小为4字节。
声明与引用
声明:使用*
来声明一个指向某种数据类型的指针:int *p
,表示声明一个指针变量p,它指向一个整型变量。即p是可以存放整型变量的地址的变量。
取地址:使用&
表示取地址运算符:p=&a
,得到的是一个变量的地址,并把这个地址赋给p。
解引用:一个指针变量所指向的地址存放的值,即通过指针得到值。使用*p
表示,这里的*
称为解引用,即把p所指向的对象的值读出来,甚至还可以直接修改值。
p.s. 引用:在C中没有而在C++中存在。就是起别名。实质上还是同一个对象。
注意
可以对指针进行算术运算。比如,对指针执行加1,应该得到的是下一个整型数据的地址,即在地址的数值上面应该加4。但如果直接对地址进行操作,可能会造成危险,C/C++并不会为我们访问的地址进行检查,因此我们可能通过指针访问到一块未分配的内存,造成值的错误。
指针的类型
指针是强类型的,即需要特定类型的指针来指向特定类型的变量存放的地址。不仅仅是用指针来存储内存地址,同时也使用它来解引用这些内存地址的内容。而不同的数据类型在所占的内存大小是不一样的,更关键的是,不同的数据类型在存储信息的方式上也是不同的(如整型和浮点型)。
void*
是通用指针类型,不针对某个特定的指针类型,在使用时将其赋值为指向某种特定的数据类型的指针时不需要做强制类型转换。因为不知道它指向的类型,因此不能直接对其进行解引用,也不能对其进行算术运算。
指向指针的指针
比如把整型变量x的地址存入变量p中,p的值表示x的地址,再把p的地址存储到一个变量q中,这个变量就是一个指向指针的指针,即int**
。
int x;
int* p;
int** q;
int*** r;
p = &x;
*p = 6;
q= &p;
r= &q;
printf("%d\n",*p) //6
printf("%d\n",*q) //-1672706964
printf("%d\n",**q) //6
printf("%d\n",**r) //-1672706964
printf("%d\n",***r) //6
*p
即对指针p的解引用,应该是 x 存储的值,即6。
*q
是对指向指针的指针q的解引用,即其指向的地址 p 所存储的值235。同时,这个值就是指针 p 的值,指向整型变量 x 的地址。
**q
是对*q
的解引用,我们已经知道*q
为235,则**q
即地址为235的位置的值,是6。
**r
是对*r
的解引用,而*r
就是q,所以**r
就是*q
,235。
***r
是对**r
的解引用,同样是235指向的值,6。
所以如果除了初始化指针表明该变量是个指针时,*
表示声明。其他时候,出现*
第一个都表示解引用,剩下的*
表示声明。
值传递和引用传递
执行一个C语言程序时,该程序将拥有内存四区。
堆区(heap):动态内存申请与释放,若不手动释放,程序结束由操作系统回收。
栈区(stack):编译器自动分配释放,存放参数的形参、局部变量等,当函数执行完毕自动释放。
全局区(global/static):存放全局变量和静态变量,程序结束后由操作系统释放。
代码区(code/text):粗放囊程序代码,字符串常量也存放在此。
void Func(int a)//值传递,对a的操作不影响main函数里的a
void Func(int* a)//引用传递,对a的操作等于对main函数里的a的操作,传进来的变量应为a的地址即&a
数组
在数组中,指针的算术运算就很有意义了,因为数组申请的是一整块连续的内存空间,我们可以通过这种偏移量的方式去访问它们。不能改变数组名称指向的地址,即数组名称可视为一个指向数组首元素地址的指针常量。
数组名就是指向数组首元素的指针。数组的首元素的地址,也被称为数组的基地址。
int A[5];
int* p;
p=A
printf("%d\n", p); // 200
printf("%d\n", *p); // 2
printf("%d\n", p+1); // 204
printf("%d\n", *(p+1));// 4
对于第 i ii 个元素:
- 取地址:
&A[i]
or(A+i)
- 取值:
A[i]
or*(A+i)
获得数组元素个数:sizeof(A)/sizeof(A[0])
。
数组作为函数参数时是传引用,需要传递的只有一个指针的大小。
在C语言中,通常以字符数组的形式存储字符串。对一个有n个字符组成的字符串,需要长度至少为n+1的字符数组。因为需要符号\0
来标志字符串的结束。
char C[10] = "Join"
,字符串会存储在分配给这个数组的内存空间中,这种情况下会被分配在栈区。
而当使用char*
的形式声明一个字符串时,比如char* C = "Join"
,它是一个字符串常量,通常会被存放在代码区。值不能更改。
const
修饰指针表示该指针是一个常量指针,通常用在引用传参时,为了避免在函数体内部对数据进行写操作,破坏元数据,可以在参数前加上关键词const
,使其成为一个常量指针,保证其指向的值不会被误操作修改。该指针非指针常量,其指向可以改变。
多维数组
二维数组:B[i][j] == *(B[i]+j) == *(*(B+i)+j)
三维数组:C[i][j][k] == *(C[i][j]+k) == *(*(C[i]+j)+k) == *(*(*(C+i)+j)+k)
注意:多维数组做函数参数时,数组的第一个维度可以省略,但是其他维度必须指定。
二维数组做参数:
void func(int (*A)[3]
void func(int A[][3])
动态内存分配
malloc
函数从堆区上找到一块给定大小的空闲的内存,并将指向起始地址的void *
指针返回给程序,程序员应当根据需要做适当的指针数据类型的转换。向堆上写值的唯一方法就是使用解引用,因为malloc
返回的总是一个指针。如果malloc
无法在堆区上找到足够大小的空闲内存,则会返回NULL
。
程序员用malloc
在堆上申请的内存空间不会被程序自动释放,因此程序员在堆上申请内存后,一定要记得自己手动free
释放。
free
接收一个指向堆区某地址的指针作为参数,并将对应的堆区的内存空间释放。
int* p;
p = (int*) malloc(sizeof(int));
*p = 10;
free(p);
在C++中,不需要做指针数据类型的转换,new
和delete
是类型安全的。它们是带类型的,返回特定类型的指针。且C++兼容C,因此C的内存分配方法也适用。
p = new int;
*p = 10;
delete p;
p = new int[20];
delete[] p; //delete时需要有[]
malloc
void* malloc(size_t size)
。函数接收一个参数size,返回的void*
指针指向了分配给我们的内存块中的第一个字节的地址。
void*
类型的指针只能指明地址值,但是无法用于解引用,所以通常我们需要对返回的指针做强制类型转换,转换成我们需要的指针类型。
通常我们不显式地给出参数size的值,而是通过sizeof
,再乘上我们需要的元素个数,计算出我们需要的内存空间的大小。
calloc
void* calloc(size_t num, size_t size)
。函数接收两个参数num
,size
,分别表示特定类型的元素的数量,和类型的大小。同样返回一个void*
类型的指针。
realloc
void* realloc(void* ptr, size_t size)
。函数接收两个参数,第一个是指向已经分配的内存的起始地址的指针,第二个是要新分配的内存大小。返回void*
指针。可能扩展原来的内存块,也可能另找一块大内存拷贝过去,如果是缩小的话,就会是原地缩小。只要是由原来分配的内存地址不会再被用到,realloc
函数自己会将这些不被用到的地址释放掉。
free
已经没有用的堆区内存如果不进行手动释放会造成内存泄漏。只需将要释放的内存的起始地址传入即可:free(p)
函数返回指针
int* Add(int* a, int* b){
int c = (*a) + (*b)
return &c;//可能会造成错误的运行结果
}
//返回被调函数在栈区的局部变量的指针是危险的**
int* Add(int* a, int* b){
int* c = (int*)malloc(sizeof(int)); //用完必须free
*c = (*a) + (*b)
return c;//正确的
}
函数指针
声明函数指针的语法是:int (*p)(int, int)
,这条语句声明了一个接收两个整型变量作为参数,并且返回一个整型变量的函数指针。注意函数指针可以指向一类函数,即可以说,指针p指向的类型是输入两整型,输出一整型的这一类函数,即所有满足这个签名的函数,都可以赋值给p这个函数指针。
另外,要注意指针要加括号。否则int *p(int, int)
,是声明一个函数名为p
,接收两个整型,并返回一个整型指针的函数。
函数指针的使用:p = &Add; int c = (*p)(2, 3)
,先对p
解引用得到函数Add
,然后正常传参和返回即可。在为函数指针赋值时,可以不用取地址符号&
,仅用函数名同样会返回正确的函数地址。p = Add; int c = p(2,3);
与之匹配的,在调用函数的时候也不需要再解引用。这种用法更加常见。
回调函数
函数指针可以用作函数参数,而接收函数指针作为参数的这个函数,可以回调函数指针所指向的那个函数。
void A(){printf("Hello world!");}
void B(void (*ptr)){ptr();}
int main(){
void (*p)()=A;
B(p);
B(A);//直接写该句也能达到回调效果
return 0;
}
在上面的例程中,将函数A()
的函数指针传给B()
,B()
在函数体内直接通过传入的函数指针调用函数A()
,这个过程称为回调。