计算机的内存是一块用于存储数据的空间,由一系列连续的存储单元组成,就像下面这样,
每一个单元格都表示1个Bit,一个bit在EE专业的同学看来就是高低电位,而在CS同学看来就是0,1两种状态。
由于1个bit只能表示两个状态,所以大佬们规定8个bit为一组,命名位byte。
并且将byte作为内存寻址的最小单元,也就是给每个byte一个编号,这个编号就叫内存的地址。
接下来我们需要考虑,int,double 这些变量是如何存储在0、1单元格的。
在C语言中我们会这样定义变量:
int a = 999 ;
char c = ‘c’ ;
当你写一个变量定义的时候,实际上是向内存申请了一块空间来存放你的变量。
我们都知道int类型占用4个字节,并且在计算机中数字都是用补码表示的。
999换算成补码就是: 0000 0011 1110 0111
这里有4个byte,所以需要四个单元格来存储:
有没有注意到,我们把高位的字节放在了低未知的地方,那能不能反过来呢?
当然,这就引出了大端和小端。
像上面这种将高位字节放在内存低地址的方式叫做大端,反之,将低位字节放在内存低地址的方式就叫做小端。
上面只说明了int类型的变量如何存储在内存,而float、char等类型实际上也是一样的,都需要先转换为补码。
对于多自己的变量类型,还需要按照大端或者小端的格式,依次将字节写入到内存单元。
记住上面这两张图,这就是编程语言中所有变量在内存中的样子,不管是int、char、指针、数组、结构体、对象…都是这样放在内存的。
变量放在哪?通过上面的讲述可知,定义一个变量实际就是向计算机申请了一块内存来存放。
那如果我们向知道变量到底存放在哪,可以通过运算符&来取得变量实际的地址,这个值就是变量所占内存块的起始地址。
ps:实际上这个地址是虚拟地址,并不是真正物理内存上的地址。
我们可以把这个地址打印出来
printf(“%x”,&a);
大概会是这样的一串数字:0x7ffcad3b8f3c
上面说,我们可以通过&符号获取变量的内存地址,那获取之后如何来表示这是一个地址,而不是一个普通的值呢?也就是在C语言中如何表示地址这个概念呢?
对,就是指针,你可以这样 int *pa = &a;
pa中存储的就是变量a的地址,也叫做指向a的指针。
在这里谈几个看起来有点无聊的话题:
1、为什么我们需要指针?直接用变量名不行吗?
当然可以,但是变量名是有局限的。
2、变量名的本质是什么?
是变量地址的符号化,变量名是为了让我们编程时更加方便,对人友好,可计算机不认识什么变量a,它只知道地址和指令。
所以当你去查看C语言汇编后的汇编代码,就会发现变量名消失了,取而代之的是一串串抽象的地址。
你可以认为,编译器会自动维护一个映射,将我们程序中的变量名转换为变量所对应的地址,然后再对这个地址去进行编写。
也就是有这样一个映射表存在,将变量名自动转化为地址:
a | 0x7ffcad3b8f3c
c | 0x7ffcad3b8f2c
h | 0x7ffcad3b8f4c
…
可是我还是不知道指针存在的必要性,那么问题来了,看下面代码:
int func(...) {
...
};
int main() {
int a;
func(...);
};
假设我有一个需求:
要求在func函数里要能够修改main函数里的变量a,这下咋整,在main函数里可以直接通过变量名去读写a所在内存。
但是在func函数里是看不见a的呀。可以通过&取地址符号,将a的地址传递过去:
int func(int address) {
....
};
int main() {
int a;
func(&a);
};
这样在func 里就能获取到a的地址,进行读写了。
理论上这是完全没有问题的,但是问题在于:
编译器该如何区分一个int里存放的到底是int类型的值,还是另外一个变量的地址(即指针)。
这如果完全靠我们编程人员去人脑记忆了,会引入复杂性,并且无法通过编译器检测一些语法错误。
而通过int * 去定义一个指针变量,会非常明确:这就是另外一个int型变量的地址。
编译器也可以通过类型检查来排除一些编译错误。
这就是指针存在的必要性。
实际上任何语言都有这个需求,只不过很多语言为了安全性,给指针戴上一层枷锁,将指针包装成了引用。
指针的本质都是变量的内存首地址,指针是用来存放地址的,地址是唯一标识一块地址空间的。指针的大小在32位平台是4个字节,在64位平台是8个字节。
那么为什么还要有各种类型呢?
比如int指针,float指针,这个类型影响了指针本身存储的信息吗?这个类型会在什么时候发挥作用?
指针变量是用来保存变量地址的一种变量,那该如何定义呢?如何让计算机一眼认出来这一串数字是地址,而不是普通的数字呢
int a = 10;
int* pa = &a; 创建指针变量pa,pa存放整型a的地址
char b = 'h';
char* pb = &b; 创建指针变量pb,pb存放字符型b的地址
指针变量的定义方式是 基本类型 指针变量名 = &变量名;
int类型的指针存放int类型变量的地址
char*类型的指针存放char类型变量的地址
pa和pb的指针类型不同("int *和char * "),这是定义指针时决定的,那么这两种类型的指针还有什么本质上的区别吗?
1、指针类型决定了指针变量访问空间的能力
2、指针类型决定了指针的步长,即指针变量加或减一个整数,指针向后或向前移动的字节数。
具体可参考:添加链接描述
&变量名表示取变量的地址,就是获取变量的指针
int a = 123;
int* p = &a; //取变量a的地址赋值给指针变量p
这个是指针运算符(或称"间接访问"运算符)
*指针变量表示取指向的变量的值
int a = 123;
int* p = &a; //取变量a的地址赋值给指针变量p
printf("%d",*p); //输出123,*p表示取a的值
先看下面代码
#include <stdio.h>
int main(void){
int *p;
*p = 10;
return 0;
}
这里我们首先定义了一个int类型的指针p,当然,这个p里面保存的是个地址,且这个地址所对应的存储空间只能存int数据,所以叫做int类型的指针。
注意
我们是没有个这个p初始化的,所以,开始p里面存的可能是任何一个数,也就是任何一个地址,这个地址,我们有可能有权限访问,也有可能没有权限访问,这个是我们无法控制的。
然后,将这个地址上存的内容取出来*p,再对这个内容进行修改,这样能成功吗?大概率是不能成功的。
因为我们没有初始化p,所以,如果p中开始给随机分配了一个我们没有权限访问的地址,然后,后面我们还要对这个位置的内容进行修改,那就肯定会报段错误。
所以,指针,一定要记得初始化。
这种,没有初始化的指针,就可以称为:野指针
注意:指针定义的关键:1、最好定义的时候就对指针进行赋值 2、int类型的指针只能存int类型的变量的地址,char类型的指针只能存char类型的地址,要对应起来
如何规避野指针。请参考添加链接描述 该片博客里面由详细介绍。我也是参考这里。
一维数组,二维数组,字符数组的定义,引用初始化的相关介绍请参考:添加链接描述
指针与数组氛围两个方面:
一、指向数组元素的指针
二、指针数组(数组元素是指针)
在C语言中,数组名就是这个数组下标为0的数组元素地址。这个地址也是这个数组的地址。
所以数组给指针赋值和数组元素给指针赋值很不一样。因为,指针只能存地址。数组名本身就保存的地址,就直接可以给指针变量赋值。数组的元素保存的就是具体的内容了,可能是int也可能是char,所以数组元素赋值的时候,就要先用取地址符&得到这个元素的地址,然后再给指针变量赋值。
int c[10], d[20];
int *p, *q=&c[0];
p = &c[3];//数组c下标为3的元素赋值给指针P
p = d;//整个数组的地址赋值给指针p
众所周知,数组是存在内存中一片连续的区域中。即,数组在逻辑上是一个元素挨着一个元素,在物理存储上也是一个元素挨着一个元素的。
所以当指针变量指向数组元素时,指针变量加/减一个整数n,表示指针向后/前移动n个元素
在这里要先了解一下C语言中的运算符优先级。
这里还要重点区分两个概念:一个是指针数组,一个是数组指针。先来看两个例子。
int i = 1;
int j = 2;
int k = 3;
int l = 4;
int *p[4] = {&i, &j, &k, Kl};//单目运算符,同优先级的情况下,丛右往左进行运算
//[]先与p结合,表示这是一个数组
//*再与p[]结合,表示这个数组元素类型是指针
//最后加上前面的int,表示这个int类型的指针
int (*p)[5];
//这里*先与p结合,表示p是一个指针,这个指针指向这个数组,这个数组是int类型的数组
int *p[5];//这个表示指针数组,每个元素都是指针,上面说过了
指针数组:是先数组后指针,本质是一个数组,数组中每一个元素都是指针。格式:数据类型 * 指针数组名[下标]
数组指针:是先指针后数组,本质是一个指针,用来指向二维数组的,也叫做行指针。多用于二维数组作为函数的参数传递时. 格式:数据类型(*数组指针名)[列宽]
int s[3][4] = {{1,2,3,4},
{5,6,7,8},
{9,10,11,12}}; //定义了一个 3行4列的二维数组
int (*p)[4] = s; //定义了个数组指针 并让这个指针指向二维数组s
指向二维数组的指针保存的是二维数组的首地址,p是一个变量。
这样我们就可以得到–>
在C语言中是没有字符串类型的,C语言中的字符串都是用字符数组进行存储的,字符串的滋镇就是字符数组的指针,也就是字符数组得首地址
C语言中字符串的两种定义形式:
二级指针是用来保存一级指针的地址的。
多用于一级指针的地址作为函数的参数传递的。
指针函数:
函数的返回值的类型既可以是整型(若没有设定,则默认为整型),实型,字符型,也可以是指针型。返回值为指针类型的函数又称为指针型的函数,则为指针函数。指针函数的本质是一个函数,返回值是一个指针类型。
格式:
返回值类型 * 函数指针名(函数形参表);
注意:
1、不能返回局部变量的地址。(局部变量的地址在函数结束时会被系统回收)
2、可以返回全局变量的地址
3、可以返回static关键字修饰的局部变量的地址
4、可以返回传递给函数的参数的地址。(函数形参表中传入的地址)
例子:
int *my_func(int x,int y){
int sum = x + y;
return ∑ //错误的,sum是局部变量 在函数结束时会被回收
}
int *my_func(int x, int y,int *sum){
*sum = x + y;
return sum; //正确的
}
C语言的每个函数在编译的时候,都分配了一段连续的内存空间和一个入口地址,这个入口地址就称为"指向函数的指针"
即函数指针。可以用一个变量来存储函数指针,这个变量就称为"指向函数的指针变量"或者函数指针变量。通过函数指针变量就可以调用所指向的函数,改变它的值就可以动态调用不同的函数。函数指针的本质是一个指针,可以指向一个函数
格式:
返回值类型(*函数指针名)(函数的形参表);
例子:
#include <stdio.h>
int my_add(int x, int y){
return x+y;
}
int main(int argc, const char *argv[])
{
int a = 10;
int b = 20;
printf("%d\n", my_add(a, b));//30
int (*p)(int, int) = NULL;
//定义了一个函数指针 指针名叫p 可以指向一个 返回值为int
//形参列表为(int, int)类型的函数
p = my_add; //让函数指针p指向函数my_add
//函数名就是函数的首地址
//指针指向函数之后 就可以通过指针来调用函数了
printf("%d\n", p(a, b));//30
return 0;
}
注意:函数指针变量定义时,并不指向哪一个具体的函数,而是指向空指针。
那如何通过函数指针变量调用函数呢?
那就是让函数指针变量p指向一个具体的函数,即将某个函数的入口地址赋值给这个指针变量。
那一般函数的入口地址在哪里呢?
函数名就代表函数的入口地址,是函数指针类型的符号常量。(这里和数组有点类似,数组名也是数组的地址。)
//比如现在有个函数,函数名叫abc,就可以对函数指针变量p赋值了
p = abc;
那赋值给函数指针变量p以后,如何使用p来调用这个函数呢?
还记得我们的取值运算符吗?
本来调用函数应该是这个的
函数名();
那现在函数名的位置用函数指针变量名来代替就可以啦
剩下的就和普通函数的使用方式基本一样啦。
举个例子
这是linux多线程编程中,创建线程的函数,这里看他的倒数第二个参数,根据上面的知识点返回值是指针的函数和指向函数的指针,来分析一下这个参数要传入的是什么?
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
//倒数第二个参数
//void * (函数名)(void*) 这个函数返回值是一个指针,函数的参数是void *
//void (*指针名)(void*) 这是一个指针,这个指针指向一个函数,函数的返回值为void,参数为void*
//void *(*指针名)(void*)
//这是一个指针,这个指针指向一个函数,函数的返回值为void*,函数的参数为void
//所以,对于这个参数,我们要先定义一个函数,这个函数的返回值是void*
//这个函数的参数是void*。然后,将这个函数的函数名传入进来。
函数指针的经典使用场景---->回调函数
例子:
#include <stdio.h>
int my_add(int x, int y){ //函数my_add
return x+y;
}
int my_sub(int x, int y){ //函数mu_sub
return x-y;
}
int jisuan(int x, int y, int (*p)(int, int)){ //第三个参数就是一个函数指针
return p(x, y); //调用函数指针p指向的函数(回调)
//通过传递不同的函数实现不同的功能
}
int main(int argc, const char *argv[])
{
int a = 10;
int b = 20;
printf("a+b = %d\n", jisuan(a, b, my_add));//调用函数my_add
printf("a+b = %d\n", jisuan(a, b, my_sub));//调用函数my_sub
return 0;
}
本质是一个数组,数组中每个元素都是一个函数指针。
格式:
返回值类型(*函数指针名下标)
例子:
#include <stdio.h>
int my_add(int x, int y){
return x+y;
}
int my_sub(int x, int y){
return x-y;
}
int main(int argc, const char *argv[])
{
int (*s[2])(int, int) = {NULL}; //定义了一个函数指针数组,数组名叫s 数组中共有2个元素
//每个元素都是一个可以指向返回值为int
//形参列表为 (int, int) 的函数指针
s[0] = my_add; //将每个元素指向函数
s[1] = my_sub;
//当函数指针数组的元素指向函数之后 就可以通过他调用函数了
int a = 10;
int b = 20;
printf("a+b = %d\n", s[0](a, b));//通过函数指针数组中的元素调用函数
printf("a-b = %d\n", s[1](a, b));
return 0;
}
本质是一个指针,指向一个函数指针数组
格式:
返回值类型(*(*函数指针数组指针名))(函数的形参表)
例子:
#include <stdio.h>
int my_add(int x, int y){
return x+y;
}
int my_sub(int x, int y){
return x-y;
}
int main(int argc, const char *argv[])
{
int (*s[2])(int, int) = {my_add, my_sub};//定义了一个函数指针数组,
//数组名叫s 数组中共有2个元素
//每个元素都是一个可以指向返回值为int
//形参列表为 (int, int) 的函数指针
//s[0]指向函数my_add;s[1]指向函数my_sub
int a = 10;
int b = 20;
printf("a+b = %d\n", s[0](a, b));//30
printf("a-b = %d\n", s[1](a, b));//-10
int (*(*p))(int, int) = NULL; //定义了一个 函数指针数组指针
p = s; //让函 数指针数组指针 p 保存 函数指针数组 的首地址
printf("a+b = %d\n", p[0](a, b));//通过指针就可以访问函数指针数组的元素了
printf("a-b = %d\n", (*(p+1))(a, b));
return 0;
}