内存和地址
我们知道,每一台电脑都有内存,内存有8G,16G,32G等等。
计算机的CPU(中央处理器)在处理数据时,需要将数据从内存中读取出来,然后再将处理后的数据放回到内存中。
计算机中内存被划分为一个个内存单元,每一个内存单元占用一个字节。
下面是计算机中经常见到的计算单位:
bit-----比特位
Byte-----字节
KB-----千字节
MB-----兆
GB-----吉咖字节
TB
PB
它们之间的换算为:
1 Byte = 8 bit
1 KB = 1024 Byte
1 MB = 1024 KB
1 GB = 1024 MB
1 TB = 1024 GB
CPU在访问内存中每个字节的空间时,必须其所在空间的内存单元编号,我们可以称之为地址,在C语言中被称为"指针".
- 了解指针,必须先要了解地址,我们现在所使用的机器都是64位机器,我们假设在32位机器上,32位机器有32个地址总线,每根线有俩种形态,表示0或者1,表示电脉冲的有无.那么我们就可以知道,一根线可以表示俩种可能,俩根线就有四种可能,在32位机器上就有2^32种可能.
当地址信息被被下达给内存,就可以通过地址信息找到对应内存中的数据,然后数据会通过数据总线传入CPU寄存器.
了解指针
在计算机科学中,指针是编程语言的一个对象,通过地址可以直接指向存在电脑存储器中另一个地方的值.
- 总的来讲,指针就是变量,用来存放内存单元的地址.
- &(取地址操作符),可以查找到所保存的数据的地址.
- 通过打印的方式,找到存放a的地址
这里要注意的是,每一次运行的时候,内存都会开辟不同的空间来存放数据,所以内存也会不同.
- 我们可以将变量a的地址存在一个指针变量里面,而此时p的类型为int*,*说明了p是一个指针变量,可以存放一个地址,而前面的int说明了p指向的是一个整型类型的变量a.
- 此时,我们使用*(解引用操作符),*p的意思是通过p的存放的地址,找到指向的空间,将所指向的空间里的值改为10.
在这里,*p等价于a,可以理解为p是a的地址
此时,我们大概了解了指针是什么,那么这个指针变量是否占用空间呢?
- 我们应该清楚,每个数据的地址是不会占用空间的,而当一个指针变量将地址保存起来,这个指针变量就会占用一定空间.
int main(void) { printf("%zd\n", sizeof(int*)); printf("%zd\n", sizeof(char*)); printf("%zd\n", sizeof(float*)); printf("%zd\n", sizeof(double*)); return 0; }
从这里我们可以知道,在x86(即32位机器)环境下,指针变量的大小只有4个字节.
- 之前我们讲过在32位机器下,假设有32个地址总线,每根地址总线的电信号转换为数字信号,只有0和1俩种情况,将32根地址线产生的二进制当作一个地址,那么一个地址在存储的时候就会占用32个bit位,也就是4个Byte(字节).
- 同样,在x64(即64位机器)环境下,个地址在存储的时候就会占用64个bit位,也就是8个Byte(字节).
指针类型
之前我们所了解,在定义一个变量的时候,变量前面的type(类型)是用来决定这个变量所占内存大小的.
int main(void) { int a = 0; char b = 'B'; float c = 1.5; return 0; }
而在了解到指针变量所占的内存空间只有4或8个字节的时候,我们所定义的指针类型是否没有意义.
- 首先,我们先了解指针的类型
int main(void) { int* pi = NULL; char* pc = NULL; short* ps = NULL; long* pl = NULL; long long* pll = NULL; float* pf = NULL; double* pd = NULL; return 0; }
- 指针定义方式是type *+name.
而在这里,char * 是为了存储char变量类型的地址,int * 是为了存储int变量类型的地址,short * 是为了存储short变量类型的地址, long * 是为了存储long变量类型的地址,long long * 是为了存储long long变量类型的地址,float * 是为了存储float变量类型的地址,double * 是为了存储double变量类型的地址.
int main(void) { int a = 0X11223344; int* pa = &a; *pa = 0; return 0; }
- 使用指针改变int的变量的值,由于int是4个字节,所以*pa改变了四个字节的数据.
int main(void) { int a = 0X11223344; char* pc = (char* )&a; *pc = 0; return 0; }
- 当我们将一个int类型的数据强制类型转换为char*指针类型时,*pc改变的值为char类型的长度.
由此我们可以得出结论:
指针的类型决定了,对指针解引用操作的权限的大小.
char* 可以访问1个字节
int* 可以访问4个字节
short* 可以访问2个字节
…
const修饰指针
int main(void) { int a = 10; a = 20; //打印 printf("%d\n",a); return 0; }
通常来讲,在使用一个类型定义变量的时候,这个变量都是可以被修改的.
int main(void) { const int b = 10; b = 30; return 0; }
当我们在类型前面定义const,可以限制变量,使得变量不能被修改.
int main(void) { const int c = 10; int* pc = &c; *pc = 20; //打印 printf("%d\n",c); return 0; }
但是我们使用指针变量将变量的地址存储起来,解引用指针变量的值却可以改变变量的值.
int main(void) { int d = 10; int f = 20; const int* p = &d; *p = 30; p = &f; return 0; }
当const在类型左边时,指针指向的内容不能通过指针改变,但是指针本身的内容可以改变.(换一种说法,就是指针指向的数据不能改变,但是存在在指针变量里的地址可以改变)
int main(void) { int d = 10; int f = 20; int const * p = &d; *p = 30; p = &f; return 0; }
当const在类型和*中间时,和const在类型左边时的情况相同,指针指向的内容不能通过指针改变,但是指针本身的内容可以改变.(换一种说法,就是指针指向的数据不能改变,但是存在在指针变量里的地址可以改变)
int main(void) { int d = 10; int f = 20; int* const p = &d; *p = 30; p = &f; return 0; }
当const在*的右边时,此时指针指向的内容可以被改变,但是指针本身的内容不可以被改变.(换言之,指针指向的数据可以被改变,但是指针变量中存储的地址不能被改变)
int main(void) { int d = 10; int f = 20; int const * const p = &d; *p = 30; p = &f; return 0; }
当*俩边都放const时,即结合了上面的俩种情况,指针指向的内容不能被改变,指针本身的内容也不能被改变.
从上面的例子中,我们可以总结除:
1.const放在 * 左边时,修饰的时指针指向的内容,保证指针指向的内容不能通过指针改变,但是指针变量本身的内容可以被改变.
2.const放在 * 右边时,修饰的时指针变量本身,保证指针变量本身的内容不能修改,但是指针指向的内容可以通过指针所改变.
3.const放在 * 左右俩侧时,可以同时修饰指针指向的内容和指针变量本身.
指针的运算
指针与整数之间的运算
int main(void) { int arr[5] = { 1,2,3,4,5 }; int* pa = arr;//arr等价于&arr[0] int i = 0; for (i = 0; i < 5; i++) { printf("%d ",*pa); pa = pa + 1;//pa++; } return 0; }
指针在通过+或者-整数时,可以跳过一个指针变量类型的字节(例如,int跳过4个字节,char跳过1个字节等等)
指针与指针之间的运算
int main(void) { int arr[5] = { 1,2,3,4,5 }; //将p1指向3 int* p1 = arr; p1 += 3; //p2指向1 int* p2 = arr; //打印p1到p2的距离 printf("%d\n", p1 - p2); return 0; }
指针与指针之间±可以计算出指针之间的距离.
值得注意的是:指针与指针之间的计算一般只能在数组中进行.
标准规定:允许指向元素的指针,与指向数组元素最后一个元素后面的那个内存位置的指针进行比较,但是不允许与指向数组元素第一个元素前面的那个内存位置的指针进行比较.
指针的关系运算
int main(void) { int arr[5] = { 1,2,3,4,5 }; //将p1指向3 int* p1 = &arr[0]; p1 += 3; //p2指向1 int* p2 = arr; //比较俩个指针变量 if (p1 > p2) { printf("Yes\n"); } return 0; }
指针与指针之间进行比较,比较的是指针变量中的地址.而且同时我们可以发现,数组是由高地址到低地址以此排序的.
void* 指针
指针的类型决定了指针访问内存的字节大小,那么在指针类型中还存在着一种特别的指针,空指针,即void* 指针.
int main(void) { int a = 10; void* pc= &a; return 0; }
void* 类型的指针,可以理解为无具体类型的指针,也可以称为泛型指针.这类指针可以用来接收任何类型的指针.
- void* 类型的指针也有局限性,在这里,说明了void* 类型的指针不可以进行解引用操作.
- 同时,void* 类型的指不可以用于指针的运算.
传值调用和传址调用
int add(int x, int y) { return x + y; } int main(void) { int a = 3; int b = 5; //传值调用 int ret =add(a, b); //打印 printf("%d", ret); return 0; }
传值调用,是将变量中的数据直接传递给函数使用,过程中不会改变变量中的值,仅仅只是使用了变量中的值.
int add(int* px, int* py) { return *px + *py; } int main(void) { int a = 3; int b = 5; //传值调用 int ret = add(&a, &b); //打印 printf("%d", ret); return 0; }
传址调用是将变量的地址传递给函数,然后函数根据地址找到变量,在变量内部进行计算
这里需要提醒大家,传值和传址是俩个完全不同的看待角度的问题,传值调用仅仅只是将数值给函数使用,函数不管怎么用都不会改变变量变量本身.而传址调用则是将变量的地址提供给了函数,函数找到变量,在变量的内存中进行改变.
可以看看下面的例子:
int change(int x) { x = 10; return x; } int main(void) { int a = 5; //传值调用 int ret = change(a); //打印ret的值 printf("%d\n",ret); //打印a的值 printf("%d\n",a); return 0; }
- 传值调用
int change(int* px) { *px = 10; return *px; } int main(void) { int a = 5; //传值调用 int ret = change(&a); //打印ret的值 printf("%d\n", ret); //打印a的值 printf("%d\n", a); return 0; }
- 传址调用
数组和指针的关系
int main(void) { int arr[5] = { 1,2,3,4,5 }; //打印数组名的地址 printf("%p\n",arr); //打印数组第一个元素的地址 printf("%p\n",&arr[0]); return 0; }
当我们以数组名打印地址时,和以数组首元素打印地址时所打印的地址相同,我们可以任务,数组名即为首元素的地址.
一篇文章带你深入了解“指针”(下)https://developer.aliyun.com/article/1583525?spm=a2c6h.13148508.setting.26.197b4f0eDwuZrP